条款4 确定对象被使用前已被初始化
读取未初始化的值会导致不明确的行为,在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。对象的初始化动作何时一定发生,何时不一定发生的规则很复杂,,最佳解决办法就是:永远在使用对象之前先将它初始化:对于内置类型,手动完成,对于非内置类型,由构造函数来完成。这个规则容易奉行,重要的是不要混淆赋值和初始化:
class PhoneNumber{};
class ABEntry{
public:
ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones);
private:
std::string theName;
std::string theAddress;
std::list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones){
theName = name; //all these are assignements, but not initializations
theAddress = address;
thePhones = phones;
numTimesConsulted = 0;
}
更好的方法是:
ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{}
基于赋值的那个版本首先调用默认构造函数为成员赋初值,然后立刻再对它们赋予新值,默认构造函数的一切作为因此浪费了,第二种方法避免了这一问题,因为各个值进行了拷贝构造,这样比第一种方法中的先调用默认构造函数再调用copy assignment高效。甚至可以使用如下的方式:
ABEntry::ABEntry()
:theName(),//调用theName的默认构造函数
theAddress(),//同上
thePhones(),//同上
numTimesConsulted(0)
{}
有些情况下即使面对的成员变量属于内置类型(初始化和赋值的成本相当),也一定得使用初值列,如果成员变量是const或reference,它们一定需要初值,不能被赋值(???)
许多类拥有多个构造函数,每个构造函数有自己的成员初始列。如果这种类存在许多成员变量和基类,多份成员初始列的存在就导致不受欢迎的重复。这时可以合理的在初值列中遗漏那些赋值和初始化一样的成员变量,改用它们的赋值操作,并将那些赋值操作移往某个函数供所有构造函数调用。
这种做法在成员变量初值由文件或数据库读入时特别有用
不同编译单元内定义的non-local static对象的初始化次序:
所谓static对象,其寿命从被构造出来直到程序结束,因此stack和heap-based对象被排除。包括global对象,定义与namespace作用域内的对象,在class内,在函数内以及在file作用域内被声明为static的对象。。
C++对于定义于不同编译单元内的non-local static对象的初始化相对次序无明确定义,有一个方法可以解决改问题,即将每个non-local static对象搬到自己的专属函数内。
这是设计模式中singleton模式的一个常见实现手法。这种方法的基础在于:C++保证,函数内的local static对象会在该函数被调用时、首次遇上该对象之定义式时才被初始化
请记住:
1.对内置对象进行手工初始化,因为C++不保证初始化它们。
2.构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和他们的类内中的声明次序相同。
3.为免除跨编译单元之初始化次序问题,请以local static对象替换non-local static对象
条款5 了解C++默默编写并调用哪些函数
编译器自动为一个类声明一个拷贝构造函数、一个拷贝赋值操作符和一个析构函数。如果没有声明任何构造函数,编译器也会为你声明一个默认构造函数。但是,只有当这些函数被调用时,他们才会被编译器创建出来。但是特殊情况下编译器只会声明而不会去创建这些函数,以拷贝赋值函数为例:
template<class T>
class NamedObject{
public:
NamedObject(std::string& name, const T& value);
private:
std::string& nameValue;
const T ObjectValue;
};
std::string newDog("Dog one");
std::string oldDog("Dog two");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
p = s;//赋值之后,p.nameValue指向s.nameValue所指的那个string吗?reference会被改动吗?面对这样的难题,C++的响应是拒绝编译这一行赋值动作,如果打算在一个内含
reference成员的class内支持赋值操作,必须自己定义拷贝赋值操作符。内含const成员的类道理相同。
最后还有一种情况:如果某个基类将复制拷贝操作符声明为private,编译器将拒绝为其derived class生成一个拷贝赋值操作符,因为编译器为基类生成的拷贝赋值操作符想象中可以处理基类成分,但他们却无法调用派生类无权调用的成员函数。
请记住:编译器可以暗自为类创建默认构造函数,拷贝构造函数,拷贝赋值函数以及析构函数
条款6 若不想使用编译器自动生成的函数,就该明确拒绝
使用private 声明来组织编译器自动生成的拷贝构造函数和拷贝赋值函数的使用:
class HomeForSale{
public:
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);//只有声明,没有实现
};
但是如果是在成员函数或者friend函数之内使用这些函数,连接器会发出抱怨,那么如何把这种抱怨移到编译期呢:
class Uncopyable{
protected: //允许派生类对象构造和析构(????)
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class HomeForSale: private Uncopyable{
//HomeForSale类不在声明拷贝构造函数或者拷贝赋值函数
//任何人(甚至是成员函数或者友元函数)尝试拷贝HomeForSale对象,编译器便试着生成一个拷贝构造函数和一个拷贝赋值操作符,这些函数的编译器生成版会尝试
//调用其基类的对应兄弟,但会被编译器拒绝,因为其基类拷贝函数是私有的
};
请记住:为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的基类也是一种做法。
条款7:为多态基类声明virtual析构函数
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
};
class AtomicClock: public TimeKeeper{};
class WaterClock: public TimeKeeper{};
许多客户只想在程序中使用时间,不想操心时间如何计算等细节,这时可以设计factory工厂函数,返回指针指向一个计时对象,factory函数会返回一个基类指针,指向新生成的基类对象:
TimeKeeper* getTimeKeeper();
当factory函数返回的每一个对象适当的delete时,getTimeKeeper返回的指针指向一个派生类对象,而那个对象却经由一个基类指针被delete,而目前的基类有个non-virtual析构函数。C++明确指出,当派生类对象经由一个基类指针被删除,而该基类带有一个non-virtual析构函数,其结果未有定义,实际执行时通常发生的是对象的派生类成份没有被销毁,造成内存泄漏。所以要给基类一个虚析构函数。
如果类不含virtual函数,通常表示它并不意图被用做一个基类,此时令其析构函数为virtual是个馊主意。所以:只有当类内至少一个virtual函数,才为它声明virtual析构函数。
标准string不含有任何virtual函数,但有时候程序员会错误的把它当作基类:
class SpecialString: public std::string{
};
如果在程序中无意间将一个pointer-to-SpecialString转换为一个pointer-to-string,然后将转换多得的那个string指针delete掉:
SpecialString* pss = new SpecialString("test");
std::string* ps;
ps = pss;
delete ps;//*ps的SpecialString资源会泄漏,因为SpecialString析构函数没被调用。
所以,所有的stl容器,都是带有non-virtual析构函数的,所以不要企图继承之。(很不幸,C++没有提供类似java的final class等禁止继承机制)
请记住:
1.带多态性质的基类应该声明一个virtual析构函数,如果类带有任何virtual函数,它就应该拥有一个virtual析构函数
2.类的设计目的如果不是作为基类来使用,或者不是为了具备多态性,就不该声明virtual析构函数