接着上一章,这一次我将继续介绍C++类与对象的知识:
用户没有显式实现,编译器自动生成的成员函数,即默认成员函数。在一个类里,我们不写的情况下编译器会默认生成下图中的6个默认成员函数函数:

其中,前4个是最为重要的,也是本次我即将讲解的,关于去取地址重载以后我们再讲。
默认成员函数比较难,我们要理解他们的两个方面:
接下我们来开始吧。
构造函数是特殊的成员函数。别看他名字有个“构造”就以为他是开辟空间来创建对象的,其实他的主要任务是在对象实例化的时候初始化对象。
构造函数的本质就是代替我们以前具有初始化功能的函数 Init,比如栈Stack,日期类Date中的Init函数,构造函数自动调用的特点完美替代的了 Init。
构造函数有以下几个特点:
如图,我将以Date类向你展示构造函数:

//缺省值可以随便给,但最好有意义,且前提是合法
Date::Date(int year = 2000, int month = 1, int day = 1)
{//全缺省构造函数
_year = year;
_month = month;
_day = day;
}那该如何调用此函数?同样我将展示代码:
int main()
{
Date d1; // 调⽤默认构造函数,全缺省,无参,都适用!
}
同时,我将强调一点,不能这样: Date d3(); 这样会被系统认为这是在声明函数!
接下来我们讲讲析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁(内存释放),只是清理类对象的资源(比如对象的内置类型p指针指向了一片堆空间,析构负责释放这个堆空间,然后delete或者free会清理对象本身的空间,即p指针本身的地址)。比如局部对象是存在栈帧的, 函数结束栈帧销毁,他的内存就自动释放了,不需要我们管,析构函数只与他对应的类有关。C++规定:对象在销毁时会自动调用析构函数,完成对象中资源(其实就是手动申请的空间资源,需要动态开辟空间,比如malloc之类的)的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能,而像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
接下来是析构函数的特点:
接下来我将展示一下Stack中析构函数的代码:
~Stack()//Stack中的析构函数
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}我们可以清晰地观察到它的特点,无参数,无返回值,函数名是 ~类名。
我们来说说第7个特点:⼀个局部域的多个对象,C++规定后定义的先析构
int main()
{
Date d1(2025,10,30);
Date d2(2025,11,9);
return 0;
}如图,我先后实例化了两个日期d1和d2,那么根据先定义的后析构,析构顺序就是d2,d1,可以认为和栈的性质一样。
如果一个构造函数的 第一个参数是他自身类类型的引用,且任何 额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说 拷贝构造函数是一个 特殊的构造函数。
拷贝构造有以下特点:
第2点恐怕很多人有疑问,无穷递归调用是怎么导致的?我来解释一下:

看这张图,main函数内, Date d1(2030,1,1); 这是 对d1的实例化,调用了构造函数
Date d2(d1) ;这就是 调用了拷贝构造函数。然而,当下一步进入拷贝构造函数后,函数的形参 Date d 需要 拷贝 传过去的实参d1,这个过程再次调用了拷贝构造函数,然后下一步进入拷贝构造函数,Date d 又要拷贝 Date d1......如此无限循环,就是无穷递归调用了。这样应该挺好理解了。
关于,拷贝构造函数,还有几点我来分享一下:
int main()//日期类
{
Date d1(2025, 10, 30);
Date d2(d1); //调用拷贝构造的一个方式
Date d3(2025, 11, 9);
Date d4 = d3; //调用拷贝构造的另一方方式
return 0;
}
如图,调用拷贝构造函数有两种方式,大家怎么喜欢怎么来。第一种方式如果是拷贝一个函数:Stack ret(func1()); 看起来难免有些奇怪,第二个方式则是延续了C语言的方法,看起来也顺眼一些。
最后还有一个小技巧:如果一个类显式写了析构函数并且释放了资源(空间),说明它申请过空间资源并且需要手动释放,那么就需要显式写拷贝构造函数,因为申请过资源说明浅拷贝必然不足以满足需求——因为浅拷贝只会复制指针地址,导致多个对象共享同一块资源,最终引发 “二次释放” 或 “资源篡改” 的问题,所以需要自己重新写一个。如果没有显式写析构函数则不需要。
定义:
运算符重载是 C++ 面向对象特性的重要组成部分。当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式为自定义类型(类或结构体)重新定义运算符行为。C++规定类类型对象使用运算符时,必须转换成调用对应的运算符重载,若没有对应的运算符重载,则会编译报错。
接下来是它的特点与性质:
1.运算符重载是具有特殊名字函数,它的名字由operator和要被定义的运算符组成。和其他函数一样,他也具有返回类型、参数列表以及函数体。
2.重载运算符函数的参数个数和该运算符的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,其左侧的运算对象传给第一个参数,右侧的运算对象传给第二个参数。
3.如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,其参数比运算对象少一个。
4.运算符重载后,其优先级和结合性不变
5.语法中没有的符号,不能被创建为新的操作符,如:operator@( )
6. .* :: sizeof ?: . 这5个运算符不能重载!一定要记住
相信很多人对 .* 运算符不理解,这是C++新规定的符号,用于在类外调用成员函数(函数指针),我将举例:

如图,(d1.*pf2)() , .* 运算符是这样用的,不过实际用处很小,了解即可。
7.运算符重载函数中至少有一个类类型参数,不能通过运算符重载去改变内置类型对象的含义,如:int operator+(int x,int y),
就像1+1=2一样,内置类型的运算符意义是绝对不允许被改变的。
8.重载运算符时,前置++和后置++ 的 重载运算符函数名 都是operator++ ,这样难以区分。所以C++规定,在后置++重载时,加一个int形参,而前置++则不需要,这样做的目的是做区分,并且因为更提倡使用前置++ 所以只在后置++添加形参, 前后置-- 也是一样。
展示一下效果:
Date operator++()
{
//前置++
}
Date operator++(int)
{
//后置++
}如图,前后置-- 就把++换成--,一样的,类型我用日期类Date举例,具体看各位的需求。
9.重载<< 和 >>时,需要重载为全局函数。因为如果重载为成员函数,第一个参数默认就是this指针,而第一个形参描述的是左侧运算对象,调用此函数时,书写就从 cout<<对象<<endl; 变成 对象<<cout<<endl; ,极其不符合可读性和使用习惯。重载为全局函数第一个参数就不会被this指针霸占,可以换成 istream&(针对>>)/ostream&(针对<<),然后第二个参数再写成类类型的对象。
同样的,我来展示一下他的调用代码:
int main()
{
d1.operator-(d2); //第一种
d1 - d2 ; //第二种
return 0;
}
如图,展示了日期类Date的重载后减法 - 的调用,第一种是代表d1通过操作符 . 访问并且调用了这个函数,以实现d1 - d2;
第二种则是直接相减,d1 - d2 ,编译器会把这句表达式,按照C++标准规定的语法逻辑 翻译成 d1.operator-(d2) ,也就是显式调用, 然后执行。
同样,喜欢用哪个都行 ,无非就是 显式调用 和 隐式调用 的区别。
我再展示一下 前后置++的调用方法:
// 编译器会转换成 d1.operator++();
++d1;
// 编译器会转换成 d1.operator++(0);
d1++;
d1.operator++(任意整型);
d1.operator++();如图。前两种会和上面的一样,翻译成注释中的写法。 第三种是后置 ++ 的写法,括号内需填写一个 int 类型的参数(仅作语法区分,值无意义,通常传 0 即可)以区分第四种 前置 ++ 的写法。
以上都是假设为成员函数时的写法。当然,如果重载运算符函数 作为全局函数时,他们的显式调用都得变一变了。因为作为全局函数时,没有this指针在参数的第一位,所以此时函数是可以接收两个参数的,所以,第一个例子就会变成这样:
operator-(d1,d2) ,第二个例子自增就会变成这样: operator++(d1,0) operator++(d1) ,分别是 后置++和 前置++。
但不变的都是,第一个参数代表左侧运算对象,第二个参数代表右侧运算对象。
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象的直接拷贝赋值,这里要与拷贝构造区分,拷贝构造是用于一个对象拷贝给另一个将要创建,还未存在的对象
赋值运算符重载的特点:
1.赋值运算符重载是一个运算符重载,规定必须重载为成员函数。其参数建议设为const当前类类型的引用,否则会传值传参,发生拷贝
2.有返回值,且建议写成当前类类型的引用。引用返回可以提高效率,减少拷贝(传值传参),有返回值的目的是为了支持连续赋值
3.同样,没有显式实现时,编译器会自动生成一个默认的赋值运算符重载函数,默认运算符重载的行为和拷贝构造类似,对内置类型成员变量进行值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型会调用他自己的赋值运算符重载函数。
4.同样,若自动生成的默认赋值运算符重载函数就够用,如Date类这种全是内置类型且没有指向什么资源的,我们不需要自己实现。像Stack这类,有指针_a指向资源的,编译器生成的只能浅拷贝,不能满足我们需求,我们需要自己写一个能实现深拷贝的。同样,MyQueue也不需要我们自己实现,因为他的内置类型Stack有自己的赋值运算符重载函数(要写也可以,不过只是挂个名,函数体就是调用Stack的赋值运算符重载函数,所以相当于没写。),所以不需要我们显式实现。
同样,如果一个类显式实现析构并释放了资源,说明同样主动申请了空间资源(需要深拷贝),那我们就需要自己实现。
接下来我演示一下赋值运算符重载的代码:

如图,返回类型 Date& ,引用返回可以减少拷贝,提高效率,前提是引用对象出了函数作用域还存在,没被销毁。 为什么要返回this指针?因为 表达式 比如: d1 = d2 ,即d2赋值给d1,其中d1作为左侧运算对象 就会传给第一个参数,也就是隐式的this指针。
还有一个特别容易混淆的需要说明,同样我直接搬出代码:

如图中注释的解释。 所以为了避免混淆,可以习惯使用Date d1(d2)作为拷贝构造,也是锻炼自己使用新语法,大多数情况下也提高了代码可读性。
到这里,类的默认成员函数中,最重要的几个就讲解的差不多了,最后一个后面会再安排时间讲解。下一篇我将会提供实现完整的日期类的博客,其中包括以上所以内容的练习,算是一个综合练习吧。敬请期待。
同样,以上内容如果有误,请大佬们指出,我将感激不尽,各位请多支持。