万字长文带你一文读完Effective C++
Effective C++
视C++为一个语言联邦
STL Template C++ C Object-oriented C++
一开始C++只是C加上一些面向对象特性,但是随着这个语言的成熟他变得更加无拘无束,接受不同于C with classes的各种观念、特性和编程战略。异常对函数的结构化带来了不同的做法,templates将我们带来到新的设计思考方式,STL则定义了一个前所未见的伸展性做法。
今天C++已经是个多重范型编程语言,一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。这些能力和弹性使C++成为一个无可匹敌的工具,因此、将C++视为一个语言联邦
尽量以cosnt、enum、inline
替换#define
因为、宏定义会被预处理器处理,编译器并未看到宏定义的信息,当出现一个编译错误信息的时候,可能会带来困惑。
解决之道就是使用一个常量替换宏定义(#define
)
const double AspectRatio = 1.653; // 大写名称通常代表宏定义,因此这里可以使用首字母大写的方法表示const全局变量
作为一个语言常量,AspectRatio
肯定会被编译器看到,当然就会进入符号表内。另外、使用常量也可以有较小的码、因为使用预处理会导致预处理器盲目的将宏名称替换为对应的数值,可能会导致目标码出现多份宏定义的数值。
基于数个理由enum hack
值得我们认识。
- enum hack的行为某方面来说比较像#define而不像const,有的时候这正是你想要的,例如取一个const的地址是合法的,但是取一个enum的地址就是不合法的,而取一个#define的地址通常也不合法。如果你不想让别人获得一个pointer或者reference指向你的某个整数常量,enum可以帮助你实现这个约束。
- 虽然优秀的编译器不会为const对象设置存储空间,但是不够优秀的编译器可能会设置另外的储存空间,enum和#define一样绝对不会导致非必要的内存分配。
- 出于实用主义考虑,很多代码特别是模板元编程中用到了它,因此、看到它你必须认识它。
对于单纯的常量,最好以const
对象或者enums
替换#define
对于形似函数的宏(macros
),最好改用inline
函数替换#define
尽可能使用const
const允许你指定一个语义约束(也就是指定一个不该被改动的对象),编译器会强制执行这一约束。
char greeting[] = "Hello";
char *p = greeting; // non-const pointer, non-const data
const char* p = greeting; // non-const pointer, const data
char* const p = greeting; // const pointer non-const data
const char* const p = greeting; // const pointer, const data
const
语法虽然变化多端,但并不是莫测高深,如果关键字const
出现在型号的左边,表示被指物是常量,如果出现在星号的右边,表示指针自身是常量,如果出现在星号两边,表示被指物和指针两者都是常量。
如果被指物是常量,有些程序员会将关键字const
写在类型之前,有些人会把它写在类型之后、星号之前,这两种写法的意义相同,所以下列两个函数的参数类型是一样的:
void f(const Widget* pw); // 一个指向常量的指针
void f2(Widget const* pw); // 一个指向常量的指针
两种形式都有人使用,是否是指向常量的指针,要看const
相对于星号的位置,星号左边为指向常量的指针,星号右边为常量指针。
const
修饰函数返回值,可以降低编码出现的低级错误
class Rational {};
const Rational operator*(const Rational& lhs, const Rational& rhs);
Rational a, b, c;
if (a*b = c) // 其实是想做个比较,当operator*返回值声明为const的时候将会返回错误,也就防止了编码不小心带来的异常
const
修饰成员函数
可以通过
const
得知哪些函数可以改动对象内容,哪些函数不可以使得操作
const
对象成为可能将某些东西声明为const可以帮助编译器侦测出错误的用法,const可以施加于任何作用于内的对象、函数参数、函数返回类型、成员函数本体
确定对象被使用前已先被初始化
关于将变量初始化这件事,C++
似乎总是反复无常。但是有一点是可以确定的是,读取没有初始化的值会导致不确定行为。
- 对于内置型对象进行手工初始化,因为C++不保证初始化它们
- 构造函数最好使用成员初值列,而不要在构造函数中使用赋值操作,初始列次序应该和它们在class中的声明次序相同
- 为了避免跨编译单元初始化次序问题,请使用local static对象替换non-local static对象,
构造析构赋值
了解C++默默编写并调用哪些函数
当你声明一个空类的时候,C++处理互生成:
- copy构造函数
- Copy assignment操作符
- 析构函数
如果你写:
class Empty {};
经过处理:
class Empty {public:Empty() {} // default构造函数 Empty(const Empty& rhs) {} // copy 构造函数~ Empty() {} // 析构函数Empty& operator=(const Empty& rhs) {} // copy assignment
};
只有当这些函数被需要(被调用),他们才会被编译器创建出来,
Empty e1; // default构造函数Empty e2(e1); // copy构造函数
e2 = e1; // copy assignment操作符
若是不想使用编译器自动生成的函数,就应该明确拒绝
为了驳回编译器自动生成的代码,可以使用将对应函数声明为private,将对应函数后面加上delete,使用Uncopyable这样的base class。
class Uncopyable {protected:Uncopyable() {}~ Uncopyable() {}
private:Uncopyable(const Uncopyable&); // 阻止copingUncopyable& operator=(const Uncopyable&);};
为多态声明virtual析构函数
- 带有多态性质的base class应该声明一个virtual析构函数,如果没有声明,那么当父类指针指向子类对象,通过指针销毁对象的时候不会调用子类的析构函数。
- 如果不具备多态性质,就不应该声明虚析构函数,因为只要有虚函数就会存在虚函数列表,导致类的大小改变,每个函数的查找偏移变大,类中就算只有成员元素也不能按照结构体使用。
别让异常逃离析构函数
有如下代码:
class Widget {public:~Widget() {} // 假设可能吐出一个异常
};void doSomething() {std::vector<Widget> v;
...} // v在这里被销毁
当销毁vector v的时候,有责任把所有的Widgets销毁掉。假设析构第一个元素的时候抛出了异常,因为还有没有析构的,所以取析构第二个,假设第二个也抛出了异常,这对C++来说异常跳多了,在两个异常同时存在的情况下,程序不是结束执行就是导致不明确行为。
- 析构函数绝对不要吐出异常,如果一个被析构的函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作,而不是在析构函数中。
绝不在构造和析构函数过程中调用virtual函数
- 在base class构造期间,virtual函数不是virtual函数
- 当base cleass构造函数执行时,derived class的成员尚未初始化,如果此期间用的virtual函数下降至derived classes阶层,要知道derived class函数几乎必然取local成员变量,而那些成员变量尚未初始化,这将是一张通往不明确行为和彻夜调试大会的直达车票。
- Derived class对象的base class构造期间,对象的类型是base class而不是derived class,不只是virtual函数会被视为
- 在构造函数和析构函数期间不要调用virtual函数,因为这类调用从不下降至derived class
令operator= 返回一个reference to *this
关于赋值你可以写成连锁的形式:
int x, y, z;
x = y = z = 9;
如果想给一个类实现上述操作,类的复制操作符实现必须返回一个reference指向操作符的左侧实参,这也是你为classes实现赋值操作符时应该遵循的协议:
注意这只是个协议,并没有强制性,如果不遵循它代码同样是可以编译通过的,然而这份协议被所有内置类型和标准程序库提供的类型实现,如: string, vector, complex等,因此除非你有一个标新立异的好理由,否则就随众吧。
- 令赋值(assignment)操作符返回一个reference to *this
在operator=中处理"自我赋值"
自我赋值发生在对象被赋值给自己时:
class Widget {};Widget w;
...
w = w;
看起来很蠢,但是却合法,并且赋值操作符并不是看起来那么明显,如果在循环赋值中:
a[i] = a[j]; // 潜在的自我赋值,如果i == j那么这就是一个潜在的自我赋值
自我赋值存在问题的operator=函数
Widget&
Widget::operator=(const Widget& rhs) {delete pb; // 停止当前使用的bitmap,但是想象下,如果传进来的是对象自身 ?pb = new Bitmap(*rhs.pb); // 使用复件,如果传进来的是对象自身,那么上面已经删除了pb指针了return *this;}
传统解决办法,使用在函数前证同测试,达到自我赋值检测目的
Widget&
Widget::operator=(const Widget& rhs) {if (this == &rhs) return *this; // 如果是自我赋值,就直接返回自己的引用delete pb; pb = new Bitmap(*rhs.pb); return *this;
}
上述做法,当new Bitmap抛出异常时,还是会存在问题,我们可以按照下面的方式实现
Widget&
Widget::operator=(const Widget& rhs) { // 即使new抛出异常也不影响正常使用Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb);delete pOrig; return *this;
}
使用交换的版本
Widget&
Widget::operator=(const Widget rhs) { // 即使new抛出异常也不影响正常使用swap(rhs) return *this;
}
- 确保当对象进行自我赋值时operator=有良好的行为,其中技术包括比较来源对象和目标对象的地址、精心周到的语句顺序、以及copy-and-swap
- 确定任何函数如果操作一个以上的对象,其中多个对象是同一个对象时,其行为仍然正确
复制对象时勿忘其每一个成分
设计良好的面向对象系统(OO-systems)会将对象内部分装起来,只留两个函数负责对象拷贝,那便是带着名称的copy构造函数和copy assignment操作符,我们称之为copy函数。
如果使用编译器生成的版本,那么编译器将会对对象中所有的成员做一份拷贝,如果你自己实现了copy函数,那么编译器就算你没有复制所有对象的时候,还是能正常通过。
- copying函数应该确保复制对象所有的成员变量,以及所有的base class成分
- 不要尝试以某个copying函数实现另一个copying函数,应该将共同机能放到第三个函数中,并由两个copying函数共同调用
以对象管理资源
资源取得的时机便是初始化时机(Resource Acquisition Is Initialzation; RAIL)
- 获得资源后立刻放进管理对象内
- 管理对象运用析构函数确保资源被释放
- 为了防止资源泄漏,请使用RAIL对象,他们在构造函数中获得资源,并在析构函数中释放资源
在资源管理类中小心coping行为
- 禁止复制,许多时候允许RAIL对象被复制是不合理的
- 对底层资源使用引用计数法,有时候我们希望保有资源,直到它的最后一个使用者被销毁
- 复制底部资源
- 转移底部资源的所有权
- 复制RAIL对象必须一并复制它所管理的资源,所以资源的Copying行为决定RAIL对象的Copying行为
- 普遍而常见的RAIL class copying行为是:抑制copying、施行引用计数法
在资源管理类中提供对原始资源的访问
资源放到资源管理类中是实现完美编程的必要手段,但是有些资源并非完美,需要将对应资源取出来直接使用
- APIs往往需要访问原始资源,所以每一个RAIL class应该提供一个取得其所管理之资源的办法
- 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,但是隐式转换对客户比较方便
成对的使用new和delete时要采取相同形式
- 如果你在new表达式中使用了[],必须在delete表达式中也使用[]。如果你在new表达式中没有使用[],一定不要在相应的delete表达式中使用[]。
以独立语句将newed的对象置入智能指针
如下代码:
int priprity();
void processWidget(std::share_ptr<Widget> pw, int priority);// 调用
processWidget(new Widget, priority());
虽然上述资源采用了对象管理式资源,但是可能存在资源泄漏,执行的过程中,编译器必须创建如下代码:
- 调用priority()
- 执行new Widget
- 调用std::shared_ptr构造函数
C++以怎样的次序生成上述代码是不确认的,这也是C++与Java和C#的不同之处,C++经过编译优化之后可能会生成如下代码顺序:
- 执行new Widget
- 调用priority()
- 调用std::shared_ptr构造函数
现在你想,如果priority函数抛出异常,将会导致资源泄露
- 以独立语句将newed对象存储只能指针内,如果不这样做,一旦异常抛出,有可能有难以察觉的资源泄露
让接口更容易被正确使用,不易被误用
- 好的接口容易被正确使用不容易被误用,你应该在你的所有接口中努力达到这样的性质
- 促进正确使用的办法包括接口的一致性以及内置类型的行为兼容
- 阻止误用的办法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户的资源管理责任
设计class犹如设计type
C++就像其他OOP语言一样,当你定义一个新的class也就定义了一个新的type。
- 新的type对象应该如何被创建和销毁
- 对象初始化和对象复制应该有什么样的区别
- 新的对象如果被pass by value意味着什么-copy 构造函数用来定义一个pass by value应该如何被实现
- 什么是新type的合法值
- 新type需要配合某个继承图系吗
- 新type需要什么样的转换-隐式转换还是显示转换
- 什么样的操作符作用到新type上是合理的
- 什么样标准函数应该驳回
- 谁该取用新type的成员-成员那个应该是public、protected、private。
- 什么是新type的未声明接口
- 你的新type有多么一般化
- 你是否真的需要一个新type
- class的设计就是type的设计。在定义一个type之前请确定你已经认真考虑过
宁以pass-by-reference-to-const替换pass-by-value
- 尽量以pass-by-reference-to-const替换pass-by-value。前者比较高效,并且可以避免切割问题
- 以上规则并不适用于内置类型,以及STL的迭代器和函数对象,对他们而言pass-by-value往往比较适当
切割问题:当一个derived class对象被by value方式并且被视为base class对象,base class的copy构造函数将会被调用,而造成对象的行为像个derived对象的那些性质全被切割掉,仅留下一个base class对象。
C++编译器的底层往往使用指针的形式实现引用,因此pass by reference通常意味着真正传递的是指针
必须返回对象时,别妄想返回其reference
- 绝对不要返回pointer或Reference指向一个local stack对象,或者返回引用指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象
将成员变量声明为private
- 切记将成员变量声明为private,这样可以赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分实现弹性
- protected并不意味着比public更具有封装性
宁以non-memeber、non-friend替换member函数
有时候使用一个non-memeber、non-friend函数,比使用Member函数根据有封装性,比如一个清理浏览器资源历史记录和缓存的函数
// 因为clearBrowser 不能方位对象的内部成员变量,成员变量的操作只能经过传进来的对象引用调用的函数操作,因此封住效果比成员函数更好
void clearBrowser(WebBrowser& wb) {wb.clearCache();wb.clearHistory();wb.removeCookies();
}
- 宁以non-memeber、non-friend替换member函数,这样可以增加封装性、包裹弹性和技能扩充性
若所有参数皆需要类型转换,请为此采用non-member函数
令classs支持隐式转换通常是个糟糕的注意,当然这条规则也有例外的情况,假设你设计一个类用来表示有理数,允许整数的隐式转换似乎颇为合理.
比如我们实现运算符*,要是将其实现为内置函数如下:
class Rational {public:const Rational operator* (const Rational& rhs) const;
};
以上实现方式,在使用能够实现隐式转换的函数时候,需要隐式转换的参数只能放到后面,否则将会报错
result = oneHalf * 2;// 正常
result = 2 * oneHalf; // 错误
然给我们写成函数调用的方式之后,问题就会一目了然了
result = oneHalf.operator*(2); // 很好
result = 2.operator*(oneHalf); // 错误,因为2没有实现operator*,全局operator*并不能处理oneHalf的运算
让我们实现non-member函数
class Rational {public:};
const Rational operator* (const Rational& lhs,const Rational& rhs)
{return Rational(lhs.numerator() * rhs.numerator(), lhs.dnominator() * rhs.denominator())
}
写成non-member函数之后
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // 没问题
result = 2 * oneFourth * 2; // 也没问题
- 如果你要为某个函数的所有参数(包括this指针所指的那个隐喻参数),进行类型转换,那么这个函数必须是个non-member
考虑写出一个不抛出异常的swap函数
- 当swap函数对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
- 如果你提供一个member swap,也该提供一个Non-member swap用来调用前者,对于class(而非templates),也请特化std::swap
- 调用swap时应针对std::swap使用using声明方式,然后调用swap并且不带有任何空间资格修饰
尽可能延后变量定义式的出现时间
- 尽量延后变量定义式出现的时间,这样可以增加程序的清晰度并改善程序效率
尽量少做转型动作
- const_cast通常被用来将对象的常量性转除(cast away the constness),它也是唯一一个具有此能力的C++Style转型操作符
- dynamic_cast主要用来执行安全向下转型(safe downcasting), 也就是用来决定某个对象是否归属继承体系中的某个类型。他是唯一一个无法由旧式语法执行的动作,也是唯一一个可能耗费重大运行成本的转型动作
- reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。
- static_cast用来强迫隐式转换,例如将Non-const对象转换为const对象,或将int转为double等。也可以执行上述多种转换的反向转换,唯一不能转换的是将const转换为非const
- 如果可以,尽量避免类型转换,特别是在注重代码效率的代码中避免dynamic_casts,如果有个设计需要转换类型动作,试着发展无需转型的替代设计
- 如果转型是有必要的,试着将其隐藏在某个函数背后,可以随后可以调用该函数,而不需要将转型放进他们自己的代码内。
- 宁可使用C++style转型,不要使用旧式转型,前者很容易辨别出来
避免返回handles指向对象内部成分
- 避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数像个const。
为异常安全而努力是值得的
- 异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛出异常型。
- 强烈保证往往能够以copy-and-swap实现出来,但强烈保证并非对所有函数都可实现或具备现实意义
- 函数提供异常安全保证,通常最高只能等于其调用之各个函数的异常安全保证中的最弱者
透彻了解inlining的里里外外
- 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更加容易,也可使潜在的代码膨胀问题最小化,使得程序速度提升机会最大化
- 函数具体是否inlined还取决于调用方式,当使用指针调用的时候,即使你声明了inline还是会按照非inline函数调用
- 不要只因为function templates出现在头文件,就将其生命为inline
将文件间的编译依存关系降至最低
- 支持依存性最小化的一般构想是:相依于声明式,不要依于定义式,基于此构想的两个手段是Handle和Interface classes
- 程序头文件应该以完全且仅有声明式的形式存在,这种做法不论是否涉及templates都适用
确定你的public继承塑膜出is-a关系
- public继承意味is-a。适用于base classes身上的每一件事情也都适用于derived classes身上,因为每一个derived class对象都是一个base class对象
避免遮掩继承而来的名称
class Derived : public Base {public:using Base::mf1; // 让Base class内名为mf1的东西在子类中可见};
- Derived classes内的名称会遮掩base classes内的名称。在public继承下来没有人希望如此
- 为了让被遮掩的名称再见天日。可使用using声明式或转交函数(forwarding functions)
区分接口继承和实现继承
- 成员函数接口总是会被继承,public 意味着is-a的关系
- 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口
- 声明简朴的impure virtual函数的目的,是为了derived classes继承该函数的接口和缺省实现
- 声明为non-virtual函数的目的是为了令derived classes继承函数的几口以及一份强制性实现
- 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口
- Pure virtual函数只具体指定接口继承
- 简朴的impure virtual函数具体指定接口继承及缺省实现继承
- non-virrtual函数具体指定接口继承以及强制性实现继承
考虑virtual函数以外的其他选择
- virtual函数的替代放哪包括NVI(non-virtual interface)手法,以及strategy设计模式的多种形式。NVI手法自身是一个特殊形式的template method设计模式
- 将机能从成员函数移到class外部函数,带来的一个缺点就是,非成员函数无法访问class内部的non-static成员
绝不重新定义继承而来的non-virtual函数
适用于子类对象的每一件事,也适用于D对象,因为每一个D对象都是一个B对象
子类一定会继承父类的非虚函数
绝对不重新定义继承而来的缺省参数值
virtual函数是动态绑定的,而缺省参数是静态绑定的
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而虚函数是动态绑定的
通过复合塑膜出has-a或根据某物实现出
- 复合的意义和public继承完全不同
明智而审慎地使用private继承
独立非附属的对象大小一定不为零。但是这个约束不适合derived class对象内的base class成分,因为他们非独立,如果你继承了Empty
class HoldsAnInt : private Empty {private:int x;
}
几乎可以肯定的是sizeof(HoldsAnInt) == sizeof(int)。这里的EBO(empty base optimization, 空白基类最优化),该技术在STL中进行了大量的实践,比如很多类进程的基础类中没有任何数据,但是有很多enum typedef的类型
- Private继承意味着is-implemented-in-terms of根据某物实现出。它通常比复合的级别低。但是derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的
- 和复合不同的是,private继承可以造成empty base最优化。这对致力于对象尺寸最小化的程序开发者很重要
明智而审慎地使用多重继承
- 多重继承比单一继承复杂。他可能导致新的歧义性,以及对virtual继承的需要
- virtual继承会增加大小、速度、初始化复杂度等等成本,如果virtual base calsses不带任何数据,将是最具有价值的情况
- 多重继承的确有正当用途。其中一个情节设计public 继承某个Interface class和private继承某个协助实现的class的两相组合
了解隐式接口和编译器多态
- Classes和templates都支持接口interfaces和多态
- 对classes而言接口是显示的explicit,以函数签名为中心。多态则是通过virtual函数发生于运行期
- 对template参数而言,接口是隐式(implicit)的,奠基于有效表达式,多态则是通过template具体化和函数重载解析(function overloading resolution)发生于编译器
了解typename的双重意义
虽然很多时候class可以和typename混用,但是有时候你一定得使用typename。
template<typename C>
void print2nd(const C& container) {if (container.size() >= 2) {typename C::const_iterator iter(container.begin());...}
}
- 任何时候你想要在template中指涉一个嵌套从属类型名称,就必须咋紧邻它的前一个位置放上一个关键字tyename
teplate<typename iTerT>
void wordWithIterator(IterT iter) {typename std::interator_traits<IterT>::value_type temp(*iter);...
}
因为std::interator_traits::value_type是嵌套从属名词,因此必须在类型名前放置typename,不然编译器不认得
但是每次都写那么长一串,谁都受不了,因此我们需要在其前方加上typedef从新定义一个类型,因此你可以看到STL中存在大量的如下定义
teplate<typename iTerT>
void wordWithIterator(IterT iter) {typedef typename std::interator_traits<IterT>::value_type temp(*iter);...
}
typename是为了编译器能够识别
typedef是为了程序员能够减轻定义变量带来的负担
- 声明template参数时,前缀关键字class和typename可互换
- 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists基类列或member initialization list成员初始列内以它作为base class修饰符
学习处理模板化基类内名称
- 可在derived class templates内通过this->指涉base class templates内的成员名称,或藉由一个明白写出的base class资格修饰符完成
将参数无关的代码抽离templates
- templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
应用成员函数模板接受所有兼容类型
同一个template的不同具体实现之间并不存在什么与生俱来的固有关系,即使模板是使用具有继承关系的两个类进行实例化的。
模板与泛化编程(Generic Programming)
template<typename T>
class SmartPtr {public:template<typename U> // 成员member templateSmartPtr(const SmartPtr<U>& other); // 为了生成copy构造函数
};
以上代码的意思是,对于任何类型T和任何类型U,这里可以根据SmartPtr生成一个SmartPtr,因为SmartPtr有个构造函数接受一个SmartPtr参数。这一类构造函数根据对象u创建对象t,而u和v的类型是同一个template的不同具体体现,有时候我们称之为泛化(generalized)copy构造函数。
- 请使用member function templates成员函数模板生成可接受所有兼容类型的函数
- 如果你声明member templates用于泛化copy 构造或泛化assigment操作,你还是 需要证明正常的copy构造函数和copy assigment操作符,因为如果你不声明,当编译器根据模板并不能实现对应的构造函数的时候,将会按照默认规则生成对应的默认copy函数和assigment操作符
需要类型转换时请为模板定义非成员函数
template实参推导过程不将隐式转换考虑在内,绝不!
当我们编写一个class template,而它所提供之"与template相关的"函数支持所有参数之隐式类型转换时,请将那函数定义为class template内部的friend函数。
请使用traits calsses表现类型信息
结合该场景只要类型足够多可以在编译期间实现if…else…
iterator_traits的运作方式是,针对每一个类型,在struct iterator_traits内声明某个typedef名为iterator_catory,这个typedef用来确认IterT的迭代器分类
如deque的迭代器,定义如下:
struct _Deque_iterator
{...typedef std::random_access_iterator_tag iterator_category;...
}
通过iterator_catory我们可以获得数据的类型,我们通过获取的类型创建对象,然后在根据重载实现在不同类型调用不同的函数。
1. advance函数内部调用函数
std::__advance(__i, __d, std::__iterator_category(__i));2. iterator_category根据iterator_catory返回对应类型的一个对象
template<typename _Iter>inline _GLIBCXX_CONSTEXPRtypename iterator_traits<_Iter>::iterator_category__iterator_category(const _Iter&){ return typename iterator_traits<_Iter>::iterator_category(); }3. 然后根据重载,实现各个类型所需调用的函数,到时候会根据具体的类型调用对应的函数
template<typename IterT, typename DistT> // 这份实现用于random access
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{iter += d;
}template<typename IterT, typename DistT> // 这份实现用于didirectional
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)
{if (d >= 0) {while(d --) ++iter;}
}template<typename IterT, typename DistT> // 这份实现用于input迭代器
void doAdvance(IterT& iter, DistT d, std::iput_iterator_tag)
{iter += d;
}template<typename IterT, typename DistT> // 这份实现用于random access
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{iter += d;
}
- Traits classes使得类型相关信息在编译期间可用,它们以templates和templates特化实现
- 结合重载技术,traits calsses有可能在编译期间对类型执行if …else…
认识template元编程
Template metaprogramming(TMP, 模板元编程)是编写template-based C++程序并执行于编译期的过程。所谓的模板元编程是以C++编写成、执行于C++编译器内的程序。一旦TMP程序执行结束,其输出,也就是从templates具体出来的若干C++源码,便会一如往常的被编译。
template编程,简称函数式语言,主要依赖于递归,但是这个递归不是真正的函数递归调用,而是递归模板化具体化。
#include <iostream>using namespace std;template<unsigned n>
struct Factorial {enum {value = n * Factorial<n - 1>::value};
};// 特化
template<>
struct Factorial<0> {enum {value = 1};
};int main(int argc, char *argv[]) {cout << Factorial<6>::value << endl;cout << Factorial<3>::value << endl;return 0;
}
- Template metaprogramming(TMP, 模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高效的执行效率。
- TMP可被用来生成基于政策选择组合的客户定制代码,可用来避免生成对某些类型并不适合的代码。
了解new-handler的行为
set_new_handler的参数是个指针,指向operator new无法分配足够内存时该被调用的函数,其返回值也是个指针,指向set_new_handler被调用前正执行的那个new-handler函数
- set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
- Nothrow new是一个颇为局限的工具,因为它只适用于内存分配,后续的构造函数还是可能抛出异常
了解new和delete的合理替换时机
有以下情况时可能需要替换编译器提供的operator new和operator delete
- 用来检测运行上的错误
- 为了强化效能
- 为了收集使用上的统计数据
- 为了检测运用错误
- 为了收集动态分配内存使用统计信息
- 为了增加分配和归还速度
- 为了降低缺省内存管理器代码的空间额外的开销
- 为了弥补缺省分配器中的非最佳齐位
- 为了相关对象成簇集中
- 为了获得非传统行为
- 有许多理由需要写个自己定制的new和delete,包括改善效能、对heap运用错误进行调试、手机heap使用信息。
编写new和delete时需固守常规
Operator new的必须返回正确的值,内存不足时必须得调用new-handling函数,必须应对零内存修的准备,还需要避免掩盖正常形式的new。operator new的返回值要十分单纯如果有能力应答客户申请的内存,就返回一个指针指向那块内存,如果没有那个能力就抛出一个bad_alloc的异常。
当然operator new也不是十分的单纯,在实际分配内存时,会不止一次的尝试分配内存,并且在每次失败后调用new-handling函数,只有当指向new-handling函数的指针是null时,operator new才会抛出异常
- Operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler,它也应该有能力处理0 bytes的申请,class专属
- Operator delete应该收到null指针时不做任何事情,class专属版本还应该处理比正确大小更大的处理
写了palcement new也要写placement delete
Widget *pw = new Widget;s
上述代码一共会有两个函数被调用,一个是用于分配内存operator new,一个是Widget的default构造函数
假设第一个函数调用成功,第二个函数调用的时候出现了异常,这时需要将步骤一申请的内存回复到原观,否则就会造成内存泄露,但是在这时候因为客户没有拿到pw所以取消步骤一并回复旧观的责任就落到了C++运行期系统上。
Placement是带有额外参数的意思
- 当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能发生隐微而时断时续的内存泄露
- 当你声明palcement new和placement delete请确定不要无意识地遮掩他们的正常的版本
不要忽略编译器的警告
- 严肃对待编译器发出的警告信息,努力在你的编译器的最高警告登记下争取无任何警告的荣誉
- 不要过度依赖编译器报警能力,因为不同编译器对待事情的态度并不相同。一旦移植到另外一个编译器上,你原有依赖的编译警告信息就有可能消失
让自己熟悉包括TR1在内的标准程序库
让自己熟悉Boost
提供PDF下载地址:
PDF版本下载地址
万字长文带你一文读完Effective C++相关推荐
- 过程或函数的副作用是_Python函数和函数式编程(两万字长文警告!一文彻底搞定函数,建议收藏!)...
Python函数和函数式编程 函数是可重用的程序代码段,在Python中有常用的内置函数,例如len().sum()等. 在Pyhon模块和程序中也可以自定义函数.使用函数可以提高编程效率. 1.函数 ...
- 万字长文带你 搞定 linux BT 宝塔面板 之外网上快速搭建苹果CMS电影网站
文章目录 万字长文带你搞定宝塔面板 一.本地搭建宝塔面板及安装ecshop 1.1前言 1.2面板特色功能 1.3安装环境说明 1.4安装BT面板 1.5常用管理命令 1.6 BT面板一键安装LAMP ...
- 万字长文带你一览ICLR2020最新Transformers进展(上)
原文链接:http://gsarti.com/post/iclr2020-transformers/ 作者:Gabriele Sarti 编译:朴素人工智能 Transformer体系结构最初是在At ...
- 兄弟姐妹们,我终于上岸了,喜获蚂蚁offer,定级p7,万字长文带你走完面试全过程
前言 在今天,我收到了蚂蚁金服A级的实习录用offer. 从开始面试到拿到口头offer(四面技术+一面HR)战线大约拉了半个月, 从拿到口头offer到收到正式录用邮件大概又是半个月. 思前想后,决 ...
- 喜获蚂蚁offer,定级p7,面经分享,万字长文带你走完面试全过程
前言 在今天,我收到了蚂蚁金服A级的实习录用offer. 从开始面试到拿到口头offer(四面技术+一面HR)战线大约拉了半个月, 从拿到口头offer到收到正式录用邮件大概又是半个月. 思前想后,决 ...
- 经过负载均衡图片加载不出来_吐血输出:2万字长文带你细细盘点五种负载均衡策略。...
Dubbo的五种负载均衡策略 2020 年 5 月 15 日,Dubbo 发布 2.7.7 release 版本.其中有这么一个 Features 新增一个负载均衡策略. 熟悉我的老读者肯定是知道的, ...
- 吐血输出:2万字长文带你细细盘点五种负载均衡策略。
Dubbo的五种负载均衡策略 2020 年 5 月 15 日,Dubbo 发布 2.7.7 release 版本.其中有这么一个 Features 新增一个负载均衡策略. 熟悉我的老读者肯定是知道的, ...
- FEMS综述: 如何从微生物网络中的“毛线球”理出头绪(3万字长文带你系统学习网络)...
如何从微生物网络中的"毛线球"理出头绪 From hairballs to hypotheses–biological insights from microbial Lisa R ...
- 1.6 万字长文带你读懂 Java IO
Java IO 是一个庞大的知识体系,很多人学着学着就会学懵了,包括我在内也是如此,所以本文将会从 Java 的 BIO 开始,一步一步深入学习,引出 JDK1.4 之后出现的 NIO 技术,对比 N ...
最新文章
- Activity之间使用intent传递大量数据带来问题总结
- CHECKLIST TO USE BEFORE SUBMITTING A PAPER TO A JOURNAL
- python中列表数据汇总和平均值_python的列表List求均值和中位数实例
- python基础-类的继承
- 董明珠的“接班人”出现了!这个22岁的小姑娘,凭什么?
- php.ini var dump,php安装xdebug后var_dump()不能输变量内容解决办法
- Reading privileged memory with a side-channel
- 语言程序设计赵山林电子版_【特别策划】崇州“老市长”赵抃系列之一:做官要像江水保持清白...
- TensorFlow 教程 --新手入门--1.2 下载安装
- python获取本地时间并向服务器发送udp报文_python3通过udp实现组播数据的发送和接收操作...
- oracle 分页_Mybatis:PageHelper分页插件源码及原理剖析
- [Node.js]Domain模块
- Java 选择排序法
- 笔记本显示电源已连接但是未充电的简单解决办法
- 使用PreTranslateMessage(MSG* pMsg)截获键盘数字键
- 在线小游戏,在线小游戏大全,网页在线小游戏大全
- 软件测试(六)——缺陷以及总结
- 数据分析师就业前景怎么样?零基础能成为数据分析师吗?
- 腾讯cos做文件服务器,将腾讯云COS对象存储挂载至腾讯云服务器实现大硬盘存储...
- 矩阵奇异值计算的一种新方法——基于R语言实现