item1:view C++ as federation of languages.
C++被认为是四个次语言的集合:
C。预处理器、内建类型、数组、指针等概念
面向对象C++。类、封装、继承、多态、虚函数、动态绑定等概念
模板C++。泛型编程
STL。模板库,包含容器、迭代器、算法、函数,并对做他们之间的关系做出了规约。
一个次语言适用的规则在另一个次语言中就不一定使用了。因此当将C++分为四个次语言后,我们可以逐个突破,记住不同次语言中的守则!
item2:prefer const、enum、inline to #define
预处理器使得在编译器处理源码前名称记号就被移走了,直接替换为数值。名称记号没有加入到记号表。这是编译器产生的错误都是直接以数值表现,而不是以名称表现。对于调试来说,也不方便。因此,提倡使用常量。
使用常量的两个特例:
1、常量指针声明,常量通常被包含在头文件以供不同程序文件所使用,因此对于一个常量指针来说,常量的意义不仅在于指针所指向的内容不能改变,指针的值也不能被改变,这才满足常量的定义!
2、类的专属常量,既然是常量,因此希望对于所有的类对象来说这个常量只有一份实体!因此需要被声明为类的静态成员,这也是为啥常看到const 和 static 需要一起使用。
静态成员需要在实现文件中进行定义,且由于在声明时已经初始化了,因而在定义时不可以再进行初始化。
enum hack:
枚举类型的数值可充当int来被使用!
特性:不可对枚举值取地址;不能以指针或是引用方式指向枚举值
使用#define进行宏定义,这可能带来程序执行的不确定性,解决之道仍是使用模板内联函数取代宏定义,内联保证了效率,模板使得不限制参数类型。
item3:use const whenever possible(多多使用const)
就是为对象增加一层语义信息!编译器能够理解!并能够帮助检测可能造成对象值发生变化的行为!
1、const修饰指针
const可保证指针指向的对象不变:const char * pc (等同于char const * pc)
const可保证指针自身的值不变:char * const pc
const保证指针和指针指向的对象均不变:const char * const pc
2、const修饰STL中的迭代器
迭代器类似于指针,因此可分为 迭代器本身值不可变 、迭代器所指向的值不可变 、指针和指向均不可变的情况
const std::vector<int> ::iterator it = a.begin();
std::vector<int>::const_iterator it1 = a.begin();
3、const用在函数返回类型、参数列表、函数自身时效果
若函数返回内建类型,编译器会保证不存在能修改返回值的行为的存在!但是对于自定义类型,就无法保证了,若是希望我们所返回的自定义类型不被修改,在返回类型前添加const修饰!
const在修饰参数列表时,除非需要修改参数,否则添加const
const在修饰返回类型时,若是希望返回的值不被客户后续修改,则添加const
const修饰函数自身是在说const成员函数:
1、为什么要有const成员函数?
C++从效率出发(对于面向对象子语言来说,函数参数建议使用const 引用类型的对象,因此我们的程序中必定大量存在这样的变量,针对const对象,编译器应该以何种快速方式确保对象所调用的函数不会修改对象?const成员函数提出由来),指定了const对象只能调用const成员函数,因此多了一种这样的成员函数类型。因此,编译器会检查在const成员函数内是否存在修改对象数据成员的行为,一旦有,编译器会为我们指出错误。
便于阅读代码,维护者可以很清楚的知道哪些成员函数不会修改对象,哪些会修改!
2、使用const的一些问题:
下面这个例子有个问题,const成员函数内没有修改对象,编译也能过去,但是客户在类外修改了对象的数据成员,可见并没有完全符合const成员函数定义,这也称为物理constness。为了修改这个问题,将返回类型声明为const类型引用,不允许客户在类外修改数据成员。
#include "stdafx.h"
#include<vector>
#include<iostream>
class test {
public:
test(const char *s) {
int n = strlen(s);
c = new char[n];
int i;
for ( i = 0; i < n; i++)
c[i] = s[i];
c[i] = '\0';
}
char & operator[](int i) const {
return c[i];
}
void display() const {
std::cout << c<<std::endl;
}
//const static int size = 10;
private:
char *c;
};
int main()
{
test t("lxyhhhh");
t[2] = 'c';
t.display();
std::cin.get();
return 0;
}
与物理constness对应的是逻辑constness!这是由于编译器的const成员函数限制太严了(类的所有数据成员都不能被修改),但是有些类数据成员不能代表类对象状态,我们需要修改这样的数据成员!同时我们又能保证影响对象状态的数据成员的值不会被修改!这相当于把数据成员分为两类,能表示类状态的和不能表示类状态的!在编译器使用mutable修饰数据成员后可以使得被mutable修饰的数据成员在const成员函数内可以被修改。
我们在编程过程中,其实要确保逻辑constness,客户不能修改察觉到数据成员的变化!
#include<cstring>
class Textcache {
public:
int length() const;
private:
mutable bool _iscache;
mutable int _len;
char *pc;
};
int Textcache::length() const
{
if (!_iscache)
{
_iscache = true;
_len = std::strlen(pc);
}
return _len;
}
成员函数实现完全相同时,仅是特征标的不同,仅函数实现较为复杂,分别编写两个函数代码冗余,效率低。如何解决这个问题?
实现一份const成员函数,在非const成员函数中调用const成员函数。为了实现调用,设计类型转化工作:
首先在非const函数内,将对象转化为const对象后调用该方法,否则将调用非const函数。这里可使用static_cast<>转化,此外,我们的const成员函数返回的是cosnt对象的引用,而我们的非const成员函数返回的是非const类型的引用,因此在这里做一个const丢弃,可利用const_cast<>。
反向处理,即实现了非const方法,利用非const方法实现const方法是坚决杜绝的!因为不能保证非const成员函数内没有修改数据成员!
const char & operator[](int i) const {
return c[i];
}
char& operator[](int i) {
return const_cast<char &>(static_cast<const test&>(*this)[i]);
// return const_cast<char &> static_cast<const test&>(*this)[i]; 错误写法 最外层丢弃const时 没加括号
}
item4: make sure that objects are initialized before they’re used.
C++并不能保证对象有初始值,因此在使用对象前确保其有初值。没有初值会我们的程序带来不确定的影响,安全性差,排除bug也麻烦!
关于初始化:
1、对于内建类型的对象,使用前手动赋予初值
2、对于用户自定义类型,初始化的重任由构造函数完成,这也就保证定义对象时,他的数据成员总是有默认值的。
对于构造函数来说,其内部实现有两种方式:对数据成员先初始化后再赋值(伪初始化)、利用初始化表直接进行初始化(真正初始化)。为什么这么说呢?其实编译器会对我们所写的构造函数做修改,在进入构造函数的函数体内前,先对成员做初始化。因此,才有了赋值和直接初始化(覆盖编译器为我们提供的默认初始化行为)。所以,提倡使用成员初始化表!为了保持一致性,将内建类型的成员也写入到成员初始化列表中!因此,直接把所有的数据成员都以成员初始化列表进行初始化是最安全(以免遗漏内建类型的成员变量或是对于const、reference类型的内建对象来说必须使用初始化列表,不能使用赋值的方式)高效的行为。
然而对于类中有许多的重载的构造函数,为了减少重复的工作量,可以将内建类型的对象赋值操作提炼到一个函数内,在构造函数内调用该函数以完成内建类型对象的初始化。
3、成员初始化的顺序是固定的
显示初始化基类对象,再初始化派生类对象!无论在构造函数内成员初始化列表的次序如何,成员初始化的次序都是固定的!只与成员声明的顺序有关!这里有个小建议:初始化列表中成员出现的次序与成员声明的次序一致!这是因为有的成员变量的依赖另一个成员变量,因此被依赖的变量需要先被初始化!
4、不同编译单元内的non-local-static对象的初始化次序
如何理解static?
自被定义起到程序运行(main()执行完成后)结束前,会一直存在!因此对于堆栈型的对象来说,他们都不是static的。那么哪些是静态的对象呢?global对象、位于namespace的对象、以static修饰的函数、类、文件作用域中的对象。什么是local-static对象呢?函数中以static声明的对象。除此之外都是non-local -static对象。
对于local-static对象(函数内定义的static对象),编译器确保当该函数被执行时,会为函数内的static变量做初始化操作!
如何理解编译单元:
头文件+源码所形成的目标文件(.obj)
当存在至少两个以上的编译单元且其中包含至少两个相关联的non-local-static对象时,比如一个static变量的初始化依赖于另一个静态变量,可是C++并未提供明确的不同编译单元内的non-local-static对象的初始化次序(主要是次序问题),这就非常有可能带来不明确的行为!
比如下面这个例子中,如何确保在初始化tempir对象前,先初始化tfs对象呢?
解决方法就是将non-local-static对象变为local-static对象!使得通过调用相应的函数时,对象得到初始化!由于我们可以决定函数调用的次序!因而可以保证以合适的顺序对存在依赖关系的static对象进行初始化!注意返回的是local-static 的引用!
item5:know what functions C++ silently writes and calls
编译器能为我们提供哪些函数?
默认构造函数、默认析构函数、拷贝构造函数、赋值运算符函数,而且都是公有的!在需要使用这些函数,而类中并未提供时,编译器可以为类增加这些函数的实现。对于构造函数来说,参数类型个数可以是任意的,但是拷贝构造函数、赋值运算符函数的参数只能是类对象,这样的参数配置是从函数功能上出发的。
各个函数功能:
默认构造函数:调用各个non-static数据成员的构造函数、所有基类构造函数
默认析构函数:调用所有基类的析构函数,各个non-static成员的析构函数,比较特别地,当基类的析构函数被声明为虚函数,编译器为我们派生类提供的默认析构函数也是虚函数。
拷贝构造函数:逐成员(non-static)进行复制,成员若是class,则调用相应的拷贝构造函数(相对于通过赋值来进行拷贝效率更高)
赋值运算符函数:与拷贝构造函数相比,当类的数据成员存在const类型、引用类型、基类的拷贝构造函数被声明为私有成员时,编译器无法为我们添加赋值运算符函数。对于const类型的对象,一旦被初始化后是不允许再次对其赋值的,因此编译器无法为这种类型的数据成员进行赋值操作。引用类型的对象只能关联一个对象,之后对引用对象所做的赋值操作,是改变所指向的对象的数据值,而不是改变所引用对象,因此编译器也很困惑,不知道该如何处理,于是选择不提供默认的赋值运算符函数。当基类的赋值运算符函数被声明为私有的,在派生类中则无法调用基类的赋值运算符函数,所以针对这个情况,编译器也无法提供赋值运算符函数。
比较拷贝构造函数和赋值构造函数,一个是进行初始化,编译器无条件的提供(特例,当基类的拷贝构造函数是私有的,也无法提供),一个是赋值,因此对于一些不支持赋值的数据成员和赋值运算符函数是私有的,编译器就不会提供默认的赋值运算符。
item6:explicit disallow the use of compiler-generated functions you do not want to
一些类的设计逻辑是需要禁止拷贝构造函数或是赋值运算符的!我们就是不希望对象间发生复制操作,但是编译器总会为我们生成默认的拷贝或是赋值函数,如何应对这种情况呢?
为类声明私有的拷贝构造函数和赋值运算符函数,如此编译器就不会为我们生成默认的拷贝构造函数和赋值运算符函数,在类外也无法使用拷贝构造函数和赋值运算符函数了,同时保证只是声明函数不给出定义,防止友元函数和成员函数误用这两种函数,如果存在调用拷贝构造函数和赋值运算符函数的行为时,在连接期会给我们错误提示!
除此之外,还有一种方法可以使得禁止生成拷贝构造函数和赋值运算符函数,创建一个基类,将拷贝构造函数和赋值运算符函数声明为私有成员!对于不希望提供拷贝构造函数和赋值运算符函数的类,可以继承此基类,编译器试图生成类的拷贝构造函数和赋值运算符函数时,会发现基类的这两个函数由于是私有成员而无法访问,因此编译器就不会为我们生成默认的拷贝构造函数和赋值运算符函数。
item7:declare destructors virtual in polymorphic base classes(这里还是需要在理解多态之后回来再看看)
何时需要声明虚析构函数?
带有多态性质的基类(通过基类的接口处理派生类对象)才需要声明一个虚析构函数!也可以理解为当类中至少包含一个虚函数时就需要将析构函数声明为虚函数!
如果不将基类的析构函数声明为虚函数,将导致释放基类对象派生类对象中的成员没有被释放,出现未定义的行为。但是析构函数定义为虚函数后,释放基类指针时将调用所指向派生类的析构函数,而在该函数内将有条不紊的先释放派生类的数据成员再调用基类的析构函数。
作为基类被继承,但没有声明虚函数(不是作为多态而存在),比如我们之前创建的Uncopyable基类存在的意义是为了不生成默认的拷贝构造函数和赋值运算符函数!就不需要将析构函数声明为虚函数!
对于那些不做为基类的类,就不需要将析构函数声明为虚函数!
为什么提倡不必要时不将析构函数声明为虚函数?
对于有虚函数的类来说,其对象不仅仅包含数据成员,还存储了一个指针(指向该类的虚函数表),虚函数表中可能的实现方式是函数指针数组,不同对象通过这个指针找到虚函数表,进而确定所调用的函数。可以看出带有虚函数的类对象存储空间将更大,函数调用机制更为繁琐!所以类的本身就不打算实现多态可别多此一举将析构函数声明为虚函数,莫名降低效率!
希望将基类变为抽象基类,这要求至少包含一个纯虚函数,若是没有合适的纯虚函数,我们可以将析构函数声明为纯虚函数,但是由于基类析构函数总是会被派生类的析构函数所调用,因此必须要定义纯虚函数。如果不定义,连接期由于无法找到基类析构函数的定义,产生bug。
item8:prevent exception from leaving destructors
C++不鼓励在析构函数内抛出异常
但确实在析构函数中可能引发异常,析构函数内引发的异常,如果不采取处理而是向上抛出,可能产生未定义的行为,该如何处理?
对于在析构函数中可能出现的异常,不妨思考能否将其产生异常的行为放入到一个新的接口函数中供用户调用,让用户来对异常进行处理。同时在析构函数中也保证消化异常。
item9:never call virtual functions during constructors or destructors
创建派生类对象过程中,其基类的构造函数所调用的虚函数绝不是派生类中所实现的函数,不可能达到预期效果!
创建b时,调用构造函数,先调用基类的构造函数,在基类函数内调用虚函数,但此时虚函数不会是派生类实现的函数!在派生类对象的基类对象构造期间,对象的类型为基类类型,因此无法调用派生类实现的虚函数。所以说,对象在未执行到派生类构造函数主体之前,所属于的类型是基类类型,而不是派生类类型。
确保构造函数和析构函数内不直接或是间接调用虚函数。(准确说是基类的构造函数不要调用虚函数,但是有可能在后续的开发中再加入新的派生类,因此最安全的操作就是所有的构造函数都不要调用虚函数)
基类设计初衷:创建一个交易,就会调用一个记录函数,且这个函数实现能够根据派生类的不同而变化!这个思路的需要虚函数的支持,但是在基类使用虚函数不是虚函数,无法达成我们的目的。如何修改?在派生类构造方法传递参数给基类,基类利用参数实现“多态”,通过向基类传递参数不再需要虚函数了。
item10:having assignment opereators return a reference to *this
为了实现连锁赋值效果,在设计类的赋值运算符时,返回左操作数!同样适用于所有与赋值运算符相关的各类运算:+=、=、*=。对于赋值运算符来说,算是公认的守则,所有标准库都遵守,因此在设计自己的类时,也需要遵守起来!
item11:handle assignment to self in operators=
潜存的赋值行为:多个指向同类型的指针或引用对象,可能存在着指向同一个对象的情况。又或是具有继承关系的类,指向基类的指针所指向的对象和指向派生类的指针指向是同一个对象(即便类型不一样,但变量值是相同的)!在编写函数时,涉及多个指针或是引用指向同一类型对象或是具有派生关系的对象,要格外注意所指向的对象是否是同一个!避免删除指针所向的内存空间后又通过另一个指针使用!
错误的赋值例子,若两个指针指向同一个对象,那么将导致另一个指针指向一个被删除的内存空间!在new时就会抛出异常,函数不断向上返回匹配try语句。
增加认同测试,判断指针的值是否相同,避免删除对象的数据成员,但是对异常没有做处理,如果内存空间不足或是调用bitmap的拷贝构造函数出现了异常,对象的数据成员仍然是指向一个被删除的空间,问题就出在先删除了对象的数据成员:
修改版本1!利用一个副本,别一开始就删除数据成员,但凡是new异常了,函数返回上级寻找try,但此时对象的指针的所指向的对象仍是原先的对象!而且能够应对同一个对象间的赋值!
修改版本2!copy and swap (之后再补充)
item12:copy all parts of an object
不采用编译器为我们提供的copy函数,就要确保自己编写的copy函数确实对所有的数据成员进行复制操作!编译器实现的版本会保证调用基类的copy函数来copy基类的数据成员,而我们自己编写函数时会忘记这一点,需要格外注意!
对于面向对象的系统的来说,copy 构造函数和copy assignment 函数绝对是必不可少的!统称为copy函数!功能就是实现逐一复制对象的所有数据成员!所以当改变类的数据成员后,一定别忘记修改copy函数、构造函数、非标准的assignment 运算符函数!
另一个易错点!当类存在继承关系,没有对派生类对象中的基类对象进行copy!所有数据成员自然也包含从基类继承来的数据成员!但是基类的数据成员在派生类中也无法直接访问到,因此需要调用从基类继承来的protected或是public方法对基类的数据成员进行copy。最简单的就是调用基类相应的copy函数。
同样注意,初始化和赋值的区别,一个是对象还未创建,一个是对现存对象的修改!初始化派生类对象时与普通的构造函数类似,需要调用基类的构造函数初始化出基类的对象,既然是初始化,我们直接在初始化列表中调用基类的copy 构造函数即可!相对于使用默认的构造函数在函数体内进行copy assignment 来说效率更好!因此,不能混淆调用copy assignment 和 copy constructor 函数!
注意一下派生类的copy assignment 调用 基类的copy assignment 写法。