多态,简单地说就是一个接口,有不同的行为。

其主要分为编译时多态和运行时多态:

        编译时多态又被称为静态绑定,在程序编译时就已经确定了函数的绑定关系。其典型方式有两种:函数重载运算符重载

        运行时多态又被称为动态绑定,在程序运行时根据对象实际类型来决定调用哪个函数。

        其原理是通过重写基类的虚函数,生成虚函数表和虚函数指针,通过基类引用或者指针指向不同派生类的对象选取指定的虚函数表从而实现多态

编译时多态(静态绑定)
定义

 在编译阶段就确定了具体调用哪个函数,所以也被称为“早绑定”。

原理:

发生在编译阶段,此时编译器会根据会根据指针或者引用的声明类型,来决定调用哪个函数

实现方式:

1、函数重载:

同一作用域内,多个函数同名但是参数列表的类型、数量、顺序不同。(注意:不能根据返回值类型来区分重载)

#include <iostream>
#include <string>

class Animal {
public:
    void Run() {
        std::cout << "Animal::Run()" << std::endl;
    }

    void Eat() {
        std::cout << "Animal::Eat()" << std::endl;
    }
};

class Dog : public Animal {
public:
    void Run() {  // 隐藏了基类函数,而不是重写!
        std::cout << "Dog::Run()" << std::endl;
    }

    void Eat(std::string s) {  // 这是函数重载,不是重写
        std::cout << "Dog::Eat(" << s << ")" << std::endl;
    }
};

void test_static() {
	Dog dog;
	Animal* animal = &dog;  // 基类指针指向派生类对象

    // 静态绑定 - 基于指针的静态类型(Animal*)
	animal->Run();   // 输出: Animal::Run()
    dog.Run();    // 输出: Dog::Run()

    // animal->Eat("肉");  // 错误!Animal 中没有这个重载
    dog.Eat("肉");  // OK 输出: Dog::Eat("肉")
    // dog.Eat();   // 错误!被隐藏了,需要写成dog.Animal::Eat()
}

int main() {
	test_static();
	return 0;
}

2、运算符重载

对已有运算符重新定义,使其适用于自定义类型

// 运算符重载示例
class Point {
public:
    int x, y;
    Point operator+(const Point& other) {
        return {x + other.x, y + other.y};
    }
};

int main() {
    Point a{1, 2}, b{3, 4};
    Point c = a + b;  // 编译时确定调用 Point::operator+
}
特点:

①在编译时即可确定函数调用

②效率较高,没有运行时的额外开销

③根据指针/引用的声明类型决定

静态绑定是C++默认的行为,可以说适用于所有函数

运行时多态(动态绑定)
定义:

在程序运行时才确定具体调用哪个函数的机制,因此也称为 “晚绑定”。

原理:

在程序运行时,此时编译器会根据指针或者引用所指向对象的实际类型来决定调用哪个函数

实现方式:

通过虚函数实现。也就是基类中使用virtual关键字声明虚函数,派生类可以重写(override)基类中的虚函数。再通过基类指针或引用指向派生类对象时,调用虚函数就会触发动态绑定。

底层原理:

其核心是虚函数表(Vtable)虚表指针(vptr)机制

1、虚函数表

①每个包含虚函数(或者继承了含虚函数)的类,在编译时都会生成一个虚函数表(存储该类中所有虚函数指针的数组)。

②虚函数表中存储的是该类所有虚函数的内存地址(也就是函数指针)

如果派生类重写了基类的虚函数,派生类的虚函数表中会用自身重写的函数地址覆盖基类对应位置的地址;而没有重写的虚函数则会沿用基类的虚函数地址。

2、虚表指针

①每个包含虚函数的类的对象,会隐含一个虚表指针(vptr)通常在对象内存布局的开头)。

②该指针在对象创建时(构造函数执行阶段)被初始化,指向所属类的虚函数表。

3、执行流程

当通过基类指针或引用调用虚函数时,程序会按以下步骤执行

①通过基类指针或引用访问对象的vptr

②利用 vptr 找到对象实际类型对应的虚函数表(Vtable)

③在虚函数表中找到对应虚函数的地址;

④调用该地址指向的函数。

示例:

#include <iostream>

class Animal {
public:
	virtual void Sound() {  // 基类虚函数
		std::cout << "unknown sound" << std::endl;
	}
	virtual void move() {    
		std::cout << "moving..." << std::endl;
	}
	virtual ~Animal() {}
};

class Dog : public Animal {
public:
	void Sound() override {  // 重写基类虚函数
		std::cout << "汪汪汪!" << std::endl;
	}
	// 未重写move(),则会沿用基类版本
};

int main() {
	Animal* animal = new Dog();  // 基类指针指向派生类对象
	animal->Sound();  // 运行时调用Dog::Sound()
	animal->move();   // 运行时调用Animal::move()
	delete animal;
	return 0;
}

其内存布局:

Animal类的虚函数表:                                             Animal对象:

+----------------------------------------+                             +------------------------+  

|  [0] 地址 ---> Animal::Sound()  |                  <------- |       vptr                 |

+----------------------------------------+                             +------------------------+

|  [1] 地址 ---> Animal::move()    |                              |      其他数据         |

+----------------------------------------+                             +------------------------+

Dog类的虚函数表:                                                 dog对象:

+----------------------------------------+                             +-------------------------+

 |   [0] 地址 ---> Dog::Sound()     |                 <-------- |        vptr                 |

+----------------------------------------+                             +-------------------------+

 |   [1] 地址 ---> Animal::move()  |                              |       其他数据          |

 +----------------------------------------+                            +-------------------------+

Animal对象的虚表指针vptr指向的是Animal类的虚函数表,同理,dog对象的虚表指针vptr也是如此

当执行animal->Sound()代码时:

①从animal指针指向的对象(dog对象)中取出vptr

②通过vptr找到Dog类的Vtable

③调用Vtable中索引0的函数(Dog::Sound())

特点:

①支持 “同一接口的复用,但实现不同”,符合面向对象的 “开闭原则”

②具有一定的性能开销。其每个对象增加一个vptr(通常是8字节),而每个类都有一个Vtable(存储虚函数指针的数组)

③必须通过基类指针或引用调用虚函数才能触发动态绑定

常见陷阱及示例
陷阱一:未将基类析构函数声明为虚函数

当通过基类指针删除派生类对象时,如果基类析构函数不是虚函数,只会调用基类的析构函数,而不会调用派生类的析构函数,导致内存泄漏(派生类中动态分配的资源无法释放)。

#include <iostream>

class Animal {
public:
	virtual void Sound() {  // 基类虚函数
		std::cout << "unknown sound" << std::endl;
	}
	virtual void move() {    
		std::cout << "moving..." << std::endl;
	}
    ~Animal() {}  // 非虚析构函数
};

class Dog : public Animal {
public:
	void Sound() override {  // 重写基类虚函数
		std::cout << "汪汪汪!" << std::endl;
	}
	~Dog() {}  // 派生类析构函数
};

int main() {
	Animal* animal = new Dog();  // 基类指针指向派生类对象
	animal->Sound();  // 运行时调用Dog::Sound()
	animal->move();   // 运行时调用Animal::move()
    // 仅仅会Animal::~Animal
	delete animal;
	return 0;
}

解决方法:

必须将基类析构函数声明为虚函数,这样派生类析构函数会自动成为虚函数,确保删除基类指针时调用正确的析构函数链

class Animal {
public:
     // 声明为虚析构函数
    virtual ~Base() {}
};

陷阱二:通过对象本身而非指针 / 引用调用虚函数

动态绑定的触发条件是通过基类指针或引用调用虚函数。如果直接用对象本身调用,会触发静态绑定(编译时根据对象的声明类型确定调用版本),导致失败。

#include <iostream>
class Animal {
public:
	virtual void move() { std::cout << "Animal::move" << std::endl; }
};

class Dog : public Animal {
public:
	void move() override { std::cout << "Dog::move" << std::endl; }
};

int main() {
	Dog dog;
	Animal animal = dog;  // 对象切片:只会复制基类部分
	animal.move();    // 调用Animal::move()(静态绑定)
	return 0;
}

原因:

animalAnimal类型的对象(即使通过派生类对象初始化),编译器会根据animal的声明类型(而非实际存储的基类部分)决定调用Animal::move()

解决方法:

始终通过基类指针或引用调用虚函数

Animal* animal = &dog;
animal->move();  
// 正确触发动态绑定,调用Dog::move()

陷阱三:虚函数重写不满足 “三同” 原则

派生类重写基类虚函数时,必须满足函数名、参数列表、返回值类型完全相同,否则会被视为 “新增函数” 而非 “重写”,导致多态失效。

例如参数列表不同:

class Base {
public:
    virtual void func(int x) { ... }  // 参数为int
};

class Derived : public Base {
public:
    void func(double x) { ... }  // 参数为double(不是重写,是新增函数)
};

此时Derived::func是独立函数,基类指针调用func(10)时仍会执行Base::func

解决方法:

使用override关键字显式标记重写函数,编译器会检查是否满足重写条件,不满足则报错:

class Derived : public Base {
public:
    void func(int x) override { ... }  // 明确重写,编译器验证“三同”
};

陷阱四:构造函数和析构函数中调用虚函数

在构造函数或析构函数中调用虚函数时,不会触发动态绑定,只会调用当前类(正在构造 / 析构的类)的虚函数版本。

原因:

构造阶段:派生类对象的构造顺序是 “先基类,后派生类”。基类构造时,派生类成员尚未初始化,此时 vptr 指向基类的虚函数表,调用虚函数只能是基类版本。

析构阶段:析构顺序是 “先派生类,后基类”。基类析构时,派生类成员已被销毁,vptr 已切换回基类虚函数表,调用虚函数只能是基类版本。

class Base {
public:
    Base() { func(); }  // 构造函数中调用虚函数
    virtual void func() { cout << "Base::func" << endl; }
    ~Base() { func(); } // 析构函数中调用虚函数
};

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

int main() {
    Derived d;  // 输出:Base::func(构造时) → Base::func(析构时)
    return 0;
}

解决办法

避免在构造函数和析构函数中调用虚函数,而如果需要的话,可以将其转为普通函数,然后再显式调用。

总结
特性 编译时多态(静态绑定) 运行时多态(动态绑定)
确定调用时机 编译阶段 运行阶段
实现方式 函数重载、运算符重载、模板 虚函数 + 继承
底层机制 编译器直接匹配函数地址 虚函数表(Vtable)+ 虚表指针(vptr)
适用场景 同一功能的不同参数 / 类型版本 同一接口的不同实现(多态行为)
效率 高(无运行时开销) 略低(额外指针寻址)
扩展性 差(新增类型需重新编译) 好(新增派生类无需修改基类代码)

(欢迎各位读者阅读、评论留言。如有错误,请各位包涵、指教,谢谢)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐