既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
此时我队列里自定义类型_s1和_s2就不需要单独写初始化了,直接用默认的。但是如果栈里没有写构造函数,那么其输出的还是随机的,因为栈里的也是内置类型。就是一层套一层,下一层生效的前提是上一层地基打稳了。
总结:
- 如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数
- 如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。
- 解释特性6:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
特性6可以简单总结为不用传参就可以调用的即为默认构造函数
既然我默认构造函数只对自定义类型才会处理,那如果我不想自己再写构造函数也要对内置类型处理呢?我们可以这样做:(后续的博文会继续详细讲解这块)
3、析构函数
析构函数概念
前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
析构函数特性
析构函数是特殊的成员函数。
其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值。
- 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 编译器生成的默认析构函数,对会自定类型成员调用它的析构函数
我们实际写一个析构函数看看:
~Date() { cout << "~Date()" << endl; }
带入示例再看看:
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; int _month; int _day; }; int main() { Date d1; return 0; }
首先,我实例化出的d1会调用它的默认构造函数进行初始化,其次,出了作用域后又调用其析构函数,这也就是为什么输出结果会是~Date()
析构的目的是为了完成资源清理,什么样的才能算是资源清理呢?像我这里定义的年月日变量就不需要资源清理,因为出了函数栈帧就销毁,真正需要清理的是malloc、new、fopen这些的,就比如清理栈里malloc出的
class Stack { public: //构造函数 Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); assert(_a); _top = 0; _capacity = capacity; } //析构函数 ~Stack() { free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack st; }
这里不难感慨C++的构造函数就像先前C语言常写的Init,而析构函数就像Destroy
- 看如下的题目:
int main() { Stack st1; Stack st2; }
现在我用类实例化出st1和st2两个对象,首先,st1肯定先构造,st2肯定后构造,这点毋庸置疑,那关键是谁先析构呢?
**答案:**st2先析构,st1后析构
**解析:**这里st1和st2是在栈上的,建立栈帧,其性质和之前一样,后进先出,st2后压栈,那么它肯定是最先析构的。所以栈里面定义对象,析构顺序和构造顺序是反的。
- 解释特性3:一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
若自己没有定义析构函数,虽说系统会自动生成默认析构函数,不过也是有要求的,和构造函数一样,内置类型不处理,自定义类型会去调用它的析构函数,如下:
class Stack { public: //构造函数 Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); assert(_a); _top = 0; _capacity = capacity; } //析构函数 ~Stack() { cout << "~Stack():" << this << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; class MyQueue { public: //默认生成的构造函数可以用 //默认生成的析构函数也可以用 void push(int x) {} int pop() {} private: Stack _S1; Stack _s2; }; int main() { MyQueue q; }
对于MyQueue而言,我们不需要写它的默认构造函数,因为编译器对于自定义类型成员(_S1和_S2)会去调用它的默认构造,Stack提供了默认构造,出了作用域,编译器会针对自定义类型的成员去默认调用它的析构函数,因为有两个自定义成员(_S1和_S2),自然析构函数也调了两次,所以会输出两次Stack()……
4、拷贝构造函数
拷贝构造函数概念
我们在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
int main() { Date d1(2022, 5, 18); Date d2(d1); return 0; }
能否让d2的值跟d1一样呢?也就是说我拿d1去初始化d2,此时调用的函数就是拷贝构造函数。
**拷贝构造函数:**只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
拷贝构造函数特性
拷贝构造函数也是特殊的成员函数,其特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
- 若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
如下即为拷贝构造函数:
Date(Date& d) { _year = d._year; _month = d._month; _day = d._day; }
- 解释特性2:拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
为什么传值传参会引发无穷递归呢?
我们先举一个普通的func函数作为例子:
//传值传参 void Func(Date d) {} int main() { Date d1(2022, 5, 18); Func(d1); return 0; }
此函数调用传参是传值传参。在C语言中,把实参传给形参是把实参的值拷贝给形参,而我的实参d1是自定义类型的,需要调用拷贝构造,传值传参是要调用拷贝构造的,但是我如果不想调用拷贝构造呢?就需要引用传参,因为此时d就是d1的别名
void Func(Date& d) {}
此时再回到我们刚才的例子:如若我不传引用传参,就会疯狂的调用拷贝构造:
Date(Date d) { _year = d._year; _month = d._month; _day = d._day; }
为了避免出现无限递归调用拷贝构造,所以要加上引用,加上引用后,d就是d1的别名,不存在拷贝构造了。同类型的传值传参是要调用拷贝构造的
Date(const Date& d) {} //最好加上const,对d形成保护
- **解释特性3:**若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
class Date { public: //构造函数 Date(int year = 1, 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; } */ void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; void Func(Date& d) { d.Print(); } int main() { Date d1(2022, 5, 18); Date d2(d1); Func(d1); d2.Print(); }
为什么我这里没有写拷贝构造函数,它也会自动完成拷贝构造呢?由此我们要深思,拷贝构造与构造和析构是不一样的,构造和析构都是针对自定义类型才会处理而内置类型不会处理,而默认拷贝构造针对内置类型的成员会完成值拷贝,浅拷贝,也就是像把d1的内置类型成员按字节拷贝给d2。
由此得知,对于日期类这种内置类型的成员是不需要我们写拷贝构造的,那是不是所有的类都不需要我们写拷贝构造呢?来看下面的栈类。
class Stack { public: //构造函数 Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); assert(_a); _top = 0; _capacity = capacity; } //不写拷贝构造,编译器调用默认拷贝构造 /* Stack(const Stack& st) { _a = st._a; _top = st._top; _capacity = st._capacity; } */ //析构函数 ~Stack() { cout << "~Stack():" << this << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: int* _a; int _top; int _capacity; }; int main() { Stack st1(10); Stack st2(st1); }
是这也写吗?
我们通过调试看到运行崩溃了,可见栈的拷贝构造函数不能像日期类一样不写而让编译器去调用默认拷贝构造(就是按照日期类的模式写了拷贝构造也会出错),因为此时的st1(指针)和st2(指针)指向的就是同一块空间,通过调试可以看出:
st1和st2指向同一块空间会引发一个巨大的问题:析构函数那出错,因为我st2会先析构,析构完后我st1再析构,不过我st1指向的空间已经被st2析构过了,因为它俩指向同一块空间,同一块空间我释放两次就会有问题。 出了析构存在问题,增删查改那也会有问题,这个后续会谈到。
其实刚才写的栈的拷贝构造就是浅拷贝,真正栈的拷贝构造应该用深拷贝来完成,此部分内容我等后续会专门出一篇博文详解,这里大家先简单接触下。
综上,我们可以得知,浅拷贝针对日期类这种是没有问题的,而类的成员若是指向一块空间的话就不能用浅拷贝了。
- 如果是自定义类型呢?让编译器自己生成拷贝构造会怎么样呢?
自定义类型的成员,去调用这个成员的拷贝构造。
class Stack { public: //构造函数 Stack(int capacity = 10) { _a = (int*)malloc(sizeof(int) * capacity); assert(_a); _top = 0; _capacity = capacity; } //不写拷贝构造,编译器调用默认拷贝构造 /* 浅拷贝 Stack(const Stack& st) { _a = st._a; _top = st._top; _capacity = st._capacity; } */ private: int* _a; int _top; int _capacity; }; class MyQueue { //默认拷贝构造针对内置类型的成员会完成值拷贝,浅拷贝 //自定义类型的成员,去调用这个成员的拷贝构造。 private: int _size = 0; Stack _S1; Stack _s2; }; int main() { MyQueue mq1; MyQueue mq2(mq1); }
其实这里同样是发生了浅拷贝,归根结底在于我栈的深拷贝还没写。
仔细看我的调试,mq2调用了其成员栈的拷贝构造,而我mq1和mq2的两个栈又发生了浅拷贝,它们对应的_S1和_S2指向的地址都是一样的,这也就意味着析构时同一块空间又会析构两次,出错。这里套了两层。
- **总结:**一般的类,自己生成拷贝构造就够用了,只有像Stack这样自己直接管理资源的类,需要自己实现深拷贝。
补充:
void TestDate2() { Date d1(2022, 5, 18); Date d3 = d1; //等价于 Date d3(d1); }
Date d3 = d1 是拷贝构造,不是赋值,拿一个对象初始化另一个对象是拷贝构造。
5、赋值运算符重载
运算符重载
如下的日期类:
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; };
我能否按照如下的方式对日期类的对象进行大小比较呢?
很明显是不可以的,从波浪线提示的警告就能看出。我们都清楚内置类型是可以直接进行比较的,但是自定义类型是不能直接通过上述的运算符进行比较的,为了能够让自定义类型使用各种运算符,于是就提出了运算符重载的规则。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 函数名字为:关键字operator后面接需要重载的运算符符号。
- 函数参数:运算符操作数
- 函数返回值:运算符运算后结果
- 函数原型:返回值类型 operator操作符(参数列表)
注意:
- 运算符重载函数的参数由运算符决定,比如运算符==应有两个参数,双操作数运算符就有两个参数,单操作数运算符(++或–)就有一个参数
就比如我现在写一个日期比较相等的运算符重载(传值传参会引发拷贝构造,所以要加上引用,最好再加上const):
bool operator==(const Date& d1, const Date& d2) //避免传值传参调用拷贝构造 { return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day; }
最好把运算符重载函数放到类里面(像图示的将成员变量变成公有也可以),因为类外不允许访问私有的成员变量(虽然可以通过友元来解决,不过现如今暂未学到)
仔细观察我的截图:毕竟我都写好了运算符重载,可是我调用运算符重载的方式怎么还能跟调用普通函数一样呢?与其这样还取名运算符重载又有何意义,所以真正的调用应该如下:
调用的时候直接和内置类型进行运算符操作那样,编译器会自动处理成调用运算符重载的样子
- **注意:**上述的运算符重载就算完成了吗?当然不是,按理说我们要把运算符重载函数放成类里的成员函数。
并且,这里的参数也不能像如上的方式写:
如若直接把运算符重载函数放到类里,编译器会报错(运算符函数的参数太多)。报错的原因就在于成员函数存在隐含的this指针。 这也就意味着实际的参数有3个,因此我们要少写一个参数:
bool operator==(const Date& d)//编译器会处理成 bool operator(Date* const this, const Date& d) { return _year == d._year && _month == d._month && _day == d._day; }
并且我在调用成员函数的时候也要做出改变:
if (d1.operator==(d2)) { cout << "==" << endl; }
和刚才一样,为了凸显出运算符重载的意义,我们调用的时候可以直接像内置类型一样操作运算符,因为编译器会帮我们处理:
if (d1 == d2)//编译器会处理成对应重载运算符调用if (d1.operator==(d2))或者if (d1.operator==(&d1, d2)) { cout << "==" << endl; }
如下:
- 现在,我们来写一个日期类的比较大小来练练手:
class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //日期比较大小 bool operator<(const Date& d) { if (_year > d._year || _year == d._year && _month > d._month || _year == d._year && _month == d._month && _day > d._day) return false; else return true; } private: int _year; int _month; int _day; }; int main() { Date d1(2022, 5, 17); Date d2(2022, 5, 20); if (d1 < d2) cout << "<" << endl; }
接下来,再来总结下运算符重载的注意点:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型(对自定义类型成员才可运算符重载)或者枚举类型的操作数
- 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
- .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
赋值运算符重载
前面我们已经学习了拷贝构造,是拿同类型的对象去初始化另一个对象,那如果我不想用拷贝构造呢?
int main() { Date d1(2022, 5, 17); Date d2(2022, 5, 20); Date d3(d1);//拷贝构造 -- 一个存在的对象去初始化另一个要创建的对象 d2 = d1; //赋值重载/复制拷贝 -- 两个已经存在的对象之间赋值 }
可不可以直接拿d1去赋值给d2呢?这就是我们要谈的赋值运算符重载。赋值运算符重载和上文的运算符重载是有点相似的。有了运算符重载的基础,写一个赋值重载还是很简单的。
//d2 = d1; -> d2.operator=(&d2, d1); void operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; }
但是这里的赋值重载是存在一定问题的,我们C语言的赋值是支持连等赋值的,如下:
int i = 0, j, k; k = j = i;
我们把i赋值给j,随后把j作为返回值再赋值给k。要知道C++是建立在C的基础上的,刚刚我们写的赋值重载支持连等吗?
很显然是不支持的,原因就是当我把d1赋值给d2后,没有一个返回值来赋给d3,这就导致出错。改正如下:
Date operator=(const Date& d) { _year = d._year; _month = d._month; _day = d._day; return *this; }
此外:这里的赋值重载还可进一步改进。
- **改进1:**刚才我们写的赋值重载是传值返回,传值返回会生成一个拷贝,会调用拷贝构造。如果出了作用域要让其对象还在我们就可以用传引用返回:
- **改进2:**有可能会存在这样的情况:d1=d1,像这样自己给自己赋值的情况还要再调用赋值重载函数属实没必要,所以我们还可以加个if条件判断。
修正如下:
//d2 = d1; -> d2.operator=(&d2, d1); Date& operator=(const Date& d) { if (this != &d) //不推荐写成if (*this != d) ,怕的是万一没有重载!=呢?,因为这里是对象的比较 { _year = d._year; _month = d._month; _day = d._day; } return *this; }
- 注意:
operator赋值也是默认成员函数,我们不写赋值重载,编译器也会默认生成,不过编译器完成的依旧是值拷贝或浅拷贝,像这个日期类就可以不写赋值重载:
赋值重载和拷贝构造一样,我们不写,它会对内置类型完成值拷贝,而像栈这样的就不能不写了,因为我们要写一个深拷贝的赋值重载才可以,理由和拷贝构造类似。 具体实现等真正谈到深拷贝再来。
- 补充:
void TestDate2() { Date d1(2022, 5, 18); Date d3 = d1; //等价于 Date d3(d1); }
Date d3 = d1 是拷贝构造,不是赋值,拿一个对象初始化另一个对象是拷贝构造。如下d2 =d1才是赋值:
void TestDate2() { Date d1(2022, 5, 18); Date d2(2022, 5, 20); Date d3 = d1; //等价于 Date d3(d1); 是拷贝构造 d2 = d1; //两个已经存在的对象才是赋值
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
,不过编译器完成的依旧是值拷贝或浅拷贝,像这个日期类就可以不写赋值重载:
赋值重载和拷贝构造一样,我们不写,它会对内置类型完成值拷贝,而像栈这样的就不能不写了,因为我们要写一个深拷贝的赋值重载才可以,理由和拷贝构造类似。 具体实现等真正谈到深拷贝再来。
- 补充:
void TestDate2() { Date d1(2022, 5, 18); Date d3 = d1; //等价于 Date d3(d1); }
Date d3 = d1 是拷贝构造,不是赋值,拿一个对象初始化另一个对象是拷贝构造。如下d2 =d1才是赋值:
void TestDate2() { Date d1(2022, 5, 18); Date d2(2022, 5, 20); Date d3 = d1; //等价于 Date d3(d1); 是拷贝构造 d2 = d1; //两个已经存在的对象才是赋值
[外链图片转存中…(img-oJVYQrOS-1715555496958)]
[外链图片转存中…(img-MaV9RGCy-1715555496959)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新