目录
类的6个默认成员函数
构造函数
析构函数
析构函数使用场景
场景1:不需要清理,但会执行默认析构函数
场景2:需要析构函数
默认生成析构函数特点
构造函数和析构函数的顺序问题
拷贝构造函数
特征
拷贝构造在传值传参和传引用传参的区别
拷贝构造在传值返回和传引用返回的区别
运算符重载
运算符重载应用
赋值运算符重载
赋值重载连续赋值
类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。构造函数
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数
文章图片
我们在使用类时,一般会先使用初始化函数,但是有人会忘记进行初始化,有些情况下忘记初始化会导致程序崩溃或出现一些错误析构函数
文章图片
C++中为了解决这个问题,提出了构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:
1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。
4. 构造函数可以重载。
5.一个类里面的默认构造只能有一个,不然调用的时候编译器会凌乱
构造函数的函数名和类名相同,此时并未使用初始化函数,传参的时候直接写到对象名后面
文章图片
文章图片
构造函数也满足缺省参数的特性
文章图片
这俩种同时存在会存在歧义,因为如果不传参数,此时编译器会凌乱,不知道调用那个函数
文章图片
以下这种情况也会报错,这是因为有俩个构造函数
文章图片
这种情况也是因为有俩个构造函数
文章图片
一般这三种方式,只写一个
构造函数写栈
文章图片
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成(如果没有构造函数,编译器自动生成一个构造函数,如果有构造函数,编译器就不会自动生成)
文章图片
此时会自动默认生成,但是产生的值是随机值
C++类型分类:1.内置类型:int/double/char/指针等等
2.定义类型:struct/class主要是类类型
默认生成构造函数,对内置类型不做处理,对自定义类型成员回去调用它的默认构造函数,
这是C++早期的一个缺陷,默认生成构造函数,本来应该内置类型也一并处理
C++11打了补丁这样做
文章图片
class Date { public: Date(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 ; }; typedef int DataType; class Stack { public: Stack(int capacity=4) { cout << "Stack(int capacity = 4)" << endl; _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; }_size = 0; _capacity = capacity; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } private: DataType* _array; int _capacity; int _size; }; // C++类型分类: // 内置类型/基本类型:int/double/char/指针等等 // 自定义类型:struct/classclass MyQueue { private: Stack _st1; Stack _st2; }; int main() { MyQueue q(); return 0; }
文章图片
文章图片
默认构造函数有三类:
1.我们不写,编译器自动生成的那个
2.我们自己写的全缺省构造函数
3.我们自己写的,无参的构造函数
默认构造函数特点:不传参数就可以调用
文章图片
当我们做修改后,此时程序会报错,因为这里,既不是无参的,也不是全缺省的,而且由于构造函数名和类同名,这里相当于我们自己写了一个构造函数,所以编译器在这里不会生成默认构造函数
文章图片
文章图片
文章图片
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作析构函数使用场景 场景1:不需要清理,但会执行默认析构函数
析构函数是特殊的成员函数,其特征如下:
1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 一个类只能有一个析构函数(这是因为没有参数)。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。
场景2:需要析构函数class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } ~Date() { //日期类的没什么需要清理的 cout << "~Date()" << endl; } private: int _year = 1; // 注意这里不是初始化,给缺省值 int _month = 1; int _day = 1; }; void fun() { Date d1; } int main() { fun(); return 0; }
此时程序执行完fun函数
文章图片
再按F11,程序直接来到了析构函数这里
文章图片
最终结果
文章图片
默认的析构函数什么都不做
默认的析构函数不会处理这里,这里的数组在堆上默认生成析构函数特点
typedef int DataType; class Stack { public: Stack(int capacity=4) { cout << "Stack(int capacity = 4)" << endl; _array = (DataType*)malloc(sizeof(DataType) * capacity); if (NULL == _array) { perror("malloc申请空间失败!!!"); return; } _size = 0; _capacity = capacity; } void Push(DataType data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { free(_array); _capacity = _size = 0; _array = nullptr; } private: DataType* _array; int _capacity; int _size; }; void fun() { Date d1; Stack st; } int main() { fun(); return 0; }
文章图片
执行完fun函数后,先进入Stack的析构函数,再进入Date的析构函数
之前博客里写栈需要写初始化Init函数和释放空间的Destory函数,而在C++中写一个构造函数和析构函数不需要调用即可,析构函数就会代替Destory的作用来释放空间(要自己写)
跟构造函数类似,内置类型不处理,自定义类型处理,自定义类型会调用自己的构造 ,指针属于内置类型,指针不处理,因为指针有的是指向动态开辟空间,有的指向一个数组,还有文件指针构造函数和析构函数的顺序问题
当写栈,队列,链表,顺序表,二叉树用自己写的析构函数就比较方便
文章图片
先构造的后析构,后构造的先析构
这是因为这里的内容存在栈中,要满足先进后出
文章图片
main 先调用f1,f1再调用f2,然后f2销毁返回f1,满足先进后出
文章图片
先初始化全局的,当进入main函数后按顺序初始化
对于析构,变量销毁后就进行析构,aa2和aa1在栈区,栈帧结束后要清理资源先清理aa2,再清理aa1,全局变量和静态变量在函数结束以后才销毁,之后清理资源,所以先清理aa0再清理aa3
文章图片
构造顺序:3 0 1 2 4 1 2
析构顺序:2 1 2 1 4 0 3
拷贝构造函数
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。特征
拷贝构造函数也是特殊的成员函数,其特征如下:拷贝构造在传值传参和传引用传参的区别
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
文章图片
对于get1,形参是实参的一份的临时拷贝,get2,形参是实参的别名
参数也是类类型,用d1去初始化d即传参的时候,就需要构造函数(初始化函数),get1的构造是拷贝构造,get2还没传参的时候就已经构造好了,在Date d1这条语句里已经构造好了
验证上面的结论
文章图片
这条语句之后按F11,直接跳到拷贝构造这里来
文章图片
【C++|C++——类和对象2|构造函数|析构函数|拷贝构造函数|运算符重载|赋值运算符重载|赋值运算符连续赋值】
先构造d1,再给get1构造
文章图片
文章图片
若想把d1拷贝过去,这俩种写法都可以
若拷贝构造函数这样写,则会进入死循环,因为形参必须是类类对象的引用
文章图片
文章图片
解决这种传参问题有俩种办法:1.引用
文章图片
2.指针(这种方式不是拷贝构造),虽然也能完成,但是不建议这种方法,这种方法比较奇怪,更容易出现错误
文章图片
文章图片
文章图片
有时候拷贝构造函数容易写错,所以要加const
文章图片
正确形式
文章图片
未定义拷贝构造
文章图片
文章图片
文章图片
对内置类型按直拷贝进行拷贝的:就是一个字节,一个字节拷贝过去
对自定义类型,调用自定义类型自己的拷贝构造函数完成
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
拷贝构造还有深浅拷贝问题,后面的博客里会写
浅拷贝举例
文章图片
文章图片
文章图片
这里能正常拷贝,但是在析构的时候会崩溃
文章图片
这是因为这里执行了俩次析构函数,由于这里是直拷贝,st和st1指向了同一块空间,而这快空间被释放了俩次, 因为这里是内置类型,指针是内置类型,把*array所指向的空间给st1拷贝过去了,然后st1和st指向了同一块空间,析构的时候又free了俩次,直接崩溃
拷贝构造在传值返回和传引用返回的区别#include
using namespace std; class A { public: A(int a = 0) { _a = a; cout << "A(int a=0)>>" <<_a<< endl; } A(const A& aa) { _a = aa._a; cout << "拷贝构造A(int a=0)>>" << _a << endl; } ~A() { cout << "~A()>>" <<_a<< endl; } private: int _a; }; void func1(A aa) {} void func2(A& aa) {}
文章图片
传值传参有一次拷贝构造,这个拷贝构造是针对aa1的拷贝构造
文章图片
引用传参没有拷贝构造
运算符重载#include
using namespace std; class A { public: A(int a = 0) { _a = a; cout << "A(int a=0)>>" <<_a<< endl; } A(const A& aa) { _a = aa._a; cout << "拷贝构造A(int a=0)>>" << _a << endl; } ~A() { cout << "~A()>>" <<_a<< endl; } private: int _a; }; A fun3() { static A aa; return aa; } A& fun4() { static A aa; return aa; } int main() { A aa1(1); fun3(); cout << endl; fun4(); return 0; }
文章图片
传值返回时,进行了一次拷贝构造
第一个:A(int a=0)>>1是A aa1(1)的默认构造
第二个:A(int a=0)>>0是fun3函数里面对 static A aa的默认构造
拷贝构造A(int a=0)>>0是fun3函数执行return语句时,对aa的拷贝构造
这是因为传值返回在返回时,会拷贝一份aa,然后返回的是拷贝的aa,对于aa本身,在函数结束后就会销毁,对于这份拷贝的aa,就要调用拷贝构造函数
对于拷贝的aa,也是有生命周期的,它的生命周期在main函数fun3(); 这一行,当这一行调用结束之后,就要拷贝的aa进行析构
稍作修改,使观察更清晰
文章图片
对于引用返回,不需要拷贝构造函数,直接调用构造函数,调用过程如下图
文章图片
析构过程如下
文章图片
这里aa(3)和aa(4)本身在静态区,所以后析构,如果aa出了fun4()的作用域就销毁,那么引用返回就有问题,前面博客里有提到过
这里有俩个日期,d1和d2,我们比较它们的大小关系运算符重载应用
文章图片
C++中规定:内置类型可以直接使用运算符运算,编译器知道如何运算,自定义类型无法直接使用运算符,编译器不知道如何运算
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表),返回值类型由运算符特点决定,如比较大小一般返回true或false,若想知道差值,返回int
注意:
不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this.*::sizeof?:. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
文章图片
我们可这样来对日期判断是否相等,但是这里会报错,因为日期这些是私有的
文章图片
解决方法:
1.写一个函数来获取这些私有数据 ,月和天也同理
文章图片
文章图片
将上面的私有数据改为共有,再进行测试
using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; cout << "构造函数" << endl; } //Date(const Date& d)//拷贝构造函数 //{ // _year = d._year; // _month = d._month; // _day = d._day; //} void Print() { cout << _year << "-" << _month << "-" << _day << endl; } int _year = 1; // 注意这里不是初始化,给缺省值 int _month = 1; int _day = 1; }; int main() { Date d1(2022,7,23); Date d2(2022, 8, 23); cout<<(d1 == d2) << endl; return 0; }
文章图片
编译器实际会将cout<<(d1==d2)<本质是函数调用
文章图片
编译器再工作时,如果发现d1,d2是内置类型,直接转换为相对应的指令,如果不是内置类型就看它是否满足运算符重载的条件,如果满足就执行
为什么不直接写一个调用函数呢?
文章图片
如果写成这样,就没什么价值了,写成运算符重载,就能像内置类型一样去调用运算符,写成运算符重载可读性更好,如果写成函数,有人会不规范写函数名,导致其他人看不懂
文章图片
在传参的时候,如果没有引用就会进行拷贝构造,如果是深拷贝,就会很麻烦,所以在传参的时候要引用,为了防止别人写错,最好加上const
,
文章图片
但运算符重载,还存在一个问题,就是内置类型,一般是私有,上面为了演示效果我们改为了共有,现在改回来,但是会报错
文章图片
为了解决这个问题,我们把运算符重载函数写道类里面,还会报错,参数太多
文章图片
参数太多是因为,还有一个隐藏的参数this指针
我们做以下修改即可
文章图片
_year==d2._year,这里的_year实际是this->_year,this就是d1
文章图片
对于日期类,我们可以比较大小,也可以去查询N天以后/以前是几几年几月几号赋值运算符重载
思路:1.直接把天数加到day
2.若day超过了该月的总天数,用day-该月总天数,再给月+1,12月要把1,给年加1
int GetMonthDay(int year, int month) { static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; //多次调用的时候,每次都要创建数组,现在改为静态,放到静态区if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))//闰年2月 { return 29; } else { return days[month]; } } Date operator+(int day) {_day += day; while (_day > GetMonthDay(_year,_month)) { _day -= GetMonthDay(_year, _month); ++_month; if (_month == 13)//12月特殊处理 { _month = 1; _year++; } } }
这样写需要一个返回值,但是我们把日期全部加到了_year,_month,_day,因此要把这些给传回去,我们直接return *this,this是指针,*this是对象
文章图片
先测试一下
文章图片
文章图片
现在写的这个,改变了d1,如果不想改变d1,把结果返回回去,做如下修改,main函数可以这样写,用一个东西来接收,符合拷贝构造
文章图片
文章图片
我们拷贝一份d1,就行,这样就不会改变d1了
int GetMonthDay(int year, int month) { static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; //多次调用的时候,每次都要创建数组,现在改为静态,放到静态区if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))//闰年2月 { return 29; } else { return days[month]; } } Date operator+(int day) { Date ret(*this); ret._day += day; while (ret._day > GetMonthDay(ret._year,ret._month)) { ret._day -= GetMonthDay(ret._year,ret. _month); ++ret._month; if (ret._month == 13)//12月特殊处理 { ret._month = 1; ret._year++; } } return ret; } int main() { Date d1(2022,7,23); Date ret = (d1 + 50); Date ret1(d1 + 50); return 0; }
文章图片
1. 赋值运算符重载格式赋值重载连续赋值
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
C语言中把一个数的值,赋值给另一个数,用=就可以,C++中把一个类的值,赋值给另一个类需要用到赋值重载
如这里,要把d3赋值给d1
void TestDate1() { Date d1(2022, 7, 24); Date d2(d1); //拷贝构造 Date d3(2022, 8, 24); d1 = d3; //赋值 } int main() { TestDate1(); return 0; }
#include
using namespace std; class Date { public: //构造函数会频繁调用,所以放在类里面作为inline Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } private: int _year; int _month; int _day; };
文章图片
main函数里d1=d3; 会被转换成d1.operatpr=(&d1,d3);
传d1地址是因为有this指针
文章图片
写成连续赋值就会报错
文章图片
对于普通变量这种连续赋值是可以的
文章图片
连续赋值是这样的:k=j=i,把i赋值给j,然后把j的返回值赋值给k
d2=d1=d3,也遵循上面的道理
d3赋值给d1,d1的返回值赋值给d2
这里应该这样修改,this是d1的指针(地址),*this就是d1
文章图片
文章图片
此时不会报错 ,如果不引用传参就要调用好多次拷贝构造
为了防止有人写成d2=d2,我们加上判断条件,d2=d2这条语句编译器是不会对其报错的
文章图片
赋值运算符只能重载成类的成员函数不能重载成全局函数
文章图片
写成全局的会报错
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重只能是类的成员函数。
文章图片
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
屏蔽掉赋值重载函数后,仍然可以拷贝
文章图片
添加一个类进行测试
文章图片
文章图片
此时调用,结论跟上面一模一样,自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
文章图片
其实相面的time也不需要赋值重载,会默认生成一个赋值重载
对于栈这样的类型,需要写赋值重载
#include"date.h" typedef int DataType; class Stack { public: Stack(size_t capacity = 10) { _array = (DataType*)malloc(capacity * sizeof(DataType)); if (nullptr == _array) { perror("malloc申请空间失败"); return; } _size = 0; _capacity = capacity; } void Push(const DataType& data) { // CheckCapacity(); _array[_size] = data; _size++; } ~Stack() { if (_array) { free(_array); _array = nullptr; _capacity = 0; _size = 0; } } private: DataType* _array; size_t _size; size_t _capacity; }; int main() { Stack st1; Stack st2; st2.Push(1); st2.Push(2); st1 = st2; return 0; }
因为默认的赋值重载,会对内置类型,实行直拷贝,指针也属于内置类型,如果对指针直拷贝,会导致俩个指针指向同一块空间,在析构的时候会析构俩次,所以会报错
文章图片
对于MyQueue这样的类,可以不写赋值重载,因为如果栈的赋值重载写好了,MyQueue直接可以躺平
文章图片
对于拷贝构造和赋值重载一些类需要些,比如栈
一些类不需要写,如Date这样的类,默认完成直拷贝/浅拷贝,如mYqueue不需要写,默认会调用自定义类型Stack的拷贝和赋值
推荐阅读
- C++|猿创征文|C++——类和对象4| 构造函数体赋值|初始化列表explicit关键字|匿名对象|static成员|静态成员变量|静态成员函数| static相关习题|友元
- C++|C++——类和对象3|日期类型|Cout运算符重载|Cin运算符重载|const成员|
- 服务器|Springboot+redis+Vue在Linux服务器下进行部署
- Linux|远程访问 Linux 服务器中的 redis 数据库(腾讯云服务器 CentOS 8.0)
- linux|CSDN IT冷知识(每日更新)
- ubuntu|从零开始搭建oj(ubuntu)
- c++|【10分】B. 访问数组元素(引用)
- 树莓派|树莓派接入阿里云IOT平台1(Python模拟数据上传)
- linux|Linux Shell重定向 管道命令 awk编程 sed文件操作高阶函数