Effective C++的讨论内容:
- 一般性的设计策略
- 带有具体细节的特性语言特性
例如: - inheritance or templates?
- public or private?
- private inheritance or composition?
- pass-by-value or pass-by-reference?
- return type of assignment?
- when to use virtual destructor?
条款01:视C++为一个语言联邦
view c++ as a federation of languages
最初的C with Classes -> Exceptions, templates
如今发展到多重范型编程语言(multiparadigm programming language),同时支持多种形式:
- 过程形式:procedural
- 面向对象形式:object-oriented
- 函数形式:functional
- 泛型形式:generic
- 元编程形式:metaprogramming
因此应该将C++视为多个次语言(sublanguage)形成的联邦。
- C:区块blocks,语句statement,预处理器preprocessor,内置数据类型built-in data types,数组arrays,指针porinters均来自C。但是C没有模板template,没有异常exceptions,没有重载overloading。
- Object-Oriented C:C with classes中的classes,constructor&destructor,封装encapsulation,继承inheritance,多态polymorphism,virtual,动态绑定等。
- Template C++:泛型编程genneric programming部分。包含Template metaprogramming(TMP,模板元编程)。
- Standard Template Library:containers,iterators,algorithms,function objects
C-like中,pass-by-value更高效;
Object-Oriented和Template中pass-by-reference-to-const更高效。
STL中,实质上都是基于指针,因此pass-by-value再次适用。
条款02:尽量以const,enum,inline替换#define
Prefer consts, enums and inlines to #defines.
i.e.“宁可以编译器替换预处理器”。
#define语句并非语言的一部分。预处理器在编译前将宏定义的值移走,因此该内容对编译器不可见,报错时也只能报具体数值而不是宏名称(原因:宏的名称不进入记号表symbol table)。
用const替换#define的两个特殊情况:
- const指针:应该将指向常量的指针本身也设为const:
const T* const p; - class中的常量:为了将其作用域限制在class内,应该定义为class的一个成员;而为了保证只有一份,应该定义为static成员。
static const int MaxValue = 10;
注意:某些旧编译器在类内只能进行const static成员的声明,而定义必须在类外进行。若编译期编译器不能找到该值(如需要使用该值创建一个数组),则可以使用“enum hack”方法:
enum{ MaxValue = 10};
int a[MaxValue];
enum和const的区别:
- enum不允许取地址,因此不能使用指针或引用
- enum不会导致非必要的内存分配,但某些编译器可能为static const对象分配另外的空间
另一种#define误用情况
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);
CALL_WITH_MAX(++a, b+10);
应该改为template inline函数:
template<typename T>
inline void callWithMax(const T& a, const T& b){
f(a>b ? a:b);
}
小结
-
#define依然是必需品,#ifdef和#ifndef也在编译控制中有重要作用。 - 对于单纯常量,最好以const或enums替换
#define - 对于类似函数的宏,最好改用inline函数替换
#define
条款03:尽可能使用const
Use const whenever possible
const用途广泛,可修饰:
- class外部的global或namespace作用域中的常量
- 文件、函数、区块作用域中static的对象
- class内部的static和非static成员变量
- 指针的顶层和底层const
STL中基于迭代器的算法都采用T*形式的参数,在大多条件下应该将其设为const。
const成员函数
const用于成员函数是为了确认该成员函数可作用于const对象。这类函数成员十分重要,比如:
- 使class接口容易被理解:得知哪些函数可以改变对象内容
- 实现“操作const对象”:pass by reference-to-const
一个重要的C++特性:如果两个成员函数只是常量性不同,可以被重载。
const char& operator[](std::size_t position) const{
return text[position];
}
char& operator[](std::size_t position){
return text[position];
}
const Text text1("hello");
Text text2("world");
cout<<text1[0]<<text2[0]<<endl;
//Text1返回的对常量的引用不能被赋值。
对constness的两大解释:
- bitwise constness(physical constness)
- logical constness
bitwise constness是C++对常量性的定义,指成员函数只有在不更改对象任何非static成员变量时才是const。
问题在于不具备const性质的成员函数也可以通过bitwise测试。即:更改了指针所指对象的成员函数虽然不是const,但如果只有指针本身属于对象,则不会引发编译错误,如为了对应C而将string改为char*的程序:
class CTextBlock{
public:
char& operator[](std::size_t position)const{
return pText[position];
}
private:
char* pText;
}
此代码中只有pText是成员,而operator[]返回一个指向对象内值的引用:
const CTextBlock cctb("hello");
char* pc = &cctb[0];
*pc = 'y'; //不会报错,内容变为yello
因此仅有bitwise const不够,延伸出了logical constness:const成员函数可以在客户端侦测不出的情况下更改所处理对象的某些bits。
只要对可能会被改变的成员变量使用mutable关键字即可:
class CTextBlock{
public:
size_t length() const;
private:
char* pText;
mutable size_t textLength;
mutable bool lengthIsValid;
};
size_t CtextBlock::length() const {
if(!lengthIsValid){
//下列赋值可行
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
在const和非const成员函数中避免重复
若同时定义了const T T::foo() const和T T::foo(),有时二者都需要进行的检测如边界检验(bounds cheking)、日志数据访问(log access data)、检验数据完整性(verify data integrity),会导致大量代码重复,应尝试不同时使用两个,而是用一个调用另一个:
使用常量性转除casting away constness
即:首先定义const版本,再利用const_cast和static_cast
const char& operator[](size_t position)const{
...
return text[position];
}
char& operator[](size_t position){
return const_cast<char&>{ //去除返回值的常量性
static_cast<const TextBlock&>(*this) //*this加const,为了确认调用的是const版本,而不是递归调用自己
[position]; //调用const的operator[]
}
}
小结
- const作用广泛,可用于任何作用域
- 编译器强制bitwise constness,但编写时注意conceptunal constness
- const和non-const成员函数有实质等价的实现时,可以使用非const版本调用const版本,注意两次转换。
条款04:确定对象被使用前已先被初始化
Make sure that objects are initialized before they are used
使用未初始化的变量可能会发生ub。大多发生在C++语言联邦的C语言部分,如内置数组。但除此之外的部分,如vector则不会发生这些问题。
唯一的解决方法是永远在使用对象之前将其初始化。对于无任何成员的内置类型,必须手工完成初始化。如int、指针等。
区分赋值和初始化
若在构造函数体内部为类成员赋值,其实再构造函数体执行之前已经调用过一次默认构造函数,因此构造函数体内部对成员变量是赋值操作而不是初始化。也就是说,该过程先调用了默认构造函数,然后调用了拷贝构造函数。
Foo::Foo(const int a){
memberA = a; //赋值而非初始化
}
应该更改为在构造函数初始化列表(函数名字后函数体前)进行成员函数的初始化。
Foo::Foo():memberA(a)){}
如果成员变量是const或reference就需要初值,而不能被赋值。最简单的方法是:应该在构造函数初始化列表写上所有成员变量。同时,C++具有十分固定的成员初始化顺序,因此应该按照类中声明顺序列出各成员变量。
static对象的UB
static对象:
- 一定不属于stack和heap-based对象,
- 一般包括global对象、namespace作用域的对象
- class、函数、file作用域内的对象
- 函数内的static对象称为local static对象,其他是non-static对象。
编译单元translation unit:产出单一目标文件的源码。基本上是单一源码文件加上含的头文件。
若两个源码文件中,每个内含至少一个non-local static对象,如果其中一个初始化时使用了另一个,初始化次序是不定的,会出现UB。
解决方式:将每个non-static对象搬到其自己的专属函数内。该对象在该函数内被声明为static。这些函数返回一个指向其所含对象的reference,用户调用这些函数而不是static对象本身。
这是一种Singleton模式。在函数中声明的static变量其实已经被初始化了,不会出现使用未初始化static变量的问题。可以理解为,通过函数将a的使用转为了fa(){static A a; return a}中对a的初始化再使用。
小结
- 为内置类型对象手动初始化
- 构造函数中使用成员初始化列表,而不是在构造函数体中进行赋值。
- 为免除“跨编译单元初始化次序”问题,以local static对象替换non-local static对象。