More effective C++阅读整理笔记(条款四、五、六)

More effective C++阅读整理笔记(条款四、五、六)

条款四:非必要不提供默认构造函数

构造函数可以初始化对象,默认构造函数可以在不利用任何外部数据就能初始化对象。有时这样的方法时合理的,比如链表、哈希表等被初始化成空容器等,但是对于很多不用外部数据进行初始化时不合理的,比如说没有输入姓名的电话簿对象就没有任何意义。
但是当类缺乏一个默认构造函数时,使用这个类会受到某些限制。
考虑到下面某个针对公司设计的class,在其中,仪器识别码是一定得有的一个构造参数:

class EquipmentPiece {
public:
	EquipmentPiece(int IDNumber){}
};

首先产生的问题是:在产生数组时,一般而言没有任何办法可以为数组中的对象执行构造参数,所以几乎不可能产生一个由EquipmentPicec对象构成的数组。

EquipmentPiece bestPieces[10];  //错误,没有正确调用EquipmentPiece构造函数
EquipmentPiece* bestPieces2 = new EquipmentPiece[10];  //错误,原因同上

有三个方法可以解决这个问题,可以使用non-heap数组,在定义数组时提供必要的自变量,但是无法扩展长度。

//解决办法:使用non-heap数组
	int ID1 = 1, ID2 = 2;
	EquipmentPiece bestPieces3[] = { EquipmentPiece(ID1), EquipmentPiece(ID2) };

第二个解决办法:使用指针数组来代替一个对象数组,但是缺点是必须删除数组中每个指针所指向的对象,忘了会内存泄漏;增加内存分配量,需要空间来容纳指针

typedef EquipmentPiece* PEP;
PEP bestPieces4[10];  //正确,指针数组不调用构造函数和析构函数
PEP* bestPieces5 = new PEP[10];  //正确
//在指针数组里的每一个指针被重新赋值,以指向一个不同的EquipmentPiece对象
for (int i = 0; i < 10; i++)
bestPieces5[i] = new EquipmentPiece(ID1);
//缺点:必须删除数组中每个指针所指向的对象,忘了会内存泄漏;增加内存分配量,需要空间来容纳指针

第三个解决办法:为了避免过度使用内存,可以先为此数组分配raw memory,然后使用placement new在这块内存上构造EquipmentPicec对象。缺点:使用起来困难;手动调用析构函数,最后还得调用operator delet[]释放raw memory。

//解决办法:为数组分配raw memory,可以避免浪费内存,使用placement new方法在内存中构造EquipmentPiece对象
void* rawMemory = operator new[](10 * sizeof(EquipmentPiece));
//让bestPieces指向此块内存,使这块内存被视为一个EquipmentPiece数组
EquipmentPiece* bestPieces6 = static_cast<EquipmentPiece*>(rawMemory);
//使用placement nes构造这块内存中的EquipmentPiece对象
for (int i = 0; i < 10; i++)
	new(&bestPieces6[i]) EquipmentPiece(ID1);
//...
//与以构造bsetPieces对象析构他
for (int i = 0; i < 10; i++)
	bestPieces6[i].~EquipmentPiece();  
//释放rawMemory
operator delete[](rawMemory);
//delete[]rawMemory;//如果使用普通的数组删除方式,运行结果不可预测

缺少默认构造函数的第二个问题是:他们不适用于许多基础模板容器类,对那些template而言,被实例化的目标类型必须得有一个默认构造函数。

template<class T>
class Array
{
public:
	Array(int size);
	...
private:
	T* data;
};

template<class T>
Array<T>::Array(int size) {
	data = new T[size];
}

缺乏默认构造函数的第三个问题是:虚基类如果没有默认构造函数,则要求其所有子类不论距离多么遥远,都必须知道、了解其意义,并且提供虚基类的构造参数。

条款五:谨慎定义类型转换函数

C++编译器允许把char隐式转换为int和从short转换为double。因此当你把一个short值传递给准备接受double参数值的函数时,依然可以成功运行。并且C++还存在令人害怕的转型,甚至可能会遗失信息,包括将int转换为short
不过当你增加自己的类型时,可以有更多的控制力,因为你能选择是否提供函数让编译器进行隐式类型转换。
有两种函数允许编译器进行这些类型的转换:单参数构造函数和隐式类型转换运算符。单参数构造函数是指只用一个参数即可以调用的构造函数,该函数可以是只定义了一个参数,也可以是虽定义了多个参数,但是第一个参数以后的所有参数都有默认值。
以下为两个例子:

class Name {
public:
	Name(const std::string& s) {};  //转换string到 Name
};

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1)  //转换int到有理数类
	{
		n = numerator;
		d = denominator;
	}

private:
	int n, d;
};

为了允许Ratinal类隐式转换为double类,可以使用隐式类型转换操作符:关键词operator后加上一个类型名称。你不能为此函数指定返回值类型,因为其返回值类型基本上已经表现于函数名称上。

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1)  //转换int到有理数类
	{
		n = numerator;
		d = denominator;
	}

	operator double() const   //const表示常函数,转换Rational为double类型
	{
		return static_cast<double>(n) / d;
	}
	
private:
	int n, d;
};

在下面这种情况下,该函数会被隐式的调用:

Rational r(1, 2);
double d = 0.5 * r;
cout << r << endl;  //虽然未定义operator<<,但r会被隐式类型转换操作符转换为double,输出double数据类型而非分数

虽然没有定义operator<<,但是编译器在调用operator<<时,会发现没有这样的函数存在,他会试图找到一个合适的隐式类型转换顺序以使得函数调用能正常运行。类型转换顺序的规则时复杂的,但是在这种情况下,编译器发现他们能调用Rational::operator double函数来把r转换为double类型
第一个解决办法:以功能对等的另一个函数取代类型转换操作符

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1)  //转换int到有理数类
	{
		n = numerator;
		d = denominator;
	}
	double asDouble() const
	{
		return static_cast<double>(n) / d;
	}

private:
	int n, d;
};

这样的话:

Rational r(1, 2);
cout << r << endl;  //错误!
cout << r.asDouble() << endl;  //一般以另一个功能对等的函数代替类型转换操作符

多数情况下,这种显示转换函数的使用虽然不方便,但是函数被悄悄调用的情况不会再发生。比如string类型没有包含隐式的从string转换为C风格的char*的功能,而是定义成员函数c_str用来完成这个转换。
通过单参数构造函数进行隐式类型转换更难消除,而且在很多情况下这些函数所导致的问题要多于隐式类型转换运算符。
比如有数组类型:

template<class T>
class Array {
public:
	Array(int lowBound, int highBound) {}
	Array(int size) {}
	T& operator[](int index) { return data[index]; }
private:
	T* data;
};

为了比较Array对象,代码为:

bool operator== ( const Array<int>& lhs, const Array<int>& rhs) {
	return false;
}

Array<int> a(10);
Array<int> b(10);
for (int i = 0; i < 10; i++) {
	if (a == b[i]) {}   //可以运行
	}

看起来上述代码发生了错误,应该是a[i]==b[i],但是编译器却不会报错,因为他把这个调用看成了Array<int>int参数调用operator,然而没有operator==是这个参数类型,编译器将能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int类型的参数。
生成的代码类似:

for (int i = 0; i < 10; i++) {
	if (a == Array<int>(b[i])) {}  //没问题,将int转换为Array<int>时一种显式行为,但是逻辑让人生疑。
}

容易的方法式利用explicit关键字,构造函数使用explicit关键字,编译器会拒绝为了隐式类型转换而调用构造函数,但是显式类型转换依然合法

template<class T>
class Array {
public:
	Array(int lowBound, int highBound) {}
	explicit Array(int size) {}
	T& operator[](int index) { return data[index]; }
private:
	T* data;
};

	//=============================================================
	//explicit关键字是为了解决隐式类型转换而特别引入的这个特性。如果构造函数用explicit声明,编译器会拒绝为了隐式类型转换而调用构造函数。显式类型转换依然合法。
Array<int> a(10);
Array<int> b(10);
for (int i = 0; i < 10; i++) {
	//if (a == b[i]) {}   //错误,如果没有explicit,不会报错,因为编译器将能通过调用Array<int>构造函数能转换int类型到Array<int>类型,这个构造函数只有一个int类型的参数,加上explicit关键字则可避免隐式转换
	if (a == Array<int>(b[i])) {}  //没问题,将int转换为Array<int>时一种显示行为,但是逻辑让人生疑。
	if(a == static_cast<Array<int>>(b[i])) {}  //同样正确,同样不合理
	if (a == (Array<int>)b[i]) {} //c风格,同样正确,但是逻辑依然不合理
}

总结:**让编译器进行隐式类型转换所造成的弊端要大于好处,所以除非确实需要,不要定义类型转换函数。

条款六:区别++/–操作符的前置和后置形式

这个条款主要为了说明++/–操作符的前置和后置形式的区别。
前置:累加(减)然后取出,后置:取出然后累加(减)。

class UPInt {
public:
	//注意:前缀与后缀形式返回值类型是不同的,前缀形式返回一个引用,后缀形式返回一个const类型
	UPInt& operator++()  //++前缀
	{
		*this += 1;
		return *this;
	}

	const UPInt operator++(int)   //++后缀
	{
		// 注意:建立了一个显示的临时对象,这个临时对象必须被构造并在最后被析构,前缀没有这样的临时对象
		UPInt oldValue = *this;  //取回值
		// 后缀应该根据他们的前缀形式来实现
		++(*this);  //增加
		return oldValue;  //返回被取回的值
	}

	UPInt& operator--()  //--后缀
	{
		i -= 1;
		return *this;
	}

	const UPInt operator--(int)  //--后缀
	{
		UPInt oldValue = *this;
		--(*this);
		return oldValue;
	}

	UPInt& operator+=(int a)  //+=操作符
	{
		i += a;
		return *this;
	}

	UPInt& operator-=(int a)  //-=操作符
	{
		i -= a;
		return *this;
	}

private:
	int i;
};

int main() {
	UPInt i;
	++i;  //调用i.operator++()
	i++;  //调用i.operator++(0)
	--i;  //调用i.operator--()
	i--;  //调用i.operator--(0)

	//i++++;  //错误,因为:++后缀返回的是const UPInt
	return 0;
}

区别1:参数不同
前置式没有参数,后置式有一个int自变量,并且在它被调用时,编译器默默地为该int指定一个0,注意后置式中并未调用其参数,其参数地唯一目的是为了区分前置和后置式。
区别2:返回类型不同
前置式返回引用,后置式返回一个const对象。
为什么后置式返回一个const对象呢?
如果成立:

UPInt i;
i++++;

这和以下代码相同:

i.operator++(0).operator++(0);

很明显:operator++第二个调用动作施加于第一个调用动作的返回对象身上去了,而不是原对象。i也只是被累加一次,这是违反直觉的,也容易混淆。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值