前边6章的内容对刚开始用C++的人来说还是挺有意思的,但是第7-8章主要讲模板和元编程的话,通常对普通开发者意义没有那么大,因为平时也确实用不太上模板元编程:)。
同时这本书因为比较久远,在c++11出来之前就已经火了很多年了,里边有相当一部分内容是比较具有时代性的,但是在C++的软件开发思想上还是有一定理解的,值得一阅。
视C++为一个语言体系,主要是由C语言、Object-Oriented C++、Template C++、STL四个主要部分组成的体系
尽量少用#define来定义变量
如果是单纯定义常量,最好用const变量或者enums来代替
如果是定义宏函数,最好是用inline函数(函数定义部分写在头文件里)来代替
为成员函数提供const版本,并尽可能的让non-const版本在内部调用const版本,以减少代码量重复
尽可能将某些变量、成员函数、函数参数等声明为const,能帮助编译器识别错误用法
构造函数最好是使用成员初始化列表,初始化的顺序最好按照类中的声明顺序排列
为内置类型变量手动初始化,C++编译器并不会保证初始化该类型对象
使用static关键词声明变量时,尽量在较小作用域中使用,以降低静态成员初始化顺序问题的影响
编译器会自动为未定义相关函数的class
提供默认构造函数、拷贝构造函数、拷贝赋值运算符、析构函数
明确需要禁止的成员函数
将该成员函数设置为delete
或者将访问权限声明为private
如果某个类需要被继承,并且需要使用多态的特性
以防止析构时发生对象析构不充分,内存资源泄露的情况发生
必须要将其析构函数声明为virtual析构函数
不要在析构函数中throw任何异常,如果析构函数中调用的成员函数可能会抛出异常,那么尽量做异常处理,或者直接记录信息后吞掉异常
如果某些函数接口会抛出异常,那么应该提供成员函数接口来让客户手动执行该操作,而避免在析构过程中调用
构造函数的执行顺序为基类->派生类,从上至下执行,因此在基类中若调用了virtual函数,那么只会执行当前基类版本的该函数
反之析构函数的执行顺序为派生类->基类,从下往上执行,因此在派生类中若调用了virtual函数,那么执行的是当前派生类版本的该函数
解决方案为,若必须在构造或析构函数中执行,切勿将该成员函数声明为virtual类型
重载赋值运算符时,返回值为*this的引用,以支持连续运算操作
重载赋值运算符时,应注意实参有可能为该对象本身,切记对该情况进行合理的处理
A& A::operator=(cosnt A& a)
{
delete ptr;
ptr = new Type(a.ptr);
return *this;
}
常见的问题形式如上,若实参为该对象本身,那么在delete操作调用之后,new操作并不能实现资源的拷贝
#+begin_src c++
A& A::operator=(cosnt A& a)
{
if (&a == this) return *this;
delete ptr;
ptr = new Type(a.ptr);
return *this;
}
常见处理方法如上,这样通常可以满足要求,但是并不具备异常安全性,因为new操作符过程中可能会抛出异常
#+begin_src c++
A& A::operator=(cosnt A& a)
{
if (&a == this) return *this;
auto temp_ptr = new Type(a.ptr);
delete ptr;
ptr = temp_ptr
return *this;
}
最合理的处理方法如上,自我检测可加可不加,取决于发生自我赋值情况的频率
另外在重载赋值运算符时,若不需要对象深拷贝,或者参数为值传递的情况,可直接调用std::swap()函数进行对象内容的对调
重载赋值运算符时,切记对基类的成员部分也调用赋值运算符,方法为Base::operate=(param),以保证该对象能够完整拷贝
当拷贝构造和赋值运算符内部实现逻辑完全一样时,切忌互相调用,应声明第三个private函数,供前两者共同调用
为防止内存泄露,尽量在对象构造函数中获取资源,并且在析构函数中释放资源
若临时获取资源,应尽量使用智能指针std::shared_ptr来进行内存管理,以防止忘记delete导致内存泄露
复制RAII对象时必须一并复制其所管理的资源,因此该资源的复制、获取、释放等行为决定了RAII对象的相关行为
常见的RAII对象实现机制为,对内部资源进行值拷贝,并且实施引用计数方式进行管理
每一个RAII类应提供能够访问所管理内部资源的接口
接口类型可为显式函数调用,此方式较为繁琐,但比较安全
或可为隐式转换调用,通过重载A::operator Type()操作符,进行隐式转换,使用方便,但是可能出现难以预测的问题
new和delete成对使用时注意调用形式
比如new [] 与 delete []
如果已经考虑使用智能指针,那么尽量考虑将资源获取和智能指针对象定义放在一起,否则若期间触发异常,则可能会造成内存泄漏
任何接口如果要求用户必须记得做某些事情,那么这个接口的设计或许就不够合理
尽量保持接口的一致性,以及对内置类型的行为兼容
在设计接口时,考虑通过建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
尽可能使用智能指针作为接口的资源类型返回值
在设计一个类的时候,考虑如下问题:
自定义类型作为函数参数时尽量用const引用类型传递,而尽量少的使用值传递方式,因为会创建临时变量,导致多次调用构造和析构函数
对于内置类型而言,使用值传递会比const引用类型传递高效,因为引用的底层实现是传递指针,内置类型直接传值即可
函数返回某些对象时,切忌传递临时变量的引用和指针,或者内部成员变量的引用和指针,重载运算符除外
尽量避免返回函数作用域内的static变量,可能会产生意料之外的结果
从封装性的角度出发,任何成员变量都应该声明为private权限,尽量少用或不用protected来类的成员变量与函数
在考虑为某个类提供功能函数时,考虑该函数是否直接属于该类型的特性,否则应尽可能的使用外部非成员函数来实现相关功能
也即类本身应该只提供基本操作,参考代理模式,由代理类来完成较高层次的功能函数
如果需要为某个函数的所有成员提供隐式类型转换,那么务必将该函数声明为外部非成员函数
若需要进行双目运算符重载,请务必将其声明为外部非成员函数
利用C++函数查找的就近就优匹配原则,实现std::swap和类内置swap函数的最优选
尽可能将变量的定义和使用放在一起,否则若先定义变量,在实际使用变量之前抛出异常,则无端浪费构造+析构的开销
在循环体内部进行临时变量的构造和析构,与在循环体外部进行构造析构,内部进行赋值操作,两者相比,需要看该对象的特性来定
旧式类型转换:
C++提供的新式类型转换:
如果可以,尽量避免使用任何形式的类型转换,特别是注重效率时避免使用dynamic_cast
如果非要进行类型转换,尽量使用C++提供的新式类型转换,尽量规避使用C型旧式转换
尽量避免成员函数返回内部低级访问权限数据的引用,这样会导致封装性被降低
如果考虑成员函数返回内部成员的引用,则理应为返回值声明为const type func(para);
满足异常安全性的函数需要满足以下条件:
inline函数的生命和定义必须被放在头文件中
inline关键词只是申请使用inline形式进行编译,但是具体是否真正inline是由编译器做决定
谨记真正的inline函数无法被调试,程序出错时难以找出问题所在
对标准库或者第三方库等几乎不做修改的文件,直接#include即可
对自建类型等需要经常修改的class、struct,使用前置声明的形式,降低代码间依存性,提高修改代码后的编译效率
如果使用指针或者引用就能够声明接口,就不要直接使用对象
使用工厂模式以及代理模式,结合多态特性来降低代码间的耦合度,毕竟修改代码比单纯使用代码更加频繁
考虑提供只含有前置声明的头文件以供其他类使用,参考标准库中文件的实现形式,降低代码间依存性
namespace std{
class XXX;
class XXX;
class XXX;
......
}
切记,public继承意味着"is-a",适用于base class的所有操作,都可以应用在derived class身上
在继承基类时,派生类的某个成员函数可能会覆盖掉基类的同名方法,也即override
此时,若在派生类中想要使用基类的该方法,需要使用using声明,或者显式调用基类的某个方法base::func()
C++的继承一般分为以下几种:
virtual函数的替代方案如下:
class GameCharacter {
public:
int healthValue() const {
int retVal = doHealthValue();
return retVal;
}
private:
virtual int doHealthValue() const {
int ret = 0;
return ret;
}
};
···
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef int (*HealthCalcFunc) (const GameCharacter&);
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf) {}
int healthValue() const {
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
同一个类的不同对象可以有不同的生命值计算函数,使得其可以在运行时进行修改
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
: healthFunc(hcf) {}
int healthValue() const {
return healthFunc(*this);
}
private:
HealthCalcFunc healthFunc;
};
class GameCharacter;
class HealthCalcFunc {
public:
virtual int calc(const GameCharacter&) const { return 0; }
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc)
: pHealthCalc(phcf) {}
int healthValue() const { return pHealthCalc->calc(*this); }
private:
HealthCalcFunc* pHealthCalc;
};
virtual函数的替代方案包括NVI方法以及Strategy设计模式(策略模式)的多种形式,NVI方法本身是一种特殊的Template Method模式(模板方法模式)
将一些操作放在函数外部,声明为非成员函数是,带来的缺点就是无法访问class的私有成员
函数指针可用std::function<>模板库代替,提供了更高的灵活性
派生类重写覆盖父类的普通非虚函数,会导致多态无法按原有意愿做出反应
class Base
{
public:
void func(); // Base version
};
class Derived: public Base
{
public:
void func(); // Derived version
};
Derived *pD = new Derived;
Base *pB = pD;
pD->func(); // Derived version;
pB->func(); // Base version;
指向父类的指针,即便它其实本身是一个子类对象,当调用一个被子类重写了的非虚函数时,会出现父类指针或引用会调用父类的该函数
子类指针或引用会调用子类的该函数,多态的特性也就无法体现
永远不要重新定义父类函数参数的默认值,参考以下代码情景:
class Shape {
virtual void func(int a = 0);
};
class Rectangle: public Shape {
virtual void func(int a = 1);
};
class Circle: public Shape {
virtual void func(int a = 0);
};
Shape* pR = new Rectangle; // 静态类型为Shape*,动态类型为Rectangle
Shape* pC = new Circle; // 静态类型为Shape*,动态类型为Circle
pR->func() // a = 0; default a by Shape::func(int a = 0);
pC->func() // a = 0; default a by Shape::func(int a = 0);
静态类型即为编译时类型,动态类型为运行时类型,因此,继承多态的应用主要是利用动态类型的特性
但是函数的默认参数值却依赖于静态类型,因此pR调用函数时若含有默认函数,则会使用Shape*也即静态类型的默认参数值
当无法通过继承来合理的利用现有的class时,尝试通过"has-a"或者"is-implemented-in-terms-of"的关系,
使用聚合模式来提供一些函数接口,这些接口具体是调用其内部成员的相关函数接口
private继承相当于隐去了父类的所有接口,无论是虚函数还是非虚函数
只保留了父类的成员变量,也即只继承了父类的实现部分,抛弃了接口部分
同时,private继承会使得多态被禁用,也即子类的指针或引用无法转换成父类
private继承意味着is-implemented-in-terms-of(根据某物实现出),当出现这种情况时,通常建议使用38条中的聚合模式来实现
不得不使用private继承的情况通常为极限内存需求情况:
class Empty{};
class HoldsInt: private Empty
{
private:
int x;
};
这样的继承形式,会通过EBO(empty base optimization: 空白基类最优化)来使得class HoldsInt的内存大小等于一个int的大小
当需要处理两个class的关系时,若其不存在任何的"is-a"关系,同时其中一个class需要访问另一个class的protected成员,
或者需要重新定义另一个class的某些virtual函数,或许可以尝试使用private继承形式
注意相同函数(非重载情况)的歧义冲突,注意菱形继承
采用虚继承方式解决菱形继承问题,但是要注意,虚继承会增加大小、降低速度、增加初始化等成本
因此非必要不使用虚继承,如果必须使用虚继承,尽量不要在基类中添加成员变量
多继承加继承权限的妙用:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() cosnt = 0;
};
class DatabaseID;
class PersonInfo {
public:
explicit PersonInfo(DataBaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
virtual const char* valueDelimOpen() cosnt;
virtual const char* valueDelimClose() cosnt;
};
class CPerson: public IPerson, private PersonInfo {
public:
explicit CPerson(DatebaseID pid)
: PersonInfo(pid) {}
virtual std::string name() const {
return PersonInfo::theName();
}
virtual std::string birthDate() const {
return PersonInfo::theBirthDate();
}
private:
virtual const char* valueDelimOpen() cosnt { return ""; };
virtual const char* valueDelimClose() cosnt { return ""; };
};
class CPerson私有继承class PersonInfo,因此CPerson可以使用PersonInfo的大部分接口,同时也能够嫁接继承到class IPerson的成员函数
模板元编程这一部分平时比较难用得上,后边也看不太懂,没遇到过这种情况就很难理解
普通class和templates都支持接口和多态,一个是运行时多态,一个是编译期多态
对class而言,接口是显式指定的,多态通过virtual声明,发生于运行时
对templates而言,接口一般是隐式指定的,基于函数内的实际表达式,多态通过template实例化和函数重载解析,发生于编译期
声明模板时,typename和class是等价的
template<typename T>
void func(T &t);
template<class T>
void func(T &t);
typename有其独特的用处,在模板内部,针对模板类型的嵌套类,需要前置使用typename声明其为某种类型
template<typename T>
class Temp: public Base<T>
{
public:
Temp();
typename Base<T>::B b;
typename T::A a;
};
考虑如下模板类继承情况:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
sendClear(info); // Error
}
};
这段代码无法通过编译,因为编译器在这个类中无法找到sendClear()函数的信息,编译器在解析基类之前并不知道基类中是否含有此成员函数
解决办法有如下三种:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
this->sendClear(info); // 显式指定sendClear()函数是继承自基类的
}
};
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
using MsgSender<Company>::sendClear;
void sendClearMsg(const MsgInfo& info)
{
sendClear(info); // 假设sendClear()函数是继承自基类的
}
};
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
MsgSender<Company>::sendClear(info); // 显式指定sendClear()函数是继承自基类的
}
};
templates按照实际调用实例化自动生成多个class和多个函数,所以任何template代码都不应该与某个造成膨胀的template参数产生相依关系,
所谓膨胀就是代码膨胀,大量只有少数差异的重复代码
因非类型模板参数而造成的代码膨胀,往往可以消除,通过以函数参数或者class成员变量来替换template的特定参数
因为类型参数而造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表述的实例化类型共享二进制代码,但是不在代码中实现
使用成员函数模板生成“可接受所有兼容类型”的函数
如果声明的成员函数模板是用于拷贝构造或者赋值运算符,那么还需要声明正常的拷贝构造和赋值运算符,否则编译器会生成默认的版本
在编写一个class template时,如果需要提供一个具备所有参数隐式类型转换的函数,则需要将该函数在类内声明和定义,并且声明为friend函数
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);
template<typename T>
class Rational
{
public:
friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{
return doMultiply(lhs, rhs);
}
};
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}
另外需要注意的是,涉及到模板的源代码,用.hpp文件存储声明和定义,不要拆开到.h和.cpp中
类内重载new和delete操作符的时候,会自动隐藏掉外部全局作用域的操作符,因此理应在类内提供全局作用域的new和delete版本
严肃对待编译器发出的警告信息,尽量使得自己的程序在编译过程中不要报出任何一个警告
但是也不要过度依赖编译器的警告能力,并不是所有的问题编译器都可以准确定位,并且不同的编译器提供的检测也不尽相同
熟悉C++标准库模块,以及其他的第三方库模块,C++11之后似乎大部分第三方库比较出彩的功能都被整合到标准库了
同上