Skip to content

Latest commit

 

History

History
323 lines (243 loc) · 16 KB

File metadata and controls

323 lines (243 loc) · 16 KB

\

TD3 C++ 复数类 - 解题过程

预习:UML 及知识回顾

1-a)Complexe 类的完整建模

Complexe 类旨在表示和操作复数。其核心属性是实部 R 和虚部 I,均为 double 类型。

属性:

  • double R: 复数的实部。
  • double I: 复数的虚部。

功能 (方法):

  1. 构造函数:

    • Complexe(const double &re = 0, const double &im = 0): 从0、1或2个 double 初始化 (例如 Complexe(), Complexe(re), Complexe(re, im)).
    • 拷贝构造函数 (编译器默认提供,若无特殊需求可不显式定义): Complexe(const Complexe &other).
  2. 算术运算 (与其他复数):

    • Complexe operator+(const Complexe &other) const: 复数加法。
    • Complexe operator-(const Complexe &other) const: 复数减法。
    • Complexe operator*(const Complexe &other) const: 复数乘法。
    • Complexe operator/(const Complexe &other) const: 复数除法。
  3. 算术运算 (与 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 类型与复数的运算,当 Tdouble 时即满足要求。)
  4. 复数特性计算:

    • Complexe Conjugue() const: 计算并返回复数的共轭。
    • double GetModule() const: 计算并返回复数的模。
    • double GetArgument() const: 计算并返回复数的辐角 (弧度)。
  5. 输出:

    • void Affiche() const: 按 (R +Ij)(R -|I|j)(R) 格式输出到控制台。
    • friend ostream& operator<<(ostream& os, const Complexe& z): 重载 << 运算符,方便流输出。
  6. 相等性测试:

    • bool operator==(const Complexe &other) const: 判断两个复数是否相等。
  7. 极坐标相关 (已在模板中部分提供):

    • 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) (角度) 输出。
  8. 其他 (可选,来自 "Pour les curieux"):

    • Complexe operator!() const: 重载 ! 运算符作为共轭的另一种调用方式。

1-b)如何计算一个复数的辐角?请给出相应的 C++ 函数代码。

复数 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);
}

1-c)在此类中总共有多少个 operator+?编译器将依据什么来调用正确的重载?这种机制叫什么?

在此类中,我们计划实现以下 operator+

  1. Complexe Complexe::operator+(const Complexe &complexe) const: 用于两个 Complexe 对象相加 (c1 + c2)。
  2. Complexe Complexe::operator+(const double &val) const: 用于 Complexe 对象加 double (c1 + 5.0)。
  3. friend Complexe operator+(const double &val, const Complexe &complexe) (或通过模板 template<class T> friend Complexe operator+(const T& val, const Complexe &complexe) 实现): 用于 doubleComplexe 对象 (5.0 + c1)。

因此,至少有 3 个 不同的 operator+ 重载(如果将模板实例化视为特定重载)。

编译器依据 函数签名 (function signature) 来调用正确的重载。函数签名包括:

  • 函数名 (在此为 operator+)。
  • 参数的数量和类型 (例如 const Complexe& vs const double&)。
  • 对于成员函数,还包括其所属的类以及 const/volatile 限定符。

当编译器遇到一个运算符表达式(如 a + b),它会查找所有与该运算符匹配的候选函数(包括成员函数和非成员/友元函数)。然后,它会执行 重载决议 (overload resolution) 过程,根据参数与形参的最佳匹配程度来选择最合适的函数版本。

这种机制叫做 函数重载 (function overloading),当用于运算符时,特指 运算符重载 (operator overloading)

1-d)对于 Complexe 类型的对象,使用 引用传参 有哪些好处?就本类的目标而言,哪一项最关键?是否还有其他实现方式?

对于 Complexe 类型的对象,使用引用传参(尤其是 const 引用传参,如 const Complexe&)的好处主要有:

  1. 效率 (Efficiency):
    • 避免了对象的复制。Complexe 对象虽然不大(通常是两个 double),但在大量运算或循环中,频繁复制会导致不必要的开销(复制实部和虚部,可能还有构造/析构函数的调用)。引用传递直接使用原始对象,速度更快,内存占用更少。
  2. 避免对象切割 (Object Slicing):
    • 在涉及继承和多态时,如果按值传递基类类型的参数,而实参是派生类对象,则会发生对象切割,丢失派生类特有的部分。虽然本 Complexe 类不直接涉及继承,但这是引用传参的一个普遍优势。
  3. 允许修改原始对象 (如果不是 const 引用):
    • 如果参数是非 const 引用 (Complexe&),函数内部对参数的修改会直接反映到调用者传入的原始对象上。这对于某些操作(如 operator+=)是必要的。

就本类的目标而言,哪一项最关键?

  • 效率 是最关键的好处。TD 的目标之一是“尽可能短的执行时间”和“尽可能少地占用内存”。通过引用传递(特别是 const Complexe& 作为输入参数)可以显著减少不必要的对象拷贝,从而提升性能并减少栈内存的瞬时消耗。

是否还有其他实现方式?

  1. 按值传参 (Pass-by-value):
    • Complexe MyFunction(Complexe c)
    • 缺点:对于非基本类型,通常会导致对象复制,效率较低。对于 Complexe 这样的小对象,编译器有时能优化,但不能保证。
  2. 按指针传参 (Pass-by-pointer):
    • Complexe MyFunction(const Complexe* c)
    • 优点:也避免了复制,效率高。
    • 缺点:
      • 调用语法不同 (func(&obj)obj_ptr->method())。
      • 需要处理空指针 (nullptr) 的可能性,增加代码复杂性。
      • 所有权和生命周期管理可能更复杂(尽管对于 const 指针作为输入参数,这方面问题较少)。
    • 在现代 C++ 中,除非确实需要指针的语义(如表示可选对象或与 C API 交互),否则引用通常是更安全、更方便的选择。

对于运算符重载如 operator+(const Complexe& other),使用 const Complexe& 是标准且最佳的做法,因为它兼顾了效率和安全性(不修改输入对象)。

1-e)const 关键字的作用是什么?对于成员函数,如何使用 const?为用户添加 const 限定有什么好处?

const 关键字在 C++ 中用于表示“常量”或“不可修改性”。

const 关键字的作用:

  1. 修饰变量:
    • const int x = 5; 声明一个常量 x,其值不能被修改。
    • const double* ptr; ptr 指向一个 double 常量 (不能通过 ptr 修改所指 double 的值)。
    • double* const ptr; ptr 是一个常量指针 (指针本身的值不能修改,即不能指向其他地址)。
    • const double* const ptr; ptr 是一个常量指针,指向一个 double 常量。
  2. 修饰函数参数:
    • void func(const int param); 表明函数内部不应修改 param 的值(对于按值传递的内置类型,这更多是文档作用;对于引用或指针传递,它阻止修改原始数据)。
  3. 修饰成员函数 (在函数声明和定义的末尾):
    • double Complexe::GetModule() const;
    • 表明该成员函数是一个“观察者”(inspector) 或 "getter",它不会修改调用它的对象的任何非静态成员变量。
    • const 成员函数可以被 const 对象调用,也可以被非 const 对象调用。而非 const 成员函数只能被非 const 对象调用。
  4. 修饰函数返回值:
    • 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 限定有什么好处?

  1. 接口清晰性与文档化:
    • const 明确地告诉用户哪些函数会修改对象状态,哪些不会。这使得类的接口更容易理解和预测。
  2. 安全性 (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.
  3. 更广泛的可用性:
    • const 成员函数可以用于 const 和非 const 对象。如果一个有用的 getter 方法不是 const,那么它就不能在 const 对象上使用,这会限制其可用性。
  4. 编译器优化:
    • 虽然不是主要原因,但有时编译器可以利用函数是 const 的信息进行一些优化。
  5. 启用某些设计模式:
    • 例如,在函数参数中传递 const 引用 (const Complexe&) 是一种常见且高效的方式,这要求被调用的相关方法也是 const

遵循 const-correctness 是良好 C++ 编程实践的重要组成部分。

练习 2 :实现

2-a)在 QtCreator 中实现你的 Complexe

本项目采用了现代 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 的好处:

  1. 跨平台支持:可以在 Windows、macOS、Linux 等不同平台上构建
  2. 依赖管理:自动处理库的链接和头文件路径
  3. 模块化:将 Complexe 类编译为静态库,便于重用
  4. 测试集成:集成了 CTest 测试框架
  5. 现代化:符合现代 C++ 项目的标准实践

实现的关键函数:

  1. GetArgument(): 使用 atan2(I, R) 计算辐角
  2. Conjugue(): 返回 Complexe(R, -I)
  3. 算术运算符: 实现 +, -, *, / 运算
  4. 与 double 的运算: 通过成员函数和友元函数实现双向运算
  5. SetPolarCoordDegre(): 角度转弧度后设置极坐标

2-b)用一些简单例子验证你的 Complexe

编译和运行方法:

# 使用构建脚本(推荐)
./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°)

2-c)各项目标如何?速度和内存占用……以及易用性?

性能测试结果:

  • 自定义 Complexe 类:约 0.025 秒
  • std::complex<double>:约 0.021 秒
  • 性能差距约为 17%,在可接受范围内

分析:

  1. 速度

    • std::complex 稍快,主要因为标准库的高度优化
    • 我们的实现性能良好,差距不大
    • 使用 -O2 优化后性能提升明显
  2. 内存占用

    • 两者都是两个 double,内存占用相同(16字节)
    • 临时对象的创建和销毁是主要开销
  3. 易用性

    • ✅ 运算符重载让使用方式直观(c1 + c2
    • ✅ 支持与 double 的双向运算(c + 5.0, 5.0 + c
    • ✅ 多种输出格式(笛卡尔、极坐标)
    • ✅ 流输出支持(cout << c
    • const 正确性保证了接口安全

优化建议:

  • 内联小函数(CMake 的 Release 模式已启用)
  • 使用 RVO/NRVO 减少临时对象拷贝
  • 对于极致性能需求,可考虑表达式模板

2-d)对于表达式 c = c * (a / b + c); 会调用多少次构造函数?对应的内存使用情况如何?

详细分析:

假设 a, b, c 都是 Complexe 类型,表达式按以下顺序计算:

  1. tmp1 = a / b:

    • 调用 operator/(const Complexe&)
    • 1 次构造函数调用(创建返回的临时对象)
  2. tmp2 = tmp1 + c:

    • 调用 operator+(const Complexe&)
    • 1 次构造函数调用(创建返回的临时对象)
  3. tmp3 = c * tmp2:

    • 调用 operator*(const Complexe&)
    • 1 次构造函数调用(创建返回的临时对象)
  4. c = tmp3:

    • 调用赋值运算符(默认或自定义)
    • 0 次构造函数调用c 已存在,只是修改其值)

总计:3 次构造函数调用

内存使用情况:

  • 每个临时对象占用 16 字节(两个 double
  • 总临时内存:3 × 16 = 48 字节
  • 这些临时对象在表达式结束时自动销毁
  • 栈内存的分配/释放开销很小

编译器优化:

  • RVO (Return Value Optimization): 可能减少实际的对象拷贝
  • NRVO (Named Return Value Optimization): 对命名返回值的优化
  • 内联: 小函数可能被内联,减少函数调用开销

在 Release 模式下,编译器会应用这些优化,实际的构造函数调用次数可能少于理论值。


项目总结

本 TD 成功实现了一个功能完备的 Complexe 类,具备以下特点:

  1. 功能完整性:支持所有要求的运算和功能
  2. 性能良好:与 std::complex 性能差距很小
  3. 易用性强:丰富的运算符重载和输出格式
  4. 代码质量:遵循 const 正确性和现代 C++ 实践
  5. 项目结构:使用 CMake 构建系统,便于维护和扩展

该实现在内存效率、运行时间和易用性三个目标之间取得了良好的平衡。