IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> Effeetive C++笔记 -> 正文阅读

[C++知识库]Effeetive C++笔记

习惯C++

视 C++为一个语言联邦

  • C
  • Object-Oriented C++
  • Template C++
  • STL

请记住

  • C++高效编程守则视状况而变化,取决于你使用 C++的哪一部分

尽量以 const ,enum ,inline 替换#define

尽可能使用 const

char greeting[]="hello";
char* p=greeting;  //non-const pointer non-const data
const char* p=greeting //not-const pointer const data
char* const p=greeting //const pointer ,non-const data

const 语法虽然变化多端,但并不莫测高深。如果关键字 const 出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
如果被指物是常量,const 可以写在类型之前,也可以写在类型之后,* 之前。
const 用在 STL 迭代器之前,不能对 iterator 进行自加,但可以对所指物赋值
const 用在函数返回类型之前 表示返回一个 const 对象 防止出现(a*b)=c
const 成员函数 在参数列表之后加 const
真实程序中 const 对象大多用于 passed by pointer-to-const 或 passed by reference-to-const 的传递结果
non-const operator[] 的返回类型是个 reference to char,不是 char。如果 operator[]只是返回一个 char,下面这样的句子就无法通过编译:tb[0]=‘x’

在 const 成员函数内,如果需要修改变量的值,可以声明变量时加上mutable 释放掉 non-static 成员变量的 bitwise constness 约束

总结

  • 将某些东西声明为 const 可帮助编译器侦测出错误用法。const 可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制实施 bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。
  • 当 const 和 non-const 成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复。

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

  • 对于无任成员的内置类型,必须手工完成此事。如 int x=0;
  • 内置类型以外的任何其他东西,初始化责任落在构造函数身上:确保每一个构造函数都将对象的每一个成员初始化(别混淆了赋值 assignment 和初始化 initialization)
ABEntry::ABEntry(const string& name,const string& address)
{
  this->name=name;  //赋值 assignment
  this->address=address;
}

上面不是最佳做法,C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。在 ABEntry 构造函数内,theName,theAddress 都不是被初始化,而是被赋值。初始化的发生时间更早,发生于这些成员的 default 构造函数被自动调用之时(比进入 ABEntry 构造函数本体的时间更早)。
构造函数的一个较佳写法是,使用所谓的 member initialization list(成员初值列)替换赋值动作

ABEntry::ABEntry(const string& name,const string& address)
:name(name),
address(address)
{

}

甚至当你想要 default 构造一个成员变量,你都可以使用成员初值列,只要指定无物(nothing)作为初始化实参即可。假设 ABEntry 有一个无参数构造函数,我们可将它实现如下:

ABEntry::ABEntry()
:name(), //调用name的default构造函数
address()
{
}

如果成员变量是 const 或 references,它们就一定需要初值,不能被赋值
为避免需要记住成员变量何时必须在成员初值列中初始化,何时不需要,最简单的做法就是:总是使用成员初值列。这样做有时候绝对必要,且又往往比赋值更高效
C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes 更早于其 derived classes 被初始化 ,而 class 的成员变量总是以其声明次序被初始化。回头看看 ABEntry,其 theName 成员永远最先被初始化,然后是 theAddress,即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。为避免你或你的检阅者迷惑,并避免某些可能存在的晦涩错误,当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序。

使用其它文件中的 extern 对象时,无法确定对象是否初始化,C++对“定义于不同的编译单元内的 non-local static 对象”的初始化相对次序并无明确定义。将每个 non-local static 对象搬到自己的专属函数内(该对象在此函数内被声明为 static)。这些函数返回一个 reference 指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static 对象被 local static 对象替换了。Design Patterns 迷哥迷姊们想必认出来了,这是 Singleton 模式的一个常见实现手法。

ABEntry& abe(){
  static ABEntry abe;
  return abe;
}

总结:

  • 为内置型对象进行手工初始化,因为 C++不保证初始化它们。
  • 构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在 class 中的声明次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以 local static 对象替换 non-local static 对象。

构造/析构/赋值运算

了解 C++默默编写并调用哪些函数

如下

class Emmpty
{
private:
  /* data */
public:
  Emmpty(/* args */);
  Emmpty(const Emmpty& rhs){}
  ~Emmpty();

  Emmpty& operator=(const Emmpty& rhs){}
};

class Empty2{};
int main(){
  Empty2 e2; //default构造函数
  Empty2 e3(e2); //copy构造函数
  e3=e2; //copy assignment操作符

}

如果你打算在一个“内含 reference 成员”的 class 内支持赋值操作(assignment),你必须自己定义 copy assignment 操作符。面对“内含 const 成员”的 classes,编译器的反应也一样。更改 const 成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。最后还有一种情况:如果某个 base classes 将 copy assignment 操作符声明为 private,编译器将拒绝为其 derived classes 生成一个 copy assignment 操作符。毕竟编译器为 derived classes 所生的 copy assignment 操作符想象中可以处理 base class 成分,但它们当然无法调用 derived class 无权调用的成员函数。编译器两手一摊,无能为力。
总结
编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数。

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

禁止生成copy构造和赋值:将 copy构造函数和copy assignment操作符声明为private就可以 或写一个专门为了阻止copying动作而设计的base class内

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。

C++11允许我们通过使用delete来禁用某些特征:

class A
{
public:
    A(int a){};
    A(double) = delete;         // conversion disabled
    A& operator=(const A&) = delete;  // assignment operator disabled
};

为多态基类声明virtual析构函数

如果class不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当作base class,令其析构函数为virtual往往是个馊主意。

如果Point class内含virtual函数,其对象的体积会增加:在32-bit计算机体系结构中将占用64 bits(为了存放两个ints)至96 bits(两个ints加上vptr);在64-bit计算机体系结构中可能占用64~128 bits,因为指针在这样的计算机结构中占64 bits。因此,为Point添加一个vptr会增加其对象大小达50%~100%!Point对象不再能够塞入一个64-bit缓存器,而C++的Point对象也不再和其他语言(如 C)内的相同声明有着一样的结构(因为其他语言的对应物并没有 vptr),因此也就不再可能把它传递至(或接受自)其他语言所写的函数,除非你明确补偿vptr——那属于实现细节,也因此不再具有移植性。

无端地将所有 classes 的析构函数声明为 virtual,就像从未声明它们为virtual一样,都是错误的。许多人的心得是:只有当class内含至少一个virtual函数,才为它声明virtual析构函数。

给base classes一个virtual析构函数”,这个规则只适用于polymorphic(带多态性质的)base classes身上。这种base classes的设计目的是为了用来“通过base class接口处理derived class对象”。
记住

  • polymorphic(带多态性质的)base classes 应该声明一个 virtual 析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
  • Classes 的设计目的如果不是作为 base classes 使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。

别让异常逃离析构函数

C++并不禁止析构函数吐出异常,但它不鼓励你这样做。
两个办法可以避免这一问题。DBConn的析构函数可以:

  • 如果close抛出异常就结束程序。通常通过调用abort完成:
  • 吞下因调用close而发生的异常:
    总之
  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。

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

由于 base class 构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived classes阶层,要知道derived class的函数几乎必然取用local成员变量,而那些成员变量尚未初始化。这将是一张通往不明确行为和彻夜调试大会串的直达车票。“要求使用对象内部尚未初始化的成分”是危险的代名词,所以C++不让你走这条路。

在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class (比起当前执行构造函数和析构函数的那层)。

令operator=返回一个 reference to*this

class Widget{
  public:
    Widget& operator=(const Widget & w){

      return * this;
    }
};

这只是个协议,并无强制性。如果不遵循它,代码一样可通过编译。然而这份协议被所有内置类型和标准程序库提供的类型如 string,vector,complex,tr1::shared_ptr或即将提供的类型 共同遵守

在operator=中处理“自我赋值”

Widget w;
w=w;
class BitMap
{

};
 

class Widget{
  public:
    Widget& operator=(const Widget & w){  //不安全的operator= 实现版本
      delete pb;
      pb=new BitMap(*w.pb);

      return * this;
    }
    private :
    BitMap* pb;
};

operator=函数内的*this(赋值的目的端)和 rhs有可能是同一个对象。果真如此delete就不只是销毁当前对象的bitmap,它也销毁rhs的 bitmap。在函数末尾,Widget——它原本不该被自我赋值动作改变的——发现自己持有一个指针指向一个已被删除的对象!

Widget& operator=(const Widget & w){  //不安全的operator= 实现版本
      if (this== &w) return *this; //证同测试  如果是自我赋值,就不做任何事
      delete pb;
      pb=new BitMap(*w.pb);

      return * this;
    }
  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

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

计良好之面向对象系统(OO-systems)会将对象的内部封装起来,只留两个函数负责对象拷贝(复制),那便是带着适切名称的copy构造函数和copy assignment操作符,我称它们为copying函数。

类中添了新的字段,要修改copy构造函数和copy assignment操作符 将新加的字段进行复制操作

只要你承担起“为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是private ,所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数:


class Base
{
};

class Derived:Base
{
private:
  /* data */
  int a ;
public:
  Derived(/* args */);
  Derived(const Derived& rhs);
  Derived& operator=(const Derived& rhs);
  ~Derived();
};
Derived::Derived(const Derived& rhs)
:Base(rhs),  //调用base class 的copy构造函数
a(rhs.a)
{
}
Derived& Derived::operator=(const Derived& rhs){
  Base::operator=(rhs); //对base class进行赋值
  a=rhs.a;
  return *this;
}
Derived::Derived(/* args */)
{
}

Derived::~Derived()
{
}

int main(){
  Derived d;

  Derived e(d);
  e=d;

}

你不该令copy assignment操作符调用copy构造函数。如果你发现你的copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private而且常被命名为init。这个策略可以安全消除 copy构造函数和 copy assignment操作符之间的代码重复。

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

资源管理

所谓资源就是,一旦用了它,将来必须还给系统。如果不这样,糟糕的事情就会发生。C++程序中最常使用的资源就是动态分配内存(如果你分配内存却从来不曾归还它,会导致内存泄漏),但内存只是你必须管理的众多资源之一。其他常见的资源还包括文件描述器(file descriptors)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets。不论哪一种资源,重要的是,当你不再使用它时,必须将它还给系统。

以对象管理资源

利用auto_ptr以对象管理资源
auto_ptrs有一个不寻常的性质:若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!这一诡异的复制行为,复加上其底层条件:“受auto_ptrs管理的资源必须绝对没有一个以上的auto_ptr同时指向它”,意味auto_ptrs并非管理动态分配资源的神兵利器。举个例子,STL容器要求其元素发挥“正常的”复制行为,因此这些容器容不能用auto_ptr。auto_ptr的替代方案是“引用计数型智慧指针” 如tr1::shared_ptr

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

createInvestment返回的“未加工指针”(raw pointer)简直是对资源泄漏的一个死亡邀约,因为调用者极易在这个指针身上忘记调用delete。(即使他们使用auto_ptr或tr1::shared_ptr来执行delete,他们首先必须记得将createInvestment的返回值存储于智能指针对象内。)为与此问题搏斗,首先需要对createInvestment进行接口修改

  • 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
  • 两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

在资源管理类中小心copying行为

  • 复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定RAII对象的copying行为。
  • 普遍而常见的 RAII class copying 行为是:抑制 copying、施行引用计数法(reference counting)。不过其他行为也都可能被实现。

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

  • APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。
  • 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

成对使用new和delete时要采取相同形式

#include <string>
using namespace std;
int main(){
  string * sp1=new string;
  string * sp2=new string[100];
  delete sp1;
  delete[] sp2;
}

如果你调用new时使用[],你必须在对应调用delete时也使用[]。如果你调用new时没有使用[],那么也不该在对应调用delete时使用[]。

以独立语句将newed对象置入智能指针

processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
执行顺序可能是
1 new widget
2 priority()
3 tr1::shared_prt

如果priority()异常,会导致 new widget的指针遗失 造成内存泄露

std::tr1::shared_ptr<Widget> pw(new Widget);  //在单狗语句内以智能指针存储newed所得对象

processWidget(pw,priority());

以独立语句将 newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

设计与声明

所谓软件设计,是“令软件做出你希望它做的事情”的步骤和做法,通常以颇为一般性的构想开始,最终演变成十足的细节,以允许特殊接口(interfaces)的开发。这些接口而后必须转换为 C++声明式。

让接口容易被正确使用,不易被误用

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

设计class犹如设计type

如何设计高效的classes呢?首先你必须了解你面对的问题。几乎每一个class都要求你面对以下提问,而你的回答往往导致你的设计规范:

  • 新type的对象应该如何被创建和销毁?这会影响到你的class的构造函数和析构函数以及内存分配函数和释放函数(operator new,operator new[],operator delete和operator delete[]——见第8章)的设计,当然前提是如果你打算撰写它们。
  • 对象的初始化和对象的赋值该有什么样的差别?这个答案决定你的构造函数和赋值(assignment)操作符的行为,以及其间的差异。很重要的是别混淆了“初始化”和“赋值”,因为它们对应于不同的函数调用。
  • 新type的对象如果被passed by value(以值传递),意味着什么?记住,copy构造函数用来定义一个type的pass-by-value该如何实现。
  • 什么是新type的“合法值”?对class的成员变量而言,通常只有某些数值集是有效的。那些数值集决定了你的 class 必须维护的约束条件(invariants),也就决定了你的成员函数(特别是构造函数、赋值操作符和所谓 “setter” 函数)必须进行的错误检查工作。它也影响函数抛出的异常、以及(极少被使用的)函数异常明细列(exception specifications)。
  • 你的新type需要配合某个继承图系(inheritance graph)吗?如果你继承自某些既有的classes,你就受到那些classes的设计的束缚,特别是受到“它们的函数是virtual或non-virtual”的影响。如果你允许其他classes继承你的class,那会影响你所声明的函数——尤其是析构函数——是否为virtual。
  • 你的新type需要什么样的转换?你的type生存于其他一海票types之间,因而彼此该有转换行为吗?如果你希望允许类型T1之物被隐式转换为类型T2之物,就必须在 class T1 内写一个类型转换函数(operator T2)或在 class T2 内写一个non-explicit-one-argument(可被单一实参调用)的构造函数。如果你只允许explicit构造函数存在,就得写出专门负责执行转换的函数,且不得为类型转换操作符(type conversion operators)或non-explicit-one-argument构造函数。(有隐式和显式转换函数的范例。)
  • 什么样的操作符和函数对此新 type 而言是合理的?这个问题的答案决定你将为你的class声明哪些函数。其中某些该是member函数,某些则否。
  • 什么样的标准函数应该驳回?那些正是你必须声明为private者。
  • 谁该取用新 type 的成员?这个提问可以帮助你决定哪个成员为 public,哪个为protected,哪个为 private。它也帮助你决定哪一个 classes 和/或 functions 应该是friends,以及将它们嵌套于另一个之内是否合理。
  • 什么是新type的“未声明接口”(undeclared interface)?它对效率、异常安全性以及资源运用(例如多任务锁定和动态内存)提供何种保证?你在这些方面提供的保证将为你的class实现代码加上相应的约束条件。
  • 你的新type有多么一般化?或许你其实并非定义一个新type,而是定义一整个types家族。果真如此你就不该定义一个新class,而是应该定义一个新的class template。
  • 你真的需要一个新type吗?如果只是定义新的derived class以便为既有的class添加机能,那么说不定单纯定义一或多个non-member函数或templates,更能够达到目标。
    这些问题不容易回答,所以定义出高效的classes是一种挑战。然而如果能够设计出至少像C++内置类型一样好的用户自定义(user-defined)classes,一切汗水便都值得。

宁以pass-by-reference-to-const替换pass-by-value

bool valid(Student s);
改为
bool valid(const Student & s);
这种传递方式的效率高得多:没有任何构造函数或析构函数被调用,因为没有任何新对象被创建。
以by reference方式传递参数也可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。

如果窥视 C++编译器的底层,你会发现,references往往以指针实现出来,因此pass by reference通常意味真正传递的是指针。因此如果你有个对象属于内置类型(例如int),pass by value往往比pass by reference的效率高些。对内置类型而言,当你有机会选择采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非没有道理。这个忠告也适用于STL的迭代器和函数对象,因为习惯上它们都被设计为passed by value

般而言,你可以合理假设“pass-by-value并不昂贵”的唯一对象就是内置类型和STL 的迭代器和函数对象。至于其他任何东西都请遵守本条款的忠告,尽量以pass-by-reference-to-const替换pass-by-value。
请记住

  • 尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题(slicing problem)。
  • 以上规则并不适用于内置类型,以及 STL 的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

必须返回对象时,别妄想返回其reference(不要返回引用)

class Rational{
  public:
  Rational(int n,int d):n(n),d(d){};
  private:
  int n;
  int d;

  friend
inline const Rational operator*(const Rational &lhs,const Rational &rhs){
  return Rational(lhs.n*rhs.n,lhs.d*rhs.d); //正确的做法
}
// const Rational& operator*(const Rational & lhs,const Rational & rhs){
//   Rational* r=new Rational (lhs.n*rhs.n,lhs.d*rhs.d); //糟糕的做法
//   return *r;
// }
};

绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款 4 已经为“在单线程环境中合理返回 reference指向一个local static对象”提供了一份设计实例。

将成员变量声明为private

一旦你将一个成员变量声明为public或protected而客户开始使用它,就很难改变那个成员变量所涉及的一切。太多代码需要重写、重新测试、重新编写文档、重新编译。从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。

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

宁以non-member、non-friend替换member函数

这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性。

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

如果你需要为某个函数的所有参数(包括被 this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member

考虑写出一个不抛异常的swap函数 (**)

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap。
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
  • 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。

实现

太快定义变量可能造成效率上的拖延;过度使用转型(casts)可能导致代码变慢又难维护,又招来微妙难解的错误;返回对象“内部数据之号码牌(handles)”可能会破坏封装并留给客户虚吊号码牌(dangling handles);未考虑异常带来的冲击则可能导致资源泄漏和数据败坏;过度热心地inlining可能引起代码膨胀;过度耦合(coupling)则可能导致让人不满意的冗长构建时间(build times)。

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

string encrypted;  //过早定义encrypted ,如果下面抛出异常 造成无用的构造 析构
if(password.length<MinLenght){
  throw logic_error("password is too short");
}

只要你定义了一个变量而其类型带有一个构造函数或析构函数,那么当程序的控制流(control flow)到达这个变量定义式时,你便得承受构造成本;当这个变量离开其作用域时,你便得承受析构成本。即使这个变量最终并未被使用,仍需耗费这些成本,所以你应该尽可能避免这种情形。

string encrypted(password); //通过copy构造函数定义并初始化
你不只应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。更深一层说,以“具明显意义之初值”将变量初始化,还可以附带说明变量的目的。

尽量少做转型动作

C++规则的设计目标之一是,保证“类型错误”绝不可能发生。
不幸的是,转型(casts)破坏了类型系统(type system)。那可能导致任何种类的麻烦,有些容易辨识,有些非常隐晦。
C++不是C,也不是Java或C#。在C++中转型是一个你会想带着极大尊重去亲近的一个特性。
转型的方式

  • C风格(T)expressionT(expression)
  • C++风格
    const_cast通常被用来将对象的常量性转除(cast away the constness)。它也是唯一有此能力的C+±style转型操作符。
  • dynamic_cast主要用来执行“安全向下转型”(safe downcasting),也就是用来决定某对象是否归属继承体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作(稍后细谈)。
  • reinterpret_cast意图执行低级转型,实际动作(及结果)可能取决于编译器,这也就表示它不可移植。例如将一个pointer to int转型为一个int。这一类转型在低级代码以外很少见。本书只使用一次,那是在讨论如何针对原始内存(raw memory)写出一个调试用的分配器(debugging allocator)时,见条款50。
  • static_cast用来强迫隐式转换(implicit conversions),例如将non-const对象转为const对象(就像条款3所为),或将int转为double等等。它也可以用来执行上述多种转换的反向转换,例如将 void*指针转为 typed 指针,将pointer-to-base转为pointer-to-derived。但它无法将const转为non-const——这个只有const_cast才办得到。

“由于知道对象如何布局”而设计的转型,在某一平台行得通,在其他平台并不一定行得通。这个世界有许多悲惨的程序员,他们历经千辛万苦才学到这堂课。

如果你发现你自己打算转型,那活脱是个警告信号:你可能正将局面发展至错误的方向上。如果你用的是dynamic_cast更是如此。
值得注意的是,dynamic_cast的许多实现版本执行速度相当慢。
之所以需要dynamic_cast,通常是因为你想在一个你认定为derived class对象身上执行derived class操作函数,但你的手上却只有一个“指向base”的pointer或reference,你只能靠它们来处理对象

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免 dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
  • 如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需将转型放进他们自己的代码内。
  • 宁可使用C+±style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。

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

如果const成员函数传出一个reference,后者所指数据与对象自身有关联,而它又被存储于对象之外,那么这个函数的调用者可以修改那笔数据。这正是bitwise constness的一个附带结果
如果它们返回的是指针或迭代器,相同的情况还是发生,原因也相同。References、指针和迭代器统统都是所谓的 handles(号码牌,用来取得某个对象),而返回一个“代表对象内部数据”的handle,随之而来的便是“降低对象封装性”的风险。同时,一如稍早所见,它也可能导致“虽然调用const成员函数却造成对象状态被更改”。
通常我们认为,对象的“内部”就是指它的成员变量,但其实不被公开使用的成员函数(也就是被声明为protected或private者)也是对象“内部”的一部分。因此也应该留心不要返回它们的handles。这意味你绝对不该令成员函数返回一个指针指向“访问级别较低”的成员函数。如果你那么做,后者的实际访问级别就会提高如同前者(访问级别较高者),因为客户可以取得一个指针指向那个“访问级别较低”的函数,然后通过那个指针调用它。
不论这所谓的 handle 是个指针或迭代器或 reference,也不论这个 handle 是否为const,也不论那个返回handle的成员函数是否为 const。这里的唯一关键是,有个handle被传出去了,一旦如此你就是暴露在“handle比其所指对象更长寿”的风险下。
这并不意味你绝对不可以让成员函数返回handle。有时候你必须那么做。例如operator[]就允许你“摘采”strings和vectors的个别元素,而这些operator[]s就是返回 references 指向“容器内的数据”(见条款3),那些数据会随着容器被销毁而销毁。尽管如此,这样的函数毕竟是例外,不是常态。

避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助 const 成员函数的行为像个 const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。

为“异常安全”而努力是值得的

当异常被抛出时,带有异常安全性的函数会:

  • 不泄漏任何资源
  • 不允许数据败坏

异常安全函数(Exception-safe functions)提供以下三个保证之一:

  • 基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态(例如所有的 class 约束条件都继续获得满足)。然而程序的现实状态(exact state)恐怕不可预料。举个例子,我们可以撰写changeBackground使得一旦有异常被抛出时,PrettyMenu对象可以继续拥有原背景图像,或是令它拥有某个缺省背景图像,但客户无法预期哪一种情况。如果想知道,他们恐怕必须调用某个成员函数以得知当时的背景图像是什么。
  • 强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会回复到“调用函数之前”的状态。
    和这种提供强烈保证的函数共事,比和刚才说的那种只提供基本承诺的函数共事,容易多了,因为在调用一个提供强烈保证的函数后,程序状态只有两种可能:如预期般地到达函数成功执行后的状态,或回到函数被调用前的状态。与此成对比的是,如果调用一个只提供基本承诺的函数,而真的出现异常,程序有可能处于任何状态——只要那是个合法状态。
  • 不抛掷(nothrow)保证,承诺绝不抛出异常,因为它们总是能够完成它们原先承诺的功能。作用于内置类型(例如 ints,指针等等)身上的所有操作都提供nothrow保证。这是异常安全码中一个必不可少的关键基础材料。

一般化的设计策略很典型地会导致强烈保证,很值得熟悉它。这个策略被称为copy and swap。原则很简单:为你打算修改的对象(原件)做出一份副本,然后在那副本身上做一切必要修改。若有任何修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的那个副本和原对象在一个不抛出异常的操作中置换(swap)。
“copy-and-swap” 策略是对对象状态做出“全有或全无”改变的一个很好办法,但一般而言它并不保证整个函数有强烈的异常安全性。

当“强烈保证”不切实际时,你就必须提供“基本保证”。现实中你或许会发现,你可以为某些函数提供强烈保证,但效率和复杂度带来的成本会使它对许多人而言摇摇欲坠。只要你曾经付出适当的心力试图提供强烈保证,万一实际不可行,使你退而求其次地只提供基本保证,任何人都不该因此责难你。对许多函数而言,“异常安全性之基本保证”是一个绝对通情达理的选择。

  • 异常安全函数(Exception-safe functions)即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
  • “强烈保证”往往能够以 copy-and-swap 实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
  • 函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

透彻了解inlining的里里外外

inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换之。 这样做可能增加你的目标码(object code)大小。在一台内存有限的机器上,过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随这些而来的效率损失。

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级(binary upgradability)更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。
  • 不要只因为function templates出现在头文件,就将它们声明为inline。

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

  • 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。
  • 程序库头文件应该以“完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及templates都适用。

继承与面向对象设计

解释 C++各种不同特性的真正意义,也就是当你使用某个特定构件你真正想要表达的意思。例如“public继承”意味 “is-a”,如果你尝试让它带着其他意义,你会惹祸上身。同样道理,virtual函数意味“接口必须被继承”,non-virtual 函数意味“接口和实现都必须被继承”。如果不能区分这些意义,会造成C++程序员大量的苦恼。

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

“public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

避免遮掩继承而来的名称

  • derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此。
  • 为了让被遮掩的名称再见天日,可使用 using 声明式或转交函数(forwarding functions)。

区分接口继承和实现继承

  • 声明简朴的(非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
  • 声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。

总结

  • 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
  • pure virtual函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性实现继承。

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

  • 使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式。它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。
  • 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
  • 以 tr1::function 成员变量替换 virtual 函数,因而允许使用任何可调用物(callable entity)搭配一个兼容于需求的签名式。这也是 Strategy设计模式的某种形式。
  • 将继承体系内的 virtual 函数替换为另一个继承体系内的 virtual 函数。这是Strategy设计模式的传统实现手法。
    总结
  • virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
  • 将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
  • tr1::function 对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式(target signature)兼容”的所有可调用物(callable entities)。

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

任何情况下都不该重新定义一个继承而来的non-virtual函数。

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

静态绑定又名前期绑定, early binding;动态绑定又名后期绑定, late binding。
virtual函数是动态绑定,而缺省参数值却是静态绑定。意思是你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值

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

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

复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。
在程序员之间复合(composition)这个术语有许多同义词,包括 layering(分层), containment (内含), aggregation(聚合)和embedding(内嵌)。

  • 复合(composition)的意义和public继承完全不同。
  • 在应用域(application domain),复合意味 has-a (有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。

明智而审慎地使用private继承

  • Private继承意味is-implemented-in-terms of(根据某物实现出)。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合(composition)不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

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

  • 多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  • virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
  • 多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。

模板与泛型编程

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

Templates 及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)移到前头了。
通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。
隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式(valid expressions)组成。
加诸于 template 参数身上的隐式接口,就像加诸于 class 对象身上的显式接口一样真实,而且两者都在编译期完成检查。就像你无法以一种“与 class 提供之显式接口矛盾”的方式来使用对象(代码将通不过编译),你也无法在template中使用“不支持template所要求之隐式接口”的对象(代码一样通不过编译)。
总结

  • classes和templates都支持接口(interfaces)和多态(polymorphism)。
  • 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
  • 对 template 参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

了解typename的双重意义

  • 声明template参数时,前缀关键字class和typename可互换。
  • 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。

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

可在derived class templates内通过 “this->;” 指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。

将与参数无关的代码抽离

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

运用成员函数模板接受所有兼容类型

  • 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
  • 如果你声明 member templates 用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。

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

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

请使用traits classes表现类型信息

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

认识template元编程

  • Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
  • TMP 可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码

定制new和delete

operator new和 operator delete只适合用来分配单一对象。A r r a y s 所用的内存由 operator new[]分配出来,并由 operator delete[]归还(注意两个函数名称中的[])。除非特别表示,我所写的每一件关于operator new和operator delete的事也都适用于operator new[]和operator delete[]。
最后请注意,STL 容器所使用的 heap 内存是由容器所拥有的分配器对象(allocator objects)管理,不是被new和delete直接管理。

了解new-handler的行为

当 operator new无法满足某一内存分配需求时,它会抛出异常。当 operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。为了指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler,那是声明于的一个标准程序库函数:

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

了解new和delete的合理替换时机

一些写自定义new和delelte的理由

  • 为了检测运用错误(如前所述)。
  • 为了收集动态分配内存之使用统计信息(如前所述)。
  • 为了增加分配和归还的速度。泛用型分配器往往(虽然并不总是)比定制型分配器慢,特别是当定制型分配器专门针对某特定类型之对象而设计时。Class专属分配器是“区块尺寸固定”之分配器实例,例如Boost提供的Pool程序库便是。如果你的程序是个单线程程序,但你的编译器所带的内存管理器具备线程安全,你或许可以写个不具线程安全的分配器而大幅改善速度。当然,在获得“operator new和operator delete有加快程序速度的价值”这个结论之前,首先请分析你的程序,确认程序瓶颈的确发生在那些内存函数身上。
  • 为了降低缺省内存管理器带来的空间额外开销。泛用型内存管理器往往(虽然并非总是)不只比定制型慢,它们往往还使用更多内存,那是因为它们常常在每一个分配区块身上招引某些额外开销。针对小型对象而开发的分配器(例如Boost的Pool程序库)本质上消除了这样的额外开销。
  • 为了弥补缺省分配器中的非最佳齐位
  • 为了将相关对象成簇集中。如果你知道特定之某个数据结构往往被一起使用,而你又希望在处理这些数据时将“内存页错误”(page faults)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这么一来它们就可以被成簇集中在尽可能少的内存页(pages)上。new和 delete的“placement版本”(见条款52)有可能完成这样的集簇行为。
  • 为了获得非传统的行为。

编写new和delete时需固守常规

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

写了placement new也要写placement delete

  • 当你写一个 placement operator new,请确定也写出了对应的 placementoperator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。
  • 当你声明placement new和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本。

杂项讨论

不要轻忽编译器的警告

  • 严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”的荣誉。
  • 不要过度倚赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本倚赖的警告信息有可能消失。

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

智能指针(smart pointers)tr1::shared_ptr和 tr1::weak_ptr。前者的作用有如内置指针,但会记录有多少个tr1::shared_ptrs共同指向同一个对象。这便是所谓的reference counting(引用计数)。一旦最后一个这样的指针被销毁,也就是一旦某对象的引用次数变成 0,这个对象会被自动删除。这在非环形(acyclic)数据结构中防止资源泄漏很有帮助

tr1::function,此物得以表示任何callable entity(可调用物,也就是任何函数或函数对象),只要其签名符合目标。
tr1::bind,它能够做STL绑定器(binders)bind1st和bind2nd所做的每一件事,而又更多。
Hash tables,用来实现sets,multisets,maps和multi-maps。每个新容器的接口都以其前任(TR1 之前的)对应容器塑模而成。
正则表达式(Regular expressions),包括以正则表达式为基础的字符串查找和替换,或是从某个匹配字符串到另一个匹配字符串的逐一迭代(iteration)等等。
Tuples(变量组),这是标准程序库中的pair template的新一代制品。pair只能持有两个对象,tr1::tuple可持有任意个数的对象。
tr1::reference_wrapper,一个“让 references 的行为更像对象”的设施。它可以造成容器“犹如持有references”。而你知道,容器实际上只能持有对象或指针。
随机数(random number)生成工具,它大大超越了rand,那是C++继承自C标准程序库的一个函数。
数学特殊函数,包括Laguerre多项式、Bessel函数、完全椭圆积分(complete elliptic integrals),以及更多数学函数。

让自己熟悉Boost

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-19 07:48:45  更:2021-09-19 07:50:40 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 -2024/12/28 13:49:39-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码
数据统计