目录
一.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数。一个类,在我们不写的情况下会自动生成以下六个默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载函数、两个取地址重载函数,C++11后新增了两个默认成员函数:移动构造和移动赋值,以后有机会再讲解,本文集中于介绍前六个默认成员函数。
二.构造函数
构造函数是特殊的成员函数,虽然名字是叫构造,但实际上构造函数的主要任务是初始化对象而不是开空间创建对象,通常我们使用的局部对象是栈帧创建时,空间就开好了。构造函数的本质是要替代我们以前Stack类中实现的Init函数的功能,构造函数的自动调用的特征完美替代了Init
构造函数有以下特点:
- 函数名与类名相同
- 无返回值(意思是什么也不要写,包括void)
- 对象实例化时系统会自动调用对应的构造函数
- 构造函数可以重载
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数
- 默认构造函数:不传实参就可以调用的构造就叫默认构造。有三种类型:无参构造函数、全缺省构造函数、编译器自动生成的无参构造函数。这三个函数有且只能存在一个,不能同时存在。
- 编译器自动生成的默认构造,对于内置类型的初始化没有要求,取决于编译器。对于自定义类型,会调用该成员变量的默认构造函数初始化,若没有默认构造函数,则会报错。
用Date类的实现来展现一下构造函数:
//Date类的实现
class Date
{
public:
//无参构造函数
//Date()
//{
// _year = 0;
// _month = 0;
// _day = 0;
//}
//有参构造函数
//Date(int year, int month, int day)
//{
// _year = year;
// _month = month;
// _day = day;
//}
//全缺省构造函数,这个和第一个无参构造只能存在一个
//同时也要屏蔽第二个,否则会报错,因为有参时不知道调用谁
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//不能写为Date d1();这样会有歧义,分不清是不是函数声明
Date d1;
d1.Print();
Date d2(2024, 1, 1);
d2.Print();
return 0;
}}
三.析构函数
析构函数与构造函数功能相反,析构函数不是完成对象本身的销毁,而是完成对象中资源的清理释放工作。局部对象是存在栈帧的,函数结束栈帧销毁,它就释放了,不需要我们手动清理。析构函数的功能就类似于Stack实现中的Destroy功能,对于Date日期类而言,没有资源需要释放(没有用malloc等开辟出来的资源空间)因此Date是不需要析构函数的。
析构函数的特点:
- 析构函数要在类名前加上~
- 无参数无返回值(跟构造类似)
- 一个类只能有一个析构函数,若未显式定义,则由编译器自动生成默认的析构函数
- 对象生命周期结束,系统自动调用析构函数
- 编译器自动生成的构造函数对内置类型不做处理,对自定义成员调用它对应的析构函数
- 类中若没有资源申请,可以不写析构函数,直接使用编译器默认生成的,但若有资源申请时,一定要亲自去写,否则会造成资源泄露
- 一个局部域的多个对象,C++规定后定义的先析构
例如,对于有资源申请的Stack而言,它的析构函数就是这样:
~Stack()
{
free(_a);
_a = nullptr;
_capacity = 0;
_top = 0;
}
四.拷贝构造函数
拷贝构造是一个特殊的构造函数,它是构造函数的一个重载,它的第一个参数是自身类类型的引用,且任何额外的参数都有默认值。
class Date
{
public:
//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_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;
};
int main()
{
Date d1(2024, 7, 13);
d1.Print();
//Date d2 = d1也是等价,都是拷贝构造
Date d2(d1);
d2.Print();
return 0;
}
C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,在自定义类型传值传参和传值返回的时候都会调用拷贝构造函数。由于上述特性,在写拷贝构造时,参数一定要写正确,是类类型的引用,不是引用而是传值传参:Date(const Date d)则会引发无穷递归
当使用Date d2(d1)时是调用拷贝构造函数,调用函数先要传参,这里传参d1对应的是Date类型,也就是传值传参,要调用拷贝构造函数,而调用拷贝构造函数又要进行传值传参,接着又进行调用拷贝构造,一直重复下去形成了无穷递归.......
因此写拷贝构造函数时一定不能写传值传参,那样会继续调用拷贝构造形成无穷递归,应该使用引用,若是Date(const Date& d)就不是传值传参,该引用传参不会调用拷贝构造,因此传参后就直接开始执行拷贝构造函数内部代码了,完成拷贝构造。
若未显式定义拷贝构造,编译器会自动生成拷贝构造函数。自动生成的拷贝构造会对内置类型进行值拷贝(浅拷贝),也就是一个字节一个字节的拷贝,对自定义类型成员变量则调用它自身的拷贝构造。这种浅拷贝方式适用于Date类这种没有资源申请的类类型,但不适用于Stack这种有资源申请的,因为浅拷贝只能将值一一拷贝,例如Stack中int* a的地址都拷贝过来,那么这两个栈就都指向同一个区域了,这显然不符合我们的要求,此时就需要我们自己手动写拷贝构造来完成深拷贝的任务了。
除了上述传值传参时调用的拷贝构造,在传值返回时也会调用拷贝构造,例如:
//传值返回
Stack func()
{
Stack st;
return st;
}
在这种情况下,由于函数结束,该局部域中创建的对象st生命周期就已经到了,对象就已经释放了,此时返回的就是st的拷贝构造,是一个临时对象。注意:此时就不能使用引用返回了,若写为:Stack& func(),返回的就是st这个已经释放的对象的别名,这就是野引用了,需要注意。
五.运算符重载
当运算符被运用于类类型的对象时,C++允许我们通过运算符重载的形式定义新的含义。例如:平常经常使用+-等运算符适用于内置类型,却不适用于自定义的类类型,此时就需要使用运算符重载:运算符重载是具有特殊名字的函数,它的名字是由operator和后面要定义的运算符组成,也具有返回值和参数列表,例如:
class Date
{
public:
Date(int year = 0, int month = 0, int day = 0)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
//全局定义的==运算符重载,判断日期是否相等
bool operator==(const Date& d1,const Date& d2)
{
return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}
其中operator==就是函数名,使用的时候可以直接调用,如:operator==(d1,d2),当然也可以直接写成:d1 == d2,编译器会自动转化为上述那种形式。
不过以上运算符重载是在全局定义的,因为类中的成员变量是公有的才能如此轻松访问,但一般成员变量都是定义为私有,那么此时解决方法就有两个:创建get成员变量的公有函数,或者更为简单地将运算符重载直接定义到类中,其实运算符重载函数就成为了类的成员函数,应有了隐含的this指针,能够访问类中私有成员,此时就只需要传参另一个类的引用即可,例如:
//在类中定义,隐含Date* const this
bool operator==(const Date& d)
{
return _year == d._year && _month == d._month && _day == d._day;
}
运算符重载的特性有:
- 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数(顺序不能调换)
- 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致
- 不能通过连接语法中没有的符号来创建新的操作符:比如operator@
- .* :: sizeof ?: .(点) 注意以上5个运算符不能重载
- 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义
- 一个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义(日期-日期=间隔天数),但是重载operator+就没有意义
六.赋值运算符重载
赋值运算符重载是一个默认成员函数,用于两个已经存在的对象直接的拷贝赋值,这里要跟拷贝构造区分,拷贝构造是构造函数,用于初始化一个新创建的对象。
赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议写成const当前类类型的引用,否则传值传参会有拷贝,效率降低。同时,应当具有类类型引用的返回值,引用是为了提高效率,有返回值是为了支持连续赋值的场景
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(const Date& d)
{
cout << " Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
//赋值运算符重载
Date& operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
//this是d1的地址,*this就是d1
// d1 = d2表达式的返回对象应该为d1,也就是*this
return *this;
}
void Print()
{
cout << _year <<"/"<< _month <<"/"<< _day << endl;
}
private:
int _year;
int _month;
int _day;
};
没有显式实现时,编译器会自动生成一个默认赋值运算符重载,它的行为跟默认拷贝构造函数类似,都是值拷贝(浅拷贝),也就是一个字节一个字节地拷贝,对于自定义类型也是调用其对应的赋值运算符重载。那么写不写也是同理,对于有资源调用的应当自己写,没有的就可以不写。
七.取地址运算符重载
在此先介绍一个const成员函数,使用const关键字修饰的成员函数,相当于用const修饰了this指针,例如在Date类中:原本的Date* const this变味了const Date* const this
//Date类中的Print成员函数
//隐含的Date* const this变为const Date* const this
void Print() const
{
cout << _year <<"/"<< _month <<"/"<< _day << endl;
}
新增的const修饰在*号的左边,表明修饰的是this指向的值不能被修改,也就是类中成员都不能被修改。
若不加const修饰,在如下场景就会出错,d1是const Date类型,那么d1调用Print时,传过去d1的地址就是const Date*类型,但现在Print没有用const修饰,隐含的this就是Date*类型,这就是典型的权限放大,会报错。
void Print()
{
cout << _year <<"/"<< _month <<"/"<< _day << endl;
}
const Date d1(2024, 7, 14);
d1.Print();
因此为了应对这些情况,建议在不改变对象的前提下,尽量在函数后都加上const
至于取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。
Date* operator&()
{
return this;
// return nullptr;
}
const Date* operator&()const
{
return this;
//return nullptr;
}