《C++ Primer》第14章 重载运算与类型转换(一)

发布时间:2024年01月11日

参考资料:

  • 《C++ Primer》第5版
  • 《C++ Primer 习题集》第5版

14.1 基本概念(P490)

重载的运算符是具有特殊名字的函数,其名字有 operator 和要定义的运算符组合而成。和其他函数一样,重载运算符也具有返回类型、参数列表、函数体。

重载运算符函数的参数数量和该运算符的运算对象数量一样多,对于二元运算符来说,左侧运算对象传递给第一个参数,右侧运算对象传递给第二个参数。除了重载调用运算符 operator() 外,其他重载运算符不能有默认实参。

如果一个重载运算符函数是成员函数,则它的第一个运算对象隐式绑定到 this 指针上。

对于一个重载运算符函数来说,它至少含有一个类类型的参数

8ec4dd2082e6596f5f4ef932fa95612

有四个符号( +, -, *, &既是一元运算符也是二元运算符,具体定义哪种运算符由参数数量决定。

重载运算符不改变优先级结合律

直接调用一个重载运算符

我们可以像调用普通函数一样直接调用运算符函数:

data1 + data2;    // 间接调用
operator+(data1, data2);    // 直接调用
data1.operator+=(data2);    // 直接调用(成员函数)

某些运算符不应该被重载

前面提到过,某些运算符规定了求值顺序(如逻辑与、逻辑或、逗号),由于使用重载运算符本质上是一次函数调用,所以这些求值顺序将无法应用到重载运算符上。另外,逻辑与和逻辑或的短路属性在重载运算符中也不能实现。此外,C++ 语言已经定义了逗号取地址运算符作用于类类型时的含义,所以一般情况下我们也不应该重载它们。

总结:通常情况下,不应该重载逗号、取地址、逻辑与、逻辑或运算符。

使用与内置类型一致的含义

如果某些类操作在逻辑上与运算符相关,则它们适合被定义成重载运算符:

  • 如果类执行 IO 操作,则定义移位运算符使其与内置类型的 IO 保持一致。
  • 如果某个类的操作检查相等性,则定义 operator==operator!=
  • 如果类包含一个单序比较操作,则定义 operator< 和其他比较操作。
  • 重载运算符的返回类型应与内置版本的返回类型兼容:逻辑运算符和关系运算符应返回 bool ;算术运算符返回一个类类型的值;赋值和复合赋值运算符返回左侧运算对象的引用。

选择作为成员或非成员

当我们定义重载运算符时,需要确定将其声明为类的成员函数还是普通函数。下面的准则有助于我们做出选择:

  • 赋值、下标、调用、成员访问箭头运算符必须是成员
  • 复合赋值运算符一般是成员
  • 改变对象状态与给定类型密切相关的运算符,如递增、递减、解引用,通常应该是成员
  • 具有对称性的运算符通常应该是非成员函数

关于最后一点,这里着重解释一下。当我们把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。而如 + 这种具有对称性的运算符,常常会遇到下面这种情况:

string s = "hello";
string t = s + "!";    // 正确
t = "!" + s;

如果 string 的重载 + 是成员函数,那么最后一条语句就是错误的,我们显然不希望这种情况发生。

14.2 输入和输出运算符(P494)

14.2.1 重载输出运算符<<(P494)

通常情况下,输出运算符的第一个形参是一个非常量 ostream 对象的引用,第二个形参是一个常量的引用,返回值为 ostream 形参(引用类型)。

Sales_data的输出运算符

ostream &operator<<(ostream &os, const Sales_data &item){
    os << item.isbn() << " " << item.units_sold << " "
       << item.revenue << " " << item.avg_price();
    return os;
}

输出运算符应尽量减少格式化操作

内置类型的输出运算符不太考虑格式化操作,尤其不会打印换行符

输入输出运算符必须是非成员函数

IO 运算符一般被声明成类的友元。

14.2.2 重载输入运算符>>

Sales_data的输入运算符

istream &operator>>(istream &is, Sales_data &item){
    double price;
    is >> item.bookNo >> item.units_sold >> price;
    if(is)    // 检测输入是否成功
        item.revenue = item.units_sold * price;
    else    // 输入失败,对象被赋予默认状态
        item = Sale_data();
    return is;
}

输入运算符必须处理输入失败的情况

输入时的错误

当读取操作发生错误时,输入运算符应该负责从错误中恢复。

14.3 算术和关系运算符(P497)

通常情况下,我们把算术运算符关系运算符定义成非成员函数,以允许左侧或右侧运算对象进行转换。

算术运算符通常会计算它的两个运算对象并得到一个新值,这个新值常常位于一个局部变量之内,最后返回该局部变量的副本。如果一个类定义了算术运算符,一般也会定义对应的复合赋值运算符,此时最有效的方式是用复合赋值来定义算术运算符

Sales_data
operator+(const Sales_data &lhs, const Sales_data &rhs) {
	Sales_data sum = lhs;
	sum += rhs;
	return sum;
}

14.3.1 相等运算符(P497)

bool operator==(const Sales_data &lhs, const Sales_data &rhs) {
	return lhs.isbn() = rhs.isbn() &&
		lhs.units_sold == rhs.units_sold
		lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs) {
	return !(lsh == rhs);
}

14.3.2 关系运算符(P498)

前面提到过,关联容器和一些算法需要用到小于运算符,所以定义 operator< 会比较有用。此时需要注意,如果类同时也含有 == 运算符,应保证:如果两个对象 == ,则不应有一个对象 < 令一个对象成立;如果两个对象 != ,则必有一个对象 < 另外一个对象。

14.4 赋值运算符(P499)

标准库 vector 支持用花括号内的元素列表赋值:

vector<string> v;
v = {"a", "an", "the"};

同样地,我们也为 StrVec 添加这种赋值方法:

class StrVec{
public:
    StrVec &operator=(initializer_list<string>);
};

StrVec &StrVec::operator=(initializer_list<string> il) {
	auto data = alloc_n_copy(il.begin(), il.end());
	free();
	elements = data.first;
	first_free = cap = data.second;
	return *this;
}

复合赋值运算符

复合赋值运算符不必须是类的成员,但一般还是将其设计成成员函数:

Sales_data &Sales_data::operator+=(const Sales_data &rhs) {
	units_sold += rhs.units_sold;
	revenue += rhs.revenue;
	return *this;
}

14.5 下标运算符(P501)

下标运算符必须是成员函数。为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值。同时,我们最好同时定义下标运算符的常量版本非常量版本

class StrVec{
public:
    string &operator[](size_t n)
        { return elements[n]; }
    const string &operator[](size_t n) const
        { return elements[n];}
}

14.6 递增和递减运算符(P502)

C++ 并不要求递增和递减运算符必须是类的成员,但因为它们改变所操作对象的状态,所以建议将其设定为成员函数/

定义前置递增/递减运算符

class StrBlobPtr {
public:
	StrBlobPtr &operator++();
	StrBlobPtr &operator--();
};

StrBlobPtr &StrBlobPtr::operator++() {
	check(curr, "increment past end of StrBlobPtr");
	++curr;
	return *this;
}
StrBlobPtr &StrBlobPtr::operator--() {
	// curr为无符号类型,如果curr为0,--后将得到一个很大的正数
	--curr;
	check(curr, "increment past end of StrBlobPtr");
	return *this;
}

区分前置和后置运算符

前置和后置版本使用的是同一个符号,并且运算对象的数量和类型也相同。为了区分前置版本和后置版本,后置版本接受一个额外的、不被使用的 int 类型形参,当我们使用后置运算符时,编译器为这个形参提供值为 0 的实参:

class StrBlobPtr {
public:
	StrBlobPtr &operator++(int);
	StrBlobPtr &operator--(int);
};

// 无需为int形参命名
StrBlobPtr &StrBlobPtr::operator++(int) {
	StrBlobPtr ret = *this;    // 记录当前的值
	++*this;
	return ret;
}
StrBlobPtr &StrBlobPtr::operator--(int) {
	StrBlobPtr ret = *this;    // 记录当前的值
	--*this;
	return ret;
}

显式地调用后置运算符

StrBlobPtr p(a1);
p.operator++(0);    // 显式调用后置版本
p.operator++();    // 显式调用前置版本

14.7 成员访问运算符(P504)

迭代器类和智能指针类常常用到解引用运算符和箭头运算符:

class StrBlobPtr {
public:
	string &operator*() const {
		auto p = check(curr, "dereference past end");
		return (*p)[curr];
	}
	string *operator->() const {
		return &(this->operator*());
	}
    // ...
};
StrBlob a1 = {"hi", "bye", "now"};
StrBlobPtr p(a1);
*p = "okay";

对箭头运算符返回值的限定

对于形如 point->mem 的表达式来说,point 必须是指向类对象的指针或者是一个重载了 operator->类对象,根据 point 类型的不同,point->mem 分别等价于:

(*point).mem;
point.operator()->mem;

point->mem 的执行过程:

  1. 如果 point 是指针,则我们应用内置箭头运算符,表达式等价于 (*point).mem
  2. 如果 point 是定义了 operator-> 的类的一个对象,则我们使用 point.operator->() 的结果来获取 mem 。如果结果是一个指针,则执行第 1 步;否则重复调用当前步骤。
文章来源:https://blog.csdn.net/MaTF_/article/details/135521927
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。