我们知道,指针是C/C 语言的一把利刀。C/C 的指针分为三类:
① 指向数据的指针
② 函数指针
③ void指针
因为指针,对数据和函数的访问而变得更加灵活。
1 函数回调函数指针做函数参数,可以调用第三方函数,让使用函数指针做参数的函数更加灵活和通用。
参考:
编程|理解回调函数是caller和callee之外的第三者(callbackee)
C |从qsort函数了解void类型和回调函数
C函数指针,C 函数对象、lambda,Java接口加方法引用来回调函数
2 实现动态绑定Direct function calls can be resolved using a process known as early binding. Early binding (also called static binding) means the compiler (or linker) is able to directly associate the identifier name (such as a function or variable name) with a machine address. Remember that all functions have a unique address. So when the compiler (or linker) encounters a function call, it replaces the function call with a machine language instruction that tells the CPU to jump to the address of the function.
直接函数调用可以使用称为早期绑定的过程来解决。早期绑定(也称为静态绑定)意味着编译器(或链接器)能够将标识符名称(如函数或变量名称)与计算机地址直接关联。请记住,所有函数都有唯一的地址。因此,当编译器(或链接器)遇到函数调用时,它会用机器语言指令替换函数调用,该指令告诉CPU跳转到函数的地址。
In some programs, it is not possible to know which function will be called until runtime (when the program is run). This is known as late binding (or dynamic binding). In C , one way to get late binding is to use function pointers. To review function pointers briefly, a function pointer is a type of pointer that points to a function instead of a variable. The function that a function pointer points to can be called by using the function call operator (()) on the pointer.
在某些程序中,直到运行时(程序运行时),才可能知道将调用哪个函数。这称为后期绑定(或动态绑定)。在C 中,获取后期绑定的一种方法是使用函数指针。简单回顾一下函数指针,函数指针是指向函数而不是变量的指针类型。函数指针指向的函数可以使用指针上的函数调用运算符(())进行调用。
#include <iostream>
int add(int x, int y)
{
return x y;
}
int subtract(int x, int y)
{
return x - y;
}
int multiply(int x, int y)
{
return x * y;
}
int main()
{
int x{};
std::cout << "Enter a number: ";
std::cin >> x;
int y{};
std::cout << "Enter another number: ";
std::cin >> y;
int op{};
do
{
std::cout << "Enter an operation (0=add, 1=subtract, 2=multiply): ";
std::cin >> op;
} while (op < 0 || op > 2);
// Create a function pointer named pFcn (yes, the syntax is ugly)
int (*pFcn)(int, int) { nullptr };
// Set pFcn to point to the function the user chose
switch (op)
{
case 0: pFcn = add; break;
case 1: pFcn = subtract; break;
case 2: pFcn = multiply; break;
}
// Call the function that pFcn is pointing to with x and y as parameters
// This uses late binding
std::cout << "The answer is: " << pFcn(x, y) << '\n';
return 0;
}
In this example, instead of calling the add(), subtract(), or multiply() function directly, we’ve instead set pFcn to point at the function we wish to call. Then we call the function through the pointer. The compiler is unable to use early binding to resolve the function call pFcn(x, y) because it can not tell which function pFcn will be pointing to at compile time!
在本例中,我们没有直接调用add()、subtract()或multiply()函数,而是将pFcn设置为指向要调用的函数。然后我们通过指针调用函数。编译器无法使用早期绑定来解析函数调用pFcn(x,y),因为它无法判断pFcn在编译时将指向哪个函数!
Late binding is slightly less efficient since it involves an extra level of indirection. With early binding, the CPU can jump directly to the function’s address. With late binding, the program has to read the address held in the pointer and then jump to that address. This involves one extra step, making it slightly slower. However, the advantage of late binding is that it is more flexible than early binding, because decisions about what function to call do not need to be made until run time.
后期绑定的效率稍低,因为它涉及额外的间接级别。通过早期绑定,CPU可以直接跳到函数的地址。对于后期绑定,程序必须读取指针中的地址,然后跳转到该地址。这需要一个额外的步骤,使其稍微慢一点。然而,后期绑定的优点是,它比早期绑定更灵活,因为在运行时之前不需要决定要调用什么函数。
3 模拟类封装函数指针放入结构体,且函数指针的函数参数是该结构体指针,这样便实现了封装。
多个函数指针放入结构体,便可以构造一个函数表。
参考:
C|简单模拟实现继承、封装、多态、泛型
C语言模拟面向对象技术:如何利用函数指针的继承来实现多态?
4 动态绑定、函数指针数组(虚函数表)、多态The virtual table is simply a static array that the compiler sets up at compile time. A virtual table contains one entry for each virtual function that can be called by objects of the class. Each entry in this table is simply a function pointer that points to the most-derived function accessible by that class.
虚函数表只是编译器在编译时设置的静态数组。虚函数表包含一个可由类的对象调用的每个虚函数的条目。此表中的每个条目都只是一个函数指针,它指向该类可访问的派生层次最深(most-derived)的函数。
In addition to, the compiler also adds a hidden pointer that is a member of the base class, which we will call *__vptr. *__vptr is set (automatically) when a class object is created so that it points to the virtual table for that class. Unlike the *this pointer, which is actually a function parameter used by the compiler to resolve self-references, *__vptr is a real pointer. Consequently, it makes each class object allocated bigger by the size of one pointer. It also means that *__vptr is inherited by derived classes, which is important.
此外,编译器还添加了一个作为基类成员的隐藏指针,我们将其称为*__vptr。*__vptr创建类对象时(自动)设置,使其指向该类的虚函数表。与*this指针不同,*this指针实际上是编译器用于解析自引用的函数参数,*__vptr是一个实指针。因此,它使分配给每个类对象变大一个指针大小。这也意味着*__vptr由派生类继承,这一点很重要。
By now, you’re probably confused as to how these things all fit together, so let’s take a look at a simple example:
到目前为止,您可能对这些东西是如何组合在一起的感到困惑,所以让我们看一个简单的例子:
class Base
{
public:
virtual void function1() {};
virtual void function2() {};
};
class D1: public Base
{
public:
virtual void function1() {};
};
class D2: public Base
{
public:
virtual void function2() {};
};
Because there are 3 classes here, the compiler will set up 3 virtual tables: one for Base, one for D1, and one for D2.
因为这里有3个类,编译器将设置3个虚函数表:一个用于Base,一个用于D1,一个用于D2。
The compiler also adds a hidden pointer member to the most base class that uses virtual functions. Although the compiler does this automatically, we’ll put it in the next example just to show where it’s added:
编译器还向使用虚函数的顶层基类添加了一个隐藏指针成员。虽然编译器会自动执行此操作,但我们将把它放在下一个示例中,只是为了显示它的添加位置:
class Base
{
public:
VirtualTable* __vptr;
virtual void function1() {};
virtual void function2() {};
};
class D1: public Base
{
public:
virtual void function1() {};
};
class D2: public Base
{
public:
virtual void function2() {};
};
When a class object is created, *__vptr is set to point to the virtual table for that class. For example, when an object of type Base is created, *__vptr is set to point to the virtual table for Base. When objects of type D1 or D2 are constructed, *__vptr is set to point to the virtual table for D1 or D2 respectively.
创建类对象时,*__vptr设置为指向该类的虚函数表。例如,创建Base类型的对象时,*__vptr设置为指向Base的虚函数表。构造D1或D2类型的对象时,*__vptr设置为分别指向D1或D2的虚函数表。
Now, let’s talk about how these virtual tables are filled out. Because there are only two virtual functions here, each virtual table will have two entries (one for function1() and one for function2()). Remember that when these virtual tables are filled out, each entry is filled out with the most-derived function an object of that class type can call.
现在,让我们谈谈这些虚函数表是如何填写的。因为这里只有两个虚函数,所以每个虚函数表将有两个条目(一个用于function1(),一个用于function2())。请记住,在填写这些虚函数表时,每个条目都会填写该类类型的对象可以调用的派生层次最深的函数。
The virtual table for Base objects is simple. An object of type Base can only access the members of Base. Base has no access to D1 or D2 functions. Consequently, the entry for function1 points to Base::function1() and the entry for function2 points to Base::function2().
基本对象的虚函数表很简单。Base类型的对象只能访问Base的成员。Base无法访问D1或D2的函数。因此,function1的条目指向Base::function1(),function2的条目指向Base::function2()。
The virtual table for D1 is slightly more complex. An object of type D1 can access members of both D1 and Base. However, D1 has overridden function1(), making D1::function1() more derived than Base::function1(). Consequently, the entry for function1 points to D1::function1(). D1 hasn’t overridden function2(), so the entry for function2 will point to Base::function2().
D1的虚函数表稍微复杂一些。D1类型的对象可以访问D1和Base的成员。但是,D1重写了function1(),使D1::function1()比Base::function1()派生层次更深一层。因此,function1的条目指向D1::function1()。D1没有重写function2(),因此function2的条目将指向Base::function2()。
The virtual table for D2 is similar to D1, except the entry for function1 points to Base::function1(), and the entry for function2 points to D2::function2().
D2的虚函数表与D1类似,只是function1的条目指向Base::function1(),function2的条目指向D2::function2()。
Here’s a picture of this graphically:
以上内容的图示化:
Although this diagram is kind of crazy looking, it’s really quite simple: the *__vptr in each class points to the virtual table for that class. The entries in the virtual table point to the most-derived version of the function that objects of that class are allowed to call.
虽然这个图看起来有点疯狂,但其实很简单:每个类中的*__vptr都指向该类的虚函数表。虚函数表中的条目指向允许该类的对象调用的函数的派生层次最深的版本。
So consider what happens when we create an object of type D1:
因此,考虑一下当我们创建D1类型的对象时会发生什么:
int main()
{
D1 d1;
}
Because d1 is a D1 object, d1 has its *__vptr set to the D1 virtual table.
由于d1是d1对象,d1将其*__vptr设置为d1虚函数表。
Now, let’s set a base pointer to D1:
现在,让我们将一个基指针设置为D1:
int main()
{
D1 d1;
Base* dPtr = &d1;
return 0;
}
Note that because dPtr is a base pointer, it only points to the Base portion of d1. However, also note that *__vptr is in the Base portion of the class, so dPtr has access to this pointer. Finally, note that dPtr->__vptr points to the D1 virtual table! Consequently, even though dPtr is of type Base, it still has access to D1’s virtual table (through __vptr).
请注意,因为dPtr是一个基指针,所以它只指向d1的基部分。但是,还要注意*__vptr位于类的基部分,因此dPtr可以访问该指针。最后,请注意dPtr->__vptr指向D1虚函数表!因此,即使dPtr是Base类型,它仍然可以访问D1的虚函数表(通过__vptr)。
So what happens when we try to call dPtr->function1()?
那么,当我们尝试调用dPtr->function1()时会发生什么呢?
int main()
{
D1 d1;
Base* dPtr = &d1;
dPtr->function1();
return 0;
}
First, the program recognizes that function1() is a virtual function. Second, the program uses dPtr->__vptr to get to D1’s virtual table. Third, it looks up which version of function1() to call in D1’s virtual table. This has been set to D1::function1(). Therefore, dPtr->function1() resolves to D1::function1()!
首先,程序识别function1()是一个虚函数。其次,程序使用dPtr->__vptr访问D1的虚函数表。第三,它查找要在D1的虚函数表中调用哪个版本的function1()。这已设置为D1::function1()。因此,dPtr->function1()解析为D1::function1()!
Now, you might be saying, “But what if dPtr really pointed to a Base object instead of a D1 object. Would it still call D1::function1()?”. The answer is no.
现在,您可能会说,“但如果dPtr真的指向一个基对象而不是D1对象,会不会仍然调用D1::function1()?”。答案是否定的。
int main()
{
Base b;
Base* bPtr = &b;
bPtr->function1();
return 0;
}
In this case, when b is created, __vptr points to Base’s virtual table, not D1’s virtual table. Consequently, bPtr->__vptr will also be pointing to Base’s virtual table. Base’s virtual table entry for function1() points to Base::function1(). Thus, bPtr->function1() resolves to Base::function1(), which is the most-derived version of function1() that a Base object should be able to call.
在这种情况下,创建b时,__vptr指向Base的虚函数表,而不是D1的虚函数表。因此,bPtr->__vptr也将指向Base的虚函数表。function1()的基虚函数表条目指向基::function1()。因此,bPtr->function1()解析为Base::function1(),这是基对象应该能够调用的function1()的派生层次最深的版本。
By using these tables, the compiler and program are able to ensure function calls resolve to the appropriate virtual function, even if you’re only using a pointer or reference to a base class!
通过使用这些表,编译器和程序能够确保函数调用解析为适当的虚函数,即使您只使用指向基类的指针或引用!
Calling a virtual function is slower than calling a non-virtual function for a couple of reasons: First, we have to use the *__vptr to get to the appropriate virtual table. Second, we have to index the virtual table to find the correct function to call. Only then can we call the function. As a result, we have to do 3 operations to find the function to call, as opposed to 2 operations for a normal indirect function call, or one operation for a direct function call. However, with modern computers, this added time is usually fairly insignificant.
调用虚函数比调用非虚函数慢,原因有两个:首先,我们必须使用*__vptr来访问相应的虚函数表。其次,我们必须对虚函数表进行索引,以找到要调用的正确函数。只有这样,我们才能调用该函数。因此,我们必须执行3个操作才能找到要调用的函数,而不是对普通间接函数调用执行2个操作,或对直接函数调用执行一个操作。然而,对于现代计算机来说,增加的时间通常是微不足道的。
Also as a reminder, any class that uses virtual functions has a *__vptr, and thus each object of that class will be bigger by one pointer. Virtual functions are powerful, but they do have a performance cost.
同时提醒一下,任何使用虚函数的类都有一个*__vptr,因此该类的每个对象都会增大一个指针。虚函数功能强大,但它们确实会带来性能成本。
当一个参数使用一个有虚函数的基类指针对参数时,该基类指针可以使用任一派生类对象做参数,该基类指针调用虚函数时,会在虚函数表中查找派生层次最深的重写函数。其实,这种函数对虚函数的调用,与普通函数通过函数指针调用调用第三方函数用异曲同工之秒,让主调函数变得更有弹性,如果一个相同的接口,可以调用不同的第三函数或具体重写的虚函数。
ref
https://www.learncpp.com/cpp-tutorial/early-binding-and-late-binding/
-End-
,