菜单

Administrator
发布于 2026-05-22 / 0 阅读
0
0

Java 开发者学 C++(七):面向对象 - 类、继承与多态

C++ 的面向对象语法和 Java 很相似,因为两者都继承自 Simula。但底层机制有很大不同——尤其是值语义 vs 引用语义、手动内存管理和编译期多态的差异。


类的基本结构

// Person.h(头文件)
#pragma once
#include <string>

class Person {
    std::string name;   // 默认为 private
    int age;

public:
    Person(const std::string& name, int age);  // 构造函数
    void sayHello() const;                      // const 成员函数
};

// Person.cpp(实现文件)
#include "Person.h"
#include <iostream>

Person::Person(const std::string& name, int age)
    : name(name), age(age) {}  // 初始化列表(重要!)

void Person::sayHello() const {
    std::cout << "Hello, I'm " << name << std::endl;
}

Java 开发者需要注意的几个关键差异:

1. public/private 是分段的

class MyClass {
    // 默认为 private
    int secret;
public:
    int visible;
    void foo();
private:
    void helper();
};

2. 初始化列表(必须掌握)

初始化列表是 C++ 构造函数中初始化成员变量的正确方式

class Example {
    const int constant;      // const 成员
    int& reference;          // 引用成员
    std::string name;        // 普通对象成员
public:
    // ✅ 初始化列表(推荐)
    Example(int c, int& r, const std::string& n)
        : constant(c), reference(r), name(n) {
        // 函数体内不需要再赋值
    }

    // ❌ 函数体内赋值(低效)
    // Example(int c, int& r, const std::string& n) {
    //     constant = c;  // 编译错误!const 成员不能赋值
    //     reference = r; // 编译错误!引用不能重新绑定
    //     name = n;      // 先默认构造 name,再赋值(多余操作)
    // }
};

必须用初始化列表的情况

  • const 成员变量
  • 引用类型成员
  • 没有默认构造函数的成员对象
  • 基类没有默认构造函数(在派生类构造函数中)

3. const 成员函数

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

class Person {
    std::string name_;
public:
    // const 成员函数——可被 const 和 non-const 对象调用
    std::string getName() const {
        return name_;
    }

    // 非 const 成员函数——只能被 non-const 对象调用
    void setName(const std::string& n) {
        name_ = n;
    }
};

void print(const Person& p) {
    std::cout << p.getName();  // ✅ const 对象可以调用 const 方法
    // p.setName("Bob");       // ❌ 编译错误!const 对象不能调用非 const 方法
}

构造函数与析构函数

特殊成员函数

每个 C++ 类都有六个特殊成员函数(C++11 起):

class Widget {
public:
    Widget();                               // 默认构造函数
    Widget(int id);                         // 有参构造
    Widget(const Widget& other);            // 拷贝构造函数
    Widget(Widget&& other) noexcept;        // 移动构造函数(C++11)
    Widget& operator=(const Widget& other); // 拷贝赋值运算符
    Widget& operator=(Widget&& other) noexcept; // 移动赋值运算符(C++11)
    ~Widget();                              // 析构函数
};

析构函数(Java 没有!)

析构函数是 RAII 的基石,在对象离开作用域时自动调用:

class ResourceHolder {
    int* data;
public:
    ResourceHolder(size_t size)
        : data(new int[size]) {
        std::cout << "分配 " << size << " 个整数" << std::endl;
    }

    ~ResourceHolder() {
        delete[] data;
        std::cout << "释放资源" << std::endl;
    }

    // 禁止拷贝(防止双重 delete)
    ResourceHolder(const ResourceHolder&) = delete;
    ResourceHolder& operator=(const ResourceHolder&) = delete;
};

void use() {
    ResourceHolder rh(1000);   // 构造时分配
    // 使用 rh ...
    if (some_condition) {
        throw std::runtime_error("出错了");
    }
}  // rh 离开作用域,析构函数自动调用!即使有异常也会调用!
特性 Java C++
对象回收 GC,不确定何时执行 离开作用域立即析构
文件/锁释放 try-finally 或 try-with-resources RAII 自动
内存泄漏风险 需手动管理(智能指针解决)
析构函数 finalize()(不推荐使用) ~T()(确定、可靠)

委托构造(Delegating Constructor)—— C++11

changkun 的《现代 C++ 教程》中介绍:C++11 引入了委托构造的概念,使得构造函数可以在同一个类中一个构造函数调用另一个构造函数,从而达到简化代码的目的。

#include <iostream>

class Person {
    std::string name_;
    int age_;
    bool has_id_;
public:
    // 基础构造函数
    Person(const std::string& name, int age, bool has_id)
        : name_(name), age_(age), has_id_(has_id) {
        std::cout << "完整构造" << std::endl;
    }

    // 委托构造:调用上面的三参数版本
    Person(const std::string& name, int age)
        : Person(name, age, false) {  // ✅ 委托给三参数构造
        std::cout << "委托构造(无 ID)" << std::endl;
    }

    // 再委托
    Person() : Person("Unknown", 0) {
        std::cout << "默认构造" << std::endl;
    }
};

继承构造(Inheriting Constructor)—— C++11

传统 C++ 中,派生类需要显式传递基类构造函数的参数。C++11 利用关键字 using 引入了继承构造函数的概念:

#include <iostream>

class Base {
public:
    int value1;
    int value2;
    Base() {
        value1 = 1;
    }
    Base(int value) : Base() {   // 委托构造
        value2 = value;
    }
};

class Subclass : public Base {
public:
    using Base::Base;  // ✅ 继承构造函数!自动继承 Base 的所有构造函数
};

int main() {
    Subclass s(3);
    std::cout << s.value1 << std::endl;  // 1(Base() 设置)
    std::cout << s.value2 << std::endl;  // 3(Base(int) 设置)
}

Rule of Three / Five / Zero

// Rule of Three:如果自定义了析构函数、拷贝构造函数、拷贝赋值运算符之一,
// 通常三者都需要自定义(C++98)
class TraditionalResource {
    int* data;
public:
    TraditionalResource(size_t size) : data(new int[size]) {}
    ~TraditionalResource() { delete[] data; }

    // 拷贝构造
    TraditionalResource(const TraditionalResource& other)
        : data(new int[*other.data]) {
        std::copy(other.data, other.data + *other.data, data);
    }

    // 拷贝赋值
    TraditionalResource& operator=(const TraditionalResource& other) {
        if (this != &other) {
            delete[] data;
            data = new int[*other.data];
            std::copy(other.data, other.data + *other.data, data);
        }
        return *this;
    }
};

// Rule of Five:C++11 添加移动构造和移动赋值
// (略,见上一章的移动语义)

// Rule of Zero:最佳实践——让智能指针管理资源,不需要自定义任何特殊成员函数 ✅
class ModernResource {
    std::unique_ptr<int[]> data;  // 自动管理资源
public:
    ModernResource(size_t size)
        : data(std::make_unique<int[]>(size)) {}
    // 不需要自定义析构、拷贝、移动——unique_ptr 自动处理!
};

💡 良好的编码习惯:始终遵循 Rule of Zero——用智能指针和 RAII 包装器管理资源,让编译器生成的特殊成员函数正确工作。只有在必须自定义资源管理时才使用 Rule of Five。


继承

基本语法——小心默认 private 继承!

class Base {
public:
    void publicMethod();
protected:
    int protectedField;
private:
    int privateField;
};

// 公有继承(对应 Java 的 extends)
class Derived : public Base {
    // 可以访问 publicMethod 和 protectedField
    // 不能访问 privateField
};

⚠️ 坏味道:忘记 public 继承关键字

class Derived : Base {  // ❌ 默认是 private 继承!
    // Base 的 public 成员在 Derived 中变成 private
};

在 C++ 中,class Derived : Base 不等于 Java 的 class Derived extends Base!必须写 : public Base

C++ 有三种继承方式:

继承方式 基类 public → 派生类 基类 protected → 派生类 基类 private → 派生类
public 继承 public protected 不可访问
protected 继承 protected protected 不可访问
private 继承 private private 不可访问

虚函数与多态

#include <iostream>
#include <memory>
#include <vector>

class Animal {
public:
    virtual void speak() const {
        std::cout << "..." << std::endl;
    }
    virtual ~Animal() = default;  // ✅ 基类必须虚析构!
};

class Dog : public Animal {
public:
    void speak() const override {  // ✅ override 关键字(C++11 推荐)
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public Animal {
public:
    void speak() const override {
        std::cout << "Meow!" << std::endl;
    }
};

int main() {
    std::vector<std::unique_ptr<Animal>> animals;
    animals.push_back(std::make_unique<Dog>());
    animals.push_back(std::make_unique<Cat>());

    for (const auto& animal : animals) {
        animal->speak();  // 多态调用:输出 Woof! 和 Meow!
    }
}
// animals 析构时,正确调用 ~Dog()、~Cat()(因为有 virtual ~Animal())
特性 Java C++
虚函数 默认所有方法都是虚的 需要 virtual 关键字
重写 @Override 注解 override 关键字(C++11)
禁止重写 final 关键字 final(C++11)
基类析构 不需要 必须有 virtual ~Base()
抽象类 abstract class 纯虚函数 virtual void f() = 0
接口 interface 关键字 纯虚基类(全是纯虚函数)

override 与 final(C++11)

changkun 教程指出:C++11 引入了 overridefinal 这两个关键字来防止意外重载而不是重写的情况。

override:显式告知编译器这是虚函数重写

struct Base {
    virtual void foo(int);
    virtual void bar() const;
};

struct SubClass : Base {
    virtual void foo(int) override;      // ✅ 合法,确实重写了 Base::foo(int)
    virtual void foo(float) override;    // ❌ 非法,基类没有 foo(float)
    // virtual void bar() override;      // ❌ 非法!忘记写 const,签名不匹配
};

final:防止类被继续继承或虚函数被继续重写

struct Base {
    virtual void foo() final;  // 禁止子类重写 foo
};

struct SubClass1 final : Base {};  // 合法(但 SubClass1 不能再被继承)
// struct SubClass2 : SubClass1 {};  // ❌ 非法,SubClass1 已 final

struct SubClass3 : Base {
    // void foo() override {}  // ❌ 非法,foo 已 final
};

💡 良好习惯永远在意图重写虚函数时使用 override 关键字。这可以捕获签名不匹配、拼写错误等 bug,是 C++11 引入的最佳实践之一。

纯虚函数与抽象类

#include <iostream>
#include <memory>
#include <vector>

class Shape {
public:
    virtual double area() const = 0;   // 纯虚函数 → 抽象类
    virtual ~Shape() = default;        // 抽象类也需要虚析构
};

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

class Rectangle : public Shape {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double area() const override {
        return width_ * height_;
    }
};

int main() {
    // Shape s;  // ❌ 编译错误!不能实例化抽象类

    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));

    for (const auto& s : shapes) {
        std::cout << "面积: " << s->area() << std::endl;
    }
}

菱形继承与虚继承

#include <iostream>

class Animal {
public:
    int weight = 0;
};

// 虚继承:确保派生类共享同一个基类子对象
class Mammal : virtual public Animal {};
class Bird : virtual public Animal {};

class Bat : public Mammal, public Bird {
public:
    void show() {
        // 如果没有虚继承,这里会有二义性:
        // weight = 10;  // error: 'weight' is ambiguous
        // 虚继承解决菱形问题:Bat 只有一个 weight
        weight = 5;  // ✅
        std::cout << "Bat weight: " << weight << std::endl;
    }
};

⚠️ 坏味道:不必要地使用多继承

多继承虽然强大,但增加了设计复杂度。实践中尽量用组合代替继承,多继承场景大部分可以用组合 + 接口(纯虚类)替代。


强类型枚举 enum class(C++11)

changkun 教程指出:传统 C++ 中枚举类型是不安全的,枚举类型会被视作整数,两种完全不同的枚举类型可以直接比较,甚至同一个命名空间中的不同枚举类型的枚举值名字不能相同。

传统 enum 的问题

// ❌ 坏味道:传统 enum
enum Color { RED, GREEN, BLUE };
enum Feeling { EXCITED, BLUE };  // ❌ 编译错误:BLUE 重复定义!

Color c = RED;
if (c == 0) {           // ❌ 隐式转换为 int,逻辑脆弱
    // ...
}
if (c == GREEN) {       // OK
}
if (c == EXCITED) {     // ❌ 不同类型的枚举也能比较!逻辑错误
}

enum class 的解决方案

// ✅ 好习惯:enum class
enum class Color : unsigned int {
    RED,
    GREEN,
    BLUE = 100
};

enum class Feeling {
    EXCITED,
    CALM
    // BLUE 不冲突!因为作用域不同
};

int main() {
    Color c = Color::RED;      // 必须用作用域限定

    // if (c == 0) {}           // ❌ 编译错误!不能隐式转换为 int
    // if (c == Feeling::EXCITED) {} // ❌ 编译错误!不同类型不能比较

    if (c == Color::RED) {}     // ✅ 正确

    // 需要整数值时,显式转换
    int val = static_cast<int>(c);
    std::cout << val << std::endl; // 输出 0

    // 相同枚举类型的值可以比较
    if (Color::BLUE == Color::BLUE) {
        std::cout << "相同" << std::endl;
    }
}

enum class 的优势

  1. 类型安全:不能隐式转换为整数
  2. 作用域隔离:枚举值在枚举类型的作用域内
  3. 可指定底层类型enum class E : char { ... }

💡 输出 enum class 的值(方便调试)

#include <iostream>
#include <type_traits>

template<typename T>
std::ostream& operator<<(
    typename std::enable_if<std::is_enum<T>::value,
        std::ostream>::type& stream, const T& e)
{
    return stream << static_cast<typename std::underlying_type<T>::type>(e);
}

// 使用
std::cout << Color::BLUE << std::endl;  // 输出 100

struct vs class

struct Point {
    int x;     // 默认 public
    int y;
};

class Point {
    int x;     // 默认 private
    int y;
};

最佳实践

  • 纯数据的 POD 类型用 struct
  • 有封装、行为、不变量保护的用 class

常见陷阱与坏味道

⚠️ 坏味道 1:忘记虚析构函数

class Base {
public:
    ~Base() { std::cout << "Base 析构" << std::endl; }  // ❌ 非虚析构
};

class Derived : public Base {
    int* data = new int[100];
public:
    ~Derived() {
        delete[] data;
        std::cout << "Derived 析构" << std::endl;
    }
};

int main() {
    Base* b = new Derived();
    delete b;  // ❌ 未定义行为!只调用了 Base 析构,Derived 的 data 泄漏!
}

✅ 解决方案:如果类有虚函数,或者作为基类使用,必须提供虚析构函数

class Base {
public:
    virtual ~Base() = default;  // ✅ 虚析构 + default 实现
};

⚠️ 坏味道 2:对象切片(Object Slicing)

class Animal {
public:
    virtual void speak() const { std::cout << "..." << std::endl; }
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "Woof!" << std::endl; }
};

void process_by_value(Animal a) {  // ❌ 按值传递!发生切片
    a.speak();  // 输出 "...", 不是 "Woof!"
}

void process_by_ref(const Animal& a) {  // ✅ 按引用传递
    a.speak();  // 正确调用派生类的版本
}

int main() {
    Dog dog;
    process_by_value(dog);  // 切片:只复制了 Animal 部分
    process_by_ref(dog);    // 多态:正确调用 Dog::speak()
}

✅ 解决方案:多态对象永远用指针或引用传递。

⚠️ 坏味道 3:在构造/析构中调用虚函数

class Base {
public:
    Base() { speak(); }  // ❌ 构造函数中调用虚函数!
    virtual void speak() { std::cout << "Base构造"; }
};

class Derived : public Base {
public:
    Derived() {}  // Base() 先执行
    void speak() override { std::cout << "Derived构造"; }
};

int main() {
    Derived d;  // 输出 "Base构造"(不是 "Derived构造"!)
}

原因:在基类构造期间,派生类部分尚未初始化,C++ 运行时将对象的动态类型视为 Base,因此 speak() 调用的是 Base::speak()。析构时同理。

⚠️ 坏味道 4:用 int 枚举而不是 enum class

// ❌ 传统 enum
enum Status { OK, ERROR, PENDING };

void handle(int s) { /* ... */ }

handle(OK);      // 隐式转换
handle(42);      // ❌ 毫无语义的调用也能编译通过!
// ✅ enum class
enum class Status { OK, ERROR, PENDING };

void handle(Status s) { /* ... */ }

handle(Status::OK);     // ✅ 类型安全
// handle(42);           // ❌ 编译错误,类型不匹配

⚠️ 坏味道 5:多继承中不用 virtual 继承(菱形继承)

未使用虚继承时,派生类会包含多个基类子对象副本,导致二义性和浪费:

class A { public: int x; };
class B : public A {};     // ❌ 非虚继承
class C : public A {};     // ❌ 非虚继承
class D : public B, public C {};

// D d;
// d.x = 10;  // ❌ 二义性:B::x 还是 C::x?

解决方案:使用虚继承(virtual public),或更好——用组合代替继承


组合优于继承

#include <iostream>
#include <memory>
#include <vector>

// ❌ 继承的误用:为了复用代码而继承
class Animal {
public:
    virtual void makeSound() const = 0;
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void makeSound() const override {
        std::cout << "Woof!" << std::endl;
    }
};

// ❌ 坏味道:"是一个"关系不成立,却使用继承
// class CoffeeMachine : public Animal { ... }; // 荒谬!

// ✅ 组合:咖啡机有一个"发声器"组件
class Speaker {
public:
    void play(const std::string& sound) const {
        std::cout << sound << std::endl;
    }
};

class CoffeeMachine {
    Speaker speaker_;  // ✅ 组合
    std::string brand_;
public:
    CoffeeMachine(const std::string& brand) : brand_(brand) {}

    void brew() {
        std::cout << brand_ << " 正在煮咖啡..." << std::endl;
        speaker_.play("Beep! 咖啡好了!");
    }
};

💡 良好习惯:优先组合,继承只在"is-a"关系明确且需要多态行为时使用。


编码习惯与坏味道总结

✅ 良好编码习惯

习惯 说明
override 关键字 重写虚函数时总加 override,防止签名不匹配
基类虚析构 任何作为基类的类都要有 virtual ~Base() = default
enum class 替代 enum 类型安全、作用域隔离
组合优于继承 只有明确的"is-a"关系才用继承
Rule of Zero 用智能指针让特殊成员函数自动正确工作
初始化列表 构造函数用初始化列表,避免默认构造 + 赋值
= default / = delete 显式声明或禁止编译器生成的成员函数
final 阻止继承/重写 明确设计意图,防止误继承/误重写
委托构造 减少重复代码

❌ 需要避免的坏味道

坏味道 问题 解决方案
忘记虚析构 派生类资源泄漏 基类加 virtual ~T() = default
对象切片 丢失派生类信息 用指针或引用传递多态对象
构造/析构中调虚函数 不会调用派生类版本,逻辑错误 不要在构造/析构中调虚函数
多继承不用 virtual 菱形继承的二义性 用虚继承,或组合替代继承
传统 enum 类型不安全、命名冲突 enum class
函数体内赋值初始化 性能损失,const/引用成员编译错误 初始化列表
继承而非组合 设计脆弱,违反 LSP 优先组合
不写 override 重写签名错误不被发现 总写 override

完整示例:综合应用

一个综合运用委托构造、继承构造、enum class、override、final、Rule of Zero 的例子:

#include <iostream>
#include <memory>
#include <vector>
#include <string>

// 强类型枚举
enum class LogLevel : unsigned int {
    DEBUG,
    INFO,
    WARNING,
    ERROR
};

// 可输出的枚举(通过重载 <<)
std::ostream& operator<<(std::ostream& os, LogLevel level) {
    static const char* names[] = {"DEBUG", "INFO", "WARNING", "ERROR"};
    return os << names[static_cast<int>(level)];
}

// 基类:日志目标(抽象类)
class LogTarget {
public:
    virtual void write(LogLevel level, const std::string& msg) = 0;
    virtual ~LogTarget() = default;  // ✅ 虚析构
};

// 派生类:控制台日志
class ConsoleTarget final : public LogTarget {  // final 阻止进一步继承
public:
    void write(LogLevel level, const std::string& msg) override {
        std::cout << "[" << level << "] " << msg << std::endl;
    }
};

// 派生类:文件日志(组合方式)
class FileTarget : public LogTarget {
    std::string filename_;
public:
    // 委托构造
    FileTarget() : FileTarget("app.log") {}

    FileTarget(const std::string& filename)
        : filename_(filename) {}

    void write(LogLevel level, const std::string& msg) override {
        // 实际项目中打开文件写入
        std::cout << "[File:" << filename_ << "][" << level << "] "
                  << msg << std::endl;
    }
};

// 日志器(组合 LogTarget,而非继承)
class Logger {
    std::vector<std::unique_ptr<LogTarget>> targets_;
    std::string name_;
public:
    Logger(const std::string& name) : name_(name) {}

    void addTarget(std::unique_ptr<LogTarget> target) {
        targets_.push_back(std::move(target));
    }

    void log(LogLevel level, const std::string& msg) {
        // 理想情况下这里用 RAII 加锁
        for (const auto& t : targets_) {
            t->write(level, name_ + ": " + msg);
        }
    }
};

int main() {
    Logger logger("MyApp");

    logger.addTarget(std::make_unique<ConsoleTarget>());
    logger.addTarget(std::make_unique<FileTarget>("myapp.log"));

    logger.log(LogLevel::INFO, "应用启动");
    logger.log(LogLevel::DEBUG, "加载配置...");
    logger.log(LogLevel::WARNING, "配置项缺失,使用默认值");
    logger.log(LogLevel::ERROR, "数据库连接失败");

    // 所有资源通过 RAII 自动释放
}

总结

C++ 的面向对象比 Java 更灵活但也更复杂。核心要点:

  1. 初始化列表是构造函数的正确写法,不是可选项
  2. 析构函数是 RAII 的基石,Java 没有等效机制
  3. overridefinal 是 C++11 引入的最实用的面向对象改进
  4. 委托构造继承构造减少构造函数的重复代码
  5. enum class 替代传统 enum 实现类型安全
  6. 基类必须有虚析构函数,否则派生类资源泄漏
  7. 避免对象切片:多态对象用指针或引用传递
  8. 优先组合,继承只在明确的多态场景使用
  9. 遵循 Rule of Zero,用智能指针自动管理资源

关于继承的对比:

场景 Java C++
继承默认 public private
多态默认 所有方法都可重写 只有 virtual 方法可重写
抽象类 abstract class 含纯虚函数的类
接口 interface 纯虚基类
多继承 接口多继承 类和接口均可多继承

下一篇我们将介绍 C++ 最强大的特性——模板(Template),和 Java 泛型的本质区别。


评论