换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的

这篇具有很好参考价值的文章主要介绍了换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

相信很多关于C++的笔试面试题里都有这样的题目:C++中一个空对象为什么还要占用一个字节空间?(或者C++的一个空对象占多少字节空间)

这篇文章我们来分析下为什么是这样的,继承空基类,组合空基类,空基类优化和使用场景。

sizeof空基类

示例1

#include<iostream>  
using namespace std;  
class Base {};  
int main()
{ 
	cout << sizeof(Base) << endl;  
    return 0;
}

先看一个实例输出结果:

1

为什么sizeof得到的的结果是1,而不是0或者4,8呢?

其实C++标准里规定对象的大小必须大于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. ”,什么意思呢?意思是说最终派生对象大小是非0值,其大小可以是1或多个字节,基类子对象可以为0。
sizeof操作符中也有相关规定“The size of a most derived class shall be greater than zero ”,可见规定的确规定了空对象大小不能为0的现实,但却不强制其大小一定为1(这为编译器为不同的操作系统进行优化留有余地,比如说在不同字长[8,16,32,64]位CPU下,最有效率的操作数类型都是CPU的字长,那么编译器可以选择机器字长来作为不为0时的最小长度)。 

 那么我们尝试看看继承关系下,继承空对象的子类又是怎么样的呢?

空基类优化

示例2:

#include<iostream>  
using namespace std;  
class Base {};  

class Derived : Base {
};

int main()
{ 
	cout << sizeof(Base) << endl; 
	cout <<  sizeof(Derived) << endl;
}

刚才我们都知道当一个空类用作基类时,不需要为它分配空间,前提是它不会导致它被分配到与另一个相同类型的对象或子对象相同的地址。

这里将输出

1

1

因此我们这里引入了一个空基类优化的概念,如果编译器实现了空基优化,它将为每个类打印相同的大小,但这些类都没有大小为零。这意味着在Derived类中,Base类没有任何空间。还要注意,具有优化的空基(没有其他基)的空类也是空的。这就解释了为什么类Base也可以具有与类Derived相同的大小。所以他们的内存布局应该是这样的:

                          换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的

如果编译器没有实现空基优化,它将打印不同的大小,而且内存布局因该是这样的:

                                         换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的

刚才我们说的是继承空基类关系的情况,那么如果基类是空基类,同时他又作为被派生类的成员的时候,空基类占的内存是没法被优化掉的, 

示例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;
}

这段代码,你觉得输出结果是什么?3个sizeof得到的结果相同吗?为什么?

实际上空基类优化对数据成员没有起作用,所以这里Derived1仍然是占用1字节,Derived2如果按照4字节对齐,那么应该是占8字节。

如果一个类或者他的基类中包含虚函数,那么该类就不是空类,因为通常一个含有虚函数的类,都有一个虚函数指针,所以就有了数据成员,虽然是隐藏的,因此,你看下这段代码,看看sizeof会是多少?

示例4:

#include<iostream>  
using namespace std;  
class Base {
public:
	virtual void fun() {
	}
}; 

int main()
{
	std::cout << "sizeof(Base):  " << sizeof(Base) << endl; 
	return 0;
}

关于虚函数和虚函数表,虚函数指针的相关内容,我将在专栏后续分享。

通过这几个简单的例子我们就发现了

以组合方式包含的空类A,导致整个类对象的大小有着近翻倍的增长。反而是以继承的方式会减少很多,

其实我们的很多STL容器都是通过继承空间配置器类别以分配空间,你会发现这些容器都不会在容器类中内含一个allocator,一般都是用继承的方式,这样通过空基类可以省下几个字节的空间。

空基类使用场景和优化

因此空基类优化对于模板库而言是一个重要的优化方案,STL中很多时候引入基类的时候都只是为了引入一些新的类型别名或者额外的函数功能,而不会增加新的数据成员。

今天从一个符合类型STL的元组谈起,浅谈c++中空基类优化的使用。

STL中的元组允许有不同类型的元素在一起,因此我们可以想象下,假设有这么几种元素:

class EmptyA {
public:
	EmptyA() {}
};
class EmptyB {
public:
	EmptyB () {}
}; 

需要放到元组中,我们通过模板(如果你对模板还不是很熟悉,不要紧,后边我还会给大家分享模板一些有趣的用法)定义一个元组:

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<> { 
};

 我们来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;  
}

得到的结果是3,我们其实可以看下内存布局应该是这样的

                      换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的

 那么我们还能节省空间吗?

 有人想到了私有继承(别急,私有继承的入坑用法同样的我会在专栏里计划更新),因为私有继承通常意味着 is implemented in terms of 的一种关系,而不是 is-a 。因此私有继承能够实现空基类的最优化。

那么我们试下:

#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得到的结果比刚才还小了一个字节,那么他对应的内存布局是下面这样的。

换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的

当然,我相信很多技术开发都有一种精益求精的态度,我们还能进一步优化MyTuple吗?使得sizeof是1??

可以的,因为我们刚才使用私有继承的方式,减少了一次MyTuple<_Rest...> tail;所带来的额外空间,那么我们是不是可以继续利用私有继承,将所有的元组元素都通过私有继承的方式来压缩空间?所以我们需要多重继承,为了防止多个元组可能存在类型相同的情况,我们增加一个MyTupleElement类, 只要MyTupleElement可以安全的从T进行继承的话,就让MyTupleElement私有继承T ,这样当 T是空类时,EBO就发挥效果了。 

#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"; 
}

 运行得到的结果是1

换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的

 最后的这段代码也就是STL标准模板库里的tuple的一个简要的原型。

C++20引入no_unique_address

从c++20起,若空成员子对象使用属性 [[no_unique_address]],则允许像空基类一样优化掉它们。取这种成员的地址会产生可能等于同一个对象的某个其他成员的地址。[[no_unique_address]]指示此数据成员不需要具有不同于其类的所有其他非静态数据成员的地址。这表示若该成员拥有空类型(例如无状态分配器),则编译器可将它优化为不占空间,正如同假如它是空基类一样。若该成员非空,则其中的任何尾随填充空间亦可复用于存储其他数据成员。 

来看示例

示例5

#include<iostream>  
using namespace std;  
class Base {};
 
class Derived1 {
	int i;
	Base c;
};
//空基类优化
class Derived2 :private Base {
	int i;
};
//使用no_unique_address进行空基类优化
class Derived3 {
	int i;
    [[no_unique_address]]Base b;
};
//使用no_unique_address进行空基类优化
class Derived4 {
	int i;
    [[no_unique_address]]Base b1,b2;//这里b1 与 i 共享同一地址,因为b1标记有 [[no_unique_address]],但b2不能共享,所以b2不能被优化。
    // 然而,其中一者可以与 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 是使用空基类优化的情况,Derived3则使用 [[no_unique_address]]进行空基类优化,所以得到的结果应该是

换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的

 这里,给各位读者一个思考题,如下输出的结果是多少?为什么?文章来源地址https://www.toymoban.com/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; 
}

到了这里,关于换个花样玩C++(5)玩转空类,空类不是一个sizeof=1就这么简单就能讲完的的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • C++中sizeof()、size()、strlen()、length()详解

    1、size 是一个 函数 ,它是在程序运行时才会计算, 用来求数组或容器中元素的个数 。 在字符串string中它与length的作用相同,只是length只能应用于string中,而不能应用于STL的容器中 2、sizeof 为 运算符 , 其结果是求对应参数的字节大小 ,它的值是在编译的时候就计算完成了

    2024年04月16日
    浏览(31)
  • C++面试八股文:了解sizeof操作符吗?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第10面: 面试官:了解 sizeof 操作符吗? 二师兄:略微了解(不就是求大小的嘛。。) 面试官:请讲以下如何使用 sizeof ? 二师兄: sizeof 主要是求变量或者类型的大小。直接使用 sizeof(type) 或 sizeof(var) 即可。 面试官:嗯。 s

    2024年02月08日
    浏览(30)
  • C++结构体内幕揭秘:sizeof之谜与内存布局探秘

      概述: C++结构体的`sizeof`不总是等于每个成员的`sizeof`之和,因为对齐和填充影响了内存布局。未对齐的结构体可能存在间隙,而对齐的结构体会插入填充以保持对齐。通过示例展示了结构体的内存对齐和填充,以及如何使用模板元编程打印结构体成员的偏移量,深入理解

    2024年03月23日
    浏览(30)
  • C++求字符串长度————sizeof()、size()、strlen()以及length()详解

    一、区分sizeof()和strlen() 首先, sizeof() 【操作数所占空间的字节数大小】是一种c中的 基本运算符 。(是操作符,并不是函数)可以以类型、指针、数组和函数等作为参数。 它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。由于在编译时计算,因此 sizeof() 不

    2024年02月02日
    浏览(45)
  • 深入解析C++中sizeof和strlen的奥秘:区别、应用与技巧全揭秘!

      sizeof  和  strlen  是 C++ 中用于处理字符串的两个不同的操作符,它们的作用和使用场景有很大的区别。 sizeof  是一个运算符,不是一个函数,用于获取一个类型或变量的字节大小。 对于数组, sizeof  返回整个数组的字节大小。 对于指针, sizeof  返回指针本身的字节大小

    2024年01月25日
    浏览(43)
  • Git 将一个分支完全覆盖(不是合并)到另一个分支

    Git 将一个分支完全覆盖(不是合并)到另一个分支 案例: 将dev分支的代码完全覆盖到master上      1.git checkout master      2.git reset --hard dev      3.git push origin master --force 注意:若被覆盖的分支有处于受保护(protected)的状态,则执行push操作后会报错:      1. remote: GitLab: Y

    2024年02月01日
    浏览(75)
  • 换个角度看境外支付系统:警惕金融风险之安全测试实践

    支付系统,这个名词相信生活在当下社会的大家应该都不在陌生了吧,他时时刻刻充斥在我们的日常生活中,哪里有交易发生,哪里就有它的身影。 其实直白的来说, 支付系统是扮演着连接消费者、商家、银行和其他金融机构之间的桥梁角色。 对于支付系统的质量保障活动

    2024年03月24日
    浏览(33)
  • Java判断一个实体是不是空的

    在Java中,我们可以使用以下方法来判断一个实体是否为空: 对象是否为null 可以使用Java中的 == 运算符来判断一个对象是否为null,如果对象为null,则表示对象为空。 例如: 字符串是否为空 可以使用Java中的 isEmpty() 方法来判断一个字符串是否为空,如果字符串为空,则返回

    2024年02月13日
    浏览(36)
  • C语言判断一个矩阵是不是对称矩阵案例讲解

    我们先看对称矩阵的例图:  通过观察对称矩阵图片我们可以得出以下结论: 1)对称矩阵以主对角线为对称轴,对应位置的数字相等。也就是:aij=aji 2)如果一个矩阵是对称矩阵,那么他的转置矩阵等于他本身。 以上文对称矩阵例图为例进行代码编写。 案例代码如下: 代码

    2024年02月08日
    浏览(38)
  • 教育新花样?看智慧教育如何出“花样”

    智慧教育是物联化、智能化、感知化、泛在化的新型教育形态和教育模式。数字孪生可视化作为智慧教育的应用之一,优化了教育发展形态。本文以智慧教育浙江大学项目为例,介绍智慧教育的具体应用场景。 智慧校园行业是我国教育事业的重要分支,也是信息化建设的重要

    2024年02月15日
    浏览(27)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包