Effective C++总结

explicit关键字

C++中的explicit关键字只能用于修饰只有一个参数或者是其他参数有默认值的类构造函数, 它的作用是表明该构造函数是显式的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).explicit关键字的作用就是防止类构造函数的隐式自动转换,防止隐式调用这个构造函数.上面也已经说过了, explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了.
https://www.cnblogs.com/DswCnblog/p/6513318.html
volatile

https://www.cnblogs.com/god-of-death/p/7852394.html
C/C++ Volatile关键词的第三个特性:”顺序性”,能够保证Volatile变量间的顺序性,编译器不会进行乱序优化。Volatile变量与非Volatile变量的顺序,编译器不保证顺序,可能会进行乱序优化。同时,C/C++ Volatile关键词,并不能用于构建happens-before语义,因此在进行多线程程序设计时,要小心使用volatile,不要掉入volatile变量的使用陷阱之中。
动态链接、静态链接

https://www.jianshu.com/p/53faba3f0661
extern

一. extern修饰变量和函数
修饰符extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。extern声明不是定义,即不分配存储空间。

也就是说,在一个文件中定义了变量和函数, 在其他文件中要使用它们, 可以有两种方式:

使用头文件,然后声明它们,然后其他文件去包含头文件
在其他文件中直接extern,多文件要一起编译
二. extern"C" 作用
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言(而不是C++)的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。

这个功能主要用在下面的情况:

C++代码调用C语言代码
在C++的头文件中使用
extern "C"包含双重含义,从字面上可以知道,首先,被它修饰的目标是"extern"的;其次,被它修饰的目标代码是"C"的。

记住,语句:extern int a; 仅仅是一个变量的声明,其并不是在定义变量a,也并未为a分配空间。变量a在所有模块中作为一种全局变量只能被定义一次(空间只能分配一次),否则会出错。

extern对应的关键字是static,static表明变量或者函数只能在本模块中使用,因此,被static修饰的变量或者函数不可能被extern C修饰。
1.视C++为一个语言联邦

四个次语言。C、面向对象C++、模板C++、STL。C++高效编程守则视状况而变化,取决于你使用C++的哪一部分。
2.尽量以const,enum,inline替换#define

从源代码到获取到可执行程序大致流程如下所示:
Step1:源代码(source code)
Step2:预处理器(preprocessor)
Step3:编译器(compiler)
Step4:目标代码(object code)
Step5:链接器(Linker)
Step6:可执行文件(executables)

预处理器工作内容:删除注释、包含(include)其他文件以及宏替换等

编译器工作时间:晚于预处理器,工作任务:语法分析、语义分析等,最后生成目标文件

#define不会进行类型安全检查,而const会,且const常量有数据类型,#define无。所以用const常量替换#define比较好。对于单纯常量,最好以const对象或enums替换#defines。

宏中所有的实参要加上小括号,否则会有麻烦。可以用内联来消除这些隐患。用内联的好处:没有调用函数时的开销,编译器可以对内联的代码进行优化。

当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。不过这么做是有代价的,代码会变长,这就意味着占用更多的内存空间或者占用更多的指令缓存。对于简短的函数并且调用次数比较多的情况,适合使用内联函数。

对于形似函数的宏(macros),最好改用inline函数替换#defines
3.尽可能使用const

如果关键字const出现在星号左边,表示被指物是常量;右边,指针本身是常量;星号两边,被指物和指针都是常量。

迭代器是基于指针建立的,const iterator代表迭代器不能指向其他东西。而const_iterator所指的东西不可被改动。(仅仅是指不能通过该const_iterator的解引用来改变所指内存中的内容,其他方式仍然可以)

将某些东西声明为const可帮助编译器侦测出错误用法(例如将“==”写成“=”)。const可被施加于任何作用域内的对象、函数参数、函数返回类型、函数成员本体。

类的成员函数后面加 const,表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。常量(即 const)对象可以调用 const 成员函数,而不能调用非const修饰的函数。两个成员函数如果只是常量性不同,是可以被重载的。如果只有const成员函数,非const对象是可以调用const成员函数的。当const版本和非const版本的成员函数同时出现时,非const对象调用非const成员函数。 本质上,const指针修饰的是被隐藏的this指针所指向的内存空间,修饰的是this指针。
4.确定对象被使用前已被初始化

构造函数要使用成员初值列,如果写在函数体内就不是初始化,是赋值。在赋值之前会调用类成员的默认构造函数,再进行赋值,这种写法效率低。成员初值列写法不会调用类成员的默认构造函数。成员变量的初始化次序完全不受它们在初始化表中次序的影响,只有成员对象在类中声明的次序来决定的。

为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。

也就是说当需要用到跨文件的静态对象时,不要直接使用对象,因为不能保证另一个文件对那个对象进行了初始化,应该以 A().function() 这种形式调用。A()保证了静态对象被初始化。
5.了解C++默默编写并调用哪些函数

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

对于内含引用成员或者是const成员的类应自己定义copy assignment操作符。因为引用不可以改指向不同对象,const成员无法改变。
6.若不想使用编译器自动生成的函数,就该明确拒绝

为驳回编译器自动(暗自)提供的功能,可将相应的成员函数声明为private并且不予实现。例如不想使用自动生成的拷贝构造函数,就自己声明一个为private,并且不实现,那么这个函数就无法被编译器调用。
7.为多态基类声明virtual析构函数

delete指向对象的指针的时候会调用析构函数,当父类指针指向子类对象时,析构函数必须是virtual才能调用子类的析构函数,否则只会调用父类的造成析构不完全,进而导致内存泄漏。但是如果类没有继承的话不要把析构写成virtual,因为会造成不必要的开销。当一个类需要被继承而且需要具备多态性(父类指针指向子类对象)时,析构函数需要声明为虚函数。调用析构函数时,先调用子类的析构函数再调用父类的。

classes的设计目的如果不是做为base classes使用,或不是为了具备多态性,就不该声明virtual析构函数。

polymorphic(带多态性质)base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
8.别让异常逃离析构函数

析构函数不能抛出异常的理由:

1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。

2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

解决办法:

1.try catch捕获异常后啥也不做,不要throw异常

2.如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9.绝不在构造和析构过程中调用virtual函数

当一个子类被构造时会先构造它的父类。当一个子类被析构时,会先析构本身再析构父类

假如在构造函数里调用了virtual,因为构造子类之前会先构造父类,那么此时构造函数里的virtual函数调用的是父类的版本,因为此时子类还没有被构造,它里面的virtual函数相当于不存在。

在调用基类析构函数时,派生类的成员已被析构,这样就会将对象视为基类对象,虚函数不再是虚函数,只有基类自身的版本。

绝不在构造和析构函数中调用virtual函数,因为这类调用从不下降至派生类。

如果想要实现不同的子类实现不同的父类的构造函数里的某个方法,可以在子类构造函数里,显式的使用成员初值列初始化父类的时候把信息传递给父类。
10.令 operator= 返回一个 reference to *this

为了实现例如 x=y=z 这样的赋值连锁形式, 重载= 需要返回自身,所以需要返回 reference to *this
11.在 operator= 中处理“自我赋值”

https://blog.csdn.net/liitdar/article/details/80656156

确定任何函数如果操作一个以上对象,而其中多个对象是同一个对象时,其行为仍然正确。

确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
12.复制对象时勿忘其每一个成分(深拷贝)

https://blog.csdn.net/weixin_41143631/article/details/81486817

当你编写一个copying函数,请确保(1)复制所有local成员变量(2)调用所有基类里的适当的copying函数。把子类对象传入基类的构造函数,相当于基类的引用指向子类对象。

不要尝试以某个copying函数实现另一个copying函数。应该将共同技能放进第三个函数中,并由两个copying函数共同调用。
13.以对象管理资源

https://blog.csdn.net/quinta_2018_01_09/article/details/93638251

把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。

获得资源后立刻放进管理对象。资源取得时机便是初始化时机(resource acquisition is initialization RAII)获得资源后再同一语句内以它初始化或赋值某个管理对象。

管理对象运用析构函数确保资源被释放。

auto_ptr和shared_ptr两者都在其析构函数内做delete而不是delete[]。因此在动态分配而得的array身上使用auto_ptr或tr1::shared_ptr是个馊主意。array可以用vector或string。

为防止资源泄露,请使用RAII(Resource Acquisition Is Intialization)对象,它们在构造函数中获得资源并在析构函数中释放资源。
14.在资源管理类中小心copying行为

当一个RAII对象被复制,会发生什么?可以有以下两种可能:

a。禁止复制。例如,互斥锁被复制。

b。对底层资源祭出“引用计数法”,如tr1::shared_ptr。tr1::shared_ptr的缺省行为是“当引用计数为0时删除其所指物”,例如对于互斥锁我们希望的释放动作是解除锁定而非删除,可以指定其所谓的deleter。shared_ptr的构造函数第一个参数是所指对象的指针,第二个参数是自定义的deleter方法。

假如期望资源共用同一份时,copying行为就要复制底部资源,即深拷贝

转移底部资源的拥有权。若希望只有一个RAII对象指向一个未加工资源(raw resource),可以让资源的拥有权从被复制物转移到目标物。像auto_ptr。

并非所有资源都是heap_based,对那种资源而言,智能指针往往不适合作为资源掌管者。
15.在资源管理类中提供对原始资源的访问

auto_ptr和tr1::shared_ptr都提供一个get成员函数,用来执行显式转换,也就是它会返回智能指针内部的原始指针。

APIs往往要求访问原始资源,所以每一个RAII类应该提供一个取得其所管理资源的方法。

对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
16.成对使用new和delete时要采取相同形式

如果在new表达式中使用[],必须在相应的delete表达式中使用[]。如果在new表达式中没有使用[],一定不要在相应的delete表达式中使用[]。
17.以独立语句将newed对象置入智能指针

以独立语句将new出来的对象置入智能指针内。如果不这样做,这两个步骤之间有可能被编译器插入其他的操作,而这个操作一旦抛出异常,有可能导致难以察觉的资源泄漏。
18.让接口容易被正确使用,不易被误用

促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容。

阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁。
19.设计class犹如设计type

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

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

新type的对象如果被pass by value(以值传递)意味着什么在拷贝构造函数中体现

什么是新type的合法值

新type需要配合某个继承图系吗(虚函数的一些使用场景)

新type需要什么类型的转换( 隐式转换:operator T (),其中T是一个类型。显式转换是专门的一个函数来返回目标类型:T get())

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

什么样的标准函数应该驳回?(声明为private可以防止被隐式调用)

谁该取用新type的成员(决定哪些成员是private、protected、public以及哪些类、函数应该是友元)
20.宁以 pass-by-reference-to-const 替换 pass-by-value

传值调用,由拷贝构造函数完成。

使用传引用 代替 传值 两个作用:

1.对于自定义类型,节约资源(剩了拷贝构造,和析构)
2.防止 切割问题(slicing problem) : 基类引用指向一个派生类对象 ,然后这个引用被函数传值调用,那么拷贝构造只复制了该对象的基类部分,virtual使用的是基类的版本。

Tips:

尽量 以 传const引用 代替 传值。 高效且避免切割问题。
只对 内置类型,和 STL 的迭代器、函数对象,使用传值 。
21.必须返回对象时,别妄想返回其reference

Tips:

绝不要返回 pointer 或 reference 指向一个 local stack 对象
// 局部变量销毁后,指针悬挂了
或 heap-allocated对象
// 可能无法正确的 delete 掉这个对象 ,比如 w=x * y * z ,operator * 返回引用的话,y*z返回的引用就无法delete,造成内存泄漏。
或 指向 local static 对象而又必须使用很多这样的对象
// static对象只有一份,使用同一份会造成错误
简单办法,返回一个新对象!
22.将成员变量声明为private

Tips:

将成员变量声明为 private。 好处:
访问数据一致性: 如果public接口内每样东西都是函数,客户就不需要在打算访问class成员时犹豫是否使用小括号,因为样东西都是函数。
细微划分访问控制:如果成员变量设为public,每个人都可以读写它。但是以函数取得或者设置其值,可以实现各种控制。
为“所有可能得实现”提供弹性:例如,可使成员变量被读写时通知其他对象、验证class约束条件、函数前提和事后状态、多线程环境下执行同步控制…
protected 并不比 public 更具封装性: 一旦你将一个成员变量声明为 public或protected 而客户开始使用它,就很难再改变那个成员变量涉及的一切。

public:可以被任意实体访问

protected:只允许子类及本类的成员函数访问

private:只允许本类的成员函数访问
23.宁以 non-member、non-friend替换member函数

https://blog.csdn.net/u014038273/article/details/76020672

考虑封装性:作为一种粗糙的量测,越多函数可以访问这个数据,那数据的封装性就越低。

“非成员非友元”函数比“成员函数”有更大的封装性。(注意是’非成员且非友元’。 友元函数和成员函数的访问权利是相同的)因为“非成员非友元”函数无法访问private。

C++,比较自然地做法是: 让这种 为对象提供便利的函数 成为一个non-member函数且位于 其服务的类 的同一个namespace内。

要知道,namespace和classes 不同,前者可以跨越多个源码文件而后者不能 。 这种切割方式并不适用于class成员函数,因为一个class必须整体定义,不能分割为片段。

将所有便利函数放在多个头文件内,但隶属同一个命名空间,意味着客户可以轻松扩展这一组便利函数。 需要做的就是添加更多 非成员非友元函数 到此命名空间。
24.若所有参数皆需类型转换,请为此采用non-member函数

如果你需要为某个函数的所有参数(包括this指针)进行类型转换,那么这个函数必须是个non-member。(例如重载运算符)
25.考虑写出一个不抛异常的swap函数

C++的访问控制是类层面的 class-level, 而不是对象级别的object-level,

同一个类可以访问所有自己类实例的私有成员, 数据成员是类私有而不是实例私有, 成员是否可访问是类的性质, 而不是对象的性质

如果 swap 缺省实现代码对你的 class 或 class template 提供可接受的效率,你不用做任何事情。

如果 swap 缺省版本效率不足 (几乎总意味着 使用了 指针指向内容 ‘pimpl’ 的手法):
  提供 public swap 成员函数。 这个函数绝不该抛出异常。
  在你的 class 所在命名空间内,提供 non-member swap, 并用它 调用 1 中的 swap 成员函数。
  如果你是class(而不是 class template), 特化 std::swap。并用它 调用 1 中的 swap 成员函数。
  调用swap, 请包含一个 using 声明式。 以便 让std::swap在函数内可见。
为“用户自定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
26.尽可能延后变量定义式的出现时间

尽可能延后变量定义式的出现时间,这样做可增加程序的清晰度并改善程序效率

尽量延后直到能够给他初值实参为止,太早的话会先调用default构造,然后再赋值,比copy构造多一次调用。
27.尽量少做转型动作

const_cast< new_type >(expression)。const_cast转换符是用来移除变量的const或volatile限定符。const_cast实现原因就在于C++对于指针的转换是任意的,它不会检查类型,任何指针之间都可以进行互相转换。所以可以把const指针或引用转换成非const指针或引用。

static_cast< new_type >(expression)
dynamic_cast< new_type >(expression)
备注:new_type为目标数据类型,expression为原始数据类型变量或者表达式。

static_cast相当于传统的C语言里的强制转换,该运算符把expression转换为new_type类型,用来强迫隐式转换如non-const对象转为const对象,编译时检查,用于非多态的转换,可以转换指针及其他,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:

①用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。
进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。
③把空指针转换成目标类型的空指针。
④把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉expression的const、volatile、或者__unaligned属性

dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(cross cast)。

在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。dynamic_cast是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。dynamic_cast 性能依赖于编译器,如果是通过字符串比较实现的,性能堪忧,尤其是继承树庞大的时候,dynamic_cast 可能需要执行若干次字符串比较。

尽量少使用转型操作,尤其是dynamic_cast,耗时较高,会导致性能的下降,尽量使用其他方法替代。
28.避免返回handles指向对象内部成分

避免返回handles(包括引用、指针、迭代器)指向内部对象。遵守这个条款可增加封装性,否则返回的内部对象的封装性将变得和这个函数的封装性一样。

帮助const成员函数的行为像个const,const成员函数意味着不修改类内的成员,那指针成员指向的对象的值也不能变。

并将发生“虚吊号码牌”(dangling handles)的可能性降到最低。假如返回一个非引用的值,那么这个值将会在函数调用语句结束后析构掉,如果直接把指针指向返回值,返回值析构后这个指针就会悬空。
29.为“异常安全“而努力是值得的

异常安全函数提供以下三个保证之一:

a。基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。然后程序的现实状态可能无法预料。

b。强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数须有这样的认知:如果函数成功,就是完全成功,如果函数失败,就会恢复到调用函数之前的状态。

c。不抛掷保证:承诺绝不抛出异常,因为它们总是能够完成承诺的功能。

异常安全函数即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈性、不抛异常型

强烈保证往往能够以copy-and-swap实现出来(先copy一个副本,对这个副本进行操作,最后swap回去。所以状态只有完全不做,和一起做),但强烈保证并非对所有函数都可实现或具备现实意义。(包含其他安全性差的函数就无法实现强烈保证,效率太差,可能不具备现实意义。)

函数提供的异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者。
30.透彻了解inlining的里里外外

https://www.runoob.com/w3cnote/cpp-inline-usage.html

inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内,定义于class内的friend函数也是。

将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,是程序的速度提升机会最大化。

虚函数不会被inline,因为虚函数是在运行时动态绑定,而inline是编译时展开

指针指向的函数不会被inline,因为无法对其取地址。

改变inline函数后,应用到该函数的客户程序都得重新编译,而非inline函数只需重新连接,若实现为dll,连接都不需要。
31.将文件间的编译依存关系降至最低

因为C++在编译的时候需要知道对象所需存储空间的大小,所以需要知道这个对象的类的所有数据成员的定义来计算大小。那么这些成员定义的文件一旦发生更改,那么使用这个类的文件都需要重新编译才能正确运行。

分离的关键在于以”声明的依存性“替换”定义的依存性“(把原本所需的数据成员单独写成一个类或结构体,类的数据成员使用指针而不是具体的类型来指向包含数据的类或结构体,分配内存空间的时候就只需要分配指针所需大小的空间就行,不再需要具体的类型的大小,也就和它们的定义分隔开了。具体使用到的类型需要声明,也就是声明的依存性。)这种设计常被称为pimpl(pointer to implementation) idiom。包含指向数据的指针的类往往被称为handle classes。

只需要类型的声明就可以定义出指向该类型的指针或引用。但定义某类型的对象,就需要用到该类型的定义式。

当声明一个函数而它用到某个类时,只需要类的声明就可以。实现函数的时候需要具体的类的定义式。

为声明式和定义式提供不同的头文件。这些文件必须保持一致性。如果某个声明式被改变了,那么定义式也要相应的改变。

另一个实现方法是接口类(interface classes),只有一个virtual析构函数以及一组纯虚函数用来描述整个接口,接口类也不排斥实现成员变量或成员函数。

支持编译依存性最小化的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classed和Interface Classes

程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。
32.确定你的public继承塑模出is-a关系

https://blog.csdn.net/wangshubo1989/article/details/48982465
“public 继承”意味 is-a 。适用于base classes 身上的每一件事情一定也适用于derived classes 身上。因为每一个derived class对象也都是一个base class 对象。
如果class D 以public形式继承class B,你的意图就是告诉编译器:每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。
33.避免遮掩继承而来的名称

derived classes内的名称会遮掩base classes内的名称。只要是同名的函数或变量都会遮掩。
34.区分接口继承和实现继承

声明一个pure virtual 函数的目的是为了让derived classes 只继承函数接口

声明 impure virtual 函数的目的,是让derived classes 继承该函数的接口和缺省实现

声明non-virtual 函数的目的是为了令 derived classes 继承函数的接口及一份强制性的实现
35.考虑virtual函数以外的其他选择

虚函数的替代方案包括NVI手法及策略设计模式的多种形式。

NVI(non virtual interface)手法自身是一个特殊形式的模板模式。以public non-virtual 成员函数包裹较低访问性的virtual函数,使得可以在虚函数调用的前后做需要的处理。

将机能从成员函数移到class外部函数(方法是将virtual函数替换为函数指针成员变量 ),好处是不同的对象可以有不同的函数实现,还有通过修改函数指针可以在运行时改变具体的函数实现。带来的一个缺点是,非成员函数无法访问class的non-public成员,可能需要降低类的封装性。

tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定目标签名式兼容”的所有可调用物。
36.绝不重新定义继承而来的non-virtual函数

父类指针调用non-virtual函数永远是父类所定义的版本,即使这个指针指向子类对象。
37.绝不重新定义继承而来的缺省参数值

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而唯一可能重写的virtual函数是动态绑定的。C++出于效率的考虑而这么设计的。如果子类重写了父类的virtual函数,修改了缺省参数值,那么父类指针指向子类对象并且调用这个函数的时候,就会使用子类的实现和父类的缺省参数值。
38.通过复合塑模出has-a或“根据某物实现出”

当某种类型的对象含有其他类型的对象时就是复合。复合的意义和public继承完全不同。

在应用域,复合意味着has-a(例如person有一个address )。在实现域,复合意味着is-implemented-in-terms-of(根据某物实现出)(例如根据list实现出set)。
39.明智而审慎地使用private继承

如果D以private形式继承B,意思是D对象根据B对象实现而得。

尽可能通过复合来实现,当需要用到protected成员或者virtual函数相关的需求时,才使用private继承。

EBO(Empty Base Optimization),空白基类最优化。派生类继承空白基类后的大小等于派生类本身成员的大小。EBO一般在单一继承下才可行。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
40.明智而审慎地使用多重继承

多重继承的确有正当用途。其中一个情节涉及“public继承某个接口类”和“private继承某个协助实现的类”的两相结合。
41.了解隐式接口和编译期多态

类和模板都支持接口和多态

对类而言接口是显式的,以函数签名为中心。多态则是通过虚函数发生于运行期。

对模板参数而言,接口是隐式的,基于有效表达式。多态则是通过模板具现化和函数重载解析发生于编译期。
42.了解typename的双重意义

嵌套从属名称:template内出现的名称如果相依于某个template参数,称之为从属名称,假如该名称在class内呈嵌套状,就称之为嵌套从属名称。

如果解析器在template中遇到嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。我们通过在嵌套从属名称前面加typename关键字来告诉编译器这是一个类型。typename不能用于非嵌套从属名称。
一个和上一条不一致的例外情况:typename不能出现在base class list(必然是类型)和members init list(必然是某个变量)中。
43.学习处理模板化基类内的名称

当一个子类继承一个模板基类时,c++编译器会拒绝承认基类内的成员,因为模板基类可能会特化,特化版本无法保证成员一定存在。

可以通过在子类模板内通过"this->"指涉基类模板内的成员名称,或借由using声明式,或借由一个明白写出的“基类资格修饰符”完成。 告诉编译器,就是去模板基类找,代码保证了成员存在。编译时,编译器会去具体化的基类内查找是否存在对应的成员。
44.将与参数无关的代码抽离templates

因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或类成员变量替换模板参数。

因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。
45.运用成员函数模板接受所有兼容类型

    请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数;

    如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。

所谓智能指针,就是资源管理类,行为像指针。因为要对不同的类型的指针进行封装,所以智能指针是类模版。

那么问题来了。对于原始指针,支持隐式类型转换,也就是说,父类指针可以指向子类指针。因此,我们当然期望,父类智能指针可以指向子类智能指针,但是,不同Base和Derived实例化出来的智能指针,是不同的类型,之间没有任何关系,更谈不上继承关系,肯定不能赋值。

怎么解决呢?

提供一个成员方法模版,对兼容的类型进行构造或者赋值。也就是copy构造模版和copy赋值模版。

这里出现了一个问题,以copy构造模版为例,当copy构造模版的形参与类模版的形参一致,那么就退化为普通的copy构造,和用户定义(或者编译器生成)的一样了,这不就重复了吗?成员方法模版并不改变语言规则。也就是说,当前存在copy构造模版,如果用户没有定义copy构造,编译器还是会自动生成一个copy构造,copy赋值也是一样的道理。可以这样认为:成员方法模版的模版形参U 不等于类模版的模版形参T。
46.需要类型转换时请为模板定义非成员函数

当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。友元的作用就是告诉编译器,参数应该隐式转化成这种类型。
47.请使用traits classes表现类型信息

    Traits class使得类型相关信息可以在编译期可用,它们以template和template特化完成实现;

    整合重载技术后,traits classes有可能在编译期对类型执行if-else测试。

48.认识template元编程

元编程本质上就是将运行期的代价转移到编译期,它利用template编译生成C++源码

元编程有何优点?

    以编译耗时为代价换来卓越的运行期性能,因为对于产品级的程序而言,运行的时长远大于编译时长。

    将原来运行期才能发现的错误提前到了编译期,要知道,错误发现的越早,代价越小。

元编程有何缺点?

    代码可读性差,写起来要运用递归的思维,非常困难。

    调试困难,元程序执行于编译期,不能debug,只能观察编译器输出的error来定位错误。

    编译时间长,运行期的代价转嫁到编译期。

    可移植性较差,老的编译器几乎不支持模板或者支持极为有限。

49.了解new-handler的行为

set_new_handler允许客户指定一个函数( void func() ) ,在内存分配无法获得满足时被调用。
50.了解new和delete的合理替换时机

有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
51.编写new和delete时需固守常规

operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler.它也应该有能力处理0bytes申请.class专属版本则还应该处理’比正确大小更大的(错误)申请’.
operator delete应该在收到null指针时不做任何事情.class专属版本则还应该处理’比正确大小更大的(错误)申请’.
52.写了placement new 也要写placement delete

placement new 意味着带任意额外参数的new ( void* operator new (std::size_t,其他参数) ) 用法是 new(其他参数) 类型。

如果在分配内存之后调用类型的构造函数时出错的话,编译器找不到对应的delete,就会造成内存泄漏。placement delete的额外参数必须和placement new一样。

placement delete只有在伴随placement new调用而出发的构造函数出现异常时才会被调用,正常的delete 一个指针调用的是正常形式的operator delete。
53.不要轻忽编译器的警告

严肃对待编译器发出的警告信息。努力在你的编译器最高(最严厉)警告级别下争取“无任何警告”的荣誉

不要过度倚赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本倚赖的警告信息有可能消失。
54.让自己熟悉包括tr1在内的标准程序库

TR1添加了诸如智能指针、一般化函数指针、哈希容器、正则表达式及其他10个组件。
55.让自己熟悉Boost

Boost是一个社群,一个网站。它致力于免费、源码开放、同僚复审的C++程序开发。Boost在C++标准化过程中扮演了重要的角色。

Boost提供了许多TR1组件实现品,以及其他许多程序库。
————————————————
版权声明:本文为CSDN博主「愿风丶裁尘」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41963107/article/details/108346270
posted on 2022-09-07 10:27  莫水千流  阅读(107)  评论(0编辑  收藏  举报