C++ Primer (第五版)-第十四章重载运算与类型转换
文章目录
- 一、基本概念
- 可以被重载
- 某些运算符不应被重载
- 尽量明智使用运算符重载
- 赋值和复合赋值运算符
- 选择作为成员或者非成员
- 输入和输出运算符
- 输入运算符尽量减少格式化操作
- 输入输出运算符必须是非成员函数
- 重载输入运算符>>
- 输入时的错误
- 标示错误
- 算数和关系运算符
- 相等运算符
- 关系运算符
- 赋值运算符
- 下标运算符
- 递增和递减运算符
- 定义前置递增、递减运算符
- 区分前置和后置运算符
- 成员访问运算符->
- 14.8 函数调用运算符
- 含有状态的函数对象类
- lambda是函数对象
- 标准库定义的函数对象
- 可调用对象与function
- 不同类别可能具有相同都调用形式
- 标准库function
- 重载函数与function
- 14.9 重载、类型转换与运算符
- 类型转换运算符
- 类类型转换成内置类型
- 类类型转换为自定义类型
- 转换为指针类型示例
- 类型转换运算符可能产生意外结果
- 显式类型转换运算符
- 转为bool
- 避免有二义性的类型转换
- 第一种情况
- 第二种情况
- 重载函数与转换构造函数
- 重载函数与用户定义的类型转换
- 函数匹配与重载运算符
一、基本概念
可以被重载
某些运算符不应被重载
通常:不应该重载逗号、取地址、逻辑与/或运算符
尽量明智使用运算符重载
赋值和复合赋值运算符
选择作为成员或者非成员
输入和输出运算符
ostream &operator<<(ostream &os.const Sales_dataaa &item)
{
os<<item.isbn()<<""<<item.units_sold<<""<<item.revenue<<""<<item.avg_price();
return os;
}
输入运算符尽量减少格式化操作
通常输出运算符应该主要负责打印对象内容而非控制格式,输出运算符不应该打印换行符
输入输出运算符必须是非成员函数
重载输入运算符>>
istream &operator>>(istream &is,sales_data &item)
{
double price;//不需要初始化,因为我们将先读入数据到Price,之后使用
is>>item.bookNo>>item.units_sold>>price;
if(is)item.revenue=item.units_sold*price;elseitem.revenue=item.units_sold*price;return is;
}
输入时的错误
当读取操作发生错误时,输入运算符应该负责从错误中恢复。
标示错误
算数和关系运算符
如果类同时定义了算术运算符和相关的复合赋值运算符,则通常情况下应该使用复合赋值来实现算术运算符。
相等运算符
如果某个类在逻辑上有相等性都含义,则该类应该定义operator==.这样做可以使得用户更容易使用标准库算法来处理这个类
关系运算符
赋值运算符
下标运算符
标示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般会定义下标运算符Operator[]
下标运算符必须是成员函数
class StrVec
{public:std::string& operator[]{std::size_t n}{return element[n];}const std::string& operator[](std::size_t n) const{return elements[n];}private:std::string *elements;
};
递增和递减运算符
定义前置递增、递减运算符
Class StrBlobPtr
{Public://递增和递减运算符StrBlobPtr& operator++(); //前置运算符StrBlobPtr& operator--(); //前置运算符
}
为了与内置版本保持一致,前置运算符应该返回递增或递减后对象的引用。
区分前置和后置运算符
成员访问运算符->
在迭代器和智能指针常常用到引用运算符和箭头运算符
class StrBlobPtr
{public: std::string& operator*() const{autop p=check(curr,"dereference past end");return (*p)[curr];}std::string* operator->()const{ return & this->operator*();}}
#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <stdexcept>// 前向声明StrBlobPtr类,以便在StrBlob中使用
class StrBlobPtr;// StrBlob类:管理动态字符串数组的容器
class StrBlob {friend class StrBlobPtr; // 允许StrBlobPtr访问私有成员public:using size_type = std::vector<std::string>::size_type;// 默认构造函数:初始化空的vectorStrBlob() : data(std::make_shared<std::vector<std::string>>()) {}// 带初始化列表的构造函数:用初始值列表初始化vectorStrBlob(std::initializer_list<std::string> il) : data(std::make_shared<std::vector<std::string>>(il)) {}// 返回容器大小size_type size() const { return data->size(); }// 判断容器是否为空bool empty() const { return data->empty(); }// 向容器尾部添加元素void push_back(const std::string &t) { data->push_back(t); }// 删除容器尾部元素(需检查容器非空)void pop_back();// 获取容器首元素的引用(非常量版本)std::string& front();// 获取容器首元素的引用(常量版本)const std::string& front() const;// 获取容器尾元素的引用(非常量版本)std::string& back();// 获取容器尾元素的引用(常量版本)const std::string& back() const;// 返回指向容器首元素的StrBlobPtrStrBlobPtr begin();// 返回指向容器尾后位置的StrBlobPtrStrBlobPtr end();private:// 共享指针管理vector,允许多个StrBlob共享同一数据std::shared_ptr<std::vector<std::string>> data;// 检查索引是否合法,不合法则抛出异常void check(size_type i, const std::string &msg) const;
};// 检查索引合法性,若不合法则抛出out_of_range异常
void StrBlob::check(size_type i, const std::string &msg) const {if (i >= data->size())throw std::out_of_range(msg);
}// 返回首元素引用(非常量版本)
std::string& StrBlob::front() {check(0, "front on empty StrBlob"); // 检查容器非空return data->front();
}// 返回首元素引用(常量版本)
const std::string& StrBlob::front() const {check(0, "front on empty StrBlob"); // 检查容器非空return data->front();
}// 返回尾元素引用(非常量版本)
std::string& StrBlob::back() {check(0, "back on empty StrBlob"); // 检查容器非空return data->back();
}// 返回尾元素引用(常量版本)
const std::string& StrBlob::back() const {check(0, "back on empty StrBlob"); // 检查容器非空return data->back();
}// 删除尾元素
void StrBlob::pop_back() {check(0, "pop_back on empty StrBlob"); // 检查容器非空data->pop_back();
}// StrBlobPtr类:StrBlob的智能指针/迭代器
class StrBlobPtr {
public:// 默认构造函数:初始化为未绑定状态StrBlobPtr() : curr(0) {}// 构造函数:绑定到指定StrBlob的指定位置StrBlobPtr(StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}// 解引用操作符:返回当前位置的字符串引用std::string& operator*() const {auto p = check(curr, "dereference past end"); // 检查索引合法性return (*p)[curr]; // 返回vector中curr位置的字符串}// 箭头操作符:返回当前位置的字符串指针std::string* operator->() const { return & this->operator*(); // 调用解引用操作符并返回地址}// 前置递增操作符:移动到下一个位置StrBlobPtr& operator++();// 前置递减操作符:移动到前一个位置StrBlobPtr& operator--();// 不等比较操作符:比较两个指针是否指向不同位置bool operator!=(const StrBlobPtr& rhs) const {return curr != rhs.curr;}// 赋值运算符:复制另一个StrBlobPtr的状态StrBlobPtr& operator=(const StrBlobPtr& rhs) {if (this != &rhs) { // 避免自我赋值wptr = rhs.wptr; // 复制弱引用(不增加引用计数)curr = rhs.curr; // 复制当前位置}return *this; // 返回自身引用,支持链式赋值}// assign函数:将指针重新绑定到指定StrBlob的指定位置StrBlobPtr& assign(StrBlob& sb, size_t pos = 0) {wptr = sb.data; // 绑定到新StrBlob的datacurr = pos; // 设置新位置return *this; // 返回自身引用,支持链式调用}// assign函数:复制另一个StrBlobPtr的状态StrBlobPtr& assign(const StrBlobPtr& rhs) {if (this != &rhs) { // 避免自我赋值wptr = rhs.wptr; // 复制弱引用curr = rhs.curr; // 复制当前位置}return *this; // 返回自身引用}private:// 弱引用:指向StrBlob的底层vector,不控制其生命周期std::weak_ptr<std::vector<std::string>> wptr;// 当前位置索引size_t curr;// 检查底层vector是否存在且索引是否合法std::shared_ptr<std::vector<std::string>> check(size_t, const std::string&) const;
};// 检查底层vector是否存在且索引是否合法,返回shared_ptr
std::shared_ptr<std::vector<std::string>>
StrBlobPtr::check(size_t i, const std::string &msg) const {auto ret = wptr.lock(); // 尝试获取shared_ptr(检查vector是否存在)if (!ret)throw std::runtime_error("unbound StrBlobPtr"); // vector已销毁if (i >= ret->size())throw std::out_of_range(msg); // 索引越界return ret; // 返回shared_ptr,确保在使用期间vector不会被销毁
}// 前置递增操作符实现
StrBlobPtr& StrBlobPtr::operator++() {check(curr, "increment past end of StrBlobPtr"); // 检查当前位置合法++curr; // 移动到下一个位置return *this; // 返回自身引用
}// 前置递减操作符实现
StrBlobPtr& StrBlobPtr::operator--() {--curr; // 移动到前一个位置check(curr, "decrement past begin of StrBlobPtr"); // 检查新位置合法return *this; // 返回自身引用
}// 返回指向StrBlob首元素的StrBlobPtr
StrBlobPtr StrBlob::begin() {return StrBlobPtr(*this);
}// 返回指向StrBlob尾后位置的StrBlobPtr
StrBlobPtr StrBlob::end() {return StrBlobPtr(*this, data->size());
}// 示例使用
int main() {// 创建StrBlob并初始化StrBlob sb = {"hello", "world", "!"};// 使用StrBlobPtr遍历StrBlobfor (auto p = sb.begin(); p != sb.end(); ++p) {// 使用解引用操作符获取字符串std::cout << *p << std::endl;// 使用箭头操作符调用字符串的成员函数std::cout << p->size() << std::endl;}// 使用assign函数重新定位指针StrBlobPtr p(sb);p.assign(sb, 1); // 指向第二个元素("world")std::cout << *p << std::endl; // 输出: worldreturn 0;
}
14.8 函数调用运算符
如果类兴义了调用运算符,则该类的对象称作函数对象。因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”
含有状态的函数对象类
#include <iostream>
#include <vector>
#include <algorithm> // for std::for_eachclass printstring {
private:std::ostream& os;char delim;public:printstring(std::ostream& os, char delim = '\n') : os(os), delim(delim) {}void operator()(const std::string& s) const {os << s << delim;}
};int main() {std::vector<std::string> vs = {"hello", "world", "!"};// 使用for_each和printstring函数对象std::for_each(vs.begin(), vs.end(), printstring(std::cerr, '\n'));// 等价于以下循环for (const auto& s : vs) {std::cerr << s << '\n';}return 0;
}
lambda是函数对象
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>class ShorterString {
public:bool operator()(const std::string &s1, const std::string &s2) const {return s1.size() < s2.size();}
};int main() {std::vector<std::string> strings = {"apple", "banana", "pear", "kiwi"};auto min_str = std::min_element(strings.begin(), strings.end(), ShorterString());if (min_str != strings.end()) {std::cout << "The shortest string is: " << *min_str << std::endl;}return 0;
}
标准库定义的函数对象
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <functional>int main() {std::vector<std::string> svec = {"apple", "banana", "cherry", "date"};// 使用greater<string>()作为比较函数对象进行排序std::sort(svec.begin(), svec.end(), std::greater<std::string>());for (const auto& str : svec) {std::cout << str << " ";}std::cout << std::endl;return 0;
}
可调用对象与function
不同类别可能具有相同都调用形式
#include <iostream>
#include <map>
#include <string>
#include <functional>// 定义加法函数
int add(int a, int b) {return a + b;
}// 定义减法函数
int subtract(int a, int b) {return a - b;
}// 定义乘法函数
int multiply(int a, int b) {return a * b;
}// 定义除法函数
int divide(int a, int b) {if (b == 0) {std::cerr << "除数不能为0" << std::endl;return 0;}return a / b;
}int main() {// 使用std::map来实现函数表// 键是表示运算符的std::string对象,值是对应的函数对象(这里用std::function包装函数指针)std::map<std::string, std::function<int(int, int)>> functionTable;functionTable["+"] = add;functionTable["-"] = subtract;functionTable["*"] = multiply;functionTable["/"] = divide;int num1 = 10;int num2 = 2;std::string op = "/";// 从函数表中查找对应运算符的函数auto it = functionTable.find(op);if (it != functionTable.end()) {// 调用找到的函数进行计算int result = it->second(num1, num2);std::cout << num1 << " " << op << " " << num2 << " = " << result << std::endl;} else {std::cerr << "不支持的运算符" << std::endl;}return 0;
}
标准库function
function定义在functional头文件中。
重载函数与function
#include <iostream>
#include <map>
#include <string>
#include <functional>// 自定义类Sales_data(简单示例,假设只有一个数据成员price)
class Sales_data {
public:double price;Sales_data(double p) : price(p) {}
};// 定义用于计算两个整数相加的函数
int add(int i, int j) {return i + j;
}// 定义用于计算两个Sales_data对象相加的函数(这里简单将price相加)
Sales_data add(const Sales_data& s1, const Sales_data& s2) {return Sales_data(s1.price + s2.price);
}int main() {// 定义一个map,键为string类型表示操作符,值为接受两个int参数并返回int结果的函数对象std::map<std::string, std::function<int(int, int)>> binops;// 以下插入操作会出错,因为存在函数重载,编译器不知道该用哪个add// binops.insert({" + ", add}); // 修正方法一:使用lambda表达式明确指定插入的函数逻辑binops.insert({" + ", [](int a, int b) { return add(a, b); }});// 修正方法二:定义一个新的函数,明确调用int版本的add函数int newAdd(int a, int b) {return add(a, b);}binops.insert({" + ", newAdd});// 测试插入后的函数调用int result = binops["+"](3, 4);std::cout << "计算结果: " << result << std::endl;return 0;
}
14.9 重载、类型转换与运算符
类型转换运算符
- 特殊成员函数:类型转换运算符是类里面特殊的成员函数,和普通成员函数不同,它没有显式写出来的返回类型(但实际有转换后的类型 ),也没有参数。
- 转换功能:作用是把类类型的值转换为其他类型,这个其他类型可以是内置类型(像 int、double 等 ),也可以是自定义类型(比如其他类 ),但不能是 void ,也不能转换为数组或者函数类型,不过能转成指针(包括数组指针、函数指针 )或者引用类型。
- const 限定:一般定义成 const 成员函数,因为转换过程通常不应该改变被转换对象本身的内容。
类类型转换成内置类型
#include <iostream>
#include <string>class MyInt {
private:int value;
public:MyInt(int v) : value(v) {}// 类型转换运算符,将MyInt类型转换为int类型operator int() const {return value;}
};int main() {MyInt num(5);int result = num + 3; // 这里会自动调用operator int()将num转换为int类型std::cout << "结果: " << result << std::endl;return 0;
}
类类型转换为自定义类型
#include <iostream>
#include <string>class SmallInt {
private:int smallValue;
public:SmallInt(int v) : smallValue(v) {}operator int() const {return smallValue;}
};class BigInt {
private:int bigValue;
public:BigInt(int v) : bigValue(v) {}// 类型转换运算符,将BigInt类型转换为SmallInt类型operator SmallInt() const {// 这里简单处理,假设取bigValue的个位数作为SmallInt的值return SmallInt(bigValue % 10);}
};int main() {BigInt big(25);SmallInt small = big; // 这里会自动调用operator SmallInt()将big转换为SmallInt类型std::cout << "转换后的SmallInt值: " << static_cast<int>(small) << std::endl;return 0;
}
转换为指针类型示例
#include <iostream>class MyClass {
private:int data;
public:MyClass(int d) : data(d) {}// 类型转换运算符,将MyClass类型转换为int*类型(这里只是示例,实际意义需根据场景确定)operator int*() const {return &data;}
};int main() {MyClass obj(10);int* ptr = obj; // 这里会自动调用operator int*()将obj转换为int*类型std::cout << "指针指向的值: " << *ptr << std::endl;return 0;
}
类型转换运算符可能产生意外结果
显式类型转换运算符
转为bool
避免有二义性的类型转换
第一种情况
假设我们有两个类 A 和 B ,以下代码展示了两个类提供相同类型转换导致的问题:
#include <iostream>class B; // 前向声明class A {
public:// 转换构造函数,接受B类对象来构造A类对象A(const B& b) {std::cout << "A的转换构造函数被调用" << std::endl;}
};class B {
public:// 类型转换运算符,将B类对象转换为A类对象operator A() const {std::cout << "B的类型转换运算符被调用" << std::endl;return A(*this);}
};void func(A a) {std::cout << "函数func接受A类对象" << std::endl;
}int main() {B b;// 这里会产生二义性// 编译器不知道是该调用A的转换构造函数,还是B的类型转换运算符/// 调用时必须显式转换
//func(static_cast<A>(b)); // 明确指定使用B的转换运算符func(b); return 0;
}
第二种情况
以一个自定义类 MyNumber 为例,假设它有多种与算术类型相关的转换规则,这可能会引发问题:
#include <iostream>class MyNumber {
private:int value;
public:MyNumber(int v) : value(v) {}// 类型转换运算符,转换为int类型operator int() const {std::cout << "转换为int类型" << std::endl;return value;}// 只允许显式转换// explicit operator int() const { return value; }// explicit operator double() const { return value; }// 类型转换运算符,转换为double类型operator double() const {std::cout << "转换为double类型" << std::endl;return static_cast<double>(value);}
};void func(int i) {std::cout << "函数func接受int类型参数: " << i << std::endl;
}void func(double d) {std::cout << "函数func接受double类型参数: " << d << std::endl;
}int main() {MyNumber num(5);// 这里会产生二义性// 编译器不知道是该将num转换为int还是doublefunc(num); return 0;
}
重载函数与转换构造函数
重载函数与用户定义的类型转换
显式转换
函数匹配与重载运算符
#include <iostream>
// 定义一个类
class MyClass {
private:int value;
public:MyClass(int v) : value(v) {}// 重载 + 运算符,作为成员函数MyClass operator+(const MyClass& other) const {return MyClass(value + other.value);}
};
// 重载 + 运算符,作为非成员函数
MyClass operator+(const MyClass& a, const MyClass& b) {return MyClass(a.value + b.value);
}
int main() {MyClass a(1);MyClass b(2);// 表达式 a + b ,这里编译器会考虑成员函数版本和非成员函数版本的 operator+MyClass result = a + b; std::cout << "Result: " << result.value << std::endl;return 0;
}