接着讲类和对象的相关知识。
今天我们首先介绍一下类的默认成员函数。
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数,一个类在不手动写的前提下会默认生成6个成员函数,这六个中最重要的是前四个,注意的是C++11后还会增加两个默认成员函数。
这四个值得注意的默认成员函数分别是构造函数、析构函数、拷贝构造、赋值重载。
针对默认成员函数,我们需要从以下2个角度思考,首先,当我们不手动实现的时候,编译器默认生成的函数是否满足我们的需求?如果不满足,我们应该如何手动实现?
首先介绍一下构造函数。
构造函数相当重要,但是构造函数的名字本身就存在一个误区,他并不是用来构造的,你以为构造函数可以开辟空间创造对象,那你想多了,他只是用来初始化的。
构造函数有以下几个特点:
函数名和类名相同
没有返回值
对象在实例化时系统会自动调用对应构造函数
构造函数可以重载
如果我们没有主动显式定义构造函数,C++会自动生成一个无参的构造函数,倘若用户自己实现了,那编译器就不会再生成。
class A { public: A(int d) { c = d; } int add(int a, int b) { return a + b; } private: int c; };以上就是定义和调用构造函数的过程。
很显然,这是我们自己写的构造函数。事实上,构造函数还有一种叫做默认构造函数,这里要跟上边那个无参的做区分,上边那个无参的构造函数是我们不显示实现时的默认构造函数,还有两种,一个是全缺省构造函数,一个是无参构造函数,这两个都是我们可以手动实现的默认构造函数,三者只能存在一个,向我们刚刚写的这个一个参数的,就不是默认构造函数。简单来讲,默认构造函数就是不传参也可以调用的构造函数 。
当我们不写构造函数时,编译器默认生成的构造函数对内置类型成员变量的初始化是不做要求的,这完全取决于编译器,可能会初始化,可能就是随机值,但是,对自定义类型的成员变量,调用编译器自己写的构造函数时,会自动调用自定义类型的默认构造函数。如果自定义类型里我们手动实现了非默认构造函数,那么系统就会因为找不到这个自定义类型的默认构造函数而报错,我们需要初始化列表解决这个问题,现在先不讲解。
有一点需要注意,当我们调用无参的构造函数时不需要加(),如果你加了,系统会因为分不清你这里是调用还是声明而报错。
其实通过上述情况你也能看出来,大部分时间,我们都是需要自己写构造函数的。
接下来是析构函数。
如果你觉得构造函数类似于数据结构的init方法,那么析构就是destroy,和构造函数正相反。
析构函数并不是对这个对象进行销毁,而是对对象内需要释放资源的资源进行释放。
析构函数是类名前加~
和构造类似,析构函数无返回值,不一样的是,析构函数不允许含参数,换句话说,析构函数不允许重载,只有一个。
假如我们不手动实现析构函数,那么编译器也会自己默认生成一个。
当对象的生命周期结束时,系统会自动调用析构函数。
和构造函数类似,编译器自己生成的析构函数不会对内置类型处理,对于外置类型,会自动调用他们的析构函数。
这里需要注意,假如我们显式实现了析构函数,且没有对自定义类型进行处理,系统也会自动调用自定义类型的析构函数,也就是说,无论怎么样,自定义类型的析构函数一定会被调用。也就是说,如果你自己写一个析构函数,只需要处理你的内置类型就好,自定义类型交给系统。
感觉析构函数很多场景下是不用自己写的对吧?其实简单来说,如果你需要释放内置类型资源,那你就要手动实现析构函数,反之则不需要。
C++规定,一个局部域内的多个对象,后定义的先析构。
接下来,我们介绍一下拷贝构造函数。
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,拷贝构造是一个特殊的构造函数。
拷贝构造函数是构造函数的重载。
C++规定,传值传参自定义类型或者传值返回自定义类型会自动调用这个自定义类型的拷贝构造函数,如果你写拷贝构造函数采用传值的方式,编译器会报错,为什么呢?当你传参自定义类型,会调用它的拷贝构造函数,因为你写的拷贝构造是传值传参,所以接着会再次调用拷贝构造,无穷无尽直至程序崩溃。虽然一般我们的拷贝构造函数只有一个当前类类型的引用这一个参数,但从语法的角度是可以有其他参数的,只不过都要有默认值,也就是缺省参数。
C++规定,自定义类型进行拷贝行为必须调用拷贝构造,所谓拷贝行为,你传值传参或者传值返回本质都是拷贝。
如果没有显示的定义拷贝构造,编译器会自动生成拷贝构造函数,自动生成的拷贝构造函数会对内置类型成员变量完成浅拷贝,所谓浅拷贝就是一个字节一个字节拷贝进去,对自定义类型的成员变量则会调用他们自己的拷贝构造函数。
补充一点,为了避免对引用传参的修改,一般加上const修饰。
那你可能会有点疑惑,既然全是内置类型的时候会实现拷贝,自定义类型还会调用他们自己的拷贝构造,那我为啥还要自己实现?明明已经达到效果了。
我这里用一段代码演示一下。
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> using namespace std; class Arr { public: Arr(int size) { _a = (int*)malloc(sizeof(int) * size); _capacity = size; _top = 0; } private: int* _a; int _capacity; int _top; }; int main() { Arr a(4); Arr a1 = a; return 0; }看这段代码的执行效果,你发现了什么?看起来好像没啥问题,但是,两个不同对象的_a成员变量指向的却是一块空间,这显然是不符合逻辑的对吧。
因此我们总结一下,当你的成员变量要向内存申请资源时,就要手动的实现拷贝构造函数。话句话讲,我们知道编译器默认实现的析构函数不对内置类型处理,但是会自动调用自定义对象的析构函数,当我们需要写析构函数的时候就代表我们类中有资源需要释放,和拷贝构造函数实现的前提是一致的。
值得注意的是,引用返回必须要保证引用对象没有被销毁,你返回一个函数内部定义的局部变量的引用那合适吗?都成野引用了。
前边我们提到const引用的传参,一般来讲我们不涉及修改实参时,都最好传const引用,但是Swap函数是涉及到实参的交换的,所以传const引用反而解决不了问题。
除了上边写到的拷贝构造方式,我们也可以用Arr a(b);的方法调用,这是一样的。
在讲赋值运算符重载函数之前,先介绍一下运算符重载。
当我们涉及到自定义类型的比较或者加减什么其他的运算的时候用系统内置的运算符是不够用的,C++支持运算符的重载,我们可以自己重载运算符达到这种效果。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } //private: int _year; int _month; int _day; }; bool operator==(const Date& a, const Date& b) { return a._year == b._year && a._month == b._month && a._day == b._day; } int main() { Date a(2026, 10, 18); Date b = a; if (b == a) { cout << "true" << endl; } if (operator==(a, b)) { cout << "true" << endl; } return 0; }这里我们重载了Date类型的==运算符。事实上,C++也规定类型对象使用运算符时必须要调用重载运算符,不然会编译报错。
在上边例子中operator==就是这个重载运算符函数的函数名,重载运算符参数个数和运算符作用的运算对象个数相同。
重载运算符函数也可以写成成员函数,你自己想想,本身重载运算符函数就是和类型绑定的,不然你重载了给谁用?所以自然是可以写成成员函数,上一讲我们还提到过,成员函数默认隐式传入一个this指针,之所以前边讲那三个默认成员函数没怎么提这个事是因为写法上不太影响,但是这里,如果我们要写在类型中,传参个数就要少一个,因为this本身指向的对象就是其中一个操作数。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } bool operator==(const Date& a) { return _year == a._year && _month == a._month && _day == a._day; } //private: int _year; int _month; int _day; }; //bool operator==(const Date& a, const Date& b) //{ // return a._year == b._year // && a._month == b._month // && a._day == b._day; //} int main() { Date a(2026, 10, 18); Date b = a; if (b == a) { cout << "true" << endl; } if (a.operator==(b)) { cout << "true" << endl; } return 0; }当运算符重载后,其优先级和结核性与对应的内置类型运算符保持一致。
注意,我们不能通过运算符重载增加一个新的运算符,这是不被允许的,C++规定.*, ::, sizeof, ?:, .
这五个运算符不允许重载。
C++规定重载运算符必须有一个类类型的参数,你不能修改内置类型的运算符含义对吧,本来1<2返回true,你非要给他改成false这从逻辑上都会不合理。
倘若成员和全局都定义了运算符重载函数,优先调成员的。
还有一点细节,当我们实现全局的运算符重载函数时,其实你是没办法访问成员对象的,因为默认是private嘛,目前来讲,除了放到公有、重载为成员函数以外还可以采用提供get方法,JAVA经常喜欢用这种方法实现封装。
铺垫了这么多,终于要来到这一讲的末尾,最后为大家介绍类型中的默认成员函数赋值运算符重载函数。
赋值运算符重载函数用于两个已经存在的对象之间的直接拷贝赋值,这里和拷贝构造的区别是拷贝构造是将一个对象赋值给另一个要创建的对象。
赋值运算符重载函数的参数建议写成引用传参,和拷贝构造不同的是,这里不传引用也不会报错,为什么呢?你想啊,传值进来调用自定义类型的拷贝构造,但是人家的拷贝构造可是正儿八经引用传参,所以拷贝构造完可以正常返回,并不会造成错误。
除此之外呢,赋值运算符重载是有返回值的,同样最好写成引用返回,一个道理,为了提高效率。
没有显示实现时,和拷贝构造类似,编译器默认生成的赋值运算符重载函数会执行浅拷贝(对内置类型),外置类型则是会调用他们的赋值运算符重载函数。
同样和拷贝构造类似,当类型没有显式实现析构函数时也就不需要弄一个手动的赋值运算符重载函数了,因为你这里没有额外资源。
多说一点,赋值运算符重载函数之所以有返回值是为了支持连续赋值的情况。