这段时间都在看《Effective C++》这本书,觉得写的确实很好,所以没看一个章节就想着能简单总结总结。
02 尽量以const,enum,inline替换#idefine
1、#define ASPECT_RATIO 1.653
Const double AspectRatio = 1.653;
这样替换的好处是:
1) 当你编译这个常量发生错误的时候,#define很可能直接指出的错误信息是1.653而不是ASPECT_RATIO,在查找的时候会有一些困惑。
2) 用#define可能会导致ASPECT_RATIO的目标码出现多份的1.653,const则不会。
需要注意的是:
1) 当使用const修饰char* -based字符串的时候,必须写const两次.
const char* const authorName = “Scott Meyers”;
2) const修饰类的成员变量的时候,为确保此常量最多只有一份实体,需要static;且#define 无法创建一个class专属常量,因为#define不重视作用域;重点内容
Class GamePlayer{
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns]; //使用该常量
…
};
const int GamePlayer::NumTurns; //常量NumTurns的定义
2、enums想比如const,取一个const的地址是合法的,但取一个enum地址就不合法,取#define地址也不合法。如果不希望别人获得一个pointer或者reference指向你的整数常量,enum可以实现这个目的。
3、内联函数相对于宏定义的优点
#define CALL_MAX(a,b) f((a) > f(b) ? (a): (b))
换成:
template<typename T>
inline void callMax(const T& a, const T& b){
f(a > b ? a : b);
}
总结:
(1) 调试问题:假设#define ASPECT_RATIO 1.653,在编译器开始处理源代码的时候,预处理器已经移除了ASPECT_RATIO,于是symbol table中根本就没有ASPECT_RATIO,在调试编译错误信息的时候最多只能看到1.653。
(2) 作用域问题:我们是无法用#define来创建一个class的专属常量,一旦宏被定义,其后的编译过程都会有效,没有任何的封装性。
(3) 宏很大的一个好处就是当用宏定义一个函数的时候不会导致因函数调用导致的额外开销,会有一定的效率提升,但这种宏的缺点也是很明显的。
【要记住】:
**对于单纯常量,最好以const对象或enums替换#defines
对于形似函数的宏,最好改用inline函数替换#defines**
03尽可能使用const
const可以用它在classes外部修饰global或者namespace作用域中的常量,或修饰文件、函数、或块作用域中被声明为static的对象。也可以修饰classes内部的static和non-static成员变量。对于指针,也可以修饰指针、指针所指对象,或者两者都是const修饰。
char greeting[] = “Hello”;
char* p = greeting; //非常量指针,非常量数据;
const char* p = greeting; //非常量指针,常量数据;
char* const p = greeting; //常量指针,非常量数据;
const char* const p = greeting; //常量指针,常量数据;
如果const出现在星号左边,表示被指物是常量,出现在星号右边,表示指针自身是常量;如果出现在星号两边表示被指物和指针两者都是常量。const修饰迭代器和指针完全相同的方式。
const最主要的用法是在函数声明时的应用。一个函数声明式内,const可以和函数返回值、各参数、函数自身产生关联。
const成员函数
将const实施于成员函数的目的,是为了确认该成员函数可以作用于const对象身上。它的重要性体现在:
第一, 他们使class接口比较容易被理解(知道哪个函数可以改动对象类型)。
第二, 使“操作const对象成为可能“。
成员函数修饰为const的,那么这个函数就不能修改成员变量,也不能调用非const的成员函数。两个成员函数,如果只有常量性不同,可以被重载。
class base{
public:
...
void print()const{ //const成员函数
cout<<n;
}
...
private:
int n;
}
C++以by value返回独享这一事实【条款20】意味着被改动的其实是参数的一个副本,而不是它本身,这不是你希望的结果。
如果const成员函数需要访问或者修改非const成员的时候,可以用c++的一个与const相关的摆动场:mutable(可变的)。mutable可以释放掉non-static成员变量的constness约束。
eg: mutable std::size_t textLength;
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;
}
//改称成mutable的形式
class CTextBlock {
...
mutable std::size_t textLength;
mutable bool lengthIsValid;
...
};
const和non-const成员函数的重复问题是无法通过mutable来解决的,
令non-const operator[]调用其const函数是一个避免代码重复的安全做法——即使过程中需要一个转型动作。
class TextBlock{
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 TextBlock{
public:
…
const char& operator[] (std::size_t position) const{
… //边界检验
… //日志数据访问
… //检验数据完整性
return text[position];
}
char& operator[] (std::size_t position) { //只调用const op[]
return const_cast<char&>( static_cast<const TextBlock&>(*this) [position]);
}
private:
std::string text;
}
为了避免无穷递归,我们必须明确指出调用的是const operator[],因此这里将*this从其原始类型TestBlock&转型为const TextBlock&,使用static_cast能够实现安全转型。
【请记住】
1) 将有些东西声明成const可以帮助编译器侦测出错误的用法。Const可以被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
2) 当const和non-const成员函数具有实质等价的实现时,令non-const版本调用const版本可以避免代码重复。
04确定对象被使用前已被初始化
首先,读取未初始化的值会导致不明确的行为。
对于内置类型以外的,初始化就由构造函数来进行,要确保每一个构造函数都能将对象的每一个成员进行初始化。所以要明白赋值和初始化的简单区别:
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。
所以ABEntry构造函数内theName, theAddress和thePhones都不是被初始化,而是被赋值。
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构造函数。
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就可以保证这个对象已经被初始化,而且如果没有调用过这个函数,就不会引发构造和析构的成本。
总结一下,对于对象的初始化:
(1)手动初始化内置型的non-member对象;
(2)使用初始化列表处理对象的所有成分;
(3)对于初始化次序不确定的情况,加强自己的设计。
【记住】
1) 为内置对象进行手工的初始化,因为C++不保证初始化它们。
2) 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初始列表的成员变量,尽量保持声明顺序。
3) 为了免除“跨编译单元的初始化次序“问题,请以local static对象替换non-local static对象。