Effective C++ - Accustoming Yourself to C++
2017-02-04 21:49
351 查看
Effective C++ - Accustoming Yourself to C++
前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。Effective C - Accustoming Yourself to C
构造函数的explicit
对象的复制
命名习惯
TR1和Boost
视C为一个语言联邦
1 C
2 Object-Oriented C
3 Template C
4 STL
尽量以const enum inline替换define
尽量使用const
确定对象被使用前已先被初始化
1 构造函数的explicit
被声明为explicit的构造函数通常比
non-explicit更受欢迎,因为它们禁止编译器执行非预期的类型转换。除非有一个好理由允许构造函数被用于隐式类型转换,否则把它声明为
explicit。
class foo { public: explicit foo(int x); };
2 对象的复制
copy构造函数被用来“以同型对象初始化自我对象”,
copy assignment操作符被用来“从另一个同型对象中拷贝其值到自我对象”。
class Widget { public: Widget(); // default构造函数 Widget(const Widget& rhs); // copy构造函数 Widget& operator=(const Widget& rhs); // copy assignment操作符 }; Widget w1; // 调用default构造函数 Widget w2(w1); // 调用copy构造函数 w1 = w2; // 调用copy assignment操作符 Widget w3 = w2; // 调用copy构造函数
copy构造和copy赋值的区别:如果一个新对象被定义,一定会有一个构造函数被调用,不可能调用赋值操作。如果没有新对象被定义,就不会有构造函数被调用,那么就是赋值操作被调用。
3 命名习惯
构造函数和析构函数分别使用缩写ctor和
dtor代替。
使用
lhs(left-hand side)和
rhs(right-hand side)表示参数名称。
4 TR1和Boost
TR1(Technical Report 1)是一份规范,描述加入C++标准程序库的诸多新机能。这些机能以新的
class templates和
function templates形式体现。所有
TR1组件都被置于命名空间
tr1内。
Boost是个组织,亦是一个网站,提供可移植,源代码开放的C++程序库。大多数
TR1机能是以
Boost的工作为基础。
5 视C++为一个语言联邦
今天的C++已经是个多重范型编程语言(multiparadigm programming language),一个同时支持以下特性的语言:* 过程形式(procedural)
* 面向对象形式(object-oriented)
* 函数形式(functional)
* 泛型形式(generic)
* 元编程形式(metaprogramming)
为了理解C++,你必须认识其主要的次语言(sublanguage):
5.1 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 here void fun() ^ 1 error generated. */ #if 0 void fun(int a) { printf("fun(int a)\n"); } #endif int main () { // 测试C语言是否支持overload重载 fun(); // C语言可以通过不同的函数指针来模拟overload重载 int n; qsort (values, 6, sizeof(int), compare); for (n=0; n<6; n++) printf ("%d ",values ); return 0; }
5.2 Object-Oriented C++
这部分就是C with Classes所诉求的:
* classes(包括构造函数和析构函数)
* encapsulation(封装)
* inheritance(继承)
* polymorphism(多态)
* virtual function(虚函数动态绑定)
* etc.
5.3 Template C++
这是C++的泛型编程(generic programming)部分,也是大多数程序员经验最少的部分。
5.4 STL
STL是个template程序库,它对
containers,
iterators,
algorithms以及
function objects的规约有极佳的紧密配合与协调。
6 尽量以const, enum, inline替换#define
宁可以编译器替换
预处理器。当你做出这样的事情:
#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源码之前就被预处理器替换了,于是记号名称有可能没有进入
记号表(symbol table)内,当你运用此常量但获得一个编译错误时可能会带来困惑,因为这个错误信息提到的是1.653而不是ASPECT_RATIO。尤其是如果ASPECT_RATIO被定义在一个非你所写的头文件内,你肯定对1.653来自何处毫无概念。解决的方法是:以一个常量替换上述的
宏(#define)。
const double AspectRatio = 1.653; // 大写名称通常用于宏
好处是:
作为一个语言常量,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来实现这个约束。
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; // 常量声明 enum { NumTurns = 5, // 令NumTurns成为5的一个记号名称 }; int scores[NumTurns]; // 使用该常量 };
预处理器和宏的陷阱:
宏看起来像函数,但是不会招致
函数调用(function call)带来的额外开销。
糟糕的做法:(有效率,但不安全)
// 以a和b的较大值调用f函数 #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
好的做法:(效率和安全同时得到保证)
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
7 尽量使用const
const允许你指定一个语义约束,也就是指定一个“不该被改动”的对象,而编译器会强制实施该项约束。
char greeting[] = "Hello"; char* p = greeting; // non-const pointer, non-const data const char* p = greeting; // non-const pointer, const data char* const p = greeting; // const pointer, non-const data const char* const p = greeting; // const pointer, const data
如果关键字
const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
注意:如果被指物是常量,将关键字
const写在类型之前,和写在类型之后星号之前,这两种写法的意义相同。
void f1(const Widget* pw); void f2(Widget const * pw);
STL迭代器系以指针为根据塑模出来,所以迭代器的作用就像个
T*指针。如果你希望迭代器所指的东西不可被改变,则需要使用
const_iterator。
std::vector<int> vec; const std::vector<int>::iterator iter = vec.begin(); *iter = 10; // ok ++iter; // error std::vector<int>::const_iterator citer = vec.begin(); *citer = 10; // error ++citer; // ok
const成员函数
将
const实施于成员函数的目的,是为了确认该成员函数可作用于
const对象身上。这一类成员函数之所以重要,是因为:
它们使class接口比较容易被理解,可以得知哪个函数可以改动对象内容,而哪个函数不行。
它们使“操作
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 constnessVS
logical constness
bitwise const指的是,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是
const,即,const成员函数不可以更改对象内任何non-static成员变量。
注意:许多成员函数虽然不完全具备
const性质,却能通过
bitwise测试。比如,一个更改了”指针所指物”的成员函数,如果只有指针隶属于对象,那么此函数为
bitwise const不会引发编译器异议,但是实际不能算是
const。
下面这段代码,可以通过
bitwise测试,但是实际上改变了对象的值。
#include<stdio.h> #include<iostream> #include<string> class TextBlock { public: TextBlock() { } TextBlock(char* lhs) { pText = lhs; } public: // operator[] for const object char& operator[] (std::size_t position) const { return pText[position]; } #if 0 // operator[] for non-const object char& operator[] (std::size_t position) { return pText[position]; } #endif private: char* pText; }; int main() { char name[] = "gerry"; const TextBlock ctb(name); std::cout << ctb[0] << std::endl; // 调用const TextBlock::operator[] char* pc = &ctb[0]; *pc = 'J'; std::cout << ctb[0] << std::endl; // 调用const TextBlock::operator[] return 0; }
logical constness主张,一个
const成员函数可以修改它所处理的对象的某些
bits,但只有在客户端侦测不出的情况才可以(即,对客户端是透明的,但是实际上对象的某些值允许改变)。正常情况下,由于
bitwise const的约束,
const成员函数内是不允许修改non-static成员变量的,但是通过将一些变量声明为
mutable则可以躲过编译器的
bitwise const约束。
#include<stdio.h> #include<iostream> #include<string> #include<string.h> class TextBlock { public: TextBlock() : lengthIsValid(false) { } TextBlock(char* lhs) : lengthIsValid(false) { pText = lhs; } public: std::size_t length() const { if (!lengthIsValid) { printf("do strlen... "); textLength = std::strlen(pText); // error? 在const成员函数内不能修改non-static成员变量 lengthIsValid = true; // 同上 } return textLength; } // operator[] for const object char& operator[] (std::size_t position) const { return pText[position]; } #if 0 // operator[] for non-const object char& operator[] (std::size_t position) { return pText[position]; } #endif private: char* pText; mutable std::size_t textLength; // 最近一次计算的文本区域块长度 mutable bool lengthIsValid; // 目前的长度是否有效 }; int main() { char name[] = "gerry"; const TextBlock ctb(name); std::cout << ctb[0] << std::endl; // 调用const TextBlock::operator[] std::cout << "length: " << ctb.length() << std::endl; char* pc = &ctb[0]; *pc = 'J'; std::cout << ctb[0] << std::endl; // 调用const TextBlock::operator[] std::cout << "length: " << ctb.length() << std::endl; return 0; } /* $./mutable g length: do strlen... 5 J length: 5 */
在
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[]的机能一次并使用它两次,令其中一个调用另一个):
#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 { // bounds checking // log access data // verify data integrity // ... std::cout << "const char& operator[]() const\n"; return text[position]; } #if 0 // operator[] for non-const object char& operator[] (std::size_t position) { // bounds checking // log access data // verify data integrity // ... return text[position]; } #endif char& operator[] (std::size_t position) { std::cout << "char& operator[]()\n"; return const_cast<char&>(static_cast<const TextBlock&>(*this)[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; } /* char& operator[]() const char& operator[]() const g const char& operator[]() const y */
请记住:
将某些东西声明为
const可帮助编译器侦测出错误用法。
const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
编译器强制实施
bitwise constness,但你编写程序时应该使用“概念上的常量性”。
当
const和
non-const成员函数有着实质等价的实现时,令
non-const版本调用
const版本可避免代码重复。
8 确定对象被使用前已先被初始化
关于“将对象初始化”这事,C++似乎反复无常(对象的初始化动作何时一定发生,何时不一定发生)。针对这种复杂的规则,最佳的处理方法是:永远在使用对象之前先将它初始化。对于内置类型,必须手工完成初始化;对于内置类型以外的其他类型,初始化责任落在构造函数(constructors)身上,即,确保每一个构造函数都将对象的每一个成员初始化。
构造函数初始化的正确方法是:使用
member initialization list(成员初值列),而不是在构造函数中的赋值。因为第一种方法的执行效率通常较高(对于大多数类型而言,比起先调用
default构造函数,然后再调用
copy assignment操作符,单只调用一次
copy构造函数是比较高效的。对于内置类型,其初始化和赋值的成本相同,但为了一致性最好也通过成员初值列来初始化)。
ABEntry:ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones) : theName(name), // 成员初值列表,这些都是初始化 theAddress(address), thePhones(phones), numTimesConsulted(0) { } ABEntry::ABEntry() : theName(), // 调用theName的`default`构造函数 theAddress(), // 同上 thePhones(), // 同上 numTimesConsulted(0) // 将内置类型int显示初始化为0 { }
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对象替换了。
class FileSystem { ... }; FileSystem& tfs() { static FileSystem fs; return fs; }
注意:这些函数内含static对象的事实使它们在多线程系统中带有不确定性。处理这种麻烦的方法是,在程序的单线程启动阶段,手工调用所有reference-returning函数,这可消除与初始化有关的
race conditions(竞速形势)。
请记住
为内置类型对象进行手工初始化,因为C++不保证初始化它们。
构造函数最好使用成员初值列(
member initialization list),而不要在构造函数本体内使用赋值操作(
assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
为免除跨编译单元的初始化次序问题,请以
local static对象替换
non-local static对象。
下一篇:
Effective C++ - Constructors, Destructors, and Assignment Operators
相关文章推荐
- 《Effective c++》学习笔记--Accustoming Yourself to C++
- 对象函数Effective C++ 读书笔记之Part1.Accustoming Yourself to C++
- Effective C++笔记 【1. Accustoming yourself to C++】
- Conclusion for Accustoming Yourself to C++
- Things to Remember: Accustoming Yourself to C++
- C++ Chapter 1. Accustoming Yourself to C++
- 经典图书评注:Accustoming Yourself to C++
- (Effective C++)第一章 让自己习惯C++(View Yourself to C++)
- How to interpret complex C/C++ declarations : Vikram A Punathambekar
- Introduction to Using the XML DOM from Visual C++
- [总结]How to Shoot Yourself In the Foot
- Introduction to Programming in C/C++ with Vim
- How to learn c++
- How to interpret complex C/C++ declarations
- Data type conversion sheet : C++ to .NET
- How to Compile XviD with Microsoft Visual C++ 6.0(zhuan)
- Beyond the C++ Standard Library: An Introduction to Boost
- Delphi to C++Builder v1.5
- Win32 Resources: Using C++ to Programmatically Retrieve a Global Cursor's Shape and ID
- [收藏]How to improve yourself