introduction 导读
1.对高效使用C++的两类忠告 :
- 一般性的设计策略
- 带有具体细节的特定语言类型
2.术语:
- 声明(declaration)与定义(definition): 声明是告诉编译器某个元素的名称和类型,但略去细节。定义则是使编译器为程序元素分配空间。
- 二者的区别: 注意区分声明和定义式,二者最根本的区别就是是否分配内存,声明不会导致内存的分配,而定义会分配内存。在C++程序中声明可以有多次,但是定义只能有一次。因此不能将变量的定义放置于头文件中,由于头文件会被多次引用,就会导致变量在多个源文件中被重复定义,这是C++所不允许的。但是也有例外的情况,以下3种定义可以放入头文件中:
a. 类的定义。
b. const变量的定义。因为const常量的作用域仅限于定义它的文件,所以可以在多个源文件中出现它的定义。
c. inline函数。
如: extern int x;
class A;
是一个对象(object)的声明式(extern 关键词用于暗示编译器去后面寻找对象的具体定义)
- 二者的联系: 定义也是声明, 因为当定义变量时我们也向程序表明了它的类型和名字;但声明不是定义, 可以通过使用extern关键字声明变量而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern;
- 特殊情况下声明可以充当为定义,除非是以下几种情况:
(1) 函数原型(无函数体的函数声明);
(2) 包含extern关键字并且没有初始化变量、对象或函数体,例如:
extern int i; //声明
extern int p = 123; //定义
(3) 没有下列定义的类名声明,如Class T;
(4) 类声明中的静态数据成员。例如:
class abc{
static const int i = 10; //常量声明式
int ui[i]; //使用该常量
};
- 值得注意的是,你看到的是i的声明式而非定义式。C++会要求你为所使用的任何东西提供定义式,但是如果它是class的专属常量且是static且为int类型时,可以区别对待。
但对于上述情况,如果需要取某个class专属常量的地址,或者编译器坚持要看到一个定义式来明白该常量是什么,此时必须提供额外的定义式如下:
const int abc:: i;
将该式子放入一个实现文件而非头文件,由于其已在声明时获得初值10,定义时不能再设初值。
- size_t:是一个typedef,在C++中用于计量。其定义是:“适于计量内存中可容纳的数据项目个数的无符号整数类型”
- size_t由来: 在C++中,设计 size_t 就是为了适应多个平台的 。size_t的引入增强了程序在不同平台上的可移植性。不同系统上,定义size_t可能不一样
经测试发现,在32位系统中size_t是4字节的,在64位系统中,size_t是8字节的。在32位系统上 size_t定义为 unsigned int ,也就是说在32位系统上是32位无符号整形。在64位系统上定义为 unsigned long ,也就是说在64位系统上是64位无符号整形。
其定义如下:
#ifdef _WIN64
typedef unsigned __int64 size_t;
typedef __int64 ptrdiff_t;
typedef __int64 intptr_t;
#else
typedef unsigned int size_t;
typedef int ptrdiff_t;
typedef int intptr_t;
#endif
可以看到其实size_t就是使用了typedef别名的unsigned int
- size_t的大小:其大小是由系统的位数决定的, 可以理解为是由你生成的程序类型决定的,只是生成的程序类型与系统的类型有一定关系。32bits的程序既可以在64bits的系统上运行,也可以在32bits的系统上运行。但是64bits的程序只能在64bits的系统上运行。然而我们编译的程序一般是32bits的,因此size_t的大小也就变成了4个字节。
3.使用关键词explicit 来声明构造函数:
- 被声明为explicit的构造函数比non- explicit构造函数更受欢迎,因为其禁止编译器执行非预期(往往也不被期望)的类型转换。因此,习惯的遵循这样的策略
- 除非有一个好的理由允许构造函数被用于隐式类型转换,否则将其声明为explicit。
4.复制构造函数(copy构造函数)
- copy构造函数被用来 “以同型对象初始化自我对象”,copy assignment operator(拷贝赋值操作符)被用来 “从另一个同型对象中拷贝其值到自我对象”
- 示例:
class Widget {
public:
Widget(); // default构造函数
Widget(const Wideget& rhs); // copy构造函数
Widget& operator=(const Widget& rhs); // copy assignment operator
};
Widget w1; // 调用default构造函数
Widget w2(w1); // 调用copy构造函数
w1 = w2; // 调用copy assignment operator
Widget w3 = w2; // 调用copy构造函数,注意与上面的赋值区分,有新对象被定义,必然有构造函数被调用,而不会是赋值操作符
5.对于接口:
- 什么是接口:接口就是规定并描述了要程序做什么(行为和功能),但不在其中完成其实现。
- 对于C++来说,接口是通过抽象类来实现的,如果一个类中至少有一个函数被声明为纯虚函数,那么这个类就是抽象类,纯虚函数式通过在声明中使用“=0”来指定的,其示例如下:
class Box
{
public:
// 纯虚函数
virtual double getVolume() = 0;
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
- 设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
- 可用于实例化对象的类被称为具体类。
- 因此,在派生类中重写纯虚函数也即对应方法在接口下的具体实现。
第一章 : Accustoming Yourself to C++ ——让自己习惯C++
条款01: 视C++为一门联邦语言
1.C++ 不单单是在C语言的基础上增加了一些面向对象的特性,可以将其视为一门联邦语言,本质上是一门多重泛型编程语言——一个同时支持过程形式(procedural),面向对象形式(OOP), 函数形式(functional),泛型形式(generic),元编程形式(metaprogramming)的语言,包括以下四个核心的次语言内容:
- C++以C语言为语言基础,是C的超集
- 面向对象的C++,也就是C with Classes所诉求的
- Template C++ 也就是C++ 泛型编程的部分,也是大多数人较难掌握,缺乏经验的部分
- STL程序库(template程序库,包括容器、迭代器、算法和函数对象)
2.请记住:C++高效编程守则视具体情况而变化,取决于你使用C++的哪一个部分。
条款02: 尽量以const,enum,inline替换 #define(宁可以编译器替换预处理器)
1.#define 不具有任何的封装性
- 一旦宏被定义,就全局可见(除非在某处被#undef)。这也就意味着#define不仅不能用来定义class专属常量,也不能够提供任何封装性。
2.enum hack
- enum hack 是枚举基础, 其理论基础是:一个属于枚举类型(enumerated type)的数值可权充ints被使用。
- 背景是:为了防止定义的值被拷贝多份, 需要我们在类内部声明 const static 常量, 但是基于旧的编译器, 不允许static成员变量在其声明式上获得初值, 所以不得以会将初值放到定义式(也就是具体实现)里面。
- 但是特殊情况下,例如数组声明式中,我们(编译器)坚持需要确切的知道数组的大小,我们就需要使用the enum hack 补偿法。
- 示例:
class GamePlayer {
private:
enum {NumTurns = 5}; //"the enum hack" ---令NumTurns 成为5的一个记号名称
int scores[Numturns];
};
- 为什么要认识enum hack?
- 第一,enum hack的行为某方面说比较像#define而不像const, 有时候这正是你想要的。 例如取一个const的地址是合法的, 但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获得一个pointer或者reference指向你的某个整数常量,enum可以帮你实现这个约束。也就是你想使用#define的特性,而又不接受#define内存复制的问题, enum hack就是你最好的选择。
- 第二, 纯粹为了实用主义,很多代码用了它,所以看到它,你必须认识它,而且了解为什么要这么用,知其然,知其所以然很重要。事实上,enum hack是template metaprogramming(模版元编程)的基础技术。
3.inline函数
- 定义: 定义函数时,在返回值类型前面加上 inline 关键字,增加了inline关键字的函数被称为“内联函数”。内联函数和普通函数的区别在于:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。
- 而上述的这一点功能上类似于宏展开, 但是比宏展开好的地方是,inline发生在编译阶段,会做类型检查,消除了宏展开可能带来的语义隐患。例如定义宏
#define f(x, y) (x*y
)就会在f(x+1,y)
的时候f(x,y)
就变成了x+1*y
,完全错误。用inline可以达到相同的意图,却不会产生错误。 - 函数加上inline的好处: 可以节省调用的开销,而且能够便于编译器和上下文配合做优化。
- 注意:inline只是程序员给编译器的建议,如果函数题过于复杂,编译器也会视情况忽略inline的使用
- 总结:inline主要用在比较简单,且可能会被多次调用的函数,如简单的数学表达式,还有面向对象时读取类成员get和set等,其优点在于节省调用的开销,方便优化,能够消除宏带来的隐患。因此,对于形似函数的宏(macros),最好该用inline函数替换#define,如果涉及到inline的可见和封装性,可以将其设计在类的priavte成员中加以控制。
4.请记住:
- 对于单纯常量,最好以const对象或enums替换#define
- 对于形似函数的宏(macros),最好该用inline函数替换#define
条款03:尽可能使用const
1.const的使用范围:
- 在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数、或block scope中被声明为static的对象
- 在classes内部修饰static和non-static成员变量
- 面对指针,可以指出指针自身、指针所指物,或两者都是(不是)const
2.const修饰指针与常量
- 示例:
const char * myPtr = &char_A;//指向常量的指针
char * const myPtr = &char_A;//常量的指针
const char * const myPtr = &char_A;//指向常量的常量指针
- 常量指针(Constant Pointers)
因为*
操作符是左操作符,左操作符的优先级是从右到左,对于:int * const p
先看const再看*
,是p是一个常量类型的指针,不能修改这个指针的指向,但是这个指针所指向的地址上存储的值可以修改。 - 指向常量的指针(Pointers to Constants)
对于:const int *p
先看*
再看const,定义一个指针指向一个常量,不能通过指针来修改这个指针指向的值,但可以修改该指针指向的地址 - 指向常量的常量指针(Constant Pointers to Constants)
对于:const int* coonst p
表示一个不能修改指向的指向常量的指针
3.令函数返回一个const类型值,能够预防很多无意义的赋值动作
4.const成员函数
- const对象只能访问const成员函数,而非const对象可以访问任意的成员函数
- const成员函数不能修改对象的数据成员,const对象的成员变量不可以修改(mutable修饰的数据成员除外)
5.bitwise constness 和 logical constness:成员函数是const时站在编译器的角度看问题
- 使用关键词mutable能够解决在const成员函数内更改成员变量的需求,但不能解决所有的相关问题
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复(使用转型,条款27提及)
也就是说,当两个函数(一个const,一个non-const除了const修饰返回类型以外,其内部实现完全相同时),为了减少代码的重复以及相应的编译,维护问题,可以通过使用casting(转型)来”运用const成员函数实现出其non-const孪生兄弟“ - 注意:上面谈到的都是通过non-const函数调用const函数,而不能进行反向操作,因为const成员函数承诺绝不改变其对象的逻辑状态,而non-const函数并没有如此承诺。
6.请记住:
- 将某些object声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
- 编译器强制实施bitwise constness,但编写程序时应该使用“概念上的常量性”,而且要站在编译器的角度去看问题
- 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本避免代码重复(使用转型,条款27提及)
条款04: 确定对象被使用前已先被初始化
1.永远在使用对象之前将它初始化
2.区分在构造函数 成员初始化中的赋值(assignment)与初始化(initialization)
3.为了规范与高效,总是使用成员初值列(member initialization list),并总是在初值列中列出所有成员变量(以免遗漏)
4.当你在成员初值列中条列各个成员时,最好总是以其声明次序为次序
5.不同编译单元内定义之non-local static对象的初始化次序问题
- static对象的寿命从被构造出来知道整个程序结束为止
- non-local static对象是指,非某个函数内定义的local-static对象,也即该对象是global或位于namespace作用域内,抑或是在class内或file作用域内被声明为static(总之就是不在函数内部被声明)
- 所以真正的问题是:如果某编译单元(通常指单一源码文件加上其所#include的头文件)内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未被初始化,因为C++对与这个初始化次序并无明确定义(因为不可能在程序设计之初就将初始化次序定死,也不值得耗费精力去这样做)
- 上述问题的解决方法是:reference-returning ——将每个non-local static对象搬到自己的专属函数内,使该对象在此函数内被声明为local static。这些函数返回一个reference指向它所含的对象,然后用户调用这些函数,而不直接指涉这些static对象自身。
- 基于该方法,C++即可保证函数内的local static 对象会在“该函数被调用期间”“首次遇上该对象之定义式”时被初始化。
6.请记住:
- 为内置型对象进行手工初始化,因为C++不保证初始化他们
- 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作(这样会调用default构造函数,再调用copy assignment赋值操作符)。初值列列出的成员变量,其排列次序应该和他们在class中的声明次序相同。
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象
第二章:构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
1.在你自己没有声明任何copy构造函数的情况下,编译器为你默认生成的copy构造函数在进行参数初始化时有两种方式:
- 实例:
NameObject<int> no1 (Smallest Prime Number, 2);
NamedObject<int> no2(no1); //调用copy构造函数(在之前类内没有声明任何copy构造函数,所以默认调用编译器为你默认生成的copy构造函数)
- 对于有copy构造函数的类型(例子中的string):调用对应的copy构造函数,以copy的原型值为实参
- 对于内置类型(例子中的int 内置类型):拷贝原型之内的每一个bits
2.如果你打算在一个“内含reference成员或const成员”的class内支持赋值操作(assignment),你必须自己定义copy assignment操作符
3.请记住:
- 编译器可以暗自(silently)为class创建default构造函数,copy构造函数,copy assignment操作符,以及析构函数(编译器产出的是non-virtual的——见条款07),而且如果你自己没有声明,那么这些默认创建的函数都是public且inline的。