More Effective C++

链接:https://pan.baidu.com/s/1oIns7Z7CWD6zAz17IFImWw 
提取码:4stq

Scott Meyers大师Effective三部曲:Effective C++、More Effective C++、Effective STL。

1. 指针与引用的区别

相同点:指针和引用都是让你间接引用其它对象。

不同点:

1.在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。在C++里,引用应被初始化。 不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。

2.指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。

    std::string s1("Nancy");
	std::string s2("Clancy");
	std::string& rs = s1; // rs引用s1
	std::string* ps = &s1; // ps指向s1
	rs = s2; // rs仍旧引用s1,但是s1的值现在是"Clancy"

参考:c++ 引用和指针_baidu_16370559的博客-CSDN博客

2. 尽量使用C++风格的类型转换

这四个操作符是:static_cast、const_cast、dynamic_cast、reinterpret_cast。

参考:c++ 4种新型的类型static_cast、dynamic_cast、reinterpret_cast、const_cast转换运算符_baidu_16370559的博客-CSDN博客

3. 不要对数组使用多态 

在对数组进行传参使用多态时,程序会crash; 因为数组在移位至下一数据时,步长是形参(基类)的size,而不是指针实际指向数据类型(派生类)的size,所以会数组会移位至一个非法的地址 。

#include <iostream>
using namespace std;
 
class Base
{
public:
  virtual void test()
  {
    cout<<"Base::test()"<<endl;
  }
  int a;
};
 
class Derived: public Base
{
public:
   void test()
  {
    cout<<"Derived::test()"<<endl;
  }
  int b, c;
};
 
void testArray(Base bArray[], int n)
{
  for(int i =0; i<n; i++)
    bArray[i].test();  //i = 1时,程序crash; 编译器原先已经假设数组中元素与Base对象的大小一致,但是现在数组中每一个对象大小却与Derived一致,派生类的长度比基类要长,数组将移动到一个非法位置。
}
 
int main()
{
  Base *p = new Derived[2];  
  testArray(p, 2);    
}
 

 4. 避免无用的缺省构造函数

对于很多对象来说,不利用外部数据进行完全的初始化是不合理的.

class EquipmentPiece {
public:
	EquipmentPiece(int IDNumber) {}
	virtual ~EquipmentPiece() {}
	int a = 1;
	float b = 2.0;
};

   //避免无用的缺省构造函数
	int ID1 = 1, ID2 = 2;
	EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) }; // 正确,提供了构造函数的参数

	// 利用指针数组来代替一个对象数组
	typedef EquipmentPiece* PEP; // PEP指针指向一个EquipmentPiece对象
	PEP* bestPieces5 = new PEP[10]; // 也正确
	// 在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象
	for (int i = 0; i < 10; ++i)
		bestPieces5[i] = new EquipmentPiece(ID1);
	for (int i = 0; i < 10; ++i)
		delete bestPieces5[i];
	delete bestPieces5;

利用指针数组代替一个对象数组这种方法有两个缺点:第一你必须删除数组里每个指针所指向的对象。如果忘了,就会发生内存泄漏。第二增加了内存分配量,因为正如你需要空间来容纳EquipmentPiece对象一样,你也需要空间来容纳指针.

解决办法:

	void* rawMemory = operator new[](10 * sizeof(EquipmentPiece));
	EquipmentPiece* bestPieces6 = static_cast<EquipmentPiece*>(rawMemory);

	for (int i = 0; i < 10; ++i)
		new(&bestPieces6[i]) EquipmentPiece(i);

	for (int i = 9; i >= 0; --i)
		bestPieces6[i].~EquipmentPiece(); // 如果使用普通的数组删除方法,程序的运行将是不可预测的

	operator delete[](rawMemory);

参考:c++ new操作符(new operator)、operator new、placement new 、operator new[] 及相对应的delete 操作符、operator delete_baidu_16370559的博客-CSDN博客

5. 谨慎定义类型转换函数 

有两种函数允许编译器进行这些的转换:单参数构造函数(single-argument constructors)和隐式类型转换运算符

隐式类型转换运算符只是一个样子奇怪的成员函数:operator关键字,其后跟一个类型符号。你不用定义函数的返回类型,因为返回类型就是这个函数的名字。

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1) // 转换int到有理数类
	{
		n = numerator;
		d = denominator;
	}

	operator double() const // 转换Rational类成double类型
	{
		return static_cast<double>(n) / d;
	}

	double asDouble() const
	{
		return static_cast<double>(n) / d;
	}

private:
	int n, d;
};

	//谨慎定义类型转换函数
	Rational r(1, 2); // r的值是1/2
	double d = 0.5 * r; // 转换r到double,然后做乘法
	fprintf(stdout, "value: %f\n", d);
	std::cout << r << std::endl; // 应该打印出"1/2",但事与愿违,是一个浮点数,而不是一个有理数,隐式类型转换的缺点

单参数构造函数是指只用一个参数即可调用的构造函数。该函数可以是只定义了一个参数,也可以是虽定义了多个参数但第一个参数以后的所有参数都有缺省值。 同时因为默认是implicit(隐式)。

explicit关键字是为了解决隐式类型转换而特别引入的这个特性。如果构造函数用explicit声明,编译器会拒绝为了隐式类型转换而调用构造函数。

template<class T>
class Array {
public:
	Array(int lowBound, int highBound) {}
	explicit Array(int size) {}
public:
	T& operator[](int index) { return data[index]; }



private:
	T* data;
};
bool operator== (const Array<int>& lhs, const Array<int>& rhs)
{
	return false;
}
	Array<int> a(10);
	Array<int> b(10);
	for (int i = 0; i < 10; ++i) {
		//if (a == b[i]) {} // 如果构造函数Array(int size)没有explicit关键字,编译器将能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int类型的参数,加上explicit关键字则可避免隐式转换

		if (a == Array<int>(b[i])) {} // 正确,显示从int到Array<int>转换(但是代码的逻辑不合理)
		if (a == static_cast<Array<int>>(b[i])) {} // 同样正确,同样不合理
		if (a == (Array<int>)b[i]) {} // C风格的转换也正确,但是逻辑依旧不合理
	}

6. 自增(increment)、自减(decrement)操作符前缀形式与后缀形式的区别 

C++规定后缀形式有一个int类型参数,当函数被调用时,编译器传递一个0作为int参数的值给该函数。

前缀形式有时叫做”增加然后取回”, 返回的是引用,效率高

后缀形式叫做”取回然后增加”。返回的是新的值,值和原来的一样。效率低。


class UPInt { // unlimited precision int
public:
	// 注意:前缀与后缀形式返回值类型是不同的,前缀形式返回一个引用,后缀形式返回一个const类型
	UPInt& operator++() // ++前缀
	{
		//*this += 1; // 增加
		i += 1;
		return *this; // 取回值
	}
 
	const UPInt operator++(int) // ++后缀
	{
		// 注意:建立了一个显示的临时对象,这个临时对象必须被构造并在最后被析构,前缀没有这样的临时对象
		UPInt oldValue = *this; // 取回值
		// 后缀应该根据它们的前缀形式来实现
		++(*this); // 增加
		return oldValue; // 返回被取回的值
	}
 
	UPInt& operator--() // --前缀
	{
		i -= 1;
		return *this;
	}
 
	const UPInt operator--(int) // --后缀
	{
		UPInt oldValue = *this;
		--(*this);
		return oldValue;
	}
};

7. 不要重载”&&”, “||”,或”,”

与C一样,C++使用布尔表达式短路求值法(short-circuit evaluation)。这表示一旦确定了布尔表达式的真假值,即使还有部分表达式没有被测试,布尔表达式也停止运算。

你不能重载下面的操作符:
.  .*   ::    ?:
new  delete  sizeof  typeid
static_cast dynamic_cast const_cast reinterpret_cast
你能重载:
operator new     operator delete
operator new[]    operator delete[]
+  -  *  /  %  ^  &  | ~
!  =  <  >  +=  -=  *=  /=  %=
^=  &=  |=  <<  >>  >>=  <<=  ==  !=
<=  >=  &&  ||  + + --  ,  ->*  ->
()  [] 

8. 理解各种不同含义的new和delete

new操作符(new operator)和new操作(operator new)的区别:

new操作符就像sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分成两部分。第一部分是分配足够的内存以便容纳所需类型的对象。第二部分是它调用构造函数初始化内存中的对象。new操作符总是做这两件事情,你不能以任何方式改变它的行为。你所能改变的是如何为对象分配内存。new操作符调用一个函数来完成必须的内存分配,你能够重写或重载这个函数来改变它的行为。new操作符为分配内存所调用函数的名字是operator new。

函数operator new通常声明:返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存。参数size_t确定分配多少内存。你能增加额外的参数重载函数operator new,但是第一个参数类型必须是size_t。就像malloc一样,operator new的职责只是分配内存。它对构造函数一无所知。把operator new返回的未经处理的指针传递给一个对象是new操作符的工作。

placement new:特殊的operator new,接受的参数除了size_t外还有其它。

class EquipmentPiece {
public:
	EquipmentPiece(int IDNumber) {}
	virtual ~EquipmentPiece() {}
	int a = 1;
	float b = 2.0;
};
	void* rawMemorysingle = operator new(sizeof(EquipmentPiece));
	EquipmentPiece* bestPiecesrawMemorysingle = static_cast<EquipmentPiece*>(rawMemorysingle);
	new(bestPiecesrawMemorysingle) EquipmentPiece(1);
	bestPiecesrawMemorysingle->~EquipmentPiece();
	operator delete(rawMemorysingle);


	//避免无用的缺省构造函数
	int ID1 = 1, ID2 = 2;
	EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) }; // 正确,提供了构造函数的参数

	// 利用指针数组来代替一个对象数组
	typedef EquipmentPiece* PEP; // PEP指针指向一个EquipmentPiece对象
	PEP* bestPieces5 = new PEP[10]; // 也正确
	// 在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象
	for (int i = 0; i < 10; ++i)
		bestPieces5[i] = new EquipmentPiece(ID1);
	for (int i = 0; i < 10; ++i)
		delete bestPieces5[i];
	delete bestPieces5;

	void* rawMemory = operator new[](10 * sizeof(EquipmentPiece));
	EquipmentPiece* bestPieces6 = static_cast<EquipmentPiece*>(rawMemory);

	for (int i = 0; i < 10; ++i)
		new(&bestPieces6[i]) EquipmentPiece(i);

	for (int i = 9; i >= 0; --i)
		bestPieces6[i].~EquipmentPiece(); // 如果使用普通的数组删除方法,程序的运行将是不可预测的

	operator delete[](rawMemory);


参考:c++ new操作符(new operator)、operator new、placement new 、operator new[] 及相对应的delete 操作符、operator delete_baidu_16370559的博客-CSDN博客

9. 使用析构函数防止资源泄漏

用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源,这种思想不只是可以运用在指针上,还能用在其它资源的分配和释放上。

资源应该被封装在一个对象里,遵循这个规则,你通常就能够避免在存在异常环境里发生资源泄漏,通过智能指针的方式。

C++确保删除空指针是安全的,所以析构函数在删除指针前不需要检测这些指针是否指向了某些对象。

延伸

1.智能指针,参考c++ 智能指针auto_ptr (c++98)、shared_ptr(c++ 11)、unique_ptr(c++ 11)、weak_ptr(c++ 11)_baidu_16370559的博客-CSDN博客

2.异常:c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客 

 10. 在构造函数中防止资源泄漏

C++仅仅能删除被完全构造的对象(fully constructed objects),只有一个对象的构造函数完全运行完毕,这个对象才被完全地构造。C++拒绝为没有完成构造操作的对象调用析构函数。

具体表现为:在执行构造函数函数体是抛出异常,该类的成员变量已经被完全构造,可以自动删除掉。而本身没有完全构造,其本身的析构函数不会被调用。

如果使用new 的方式创建对象即A *pa = new A();

如果A的构造函数有异常,A没有被完全构造,new 操作失败,返回的指针pa为空,后面使用delete pa;也不会调用 A的析构函数。

延伸:

对于使用正常签名式的new来创建对象时,其可能在两个地方会抛出异常:
①当在调用new()时,new()函数中可能会抛出异常
②如果new()函数没有抛出异常,但是后面对象的构造函数可能会抛出异常。系统会自动调用delete()来释放new()所做的一切,使其恢复原状(释放内存等)。
分析:构造函数抛出异常,指针尚未被赋值,客户端无法取得该指针归还内存,因此此处的内存归还操作就交给了C++运行期系统身上。注意:该delete操作不会调用对象的析构函数(前提是要处理掉异常),只释放内存。
参见:c++ new操作符(new operator)、operator new、placement new 、operator new[] 及相对应的delete 操作符、operator delete_小飞侠hello的博客-CSDN博客

解决办法:

1在构造函数中可以使用try catch throw捕获所有的异常。

参考:c++ 中 try catch throw异常_小飞侠hello的博客-CSDN博客

2.更好的解决方法是同时使用智能指针和初始化列表的方式。

参考:C++构造函数初始化列表与赋值_baidu_16370559的博客-CSDN博客

11. 禁止异常信息(exceptions)传递到析构函数外

禁止异常传递到析构函数外有两个原因:第一能够在异常传递的堆栈辗转开解(stack-unwinding)的过程中,防止terminate被调用。第二它能帮助确保析构函数总能完成我们希望它做的所有事情。

解决办法使用 try catch 函数

延伸:

1.C++中处理异常的过程是这样的:在执行程序发生异常,可以不在本函数中处理,而是抛出一个错误信息,把它传递给上一级的函数来解决,上一级解决不了,再传给其上一级,由其上一级处理。如此逐级上传,直到最高一级还无法处理的话,运行系统会自动调用系统函数terminate.

关于try catch throw 知识参考:

c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客

2.这个和effective c++ 条款8 别让异常逃离析构函数 一样的参考effective c++ 学习_baidu_16370559的博客-CSDN博客

12. 理解”抛出一个异常”与”传递一个参数”或”调用一个虚函数”间的差异

你调用函数时,程序的控制权最终还会返回到函数的调用处,但是当你抛出一个异常时,控制权永远不会回到抛出异常的地方。

C++规范要求被作为异常抛出的对象必须被复制。即使被抛出的对象不会被释放,也会进行拷贝操作。抛出异常运行速度比参数传递要慢。

当异常对象被拷贝时,拷贝操作是由对象的拷贝构造函数完成的。该拷贝构造函数是对象的静态类型(static type)所对应类的拷贝构造函数,而不是对象的动态类型(dynamic type)对应类的拷贝构造函数。

class Widget { ... };
class SpecialWidget: public Widget { ... };
void passAndThrowWidget()
{
SpecialWidget localSpecialWidget;
...
Widget& rw = localSpecialWidget; // rw 引用 SpecialWidgetthrow rw; //它抛出一个类型为 Widget
// 的异常
}
这里抛出的异常对象是 Widget,即使 rw 引用的是一个 SpecialWidget。因为 rw 的静态
类型(static
type)是 Widget,而不是 SpecialWidget。

catch子句中进行异常匹配时可以进行两种类型转换:第一种是派生类与基类间的转换。一个用来捕获基类的catch子句也可以处理派生类类型的异常。这种派生类与基类(inheritance_based)间的异常类型转换可以作用于数值、引用以及指针上。第二种是允许从一个类型化指针(typed pointer)转变成无类型指针(untyped pointer),所以带有const void*指针的catch子句能捕获任何类型的指针类型异常。

catch子句匹配顺序总是取决于它们在程序中出现的顺序。异常传递到 catch 子句中有三种方式:通过指针(by pointer),通过传值(by value)或通过引用(by reference),其中引用最好

参考见:c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客

13. 通过引用(reference)捕获异常

异常传递到 catch 子句中有三种方式:通过指针(by pointer),通过传值(by value)或通过引用(by reference),其中引用最好.

1..通过指针的方式,对静态异常变量catch中不需要delete,但堆上异常变量需要delete,因此较复杂。而且通过指针捕获异常也不符合C++语言本身的规范。四个标准的异常----bad_alloc(当operator new不能分配足够的内存时被抛出);bad_cast(当dynamic_cast针对一个引用(reference)操作失败时被抛出);bad_typeid(当dynamic_cast对空指针进行操作时被抛出);bad_exception(用于unexpected异常)----都不是指向对象的指针,所以你必须通过值或引用来捕获它们。

2.通过传值时,需要进行拷贝两次(离开作用域一次,catch接收一次),而且它会产生 slicing problem(切割问题),即派生类的异常对象被做为基类异常对象捕获时,那它的派生类行为就被切掉了(sliced off)。这样的sliced对象实际上是一个基类对象:它们没有派生类的数据成员,而且当本准备调用它们的虚拟函数时,系统解析后调用的却是基类对象的函数。

3.异常变量复制一次,避免了上述所有问题

参考:c++ 中 try catch throw异常_baidu_16370559的博客-CSDN博客

14. 审慎使用异常规格(exception specifications)

如果一个函数抛出一个不在异常规格范围里的异常,系统在运行时能够检测出这个错误,然后一个特殊函数std::unexpected将被自动地调用(This function is automatically called when a function throws an exception that is not listed in its dynamic-exception-specifier.)。std::unexpected缺省的行为是调用函数std::terminate,而std::terminate缺省的行为是调用函数abort。应避免调用std::unexpected。

1.模板和异常规格不要混合使用.

2.能够避免调用 unexpected 函数的第二个方法是如果在一个函数内调用其它没有异常规
格的函数时应该去除这个函数的异常规格.

C++允许你用其它不同的异常类型替换std::unexpected异常,通过std::set_unexpected

15. 了解异常处理的系统开销

采用不支持异常的方法编译的程序一般比支持异常的程序运行速度更快所占空间也更小。

为了减少开销,你应该避免使用无用的try块。如果使用try块,代码的尺寸将增加并且运行速度也会减慢。

16. 牢记80-20准则(80-20 rule)

80-20准则说的是大约20%的代码使用了80%的程序资源;大约20%的代码耗用了大约80%的运行时间;大约20%的代码使用了80%的内存;大约20%的代码执行80%的磁盘访问;80%的维护投入于大约20%的代码上。基本的观点:软件整体的性能取决于代码组成中的一小部分。

17. 考虑使用lazy evaluation(懒惰计算法)

在某些情况下要求软件进行原来可以避免的计算,这时lazy evaluation才是有用的。

18. 分期摊还期望的计算

over-eager evaluation(过度热情计算法):在要求你做某些事情以前就完成它们。隐藏在over-eager evaluation后面的思想是如果你认为一个计算需要频繁进行,你就可以设计一个数据结构高效地处理这些计算需求,这样可以降低每次计算需求时的开销。

当你必须支持某些操作而不总需要其结果时,lazy evaluation是在这种时候使用的用以提高程序效率的技术。当你必须支持某些操作而其结果几乎总是被需要或不止一次地需要时,over-eager是在这种时候使用的用以提高程序效率的一种技术。

19. 理解临时对象的来源

在C++中真正的临时对象是看不见的,它们不出现在你的源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象。这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换函数返回对象时
函数成功调用而进行隐式类型转换的场景:

当传送给函数的对象类型与参数类型不匹配时会产生这种情况,仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生。C++语言禁止为非常量引用(reference-to-non-const)产生临时对象。

常量引用的介绍见:C++ 常量引用用法_baidu_16370559的博客-CSDN博客

消除隐式类型转换的办法:
一种是重新设计你的代码,不让发生这种类型转换。这种方法在条款M5中被研究和分析。

另一种方法是通过修改软件而不再需要类型转换,条款M21讲述了如何去做。

函数返回对象时

最常见和最有效的是返回值优化。

临时对象是有开销的,所以你应该尽可能地去除它们。在任何时候只要见到常量引用(reference-to-const)参数,就存在建立临时对象而绑定在参数上的可能性。在任何时候只要见到函数返回对象,就会有一个临时对象被建立(以后被释放)。

20. 协助完成返回值优化

一些函数(operator*也在其中)必须要返回对象。这就是它们的运行方法。

C++规则允许编译器优化不出现的临时对象(temporary objects out of existence)。

从效率的观点来看,你不应该关心函数返回的对象,你仅仅应该关心对象的开销。你所应该关心的是把你的努力引导到寻找减少返回对象的开销上来,而不是去消除对象本身(我们现在认识到这种寻求是无用的)

class Rational20 {
public:
	Rational20(int numerator = 0, int denominator = 1) {}
 
	int numerator() const { return 1; }
	int denominator() const { return 2; }
};
 
const Rational20 operator*(const Rational20& lhs, const Rational20& rhs)
{
	// 以某种方法返回对象,能让编译器消除临时对象的开销:这种技巧是返回constructor argument而不是直接返回对象
	return Rational20(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
 
int test_item_20()
{
	Rational20 a = 10;
	Rational20 b(1, 2);
	Rational20 c = a * b; 
 
	return 0;
}

21. 通过重载避免隐式类型转换

在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型(user-defined type)的参数。利用重载避免临时对象的方法不只是用在operator函数上。

没有必要实现大量的重载函数,除非你有理由确信程序使用重载函数以后其整体效率会有显著的提高。

class UPInt21 { // unlimited precision integers class
public:
	UPInt21() {}
	UPInt21(int value) {}
};
 
const UPInt21 operator+(const UPInt21& lhs, const UPInt21& rhs) // add UPInt21+UPInt21
{
	return UPInt21(1);
}
 
const UPInt21 operator+(const UPInt21& lhs, int rhs) // add UPInt21+int
{
	return UPInt21(1);
}
 
const UPInt21 operator+(int lhs, const UPInt21& rhs) // add int+UPInt21
{
	return UPInt21(1);
}
 
int test_item_21()
{
	UPInt21 upi1, upi2;
	UPInt21 upi3 = upi1 + upi2; // 正确,没有由upi1或upi2生成临时对象
	upi3 = upi1 + 10; // 正确,没有由upi1或10生成临时对象
	upi3 = 10 + upi2; // 正确,没有由10或upi2生成临时对象
 
	// 注意:注释掉上面的operator+(UPInt21&, int)和operator+(int, UPInt21&)也正确,但是会通过临时对象把10转换为UPInt21
 
	return 0;
}

此外另外一种避免隐式类型转换见条款5

22. 考虑用运算符的赋值形式(op=)取代其单独形式(op)

就C++来说,operator+、operator=和operator+=之间没有任何关系,因此如果你想让三个operator同时存在并具有你所期望的关系,就必须自己实现它们。同理,operator-, *, /, 等等也一样。

确保operator的赋值形式(assignment version)(例如operator+=)与一个operator的单独形式(stand-alone)(例如operator+)之间存在正常的关系,一种好方法是后者(指operator+)根据前者(指operator+=)来实现。
 

23. 考虑变更程序库

不同的程序库在效率、可扩展性、移植性、类型安全和其它一些领域上蕴含着不同的设计理念,通过变换使用给予性能更多考虑的程序库,你有时可以大幅度地提供软件的效率。

24. 理解虚拟函数、多继承、虚基类和RTTI所需的代码

当调用一个虚拟函数时,被执行的代码必须与调用函数的对象的动态类型相一致;指向对象的指针或引用的类型是不重要的。大多数编译器是使用virtual table和virtual table pointers,通常被分别地称为vtbl和vptr。

一个vtbl通常是一个函数指针数组。(一些编译器使用链表来代替数组,但是基本方法是一样的)在程序中的每个类只要声明了虚函数或继承了虚函数,它就有自己的vtbl,并且类中vtbl的项目是指向虚函数实现体的指针。

你必须为每个包含虚函数的类的virtual table留出空间。类的vtbl的大小与类中声明的虚函数的数量成正比(包括从基类继承的虚函数)。每个类应该只有一个virtual table,所以virtual table所需的空间不会太大,但是如果你有大量的类或者在每个类中有大量的虚函数,你会发现vtbl会占用大量的地址空间。

一些原因导致现在的编译器一般总是忽略虚函数的inline指令。

Virtual table只实现了虚拟函数的一半机制,如果只有这些是没有用的。只有用某种方法指出每个对象对应的vtbl时,它们才能使用。这是virtual table pointer的工作,它来建立这种联系。每个声明了虚函数的对象都带着它,它是一个看不见的数据成员,指向对应类的virtual table。这个看不见的数据成员也称为vptr,被编译器加在对象里,位置只有编译器知道。

虚函数是不能内联的。这是因为”内联”是指”在编译期间用被调用的函数体本身来代替函数调用的指令”,但是虚函数的”虚”是指”直到运行时才能知道要调用的是哪一个函数”。

虚函数实现原理见:c++ 虚函数实现原理_baidu_16370559的博客-CSDN博客

RTTI(运行时类型识别)能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息让我们查询。这些信息被存储在类型为type_info的对象里,你能通过使用typeid操作符访问一个类的type_info对象。

RTTI被设计为在类的vtbl基础上实现。

25. 将构造函数和非成员函数虚拟化

虚拟构造函数是指能够根据输入给它的数据的不同而建立不同类型的对象。

虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝。类的虚拟拷贝构造函数只是调用它们真正的拷贝构造函数。这个有点像设计模式中的原型模式。

被派生类重定义的虚拟函数不用必须与基类的虚拟函数具有一样的返回类型。如果函数的返回类型是一个指向基类的指针(或一个引用),那么派生类的函数可以返回一个指向基类的派生类的指针(或引用)。

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

阻止建立某个类的对象,最容易的方法就是把该类的构造函数声明在类的private域

第一种办法:定义一个全局函数,同时使其成为类的友元函数(目的就是可以访问该类的私有构造函数)。在全局函数中,创建类对象。

第二种办法:定义一个类的静态函数,在类的静态函数中,创建类对象。

不管是全局函数还是类的静态函数,如果控制只限制生成一个对象,可以创建静态对象,参考设计模式中的单例模式。

class  ctestprint
{
private:
	 ctestprint();
	 ~ctestprint();
	 int m_a;
protected:
	int m_b;
public:
	int m_c;
	 friend ctestprint& createobj();
	 static ctestprint& create();
};
ctestprint& createobj();
ctestprint::ctestprint()
{

}

ctestprint::~ctestprint()
{

}

ctestprint& ctestprint::create()
{
	static ctestprint print;
	return print;
}

ctestprint& createobj()
{
	static ctestprint print;
	return print;
}
	createobj().m_c = 1;
	ctestprint::create().m_c = 3;

要限制类的个数,可以定义一个类的静态成员用于计数,在构造函数中加1,在析构函数中减1.

class  ctestprint
{
public:
	 ctestprint();
	 ~ctestprint();
	 int m_a;
protected:
	int m_b;
public:
	int m_c;
	static int  m_ncount;
};
int ctestprint::m_ncount = 0;
ctestprint::ctestprint()
{
	if (m_ncount >= 2)
	{
		string str("create toomanyobj");
		throw str;
	}
	else
	{
		++m_ncount;
	}
}

ctestprint::~ctestprint()
{
	--m_ncount;
}

	ctestprint *print;
	try
	{
		print = new ctestprint[3];
	}
	catch (string &str)
	{
		cout << str << endl;
		cout << ctestprint::m_ncount << endl;
	}

 输出 : ctestprint::m_ncount = 0

因为new 数组对象调用构造函数抛出异常,系统会自动调用前面成功的个数(上面例子就是2次)对象的析构函数,并且自动整个释放内存。

27.要求或禁止在堆中产生对象

1.要求在堆中建立对象(即阻止建立非堆对象

反向思维:通过限制访问一个类的析构函数或它的构造函数来阻止建立非堆对象

有2种办法:

1.让析构函数成为 private,让构造函数成为 public。同时定义一个伪析构函数,让其delete this;释放自己类的资源。最好用该办法,因为析构函数就一个,而构造函数有多个。
2.把全部的构造函数都声明为 private。个人理解:这种办法的后续手段应该是定义一个类的静态成员函数,在其函数new 对象。
但两种办法都是问题,就是该类不能派生继承和包容。所以更好的办法是把析构函数private 改成protected,这样就解决继承的问题。把需要包含类的对象修改为包含指向该类的指针(即组合关系改成依赖关系)。

class  ctestprint
{
public:
	 ctestprint();
private:
	int m_a;
protected:
	 ~ctestprint();
	int m_b;
public:
	int m_c;
	void deleteobj();
};

class deprint : public ctestprint
{
public:
	deprint();
	~deprint();
};

class cuseprint
{
public:
	cuseprint();
	~cuseprint();
	ctestprint *pprint;
};
ctestprint::ctestprint()
{

}

ctestprint::~ctestprint()
{

}

void ctestprint::deleteobj()
{
	delete this;
}

deprint::deprint()
{

}

deprint::~deprint()
{

}

cuseprint::cuseprint()
{
	pprint = new ctestprint();
}

cuseprint::~cuseprint()
{
	pprint->deleteobj();
}

	ctestprint *print = new ctestprint;
	print->deleteobj();

	deprint *pdeprint = new deprint;
	pdeprint->deleteobj();

	cuseprint  useprint;

2. 禁止堆对象

禁止用于调用new,利用new操作符总是调用operator new函数这点来达到目的,可以自己声明这个函数,而且你可以把它声明为private。即

private:
void *operator new(size_t size);
void  operator delete(void *ptr);

class cteststudy
{
public:
	cteststudy() {}
	~cteststudy() {}
private:
	 void * operator new (size_t size);
	 void  operator delete(void *ptr);
};
只能:
cteststudy   study;

通常对象的建立这样三种情况:对象被直接实例化(依赖关系);对象做为派生类的基类被实例化(继承);对象被嵌入到其它对象内(组合)。
 

对象做为派生类的基类被实例化(继承)

当operator new 和 operator delete 在基类是 private 的版本。因为 operator new 和 operator delete 是自动继承的,如果 operator new 和 operator delete 没有在派生类中被声明为 public(进行改写,overwrite),它们就会继承基类中 private 的版本。所以要在派生类把 operator new 和 operator delete 声明为public。

class cdestudy : public  cteststudy
{
public:
	cdestudy() {}
	~cdestudy() {}
	void * operator new (size_t size);
	void  operator delete(void *ptr);
};
void * cdestudy::operator new(size_t size)
{
	return ::operator new(size);
}

void cdestudy::operator delete(void *ptr)
{
	::operator delete (ptr);
}
    cdestudy destudy;
	cdestudy   *pstudy = new cdestudy;
	delete pstudy;

对象被嵌入到其它对象内(组合)。

class cusestudy
{
public:
	cusestudy() {};
	~cusestudy() {};
	cteststudy  test1;
	cdestudy  test2;
};

	cusestudy usedestudy;
	cusestudy   *pusestudy = new cusestudy;
	delete pusestudy;

28. 灵巧(smart)指针

灵巧指针其实就是智能指针

灵巧指针是一种外观和行为都被设计成与内建指针相类似的对象,不过它能提供更多的功能。它们有许多应用的领域,包括资源管理和重复代码任务的自动化。

在C++11中auto_ptr已经被废弃,用unique_ptr替代。

参考:c++ 智能指针auto_ptr (c++98)、shared_ptr(c++ 11)、unique_ptr(c++ 11)、weak_ptr(c++ 11)_baidu_16370559的博客-CSDN博客

29. 引用计数 

引用计数是这样一个技巧,它允许多个有相同值的对象共享这个值的实现。这个技巧有两个常用动机。第一个是简化跟踪堆中的对象的过程。一旦一个对象通过调用new被分配出来,最要紧的就是记录谁拥有这个对象,因为其所有者----并且只有其所有者----负责对这个对象调用delete。但是,所有权可以被从一个对象传递到另外一个对象(例如通过传递指针型参数)。引用计数可以免除跟踪对象所有权的担子,因为当使用引用计数后,对象自己拥有自己。当没人再使用它时,它自己自动销毁自己。因此,引用计数是个简单的垃圾回收体系。第二个动机是由于一个简单的常识。如果很多对象有相同的值,将这个值存储多次是很无聊的。更好的办法是让所有的对象共享这个值的实现。这么做不但节省内存,而且可以使得程序运行更快,因为不需要构造和析构这个值的拷贝。

实现引用计数不是没有代价的。每个被引用的值带一个引用计数,其大部分操作都需要以某种形式检查或操作引用计数。对象的值需要更多的内存,而我们在处理它们时需要执行更多的代码。引用计数是基于对象通常共享相同的值的假设的优化技巧。如果假设不成立的话,引用计数将比通常的方法使用更多的内存和执行更多的代码。另一方面,如果你的对象确实有具有相同值的趋势,那么引用计数将同时节省时间和空间。

参考见:c++ 智能指针auto_ptr (c++98)、shared_ptr(c++ 11)、unique_ptr(c++ 11)、weak_ptr(c++ 11)_baidu_16370559的博客-CSDN博客

 30. 代理类

可以通过代理类实现二维数组。

可以通过代理类帮助区分通过operator[]进行的是读操作还是写操作。

Proxy类可以完成一些其它方法很难甚至可不能实现的行为。多维数组是一个例子,左值/右值的区分是第二个,限制隐式类型转换是第三个。

同时,proxy类也有缺点。作为函数返回值,proxy对象是临时对象,它们必须被构造和析构。Proxy对象的存在增加了软件的复杂度。从一个处理实际对象的类改换到处理proxy对象的类经常改变了类的语义,因为proxy对象通常表现出的行为与实际对象有些微妙的区别。
 

  • 3
    点赞
  • 10
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论

打赏作者

小飞侠hello

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值