6 复习 Complex 类的实现过程

6 复习 Complex 类的实现过程

首先一定要有一个好习惯, 就是防卫式的定义

#ifndef __COMPLEX__
#define __COMPLEX__

//...

#endif

这个名字是由你自己取的, 现在你看到的这个名字 COMPLEX 是标准库取的, 复数


接下来我要设计一个复数, 我先写一个 class head

#ifndef __COMPLEX__
#define __COMPLEX__

class complex // class head
{

    //...

};

#endif

然后我们写这个类, ,那我第一个考虑的是, 复数需要什么样的数据, 当然这个数据是私有 private 的

复数有实部和虚部, 所以我们给它写上

#ifndef __COMPLEX__
#define __COMPLEX__

class complex
{

    private:
        double re, im;  // 实部 re 和虚部 im

};

#endif

这时候我要考虑实部和虚部是什么样的数据类型, 我现在想的是 double 类型, 当然你想其它类型的数据也可以


然后我就想, 我要写哪些函数, 这些函数当然是操作在复数身上的

首先任何一个 class 我们都要去想它的构造函数

构造函数的语法是, 它是和 class 名称相同的函数, 并且没返回类型

那一个构造函数需要接收哪些参数呢 ? 这个例子它需要接收实部和虚部, 所以这里有两个参数 r i 来接收实部虚部

然后我们考虑要不要默认值, 我现在是想要给它默认值, 所以默认实部为 0 虚部为 0

我们还要考虑这个参数的传递是 pass by reference 还是 pass by value

我们这里传的是 double, 就是 pass by value, double 是 4 个字节, 所以在效率上它和引用是一样的, 当然你也可以把它改成传引用

#ifndef __COMPLEX__
#define __COMPLEX__

class complex
{
    public:
        complex(double r = 0, double i = 0) // 这个函数是对外使用的, 所以放在 public

    private:
        double re, im;
};

#endif

当我们想到构造函数, 我们要想到构造函数有一个很特殊的语法, initialization list, 初值列

#ifndef __COMPLEX__
#define __COMPLEX__

class complex
{
    public:
        complex(double r = 0, double i = 0)
            : re(r), im(i); // 初值列
        {}  // 这里不需要做任何事了, 这里就是空的

    private:
        double re, im;
};

#endif

初值列的语法很特别, 它有一个冒号

初值列的后面要做什么事情呢 ? 我们目前学的是最简单的一种情况. 初值列既然叫初值嘛, 那他就要设初值, 它多半是做这个事情, 所以我们这里把实部和虚部设置为传进来的 r 和 i

我充分运用了初值列这个时机点, 给它设了初值之后, 我还要继续想这个构造函数要干什么事情

以这个例子来讲已经不需要再做任何事情了, 所以大括号 {} 是空的


抛开一些奇怪的函数, 剩下的就是, 你作为设计者, 你要给复数设计一些什么函数, 给这个类一些什么能力

我们这里的想法是, 重载运算符

#ifndef __COMPLEX__
#define __COMPLEX__

class complex
{
    public:
        complex(double r = 0, double i = 0)
            : re(r), im(i);
        {}

    complex& operator += (const complex&);  // 重载运算符; 成员函数

    private:
        double re, im;
};

#endif

那在这些函数里面, 我们有两种选择, 一种是设计为成员函数, 一种是把它设计成非成员函数

都可以, 我这里是把这个函数设计成成员函数, 所以写在 class 里面


我还想给这个类设计函数, 取出实部和虚部的值

#ifndef __COMPLEX__
#define __COMPLEX__

class complex
{
    public:
        complex(double r = 0, double i = 0)
            : re(r), im(i);
        {}

    complex& operator += (const complex&);

    double real(){return re;}   // 取实部
    double imag(){return im;}   // 取虚部

    private:
        double re, im;  // 因为这里实部和虚部是 double, 所以上面两个函数的返回值类型就很好想了
};

#endif

然后我们要想, 当我们设计任何一个函数的时候, 它后面是不是要设计一个 const

我们之前说过, 如果这个函数里面不需要改它的数据的话, 我们应该给它设计一个 const

这里两个函数只是取出实部虚部, 并没有改动, 所以给它们加上 const

#ifndef __COMPLEX__
#define __COMPLEX__

class complex
{
    public:
        complex(double r = 0, double i = 0)
            : re(r), im(i);
        {}

    complex& operator += (const complex&);

    double real() const {return re;}    // 加 const
    double imag() const {return im;}    // 加 const

    private:
        double re, im;
};

#endif

但是这个例子比较特别的是, 我们有一个函数 __doapl do assignment plus, 这个是后面才想到的, 这里先打出来

这个函数由于需要直接取的实部虚部, 但是这些数据是私有的, 所以我这里在 class 里面, 宣布这个函数是我的朋友, 是友元

这样这个函数就可以直接取私有的实部和虚部了

#ifndef __COMPLEX__
#define __COMPLEX__

class complex
{
    public:
        complex(double r = 0, double i = 0)
            : re(r), im(i);
        {}

    complex& operator += (const complex&);

    double real() const {return re;}
    double imag() const {return im;}

    private:
        double re, im;

        friend complex& __doapl(complex*, const complex&);  // 宣布这个函数是我的朋友
};

#endif

现在我们已经把这个 class 的函数的想法写出来了, 当然里面一些函数已经是写好了, 那它就是 inline function 内联函数

那这些没有写出来的, 要在类的外面去写, 我们也可以让它是 inline function

现在我们来看这个类本体之外我们要写什么


我第一个要写的是 += 这个操作

complex::operator +=

这是一个操作符重载, 而它又是属于这个类里面的, 所以整个函数的全名就是这样. 里面有没有空格是无所谓的


complex::operator += (const complex& r)

然后我们去思考它的参数

+= 一定有一个左边和一个右边

由于它是一个成员函数, 是作用在左边身上, 所以左边就作为一个隐藏的参数放进来 (this). 所以 += 函数的参数, 我们只需要写右边就好

那右边我们考虑把它命名为 r

这里是首先考虑传递引用类型, 然后再考虑要不要加 const

这里右边是要加到左边身上, 右边是不动的所以是 const


接下来考虑它的 return type, 返回值类型

你把右边加到左边身上去, 得到的结果就是左边这种东西

左边是复数, 所以这里返回值类型是 complex

那么要不要传引用呢, 你所传出去的东西, 如果不是 local object, 也就是说不是在这个函数本体里面创建出来的, 那你就可以传递引用

我们是右边加到左边身上, 左边本来就存在的, 所以它不是 local object 不是在函数内部创建的, 所以这里可以传引用

这个函数是在 class 之外写的, 所以这里加上 inline, 当然它最后能不能成为 inline 还得看编译器, 这里建议它是 inline 的

inline complex&
complex::operator += (const complex& r)

实现任何一个函数都最好按照这样的想法去思考


接下来是定义部分, 我们写这个函数内部的操作

complex::operator += (const complex& r)
{
    return __doapl(this, r);
}

在这个设计里面, 我们把 += 这个操作丢给另外一个函数去做 __doapl, 因为所有的工作都是它来做, 所以我们把收到的两个参数原封不动地传给它

其实你可以不用这样设计, 你可以直接在这里完成 += 右边加到左边的操作, 标准库由于某些考量, 它把工作丢给了另外一个函数

inline complex& // 这种小小的函数我们一般都希望它是 inline 的
__doapl(complex* ths, const complex& r)

complex::operator += (const complex& r)
{
    return __doapl(this, r);
}

既然这两个参数已经确定下来了, 所以 __doapl 的参数类型也没什么问题了

因为是右边, 所以命名为 r, 然后要加到左边身上, 左边是会变动的, 所以左边不能加 const

接下来考虑它的返回值类型, 是复数 complex

然后我们就开始写里面的内容了, 真正去做 += 的操作

inline complex&
__doapl(complex* ths, const complex& r)
{
    ths->re += r.re;    // 把右边的实部加到左边实部身上
    ths->im += r.im;    // 把右边的虚部加到左边虚部身上
    return *ths;
}

complex::operator += (const complex& r)
{
    return __doapl(this, r);
}

这个很简单了, 把右边的实部加到左边实部身上, 把右边的虚部加到左边虚部身上, 这就完成了, 最后的结果在左手边 ths, 然后我们把它传回去


刚刚举的例子是一个具有代表性的它的成员函数

另外在复数的一些其它动作呢, 我会把它设计为非成员函数, 也就是全局函数

现在我打算设计一个 +

operator + (const complex& x, const complex& y)

为什么标准库不把 + 这个设计为成员函数 ?

一个考虑是, 你复数可以加实数, 实数也可以加复数, 而不只是复数加复数

我这里设计为一个全局函数, 所以它的名字前面就没有 class 的名称

inline complex
operator + (const complex& x, const complex& y)

加也有左边和右边, 所以两个参数接收左边和右边, 左边加完右边之后不会改变内容, 而是要放到另外一个地方去, 所以是 const, 然后尽量传引用

虽然我们还没写函数内部, 但是左边加完右边之后, 你需要一个新的地方来放结果, 那么这个结果一定是放在这个函数的里面新创建的一个对象

所以它是 local 的, 是 local 所以这里函数返回值一定不能是引用 return by value


然后我们开始写函数内部的动作

inline complex
operator + (const complex& x, const complex& y)
{
    return complex
    (
        real(x) + real(y),
        imag(x) + imag(y)
    );
}

刚刚已经说了这个函数里面必须要创建一个复数来存放结果

你在函数里面创建一个复数, 你可以 complex c1;

这里我们用一种比较罕见的语法

要创建对象是吧, 那我直接在返回这里创建对象

现在我们想这个新创建的复数, 它是可以赋初值的, 所以我就利用这样的写法, 把 x 和 y 的实部相加, 虚部相加, 当成新的复数的初值, 一口气把这些事情全部完成

这就是个临时对象

一般如果没经过训练的话会这么写 :

inline complex
operator + (const complex& x, const complex& y)
{
    double l, r;
    l = real(x) + real(y);
    r = imag(x) + imag(y);
    complex temp(l, r);
    return temp;
}

上面这个是复数加复数

我们又考虑到复数加实数

inline complex
operator + (const complex& x, double y) // 这里我们一开始就把实部虚部定义为 double 所以这里的 y 就是 double
{
    return complex(real(x) + y, imag(x));
}

我们再考虑到实数加复数, 这个次序不一样

inline complex
operator + (double x, const complex& y
{
    return complex(x + real(y), imag(y));
}

如果你把 + 这个操作写在复数这个类里面, 那它就只能应付复数加复数这种情况


complex c1(9, 8);
cout << c1;
//c1 << cout;

cout << c1 << endl;

现在想把复数丢到 cout 身上, 我们想使用者可能这样用

这里仍然是一个操作符重载, 重载 << 操作符

它一定是作用在左边身上的, 然后我们就可以写这个重载函数了

但是如果你这么思考的话, 使用者就必须这么写了 c1 << cout;, 把这个符号作用在复数身上. 可是使用者怎么可能会这样用, 因为使用者习惯 cout 放在左边, 东西放在右边

所以这一行应该避免

你可以写出来让使用者这么用, 可是使用者会很蛋疼它不喜欢这样用

所以只好把它写成非成员函数了


OK 我们开始写这个函数了

先把函数名称写出来, 它是非成员函数所以它的全名并没有前面的类名

第二个思考的是参数的类型

这样的设计是为了应付这种 cout << c1; 的用法, 所以左边是什么, 右边是什么就可以决定这个函数第一和第二参数是什么

左边是 cout, 那我们得去查手册, 查到它是 ostream 这个类, 然后传引用

右边是复数, 所以第二个参数是复数, 我们也传引用

再思考要不要加 const

现在把右边丢到左边身上, 那么右边是不会被改变的, 所以右边加上 const

那么丢到左边的话, 左边这个东西会不会改变呢, 这个如果基础不够的话是可能不知道它会不会改变, 其实它是会改变的, 它的状态会改变, 所以不可以写 const

因为我们现在要使用的东西叫 ostream, 它是在 iostream.h 这个头文件里面

然后我们思考它的返回值类型

我们看上面的这个代码 cout << c1 << endl;

如果你想使用者只是这么用 cout << c1;, 右边丢到左边身上, 丢完就算了, 那你这里返回值可以写 void, 不在乎

可是由于使用者可能这么用 cout << c1 << endl;, 连串地丢出

所以 c1 丢到 cout 之后, 应该传回一种东西可以接收右边丢过来的 endl

所以它应该传回像 cout 的这种东西, cout 是 ostream, 于是返回值是 ostream 类型

这个 ostream 并不是这一个函数的 local object, 不是在这个函数里面创建的, 所以这里可以返回引用类型

#include <iostream.h>
ostream&
operator << (ostream& os, const complex& x)

接口定义好了, 现在我们写函数内部代码

#include <iostream.h>
ostream&
operator << (ostream& os, const complex& x)
{
    return os << '(' << real(x) << ',' << imag(x) << ')';
}

这里既然是要输出到屏幕, 你想怎么输出到屏幕, 你就怎么写, 你想要输出小括号, 这里就写小括号

小括号, 实部, 逗号, 虚部, 小括号

输出完之后, 干脆我把输出的结果再丢回去, 所以这里直接 return os


最后你会获得的是 complex.h 和 complex-test.cpp 这两个代码文件