目录
5.1. 运算符重载
运算符函数的格式:
operator<op>(<argument-list>)
5.1.1 重载限制
- 重载的运算符不一定要是类成员函数,但至少有一个参数是用户定义的类型(防止用户为标准类型重载运算符)。
- 重载运算符不能违反运算符原来的句法规则。例如,不能将一元运算符重载为二元运算符。
- 不能重载新运算符。
- 不能重载以下运算符:
sizeof
.
:成员运算符.*
和->*
:成员指针运算符::
:作用域解析运算符?:
:条件运算符typeid
:一个RTTI运算符const_cast
:强制类型转换运算符dynamic_cast
:强制类型转换运算符reinterpret_cast
:强制类型转换运算符static_cast
:强制类型转换运算符
- 以下运算符只能重载为成员函数:
=
:赋值运算符()
:函数调用运算符(function object 或 functor)[]
:下标运算符->
:通过指针访问类成员的运算符
5.1.2 成员指针运算符
成员指针运算符.*
和->*
也称成员解除引用运算符。
成员指针运算符用来使用成员指针。
class Example
{
public:
int feet;
int inches;
Example();
Example(int ft);
~Example();
void show_feet() const;
void show_inches() const;
};
Example obj1(16);
Example obj2(24);
Example* pobj = new Example(36);
int Example::*pm = &Example::inches;
cout << obj1.*pm << endl;
cout << obj2.*pm << endl;
cout << pobj->*pm <<endl;
pm = &Example::feet;
cout << obj1.*pm << endl;
cout << obj2.*pm << endl;
cout << pobj->*pm <<endl;
成员指针并不指向特定的内存单元,而是指向类的任意对象中的特定public
成员变量或成员函数。
实际上,*pm
相当于一个成员名。可以用来表识其他任何相同类型的成员。
注意:只有public
成员变量或函数可以让成员指针引用。
将成员指针指向成员函数上:
void (Example::*pf)() const = &Example::show_feet;
(obj1.*pf)();
(obj2.*pf)();
(pobj->*pf)();
pf = &Example::show_inches;
(obj1.*pf)();
(obj2.*pf)();
(pobj->*pf)();
有几点需要注意:
Example::*pf
必须放在括号里。- 即使是对类中的函数名,也必须使用地址运算符
&
。 - 使用时要像
(pobj->*pf)
这样将对象名也放进括号里。
5.2 友元
5.2.1 友元函数
声明友元函数要在声明前加上friend
关键字,并将声明放进类声明中。
在类外部的说明符是无效的,编译器甚至会报错。
友元函数可以访问类的私有和保护成员。
#include <iostream>
using std::cout;
using std::ostream;
using std::endl;
class Date
{
private:
short m, d, y;
friend void show(Date& dt);
friend ostream& operator <<(ostream& os, const Date& dt);
public:
Date();
Date(short m, short d, short y);
~Date();
};
int main()
{
Date today(2, 7, 2020);
show(today);
cout << today << endl;
}
void show(Date& dt)
{
cout << "month: " << dt.m << ", day: " << dt.d << ", year: " << dt.y << endl;
}
ostream& operator <<(ostream& os, const Date& dt)
{
os << "month: " << dt.m << ", day: " << dt.d << ", year: " << dt.y;
return os;
}
// ...狗构造函数和析构函数的实现...
输出:
month: 2, day: 7, year: 2020
month: 2, day: 7, year: 2020
注意到,类中的访问控制说明对友元函数没有效果,这是因为友元函数并不在类的名称空间中。
5.2.2 友元成员函数
5.2.3 友元类
5.3 类的自动类型转换和强制类型转换
5.3.1 将其他类型转换为本类型(构造函数)
class Stonewt
{
// ...
public:
Stonewt(double d);
// ...
};
Stonewt mycat
mycat = 19.6;
在类声明中声明了接受一个double
参数的构造函数。
下面两行代码等效:
mycat = 19.6;
mycat = Stonewt(19.6);
这里隐式调用了构造函数创建了一个临时对象,并将临时对象内容复制到mycat
中(临时对象会在之后的某一时刻被清除)。
这一过程称为隐式(类型)转换,也称为自动(类型)转换。
隐式转换发生在以下情况中:
- 用
double
值初始化Stonewt
对象时。 - 将
double
值赋值给Stonewt
对象时。 - 将
double
值传递给接受Stonewt
参数的函数时。 - 返回值被声明为
Stonewt
的函数试图返回double
时。 - 在上面任一情况中,使用可转换为
double
的内置类型时。
C++中还提供了explicit
关键字关闭这种特性,指明需要显式写出构造函数。
语法就像这样:
explicit Stonewt(double d);
声明为explicit
时,仍可以显式使用这样的构造函数来进行显式类型转换。
5.3.2 将本类型转换为其他类型(转换函数)
转换函数时C++中的一种特殊的运算符函数。
基本语法如下:
operator <typename>();
class Stonewt
{
// ...
public:
double();
// ...
};
Stonewt mycat
mycat = 19.6;
有趣的是,typename
还可以是用户定义的类型。
class Weight
{
double wt;
public:
Weight(double wt){this->wt = wt;}
};
class Stonewt
{
double st;
public:
operator Weight(){return Weight(st);}
Stonewt(double d){st = d;}
};
int main()
{
Stonewt mycat(19.6);
Weight wt = mycat;
return 0;
}
使用转换函数有以下限制:
- 转换函数必须是类函数。
- 转换函数不能由用户指定返回类型。
- 转换函数不能有参数。
如果定义了多个转换函数,还需要注意避免二义性。
这时就需要使用显式类型转换,说明要调用的转换函数。
另外,还可以使用explicit
关键字。这是C++11中新增的特性。
5.3.3 友元函数与成员函数
大多数情况下,友元函数和成员函数可以用来实现对方能实现的功能,但是在以下情况下例外。
存在Stonewt
类,Stonewt
类中定义了构造函数Stone(double)
,(没有定义duoble()
函数)。
现在分别用成员函数或友元函数的方式定义operator+
函数。
// 成员函数的声明
Stonewt operator+(const Stonewt& st) const;
// 友元函数的声明
friend Stonewt operator+(const Stonewt& st1, const Stonewt& st2);
然后又如下代码:
Stonewt mycat(19.6);
double yourcat = 18.6;
Stonewt total;
total = yourcat + mycat; // 只有友元函数能实现
以上代码只有用友元函数定义的operator+
才能实现。
因为yourcat
是一个:double
类型的变量,使用友元函数的可以这样:
total = operator+(yourcat, mycat);
// friend Stonewt operator+(const Stonewt& st1, const Stonewt& st2);
虽然友元函数的第一个参数类型为Stonewt&
,但是可以通过构造函数Stone(double)
隐式生成一个临时对象,并将临时对象的内容复制给st1
。
但是double
没有下面这样的成员函数(C++也不允许用户为内置类型定义成员函数):
Stonewt operator+(const Stonewt& st) const;
// 或者这样
double operator+(const Stonewt& st) const;
另外,以上问题还可以通过重载operator+
的方法解决。
Stonewt operator+(double, const Stone&);
使用这种方法需要更多的工作,但是不需要进行类型转换,减少了时间和内存开销。
5.4 浅复制&深度复制
5.4.1 复制构造函数
Class_name(const Class_name&);
如果用户未定义复制构造函数,C++会自动在必要时提供一个进行浅复制的复制构造函数。
如果在类中使用了动态内存分配(有一个指针成员变量指向内存地址),浅复制只会简单地复制指针成员变量的值,而不是将指针指向的内容复制一份。
这里就需要自己定义一个复制构造函数进行深度复制。
5.4.2 赋值运算符
Class_name& Class_name::operator=(const Class_name&);
用一个对象给另外一个已有的对象赋值时,会调用赋值运算符函数。
用一个对象用赋值的方式初始化另一个对象时,可能也会直接调用复制构造函数,也可能会调用赋值运算符函数。
这里总结一下,使用使用=
运算符初始化总是可能会直接调用匹配的构造函数。
Stonewt new_obj = Stonewt(13.4);
Stonewt new_obj = 13.4;
// Stonewt new_obj(13.4);
// 也可能会先生成一个临时对象,然后使用复制构造函数
// 用赋值语句初始化的可能过程我也没弄清楚,以下为我的猜测
Stonewt new_obj = existed_obj;
// Stonewt new_obj(existed_obj);
// operator=(new_obj, existed_obj);
OK,回来。C++提供的默认赋值运算符也是进行浅复制,因此也可能需要由用户自定义深度复制的版本。
与复制构造函数相比,有两点需要注意:
- 首先要检查自我赋值,如果是自我赋值,应直接返回(返回值一般为
*this
,因为可能要连续赋值。 - 指针所指内存大小改变时,释放原来动态分配的内存,再分配新的内存。
5.5 使用动态内存分配要注意的问题
new
对应delete
,new[]
对应delete[]
- 析构函数只有一个,所以在构造函数中要么都使用
new
,要么都使用new[]
。 - 考虑用
new type_name[1]
代替new type_name
。
5.6 定位new运算符
如果使用定位new运算符将类的对象(数组)“放在”动态分配的内存上,有两点要注意。
- 不要用
delete([])
释放类的对象(数组),因为定位new运算符只是将指针的值强制转换为void*
,并没有分配内存。 - 如果这个类中使用了动态内存分配,要先显式地调用析构函数(其中有
delete([])
语句)。