当前位置: 首页 > news >正文

构造函数,析构函数,深浅拷贝【c++】

一.构造函数

构造函数是是特殊的成员函数。我们在c语言阶段实现栈通常会写一个init函数去初始化栈,但有时候会忘记写栈的初始化导致出现异常,同时去单独写一个函数去初始化封装性也不太好。为了解决这个问题祖师爷就提供了构造函数来主动帮我我们去初始化栈,不需要我们手动调用,而是当对象被创建之后完成对象的初始化工作,并且在对象整个生命周期中只调用一次。

特点:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
1.函数名与类名必须相同
2.无返回值
3.构造函数支持重载和缺省参数

1.构造函数初始化成员变量

我们以Date类来看:

class Date
{
public:Date(){cout << "Date()" << endl;_year = 2024;_month = 3;_day = 2;}Date(int year , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}
private:int _year;int _month;int _day;};
int main()
{Date d1(2015,12,20);d1.printf1();Date d2;d2.printf1();return 0;
}

在这里插入图片描述

我们并没有去调用构造函数,而是编译器自动调用去初始化成员变量。该程序中有2个构造函数,一个带参一个无参,说明构造函数是可以重载的。构造函数初始化了年,月,日三个变量,所以构造函数最主要的用途是初始化变量。
要注意无参的构造函数和缺省的构造函数不能同时出现否则会有二义性。

class Date
{
public:Date(){cout << "Date()" << endl;_year = 2024;_month = 3;_day = 2;}Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}
private:int _year;int _month;int _day;};
int main()
{Date d1(2015,12,20);d1.printf1();Date d2;d2.printf1();return 0;
}

在这里插入图片描述
通常情况下我们只保留一个全缺省的构造函数,这个构造函数通常能代表很多种情况。只保留一个构造函数可以使代码的可读性更好。
在这里插入图片描述
在这里插入图片描述
定义无参构造函数时只能写作Date d1;不能写做Date d2();因为第二种写法编译器不知道是调用无参的构造函数还是函数声明。
在这里插入图片描述

2.编译器自动生成构造函数

当我们不写构造函数时编译器会自动生成一个构造函数去初始化变量,一但用户编写编译器就不会自动生成

class Date
{
public:/*Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}*/void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}
private:int _year;int _month;int _day;};
int main()
{/*Date d1(2015,12,20);d1.printf1();*/Date d2;d2.printf1();return 0;
}

在这里插入图片描述
虽然编译器自动生成了构造函数,但是并没有帮我们初始化成员变量,仍然是一个随机值。这是为什么呢?在来看一个自己实现的栈代码。

typedef int IntDateType;
class Stack {
public:Stack(size_t capacicty=3){_a = (IntDateType*)malloc(sizeof(IntDateType) * 3);if (NULL == _a){perror("malloc fail::");return;}_capacity = capacicty;_size = 0;}Stack(Stack& stack){_a = (IntDateType*)malloc(sizeof(IntDateType) * stack._capacity);if (NULL == _a){perror("malloc fail::");return;}memcpy(_a,stack._a,sizeof(IntDateType) * stack._capacity);_size = stack._size;_capacity = stack._capacity;}void PushStack(IntDateType x){if (_size == _capacity){int NewCapacity = _capacity == 0 ? 3 : 2 * _capacity;IntDateType* tmp = (IntDateType*)realloc(_a, sizeof(IntDateType) * NewCapacity);if (tmp == nullptr){perror("realloc fail::");return;}_a = tmp;_capacity = NewCapacity;}_a[_size++] = x;}void DrawStack(){for (int i = 0; i < _size; i++){cout << _a[i] << endl;}}bool Empty(){return _size == 0;}void PopStack(){if (Empty())return;_size--;}IntDateType PeekStack(){if (Empty())return NULL;return _a[_size];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}
private:IntDateType* _a;int _size;int _capacity;
};
class Date
{
public:/*Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}*/void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}
private:int _year;int _month;int _day;};
int main()
{/*Date d1(2015,12,20);d1.printf1();*//*Date d2;d2.printf1();*/Stack s1;s1.PushStack(1);s1.PushStack(2);s1.PushStack(3);s1.DrawStack();return 0;
}

继续用这个栈定义一个类

class Queue
{Stack s1;Stack s2;
};
int main()
{/*Date d1(2015,12,20);d1.printf1();*//*Date d2;d2.printf1();*//*Stack s1;s1.PushStack(1);s1.PushStack(2);s1.PushStack(3);s1.DrawStack();*/Queue que;return 0;
}

Queue里面有2个成员函数s1,s2,该类并没有去实现对栈初始化的代码,按照前面的逻辑没有构造函数编译器会提供一个,会把成员变量初始化为随机值,但真的是这样吗?
在这里插入图片描述
通过调试,虽然没有给queue类提供构造方法但编译器却对它的成员变量进行了初始化,为什么编译器对有的变量初始化有的不呢?这是因为c++把类型定义为内置类型和自定义类型,内置类型就是语言提供的数据内型,比如Int,double,short等,自定义类型就是我们自己写的类型如stack,Date.
注意Date是自定义类型,但Date是内置内型,因为Date本身是指针,同理Stack*也是。编译器对内置类型不做处理,对自定义类型调用它的构造函数,所以Date内的成员变量是随机值。Queue内里面的s1,s2是我们自定义的栈内型,会调用栈的构造函数初始化它的值。
总结:

那么我们什么时候需要自己提供构造函数,什么时候需要编译器生成构造函数呢?看自己需求,当编译器生成的构造函数能满足我们需求时候我们不需要自己重新写构造函数,当不满足时需要我们重新写。

3.c++11的补丁

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

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;
}

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

class Date
{
public:
Date()
{
_year = 1900;
_month = 1;
_day = 1;
}
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day
}

三.析构函数

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

特点:析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
  4. 对象生命周期结束时,C++编译系统系统自动调用析构函数
    注意:当变量的生命周期结束变量会销毁,所以位于函数中的对象会在函数结束时调用析构函数销毁,位于Main函数的全局变量会在程序快结束时调用析构函数销毁。后定义的变量会先销毁。

我们给Date类增加析构函数:

1.析构函数

class Date
{
public:Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;};
//class Queue
//{
//	Stack s1;
//	Stack s2;
//};
int main()
{Date d1(2015,12,20);d1.printf1();Date d2;d2.printf1();///*Stack s1;//s1.PushStack(1);//s1.PushStack(2);//s1.PushStack(3);//s1.DrawStack();*///Queue que;return 0;
}

在这里插入图片描述

析构函数和构造函数一样都是通过编译器帮我们去调用,因为d1,d2的销毁都需要调用构造函数,所以构造函数调用了2次。

2.选择性处理

我们通过构造函数知道,构造函数会对内置类型不进行处理,对自定义内型进行处理,那么析构函数是不是也是这样的?
在这里插入图片描述
通过上述编译器自动调用了析构函数但并没有对内置内型进行销毁,那么对自定义内型呢?

typedef int IntDateType;
class Stack {
public:Stack(size_t capacicty=3){_a = (IntDateType*)malloc(sizeof(IntDateType) * 3);if (NULL == _a){perror("malloc fail::");return;}_capacity = capacicty;_size = 0;}Stack(Stack& stack){_a = (IntDateType*)malloc(sizeof(IntDateType) * stack._capacity);if (NULL == _a){perror("malloc fail::");return;}memcpy(_a,stack._a,sizeof(IntDateType) * stack._capacity);_size = stack._size;_capacity = stack._capacity;}void PushStack(IntDateType x){if (_size == _capacity){int NewCapacity = _capacity == 0 ? 3 : 2 * _capacity;IntDateType* tmp = (IntDateType*)realloc(_a, sizeof(IntDateType) * NewCapacity);if (tmp == nullptr){perror("realloc fail::");return;}_a = tmp;_capacity = NewCapacity;}_a[_size++] = x;}void DrawStack(){for (int i = 0; i < _size; i++){cout << _a[i] << endl;}}bool Empty(){return _size == 0;}void PopStack(){if (Empty())return;_size--;}IntDateType PeekStack(){if (Empty())return NULL;return _a[_size];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}
private:IntDateType* _a;int _size;int _capacity;
};
class Date
{
public:Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}~Date(){cout << "~Date()" << endl;}
private:int _year;int _month;int _day;};
class Queue
{Stack s1;Stack s2;
};
int main()
{/*Date d1(2015,12,20);d1.printf1();Date d2;d2.printf1();*////*Stack s1;//s1.PushStack(1);//s1.PushStack(2);//s1.PushStack(3);//s1.DrawStack();*/Queue que;return 0;
}

在这里插入图片描述
通过queue类型我们发现因为s1是我们自定义内型,queue函数会去调用s1栈的析构函数。

总结:析构函数和构造函数一样自己不写由编译器自动生成,析构函数对内置内型不做处理对自定义内型调用他的析构函数。

四.拷贝构造

1.什么是拷贝构造

在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。
在这里插入图片描述
那在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

拷贝构造函数也是特殊的成员函数,其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  1. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
  2. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
  3. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

2、无穷递归

拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

class Date
{
public:Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}~Date(){cout << "~Date()" << endl;}Date(Date d1){_year = d1._year;_month = d1._month;_day = d1._day;}
private:int _year;int _month;int _day;};
class Queue
{Stack s1;Stack s2;
};
int main()
{/*Date d1(2015,12,20);d1.printf1();Date d2;d2.printf1();*////*Stack s1;//s1.PushStack(1);//s1.PushStack(2);//s1.PushStack(3);//s1.DrawStack();*//*Queue que;*/Date d1;Date d2(d1);return 0;
}

在这里插入图片描述
为什么会报错呢?我们使用d1创建对象d2时,编译器会自动调用拷贝构造,传值传参本来形参是实参的一份临时拷贝,所以拷贝构造在执行函数体之前会再去调用一次拷贝构造,如此反复。也就是创建d2时,d1会去调用拷贝构造,而d1调用的拷贝构造会去在调用拷贝构造如此反复,发生无穷递归。
在这里插入图片描述
如果传递的是引用形参作为实参的别名,不需要拷贝实参,从而避免无穷递归。为了防止d的内容被修改一般会加const
在这里插入图片描述

3.特性分析

默认的拷贝规则:默认的拷贝构造函数对内置类型以字节为单位直接进行拷贝,对自定义类型调用其自身的拷贝构造函数这种叫浅拷贝。

typedef int IntDateType;
class Stack {
public:Stack(size_t capacicty=3){_a = (IntDateType*)malloc(sizeof(IntDateType) * 3);if (NULL == _a){perror("malloc fail::");return;}_capacity = capacicty;_size = 0;}/*Stack(Stack& stack){_a = (IntDateType*)malloc(sizeof(IntDateType) * stack._capacity);if (NULL == _a){perror("malloc fail::");return;}memcpy(_a,stack._a,sizeof(IntDateType) * stack._capacity);_size = stack._size;_capacity = stack._capacity;}*/void PushStack(IntDateType x){if (_size == _capacity){int NewCapacity = _capacity == 0 ? 3 : 2 * _capacity;IntDateType* tmp = (IntDateType*)realloc(_a, sizeof(IntDateType) * NewCapacity);if (tmp == nullptr){perror("realloc fail::");return;}_a = tmp;_capacity = NewCapacity;}_a[_size++] = x;}void DrawStack(){for (int i = 0; i < _size; i++){cout << _a[i] << endl;}}bool Empty(){return _size == 0;}void PopStack(){if (Empty())return;_size--;}IntDateType PeekStack(){if (Empty())return NULL;return _a[_size];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}
private:IntDateType* _a;int _size;int _capacity;
};
class Date
{
public:Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}~Date(){cout << "~Date()" << endl;}Date(const Date &d1){_year = d1._year;_month = d1._month;_day = d1._day;}
private:int _year;int _month;int _day;};
class Queue
{Stack s1;Stack s2;
};
int main()
{/*Date d1(2015,12,20);d1.printf1();Date d2;d2.printf1();*////*Stack s1;//s1.PushStack(1);//s1.PushStack(2);//s1.PushStack(3);//s1.DrawStack();*//*Queue que;*/
//	Date d1;//Date d2(d1);Stack s1;s1.PushStack(1);s1.PushStack(2);s1.PushStack(3);Stack s2(s1);return 0;
}

在这里插入图片描述
从上面栈的代码中可以看出我们并没有写拷贝函数,该程序只是对s1进行了值的插入,但是系统自己调用了默认的构造拷贝,将s1的成员变量_a,_top,_capacity拷贝到s2中。
当程序继续执行时会报一个异常。
在这里插入图片描述

这是因为_a是动态内存开辟的节点,存放在堆上,_a本身指向一个空间,s1和s2的_a都指向同一块空间。当main函数结束时,s1和s2都会调用析构函数,s2后创建的会先调用析构函数,_a所指向的这块空间会释放掉,当s1在去调用析构函数时_a这块空间已经被释放掉了再去访问会野指针,_a这块空间会被析构2次从而引发异常。
在这里插入图片描述
解决方法是为d2重新申请一块空间,并将d1中_a空间中所指向的内容拷贝到该空间中,其余成员变量因为是内置类型按字节拷贝。

typedef int IntDateType;
class Stack {
public:Stack(size_t capacicty=3){_a = (IntDateType*)malloc(sizeof(IntDateType) * 3);if (NULL == _a){perror("malloc fail::");return;}_capacity = capacicty;_size = 0;}Stack(Stack& stack){_a = (IntDateType*)malloc(sizeof(IntDateType) * stack._capacity);if (NULL == _a){perror("malloc fail::");return;}memcpy(_a,stack._a,sizeof(IntDateType) * stack._capacity);_size = stack._size;_capacity = stack._capacity;}void PushStack(IntDateType x){if (_size == _capacity){int NewCapacity = _capacity == 0 ? 3 : 2 * _capacity;IntDateType* tmp = (IntDateType*)realloc(_a, sizeof(IntDateType) * NewCapacity);if (tmp == nullptr){perror("realloc fail::");return;}_a = tmp;_capacity = NewCapacity;}_a[_size++] = x;}void DrawStack(){for (int i = 0; i < _size; i++){cout << _a[i] << endl;}}bool Empty(){return _size == 0;}void PopStack(){if (Empty())return;_size--;}IntDateType PeekStack(){if (Empty())return NULL;return _a[_size];}~Stack(){cout << "~Stack()" << endl;free(_a);_a = nullptr;_size = _capacity = 0;}
private:IntDateType* _a;int _size;int _capacity;
};
class Date
{
public:Date(int year=1 , int month = 1, int day=1 ){cout << "Date(int year,int month,int day)" << endl;_year = year;_month = month;_day = day;}void printf1(){cout << "_year:" << _year << endl;cout << "_month:" << _month << endl;cout << "_day:" << _day << endl;}~Date(){cout << "~Date()" << endl;}Date(const Date &d1){_year = d1._year;_month = d1._month;_day = d1._day;}
private:int _year;int _month;int _day;};
class Queue
{Stack s1;Stack s2;
};
int main()
{/*Date d1(2015,12,20);d1.printf1();Date d2;d2.printf1();*////*Stack s1;//s1.PushStack(1);//s1.PushStack(2);//s1.PushStack(3);//s1.DrawStack();*//*Queue que;*/
//	Date d1;//Date d2(d1);Stack s1;s1.PushStack(1);s1.PushStack(2);s1.PushStack(3);Stack s2(s1);return 0;
}

在这里插入图片描述
对于Date类由于它的成员函数都是内置内型所以不需要写拷贝构造。Queue类由于他的成员变量为stack类型会去调用stack的拷贝构造所以也不需要写。stack需要写拷贝构造。

总结:类中如果没有资源申请则不需要手动实现拷贝构造,如果出现资源申请则需要写,否则会出现浅拷贝或者同一块空间被多次析构的可能性。拷贝构造和析构函数有很多方面类似,可以大概的认为需要写析构函数就需要写拷贝构造,不需要写析构函数就不需要写拷贝构造。


http://www.mrgr.cn/news/48843.html

相关文章:

  • 基于SpringBoot的体育商城购物系统
  • 【C++ 真题】B2062 乘方计算
  • [Linux#65][TCP] 详解 延迟应答 | 捎带应答 | 流量控制 | 拥塞控制
  • C++——模板进阶
  • UE5运行时动态加载场景角色动画任意搭配-场景角色相机动画音乐加载方法(三)
  • 【动手学深度学习】6.1 从全连接层到卷积
  • 如何让本地浏览器不走代理上网
  • FlexMatch: Boosting Semi-Supervised Learning with Curriculum Pseudo Labeling
  • C#基础-面向对象的七大原则
  • C++ 游戏开发技术选型指南
  • 【用大模型提示工程处理NLP任务】
  • lodash 和 lodash-es 的区别
  • 位操作解决数组的花样遍历
  • 从知乎神贴挖掘财富的思路分析
  • SpringCloud网关聚合knife4j方案
  • JAVA数组基础
  • Nginx UI 一个可以管理Nginx的图形化界面工具
  • 机器学习篇-day06-集成学习-随机森林 Adaboost GBDT XGBoost
  • Java数组的值拷贝和地址拷贝
  • 删除链表的倒数第 N 个结点 | LeetCode-19 | 双指针 | 递归 | 栈 | 四种方法