相信很多關(guān)于C++的筆試面試題里都有這樣的題目:C++中一個(gè)空對(duì)象為什么還要占用一個(gè)字節(jié)空間?(或者C++的一個(gè)空對(duì)象占多少字節(jié)空間)
這篇文章我們來(lái)分析下為什么是這樣的,繼承空基類(lèi),組合空基類(lèi),空基類(lèi)優(yōu)化和使用場(chǎng)景。
sizeof空基類(lèi)
示例1
#include<iostream>
using namespace std;
class Base {};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
先看一個(gè)實(shí)例輸出結(jié)果:
1
為什么sizeof得到的的結(jié)果是1,而不是0或者4,8呢?
其實(shí)C++標(biāo)準(zhǔn)里規(guī)定對(duì)象的大小必須大于0,“An object is a region of storage. ”。
“a most derived object shall have a non-zero size and shall occupy one or more bytes of storage. Base class sub-objects may have zero size. ”,什么意思呢?意思是說(shuō)最終派生對(duì)象大小是非0值,其大小可以是1或多個(gè)字節(jié),基類(lèi)子對(duì)象可以為0。
sizeof操作符中也有相關(guān)規(guī)定“The size of a most derived class shall be greater than zero ”,可見(jiàn)規(guī)定的確規(guī)定了空對(duì)象大小不能為0的現(xiàn)實(shí),但卻不強(qiáng)制其大小一定為1(這為編譯器為不同的操作系統(tǒng)進(jìn)行優(yōu)化留有余地,比如說(shuō)在不同字長(zhǎng)[8,16,32,64]位CPU下,最有效率的操作數(shù)類(lèi)型都是CPU的字長(zhǎng),那么編譯器可以選擇機(jī)器字長(zhǎng)來(lái)作為不為0時(shí)的最小長(zhǎng)度)。?
?那么我們嘗試看看繼承關(guān)系下,繼承空對(duì)象的子類(lèi)又是怎么樣的呢?
空基類(lèi)優(yōu)化
示例2:
#include<iostream>
using namespace std;
class Base {};
class Derived : Base {
};
int main()
{
cout << sizeof(Base) << endl;
cout << sizeof(Derived) << endl;
}
剛才我們都知道當(dāng)一個(gè)空類(lèi)用作基類(lèi)時(shí),不需要為它分配空間,前提是它不會(huì)導(dǎo)致它被分配到與另一個(gè)相同類(lèi)型的對(duì)象或子對(duì)象相同的地址。
這里將輸出
1
1
因此我們這里引入了一個(gè)空基類(lèi)優(yōu)化的概念,如果編譯器實(shí)現(xiàn)了空基優(yōu)化,它將為每個(gè)類(lèi)打印相同的大小,但這些類(lèi)都沒(méi)有大小為零。這意味著在Derived類(lèi)中,Base類(lèi)沒(méi)有任何空間。還要注意,具有優(yōu)化的空基(沒(méi)有其他基)的空類(lèi)也是空的。這就解釋了為什么類(lèi)Base也可以具有與類(lèi)Derived相同的大小。所以他們的內(nèi)存布局應(yīng)該是這樣的:
? ? ? ? ? ? ? ? ? ? ? ? ??
如果編譯器沒(méi)有實(shí)現(xiàn)空基優(yōu)化,它將打印不同的大小,而且內(nèi)存布局因該是這樣的:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
剛才我們說(shuō)的是繼承空基類(lèi)關(guān)系的情況,那么如果基類(lèi)是空基類(lèi),同時(shí)他又作為被派生類(lèi)的成員的時(shí)候,空基類(lèi)占的內(nèi)存是沒(méi)法被優(yōu)化掉的,?
示例3
#include<iostream>
using namespace std;
class Base {};
class Derived1 {
Base c;
};
class Derived2 {
int i;
Base c;
};
class Derived3 : Base {
int i;
};
int main()
{
std::cout << "sizeof(Derived1): " << sizeof(Derived1) << endl;
std::cout << "sizeof(Derived2): " << sizeof(Derived2) << endl;
std::cout << "sizeof(Derived3): " << sizeof(Derived3) << endl;
}
這段代碼,你覺(jué)得輸出結(jié)果是什么?3個(gè)sizeof得到的結(jié)果相同嗎?為什么?
實(shí)際上空基類(lèi)優(yōu)化對(duì)數(shù)據(jù)成員沒(méi)有起作用,所以這里Derived1仍然是占用1字節(jié),Derived2如果按照4字節(jié)對(duì)齊,那么應(yīng)該是占8字節(jié)。
如果一個(gè)類(lèi)或者他的基類(lèi)中包含虛函數(shù),那么該類(lèi)就不是空類(lèi),因?yàn)橥ǔR粋€(gè)含有虛函數(shù)的類(lèi),都有一個(gè)虛函數(shù)指針,所以就有了數(shù)據(jù)成員,雖然是隱藏的,因此,你看下這段代碼,看看sizeof會(huì)是多少?
示例4:
#include<iostream>
using namespace std;
class Base {
public:
virtual void fun() {
}
};
int main()
{
std::cout << "sizeof(Base): " << sizeof(Base) << endl;
return 0;
}
關(guān)于虛函數(shù)和虛函數(shù)表,虛函數(shù)指針的相關(guān)內(nèi)容,我將在專(zhuān)欄后續(xù)分享。
通過(guò)這幾個(gè)簡(jiǎn)單的例子我們就發(fā)現(xiàn)了
以組合方式包含的空類(lèi)A,導(dǎo)致整個(gè)類(lèi)對(duì)象的大小有著近翻倍的增長(zhǎng)。反而是以繼承的方式會(huì)減少很多,
其實(shí)我們的很多STL容器都是通過(guò)繼承空間配置器類(lèi)別以分配空間,你會(huì)發(fā)現(xiàn)這些容器都不會(huì)在容器類(lèi)中內(nèi)含一個(gè)allocator,一般都是用繼承的方式,這樣通過(guò)空基類(lèi)可以省下幾個(gè)字節(jié)的空間。
空基類(lèi)使用場(chǎng)景和優(yōu)化
因此空基類(lèi)優(yōu)化對(duì)于模板庫(kù)而言是一個(gè)重要的優(yōu)化方案,STL中很多時(shí)候引入基類(lèi)的時(shí)候都只是為了引入一些新的類(lèi)型別名或者額外的函數(shù)功能,而不會(huì)增加新的數(shù)據(jù)成員。
今天從一個(gè)符合類(lèi)型STL的元組談起,淺談c++中空基類(lèi)優(yōu)化的使用。
STL中的元組允許有不同類(lèi)型的元素在一起,因此我們可以想象下,假設(shè)有這么幾種元素:
class EmptyA {
public:
EmptyA() {}
};
class EmptyB {
public:
EmptyB () {}
};
需要放到元組中,我們通過(guò)模板(如果你對(duì)模板還不是很熟悉,不要緊,后邊我還會(huì)給大家分享模板一些有趣的用法)定義一個(gè)元組:
template <typename ...Types>
class MyTuple;
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...>
{
_This val;
MyTuple<_Rest...> tail;
public:
MyTuple(_This const& v, _Rest const& ... rest)
: val(v), tail(rest...) {}
};
template <>
class MyTuple<> {
};
?我們來(lái)sizeof求下MyTuple<A,B>的大小
#include <iostream>
class EmptyA {
public:
EmptyA() { std::cout << "EmptyA\n"; }
};
class EmptyB {
public:
EmptyB() { std::cout << "EmptyB\n"; }
};
template <typename ...Types>
class MyTuple;
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...>
{
_This val;
MyTuple<_Rest...> tail;
public:
MyTuple(_This const& v, _Rest const& ... rest)
: val(v), tail(rest...) {}
};
template <>
class MyTuple<> {
};
int main() {
MyTuple<EmptyA, EmptyB> t(EmptyA{}, EmptyB{});
std::cout << sizeof(t) << std::endl;
}
得到的結(jié)果是3,我們其實(shí)可以看下內(nèi)存布局應(yīng)該是這樣的
? ? ? ? ? ? ? ? ? ? ??
?那么我們還能節(jié)省空間嗎?
?有人想到了私有繼承(別急,私有繼承的入坑用法同樣的我會(huì)在專(zhuān)欄里計(jì)劃更新),因?yàn)樗接欣^承通常意味著?is implemented in terms of?的一種關(guān)系,而不是?is-a?。因此私有繼承能夠?qū)崿F(xiàn)空基類(lèi)的最優(yōu)化。
那么我們?cè)囅拢?/p>
#include <iostream>
class EmptyA {
public:
EmptyA() { std::cout << "EmptyA\n"; }
};
class EmptyB {
public:
EmptyB() { std::cout << "EmptyB\n"; }
};
template <typename ...Types>
class MyTuple;
template <>
class MyTuple<> {
public:
MyTuple() {
}
};
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...> : private MyTuple<_Rest...>
{
_This val;
public:
MyTuple() : val() {}
};
int main() {
MyTuple<EmptyA, EmptyB> t;
std::cout << sizeof(t) << "\n";
}
這次果然如你所愿,sizeof得到的結(jié)果比剛才還小了一個(gè)字節(jié),那么他對(duì)應(yīng)的內(nèi)存布局是下面這樣的。
當(dāng)然,我相信很多技術(shù)開(kāi)發(fā)都有一種精益求精的態(tài)度,我們還能進(jìn)一步優(yōu)化MyTuple嗎?使得sizeof是1??
可以的,因?yàn)槲覀儎偛攀褂盟接欣^承的方式,減少了一次MyTuple<_Rest...> tail;所帶來(lái)的額外空間,那么我們是不是可以繼續(xù)利用私有繼承,將所有的元組元素都通過(guò)私有繼承的方式來(lái)壓縮空間?所以我們需要多重繼承,為了防止多個(gè)元組可能存在類(lèi)型相同的情況,我們?cè)黾右粋€(gè)MyTupleElement類(lèi),?只要MyTupleElement可以安全的從T進(jìn)行繼承的話(huà),就讓MyTupleElement私有繼承T?,這樣當(dāng) T是空類(lèi)時(shí),EBO就發(fā)揮效果了。?
#include <iostream>
class EmptyA {
public:
EmptyA() { std::cout << "EmptyA\n"; }
};
class EmptyB {
public:
EmptyB() { std::cout << "EmptyB\n"; }
};
template <typename... Types>
class MyTuple;
template <size_t Index, typename T, bool = std::is_class_v<T> && !std::is_final_v<T>>
class MyTupleElement;
template <>
class MyTuple<> {
};
template <class _This, class... _Rest>
class MyTuple<_This, _Rest...> :
private MyTuple<_Rest...>,
private MyTupleElement<sizeof...(_Rest), _This>
{
};
template <size_t Index, typename T>
class MyTupleElement<Index, T, true> : private T {
};
template <size_t Index, typename T>
class MyTupleElement<Index, T, false> {
T val;
};
int main() {
MyTuple<EmptyA, EmptyB> t;
std::cout << sizeof(t) << "\n";
}
?運(yùn)行得到的結(jié)果是1
?最后的這段代碼也就是STL標(biāo)準(zhǔn)模板庫(kù)里的tuple的一個(gè)簡(jiǎn)要的原型。
C++20引入no_unique_address
從c++20起,若空成員子對(duì)象使用屬性 [[no_unique_address]],則允許像空基類(lèi)一樣優(yōu)化掉它們。取這種成員的地址會(huì)產(chǎn)生可能等于同一個(gè)對(duì)象的某個(gè)其他成員的地址。[[no_unique_address]]指示此數(shù)據(jù)成員不需要具有不同于其類(lèi)的所有其他非靜態(tài)數(shù)據(jù)成員的地址。這表示若該成員擁有空類(lèi)型(例如無(wú)狀態(tài)分配器),則編譯器可將它優(yōu)化為不占空間,正如同假如它是空基類(lèi)一樣。若該成員非空,則其中的任何尾隨填充空間亦可復(fù)用于存儲(chǔ)其他數(shù)據(jù)成員。?
來(lái)看示例
示例5
#include<iostream>
using namespace std;
class Base {};
class Derived1 {
int i;
Base c;
};
//空基類(lèi)優(yōu)化
class Derived2 :private Base {
int i;
};
//使用no_unique_address進(jìn)行空基類(lèi)優(yōu)化
class Derived3 {
int i;
[[no_unique_address]]Base b;
};
//使用no_unique_address進(jìn)行空基類(lèi)優(yōu)化
class Derived4 {
int i;
[[no_unique_address]]Base b1,b2;//這里b1 與 i 共享同一地址,因?yàn)閎1標(biāo)記有 [[no_unique_address]],但b2不能共享,所以b2不能被優(yōu)化。
// 然而,其中一者可以與 c 共享地址。
};
int main()
{
std::cout << "sizeof(Derived1): " << sizeof(Derived1) << endl;
std::cout << "sizeof(Derived2): " << sizeof(Derived2) << endl;
std::cout << "sizeof(Derived3): " << sizeof(Derived3) << endl;
std::cout << "sizeof(Derived4): " << sizeof(Derived4) << endl;
}
這里我們Derived2 是使用空基類(lèi)優(yōu)化的情況,Derived3則使用 [[no_unique_address]]進(jìn)行空基類(lèi)優(yōu)化,所以得到的結(jié)果應(yīng)該是
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-437340.html
?這里,給各位讀者一個(gè)思考題,如下輸出的結(jié)果是多少?為什么?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-437340.html
示例6
#include<iostream>
using namespace std;
class Base {};
class Base2 {};
class Derived1 {
int i;
[[no_unique_address]]Base b1;
[[no_unique_address]]Base2 b2;
};
class Derived2 {
int i;
[[no_unique_address]]Base b1,b2;
[[no_unique_address]]Base2 b3,b4;
};
int main()
{
std::cout << "sizeof(Derived1): " << sizeof(Derived1) << endl;
std::cout << "sizeof(Derived2): " << sizeof(Derived2) << endl;
}
到了這里,關(guān)于換個(gè)花樣玩C++(5)玩轉(zhuǎn)空類(lèi),空類(lèi)不是一個(gè)sizeof=1就這么簡(jiǎn)單就能講完的的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!