条款01:视C++为一个语言联邦
C++可以理解为四个次语言的结合:
- C。 基本的区块(blocks)、语句(statements)、预处理(preprocessor)、内置数据类型(build-in data types)、数组(arrays)、指针(pointers)等都来自于C
- Object-oriented C++。这部分是C with Class部分,包括:封装、继承、多态等古典面向对象设计守则。
- Template C++。这是C++泛型编程的部分,也是大多数程序员经验最少的部分。
- STL。容器、迭代器、算法及函数对象的标准库。
总结:
- C++高效编程守则视情况而变化,取决于你使用C++的哪一部分。
条款02:尽量以const,enum,inline替换#define
1)基本数据结构:
#define ASPECT_RATIO 1.653
替换为
const double AspectRatio = 1.653;
2) 常量指针
#define AUTHOR_NAME "Scott Meyers"
替换为
const char* const AuthorName = "Scott Meyes";
注意将指针和指针指向的内容都定义为const。
3)类成员常量
防止常量生成多份实体,将常量定义为static
class GamePlayer{
pravite:
static const int NumTurns = 5; //声明常量
int scores[NumTurns]; //使用该常量
}
4) 类成员常量使用enum
有些旧式编译器不支持static 型常量在class中声明时初始化,那么就需要用到enum的方式。
class GamePlayer{
private:
enum {NumTurns = 5}; //声明常量
int scores[NumTurns]; //使用该常量
}
5)函数宏
#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+10); //a被累加3次
所以我们使用tempate inline函数解决这些问题。
template<typename T>
inline void callWithMax(const T& a, const T& b){
f(a > b ? a : b);
}
总结:
- 对于单纯变量,最好以const 对象或enums替换 #define
- 对于形似函数的宏,最好改用template的inline函数替换#define
条款03:尽可能使用const
1)const指针的用法
const 出现在指针左边,则修饰指针指向的对象;若const 出现在指针右边,则修饰指针本身。
char greeting[] = "hello";
char* p = greeting; // no-const pointer, no-const data
const char* p = greeting; //no-const pointer, const data
char const* p = greeting; //no-const pointer, const data
char* const p = greeting; //const pointer, no-const data
const char* const p = greeting; // const pointer, const data
2) const 迭代器
const 修饰迭代器表明迭代器不得指向不同的东西,如果想要迭代器指向的东西不能改变,需要用const_iterator。
std::vecor<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();
*iter = 10; //没问题,改变迭代器指向的内容
++iter; //错误, iter是const的
std::vector<int>::const_iterator cIter = vec.begin();
*cIter = 10; //错误, *cIter是const的
++cIter; //没问题,改变cIter
3) const 修饰成员函数
原则上,const修饰成员函数,表明在该函数内不能改变本对象的任何数据。
class CTextBlock {
public:
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); //错误!在const成员函数内,不能赋值给textLength 和lengthIsValid
lengthIsValid = true;
}
return textLength;
}
这种情况下,可以使用mutable关键词。表明成员变量即使在const 成员函数内也可能被更改
class CTextBlock {
public:
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const{
if(!lengthIsValid){
textLength = std::strlen(pText); //正确
lengthIsValid = true;
}
return textLength;
}
4)const和no-const成员函数避免重复
首先要知道:两个成员函数如果只是常量性不同,可以被重载。
class TextBlock {
public:
const char& operator[](std::size_t position) const{ //operator[] for const对象
return text[position];
}
char& operator[](std::size_t position){
return text[position];
}
private:
std::string text;
};
TextBlock tb("Hello");
std::cout << tb[0]; //调用no-const TextBlock::operator[]
const TextBlock ctb("Hello");
std::cout << ctb[0]; //调用const TextBlock::operator[]
当const和no-const函数的内容一致,但是代码量庞大时,就会造成严重的代码重复。所以我们可以通过no-const版本调用const版本的方式来简化代码。
class TextBlock {
public:
const char& operator[](std::size_t position) const{ //operator[] for const对象
... //边界检查
... //志记数据访问
... //检验数据完整性
...
return text[position];
}
char& operator[](std::size_t position){
const_cast<char&>( //将operator[]返回值的const转除
static_cast<const TextBlock&> (*this)[position]); //将*this加上const属性,并调用const operator[]
return text[position];
}
private:
std::string text;
};
这个代码有两次强制类型转换。第一次是使用static_cast将*this加上const属性,以避免no-const operator[]一直重复调用自己。
第二次是使用const_cast强制从返回值移除了const属性。
总结:
- 将某些东西声明为const可帮助编译器侦测出错误的用法。const可被施加于在任何作用域的对象、函数参数、函数返回类型、成员函数本体。
- 编译器强制实施bitwise constness,但你在编写程序时应该使用"概念上的常量性"。
- 当const和no-const成员函数有实质上的等价关系时,令no-const版本调用const版本可避免代码重复。
条款04:确定对象被使用前已先被初始化
1)内置型对象进行手工初始化
int x = 0;
const char* text = "A C-style string";
double d;
std::cin >> d;
2)成员函数初始化
注意不要搞混赋值和初始化
class PhoneNumber {...};
class ABEntry {
public:
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
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构造函数会先对这些成员进行初始化,在初始化后又进行赋值。效率比较低,正确做法应该是使用member initialiazation list
(成员初始列)来代替赋值。如下所示:
class PhoneNumber {...};
class ABEntry {
public:
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0){
}
3)不同编译单元内定义之non-local static 对象的初始化
如:
class FileSystem {
public:
...
std::size_t numDisks() const; //众多成员函数之一
...
};
extern FileSystem tfs; //预备给客户使用的对象
class Directory { //客户程序类
public:
Drectory(params);
...
};
Directory::Directory(params){
...
std::size_t disks = tfs.numDisks() ; //使用tfs对象
...
}
如果客户程序先一步初始化,则会调用未初始化的对象tfs,产生不可预知的后果。
而C++本身也确实无法设定不同编译单元内的non-local static 对象的初始化顺序。所以就要靠设计模式来解决了。
用Singleton模式比较巧妙的解决这一问题。
class FileSystem {
public:
...
std::size_t numDisks() const; //众多成员函数之一
...
};
FileSystem& tfs(){
static FileSystem fs;
return fs;
}
class Directory { //客户程序类
public:
Drectory(params);
...
};
Directory::Directory(params){
...
std::size_t disks = tfs().numDisks() ; //使用tfs函数
...
}
当然这样写不是线程安全的,线程安全的单例模式网上也有很多,这里只是举个例子。
总结
- 为内置型对象进行手工初始化,因为C++不能保证初始化它们。
- 构造函数最好使用成员初始列,而不要在构造函数本体内使用赋值操作。且初始列列出的成员变量,其排列次序应该与它们声明的次序一致。
- 为免除“跨编译单元之初始化次序”问题,请以local static 对象替换non-local static 对象。