C++继承

继承

1. 继承的基本概念

  • 继承是面向对象编程的核心特性之一,它允许一个类(子类或派生类)从另一个类(父类或基类)继承数据成员和成员函数,但不能继承父类的private私有成员、不能继承构造函数、拷贝构造函数、析构函数、=操作符重载,但可以被子类复用。
  • 通过继承,子类可以复用父类的代码,并可以在子类中扩展或重写父类的功能。

2. 继承的语法

  • C++中使用:符号来表示继承。继承时可以指定继承的访问权限(publicprotectedprivate)。

  • 语法格式:

    class BaseClass {
        // 基类内容
    };
    
    class DerivedClass : public BaseClass {
        // 派生类内容
    };
    

3. 继承的访问控制

  • Public 继承:
    • 父类的public成员在子类中仍然是publicprotected成员在子类中是protected
    • 这是最常用的继承方式。
  • Protected 继承:
    • 父类的publicprotected成员在子类中都变为protected
    • 用于需要限制子类接口但仍想允许继承的情况下。
  • Private 继承:
    • 父类的publicprotected成员在子类中都变为private
    • 使用这种继承方式,子类仅可以在内部使用父类的成员,对外界不可见。

4. 继承的类型

  • 单继承: 一个子类只能有一个直接父类。

    class Base {};
    class Derived : public Base {};
    
  • 多继承: 一个子类可以有多个父类。

    class Base1 {};
    class Base2 {};
    class Derived : public Base1, public Base2 {};
    
    • 注意: 多继承可能导致命名冲突和二义性问题,如父类中有相同的成员函数或数据成员。

5. 构造函数与析构函数

  • 构造函数:

    • 先按父类的继承顺序构造父类
    • 按类内对象的声明顺序构造其它类的对象,先声明的先构造,后声明的后构造
    • 再执行本类构造;先执行初始化列表,再执行本类构造函数内部
    class Parent1
    {
    public:
        int data1;
        Parent1(){cout << "Parent1" << endl; }
        Parent1(int data){cout << "Parent1" << endl; }
        ~Parent1(){cout << "~Parent1()" << endl; }
    };
    
    class Parent2
    {
    public:
        int data2;
        Parent2(){cout << "Parent2" << endl; }
        Parent2(int data){cout << "Parent2" << endl; }
        ~Parent2(){cout << "~Parent2()" << endl; }
    };
    
    
    class Member1
    {
    public:
        int data;
        Member1(){ cout << "Member1()" << endl; }
        Member1(int data) { cout << "Member1(int data)" << endl; this->data = data;}
        ~Member1(){ cout << "~Member1()" << endl; }
    };
    
    class Member2
    {
    public:
        int data;
        Member2(){ cout << "Member2()" << endl; }
        Member2(int data) { cout << "Member2(int data)" << endl; this->data = data;}
        ~Member2(){ cout << "~Member2()" << endl; }
    };
    
    class Student : public Parent2,public Parent1
    {
    public:
        string name;
        int age;
        float grade;
    
        Member2 m2;
        Member1 m1;     //其它类的对象作本类的数据成员
    
        Student():m1(0),m2(0),Parent1(0),Parent2(0)
        {
            cout << "Student()" << endl;
            this->name = "";
            this->age = 0;
            this->grade = 0;
        }
    
        Student(const string &name,int age,float grade,int data1,int data2):m1(data1),m2(data2),Parent1(0),Parent2(0)
        {
            cout << "Student(const string &name,int age,float grade)" << endl;
            this->name = name;
            this->age = age;
            this->grade = grade;
        }
    
        ~Student()
        {
            cout << "~Student()" << endl;
        }
    };
    
    int main()
    {
        Student stu1;
    }
    
    输出:
    Parent2
    Parent1
    Member2(int data)
    Member1(int data)
    Student()
    ~Student()
    ~Member1()
    ~Member2()
    ~Parent1()
    ~Parent2()
    
  • 析构函数:

    • 析构函数调用顺序与构造函数相反。首先调用子类的析构函数,然后调用父类的析构函数。

6. 继承的优缺点

  • 优点:
    • 代码复用: 通过继承,子类可以重用父类的代码,减少重复代码。
    • 扩展性: 继承提供了扩展类功能的途径,可以在子类中添加新的功能或修改现有功能。
  • 缺点:
    • 耦合性增加: 继承导致子类与父类之间的紧密耦合,如果父类发生变化,子类可能需要调整。
    • 复杂性增加: 尤其是在使用多继承时,代码的复杂性和维护难度会显著增加。

7. 继承与组合

  • 在设计类时,继承并不是唯一的选择。组合(Composition)也是一个重要的设计原则,它指的是在一个类中包含另一个类的对象,而不是通过继承来扩展类的功能。
  • 一般来说,如果“某个类是另一个类的一种类型”,使用继承;如果“某个类拥有另一个类的功能”,使用组合。

多继承

1. 多继承的基本语法

  • 在 C++ 中,一个派生类可以继承多个基类,基类之间使用逗号分隔。
class Base1 {
public:
    void show() {
        std::cout << "Base1 show" << std::endl;
    }
};

class Base2 {
public:
    void display() {
        std::cout << "Base2 display" << std::endl;
    }
};

class Derived : public Base1, public Base2 {
};
  • 在这个例子中,Derived 类继承了 Base1Base2,因此它可以访问这两个基类的公有成员。

2. 多继承遇见的问题

1. 菱形继承问题(钻石继承问题)

1.1 问题描述
  • 菱形继承问题发生在如下情况:一个派生类继承自两个基类,而这两个基类又继承自同一个祖先类。这样,派生类会继承祖先类的两份副本,导致数据成员和函数的二义性问题。

    class A {
    public:
        int data;
        void show() {
            std::cout << "Class A" << std::endl;
        }
    };
    
    class B : public A {
    };
    
    class C : public A {
    };
    
    class D : public B, public C {
    };
    

    在上面的例子中,D 类将继承 A 类的两个副本,一份来自 B 类,另一份来自 C 类。这就引发了问题:当访问 A 类的成员时,如 datashow(),编译器会不知道该访问哪一个。

1.2 解决方法

方法一: 虚继承

  • 通过虚继承,我们可以确保派生类只继承祖先类的一份副本。

    class A {
    public:
        int data;
        void show() {
            std::cout << "Class A" << std::endl;
        }
    };
    
    class B : virtual public A {
    };
    
    class C : virtual public A {
    };
    
    class D : public B, public C {
    };
    

    BC 虚继承 A。虚继承使得 BC 共享同一个 A 类的实例,从而在 D 中只有一个 A 类的实例。

    工作原理

    • 虚基类表: 虚继承通过一种机制实现,该机制在派生类中包含一个指向虚基类的指针表。这个表用于确保无论通过哪个派生类访问基类,都指向同一个基类实例。
    • 构造顺序: 在虚继承中,虚基类的构造函数会被最底层派生类负责调用,这也是为了确保基类的成员只被初始化一次。

方法二: 使用类域指定使用哪个类的成员

  • 如果不想使用虚继承,我们可以在访问祖先类的成员时,明确指出使用哪一个基类的成员。

    D obj;
    obj.B::data = 5;  // 使用 B 继承的 A 类的 data 成员
    obj.C::show();    // 使用 C 继承的 A 类的 show() 函数
    

    这种方式虽然解决了二义性,但代码的可读性和维护性较差,因此不推荐作为常规解决方案。

2. V形继承问题

2.1 问题描述
  • V 形继承与菱形继承类似,不同的是,它的派生类从两个基类继承,而这两个基类没有共同的祖先类。但由于这两个基类中可能存在相同的成员函数或数据成员,仍会引发类似的二义性问题。

    class A {
    public:
        void show() {
            std::cout << "Class A" << std::endl;
        }
    };
    
    class B {
    public:
        void show() {
            std::cout << "Class B" << std::endl;
        }
    };
    
    class C : public A, public B {
    };
    

    在上面的例子中,C 类从 AB 两个基类分别继承了 show() 函数。当我们在 C 类中调用 show() 时,编译器会不知该调用哪个版本。

2.2 解决方法

方法: 使用类域指定使用哪个类的成员

  • 由于 V 形继承的问题主要是函数或成员的二义性问题,可以通过类域来明确指定要调用的函数。

    C obj;
    obj.A::show();  // 调用 A 类的 show()
    obj.B::show();  // 调用 B 类的 show()
    

    这种方式可以有效解决二义性问题,但同样会增加代码的复杂度。

总结

  • 菱形继承问题:由于派生类从多个路径继承自同一个基类,导致该基类的成员出现多个拷贝。解决方法包括使用虚继承或明确指定使用哪个基类的成员。
  • V形继承问题:虽然没有共同的祖先类,但由于基类中可能存在同名成员,也会引发二义性问题。解决方法主要是通过类域明确指定使用哪个基类的成员。

虚函数

1. 虚函数的基本概念

  • **虚函数(Virtual Function)**是一个在基类中使用virtual关键字声明的成员函数。它允许子类重写该函数,以实现不同的功能。
  • 虚函数的主要目的是支持多态性,即通过基类指针或引用调用子类的函数版本。

2. 虚函数的声明

  • 在基类中,使用virtual关键字声明虚函数。

  • 子类可以重写(override)这个虚函数,而不需要再次使用virtual关键字,但最好显式使用override关键字以增加代码的可读性和安全性。

  • 虚函数在基类和派生类中的声明形式:

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

3. 虚函数的特性

  • 动态绑定(Dynamic Binding):
    • 当通过基类指针或引用调用虚函数时,函数调用是在运行时解析的(动态绑定)。这意味着程序将调用实际对象类型(即子类)的函数版本。
    • 这种机制允许实现多态行为,即同一函数名在不同上下文中表现出不同的行为。
  • 非虚函数:
    • 如果一个函数在基类中没有声明为虚函数,那么在使用基类指针或引用调用该函数时,调用的是基类的版本(静态绑定)。

4. 虚函数表(V-Table)与虚指针(V-Ptr)

  • 虚函数表(V-Table)
    - 定义: 每个包含虚函数的类都有一个虚函数表,它是一个指针数组,数组中存储的是指向该类的虚函数的地址。每个类的虚函数表是由编译器在编译时生成的。
-   **内容**: 虚函数表中的每个条目对应一个虚函数,按顺序存储该类及其父类中的虚函数的地址。子类的虚函数表会包含它自己的重写函数的地址,以及从父类继承的虚函数的地址。
  • 虚指针(V-Ptr)
  • 每个对象(包含虚函数的类的实例)都有一个隐藏的指针,称为虚指针(V-Ptr),指向该对象所属类的虚函数表。当对象被创建时,构造函数会初始化虚指针,使其指向相应的虚函数表。

  • 在调用虚函数时,程序会通过对象的虚指针找到对应的虚函数表,然后根据虚函数表中的函数指针调用实际的函数实现。

5. 纯虚函数(Pure Virtual Function)

  • 纯虚函数是没有具体实现的虚函数,用于定义接口。基类中的纯虚函数要求所有派生类必须提供自己的实现。

  • 纯虚函数的声明:

    class Base {
    public:
        virtual void show() = 0;  // 纯虚函数
    };
    
  • 抽象类:

    • 包含至少一个纯虚函数的类被称为抽象类。抽象类不能被实例化,只能作为基类供派生类继承。

6. 虚析构函数和纯虚析构函数

虚析构函数

  • 作用:

    • 如果一个类包含虚函数,且可能通过基类指针删除派生类对象,那么基类的析构函数应当声明为虚函数。

    • 这是为了确保在删除基类指针时,派生类的析构函数能够正确调用,避免资源泄漏。

class Base {
public:
    virtual ~Base() { cout << "Base destructor" << endl; }
};

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

纯虚析构函数

1.1 概念
  • 纯虚析构函数是一种特殊类型的虚析构函数,用于使类成为抽象类,并强制派生类实现自己的析构函数。
  • 它声明为虚函数并且被定义为= 0,表示该函数没有提供具体的实现,必须在派生类中提供实现。
1.2 定义
class Base {
public:
    virtual ~Base() = 0; // 纯虚析构函数
};

Base::~Base() {
    // 纯虚析构函数的实现,必须提供
    cout << "Base pure virtual destructor" << endl;
}
2. 使用纯虚析构函数
2.1 使类成为抽象类
  • 声明一个纯虚析构函数使得基类Base成为抽象类,即不能直接实例化。
  • 派生类必须实现纯虚析构函数才能实例化。
2.2 提供纯虚析构函数的实现
  • 虽然纯虚析构函数没有具体的实现,但必须提供一个实现以便在对象销毁时正确调用。
2.3 代码示例
#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() = 0; // 纯虚析构函数
};

Base::~Base() {
    cout << "Base pure virtual destructor" << endl;
}

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

int main() {
    Base* b = new Derived();
    delete b;  // 正确调用 Derived 的析构函数,然后调用 Base 的析构函数
    return 0;
}
3. 注意事项
3.1 确保析构函数实现
  • 纯虚析构函数必须提供一个实现,即使它本身是纯虚的。否则,编译器会报错。
  • 实现通常是为了确保在销毁基类部分时能够完成必要的清理操作。
3.2 派生类必须实现析构函数
  • 派生类必须实现其析构函数。否则,派生类也将成为抽象类,无法实例化。
3.3 避免资源泄漏
  • 确保在派生类中实现析构函数时,正确地调用基类的析构函数,以避免资源泄漏。
4. 总结
  • 纯虚析构函数使类成为抽象类,并强制所有派生类实现析构函数。
  • 必须实现纯虚析构函数,确保在销毁对象时能够正确调用基类的析构函数。
  • 派生类必须提供析构函数实现,确保资源能够正确释放。

7. 虚函数的性能影响

  • 虚函数在运行时的调用会有少量的性能开销,因为它涉及动态绑定和通过虚函数表查找函数地址。
  • 这种开销在多数应用场景下是可以忽略的,但在性能关键的代码中需要注意。

8. 虚函数与多态性

  • **多态性(Polymorphism)**是虚函数最重要的应用,它允许程序根据实际对象类型调用对应的函数版本,从而实现灵活的接口设计。
  • 通过多态性,基类指针可以指向不同的派生类对象,且调用相应的虚函数时会执行派生类的实现。
void display(Base* base) {
    base->show();  // 调用的是实际对象的 show() 函数
}

抽象类

1. 定义抽象类

抽象类是一个包含至少一个纯虚函数的类。它不能被直接实例化,只能作为基类存在。

  • 纯虚函数的定义方式是将函数声明后加上= 0,例如:

    class AbstractClass {
    public:
        virtual void pureVirtualFunction() = 0;  // 纯虚函数
    };
    

2. 纯虚函数

  • 声明:纯虚函数在类中声明时后面要加上= 0,这样它就成为了一个抽象类的一部分。它表示这个函数在基类中没有实现,必须由派生类提供实现。

    class AbstractClass {
    public:
        virtual void pureVirtualFunction() = 0;  // 纯虚函数
    };
    
  • 实现:派生类必须实现所有基类中的纯虚函数。否则,派生类仍然是抽象类,不能被实例化。例如:

    class ConcreteClass : public AbstractClass {
    public:
        void pureVirtualFunction() override {
            // 实现纯虚函数
        }
    };
    

3. 构造函数和析构函数

  • 构造函数:抽象类可以有构造函数。虽然抽象类不能直接实例化,但构造函数可以用来初始化某些成员变量。例如:

    class AbstractClass {
    public:
        AbstractClass() {
            // 构造函数的实现
        }
    };
    
  • 析构函数:抽象类通常会定义虚析构函数,以确保当通过基类指针删除派生类对象时,派生类的析构函数能够正确调用。否则,可能导致资源泄漏或未定义行为。例如:

    class AbstractClass {
    public:
        virtual ~AbstractClass() {
            // 虚析构函数的实现
        }
    };
    

4. 抽象类的作用

  • 定义接口:抽象类用于定义一个接口,这个接口规定了所有派生类必须实现的函数。通过抽象类,你可以强制派生类遵循某种协议,确保其具有一定的功能。
  • 多态:抽象类可以用于多态操作。你可以通过基类指针或引用来调用派生类的实现,这使得程序可以动态地决定要调用哪个派生类的方法。

5. 派生类

  • 实现要求:派生类必须实现所有基类中的纯虚函数,否则它仍然是一个抽象类,不能被实例化。例如:

    class ConcreteClass : public AbstractClass {
    public:
        void pureVirtualFunction() override {
            // 实现纯虚函数
        }
    };
    
  • 可实例化:一旦派生类实现了所有的纯虚函数,它就变成了具体类,可以被实例化。例如:

    ConcreteClass obj;  // 这是合法的
    

6. 注意事项

  • 不能实例化:抽象类本身不能被实例化。例如:

    AbstractClass obj;  // 错误:无法实例化抽象类
    
  • 多重继承:在多重继承中,如果多个基类都包含纯虚函数,派生类必须实现所有这些纯虚函数。例如:

    class Base1 {
    public:
        virtual void foo() = 0;  // 纯虚函数
    };
    
    class Base2 {
    public:
        virtual void bar() = 0;  // 纯虚函数
    };
    
    class Derived : public Base1, public Base2 {
    public:
        void foo() override {
            // 实现 Base1 的纯虚函数
        }
    
        void bar() override {
            // 实现 Base2 的纯虚函数
        }
    };
    

    这里,Derived类必须实现foobar,才能成为具体类。

静态联编与动态联编

1. 静态联编(Static Binding)

  • 概念:

    • 静态联编,也称为早期绑定(Early Binding),是在编译时确定函数调用的实现。这意味着编译器在编译阶段就已经决定了哪个函数会被调用。
    • 静态联编的函数通常是非虚函数,或者通过对象直接调用的函数。
  • 实现方式:

    • 当通过对象调用一个非虚函数时,编译器在编译时就已经知道应该调用哪个函数。这种调用不需要在运行时进行任何查找,因此执行速度更快。
  • 代码示例:

    class Base {
    public:
        void show() { cout << "Base show" << endl; } // 非虚函数
    };
    
    class Derived : public Base {
    public:
        void show() { cout << "Derived show" << endl; }
    };
    
    int main() {
        Base b;
        b.show(); // 静态联编,调用 Base::show
        return 0;
    }
    
  • 优点:

    • 性能高: 因为函数调用在编译时已经确定,省去了运行时的查找过程,因此执行速度较快。
    • 实现简单: 静态联编的实现相对简单,不需要虚函数表等机制的支持。
  • 缺点:

    • 灵活性低: 由于函数绑定在编译时已经确定,不能实现多态性。

2. 动态联编(Dynamic Binding)

  • 概念:

    • 动态联编,也称为晚期绑定(Late Binding),是在运行时根据实际对象类型决定调用哪个函数。这通常通过虚函数实现。
    • 动态联编是C++实现多态性的重要机制,允许在基类指针或引用上调用派生类的重写函数。
  • 实现方式:

    • 动态联编通常依赖于虚函数表(V-Table)和虚指针(V-Ptr)。当通过基类指针或引用调用虚函数时,程序在运行时查找虚函数表,确定实际调用的函数。
  • 代码示例:

    class Base {
    public:
        virtual void show() { cout << "Base show" << endl; } // 虚函数
    };
    
    class Derived : public Base {
    public:
        void show() override { cout << "Derived show" << endl; }
    };
    
    int main() {
        Base* b = new Derived();
        b->show(); // 动态联编,调用 Derived::show
        delete b;
        return 0;
    }
    
  • 优点:

    • 支持多态性: 通过动态联编,基类指针可以调用派生类的函数,实现多态性。
    • 灵活性高: 函数调用的实现是在运行时决定的,程序可以根据实际对象类型做出不同的响应。
  • 缺点:

    • 性能开销: 由于需要在运行时进行函数查找,动态联编比静态联编有一定的性能开销。
    • 复杂度增加: 动态联编需要虚函数表和虚指针的支持,增加了实现的复杂性。

3. 静态联编与动态联编的选择

  • 静态联编适用于不需要多态性的场景,如普通函数调用、性能要求较高的场景。
  • 动态联编则用于需要多态性的场景,尤其是在设计需要扩展性和灵活性的系统时,动态联编是必不可少的。

4. 静态联编的注意事项

  1. 虚函数与非虚函数的混用:
    • 如果基类中的函数没有声明为虚函数,但在派生类中被重写,当通过基类指针或引用调用该函数时,调用的仍然是基类的版本(静态联编)。
    • 解决方法:如果需要多态行为,确保基类的函数被声明为virtual
  2. 对象切片(Object Slicing):
    • 当基类对象被复制或赋值给派生类对象时,派生类的部分(新增的成员变量和函数)会被“切掉”,只保留基类的部分。这是因为静态联编只考虑基类的成员。
    • 解决方法:尽量避免直接使用基类对象指针或引用操作派生类对象。如果必须使用,应确保函数是虚函数,并通过基类指针或引用进行操作。
  3. 函数的隐藏(重写的错误):
    • 如果在派生类中声明了一个与基类同名但参数不同的函数,基类中的同名函数会被隐藏。
    • 解决方法:在派生类中显式使用using关键字引入基类的同名函数,或者避免同名函数。

5. 动态联编的注意事项

  1. **确保虚析构函数:
    • 如果一个类包含虚函数,而它可能会被用作基类(通过基类指针或引用删除派生类对象),那么基类的析构函数应该声明为虚函数。
    • 这样可以确保在删除基类指针时,派生类的析构函数也会被正确调用,避免资源泄漏。
  2. 虚函数的调用方式:
    • 虚函数只能通过对象的指针或引用来实现动态联编。如果通过对象直接调用虚函数,则会采用静态联编,即调用的是对象所属类的版本。
    • 解决方法:始终通过基类的指针或引用调用虚函数,以确保实现多态性。
  3. 性能开销的考虑:
    • 动态联编引入了一定的性能开销,因为每次调用虚函数时,程序需要查找虚函数表。这种开销在性能敏感的代码中可能需要考虑。
    • 解决方法:如果性能至关重要,可以在关键路径中避免使用虚函数,或者使用静态联编。
  4. 避免多重继承中的二义性:
    • 在多重继承中,不同的基类可能会有相同名称的虚函数,导致二义性问题。
    • 解决方法:明确指定要调用哪个基类的虚函数,或者使用虚继承来减少二义性。
  5. 谨慎使用纯虚函数:
    • 纯虚函数要求所有派生类必须实现它。如果派生类没有实现该函数,则派生类也将成为抽象类,无法实例化。
    • 解决方法:在设计接口时,确保确实需要所有派生类都实现该函数,否则可以提供一个默认实现。

函数重载和函数重写

主要区别

特性函数重载函数重写
定义位置同一作用域内(类内部或全局作用域)派生类中重写基类中的虚函数
函数签名函数名相同但参数列表不同函数名、参数列表和返回类型相同
编译时/运行时编译时决定函数调用(静态联编)运行时决定函数调用(动态绑定)
虚函数不需要虚函数基类中的函数需要声明为虚函数
作用提供不同的函数版本以处理不同类型的参数提供不同的实现以扩展或修改基类的行为

总结

  • 函数重载是同一作用域内对函数名的不同定义,主要用于处理不同的参数。
  • 函数重写是子类对基类虚函数的重新定义,主要用于实现多态性和扩展基类的功能。

函数重写时被屏蔽的情况

1. 子类函数名与父类相同,参数相同,但父类没有 virtual 关键字

错误现象:

  • 如果在父类中定义了一个函数,但没有使用 virtual 关键字,那么即使子类中定义了一个同名且参数列表相同的函数,这个子类函数也不会重写父类函数。相反,它会被视为一个新的、与父类无关的函数。这种情况下,父类指针或引用指向子类对象时,调用的仍然是父类版本的函数(静态联编)。

示例:

class Base {
public:
    void show() {  // 没有virtual关键字
        std::cout << "Base show" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() {  // 试图重写Base类的show函数
        std::cout << "Derived show" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->show();  // 输出: "Base show"
    delete ptr;
    return 0;
}

解释:

  • 由于 Base 类中的 show() 函数不是虚函数,编译器在编译时会静态地将 ptr->show() 绑定到 Base 类的 show() 函数。因此,即使指针实际上指向 Derived 类的对象,也不会调用 Derived 类的 show() 函数。这种情况下,无法实现多态性。

2. 子类函数名与父类相同,参数不同,此时不管父类有无 virtual 都是“屏蔽”

错误现象:

  • 如果子类中的函数名称与父类中的函数名称相同,但参数列表不同,无论父类函数是否使用 virtual 关键字,子类中的新函数都会“屏蔽”父类中的所有同名函数。换句话说,父类中的函数不会被重写或覆盖,而是被隐藏。

示例:

class Base {
public:
    virtual void show() {  // 虚函数
        std::cout << "Base show" << std::endl;
    }
};

class Derived : public Base {
public:
    void show(int x) {  // 参数不同,试图重载show函数
        std::cout << "Derived show with int " << x << std::endl;
    }
};

int main() {
    Derived obj;
    obj.show(10);  // 输出: "Derived show with int 10"

    Base* ptr = &obj;
    ptr->show();   // 输出: "Base show"
    return 0;
}

解释:

  • Derived 类中,show(int x) 函数与 Base 类中的 show() 函数具有相同的名字但不同的参数列表。这导致 Base 类的 show() 函数被隐藏,但不会被重写。Derived 类中并没有 show() 的重写版本,因此当通过基类指针调用 show() 函数时,仍然会调用 Base 类的版本。这种“屏蔽”行为使得多态性无法正常工作。

总结

  • 屏蔽错误 1: 当父类函数没有 virtual 关键字时,子类中相同签名的函数不会触发多态,而是静态绑定父类函数。
  • 屏蔽错误 2: 当子类函数与父类函数同名但参数不同,即使父类函数是虚函数,子类函数也不会重写它们,而是将其隐藏。这种情况下无法通过基类指针或引用调用子类函数。

多态

允许同一个函数在不同对象上具有不同的表现形式。

  • 多态性可以通过函数重载和函数重写、运算符重载和虚函数来实现。

  • 多态还可以使用模板技术: 函数定义时没有确定参数的类型,在调用时才确定参数的类型

1. 编译时多态性(静态多态性)

  • 函数重载(Function Overloading): 同名函数可以有不同的参数列表(类型和数量),编译器根据调用时的参数类型选择相应的函数。
  • 运算符重载(Operator Overloading): 运算符可以被重载以对用户自定义的类型执行特定操作。

示例:

class Print {
public:
    void display(int i) {
        std::cout << "Integer: " << i << std::endl;
    }

    void display(double d) {
        std::cout << "Double: " << d << std::endl;
    }
};

2. 运行时多态性(动态多态性)

  • 运行时多态性通过基类指针或引用调用派生类对象的虚函数来实现。C++ 的动态多态性通过虚函数表(vtable)和虚指针(vptr)机制实现。
2.1 虚函数(Virtual Functions)
  • 虚函数是一个在基类中使用 virtual 关键字声明的函数,它允许在派生类中重写。通过基类指针或引用调用虚函数时,将执行派生类的重写版本。
class Base {
public:
    virtual void show() {
        std::cout << "Base class" << std::endl;
    }
};

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

int main() {
    Base* ptr;
    Derived obj;
    ptr = &obj;

    // 调用的是 Derived 类的 show 函数
    ptr->show();  // Output: "Derived class"

    return 0;
}
2.2 纯虚函数与抽象类(Pure Virtual Functions and Abstract Classes)
  • 纯虚函数是一个没有实现的虚函数,必须在派生类中重写。含有纯虚函数的类称为抽象类,无法直接实例化。
class AbstractBase {
public:
    virtual void display() = 0;  // 纯虚函数
};

class ConcreteDerived : public AbstractBase {
public:
    void display() override {
        std::cout << "Concrete implementation" << std::endl;
    }
};

int main() {
    ConcreteDerived obj;
    obj.display();  // Output: "Concrete implementation"

    return 0;
}

3. 多态性与继承

  • 多态性通常与继承紧密结合,通过基类指针或引用可以调用不同派生类的重写函数,实现在运行时选择合适的函数版本。

4. 多态的优点

  • 代码复用: 通过多态性,可以使用基类指针或引用处理不同派生类的对象,减少代码重复。
  • 扩展性: 新的派生类可以添加新的功能而不需要修改基类代码。

5. 虚函数表(vtable)与性能考虑

  • 虚函数的调用涉及到 vtable 查找,可能会引入一些运行时开销,因此在性能敏感的应用中需要谨慎使用虚函数。

6. 常见问题与注意事项

  • 切片问题(Slicing Problem): 当使用基类对象赋值派生类对象时,派生类的特有数据成员可能会丢失。
  • 构造函数与析构函数的多态性: 构造函数不能是虚函数,而基类的析构函数通常应该是虚函数,以确保正确调用派生类的析构函数。
  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值