【C++】类和对象2

本篇文章主要介绍类和对象中的重要内容,包括类的六大默认成员函数:构造函数、析构函数、拷贝构造函数、赋值运算符重载以及普通取地址运算符重载以及const 取地址运算符重载,主要是讲解前四个函数;在讲解完之后会实现一个日期类。


目录

1  类的默认成员函数 

2  构造函数

1) 构造函数完成的功能

2) 构造函数的特性

3) 构造函数的编写

4) 默认构造函数

5) 编译器自动生成的构造函数的行为

3  析构函数

1) 析构函数完成的功能

2) 析构函数的特性

 3) 析构函数的编写

4) 编译器自动生成的析构函数的行为

4  拷贝构造函数

1) 拷贝构造函数完成的功能

2) 拷贝构造函数的特性

3) 拷贝构造函数的编写 

4)  编译器自动生成的拷贝构造函数的行为

5  赋值运算符重载

1) 运算符重载

(1)  定义

(2)  特性

2) .* (成员指针访问运算符)

3) 赋值运算符重载

(1) 赋值运算符重载函数完成的功能

(2) 赋值运算符重载函数的特性

(3) 赋值运算符重载函数的编写 

(4) 默认生成的赋值运算符重载函数的行为

6  四大默认成员函数总结

7  取地址运算符函数重载

8  实现 Date 类

1) 成员变量与默认成员函数

2) Date 类要实现的功能

(1) 日期比较功能的实现

(2) +=、+、-=、- 功能的实现 

(3) 两个天数相隔天数的计算

(4) 前置++、后置++、前置--、后置-- 运算符重载

(5) >> 与 << 的重载

3)代码

9  总结


1  类的默认成员函数 

        类的默认成员函数就是指当我们在类中没有实现这个函数时,编译器会帮助我们自动在类中生成的成员函数称为默认成员函数。类中的默认成员函数具体分为以下几个:

需要重点学习的是前两种,也就是前四个默认成员函数,最后两种不需要重点学习,因为一般不需要自己实现。而在C++11之后,又增添了两个默认成员函数,分别是移动构造和移动赋值(学到C++11部分会进行讲解)。所以本篇文章主要进行前四个函数的讲解。


2  构造函数

1) 构造函数完成的功能

        构造函数虽然叫做构造函数,但是其完成的功能并不是为对象创造空间(在开辟函数栈帧时,空间已经开好),而是对创建的对象进行初始化,类似于之前初阶数据结构中的 Init 函数。构造函数的本质就是为了利用其创建对象能够自动调用的特性来完美替代 Init 函数。例如,你写了一个日期类,里面包含 year,month,day 三个整型成员变量,而你的构造函数规定了实例化对象时将年月日初始化为 1 年 1 月 1 日。在你实例化一个对象之后,不必调用任何函数,对象里面的 year,month,day 三个属性就会被赋值为1 1 1(不进行初始化的话里面为随机值)。


2) 构造函数的特性

  构造函数具有以下特性:

(1)函数名与类名相同

(2) 没有返回值

(3) 构造函数可以重载

(4) 对象实例化时会自动调用对应的构造函数

(5) 当我们没有显示的写出构造函数时,编译器会自动生成构造函数

        在上述四个特性中,前三个特性是规定构造函数的语法,而第四个和第五个特性是C++中类和对象所具有的特性。有了这两个特性,我们在进行对象的实例化之后,就不需要额外调用初始化函数 Init 了,编译器会自动帮我们调用构造函数,这就使得对象的创建变得更加方便。


3) 构造函数的编写

        借助构造函数特性中的前三条,我们就可以编写一个类的构造函数了:

#include<iostream>

using namespace std;

class Date
{
public:
    //实现日期类的构造函数
    //无需返回值
    Date()  //函数名与类名相同
    {
        //函数体为想要如何对对象进行初始化
        _year = 1;
        _month = 1;
        _day = 1;
    }

    int _year;
    int _month;
    int _day;
};

int main()
{
    //实例化对象时会调用日期类的构造函数
    Date d1;

    cout << d1._year << '-' << d1._month << '-' << d1._day << endl;

    return 0;
}

输出结果:

1-1-1

        当然构造函数可以进行重载,实例化对象时会调用对应的构造函数:

#include<iostream>

using namespace std;

class Date
{
public:
    //实现日期类的构造函数
    //无参构造
    Date()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

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

    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1;//调用无参构造

    cout << d1._year << '-' << d1._month << '-' << d1._day << endl;

    Date d2(2002, 1, 2);//调用需要传入参数的构造函数时,直接在实例化的对象后面传入参数
    
    cout << d2._year << '-' << d2._month << '-' << d2._day << endl;

    return 0;
}

输出结果:

1-1-1
2002-1-2

在上述代码中,在 Date 类里面重载了两个构造函数,一个是无参构造,一个是需要传入参数的构造函数。在创建 d1 与 d2 对象时,d1 没有传入参数,所以会去调用无参构造,而 d2 传入了三个参数,所以会去调用第二个构造函数(在调用需要传参的构造函数时,直接在实例化对象时在对象后面传入参数即可)。


4) 默认构造函数

        所谓默认构造函数,就是指不需要传入参数就可以直接调用的构造函数。一共包括三种:无参的构造函数、全缺省的构造函数以及我们不写编译器自动生成的构造函数,这三种构造函数都称为默认构造函数。通过这三个函数我们可以发现,默认构造函数只会有一个,因为如果显示写了前两种,编译器就不会自动生成构造函数了,而前两种构造函数虽然构成重载,但是都存在的话,当不传参时,编译器不知道去调用全缺省的函数还是无参的函数,就会报错。

#include<iostream>

using namespace std;

class Date1
{
public:
    //无参构造函数 -- 默认构造函数的一种
    Date1()
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

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

class Date2
{
public:
    //全缺省构造函数 -- 默认构造函数的一种
    Date2(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

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


class Date3
{
public:
    //没有显示写出构造函数,编译器会自动生成构造函数 -- 默认构造函数的一种
    void Print()
    {
        cout << _year << '-' << _month << '-' << _day << endl;
    }
private:
    int _year;
    int _month;
    int _day; 
};


int main()
{
    Date1 d1;
    d1.Print();
    
    Date2 d2;//全缺省构造函数可不传参
    d2.Print();
    
    Date2 d3(2023, 1, 1);//全缺省构造函数也可进行传参
    d3.Print();

    Date3 d4;//编译器会自动生成构造函数
    d4.Print();

    return 0;
}

输出结果:

1-1-1
1-1-1
2023-1-1
-858993460--858993460--858993460

可以看到编译器自动生成的构造函数,将 Date3 类中的三个成员变量初始化为了随机值,那么如果成员变量不是整形也会初始化为随机值吗?接下来我们就来讲解这个问题。


5) 编译器自动生成的构造函数的行为

        默认构造函数中有一种是我们不写编译器自动生成的构造函数,而前两种默认构造函数我们是可以自己控制的,但是编译器自动生成的构造函数行为我们是无法控制的,所以我们需要了解该默认构造函数会产生什么行为。

编译器自动生成的构造函数会有两种行为

(1) 如果成员变量为内置类型(整型,浮点型,指针等),是否进行初始化是不确定的,需要去看编译器(通过上述例子,可以看到vs是不做处理的,被初始化为了随机值)

(2) 对于自定义类型,会去调用该自定义类型的默认构造函数,如果没有默认构造函数,是会报错的

#include<iostream>

using namespace std;

//vs对于内置类型不做处理
class A1
{
public:
    void Print()
    {
        cout << _a1 << ' ' << _a2 << ' ' << _p1 << endl;
    }

private:
    int _a1;
    double _a2;
    int* _p1;
};

struct A2
{
    A2(int a1 = 1, int a2 = 2)
    {
        _a1 = a1;
        _a2 = a2;
    }

    int _a1;
    int _a2;
};

//对于自定义类型会去调用其默认构造函数
class B
{
public:
    void Print()
    {
        cout << _b1._a1 << ' ' << _b1._a2 << ' ' << _b2._a1 << ' ' << _b2._a2 << endl;
    }

private:
    A2 _b1;
    A2 _b2;
};

int main()
{
    A1 a1;
    a1.Print();

    B b1;
    b1.Print();

    return 0;
}

 输出结果:

-858993460 -9.25596e+61 CCCCCCCCCCCCCCCC
1 2 1 2
#include<iostream>

using namespace std;

//如果自定义类型没有默认构造函数,会报错
class A
{
public:
    A(int a1, int a2)
    {
        _a1 = a1;
        _a2 = a2;
    }

    void Print()
    {
        cout << _a1 << ' ' << _a2 << endl;
    }

private:
    int _a1;
    int _a2;
};

class B
{
public:
    void Print()
    {
        _b1.Print();
        _b2.Print();
    }

private:
    A _b1;
    A _b2;
};

int main()
{
    B b1;
    b1.Print();

    return 0;
}

运行结果:

这里 B 类里面有两个自定义类型 A 的两个成员变量,没有显示写出构造函数, 所以编译器自动生成的构造函数会去调用 A 类中的默认构造函数,但是 A 类中没有默认构造函数,所以是会发生报错的。


3  析构函数

1) 析构函数完成的功能

        析构函数完成的功能正好与构造函数相反。析构函数并不是完成对象的销毁工作的,而是完成对对象资源的清理工作,类似于初阶数据结构中的 Destroy 函数实现的功能。因为如果动态开辟了内存空间而不进行释放的话,是会造成内存泄漏现象的。而程序员在开辟了内存空间之后,总是容易忘记释放内存空间;析构函数就是为了解决此类问题而存在的,借助析构函数会自动调用的特性,就能借助编译器来避免内存泄漏现象。


2) 析构函数的特性

析构函数具有以下特性

(1) 析构函数函数名为类名前加上字符 ~(取反符号),如:~Date

(2) 析构函数不需要写参数与返回值

(3) 一个类只能有一个析构函数。若没有显示写出析构函数,编译器会自动生成析构函数

(4) 对象生命周期结束时会自动调用析构函数

(5) 如果有局部域内有多个对象,C++定义后创建的先析构

同样类比构造函数,析构函数特性中的前三条是如何编写析构函数,最后一条是析构函数本身所具有的特性,有了第四条使得资源的申请与释放工作会更加方便。


 3) 析构函数的编写

         借助析构函数特性的前三条就可以编写一个类的析构函数了:

#include<iostream>

using namespace std;

class Stack
{
public:
    Stack(int n = 4)
    {
        cout << "Stack(int n = 4)" << ':' << n << endl;
        _arr = (int*)malloc(sizeof(int) * n);
        if (_arr == nullptr)
        {
            perror("malloc fail!\n");
            exit(1);
        }
        _size = n;
        _top = 0;
    }

    //写出Stack类的析构函数
    //函数名为 “ ~ + 类名 ”
    //不需要参数与返回值  
    ~Stack()
    {
        //释放资源
        cout << "~Stack()" << endl;
        free(_arr);
        _arr = nullptr;
        _top = _size = 0;
    }

private:
    int* _arr;
    size_t _top;
    size_t _size;
};

int main()
{
    Stack s1;
    
    return 0;
}

输出结果:

Stack(int n = 4):4
~Stack()

通过该运行结果可以看到在实例化对象时确实会自动调用类的构造函数;而在对象生命周期结束时,会自动调用类的析构函数。

接下来我们来验证一下是否是后创建的对象先析构,看以下这段代码:

#include<iostream>

using namespace std;

class A
{
public:
    A(int a1 = 1, int a2 = 2)
    {
        _a1 = a1;
        _a2 = a2;
    }

    ~A()
    {
        cout << "~A():" << _a1 << ' ' << _a2 << endl;
    }
private:
    int _a1;
    int _a2;
};

int main()
{
    A a1;
    A a2(2, 3);

    return 0;
}

输出结果:

~A():2 3
~A():1 2

 可以看到确实是后创建的先析构。


4) 编译器自动生成的析构函数的行为

        与构造函数类似,如果我们不显示写出析构函数,编译器同样会自动生成一个析构函数,我们无法控制其行为,所以我们需要了解编译器自动生成的析构函数会产生怎么样的行为:

对于内置类型不做处理,对于自定义类型会去调用其类的析构函数

可以看到编译器自动生成的析构函数与构造函数行为是相同的,但是需要注意的一点是:尽管显示写出了析构函数,自定义成员依旧会去调用其类的析构函数,也就是不管什么情况下,自定义成员一定会去调用自己的析构函数,这样是为了保证自定义成员也不会内存泄漏情况。

#include<iostream>

using namespace std;

class A1
{
public:
    //构造函数
    A1(int a1 = 1, int a2 = 2)
    {
        _a1 = a1;
        _a2 = a2;
    }

    //不显示写出析构函数,编译器会自动生成。对于内置类型不做处理
private:
    int _a1;
    int _a2;
};

class A2
{
public:
    //构造函数
    A2(int a1 = 1, int a2 = 2)
    {
        _a1 = a1;
        _a2 = a2;
    }
    //析构函数
    ~A2()
    {
        cout << "~A2()" << endl;
    }
private:
    int _a1;
    int _a2;
};

class B1
{
public:
    //构造函数
    B1(A2 b1, A2 b2)
    {
        _b1 = b1;
        _b2 = b2;
    }
    //没有显示写出析构函数,自定义成员会去调用其析构函数
private:
    A2 _b1;
    A2 _b2;
};

class B2
{
public:
    //构造函数
    B2(A2 b1, A2 b2)
    {
        _b1 = b1;
        _b2 = b2;
    }
    //即使显示写出析构函数,自定义成员也会去调用其析构函数
    ~B2()
    {
        cout << "~B2()" << endl;
    }

private:
    A2 _b1;
    A2 _b2;
};
        

int main()
{
    A2 a1;
    A2 a2;
    B1 b1(a1, a2);
    B2 b2(a1, a2);

    return 0;
}

输出结果:

~A2()
~A2()
~A2()
~A2()
~B2()
~A2()
~A2()
~A2()
~A2()
~A2()
~A2()

可以看到这里是 b1 和 b2 析构时都去调用了 A2 的析构函数,至于为什么会调用这么多次 A2 的析构函数,等讲解玩下面的拷贝构造函数就可以理解了。


4  拷贝构造函数

1) 拷贝构造函数完成的功能

        拷贝构造函数呢,主要是用来完成对一个对象的深拷贝(就是在拷贝数据的同时,连同资源一起拷贝),那么为什么要完成对一个对象的深拷贝呢?如一下两个例子,完成对一个顺序表的拷贝:

顺序表中有三个成员,一个为整型指针变量 arr,另外两个为整型变量 size 和 capacity,分别表示有效元素个数和容量,然后 arr 指向了一个动态开辟的数组来存储了1-8八个数字。

如果只对顺序表中的元素进行浅拷贝(值拷贝),那就会出现以下情况:


 

其中 sqlist1 为原来的顺序表,sqlist2 为进行浅拷贝后的顺序表,由于浅拷贝只是进行值拷贝,那么 sqlist1 中 arr 指针变量和 sqlist2 中的 arr 指针变量内容就为同一个地址,也就指向了同一块地址空间,那么释放的时候该地址空间就会被释放两次,从而报错。

所以对一个顺序表进行拷贝,就应该进行深拷贝,将 sqlist1 中的数据连用开辟的内存空间一起拷贝,使得 sqlist2 中的 arr 也指向一块再次开辟后的内存空间,达成互不干扰,就不会出现释放两次的问题了:
         这就是拷贝构造函数所要解决的问题,就是为了完成对一个对象的深拷贝,使得两个对象能够独立使用各自的资源,互不干扰,不然很容易就会出现释放两次的问题。


2) 拷贝构造函数的特性

拷贝构造函数具有以下特性

(1) 拷贝构造函数为一个构造函数的一个重载,是一个特殊的构造函数,其参数只有一个且必须为类类型的引用,不能为传值方式,否则直接报错

(2) 自定义类型对象进行拷贝行为时必须调用拷贝构造函数,也就是自定义类型对象进行传值传参和传值返回时都必须调用拷贝构造函数

这两个特性里,第一条为拷贝构造函数的编写原则,第二条为拷贝构造函数所具有的独特特性。


3) 拷贝构造函数的编写 

        既然拷贝构造函数为构造函数的重载,所以其编写原则与构造函数是相同的,只需要注意其参数为类类型的引用就可以了:

#include<iostream>

using namespace std;

class Date
{
public:
    //构造函数
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //拷贝构造函数
    //函数名与类名相同,不需要返回值
    //参数为类类型的引用
    //Date(Date d) 参数为传值直接报错
    Date(const Date& d)
    {
        cout << "Date(Date& d)" << endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2025, 4, 1);
    //会调用拷贝构造函数
    Date d2(d1);
    //这个是拷贝构造,不是赋值
    Date d3 = d1;

    return 0;
}

输出结果:

Date(Date& d)
Date(Date& d)

这里有一点需要注意,就是"Date d3 = d1"这个语句,虽然中间是个赋值符号,但是这个语句确是用 d1 这个对象来拷贝构造 d3 这个对象,因为 d3 是还未实例化的对象。另外,在编写拷贝构造函数的参数的时候,最好写成 const 引用,因为写成 const 引用,不仅可以对普通对象进行拷贝构造,也可以对 const 对象进行拷贝构造;如果写成普通引用,就只能对普通对象进行拷贝构造了(const 对象无法传参,因为参数为 const 引用,传递参数为普通对象,涉及到了权限放大的问题)。

        对于自定义类型传值传参会去调用其拷贝构造函数,咱们来看这个例子:

#include<iostream>

using namespace std;

class Date
{
public:
    //构造函数
    Date(int year = 1, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //拷贝构造函数
    //函数名与类名相同,不需要返回值
    //参数为类类型的引用
    //Date(Date d) 参数为传值直接报错
    Date(const Date& d)
    {
        cout << "Date(Date& d)" << endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

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

private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2025, 1, 1);
    Date d2(2025, 2, 1);
    d2.Print(d2);

    return 0;
}

输出结果:

Date(Date& d)
2025-2-1

上述代码中 Date 类里面,写了一个 Print 函数,该函数需要传值传参传入一个 Date 类对象,在 main 函数中利用 d1 对象调用了 Print 函数(Print函数中隐含一个 this 指针参数,所以需要通过 Date 类的对象来调用 Print 函数),传递参数为 d2 对象,可以看到传值传参确实调用了 Date 类的拷贝构造函数。这里为什么会调用拷贝构造函数呢?原因就是因为传值传参参数 d 为传入参数 d2 的拷贝,所以需要通过拷贝构造函数来拷贝构造参数 d。

        理解了传值传参会调用拷贝构造,我们就能理解为什么拷贝构造函数参数必须为对象的引用了,因为如果为传值传参,那么调用第一次拷贝构造函数之前,就需要调用第二次拷贝构造函数来拷贝构造第一个拷贝构造函数的参数,而第二次拷贝构造函数的参数就需要调用第三次拷贝构造函数来构造,这样就会在语法上发生函数的无穷递归:


4)  编译器自动生成的拷贝构造函数的行为

        拷贝构造函数也是六大默认成员函数之一,所以与构造函数和析构函数一样,拷贝构造函数我们不写,编译器也会自动生成,我们也需要了解自己生成的拷贝构造函数会产生什么行为:

对于内置类型会进行值拷贝(浅拷贝,一个字节一个字节的拷贝),对于自定义类型会去调用其拷贝构造函数

所以什么时候我们需要自己实现拷贝构造函数呢?既然编译器自动生成的拷贝构造函数会对内置类型进行浅拷贝,所以如果类的成员变量全部为内置类型且没有申请资源的时候,就不需要我们写拷贝构造函数;当成员变量中有指针变量并且该指针变量指向了申请的资源时,就需要我们自己实现拷贝构造函数来完成深拷贝,否则两个对象里的指针变量进行浅拷贝就会指向同一块空间,不仅会析构两次,并且会相互影响。另外,如果类里面的成员变量全部为自定义类型,并且这些类全部实现了拷贝构造函数,这种情况下我们也是不需要自己实现拷贝构造函数的,编译器会自动的帮我们调用这些类的拷贝构造函数。

#include<iostream>

using namespace std;

class Stack
{
public:
    //构造函数
    Stack(int n = 4)
    {
        _arr = (int*)malloc(sizeof(int) * n);
        if (_arr == nullptr)
        {
            perror("malloc fail!\n");
            exit(1);
        }
        _top = 0;
        _capacity = n;
    }

    //拷贝构造
    Stack(const Stack& st)
    {
        cout << "Stack(const Stack& st)" << endl;
        _arr = (int*)malloc(sizeof(int) * st._capacity);
        if (_arr == nullptr)
        {
            perror("malloc fail!\n");
            exit(2);
        }
        
        //将st中的数据拷贝到新的栈中
        memcpy(_arr, st._arr, sizeof(int) * st._top);

        _top = st._top;
        _capacity = st._capacity;
    }

    //析构函数
    ~Stack()
    {
        if (_arr)
        {
            free(_arr);
            _arr = nullptr;
        }
        _top = _capacity = 0;
    }

   
private:
    int* _arr;
    size_t _top;
    size_t _capacity;
};

class MyQueue
{
public:
private:
    Stack _st1;
    Stack _st2;
};

class Ptr
{
public:
    Ptr(int* ptr = nullptr)
    {
        _ptr = ptr;
    }

    void Print()
    {
        cout << _ptr << endl;
    }
private:
    int* _ptr;
};

int main()
{
    MyQueue q1;
    //MyQueue对象的拷贝构造,没有显示写出拷贝构造,会去调用Stack类的拷贝构造函数
    MyQueue q2 = q1;

    int a = 10;
    Ptr p1(&a);
    //编译器自动生成的拷贝构造函数,会对内置类型完成浅拷贝
    Ptr p2 = p1;
    p1.Print();
    p2.Print();

    return 0;
}

输出结果:

Stack(const Stack& st)
Stack(const Stack& st)
000000DAD90FF994
000000DAD90FF994

5  赋值运算符重载

1) 运算符重载

(1)  定义

        当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。当一个类里面重载了这个运算符,该类类型的对象使用该运算符时,就可以与该类类型相关的特殊功能,这就叫做运算符重载。比如 '+' 这个运算符,本来应该是一个整数加上一个整数,而如果在日期类中重载了 '+' 这个运算符,使其功能变为一个日期加上一个天数后,返回另一个日期,那么 '+' 这个运算符就在日期类中被赋予了新的功能。

(2)  特性

        运算符重载具有以下特性:

a. 运算符重载是具有特殊名字的函数,其函数名由 operator 关键字和所要重载的运算符组成,如:operator+。运算符重载也要有参数和返回类型

b. C++规定类类型对象使用运算符时,必须转换成调用对应的运算符重载,如果没有,会直接报错

c. 重载一元运算符要有一个参数,重载二元运算符要有两个参数,且左边对象传给第一个参数,右边对象传给第二个参数

d. 如果一个运算符重载函数为成员函数,则其参数会比普通运算符重载函数少一个,因为成员函数第一个参数为隐含的 this 指针

e. 运算符重载之后,优先级与结合性要与对应的内置类型运算符保持一致

f. 不能重载语法中没有的运算符,比如:operator@

g. 有 5 个运算符是不能重载的:.*    :: (域作用限定符)  sizeof(计算类型所占字节大小)   ?:(三目操作符)   .(成员访问运算符)

h. 重载运算符至少有一个类类型参数,不能通过重载改变内置类型对象的含义。如:int operator+(int x, int y) 这个是不允许的

i.  ++ 运算符有两种类型,第一种为前置++,第二种为后置++,为了区分两种 ++ 类型,规定在重载后置++运算符时,增加一个 int 形参,与前置++重载函数构成函数重载

运算符重载的特性较多, 所以初学可能比较复杂,不过后来写的代码越来越多,运算符重载也就没有这么复杂了,只需要记住最关键的一点:对象在使用运算符时必须对运算符进行重载,且要重载对于该类类型有意义的运算符。这里就不展示运算符重载具体的例子了,在下面的赋值运算符重载与 Date 类的实现中会有具体的例子,这里主要讲一个 C++ 中新增的一个运算符 .* 运算符。


2) .* (成员指针访问运算符)

        '.*' 运算符是由于 C++ 中出现了类的概念,从而出现的一个新的运算符,其作用主要是用来对象调用成员函数指针的:
 

#include<iostream>

using namespace std;

class A
{
public:
    void Func()
    {
        cout << "Func()" << endl;
    }
};

//在 typedef 对成员函数指针重新取名时,成员函数需要加上域作用限定符
typedef void(A::*PF)();

int main()
{
    //C++规定取成员函数的地址时,需要加上&运算符
    PF pf = &A::Func;

    A a;

    //对象通过成员函数指针调用函数时,需要用到 '.*' 运算符
    (a.*pf)();
    return 0;
}

输出结果:

Func()

在上述代码中,需要有几个注意的点:
(1) 在用 typedef 对成员函数指针类型重新取名时,需要在 * 前面说明类域

(2) C++ 中规定在取成员函数地址时,不仅需要说明类域,还要加上 & 运算符;而普通函数的函数名就是该函数的地址,也不需要说明类域

(3) 在对象通过成员函数指针调用函数时,需要用到 '.*' 运算符,. 的左边为类类型的对象,右边为成员函数指针。还记得调用利用函数指针调用普通函数吗,可以是 pf(),也可以是 (*pf)()(pf为普通函数指针),这里就不需要用到对象。利用函数指针调用成员函数与普通函数不一样本质还在于隐含的 this 指针,因为成员函数是有一个 this 指针参数的,但又不可以显示调用,所以需要加上对象来传递 this 指针参数。


3) 赋值运算符重载

(1) 赋值运算符重载函数完成的功能

        赋值运算符重载主要用于已经存在的对象的直接拷贝赋值,赋值运算符重载函数与拷贝构造函数很像,但要注意对他们两个进行区分,拷贝构造函数是用已经存在的对象来初始化一个未存在的对象,赋值运算符重载函数是用于两个已经存在的对象直接拷贝赋值。所以赋值运算符的功能就类似于拷贝构造函数,主要用于深拷贝的场景。就比如有两个顺序表 sqlist1 和 sqlist2 ,每个顺序表里面都有三个成员,分别是 int*  _arr,size_t  _size 和 size_t  _capacity,如果仅仅是进行浅拷贝赋值,那么两个顺序表里的 _arr 指针就会指向同一块空间,不仅会析构两次,而且会相互影响。


(2) 赋值运算符重载函数的特性

  赋值运算符重载函数具有以下特性

a.  赋值运算符重载函数为一个运算符重载,且规定必须为成员函数,参数建议写成类类型的 const 引用,否则会有拷贝,降低效率

b.  返回值建议写成类类型的引用,引用返回可以提高效率,而且有返回值也可以实现连续赋值

这两条特性主要是指导我们来编写赋值运算符重载函数的。


(3) 赋值运算符重载函数的编写 

        根据上面两条,我们就可以写出一个类的赋值运算符重载函数:

#include<iostream>

using namespace std;

class Date
{
public:
    //构造函数
    Date(int year = 2025, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //拷贝构造
    Date(const Date& d)
    {
        cout << "Date(const Date& d)" << endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    //赋值运算符重载
    //引用传参与引用返回可以提高效率
    Date& operator=(const Date& d)
    {
        cout << "Date& operator=(const Date& d)" << endl;
        //判断,防止自己给自己赋值
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this;
    }

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

private:
    int _year;
    int _month;
    int _day;
};



int main()
{
    Date d1;
    d1.Print();
    //拷贝构造
    Date d2(d1);

    Date d3(2025, 7, 7);
    //赋值重载
    d1 = d3;
    d1.Print();
    //这里为拷贝构造,不是拷贝赋值
    Date d4 = d2;

    return 0;
}

输出结果:

2025-1-1
Date(const Date& d)
Date& operator=(const Date& d)
2025-7-7
Date(const Date& d)

可以最后一行代码确实为拷贝构造,而不是拷贝赋值。


(4) 默认生成的赋值运算符重载函数的行为

        赋值运算符重载函数也是一个默认成员函数,所以没有显示写出的时候,编译器也会自动生成赋值运算符重载函数:

编译器自动生成的赋值运算符重载函数会对内置类型进行浅拷贝,对于自定义类型会去调用他的拷贝赋值运算符重载函数

#include<iostream>

using namespace std;

class Date
{
public:
    //构造函数
    Date(int year = 2025, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    //拷贝构造
    Date(const Date& d)
    {
        cout << "Date(const Date& d)" << endl;
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    //赋值运算符重载
    //引用传参与引用返回可以提高效率
    Date& operator=(const Date& d)
    {
        cout << "Date& operator=(const Date& d)" << endl;
        //判断,防止自己给自己赋值
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this;
    }

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

private:
    int _year;
    int _month;
    int _day;
};

class A
{
    Date _d1;
    Date _d2;
};



int main()
{
    A a1;
    A a2;
    //编译器自动生成的赋值运算符重载函数会去调用自定义类型的赋值运算符重载函数
    a2 = a1;

    return 0;
}

输出结果:

Date& operator=(const Date& d)
Date& operator=(const Date& d)

6  四大默认成员函数总结

        四大默认成员函数包括构造函数,析构函数,拷贝构造函数与赋值运算符重载函数。前两个主要用来实现对象的初始化与销毁,编译器自动生成的构造函数和析构函数行为很像,都是对于内置类型不做处理,对于自定义类型会去调用他的构造函数和析构函数。对于拷贝构造函数与赋值运算符重载函数,都是用来实现对于一个对象的拷贝,只不过一个是初始化对象,一个是对已存在的对象进行赋值,编译器自动生成的这两个函数的行为也很像,对于内置类型会进行浅拷贝,对于自定义类型会去调用他的拷贝构造函数和赋值运算符重载函数。

        那么什么时候需要我们去显示实现这几个函数呢?大部分情况下构造函数是需要我们显示实现的,而另外三个函数需要看情况。当成员变量全部为内置类型时,如 Date 类,他就不需要显示实现这三个函数;而对于申请了额外资源的函数,比如 Stack 类,就需要我们来显示实现这三个函数;而对于成员变量中全是自定义类型或者有一部分为内置类型,并且没有申请额外资源,且自定义类型中实现了上述三个函数,这时候我们也是不需要实现这三个函数,因为编译器会帮助我们自动调用。这里有一个小技巧,就是如果没有显示写出析构函数,就不需要显示写出拷贝构造和赋值运算符重载;如果显示写了析构函数,就需要写那两个函数。


7  取地址运算符函数重载

        取地址运算符函数重载包括两个,第一个是普通取地址运算符重载和 const 取地址运算符重载,一般这两个函数是不需要显示实现的,编译器自动生成的就可以。但是如果想要实现特别的功能,也是可以自己显示实现的:

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

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

8  实现 Date 类

1) 成员变量与默认成员函数

        实现一个类首先我们要先想他的成员变量,既然是日期类,那成员变量就很简单了,只需要三个整型变量就可以了,分别是年、月、日:

int _year;
int _month;
int _day;

既然成员变量都为整型变量,也没有开辟资源,所以析构函数、拷贝构造函数以及赋值运算符重载函数就不需要我们实现了,只需要实现构造函数就可以了,这里我们选择实现一个全缺省的构造函数,既可以传参,也可以不传参(缺省值自己随便取就可以):

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

2) Date 类要实现的功能

        Date 类要实现的功能包括实现两个日期的比较,包括 ">、<、>=、<=、==、!=" ;还包括一个日期 += 一个天数,返回加上天数后的日期以及一个日期 -= 一个天数后的日期;还包括一个日期与一个日期之间相隔多少天;最后就是实现一个日期的前置++,后置++,前置-- 以及后置 --。

(1) 日期比较功能的实现

        实现日期比较功能,这里是通过运算符重载语法来实现的,即在日期类中重载 6 个比较运算符,当左右操作数为日期类对象的时候,就可以赋予比较运算符新的功能。在实现一个类的比较功能的时候,这里有一个技巧,就是只需要重载其中两个比较运算符,别的比较运算符就可以复用这两个运算符,就不用重载 6 个运算符了。这里我们选择重载 "==" 和 "<" 两个运算符,别的运算符复用这两个运算符。

"==" 运算符的重载:只需要比较两个 Date 类对象的年月日是否分别相等就可以了,即 _year == d._year && _month == d._month && _day == d._day(d 为要比较的另一个 Date 类对象)。

" < " 运算符的重载:我们先来想两个日期平常是如何比较大小的,即年小就小,年不小月小就小,月不小日小就小,否则就是不小,按照这个逻辑我们就可以写出 "<" 的重载函数了(具体代码详见下面代码部分)。

剩下运算符的重载复用上述两个运算符就可以了,比如 "!=" 的重载就是 !(*this == d)。

(2) +=、+、-=、- 功能的实现 

        这里依然是采用运算符重载的语法来实现四个功能。为了实现这四个功能,我们可以写一个 GetMonthDay 的函数来辅助实现这一功能,因为如果要实现一个日期 += 或者 -= 一个天数后变为相应的日期,需要多次获得每月的天数,下面是 GetMonthDay 函数的代码:

int GetMonthDay(int year, int month)
{
    assert(month > 0 && month < 13);
        
    static int MonthDay[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    //如果是闰年,并且是二月,那就返回29
    if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
        return 29;
    else
        return MonthDay[month];
}

"+=" 运算符的重载:其实就是模拟一个日期加上一个天数的运算过程

首先我们先让 _day 加上要 += 的天数(这里设为 day),然后进入循环,如果 _day > GetMonthDay(_year, _month),那就让 _day 减去该月天数,然后再 ++_month,直到 _month 变为 13,然后 ++_year,再让 _month 变为 1,直到循环结束为止。

"-=" 运算符的重载:-= 运算符实现逻辑跟 += 运算符逻辑差不多,只不过需要逆向思维一下,大家可以自己想一下,模拟一下一个日期是如何减去一个天数的,如果思考不出来可以看下面的代码,这里就不过多解释了。

实现完这两个运算符之后,还有一个点需要注意,如果 +=上一个负数,实际上就是 -= 上这个天数的相反数;相应的,如果 -= 上一个负数,实际上就是 += 上这个天数的相反数。

"+" 运算符的重载:+ 运算符我们可以通过复用 += 运算符来实现,只需要拷贝构造 *this 一个 Date 类对象,然后返回该对象 += 天数后的拷贝即可。

“-” 运算符的重载:- 运算符我们可以通过复用 -= 运算符实现,跟 + 运算符复用 += 运算符是一样的。

        在重载这四个运算符的时候需要注意 "+=、-=" 运算符重载的返回值为 Date&,而 "+、-" 运算符返回值为 Date ,因为 += 与 -= 运算符返回的是 *this,出了 += 、-= 运算符重载函数后,原对象并不会被销毁,仍存在于原来的作用域之中,所以可以引用返回;而 +、- 运算符返回的是在该函数内创建对象的拷贝,除了作用域在局部域内创建的对象会被销毁,所以不能引用返回。

(3) 两个天数相隔天数的计算

        这个功能的实现很简单,我们只需要计算由小天数加到与大天数相等加了多少天就可以实现这个功能了。

(4) 前置++、后置++、前置--、后置-- 运算符重载

        前置++,后置++ 的实现也是复用了 += 运算符,只不过在实现后置++ 时,需要先利用 *this 拷贝构造一个 d 对象,然后让 *this += 1,最后返回 d 对象,所以这里前置++ 返回值为Date&,后置++ 返回值为 Date。

        前置--,后置-- 的实现依然复用 -= 运算符,逻辑与 ++ 运算符几乎相同,这里就不再赘述。

(5) >> 与 << 的重载

        我们会在 Date 类中实现一个 Print 函数,但是每次都用 Print 函数未免太麻烦了一点,而且如果想要从键盘中输入日期,也是没法实现的,所以我们这里重载两个运算符 >> 和 << 来实现对日期类对象的输入和输出。

        要想实现这两个运算符的重载,需要用到之前的 iostream 的知识,iostream 中包含两个类,一个是 ostream,cout 就是该类的一个对象;另一个是 istream,cin 是该类的一个对象。所以如果想要重载这两个运算符,就需要用到这两个类。但是如果写到类里面,我们就会面临一个问题,因为成员函数的第一个参数始终都是一个隐含的 this 指针,而在运算符重载时,第一个参数为左操作数,所以如果把 << 重载函数写在​类里面,必须这样调用:

Date d(2020, 1, 1);
d << cout;

这样调用会显得很别扭,所以我们选择将 >> 与 << 重载函数写在类外面,这样就可以控制参数了,但是这样又会面临一个问题,那就是在重载的时候,必然会用到 Date 类的私有成员变量 _year、_month、 _day,但是私有成员变量无法在类外进行访问。遇到这种情况,我们有 3 中解决办法:
a.  在类中提供 GetXXX 函数,这样调用该函数就可以得到成员变量了

b.  友元函数

c.  成员变量放在公有

这里我们选择友元的方式,友元的语法很简单,即要在友元的函数名前面加上一个 friend 就可以了,比如:

class Date
{
    friend void Print(const Date& d);
private:
    int _year;
    int _month;
    int _day;
};

void Print(const Date& d)
{
    cout << d._year << '-' << d._month << '-' << d._day << endl;
}

友元的作用在于使得友元的函数成为该类的一个朋友,这样函数就可以访问类中的私有成员了。

        有了以上知识之后,我们就可以写出该 >> 与 << 重载函数了:

ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << '-' << d._month << '-' << d._day << endl;
    return out;
}

istream& operator>>(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}

这里的返回值是为了能够实现连续输出与输入。


3)代码

//Date.h 头文件
#pragma once
#include<iostream>
#include<assert.h>

using namespace std;

//这里采取声明与定义相分离的形式
class Date
{
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
public:
    Date(int year = 1990, int month = 1, int day = 1);//缺省值只能在声明时给
    //这里不需要显示实现拷贝构造,赋值运算符重载以及析构函数,因为编译器自动生成的函数就够用了
    void Print() const;
    
    bool CheckDate();

    //直接将该函数定义在类里面,该函数默认为 inline 函数
    //频繁调用效率会变高
    int GetMonthDay(int year, int month)
    {
        assert(month > 0 && month < 13);
        
        static int MonthDay[13] = {-1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
        //如果是闰年,并且是二月,那就返回29
        if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
            return 29;
        else
            return MonthDay[month];
    }

    bool operator<(const Date& d) const;
    bool operator<=(const Date& d) const;
    bool operator>(const Date& d) const;
    bool operator>=(const Date& d) const;
    bool operator==(const Date& d) const;
    bool operator!=(const Date& d) const;

    //日期 += 天数
    Date& operator+=(int day);
    Date operator+(int day) const;

    //日期 -= 天数
    Date& operator-=(int day);
    Date operator-(int day) const;
    
    //日期 - 日期算出相隔天数
    int operator-(const Date& d) const;

    //前置++
    Date& operator++();
    //后置++,在参数位置增加一个 int 形参
    Date operator++(int);

    //前置--
    Date& operator--();
    //后置--,增加一个int形参来区别前置--
    Date operator--(int);

private:
    int _year;
    int _month;
    int _day;
};


//Date.cpp文件
#include"Date.h"

bool Date::CheckDate()
{
    if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month))
        return false;
    else
        return true;
}

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

    if (!CheckDate())
    {
        cout << "日期非法" << endl;
        assert(0);
    }
}


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

//实现两个比较符号,其他的进行复用即可
bool Date::operator<(const Date& d) const
{
    if (_year < d._year)
        return true;
    else if (_month < d._month)
        return true;
    else if (_day < d._day)
        return true;
    else
        return false;
}

bool Date::operator<=(const Date& d) const
{
    return ((*this < d) || (*this == d));
}
 
bool Date::operator>(const Date& d) const
{
    return !(*this <= d);
}

bool Date::operator>=(const Date& d) const
{
    return !(*this < d);
}

bool Date::operator==(const Date& d) const
{
    return (_year == d._year && _month == d._month && _day == d._day);
}

bool Date::operator!=(const Date& d) const
{
    return !(*this == d);
}

Date& Date::operator+=(int day)
{
    //如果day < 0,就相当于减去正的day
    if (day < 0)
    {
        *this -= (-day);
        return *this;
    }

    _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) const
{
    //利用*this拷贝构造一个d对象
    Date d(*this);
    //复用+=运算符
    d += day;
    return d;
}

Date& Date::operator-=(int day)
{
    //如果day < 0,就相当于加上正的day
    if (day < 0)
    {
        *this += (-day);
        return *this;
    }

    _day -= day;
    while (_day <= 0)
    {
        --_month;
        if (_month == 0)
        {
            --_year;
            _month = 12;
        }
        _day += GetMonthDay(_year, _month);
    }
    
    return *this;
}
        
Date Date::operator-(int day) const
{
    //复用-=运算符
    Date d(*this);
    d -= day;
    return d;
}

int Date::operator-(const Date& d) const
{
    int day = 0;
    //求出较小的日期与较大的日期
    Date mind = *this;
    Date maxd = d;
    if (*this > d)
    {
        maxd = *this;
        mind = d;
    }
    
    //计算出由较小的日期加到较大的日期需要多少天
    while (mind != maxd)
    {
        ++day;
        ++mind;
    }

    return day;
}

Date& Date::operator++()
{
    //复用 += 运算符
    *this += 1;
    return *this;
}

Date Date::operator++(int)
{
    Date d(*this);
    *this += 1;
    return d;
}

Date& Date::operator--()
{
    //复用 -= 运算符
    *this -= 1;
    return *this;
}

Date Date::operator--(int)
{
    Date d(*this);
    *this -= 1;
    return d;
}

//重载 << 与 >> 操作符
ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << '-' << d._month << '-' << d._day << endl;
    return out;
}

istream& operator>>(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}

测试代码:

#include"Date.h"

//测试大小比较符号
void Date_Test01()
{
	Date d1(2025, 1, 31);
	Date d2(2025, 3, 29);
	cout << (d1 < d2) << endl;
	cout << (d1 <= d2) << endl;
	cout << (d1 > d2) << endl;
	cout << (d1 >= d2) << endl;
	cout << (d1 == d2) << endl;
	cout << (d1 != d2) << endl;
}

//测试+=,+,-=,-
void Date_Test02()
{
	Date d1(2025, 1, 31);
	Date d2(d1);
	d1 += 10001;
	d1.Print();
	Date d3 = d2 + 10001;
	d3.Print();
	d1 -= 10000;
	d1.Print();
	Date d4 = d3 - 10000;
	d4.Print();

	//测试一下加上一个负数与减去一个负数
	Date d5(2070, 10, 10);
	d5 += -10001;
	d5.Print();
	d5 -= -10001;
	d5.Print();
}

//测试两个日期相隔多少天以及自增与自减
void Date_Test03()
{
	Date d1(2002, 10, 12);
	Date d2(2005, 2, 26);
	cout << (d1 - d2) << endl;

	++d1;
	d1.Print();
	Date d3 = d1++;
	d1.Print();
	d3.Print();
	--d2;
	d2.Print();
	Date d4 = d2--;
	d2.Print();
	d4.Print();
}


int main()
{
	//Date_Test01();
	//Date_Test02();
	//Date_Test03();

    //测试日期非法会不会报错
	Date d(2025, -1, 20);

	return 0;
}

9  总结

        这一篇文章中的语法难度较大,且是学习 C++ 语言的重要语法,其中构造函数,析构函数,拷贝构造函数,运算符重载,赋值运算符重载函数是这一篇文章的重点,大家一定要熟练掌握他们的特点与语法,后面的学习才会如鱼得水,否则后面学起来会很困难。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值