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


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;

上述实例可得:

  1. 结构体的自引用要带 指针
  2. 结构体的自引用成员类型不能使用结构体类型的别名
结构体内存对齐

结构体的对齐规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = min(编译器默认的一个对齐数该成员类型最大者) 。 VS中默认的值为8 Linux中的默认值为4
  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  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(无空字节),比较节省空间啦,有些场和还可使结构体更易于控制。

为什么存在内存对齐?

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

总结来说:

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

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 的结构体进行解析

位段

什么是位段?

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

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

实例如下:

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

程序分析图:
在这里插入图片描述

程序生成图
在这里插入图片描述

注意:

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

位段的内存分配:

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

位段的跨平台问题:

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

总结:

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

枚举

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

构造:

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

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

实例如下:

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

枚举的优点:

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

定义:

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

构造:

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;
}
  • 大段存储:低位存高地址,高位存低地址
  • 小段存储:低位存低地址,高位存高地址

程序分析图:
在这里插入图片描述

应用实例:

  1. 面试题:判断当前计算机的大小端存储
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;
}
  1. 大小端和操作系统是否有关系?

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

知识点习题

  1. 下面两个结构体
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

  1. 在一个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.

  1. 下面代码不能正确输出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,局部变量时初始化为随机值。

  1. 在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

  1. 在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

  1. 请调节如下结构体成员变量的顺序,使之内存对齐
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;


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值