大话C++:第19篇 类继承下的若干关系

1 继承与构造函数

在C++中,继承与构造函数之间有一些重要的关系。当一个类继承自另一个类时,派生类的构造函数需要处理两部分的初始化:一部分是派生类自己新增加的成员变量,另一部分是继承自基类的成员变量。

基类的构造函数不会被派生类的构造函数自动调用。如果基类有构造函数,并且派生类需要使用基类构造函数来初始化从基类继承下来的成员,那么派生类必须在它的构造函数中显式地调用基类的构造函数。这通常通过成员初始化列表来完成。

#include <iostream>
#include <string>

// 基类 Person
class Person 
{
public:
    // 构造函数
    Person(const std::string& name, int age)
        : name(name), age(age) 
    {
        std::cout << "调用基类构造函数" << std::endl;
    }

    // 成员函数
    void Display() const 
    {
        std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;
    }

protected:
    // 保护成员变量
    std::string name;
    int age;
};

// 派生类 Student
class Student : public Person 
{
public:
    // 构造函数
    Student(const std::string& name, int age, int id)
        : Person(name, age)  // 调用基类的构造函数来初始化基类的成员
        , studentId(id) 
    { 
        std::cout << "调用派生类构造函数." << std::endl;
    }

    // 成员函数
    void Display() const 
    {
        // 调用基类的成员函数来显示基本信息
        Person::Display(); 
        std::cout << "学号: " << studentId << std::endl;
    }

private:
    // 私有成员变量
    int studentId;
};

int main() 
{
    // 创建 Student 对象
    Student student("Alice", 20, 12345);

    // 显示学生信息
    student.Display();

    return 0;
}

当创建 Student 类的对象时,首先会调用基类 Person的构造函数(通过派生类构造函数的成员初始化列表),然后是派生类自己的构造函数。这样,派生类可以确保基类成员被正确初始化,同时也能够初始化自己新增的成员。

如果基类有多个构造函数,派生类需要在构造函数的成员初始化列表中明确指定要调用哪一个。如果基类没有默认构造函数(即没有不带参数的构造函数),而派生类的构造函数没有使用成员初始化列表来调用基类的某个构造函数,那么编译器将会报错,因为派生类的构造函数试图调用基类的一个不存在的默认构造函数。

2 继承与析构函数

当一个类继承自另一个类时,派生类通常需要负责正确地销毁从基类继承的资源。这通常是通过在派生类中定义析构函数来实现的,这个析构函数可以调用基类的析构函数来释放基类管理的资源。

基类的析构函数通常被声明为虚函数(virtual),以确保当通过派生类对象的指针或引用来删除对象时,会调用正确的析构函数。如果基类的析构函数不是虚函数,当删除一个指向派生类对象的基类指针时,只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致资源泄漏和其他问题。

#include <iostream>
#include <string>

// 基类 Person
class Person 
{
public:
    // 构造函数
    Person(const std::string& name, int age)
        : name(name), age(age) 
    {
        std::cout << "调用基类构造函数" << std::endl;
    }

    // 虚析构函数
    virtual ~Person() 
    {
        std::cout << "调用基类构造函数" << std::endl;
    }
    
    // 成员函数
    void Display() const 
    {
        std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;
    }

protected:
    // 保护成员变量
    std::string name;
    int age;
};

// 派生类 Student
class Student : public Person 
{
public:
    // 构造函数
    Student(const std::string& name, int age, int id)
        : Person(name, age)  // 调用基类的构造函数来初始化基类的成员
        , studentId(id) 
    { 
        std::cout << "调用派生类构造函数." << std::endl;
    }

    // 析构函数
    ~Student() 
    {
        std::cout << "调用派生类析构函数 " << std::endl;
    }
    
    // 成员函数
    void Display() const 
    {
        // 调用基类的成员函数来显示基本信息
        Person::Display(); 
        std::cout << "学号: " << studentId << std::endl;
    }

private:
    // 私有成员变量
    int studentId;
};

int main() 
{
    // 创建 Student 对象
    Student student("Alice", 20, 12345);

    // 显示学生信息
    student.Display();

    return 0;
}

注意,顺序非常重要:首先调用派生类的析构函数,然后是基类的析构函数。这样可以确保派生类有机会在基类析构函数执行之前清理它自己的资源。如果基类的析构函数不是虚函数,派生类的析构函数将不会被调用,这可能导致资源泄漏。

3 继承与同名函数

在C++中,当派生类包含与基类同名的函数时,这被称为同名函数(也称为函数隐藏或名称遮蔽)。这并不意味着函数重载,因为重载是指在同一个类中有多个同名但参数列表不同的函数。在继承中,同名函数指的是派生类中的函数与基类中的函数具有相同的名称,但它们可以是不同的函数签名(参数类型和数量不同)或甚至是相同的函数签名。

当派生类中的同名函数与基类中的函数具有相同的函数签名时,派生类中的函数会隐藏(或遮蔽)基类中的同名函数。这意味着,如果通过派生类对象调用该函数,将只会调用派生类中的版本,而不会调用基类中的版本。即使基类函数是虚函数,这种情况下的调用也不会导致动态绑定(多态)。

#include <iostream>
#include <string>

// 基类 Person
class Person 
{
public:
    // 构造函数
    Person(const std::string& name, int age)
        : name(name), age(age) 
    {
        std::cout << "调用基类构造函数" << std::endl;
    }

    // 虚析构函数
    virtual ~Person() 
    {
        std::cout << "调用基类构造函数" << std::endl;
    }
    
    // 非虚成员函数
    void Display() const 
    {
        std::cout << "姓名: " << name << ", 年龄: " << age << std::endl;
    }

protected:
    // 保护成员变量
    std::string name;
    int age;
};

// 派生类 Student
class Student : public Person 
{
public:
    // 构造函数
    Student(const std::string& name, int age, int id)
        : Person(name, age)  // 调用基类的构造函数来初始化基类的成员
        , studentId(id) 
    { 
        std::cout << "调用派生类构造函数." << std::endl;
    }

    // 析构函数
    ~Student() 
    {
        std::cout << "调用派生类析构函数 " << std::endl;
    }
    
    // 成员函数
    // 同名函数Display,隐藏了基类的同名函数
    void Display() const 
    {
        // 调用基类的成员函数来显示基本信息
        // Person::Display(); 
        std::cout << "学号: " << studentId << std::endl;
    }

private:
    // 私有成员变量
    int studentId;
};

int main() 
{
    // 创建 Student 对象
    Student student("Alice", 20, 12345);
    // 显示学生信息
    student.Display();
    
    // 尝试通过 Person 类型的指针调用 Display 方法
    Person* personPtr = &student;
    personPtr->Display();     

    return 0;
}

4 继承与静态成员

在面向对象编程中,当涉及到继承时,静态成员有一些特殊的行为和规则,关键要点包括:

  • 继承与静态成员变量

    • 静态成员变量是类级别的变量,它们在内存中只有一个实例,无论创建了多少个该类的对象。

    • 当一个类继承自另一个类时,子类不会继承父类的静态成员变量。每个类都有其自己的静态成员变量。

    • 子类可以访问父类的公有(public)和保护(protected)静态成员变量,但不能直接访问私有(private)静态成员变量。

    • 静态成员变量在子类中可以通过类名或者对象名来访问,但是通常使用类名来访问静态成员变量。

  • 继承与静态成员函数

    • 静态成员函数只能访问静态成员变量和其他静态成员函数,以及非静态成员函数和非静态成员变量(通过对象参数)。

    • 静态成员函数可以被继承和覆盖(override)。如果一个子类提供了一个与父类静态成员函数同名的静态成员函数,那么子类的静态成员函数将覆盖父类的静态成员函数。

    • 覆盖的静态成员函数在子类中具有不同的实现,但它们仍然保持静态性质,即它们不能访问非静态成员。

#include <iostream>
#include <string>

class Person 
{
public:
    // 构造函数
    Person(const std::string& name) : name(name) 
    {
		// 每次创建Person对象时,增加总数
        totalPeople++; 
    }

    // 析构函数
    ~Person() 
    {
         // 每次销毁Person对象时,减少总数
        totalPeople--;
    }

    // 显示Person信息
    virtual void Display() const 
    {
        std::cout << "姓名: " << name << std::endl;
    }

    // 显示总人数
    static void DisplayTotalPeople() 
    {
        std::cout << "总人数为: " << totalPeople << std::endl;
    }

public:    
    // 静态成员变量
    static int totalPeople;    
    
protected:
    std::string name;
};

// 静态成员变量的定义和初始化
int Person::totalPeople = 0;

// Student类继承自Person类
class Student : public Person 
{
public:
    // 构造函数
    Student(const std::string& name, int id) : Person(name), studentId(id) 
    {
        // 每次创建Student对象时,增加总学生数
        totalStudents++; 
    }

    // 析构函数
    ~Student() 
    {
        // 每次销毁Student对象时,减少总学生数
        totalStudents--; 
    }

    // 显示Student信息
    void Display() const override 
    {
        std::cout << "学生姓名: " << name << ", 学号: " << studentId << std::endl;
    }

    // 显示总学生数
    static void DisplayTotalStudents() 
    {
        std::cout << "总学生数为: " << totalStudents << std::endl;
    }

private:
	// 静态成员变量,用于跟踪Student对象的总数
    static int totalStudents; 
    int studentId; // 学生ID
};

// 静态成员变量的定义和初始化
int Student::totalStudents = 0;

int main() 
{
    // 创建Person对象
    Person person1("Alice");
    Person person2("Bob");

    // 创建Student对象
    Student student1("Charlie", 1234);
    Student student2("David", 5678);

    // 显示Person和Student信息
    person1.Display();
    student1.Display();

    // 显示总人数和总学生数
    Person::DisplayTotalPeople();
    Student::DisplayTotalStudents();

    return 0;
}

注意,静态成员函数和非静态成员函数的共同点:

  • 它们都可以被继承到派生类中。

  • 如果重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。

  • 如果我们改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。


欢迎您同步关注我们的微信公众号!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值