网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
_month = month;
_day = day;
}像这个重载函数是明确了我们要传参的,所以我们在实例化对象后就必须把参数写上去(虽然看着奇奇怪怪,但是没有办法,毕竟我们普通的调用,参数都是在函数名后面,而这个参数在实例化对象后面):
Date d2(2022, 5, 17);
来输出和我们先前的构造函数对比看看: ![](https://img-blog.csdnimg.cn/f6ca870c35684dffbed0b322a14300f0.png) * 注意:没有参数时我在调用的时候不能加上括号(),切忌!!构造函数尤为特殊 * 如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 ![](https://img-blog.csdnimg.cn/52f42c16fb93417c8e2c99b2a01526e8.png) 无参的情况下必须要像我们刚开始实例化的d1那样:
Date d1;
d1.Print();* 构造函数的重载我们推荐写成全缺省的样子:
//普通的构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//全缺省的构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}首先,普通的构造函数和全缺省的构造函数在不调用的情况下可以同时存在,编译也没有错误。但是在实际调用的过程中,会存在歧义。如下的调用:
class Date
{
public:
//普通的构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//全缺省的构造函数
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;
};
int main()
{
Date d1;
d1.Print();
}此时我实例化的d1到底是调用普通的构造函数?还是调用全缺省的构造函数?并且此段代码编译出现错误。何况我在没有调用函数的情况下编译是没错的。 **由此可见:**它们俩在语法上可以同时存在,但是使用上不能同时存在,因为会存在调用的歧义,不知道调用的是谁,所以一般情况下,我们更推荐直接写个全缺省版的构造函数,因为是否传参数可由你决定。传参数数量也是由你决定。 * **解释特性5:如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成**
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;
};
int main()
{
// 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
Date d;
d.Print();
}![](https://img-blog.csdnimg.cn/0802b6a6d3b3457db214e26404e8f064.png) 不是说好我不自己写构造函数,编译器会默认生成吗?为什么到这又是随机值了?这难道也算初始化?别急,搞清楚这个得先明白**默认构造函数**: **注意:C++把变量分成两种** * 内置类型/基本类型:int、char、double、指针…… * 自定义类型:class、struct去定义的类型对象 C++默认生成的构造函数对于内置类型成员变量不做处理,对于自定义类型的成员变量才会处理,这也就能很好的说明了为什么刚才没有对年月日进行处理(初始化),因为它们是内置类型(int类型的变量) 让我们来看看自定义类型是如何处理的。
class A
{
public:
A()
{
cout << “A()” << endl;
_a = 0;
}
private:
int _a;
};首先,这是一个名为A的类,有成员变量\_a,并且还有一个无参的构造函数,对\_a初始化为0。接着:
class Date
{
public:
void Print()
{
cout << _year << “-” << _month << “-” << _day << endl;
}
private:
int _year;
int _month;
int _day;
A _aa;
};
int main()
{
Date d;
d.Print();
}日期类里有三个内置类型,一个自定义类型(A\_aa),我们编译运行看看结果: ![](https://img-blog.csdnimg.cn/3588326ff148436aa385cc8f41f0170e.png) ![](https://img-blog.csdnimg.cn/433616dd9aae4e11ac508f001fe94dcf.png) 通过运行结果以及调试,也正验证了默认构造函数对自定义类型才会处理。这也就告诉我们,当出现内置类型时,就需要我们自己写构造函数了。 什么时候使用默认构造函数会凸显出其价值呢?就比如我们之前写的括号匹配这道题:
class Stack
{
public:
Stack()
{
_a = nullptr;
_top = _capacity;
}
private:
int* _a;
int _top;
int _capacity;};
class MyQueue
{
public:
//默认生成的构造函数就可以用了
void push(int x)
{}int pop()
{}
private:
Stack _S1;
Stack _s2;
};此时我队列里自定义类型\_s1和\_s2就不需要单独写初始化了,直接用默认的。但是如果栈里没有写构造函数,那么其输出的还是随机的,因为栈里的也是内置类型。就是一层套一层,下一层生效的前提是上一层地基打稳了。 总结: 1. 如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数 2. 如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。 * **解释特性6:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。** 特性6可以简单总结为**不用传参**就可以调用的即为默认构造函数 ![](https://img-blog.csdnimg.cn/053427249cdd43b39e35eb5b7b4e693d.png) 既然我默认构造函数只对自定义类型才会处理,那如果我不想自己再写构造函数也要对内置类型处理呢?我们可以这样做:(后续的博文会继续详细讲解这块) ![](https://img-blog.csdnimg.cn/93685324c2024d0cace3e3922362a33d.png)
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; }
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C 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; }
[外链图片转存中…(img-XpnB6rXM-1715820178658)]
[外链图片转存中…(img-nn2YCsoJ-1715820178658)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新