通过上文 类和对象(中) 构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数: 与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。 这样的功能就像我们写数据结构时的Destory()
函数,来释放申请的资源(尤其是堆上开辟的空间,或是fopen
一个文件等)。
析构函数是特殊的成员函数,其特征如下:
~Date()
)。typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
//析构函数 --- 释放动态开辟内存,防止内存泄露
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
class Time
{
public:
~Time()
{
cout << "~Time()" << endl;
}
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;
}
程序运行结束后输出:~Time()
。在main
方法中根本没有直接创建Time
类的对象,为什么最后会调用Time
类的析构函数?
因为:main
方法中创建了Date
对象d
,而d
中包含4个成员变量,其中_year
, _month
, _day
三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t
是Time
类对象,所以在d
销毁时,要将其内部包含的Time
类的_t
对象销毁,所以要调用Time
类的析构函数。
但是:main
函数中不能直接调用Time
类的析构函数,实际要释放的是Date
类对象,所以编译器会调用Date
类的析构函数,而Date
没有显式提供,则编译器会给Date
类生成一个默认的析构函数,目的是在其内部调用Time
类的析构函数*,即当Date
对象销毁时,要保证其内部每个自定义对象都可以正确销毁 main
函数中并没有直接调用Time
类析构函数,而是显式调用编译器为Date
类生成的默认析构函数
注意:创建哪个类的对象则调用该类的构造函数完成初始化,销毁哪个类的对象时则自动调用该类的析构函数。
默认成员函数中析构函数的初步理解可以通过此题:232. 用栈实现队列。我们定义一个队列其中包含了两个栈,结合上面的理解,那么队列无需写析构函数,栈需要显示写析构函数来释放动态开辟的空间。
class MyQueue
{
//队列的初始化通过调用Mystack类的构造函数
//队列的销毁(释放堆)通过调用Mystack类的析构函数
MyStack st1;
MyStack st2;
};
Date
类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack
类。在创建对象时,可否创建一个与已存在对象一某一样的新对象呢?
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数也是特殊的成员函数,其特征如下:
class Date
{
public:
Date(int year = 2024, int month = 6, int day = 22)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d)// 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
C++规定自定义类型的拷贝都会调用拷贝构造!而我们都知道传值传参,形参是实参的一份临时拷贝! 那么拷贝构造函数如果不是引用就会形成无穷递归调用。有人说为什么不写一个返回条件来结束递归?事实上这儿都不会进入函数的内部,每当传值传参时就会形成一个新的拷贝构造。
同样的Date
类,如果用已经存在的d1
拷贝构造d2
,此处会调用Date
类的拷贝构造函数。但Date
类并没有显式定义拷贝构造函数,则编译器会给Date
类生成一个默认的拷贝构造函数。
注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。
关于const
引用的补充:
//b -> 只读; a -> 可读可写 --- 权限缩小
int a = 10;
const int& b = a;
//ax -> 只读; bx -> 可读可写 --- 权限放大 (报错)
const int ax = 10;
int& bx = ax;
const
,此时b
可以使用a
的值但不可以修改,这属于权限的缩小。 但权限的放大是不被允许的!const int& m = 10; // 常量
const int& n = a + b; // 临时对象
//int& n = a + b; //权限放大
const
常引用,其中a + b
是一个表达式,其返回值是一个临时变量,且临时变量具有常性! 这就引出了函数接收参数的方式,:Func(const int& x) {}
int main()
{
func(10);
func(a + b);
return 0;
}
const
,这样常量和表达式也可以进行传参了。double d = 1.1;
int i = d; //隐式类型转换
const int& ri = d;
double
类型赋值给int
类型时会发生隐式类型转换,此时d
会创建一个int
类型的临时变量,并将此临时变量传给i
。则int&
接收double d
是不行的,需要const
引用,ri
为临时变量的别名。int i = 97;
char a = 'a';
if(i == a) {}
char a
一字节,int i
四字节,两者并不能直接比较。与i
比较的是整型提升后的a
的临时变量!// 这里会发现下面的程序会崩溃掉?这就是浅拷贝的问题
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (_array == nullptr)
{
perror("malloc fail:");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(int val)
{
//CheckCapacity();
_array[_size++] = val;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = _size = 0;
}
}
private:
int* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2(s1);
}
此处应该实现深拷贝 --> 开辟一段与s1
类中_array
一样大的空间,并将_array
中的值拷贝下来。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数(实现深拷贝)是一定要写的,否则就是浅拷贝。
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 temp(d);
return temp;
}
int main()
{
Date d1(2024, 6, 22);
Test(d1);
return 0;
}
为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator
后面接需要重载的运算符符号。
函数原型:返回值类型 operator
操作符(参数列表)
注意:
operator@
+
,不 能改变其含义this
.* :: sizeof ?: .
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。传统的比较日期类写法,函数的命名是一件困难的事,容易引人误解且需要将成员变量设为公有:
bool DateEqual(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
全局的operator==
。这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证? 这里其实可以用我们后面学习的友元解决,也可以定义一些Get()
函数来获取成员变量,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
//调用处: d1 == d2 => operator==(d1, d2);
重载为成员函数。这里需要注意的是,左操作数是this
,指向调用函数的对象。
//bool operator==(Date* this, const Date& d2)
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
//调用处: d1 == d2 => d1.operator==(d2);
const T&
,传递引用可以提高传参效率T&
,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值*this
:要复合连续赋值的含义Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
编译失败: error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?这一点与编译器默认生成的拷贝构造函数相似,
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。