函数是如何调用的

转自:http://www.cnblogs.com/orlion/archive/2015/12/20/5062165.html


一、局部变量与全局变量

  函数中出现的变量可以分为局部变量和全局变量,在函数内部定义的变量(没有global语句)就是局部变量,只有在函数内部才能够使用它们。在函数外定义的变量就是全局变量

全局变量的作用是增加了函数间数据联系的渠道,全局变量在全部执行过程中都占用存储单元,如果在同一个源文件中,局部变量和全局变量同名,则在局部变量的作用范围内全局变量被屏蔽即它不起作用。

  静态局部变量,有时希望局部变量的值在函数调用结束后不消失而保持原值,即其占用的存储空间不释放,在下一次函数调用时,该变量已有值,即上次函数调用结束时的值,就应该指定该局部变量为"静态局部变量",用static声明。静态局部变量属于静态存储类别,在静态存储区分配内存单元,在程序整个运行期间都不释放,动态局部变量属于动态存储类别,站动态存储区,函数调用结束即释放。静态局部变量的赋值是在编译期,即只赋值一次,在程序运行时它已有初值,以后每次调用函数不再重新赋值而是保留上次函数调用结束的值,而对动态局部变量不是在编译时期进行的,而是在函数调用时进行的,每调用一次函数就重新给一次赋值

二、函数调用过程的分析

1、返回地址的存储

  执行一条指令时,是根据PC中存放的指令地址,将指令由内存取到指令寄存器IR中。程序在执行时按顺序依次执行每一条语句,PC通过加1来指向下一条将要执行的程序语句。但也有一些例外:(1)调用函数(2)函数调用后的返回(3)控制结构(if else while for等)

主调函数是指调用其他函数的函数,被调函数是指被其他函数调用的函数,一个函数既调用别的函数又被另外的函数调用

  上图中,fun0函数调用fun1,fun0函数就是主调函数,fun1是被调函数

发生函数调用时,程序会跳转到被调函数的第一条语句,然后按顺序依次执行被调函数中的语句。函数调用后返回时,程序会返回到主调函数中调用函数的语句的后一条语句继续执行。换句话说,也就是“从哪里离开,就回到哪里”。

  CPU执行程序时,并不知道整个程序的执行步骤是怎样的,完全是“走一步,看一步”。前面我们提到过,CPU都是根据PC中存放的指令地址找到要执行的语句。函数返回时,是“从哪里离开,就回到哪里”。但是当函数要从被调函数中返回时,PC怎么知道调用时是从哪里离开的呢?答案就是——将函数的“返回地址”保存起来。因为在发生函数调用时的PC值是知道的。在主调函数中的函数调用的下一条语句的地址即为当前PC值加1,也就是函数返回时需要的“返回地址”。我们只需将该返回地址保存起来,在被调函数执行完成后,要返回主调函数中时,将返回地址送到PC。这样,程序就可以往下继续执行了。

  函数调用的特点是:越早被调用的函数,越晚返回。比如fun1函数比fun2函数先调用,但是返回的时候fun1晚于fun2返回。这一特点正是"后进先出",所以我们采用栈来保存返回地址

如上图调用过程(1)发生时,需要压入保存返回地址A,栈的状态如图中(a)所示;调用过程(2)发生时,需要压入保存返回地址B,栈的状态如图中(b)所示;返回过程(3)发生时,需要弹出返回地址B,栈的状态如图中(c)所示;调用过程过程(4)发生时,需要压入保存返回地址C,栈的状态如图中(d)所示;返回过程(5)发生时,需要弹出返回地址C,栈的状态如图中(e)所示;返回过程(6)发生时,需要弹出返回地址A,此时栈被清空,图中未画出具体情况

2、函数调用时栈的管理

  如上图所示,fun函数里的变量a和do_add函数里的变量a是两个不同的变量,这两个变量需要存放在不同的地方。局部变量a只在do_add函数内才有意义;局部变量的存储一定是和函数的开始与结束息息相关的。局部变量如同返回地址般也是存在栈里。当函数开始执行时,这个函数的局部变量在栈里被设立(压入),当函数结束时,这个函数的局部变量和返回地址都会被弹出。

  当函数调用时,do_add函数里局部变量c就复制fun函数里变量a的值。在函数返回时,与参数传递同理,在传递返回值时也是将do_add函数里的值赋值给主调函数中的变量b。局部变量只在函数内有意义,离开函数后该局部变量就失效。比如do_add函数里的局部变量d,执行do_add函数时d是有意义的。但执行完do_add函数后,返回到fun函数中,do_add函数里的局部变量d就失效了。因此在弹出d时需要用一个寄存器将返回值d保存起来,所以在外面的调用函数可以来读取这个值。

  局部变量的调用是和栈的操作模式“后进先出”的形式是相同的。这就是为什么返回地址是压入栈里,同样的,局部变量也会压到相对应的栈里面。当函数执行时,这个函数的每一个局部变量就会在栈里有一个空间。在栈中存放此函数的局部变量和返回地址的这一块区域叫做此函数的栈帧(frame)。当此函数结束时,这一块栈帧就会被弹出。

调用do_add()函数前执行的操作:(1)fun的局部变量a压入栈中,其值为10(2)局部变量b压入栈中,由于b的值还未知,因此先为b预留空间

调用do_add()函数时执行的操作:(1)返回地址压到栈中(2)局部变量c的值10压入栈中(c的值是通过复制fun函数中变量a得到的)(3)压入do_add中的局部变量a,其值为3(4)执行a+c,其中a=3,c=10,相加后得d的值为13

do_add()函数返回时执行的操作:(1)do_add()函数执行完后,依次弹出do_add()的局部变量,由于需要将d的值返回,因此在弹出d的时候需要一个寄存器将返回值d保存起来(2)弹出返回地址,将返回地址传到PC(3)返回到fun函数,fun中的局部变量b的值即为do_add()中的返回值d,此时将寄存器中的值赋给b。

  在函数调用时,用一个寄存器将栈顶地址保存起来,称为栈顶指针SP。另外还有一个帧指针FP,用来指向栈中函数信息的底端。这样,栈就被分成了一段一段的空间。每个栈帧对应一次函数调用,在栈帧中存放了前面介绍的函数调用中的返回地址、局部变量值等。每次发生函数调用时,都会有一个栈帧被压入栈的最顶端;调用返回后,相应的栈帧便被弹出。当前正在执行的函数的栈帧总是处于栈的最顶端。

  由于函数调用时,要不断的将一些数据压入栈中,SP的位置是不断变化的,而FP的位置相对于局部变量的位置是确定的,因此函数的局部变量的地址一般通过帧指针FP来计算,而非栈指针SP。

  综合前面所讲,可以总结出:(1)一个函数调用过程就是将数据(包括参数和返回值)和控制信息(返回地址等)从一个函数传递到另一个函数。(2)在执行被调函数的过程中,还要为被调函数的局部变量分配空间,在函数返回时释放这些空间。这些工作都是由栈来完成的。所传参数的地址可以简单的从FP算出来。下图展示了栈帧的通用结构

 

三、实例分析

举一个下图中例子来综合研究一下函数调用时对栈的管理

 

pre函数调用fac(1)函数前执行的操作:

(1)pre的局部变量m压入栈中,其值为1

(2)局部变量f压入栈中,由于f的值还未知,因此先为f预留空间

pre函数调用fac(1)函数时执行的操作:

(1)返回地址压入栈中;

(2)fac(1)的局部变量n压入栈中,其值为1;

(3)局部变量r压入栈,由于r的值还未知,因此先为r预留出空间

fac(1)函数调用fac(0)时执行的操作:

(1)返回地址压入栈中;

(2)fac(0)的局部变量n压入栈中,其值为0;

(3)此时递归达到了终止条件(n==0),结束递归,局部变量r压入栈,r的值为1。

fac(0)函数返回时执行的操作

(1)fac(0)函数执行完后,依次弹出fac(0)的局部变量。在弹出r时用一个寄存器将返回值r保存起来;

(2)弹出返回地址,将返回地址传到PC;

(3)SP=FP,令SP指回fac(1)栈帧的顶部,令FP指回fac(1)栈帧的底部

(4)继续执行函数fac(1),fac(1)中的局部变量r的值即为fac(0)中的返回值乘以n

fac(1)函数返回时执行的操作

(1)fac(1)函数执行完后,依次弹出fac(1)的局部变量,在弹出r时用一个寄存器将返回值r保存起来;

(2)弹出返回地址,将返回地址传回到PC;

(3)SP=FP,令SP指回pre栈帧的顶部,令FP指回pre栈帧的底部

(4)继续执行函数pre,pre中的局部变量f的值即为fac(1)中的返回值r,此时将寄存器中的值赋值给f

 

  各类微处理器对函数调用的处理方式会有所差异,同一体系结构中对不同语言的函数调用的处理方式也会有少许的差异。但通过栈存储局部变量和返回地址等信息,这一点是共同的。我们不需要对函数调用中的每一个执行的细节都了解清楚:知道每一次函数调用对应一个栈帧,栈帧中包含了返回地址、局部变量值等信息。还有一点要注意,解释性语言中发生函数调用时所建立的栈,不是编译时建立的(像C语言等是在编译时就建好了栈),是在有需要的时候再建立的。


<think>首先,用户的问题是:“C++ 虚函数内部调用非虚函数调用的是指针类还是对象类的实现?” 这涉及到C++中虚函数和多态性的行为。 关键点: - 虚函数允许运行时多态:当通过基类指针或引用调用函数时,实际调用的是对象类型(派生类)的实现。 - 非虚函数是静态绑定的:调用基于指针或引用的静态类型,而不是实际对象类型。 现在,问题聚焦于:在虚函数内部调用非虚函数时,这个非虚函数调用是基于什么? - 虚函数内部:意思是,在虚函数的定义中,调用了另一个非虚函数。 - 调用的是指针类还是对象类的实现:这里的“指针类”可能指的是指针的静态类型(声明类型),“对象类”指的是实际对象类型。 在C++中,函数调用(包括非虚函数)的绑定取决于函数的类型: - 非虚函数:在编译时绑定,基于调用者的静态类型。 - 虚函数:在运行时绑定,基于调用者的动态类型。 当在虚函数内部调用一个非虚函数时,由于非虚函数是静态绑定的,调用会基于当前对象的静态类型进行。这里的“当前对象”指的是调用函数的对象。 在虚函数中,`this`指针指向实际对象。但由于非虚函数是静态绑定的,调用非虚函数时,编译器使用`this`的静态类型来决定调用哪个函数。 例如: - 假设有一个基类`Base`和一个派生类`Derived`。 - `Base`有一个虚函数`virtual void foo()`,其中调用了非虚函数`void bar()`。 - `Derived`可能重写了`foo`,但没有重写`bar`。 当通过基类指针指向派生类对象调用`foo`时: - `foo`是虚函数,所以调用`Derived::foo`(如果重写了)。 - 在`Derived::foo`内部调用`bar`时,`bar`是非虚函数,所以调用基于`this`的静态类型。如果`bar`在`Derived`中没有重写,它会调用`Base::bar`,因为静态类型是`Base*`(在指针上下文中)或`Derived`对象但函数未重写。 更精确地说: - 在成员函数中,调用另一个成员函数时,隐式使用`this`指针。 - 对于非虚函数,绑定到`this`的静态类型。 - 在虚函数`foo`中,当调用`bar()`,它等价于`this->bar()`。 - 如果`bar`是非虚函数,`this->bar()`在编译时解析为`this`静态类型的`bar`函数。 在派生类的虚函数实现中,`this`的静态类型是派生类类型,所以如果派生类重写了非虚函数`bar`,它会调用派生类的`bar`;否则,调用基类的`bar`。 但非虚函数不能被重写;派生类可以定义一个同名的函数,但它是隐藏的,不是重写。 在C++中: - 如果基类有一个非虚函数`bar`,派生类定义一个同名的非虚函数`bar`,它不会重写基类的函数;它隐藏了基类的版本。 - 当通过派生类对象调用`bar`时,会调用派生类的版本;通过基类指针调用时,会调用基类的版本,因为非虚。 所以,在虚函数内部调用非虚函数: - 调用取决于`this`的静态类型在编译时。 - 在虚函数`foo`中,`this`的静态类型是声明`foo`的类类型。 - 例如,在`Derived::foo`中,`this`的静态类型是`Derived*`,所以调用`bar()`会解析为`Derived::bar`如果存在,否则`Base::bar`(通过继承)。 但用户的问题是“虚函数内部调用非虚函数时”,所以上下文是虚函数定义中调用另一个函数。 总结: - 当在虚函数内部调用一个非虚函数时,该调用是静态绑定的,基于当前函数(虚函数)的类上下文。 - 具体来说,基于`this`指针的静态类型,即在定义虚函数的类中。 现在,参考用户提供的引用: - 引用[^1]:虚函数在基类定义,通过基类指针或引用调用时动态绑定。 - 引用[^2]:虚函数表指针(vptr)和虚函数表(vtable),用于动态绑定。 - 引用[^3]:虚函数表指针在对象起始位置。 - 引用[^4]:当对象指针为空时,调用非虚函数可能,但虚函数不行。 - 引用[^5]:虚函数调用通过vptr和vtable实现。 这些引用支持虚函数的动态绑定,但对于非虚函数,是静态绑定。 用户意图:了解在虚函数内部调用非虚函数时,调用的是指针的静态类型还是对象的动态类型的实现。 答案应该是:调用的是对象类的实现?不,非虚函数是静态绑定。 澄清: - “指针类”:可能指指针的静态类型(声明类型)。 - “对象类”:指实际对象类型。 在调用函数时: - 虚函数本身调用基于对象动态类型。 - 但在虚函数内部调用非虚函数,是非虚的,所以基于当前上下文,即虚函数调用的对象的静态类型。 例如: ```cpp class Base { public: virtual void foo() { bar(); // bar is non-virtual } void bar() { std::cout << "Base::bar\n"; } }; class Derived : public Base { public: void foo() override { // overrides Base::foo bar(); // calls Derived::bar if it exists, or Base::bar } void bar() { std::cout << "Derived::bar\n"; } // hides Base::bar, not override }; int main() { Base* ptr = new Derived(); ptr->foo(); // calls Derived::foo because virtual // Inside Derived::foo, bar() is called. Since bar is non-virtual, it calls Derived::bar. } ``` 在`Derived::foo`中,`bar()`调用`Derived::bar`,因为`this`的静态类型是`Derived*`。 如果`Derived`没有定义`bar`,那么`bar()`会调用`Base::bar`,因为继承。 但`bar`是非虚的,所以没有多态。 在基类虚函数中: ```cpp class Base { public: virtual void foo() { bar(); // bar is non-virtual } void bar() { std::cout << "Base::bar\n"; } }; class Derived : public Base { public: void foo() override { // if not overridden, it would use Base::foo, which calls Base::bar } // no bar defined }; int main() { Base* ptr = new Derived(); ptr->foo(); // if Derived doesn't override foo, calls Base::foo, then Base::bar // if Derived overrides foo, calls Derived::foo, then if bar called, depends } ``` 所以,在虚函数内部调用非虚函数时: - 调用基于当前`this`指针的静态类型。 - 当前`this`指针的静态类型是定义该虚函数的类(在派生类重写时,是派生类类型)。 - 因此,它调用的是实际对象类的实现,但仅限于非虚函数;如果是虚函数,则动态绑定。 在派生类重写的虚函数中,`this`的静态类型是派生类,所以调用非虚函数时,使用派生类的版本(如果存在),否则基类版本。 因此,回答用户的问题:调用的是对象类的实现,因为对象是实际类型,但通过静态绑定。 更精确地说:调用的是定义虚函数的那个类(可能基类或派生类)的上下文中的非虚函数实现。 在C++术语中,非虚函数调用基于调用点的静态类型。 对于用户:当在虚函数内部调用非虚函数时,调用的是实际对象类的实现,但仅限于非虚函数没有被隐藏的情况下;实际上,它基于虚函数定义所在的类。 在代码中: - 如果虚函数在派生类中被重写,并且在重写的函数调用非虚函数,它调用派生类的版本(如果派生类定义了它)。 - 如果虚函数在基类中定义并调用非虚函数,它调用基类的版本。 所以,它依赖于虚函数的实现是在哪个类中。 但本质上,由于非虚函数是静态绑定的,调用基于`this`的静态类型,而`this`在虚函数中指向实际对象,但静态类型是定义函数的类类型。 在派生类成员函数中,`this`的静态类型是派生类。 因此,在派生类的虚函数调用非虚函数调用派生类的实现(如果存在),否则基类。 在基类虚函数调用非虚函数调用基类的实现。 所以,对于用户的问题:“调用的是指针类还是对象类的实现” - “指针类”:指针的静态类型(例如,`Base*`) - “对象类”:实际对象类型(例如,`Derived`) 在调用`ptr->foo()`时,`ptr`是`Base*`,但实际对象是`Derived`。 - 虚函数`foo`调用基于对象类型。 - 在`foo`内部,调用非虚函数`bar`:如果`foo`是`Derived::foo`,则`bar`调用`Derived::bar`;如果`foo`是`Base::foo`,则`bar`调用`Base::bar`。 所以,非虚函数调用不直接基于指针的静态类型,而是基于虚函数调用的实现所在的类类型。 因此,它调用的是对象类的实现,因为对象类决定了哪个虚函数调用,然后在该虚函数的上下文中调用非虚函数。 在例子中,当`Derived::foo`被调用,`this`指向`Derived`对象,静态类型`Derived*`,所以`bar()`调用`Derived::bar`。 如果`Derived`没有定义`bar`,则调用`Base::bar`。 但指针的静态类型是`Base*`,但调用`bar`时不使用指针的静态类型,而是使用当前对象的静态类型。 所以,调用的是对象类的实现(实际对象的类),但非虚函数是静态的,所以它取决于类定义。 最终回答:调用的是实际对象类的实现,因为虚函数调用基于对象,然后在虚函数内部,非虚函数调用基于该对象在编译时的类型。 现在,结构化回答: - 用中文回答。 - 清晰结构。 - 生成相关问题。</think>### C++ 虚函数内部调用非虚函数的行为分析 在 C++ 中,当虚函数内部调用非虚函数时,**调用的是实际对象类的实现(即对象类的非虚函数版本),而不是指针类的实现**。这是因为非虚函数是静态绑定的(编译时确定),而虚函数是动态绑定的(运行时确定)。在虚函数执行时,`this` 指针指向实际对象,非虚函数调用基于 `this` 的静态类型,即实际对象的类类型。下面通过示例和原理逐步解释。 #### 关键原理 1. **虚函数的动态绑定**: - 虚函数通过虚函数表(vtable)实现运行时多态。当通过基类指针或引用调用函数时,实际调用的是对象类型(派生类)的重写版本[^2][^5]。 - 例如:`Base* ptr = new Derived(); ptr->virtualFunc();` 会调用 `Derived::virtualFunc`。 2. **非虚函数的静态绑定**: - 非虚函数在编译时绑定,基于调用点的静态类型(即 `this` 指针的声明类型)[^4][^5]。 - 在虚函数内部调用非虚函数(如 `this->nonVirtualFunc()`),`this` 的静态类型是实际对象类的类型(在派生类重写虚函数时,静态类型是派生类)。 3. **`this` 指针的作用**: - 在成员函数(包括虚函数)中,`this` 指向实际对象。对于非虚函数调用,编译器使用 `this` 的静态类型决定目标函数,而非指针的声明类型[^3][^5]。 #### 示例代码分析 ```cpp #include <iostream> class Base { public: virtual void foo() { std::cout << "Base::foo calls bar: "; bar(); // 调用非虚函数 bar } void bar() { std::cout << "Base::bar\n"; } // 非虚函数 }; class Derived : public Base { public: void foo() override { std::cout << "Derived::foo calls bar: "; bar(); // 调用非虚函数 bar } void bar() { std::cout << "Derived::bar\n"; } // 非虚函数,隐藏基类版本 }; int main() { Base* ptr = new Derived(); // 基类指针指向派生类对象 ptr->foo(); // 动态绑定调用 Derived::foo delete ptr; return 0; } ``` **输出**: ``` Derived::foo calls bar: Derived::bar ``` **解释**: - `ptr->foo()` 调用函数 `foo`:由于 `ptr` 指向 `Derived` 对象,运行时绑定到 `Derived::foo`。 - 在 `Derived::foo` 内部调用 `bar()`: - `bar` 是非虚函数,静态绑定基于 `this` 的静态类型(`Derived*`)。 - 因此调用 `Derived::bar`,而非 `Base::bar`。 - 如果 `Derived` 未重写 `bar`,则调用 `Base::bar`(通过继承),但绑定仍基于 `Derived` 的静态类型。 #### 核心结论 - **调用的是对象类的实现**:虚函数内部对非虚函数调用,始终基于实际对象的类类型(即 `this` 的静态类型),而非指针的声明类型。这是因为: - 虚函数执行时,`this` 指向实际对象,其静态类型在编译时已确定为对象类(如 `Derived`)。 - 非虚函数无多态性,直接绑定到 `this` 静态类型的函数版本[^4][^5]。 - **对比指针类的影响**:如果直接通过指针调用非虚函数(如 `ptr->bar()`),会基于指针的静态类型(`Base*`)调用 `Base::bar`,但这与虚函数内部无关。 #### 注意事项 - **隐藏 vs 重写**:派生类定义同名非虚函数会隐藏基类版本(非重写)。在虚函数内部调用时,优先使用派生类版本(如示例所示)。 - **性能**:非虚函数静态绑定无运行时开销,适合性能敏感场景[^5]。 - **设计建议**:在虚函数调用非虚函数时,确保派生类正确实现所有依赖函数,避免意外行为。如果需多态行为,应将内部函数也声明为虚函数。 ### 相关问题 1. **虚函数内部调用另一个虚函数时,绑定行为是怎样的?** (例如:在 `Derived::foo` 中调用 `virtual bar()` 会触发动态绑定吗?) 2. **如何避免非虚函数在继承中被隐藏的问题?** (例如:使用 `using` 声明或设计接口时需要注意什么?) 3. **C++ 中静态绑定和动态绑定的性能差异有多大?** (例如:在高频调用场景下,虚函数 vs 非虚函数的开销对比。) 4. **当对象指针为 `nullptr` 时,调用非虚函数是否安全?** (例如:`ptr->nonVirtualFunc()` 在 `ptr = nullptr` 时的行为分析[^4]。)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值