程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++虛函數的一點分析與思考

C++虛函數的一點分析與思考

編輯:C++入門知識

簡介:
以下是自己看過的書籍以及自己思考的流程和總結,主要是對C++虛函數分析了,分析並不算足夠深入,但相信對理解c++的虛函數會有些幫助。現在僅僅寫到了單繼承下的一些皮毛,後面還要繼續挖掘一下,希望自己能以淡定一點的心做好一塊,不負自己。
以下內容適合了解一些C++虛函數以及對指針操作相對來說有點基礎的朋友,因為裡面為了驗證自己的思考進行了很多指針的強轉,下面的測試需要有實際的代碼操作,不希望大家僅僅看結論就算完了,那很沒有意思。我在本地建了一個vs工程,先從最簡單的測試做起,然後一點點的加入代碼.一點點的深入,代碼寫的比較亂,僅僅是為了自己的測試,望朋友包涵一下了。

代碼如下:
Shape.h
log.h
typedef.h
main.cpp


Shape.h:
#ifndef __SHAPE__HEAD_H
#define __SHAPE__HEAD_H


#include "typedef.h"
#include "log.h"


class CShape
{
public:
CShape(){
TRACE_FUCTION_AND_LINE("");
m_color = 15;
}
~CShape(){
TRACE_FUCTION_AND_LINE("");
}
void SetColor(int colore){
TRACE_FUCTION_AND_LINE("");
m_color = colore;
}
protected:
private:
int m_color;
};


class CRect : public CShape
{
public:
CRect(){TRACE_FUCTION_AND_LINE(""); m_width = 0; m_height = 255;}
~CRect(){TRACE_FUCTION_AND_LINE("");}
void PrintMemory(){
TRACE_FUCTION_AND_LINE("this: %p", this);
int *p = (int*)this;
TRACE_FUCTION_AND_LINE("4: %d", *p);
TRACE_FUCTION_AND_LINE("4: %d", *(p+1));
TRACE_FUCTION_AND_LINE("4: %d", *(p+2));
}
protected:
private:
int m_width;
int m_height;
};
#endif


log.h:
#define TRACE_FUCTION_AND_LINE(fmt, ...) printf("[%30s:%4d] "fmt"\n",__FUNCTION__, __LINE__, ##__VA_ARGS__)


typedef.h:
僅僅是一些跨平台的宏定義,就不列出來了,針對我們的問題不起主要作用。


main.cpp:
#include <iostream>
using namespace  std;
#include "Shape.h"


int main()
{
CRect rect1;

TRACE_FUCTION_AND_LINE("sizeof(CShape):%d", sizeof(CShape));
TRACE_FUCTION_AND_LINE("sizeof(CRect):%d, %p", sizeof(CRect), &rect1);
rect1.PrintMemory();
rect1.SetColor(10);
rect1.PrintMemory();
return 0;
}

 


問題1:
派生類和基類的內存如何排布?
通過main.cpp中rect1的打印內存的操作我們可以看出,派生類占用12字節內存,分別是基類的m_color,以及自己的兩個int成員。
基類占有4個字節的內存,SetColor函數本身不占用任何內存。

真理:對象所占用內存包含3個部分,非靜態數據成員(自身的和父類的),vptr(後面介紹),字節對齊。
因此不要武斷的說,c++占用內存比C多,其實就一個vptr的問題,字節對齊在結構體中同樣會出現,字節對齊對上層來說是透明的,因此不用太過於例會。

派生類如何調用基類的非虛public函數,例如本例的SetColor?
1,對於SetColor方式,編譯器會將其編譯成SetColor(int colore, CShape* pShape); rect進行調用的時候采用的純C的方式,也就沒有任何多余的開銷。
rect1.SetColor(color)將會被展開為 SetColor(color, &rect1); 於是rect1的地址被傳入到pShape中,繼而調用pShape->m_color給m_color賦值。
對於編譯器來說,它只看到傳過來的參數地址為&rect1, 它並不知道它的實際類型是什麼,對於任何類型都將會被SetColor強轉為CShape的類型,於是這就引出一個問題,編譯器怎麼知道實際的rect1的m_color地址偏移是多少呢?實際上它壓根就不知道它是CRect類型.在SetColor這個函數指令運行的時候,它僅僅是根據傳入pShape的地址,以CShape的方式偏移特定的地址(對於本例子偏移為0),然後賦值。可以判斷對於子類CRect來說, 也是以偏移為0的地址進行賦值的;換句話說,子類對象的內存有一塊內存是父類的,而且父類的內存必須在內存塊的前半部分,要不對rect1的地址偏移為0的地址賦值時,就有可能賦值到另一個數據成員上,而不是m_color。

2,如何驗證此想法?
1)根據上面例子的內存打印可以看出,rect1的m_color的內存確實在地址的前半部分。
2)可以給SetColor傳遞一個假的CShape類型,觀測其是否確實是對假的對象的前四個字節賦值?
測試代碼如下:
struct FakeCRect{
int fake1;
int fake2;
int fake3;
int fake4;
}fakerect = {1,1,1,1};
TRACE_FUCTION_AND_LINE("fakerect:%d, %d, %d, %d", fakerect.fake1, fakerect.fake2, fakerect.fake3, fakerect.fake4);
CRect* pfakerect = (CRect*)&fakerect;
pfakerect->SetColor(20);
TRACE_FUCTION_AND_LINE("fakerect:%d, %d, %d, %d", fakerect.fake1, fakerect.fake2, fakerect.fake3, fakerect.fake4);

打印結果如下:
main:  23] fakerect:1, 1, 1, 1
main:  26] fakerect:20, 1, 1, 1
可以看到確實是第一個字節變了,傳入一個假的CRect類型,它依然是對其第一個int變量處理,你甚至可以傳遞一個char型的數組來測試都行。

3,拓展延伸,這種情況的例外。
1)上面的例子沒有考慮帶有虛函數的情況,現在給父類和子類分別加入一個虛函數,display方法。子類繼承父類的虛函數,並重寫此方法。
virtual void display(){
TRACE_FUCTION_AND_LINE("");//打印當前函數的名字和行號,便於判斷是調用哪一個類的display方法。
}
這個時候可以看到,父類和子類的對象內存大小改變了,分別增加了四個字節,分別是8, 16,而且根據子類的PrintMemory可以清楚的看到添加的內存是占用的對象最前面四個字節,其他不變。虛函數機制使得使用基類的指針指向不同的對象來實現多態成為現實。pShape->display();
通過pShape指向不同的對象, 將會調用不同方法,可以再創建一個CCircle類繼承於CShape類來觀測這種結果。
CShape* pShape = new CCircle();
pShape->display();//調用CCircle的display方法
pShape = new CRect;
pShape->display();//調用CShape的display方法
如果你還不是很清楚虛函數的用法或者說你不打算深入探究虛函數的實現原理,建議一下內容就不要看了。
只要一個類有虛函數(繼承下來的或者是本身的),它就會有一個vptr,vptr是一個指針,執行一個vbtl虛函數表。你可以把vbtl想象成一個指針數組,它的數組的元素是一個個的函數指針,指向它自己的虛函數,一般還會有一個指向typeinfo的結構的指針,以實現運行時刻類型識別。
簡單說來,現在CRect有一個虛函數display,那麼他的vbtl會有兩個元素,一個是typeinfo指針,一個是dispaly方法指針。

2)簡單看看typeinfo:
CShape* pShape = &rect1;
if(typeid(rect1) == typeid(CRect) && typeid(*pShape) == typeid(CRect)){
TRACE_FUCTION_AND_LINE("rect1 is type CRect");
TRACE_FUCTION_AND_LINE("rect1 name:(%s) raw_name:(%s)", typeid(rect1).name(), typeid(rect1).raw_name());
}
通過上述代碼可以清楚看到可以在運行時候判斷一個對象時什麼類型,即使將rect1的地址轉換父類指針,依然可以判斷出它實際是什麼類型。
typeinfo是一個類一個,編譯器編譯的時候靜態靜態分配,每一個帶有虛函數的類的對象都會有一個指針指向它,同一個類的對象指針應該一致下面開始測試這一猜想。
看有些書上說的是typeinfo的結構指針位於vtbl虛表的第0個item上,但是我在vc++編譯調試沒能找到,第0個item上是虛函數display的地址。
於是又改在仿linux環境,MINGW下測試,在第-1個item選項上找到了typeinfo的地址,很是欣慰。但是在windows上還是始終找不到,猜測是在-1的item選項上,不過這個選項應該還有其他的東西,我這麼推斷的主要原因是除了-1和0item這兩個位置,它們的前後地址都是0. 只是-1這個item的位置應該被微軟又封裝了一下,而不是單純的指向typeinfo結構。
以下為測試代碼:
const type_info* ptypeidinfo = &(typeid(rect1));
TRACE_FUCTION_AND_LINE("ptypeidinfo: %p", ptypeidinfo);
int *p = (int*)&rect1;
int *pp = (int*)(p[0]);//vptr
type_info *prectinfo = (type_info*) (*(pp-1));//pp-0: virtual function address
TRACE_FUCTION_AND_LINE("prectinfo: %p", prectinfo);
vs2008輸出:
main:  36] ptypeidinfo: 003A9004
main:  40] prectinfo: 003A7748
MINGW輸出:
main:  36] ptypeidinfo: 004085a4
main:  40] prectinfo: 004085a4
在vs2008上對pp-2和pp+1都查看了,都是0地址,因此可以猜測typeinfo肯定跟pp-1有關,只是被封裝了而已。

3)回到問題 (上面的例子沒有考慮帶有虛函數的情況):
父類和子類都有一個虛函數,這樣每一個子類對象的父類部分也就自動向下偏移,因此可以看到現在的fakerect是第二個int變量被改變了,這個比較容易理解,對於SetColor方法來說,它的參數是CShape,那麼他就CShape的m_color的偏移地址(此處為4)進行賦值。試想另外一種情況,父類沒有虛函數,子類有虛函數,這樣父類占用的內存為4,子類的內存占用仍有vptr的4個字節內存。
測試:可以將父類的display虛函數注釋掉,打印結果如下:
fakerect:1, 1, 1, 1
fakerect:1, 20, 1, 1
此時父類對象部分就不是位於子類對象起始部分,子類對象最起始是vptr,而SetColor還根據它本身的參數CShape類型來進行偏移。對於CShape來說,它看到的僅僅是四個字節的m_color,它對m_color進行賦值的時候,就會對pShape指針的最開始四個字節賦值,然而傳遞過去的fackrect對象的首地址卻是fake1變量,而被改變的確是fake2變量,這個到底是怎麼回事呢??
傳遞過去的首地址是fake1,SetColor改變也是傳遞過去的首地址,而最終被改變的是fake2,這個肯定是有一些內部轉換機制在作怪,猜測應該講傳遞過去的( CRect* pfakerect = (CRect*)&fakerect; pfakerect->SetColor(20);) pfakerect指針在被強轉為CShape*的時候地址被自動偏移了4個字節,下面進行測試:

CShape類的SetColor方法,打印this指針地址:
void CShape::SetColor(int colore)
{
//TRACE_FUCTION_AND_LINE("");
TRACE_FUCTION_AND_LINE("pShape:%p", this);
m_color = colore;
}
main.cpp程序,打印傳遞過去的pfakerect地址:
CRect* pfakerect = (CRect*)&fakerect;
TRACE_FUCTION_AND_LINE("fakerect:%p", pfakerect);
pfakerect->SetColor(20);
輸出結果:
           main:  27] fakerect:002DF958
CShape::SetColor:  12] pShape:002DF95C
 可以清楚的看到傳遞過去的地址是002DF958,而setColor在運行的時候處理的首地址卻是002DF95C,這個應該是pfakerect轉化為pShape時候的自動偏移的。
 
 為了更清楚的感覺到這種變化,現在直接在main.cpp加入測試代碼:
main.cpp:
  CRect* pfakerect = (CRect*)&fakerect;
TRACE_FUCTION_AND_LINE("fakerect:%p", pfakerect);
pfakerect->SetColor(20);
CShape* pfakeshape = pfakerect;
TRACE_FUCTION_AND_LINE("pfakeshape:%p", pfakeshape);
輸出結果如下:
           main:  27] fakerect:003EF77C
CShape::SetColor:  12] pShape:003EF780
           main:  30] pfakeshape:003EF780
可以看出fakerect的地址為003EF77C,在SetColor的時候進行強轉的時候偏移了四個字節,當然我們通過CShape* pfakeshape = pfakerect直接進行強轉的時候,還是會偏移4個字節。
結論:
對於SetColor這種很純粹的方法來說,它位於CShape類,它僅僅看到CShape類的成員,進行成員賦值的時候,它也是根據自身的類的描述結果進行賦值,但是在將CRect*賦值給CShape*的時候,編譯器會查看這個類的描述,發現CShape類沒有虛函數,CRect類有虛函數,因此在CRect*轉換為CShape*的時,編譯器會進行自動4個字節的偏移。我認為這個應該是在編譯器編譯的時候自動做好的。由於我現在還沒有達到能看懂編譯器對代碼所生成的目標代碼的地步,也就沒有繼續向下追蹤,如果你可以讀懂它生成的目標代碼,可以跟蹤一下,是不是對於pfakerect轉換為pfakeshape這一句的目標代碼中,pfakeshape已經自動進行了偏移4個字節,我認為應該是編譯器編譯期的行為。


想做一個簡單總結了:
OK,通過以上描述(說的比較亂,本身語言表達能力不行,還是邊寫邊思考邊測試的,望你理解,有問題可留言),上面的基本的討論大部分都是針對普通成員變量m_color的賦值,非虛函數SetColor的運行機制。
1,首先講述了繼承的父類和子類的內存排布,主要包含了普通成員變量,vptr,以及字節對齊的內存(欲驗證字節對齊的朋友,可以在CRect去掉虛函數再加一個double的成員變量就可以看到CRect的大小變化了。),也許你會問靜態數據成員是不是位於對象裡面,可以明確的答復你,不會,靜態對象成員是位於類,不是位於對象的,因此不可能每一個對象存儲一份copy。上面的三種其實就是整個對象的內存占用。
2,然後講述了普通成員函數SetColor的運行機制,它被編譯成的目標代碼實際上是以C的機制進行的,在進行rect1.SetColor(10);這樣的調用的時候,實際上也是以C的方式進行的調用SetColor(10, &rect1),這個通過加入一個普通的SetColor函數,與它對比做了驗證,還測試了時間消耗,也相差不大。還討論了通過fakerect也確定SetColor的運行機制,它就可以當成很普通的C賦值,不管你傳入的實際類型是什麼,它所要做的就是對傳遞過來的地址進行一定m_color的偏移,然後賦值。
3,然後又討論了拓展延伸一部分,簡單描述了虛函數,虛函數對於對象內存大小的影響。一不小心又扯遠了,講到了typeinfo結構,講到了typeinfo同樣是占用了vptr所指向的vbtl指針表的一個表項,可以通過類型的強轉從vbtl中讀取typeinfo的結構與通過typeid取到的結構的地址進行對比,討論了Vs2008的不一致和MINGW的一致,之所以可以比較,是基於一個類的typeinfo只有一份內存來進行的。每一個對象(含有虛函數的)會存有一個指針指向它(以實現運行期的動態類型識別),typeid可以獲取到它(這個應該是編譯器設置的,編譯器就分配好typeinfo的內存,然後給typeid指令返回它的地址)。
4,然後又討論了父類對象無虛函數,子類對象有虛函數,在進行SetColor的時候,內部機制到底是如何運行的。再次驗證了對於SetColor來說,它只根據自己自身對象的CShape結果來決定對m_color所進行的偏移,對於父類沒有虛函數,子類有虛函數,於是將CRect*賦值給CShape*時外部負責默認偏移了4個字節的地址,這樣可以保證SetColor賦值時僅僅考慮自身的CShape的結構,那麼這個SetColor就可以在編譯期根據自身的結構來對m_color進行自身的偏移,編譯生成一份唯一的目標代碼,外部傳遞的參數來保證傳遞過來的有效性。於是我們看到了將CRect*結構傳遞給CShape*的時候,外部負責進行4個字節的偏移傳遞給SetColor的pShape,對於SetColor方法來說,它只考慮自身的結構。還闡述了這個是編譯器的工作,不過僅僅是個人猜測,理論上會是這樣,為了高效而言。
5,簡單測試了一下,對於偏移和不偏移的情況,cpu耗費時間基本相同,可以大概確定是編譯期進行的偏移,運行期進行偏移的話,時間消耗應該會大的多。不過這個需要能觀測到目標代碼的大牛來查看確定。


---------------------------2013/5/12 12:22:33---------------------------------------------------------
1,對虛函數的著迷,讓我禁不住再看看虛函數表:
1)既然虛函數表中有一個虛函數位於上述的pp[0]的位置,那麼我們可否取出來嘗試調用一下以驗證一些正確性呢,說實話,有點不合常理,不過上
述的取typeinfo的時候,不就是那麼的不合常理嗎?好東西就是用來折騰的。
main.cpp加入以下代碼:
const type_info* ptypeidinfo = &(typeid(rect1));
TRACE_FUCTION_AND_LINE("ptypeidinfo: %p", ptypeidinfo);
int *p = (int*)&rect1;
int *pp = (int*)(p[0]);//vptr
type_info *prectinfo = (type_info*) (*(pp-1));//virtual function address
TRACE_FUCTION_AND_LINE("prectinfo: %p", prectinfo);

typedef void* pDisplayFunc(CRect* rect);
TRACE_FUCTION_AND_LINE("test begin:");
pDisplayFunc* pfunc = (pDisplayFunc*)(*pp);
pfunc(&rect1);
TRACE_FUCTION_AND_LINE("test end..");
pp是vptr的值,它指向的是一個vtbl,因此可以取出它的表的第0項,*pp就是它的第0項的值,它是一個函數指針,是指向display函數,因此根據上面的討論可以判斷它的C函數原型應該是void display(CRect*);的,故而將它強轉為pDisplayFunc,然後通過pfunc(&rect1)進行調用,然後你會發現它真的就是調用的CRect的display方法。
輸出如下,可以看到確實調用的CRect的display方法:
[                          main:  47] test begin:
[                CRect::display:  44]
[                          main:  50] test end..
2)再玩點傳參的吧
Shape.h中CRect類再加入一個虛函數:
virtual void display1(int i){
TRACE_FUCTION_AND_LINE("i=%d", i);
}
main.cpp:
typedef void* pDisplayFunc1(int i, CRect* rect);
TRACE_FUCTION_AND_LINE("test begin:");
pDisplayFunc1* pfunc1 = (pDisplayFunc1*)(*(pp+1));
(*pfunc1)(8888, &rect1);
TRACE_FUCTION_AND_LINE("test end..");
前面關於虛函數表還沒有詳細說明,再加一個虛函數,它的位置是位於第1個item表項上,可以通過取*(pp+1)獲得,調用傳遞的參數為8888.
打印結果如下:
[                          main:  53] test begin:
[               CRect::display1:  47] i=8888
不過我的vs2008隨後報告了內存訪問錯誤了,不知道為什麼,但是可以看到函數還是調用了,證明預想不錯。采用MINGW仿linux倒沒有問題正常打印出了結果,應該兩個環境的安全檢查程度不一樣。


3)再玩點C函數的感覺
函數的地址應該是編譯期確定,不知道如何打印出來?
再看個更洋氣的:既然pfunc1是display1的函數地址,那麼我完全就不需要傳入&rect1的參數了,傳遞一個NULL驗證猜想。在(*pfunc1)(8888, &rect1);下面再加上一個調用(pfunc1)(8899, NULL);   如我所願,還是正常打印出了8899,
打印結果(先注釋掉前面那一句話,因為此方法引起內存訪問錯誤):
[                          main:  53] test begin:
[               CRect::display1:  47] i=8899
4)繼續新花樣,更改vtbl看看效果。
將vtbl的第0個item和第1個item的內容交換一下,那麼再調用display會發生什麼情況呢?迫不及待啊。
很不幸,由於內存保護不讓寫,很是郁悶。但是有什麼東西能夠阻擋我們的興趣呢?你保護,我自己制造一個rect,自己制造一個vtbl。基本思想是創建一個rectbuf的char數組保存rect1的內存,創建一個rectvtbl保存vtbl的項目,而且將rectbuf數組的前四個字節變成一個vptr指針指向rectvtbl的item0地址,由於這個vbtl表是我們自己的內存,因此是可以訪問的,然後交換一下rectvtbl表項的兩個函數指針的值即可.
測試中發現將display和display1交換,vs2008總是說內存訪問錯誤,猜想是因為兩個函數一個有參數一個無參數導致的,於是又增加了一個虛函數display2,它和display1的內容一模一樣。
Shape.cpp:
virtual void display2(int i){
TRACE_FUCTION_AND_LINE("i=%d", i);
}
main.cpp:
char rectbuf[sizeof(rect1)];
memcpy(rectbuf, &rect1, sizeof(rect1));//分配內存保存rect1的內存
char rectvtbl[sizeof(int*) * 4];
memcpy(rectvtbl, pp-1, sizeof(int*)*4);//分配內存報錯rectvtbl的內存
int *prectbuf = (int*)rectbuf;
int *pvtbl = (int*)((int*)rectvtbl + 1);//將pvtbl指向虛表的item0項目,因為前四個字節是typeinfo指針
prectbuf[0] = (int)(pvtbl);

CRect* fakecharRect = (CRect*)rectbuf;
//fakecharRect->display();
TRACE_FUCTION_AND_LINE("before deal: fakecharRect.display.....");
fakecharRect->display1(100);
fakecharRect->display2(100);//打印,正常調用。先調用display1,再調用display2

long temp = *(pvtbl+1);
*(pvtbl+1) = *(pvtbl+2);
*(pvtbl+2) = temp;//交換虛表的item1和item2項目,也就是display1和display2

TRACE_FUCTION_AND_LINE("after deal: fakecharRect.display.....");
//fakecharRect->display();
fakecharRect->display1(100);
fakecharRect->display2(100);//打印,驚訝調用。先調用display2,再調用display1(交換表項目所致)
輸出結果如下:
[                          main:  61] before deal: fakecharRect.display....
[               CRect::display1:  54] i=100
[               CRect::display2:  57] i=100
[                          main:  69] after deal: fakecharRect.display.....
[               CRect::display2:  57] i=100
[               CRect::display1:  54] i=100
[                 CRect::~CRect:  34]
從日志打印的函數名字可以看到沒有處理之前先調用diplay1,在調用display2。處理之後就變了。
上面的測試可以驗證幾個結論,不要把對象之類的東西看的過於神秘了,對象就是內存,你可以通過CRect rect1來創建對象,你也可以通過char數組來創建對象,完全取決於你。函數是什麼?函數就是一個地址而已,那個地址是編譯時確定的。由於我們知道了虛函數的一些機制,虛函數表項目中的內存實際代表的是哪一個函數,我們就可以通過交換表項目的內存,來進行一些匪夷所思的操作,將display1函數實際上調用的是display2,如果你想,可以自己隨便寫一個函數,然後將函數地址賦值到表的項目中,然後當你進行類的diplay函數調用的時候,你會驚訝的發現它竟然調用的是一個外部C函數,這個絕對會另你周圍的人驚訝不已。當然玩這個可不是 為了嘩眾取寵,理解它可以讓我們更懂得控制我們程序的行為,當程序出現一些百思不得解的詭異狀況時,仔細思考一下程序的內部執行邏輯,你就會豁然開朗(相信我,你碰到的一般都不會涉及到如此的深入)。

想總結一下了:
OK,通過虛函數的描述,讓我們知道了可以通過取虛函數表項目來進行函數調用,它可以如真實的C函數一樣,我們可以通過更改虛函數的表項來操縱函數的調用,可以交換,可以給虛函數表項賦值。這樣你將不再對虛函數感到很大的神秘性,它其實就是一個函數指針數組,裡面存儲著一堆當前類的虛函數而已。通過存儲的虛函數可以達到運行時刻的動態調用。對於非虛函數,是編譯器確定具體調用哪一個函數,因此會在目標代碼中直接寫死調用。還有前面說到不知道取類的函數地址,後面突然想到了一個方式,測試了一下還行。
代碼如下:
char buf[100];
sprintf(buf, "%d", &(CRect::PrintMemory));//sprintf的目的主要是因為編譯器編譯期禁止我對函數指針強轉為int,我繞了一個彎,做到了。
*(pvtbl) = atoi(buf);
TRACE_FUCTION_AND_LINE("after set CRect::PrintMemory function......");
fakecharRect->display();//打印,驚訝調用。調用PrintMemory方法
確定如此,調用的PrintMemory方法。此外我還簡單測試了一下不同CRect對象的vptr,確實都是一樣的,一個類一份內存。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved