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

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

时间: 2024-08-03 06:19:01

《Effective C++ 读书笔记》( 一 )的相关文章

《三层理论篇》一

一.联系生活: 先从我们的生活场景说起,我们要吃烧烤,一种情况是在路边,有人摆摊,我们要完东西之后就在这看着等着烤好了,然后拿着开始吃,第二种情况是我们到一个小店只要坐下来有人过来问你想吃什么,你说完之后过会就会有人把你要的东西送过来(这两种场景估计大家都有过哈) 两种场景的烤肉过程: 我们生活中的这种烤肉现象就对应这我们系统的开发的两种方式,二层和三层(多层). 路边烤肉一个人什么都干,总有忙不过来出错的时候,当他积累到一定的钱之后可能就会去开店,这样就能满足更多的顾客. 从路边烤肉到开店的过

《三层理论篇》二

四.三层之间的联系 依赖:三层之间的关系 数据访问层的类:直接访问数据库,实现对基本记录的增删改查操作. 业务逻辑层的类:用相关的数据访问类,实现用户所需功能. 界面层:部署控件后,调用业务逻辑层的类,实现功能. 图解: 实体类:层之间的数据传输对象 为什么选择实体类作为三层之间的传输对象?  ORM(对象关系映射) Object Relational Mapping,简称ORM,是为了解决面向对象的类,与关系数据库的表之间,存在的不匹配的现象,通过使用描述对象和关系之间映射的元数据,在程序中的

三层学习------理论篇

学校放假了,刚回家的孩子就像个客人被父母招待着.在放假的前几天里,你尽管开口,想吃啥爸妈都会满足你,不过好景可不长!在我家,厨房是老妈的地盘,买菜.做饭.洗碗刷锅,一个人全包了.而在饭店吃饭呢,吃饭的人多了,顾客点的饭菜种类各不相同.前前后后,一个人忙乎,哪里顾得过来,所以饭店就有了分工.前台服务员负责将顾客点的菜上报给厨师和:厨师根据上报的菜单做菜:采购员负责柴米油盐酱醋茶.这样,大家各司其职,井井有条. 我们在家中吃饭比较简单,没有具体的分工.饭店就是一个复杂庞大的系统了,需要合理规划,分工

三层架构-------理论篇

概念: 通常意义上的三层架构就是将整个业务应用划分为:表现层(UI).业务逻辑层(BLL).数据访问层(DAL).区分层次的目的即为了"高内聚,低耦合"的思想. 各层概念 1.表现层(UI):通俗讲就是展现给用户的界面,即用户在使用一个系统的时候他的所见所得. 2.业务逻辑层(BLL):针对具体问题的操作,也可以说是对数据层的操作,对数据业务逻辑处理. 3.数据访问层(DAL):该层所做事务直接操作数据库,针对数据的增添.删除.修改.查找等. 注:应用三层离不开另一个重要的类:实体类,

三层结构——理论篇

为什么要分层? 1.开发人员可以只关注整个结构中的其中某一层:2.可以很容易的用新的实现来替换原有层次的实现:3.可以降低层与层之间的依赖:4.有利于标准化:5.利于各层逻辑的复用.6..方便团队分工 分层: 将整个业务应用划分为:表现层(UI).业务逻辑层(BLL).数据访问层(DAL).区分层次的目的即为了"高内聚,低耦合"的思想. 1. 表现层 位于最外层(最上层),离用户最近.用于显示数据和接收用户输入的数据,为用户提供一种交互式操作的界面.它是系统的UI部分,负责使用者与整个

三层架构理论篇

对于三层架构的理论阐述,我将从三个大的方面去讨论:what.why和how,说白了也就是以三层架构为中心,去了解什么是三层,为什么用三层以及怎么用三层这个三个问题.OK,废话不多说,进入正题. 什么是三层架构?(What) 通常多层结构的划分方式有两种:分别是物理和逻辑.物理上的三层结构是指将整个应用系统分为显示层.业务层和数据层,并且这三个层面上的实体都是硬件,比如显示层就是客户机器,业务层通常是应用程序服务器,而数据层就是数据库服务器了. 今天我们讨论的主要是逻辑上的三层架构,是在软件设计时

【SSH2(理论篇)】--Struts2配置详解

上篇博客讨论了SSH2框架模型,在开发过程中发现SSH2的开发模型其实类似于经典的三层模式,在每一层中分别添加了不同的框架,显示层使用的是Struts2进行配置的,业务逻辑层使用的是Spring配置,数据持久层则采用的是Hibernate,开发模式简单易懂,接下来将会分别从三层着手讨论每一层的运行内容. 一.Struts体系简介 struts,是Apache软件基金会(ASF)赞助的一个开源项目,它通过采用Java Servlet/JSP技术,实现了基于Java EE Web应用的Model-V

【机器学习】Logistic Regression 的前世今生(理论篇)

Logistic Regression 的前世今生(理论篇) 本博客仅为作者记录笔记之用,不免有非常多细节不正确之处. 还望各位看官能够见谅,欢迎批评指正. 博客虽水,然亦博主之苦劳也. 如需转载,请附上本文链接,不甚感激! http://blog.csdn.net/cyh_24/article/details/50359055 写这篇博客的动力是源于看到了以下这篇微博: 我在看到这篇微博的时候大为触动,由于,如果是rickjin来面试我.我想我会死的非常慘,由于他问的问题我基本都回答不上来.

Logistic Regression 的前世今生(理论篇)

[机器学习]Logistic Regression 的前世今生(理论篇) 本博客仅为作者记录笔记之用,不免有很多细节不对之处. 还望各位看官能够见谅,欢迎批评指正. 博客虽水,然亦博主之苦劳也. 如需转载,请附上本文链接,不甚感激! http://blog.csdn.net/cyh_24/article/details/50359055 写这篇博客的动力是源于看到了下面这篇微博: 我在看到这篇微博的时候大为触动,因为,如果是rickjin来面试我,我想我会死的很惨,因为他问的问题我基本都回答不上

三层理论

什么是三层,是说的像房屋那样的三层么?当然不是啦,我这里要给大家说的三层是软件体系架构设计中的一种分层方式. 三层架构:通常意义上的三层架构就是整个业务应用划分为:表现层(UI).业务逻辑层(BLL).数据访问层(DAL).区分层次的目的就是为了"高内聚,低耦合"的思想. 三层概念简介: 1.显示层(UI):通俗讲就是展现给用户的界面,即用户在使用一个系统的时候他的所见所得. 2.业务逻辑层(BLL):针对具体问题的操作,也可以说是对数据层的操作,对数据业务逻辑处理. 3.数据访问层(