C++中的多态
-
- [**Day7-5 多态(重要)**](#Day7-5 多态(重要))
-
- [**1. 多态的基础**](#1. 多态的基础)
- [**2. 多态的分类**](#2. 多态的分类)
- [**3. 虚函数**](#3. 虚函数)
- [**4. 虚函数原理(重要)**](#4. 虚函数原理(重要))
- [**5. 虚函数机制的激活条件(动态多态的五个条件)**](#5. 虚函数机制的激活条件(动态多态的五个条件))
- [**6. `Base& ref = derived;` 的相似写法**](#6.
Base& ref = derived;
的相似写法) -
- [**1. 语法形式相似**](#1. 语法形式相似)
- [**2. 核心区别**](#2. 核心区别)
-
- [**(1) 引用绑定的对象类型**](#(1) 引用绑定的对象类型)
- [**(2) 可访问的成员**](#(2) 可访问的成员)
- [**(3) 多态行为(虚函数)**](#(3) 多态行为(虚函数))
- [**3. 底层机制**](#3. 底层机制)
- [**4. 总结**](#4. 总结)
- **关键结论**
- **7、虚函数的限制**
- **8、虚函数的访问**
- **9、抽象类**
- **10、纯虚函数**
- **11、虚析构函数**
- **12.构造函数和析构函数中的虚函数调用**
- **13、虚表**
- **14、函数指针**
-
- **正确解析**
- **详细解释**
- [**对比正确 vs 错误写法**](#对比正确 vs 错误写法)
- [**为什么 `typedef void (*FuncPtr)(void);` 是正确的?**](#为什么
typedef void (*FuncPtr)(void);
是正确的?) - **实际应用示例**
- **总结**
Day7-5 多态(重要)
1. 多态的基础
面向对象的四大基本特征:抽象、封装、继承、多态。
多态:对于同一种指令(如警车鸣笛),针对不同对象(警察、普通人、嫌疑犯),产生不同的行为(行动、正常行走、藏起来)。
2. 多态的分类
- 静态多态 (编译时确定):
- 函数重载
- 运算符重载
- 模板
- 动态多态 (运行时确定):
- 通过虚函数实现
示例代码:
cpp
int add(int x, int y) {}
double add(double dx, double dy) {}
std::string add(std::string s1, std::string s2) {}
add(1, 2);
add("hello", "world");
3. 虚函数
概念
虚函数是成员函数 ,在前面加上 virtual
关键字,使其在派生类中可以被覆盖,实现动态绑定。
性质
- 在基类中声明为虚函数的函数,在派生类中仍然是虚函数(即使没有显式声明
virtual
)。 - 重定义(重写、覆盖) :派生类中的虚函数必须保持相同的名称、返回类型、参数列表。
- 构造函数不能是虚函数 ,但析构函数应该是虚函数 ,否则可能会导致内存泄漏。
示例代码
cpp
#include <iostream>
/*
决不能在基类的构造/析构函数里调用虚函数
因为在构造基类时,子类还没构造,无法调用子类的虚函数
在析构基类时,子类已经被析构了,无法调用子类的虚函数
*/
class Shape {
protected:
double x = 0.0, y = 0.0;
public:
// 如果这个类有被继承的可能性,必须把析构函数声明为虚的
// 否则会调用编译时的静态版本,即子类可能错误调用基类的析构函数,导致内存泄漏
virtual ~Shape() = default; // 虚析构函数,防止内存泄漏
// 普通函数,编译时确定,如果指针实际指向子类,但此时被转为了基类,也只是调用基类的函数,即静态绑定
void print() const { std::cout << "(" << x << ", " << y << ")" << std::endl; }
// virtual 关键字使得函数成为虚函数
// 如果没有实现函数体,可以加上 = 0 使得成为纯虚函数,纯虚函数会导致类成为虚类,而无法实例化,即无法调用构造函数
// 虚函数,运行时决定,如果指针实际指向子类,但此时被转为了基类,可以调用正确的版本,即子类的版本,这种特性称为多态,也叫动态绑定
virtual void v_print() const { std::cout << "virtual(" << x << ", " << y << ")" << std::endl; }
};
// 多态必须经继承实现
class Circle : public Shape {
private:
double radius = 100.0;
public:
// 覆盖了基类的同名版本
void print() const { std::cout << "(" << x << ", " << y << ") " << radius << std::endl; }
// override 是可选的,但强烈建议加上,表明自己的意图,也能在编译时发现一些错误,比如重写了一个非虚函数,或者忘记继承
// final 关键字可以禁止子类重新实现该虚函数
void v_print() const override final { std::cout << "virtual(" << x << ", " << y << ") - " << radius << std::endl; }
};
int main()
{
Circle c;
Shape* s = &c;
s->print(); // 编译时决定,静态绑定,总是调用基类,因为表面上这是个指向基类的指针
s->v_print(); // 运行时决定,动态绑定,调用子类的版本,因为实际上这个指针指向了子类
// 为了实现多态,编译器不得不安插一些代码到类里,运行时才能调用正确的虚函数,细节请阅读《深度探索C++对象模型》
sizeof(Shape); // 24字节 = 2个double + 为了实现多态的一个指针
sizeof(Circle); // 32字节 = 基类的2个double + 自己的1个double + 为了实现多态的一个指针
// RTTI 称为运行时类型识别,这是和多态一起诞生的概念
// dynamic_cast 可以尝试将基类指针转为子类,如果不是对应的子类,转换结果为空指针
// dynamic_cast 必须在存在虚函数的情况下,即多态下,如果没有虚函数,只是普通的继承,没法使用dynamic_cast,编译报错
auto pC = dynamic_cast<Circle*>(s);
if (pC)
{
pC->v_print();
}
// RTTI 的运行时信息,typeid(类名)可以生成对应的 type_info 类,name()是其中一个成员函数
// 这是非常有限的反射
// 对反射感兴趣可以去学习 Java/C# 等纯面向对象语言,其强大的运行时系统提供了完备的类型系统,包括反射
std::cout << typeid(Shape).name() << std::endl;
std::cout << typeid(Circle).name() << std::endl;
return 0;
}
4. 虚函数原理(重要)
虚函数表(vtable)和虚函数指针(vptr)
- 基类 定义虚函数后,每个对象都会有一个虚函数指针(vptr) ,指向虚函数表(vtable)。
- 派生类继承后,vptr 指向其自己的 vtable。
- 如果派生类重写了虚函数,vtable 中的对应函数入口地址会被替换为派生类的版本。
示例代码
cpp
//virtual1.cpp
#include <iostream>
using namespace std;
#if 0
class Base
{
public:
Base(double base = 0.0)
:_base(base)
{
cout << "Base(double base = 0.0)" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
virtual void print() const
{
cout << "void Base::_base = " << _base << endl;
}
private:
double _base;
};
class Derived : public Base
{
public:
Derived(double base = 0.0, double derived = 0.0)
:Base(base)
,_derived(derived)
{
cout << "Derived(double base = 0.0, double derived = 0.0)" << endl;
}
~Derived()
{
cout << "~Derived()" << endl;
}
void print() const override
{
cout << "Derived::_derived = " << _derived << endl;
}
private:
double _derived;
};
void func(Base* pbase)
{
pbase->print();
}
void test()
{
//注意:
/*构造函数不能被继承,但是虚函数是可以被继承的;构造函数发生的时机在编译的时候,而虚函数要体现多态
,发生的时机在运行的时候。如果构造函数被设置为虚函数,那么要体现出多态,就要放在虚表中,需要使用
虚函数指针找到虚表,而如果构造函数都不调用,那对象是完全没有创建出来的,对象都不完整,此时有没有
虚函数指针都不一定。*/
Base base(11.11);
Derived derived(22.22, 33.33);
cout << endl;
func(&base); //Base *pbase = &base;
func(&derived);//Base *pbase = &derived;
}
int main()
{
test();
return 0;
}
#endif // 0
当基类定义了虚函数,就会在该类创建的对象的存储布局的前面,新增一个虚函数指针,该指针指向虚函数表(简称虚表),虚表中存的是虚函数的入口地址(有多少虚函数都会入虚表)。当派生类继承基类的时候,会满足吸收的特点,那么派生类也会有该虚函数,所以派生类创建的对象的布局的前面,也会新增一个虚函数指针,该指针指向派生类自己的虚函数表(简称虚表),虚表中存的是派生类的虚函数的入口地址(有多少虚函数都会入虚表),如果此时派生类重写了从基类这里吸收过来的虚函数,那么就会用派生类自己的虚函数的入口地址覆盖从基类这里吸收过来的虚函数的入口地址。
c++
//virtual2.cpp
#include <iostream>
using namespace std;
class Base
{
public:
Base(double base = 0.0)
:_base(base)
{
cout << "Base(double base = 0.0)" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
virtual void print() const
{
cout << "void Base::_base = " << _base << endl;
}
private:
double _base;
};
class Derived : public Base
{
public:
Derived(double base = 0.0, double derived = 0.0)
:Base(base)
, _derived(derived)
{
cout << "Derived(double base = 0.0, double derived = 0.0)" << endl;
}
~Derived()
{
cout << "~Derived()" << endl;
}
void print() const override
{
cout << "Derived::_derived = " << _derived << endl;
}
private:
double _derived;
};
void func(Base* pbase)
{
pbase->print();
}
void func1(Base& ref)
{
ref.print();
}
void test_func1()
{
Base base(11.11);
Derived derived(22.22, 33.33);
cout << endl;
func1(base); //Base& ref = base;
func1(derived);//Base& ref = derived;
}
void testObjection()
{
Base base(11.11);
Derived derived(22.22, 33.33);
cout << endl;
base.print();//对象调用
derived.print();//对象调用
}
int main()
{
//test_func1();
testObjection();
return 0;
}
5. 虚函数机制的激活条件(动态多态的五个条件)
- 基类定义虚函数
- 派生类重写(重定义、覆盖)该虚函数
- 创建派生类对象
- 用基类指针(或引用)指向派生类对象
- 通过基类指针(或引用)调用该虚函数
6. Base& ref = derived;
的相似写法
在C++中,Base& ref = derived
(基类引用绑定到派生类对象)是一种 推荐 的写法,但具体是否适用取决于你的需求。
int &ref = number;
和 Base &ref = derived;
这两种引用声明方式在语法形式上是相似的 ,但在语义和底层行为上有重要区别。下面我们详细对比它们的异同:
1. 语法形式相似
两种写法都遵循 C++ 引用的基本语法:
cpp
T& ref = obj; // T 是类型,obj 是已存在的对象
int& ref = number;
:ref
是int
的引用,绑定到number
。Base& ref = derived;
:ref
是Base
的引用,绑定到derived
(派生类对象)。
2. 核心区别
(1) 引用绑定的对象类型
引用声明 | 绑定对象类型 | 是否允许 |
---|---|---|
int& ref = number; |
相同类型(int → int ) |
✅ 允许 |
Base& ref = derived; |
派生类 → 基类(Derived → Base ) |
✅ 允许(多态) |
Derived& ref = base; |
基类 → 派生类(Base → Derived ) |
❌ 错误(除非基类实际是派生类) |
关键点:
- C++ 允许 基类引用绑定到派生类对象(向上转型,Upcasting),这是多态的基础。
- 但反过来(派生类引用绑定基类对象)是禁止的,除非通过
dynamic_cast
且基类具有多态性(含虚函数)。
(2) 可访问的成员
引用类型 | 能访问的成员 |
---|---|
int& ref |
可访问 int 的所有操作(如加减乘除) |
Base& ref |
只能访问 Base 的成员,无法直接访问 Derived 的特有成员 |
示例:
cpp
class Base {
public:
void base_func() { cout << "Base\n"; }
};
class Derived : public Base {
public:
void derived_func() { cout << "Derived\n"; }
};
int main() {
Derived derived;
Base& ref = derived;
ref.base_func(); // ✅ 允许
// ref.derived_func(); // ❌ 错误:Base 没有 derived_func
}
(3) 多态行为(虚函数)
如果 Base
有虚函数,且 Derived
覆盖了它,通过 Base& ref
调用时会触发动态绑定:
cpp
class Base {
public:
virtual void print() { cout << "Base\n"; }
};
class Derived : public Base {
public:
void print() override { cout << "Derived\n"; }
};
int main() {
Derived derived;
Base& ref = derived;
ref.print(); // 输出 "Derived"(多态调用)
}
而 int&
不涉及多态,因为内置类型(如 int
)没有虚函数。
3. 底层机制
int& ref = number;
:ref
直接绑定到number
的内存地址,操作ref
等同于操作number
。
Base& ref = derived;
:ref
绑定到derived
对象中的 基类子对象部分 (即Base
的部分)。- 如果
Base
有虚函数,ref
会通过虚表(vtable)实现多态。
4. 总结
特性 | int& ref = number; |
Base& ref = derived; |
---|---|---|
绑定类型 | 相同类型(int → int ) |
派生类 → 基类(Derived → Base ) |
是否支持多态 | 否(int 无虚函数) |
是(如果 Base 有虚函数) |
可访问成员 | 全部 int 操作 |
仅 Base 的成员 |
底层行为 | 直接别名 | 绑定到派生类对象的基类部分 |
关键结论
- 语法形式相同 ,但
Base& ref = derived;
利用了 C++ 的多态特性。 - 推荐使用基类引用/指针绑定派生类对象 ,以实现运行时多态(比直接赋值
Base base = derived;
更高效且安全)。 - 如果不需要多态,优先使用 值类型或模板,而非继承体系。
7、虚函数的限制
哪些函数不能被设置为虚函数?
1、普通函数(自由函数、全局函数):虚函数必须是成员函数,而普通函数是非成员函数。
2、内联成员函数:内联成员函数进行函数替换的时候,发生时机在编译的时候,而虚函数要体现多态,发生时机在运行的时候;如果将内联函数设置为虚函数,那此时就会失去内联的含义。
3、静态成员函数:静态成员函数发生时机在编译的时候,而虚函数要体现多态,发生时机在运行的时候;静态成员函数是共享的,被该类的所有对象共享。没有this指针
4、友元函数:如果友元函数本身是一个普通函数,那么友元函数不能被设置为虚函数。如果友元函数本身是另外一个类的成员函数,是可以被设置为虚函数的。友元关系不能被继承。
5、构造函数:构造函数不能被继承,但是虚函数是可以被继承的;构造函数发生的时机在编译的时候,而虚函数要体现动态多态,发生的时机在运行的时候;如果构造函数被设置为虚函数,那么要体现出多态,就需要放在虚表中,需要使用虚函数指针找到虚表,而如果构造函数都不调用,那对象是没有完全创建出来的,对象都不完整,此时有没有虚函数指针都不一定。(重要)
8、虚函数的访问
1、使用指针与引用可以体现出动态多态
2、使用对象不能体现动态多态,体现的是虚函数作为普通函数的特征。
3、使用其他普通成员函数有可能体现动态多态
9、抽象类
抽象类是作为接口使用的。
cpp
//abstractClass.cpp
#include <iostream>
using namespace std;
#if 0
//抽象类是作为接口使用的
//抽象类的第二种表现形式:构造函数用protected 限定
//这种情况下,抽象类也是不能创建对象的,但是该构造函数是可以被派生类访问的
class Base
{
protected:
Base()
{}
public:
~Base()
{}
//纯虚函数,在基类中只是声明该虚函数,但是不实现,将该函数的实现放在派生类中
virtual void show() const = 0;
virtual void display() const = 0;
//如果抽象类的派生类没有将所有的纯虚函数都实现,那么抽象类的派生类也是抽象类
private:
};
class Derived : public Base
{
public:
Derived(){}
~Derived() {}
void show() const override
{cout << "void Derived::show() const" << endl;}
virtual void display() const override
{cout << "void Derived::display() const" << endl;}
private:
};
void test()
{
//Base base;//不允许使用抽象类类型 "Base" 的对象:
Derived derived;
}
int main()
{
test();
return 0;
}
#endif // 0
10、纯虚函数
cpp
//pureVirtualFunction.cpp
#include <iostream>
using namespace std;
//抽象类是作为接口使用的
class Base
{
public:
Base(){}
~Base(){}
//纯虚函数,在基类中只是声明该虚函数,但是不实现,将该函数的实现放在派生类中
virtual void show() const = 0;
virtual void display() const = 0;
//如果抽象类的派生类没有将所有的纯虚函数都实现,那么抽象类的派生类也是抽象类
private:
};
class Derived : public Base
{
public:
Derived()
{}
~Derived() {}
void show() const override{cout << "void Derived::show() const" << endl;}
virtual void display() const override
{
cout << "void Derived::display() const" << endl;
}
private:
};
void test()
{
//Base base;//不允许使用抽象类类型 "Base" 的对象:
Derived derived;
derived.show();
Base* pbase = &derived;
pbase->show();
pbase->display();
//虽然Base 是抽象类,是不完整的类型,不能创建对象,但是可以创建该种类型的指针或引用
cout << endl << endl;
Derived* pderived = &derived;
pderived->show();
pderived->display();
}
int main()
{
test();
return 0;
}
11、虚析构函数
cpp
#include <iostream>
using namespace std;
class Base
{
public:
Base(const char* pBase)
:_pBase(new char[strlen(pBase) + 1]())
{
cout << "Base(const char* pBase)" << endl;
strcpy_s(_pBase, strlen(pBase) + 1, pBase);
}
virtual
~Base()
{
cout << "~Base()" << endl;
if (_pBase)
{
delete[] _pBase;
_pBase = nullptr;
}
}
virtual void print() const
{
if (_pBase)
{
cout << "Base::_pBase = " << _pBase << endl;
}
}
private:
char* _pBase;
};
class Derived : public Base
{
public:
Derived(const char* pBase, const char* pDerived)
:Base(pBase)
, _pDerived(new char[strlen(pDerived) + 1]())
{
cout << "Derived(const char* pBase, const char* pDerived)" << endl;
strcpy_s(_pDerived, strlen(pDerived) + 1, pDerived);
}
~Derived()
{
cout << "~Derived()" << endl;
if (_pDerived)
{
delete [] _pDerived;
_pDerived = nullptr;
}
}
void print() const override {
if (_pDerived)
{
cout << "void Derived::_pDerived = " << _pDerived << endl;
}
}
private:
char* _pDerived;
};
void testMemoryLeak()
{
Base* pbase = new Derived("hello", "cpp");
pbase->print();
//问题:内存泄漏
/*delete pbase;*/
delete dynamic_cast<Derived*>(pbase);
pbase = nullptr;
}
void testMemoryLeak2()
{
Base* pbase = new Derived("hello", "cpp");
pbase->print();
//给基类的析构函数前面加上virtual关键字,可以解决内存泄漏
delete pbase;
pbase = nullptr;
}
int main()
{
testMemoryLeak2();
return 0;
}
12.构造函数和析构函数中的虚函数调用
问题描述
我们有以下C++代码:
cpp
#include <iostream>
class B {
public:
B() {
test();
}
virtual void test() {
std::cout << "B\n" << std::endl;
}
private:
};
class D : public B {
public:
D() = default;
virtual void test() override {
std::cout << "D\n" << std::endl;
}
private:
};
int main() {
D d;
return 0;
}
运行这段代码时,输出的是 B
而不是 D
。这是为什么?
初步观察
首先,我们有一个基类 B
和一个派生类 D
。B
的构造函数中调用了虚函数 test()
,而 D
重写了这个虚函数。当我们创建一个 D
的对象时,B
的构造函数会被调用,然后在 B
的构造函数中调用了 test()
。然而,输出的是 B
的 test()
实现,而不是 D
的。
理解对象构造过程
在C++中,对象的构造是从基类到派生类的顺序进行的。具体到这段代码:
- 当创建
D
的对象d
时,首先调用B
的构造函数。 - 在
B
的构造函数中,调用了test()
。 - 之后,
D
的构造函数被调用(这里D
的构造函数是默认的,没有额外操作)。
虚函数在构造函数中的行为
关键在于:在构造函数中调用虚函数时,虚函数的动态绑定(多态)行为不会发生 。也就是说,在 B
的构造函数中调用 test()
时,它调用的是 B
的 test()
,而不是 D
的 test()
。
这是因为:
- 当
B
的构造函数正在执行时,D
的部分还没有被构造。此时对象的D
部分还不完整,因此不能安全地调用D
的重写函数。 - C++ 的设计决定了在基类的构造函数中,虚函数调用被解析为当前类(即基类)的实现,而不是派生类的实现。这是为了避免在对象未完全构造时调用未初始化的派生类成员。
C++标准的规定
根据C++标准(如ISO/IEC 14882:2017):
When a virtual function is called directly or indirectly from a constructor or from a destructor, including during the construction or destruction of the class's non-static data members, and the object to which the call applies is the object under construction or destruction, the function called is the one defined in the constructor or destructor's own class or in one of its bases, but not a function overriding it in a class derived from the constructor or destructor's class, or overriding it in one of the other base classes of the most derived object.
简单来说,在构造函数或析构函数中调用虚函数时,调用的版本是当前构造函数或析构函数所在类的版本,而不是派生类的重写版本。
示例验证
让我们稍微修改代码,以更清楚地看到构造顺序:
cpp
#include <iostream>
class B {
public:
B() {
std::cout << "B constructor\n";
test();
}
virtual void test() {
std::cout << "B::test\n" << std::endl;
}
};
class D : public B {
public:
D() {
std::cout << "D constructor\n";
test();
}
virtual void test() override {
std::cout << "D::test\n" << std::endl;
}
};
int main() {
D d;
return 0;
}
输出:
B constructor
B::test
D constructor
D::test
可以看到:
B
的构造函数先被调用,此时test()
调用的是B::test()
。- 然后
D
的构造函数被调用,此时test()
调用的是D::test()
。
为什么这样设计?
这种设计是为了保证对象构造的安全性。考虑以下情况:
cpp
class B {
public:
B() {
test(); // 如果这里调用 D::test(),而 D 的成员还未初始化
}
virtual void test() {}
};
class D : public B {
int x;
public:
D() : x(42) {}
virtual void test() override {
std::cout << x; // 如果 B::B() 中调用 D::test(),x 还未初始化
}
};
如果在 B
的构造函数中调用 D::test()
,而 D
的成员 x
还未初始化(因为 D
的构造函数还未执行),那么访问 x
将是未定义行为。因此,C++ 规定在构造函数中虚函数调用不进行动态绑定,而是使用当前类的实现。
类似行为:析构函数
类似的情况也适用于析构函数:
cpp
class B {
public:
virtual ~B() {
test(); // 调用 B::test(),即使是在 ~D() 中
}
virtual void test() {
std::cout << "B::test\n";
}
};
class D : public B {
public:
~D() {
// D 的部分已经被销毁
}
virtual void test() override {
std::cout << "D::test\n";
}
};
int main() {
B* b = new D();
delete b; // 输出 B::test
return 0;
}
在析构函数中,派生类的部分已经被销毁,因此虚函数调用也会被解析为当前类的实现,以避免访问已销毁的派生类成员。
解决方案
如果需要在构造时调用派生类的虚函数实现,可以考虑以下方法:
-
不要在构造函数中调用虚函数:这是最直接的解决方案。将需要在构造后进行的操作移到单独的初始化函数中,由用户显式调用。
cppclass B { public: B() {} void init() { test(); } // 构造后由用户调用 virtual void test() { std::cout << "B::test\n"; } }; class D : public B { public: virtual void test() override { std::cout << "D::test\n"; } }; int main() { D d; d.init(); // 输出 D::test return 0; }
-
使用工厂模式:通过工厂函数创建对象,并在构造后调用虚函数。
cppclass B { public: virtual void test() { std::cout << "B::test\n"; } }; class D : public B { public: virtual void test() override { std::cout << "D::test\n"; } }; template <typename T> T* createAndTest() { T* obj = new T(); obj->test(); // 安全调用派生类的实现 return obj; } int main() { D* d = createAndTest<D>(); // 输出 D::test delete d; return 0; }
总结
- 在构造函数中调用虚函数时,调用的版本是当前构造函数所在类的实现,而不是派生类的重写版本。
- 这是因为派生类的部分在基类构造函数执行时还未构造完成,调用派生类的虚函数可能导致未定义行为。
- 这种设计是C++语言为了保证对象构造的安全性而做出的规定。
- 如果需要多态行为,应避免在构造函数中调用虚函数,或通过其他方式(如初始化函数或工厂模式)实现。
最终答案
在 D d;
的构造过程中,B
的构造函数首先被调用。在 B
的构造函数中调用虚函数 test()
时,由于 D
的部分尚未构造完成,C++ 的规则规定此时虚函数的调用被静态绑定到当前类(即 B
)的实现,而不是派生类 D
的重写版本。因此,输出的是 B
而不是 D
。这是C++为了保证对象构造的安全性而设计的特性。
13、虚表
cpp
//vtable.cpp
#include <iostream>
using namespace std;
class Base
{
public:
Base(long base = 0)
:_base(base)
{
cout << "Base(long base = 0)" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
virtual void f()
{
cout << "Base::f()" << endl;
}
virtual void g()
{
cout << "Base::g()" << endl;
}
virtual void h()
{
cout << "Base::h()" << endl;
}
private:
long _base;
};
class Derived : public Base
{
public:
Derived(long base = 0 ,long derived = 0)
:Base(base)
,_derived(derived)
{
cout << "Derived(long base = 0 ,long derived = 0)" << endl;
}
~Derived()
{
cout << "~Derived()" << endl;
}
virtual void f()
{
cout << "Derived::f()" << endl;
}
virtual void g()
{
cout << "Derived::g()" << endl;
}
virtual void h()
{
cout << "Derived::h()" << endl;
}
private:
long _derived;
};
int add(int a, int b)
{
return a + b;
}
void testAdd()
{
typedef int (*pFunc)(int, int);
pFunc pf = add;
cout << pf(2,3);
}
void test_typedef()
{
Derived derived(10, 20);
typedef void (*pFunc)(void);
pFunc pf = nullptr;
pf = (pFunc)* ((long*)*(long*)&derived);
pf();//调用pf
printf("第一个虚函数的入口地址: %p\n", pf);
}
//函数指针
void testFunctionPointer()
{
Derived derived1(10, 20);
typedef void (*FuncPtr)(); // 定义函数指针类型
FuncPtr* vtable = *(FuncPtr**)&derived1; // 获取虚函数表
printf("对象derived1的地址: %p\n", &derived1);
printf("虚函数表的地址: %p\n", (void*)vtable);
printf("第一个虚函数的入口地址: %p\n", (void*)vtable[0]);
printf("第二个虚函数 (g): %p\n", (void*)vtable[1]);
printf("第三个虚函数 (h): %p\n", (void*)vtable[2]);
Derived derived2(100, 200);
typedef void (*FuncPtr)(); // 定义函数指针类型
FuncPtr* vtable2 = *(FuncPtr**)&derived2; // 获取虚函数表
printf("对象derived2的地址: %p\n", &derived2);
printf("虚函数表的地址: %p\n", (void*)vtable2);
printf("第一个虚函数的入口地址: %p\n", (void*)vtable2[0]);
printf("第二个虚函数 (g): %p\n", (void*)vtable2[1]);
printf("第三个虚函数 (h): %p\n", (void*)vtable2[2]);
//打印_base 和 _derived的值
cout << "_base = " << (long) *((long*)&derived2 + 1) << endl;
cout << "_derived = " << (long) *((long*)&derived2 + 2) << endl;
}
void testFunctionPointer2()
{
// FuncPtr 是一个指向"返回 void 且无参数的函数"的指针类型
typedef void (*FuncPtr)(void);//typedef void (*)(void) FuncPtr; // 逻辑上等价,但语法上不合法
/*正确的 typedef 语法要求 类型名必须紧跟在* 后面。typedef 返回类型 (*别名)(参数列表); */
}
int main()
{
testAdd();
//test_typedef();
testFunctionPointer();
cout << endl << endl;
Derived derived1(10, 20);
printf("对象derived1的地址: %p\n", &derived1);
printf("对象derived1的地址: %p\n", (long*) &derived1);//把derived1的地址强转成long*
//将derived1的地址进行解引用,得到的就是虚函数表的地址
printf("虚函数表的地址: %p\n", *(long*)&derived1);
printf("虚函数表的地址: %p\n", (long*) *(long*)&derived1);//把虚函数表的地址强转成long*
//printf("虚表中第一个虚函数的入口地址: %p\n", (long*) *(long*)*(long*)&derived1);//第一个虚函数的入口地址强转成long*
printf("虚表中第一个虚函数的入口地址: %p\n", (void*)*(*(long**)&derived1));
//&derived1是对象的地址,
//*&derived1是虚函数表的地址
//*(*&derived1)是虚表中第一个虚函数的入口地址
/*printf("虚表中第一个虚函数的入口地址: % p\n", *(*&derived1));*/
return 0;
}
14、函数指针
在 C/C++ 中,typedef void (*FuncPtr)(void);
的真实含义是 typedef void(*)(void) FuncPtr;
的简写形式,但语法上更倾向于:
正确解析
cpp
typedef void (*FuncPtr)(void);
等价于:
cpp
typedef void (*)(void) FuncPtr; // 逻辑上等价,但语法上不合法
或更准确地说:
cpp
// FuncPtr 是一个指向"返回 void 且无参数的函数"的指针类型
typedef void (FuncPtr)(void); // 错误,这样定义的是函数类型,不是指针
typedef void (*FuncPtr)(void); // 正确,定义的是函数指针
详细解释
-
typedef void (*FuncPtr)(void);
-
定义了一个名为
FuncPtr
的类型,它是指向 "返回void
且无参数的函数" 的指针。 -
用法:
cppFuncPtr ptr = &someFunction; // ptr 是一个函数指针 ptr(); // 调用函数
-
-
typedef void(*)(void) FuncPtr;
(逻辑等价,但语法错误)- 这种写法在逻辑上是等价的,但 C/C++ 语法不允许直接这样写。
- 正确的
typedef
语法要求 类型名必须紧跟在*
后面。
-
typedef void()(void) *FuncPtr;
(完全错误)- 这是非法的语法:
void()(void)
不是合法的类型声明。*FuncPtr
不能放在末尾。
- 这是非法的语法:
对比正确 vs 错误写法
写法 | 含义 | 合法性 |
---|---|---|
typedef void (*FuncPtr)(void); |
定义函数指针类型 FuncPtr |
合法 |
typedef void(*)(void) FuncPtr; |
逻辑等价,但语法错误 | 非法 |
typedef void()(void) *FuncPtr; |
无意义,语法错误 | 非法 |
为什么 typedef void (*FuncPtr)(void);
是正确的?
-
void (*)(void)
是一个 函数指针类型 (指向无参数、返回void
的函数)。 -
typedef
的作用是为这个类型定义一个别名FuncPtr
。 -
语法规则要求
*
必须紧跟在类型名之前:cpptypedef 返回类型 (*别名)(参数列表);
实际应用示例
cpp
#include <iostream>
void hello() {
std::cout << "Hello, World!" << std::endl;
}
int main() {
typedef void (*FuncPtr)(void); // 定义函数指针类型
FuncPtr ptr = &hello; // 赋值函数地址
ptr(); // 调用函数
return 0;
}
输出:
Hello, World!
总结
- 正确写法 :
typedef void (*FuncPtr)(void);
- 定义
FuncPtr
为"无参数、返回void
的函数指针"类型。
- 定义
- 错误写法 :
typedef void(*)(void) FuncPtr;
(语法错误,*
不能放在中间)typedef void()(void) *FuncPtr;
(完全非法)
如果你想要更直观的理解,可以想象:
cpp
void (*FuncPtr)(void); // FuncPtr 是一个函数指针
typedef void (*FuncPtr)(void); // 现在 FuncPtr 是一个类型