C++ :六、继承(1)

      在 C++ 编程中,继承是一个非常重要的特性,它允许我们基于现有的类创建新的类,新类可以继承现有类的属性和方法,同时还能添加自己独特的功能。

(一)继承的基本概念与意义

继承描述了类与类之间 “是一个” 的关系,它是实现代码复用和类型扩展的重要手段。

例如,我们可以将 “人(Person)” 视为一个基类,它包含了一些普遍的属性,如身份证号(idperson)、姓名(name)和年龄(age),以及可能的行为方法。而 “学生(Student)” 类则可以继承自 “人” 类,因为学生本质上也是人,同时学生还具有一些特有的属性,比如学号(num)和成绩(score)。可以理解为:学生是一个人。

class Person 
{
private:
    std::string idperson;
    std::string name;
    int age;
public:
    Person(const std::string &id, const std::string &name, int age) : idperson(id), name(name), age(age) {}
};

class Student : public Person 
{
private:
    std::string num;
    float score;
public:
    Student(const std::string &id, const std::string &name, int age, const std::string &num, float score) : 
        Person(id, name, age), num(num), score(score) {}
};

通过继承,“Student” 类无需重复编写 “Person” 类已有的属性和方法,从而减少了代码的冗余,提高了开发效率。同时,这也使得代码的结构更加清晰。在软件开发中,当我们需要处理不同类型但又有共性的对象时,继承可以帮助我们更好地组织和管理代码。

(二)继承的方式及其影响

C++ 为开发者提供了三种继承方式,每种方式都对基类成员在派生类中的访问权限有着不同的规定。

1. 公有继承(public)

在公有继承中,基类的公有成员和保护成员在派生类中保持原有的访问属性。

也就是说,基类的公有成员在派生类中仍然是公有成员,可以在类外通过派生类对象直接访问;基类的保护成员在派生类中依旧是保护成员,只能在派生类及其子类的成员函数中访问。而基类的私有成员在派生类中是不可访问的,即使是派生类的成员函数也无法直接操作它们。

class Object
 {
private:
    int oa;
protected:
    int ob;
public:
    int oc;
};

class Base : public Object 
{
    // 在Base类中,oc是public,在类外可以通过Base对象访问;
    // ob是protected,只能在Base类及其派生类的成员函数中访问;
    // oa不可访问,Base类的成员函数也无法直接操作它
};

这种继承方式常用于需要保持基类接口完整性的场景,派生类可以在不改变基类对外接口的基础上,添加自己的功能。

2. 保护继承(protected)

当使用保护继承时,基类的公有成员和保护成员在派生类中都会变为保护成员。这意味着原本在基类中可以在类外访问的公有成员,在派生类中只能在派生类及其子类的成员函数中访问了。基类的私有成员仍然不可访问。

class AnotherBase : protected Object
 {
    // 此时,Object类的oc和ob在AnotherBase类中都变为保护成员,
    // 只能在AnotherBase类及其派生类的成员函数中访问,
    // oa依旧不可访问
};

保护继承适用于希望在派生类及其子类的范围内共享基类成员,但又不想让这些成员在更广泛的外部被访问的情况。

3. 私有继承(private)

私有继承会使基类的公有成员和保护成员在派生类中都变为私有成员。这就导致在派生类外,无论是通过派生类对象还是其他方式,都无法直接访问这些成员,只有派生类的成员函数可以访问它们。基类的私有成员同样不可访问。

class PrivateBase : private Object
 {
    // Object类的oc和ob在PrivateBase类中都变为私有成员,
    // 只能在PrivateBase类的成员函数中访问,
    // oa还是不可访问
};

私有继承通常用于派生类只是内部使用基类的功能,不希望基类的接口在外部暴露的情况。

(三)派生类的编制步骤详解

1. 吸收基类的成员

派生类会自动接收基类的成员,除了构造函数和析构函数。这意味着基类中定义的成员变量和成员函数(除特殊情况外)都会成为派生类的一部分。这种吸收过程是隐式的,为派生类提供了基础的功能和属性。

2. 改造基类成员

在派生类中,可以声明与基类成员同名的新成员,从而产生同名隐藏或同名覆盖的效果。

同名隐藏是指当派生类中定义了与基类同名的成员时,基类的该成员在派生类中的访问会被屏蔽。同名覆盖则是在多态的场景下,派生类重新定义基类的虚函数,以实现不同的行为。

class BaseClass 
{
public:
    void func() 
    {
            std::cout << "BaseClass func" << std::endl;
    }
};

class DerivedClass : public BaseClass 
{
public:
    void func() 
    {
        std::cout << "DerivedClass func" << std::endl;
    }
};

在上述代码中,DerivedClass 中的 func 函数隐藏了 BaseClass 中的同名函数。当通过 DerivedClass 对象调用 func 函数时,会执行派生类中的版本。

3. 发展新成员

为了使派生类具有独特的功能,我们需要为其添加新的成员,这些新成员的名称不能与基类成员相同。新成员可以是成员变量,用于存储派生类特有的数据;也可以是成员函数,实现派生类特定的行为逻辑。

4. 重写派生类的构造函数与析构函数

由于派生类对象的创建和销毁过程与基类对象有所不同,构造函数和析构函数不能被继承。

在创建派生类对象时,首先会调用基类的构造函数来初始化从基类继承的成员,然后再调用派生类自己的构造函数来初始化派生类特有的成员。析构函数的调用顺序则相反,先调用派生类的析构函数,再调用基类的析构函数。

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

        std::cout << "Base destructor" << std::endl;
    }
};

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

当创建 Derived 对象时,会先输出 “Base constructor”,再输出 “Derived constructor”;销毁对象时,会先输出 “Derived destructor”,再输出 “Base destructor”。

(四)继承中的同名隐藏现象深入分析

同名隐藏不仅存在于成员函数中,成员属性也会出现这种情况。当派生类中定义了与基类同名的成员属性时,在派生类中访问该名称,默认会访问派生类自己的成员,而隐藏基类的同名成员。

class Object 
{
public:
    int value;
};

class Base : public Object 
{
public:
    int value;
};

在上述代码中,Base 类中的 value 成员隐藏了 Object 类中的 value 成员。如果要访问基类中的 value 成员,需要使用作用域运算符 :: 进行显式指定。

同名隐藏在成员函数中表现得更为复杂。除了前面提到的简单隐藏情况外,当基类和派生类的同名函数参数列表不同时,也会发生隐藏,而不是重载。

这是因为在 C++ 中,通过派生类对象调用函数时,只会在派生类的作用域中查找函数,如果找到了同名函数,就不会再去基类中查找,即使基类中的函数可以构成重载关系。

class BaseFunc 
{
public:
    void print(int x) 
    {
        std::cout << "BaseFunc print int: " << x << std::endl;
    }
};

class DerivedFunc : public BaseFunc 
{
public:
    void print(double x) 
    {
        std::cout << "DerivedFunc print double: " << x << std::endl;
    }
};

在这个例子中,DerivedFunc 中的 print 函数隐藏了 BaseFunc 中的 print 函数,即使它们的参数类型不同。当使用 DerivedFunc 对象调用 print 函数时,只有 DerivedFunc 中的版本会被考虑。

(五)继承关系中的访问控制和对象模型

1. 数据成员的继承与访问

无论选择哪种继承方式,基类中的所有数据成员都会被派生类继承。然而,它们在派生类中的访问权限会根据继承方式的不同而改变。

例如,在公有继承中,基类的公有数据成员在派生类中仍然是公有可访问的;在保护继承中,基类的公有和保护数据成员在派生类中都变为保护可访问的;在私有继承中,基类的公有和保护数据成员在派生类中都变为私有可访问的。

2. 保护属性的特殊作用

它在派生类内部可以当作公有属性来使用,即派生类的成员函数可以直接访问从基类继承来的保护数据成员和调用保护成员函数。

这使得在派生类及其子类的范围内,可以方便地共享和操作这些成员,同时又限制了它们在外部的访问,提供了一定的封装性和安全性。

3. 继承的传递性

继承具有传递性,也就是说,一个派生类可以作为新的基类被其他类继承。例如,ClassC 继承自 ClassB,而 ClassB 又继承自 ClassA,那么 ClassC 不仅会继承 ClassB 的成员,还会间接地继承 ClassA 的部分成员(根据继承方式和访问权限的规则)。

4. 派生类对象对基类成员的访问

派生类对象的成员方法可以访问基类对象中的保护和公有属性。这是继承机制的一个重要特性,它允许派生类在继承基类功能的基础上,对基类的部分成员进行操作和扩展。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值