《C++编程惯用法——高级程序员常用方法和技巧》——2.3 公用数据

    xiaoxiao2024-03-17  20

    本节书摘来自异步社区出版社《C++编程惯用法——高级程序员常用方法和技巧》一书中的第2章,第2.3节,作者: 【美】Robert B. Murray ,更多章节内容可以访问云栖社区“异步社区”公众号查看。

    2.3 公用数据

    假设有如下一个复数类:

    class Complex { public:    double real;    double imag;    Complex (double r, double i) : real (r), imag(i) {}    //其他的被忽略 };

    这个类可能可以工作,但它的接口却有着重大的缺陷。问题出在那两个公用的数据成员:real和imag上面。这个接口保证Complex对象中的实部和虚部会以浮点数的形式存储在对象中。由于用户代码可以直接存取到公用的数据成员:

    Complex c(3.0,4.0); double d = c.real; c.imag = 0.0;

    所以一旦我们需要改变信息的存储格式或存储位置时,这样的改动会变得相当困难。

    假设在后面的某个版本中,我们希望将Complex的实现(而不是接口)中的信息存储方式由笛卡儿坐标格式改为极坐标格式。(也许我们有着的某些算法在处理使用极坐标格式表示的数字时效果最好。)如果笛卡儿坐标是公用数据,我们就将陷入泥潭:我们将不得不改变所有的用户代码或者是在每个Complex对象中同时维持极坐标和笛卡儿坐标格式。

    如果我们在一开始就避免使用公用数据,我们就不会碰到这样的问题了:

    class Complex { private:    double real_d;    double imag_d; public:    Complex(double r,double i):real_d(r),imag_d(i){}    double real() const { return real_d; }    duoble imag() const { return imag_d; }    void real(double r) { real_d = r; }    void imag(double i) { imag_d = i; }      //此处忽略细节 };

    一旦接口中指定可以通过Complex对象得到浮点型的实部和虚部之后,如何在对象中存储这些浮点数就变成了类的私有实现细节。用户代码要想取得这些数据,就必须调用类的成员函数,而不是直接去访问数据成员。

    Complex c(3.0, 4.0); double d = c.real(); c.imag (0.0);

    现在对于用户来说,唯一的不同就是他们为了获取值将不得不多输入两个字符(即括号)。由于Complex::real是一个内嵌函数,所以这两种形式产生的代码应该是一致的——将取值操作封装在一个内嵌函数中不会带来任何效率上的损失。同样,我们也可以将修改值的操作封装到内嵌函数中去。

    现在,我们的类中再也没有公用数据了,为了将信息以其他格式存储而修改类的实现也变得容易起来:

    //文件Complex.h class Complex { private:    double r;    double theta; public:    Complex(double re, double im); //不再是内嵌函数了    double real() const { return r*sin(theta); }    double imag() const { return r*cos(theta); }    void real(double); //不再是内嵌函数了    void imag(double); //不再是内嵌函数了    //此处忽略细节 }; //文件Complex.c Complex::Complex(double re, double im)    : r(sqrt( re*re + im*im )),     theta (atan2 (im,re)){ }   //"real(double)"和"imag(double)"留给读者作为练习

    此时,用户代码必须重新编译,程序的性能也会有一定的影响,但程序的接口没有改变。原来可以正常工作的程序仍然可以正常工作。

    我们也可以将实现改为把信息存储到其他的位置上去:

    class Complex_rep { private:    friend class Complex;    double real_d;    double imag_d;    Complex_rep(double r, double i): real_d(r),imag(i){} }; class Complex { private:    Complex_rep*rep; public:    Complex(double r, double i)    : rep(new Complex_rep(r,i)){}    ~Complex() { delete rep;}    //此处忽略细节 };

    我们将在第3章中解释为什么这么做。

    2.3.1 表示不变量(Representation invariant)

    公用数据还会使得我们的类难以用来保证表示不变量。表示不变量是一个谓词,对于某个完全被构建好的对象,它都将得到真值。

    例如,假设我们有一个用来表示有理数的类Rational,它里面有两个整型数(一个用于分子,一个用于分母):

    class Rational { private:    int num_d;    int denom_d, public:    Rational(int n, int d);    int num()const { return num_d;}    int denom()const { return denom_d;}    voie num(int n) {num_d = n;}    void denom(int);//改变分母 };

    如果我们的类具有这么一个表示不变量(分母denom_d不可能为0),这将会使得我们的算法变得更简单,并且更高效。我们在每个可能改变分母的函数中都对新的分母进行检测,如果它是0的话,我们就向外报告一个错误[2]:

    static void check_zero(int d){    if (d == 0) {      cerr << "Zero denominator in Rational\n";      abort();    } } Rational::Rational(int n, int d) : num_d(n),   denom_d(d) {    check_zero(d); ) void Rational::denom(int d){    check_zero(d);    denom_d = d; }

    有了这两个函数我们就可以保证不变量的成立,因此在Rational的其他成员函数中,我们并不需要担心出现分母为0的情况。如果我们将分母声明为一个公用数据成员,我们就无法来保证不变量的成立,用户代码也就可以在任意时刻将分母赋值为0。有关Rational的所有操作也都将不得不对这种可能性进行检测。

    综上所述,我们得到如下的结论:避免公用数据成员的出现。公用成员将使得我们很难去更改类的实现(如改变信息的存储格式或者存储位置);它也无法保证表示不变量的长期有效性。

    相关资源:敏捷开发V1.0.pptx
    最新回复(0)