《Effective C++ 读书笔记》( 一 )

时间:2014-08-27 16:37:18   收藏:0   阅读:353

《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对象。

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!