more effective C++ 学习总结

本文详细讲解了C++中的指针、引用、转型、新式转型、资源管理、异常处理、虚函数与多态、智能指针、内存分配与释放、异常安全、设计原则等内容,帮助理解C++编程实践中的关键概念和技术策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.区别pointers和references

  • 引用总是指向最初获得的对象(必须被初始化)
  • 当需要指向某个东西,且绝不会改变其指向 或者 实现一个操作符而其语法需求无法由pointers完成时选择references,其他时候,选择pointers.
    • vectoroperator[]返回references , 使用时可以v[0] = 1; , 若返回指针,则需要比较奇怪的形式:*v[0] = 1.

2.新式转型

static_cast: 实现旧式转型并受到相同限制。
dynamic_cast:执行继承体系中安全的向下转型或跨系转型。无法用于缺乏虚函数的类型。
const_cast:去除常量性。
reinterpret_cast: 不具有移植性, 最常用用途是 转换“函数指针”类型。

新式转型 严谨且易于辨识,更容易被解析,编译器也得以诊断转型错误。(新式转型失败会以一个null或exception表现出来)。


3.不要以多态方式处理数组

书中举例是将子类数组传入以父类数组为参数的函数中,且函数体内涉及了指针算术array[i] -> *(array + i)), 而参数是父类数组,每个元素的大小则会被认为是父类对象的大小,造成未定义结果。

总结就是,多态和指针算术不能混用, 尽量不要让一个具体类继承自另一个具体类


4.非必要不提供默认构造函数

凡可以合理地从无到有生成对象的类,应该内含默认构造函数;
必须有某些外来信息才能生成对象的类,则不必拥有。

添加无意义的默认构造函数,也会影响class的效率(如验证该值是否有意义等)。
若没有默认构造函数时,会造成诸多麻烦

  • 产生数组时,一般而言没有办法为数组中的对象指定构造函数自变量
    • 可以使用对象数组的方法,即为数组中每个元素调用构造函数传入自变量
    • 使用指针数组的方法,定义一个数组存放指向每个对象的指针
      • 可能会造成资源泄露(必须记得将此数组指向的所有对象删除)和内存浪费(多了指针的空间,但可以通过分配raw memory,之后使用placement new在这块内存构造对象解决)
  • 将不适用于许多template-based container classes(对templates,被实例化的目标类型必须得有默认构造函数,因为这些模板中总会产生以template类型参数作为类型的数组
    • 严谨的模板则不要求,如vector template就不要求其类型参数拥有默认构造函数

5.最好不要提供任何类型转换函数

单自变量constructors(只有一个参数 或 剩下参数都有默认值)和 隐式类型转换操作符允许编译器进行隐式类型转换之用。(即可以成为类型转换函数

不要提供类型转换函数的根本问题是:此类函数经常在无意愿调用的情况下被调用。
一般通过下列方法解决这两种转换函数:

  • 隐式类型转换操作符
    class Rational {
    ...
    public:
       operator double() const;  // 将Rational对象转为double` 
    };
    
    Rational r(1,2);
    cout << r ; 
    // 未定义operator<< , 但r会被隐式类型转换操作符转换为double,输出double类型数据而非分数
    
    因此一般以另一个功能对等的函数代替类型转换操作符
    class Rational {
     ...
     public:
    	double asDouble() const; 
    };
    	Rational r(1,2);
    	cout << r; // error,没有定义operator<< , 也不会隐式转换
    	cout << r.asDouble() ; // 显式调用函数进行转换
    
  • 单自变量constructors
    • 1.通过explicit关键字解决,拒绝编译器因隐式类型转换调用之,仍允许显式类型转换;
    • 2.若不支持上述关键字,可以通过proxy classes解决。方法就是 在类内额外加入一个新的class,这个类相当于单自变量构造函数参数的替身。
      • 利用任何一个转换程序都不能含有一个以上的“用户定制转换行为”。(即不能连续隐式转换1次以上)这个规则,达到拒绝隐式转换的目的。

6.区别increment/decrement操作符的前置和后置形式

class A {
public:
	A& operator++();		// prefix ++
	const A operator++(int);	// postfix ++
	
	A& operator--();		// prefix --
	const A operator--(int);	// postfix --
	
	A& operator+=(int);
	...
}

前置、后置的区分是通过一个int自变量实现的,编译器为其默默指定一个0(i++; - > i.operator++(0);
前置返回reference, 后置返回const对象:

  • 其原因是前置是先累加再取出,得到最新值;后置是先取出,在累加,得到旧值。因此后置只能返回一个对象而非引用。
  • 后置不仅返回一个对象,而且是const对象,其原因是杜绝连续的两次累加:
    • i++++; // error, 返回的是const对象 (由于返回的对象,第二次累加是在返回的临时对象身上累加,行为非所预期,因此使用返回const对象的方法防止此类错误)

7.不要重载&&||,操作符

  • 重载&& || 操作符后,函数调用语义会替代短路式语义,所有参数都会被评估;
  • C++并未定义函数调用动作中参数评估顺序,而短路式的语义总是自左向右评估。
  • 逗号操作符也类似,无法保证从左向右的函数自变量的评估顺序。

8.了解不同意义的new和delete

new operatoroperator new:

  • string *ps = new string("hello_world");
    • 上述语句中的newnew operator , 是语言内建的操作符。其操作每次都同,分为两步:

      • 1.分配足够的内存(通过operator new实现)
        • 唯一能够改变的就是这一步,通过重载函数void * operator new(size_t size);, 改变内存的分配行为。和malloc一样,operator new唯一任务就是分配内存
      • 2.调用constructor,为分配的内存中的对象设定初值。
    • 编译器会将上述语句转换为类似下列代码的行为:

      void *memory = operator new(sizeof(string));
      call string::string("hello_world"); on *memory;
      string *ps = static_cast<string*>(memory);
      
      • 由于我们无法直接调用构造函数,因此要构建一个heap-based object,一定要使用new operator.

总结就是:
使用new operator使得对象产生于heap, 其分配内存并为该对象调用constructor
使用operator new只分配内存;
若打算自己决定内存分配方式,则重载operator new
若打算在已分配(并拥有指针)的内存中构造对象,使用placement new.

new (loc) Widget(Widget_size);	
// new operator的用法之一,loc在其隐式调用operator new时使用
// 所谓placement new类似于下面的operator new形式,size_t并未使用,因为placement new是一种特殊的operator new,而其主要任务就是找到一块内存,而内存已经有了,可以直接返回
void* operator new(size_t, void* loc) {
	return loc;
}

同样,delete operator完成在对应内存析构,并释放空间operator delete完成)。且operator delete也可以被重载。

这两者相当于mallocfree

void *buffer = operator new(50*sizeof(char));	// 50个char的空间,不调用任何ctors
...
operator delete();		// 释放内存,没有调用dtors

如果使用placement new在某块内存产生对象,则应避免使用delete operator,因为该内存并非由operator new分配得来。可以直接调用该对象的dtors.

另外,上述的都是针对单个对象,对于数组版的处理要用operator new[]operator delete[].(分配空间,同样可以被重载。

string *ps = new string[10]; 
// 调用operator[] 分配10个string对象的内存,对每个元素调用string default ctor

delete [] ps; 
// 为数组中每个元素调用string dtor,调用operator delete[] 释放内存

new operator delete operator都是内建操作符,无法被控制。我们只能够更改他们所调用的内存分配/释放函数


9. 利用destructor避免泄露资源

异常无法被忽略,若程序仅仅以返回错误码的方式发出异常信号,无法保证此函数的调用者会检查那个变量或检验那个错误码。
如果函数以抛出exception的方式发出异常信号,而该exception未被捕捉,程序执行便会立刻中止。

举了指针及其他资源使用过程发生异常,导致未能够释放资源,造成泄露资源的例子。
本讲就是鼓励将使用的资源放在对象内,使得即使异常发生,离开该函数时也能够释放资源,避免泄露资源。


10.在constructors中阻止资源泄露

考虑拥有两个其他成员对象A、B的类C。在constructor中初始化完A后,初始化B时发生异常,则C对象未构造完成,C++不会析构未初始化完全的对象,因此资源A发生泄露

  • 解决方案是在consructor捕捉全部exception,执行某种清理工作(如删除对应指针资源),重新抛出exception(throw),使其继续传播出去即可。

但对于常量指针成员,只能在成员初值列表内加以初始化。由于无法将try和catch放入成员初值列内,故可以将其放入工具函数内,虽然能够解决,但写起来繁琐。

因此,还是采用第九讲的方法,将对象视作资源,交给局部对象管理(如智能指针管理原始指针对象),如此便可免除构造函数内exception出现时发生的上述资源泄露,且无需在destructors内亲自手动释放资源。


11.禁止exception流出destructor之外

析构函数在两种情况下会被调用:

  • 1.对象正常情况下被销毁;
  • 2.exception传播过程中的栈展开机制——销毁。
    • 栈展开(stack unwinding):如果在一个函数内部抛出异常,而此异常并未在该函数内部被捕捉,就将导致该函数的运行在抛出异常处结束,所有已经分配在栈上的局部变量都要被释放。

如果因为栈展开机制调用destructor又因为exception的因素离开destructor。此时正有另外一个exception处于作用状态,c++则会调用terminate结束程序。
为了防止程序强制结束,可以在析构函数内部捕捉所有异常,并使用空的catch(...){}防止异常流出。
这样也能够让析构函数剩余的部分得以执行


12.※抛出一个exception 与 传递一个参数 和 调用一个虚函数 之间的差异

主要讨论了传递对象到函数 或以对象调用虚函数 和 将对象抛出成为一个exception之间的3个主要差异:

  • exception对象总会被复制,以by value方式捕捉,甚至会被复制两次(临时对象、参数)。以指针方式抛出是唯一在搬移异常相关信息时不需要复制对象(复制指针)的做法,但要注意不要抛出局部对象的指针。
  • 被抛出成为exception的对象,被允许的类型转换动作少(仅允许继承架构中的类转换、有型指针转化为无型指针
  • catch子句以其出现于源代码的顺序被编译器检验比对(最先吻合策略)。

13.以by reference方式捕捉exceptions

开头举例以对象指针的方式捕捉exception,但很多coder往往忘记抛出全局或者静态对象的指针(或构建heap中的新对象),返回了局部对象的指针,造成错误。并且4个标准的exceptionbad_alloc,bad_cast,bad_typeid,bad_exception)都是对象,只能by valueby reference方式捕捉。
另外,也可以在heap中构建一个新对象,抛出指向它的指针。这时捕捉到的人就必须删除之,否则会造成资源泄露。
但捕捉者又不清楚返回的是在heap中构建的新对象 还是 全局或静态对象的指针,因此就不知道是否要删除。

为了解决该困境,并与标准exception兼容,最好采用by valueby reference的方式捕捉exception

by value方式可能造成 对象切割(捕捉处是基类,抛出子类对象),并且复制2次;

故最好采用by reference捕捉异常对象。既可以避免对象切割,也可以捕捉标准异常,亦约束了exception对象复制的次数。


14、15

主要讨论的就是谨慎使用exception specifiction, 因为与说明不一致会造成程序terminate
且要考虑exception抛出造成的成本问题,必要时才使用try.


16.80-20法则


17.缓式评估

  • 本节首先举了字符串共享计数的方法,在需要赋值动作时,先不要真的拷贝赋值,而是记录这个字符串由哪些串共享,读操作不影响,写操作时才创建其副本进行更改。如果没有写操作,就可以避免非必要的对象复制。
  • 对于共享计数的字符串,读操作的代价远远低于写(需要创建副本),通过以proxy class 方法以及缓式评估的思想,在operator[]内区别读写操作。
  • 在取用大型对象时,可能只用其中某个成员,可以先构造空对象,内部置位null,用到时如果是null,再从数据库取出对应成员, 避免非必要的数据库读取动作
  • 最后举了矩阵计算的例子,如矩阵相乘的结果,可以记录乘法动作及参与矩阵,若只是取用某一行,则用的时候只计算那一行,避免非必要的数值计算动作。

但是缓式评估在某些计算是必要的情况下,反而会因为额外的记录信息浪费更大内存,且无提升。使用要依照场景而定。

最后比较有用的做法是,可以用急式评估的方法实现,撰写报告时将缓式评估的方法作为优化点写入报告。


19 分期摊还预期的计算成本

本讲提出了over-eager evaluation (如cachingprefetching等)分期摊还预期预算成本。这种做法与上讲的lazy evaluation使用场景不同。在必须支持某些运算而其结果几乎总是被需要,或常常被多次需要时适用。

举了提前计算平均值、最大值、最小值等例子,增量计算;
或者利用相对廉价的内存数据结构查找动作(如map存储查找)代替相对昂贵的数据库查询动作;
根据局部性原理多取数据的思想;
以及类似于vector源码中采用的动态数组分配2倍空间,减少系统调用分配空间,节省时间。

虽然本讲提倡的是 空间换时间。但事实并不总是这样,比如较大的对象不易塞入虚内存分页或缓存分页中。若换页活动增加,或缓存击中率降低,结果就适得其反。


19.了解临时对象的来源

C++所谓的临时对象不可见的。通常发生于2中情况:

  • 隐式类型转换时
    • 如将一个char*类型的字符串传入string类型的函数参数时,会产生一个string临时对象(以char*为自变量,调用string ctor)。
      • 上述情况只有在by value或者reference-to-const参数传递时才会产生临时对象。(因为若传递给非const的reference,产生了修改操作,修改的是临时对象,不符合修改意图)。
  • 函数返回对象时
    • 可以进行RVO优化优化临时对象的产生

20. Return Value Optimization

主要是讲RVO优化掉临时对象的产生。
考虑一个分数类:

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1);
	...
	int numerator() const;
	int denominator() const;
};

const Rational operator*(const Rational& lhs, const Rational& rhs);

operator*必须返回一个对象,若返回指针可能会造成资源泄露问题,返回引用又不正确(局部对象引用问题)。
必须以返回临时对象的方法才有效,这样却会造成多次构造析构(临时对象,函数返回对象)。
可以采用constructor arguments取代局部对象,当做返回值:

inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
	return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

Rational a = 10;
Rational b(1, 2);
Rational c = a * b;

这样C++允许编译器将临时对象优化,使其不复存在(得以消除operator*内的临时对象+返回的临时对象,即将return表达式定义的对象构造于c的内存内

另外:像下面这样先构造命名对象res,再返回该对象,编译器也会进行优化,叫做NRVOnamed return value optimization - 具名返回值优化)。

inline const Rational operator*(const Rational& lhs, const Rational& rhs) {
	Rational res(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
	return res;
}

Rational a = 10;
Rational b(1, 2);
Rational c = a * b;

RVO大部分编译器都支持。
NRVO要VC++8.0或以上才支持。


21.利用函数重载避免隐式转换

举的 自定义数据类型 和 int类型(内置类型) 重载operator+的例子中。int会被隐式转换为自定义数据类型,之后进行加法运算。

为了避免该隐式转换带来的开销,同时完成不同类型数据的加法任务。可以多次重载operator+运算符,支持不同了类型数值的运算。

  • 但要注意C++规定,每个重载操作符必须获得至少一个用户定制类型的自变量。(本例中不能重载出一个两个参数都是int的operator+,因为int是内置类型)。

22.考虑以操作符复合形式(op=)取代其独身形式(op)

在自定义类型时,往往以operator+=实现operator+

template<class T>
const T operator+(const T& lhs, const T& rhs) {
	return T(lhs) += rhs;	
	// T()以lhs构造一个临时对象,该临时对象用来调用operator+=,并以rhs为自变量。
}

operator+= (复合形式,返回引用)直接将结果写入左端自变量,不需要产生临时对象放置返回值。


23.考虑使用其他程序库

operator<<类型安全且可扩充,printf两者皆否。但iostream的速度慢,可执行文件会变大。
因此当发现程序中I/O是瓶颈,则可以考虑将以stdio取代iostream,当发现程序在动态分配内存方面花销很大,则可以考虑是否有其他提供了operator new operator delete的程序库。


24.了解虚函数、多重继承、虚基类、runtime type identification(RTTI)的成本

  • 凡是声明或继承虚函数的每个类,都拥有一个vtbl(virtual table),其中的条目是该类各个虚函数实现体的指针。(包括基类的虚函数)。
    • 1.拥有虚函数的类付出了额外vtbl空间的代价
  • virtual table pointer (vptr)存在于每个声明或继承了虚函数的class对象内,负责指示出每个对象对应于哪一个vtbl.
    • 2.拥有虚函数的对象付出了额外指针的代价
      pC1调用虚函数f1(),且该函数位于vtbl的i索引:
      (*pC1->vptr[i])(pC1);  // pC1被传给函数作为this指针
      
    • 3.另外,由于虚函数不能被声明为inline(inlining是编译期行为,虚函数是运行期),也相应的少了减少成本的机会。

多重继承中,每个子类对象中会包含多个vptr(每个基类对应一个)。
并且为了消除多重继承中的“钻石继承”问题,会采用虚继承的方式。此时,子类对象又会多出一个(或多个)指针用来指向虚基类成分,以此消除基类的复制行为。

RTTI让我们在运行期获得对象和类的相关信息,这些额外的信息被存放在类型为type_info的对象内。可以使用typeid操作符取得某个类对应的type_info对象。
一个类只需要拥有一份RTTI信息,其type_info对象经常在vtbl的索引0的条目内被指针指向。

  • 因此,其成本就是vtbl内的一个指针,和每个class所需的一份type_info对象空间。

25.将constructornon-member function虚化

所谓虚构造函数,是某种函数,视其获得的输入,可产生不同类型的对象。
作者举了如下例子:
一个类D拥有一个list成员,list中元素是基类A的指针。
现在需要从磁盘中读取对象并将其存入list。将读取对象的功能集中于一个函数内,由于其产生一个个崭新对象,行为类似构造函数,并且根据读入数据产生不同类型的对象(可能是A,也可能是子类B、C等),称为虚构造函数

也介绍了更为特殊的virtual copy constructor , 其返回一个指针,指向其调用者(某对象)的一个新副本。

class A {
public:
	virtual A * clone() const = 0;
	...
};

class B : public A {
public:
	// virtual copy constructor
	virtual B * clone() const {
		return new B(*this);
	}
	...
};


class D {
...
private:
	list<A*> components;
}

类D中list含有基类指针,指向不同类的对象。通过调用每个指针的clone()虚函数,达到拷贝构造不同对象的目的。

最后作者介绍了“虚化” 非成员函数 的做法:写一个虚函数做实际工作,再写一个非虚函数调用虚函数。如operator<<(可以inline降低成本)作为非成员函数,拥有参数ostream &s, 在其内调用虚函数print(s), 达到输出list内的不同对象,并且符合传统语法的目的。


26.限制某个class所能产生的对象数量

阻止某个类产出对象的最简单方法是将其constructor声明为private

若希望只能存在一个对象,可以将该对象设为static放置于函数内,而非类内:

class Printer {
public:
	...
	friend Printer& thePrinter();
private:
	Printer();
	Printer(const Printer& rhs);
	...
};
Printer& thePrinter() {
	static Printer p;	// 唯一的打印机对象
	return p;
}

  • 若放置于类内,成为static member function,调用需加类名,使用不方便;
  • 并且函数中的staitc对象在第一次调用时产生(代价是每次要检查是否需要产生),而class内static对象不论是否用到,都会被构造和析构。
  • 另外,函数内的static对象初始化时机是确定的(第一次调用函数且到达其定义处),class中的不一定什么时候初始化。

此外,作者举了另一个限制只能存在一个对象的错误例子,但很有代表性:
不将构造函数设为private,使用一个static对象计数当前产生了几个对象,若大于规定的对象数(设为1)就抛出异常,每次对象析构时计数减一。

  • 表面上看设计简洁,且可扩充。但是这种设计只是限制了对象产生的个数,而忽视了该对象的存在形态可以有多种(对象本身、存在于子类对象中、内嵌于其他对象)。若有子类继承于该类,产生多个子类对象时也会被抛出异常。(而将构造函数设为private的class是不能被继承的,也不能被嵌入其他对象内,避免了此问题)。

上述的函数内static对象的例子虽然保证了唯一的Printer对象,但不能够使用不同的对象(如程序不同位置先后使用不同的Printer对象,这样也是不允许的)

解决方案是采用伪构造函数 + 对象计数
将构造函数设为private禁用,而采用显示调用伪构造函数的方法调用构造函数,当构造对象大于限定数值则抛出异常或者返回null。

这样不但实现了限定个数的对象,更加灵活,也可以允许对象构造和析构。

最后,作者又实现了一个基类计数器,所有需要的class都可以继承它做计数以减少代码重复。


27.要求(或禁止)对象产生于heap之中

可以通过限制ctordtor的使用(private),阻止非堆对象产生。但会影响继承和内含

  • 可以将其设为protected解决继承问题,将内含该对象改为内含指向该对象指针解决内含问题。

另外,目前没有一个具有移植性的方法判断对象是否位于heap内。

  • 那种以程序的地址空间以线性序列组织而成,stack从高地址向低地址成长,heap从低地址向高地址成长为依据,判断是否位于堆内的方法是不完善的。因为除此之外还可能有static对象(声明为static的对象、全局范围、命名空间范围的对象),其位置是根据系统不同而不同的。

禁止对象产生于堆中的方法可以是将operator new(和delete)重载并置于private,但同样也会受到继承和内含关系不是意向行为的困扰。


28.智能指针

其主要内容类似:
简单实现智能指针

主要讨论了智能指针设计中所要经受的 nullness的测试、原生指针转换、以继承为本的转换、对pointers-to-consts的支持等挑战。

在继承相关的类型转换问题中提出:

智能指针不能够在继承相关的类型转换上做到与原生指针一样好,所能做的最好情况是使用member templates产生转换函数,然后在出现模棱两可时使用转型动作(如C++的理念是,对任何转换函数的调用动作,都一样好。传递给基类 或者 基类之基类的智能指针对象会被视为模棱两可)

另外,在支持pointers-to-consts时说道:
若编译器支持member template,可以以此解决;
若不支持,可以类比子类public继承基类(对子类可以做更多事情,对non-const对象同样),设计2个智能指针类,用于const对象,另一个用于non-const对象。
并在基类中使用union减少子类smartPtr对象内含的原生指针数目。(但要自行约束各类成员函数使用恰当的指针)

template<class T>
class SmartPtrToConst {
...
functions

protected:
	union {
		const T* constPointee;
		T* pointee;
	};
};

template<class T>
class SmartPtr : public SmartPtrToConst<T> {	// 没有数据成员,与基类共享union内的指针
...
};


29. 引用计数

作者逐层递进讲解了如何实现一个简单的string类,并使其具备引用计数功能。
其整体结构如下:
在这里插入图片描述
string类内包含RCPtr对象,相当于自动管理引用计数的智能指针,处理引用计数的细节;
该智能指针管理stringValue对象(含有实值,继承自引用计数对象RCObject(含有可共享标志、共享计数)).并且限制对象只在堆中产生(要用delete删除计数为0的对象)。

另外,作者还给出了一般类对象想要使用共享计数,但又不能继承RCObject classs的类的解决方案(如库内的无法更改),可以加一层间接性实现:
在这里插入图片描述

最后,讨论了共享计数的使用场景,毕竟实现较为复杂,并且有额外空间的代价。因此,对象数量/实值数量 比值越高,共享计数的收益越大。

注:上述讨论未解决共享计数在应用于某些数据结构时引发的 自我参考(或循环相依)结构

  • 循环引用

    class TestA{ public TestB b; }
    class TestB{ public TestA a; }
    public class Main{
    public static void main(String[] args)
    { A a = new A();//对象A的引用计数为1
    B b = new B(); //对象B的引用计数为1
    a.b=b; //对象B的引用计数为2
    b.a=a; //对象A的引用计数为2
    a = null; //对象A的引用计数为1
    b = null; //对象B的引用计数为1
    }
    }
    

    上述是循环引用的一个例子:将a=null后,对象A的引用计数为1,b=null后B的引用次数是1,不会释放对象B,A的引用计数就不会减为0,B也同样。这样就造成了误用共享计数,造成资源泄露无法释放的问题。


30 proxy classes

讲了使用proxy class解决 区分读写(左右值区分)operator[]多维数组的做法:

  • 多维数组中通过内嵌返回一维数组的对象的proxy class实现;(为了使用a[dim1][dim2]的形式)
  • 区分左右值时,调用operator[]后返回的是proxy object,后续根据读写的不同场景,调用不同的隐式类型转换函数完成区分读写场景。(如作者举例的cout << s[1]; 此处由于proxy object未重载operator<< , 故调用proxy class内的隐式转换char(), 而s[1] = 'a'; 则调用proxy class内的operator=

但使用proxy class扮演函数返回值的角色,这种proxy object是一种临时对象,也会有构造析构代价,且编码复杂度增高。
另外,proxy object展现的行为也会和真正对象有些许差异:

  • 如需要重载适用于真实对象的每一个函数(否则不能使用对应成员函数;
  • 传递给reference to non-const object时,无法将该代理对象转换为T& (即使能转,该proxy object为临时对象,也不允许绑定到non-const reference)
  • 还有就是不允许用户定制的一次以上的隐式转换,故使用代理类可能隐式转换也会有区别。

31.让函数根据 一个以上的对象类型 决定如何虚化

虚函数可以根据调用对象的动态类型决定调用哪个函数。当需要确定两个(或多个)对象的动态类型才能得到最终调用的函数时,单纯的虚函数就满足不了需求。
本讲讨论的就是让函数根据 一个以上的对象类型 决定如何虚化。(以两个为例)

首先给出了虚函数 + RTTI的方法。

  • 先根据虚函数机制确定第一个对象的真实类型,然后利用typeid()在运行期确定另一个对象的真实类型。但这种方法毫无封装性,虚函数要知道每个兄弟类。若添加一个新对象,就必须修改每个可能遇到新对象的类。

接着介绍了只用虚函数的方法:

  • 重载每个类型所需的虚函数,利用虚函数机确定一个对象的真实类型,然后在函数处理体内再次调用,利用虚函数确定第二个对象真实类型(将第一个对象真实类型作为参数传入,参数根据静态类型确定重载函数)。
  • 这种方法需要对头文件有修改权限,因为每增加一个新类型,就要对每个类增加一个新的重载函数(参数为新类型)。

通过对上述2种方法的讨论,我们应该尽可能避免这种double-dispatching的状况,如若不能,虚函数法则必RTTI更安全一些,但需要更大修改权限。RTTI不需要重新编译,但维护困难。

最后,介绍了自行仿真虚函数表格的方法。

这里要注意的是不能重载,需要定义多个不同的函数。因为要求每个函数的参数都要相同,才能够利用智能指针管理返回的指针(避免返回对象的成本)。

利用map映射函数名到函数指针(模仿虚表),并且最后还添加了自行往map添加和删除映射关系的函数,灵活性更大。


32. 在未来时态下编写程序

本讲列出了一些常见错误,如用户会抛出exception,会对对象自我赋值、会未获得初值就使用对象、会给初值而从来不使用、会给过大值、过小值、null值。这些不能仅仅依靠文档说明规范,而应该利用C++本身来表现各自规范

另外,尽量设计代码保证局部化:如尽量使用匿名namespaces、文件内的static对象和static函数;
尽量避免设计出虚基类(因为这种类必须被每一个子类初始化,即使是间接的);
避免RTTI作为设计基础导致一层层的if-then-else,因为一旦继承体系有变动,每一组这样的语句都要更新。

总结:本讲核心思想就是设计class时,要以未来时态完成,对class定位是如何的就如何设计,而不是根据现在情况(如定位为基类,即便现在没有子类,也设计出虚函数)。

  • 提供完整的类,即使有的部分暂时用不到;
  • 设计接口,让class轻易地被正确运用,难以被错误运用(如禁止拷贝和赋值操作);
  • 尽量泛化代码,除非有巨大的不良后果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值