Effective C++读书笔记2

条款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析构函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值