👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
【本章内容】
前言
本章是补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的。 【上期地址】
目录
- 前言
- 一、类的6个默认成员函数
- 二、为什么会存在构造函数和析构函数
- 三、构造函数的使用例子
- 四、 构造函数特性
- 五、析构函数的使用例子
- 六、析构函数的特征
- 六、总结
一、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。 然而,空类中真的什么都没有吗?其实并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数(不写函数,编译器会自动生成函数)。 那么类中有哪些默认成员函数呢?让我们接着往下看
二、为什么会存在构造函数和析构函数
在使用类的函数的时候,某些粗心的程序员可能会忘记初始化和销毁。特别是销毁,如果程序结束后没有及时销毁,可能会存在内存泄漏。为了能一劳永逸解决这个问题,
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 0 == top;
}
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");
return 0;
}
以上代码问题主要在于未给栈初始化,所以导致程序崩溃(这里就不展示结果了)。因此,我们可以通过使用构造函数(作用:主要完成初始化工作)
【构造函数代码实现】
#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 0 == top;
}
// 栈的销毁
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;
}
构造函数是一个特殊的成员函数,其名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在 对象整个生命周期内只调用一次。
【程序结果】
四、 构造函数特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务:不是开空间创建对象,而是初始化对象
其特征如下:
-
函数名与类名相同。
-
无返回值。意思是:可以不用写返回类型
-
对象实例化时编译器自动调用对应的构造函数。
-
构造函数支持函数重载
【例如】
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: 未调用原型函数(是否是有意用变量定义的?)
}
【构造函数的调用】
- 就是在对象后加上参数列表或者不加参数列表。如果通过无参构造函数创建对象时,对象后面不用跟括号,否则编译器无法区分是否是对象还是函数名,如
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
对象调用了编译器生成的默认构造函数,但是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 << year << ' ' << month << ' ' << day << endl;
}
private:
int year = 20023;
int month = 5;
int day = 20;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
【程序结果】
注意:以上的成员变量都是变量的声明,而不是初始化。这里给的是默认缺省值,供编译器生成默认构造函数使用
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数
#include <iostream>
using namespace std;
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();
return 0;
}
【错误报告】
五、析构函数的使用例子
#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 0 == top;
}
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;
}
释放内存空间是程序员最容易忘记的一件事,未被释放的内存空间最后都会导致内存泄漏。因此,
Bjarne Stroustrup
大佬就对此进行了优化,就引出了构析函数。优化代码如下:
#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 0 == top;
}
/*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++代码是至关重要的。