前言
本节是要学习六个默认成员函数。主要是从四个方面讲解:
1)什么是该默认成员函数?
2)默认成员函数做了什么?
3)一些易错的注意事项
4)什么时候用默认成员函数,什么时候显式实现?
本篇用 日期类(Date)、栈(Stack) 、队列(Queue)三种类来举例
类的六个默认成员函数:
- 如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
1. 构造函数
构造函数就与我们所写的Init()方法一样,用于类对象属性的初始化。但这个构造函数不用用户调用,而是在类对象实例化时自动调用。
概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
特性
我们围绕第一个问题展开:什么是构造函数?
其有如下特征:
- 函数名与类名相同
- 无返回值
class Date { public: //函数名和类名相同,无返回值 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; };
- 函数可以重载
class Date { public: //无参数 Date() { _year = year; _month = month; _day = day; } /* //全缺省:注意全缺省和无参数不能同时存在,他们实例化方式可以相同,编译器无法辨别 Date(int year=2024,int month=4,int day=27) { _year = year; _month = month; _day = day; } */ //半缺省 Date(int year,int month=4,int day=27) { _year = year; _month = month; _day = day; } //不缺省 Date(int year, int month, int day) { _year = year; _month = month; _day = day; } private: int _year; int _month; int _day; };
- 对象实例化时编译器自动调用
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。后面实例化对象按照显式定义的函数调用(举例看下面代码注释)
Date类举例:
class Date { public: /* // 5.如果用户显式定义了构造函数,编译器将不再生成默认构造函数,后面也不能用Date d1;这样实例化,而是采用Date d1(2024,4,27);这样来实例化对象; Date(int year, int month, int day) { _year = year; _month = month; _day = day; } */ void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1; d1.Print(); return 0; }
这里我们用Date实例化了个对象d1,调用Print函数打印日期,发现个问题如下图:
我们发现我们的实例化对象d1并没有初始化啊,那默认构造函数到底干了什么呢?
做了什么?
C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char…,自定义类型就是我们使用class/struct/union等自己定义的类型。默认生成的构造函数对两者处理不同:
1)内置类型不做处理;
2)自定义类型调用它自己的默认构造函数;
易错注意:
- 实例化对象,错误:
int main() { Date d1; //表示实例化一个Date类对象; Date d1(); //表示一个返回值为Date的d1()函数方法; return 0; }
2.无参数构造函数、全缺省构造函数、默认构造函数三种都可以当做默认构造函数,只能存在其中一个,不然会发生实例化时编译器不知道调用哪一个构造函数的错误。因为都可以用Date d1;来实例化对象。
3.C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Date { public: void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: //可以在这里给缺省值 int _year=2024; int _month=4; int _day=27; }; int main() { Date d1; d1.Print(); return 0; }
显式定义和默认构造函数
那么什么时候我们采用显式定义,什么时候采用默认构造函数呢?
我给出的答案是,一般都自己显式定义比较好。
默认构造函数在以下几种情况下可以使用:
- 内置类型成员都具有缺省值(默认值);
- 类中全是自定义类型,如:Queue;
2. 析构函数
析构函数是一个特殊的成员函数,其名称与类名相同,前面加上波浪号(~)作为前缀。析构函数的主要作用是执行对象生命周期结束时的清理工作。当一个对象的生命周期结束时,无论是因为超出作用域、被显式删除,还是因为其所在的动态内存分配被释放,析构函数都会被自动调用。
概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特征
那么什么是析构函数呢?
它有以下特征:
- 函数名就是在类名前加~;
- 无参数、无返回类型;
- 一个类只能由一个析构函数。若没有显示定义,系统自动调用默认析构函数。注意:析构函数不能够重载;
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
Stack类 举例:
class Stack { public: //构造函数 Stack(int capacity=4) { _a = (int*)malloc(sizeof(int) * capacity); if (NULL == _a) { perror("malloc fail"); return; } _capacity = capacity; _size = 0; } //析构函数:显式定义 ~Stack() { //释放动态调用的空间资源 free(_a); _a = NULL; } private: int* _a; int _size; int _capacity; }; int main() { { Stack s1(10); } //s1作用域结束自动调用 ~Stack(); //不论显示定义,还是默认析构函数,都不需要显示调用; return 0; }
做了什么?
默认析构函数做了什么?
- 内置类型不做处理;
- 自定义类型调用它的析构函数;
内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可
注意事项:
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
- Queue类,成员全是自定义类型,会调用成员各自的析构函数。所以不用调用析构函数。
- 只有堆上的资源需要手动释放。
3.拷贝构造函数
拷贝构造函数是C++中的一种特殊的构造函数,用于创建一个新对象,该对象是已存在对象的副本。拷贝构造函数在多种情况下会自动被调用。
概念
概念:拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数有以下特征:
- 拷贝构造函数是构造函数的重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用。
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
Date类 举例:
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //拷贝构造函数 一个参数,必须是对象引用哦!!! Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; }; int main() { Date d1; //拷贝构造函数 Date d2(d1); return 0; }
做了什么?
默认拷贝构造:依旧是一样的,对内置类型进行浅拷贝,自定义类型调用它们自己的拷贝构造。不需要显示定义的类,比如:Date、Queue。需要显式定义的类,比如:Stack。因为有动态空间的开辟,所以需要深拷贝。注意事项:
这个比较容易错,大家请注意!
1.默认拷贝构造只是浅拷贝,需要深拷贝的对象需要显示定义拷贝构造函数。
举个Stack的例子:我们如果只是浅拷贝来处理Stack会出现错误。
比如:Stack s2(s1); 如果只是浅拷贝: s2._a=s1._a; 只是这种两个指针指向同一个开辟的空间;
- 问:这种会出现怎样的问题呢?
- 答:在函数结束的时候,s1调用一次析构函数,把空间释放了;s2也会在调用一次,此时原本空间已经被释放了,无法再次释放,会报错。
- 强调,显示定义拷贝构造函数,只能传一个参数并且必须为该类的引用。不能传值。
原因:如果进行传值传参,会在过程中调用拷贝构造去拷贝一个data临时对象,用于传值。如下图,便无限的调用下去,没有结束点,进入死循环。所以发生报错。
4.赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能新增加运算符;
- 保持运算符具有原有语意;
- 不改变运算符原有操作数个数;(比如+,就只能两个对象进行)
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符的重载
1. 赋值运算符重载格式 参数类型:const T&,传递引用可以提高传参效率 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值 检测是否自己给自己赋值 返回*this :要复合连续赋值的含义
Date类 举例:
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //拷贝构造函数 Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } //赋值运算符重载 Date& operator=(const Date& d) { if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } return *this; } private: int _year; int _month; int _day; }; int main() { Date d1(2024,5,5); //拷贝构造函数 Date d2(2024,5,6); Date d3; d3 = d1 = d2; return 0; }
注意事项:
- 赋值运算符重载是默认构造函数,不写,会进行简单的赋值和浅拷贝类似。通常需要为包含动态分配内存的类重载赋值运算符,以执行深拷贝操作。深拷贝意味着为新对象分配新的内存,并复制原对象所指向的内存内容,从而确保两个对象独立拥有自己的内存资源。
- 注意区分什么是赋值,什么时候是构造:
Date d1(2024,5,5); //构造 Date d2 = d1; //构造 Date d3; d3=d1; //赋值 辨别方法:已存在的对象初始化另一个对象叫构造;两个都存在的对象,则是赋值。 //小技巧:看前面有没有Date 类
5.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date { public : Date* operator&() { return this ; } const Date* operator&()const { return this ; } private : int _year ; // 年 int _month ; // 月 int _day ; // 日 };
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
总结
本章节介绍了四个主要的默认成员函数,还有两个不常用就没有过多介绍。