\
Complexe 类旨在表示和操作复数。其核心属性是实部 R 和虚部 I,均为 double 类型。
属性:
double R: 复数的实部。double I: 复数的虚部。
功能 (方法):
-
构造函数:
Complexe(const double &re = 0, const double &im = 0): 从0、1或2个double初始化 (例如Complexe(),Complexe(re),Complexe(re, im)).- 拷贝构造函数 (编译器默认提供,若无特殊需求可不显式定义):
Complexe(const Complexe &other).
-
算术运算 (与其他复数):
Complexe operator+(const Complexe &other) const: 复数加法。Complexe operator-(const Complexe &other) const: 复数减法。Complexe operator*(const Complexe &other) const: 复数乘法。Complexe operator/(const Complexe &other) const: 复数除法。
-
算术运算 (与
double类型):Complexe operator+(const double &val) const: 复数加double。Complexe operator-(const double &val) const: 复数减double。Complexe operator*(const double &val) const: 复数乘double。Complexe operator/(const double &val) const: 复数除以double。friend Complexe operator+(const double &val, const Complexe &c):double加复数。friend Complexe operator-(const double &val, const Complexe &c):double减复数。friend Complexe operator*(const double &val, const Complexe &c):double乘复数。friend Complexe operator/(const double &val, const Complexe &c):double除以复数。 (注:友元函数template<class T> friend Complexe operator...(const T& val, const Complexe &complexe)可以通用地处理T类型与复数的运算,当T为double时即满足要求。)
-
复数特性计算:
Complexe Conjugue() const: 计算并返回复数的共轭。double GetModule() const: 计算并返回复数的模。double GetArgument() const: 计算并返回复数的辐角 (弧度)。
-
输出:
void Affiche() const: 按(R +Ij)或(R -|I|j)或(R)格式输出到控制台。friend ostream& operator<<(ostream& os, const Complexe& z): 重载<<运算符,方便流输出。
-
相等性测试:
bool operator==(const Complexe &other) const: 判断两个复数是否相等。
-
极坐标相关 (已在模板中部分提供):
void SetPolarCoord(const double &mod, const double &arg): 通过模和辐角 (弧度) 设置复数。void SetPolarCoordDegre(const double &mod, const double &arg_degre): 通过模和辐角 (角度) 设置复数。void AffichePolar() const: 以极坐标形式Mei(A)(弧度) 输出。void AffichePolarDegre() const: 以极坐标形式Mei(A)(角度) 输出。
-
其他 (可选,来自 "Pour les curieux"):
Complexe operator!() const: 重载!运算符作为共轭的另一种调用方式。
复数 z = R + I*j 的辐角 arg(z) 可以通过 atan2(I, R) 函数计算。atan2 函数能够正确处理所有象限,并处理 R=0 的情况。
C++ 函数代码 (作为 Complexe 类的成员函数):
#include <cmath> // For atan2
double Complexe::GetArgument() const {
return atan2(this->I, this->R);
}在此类中,我们计划实现以下 operator+:
Complexe Complexe::operator+(const Complexe &complexe) const: 用于两个Complexe对象相加 (c1 + c2)。Complexe Complexe::operator+(const double &val) const: 用于Complexe对象加double(c1 + 5.0)。friend Complexe operator+(const double &val, const Complexe &complexe)(或通过模板template<class T> friend Complexe operator+(const T& val, const Complexe &complexe)实现): 用于double加Complexe对象 (5.0 + c1)。
因此,至少有 3 个 不同的 operator+ 重载(如果将模板实例化视为特定重载)。
编译器依据 函数签名 (function signature) 来调用正确的重载。函数签名包括:
- 函数名 (在此为
operator+)。 - 参数的数量和类型 (例如
const Complexe&vsconst double&)。 - 对于成员函数,还包括其所属的类以及
const/volatile限定符。
当编译器遇到一个运算符表达式(如 a + b),它会查找所有与该运算符匹配的候选函数(包括成员函数和非成员/友元函数)。然后,它会执行 重载决议 (overload resolution) 过程,根据参数与形参的最佳匹配程度来选择最合适的函数版本。
这种机制叫做 函数重载 (function overloading),当用于运算符时,特指 运算符重载 (operator overloading)。
对于 Complexe 类型的对象,使用引用传参(尤其是 const 引用传参,如 const Complexe&)的好处主要有:
- 效率 (Efficiency):
- 避免了对象的复制。
Complexe对象虽然不大(通常是两个double),但在大量运算或循环中,频繁复制会导致不必要的开销(复制实部和虚部,可能还有构造/析构函数的调用)。引用传递直接使用原始对象,速度更快,内存占用更少。
- 避免了对象的复制。
- 避免对象切割 (Object Slicing):
- 在涉及继承和多态时,如果按值传递基类类型的参数,而实参是派生类对象,则会发生对象切割,丢失派生类特有的部分。虽然本
Complexe类不直接涉及继承,但这是引用传参的一个普遍优势。
- 在涉及继承和多态时,如果按值传递基类类型的参数,而实参是派生类对象,则会发生对象切割,丢失派生类特有的部分。虽然本
- 允许修改原始对象 (如果不是
const引用):- 如果参数是非
const引用 (Complexe&),函数内部对参数的修改会直接反映到调用者传入的原始对象上。这对于某些操作(如operator+=)是必要的。
- 如果参数是非
就本类的目标而言,哪一项最关键?
- 效率 是最关键的好处。TD 的目标之一是“尽可能短的执行时间”和“尽可能少地占用内存”。通过引用传递(特别是
const Complexe&作为输入参数)可以显著减少不必要的对象拷贝,从而提升性能并减少栈内存的瞬时消耗。
是否还有其他实现方式?
- 按值传参 (Pass-by-value):
Complexe MyFunction(Complexe c)。- 缺点:对于非基本类型,通常会导致对象复制,效率较低。对于
Complexe这样的小对象,编译器有时能优化,但不能保证。
- 按指针传参 (Pass-by-pointer):
Complexe MyFunction(const Complexe* c)。- 优点:也避免了复制,效率高。
- 缺点:
- 调用语法不同 (
func(&obj)或obj_ptr->method())。 - 需要处理空指针 (
nullptr) 的可能性,增加代码复杂性。 - 所有权和生命周期管理可能更复杂(尽管对于
const指针作为输入参数,这方面问题较少)。
- 调用语法不同 (
- 在现代 C++ 中,除非确实需要指针的语义(如表示可选对象或与 C API 交互),否则引用通常是更安全、更方便的选择。
对于运算符重载如 operator+(const Complexe& other),使用 const Complexe& 是标准且最佳的做法,因为它兼顾了效率和安全性(不修改输入对象)。
const 关键字在 C++ 中用于表示“常量”或“不可修改性”。
const 关键字的作用:
- 修饰变量:
const int x = 5;声明一个常量x,其值不能被修改。const double* ptr;ptr指向一个double常量 (不能通过ptr修改所指double的值)。double* const ptr;ptr是一个常量指针 (指针本身的值不能修改,即不能指向其他地址)。const double* const ptr;ptr是一个常量指针,指向一个double常量。
- 修饰函数参数:
void func(const int param);表明函数内部不应修改param的值(对于按值传递的内置类型,这更多是文档作用;对于引用或指针传递,它阻止修改原始数据)。
- 修饰成员函数 (在函数声明和定义的末尾):
double Complexe::GetModule() const;- 表明该成员函数是一个“观察者”(inspector) 或 "getter",它不会修改调用它的对象的任何非静态成员变量。
const成员函数可以被const对象调用,也可以被非const对象调用。而非const成员函数只能被非const对象调用。
- 修饰函数返回值:
const MyClass& getObject() const;返回一个对const对象的引用。
对于成员函数,如何使用 const?
- 将
const关键字放在成员函数声明和定义的参数列表之后,函数体{}之前(或分号;之前,如果是纯声明)。// In Complexe.h class Complexe { public: double GetModule() const; // const member function void SetReal(double r); // non-const member function }; // In Complexe.cpp double Complexe::GetModule() const { // this->R = 0; // Error: cannot modify member in a const function return sqrt(R*R + I*I); } void Complexe::SetReal(double r) { this->R = r; // OK: non-const function can modify members }
- 在
const成员函数内部,this指针的类型是const Complexe*,意味着不能通过this修改对象的成员变量(除非它们被声明为mutable)。
为用户添加 const 限定有什么好处?
- 接口清晰性与文档化:
const明确地告诉用户哪些函数会修改对象状态,哪些不会。这使得类的接口更容易理解和预测。
- 安全性 (Const-correctness):
- 防止意外修改。如果一个对象被声明为
const(因为它不应该被改变),那么只能调用它的const成员函数。这有助于在编译时捕获逻辑错误。 - 例如:
const Complexe origin(0,0); double mod = origin.GetModule(); // OK, GetModule is const. origin.SetReal(5); // Error, SetReal is not const.
- 防止意外修改。如果一个对象被声明为
- 更广泛的可用性:
const成员函数可以用于const和非const对象。如果一个有用的 getter 方法不是const,那么它就不能在const对象上使用,这会限制其可用性。
- 编译器优化:
- 虽然不是主要原因,但有时编译器可以利用函数是
const的信息进行一些优化。
- 虽然不是主要原因,但有时编译器可以利用函数是
- 启用某些设计模式:
- 例如,在函数参数中传递
const引用 (const Complexe&) 是一种常见且高效的方式,这要求被调用的相关方法也是const。
- 例如,在函数参数中传递
遵循 const-correctness 是良好 C++ 编程实践的重要组成部分。
本项目采用了现代 C++ 项目结构,使用 CMake 作为构建系统。项目结构如下:
.
├── CMakeLists.txt # CMake 构建配置文件
├── build.sh # 构建脚本
├── include/ # 头文件目录
│ └── Complexe.h # Complexe 类的头文件
├── src/ # 源代码目录
│ └── Complexe.cpp # Complexe 类的实现文件
├── tests/ # 测试文件目录
│ ├── TD_main_Validation.cpp # 功能验证测试程序
│ └── TD_main_Bench_CPU.cpp # 性能基准测试程序
└── build/ # 构建输出目录
使用 CMake 的好处:
- 跨平台支持:可以在 Windows、macOS、Linux 等不同平台上构建
- 依赖管理:自动处理库的链接和头文件路径
- 模块化:将
Complexe类编译为静态库,便于重用 - 测试集成:集成了 CTest 测试框架
- 现代化:符合现代 C++ 项目的标准实践
实现的关键函数:
- GetArgument(): 使用
atan2(I, R)计算辐角 - Conjugue(): 返回
Complexe(R, -I) - 算术运算符: 实现
+,-,*,/运算 - 与 double 的运算: 通过成员函数和友元函数实现双向运算
- SetPolarCoordDegre(): 角度转弧度后设置极坐标
编译和运行方法:
# 使用构建脚本(推荐)
./build.sh
# 或手动构建
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j4
# 运行验证测试
./validation_test
# 运行性能测试
./bench_test
# 运行所有测试
make test验证结果:
- 基本算术运算结果正确
- 复数的模和辐角计算准确
- 并联阻抗计算:
Z = 50ei(-0.009°)(主要由电阻决定,电容影响很小) - 复数旋转:
w = 1旋转 45° 得到1ei(45°)
性能测试结果:
- 自定义
Complexe类:约 0.025 秒 std::complex<double>:约 0.021 秒- 性能差距约为 17%,在可接受范围内
分析:
-
速度:
std::complex稍快,主要因为标准库的高度优化- 我们的实现性能良好,差距不大
- 使用
-O2优化后性能提升明显
-
内存占用:
- 两者都是两个
double,内存占用相同(16字节) - 临时对象的创建和销毁是主要开销
- 两者都是两个
-
易用性:
- ✅ 运算符重载让使用方式直观(
c1 + c2) - ✅ 支持与
double的双向运算(c + 5.0,5.0 + c) - ✅ 多种输出格式(笛卡尔、极坐标)
- ✅ 流输出支持(
cout << c) - ✅
const正确性保证了接口安全
- ✅ 运算符重载让使用方式直观(
优化建议:
- 内联小函数(CMake 的 Release 模式已启用)
- 使用 RVO/NRVO 减少临时对象拷贝
- 对于极致性能需求,可考虑表达式模板
详细分析:
假设 a, b, c 都是 Complexe 类型,表达式按以下顺序计算:
-
tmp1 = a / b:- 调用
operator/(const Complexe&) - 1 次构造函数调用(创建返回的临时对象)
- 调用
-
tmp2 = tmp1 + c:- 调用
operator+(const Complexe&) - 1 次构造函数调用(创建返回的临时对象)
- 调用
-
tmp3 = c * tmp2:- 调用
operator*(const Complexe&) - 1 次构造函数调用(创建返回的临时对象)
- 调用
-
c = tmp3:- 调用赋值运算符(默认或自定义)
- 0 次构造函数调用(
c已存在,只是修改其值)
总计:3 次构造函数调用
内存使用情况:
- 每个临时对象占用 16 字节(两个
double) - 总临时内存:3 × 16 = 48 字节
- 这些临时对象在表达式结束时自动销毁
- 栈内存的分配/释放开销很小
编译器优化:
- RVO (Return Value Optimization): 可能减少实际的对象拷贝
- NRVO (Named Return Value Optimization): 对命名返回值的优化
- 内联: 小函数可能被内联,减少函数调用开销
在 Release 模式下,编译器会应用这些优化,实际的构造函数调用次数可能少于理论值。
本 TD 成功实现了一个功能完备的 Complexe 类,具备以下特点:
- 功能完整性:支持所有要求的运算和功能
- 性能良好:与
std::complex性能差距很小 - 易用性强:丰富的运算符重载和输出格式
- 代码质量:遵循
const正确性和现代 C++ 实践 - 项目结构:使用 CMake 构建系统,便于维护和扩展
该实现在内存效率、运行时间和易用性三个目标之间取得了良好的平衡。