前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。
Effective C - Accustoming Yourself to C构造函数的explicit对象的复制命名习惯TR1和Boost视C为一个语言联邦尽量以const enum inline替换define尽量使用const确定对象被使用前已先被初始化
被声明为explicit
的构造函数通常比non-explicit
更受欢迎,因为它们禁止编译器执行非预期的类型转换。除非有一个好理由允许构造函数被用于隐式类型转换,否则把它声明为explicit
。
copy
构造函数被用来“以同型对象初始化自我对象”,copy assignment
操作符被用来“从另一个同型对象中拷贝其值到自我对象”。
copy构造和copy赋值的区别:如果一个新对象被定义,一定会有一个构造函数被调用,不可能调用赋值操作。如果没有新对象被定义,就不会有构造函数被调用,那么就是赋值操作被调用。
构造函数和析构函数分别使用缩写ctor
和dtor
代替。 使用lhs
(left-hand side)和rhs
(right-hand side)表示参数名称。
TR1
(Technical Report 1)是一份规范,描述加入C++标准程序库的诸多新机能。这些机能以新的class templates
和function templates
形式体现。所有TR1
组件都被置于命名空间tr1
内。 Boost
是个组织,亦是一个网站,提供可移植,源代码开放的C++程序库。大多数TR1
机能是以Boost
的工作为基础。
今天的C++已经是个多重范型编程语言(multiparadigm PRogramming language),一个同时支持以下特性的语言: * 过程形式(procedural) * 面向对象形式(object-oriented) * 函数形式(functional) * 泛型形式(generic) * 元编程形式(metaprogramming)
为了理解C++,你必须认识其主要的次语言(sublanguage):
C 说到底C++仍是以C为基础。blocks, statements, preprocessor, built-in data types, arrays, pointers等统统来自C。许多时候C++对问题的解法其实不过就是较高级的C解法,但是C++提供了C没有的templates, exceptions, overloading(重载)等功能。C语言可以重载吗
// http://www.cplusplus.com/reference/cstdlib/qsort//* qsort example */#include <stdio.h> /* printf */#include <stdlib.h> /* qsort */int values[] = { 40, 10, 100, 90, 20, 25 };int compare (const void * a, const void * b){ return ( *(int*)a - *(int*)b );}void fun(){ printf("fun()/n");}/*$gcc -o overload_test overload_test.c overload_test.c:18:6: error: redefinition of 'fun'void fun(int a) ^overload_test.c:13:6: note: previous definition is herevoid fun() ^1 error generated. */#if 0void fun(int a){ printf("fun(int a)/n");}#endifint main (){ // 测试C语言是否支持overload重载 fun(); // C语言可以通过不同的函数指针来模拟overload重载 int n; qsort (values, 6, sizeof(int), compare); for (n=0; n<6; n++) printf ("%d ",values[n]); return 0;}Object-Oriented C++ 这部分就是C with Classes
所诉求的:
Template C++ 这是C++的泛型编程(generic programming)
部分,也是大多数程序员经验最少的部分。
STL STL
是个template程序库,它对containers
, iterators
, algorithms
以及function objects
的规约有极佳的紧密配合与协调。
宁可以编译器
替换预处理器
。当你做出这样的事情:
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源码之前就被预处理器替换了,于是记号名称有可能没有进入记号表(symbol table)
内,当你运用此常量但获得一个编译错误时可能会带来困惑,因为这个错误信息提到的是1.653而不是ASPECT_RATIO。尤其是如果ASPECT_RATIO被定义在一个非你所写的头文件内,你肯定对1.653来自何处毫无概念。解决的方法是:以一个常量替换上述的宏(#define)
。
好处是:
作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。使用常量可能比使用#define导致较小量的目标代码,因为预处理器盲目地将宏名称进行替换会导致目标代码出现多份1.653,而若改用常量则不会出现。字符串常量,string
对象通常比char*-based
合适。const char* const authorName = "gerry";const std::string authorName("gerry");class专属常量。为了将常量的作用域(scope)限制在class内,你必须让它成为class的一个成员(member)
,另外为了保证此常量至多只有一份实体,必须让它成为一个static成员
。#include<stdio.h>class GamePlayer {public: void set_scores() { for (int i = 0; i != NumTurns; ++i) { scores[i] = i; } } void get_scores() { for (int i = 0; i != NumTurns; ++i) { printf("%d ", scores[i]); } printf("/n"); } static int get_numturns() { //printf("addr GamePlayer::NumTurns[%p]/n", &GamePlayer::NumTurns); return GamePlayer::NumTurns; }private: static const int NumTurns = 5; // 常量声明 int scores[NumTurns]; // 使用该常量};int main(){ printf("GamePlayer::NumTurns[%d]/n", GamePlayer::get_numturns()); GamePlayer player; player.set_scores(); player.get_scores(); GamePlayer player2; printf("player.NumTurns[%d] player2.NumTurns[%d]/n", player.get_numturns(), player2.get_numturns()); return 0;}/*GamePlayer::NumTurns[5]0 1 2 3 4 player.NumTurns[5] player2.NumTurns[5] */然而,上面你所看到的是NumTurns的声明式
,而非定义式
。通常C++要求所使用的任何东西提供一个定义式,但如果它是class专属常量且又是static整数类型,只要不取它们的地址,你可以声明并使用它们而无须提供定义式。
但是,如果你需要取某个class专属常量的地址,或者编译器要求(比如,老编译器)需要看到一个定义式,那么需要另外提供定义式。
#include<stdio.h>class GamePlayer {public: void set_scores() { for (int i = 0; i != NumTurns; ++i) { scores[i] = i; } } void get_scores() { for (int i = 0; i != NumTurns; ++i) { printf("%d ", scores[i]); } printf("/n"); } static int get_numturns() { printf("addr GamePlayer::NumTurns[%p]/n", &GamePlayer::NumTurns); return GamePlayer::NumTurns; }private: static const int NumTurns = 5; // 常量声明 int scores[NumTurns]; // 使用该常量};const int GamePlayer::NumTurns; // NumTurns的定义int main(){ printf("GamePlayer::NumTurns[%d]/n", GamePlayer::get_numturns()); GamePlayer player; player.set_scores(); player.get_scores(); GamePlayer player2; printf("player.NumTurns[%d] player2.NumTurns[%d]/n", player.get_numturns(), player2.get_numturns()); return 0;}/*addr GamePlayer::NumTurns[0x102092f30]GamePlayer::NumTurns[5]0 1 2 3 4 addr GamePlayer::NumTurns[0x102092f30]addr GamePlayer::NumTurns[0x102092f30]player.NumTurns[5] player2.NumTurns[5]*/通过提供定义式,我们就可以获取class专属常量的地址。
注意:
NumTurns的定义式中没有赋值是因为,class常量已在声明时获得了初值,因此定义时不可以再设置初值。 我们无法利用#define
创建一个class专属常量,因为#define并不能限制作用域(scope),一旦宏被定义,它就在其后的编译过程中有效,除非在某处被#undef
。因此,#define
不仅不能用来定义class专属常量,也不能提供任何封装性。 如果想具备作用域,但又不想取地址,可以使用enum
来实现这个约束。
预处理器和宏的陷阱:
宏看起来像函数,但是不会招致函数调用(function call)
带来的额外开销。 糟糕的做法:(有效率,但不安全)
好的做法:(效率和安全同时得到保证)
template<typename T>inline void callWithMax(const T& a, const T& b){ f(a > b ? a : b);}这个template
根据实例化可以产出一整群函数,每个函数都接受两个同类型对象,并以其中较大的调用f。这里不需要在函数本体中为参数加上括号,也不需要操心参数被计算的次数,同时,由于callWithMax是个真正的函数,它遵守作用域和访问规则,因此可以写出一个class内的private inline函数,而对于宏是无法完成的。
请记住:
对于单纯常量,最好以const
对象或enum
替换#define
对于形似函数的宏,最好改用inline函数
替换#define
const
允许你指定一个语义约束,也就是指定一个“不该被改动”的对象,而编译器会强制实施该项约束。
如果关键字const
出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
注意:如果被指物是常量,将关键字const
写在类型之前,和写在类型之后星号之前,这两种写法的意义相同。
STL迭代器系以指针为根据塑模出来,所以迭代器的作用就像个T*
指针。如果你希望迭代器所指的东西不可被改变,则需要使用const_iterator
。
const成员函数
将const
实施于成员函数的目的,是为了确认该成员函数可作用于const
对象身上。这一类成员函数之所以重要,是因为:
const
对象”成为可能,这对编写高效代码是个关键,比如,改善程序效率的一个根本方法是以pass by reference-to-const
方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得的const对象。 注意:两个成员函数如果只是常量性不同,可以被重载(overload)。只有返回值类型不同的两个函数不能重载(functions that differ only in their return type cannot be overloaded)。
#include<stdio.h>#include<iostream>#include<string>class TextBlock {public: TextBlock() { } TextBlock(const char* lhs) { text = lhs; }public: // operator[] for const object const char& operator[] (std::size_t position) const { return text[position]; } // operator[] for non-const object char& operator[] (std::size_t position) { return text[position]; }private: std::string text;};int main(){ TextBlock tb("gerry"); std::cout << tb[0] << std::endl; // 调用non-const TextBlock::operator[] const TextBlock ctb("yang"); // 调用const TextBlock::operator[] std::cout << ctb[0] << std::endl; return 0;}成员函数如果是const
意味着什么?—— bitwise constness或者physical constness
VS logical constness
bitwise const
指的是,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const
,即,const成员函数不可以更改对象内任何non-static成员变量。
注意:许多成员函数虽然不完全具备const
性质,却能通过bitwise
测试。比如,一个更改了”指针所指物”的成员函数,如果只有指针隶属于对象,那么此函数为bitwise const
不会引发编译器异议,但是实际不能算是const
。
下面这段代码,可以通过bitwise
测试,但是实际上改变了对象的值。
logical constness
主张,一个const
成员函数可以修改它所处理的对象的某些bits
,但只有在客户端侦测不出的情况才可以(即,对客户端是透明的,但是实际上对象的某些值允许改变)。正常情况下,由于bitwise const
的约束,const
成员函数内是不允许修改non-static成员变量的,但是通过将一些变量声明为mutable
则可以躲过编译器的bitwise const
约束。
在const
和non-const
成员函数中避免重复
方法是:运用const
成员函数实现出其non-const
孪生兄弟。
不好的做法(因为有重复代码):
// operator[] for const object const char& operator[] (std::size_t position) const { // bounds checking // log access data // verify data integrity // ... return text[position]; } // operator[] for non-const object char& operator[] (std::size_t position) { // bounds checking // log access data // verify data integrity // ... return text[position]; }好的做法(实现operator[]
的机能一次并使用它两次,令其中一个调用另一个):
请记住:
将某些东西声明为const
可帮助编译器侦测出错误用法。const
可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。 编译器强制实施bitwise constness
,但你编写程序时应该使用“概念上的常量性”。 当const
和non-const
成员函数有着实质等价的实现时,令non-const
版本调用const
版本可避免代码重复。 关于“将对象初始化”这事,C++似乎反复无常(对象的初始化动作何时一定发生,何时不一定发生)。针对这种复杂的规则,最佳的处理方法是:永远在使用对象之前先将它初始化。
对于内置类型,必须手工完成初始化;对于内置类型以外的其他类型,初始化责任落在构造函数(constructors)身上,即,确保每一个构造函数都将对象的每一个成员初始化。
构造函数初始化的正确方法是:使用member initialization list(成员初值列)
,而不是在构造函数中的赋值。因为第一种方法的执行效率通常较高(对于大多数类型而言,比起先调用default
构造函数,然后再调用copy assignment
操作符,单只调用一次copy
构造函数是比较高效的。对于内置类型,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化)。
C++有着十分固定的”成员初始化次序”:总是base classes
更早于其derived classes
被初始化。而class的成员变量总是以其声明次序被初始化,而和它们在成员初始值列中的出现次序无关。建议,当你在成员初值列中初始化各个成员时,最好总是和其声明的次序一致。
最后一个问题:不同编译单元内定义的non-local static
对象的初始化顺序是怎么样的?
函数内的static
对象称为local static
对象,其他static对象称为non-local static
对象。
C++对定义于不同编译单元内的non-local static
对象的初始化次序并无明确定义。因此,如果某编译单元内的某个non-local static
对象的初始化动作依赖另一编译单元内的某个non-local static
对象,那么它所用到的这个对象可能尚未被初始化。
针对上面这个问题的解决方法是: 将每个non-local static
对象搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象。即,non-local static
对象被local static
对象替换了。
注意:这些函数内含static对象的事实使它们在多线程系统中带有不确定性。处理这种麻烦的方法是,在程序的单线程启动阶段,手工调用所有reference-returning函数,这可消除与初始化有关的race conditions(竞速形势)
。
请记住
为内置类型对象进行手工初始化,因为C++不保证初始化它们。 构造函数最好使用成员初值列(member initialization list
),而不要在构造函数本体内使用赋值操作(assignment
)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。 为免除跨编译单元的初始化次序问题,请以local static
对象替换non-local static
对象。 下一篇: Effective C++ - Constructors, Destructors, and Assignment Operators
新闻热点
疑难解答