![](/img/trans.png)
[英]in C++, what's the difference between an object and a pointer to an object?
[英]What's the difference between a derived object and a base object in c++?
在c ++中派生對象和基礎對象之間有什么區別,
特別是,當班級中有虛擬功能時。
派生對象是否維護其他表來保存指針
功能?
派生對象繼承基類的所有數據和成員函數。 根據繼承的性質(公共,私有或受保護),這將影響這些數據和成員函數對類的客戶端(用戶)的可見性。
比如說,你私下從A繼承了B,就像這樣:
class A
{
public:
void MyPublicFunction();
};
class B : private A
{
public:
void MyOtherPublicFunction();
};
即使A具有公共功能,B的用戶也不會看到它,例如:
B* pB = new B();
pB->MyPublicFunction(); // This will not compile
pB->MyOtherPublicFunction(); // This is OK
由於私有繼承,A的所有數據和成員函數雖然可用於B類中的B類 ,但對於僅使用B類實例的代碼將不可用。
如果您使用公共繼承,即:
class B : public A
{
...
};
然后,所有A的數據和成員都將對B類用戶可見。 此訪問仍然受到A的原始訪問修飾符的限制,即A中的私有函數永遠不會被B的用戶訪問(或者,對於B類本身的代碼而言)。 此外,B可以重新聲明與A中相同名稱的函數,從而“隱藏”這些函數來自B類的用戶。
至於虛函數,這取決於A是否具有虛函數。
例如:
class A
{
public:
int MyFn() { return 42; }
};
class B : public A
{
public:
virtual int MyFn() { return 13; }
};
如果嘗試通過類型A *的指針在B對象上調用MyFn()
,則不會調用虛函數。
例如:
A* pB = new B();
pB->MyFn(); // Will return 42, because A::MyFn() is called.
但是,假設我們將A更改為:
class A
{
public:
virtual void MyFn() { return 42; }
};
(注意A現在將MyFn()
聲明為虛擬 )
然后這個結果:
A* pB = new B();
pB->MyFn(); // Will return 13, because B::MyFn() is called.
這里調用了MyFn()
的B版本,因為類A已將MyFn()
聲明為虛擬,因此編譯器知道在A對象上調用MyFn()
時它必須在對象中查找函數指針。 或者它認為它是A的對象,就像在這種情況下,即使我們已經創建了一個B對象。
那么對於你的最后一個問題,虛擬函數存儲在哪里?
這是編譯器/系統相關的,但最常用的方法是對於具有任何虛函數(無論是直接聲明,還是從基類繼承)的類的實例,這樣的對象中的第一個數據是'特殊'指針。 此特殊指針指向“ 虛函數指針表 ”,或通常縮寫為“ vtable ”。
編譯器為它編譯的每個具有虛函數的類創建vtable。 所以對於我們的最后一個例子,編譯器將生成兩個vtable - 一個用於類A,一個用於類B.這些表的單個實例 - 對象的構造函數將在每個新創建的對象中設置vtable指針指向到正確的vtable塊。
請記住,具有虛函數的對象中的第一個數據是指向vtable的指針,因此在給定需要調用虛函數的對象的情況下,編譯器始終知道如何查找vtable。 編譯器所要做的就是查看任何給定對象中的第一個內存槽,並且它有一個指向該對象類的正確vtable的指針。
我們的情況非常簡單 - 每個vtable都是一個條目長,所以它們看起來像這樣:
A類的vtable:
+---------+--------------+
| 0: MyFn | -> A::MyFn() |
+---------+--------------+
B類vtable:
+---------+--------------+
| 0: MyFn | -> B::MyFn() |
+---------+--------------+
請注意,對於B
類的vtable, MyFn
的條目已被指向B::MyFn()
的指針覆蓋 - 這確保了當我們調用虛函數MyFn()
甚至在類型A*
的對象指針上,正確調用了MyFn()
的B
版本,而不是A::MyFn()
。
“0”數字表示表格中的條目位置。 在這個簡單的情況下,我們在每個vtable中只有一個條目,因此每個條目都在索引0處。
因此,要在對象(類型A
或B
MyFn()
上調用MyFn()
,編譯器將生成如下代碼:
pB->__vtable[0]();
(注意:這不會編譯;它只是對編譯器將生成的代碼的解釋。)
為了使它更明顯,讓我們說A
聲明另一個函數, MyAFn()
,它是虛擬的,B不會覆蓋/重新實現。
所以代碼是:
class A
{
public:
virtual void MyAFn() { return 17; }
virtual void MyFn() { return 42; }
};
class B : public A
{
public:
virtual void MyFn() { return 13; }
};
然后B將在其界面中具有MyAFn()
和MyFn()
函數,現在vtables將如下所示:
A類的vtable:
+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn | -> A::MyFn() |
+----------+---------------+
B類vtable:
+----------+---------------+
| 0: MyAFn | -> A::MyAFn() |
+----------+---------------+
| 1: MyFn | -> B::MyFn() |
+----------+---------------+
所以在這種情況下,要調用MyFn()
,編譯器將生成如下代碼:
pB->__vtable[1]();
因為MyFn()
在表中是第二個(因此在索引1處)。
顯然,調用MyAFn()
會導致代碼如下:
pB->__vtable[0]();
因為MyAFn()
在索引0處。
應該強調的是,這是依賴於編譯器的,並且iirc,編譯器沒有義務按照聲明的順序對vtable中的函數進行排序 - 這只取決於編譯器使其全部工作。
在實踐中,該方案被廣泛使用,並且vtable中的函數排序是相當確定的,因此維護由不同C ++編譯器生成的代碼之間的ABI,並且允許COM互操作和類似機制跨越由不同編譯器生成的代碼的邊界。 這絕不是保證。
幸運的是,你永遠不必擔心vtable,但是讓你的心理模型變得有意義並且不會在將來給你帶來任何驚喜絕對是有用的。
理論上,如果從另一個派生一個類,則有一個基類和一個派生類。 如果創建派生類的對象,則會有派生對象。 在C ++中,您可以多次從同一個類繼承。 考慮:
struct A { };
struct B : A { };
struct C : A { };
struct D : B, C { };
D d;
在d
對象中,每個D
對象中有兩個A
對象,稱為“基類子對象”。 如果你嘗試轉換D
到A
,那么編譯器會告訴你的轉換是不明確的,因為它不知道到 A
要轉換的對象:
A &a = d; // error: A object in B or A object in C?
如果您將A
的非靜態成員命名為A
,則同樣如此:編譯器將告訴您有關歧義的信息。 在這種情況下,您可以先通過轉換為B
或C
來規避它:
A &a = static_cast<B&>(d); // A object in B
對象d
被稱為“最派生對象”,因為它不是另一個類類型對象的子對象。 為避免上述歧義,您可以虛擬繼承
struct A { };
struct B : virtual A { };
struct C : virtual A { };
struct D : B, C { };
現在,只有一個 A
類子對象,即使你有兩個子對象,這個對象包含在:subobject B
和sub-object C
。 將D
對象轉換為A
現在是非模糊的,因為B
和C
路徑上的轉換將產生相同的A
子對象。
以上是一個復雜的問題:從理論上講,即使不考慮任何實現技術, B
和C
子對象中的任何一個或兩個現在都不再是連續的。 兩者都包含相同的A對象,但兩者都不包含彼此。 這意味着其中一個或兩個必須“拆分”並僅引用另一個的A對象,以便B
和C
對象可以具有不同的地址。 在線性存儲器中,這可能看起來像(假設所有對象都有1個字節的大小)
C: [1 byte [A: refer to 0xABC [B: 1byte [A: one byte at 0xABC]]]]
[CCCCCCC[ [BBBBBBBBBBCBCBCBCBCBCBCBCBCBCB]]]]
CB
是C
和B
子對象包含的內容。 現在,如你所見, C
子對象將被拆分,沒有辦法沒有,因為B
不包含在C
,反之亦然。 使用C
函數中的代碼訪問某些成員的編譯器不能只使用偏移量,因為C
函數中的代碼不知道它是否包含為子對象,或者 - 當它不是抽象時- 它是否是一個派生程度最高的對象,因此它旁邊有一個A
對象。
一個public
冒號。 (我告訴過你C ++很討厭)
class base { }
class derived : public base { }
讓我們:
class Base {
virtual void f();
};
class Derived : public Base {
void f();
}
沒有f是虛擬的(在偽“c”中實現):
struct {
BaseAttributes;
} Base;
struct {
BaseAttributes;
DerivedAttributes;
} Derived;
具有虛擬功能:
struct {
vfptr = Base_vfptr,
BaseAttributes;
} Base;
struct {
vfptr = Derived_vfptr,
BaseAttributes;
DerivedAttributes;
} Derived;
struct {
&Base::f
} Base_vfptr
struct {
&Derived::f
} Base_vfptr
對於多重繼承,事情變得更復雜:o)
Derived是Base,但Base不是Derived
base-是您派生的對象。 derived - 是繼承父親的公共(和受保護)成員的對象。
派生對象可以覆蓋(或在某些情況下必須覆蓋)他父親的一些方法,從而創建不同的行為
基礎對象是從中導出其他對象的對象 。 通常它會有一些虛擬方法(甚至是純虛擬方法),子類可以覆蓋它們以進行特化。
基礎對象的子類稱為派生對象 。
派生對象派生自其基礎對象。
你在詢問各個對象在記憶中的表現嗎?
基類和派生類都有一個指向其虛函數的指針表。 根據已覆蓋的功能,該表中條目的值將更改。
如果B添加了更多不在基類中的虛函數,則B的虛方法表將更大(或者可能存在單獨的表,具體取決於編譯器實現)。
在c ++中派生對象和基礎對象之間有什么區別,
可以使用派生對象代替基礎對象; 它擁有基礎對象的所有成員,也許還有更多自己的成員。 因此,給定一個函數獲取基類的引用(或指針):
void Function(Base &);
您可以將引用傳遞給派生類的實例:
class Derived : public Base {};
Derived derived;
Function(derived);
特別是,當班級中有虛擬功能時。
如果派生類重寫了虛函數,那么即使通過對基類的引用,也會始終在該類的對象上調用被覆蓋的函數。
class Base
{
public:
virtual void Virtual() {cout << "Base::Virtual" << endl;}
void NonVirtual() {cout << "Base::NonVirtual" << endl;}
};
class Derived : public Base
{
public:
virtual void Virtual() {cout << "Derived::Virtual" << endl;}
void NonVirtual() {cout << "Derived::NonVirtual" << endl;}
};
Derived derived;
Base &base = derived;
base.Virtual(); // prints "Derived::Virtual"
base.NonVirtual(); // prints "Base::NonVirtual"
derived.Virtual(); // prints "Derived::Virtual"
derived.NonVirtual();// prints "Derived::NonVirtual"
派生對象是否維護其他表來保存指向函數的指針?
是 - 兩個類都將包含一個指向虛函數表的指針(稱為“vtable”),以便在運行時找到正確的函數。 您不能直接訪問它,但它確實會影響內存中數據的大小和布局。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.