接口、类、函数的设计问题

一、让接口容易被正确使用,不易被误用

1.接口应该让客户使用时,如果未得到预期行为,该代码不该通过编译,如果通过了编译,应该得到客户所想要的行为。

以下代码中,用户可能会使用错误的顺序,或者是使用错误超出范围的值。

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

Date d(30, 3, 1995);
Date d(2, 30 1995);

改进方法,最好struct还是改为class:

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

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

Date d(30, 3, 1995);	//错误
Date d(Day(30), Month(3), Year(1995));	//错误
Date d(Month(3), Day(30), Year(1995));	//正确

有些类型确定后,其值是确定的:

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

2.shared_ptr虽然比原始指针大且慢,但是这额外的执行成本并不显著,降低客户错误的成效是更加显著的

二、设计class犹如设计type

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

1.一般默认情况下,函数的参数以及返回值都是以实际值的复件(副本)传回的,通过对象的copy构造函数产出,这将是一个非常昂贵的操作。改为pass by reference-to-const方法,通过const方法可以让使用者不用为自己传入的对象担心。

bool validateStudent(const Student& s);

2.通过by reference方式传递参数时可以和避免slicing(对象切割)问题。在以下方法中,传入的是WindowWithScrollBars对象,但是会通过复制构造函数,构造为Window对象,因此以下调用的w.display()为Window对象的方法。

class Window{
public:
    ...
    std::string name() const;
    virtual void display() const;
};
class WindowWithScrollBars: public Window{
public:
    ...
    virtual void display() const;
}
void printNameAndDisplay(Window w){
    std::cout<<w.name();
    w.dispaly();
}

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

改进,在C++编译器的底层,往往以指针实现出来,因此pass by reference通常意味真正传递的是指针:

void printNameAndDisplay(const Window& w){
    std::cout<<w.name();
    w.dispaly();
}

通常对内置对象以及STL的迭代器和函数对象采用pass-by-value,更加高效

四、必须返回对象时,就不用返回reference

1.错误写法1

以下方法中,会导致严重问题,因为初始化的Rational为local对象,在函数退出前就被销毁了。

const Rational& operator* (const Rational& lhs, const Rational& rhs){
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
    return result;
}

2.错误写法2,以下方法中x*y*z会生成两个对象,但是后面没有调用delete方法。

const Rational& operator* (const Rational& lhs, const Rational& rhs){
    Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
    return *result;
}

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

3.正确写法,直接返回构造的对象

const Rational& operator* (const Rational& lhs, const Rational& rhs){
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

五、为了保证封装性,需要将成员变量声明为private

1.将成员变量声明为private,访问其的唯一方法就是通过成员函数,可以防止用户迷惑地记住是否该使用小括号

2.可以实现出“不准访问”、“只读访问”、“读写访问”。

3.可以通过函数访问成员变量,相当于把该成员变量封装起来了,以后我们要更改该成员变量的存储形式和内容,可以直接去private中修改,用户在其他地方调用不了该成员变量,因此我们下一次在修改该成员变量形式的时候可以少修改很多地方。某些东西的封装性与“成员变量的内容改变时所破坏的代码数量”成反比。

class SpeedDateCollection{
public:
    void addValue(int speed);	//添加一笔新数据
    double averageSoFar() const;	//	返回平均速度
}

以上代码中返回均速度,有两种形式,第一种是每一次添加新数据时,将其用来计算平均速度存下来,下一次调用返回平均速度直接读取就行。第二种是每次把新数据存下来,直到需要返回平均速度时再去计算。两种方法实现形式是不一样的,但是都能通过averageSoFar方法进行返回,根据系统的内存以及速度要求进行选择。

六、宁以non-member、non-friend替换member函数

在以下方法中,方法二提供了更好的封装性,

class WebBrowser{
public:
    ...
        void clearCache();
    void clearHistory();
    void removeCookies();
    
    
    //方法一
    void clearEverything();
}

//方法二
void clearBrowser(WebBrowser& wb){
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}


1.friend函数对class private成员的访问权力和member函数相同,以上论述只适用于non-meber non-friend函数

2.该non-member函数可以放在其他类里,也就是说可以声明一个工具类用于存放该函数

在C++中通常将该函数放在与类同一个命名空间中

namespace WebBrowserSStuff{
    class webBrowser{...};
    void clearBrowser(WebBrowser& wb);
}

七、若所有参数皆需类型转换,请为此采用non-member

class Rational{
public:
    ...
    const Rational operator* (const Rational& rhs) const;
};

以下方法中,在编译器对整数2并没有相应的class,没有operator成员函数,编译器会去尝试寻找non-member operator,但在此例中会寻找失败

result = oneHalf.operator*(2);
result = 2.operator*(oneHalf);

解决办法,声明一个non-member operator方法

const Rational operator*(const Rational& lhs, const Rational& rhs){
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

八、考虑写出一个不抛异常的swap函数,当std::swap效率不高时提供一个swap成员函数,并命名在对应成员空间

Swap函数

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

传统的swap函数用于交换两个对象,他会对对象内部的所有数据进行复制和交换。在以下方法中,交换两个Widget对象,原本只需要交换两个指针pImpl,但是会先进行初始化,初始化的过程中会复制构造WidgetImpl,导致大量的运算量

class WidgetImpl{
public:
    ...
private:
    int a, b, c;
    std::vector<double> v;
};

class Widget{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs){
        ...
        *pImpl = *(rhs.pImpl);
        ...
    }
private:
    WidgetImpl* pImpl;
};


1.重新定义特化版本swap,以下方法访问的变量是其private成员,无法访问,

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

2.修改方法,通过成成员函数进行swap

class Widget{
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs){
        ...
        *pImpl = *(rhs.pImpl);
        ...
    }
    
    void swap(Widget& other){
        using std::swap;
        swap(pImpl, other.pImpl);
    }
private:
    WidgetImpl* pImpl;
};

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

3.如果两个类是类模板的话,可以尝试声明为类模板的函数

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

以上方法无法进行,以上属于偏特化一个函数模板,无法进行,可以修改为以下重载函数,但是最好不要重载std命名空间里的模板

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

可以将其声明在其他命名空间,实际在调用的时候,如果不指明命名空间,比如std::swap(obj1,obj2),编译器将会更具T的类型去对应命名空间中找对应函数,如果没有再去std中查找。

namespace WidgetStuff{
    template<typename T>
    class Widget{
    }
    
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b){
		a.swap(b);
    }
}

4.当提供member swap时,应该提供一个non-member swap方法

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值