目录
一、类的六个默认函数
以下这个思维导图就是今天这篇博客的大纲了。
注意点:
以上6个默认的成员函数,假如我们不写的话,编译器会自动生成一个默认的。
但是我们实现了,其编译器就会调用我们实现的,就不会使用默认生成的了。
二、构造函数
定义:
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
注意事项:
需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征:
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载。(说明一个类可以有多个构造函数)
那么,构造函数有什么作用呢?
其实,它的主要作用就是补充了C语言中,我们在实例化一个对象的时候,总是容易忘记对其进行初始化的操作了。
具体实现例子:
#include<iostream>
using namespace std;
class Bag {
public:
//构造函数,无参
Bag() {
_pocket = nullptr;
_book = _pencil = 0;
}
//构造函数,有参
Bag(int n) {
_pocket = (int*)malloc(sizeof(int) * n);
if (nullptr == _pocket) {
perror("malloc失败");
exit(-1);
}
_book = n;
_pencil = 1;//至少得有一支笔吧
}
void Init(int book ,int pencil) {
this->_book = book;//这里的this是默认存在的,我们可以手动加
this->_pencil = pencil;
}
void Destory() {
//这里我们不实现Destory的具体功能,只是拿来做个参照。
cout << "Destory()启动了!" << endl;
}
void Add(int x,int y) {
_book += x;
_pencil += y;
}
private:
int* _pocket;//包的袋子的个数,用数组来存储
int _book;//声明,口头上的,没有开辟存储空间
int _pencil ;
};
int main() {
//构造函数的有参形式
Bag b1(4);
Bag b1;//正确构造函数的无参形式
//Bag b1();构造函数的无参形式不能这么写,编译器无法识别这是一个普通函数还是默认的构造函数。
b1.Add(3, 4);
b1.Add(2, 1);
b1.Destory();
return 0;
}
补充:
补充一:
class Bag {
public:
Bag() {
_pocket = nullptr;
_book = _pencil = 0;
}
Bag(int n = 4) {
_pocket = (int*)malloc(sizeof(int) * n);
if (nullptr == _pocket) {
perror("malloc失败");
exit(-1);
}
_book = n;
_pencil = 1;//至少得有一支笔吧
}
private:
int* _pocket;//包的袋子的个数,用数组来存储
int _book;//声明,口头上的,没有开辟存储空间
int _pencil;
};
int main() {
//构造函数的有参形式
Bag b1(4);
Bag b1;//正确构造函数的无参形式
return 0;
}
如上,假如我们在定义构造函数的时候,
一个构造函数是无参的。
一个是有参的,但是它的参数是全缺省的。
这样,我们在main函数要调用无参的构造函数时,会发生什么呢?
运行结果:
根据这里的结果,我们了解到,假如在定义一个有参的构造函数的时候,要注意全缺省,容易和无参的构造函数混淆。
补充二:
#include<iostream>
using namespace std;
class Bag {
public:
//默认生成的构造函数,对内置类型不处理。
void Print() {
cout << "书包里有" << _pencil << "支笔" << endl;
}
private:
//基本类型/内置类型
int* _pocket;//包的袋子的个数,用数组来存储
int _book;//声明,口头上的,没有开辟存储空间
int _pencil;
};
int main() {
Bag b1;//正确构造函数的无参形式
b1.Print();
return 0;
}
运行结果:
这里我们就有疑问了?
我们这里没有自己来实现构造函数。
根据以上总结我们可知,编译器会自动生成一个默认的构造函数。但是看起来其好像没啥作用啊!其结果由上面这一段代码及输出结果可知其仍然是随机值。
这其实是C++设计的一个没有考虑到的点,的确在实际使用中存在一点问题。
其解决办法在C11给出了,即:内置类型成员变量在类中声明时可以给默认值。
C++把类型分为内置类型(基本类型)和自定义类型。
1.内置类型就是语言提供的数据类型,如:int,char,float....
2.自定义数据类型就是我们自己使用的struct/class/union等
3.且C++早期语法说默认生成的构造函数对:
3.1内置类型成员不做处理
3.2 而对自定义类型的成员,会去调用它的默认构造(不用传参数的构造)。
解答:
至此,我们总算了解了为啥上面的代码 _pencil 最终运行出来的结果为那么一大串数字了。
原因就是其类型是内置成员类型int,编译器对其不做处理的。
补充三:
对自定义类型的成员,会去调用它的默认构造(不用传参数的构造)
其上具体实现的实例:
#include<iostream>
using namespace std;
class Bag {
public:
/*Bag() {
_pocket = nullptr;
_book = 10;
_pencil = 5;
}*/
void Print() {
cout << "书包里有" << _pencil << "支笔" << endl;
}
private:
int* _pocket;//包的袋子的个数,用数组来存储
int _book;//声明,口头上的,没有开辟存储空间
int _pencil;
};
class Table {
public:
//默认生成的构造函数,对自定义类型,会调用其默认构造函数
void Lay() {
}
Bag _B1;
Bag _B2;
};
int main() {
Bag b1;//正确构造函数的无参形式
b1.Print();
Table T;
T.Lay();
return 0;
}
当我们调试到Table类中的时候,通过监视可以看到:
其不再是一个随机数了,而是-1.
补充四:
以上代码无法编译通过。
因为对象在实例化的时候一定会调用构造函数,如果我们自己不定义,编译器就是自动生成一个默认的。但如果我们自己定义了,就会调用我们自己定义的。
以上由于我们自己定义了构造函数,而且是带参数的,所以我们在实例化对象的时候,必须要带参数。
三、析构函数
定义:
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
特征:
1.析构函数名是在类名前加上字符 ~ (小波浪号)
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
4.对象生命周期结束时,C++编译系统系统自动调用析构函数。
具体例子:
#include<iostream>
using namespace std;
class Bag {
public:
//构造函数,无参
Bag() {
_pocket = nullptr;
_book = _pencil = 0;
}
//构造函数,有参
Bag(int n) {
_pocket = (int*)malloc(sizeof(int) * n);
if (nullptr == _pocket) {
perror("malloc失败");
exit(-1);
}
_book = n;
_pencil = 1;//至少得有一支笔吧
}
void Init(int book ,int pencil) {
this->_book = book;//这里的this是默认存在的,我们可以手动加
this->_pencil = pencil;
}
void Destory() {
//这里我们不实现Destory的具体功能,只是拿来做个参照。
cout << "Destory()启动了!" << endl;
}
void Add(int x,int y) {
_book += x;
_pencil += y;
}
//析构函数
~Bag() {
free(_pocket);
_pocket = nullptr;
_pencil = _book = 0;
}
private:
int* _pocket;//包的袋子的个数,用数组来存储
int _book;//声明,口头上的,没有开辟存储空间
int _pencil ;
};
int main() {
//构造函数的有参形式
Bag b1(4);
Bag b1;//正确构造函数的无参形式
//Bag b1();构造函数的无参形式不能这么写,编译器无法识别这是一个普通函数还是默认的构造函数。
b1.Add(3, 4);
b1.Add(2, 1);
//b1.Destory();//由于我们这里已经定义好了析构函数,所以就不用调用Destory函数了
//编译器在对象销毁时会自动调用析构函数了
return 0;
}
补充:
一起来看看下面这段代码:
#include<iostream>
using namespace std;
class Bag {
public:
~Bag() {
cout << "书包已经放好了!" << endl;
}
private:
int* _pocket;//包的袋子的个数,用数组来存储
int _book;//声明,口头上的,没有开辟存储空间
int _pencil;
};
class Table {
public:
int _cup;
int _fruit;
Bag _B1;
};
int main() {
Table T;
return 0;
}
最终输出结果为:书包已经放好了!
在main方法中根被没有直接创建Bag类的对象,为什么最后会调用Bag类的析构函数呢?
因为:main方法中创建了Table对象T,而T中包含三个成员变量,其中_cup 、_fruit两个是内置成员类型,销毁时不需要资源清理,最后系统直接将其内存回收即可。而_B1是Bag类对象,所以在T销毁时,要将其内部包含的Bag类的_B1对象销毁,所以要调用Bag类的析构函数。
但,注意,main函数中不能直接调用Bag类的析构函数,因实际要释放的是Table类对象,所以main函数会调用Table类的编译器默认提供的析构函数,用来保证在Table对象销毁时,其内部的自定义对象可以正确的销毁掉。
小总结:
如果类中没有申请资源时,析构函数可以不写。但要是有申请,则必须写上。
四、拷贝构造函数
场景导入:
在实际应用中,我们可能会需要要创建一个与已存在对象一样的新对象。那么有没有什么简单的方法呢?
拷贝构造函数定义:
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
特征:
1.拷贝构造函数是构造函数的一个重载形式。
2. 楼贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错因为会引发无穷递归调用。
具体例子:
例一:
class Bag {
public:
Bag(int book = 10, int pencil = 2) {
_book = book;
_pencil = pencil;
}
Bag(Bag B) {
_book = B._book;
_pencil = B._pencil;
}
void Print() {
cout << "书包已经放好了!" << endl;
}
private:
int* _pocket;//包的袋子的个数,用数组来存储
int _book;//声明,口头上的,没有开辟存储空间
int _pencil;
};
int main() {
Bag B1(1,2);
Bag B2(B1);
return 0;
}
以上这段代码无法跑过。
会导致无穷递归调用。
其实也比较容易理解为什么会产生无穷递归调用了,
举个例子,当然不一定能编译出来,但是用于辅助理解还行。
就是假如有一个函数Func(),这时,假如在main函数中写了Func(Func());
这一段代码,是不是就会产生无穷递归了。
解决办法:
例二:
传值传参的注意事项:
class Bag {
public:
Bag(int book = 10, int pencil = 2) {
_book = book;
_pencil = pencil;
}
Bag(Bag& B){
_book = B._book;
_pencil = B._pencil;
}
void Print() {
cout << "书包已经放好了!" << endl;
}
private:
int _book;//声明,口头上的,没有开辟存储空间
int _pencil;
};
//传值传参
//内置类型,编译器可以直接拷贝
//自定义类型的拷贝,需要调用拷贝构造
void Fuc1(Bag b) {
}
//传引用传参
void Fuc2(Bag& b) {
}
int main() {
Bag B1(1,2);
Fuc1(B1);
Fuc2(B1);
return 0;
}
以上Fuc1传值传参的代码运行没问题。
主要是因为我去掉了Bag类中原有的int*成员变量,这样就可以实现浅拷贝了 。
可要是我没去掉Bag类中的int*成员变量,则需要使用深拷贝。不过这里使用拷贝构造也能解决,因为引用的话就是直接公用一块内存空间了,避免了深拷贝。
也可以直接使用传引用传参来解决,更为方便。
小补充:
补充1:
int main() {
Bag B1(1,2);
Bag B2 = B1;//这样也可以实现对象拷贝了。
return 0;
}
补充2:
注意,在实现拷贝构造时,注意参数要设置为const。
不然,可能会产生赋值错误的情况。
实现一个函数,获取任意天后的日期。
#include<iostream>
#include <assert.h>
using namespace std;
class Data {
public:
Data (int year,int month,int day) {
_year = year;
_month = month;
_day = day;
}
int GetMonthDay(int year, int month) {
assert(month > 0 && month < 13);
int monthArray[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) {
return 29;
}
else {
return monthArray[month];
}
}
Data GetAfterDay(int x) {
Data tmp = *this;
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {
//进位
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13) {
tmp._year++;
tmp._month = 1;
}
}
return tmp;//this指的就是本身,就是这个Data类
}
void Print() {
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Data d1(2023, 10, 30);
Data d2 = d1.GetAfterDay(100);
d1.Print();
d2.Print();
}
运行结果:
补充:
//相当于 "+" 的形式,不会改变自身的值
//使用的是传值返回。
//且注意返回的不是tmp,因为传值返回时作用域会被销毁,其内部数据都会被清理
//所以这里return tmp的下一步会跑到Data类的拷贝构造中去产生一个临时对象了来使用了
//这里不会改变d1的原来数值,但会增加空间的开辟
Data GetAfterDay01(int x) {
Data tmp = *this;
tmp._day += x;
while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {
//进位
tmp._day -= GetMonthDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13) {
tmp._year++;
tmp._month = 1;
}
}
return tmp;
}
//是+=的形式的。
//并且这里可以使用引用返回,因为其中的*this在函数掉用完后不会被销毁,节省了拷贝构造的空间
//但这里仍然会改变d1的数值,但减少了空间的开辟。
Data& GetAfterDay02(int x) {
_day += x;
while (_day > GetMonthDay(_year, _month)) {
//进位
_day -= GetMonthDay(_year, _month);
++_month;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}