C/C++函数调用过程分析
这里以一个简单的C语言代码为例,来分析函数调用过程
代码:
1 #include <stdio.h> 2 3 int func(int param1 ,int param2,int param3) 4 { 5 int var1 = param1; 6 int var2 = param2; 7 int var3 = param3; 8 9 printf("var1=%d,var2=%d,var3=%d",var1,var2,var3); 10 return var1; 11 } 12 13 int main(int argc, char* argv[]) 14 { 15 int result = func(1,2,3); 16 17 return 0; 18 }
首先说明,在堆栈中变量分布是从高地址到低地址分布,EBP是指向栈底的指针,在过程调用中不变,又称为帧指针。ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称为栈指针。
下面来逐步分析函数的调用过程
1.函数main执行,main各个参数从右向左逐步压入栈中,最后压入返回地址
2.执行第15行,3个参数以从左向右的顺序压入堆栈,及从param3到param1,栈内分布如下图:
3.然后是返回地址入栈:此时的栈内分布如下:
4.第3行函数调用时,通过跳转指令进入函数后,函数地址入栈后,EBP入栈,然后把当前ESP的值给EBP,对应的汇编指令:
push ebp mov ebp esp
此时栈顶和栈底指向同一位置,栈内分布如下:
5.第5行开始执行, int var1 = param1; int var2 = param2; int var3 = param3;按申明顺序依次存储。对应的汇编:
mov 0x8(%ebp),%eax mov %eax,-0x4(%ebp)
其中将[EBP+0x8]地址里的内容赋给EAX,即把param的值赋给EAX,然后把EAX的中的值放到[EBP-4]这个地址里,即把EAX值赋给var1,完成C代码 int var1 = param1,其他变量雷同。
6.第9行,输出结果,第10行执行 对应的汇编代码:
mov -0x4(%ebp),%eax
最后通过eax寄存器保存函数的返回值;
7.调用执行函数完毕,局部变量var3,var2,var1一次出栈,EBP恢复原值,返回地址出栈,找到原执行地址,param1,param2,param3依次出栈,函数调用执行完毕。图略
- #include <iostream>
- using namespace std;
- class A
- {
- private:
- int m_value;
- public:
- A(int value)
- {
- m_value = value;
- }
- void Print1()
- {
- printf("hello world");
- }
- void Print2()
- {
- printf("%d", m_value);
- }
- };
- int main()
- {
- A* pA = NULL;
- pA->Print1();
- pA->Print2();
- return 0;
- }
分析:答案是Print1调用正常,打印出hello world,但运行至Print2时,程序崩溃。调用Print1时,并不需要pA的地址,因为Print1的函数地址是固定的。编译器会给Print1传入一个this指针,该指针为NULL,但在Print1中该this指针并没有用到。只要程序运行时没有访问不该访问的内存就不会出错,因此运行正常。在运行print2时,需要this指针才能得到m_value的值。由于此时this指针为NULL,因此程序崩溃了。
题目(二)程序的输出结果
- #include <iostream>
- using namespace std;
- class A
- {
- private:
- int m_value;
- public:
- A(int value)
- {
- m_value = value;
- }
- void Print1()
- {
- printf("hello world");
- }
- virtual void Print2()
- {
- printf("hello world");
- }
- };
- int main()
- {
- A* pA = NULL;
- pA->Print1();
- pA->Print2();
- return 0;
- }
分析:答案是Print1调用正常,打印出hello world,但运行至Print2时,程序崩溃。Print1的调用情况和上面的题目一样,不在赘述。由于Print2是虚函数。C++调用虚函数的时候,要根据实例(即this指针指向的实例)中虚函数表指针得到虚函数表,再从虚函数表中找到函数的地址。由于这一步需要访问实例的地址(即this指针),而此时this指针为空指针,因此导致内存访问出错。
题目(三):静态成员函数能不能同时也是虚函数?
分析:答案是不能。调用静态成员函数不要实例。但调用虚函数需要从一个实例中指向虚函数表的指针以得到函数的地址,因此调用虚函数需要一个实例。两者相互矛盾。
题目(四):我们可以用static修饰一个类的成员函数,也可以用const修饰类的成员函数(写在函数的最后表示不能修改成员变量,不是指写在前面表示返回值为常量)。请问:能不能同时用static和const修饰类的成员函数?
分析:答案是不可以。C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时static的用法和static是冲突的。
我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。因此不能同时用它们。
类的成员、对象成员的引用
本讲基本要求
掌握:类成员函数的性质、定义及存贮方式。对象成员三种引用方式,。
理解:类函数、作用域运算符、inline成员函数的概念。
重点、难点: 类成员函数的性质、定义、存贮方式及引用;对象成员三种引用方式。
一、 类的成员函数(成员数据、成员函数与全局函数)(成员函数的性质、定义)
1、成员函数的性质
1、类的成员函数(简称类函数)是函数的一种,它也有返回值和函数类型,它与一般函数的区别只是:它是属于一个类的成员,出现在类体中。它可以被指定为private(私有的)、public(公用的)或protected(受保护的)。
2、在使用类函数时,要注意调用它的权限(它能否被调用)以及它的作用域(函数能使用什么范围中的数据和函数)。成员函数可以访问本类中任何成员(包括私有的和公用的),可以引用在本作用域中有效的数据。
3、一般的做法是将需要被外界调用的成员函数指定为public,它们是类的对外接口。
4、类的成员函数是类体中十分重要的部分。如果一个类中不包含成员函数,就等同于C语言中的结构体了,体现不出类在面向对象程序设计中的作用。
2、在类外定义成员函数
前面看到的成员函数是在类体中定义的。也可以在类体中只写成员函数的声明,而在类的外面进行函数定义。如:
class Student
{ public:
void display(); //公用成员函数原型声明
private:
int num;
string name;
char sex; //以上 3行是私有数据成员
};
void Student::display() //在类外定义display类函数
{ cout<<"hum:"<<num<<endl;//函数体
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl;}
Student stud1,stud2; //定义两个类对象
说明:
1、但成员函数在类外定义时,必须在函数名前面加上类名,予以限定(Qualifed),“::”是作用域限定符(field qualifier)或称作用域运算符,用它声明函数是属于哪个类的。
(a)Student::display();(b)没有“Student::”的限定、“::”如 ::display() 或 display()
2、类函数必须先在类体中作原型声明,然后在类外定义,也就是说类体的位置应在函数定义之前(如上面所示的那样),否则编译时会出错。
3、在类的内部对成员函数作声明,而在类体外定义成员函数,这是程序设计的一种良好习惯。
3、inline 成员函数(类内置成员函数)
类的成员函数也可以指定为内置(inline)函数。
问题提出: 在类体中定义的成员函数的规模一般都很小,而系统调用函数的过程所花费的时间开销相对是比较大的。调用一个函数的时间开销远远大于小规模函数体中全部语句的执行时间。
解决办法: 为了减少时间开销,如果在类体中定义的成员函数中不包括循环等控制结构,C++系统会自动将它们作为内置(inline)函数来处理。也就是说,在程序(对象)调用这些成员函数时,并不是真正地执行函数的调用过程(如保留返回地址等处理),而是把函数代码嵌入程序的调用点。这样可以大大减少调用成员函数的时间开销。(程序调用,不占对象物理内存)
定义方法:C++要求对一般的内置函数要用关键字inline声明,但对类内定义的成员函数,可以省略inline,因为这些成员函数已被隐含地指定为内置函数。
例如:
class Student
{ public:
void display()//也可以写成: inline void display()
{ cout<<”illlm:”<<hum<<endl;
cout<<”name:”<<name<<endl;
cout<<”sex:”<<sex<<endl;
}
pnvate:
int num;
string name;
char sex;
};
注意:如果成员函数不在类体内定义,而在类体外定义,系统并不把它默认为内置(inline)函数,调用这些成员函数的过程和调用一般函数的过程是相同的。如果想将这些成员函数指定为内置函数,应当用inline作显式声明。
例如:
class Student
{ public:
inline void display(); //声明此成员函数为内置函数
Private:
int num;
string name;
char sex;
};
inline void Student::display() //在类外定义display函数为内置函数
{ cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"SCX:"<<sex<<endl;
}
注意:
1、如果在类体外定义inline函数,则必须将类定义和成员函数的定义都放在同一个头文件中(或者写在同一个源文件中),否则编译时无法进行置换(将函数代码的拷贝嵌入到函数调用点)。但是这样做不利于类的接口与类的实现分离,不利于信息隐蔽。虽然程序的执行效率提高了,但从软件工程质量的角度来看,这样做并不是好的办法。
2、只有在类外定义的成员函数规模很小而调用频率较高时,才将此成员函数指定为内置函数。
4、 成员函数的存储方式
C++编译系统为每个对象所占用的存储空间只是该对象的数据部分所占用的存储空间,而不包括函数代码所占用的存储空间。
图4: 一般理解 图5:C++编译系统处理
如果声明了一个类:
class Time
{ public:
int hour;
int minute;
int sec;
void set()
{ cin>>a>>b>>c;}
};
可以用下面的语句来输出该类对象所占用的字节数:
cout<<sizeof(Time)<<endl; 输出的值是12。
需要注意:
1、虽然调用不同对象的成员函数时都是执行同一段函数代码,但是执行结果一般是不相同的。
2、不论成员函数在类内定义还是在类外定义全局函数,函数的代码段都用同一种方式存储,即都不占用对象的存储空间。
3、不要将成员函数的这种存储方式和inline(内置)函数的概念混淆。用inline声明的作用是在调用该函数时,将函数的代码段复制插入到函数调用点,而若不用inline声明,在调用该函数时,流程转去函数代码段的人口地址,在执行完该函数代码段后,流程返回函数调用点。inline与成员函数是否占用对象的存储空间无关。
4、既然成员函数的代码并不放在对象的存储空间中,那么前面说的“对象studl的成员函数display”的说法是否不对呢?应当说明:常说的“某某对象的成员函数”,是从逻辑的角度而言的,而成员函数的存储方式(不存储在对象的空间内),是从物理的角度而言的,是由计算机根据优化的原则实现的,二者是不矛盾的。物理上的实现必须保证逻辑上的实现。例如某人有钱若干,可以放在家中,也可以放在银行中租用的保险箱中,虽然在物理的角度上保险箱并不在他家中,但保险箱是他租用的,这笔钱无疑是属于他的,这是从逻辑的角度而言的。同样.虽然成员函数并没有放在对象的存储空间中,但从逻辑的角度,成员函数是封装在—个对象中的,所以完全可以说“调用对象studl的成员函数display”,不会引起误解。作为程序设计人员,了解一些物理实现方面的知识是有好处的,可以加深对问题的理解。
二、对象成员的引用(包含成员函数、成员数据)
在程序中经常需要访问对象中的成员。访问对象中的成员可以有3种方法:
- 通过对象名和成员运算符访问对象中的成员;
- 通过指向对象的指针访问对象中的成员;
- 通过对象的引用变量访问对象中的成员。
1、通过对象名和成员运算符访问对象中的成员
访问对象中成员的一般形式为:
对象名.成员名(包含成员函数、成员数据)
说明:
1、“.”是成员运算符,用来对成员进行限定,指明所访问的是哪一个对象中的成员。
2、不能只写成员名而忽略对象名, 不应该这样写:hum=1001; //错误。这样写成为对象中的临时变量。
3、不仅可以在类外引用对象的公用数据成员,而且还可以调用对象的公用成员函数,但同样必须指出对象名,如 studl.display()。
4、应该注意所访问的成员是公用的(public)还是私有的(pavate)。私有数据成员,不能被外界引用。
5、在类外只能调用公用的成员函数。显然在一个类中应当至少有一个公用的成员函数,以作为类的对外接口否则程序就无法对对象进行任何操作。
2、通过指向对象的指针访问对象中的成员
访问对象中成员的一般形式为:
对象指针名->成员名(包含成员函数、成员数据)
或:(*对象指针名).成员名(包含成员函数、成员数据)
例如:
class Time
{ public: //数据成员是公用的
int hour;
int minute;
};
Time t,*p //定义对象t和指向Time类的指引变量P
P=&T //使p指向对象t
cout<<P->hour;//输出P指向的对象中的成员hour
3、通过对象的引用来访问对象中的成员
如果为一个对象定义了一个引用,它们是共占同一段存储单元的,实际上它们是同—个对象,只是用不同的名字表示而已。因此完全可以通过引用来访问对象中的成员,其概念和方法与通过对象名来引用对象中的成员是相同的。
例如:如果已声明了Time类,并有以下定义语句:
Time t1; //定义对象t1
Time &t2=t1; //定义Time类引用变量t2,并使之初始化为tl
cout<<t2.hour;//输出对象t1中的成员hour
由于t2与t1共占同一段存储单元(即t2是tl的别名),因此t2.hour就是t1.hour。
首先请看下面的语句:
Point3d obj;
Point3d *ptr = &obj;
当使用上述指针或者对象调用成员函数Func()时,会有:
obj.Func();
ptr->Func();
上述调用的背后到底完成了一些什么呢?
假设Func函数的定义如下:
Point3d Point3d::Func() const
{
Float a = getA();
Point3d ret;
ret._x = _x/a;
ret._y = _y/a;
ret._z = _z/a;
return ret;
}
getA的定义为:
float Point3d::getA()
{
return sqrt(_x*_x+_y*_y+_z*_z);
}
那么看过这些函数的定义之后,我们能否得知上述代码的执行过程呢?答案是不行!上述的代码最多能告诉我们Func函数一定不是static。(因为它操作了non-static成员变量,以及是const函数)。
那么上面这样的成员函数的调用过程到底是怎么完成的呢?
C++的设计准则之一是:non-static成员函数的调用与非成员函数的调用的效率应该是一致的。选择将函数声明为成员函数是不应
该有任何的额外负担的。于是乎编译器在调用成员函数的时候是将其视为非成员函数来调用的。
例如:一个getA的非成员函数的定义为:
float getA(const Point3d *this)
{
return sqrt(this->_x* this->_x + this->_y * this->_y + this->_z * this->_z);
}
这样的函数会给我们一个错觉,那就是非成员函数的调用是效率较低的,因为它是间接地去得到对象的各个成员变量的。而在成员函数里面我们是直接使用的。其实这是一个很大的误区。
这里其实可以很简单的理解一下,类的成员函数其实是有一个隐含的形式参数的,那就是this指针。然而编译器就是这么做的,编译器按照下面的步骤来处理成员函数的调用的:
1 改写成员函数的函数原型,将那个隐含的this指针表示出来。提供一个存取的管道,也就是说我们的函数中调用的就是这个形参(this)的成员变量。如下:
Point3d Point3d::Func ( Point3d * const this)
注意此处const的位置,this指针本身是const的。如果该成员函数本身也是一个const成员函数的话,那么该函数原型应该是
Point3d Point3d::Func ( const Point3d * const this)
2 将该函数中的成员变量使用this指针来进行间接存取。例如:
{
return sqrt(this->_x* this->_x + this->_y * this->_y + this->_z * this->_z);
}
3 将成员函数重新书写为一个外部函数,但是这里值得注意的一个技术“mangling”这个是C++里用于处理重载的同名函数的一个技术。使得该函数名字在程序中是独一无二的。
上述的Func函数或许会处理为下面的名字:(不同的编译器处理方式不同)
Extern Func_Point3dFv(Point3d *const this);
OK!大功告成,现在上面的 obj.Func()的调用就变成了 Func_Point3dFv(&obj)。
这里的整个过程都是编译器在诉说 “成员函数的调用的效率必须与非成员函数一致”。
[updated] 使用类作用域符号调用成员函数(无论是虚函数还是非虚函数),编译器的决议方式与non-static成员函数是一致的。
使用对象调用成员函数(无论是虚函数还是非虚函数),编译器的决议方式与non-static成员函数是一致的。