effect C++笔记
视C++为一个语言联邦
C
Object-Oriented C++
Template C++
STL
对内置(也就是C-like)类型而言pass-by-value通常比pass-by-reference高效,但当你从C part of C++移往Object-OrientedC++,由于用户自定义(user-defined)构造函数和析构函数的存在, pass-by-reference-to-const往往更好。
尽量以const,enum,inline替换#define
对于单纯常量,最好以const对象或enums替换#defines
使用const创建class专属常量
通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(integral type,例如ints,chars,bools),则需特殊处理。只要不取它们的地址,你可以声明并使用它们而无须提供定义式。但如果你取某个class专属常量的地址,或纵使你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义式如下:
const int GamePlayer::NumTurns;
请把这个式子放进一个实现文件而非头文件。由于 class 常量已在声明时获得初值(例如先前声明NumTurns时为它设初值5),因此定义时不可以再设初值。
可以使用const创建一个class的专属常量,但却不能使用#define创建一个专属常量
为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个成员(member):
class GamePlayer
{
private:
static const int Num=5;//常量声明式
int scores[Num];//使用该常量
...
};
通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(integral type,例如ints,chars,bools),则需特殊处理。只要不取它们的地址,你可以声明并使用它们而无须提供定义式。但如果你取某个class专属常量的地址,或纵使你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义式如下:
const int GamePlayer::Num;//Num的定义式,下面告诉你为什么没赋值
请把这个式子放进一个实现文件而非头文件。由于 class 常量已在声明时获得初值(例如先前声明NumTurns时为它设初值5),因此定义时不可以再设初值。
如果你的编译器不支持上述语法,你可以将初值放在定义式:
class ConstEstimate{
private:
static const int Num;
};//位于头文件内
const int Num=5;//位于实现文件内
使用enum创建类内常量
如果你的编译器不允许使用static const创建类内常量,那么可以使用enum来代替:
class GamePlayer{
enum {Num=5};
int scores[Num];
};
需要注意的是,enum常量是不能被取址的。
对于形似函数的宏(macros),最好改用inline函数替换#defines
使用# define
创建一个比较大小的宏:
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
需要注意的是所有实参都需要加上小括号
以上函数有着很多缺点,但优点是不需要调用函数的开销。
使用inline内联函数,不仅可以获得#define
的效率,还可以获得函数编译和检查。
因此,对于形似函数的宏(macros),最好改用inline函数替换#defines。
内联函数inline和函数的区别
内联函数会进行副本替换,会存在多个副本占用多份内存,而函数只有一个地址只占用一份内存,但函数调用会产生压栈,速度会降低,而内联函数是直接将代码嵌入到调用处,省去了压栈寻址的过程,这是典型的空间和时间转换的问题。
const 语法虽然变化多端,但并不莫测高深。如果关键字 const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
以下两种方式是相同的,都表明指针所指向的内容不可以被改变
void f1(const Widget* pw);
void f2(Widget const* pw);
尽可能使用const
将某些东西声明为 const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
如果关键字 const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
以下写法是等价的,都是限制指针所指内容是常量的:
void f1(const int * pw);
void f2(int const * pw);
使用const修饰函数返回值时,如const引用,则可以既获得返回值传递的高效性,也可以获得返回值的常量性。
编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)
const成员函数
const修饰成员函数,作用是限制对象内成员函数不修改任何成员变量,需要注意的是,在定义和实现中都需要写上const
此时函数内部如果发生变量被改变的行为都会导致编译错误
得知哪个函数可以改动对象内容而哪个函数不行,很是重要。
C++定义,bitwise constness是指成员函数不改变任何成员变量。
但是如果成员函数改变了指针所指物,编译器却也能通过编译,这个问题引出了logical constness概念。
因此以上叙述的是C++定义的成员函数是只要成员函数不更改成员变量,就是const成员函数,而logical constness认为,如果返回值可以被更改,则不属于const成员函数,因为这种方式虽然不是通过成员函数更改了成员变量,但最终还是通过返回值更改了成员变量,这是不被认为是const成员函数的。
如果const成员函数希望能更改某些成员变量,那么这个成员变量应该用mutable进行修饰。
当 const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复
Class TextBlock{
Public:
…
const char & operator[] (std::size_t position) const
{
…
…
…
return text[positon];
}
char& operator[] (std::size_t position)
{
return cosnt_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
…
};
这里共有两次转型:第一次用来为*this
添加 const(这使接下来调用 operator[]时得以调用 const版本),第二次则是从const operator[]的返回值中移除const。
更值得了解的是,反向做法——令 const版本调用 non-const 版本以避免重复——并不是你该做的事。
条款04:确定对象被使用前已先被初始化
为内置型对象进行手工初始化,因为C++不保证初始化它们。
为了减少初始化造成的消耗,c语言会允许变量未初始化。
因此array不保证初始化,而vector却可以保证初始化
构造函数最好使用成员初值列(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
在构造函数中,一定要使用成员初始化列表列出所有成员变量,即使有些变量不需要赋初值,以免某些变量被忘记赋值。
使用成员初始化列表的原因是,如果不使用成员初始化列表,而是在构造函数中赋值,将会面临的过程是,首先调用了默认构造函数,其次调用了赋值运算符,而如果使用成员初始化列表,将会只调用一次复制构造函数。
许多class拥有多个构造函数,这时如果每个构造函数都去列表初始化一次成员变量,则会造成代码冗余,解决这个问题的方法可以是,将内置类型的初始化用赋值方式代替,并封装到函数内,然后在每个构造函数的代码块内调用该函数。因为对于内置变量而言,这两种方式的消耗是差不多的。
//.h文件
class People{
public:
People(const string& name,const string& dress);
};
//.c文件
People(const string& name,const string& dress) //低效写法,会先调用默认构造函数再调用赋值运算符
{
name="XiaoMing";
adress="TianJin";
}
People(const string& name,const string& dress)//成员初始化列表
:name("XiaoMing"), adress(TianJin)
{
}
C++有着十分固定的“成员初始化次序”。是的,次序总是相同:base classes更早于其derived classes被初始化(见条款12),而class的成员变量总是以其声明次序被初始化。即使它们在成员初值列中以不同的次序出现(很不幸那是合法的),也不会有任何影响。
为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-localstatic对象。
static的生命周期
从被构造开始,直到程序结束为止。函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。
程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
所谓编译单元(translation unit)是指产出单一目标文件(single object file)的那些源码。基本上它是单一源码文件加上其所含入的头文件(#include files)。
C++对“定义于不同的编译单元内的non-local static对象”的初始化相对次序并无明确定义。这是有原因的:决定它们的初始化次序相当困难,非常困难,根本无解。
幸运的是一个小小的设计便可完全消除这个问题。唯一需要做的是:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。Design Patterns迷哥迷姊们想必认出来了,这是Singleton模式的一个常见实现手法。
为避免在对象初始化之前过早地使用它们,你需要做三件事。第一,手工初始化内置型non-member对象。第二,使用成员初值列(memberinitialization lists)对付对象的所有成分。最后,在“初始化次序不确定性”(这对不同编译单元所定义的non-local static对象是一种折磨)氛围下加强你的设计。
了解C++默默编写并调用哪些函数
如果你声明一个类却什么都没做,C++会默默帮你生成如下内容:
默认构造函数,拷贝构造函数,复制构造函数,赋值运算符,析构函数。
所有这些函数都是public且是inline的。
如果成员函数被声明为引用类型或const类型,而类内又没有声明赋值运算符,
那么编译器将拒绝编译,除非你定义了赋值运算符。
class temp
{
public:
....
private:
int& Num;
const string Name;
};
若不想使用编译器自动生成的函数,就该明确拒绝
如果不想让类拥有复制构造函数和赋值运算符,可以将复制构造函数和赋值运算符声明为private,并不去实现他们。
更好的做法是,制作一个Uncopyable基类,使用时直接继承基类就可以:
class Uncopyable{
protected:
Uncopyable(){} //允许继承类构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&)//阻止复制构造函数
Uncopyable& operator=(const Uncopyable &);
};
使用时:
class HomeForSale:Uncopyable{
};
继承类构造函数
写子类的构造函数时,必须要看看父类的构造函数能否被自动调用;若不能被自动调用,应该显示调用。
class A
{
int a;
public:
A(int a)
{
this->a = a;
}
};
class B:public A
{
int b;
public:
B(int b):A(10)
{
this->b = b;
}
};
为多态基类声明virtual析构函数
作为基类的class一定要有虚析构函数,这样在销毁基类的时候会继续调用继承类的虚构函数。
不作为基类的class一定不要带有虚析构函数,因为这样会使对象的大小多出50%~100%,因为会多出一个vptr
欲实现出virtual 函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。
注意:
STL容器都不带虚析构函数的,因此不适合做基类。
比如以下这样会导致继承类指针没有被释放:
class SpecialStirng:std::String{
};
SpecialString* pss=new SpecialString("abc");
string* ps;
ps=pss;
delete ps;//此时pss没有被释放,造成内存泄漏
条款08:别让异常逃离析构函数
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序
C++允许在析构函数中抛出异常,但不建议这样做,因为STL容器内包含的对象,如果每个对象都会抛出异常,在同时出现两个异常的情况时,则会导致unexception 行为。
如果确实在析构函数中发生了异常,导致程序不能再继续运行,可以采取两种方式:
如果close抛出异常就结束程序。通常通过调用abort完成
DBConn::~DBConn(){
try{
db.close();
}
Cath(){
std::abort();//如果close()发生异常,调用abort终止程序,并记录
}
}
吞下因调用close而发生的异常
DBConn::~DBConn(){
try{
db.close();
}
Cath(){
}
}
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作
应该提供一个函数供客户处理异常:
class DBConn{
public:
close(){ //供客户使用的新函数
db.clsose();
closed=true;
}
~DBConn(){
if(!closed){ //关闭连接,如果客户没有调用close
try{db.closed();}
catch(){};
}
}
private:
DBConnection db.close();
bool closed;
};