C++类和对象3
Posted 山舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++类和对象3相关的知识,希望对你有一定的参考价值。
文章目录
一、析构函数
1.概念
上一篇文章中讲到构造函数来初始化一个对象,那么析构函数的作用是什么呢?
已创建的对象在销毁(也即生命周期结束)时会自动调用析构函数,完成类的一些资源清理工作。
注意:构造函数不是创建对象,析构函数不是销毁对象。构造函数初始化对象,析构函数完成资源清理工作。
2.特性
(1)函数名
析构函数的函数名是在类名前加上字符~。
(2)无参数无返回值
注意这里与构造函数一样是无返回值,而不是返回值为空。同时析构函数不需要参数。
(3)一个类有且只有一个析构函数,编译器自动调用
若显式定义,则在对象销毁时自动调用已经写好的析构函数。否则系统会自动生成并调用默认的析构函数。
之前实现的Date类的析构函数其实不需要实现,因为它的成员变量是三个内置类型,且没有动态开辟内存等等的操作,编译器自动生成的已经足够了。
但是下面这个栈的类由于有动态开辟内存,所以就需要自己实现析构函数(注意需要自己实现析构函数不仅限于动态开辟了内存的类,这里以此为例)。
代码如下(示例):
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)* capacity);
_size = 0;
_capacity = capacity;
}
//析构函数,清理资源
~Stack()//无参数,无返回值!!!
{
free(_a);//释放动态开辟的内存
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
(4)编译器生成的默认的析构函数
同编译器生成的默认构造函数一样,编译器生成的默认的析构函数对内置类型不处理,对自定义类型则去调用它自己的析构函数。
二、拷贝构造函数
1.概念
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的对象创建新对象时由编译器自动调用。
2.特性
(1) 拷贝构造函数是构造函数的一个重载
拷贝构造函数和构造函数函数名相同,形参列表不同,所以构成重载。
(2) 拷贝构造函数的参数
拷贝构造函数的参数只有一个,必须使用引用传参,建议在前面加const,使用传值方式会引发无穷递归调用。
(其实传参时传地址可以正常实现功能,但这样就不太像是拷贝构造,而且在C++中建议尽量使用引用)
代码如下(示例):
class Date
{
public:
//默认构造函数
Date(int year = 1970, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造,传引用
Date(const Date& d)
{
//将d的三个成员变量的值依次赋给*this
_year = d._year;
_month = d._month;
_day = d._day;
}
//析构函数什么都不需要做,也可以不写
~Date()
{}
private:
int _year;
int _month;
int _day;
};
若使用传值的方式:
代码如下(示例):
class Date
{
public:
//...
Date(const Date d)
{
//将d的三个成员变量的值一次赋给*this
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
//...
}
int main()
{
Date d1(2021, 7, 9);
Date d2(d1);
return 0;
}
传值本质上就是一个拷贝,以上面的代码为例就是拿main函数中的d1初始化拷贝构造函数中的形参d,但是这本身仍是一个拷贝构造,所以又需要用d1拷贝构造一个形参d,如此下去无穷递归调用。
这里可能较难理解,但只要记住,实现拷贝构造时一定要传引用,最好再加上const防止被修改即可。编译器在这个地方的检查本身也比较严格。上面的代码在VS2013编译器下无法通过编译。
编译结果如下:
(3)系统生成的默认的拷贝构造函数
若未显示定义,系统会自动生成默认的拷贝构造函数。 默认的拷贝构造函数按内存存储的字节序一个字节一个字节依次完成拷贝,这种拷贝一般叫做浅拷贝,或者值拷贝。
(深拷贝会在之后的文章中写到)
对上面的Date类来说系统生成的拷贝构造函数足够了,但对Stack类来说不行。
代码如下(示例):
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int)* capacity);
_size = 0;
_capacity = capacity;
}
~Stack()
{
free(_a);
_a = nullptr;
_size = 0;
_capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}
这里没有实现Stack的拷贝构造,系统自动生成的默认的拷贝构造函数按照字节把st1的内容(三个内置类型)全部拷贝到st2内,也就是说这两个对象内的三个成员变量是完全一致的,其中各自的_a指向同一片空间。在析构函数调用时,对同一块内存空间free了两次,报错。同时若其中一个对象对这块空间的处理会影响另一个对象。
而对自定义类型的拷贝构造则会调用这个自定义类型的拷贝构造。
三、赋值运算符重载
1.运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。这样就可以让自定义类型像内置类型一样使用运算符。
函数名:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator 操作符(参数列表)
运算符重载和函数重载虽然都是重载,但没有关联。函数重载是支持同名函数,运算符重载是为了让自定义类型像内置类型一样使用运算符。
下面看一个例子(省去了不必要的成员函数):比较两个日期类是否相等
代码如下(示例):
class Date
{
public:
bool Equal(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 7, 9);
Date d2(d1);
d1.Equal(d2);
return 0;
}
但是内置类型的比较相等是通过==来实现的,如果Date类也能实现这个运算符,那么可读性会更强。
代码修改如下(示例):
class Date
{
public:
//返回值类型 operator 操作符(参数列表)
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 7, 9);
Date d2(d1);
if(d1 == d2)//编译器会自动转化成d1.operator==(&d1,d2)
{
//...
}
return 0;
}
这样,就可以把各种两个日期的比较都写出来:
代码如下(示例):
class Date
{
public:
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool operator>(const Date& d)
{
if (_year > d._year)
return true;
else if (_year == d._year)
{
if (_month > d._month)
return true;
else if (_month == d._month)
{
if (_day > d._day)
return true;
}
}
return false;
}
//注意下面的代码复用
bool operator>=(const Date& d)
{
return (*this > d) || (*this == d);
}
bool operator<(const Date& d)
{
return !(*this >= d);
}
bool operator<=(const Date& d)
{
return !(*this > d);
}
bool operator!=(const Date& d)
{
return !(*this == d);
}
private:
int _year;
int _month;
int _day;
};
注意:
(1)不能通过连接C语言没有的操作符来创建操作符:比如operator@
(2)重载操作符必须至少有一个类类型或者枚举类型(非内置类型)的操作数。如果全部都是内置类型就没必要重载,所以至少有一个非内置类型的操作数
(3)用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义,仍应该为加法操作
(4)作为类成员的重载函数时,其形参看起来比操作数数目少1,因为成员函数的操作符有一个默认的形参this,不嗯呢该写出且被限定为第一个形参
(5)不能重载的5个运算符
.* (注意不是 * 而是.* )、::(域作用限定符) 、sizeof 、?:(三目运算符) 、.(访问类的成员的操作符)
2.赋值运算符重载
赋值也是拷贝,但是拷贝构造是创建对象时,用同类对象初始化这个新的对象;而赋值是两个对象都已经存在、已经被初始化过,把一个对象的值赋给另一个对象。
代码如下(省去了其它成员函数):
class Date
{
public:
void operator=(const Date& d)//被转化为void operator=(Date* this, const Date& d)
{
if(this != &d)//防止自己给自己赋值
{
//把d的成员变量赋值给*this
_year = d._year;
_month = d._month;
_day = d._day;
}
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d2(2021, 7, 9);
Date d1;
d1 = d2;//会被编译器处理成d1.operator=(&d1, d2);
return 0;
}
下面的赋值运算符重载就可以支持类似i = j = k(i、j、k是三个内置类型)的连续赋值。
代码如下:
Date& operator=(const Date& d)
{
if(this != &d)
{
//把d的成员变量赋值给*this
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
一个类如果没有显式定义赋值运算符重载,编译器也会自动生成一个,这个自动生成的赋值运算符重载与拷贝构造的特性一致,完成对象按字节序的值拷贝。
四、const成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
代码如下:
class Date
{
public:
void print() const//相当于void print(const Date* this)
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
上面的print函数被const修饰(注意const的位置),这样写相当于void print(const Date* this),即该函数内部无法对this修改。
五、取地址及const取地址操作符重载
即下面两种操作符重载:
代码如下:
Date* operator&();
Date* operator&() const;
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载。
感谢阅读,如有错误请批评指正
以上是关于C++类和对象3的主要内容,如果未能解决你的问题,请参考以下文章