【C++】类和对象(1)
文章目录
- 一、类的6个默认成员函数
- 1.1 构造函数
- 1.2 析构函数
- 1.3 拷贝构造函数
- 1.4 赋值运算符重载
- 1.5 取地址及const取地址操作符重载
- const成员
一、类的6个默认成员函数
如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
假如我们创建了一个类 - 队列,当我们使用队列这个类创建了一个对象后,有两个必须要考虑的操作:初始化和销毁(特别是在堆区上开辟了空间的)
初始化倒还好,一般人不会忘记,但销毁,很多人会在不经意间忘记,从而导致内存泄漏。c++之父深感其害,于是他想了一个办法,让初始化和销毁操作自动完成,不需要让程序员自己手动调用。而这就有了6个默认函数中的构造函数和析构函数
1.1 构造函数
构造函数的作用是完成对象的初始化。对象在创建之后便会自动调用它。
那如何定义一个构造函数呢?
构造函数有以下几个特征:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
构造函数可以重载,如下:
但这样的代码有问题,了解函数重载的读者应该知道,全缺省函数和无参函数重载时,虽然在语法上可以,但在函数调用时出问题。
我们最好保留带缺省参数的哪一个。
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。那我们为什么不直接使用编译器自己生成的构造函数呢?
因为编译器自己生成的默认构造函数非常奇怪。
c++规定:编译器自己生成的构造函数需要对自定义类型做处理,对内置类型(基本类型如char,int……)不作要求。
这中间有两个关键点:
- 编译器对自定义类型做处理:编译器通过调用自定义类型的构造函数来进行初始化。如下:
- 内置类型不作要求:这就是一个非常奇怪的点,可以说是c++的一个bug,所有的自定义类型都是由内置类型组成的,所有的对自定义类型的初始化是通过调用它的默认构造函数来实现的。 也就是说,本质上都是要对内置类型初始化,但c++又对内置类型的初始化不作要求,这就出现下面的问题:
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
1.2 析构函数
析构函数的作用是对对象进行清理。比如:返回对象在堆上开辟的空间。
对象在生命周期结束时自动调用析构函数。
那如何定义一个析构函数呢?
析构函数的特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数。
和构造函数一样,编译器自己生成的析构函数同样有问题:
它不会对内置类型作处理。
所以一般我们也是需要自己写析构函数,除非成员变量全是自定义类型。
1.3 拷贝构造函数
**拷贝构造函数的作用是拷贝一个对象。**它是构造函数的重载。
它的特征如下:
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
拷贝构造函数相比于构造函数有更多的细节问题。
4. 参数为什么要规定为引用?
c++规定了内置类型直接拷贝,自定义类型要调用拷贝构造函数。
- 拷贝有两种类别:浅拷贝和深拷贝。我们定义拷贝构造函数必须要考虑当前拷贝属于那一类别。
如果需要进行深拷贝,使用编译器自己生成的拷贝构造函数则会出错。
请问出错的原因是什么?即使s1和s2的a会指向同一块,但指向同一块空间又不会报错。那它真正错误的原因是什么?
答:不要忘了,s1和s2在销毁时会自动调用析构函数,那将导致s1和s2指向的空间被释放两次,而这就是错误点。
1.4 赋值运算符重载
运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
特征如下:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.* :: sizeof ?: .
注意以上5个运算符不能重载。特别注意.*
(这是一个操作符)
举例
但d1.less(d2)看起来够直观吗?不。我们认为最直观应该像下面这样写:
d1 < d2
,但<
这个运算符不能用来直接比较自定义类型,因此我们需要进行运算符重载
编译器在运行到d1 < d2时便会调用operator<函数
赋值运算符重载
1.3提到的拷贝构造函数只适用于初始化,而对于一般的赋值操作则需要用到赋值运算符重载
赋值运算符重载的特征:
参数类型:const type &,传递引用可以提高传参效率
返回值类型:type&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
其他的运算符重载可以写在全局里,但赋值运算符重载必须定义为成员函数。
这里需要用到this指针,我在这篇博客【C++】类和对象(1)已经讲解过。之前提到,this指针不能在函数外部显示使用(即不能出现在实参和形参)但可以在成员函数内部显示使用。
之前不明白为什么它规定在函数内部可以显示使用,学了赋值运算符重载,才发现这个规定的妙处,不得不佩服祖师爷的智慧。
但转念一想,我为什么要写返回值呀?我已经完成了赋值操作,为什么还要放回一个引用?
如果只是d1 = d2
这种只涉及两个对象的情况,没有返回值也可以。但如果是d1 = d2 = d3 = d4
这样,没有返回值将导致错误?
这就是为什么要写返回值的原因,但为什么要返回引用Date&
,而不返回Date
呢?
为了提高效率。原因:【C++】5. 引用
同拷贝构造函数一样,编译器自动生成的赋值运算符重载函数也只能进行浅拷贝
运算符重载扩展
- 关系运算符重载简化
假如我要重载< > == <= >=
这五个运算符,实际上我只用实现其中两个 ,<
和==
,剩下可以通过逻辑取反来进行。
- 前置一元和后置一元
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递(作用就是占个位置),编译器自动传递
由于后置++存在临时对象tmp, 效率比前置++低,所以自定义类型一般能用前置就用前置
- 用+来实现+=时,总的比用+=来实现+多了一次拷贝,效率低了。
class Date {
public:
//构造函数
Date(int year = 0, int month = 0, int day = 0) {
_year = year;
_month = month;
_day = day;
}
//获取天数
int GetMonthDay(int year, int month) {
static int dayArr[] = { 0,31,30,31,30,31,30,31,31,30,31,30,31 };
if (month == 2 && (year % 4 == 0 && year != 100 == 0) && (year % 400 == 0)) {
return 29;
}
else {
return dayArr[month];
}
}
//拷贝构造函数
Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
//运算符重载
Date& operator=(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
bool operator==(const Date& d) {
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool operator<(const Date& d) {
return (_year < d._year ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day));
}
bool operator<=(const Date& d) {
return (*this < d || *this == d);
}
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);
}
//前置++
Date& operator++() {
_day++;
return *this;
}
//后置++
Date operator++(int) {
Date tmp(*this);
_day++;
return tmp;
}
//前置--
Date& operator--() {
_day--;
return *this;
}
//后置--
Date operator--(int) {
Date tmp(*this);
_day--;
return tmp;
}
// 日期-日期 返回天数
int operator-(const Date& d) {
int ans = 0;
int sum1 = _day, sum2 = d._day, sum3 = 0;
for (int i = 1; i < _month; i++) sum1 += GetMonthDay(_year, _month);
for (int i = 1; i < d._month; i++) sum2 += GetMonthDay(d._year, d._month);
sum3 = (_year - d._year) * 365 + (_year - d._year-1) / 4;
ans = sum1 - sum2 + sum3;
return *this > d ? ans : -ans;
}
// 日期+天数 返回日期
Date operator+(int day) {
Date ans(*this);
ans += day;
return ans;
}
Date& operator+=(int day) {
_day += day;
//2023 5 123
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month > 12) {
_year++;
_month = 1;
}
}
return *this;
}
void Print() {
cout << _year << ' ';
cout << _month << ' ';
cout << _day << endl;
}
private:
int _year = 0;
int _month = 0;
int _day = 0;
};
1.5 取地址及const取地址操作符重载
const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
我们之前提到,成员函数会传隐含参数this,即:void Print() <==> void Print(Date* const this)
,但有时候我不想this指向的内容不能被改变,如果我想将this指向的内容设置为只读,即设置为const Date* const this
,该怎么做呢?如下
那么它有什么用呢?
因此当函数里面只对this进行读取操作,函数最好加上const.
之前已经提到了4个默认成员函数,还剩下2个:取地址和const取地址操作符重载
同之前的4个默认成员函数一样,编译器会自己生成。但有差别的是基本上不用我们自己去写。
使用编译器自己生成的就可以了。