写在前面
这是对Effective C++这本书中的部分内容进行的总结以及代码实践,主要是记录一些对我印象深刻的,确实能改善程序的内容,和有需要实践验证加深印象的一部分实践和我自己的理解
01:视C++为一个语言联邦
C++发展许久一直到今天,已经是一个很丰富并且很成熟的语言,有很多冒险大胆的做法,要把C++看成一个联邦而不是单独的一个语言,联邦可以由很多部分组成,例如在C++中有
- C
- Object-Oriented C++:包括但不限于构造析构,封装,继承多态等等
- Template C++
- STL
你可以把C++看成是一个由四个次语言组成的联邦
C++高效编程手册视情况而变化,取决于用哪一部分
02:尽量用const enum inline替换#define
使用const的场景和好处:
使用#define有诸多不便,例如:
- 在编译的预处理阶段,会把#define定义的内容全部进行替换,也就是说define的内容其实根本就不在记号表中,因此如果此时出现了错误,调试信息是一个巨大的成本,十分不方便,而如果采用const常量来代替宏就很方便
- 如果使用#define,在预处理会全部被替换,因此就会导致目标码中出现多份同样的数字,而如果使用常量就只会在记号表中留下一份数据
// 宏定义有诸多不便 #define rate1 0.8 // 才用const常量可以有效解决很多不便 const double rate2 = 0.8;
- #define定义的内容的作用域大概率是全局,会直接进行全局的替换,除非使用其他预处理指令,也就是说#define是不可以被封装,也不可以在class中定义专属常量,而你只需要在class中使用static const就可以声明一个常量,在新标准中甚至可以给值,这样如果不到特殊场景你都可以不需要定义,直接使用它的声明即可
使用enum的场景和用处
假设有下面的代码:
class A { private: static const int _num = 10; int _proinf[_num]; };
在有些编译器中,编译器必须要知道数组的大小,而如果编译器此时不支持const常量作为数组大小,这时你可以选择enum作为替代方法:
class A { private: //static const int _num = 10; enum { _num = 5 }; int _proinf[_num]; };
这样的操作是被允许的
inline代替宏函数
inline可以代替宏函数,好处在这里不进行说明了~
对于单纯的常量,最好用const和enum代替它
对于宏函数,用inline代替它
03:尽可能使用const
对于const修饰指针前后的问题较为简单,这里主要写一个新场景
给函数的返回值加const:
const Rational operator* (const Rational& lhs, const Rational& rhs);
这个函数返回了一个Rational类型的值,这是已经定义好的,问题在于这里为什么前面要加const?
其实是为了防止这样情况出现
Rational a, b, c; ... (a*b) = c;
显然如果有了const,这里就会报错,因为常量是不能被修改的,这里也很好的说明了const的功能,在有些地方可以很好的避免出现奇怪的错误
const成员函数
这里涉及一些简单的权限问题,const是常量,而常量不能调用普通成员函数,这涉及到权限的扩大,因此只能调用const成员函数
const如果作为成员函数,那么就会引入两个概念,bitwise constness和logical constness,分别译为二进制位常量性和逻辑常量性
bitwise constness:
这个概念的意思是说,不改变对象内的任何一个bit,就是const,这也正是C++对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量
但这个概念是有些问题的,假设类中的成员是一个指针,而这个指针也参与到了const成员函数中,返回的是引用
class CTextBlock { public: CTextBlock( char* p) { pText = p; } char& operator[](std::size_t position) const { return pText[position]; } private: char* pText; }; int main() { char str[] = "hello"; const CTextBlock cctb(str); char* pc = &cctb[0]; *pc = 'j'; cout << str; return 0; }
因此对于外界来说,你创建了一个常量对象并给了值,而且也只对它用const成员函数,但最后却能通过指针改变这当中的内容,于是就引出了logical constness
logical constness:
这个概念是说,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不到的地方才能这样
于是引入了mutable的概念,这个关键字可以作用在类的成员内,使得它们可以在const函数中被修改,而不是意味着规定const函数内的成员都不能被有任何bits的更改,事实上也不能完全保证
在const和non-const成员函数中避免重复
简单来说,这个修改程序的思想就是对于const成员函数和普通成员函数且这两个函数构成重载,如果两个函数实现的功能基本相同,那么就可以让普通成员函数通过一些加const和摘除const的方式,也用const函数完成函数本身的功能,这样的好处在于避免代码重复,但要时刻理解保持权限的概念
将某些东西声明为const可以帮助编译器侦测出错误的用法
const可以被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体
编译器本身是强制进行bitwise constness,但是可以使用logical constness
当const和non-const成员函数有实质性的等价实现时,可以避免代码重复
04:确认对象被使用前已先被初始化
这里提及的大部分是前面的一些基础内容:
为内置类型进行手工初始化,编译器不会对内置类型初始化
构造函数最好用参数初始化表,最好不用赋值,参数初始化表的顺序和private中相同
为避免跨编译单元之初始化次序的问题,要用local static代替non-local static对象
下面重点介绍:跨编译单元之初始化次序是什么
首先,要先知道extern这个关键字是干什么的
- 如果定义于函数,那么可以使b.cpp中用a.cpp的函数进行使用
- 如果定义于变量,可以使b.cpp中用a.cpp定义的变量
因为有上面的概念,因此才会出现跨编译单元的问题
现在我们利用上面的第二条来看问题,现在假定,在a.cpp文件中我们定义了变量并用extern修饰,而在b.cpp文件中直接对它进行使用了,那么问题来了,如果初始化的次序并非先a.cpp,再b.cpp,那岂不是就进行了一次使用未初始化的内容?这是一个有风险的操作
解决方案是,把non-local static对象搬到自己的专属函数内,就可以解决这个问题,原因在于,C++保证,函数内的local static对象会在该函数被调用的期间,首次遇到该对象的定义式的时候进行初始化,这意味着只要你走到这个static对象,这个对象其实是必然被初始化过的,因此用这样的解决方案,可以以函数调用的形式,保证你获得的那个引用一定是指向的被初始化过的内容