结构体、枚举与联合(共用体)的应用(内存对齐规则)

mac2024-07-27  14

自定义类型

C语言的数据类型如下图所示:结构体结构体类型创建结构体成员访问结构体自引用结构体内存对齐 pragma pack介绍:offsetof介绍: 位段枚举联合(共用体) 知识点习题


C语言的数据类型如下图所示:

结构体

一种新的自定义类型~~

 为什么需要结构体类型?

内置类型并不能表示所有的场景,比如学生群体 描述学生:name、gender、age、height

结构体类型创建

 定义:

结构体是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

  结构:

struct 结构体名 { 成员列表 }

 声明与定义:

例如将学生的信息定义为结构体:

struct Stu { char name[20];//名字 int age;//年龄 char sex[5];//性别 char id[20];//学号 };//分号不能丢 int main(){ struct Stu s; return 0; }

 代码含义:

struct Stu 是结构体类型,相当于int、float,定义时不申请空间。Stu 作为结构体的标签注意: 1.标签尽量不要省略,见名知意; 2.成员列表不能全部省略; 3.变量可以省略。struct Stu s通过类型创建变量,申请空间(实例化)

 特殊的声明:

在声明结构的时候,可以不完全的声明(匿名结构体类型)

struct{ int a; char b; float c; }x;

现在有一个小实例:

#include <stdio.h> struct{ int a; char b; float c; }x; struct { int a; char b; float c; }*p; int main(){ p = &x; return 0; }

然而代码运行有警告: 这是因为编辑器会把上面两个声明当成完全不同的两个类型。

怎么改正呢? 其实很简单

#include <stdio.h> struct{ int a; char b; float c; }x, *p; int main(){ p = &x; return 0; }

结构体成员访问

成员: 结构的成员可以是变量、数组、指针,甚至是其他结构体。

点操作符( . )与指向操作符( -> )

#include <stdio.h> struct Stu { char name[20]; int age; }; struct Stu s = { "xiaoming", 19 }; struct Stu *ps = &s; int main(){ printf("name = %s age = %d\n", s.name, s.age); printf("name = %s age = %d\n", ps->name, ps->age); return 0; }

结构体自引用

struct Node { int data; struct Node* next; };

现在有两个错误实例:

struct Node { int data; struct Node next; };

typedef struct { int data; Node* next; }Node; //这样写代码,可行否? //不行,解决方案: typedef struct Node { int data; struct Node* next; }Node;

上述实例可得:

结构体的自引用要带 指针结构体的自引用成员类型不能使用结构体类型的别名

结构体内存对齐

结构体的对齐规则:

第一个成员在与结构体变量偏移量为0的地址处。其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = min(编译器默认的一个对齐数,该成员类型最大者) 。 VS中默认的值为8 Linux中的默认值为4结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处(结构体内成员变量不会和嵌套结构体的成员变量内存对齐),结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。 //练习1 struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1));//12=1+3+4+1+3 //练习2 struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2));//8=1+1+2+4 //练习3 struct S3 { double d; char c; int i; }; printf("%d\n", sizeof(struct S3));//16=8+1+4+3 struct S5 { char c; int i; double d; }; cout << sizeof(S5) << endl; // 16 = 1+3+4+8 //练习4-结构体嵌套问题 struct S4 { char c1; struct S3 s3; double d; }; printf("%d\n", sizeof(struct S4)); //32 = 1+7+16+8 struct S6 { char c1; struct S5 s3; double d; }; cout << sizeof(S4) << endl; // 32 = 1+7+16+8 struct S7 { char c1; char c2; int i; double d1; double d2; }; cout << sizeof(S5) << endl; // 24 = 1+1+2+4+8+8

pragma pack介绍:

这是给编译器用的参数设置,有关结构体字节对齐方式设置, #pragma pack是指定数据在内存中的对齐方式。

#pragma pack (n) 作用:C编译器将按照n个字节对齐。 (n得设置成 2 的 i 次幂(i = 0, 1, 2, 3…),如果将n写成3、5之类,编译器会按照默认字节对齐)#pragma pack () 作用:取消自定义字节对齐方式。 #pragma pack(1) struct sample { char a; double b; }; #pragma pack()

注:若不用#pragma pack(1)和#pragma pack()括起来,则sample按编译器默认方式对齐(成员中size最大的那个)。即按8字节(double)对齐,则sizeof(sample) == 16。成员char a占了8个字节(其中7个是空字节);若用#pragma pack(1),则sample按1字节方式对齐sizeof(sample) == 9(无空字节),比较节省空间啦,有些场和还可使结构体更易于控制。

为什么存在内存对齐?

平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

总结来说:

结构体的内存对齐是拿空间来换取时间的做法。 (没有内存对齐的话,直接将结构体中所有成员类型大小加起来。比较节省空间)如果我们既要满足内存对齐,又要节省空间,如何做到: 让占用空间小的成员尽量集中在一起。

offsetof介绍:

该宏用于求结构体中一个成员在该结构体中的偏移量。

size_t offsetof( structName, memberName ); 该宏返回结构体structName中成员memberName的偏移量。偏移量是size_t类型的。第一个参数是结构体的名字第二个参数是结构体成员的名字。

offsetof 的定义:

#ifdef __cplusplus #ifdef _WIN64 #define offsetof(s,m) (size_t)( (ptrdiff_t)&reinterpret_cast<const volatile char&>((((s *)0)->m)) ) #else #define offsetof(s,m) (size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m)) #endif #else #ifdef _WIN64 #define offsetof(s,m) (size_t)( (ptrdiff_t)&(((s *)0)->m) ) #else #define offsetof(s,m) (size_t)&(((s *)0)->m) #endif #endif /* __cplusplus */

模拟实现该宏:

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

s * 的含义:让编译器将0号地址单元开始的一块内存空间当成 s 的结构体进行解析

位段

什么是位段?

位段的声明和结构是类似的,但有两个不同:

位段的成员必须是 int、unsigned int 或signed int、char(属于整型家族类型) 。位段的成员名后边有一个冒号和一个数字。

实例如下:

#include <stdio.h> struct S { char a : 3; char b : 4; char c : 5; char d : 4; }; int main(){ struct S s = { 0 }; s.a = 10; s.b = 12; s.c = 3; s.d = 4; printf("%d %d %d %d\n", s.a, s.b, s.c, s.d); printf("%d\n", sizeof(struct S)); // 3 return 0; }

程序分析图:

程序生成图

注意:

如果位段中的类型是有符号的,位段的高位表示符号位前后类型相同,比特位能共用则共用,否则重新开辟对应类型的空间;前后类型不一致,重新开辟空间。

位段的内存分配:

位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

位段的跨平台问题:

int 位段被当成有符号数还是无符号数是不确定的。位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。)位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

总结:

跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。

枚举

定义: 枚举顾名思义就是把可能的取值一一列举。

构造:

enum 枚举名{ 标识符[=整型常数], 标识符[=整型常数], ... 标识符[=整型常数] } 枚举变量;

如果枚举没有初始化, 即省掉”=整型常数”时, 则从第一个标识符开始, 顺次赋给标识符0, 1, 2, …。但当枚举中的某个成员赋值后, 其后的成员按 依次加1 的规则确定其值。

实例如下:

enum week{ Mon = 1, Tues, Wed, Thur, Fri, Sat, Sun }; enum week 是枚举类型{ }中的内容是枚举类型的可能取值,也叫枚举常量 。

枚举的优点:

增加代码的可读性和可维护性和#define定义的标识符比较枚举有类型检查,更加严谨。防止了命名污染(封装)便于调试使用方便,一次可以定义多个常量

联合(共用体)

定义:

联合是一种特殊的自定义类型, 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。

构造:

union 共用体名{ 成员1; 成员2... 成员n; }变量表列;

特点:

同一个内存段可以用来存放几种不同类型的成员,但是在每一瞬间只能存放其中的一种,而不是同时存放几种。换句话说,每一瞬间只有一个成员起作用,其他的成员不起作用,即不是同时都在存在和起作用。共用体变量中起作用的成员是最后一次存放的成员,在存入一个新成员后,原有成员就失去作用。联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的那个成员)。共用体变量的地址和它的各成员的地址都是同一地址。

大小的计算:

联合的大小至少是最大成员的大小。当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。

实例如下:

//联合类型的声明 union Un { char c; int i; }; //联合变量的定义 union Un un; union data{ int a; char b; double c; }; int main(){ printf("%d\n", sizeof(un));//4 = 1 + 3 printf("%d\n", sizeof(union data));//8 = 4 + 1 +3 un.i = 0x11223344; un.c = 0x55; printf("%x\n", un.i);//11223355(小端存储) printf("%x\n", un.c);//55 return 0; } 大段存储:低位存高地址,高位存低地址小段存储:低位存低地址,高位存高地址

程序分析图:

应用实例:

面试题:判断当前计算机的大小端存储 int sys_check(){ int a = 1; return *((char*)&a); } int main(){ int n = sys_check(); if (n == 1){ printf("小端\n");//低位存储在低地址 } else{ printf("大端\n"); } return 0; } 大小端和操作系统是否有关系?

与操作系统没关系,是与CPU架构有关

知识点习题

下面两个结构体 struct One{ double d; char c; int i; } struct Two{ char c; double d; int i; }

在#pragma pack(4)和#pragma pack(8)的情况下,结构体的大小分别是

A. 16 24,16 24 B. 16 20,16 20 C. 16 16,16 24 D. 16 16,24 24

正确答案:

C

在一个64位的操作系统中定义如下结构体: struct st_task { uint16_t id; uint32_t value; uint64_t timestamp; };

同时定义fool函数如下:

void fool() { st_task task = {}; uint64_t a = 0x00010001; memcpy(&task,&a,sizeof(uint64_t)); printf("%11u,%11u,%11u",task.id,task.value,task.timestamp); }

上述fool函数()程序的执行结果为:

A. 1,0,0 B. 1,1,0 C. 0,1,1 D. 0,0,1

正确答案

A

答案解析

因为字节对齐的原因,所以id占用4个字节,value和timestamp分别是4个字节、8个字节。虽然id占用四个字节的地址,但是只有低两位地址的数值有效(字节对齐的机制,即value存储时的地址对4(自身对齐值)求余应为0)。所以id为 0001 0001,高四位无效,所以为0001,value与timestamp分别为0.

下面代码不能正确输出hello的选项为 #include<stdio.h> struct str_t{ long long len; char data[32]; }; struct data1_t{ long long len; int data[2]; }; struct data2_t{ long long len; char *data[1]; }; struct data3_t{ long long len; void *data[]; }; int main(void) { struct str_t str; memset((void*)&str,0,sizeof(struct str_t)); str.len=sizeof(struct str_t)-sizeof(int); snprintf(str.data,str.len,"hello");//VS下为_snprintf ____________________________________; ____________________________________; return 0; }

A. struct data3_t * pData=(struct data3_t*)&str; printf(“data:%s%s\n”,str.data,(char*)(&(pData->data[0]))); B. struct data2_t * pData=(struct data2_t*)&str; printf(“data:%s%s\n”,str.data,(char*)(pData->data[0])); C. struct data1_t * pData=(struct data1_t*)&str;printf(“data:%s%s\n”,str.data,(char*)(pData->data)); D. struct str_t * pData=(struct str_t*)&str; printf(“data:%s%s\n”,str.data,(char *)(pData->data));

正确答案:

B

答案解析

enum string{ x1, x2, x3=10, x4, x5, } x;

函数外部访问x等于什么?

A. 5 B. 12 C. 0 D. 随机值

正确答案: C

答案解析: 全局变量时初始化为0,局部变量时初始化为随机值。

在x86_64环境下,请问printf输出的结果是( 48 typedef union{ long i; char j[10]; int k; } DATE; struct data{ int m; DATE n; double l; } test; DATE max; printf("%d", sizeof(struct data) + sizeof(max));

答案解析:

sizeof (DATE) = 16(当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。)

sizeof(test) = 32

在64位系统下,默认对齐方式,针对如下结构体,下列选项正确的有 typedef struct A{ int a; long b; char c; }A;

A. A x; x.a = 1; x.b = 2; *(long *)&x.a == 1; 一定成立 B. sizeof(A) = 13; C. sizeof(A) = 24; D. A x = { 1,2,‘a’ };A y = { 1,2,‘a’ }; memcmp(&x, &y, sizeof(A))==0

正确答案: A、C、D

请调节如下结构体成员变量的顺序,使之内存对齐 struct process{ uint16_t lis_port; uint8_t protocol; char path[4096]; char name[256]; int32_t pid; };

正确答案:

struct process{ uint8_t protocol; char path[4096]; char name[256]; uint16_t lis_port; int32_t pid; };

答案解析:

int8_t : typedef signed char; uint8_t : typedef unsigned char; int16_t : typedef signed short ; uint16_t : typedef unsigned short ; int32_t : typedef signed int; uint32_t : typedef unsigned int; int64_t : typedef signed long long; uint64_t : typedef unsigned long long;

如有不同见解,欢迎留言讨论!

最新回复(0)