写在前面
第一遍看《Effective C++》时,在准备暑期实习生的招聘,没有时间好好地捋一下,将一些要点记录下来。现在实习回来,重读此书,并记录一些要点,为今后的复习亦或是学习铺垫。
这篇介绍第一章的4个条款。
条款01:视C++为一个语言联邦
- C++是一个多重范型编程语言:
- 支持过程形式
- 支持面向对象形式
- 支持函数/泛型形式
- 支持元编程形式
- 理解C++,必须首先认识其主要的次语言:
- C语言。 C++ 以C为基础,区块,语句,预处理器,内置数据类型,数组,指针等全部都来自于C。C++是C的高级解法揭露了C语言的局限性:
- 没有模板。
- 没有异常处理。
- 没有重载。
- ...(以及封装,继承等面向对象的特性等其它)
- Object-Oriented C++: 简单总结就是面向对象的特点。
- 类。每个类都有构造函数,析构函数。
- 封装,继承,多态,虚函数(动态绑定).
- Template C++: C++范型编程的部分。有了模板可以带来崭新的编程范型。
- STL: Standard Template Library。对容器,迭代器,算法以及函数对象的规约有极佳的机密配合与协调。
- 对于内置类型,pass-by-value通常比pass-by-reference更加高效,但是对于用户自定义(user-defined)类型,由于构造函数和析构函数的存在,pass-by-reference-to-const往往更好。
作者总结:
C++的高效编程守则视状况而变化,取决于你使用C++的哪一部分。
个人总结:
此条款介绍了C++的组成部分,在开发过程中,要高效利用C++的这几个“次语言”特性,在使用不同的“次语言”的时候,要选择相应的高效的编程方式。
条款02:尽量用const,enum,inline替换#define
首先要明白使用#define的缺点:
-
如#define NUM 1.2,调试的时候出现的是1.2而不是NUM,如果这个宏定义不是自己写的就更难定位问题了。
-
宏定义#define MAX(a,b) ((a) > (b) ? (a) : (b))看似可行的一个宏定义函数,但是考虑以下情况:
int a = 5,b = 0;
MAX(a++,b); // a被累加两次
MAX(++a,b + 10); // a被累加一次
a的累加次数取决于a和b的大小,显然不是调用者所期待的情形。
class的专属常量
假定我们在GamePlayer类中有个常量成员,有个数组,数组大小使用该常量表示。
class GamePlayer
{
public:
static const int iNum = 5; //常量声明式
int iScores[iNum];
。。。 // 其它成员
}
要明确一点:上述const是一个声明式,并不是一个定义式。
为什么要声明为static?
如果不是static,在该类还未构造时,iNum是不存在的,编译器也就无法知道数组iScores的大小。编译器会坚持要求知道数组的大小。
旧式的编译器中,不允许static在声明的时候不允许被赋初值。如果不支持声明时候赋初值,就应该改为:
class GamePlayer
{
public:
static const int iNum;
...
}
在函数体外再赋初值:
int GamePlayer::iNum = 5;
但是采取这种写法就无法在类中定义一个常量大小的数组。
采用enum解决
声明enum常量,就可以防止不同编译器对const能否赋初值所带来的不便之处。
class GamePlayer
{
public:
enum { iNum = 5 };
int iScores[iNum];
}
使用enum的更多好处
enum声明的常量是一个右值。如果不想别人用一个pointer或者reference指向一个整型常量,使用enum即可。引用和指针都无法绑定在一个枚举常量上。
enum
{
first = 1,
};
//int &First = first; 无法通过编译
//int *pFirst = &first; 无法通过编译
作者总结
对于单纯常量,最好以const对象或enum替换#define.
对于形似函数的宏,最好改用inline function替换#define.
条款03:尽可能使用const
先写一下老生常谈的const和pointer的不同组合的效果。
char hello[] = "hell0";
char *p1 = hello; // non-const pointer,non-const data
const char *p2 = hello; // non-const pointer,const data
char* const p3 = hello; // const pointer,non-const data
const char* const p = hello; // const pointer,const data
初学者不容易记住,其实只要记住const后面是什么(数据类型不看),什么就不变就对了。
比如说,const char *p,const 后面是 *p, p是指针所指向的数据,所以是data不变。又比如 char const p; const后面是p,p是一个指针,所以是个const pointer,指针指向的地址不能改变。
由此延申出const在STL迭代器中的使用
假设我们用一个迭代器指针去操作一个vector容器:
如果用const显式修饰:
vector<int> vct(10,1);
const vector<int>::iterator it = vct.begin(); // 此迭代器指针是一个non-const data,const pointer类型。
*it = 9; // 正确。
++it; // 错误,const pointer不能改变指向的位置。
上述代码中,const 修饰的迭代器指向的地址不可变,所以只能指向vct.begin()位置。
此外,STL的迭代器中有一个const_iterator,是一个non-const pointer,const data类型的迭代器。
vector<int> vct(10,1);
vector<int>::const_iterator cIt;
*cIt = 10; // 错误,const data,不可改变其值。
++cIt; // 正确。non-const pointer.可以改变其指向。
令函数返回一个常量值可以降低错误发生的概率
const Rational operator *(const Rational &lhs, const Rational &rhs);
如果返回的不是const,那么很可能写成
if(a * b = c)
这个是程序员写错的时候的情形,如果返回const那么就会提示报错,就能立即定位错误。而如果不是const类型,那么这个错误就可能很难被发现。
const修饰成员函数
const修饰的成员函数,在函数体内不能修改任何一个成员变量。如果是可能被修改的成员变量,那么这些成员变量应该是使用mutable关键词来修饰。mutable关键词可以去掉non-static成员变量的bitwise constness约束。
注意:两个成员函数的常量性不同,是可以被重载的。
例如:
class TextBlock
{
public:
const char &operator[](std::size_t position) const
{
... // 记录数据1
... // 记录数据2
... // 记录数据3
return text[position];
}
char & operator[](std::size_t position)
{
... // 记录数据1
... // 记录数据2
... // 记录数据3
return text[position];
}
}
如果这两个版本实现了相同的函数体,只是返回值的常量性不同,那么可以将non-const版本改成以下版本:
char & operator[](std::size_t position)
{
return const_cast<char &>(
static_cast<const TextBlock&>(*this)
[position])
);
}
这语句有两个转型的动作:
(1) static_cast<const TextBlock&>.将当前对象转成const的对象。
(2) const_cast<char &>是去掉const属性,恢复成原来的非const对象。
我们重载了[]运算符,const和非const版本都有。当对象为const 属性的时候调用的是const版本,非const属性对象就调用非const版本。
这样写可以避免代码冗余。
注意:只能用非const去调用const,如果使用const去调用非const,那么就先要将const属性去掉,那么原本const函数体中的数据就不被保证不会被修改,也就失去了我们一开始使用const修饰的初衷。
作者总结
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于在任何作用域内的对象,函数参数,函数返回类型,成员函数本体。
编译器强制实施bitwise constness,但你编写程序的时候应该使用“概念上的常量性”。
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
条款04:确定对象使用前已经被初始化。
先搞清楚赋值和初始化是不一样的:
假设有一个类ABEntry:
class ABEntry
{
public:
ABEntry(const string name,const string addr);
private:
string theName;
string theAddr;
}
以下的构造函数中是赋值给私有成员变量,而不是初始化私有成员变量。
ABEntry::ABEntry(const string name,const string addr)
{
theName = name; // 赋值
theAddr = addr; // 赋值
}
真正的初始化:
ABEntry::ABEntry(const string name,const string addr)
:theName(name),theAddr(addr)
{
}
二者的不同:
第一个构造函数中:
(1) 在赋值之前,theName和theAddr先执行了它们各自的默认构造函数,也就是string类中的默认构造函数,有了一个初值(为空)。
(2) 进行赋值的时候,调用了copy assignment操作符。将name和addr复制给theName和theAddr.
所以它充其量只是一个赋值,并不能说是初始化,第一小步就已经初始化成一个空string了。
而在第二个构造函数中,只调用了一个copy构造函数去构造初始值。《C++ Primer》中将这种初始化方式叫做成员列表初始化。
单单使用一个copy构造函数显然是比较高效的。在内置类型中,不需要调用默认构造函数,二者的效率是差不多的。
const和reference初始化
由于const和reference一定需要初值,而不能被赋值改变,所以需要采用成员列表初始化的方式来进行初始化操作。
成员变量的初始化顺序
在C++中,成员变量的初始化顺序严格遵守变量的声明顺序。
class Text
{
public:
...
private:
string strAddr;
string strName;
int iCall;
}
在上述类之中,如果采用成员列表初始化,那么初始化顺序依此为strAddr,strName,iCall.如果需要使用strName的值去初始化strAddr, 那么是错误的做法,因为strAddr先于strName初始化,strName这个时候尚未有值。
作者总结:
为内置型对象进行手工初始化,因为C++不保证初始化它们。
构造函数最好使用成员初始列,而不要在构造函数本体内使用赋值操作,初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。