以下为个人学习笔记整理。参考书籍《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
  • 如下运算符不支持重载:
    • sizeofsizeof 运算符。
    • . :成员运算符。
    • .* :成员指针运算符。
    • :: :作用域解析运算符。
    • ?: :条件运算符。
    • typeid :一个 RTTI 运算符。
    • const_cast :强制类型转换运算符。
    • dynamtic_cast :强制类型转换运算符。
    • reinterpret_cast :强制类型转换运算符。
    • static_cast :强制类型转换运算符。

# 可重载运算符:

image-20210305102129115

# 只能通过成员函数进行重载的运算符:

  • = :赋值运算符。
  • () :函数调用运算符。
  • [] :下标运算符。
  • -> :通过指针访问成员运算符。

# 友元

通常情况下,公有类方法是类对象提供外界访问的唯一途径,但这有时候过于严苛,因此加入友元的概念,进一步对访问控制进行划分。

  • 友元函数。
  • 友元类。
  • 友元成员函数。

通过让函数称为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

# 为何需要友元?

当为类重载二元运算符时。例如乘除操作,往往会涉及到多个不同类型的对象,这时候,友元便派上了用场。

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 使用友元函数

由于 couttime 是两个不同类型的对象,且需要访问 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 类型进行加法运算。