【C++初阶】:C++类和对象(中):类的默认成员函数---万字解说(最主要的四点)

🎈主页传送门****:良木生香

🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》《鼠鼠的C++学习之路》

🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离



前言:在上一篇文章中,我们学习了C++类和对象的"上"篇,学习了类的定义,类的实例化以及this指针,文章的链接我会放在本篇文章的最后。今天这篇文章我们主要讲的是类和对象的核心部分,即类的默认成员函数,主要分为三大部分:初始化和清理,拷贝复制以及取地址重载。这些内容相对来说都比较晦涩难懂,看一遍看不懂的小伙伴可以多看几遍,话不多说,我们直接进入正题。


目录

一、类的默认成员函数:

二、构造函数

三、析构函数

四、拷贝构造函数

五、赋值运算符重载

5.1、运算符重载

5.2、赋值运算符重载

5.3、完整的日期类实现

5.4、前置++与后置++


一、类的默认成员函数:

所谓的默认成员函数就是尽管用户没有显式实现,但是编译器会自动生成的函数。在一个类中,我们不自己写的情况下编译器会自动生成六个成员函数,需要注意的是接下俩讲的前四个是最重要的。后两个是C++11之后才加上的,这两个相比于前四个并没有那么重要,还有一个用于过渡理解的就是运算符重载,这些在这一篇文章中都会全部讲到。

默认成员函数都很重要,在理解他们上也更加复杂,我们将围绕一下两个方面对他们进行学习和理解:

1.我们不显式实现时,编译器默认生成的函数行为是什么,是否满足我们的需求

2.当编译器默认生成的函数不符合我们想要的要求,我们需要自己实现,该如何自己实现?

下面是我们今天要学习的大纲,也是默认成员函数的分类:

二、构造函数

构造函数,很多人一同,构造?是不是创建一个对象的函数呢?并不是。构造函数是为对象进行初始化的,是特殊的成员函数。为什么不是用来创建对象的呢?因为对象的创建不用我们去管理,编译器会自己自动帮我们一条龙服务搞定的。构造对象的作用就像我们之前写数据结构的Init()函数一样,为类进行初始化。

为何要代替我们自己写的初始化呢?这个问题我们后面再来回答。

对于构造函数,我们不能用之前看待普通函数的眼光来看待它,本贾尼博士另外新拓展的道路,又有什么特殊之处呢?请看下面:

  1. 函数名与类名相同
  2. 无返回值(不需要给返回值,也不用写void,这是C++的规定)
  3. 对象实例化时系统会自动调用对应的构造函数
  4. 构造函数可以重载
  5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成。
  6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。
    7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始
    化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。

在上面的特点中,最重要的就是第三点,也是最复杂的,第四点是最基本的。以前我们写代码是先创建对象,再调用自己的Init()函数,但现在我们只用写他的同名函数即可:

cpp 复制代码
#include<iostream>
using namespace std;

class Date {
public:

	Date() {    //无参初始化,Date的同名函数,就是Date对象的初始化函数
		_year = 1;
		_month = 1;
		_day = 1;
	}

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

int main() {
	Date d1;
	return 0;
}

当然,我们也可以对Date()进行重载,像这样子:

cpp 复制代码
Date(int year,int month,int day) {   //有参数初始化
	_year = year;
	_month = month;
	_day = day;
}

那么在main函数中的对象d1就要改成:

cpp 复制代码
Date d1(2026,3,21);

要将参数传进成员函数中,那么在运行时,d1被创建之后,编译器就会自动调用Date()函数从而实现对d1的初始化,但是这里有一个细节要注意:如果我们在创建对象时想要将参数传进去,那么成员函数就要有形参对其进行接收,如果没有参数但成员函数有形参,那就会报错,这是语法的要求:

那我们现在来想一个问题,我们的对象在无参数时可以写成这样子吗:

Date d1();

当然时不行的,这样写就像是声明函数,但这个是对象,会让编译器产生误解。所以有参数时才可以这么写,无参数时不能这么写。

现在回到我们的第一个问题:构造函数与自己写的Init()有什么区别?为何一定要使用构造函数?

因为我们的Init()函数的定义和声明是分离的,如果我们不加以注意,就会忘记初始化,可能导致不必要的结果出现,所以为了解决这个问题,祖师爷才想出了这么一个办法。在上面的七个特性中,我们将1-4点看成一组,5-7点看成另外一组

对于第五点:如果我们的类中没有自己写构造函数(即显式构造函数),那么编译器就会自动生成一个"无参"的构造函数。但是只要有一个构造函数时自己写的,编译器就再也不会自动生成,所以这就保证了,不论是什么情况,我们的对象都能被保证初始化。

对于第六点:有一个地方要强调的是---默认构造函数 != 默认成员函数。

这三者只能出现一个,任意两者都不能同时出现,为什么呢?当我们自己写构造函数时,编译器生成的肯定是不会出现的了,无参构造函数和全缺省构造函数同时出现的话,会在调用时出现争议,所以这三者中只能出现一个。

对于第七点:C++将类型分为内置类型和自定义类型,int /float/double这些都属于是内置类型,class/struct这些都属于是自定义类型,那编译器生成的构造函数时如何处理这些成员的?对于内置类型的变量,编译器对他们不做要求,也就是有时候初始化,有时候不初始化,春春看编译器当时的心情怎么样(很坑),所以我们要把他们当作不被初始化来使用。对于自定义类型,如果这个成员没有默认构造函数,那就会报错,此时我们想要初始化这个成员变量,就要用到初始化列表,这个东西我们会在下一章节的内容中讲到。

在类中写的函数是带参数的构造函数,那就是非默认构造。

加入我们A类的成员在B类中被使用,那么在实例化B类的对象时也会调用到A类的初始化,我们拿两个栈指针实现队列来举个例子:

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        _capacity = n;
        _top = 0;
    }
private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

// 两个Stack实现队列
class MyQueue
{
public:
    //编译器默认生成MyQueue的构造函数调用了Stack的构造,完成了两个成员的初始

private:
    Stack pushst;
    Stack popst;
};

int main()
{
    MyQueue mq;

    return 0;
}

总结:构造函数不是用来创建对象的,而是用来初始化对象的。在大部分情况下自己写构造函数是最保险的。

三、析构函数

析构函数,看名字就知道:解析构造函数,即拆分构造函数,那起到的作用就是与构造函数相反的功能------清理资源,起作用就相当与我们在写数据结构时的destory()函数,释放内存。这里有一个小小的规则:后定义的先销毁。

那销毁的是什么呢?析构函数并不是对对象本身进行销毁,而是对于那些占用了资源的东西进行销毁,比如说通过malloc或者realloc开辟空间的东西,可以理解为destory()销毁了哪部分,那么析构就销毁的就是哪部分。对于局部对象是生成在栈中的,函数结束之后就自己销毁了,不需要我们操心。

析构函数的特点如下:

  • 析构函数名是在类名前加上字符~。
  • 无参数无返回值。(这里跟构造类似,也不需要加 void)
  • 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
  • 对象生命周期结束时,系统会自动调用析构函数。
  • 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定义类型成员会调用他的析构函数。
  • 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如 Date;如果默认生成的析构就可以用,也就不需要显示写析构,如 MyQueue;但是有资源申请时,一定要自己写析构,否则会造成资源泄漏,如 Stack。
  • 一个局部域的多个对象,C++ 规定后定义的先析构。

代码实操如下,依旧是以队列为例子:

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        _capacity = n;
        _top = 0;
    }

    ~Stack()
    {
        cout << "~Stack()" << endl;
        free(_a);
        _a = nullptr;
        _top = _capacity = 0;
    }

private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

// 两个Stack实现队列
class MyQueue
{
public:
    //编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的St

    // 显示写析构,也会自动调用Stack的析构
    /*~MyQueue()
    {}*/

private:
    Stack pushst;
    Stack popst;
};

int main()
{
    Stack st;
    MyQueue mq;

    return 0;
}

这段代码的运行结果是这样的:

为什么是这样的呢?当main()函数结束的时候,系统会释放st,mq的资源,按照"后定义的先析构"的原则,会先析构掉mq,mq中又有pushst和popst,会先析构popst,才到pushst,最后才是st,所以会打印三次~Stack()。

当我们在熟练使用构造函数和析构函数之后,会发现我们不必为了初始化和释放资源而担忧了。

四、拷贝构造函数

拷贝构造函数,听名字我们就不难猜出,这个函数跟构造函数是有关联的。拷贝构造函数是构造函数的一种重载函数。我们什么时候会调用拷贝构造函数呢?像这样:

cpp 复制代码
Date d1(2026, 3, 21);
Date d2(d1);

当我们想用一个对象的值去初始化另外一个对象的时候,就会用到拷贝构造函数。它跟默认构造函数的区别是:++默认构造函数是给创建出来的对象进行初始化,而拷贝构造函数是用已经存在的对象去给创建出来的对象进行初始化。++ 像上面那两行代码一样,用d1去初始化d2,将d1的值全部赋值给d2以帮助d2完成初始化。这个函数的特点是,在传参的时候,第一个参数(除了this指针外),用的是Date& d,即赋值对象的别名。如下面代码所示:

cpp 复制代码
#include<iostream>
using namespace std;

class Date {
public:

	//Date对象默认构造函数
	Date(int year = 1, int month = 1, int day = 1) {
		_year = year;
		_month = month;
		_day = day;
	}

	//拷贝构造函数
	Date(const Date& d) {    //像main()函数中那样的话,那么此时传进来的就是d1的别名
		_year = d._year;//this->_year = d._year;
		_month = d._month;//以此类推
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main() {

	Date d1(2026, 3, 21);
	Date d2(d1);

	return 0;
}

在参数传递中我们可以像默认构造函数那样使用传值传参吗?为什么一定要用到引用传参呢?

在解决这个问题之前,我先插一嘴:还有一种场景会用到拷贝构造:

cpp 复制代码
#include<iostream>
using namespace std;

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

	//这个是拷贝构造
	Date(Date& d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

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

void Func(Date d) {
	cout << "&d:"<< &d << endl;
	d.print();
}

int main() {
	Date d1;
	Func(d1);
	cout << "&d1:"<< & d1 << endl;
	return 0;
}

传入的参数是自定义类型时使用拷贝构造函数将自定义类型传进去,为什么?因为如果d1是内置类型(int/float/double等)时,他们的大小(1/4/8等)字节,并不算大,编译器可以直接通过指令对他们进行拷贝,是非常方便的。但如果是自定义类型,因为编译器不知道我们的自定义类型有什么东西在里面,所以就不能用指令直接传参,而是先将自定义类型(这里的d1)先拷贝一份给形参d,再由形参d执行d.print()函数,此时形参d是独立于d1的新对象:

如果传入的是d1的别名(d1的引用),则不会触发拷贝构造,而是相当于直接将d1传入Func()中。当然,你想传入指针也行,就是太麻烦了,现阶段我们学了引用,就应该多使用引用来巩固知识点。

现在我们回到刚才的问题:在拷贝构造中,为什么要用引用传参而不是传值传参?如果使用的是传值传参,那么在初始化形参d的时候就又会继续调用拷贝构造,如此一来,就会导致无限递归,最终会抛出"栈溢出"的错误。使用&或者指针就能打破递归。

在拷贝构造中,对于像上面的日期类是很容易拷贝的,只用浅浅的复制一下就能实现拷贝:

但如果是栈或队列,那就比较麻烦了:

cpp 复制代码
#include<iostream>
using namespace std;

typedef int STDataType;

class Stack
{
public:
    Stack(int n = 4)
    {
        _a = (STDataType*)malloc(sizeof(STDataType) * n);
        if (nullptr == _a)
        {
            perror("malloc申请空间失败");
            return;
        }

        _capacity = n;
        _top = 0;
    }

    Stack(const Stack& s) {
        _a = s._a;
        _capacity = s._capacity;
        _top = s._top;
    }


    void Push(STDataType x)
    {
        if (_top == _capacity)
        {
            int newcapacity = _capacity * 2;
            STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
            if (tmp == NULL)
            {
                perror("realloc fail");
                return;
            }

            _a = tmp;
            _capacity = newcapacity;
        }

        _a[_top++] = x;
    }

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

private:
    STDataType* _a;
    size_t _capacity;
    size_t _top;
};

// 两个Stack实现队列
class MyQueue
{
public:

private:
    Stack pushst;
    Stack popst;
};

int main()
{
    Stack st1;
    st1.Push(1);
    st1.Push(2);
   
    Stack st2 = st1;
    return 0;
}

在这段代码中,我们的本意是像将st1赋值个st2来实现对st2的初始化,如果像日期类那样直接拷贝的话,我们发现会出现一下问题:

为什么?我们不妨来猜想一下,问题是出现在拷贝环节吗?调试一下:

很显然,在赋值这一块并没有问题,继续调试看看:

当程序走到析构函数的时候出现了问题。先定义的先析构,当st2被析构了之后,编译器还会再执行一遍析构函数来将st1析构,此时,由于我们在对两者赋值的时候是将st1的地址直接复制给了st2,那st1与st2指向的是同一块空间,st2被析构之后,会导致st1指向的空间被释放,此时再析构st1,会导致同一块空间被释放两次,这是不被允许的,所以问题就是在这里出现的。如图所示:

此时st1和st2都指向的是同一块空间,自然是不能释放两次的,那该怎么办呢?很简单,使用memcpy()函数就行,正确的做法是将是上面代码中的Stack换成下面的Stack:

cpp 复制代码
 Stack(const Stack& st)
 {
     // 需要对_a指向资源创建同样大的资源再拷贝值
     _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity);
     if (nullptr == _a)
     {
         perror("malloc申请空间失败!!!");
         return;
     }

     memcpy(_a, st._a, sizeof(STDataType) * st._top);

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

先对st2的_a开辟空间,在将数据传进来,这样在析构的时候就不会因为释放同一块空间两次而报错,我们将这种拷贝称之为"深拷贝",将淡出地复制值的称之为"浅拷贝"。

总结:当遇到像栈或者队列这种含由系统资源的,会有一下问题:

  • 一个对象被修改,会影响到另外一个对象
  • 析构时,会释放两次同一块空间

这时候就需要"深拷贝",只要是对空间资进行处理
如果我们未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),对⾃定义类型成员变量会调⽤他的拷⻉构造。对于自定义类型可以不写拷贝构造。
在C/C++中,释放空间都是一整段一整段的释放,不会截断释放,如果是自己申请的空间,那就要自己显式写析构函数。

对于第六点:传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

还有一个问题:

cpp 复制代码
Stack s1;
Stack s2(s1);
Stack s3 = s1;//函数一般是 Stack s3 = Func();这样子写

以上两种写法都是拷贝构造,第二行是针对对象与对象的,第三行更偏向于函数的赋值。

五、赋值运算符重载

在讲赋值运算符重载之前,我们要做一个小小的铺垫---运算符重载。

5.1、运算符重载

对于内置类型,做加减运算的时候都是可以通过指令完成的,但是对于自定义类型,就不一样了,编译器不知道自定义类型该怎么相加,这时候就要用到运算符重载了。

运算符重载和之前讲的函数重载没有半毛钱关系,虽然只是名字相同!!!

C++同意我们在对自定义类型对象进行运算时,可以通过重载的方式给原有的操作符赋予新的定义,语法如下:

cpp 复制代码
运算符的原返回值 + operator + 运算符 + 参数

就像这样(以加法为例):

cpp 复制代码
Date operator+(const Date& d1,const Date& d2){
    //此处执行加法操作
    return ans;
}

Date表示的是类类型。这里就有一写小细节要注意:重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数,返回值与符号含义相关联。

但是像这样子写可以吗?:

cpp 复制代码
int operator+(int a,int b){
    return x+y;
}

不可以!!!在运算符重载中,必须要有一个类类型对象!!!

像日期类类型,我们可以通过一下代码来判断两个日期相不相等:

cpp 复制代码
//前面的代码在此处就先神省略,后面会补上的
bool operator==(const Date& d1, const Date& d2) {
	return d1._year == d2._year 
		&& d1._month == d2._month 
		&& d1._day == d2._momth;
}

int main() {
	Date d1(2026, 3, 23);
	Date d2(2026, 3, 23);
	return 0;
}

如果就这么写的话其实是会报错的,像这样:

因为我们已经将_year等成员变量设置成为了私有,外面的函数自然无法访问,想要解决这个问题,有四种解决方式:

1.将成员放到公有范围:

cpp 复制代码
public:

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

//private:
	int _year;//将_year这些成员全部放到公有范围
	int _month;
	int _day;
};

2.使用Date提供xxx的函数:

cpp 复制代码
class Date {
public:

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

	int getYear() const {  //添加提取私有成员变量的函数
		return _year;
	}
	int getMonth() const {
		return _month;
	}
	int getDay() const {
		return _day;
	}


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

//现在来判断两个日期是不是相等的,通过调用访问私有成员的函数
bool operator==(const Date& d1, const Date& d2)
{
	return d1.getYear() == d2.getYear()
		&& d1.getMonth() == d2.getMonth()
		&& d1.getDay() == d2.getDay();
}

3.添加友元函数(这个方法对于目前的我们来说还理解不了,有兴趣的小伙伴可以自己查查,到后面的章节会讲到)

4.将运算符重载函数重载成成员函数:

cpp 复制代码
class Date {
public:

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

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


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

但这时候就会有一个报错:

为什么会显示运算符中调用的参数太多呢?就是因为this指针!!!在类中的成员函数中,是默认传入this指针的,既然不能对this指针动手,那我们只能修改自己的参数,这时候我们就要改成:

cpp 复制代码
class Date {
public:

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

    //main函数中也要同时改成:
int main() {
	Date d1(2026, 3, 23);
	Date d2(2026, 3, 23);
	bool ans = d1.operator==(d2);
	return 0;
}

这样一来,就变成了对象d1调用operator函数来与d2进行比较,而且编译器会在类和全局中寻找我们的运算符函数。

我们不能使用不存在的运算符进行重载,如@,@这个符号在语言中没有运算的作用,就不能operator@这样使用。

五种不能用来重载的运算符:.* 、 :: 、sizeof 、 ?:_ 、 .

第一个是访问成员函数的指针,第二个是限定域运算符,第三个是大小运算符,第四个是条件运算符,第五个是访问运算符。

对于第一个访问成员函数运算符是这样使用的:

cpp 复制代码
class A {
public:
	void Func() {  //类里面有一个成员函数 Func()
		cout << "A::Func()" << endl;
	}
};

typedef (A::*PF)();

int main(){
    //普通的函数指针(假设类里面的函数是全局的话):void (*)()
    //那么类中的函数指针:void (A::*)(),因为有this指针的存在,就要把函数指针的范围明确
    //重命名就是:typedef (A::*PF)()
    PF pf = &A::Func;
	A obj;
	(obj.*pf)();
	return 0;
}

整个流程就是:

  1. 先将成员函数指针重命名为 PF,再定义成员函数变量 pf来接收成员函数指针
  2. 定义一个类叫obj,用obj通过函数指针的解引用来调用成员函数

输出的结果为:

这就是成员函数指针的访问。

如果不对成员函数指针进行重命名,那就是这样子在main()函数中进行访问:

cpp 复制代码
void (A::*pf)() = &A::Func;  //pf接收的就是成员函数Func的指针

总结:运算符重载就是为了自定义类型运算而产生的,重载时至少需要一个类类型参数,可以不用所有参数都是类类型,允许与内置类型进行搭配

5.2、赋值运算符重载

有了运算符重载作为铺垫,现在我们就可以讲讲今天的大头了---赋值运算符重载。

既然名字中有赋值,没那么我们就要将之前的知识联系起来:

构造函数:给刚刚创建好的对象进行初始化:Date d1(2026,3,23);

拷贝构造函数:用已经创建并且初始化好的对象给另一个刚刚创建的对象初始化Date d2(d1);

赋值运算符重载:将一个已经存在的对象的值赋给另一个已经存在的对象。

我们可以通过下面的代码来看看他们三者之间的关系:

cpp 复制代码
#include<iostream>
using namespace std;

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

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

	//赋值重载函数
	void operator=(const Date& d) {
		_year = d._year;
		_month = d._month;
		_day = d._day;

	}

	void print_Date() {
		cout << _year << " " << _month << " " << _day << endl;
	}
private:
	int _day;
	int _month;
	int _year;
};

int main() {
	Date d1(2026, 3, 23); //这是构造函数,用于给d1进行初始化
	cout << "d1:"; d1.print_Date();
	Date d2(d1);   //这是拷贝构造函数,用于将d1给d2进行初始化
	cout << "d2:"; d2.print_Date();
	Date d3(2026, 4, 1);
	cout << "d3:"; d3.print_Date();
	d3 = d1;  //这是赋值运算符重载,用于将d1的值复制给d3(d1和d3都已经存在了)
	cout << "d3(after):"; d3.print_Date();
	return 0;
}

有朋友会问了,在赋值重载函数中能不能传值进去?可以的,只是会先调用一遍构造函数传给形参,降低效率。

赋值重载函数必须是成员函数。

我们之前说,运算符重载要有返回值,那赋值运算符重载的返回值是什么?好像也没见过有返回值啊...有的兄弟,有的,赋值重载函数的返回值是类类型的引用,为的就是连续赋值的情况:

当我们的赋值重载返回的是void类型,就不能连续赋值了,因为当d1赋值给d5之后,他们两个没有返回值,那么就无法赋值给d3,就会出现这个错误。我们只用把void改成Date&即可:

这时候又有人要问了,返回值可以用传值吗?:Date operator=(const Date d)这样写。当然可以,但没必要,因为在返回值的时候还是会调用一遍拷贝构造函数,增加程序负担。

当然了,在赋值上还有个小问题,那就是d1=d1,自己给自己赋值,这样的操作毫无意义,只会增加程序负担,那该怎么处理?很简单,只用加上这个判断:

cpp 复制代码
Date& operator=(const Date d){   
    if(this != &d){  //用if判断一下是不是自己
         //赋值操作...
    }
    return *this;
}

总结:对于赋值运算符重载,如果不涉及空间指向问题的时候,我们不显式写,那么会自动调用编译器自己生成的赋值重载函数(只进行浅拷贝),如果涉及空间指向,那就要自己显式写了,不然在析构时会出错。

5.3、完整的日期类实现

在有了拷贝构造,赋值运算符重载这些前备知识之后,我们现在来简单的练习一下,实现日期类的相关运算,比如说:日期+天数。

假设我们我们有个日期,是2026.1.1,往后相加100天,那是多少天呢?其实就像加法一样,当一个月到头了,那就往下一个月进位,最终加上一百天就应该是2026.4.12,我们先在用程序来计算一下:

cpp 复制代码
#include<iostream>
using namespace std;

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

	int GetMonthDay(int year, int month) {
		static int month_arr[13] = { -1,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;
		}
		else {
			return month_arr[month];
		}
	}
    
    //想要只返回修改后的自己,那就d1 += 100,同时将Date改成:Date& ,返回引用
	Date operator+(int day) {
		
		_day += day;
		while(_day>GetMonthDay(_year,_month)){
			_day -= GetMonthDay(_year, _month);
			++_month;
			if (_month == 13) {
				_year++;
				_month = 1;
			}
		}
		return *this;
	}

	void print_Date() {
		cout << _year << " " << _month << " " << _day << endl;
	}

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

int main() {
	Date d1(2025, 8, 1);
	Date d2  =d1 + 100;
	d1.print_Date();
	d2.print_Date();
	return 0;
}

在这段代码中,我们用d2来接收d1加100天之后的日期,相加后是我们将两个日期都打印出来看看运行结果:

此时我们发现,在d1增加100天之后,两者打印出来的结果是一样的,说明我们的逻辑是有点错误的,如果是想返回修改之后自己的值,就应该将"+"改成"+=",如果我们只想返回增加之后的日期呢?不修改自身数值的情况下?这时候我们就要用一个临时变量了:Date temp(*this);

cpp 复制代码
//在operator中加上temp变量,这样子修改的就是temp变量,而不是d1本身了	
Date operator+(int day) {
		Date temp(*this);
		temp._day += day;
		//_day += day;
		while(temp._day>GetMonthDay(temp._year,temp._month)){
			temp._day -= GetMonthDay(temp._year, temp._month);
			++temp._month;
			if (temp._month == 13) {
				temp._year++;
				temp._month = 1;
			}
		}
		return temp;
	}

基于这种改变,我们再来看看运行结果:

这样子就达到我们的要求了。

现在我们来想想一个问题,既然+和+=两者这么相似,是不是可以对他们进行复用?就是说,先使用"+",再使用"+=",或者两者的顺序反过来?当然是可以的。但是效率会有所不同

cpp 复制代码
//拷贝三次:
Date operator+=(int day) {
	Date temp(*this);
	temp._day += day;
	//_day += day;
	while(temp._day>GetMonthDay(temp._year,temp._month)){
		temp._day -= GetMonthDay(temp._year, temp._month);
		++temp._month;
		if (temp._month == 13) {
			temp._year++;
			temp._month = 1;
		}
	}
	return temp;
}
Date operator+(int day) {
	Date temp(*this);
	temp += day;
	return temp;
}


//////////////////////////////////////////////////////////////////
//拷贝两次:
Date operator+=(int day) {
	//只负责修改自己
	
	_day += day;
		//_day += day;
	while(_day>GetMonthDay(_year,_month)){
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13) {
			_year++;
			_month = 1;
		}
	}
		return *this;

}

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

对于上面两种方法,我更推荐使用第二种方法,因为来拷贝的次数少,效率高。

5.4、前置++与后置++

前置++与后置加加在平时使用时或许感受不到这么答的作用,但是放在运算符重载里面的话,差别就大了,首先我们知道,前置++是先使用再++,而后置++则是先++再使用,所以为了区分这两者在重载时的差别,祖师爷就为这两者的语法做了一个小区分

如果在重载是时是这么写:

cpp 复制代码
Date operator++();

普通人肯定分辨不出来那个是前置那个是后置,那么编译器就将operator(int)作为后置加加,只是比前置++:operator++()多了个参数int ,这样在编译的时候就能分清谁是谁了。当然,形参int在实际中写不写都行,只用传一个随机的整数即可。

cpp 复制代码
//前置++
Date& operator++() {
	*this += 1;
	return *this;
}

//后置++
Date operator(int) {//实际上在int那一类,传什么证书都行,0,10,23等等都可以
	Date temp(*this);//先拷贝一份,再能++
	*this += 1;
	return temp;//返回的是++原来的值
}

C++规定,前置++使用原生operator(),不加参数,因为C++想让我们在自定义类型中对使用前置++,因为没有拷贝,但是后置有拷贝(因为要保证先使用自己才++),并且前置++是引用返回,效率会比后置++要高。

上一篇文章链接:

【C++初阶】:C++类和对象(上):类的定义 & 类的实例化 & this指针-CSDN博客https://blog.csdn.net/2501_91249865/article/details/159155480?spm=1001.2014.3001.5501

那么以上就是本次所有的内容了

文章是自己写的哈,有什么描述不对的、不恰当的地方,恳请大佬指正,看到后会第一时间修改,感谢您的阅读~

相关推荐
txinyu的博客2 小时前
解析muduo源码之 TcpServer.h & TcpServer.cc
c++
☆5662 小时前
C++安全编程指南
开发语言·c++·算法
无心水2 小时前
【时间利器】4、JavaScript时间处理全解:Date/moment/dayjs/Temporal
开发语言·前端·javascript·状态模式·openclaw·date/moment·dayjs/temporal
星轨初途2 小时前
类和对象(中):六大默认成员函数与运算符重载全解析
开发语言·c++·经验分享·笔记·ajax·servlet
骇客野人2 小时前
用python实现一个查询当天天气的MCP服务器
服务器·开发语言·python
天空属于哈夫克32 小时前
拒绝被动响应:企业微信主动调用接口高阶方案
开发语言·python
cccyi72 小时前
【C++ 脚手架】gtest 单元测试库的介绍与使用
c++·单元测试·gtest
2501_941982052 小时前
Go 语言实现企业微信外部群消息主动推送方案
开发语言·golang·企业微信
南山love2 小时前
spring-boot多线程并发执行任务
java·开发语言