C++/C#的区别之在构造函数中调用虚函数

来源:互联网 发布:韩国高清网络电视直播 编辑:程序博客网 时间:2024/05/22 00:16

今天在Essential C# 2.0里面看到的一段话:

In C++, methods called during construction will not dispatch the virtual method. Instead, during construction, the type is associated with the base type rather than the derived type, and virtual methods call the base implementation. In contrast, C# dispatches virtual method calls to the most derived type. This is consistent with the principal of calling the most derived virtual member, even if the derived constructor has not completely executed.

引发的下面的故事:

结论:

1. 上面的那段话是正确的(好像这个结论比较废话)

2. C++中的指针,永远只是一个指针,一个32bit的值,它不随他指向的类型变化而变化。

故事过程:

为了验证上面的两个结论,我用C++和C# 分别写了3个类,继承关系:

验证1:

C++

1 class RootClass
2 {
3 public:
4 RootClass()
5 {
6 TestFunc();
7 }
8 virtual void TestFunc()
9 {
10 cout<<"RootClass::TestFunc"<<endl;
11 }
12 };
13
14 class Level1DerivedClass: public RootClass
15 {
16 public:
17 Level1DerivedClass()
18 {
19 TestFunc();
20 }
21 virtual void TestFunc()
22 {
23 cout<<"Level1DerivedClass::TestFunc"<<endl;
24 }
25 };
26 class Level2DerivedClass:public Level1DerivedClass
27 {
28 public:
29 Level2DerivedClass()
30 {
31 TestFunc();
32 }
33 virtual void TestFunc()
34 {
35 cout<<"Level2DerivedClass::TestFunc"<<endl;
36 }
37 };
38
39
40 int main()
41 {
42
43 Level2DerivedClass* pl2c = new Level2DerivedClass();
44 RootClass* pRc = new RootClass();
45
46 pRc = pl2c;
47 pRc->TestFunc();
48
49 delete pl2c;
50 pl2c = NULL;
51 pRc = NULL;
52
53 system("pause");
54 return 0;
55 }

C#

1 class RootClass
2 {
3 public RootClass()
4 {
5 TestFunc();
6 }
7 public virtual void TestFunc()
8 {
9 Console.WriteLine("RootClass::TestFunc");
10 }
11 }
12
13 class Level1DerivedClass : RootClass
14 {
15 public Level1DerivedClass()
16 {
17 TestFunc();
18 }
19 public override void TestFunc()
20 {
21 Console.WriteLine("Level1DerivedClass::TestFunc");
22 }
23 }
24 class Level2DerivedClass : Level1DerivedClass
25 {
26 public Level2DerivedClass()
27 {
28 TestFunc();
29 }
30 public override void TestFunc()
31 {
32 Console.WriteLine("Level2DerivedClass::TestFunc");
33 }
34 }
35 class Program
36 {
37 static void Main(string[] args)
38 {
39
40 Level2DerivedClass l2c = new Level2DerivedClass();
41 RootClass rc = new RootClass();
42
43 Console.WriteLine();
44
45 rc = l2c;
46 rc.TestFunc();
47 Console.Read();
48 }
49 }

C++ 结果

C#结果

分析:

无争议过程:先调基类构造函数,在执行本类构造函数内的函数。

C++/C#区别:

C++在调用构造函数的时候,如果调用了虚函数,那么,这个调用过程,不会去寻找继承类的该函数重写。(有点绕口:看结果截图:Root构造时,只调用它自己的TestFunc,不会去寻找继承类的TestFunc)

C#在调用构造函数的时候,如果调用了许函数,那么,基类回去检查继承类是否重写了这个函数,如果有,就调用。(截图里可以看出, Root构造的时候,调L2的TestFunc, L1构造的时候,也调用L2的TestFunc)

这里是从表现上证明了文章开始的那段话。

但是为什么呢?我根据我的理解如下,在基类的构造函数中调用虚函数 A:

C++ 不会直接调用派生类的重写方法A; C# 会查找派生类是否重写了方法A,如果重写了,那么调用派生类的方法A。这里就出现了一个比较“Tricky”的问题。

这里我设置了一个比较特殊的虚函数A, 这个虚函数A, 对基类中的一些数据成员m_val做了修改。

C++说:我这个基类的数据成员必须在这个虚函数A里面进行操作!因此,我的构造函数在调用这个虚函数的时候,必须调用自己的A函数体,而不是继承类的A函数体。

C#说:我的这个基类的成员调用了虚函数,从多态的角度上看,我必须要检查我的派生类是否重写了这个函数。如果重写了,必须要调用派生类的这个函数!!因此,我调用了派生类的A函数。

所以,如果在C#中,m_val是一个很关键的数据,没有被正常赋值而造成了崩溃。但是C++似乎有违背了多态的特性...所以,为了避免这样的问题出现,我觉得一个比较稳妥的办法是,别在构造函数里面调用虚函数。如果因为代码太长,需要用函数的话,也不要把这个函数定义成虚函数...后患无穷阿...

验证2:

C++中,类指针就是一个指针,不是别的!!

还是上面例子中的3个类,但是main函数有点儿区别:

1 //======= Instance Assignment
2 Level2DerivedClass l2c;
3 RootClass rc;
4 cout<<endl;
5
6 rc = l2c;
7 rc.TestFunc();
8
9 cout<<endl;
10
11 //======= Class ptr assignment
12 Level2DerivedClass* pl2c = new Level2DerivedClass();
13 RootClass* pRc;
14
15 cout<<endl;
16
17 pRc = pl2c;
18 pRc->TestFunc();
19 20 delete pl2c;
21 pl2c = NULL;
22 pRc = NULL;

这两次调用的唯一区别是 用实例名调用和类指针调用。

而出来的结果却是截然不同的:

下面做一下分析:

实例对象调用函数:

1。因为12c/rc都是类实例,因此分别调用了构造函数。

2。r2 = l2c。 这里就是出现区别的地方。咱们看一下汇编码:

1 50: Level2DerivedClass l2c;
2 001B1ADD lea ecx,[ebp-14h]
3 001B1AE0 call Level2DerivedClass::Level2DerivedClass (1B1299h)
4 51: RootClass rc;
5 001B1AE5 lea ecx,[ebp-20h]
6 001B1AE8 call RootClass::RootClass (1B113Bh)
7 。。。
8 54: rc = l2c;
9 001B1B08 lea eax,[ebp-14h]
10 001B1B0B push eax
11 001B1B0C lea ecx,[ebp-20h]
12 001B1B0F call RootClass::operator= (1B12A3h)
13 55: rc.TestFunc();
14 001B1B14 lea ecx,[ebp-20h]
15 001B1B17 call RootClass::TestFunc (1B116Dh)

在r2 =l2c的时候,调用了r2 的默认类赋值操作符函数

RootClass::operator= (1B12A3h)

事实上,

1)。如果r2 和l2c不是继承关系,编译器会在编译时报错。

2)。在我们没有重写默认类赋值操作符函数的时候,赋值操作符不会做任何操作。这一点,内存可以证明:

(赋值前后的内存变化,左边是rc的指针,右边是l2c的指针)

3。如果修改一下代码: rc = (RootClass)l2c,他就会调用默认拷贝构造函数....然后调用了默认赋值操作符重写:

00CB1B08 lea eax,[ebp-14h]

00CB1B0B push eax

00CB1B0C lea ecx,[ebp-128h]

00CB1B12 call RootClass::RootClass (0CB12A8h)

00CB1B17 push eax

00CB1B18 lea ecx,[ebp-20h]

00CB1B1B call RootClass::operator= (0CB12A3h)

4。因为上面一步的赋值操作没有任何变化,所以,调用TestFunc的时候,就会直接调用RootClass的函数。输出自然就像截图所示。

类指针调用函数:

1 Level2DerivedClass* pl2c = new Level2DerivedClass();
2 00CB1B43 push 4
3 00CB1B45 call operator new (0CB1217h)
4 00CB1B4A add esp,4
5 00CB1B4D mov dword ptr [ebp-110h],eax
6 00CB1B53 mov dword ptr [ebp-4],0
7 00CB1B5A cmp dword ptr [ebp-110h],0
8 00CB1B61 je main+0D6h (0CB1B76h)
9 00CB1B63 mov ecx,dword ptr [ebp-110h]
10 00CB1B69 call Level2DerivedClass::Level2DerivedClass (0CB1299h)
11 00CB1B6E mov dword ptr [ebp-130h],eax
12 00CB1B74 jmp main+0E0h (0CB1B80h)
13 00CB1B76 mov dword ptr [ebp-130h],0
14 00CB1B80 mov eax,dword ptr [ebp-130h]
15 00CB1B86 mov dword ptr [ebp-11Ch],eax
16 00CB1B8C mov dword ptr [ebp-4],0FFFFFFFFh
17 00CB1B93 mov ecx,dword ptr [ebp-11Ch]
18 00CB1B99 mov dword ptr [ebp-2Ch],ecx
19 61: RootClass* pRc;
20 。。。
21 65: pRc = pl2c;
22 00CB1BB7 mov eax,dword ptr [ebp-2Ch]
23 00CB1BBA mov dword ptr [ebp-38h],eax
24 66: pRc->TestFunc();
25 00CB1BBD mov eax,dword ptr [ebp-38h]
26 00CB1BC0 mov edx,dword ptr [eax]
27 00CB1BC2 mov esi,esp
28 00CB1BC4 mov ecx,dword ptr [ebp-38h]
29 00CB1BC7 mov eax,dword ptr [edx]
30 00CB1BC9 call eax
31 00CB1BCB cmp esi,esp
32 00CB1BCD call @ILT+455(__RTC_CheckEsp) (0CB11CCh)

  

1。调用了L2类的构造函数,因为new了

2。因为在程序中,只是声明RootClass的类指针,所以不调用任何构造函数。

3 。然后再看看赋值操作:因为他们都是指针,所以直接赋值!!

65: pRc = pl2c;

00CB1BB7 mov eax,dword ptr [ebp-2Ch]

00CB1BBA mov dword ptr [ebp-38h],eax

(左边是pRc的指针,右边是pl2c)

4。因此在pRc->TestFunc()的时候,就是用L2的类指针调用了实例方法。

结论,指针赋值就是指针指的赋值,类型检查交给编译器。类赋值是需要深度拷贝才能实现的类复制的。多态的实现是靠指针实现的...

恩。写完收工......

虽然都是很基础的东西,但是还是需要好好理解阿...

有板砖的拍过来啊...

(注:以上均为32位机器程序结果)