Contents

《Effective C++》 条款37

永远不要重新定义继承而来的函数默认参数值。

虚函数是动态绑定的,而默认参数值是静态绑定的。

C++为什么不对参数进行动态绑定?

为什么C++坚持用一种反常的方式来运行?答案和运行时效率相关。如果一个默认参数是动态绑定的,编译器就需要用一种方法在运行时为虚函数参数确定一个合适的默认值,比起当前在编译期决定这些参数的机制,它更慢更加复杂。做出的决定是更多的考虑了速度和实现的简单性,结果是你可以享受高效的执行速度,但是如果你没有注意到这个条款的建议,你就会很迷惑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Base
{
    virtual void display(int i = 5)
    {
        std::cout << "Base::" << i << "\n";
    }
};

struct Derived : public Base
{
    virtual void display(int i = 9) override
    {
        std::cout << "Derived::" << i << "\n";
    }
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    Base *a = new Derived();
    a->display();

    Base *aa = new Base();
    aa->display();

    Derived *b = new Derived();
    b->display();
}

输出

1
2
3
Derived::5
Base::5
Derived::9

默认参数是静态绑定的。

为什么通过空指针(NULL)可以正确调用类的部分成员函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

#include <iostream>
 
using namespace std;
 
class B {
public:
    void foo() { cout << "B foo " << endl; }
    void pp() { cout << "B pp" << endl; }
    void FunctionB() { cout << "funB" << endl; }
};
 
int main()
{
    B *somenull = NULL;
    somenull->foo();
    somenull->pp();
    somenull->FunctionB();
 
    return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class CNullPointCall
{
   public:
    static void Test1()
    {
        std::cout << m_iStatic << std::endl;
    }
    void Test2()
    {
        std::cout << "Very Cool!" << std::endl;
    }
    void Test3(int iTest)
    {
        std::cout << iTest << std::endl;
    }
    void Test4()
    {
        std::cout << m_iTest << std::endl;
    }

   private:
    static int m_iStatic;
    int m_iTest = 888;
};

int CNullPointCall::m_iStatic = 999;

输出

1
2
3
4
999
Very Cool!
13
段错误 (核心已转储)

Test4那行代码跟其他3行代码的本质区别: Test4中用到了this指针。

​ 对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。 当程序被编译之后,此成员函数地址即已确定。而成员函数之所以能把属于此类的各个对象的数据区别开, 就是靠这个this指针。函数体内所有对类数据成员的访问, 都会被转化为this->数据成员的方式。

而一个对象的this指针并不是对象本身的一部分,不会影响sizeof(“对象”)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
 
对于上面的例子来说,this的值也就是pNull的值。也就是说this的值为NULL。而Test1()是静态函数,编译器不会给它传递this指针,所以call 1那行代码可以正确调用(这里相当于CNullPointCall::Test1());对于Test2()和Test3()两个成员函数,虽然编译器会给这两个函数传递this指针,但是它们并没有通过this指针来访问类的成员变量,因此call 2和call 3两行代码可以正确调用;而对于成员函数Test4()要访问类的成员变量,因此要使用this指针,这个时候发现this指针的值为NULL,就会造成程序的崩溃。  
1
2
3
4
void CNullPointCall::Test4(CNullPointCall *this)
{
    cout << this->m_iTest << endl; 
}

通过基类指针调用派生类的函数时,虚指针是如何在虚表中找到相应的虚函数位置?

在C++中,通过基类指针调用派生类的函数时,会使用虚指针(vptr)和虚表(vtable)来实现动态绑定

首先,每个有虚函数的类都会有一个虚表,这个虚表是一个函数指针数组,数组中的每个元素都是一个指向类中虚函数的指针。而且,每个此类对象都有一个虚指针,指向它的虚表。

当通过基类指针调用派生类函数时,会发生以下步骤:

  1. 首先,编译器会检查该基类是否有虚函数。如果有,那么它就会在该类的虚表中查找对应函数的地址。
  2. 如果没有找到,就会去检查该基类的父类的虚表(由于继承的关系,子类对象可以访问基类,所以也能访问到基类的虚函数定义)。这个过程会一直持续到找到对应的函数或者到达虚表的顶部(通常是基类的虚表)。
  3. 找到对应的函数后,编译器就会生成代码来调用这个函数。

参考

静态绑定 & 动态绑定

读书笔记 effective c++ Item 37 永远不要重新定义继承而来的函数默认参数值

为什么通过空指针(NULL)可以正确调用类的部分成员函数