C++ 面向对象

C++ 面向对象编程(OOP)是语言的核心特性,通过封装、继承、多态三大支柱实现代码复用、模块化和可扩展性。

1、三大特性

1.1 封装

将数据(成员变量)和操作数据的方法(成员函数)捆绑在类中,通过访问控制符限制外部对内部数据的直接访问,仅暴露必要接口。

隐藏实现细节,确保数据安全性(避免意外修改)。

C++ 通过 publicprivateprotected 控制成员的可见性:

  • public:类内外均可访问(暴露的接口)。
  • private:仅类内及友元可访问(隐藏的实现细节)。
  • protected:类内、友元及派生类可访问(用于继承场景)。
cpp 复制代码
class BankAccount {
private:
    // 私有数据(隐藏,外部无法直接访问)
    string account_id;
    double balance; 

public:
    // 公有接口(暴露给外部的操作)
    BankAccount(string id, double init_balance) 
        : account_id(id), balance(init_balance) {}

    void deposit(double amount) { // 存款(仅允许通过接口修改)
        if (amount > 0) balance += amount;
    }

    double get_balance() const { // 查询余额(只读接口)
        return balance;
    }

private:
    // 私有方法(内部实现,不暴露)
    void log_transaction() { 
        // 记录交易日志(细节隐藏)
    }
};

int main() {
    BankAccount acc("12345", 1000);
    acc.deposit(500); // 正确:通过公有接口操作
    cout << acc.get_balance() << endl; // 正确:通过接口查询
    // acc.balance = 2000; // 错误:private 成员不可直接访问
    return 0;
}

问题:封装的主要优点是什么?

  1. 数据隐藏:保护内部状态不被意外修改
  2. 实现隔离:可以修改内部实现而不影响外部代码
  3. 接口标准化:提供清晰的使用方式
  4. 增强安全性:通过验证逻辑保护数据完整性

1.2 继承

从已有类(基类 / 父类)派生出新类(派生类 / 子类),子类自动继承父类的成员(变量和函数),并可扩展新功能或重写父类方法。

代码复用(避免重复实现),建立类之间的层次关系。

继承方式与访问权限

继承方式(public/private/protected)会改变基类成员在派生类中的可见性:

  • 公有继承:基类的public→派生类public,protected→protected
  • 保护继承:基类的public和protected→派生类protected
  • 私有继承:基类的public和protected→派生类private

默认继承方式class 默认 private 继承,struct 默认 public 继承(建议显式指定)。

cpp 复制代码
// 基类
class Shape {
protected: // 受保护成员,派生类可访问
    std::string name;
    int x, y;

public:
    Shape(const std::string& n, int xPos, int yPos) 
        : name(n), x(xPos), y(yPos) {}
    
    virtual void draw() const {
        std::cout << "Drawing " << name << " at (" << x << ", " << y << ")" << std::endl;
    }
    
    virtual double area() const = 0; // 纯虚函数,使Shape成为抽象类
    
    virtual ~Shape() {} // 虚析构函数,确保正确释放资源
};

// 公有继承 - "是一个"关系
class Circle : public Shape {
private:
    double radius;

public:
    Circle(int xPos, int yPos, double r) 
        : Shape("Circle", xPos, yPos), radius(r) {}
    
    void draw() const override {
        std::cout << "Drawing Circle with radius " << radius 
                  << " at (" << x << ", " << y << ")" << std::endl;
    }
    
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

// 保护继承 - 不常用
class ProtectedBase {
public:
    int publicVar;
protected:
    int protectedVar;
};

class Derived : protected ProtectedBase {
    // publicVar 和 protectedVar 在Derived中都变成protected
};

// 私有继承 - "实现为"关系
class Stack : private std::vector<int> {
public:
    void push(int value) {
        this->push_back(value); // 使用基类的实现
    }
    
    int pop() {
        if (!this->empty()) {
            int value = this->back();
            this->pop_back();
            return value;
        }
        throw std::runtime_error("Stack is empty");
    }
    
    // 使用using恢复访问权限
    using std::vector<int>::empty;
    using std::vector<int>::size;
};

1.3 多态

同一接口(如函数名)在不同对象上表现出不同行为。C++ 多态分为 静态多态 (编译期)和 动态多态(运行期)。

静态多态(编译期多态)

通过 函数重载模板 实现,编译器在编译期确定调用哪个函数。

  • 函数重载:同一作用域内,函数名相同但参数列表(类型 / 个数 / 顺序)不同。

    cpp 复制代码
    class Math {
    public:
        int add(int a, int b) { return a + b; }
        double add(double a, double b) { return a + b; } // 重载(参数类型不同)
    };
  • 模板:通用函数 / 类,编译器根据实参类型生成具体版本。

    cpp 复制代码
    template <typename T>
    T add(T a, T b) { return a + b; }
    
    int main() {
        add(1, 2); // 生成 int 版本
        add(3.14, 2.7); // 生成 double 版本
    }

动态多态(运行期多态)

通过 虚函数(virtual) 实现,程序运行时根据对象实际类型确定调用的函数(核心是 "迟绑定")。

动态多态的实现条件

  1. 基类声明虚函数(virtual 关键字)。
  2. 派生类重写(override)该虚函数(函数签名必须完全一致)。
  3. 通过 基类指针或引用 调用虚函数。
cpp 复制代码
class Animal {
public:
    virtual void speak() const { cout << "Animal speaks"; } // 虚函数
    virtual ~Animal() {} // 虚析构(避免内存泄漏)
};

class Dog : public Animal {
public:
    void speak() const override { cout << "Dog barks"; } // 重写
};

class Cat : public Animal {
public:
    void speak() const override { cout << "Cat meows"; } // 重写
};

int main() {
    Animal* a1 = new Dog();
    Animal* a2 = new Cat();

    a1->speak(); // 运行时绑定:输出 "Dog barks"
    a2->speak(); // 运行时绑定:输出 "Cat meows"

    delete a1; // 调用 Dog 析构 + Animal 析构
    delete a2; // 调用 Cat 析构 + Animal 析构
    return 0;
}

动态多态的底层原理:虚函数表(vtable)与虚指针(vptr)

  1. 虚函数表(vtable):每个包含虚函数的类会生成一个全局唯一的 vtable(数组),存储该类所有虚函数的地址。
  2. 虚指针(vptr):类的每个对象会隐含一个 vptr 成员,指向该类的 vtable。
  3. 重写机制:派生类重写虚函数时,会替换 vtable 中对应位置的函数地址(基类 vtable 不变)。
  4. 调用过程:通过基类指针调用虚函数时,先通过 vptr 找到 vtable,再调用对应地址的函数(运行时确定)。

2、重要概念

2.1 纯虚函数与抽象类

  • 纯虚函数 :声明时初始化为 0 的虚函数(virtual return_type func() = 0;),无实现,仅作为接口。
  • 抽象类 :包含纯虚函数的类,不能实例化,只能作为基类被继承(派生类必须实现纯虚函数才能实例化)。
cpp 复制代码
class Vehicle { // 抽象类
public:
    virtual void run() = 0; // 纯虚函数(接口)
};

class Car : public Vehicle {
public:
    void run() override { cout << "Car runs"; } // 必须实现
};

// Vehicle v; // 错误:抽象类不能实例化
Car c; // 正确:已实现纯虚函数

2.2 this 指针

每个非静态成员函数都隐含一个 this 指针,指向当前对象(函数所属的实例)。

区分成员变量与函数参数(同名时),返回当前对象的引用。

cpp 复制代码
class Person {
private:
    string name;
public:
    Person(string name) { 
        this->name = name; // this 区分成员变量与参数
    }

    Person& set_name(string name) {
        this->name = name;
        return *this; // 返回当前对象(支持链式调用)
    }
};

int main() {
    Person p("Alice");
    p.set_name("Bob").set_name("Charlie"); // 链式调用
    return 0;
}

2.3 构造函数和析构函数

  • 构造函数: 对象创建时自动调用,用于初始化成员变量(与类名同名,无返回值)。
    • 可重载(支持不同初始化方式)。
    • 派生类构造函数必须初始化基类构造函数(通过初始化列表)。
  • 析构函数: 对象销毁时自动调用,用于释放资源(如动态内存、文件句柄),格式为~类名()
    • 若类作为基类且可能被继承,析构函数必须声明为 virtual(否则删除基类指针时可能泄漏派生类资源)。
cpp 复制代码
class Example {
private:
    int* data;
    size_t size;
    
public:
    // 1. 默认构造函数
    Example() : data(nullptr), size(0) {}
    
    // 2. 参数化构造函数
    Example(size_t s) : size(s) {
        data = new int[size]{};
    }
    
    // 3. 拷贝构造函数
    Example(const Example& other) : size(other.size) {
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    
    // 4. 移动构造函数 (C++11)
    Example(Example&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
    }
    
    // 5. 拷贝赋值运算符
    Example& operator=(const Example& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }
    
    // 6. 移动赋值运算符 (C++11)
    Example& operator=(Example&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            other.data = nullptr;
            other.size = 0;
        }
        return *this;
    }
    
    // 7. 析构函数
    ~Example() {
        delete[] data;
    }
};

2.4 拷贝构造与移动构造(C++11)

拷贝构造函数 :用已有对象初始化新对象(类名(const 类名& other)),默认浅拷贝(仅复制指针地址),可能导致 "double free" 问题,需手动实现深拷贝。

cpp 复制代码
class String {
private:
    char* data;
public:
    // 深拷贝构造(避免浅拷贝问题)
    String(const String& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }
};

移动构造函数 (C++11+):用临时对象(右值)的资源初始化新对象(类名(类名&& other)),转移资源所有权(避免拷贝,提升性能)。

cpp 复制代码
String(String&& other) noexcept {
    data = other.data; // 直接接管资源
    other.data = nullptr; // 原对象放弃资源
}

2.5 友元

允许外部函数或类访问当前类的 private/protected 成员(突破封装,慎用)。

运算符重载(如 operator<< 需要访问对象内部数据)

cpp 复制代码
class Matrix {
private:
    double data[4][4];
    
public:
    // 友元函数
    friend Matrix operator*(const Matrix& a, const Matrix& b);
    
    // 友元类
    friend class MatrixTransformer;
    
    // 友元成员函数
    friend void Vector::transform(const Matrix& m);
};

// 友元函数实现
Matrix operator*(const Matrix& a, const Matrix& b) {
    Matrix result;
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            result.data[i][j] = 0;
            for (int k = 0; k < 4; k++) {
                result.data[i][j] += a.data[i][k] * b.data[k][j];
            }
        }
    }
    return result;
}

2.6 多重继承和菱形继承

当一个类间接继承自同一个基类两次时,会导致基类成员重复存储(二义性)。

多重继承

cpp 复制代码
class A { public: int x; };
class B : public A {};
class C : public A {};
class D : public B, public C {}; 

int main() {
    D d;
    // d.x = 10; // 错误:二义性(B::x 还是 C::x?)
    return 0;
}

使用 虚继承(virtual inheritance),确保基类在派生类中只存在一份实例。

cpp 复制代码
class A { public: int x; };
class B : virtual public A {}; // 虚继承 A
class C : virtual public A {}; // 虚继承 A
class D : public B, public C {}; 

int main() {
    D d;
    d.x = 10; // 正确:A 只存在一份
    return 0;
}

3、现代 C++ 中的面向对象特性

3.1 override 和 final关键字

cpp 复制代码
class Base {
public:
    virtual void func() const;
    virtual void finalFunc() final; // 禁止重写
};

class Derived : public Base {
public:
    void func() const override; // 显式重写
    // void finalFunc(); // 错误:不能重写final函数
};

class FinalClass final : public Base { // 禁止继承
    // ...
};

3.2 智能指针

cpp 复制代码
class Base {
public:
    virtual void process() = 0;
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void process() override {
        std::cout << "Processing in Derived" << std::endl;
    }
};

// 使用智能指针管理多态对象
std::unique_ptr<Base> createObject() {
    return std::make_unique<Derived>();
}

void processObjects(const std::vector<std::unique_ptr<Base>>& objects) {
    for (const auto& obj : objects) {
        obj->process();
    }
}

4、面向对象设计原则 (SOLID)

4.1 单一职责原则 (Single Responsibility)

一个类应该只有一个引起变化的原因。

c++ 复制代码
// 违反单一职责
class Report {
public:
    void generateContent() { /* 生成内容 */ }
    void saveToFile() { /* 保存到文件 */ }
    void print() { /* 打印 */ }
    void sendByEmail() { /* 发送邮件 */ }
};

// 遵循单一职责
class ReportContent {
public:
    void generate() { /* 生成内容 */ }
};

class ReportSaver {
public:
    void saveToFile(const ReportContent& content) { /* 保存到文件 */ }
};

class ReportPrinter {
public:
    void print(const ReportContent& content) { /* 打印 */ }
};

class ReportEmailSender {
public:
    void sendByEmail(const ReportContent& content) { /* 发送邮件 */ }
};

4.2 开闭原则 (Open-Closed)

对扩展开放,对修改关闭。

c++ 复制代码
// 违反开闭原则
class AreaCalculator {
public:
    double calculateArea(const std::string& shapeType, double param1, double param2 = 0) {
        if (shapeType == "circle") {
            return 3.14 * param1 * param1;
        } else if (shapeType == "rectangle") {
            return param1 * param2;
        }
        // 添加新形状需要修改这个函数
        return 0;
    }
};

// 遵循开闭原则
class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14 * radius * radius; }
};

class Rectangle : public Shape {
private:
    double width, height;
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override { return width * height; }
};

class AreaCalculator {
public:
    double calculateArea(const Shape& shape) {
        return shape.area(); // 添加新形状不需要修改这个函数
    }
};

4.3 里氏替换原则 (Liskov Substitution)

派生类必须能够替换其基类。

c++ 复制代码
// 违反里氏替换原则
class Rectangle {
protected:
    double width, height;
public:
    virtual void setWidth(double w) { width = w; }
    virtual void setHeight(double h) { height = h; }
    double area() const { return width * height; }
};

class Square : public Rectangle {
public:
    void setWidth(double w) override {
        width = height = w; // 改变了父类的行为
    }
    void setHeight(double h) override {
        width = height = h; // 改变了父类的行为
    }
};

// 使用
void testRectangle(Rectangle& rect) {
    rect.setWidth(5);
    rect.setHeight(4);
    assert(rect.area() == 20); // 对于Square会失败
}

4.4 接口隔离原则 (Interface Segregation)

客户端不应该被迫依赖它们不使用的接口。

c++ 复制代码
// 违反接口隔离
class Worker {
public:
    virtual void work() = 0;
    virtual void eat() = 0;
    virtual void sleep() = 0;
};

class Robot : public Worker {
public:
    void work() override { /* 工作 */ }
    void eat() override { /* 机器人不需要吃饭 */ }
    void sleep() override { /* 机器人不需要睡觉 */ }
};

// 遵循接口隔离
class Workable {
public:
    virtual void work() = 0;
};

class Eatable {
public:
    virtual void eat() = 0;
};

class Sleepable {
public:
    virtual void sleep() = 0;
};

class Human : public Workable, public Eatable, public Sleepable {
    // 实现所有接口
};

class Robot : public Workable {
    // 只实现需要的接口
};

4.5 依赖倒置原则 (Dependency Inversion)

高层模块不应该依赖低层模块,两者都应该依赖抽象。

c++ 复制代码
// 违反依赖倒置
class MySQLDatabase {
public:
    void saveData(const std::string& data) {
        // MySQL特定的保存逻辑
    }
};

class Application {
private:
    MySQLDatabase database; // 直接依赖具体实现
public:
    void process() {
        // 处理数据
        database.saveData("processed data");
    }
};

// 遵循依赖倒置
class Database {
public:
    virtual void saveData(const std::string& data) = 0;
    virtual ~Database() = default;
};

class MySQLDatabase : public Database {
public:
    void saveData(const std::string& data) override {
        // MySQL特定的保存逻辑
    }
};

class Application {
private:
    Database& database; // 依赖抽象
public:
    Application(Database& db) : database(db) {}
    
    void process() {
        // 处理数据
        database.saveData("processed data");
    }
};

5、常见问题

  1. 构造函数为什么不能是虚函数?

    构造函数:虚函数依赖vtable,而vtable在构造函数中初始化,形成循环依赖

  2. 如何选择继承与组合?

    使用继承:is-a关系,需要多态行为

    使用组合:has-a关系,代码复用无需多态

    优先选择组合:更灵活,降低耦合度

    组合的最佳实践

    • 依赖倒置原则

      cpp 复制代码
      // 高层模块依赖抽象
      class ReportGenerator {
      private:
          IDataProvider& provider; // 依赖接口
          
      public:
          ReportGenerator(IDataProvider& prov) : provider(prov) {}
          
          void generate() {
              auto data = provider.getData();
              // 生成报告
          }
      };
      
      // 低层模块实现接口
      class DatabaseProvider : public IDataProvider { /* ... */ };
      class APIDataProvider : public IDataProvider { /* ... */ };
    • 使用智能指针管理

      cpp 复制代码
      class NetworkConnection { /* ... */ };
      
      class ChatClient {
      private:
          std::unique_ptr<NetworkConnection> connection;
          
      public:
          void connect() {
              connection = std::make_unique<NetworkConnection>();
              // ...
          }
          
          void disconnect() {
              connection.reset(); // 显式释放
          }
      };
    • 接口隔离

      cpp 复制代码
      // 细粒度接口
      class IReader {
      public:
          virtual std::string read() = 0;
      };
      
      class IWriter {
      public:
          virtual void write(const std::string&) = 0;
      };
      
      class FileHandler : public IReader, public IWriter { /* ... */ };
      
      class Logger {
      private:
          IWriter& writer; // 只需要写功能
      public:
          Logger(IWriter& w) : writer(w) {}
      };
  3. 移动语义如何影响面向对象设计?

    移动语义允许更高效的对象传递和返回,使得可以设计更复杂的对象层次结构而不用担心性能问题。

  4. 什么时候应该使用私有继承?

    当你想表达"实现为"关系而不是"是一个"关系时,或者当你需要重写虚函数但不想暴露基类接口时。

  5. 深拷贝与浅拷贝的区别?如何实现深拷贝?

    • 浅拷贝:仅复制对象的成员变量值(如指针地址),新旧对象共享同一块内存(可能导致 "double free" 或一方修改影响另一方)。

    • 深拷贝:为新对象分配独立内存,复制原对象数据内容,新旧对象互不影响。

    • 实现:手动定义拷贝构造函数和赋值运算符重载,为指针成员分配新内存并复制数据。

      cpp 复制代码
      class MyString {
      private:
          char* str;
      public:
          // 深拷贝构造
          MyString(const MyString& other) {
              str = new char[strlen(other.str) + 1];
              strcpy(str, other.str);
          }
          // 深拷贝赋值
          MyString& operator=(const MyString& other) {
              if (this != &other) { // 避免自赋值
                  delete[] str; // 释放旧内存
                  str = new char[strlen(other.str) + 1];
                  strcpy(str, other.str);
              }
              return *this;
          }
          ~MyString() { delete[] str; }
      };
  6. this 指针的特性?能否在静态成员函数中使用 this 指针?

    特性:

    1. this 是隐含在非静态成员函数中的常量指针(T* const),指向当前对象。
    2. 只能在非静态成员函数中使用,不能在全局函数、静态成员函数中使用。
    3. 不能被赋值(this = &obj 错误)。

    静态成员函数中不能使用 this 指针:因为静态成员函数属于类而非对象,没有具体的实例对象,因此不存在 this 指针。

相关推荐
沐怡旸3 小时前
【底层机制】std::shared_ptr解决的痛点?是什么?如何实现?如何正确用?
c++·面试
感哥9 小时前
C++ STL 常用算法
c++
saltymilk20 小时前
C++ 模板参数推导问题小记(模板类的模板构造函数)
c++·模板元编程
感哥20 小时前
C++ lambda 匿名函数
c++
沐怡旸1 天前
【底层机制】std::unique_ptr 解决的痛点?是什么?如何实现?怎么正确使用?
c++·面试
感哥1 天前
C++ 内存管理
c++
博笙困了1 天前
AcWing学习——双指针算法
c++·算法
感哥1 天前
C++ 指针和引用
c++
感哥2 天前
C++ 多态
c++