1. 让自己习惯C++

1.1 条款01:视C++为语言联邦

C++可以视为由相关次语言组成的联邦,每个次语言都有自己的规约。

大致分为四个:

C:C++仍然是以C为基础。区块,语句,预处理,内置数据类型,数组,指针都是来自C
Object-Oriented C++:面向对象设计。封装,继承,多态,虚函数,析构构造。
Template C++:泛型编程。templates威力强大,可以带来崭新的编程范型。
STL:标准模板程序库。他对容器,迭代器,算法以及函数对象的规约有极佳的紧密配合与协调。

1.2 条款02:尽量以const, enum, inline替换 #define

#define用于常量的场景
缺点:
a. #define定义的常量导致编译出错时,出错提示不友好;
b. #define无视作用域(scope),无封装性。
c. #define会产生大量目标码。
解决:
a. 大多数情况可以用const替换;
b. 旧编译器对类内static const语法较苛刻,此时可用enum替换#define。
#define用于宏(函数)的场景
缺点:
a. 需要注意括号的使用,给自己添麻烦;
b. #define无视作用域(scope),无封装性。
解决:使用inline函数。

1.3 条款03:尽可能使用const

原因:让编译器辅助对常量/常量函数的错误使用。编译器强制对某值保持不变的约束。
const常量
const char* p = …; // p可改变,p不可改变
char const * p = …; // 同上,有些人会这么写
char
const p = …; // p不可改变,p可改变
const char
const p = …; // p不可改变,*p不可改变
const std::vector::iterator iter = …; // iter不可改变,*iter可改变std::vector::const_iterator iter = …; // iter可改变,iter不可改变
const Rational operator
(const Rational& lhs, const Rational& rhs); // 用于避免出现 (a * b) = c 这样的代码
const函数
bool func(int a, int b) const; // const表示该函数不改变类中的成员变量,mutable修饰的成员变量除外

1.4 条款04:对象使用前先初始化

读取未初始化的值会导致不明确的行为。在某些平台上,仅仅只是读取未初始化的值,就可能让你程序终止。所以最佳的处理办法就是:永远在使用对象之前先将它初始化
C++规定对象的成员变量的初始化动作发生在进入构造函数本体之前。所以,尽可能使用成员初值列(member initialization list)替换赋值操作。而且这样效率较高。
用户创建的类在构造函数用初始化列表初始化(注意:初始化顺序为成员变量的声明顺序有关,与初始化列表如何排列无关);
不同编译单元的non-local static对象的初始化顺序不确定,尽量使用local static对象代替。

2. 构造/析构/赋值

2.1 条款05:C++会自动编写default构造、拷贝构造、析构、赋值函数

#include <iostream>
using namespace std;
//你以为你声明了一个空类
class  A
{};int main()
{//但是编译器会帮你声明一个默认的inline public的函数A a;     //构造函数A a1(a);//copy 构造函数a1 = a;//copy assignment函数//析构函数return 0;
}

2.2 条款06:声明为private防止自动生成的函数被使用

//把拷贝构造函数和赋值函数声明为private类型,防止被使用
class  UnCopyable
{protected:UnCopyable() {}~UnCopyable() {} //允许derived对象构造和析构
private:UnCopyable(const UnCopyable& uc);UnCopyable& operator=(const UnCopyable& uc);
};

2.3 条款07:使用多态特性时,基类的析构函数必须为virtual

  //一个多态的场景
class TimeKeeper
{   //计时器(基类)
public:     TimeKeeper();    virtual ~TimeKeeper();      ...
};
class AtomicClock: public TimeKeeper {...}    //原子钟
class WristWatch: public TimeKeeper {...}    //腕表  //往往这么使用多态特性
TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;//调用了不仅调用基类的析构函数,也调用了子类的析构函数

上面是使用多态的一个场景,当delete ptk时,因为ptk是TimeKeeper类型,如果基类析构函数不是virtual,那么ptk的析构时只调用基类析构函数,析构不正确;使用virtual ~TimeKeeper();保证ptk析构时调用正确的子类析构函数

1.如果一个类带有virtual函数,说明这个类有可能用于多态,那么就应该有virtual析构函数。不要对non-virtual析构函数的类使用多态特性,包括string、vector在内的STL容器。
2.带有virtual函数的类会生成vtbl (virtual table)用于在运行期间确定具体调用哪个函数,由vptr (virtual table pointer)指向,占用空间。胡乱声明virtual会增加体积。

2.4 条款08:别让异常逃离析构函数

析构函数抛出异常,程序终止析构,会导致资源没有完全释放。

#include <iostream>
using namespace std;class  A
{public:void close() {cout <<"close" << endl;}private:};class Con
{public:Con() :closed(false) {}void Close()//设下双重保险{a.close();closed = true;}~Con(){cout << "~Con " << closed << endl;if (!closed){//在析构函数内捕捉异常,不要让析构函数的异常抛出try{a.close();}catch (exception ex){cout << ex.what() << endl;}}}
private:A a;bool closed;//设下双重保险
};
int main()
{Con c;c.Close();//用户主动调用CloseCon c1;//用户不调用Closereturn 0;
}
/*
运行结果:
close
~Con 0
close
~Con 1
*/

2.5 条款09:不在构造和析构函数中使用virtual函数

构造函数在构造时,派生类的成员变量未初始化完毕,virtual函数指向基类;
析构函数在析构时,派生类的成员变量部分析构,virtual函数指向基类。

2.6 条款10:令 operator= 返回值为 reference to *this

这么做的目的是为了实现连续赋值

int x, y, z;
x = y = z = 1;   //连续赋值int& operator=(const int& rhs) {...return *this;   //返回左侧对象的引用
}

2.7 条款11:在 operator= 中处理自我赋值

//下面代码,如果出现自我赋值,则出错
//这里自我赋值的问题就是有可能this和 rhs可能是同一个对象。
//这样的话,delete b同时也销毁了rhs的b。A& operator=(const A& rhs){delete b;b = new B(*rhs.b);return *this;}//方法1
A& operator=(const A& rhs)
{if (&rhs == this)return *this;delete b;b = new B(*rhs.b);return *this;
}
//方法2
A& operator=(const A& rhs)
{B ori = *(rhs.b);B temp = ori;ori = *b;*b = temp;return *this;
}
//方法3
A& operator=(A rhs)
{B temp = *rhs.b;rhs.b = b;*b = temp;return *this;
}

2.8 条款12:复制对象时勿忘其每一个成分

编译器不检查拷贝构造和赋值函数是否对所有成员变量进行了拷贝;
派生类的拷贝构造和赋值函数记得要先调用基类的拷贝构造和赋值函数。

3. 资源管理

3.1 条款13:使用"资源管理类"管理资源

我们创建了对象之后,很可能会忘记delete它,或者因为某些原因无法delete它,最后导致内存泄漏。为了确保资源总是会被释放的,我们需要将对象资源放进对象内,当控制流离开,对象的析构函数会自动释放掉这些资源。
auto_ptr在拷贝或者赋值时,新指针获得资源拥有权,旧指针变为null。
shared_ptr使用计数的方式来确定有哪些指针在使用该资源,计数为0释放资源。它的拷贝和赋值则没有auto_ptr的那个问题。
以对象管理资源的关键思想就是:
1.获得资源后立刻放进管理对象内:初始化的时候就取得资源。(Resource Acquisition Is Initialiaztion RAII)
2.管理对象运用析构函数确保资源被释放:不论控制流如何离开区块,一旦对象被销毁其析构函数自然会调用,资源就能释放。

3.2 条款14:在资源管理类中小心拷贝行为

接3.1,在某些不适用"智能指针"的场景,需要自定义"资源管理类"来管理资源,比如一个类内部存了一个mutex*资源,在初始化的给mutex上锁,析构的时候解锁。
但是如果在他析构之前发生了copying行为会怎么样?
有如下解决方案:
1.禁止复制。
2.对底层资源进行引用计数。
3.复制底层资源。
4.转移底部资源拥有权。

3.3 条款15:在"资源管理类"中提供对原始资源的访问

智能指针有.get()函数可以显式转换成原始指针;它们也重载了operator->和operator*,可以隐式转换。
类似的,自定义的"资源管理类"也需要考虑显示转换和隐式转换,提供对原始资源的访问。

3.4 条款16:成对使用new和delete时要采取相同形式

new要搭配delete
new [] 要搭配 delete []
如果不一一对应使用会发生未定义事件。

3.5 条款17:以独立语句将newed对象置于智能指针

Process(shared_ptr<A>(new A()), priority());
//假设有上面这个函数,它有两个参数,调用该函数时,会做下面三件事:
//a. 调用new Widget
//b. 执行shared_ptr的构造
//c. 执行priority()
//
//执行顺序是未定义的,可能为a->b->c,也可能a->c->b等等。注意,当顺序为a->c->b,且priority()抛出异常时,资源泄漏。//解决方法:把new A和调用shared_ptr的构造函数放一起,调用pirority则单独出来。
shared_ptr<A> a = shared_ptr<A>(new A());
Process(a, priority());

4. 设计与声明

4.1 条款18:让接口不易被误用

简单来说,就是考虑用户可能的误用行为,在代码中规避。比如保持接口一致性stl都用size;比如避免用户使用一般指针管理资源带来的资源泄漏风险,而用智能指针;比如将 operator* 的返回类型设为const,避免用户写出"a*b=c"这样的代码。

4.2 条款19:像设计type一样设计class

设计class要考虑很多细节,尽量使设计出来的class像C++内置类型一样有极大的可用性和鲁棒性。

4.3 条款20:宁以 pass-by-reference-to-const 替换 pass-by-value

原因:a. 参数使用引用传递,不需要构造新对象,比较快;b. 避免对象切割问题。
对象切割(slicing):当一个继承对象by value方式传递并视为一个base class对象,那么dervied对象的特化性质全被切割掉了,只留下base对象。因为正是base class对象构造了它。

"引用"在编译器底层的实现就是指针(指针的本质是int类型的变量),所以在以下场景,"引用"并不一定比pass-by-value快:
a. C++内置类型;
b. STL的迭代器(底层实现是指针);
c. 函数对象(底层实现是指针)。

4.4 条款21:不要让函数返回值是指向局部变量的引用或指针

原因应该很容易理解:局部变量在函数调用结束后就销毁了,那么这个函数返回的引用和指针指向的内存已经无效了。

4.5 条款22:将成员变量声明为 private

类似C#的属性一样,C++可以通过某些手段达到和C#属性一样的效果,只读或者只写,或者可读可写。
将成员变量设置为private为的是封装性,而且这样外界只能靠成员函数影响它们。
protected其实很打咩,因为当我们取消了某个protected成员变量,所有的derived classes都会被破坏,这和public其实如出一辙,所以不要觉得protected的封装性高过public。
从封装的角度看,访问权限只有private和其他。

4.6 条款23:宁以 non-menber、non-friend 替换 member 函数

使用非成员函数调用成员来增加封装性,包裹弹性和机能扩充性。

#include <iostream>
using namespace std;
namespace Aclass
{class A{public:void clear1() {}void clear2() {}void remove() {}void clearAll() { clear1(); clear2(); remove(); }};void ClearA(A& a) { a.clear1(); a.clear2(); a.clearAll(); }
}
int main()
{Aclass::A a;a.clearAll();//这个不好Aclass::ClearA(a);//这个更好return 0;
}

4.7 条款24:若所有参数都需要类型转换,请把这个函数写成 non-member 函数

//第一种情况:乘法函数为member函数#include <iostream>
using namespace std;class A
{public:A(int x) :value(x) {}int value;const A operator*(const A& rhs)const{return A(value * rhs.value);}
};int main()
{A a(10);A b = a * 10;//可以A c = 10 * a;//不行return 0;
}//第二种情况:乘法函数为non-member函数
#include <iostream>
using namespace std;class A
{public:A(int x) :value(x) {}int value;
};
const A operator*(const A& lhs, const A& rhs)
{return A(lhs.value * rhs.value);
}
int main()
{A a(10);A b = a * 10;//可以A c = 10 * a;//可以return 0;
}

4.8 条款25:考虑写出一个不抛异常的swap函数

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

5. 实现

5.1 条款26:尽可能延后变量定义式出现的时间

string CreateStr(const string& temp )
{static int minLength = 10;string res(temp);
//定义过早,异常之后就浪费资源了if (temp.size() < minLength){throw logic_error("too short");}cout << "处理res" << endl;res = temp + "123456";return res;
}
string CreateStr(const string& temp )
{static int minLength = 10;if (temp.size() < minLength){throw logic_error("too short");}cout << "处理res" << endl;string res(temp);
//用的时候再定义res = temp + "123456";return res;
}
//循环内的变量怎么定义//方1A a;for (int i = 0; i < 10; i++){a = i;}//方2for (int i = 0; i < 10; i++){A a1(i);}

方1:1个构造函数+1个析构函数+n个赋值操作
方2:n个构造函数+n个析构函数
所以具体哪个好需要看构造析构的成本和赋值的成本谁大

5.2 条款27:尽量少做转型动作

1.const_cast通常被用来将对象的常量性转出。它也是唯一有此能力的C+±style转型操作符;
2.dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也就是唯一可能耗费重大运行成本的转型动作;
3.reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。
4.static_cast用来强迫隐式转换,例如将non-const对象转为const对象(条款3),或将int转为double等。它也可以用来执行上述多种转换的反向转换,例如将void*指针转为typed指针,将pointer-to-base转为pointer-to-derived,但无法将const转为non-const(仅仅const_cast可做到);

5.3 条款28:避免返回handles指向对象内部成分

References,Pointer,Iterators都是所谓的handles(号码牌,用来取得某个对象),而返回一个代表对象内部(private)数据的handle,随之而来的是降低对象封装性的风险。
解决方法是将返回的引用或指针设为const类型,禁止修改。
有些特殊情况下,返回的引用或指针指向的成员变量被销毁,导致引用和指针无效(dangling handles),这种情况要在代码中避免。

5.4 条款29:为”异常安全“而努力是值得的

异常安全的函数,即使在发生异常的时候也能保证不泄漏资源不破坏数据结构,可以分为三种:
1.基本承诺:如果异常抛出,程序内任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。
2.强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就完全成功,如果函数失败,程序会回到“调用函数之前”的状态。
3.不抛异常(nothrow)保证:承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如ints,指针等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

当你写代码时,请仔细想想是否具备异常安全性,在实际开发中,我们应该避免内存泄漏和数据被破坏的情况,但是要做到上述所说的不抛异常型函数很难,所以我们至少需要了解这些问题,才能写出优质代码。

5.5 条款30:透彻了解inlineing的里里外外

1.inline函数背后整体观念是,将"对此函数的每一个调用"都以函数本体替换之
2.Inlining在大多数C++程序中是编译行为
3.大部分编译器拒绝将太过复杂(例如循环或递归)的函数inlining,而所有对virtual函数的调用也会使inlining落空,因为virtual意味着"等待,知道运行期才确定调用那个函数",而inling意味着"执行前,先将调用动作替换为被调用函数的本体";
4.编译器通常不对"通过函数指针而进行的调用"实施inlining,这意味着对inline函数的调用有可能被inlined,也有可能不被inlined
5.构造函数和析构函数往往是inlining的糟糕候选人。C++对于“对象被创建和销毁时发生什么事”做了各式各样的保证。C++描述了什么一定会发生,但没有说如何发生。”事情如何发生”是编译器实现者的权责,不过至少我们知道他不会凭空发生。所以编译器可能会帮你偷偷的制造一些代码,但是如果你用inline的话,会影响编译器。
6.大部分调试器对inline函数都束手无策,因为你无法在一个并不存在的函数内打断点调试。

5.6 条款31:将文件间的编译依存关系降至最低

当改动一个文件后,不需要整个工程都重新编译。
解决方案是把类的定义和声明放在两个文件中

6. 继承与面向对象

6.1 条款32:确定你的public继承是is-a关系

如果你令class D(“Derived”)以public形式继承class B(“Base”),你便是告诉C++编译器(以及你的代码读者)说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。你的意识是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。你主张“B对象可派上用场的任何地方,D对象一样可以派上用场”,因为每一个D对象都是一种B对象。反之如果你需要一个D对象,B对象无法效劳,虽然每个D对象都是一个B对象,反之并不成立。

每个学生都是人,但并非每个人都是学生,大概就是这种关系吧。

但这种关系就容易设计出奇怪的关系,比如鸟会飞,企鹅是鸟,但企鹅不会飞。如果你的鸟类有fly()的话,那你的企鹅类就会产生问题了!当然我们可以让其在调用企鹅的fly的时候抛出异常,这样就能使程序在运行期发生错误,但更好的设计是在编译器发生错误,即鸟不声明fly函数!

这就是public继承,当我们以public模塑base class和dervied class关系时,能够施行与base class对象身上的每件事,也可以在dervied class施行。编译器能通过编译,但是这并不一定代表程序行为一定正确(逻辑错误)

6.2 条款33:避免遮蔽继承而来的名称

//C++的内层作用域的名称会遮掩外围作用域的名称。编译器会从local作用域开始查找,如果找到就不再找其他作用域(遮蔽),如果找不到就会找其外围作用域,直到往golbal作用域找去。#include <iostream>
using namespace std;class Base
{public:virtual void mf1() = 0;virtual void mf1(int) { cout << "Base mf1(int)" << endl; }virtual void mf2() { cout << "Base mf2()" << endl; }void mf3() { cout << "Base mf3()" << endl; }void mf3(double x) { { cout << "Base mf3("<<x<<")" << endl; } }
};
class Derived:public Base
{public:virtual void mf1() { cout << "Derived mf1()" << endl; }void mf3() { cout << "Derived mf3()" << endl; }void mf4() { cout << "Derived mf4()" << endl; }
};int main()
{Derived d;int x = 10;d.mf1();//Derived mf1()//d.mf1(x);//错误,因为dervied::mf1遮掩了Base:mf1d.mf2();//Base mf2()d.mf3();//Derived mf3()//d.mf3(x);//错误,因为dervied::mf3遮掩了Base:mf3d.mf4();//Derived mf4()return 0;
}
//解决方法1
//使用using
class Base
{public:virtual void mf1() = 0;virtual void mf1(int) { cout << "Base mf1(int)" << endl; }virtual void mf2() { cout << "Base mf2()" << endl; }void mf3() { cout << "Base mf3()" << endl; }void mf3(double x) { { cout << "Base mf3("<<x<<")" << endl; } }
};
class Derived:public Base
{public:using Base::mf1;//让base class内名为mf1和mf3的所有东西在derived的作用域内可见。using Base::mf3;virtual void mf1() { cout << "Derived mf1()" << endl; }void mf3() { cout << "Derived mf3()" << endl; }void mf4() { cout << "Derived mf4()" << endl; }
};
//解决方法2
//使用
private继承。class Base
{public:virtual void mf1() = 0;virtual void mf1(int) { cout << "Base mf1(int)" << endl; }virtual void mf2() { cout << "Base mf2()" << endl; }void mf3() { cout << "Base mf3()" << endl; }void mf3(double x) { { cout << "Base mf3("<<x<<")" << endl; } }
};
class Derived:private Base
{public:virtual void mf1() //转交函数{ Base::mf1(); //暗自成为inline}
};

6.3 条款34:区分接口继承和实现继承

public继承有两个部分组成:函数接口继承和函数实现继承。
1.成员函数的接口总是被继承。
纯虚函数必须被任何”继承了它们”的具象类重新声明,而且它们在抽象的class中通常没有定义。

2.声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。
并且具有纯虚函数的类称为抽象类,他不能被new出来。

3.声明简朴的impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
但是有可能会出现继承类忘记重写impure virtual函数了,导致发生惨案!
所以有一个好的设计是使得基类声明为pure virtual,并且给一份默认缺省实现。

4.声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

6.4 条款35:考虑virtual函数以外的其他选择

//1.使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式,它以public non-virtual成员函数包裹较低访问性的virtual函数class Base
{protected:virtual void DoFunc() { cout << "Base::DoFunc()" << endl; }
public:virtual void Func() { DoFunc(); }
};class A :public Base
{protected:virtual void DoFunc() { cout << "A::DoFunc()" << endl; }
};
class  B:public Base
{public:virtual void DoFunc() { cout << "B::DoFunc()" << endl; }
};
//2.将virtual函数替换为“函数指针成员变量”,这是策略模式的一种分解表现形式class Base;
void defaultFuc(const Base&);
class Base
{public:typedef void (*DoFunc)(const Base&);Base(DoFunc func = defaultFuc):dofunc(func) {}void Func() const { dofunc(*this); }
private:DoFunc dofunc;
};class A :public Base
{public:A(DoFunc func = defaultFuc) :Base(func) {}
};
class  B:public Base
{public:B(DoFunc func = defaultFuc) :Base(func) {}
};
void funcA(const Base&) { cout << "FuncA" << endl; }
void funcB(const Base&) { cout << "FuncB" << endl; }
//3.以trl::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是策略设计模式的某种形式。
class Base;
void defaultFuc(const Base&);class Base
{public:typedef std::tr1::function<void(const Base)> DoFunc;Base(DoFunc func = defaultFuc):dofunc(func) {}void Func() const { dofunc(*this); }
private:DoFunc dofunc;
};class A :public Base
{public:A(DoFunc func = defaultFuc) :Base(func) {}
};
class  B:public Base
{public:B(DoFunc func = defaultFuc) :Base(func) {}
};
void funcA(const Base&) { cout << "FuncA" << endl; }
void funcB(const Base&) { cout << "FuncB" << endl; }
//4.将继承体系内virtual函数替换为另一个继承体系内的virtual函数。这是策略模式传统实现手法.class Base;class DoFunc
{public:virtual void dofuc(const Base&) { cout << "DoFuc::dofuc()" << endl; }
};
class FuncA :public DoFunc
{public:virtual void dofuc(const Base&) { cout << "FuncA::dofuc()" << endl; }
};
class  FuncB :public DoFunc
{public:virtual void dofuc(const Base&) { cout << "FuncB::dofuc()" << endl; }
};
DoFunc defaultFuc;
class Base
{public:Base(DoFunc* func = &defaultFuc):dofunc(func) {}void Func() const { dofunc->dofuc(*this); }
private:DoFunc* dofunc;
};

6.5 条款36:绝不重新定义继承而来的non-virtual函数

如果继承类重新定义了继承而来的non-virtual就 发生违反is-a的关系

6.6 条款37:绝不重新定义继承而来的缺省参数值

在C++中缺省参数值是静态绑定的。因为如果缺省值是动态绑定的话,编译器就必须有某种方法在运行期为virtual函数决定实当的参数缺省值。这比目前实行的”在编译器决定”的机制更慢而且更复杂。为了程序的执行速度和编译器实现上的简易度,C++做了这样的取舍。

6.7 条款38:确保"复合"是has-a或is-implemented-in-terms-of关系

复合是类型之间的一种关系,比如人有电话号码和家庭住址。
这种关系意味着has-a(有一个),通常用于处理两个不同领域。
另外,像stl中的Set内部也是用到了list,Set的函数成员大量依赖list及标准程序库其他部分提供的机能来完成,即根据list实现出set

6.8 条款39:明智而审慎地使用private继承

如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。由private base class继承而来的所有成员,在derived class中都会变成private属性,纵使它们在base class中原本是protected或public属性。

private继承意味implemented-in-terms-of(根据某物实现出)。如果D以private形式继承B,意思是D对象根据B对象实现 ,这在软件”设计”层面毫无意义,其意义只及于软件实现层面。
但我们大多情况下可以用复合取而代之。

6.9 条款40:明智而审慎地使用多重继承

多重继承会发生"成员函数或成员变量重名"、"钻石型继承"等问题,对于这种情况建议使用virtual public继承,但这会带来空间和效率的损失。
多重继承的复杂性,导致一般不会使用。只有virtual base classes(也就是继承多个接口)才最有实用价值,它不一定需要virtual public继承。

我们应该public继承自接口,private继承自实现

7. 模板与泛型

7.1 条款41:了解隐式接口和编译期多态

1.以不同的参数具现化会导致调用不同的函数,这便是所谓的编译期多态。
2.泛型编程必须支持对应的隐式转换,如果函数中使用到诸如,size(),Init(),swap(),!=

7.2 条款42:了解typename的双重意义

//第一重意义
template<class T>
template<typename T>
//上面两句话效果完全一样
//第二重意义
//涉及嵌套从属名称时只能使用关键字typenameclass A
{public:typedef int const_iterator;
private:int x;
};
template<typename T>
void Func(T& a)
{//因为我们不知道T::const_iterator是不是一个类型。我们无法确定!typename T::const_iterator* x;
}

7.3 条款43:学习处理模板化基类内的名称

//Send(info)无法通过编译class CompanyA
{public:void Send(string msg){cout << "A Send : " << msg << endl;}
};
class CompanyB
{ void Send(string msg){cout << "B Send : " << msg << endl;}
};
class MsgInfo
{public:string msg;
};
template<typename Company>
class MsgSender
{void Send(const MsgInfo& info){string msg;msg = info.msg;Company c;c.Send(msg);}
};
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{public:void SendLog(const MsgInfo& info){//logSend(info);//编译器不知道Company是什么,所以无法知道MsgSender<Company>是什么}
};
解决方案有三种:
1.加上this->
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{public:void SendLog(const MsgInfo& info){//logthis->Send(info);}
};2.使用using声明式
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{public:using MsgSender<Company>::Send;void SendLog(const MsgInfo& info){//logSend(info);}
};3.明白指出被调用的函数位于base class内:
template<typename Company>
class LoggingMsgSender :public MsgSender<Company>
{public:void SendLog(const MsgInfo& info){//logMsgSender<Company>::Send(info);}
};

7.4 条款44:将与参数无关的代码抽离templates

使用template可能导致代码膨胀,二进制码会带着重复(或者几乎重复)的代码、数据,或两者。其结果有可能源码看起来合身而整齐,但目标码却不是这么回事。所以该条例用于我们解决template带来的代码膨胀问题。

7.5 条款45:运用成员函数模板接受所有兼容类型

真实的智能指针可以支持隐式转换。但我们自己简单写的指针不能这样

#include <iostream>
using namespace std;
template <class T>
class SmartPtr
{public:SmartPtr(T* p) : p(p) {}
private:T* p;
};class A
{};
class B :public A
{};
int main()
{SmartPtr<A> p1=SmartPtr<B>(new B);//不行shared_ptr<A> p2 = shared_ptr<B>(new B);//可以
}

为什么?如果以带有base-derived关系的A和B类分别具现化某个template,产生出来的两个具现化并不带有base-derived的关系,所以编译器认为这是两个完全无关的类。

解决方案是生成一个member template copy构造函数

class SmartPtr
{public:template<typename U>SmartPtr(const SmartPtr<U> & other):p(static_cast<T*>(other.get()))  {}SmartPtr(T* p) : p(p) {}T* get() const{return p;}
private:T* p;
};

7.6 条款46:需要类型转换时,请为模板定义非成员函数

A<int> t2 = t1 * 10;//报错

因为第一个参数t1是A类型的,但第二个参数是int类型,那么编译器如何推算出T?
只需要:template class内的friend声明式可以指涉某个特定函数。那意味class A可以声明operator*为friend函数。所以编译器总是能够在class A具现化时得知T。

7.7 条款47:使用traits classes表现类型信息

STL中广泛使用traits classes来标记容器属于哪一类容器(比如"可随机访问容器":vector、deque等)

7.8 条款48:认识template元编程(TMP)

模板元编程它有两个伟大效力:
第一,它让某些事情更容易。
第二,由于TMP执行于C++编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期转移到编译期的另一个结果是,编译时间变长了。

8. 定制new和delete

8.1 条款49:了解new-handler的行为

new-handler相当于new的异常处理函数。new申请内存发生异常,调用new-handler处理,处理完了之后继续尝试new,如果还是出错,继续调用new-handler,以此反复。

为了防止当operator无法满足内存申请时反复调用new-handler函数,我们需要做以下事情::
a. 让更多内存可被使用。使得再次尝试new能成功。
b. 安装另一个new-handler。使用更强力的new-handler处理。
c. 卸载new-handler。实在不行,卸载new-handler使new抛出异常。
d. 抛出bad_alloc的异常。
e. 退出程序。abort()或exit()。

8.2 条款50:了解new和delete的合理替换时机

用来检测运用上的错误。方便log日志之类的,查看new分配的地址。
为了强化效能。可以自己定制new/delete以加快性能和内存使用。
为了收集使用上的统计数据。收集某些信息,比如分配区块大小分布,寿命分布,分配次序等。

8.3 条款51:编写new和delete时需要固守常规

1.内存不足的时候必须调用new-handing函数
2.C++规定即使用户要求0bytes,operator new也得返回一个合法的指针。一般是把0byte视为1byte
3.将大小有误的释放行为,转交给标准的delete。
4.operator new[]唯一需要做的一件事就是分配一块未加工内存.
5.operator delete应该在收到null指针时,不做任何事.class专属版本则还应该处理"比正确大小更大的错误申请"

8.4 条款52:写了placement new也要写placement delete

1.如果operator new接受的参数除了一定会有的那个size_t之外,还有其他参数,这便是个所谓的placement new.
2.相应的placement delete指"参数个数和类型都与operator new相同的"operator delete
3.运行期,若replacement new抛出异常,则系统会去找与placement new相对应的placement delete版本,若找不到则什么也不做.
但是placement delete只有在"伴随placement new调用而触发的构造函数"出现异常时才会被编译器自动调用.对一个指针显式调用delete不会导致调用placement delete.
4.缺省情况下,C++在global作用域提供三种形式的operatornew

  • void* operator new(std::size_t)throw(std::bad_alloc); //normal new
  • void* operator new(std::size_t,void*) throw(); //placement new
  • void* operator new(std::size_t,const std::nothrow_t&)throw()//nothrow new
    5.当声明了专有类的new和delete之后,请注意,它会遮掩std的标准new和delete.
    6.为了避免class的专属news掩盖其他的news。我们可以使用继承。在base class中包含所有正常形式的new和delete。凡是想要以自定义扩充标准形式的客户可以利用继承机制和using声明式来取得标准形式。

9. 杂项

9.1 条款53:不要轻易忽视编译器的警告

也不要依赖编译器给你指出错误,因为不同的编译器对错误的敏感度是不同的。

9.2 条款54:让自己首席包括TR1在内的标准程序库

C++的一些扩展特性会在TR1,虽然这些特性随着C++标准版本的更新逐渐合并到标准中.

9.3 条款55:让自己熟悉Boost

Boost是一个C++开发者社群,由C++委员会创建,它是C++新特性的试验场,TR1的许多扩展特性是从Boost提交的。

《Effective C++》全局概述篇相关推荐

  1. 部署到gcp_GCP 网络系统Andromeda --- 概述篇

    这个系列总共有三篇,分别在: 肖宏辉:GCP 网络系统Andromeda --- 概述篇 肖宏辉:GCP 网络系统Andromeda --- 控制面 肖宏辉:GCP 网络系统Andromeda --- ...

  2. 零基础快速开发全栈后台管理系统(Vue3+ElementPlus+Koa2)—项目概述篇(一)

    零基础快速开发全栈后台管理系统(Vue3+ElementPlus+Koa2)-项目概述篇(一) 一.项目开发总体框架 二.项目开发流程 三.项目技术选型

  3. App开发智能车载应用之概述篇

    App开发智能车载应用之概述篇 Apple CarPlay Vs Android Auto 苹果和谷歌都先后公布了自己的智能车载解决方案Apple CarPlay和Android Auto. 两家采用 ...

  4. 【区块链 | Compound】1.剖析DeFi借贷产品之Compound:概述篇

    前言 我前段时间一直在研究 Compound,走过一点弯路,也趟过一些坑,最终把它啃了下来.最近有些小伙伴也在咨询我相关的一些问题,那我本着乐善好施的优良传统,决定将我所学的知识整理成文字分享出来.我 ...

  5. 北京大学肖臻老师《区块链技术与应用》公开课笔记15——ETH概述篇

    北京大学肖臻老师<区块链技术与应用>公开课笔记 以太坊概述篇,对应肖老师视频:click here 全系列笔记请见:click here About Me:点击进入我的Personal P ...

  6. iVX低代码平台系列详解 -- 概述篇(二)

    写在前面 ivx动手尝试电梯:ivx在线编辑器 iVX系列教程持续更新中 上篇文章可看:iVX低代码平台系列详解 – 概述篇(一) ivx目录 写在前面 一.iVX优势 1.快速学习 2.快速开发 3 ...

  7. Linux内核学习(七):linux kernel内核启动(一):概述篇

    Linux内核学习(七):linux kernel内核启动(一):概述篇 这一篇让我们来大致的了解一下Linux内核的启动过程 这篇文章不涉及源码,重在让你知道这个linux内核的启动过程,源码详细的 ...

  8. B2C电子商务系统研发——概述篇

    # 前言 计划接下来一段日子写若干篇关于整个B2C电子商务系统研发流程博文, 包括前台.后台各个主要功能模块的需求分析.概要设计.详细设计和一些 伪编码编写.计划2~3天写一篇,对于一些复杂的模块会通 ...

  9. 规则引擎Ilog Jrules开发基础教程【连载1】-- 概述篇

    概述篇 规则引擎是一种嵌套在应用程序中的组件,它实现了将业务规则从应用程序代码中分离出来.规则引擎使用特定的语法编写业务规则,规则引擎可以接受数据输入.解释业务规则.并根据业务规则做出相应的决策. 通 ...

最新文章

  1. mysql timdir_MySQL备份之mysqlhotcopy与注意事项
  2. Codeforces Beta Round #7 C. Line (扩展欧几里德)
  3. C# MVC的博客开发(三)注册
  4. SpringMVC默认访问路径配置
  5. spark内存溢出怎么解决_和平精英:更新需要预留6G内存,玩家抱怨手机扛不住,怎么解决?...
  6. RHEL6.1 java显示乱码
  7. Web Components 小榄
  8. 《Effective Python 2nd》 读书笔记
  9. 黑科技神器-uTools
  10. 关于DHCP的中继问题
  11. 傅里叶分析的方方面面:复正弦、负频率
  12. 日期时间在ios上显示错误
  13. 宠爱吖用计算机怎么弹,宠爱吖简谱-歌谱-歌词
  14. OLAP引擎 :CH Doris impala+kudu优缺点分析
  15. 基于MATLAB金属表面缺陷分类与测量的GUI设计
  16. JVM性能调优2_垃圾收集器与内存分配策略__享学课堂
  17. ISO14443标准详细介绍
  18. Unity做一个魔方
  19. 90% 的 CDP 成了摆设?3 家零售企业说可以这么玩
  20. 大数据培训技术Elasticsearch集群健康

热门文章

  1. 终于我看到了一支活动活动铅笔和一外哆啦a梦的玩具
  2. ImageMagick:png序列转gif(适合处理带透明度的图片)
  3. python 报错 can‘t start new thread
  4. 创见内存卡修复工具带数据恢复RecoveRx Tool v2.0
  5. 最新h5微信大灌篮小游戏投篮赚钱源码+手动提现+免公众号
  6. PVE虚拟化平台之创建虚拟机流程
  7. 转帖 饱含人生哲理的真情嘱咐
  8. 为什么应该通过效果规划师规划提升广告效果?
  9. 用计算机算账老是出负数是怎么回事,存货为什么出现负数?会计人员怎么处理?...
  10. 声学中自由场、扩散场、压力场的定义,及自由场/扩散场/压力场麦克风的定义