从C到C++
条款1:尽量用const和inline而不用#define
1、比如#define ASPECT_RATIO 1.653
如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。
解决这个问题的方案很简单:不用预处理宏,定义一个常量:
const double ASPECT_RATIO = 1.653;
关于const的含义和用法,特别是和指针相关联的问题,参见条款21。
另外,定义某个类(class)的常量一般也很方便,只有一点点不同。要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝,还要把它定义为静态成员:
class GamePlayer {
private:
static const int NUM_TURNS = 5; // constant eclaration
int scores[NUM_TURNS]; // use of constant
...
};
const int GamePlayer::NUM_TURNS; // mandatory definition;
如果编译器不允许在声明时fuzhi,那么可以用enum:
class GamePlayer {
private:
enum { NUM_TURNS = 5 } // "the enum hack" — makes
// NUM_TURNS a symbolic name
// for 5
int scores[NUM_TURNS];// fine
};
2、#define max(a,b) ((a) > (b) ? (a) : (b))
这样做具有很大危险,比如:
int a = 5, b = 0;
max(++a, b);// a 的值增加了2次
max(++a, b+10); // a 的值只增加了1次
==〉用inline函数代替。
条款2:尽量用<iostream>而不用<stdio.h>
scanf/printf不是类型安全的,而且没有扩展性,另外,scanf/printf系列函数把要读写的变量和控制读写格式的信息分开来;这些弱点正是操作符>>和<<的强项。
条款3:尽量用new和delete而不用malloc和free
malloc和free(及其变体)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数。
也不要混合食用
条款4:尽量使用c++风格的注释
内存管理
条款5:对应的new和delete要采用相同的形式
string *stringarray = new string[100];
...
delete stringarray;
//(VC上assert)
typedef string addresslines[4]; //一个人的地址,共4行,每行一个string
//因为addresslines是个数组,使用new:
string *pal = new addresslines; // 注意"new addresslines"返回string*, 和
// "new string[4]"返回的一样
delete时必须以数组形式与之对应:
delete pal;// 错误!
delete [] pal;// 正确
为了避免混乱,最好杜绝对数组类型用typedefs。
条款6:析构函数里对指针成员调用delete
增加一个指针成员意味着几乎都要进行下面的工作: ·在每个构造函数里对指针进行初始化。对于一些构造函数,如果没有内存要分配给指针的话,指针要被初始化为0(即空指针)。 ·删除现有的内存,通过赋值操作符分配给指针新的内存。 ·在析构函数里删除指针。
条款7:预先准备好内存不够的情况
条款8: 写operator new和operator delete时要遵循常规
要有正确的返回值;可用内存不够时要调用出错处理函数(见条款7);处理好0字节内存请求的情况。此外,还要避免不小心隐藏了标准形式的new,不过这是条款9的话题。
条款12: 尽量使用初始化而不要在构造函数里赋值
有些情况下必须用初始化。特别是const和引用数据成员只能用初始化,不能被赋值。
template<class t> class namedptr {
public:
namedptr(const string& initname, t *initptr); ...
private: string name;
t *ptr;
};
对象的创建分两步: 1. 数据成员初始化。(参见条款13) 2. 执行被调用构造函数体内的动作。
(对有基类的对象来说,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行之前)
对namedptr类来说,这意味着string对象name的构造函数总是在程序执行到namedptr的构造函数体之前就已经被调用了。问题只在于:string的哪个构造函数会被调用?
这取决于namedptr类的成员初始化列表。如果没有为name指定初始化参数,string的缺省构造函数会被调用。当在namedptr的构造函数里对name执行赋值时,会对name调用operator=函数。这样总共有两次对string的成员函数的调用:一次是缺省构造函数,另一次是赋值。
相反,如果用一个成员初始化列表来指定name必须用initname来初始化,name就会通过拷贝构造函数以仅一个函数调用的代价被初始化。
但有一种情况下,对类的数据成员用赋值比用初始化更合理。这就是当有大量的固定类型的数据成员要在每个构造函数里以相同的方式初始化的时候。
请注意static类成员永远也不会在类的构造函数初始化。静态成员在程序运行的过程中只被初始化一次,所以每当类的对象创建时都去“初始化”它们没有任何意义。
条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同
对一个对象的所有成员来说,它们的析构函数被调用的顺序总是和它们在构造函数里被创建的顺序相反。那么,如果允许上面的情况(即,成员按它们在初始化列表上出现的顺序被初始化)发生,编译器就要为每一个对象跟踪其成员初始化的顺序,以保证它们的析构函数以正确的顺序被调用。这会带来昂贵的开销。所以,为了避免这一开销,同一种类型的所有对象在创建(构造)和摧毁(析构)过程中对成员的处理顺序都是相同的,而不管成员在初始化列表中的顺序如何。
另外,基类数据成员总是在派生类数据成员之前被初始化,所以使用继承时,要把基类的初始化列在成员初始化列表的最前面。
条款14: 确定基类有虚析构函数
如果~Base()不是虚拟函数,那么Base* pObj = new Derived();delete pObj;
这时,由于不是虚拟函数,进行静态联编,毁掉用指针所指向的析构函数,即Base的析构函数,那么Derived的析构函数永远不会调用。
实现虚函数需要对象附带一些额外信息,以使对象在运行时可以确定该调用哪个虚函数。对大多数编译器来说,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr指向的是一个称为vtbl(虚函数表)的函数指针数组。每个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl的vptr在vtbl里找到相应的函数指针来确定的。
如果一个类确定不会作为基类,那么就不要声明虚构函数。
抽象类-〉不能实例化的类-〉必须要有一个纯虚函数-〉如果找不到合适的纯虚函数,可以将析构函数声明为纯虚函数。
注意,必须提供纯虚析构函数的定义,,因为虚析构函数工作的方式是:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数被调用。这就是说,即使是抽象类,编译器也要调用西构函数,所以要保证为它提供函数体。
条款15: 让operator=返回*this的引用
当定义自己的赋值运算符时,必须返回赋值运算符左边参数的引用,*this。如果不这样做,就会导致不能连续赋值,或导致调用时的隐式类型转换不能进行,或两种情况同时发生。
用new的时候会发生两件事。首先,内存被分配(通过operator new 函数,详见条款7-10和条款m8),然后,为被分配的内存调用一个或多个构造函数。用delete的时候,也有两件事发生:首先,为将被释放的内存调用一个或多个析构函数,然后,释放内存(通过operator delete 函数,详见条款8和m8)
条款16: 在operator=中对所有数据成员赋值
如果存在继承关系,如果子类中没有重新定义operator=,那么系统会自动在子类的operator=中调用父类的operator=,但是如果子类中重新定义了,那么将会掉用子类中定义的operator=,不会自动调用父类的operator=.
此时,需要在子类中对父类成员fuzhi:
CDerived& operator = (const CDerived& dx)
{
if (this != &dx)
{
// CBase::operator =(dx);
static_cast<CBase&>(*this) = dx;
m_y = dx.m_y;
}
return *this;
}
如果编译器不支持CBase::operator =(dx);,则用:static_cast<CBase&>(*this) = dx;注意,是CBase&而不是CBase。如果将*this强制转换为base对象,就要导致调用base的拷贝构造函数,创建出来的新对象(见条款m19)就成为了赋值的目标,而*this保持不变。这不是所想要的结果。
子类的拷贝构造函数也有类似的问题。
条款17: 在operator=中检查给自己赋值的情况
一个赋值运算符必须首先释放掉一个对象的资
源(去掉旧值),然后根据新值分配新的资源。在自己给自己赋值的情况下,释放
旧的资源将是灾难性的,因为在分配新的资源时会需要旧的资源。
注意if (*this == rhs) // 假设operator=存在 和if (this == &rhs) return *this;的区别