Bjarne Stroustrup 的 C++ 风格与技术 FAQ


Bjarne Stroustrup 的 C++ 风格与技术 FAQ(中文版) http://www2.research.att.com/~bs/bs_faq2.html 原作:Bjarne Stroustrup 翻译:Antigloss 译者的话:尽管我已非常用心,力求完美,但受水平所限,错误在所难免,还请各路高手 不吝斧正。邮箱地址:Antigloss at 163 dot com。本译文是对以前紫云英的译文的补充,之 前他们翻译过的内容我没有重译,故亦没有出现于本页面,想看的朋友可以自行搜索一下。 目 录  开始: o 您可以推荐一种编写代码的标准吗?  类: o C++ 的对象在内存中的存放形式是怎么样的? o 为什么“this”不是引用? o 为什么(对象)退出作用域时没有调用析构函数? o “友元”违反了封装吗? o 为什么我的构造函数不太对劲?  类继承体系: o 什么是纯虚函数? o 为何 C++ 没有 final 关键字 o 为什么 C++ 没有通用类对象?  模板与泛型编程: o 为何 vector 不能赋值给 vector? o 模板(templates)本应被设计为“泛型(generics)”那样吗? o 为何 C++ 不提供多态的容器? o 为何标准容器效率如此低下?  内存: o “new”和“malloc()”的不同点何在? o 数组有何不好之处?  异常: o 如何使用异常? o 可以在构造函数里抛出异常吗?析构函数里呢?  其它语言特性: o C++ 中如何调用 C 函数? o C 中如何调用 C++ 函数? o 为何 C++ 既有指针也有引用? o 我应该使用 NULL 还是 0? o i++ + i++ 的值是多少? o 为何 C++ 里有些东西是未定义的呢? o static_cast 有什么好处?  琐事及风格: o “cout”怎么念? o “char”怎么念? o 你如何命名变量?是否推荐匈牙利命名法? o 我应该使用按值传递还是按引用传递? 您可以推荐一种编写代码的标准吗? C++ 代码编写标准的要点是:根据使用 C++ 的具体的环境和具体目的制定一套规则。因此, 没有哪一种代码编写标准是符合所有需要和所有用户的。对于一个特定的应用程序(或者 公司、应用领域,等等)来说,一种好的代码编写标准当然比没有标准要好得多。话说回 来,我看到过很多例子表明一种差劲的代码编写标准比没有标准还要更糟糕。 选择规则时,请切记细心,而且你必须对该应用领域有过硬的知识。一些最差劲的代码编 写标准(“为了保护罪犯”,我不会提及这些名字)的作者既没有过硬的 C++ 知识,而且对 其应用领域也相对无知(他们是“专家”,而非开发人员),更误以为约束总是多比少好。 针对前面这种误解的一个反例是:某些特性的存在会导致程序员不得不使用甚至更糟糕的 特性。怎么都好,请牢记,安全性、生产率等是设计和开发过程的所有部分的总和 ——而 非各种语言特性的总和,更不是所有语言的总和。 基于以上原因,我的推荐有三: o 看 Sutter 和 Alexandrescu 合著的《C++ 代码编写标准(C++ Coding Standards)》。Addison-Wesley 出版,ISBN 0-321-11358-。这本书里有很多 好的规则,但请把这些规则看作一套元规则(meta-rules)。更明确地说, 就是把这本书当作一本关于 “一套优秀的代码编写规则应该是怎么样的”的 指南。如果你正在写代码编写标准,不看这本书将是一大损失。 o 看 JSF 航空器 C++ 代码编写标准。我认为这是一套关于编写对安全性和性能 要求苛刻的代码的优秀规则。如果你从事嵌入式系统编程,那你应该考虑 看一下这份标准。告诫:我参与了这些规则的制定,所以你可以认为我带 有偏心。话说回来,请将你对这份标准的建设性意见发给我。这些意见可 能会推动这份标准的改进——所有优秀的标准都会定期地被重新审视,并 且根据经验和工作环境的变化而更新。如果你不是在构建苛刻的实时系统 或者对安全性要求苛刻的系统,那你会觉得这些规则过于严格——毕竟这 些规则并非为你而设(至少并非所有这些规则都是为你而设的)。 o 不要使用 C 语言代码编写标准(即使已将其修改为 C++ 版),也不要使用 10 年前的 C++ 代码编写标准(即使在那时来说是很优秀的标准)。C++ 并 非(仅仅)是 C,而标准 C++ 也并非(仅仅)是标准前的 C++。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#coding-standard C++ 的对象在内存中的存放形式是怎么样的? 和 C 一样,C++ 也没有定义对象在内存中的存放形式,而仅仅定义了一些必须遵循的语义 约束。因此,不同的编译器实现起来都有所不同。不幸的是,我知道的最好的解释出自于 一本过时的书,而且这本书并没有描述任何当前的 C++ 实作——《带评注的 C++ 参考手册》 (The Annotated C++ Reference Manual,常简称为 ARM)。该书有一些存放形式的图例。 TC++PL 第二章也有一个简短的解释。 基本上,C++ 简单地通过连接各个子对象来构建对象。例如: struct A { int a,b; }; 在内存中的表现就是两个 int 型变量彼此相邻。又如: struct B : A { int c; }; 在内存中的表现是类型为 A 的对象和 int 型变量彼此相邻,c 跟在 A 型对象的后面;也就是 说,a 和 b 彼此相邻,b 和 c 彼此相邻。 虚函数通常是通过在含有虚函数的类的每个对象中加入一个指针(vptr)来实现的。这个 指针指向一个相应的函数表(vtbl)。每个类都有其独特的 vtbl,所有属于同一个类的对 象共享同一个 vtbl。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#layout-obj 为什么 “this” 不是引用? 因为“this”被引入 C++(事实上那时还是带类的 C)的时候,还没有引用(reference)这个 概念。同样地,我遵循 Simula 的用法而选择了“this”这个词,而不是(后来)Smalltalk 的 “self”。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#this 为什么(对象)退出作用域时没有调用析构函数? 最简单的答案是“肯定会调用!”,不过还是先来看看一个经常伴随着这个问题的例子吧: void f() { X* p = new X; // use p } 也就是说,有些人误以为 new 创建的对象会在函数的最后被析构。 基本上,只有当你希望一个对象能“生存”于其被创建的域之外时,才应该使用“ new”。若 然如此,你就需要使用“delete”来析构该对象。例如: X* g(int i) { /* ... */ return new X(i); } // the X outlives the call of g() void h(int i) { X* p = g(i); // ... delete p; } 如果你希望一个对象只能“生存”于一个域中,那就不要使用“new”,而应该单纯地定义一 个变量: { ClassName x; // use x } 变量在退出作用域时会被隐式析构。 在同一个域中使用 new 创建对象,然后使用 delete 来将之析构不但难看,而且容易出错, 更是效率低下。例如: void fct() // ugly, error-prone, and inefficient { X* p = new X; // use p delete p; } 原文地址:http://www.research.att.com/~bs/bs_faq2.html#delete-scope “ 友元”违反了封装吗? 不,并非如此。和成员函数类似,“友元”是一种显式地授予访问权限的机制。你不能(于 一个符合标准的程序)在不修改源代码的情况下授予你访问类的权限。例如: class X { int i; public: void m(); // grant X::m() access friend void f(X&); // grant f(X&) access // ... }; void X::m() { i++; /* X::m() can access X::i */ } void f(X& x) { x.i++; /* f(X&) can access X::i */ } 想了解 C++ 的(数据)保护模型,可参考 D&E 章节 2.10 以及 TC++PL 章节 11.5、15.3,以 及 C.11。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#friend 为什么我的构造函数不太对劲? 类似这样的问题千奇百怪。例如: o 为什么我明明不想复制对象,而编译器却偏偏这么做了呢? o 如何关闭复制机制? o 如何防止隐式转换? o 为何 int 自动转换成了复数? 类的默认复制构造函数和赋值运算符可以复制所有元素。例如: struct Point { int x,y; Point(int xx = 0, int yy = 0) :x(xx), y(yy) { } }; Point p1(1,2); Point p2 = p1; 至此,p2.x==p1.x 并且 p2.y==p1.y。这可能正是你想要的(而且也是为了和 C 兼容所必需 的),但是,以下代码: class Handle { private: string name; X* p; public: Handle(string n) :name(n), p(0) { /* acquire X called "name" and let p point to it */ } ~Handle() { delete p; /* release X called "name" */ } // ... }; void f(const string& hh) { Handle h1(hh); Handle h1 = h2; // 会引起灾难! // ... } 在此,默认复制构造函数使得 h2.name==h1.name 并且 h2.p==h2.p。这将导致一场灾难: 当函数 f() 运行结束时,会调用 h1 和 h2 的析构函数,这就导致 h1.p 和 h2.p 所指向的对象 被 delete 了两次。 如何避免这场灾难?最简单的办法是,将复制构造函数和赋值运算符声明为私有成员,从 而关闭复制机制: class Handle { private: string name; X* p; Handle(const Handle&); // 阻止复制 Handle& operator=(const Handle&); public: Handle(string n) :name(n), p(0) { /* acquire the X called "name" and let p point to it */ } ~Handle() { delete p; /* release X called "name" */ } // ... }; void f(const string& hh) { Handle h1(hh); Handle h1 = h2; // 编译器报错 // ... } 如果需要复制机制,我们可以定义自己的复制构造函数和赋值运算符,让它们按我们期待 的那样工作。 现在回过头来再看看类 Point。对 Point 来说,可以使用默认的复制机制,但它的构造函数 有点问题: struct Point { int x,y; Point(int xx = 0, int yy = 0) :x(xx), y(yy) { } }; void f(Point); void g() { Point orig; // 使用默认值 (0,0) 创建 orig Point p1(2); // 使用 yy 的默认值 (0) 来创建 p1 f(2); // 调用 Point(2,0); } 为了便于创建对象(如这里的 orig 和 p1),我们为 Point 的构造函数提供了默认参数。然 后,有些人会感到惊讶的事情发生了:调用 f() 时,2 会转换成 Point(2,0)。当我们定义一 个接受单个参数的构造函数时,同时亦定义了一种类型转换方式。默认情况下,类型转换 是隐式进行的。若想把类型转换改成显式进行,就要将构造函数声明为 explicit: struct Point { int x,y; explicit Point(int xx = 0, int yy = 0) :x(xx), y(yy) { } }; void f(Point); void g() { Point orig; // 使用默认值 (0,0) 创建 orig Point p1(2); // 使用 yy 的默认值 (0) 来创建 p1 // 显式调用构造函数 f(2); // 错误(试图进行隐式转换) Point p2 = 2; // 错误(试图进行隐式转换) Pont p3 = Point(2); // 正确(显式转换) } 原文地址:http://www.research.att.com/~bs/bs_faq2.html#explicit-ctor 什么是纯虚函数? 纯虚函数是指不必在基类中定义,但必须在派生类中被覆盖(override)的函数。通过新 奇的“=0”语法可将虚函数声明为纯虚函数。例如: class Base { public: void f1(); // 不是虚函数 virtual void f2(); // 是虚函数,但不是纯虚函数 virtual void f3() = 0; // 纯虚函数 }; Base b; // error: pure virtual f3 not overridden 在此,Base 是抽象类(因为它有一个纯虚函数),所以不能直接用它来定义对象:Base (很显然)是用来做基类的。例如: class Derived : public Base { // 没有定义 f1:没关系 // 没有定义 f2:没关系,继承了 Base::f2 void f3(); }; Derived d; // ok: Derived::f3 覆盖了 Base::f3 抽象类是定义接口的非常好的工具。事实上,一个只有纯虚函数的类通常被称为接口。 当然你也可以定义纯虚函数: Base::f3() { /* ... */ } 这样做往往意义不大(虽然这样做可为派生类提供一些简单的公共代码),而且在派生类 中仍然需要覆盖 Base::f3()。 如果你没有在派生类中覆盖纯虚函数,那该派生类也是抽象类: class D2 : public Base { // 没有定义 f1:没关系 // 没有定义 f2:没关系,继承了 Base::f2 // 没有定义 f3:没关系,但 D2 因此也是抽象类 }; D2 d; // 错误:没有覆盖纯虚函数 Base::f3 原文地址:http://www.research.att.com/~bs/bs_faq2.html#pure-virtual 为什么 C++ 没有 final 关键字 因为无论过去,还是现在,都没有这个必要。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#final 为什么 C++ 没有通用类对象(universal class Object)?  我们不需要这个:大多数情况下,泛型编程提供的静态类型安全机制是非常不错的 替代品。其它情况可使用多继承(multiple inheritance)来解决。  不存在有用的通用类:纯粹的通用类本身不含任何语义。  “通用”类会怂恿人们对类型和接口的考虑粗枝大叶,从而导致多余的运行时检查。  使用通用基类意味着额外花销:为了使用多态,对象必须在堆中分配;这就会导致 额外的内存及访问花销。堆对象天生就不支持复制语义(copy semantics)。堆对 象没有作用域的概念(这导致资源管理变得复杂化)。通用基类会怂恿 dynamic_cast 的使用及其它运行时检查。 是的。我简化了论据,毕竟这只是一篇 FAQ,而非学术论文。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#object 为何 vector 不能赋值给 vector? 因为这将降低类型系统的安全性。例如: class Apple : public Fruit { void apple_fct(); /* ... */ }; class Orange : public Fruit { /* ... */ }; // Orange 没有 apple_fct() vector v; // vector of Apples void f(vector& vf) // innocent Fruit manipulating function { vf.push_back(new Orange); // 将 Orange 对象指针加入 vf } void h() { f(v); // 错误:不能传递 vector 给 vector for (int i=0; iapple_fct(); } 如果调用 f(v) 是合法的,我们将得到伪装成 Apple 的 Orange。 当然,也可以把语言设计成允许这种不安全的类型转换,然后依赖动态类型检查保证访问 的合法性。这将导致每次访问 v 的成员时,都要进行运行时检查,而且 h() 也必须在遇到 v 的最后一个元素后抛出异常。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#conversion 模板(templates)本应被设计为“泛型(generics)”那样吗? 非也。generics 其实是为抽象类而设的语法;亦即,利用 generics(无论是 Java generics 或 C# generics),你从此不再需要定义精确的接口,但相对地,你也要为此付出诸如虚函数 调用以及/或者动态类型转换的花销。 Templates 通过其各种特性的组合(整型模板参数(integer template arguments)、特化 (specialization)、同等对待内建/用户定义类型等),可支持泛型编程(generic programming)、模板元编程(template metaprogramming)等。Templates 带来的灵活性、 通用性,以及性能都是“generics”不能比美的。STL 就是最好的例子。 不过,Templates 带来灵便的同时,亦带来了一些不尽人意的后果——错误检查滞后、出 错信息非常糟糕。目前,可通过 constraints classes 间接解决这个问题。C++0x 将引入 concepts 来直接解决这个问题(参考我的论文、提案,以及标准委员会网站的所有提案)。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#generics 为何 C++ 不提供多态的(heterogeneous)容器? C++ 标准库提供了一套非常好用的、静态类型安全的、高效的容器。例如 vector、list,以 及 map: vector vi(10); vector vs; list lst; list l2 map tbl; map< Key,vector > t2; 所有优秀的 C++ 教材都有对这些容器的描述。应该优先使用标准容器,而非数组和“自制 的”容器,除非你有很充分的不使用 STL 的理由。 这些容器都是单态的;亦即,它们的元素是同一类型的。如果你希望某个容器能保存多种 类型的元素,必须使用联合体或者在容器里保存指向多态类型的指针(这个方法通常更 好)。一个经典的例子是: vector vi; // 该 vector 保存指向 Shape 的指针 在此,vi 的元素可以是从 Shape 派生出来的任何类型(的指针)。亦即,既可以说 vi 是单 态的,因为其所有元素都是 Shape(精确地说是指向 Shape 的指针),也可以说它是多态 的,因为它可以保存多种类型的 Shape,例如 Circles、Triangles 等等。 所以,可以说,所有容器(无论任何语言)都是单态的,因为为了使用它们,必须有一个 可供用户用来访问其中所有元素的公共接口。提供多态容器的语言,其实无非是容器里的 元素都提供了一个标准的接口。例如,Java collection 提供的容器保存的是 Object 类型(的 引用),可用(公共的)Object 接口来获取元素的真正类型。 C++ 标准库提供单态的容器,因为大多数情况下,它们常用且易用,并能提供尽可能好的 编译时错误信息,而且没有不必要的运行时开销。 如果你需要在 C++ 中使用多态的容器,可为所有元素定义一个公共的接口,然后即可制出 这样的容器。例如: class Io_obj { /* ... */ }; // 进行 I/O 所需的接口 vector vio; // 如果你想直接管理指针 vector< Handle > v2; // 如果你想用“智能指针”来处理各个对象 如非必要,绝对不要使用最底层的实现细节: vector memory; // 很少用到 辨别你是否“走入底层”的一个很好的办法是,看看你的代码里是否夹杂着显式类型转换。 在某些程序里,也可以使用 Any 类(例如 Boost::Any): vector v; 原文地址:http://www.research.att.com/~bs/bs_faq2.html#containers 为何标准容器效率如此低下? 不,它们的效率并不低下。或许“和什么比较?”会是一个更有用的回答。当人们抱怨标准 库容器的性能时,通常会是以下三个现实问题之一: o 复制开销 o 查表很慢 o 我写的(浸入式)链表比 std::list 要快得多 在优化之前,请先考虑是否真有性能问题。在我收到的大多数案例中,性能问题只是理论 上的或者只存在于想象中:首先仔细思量,除非必要,就不要优化。 让我们一个接一个地来分析这些问题。通常,vector 要慢于某些人专门写的 My_container,因为 My_container 的实现是“一个保存指向 X 的指针的容器”。标准 容器保存值的拷贝,当你将一个值放入容器时,该值是被复制进去的。对小型的值来说, 这是无可挑剔的,但对大型对象来说,这又是非常的不合适的: vector vi; vector vim; // ... int i = 7; Image im("portrait.jpg"); // 使用文件来初始化 Image // ... vi.push_back(i); // 将 i(的一个拷贝)放入 vi vim.push_back(im); // 将 im(的一个拷贝)放入 vim 假若 portrait.jpg 有好几兆那么大,而且 Image 是值语义(value semantics。例如,复制赋 值和复制构造会创建新的拷贝),那么 vim.push_back(im) 的开销无疑是非常大的。但—— 俗话说得好——不要做赔本的买卖。取而代之,你应该使用容器来保存句柄或者指针。例 如,如果 Image 是引用语义(reference semantics),那么上面的代码招致的仅仅是调用复 制构造函数的开销,而且这个开销和大多数图像处理操作相比是微不足道的。如果某些类, 比如 Image,因为一些合适的理由,必须采用复制语义(copy semantics),那么使用容器 来保存其指针通常是个合理的解决方案: vector vi; vector vim; // ... Image im("portrait.jpg"); // 使用文件来初始化 Image // ... vi.push_back(7); // 将 i(的一个拷贝)放入 vi vim.push_back(&im); // 将 &im(的一个拷贝)放入 vim 自然而然,如果你使用指针,就必须考虑资源管理的问题,不过,保存指针的容器本身就 可以是一个有效且低开销的资源处理器(通常,你需要这么一种容器:它带有用于删除 “属于它的”对象的析构函数)。 第二个常见的现实问题是使用 map 来处理数量庞大的 (string,X) pair。map 适用于处理相对 小型的容器(例如好几百或好几千个元素——访问 10000 个元素的 map 中的一个元素需要 大约 9 次比较)。这些相对小型的容器的“小于”比较应该是低开销的,并且不能构建出优 秀的哈希函数。如果你要处理大量字符串,而且也有一个优秀的哈希函数,那么你应该使 用哈希表。标准委员会的技术报告(Tecnical Report)中定义的 unordered_map 目前已经 广泛可用,而且远远胜于大多数人的“私藏佳酿”。 有时,你可以使用 (const char*,X) pair 来代替 (string,X) pair,从而提高程序效率。但切记 < 并不能比较 C 风格的字符串。而且,如果 X 很庞大,你还是有可能遇到复制问题(可选用 一种常用办法来解决)。 浸入式链表当然可以很快。然而,首先你应该考虑一下你是否需要使用链表:vector 更加 紧凑,因此它比链表更小,而且在很多情况下也比链表更快——甚至于进行插入/删除操作 时亦是如此。例如,如果你的 list 只不过拥有为数不多的整型元素,那么使用 vector 无疑 会明显快于 list(无论任何链表)。而且,浸入式链表不能直接保存内建类型(int 没有 link 成员变量)。所以,假设你真的需要使用链表,而且你可以为每种元素类型提供 link 成员变量,才可以使用浸入式链表。每当进行插入元素的操作,标准库 list 默认会进行一 次内存分配,然后将该元素复制到新分配好的空间里(而每当进行删除元素的操作,list 都会进行一次内存回收)。对于使用默认分配器的 std::list 来说,这样做可能会带来很明 显的性能损失。对于复制开销不大的小型元素,可以考虑使用经过优化的分配器。只有当 你需要使用链表并且不能错失哪怕是一盎司的性能提升时,才应该使用自制的浸入式链表。 人们有时会担心 std::vector 的增长开销。我过去也担心这个,并且使用 reserve() 来优化其 增长。在仔细思量代码,并且一次又一次地在实际程序中遇到难以计算 reserve() 所带来的 性能提升这个麻烦之后,我停止了使用 reserve(),除非是为了避免迭代器失效(我的代码 中很少有这种情况)而不得不使用它。重申:优化前请仔细思量。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#slow-containers “new”和“malloc()”的不同点何在? “malloc()”是个函数,接受(字节)数目作为参数;它返回一个指向未初始化空间的 void * 指针。“new”是个运算符,接受一个类型以及一套该类型的初始值(可选)作为参数;它 返回一个指向已被初始化(可选)的该类型的对象的指针。当你想为带有非平凡初始化语 义(non-trivial initialization semantics)的用户自定义类型分配空间时,这两者的区别是很 明显的。例如: class Circle : public Shape { public: Cicle(Point c, int r); // 没有默认构造函数 // ... }; class X { public: X(); // 默认构造函数 // ... }; void f(int n) { void* p1 = malloc(40); // 分配 40 个(未初始化的)字节 int* p2 = new int[10]; // 分配 10 个未初始化的 ints int* p3 = new int(10); // 分配 1 个初始化为 10 的 int int* p4 = new int(); // 分配 1 个初始化为 0 的 int int* p4 = new int; // 分配 1 个未初始化 int Circle* pc1 = new Circle(Point(0,0),10); // 分配一个使用指定参数构造的 Circle Circle* pc2 = new Circle; // 错误:没有默认构造函数 X* px1 = new X; // 分配一个默认构造的 X X* px2 = new X(); // 分配一个默认构造的 X X* px2 = new X[10]; // 分配 10 个默认构造的 X // ... } 注意,当你使用“(值)”来指定初始值时,分配到的内存将被初始化为该指定值。不幸的是, 这种方法对数组无能为力。通常,vector 是动态数组的一个很好的替代品(例如,vector 是异常安全[exception safety]的)。 每当使用 malloc(),你必须考虑初始化问题以及将其返回的指针转换为合适的类型。你也 不得不考虑你是否已经分配了足够的空间。当你把初始化算进去后,malloc() 和 new 的性 能差异就是零。 malloc() 通过返回 0 来表示内存耗尽;而 new 通过抛出异常来报告内存分配和初始化错误。 使用 new 创建的对象都要用 delete 来销毁。使用 malloc() 分配的内存空间都要用 free() 来 释放。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#malloc 数组有何不好之处? 从时间和空间的角度来讲,数组是访问内存中连续对象的最佳结构。然而,它同时也是非 常底层的数据结构,不当地使用它常常会导致大量潜在的错误。而且,基本上在所有需要 用到数组的地方,我们都有更好的替代品。我所说的“更好”是指更易于读写、不易导致错 误,以及同等效率。 和数组如影随形的两个基本问题是: o 数组不知道其自身的长度 o 稍有风吹草动,数组的名字就会转换成指向其首元素的指针 思考以下一些例子: void f(int a[], int s) { // 处理 a;a 的长度是 s for (int i = 0; i < s; ++i) a[i] = i; } int arr1[20]; int arr2[10]; void g() { f(arr1,20); f(arr2,20); } 第二个函数调用会玷污不属于 arr2 的内存。通常,程序员都不会传递错误的长度给函数 f, 但传递参数是个额外的负担,而且不时都会有些人犯错(传递了错误的长度)。我更喜欢 使用标准库里的 vector,这样写出来的程序更加简单明了: void f(vector< int >& v) { // 处理 v for (int i = 0; i < v.size(); ++i) v[i] = i; } vector< int > v1(20); vector< int > v2(10); void g() { f(v1); f(v2); } 因为数组不知道其自身的长度,所以不能直接进行数组赋值: void f(int a[], int b[], int size) { a = b; // 并非数组赋值 memcpy(a,b,size); // a = b // ... } 同样,我更喜欢使用 vector: void g(vector< int >& a, vector< int >& b, int size) { a = b; // ... } vector 的另一个好处是,memcpy() 不能正确处理带有复制构造函数的元素,例如 string: void f(string a[], string b[], int size) { a = b; // 并非数组赋值 memcpy(a,b,size); // 灾难 // ... } void g(vector< string >& a, vector< string >& b, int size) { a = b; // ... } 数组的大小在编译时就已固定: const int S = 10; void f(int s) { int a1[s]; // 错误 int a2[S]; // ok // 若想增加 a2 的长度,必须改用 malloc() 从堆中分配数组空间, // 然后使用 realloc() 改变分配到的空间的大小 // ... } 作为对比: const int S = 10; void g(int s) { vector< int > v1(s); // ok vector< int > v2(S); // ok v2.resize(v2.size()*2); // ... } C99 允许可变长的局部数组,但 VLA(变长数组,variable-length array)也有其独特的问题。 在 C 和 C++ 中,数组名“退化”为指针的方式是基本常识。然而,数组退化和继承“互动”时, 是非常不妙的。例如: class Base { void fct(); /* ... */ }; class Derived { /* ... */ }; void f(Base* p, int sz) { for (int i=0; i < sz; ++i) p[i].fct(); } Base ab[20]; Derived ad[20]; void g() { f(ab,20); f(ad,20); // 灾难! } 在后一个函数调用里,Derived[] 被认为是 Base[],以至于当 sizeof(Derived)!=sizeof(Base) 时, 余下的代码不再能正常工作。如果我们使用 vector 的话,在编译时就能捕捉到这个错误: void f(vector< Base >& v) { for (int i=0; i < v.size(); ++i) v[i].fct(); } vector< Base > ab(20); vector< Derived > ad(20); void g() { f(ab); f(ad); // 错误:不能将 vector< Derived > 转换成 vector< Base > } 我发现大量 C 和 C++ 初学者的程序错误都和(错误地)使用数组有关。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#arrays 如何使用异常? 请参考 TC++PL 章节 8.3、第十四章,以及附录 E。附录聚焦于如何为“苛刻的”应用程序编 写异常安全(exception-safe)的代码,它并非写给初学者看的。 C++ 里,异常用于发出一种信号,表示发生了“本地”处理不了的错误,比如构造函数里某 个获取资源的操作失败了。例如: class Vector { int sz; int* elem; class Range_error { }; public: Vector(int s) : sz(s) { if (sz < 0) throw Range_error(); /* ... */ } // ... }; 不要简单地将异常当作又一种从函数中返回一个值的方法。大多数用户以为异常处理代码 等同于错误处理代码(因为 C++ 语言的定义怂恿他们这么想),他们的代码优化方式也反 映了这种想当然的“以为”。 一种关键的技术被称之为资源获取即初始化(有时被简称为 RAII),该技术使用带有析构 函数的类来使资源管理有序化。例如: void fct(string s) { File_handle f(s,"r"); // File_handle 的构造函数打开名为 s 的文件 // 使用 f } // File_handle 的析构函数在此关闭文件 就算 fct() 中“使用 f”的那部分代码抛出了异常,析构函数仍然会被执行,所以文件会被正 常关闭。下面这种常见的不安全的用法则恰恰相反: void old_fct(const char* s) { FILE* f = fopen(s,"r"); // 打开名为 s 的文件 // 使用 f fclose(f); // 关闭文件 } 如果 old_fct 中“使用 f”的那部分代码抛出了异常(或者简单地返回了),那么文件就没有 被关闭。在 C 程序里,longjmp() 是又一种危险。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#exceptions 可以在构造函数里抛出异常吗?析构函数里呢?  可以:当你不能正常地初始化(构造)对象时,你应该在构造函数里抛出异常。没 有任何其它方法比抛出异常退出构造函数更合适了。  不然:你可以在析构函数里抛出异常,但这个异常必须不能越过析构函数;如果因 为抛出异常而退出了析构函数,任何糟糕的情况都可能发生,因为这违反了标准库 及 C++ 语言本身的基本规则。不要这么做。 更详细的实例和解释尽在 TC++PL 附录 E。 给你一个忠告:在某些“苛刻的”实时系统项目中,不该使用异常。例如,请参考 JSF 航空 器 C++ 代码标准。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#ctor-exceptions C++ 中如何调用 C 函数? 将 C 函数声明为``extern "C"''(在你的 C++ 代码里做这个声明),然后调用它(在你的 C 或 者 C++ 代码里调用)。例如: // C++ code extern "C" void f(int); // 方法一 extern "C" { // 另一种声明方法 int g(double); double h(); }; void code(int i, double d) { f(i); int ii = g(d); double dd = h(); // ... } 函数的定义可类似如下所示: /* C code: */ void f(int i) { /* ... */ } int g(double d) { /* ... */ } double h() { /* ... */ } 注意,声明里使用的可是 C++ 的类型规则,而不是 C 的哦。所以调用声明为 ``extern "C"'' 的函数时,传递的参数个数必须正确。例如: // C++ code void more_code(int i, double d) { double dd = h(i,d); // 错误:不速之参数 // ... } 原文地址:http://www.research.att.com/~bs/bs_faq2.html#callC C 中如何调用 C++ 函数? 将 C++ 函数声明为``extern "C"''(在你的 C++ 代码里做这个声明),然后调用它(在你的 C 或者 C++ 代码里调用)。例如: // C++ code: extern "C" void f(int); void f(int i) { // ... } 然后,你可以这样使用 f(): /* C code: */ void f(int); void cc(int i) { f(i); /* ... */ } 当然,这招只适用于非成员函数。如果你想要在 C 里调用成员函数(包括虚函数),则需 要提供一个简单的包装(wrapper)。例如: // C++ code: class C { // ... virtual double f(int); }; extern "C" double call_C_f(C* p, int i) // wrapper function { return p->f(i); } 然后,你就可以这样调用 C::f(): /* C code: */ double call_C_f(struct C* p, int i); void ccc(struct C* p, int i) { double d = call_C_f(p,i); /* ... */ } 如果你想在 C 里调用重载函数,则必须提供不同名字的包装,这样才能被 C 代码调用。例 如: // C++ code: void f(int); void f(double); extern "C" void f_i(int i) { f(i); } extern "C" void f_d(double d) { f(d); } 然后,你可以这样使用每个重载的 f(): /* C code: */ void f_i(int); void f_d(double); void cccc(int i,double d) { f_i(i); f_d(d); /* ... */ } 注意,这些技巧也适用于在 C 里调用 C++ 类库,即使你不能(或者不想)修改 C++ 头文件。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#callCpp 为何 C++ 既有指针也有引用? C++ 的指针继承于 C,若要移除指针,势必造成严重的兼容性问题。引用有几方面的用处, 但我在 C++ 中引入它的主要目的是为了支持运算符重载。例如: void f1(const complex* x, const complex* y) // 没有引用 { complex z = *x+*y; // 难看 // ... } void f2(const complex& x, const complex& y) // 使用引用 { complex z = x+y; // 看起来不错 // ... } 更一般地,如果你想要同时拥有指针功能和引用功能,那就需要两种不同的类型(C++ 里 就是这么干的)或者对一个单独的类型有两套不同的操作。例如,如果采用单一类型的话, 则需要有给被引用的对象赋值的操作以及给引用/指针赋值的操作。这可通过使用不同的 运算符来完成(Simula 里就是这么干的)。例如: Ref r :- new My_type; r := 7; // 赋值给对象 r :- new My_type; // 赋值给引用 或者,你也可以依赖类型检测系统(重载)。例如: Ref r = new My_type; r = 7; // assign to object r = new My_type; // assign to reference 原文地址:http://www.research.att.com/~bs/bs_faq2.html#pointers-and-references 我应该使用 NULL 还是 0? C++ 里,NULL 的定义就是 0,所以到底使用哪个只是个审美问题。我个人倾向于避免使用 宏,所以我使用 0。还有个问题是,有些人误以为 NULL 和 0 并不相同,并且/或者以为 NULL 不是整数。在标准前的代码里,NULL 有时被定义成不恰当的东西,因此不得不避免 使用它。不过现今这已不常见。如果你必须给空指针起个名字,那就叫它 nullptr;C++0x 中将会这么叫。届时,“nullptr”将会是个关键字。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#null i++ + i++ 的值是多少? 未定义。基本上,无论 C 还是 C++,如果你在同一个表达式中两次读取同一个变量,并且 还对该变量进行写操作,那么结果就是未定义的。不要这么干。还有个例子是: v[i] = i++; 相关的例子: f(v[i],i++); 在此,因为函数参数的求值顺序是未定义的,所以结果也是未定义的。 之所以不定义求值顺序,是为了让编译器有更大的自由度去生成性能更高的代码。编译器 应该为类似这些例子发出警告,因为这些都是典型的微小错误(或者说是潜在的微小错 误)。很遗憾,尽管数十年的工夫过去了,大多数编译器仍然不会为此发出警告,而将这 项工作交给了专门的、独立的、并且鲜有人用的工具。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#evaluation-order 为何 C++ 里有些东西是未定义的? 因为机器的不同以及 C 里面也有很多未定义的东西。ISO C++ 标准里有以下术语的详细定 义:“未定义”、“未指明(unspecified)”、“由实现定义”,以及“合乎语法的(well- formed)”。注意,这些术语的含义和 ISO C 标准里的定义不太相同,而且也和它们常见的 用法不同。假若没有察觉到不同的人对这些术语的认识会有所偏差,讨论问题的时候常常 会极度混乱。 这是一个正确的答案,虽然可能不尽人意。和 C 一样,C++ 力图榨干硬件的每一滴血。这 就是说,C++ 必须使用各种特定机器的“自然”方式来和硬件实体(位、字节、字、地址、 整数计算,以及浮点数计算等)打交道,而不是我们想怎么搞就怎么搞。注意,很多被人 们称为“未定义”的“东西”,事实上都是“由实现定义”的,所以只要了解我们正在使用的机 器,就可以编写出完美的专门代码。整数的大小以及浮点数的取整行为正是如此。 下面这个关于未定义行为的例子可能是最广为人知且臭名昭彰的: int a[10]; a[100] = 0; // 范围错误 int* p = a; // ... p[100] = 0; // 范围错误(除非赋值之前,p 已经指向了另一段足够大的内存空间) C++(和 C)中数组和指针的概念是对机器中内存和地址概念的直接表述,所以没有任何 额外开销。指针的基本操作直接被映射成机器指令,不会进行范围检测。进行范围检测会 影响运行时效率以及生成代码的大小。C 是被设计来编写操作系统的,要和汇编代码拼速 度,所以这么决定(不检测范围)是必须的。同样,和 C++ 不同的是,即使编译器生成了 检测错误的代码,C 也没有报告错误的合适的方法:C 没有异常。C++ 跟随 C 是为了与之兼 容以及直接和汇编竞赛(在 OS、嵌入式系统以及数值计算领域)。如果你需要范围检测, 可用一个合适的带检测的类(vector、智能指针、string 等)。好的编译器可在编译时捕捉 到 a[100] 越界了,然而,要判定 p[100] 是否越界就要困难得多。一般来说,在编译时是不 可能捕捉到所有范围错误的。 其它关于未定义行为的例子起源于编译模型。编译器不能检测到各个单独的编译单元里, 对象或者函数的定义是否不一致。例如: // file1.c: struct S { int x,y; }; int f(struct S* p) { return p->x; } // file2.c: struct S { int y,x; } int main() { struct S s; s.x = 1; int x = f(&s); // x!=ss.x !! return 2; } 在 C 和 C++ 里,编译 file1.c 和 file2.c 后,将它们链接成为同一个程序是非法的。链接器应 该能捕捉到 S 的定义不一致,但它没有必须这么做的义务(大多数编译器都不捕捉)。很 多情况下,很难捕捉各个单独的编译单元之间的不一致性。确保使用头文件的一致性有助 于最大限度地减少这种问题。链接器也有正在不断改善的好兆头。注意,C++ 链接器捕捉 几乎所有和函数声明不一致有关的错误。 最后,我们来看一些非常恼人的表达式的未定义行为(很明显,应该对这些行为进行定 义)。例如: void out1() { cout << 1; } void out2() { cout << 2; } int main() { int i = 10; int j = ++i + i++; // j 的值未定义 f(out1(),out2()); // 输出 12 或者 21 } j 的值是未定义的,这是为了允许编译器生成最优化的代码。据称,和确保“平常地从左到 右进行求值”相比,让编译器拥有求值顺序的自由这种做法能生成明显高效的多的代码。 我不这么认为,但目前无数的编译器都利用了这种自由,而且有不少人热烈地为这种自由 呐喊,所以要改变它并非易事,而且可能需要数十年的时间才能被整个 C 和 C++ 世界的人 接受。我很失望,并非所有编译器都能为类似 ++i+i++ 这样的代码发出警告。类似地,参 数的求值顺序也是未指明的。 我觉得,未定义、未指明或者由实现定义等等的“东西”实在是太多了。然而,这说起来容 易,甚至也很容易给出这样的例子,但是要修正却太难了。不过,避免这些问题从而编写 出可移植的代码也并非什么难事。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#undefined static_cast 有什么好处? 总的来说,应该尽量避免类型转换(dynamic_cast 除外)。使用类型转换常常会引起类型 错误或者数值截断。甚至于看起来“无辜”的类型转换也有可能变成很严重的问题,如果在 开发或者维护期间,其中一个相关的类型改变了的话。例如,下面这个是什么意思: x = (T)y; 我们不得而知。这取决于类型 T 以及 x 和 y 的类型。T 可能是类的名字、typedef 或者模板 参数。可能 x 和 y 都是标量变量,而 (T) 代表值的转换。也可能 x 是 y 的派生类的对象,而 (T) 是一个向下转换(downcast)。还可能 x 和 y 是不相关类型的指针。由于 C 风格的类型 转换 (T) 可用于表述很多逻辑上不同的操作,所以编译器很难捕捉误用。同样的道理,程 序员不可能精确地知道类型转换到底做了什么。有些菜鸟程序员认为这是一个有利条件, 但假若他们错误地判断了形势,将会导致许多细微的错误。 “新风格的类型转换”因此应运而生,它给予了程序员更清晰地表达他们的真实意图的机会, 也使得编译器能捕捉到更多错误。例如: int a = 7; double* p1 = (double*) &a; // ok(但指向的并非 double 类型的对象) double* p2 = static_cast(&a); // 错误 double* p2 = reinterpret_cast(&a); // ok:我真的想这么干 const int c = 7; int* q1 = &c; // 错误 int* q2 = (int*)&c; // ok(但 *q2=2; 仍然是不合法的代码,而且有可能失败) int* q3 = static_cast(&c); // 错误:static_cast 不能去除 const 属性 int* q4 = const_cast(&c); // 我的确想这么干 static_cast 所允许的转换都比需要使用 reinterpret_cast 才能进行的转换更安全,更不易出 错。大体上,可以直接使用 static_cast 转换后的值,而无需将其再转换成原来的类型。而 由 reinterpret_cast 得到的值却总是应该被转换成原来的类型后才使用,这样才能确保可移 植性。 引入新风格类型转换的第二个原因是,C 风格的类型转换在程序中难以被发现。例如,在 普通的编辑器或者文字处理软件里,你不能方便地查找类型转换。C 风格类型转换的这一 隐秘性实在是糟透了,因为类型转换潜在着极其高的破坏性。丑陋的操作应该使用丑陋的 语法形式。这个事实也是选择新风格类型转换语法的部分依据。更深一层的原因是,让新 风格的类型转换语法和模板语法一致,这样程序员就能编写自己的类型转换,尤其是带运 行时检查的类型转换。 或许,因为 static_cast 很难看,而且也相对难拼,所以你更可能会充分考虑后才决定是否 使用它?这很好,因为现代 C++ 里,类型转换真的是最容易避免的。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#static-cast “cout”怎么念? “cout”读作“斯—奥”。“c”代表“character(字符)”,因为 iostream 将值和字节(char)形式 相映射。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#cout “char”怎么念? “char”通常念作“嚓”,而非“咔”。这看起来不太符合逻辑,因为“character”念作“咔啦克特”, 但也从来没有人就逻辑问题非议过英文发音 (pronunciation,并不写作“pronounciation” :-) 和拼写。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#char 你如何命名变量?是否推荐匈牙利命名法? 不,我并不推荐“匈牙利命名法”。我认为“匈牙利命名法”(在变量名中嵌入类型的缩写) 是一种对隐式类型语言来说行之有效的技巧,但对支持泛型编程和面向对象编程(这两种 编程范型都是基于类型和参数来选择合适的操作)的语言来说,它却是完全不合适的。在 这种情况下,“把对象的类型用作名字的一部分”不仅复杂化了抽象,更限制了抽象的程度。 在不同程度上,我对各种将语言技术细节信息(例如:作用域、存储类型、语法类别)嵌 入(变量)名字的方案都持有保留态度。我同意在某些情况下,将类型提示嵌入变量名会 很有帮助,但大多数情况下,特别是随着软件的发展,这会导致维护危机,甚至会严重损 害优秀的代码。像躲避瘟疫一般地远离它吧。 因此,我不喜欢根据类型命名变量;我喜欢并推荐什么?根据功能命名变量(函数、类型 等等)。选择有意义的名字;亦即,选择有利于别人读懂你的程序的名字。甚至你自己往 往也会难以理解你的程序到底是要干嘛用的,如果你在程序中胡乱使用“易于拼写”的名字, 例如 x1、x2、s3、p7 等等。缩写词和首字母缩写词很容易混淆视听,所以应该“省点儿”用 这种词。首字母缩写词更是应该尽可能地避免。比如 mtbf、TLA、myw、RTFM、NBV 等等。 此时此刻,它们的含义可能显而易见。但几个月过后,任谁也不敢担保一定不会忘掉其中 任何一个(的含义)。 短小的名字,例如 x 和 i,如果按传统习惯来用的话,是有意义的;亦即,x 只被用作局部 变量或者参数,而 i 用作循环计数器。 不要使用过长的名字;它们难以拼写,并使代码行变得很长,以致不能完全显示于屏幕上, 而且也不易于阅读。下面这些变量名看起来不错: partial_sum element_count staple_partition 这两个就太长了点: the_number_of_elements remaining_free_slots_in_symbol_table 我更喜欢使用下划线来分隔标识符(例如 element_count)里的单词,而非替换使用大小 写,例如 elementCount 和 ElementCount。名字里的字母绝对不要全部都用大写(例如 BEGIN_TRANSACTION),因为全部大写习惯上是用于命名宏的。即使你不用宏,但其他人 也许会在他们的头文件中引用你的头文件。命名类型时,最好大写首字母(例如 Square 和 Graph)。C++ 语言和标准库都不使用大写字母,因此 int 非 Int,string 非 String。这样, 你就能很容易地辨认出哪些是标准类型,哪些是你定义的类型。 避免使用易于拼错、看错或混淆的名字。例如: name names nameS foo f00 fl f1 fI fi 字符 0、o、O、1、l 以及 I 特别容易引起问题。 通常,命名习惯的选择仅受限于局部的风格规则。切记,保持风格的一致性常常比使用你 认为最好的方式处理各种小细节更为重要。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#Hungarian 我应该使用按值传递还是按引用传递? 这取决于你到底想达到什么目的: o 如果你想改变被传递的对象,那就按引用传递或者使用指针;例如 void f(X&); 或者 void f(X*); o 如果你并不想改变被传递的对象,但该对象很大,那就按常量引用传递; 例如 void f(const X&); o 其它情况则应该按值传递;例如 void f(X); 我所说的“大”为何解?任何超过两个字长的对象。 我为何会想改变参数的值?呃,通常我们不得不这样做,但通常我们也可用另一种方法: 产生一个新的值。例如: void incr1(int& x); // increment int incr2(int x); // increment int v = 2; incr1(v); // v becomes 3 v = incr2(v); // v becomes 4 我认为对于代码的阅读者来说,incr2() 更易于理解。亦即,incr1() 更易于导致误解和错误。 因此,只要创建和复制一个新的值的开销并不“昂贵”,我更喜欢返回新值这种风格,而非 那种修改值的风格。 我的确想修改参数的值,那么我应该使用指针还是引用?对此我并没有雄厚的符合逻辑的 理由。如果传递“非对象”(例如空指针)是可接受的,那么使用指针是个不错的选择。我 的个人风格是,当我想要改变对象的时候,我会使用指针,因为某些情况下,这样更易于 看出是否潜在对象被修改的可能性。 注意,调用成员函数本质上就是对对象进行按引用传递,所以,当我们想要改变对象的值 /状态的时候,往往会使用成员函数。 原文地址:http://www.research.att.com/~bs/bs_faq2.html#call-by-reference
还剩30页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

wangyunfeibaby

贡献于2012-05-24

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf