上篇文章分享了一些构造函数和析构函数的易错点,这篇文章则将继续分享一些构造函数、拷贝构造函数的易错点。
1.变量声明处赋缺省值
我们已经知道了自动构造函数的初始化规则了。我们可以认为这个初始化规则比较保守,能不修改成员变量的值就不修改,只有在自定义类型明确规定了初始化方案后才会去初始化。
但是,有的时候我们又觉得不太方便,因为很多时候我们都想让成员变量被初始化的。因此规定了一种给成员变量赋初始值的方法,在没有显式定义构造函数时,能将成员变量按我们的想法来自动初始化。
这里可以看到定义后_x和_y按照声明处的缺省值进行赋值了。
但如果有构造函数那应该怎么赋值呢?
我们可以看到,当实例化对象时成员变量首先会被按照声明处的缺省值进行赋值。
然后再对c1进行初始化。
最后调用构造函数,这个时候_x和_y就会得到我们想要的初始值了。
从上面的顺序可以看出,成员变量的缺省值可以让我们的成员变量创建时就得到一个值,如果后续有构造函数,再按照规则对这些成员变量进行覆盖,如果是编译器生成的默认构造函数,那么不会进行任何处理,此时我们的成员变量就是声明处的缺省值。
2.析构函数
析构函数和构造函数有相似之处:自动生成的默认析构函数同构造函数的自动生成(内置类型不处理,自定义类型调用它的析构函数)。
析构函数和构造函数有一个区别:析构函数可以显式调用,而构造函数不能显式调用。
析构函数的使用场景:如果有资源要清理(堆区开辟空间),防止调用自动生成的析构函数导致的内存泄漏,就要写析构函数。但是如果嵌套的自定义类型有构造和析构函数,也可以不显式地写。内置类型的变量是自动变量,是编译器自动开辟的,不需要人为销毁,因此也不需要写析构函数。
3.拷贝构造函数
拷贝构造可以说是构造函数的重载,它看起来和构造函数十分相似,但是它的使用场景、功能还是有很大的区别的。
拷贝构造最大的用处就是使得类的对象的定义可以像内置类型那样,使用已经存在的对象以赋值的形式来初始化新的对象。
在这里可以看出,在C这个类中的函数可以访问调用该函数的对象的私有成员,也可访问同类型的参数对应的私有成员。但是如果在C1类中传了个C2类的参数,自然就不能正常访问
注意事项:
(1)拷贝构造的实现的参数有且仅有一个,是同类型的引用,否则会发生无穷递归:
要理解这个,我们要探究拷贝函数为什么不能用传值调用,本质是要理解如果传值的话形参和实参之间本身就要进行一次拷贝,这样就会永远去调用拷贝构造。
要解决这个无穷递归的情况,可以选择传址或传引用,因为这两者本质相同,而传引用不用显式地写&,从而实现C c2 = c1这样方便的写法,所以规定拷贝构造都是传引用。
(2)默认拷贝构造:默认拷贝构造是浅拷贝(值拷贝),直接按字节拷贝,如果有指针会导致两个指向同一块空间,如栈、队列等数据结构。因此,我们多采用深拷贝来处理这些情况,这个时候就必须显式实现拷贝构造函数
(3)拷贝构造和析构函数的使用场景:没有资源管理(堆空间开辟)的情况可以不写拷贝构造,内部有指针或有一些值指向堆,需要显式写析构函数,也要写拷贝构造,如栈、队列、二叉树等数据结构。我们发现它们需要显式定义的场景很相似
(4)普通构造和拷贝构造之间的误区:普通构造不会干扰拷贝构造,即参数必须是引用才算拷贝构造,否则编译器都会自己生成拷贝构造函数:调用自动生成的拷贝构造,内置类型浅拷贝,自定义类型调用它的拷贝构造
4.默认构造:无参、全缺省、不写构造函数编译器自己生成的构造函数,默认构造就是不传参数,自动调用的就是默认构造。
5.运算符重载
也可叫操作符重载,可以重载多种符号
operator>构成函数名,可以直接用a > b,可以支持比较了。
> < >= <= == != << >> + - * / % += -= *= /= %= 等都可以构成运算符重载
但 .* : : sizeof ? : . 这五个运算符
.*(成员函数取地址要加&才能取到函数指针,全局函数的可以不加,调用时要用.*才能访问函数。用函数指针来访问类里面的函数,.相当于取成员,*是函数指针解引用,temp.*func,使用场景较少。)
注意事项:
(1)operator不能链接其他符号如@#等无实际运算意义的构成新的操作符,要加运算符
(2)运算符重载的返回值按照实际含义给,注意返回值加不加引用的区别(在重载函数内定义的变量不能返回引用,这和野指针同理)
(3)运算符重载必须要有一个类的类型,不能完全对内置类型进行该操作,如不能对int-int进行重载操作
(4)对于内置类型的运算符,含义最好不要改变,不要把+实现成了+=
(5)要对类里面的数据进行运算符重载,访问私有成员,常用重载operator为成员函数:
重载operator作为成员函数时,它的第一个参数强制是this指针,形参比实参少1,看上去只有两个参数其实有3个,而参数多于运算符的操作数的时候,就会报错。
因此,代码上要调整,要注意函数内实现的顺序,转换调用d1 == d2相当于d1.operator==(d2),注意根据后者的实质调用来写代码,有的调换方向后功能完全不同了。
(6)调用运算符重载时会根据传参的类型优先到类里面去找,然后去全局找。
(7)可以显式调用运算符重载,也可以转换调用(编译器自动处理),一般推荐后者,更直观
6.赋值重载(赋值拷贝)
注意事项:
(1)和拷贝构造的区别:赋值重载是已存在的对象给另一个已存在的对象赋值,拷贝构造是已存在的对象给另一个要初始化的对象拷贝值
(2)operator=赋值重载要考虑写返回值,当遇到连续赋值的时候能够处理,注意可以考虑使用引用作为返回值,因为传值返回要调用拷贝构造(效率受到影响),返回引用就不会了,返回的是别名(实际上是指针),但返回后可能对象被销毁出现野引用(会产生越界访问),引用返回是存在风险的。所以要特别注意是否有在函数里创建变量并把它的引用返回的情况发生
(3)默认的赋值运算符重载类似于拷贝构造,都是按字节拷贝(浅拷贝),如果遇到深拷贝的情况,要自己显式地实现
(4)默认成员函数的规定:赋值重载函数不能写成全局的。注意运算符重载可以,运算符重载不是六大默认成员函数之一。
(5)区别赋值重载函数和运算符重载函数。这两个不一样,赋值重载函数可以自动生成且只能作为成员函数,运算符重载函数不自动生成且可以在全局定义
7.直接在类里面定义的函数默认是内联函数,不用写inline,会自动展开。建议频繁调用的函数直接定义在类里,不要声明和定义分离。
8.运算符重载的复用
基本思路:先实现几个基本功能,后续功能直接嵌套,用逻辑联系起来。
这样能极大减少代码错误和编写代码的效率。
下面是日期类实现中复用的经典体现:
Date& Date::operator+= (int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_year++;
_month = 1;
}
}
return *this;
}
Date Date::operator+ (int day)//日期+天数
{
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date::operator-= (int day)//日期-天数
{
_day -= day;
while (_day <= 0)
{
_month--;
if (!_month)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator- (int day)//日期-天数
{
Date tmp = *this;
tmp -= day;
return tmp;
}
Date Date::operator++ (int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
Date Date::operator-- (int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
Date& Date::operator++ ()
{
return *this += 1;
}
Date& Date::operator-- ()
{
return *this -= 1;
}
int Date::operator- (const Date& date)
{
int count = 0;
Date tmp = *this;
while (1)
{
--tmp;
count++;
if (tmp == date)
{
break;
}
}
return count;
}
bool Date::operator> (const Date& date)
{
if (_year > date._year)
{
return true;
}
else if(_year < date._year)
{
return false;
}
else
{
if (_month > date._month)
{
return true;
}
else if (_month < date._month)
{
return false;
}
else
{
if (_day > date._day)
{
return true;
}
return false;
}
}
}
bool Date::operator>= (const Date& date)
{
return *this > date || *this == date;
}
bool Date::operator< (const Date& date)
{
return !(*this >= date);
}
bool Date::operator<= (const Date& date)
{
return *this < date || *this == date;
}
bool Date::operator!= (const Date& date)
{
return !(*this == date);
}
Date& Date::operator= (const Date& date)
{
_year = date._year;
_month = date._month;
_day = date._day;
return *this;
}