第7章:面向对象基础

🎯 学习目标

理解面向对象编程的核心思想,掌握类的设计与实现,学会正确使用对象,建立清晰的OOP编程思维。

📖 面向对象编程思想

什么是面向对象编程?

面向对象编程(OOP)是一种程序设计方法,它将数据操作数据的方法组织在一起,形成对象。这种方式更符合我们日常思考问题的方式。

举个生活中的例子:

  • 汽车是一个对象,它有颜色、品牌等属性(数据)
  • 它可以启动、停止、加速等行为(方法)
  • 我们不需要知道发动机如何工作,只需要知道踩油门就能加速(封装)

OOP的三大核心特征

1. 封装(Encapsulation)

本质:把相关的数据和方法放在一起,对外隐藏实现细节。

为什么需要封装?

  • 保护数据不被随意修改
  • 降低使用者的学习成本
  • 便于内部实现的修改和优化

实际例子:

class BankAccount {
private:  // 私有成员,外部无法直接访问
    double balance;  // 余额
    string password; // 密码

public:   // 公有接口,外部可以使用
    void deposit(double amount);   // 存款
    bool withdraw(double amount);  // 取款
    double getBalance();           // 查询余额
};
2. 继承(Inheritance)

本质:基于已有的类创建新类,实现代码复用。

为什么需要继承?

  • 避免重复代码
  • 建立类之间的层次关系
  • 实现"是一种"的关系(如"狗是一种动物")
3. 多态(Polymorphism)

本质:相同的接口,不同的实现。

为什么需要多态?

  • 提高代码的灵活性
  • 实现统一的接口
  • 支持运行时动态绑定

🏗️ 类的定义与使用

类的基本结构

class 类名 {
private:
    // 私有成员:只能被类自己的成员函数访问
    数据类型 成员变量;
    
protected:
    // 保护成员:可以被自己和子类访问
    数据类型 保护成员;
    
public:
    // 公有成员:可以被任何人访问
    返回类型 成员函数(参数);
    
    // 构造函数:创建对象时自动调用
    类名(参数);
    
    // 析构函数:销毁对象时自动调用
    ~类名();
};

访问控制的设计智慧

访问权限矩阵:

访问修饰符 类内部 派生类 外部函数 说明
private 最严格,完全隐藏实现细节
protected 平衡封装性和继承需求
public 对外接口,需要保持稳定

设计原则:

  • 数据成员通常设为private,通过公有方法访问
  • 接口方法设为public,提供清晰的功能说明
  • 辅助方法可以设为protected,便于子类复用

⚙️ 成员函数的设计

构造函数:对象的"出生证明"

构造函数是特殊的成员函数,名称与类相同没有返回类型,在创建对象时自动调用。

构造函数的类型

1. 默认构造函数

class Point {
public:
    Point() : x(0), y(0) {}  // 默认构造函数
private:
    int x, y;
};

作用:创建对象时提供合理的初始状态。

2. 参数化构造函数

class Point {
public:
    Point(int x_val, int y_val) : x(x_val), y(y_val) {}
private:
    int x, y;
};

作用:允许创建对象时指定初始状态。

3. 构造函数重载

class Point {
public:
    Point() : x(0), y(0) {}                    // 默认构造
    Point(int x_val) : x(x_val), y(0) {}      // 单参数构造
    Point(int x_val, int y_val) : x(x_val), y(y_val) {}  // 双参数构造
};

作用:提供多种创建对象的方式。

初始化列表的妙用
class Student {
private:
    string name;
    int age;
    double gpa;

public:
    // 使用初始化列表(推荐)
    Student(const string& n, int a, double g) : name(n), age(a), gpa(g) {}
    
    // VS 构造函数体内赋值(不推荐)
    Student(const string& n, int a, double g) {
        name = n;  // 先默认构造,再赋值
        age = a;
        gpa = g;
    }
};

为什么初始化列表更好?

  • 避免重复构造和赋值
  • 对于const成员和引用成员是必须的
  • 初始化顺序与声明顺序一致,更直观

this指针:对象的"自我介绍"

this指针是编译器自动添加的隐式参数,指向调用成员函数的对象本身。

class Point {
public:
    void setX(int x) { 
        this->x = x;  // this指向当前对象
    }
private:
    int x;
};

编译器视角:

// 你写的代码
void Point::setX(int x) { this->x = x; }

// 编译器看到的代码
void Point_setX(Point* const this, int x) { this->x = x; }

常量成员函数:承诺不修改对象

class Student {
public:
    string getName() const;  // 承诺不修改对象
    void setName(string name);  // 可以修改对象
};

好处:

  • 可以被const对象调用
  • 提高代码的可读性和安全性
  • 编译器可以帮助检查错误

静态成员:属于类的"共享财产"

静态成员属于整个类,而不是某个具体的对象。

class BankAccount {
private:
    static double interest_rate;  // 所有账户共享一个利率
    static int account_count;     // 账户总数
    
public:
    static double getInterestRate() { return interest_rate; }
    static int getAccountCount() { return account_count; }
};

特点:

  • 所有对象共享同一个静态成员
  • 可以通过类名直接访问:BankAccount::getInterestRate()
  • 静态函数没有this指针,不能访问非静态成员

🧹 析构函数:对象的"善后工作"

析构函数在对象销毁时自动调用,负责清理资源。

RAII:资源管理的智慧

RAII(Resource Acquisition Is Initialization)是C++的核心思想:
资源的生命周期 = 对象的生命周期

class FileHandler {
private:
    FILE* file;
    
public:
    FileHandler(const string& filename) {  // 构造函数获取资源
        file = fopen(filename.c_str(), "r");
    }
    
    ~FileHandler() {  // 析构函数释放资源
        if (file) fclose(file);
    }
};

// 使用示例
void processFile() {
    FileHandler handler("data.txt");  // 自动打开文件
    // 使用文件...
}  // 函数结束时自动调用析构函数,关闭文件

好处:

  • 自动管理资源,避免内存泄漏
  • 异常安全,即使抛出异常也能正确清理
  • 代码简洁,不需要手动释放资源

🤝 友元机制:打破封装的"特例"

友元机制允许特定的函数或类访问私有成员。

class Point {
private:
    int x, y;
    
public:
    friend double distance(const Point& p1, const Point& p2);  // 友元函数
};

double distance(const Point& p1, const Point& p2) {
    // 友元函数可以访问私有成员
    int dx = p1.x - p2.x;
    int dy = p1.y - p2.y;
    return sqrt(dx*dx + dy*dy);
}

使用场景:

  • 运算符重载(如<<输出运算符)
  • 紧密协作的类(如容器类和迭代器类)
  • 需要访问内部状态的辅助函数

**注意:**友元破坏了封装性,应该谨慎使用。

💡 设计原则与最佳实践

1. 单一职责原则

每个类应该只有一个主要职责。

// 不好的设计:一个类承担多个职责
class StudentManager {
    void addStudent();      // 学生管理
    void calculateGrade();  // 成绩计算
    void printReport();     // 报表打印
    void saveToFile();      // 文件存储
};

// 好的设计:职责分离
class Student { /* 学生信息 */ };
class GradeCalculator { /* 成绩计算 */ };
class ReportPrinter { /* 报表打印 */ };
class FileManager { /* 文件存储 */ };

2. 接口清晰原则

class BankAccount {
public:
    // 清晰的接口:只暴露必要的方法
    void deposit(double amount);      // 存款
    bool withdraw(double amount);     // 取款
    double getBalance() const;        // 查询余额
    
private:
    // 隐藏实现细节
    void validateAmount(double amount);
    void updateTransactionLog(const string& type, double amount);
    
    double balance;
    vector<string> transactionHistory;
};

3. 构造函数应该建立类不变式

class Rectangle {
public:
    Rectangle(double width, double height) {
        if (width <= 0 || height <= 0) {
            throw invalid_argument("Width and height must be positive");
        }
        this->width = width;
        this->height = height;
    }
    
private:
    double width, height;
    // 类不变式:width > 0 && height > 0
};

🎯 实战练习

练习1:学生类设计

#include <iostream>
#include <string>
#include <stdexcept>

class Student {
private:
    std::string name;
    int age;
    double gpa;
    static int totalStudents;  // 静态成员
    
public:
    // 构造函数
    Student() : name("Unknown"), age(0), gpa(0.0) {
        totalStudents++;
    }
    
    Student(const std::string& n, int a, double g) : name(n), age(a), gpa(g) {
        validateAge(a);
        validateGpa(g);
        totalStudents++;
    }
    
    // 析构函数
    ~Student() {
        totalStudents--;
    }
    
    // 访问函数(const)
    const std::string& getName() const { return name; }
    int getAge() const { return age; }
    double getGpa() const { return gpa; }
    
    // 修改函数
    void setName(const std::string& n) { name = n; }
    void setAge(int a) {
        validateAge(a);
        age = a;
    }
    void setGpa(double g) {
        validateGpa(g);
        gpa = g;
    }
    
    // 静态函数
    static int getTotalStudents() { return totalStudents; }
    
private:
    void validateAge(int a) {
        if (a < 0 || a > 150) {
            throw std::invalid_argument("Invalid age");
        }
    }
    
    void validateGpa(double g) {
        if (g < 0.0 || g > 4.0) {
            throw std::invalid_argument("Invalid GPA");
        }
    }
};

// 静态成员初始化
int Student::totalStudents = 0;

练习2:银行账户类

class BankAccount {
private:
    std::string accountNumber;
    std::string ownerName;
    double balance;
    static double interestRate;
    static int accountCounter;
    
public:
    // 构造函数
    BankAccount(const std::string& owner, double initialBalance = 0.0) 
        : ownerName(owner), balance(initialBalance) {
        if (initialBalance < 0) {
            throw std::invalid_argument("Initial balance cannot be negative");
        }
        accountNumber = generateAccountNumber();
        accountCounter++;
    }
    
    // 存款
    void deposit(double amount) {
        validateAmount(amount);
        balance += amount;
    }
    
    // 取款
    bool withdraw(double amount) {
        validateAmount(amount);
        if (amount > balance) {
            return false;  // 余额不足
        }
        balance -= amount;
        return true;
    }
    
    // 查询信息
    const std::string& getAccountNumber() const { return accountNumber; }
    const std::string& getOwnerName() const { return ownerName; }
    double getBalance() const { return balance; }
    
    // 静态函数
    static double getInterestRate() { return interestRate; }
    static void setInterestRate(double rate) { interestRate = rate; }
    
private:
    std::string generateAccountNumber() {
        return "ACC" + std::to_string(1000000 + accountCounter);
    }
    
    void validateAmount(double amount) {
        if (amount <= 0) {
            throw std::invalid_argument("Amount must be positive");
        }
    }
};

// 静态成员初始化
double BankAccount::interestRate = 0.03;  // 3%年利率
int BankAccount::accountCounter = 0;

🎓 学习要点总结

核心概念

  1. 类是模板,对象是实例 - 理解抽象与具体的关系
  2. 封装隐藏实现,接口定义行为 - 平衡复杂性和可用性
  3. 构造函数建立状态,析构函数清理资源 - RAII的核心思想
  4. 访问控制保护数据,友元机制处理特例 - 封装与功能的平衡

设计原则

  1. 保持接口简单清晰
  2. 构造函数要建立类不变式
  3. 优先使用初始化列表
  4. 合理使用const保证安全性
  5. 谨慎使用友元,避免破坏封装

常见错误

  1. 在构造函数中抛出异常而不处理 - 可能导致资源泄漏
  2. 忘记初始化静态成员 - 会导致链接错误
  3. 滥用友元机制 - 破坏封装性
  4. 忽视const的正确使用 - 降低代码安全性

性能考虑

  • 初始化列表比构造函数体内赋值更高效
  • 内联小函数可以减少函数调用开销
  • 静态成员可以减少内存占用
  • const函数可以帮助编译器优化

理解这些概念不仅是学习C++的需要,更是培养良好编程思维的重要一步。面向对象编程让我们能够用更自然的方式思考和解决问题。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐