数组、结构体、联合、位断、枚举

目录

前言

1. 数组的分配与访问

1.1 基本原则

1.2 指针运算

1.3 嵌套的数组

1.4 定长数组

1.5 变长数组

2. 结构体

2.1 结构体声明

2.2 结构体的自引用 

2.3 结构体访存

2.4 结构体变量的初始化 

2.5 结构体内存对齐

2.6 修改默认对齐数 

3. 联合

3.1 联合的定义 

3.2 联合的大小 

4. 位断

4.1 位断的声明 

4.2 位断的内存分配 

​4.3 位段的跨平台问题

5. 枚举

5.1 枚举定义

5.2 枚举的优点 

5.3 枚举的使用 


前言

本章介绍了一些数据类型——包括数组结构体联合位断枚举,其中前三节是笔者学习csapp的学习笔记,为更详细讲解自定义类型,加入了后两节的内容

1. 数组的分配与访问

1.1 基本原则

C语言中的数组是一种将标量数据聚集成更大数据类型的方式,规定数组是一组相同类型元素的集合 

对于数据类型和整型常数N ,声明如下:

A[ N ]

这里的类型可以是int、char、float......等等,声明之后代表数组内的数据只能为所声明的类型。

这个声明有两个效果:

  1. 在内存中分配了一个L·字节的连续区域 (L为数据类型T的大小)
  2. 引入了标识符A,作为指向数组开头的指针,指针的值为xa

如 char A[8] 和 int B[4] 分别声明了一个含8个char 类型的数组A和一个含4个int 类型数据的数组B——内存中的大小分别为1 * 8 = 8 字节和 4 * 4 = 16 个字节

1.2 指针运算

在C语言中,允许对指针进行运算 ,如果我们声明一个char类型的指针char* p和一个int类型的指针int* q,假设指针p和q指向同一个内存地址

  • 对指针p加1的操作使得p指向0x101处
  • 对指针q加1的操作使得q指向0x104处

虽然都是对指针进行加1运算,得到的结果却不同,这是因为对指针进行运算时,计算结果会根据该指针引用的数据类型的大小进行伸缩——int*类型指针一次向后访问四字节,char*类型指针一次向后访问一字节

假设定义一个数组 int E[6] 

对于数组的每个元素都有两个属性——

  1. 存储的内容
  2. 存储地址 

通常我们使用数组引用的方式来访问数组中的元素,如E[2]可以访问数组第二个元素e2

数组引用E[2]等价于*(E + 2),其中表达式E + 2表示数组第二个元素的存储地址,然后访问这个内存位置,这里的加2与指针加2类似,也是数据类型相关

假设数组E的起始地址和整数索引i分别放在寄存器 %rdx 和 %rcx 中,而结果保存在 %eax(数据)或 %rax (地址)中,那么对数组引用的相关表达式的汇编代码实现如下表

表达式类型汇编代码
E

int*

 xEmovq %rdx, %rax
E[0]int M[xE]movl (%rdx), %eax
E[i]int M[xE + 4]movl (%rdx, %rcx, 4), %eax
&E[2]int* xE + 8leaq 8(%rdx), %rax
E + i - 1int* xE + 4*i - 4leaq -4(%rdx, %rcx, 4), %rax
*(E + i - 3)int M[xE + 4*i -12]movl -4(%rdx, %rcx, 4), %eax
&E[i] - Elong imovq %rcx, %rax

1.3 嵌套的数组

嵌套数组就是二维数组 

图中声明了一个二维数组A,int A[5][3],是一个5行3列的二维数组

对于二维数组的声明,int A[5][3]等价于

typedef int row3_t[3];
row3_t A[5];

我们可以将二维数组看成数据类型是一维数组的一维数组,这意味着这个一维数组的每一个元素都是一个一维数组,在这里将A看成一个有5个元素的数组,每一个元素都是一个长度为3的数组

二维数组中元素地址的计算和一维数组类似,对于数组

T  D[R][C

其元素D[i][j]的内存地址为:

&D\left [ i \right ]\left [ j \right ]=x_{D}+L(C\cdot i+j)

其中,xD表示数组的起始地址,L表示数据类型T的大小,根据这个公式,对于任意一个5×3的数组A[5][3]中任意元素地址为:

&A\left [ i \right ]\left [ j \right ]=x_{A}+4(3\cdot i+j)

假设A在寄存器%rdi中,i在寄存器%rsi中,j在%rdx中,可以用以下汇编代码将数组元素A[i][j]复制到寄存器%eax中

leaq  (%rsi, %rsi, 2), %rax   // 计算 3i
leaq  (%rdi, %rax, 4), %rax   // 计算 xA + 12i
movl  (%rax, %rdx, 4), %eax   // 从M[xA + 12i + 4j]中读数据

1.4 定长数组

C语言编译器能够对于定长的多维数组进行优化 

首先使用下面的方式声明一个数据类型为fix_matrix的16*16的数组 

#define N 16
typedef int fix_matrix[N][N];

当程序要用一个常数作为数组的维度或者缓冲区的大小时,最好通过 #define 声明将这个常数与一个名字联系起来,然后在后面一直使用这个名字代替常数的数值。这样一来,如果需要修改这个值,只用简单地修改这个 #defien 声明就可以了,这是一种很好的编码习惯

我们用下面的代码来计算矩阵中A中的第i行和矩阵B的第k列的乘积 

#define N 16
typedef int fix_matrix[N][N];
int matrix(fix_matrix A, fix_matrix B, long i, long k)
{
	long j;
	int result = 0;

	for (j = 0; j < N;j++)
		result += A[i][j] * B[j][k];

	return result;
}

 注:为方便展示,矩阵下标与代码下标并不一致

 经编译器优化后的汇编为代码:

matrix:
  salq    $6, %rdx;
  addq    %rdx, %rdi;
  leaq    (%rsi, %rcx, 4), %rcx;
  leqa    1024(%rcx), %rsi;
  movl    $0, %eax;
.L7:
  movl    (%rdi), %edx;
  imull   (%rcx), %edx;
  addl    %edx, %eax;
  addq    $4, %rdi;
  addq    $64, %rcx;
  cmpq    %rsi, %rcx;
  jne     .L7
  rep; ret

在进行循环前,编译器先计算了三个指针,分别是Aptr、Bptr和Bend,分别指向A数组第i行的首元素、B数组第k列的首元素和B数组第k列的最后一个元素后一个内存空间,并将这三个指针放到不同的寄存器中,具体如图

具体的C代码为

int matrix(fix_matrix A, fix_matrix B, long i, long k)
{
	int* Aptr = &A[i][0];
	int* Bptr = &B[0][k];
	int* Bend = &B[N][K];
	
	int result = 0;

	do {
		result += *Aptr * *Bptr;
		Aptr++;
		Bptr += N;
	} while (Bptr != Bend);

	return result;
}

优化后,

  1. 先读取Aptr指向元素的内容
  2. 然后将指针Aptr指向的元素与Bptr指向的元素相乘
  3. 将乘积结果累计并保存到eax中
  4. 计算完成后,分别移动指针Aptr和Bptr指向下一个元素(这里Aptr移4动个字节,Bptr移动64个字节)
  5. 直到 Bptr = Bend

这种优化明显提高了程序的效率,在这个优化中

  • 去掉了整数索引 j 
  • 将所有的数组引用转换成了指针间接引用

1.5 变长数组

历史上,C语言只支持在编译时就能确定的多维数组,如果想要使用变长数组则需要用到内存分配函数。ISO C99引入了一种功能,允许数组的维度是表达式。在变长数组的C版本中,一个变长数组的声明可以为:

int A [ expr1 ][ expr2 ]

int var_ele(long n, int A[n][n], long i, long j) {
	return A[i][j];
}

它可以作为一个局部变量,也可以作为函数的参数(在这种情况下,参数n必须在数组A之前) 

变长数组的地址计算与定长数组类似,不同的是他引入了参数n,需要用乘法指令来计算n*i 


2. 结构体

2.1 结构体声明

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

一个结构体的声明

struct tag
{
	member - list;
}variable - list;

2.2 结构体的自引用 

我们知道函数内部可以包含它自身,实现递归。那么结构体成员能否包含该结构体本身呢?

考虑下面的声明

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

由于结构体并不像函数那样可以返回,因此结构体自引用的结果是不断地消耗内存,正确的声明应该是

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

使用结构体类型的指针能够很好的实现结构体之间的引用关系

2.3 结构体访存

为了清晰地研究结构体,考虑下面的结构体声明 

struct rec 
{
	int i;
	int j;
	int a[2];
	int* p;
};

这个结构体包含四个部分,分别是两个4字节d饿整型变量,由两个4字节整型组成的数组,一个8字节的整型指针变量,它们相对于结构体起始位置的偏移为

对于结构体元素的访问,也是通过地址+偏移量来实现的,例如,假设将结构体指针r放在寄存器%rdi中,将 i 复制到 j 中的汇编代码为

movl  (%rdi), %eax;  //从%rdi中读出i
movl  %eax, 4(%rdi)  //将i放到j中

2.4 结构体变量的初始化 

struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1

struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };

struct Stu     //类型声明
{
	char name[15];//名字
	int age;    //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化

struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化

2.5 结构体内存对齐

首先阐明结构体内存对齐的规则

  1. 第一个成员在与结构体变量偏移量为0的地址处
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处,对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
  3.  结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍

考虑下面的结构体声明

struct S1 {
	int i;
	char c;
	int j;
};

一般我们会认为结构体的大小为9字节,但我们用sizeof求得的结果却是12

 原因是为了提高内存系统的性能,系统对于数据存储的合法地址做出了限制

例如,变量 j 是 int 类型,占4个字节,因此它的起始地址必须是4的倍数

因此,编译器会在变量 c 和变量 j 之间插入3个字节的间隙,这样,变量 j 相对于起始位置的偏移量为8,满足4的倍数

结构体内每个元素都需要满足这样的对齐原则,除此之外,结构体的尾部也可能需要进行填充,考虑下面的声明

struct S2 {
	int i;
	int j;
	char c;
};

虽然这样的排列方式能够满足所有结构体成员的对齐要求,但是,如果我们声明了一个结构体数组——那么显然,数组内的第二个结构体就不满足对齐的要求了,因为第二个结构体的第一个成员的偏移量是9,而对齐数是4,因此真正的偏移量应该为 9 + 3 = 12,编译器会在结构体的末尾分配3字节的间隙

2.6 修改默认对齐数 

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。 

#pragma pack(8)//设置默认对齐数为8
struct S1
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认

#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认


3. 联合

3.1 联合的定义 

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

3.2 联合的大小 

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

考虑下面的声明

union Un1
{
	char c[5];
	int i;
};

union Un2
{
	short c[7];
	int i;
};
  • 对于Un1,最大对齐数是4(数组的对齐数是其数据类型的对齐数,在这里是1),而char c[5]需要五个字节,因此需要对齐到8 
  • 对于Un2,最大对齐数是4,而short c[7]需要14个字节,因此需要对齐到8


4. 位断

4.1 位断的声明 

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

  1. 位段的成员必须是 intunsigned intsigned int 
  2. 位段的成员名后边有一个冒号和一个数字
struct A
{
	int _a : 2;
	int _b : 5;
	int _c : 10;
	int _d : 30;
};

4.2 位断的内存分配 

同样考虑上述的声明,通过sizeof计算得到位断的大小是8字节原因是: 

  • 首先分配一个整型的长度——4个字节(32个比特位)
  • 数字代表着所分配的比特位,int_a : 2,说明给_a分配了2个位长度
  • int_b : 5 给b分配了5个比特位,a,b加起来7个比特位
  • int_c :10,abc加起来17个比特位(不满4字节),剩15个比特位,不够_d
  • 再分配一个比特位,用来存_d,因此总共2个int,8个字节

考虑下面的代码:

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;
	
	return 0;
}

对于这样一个例子, 

4.3 位段的跨平台问题

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


5. 枚举

5.1 枚举定义

考虑下面的声明

enum Day//星期
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
enum Sex//性别
{
	MALE,
	FEMALE,
	SECRET
};
enum Color//颜色
{
	RED,
	GREEN,
	BLUE
};

以上定义的 enum Day , enum Sex , enum Color 都是枚举类型,{}中的内容是枚举类型的可能取值,也叫 枚举常量,这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值,例如

enum Color//颜色
{
	RED = 1,
	GREEN = 2,
	BLUE = 4
};

5.2 枚举的优点 

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

5.3 枚举的使用 

enum Color//颜色
{
	RED = 1,
	GREEN = 2,
	BLUE = 4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值