Day8-1 操作符重载再探(2025.03.31)
一、 操作符重载深入解析
1.1 操作符重载的应用场景
- 自定义数学类型(向量、矩阵、复数等)
- 实现类对象的自然语义操作
- 流操作定制化
1.2 Int类完整实现示例
class Int {
friend std::ostream& operator<<(std::ostream& os, Int i);
public:
// 构造函数
Int() = default;
Int(int i) : _i(i) {}
// 算术运算符
Int operator+(const Int& i) const { return _i + i._i; }
Int operator+(int i) const { return _i + i; }
// 赋值运算符
Int& operator=(const Int& i) { _i = i._i; return *this; }
Int& operator=(int i) { _i = i; return *this; }
// 类型转换运算符
operator int() const { return _i; }
private:
int _i = 0;
};
// 流输出运算符
std::ostream& operator<<(std::ostream& os, Int i) {
os << i._i;
return os;
}
1.3 操作符重载关键要点
运算符类型 | 返回值类型 | 参数传递 | 备注 |
---|---|---|---|
算术运算符 | 值类型 | const引用 | 应保证不修改操作数 |
赋值运算符 | 左值引用 | const引用 | 需返回*this |
流运算符 | 流引用 | 非const引用 | 通常定义为友元 |
1.4 字符串操作示例
void stringDemo() {
string a = "cpp";
string b = a + " java"; // 调用operator+(const char*)
string c = "python " + b; // 调用operator+(const char*, string)
}
1.5示例代码
#include <iostream>
/*
操作符重载
常见的操作符:
1. 数值运算 + - * /
2. 赋值 =
3. 流操作符 << >>
4. 其他,比如取地址&,解引用*(和乘法一样),打开域::,类指针成员访问->,类实例成员访问. 等
编译器为内置类型定义了基本的操作符,比如以上的 1 & 2 & 3 项
每个操作符只是个函数,即 + 运算符只是个以 operator+ 为函数名的普通函数而已
我们需要用到操作符重载的地方:
1. 实现一些自定义的数学类型,比如 向量,矩阵,复数等,这些类也可以进行数值运算,那就需要为这些类定义这些操作符
2. 拷贝赋值运算符和移动赋值运算符是编译器默认生成的两个重要函数,我们理应掌握这两个函数
常见地,我们更多需要的是重载数值运算符,赋值和流操作符
对于数值运算符,计算的结果是个右值,意思是计算结果的返回值是不可以赋值的,比如
int a = 1;
int b = 2;
(a + b) = 100; // 报错,右值不能赋值
对于自定义类型,比如T
T a;
T b;
要支持 a + b,有两种定义方法,要么定义T类的成员函数,要么定义类外的普通函数
class T
{
public:
T operator+(const T& r); // 默认该类本身是左操作数,传入的参数是右操作数
};
或者
T operator(const T& l, const T& r); // 分别传入左操作数和右操作数
对于赋值运算符,计算的结果是个左值,比如
int a = 1;
(a = 2) = 3; // 正确,a = 2的返回值就是 a 本身,是个左值,可以继续赋值
对于自定义类型T,比如
T a;
T b;
要支持 a = b,一般就定义拷贝赋值运算符,即
class T
{
public:
// 这是个编译器会默认生成的函数,非常重要
// 请牢记它的样子,返回类的左值引用,参数是类的常量引用
// 返回值需要返回 *this,即它本身,必须是个引用,如果不是引用会发生拷贝,就和当前这个类对象没关系了
// 返回值还要支持修改,即左值属性,所以必须是左值引用
// 传入参数不应该发生拷贝,即传引用,也不可以修改,所以使用常量引用
T& operator=(const T& r);
};
流操作符的左边一般是一个输出流对象,右边是某种类型,计算结果仍是流对象本身,比如
int a = 1;
int b = 2;
(std::cout << a) << b; // std名称空间中的cout是个全局变量,cout << a 后返回就是 cout 本身,可以继续 << b
对于自定义类型T,没法定义成员函数,因为左操作数不是T,所以一般定义类外函数
std::ostream& operator<<(std::ostream& os, T r);
现在我们定一个自己的int,名为Int,希望它表现得像int
*/
// 第一个版本
namespace v1
{
class Int
{
// 由于流运算符要访问 _i,需要声明为友元,友元可以突破访问控制,访问类的 private 成员
// 友元声明无所谓访问控制,放在 public / private 下面都可
friend std::ostream& operator<<(std::ostream& os, Int r);
private:
int _i;
public:
// 可以使用同类型变量构造(特殊函数,拷贝构造函数)
// 为了支持 Int a = b; // b也是Int
Int(const Int& r) : _i(r._i) {} // 编译器会生成这个版本,可以不实现,或者直接 = default
// 也可以使用普通int构造
// 为了支持 Int a = 1;
Int(const int& r) : _i(r) {}
// 它应该支持和同类型变量相加
// 让 a + b 可以工作
Int operator+(const Int& r) const { return Int(_i + r._i); }
// 也应该和普通int相加
// 让 a + 1 可以工作
Int operator+(const int& r) const { return Int(_i + r); }
// - * / 这里就省略了
// 可以把同类型变量赋给它(特殊函数,拷贝赋值运算符)
// 让 a = b 工作
Int& operator=(const Int& r) { _i = r._i; return *this; } // 编译器会生成这个版本,可以不实现,或者直接 = default
// 也可以把普通int赋给它
// 让 a = 1 工作
Int& operator=(const int& r) { _i = r; return *this; }
};
// 流运算符
// << 会改变流,所以要传左值引用,把r写入流后,继续把流以左值引用传出去
// 流不可以拷贝,其拷贝构造函数和拷贝赋值运算符被定义为 delete,所以只能以引用传递
std::ostream& operator<<(std::ostream& os, Int r)
{
os << r._i;
return os;
}
}
void demo1()
{
using namespace v1;
Int a = 1;
Int b = 2;
b = a;
Int c = a + b;
std::cout << c << std::endl;
}
/*
为了少写点,我们希望 Int 能隐式转换为 int,这样能少写一半成员函数,
即调用 operator+(Int r) 时先把 Int转为 int,再调用 operator+(int r)
为了支持转换,需要定义类型转换操作符,比如想把 T1 转为 T2
class T1
{
public:
operator T2() const;
};
*/
//第二个版本
namespace v2
{
class Int
{
friend std::ostream& operator<<(std::ostream& os, Int r);
private:
int _i;
public:
Int(const int& r) : _i(r) {}
Int operator+(const int& r) const { return Int(_i + r); }
Int& operator=(const int& r) { _i = r; return *this; }
// 让 Int 在需要转换的地方自动转为 int,所以以上函数的 Int 参数版本都被删除了
operator int() const { return _i; }
};
std::ostream& operator<<(std::ostream& os, Int r)
{
os << r._i;
return os;
}
}
void demo2()
{
using namespace v2;
Int a = 1;
Int b = 2;
b = a;
Int c = a + b;
std::cout << c << std::endl;
// 由于定义类型转换操作符,这句可以编译通过
int d = c;
}
int main()
{
demo1();
demo2();
return 0;
}
二、 可调用对象详解
2.1 可调用对象类型
- 普通函数
- 函数指针
- 重载operator()的类对象(函数对象)
- lambda表达式
- std::function对象
2.2 函数对象实现
class Callable {
public:
// 函数调用运算符重载
void operator()() { cout << "Function call" << endl; }
bool operator()(int a, int b) { return a < b; }
// 静态成员函数
static bool compare(int a, int b) { return a > b; }
};
2.3 实际应用场景
排序算法示例:
int main() {
vector<int> v = {5, 3, 7, 1};
// 使用函数对象
Callable comp;
sort(v.begin(), v.end(), comp);
// 使用lambda表达式
sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
// 使用静态成员函数
sort(v.begin(), v.end(), Callable::compare);
}
多线程示例:
void threadDemo() {
Callable c;
// 使用成员函数
thread t1(&Callable::sum, &c, 100);
// 使用函数对象
thread t2(c);
// 使用lambda
thread t3([](){
cout << "Lambda thread" << endl;
});
}
附录:测试代码
void testAll() {
// 操作符重载测试
Int a = 10;
Int b = a + 5;
cout << "Result: " << b << endl;
// 可调用对象测试
Callable callable;
callable();
cout << "Compare: " << callable(3, 5) << endl;
}
2.4 最佳实践建议
- 算术运算符应定义为非成员函数以支持对称性
- 赋值运算符必须定义为成员函数
- 流运算符应定义为友元非成员函数
- 函数对象应设计为无状态(或明确管理状态)
- lambda表达式适合简单的一次性操作
2.5完整示例代码
/*
可调用对象就是可以用括号并传参数调用的东西
函数就是可调用对象
类对象重载括号运算符 operator() 也可以成为可调用对象
*/
#include <iostream>
#include <algorithm>
#include <thread>
// 关于算法库和线程库的细节,后面再说
class CallableClass
{
public:
void test()
{
std::cout << "test" << std::endl;
}
// 重载 operator() 使得该类成为可调用对象
void operator()()
{
std::cout << "Callable" << std::endl;
}
bool operator()(int l, int r)
{
return l < r;
}
static bool compare(int l, int r)
{
return l > r;
}
void operator()(int start)
{
int sum = 0;
for (int i = start; i <= start + 100; ++i)
{
sum += i;
}
std::cout << sum << std::endl;
}
void sum(int start)
{
int sum = 0;
for (int i = start; i <= start + 100; ++i)
{
sum += i;
}
std::cout << sum << std::endl;
}
};
void thread_func(int start)
{
int sum = 0;
for (int i = start; i <= start + 100; ++i)
{
sum += i;
}
std::cout << sum << std::endl;
}
bool compare(int l, int r)
{
// 左边的数大于右边的数是排序的依据,只有满足这个要求,函数才返回 true
return l > r;
}
int main()
{
CallableClass c;
c.test();
c(); // 调用 operator()
// 使用 auto 自动推断类型,因为 lambda 表达式的类型比较复杂
// 本质上是一个重载了 operator() 的类
auto lambda_compare = [](int l, int r) -> bool
{
return l > r;
};
int array[10] { 2, 4, 5, 6, 9, 8, 1, 3, 7, 0 };
// std::sort
// 第一个参数是数组的首元素的地址
// 第二个参数是数组最后一个元素的下一个位置的地址
// 第三个参数是一个可调用对象,接受两个参数,参数类型是要排序的元素的类型,返回bool值
// 使用普通函数作为可调用对象
std::sort(array, array + 10, compare);
// 使用重载了operator()的类实例作为可调用对象
std::sort(array, array + 10, c);
// 使用类的静态函数作为可调用对象
std::sort(array, array + 10, CallableClass::compare);
// 使用 lambda 表达式(左值)作为可调用对象
std::sort(array, array + 10, lambda_compare);
// 就地写一个 lambda 表达式(右值)作为可调用对象
std::sort(array, array + 10, [](int l, int r) { return l > r; });
// C++11新语法 范围for
for (int i : array)
{
std::cout << i << ' ';
}
std::cout << std::endl;
// 使用类实例和非静态成员函数作为可调用对象
std::thread t1(&CallableClass::sum, c, 1);
t1.join();
// 使用重载了operator()的类实例作为可调用对象
std::thread t2(c, 1);
t2.join();
// 使用普通函数作为可调用对象
std::thread t3(thread_func, 1);
t3.join();
// 同样,可以使用 lambda 表达式作为可调用对象
return 0;
}
Day8-2 拓展:C++ 中创建线程并传入类类型的正确写法
在 C++ 中创建线程并传入类类型有几种正确的方式,主要取决于你想如何传递类实例以及如何使用它。以下是几种常见的正确写法:
1. 传递成员函数(使用对象指针)
#include <iostream>
#include <thread>
class MyClass {
public:
void memberFunction(int x) {
std::cout << "Member function called with " << x
<< " (thread ID: " << std::this_thread::get_id() << ")\n";
}
};
int main() {
MyClass obj;
std::thread t(&MyClass::memberFunction, &obj, 10);
t.join();
return 0;
}
2. 传递 lambda 表达式捕获对象
#include <iostream>
#include <thread>
class MyClass {
public:
void operator()(int x) const {
std::cout << "Functor called with " << x
<< " (thread ID: " << std::this_thread::get_id() << ")\n";
}
};
int main() {
MyClass obj;
std::thread t([&obj]() { obj(20); });
t.join();
return 0;
}
3. 传递临时对象(移动语义)
#include <iostream>
#include <thread>
#include <memory>
class MyClass {
public:
void operator()() const {
std::cout << "Temporary object called"
<< " (thread ID: " << std::this_thread::get_id() << ")\n";
}
};
int main() {
std::thread t(MyClass()); // 注意:这里可能会被解析为函数声明
// 更好的写法:
// std::thread t{MyClass()};
// 或
// std::thread t((MyClass()));
t.join();
return 0;
}
4. 使用 std::ref 传递引用
#include <iostream>
#include <thread>
#include <functional>
class MyClass {
public:
void operator()() {
std::cout << "Object called by reference"
<< " (thread ID: " << std::this_thread::get_id() << ")\n";
}
};
int main() {
MyClass obj;
std::thread t(std::ref(obj));
t.join();
return 0;
}
5. 使用智能指针
#include <iostream>
#include <thread>
#include <memory>
class MyClass {
public:
void operator()() {
std::cout << "Shared object called"
<< " (thread ID: " << std::this_thread::get_id() << ")\n";
}
};
int main() {
auto obj = std::make_shared<MyClass>();
std::thread t([obj]() { (*obj)(); });
t.join();
return 0;
}
注意事项
- 线程的生命周期必须长于或等于它所引用的对象
- 如果需要共享对象,确保线程安全性(使用互斥锁等)
- 避免悬垂引用(不要传递局部变量的引用给可能比它生命周期长的线程)
- 考虑使用 join() 或 detach() 明确线程的结束方式
选择哪种方式取决于你的具体需求,如对象生命周期管理、是否需要修改对象状态等。