Effective C++ 第四章——设计与声明

Effective C++ 第四章——设计与声明

条款18——让接口容易被正确使用,不易被误用

假设我们需要设计一个用于表现日期的class设计构造函数:

class Date{
public:
	Date(int month,int day,int year);
...
};

这种设计会导致至少两个错误,客户以错误的次序传参和传递一个错误的月份或者天数。
设计者需要避免错误的次序传参可以采用建立新的类型来避免

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

struct Mouth{
explicit Mouth(int m)
:val(m) {}
int val;

struct Year{
explicit Year(int y)
:val(y) {}
int val;

class Date{
public:
	Date(const Month& m,const Day& d,const Year& y);
...
};

Date d(Month(3),Day(30),Year(1995));

但是现在还会出现数值范围不正确的问题,比如一年只有12个月,不可能为1-12以外的值,需要采用束缚对象值的方法,预先给定有效的范围。

class Month{
public:
	static Month Jan() { return Month(1); }
	static Month Feb() { return Month(1); }
	...
	static Month Dec() { return Month(1); }
	...
private:
	explicit Month(int m);
	int month;
};
Date d(Month::Mar(),Day(30),Year(1995));

请记住:

  • 好的接口很容易被正确使用,不容易被误用。你应该在你所有接口中努力达到这些性质;
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容性;
  • “阻止误用”的办法包括建立新的类型、限制类型上的操作、束缚对象值,以及消除客户的支援管理责任
  • tr1::shared_ptr支持定制型删除器(custom delete)。这可以防范DLL问题,可被用来自动解除互斥锁(mutexes)等等。


条款19——设计class犹如设计type一样

    在C++中,当你定义一个新的class时,也就定义了一个新的type;重载函数和操作符、控制内存的分配与归还、定义对象的初始化和终结,你都需要考虑。
    设计class需要考虑的问题:

  • 新type的对象应该如何被创建和销毁:影响你的构造函数、析构函数以及内存分配函数(operator new、operator new[ ])和内存释放函数(operator delete、operator delete[ ])的设计;
  • 对象的初始化和对象的复制应该有什么差别:影响你的构造函数以及赋值运算符的设计;
  • 新type的对象如果被pass by value(按值传递),意味着什么:copy构造函数用于定义一个type的pass by value;
  • 新type的“合法值”:class中的成员变量可能有其取值范围;你的成员函数(特别是构造函数、赋值操作符或者所谓的“setter”函数)需要进行错误检查工作。
  • 新type需要配合谋和继承图系:如果新type继承自某些class,需要考虑哪些class中函数的virtual或者non-virtual;若你允许其他class继承新的type,你也需要考虑你新type的函数是否为virtual(特别是析构函数)。
  • 新type需要何种类型转换:如果你允许类型T1到T2的隐式类型转换,则需要在T1中写一个类型转换函数(operator T2)或在T2中写一个non-explicit-one-argument的构造函数。若只允许显示转换,则需写出负责执行转换操作的函数且不得是类型转换操作符和non-explicit-one-argument构造函数。
  • 什么操作符和函数对新type是合理的:你需要为你的class声明哪些函数,哪些应该是成员函数,哪些不是;
  • 编译器提供的函数不需要时应驳回:驳回的函数应当声明为private;
  • 谁该取用新type的成员:决定成员是public、protected或者private;决定哪一个class或者function是friend;
  • 决定type的一般化程度:是定义一个特点的class,还是定义一个class template;
  • 是否需要定义一个新type:如果只是定义新的derived class 为已有class添加新的机能,说不定定义一个或多个non-member函数或templates,会更好。

请记住:

  • class的设计就是type的设计,在定义一个新type时,考虑上述问题。


条款20——宁以pass-by-reference-to-const替换pass-by-value

问题一:不希望函数改变对象,按值传递昂贵的问题
    缺省情况下,C++都是以by-value的方式传递对象至函数。除非另外指定,否则函数参数都是以实际实参的副本为初值,而函数返回的也是返回值的副本,这些副本是由对象的copy构造函数生成的,以值传递是昂贵的。
    按值传递参数示例:

class Person{
public:
	Person();
	~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 f = validateStudent(plato);//这里调用一次Student构造函数,
                                //一次Person的构造函数以及四次string的构造函数
                                //与其对应的析构函数。

按pass-by-reference-to-const传递可以避免复制的成本太大的问题。

bool validateStudent (const Student& s);

问题二:避免slicing(对象切割)的问题
如下示例:

class Window
{
public:
	...
	std::name() const;
	virtial void display() const;
};
class WindowWithScrollBalls:public Window
{
public:
	...
	virtial void display() const;
};
//现在有一个函数会根据对象类型调用dispaly函数打印窗口
//如果采用按值传递的情况
void printNameAndDisplay(Window w)
{
	std::cout<<w.name();
	w.dispaly();
}
WindowWithScrollBalls wwsb;
printNameAndDisplay(wwsb);//这里调用的是Window的name和dispaly函数,因为wwsb对象被切割了。
//改成按照引用传递则不会出现这种问题
void printNameAndDisplay(const Window& w)
{
	std::cout<<w.name();
	w.dispaly();
}

解决切割问题的办法是按照by-reference-to-const的方式传递w。

两种例外,用pass-by-value比pass-by-reference-to-const更高效些

  • 对于内置类型(int 等)
  • STL的迭代器和函数对象

请记住:

  • 尽量以pass-by-reference-to-const代替pass-by-value,前者通常更加高效一些,并可以避免切割问题;
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往跟适合一些。


条款21——必须返回对象(值)时,别妄想返回其reference

    传递一些reference指向不存在的对象是不好的;
    任何函数如果返回一个reference指向某个local对象,都是错误的(如果函数返回的指针指向一个local对象也是一样)。

//Rational是一个class
const Rational& operator*(const Rational& lhs,const Rational&rhs)
{
	Rational result(lhs.n*rhs.n,lhs.d*rhs.d);
	return result;//返回了一个local变量的引用,是错误的
}

一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象呗

//Rational是一个class
const Rational operator*(const Rational& lhs,const Rational&rhs)
{
	return Rational(lhs.n*rhs.n,lhs.d*rhs.d);//返回一个对象
}

当你必须在“返回一个reference和返回一个object”之间抉择时,你需要的工作就是挑选行为正确的那个。

请记住:

  • 绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static 对象而可能同时需要多个这样的对象。


条款22——将成员变量声明为private

将成员变量声明为private,只能通过函数对其操作,可以实现精准的控制变量。如下所示:

class AccessLevels{
public:
	...
	int getReadOnly() const { return readOnly; }
	int getReadWrite() const { return readWrite; }
	void setReadWrite(int val) { readWrite = val; }
	void setWriteOnly(int val) { writeOnly = val; }
private:
	int noAccess;
	int readOnly;
	int readWrite;
	int writeOnly;

对每个成员变量可以设定(也可以不)getter函数和setter函数。
如果你将成员变量声明为public,一旦你改变它,则你不知道外部有多少的代码会改变;如果你将成员变量声明为protected,一旦你改变它,则你不知道有多少的derived class的代码会改变。这是极其不安全的。

请记住:

  • 切记将成员变量声明为private。这可以赋予客户访问数据的一致性、可细微划分访问权限控制、许诺约束条件获得保证,并提供class作者以充分的实现弹性。
  • protected并不比public更具有封装性。


条款23——宁以non-member、non-friend函数替换member函数(Prefer non-member non-friend functions to member functions)

    non-member non-friend函数比member函数具有更大的封装性(越少的代码可以看到数据,越多的数据可被封装,我们也能越自由的改变对象数据),在许多方面,non-member成员函数比member更好。
    一个例子:
   WebBrowser class中实现clearCache、clearHistory、removeCookies三个功能,现在希望一个函数能同时实现上述三种功能,有两种实现方法,一个是member函数,还有一个是nonmember函数。

class WebBrowser{
public:
	...
	void clearCache();
	void clearHistory();
	void removeCookies();
	...
	void clearEverything();
};
//以成员函数来实现
void WebBrowser::clearEverything()
{
	this->clearCache();
	this->clearHistory()this->removeCookies();
}
//以非成员函数来实现
void clearBrowser(WebBrowser& w)
{
	w.clearCache();
	w.clearHistory();
	w.removeCookies();
}

   non-member函数更好一些。上述讨论需要注意:只适用于non-member、non-friend函数;在意封装性而让函数“成为class的non-member”,并不意味着它“不可以是另外一个class的member”。
   让其成为non-member、non-friend函数并与class处于同一个namespace中更加好。

请记住:

  • 宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。


条款24——若所有参数皆需类型转换,请为此采用non-member函数

    只有当参数被列于参数列(parameter list)内,这个参数才可以进行隐式类型转换,而this对象不能够进行隐式类型转换。
    如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
    无论何时,如果你可以避免friend函数,就应该避免。

请记住:

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


条款25——考虑写出一个不抛出异常的swap函数

swap(置换)两对象的值,意思是将两对象的值彼此赋予对方。在缺省情况下,swap动作有标准程序库提供的swap算法完成,典型实现如下:

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

    只要T类型支持copying(copy构造函数和copy assignment操作符),缺省的swap实现代码会帮你置换类型T的对象,你无需进行其他任何工作。

    对于某些类型,默认的swap不够高效,比如“以指针指向一个对象,内含真正数据”,称为“pimpl”手法(“pointer to implementation”).
pimpl手法:

class WidgetImpl{
public:
	...
private:
	int a,b,c;
	std::vector<int> v;
	...
};
class Widget{
public:
	Widget(const Widget& rhs);
	Widget& operator=(const Widget& rhs)
	{
		...
		*pImpl = rhs.pImpl;
		...
	}
	...
private:
	WidgetImpl* pImpl;
};
//交换两个Widget对象,只需要交换两个pImpl指针,
//但是默认的swap算法复制了三个Widget对象还复制了三个WidgetImpl对象

请记住:

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
  • 如果你提供一个member swap,也该提供一个non-member swap 用来调用前者,对于classes(而非templates),也请特化std::swap;
  • 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何的“命名空间资格修饰”;
  • 为“用户定义类型”进行std template全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值