0.前言
在我们写某些需要动态开辟内存空间的函数时候,会经常忘记初始化、销毁,而且有时候程序返回的情况很多,那么销毁函数写起来就会很繁琐,那么有没有什么办法解决这个问题呢?答案是:当然有!在C++中有两个默认构造函数–构造函数和析构函数,可以自动帮助我们完成初始化和销毁工作!🎉🎉🎉
1.构造函数
1.1构造函数概念
构造函数是一个特殊的成员函数,名字和类相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次 🦀🦀🦀
1.1构造函数特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象 🎾🎾🎾
其特征如下:
1.函数名与类名相同
2.无返回值(也不需要写void)
3.对象实例化时编译器自动调用对应的构造函数
一个栗子:
#include<iostream>
typedef int Datatype;
class Stack
{
public:
Stack()
{
printf("Stack()\n");
_a = (Datatype*)malloc(sizeof(Datatype) * 6);
if (_a == NULL)
{
perror("Stack()::malloc");
return;
}
_capacity = 6;
_size = 0;
}
private:
Datatype* _a;
int _size;
int _capacity;
};
int main()
{
Stack s;
return 0;
}
代码运行的结果:
注意:
Stack s();//定义的时候,不能这样调用构造函数,编译无法很好区分s是对象还是函数名
4.构造函数可以重载(可以有多种初始化方式)
#include<iostream>
typedef int Datatype;
class Stack
{
public:
Stack()
{
printf("Stack()\n");
_a = (Datatype*)malloc(sizeof(Datatype) * 6);
if (_a == NULL)
{
perror("Stack()::malloc");
return;
}
_capacity = 6;
_size = 0;
}
Stack(int capacity = 6)
{
printf("Stack(int capacity = 6)\n");
_a = (Datatype*)malloc(sizeof(Datatype) * capacity);
if (_a == NULL)
{
perror("Stack()::malloc");
return;
}
_capacity = capacity;
_size = 0;
}
private:
Datatype* _a;
int _size;
int _capacity;
};
int main()
{
Stack st1(8);
return 0;
}
代码运行的结果:
但是当像这样Stack st2;
定义对象时,无参构造函数和缺省构造函数会造成歧义,编译器不知道该调用哪个进而引发错误
一般情况下我们保留含缺省参数的构造函数,方便我们可以指定申请的内存空间的大小
5.如果类中没有显示定义构造函数,则C++编译器会自动生成无参的默认构造函数,一旦用户自己显示定义编译器将不再生成!
#include<iostream>
using std::cout;
using std::endl;
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类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数
//将Date类中的构造函数放开,代码编译失败,因为一旦显示定义任何构造函数,编译器将不会再生成
//无参构造函数放开后报错,类Date没有合适的默认构造函数
Date d1;
return 0;
}
代码编译运行的结果为:
6.内置类型/基本类型,即语言本身定义的基础类型int/char/double/指针
类型等等,编译器不做处理,需要自己写构造函数;自定义类型:用struct/class
等定义的类型,编译器默认生成构造函数。
eg:
#include<iostream>
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;
}
调试运行之前:
调试运行之后:
注意:C++11中针对内置类型成员不初始化的缺陷,打了一个补丁,即:内置类型成员变量在类中声明时可以给默认值。
#include<iostream>
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=1970;
int _month=1;
int _day=1;
//自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
调试运行之后:
结论:1、一般情况下,构造函数都需要我们自己写;2、不需要自己写构造函数的情况a、内置类型成员都有缺省值,且初始化符合我们的要求;b、类成员全是自定义类型的构造,且这些类型都定义默认构造。
7、无参构造函数、全缺省的构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数(不传参就可以调用的就是构造函数)。
2.析构函数
2.1概念
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中的资源的清理工作。
2.2特性
析构函数是特殊的成员函数,其特征如下:
1.析构函数是在类名前加上字符~。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数,不能重载。
4.对象的生命周期结束时,C++编译系统自动调用析构函数。
5.一般情况下,有动态内存申请资源,就要写析构函数释放资源;没有动态内存开辟的资源,不需要写析构;需要释放的成员都是自定义类型,不需要写析构。
栗子:
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 3)
{
cout << "Stack(int capacity = 3)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("Stack(int capacity = 3)::malloc");
return;
}
_capacity = capacity;
top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = 0;
top = 0;
}
private:
int* _a = nullptr;
int _capacity = 0;
int top = 0;
};
int main()
{
Stack st1;
return 0;
}
代码运行的结果为:
3.拷贝构造函数
3.1概念
拷贝构造函数:只是单个形参,该形参是对本类类型对象的引用(一般用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
3.2特征
拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
错误的栗子
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,1,1);
Date d2(d1);
return 0;
}
代码编译的结果为:
C++规定:在传值传参的时候,内置类型传参直接拷贝;自定义类型必须调用拷贝构造函数完成拷贝
正确的使用:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,1,1);
Date d2(d1);
return 0;
}
代码调试运行的结果为:
一般加上const修饰
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
发生如下情况编译器会进行报错
Date(const Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
注意:使用传引用传参,自定义类型不会调用拷贝构造函数
3.若为显示定义,编译器会生成默认拷贝构造函数。默认的拷贝构造函数对象按内存存储(即按字节节序完成拷贝),这种拷贝叫做浅拷贝(值拷贝)。
eg1:
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year = 1970, 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;
//}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023,1,1);
Date d2(d1);
return 0;
}
代码调试运行的结果为:
eg2:
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 3)
{
cout << "Stack(int capacity = 3)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("Stack(int capacity = 3)::malloc");
return;
}
_capacity = capacity;
top = 0;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = 0;
top = 0;
}
private:
int* _a;
int _capacity;
int top;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
代码调试运行的结果为:
Stack类型进行值拷贝时存在的问题:1.会调用两次析构函数,即会对动态开辟的同一块空间释放两次;2.对一个Stack类型的对象修改会影响另一个对象。所以,我们需要自己实现进行深拷贝的构造函数。
深拷贝:
#include<iostream>
using namespace std;
class Stack
{
public:
Stack(int capacity = 3)
{
cout << "Stack(int capacity = 3)" << endl;
_a = (int*)malloc(sizeof(int) * capacity);
if (_a == nullptr)
{
perror("Stack(int capacity = 3)::malloc");
return;
}
_capacity = capacity;
top = 0;
}
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);
if (_a == nullptr)
{
perror("Stack(const Stack& st)");
return;
}
memcpy(_a, st._a, sizeof(int) * st.top);
_capacity = st._capacity;
top = st.top;
}
~Stack()
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_capacity = 0;
top = 0;
}
private:
int* _a;
int _capacity;
int top;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
代码运行的结果为:
进行深拷贝,st1\st2指向的动态内存的空间不同,正常结束会调用两次析构函数。
总结: 成员类型为内置类型的Date类型(编译器会进行浅拷贝构造)和成员类型都为自定义类型的类类型(前提为该自定义类型已经自己实现了深拷贝构造)不需要写拷贝构造函数;Stack类型需要自己深拷贝实现。
4.拷贝构造函数的使用场景:
- 使用已存在的对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
{
cout << "Date(int,int,int):" << this << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d):" << this << endl;
}
~Date()
{
cout << "~Date():" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
Date Test(Date d)
{
Date tmp(d);
return tmp;
}
int main()
{
Date d1(2023, 1, 1);
Test(d1);
return 0;
}
代码运行的结果为:
4.总结
本章我们一起学习了类和对象的构造函数、析构函数、拷贝构造函数等默认成员函数的相关知识,希望对大家认识C++中的类和对象由些许帮助!感谢大家阅读,如有不对,欢迎纠正!!!🎉🎉🎉