C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)

文章目录

  • 1. 类的6个默认成员函数
  • 2. 构造函数
    • 构造函数概念
    • 构造函数特性
      • 特性1,2,3,4
      • 特性5
      • 特性6
      • 特性7
  • 3. 析构函数
    • 析构函数概念
    • 析构函数特性
      • 特性1,2,3,4
      • 特性5
      • 特性6
  • 4. 拷贝构造函数
    • 拷贝构造函数概念
    • 拷贝构造函数特性
      • 特性1,2
      • 特性3
      • 特性4
      • 特性5
  • 5. 运算符重载
    • 一般运算符重载
    • 赋值运算符重载
      • 赋值运算符重载格式
      • 赋值运算符只能重载成类的成员函数不能重载成全局函数
      • 用户没有显式实现时, 编译器会生成一个默认赋值运算符重载, 以值的方式逐字节拷贝
    • 前置 `++` 和 后置 `++` 重载
    • 流插入 流提取 的重载
  • 6. `const` 成员
  • 7.取地址及 `const` 取地址操作符重载

1. 类的6个默认成员函数

如果一个类中什么成员, 简称为空类.

空类中并不是什么都没有, 任何类在什么都不写时, 编译器会自动生成以下 6 个默认成员函数.

默认成员函数: 用户没有显示实现, 编译器会生成的成员函数称为默认成员函数.

class Date{};

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第1张图片

在我们学习这些默认生成的成员函数时, 我们需要把两点疑问放在心上

1. 我们不写这些成员函数, 编译器自己生成默认的成员函数, 这些函数做了些什么?

2. 我们自己要写这些成员函数, 应该如何实现?

2. 构造函数

构造函数概念

在类和对象(上)中, 初始化类生成的对象, 使用了成员函数 Init(), 但有时候, 会经常忘记初始化, 造成不可预料的结果.

例如日期类:

class Date
{
private:
    int _year;
    int _month;
    int _day;

public:
    void Init(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print()
    {
        cout << _year << '-' << _month << '-' << _day << endl;
    }
};

如果不初始化, 或者忘记初始化, 该对象的年月日是随机值:

int main()
{
    Date d;
    d.Print();

    return 0;
}

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第2张图片


为了防止忘记初始化, 或者简化每次创建对象都需要初始化的行为.C++引入了构造函数

构造函数是一个特殊的成员函数, 名字与类型相同, 创建类类型对象时由编译器自动调用, 以保证每个数据成员都有一个合适的初始值, 并且在对象整个生命周期内只调用一次.

构造函数特性

构造函数是特殊的成员函数, 需要注意的是, 构造函数虽然名称叫构造, 但是构造函数的主要任务并不是开空间创造对象, 而是初始化对象.

特性1,2,3,4

其特征如下

  1. 函数名与类名相同.
  2. 无返回值
  3. 对象实例化时编译器自动调用对应的构造函数
  4. 构造函数可以重载

首先我们要注意的是, 全缺省函数和无参函数虽然构成函数重载, 但是不能同时存在, 会让编译器不知道具体调用哪一个:

Date()
{}

Date(int year = 1, int month = 1, int day = 1)
{
    _year = year;
    _month = month;
    _day = day;
}

下面的代码展示了构造函数是怎么起作用的:

class Date
{
private:
    int _year;
    int _month;
    int _day;
public:
    //1. 无参构造函数
    Date()
    {}

    //2. 带参构造函数
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
};

int main()
{
    Date d1;                //调用无参构造函数
    Date d2(2000, 1, 1);    //调用带参构造函数

    return 0;
}

在这里插入图片描述

调用无参构造函数只能类名 对象名, 不能在后面跟上圆括号, Date d1()这是错的, 这会被编译器当成函数声明.

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第3张图片


特性5

  1. 如果类中没有显示定义构造函数, 则 C++ 编译器会自动生成一个无参的默认构造函数, 一旦用户显示定义编译器将不再生成.

一开始没有写构造函数, 直接创建对象是不会出问题的, 但是生成的是随机值, 可以大概得到一个模糊的结果: 编译器会自动生成一个默认构造函数.

一开始没有写构造函数, Date d1 不会报错
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第4张图片

如果自己写了带参数的构造函数, Date d1 就会报错了
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第5张图片


不实现构造函数的情况下, 编译器会生成默认的构造函数. 但是生成的仍然是随机值, 看起来构造函数却没有什么用?

特性6

  1. C++ 默认生成的构造函数, 内置类型不处理, 自定义类型会调用其构造函数.

C++ 把类型分为内置类型和自定义类型. 内置类型就是语言提供的数据类型, 如:int, char, double, 指针...(自定义类型的指针也是内置类型如Date*);自定义类型就是使用class, struct, union...定义的类型.

下面的代码就可以证明这个特性:
之前做过用两个栈模拟队列的题目, 这边先创建队列的对象.

typedef int STDataType;
class Stack
{
private:
    STDataType* _array;
    int _capacity;
    int _top;
public:
    Stack(int capacity = 4)
    {
        _array = (STDataType*)malloc(sizeof(STDataType) * capacity);

        if (nullptr == _array)
        {
            perror("malloc fail");
            exit(-1);
        }

        _capacity = 4;
        _top = 0;
    }
};

class MyQueue
{
private:
    //自定义类型
    Stack _pushst;
    Stack _popst;

    //内置类型
    int _size;
};

int main()
{
    Stack st;
    MyQueue mq;

    return 0;
}

我只创建了 Stack 的构造函数, MyQueue 并没有创建.
但是调试发现 MyQueue 创建的对象 mq 的自定义类型成员被默认初始化了, 内置类型成员却仍然是随机值.
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第6张图片

如果我把 Stack 的构造函数删去, 则会出现下面的结果:
所有的类型全是随机值, 即使创建 mq 的时候会调用 Stack 类型的构造函数, 但是是默认构造函数, 而 Stack 类中的所有成员变量都是内置类型, 所以最后所有的成员都是随机值.
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第7张图片

由此可以看出, C++ 默认生成的构造函数, 不处理内置类型成员变量, 自定义类型成员变量会调用其构造函数


对于 C++ 默认构造函数只处理自定义类型的缺陷

C++11 中打了补丁, 即:内置类型成员变量在声明中可以给默认值

class Time
{
private:
    int _hour;
    int _minute;
    int _second;
public:
    Time()
    {
        cout << "Time()" << endl; 
        _hour = 0;
        _minute = 0;
        _second = 0;
    }
};

class Date
{
private:
    // 内置类型
    int _year = 1970;
    int _month = 1;
    int _day = 1;

    // 自定义类型
    Time _t;
};

int main()
{
    Date d1;

    return 0;
}

打印 d1 的所有成员, 均初始化完毕
在这里插入图片描述


如果同时存在缺省值和自己创建的构造函数, 优先构造函数.

在上面的 Date 类中添加了自己写的 Date 构造函数, 结果编译器优先使用构造函数进行构造.

Date()
{
    _year = 2000;
    _month = 4;
    _day = 1;
}

程序运行如下:
在这里插入图片描述


特性7

  1. 无参构造函数, 全缺省构造函数, 我们没写编译器默认生成的构造函数, 都可以认为是默认构造函数.
    但是只能有一个默认构造函数.

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第8张图片

//无参构造函数
Date()
{
    _year = 2000;
    _month = 4;
    _day = 1;
}
//全缺省构造函数
Date(int year = 2000, int month = 4, int day = 1)
{
    _year = year;
    _month = month;
    _day = day;
}

推荐使用全缺省构造函数作为默认构造函数, 这样可以做到程序自己初始化, 自己自定义初始化.

int main()
{
    Date d1;
    Date d2(1999, 1, 1);

    return 0;
}

在这里插入图片描述


关于如何写构造函数的策略:

  1. 一般情况下, 自己要写构造函数
  2. 如果成员都是自定义类型, 或者声明类时已经给了缺省值, 可以考虑让编译器自己生成

3. 析构函数

析构函数概念

构造函数可以对对象进行初始化, 那么这些数据是如何归还给操作系统的呢?

析构函数: 与构造函数功能相反, 析构函数不是完成对象本身的销毁, 局部对象销毁工作是由编译器完成的. 而对象在销毁时会自动调用析构函数, 完成对象中的资源清理工作.

析构函数特性

特性1,2,3,4

析构函数是特殊的成员函数, 其特征如下:

  1. 析构函数名是在类名前面加上字符~
  2. 无参数无返回类型
  3. 一个类只能有一个析构函数. 若未显示定义, 系统会自动生成默认的析构函数. 注意: 析构函数不能重载
  4. 对象生命周期结束时, C++ 编译系统自动调用析构函数
class Date
{
private:
    int _year = 1970;
    int _month = 1;
    int _day = 1;
public: 
    ~Date()
    {//严格来说, Date类不需要析构函数
        cout << "~Date()" << endl;
    }
};

int main()
{
    Date d1;

    return 0;
}

在对象被自动销毁前, 程序会自行调用析构函数, 先把类中的成员得到的资源归还给操作系统.
在这里插入图片描述


特性5

  1. 编译器生成的默认析构函数, 对内置类型不做处理, 自定义类型调用它的析构函数

还是双栈构造队列的代码:

typedef int STDataType;
class Stack
{
private:
    STDataType* _array;
    int _capacity;
    int _top;
public:
    Stack(int capacity = 4)
    {//Stack的构造函数
        _array = (STDataType*)malloc(sizeof(STDataType) * capacity);

        if (nullptr == _array)
        {
            perror("malloc fail");
            exit(-1);
        }

        _capacity = 4;
        _top = 0;
    }

    ~Stack()
    {//Stack的析构函数
        cout << "~Stack()" << endl; 
        free(_array);
        _array = nullptr;
        _capacity = _top = 0;
    }
};

class MyQueue
{
private:
    //自定义类型
    Stack _pushst;
    Stack _popst;

    //内置类型
    int _size = 0;
};

int main()
{
    Stack st;
    MyQueue mq;

    return 0;
}

可以看到, 虽然 MyQueue 没有写析构函数, 但是系统生成的默认析构函数调用了其自定义类型成员的析构函数, 所以一共会有三次调用 ~Stack() 的记录.

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第9张图片

main 方法中只创建了 1 次 Stack 类的对象, 但是最终会调用 3 次 Stack 类的析构函数.

原因是, 系统要销毁 mq 对象前, 会先调用 MyQueue 类的析构函数, 但是我们没有写, 会调用系统默认的析构函数.

系统默认的析构函数, 会自动调用自定义类型成员的析构函数, 正好 MyQueue 类有两个 Stack 类的成员, 调用了 2 次.

剩下的内置类型空间交由系统释放, 入栈出栈用栈帧指针控制.


特性6

  1. 如果类中没有申请资源时, 析构函数可以不写, 直接使用编译器生成的默认析构函数, 比如 Date 类; 有资源申请时, 一定要写, 否则会造成资源泄露, 比如 Stack 类.

4. 拷贝构造函数

拷贝构造函数概念

在创建对象的时候, 可否创建一个与已存在对象一模一样的新对象呢?

拷贝构造函数: 只有单个形参, 该形参是对本类类型对象的引用(一般用 const 修饰), 再用已存在的类类型对象创建新对象时由编译器自动调用.


那么拷贝构造函数有什么用呢? 系统明明会自己拷贝, 例如在函数调用的时候.

我们都知道当调用函数时, 函数形参是实参的拷贝.

若实参所占的内存空间很大, 传值效率很低. 如果该实参类型的对象是需要申请空间的, 如果还是传值操作, 编译器的默认操作就会造成错误:

typedef int STDataType;
class Stack
{
private:
    STDataType* _array;
    int _capacity;
    int _top;
public:
    void Print()
    {
        cout << "_array: " << _array << endl;
        cout << "_capacity: " << _capacity << endl;
        cout << "_top: " << _top << endl << endl;
    }

    // 构造函数
    Stack(int capacity = 4)
    {
        cout << "Stack()" << endl; 
        _array = (STDataType*)malloc(sizeof(STDataType) * capacity);

        if (nullptr == _array)
        {
            perror("malloc fail");
            exit(-1);
        }

        _capacity = 4;
        _top = 0;
    }

    // 析构函数
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_array);
        _array = nullptr;
        _capacity = _top = 0;
    }
};

// 写一个函数, 将 Stack 类类型的变量传值操作当作参数
void func(Stack st)
{
    cout << "func方法中的st:" << endl; 
    st.Print();
}

int main()
{
    Stack stack;
    cout << "main方法中的stack:" << endl; 
    stack.Print();

    func(stack);   //传值操作

    return 0;
}

程序调用了两次 Stack 类的析构函数后, 崩溃了

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第10张图片

分析一下程序的操作

  1. 首先创建了一个 Stack 类的对象调用了一次构造函数

  2. 接着调用 func, 函数是传值操作, 编译器直接创建了一个 Stack 类类型的对象,(其实这个时候也调用了系统默认生成的构造函数) 并同时将 mainstack 的成员数据复制给给 func 中的 st. 这个时候发现, stackst 两个对象的 _array 的地址是一样的, 即指向一片空间
    C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第11张图片

  3. func 函数结束前, 需要销毁创建的临时变量 st, 调用析构函数, 直接释放了 _array 的空间, 而这一片空间同时也被 main 方法中的 stack 使用着.

  4. 最后 main 方法结束, 销毁 stack 就出现错误了, 调用析构函数释放已经释放过的内存空间, 造成了内存泄漏.

由此可以看到, 编译器默认能做的只是浅拷贝, 如果拷贝需要动态开辟空间的类型对象, 会出现问题.

而自己写的拷贝构造函数就要做到"深拷贝", 如果被拷贝对象有动态开辟的空间, 需要把这一块空间的数据也拷贝一份到新的对象中去.

拷贝构造函数特性

特性1,2

拷贝构造函数也是特殊的成员函数

  1. 拷贝构造函数是构造函数的一种重载形式
  2. 拷贝构造函数的参数只有一个必须是类类型对象的引用, 使用传值方式编译器直接报错, 因为会引发无穷递归调用.

如果拷贝构造函数使用传值操作, 会不停的创建临时对象调用拷贝构造, 造成无穷递归
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第12张图片

同时编译器也会直接报错, 提示只能传引用
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第13张图片

下面是正确的拷贝构造函数写法, 当然传指针也是可以的, 但是明显引用更好.

typedef int STDataType;
class Stack
{
private:
    STDataType* _array;
    int _capacity;
    int _top;
public:
    void Print()
    {
        cout << "_array: " << _array << endl;
        cout << "_capacity: " << _capacity << endl;
        cout << "_top: " << _top << endl << endl;
    }

    // 构造函数
    Stack(int capacity = 4)
    {
        cout << "Stack()" << endl; 
        _array = (STDataType*)malloc(sizeof(STDataType) * capacity);

        if (nullptr == _array)
        {
            perror("malloc fail");
            exit(-1);
        }

        _capacity = 4;
        _top = 0;
    }

    // 拷贝构造函数
    Stack(const Stack& st)
    {
        cout << "Stack(const Stack& st)" << endl;

        _array = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
        if (_array == nullptr)
        {
            perror("malloc fail");
            exit(-1);
        }
        memcpy(_array, st._array, sizeof(STDataType) * st._top);
        _top = st._top;
        _capacity = st._capacity;
    }

    // 析构函数
    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_array);
        _array = nullptr;
        _capacity = _top = 0;
    }
};

// 写一个函数, 将 Stack 类类型的变量传值操作当作参数
void func(Stack st)
{
    cout << "func方法中的st:" << endl; 
    st.Print();
}

int main()
{
    Stack stack;
    cout << "main方法中的stack:" << endl; 
    stack.Print();

    func(stack);   //传值操作

    return 0;
}

程序正常运行.
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第14张图片


特性3

  1. 若未显示定义, 编译器会生成默认的拷贝构造函数. 默认的拷贝构造函数对象按内存存储按字节序完成拷贝, 这种拷贝叫做浅拷贝, 或者值拷贝.

默认生成的拷贝构造函数完成值拷贝, 也是 C++ 对 C语言 的传承.

class Time
{
private:
    int _hour;
    int _minute;
    int _second;
public:
    // 构造函数
    Time()
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }

    // 拷贝构造函数
    Time(const Time& t)
    {
        _hour = t._hour;
        _minute = t._minute;
        _second = t._second;
    }

    void Print()
    {
        cout << _hour << ": " << _minute << ": " << _second << endl;
    }
};

class Date
{
private:
    // 内置类型
    int _year = 1970;
    int _month = 1;
    int _day = 1;

    // 自定义类型
    Time _t;
public:
    void Print()
    {
      cout << _year << '-' << _month << '-' << _day << ' ';
      _t.Print();
    }

};

int main()
{
    Date d1;
    d1.Print();

    // 用已经存在的 d1 拷贝构造 d2, 此处会调用 Date 类的拷贝构造函数
    // 但 Date 类没有显式定义拷贝构造函数, 则编译器会给 Date 类生成一个默认的拷贝构造函数
    Date d2(d1);
    d2.Print();
    return 0;
}

两个对象完全一样.
在这里插入图片描述

在编译器生成的默认拷贝构造函数中, 内置类型是按照字节方式直接拷贝的, 而自定义类型是调用其拷贝构造函数完成拷贝的.


特性4

  1. 如果类中涉及资源申请, 必须要自己写拷贝构造函数进行深拷贝; 如果不涉及, 不写使用编译器默认的拷贝构造函数也是可以的.

特性5

  1. 拷贝构造函数典型调用场景:
  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象
  • 函数返回值类型为类类型对象
class Date
{
public:
    // 构造函数
    Date(int year, int minute, int day)
    {
        cout << "Date(int, int, int)" << this << endl;
    }

    // 拷贝构造函数
    Date(const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;

        cout << "Date(const Date& d)" << this << endl;
    }

    // 析构函数
    ~Date()
    {
        cout << "~Date()" << this << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

Date Test(Date d)
{
    Date temp(d);
    return temp; 
}

int main()
{
    Date d1(2000, 1, 1);
    Test(d1);

    return 0;
}

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第15张图片

如果我们返回的是一个树怎么办, 需要一个一个结点进行拷贝, 这所消耗的时间和空间都是巨大的.

为了提高程序效率, 一般对象传参时, 尽量使用引用类型, 返回时根据实际场景, 能用引用尽量用引用.

5. 运算符重载

一般运算符重载

C++ 为了增强代码的可读性引入了运算符重载, 运算符重载是具有特殊函数名的函数, 也具有其返回值类型, 函数名字以及参数列表, 其返回值类型与参数列表与普通的函数类似.

内置对象可以直接使用各种运算符, 但是自定义类型是不可以的.

运算符重载主要是用在类的比较上面的, 类比较是不能直接用语言自带的运算符的.例如下面的 Date 类:

class Date
{
private:
    int _year = 1970;
    int _month = 1;
    int _day = 1;

public:
    // 构造函数
    Date()
    {

    }

    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
};

int main()
{
    Date d1;
    Date d2(2023, 1, 1);

    cout << (d1 > d2) << endl;

    return 0;
}

编译器会直接报错: 内置 > 运算符不支持类对象
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第16张图片


涉及到类对象相关运算符操作, 就需要自己写相关比较函数了.

当然, 不可以使用外部函数 Compare()...

  • 函数名不够见名知意, 谁也不知道这个函数究竟是用来干什么的.
  • 类的成员变量一般都是 private 的, 不可以直接进行访问, 还需要写 get 成员函数, 太麻烦

C++ 提供了运算符重载的语法
函数名字为: 关键字 operator 后面接需要重载的运算符符号
函数原型: 返回值类型 operator 操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符: 比如 operator@
  • 重载操作符必须有一个类类型参数(操作符至少有一个对象)
  • 用于内置类型的运算符, 其含义不能改变. 例如: 内置的整型+, 不能改变其含义.
  • 作为类成员函数重载时, 其形参看起来比操作数少1, 因为成员函数的第一个参数为隐藏的 this 指针
  • .* :: sizeof ?: .注意以上5个运算符不能重载

首先写一下比较两个日期是否相等的运算符重载
先在类外面尝试一下, 这里先将 Date 的成员变量设置成 public

bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
    && d1._month == d2._month
    && d1._day == d2._day;
}

int main()
{
    Date d1;
    Date d2(2023, 1, 1);
    Date d3(1970, 1, 1);

    // 可以写成函数调用的形式
    cout << operator==(d1, d2) << endl;

    // 最好写成正常运算符的形式, 编译器会自己转化成运算符重载函数
    cout << (d1 == d3) << endl;

    return 0;
}

程序运行结果如下:

在这里插入图片描述

使用运算符重载可以规避乱取名, 可以直接使用运算符进行比较, 提高了程序的可读性.


但是成员公有的话, 就不能保证封装性了, 可以直接将运算符重载包装入类中, 需要简单修改一下:

class Date
{
    //...

    // 包装入类中后 只有一个参数了
    bool operator==(const Date& d)
    {
        return _year == d._year
        && _month == d._month
        && _day == d._day;
    }
};

int main()
{
    Date d1;
    Date d2(2023, 1, 1);
    Date d3(1970, 1, 1);

    // 两种调用也都是可以的
    cout << d1.operator==(d2) << endl;
    cout << (d1 == d3) << endl;

    return 0;
}

程序没有问题:
在这里插入图片描述

  • 不同于之前写在类外的运算符重载, 包装在类中的运算符重载只有一个参数, 另一个参数就是指向调用该成员函数的对象的 this 指针. 这和构造函数等都是一样的.等同于operator=(Date* const this, const Date& d)

  • 同时, 调用可以使用 对象.成员函数 的格式, 但是为了维护抽象性, 推荐直接使用运算符来进行调用, 编译器会自己来处理.


接下来是 > 运算符重载的实现

class Date
{
    //...
    int getMonthDay(int year, int month)
    {
        int day[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31};

        // 如果是二月且是闰年
        if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
        {
            return 29;
        }

        return day[month];
    }

    bool operator>(const Date& d)
    {
        if (_year > d._year)
            return true;
        if (_month > d._month)
            return true;
        if (_day > d._month)
            return true;
        return false;
    }
};

int main()
{
    Date d1;
    Date d2(2023, 1, 1);
    Date d3(1970, 1, 1);

    cout << (d1 > d2) << endl; 
    cout << (d2 > d3) << endl;

    return 0;
}

程序运行正确:
在这里插入图片描述


运算符重载和函数重载没有关系

  • 函数重载: 可以允许参数不同的同名函数
  • 运算符重载: 自定义类型可以直接使用运算符

++= 的运算符重载

首先先实现 += 的运算符重载, 再用 += 复用写 + 的运算符重载

Date& Date::operator+=(int day)
{
  // 如果 day 是负数, 直接进行-=
  if (day < 0)
  {
    return (*this -= -day);
  }

  _day += day;
  // 如果日超出则进月份
  while(_day > getMonthDay(_year, _month))
  {
    _day -= getMonthDay(_year, _month);
    ++_month;
    // 如果月超出则进年份
    if (_month == 13)
    {
      ++_year;
      _month = 1;
    }
  }

  return *this;
}

Date Date::operator+(int day)
{
  Date temp(*this);
  temp += day;

  return temp;
}

+= 的实现需要注意的就是函数返回类型是 Date& 的, 这样可以省去一次的对象拷贝构造, 函数内部返回 *this 即可, 这里的 this 指针出函数作用域不会被销毁, 所以返回 *this 的引用是不会造成错误的.

重点需要研究的是 使用 += 复用 + 和 使用 + 复用 += 哪个效率更高.
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第17张图片

目前只是日期类, 拷贝不会消耗太多性能, 但是如果是拷贝一个有着一万个结点的树呢? 那肯定拷贝会消耗很多性能.

+= 复用创建 + 明显是效率更高的. 同时, 如果要将一个对象进行加操作, += 的效率肯定是要比 + 效率高的.

赋值运算符重载

赋值运算符重载格式

  • 参数类型: const T&, 传递引用可以提高传参效率
  • 返回值类型: T&, 返回引用可以提高返回的效率, 有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this: 要符合连续赋值的含义
Date& Date::operator=(const Date& d)
{
  // 如果赋值自己本身, 直接返回自身
  if (&d == this)
  {
    return *this;
  }

  _year = d._year;
  _month = d._month;
  _day = d._day;

  // *this就是类对象
  return *this;
}

赋值运算符重载和拷贝构造还是有点区别的, 赋值运算符重载使用的情况是: 已经有两个对象. 而拷贝构造则是: 只有一个已经存在的对象, 用该对象去拷贝初始化一个新对象.

赋值运算符只能重载成类的成员函数不能重载成全局函数

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第18张图片

如果我直接在类外定义赋值运算符重载, 编译器会直接报错: operator= 必须是非静态成员.

原因是: 如果赋值运算符不显示实现, 编译器会生成一个默认的. 此时用户再在类外自己实现一个全局的赋值运算符重载, 就和编译器在类中生成的默认赋值运算符重载冲突了, 故赋值运算符重载只能是类的成员的函数.

用户没有显式实现时, 编译器会生成一个默认赋值运算符重载, 以值的方式逐字节拷贝

对于 Date 类, 显然是没有必要进行显式的进行赋值运算符重载的. 但是对于需要开辟空间的类来说, 就需要自己显示实现赋值运算符重载, 具体内容涉及深拷贝, 之后会详细讲解.

前置 ++ 和 后置 ++ 重载

Date& Date::operator++()
{
  *this += 1;

  return *this;
}

Date Date::operator++(int)
{
  Date temp(*this);
  *this += 1;

  return temp;
}

C++ 规定 operator++(int) 是前置 ++; operator++(int) 是后置 ++. 这是语法涉及, 涉及到无法逻辑闭环的操作, 只能进行特殊处理了.

让它们构成函数重载, 这样编译器就能进行区分了.

可以看出, 前置 ++ 的效率更高, 少了两次拷贝构造.

流插入 流提取 的重载

如果需要打印对象, 我们不可以直接 cout << 对象;, 需要自己实现

首先要知道, coutcin 其实分别是 ostream 类和 istream 类的对象
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第19张图片

<<>> 分别接受两个参数, 左操作数必须是 ostream 或者 istream 的对象, 右操作数是要打印的对象.

学过了函数重载, 就可以很好理解为什么流插入和流读取可以自动识别类型了.
std 命名空间里, C++已经提供了常用的内置类型:
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第20张图片

但是自定义类型的流插入和流提取需要我们自己实现
首先先像之前一样, 直接写在类域里面:

ostream& Date::operator<<(ostream& out)
{
    out << _year << '-' << _month << '-' << _day << endl; 
	
	return out;
}

istream& Date::operator>>(istream& in)
{
    in >> _year >> _month >> _day;

	return in;
}

为了让 cout << 对象 后还能继续流插入, 需要返回 ostream 类型的引用

但是在main函数中, 却出现了问题:
C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第21张图片

正常方向写出现了错误, 但是反方向写却是正确的.

这里复习一下, 类中的成员函数, 第一个参数是一个隐含的 this 指针指向调用该函数的对象, 但是运算符重载第一个参数是左操作数, 第二个参数是右操作数.

这明显就反了, 为了将左右操作数顺序正确, 必须要将流插入和流提取的运算符重载放到全局中.

但这又会出现一个新的问题, private 的成员变量是不允许类外访问的

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第22张图片

为了解决类外访问的问题, 这里简单引入一下友元的概念

在类外定义的函数, 如果在类域中声明时在最前面添加 friend, 表示该函数可以访问类成员私有变量

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第23张图片

最终, 流插入流提取操作符重载实现完毕

在这里插入图片描述

可以看出, C++引入流的概念, 相比于 C语言使用 printf 打印对象, 要方便的多.


总结:

    1. 其他运算符一般是实现成成员函数
    1. 流运算符必须实现全局, 这样才能用流对象作为第一个参数

6. const 成员

之前写过 Date 类的成员函数 Print(), 用来打印对象的数据
但是这个函数是不安全的, 对象的内容可能会被修改, 正常的操作应该是将传入的参数前加上 const 修饰, 但是成员函数的 this 指针参数被隐藏了, 应该怎么办呢?

C++ 引入了可以对类成员函数修饰的 const

const 修饰的"成员函数"称之为 const 成员函数, const 修饰类成员函数, 实际修饰该成员函数隐含的 this 指针, 表明在该成员函数中不能对类的任何成员进行修改.

C++: 类和对象(中) (构造函数, 析构函数, 拷贝构造函数, 赋值重载, 取地址重载)_第24张图片


思考以下几个问题:

  1. const 对象可以调用非 const 成员函数吗?

    不可以. const 修饰的对象为只读类型, 若调用非 const 成员函数, 属于权限放大, 只读权限变为可读可写.

  2. const 对象可以调用 const 成员函数吗?

    可以. 权限缩小, 可读可写变为只读.

  3. const 成员函数内可以调用其它的非 const 成员函数吗?

    不可以. const 修饰的对象为只读类型, 若调用非 const 成员函数, 属于权限放大, 只读权限变为可读可写.

  4. const 成员函数内可以调用 其它的 const 成员函数吗?

    可以. 权限缩小, 可读可写变为只读.

成员函数的定义规则:

  • 能定义成 const 的成员函数都应该定义成 const, 这样 const 和非 const 对象都能调用
  • 要修改成员变量的成员函数不能定义成 const, const 对象不能调用, 非 const 对象可以调用

7.取地址及 const 取地址操作符重载

这两个默认成员函数一般不用定义, 编译器会自己生成

Date* Date::operator&()
{
    return this;
}

const Date* Date::operator&() const
{
    return this;
}

这两个运算符一般不需要重载, 使用编译器生成的默认取地址的重载即可, 只有特殊情况, 才需要重载, 比如想要别人获取到指定的内容

本章完.

你可能感兴趣的:(c++学习,c++)