当前位置:首页 >> 编程开发 >> Visual C++ >> 内容

工作中的C++:基本原理,重点推荐和结束语

时间:2015/5/19 21:34:28 作者:平凡之路 来源:xuhantao.com 浏览:

目录

基本原理

抛弃托管扩展

未来的乐趣

编程的演变

终极目标

结束语,尽管不是再见

这个月,我要改变常规的问答形式来告诉您关于我在网上发现的一个非常不错的文档。几个星期前,有人写信询问我,为什么他不能在 C++/CLI 中声明 const 函数:

// 引用类
ref class A {
  void f() const; // 不!
};

对此,我回复道:您就是不能,这是规则。公共语言基础结构 (CLI) 是为支持诸如 Visual Basic®、Java 甚至是 COBOL 语言而设计的 - 这些语言甚至不知道 const 的含义。因为 CLI 不知道 const 成员函数为何物,所以您不能使用。

我打完回复后,模糊地记起了一些埋藏在记忆深处的一些东西,关于 const,关于如何处理编译器提示以使其他语言可以忽略这些提示。我查找了以前的专栏,发现曾在 2004 年 7 月回答过一个关于 const 的问题。实际上,C++/CLI 的确允许声明 const 数据成员和参数 - 但不是 const 成员函数。图 1 显示了一个小程序,其具有 const 静态数据成员的引用类。如果编译此程序,然后使用 ILDASM 来反汇编,您将看到类似于下面的信息:

field public static int32
  modopt([mscorlib]System.Runtime.CompilerServices.IsConst)
    g_private = int32(0x00000001)

Figure1const.cpp

////////////////////////////////////////////////////////////////
// 要编译类型:
//  cl /clr const.cpp
//
#include <stdio.h>
ref class A {
  int m_val;
  // 允许使用常量数据成员,将生成 modopt
  static const int g_private = 1;
public:
  // 公共常量成员可由 Visual Basic 或其他不能识别常量的程序
  // 来修改(因而改用字面量)。
  literal int g_public = 1;
  A(int i) { m_val = i; }
  void print(); // const;       // 不!不允许使用常量 fn
};
void A::print()
{
  printf("Val is %d\n",m_val);
}
int main()
{
  A a(17);
  a.print();
}

Modopt(可选的修饰符)是一种对 CLI 使用者宣告的 MSIL 声明符:如果您了解它,很好;如果不了解,可以完全忽略它。相反,modreq(所需的修饰符)表示:如果您不了解它,就不能使用此函数。Volatile 便是 modreq 的一个示例。因为一个 volatile 引用随时都可以被操作系统/硬件(甚至是另一个线程)更改,所以 CLI 使用者最好知道 volatile 的含义,如果想要使用 volatile 对象的话。但 const 是可选的。请注意,尽管托管扩展会将 C++ const 对象转化成 CLI 文字量,C++/CLI 不再这么做了。因此要小心 - 如果声明一个公共 const 数据成员,用像 Visual Basic 这样的语言编写的客户端能够改变它的值。如果您希望 CLI 客户端不能改变该值,就应该声明对象字面量,如在图 1 中所示。但是,成员函数呢 - 为什么 const 成员函数是不允许的?

基本原理

噢,我在查找回复的过程中,偶然发现了一篇很好的文章,称为《A Design Rationale for C++/CLI》(C++/CLI 设计基本原理),出自 Microsoft 的 Herb Sutter 之手。Herb 是 C++/CLI 的架构师之一。您可以从 Herb 的博客上获得我在后面称作“基本原理”的文档(URL 是非常长的字符串 - 只要在 Web 上搜索“C++/CLI Rationale”就会找到)。正如标题表明的那样,“基本原理”解释了 C++/CLI 中所有事物为何以那种方式存在。它回答了从“为什么首先扩展 C++”到我自己关于 const 函数疑问的所有问题。我认为,每个想要了解 C++/CLI 深层原理的人都有必要阅读“基本原理”。由于“基本原理”中有很多重复,因此我想给出一些重点推荐。

就让我们从最重要的问题开始吧,为什么首先扩展 C++?简单明了的回答就是:要使 C++ 成为一等 CLI 公民。Microsoft® .NET Framework 是 Windows® 开发的未来。哦,连 COBOL 都支持 CLI。所以,C++/CLI 旨在于确保 C++ 的不断成功。

但为什么弄乱 C++?为什么要用像 ^ 和 % 这样讨厌的新概念和像 ref、value 和 property 这样的新关键字来扩展语言呢?哦,是因为没有其他办法了。关于此内容,“基本原理”引用的不是别的,正是 Bjarne Stroustrup 写的:“类几乎可以表示我们所需的所有概念。只有库的道路确实不可行时,才应该走语言扩展的道路。”

对于普通的 C++ 代码,把 CLI 作为目标就像是编写一个针对不同处理器的编译器:没有问题。但是,CLI 引入了需要特殊代码生成且不能在 C++ 中表达的新概念。例如,属性需要特殊的元数据。无法用库或模板实现属性。“基本原理”讨论了一些为属性(和其他 CLI 功能)考虑的替代的语法,以及它们被拒绝的原因。

抛弃托管扩展

“基本原理”还解释了为什么 Microsoft 决定抛弃托管扩展。托管扩展为托管对象和本机对象均使用 * 是一个聪明勇敢的尝试,将 C++ 和 CLI 统一,而不改变语言,但这样使引用和本机对象之间的重要区别不明显了。这两种指针看起来一样,但行为不同。例如,析构语义、复制构造函数和构造函数/析构函数中的虚拟调用都根据指针实际指向的对象种类而具有不同的行为。太糟了!正如“基本原理”所述,“不但要隐藏不必要的区别,而且要显露本质的区别,这很重要”,再次引用 Bjarne 说过的:“我试着使重要操作高度可见”。本机和托管类是根本不同的领域,因此最好将区别突出,而不是掩盖。C++/CLI 团队考虑并否定了各种机制,最终决定是 gcnew ^(句柄)和 %(跟踪引用)。像这样区分托管和本机类具有意想不到的附加收益。例如,使用单独的 gcnew 运算符来分配托管对象开启了某一天本机类可以从托管堆分配的可能性 - 反之亦然!

您有没有想过,为什么托管扩展具有那些讨厌的下划线关键字(像 __gc 和 __value)?因为托管扩展严格遵循 C++ 标准,标准提到“如果你确实必须引入新的关键字,就应该将其命名为从双下划线开始!”猜猜会发生什么?Microsoft 引入了 __gc、__value 和其他的关键字后,Redmondtonians 遭到了来自程序员们“出乎意料强烈”的抱怨。是的!全世界的程序员们都联合起来了!只有放弃下划线了。下划线使代码看起来很讨厌,就像某种汇编语言程序或别的什么。所以 C++/CLI 有 ref 和 value,没有下划线。这意味着向 C++ 添加新的关键字,但那又怎么样呢?就像 Bjarne 说的,“我的经验是人们热衷于引入概念的关键字,本身不具有关键字的概念很难讲授。这个作用比人们口头上表达的对新的关键字不喜欢来说更重要,更根深蒂固。”(确实如此。我喜欢 Bjarne 描述编程心理学。)所以 C++/CLI 抛弃了下划线。通过使其成为位置关键字,而不是保留的关键字,它们就不会与可能已经用这些字作为变量或函数名称的程序相冲突了。

好奇的读者可能想知道:在抛弃其下划线的过程中,gc 怎么就变成 ref 了呢?正如“基本原理”解释的那样,关于托管类,重要之处不是它们存在于托管(垃圾收集)堆上。重要之处是它们的引用语义。句柄 (^) 起到类似于引用的作用,而不是指针。明白了吗?读过“基本原理”之后,一切自然明了。

如果显露本质的区别很重要,那么隐藏表面的区别也同样重要。例如,无论对象是本机、引用还是值,所有运算符重载都按照任何一个 C++ 程序员都会期望的方式“正常运行”。如果需要 C++/CLI 掩盖区别的其他例子,请看下面的摘录:

// 引用类 R 作为本地变量
void f()
{
  R r; // 引用类位于栈上?
  r.DoSomething();
  ...
}

在这里,r 看起来像是一个栈对象,但就连 Mopsie 大婶都知道托管类不能在栈上进行物理分配;它们必须从托管堆中分配。那么?编译器可以使这个摘录片断按照期望的方式运行,方法是生成和下面一样的内容:

// 编译器如何才能看到它。
void f()
{
  R^ r = gcnew R; // 在 gc 堆上分配
  try
  {
   r->DoSomething();
   ...
  }
  finally
  {
   delete r;
  }
}

对象不存在于物理栈上,但谁又介意呢?重要的是局部变量语法按照任何一个 C++ 程序员都会期望的方式运行。特别是,r 的析构函数在离开 f 前被调用。而说到析构函数,同理解释了 C++/CLI 恢复决定性析构的原因:这样,析构就遵循了每个 C++ 程序员都熟悉并喜爱的相同语义。好啊!非决定性析构是托管扩展最痛苦的事情之一。C++/CLI 将析构函数映射到 Dispose 并为终结器引入特殊的 ! 语法。直到垃圾收集器抽出时间时,引用对象使用的内存才会被收回,但这没什么大不了。C++ 程序员不怎么在乎一个对象被销毁时收回内存;在乎的是析构函数可以在期望运行时运行。C++ 程序员经常使用构造/析构模式来初始化/释放非内存资源(像文件句柄、数据库锁等)。使用 C++/CLI,对于引用和本机类,熟悉的构造/析构模式就会像您预期的那样运行。Redmondtonians 值得信任,因为他坦白承认托管扩展存在的问题 - 还因为他处理了这些问题。

未来的乐趣

在“基本原理”更吸引人的章节中,有一个称为“Future Unifications”(未来统一化)的章节。它给出了一些关于 C++/CLI 未来走势的引人入胜的提示。例如,本机类当前无法从托管类派生,反之亦然。但可以获得同样的效果,解决办法是:将“基”类作为数据成员,然后编写 passthrough 包装程序,该包装程序只起到调用包含的实例的作用。哦,这个听起来挺老套,那么为什么编译器做不到呢?编译器可以将混合对象作为分成两部分的对象表示,其中一个是包含所有 CLI 部分的 CLI 对象,另一个是包含所有 C++ 部分的 C++ 对象。

在这里,Sutter 报道了一个有趣的轶事:当他第一次向 Bjarne Stroustrup 表明这个混合类的主意时,Bjarne 走向书架“然后打开一本书,其中写道他一贯坚持(不管批评)C++ 不必要求对象在一个单独的内存块中连续排列。”那时,没有人看到非连续对象有任何好处 - 但那时也没有谁预料到 .NET 和 CLI。Bjarne 坚持敞开非连续的大门使得混合对象成为可能。如果在未来的 C++/CLI 版本中看到它们,不要惊讶。而寓意就是:当您正在设计一种期望会永久存在并会以不可预计的方式发展的新的语言或复杂的程序时,请不要做不必要的设想,因为这样会让生活更轻松!

“基本原理”提供了另一则有趣的历史珍闻:Redmondtonians 原来为 C++/CLI 取的内部名称是 MC^2。正如在 M(托管)C(++) 中,有一个致与 Albert Einstein 的帽尖 (^)。但正如 Sutter 所说的,那样“太矫揉造作”。我同意。您真的认为如果它被称作 MC^2,大家就会热烈接受 C++/CLI 吗?在决定将其称为 C++/CLI 方面,架构师再一次沿着 Bjarne 的足迹。Bjarne 说:“我选择了 C++,因为它简短、有很好的释义,而且它不是‘修饰的 C’的形式”。C++/CLI 表明 C++ 在前,而且还谨慎地避免了“修饰的 C++”的形式。

“基本原理”证明了其他的 C++/CLI 扩展(像 property、gcnew、generic 和 const)是有效的,并以很有用的常见问题解答部分结束。有关详细信息,请下载“基本原理”自行阅读。说到 const,回到我最初的问题 - 为什么 C++/CLI 允许 const 数据,但不允许 const 函数呢?简短的回答就是:CLI 不允许您将 modopt/modreq 直接放在函数上(尽管 CLI 实际上确实有办法在元数据中编码此信息,只是未测试而已)。至少现在还不行,“基本原理”阐述得很小心,暗示可能某一天会加上这个功能。

编程的演变

C++/CLI 使 C++ 成为了一等 CLI 公民。此外,如果您阅读了“基本原理”,就会意识到做到这一点只对 C++ 作了最少的改动。而且 C++ 仍然是系统编程最好的语言,因为其相对于任何其他语言,提供了更多对 CLI 的直接访问,还因为您如果想要调用旧的 Win32® API(将会陪伴我们更长的时间,我肯定),仍可以降回到 C。

要理解 C++/CLI 有多么重要以及它代表了什么,就必须考虑我们处于编程演变过程中的什么位置。就让我给您一个简洁、特别而快速的回顾吧。在过去,程序员使用切换开关来编写程序。纸带是一个改进,但每台计算机都有其自身的“机器”语言。随着计算机的发展壮大,程序员不得不为每台新的机器都重新编写其程序。唉,那是非常烦的,因此程序员发明了像 FORTRAN、BASIC 和 C 这样的高级语言,使用了称为“编译器”的东西来将高级语言翻译为针对每台机器的机器指令。图 2 说明了这点。现在,可以编写一次程序,然后针对不同的机器进行编译。太酷了!C 语言成为系统编程的选择,因为它是“最低级的高级语言”,这意味着它在自身和机器之间引入了最少的累赘。现在使用的大多数操作系统都是用 C 语言编写的,在少数对性能要求特别严格或与硬件交互的部分可能采用汇编语言进行编码。

许多年以后,C++ 改进了 C 语言,使其面向对象,哦,而且更有趣。再次引用 Bjarne 的话:“C++ 的设计旨在于使作者和他的朋友不必用汇编语言、C 语言或各种现代高级语言来编程。它的主要目的就是使单个程序员更轻松愉快地编写好的程序。”C++ 很棒,但是高级语言互相之间的通讯不太好。如果您用 C++ 编写了某些代码,那么不能在 BASIC 中使用它 - 反过来也一样,至少非常困难。每种语言都在其自己的世界中运行。对于独立的应用程序来说,这很好,但是随着应用程序变得更加复杂和环境更加分散,共享代码的需求就变得更加迫切。从第一个子程序开始,程序员就在寻求完全封装的可重用组件的终极目标:程序员可通过组装软件小构造块来创建应用程序。那么为什么所有部分都得用相同的语言编写呢?

多年来,形成了各种各样解决互操作组件问题的办法。起初,语言与库一起提供(想想 C 运行库和 printf)。在 Windows 领域内,DLL 提供了延时加载(DLL 中的动态)来节约内存。DLL 也提供了互操作性,因为像 Visual Basic 和 COBOL 这样的语言可以调用 DLL,方法是通过引入指引编译器将正确的 C 链接调用发到 DLL 中的导入语句。但是应用程序和 DLL 之间的链接太紧密、太脆弱而且太容易中断。每个应用程序都要知道 DLL 中每个入口的名称和签名。另外,反向调用(从 DLL 到应用程序)也很讨厌,因为您必须传递函数指针作为回调。因此,程序员发明了 VBX,后来成为了 OCX,再后来成为了 COM。很高兴,COM 与语言无关:它具有“类型库”,所以语言不必在链接时知道函数;它们可以在运行时查询类型库。COM 非常酷,但众所周知,很难编写。(讽刺地是,用 C++ 编写 COM 是最困难的,而 COM 正是使用这种语言构思和实现的!)COM 也有其他问题:它太低级了,而且不处理安全性或内存管理这样的事情。

终极目标

现在是 2007 年,我们拥有 .NET Framework 及其标准子集 CLI。通过在编程语言和机器之间插入一个新的抽象层,CLI 以一种截然不同的方式解决了复用性问题。编译器现在生成的是 MSIL 代码,而不是机器指令,然后 CLI 虚拟机/实时 (JIT) 编译器将代码动态编译为机器代码。虚拟机(VES 或虚拟执行系统,对于那些喜欢缩写词的人)提供了一个位于机器之上的抽象层。虚拟机不是新的。事实上,它们已经存在了很长时间。像 Pascal 和 ZIL(Zork Implementation Language,在 Infocom 内部用于编写游戏,我曾在 Infocom 工作过)这样的语言,通过将高级程序编译为 P 代码(或 Z 代码)来运行,随后由虚拟机进行解释。但是,CLI 提供了一个所有语言都可以使用的公共虚拟机(CLI 中的 C)。CLI 支持像类、属性、继承、反射等的基本概念。VES/VM 提供了像内存管理和安全性这样的功能,因此程序员不必担心缓冲区溢出,或者其它可能为恶意病毒打开大门的程序错误。随着 .NET Framework 和 CLI 越来越受欢迎,像 Visual Basic、Fortran、COBOL 和 C# 这样的高级语言都已变得越来越相似,也与 C++ 越来越相似,因为它们都必须支持基本 CLI 概念 - 类、继承、成员函数及其他基本概念。每种语言仍保留其特性,因此程序员不必完全重新学习以使用 .NET Framework;它们只需要学习一些新的概念。

所以,现在程序员可以用他们所选的任何语言来编写类,其他程序员可以用他们所选的任何语言来使用这些类。任何人可以用任何语言来编写任何组件,只需很少的编程就可以使所有组件无缝地一起工作。它们均受益于安全性、垃圾收集和其他的基础特征(CLI 中的 I)。而且,将来 Redmondtonians 添加新的 CLI 特征后,所有语言也都将从中受益。有了 Windows Vista™ 和 .NET Framework 3.0(具有与 Windows Presentation Foundation、Windows Communication Foundation、Windows Workflow Foundation 和 Windows CardSpace™ 这样的新技术相联系的 10,000 个新类),Windows 本身正被作为 CLI 类重写。可重用的、语言无关并且可互操作的组件的目标似乎已经最终达到了。这代表了巨大的模式转变,而且令人惊讶地以一种相对渐进、进化的方式进行。您应该高兴!编程的确是变容易了。

如果您回首凝视,就会发现 C++ 怎么能不参与到这个“勇敢的新世界”呢?C++/CLI 就加入进来了!没有 C++/CLI,C++ 就会成为不能用于编写 Windows 程序的仅有的现代编程语言,处于孤立的位置。C++ 可能会缓慢消亡,或至少严重地被边缘化。C++/CLI 确保不会发生那样的情况。它保证了热爱 C++ 的程序员(像我)可以在新的时代继续使用它。图 2 说明了从物理机器到虚拟机的模式转换,以及“C++”在何处适合和不适合“/CLI”。

图 2编程是怎样演变的

结束语,尽管不是再见

那么,现在有一条特别的消息。这是最后一次在“工作中的 C++”中写入(键入?)“您忠实的”。我的专栏要退休了。我喜欢把这看成是为了祝愿我长命百岁 - 您知道,就像团队让许多优秀的运动员引退的方式?我的离开并不会影响 Microsoft 或 MSDN® 杂志对 C++ 所做的承诺。更正确的是,它反映了这样一个简单的事实,就是继 164 个专栏(这是我的第 165 个)之后,横跨了几乎 14 年,DiLascia 先生有点累了。而且,这有一个有趣的轶事:执行编辑 Josh Trupin 最近称我为“MSDN 杂志的 Cal Ripken”。哦,我虽不是棒球球迷(我更喜欢足球),但我也知道谁是 Cal Ripken。尽管我很好奇:Cal 到底打了多少场比赛呢?我在 Web 上找到了答案:2,632. 我还找到一个传记,这样写道“大多数专家都认为 Cal 如果能休息一下的话,将会是一个更棒的运动员”。那么,我把它作为了一个启示。但从不害怕,这并不意味着我不会再为 MSDN 杂志写稿了。

在离开之前,我想感谢所有忠实的读者,是他们发来问题、指出错误和疏漏,使我真实可信,并给出善意的赞扬之词,鼓舞了我的自尊心,这些我将永远珍惜。还有一些读者,甚至在发表之前测试解决方案来帮助我写专栏。我还想要感谢在 MSDN 杂志和我一起工作过的所有了不起的同事们,包括以前和现在的,没有特定的顺序:Steve、Josh、Joanne、Eric、Etya、Terry、Laura、Joan(她们俩)、Nancy、Val 和 Milo。我漏下谁了吗?多年来,这些人一直在大力支持我,并给了我广阔的自由空间去写我最想写的内容。我尤其想要感谢 Gretchen Bilson。几年前她离开了,但她是在 1993 年反聘我的人,那时 MSDN 杂志还是《Microsoft Systems Journal》。谢谢,Gretchen!

相关文章
  • 没有相关文章
共有评论 0相关评论
发表我的评论
  • 大名:
  • 内容:
  • 徐汉涛(www.xuhantao.com) © 2024 版权所有 All Rights Reserved.
  • 部分内容来自网络,如有侵权请联系站长尽快处理 站长QQ:965898558(广告及站内业务受理) 网站备案号:蒙ICP备15000590号-1