写出更有效的C++代码

写在前面

一次偶然的机会同时拿到了游戏客户端、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;

着重说明

  1. 由于常量经常定义于头文件内,因此有必要将指针(而不是指针所指之物)声明为const

    const char* const authorName = "TOSHIRON";
    
  2. 对于Class专属常量,为了确保他只有一份实体,必须使其成为static成员

    class GamePlayer
    {
    	private:
    		static const int NumTurns = 5;
    		...
    }
    
  3. 如果不想让别人获取到指向某个常量的指针,因为取const地址是合法的,所以可以用enum取代

    class GamePlayer
    {
    	private:
    		enum{ NumTurns = 5 };
    		...
    }
    
  4. 用内联函数替代宏,以获得相同的效率和功能

    #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出现在*两边,指针和所指事物都是常量

着重说明

  1. 令一个函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性
    我感觉这几乎主要是为了单独避免一种情况

    class Rational{...};
    const Rational operator* (const Rational& lhs, const Rational& rhs);
    ...
    Rational a,b,c;
    ...
    if(a*b = c){...}
    

    找错麻烦?因为常量不允许赋值所以会直接报错

  2. 利用常量性(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
    
  3. const成员函数不可以更改对象内任何non-static成员变量;解决办法就是用 mutable 关键字修饰,使变量总是可更改的,及时在const成员函数内。

  4. 在 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,有时候不会,所以手动初始化很有必要。

着重说明

  1. 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)
    { }
    
  2. 为解决两个不同编译单元内的初始化次序问题,使用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){...}
};

着重说明

  1. 当对一个“内含reference 成员”或“内含const 成员”进行赋值操作时,编译器自己生成的赋值重写无法完成此工作,需要自己专门写。
  2. 如果把赋值重写设为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: 别让异常逃离析构函数

  1. 在析构函数中用 try-catch 把错误盖住,至少要比草率结束程序要好,比如在构造函数中建立了数据库链接(?谁会干这种傻事)

    ~DBconn()
    {
    	try{db.close();}
    	catch(...)
    	{
    		记录错误日志
    	{
    }
    
  2. 对上面的方法进行改良的话,就是用一个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的时候把=左边的也删除了,主要解决办法有:

  1. 证同测试

    Widget& Widget::operator=(const Widget& rhs)
    {
    	if(this == &rhs) return *this;
    
    	delete pb;
    	pb = new Bitmap(*rhs.pb);
    	return *this;
    }
    
  2. 第二个做法是先复制一份pb,再删除之前的,但我感觉有点浪费,所以觉得好像不是一个好方法。

12: 复制对象时不要遗漏任何一个部分

如果为class添加了成员变量,那必须同时修改copying函数,以及operator=的重写。

  1. 在派生类的copying函数调用基类的构造。
  2. 在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 行为

首先思考:被复制时会发生什么?可能会重复锁定同一个资源,在析构的时候可能重复销毁同一个资源。
解决方法

  1. 禁止复制,利用条款6,把copying private掉。
  2. 使用引用计数法,让最后一个使用者销毁资源。
    class Lock
    {
    	public:
    		explicit Lock(Mutex* pm): mutexPtr(pm,unlock)   ///unlock为删除资源的函数
    		{
    			lock(mutexPtr.get());
    		}
    	private:
    		std::tr1::shared_ptr<Mutex> mutexPtr;
    }
    
  3. 复制底部资源,深度拷贝大法
  4. 当只想有一个对象加工资源时,可以利用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: 让接口容易被正确使用,不易被误用

  1. “促进正确使用”可以 功能性相似接口的一致性,以及内置类型的行为兼容。以C#的 .Length 和 .Count() 为反例
  2. “阻止误用”可以 对类型限制,建立新类型,束缚对象值。
    Date a(1998,12,28);
    //为了防止无效日期,增加Day,Month,Year类,对Int封装,做有效性限制
    Date a(Year(1998),Month(12),Day(28));
    
    但我觉得这十分麻烦,或许只是这个例子不好,比如2001.02.29这个日期,感觉还是在函数内检验比较好
  3. 利用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

  1. 新 type 的对象应该如何被创建和销毁?构造,析构,内存分配及释放
  2. 对象的初始化和对象的赋值该有什么样的差别?明确构造函数和赋值操作符的行为
  3. 新 type 被 passed by value 意味着什么? copy构造函数的实现
  4. 什么是新 type 的“合法值”?在构造函数,赋值操作上的约束
  5. 新 type 需要配合某个继承图系吗?注意他们的virtual函数
  6. 新 type 需要什么样的转换? 显示、隐式转化 参考15
  7. 新 type 允许那些操作符和函数?
  8. 什么标准函数需要驳回? 参考6
  9. 新 type 成员的作用域
  10. 什么是新 type 的“未声明接口”? 参考29 对效率。异常安全性及资源运用提供保证
  11. 如果需要一个types家族,那应该定义一个 class template
  12. 有没有必要定义一个新的 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

  1. 将成员变量声明为 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;
    }
    
  2. protected 并不比 public 更具封装性。

23: 宁以 non-nember、non-friend 替换 member 函数

好吧,C#过来的感到震惊

class WebBrowser
{
	public:
	...
	void clearCache();
	void clearHistory();
	void removeCookies();
	...
};

现在想要做一个同时调用 WebBrowser里三个清理函数的函数,下面两种做法那种好

  1. 在类里提供
    class WebBrowser
    {
    	public:
    	...
    	void clearEverthing();
    	...
    };
    
  2. 在同空间名里额外做一个函数
    void clearBrowser(WebBrowser& wb);
    

居然是2好,因为更具有封装性。它的逻辑大概是这样
首先,成员变量声明为 private 就意味着有更少的函数能访问它,如果不是 private 那么就有很多函数可以访问它,它就不具有封装性。那么,越少函数能访问 private,那封装性越好。所以2好。

24: 若所有参数皆需类型转换,请为此采用 non-member 函数

令 classes 支持隐式类型转换通常是个糟糕的主意。
请记住:如果需要为某个函数的所有参数进行类型转换,那么这个函数必须是个 non-member.

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

  1. 交换两个对象真正要做的是交换它们的指针
  2. 当 std::swap 对你的类型效率不高时,提供一个 swap 成员函数,并确定这个函数不抛出异常
  3. 如果你提出一个 member swap,也应该提供一个 non-member swap 来调用前者
  4. 调用 swap 时应针对 std::swap 使用 using 声明式,然后调用 swap 并且不带任何 “命名空间资格修饰”
  5. 为“用户自定义类型”进行 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 才行)

注意

  1. c++ 中一个对象可以拥有一个以上的地址(如以派生类指向它 和 以基类指向它)
  2. 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();
    	}
    }
    
    所以还是尽量在基类里提供虚函数比较好
  3. 尽可能的使用新式转换

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#玩家的震惊

  • 程序库头文件应该以 “完全且仅有声明式”的形式存在。

依赖于声明式,不依赖于定义式的两个手段

  1. 利用 Interface class,解除接口和实现之间的耦合关系,从而降低编译依赖性,在interface class 内声明 static 处理函数,和下面有点类似。
  2. 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” 的两个组合。

情况说明

  1. 钻石型多继承导致歧义性
    class File{...};
    class InputFile: public File{...};
    class OutputFile: public File{...};
    class IOFile: public InputFile, public OutputFile{...};
    
    如果File Class有个成员变量 fileName,在IOFile中调用 fileName,就会出现歧义性。
  2. 使用 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); //报错
		}
		...
};

解决办法

  1. 在 base class 函数调用动作之前加上“this->”

    template<typename Company>
    class LoggingMsgSender: public MsgSender<Company>
    {
    	public:
    		...
    		void sendClearMsg(const MsgInfo& info)
    		{
    			this->sendClear(info); 
    		}
    		...
    };
    
  2. 使用 using 声明式

    template<typename Company>
    class LoggingMsgSender: public MsgSender<Company>
    {
    	public:
    		using MsgSender<Company>::sendClear; //告诉编译器,假设sendClear 位于 base class
    		...
    		void sendClearMsg(const MsgInfo& info)
    		{
    			sendClear(info); 
    		}
    		...
    };
    
  3. 明白支持被调用的函数位于 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_ptrtr1::weak_ptr 智能指针
  • tr1::function 可表示任何符合签名的函数和函数对象
  • tr1::bind 绑定器
  • tr1::unordered_settr1::unordered_multisettr1::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》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章