《Effective C++》 读书笔记(详细总结55条款)上篇

文章阅读时间可能会超过1小时

Effective C++ 目录

    • 一、让自己习惯 C++
      • 1、视 C++ 为一个语言联邦
      • 2、尽量以 const、enum、inline 替换 #define
      • 3、尽可能使用 const
      • 4、确定对象被使用前以被初始
    • 二、构造、析构、赋值运算
      • 5、了解 C++ 默认编写调用的函数
      • 6、若不想使用编译器自动生成的函数,就该明确拒绝
      • 7、为多态基类声明 virtual 析构函数
      • 8、别让异常逃离析构函数
      • 9、绝不在构造和析构过程中调用 virtual 函数
      • 10、令 operator= 返回一个引用指向 const 类型的 this 指针
      • 11、在 operator= 中处理 "自我赋值"
      • 12、复制对象时勿忘其每一个成分
    • 三、资源管理
      • 13.以对象管理资源
      • 14、在资源管理类中小心 copying 行为
      • 15、在资源管理类中提供对原始资源的访问
      • 16.成对使用 new 和 delete 时要采取相同的形式
      • 17、以独立的语句将 newed 对象置入智能指针
    • 四、设计与声明
      • 18、让接口容易被正确使用,不易被误用
      • 19、设计 class 犹如设计type
      • 20、const 引用传递替换值传递
      • 21、必须返回对象时,不要妄想返回引用
      • 22、将成员变量声明为 private
      • 23、以 non-member、non-friend 替换member
      • 24、若所有参数皆需要类型转换,请为此采用non-member 函数
      • 25、考虑写出一个不抛异常的 swap 函数

一、让自己习惯 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<int>s (oldDog,36);
	p = s; // 注意这一行语句是编译不过的
}

赋值之前,p.nameValue 和 s.nameValue 都是指向 string 对象的,赋值之后,引用的指向发生改变,这是不符合常理的,而且当前对象 Objectvalue 也是 const 类型,所以在这种情况下是不允许赋值的

6、若不想使用编译器自动生成的函数,就该明确拒绝

方案1:可以把生成的函数权限设置为私有的,并且只给声明不进行实现。

方案2:利用一个 base class 类,然后让指定类继承这个 base 类,这个类写法如下

class UncopyAble{
protected:
	UncopyAble(){

	}
	~UncopyAble(){}
private:
	UncopyAble (const UncopyAble& u);
	UncopyAble& operator=(const UncopyAble& u);
};

class HomeForstyle:public UncopyAble{
	//...
};

当尝试拷贝 HomeForstyle 对象的时候,编译器会调用基类的拷贝构造和赋值运算符函数,由于是 private 所以会被拒绝。

7、为多态基类声明 virtual 析构函数

如果一个类可能在未来的某一时间会作为基类被继承或者带有多态性质,那么应该把析构函数设置为虚函数,因为会有内存泄漏的危险!

举例:当一个基类指针对象,指向一个派生类动态分配的对象时,当进行释放内存的时候,基类只会调用自己的析构函数释放栈对象资源,但是并没有调用派生类析构函数释放堆资源。

但是当一个类不准备继承的话,最好不要把析构函数设置为虚函数,因为会占用空间,虚函数内存放函数的入口地址,还有指向虚表的虚表指针也会占用空间,这也就是为什么默认的析构函数不是虚函数的原因了.

8、别让异常逃离析构函数

什么意思呢?
就是析构函数你不要吐出异常,即使有可能抛出异常,析构函数应该捕捉到异常并且处理吞掉它,不要进行传播

举例:

//=========================================
// 析构函数抛出异常
class DBconnection{
public:
	//...
	// 这个函数返回DBconnection 对象
	static DBconnection creat(){
	}

	// 关闭连接,失败抛出异常
	void close(){

	}
};

class DBConn{
public:
	//...
	~DBConn(){  // 这样就可以确保数据库关闭被调用到
		db.close();
	}
private:
	DBconnection db;
}

如果可以成功调用 close 函数还好说,如果失败就会从析构函数中抛出异常,DBCon会传播异常,如果允许异常离开析构函数就不好驾驭了,而且有可能导致程序异常退出或者不明确行为

怎么解决?

(1) 在析构函数进行捕捉异常

DBConn::~DBConn(){
	try{
		db.close();
	}
	catch(...){
		//分析,并且记录下来
		std::abort(); //进行中断
		// 如果这一步使程序结束,那么也是合理的,
		// 毕竟可以阻止异常被抛出带来不明确行为
	}
}

(2) 对异常不做任何处理,直接吞掉,虽然这并不太好,但是总比让程序直接结束带来的不明确结果要好很多!

(3) 比较好的做法是,重新设计一下DBcon接口,让用户有机会,有时间对可能出现的问题作出反应,举例:


class DBConn{
public:
	//...
	void close(){
		db.close();
		close = true;
	}
	~DBConn(){
		if(!close){
			try{
				db.close();
			}
			catch(...){
				// 制作运转记录,记下对 close 调用的失败
			}
		}
	}	
private:
	DBconnection db;
	bool closed;
};

把调用 close 失败这个操作,从析构函数移到客户手上,如果一个操作可能会有异常,应该是由非析构函数抛出,这样一来,客户就多一次机会去处理这个异常,如果觉得这个异常无伤大雅,完全可以忽略,如果造成后果,客户也就没有抱怨的权利,因为给过你一次机会,你却把它忽略了…

9、绝不在构造和析构过程中调用 virtual 函数

因为这样做不会带来预想结果

举个例子就明白了

//==================================
// 构造函数中不能放析构函数
class Transaction{
public:
	Transaction();
	virtual void logTranction()=0;
	//...
};
Transaction::Transaction(){
	...
	logTranction();
}

class BuyTranction:public Transaction{
public:
	virtual void logTranction()const;
};

class SellTranction:public Transaction{
public:
	virtual void logTranction()const;
} ;

int main(){
	BuyTranction B;
}

当调用 BuyTrancion 构造函数的时候,一定会先调用基类的构造函数,然后发现构造函数中是有虚函数,这个时候不会调用子类重写的虚函数,因为子类对象还没构建好,这个时候调用的基类本身的函数,所以和预想完全不一致

当然这个只是潜在原因,下面才是本质原因

当派生类对象没有构造器初始化的时候,类型其实是一个基类类型,并不是派生类类型,所以没办法调用派生类虚函数

对于析构函数也是一样的,当派生类的析构函数开始执行时,对象内的派生类成员变量呈现未定义值,C++ 会把他们忽略掉,当进入到 base 类中,对象才会变为基类对象

10、令 operator= 返回一个引用指向 const 类型的 this 指针

(1) 为什么要返回引用?
避免出现这种情况,(x = y) = z ; 因为会先调用 x == y,如果不是引用类型而是值类型,则右边会是一个右值,C++不允许改变右值

(2) 为什么返回的是类类型,而不能是void 类型
避免 z = y = x =15; 这种情况,x 赋值给 y 后返回的是一个 void 类型,那它还怎么给 z 赋值啊!

(3) 为什么参数是 const ?
防止用户恶意修改

当然如果返回类型不是引用指向当前对象编译器也不会报错,只是不符合赋值的规则

11、在 operator= 中处理 “自我赋值”

//==========================
class weight{...};
weight w;
...
w = w; // 自己给自己赋值
w[i] = w[j] // 自己给自己赋值

自己给自己赋值,当然这种情况也是由可能发生的,如果发生那么也会造成严重后果

举例:

class Bitmap{...};
class weight{
public:
	weight& operator=(const&weight rhs);
private:
	Bitmap* pb;
} 

weight & weight::operator = (const weight& rhs){
	// 假如此时 rhs 就是 this 指针
	delete pb;
	pb = new Bitmap(*rhs.pb); // 这一步就会出错,因为上一行代码已经释放内存了
	return *this;
}

正确做法是首先进行判断,它是不是当前 this 指针,如果是的话不做任何处理

weight&  weight::operator = (const weight& rhs){
	if(&rhs == this) return *this;
	Bitmap* pOrig = pb;
	pb = new Bitmap(*rhs.pb);
	delete pOrig; // 释放原来的 pb
	return *this;
}
weight&  weight::operator = (const weight& rhs){
	if(&rhs == this) return *this;
	weight temp(rhs);
	swap(temp);
	return *this;
}

12、复制对象时勿忘其每一个成分

当进行赋值或者拷贝构造的时候,不要只拷贝类的一部分成员

实例代码

//===============================
class Date{};
void logCall (const std::string& funcName);
class Custom{
public:
	...
	Custom(const Custom& rhs);
	Custom& operator=(const Custom& rhs);
	...
private:
	std::string name;
	Date date;
};

Custom::Custom(const Custom& rhs):name(rhs.name){
	logCall("Custom copy constructor");
}

Custom& Custom::operator=(const Custom& rhs){
	logCall("Custom copy assignment operator");
	name = rhs.name;
	return *this;
}

Date 对象没有被拷贝和赋值,而且编译器还不会报错,因为你没用人家的默认函数,出了责任也是你自己承担…

所以我们添加一个新成员后就要修改拷贝构造函数和赋值操作符,如果在继承的时候,需要将指定形参传递给基类构造函数,因为基类的成员会继承到子类(其实是基类的副本),如果不这样做就是未初始化行为,风险参考条款4

三、资源管理

13.以对象管理资源

不把资源交给对象管理,就得程序猿自己去管理,那多麻烦,最关键的时候,资源使用完必须释放,谁知道程序猿有没有忘记释放,如果忘记就可能内存泄漏,把资源交给对象管理,可以保证一定释放资源.

说白了,就是智能指针,利用C++析构函数调用释放资源的函数

(1) auto_ptr 是一个类指针对象,析构函数自动对其对象进行 delete ,实例化时需要指定类型,比如auto_ptr ap(createInverment());

其中 createInverment() 是申请资源函数

资源在获得的同时被放进管理对象中,管理对象运行析构函数释资源

注意:auto_ptr 被销毁后,自动删除所指向的资源,不要让多个 auto_ptr 指向同一对象,否则对象会被删除多次产生未定义行为,但是 auto_ptr 为了预防这个问题,在进行拷贝构造或赋值的时候,他们会变成 null ,管理对象权力转移到被赋值的对象上,旧对象无法继续使用,这也是 auto_ptr 的缺陷

可以通过 引用计数型指针shard_ptr 替换auto_ptr, 它也是一个智能指针,底层会对引用该资源的对象计数,当没有对象指向的时候自动删除资源,类似垃圾回收站

但无法打破环状引用,也就是循环引用,例如两个没有被使用的对象互相指向,给编译器的感觉是好像还在使用,这个时候对象计数没办法进行,资源无法得到释放,解决办法:weak_ptr 指针

最后:不管是auto_ptr 还是 shared_ptr 在进行析构函数调用的时候,动作是 delete ,而不是 delete[] ,所以在动态分配的数组身上不要使用 auto_ptr 以及 shared_ptr

14、在资源管理类中小心 copying 行为

比如我们需要建立自己的资源管理类,使用C语言apl,unlock 和 lock 两种函数,为了确保锁会被释放,可以把它将给对象管理,在构造期间获得资源,析构期间释放

实例代码

class Lock{
public:
	explicit Lock(Mutex* pm)
		:mutexPtr(pm){
		lock(mutexPtr);
	}
	~Lock(){
		unlock(mutexPtr);
	}
private:
	Mutex* mutexPtr;
};

客户端

{
	Mutex m;
	Lock m1(&m);
	...
} // 这里会自动释放资源

但是如果 m1 被拷贝了怎么办,例 Lock m2(m1);

有的编译器会禁止拷贝,有的编译器拷贝底层资源(深拷贝),有的转移底部资源权(auto_ptr )

但是最常见的行为是实施引用计数法,只要内含一个 shared_ptr 成员变量,在使用的时候加一个删除器(因为shared_ptr 的结束不是解锁,而是删除),指定删除器我们可以对资源进行解锁释放

class Lock{
public:
	explicit Lock(Mutex* pm)
		:mutexPtr(pm,unlock){ // 以 unlock 函数为删除器
		lock(mutexPtr.get());
	}
private:
	std::trl::shared_ptr<Mutex>mutexPtr;
};

当引用计数为 0 的时候,mutexPtr 析构函数会自动调用 unlock 删除器.

15、在资源管理类中提供对原始资源的访问

智能指针本身就是把一个指针封装为一个类,然后实现了一个指针的行为,所以在资源管理类中也会提供一些函数,返回它的原始指针

因为有些接口参数就是类指针形式,需要把当前对象转换为底层指针,才可以调用接口

比如

std::trl::shared_ptr<Inverstment>pInv(createInvestment())
int daysHeld(const Inverstment* pi);

接口参数类型是一个 Inverstment 类型的指针,我们如果直接传对象编译器报错,daysHeld(pInv);

显示转换更加安全,直接在对象上调用 get 函数
int days = daysHeld(pInv.get()); // 将原始指针传给 daysHeld

16.成对使用 new 和 delete 时要采取相同的形式

使用 new 就用 delete 释放,使用 new [] 就得使用 delete [] 释放

这个看似好理解,深挖起来也够呛

当我们使用一个 new 时,有两件事发生,一件事是内存被分配出来,第二,针对此内存会有一个构造函数被调用,来初始化这块内存,使用delete 也会发生两件事,第一是调用析构函数,然后再释放内存,对于delete 来说,它还要明确一件事就是:究竟有多少对象需要被释放,我要调用多少次析构函数

如果我们使用 new[] 开辟内存,使用 delete 释放会导致有的内存并没有被释放,因为 new 调用了多次构造函数,而 delete 只调用了一次

如果我们调用 new 开辟内存,却调用 delete [] 释放又会怎么样?

首要要明确 delete [] 中的’方括号’,编译器是怎么解释的,编译器如果发现 delete 后面有 [] 就会认为这是一个对象数组,不是单一的对象,再调用析构函数之前访问对象内存前面的若干字节,然后根据字节决定调用多少次析构函数,但是注意:只有对象数组前面才有对应的字节数,在单一对象前面访问,会得到一个随机值,结果可想而知,本来需要调用一次析构函数,但是可能会把其它地方的内存释放掉.
《Effective C++》 读书笔记(详细总结55条款)上篇_第1张图片

17、以独立的语句将 newed 对象置入智能指针

乍一看,有点懵,举个例子就明白了…

//=================================
int priority();
void processWidegt(std::trl::shared_ptr<Widegt>pw,int priority);

// processWidegt 决定对其动态分配的内存运用智能指针
// 方法一调用
processWidegt(new Widegt,priority());

// 方法二调用
processWidegt(std::trl::shared_ptr<Widegt>(new Widegt),priority());

上述方法一其实会编译报错,shared_ptr 构造函数需要一个原始指针,但是构造函数是一个 explicit 函数,不可以隐式转换,所以方法二才是正确的

但是即使这样也可能导致内存泄漏,不同的编译器执行方法二的调用,参数压栈的顺序也不一样

比如顺序是这样的

  • 调用 priority
  • 执行 new Widget
  • 调用 shared_ptr 构造函数

如果上面这样还行,但如果是下面这样执行呢?

  • 执行 new Widget
  • 调用 priority
  • 调用 shared_ptr 构造函数

假设priority 抛了异常,new Widget 返回的指针被遗失,并没有放进 shared_ptr 内部,这就会引发资源泄漏

怎么办?分离语句

上述发生的根源在于压栈顺序不一定,那直接把语句分开,就可以确定先执行 new Widget ,然后调用 shared_ptr ,最后执行 priority 函数

代码案例

std::trl::shared_ptr<Widget>pw(new Widget);
processWidget(pw,priority());

四、设计与声明

18、让接口容易被正确使用,不易被误用

先上代码

//////////////////////////////////
// 让接口容器被使用
class Date{
public:
	Date(int day,int month,int year);
	...
};

// 这个接口简单吧,但是传递参数时会出现这种情况
Date(30,2,2010) , Date(3,23,2010);

这就是一个容易误用的接口

当然做法可以让每个参数作为一个结构体对象

//========================
struct Day{
explicit Day(int d):val(d){}	

int val;
};

struct Month{
explicit Month(int m):val(m){}	

int val;
};

struct Year{
explicit Year(int y):val(y){}	

int val;
};

class Date{
public:
	Date(const Month& m,const Day& d,const Year& y);
	...
};

// 客户使用
Date d(Month(3),Day(30),Year(2010));

条款 13 导入一个 factory 工程函数,它返回一个指针指向 Investment 继承体系动态分配的对象,例
Investment* createInvestment()
为避免客户忘记使用智能指针,可以先发制人
std::trl::shared_ptrcreateInvestment();

强迫客户返回值存放在 shared_ptr 中,这样就不会忘记释放资源

但是哪些客户拿到指针后,胡乱搞,把指针传递给一个gitRidInvestment 的函数,总之就是不会调用到 delete 了,使用了错误的析构,聪明的设计者又一次先发制人,你不是要调用这个函数吗,我把这个函数作为删除器,绑定在析构函数上

//====
std::trl::shared_ptr<Investment>creatInvestment(){
	// 第一个参数是引用计数变为0时,会调用删除器
	std::trl::shared_ptr<Investment>pInv(static_cast<Investment*>(0),getRidInvestment);

	pInv = ... // 指向正确的对象
	return pInv;
}

19、设计 class 犹如设计type

当然这个条款我们只是考虑一些问题,没有结论

设计 class 我们要面对哪些问题?

1、新的 type 对象如何被创建、如何被销毁,因为这会影响到构造函数,析构函数以及内存分配函数和释放的设计

2、对象的初始化和赋值有什么样的差别?这个答案决定了构造函数和赋值操作符的行为,因为他们调用的是不同的函数

3、新的 type 对象如果被以值传递,意味着什么?

4、什么是 type 的合法值,这对应着必须进行一些检查工作

5、你的新 type 需要配合继承吗,是否允许别的类继承你的 type 对象

6、你的新 type 需要什么样的转换,隐式转换还是显示转换,如果允许explicit 构造函数的存在,那必须写出专门转换的函数,因为 explicit 不允许隐式转换

7、哪些操作符和函数对应 type 是合法的,这决定于哪些需要被设计为member函数

8、哪些标准函数应该驳回,这些应该设计为 private

9、谁该取用新 type 成员,这决定于哪些设计为 public,哪些是protected ,或 private,以及一些友元函数的设计

10、什么是type的未声明接口,它对效率,异常安全,资源运用提供哪些保障?

如果你想设计一个 class,在定义一个新的 type 之前,可以考虑上面内容

20、const 引用传递替换值传递

引用传递有两个好处

  • 传递高效
  • 避免对象切割

当我们进行拷贝构造的时候,参数是const 类型引用,为什么?

首先const 是防止用户非法修改,如果修改引用,会影响实体的改变,使用引用传递是避免调用构造函数带来开销

如果是传值则不需要加 const ,因为无论你如何修改函数内的值,它都是一份副本,不会影响到实体的值,所以可以不加

对象切割问题,当参数是一个基类对象或引用时

//////////////////////////////
//对象切割问题
//(1) 传值
void printDispaly(Windows w){
	std::cout<<w.name<<endl;
	w.print();
}
//(2) 传引用
void printDispaly(Windows& w){
	std::cout<<w.name<<endl;
	w.print();
}

第一种,以值传递,调用的是基类的 name 以及 基类的 print 函数,并且只能调用基类的那一部分成员

第二种,以引用传递,实参是什么类型,就会调用那个类型的函数,所以不会有切割问题

当然并不是所有的传递都用引用,对于一些 stl 的迭代器和函数对象,最好选用传值方式

21、必须返回对象时,不要妄想返回引用

引用虽然效率高,但是一定注意不要传递一些引用指向其实并不存在的对象。

/////////////////////////////
// 有些情况不可以引用传递

class Rational{
public:
	Rational(int number = 0,int denominator = 1);
private:
	int n,d;
	// 方法一:传值返回 (正确)
	firend const Rational operator*(const Rational& lhs,
		const Rational& rhs);

	// 方法二:引用返回 (错误)
	firend const Rational& operator*(const Rational& lhs,
		const Rational& rhs);
};

我们需要明白,引用它是一个别名,要知道它引用的是谁,如果返回后对象被释放,引用的实体也就失效

如果要引用返回,必须先要创建引用的对象,创建可以通过在堆中,或者栈中,在栈中创建好,函数结束后依然会销毁失效,在堆中创建好,返回回去后,谁来执行 delete ,所以两种都不行

那有人说,我在函数内部定义一个 static 的 Rational 对象,即使函数结束后他依然存在,这可以吗 ?

const Rational& operator* (const Rational& lhs,const Rational& rhs){
	static Rational result;
	result = ... //进行赋值
	return result;
}

答案是:不行

原因:线程不安全,因为 static 变量共享与对象,缺乏锁的保护,当然这还只是浅层原因,更深的原因是这个

bool operator==(const Rational& lhs,const Rational& rhs);
Rational a,b,c,d;

if((a * b) == (c*d)){
	// 乘机相等做出对应操作
}else{
	// 不等做对应操作
}

// 我们会发现 a*b 永远等于 c*d  
// 案例如下
if(operator==(operator*(a,b),operator*(c,d)));

operator*(a,b) 返回一个引用,指向的是 result ,比如 a = 3,b = 4;那么 result = 12

operator*(c,d)返回一个引用,指向的也是 result ,比如 c = 5,d = 6,那么此时result 会变为 30,注意,operator(a,b)返回的引用值也会由 12 更新为 30,因为两个 result 他们根本就是一个对象 (因为类型是 static ,内存只会分配一次内存) 这个就是条款中说的“现值”意思
在这里插入图片描述
那有人又抗议,既然一个 static 变量不行,来个数组怎么样,如果那样的话,越麻烦,定义数组要指定大小,太大浪费空间,太小又和单一static 变量没啥区别,对象要搬移元素浪费时间,所以遇到必须返回对象的情况,还是老老实实的值传递吧!

如果你看到这里,没发现什么破绽的话,还是再看一遍,回头想想条款10,为什么赋值操作符返回的是一个引用,这里就不行了?

是因为赋值返回的是一个 this 指针,而它本来就存在了,并不是一个临时创建的对象

最后:流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数需要使用引用, 在另外的一些操作符中,却千万不能返回引用:±*/ 四则运算符。它们不能返回引用

22、将成员变量声明为 private

目的:体现封装性,更好的控制成员变量,如果设置为 public 客户想怎么用就怎么用,那还得了,所以往往是通过成员函数去访问成员变量的.

23、以 non-member、non-friend 替换member

这句话什么意思,就是字面意思,用非成员函数替换成员函数

举个例子:

////////////////////////////////
// 非成员函数替换成员函数
class WebBrowser{
public:
	void clearCache();
	void clearHistory();
	void removeCookies();
};

// 这是一个非成员函数
void ClearBrowse(WebBrowser* wb){
	wb.clearHistory();
	wb.clearCache();
	wb.removeCookies();
}

有两个好处

  • 提供更好的封装性
  • 扩展性高,可跨越多个源码文件

第一个不说了,第二个的意思是这样的

我举个例子,C++ STL 容器有,vector,list ,stack … 他们都是封装在不同的头文件里,我们用到哪个去使用哪个,如果把他们放在同一个类中,或者同一个命名空间中就会特别大,编译也费时

如果我们使用成员函数,编译的依赖性会很高

24、若所有参数皆需要类型转换,请为此采用non-member 函数

原因:成员函数的第一个参数是 this 指针,因为无法进行类型转换,而非成员函数不存在这个问题,在传递参数的同时,如果发现类型不同,可以发生隐式转换

25、考虑写出一个不抛异常的 swap 函数

一般我们会这样实现

namespace std{
	template<class T>
	void swap(T& a,T& b){
		T temp(a);
		a = b;
		b = temp;
	}
}

但是太过于低效

如果指针指向对象,那么可以通过交换指针来实现数据的交换,这样就不用拷贝了

//////////////////////////
// 实现一个 swap 函数
class Widget{
public:
	...
	void swap(Widget& other){
		using std::swap;
		// 通过交换指针来到达交换数据的目的
		swap(pImpl,other.pImpl);
	}
	...
};

// 特化版本
namespace std{
	template<>
	void swap<Widget>(Widget& a,Widget& b){
		a.swap(b);
	}
}

由于字数太多,剩下的内容在下一篇博客总结

你可能感兴趣的:(C++基础)