欢迎来到 Claffic 的博客
专栏:《是C++,不是C艹》
前言:
在完成类与对象的认识后,我们接着学习类与对象的第二部分:默认成员函数,它包括构造函数,析构函数,拷贝构造,赋值重载,普通对象取地址和const对象取地址重载,放心,这一期不会都讲给你的,让我们来慢慢研究构造函数和析构函数:
注:
你最好是学完了C语言,并学过一些初阶的数据结构。
(没有目录) ヽ( ̄ω ̄( ̄ω ̄〃)ゝ
上一次我们提到了空类,里面没有成员,编译器给了它一个字节表示它存在,
class Date {};
❓那空类中真的什么也没有吗?
并不是的,编译器可是让你省心的存在:
类为空时,编译器会自动生成6个默认成员函数。
❓生成是会生成,那它们有什么用呢?
有个例子:
上次我们提到了 Stack 的实现,其中有初始化函数 StackInit 和销毁函数 StackDestroy
现在告诉你个好事:构造函数可以代替 StackInit ,析构函数可以代替 StackDestroy 。
重点:它们都是编译器自动生成的,也就是说 像这种初始化函数和销毁函数不需要自己造了
当然不止这些,还有拷贝赋值和取地址重载等,大大方便了我们。
那么接下来,就由“构造函数”讲起:
为了更方便地讲解,我们先定义一个 Date 类:
#include
using namespace std;
class Date
{
public:
void SetDate(int year, int month, int day)
{
_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.SetDate(2023, 6, 1);
d1.Print();
Date d2;
d2.SetDate(2023, 6, 4);
d2.Print();
return 0;
}
对于 Date 类来说,我每次要初始化设置信息,就要调用一次 SetDate 函数,那岂不是很烦?
❓那有没有一种办法,自动将我要传递的数值传递进去呢?
❗还真有,那就是构造函数,让我们有请构造函数登场!!!
构造函数 是一个 特殊的成员函数,名字与类名相同 ,创建类类型对象时由编译器自动调用
能保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
️构造函数的意义在于初始化对象,而不是给对象开辟空间(虽然名字叫做构造)
构造函数是特殊的成员函数,其特性如下:
① 函数名与类名相同;
② 没有返回值;
③ 构造函数可以重载;
④ 对象实例化时编译器自动调用对应的构造函数:
如上方代码中的 Date d1
构造函数就在这个时候被调用了
让我们康康它具体是怎么使用的:
#include
using namespace std;
class Date
{
public:
Date() // 无参构造函数
{
_year = 1;
_month = 0;
_day = 0;
}
Date(int year, int month, int day) // 带参构造函数
{
_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();
Date d2(2023, 6, 4); // 调用带参构造函数
d2.Print();
return 0;
}
️️输出结果:
不给参数就调用无参构造,给参数就调用带参构造
你以为你会了?其实有很多需要注意的地方:
注意:
Ⅰ构造函数是特殊的函数,不是普通的成员函数,所以不可以这样调用
#include
using namespace std;
class Date
{
public:
Date()
{
_year = 1;
_month = 0;
_day = 0;
}
Date(int year, int month, int day)
{
_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.Date; // 不可以这样调用
d1.Print();
return 0;
}
️️输出结果:
Ⅱ无参构造对象,对象后面不用跟括号,否则就成了函数的声明
#include
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023, 6, 5); // 带参就这样,括号加参数
d1.Print();
Date d2(); // 调用不带参构造函数,不要加括号
d2.Print(); // 实际上这个类没有创建出来
return 0;
}
报错:
Ⅲ 带参构造,该传递多少参数就传递多少参数
如上三点要注意!
我们聊完了构造函数,就要说说默认构造函数了:
如果你没有在类中定义构造函数(类中未显式定义),那么C++编译器就会自动生成一个无参的默认构造函数。
#include
using namespace std;
class Date
{
public:
// 没有显式定义构造函数
/*Date(int year, int month, int day)
{
_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();
return 0;
}
️️输出结果:
没有定义构造函数,也会成功创建对象,只不过用随机值来进行初始化
❓你是否和我有着同样的困惑:既然构造函数是用来初始化的,调用默认构造函数之后,确实是把类的成员参数初始化了,but 是用随机值初始化的,有一种没有初始化的赶脚... ...
默认构造函数没卵用吗?
这里我不说,等到后面再解答(哎呦,不就是想要骗你把文章读完嘛~)(狗头
继续回到默认构造函数:
无参构造函数、全缺省构造函数都被称为默认构造函数。
并且默认构造函数只能有一个!
class Date
{
public:
// 全缺省的默认构造函数
Date(int year = 2023, int month = 6, int day = 5)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
注意:
① 无参构造函数、全缺省构造函数、什么没写时编译器默认生成的构造函数,都可以认为是默认构造函数(并不只有编译器默认生成的构造函数才叫做默认构造函数)
② 无参构造和全缺省构造同时存在时会引发歧义:
#include
using namespace std;
class Date
{
public:
// 无参的默认构造函数
Date()
{
_year = 2023;
_month = 6;
_day = 5;
}
// 全缺省的默认构造函数
Date(int year = 2023, int month = 6, int day = 5)
{
_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();
return 0;
}
️️输出结果:(报错)
存在两个默认构造函数:无参的和全缺省的,但默认构造函数只能有一个,当类 d1 创建好后,编译器不知道用哪个构造函数来初始化。
图源:柠檬叶子C这种全缺省/半缺省的格外好用:
#include
using namespace std;
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 d1;
d1.Print();
Date d2(2023, 7, 14);
d2.Print();
Date d3(2023, 7);
d3.Print();
Date d4(2023);
d4.Print();
return 0;
}
️️输出结果:
这一部分主要解决上面留下的一个疑问:
如果你没有在类中定义构造函数(类中未显式定义),那么C++编译器就会自动生成一个无参的默认构造函数。
#include
using namespace std;
class Date
{
public:
// 没有显式定义构造函数
/*Date(int year, int month, int day)
{
_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();
return 0;
}
️️输出结果:
没有定义构造函数,也会成功创建对象,只不过用随机值来进行初始化
❓你是否和我有着同样的困惑:既然构造函数是用来初始化的,调用默认构造函数之后,确实是把类的成员参数初始化了,but 是用随机值初始化的,有一种没有初始化的赶脚... ...
默认构造函数没卵用吗?
解答:
C++把类型分成内置类型(基本类型)和自定义类型。
内置类型:语言本身提供的数据类型,如 int / char / float ;
自定义类型:我们使用 class / struct / union 等自己定义的类型。
我们一起来看看下面的程序:
#include
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()被调用" << endl; // 调用就会打印
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
// 这里没有显式定义构造函数
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
️️输出结果:
️我们可以看出,编译器生成的 Date 默认构造函数会对自定义 Time 类型成员 _t 调用它的默认成员函数。
编译器默认生成构造函数:
对于内置类型的成员变量,会用随机值进行“处理”。
对于自定义类型的成员变量,会去调用它的默认构造函数(不用参数就可以调的)初始化。
❓随机值果然很挫,有没有一种办法,即能用到编译器默认生成的构造函数,又能使内置类型不是随机值?
这里提供一种方法:就是在定义内置类型的时候就给一个初始值(C++11 内置类型成员变量在类中声明时可以给默认值)
#include
using namespace std;
class Time
{
public:
Time()
{
cout << "Time()被调用" << endl; // 调用就会打印
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
// 这里没显式构造函数
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
// 基本类型(内置类型) 给初始值
int _year = 1;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
d.Print();
return 0;
}
️️输出结果:
是不是爽歪歪?
❓在前面构造函数的学习之后,我们知道了一个对象是怎么来的,那么一个对象是怎么没的呢?
构造函数的使命是初始化,那么谁来做清理工作?
❗那就是 -- 析构函数
对象在销毁时会调用析构函数,完成对象中资源的清理工作
注意:析构函数不是完成对象本身的销毁,局部对象销毁工作是由编译器完成的
析构函数也是特殊的成员函数,其特征如下:
① 析构函数名是在类名前加上字符 ~
② 无返回类型,也无参数
③ 一个类只能有一个析构函数,若无显式定义,系统会自动生成默认的析构函数。
注意:析构函数不能重载
④ 对象的声明周期结束时,C++编译系统自动调用析构函数:
#include
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date() 仙逝~ " << endl; // 测试一波
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2023, 6, 5);
return 0;
}
️️输出结果:
因为创建了两个 Date 类 d1 , d2 ,所以都会自动销毁
析构函数的魅力不止,为了更好的演示,下面就采用 Stack :
之前实现 Stack ,最后需要 StackDestroy 来清理栈中的数据,
现在可以让析构函数来干这个活,泰爽辣:
#include
#include
using namespace std;
typedef int DataType;
class Stack
{
public:
// 构造函数 默认容量为4
Stack(size_t capacity = 4)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc failed");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 析构函数 清理开辟的空间,防止内存泄露
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s1; // 默认容量为4
Stack s2(10); // 设置初始容量为10
}
缺省了一个初始容量,也可以自己定义 另外不需要手动调用析构函数,都交给编译器。
继续回到析构函数的特性:
⑤ 编译器默认生成的析构函数,对自定义类型成员调用它的析构函数:
这一点与构造函数相同,下面来测试
#include
using namespace std;
class Time
{
public:
~Time()
{
cout << "~Time() 仙逝~" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
️️输出结果:
❓明明没有创建 Time 类的对象,为什么最后会调用 Time 类的析构函数?
解释:
编译器默认生成了 Date 类的析构函数,Date 类中含有自定义类型 Time ,于是 Date 类的析构函数调用了 Time 类的析构函数,做到当 Date 对象销毁时,保证其内部每个对象都正确销毁。
注意:创建哪个类的对象则调用该类的析构函数,销毁哪个类的对象则调用该类的析构函数
⑥ 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如 Date 类;
如果有资源申请,一定要写,否则会造成内存泄漏,如 Stack 类,其中含有动态内存申请,所 以一定要写。
我们知道了如果你不写析构函数,编译器会自动生成一个默认析构函数,这个默认析构函数会做些什么呢?
先来串联一下过来的知识:
如果你不写构造函数,编译器会自动生成,这个自动生成的 默认构造函数:
• "内置类型" 成员变量:不会做初始化处理
• "自定义类型" 成员变量:会调用它的默认构造函数初始化
对应的,析构函数也是这样:
如果你不写析构函数,编译器会自动生成,这个自动生成的 默认析构函数:
• "内置类型" 的成员变量:不作处理,也不需要处理,系统将其内存回收
• "自定义类型" 的成员变量:会调用它对应的析构函数
就如特性⑤所提到的:
编译器默认生成的析构函数,对自定义类型成员调用它的析构函数:
#include
using namespace std;
class Time
{
public:
~Time()
{
cout << "~Time() 仙逝~" << endl;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
️️输出结果:
总结:
这篇带大家认识了6个默认成员函数,再给大家详细讲解了构造函数和析构函数,思维不要停留在C语言阶段啦,C++编译器可以帮你做很多事情的!
码文不易
如果你觉得这篇文章还不错并且对你有帮助,不妨支持一波哦