【Effective C++】阅读笔记3

1. 成员变量声明为Private

建议将成员变量声明为Private,然后再public中提供调用该数据的接口

设置成Private的原因分析

  • 类内成员变量被声明为Private,那么就可以外部代码直接访问或者修改内部数据
  • 通过公共接口获取内部数据,这样可以减少对外部代码的影响
  • 直接将成员变量设置为public,可能会导致数据不一致或者逻辑错误,将成员变量设置为Private就可以避免外界随意修改数据,从而确保数据的完整性

将成员变量设置为public错误事例分析

外部代码可以随意修改变量的数值,与此同时还可以将其设置负数

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    std::string name;  
    int age;           
};

int main() {
    Person p;
    p.name = "Alice";
    p.age = -5;  
    std::cout << "名字:" << p.name << ",年龄:" << p.age << std::endl;
    return 0;
}

解决方法:成员变量声明为Private,同时提供公共接口

  • 获取成员变量只可以通过getter方法访问
  • 设置成员变量则是通过setAge,该方法内部会检查数据的有效性
cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name, int age) : name_(name) {
        setAge(age);  // 使用 setter 进行初始化
    }

    // Getter
    std::string getName() const { return name_; }
    int getAge() const { return age_; }

    // Setter
    void setAge(int age) {
        if (age >= 0) {  // 检查年龄有效性
            age_ = age;
        }
        else {
            std::cerr << "错误:年龄不能为负数" << std::endl;
        }
    }

private:
    std::string name_;
    int age_;
};

int main() {
    Person p("Alice", 25);
    std::cout << "名字:" << p.getName() << ",年龄:" << p.getAge() << std::endl;

    p.setAge(-5);  // 试图设置无效年龄
    std::cout << "名字:" << p.getName() << ",年龄:" << p.getAge() << std::endl;
    return 0;
}

成员变量声明为Private的好处

还可以支持数据验证和日志记录功能,也就是通过接口访问成员变量的时候,可以添加验证逻辑或者日志逻辑,从而更好的实现数据管理;

成员变量设置为Private后,还可以延迟初始化,避免不必要的消耗

2. 优先使用非成员非友元函数

主要是为了减少类的复杂性,如果一个函数在实现功能的时候不需要访问类的私有成员,那么就可以将这个类设计成为非成员非友元函数

问题分析

例如在一个表示分数的类中,实现了*运算符重载,此时如果将*运算符重载成为成员函数,那么就会增加这个类的复杂性

cpp 复制代码
#include <iostream>

class Rational {
public:
    Rational(int numerator = 0, int denominator = 1)
        : numerator_(numerator), denominator_(denominator) {}

    Rational operator*(const Rational& rhs) const {
        return Rational(numerator_ * rhs.numerator_, denominator_ * rhs.denominator_);
    }

    void print() const {
        std::cout << numerator_ << "/" << denominator_ << std::endl;
    }

private:
    int numerator_;
    int denominator_;
};

int main() {
    Rational a(1, 2);
    Rational b(3, 4);
    Rational result = a * b;
    result.print(); 
    return 0;
}

解决方法:如果一个类并不需要访问私有成员(因为a,b的数值都是构造时就赋值了),此时就可以将其设计为非成员非友元函数

  • operator*定义成非成员函数,然后声明为Rational的友元,这样就可以访问其私有成员
  • 通过这样的方法简化结构的同时,还可以访问其成员
cpp 复制代码
class Rational {
public:
    Rational(int numerator = 0, int denominator = 1)
        : numerator_(numerator), denominator_(denominator) {}

    void print() const {
        std::cout << numerator_ << "/" << denominator_ << std::endl;
    }

private:
    int numerator_;
    int denominator_;

    // 提供访问私有成员的友元声明
    friend Rational operator*(const Rational& lhs, const Rational& rhs);
};

// 非成员运算符重载
Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.numerator_ * rhs.numerator_, lhs.denominator_ * rhs.denominator_);
}

int main() {
    Rational a(1, 2);
    Rational b(3, 4);
    Rational result = a * b;
    result.print(); 
    return 0;
}

适合使用该方法场景分析

  • 不依赖对象内部状态的操作,例如只是数学计算、全局功能等
  • 运算符重载
  • 类中的一些辅助函数

总结反思

  • 优先使用非成员非友元函数:如果一个函数不需要访问类的私有成员,将它设计为非成员非友元函数更好
  • 减少类的复杂性:使用非成员函数可以减少类的职责和耦合度,使类的设计更清晰
  • 运算符重载优先使用非成员实现:运算符重载时,优先考虑非成员实现,除非必须访问类的私有成员
  • 提供友元函数访问私有成员:在非成员函数需要访问私有数据时,可以将其声明为友元函数

3. 如果所有参数都需要类型转换,使用非成员函数

C++中运算符重载时,可能会需要对操作数进行隐式类型转换,当所有操作数都需要进行类型转换的时候,最好选择非成员函数实现运算符重载 ,因为成员函数的运算符重载只会对右侧参数进行隐式类型转换 ,而非成员函数则允许对所有参数进行隐式类型转换。

成员函数运算符重载限制分析

成员函数实现运算符重载的时候,隐式类型转换 只会应用右侧的参数 ,也就是说,左侧的参数必须和对象类型匹配,否则编译器不会进行隐式类型转换

  • 下述代码编译的时候,编译器会尝试将2作为*左侧的操作数(this对象,也就是Rational类型)
  • 但是2不是Rational类型,所以最终肯定会导致编译失败
cpp 复制代码
#include <iostream>

class Rational {
public:
    Rational(int numerator = 0, int denominator = 1)
        : numerator_(numerator), denominator_(denominator) {}

    // 成员函数重载运算符*
    Rational operator*(const Rational& rhs) const {
        return Rational(numerator_ * rhs.numerator_, denominator_ * rhs.denominator_);
    }

    void print() const {
        std::cout << numerator_ << "/" << denominator_ << std::endl;
    }

private:
    int numerator_;
    int denominator_;
};

int main() {
    Rational r1(1, 2);
    Rational result = 2 * r1;
    result.print();
    return 0;
}

解决方法:使用非成员函数运算符重载

因为非成员函数可以允许对所有参数进行隐式类型转换的,这样就可以让编译器将2隐式转换为Rational类型,从而避免了成员函数的类型限制

cpp 复制代码
#include <iostream>

class Rational {
public:
    Rational(int numerator = 0, int denominator = 1)
        : numerator_(numerator), denominator_(denominator) {}

    void print() const {
        std::cout << numerator_ << "/" << denominator_ << std::endl;
    }

private:
    int numerator_;
    int denominator_;

    // 友元声明,允许非成员函数访问私有成员
    friend Rational operator*(const Rational& lhs, const Rational& rhs);
};

// 非成员函数重载运算符*
Rational operator*(const Rational& lhs, const Rational& rhs) {
    return Rational(lhs.numerator_ * rhs.numerator_, lhs.denominator_ * rhs.denominator_);
}

int main() {
    Rational r1(1, 2);
    Rational result = 2 * r1;  // 允许隐式类型转换
    result.print();  
    return 0;
}

使用场景分析

  • 非成员函数运算符重载
    • 所有参数可能都需要进行隐式类型转换 :例如 int * Rational,这时非成员函数更灵活
    • 对称的二元运算符 :像 +-*/ 等对称运算符,通常适合实现为非成员函数
    • 允许访问私有成员 :通过 friend 声明,可以让非成员函数访问类的私有数据成员,确保函数实现的灵活性
  • 成员函数运算符重载
    • 赋值相关运算符 :如 =+=-=*=/=,因为它们会改变左侧操作数的值,通常应实现为成员函数
    • 单目运算符 :如前置和后置 ++--、取地址 &、解引用 * 等,通常适合实现为成员函数,因为它们通常只对一个操作数进行操作

4. 实现一个不抛异常的swap函数

自我实现swap函数的原因

首先std库中提供的swap函数默认是通过拷贝构造函数和赋值运算符来实现,但是这些操作有可能会出现异常,其次主要目的就是减少性能开销,提高代码安全性

错误分析,使用默认swap交换一个有动态资源的类

  • swap使用的是拷贝构造和赋值与赋值运算符,那么就很有可能触发内存分配和释放操作
  • 代码中如果构造和赋值运算符抛出异常,那么swap就可能引发异常,最终导致w1和w2状态不一致
  • 中断原因分析: widget类中name_是一个指针类型,当使用swap交换两个widget对象中的name_指针时,一个对象被销毁后,其会释放name_指针指向的内存,而另一个对象也会释放这块已经释放的内存,所以导致了双重释放,最终导致了异常中断
    • 双重释放的根源在于,在没有定义深拷贝的时候,swap使用的是浅拷贝,所以只是简单的拷贝了其数值,所以就造成了多个对象持有相同的指针,所以最终在析构的时候就会重复释放同一块内存,最终引发异常
cpp 复制代码
#include <iostream>
#include <string>
#include <algorithm>  // for std::swap

class Widget {
public:
    Widget(const std::string& name) : name_(new std::string(name)) {}

    ~Widget() { delete name_; }

    // 打印 name_
    void print() const {
        std::cout << "Widget 名称: " << *name_ << std::endl;
    }

private:
    std::string* name_;
};

int main() {
    Widget w1("Alice");
    Widget w2("Bob");

    std::swap(w1, w2);  // 使用 std::swap,默认调用拷贝构造和赋值

    w1.print();
    w2.print();

    return 0;
}

解决方法:实现一个成员函数swap来直接交换数据成员,这样就可以有效避免不必要的拷贝和赋值

  • 通过交换指针,实现两个对象动态资源的交换,这样就避免了浅拷贝的问题
  • 通过非成员函数调用类中的swap函数,从而确保std::swap兼容
cpp 复制代码
#include <iostream>
#include <string>
#include <utility>  // for std::swap

class Widget {
public:
    Widget(const std::string& name) : name_(new std::string(name)) {}

    ~Widget() { delete name_; }

    // 提供一个不抛异常的 swap 成员函数
    void swap(Widget& other) noexcept {
        std::swap(name_, other.name_);  // 直接交换指针,不抛异常
    }

    // 打印 name_
    void print() const {
        std::cout << "Widget 名称: " << *name_ << std::endl;
    }

private:
    std::string* name_;
};

// 提供一个非成员的 swap 函数,以便与 std::swap 兼容
void swap(Widget& lhs, Widget& rhs) noexcept {
    lhs.swap(rhs);  // 使用 Widget 的成员 swap
}

int main() {
    Widget w1("Alice");
    Widget w2("Bob");

    swap(w1, w2);  // 使用自定义 swap 函数

    w1.print();
    w2.print();

    return 0;
}

总结反思

  • 优先自己定义一个没有异常的swap函数,同时通过noexcept声明来确保swap函数不会抛出异常
  • 实现一个非成员swap函数,也就是通过调用成员swap函数,从而确保与std库中的swap函数兼容,使得可以无缝衔接到自己类函数中

5. 尽量延后变量定义的时间

减少性能开销,当定义个类对象的时候,构造函数会立即调用,有可能会涉及到资源分配,所以适当的将变量定义延后,可以避免不必要的初始化

错误分析

例如代码中,即使最终a不是大于b的,变量Result和temp还是被创建了,这就浪费了内存空间

cpp 复制代码
#include <iostream>
#include <string>

int main() {
    int a = 5;
    int b = 10;
    int result = 0;  // 提前定义变量
    std::string temp = "未使用的字符串";  // 提前定义变量

    if (a > b) {
        result = a + b;
    }

    std::cout << "Result: " << result << std::endl;
    return 0;
}

解决方法:延后变量的定

也就是说,只有在变量Result和temp需要的时候才被定义,这样就可以避免不必要的内存分配

cpp 复制代码
int main() {
    int a = 5;
    int b = 10;

    if (a > b) {
        int result = a + b;  // 延后定义变量
        std::cout << "Result: " << result << std::endl;
    }

    if (a == 5) {
        std::string temp = "延迟定义的字符串";  // 延后定义变量
        std::cout << "Temp: " << temp << std::endl;
    }

    return 0;
}

总结反思

  • 尽量延后变量的定义,减少性能损耗
  • 变量定义在首次使用的地方,可以增强代码可读性

6. 尽量少进行转换

转换增加的劣势

  • 频繁的类型转换会增加代码的复杂程度,使得代码难以理解和维护
  • 运行时的类型转换和多层次的隐式类型转换会损耗性能,最终会拖慢程序的运行速度

隐式类型转换问题分析

  • 代码中如果将double转换成int类型,那么double后面的小数点数据就会丢失
cpp 复制代码
#include <iostream>

void printDouble(double value) {
    std::cout << "Double 值: " << value << std::endl;
}

int main() {
    int intValue = 10;
    printDouble(intValue);  // 隐式转换 int 到 double

    double doubleValue = 5.99;
    int truncatedValue = doubleValue;  // 隐式转换 double 到 int
    std::cout << "截断后的 int 值: " << truncatedValue << std::endl;

    return 0;
}

方法:使用显式类型转换,明确自己想要转换成何种类型

  • 例如可以使用static_cast表明转换意图
cpp 复制代码
#include <iostream>

int main() {
    double doubleValue = 5.99;
    int truncatedValue = static_cast<int>(doubleValue);  
    std::cout << "截断后的 int 值: " << truncatedValue << std::endl;

    return 0;
}

技巧:要避免将基础类型转换为自定义类型的行为

  • 使用explicit关键字避免构造函数进行隐式类型转换,也就是只有在显式声明的时候才可以将double转换为complex对象
  • 隐式类型转换分析
    • Complex c2 = 3.0:double到complex的隐式类型转换
    • 首先explicit关键字的作用就是在一个类中接受单个参数的构造函数,编译器允许使用该函数进行隐式类型转换,所以在这个类中c1对象可以通过传入单个参数完成构造
    • c2报错的原因在于编译器需要从double隐式转换到complex,但是此时构造函数被标记为explicit,所以造成了编译器报错,阻止了这种隐式转换的发生
cpp 复制代码
#include <iostream>

class Complex {
public:
    Complex(double real, double imaginary) : real_(real), imaginary_(imaginary) {}
    // 避免隐式转换的构造函数
    explicit Complex(double real) : real_(real), imaginary_(0) {}

    void print() const {
        std::cout << "Complex 数: " << real_ << " + " << imaginary_ << "i" << std::endl;
    }

private:
    double real_;
    double imaginary_;
};

int main() {
    Complex c1(3.0);  // 合法
    // Complex c2 = 3.0;  // 编译错误,防止隐式转换
    c1.print();

    return 0;
}

总结反思

  • 代码中减少隐式类型转换的使用,从而避免隐式类型转换而导致的错误
  • 多使用显示类型转换,例如可以使用static_cast或者dynamic_cast等,以确保代码意图明确
  • 使用explicit关键字,防止构造函数的隐式类型转换

7. 避免返回对象内部的指针或者引用

直接返回对象的内部指针或者引用会使得对象的实现细节暴露,这样就会导致数据不安全和未定义的行为,特别是当返回指针或者引用是一个局部变量的时候,如果无意中修改了类中的数据,这样就可能会导致问题。

错误分析:返回对象的内部指针或者引用

  • getName函数返回了name_指针,暴露了Person类中的细节
  • 外部可以通过namePtr直接访问或者间接修改Person类的私有数据,最终破坏其封装性
cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : name_(name) {}

    // 返回内部指针(不安全)
    const std::string* getName() const {
        return &name_;
    }

private:
    std::string name_;
};

int main() {
    Person p("Alice");
    const std::string* namePtr = p.getName();

    std::cout << *namePtr << std::endl;  // 输出:Alice
    // 非法操作:尽管是 const,外部代码可以修改底层数据或产生悬空指针风险
    return 0;
}

解决方法:返回name数据的副本,保证不对原始对象的影响

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : name_(name) {}

    // 返回副本
    std::string getName() const {
        return name_;
    }

private:
    std::string name_;
};

int main() {
    Person p("Alice");
    std::string name = p.getName();

    std::cout << name << std::endl; 
    // 修改 name 的副本不会影响 Person 对象的 name_
    name = "Bob";
    std::cout << p.getName() << std::endl; 
    return 0;
}

返回指针或者引用保证安全方法:使用const限制修改

通过返回数据引用,从而使得const从而确保调用者无法修改返回的内容

cpp 复制代码
#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : name_(name) {}

    // 返回 const 引用,防止修改
    const std::string& getName() const {
        return name_;
    }

private:
    std::string name_;
};

int main() {
    Person p("Alice");
    const std::string& nameRef = p.getName();

    std::cout << nameRef << std::endl; 
     nameRef = "Bob"; 
    return 0;
}

使用智能指针安全的返回指针和引用

针对于动态资源分配,使用智能指针可以更好的管理资源分配

  • 通过智能指针管理动态分配的资源,同时通过const限制修改权限
cpp 复制代码
#include <iostream>
#include <memory>
#include <string>

class Person {
public:
    Person(const std::string& name) : name_(std::make_shared<std::string>(name)) {}

    // 返回智能指针
    std::shared_ptr<const std::string> getName() const {
        return name_;
    }

private:
    std::shared_ptr<const std::string> name_;
};

int main() {
    Person p("Alice");
    std::shared_ptr<const std::string> namePtr = p.getName();

    std::cout << *namePtr << std::endl;  // 输出:Alice
    // *namePtr = "Bob";  // 编译错误,无法修改
    return 0;
}

总结反思

  • 要避免返回内部数据的指针或引用,尽量选择返回数据的副本,从而保证类的封装性和安全性
  • 如果必须返回引用或者指针,那么使用const限制访问权限,防止其调用修改内部数据
  • 如果是动态资源,需要分配内存空间,那么优先使用智能指针对资源进行管理,从而避免内存泄漏或者悬空指针的情况

8. 为"异常安全"努力

核心意思就是实现在抛出异常的时候保证程序运行安全,也就是在抛出异常的时候要确保状态的完整性和一致性

异常安全的三种保护标准

  • 基本保证:也就是即使发生异常,程序不会发生资源泄漏或者数据不会发生资源泄漏或者数据损坏,即使程序在异常发生后不能恢复正常操作
  • 强烈保证:要求程序在异常后可以退回到异常发生前的状态,也就是操作要么成功要么完全失败,类似于数据库中的原子性
  • 不抛出异常安全保证:承诺某个函数绝对不会抛出异常,也是最强的安全保证,可以通过noexcept关键字声明不抛出异常的函数

方法1:使用RAII

通过RAII机制,保证即使发生异常的时候,也可以保证资源正常释放

cpp 复制代码
#include <iostream>
#include <memory>
#include <string>

class Person {
public:
    Person(const std::string& name) 
        : name_(std::make_unique<std::string>(name))
        {}

    void printName() const {
        std::cout << "姓名: " << *name_ << std::endl;
    }

private:
    std::unique_ptr<std::string> name_;
};

int main() {
    try {
        Person p("Alice");
        p.printName();
    }
    catch (...) {
        std::cout << "异常发生" << std::endl;
    }
    return 0;
}

方法2:使用noexcept保证不抛出异常

利用该关键字声明一个函数不会抛出异常,通过noexcept提高代码的稳定性和性能

  • swap函数使用noexcept声明不抛出异常,从而保证swap操作异常的安全性
cpp 复制代码
#include <iostream>
#include <utility>
#include <vector>

class Widget {
public:
    Widget(int value) : value_(value) {}
    void swap(Widget& other) noexcept {
        std::swap(value_, other.value_);
    }

private:
    int value_;
};

int main() {
    Widget w1(1);
    Widget w2(2);

    w1.swap(w2);  // 不抛出异常的 swap 操作
    return 0;
}

方法3:使用事务式编程

也就是进行操作之前,先创建一个副本或者备份,目的就是确保操作成功后替换原始对象,这样即使操作过程中发生异常的时候,程序依然可以恢复原状

cpp 复制代码
class Transaction {
public:
    Transaction(const std::string& data) : data_(data) {}

    void setData(const std::string& newData) {
        // 先创建备份,保证操作失败时能够恢复
        std::string backup = data_;

        // 假设此操作可能抛出异常
        processData(newData);

        // 更新成功才替换原数据
        data_ = newData;
    }

    void printData() const {
        std::cout << "数据: " << data_ << std::endl;
    }

private:
    void processData(const std::string& newData) {
        if (newData.empty()) throw std::runtime_error("无效的数据");
    }

    std::string data_;
};

int main() {
    Transaction transaction("初始数据");

    try {
        transaction.setData("");
    }
    catch (const std::exception& e) {
        std::cout << "异常: " << e.what() << std::endl;
    }

    transaction.printData();  // 输出:新数据或初始数据
    return 0;
}

总结反思

  • 优先使用智能指针管理动态内存资源,从而减少资源泄漏的风险
  • 优先使用noexcept,可以避免其抛出异常
  • 要谨慎操作状态修改,使用备份或者事务机制,从而确保即使发生异常也可以回滚

9. 理解Inline函数的利弊

内联函数就是将函数代码直接嵌入到调用点,从而避免函数调用的开销,编译器会在调用点替换函数调用为函数体代码,这样就减少了调用堆栈的实践。

内联函数的优点分析

  • 减少函数调用时的开销
    • 因为调用函数的时候,程序会进行一系列的操作,例如压栈、跳转到函数地址、返回值处理等,这些都会增加函数的开销
  • 提高性能
    • 对于一些小型、频繁调用的函数使用内联函数可以显著的提高性能
cpp 复制代码
class Rectangle {
public:
    Rectangle(int width, int height) : width_(width), height_(height) {}

    // 内联 getter 函数
    inline int getWidth() const { return width_; }
    inline int getHeight() const { return height_; }

private:
    int width_;
    int height_;
};

int main() {
    Rectangle rect(10, 20);
    int area = rect.getWidth() * rect.getHeight();  // 内联后无调用开销
    return 0;
}

内联函数的缺点

  • 增加代码体积
    • 内联函数会显著增加代码的体积,也就会导致指令缓存压力增加,最终降低性能
  • 编译时间加长
    • 内联增加了编译器的负担,特别是当函数定义在头文件中时,每次编译都会将函数体嵌入每个调用点,导致编译时间变长,且链接时间也可能增加
  • 内联函数多不方便进行调试

内联函数适用的场景

小型而且简单函数,因为这样的函数逻辑简单,编译器更好对其进行优化;对于一些执行频繁而且对性能要求高的小函数,内联函数也可以显著的提高其性能。

不适合内联函数分析

  • 复杂大型的函数
  • 递归函数
    • 因为递归的调用次数在编译期间是未知的,递归的内联会导致代码膨胀
  • 虚函数
    • 调用的时候需要通过虚函数表进行解析,编译器通常无法再编译期间确定调用的具体函数

总结反思

  • 小函数适合使用内联,相反大且复杂的函数不适合内联
  • 递归和虚函数不适合内联,因为递归次数是不确定,且虚函数通常需要通过表去查找,内联通常会被编译器忽略
  • 编译器自动优化,编译器会自动识别哪些函数适合内联

10. 将文件间的编译依赖降到最低

文件之间编译过度依赖代价

  • 增加编译时间
    • 因为每次修改头文件,或者所有直接或间接依赖该头文件的文件都需要重新编译,这可能导致整个项目的编译时间显著增加。随着项目规模增长,频繁的头文件依赖会拖慢编译速度
  • 耦合度增加
    • 修改一个文件,会影响其他所有与之相关的文件
  • 代码难以维护

方法1:使用前置声明

头文件中尽量使用前置声明,而不是选择包含完整的头文件,使用前置声明一个类的存在,而不需要包含其完整定义,从而减少头文件之间的依赖关系

  • 前置声明适用于指针或者引用成员变量的声明
cpp 复制代码
// Employee.h
#include <string>

class Department;  // 前向声明 Department 类

class Employee {
public:
    Employee(const std::string& name);
    void setDepartment(Department* dept);  // 使用前向声明的指针
private:
    std::string name_;
    Department* department_;
};
cpp 复制代码
// Department.h
#include <string>

class Department {
public:
    Department(const std::string& name);
private:
    std::string name_;
};

方法2:使用#include在源文件中实现依赖

也就是说将头文件的依赖放在源文件中,而不是头文件中,这样就可以减少头文件的包含。只有在类的成员函数需要某个类的定义时,才在.cpp文件中使用#include所需头文件即可

cpp 复制代码
// Employee.h (头文件中只放前置声明)
#include <string>

class Department;  // 使用前向声明

class Employee {
public:
    Employee(const std::string& name);
    void setDepartment(Department* dept);
private:
    std::string name_;
    Department* department_;
};
cpp 复制代码
// Employee.cpp
#include "Employee.h"
#include "Department.h"  // 只在 .cpp 文件中包含

Employee::Employee(const std::string& name) : name_(name), department_(nullptr) {}

void Employee::setDepartment(Department* dept) {
    department_ = dept;
}

方法3:pimpl方法

在类中使用指针指向该类的方法,将类的实现细节隐藏在源文件中,从而降低头文件的依赖关系;简单可以理解成调用的时候,直接通知封装好的指针

cpp 复制代码
// Widget.h
#include <memory>
class WidgetImpl;  // 前向声明 WidgetImpl 类

class Widget {
public:
    Widget();
    ~Widget();
    void doSomething();

private:
    std::unique_ptr<WidgetImpl> pImpl;  // Pimpl 指针
};
cpp 复制代码
// Widget.cpp
#include "Widget.h"
#include "WidgetImpl.h"  // 只有在实现文件中包含实现类

Widget::Widget() : pImpl(std::make_unique<WidgetImpl>()) {}

Widget::~Widget() = default;

void Widget::doSomething() {
    pImpl->performTask();
}

总结反思

  • 头文件使用前向声明,减少对编译的依赖
  • 尽量在.cpp文件中使用#include,而不是头文件
  • pimpl收发,通过指针调用,隐藏细节
相关推荐
幼儿园园霸柒柒7 分钟前
第七章: 7.3求一个3*3的整型矩阵对角线元素之和
c语言·c++·算法·矩阵·c#·1024程序员节
2401_8582861130 分钟前
C6.【C++ Cont】cout的格式输出
开发语言·c++
忘梓.33 分钟前
排序的秘密(1)——排序简介以及插入排序
数据结构·c++·算法·排序算法
zhj186791306131 小时前
远程控制项目第四天 功能实现
c++
摆烂小白敲代码1 小时前
背包九讲——背包问题求方案数
c语言·c++·算法·背包问题·背包问题求方案数
小柯J桑_1 小时前
C++:set详解
c++·set
齐 飞2 小时前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
白子寰2 小时前
【C++打怪之路Lv13】- “继承“篇
开发语言·c++
flying robot2 小时前
Go结构体(struct)
笔记
王俊山IT2 小时前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(1)
开发语言·c++·笔记·学习