菜鸟教程 -- 学的不仅是技术,更是梦想!

C++ 教程
C++ 教程 C++ 简介 C++ 环境设置 C++ 基本语法 C++ 注释 C++ 数据类型 C++ 变量类型 C++ 变量作用域 C++ 常量 C++ 修饰符类型 C++ 存储类 C++ 运算符 C++ 循环 C++ 判断 C++ 函数 C++ 数字 C++ 数组 C++ 字符串 C++ 指针 C++ 引用 C++ 日期 & 时间 C++ 基本的输入输出 C++ 结构体(struct) C++ vector 容器 C++ 数据结构

C++ 面向对象

C++ 类 & 对象 C++ 继承 C++ 重载运算符和重载函数 C++ 多态 C++ 数据抽象 C++ 数据封装 C++ 接口(抽象类)

C++ 高级教程

C++ 文件和流 C++ 异常处理 C++ 动态内存 C++ 命名空间 C++ 模板 C++ 预处理器 C++ 信号处理 C++ 多线程 C++ Web 编程

C++ 资源库

C++ STL 教程 C++ 导入标准库 C++ 标准库 C++ 有用的资源 C++ 实例 C++ 测验 C++ <iostream> C++ <fstream> C++ <sstream> C++ <iomanip> C++ <array> C++ <vector> C++ <list> C++ <forward_list> C++ <deque> C++ <stack> C++ <queue> C++ <priority_queue> C++ <set> C++ <unordered_set> C++ <map> C++ <unordered_map> C++ <bitset> C++ <algorithm> C++ <iterator> C++ <functional> C++ <numeric> C++ <complex> C++ <valarray> C++ <cmath> C++ <string> C++ <regex> C++ <ctime> C++ <chrono> C++ <thread> C++ <mutex> C++ <condition_variable> C++ <future> C++ <atomic> C++ <type_traits> C++ <typeinfo> C++ <exception> C++ <stdexcept> C++ <cstdio> C++ <cstdint> C++ <memory> C++ <new> C++ <utility> C++ <random> C++ <locale> C++ <codecvt> C++ <cassert> C++ <cwchar> C++ <climits> C++ <cfloat> C++ <cstdlib> C++ <numbers> C++ OpenCV
(追記) (追記ここまで)

C++ 多态

多态按字面的意思就是多种形态。

当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

在 C++ 中,多态(Polymorphism)是面向对象编程的重要特性之一。

C++ 多态允许使用基类指针或引用来调用子类的重写方法,从而使得同一接口可以表现不同的行为。

多态使得代码更加灵活和通用,程序可以通过基类指针或引用来操作不同类型的对象,而不需要显式区分对象类型。这样可以使代码更具扩展性,在增加新的形状类时不需要修改主程序。

以下是多态的几个关键点:

虚函数(Virtual Functions):

  • 在基类中声明一个函数为虚函数,使用关键字virtual
  • 派生类可以重写(override)这个虚函数。
  • 调用虚函数时,会根据对象的实际类型来决定调用哪个版本的函数。

动态绑定(Dynamic Binding):

  • 也称为晚期绑定(Late Binding),在运行时确定函数调用的具体实现。
  • 需要使用指向基类的指针或引用来调用虚函数,编译器在运行时根据对象的实际类型来决定调用哪个函数。

纯虚函数(Pure Virtual Functions):

  • 一个包含纯虚函数的类被称为抽象类(Abstract Class),它不能被直接实例化。
  • 纯虚函数没有函数体,声明时使用= 0
  • 它强制派生类提供具体的实现。

多态的实现机制:

  • 虚函数表(V-Table):C++运行时使用虚函数表来实现多态。每个包含虚函数的类都有一个虚函数表,表中存储了指向类中所有虚函数的指针。
  • 虚函数指针(V-Ptr):对象中包含一个指向该类虚函数表的指针。

使用多态的优势:

  • 代码复用:通过基类指针或引用,可以操作不同类型的派生类对象,实现代码的复用。
  • 扩展性:新增派生类时,不需要修改依赖于基类的代码,只需要确保新类正确重写了虚函数。
  • 解耦:多态允许程序设计更加模块化,降低类之间的耦合度。

注意事项:

  • 只有通过基类的指针或引用调用虚函数时,才会发生多态。
  • 如果直接使用派生类的对象调用函数,那么调用的是派生类中的版本,而不是基类中的版本。
  • 多态性需要运行时类型信息(RTTI),这可能会增加程序的开销。

实例 1

我们通过一个简单的实例来了解多态的应用:

实例 1

#include <iostream>
using namespace std;

// 基类 Animal
class Animal {
public:
// 虚函数 sound,为不同的动物发声提供接口
virtual void sound() const {
cout << "Animal makes a sound" << endl;
}

// 虚析构函数确保子类对象被正确析构
virtual ~Animal() {
cout << "Animal destroyed" << endl;
}
};

// 派生类 Dog,继承自 Animal
class Dog : public Animal {
public:
// 重写 sound 方法
void sound() const override {
cout << "Dog barks" << endl;
}

~Dog() {
cout << "Dog destroyed" << endl;
}
};

// 派生类 Cat,继承自 Animal
class Cat : public Animal {
public:
// 重写 sound 方法
void sound() const override {
cout << "Cat meows" << endl;
}

~Cat() {
cout << "Cat destroyed" << endl;
}
};

// 测试多态
int main() {
Animal* animalPtr; // 基类指针

// 创建 Dog 对象,并指向 Animal 指针
animalPtr = new Dog();
animalPtr->sound(); // 调用 Dog 的 sound 方法
delete animalPtr; // 释放内存,调用 Dog 和 Animal 的析构函数

// 创建 Cat 对象,并指向 Animal 指针
animalPtr = new Cat();
animalPtr->sound(); // 调用 Cat 的 sound 方法
delete animalPtr; // 释放内存,调用 Cat 和 Animal 的析构函数

return 0;
}

程序执行输出为:

Dog barks
Dog destroyed
Animal destroyed
Cat meows
Cat destroyed
Animal destroyed

代码解释

基类 Animal:

  • Animal 类定义了一个虚函数 sound(),这是一个虚函数(virtual),用于表示动物发声的行为。
  • ~Animal() 为虚析构函数,确保在释放基类指针指向的派生类对象时能够正确调用派生类的析构函数,防止内存泄漏。

派生类 DogCat:

  • DogCat 类都从 Animal 类派生,并各自实现了 sound() 方法。
  • Dogsound() 输出"Dog barks";Catsound() 输出"Cat meows"。这使得同一个方法(sound())在不同的类中表现不同的行为。

主函数 main():

  • 创建一个基类指针 animalPtr
  • 使用 new Dog() 创建 Dog 对象,将其地址赋给 animalPtr。此时,调用 animalPtr->sound() 会输出"Dog barks",因为 animalPtr 实际指向的是 Dog 对象。
  • 释放 Dog 对象时,先调用 Dog 的析构函数,再调用 Animal 的析构函数。
  • 使用 new Cat() 创建 Cat 对象并赋给 animalPtr,再调用 animalPtr->sound(),输出"Cat meows",显示多态行为。

关键概念

  • 虚函数:通过在基类中使用 virtual 关键字声明虚函数,派生类可以重写这个函数,从而使得在运行时根据对象类型调用正确的函数。

  • 动态绑定:C++ 的多态通过动态绑定实现。在运行时,基类指针 animalPtr 会根据它实际指向的对象类型(DogCat)调用对应的 sound() 方法。

  • 虚析构函数:在具有多态行为的基类中,析构函数应该声明为 virtual,以确保在删除派生类对象时调用派生类的析构函数,防止资源泄漏。


实例 2

下面的实例中,我们通过多态实现了一个通用的 Shape 基类和两个派生类 Rectangle 和 Triangle。

通过基类指针调用不同的派生类方法,展示了多态的动态绑定特性。

实例 2

#include<iostream>usingnamespacestd; // 基类 Shape,表示形状classShape{protected: intwidth, height; // 宽度和高度public: // 构造函数,带有默认参数Shape(inta = 0, intb = 0) : width(a), height(b){}// 虚函数 area,用于计算面积// 使用 virtual 关键字,实现多态virtualintarea(){cout << "Shape class area: " << endl; return0; }}; // 派生类 Rectangle,表示矩形classRectangle : publicShape{public: // 构造函数,使用基类构造函数初始化 width 和 heightRectangle(inta = 0, intb = 0) : Shape(a, b){}// 重写 area 函数,计算矩形面积intarea()override{cout << "Rectangle class area: " << endl; returnwidth * height; }}; // 派生类 Triangle,表示三角形classTriangle : publicShape{public: // 构造函数,使用基类构造函数初始化 width 和 heightTriangle(inta = 0, intb = 0) : Shape(a, b){}// 重写 area 函数,计算三角形面积intarea()override{cout << "Triangle class area: " << endl; return(width * height / 2); }}; // 主函数intmain(){Shape *shape; // 基类指针Rectanglerec(10, 7); // 矩形对象Triangletri(10, 5); // 三角形对象// 将基类指针指向矩形对象,并调用 area 函数shape = &rec; cout << "Rectangle Area: " << shape->area() << endl; // 将基类指针指向三角形对象,并调用 area 函数shape = &tri; cout << "Triangle Area: " << shape->area() << endl; return0; }

当上面的代码被编译和执行时,它会产生下列结果:

Rectangle Area: Rectangle class area: 
70
Triangle Area: Triangle class area: 
25

代码分析

Shape 类的定义:

  • Shape 是一个抽象基类,定义了一个虚函数 area()area() 是用来计算面积的虚函数,并使用了 virtual 关键字,这样在派生类中可以重写该函数,进而实现多态。
  • widthheightprotected 属性,只能在 Shape 类及其派生类中访问。
// 基类 Shape,表示形状
class Shape {
 protected:
 int width, height; // 宽度和高度
 public:
 // 构造函数,带有默认参数
 Shape(int a = 0, int b = 0) : width(a), height(b) { }
 // 虚函数 area,用于计算面积
 virtual int area() {
 cout << "Shape class area: " << endl;
 return 0;
 }
};

Rectangle 类的定义:

  • Rectangle 继承了 Shape 类,并重写了 area() 方法,计算矩形的面积。
  • area() 方法使用了 override 关键字,表示这是对基类 Shapearea() 方法的重写。
  • Rectangle::area() 返回 width * height,即矩形的面积。
// 派生类 Rectangle,表示矩形
class Rectangle : public Shape {
 public:
 // 构造函数,使用基类构造函数初始化 width 和 height
 Rectangle(int a = 0, int b = 0) : Shape(a, b) { }
 // 重写 area 函数,计算矩形面积
 int area() override { 
 cout << "Rectangle class area: " << endl;
 return width * height;
 }
};

Triangle 类的定义:

  • Triangle 类也继承自 Shape,并重写了 area() 方法,用于计算三角形的面积。
  • Triangle::area() 返回 width * height / 2,这是三角形面积的公式。
// 派生类 Triangle,表示三角形
class Triangle : public Shape {
 public:
 // 构造函数,使用基类构造函数初始化 width 和 height
 Triangle(int a = 0, int b = 0) : Shape(a, b) { }
 // 重写 area 函数,计算三角形面积
 int area() override { 
 cout << "Triangle class area: " << endl;
 return (width * height / 2); 
 }
};

主函数中的多态行为:

  • 定义了一个基类指针 shape,这个指针可以指向任何 Shape 类的对象或其派生类的对象。
  • 首先将 shape 指针指向 Rectangle 对象 rec,然后调用 shape->area()。由于 area() 是虚函数,此时会动态绑定到 Rectangle::area(),输出矩形的面积。
  • 接着,将 shape 指针指向 Triangle 对象 tri,调用 shape->area() 时会动态绑定到 Triangle::area(),输出三角形的面积。
// 主函数
int main() {
 Shape *shape; // 基类指针
 Rectangle rec(10, 7); // 矩形对象
 Triangle tri(10, 5); // 三角形对象
 // 将基类指针指向矩形对象,并调用 area 函数
 shape = &rec;
 cout << "Rectangle Area: " << shape->area() << endl;
 // 将基类指针指向三角形对象,并调用 area 函数
 shape = &tri;
 cout << "Triangle Area: " << shape->area() << endl;
 return 0;
}

关键概念

  • 虚函数:在基类 Shape 中定义了虚函数 area()。虚函数的作用是让派生类可以重写此函数,并在运行时根据指针的实际对象类型调用适当的函数实现。

  • 动态绑定:因为 area() 是虚函数,shape->area() 调用时会在运行时根据 shape 实际指向的对象类型(RectangleTriangle)来调用相应的 area() 实现。这种在运行时决定调用哪个函数的机制称为动态绑定,是多态的核心。

  • 基类指针的多态性:基类指针 shape 可以指向任何派生自 Shape 的对象。当 shape 指向不同的派生类对象时,调用 shape->area() 会产生不同的行为,这体现了多态的特性。

  • 虚函数

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

    虚函数允许子类重写它,从而在运行时通过基类指针或引用调用子类的重写版本,实现动态绑定。

    我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。

    特点:

    • 在基类中可以有实现。通常虚函数在基类中提供默认实现,但子类可以选择重写。
    • 动态绑定:在运行时根据对象的实际类型调用相应的函数版本。
    • 可选重写:派生类可以选择性地重写虚函数,但不是必须。

    实例

    #include <iostream>
    using namespace std;

    class Animal {
    public:
    virtual void sound() { // 虚函数
    cout << "Animal makes a sound" << endl;
    }
    };

    class Dog : public Animal {
    public:
    void sound() override { // 重写虚函数
    cout << "Dog barks" << endl;
    }
    };

    int main() {
    Animal *animal = new Dog();
    animal->sound(); // 输出: Dog barks
    delete animal;
    }

    以上代码中,sound 是 Animal 类的虚函数。通过 Animal* 指针 animal 调用 sound() 时,程序会根据实际对象类型(Dog)来选择调用 Dog::sound()。

    纯虚函数

    您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

    纯虚函数是没有实现的虚函数,在基类中用 = 0 来声明。

    纯虚函数表示基类定义了一个接口,但具体实现由派生类负责。

    纯虚函数使得基类变为抽象类(abstract class),无法实例化。

    特点:

    • 必须在基类中声明为 = 0,表示没有实现,子类必须重写。
    • 抽象类:包含纯虚函数的类不能直接实例化,必须通过派生类实现所有纯虚函数才能创建对象。
    • 接口定义:纯虚函数通常用于定义接口,让派生类实现具体行为。

    我们可以把基类中的虚函数 area() 改写如下:

    #include<iostream>usingnamespacestd; classShape{public: virtualintarea() = 0; // 纯虚函数,强制子类实现此方法}; classRectangle : publicShape{private: intwidth, height; public: Rectangle(intw, inth) : width(w), height(h){}intarea()override{// 实现纯虚函数returnwidth * height; }}; intmain(){Shape *shape = newRectangle(10, 5); cout << "Rectangle Area: " << shape->area() << endl; // 输出: Rectangle Area: 50deleteshape; }

    = 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数

    虚函数与纯虚函数的对比

    特性虚函数(Virtual Function)纯虚函数(Pure Virtual Function)
    定义基类中使用 virtual 声明,有实现基类中使用 = 0 声明,无实现
    子类重写子类可以选择重写子类必须实现
    抽象性可以实例化类使类变为抽象类,无法实例化
    用途提供默认行为,允许子类重写定义接口,强制子类实现具体行为
AI 思考中...

7 篇笔记 写笔记

  1. #0

    1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。

    2、虚函数声明如下:virtual ReturnType FunctionName(Parameter) 虚函数必须实现,如果不实现,编译器将报错,错误提示为:

    error LNK****: unresolved external symbol "public: virtual void __thiscall ClassName::virtualFunctionName(void)"

    3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

    4、实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。

    5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。

    6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。

    7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。

    8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。

    9年前 (2017年08月02日)
  2. #0

    ZZsprite

    125***[email protected]

    137

    C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;

    形成多态必须具备三个条件:

    1、必须存在继承关系;

    2、继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);

    3、存在基类类型的指针或者引用,通过该指针或引用调用虚函数;

    ZZsprite

    125***[email protected]

    9年前 (2018年01月04日)
  3. #0

    Coder

    cm6***[email protected]

    127

    动态联编的实现机制 VTABLE

    编译器对每个包含虚函数的类创建一个虚函数表VTABLE,表中每一项指向一个虚函数的地址,即VTABLE表可以看成一个函数指针的数组,每个虚函数的入口地址就是这个数组的一个元素。

    每个含有虚函数的类都有各自的一张虚函数表VTABLE。每个派生类的VTABLE继承了它各个基类的VTABLE,如果基类VTABLE中包含某一项(虚函数的入口地址),则其派生类的VTABLE中也将包含同样的一项,但是两项的值可能不同。如果派生类中重载了该项对应的虚函数,则派生类VTABLE的该项指向重载后的虚函数,如果派生类中没有对该项对应的虚函数进行重新定义,则使用基类的这个虚函数地址。

    在创建含有虚函数的类的对象的时候,编译器会在每个对象的内存布局中增加一个vptr指针项,该指针指向本类的VTABLE。在通过指向基类对象的指针(设为bp)调用一个虚函数时,编译器生成的代码是先获取所指对象的vtb1指针,然后调用vtb1所指向类的VTABLE中的对应项(具体虚函数的入口地址)。

    当基类中没有定义虚函数时,其长度=数据成员长度;派生类长度=自身数据成员长度+基类继承的数据成员长度;

    当基类中定义虚函数后,其长度=数据成员长度+虚函数表的地址长度;派生类长度=自身数据成员长度+基类继承的数据成员长度+虚函数表的地址长度。

    包含一个虚函数和几个虚函数的类的长度增量为0。含有虚函数的类只是增加了一个指针用于存储虚函数表的首地址。

    派生类与基类同名的虚函数在VTABLE中有相同的索引号(或序号)。

    Coder

    cm6***[email protected]

    8年前 (2018年07月19日)
  4. #0

    白菜

    109***[email protected]

    169

    虚函数这里说的有些乱,因为 C++ 写法奇葩略多。其实可以简单理解。

    虚函数可以不实现(定义)。不实现(定义)的虚函数是纯虚函数。

    在一个类中如果存在未定义的虚函数,那么不能直接使用该类的实例,可以理解因为未定义 virtual 函数,其类是抽象的,无法实例化。将报错误:

    undefined reference to `vtable for xxx'

    这和其它语言的抽象类,抽象方法是类似的——我们必须实现抽象类,否则无法实例化。(virtual 和 abstract还是有些区别的)

    也就是说,如果存在以下代码:

    using namespace std;
    class Base {
    public:
     virtual void tall();
    };
    class People : Base {
    public:
     void tall() {
     cout << "people" << endl;
     };
    };

    那么,在 main 方法中,我们不能使用 Base base; 这行代码,此时的 tall 没有实现,函数表(vtable)的引用是未定义的,故而无法执行。但我们可以使用 People people; 然后 people.tall();(&people)->tall(); 因为People实现或者说重写、覆盖了 Base 的纯虚方法 tall(),使其在 People 类中有了定义,函数表挂上去了,于是可以诞生实例了。

    int main() {
    // Base base;//不可用
     People people;//可用
     people.tall();
     (&people)->tall();
     return 0;
    }

    上述的是针对虚函数而言,普通的函数,即使我们只声明,不定义,也不会产生上述不可用的问题。

    白菜

    109***[email protected]

    8年前 (2018年09月06日)
  5. #0

    ILSYT

    175***[email protected]

    75

    父类的虚函数或纯虚函数在子类中依然是虚函数。有时我们并不希望父类的某个函数在子类中被重写,在 C++11 及以后可以用关键字 final 来避免该函数再次被重写。

    例:

    #include<iostream>
    using namespace std;
    class Base
    {
     public:
     virtual void func()
     {
     cout<<"This is Base"<<endl;
     }
    };
    class _Base:public Base
    {
     public:
     void func() final//正确,func在Base中是虚函数
     {
     cout<<"This is _Base"<<endl;
     }
    };
    class __Base:public _Base
    {
    /* public://不正确,func在_Base中已经不再是虚函数,不能再被重写
     void func()
     {
     cout<<"This is __Base"<<endl;
     }*/
    };
    int main()
    {
     _Base a;
     __Base b;
     Base* ptr=&a;
     ptr->func();
     ptr=&b;
     _Base* ptr2=&b; ptr->func();
     ptr2->func();
    }

    以上程序运行结果:

    This is _Base
    This is _Base
    This is _Base

    如果不希望一个类被继承,也可以使用 final 关键字。

    格式如下:

    class Class_name final
    {
     ...
    };

    则该类将不能被继承。

    ILSYT

    175***[email protected]

    8年前 (2019年01月31日)
  6. #0

    初次

    130***[email protected]

    40

    visual studio code1、多态的理念主要就是用到纯虚函数或者虚函数这个,这个c++的特殊处理函数利用父对象指针来访问子对象,然后根据子对象的函数来调用。

    #include <iostream>
    using namespace std;
    class parent
    {
     public:
     parent(int a =0):a(a){};
     ~parent(){};
     virtual void show_classname() = 0;
     private:
     int a;
    };
    class child1 : public parent
    {
     public:
     child1(int a= 0, int b = 0):parent(a),b(b){};
     ~child1(){};
     void show_classname(){
     cout << "class name is child1" <<endl; 
     }
     private:
     int b;
    };
    class child2 : public parent
    {
     public:
     child2(int a=0,int c =0):parent(a),c(c){};
     ~child2(){};
     void show_classname()
     {
     cout<< "class name is child2" <<endl;
     }
     private:
     int c;
    };
    int main()
    {
     child1 b;
     child2 c;
     parent *a;
     a= &b;
     cout << "a ---> b"<<endl;
     a->show_classname();
     a= &c;
     cout << "a ---> c"<<endl;
     a->show_classname();
    }

    结果:

    a ---> b
    class name is child1
    a ---> c
    class name is child2

    初次

    130***[email protected]

    7年前 (2019年09月03日)
  7. #0

    泊宇

    aiy***[email protected]

    41

    A 类继承 B 类,A 类重写了 B 类的虚函数 B2,而 B 类的 B1 中调用了 B2,则 A 类使用 B1 时,运行时 B1 中的 B2 将执行 A 类中重写的虚函数B2。

    例程如下:

    #include <iostream>
    using namespace std;
    class B
    {
    public: 
    void B1(void)
    {
     B2();
    }
    virtual void B2(void)
    {
     cout << "Hello, B2" << endl;
    }
     
    };
    class A:public B
    {
    public:
    virtual void B2(void)
    {
     cout << "Hello, A" << endl;
    }
    };
    int main()
    {
     A *pA=new A;
     pA->B1();
     cout << "Hello, world!" << endl;
     return 0;
    }

    执行结果:

    Hello, A
    Hello, world!

    泊宇

    aiy***[email protected]

    5年前 (2021年02月21日)

点我分享笔记

  • 昵称 (必填)
  • 邮箱 (必填)
  • 引用地址

AltStyle によって変換されたページ (->オリジナル) /