C++ | 一文整理Effective C++ 55条款内容(全)

前言

文章目录

1. 视C++为一个语言联邦

C++是一个支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言;

C++的主要四个次语言:

  • C语言
  • OJ C++
  • Template C++
  • STL

2. 尽量使用const、enum、inline替换#define

使用#define预定义可能出现的状况及缺陷:

  • 可能被未被编译器识别
  • 可能在处理源码时被移除
  • 也可能被#undef解除;
  • 替换目标时,可能出现多份
  • 不重视作用域,没有封装性
  • 不能进行取地址

const使用:

  • 在头文件中使用常量的字符串则需要时使用两次const
  • 类内使用常量,未了确保常量至多只有一份,则需加上static

enum使用

  • 不能够被获取地址,当不想要提供内存时,阔以使用;

建议

  • 对于常量,最好用enumsconst替换#define
  • 对于,最好使用inline函数;

3.尽可能使用const

const可修饰各个作用域中常量:

  • 文件;
  • 函数;
  • 区块作用域;

const语法:

  • const*左边时,被指物是常量,即不能修改值;
  • const*右边时,指针自身时常量,即不能修改其指向;
  • *两边都有const,则即不能修改值,也不能修改指向;
// 情况一:
int i=0;
const int *p = &i;
*p = 20;// 情况二:
int i=0;
int j=1;
int * const p = &i;
p = &j;// 情况三:
int i=0;
int j=1;
const int * const p = &i;
p = &j;*p = 2;

const的作用:

  • 修改函数返回值参数,提高数据的安全性高效性

mutable的作用:

  • 当成员变量加上该关键字时,即可在const成员函数内进行修改;

在这里插入图片描述

<const_cast>的作用:

可将const转化为not-const,将non-const转化为const

const成员变量

在类中的const成员函数至多只有一份实体,必须让它成为static成员;

class A{
private:
	static const int a = 5;
}

const使用在成员函数

改善程序的效率阔以通过pass by reference to const

void pprint(const context& ctx){
	// .....
}

建议

  • 声明为const可帮助编译器检测处一些错误;
  • constnon-const不会造成代码重复;
  • const成员函数不能调用非const

在这里插入图片描述

4. 确定对象被使用前已先被初始化

4.1 成员对象初始化

对象的初始化是交给构造函数处理的;

  • 其中构造函数可使用参数列表进行初始化,发生于默认构造函数被自动调用之时;
  • 且C++的成员初始化次序是相同的,按其声明的顺序被初始化;
class test{
private:
    int m_id;
    int m_num;
    float m_score;
    
public:
    test(int id, int num, float score) : m_id(id), m_num(num), m_score(score) {}; 
};

int main() {

    return 0;
}

在这里插入图片描述
注意

若出现遗漏的成员变量,可将其改用赋值操作封装在单独的函数内部,供其他函数调用;

建议:

  • 内置型对象进行手工初始化;
  • 构造函数最好使用成员初始化列表,效率会略微提高,且次序需和声明时一致;
  • 为免除跨编译单元的问题,使用local-static对象替换non-local-static对象;

4.2 如何保证定义于不同的编译单元内的对象先被初始化

将需要先初始化的对象封装在专属的函数内部,切将其声明为static,并返回一个reference;

class A{};

A& getA() {
	static A a;
	return a;
}

当处于多线程时,该方法并不安全,需要在程序的单线程启动阶段手动调用所有reference-returning函数,以此来消除初始化有关的竞速形势;

5. 了解C++默默编写并调用那些函数

当创建一个对象时,不做任何编写时,编译器会为我们提供构造函数、拷贝构造函数、copy assignment操作符以及一个析构函数;

  • 但当你创建一个构造函数,编译器则不会继续为我们提供默认构造函数;

6. 若不想使用编译器自动生成的函数,就改明确拒绝

首先想到的做法就是将函数置于private中,但该做法有缺陷:

  • 成员函数或者友元函数还是可以对其进行调用;

以上的方法行不通,但是我们知道private的特性,故可以使用private继承来拒绝编译器生成的函数:

class Uncopyable {
public:
	Uncopyable() = default;
	~Uncopyable() = default;

	Uncopyable(const Uncopyable&) = delete;
	Uncopyable& operator=(const Uncopyable&) = delete;
};

class test : Uncopyable{
public:
	// .....
};

7. 多态基类声明virtual析构函数

当子类继承的基类中没有定义虚函数,再释放的过程中,子类的成分没有被销毁;

出现上述情况,则需要将base class的析构函数前添加关键词virtual

  • 且当在其他成员函数中加上该virtual,使用多态调用成员函数时,若子类与父类重复,则会调用子类的成员函数;
#include<iostream>
using namespace std;


/*
	当没有在spreak前加上virtual时函数在编译阶段即确定好了地址;
	若要调用猫的,则在该函数前加上virtual。
*/
class  Animal
{
public:
	virtual void speak(){
		cout << "我是动物" << endl;
	}
private:

};

class Cat :public Animal
{
public:
	void speak(){
		cout << "我是小猫" << endl;
	}
private:

};

void doSpeak(Animal & animal){
	animal.speak();
}


void test(){
	Cat cat;
	doSpeak(cat);
}


int main(){

	test();

	system("pause");
	return 0;
}

virtual分析:

  • 该关键词的作用主要让函数在位于运行期被调用,该对象必须携带某些信息,该信息由vptr指针指出,vptr指向一个由函数指针构成的数组为vtbl
  • 当该函数被调用时,则实际被调用的函数取决于vtbl中的函数指针
  • 当使用该关键字时,其对象的体积增加
  • 禁止(多态性质)继承没有虚析构函数的类;

建议

  • 多态性质的基类应该声明一个virtual析构函数,若class带由任何的virtual函数,则应该声明一个virtual析构函数;
  • 若基类中的析构函数声明为纯虚函数,则该对象不能被实例化,且需要将该析构函数提供一份定义;

8. 别让异常逃离析构函数

在C++中,不鼓励在析构函数中吐出异常

  • 由于析构函数正常是用来释放内存的,若在释放内存的同时出现了异常,则后面将会导致内存泄漏

针对于上述问题有一个较佳的策略重新设计接口

  • 提供一个close函数对原析构函数内容进行实现,在将该接口在析构中调用;
  • 这样处理,可以将异常排除在析构函数之外,否则将会给程序带来过早结束或者发生不明确行为

自定义一个函数,为可能出现的问题作出反应

class DBConn{
public:
	void close(){
		db.close();
		closed = true;
	}

	~DBConn() {
		if (!closed) {
			try{
				close();
			}
			catch{
				//退出程序或记录
			}
		}
	}

private:
	DBConnection db;
	bool closed;
};

建议:

  • 析构函数中绝对不要吐出异常,析构函数应该捕捉异常,将异常吞下或者结束程序
  • 若该class在运行期间抛出异常,则应该提供一个普通函数,来执行该操作;

9. 绝不在构造析构过程中调用virtual函数

若基类中将纯虚函数在构造函数中调用,则会产生错误;

  • 由于纯虚函数需要在子类中实现,而基类被继承,当子类创建对象时,就先调用基类构造,此时的纯虚函数还没来得及初始化

若基类中需要调用到纯虚函数该如何改进呢??

将该纯虚函数该为非虚函数,将子类要传达的信息传递给基类的构造函数;

class Transaction{
public:
	explicit Transaction(const string&);
	void LogTransaction(const string&) const;
};

Transaction::Transaction(const string& info){
	LogTransaction(info);
}

void Transaction::LogTransaction(const string& info) const {
	cout << info << endl;
}

class BuyTransaction : Transaction {
public:
	// 通过初始化列表的形式将信息到基类中
	BuyTransaction(string info) : Transaction(info) {
	}
};

建议:

  • 构造析构期间不要调用virtual函数,由于该类调用从不下降至derived class

10. operator=返回reference to *this

可实现将赋值语句写成连锁形式;

class base{
public:
    base(int a): num(a){};

    base& operator+=(const base& p){
        this->num += p.num;

        return *this;
    }

    int getNum(){ return num;  }

private:
    int num = 0;
};

int main() {
    base b(2);
    base c(3);
    base e(4);
    b += c += e;	

    std::cout << b.getNum() << std::endl;	// 9

    return 0;
}

建议:

  • 当重载操作运算符时,返回一个reference to *this

11. operator=处理自我赋值

当该使用该运算符时,可能遇到的问题时,传入的对象时同一个:

  • 则此时需要我们对该函数进行优化,在代码起始位置添加一个证同测试,判断是否是自我赋值;
1、判断是否为自我赋值
2、重新分配空间且释放原内存;

第一版

在开头直接检测是否同一对象;

class Widget { 
public:
	Widget& Widget::operator=(const Widget& rhs) {
		// 检测是否是自我赋值
		if (this == &rhs) return *this;

		delete m_pd;
		m_pd = new char[20];
		return *this;
	}

private:
	char* m_pd;
}

第二版

通过副本保存,在删除;

class Widget { 
public:
	Widget& Widget::operator=(const Widget& rhs) {
		char *tmp = m_pd;
		m_pd = new char[20];
		delete tmp;
		return *this;
	}

private:
	char* m_pd;
}

第三版

使用交换函数;

class Widget { 
public:
	void swap(Widget& rhs);

	Widget& Widget::operator=(const Widget& rhs) {
		Widget tmp(rhs);	// 产生副本
		swap(tmp);			// 将*this与tmp交换; 
		return *this;
	}

private:
	char* m_pd;
}

建议:

  • 确保operator=有良好行为:来源对象目标对象的地址语句顺序复制交换
  • 确定任何函数中如果一个以上的对象,而其中多个对象是同一个对象时不会出现任何问题;

12. 复制对象勿忘其每一个成分

一个设计良好的类内部只留两个函数负责对对象的拷贝

  • 拷贝构造函数;
  • copy assignment操作符;

如果自行声明coping函数,可能导致一下错误:

  • 在类中新增成员变量,若不在自定义coping函数中添加则会导致缺漏而编译器不给予警告;
  • 当使用于继承中,子类中的coping函数不会将父类的成员变量复制;

如何做到拷贝过程不讲新增成员遗漏呢

在子类的赋值操作中或拷贝构造中调用父类的方法;

class A {
	A(int a) :m_a(a) {}
	virtual~A(){}

	A& operator=(const A& rhs) {
		m_a = rhs.m_a;
		return *this;
	}

private:
	int m_a;
};

class B : public A{
	B() {}
	~B(){}
	
	B& operator=(const B& rhs) {
		A::operator=(rhs);
		m_b = rhs.m_b;
		return *this;

	}
	int m_b;
};

copy函数中不合理的做法:

  • copy assignment操作符copy构造函数中,两者不能相互调用;

建议:

  • copying函数应该确保复制对象内的所有成员变量所有base class成分;
  • 不要尝试以某个copying函数实现另一个copying函数,应该将共同机能放进第三个函数中,并由两个coping函数共同调用;

13. 以对象管理资源

为了确保对象返回的资源总是被释放:

  • 把资源放进对象内,我们便可依赖C++的析构函数自动调用机制确保资源被释放;
  • 许多资源被动态分配于heap中内置于单一区块或函数内,应在控制流离开时被释放;
  • 故标准程序库提供智能指针,可利用其析构函数自动让该对象调用delete

auto_ptr

  • 被销毁时,自动删除所指之物;
  • 不能让多个auto_ptr指向同一个对象,使用拷贝构造赋值操作符会变成nullptr;

shared_ptr

  • 获得资源后立刻放进管理对象;
  • 可以指向同一个对象;
  • 一般不用于数组;
    • 管理对象运用析构函数确保资源被释放

建议:

  • 为了防止资源泄漏,请使用资源取得的时机便初始化时机对象,它们在构造函数中获得资源并在析构函数中释放资源;
  • 一般使用tr1::shared_ptrauto_ptr前者较佳,后者复制动作会使它指向null

14. 资源管理类中小心coping行为

当一个RAII(资源取得时机即初始化时机)对象被复制时,一般选择以下可能:

  • 禁止复制;
    • 当该动作对对象不合理时,应禁止(使用uncopyable);
  • 对底层资源祭出引用计数法
    • 为了保证最后一个对象被销毁,在复制时,应递增引用计数;
  • 复制底部资源
    • 当复制底部资源时,需要使用深度拷贝,可避免该资源在释放的时候出现问题;
  • 转移底部资源的拥有权;
/**
1、继承uncopyable禁止copy
2、使用shared_ptr删除器
3、不需要声明虚构函数
*/
class Lock : private Uncopyable {
public:
	explicit Lock(Mutex* mutex) : m_muetx(mutex) {
		lock(m_muetx.get());
	}

private:
	std::tr1::shared_ptr<Mutex> m_mutex;
};

建议:

  • 复制RAII对象必须一并复制它所管理的资源,故资源的coping行为决定RAII对象的coping行为;
  • 普遍的RAII对象的复制行为:抑制copying施行引用计数法

15. 在资源管理类中提供对原始资源的访问

对原始资源的访问可能经由显式转换或隐式转换,一般而言显式转换比较安全,但隐式转换对客户较方便

显示转换

class Font{
public:
	FontHandle get() const { return f; }
};

隐式转换

class Font{
public:
	operator FontHandle() const { return f; }
};

16. 承兑使用new和delete是要采取相同形式

当使用new时会发生两件事:

  • 内存被分配;
  • 构造函数被调用;

当使用delete时也会发生两件事:

  • 析构函数被调用;
  • delete函数被调用;

new一个数组于单一对象的区别:

  • 由于单一对象的内存布局于数组的内存布局不同,故释放的用法也不同;
  • 数组使用delete []

建议:

  • 若在new中使用[],则在delete中也需要使用[]

17. 独立语句将newed对象植入智能指针

使用new返回的指针来初始化智能指针;

  • 一般初始化智能指针的指针必须指向动态内存;
  • 由于之智能指针内部的构造函数是explicit,不能进行隐式转换,必须使用直接初始化
  • 由于不清楚对象何时销毁,则最好不使用内置指针来访问一个智能指针且不使用get()初始化或赋值;
shared_ptr<T> p(new T(1));

当new与share_ptr作为参数时

// 函数声明
int func();
void test(std::shared_ptr<int> p, int func);

由于智能指针内部的构造函数是explicit,故不能直接传入new创建的指针;

// 可考虑使用以下:
test(std::shared_ptr<int> (new int), func());

第一个参数被分为俩部分:std::shared_ptr<int>new int
由于编译器考虑高效的操作,故传入参数的执行顺序:

  • 执行new int
  • 调用func
  • 调用智能指针构造

但需要考虑到,万一func函数内部出现异常,则此时将会引起内存泄漏,由于new int返回的指针遗失;


故将实参一先单独出来:

std::shared_ptr<int> p(new int);
test(p, func());

18. 接口正确使用,不易被误用

开发一个接口,首先需要考虑客户可能做出什么错误;

  • 限制类型内什么事可做,什么事不能做,常见限制加上const

建议:

  • 好的接口很容易被正确使用,应在所有接口中努力达成这些性质;
  • 促进正确使用的办法包括接口的一致性,以及内置类型的行为兼容;
  • 阻止误用的办法有:
    • 新类型;
    • 限制类型上的操作;
    • 束缚对象值;
    • 消除客户的资源管理责任;
  • tr1::shared_ptr支持定制型删除器。可防范DLL问题,可被用来自动解除互斥锁

19. 设计class犹如设计type

设计规范:

新type的对象应该如何被创建和销毁:

  • 会影响到你的class的构造函数和析构函数以及内存分配函数和释放函数;

对象初始化和对象的赋值该有什么样的差别:

  • 决定你的构造函数和赋值操作符的行为,以及其间的差异;

新type的对象如果被以值传递,意味着什么:

  • 拷贝构造函数用来定义一个type的值传递该如何实现;

什么是新type的合法值:

  • 对class成员变量而言,通常只有某些数值集是有效的;会决定成员函数中的错误检查;

你的新type需要配合某个继承图系嘛:

  • 若你继承自某些既有的classes,就受到那些classes的设计的束缚,尤其是析构函数;

你的新type需要什么样的转换:

  • 若只允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不得为类型转换操作符;

什么样的操作符和函数对此新type而言是合理的:

  • 决定你将为你的clas声明哪些函数;

20. 宁以pass-by-reference-to-const传递替换pass-by-value

值传递将会拷贝一份原原本本的数据,需要消耗较多的时间和`空间;

当一个对象通过传值的方式传递,传入的时候会调用一次拷贝构造,且当该值返回时又会调用析构

使用pass-by-reference-to-const好处

  • const保证数据传入的安全
  • 使用reference,在多态的情况下不会导致对象被切割

建议:

  • 尽量以pass-by-reference-to-const替换pass-by-value。前者较高效,且避免切割问题;

21. 必须返回对象时,别返回reference

在我们上一条例子中了解到了pass-by-value的效率低于reference,故在追求的效率的同时可能会传递一些reference指向并不存在的对象;

需要注意以下几点:

  • 在函数内创建stack空间的对象,此时不能用reference返回,应该离开作用域即释放;
  • 或者有人认为,可以用new出来一个对象,但是有多出了一个delete的问题;当你返回该对象,又该如何释放?

正确的做法

当该函数必须返回一个对象时,就让函数返回一个新对象;

  • return 对象;
  • 该函数得承受该对象的构造和析构;
const Rational operator*(const Rational& lhs, const Rational& rhs){
	return Rational(lhs.n * rhs.n, lhs.d * lhs.d);
}

建议:

不要返回指针或引用指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回指针或引用指向一个local static对象而有可能同时需要多个这样的对象;

22. 成员变量声明为private

成员变量声明为private

如果将成员变量声明为private则唯一能够访问对象的方法就是通过成员函数

  • 使用函数可以让你对成员变量的处理又更精确的控制;
  • 便于日后可改以某个计算替换这个成员变量
  • 且该函数是一个inline函数;
  • 可为所有可能实现的提供弹性

成员变量声明为public

  • 意味着不封装
  • 不封装意味着不可改变;
  • 某些东西的封装性与当其内容改变时可能改变造成的代码破坏量成反比

成员变量声明为protected

  • 当我们取消了其中的成员变量;
  • 它所使用derived classes都会被破坏;

建议:

  • 切记将成员变量声明为private,可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性;
  • protected不比public更具封装性

23. 宁以non-member,non-friend替换member函数

可直观得看出member函数比non-member函数的封装性低,提供non-member函数对类相关机能会有较大的包裹弹性;

  • 当一个东西被封装得越少人看得到,则我们就有越大的弹性去变化它;
  • 封装使我们能够改变事物而只影响有限客户;
  • non-member函数不可以是另一个class的member

提供便利函数:

将所有便利函数放在多个头文件内但隶属于同一个命名空间,可轻松扩展及提供使用;

  • 可以在该命名空间下添加更多的non-membernon-friend函数;

建议:

  • 宁可拿non-membernon-friend函数替换member函数,可增加封装性包裹弹性机能扩充性

24. 所有参数皆需类型转换,采用non-member

若需要用到类型转换,则定义为非成员函数即可;

class Rational{
public:
	Rational(int x, int y) : n(x), d(y){}

	int n, d;
};

const Rational operator*(const Rational& lhs
	, const Rational& rhs){
	return Rational(lhs.n * rhs.n, lhs.d * lhs.d);
}

建议:

如果你需要为某个函数的所有参数进行类型转换,则该函数必须是一个non-member

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

swap是异常安全性编程的脊柱,以及用来处理自我赋值的可能性的常见机制;

当swap缺省版的效率不足:

  • 提供一个public swap函数,提高效率,且该函数不该抛出异常;
  • 在该命名空间中提供一个non-member swap函数,且调用swap成员函数;
  • 若调用swap,则记得包含using声明式;

建议:

  • 当std::swap对你的类型效率不高时,需提供一个swap成员函数,且该函数不能抛出异常;
  • 若你提供一个member swap,也该提供一个non-member swap来调用member swap ,对于classes,需特化std::using

26. 尽可能延后变量定义式的出现时间

最好延后定义式,且在确定需要它在定义;

  • 可以避免构造非必要对象,还可以避免无意义的默认构造行为;
string encryptPassword(const string& password) {
	
	//string encrypted;
	//encrypted = password;

	// 修改,直接使用构造初始,避免无意义的默认构造
	string encrypted(password);

	encrypt(encrypted);

	return encrypted;
}

建议:

  • 尽可能延后变量定义式,可增加程序的清晰度改善程序效率

27. 尽量少做转型动作

C++规则的设计目标之一式要保证类型错误绝不可能发生,即不在任何对象上执行不安全的操作;

C++不建议,但提供四种方式:

const_cast

  • 用来将对象的常量性转除

dynamic_cast

  • 向下转型,决定某对象是否归属继承体系中的某个类型;速度,应尽量避免使用;
  • 不能用于缺乏虚函数的类型;

reinterpret_cast

  • 执行低级转型,如pointer to int转成int
  • 转换结果都是执行期定义,代码移植性差;

static_cast

  • 强迫隐式转换,如non const转成constint转为double

建议:

  • 尽量避免转型,特别在注重效率的代码中避免使用dynamic_cast
  • 若转型是必要的,则需要将它隐藏于某个函数后面;

28. 避免返回handles指向对象内部成分

class A{
public:
	A();
	~A();

	string& getName() const { return m_name; }
private:
	string m_name;
};

如上述所示,通过reference将私有的数据成员返回,有可能会降低数据的安全性,到时候外部通过reference修改数据;

改进

class A{
public:
	A();
	~A();

	const string& getName() const { return m_name; }
private:
	string m_name;
};

建议:

  • 避免返回handles指向对象内部

29. 为异常安全而努力是值得的

当代码中的异常被抛出,带有异常安全性的函数

  • 不泄漏任何资源;
  • 不允许数据败坏;
  • 对象或数据结构不被破坏;
  • 程序状态不改变;
  • 绝不抛出异常;

建议:

  • 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏(基本型强烈型不抛异常型);

30. 透彻了解inlining的里里外外

内联函数动作像函数,比宏好得多,调用它不需消耗像函数一样的开销;

  • inline只是对编译器的申请,不是强制命令,编译器会根据函数的实际功能做出判断是否为内联函数;
  • 该函数一般被置于头文件中;
  • 但inline不能随程序库的升级而升级,若修改则都必须重新编译;

建议:

  • 将大多数inline限制在小型、被频繁调用的函数。便于日后调试二进制升级,提升程序速度;
  • 不要只因为function templates出现在头文件,就将它们声明为inline

31. 文件间的编译依存关系将至最低

class Person
{
public:
	Person(const string& name, const Date& day, const Address& addr);
	string getName() const;
	string getday() const;
	string getaddr() const;

private:
	string m_name;
	Date m_day;
	Address m_addr;
};

当编译器编译的时候,需要获取string、Date、Address的定义式,才能知道该对象的大小,好让编译器分配多少内存;

上述代码中,该类与include的文件形成一种编译依赖的关系,一旦这些依赖头文件有所改变,则任何使用该Person类的文件也必须重新编译

  • 一旦在开发过程中,头文件的数量众多,将会浪费大量的时间;

使用指针来确定编译器间对象的大小

由于在编译期间需要确定对象的大小,故使用指针来指向该对象,即编译器不用对该对象一探究竟,知道该指针的大小

使用智能指针指向实现物,从而将接口与实现分离,故在类种做修改不需要使用时重新编译;

class Person
{
public:
	Person(const string& name, const Date& day, const Address& addr);
	string getName() const;
	string getday() const;
	string getaddr() const;

private:
	std::shared_ptr<PersonImpl> pImpl;	
};

设计策略:

  • 如果使用object referencesobject pointers可以完成任务,就不要使用object;
  • 如果能够,尽量以class声明式替换class定义式
    • 当声明函数需要使用某个类时,只需将其声明即可;
  • 为声明式和定义式提供不同的头文件,让接口与实现分离,实现修改不需要客户重新编译;

Handle class

让Person成员函数调用pImpl的函数,接触实现与接口之间的耦合关系;

  • 该举措不会改变Person做到事,指挥改变其做事的方法
#include "Person.h"
#include "PersonImpl.h"

Person::Person(const string& name, const Date& day
	, const Address& addr)
	: pImpl(new PersonImpl(name, day, addr)) {}

string Person::getName() const {
	return pImpl->getName();
}

Interface class

通过将Person变成抽象基类,为子类提供接口,只有在Person类接口被修改才需要重新编译;

class Person
{
public:
	virtual ~Person();
	virtual string getName() const = 0;
	virtual string getday() const = 0;
	virtual string getaddr() const = 0;
};

class RealPerson : Person {
public:
	RealPerson(const string& name, const Date& day, const Address& addr) 
		: m_name(name), m_day(day), m_addr(addr){}
	virtual ~RealPerson();
	string getName() const;
	string getday() const;
	string getaddr() const;

private:
	string m_name;
	Date m_day;
	Address m_addr;
};

建议:

  • 支持编译依存性最小化的一般构想:相依于声明式,不要相依于定义式。基于此构想的两个手段是handleinterface classes
  • 程序库头文件应该以完全且仅有声明式的形式存在;

32. 确定你的public继承塑模出is-a关系

在C++中public inheritance意味着is-a的关系;

  • 使用public继承,可以使用base class类型接收derived class对象;
  • 好的接口可以防止无效代码通过编译,需要在编译期拒绝发生错误的设计;

建议:

  • public继承意味着is-a,适用于base-classes身上的一定适用于derived classes身上;

33.避免遮掩继承而来的名称

内层作用域的名称会遮掩外围作用域的名称,仅名称,与数据类型函数参数无关;

  • 编译器会查找作用域,由内到外

如何避免函数被遮掩

class Base {
public:
	virtual void func1() = 0;
	virtual void func1(int);
	void func2();
	void func2(double);
	virtual ~Base() {}
};

class sun : public Base {
public:
	//1、当在函数中使用using即可解除遮掩
	using Base::func1;
	using Base::func2;

	virtual void func1();	// sun::func1遮掩Base::func1(int)
	void func2();	// sun::func2遮掩了Base::func2(double)
	virtual ~sun() {}
};

建议:

  • derived classes内的名称会遮掩base classes内的名称;
  • 为了避免被遮掩,可使用using声明或转交函数(前面添加virtual);

34. 区分接口继承和实现继承

当使用public继承时:

  • 成员函数的接口总是会被继承的;
    • 纯虚函数虚被继承的重新声明,且在base class中没有定义,为了让子类只继承接口;
    • 虚函数是为了让子类继承该函数的接口和缺省实现,必须继承,若不想写,则使用父类的;
    • non-virtual函数是为了令derived classes继承函数的接口及一份强制性实现;

三种函数之间的差别:

  • pure virtual函数:只继承接口
  • virtual函数:继承接口和一份缺省实现
  • non-virtual函数:继承接口和一份强制实现

建议:

  • 接口继承实现继承不同;

35. 考虑virtual函数以外的其他选择

non-virtual interface实现teplate method模式:

让用户通过public non-virtual成员函数直接调用private virtual函数;

  • non-virtual函数为virtual的外覆器
  • 该外覆器确保在调用virtual之前将设定好事前工作【加锁,验证等】,在结束后做清理工作【解锁,解除约束等】;
class GameChar {
public:
	int getVal() const {

		// ....
		int ret = _getVal();

		// ...
		return ret;
	}

private:
	virtual int _getVal() const {
		// ....
	}
};

使用Function Pointer实现策略模式

class GameChar;

int defaultHeadlthCalc(const GameChar& gc);
class GameChar {
public:
	
	typedef int(*HCalcFunc)(const GameChar&);

	explicit GameChar(HCalcFunc hcf = defaultHeadlthCalc)
		: hFunc(hcf) {}

	int getVal() const {
		return hFunc(*this);
	}
private:
	HCalcFunc hFunc;
};

// 派生类
class EvilBadGuy : public GameChar {
public:
	explicit EvilBadGuy(HCalcFunc hcf = defaultHeadlthCalc)
		: GameChar(hcf){ }
};

// 即刻自定义,创建对象时通过构造函数传入
int myHfunc(const GameChar&);

EvilBadGuy eb(myHfunc);

std::function替代function Pointer

当使用std::function即可保存任何函数类型

class GameChar;

int defaultHeadlthCalc(const GameChar& gc);
class GameChar {
public:
	
	typedef std::function<int(const GameChar&)> HCalcFunc;

	explicit GameChar(HCalcFunc hcf = defaultHeadlthCalc)
		: hFunc(hcf) {}

	int getVal() const {
		return hFunc(*this);
	}
private:
	HCalcFunc hFunc;
};

36. 不重新定义继承而来的non-virtual函数

virtual函数是动态绑定,而non-virtual函数是静态绑定;

  • 当重定义non-virtual函数时,使用多态则对象调用的始终是base class的函数;
  • 故在任何情况下都不该重新定义一个继承而来的non-vitrual函数;

建议:

  • 绝不要重新定义继承而来的non-virtual函数;

37. 不重定义继承而来的缺省参数值

虽然由上一点能得出virtual函数是动态绑定,但其缺省参数值静态绑定

  • virtual函数是动态绑定,调用一个virtua;函数时,则该调用那一份代码,却决于发出调用的那个对象的动态类型;
  • 以上的动态类型指的是目前所指对象的类型,若指向其他类型则为静态类型
#include <iostream>

class Shape{
public:
    enum ShapeColor { R, G, B };

    virtual void draw(ShapeColor color = R) const = 0;
};

class test1 : public Shape{
public:
    virtual void draw(ShapeColor color = G) const;
};

class test2 : public Shape{
public:
    virtual void draw(ShapeColor color) const;
};

void test1::draw(ShapeColor color) const {
    std::cout << color << std::endl;
}

void test2::draw(ShapeColor color) const {
    std::cout << color << std::endl;
}

void test(){
    Shape *ps;
    Shape *pc = new test1;
    Shape *pr = new test2;

    test1 *t = (new test1);
    t->draw();      // 1

    pc->draw();     // 0
}

int main() {

    test();
    return 0;
}

根据以上的例子,在子类中重新给出了默认参数

  • 当左边的类型为当前类(动态类型)时,默认参数生效;
  • 当左边的类型为父类(静态类型)时,子类中的默认参数无效;
    由于运行期效率,若缺省参数值时动态绑定,则编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值

建议:

  • 不要重新定义一个继承而来的缺省参数值,因为缺省参数都是静态绑定,而virtual函数你唯一应该覆写的东西是动态绑定;

38. 通过复合塑模出has-a或根据某物实现出

建议:

-复合的意义和public继承完成不同;

  • 在应用域,复合意味has-a。在实现域复合意味着is-implemented-in-terms-of

39. 明智而审慎的使用private继承

当子类以private继承时编译器不会自动将一个子类转化为基类,基类的成员到子类中都将是private属性

尽可能使用复合,必要时在使用private

  • 当牵扯到protected成员和virtual函数时;
  • 空间方面的利害关系;

为了实现某个功能而继承,但避免不然接口误用

有一个定时器类,当一个类需要用到改类的virtual此时又要避免用户使用时,调用到Timer的接口,故将使用private继承;

class Timer {
public:
	explicit Timer(int t);
	virtual void onTick() const;
};

改进,使用复合设计,且将该类在编译的依存性降至最低

class Timer {
public:
	explicit Timer(int t);
	virtual void onTick() const;
};

class WidgetTimer : public Timer {
	virtual void onTick() const;
};

class Widget {

	WidgetTimer* timer;
};

空白基类最优化

使用private继承可以节约内存空间。当面临空基类情况时,用private可以实现空白基类最优化(EBO),节约了空间;

建议:

  • private继承意味is-implemented-in-terms of,通常比复合的级别低。适用于子类需要访问protected父类的成员。或需要重新定义继承而来的virtual函数;
  • 和复合不同,private继承可造成empty base最优化;

40. 明智而审慎地使用多重继承

当使用多重继承时,程序可能会造成一些歧义

最为显著的例子:菱形继承

在这里插入图片描述

  • virtual base的初始化责任时继承体系中最底层的class负责:
  • 尽量少在base class内放置数据;

虚继承如何解决该继承问题:

解决菱形方案,操作的是共享的一份数据。

  • vbptr 虚基类指针;
  • 指向一张虚基类表;
  • 通过表找到偏移量;
  • 找到共有的变量。

建议:

  • 多重继承比单一继承复杂,它可能导致新的歧义性,以及对vrtual继承的需要;
  • virtual继承会增加体积大小、速度、初始化等成本,若virtual base classes不带任何数据,将最具实用价值;
  • 多重继承的确有正当用途,可用于:
    • public继承某个Interface classprivate继承某个协助实现的class;

41. 了解隐式接口和编译期多态

编译期多态:函数调用造成template具现化,以不同的模板参数具现函数模板从而调用不同的函数;

建议:

  • classestemplates都支持接口和多态;
  • classes而言接口是显式的,以函数签名为中心,多态则是通过virtual函数发生于运行期;
  • 对于template参数,接口时隐式的,多态是通过template具现化函数重载解析发生于编译期;

42. typename双重意义

class和typename有什么不同:

  • 两者的意义完全相同;
  • 但有些情况需要使用typename:当你想要载template中指涉一个嵌套从属类型名称迭代器),就必须在紧临它的前一个位置放上关键字;
template<typename C>
void test(const C& container, typename C::iterator iter) {// 嵌套从属类型名称
	if (container.size() >= 2) {
		typename C::const_iterator iter(container.begin()); // 嵌套从属类型名称
	}
}
  • typename不可以出现在base classes list内的嵌套从属类型名称前,也不可以在member initlization list中作为base class修饰符;
template<typename T>
class sun : public Base<T>::Nested { // base classes list
public:
	explicit sun(int x) : Base<T>Nested(x) { // member initlization list

	}
};

可使用typedef为使用typename的嵌套从属类型名称起别名;

建议:

  • 声明template参数时,前缀关键字class和typename可互换;
  • 请使用关键字typename标识嵌套从属类型名称;但不可以出现在base classes list内的嵌套从属类型名称前,也不可以在member initlization list中作为base class修饰符;

43. 学习处理模板化基类内的名称

class CompanyA {
public:
	void sendClearTxt(const string& msg);
	void sendEncryted(const string& msg);
};

class CompanyB {
public:
	void sendClearTxt(const string& msg);
	void sendEncryted(const string& msg);
};


template<typename Company> 
class MsgSender {
public:
	void sendClear(string msg) {
		Company c;
		c.sendClearTxt(msg);
	}
};

// 编译器不明确自己要继承的基类,故LoggingMsgSend无法被具现化
// 由于基类模板可能被特化且和一般版本提供的接口不同
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
	void sendClearMsg(const string& msg) {
		sendClear(msg);
	}
};

当继承基类时避免不进入templatized base classes观察行为失效时:

  • base class函数调用动作之前加上this->
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
	void sendClearMsg(const string& msg) {
		this->sendClear(msg);
	}
};
  • 使用using声明式;
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
	using MsgSender<Company>::sendClear;
	void sendClearMsg(const string& msg) {
		sendClear(msg);
	}
};
  • 明白指出被调用的函数位于base class内;
template<typename Commany>
class LoggingMsgSend : public MsgSender<Company> {
	void sendClearMsg(const string& msg) {
		MsgSender<Company>::sendClear(msg);
	}
};

建议:

  • 可在derived class templates内通过this->指涉base class templates内的成员名称,或写出base class资格修饰符;

44. 与参数无关的代码抽离templates

建议:

  • templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
  • 因非类型模板参数而造成的代码膨胀,往往可消除,以函数参数或class成员变量替换template参数
  • 因类型参数而造成的代码膨胀,往往可降低,让带有完全相同二进制表述的具现类型共享实现码

45. 成员函数模板接收所有兼容类型

成员模板

以下为一个构造模板


template<typename T>
class Test{
public:
    template<class I>
            Test(const Test<I>&);	// 成员模板
};

成员函数模板并不会改变语言基本规则;

  • 当提供一个泛化的构造函数并不会阻止编译器生成一个构造函数;

建议:

  • 请使用member function template生成可接受所有兼容类型的函数;
  • 如果你声明member template用于泛化copy构造赋值操作,还是需要声明正常的copy构造函数copy assignment操作符;

46. 需要类型转换时请为模板定义非成员函数

template<typename T>
class Rational {
public:
	friend const Rational operator*(const Rational& lhs,
		const Rational& rhs){
		return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
	}
};

建议:

  • 当我们编写一个class template,而它提供之与此template相关的函数支持所有参数之隐式类型转换时,请将那些函数定义为class template内部的friend函数

47. traits classes表现类型信息

如何设计traits class:

  • 确认若干你希望将来可取得的类型相关信息;
  • 为该信息选择一个名称;
  • 提供一个template和一组特化版本,内含你希望支持的类型相关信息;

如何使用trait class:

  • 建立一组重载函数函数模板,彼此间的差异只在于各自的train参数。令每个函数实现码与其接受之traits信息相应和
  • 建立一个控制函数函数模板,它调用上述那些函数并传递traits所提供的信息;
/** 类的萃取机 */
template<class I>
struct iter_traits {
    typedef typename I::iter_category iterator_category;
    typedef typename I::value_type value_type;
    typedef typename I::differ_type differ_type;
    typedef typename I::pointer pointer;
    typedef typename I::reference reference;
};

/** 指针萃取机 */
template<class T>
struct iter_traits<T*> {
    typedef random_access_iterator_tag iter_category;
    typedef T value_type;
    typedef ptrdiff_t differ_type;
    typedef T* pointer;
    typedef T& reference;
};

/** const 萃取机 */
template<class T>
struct iter_traits<const T*> {
    typedef random_access_iterator_tag iter_category;
    typedef T value_type;
    typedef ptrdiff_t differ_type;
    typedef const T* pointer;
    typedef const T& reference;
};

template<class T>
class A{
public:
    A() {}
    typedef  T value_type;
    typedef  T iter_category;
    typedef  ptrdiff_t differ_type;
    typedef  T* pointer;
    typedef  T& reference;
private:

};

void use_class() {
    int a = 10;
    iter_traits<A<int> >::value_type vt = 10;
    iter_traits<A<int> >::pointer p = &a;
    iter_traits<A<int> >::differ_type dt = a;
    iter_traits<A<int> >::reference r = a;      // 引用必须初始化
    iter_traits<A<int> >::iterator_category ic = a;

}

void use_pointer() {
    int a = 10;
    iter_traits<int*>::value_type vt = 10;
    iter_traits<int*>::pointer p = &a;
    iter_traits<int*>::differ_type dt = a;
    iter_traits<int*>::reference r = a;      // 引用必须初始化
    iter_traits<int*>::iter_category ic;
}

void use_const_pointer() {
    int a = 10;
    iter_traits<const int*>::value_type vt = 10;
    iter_traits<const int*>::pointer p = &a;
    iter_traits<const int*>::differ_type dt = a;
    iter_traits<const int*>::reference r = a;      // 引用必须初始化
    iter_traits<const int*>::iter_category ic;
}

建议:

  • Traits classes使得类型相关信息在编译期可用。它们以templates和templas特化完成实现;
  • 整合重载技术后,traits classes有可能在编译期对类型执行if else测试;

48. 认识template元编程

  • 元编程让某些编程更简洁
  • 工作由运行期转移到编译器

达到好的效果:

  • 确保度量单位正确;
  • 优化矩阵运算;
  • 可以生成客户定制之设计模式实现品;

建议:

  • 可将工作由运行期移到编译期,因而得以实现早期错误侦测更高的执行效率
  • TMP可被用来生成基于政策选择组合的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码;

49. 了解new-handler的行为

operator new无法满足内存分配需求时,将会抛出异常;

  • 在抛出异常前客户可以自定指定错误处理函数new-handle
typedef void(*new_handler)();
new_handler set_new_handler(new_handler p)throw();

new-handle做的事情:

  • 让更多内存可被使用;
  • 安装另一个new-handle:若无法获取更多内存,则下次调用就要做不同事;
  • 卸除new-handle:将null指针传给set_new_handler
  • 抛出bad_alloc的异常;
  • 不返回:调用abort或exit;

建议:

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用;
  • nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常

50. 了解new和delete合理替换时机

.替换编译器提供的operator new 和delete:

  • 为了检测运用错误
  • 为了收集动态分配内存值使用统计信息
  • 为了增加分配和归还的速度
    • 自定义将针对与某特定类型的对象设计的;
  • 为了降低缺省内存管理带来的空间额外开销
    • 泛用型内存管理器内部会使用其他内存开销;
  • 为了弥补缺省分配器中的非最佳齐位
    • 泛用型内存管理器的动态分配采取的齐位并不确定;
  • 为了将相关对象成簇集中
    • 将数据集中成簇在尽可能少的内存页中;
  • 为了获得非传统的行为
    • 实现一些功能,而不使用传统的编译器定义的功能;

建议:

  • 自定new和delete,可以改善效能、对heap运用错误进行调试收集heap使用信息

51. 编写new和delete时需固守常规

建议:

  • operator new应该内含一个无穷循环,并在其中尝试分配内存,若无法满足,则调用new-handle
  • operator delete应在受到null指针时,不做任何事;

52. 写了placement new也要写placement delete

建议:

  • 当写出一个placement new也要写出对应的placement delete否则会发生内存泄漏;
  • 当你声明placement newplacement new不要遮掩正常版本;

53. 不要轻忽编译器的警告

建议:

  • 严肃对待编译器发出的警告信息
  • 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度不同;

54. 让自己熟悉包括TR1在内的标准程序库

tr1组件:

  • 智能指针:tr1::shared_ptrtr1_weak_ptr
  • tr1::function:可调用任何函数、对象;
  • tr1::bind
  • Hash tables:用来实现sets、myltiset、map、multi-maps;
  • 正则表达式;
  • tuples;
  • tr1::array
  • tr1::reference_wrapper
  • 随机数生成工具;
  • 数学特殊函数;
  • C99兼容扩充;
  • Type traits
  • tr1::result_of

55. 熟悉boost

可处理多种问题:

  • 字符串与文本处理;
  • 容器;
  • 函数对象和高级编程;
  • 泛型编程;
  • 模板元编程;
  • 数学和数值;
  • 正确性与测试;
  • 数据结构;
  • 语言间的支持;
  • 内存;
  • 杂项;

版权声明:本文为weixin_45926547原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。