![](http://www.cublog.cn/images/face/001.gif)
导读
如果没有什么训练和素养,就贸然使用C++,会导致写出来的代码不易理解,不宜维护,不宜扩充,缺乏效率,而且容易出错。
Scott Meyers在狠夸了C++语言怎么好之后,抛出了这么一句,点名了使用C++的难度,我们在读一些前辈的代码时,有时经常觉得难懂,这是否属于不易理解、不宜维护呢?其实不然,C++语言引入继承和多态后,使语言更加具有隐藏性,层次更加复杂!特别是使用了多种设计模式的程序,对于没有过训练的程序员更加艰涩难读。
那都需要什么训练呢?之一,细读一本C++教程后,读这本书,然后深入实践C++,“找出C++可能绊倒你的障碍,学习避开它们”;之二,学习常用设计模式,理解高手为了易于扩展而使用类层次;之三,多看库的源码,理解库的类设计和层次,如STL、BOOST、ACE等。
1、拷贝构造函数被调用:
第一种:
test b;
test a = b;
第二种:
传值方式将对象传递给方法作参数或返回
test operator+(const test a, const test b);
2、构造函数通常必须检验其参数的有效性,而大部分赋值运算符不必如此,因为其参数必然合法(检验已在它的构造函数中完成)。
3、语言内建的关系运算符(如>、<、==)的返回类型都是bool类型
![](http://www.cublog.cn/images/face/001.gif)
<改变旧有的C习惯>
1.尽量以const和inline取代#define
C语言用#define可以做到两点,第一,定义常量,如#define PI 3.1415926或#define NAME "Jade";第二,定义宏表达式(函数),如#define min(a,b) (a)<(b)?(a):(b)。
但是这样做会有些问题,首先,不易调试,因为这个宏在预编译时就被替换为实际代码了,跟踪器一般都很难跟踪(inline函数一样不能被跟踪!见条款33);其次,使用宏表达式,虽然样子像函数,但是由于会被替换为实际代码,行为又和函数不同,容易造成错误。
因此,建议使用const变量和inline函数替换宏的这两个功能。
const double PI = 3.1415926;
const char NAME[] = "jade";
inline min(a,b) { if (a < b) return a; else return b; }
class A
{
public:
static const int N;
};
const int A::N = 100;
2.尽量以<iostream>取代<stdio.h>
Scott Meyers建议我们使用<<和>>代替printf和scanf函数。
因为只要一个自定义类型实现了<<操作符(friend ostream& operator<<(ostream &os, const ClassName &r);),就可以使用<<打印自定义类型的内容,这是用printf所无法做到的。
<iostream>是在std名字空间里的函数,而<iostream.h>中的函数是全局函数,没有名字空间
3.尽量以new和delete取代malloc和free
如果使用new获得的指针,就使用delete释放它;如果用malloc申请的空间,就用free释放它,千万不要混用!
4.尽量使用C++风格的注释形式
C语言风格注释/**/不支持嵌套
![](http://www.cublog.cn/images/face/001.gif)
<内存管理>
5.使用相同形式的new和delete
使用new[]生成对象数组,释放时也要使用delete[],如果使用了delete,则除第一个对象的析构函数被调用外,其他对象的都没有被调用
6.记得在destructor中以delete对付指针成员
有point成员的类中,应做到:
1、在每一个构造函数中将该指针初始化。如果没有任何一个构造函数会将内存分配给该指针,那么指针应该初始化为NULL。
2、在=操作符中将指针原有的内存删除,重新分配一块。
3、在析构函数中delete这个指针(delete一个NULL指针是安全的),除非这个指针不是在这个类中被new出来的,除了smart point。
7.为内存不足的状况预作准备
当C++使用new分配内存出现内存不足的情况时,会抛出std::bad_alloc异常(不使用new(nothrow)形式,返回NULL),应该捕捉这个异常进行处理。另外还可以设置一个分配内存失败时的回调函数,在头文件<new>中提供了一个函数set_new_handler用来注册这个回调函数,原型如下:
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
参数为注册的回调函数,返回为原来的回调函数。
如果注册了回调函数,在new抛出bad_alloc异常前,会回调这个函数,如下例所示:
#include <memory>
using namespace std;
void func()
{
set_new_handler(NULL); //如果不把处理函数设为空,它会被new不停地调用,直到分配内存成功
cout << "call func()" << endl;
}
int main(int argc, char *argv[])
{
new_handler f = set_new_handler(func);
try
{
char *p = new char[0x7fffffff];
}
catch(bad_alloc&)
{
set_new_handler(f);
cout << "get exception" << endl;
}
getchar();
return 0;
}
会打印出:
call func()
get exception
Scott Meyers并对set_new_handler函数行为作出了自己的建议:
1、让更多的内存可用,在一开始分配出一块大的内存,当发生内存不足时,释放此内存。
2、安装不同的new_handler,改变new_handler的行为。
3、卸载new_handler,即再次调用set_new_handler指针p传NULL,这样就不会每次发生内存不足重复调用new_handler了,它会抛出std::bad_alloc异常。
4、抛出自己的异常,可以抛出std::bad_alloc或它的派生类。
5、不返回,直接调用abort或exit函数。
也可以使每个类拥有自己的new_handler,这需要重载new操作符。因为这个new操作符和new_handler的行为都是相同的,所以可以定义为一个模板,让其他类继承它,得到这些接口。
template<class T>
class NewHandlerSupport
{
public:
static new_handler set_new_handler(new_handler p);
static void *operator new(size_t size);
private:
static new_handler currentHandler;
};
template<class T>
new_handler NewHandlerSupport<T>::set_new_handler(new_handler p)
{
new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<class T>
void* NewHandlerSupport<T>::operator new(size_t size)
{
/* 使全局new的handler为自己的new_handler */
new_handler globalHandler = std::set_new_handler(currentHandler);
void *memory;
try
{
memory = ::operator new(size);
}
catch (std::bad_alloc&)
{
std::set_new_handler(globalHandler);
throw;
}
std::set_new_handler(globalHandler);
return memory;
}
//使得每一个currentHandler为0
template<class T>
new_handler NewHandlerSupport<T>::currentHandler;
使用举例:
class X: public NewHandlerSupport<X> {
};
void noMoreMemory();
X::set_new_handler(noMoreMemory);
X *p = new X[N]; //内存不足
其实这样使用起来也挺麻烦,可以直接在类内定义自己的new_handler方法(作为一个抽象方法),不用set_new_hander直接使用,就更加方便了。
8.撰写new和delete操作符时应遵守公约
自己编写的new操作符的行为应与缺省的new操作符保持一致,就像上一个主题讲的那些new的特征:
1、当分配内存成功时,返回内存的地址;分配失败,如果注册了处理函数,则会调用此函数,(如果没有退出)并一再尝试分配内存,并在每次失败后再调用此函数,直到分配内存成功或处理函数中抛出异常或将注册的处理函数撤消(设为NULL,这时再分配失败则按默认方式处理(见后));如果没有注册处理函数,则抛出bad_alloc异常。
2、如果用户要求分配0字节内存,就给他分配1字节并返回此地址
3、没有3了吧?
Scott Meyers提供的伪代码说明:
void *operator new(size_t size /* ,... */)
{
if (size == 0)
size = 1;
while (true)
{
尝试分配size个字节的内存;
if (分配成功)
{
return 内存的地址;
}
//由于不能访问当前配置的handler,所以使用这种方法获得
new_handler globalHandler = set_new_handler(0);
set_new_handler(globalHandler);
if (globalHandler)
(*globalHandler)();
else
throw std::bad_alloc();
}
}
很值得关注的一个问题,基类的new操作符会被它的子类继承!如果子类自己不实现new操作符,将会调用基类的new操作符,而不是缺省的!
class Base {
public:
static void *operator new(size_t size);
};
class Derived: public Base {
};
Derived *p = new Derived; //调用Base::operator new,传入的size为sizeof(Derived)
Scott Meyers的建议是在基类的new中检查分配内存大小是否等于sizeof(基类),如果不等于则调用缺省的new。
void *Base::operator new(size_t size)
{
if (size != sizeof(Base))
return ::operator new(size);
...
}
delete也和new一样要符合标准,但相对简单,只要保证delete一个NULL指针是安全的就可以了。
void operator delete(void *p)
{
if (p == 0)
return;
释放p指向的内存;
return;
}
void Base::operator delete(void *p, size_t size)
{
if (p == 0)
return;
//如果这个类的destructor不是virtual的,传给delete的size是错误的,可能等于sizeof(Base)
//所以这么比较的前提是Base必须使用virtual destructor
if (size != sizeof(Base))
{
::operator delete(p);
return;
}
...
}
另外要提一句,在调完自定义的new后,C++会自动去调构造函数;delete时则是先调析构函数,再调自定义的delete。
最后还有个有意思的,就是当函数有包含虚函数时,传给new的size会自动增加4字节的虚指针,而虚指针的初始化呢?会在构造函数中自动完成,这样用户重载自己的new操作符就完全不用关心虚表了,想得很周到。
9.避免遮掩了new的正规形式
new()除了有一个size_t的参数外还可以添加自己的参数void *operator new(size_t, 自己参数),在调用new时指出,new (值) A;
如果你定义了一个自定义形式的new(不同于缺省new的参数),而没有定义缺省new形式,将导致调用像new A这样的语句失败。 所以应该再定义一个缺省new形式,或给新添参数加一个缺省值。
void operator delete(void *p);
void operator delete(void *p, size_t size);
10.如果你写了一个new操作符,那对应也写一个delete操作符
通常编写自己的new操作符,是为了分配小内存。首先,申请一块大内存,然后,每次调new时,从中分配一块空闲的小内存,返回其地址。C++缺省的new会浪费一些内存空间(
在分配的内存前保存内存的大小,这也是为什么需要自己的delete对应自己的new的原因,自己的new并没有保存它,使用缺省delete找不到它)。
不要用缺省的delete释放自定义的new分配的内存,其实如果你知道自己的new做了什么,就绝对不会这么做了。
Scott Meyers的意思是这样地:
class Pool
{
public:
Pool(size_t n);
~Pool();
void* alloc(size_t n);
void free(void *p, size_t n);
...
};
class A
{
public:
static void* operator new(size_t size)
{
return memPool.alloc(size);
}
static void operator delete(void *p, size_t size)
{
memPool.free(p, size);
}
privat:
static Pool memPool;
}
Pool A::memPool(sizeof(A) * N);
但Pool怎样工作,Scott Meyers的意思是让它的设计者去操心吧,其实就是推给读者了。。。
其实关于这个问题在《Modern C++ design》里有更详细的方案描述。
![](http://www.cublog.cn/images/face/001.gif)
<构造函数、析构函数和赋值(=)运算符>
11.如果class内有动态分配内存的成员变量,则为此类声明一个copy构造函数和一个赋值运算符
默认的赋值运算符和拷贝构造函数是按位拷贝,它会拷贝指针的值,这很可怕!
问题那就多了:首先,a = b可能会造成原来a的内存泄漏;A a = b,如果b的内存释放将导致a的内存失效;其次,使用值传递时,临时变量退出时被析构导致指针指向内存失效。
实现拷贝构造函数和赋值运算符:
1、申请新内存,进行拷贝
2、使用引用计数,记录内存被引用次数,引用次数为0才释放。
如果确实不会使用到这两个函数时呢?请看第27条。
12.在构造函数中尽量以初始化动作取代赋值动作
首先,构造函数中使用赋值实际上是两步动作,第一,调用变量的默认构造(因为如果没有在初始化列表中调用子对象的构造函数,编译器就会默默地调用子对象的默认构造函数初始化它,这就是构造过程中先调用基类和子对象的构造函数再调用自己的构造函数的原则),第二,调用赋值;而初始化呢?只有一步就是调用变量对应的构造函数,如拷贝构造。
其次,const成员变量和引用成员变量只能够被初始化,不能够被赋值。
另外,静态成员变量的初始化见第47条。
13.初始化链表中的成员初始化次序应该和其在class内的声明次序相同
类成员变量是以它们在类中的声明次序来初始化,与它们在成员初始化列表中出现的次序无关。
继承时,基类于类成员变量之前初始化;多重继承时,基类的初始化顺序和继承的顺序一致。亦与初始化列表中的次序无关。
一个类的析构过程是完全和它的构造过程相反的。
综上所述:
1、初始化链表中的成员初始化次序应该和其在class内的声明次序相同
2、成员变量初始化列表起始处列出基类的初值设定式
14.总是让基类拥有virtual析构函数
为了防止在使用多态时,用基类指针指向派生类成员,而delete此指针时,调用的是基类的析构函数,而没有调用派生类的,所以应该把基类的析构函数声明为virtual函数。
找不到合适的函数作为纯虚函数,就是用纯虚析构函数吧!
内联虚拟函数并不是一个独立存在的函数,怎样得到它们的地址放在VTABLE中呢?见条款33
15.令赋值操作符函数传回*this的引用(非const)
int a, b, c;
(a = b) = c;
这样是可以的,那下面这样就应该也可以:
test a, b, c;
(a = b) = c;
16.在赋值操作符函数中为所有的成员变量赋值
在向类中加入新成员数据时,同步更新构造函数和赋值运算符。
在派生类的赋值运算符中,还要处理基类的赋值工作。
class Base {
public:
Base(int init = 0): x(init) {}
Base(const Base& rhs): x(rhs.x) {}
//私有成员可以被本类方法调用,即使是其他同类对象的私有成员
private:
int x;
};
class Derived: public Base {
public:
Derived(int init): Base(init), y(init) {}
Derived(const Devired& rhs): Base(rhs), y(rhs.y) {}
Derived& operator=(const Derived &rhs);
private:
int y;
};
Devrived& Derived::operator=(const Derived &rhs)
{
if (this == &rhs)
return *this;
//Base::operator=(rhs); //rhs向上转换,有的编译器可能不支持调用编译器生成的operator=
static_cast<
Base&>(*this) = rhs; //向上转换(Base&)调用Base::operator=,如果operator=是多态的,就不能这样做了,上面的方法可能更有保证
y = rhs.y;
return *this;
}
17.在赋值操作符函数中检查是否自己赋值给自己
防止a = a或test a; test& b = a; a = b; 使得类其中的指针成员发生错误!
![](http://www.cublog.cn/images/face/001.gif)
<类与函数之设计和声明>
设计每一个类都会要求你面对以下问题:
@ 对象应该如何被产生和销毁? 影响构造函数和析构函数
@ 对象的初始化动作和赋值动作有何不同? 影响赋值运算符的行为
@ 对象以传值方式传递给新类型,是什么意思? 影响拷贝构造函数
@ 对于新类型而言,合法值的规范是什么? 影响成员函数(特别是构造函数和赋值运算符)中做的错误检查
@ 新类型能够塞进某个继承体系之中吗? 影响多态作用,virtual函数的重写(派生类),或virtual函数的定义(基类)
@ 哪一种类型转换时允许的? 影响重载类型转换运算符
@ 什么样的运算符和函数对于新类型是合理的? 影响接口声明
@ 需要明白禁止使用什么样的标准运算符和函数? 影响将那些函数声明为private
@ 谁有权力取用新类型的成员? 影响成员应该是public, protected, 还是private,还有哪些类或方法是friend
@ 新类型一般化的程度有多高? 影响是否将此类定义为一个class template
18.努力让接口全面和最小化
别人想用这个类做什么用,决定了类的接口有哪些
一个全面的接口,即对于用户可能希望完成的任何合理工作,类都应该提供一个合理的方法来完成。
一个最小化的接口,即尽可能让函数个数最少,不至于有任何两个成员函数功能重叠。
19.区分成员方法,非成员方法和友元方法三者
往往一个成员方法,变一个思路,通过提供一些基本的对外接口,也可以通过一个非成员方法实现;而如果将一个非成员方法设为友元,也可以不提供对外接口,而通过一个非成员友元方法实现。
那应该如何选择这几种方法呢?
1、虚函数必须是类成员函数
2、如果一个操作的左右两边参数可以互换,并且需要进行隐式转换,则要使用非类成员函数
例如,一个分数类Rational的乘法operator*,既会有r*2这样的使用,也会有2*r这样的使用,而2.operateor*(r),这样的接口是没有的,而且作为左值又不能进行转换。
3、一些惯用操作,如operator<<和operator>>应为非类成员函数
4、其他情况应该设计为类成员函数
构造函数、类型转换操作符,explicit constructor
20.避免将成员变量放在公开接口中
使用函数,可以使你更精确地掌控data member的可存取性。另外,可以做一些错误检查(抛出异常),或额外处理(使用计算式代替data member,不要让客户使用data member自己去计算)
21.尽可能使用const
const的作用是在语意上告诉使用者和编译器“某个对象不应该被改变”,这样你会得到编译器的帮助,使用者也会得到某些约束和警告。
如果一个函数的返回值不应被赋值或改变(被赋值不合理)就应该返回const对象,例如:
const Rationnal operator*(const Rational& lhs, const Rational& rhs);
不应该会有语法支持(a * b) = c,那么就不允许使用者这样做。
也有可能使用一个函数的返回值作为另一个函数的参数的情况,这时函数的参数就不应该修改它,应该把参数定义为const对象引用。
一个const对象调用const成员方法,如果成员方法返回类中内部数据的引用,则应返回const引用,不要给使用者造成可以修改const对象的误会!
如果const对象和非const对象都可能调用一个成员方法,就生成两个版本:
const char& String::operator[] const; //const函数重载
char& String::operator[];
22.尽可能使用传引用,少用传值
传递引用不会引入传递值时拷贝构造函数和析构函数的效率问题,还会防止“对象切片”问题(出现在派生类对象向基类转换过程)。
23.当你必须返回一个对象时,不要尝试返回引用
传回局部变量的引用是错误的( 见条款31),
传回局部变量是可以的,
传回构造函数构造变量更好。
24.在函数重载和缺省参数之间谨慎抉择
函数重载和缺省参数之间所引起的混淆,因为它们都允许以不同的形式调用同一个函数名称。
如果可以为函数参数选择一个缺省值,而且函数只要一个算法,可以使用缺省参数,否则应该使用重载函数。
25.避免对指针类型和数值类型进行重载
C++是强类型语言,常数可能与参数不一致
0L 0UL 0.0F分别为long int、unsigned long int、float
#define NULL 0
void f(int x);
void f(string *ps);
f(NULL) //会调用f(int)
f(static_cast<string*>(NULL)); //这样才会调用f(string*)
#define NULL (void*)0
f(static_cast<string*>(NULL)); //这样才能编译过
#define NULL 0L //这样f(NULL)编译不过
没有特别方便的解决办法
26.
防范潜在的模糊状态
自定义类型转换引起的含糊:
class B;
class A {
public:
A(const B&);
};
class B {
public:
operator A() const;
};
void f(const A&);
B b;
f(b); //错误!不知道使用哪个转换?
基本类型转换引起的含糊:
void f(int);
void f(char);
double d = 6.02;
f(d); //错误!double可以转换为int,也可以转换为char,不知道调哪个f
f(static_cast<int>(d));
f(static_cast<char>(d));
多继承引起的含糊:
class Base1 {
public:
virtual int doit();
};
class Base2 {
public:
virtual int doit();
};
class Derived: public Base1, public Base2 {
};
Derived d;
d.doit(); //错误!不知道调哪个基类的doit()
失去了虚函数多态的特性,函数必须被明确的调用!解决办法见条款43。
d.Base1::doit(); //ok,调用Base1::doit()
d.Base2::doit(); //ok,调用Base2::doit()
private方法定义不能影响这种模糊性!
27.
如果不想使用编译器自动生成的成员函数,就应该明确拒绝它
因为编译器会自动产生赋值运算符,所以如果不想让类具有赋值行为,就把赋值运算符声明为private,并且不需要定义。
template<class T>
class Array {
private:
//不要定义这个函数
Array& operator=(const Array &rhs);
};
如果不想让类具有其他编译器自动生成函数的能力,也可以这样做。
28.尝试切割全局命名空间
使用命名空间
![](http://www.cublog.cn/images/face/001.gif)
<类与函数之实现>
29.避免返回内部数据的handles
返回内部数据的handle违反了数据抽象性!
30.避免写出一个成员函数,返回一个指向较低存取层级(less accessible then themself)成员的非const指针或引用
不要用public成员方法返回private或protected数据成员和成员方法的引用或指针。这样将使存取限制失去作用!
class Address {};
typedef void (Person::*PPMF)();
class Person {
publci:
Address& personAddress() { return address; }
static PPMF getFunc()
{
return &Person::verifyAddress;
}
...
private:
Address& address;
void verifyAddress();
};
Person scott();
Address& addr = scott.personAddress(); //存取了private数据成员address
PPMF pmf = scott.getFunc(); //存取了private成员方法verifyAddress
(scott.*pmf)();
31.千万不要传回函数内local对象的引用或函数内以new获得的指针所指的对象
local对象在离开函数scope后就会被析构,当使用引用时,它所引用的内存已经被释放了!
返回new获得对象地址,然后由使用者delete它,只会引来麻烦!(如果函数返回值直接被其他函数做参数使用后就丢失了,更是无从处理)
32.尽量延缓变量定义
延缓变量的定义,直到能够给它赋予初值参数为止,这样只需一次构造,既可以改善程序效率,又可以增加程序清晰度。
33.
明智地运用内联
太多和太大的内联函数会使得程序代码膨胀,导致病态的换页行为(thrashing现象)。
大多数编译器会拒绝将复杂的(也就是内含循环或递归调用的)函数inline化,而所有的virtual函数都会阻止inline的进行,因为virtual意味着“等待,知道执行时期在决定应该调用哪一个函数”,而inline却意味着“在编译阶段,将调用动作以备调用函数的本体来代替”。
如果编译器无法将你所要求的函数inline化,会给你一个警告信息,见条款48。
不管是编译器,还是inline函数的使用者,当需要得到inline函数的地址时,就会导致产生一个inline函数的函数实体。
inline void f() {}
void (*pf)() = f;
构造和析构一个对象数组,不可能直接使用inline构造函数和析构函数代替数组初始化代码。见条款M8。
程序库的设计者使用inline函数时还要考虑一个问题:inline函数无法随着程序库的升级而升级。inline函数被用户编译到了他们的应用程序中,只有重新编译才能改变inline函数的定义。而如果程序库采用动态连接,升级后的非inline函数会自动被应用程序使用。
函数内定义了static对象,则应避免将它声明为inline,详见条款M26。
inline函数和宏定义有个相同的问题,inline函数不能被调试,所以不要将复杂的函数声明为inline。
34.将文件之间的编译依赖关系降到最低
首先看一个会有依赖关系的类定义:
#include <string>
#include "date.h"
#include "address.h"
#include "country.h"
class Person {
public:
Person(const string& name, const Date& birthday, const Address& addr, const Country& country);
virtual ~Person();
//... 拷贝构造函数和赋值运算符
string name() const;
string birthDate() const;
string address() const;
string nationality() const;
private:
string name_;
Date birthDate_;
Address address_;
Country citizenship_;
};
要使这个Person类编译通过,需要引用string, Date, Address, Country这些类的定义。如果这些类或它们依赖的其他类中任一个改变了实现代码,含有Person类的所有文件就得重新编译。(用户包含person类的定义就等于包含了person类用到的其他类的定义,只有消除这层关系才能够消除编译依赖)
消除编译依赖的几种技巧:
@ 在类中如果使用某一个类对象的引用或指针,只需用到该类的声明,如果使用对象,就需要该类的定义。所以尽量使用引用或指针。
@ 声明一个函数而它会用到某个类时,并不需要该类的定义,即使函数是参数传值时用到该类。使用函数代替直接定义类对象。
@ 不在头文件中再引用其他头文件,除非不这样头文件就无法编译通过。
两种消除编译依赖的Person类定义方法:Handle类和Protocol类定义方法
Handle类
通过一层封装,让实现与声明分离
class string;
class Date;
class Address;
class Country;
class PersonImpl;
class Person {
public:
Person(const string& name, const Date& birthday, const Address& addr, const Country& country);
//用户必须引用这些类定义,才能使用这个构造函数,那怎么能消除依赖呢?
virtual ~Person();
//... 拷贝构造函数和赋值运算符
string name() const;
string birthDate() const;
string address() const;
string nationality() const;
private:
PersonImpl *impl; //用户在引用Person类时,不需要引用PersonImpl,所以也就与实现细节无关
};
//Person类实现文件
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const string& name, const Date& birthday, const Address& addr, const Country& country)
{
impl = new PersonImpl(name, birthday, addr, country);
}
string Person::name() const
{
return impl->name();
}
Protocol类
Person定义为抽象基类,只是为派生类指定一个接口,没有数据成员,也没有构造函数,只有一个虚析构函数和一组用来表示接口的纯虚函数。
class Person {
public:
virtual ~Person();
virtual string name() const = 0;
virtual string birthDate() const = 0;
virtual string address() const = 0;
virtual string nationality() const = 0;
};
用户必须使用Person类的引用或指针,产生方法通常是使用一个函数(工厂函数)构造一个实体化的派生类对象,传回其指针。
class Person {
public:
static Person* makePerson(const string& name, const Date& birthday, const Address& addr, const Country& country);
};
Person *Person::makePerson(const string& name, const Date& birthday, const Address& addr, const Country& constry)
{
return new RealPerson(name, birthday, addr, country);
}
不能在Handle类和Protocol类中使用inline函数。
![](http://www.cublog.cn/images/face/001.gif)
<继承关系与面向对象设计>
35.确定你的公开继承,使它成为is-a的模型
C++公开继承体现了派生类和基类的is-a的关系,基类有的行为派生类也应该有(Liskov替代原则),但是有时在我们的语言中“是一个”的关系(语言的不严谨性),并不完全能够符合公开继承的替代原则。例如,企鹅是一种鸟,但是却不能将飞行定为鸟的一个接口,因为企鹅不能够飞行。
所以我们在确定继承时,除了按照我们的常识外,还要考虑C++继承设计的一些原则。
开闭原则
36.区分接口继承和实现继承
@ 声明一个纯虚函数的目的是为了让派生类只继承其接口
子类设计者自己提供纯虚函数的实现代码
纯虚函数也可以有定义,除了可以在子类方法中使用外,还可以使用派生类引用或指针调用(静态调用)。
Shape *ps = new Rectangle;
ps->Shape::draw(); //这样可以为一般(非纯)虚函数提供更安全的缺省行为
@ 声明一般(非纯)虚函数的目的是为了让派生类继承该函数的接口和缺省行为
子类设计者可以不实现虚函数,而使用基类实现的缺省版本
基类的虚函数实现了类的共同性质,提高了代码复用性
如果派生类本应该使用自己的实现替换虚函数的缺省版本,但是没有,将造成隐藏的错误!虚函数一定要引起注意。
可以使用有定义的纯虚函数防止上述错误的出现:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
};
void Airplane::fly(const Airport& destination)
{
//default fly behavior
}
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{
Airplane::fly(destination); //ModelA必须指明使用的是默认行为
}
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination);
};
//ModelB有自己的飞行行为
void ModelB::fly(const Airport& destination)
{
//ModelB fly behavior
}
@ 声明非虚函数的目的是为了令派生类继承函数的接口及其实现
如果一个成员方法不是虚函数,则意味着它并不打算在派生类中有不同的行为,任何一个派生类都不应该尝试改变其行为,即我们不应该在派生类中重新定义它。
37.绝对不要重新定义继承而来的非虚拟函数
派生类要改变一个函数,就把它定义为虚函数,定义为非虚函数就应该保持不变
38.绝对不要重新定义继承而来的缺省参数值
虚函数是动态绑定的,而缺省参数值却是静态绑定的。
enum ShapeColor {RED, GREEN, BLUE};
class Shape {
public:
virtual void draw(ShapeColor color = RED) const = 0;
};
class Rectangle: public Shape {
public:
virtual void draw(ShapeColor color = GREEN) const;
};
class Circle: public Shape {
public:
virtual void draw(ShapeColor color) const;
};
Shape *ps;
Shape *pc = new Circle;
Shape *pr = new Rectangle;
pr->draw(); //调用Rectangle::draw(RED),默认参数跟指针的类型有关,而与指向类型无关
39.避免在继承体系中做向下转型动作
把所有派生类整合到一个基类集合中,往往会引入向下转型,使用基类指针调用派生类才有的方法,肯定是不行的。组成这种集合前,要慎重考虑其要达到的目的!
尽量使用虚函数代替向下转型动作,如果不得已非要使用,需使用dynamic_cast转型,如果指针或引用的动态类型(即指向对象类型)与其转型目标不一致,dynamic会失败返回NULL,所以向下转型必须判断结果,但是if-elseif-else组合会影响效率。
40.通过layering技术来建模has-a(有一个)或is-implemented-in-terms-of(根据某物实现)的关系
有时当is-a(是一个)不适合用来实现复用时(见条款35,public继承的替代原则),可以尝试使用is-implemented-in-terms-of(根据某物实现)来复用,即将要复用的类作为新类的子对象。
书中举例使用list来实现一个set(任意类型对象的集合,不允许元素重复),因为按照is-a的原则可以对list成立的行为,对set也应该成立,但是可以让list包含重复元素,但是对set却不可以,所以不能使用is-a的关系描述两者。但是可以根据list实现一个set,所以可以以此关系描述。
template <typename T>
class Set {
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
int cardinality() const; //集合元素的个数(集的势)
private:
list<T> rep;
};
template<typename T>
bool Set<T>::member(const T& item) const
{
return ( find(rep.begin(), rep.end(), item) != rep.end() );
}
template<typename T>
void Set<T>::insert(const T& item)
{
if (!member(item))
rep.push_back(item);
}
template<typename T>
void Set<T>::remove(const T& item)
{
list<T>::iterator it = find(rep.begin(), rep.end(), item);
if (it != rep.end())
rep.erase(it);
}
template<typename T>
int Set<T>::cardinality() const
{
return rep.size();
}
备注:layering技术就是组合复用
41.区分继承和模板
模板应该用来产生一群类,其中
对象类型不会影响类的函数行为
继承应该用于一群类身上,其中对象类型会影响类的函数行为
42.明智地运用私有继承
私有继承和layering技术一样也是用来描述is-implemented-in-terms-of(根据某物实现)关系,但是layering类(被包含的)必须使接口public,才能让layered类使用,用户也可以layering类的public接口,有时会带来危害。私有继承可以使用protected成员来阻止其他人使用这些接口,而且继承还可以带来使用虚函数的能力。
template<typename T>
class Stack: private GenericStack {
public:
void push(T *objectPtr)
{
GenericStack::push(objectPtr);
}
T *pop()
{
return static_cast<T*>(GenericStack::pop());
}
bool empty() const
{
return GenericStack::empty();
}
};
class GenericStack {
protected: //阻止其他类使用此类的方法
GenericStack();
~GenericStack();
void push(void *object);
void *pop();
bool empty() const;
private:
struct StackNode {
void *data;
StackNode *next;
StackNode(void *newData, StackNode *nextNode) : data(newData), next(nextNode) {}
};
StackNode *top;
GenericStack(const GenericStack& rhs); //阻止拷贝构造函数和赋值运算符(条款27)
GenericStack& operator=(const GenericStack& rhs);
};
43.明智地运用多继承
多继承会带来模棱两可的问题(见条款26)
如何使基类的虚函数仍有用?
class Lottery { //彩卷
public:
virtual int draw();
};
class GraphicalObject {
public:
virtual int draw();
};
class AuxLottery: public Lottery {
public:
virtual int lotteryDraw() = 0;
virtual int draw()
{
return lotteryDraw();
}
};
class AuxGraphicalObject: public GraphicalObject {
public:
virtual int graphicalObjectDraw() = 0;
virtual int draw()
{
return graphicalObjectDraw();
}
};
class LotterySimulation: public AuxLottery,
public AuxGraphicalObject {
public:
virtual int lotteryDraw();
virtual int graphicalObjectDraw();
};
//在这个类继承体系中,单一而模棱两可的名称draw被有效地分割为两个明确的,操作性质等同的名称,lotteryDraw和graphicalObjectDraw:
LotterySimulation *pls = new LotterySimulation;
Lottery *pl = pls;
GraphicalObject *pgo = pls;
pl->draw(); //调用LotterySimulation::lotteryDraw
pgo->draw(); //调用LotterySimulation::graphicalObjectDraw
@ 菱形继承
class A { public: virtual void mf(); };
class B : virtual public A {};
class C : virtual public A { public: virtual void mf(); };
class D : public B, public C {};
避免virtual基类(A)拥有数据成员!
C类override了A类的虚函数mf(),那么:
D *pd = new D;
pd->mf(); //调用那个版本?A::mf()或C::mf()?
如果A是B和C的nonvirtual基类,则发生模糊错误;如果A是B和C的virtual基类,则调用C::mf()
@
避免引入菱形继承!避免virtual基类!
virtual基类会增加派生类的大小,因为除了数据外,还像虚函数那样加入了其他信息!
44.说出你的意思并了解你所说的每一句话
对上述条款的总结:
@ 共同的基类意味着共同的特性
@ 公开继承意味着“是一种(is-a)”的关系
@ 私有继承意味着“根据某物实现(in-implemented-in-terms-of)”的关系,这种继承关系没有任何概念上的关系
@ layering意味着“有一个(has-a)“或“根据某物实现(in-implemented-in-terms-of)“的关系
公开继承中:
@ 纯虚函数意味着只有接口会被继承。
@ 一般(非纯)虚拟函数意味着函数的接口及缺省实现代码会被继承。
@ 非虚函数意味着此函数的接口和其实现代码都会被继承。
![](http://www.cublog.cn/images/face/001.gif)
<杂项讨论>
45.清楚知道C++编译器默默为我们完成和调用哪些函数
class Empty {};
相当于
class Empty {
public:
Empty() { } //默认构造函数
Empty(const Empty& rhs) //拷贝构造函数
{
//调用非static数据成员的拷贝构造函数,如果基本类型的成员则执行位拷贝
}
~Empty() { } //析构函数
Empty& operator=(const Empty& rhs) //赋值运算符
{
//调用非static数据成员的赋值运算符,如果基本类型的成员则执行位拷贝
}
Empty* operator&() { return this; } //取址运算符
const Empty* operator&() const { return this; } //常量取址运算符
};
如果一个类声明有至少一个构造函数,则编译器不会为它产生默认构造函数。
如果含有引用成员或const成员,则编译器拒绝产生赋值操作符,因为不能修改它们
46.宁可编译和连接出错,也不要执行时才错
设计用户使用错误将在编译期报错的类
class Month {
public:
static const Month Jan() { return 1; }
...
static const Month Dec() { return 12; };
private:
Month(int number): monthNumber(number) {}
const int monthNumber;
};
class Date {
public:
Date(int day, const Month& month, int year);
};
Date d(22, Month::Jan(), 1999); //只能使用Jan()~Dec(),防止用户输入错误
47.使用非local静态对象之前先确定它已有初值
非local静态对象包括:
@ 定义于global或名字空间里
@ 在某个class内声明为static
@ 在某个文件内定义为static
无法控制不同编译单元中的非local静态对象的初始化次序:
//FileSystem编译单元
class FileSystem {
//...
};
FileSystem theFileSystem;
//Directory编译单元
class Directory {
public:
Directory();
};
Directory::Directory()
{
//调用用到theFileSystem的成员函数来创建一个Directory对象
}
Directory tempDir; //给暂时文件用的一个目录
tempDir初始化时使用到了theFileSystem,但是有可能theFileSystem还没有被初始化呢!
使用Singleton模式代替非local静态对象:
class FileSystem {
//...
};
FileSystem& theFileSystem()
{
static FileSystem tfs;
return tfs;
}
class Directory {
//...
};
Directory::Directory()
{
//使用theFileSystem()代替theFileSystem对象
}
Directory& tempDir()
{
static Directory td;
return td;
}
48.不要对编译器的警告信息视而不见
忽略任何一个警告之前,必须精确了解编译器企图告诉你的是什么!
49.尽量让自己熟悉C++标准程序库
C函数库的机能也有对应的新式C++头文件,其名称类似<cstdio>、 <cstring>。这类头文件提供“与对应之旧式C头文件相同”的内容,但全部放在std中。
标准程序库中几乎每一样东西都是template,例如iostreams用来处理字符流,可以是char/wchar_t/Unicode,可以在实例化basic_ostream<>、basic_istream<>时指定字符类型。
typedef basic_istream<char> istream;
typedef basic_ostream<char> ostream;
这些template除了基本类型参数外,还有一些关于细节的特性,这些在C++标准规格书中被称为traits,以额外的template参数指定。还有Allocator类型用来说明如何分配和释放动态内存。
template<class charT,
class traits = char_traits<charT>,
class Allocator = allocator<charT> >
class basic_string;
typedef basic_string<char> string;
注意traits参数和Allocator参数的默认值,这种形式很典型,不但提供了足够的弹性,也可为典型的情况忽略其复杂度。
C++标准库的主要组件:
@ C标准函数库
@ iostreams
@ strings
@ Containers 类模板
vector、list、queue、stack、deque、map、set、bitset
@ Algorithms 函数模板
算法将容器的内容视为一个序列,每一个算法都可施行于由所有元素构成的序列(或子序列)上。
for_each:将某个函数施行于序列中的每个元素上
find:找出序列中持有某特定值的第一个位置
count_if:计算序列中合乎条件(predicate,传回bool值的function object)的元素个数
equal:判断两个序列是否持有等值元素
search:寻找序列中某个子序列的第一次出现位置
copy:将一个序列复制到另一个序列身上
unique:移除某序列中的重复元素
rotate:旋转序列中的元素
sort:对序列中的元素排序
@ 国际化支持
C++对于软件产品国际化的支持,大面积地运用了templates以及继承机制和虚函数。
支持软件国际化的主要组件是facets和locales。facets描述某一文化的特殊字符集应该如何处理,包括校对规则(亦即国际字符集中的字符串如何排序)、日期和时间表示法、数值和货币表示法、信息代码与自然语文之间的映对关系等。locales含有一整组的facets,如针对美语的一组facets,针对法语的一组facets等。
@ 数值处理
@ 诊断功能
exception类继承体系
STL是一个可扩充的架构,它是一组公约。
50.加强自己对C++的了解
看《The Design and Evolution of C++》和《The Annotated C++ Reference Manual》(注释的C++参考手册)
![](http://www.cublog.cn/images/face/005.gif)
<基础议题>
M1.仔细区别指针和引用
最好使用C++转型操作符
绝对不要以多态方式处理数组
非必要不提供默认构造函数
![](http://www.cublog.cn/images/face/003.gif)
<操作符>
对定制的类型转换函数保持警觉
区别++/--操作符的前置和后置形式
千万不要重载&&、||和,操作符
了解各种不同意义的new和delete
![](http://www.cublog.cn/images/face/002.gif)
<异常>
利用析构函数避免泄漏资源
在构造函数内阻止资源泄漏
禁止异常流出析构函数之外
了解抛出一个异常与传递一个参数或调用一个虚函数之间的差异
以按引用方式捕捉异常
明智地运用异常specifications
了解异常处理的成本
![](http://www.cublog.cn/images/face/032.gif)
<效率>
谨记80-20法则
考虑使用缓式评估(lazy evaluation)
分期摊还预期的计算成本
了解临时对象的来源
协助完成返回值优化
利用重载技术避免隐式类型转换
考虑以操作符复合形式(op=)取代其独身形式(op)
考虑使用其他程序库
了解虚函数、多重继承、虚基类、RTTI(运行时类型识别)的成本
![](http://www.cublog.cn/images/face/004.gif)
<技术>
将构造函数和非成员函数虚化
限制某个类所能产生的对象数量
要求或禁止对象产生于堆中
智能指针(smart pointer)
引用计数
代理类(proxy classes)
让函数根据一个以上的对象类型来决定如何虚化
![](http://www.cublog.cn/images/face/008.gif)
<杂项讨论>
在未来时态下编程
将非叶子类(non-leaf classes)设计为抽象类
如何在同一个程序中结合C++和C
让自己习惯于标准C++语言