Effective C++ 读书笔记(4)——设计与声明

让接口更容易被正确使用

我们设计的接口应该在没有获得预期行为的时候禁止通过编译。
可以通过导入简单的外覆类型来避免:

struct Day{
	explicit Month(int d);
		:void (d){ }
	int val;
};

class Date{
public:
	Date(const Month& m);
	...	
};

Date d(Month(3));			//此时我们可以通过外覆类型知道传进去的这个参数表示的是月份
							//避免传进去参数出错,毕竟不会有十三个月。

也可以以函数替换对象表示一个特定的月份。

class Month{
public:
	static Month Jan() {return Month(1);}	//用函数替换对象
	...
private:
	explicit Month(int m);	//阻止生成新的月份,这是月份的专属数据
};

Date d(Month::Jan());		//用函数来代替对象也会避免非当地静态变量初始化顺序出问题的好处

我们也可以使用const限制客户的使用。

if(a * b = c)		//其实是想要进行比较的,但是写错了,写成赋值,要是重载操作符的话我们可以返回一个const来阻止这个情况编译通过。

书上建议,除非有非常好的理由,不然的话,我们需要让自己所设计的type类型与内置的type类型一致。

如果接口要求客户必须做什么事情,那么这个接口就是不好的接口。

//就像前面的资源管理的笔记一样,我们在返回对象的时候,最好是先发制人,直接返回一个智能指针
//也可以定制删除器,详见上篇笔记
std::tr1::shared_ptr<Investment> createInvestment();

设计class犹如设计type

在设计class(就是设计type)的时候,自我询问:
1、新的type的对象应该如何创建与销毁
2、对象的初始化和对象的赋值有什么差别
3、新type如果被passed by value,这意味着什么
4、什么是新type的合法值
5、你的新type要配合某个继承图系吗
6、你的新type需要什么样的转换
7、什么样的操作和函数对此新type是合理的
8、什么样的标准函数应该驳回
9、谁该取用新type的成员
10、什么是新type的“未声明接口”
11、你的新type有多么 的一般化
12、你真的需要一个新type吗

宁以pass-by-reference-to-const替换pass-by-value

缺省条件下C++是以by value的方式传递对象至函数的。
这些函数的参数是以实际参数的副本为初值,而调用端所获的也是一个函数返回值的副本,这些都是利用copy构造函数产出的,会产生昂贵的操作。

class Person{
publicPerson();
	virtual ~Person();
	...
private:
	std::string name;
	std::string address;
}class Student: public Person{
public:
	Student();
	~Student();
	...
private:
	std::string schoolName;
	std::string schoolAddress;
};

bool validateStudent(Student s);
Student plato;
bool platoIsOK = validateStudent(plato);
//上面的这种以值传递的方法传递对象,总计的成本是六次构造函数和六次析构函数(包括string对象)

bool validateStudent(const Student& s);
//加上const保证不对s进行改变

使用by reference进行传递参数还可以避免对象的切割问题

class Window{				//base class
public:
	...
	std::string name() const;
	virtual void display() const;
};

class Window1: public Window{		//derived class
public:
	...
	virtual void display() const;
};


void print(Window w)		//by value,该函数用于打印信息
{
	std::cout << w.name();
	w.display();
}

Window1 w1;		
print(w1);				//此时print的里面的特化信息都会被切割,因为print不管传进函数的是什么类型,因为w是个window

void print(const Window& w)	//by reference
{
	...
}
//用引用的话,只要传进来是什么类型,就表现什么样的类型

由于by reference传递是以指针实现的,如果对象是内置类型、函数对象、STL的迭代器的话,其实使用by value的话,效率会比by reference高,而且并不昂贵。

必须返回对象的时候,别妄想返回其reference

reference其实是一个别名,所以说reference是一个存在的对象的别名,我们必须返回一个存在的对象,实现的途径有两个,一个是在stack上建立对象,另一个是在heap上建立对象。

//stack
const Ration& operate*  (const Ration& lhs, const Ration& rhs)
{
	Ration result(lhs.n * rhs.s + lhs.d * rhs.d);
	return result;			//此时是返回一个local对象,无定义行为!!!!
}
//heap
const Ration& operate*  (const Ration& lhs, const Ration& rhs)
{
	Ration result = new Ration(lhs.n * rhs.s + lhs.d * rhs.d);
	return result;			//此时需要delete,而且还是得付出调用构造函数的代价
}

如果返回的是local static呢,会出现什么样的结果呢?

const Ration& operate*  (const Ration& lhs, const Ration& rhs)
{
	//一个是在多线程中使用静态对象可能会对线程安全有影响
	static Ration result;	
	result = ...
	return result;			
}

//还有一个问题是
if((a * b) == (c * d))
{...}
//此时一定会是true
//书上说由于operate* 返回的是引用,所以调用端看见的是对象的“现值”(不太理解)
//此时一定是相等的

所以在必须返回对象的时候,千万别返回引用!

将成员变量声明为private

为了一致性考虑,成员变量为private,这样客户对于对象的访问智能通过成员函数。
处于封装的考虑,日后在改变这个成员变量的时候,客户不会知道类已经改变。
protected并不比public更具有封装性。

宁以non-member、non-friend替换member成员

class Web{
public:
	void A();
	void B();
	...
};
//在需要连续调用A、B的时候,我们可以有两个选择
//设置一个成员函数C调用A、B
class Web{
public:
	void A();
	void B();
	void C();		//C中调用A、B
	...
};
//设置一个non-member函数C
void C(Wed& w)
{
	w.A();
	w.B();
}

使用非成员函数会比较好,因为此时会有比较好的封装性,这正是面向对象所要求的。
这适用于non-member和non-friend。
当然封装不是唯一要考虑的事情。

还有一点要注意的是,成为class的non-member并不意味着它不会是另一个class的member。

我们可以让某个函数fuc(可以对A进行操作的便利函数)成为B类的静态成员函数,只要它不是A类的一部分,就不会影响A类的封装性。

通常的做法是让fuc成为non-member,并且与A类在同一个namespace里面。

namespace与类不一样,namespace可以跨越多个源码文件。

namespace用法

namespace ccc{
	class A{...}void fuc(A& a);
	...
}

将所有的便利函数放在多个头文件中但隶属同一个命名空间意味着让用户轻松扩展这些便利函数(class对于客户来说是不能扩展的,但可以进行派生)。

若所有参数都需要类型转换,请为此采用non-member函数

class Ration{		//支持有理数运算
publicconst Ration operator* (const Ration& rhs) const;
	...
};
Ration a(1, 8);			
result = r * 2;		//可以通过编译,2隐式转换为Ration类
result = 2 * r;		//无法通过编译,2并没有operator* 成员函数。

class Ration{...};
//非成员函数
const Ration operate*(const Ration& lhs, const Ration& rhs)
{
	return Ration(...);
}

result = 2 * r;	//可以通过编译

所以如果需要对某个函数的所有参数进行隐式类型转换的话(包括this指针所指的那个隐喻参数),这个函数必须是个non-member。

考虑写一个不抛出异常的swap函数

实际上就是使用swap的特化。
swap函数的典型实现:

namespace std{
	template<typename T>
	void swap(T& a, T& b){
		T temp(a);
		a = b;
		b = temp;
	}
}

当在交换一些以指针指向一个对象,内含真正数据的那种类型的话,需要写一个swap来提高交换效率。

class Data{				//针对A设计的数据类
publicprivate:
	int a;
	...
}class A{
public:
	...
private:
	Data* pImpl;		//指针,所指对象内含A的数据
};

要置换两个A的对象的时候,其实只要置换pImpl指针即可。但是swap,会复制三个A,还会复制三个Data。非常缺乏效率。此时我们可以对swap进行A的特化。

不能进行swap的全特化,但是我们可以使用声明一个public的成员函数用作交换数据,然后特化swap函数,在这个函数中调用前面的成员函数(因为pImpl是private的,外部没有权限访问)。

这种方法与STL容器有一致性,STL容器有public的swap成员函数与std::swap的特化版本。

特化

class A{
public:
	...
	void swap(A& other){
		using std::swap;
		swap(pImpl, other.pImpl);
	}
};

namespace std{
	template<> 
	swap<A>(A& a,A& b)
	{
		a.swap(b);
	}
}

如果A与Data是class template的情况。

建立public的swap是合法的,特化一个function template是不合法的行为。C++只允许偏特化class template,在进行function template特化的打算的时候,我们可以利用函数的重载。

namespace std{
	template<typename T>
	void swap(A<T>& a,A<T>& b) 		//这里不加<>是因为规定客户不可以加任何typelate到std(但是可以全特化),因为std是由标准委员会决定的
	{a.swap(b);}
}

如果要调用swap的时候可以取得template的特定版本的话,可以使用non-member swap来调用member-swap。

namespace ccc{
	template<typename T>
	class A{...};
	...
	template<typename T>
	void swap(A<T>& a,A<T>& b)		//此处并不是std空间
	{
		a.swap(b);
	}
}

根据C++名称查找法则,合适的swap被调用是很容易的,不要额外进行添加修饰符。

就是在调用swap的时候,请确定包含一个using声明,以便让std::swap在你的函数内曝光,然后不加任何namespace修饰符,赤裸裸的调用swap。

std::swap(obj1,obj2);	//强迫只认std里面的swap了

这章挺麻烦的。

总结:只要swap缺省代码效率不足(class和template使用pimpl手法):
1、提供public的成员函数,高效置换类型的两个对象值。这个函数不能抛异常,因为成员版的swap是作为异常安全性的一个保障手法,注意是成员版。
2、在class或template所在的命名空间提供一个non-member swap函数,调用swap成员函数。
3、如果在编写一个class而不是class template,为你的class特化std::swap,并让它调用是swap成员函数。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值