C/C++ sizeof(下)

sizeof 作用于基本数据类型,在特定的平台和特定的编译器中,结果是确定的,如果使用 sizeof 计算构造类型:结构体、共用体和类的大小时,情况稍微复杂一些。

1.计算结构体

考察如下代码:

struct S1 {
    char c;
    int i;
};
cout<<sizeof(S1)=<<sizeof(S1)<<endl;

sizeof(S1) 结果是 8,并不是想象中的 sizeof(char)+sizeof(int)=5。这是因为结构体或类成员变量具有不同类型时,需进行成员变量的对齐。《计算机组成原理》一书中说到对齐的目的是减少访存指令周期,提高 CPU 存储速度。

1.1 内存对齐原则

内存对齐原则与编译器实现相关,一般而言,需满足三个准则:

(1)结构体变量的首地址能够被其最宽基本成员类型大小所整除;

(2)结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要,编译器会在成员之间加上填充字节;

(3)结构体的总大小为结构体最宽基本成员类型大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

有了以上三个内存对齐的原则,就可以轻松应对嵌套结构体类型的内存对齐。

struct S2 {
	char c1;
    S1 s;
    char c2;
};

在寻找 S2 的最宽基本数据类型时,包括其嵌套的结构体中的成员,从S1中寻找出最宽结构体数据类型是int,因此S2的最宽数据类型是int。S1 s 在结构体 S2 中的对齐也遵守前三个准则,因此sizeof(S2)=sizeof(char)+pad(3)+sizeof(S1)+1+pad(3)=1+3+8+1+3=16字节,其中 pad(3) 表示填充 3 个字节。

结构体某个成员相对于结构体首地址的偏移量可以通过宏 offsetof() 来获得,这个宏也在 stddef.h 中定义,如下:

#define offsetof(s,m) (size_t)&(((s *)0)->m)

例如获得 S1 中的偏移量,方法为

size_t pos = offsetof(S1, i); 	//pos等于4	

1.2 修改对齐方式

1.2.1 #pragma pack

#pragma pack(n)中n为字节对齐数,其取值为1、2、4、8、16,默认是8。结构体对齐时,
(1)成员的偏移量为成员本身大小和n二者最小值的整数倍;
(2)结构体最终大小是结构体中最宽基本类型成员大小和n二者中的最小值的整数倍。

考察如下代码:

#pragma pack(push) //将当前pack设置压栈保存
#pragma pack(2)    //必须在结构体定义之前使用
struct S1 {
    char c;
    int i;
};
struct S2 {
    char c1;
    S1 s;
    char c2
};
#pragma pack(pop) // 恢复先前的pack设置

//或者
#pragma pack(2)
...
#pragma pack()

因此,sizeof(S2)=sizeof(char)+pad(1)+sizeof(S1)+1+pad(1)=1+1+6+1=10字节。

注意,#pragma pack 不能指定变量的存储地址,变量的首地址默认为最大基本成员类型大小的整数倍。

1.2.2 __declspec(align(#))

VC++支持 __declspec(align(#)),在 GNU C++ 并不支持。# 的取值为 1~8192,为 2 的幂。使用示例如下:

__declspec(align(256)) struct TestSize {
	char a;
	int i;
};
cout<<sizeof(TestSize)<<endl;	// 输出256

__declspec(align(#)) 要求 # 为 2 的整数次幂,作用主要有两个方面:

(1)使结构体或类成员按 #pragma pack 确定内存布局之后,在末尾填充内存使得整个对象的大小至少是 # 的整数倍。

(2)作用于变量时,强制要求编译器将变量放置在地址是#整数倍的内存位置上。这点在调用原生API等要求严格对齐的方法时十分重要。

1.3 空结构体

C/C++中不允许长度为0的数据类型存在。对于“空结构体”(不含数据成员)的大小不为 0,而是 1。“空结构体”变量也得被存储,这样编译器也就只能为其分配一个字节的空间用于占位了。

struct S3 {};
sizeof(S3); // 结果为1

1.4 位域结构体

有些信息在存储时,并不需要占用一个完整的字节, 而只需占一个或多个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位即可表示。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为"位域"或"位段"。包含位域变量的结构体叫作位域结构体。

位域结构体的定义形式:

struct 位域结构体名 { 
	类型说明符 位域名:位域长度;
	...
};

注意,位域长度不应该大于该类型说明符对应的数据类型的位长度。
使用位域的主要目的是压缩存储,其大致规则为:
(1)如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;
(2)如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
(3)如果相邻位域字段的类型不同,则各编译器的具体实现有差异,VC++采取不压缩方式,GNU C++采取压缩方式;
(4)如果位域字段之间穿插着非位域字段,则不进行压缩;
(5)整个结构体的总大小为最宽基本类型成员大小的整数倍;
(6)位域可以无位域名,这时它只用作填充或调整位置,不能使用。例如:

struct BitFiledStruct { 
	int a:1;
	int :2;	//该2位不能使用
	int b:3;
	int c:2;
 };

关于位域结构体的 sizeof 大小,考察如下代码:

#include <iostream>
using namespace std;

struct BFS1 {
    char f1 : 3;
    char f2 : 4;
    char f3 : 5;
};
struct BFS2 {
    char f1 : 3;
    int i : 4;
    char f2 : 5;
};
struct BFS3 {
    char f1 : 3;
    char f2;
    char f3 : 5;
};

int main() {
	cout<<sizeof(BFS1)<<endl;
	cout<<sizeof(BFS2)<<endl;
	cout<<sizeof(BFS3)<<endl;
}

运行上面的程序,VC++ 和 GNU C++ 输出结果如下:

// VC++输出结果
2
12
3

// GNU C++输出结果
2
4
3

考察以上代码,得出:
(1)sizeof(BFS1)==2。当相邻位域类型不同,在 VC++中sizeof(BFS2)=1+pad(3)+4+1+pad(3)=12,采用不压缩方式,位域变量i的偏移量需要是4的倍数,并且位域结构体BFS2的总大小必须是sizeof(int)的整数倍。在GNU C++中为sizeof(BFS2)=4,相邻的位域字段的类型不同时,采取了压缩存储,位域变量i紧随位域变量f1的剩余位进行存储,位域变量f2同样是紧随位域变量i的剩余位进行存储,并且位域结构体BFS2的总大小必须是sizeof(int)的整数倍,所以最终结果sizeof(BFS2)=1+pad(3)=4。

(2)sizeof(BFS3)==3,当非位域字段穿插在其中,不会产生压缩,在VC++和GNU C++中得到的大小均为3,如果压缩存储,则sizeof(BFS3)==2。

2.计算共用体

结构体在内存组织上是顺序式的,共用体则是重叠式,各成员共享一段内存,所以整个共用体的 sizeof 也就是每个成员sizeof的最大值。结构体的成员也可以是构造类型,这里构造类型成员是被作为整体考虑的。所以,下面例子中,假设 sizeof(s) 的值大于 sizeof(i) 和 sizeof©,那么 sizeof(U) 等于 sizeof(s)。

union U {
    int i;
    char c;
    S1 s;
};

3.计算类

类是 C++ 中常用的自定义构造类型,有数据成员和成员函数组成,进行sizeof计算时,和结构体并没有太大的区别。

#include <iostream>
using namespace std;

class Small{};

class LessFunc {
	int num;
	void func1(){};
};

class MoreFunc {
	int num;
	void func1(){};
	int func2(){return 1;};
};

class NeedAlign {
	char c;
	double d;
	int i;
};

class Virtual {
	int num;
	virtual void func(){};
};

int main(int argc,char* argv[]) {
	cout<<sizeof(Small)<<endl;   //输出1
	cout<<sizeof(LessFunc)<<endl;//输出4
	cout<<sizeof(MoreFunc)<<endl;//输出4
	cout<<sizeof(NeedAlign)<<endl;//输出24
	cout<<sizeof(Virtual)<<endl; //输出8
	return 0;
}

注意一点,C++中类同结构体没有本质的区别,结构体同样可以包含成员函数,构造函数,析构函数,虚函数和继承,但一般不这么使用,沿用了C的结构体使用习惯。类与结构体唯一的区别就是结构体的成员的默认权限是public,而类是private。

基于以上这点,再考察从程序的输出结果,得出如下结论:
(1)类同结构体一样,C++中不允许长度为0的数据类型存在,虽然类无任何成员,但该类的对象仍然占用1个字节。
(2)类的成员函数并不影响类对象占用的空间,类对象的大小是由它数据成员决定的。
(3)类和结构体一样,同样需要对齐,具体对齐的规则见上文结构体的内存对齐。
(4)类如果包含虚函数,编译器会在类对象中插入一个指向虚函数表的指针,以帮助实现虚函数的动态调用。

所以,该类的对象的大小至少比不包含虚函数时多4个字节。如果考虑内存对齐,可能还要多些。如果使用数据成员之间的对齐,当类对象至少包含一个数据成员,且拥有虚函数,那么该对象的大小至少是8B,读者可自行推导。


参考文献

陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.P15-P18.1.7sizeof的用法
sizeof,终极无惑(上)
align (C++)
C++对齐杂注__declspec(align(#))和#pragma pack的区别

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 在C/C++编程语言中,内存是一个非常重要的概念。内存是计算机用于存储和访问数据的地方,它可以被看作是一个巨大的存储器数组,每个元素都有一个独特的地址。 在C/C++中,我们可以使用指针来访问和操作内存。指针是一个特殊类型的变量,它存储了一个内存地址。通过指针,我们可以间接访问和修改内存中的数据。 当我们在程序中声明一个变量时,系统会为该变量分配一块内存空间,并将其地址存储在变量名中。我们可以通过使用变量名来访问和修改该内存空间中的值。 另外,我们可以使用动态内存分配函数来在运行时动态地分配内存。这在需要在程序中创建变量长度的数组或者临时存储空间时非常有用。动态内存分配函数包括malloc、calloc和realloc。在使用这些函数分配内存后,我们需要记得通过使用free函数来释放这些内存空间。 值得注意的是,C/C++中的内存管理是程序员的责任。这意味着我们在使用指针和动态内存分配函数时需要小心,以避免内存泄漏和悬挂指针等问题。我们需要确保我们在使用完内存后及时释放它,以避免浪费内存资源。 总结来说,C/C++中的内存是一个重要的概念,我们可以使用指针来访问和操作内存。通过动态内存分配函数,我们可以在程序运行时动态地分配内存。然而,我们也需要负责管理内存,以避免出现内存泄漏和悬挂指针等问题。 ### 回答2: C/C++中的内存填空题是指填写一段代码,完成特定的内存操作。以下是一个例子: ```c #include <stdio.h> int main() { int array[5]; // 声明一个包含5个整数的数组 int *p = array; // 声明一个指向数组首元素的指针 // 使用循环将数组中的元素赋值为0到4 for (int i = 0; i < 5; i++) { *(p + i) = i; } // 打印数组中的元素 for (int i = 0; i < 5; i++) { printf("%d ", array[i]); } return 0; } ``` 在这个例子中,我们声明了一个包含5个整数的数组`array`,然后使用指针`p`指向数组的首元素。接下来,通过循环遍历数组,利用指针`p`对数组元素进行赋值操作,赋值的值为数组下标。最后,再通过循环遍历数组,利用数组`array`打印出各个元素的值。这段代码展示了C/C++中的指针和数组的使用,以及对内存空间的操作。 ### 回答3: C/C++ 内存填空题一般涉及指针和内存管理的知识。下面给出一个例子以300字来回答: 以下是一道关于C/C++ 内存填空题的解答。 ```c #include <stdio.h> #include <stdlib.h> int main() { int* ptr = (int*)malloc(sizeof(int)); int* arr = (int*)calloc(5, sizeof(int)); *ptr = 10; for (int i = 0; i < 5; i++) { arr[i] = i; } printf("Ptr: %d\n", *ptr); printf("Arr: "); for (int i = 0; i < 5; i++) { printf("%d ", arr[i]); } printf("\n"); free(ptr); free(arr); return 0; } ``` 上述代码中包含了两个关于内存的填空处,首先是通过`malloc(sizeof(int))`来分配存储 int 类型数据的内存空间,并将其地址赋值给`ptr`指针;另一个是通过`calloc(5, sizeof(int))`来分配存储 5 个 int 类型数据的连续内存空间,并将其地址赋值给`arr`指针。 接着通过`*ptr = 10`给指针 `ptr` 所指向的内存位置赋值为 10。并用一个 for 循环给数组 `arr` 赋值为 0 到 4。 最后通过`printf`打印结果。Ptr 输出为 10, Arr 输出为 0 1 2 3 4,表示内存填空处正确。 最后需要调用`free`函数手动释放内存,以避免内存泄漏。 在实际编程中,动态内存分配是一个常见的操作,合理地申请内存并及时释放内存对于提高程序的性能和效率十分重要。因此对于这类题目要熟悉`malloc`、`calloc`、`realloc`、`free`等函数的使用规则和注意事项,以及指针的正确使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值