C++ 6.析构函数和拷贝构造函数
1. 析构函数
a. 概念
i. 析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作. 构造函数是在对象销毁时,有编译器会自动调用,,,目的:将对象中的资源拿走
b. 特性
1.析构函数是在类名称前面加上字符~。
2.无参数和返回值。
~stack()
{
}
3.一个类有且只有一个析构函数。
若未显示定义,系统会自动生默认的显示的析构函数。析构函数不能有参数,因此析构函数是不能发生重载的。
4.对象生命周期结束时,C++编译系统自动调用析构函数。
(反汇编查看),我们可以通过反汇编看到系统自动调用了析构哈函数。
#include <malloc.h>
#include <assert.h>
typedef int DataType;
class stack
{
public:
//析构函数
//析构函数不能有参数
//~stack(int a)编译报错
//~stack(void) 编译通过--一般不会使用
~stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
//构造函数
stack()
{
//StackDestroy(&s);-->在C语言中的做法
_array = (DataType*)malloc(sizeof(DataType) * 10);
if (NULL == _array)
{
assert(0);
return;
}
_capacity = 10;
_size = 0;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
--_size;
}
DataType Top()
{
return _array[_size-1];
}
size_t Size()
{
return _size;
}
DataType Empty()
{
return 0 == _size;
}
private:
DataType* _array;
size_t _capacity;
size_t _size;
};
void Test()
{
stack s;
s.Push(5);
s.Push(4);
s.Push(3);
s.Push(2);
cout << s.Size() << endl;
cout << s.Top() << endl;
s.Pop();
cout << s.Size() << endl;
cout << s.Top() << endl;
//在C语言中需要使用StackDestroy(&s)销毁栈
}
//C++中,对象生命周期结束时,C++编译系统自动调用析构函数
int main()
{
Test();
return 0;
}
4
2
3
3
//反汇编调用了析构函数
//00007FF796911D49 lea rcx, [s]
//00007FF796911D4D call stack::~stack(07FF796911375h)
//00007FF796911D52 lea rcx, [rbp - 20h]
//00007FF796911D56 lea rdx, [__xt_z + 160h(07FF79691AC00h)]
//00007FF796911D5D call _RTC_CheckStackVars(07FF796911348h)
//00007FF796911D62 mov rcx, qword ptr[rbp + 168h]
5.编译器生成的默认析构函数,对会自定义类型成员调用他的析构函数
1.编译器是否生成默认的构造,析构函数
这种没有生成构造和析构函数,运行速度就快了很多。
默认的成员函数到底会不会生成,语法是一定会生成,,实际具体的编译器,比如vs2022是不一定生成的
有些情况下会生成,有些编译器判断自己需要时才会生成
class Date
{
//语法:构造函数没有显式定义,则编译器会自动生成一份无参的默认构造函数
//析构函数没有显式定义,则编译器会自动生成一份析构函数
public:
void Print()
{
cout << _year << "-" << _month << "_" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
//Date d1;
//return 0;
//00007FF747381A4C xor eax, eax
2.那么编译器为什么给日期生成构造和析构函数?
因为日期类包含了自定义对象 Time _t,日期类型对象内部包含了时间类型对象。当给日期类生成一份构造函数时,会调用时间类的构造函数,当出了函数作用域时,会将时间类的析构函数放在日期类中,及调用日期类析构函数。通过反汇编看到。
给内置类型发送随机值,都是可行的;
但是不能给对象发送随机值。
//时间
class Time
{public:
Time()
{
}
~Time()
{
}
private:
int _hour;
int _minute;
int _second;
};
//日期
class Date
{
public:
void Print()
{
cout << _year << "-" << _month << "_" << _day << endl;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d1;
return 0;
}
//Date d1;
//00007FF7BC4719FD lea rcx, [d1]
//00007FF7BC471A01 call Date::Date(07FF7BC47146Ah)
//return 0;
//00007FF7BC471A06 mov dword ptr[rbp + 0F4h], 0
//00007FF7BC471A10 lea rcx, [d1]
//00007FF7BC471A14 call Date::~Date(07FF7BC471460h)
//00007FF7BC471A19 mov eax, dword ptr[rbp + 0F4h]
由以上可知:
有些类的析构函数实现出来没有任何意义——单独的Date类,有些类的析构函数时必须给出的,否则程序中会存在内存泄漏。类中如果不涉及到资源管理时,比如Date类,则析构函数不用写,一旦涉及到资源管理师,则析构函数一定要给出
2. 拷贝构造函数
a. 概念
构造拷贝函数:只有单个形参,该形参是对本类类型对象的引用(一般是常用const修饰),再用已存在的类类型对象,创建新对象是由编译器自动调用。
//日期
class Date
{
public:
Date(int year=2000,int month=2,int day=2)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//一般情况下加const不需要要改变d1 ,使用引用传参
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2008,8,8);
d1.Print();
Date d2(d1);
d2.Print();
return 0;
}
2008-8-8
2008-8-8
需求:创建的d2与d1的内容一样,既然创建一个新对象就需要调用构造函数。
并且 拷贝构造函数,参数只有一个,是构造函数的一个重载形式
b. 特征
1.拷贝构造函数是构造函数的一个重载。
2.拷贝构造函数的参数只有一个且必须使用引用传参
如果使用传值方式,会引发无穷递归调用。
3.若未显示定义,系统会生成默认的拷贝构造函数
默认拷贝的拷贝构造函数对象按内存存储字节序完成拷贝,这种拷贝称为浅拷贝,或者值拷贝。
1.编译器默认拷贝构造
浅拷贝后果;多个对象共用同一份资源,当这些对象销毁时,资源会被释放多次而引起程序崩溃.
4.编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了
是否直接说明了:直接使用编译器生成的默认拷贝构造函数就可以了呢?
答案;对于日期类这种没有涉及到资源管理的类是可以的
一旦涉及到资源管理,拷贝构造函数就一定要实现。以下,,在调用析构函数时报错
3.详解编译器会生成一份默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储字节序完成拷贝(将d1原封不动的拷贝到d2中,即内部指针指向同一对象),及调用析构函数,通过d2中的指针_array将堆上的空间释放一次,但是d1中的指针_array将堆上的空间再次释放一次。程序报错(d1中的_array成为野指针了。)
#include <stdlib.h>
#include <assert.h>
typedef int DataType;
class stack
{
public:
~stack()
{
if (_array)
{
free(_array);
_array =nullptr;
_capacity = 0;
_size = 0;
}
}
stack()
{
_array = (DataType*)malloc(sizeof(DataType) * 10);
if (NULL == _array)
{
assert(0);
return;
}
_capacity = 10;
_size = 0;
}
void Push(const DataType& data)
{
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
--_size;
}
DataType Top()
{
return _array[_size - 1];
}
size_t Size()
{
return _size;
}
DataType Empty()
{
return 0 == _size;
}
private:
DataType* _array;
size_t _capacity;
size_t _size;
};
void Test()
{
stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
s1.Push(5);
stack s2(s1);
//刚开始创建了s1,有三个成员变量。在Test的栈帧中创建通过构造函数在堆上创建内存空间
//s2,内部也有三个成员变量。调用拷贝构造函数完成,但是Stack的拷贝构造函数并没有实现
//编译器会生成一份默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储字节序完成拷贝(将d1原封不动的拷贝到d2中,即内部指针指向同一对象)
//及调用析构函数,通过d2中的指针_array将堆上的空间释放一次,但是d1中的指针_array将堆上的空间再次释放一次。程序报错(d1中的_array成为野指针了。)
cout << s2.Size() << endl;
cout << s2.Top() << endl;
}
int main()
{
Test();
return 0;
}
5.作为自定义类型作为参数或者返回值时,能传递引用尽量传递引用
1.用一个对象直接构造一个新对象。
2.以类类型方式传参–以值传参。
3.以类类型方式作为函数的返回值–以值的方式返回的
4. 创建一个匿名对象: 及没有名字的对象。编译器在返回时,编译器不会再通过拷贝构造函数创建一个临时对象返回,而是将匿名对象直接返回,编译器会优化。
//日期
class Date
{
public:
Date(int year = 2000, int month = 2, int day = 2)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//2.以类类型方式传参--以值传参
void Test1(Date d)
{}
//3.以类类型方式作为函数的返回值--以值的方式返回的
Date Test2()
{
Date d(2022, 2, 2);
return d;
}
Date Test3()
{
//创建一个匿名对象:及没有名字的对象
//编译器在 返回时,编译器不会再通过拷贝构造函数创建一个临时对象返回
//而是将匿名对象直接返回,编译器会优化
return Date(2002,2,2);
}
int main()
{
//1.用一个对象直接构造一个新对象
Date d1(2000, 2, 2);
Date d2(d1);
Test1(d1);
d2 = Test2();
d2 = Test3();
return 0;
}