剛看完了《深度探索C++物件模型》第三章,這里做一下總結,也寫一下我自己在g++ 7.5.0上的驗證,
本文中所有的源檔案都可以在這里拿到(百度網盤鏈接),
注意,這里所說的“物件”是指在C++中使用class或struct關鍵字創建的類的實體,
無繼承情況下的C++物件記憶體布局
首先當然是從最基礎的情況來講,在沒有繼承的情況下的C++物件記憶體布局是什么樣的?這又分為兩種:無虛函式和有虛函式,
無虛函式
C++類內成員變數分為兩類:static成員變數和非static成員變數,static成員變數不在類的實體的內部,在整個記憶體中只有一份,只需要使用類名即可訪問;而非static成員變數在類的實體內部,需要為其分配空間,
在這種情況下C++的物件和C的結構體是一樣的,畢竟要實作和C的兼容,主要就是結構體/類內成員變數的對齊,
其一般規則總結如下:
- 所有成員按照在類內的宣告順序在記憶體中排列;
// test00.cpp
#include <iostream>
int main();
class Test00 {
friend int main();
public:
int i1;
private:
int i2;
public:
int i3;
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << showOffset(Test00, i1) << std::endl;
std::cout << showOffset(Test00, i2) << std::endl;
std::cout << showOffset(Test00, i3) << std::endl;
}
// Output:
// 0
// 4
// 8
- 任一非
static成員變數的偏移(offset)要是其大小的倍數;
// test01.cpp
#include <iostream>
struct Test01 {
char c;
int i; // 如果緊湊排列,則i的偏移為1,但i的size為4,偏移要是4的倍數,因此i的偏移為4
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << showOffset(struct Test01, i) << std::endl;
}
// Output:
// 4
- 結構體整體的size需要為最大非
static成員變數size的倍數;
// test02.cpp
#include <iostream>
// 如果緊湊排,Test02_1的size應為9,
// 但要與int(size為4)對其,所以其size為12
struct Test02_1 {
char c1; // Offset: 0
int i; // Offset: 4
char c2; // Offset: 8
};
// Test02_2成員和Test02_1相同,但順序不同,
// 受規則2和3影響,其size為8
struct Test02_2 {
char c1; // Offset: 0
char c2; // Offset: 1
int i; // Offset: 4
};
int main() {
std::cout << "sizeof Test02_1: " << sizeof(Test02_1) << std::endl;
std::cout << "sizeof Test02_2: " << sizeof(Test02_2) << std::endl;
}
// Output:
// sizeof Test02_1: 12
// sizeof Test02_2: 8
- 空物件的size為1,為了保證每個物件都有唯一的記憶體位置(memory location)
// test03.cpp
#include <iostream>
struct Test03 {}; // Empty class
int main() {
Test03 a, b;
std::cout << "sizeof Test03: " << sizeof(Test03) << std::endl;
if (&a == &b)
std::cerr << " Error! &a == &b, at " << static_cast<void*>(&a) << std::endl;
else
std::cout << "a and b has different address, &a = " << static_cast<void*>(&a) << " and &b = " << static_cast<void*>(&b) << std::endl;
}
// Output:
// sizeof Test03: 1
// a and b has different address, &a = 0x7fffe62e8486 and &b = 0x7fffe62e8487
- 當類(
class)/結構體(struct) A 作為一個類B的內部成員變數時,其對齊要求為類A內部最大的對齊要求;
// test04.cpp
#include <iostream>
// 規則2中的類,size為8,對齊要求為4
struct Test01{
char c;
int i;
};
struct Test04 {
char c; // Offset: 1, size 1
Test01 t; // Offset: 4, size 8
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << "Offset of t in struct Test04: " << showOffset(Test04, t) << std::endl;
std::cout << "sizeof Test04: " << sizeof(Test04) << std::endl;
}
// Output:
// Offset of t in struct Test04: 4
// sizeof Test04: 12
- 空的類(empty class)A作為作為一個類B的成員變數時,類A占用一個位元組的空間,對其要求也為1;
// test05.cpp
#include <iostream>
// 規則4中的類,空類,size = 1
struct Test03 {};
struct Test05 {
char c; // Offset: 0, size: 1
Test03 t; // Offset: 1, size: 1
};
#define showOffset(ClassName, memberName) (reinterpret_cast<unsigned long>( &(static_cast<ClassName*>(nullptr)->memberName)))
int main() {
std::cout << "Offset of t in struct Test04: " << showOffset(Test05, t) << std::endl;
std::cout << "sizeof Test05: " << sizeof(Test05) << std::endl;
}
// Output:
// Offset of t in struct Test04: 1
// sizeof Test05: 2
有虛函式
C++使用虛函式來實作多型,非虛函式不展現多型性,當呼叫非虛函式時,只要呼叫一個寫死的地址即可,無論是使用物件呼叫還是使用指標/參考呼叫;而當使用指標/參考呼叫虛函式需要視其系結到的實際物件來呼叫對應的虛函式,以展現多型性(用物件呼叫虛函式不展現多型性),
而C++實作虛函式用到的便是虛表,所謂虛表,就是保存該類所有虛函式地址的一張表,一個類的某個確定的虛函式在虛表的確定位置,而類實體中有一個虛表指標指向該虛表,當出現類繼承并覆寫(override)了該虛函式時,只需要將虛表指標指向另一張虛表,該虛表中對應位置的函式指標換為新的函式即可,另外,一個類的所有物件共享同一張虛表,因此不會帶來大的記憶體消耗,該虛表由編譯器生成,
這里只是對于虛函式和虛表進行了簡單的描述,詳細可查詢網路資源,這里不再贅述,
就像上面所說,相比于沒有虛函式的類,由虛函式的類的實體只是多了一個指向虛表的指標,其放在類的開頭或者結尾(g++將其放在類的開頭),大小和對其要求視平臺而定,在x86-64平臺上,虛表指標大小和對其要求為8位元組,
// test06.cpp
class Point {
public:
Point(int x)
:m_x(x)
{}
virtual
int getX()
{ return m_x; }
private:
int m_x;
};
int main() {
Point p(1);
int x = p.getX();
}

使用gdb觀察,可以看到Point類實體p的size為16,包括size為8的虛表指標和size為4的int型別的成員變數m_x,同時,由于虛表指標的對其要求為8,所以Point的size必須是8的倍數,所以其size為16,
同時查看p的記憶體布局,可以看到虛表指標被放置于類實體的頭部,占用8個位元組,后面緊跟4個位元組的int型別的成員變數m_i,最后填充了4個位元組以使類Point的size為8的倍數,

我們在查看一下虛表指標指向的記憶體,我這里使用的是64位系統和程式,所以函式指標是8位大小,虛表指標指向的虛表的第一個表項是地址0x080007b2,同時查看反匯編,因為我們使用物件來呼叫虛函式,不展現多型性,這里直接call了Point::getX()的地址,可以看到其地址為0x080007b2,正好是前面虛表的第一個表項,
還有就是Point型別對應的typeinfo物件的地址,在《深度探索C++物件模型》中提到其位于虛表的第一個表項,但前面我們看到虛表第一個表項存放的是虛函式,那typeinfo的地址放在哪里呢?我們來找一下,
// file test07.cpp
#include <typeinfo>
2 class Point {
3 public:
4 Point(int x)
5 :m_x(x)
6 {}
7
8 virtual
9 int getX()
10 { return m_x; }
11
12 private:
13 int m_x;
14 };
15
16 int main() {
17 Point p(1);
18 auto& ti = typeid(p);
19 int x = p.getX();
20 }

可以看到反匯編中保存了0x8200da8這一地址到堆疊上,再結合我們的原始碼,很可能gdb所提示的<_ZTI5Point>這一物件就是Point類的typeinfo物件,我們使用工具c++filt來看_ZTI5Point這個被修飾過的符號是什么含義,不出所料,正是Point類對應的typeinfo物件,
liuyun@DESKTOP-Q5AT31V:/tmp/test/cppObjectModel/chap03/blog$ c++filt _ZTI5Point
typeinfo for Point
既然Point對應的typeinfo物件的地址為0x8200da8,我們查看虛表附近的地址,發現虛表指標指向的地址的前面的一個QWORD的內容正好是typeinfo的地址,那是不是虛表指標指向的并不是虛表的開頭,而是第一個虛函式所在的地址,而在虛表中,第一個虛函式這一表項前面便是該類對應的typeinfo的地址?

在查閱資料的時候,《C++虛函式之二:虛函式表與虛函式呼叫》這篇博客提到g++支持-fdump-class-hierarchy這一編譯選項,可以生成一個名為{source_file_name}.002t.class的檔案,檔案中詳細記錄了各個類的資訊,包括其虛表資訊,
正如我們所想,如果我們使用vptr指代虛表指標,那么vptr[0]就是第一個虛函式的地址,vptr[-1]則是該類對應的typeinfo的地址,而在最前面,g++還填充了一個空的表項,
最后還有一個問題,再沒有虛函式的時候,編譯器為了讓每一個物件都有自己獨一無二的地址,會在物件中插入一個位元組占位,而在有虛函式的時候類中會有一個原生的虛表指標vptr,從而至少占8位元組大小(x86-64上),那么是否就不需要再插入一個位元組了呢?事實正如我們所想,Test08類的size為8而不是16,
// test08.cpp
#include <iostream>
class Test08 {
public:
virtual
int getNumber() { return s_i++; }
private:
static int s_i;
};
int Test08::s_i = 0;
int main() {
std::cout << "sizeof Test08: " << sizeof(Test08) << std::endl;
Test08 t;
int i = t.getNumber();
}
// Output:
// sizeof Test08: 8
這一篇博客就先寫到這里,下一篇再談談在繼承體系下g++是如何實作C++物件的記憶體布局的,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/521765.html
標籤:C++
上一篇:c++執行期語意
下一篇:C++物件模型:g++實作(二)
