这是一本非常经典C++书籍,也是我在工作中发现自己C++上还有很多薄弱点的时候经常拿来充电的。这本书内容很多,讲了很多如何高效地使用C++的方法,有些地方自己也没能啃透,读过一遍后很多知识点容易忘记,这次是一条一条地进行温习,之所以会分享出来是觉得对于程序员来说,好记性不如烂笔头,自己多动手往往在真正写程序的时候能够像条件反射一样写出好的代码。慢工出细活,对待技术要有谨慎和敬畏之心。
这本书推荐喜欢C++的人多读,对文中的一些观点可以自己进行实践,因为内容比较多,所以根据章节进行了划分,笔记中会有一些疑问,也是希望大家能够帮我解答,多多交流才能有提高。
导读
术语
声明式(declaration):这是告诉编译器名称和类型,略去了细节。
这几个常见的定义式:
extern int x; //具体的原因可以看扩展知识部分
std::size_t numDigits(int number);
class Widget;
template<typename T>;
class GraphNode;
定义式(definition):提供编译器声明式遗漏的细节(编译器会为变量分配内存)。
int x;
std::size_t numDigits(int number)
{
//your codes...
}
explicit用来阻止被用来执行隐式类型转换(implicit type conversions),它们可以用来进行显式类型转换(explicit type conversions),所以在声明构造函数的时候如果没有隐式类型转换的需求,尽量声明为explicit。(这里有个疑问,很多C++游戏引擎中的构造函数大部分都没有声明为explicit?)
copy构造函数和copy赋值函数的区别相信大家应该很熟悉了,一个小点需要注意,使用“=”语法也是可以用来调用copy构造函数的。
Widget w1; //default构造函数
Widget w2(w1); //copy构造函数
w1 = w2; //copy assignment操作符
Widget w3 = w2; //copy构造函数
扩展知识
(1) size_t只是一个typedef,是C++计算个数时所用的一个不带正负号的类型。
(2) extern置于函数或变量前表示该函数和变量定义在别的文件中,编译器会从其他模块中寻求定义,也就是表明了extern声明的变量只能是定义式,并没有分配内存,找到变量或函数的声明式时才能分配内存。
(3) extern同时也可以用来进行链接指定,常见的用法就是extern “C”,因为C++在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间函数名,而C语言就不会,因此在链接阶段会出现找不到函数的情况,此时C函数就需要使用extern “C”来进行链接指定。如果想要知道链接的过程时怎么进行的,可以去看《程序员的自我修养》这本书,里面有很详细的解释,这本书我还没啃透,就不多啰嗦了。
(4) 与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
(5) extern常见的用法:
#ifdef __cplusplus
extern "C" {
#endif
/insert you code here.../
#ifdef __cplusplus
}
#endif
条款一
条款二
尽可能地少用#define,可以用const,enum,inline来替换它。#define的缺点:
(1) 调试问题:假设#define ASPECT_RATIO 1.653,在编译器开始处理源代码的时候,预处理器已经移除了ASPECT_RATIO,于是symbol table中根本就没有ASPECT_RATIO,在调试编译错误信息的时候最多只能看到1.653。
(2) 作用域问题:我们是无法用#define来创建一个class的专属常量,一旦宏被定义,其后的编译过程都会有效,没有任何的封装性。
(3) 宏很大的一个好处就是当用宏定义一个函数的时候不会导致因函数调用导致的额外开销,会有一定的效率提升,但这种宏的缺点也是很明显的。
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))
int a = 5,b = 0;
CALL_WITH_MAX(++a,b); //a被累加2次
CALL_WITH_MAX(++a,b); //a被累加1次
这个时候如果你使用template inline不仅可以同样可以获得宏带来的效率提升,也可以保证函数的安全性。
template<typename T>
inline void callWithMax(const T& a,const T& b)
{
f(a > b ? a : b);
}
扩展知识
(1)class的专属常量,这样一个专属常量的初始化是有很多限制的,为了保证常量的作用域是限制在class内的,必须让它成为class的一个成员,而为了保证这个常量至多只能有一份实体,就必须让它成为一个static成员(为了避免复杂的编译器规则,C++要求每一个对象只能有一个单独的定义,如果C++允许在类的内部定义一个和对象一样占据内存的实体的话,这样的规则就被破坏了)。
class GamePlayer {
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns]; //使用该常量
...
};
注意,这仅仅是一个常量的声明式而非定义式,通常C++要求你对所使用的任何东西都要提供一个定义式,但如果它是一个class的专属变量又是static且为整数类型(例如ints,chars,bools),则需要特殊处理。只要不取它们的地址,你就可以声明和使用它们而无需提供定义式。
另一种方式你可以采用enum的方式:
class GamePlayer {
private:
enum { NumTurns = 5 };
int scores[NumTurns];
...
};
这个时候enum可以权充ints,也就是说常量必须是被一个常量表达式初始化的整型或枚举类型。这样做的一个好处在于别人是无法获得一个pointer或reference指向这个整型常量。
条款三
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
STL迭代器就是以指针为根据模塑出来的,所以迭代器的作用就像是 T* 指针。
声明迭代器为const就像声明指针为const一样(T * const),如果希望迭代器所指向的东西是不可改动的(const T *指针),需要的就是const_iterator。
std::vector<int> vec;
const std::vector<int>::iterator iter = vec.begin();
*iter = 10;
++iter; // Error!!!
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; // Error!!!
++cIter;
const成员函数的意义:
(1)使函数的接口更容易被理解;
(2)操作const对象成为可能。
两个成员函数如果只是常量性不同,是可以被重载的。成员函数是const就意味着bitwise constness和logical constness。
bitwise constness是C++对常量性的定义,const成员函数是不可以更改对象内任何not-static成员变量。这也是编译器强制实施的,编译器只会去寻找成员变量的赋值动作,下面的这种做法就可以绕过语法检查。
class CTextBlock {
public:
CTextBlock(char str[]) { pText = str; }
char& operator[](std::size_t position) const { return pText[position]; }
private:
char* pText;
};
const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = ‘J‘; // 这里编译器的语法检查是不会报错的,但编译的时候就会报错
在实际使用const成员函数经常会遇到的问题如下:
class CTextBlock {
public:
CTextBlock(char str[]) { pText = str; }
std::size_t length() const;
private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid) {
textLength = std::strlen(pText); // Error!!!
lengthIsValid = true; // Error!!!
}
return textLength;
}
你是无法在const成员函数中对non-static的变量进行修改的,那么如何摆脱这个约束呢?解决方法——mutable(可变的)。
class CTextBlock {
...
mutable std::size_t textLength;
mutable bool lengthIsValid;
...
};
const和non-const成员函数的重复问题是无法通过mutable来解决的,例如:
class CTextBlock {
public:
const char& operator[](std::size_t position) const {
...//边界检查
...//记录数据访问
...//检验数据完整性
return text[position];
}
char& operator[](std::size_t position) {
...//边界检查
...//记录数据访问
...//检验数据完整性
return text[position];
}
private:
std::string text;
};
解决问题的方法就是进行转型:
class CTextBlock {
public:
const char& operator[](std::size_t position) const {
...
return text[position];
}
char& operator[](std::size_t position) {
return const_cast<char&>(
static_cast<const CTextBlock&>(*this)[position]
);
private:
std::string text;
};
这里进行了两次转型,第一是为*this添加const(调用const版本的operate[]),第二次是从const operator[]的返回值中移除const。
条款四
通常如果使用C part of C++,初始化肯定会导致运行期的成本,那么就不能保证发生初始化,non-C parts of C++就可以保证这一点。(这也就是为什么array不能保证其内容被初始化,而vector却有此保证)
对于内置类型以外的,初始化就由构造函数来进行,要确保每一个构造函数都能将对象的每一个成员进行初始化。一点需要注意的是不能混淆了赋值和初始化。
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
{
theName = name; // 赋值,而非初始化
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,也就是说在构造函数内进行的实际上只是赋值的操作,初始化动作的发生在成员变量(非内置类型)的default构造函数被调用时。所以通常的初始化方法通常采用初始化列表。
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
: theName(name), // 初始化操作
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}
这样做的效率会更高,原因在于基于赋值的版本,会首先调用default构造函数为theName,theAddress和thePhones设置初值,然后立马重新进行赋值,而初始化列表的方法会直接传递实参给构造函数,相当于只调用了一次copy构造函数。
对于大多数的类型来说,比起先调用default构造函数然后再调用copy assignment操作符,只调用一次copy构造函数会更高效。总之使用成员初始列表,简单而且高效。对于那些拥有许多成员变量和基类,或者成员变量的初值是由文件或数据库读入的,可以把一些初始化操作放到private的函数中,供构造函数进行调用,避免重复的初始化。(cocos2dx中的init函数)
C++的成员初始化次序是固定的,base classes早于derived classes被初始化,而class的成员变量总是按照其声明次序(注意声明式)初始化,即使在初始化列表中出现的次序不同。
不同编译单元内定义non-local static对象的初始化次序是不确定的:
(1)static对象包括global对象,定义域namespace作用域内的对象,在classes内、函数内、以及file作用域内声明为static的对象。函数内的static对象可以称为local static对象,其他的static对象称为non-local satic对象。程序结束的时候,static对象会被自动销毁,析构函数会在main()结束时被自动调用。
(2)编译单元是指产出单一目标文件的源码,基本上是单一源码文件加上包含的头文件。
多个编译单元内的non-local static对象经由“模板函数具现化”形成,无法决定正确的初始化次序。那么要如何解决这个问题呢?
设计模式中的一个简单的方法就可以解决这个问题:将每个non-local static对象放到自己的专属函数中(该对象在此函数中被声明为static),这些函数返回一个reference指向它所指向的对象。然后用户调用这些函数,而不直接指涉这些对象。用local static对象替换non-local static对象,因为local static的对象会在函数调用期间或第一次遇到定义式的时候被初始化,那么函数返回的reference就可以保证这个对象已经被初始化,而且如果没有调用过这个函数,就不会引发构造和析构的成本。这就是Singleton模式的一种常见实现手法。
class FileSystem {
public:
...
std::size_t numDisks() const;
...
};
extern FileSystem tfs;
class Directory {
public:
Directory(params);
...
};
Directory::Directory(params) {
...
std::size_t disks = tfs.numDisks();
...
}
Directory tempDir(params); // 如何保证tfs在tempDir之前初始化
class FileSystem { ... };
FileSystem& tfs() {
static FileSystem fs; // 用函数代替tfs,返回一个reference指向local static对象
return fs;
}
class Directory { ... };
Directory::Directory(params) {
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td; // 同理
return td;
}
这种结构十分单纯,定义并初始化一个local static对象然后返回,如果被频繁地使用的话,可以让它们成为inlining函数。对于内含static对象,在多线程系统中带有不确定的因素,所以可以在单线程启动阶段手动调用所有的reference-returning函数。
总结一下,对于对象的初始化:
(1)手动初始化内置型的non-member对象;
(2)使用初始化列表处理对象的所有成分;
(3)对于初始化次序不确定的情况,加强自己的设计。
扩展知识
(1)关于C++编译器和运行期的问题,在《深度探索C++对象模型》这本书的读书笔记再和大家介绍吧。
原文地址:http://blog.csdn.net/john_cdy/article/details/45476957