由浅至深->C语言中struct关键字的经典问题分析

引言:先观其大略再逐步深入,这是笔者的创作初衷也是其学习感悟!

文章向导

  • struct出场的原由
  • 结构声明、结构变量及初始化
  • 结构指针(指向结构的指针)
  • 结构体与柔性数组(C99新成员)
  • 结构体对齐问题

正文

一、struct出场的原由
  在程序设计中最重要的一个问题就是如何有效地表示数据,而单纯地使用简单的变量或数组都是不够的,所幸的是C语言提供的struct关键字为人们打开了一扇新的大门。

二、结构声明、结构变量及初始化

1.结构声明和结构变量

struct test
{
	char name;
	char class;
	float value;
};

struct test T; //编译器使用test模板定义结构变量T

如上所示的代码片段就为一种结构声明,该声明描述了一个由两个字符变量和一个float变量组成的结构,但它并没有创建一个实际的数据对象,仅仅只是描述组成这类对象的元素。有时候,结构声明也被称为模板,不过这并不是C++中所谈及的模板概念。

2.如何初始化一个结构变量?

想对一结构变量进行初始化,可使用与初始化数组相似的方式(以上文中的结构变量T为例):

struct test T = {
	'A',
	'2',
	2.00
};

不过采用这种方式进行初始化时,需注意初始化的项目要和初始化的结构成员类型相匹配,当然也可以使用如下这种任意顺序的初始化方法:

/*使用点运算符和成员名来进行初始化*/
struct test T = {
	.value = 2.00,
	.name = 'a',
	.class = '2'
};

3.结构声明的几种形式

使用结构体时,不免会碰见其各种形式的写法,现对此作一番梳理:

struct A  //第一种,最基本的形式
{
   int a;
}struct B  //第二种,在声明结构B的同时定义了一个使用该结构的变量m
{
  int b;
} m;

struct  //第三种,没有给出该结构的标记,但定义了一个结构变量n
{
  int c;
} n;

/* 第四种,加上typedef关键字后,可将struct D{int d;} 看成是一个数据类型,故用struct D 定义变量即可。*/
typedef struct D 
{
  int d;
}/* 第五种,在第四种的基础上加上了别名x,故此时可以直接使用x定义结构变量。*/
typedef struct E 
{
  int e;
} x;

/* 第六种,在第五种的基础上删掉了结构标记,但还是可以直接使用别名y来定义结构变量。*/
typedef struct   
{
  int f;
} y;

三、结构指针(指向结构的指针)

程序设计者应该都知道指向数组的指针比数组本身更容易操作(如在一个排序问题中),那么类比分析可知指向结构的指针也应比结构本身更容易操作,有了这一前提认识也就更加能意识到学习结构指针的重要性。

typedef struct
{
  int init;
  char field;
} my_struct;

int main(void)
{
    my_struct * s; //定义结构指针s

	s = (my_struct *)malloc (sizeof(my_struct));
    s->init = 4; //使用间接成员运算符访问成员
	(*s).field = 'a'; //使用.运算符访问成员,注:.运算符比*的优先级更高,故()不能少

	printf("%d\n", s->init);
	printf("%c\n", s->field);

    return 0;
}

上面这个例子就是如何定义并使用一个结构指针,其中值得注意的就是访问其结构成员的两种方式。

四、结构体与柔性数组

1.柔性数组

柔性数组在C99中也称之为伸缩型数组,仅从名称来看并不能很好地理解其妙用,在正式介绍其用法之前,首先看看声明一个柔性数组的规则:

  • 柔性数组成员必须是结构中的最后一个成员;
  • 结构中必须至少有一个其他成员;
  • 柔性数组就像普通数组一样被声明,除了它的方括号内为空。
struct SoftArray
{
	int len;
	int array[]; //柔性数组
};
int main()
{
	printf("sizeof(struct SoftArray)=%d\n",sizeof(struct SoftArray)); //输出结果为4
	return 0;
}

从此程序片段不仅了解到如何声明一个柔性数组,同时还能发现柔性数组的一个特性,也就是不立即存在的特性(因打印结果为4)。

2.柔性数组的用法

谈到如何使用一个柔性数组,那么就不得不牵涉到之前所提及的结构指针。接下来,请看一个完整的例子就能深刻体会柔性数组的妙用!

#include
#include

typedef struct SoftArray
{
	int len;
	int array[];
} my_SoftArray;

my_SoftArray* creat_soft_array(int size)
{
	my_SoftArray * ret = NULL;
	if(size>0)
	{
		ret = (my_SoftArray*)malloc(sizeof(my_SoftArray)+sizeof(int)*size); //注意这一句
		ret->len = size;
	}
	return ret;
}

void delete_soft_array(my_SoftArray** sa)
{
	free(*sa);
	*sa = NULL;
}

/*给柔性数组中的每个元素赋值*/
void func(my_SoftArray* sa)
{
	int i = 0;
	if(NULL != sa)
	{
			for(i=0; i<sa->len; i++)
			{
				sa->array[i] = i+1;
			}
	}
}

int main()
{
	int i = 0;
	my_SoftArray* sa = creat_soft_array(10); //初始化结构指针
	
	func(sa); //使用柔性数组
	for(i=0; i<sa->len; i++)
	{
		printf("%d\n",sa->array[i]);
	}
	
	delete_soft_array(&sa); //释放操作柔性数组的结构指针
	
	return 0;
}

现具体分析柔性数组的“柔”,为便于理解则摘取上述的程序片段:

typedef struct SoftArray
{
	int len;
	int array[];
} my_SoftArray;

my_SoftArray* sa = NULL;
sa = (my_SoftArray*)malloc(sizeof(my_SoftArray) + sizeof(int)*size);
sa->len = size;

这里不妨假设size=5,那么malloc请求申请的堆空间应为sizeof(my_SoftArray)=4 + sizeof(int)*size=20 ==> 24字节数,具体的内存布局如下图所示:
由浅至深->C语言中struct关键字的经典问题分析_第1张图片
  由上图清晰可知,sizeof(my_SoftArray)得到的空间用于存放len,而sizeof(int)*size得到的空间则用于存放柔性数组的内容,同时柔性数组的长度信息已由len所指定。换句话说,柔性数组在声明时并不知其具体大小,而是在使用时通过结构指针来动态指定的。

五、结构体对齐问题

结构体对齐也称之为内存对齐,它说的是不同类型的数据在内存中按照一定规则进行排序,而不一定是一个接一个的顺序排序。研究此问题的意义在于,不仅是为了更好地使用结构体,同时在笔试/面试题中也会碰见此类问题的身影。

struct test1				struct test2
{						   {
	char c1;                   char c1;
	short s;                   char c2;
	char c2;                   short s;
	int i;                     int i;
};                         };

现以上面这两个结构为例,两者占用的内存空间是相同的吗?咋一看,除了成员顺序不同外,两结构基本没什么差异,但用编译器检验可知前者占用12个字节,后者占用8个字节。好吧,直观上这确实让人很难以理解,下面从内存布局的角度来进行一番分析:
由浅至深->C语言中struct关键字的经典问题分析_第2张图片
上图呈现出两结构在内存中的存储差异,但不免就引出了两个问题:
Q1:为什么需要内存对齐?
Q2:这样的对齐方式是如何计算的?
首先,回答第一个问题,即为何需要内存对齐。

  • CPU对内存的读取不是连续的,而是分块读取的,块的大小只能是2^i字节数(i=0,1,2,3…)。
  • 从CPU的读取性能和效率来考虑,若读取的数据未对齐,则需要两次总线周期来访问内存,因而效率会大打折扣。
  • 某些硬件平台只能从规定的相对地址处读取特定类型的数据,否则产生硬件异常。

接着,回答第二个问题,即如何计算内存对齐。
  此时就要引出一位新成员#pramga pack指令,它用于指定内存对齐的方式(按指定的字节数进行对齐),在正式举例之前,先看看其使用规则:

struct占用的内存大小(重要规则)

  • 结构中的第一个成员位于0偏移地址处

  • 每个成员按其类型大小和pack参数中较小的一个进行对齐(对齐参数的选定)
     偏移地址必须能被对齐参数整除
     结构成员的大小取其内部长度最大的数据成员作为其大小(适用于结构嵌套的情形)

  • 结构总长度必须为所有对齐参数的整数倍

  • 编译器在默认情况下按照4字节对齐,即#pragma pack(4)。

实例分析

#pragma pack(4)
struct test1
{				//对齐参数	偏移地址  大小
	char c1;   // 1         0       1
	short s;   // 2         2       2
	char c2;   // 1         4       1
	int i;     // 4         8       4
}
#pragma pack()

#pragma pack(4)
struct test2
{				//对齐参数	偏移地址  大小
	char c1;   // 1         0       1
	char c2;   // 1         1       1
	short s;   // 2         2       2
	int i;     // 4         4       4
}
#pragma pack()

Tips:结构占用内存大小 = (末行)(偏移地址+大小)
  该例子展示了如何使用上述提到的规则进行计算,以及如何使用#pragma pack指定内存对齐方式,读者可对照之前的内存布局图自行验证。

参阅资料
C primer Plus
狄泰软件学院- C语言进阶剖析教程
C语言经典编程282例

本文也是笔者的第一篇文章,水平和学识有限,希望各位读者能多多交流、批评指正。

你可能感兴趣的:(C/C++,语录)