👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
【本章内容】
前言
本章是补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的。 【上期地址】
目录
- 前言
- 一、介绍类的6个默认成员函数
- 二、为什么会存在构造函数和析构函数
- 三、构造函数
- 3.1 什么是构造函数
- 3.2 构造函数的特性
- 四、析构函数
- 4.1 什么是析构函数
- 4.2 析构函数的特性
- 五、总结构造函数和析构函数
- 六、拷贝构造函数
- 6.1 什么是拷贝构造函数
- 6.2 拷贝构造函数的特性
一、介绍类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。 然而,空类中真的什么都没有吗?其实并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数(不写函数,编译器会自动生成函数)。 那么类中有哪些默认成员函数呢?让我们接着往下看
【空类】
//ClassName -- 类名
class ClassName
{
};
二、为什么会存在构造函数和析构函数
在使用类的函数的时候,某些粗心的程序员可能会忘记初始化和销毁。特别是销毁,如果程序结束后没有及时销毁,可能会存在内存泄漏。为了能一劳永逸解决这个问题,
C++
就有了构造函数和析构函数,他可以自动完成初始化工作和销毁。
以下是部分栈的实现,在主函数中缺少栈的初始化和销毁
#include <iostream>
#include <stdlib.h>
using namespace std;
class Stack
{
public:
// 栈的初始化
void Init(int defaultCapacity = 4)
{
a = (int*)malloc(sizeof(int) * defaultCapacity);
if (nullptr == a)
{
return;
}
capacity = defaultCapacity;
top = 0;
}
// 入栈
void Push(int x)
{
CheckCapacity();
a[top] = x;
top++;
}
// 出栈
void Pop()
{
if (Empty())
return;
top--;
}
// 栈顶元素
int Top()
{
return a[top - 1];
}
// 判断栈是否为空
int Empty()
{
return top == 0;
}
// 栈的销毁
void Destroy()
{
free(a);
a = nullptr;
top = capacity;
}
// 检查栈的容量
void CheckCapacity()
{
if (top == capacity)
{
int newcapacity = capacity * 2;
int* tmp = (int*)realloc(a, newcapacity * sizeof(int));
if (tmp == nullptr)
{
return;
}
a = tmp;
capacity = newcapacity;
}
}
private:
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
//假设粗心的程序员未给栈初始化(已注释掉)
//s1.Init();
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
while (!s1.Empty())
{
printf("%d ", s1.Top());
s1.Pop();
}
printf("\n");
//假设粗心的程序员为进行栈的销毁(已注释掉)
//s1.Destroy();
return 0;
}
我们知道未给栈初始化会导致程序崩溃,未给栈销毁会导致内存泄漏。那么构造函数和析构函数又是如何帮助我们初始化和销毁的?以及它们的写法是怎么样的呢?让我们接着往下看
三、构造函数
3.1 什么是构造函数
构造函数是一个特殊的成员函数,其名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在 对象整个生命周期内只调用一次。
3.2 构造函数的特性
构造函数虽然名称叫构造,但构造函数的主要任务:不是开空间创建对象,而是初始化对象
其特征如下:
- 函数名与类名相同。
- 无返回值。意思是:可以不用写返回类型
- 对象实例化时编译器自动调用对应的构造函数。
根据以上三个特征,我们可以轻松对以上部分栈进行修改
#include <iostream>
#include <stdlib.h>
using namespace std;
class Stack
{
public:
//构造函数(初始化)
Stack(int defaultCapacity = 4)
{
//其逻辑和栈的初始化一模一样
a = (int*)malloc(sizeof(int) * defaultCapacity);
if (nullptr == a)
{
return;
}
capacity = defaultCapacity;
top = 0;
}
// 栈的初始化
//有了构造函数以下代码可以屏蔽
/*void Init(int defaultCapacity = 4)
{
a = (int*)malloc(sizeof(int) * defaultCapacity);
if (nullptr == a)
{
return;
}
capacity = defaultCapacity;
top = 0;
}*/
// 入栈
void Push(int x)
{
CheckCapacity();
a[top] = x;
top++;
}
// 出栈
void Pop()
{
if (Empty())
return;
top--;
}
// 栈顶元素
int Top()
{
return a[top - 1];
}
// 判断栈是否为空
int Empty()
{
return top == 0;
}
// 栈的销毁
void Destroy()
{
free(a);
a = nullptr;
top = capacity;
}
// 检查栈的容量
void CheckCapacity()
{
if (top == capacity)
{
int newcapacity = capacity * 2;
int* tmp = (int*)realloc(a, newcapacity * sizeof(int));
if (tmp == nullptr)
{
return;
}
a = tmp;
capacity = newcapacity;
}
}
private:
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
//假设粗心的程序员未给栈初始化(已注释掉)
//s1.Init();
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
while (!s1.Empty())
{
printf("%d ", s1.Top());
s1.Pop();
}
printf("\n");
//假设粗心的程序员为进行栈的销毁(已注释掉)
//s1.Destroy();
return 0;
}
【程序结果】
4. 构造函数支持函数重载
【例如】
class Date
{
public:
// 1.无参构造函数
Date()
{
}
// 2.带参构造函数
Date(int year, int month, int day)
{
Year = year;
Month = month;
Day = day;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
Date d3();//错误
// warning: 未调用原型函数(是否是有意用变量定义的?)
return 0;
}
【构造函数的调用】
- 构造函数的调用就是在对象后加上参数列表或者不加参数列表。如果要调用无参构造函数时,对象后面不用跟括号,否则编译器无法区分是否是对象还是函数名,如以上
d3
的错误- 对于函数重载,避免产生调用歧义(例子配合下面第7点使用)。重载知识点详细请参考这篇博客–>点击跳转
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数(不传参可以直接调用的),一旦用户显式定义编译器将不再生成
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << Year << ' ' << Month << ' ' << Day << endl;
}
private:
//声明
int Year;
int Month;
int Day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
【笔记】
类中成员函数d1
对象的Year、Month、Day
,原因是这里其实隐藏了一个this
指针,在 C++ 中,其实有一个隐式指针叫this指针,它指向当前对象的地址。当一个对象调用它的成员函数时,编译器会隐式自动地将该对象的地址作为第一个参数传递给成员函数。因此,在一个类的成员函数内部,可以使用 this指针来访问该对象的成员变量和成员函数。
更多详细知识请查看这篇博客 --> 点击跳转
回归正传,我们继续观察其打印结果:
【运行结果】
上述类中并没有显式定义构造函数,因此编译器会自动生成一个无参的默认构造函数。但
d1
的成员变量通过编辑器初始化后打印的结果是一个随机的值,这是为什么呢?为什么不初始化为0
呢?让我们接着往下看
- 不自己定义的构造函数情况下,编译器会生成默认的构造函数。但是看起来默认构造函数好像没什么用?原因是:上面
d1
对象调用了编译器生成的默认构造函数,使得d1
对象的year
、month
、day
却是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?
解答:
在C++中,一般把类型分为两类:第一类是内置类型(基本类型),像int
、char
、double
这些语言本身定义的类型就称为内置类型;第二种是自定义类型,就是自己定义的类型,诸如用struct、class、union等定义的类型。所以,若用户不自己定义构造函数,编译器默认的构造函数对内置类型不做初始化处理(注意:如果你的编译器对内置类型有初始化处理,这其实是编辑器的个性化行为,但我们还是要坚信编译器默认的构造函数对内置类型不做初始化处理),然而自定义类型会去调用编译器默认的构造函数。
【总结】
- 当成员变量是内置类型的时候,最好自己写一个构造函数,不然如上所描述的,编译器默认的构造函数会对成员变量初始化成随机值;
- 若成员变量都是自定义类型,可以考虑使用编译器默认的构造函数。并且默认初始化都为0,当然也可以自己写一个构造函数,具体看要求
后来,C++11中针对编译器默认构造函数对内置类型成员不初始化的缺陷做出了优化,即:内置类型成员变量在类中声明时可以给缺省值(默认值)。
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << this->Year << ' ' << this->Month << ' ' << this->Day << endl;
}
private:
int Year = 2023;
int Month = 5;
int Day = 15;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
【程序结果】
再次提醒:以上的成员变量都是变量的声明,而不是初始化。这里给的是默认缺省值,供编译器生成默认构造函数使用
7. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
#include <iostream>
using namespace std;
class Date
{
public:
// 1.无参的构造函数
Date()
{
Year = 2023;
Month = 5;
Day = 15;
}
// 2.全缺省的构造函数
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();
return 0;
}
【错误报告】
四、析构函数
4.1 什么是析构函数
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
4.2 析构函数的特性
析构函数是特殊的成员函数,其特征如下:
- 析构函数的函数名与类名也是相同,但是在类名前加上字符
~
。 - 无参数无返回值类型。因此析构函数不能重载,所以一个类只能有一个析构函数
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
根据以上三个特性,我们对一开始栈的代码末尾未初始化进行修改如下:
#include <iostream>
#include <stdlib.h>
using namespace std;
class Stack
{
public:
//构造函数(初始化)
Stack(int defaultCapacity = 4)
{
//其逻辑和栈的初始化一模一样
a = (int*)malloc(sizeof(int) * defaultCapacity);
if (nullptr == a)
{
return;
}
capacity = defaultCapacity;
top = 0;
}
//析构函数(销毁)
~Stack()
{
cout << "空间已被销毁" << endl;
free(a);
a = nullptr;
top = capacity;
}
// 栈的初始化
//有了构造函数以下代码可以屏蔽
/*void Init(int defaultCapacity = 4)
{
a = (int*)malloc(sizeof(int) * defaultCapacity);
if (nullptr == a)
{
return;
}
capacity = defaultCapacity;
top = 0;
}*/
// 入栈
void Push(int x)
{
CheckCapacity();
a[top] = x;
top++;
}
// 出栈
void Pop()
{
if (Empty())
return;
top--;
}
// 栈顶元素
int Top()
{
return a[top - 1];
}
// 判断栈是否为空
int Empty()
{
return top == 0;
}
// 栈的销毁
//void Destroy()
//{
// free(a);
// a = nullptr;
// top = capacity;
//}
// 检查栈的容量
void CheckCapacity()
{
if (top == capacity)
{
int newcapacity = capacity * 2;
int* tmp = (int*)realloc(a, newcapacity * sizeof(int));
if (tmp == nullptr)
{
return;
}
a = tmp;
capacity = newcapacity;
}
}
private:
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
//假设粗心的程序员未给栈初始化(已注释掉)
//s1.Init();
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
while (!s1.Empty())
{
printf("%d ", s1.Top());
s1.Pop();
}
printf("\n");
//假设粗心的程序员为进行栈的销毁(已注释掉)
//s1.Destroy();
return 0;
}
为了验证《对象生命周期结束时,C++编译系统系统自动调用析构函数》,我在析构函数加了【“空间已被销毁”】,最后让我们来看看结果:
- 若未显式定义析构函数,系统会自动生成默认的析构函数。注意:系统自动生成默认的析构函数对内置类型成员不做处理,而自定义类型会去调用它的析构函数。 因此,什么时候写析构我做了一下总结:
①如果类中没有动态申请资源(堆区)时,析构函数就可以不写,直接使用编译器生成的默认析构函数就行。
②如果有动态申请资源,就需要显示写析构函数释放资源。
③其次,如果需要释放资源的成员都是自定义类型,不需要写析构。
五、总结构造函数和析构函数
构造函数和析构函数都是C++中的特殊函数。
-
构造函数用于在创建对象时初始化对象的成员变量,它的名称与类名相同,没有返回类型,可以有参数。当对象被创建时,会自动调用构造函数。
-
析构函数用于在对象被销毁时释放对象所占用的资源,它的名称也与类名相同,但在名称前面加上了一个波浪号(~),没有参数和返回值。当对象被销毁时,会自动调用析构函数。
-
构造函数和析构函数都是可选的,如果不定义,编译器会自动生成默认的构造函数和析构函数。但在某些情况下,我们需要自定义构造函数和析构函数,以满足特定的需求,如初始化对象时需要进行一些特殊的操作,或者对象被销毁前需要释放一些资源。
总之,构造函数和析构函数是C++中非常重要的特殊函数,掌握它们的使用方法对于编写高质量的C++代码是至关重要的。
六、拷贝构造函数
6.1 什么是拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
6.2 拷贝构造函数的特性
拷贝构造函数也是特殊的成员函数,其特征如下:
1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
根据以上特性,举一个拷贝构造函数的例子:
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数(对d1初始化)
Date(int year = 1, int month = 1, int day = 1)
{
Year = year;
Month = month;
Day = day;
}
// 拷贝构造函数
//d是d1的别名
Date(Date& d)
{
this->Year = d.Year;
this->Month = d.Month;
this->Day = d.Day;
}
void Print()
{
cout << this->Year << '-' << this->Month << '-' << this->Day << endl;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1(2024, 5, 15);
d1.Print();
Date d2(d1);
d2.Print();
return 0;
}
【程序结果】
这里的
this指针
就不再赘述了,在构造函数中还提了一嘴,详细内容还请看 —>[点击跳转]
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试? - 拷贝构造函数典型调用场景:
①使用已存在对象创建新对象
②函数参数类型为类类型对象
③函数返回值类型为类类型对象