编译过程:.c文件--预处理-->.i文件--编译-->.o文件--链接-->bin文件
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。检查包含预处理指令的语句和宏定义,并对源代码进行相应的转换。预处理过程还会删除程序中的注释和多余的空白字符。可见预处理过程先于编译器对源代码进行处理。预处理指令是指在编译之前进行处理的命令,以#号开头,包含3个方面的内容:宏定义、文件包含、条件编译(#ifdef #endif)。
例:#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见,也许在编译器开始处理源代码之前它就被预处理器移走了。即编译源代码时ASPECT_RATIO已被1.653取代。ASPECT_RATIO可能并未进入记号表(symbol table)。
替换:const double AspectRatio = 1.653;
好处应该有:多了类型检查,因为#define 只是单纯的替换,而这种替换在目标码中可能出现多份1.653;改用常量绝不会出现相同情况。
static const int NumTurns = 5;//static 静态常量 所有的对象只有一份拷贝。
万一你编译器不允许“static int class常量”完成“in calss初值设定”(即在类的声明中设定静态整形的初值),我们可以通过枚举类型予以补偿:
enum { NumTurns = 5 };
*取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获取一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。[enum用法链接]
例:#define CALL_WITH_MAX(a,b) f((a) > (b)) ? (a) : (b))
宏看起来像函数,但不会招致函数调用带来的额外开销,而是一种简单的替换。
替换:
template<typename T>
inline void callWithMax(cosnt T &a, cosnt T &b)
{
f(a > b ? a : b);
}
callWithMax是个真正的函数,它遵循作用于和访问规则。
请记住:
条款03:尽可能使用const
STL例子:
声明迭代器为const就像声明指针为const一样(即声明一个T*const指针),表示这个迭代器不得指向不同的东西,但它所指的东西的值是可以改 动的。如果希望迭代器所指的东西是不可被改动的,需要的是const_iterator。
const std::vector<int>::interator iter = vec.begin();//iter是const常量, ++iter 错误
std::vector<int>::const_iterator cIter = vec.begin();//iter所指的元素是const常量,*cIter = 10 错误
以下几点注意:
static对象,其寿命从被构造出来直到程序结束为止。
函数内的static对象称为local static对象(因为它们对函数而言是local),其他static对象称为non-local static对象。程序结束时static对象会被自动销毁,也就是它们的析构函数会在main()结束时被自动调用。
当我们的某个编译单元内的某个non-local static对象的初始化动作使用了另一编译单元的某个non-local static对象,它所用到的这个对象可能尚未被初始化。因为C++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。class FileSystem { public: size_t numDisks()const; }; extern FileSystem tfs;
class Directory { public: Directory(params); }; Directory::Directory(params) { size_t disks = tfs.numDisks(); }
Directory tempDir(params);
所以如我们把tfs和tempDir设计为一个函数,函数返回该类的一个static对象引用就可以解决问题了。
所以我们可以改写上面的代码:
class FileSystem{...}; FileSystem& tfs() //用这个函数替换tfs对象,它在 { //FileSystem class中可能是个static. static FileSystem fs; //定义并初始化一个local static对象 return fs; //返回一个reference指向上述对象 } class Directory{...}; Directory::Directory(params) //同前,但原本的reference to tfs { //现在改为tfs() ... int disks = tfs().numDisks(); ... } Directory& tempDir() //用这个函数替换tempDir对象,它在 { //tempDir class中可能是个static. static Directory td; //定义并初始化一个local static对象 return td; //返回一个reference指向上述对象 }
二.构造/析构/赋值运算
条款05:了解C++默默编写并调用哪些函数
如果你自己没有声明,编译器就会为类声明(编译器版本的)一个拷贝构造函数,一个拷贝赋值操作符和一个析构函数。此外如果你没有声明任何构造函数,编译器也会成为你声明一个默认构造函数。所有这些函数都是public且inline。
惟有当这些函数被需要(被调用),它们才会被编译器创建出来。即有需求,编译器才会创建它们。
默认构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用基类和非静态成员变量的构造函数和析构函数。
注意:编译器产生的析构函数是个non-virtual,除非这个类的基类自身声明有virtual析构函数。
至于拷贝构造函数和拷贝赋值操作符,编译器创建的版本只是单纯地将来源对象的每一个非静态成员变量拷贝到目标对象。
如一个类声明了一个构造函数(无论有没有参数),编译器就不再为它创建默认构造函数。
编译器生成的拷贝赋值操作符:对于成员变量中有指针,引用,常量类型,我们都应考虑建立自己“合适”的拷贝赋值操作符。因为指向同块内存的指针是个潜在危险,引用不可改变,常量不可改变。
引用不可改变:
int main() { string s1="aa"; string s2="bb"; string& s=s1; s=s2; cout<<s<<endl; cout<<s1<<endl; cout<<s2<<endl; return 0; }
bb bb bb
由此可见,当给reference s重新赋值时,s1的值也被改变了,所以C++规定reference一旦初始化之后,就不能改变(不能再指向别的对象)。
请记住:
通常如果你不希望类支持某一功能,只要不声明对应函数就是了。但这个策略对拷贝构造函数和拷贝赋值操作符却不起作用。因为编译器会“自作多情”的声明它们,并在需要的时候调用它们。
由于编译器产生的函数都是public类型,因此可以将拷贝构造函数或拷贝赋值操作符声明为private。通过这个小“伎俩”可以阻止人们在外部调用它,但是类中的成员函数和友元函数还是可以调用private函数。解决方法是设计一个专门为了阻止拷贝动作而设计的基类。(Boost提供的那个类名为noncopyable)。
即如果某些对象是独一无二的(比如房子),你应该禁用copy 构造函数或copy assignment 操作符,可选的方案有两种:
(1)将copy构造函数和copy assignment操作符声明为private,并不予实现;
(2)在(1)中,当member函数或friend函数调用copy构造函数(或copy assignment操作符),会出现链接错误。我们可以将连接期间的错误移到编译期间(这是好事,越早侦测出错误越好)。我们可以定义一个Uncopyable公共基类,并将copy构造函数和copy assignment操作符声明为private,并不予实现,然后让所有独一无二的对象继承它。
class Uncopyable { protected: //允许derived对象构造和析构 Uncopyable() {} -Uncopyable(){} private: Uncopyable(const Uncopyable&}; //但阻止copying Uncopyable& operator=(const Uncopyable&); }; class HomeForSale: private Uncopyable{ … };
为了阻止Uncopyable对象被拷贝,我们唯一要做的就是继承Uncopyable:
class HomeForSale:private Uncopyable{ //不再声明copy构造函数和copy assignment操作符 }
这样是行得通的,因为我们知道,当外部对象(甚至是member函数或者friend函数)尝试拷贝HomeForSale对象时,编译器都会尝试着生成一个默认的copy构造函数和copy assignment操作符,而这些默认生成的函数会调用其基类base class中对应的函数,因为基类已经将其声明为private,所以这些调用会被编译器拒绝。
class AWOV{ public: virtual ~AWOV() = 0; //声明纯虚析构函数 }; AWOV::~AWOV(){} // pure virtual析构函数的定义任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
(1)析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
(2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class 应该提供一个普通函数(而非在析构函数中)执行该操作。即如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。如下面程序所示:
class DBConn{ public: ... void close() { db.close(); closed = true; } ~DBConn() { if (!closed) { try{ //如果客户没有关闭连接 db.close(); } catch(...){ //结束程序或吞下异常 std::abort() 或者制作运转记录,记下对close的调用失败; } } } private: DBConnection db; bool closed; };
条款09:决不让构造和析构过程中调用virtual函数
你不该在构造函数和析构函数中调用virtual函数,因为这样的调用不会带来你预想的结果。
因为:基类的构造函数的执行要早于派生类的构造函数,当基类的构造函数执行时,由于其包含了一个virtual函数,而该virtual函数除非被定义,否则连接器将找不到该函数的实现代码,从而出现未定义行为。而且,base class构造期间,virtual函数绝不会下降到derived层,他被当做base class的函数直接调用。这样做的原因在于,当base class构造函数执行时,derived class的成员变量尚未初始化,如果此期间调用的virtual函数下降到derived class(derived class成员尚未初始化),就会出现使用未初始化成员变量的危险行为。
唯一好的做法是:确定你的构造函数和析构函数都没有调用虚函数,而它们调用的所有函数也不应该调用虚函数。
解决的方法可能是:既然你无法使用虚函数从基类向下调用,那么我们可以使派生类将必要的构造信息向上传递至基类构造函数。即在派生类的构造函数的成员初始化列表中显示调用相应基类构造函数,并传入所需传递信息。
请记住:
对于赋值操作符,我们常常要达到这种类似效果,即连续赋值:
int x, y, z;
x = y = z = 15;
为了实现“连锁赋值”,赋值操作符必须返回一个“引用”指向操作符的左侧实参。
即:
Widget & operator = (const Widget &rhs)
{
...
return *this;
}
所有内置类型和标准程序库提供的类型如string,vector,complex或即将提供的类型共同遵守。
如果不返回引用,那么就会调用拷贝构造函数,从而增加了系统开销。
#include <iostream> using namespace std; class Widget { public: Widget(int ii):i(ii){} Widget(const Widget &rhs) { i=rhs.i; cout<<"copy"<<endl; } Widget& operator=(const Widget &rhs) { this->setValue(rhs.getValue()); cout<<"assign"<<endl; return *this; } Widget& operator+=(const Widget& rhs) { this->setValue(rhs.getValue() + this->getValue()); return *this; } int getValue() const {return i;} void setValue(int ii){i=ii;} private: int i; }; int main() { Widget w1(1),w2(2),w3(3); w2=w1; return 0; }如果是Widget& operator=(const Widget &rhs)
则输出:
assign如果是 Widget operator=(const Widget &rhs)
assign copy
条款12:复制对象时勿忘其每一个成员
还记得条款5中提到编译器在必要时会为我们提供拷贝构造函数和拷贝赋值函数,它们也许工作的不错,但有时候我们需要自己编写自己的拷贝构造函数和拷贝赋值函数。如果这样,我们应确保对“每一个”成员进行拷贝(复制)。
如果你在类中添加一个成员变量,你必须同时修改相应的copying函数(所有的构造函数,拷贝构造函数以及拷贝赋值操作符)。
在派生类的构造函数,拷贝构造函数和拷贝赋值操作符中应当显示调用基类相对应的函数,否则编译器可能又“自作聪明了”。
当你编写一个copying函数,请确保:
Class Base{..} class Derived:public Base{ Derived(const Derived& rhs); Derived& operator=(const Derived& rhs); } Derived::Derived(const Derived& rhs) :Base(rhs) //调用base class的copy构造函数 { ... } Derived& Derived::Derived(const Derived& rhs) { ... Base::operator=(rhs); //对base class成分进行赋值操作 ... }
条款13:以对象管理资源
例:
void f()
{
Investment *pInv = createInvestment();
... //这里存在诸多“不定因素”,可能造成delete pInv;得不到执行,这可能就存在潜在的内存泄露。
delete pInv;
}
解决方法:把资源放进对象内,我们便可依赖C++的“析构函数自动调用机制”确保资源被释放。
许多资源被动态分配于堆内而后被用于单一区块或函数内。它们应该在控制流离开那个区块或函数时被释放。标准程序库提供的auto_ptr正是针对这种形势而设计的特制产品。auto_ptr是个“类指针对象”,也就是所谓的“智能指针”,其析构函数自动对其所指对象调用delete。
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
} //函数退出,auto_ptr调用析构函数自动调用delete,删除pInv;无需显示调用delete。
“以对象管理资源”的两个关键想法:
我们在条款13中讨论的资源是在堆上申请的资源,而有些资源并不适合被auto_ptr和tr1::shared_ptr所管理。可能我们需要建立自己的资源管理类。
例:
void lock(Mutex *pm); //锁定pm所指的互斥量
unlock(Mutex *pm); //将pm解除锁定
我们建立的资源管理类可能会是这样:
class Lock { public: explicit Lock(Mutex *pm) : mutexPtr(pm) { lock(mutexPtr); //获取资源 } ~Lock() { unlock(mutexPtr); //释放资源 } private: Mutex *mutexPtr; };
Lock m1(&m) //锁定m
Lock m2(m1) //将m1复制到m2上,这回发生什么??
“当一个RAII对象被复制,会发生什么事?”大多数时候你会选择一下两种可能:
由于tr1::shared_ptr缺省行为是”当引用计数为0时删除其所指物“,幸运的是shared_ptr允许我们指定所谓的“删除器”(deleter),那是一个函数对象,当引用次为0时被调用。
class Lock { public: explicit Lock(Mutex *pm) : mutexPtr(pm, unlock) //以某个Mutex初始化shared_ptr,并以unlock函数为删除器 { lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; };
本例中,并没说明析构函数,因为没有必要。编译器为我们生成的析构函数会自动调用其non-static成员变量(mutexPtr)的析构函数。而mutexPtr的析构函数会在互斥量”引用计数“为0时自动调用tr1::shared_ptr的删除器(unlock)。
class Font { public: ... FontHandle get() const //FontHandle 是资源; 显示转换函数 { return f; } operator FontHandle() const //隐式转换 这个值得注意,可能引起“非故意之类型转换” { return f; } ... };