MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++派生新类的步骤详解

2024-03-237.3k 阅读

C++派生新类的基本概念

在C++编程中,类的继承是一个核心特性,它允许我们基于已有的类创建新的类,这个新类被称为派生类(也叫子类),而被继承的类则称为基类(也叫父类)。派生类继承了基类的成员(包括数据成员和成员函数),并可以在此基础上添加新的成员或重写基类的成员,以满足特定的需求。这种机制极大地提高了代码的复用性和可扩展性。

定义派生类的语法结构

定义派生类的基本语法如下:

class DerivedClass : access-specifier BaseClass {
    // 新的数据成员和成员函数声明
    // 可能会重写基类的成员函数
};

在这里,DerivedClass 是派生类的名称,BaseClass 是基类的名称。access - specifier(访问说明符)决定了基类成员在派生类中的访问权限,常见的访问说明符有 publicprivateprotected

public 继承

当使用 public 继承时,基类的 public 成员在派生类中仍然是 public,基类的 protected 成员在派生类中仍然是 protected,而基类的 private 成员在派生类中是不可访问的,但仍被派生类对象包含在内存布局中。

class Base {
public:
    int publicData;
protected:
    int protectedData;
private:
    int privateData;
};

class PublicDerived : public Base {
public:
    void printPublicData() {
        std::cout << "Public data: " << publicData << std::endl;
    }
    void printProtectedData() {
        std::cout << "Protected data: " << protectedData << std::endl;
    }
    // 下面这行代码会报错,因为privateData在派生类中不可访问
    // void printPrivateData() { std::cout << "Private data: " << privateData << std::endl; }
};

在上述代码中,PublicDerived 类以 public 方式继承自 Base 类。在 PublicDerived 的成员函数中,可以访问 Base 类的 publicprotected 成员,但不能访问 private 成员。

private 继承

使用 private 继承时,基类的 publicprotected 成员在派生类中都变成 private 成员,而 private 成员仍然不可访问。这意味着派生类对象不能直接访问这些成员,只有派生类的成员函数可以访问它们。

class PrivateDerived : private Base {
public:
    void printPublicData() {
        std::cout << "Public data: " << publicData << std::endl;
    }
    void printProtectedData() {
        std::cout << "Protected data: " << protectedData << std::endl;
    }
};

int main() {
    PrivateDerived pd;
    // 下面这两行代码会报错,因为publicData和protectedData在派生类中是private的
    // pd.publicData = 10;
    // pd.printProtectedData();
    return 0;
}

在这个例子中,PrivateDerived 类以 private 方式继承自 Base 类。虽然 PrivateDerived 的成员函数可以访问 Base 类的 publicprotected 成员,但外部代码无法通过 PrivateDerived 对象访问这些成员。

protected 继承

protected 继承时,基类的 public 成员在派生类中变成 protected,基类的 protected 成员仍然是 protectedprivate 成员不可访问。这种继承方式主要用于当你希望派生类的成员可以访问基类成员,但又不想让外部代码直接访问这些成员时。

class ProtectedDerived : protected Base {
public:
    void printPublicData() {
        std::cout << "Public data: " << publicData << std::endl;
    }
    void printProtectedData() {
        std::cout << "Protected data: " << protectedData << std::endl;
    }
};

class FurtherDerived : public ProtectedDerived {
public:
    void accessBaseData() {
        printPublicData();
        printProtectedData();
    }
};

在上述代码中,ProtectedDerived 类以 protected 方式继承自 Base 类。FurtherDerived 类又以 public 方式继承自 ProtectedDerived 类,由于 ProtectedDerived 中的 publicDataprotectedData 都是 protected 的,所以 FurtherDerived 的成员函数可以访问它们。

派生类的构造函数和析构函数

派生类构造函数的执行顺序

当创建一个派生类对象时,会先调用基类的构造函数,然后再调用派生类自己的构造函数。这是因为派生类对象包含了基类对象的部分,需要先初始化基类部分。

class Base {
public:
    Base() {
        std::cout << "Base constructor called" << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        std::cout << "Derived constructor called" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called" << std::endl;
    }
};

int main() {
    Derived d;
    return 0;
}

运行上述代码,输出结果为:

Base constructor called
Derived constructor called
Derived destructor called
Base destructor called

可以看到,先调用了 Base 类的构造函数,然后调用了 Derived 类的构造函数。在对象销毁时,析构函数的调用顺序与构造函数相反,先调用派生类的析构函数,再调用基类的析构函数。

向基类构造函数传递参数

如果基类有带参数的构造函数,派生类构造函数需要显式地调用基类构造函数并传递相应的参数。

class Base {
public:
    int value;
    Base(int v) : value(v) {
        std::cout << "Base constructor with value: " << value << std::endl;
    }
    ~Base() {
        std::cout << "Base destructor called" << std::endl;
    }
};

class Derived : public Base {
public:
    Derived(int v) : Base(v) {
        std::cout << "Derived constructor called" << std::endl;
    }
    ~Derived() {
        std::cout << "Derived destructor called" << std::endl;
    }
};

int main() {
    Derived d(10);
    return 0;
}

在这个例子中,Base 类有一个带参数的构造函数。Derived 类的构造函数通过 Base(v) 语法调用了 Base 类的构造函数,并传递了参数 v

重写基类成员函数

函数重写的概念

当派生类需要提供与基类不同的行为时,可以重写基类的成员函数。重写的函数需要满足以下条件:

  1. 函数名、参数列表和返回类型必须与基类中的函数完全相同(协变返回类型除外,在C++11及以后支持)。
  2. 基类函数必须是虚函数(使用 virtual 关键字声明),否则只是隐藏基类函数而不是重写。
class Shape {
public:
    virtual void draw() {
        std::cout << "Drawing a shape" << std::endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

在上述代码中,Shape 类定义了一个虚函数 drawCircleRectangle 类继承自 Shape 类,并分别重写了 draw 函数。这里使用了 override 关键字,它不是必需的,但使用它可以让编译器检查是否真的在重写基类函数,如果不小心写错函数签名,编译器会报错。

动态绑定与多态性

通过重写基类的虚函数,结合指针或引用,可以实现动态绑定和多态性。动态绑定意味着在运行时根据对象的实际类型来决定调用哪个函数版本,而不是在编译时根据指针或引用的类型决定。

int main() {
    Shape* shapes[2];
    shapes[0] = new Circle();
    shapes[1] = new Rectangle();

    for (int i = 0; i < 2; ++i) {
        shapes[i]->draw();
    }

    for (int i = 0; i < 2; ++i) {
        delete shapes[i];
    }

    return 0;
}

在这个 main 函数中,定义了一个 Shape 指针数组,分别指向 CircleRectangle 对象。当调用 draw 函数时,实际调用的是 CircleRectangle 类中重写的 draw 函数,而不是 Shape 类中的版本,这就是动态绑定和多态性的体现。

多重继承

多重继承的概念

多重继承允许一个派生类从多个基类继承成员。语法如下:

class Derived : public Base1, public Base2 {
    // 派生类成员
};

在这里,Derived 类同时继承自 Base1Base2 两个基类。

多重继承的优缺点

优点:

  1. 功能复用:可以从多个不同的基类中复用代码,减少代码冗余。
  2. 表达复杂关系:适用于需要表示对象同时具有多种不同类型特征的场景。

缺点:

  1. 菱形继承问题:当多个基类继承自同一个基类时,会导致派生类中出现该基类成员的多份拷贝,浪费内存且可能引发歧义。
  2. 复杂性增加:代码的可读性和维护性降低,因为需要处理多个基类的成员和继承关系。

菱形继承问题及解决方案

class A {
public:
    int data;
};

class B : public A {};
class C : public A {};

class D : public B, public C {};

在上述代码中,D 类通过 BC 间接继承了 A 类,这就形成了菱形继承结构。此时 D 类对象中会包含两份 A 类的 data 成员,这不仅浪费内存,而且在访问 data 成员时会出现歧义。

解决方案是使用虚继承:

class A {
public:
    int data;
};

class B : virtual public A {};
class C : virtual public A {};

class D : public B, public C {};

通过 virtual public A 声明,BC 类以虚继承的方式继承 A 类,这样 D 类对象中只会包含一份 A 类的成员,避免了菱形继承问题。

虚基类和虚函数表

虚基类的内存布局

在虚继承的情况下,派生类对象的内存布局会有所不同。虚基类的成员不再直接包含在派生类对象中,而是通过一个指针(称为虚基类指针)来间接访问。这样可以确保无论有多少个路径继承自虚基类,虚基类的成员在派生类对象中都只有一份拷贝。

虚函数表

对于包含虚函数的类,编译器会为每个类生成一个虚函数表(vtable)。虚函数表是一个函数指针数组,存储了类中虚函数的地址。每个包含虚函数的对象都有一个指向虚函数表的指针(vptr)。当通过对象指针或引用调用虚函数时,实际上是通过 vptr 找到对应的虚函数表,再从虚函数表中获取函数地址并调用。

class Base {
public:
    virtual void func1() {
        std::cout << "Base::func1" << std::endl;
    }
    virtual void func2() {
        std::cout << "Base::func2" << std::endl;
    }
};

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

在这个例子中,Base 类有两个虚函数 func1func2,编译器会为 Base 类生成一个虚函数表,Base 对象的 vptr 指向这个虚函数表。Derived 类重写了 func1 函数,编译器会为 Derived 类生成自己的虚函数表,其中 func1 的地址是 Derived::func1 的地址,而 func2 的地址仍然是 Base::func2 的地址。

访问控制和继承的深入理解

访问控制在继承体系中的传播

不同的继承方式(publicprivateprotected)会影响基类成员在派生类中的访问权限,并且这种访问权限在多级继承中也会传播。例如,在 public 继承中,基类的 public 成员在各级派生类中仍然保持 public 访问权限,除非在中间的派生类中被重新定义。

友元关系与继承

友元关系在继承体系中不会自动继承。也就是说,如果一个类 A 是另一个类 B 的友元,那么 A 的派生类并不会自动成为 B 的友元。同样,B 的派生类也不会自动将 A 作为友元。如果需要在派生类中保持友元关系,需要在派生类中重新声明友元。

class Base {
    friend class FriendClass;
    int data;
};

class FriendClass {
public:
    void accessBase(Base& b) {
        b.data = 10; // 可以访问,因为FriendClass是Base的友元
    }
};

class Derived : public Base {};

class DerivedFriendClass {
public:
    void accessDerived(Derived& d) {
        // 下面这行代码会报错,因为DerivedFriendClass不是Derived的友元
        // d.data = 20;
    }
};

在上述代码中,FriendClassBase 的友元,可以访问 Base 的私有成员 data。但 DerivedFriendClass 不是 Derived 的友元,不能访问 DerivedBase 继承的私有成员 data

总结派生新类的步骤

  1. 确定基类:明确要继承的现有类,分析其成员结构和功能,确定哪些成员将被继承,哪些可能需要重写或扩展。
  2. 选择继承方式:根据需求选择合适的继承方式(publicprivateprotected),以控制基类成员在派生类中的访问权限。
  3. 定义派生类:按照语法规则定义派生类,在类体中声明新的数据成员和成员函数,这些新成员将扩展派生类的功能。
  4. 实现构造函数和析构函数:确保派生类构造函数正确调用基类构造函数,并传递必要的参数。析构函数的调用顺序会自动按照与构造函数相反的顺序进行。
  5. 重写基类虚函数(如果需要):如果派生类需要提供不同的行为,重写基类的虚函数。注意使用 override 关键字以确保函数签名正确。
  6. 处理多重继承(如果需要):在涉及多重继承时,要注意菱形继承问题,合理使用虚继承来避免数据成员的重复和访问歧义。

通过以上步骤,我们可以在C++中有效地派生新类,利用继承机制实现代码的复用和功能扩展,从而编写出更高效、更灵活的程序。在实际编程中,要根据具体的需求和场景,谨慎选择继承方式和设计派生类的结构,以保证代码的质量和可维护性。

希望通过本文的详细讲解和示例代码,你对C++中派生新类的步骤有了更深入的理解和掌握,能够在实际项目中灵活运用这一强大的特性。