《Effective C++ 读书笔记》 条款01 : 视C++为一个语言联邦
将C++ 视为一个由相关语言组成的联邦而非单一语言。
我们可以将C ++ 看成四个部分 :
1. C 语言 。 C ++ 中的区块 , 语句, 预处理器 , 内置数据类型 , 数组 , 指针等统统来自于C语言。
2. 面向对象的 C ++ 。 类( 包括构造函数和析构函数 ) , 封装 , 继承 ,多态 , virtual 函数 ( 动态绑定 ) ... 等等 。 这一部分都是基于面向对象而设计的。
3. Template C ++ 。 这是C++泛型编程的部分 。
4. STL 。STL是一个template库 , 包含了容器,迭代器,算法以及函数对象。
C++ 高效编程守则视状况而变化 , 取决于你使用 C++ 的哪一个部分。
《Effective C++ 读书笔记》 条款02 : 尽量以 const , enum ,inline 代替 #define
1. 用 const 来定义常量 代替 #define 定义常量 , 因为使用const 编译器可以更好的帮助你
比如 #define ASPECT_RATIO 1.653
因为 #define 是在预处理阶段就将 ASPECT_RATIO 代替为 1.653 , 所以编译器从未看见过 ASPECT_RATIO 这个符号
当因为这个常量而发生编译错误的时候 ,我们只能得到关于 1.653 的相关信息 , 若你对 1.653 这个数字毫无概念 或者 这个常量是来自他人的头文件 , 那么你调试的时候就会无从下手
因此我们应该利用 const double AspectRatio = 1.653 ;
以此就可以避免这种情况的发生
2. class 专属常量应该定义为 static const
不使用#define 是因为#define不重视作用域 ,这样既不能用来定义class的专属常量 , 也不能用来提供任何封装性,也就是说 #define 不可能拥有private 属性
用 static 的原因是让整个类共享一个实体,而不是每个对象都拥有一个实体
// 另外注意 static 对象的初始化 class GamePlayer{ private: static const int Num = 5 ; // 这里是声明一个 static const 类型的变量 // 部分编译器,只允许 整型 的static常量在类内初始化 // 甚至部分编译器连 整型的static常量都不行 int score[Num] ; ... }; // 定义 class 专属常量 , 若不在类内初始化 ,这里需要赋初值 , 并且不能标示为 static const int GamePlayer::Num ;
3. 可以用 enum 定义常量
如上述注释中所提 , 有些编译器甚至不允许整型的static常量在类内初始化 , 那么就不能用这个static常量来初始化类内的数组了( 因为编译器必须在编译期知道数组的大小 )
ps : 上面这个括号里面是直接抄书上的 , 这么说在类外初始化static常量是运行时才完成的么? 搞不懂啊,太弱了啊,求走过路过的大神帮忙解答下啊
所以我们可以用 enum 来代替
// 那么上述类可以改成如下 : class GamePlayer{ private: enum{ Num = 5 } ; int score[Num] ; ... };
并且 enum 比 const 更想 #define ,比如和#define一样 , 不能对其取地址 , 不能用引用或者指针指向它
4. 用 inline 来代替 #define 的宏
首先,先看看 #define 定义的宏有何缺点
MAX( a , b ) (a) > (b) ? (a) : (b)
这是一个求最大值的宏
第一 , 宏在一定情况下效率会比较低 , 假设有一个比较复杂(运行时间可能比较长)的函数 f( int ) ;
那么你使用 MAX( f(1) , f(2) ) 效率直接就可以呵呵了
第二 ,容易造成意想不到的问题
比如 :
int a = 5 , b = 0 ; MAX( ++ a , b ) ; // a 自加了两次 MAX( ++ a , b + 10 ) ; //a 自加了一次
这种不确定性的情况明显不是我们想要的
当然了,还有一些情况比如说加括号不仔细之类的,有可能会因为运算符优先级之类的得到莫名其妙的结果。
所以,用#define 来实现宏还是有很多缺陷的,我们应该用 inline 函数来代替之
// 对于#define是类型无关的这个特性,可以利用模板函数来实现 template <typename T> inline T ( const T & a , const T & b ) { return a < b ? a : b ; }
同样 , 你不能利用一个 #define 来实现class 的private 函数 , 用inline 就可以
请记住 :
对于单纯常量 , 最好以 const 对象或enums 代替 #define
对于形似函数的宏 , 最好改用 inline 函数代替#define
《Effective C++ 读书笔记》 条款03 : 尽可能使用 const
关键字const的作用有很多 , 你可以用她在classes外部修饰global或者namespace 作用域中的常量 , 或修饰文件、函数、或区块作用域中被声明为static的对象 。你也可以指出指针自身、指针所指物,或者两者都(或者都不)是const。
char greeting[] = "Hello" ; char * p = greeting ; // non-const pointer , non-const data const char * p = greeting ; // non-const pointer , const data char * const p = greeting ; // const pointer , non-const data const char * const p = greeting ; // const pointer , const data
如果 const 出现在 * 右边 , 那么代表指针本身是常量 ,如果 const 出现在指针左边 , 表示指针所指的是常量
const最具威力的用法是在函数声明时的应用。一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。
用const来修饰函数返回值
若函数的返回值不可能是成为左值的时候,我们可以将函数的返回值设为 const 来降低产生错误的几率( 或者说应该是不小心手残了编译器可以帮帮你 )
比如 :
class Rational { ... } ; const Rational operator * ( const Rational & lhs , const Rational & rhs ) ;
不返回一个 const 的话 , 这样的代码也是可以编译通过的
Rational a , b , c ; ... ( a * b ) = c ;
但我们给 a * b 的结果赋值完全没必要啊 , 因为 a * b 的结果就是个临时变量 , 就算赋了值也没啥用处
其实只是想些比较,漏了一个等号
if( ( a * b ) = c ) if( ( a * b ) == c )
如果你返回值是const , 那么这样手残的行为编译器就帮你找出来了
否则 ... 慢慢调吧
const 成员函数
const 成员函数的使用基于两个原因 :
第一个是,他们是 class 接口比较容易被理解。因为比较容易的得知那个函数可以改动对象内容而哪个函数不行。
第二个是,可以用来操作const 对象 , 比如下面这个例子
class Point{ public : Point() : a(0) , b(0) {} Point( int _a , int _b ) : a(_a) , b(_b) {} int getA() const { return a ; } private: int a , b ; }; int main(){ const Point a ( 1 , 2 ) ; a.getA() ; return 0 ; }
如果 getA 不用const修饰的话 ,对于常量 const Point 就会发生编译错误
而我们通常又无法避免对常量对象进行操作 , 比如 函数传参的时候会经常传 const T & , 如果传进来连个get函数都不能用,那传个蛋
因此如果确定这个函数不会改变对象的成员变量的话,那就用const修饰吧
在 const 和 non-const 成员函数中避免重复
另外两个成员函数仅仅常量性不同也是可以形成重载的 , 比如 :
class TextBlock{ public : ... const char & operator[]( std::size_t position ) <span style="color:#FF0000;"><strong>const </strong></span>{ ... // 边界检查 ... // 志记数据访问 ... // 检验数据完整性 return text[position] ; } char & operator[]( std::size_t position ) { ... // 边界检查 ... // 志记数据访问 ... // 检验数据完整性 return text[position] ; } };
这样两个函数的重载是可行的
然后问题又来了 , 这里两个函数基本上的操作都是相同的,这样的重复代码是很糟糕的
这里有一种比较好的方法解决这个问题 , 就用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 const_cast<char&> ( static_cast<const TextBlock&>(*this)[postion] ) ; } };
这里做了两种转化 ,先是使用 static_cast 将 (this) 也就是本对象转成 const 的对象 , 然后调用 operator[] 。这是调用的就是 const 版本的operator [] , 调用完之后 , 使用 const_cast 将返回的 const char& 的const 属性去掉。
请记住 :
1.将某些东西声明为const可帮助编译器侦测出错误用法.const可被施加于任何作用域的对象,函数参数,函数返回类型,成员函数本体.
2.编译器强制实施bitwise constness,但你编写程序时应该使用"概念上的常量性"(conceptual constness);
3.当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复
《Effective C++ 读书笔记》 条款04 : 确定对象被使用前已先被初始化
关于 "将对象初始化" 这事 , C ++ 似乎反复无常 。比如对于内置变量来说,定义在定义在堆内存,会被初始化,定义在栈内存就不会被初始化。
总之 , "对象的初始化动作何时一定发生 ,何时不一定发生" , 这个规定非常复杂 , 那么最好的方法就是永远记得使用之前初始化
内置数据类型的初始化
只能是手工初始化 , 如 :
int x = 0 ; // int 类型初始化 const char * text = "Hello World" ; // 对指针进行手工初始化 double d ; // 用输入的方式初始化 cin >> d ;
对象的初始化( 利用构造函数 )
规则很简单 : 确保被一个构造函数都将对象的每一个成员初始化
首先 , 我们应该区分初始化和赋值的区别
class People{ public : People(){} People( string _name , int _age ) ; private : string name ; int age ; }; // 这种形式是初始化 People::People( string _name , int _age ) : name(_name) , age(_age) {} // 这种形式是赋初值 People::People( string _name , int _age ) { name = _name ; // 这样只是赋值 , 括号开始说明初始化已经结束了,这里是做一些初始化之后的工作 age = _age ; }
第一种方式更加高效 , 直接调用的构造函数,而第二种方法要调用赋值构造函数
C++ 成员初始化的顺序为 : 基类对象先初始化 , 然后是派生类的成员变量 , 派生类的成员变量初始化的顺序和声明顺序相同
不同编译单元内定义之non-local static 对象的初始化
所谓static对象,其寿命从被构造出来直到程序结束为止,因此stack和heap-based对象都被排除。这种对象包括global对象、定义于namespace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
问题出在两个编译单元都有一个非non-local static对象 , 并且必须用一个对象去初始化另外一个对象 , 这样的编译器就无法控制其先后顺序了
// FileSystem.cpp class FileSystem{ public : ... std::size_t numDisks() const ; ... }; extern FileSystem tfs ; // Directory.cpp class Directory{ public : Directory(){ ... std::size_t disks = tfs.numDisks() ; ... } ... private: ... std::size_t disks ; }; extern Directory tempDir ;
从上述代码可以得到 , Directory 对象能否初始化成功 , 取决于 tfs 是否已经初始化
因此 , 我们可以用单件模式 ,具体操作是 , 将 tfs 和 temp Dir 转化为 local static 对象 , 改成如下即可
// FileSystem.cpp class FileSystem{ public : ... std::size_t numDisks() const ; ... }; FileSystem & tfs() { static FileSystem fs ; return fs ; } // Directory.cpp class Directory{ public : Directory(){ ... std::size_t disks = tfs.numDisks() ; ... } ... private: ... std::size_t disks ; }; Directory & tempDir(){ static Directory td ; return td ; }
请记住 :
1. 为内置型对象进行手工初始化,因为C++不保证初始化它们。
2. 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
3. 为免除"跨编译单元之初始化次序"问题,请以local static对象替换non-local static对象。