Effective C++ 笔记(四)

四:设计与声明
条款18:让接口容易被正确使用,不易被误用
请记住:1.好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达到这些性质。
2.“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
3.“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
4.tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。

条款19:设计class犹如设计type

请记住:Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。p84-86

条款20:宁以pass-by-reference-to-const替换pass-by-value
尽量以pass-by-reference-to-const替换pass-by-value,因为前者没有任何构造函数或析构函数被调用,没有任何新对象被创建。并且前者还可以避免切割问题。
切割问题:当一个derived class 对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而“造成此对象的行为像个derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。

//图形窗口系统
class Window{
public:
    ... 
    std::string name() const;
    virtual void display() const;//display是个virtual函数,这意味着Window对象的显示方式和WindowWithScrollBars的显示方式不同
};
class WindowWithScrollBars :public Window{
    ...
    virtual void display() const;
};
	
//假设写个函数打印窗口名称
void printNameAndDisplay(Window w) {   //不正确  参数可能被切割
	std::cout<<w.name();
	w.display();
}
//当调用上述函数并传递给他一个WindowWithScrollBars对象
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
/*
参数w会被构造成一个Window 对象:它是passed by value,造成WindowWithScrollBars的行为像个WindowWithScrollBars对象的那些特化性质全
被切割掉了,在printNameAndDisplay函数内不论传递过来的对象原来是什么类型,参数w就像一个Window对象,因此printNameAndDisplay内调用的
display调用的总是Window::display,绝不会是WindowWithScrollBars::display.
*/
//解决办法,传递引用
void printNameAndDisplay(const Window &w) {   //不正确  参数可能被切割
	std::cout<<w.name();
	w.display();
}

请记住:1.尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
2.以上规则不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较合适。

条款21:必须返回对象时,别妄想返回reference
容易犯下一个致命错误:开始传递一些references指向其实并不存在的对象。

//表现有理数的class,内含一个函数用来计算两个有理数的乘积
class Rational{
public:
    Rational(int numerator = 0,int denominator = 1);
private:
    int n, d;
    //以by value方式返回其计算结果(一个对象),会付出该对象构造和析构成本。
    //friend const Rational operator*(const Rational &lhs, const Rational &rhs);
};
//改变一:以by reference方式返回其计算结果
const Rational& operator*(const Rational &lhs, const Rational &rhs){
    /*
    警告,糟糕的代码,不仅需要构造,更严重的是:这个函数返回一个reference指向local对象,而local对象在函数退出前被销毁了,
    任何调用者甚至只是对此函数的返回值做一点点运用,都会发生致命的错误。
    */
    Rational result(lhs.n * rhs.n, lhs.d*rhs.d); 
    return result; 
}
//改变二:考虑在heap内构造一个对象,并返回reference指向他。
const Rational& operator*(const Rational &lhs, const Rational &rhs){
    /*
    还是必须付出一个“构造函数调用”代价,并会留下一个问题,谁该对着被你new出来的对象实施delete,并且某些情况下
    无法阻止资源泄漏,如下:
    Rational w,x,y,z;
    w = x*y*z; 
    没有合理的办法让调用者取得operator*返回的reference背后隐藏的那个指针
    */
    Rational *result = new Rational(lhs.n * rhs.n, lhs.d*rhs.d); 
    return *result; 
}
//改变三:返回的reference指向一个被定义于函数内部的static Rational对象。
const Rational& operator*(const Rational &lhs, const Rational &rhs){
    /*
        考虑以下这些完全合理的客户代码
        bool operator == (const Rational& lns,const Rational& rhs);
        Rational a,b,c,d;
        if((a*b)==(c*d)){

        }else{

        }
        表达式(a*b)==(c*d)总是被核算为true,不论a,b,c,d的值是什么,等价形式if(operator==(operator*(a,b),operator*(c,d)))
        两次operator*调用的确各自改变了static Rational对象值,但由于他们返回的都是reference,因此调用端看到的永远是static Rational
        对象的“现值”。
    */
    static Rational result; // 警告,又一堆烂代码。
    result = ...;  
    return result; 
}
//一个“必须返回新对象”的函数的正确写法是:就让那个函数返回一个新对象
inline const Rational operator*(const Rational &lhs, const Rational &rhs){
    //ps:关键字inline必须与函数定义放在一起才能使函数成为内联函数,仅仅将inline放在函数声明前面不起任何作用
    return Rational(lhs.n * rhs.n, lhs.d * rhs);
}

请记住:绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

条款22:将成员变量声明为private
将成员变量声明为private,使用函数可以让你对成员变量的处理有更精确的控制。如下:

class AccessLevels{
public:
    int getReadOnly() const { return readOnly; }
    void setReadWrite(int value) { readWrite = value; }
    int getReadWrite() const { return readWrite; }
    void setWriteOnly(int value) { WriteOnly = value; }
private:
    int noAccess;   //对此int无任何访问动作
    int readOnly;   //对此int做只读访问
    int readWrite;  //对此int做读写访问
    int WriteOnly;  //对此int做惟写访问
};

封装!如果你通过函数访问成员变量,日后可改以某个计算替换这个成员变量,而class客户一点也不会知道class的内部实现已经起了变化。将成员变量隐藏在函数接口背后,可以为“所有可能的实现”提供弹性。

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

条款23:宁以non-member、non-friend替换member函数
namespace可跨越多个源码文件。将所有便利函数放在多个头文件内但隶属用同一个命名空间,意味客户可以轻松扩展这一组便利函数。他们需要做的就是添加更多non-member non-friend函数到此命名空间内。p101

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

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

class Rational{
public:
    Rational(int numerator = 0,int denominator = 1);//构造函数刻意不为explicit;允许 int-to-Rational隐式转换
    int numerator() const;             //分子分母的访问函数
    int denominator () const;
    const Rational operator*(const Rational &rhs);
private:
    int n, d;
};
//看下面这些例子
	Rational oneEighth(1, 8);
	Rational oneHalf(1, 2);
	Rational result = oneEighth * oneHalf;  //很好
	result = result * oneEighth;  //很好
	//----------------------------------------------------
	result = oneHalf * 2;   // 没问题,在non-explicit构造函数的情况下
	result = 2 * oneHalf; // 错误,不满足乘法交换律,不符合实际。
	/*
	结论是,只有当参数被列于参数列内,这个参数再试隐式类型转换的合法参与者。地位相当于“被调用之成员函数所隶属的那个对象”————
	即this对象————的那个隐喻参数,绝不是隐式转换的合法参与者。这就是为什么上述第一次调用可以通过编译(result = oneHalf * 2;)
	,而第二次则否(result = 2 * oneHalf;)
	*/

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

条款25:考虑写出一个不抛异常的swap函数
全特化版本:

namespace std{
	template<>
	void swap<Widget>(Widget &a,Widget &b){
		swap(a.pImpl,b.pImpl);
		}
}
/*
这个函数一开始的“template<>”表示它是std::swap的一个全特化版本,函数名称之后的“<Widget>”表示这一特化版本系针对“T是Widget”而设计。
换句话说当一般性的swap template施行于Widgets身上便会以(被允许)为标准template(如swap)制造特化版本,使他专属于我们自己的classes(例如Widget)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二零二三.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值