Effective C++条款21、22、23

条款二十一:当必须返回对象时,别妄想返回其reference

当你理解条款20后,很可能出现一种过度使用的情况:只要看到是一个非内置类型,就去使用引用传值。举个书上的例子:

class Rational
{
private:
    int numerator;
    int denominator;
public:
    Rational():numerator(0), denominator(1){}
    friend const Rational& operator* (const Rational& r1, const Rational& r2){…}
};

numerator表示分子,denominator表示分母,这里我们需要把注意力放在operator*上,不是在它的形参上(用的const引用是完全正确的),而是在它的返回值上,这个带引用的返回值是一个巨大的bug。

我们可以设想一下实现:

friend const Rational& operator* (const Rational& r1, const Rational& r2)
{
        Rational temp;
        temp.numerator = r1.numerator * r2.numerator;
        temp.denominator = r1.denominator * r2.denominator;
        return temp;
}

这是一种最容易想到的实现方式,在栈上创建对象temp,分子、分母分别相乘后,将之返回。

但仔细再想想,调用函数真的能收到这个temp吗?它是在operator*函数调用时的栈上产生的,当这个函数调用结束后,栈上的temp也会被pop掉,换言之,temp的生命周期仅存在于operator*函数内,离开这个函数,返回的引用将指向一个已经不在的对象!
对此,VS编译器已经给出了warning,如下:

“warning C4172: returning address of local variable or temporary”

千万不能忽略它。
那既然栈上创建对象不行,还可以在堆上创建嘛(new出来的都是在堆上创建的),于是我们有:

friend const Rational& operator* (const Rational& r1, const Rational& r2)
{
    Rational *temp = new Rational();
    temp->numerator = r1.numerator * r2.numerator;
    temp->denominator = r1.denominator * r2.denominator;
    return *temp;
}

这下VS编译器没有warning了,之前在资源管理那部分说过,new和delete要配对,这里只有new,那delete了?delete肯定不能在这个函数里面做,只能放在外面,这样new和delete实际位于两个不同的模块中了,程序员很容易忘记回收,而且给维护也带来困难,所以这绝对不是一种好的解决方案。书上举了一个例子,比如:

Rational w, x, y, z;
w = x * y * z;

连乘操作会有两次new的过程,我们很难取得operator*返回的reference背后隐藏的那个指针。

当然,如果把new换成auto_ptr或者是shared_ptr,这种资源泄露的问题就可以避免。

栈上创建的临时对象不能传入主调模块,堆上创建的对象就要考虑资源管理的难题,还有其他解决方法吗?

我们还有static对象可以用,static对象位于全局静态区,它的生命周期与这个程序的生命周期是相同的,所以不用担心它会像栈对象那样很快消失掉,也不用担心它会像堆对象那样有资源泄露的危险。可以像这样写:

friend const Rational& operator* (const Rational& r1, const Rational& r2)
{
    static Rational temp;
    temp.numerator = r1.numerator * r2.numerator;
    temp.denominator = r1.denominator * r2.denominator;
    return temp;
}

这样写编译器同样不会报错,但考虑一下这样的式子:

Rational r1, r2, r3;
if(r1 * r2 == r1 * r3){…}
if条件恒为真,这就是静态对象做的!因为所有对象共享这个静态对象,在执行r1*r2时,temp的值为t1,但执行r1*r3之后,temp的值统一都变成t2了。它在类中只有一份,明白这个原因后就不难理解了。

既然一个static对象不行,那弄一个static数组?把r1*r2的值放在static数组的一个元素里,而把r1*r3放在static数组的另一个元素里?仔细想想就知道这个想法是多么的天马行空。
一个必须返回新对象的正确写法是去掉引用,就这么简单!

friend const Rational operator* (const Rational& r1, const Rational& r2)
{
    Rational temp;
    temp.numerator = r1.numerator * r2.numerator;
    temp.denominator = r1.denominator * r2.denominator;
    return temp;
}

该让编译器复制的时候就要放手去复制。

最后总结一下:
绝对不要返回pointer或reference指向一个local stack对象,指向一个heap-allocated对象也不是好方法,更不能指向一个local static对象(数组),该让编译器复制对象的时候,就让它去复制!
返回 在函数中创建的变量,不要用引用,就用复制就好。

条款二十二:将成员变量声明为private

class Clothes
{
public:
    int price;
    string name;
};

假设有一个衣服的类,里面的成员变量用来描述它的价格和衣服名。将之设为public的话,类外可以直接接触到price成员变量。这样很危险,因为客户端可以直接修改price了,卖衣服的商家就会不开心了。而将price设置成private,像这样:

class Clothes
{
private:
    int price;
    string name;
public:
    int GetPrice()
    {
        return price;
    }
};

price的访问需要经由GetPrice()的成员函数才能进行,类外客户端不能直接对price进行操作。从这里就可以看到将price设为private的三个好处:

  1. 可以对访问进行控制,对于一个变量,可以设置它为可读可写(同时提供get和set函数),亦可设置为只读(只提供get函数),甚至可以设置成为只写(只提供set函数)。
  2. 语法一致性。很多时候程序员在编程的时候都在想到底是Object.price呢,还是Object.price()呢?如果只能通过成员函数访问,就知道后面应该加上小括号了。
  3. 面向对象的三大基石之一——封装。商家修改了这个类,比如在商店门口公布了打折信息,那么只要在类中这样做:
 int GetPrice()
 {
     return price * 0.9;
 }

客户端完全不需要作任何修改。封装可以更直观地打一个比方,有两个同学A和B,他们都约自己的朋友去爬山,A约了10个人,而B只约了1个人,不凑巧天下雨了,爬山的计划只能暂时取消,需要通知自己的朋友,这时你想想,是A容易通知呢,还是B容易通知呢?

是的,很明显应该是B好解决问题一些。为什么呢?因为与它发生关系(爬山)的人最少,所以处理起来很方便(一句话,接触越少越容易维护)。这唯一的1个人就好比是成员函数,如果功能变更,只要修改这个成员函数就可以了,而不必修改类外任何代码。

好了,我们总结一下为什么要将成员变量声明为private而不是public的原因:

  1. 能够提供语法一致性,写代码的时候能够马上区分要不要加函数括号;

  2. 提供更好的访问控制,只读、只写还是可读可写,都容易控制;

  3. 封装,减少与外界接触的机会,方便修改和维护。

现在回答下一个问题:为什么protected不行?

如果不存在继承关系,protected的作用与private等同,除了本类之外,其他类或者类外都不能访问,但如果存在继承关系时,protected标识的成员变量,在它的子类中仍可以直接访问,所以封装性就会受到冲击。这时如果更改父类的功能,相应子类涉及到的代码也要修改,这就麻烦了。而如果使用private标识,则根据规则,所有private标识的变量或者函数根本不被继承的,这样就可以保护父类的封装性了,仍可以在子类中通过父类的成员函数进行访问。

书上说了这样一句话:“从封装的角度观之,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。

最后总结一下:

  1. 切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性
  2. protected并不比public更具封装性
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值