第九章 面向对象:把数据和操作打包成"类"

← 返回目录

本章目标:类、封装、构造函数、析构函数、继承、多态、 虚函数、移动语义(C++11 起最重要的部分之一)。 这是 C++ 最核心的知识块。

9.1 类:数据和操作打包

class Dog {
public:                  // 公开:外面能访问
    Dog(std::string name, int age)
        : name_(std::move(name)), age_(age) {}  // 构造函数
    void bark() const {
        std::println("{}: 汪汪!", name_);
    }
    int age() const { return age_; }   // 读取器 getter
private:                 // 私有:只有类内部能访问
    std::string name_;
    int age_;
};

Dog d("旺财", 3);
d.bark();

要点:

9.2 构造函数:对象出生时调用

class Point {
public:
    Point() {}                    // 默认构造
    Point(int x, int y) : x_(x), y_(y) {}  // 带参构造
    // 初始化列表 : x_(x), y_(y) —— 成员在冒号后初始化
    // (比在函数体里赋值更高效、更安全)
private:
    int x_ = 0, y_ = 0;   // 默认成员初始化器
};

三种写法等价:

Point a;                 // 默认构造(注意不是 Point a()!)
Point b{3, 4};           // 花括号,推荐
Point c = Point{3, 4};   // 显式

新手大坑:Point a(); 会被解析成"函数声明",不是创建对象! 用花括号 Point a{}; 就永远不会踩这个坑。

9.3 析构函数:对象去世时调用

class File {
public:
    ~File() {          // 析构函数:~类名
        std::println("文件已关闭");
    }
};

把释放逻辑写在析构函数里——但优先用智能指针/标准库, 让析构函数保持"默认"。

9.4 拷贝与移动:值语义的两面

C++ 传参、返回、赋值都会触发"拷贝"或"移动"。

std::string s1 = "hello";
std::string s2 = s1;          // 拷贝:s2 是独立副本
std::string s3 = std::move(s1);  // 移动:s1 的资源"搬家"到 s3
                               // s1 变成空壳(内容是合法的但未指定)

为什么需要移动? 拷贝大字符串 = 复制所有字节。移动只把"指针+长度"交接过去,O(1)。

什么时候自动移动?返回局部对象时(RVO/移动),临时对象传参时。 自己写类需要移动语义时再定义移动构造/赋值,新手先会用 std::move。

"Rule of Zero"(零规则):如果类里的成员都能自管理 (string、vector、智能指针),你什么都不用写——默认拷贝、 移动、析构全都正确。这是现代 C++ 最推荐的写法。

9.5 继承:子类"是"父类的一种

class Animal {
public:
    virtual void speak() const {   // virtual = 虚函数,可被覆盖
        std::println("动物叫");
    }
    virtual ~Animal() = default;   // 重要!虚析构
};

class Cat : public Animal {
public:
    void speak() const override {  // override = 明确覆盖父类
        std::println("喵");
    }
};

class Dog : public Animal {
public:
    void speak() const override { std::println("汪"); }
};

用法(多态的核心):

std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Cat>());
zoo.push_back(std::make_unique<Dog>());
for (const auto& a : zoo) a->speak();
// 输出:喵、汪 —— 同一个接口,不同行为!

三个关键点:

  1. virtual:父类方法想被覆盖就标 virtual
  2. override:子类覆盖时写 override,拼错了编译器会报错
  3. 虚析构:父类有虚函数就必须有 virtual ~Animal()

——否则 delete 父类指针时不会调用子类析构,泄漏/崩溃

9.6 纯虚函数与抽象类:只定接口不给实现

class Shape {
public:
    virtual double area() const = 0;   // 纯虚函数
    virtual ~Shape() = default;
};
// Shape 是抽象类:不能创建 Shape 对象,只能被继承

class Circle : public Shape {
    double r_;
public:
    explicit Circle(double r) : r_(r) {}
    double area() const override { return 3.14159 * r_ * r_; }
};

std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(2.0));

用途:定义"接口契约",所有子类必须实现。 这就是面向对象设计的核心武器。

9.7 多态的实现原理(一句话版)

有虚函数的类,内部藏了一个"虚函数表"(vtable), 记录每个虚函数真正应该调谁的实现。 调用虚函数 = 查表跳转,比普通函数调用慢一点点(可忽略)。 "同一个指针,指向不同子类,调出不同行为"——就是多态。

9.8 组合 vs 继承:优先用组合

现代 C++ 建议:"is-a"(确实是)才用继承; "has-a"(拥有)用组合(成员对象/智能指针成员)。

class Engine {};        // 组合:
class Car {
    Engine engine_;     // Car 有一个 Engine(has-a)
};
class Dog : public Animal {};   // 继承:Dog 是一种 Animal(is-a)

继承滥用会让类层次又深又脆,能用组合就用组合。

9.9 struct vs class

struct 默认 public;class 默认 private。就这么点区别。
约定:只装数据的小东西用 struct,有行为的用 class。

struct Config {
    int width = 800;
    int height = 600;
    std::string title;
};

本章小结

练习题


  1. 写一个 BankAccount 类:私有余额、存款、取款、查询方法。
  2. 写 Shape 抽象类和 Circle/Rectangle 子类,算面积总和。
  3. 写一个类,成员包含 string 和 vector,什么都不用写,

验证它可以拷贝、可以移动、析构正确(Rule of Zero 体验)。

  1. 用 unique_ptr<Animal> 容器存猫狗,循环调用 speak。
  2. 给 9.5 的例子去掉 virtual 试试,看看会发生什么,然后解释。