既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
return 0;
}![](https://img-blog.csdnimg.cn/eb0537bc5e4448239ddcd301569c5397.png) 执行完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修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征
拷贝构造函数也是特殊的成员函数,其特征如下:
拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
对于get1,形参是实参的一份的临时拷贝,get2,形参是实参的别名
参数也是类类型,用d1去初始化d即传参的时候,就需要构造函数(初始化函数),get1的构造是拷贝构造,get2还没传参的时候就已经构造好了,在Date d1这条语句里已经构造好了
验证上面的结论
这条语句之后按F11,直接跳到拷贝构造这里来
先构造d1,再给get1构造
若想把d1拷贝过去,这俩种写法都可以
若拷贝构造函数这样写,则会进入死循环,因为形参必须是类类对象的引用
解决这种传参问题有俩种办法:1.引用
2.指针(这种方式不是拷贝构造),虽然也能完成,但是不建议这种方法,这种方法比较奇怪,更容易出现错误
有时候拷贝构造函数容易写错,所以要加const
正确形式
未定义拷贝构造
对内置类型按直拷贝进行拷贝的:就是一个字节,一个字节拷贝过去
对自定义类型,调用自定义类型自己的拷贝构造函数完成
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
拷贝构造还有深浅拷贝问题,后面的博客里会写浅拷贝举例
这里能正常拷贝,但是在析构的时候会崩溃
这是因为这里执行了俩次析构函数,由于这里是直拷贝,st和st1指向了同一块空间,而这快空间被释放了俩次, 因为这里是内置类型,指针是内置类型,把*array所指向的空间给st1拷贝过去了,然后st1和st指向了同一块空间,析构的时候又free了俩次,直接崩溃
拷贝构造在传值传参和传引用传参的区别
#include<iostream> 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<iostream> 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<<(d1d2)<<endl;转换为cout<<operate(d1,d2)<<endl;
本质是函数调用
编译器再工作时,如果发现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; }
赋值运算符重载
- 赋值运算符重载格式
参数类型: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<iostream> 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这条语句编译器是不会对其报错的
赋值运算符只能重载成类的成员函数不能重载成全局函数
写成全局的会报错
原因**:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重只能是类的成员函数。**
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意**:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算**符重载完成赋值。
屏蔽掉赋值重载函数后,仍然可以拷贝
添加一个类进行测试
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重只能是类的成员函数。**
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意**:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算**符重载完成赋值。
屏蔽掉赋值重载函数后,仍然可以拷贝
添加一个类进行测试
[外链图片转存中…(img-1SutqjCb-1715735619694)]
[外链图片转存中…(img-FcW8gTGF-1715735619694)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!