More effective C++ 35 提炼

一 基础议题

1 仔细区别pointers和references

  • pointers可以指向null,但是references不行。
  • references必须要有初值,但pointers不需要。
  • pointers可以被重新赋值,但是references不行。

当你知道你需要指向某个东西,而且绝对不会改变指向其他东西,或是当你实现一个操作符而其他语法需求无法有pointers达成,你就应该选择references。任何其他时候,请采用pointers。

2 最好使用C++转型操作符

static_cast基本上拥有与C旧式转型相同的威力和意义,以及相同的限制。

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

4 非必要不提供default constructor

二 操作符

1 对定制的”类型转换函数“保持警觉

类的两种函数可以做到类型转换:

  • 单自变量constructors
  • 隐式类型转换操作符

允许编译器执行隐式类型转换,害处将多过好处。所以不要提供转换函数,除非你确定你需要它们。

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

前置式比后置式效率更高,没有必要的理由,应该优先选择前置式。而且后置式操作符应以前置式操作符为实现基础。因为后置形式会产生一个临时对象

3 千万不要重载&&,|| 和,操作符

“函数调用“语义和”骤死式“语义有两个重大的区别。第一,当函数调用动作被执行,所有参数值都必须评估完成。第二,C++语言规范并没有明确定义函数调用动作中各参数的评估顺序。

如果重载&&,|| 和,无法令其行为像它们应有的行为一样。

4 了解各种不同意义的new 和 delete

operator new 唯一的任务就是分配内存。

取得operator new 返回的内存并将之转换为一个对象,是new operator的责任。

如果你希望将对象产生于heap,请使用new operator。它不但分配内存而且为该对象调用一个constructor。

如果你只打算分配内存,请调用operator new,那就没有任何constructor会被调用。

operator new, new operator,placement new

数组:

operator new[]

new operator (其他教程称为new expression)。

这个操作符是由语言内建的,就像是sizeof那样,不能被改变意义,总是做相同的事情。它的动作分为两方面:

  1. 分配足够的内存,用来放置某类型的对象。
  2. 然后调用一个constructor,为刚才分配的内存中的那个对象设定初值。

它总是做这两件事,无论如何不能改变其行为。

operator new:

唯一的任务就是分配内存。

比如:

void *rawMemory = operator new(sizeof(stirng));

这里operator分配了一块足够容纳一个string对象的内存。

new operator (其他教程称为new expression):

这个操作符是由语言内建的,就像是sizeof那样,不能被改变意义,总是做相同的事情。比如如下代码:

string *ps = new string("Memory Management");

它实际上会做如下这些动作:

  1. 调用operator new分配一个sizeof(string)大小的内存
  2. 在这块内存上调用string相应的构造函数,然后将指向这块内存的指针强转成string*类型返回

它总是做这两件事,无论如何不能改变其行为。但是你能够改变用来容纳对象的那块内存的分配行为——通过重载operator new做到。

placement new:

placement即是一个特殊版本的operator new。它被用来在一个既有的内存块上构造对象。

有如下代码:

class Widget {
......
};
Widget *constructWidgetInBuffer(void *buffer, int widgetSize) {
	return new(buffer) Widget(widgetSize);
}

此函数返回一个指针,指向一个Widget object,它被构造于传递给此函数的一块内存缓冲区上。它往往被用于将对象放置在一些固定的内存地址上。

上面那个new调用的operator new是这样的:

void* operator new(size_t, void *location) {
	return location;
}

这就是placement new,它是C++ 标准库的一部分。要使用placement new,你必须用#include 。

总结:

  • 如果希望将对象产生于heap,请使用new operator;
  • 如果只打算分配内存,请调用operator new;
  • 如果打算在heap objects产生时自己决定内存的分配方式,请写一个自己的operator new;
  • 如果打算在已经分配的内存中构造对象,请使用placement new。

删除与内存释放:

为了避免资源泄露,每一个动态分配行为,都必须匹配一个相应但相反的释放动作。

数组:

有如下代码:

string* ps = new string[10];

这里,内存分配不在以operator new分配,而是尤其”数组版“兄弟,一个名为operator new[] 的函数负责分配。和operator new一样,operator new[]也可以被重载。

“数组版”与”单一对象版“的new operator的第二个不同是,它所调用的constructor数量,它会针对数组中的每个对象调用一个constructor。

三 异常

1 利用 destructors 避免泄露资源

把资源封装在对象内,通常便可以在exceptions出现时避免泄露资源。

2 在 constructors 内阻止资源泄露

C++只会析构已经构造完成的对象。对象只有在其constructor执行完毕才算是完全构造妥当。

new一个对象时,如果在构造函数中抛出异常,new出来的内存会被自动释放。

C++中的new运算符和构造函数之间存在一种异常安全机制,称为构造函数异常安全。根据这个机制,在构造函数抛出异常时,会自动调用已构造子对象的析构函数来销毁子对象,其顺序是构造的逆序,并释放由new分配的内存。

处理”“构造过程中可能发生的exceptions”,相当棘手,需要在发生异常后防止资源泄露。这里可以使用智能指针来消除大部分劳役。

3 禁止异常流出destructors之外

原因有二:

  1. 它可以避免terminate函数在exception传播过程的栈展开机制中被调用。
  2. 它可以协助确保destructors完成其应该完成的所有事情。

当对象被exception处理机制销毁时,如果此时抛出一个异常,因为此刻正有另一个exception处于作用状态,C++会调用terminate函数。

4 了解“抛出一个exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异

有如下一个函数:

istream operator>>(istream& s, Widget& w);
void passAndThrowWidget() {
	Widget localWidget;
	cin >> localWidget;
	throw localWidget;
}

被当作函数参数的localWidget,是被以引用方式传播的。

而被当作exception的localWidget, 不论被捕获的exception是以by value或 by reference 方式传递,都会发生 localWidget 的复制行为,交到catch字句手上的正是那个副本,因为一旦控制权离开passAndThrowWidget,localWidget便离开了作用域,于是它便被销毁了。因此catch只能使用它的副本。即使是static的localWidget,也是一样。

当对象被当作exception时,复制行为是由对象的copy constructor执行的。这个copy constructor相当于该对象的静态类型而非动态类型。任何时候,复制动作都是以对象的静态类型为本。

一个被抛出的对象(必为临时对象)可以简单地用by reference的方式捕捉,不需要以by reference-to-const的方式捕捉。但是函数调用过程中,将一个临时对象传递给一个non-const reference 参数是不允许的。

有如下三条条语句:

catch (Widget w) ...
catch (Widget& w) ...
catch (const Widget& w) ...

对于第一条语句,需要付出“被抛出物”的 “两个副本” 的构造代价。

对于第二三条语句,只需要付出 “被抛出物” 的 “一个副本” 的构造代价。

关于类型转换:

“exceptions 与 catch 子句相匹配” 的过程中,仅有两种转换可以发生。第一种是 “继承架构中的类转换”,第二种是从一个“有型指针”转为“无型指针”。

catch子句总是依出现顺序做匹配尝试,而非最佳匹配。而虚函数采用的是最佳吻合策略。因此绝不要将“针对base class而设计的catch子句” 放在 “针对derived class 而设计的 catch 子句”之前。

5 以by reference 方式捕捉 exceptions

6 了解异常处理的成本

1 “即使从未使用任何exception处理机制,也必须付出”的最低消费额:

必须付出一些空间,放置某些数据结构(记录着哪些对象已被完全构造妥当);必须付出一些时间,随时保持那些数据结构的正确性。

2 try语句块的成本

只要用上了一个,即决定捕捉exceptions,就得付出这样的成本。初略估计,如果使用try语句块,代码大约整体膨胀5%~10%,执行速度亦大约下降这个数。这是在假设没有任何 exceptions 被抛出的情况下。

3 抛出一个exception的成本

抛出一个exception的成本十分巨大。和正常的函数返回动作比较,由于抛出exception 而导致的函数返回,其速度可能比正常情况下慢3个数量级。但是只有在抛出exception时,你才需要承受这样的冲击,而exception的出现应该是罕见的。如果exception的出现比较频繁,往往是代码设计有问题。

因此为了尽可能降低异常处理的成本,请将try语句块的使用限制在非用不可的地方,并且在真正异常的情况下才抛出exceptions。

四 效率

1 谨记 80-20 法则

一个程序 80% 的资源用于 20% 的代码身上。

如果软件上有性能问题,正确的做法是:尽可能地将最多的数据提供给程序分析器,让其分析出软件的瓶颈所在的那20%的代码。

2 考虑使用lazy evaluation(缓式评估)

eager evaluation(急式评估)

可以做到的常见四点:

  1. 可避免非必要的对象复制;
  2. 可区别operator[]的读取和写动作;
  3. 可避免非必要的数据库读取动作;
  4. 可避免非必要的数值计算动作。

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

可以通过over-eager evaluation如 caching 和 prefetching 等做法分期摊还预期运行成本。

4 了解临时对象的来源

1 “为了让函数调用成功” 而产生的临时对象。

发生于“传递某对象给一个函数,而其类型与它即将绑定上去的参数类型不同”的时候。

有如下代码:

size_t countChar(const string& str, char ch);
char buffer[MAX_STRING_LEN];
char c;

cin >> c >> setw(MAX_STRING_LEN) >> buffer;

cout << "There are " << countChar(buffer, c)
     << " occurrences of the character " << c
     << " in " << buffer << endl;

countChar的调用动作,第一个自变量是char数组,但是相应函数参数类型是const string&。因此为了调用成功,编译器会以buffer作为自变量,生成一个类型为string的临时对象。于是 str 参数会被绑定于这个string临时对象上。当countChar返回,此临时对象会被自动销毁。

只有当对象以by value方式或reference-to-const 参数时,这些转换才会发生。如果对象被传递给一个reference-to-non-const 参数时,并不会发生这样的转换,因为能够修改临时变量没有意义。

2 当函数返回一个对象时

有如下一个函数:

const Number operator+(const Number& lhs, const Number& rhs);

此函数的返回值是一个临时变量,因为它没有名称:它就是函数的返回值。

这个临时变量很难被消除。

结论:

临时对象那个很可能很消耗成本,所以应该尽可能消除它们。至少要知道哪些地方可能产生临时对象。

5 协助完成 “返回值优化(RVo)”

如果函数一定得以 by-value 方式返回对象,那么就绝对无法消除临时变量。

可以通过 return value optimization完成优化。这个需要通过网络调查下。

6 利用重载技术避免隐式类型转换

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

复合操作比其对应的独身版本效率高,因为独身版本通常必须返回一个新对象(临时变量),而复合版本直接将结果写入其左端自变量。

如果同时提供某个操作符的复合形式和独身形式,便允许你的客户在效率与便利性之间做取舍。

面临命名对象或临时对象的抉择时,最好选择临时对象。

身为库开发者,应该两者都提供;身为应用软件开发者,如果性能时重要因素的话,应该以 “复合版本” 操作符取代其 “独身版本”。

8 考虑使用其他程序库

类型安全指的是什么???

9 了解virtual functions、multiple inheritance、 virtual base classes、runtime type identification 的成本(这章需要结合inside the C++ 分析)

virtual functions:

大部分编译器使用所谓的virtual tables 和 virtual table pointers——此二者常被简写为 vtbls 和 vptrs。

vtbl 通常是一个由 “函数指针” 架构而成的数组。某些编译器会以链表取代数组,但基本策略相同。程序中的每个class 凡声明(或继承)虚函数者,都有自己的一个vtbl,而其中的条目就是该class的各个虚函数实现体的指针。

如果derived class基础 base class,然后重新定义了某些继承而来的虚函数,并加上新的虚函数,其vtal内的条目将会指向对应于对象类型的各个适当函数,以及未被derived class重新定义的base class的虚函数。

凡声明有虚函数的class,其对象都含有一个隐藏的data member——vptr。这也意味着,每一个拥有虚函数的对象都需要付出 “一个额外指针” 的代价。

虚函数在被调用时的成本不大,和一个非虚函数的效率相当。使用虚函数等于放弃了inlining(如果虚函数通过对象被调用,倒是可以inline,但是大部分调用虚函数的动作是通过对象的指针或references完成的,这是无法被inlined的)。

在多重继承下,“找出对象内的vptrs”会比较困难,因为此时一个对象内会有多个vptrs(每个base class各对应一个),针对base classes而形成的特殊vtbls也会被生产出来,因此运行时期的调用成本也有轻微的成长。而且往往为了避免菱形继承,还会引入虚继承。virtual base classes会导致另一个成本,因为其实现做法常常利用指针,指向”virtual base class成分”,以消除复制行为,而你的对象内可能出现一个(或多个)这样的指针。

如果有这样的继承关系:

class A {...};
class B: virtual public A {...};
class C: virtual public A {...};
class D: public B, public C {...};

D对象内存布局看起来可能如下所示:

More effective C++ 35 提炼_第1张图片

运行时期类型辨识的成本(RTTI):

五 技术

1 将constructor 和 non-member functions虚化

所谓virtual constructors 是某种函数,视其获得的输入,可产生不同类型的对象。

可以简介将non-member functions 虚化。

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

非成员函数中的static对象:此对象在第一次被调用时才产生。
class中的对象:即使从未被用到,它也会被构造(及析构)

带有 private constructors 的class 不得被继承

限制能产生对象数量的class,不建议被继承。

一个用来计算对象个数的Base Class,private继承这个Base Class。

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

所谓static对象,不只涵盖明白声明为static对象,也包括global scope 和 namespace scope 内的对象。

为什么当对象涉及多重继承或虚拟继承的基类时,会拥有多个地址?

判断某个对象是否位于heap内

这往往是为了解决安全的delete this的问题。

可以定义一个存虚类,然后让工作类public继承它。

class HeapTracked {
public:
	class MissingAddress {};
	virtual ~HeapTracked() = 0;
	static void *operator new(size_t size);
	static void operator delete(void *ptr);
	bool isOnheap() const;
private:
	typedef const void* RawAddress;
	static list<RawAddress> addresses;
}

list<RawAddress> HeapTracked::addresses;
HeapTracked::~HeapTracked() {}
void* HeapTracked::operator new(size_t size) {
	void *memPtr = ::operator new(size);
	addresses.push_front(memPtr);
	return memPtr;
}
void HeapTracked::operator delete(void *ptr) {
	list<RawAddress>::iterator it = find(addresses.begin(), addresses.end(), ptr);
	if (it != addresses.end()) {
		address.erase(it);
		::operator delete(ptr);
	} else {
		throw MissingAddress();
	}
}
bool HeapTracked::isOnHeap() const {
	const void* rawAddress = dynamic_cast<const void*>(this);
	auto it = find(addresses.begin(), addresses.end(), rawAddress);
	return it != addresses.end();
}

这样的mixin class有一个缺点,就是不能用于内建类型上,因为它们不能继承。

禁止对象产生于heap之中

??似乎做不到

4 智能指针

借用unique_ptr后,别名过期,会销毁其别名指向的对象吗??

对基类函数调用*,访问的是父类还是子类??

167→问题??

利用member templates来转换smart pointer的技术点:

  1. 函数调用的自变量匹配规则
  2. 隐式类型转换函数
  3. template functions的暗自实例化
  4. member function templates等技术

unique_ptr 可以转化为 unique_ptr吗??原理??

5 引用计数

25的virtual copy constructor

RCObjects只能诞生于heap

6 Proxy classes(代理类)

编译器在const 和 non-const member functions 之间的选择,只以“调用该函数的对象是否是const“为基准。

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

六 杂项讨论

1 在未来时态下发展程序

现在式思维:

未来式思维:

2 将非尾端类设计为抽象类

将animal的assignment操作符声明为虚函数会导致”异性赋值“。

将函数声明为纯虚函数意味着:

  • 目前这个类是抽象类;
  • 任何继承此类的具体类,都必须将该存虚函数重新声明为一个正常的虚函数;

pure virtual destructors必须被实现出来。

继承体系中的non-leaf类应该是抽象类。

数组问题???

3 如何在同一个程序中结合C++ 和 C

需要先确定你的C++ 和 C编译器产生兼容的目标文件。

然后需要考虑4个方面的问题:

  1. Name Mangling(名称重整)
    由于重载,C++会对函数名重载,但是C语言或者其他语言不会。因此当调用的函数在C语言库的时候,需要使用”extern ”C““让那个函数名称不要被重整
  2. Statics的初始化
    static class对象、全局对象、namespace内的对象以及文件范围内的对象,其constructors总是在main之前就获得执行。这个过程称为static initialization。通过static initialization产生出来的对象,其destructors必须在所谓的static destruction过程中被调用。那个程序发生在main解锁之后。
    在许多编译器实现上,编译器会在mian一开始出安插一个函数调用,调用一个由编译器提供的特殊函数。正是这个特殊函数完成了static initialization。同样,编译器往往在main的最尾端安插一个函数调用,调用另一个特殊函数,其中完成static对象的析构。
    因此,除非程序(被C++编译的文件)中有main函数,否则这些对象既不会被构造也不会被析构。
    因此对于C main调用C++的支持库,而库中有static对象是极有可能的,最好是将C 的 main重新命名为realMain,然后让C++ main调用realMain即可。如果不能使用C++ main,则需要查看编译器厂商的文档。
  3. 动态内存分配
    new分配的内存,必须由delete释放;malloc及其变种释放的内存必须由free释放。
  4. 数据结构的兼容性
    C 和 C++之间对数据结构做双向交流,应该是安全的——前提是那些结构的定义式在C和C++中都可以编译。为C++ struct加上非虚函数,往往不会影响兼容性。

你可能感兴趣的:(c++)