文章目录
- 类的六个默认成员函数
- 一、构造函数
- 二、析构函数
- 三、拷贝构造函数
- 四、赋值运算符重载
- 五、const成员
- 六、取地址及const取地址操作符重载
类的六个默认成员函数
如果一个类中什么成员都没有,称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
前面四个默认成员函数很重要,后面两个很少自己实现。
默认成员函数:用户没有显式实现,编译器会自己生成的成员函数称为默认成员函数。
一、构造函数
构造函数的概念
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有
一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数的特性
1.函数名与类名相同。
2.无返回值。
3.对象实例化时编译器自动调用对应的构造函数。
4.构造函数可以重载,在创建对象时根据传递实参来判断调用哪一个构造函数。
注意:通过无参构造函数创建对象时,对象后面不能跟括号,否则就成了函数声明。
5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数
,一旦用户显式定义,编译器将不再生成。
这里显示定义了一个带参的构造函数,编译器将不会生成无参的默认构造函数,由于不存在无参构造函数,所以这样调用是错误的。
一般情况下,我们可以写成全缺省的构造函数,这样既可以无参调用,也可以带参调用构造函数。
6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。因为都可以无参调用,不能同时存在,否则调用会存在歧义。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
class Date
{
public:
//全缺省构造函数 可以认为是默认构造函数
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//可以无参调用
Date d2(2024, 6, 1);//也可以带参调用
return 0;
}
如果我们不写构造函数,使用编译器默认生成的构造函数,会发现打印结果是随机数。
不实现构造函数的情况下,编译器会生成默认的构造函数。d1对象调用了编译器生成的默认构造函数,但是它的_year、_month、_day,依旧是随机值。也就说在这里编译器生成的默认构造函数似乎并没有什么用?
这是因为,C++把类型分成内置类型(基本类型)和自定义类型。
内置类型就是语言提供的数据类型,如int、char、指针等基本类型。
自定义类型就是struct、class、union等自己定义的类型。
7.我们不写,编译器默认生成的构造函数,对内置类型不做处理,对于自定义类型会去调用它的默认构造函数。
可以发现,创建一个A类的对象,会自动调用B的默认构造函数(自己定义的无参构造函数也认为是默认构造函数)。
C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
那我们什么时候自己写构造函数,什么时候不写,使用编译器默认生成的呢?
1.一般情况下,有内置类型成员,就需要自己写构造函数,不能用编译器自己生成的。如果内置类型成员在定义时已初始化且初始化符合我们的要求,这种情况可以不写。
2.全部是自定义类型成员,可以考虑让编译器自己生成。
怎么判断是不是默认构造函数呢?
可以不传参调用的都可以看做是默认构造函数。 无参构造函数、全缺省构造函数、我们不写编译器自己生成的都叫做默认构造函数,都无需传参。没有缺省值需要传参调用的不是默认构造。
二、析构函数
析构函数的概念
与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数的特性
1.析构函数名是在类名前加上字符
~
。
2.无参数无返回值类型。
3.一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。
4.对象生命周期结束时,C++编译系统会自动调用析构函数。
比如我们写一个顺序栈。
typedef int STDateType;
class Stack
{
public:
//给缺省值的默认构造函数 完成初始化工作
Stack(int capacity = 4)
{
_a = (STDateType*)malloc(sizeof(STDateType) * capacity);
if (nullptr == _a)
{
perror("malloc");
return;
}
_capacity = capacity;
_top = 0;
}
void Push(STDateType x)
{
// CheckCapacity();
_a[_top++] = x;
}
//...
//析构函数完成销毁工作
~Stack()
{
if (nullptr != _a)//空指针不能free
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
}
private:
STDateType* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(10);
return 0;
}
是不是比C语言的好写并且好用,不需要我们手动去调用初始化函数和销毁函数,构造函数和析构函数会自动调用。
5.系统自动生成的默认析构函数对内置类型不做处理,自定义类型会去调用它的析构函数。
注意:
1.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如日期Date类;有资源申请时,析构函数一定要写,否则会造成资源泄漏,比如Stack类。
2.析构函数不能重载!
三、拷贝构造函数
拷贝构造函数的概念
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造函数与构造函数的名字相同,二者构成重载关系;拷贝构造是用已存在的类对象来初始化新对象。
拷贝构造函数的特性
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式引发无穷递归调用。//拷贝构造函数 Date(const Date& d)//引用类型 { _year = d._year; _month = d._month; _day = d._day; } int main() { Date d1(2024, 6, 10); Date d2(d1);//调用拷贝构造,用d1初始化d2 return 0; }
3.若未显式定义,编译器会生成默认的拷贝构造函数。默认拷贝构造对于内置类型按照字节拷贝;对于自定义类型则调用它的拷贝构造函数。(这点与构造、析构函数一样)
4.编译器生成的默认拷贝构造函数只能完成浅拷贝也就是按照字节拷贝,涉及到资源申请时需要我们自己写深拷贝。
浅拷贝:通过默认的拷贝构造函数构造的对象,按字节完成拷贝,这种拷贝被称为浅拷贝或者值拷贝。
对于日期类这种内置类型的成员,我们不需要显示定义拷贝构造,使用编译器默认生成的就可以。
但是涉及到动态开辟空间时,如果还使用浅拷贝,就会出现问题。
浅拷贝是按字节进行拷贝,所以s1和s2的指针内容是一样的,即指向同一块地址。所以其中一个修改,另一个也会修改。生命周期结束时,s2先析构,s1再析构,一块空间释放两次,就会出错。
因此,对于需要动态申请资源的类,我们就需要自己写深拷贝。
class Stack
{
public:
//构造函数
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
if (nullptr == _a)
{
perror("malloc");
return;
}
_top = 0;
_capacity = capacity;
}
//深拷贝构造
Stack(const Stack& st)
{
_a = (int*)malloc(sizeof(int) * st._capacity);//重新开辟空间
if (nullptr == _a)
{
perror("malloc");
return;
}
memcpy(_a, st._a, sizeof(int) * st._top);//内容拷贝过来
_capacity = st._capacity;
_top = st._top;
}
//析构函数
~Stack()
{
if (nullptr != _a)
{
free(_a);
_a = nullptr;
_capacity = _top = 0;
}
}
private:
int* _a;
int _top;
int _capacity;
};
int main()
{
Stack s1;
Stack s2(s1);//调用拷贝构造
return 0;
}
拷贝构造函数的调用场景
1.使用已存在的对象创建新对象
Date d1(2024, 6, 10);//构造函数
Date d2(d1);//拷贝构造函数
2.函数参数类型为类的类型对象(传值调用,实参拷贝给形参)
所以我们自定义拷贝构造函数只能传引用,不可以传值,否则会引发无穷递归调用。因为如果拷贝构造的参数类型是类类型的对象(传值),则又会调用拷贝构造即调用自身,于是就会无限递归循环调用,程序会出错。
3.函数返回值类型为类类型对象(传值返回)
注意:不能返回局部对象的引用!
之前通过C++入门篇,我们知道,引用做返回值时是不可以返回局部对象的。
可以看到结果是随机数,其实返回的结果可能是随机数也可能是正确结果。
因为是局部变量,函数结束,栈帧销毁,如果没有清理栈帧,那么返回结果侥幸是正确的;如果清理了栈帧,那么结果就是随机值。
函数参数是类类型的引用或者返回类型是类类型的引用时,是不会调用拷贝构造的,因此效率也比较高。为了提高程序效率,一般对象传参时,尽量使用引用类型;返回时根据实际场景,能用引用尽量使用引用。
四、赋值运算符重载
首先,什么是运算符重载?
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
函数名:关键字
operator
后面加需要重载的运算符符号
运算符重载定义:返回值类型 operator操作符(参数列表)
例如,对于日期类,我们如何进行日期的大小比较呢?
先写一个判断日期是否相等的运算符重载函数。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
判断是否相等的操作符==
是双目运算符,所以参数列表有两个参数。因为传值传参会调用拷贝构造,降低效率,所以用传引用传参;因为不改变实参,所以最好加上const,提高代码健壮性。
但是我们如果重载成全局的,就需要类成员变量是公有属性,因为类外是无法访问私有成员的。但是这样无法保证封装性。
重载为全局函数,则调用方法如下
除了上述方法(不建议,封装性很差),我们嗨可以用友元来解决(下篇内容),或者重载为类的成员函数。
可不能直接这些写,别忘了,每个成员函数都有一个隐藏的this指针,为第一个参数,也就是说,上述函数写法其实有三个参数。所以显示参数只需要写一个。正确写法:
重载运算符的调用方法
以上就是运算符重载的使用方法和注意事项,我们再来写一下日期类的比较<、<=等运算符来练习一下。
bool operator<(const Date& d)
{
if (_year == d._year)
{
if (_month == d._month)
return _day < d._day;
else
return _month < d._month;
}
else
{
return _year < d._year;
}
}
写<=的时候就可以调用前面写的<和==运算符,同理后面的>、>=、!=等都可以这样实现,自己可以练习实现一下。
bool operator<=(const Date& d)
{
return *this < d || *this == d;
}
运算符重载的注意事项:
1.不能创建新的操作符:比如operator@
2.重载操作符必须有一个类类型参数(内置类型不需要操作符重载)
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
5.>*
::
sizeof
?:
.
这5个运算符不能重载。
赋值运算符重载
赋值运算符重载用于自定义类型,在两个及以上已存在的对象之间进行赋值。
Date d1(2024, 6, 1);
Date d2(2000, 1, 1);
d1 = d2;
Date d3, d4;
d4 = d3 = d1;//连续赋值
重载后应实现原本赋值运算符应实现的功能,赋值以及连续赋值等。所以赋值运算符重载是带有返回值的(支持连续赋值),返回引用和传引用可以提高返回效率,因为不用调用拷贝构造(拷贝构造调用的场景)。赋值前可以进行判断优化,相同就不用继续赋值。下面是日期类的赋值运算符重载。
Date& operator=(const Date& d)//传引用提高传参效率
{
if (&d != this)//判断优化
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
前面提到过,引用返回不可以返回临时对象,而*this就是对象本身,作用域在定义的域(此处定义在main()函数中)。
赋值运算符和拷贝构造的区别
赋值运算符:两个已经存在的对象之间进行赋值;
Date d1(2024, 6, 1);
Date d2(2024, 6, 1);
d2 = d1;
//或者
Date d1(2024, 6, 1);
Date d2, d3;
d2 = d3 = d1;//d2,d3是已存在的对象
拷贝构造:用一个已经存在的对象初始化新对象;
Date d1(2024, 6, 1);
Date d2(d1);
//或者
Date d1(2024, 6, 1);
Date d2 = d1;//d2是新创建对象
搞明白底层逻辑,就很容易区分了。
赋值运算符重载也是类的默认成员函数,我们不写,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。(浅拷贝)
和前面的拷贝构造一样,对于内置类型例如日期类这种,我们不需要显示定义赋值运算符重载,使用编译器默认生成的就可以了。但是涉及到资源申请的比如栈这种,浅拷贝就不可以了,需要我们自己显示定义赋值运算符重载完成深拷贝。(参考拷贝构造)
赋值运算符只能重载成类的成员函数不能重载成全局函数。
因为赋值运算符如果不显式实现,编译器会生成一个默认的。此时再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中默认生成的起冲突,所以赋值运算符重载只能是类的成员函数。
五、const成员
在介绍最后两个默认成员函数之前,我们先了解下什么是const成员。
const修饰的类成员函数称为const成员函数,const实际修饰的是隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
如果我们定义一个const的类对象d1,那么这个const对象还可以使用我们之前定义的函数吗?
不可以,之前在C++入门篇中提过,权限不可以放大,只能平移或者缩小。上述Print()函数中有一个隐藏的
Date*
类型的this指针,而d1是const对象,所以传的是const Date*
类型的this指针,所以权限会放大,因此报错。
cosnt成员函数只需要在原本成员函数后面加上cons即可,这样const对象就可以正常调用。
const对象不可以调用非const成员函数(权限放大)
const成员函数不可以调用其他非成员函数(权限放大)
非const对象可以调用const成员函数(权限缩小)
非const成员函数可以调用其他非const成员函数(权限缩小)
总结: 不修改成员变量的成员函数,最好都加上const修饰成const成员函数,这样const对象和非const对象都可以调用。
六、取地址及const取地址操作符重载
这两个默认成员函数基本上很少需要我们自己去实现,一般不需要重载,使用编译器默认生成的即可,除非想让别人通过取地址操作符获取到指定的内容。
class Date
{
public:
Date* operator&()//取地址操作符重载
{
return this;
}
const Date* operator&()const//const取地址操作符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};