effective c++ 学习

3 篇文章 3 订阅

链接:https://pan.baidu.com/s/1GTC7K23bUF9chPv-SxbRAA 
提取码:7n3o

这是第三版,2006年出版的,c++11还没出来。所以有些知识比较旧。

条款2:尽量以const,enum,inline替换 #define

const:
#define直接替换导致名称从未被编译器看到
const定义常量也可能比#define导致较小量的码
#define不重视作用域,故不提供封装性
enum:
取一个const的地址是合法的,但取一个enum的地址不合法
inline:
#define定义函数可能招致误用,最好用inline函数替换
记:
对于单纯常量,最好以const对象或enums替换#defines;对于形似函数的宏,最好改用inline函数替换#defines。

延伸:程序的执行顺序及其处理 

预处理-> 编译->汇编->链接

(1).预处理(cpp):预处理器不止一种,而C/C++的预处理器就是其中最低端的一种——词法预处理器,主要是进行文本替换、宏展开、删除注释这类简单工作。

(2).编译器(ccl):将文本文件.i翻译成文本文件.s,得到汇编语言程序(把高级语言翻译为机器语言),该种语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令。

(3).汇编(as):将.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。

(4).链接(ld):gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去。 函数库一般分为静态库和动态库两种。

延伸:

1.const :c++ const 和 define_小飞侠hello的博客-CSDN博客

2.inline 的理解:c++ inline 函数及变量_小飞侠hello的博客-CSDN博客_c++ inline变量

条款3:尽可能使用const

char greeting[] = "hello";

 const char *p = greeting;

 char *const p = greeting;

 const  char *const p = greeting;

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

const 成员函数,如 viod func () const;   必须保证该方法不会修改任何类的成员的值。

延伸:可以使用const_cast<> 的方式去掉const.

延伸:在c++11之后,有constptr 表示在编译器使用

c++ const 和 define_小飞侠hello的博客-CSDN博客

条款4.确保对象使用前已被初始化。

方法是:在构造函数中对每一个成员初始化。

引申到初始化和赋值的 差异。初始化要比赋值效率高。做法是:使用成员初值列,如即在构造函数后:m_nindex(0)

对象的成员变量的初始化动作发生在进入构造函数本体之前。

而在构造函数里面的是赋值。 

延伸: c++ 11 后,可以直接在结构体、类的头文件对成员变量初始化(即支持就地初始化)。若同时对一个非静态的成员变量进行就地初始化和初始化列表初始化,最后起作用的是初始化列表(即后起作用)。

    struct MyStruct
    {
        const static int a{ 1 };
        int b = 1;
        int c{ 1 };
    };

详见:c++ 11 新特性讲解大全_baidu_16370559的博客-CSDN博客的第17条快速初始化成员变量

初始化列表的介绍见:C++构造函数初始化列表与赋值_小飞侠hello的博客-CSDN博客

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

base class如果把copy构造函数或copy assignment操作符设置为private,derived class将拒绝生成copy构造函数或copy assignment操作符。或者是使用关键字“=delete”

如果自己声明了一个构造函数,编译器就不会再为它创建default构造函数。

延伸:

1.声明了带参数的构造函数版本,则需要声明不带参数的版本以完成无参的变量初始化,而这会导致对应的类不再是POD的(非POD类型,意味着编译器失去了优化这样简单的数据类型的可能),通过=default 可以使其重新成为POD类型。

关于pod 介绍:什么是 POD 数据类型_小飞侠hello的博客-CSDN博客

具体见:c++ 11 新特性讲解大全_baidu_16370559的博客-CSDN博客的第3条 = default
2. c++11 可以使用using来继承基类构造函数 

具体见c++11中using的使用_baidu_16370559的博客-CSDN博客

条款6:所有的编译器产出的函数都是public,想阻止编译器暗自创建,可以把这些函数声明为private并且故意不实现定义。如copy构造函数、copy assignment 操作符

或者是在基类定义这些函数为private,子类通过private基类,在子类的对象实现copy操作会报错的,达到同样的效果。

延伸:在c++11之后,直接在函数的定义或者声明加上”= delete“就可达到同样的效果。

class NoCopyConstructor2
{
    public:
        NoCopyConstructor2() = default;
 
        NoCopyConstructor2(int i)
            :data(i)
        {
        }
        NoCopyConstructor2(const NoCopyConstructor2&) = delete;
 
    private:
        int data;
};

条款7.带多态性质的base classes 应该声明一个virtual 析构函数

如果class 带有任何virtual 函数,他就应该拥有一个virtual 析构函数。如果class 不带有任何virtual 函数,不需要使析构函数为virtual函数,因为虚函数使对象体积增加。(因为存在虚函数表指针,正常为4字节),所以,只有当class 至少有一个 virtual 函数,才为他声明virtual 析构函数。

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

析构函数绝对不能抛出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉异常,然后结束程序或者吞下他们(不传播)。

引申到:如果需要对某个操作函数运行期间可能抛出的异常做出反应,应该在普通函数做那个执行该操作,而不是析构函数。

理解:

如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。 [正常情况下调用析构函数抛出异常导致资源泄露].

如果析构函数中异常非抛不可,那就用try catch来将异常吞下,必须要把这种可能发生的异常完全封装在析构函数内部,决不能让它抛出函数之外。

.c++ 中 try catch throw异常_小飞侠hello的博客-CSDN博客

另外:无论何时,从构造函数中抛出异常都是可以的。经典的解决方案是使用STL的智能指针,如shared_ptr。

条款9.在构造和析构函数中,不要调用virtual 函数。

因为这类调用从不下降至derived class. 

 原因是:在构造函数中,该对象的derived class 成员尚未初始化,对象的类型是base class ,而不是derived class。调用的虚函数不会下降到子类中去。所以说在base class 构造期间,virtual 函数不是真正的virtual 函数。   

在析构函数该对象的derived class 成员呈现出未定义值。还是执行基类的虚函数。

条款10.令operator= 返回一个reference to *this

  • 为了实现“连锁赋值”等
ctestclass& operator= (ctestclass &aa);

ctestclass& ctestclass::operator=(ctestclass &aa)
{
	m_b = aa.m_b;
	return *this;
}

条款11.在operator= 中处理“自我赋值”

加一个“证同测试”使具有“自我赋值安全性”
精心安排的语句可以使代码具有“异常安全性”(自动获得“自我赋值安全性”):在复制构造之前别删除原指针
另一种替代方案是“copy and swap”技术

条款12.复制对象时务忘其每一个成分

copying 函数应该确保复制 对象内的所有成员变量及所有 的base class 成分。

另外注意指针类深拷贝问题。

不要尝试以一个copying函数实现另一个copying函数。应将共同机能放进第三个函数中并由它们共同调用

条款13.以对象管理资源.

为了防止资源泄漏,请使用RAII对象资源获得时机便是初始化时机 Resource Acquisition Is Initialization,在构造函数里面获得资源,并在析构函数里面释放资源。所以不需要手动delete.

利用的原理是:把资源放到对象内,依赖c++的析构函数自动调用机制确保资源被释放。

auto_ptr:被销毁会自动删除它所指之物,复制所得的指针将获得资源的唯一拥有权。

在C++11后,请使用unique_ptrshared_ptr来达到这个目的。

c++ 智能指针auto_ptr (c++98)、shared_ptr(c++ 11)、unique_ptr(c++ 11)、weak_ptr(c++ 11)_baidu_16370559的博客-CSDN博客

条款14.在资源管理类小心copy行为

一般资源管理类复制时可以选择以下做法:

  • 禁止复制(复制不合理)
  • “引用计数法”(使用tr1::shared_ptr指定“删除器”阻止引用次数为0时的删除行为)
  • 复制底层资源(“深度拷贝”)
  • 转移底部资源的拥有权(auto_ptr)

unique_ptr和shared_ptr并非资源管理的全部,因为这两者往往只使用于heap-based资源。有时,有必要建立自己的资源管理类。

复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。

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

条款16.成对使用new和delete要采用相同的格式.

如果在new 表达式中使用[ ],必须在相应的delete表达式中也使用[ ] 。如果在new 表达式中不使用[ ],一定不要在相应的delete 表达式中使用。

条款17.以独立语句将new 的对象存储于智能指针内。

如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。

 条款18.让接口容易被正确使用,不易被误用

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

条款19:设计class犹如设计type

如何设计高效的classes?想要回答这个问题必须了解面对的问题:

  • 新type的对象应该如何被创建和销毁
  • 对象的初始化和对象的赋值有什么样的差别
  • 新type的对象如果被passed by value,意味着什么(copy构造函数用来定义一个type的passed by value该如何实现)
  • 什么是新type的“合法值”
  • 你的新type需要配合某个继承图系吗
  • 你的新type需要什么样的转换
  • 什么样的操作符和函数对此新type而言是合理的
  • 什么样的标准函数应该驳回
  • 谁该取用新type的成员
  • 什么是新type的“未声明接口”
  • 你的新type有多么一般化
  • 你真的需要一个新type吗

条款20:尽量以pass_by-reference-to-const 替换pass-by-value 

缺省情况下,C++是以by value方式传递对象至函数的,这种方式默认调用对象的copy构造函数产生一个副本,这就有可能造成比较严重的耗时。采用pass-by-reference-to-const可以有效避免这个问题,const是必要的,因为这保证传入对象不会被修改。

同时,以by-reference方式传递参数可以避免slicing(对象切割)问题,还是可以实现多态。

但是,对于内置类型,以及STL的迭代器和函数对象,pass-by-value比较合适。

对象切割:当把一个派生类对象赋给一个基类对象时,会发生对象切割。(另外用基类对象强制转换派生类对象也会)。

发生对象切割后派生类的覆盖部分就被切掉了,所以调用的方法将会是父类方法。即使是虚函数,也不能实现多态了。

条款21:.必须返回对象时,别妄想返回其reference

绝不要返回pointer或reference指向一个local stack对象(在函数退出前被销毁)必须避免
不要返回pointer或reference指向一个heap对象(用户不知道如何delete)
不要返回pointer或者reference指向local static对象而有可能需要多个这样的对象(同一行不能调用多次该函数,static只有一份)

通常来说,并不太需要担心返回对象的消耗,编译器是存在RVO优化的,尤其是C++11引入move后,选择就更加多样了。

一个必须返回新对象的函数的正确写法:就让那个函数返回一个新对象。尤其是C++11引入move后,选择就更加多样了。

条款22:切记将成员变量声明为private.

可通过set或get 的public成员函数访问这些private成员变量。另外protected并不比public更具封装性。

条款23:宁以non-member,non-friend替换member

  • 愈多函数可访问它,数据的封装性就愈低,故member函数封装性差
  • 将所有便利函数放在多个头文件内但隶属同一个命名空间,意味客户可以轻松扩展这一组便利函数,降低了编译依存性,这正是STL的做法

依据是类的封装性。

 non-member,non-friend 函数不能访问class 的private成分的函数,所以封装性更好

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

  • 只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者;this对象(隐喻参数)绝不是隐式类型转换的合格参与者
  • menber函数的反面是non-member函数,不是friend函数
class Rational{
……
}
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
……
}

延伸:

1.C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式).参考:C++中的explicit关键字_baidu_16370559的博客-CSDN博客

2.类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend。

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

当std::swap对自定义类型效率不高时(例如深拷贝),提供一个swap成员函数,并确定不会抛出异常
如果提供一个member swap,也该提供一个non-member swap用来调用前者 (对class而言,需特化std::swap;对class template而言,添加一个重载模板到非std命名空间内)

注:
不可以添加新的东西到std内
调用swap时应该针对std::swap使用using声明式,然后调用swap不带任何”命名空间修饰”

延伸:c++11 使用右值引用和数据移动的原理,代替了不必要的内存拷贝,因此效率会大大提高

template <class T> void swap (T& a, T& b)
{
  T c(std::move(a));

    a=std::move(b);

    b=std::move(c);
}

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

  • 不只应该延后变量定义直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止,这样可增加程序的清晰度并改善程序效率

依据是:减少无意义、不必要的构造函数、析构函数、赋值函数的调用。

条款27:尽量少做转型动作

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast;试着发展无需转型的替代设计
  • 如果转型是必要的,试着将它隐藏于某个函数后(即封装成在一个函数里面)
  • 宁可使用C++-style转型,不要使用旧式转型(新式转型很容易辨识出来,而分门别类)

四种新式转型如下:
static_cast:适用范围最广的,适用于很多隐式转换,基本数据类型的转换,基类指针与子类指针的相互转换,或者添加const属性,任何类型转换为void类型
dynamic_cast:主要用来执行“安全向下转型”,决定某对象是否归属继承体系中的某个类型。static_cast在下行转换时不安全,是因为即使转换失败,它也不返回NULL ,而dynamic_cast转换失败会返回NULL;对于上行转换,dynamic_cast和static_cast是一样的
const_cast:通常用来将对象的常量性消除
reinterpret_cast:在比特位级别上进行转换。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针,不能将非32bit的实例转成指针。最普通的用途就是在函数指针类型之间进行转换,不可移植
具体见:c++ 4种新型的类型转换运算符_baidu_16370559的博客-CSDN博客

条款28.避免返回handles(号码牌)包括 (引用、指针、迭代器)指向对象内部。

其根本原因是不能让handle比其所值的对象更长寿(函数内的局部变量都已经析构销毁了)。否则会破坏封装性,使const成员函数的行为矛盾。

可使用const 来帮助。

条款29.为“异常安全”而努力是值得的

“异常安全函数”即使发生异常也不会有泄漏资源或允许任何数据结构败坏,区分为以下三种保证:
基本承诺:异常抛出,程序内的任何事物仍然保持在有效状态下
强烈保证:异常抛出,程序状态不改变,回复到调用函数之前的状态(往往能够以copy-and-swap实现出来)
不抛掷保证:绝不抛出异常(如内置类型)在函数后面加上throw()
可能的话提供“nothrow保证”,当“强烈保证”不切实际时,就必须提供“基本保证”
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者

延伸:像new 资源的问题,可以通过智能指针来实现,减少异常的出现。

条款30.透彻了解inline函数的里里外外

inline意味着执行前,先将调用动作替换为被调用函数的本体。不适用在太复杂的函数(如循环、递归函数)和虚函数。

将大多数inlining限制在小型、被频繁调用的函数身上。
Template的具现化与inlining无关(Template放在头文件只是因为一般在编译器完成具现化动作)
注:
inline只是给编译器的建议,大部分的编译器拒绝将太过复杂的函数inlining,隐喻方式是将函数定义于class定义式内
构造函数和析构函数往往是inlining的糟糕候选人
随着程序库的升级,inline函数需要重新编译,而non-inline函数只需重新连接

具体见:c++ inline 函数及变量_baidu_16370559的博客-CSDN博客

条款31.将文件的编译依存关系降到最低

  • 支持”编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classesInterface classes

Handle classes:在.h文件中用class 声明代替include头文件,把成员变量替换为指针的形式。

class 声明式  在 头文件处 使用class  类型 ; 然后在使用到的函数再#include(具体表现在源文件) 。

而class定义式 是在头文件直接使用#include .

两者比较,声明式要比定义式效率要高,编译依存性要低。

具体见:c++ 前置声明_baidu_16370559的博客-CSDN博客

#include 和前置声明_baidu_16370559的博客-CSDN博客

Interface Classes:也可以降低编译的依赖,实现方法大致是父类只提供虚方法,而将实现放置在子类中,再通过父类提供的一个特别的静态函数,生成子类对象,通过父类指针来进行操作;从而子类头文件的改动也不会导致使用该类的文件重新编译,因为用的是父类指针,客户include的是只是父类头文件。

注意:对于C++类而言,如果它的头文件变了,那么所有这个类的对象所在的文件都要重编,但如果它的实现文件(cpp文件)变了,而头文件没有变(对外的接口不变),那么所有这个类的对象所在的文件都不会因之而重编。

编译依存最小化的设计策略
1、如果使用object references或object pointers可以完成任务,就不要用objects
2、如果能够,以class声明式替换class定义式
3、为声明式和定义式提供不同的头文件

条款32.确定你的public继承塑模出is-a模型

public继承意味着is-a。适用于base class身上的每一件事情也一定适用于derived class身上

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

  • 编译器对于各作用域有查找顺序,所以会造成名称遮掩,各作用域依次为:
    • global作用域
    • namespace
    • base class
    • derived class
    • local作用域
    • 等等
  • 可以利用using声明式或者inline转交函数使遮掩函数重见天日。
  • inline转交函数具体是在子类的函数中,直接调用基类:函数的方式。
  • 利用using声明式具体见:c++11中using的使用_baidu_16370559的博客-CSDN博客

子类内的函数名称会遮掩基类内的名称,遮掩 其实相当于隐藏。这个延伸到重定义(隐藏)、重写(覆盖)。

重写(覆盖):同名 + 基虚函数 + 同参          

重定义(隐藏):同名 + (基非虚或者是不同参)   像这种情况,  基类的函数将被遮掩,不能直接调用。如想调用,必须是使用using 声明式 或者是域解析操作符(即调用时在方法名前加上基类名和两个冒号,子类对象.父类类名::重定义方法)  如:   b.AA::print();//通过子类的对象访问父类的print

如:内层作用域的名字会遮掩外围作用域的名称。

延伸:继承关系中的重写(覆盖)和重定义(隐藏,隐藏是指派生类的函数屏蔽了与其同名的基类函数。注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。)

具体见:学习c++ 必知三大特性_baidu_16370559的博客-CSDN博客

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

  • pure virtual函数使derived class只继承函数接口(即函数声明)。这样是子类必须重写自己的实现方式,不能和基类有一样的实现定义。  纯虚函数也可以有自己的实现,但一般基类不写实现。
  • impure virtual函数使derived class继承函数接口和缺省实现。隐患在于:子类可能忘记了重写虚函数,会直接调用基类的缺省实现,不符合实际要求。改善办法:使用pure virtual函数必须早子类重新定义和在基类虚函数部分提出来做一个普通的公共函数。让子类的虚函数显性调用这个基类的公共函数。
  • non-virtual函数使derived class继承函数的接口和一份强制性实现。

直白点:接口继承就是声明继承,用纯虚函数;实现继承就是定义继承,用虚函数。

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

1.Non-Virtual Interface手法实现Template Method模式:令客户通过public non-virtual成员函数间接调用private virtual函数,得以在一个virtual函数被调用之前设定好场景,并在调用结束之后清理场景
2.藉由Function Pointers实现Strategy模式:可以由构造函数接受一个指针,指向一个提供的函数。

3.藉由tr1::function完成Strategy模式:改用一个类型为tr1::function的对象,这样的对象可以保存任何可调用物(callable entity,即函数指针、函数对象、成员函数指针),只要其签名式兼容于需求端。

4.古典的Strategy模式:将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。

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

  • non-virtual函数是静态绑定的,virtual函数是动态绑定的。

 pb被声明为一个指向B的指针,通过pb调用非虚函数,永远是B定义的版本,即使pb指向一个类型为B派生类D的对象。归根到底是静态绑定。

如果mf为虚函数(属于动态绑定),不论通过pb还是pd调用mf,都会调用d::mf。因为pb和pd真正指向的都是一个类型为D的对象。

另外补充:

1.如果是用对象赋值的,如

D x;

B b = x;

b.mf();  //永远是调用B::mf, 即使mf是虚函数。因为对象切割。

2.如果是使用引用的话

D x;

B &b = x;

当mf()为非虚函数时, 调用的是B::mf

当mf()为虚函数时,调用的是D::mf.

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

因为缺省参数值是静态绑定的。

如拒绝这样做,可能会在调用一个定义于derived class内的virtual函数时,使用base class指定的缺省参数值。

 

 使用指针或者引用都会出现这样的问题。使用对象就不会,因为对象直接调用基类的函数了。

 静态类型:指声明的类型,而不管它们真正指向什么。如非虚函数就是静态绑定,另外缺省参数值也是静态绑定。处于编译器阶段。好处是效率高。

动态类型:是指目前所指对象的类型,主要通过虚函数方式。处于运行期,执行效率低。

使用NVI手法(令public non-virtual函数调用private virtual函数)可以防止缺省参数值被重新定义
注:
为了运行期效率,c++坚持缺省参数值为静态绑定,防止运行期复杂的决定。

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

  • 当复合发生于应用域内的对象之间,表现has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系
  • 复合的意义和public继承完全不同        复合又叫,组合、聚合  是一种has-a 的关系    有一个 ,而public 继承是is-a 关系  是一个。
  •     在应用域,复合意味has-a 。在实现域,复合意味is-implemented-in-terms-of (根据某物实现出)。

  • 面向对象原则:高内聚、低耦合。多聚合、少继承

    各种关系的强弱顺序:

     继承 = 实现 > 组合 > 聚合 > 关联 > 依赖          

参考: c++类之间的基本关系_baidu_16370559的博客-CSDN博客

条款39.明智而审慎地使用private继承

Private继承意味implemented-in-terms-of(只有实现被继承,接口部分应略去)
尽可能使用复合,必要时才使用private继承(当derived class想访问base class的protected成分时,或为了重新定义virtual函数时,还有造成EBO(empty base optimization)节省内存时才为必要)

private继承规则如下:

  • 编译器不会自动将一个derived class对象转换为一个base class对象,不是is-a
  • 所有成员都会变成private属性

private继承在软件设计上没有意义,其意义只涉及软件实现层面上。

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

1.多重继承比单一继承 复制。可能导致新的歧义。   

      如继承相同名称的函数、变量。对应的解决办法是:指出调用哪一个基类的函数。

 2. 在一个继承体系,如某一个基类和派生类之间有一条以上的相通路线。即至少有3层继承关系。出现的现状是:  默认情况下,最底下的派生类(孙子辈)会继承至少2份最上面的基类的成员数据。另外,如果想只要继承一份数据,必须用到“virtual继承”。但虚继承会增加大小、速度、初始化及赋值复杂度等成本。虚继承参考:虚继承_baidu_16370559的博客-CSDN博客

多重继承的一个正当用途是“复合+继承”技术,单一继承更受欢迎。

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

  • classe和template都支持接口和多态
  • 对class而言接口是显式的,由函数签名式构成;多态是通过virtual函数发生于运行期
  • 对template而言接口是隐式的,由有效表达式组成;多态是通过template具现化和函数重载解析发生于编译期

条款42.了解typename的双重意义

声明template参数时,前缀关键字class和typename可以互换。说明意义完全相同。
使用typename标识嵌套从属类型名称(如果编译器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型),但是不得在base class lists或member initialization list内作为base class修饰符。不太理解?

当想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename.

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

template特化版本可能不提供和一般性template相同的接口,所以从Object Oriented C++跨进Template C++时,继承就不像以前那般畅行无阻了

 
为了令c++不进入templatized base classes观察的行为失效,可以有3种办法:
1.在调用动作之前加上“this->”
2.使用using声明式(using baseclass::func;)
3.明白指出被调用的函数位于base class内(baseclass::func())最好不用这办法,因为实现不了虚函数的多态。

参见:C++中 模板Template的使用_小飞侠hello的博客-CSDN博客

条款44.将参数无关代码抽离template

  • 非类型模板参数造成的代码膨胀,以函数参数或者class成员变量替换template参数
  • 类型模板参数造成的代码膨胀,可以让具有完全相同二进制表述的具现类型共享实现码

条款45.运用成员函数模版接收所有兼容类型

 请使用成员函数模版生成“可接受所有兼容类型”的函数

  • temmplate<typename T>
    class SmartPtr{
    public:
        template<typename U>
        SmartPtr(const SmartPtr<U>& other)     //泛化copy构造函数
            : heldPtr(other.get()){...}       //只有当“存在某个隐式转换可将U*                      
                                               //转换为T*”时才能通过编译,从而
                                               //约束转换行为
        T* get() const {return heldPtr;}
        ...
    private:
        T* heldPtr;                            //持有的内置指针
    };

即使声明了“泛化拷贝构造函数”和“泛化的赋值操作符”,仍然需要声明正常的拷贝构造函数和拷贝赋值操作符

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

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

template <typename T>
class Rational{
public:
    …
    friend const Rational<T> operator* (const Rational<T>& lhs, 
                                        const Rational<T>& rhs)
    {                                                           //定义体
        return Rational (lhs.numerator() * rhs.numerator(),
                         lhs.denominator() * rhs.denominator());
    }
    …
}

template实参推导过程中从不将隐式类型转换函数纳入考虑,而class template并不依赖template实参推导,在生成模板类时就可推导出函数而非函数模板

条款47.请使用traits classes表现类型信息

Traits classes 使得“类型相关信息”在编译期可用。它们以 templates 和 “templates 特化”完成实现,如下:

template<...>                 //自定义迭代器
class deque{
public:
    class iterator{
    public:
        //嵌套的typedef迭代器分类
        typedef random_access_iterator_tag iterator_category;  
    };
};

template<typename IterT>
struct iterator_traits{
    //用iterator_category表现IterT的类型
    typedef typename IterT::iterator_category iterator_category;
    ...
};

iterator_traits通过特化版本可以提供希望支持的相关类型(如指针类型)
上述iterator_traits::iterator_category在编译期确定,而if判定却是运行期
确定,不仅浪费时间,也造成可执行文件膨胀
整合重载技术后。traits classes 有可能在编译期对类型执行 if…else 测试

条款48.认识模板元编程

  • 模板元编程可将工作由运行期移至编译期,因而得以实现早期错误侦测和更高的执行效率,可能导致较小的可执行文件,较短的运行期,较少的内存需求,可以解决不少问题

条款49.了解new-handler的行为

当operator new无法满足某一内存分配需求时,它会抛出异常;抛出异常之前,也可以先调用一个客户指定的错误处理函数(new-handler),调用set_new_handler可以指定该函数
Nothrow(在无法分配足够内存时返回NULL)是一个颇为局限的工具,它只适用于内存分配,后继的构造函数调用还是可能抛出异常

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

  • 有许多理由需要写个自定的new和delete,包括检测错误、改善效能,以及收集使用上的统计数据等等

条款51.编写符合常规的new和delete

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

条款52.写了placement new也要写相应的placement delete

new表达式先后调用operator new和default构造函数
当你写一个placement operator new,请确定也写出了对应的placement operator delete.如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏(运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete,如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operator delete会被调用)

  即

1. delete p;默认调用的是正常形式的operator delete,  placement delete 只有在伴随placement new调用而触发的构造函数出现异常才被调用。

2.operator delete 用于构造期间无任何异常被抛出。即使使用了placement new,也可以使用delete.

当你声明placement new和placement delete,请确定不要无意识地遮掩了它们的正常版本

 办法就是

1.在base 类含有所有形式的的new和delete。

2.如还想扩展自定义的,利用继承机制及using声明式,避免基类的函数被遮挡。using的用法见:c++11中using的使用_baidu_16370559的博客-CSDN博客

继承和虚函数影响c++设计的耦合性吗?

 

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值