- 声明的定义
- 导入C头文件和C头文件的区别
- explicit阻止编译器执行隐式类型转换隐式执行构造函数
- 拷贝构造函数和拷贝赋值操作符
- 命名习惯
- operator重载
- C多线程
- C常见的三个库
- 条款1视C为一个语言联邦
- 条款2尽量使用constenuminline替代define
- 类专属成员的定义static成员静态成员
- 类专属成员静态成员static成员的声明和定义形式
- 条款3尽可能使用const
- 条款4确定对象被使用前已先被初始化
- 条款7为多态基类声明virtual析构函数
- 如果没有将多态基类的析构函数声明为virtual
- 将多态基类的析构函数声明为virtual的目的
- 总结将析构函数声明为virtual是为了激活动态绑定用对应子类的析构函数来执行析构操作
- 任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数
- 当一个对象不存在virtual函数时往往表示它并不希望自己成为基类并不希望被继承
- 当一个类不会被继承的时候让它的析构函数成为virtual是不良的设计因为会带来额外的开销和无用处的virtual标记
- 总结当class内包含virtual时就把析构函数声明为virtual否则不要声明为virtual析构
- 纯虚析构函数virtual Foo 0可以确保动态绑定调用子类的析构函数
- 注意纯虚函数所在的类是纯虚类
- 条款8别让异常逃离析构函数
- 条款9绝对不要在构造和析构函数中调用virtual函数
- 条款10令operator返回this
- 条款11处理自我赋值
- 条款12复制对象时不要忘记复制其基类部分
- 资源
- 条款13以对象管理资源
- 条款14在资源管理类中小心copying行为
- 条款15在资源管理类中提供对原始资源的访问
- 允许对象进行直接隐式转换的方法
- 条款16成对使用new和delete时采用相同的形式带或不带
- 条款17以独立语句将new对象装入智能指针
- 条款18让接口不易被误用
- 条款20对于对象尽量使用pass-by-reference-to-const替代pass-by-value对于内置类型以及迭代器函数对象应该使用pass-by-value而不是reference
- 条款21不要返回绑定了函数内的local_stack或heap对象的引用指针
- 条款22尽量使用private
- namespace可以跨越多个源文件而class不行
- 将多个类之间类与非成员函数即类外函数之间联系封装起来的办法namespace
- 条款24如果函数的所有参数都可能需要类型转换则应该将这个函数设计为non-member因为这样才能保证允许参数类型转换
- 模板的全特化template
- 条款26尽可能延后变量定义式的出现时间
- C的几种类型转换方法
- 条款28避免返回handles指向对象内部成分
- 条款29异常安全
- 条款30inline函数的各个特点
- 80-20法则一个程序80的运行时间是在20的代码上面开发者的工作室找到这20的代码进行优化
- 注意string并不是一个class而是basic_string
- 条款31降低文件间的编译依存关系
- 什么时候可以使用前置声明
- 在纯虚类中如果想要使用它的一个成员函数就必须把它定义为static因为纯虚类不能实例化只能使用它的static成员或继承后使用
- 纯虚类不能实例化但并不是说它的任何一个成员都不能使用比如static成员就是可以直接使用的
- C的类将接口和实现分离开的方法使用一个类代表接口这个接口利用指针或引用指向实际的实现类另一个方法是将接口声明为纯虚类而使用该接口类的static成员作为工厂构造出绑定实现类该实现类继承纯虚接口基类
- 条款32仅仅只要is-a关系才能考虑使用public继承
- 条款33避免遮掩继承而来的名称
- 利用private继承配合名称遮蔽来实现只继承父类的单一函数成员
- 可以通过pure virtual class的子类实例来调用pure virtual class的pure virtual func调用格式在纯虚类的子类实例中调用PureVirtualBaseClassNamepure_virtual_func这个特性意味着允许给纯虚函数提供实现只不过必须通过子类实例来调用
- 接口继承和实现继承
- 条款37绝不重新定义继承而来的缺省参数值
- private继承的特点和意义
- 条款40明智而慎重地使用多重继承
- 多重继承的特点意义和使用条件
- 编译时多态包括class的函数重载template的具现化
- C模板生成函数的阶段是编译阶段根据调用模板的对象的类型生成对应的函数即为模板的具现化发生在编译期间而不是运行区间是属于编译时多态
- 模板中的typename与class关键字可以互换作用相同class暗示该模板倾向于接收用户定义的类而不是内置基本类型
- 当模板内部含有嵌套从属名称的时候需要在该名称前面加上typename以表示这是一个类型否则编译器默认不把它当做类型
- 什么时候必须加上typename什么时候不可以加上typename
- 注意Ctemplate的特点
- 从同一个template具现出来的class之间没有任何关系不能像基类子类那样进行互相转换
- STL五大类型的迭代器
- TMPC元编程
- new-handler
- 使用new在heap中构造对象发生了两个函数调用调用operator new调用对象的构造函数
- 条款53不要忽略编译器的警告
- 条款54标准程序库STLTR1
‘声明’的定义
声明:即向编译器指示某个东西(类型+名称)的存在,但是略去其细节实现(细节、实现,如:int类型变量的值,class的成员信息,函数的函数体,template<>模板的内容等等都是细节实现)。
==声明并不让编译器为该对象分配内存并构造。==
样例
extern int x; //声明;如果不加extern,就变成定义了
std::size_t numsDigits(int nums); //声明,没有函数体{...}
class Object; //声明,没有类体{....}
template<typename T> class NodeTemplate; //声明
并不是有等号’=’就是定义
例:
class Foo
{
public:
static const int i = 10; //虽然有等号,但是是在class scope内部,因此它是声明而不是定义。这里等号是赋予默认值而不是对其定义并初始化。
.......
}
导入C++头文件和C头文件的区别
C++头文件没有.h后缀,从中导入的名称位于作用域std中
C头文件带有.h后缀,从中导入的名称位于全局作用域global中
explicit阻止编译器执行隐式类型转换/隐式执行构造函数
explicit用于禁止编译器使用explicit限定的构造函数来自动构造对象/执行类型转换。
class Foo
{
public:
explicit Foo(int x = 10);
......
}
void func(Foo f);
Foo f;
func(f); //可行,传递Foo实例
func(1); //不可行;企图将int传递到Foo参数,这要求使用Foo的构造函数,但是这个构造函数已经被限定为explicit了,不允许隐式调用。
func(Foo(1)); //显式调用构造函数,可行
拷贝构造函数和拷贝赋值操作符
拷贝构造函数
使用同类型对象来初始化自我对象(this)
格式
Foo(const Foo& f);
//只带一个参数,参数是同类型的const &
何时调用拷贝构造函数
- 当使用拷贝构造时调用:
Foo f1(f2);
- 使用等号的时候不一定是调用拷贝/赋值运算符,也有可能调用拷贝构造函数:
Foo f1 = f2; //虽然使用了等号,但是由于定义了新的对象,因此不是调用赋值运算符,而是调用拷贝构造函数。
==使用赋值/拷贝运算符’=’时,不一定是调用拷贝运算符,有可能是调用拷贝构造函数。当等号左边有新对象定义出现时,调用拷贝构造函数;当等号左边的对象是已经存在的实例,则调用拷贝操作符’=’。==
拷贝/赋值操作符
用于从另一个同类型对象实例中拷贝其内容到自我对象实例。
格式
Foo& operator=(const Foo& f);
何时调用拷贝赋值操作符
当使用了等号且没有出现新的对象的时候,调用的就是拷贝/赋值操作符。
命名习惯
- lhs:Left-Hand Side
- rhs:Right-Hand Side
- non-member:非成员,即static成员
- 指向一个T类型的对象:pointer to T:命名为pt;如
Foo* pf;
- 在’ClassName’前面加一个’a’表示一个该类的实例:
Foo aFoo;
operator重载
- 对于成员operator重载函数,operator的left-hand side就是隐式参数this。
- 对于非成员non-member operator重载函数(即static函数),operator的left-hand side是该操作符重载函数的第一个参数。
C++多线程
C++以及STL对多线程并没有对应的处理,因为C++设计的时候没有人用过多线程和并发。当下,必须考虑多线程、并发时的线程安全性。
C++常见的三个库
- STL标准模板库
- TR1:
std::tr1::TR1组件
;//Technical Report 1,包含hash tables,reference-counting smart pointers,regular expressions - Boost
条款1:视C++为一个语言联邦
C++功能强大,是一种多重泛型编程语言。同时支持过程式(C-like)、面向对象式(object-oriented)、函数式(functional)、泛型形式(generic)、元编程形式(metaprograming)。当应用C++进行不同类型的编程的时候,所采用的策略不同。
- 当使用过程式时,像C那样的pass-by-value通常比pass-by-reference更高效。
- 当使用OOP时,使用pass-by-reference-to-const更好。
- 当使用STL时,由于==STL的迭代器和函数对象都是基于C的指针,因此STL中使用pass-by-value值传递更好。==
==pass-by-reference-to-const:使用引用传递,并且参数使用const &
来接收而不是仅仅使用&,要带上const。==
条款2:尽量使用const,enum,inline替代#define
#define
等预处理器命令并不被视为语言的一部分。#define
定义的常亮或许从来都没有进入到编译器,或许编译器没有使用#define的名称而只使用了其值。因此,当该define出现错误时,编译器给出的提示中如果写的是定义的值而不是名称,则难以被追踪(当这个宏不是你定义的时候,看到这个错误中的值甚至不知道这是宏定义还是变量的值还是返回值)。
- 使用’指向常量的常量指针’来替代宏定义的字符串
#define MBP MacBookPro
//替代:
const char* const mbp = "MacBookPro";
- 使用std::string来替代宏定义的字符串
const std::string rs7("Audi RS7"); //用于替代#define
- 如果老式编译器不允许在类内赋默认初值,而在类编译期间需要使用这个值,可以使用enum来定义常量
class Foo
{
public:
........
enum { x = 10};
}
==使用enum{something = x}
来替代#define
是很好的解决办法。==
- 务必对所有的宏中的每一个实参都加上小括号;务必禁止在使用宏函数的时候传入带有’++’、’–’运算符的参数
使用模板template inline函数来替代宏函数
#define CALL_WITH_MAX(a,b) \
f( (a) > (b) ? (a) : (b) )
替代:
template<typename T>
inline void callWithMax(const T& a,const T& b)
{
/*得到a、b的大值,以之调用f*/
f( a > b ? a : b); //f是一个函数
}
要点:
1. 使用inline
来模仿#define
宏函数,inline与宏函数一样不存在函数调用时的额外开销。
2. 使用const T&来接收参数,而不使用T和T&。(pass-by-reference-to-const)
总结
使用以上几种方法来替代宏定义,但是,宏定义并非可以完全不使用,#define尽量少用。
对于单纯的常量,使用const对象或enum替代。
对于宏函数(macros),尽量使用template inline函数来替代。
类专属成员的定义、static成员、静态成员
类的专属成员,即只属于类的成员,同时该成员不属于类的任何一个实例。
符合以上条件的,即为static成员:类的static成员属于类:可以ClassName::StaticName来访问;不属于类的实例:不能使用ClassInstanceName.StaticName来访问。类的专属成员的一个特点:不论这个类实例化了多少个实例,static成员(就是专属成员)都有且只有一份。
总结:类的专属成员就是static成员,它属于类但不属于类的实例
类专属成员/静态成员/static成员的声明和定义形式
==特别注意:static成员在类内(包括一些普通成员),由于它们在类内,即时它们有被赋值(初始化默认值),它们是声明而不是定义(不是有赋值就是定义)。==
class Foo()
{
public:
static const int DataIndex = 10; //虽然有赋值,但是这只是一个声明,并不是定义。
......
}
在类外定义类的static成员/静态成员/专属成员
明确:在类内那个static成员是声明而不是定义。只要不取其地址且编译器不要求你定义,则类内static成员声明后就能用;要取地址,就必须定义。
const int Foo::DataIndex; //定义,不需要且不允许对其赋值
//根据这个例子,可以看出,不能因为一个定义中出现了const就一定要进行赋值
根据这个例子,可以看出,不能因为一个定义中出现了const就认为一定要进行赋值
条款3:尽可能使用const
分析const是修饰pointer本身还是修饰pointer指向的data
const的位置存在一点随意性,但是分析它是修饰data还是修饰pointer是很简单的:
1. 只要const出现在’‘的左边,那么它就是修饰pointer指向的data为只读;只要const出现在’ ‘的右边,那么它就是修饰pointer本身是只读的;
2. 当const出现在’‘的左边的时候,不论它是出现在类型符的左边还是右边,它都是完全等价的两种写法(如const int 与int const *完全等价)。
对迭代器声明const(不是const_iter)实际const修饰的是iter指向的data为只读
STL iterator建立在指针的基础上,因此,可以将迭代器看成T*。对迭代器使用const等同于对T * 这个指针使用const,const对指针,即对pointer本身进行限定,而不是对指向的data限定。对指向的data进行限定,使用const_iter。
==const std::vector<int>::iterator iter = aIntVector.begin();
等同于T* const;==
在满足功能的前提下,尽量为函数的返回值使用const限定可以避免一些编程失误
例:
class Foo{........};
const Foo operator*(const Foo& rhs,const Foo& lhs)
{
....
//函数返回两参数的积
}
Foo x,y,z;
......
(x * y) = z; //对两数之积的结果 赋值。如果使用const限定返回值,则这种不符合逻辑的操作或失误将会被编译器发现并报告错误
if( (x * y) =z){} //这里失误:'=='错误地变成了'=',如果没有const返回值限定,则能够通过编译,导致发生错误;有const时,编译器会发现错误
总结:对函数的返回值使用const限定,有助于排查错误、防呆检测,避免调用者错误使用
const成员函数
const成员函数是指不会修改对象的函数,在参数列表括号后面有一个const;返回值中带有const的函数不是成员函数,这个const只修饰返回值。
const成员函数的作用是:用于操作const对象。
==注意:C++中可以从一个普通成员函数中重载出它的const版本==;即:参数相同的两个成员函数,可以通过是否为const成员函数来进行重载。
const成员函数的作用
const成员函数就是用于/针对const对象进行操作。当对一个非const对象或一个const对象操作时(调用函数),根据对象是否为const选择调用普通或const成员函数。非const对象调用普通成员函数,const对象使用的是cosnt成员函数。
const成员函数的格式特点
- 参数列表(即函数名称后面的括号)后面带着一个const表示为cosnt成员函数;
- cosnt成员函数一般要返回const类型,而普通成员函数返回类型不带const;(但是带不带const不是确认它是不是const成员函数的标准,第一条才是。)
- 注意判断应该返回value还是应该返回reference。
mutable关键字:允许const成员函数修改这个成员
将类内的一些成员定义为mutable,将允许const成员函数修改它的值。
当一个类提供const、non-const两类成员函数时,避免代码重复,增强代码复用
- 方法1:将重复的代码封装为另一个private函数,让non-const和const调用。这样,重复代码只剩下两句调用语句和return语句;
- 方法2:让non-const版本调用const版本;这样,只需要实现const版本即可,但是要求在non-const中使用强制类型转换static_cast和const_cast;
条款4:确定对象被使用前已先被初始化
注意:==在构造函数的函数体内进行的是赋值操作;而在初始化列表中的才是初始化。==
注意对象构造流程:首先完成初始化列表,再进入构造函数体,因此,初始化列表的效率更高
构造函数体内的赋值操作,相当于抹去初始化列表中的初始化,然后赋值,效率较低。
引用reference或const都必须要使用初始化列表
因为它们不能赋值。
不论有无显式写出初始化列表,初始化列表和初始化操作都是存在的,只是按照默认构造进行
条款7:为多态基类声明virtual析构函数
如果没有将多态基类的析构函数声明为virtual
当通过基类指针构造子类对象的时候,此时使用基类来引用/表示子类:
BaseClass *b = new ChildClass;
如果析构函数不是virtual的,那么调用析构函数时:
delete b;
这时,调用BaseClass的析构函数,进行析构;==问题出现:使用基类的析构函数进行析构操作,但是实际对象却是这个基类的子类。所以,这样析构只能释放基类的部分,而子类的部分没有被释放(因为基类的析构函数并没有能力去析构子类的成员部分),造成内存泄露。==
将多态基类的析构函数声明为virtual的目的
继续上面的例子,但是将BaseClass的析构函数定义为virtual。这时,delete b
仍然通过子类BaseClass来调用析构函数。==但是,此时析构函数是virtual的,并且基类变量实际指向的是其子类对象,因此,发生动态绑定。==动态绑定的结果是,delete
调用的析构函数实际是子类的析构函数。这样,通过基类进行的析构操作成功地通过动态绑定调用了子类的析构函数,能够将子类的部分析构释放掉,避免内存泄露。
总结:将析构函数声明为virtual是为了激活动态绑定,用对应子类的析构函数来执行析构操作
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数
当一个对象不存在virtual函数时,往往表示它并不希望自己成为基类,并不希望被继承
当一个类不会被继承的时候,让它的析构函数成为virtual是不良的设计,因为会带来额外的开销和无用处的virtual标记
virtual要求对象携带一些额外的信息(包括Virtual Table Pointer),这些信息用来进行动态绑定。当一个类不会被继承,动态绑定根本对它没有任何作用且带来额外开销,因此不应该强行套一个virtual。
除了额外开销外,==virtual函数还会使对象的内部组织结构发生变化,这个变化导致这个对象不可能与C语言兼容==。(例:如果一个对象内只有一个int而没有其它东西,那么这个对象可以装在一个int空间内并以in表示,这个对象可以被C使用。)
总结:当class内包含virtual时,就把析构函数声明为virtual,否则不要声明为virtual析构
string的析构函数是non-virtual的,如果从string派生自己的类,然后用string来delete这个类,这时调用的就是string的析构函数,而不是自己派生的类的析构函数,这时就会导致部分内容未析构,导致行为未定义,属于程序事故。
纯虚析构函数virtual ~Foo() = 0;可以确保动态绑定调用子类的析构函数
纯虚函数不能被调用,且要求子类进行override,因此,能够确保动态绑定调用子类析构函数。
注意:纯虚函数所在的类是纯虚类
条款8:别让异常逃离析构函数
C++并没有禁止析构函数抛出异常,但是在析构函数中抛出异常是一种很不好处理的情况。
方法
- 捕捉该异常,使异常不被传播到析构函数之外;
- 将这个异常视为致命故障,发生该异常时,在try-catch{}中使用
std::abort()
退出程序; - 将可能出现异常的代码封装为类内的一个方法由用户决定要不要手动调用,如果手动调用,用户则能够实现对其的处理。此时,在析构函数内,一样要捕捉所有异常。
总结,不论如何,捕捉析构函数中所有可能的异常
条款9:绝对不要在构造和析构函数中调用virtual函数
==child class在构造时,需要首先构造基类。在基类的构造期间,virtual函数的virtual性质是不能体现的,不能发生动态绑定,在构造函数中调用的virtual函数,难以确定是基类的还是子类的。==
子类在其基类部分的构造期间,它的type就是基类而不是自身
==derived class在构造期间,首先构造基类部分,然后构造自身部分;当处于基类构造部分的时候,它就是子类,而不是自身type。这时,不论是RTTI还是typeid都会得到基类类型而不是自己这个派生类类型。==
子类在析构函数释放其子类部分后(剩余基类部分),它的type从自身的类型变成基类类型
==析构函数首先释放派生类部分,再释放基类部分。当派生类部分被释放后,剩下基类部分,此时,该对象的type从自身类型(派生类类型)变为基类类型;RTTI和typeid的结果也跟着发生变化。==
条款10:令operator()返回*this
- 返回*this;
- 方法的return_type为THISObject&;
作用:使这种操作符op支持连续赋值、连续调用。
条款11:处理自我赋值
注意:自我赋值可能很隐蔽:如s[i] = s[j];
难以一眼看出来是不是自我赋值。
其他隐蔽点:
1. 指针;不同的指针可能指向同一对象,指针之间赋值可能发生自赋值。
2. 派生类变量和基类变量可以指向同一对象;它们之间赋值可能发生自赋值。
operator=自赋值的基本处理
- 要认真分析代码,查看会不会出现自赋值危险(如:指针op=的时候,可以使用this与参数&进行比较;更好的办法是保存原指针,然后执行=操作,没有问题后再delete旧指针;而不要先delete旧指针再op=)。
- 使用if(this == &rhs)证同;注意:==是地址比较,因此需要使用this指针和对rhs的引用取地址进行比较==。
- 保证先完成赋值,再delete
- 可以将证同用到任何一种op=内,效率高了但是代码和控制分支多了。(不使用证同:保存旧对象后赋值,赋值后再delete对象,这样不需要证同,因为不论是不是’同’,它都能确保安全赋值。加入证同,当是自赋值时,直接返回,效率高了,但是多了一点代码)
- 通用、标准的解决办法:copy and swap;
==copy and swap技术:实现op=时,将rhs的副本与*this交换(将swap()实现为thisClass的方法)。==
自赋值的危险在于可能发生的异常和自赋值时错误地delete操作,而不是赋值’=’本身的问题
总结:安全进行自我赋值、实现operator=的方法
- 比较lhs(即this)和rhs是否是同一个对象。
if( this == &rhs);
注意:比较的是地址,因此使用this指针和对rhs引用取地址进行比较。使用*this == rhs是错误的。
- 精心设计op=的语句顺序。
- 如:赋值之前保存旧的变量;
- 在赋值之后再delete旧变量,不要先delete再赋值;
- copy and swap
- 使用rhs&的副本,将它与*this交换。
条款12:复制对象时不要忘记复制其基类部分
复制对象,包括调用拷贝构造函数和拷贝赋值操作符,都必须要记得手动/显式地完成基类部分的拷贝。
==当派生类使用拷贝构造函数或拷贝赋值操作符的时候,在其初始化列表和构造函数体内会完成拷贝操作,但是注意,并不会自动调用基类的拷贝构造函数或拷贝赋值操作符。==
- ==基类的成员,不论是private还是public都应该使用基类的拷贝构造函数或拷贝赋值操作付来拷贝。==
- ==子类的拷贝构造函数/赋值操作符调用的时候,并不会自动调用基类的拷贝构造函数/赋值操作符==
- 因此,必须显式地增加调用代码:在拷贝构造函数的初始化列表中添加子类的拷贝构造函数以实现调用;在拷贝赋值操作符的函数体内利用BaseClass::operator=(arg)来调用基类的拷贝操作符。
如果忘记了基类部分的后果
如果拷贝构造函数忘记在初始化列表中调用基类拷贝构造函数,则会导致基类部分的成员按照默认构造函数进行构造,使拷贝构造得到的对象的基类部分是默认值而不是拷贝值
如果拷贝赋值操作符忘记在函数体中调用基类的拷贝操作符,将导致复制所得对象的基类部分的成员的值不变(因为没有代码操作了它们),而不是预期想要得到的拷贝值
总结:保证调用基类的方法来复制基类成员,否则基类部分的成员的值不会是预期的拷贝值
复制所有的local变量,调用base class的copy_constructor、copy_assignment。
资源
资源:包括内存、文件描述符fd、各种锁、GUI中的笔刷和字型、数据库连接和Socket。
不仅仅是内存,其他任何一种资源在用完以后都必须释放、还给系统。
条款13:以对象管理资源
利用’对象的析构函数一定会被调用’这一特性,在对象析构的时候delete资源。对于的机制是智能指针。目的是帮助自动释放资源。
采用智能指针
常用的智能指针:STL的std::auto_ptr<>、TR1的std::tr1::shared_ptr<>
std::auto_ptr<>
auto_ptr<>在被销毁的时候(离开其scope时),将会==对其所指向对象调用delete==。
1. 不要令多个auto_ptr<>指向同一个对象,否则该对象会被delete多次;一个对象被delete多次将会导致未定义的结果甚至是致命错误;
2. auto_ptr<>不会保留副本:当拷贝一个auto_ptr<>的时候,被拷贝的那个auto_ptr<>会变成nullptr;
3. auto_ptr<>不是引用计数型的,只要它到达生命期,指向的对象就被delete;
std:: tr1::shared_ptr<>
- shared_ptr<>是引用计数型的,ref_cnt == 0时才delete对象
- shared_ptr<>的拷贝是正常的拷贝,允许保留副本
智能指针未完善的地方
- 以上两个智能指针只能指向delete而不能执行delete [];因此==**绝对不要使用auto/shared_ptr来指向动态数组(也没有必要使用动态数组,因为vector和string完全ok);
- 针对数组的替代品:boost::scoped_array、boost::shared_array;
总结:使用对象来管理资源,就是使用智能指针
条款14:在资源管理类中小心copying行为
- 禁止复制
- 使用引用计数
- 允许复制,并且在复制智能指针的时候一并复制其指向的实际对象
std::tr1::shared_ptr<>有一个重载形式:第二个参数可以接收删除器deleter
当引用计数为0时,shared_ptr不delete对象,而是执行删除器deleter。
因此,可以在deleter中定制自己的管理操作。
条款15:在资源管理类中提供对原始资源的访问
因为某些API要求接收原始资源的原始指针。
提供Raw Resource访问的方式
- 显式转换:提供一个get()函数;如大多数智能指针都能通过get()返回原始指针;
- 隐式转换:使用op’->’、op’.’获取原始指针,所使用的就是隐式转换;
- 提供隐式转换operator方法;
operator Foo() const
{
return f;
}
允许对象进行直接隐式转换的方法
例:
class Foo{
public:
operator int() const{
return (int)10;
}
};
int main(int argc,char*argv[])
{
Foo f;
int ten = f; //f被隐式直接转换为int
cout << ten << endl; //ten 是f隐式转换得到的int
ten = 0;
Foo ff;
ten = (int)ff; //发生转换
cout << ten << endl;
return 0;
}
令C++对象支持隐式转换/类型转换的方法
operator return_type() const
{
return (return_type)(value);
}
条款16:成对使用new和delete时采用相同的形式(带或不带[])
当通过new来调用对象时,使用的是对象的operator new()、operator delete
条款17:以独立语句将new对象装入智能指针
==将Raw Resource的获取和装入智能指针这两个操作放到同一个语句内,并且该语句不要其他任何操作,放置被干扰。==
如果不这么做,那么一旦另一个操作干扰(如抛出异常)装入智能指针,将导致指针实际并没有装入。原理是在同一个语句内,编译器可能因为对代码的优化导致Raw获取和智能装入不是连在一起的操作,这中间间隔的操作如果有干扰,将导致装入失败。
条款18:让接口不易被误用
P79:
- 引入新的接口类型,替换方法中的易混淆的参数;
- 使用enum来表示函数的所有参数(如果参数个数是有限的);
- 使用静态的成员函数来帮助构造参数;==这时,构造函数应该声明为private以禁止用户构造新的参数。==
- 将构造函数声明为explicit,配合上面几点使用;
C++STL的接口比较一致,而Java和C#的则不一致;如:所有STL容器得到元素个数都可以使用size(),而Java的String使用length、List使用size(),并不统一。
智能指针的一个技巧:当该智能指针还不知道要指向谁的时候,可以先让它指向null,然后再赋值以指向目标对象
std::tr1::shared_ptr<Foo> //return_type
pFoo(static_cast<Foo*>(0),aDeleter);
//arg1:指向raw_source的null对象,转换为指针
注意:智能指针比普通指针大,而且速度更慢
条款20:对于对象:尽量使用pass-by-reference-to-const替代pass-by-value;对于内置类型以及迭代器、函数对象,应该使用pass-by-value而不是reference
==pass-by-reference的另一个巨大优点:可以防止对象在参数传递的时候被切割。具体是:如果参数是基类pass-by-value,如果实参是子类,则传递的时候子类会转换为父类然后拷贝后pass-by-value,这时,参数就丢失了子类部分而只剩下基类部分,导致对象被切割。pass-by-reference就没有这个缺点。==
条款21:不要返回绑定了函数内的local_stack或heap对象的引用/指针
当需要返回函数内创建的对象时(local stack),返回类型必须不是引用或指针,即只能以拷贝的形式返回,不能以引用/指针返回,因为local stack对象在函数退出后就被释放了。
也不能返回函数内new出来的指针或new出来的对象的引用,实际上,在函数内使用new而没有delete掉本身就是不佳的设计(因为要求调用者delete,要么干脆就没有delete),因为这样难以进行delete。
总结:不要返回会被销毁的对象或heap中的对象(在函数内new的对象就应该在函数内delete),使用开销较大的值传递返回能够带来正确
inline const Foo operaor*(const operator& rhs,const operator&lhs)
{
return Foo(lhs.data * data1,rhs.data * data2);
}
条款22:尽量使用private
==protected并不比public具有更多的封装性;因为继承之后,protected对于derived class来说等同于public。==
namespace可以跨越多个源文件而class不行
将多个类之间、类与非成员函数(即类外函数)之间联系、封装起来的办法:namespace
当完成一个任务需要若干类和若干non-member函数的时候,为了方便用户调用,也为了更好地表示和体现这些类和函数的关系,可以将它们放到同一个namespace里面。
==注意,namespace是可以跨越文件的。完全可以在多个独立的源文件中定义类和函数,然后将它们组合到namespace里面。==
例:
//a.h:
class A{...};
funcA(...){...};
namespace BigProject{
class A;
funcA();
}
//b.h:
namespace BigProject{
class B{...};
funcB(...){...};
}
//c.h:
class C{...};
namespace BigProject{
class RenameC : public C {...}; //空继承C,没有新增特性,相当于用一个新的类名来表示C
}
条款24:如果函数的所有参数都可能需要类型转换,则应该将这个函数设计为non-member,因为这样才能保证允许参数类型转换
模板的全特化template<>
对于STL中提供的模板方法,如果我们想定制它对一个特定的T有指定的处理办法,则使用全特化,当对这种类型为T的对象调用这个模板函数的时候,就会使用指定的代码;如果没有全特化,则调用模板定义好的方法。
格式:模板名称后面带上<>,<>里面写上要被特化的类型。
注意:特化的格式并不是template后面的<>为空就是特化,而是函数名称后面带有<>才是特化
全特化STL中某个模板template的方法
namespace std{ //表示是对namespace下的组件进行操作
template<> //尖括号内为空,表示对已有模板进行全特化,而不是声明一个新模板
void swap<Foo>(Foo& rhs,Foo& lhs) //函数名称后面尖括号写上想要全特化的类的名称,当该模板函数发现参数是该类时,使用这个特化的处理方法
{
Foo::swap(rhs,lhs);
}
}
理解特化
==不允许修改STL里面的内容,但是允许对STL的标准templates制造特化版本使之适合我们自定义的类==
==特化相当于,遇到指定的类型时,对模板的行为进行重写/覆盖;类似于class中的函数重载:遇到匹配的类型参数的时候调用对应的重载函数。==
特化的应用例:将模板特化与swap()结合使用
class Foo
{
public:
...
void swap(const Foo& lhs)
{
using std::swap; //告诉编译器:如果没有Foo对应的专用特化版本,则调用std::的版本
swap(this.data,lhs.data);
}
...
};
namespace std{
template<>
void swap<Foo>(const Foo& rhs,const Foo& lhs)
{
rhs.swap(lhs); //调用对应对象的自定义函数
}
}
总结:当swap()对于某个类的效率不够高的时候,可以提供一个swap()函数(member或template<> void swap<>()特化)
当提供member swap()时,应该同时提供non-member swap()用于调用member版本;注意特化std::swap;注意使用using std::swap;
注意:不能向std的namespace内添加任何东西,否则行为未定义
条款26:尽可能延后变量定义式的出现时间
防止一些变量被定义了但是没有使用。
C++的几种类型转换方法
- const_case() 将const属性擦除;
- dynamic_cast 用来向下转型,将基类转换为子类;
- reinterpret_cast 把内存中的bit强制解释为另一种类型:例如将一个pointer to int强行解释为int;
- static_cast 执行一些本身就是合理的、正常的类型转换:如将int转为double,将non-const转为const,将void*转为type *,将pointer to base转为pointer to derive; static _ case<>类似于C的转型:Foo(10) –> static _ cast(10)。
注意:不同类型指针赋值时,实际发生了类型转换,因为这两个指针的实际值可能不同
注意:不同类型的两个指针,即使使用等号进行赋值,它们的实际值也不一定相等;一个对象被两个类型的指针指向的时候,这两个指针的值不一定相等,即:不同类型的指针使对象有不同的地址!!!
注意:许多dynamic_cast<>的实现版本的运行速度相当慢且效率低
尽量少使用转型cast,但是实际上不可能永远都不需要使用cast
条款28:避免返回handles指向对象内部成分
handles:号码牌,用于表示/取得某个对象(可以是某个类内的成员)
如果函数返回了内部成分的handles,那么调用者就有机会通过这个handles修改内部成分,破坏了类的封装性,使private失去作用变成public。
例:返回private成员的引用或迭代器或指针等handles,将可以通过这个handles修改private成员,导致封装被破坏。
另一个风险:返回的handles在对象析构后会变成空悬的号码牌dangling handles。
避免返回handles,即使返回的是const handles,它也有可能成为dangling handles
条款29:异常安全
异常安全的两个基本要求:不泄露任何资源、不破坏任何数据
- 不泄露任何资源;例:取得锁后,如果接下来的临界区代码中可能抛出异常而未处理,则会导致锁永远未被释放,资源被泄露。
- 不允许数据被破坏;如果抛出异常后使某个变量指向未定义,而这个变量原先的值又被delete掉了,因此无法恢复其数据,这就是数据被破坏。==在异常前后,所有的class的状态保持不变(或保持一个有效的状态,如异常发生后置为缺省值,只要是有效的合法的数据就行了。当然,最好保证数据不变。)。==
确保锁能够被自动释放的方法
使用一个获取锁的自定义对象来替代lock(),当需要锁的时候,构造那个对象,对象取得锁;在函数退出的时候,对象析构,在析构的时候释放锁,这样,保证能够把锁释放掉。
条款30:inline函数的各个特点
inline过多将导致程序的内存占用膨胀,进而导致效率降低
- 导致完全装入内存,必须使用虚拟内存,降低速度
- 导致额外的内存换页行为,降低指令高速缓冲的命中率
==因此,慎重考虑是否要将一个template声明为inline,因为这会导致它的所有具现都会是inline。==
inline只是一个请求,并不是一个命令,编译器不一定执行
定义在类体内的函数(包括类体内定义的friend)隐含inline
不要在类内定义构造函数体,因为有可能会被inline,而inline后的构造函数会带来影响(不一定能加快程序反而可能使程序体积膨胀)
很多调试器并不能进行对inline函数的调试,因此,它们的编译器生成的Debug版本并没有启动任何inline
80-20法则:一个程序80%的运行时间是在20%的代码上面;开发者的工作室找到这20%的代码进行优化。
注意:string并不是一个class而是basic_string
条款31:降低文件间的编译依存关系
文件之间存在较大编译依存关系的一个体现:修改一个文件之后,重新编译,导致整个工程的很多文件甚至全部文件都要重新编译,即使它们并没有任何修改。
- 如果一个类的内部成员使用object reference和object pointer能够完成任务(将一个class的内部成员封装到实现类,本class再用指针或引用链接过去),就不要使用object。这是==对象的接口与实现的分离,声明和定义的分离的体现。==
- 如果能够,尽量使用class声明式替代定义式:声明式即前置声明:class Foo;注意:==什么时候能够使用前置声明:当某个class仅仅只出现在函数的声明式中(不论它是作为函数的return_typr还是函数的arg_type),它都不需要定义式而仅仅需要声明式(使用前置声明)。==
- 为声明式和定义式提供不同的头文件。设计一个会被别人调用的class时,为这个class提供一份定义式头文件,一份声明式头文件;当调用者只需要声明式时,包含声明式头文件即可。声明式头文件的命名:以fwd结尾;如iostream的声明式就是
C++其实也有一个export关键字能够分离template的声明和定义…但是支持这个关键字的编译器特别少
什么时候可以使用前置声明
当某个class出现在函数的声明式中,不论它出现在函数的return_type还是函数的arg_type,那么该class就不需要定义式而仅仅需要声明式,即不需要包含其定义的头文件而仅仅需要前置声明即可。注意前提条件:只出现在函数的声明式中。
在纯虚类中如果想要使用它的一个成员函数,就必须把它定义为static,因为纯虚类不能实例化,只能使用它的static成员或继承后使用。
纯虚类不能实例化但并不是说它的任何一个成员都不能使用,比如static成员就是可以直接使用的
C++的类将接口和实现分离开的方法:使用一个类代表接口,这个接口利用指针或引用指向实际的实现类;另一个方法是将接口声明为纯虚类,而使用该接口类的static成员作为工厂构造出/绑定实现类,该实现类继承纯虚接口基类
条款32:仅仅只要’is-a’关系才能考虑使用public继承
为什么鸟会飞而企鹅是鸟但不能继承鸟?因为“鸟会飞”这句话是错误的不严谨的,应该说有些鸟会飞,故企鹅不能继承鸟而只能继承不会飞的鸟
在逻辑中、数学中等其它领域成立的所属关系/属于关系,可能并不符合OOP的继承要求,因此,考虑继承时必须以OOP原则为标准
例:数学上,正方形属于矩形,但是OOP中不应该让正方形继承矩形,因为正方形只有边长而没有长和宽。
条款33:避免遮掩继承而来的名称
当基类的某个函数不论是不是virtual时,子类如果定义了一个名称相同的函数(不论带不带参数、不论是否满足重载条件),由于作用域的原因,子类定义的函数将会屏蔽基类定义的同名函数,造成Base的函数名称被遮蔽。
解决:要么子类定义的函数不要与基类的函数重名,要么显式使用BaseClassObject.func()来调用,要么就使用using声明将被覆盖的名称重新加入到当前作用域:
using BaseClassName::func; //注意,只是名称,不带括号!
总结:子类内包含的名称(变量或方法名)如果与父类内包含的名称重名,将会导致父类名称被屏蔽/遮掩
class Base
{
public:
virtual void f1() = 0;
virtual void f1(int);
void f2();
void f3();
void f3(int);
};
class Derived : public Base
{
public:
void f1(); //将遮蔽Base的两个f1
void f3(); //将遮蔽Base的两个f3
//解决:
using Base::f1;
using Base::f3; //注意:Base不是keyword而是BaseClassName
};
如上例:class Derived的f3是对Base::f1的重载,但是由于名称作用域的规则,Base::f1被屏蔽;
Derived::f1是对Base::(pure_virtual)f1纯虚函数的override重写实现,但是它屏蔽掉了Base的virtual void f1(int),因此通过名称查找,是只能调用重载版本void f1()而不能调用原作用域内的原版本void f1(int);
总结:子类对于父类的重名名称将会导致父类名称被遮蔽,而不是触发重载;不能重载的原因是防止一条继承链中的类会包含其遥远祖先的重载函数,这要求如果要使用父类们的函数,必须显式使用using来曝光作用域。
利用private继承配合名称遮蔽来实现只继承父类的单一函数/成员
步骤:
1. private继承基类
2. 在derived类中声明你想继承的那个成员的名称,这时由于重名,将使父类的名称被遮蔽(其他名称不可访问,因为private)
3. 在derived声明的函数中通过BaseClassName调用那个函数。
4. 把derived声明的函数放到public中,这样,调用者就只能调用你指定的那个函数了,而不可能调用它的父类的其他任何函数或该同名函数的其他重载版本。
可以通过pure virtual class的子类实例来调用pure virtual class的pure virtual func;调用格式:在纯虚类的子类实例中调用PureVirtualBaseClassName::pure_virtual_func()。这个特性意味着允许给纯虚函数提供实现,只不过必须通过子类实例来调用。
接口继承和实现继承
- pure virtual函数:希望继承接口,要求子类完成实现
- impure virtual函数:希望继承接口,并提供默认的实现,子类可以override
- non-virtual函数:希望继承接口和实现,不能override
==别忘了基类的析构函数务必声明为virtual==。
条款37:绝不重新定义继承而来的缺省参数值
原因是virtual函数是会进行动态绑定的,但是virtual函数的默认参数值却是静态绑定的
class A内部:
virtual void foo(int i = 10); //foo可以进行动态绑定但是它的参数i是静态绑定
参数是静态绑定的结果:不论动态绑定了那个函数,实际使用的默认参数是按照静态类型来确认的
class B内部,B public继承A
virtual void foo(int i = 100) override;
B b;
A* pa = &b; //用基类A来绑定子类B的实例
pa->foo(); //使用动态绑定,通过基类指针来调用子类方法
//使用foo()方法的默认实参
/*此时,动态绑定调用了B类的方法,但是pa的静态类型是A,因此foo()方法的默认参数是A内的foo()定义的10而不是B中的100*/
//使用了B类的方法,但是按照静态类型为A的原因,因此使用了A的默认实参
private继承的特点和意义
private继承导致的结果
- 基类中的所有成员都会变成private;
- 如果是private继承,则不会自动将派生类类型转换为基类类型(不会自动转换,但是可以手动显式进行转换),不能隐式转换;
private继承的意义
==不同于public继承是表示子类与基类之间的’is-a’关系,private并不象征着类之间的’is-a’关系。==
private继承意味着“子类根据基类的实现而得到”。(implemented-in-terms-of,根据基类实现出子类)
由于private继承将基类的所有public都改为private,因此,可以将private继承视为==继承子类的实现而略去子类的接口==(因为子类的接口由于private的原因不能再被调用者调用,相当于接口被取消),接口指一个class内的public方法。
什么情况下该/不该使用private继承
由于private就是将一个类(基类)作为另一个类(子类)的一部分实现内容,即用基类来实现子类,它与对象之间的复合/包含关系很像,因此需要明确它们的异同点并考虑替代。
复合/聚合关系:即一个类包含另一个类,如在class Foo中定义一个成员,其类型是另一个类,这两个类之间就是复合关系。
使用private继承来代替复合的情况
1. 存在protected成员的时候,使用private继承和使用复合的结果是不同的:复合后一个类仍然不能使用另一个类的protected成员;而private继承可以。
2. 存在virtual成员的时候:复合与private继承的结果也不同。
==尽量使用复合而必要的时候才使用private继承==。
总结:什么时候使用private继承比复合更好
- 当你希望访问protected接口的时候,使用private继承比复合更好,因为private继承能够提供访问权限;
- 当你希望override它的virtual函数的时候,使用private继承更好,因为继承能够提供override。
- 除此之外一般来说,使用复合比使用private继承更好。
条款40:明智而慎重地使用多重继承
多重继承的特点、意义和使用条件
多重继承MI:Multiple Inheritance;单一继承:Single Inheritance;
C++阵营中有两派:一种是认为多重继承不好用,一种认为多重继承好用。实际上,多重继承有时候效果比单一继承更好,但是比较复杂;如标准库的iostream实际就是一个多重继承得到的类。
多重继承的特点
在继承树只有一层的情况下,多重继承几乎等同于按顺序单一继承了若干个类。但是,在继承树比较长的情况下,多重继承的情况会很复杂。
1. 多重继承的类分为public、private、protected混合的继承,还包含普通继承和virtual继承。
2. 如果一个继承树中,从最早的最上面的基类到下面的后面的子类存在两条或以上的继承路径,将会导致子类会有两条或以上的基类构造路径,将会导致含有同名称的重复数据成员。
钻石型多重继承:
class BaseBase{};
class BaseA{} : public BaseBase{};
class BaseB{} : public BaseBase{};
class Derive : public BaseA,public BaseB{};
Derive与BaseBase之间有两条继承路径,这将导致Derived会从两条继承路径中分别继承公共基类中的同名成员。
例:BaseBase::data被BaseA继承、BaseB继承,然后Derived继承BaseA、BaseB,这导致Derive中含有两个data成员,==**直接使用derive.data调用将会导致歧义,必须使用derive.BaseA::data、derive.BaseB::data来调用。
如果发生钻石继承即含有多个同名称的成员时,使用virtual避免
继上:
class BaseA{} : virtual public BaseBase{};
class BaseB{} : virtual public BaseBase{};
class Derive : public BaseA,public BaseB{};
==使用virtual继承保证多重继承的时候,被继承的基类中的成员不会重复==。
virtual继承的作用和代价
virtual继承将导致编译器生成的代码更大,生成的程序速度减慢。
作用:当某个类可能作为其他类的基类的时候,且它继承自某个类,则使用virtual继承,可以让它继承的类中如果有与其它共同基类重名的情况,不会导致保留两份成员而只会保留一份。
virtual base class的使用
如果有必要使用多重继承时,平常最好使用普通的继承即non-virtual继承;必须使用virtual继承时,尽可能避免在virtual base class中定义数据成员,防止多条路径继承时产生多个同名称的数据成员。
C++中不带数据成员的virtual base class类似于Java和C#的接口类interface;Java、C#不允许在接口类中定义数据成员的原因与C++的相同,都是防止同名数据成员有多个副本
总结:多重继承在不得已的情况下可以使用,但是几乎可以肯定存在不需要多重继承就能解决问题的方案;如果使用多重继承,应该考虑是否要使用virtual继承;如果使用virtual继承,则virtual base class最好不要定义任何数据成员,这样生成的代码速度更快、更小且降低复杂度
编译时多态包括class的函数重载、template的具现化
C++模板生成函数的阶段是编译阶段,根据调用模板的对象的类型生成对应的函数,即为模板的具现化,发生在编译期间而不是运行区间,是属于编译时多态。
模板中的typename与class关键字可以互换,作用相同;class暗示该模板倾向于接收用户定义的类,而不是内置基本类型
当模板内部含有嵌套从属名称的时候,需要在该名称前面加上’typename’以表示这是一个类型,否则编译器默认不把它当做类型
什么时候必须加上typename什么时候不可以加上typename
- template class的继承列表中不能使用typename
- template class的构造函数的初始化列表中不能使用typename
- template class的嵌套从属名称必须加上typename
注意C++template的特点
- template会按照具体所需要的类型,在编译期间具现化出对应的class、func;
- 当template需要时,才会将对应Type的函数/class具现出来。
从同一个template具现出来的class之间没有任何关系,不能像基类、子类那样进行互相转换。
STL五大类型的迭代器
- 只能R或只能W的单向迭代器:两种:R或W,执行完一次R/W操作操作后自动移动一步;
- 单向迭代器:可以R和W,可以R/W多次,即可以对一个位置R/W多次后再移动(上面那个iterR/W一次后会自动移动)
- 双向迭代器:单向迭代器的双向版本
- 随机迭代器:可以在常量时间内跳动任意距离;其他迭代器有些可以在代码中体现出来’+=’一个数值来体现一次性移动多次,但是实际是依靠多次走一步来实现的;随机迭代器是一次能够走多步。
TMP:C++元编程
C++元编程:使用C++写成的、编译期间在编译器中运行的程序,它的运行结果再由C++编译器编译后得到最后的文件。
new-handler
new失败的时候,它会调用new-handler。使用头文件中的set_new_handler来设定new-handler
当new无法取得内存时,==它会不断地调用new-handler(如果有new-handler就调用它,如果没有,就抛出异常),并不会在调用后自动终止程序,而是不断调用handler直到内存足够。==
使用new在heap中构造对象:发生了两个函数调用:调用operator new;调用对象的构造函数
如果构造函数发生异常,则由C++运行时系统来完成对new的撤销、回滚操作。因此,C++会调用new对应的delete(标准new对应标准delete,自定义的对应,定位placement对应)
条款53:不要忽略编译器的警告
如果子类声明了父类的同名virtual函数但是漏掉了该函数的const属性(父类的该函数为const时),这时并不是virtual-override而是名称重合导致命名遮蔽。
class Base
{
public:
virtual void func() const;
};
class Derive : public class
{
public:
virtual void func(); //漏掉了const
};
//此时,Base::func()被遮掩,不能从Derive访问
条款54:标准程序库STL、TR1
C++98:STL、iostream(cin cout cerr clog)、国际化支持(unicode wchar_t)、数值处理(复数模板complex、纯数值数组valarray)、异常体系。
TR1:智能指针shared_ptr、weak_ptr;tr1::function表示可调用物;tr1::bind绑定器;Hash Tables;正则表达式;Tuples(类似python的元组,相当于容量为n的pair);固定大小的未使用堆的数组;mem_fn;ref_warpper;随机数工具;数学函数;
TR1是一份文档(Technical Report 1),真正的库实现是其附带的代码。
在Boost中有一份TR1-like实现,是Boost的TR1实现
TR1只是一份文档,并不完全表示一种实现。