写在前面
一次偶然的机会同时拿到了游戏客户端、TA、和游戏引擎开发的Offer
因为对图形感兴趣,所以打算自己造一次轮子
如你所见,开始复习C++
读了读《Effective C++》甚至感觉这本书就是专门给我这种弟弟写的,本文也是此书的阅读笔记
正文
一、习惯C++
作为一个写了两年C#和Shader被商业引擎惯坏了的程序员,看见老朋友甚至有点陌生
01:把C++当做一个语言联邦
把C++当做由四个次语言组成的联邦,从一个次语言到另一个次语言时,守则可能改变。
这四个次语言分别是:C、Object-Oriented C++、Template C++、STL
02:尽量以const、enum、inline 替换 #define
#define ASPECT_RATIO 255
开发者极有可能被它所带来的编译错误感到困惑,编译器可能提到255而不是ASPECT_RATIO,也许该语句被其他预处理干掉,追踪它会浪费时间。
解决办法就是用常量替换宏
const int AspectRatio = 255;
着重说明
-
由于常量经常定义于头文件内,因此有必要将指针(而不是指针所指之物)声明为const
const char* const authorName = "TOSHIRON";
-
对于Class专属常量,为了确保他只有一份实体,必须使其成为static成员
class GamePlayer { private: static const int NumTurns = 5; ... }
-
如果不想让别人获取到指向某个常量的指针,因为取const地址是合法的,所以可以用enum取代
class GamePlayer { private: enum{ NumTurns = 5 }; ... }
-
用内联函数替代宏,以获得相同的效率和功能
#define MAX(a,b) f((a) > (b) ? (a) : (b))
template<typename T> inline void callWithMax(const T& a, const T& b) { f(a > b ? a : b); }
03: 尽可能使用 const
const出现在*左边,被指物是常量
const出现在*右边,指针自身是常量
const出现在*两边,指针和所指事物都是常量
着重说明
-
令一个函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性
我感觉这几乎主要是为了单独避免一种情况class Rational{...}; const Rational operator* (const Rational& lhs, const Rational& rhs); ... Rational a,b,c; ... if(a*b = c){...}
找错麻烦?因为常量不允许赋值所以会直接报错
-
利用常量性(constness)不同,重载函数
class TextBlock { public: ... const char& operator[] (std::size_t position) const //第二个const是其重载的依据 { return text[positon]; } char& operator[] (std::size_t position) { return text[position]; } private: std::string text; }
TextBlock tb("Hello"); std::cout << tb[0]; //调用non-const Const TextBlock ctb("World"); std::cout << ctb[0]; //调用const
-
const成员函数不可以更改对象内任何non-static成员变量;解决办法就是用 mutable 关键字修饰,使变量总是可更改的,及时在const成员函数内。
-
在 const 和 non-const 成员函数中避免重复,常量性重载往往伴随着大量重复代码,这时,我们需要让non-const 利用 const 函数。
class TextBlock { public: ... const char& operator[] (std::size_t position) const { ... ... return text[positon]; } char& operator[] (std::size_t position) { return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]); } private: std::string text; }
首先,将 *this 转型为 const,调用const成员函数,再移除const
04: 确定对象在使用前已经初始化
这是当然了,至少我使用过的任何编程语言都有要求这一点
int x;
x有可能被初始化为0
class Point
{
int x, y;
};
...
Point p;
p的成员变量有时候会初始化为0,有时候不会,所以手动初始化很有必要。
着重说明
-
C++规定,对象的成员变量初始化动作发生在进入构造函数本体之前。
class PhoneNumber{...}; class ABEntry { public: ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones); private: string theName; string theAddress; list<PhoneNumber> thePhones; int numTimeConsulted; ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones) { theName = name; theAddress = address; thePhones = phones; numTimesConsulted = 0; }
书上的说法是 构造函数中那四行四赋值,而不是初始化
构造函数应该改为ABEntry(const string& name, const string& address, const list<PhoneNumber>& phones) :theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) { }
-
为解决两个不同编译单元内的初始化次序问题,使用local static 替代 non-local
class FileSystem { public: ... size_t numDisks() const; ... }; extern FileSystem tfs;
替代为
class FileSystem { public: ... size_t numDisks() const; ... }; FileSystem& tfs() { static FileSystem fs; return fs; }
这样,在调用时才不用在乎初始化顺序的问题
二、构造、析构、赋值运算
因为GC的存在,很长时间没有用析构函数了
05: 了解C++默默编写并调用哪些函数
编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。
class Empty{};
相当于
class Empty
{
public:
Empty(){...}
Empty(const Empty& rhs){...}
~Empty(){...}
Empty& operator = (const Empty& rhs){...}
};
着重说明
- 当对一个“内含reference 成员”或“内含const 成员”进行赋值操作时,编译器自己生成的赋值重写无法完成此工作,需要自己专门写。
- 如果把赋值重写设为private,那更调用不了。
06: 如果不想用编译器自动生成的函数,就应该明确拒绝
如果想要驳回编译器自动提供的函数,可以将成员函数声明为 private 并且不与实现。
class Uncopyable
{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator = (const Uncopyable&);
};
然后之后的类可以继承Uncopyable,反正C++可以多继承,但是多继承会阻止empty base class optimization,慎重
class Abc:private Uncopyable{...}
07: 为多态基类生命 virtual 析构函数
class TimeKeeper
{
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
class WaterClock:public TimeKeeper{...};
class AtomicClock:public TimeKeeper{...};
这时,才会正确摧毁整个对象
WaterClock wc;
...
delete &wc;
然而,没有多态用途的类,尽量避免使用 virtual。
08: 别让异常逃离析构函数
-
在析构函数中用 try-catch 把错误盖住,至少要比草率结束程序要好,比如在构造函数中建立了数据库链接(?谁会干这种傻事)
~DBconn() { try{db.close();} catch(...) { 记录错误日志 { }
-
对上面的方法进行改良的话,就是用一个bool记录是否关闭过,如果是,就不必再close了
void close() { db.close(); closed = true; } ~DBconn() { if(!closed) { try{db.close();} catch(...) { 记录错误日志 } } }
09: 绝不在构造和析构函数中调用 virtual 函数
据说像我这种C#过来的,更应该重视这一点。
主要是因为,首先会调用基类的版本。理由就是:base class构造函数会优先于其派生类的构造函数,这时,派生类的变量什么的还没初始化。不会下降到派生类的重写版。
10: 令 operator= 返回一个 reference to *this
这是一个协议?也包括*= += -= /=等。
class Widget
{
public:
...
Widget& operator = (const Widget& rhs)
{
...
return *this;
}
...
};
11: 在 operator= 中处理 “自我赋值”
大概是这么个情况
class Bitmap{...};
class Widget
{
...
private:
Bitmap* pb;
};
...
Widget w;
...
w = w;
主要为了避免在delete的时候把=左边的也删除了,主要解决办法有:
-
证同测试
Widget& Widget::operator=(const Widget& rhs) { if(this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; }
-
第二个做法是先复制一份pb,再删除之前的,但我感觉有点浪费,所以觉得好像不是一个好方法。
12: 复制对象时不要遗漏任何一个部分
如果为class添加了成员变量,那必须同时修改copying函数,以及operator=的重写。
- 在派生类的copying函数调用基类的构造。
- 在operator= 中对基类成员变量赋值。
三、资源管理
好吧,我被GC惯坏了
13: 以对象管理资源
看起来没毛病的操作
void f()
{
Investment* pInv = createInvestment();
...
delete pInv;
}
竟然考虑到在…提前return的情况。使用auto_ptr 智能指针
void f()
{
auto_ptr<Investment> pInv(createInvestment());
...
}
auto_ptr在销毁时会自动销毁它的所指之物,但是要注意不能让多个auto_ptr指向同一个对象,如果利用copying来复制,那么将会得到"剪切"的效果。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
std::auto_ptr<Investment> pInv2(pInv); //pinv 设为null,pInv2 指向原对象
...
}
解决办法就是用shared_ptr 替代,“引用计数型智慧指针” 持续追踪共有多少对象指向某笔资源,并在无人指向它时自动删除该资源。(有点GC的意思)
void f()
{
std::tr1::shared_ptr<Investment> pInv(createInvestment());
std::tr1::shared_ptr<Investment> pInv2(pInv); //pInv 和 pInv2 指向同一个对象
...
}
所以建议用 shared_ptr。
14: 在资源管理类中小心 copying 行为
首先思考:被复制时会发生什么?可能会重复锁定同一个资源,在析构的时候可能重复销毁同一个资源。
解决方法
- 禁止复制,利用条款6,把copying private掉。
- 使用引用计数法,让最后一个使用者销毁资源。
class Lock { public: explicit Lock(Mutex* pm): mutexPtr(pm,unlock) ///unlock为删除资源的函数 { lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; }
- 复制底部资源,深度拷贝大法
- 当只想有一个对象加工资源时,可以利用auto_ptr转变拥有权。
15: 在资源管理类中提供对原始资源的访问
就像 auto_ptr.get() 从智能指针中获取原始指针那样
int daysHeld(const Investment* pi);
...
std::tr1::shared_ptr<Investment> pInv(createInvestment());
int days = daysHeld(pInv.get());
提供一个函数(显示转换),或者提供隐式转换
class FontHandle{...}
class Font
{
public:
...
operator FontHandle() const
{
return f;
}
private:
FontHandle f;
}
16: 成对使用 new 和 delete 时要采用相同形式
std::string* stringArray = new std::string[100];
...
delete stringArray;
看起来好像没什么毛病,但是实际上只删除了第一个string。
应该使用
delete[] stringArray;
如果 new 表达式中使用 [],对应的 delete 表达式中也使用 []。
如果 new 表达式中不使用 [],对应的 delete 表达式中一定不要使用 []。
17: 以独立语句将 newed 对象置入智能指针
int A();
void B(std::tr1::shared_ptr<CClass> pw, int a);
务必不要直接
B(std::tr1::shared_ptr<CClass>(new CClass), A());
因为,编译器执行次序不定,如果A()导致异常,可能导致难以察觉的错误
所以至少要把智能指针的创建分离出来
pc = std::tr1::shared_ptr<CClass>(new CClass);
B(pc, A());
四、设计与声明
18: 让接口容易被正确使用,不易被误用
- “促进正确使用”可以 功能性相似接口的一致性,以及内置类型的行为兼容。以C#的 .Length 和 .Count() 为反例
- “阻止误用”可以 对类型限制,建立新类型,束缚对象值。
但我觉得这十分麻烦,或许只是这个例子不好,比如2001.02.29这个日期,感觉还是在函数内检验比较好Date a(1998,12,28); //为了防止无效日期,增加Day,Month,Year类,对Int封装,做有效性限制 Date a(Year(1998),Month(12),Day(28));
- 利用shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变成0时将被调用的“删除器”。
std::tr1::shared_ptr<Investment> createInvestment() { std::tr1::shared_ptr<Investment> retVal(static_cast<Investment *>(0), getRidOfInvestment); retVal = ...; return retVal; }
19: 设计 class 犹如设计 type
- 新 type 的对象应该如何被创建和销毁?构造,析构,内存分配及释放
- 对象的初始化和对象的赋值该有什么样的差别?明确构造函数和赋值操作符的行为
- 新 type 被 passed by value 意味着什么? copy构造函数的实现
- 什么是新 type 的“合法值”?在构造函数,赋值操作上的约束
- 新 type 需要配合某个继承图系吗?注意他们的virtual函数
- 新 type 需要什么样的转换? 显示、隐式转化 参考15
- 新 type 允许那些操作符和函数?
- 什么标准函数需要驳回? 参考6
- 新 type 成员的作用域
- 什么是新 type 的“未声明接口”? 参考29 对效率。异常安全性及资源运用提供保证
- 如果需要一个types家族,那应该定义一个 class template
- 有没有必要定义一个新的 type
20: 宁以 pass-by-reference-to-const 替换 pass-by-value
因为值传递会调用 copy 构造函数带来不必要的构造和析构,所以可以
bool validateStudent(const Student& s);
这样,因为const不允许更改,函数内编写时也会自律不去修改
注意
这并不适用于内置类型,STL 迭代器 和函数对象。
21: 必须返回对象时,别妄想返回其 reference
为了正常的 delete 和析构,在返回reference 和 object之间做出选择。不要忘记可能同时需要多个引用或指针指向的对象,而在内存释放上出现问题。
22: 将成员变量声明为 private
- 将成员变量声明为 private,可以更细微的划分访问控制。
class AccessLevels { public: ... int getReadOnly() const { return readOnly;} void setReadWrite(int value) { readWirte = value;} int getReadWrite() const { return readWrite;} void setWriteOnly(int value) { writeOnly = value;} private: int noAccess; int readOnly; int readWrite; int writeOnly; }
- protected 并不比 public 更具封装性。
23: 宁以 non-nember、non-friend 替换 member 函数
好吧,C#过来的感到震惊
class WebBrowser
{
public:
...
void clearCache();
void clearHistory();
void removeCookies();
...
};
现在想要做一个同时调用 WebBrowser里三个清理函数的函数,下面两种做法那种好
- 在类里提供
class WebBrowser { public: ... void clearEverthing(); ... };
- 在同空间名里额外做一个函数
void clearBrowser(WebBrowser& wb);
居然是2好,因为更具有封装性。它的逻辑大概是这样
首先,成员变量声明为 private 就意味着有更少的函数能访问它,如果不是 private 那么就有很多函数可以访问它,它就不具有封装性。那么,越少函数能访问 private,那封装性越好。所以2好。
24: 若所有参数皆需类型转换,请为此采用 non-member 函数
令 classes 支持隐式类型转换通常是个糟糕的主意。
请记住:如果需要为某个函数的所有参数进行类型转换,那么这个函数必须是个 non-member.
25: 考虑写出一个不抛异常的 swap 函数
- 交换两个对象真正要做的是交换它们的指针
- 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常
- 如果你提出一个 member swap,也应该提供一个 non-member swap 来调用前者
- 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何 “命名空间资格修饰”
- 为“用户自定义类型”进行 std templates 全特化是好的,但千万不要尝试在 std 内加入某些对 std 而言全新的东西
五、实现
有些细节我从来没有考虑过
26: 尽可能拖延变量定义式的出现时间
定义一个带有构造和析构函数类型的变量,就要承担其构造和析构带来的消耗。
好吧,抠得真细,说来惭愧,这是我从来没有考虑过的事情。
再加上 通过 default 构造函数构造出一个对象然后对它赋值 比 直接在构造时指定初值 效率差。可以看情况选择在必要时利用其 copy 构造函数初始化。
遇到循环时,我还是觉得应该在循环外声明,构造代价小于赋值 个人认为情况很少。
27: 尽量少做转型动作
两种形式的 “旧式转型”,c风格
(T)expression
T(expression)
c++的新式转型
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
- const_cast 通常被用来将对象的常量性转除
- dynamic_cast 主要用来执行“安全向下转型”,也就是用来决定某个对象是否归属继承体系中的某个类型,效率低
- reinterpret_cast 执行低级转型,动作及结果取决于编译器,不可移植
- static_cast 强迫隐式转换,例如将 non-const 对象转换为 const 对象,或将 int 转换为double 等。可以执行上述多种转换或反向转换,除了const 转换为 non-const(这只有 const_cast 才行)
注意
- c++ 中一个对象可以拥有一个以上的地址(如以派生类指向它 和 以基类指向它)
- dynmaic_cast 往往在你手上只有对象的基类但是又想当派生类处理一个对象时,只能用它处理
所以还是尽量在基类里提供虚函数比较好class Window{...}; class SpecialWindow: public Window { public: void blink(); ... }; typedef std::vector<std::tr1::shared_ptr<Window>> VPW; VPW winPtrs; ... for(VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) { if(SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get())) { spw->blink(); } }
- 尽可能的使用新式转换
28: 避免返回 handles 指向对象内部成分
handles 包括 references、指针、迭代器
既然成员变量是private,就不能把它的指针return
可以参考3的做法用const
29: 为“异常安全”而努力是值得的
异常安全函数提供以下三个保证之一:
- 基本承诺:如果异常抛出,程序内的任何事物仍然保持在有效状态下。虽然结果可能不同,但是没有对象或数据结构被破坏。
- 强烈保证:如果异常抛出,程序状态保持不变。如果函数失败,程序会恢复到“调用函数之前”的状态。
- 不抛掷保证:承诺不抛出异常,因为他们总是能完成他们原先承诺的功能。
强烈保证往往用 copy-and-swap 的方法完成:修改对象数据的副本,然后如果不抛出异常,就将修改后的数据和原件置换。
至少完成基本承诺。
30: 透彻了解 inlining 的里里外外
-
热衷于 inlining 会造成程序体积太大,inline造成的代码碰撞会导致额外的换页行为,降低指令告诉缓存设置的击中率
-
编译器通常不对“通过函数指针而进行的调用”实施inlining
inline void f(){...} void(* pf)() = f; //指针pf指向f ... f(); //被inlined pf(); //不被inlined
-
inline 函数无法随着程序库的升级而升级。如果改变inline 函数 f,那所有用到f的客户端程序都要重新编译
-
大多数 inlining 限制在小型、频繁调用的函数身上
31: 将文件间的编译依存关系降至最低
目的是降低修改实现导致不必要的编译
-
如果使用 object references 或 object pointers 可以完成任务,就不要使用 objects。
-
如果可以,尽量以 class 声明式替换 class 定义式。注意:当声明一个函数而它用到某个 class 时,你并不需要该 class 的定义
class Date; //class 声明式 //下面两个在不使用的情况下不会用到定义式,关键点在于,并不是每个人都会调用它 Date today; void clearAppointments(Date d);
-
声明式和定义式提供不同的头文件。
#include "classafwd.h" //此头文件中申明,但没有定义 ClassA ClassA Fun1(); void Fun2();
-
C++ 允许在Interfaces 内实现成员变量或成员函数。来自C#玩家的震惊
-
程序库头文件应该以 “完全且仅有声明式”的形式存在。
依赖于声明式,不依赖于定义式的两个手段
- 利用 Interface class,解除接口和实现之间的耦合关系,从而降低编译依赖性,在interface class 内声明 static 处理函数,和下面有点类似。
- Handle classes,专门用来处理的类,不会有依赖其它类的成员函数,一切输入来自于传递给函数的指针或引用。
#include "Person.h" //定义式头文件 #include "PersonImpl.h" //实现类的头文件,接口相同 Person::Person(const string& name. const Date& birthday, const Address& addr) : pImpl(new PersonImpl(name,birthday,addr)){} std::string Person::name() const { return pImpl->name(); }
六、继承与面向对象设计
32: 确定你的 public 继承塑模出 is-a 关系
书上的例子很生动 -_-||
public 继承 意味着 is-a。适用于 base classes 身上的每一件事一定也适用于 derived classes 身上。因为每一个 derived class 对象也都是一个 base class 对象。
33: 避免遮掩继承而来的名字
-
只要重写一个虚函数,基类的重载也无效了
class Base { private: int x; public: virtual void mf1() = 0; virtual void mf1(int); virtual void mf2(); void mf3(); void mf3(double); ... }; class Derived: public Base { public: virtual void mf1(); void mf3(); void mf4(); ... };
Derived d; int x; ... d.mf1(); //调用 Derived:mf1 d.mf1(x); //错误,被掩盖 d.mf2(); //调用 Base::mf2 d.mf3(); //调用 Derived::mf3 d.mf3(x); //错误,被掩盖
如何解决呢?使用 using 声明式
class Derived: public Base { public: using Base::mf1; //让Base class内名为 mf1 和 mf3 的所有东西可见 using Base::mf3; virtual void mf1(); void mf3(); void mf4(); ... };
-
如果我们只想让上例中 Derived 只继承 mf1无参版本,应该怎么办? 定义转交函数
class Derived: public Base { public: virtual void mf1() { Base::mf1(); } ... };
34: 区分接口继承和实现继承
- C#中没有的操作,调用基类中虚函数的实现
class Base { public: virtual void draw() cosnt = 0; ... } class A:Base{...} ... Base* a = new A; a->draw(); a->Base::draw(); //调用基类的虚函数
- 接口继承和实现继承不同。在public继承下,derived classes 总是继承 base class 的接口。
- pure virtual 函数只具体制定接口继承
- impure virtual 函数具体指定接口继承及缺省实现继承
- non-virtual 函数具体指定接口继承以及强制性实现继承
35: 考虑 virtual 函数之外的其他选择
-
使用 non-virtual interface 手法,以 public non-virtual 成员函数包裹较低访问性(private 或 protected) 的 virtual 函数。和模板模式很像
-
将 virtual 函数替换为“函数指针成员变量” 或 用tr1::function 成员变量替换 virtual函数
-
将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数
-
tr1::function 对象行为就像一般函数指针,可以接纳“与给定之目标签名式兼容”的所有可调用物
typedef std::tr1::function<int (const ClassA&)> TypeA;
类型TypeA接受任何兼容 ClassA& ,返回 int
可以在构造时赋值,之后使用
class A; int DealFunction(const A& a); class A { public: typedef std::tr1::function<int (const ClassA&)> TypeDealA; explicit A(TypeDealA tda = DealFunction) : dealFun(hcf){} int Deal() const { return dealFun(*this); } ... private: TypeDealA dealFun; };
在C#里通过Action或delegate来传递函数,C++可以直接这样做
36: 绝不重新定义继承而来的 non-virtual 函数
其实这本身就符合条例32,不重新 non-virtual 就对了
37: 绝不重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而 virtual 函数——你唯一应该复写的东西是动态绑定
class Shape
{
public:
enum ShapeColor
{
Red,
Green,
Blue
};
virtual void draw(ShapeColor color = Red) const = 0;
...
};
class Rectangle:public Shape
{
public:
virtual void draw(ShapeColor color = Green) const;
...
};
class Rectangle:public Shape
{
public:
virtual void draw(ShapeColor color) const;
...
};
这个时候
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
pc->draw(); // color == Red 自动调用缺省
pr-draw(); // color == Red 因为pr 的静态类型是 Shape 所以调用 基类的缺省参数值
38: 通过复合塑造模出 has-a 或 “根据某物实现出”
- 复合的意义和public继承完全不同
- 应用域,复合意味 has-a。在实现域,复合意味 is-implemented-in-terms-of
实例中仿佛在讲如何处理 set 和 list 的关系。
39: 明智而审慎地使用 private 继承
- private 继承 编译器不会自动将派生类对象转换为 基类对象。
- 由 基类继承而来的所有成员,在派生类中会变成 private 属性。
- 作用类似 C# 中的 sealed,禁止重写
class Widget { private: class WidgetTimer:public Timer { public: virtual void onTick() const; ... } WidgeTimer timer; ... }
- Private 继承意味 is-implemented-in-terms of。它通常比复合的级别低。但是当 派生类需要访问 protected base class 的成员,需要重新定义继承而来的 virtual 函数时,这么设计是合理的。
- 和复合不同,private 继承可以造成 empty base 最优化。
- 当面对“并不存在 is-a 关系”的两个 classes,其中一个需要访问另一个的 protected 成员,或需要重新定义其中一个或多个 virtual 函数,private 继承极有可能成为正统策略。
40: 明智而慎重地使用多继承
- 多重继承比单一继承复杂。它可能导致新的歧义性,以及对 virtual 继承的需要。
- virtual 继承会增加大小、速度、初始化复杂度等等成本。如果 virtual base classes 不带任何数据,将是最具有实用价值的情况。
- 多重继承的确有正当用途。其中一个情节涉及“public 继承某个 Interface class” 和 “private继承某个协助实现的 class” 的两个组合。
情况说明
- 钻石型多继承导致歧义性
如果File Class有个成员变量 fileName,在IOFile中调用 fileName,就会出现歧义性。class File{...}; class InputFile: public File{...}; class OutputFile: public File{...}; class IOFile: public InputFile, public OutputFile{...};
- 使用 virtual base classes 继承可以解决,但是非必要不要用,而且用也不要在 virtual base class 中放置数据。
class File{...}; class InputFile: virtual public File{...}; class OutputFile: virtual public File{...}; class IOFile: public InputFile, public OutputFile{...};
七、模板与泛型编程
让我看看有什么不一样
41: 了解隐式接口和编译期多态
- classes 和 templates 都支持接口 (interfaces)和多态(polymorphism)。
- 对 classes 而言接口是显式的,以函数签名为中心。多态则是通过 virtual 函数发生于运行期。
- 对 template 参数而言,接口是隐式的,奠基于有效表达式。多态则是通过 template 具体化和函数重载解析发生于编译期。
42: 了解 typename 的双重意义
在 template 声明式中,class和typename没有什么不同
template<class T> class Widget;
template<typename T> class Widget;
我觉得,书上的例子太极端了。
- 声明 template 参数时,前缀关键字 class 和 typename 可互换,
- 请使用外部关键字 typename 标识嵌套从属类型名称;但是不得在 base class lists 或 member initialization list 内以它作为 base class修饰符。
template<typename T>
class Derived: public Base<T>::Nested //不允许
{
public:
explicit Derived(int x) : Base<T>::Nested(x) //不允许
{
typename Base<T>::Nested temp; //必须以 typename 修饰
...
}
...
};
43: 学习处理模块化基类内的名称
class CompanyA
{
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(cosnt std::string& msg);
...
};
class CompanyB
{
public:
...
void sendCleartext(const std::string& msg);
void sendEncrypted(cosnt std::string& msg);
...
};
...
class MsgInfo{...};
template<typename Company>
class MsgSender
{
public:
...
void sendClear(const MsgInfo& info)
{
std::string msg;
Company c;
c.sendClearText(msg);
}
void sendSecret(const MsgInfo& info)
{...}
};
因为不知道 LoggingMsgSender 继承什么样的Class,他继承自模板,所以调用sendClear会报错
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
sendClear(info); //报错
}
...
};
解决办法
-
在 base class 函数调用动作之前加上“this->”
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { this->sendClear(info); } ... };
-
使用 using 声明式
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: using MsgSender<Company>::sendClear; //告诉编译器,假设sendClear 位于 base class ... void sendClearMsg(const MsgInfo& info) { sendClear(info); } ... };
-
明白支持被调用的函数位于 base class内
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { MsgSender<Company>::sendClear(info); } ... };
44: 将与参数无关的代码抽离 templates
- Templates 生成多个 classes 和多个函数,任何 template 代码都不该与某个造成膨胀的 template 参数产生依赖关系
- 因非类型模板参数造成的代码膨胀,往往可以消除,以函数参数或 class成员变量替换 template参数
- 因类型参数造成的代码膨胀,往往可以降低,让带有完全相同二进制表述的具体类型共享实现码
45: 运用成员函数模板接受所有兼容类型
- 使用 member function templates (成员函数模板) 生成 “可接受所有兼容类型” 的函数
- 如果你声明 member templates 用于“泛化 copy 构造” 或 “泛化 assignment 操作”,你还是需要声明正常的 copy 构造函数和 copy assignment 操作符。
template<class T>
class shared_ptr
{
public:
//构造
shared_ptr(shared_ptr const& r);
template<class Y>
shared_ptr(shared_ptr<Y> const& r);
//copy
shared_ptr& operator = (shared_ptr const& r);
template<class Y>
shared_ptr& operator = (shared_ptr<Y> const& r);
...
46: 需要类型转换时请为模板定义非成员函数
当我们编写一个 class template,而它所提供之 “于此 template 相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend 函数”。
template<typename T>
class Rational
{
public:
Rational(const T& numerator = 0, const T& denominator = 1);
cosnt T numerator() cosnt;
const T denominator() const;
...
};
template<typename T>
const Rational<T> operator* (cosnt Rational<T>& lhs, const Rational<T>& rhs){...}
然后
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2; //报错,无法通过编译
因为编译器不知道T是什么,所以找不到正确的 operator*
必须先有相关函数推导出参数类型,声明 operator* 为友元函数可以化简这个过程
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());
}
现在执行不会报错了,因为,当对象 oneHalf 被声明为一个 Rational<int>
,classRational<int>
于是被具现化出来,而作为过程的一部分,friend 函数operator*
也就被自动声明出来。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数。
47: 请使用 traits classes 表现类型信息
- STL 主要由 “用以表现容器、迭代器和算法” 的 templates 构成
- Traits classes 使得“类型相关信息”在编译期可用。它们以 templates 和 “templates 特化”完成实现
- 整合重载技术后,traits classes 有可能在编译期对类型执行 if…else 测试
48: 认识 template 元编程
- Template metaprogramming(TMP,模板元编程) 可将工作由运行期移往编译器,因而得以实现早期错误侦测和更高的执行效率
- TMP 可被用来生成“基于政策选择组合”的客户定制代码,也用来避免生成对某些特殊类型并不合适的代码。
吃鲸,还有这种操作
template<unsigned n>
struct Factorial
{
enum
{
value = n * Factorial<n-1>::value
};
};
template<>
struct Factorial<0>
{
enum
{
value = 1
};
};
在main中
std::cout<<Factorial<5>::value; //打印 5! 120
还能这样?这只是“hello world”而已
八、定制 new 和 delete
STL容器所使用的 heap 内存是由容器所拥有的分配器对象(allocator objects)管理,不是被 new 和 delete 直接管理。所以本章并不讨论
49: 了解 new-handler 行为
可以理解为专门catch operator new 的函数
void outOfMem()
{
std::cerr << "Unable to satisfy request for memory\n";
std::abort();
}
int main()
{
std::set_new_handler(outOfMem);
int* pBigDataArray = new int[1000000000L];
...
}
当 无法为 100000000个整数分配足够的空间,outOfMem会被调用。
一个设计良好的 new-handler 函数必须做以下事情:
- 让更多内存可被使用。这使得 new 操作下一次分配内存动作成功
- 安装另一个 new-handler。就像是交换机一样,寻找合适的。
- 卸除 new-handler。将null指针传给 set_new_handler,抛出异常
- 抛出 bad_alloc的异常。不会被 operator new捕捉
- 不返回。一般是调用 abort 或 exit
50: 了解 new 和 delete 的合理替换机制
替换编译器提供的operator new 或 operator delete 主要有三个理由:
- 用来检测运行上的错误
- 增加效能
- 增加分配和归还的速度
- 降低缺省内存管理器带来的空间额外开销
- 弥补缺省分配器中的非最佳齐位
- 将相关对象成簇集中
- 为了收集使用上的统计数据
51: 编写 new 和 delete 时需要固守常规
- operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用 new-handler。它也应该有能力处理0 bytes申请。class 专属版本则还应该处理“比正确大小更大的申请”。
- operator delete 应该在收到 null 指针时不做任何事。Class 专属版本则还应该处理“比正确大小更大的申请”。
52: 写了 placement new 也要写 placement delete
- 当写了一个 placement operator new,请确定也写出了对应的 placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
- 当你声明 placement new 和 placement delete,请确定不要无意识地遮掩了他们的正常版本。
九、杂项讨论
53: 不要轻忽编译器的警告
54: 让自己熟悉包括 TR1 在内的标准程序库
C++标准程序库包括
- STL,包括
- 覆盖容器,如 vector、string、map
- 迭代器 iterators
- 算法 find、sort、transform
- 函数对象 less、greater
- 各种容器适配器 stack、priority_queue
- Iostreams,包括 自定义缓冲区、国际化I/O,以及预定义好的 cin、cout、cerr 和 clog
- 国际化支持,多区域能力
- 数值处理,包括 复数模板 complex 和 纯数值数组 valarray
- 异常阶层体系,包括 base class expection 、derived classes logic_error 和 runtime_error
- C89 标准程序库
TR1 详细描述了 14 个新组件
tr1::shared_ptr
和tr1::weak_ptr
智能指针tr1::function
可表示任何符合签名的函数和函数对象tr1::bind
绑定器tr1::unordered_set
,tr1::unordered_multiset
,tr1::unordered_map
以及tr1::unordered_multimap
哈希表- 正则表达式
- Tuples 变量组
tr1::array
支持成员函数的数组tr1::mem_fn
类成员函数指针功能tr1::referene_wrapper
让引用行为更新对象?- 随机数工具
- 数学特殊函数
- C99 兼容扩充
- Type traits 用以提供类型(types) 的编译期信息
tr1::result_of
template,用来推导函数调用的返回类型
55: 让自己熟悉 Boost
C++开发者社区
写在后面
感觉C++具有高自由度,我不得不说一下我的感触。用C#的时候,就仿佛用SRP写管线,一些底层的东西你没有参与其中(比如为物体维护光源索引),这可能也是SRP迷人的地方,轻轻松松让Unity按照你的想法渲染;用C++感觉像是直接拿着大写开头的DX API写管线,你要考虑所有细节。
阅读此书给我打开了新世界。
书中的一部分内容我还没有看懂,之后如果在使用过程中产生感触,会继续补充。
下一步我要更加熟悉C++标准库(正如54所说),下一本书不出意外是《Effective STL》