文章阅读时间可能会超过1小时
Effective C++ 目录
一、让自己习惯 C++
1、视 C++ 为一个语言联邦
怎么理解这句话,就是不要把C++理解为一个单一的语言,它有很多个语言规则的联邦组成,例如C++ 支持多重泛型编程,既面向过程又面向对象、泛型形式、元编程形式,(看不懂不用管,你只要知道它不单一,有很多知识就行了)
下面从四个方面来说
(1) C++ 以 C 为基础,很多语句语法都没有改变,只是进行了扩展,C++ 解决问题的时候,可以理解为 C 的高效解法
(2) 面向对象,有构造函数,析构函数,继承、封装、多态、虚函数动态绑定,这是不同于C 语言的部分
(3) 泛型编程、可以理解为代码的模型,但是代码本身并不占用内存空间,只有在实例化的时候,才分配内存,所以可以提高代码复用性
(4) STL 模板程序库,是C++ 标准库组件之一,有容器,算法、迭代器、适配器、空间配置器、仿函数。其中空间配置器为容器在内存中分配空间,算法提供了排序、查找、删除等操作;迭代器方便用户进行遍历容器内部的元素,迭代器也可以和算法在一起使用;仿函数也是和算法在一起使用,仿函数是一个类,可以理解为用户自定义规则;适配器是通过接口的一些特性,把它转为另一个容器
然后我们再回头看那句话,C++ 是一个语言联邦,它就是根据这4个次语言组成,每一个次语言都有自己要遵守的规则。
2、尽量以 const、enum、inline 替换 #define
这句话说的是什么?
#define 这玩意不好使,尽量不要使用,因为它是在预处理器执行的,不属于语言的一部分,而 const 、enum、inline 是在编译的时候执行的
那他们之间有啥区别?
举个例子
#define num 1.653
const double num = 1.653;
第一行语句在预处理执行,num 这个记号不会被编译器看见,不会进入符号表,如果把第一行语句放入头文件,那么后序代码有问题需要进行调试的话,别人看你的代码,会对 1.653 很懵逼,谁知道它哪冒出来的,或者你看别人的代码会一头雾水.
还有一个缺点是,#define 进行宏定义的时候容易导致歧义,如果括号使用不当,最后的结果可能不是预想的结果
建议做法:使用一个常量替换宏,例第二行语句,这两行语句意思和目的是一样的
关于这个建议还需要注意两个地方
(1) const 定义一个指针,如果把它写在头文件,需要写两次 const ,例 const int* const a = 10; 这样指针指向和指向的值就会被确定。
(2) const 变量可以使 class 的专属常量,确保它的作用域限制于 class 内,可以加 static 修饰,保证最多提供一个实体;但是我们无法使用 #define 创建一个class 常量,因为 #define 并不是重视作用域,宏一旦被定义,在后面编译过程中有效(除非被 #undef)
最后再补充一点 static 变量一般情况下是在类外定义赋值,类中只是声明一下,但是遇到下面情况怎么办?
class GamePlayer{
private:
static const int NumTerns;
int score[NumTerns];
...
};
编译期间要知道数组的大小,否则编译不通过(有的编译器可以在类内定义的同时可以赋值,但是对于旧编译器不可以在类内定义,这时候怎么办?)
看看条款,我们就知道需要 enum 枚举
class GamePlayer{
private:
static const int NumTerns;
enum{
NumTerns = 5 // 令 NumTerns 成为 5 的记号名称
};
int score[NumTerns];
...
};
这样就可以完美解决问题了
对于 inline 关键是替换宏函数的,因为宏函数没有参数类型检查,出错了也很难追踪到,所以使用 inline 建议编译器把函数定义为内联函数
小总结:使用 const,enum 替换宏常量,使用 inline 替换宏函数
3、尽可能使用 const
使用 const 可以保证一定的安全性
const 有啥作用,使用范围?
(1) 在类外修饰全局变量,常量,或者修饰函数,区块作用域中被声明为 static 的变量
(2) 在类内可以修饰声明为 static 成员变量和非static 成员变量
(3) 面对指针,可以修饰指针本身或者指针所指物
(4) STL 迭代器作用像一个 T* 指针,声明迭代器为 const 就相当于声明指针 T* const ,表示指针的指向不可以改变,但是所指物可以改变,如果是一个 const_iterator 表示所指物不可以改变,即相当于const T* 或者 T const * (二者本身没有区别)
(5) const 修饰函数的时候,可以保证安全性,比如返回值不可以被修改,或者修饰 this 指针,可以保证意外或者恶意修改
未完待补充… 原文中还有许多例子,我只能理解这么多了…
4、确定对象被使用前以被初始
因为读取未初始化值可能发生不明确行为,有的平台可能直接导致程序终止了…
所用永远在对象使用之前初始化它
int x = 0;
const char * text = "A C-style string";
//读取input stream 的方式进行初始化
double d; std::cin>>d;
对于内置类型以外的,可以使用构造函数进行初始化
需要注意的是:构造函数体里面叫赋值,初始化列表中才叫初始化,使用初始化列表比较高效,因为其只调用一次拷贝构造函数,而不需要调用构造函数再调用赋值操作符重载函数.
而且有的变量必须要在初始化列表中进行,比如引用类型,const 类型,因为在构造函数体内的是赋值,因为这些变量不可以被赋值.
所以不管是什么变量,一股脑把它放到成员初始化列表中进行初始化,就好了
注意:成员变量的初始化顺序是和声明顺序一致的,和初始化列表中的顺序没有半毛钱关系.
最后一点,跨编译单元的初始化次序问题,怎么确定?
个人觉得还是挺重要的,举个例子
// 服务器建立
class FileSystem{
public:
// ...
std::size_t numDisk()const; // 成员函数
// ....
};
extern FileSystem tfs; // 准备给客户使用的对象
// 客户建立
class Directory{
public:
Directory(params);
~Directory();
};
Directory::Directory(params){
//...
// 问题就在这一行语句,你怎么确定 tfs 已经被初始化了?
std::size_t disks = tfs.numDisk();
//...
}
// 客户决定创建一个对象来存放临时文件
void main(){
Directory tempDir(params);
}
C++ 对于不同编译单元的对象初始化次序无法得知,但是可以通过一个设计,解决问题,也就是单例模式
解决办法:把非 static 对象设置为 static 成员变量,这样类在创建的时候,就已经初始化了,所以当客户去使用这个对象的时候,是没有风险的!
如下
//=============================================
// 高效做法
class FileSystem{
...};
// 不会引发构造函数
FileSystem& tfs(){
static FileSystem fs;
return fs;
}
class Directory{
...};
Directory::Directory(params){
// ..
std::size_t disks = tfs().numDisk();
//...
}
Directory& tempDir(){
static Directory td;
return td;
}
二、构造、析构、赋值运算
5、了解 C++ 默认编写调用的函数
创建一个类,经过C++编译器处理后,如果自己没有声明,那 C++ 编译器会为其生成默认的构造函数、拷贝构造函数、non-virtual 析构函数、赋值操作符重载,取地址操作和 const 类的取地址操作符。
如果自己声明这些函数,则编译器不会再创建对应的函数,只要当这些函数需要的时候(被调用时)才会被创建出来
为什么编译器要默认创建这些函数,有什么意义,又有什么缺点?
原因1:因为对象的实例化是很常见的事,调用默认函数比较方便
原因2:可以管理栈对象,栈对象的赋值和拷贝构造一般情况不会出大问题,但是对于堆上的对象需要用户自己去创建.
缺点:拷贝构造、赋值操作符这些函数都是浅拷贝的形式,所以当有不符合规则的情况下,编译器禁止调用赋值操作符和拷贝构造,另外如果用户进行 new 申请了空间,一定不要使用默认的成员函数,需要自己创建.
举例:
template<class T>
class NameObject{
public:
// 注意这是一个引用类型
NameObject(std::string& name,const T& value){
}
private:
std::string& name;
const T objectValue;
};
int main(){
std::string newDog("Persphone");
std::string oldDog("Starch")
NameObject<int>p (newDog,2);
NameObject<