以下为个人学习笔记整理。参考书籍《C++ Primer Plus》
# 使用类
# 运算符重载
C++
允许给运算符赋予多种含义, C++
支持自定义运算符的含义。
// op: +、-、*、[] ... | |
operator op(arguemrnt-list) |
# 计算时间:重载运算符示例
tm.h
头文件:
class Time { | |
private: | |
int hours; | |
int minutes; | |
public: | |
Time(); | |
Time(int h, int m); | |
Time operator +(const Time& t) const; | |
void show() const; | |
}; |
tm.cpp
源文件:
#include "tm.h" | |
#include <iostream> | |
Time::Time() { | |
this->hours = this->minutes = 0; | |
} | |
Time::Time(int h, int m) { | |
this->hours = h; | |
this->minutes = m; | |
} | |
Time Time::operator+(const Time& t) const { | |
Time sum; | |
sum.minutes = this->minutes + t.minutes; | |
sum.hours = this->hours + t.hours + sum.minutes / 60; | |
sum.minutes %= 60; | |
return sum; | |
} | |
void Time::show() const { | |
std::cout << "minues:" << this->minutes << ",hours:" << this->hours << std::endl; | |
} |
使用:
Time t_1{ 20,20 }; | |
Time t_2{ 10,10 }; | |
Time sum_t = t_1 + t_2; | |
Time sum_z = t_1.operator+(t_2); // 两者等效,因为 operator+ 和 + 都被视作 Time 类的成员函数 |
# 重载限制
- 重载后的运算符必须至少有一个操作数是用户自定义的类型。因此用户不能为标准类型进行重载。
- 重载运算符时,不能对运算符本身的句法规则进行修改。例如求模运算符(%),不能只有一个操作数。并且不能修改运算符的优先级。
- 不能创建新的运算符。例如不能定义
**
表示求幂,operator **() // is fail
。 - 如下运算符不支持重载:
sizeof
:sizeof
运算符。.
:成员运算符。.*
:成员指针运算符。::
:作用域解析运算符。?:
:条件运算符。typeid
:一个 RTTI 运算符。const_cast
:强制类型转换运算符。dynamtic_cast
:强制类型转换运算符。reinterpret_cast
:强制类型转换运算符。static_cast
:强制类型转换运算符。
# 可重载运算符:
# 只能通过成员函数进行重载的运算符:
=
:赋值运算符。()
:函数调用运算符。[]
:下标运算符。->
:通过指针访问成员运算符。
# 友元
通常情况下,公有类方法是类对象提供外界访问的唯一途径,但这有时候过于严苛,因此加入友元的概念,进一步对访问控制进行划分。
- 友元函数。
- 友元类。
- 友元成员函数。
通过让函数称为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
# 为何需要友元?
当为类重载二元运算符时。例如乘除操作,往往会涉及到多个不同类型的对象,这时候,友元便派上了用场。
Type_A = Type_A * Type_Int; // 如果在 Type_A 中定义了该操作,那么这段代码有校 | |
Type_A = Type_Int * Type_A; // 但是如果调换两者的位置关系,那么将会报错 |
常见的解决办法:
- 规定写法,只能是
Type_A * Type_Int;
,这显然不是一个好的选择。 - 把运算符设置为一个非成员函数,这会导致没办法通过参数对象访问其私有成员。
# 创建友元函数
创建友元函数的第一步是将函数原型放在类声明中,并且加上关键字 friend
修饰。
friend Time operator * (int c, const Time& t); | |
Time operator*(double m, const Time& t) { | |
Time dot; | |
long total_mintues = t.hours * m * 60 + t.minutes * m; | |
dot.hours = total_mintues / 60; | |
dot.minutes = total_mintues % 60; | |
return dot; | |
} |
友元函数的两个特性:
- 虽然
operator *()
函数的声明是在类声明内,但此次,该函数已经不能视作成员函数,因此不能用time.operator *()
的方式进行调用。 - 虽然
operator *()
函数不是成员函数,但是其访问权限和成员函数一致。
此时的友元函数其实更像一个非成员函数,所以可以用如下方式调用:
Time t{1,1}; | |
Time result = operator *(1.1, t); |
# 常用的友元:重载 <<
运算符
最常见的输出方式就是通过 cout << xxx
的形式,在控制台打印对象的信息。那么如果通过重载 <<
运算符,使得 Time
类也能够被输出呢。
# op.1 使用友元函数
由于 cout
和 time
是两个不同类型的对象,且需要访问 time
的私有成员,因此必须把函数声明为友元函数。
由于该友元函数不需要访问 ostream
类的私有成员,所以可以不必设置为 ostream
的友元函数。
// .h | |
friend void operator <<(std::ostream& os, const Time& t); | |
// .cpp | |
void operator<<(std::ostream& os, const Time& t) { | |
os << t.hours << " hours," << t.minutes << ",minutes"; | |
} |
# op.2 void 返回值存在的问题
上面的友元函数在某些特定的情况下没办法正常工作:
Time t; | |
cout << "is string" << t << "is string"; // 执行到第二个 is string 的时候程序会出错,因为重载后的运算结果是一个 void 而不是 ostream 对象。 |
因此我们需要对函数进行一些调整:
// .h | |
friend std::ostream& operator <<(std::ostream& os, const Time& t); | |
// .cpp | |
std::ostream& operator<<(std::ostream& os, const Time& t) { | |
os << t.hours << " hours," << t.minutes << ",minutes"; | |
return os; | |
} |
friend
关键字只能在函数的原型中使用,切忌用在函数的定义上。
# 重载运算符:作为成员函数还是非成员函数
有些情况下,成员函数是唯一的选择。而在其他情况下,两者并无区别,但对于代码健壮的角度来说,非成员函数是更好的选择。
# 类的自动转换和强制类型转换
通常情况下,将一个标准类型变量赋值给另一中标准类型变量时,如果两种类型兼容,那么将自动发生类型转换:
long count = 8; // int -> long | |
double time = 11; // int -> double | |
int side = 3.33; // double -> int |
当然也不是所有的类型都可以这么玩,还是有挺多例外的,但可以通过强制类型转换强行「矫正」:
int *p = 10;// fail | |
int *p = (int *) 10; // ok |
# 实现自动类型转换的方式 —— 单个参数的构造函数:
class Data{ | |
private: | |
int data; | |
public: | |
Data(double d); | |
Data(float d); | |
Data(int *d); | |
}; | |
Data d = 1.1; | |
Data d = 1.1f; | |
int *p; | |
Data d = p; |
# 关键字 explicit
某些特殊情况下,单一参数的构造函数所支持的这种类似自动类型转换的功能,可能并不是我们所需要的,这时候可以通过 explicit
关键字对构造函数进行限定,从而达到屏蔽该项功能的作用。但任然可以通过显式的类型转换实现。
explicit Data(int *d); | |
int *p; | |
Data d = p; // is fail | |
Data d_1 = Data(p); // is ok |
# 何时会发生自动类型转换
- 将
Data
对象初始化为double
值时。Data d = 1.1;
- 将 都变了值赋给
Data
对象时。Data d; d = 1.1;
- 将
double
值传递给接受Data
参数的函数。void func(Data d); func(1.1);
- 返回值为
Data
的函数试图返回double
值时。Data func(); return 1.1;
- 在上述任意情况下,可以转化为
double
类型的对象,都可以满足以上的自动转换,从而成为Data
对象。
# 强制类型转换 —— 转换函数
- 转换函数必须时类方法。
- 转换函数不能指定返回值类型。
- 转换函数不能有参数。
转换函数的定义:
operator typeName(); | |
// example | |
operator int(); | |
operator double; |
转换函数需要在类中进行声明和定义:
// .h | |
class Data{ | |
private: | |
int data; | |
public: | |
Data(double d); | |
Data(float d); | |
Data(int *d); | |
// covert func | |
operator int() const; | |
operator double() const; | |
}; | |
// .cpp | |
Data::operator int() const{ | |
return data; | |
} | |
Data::operator double() const{ | |
return double(data); | |
} |
隐式转换函数的使用应该小心且谨慎。
# 转换函数和友元函数
转换函数和友元函数往往会造成某些误解:
- 如果定义了类型 Data 和 double 类型的转换。
- 并且定义了 Data 类型和 double 类型的加法操作。
这时如果执行如下代码将会有二义性的问题:
Data d{1}; | |
double dou = 1.1; | |
Data dd = d + dou; // double + double or Data + double ??? | |
// 到底是两个 double 相加呢, 还是一个 Data 和 一个 double 相加,这是一个值得思考的问题。 | |
// 实际的结果是 两个 double 类型进行加法运算。 |