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

Effective C++ 目录

    • 五、实现
      • 26、尽可能延后变量定义的时间
      • 27、尽量少做转型动作
      • 28、避免返回 Handles 指向对象内部成分
      • 29、为异常安全努力是值得的
      • 30、透彻理解 inlining 里里外外
      • 31、将文件间的编译依存关系降到最低
    • 六、继承与面向对象设计
      • 32、确定你的 public 继承塑模出is-a的关系
      • 33、避免遮掩继承而来的名称
      • 34、区分接口继承和实现继承
      • 35、考虑 virtual 函数以外的其他选择
      • 36、绝不重新定义继承而来的 non-virtual 函数
      • 37、绝不重新定义继承而来的缺省参数值
      • 38、通过复合塑模出 has -a 或根据某物实现出
      • 39、明智而慎重使用 private
      • 40、明智而慎重使用多继承
    • 七、模板与泛型编程(重点掌握)
      • 41、了解隐式接口和编译期多态
      • 42、了解 typename 的双重意义
      • 43、学习处理模板化基类内的名称
      • 44、将与学习无关的代码抽离 templates
      • 45、运用成员函数模板接受所有兼容类型
      • 46、需要类型转换时请为模板定义非成员函数
      • 47、请使用 traits classes 表示类型信息
      • 48、认识 template 元编程
    • 八、定制 new 和 delete
      • 49、了解 new-handler 的行为
      • 50、了解 new 和 delete 的合理替换时机
      • 51、编写 new 和 delete 时需要固守常规
      • 52、写了 placement new 也要写 placement delete
    • 九、杂项讨论
      • 53、不要忽视编译器的警告
      • 54、让自己熟悉包括TR1在内的标准库
      • 55、让自己熟悉 Boost 库

五、实现

26、尽可能延后变量定义的时间

什么意思呢?就是你什么时候用变量再去定义,别一上来定义很多变量,因为有的时候,如果你定义的变量带有构造函数和析构函数,但是变量却没有用到,但依然要承受构造函数和析构函数的成本,这不就低效了吗?

例如

/
// 真正用的时候再定义
std::string func(string& password){
	....
	// 定义一个变量
	string res;

	// 下面是一些业务操作
	if(...){
		// 如果这里抛出异常,那res根本就没有执行到
		throw logic_error("erron!");
	}	
	...	
	// string res  的一些执行动作
	....

	return res;
}

当然了,并不是让你再使用那个变量的前一刻才定义它

再比如看


// 哪个更加好一点

// 方法A
Widget w;
for(int i = 0;i<n;++i){
	w = i;
}

// 方法B
for(int i = 0;i<n;++i){
	Widget w(i);
}

答案:方法A 调用一次构造函数,一次析构函数,n次赋值操作
方法B 调用 n 次构造函数,n 次析构函数,你说哪个好?其实这个有分情况,如果赋值成本低于一次构造和一次析构,那A还是比较好,如果 n 特别大的情况,B 可能会更好一些

当然了如果违背这则条款并不是很严重,只是追求一种好的写法却是挺高效的

27、尽量少做转型动作

C++ 提供四种转型方式

  • const_cast 用来将对象的常量性移除
  • dynamic_cast 执行“安全向下转型”
  • reinterpret_cast 执行低级转型,比如 int* -》 int
  • static_cast 强迫隐式转换

没看不懂没关系,下面详细举例以及他们的好坏

/
// static_cast 举例
class Widget{
public:
	explicit Widget(int size);	
	...
};

void dosomething(const Widget& w){
	...	
}

// 方法一
dosomething(Widget(15));

// 方法二
dosomething(static_cast<Widget>(15));

这两种方法都行

但是下面这种方法是错误的使用


//错误使用
class Window{
public:
	virtual void onResize(){...}
	...
}

class SpecialWindow:public Window{
public:
	virtual void onResize(){
		// 将 this 指针转换为 Window 类型去调用 onResize 函数
		static_cast<Window>(*this).onResize();
		....
	}
}

注意:上述代码并非是在当前对象调用 Window::onResize() 函数,而是在当前对象基类成分的副本去调用的,然后回到子类中,在当前对象上执行 Special Window 的专属动作,如果 Window::onResize 修改了对象内容,也不影响子类中对象,因为改动的是一个副本

正确写法为


//正确使用
class Window{
public:
	virtual void onResize(){...}
	...
}

class SpecialWindow:public Window{
public:
	virtual void onResize(){
		Window::onResize();
		....
	}
}

书中还有很多案例,本人能力有限(笑哭),大致意思是:

尽量不要使用转型,而去想一想别的设计方案,如果非要使用的话尽量把它写在函数里面,不要让客户把转型写在他们的代码里,另外不要使用隐式类型转换,而使用显示的那四种

28、避免返回 Handles 指向对象内部成分

话不多说,先上案例

///
// 涉及矩形设计
class Point{
public:
	Point(int x,int y){

	}
	...
	void setX(int newVal);
	void setY(int newVal);
};

struct RectDate{
	Point ulhc; // 左上角
	Point lrhc; // 右下角
};

class Rectangle{
public:
	Point& upperLeft()const{
		return pDate->ulhc;
	}
	Point& lowerRight()const{
		return pDate->lrhc;
	}
private:
	std::trl::shared_ptr<Rectangle>pDate;
};

这个设计有什么问题?

如果你没看出来,那我加一段代码进行访问

// 客户调用
Point coord1(0,0);
Point coord2(10,10);
const Rectangle rec(x,y);

rec.upperLeft().setX(100); // 这一行语句会修改 x 的值

由于 Rectangle 返回的是对象成员的引用,所以用户可以随意去修改了

解决办法:在成员函数前面加 const 限制修改就可以了

但是即便如此,依然还是返回了内部对象,有的情况会导致引用对象不存在,而造成空悬,虚吊等情况。

所以我们尽量不要返回 Handles指向对象内部,可以增强封装性(其中Handles 包括引用,指针,迭代器)

29、为异常安全努力是值得的

异常安全指的是什么?比如一个函数里面抛出了异常,安全的函数需要满足两点

  • 不会泄露资源
  • 不会破坏数据

但是看下面这个案例

class PrettyMenu{
public:
	PrettyMenu();
	// 改变背景图像	
	void changeBackground(std::istream& image);
	
	~PrettyMenu();
private:
	Mutex mutex;    // 保证并发控制
	Image* bgImage; // 目前的图像
	int imageChanges; // 背景图像被改变的次数
};

void PrettyMenu::changeBackground(std::istream& imgsrc){
	lock(&mutex);   // 取得互斥器
	delete bgImage; // 去重之前的背景
	++imageChanges; // 修改图像次数
	bgImage = new Image(imgsrc); // 安装新的背景
	unlock(&mutex); // 释放互斥器
}

上面代码new 函数抛出异常后,资源被泄露(unlock 无法调用),互斥器被锁,然后 bgImage 指向一个删除的对象,次数也被增加,然后图像并没有更换成功,显然数据被破坏

怎么修改?

对于互斥器我们可以使用对象去管理资源,对象释放的时候自动调用析构函数,里面释放 unlock 就行了

void PrettyMenu::changeBackground(std::istream& imgsrc){
	Lock m1 (&mutex);   // 使用互斥器类构造互斥器
	delete bgImage; // 去重之前的背景
	++imageChanges; // 修改图像次数
	bgImage = new Image(imgsrc); // 安装新的背景
}

但是资源怎么解决呢?

我们只要调换一下语句顺序就可以了

void PrettyMenu::changeBackground(std::istream& imgsrc){
	lock m1(&mutex);   
	PrettyMenu* tmp = new Image(imgsrc);  // 临时资源
	++imageChanges; 
	delete bgImage; // 释放之前的对象资源
	bgImage = tmp; // 把新对象赋值给 bgImage
}

当然也可以使用智能指针去管理资源,这样就不需要手动delete,这种方法就比较完美了

class PrettyMenu{
public:
	// 改变背景图像	
	void changeBackground(std::istream& image);
	void reset(PrettyMenu* bgImage);
private:
	Mutex mutex;    // 保证并发控制
	std::trl::shared_ptr<Image>bgImage; //智能指针管理资源
	int imageChanges; // 背景图像被改变的次数
};

void PrettyMenu::changeBackground(std::istream& imgsrc){
	lock m1(&mutex);   
	bgImage.reset(new Image(imgsrc));

	++imageChanges; 
}

30、透彻理解 inlining 里里外外

终于到了我比较熟悉的内联函数了

内联函数可以避免函数调用的开销,在函数编译期间对函数调用进行替换为函数本体,以空间换取时间的策略

当然 inline 只是给编译器的一个申请建议,编译器会不会采取,还得看代码的复杂程度,和编译器环境等,如果展开后太多导致代码膨胀带来的效率低于函数调用,那么有的编译器会给你一个警告信息

当然有的时候,编译器会不会把函数设置为内联是根据函数的调用方式决定的,如果函数是通过指针调用,则不会设置内联,因为指针调用是要知道函数地址的,函数一旦设为内联,编译器没有能力提出一个指针指向不存在的函数,如果是函数调用的话,有可能被内联

并不是所有的函数都可以设置为内联函数

1、虚函数

不可以设置为内联的,因为虚函数是在运行期间确定调用哪个接口的,而内联函数意味着 在执行前将调用动作替换为函数本体,所以肯定不行滴

2、构造函数

不行,会导致代码的膨胀,如果一个基类构造函数被内联,那么当派生类调用基类构造函数的时候,也会把基类的函数体插入到派生类中,导致派生类构造函数过于复杂,所以编译器不会那样搞

最后:程序猿需要考虑的是,如果将函数被内联带来的冲击有多大,如果fun函数是程序库的内联函数,当客户端将fun函数编到自己代码中,如果将来fun需要修改,那么用户就要重新编译程序,而如果fun不是内联,如果有改动,用户只需要重新连接就好了,比重新编译负担少很多,有的应用采用动态链接的方式,不知不觉就升级了,所以这些都应该程序猿考虑的因素

31、将文件间的编译依存关系降到最低

文件间的依赖关系,意思就是,A 文件编译的时候需要依赖B 或者 C 其他文件,否则无法编译,看下面代码


// 文件编译依赖关系
#include 
#include "date.h"
#include "address.h"

class Person{
public:
	Person(const std::string& name,const Date& birthday,const Address& addr);
	std::string name()const;
	std::string birthday()const;
	std::string address()const;
	...
private:
	std::string theName;
	Date theBirthday;
	Address theAddress;
};
// Person 这个类所在的文件需要依赖于 string  date.h  address.h  这些文件

如果 date.h 文件,或者 address.h 里面某个东西修改了,那么Person文件就需要重新编译,以及 包含Person 的文件都需要重新编译,试想如果这是一个大项目,得多费时

那应该怎么修改把依赖关系降到最低?

声明和定义分开实现


// 文件编译依赖关系
#include 
#include  // shared_ptr 需要用的头文件

class Date;  // 声明Date 否则编译不通过
class Address;  // 同上
class PersonImpl; // 负责提供 Person 的接口 

// 此文件才是 Person 的真正实现
class Person{
public:
	Person(const std::string& name,const Date& birthday,const Address& addr){}
	std::string name()const{}
	std::string birthday()const{}
	std::string address()const{}
	...
private:
	std::trl::shared_ptr<PersonImpl>pIml; // 使用一个指针指向实现物
};

无论是修改 adress 还是 date 都不会影响到 person 文件

本质:尽可能让头文件可以自己满足,万一做不到就让它与其他文件的声明式(非定义式)相依

当然了还可以为声明式和定义式提供两个不同的头文件,编译的时候只要包含声明式(非定义式)的头文件,这样就不需要类的前置声明了

六、继承与面向对象设计

32、确定你的 public 继承塑模出is-a的关系

这是一种设计模式,这种继承关系的设计模式必须是符合逻辑的,举个例子:

/
// 鸟类
class Bird{
	//... 鸟的基本特性
};

// 会飞的鸟类
class FlyingBird:public Bird{
public:
	// 该鸟有飞的功能
	virtual void flay(){
		...	
	}
};

// 现在有一种动物叫企鹅,它要继承鸟类
class Penguin:public FlyingBird{
	... // 这就是一个错误的设计
	// 因为企鹅根本不会飞,虽然代码通过编译
	// 但是不符合逻辑,达不到预期效果
};
/

那应该怎么修改呢?需要单独写一个类,而这个类就是不会飞的鸟类,让它继承不会飞的类,后面会详细总结…

33、避免遮掩继承而来的名称

当派生类和基类中的成员对象或者函数名相同时,会发生覆盖(不考虑多态)

class A {
public:
	void fun1() {
		cout << "基类函数 fun1" << endl;
	}
private:
	int a = 10;
};
class B :public A {
public:
	void fun1() {
		cout << a << endl;
		cout << "派生类函数 fun1" << endl;
	}
private:
	int a = 20;
};

B b;
b.fun1();

这样会输出派生类的函数,a 会输出 20,而不是 10,是因为被基类的 a 值被覆盖了

可以通过域名符操作进行制定调用哪个类的函数

B b;	
b.A::fun1();

34、区分接口继承和实现继承

我们先看一个列子

class Shape{
public:
	virtual void draw() const = 0;
	virtual void error(const Shape&ms);
	void obJectID() const;
};

一个重虚函数就可以理解为真正的接口继承,客户不可以创建它的基类实体,只能在派生类创建和定义函数,否则就不能使用,因为基类是不给接口的定义.

对于非重虚函数,可以继承接口,也可以继承缺省实现,非重虚函数在基类是可以被定义的,如果子类没有定义,则会使用基类的函数,表示它的行为和基类一样,如果子类给出定义,则表示子类的行为和基类行为是不同的,这也就是多态的概念

对于非虚函数,则表示它在子类并不打算实现不同行为,它的状态一直是不变 ,所以它不应该在子类中重定义.

35、考虑 virtual 函数以外的其他选择

这个说的就是设计模式,书上提到的是策略模式

什么是策略模式?

定义一系列的算法,把它们用类封装起来,然后在运行的时候,根据对象不同,互相替换调用。从依赖的角度来看,变化依赖稳定,稳定依赖于抽象

采用的是面向对象设计中的开闭原则,对扩展开放,对修改封闭

主要解决使用 if else 语句,或者 switch 开关语句带来的复杂以及难以维护

举例:比如进行税法的计算,每个国家都有自己的税法计算规律,把这些国家的计算规律封装到不同的类中,当然这些类都是继承一个稳定的抽象类,然后实例化出不同的对象去调用对应的国家税法

36、绝不重新定义继承而来的 non-virtual 函数

因为会发生覆盖,non-virtual 都是静态绑定,在函数编译的时候就知道应该调用哪个函数了,所以它是根据指针类型去决定的,而不是根据对象类型去决定调用哪个函数

37、绝不重新定义继承而来的缺省参数值

因为继承而来的却是参数是静态绑定的,即使这个函数虚函数,虚函数在实现层面是动态绑定,但是参数如果你没有指定,它会继承基类的参数,而不会使用是你给出的参数

class A {
public:
	virtual void fun1(int a = 10) {

	}
	virtual ~A() {

	}
private:
	int a;
};

class B :public A {
public:
	virtual void fun1(int b = 20) {
		cout << b << endl;
	}
private:
	int b;
};


B b;
A* a = &b;
a->fun1();  // 输出的是 10 ,而不是 20
// 就是因为它绑定的是基类的参数

38、通过复合塑模出 has -a 或根据某物实现出

复合是一种类型之间的关系,当然复合也有很多含义,要看你怎么理解,比如可以理解为内嵌,分层,聚合,下面举一个泛型编程的例子

实现:通过 list 容器来实现 set 容器

看代码

template<class T>
class Set {
public:
	// 支持插入,删除,操作
	bool memset(const T& item)const;
	void insert(const T& item);
	void remove(const T& item);
	std::size_t size()const;
private:
	std::list<T> rep;
};

// 判断 Set 中有无相同元素
template<class T>
bool Set<T>::memset(const T& item)const {
	auto it = find(rep.begin(), rep.end(), item);
	if (it != rep.end()) {
		return true;
	}
	return false;
}

template<class T>
void Set<T>::insert(const T& item) {
	if (!memset(item)) {
		rep.push_back(item);
	}
	return;
}

template<class T>
void Set<T>::remove(const T& item) {
	// 为什么使用 typename ,后面条款 46 具体讨论
	typename std::list<T>::iterator it =
		std::find(rep.begin(), rep.end(), item);
	if (it != rep.end()) {
		rep.erase(it);
	}
	return;
}

template<class T>
size_t Set<T>::size()const{
	return rep.size();
}

当然这样写不代表 C++ set 容器就是 list 实现的,这个条款只是想说明它们之间关系,理解复合的意思

39、明智而慎重使用 private

private 继承关系表示,基类和子类完全没有任何观念上的关系,单纯只是一个实现技术,子类只是想采用基类已经具备好的特性

private 在软件设计层面上没有实际意义,所以很少使用

一般用于派生类去访问基类的 protected 成员的时候,或者需要重新定义继承而来多个虚函数的时候,才会用到

40、明智而慎重使用多继承

多继承的关系特别复杂,尤其是

多继承中的菱形继承会导致歧义性和数据的冗余性

可以通过指定类来调用哪个函数

数据冗余可以采用虚继承的方法,让子类继承它的虚方法

采用虚继承会增加大小,速度,初始化以及复杂度的成本

一般也是很少用的

七、模板与泛型编程(重点掌握)

模板掌握的好与否,是一个程序猿的功力的体现,也是在面试过程中的装逼体现

41、了解隐式接口和编译期多态

我们之前都是这样去写代码的

class Widegt{
public:
	Widegt();
	virtual ~Widegt();
	virtual std::size_t size()const;
	virtual void normalize();
	void swap(Widegt& other);
	...
};

void doProcessing(Widegt& w){
	//...
}

所有的接口都是显示定义的,函数中的 w 参数类型是 Widget ,所以必须支持 Widegt 接口,这是一个显示接口

隐式接口是啥样的?

template <typename T>
void doProcessing(T& w){
	//...
}

T 的类型在编译期间会进行推导,当然这个接口依然支持多态性,这叫函数模板,参数为隐式类型,T 可以自定以的任意类型,提高代码的复用性

以不同的模板参数具现化函数模板,会导致调用不同的函数,这就是编译器的多态

最后稍微总结一下

  • class 和 template 都支持接口和多态
  • class 接口是显式的,多态是通过虚函数发生在运行期
  • template 接口是隐式的,多态是通过 template 具现化和函数重载解析发生在编译期

42、了解 typename 的双重意义

template<class T> class Widegt;
template<typename T> class Widegt;

class 和 typename 的区别在哪? 没有区别

至少在 template 这里,它们的的意义是相同的

但是并不代表它们两个关键字就是一样的

typename 功能

  • 定义一个类型的别名
  • 标识嵌套从属类型

我们这里用到第二个作用

先看一段代码

template<typename T>
void Print(const T& container){
	//...
	T::const_iterator iter = container.begin();
	T::const_iterator* x;
	//...
}

iter 的类型是 T::const_iterator ,这是一个嵌套从属名称,这会导致解析困难

关键就在于 T::const_iterator ,你怎么知道它就是一个类型,如果在T中有一个静态变量的名字是 const_iterator,那它就变为 const_iterator 乘以 x 的表达式了

所以会导致歧义问题,这个时候我们就可以使用 typename ,声明这是一个类型

注意:typename 不可以出现在基类列或者成员初值列中内作为基类修饰符

43、学习处理模板化基类内的名称

class CompanyA{
public:
	...
	void sendCleartext(const std::string& msg);
	void sendEncrypted(const std::string& msg);
	...	
};

class CompanyB{
public:
	...
	void sendCleartext(const std::string& msg);
	void sendEncrypted(const std::string& msg);
	...	
};

// 用来保存信息
class MsgInfo{...};

// 模板类发送加密消息已经清空信息
template<typename Company>
class MsgSender{
public:
	...
	void sendClear(const MsgInfo& info){
		std::string msg;
		Company c;
		c.sendCleartext(msg);
	}
	void sendSeret(const MsgInfo& info){
		std::string msg;
		Company c;
		c.sendEncrypted(msg);
	}
};

// 继承上一个类
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{ 
public:
	...
	void sendClearMsg(const MsgInfo& info){
		// 调用基类的函数会报错
		// 是因为编译器不知道继承的这个类是什么类型
		//  类型不知,所以无法通过编译
		sendClear(info);
	}
	...
};

解决办法:模板特化

指定 typename 中的类型,这样编译器就可以识别了

如果是全特化(typename 所有参数都被指定),那么再没有其他参数可变化

需要注意的是:类模板的基础,不像 C++ 对象那样畅通无阻,比如在模板的基类定义一个 fun1 函数,如果在模板派生类直接访问 fun1 函数是访问不到的,需要在前面加 this-> 指涉基类模板中的成员,或者使用 using 把函数引进来

44、将与学习无关的代码抽离 templates

实现固定尺寸的正方形矩阵,编写一个 template

template<typename T,std::size_t n>
class SquareMatrix{
public:
	...
	void invert();
	...
};
// 实例化 5 * 5 矩阵
SquareMatrix<double,5> sm1;
sm1.invert();

// 实例化 10 * 10 矩阵
SquareMatrix<double,10> sm2;
sm2.invert();

实例化出2 个矩阵,一个是 5 * 5 的,还有一个是 10 * 10 的,但是我们发现,这两个矩阵除了常量不一样,其余函数部分都一样,这可能会导致 template 膨胀

解决办法:建立一个带有数值参数的函数,然后以 5 或者 10 来调用这个带参数的函数

template<typename T>
class SquareMateixBase{
protected:
	...
	// 以给定的尺寸求逆矩阵
	void invert(std::size_t matrixSize){
		//...
	}
	...
};

template<typename T,std::size_t n>
class SquareMatrix:private SquareMateixBase<T>{
private:
	// 避免遮掩 Base 类的invert
	using SquareMateixBase<T>::invert
public:
	...
	// 这是一个 inline 函数,调用基类的 invert 函数
	void invert(){
		this->invert(n);
	}
	...
};

基类只对矩阵元素类型参数化,不对矩阵尺寸参数化

但是还有个问题要解决,就是基类如何查看派生类矩阵的数据,这个也好办,定义一个指针指向内存地址就可以了

template<typename T>
class SquareMateixBase{
protected:
	...
	// 以给定的尺寸求逆矩阵
	void invert(std::size_t n,T* pMem)
		:size(n),pData(pMem){
		//...
	}
	void setDataPtr(T* ptr){
		pData = ptr;
	}
	...
private:
	std::size_t size; // 矩阵大小
	T* pData;        //  指针指向矩阵内容
};

template<typename T,std::size_t n>
class SquareMatrix:private SquareMateixBase<T>{
public:
	SquareMatrix()
		:SquareMateixBase<T>(n,0)
		,pData(new T[n*n]){
	
		// 把副本交给 base class
		this->setDataPtr(pData.get());
	}

private:
	boost::scoped_array<T> pData;
};

45、运用成员函数模板接受所有兼容类型

这是指针之间的隐式转换

class Top{...};
class Middle:public Top{...};
class Bottom:public Middle{...};

Top* pt1 = new Middle; // Middle* 转 Top*
Top* pt2 = new Bottom; // Bottom* 转 Top*
const Top* pct2 = pt1; // Top* 转 const Top*

但是如果引进模板,就比较麻烦了

下面的代码是通过不了的,为什么?

template<typename T>
// 实现一个智能指针
class SmartPtr{
public:
	explicit SmartPtr(T* realPtr);
	...	
};

SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top>pct2 = pt1;

因为虽然是同一个模板类,但是具现出来的类完全扯不上关系,就比如 Smart 和 Smart 完全没有任何关系

所以也就不存在转换这一说法了

那应该怎么做?

把关注点放在 Smart 的构造函数上

下面才是重点,怎么写构造函数?

template<typename T>
// 实现一个智能指针
class SmartPtr{
public:
	SmartPtr(const SmartPtr& other);
	template<typename U>
	// 为了生成拷贝构造函数
	// 意思就是对任何类型T和任何类型U,这里可以
	// 根据Smartptr 生成一个 Smartptr
	// 这被称为泛化拷贝构造函数	
	SmartPtr(const SmartPtr<U>& other)
		// 返回智能指针对象原始指针的副本
		:heldPtr(other.get()) 
	{
		...
	}

	T* get()const{
		return heldPtr;
	}
private:
	T* heldPtr;
};

需要注意的是,当你声明一个泛化的拷贝构造或者赋值操作符,同时还需要声明正常的拷贝构造和赋值操作符

46、需要类型转换时请为模板定义非成员函数

template<typename T>
class Rational{
public:
	Rational(const T& numerator = 0,
		const T& denominator = 1);
	const T numerator()const;
	const T denominator()const;
	...
};

template<typename T>
const Rational<T>operator* (const Rational<T>& lhs,const Rational& rhs){
	//...
}

// 实例化
{
	Rational<int>onehalf(1,2);
	Rational<int> result = onehalf * 2; //会报错
}

为什么会报错,因为编译器无法推导出T类型(对于第一个参数是Rational,编译器可以根据onehalf 推导T为 int 类型,但是第二个实参数 int 型,形参是一个 Rationa ,编译器推不出第二个T类型,自然也就无法具现化函数

解决办法:友元函数

class Rational 可以声明 operator* 是它的一个 frined 函数

因为当对象 oneHalf 被声明为Rational 时 class Rational 已经被具现化出来了,作为过程的一部分,友元函数 operator* 也就自动被声明出来了,


template<typename T>
class Rational{
public:
	friend 
	const Rational<T>operator* (const Rational<T>& lhs,
								const Rational& rhs){
		return Rational(lhs.numerator()*rhs.numerator(),
			lhs.denominator()*rhs.denominator());							
	}
	...
};

template<typename T>
const Rational<T>operator* (const Rational<T>& lhs,const Rational& rhs){
	//...
}

47、请使用 traits classes 表示类型信息

这是一种模板萃取的技术,比如我们对不同的类型有不同的操作,所以可以针对一部分类型进行特化 ,特化的类型执行某一个逻辑程序,没有特化的类型执行另一个逻辑程序

比如我们针对内置类型和非内置类型有不同的操作


// 内置类型
struct TrueType{
	static bool Get(){
		return true;
	}
};

// 自定义类型
struct FalseType{
	static bool Get(){
		return false;
	}
};


// 给出以下模板,将来用户可以按照任意类型实例化该模板
template<class T>
struct TypeTraits{
	typedef FalseType IsPODType;
};

// 实例化
template<>
struct TypeTraits<char>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<short>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<int>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<long>{
	typedef TrueType IsPODType;
};

template<>
struct TypeTraits<double>{
	typedef TrueType IsPODType;
};
  
template<>
struct TypeTraits<...>{//所有内置类型都特化一下
	typedef TrueType IsPODType;
};

// 确认对象的实际类型
template<class T>
void func(T* dst,const T* src,size_t size){
	if(TypeTraits<T>::IsPODType::Get()){
		// 内置类型逻辑处理程序
	}
	else{
		// 自定义类型处理程序
	}
}

48、认识 template 元编程

简单总结一下,书上案例看不太懂了 …

元编程可将工作由运行期移往编译期间,因而得以实现早期错误侦测和更高的执行效率

八、定制 new 和 delete

C++ 可以手动管理内存,因为系统定制的 new 和 delete 可能不符合客户的需求,所以这章主要讲的是 operator new 和 operator delete

这章写的很简略(因为看不懂),书上还是很详细的

49、了解 new-handler 的行为

new-handler 是什么?就是当你 new 出的内存不够需求、或者无法分配足够的内存,这个时候定义一个错误处理程序,这个处理程序就是 new-handler ,当然你不定义也行,默认的处理方式是抛出异常

void outOfMem(){
	std::cout<<"Unable to satisfy request for memory!"<<endl;
	// 这个程序里通常会做 5 件事
	// 1 让更多的内存可被使用
	// 2 安装另一个 new-handler 如果这个无法获得内存,则会安装另一个替换自己
	// 3 卸除 new-handler 也就是交 null 指针传给 set_new_handler 
	// 4 抛出 bad_alloc 异常
	// 5 不返回,调用 abort 函数
}

int main(){
	std::set_new_handler(outOfMem);
	int* pBigData = new int[1000000000L]; // 会调用outOfMem函数

	return 0;
}

以上是基本使用方法

如果你希望以不同的方式处理内存分配失败情况,比如

class X{
public:
	static void outOfMemory();
};

class Y{
public:
	static void outOfMemory();
};

X* p1 = new X; //如果分配失败,则执行 X::outOfMemory
Y* p2 = new Y; //如果分配失败,则执行 Y::outOfMemory

书上还有很多,我看不懂了,只能写这么多了

50、了解 new 和 delete 的合理替换时机

为什么需要替换编译器提供的 new operator和 delete operator

  • 检测运用上的错误,例多次执行 delete 导致未定义行为
  • 强化效能,差别对待不同的需求
  • 收集使用上的统计数据,在分配内存之前搞懂软件如何使用内存,先进先出?分配区块大小?以及寿命如何

51、编写 new 和 delete 时需要固守常规

在 new operator 内部写一个无穷循环,并在其中尝试分配内存,如果他无法满足内存需求,就调用 new-handler ,它也应该有能力处理 0 byte 字节的申请

delete operator 应该在收到null 指针时不作任何事情

52、写了 placement new 也要写 placement delete

使用 new 关键字,会先调用 new operator 分配空间,在调用构造器进行初始化,但是如果在调用构造器的时候抛出异常, 没有指针可以释放之前申请的资源,就会断断续续的造成内存泄漏的问题

所以在定义的 placement new 后也应该定义 placement delete

九、杂项讨论

53、不要忽视编译器的警告

把警告当做错误来处理

因为不同的编译器可能会发出不一样的警告,可能在这台编译器是警告,把代码移植到另一个编译器就可能是错误了

54、让自己熟悉包括TR1在内的标准库

C++ 标准程序库

STL、Iostreams 、locales 组成

TRI 添加了智能指针,hash-based 容器、正则表达式、…

55、让自己熟悉 Boost 库

Boost 是一个社群,也是一个网站,致力于C++开发,Boost 库提供了 TRI 组件实现品,以及其他程序库

你可能感兴趣的:(学习篇---服务端,C/C++,EffecitveC++,Effective,C++,c++)