1. 让自己习惯C++

1. 视C++为一个语言联邦

  • C语言
  • Object-Oriented C++
  • Template C++。这是C++的泛型编程
  • STL。STL是个template程序库,

2. 尽量以const,enum,inline替换#define

1
#define ASPECT_RATIO 1.653
  1. 记号名称ASPECT_RATIO也许从未被编译器看见:也许在编译器开始处理源码之前它就被预处理器移走了。于是记号名称ASPECT_RATI0有可能没进入记号表(symbol table)内。
  2. 解决之道是以一个常量替换上述的宏(#define):
1
const double AspectRatio 1.653:
  1. 作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。此外对浮点常量(floating point constant,就像本例)而言,使用常量可能比使用#define导致较小量的码,因为预处理器“盲目地将宏名称ASPECT RATIO替换为1.653”可能导致目标码(object code)出现多份1.653,若改用常量AspectRatio绝不会出现相同情况。
  2. 两种特殊情况
    1. 定义常量指针,常量定义式放在头文件内,指针要声明为const,
1
2
3
4
//定义一个常量的char*-based字符串,const两次
const char* const authorName = "Scott";
//string对象比char*-based更合时宜,往往定义为
const std::string authorName("Scott");
2. class专属常量
    1. 常量的作用域限制于class内,必须让其成为class的一个成员;为确保此常量纸多只有一个实体,必须让他成为一个static成员:
1
2
3
4
5
6
7
8
9
class GamePlayer
{
private:
statin const int NumTurns = 5; //常量声明式
int scores[Numturns];
};

//定义式
const int GamePlayer::NumTurns;
    2. C++要求对所使用的任何东西提供一个定义式,但如果他是个class专属常量又是static且为整数类型(int,char,bool),则需要特殊处理。只要不取他们的地址,可以声明并使用而无需提供定义式
    3. 如果取某个class专属常量的地址,或者不取地址而编译器要坚持看到一个定义式,就要使用定义式
    4. 定义式放入实现文件而非头文件,由于声明时已经有初值,因此定义时不可以再设初值
    5. 我们无法利用#define创建一个class专属常量,因为#define不重视作用域
    6. 若编译器不预序static整数型class常量,可用枚举类型补偿
1
2
3
4
5
6
7
class GamePlayer
{
private:
enum { NumTurns = 5};
int scores[Numturns];
};

    7. enum hack的行为更像是#define而不像const,因为取enum的地址不合法,如果不想让别人通过指针或者引用指向某个整数变量,enum可以实现。
  1. define误用情况实现宏
    1. 宏中的所有实参要加上小括号,否则回出问题,即使加了小括号,也会有问题
1
2
3
4
5
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
2. 对于形似函数的宏,最好改用inline函数替换#define

3. 尽可能使用const

  1. const允许指定一个语义约束(不被改动的对象),编译器会强制实施这项约束。
  2. const在左边,表示被指物是常量,如果在右边,表示指针自身是常量,如果出现在型号两边,表示被指物和指针两者都是常量。
  3. 被指物是常量,const写在类型之前或者类型之后星号之前。
1
2
void f1(const Widget* pw);
void f2(Widget const * pw);
  1. STL迭代器作用就像T*指针。不能指向不同的东西,但指向的东西的值可以改动
1
const std::vector<int>::iterator iter = vec.begin();   //iter作用类似T* const
1. 若希望所指的东西不能改变,,需要用const_iterator,类似于const T*指针
  1. const成员函数
    1. 将const实施于成员函数的目的,是为了确认该成员函数可作用域const对象身上
    2. 它们使class接口比较容易被理解
    3. 使“操作const对象成为”可能
    4. 两个成员函数如果只是常量性不同,可以被重载
    5. const对象大多用于passed by pointer-to-const或者passed by reference-to-const的传递结果
    6. 如果函数的返回类型是个内置类型,那么改动函数返回值从来就不合法
    7. 利用c++的一个与const相关的摆动场:mutable(可变的),mutable释放掉non-static成员变量的bitwise constness约束
  2. 在const和non-const成员函数中避免重复
    1. const成员函数承诺绝不改变其对象的逻辑状态,如果在const函数内调用non-const函数,就会冒风险
  3. 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  4. 编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”
  5. 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复

4. 确定对象使用前已先被初始化

  1. 读取未初始化的值会导致不明确的行为。
    1. 在某些平台上,读取未初始化的值,可能使程序终止
    2. 可能读入一些半随机的bits,污染了正在进行读取操作的那个对象,最终导致不可预知的程序行为,以及令人不愉快的调试过程
  2. 永远在适用对象之前先将它初始化。对于无任何成员的内置类型,必须手工完成,确保每一个构造函数都将对象的每一个成员初始化
1
2
3
4
5
int x = 0;
const char* text = "A C-style string";

double d;
std::cin >> d;
  1. 注意赋值和初始化的区别,ABEntry构造函数的一个较佳写法是,使用所谓的成员初值列替换赋值动作:效率更高
1
ABEntry::ABEntry(const std::string& name):theName(name){}
  1. 对于大多数类型而言,比起先调用default狗在函数然后再调用copy assignment操作符,单只调用一次copy构造函数时比较高效的。
  2. 要default构造一个成员变量,可以使用成员初值列,只要指定无物作为初始化实参
1
2
3
4
ABEntry::ABEntry()
:theName(),
numTimesConsulted(0) //显式初始化为0
{}
  1. 编译器会为用户自定义类型之成员变量自动调用default构造函数——如果那些成员变量在成员初值列中没有被指定初值的话
  2. 规定总是在初值列中列出所有成员变量
  3. 有些情况,即使成员变量属于内置类型,也一定要使用初值列。如果成员变量时const或者references,就一定要使用初值,不能被赋值
  4. 总是使用成员初值列,这样做有时候绝对必要,且又往往比赋值更高效
  5. 许多类有多个构造函数,每个构造函数有自己的成员初值列,如果这种类存在许多成员变量或基类,多份成员初值列的存在就会导致重复。这种情况在初值列中遗漏那些“赋值表现像初始化一样好”的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数(通常是private),供所有构造函数调用
  6. c++有固定的成员初始化次序,基类更早于派生类被初始化,类成员变量总是以其声明次序被初始化,即使他们在成员初始列中以不同的次序出现。为避免一些晦涩错误,当在成员初值列中条列各个成员时,最好总是以其声明次序为次序(两个成员变量的初始化带有次序性,例如初始化array时要制定大小,因此代表大小的那个成员变量必须先有初值)。
  7. non-local static对象
    1. static对象,寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。包括global、定义于namespace作用域内、在calsses内、在函数内、在file作用域内被声明为static的对象。
    2. 函数内的static对象称为local static 对象,其他static对象称为non-local static对象。程序结束时static对象就会被自动销毁,也就是他们的析构函数会在main()结束时被自动调用。
    3. 编译单元是指产出单一目标文件的源码,时单一源码文件加上其所含入的头文件
    4. 如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,他所用到的这个对象可能尚未被初始化,因为C++对于定义于不同编译单元内的non-local static对象的初始化次序并无明确定义
    5. 将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所包含的对象。然后用户调用这些函数。这是Singleton模式的一个常见实现手法。
    6. 函数内的local static对象会在该函数被调用、首次遇上该对象之定义式时被初始化。所以用函数调用(返回一个reference指向local static对象)替换直接访问non-local static对象,保证获得的references将指向一个历经初始化的对象。
    7. 更棒的是,如果从未调用non-local static对象的“仿真函数”,就绝不会引发构造和析构成本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FileSystem{};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory{};
Directory::Directory()
{
std::size_t disks = tfs().numDisks();
}

Directory& tempDir()
{
static Directory td;
return td;
}
  1. 这种结构下的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回它。如果频繁调用,可以使用inlining。
  2. 从另一个角度看,这些函数内含tataic对象,在多线程系统中带有不确定性。
  3. 任何一种non-const static对象,无论它是local还是non-local,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有reference-returning函数,这可以消除与初始化有关的“竞速形势”。
  4. 运用reference-returning函数防止“初始化次序问题”,前提是其中有着很一个对对象而言合理的初始化次序。如果有一个系统,其中对象A必须在对象B之前先初始化,但A的初始化能否成功却又受制于B是否已被初始化
  5. 在避免对象初始化之前过早地使用它们
    1. 手工初始化内置型non-member对象
    2. 使用成员初值列对付对象的所有成分
    3. 在初始化次序不确定性氛围下加强你的设计(对不同编译单元定义的non-local static对象是一种折磨)
  6. 对内置型对象进行手工初始化,因为c++不保证初始化它们
  7. 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同
  8. 为避免跨编译单元之初始化次序问题,用local static对象替换non-local static对象

1. 让自己习惯C++
http://binbo-zappy.github.io/2024/11/27/effective-cpp/1-让自己习惯C++/
作者
Binbo
发布于
2024年11月27日
许可协议