C++特性的C语言实现(1)继承

PS:阅读文章之前应当有一定的汇编、C和C++的基础。

面向对象的程序设计有三大特性:封装、继承和多肽。这里只讨论后两种特性继承和多肽,因为C++中封装性实际上是由编译器去识别关键字public、private和protected来实现的。封装性的目的也是为了增加代码健壮性,减少写代码时出错的概率。也就是说只要能编译通过,在C++的类中无论是public、protected还是private,最终生成的汇编代码其实是一致的。

而继承和多肽是C++对C语言的补充,使得代码写起来更加简单。
先来看继承。
C++中的继承有两个方面的意思,第一子类拥有父类所有的成员变量,第二子类也拥有父类的所有成员函数,所以很多C++教材会说一句话,子类拥有父类的所有特性。


1.没有成员函数的类


如果一个类中没有成员函数,例如:

#include 

class Point{
public:
	int x;
	int y;
};

class PointEx : public Point{
public:
	int z;
};

int main(){

	Point * pPoint = new Point;
	PointEx * pPointEx = new PointEx;

	pPoint->x = 1;
	pPoint->y = 2;

	pPointEx->x = 3;
	pPointEx->y = 4;
	pPointEx->z = 5;

	delete pPoint;
	delete pPointEx;

	return 0;
}
反汇编代码如下:
	Point * pPoint = new Point;
00AF34FE 6A 08                push        8  
00AF3500 E8 72 DC FF FF       call        operator new (0AF1177h)  
00AF3505 83 C4 04             add         esp,4  
00AF3508 89 85 FC FE FF FF    mov         dword ptr [ebp-104h],eax  
00AF350E 8B 85 FC FE FF FF    mov         eax,dword ptr [ebp-104h]  
00AF3514 89 45 F8             mov         dword ptr [pPoint],eax  
	PointEx * pPointEx = new PointEx;
00AF3517 6A 0C                push        0Ch  
00AF3519 E8 59 DC FF FF       call        operator new (0AF1177h)  
00AF351E 83 C4 04             add         esp,4  
00AF3521 89 85 08 FF FF FF    mov         dword ptr [ebp-0F8h],eax  
00AF3527 8B 85 08 FF FF FF    mov         eax,dword ptr [ebp-0F8h]  
00AF352D 89 45 EC             mov         dword ptr [pPointEx],eax  

	pPoint->x = 1;
00AF3530 8B 45 F8             mov         eax,dword ptr [pPoint]  
00AF3533 C7 00 01 00 00 00    mov         dword ptr [eax],1  
	pPoint->y = 2;
00AF3539 8B 45 F8             mov         eax,dword ptr [pPoint]  
00AF353C C7 40 04 02 00 00 00 mov         dword ptr [eax+4],2  

	pPointEx->x = 3;
00AF3543 8B 45 EC             mov         eax,dword ptr [pPointEx]  
00AF3546 C7 00 03 00 00 00    mov         dword ptr [eax],3  
	pPointEx->y = 4;
00AF354C 8B 45 EC             mov         eax,dword ptr [pPointEx]  
00AF354F C7 40 04 04 00 00 00 mov         dword ptr [eax+4],4  
	pPointEx->z = 5;
00AF3556 8B 45 EC             mov         eax,dword ptr [pPointEx]  
00AF3559 C7 40 08 05 00 00 00 mov         dword ptr [eax+8],5  
在main函数中,new出两个类的实例各一个,然后通过指针逐一给他们赋值。

我们也可以用纯C语言实现一样的结果:

#include 

typedef struct Point{
	int x;
	int y;
}Point_t;

typedef struct PointEx{
	Point_t point;
	int z;
}PointEx_t;

int main(){

	Point * pPoint = new Point;
	PointEx * pPointEx = new PointEx;

	pPoint->x = 1;
	pPoint->y = 2;

	pPointEx->point.x = 3;
	pPointEx->point.y = 4;
	pPointEx->z = 5;

	delete pPoint;
	delete pPointEx;

	return 0;
}

反汇编:

	Point * pPoint = new Point;
012734FE 6A 08                push        8  
01273500 E8 72 DC FF FF       call        operator new (1271177h)  
01273505 83 C4 04             add         esp,4  
01273508 89 85 FC FE FF FF    mov         dword ptr [ebp-104h],eax  
0127350E 8B 85 FC FE FF FF    mov         eax,dword ptr [ebp-104h]  
01273514 89 45 F8             mov         dword ptr [pPoint],eax  
	PointEx * pPointEx = new PointEx;
01273517 6A 0C                push        0Ch  
01273519 E8 59 DC FF FF       call        operator new (1271177h)  
0127351E 83 C4 04             add         esp,4  
01273521 89 85 08 FF FF FF    mov         dword ptr [ebp-0F8h],eax  
01273527 8B 85 08 FF FF FF    mov         eax,dword ptr [ebp-0F8h]  
0127352D 89 45 EC             mov         dword ptr [pPointEx],eax  

	pPoint->x = 1;
01273530 8B 45 F8             mov         eax,dword ptr [pPoint]  
01273533 C7 00 01 00 00 00    mov         dword ptr [eax],1  
	pPoint->y = 2;
01273539 8B 45 F8             mov         eax,dword ptr [pPoint]  
0127353C C7 40 04 02 00 00 00 mov         dword ptr [eax+4],2  

	pPointEx->point.x = 3;
01273543 8B 45 EC             mov         eax,dword ptr [pPointEx]  
01273546 C7 00 03 00 00 00    mov         dword ptr [eax],3  
	pPointEx->point.y = 4;
0127354C 8B 45 EC             mov         eax,dword ptr [pPointEx]  
0127354F C7 40 04 04 00 00 00 mov         dword ptr [eax+4],4  
	pPointEx->z = 5;
01273556 8B 45 EC             mov         eax,dword ptr [pPointEx]  
01273559 C7 40 08 05 00 00 00 mov         dword ptr [eax+8],5  
这也就是说C++中类的成员变量的继承其实就相当于是C语言的结构体嵌套,而在写代码的时候可以省略其中父类的嵌套,就像上面的C代码中pPointEx->point.x在C++中可以省略父类的实例引用直接写成pPointEx->x。

2.带有成员函数的类


对于成员函数可能不少朋友会有一定的误解(包括很多工作了多年的人),觉得成员函数跟函数指针会有一定的关联。其实不然,C++的成员函数并没有用到函数指针这玩意。
就1中的类Point来说,不妨先看看她的大小:
class Point{
public:
	int x;
	int y;
};

int main(){
	
	int size = sizeof(Point);

	return 0;
}
运行完int size = sizeof(Point)后,在调试框中可以看到:
		size	8	int
也就是说Point类占8个字节,这也很好理解,Point类中有两个成员变量,x和y各32位4字节,两个一起一共8个字节。
就上面的类,可以给她添加一个成员函数,再来看看他的size:
class Point{
public:
	int x;
	int y;
public:
	void set(int _x, int _y){
		this->x = _x;
		this->y = _y;
	}
};

int main(){
	
	int size = sizeof(Point);

	return 0;
}
		size	8	int
可以发现还是8个字节。
如果成员函数是用函数指针实现的,那么添加成员函数相当于添加函数指针变量,那么类的大小也会随之变化。 这也就是说一个Point类的实例大小不会因为添加成员函数而发生改变,成员变量是用其他的方式实现的。
我们来执行一下这个函数,并看看反汇编的结果:
int main(){
	
	Point * pPoint = new Point;

	pPoint->set(1, 2);

	delete pPoint;

	return 0;
}
	pPoint->set(1, 2);
00CA1AD7 6A 02                push        2  
00CA1AD9 6A 01                push        1  
00CA1ADB 8B 4D F8             mov         ecx,dword ptr [pPoint]  
00CA1ADE E8 02 F7 FF FF       call        Point::set (0CA11E5h)  
当调用函数的时候,形参从右到左依次入栈,然后将pPoint的this指针传递给ecx,最后跳转到set执行。
这也就是说实际上成员函数set(int _x, int _y)出了_x和_y以外,还隐含了一个Point类指针的形参,并利用ecx传递给set函数。
在VC中,成员函数的默认的调用约定是__thiscall。如果参数确定,this 指针存放于 ECX 寄存器,函数自身清理堆栈;如果参数不确定,this指针在所有参数入栈后再入栈,调用者清理栈。__thiscall 不是关键字,程序员不能使用。参数按照从右至左的方式入栈。
但是在纯的C语言中是没有类的this指针的,在C代码中没有办法使用这个调用约定。
我们将CPP代码做一个修改,使用__cdecl的调用约定:
class Point{
public:
	int x;
	int y;
public:
	void __cdecl set(int _x, int _y){
		this->x = _x;
		this->y = _y;
	}
};

int main(){
	
	Point * pPoint = new Point;

	pPoint->set(1, 2);

	delete pPoint;

	return 0;
}
反汇编代码:
	pPoint->set(1, 2);
00991AD7 6A 02                push        2  
00991AD9 6A 01                push        1  
00991ADB 8B 45 F8             mov         eax,dword ptr [pPoint]  
00991ADE 50                   push        eax  
00991ADF E8 06 F7 FF FF       call        Point::set (9911EAh)  
00991AE4 83 C4 0C             add         esp,0Ch  
然后是用于实现这种功能C代码:
typedef struct Point{
	int x;
	int y;
}Point_t;

void __cdecl set(Point_t * _p, int _x, int _y){
	_p->x = _x;
	_p->y = _y;
}

int main(){

	Point_t * pPoint = new Point;

	set(pPoint, 1, 2);

	delete pPoint;

	return 0;
}
对应的反汇编:
	set(pPoint, 1, 2);
00B61417 6A 02                push        2  
00B61419 6A 01                push        1  
00B6141B 8B 45 F8             mov         eax,dword ptr [pPoint]  
00B6141E 50                   push        eax  
00B6141F E8 B2 FD FF FF       call        set (0B611D6h)  
00B61424 83 C4 0C             add         esp,0Ch  
对于有static关键修饰的成员函数,她的本质其实就相当于C语言中普通的函数,她不会隐含this指针。
通过上面的描述,如果联想到windows开发,就能够理解为什么在用类包装windows的窗体类的时候,消息处理函数必须有static修饰。因为普通成员函数会隐含一个类的this指针,这跟消息处理的函数指针声明不一致。

3.成员函数的继承


2中的成员函数的C语言实现方法,能不能满足类继承的要求呢?也就是说我们把子类的实例指针传递给父类的函数,会不会发生错误呢?
直接看C代码:
typedef struct Point{
	int x;
	int y;
}Point_t;

void __cdecl set(Point_t * _p, int _x, int _y){
	_p->x = _x;
	_p->y = _y;
}

typedef struct PointEx{
	Point point;
	int z;	
}PointEx_t;

int main(){

	PointEx_t * pPointEx = new PointEx_t;

	set((Point_t *)pPointEx, 1, 2);

	delete pPointEx;

	return 0;
}

执行完set((Point_t *)pPointEx, 1, 2)以后,查看局部变量:
-		pPointEx	0x002d1d38 {point={...} z=-842150451 }	PointEx *
-		point	{x=1 y=2 }	Point
		x	1	int
		y	2	int
		z	-842150451	int
可以看出x和y结果是正确的。
会不会出现意外呢?
再来看一段代码:
typedef struct PointEx{
	int z;	
	Point point;	
}PointEx_t;
这段代码中先声明的是子类的新成员z然后再声明的父类的一个实例point。
执行后的结果:
-		pPointEx	0x00741d38 {z=1 point={...} }	PointEx *
		z	1	int
-		point	{x=2 y=-842150451 }	Point
		x	2	int
		y	-842150451	int
父可以看出原因在于子类声明的时候,必须首先声明父类的一个实例,否则变量对应的关系会发生错乱。

结语:


原因用纯C语言来实现C++的继承特性就先总结这么多。
下一篇文章总结多肽的继承方法。

你可能感兴趣的:(C++特性的C语言实现(1)继承)