一、拷贝构造函数
上篇结尾谈到,在C++中,类在定义时编译器会自动生成6个默认成员函数,如果我们自己已经实现,则编译器不会再生成。
分别是构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址操作符重载、const取地址操作符重载;
拷贝构造函数实际上是构造函数的函数重载,声明如下:
class Date
{
public:
// 拷贝构造函数
Date(const Date& d);
private:
int _year;
int _month;
int _day;
};
拷贝构造函数无返回值类型,函数名必须与类名相同,参数只有一个,且必须是类类型对象的引用。
拷贝构造函数是用一个已经存在的对象去初始化另一个对象。
如图所示,d2在创建时用d1初始化,调用类的拷贝构造函数,将d1的值拷贝给d2。d3在创建时调用构造函数,用构造函数中的缺省值来初始化。
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
cout << "Date(int year = 1900, int month = 1, int day = 1)" << endl;
if (month < 1 || month>13 || day<1 || day>GetMonthDay(_year, _month))
{
cout << "输入错误" << endl;
}
}
Date::Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
如图所示,d2调用了拷贝构造函数。
二、赋值运算符重载
运算符重载
在上述的日期类中,可能会需要计算某个日期再过几天之后是什么时间,例如计算2023/2/15再过100天是几月几日。我们当然可以用函数来实现;
Date Date::AddDay(int day);
调用的时候这样调用:
Date d1(2023, 2, 15);
d1.AddDay(100);
C++为了增强代码的可读性,支持了运算符重载。关键字是operator。
Date Date::operator+(int day);
// ...
Date d1(2023, 2, 15);
d1 + 100;
很明显,第二种代码的可读性更好。在运算符重载时,需要注意以下几点:
关键字operator后面接需要重载的运算符符号;
作为类成员函数重载时,形参比操作数少1个,是因为第一个参数默认是this。且传递的参数的顺序就是操作符的操作数的顺序。
Date Date::operator+(int day);
// ...
Date d1(2023, 2, 15);
d1 + 100;
//下面的情况会报错, 因为在重载+运算符时,第一个操作数是Date对象的指针,不是int
100 + d1;
有5个运算符无法重载:.* , :: , sizeof , ?: , . ;
2.赋值运算符重载
Date& operator=(const Date& d);
赋值运算符重载的声明如上,参数是一个const Date的引用,const表示d的内容无法被改变,同时,为了应对连续赋值的情况,需要一个Date类型的返回值,为了减少拷贝次数,使用引用做返回值。
Date& Date::operator=(const Date& d)
{
cout << "operator=()" << endl;
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
需要注意的是,赋值运算符重载必须被定义为成员函数。当我们在全局声明赋值运算符重载时,因为在类中没有声明,所以编译器会自动生成一个默认的赋值运算符重载,在调用时两个运算符重载冲突了,所以赋值运算符重载只能是类的成员函数。
编译器默认生成的赋值运算符重载,是以值的方式逐字节拷贝,如果类中的成员有自定义成员变量,则会调用对应的赋值运算符重载。
3.前置++与后置++重载
前置++和后置++都是一元运算符,也就意味着他们的操作数都只有对象,为了能让前置++和后置++正确重载,C++规定:
后置++在重载时要多一个int类型的参数,但调用时不需要传参,由编译器自动传递;
// 前置++重载,返回++之后的值
Date& Date::operator++()
{
*this += 1;
return *this;
}
// 后置++重载,返回++之前的值
Date Date::operator++(int)
{
Date d(*this);
*this += 1;
return d;
}
三、const成员函数
将const修饰的成员函数称之为const成员函数,const在修饰类的成员函数时,实际修饰的是该函数的隐含this指针,表明在该函数中,类的成员变量不可修改;
需要注意的是,const修饰类成员函数,需要放在函数的后面;
void Date::Print()const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
需要注意:
const对象不能调用非const成员函数,因为会导致权限的放大;
const成员函数内部不可以调用非const成员函数,也是会导致权限的放大。
四、取地址及const取地址操作符重载
// 取地址运算符重载
Date* Date::operator&()
{
return this;
}
// const取地址操作符重载
const Date* Date::operator&()const
{
return this;
}
这两个运算符一般不会由我们自己实现,只有在某些特定情况下,比如这个类不想让别人拿到它的地址。我们才需要显示实现。
五、初始化列表
一些变量在定义时必须赋初值,也就是必须初始化,例如const和引用:
const int a = 0;
// 下面是错误的写法
const int b;
因为const修饰的变量的内容无法被改变,所以必须在变量定义时赋值。
有时在类中会有const成员变量;
#include <iostream>
using namespace std;
class Test
{
public:
Test()
{
_n = 10;
}
private:
const int _n;
};
int main()
{
Test t;
return 0;
}
如果在构造函数中给_n赋值,显然不对;所以必须找到const成员变量是何时定义的,在定义时给const成员赋值,类中给出的成员变量都是声明,并没有定义。所以C++有了初始化列表。
C++规定,类中的成员变量一定会在初始化列表中初始化,初始化列表如下:
class Test
{
public:
Test(int n = 0, int x = 0, int y = 0)
:_n(n)
, _x(x)
, _y(y)
{}
private:
const int _n;
int _x;
int _y;
};
在构造函数参数列表与函数体直接使用: ,可以给成员变量赋值,所有成员变量都会在这里被定义,若没有显示实现初始化列表,内置类型不做处理,自定义类型调用其构造函数。
需要注意:
每个成员变量在初始化列表中最多只能出现一次;
类中如果有const、引用、自定义类型且该类没有默认构造函数时,必须使用初始化列表对其赋初值;
因为对象在定义时一定会执行初始化列表,所以应尽量在初始化列表中初始化成员变量,减少消耗;
成员变量在类中的声明顺序决定了其在初始化列表中的初始化顺序,上述类中的初始化列表顺序如果打乱,仍然会按_n、_x、_y的顺序对成员变量进行初始化。
六、explicit
1.隐式类型转换
// i 是int类型
int i = 10;
// d 是double类型
double d = i;
// i赋给d因为类型不同,会进行一个隐式类型转换,具体过程为
// 先创建一个临时变量,来存放i转化后的值
// 再将临时变量中的值赋给d
// 最后销毁临时变量
double& rd = i;
// rd是一个double类型的引用,因为直接给rd赋值的是i转换为double类型的临时变量
// 这个临时变量赋完值之后就销毁了,因此这个临时变量具有常属性,即不能被修改
// 所以上述的写法是错误的,下面的才是对的
const double& rrd = i;
2.单参数构造函数
class Date
{
public:
Date(int year = 1970)
:_year(year)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2023);
Date d2 = d1;
Date d3 = 2023;
Date& d4 = 2023;
const Date& d5 = 2023;
return 0;
}
在上述的代码中,d1调用构造函数初始化,d2调用拷贝构造函数拷贝d1的值完成初始化;
但是d3却是用int类型直接初始化Date类型,实际上是使用了隐式类型转换;
因为Date的构造函数形参列表中只有一个int类型的形参,因此d3实际上执行的是:
先创建一个Date类型的临时变量并用int类型的2023隐式构造,再将d3调用拷贝构造函数将临时变量的值拷贝到d3中。
如果不想让隐式类型转换发生,可以在构造函数前加关键字explicit。
如图所示,用关键字explicit修饰构造函数后,可以禁止隐式类型转换发生,因此上图中第21行的代码会报错。
3.全缺省与只有一个参数未缺省的构造函数
class Date
{
public:
Date(int year, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d = 2023;
return 0;
}
对于全缺省或者只有一个形参未缺省的构造函数,也可以用隐式类型转换来初始化Date类型的数据;
也可以使用关键字explicit来修饰其来禁止隐式类型转换。
4.多参数构造函数
在C++11标准中,支持了多参数的隐式类型转换。
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d = { 2023,2,23 };
return 0;
}
如上述代码所示,实际原理与上述一样,即用{}中的内容构造一个Date类型的临时变量(隐式类型转换),在调用拷贝构造函数将临时变量的值赋给d。