c数组与结构体

数组,存储同类型的复合类型;结构体,存储不同类型的复合类型,用于自定义数据结构。

计算机中,针对存储大量数据的集合,有着两种方式,一种是以块式集中存储数据,这就是数组的存储方式,大量同类型的数据集中放在一块;另外一种大量数据逐个分开,但其存储的数据项就包括下一个数据的存储地址,就像一个方向标,指向下一个数据,整体来看就像连接起来的表格,所以这种结构被称为链表。

一、数组和指针

数组作为顺序存储的典型,存储相同类型的值,以该类型存储大小为单位划分,其长度就是其容量,可容纳多少个该类型的值在声明之初就定好了,数组内元素的访问可通过下标进行访问(下标就是整数索引,从0开始)。

1.1 初探数组

数组的常见声明如下:

type array[size];

数组常见的声明就是数据类型加,数组名后面中括号括起来数组大小。在程序中,数组往往用来存储重要数据,它们的使用往往需要先进行初始化,比较直接的就是用大括号括起来的数值列表对数组进行初始化(数值个数不得大于数组大小)。如下:

//完整的初始化
int nums1[4] = {
    
    1, 2, 3, 4};
//部分初始化
int nums2[4] = {
    
    1, 2};
//C99后支持的新特性,只初始化最后一个元素
int nums3[4] = {
    
    [3] = 4};
//不明确指定数组大小的声明初始化
int nums4[] = {
    
    1, 2, 3, 4, 5};
//错误示范
int nums5[4] = {
    
    1, 2, 3, 4, 5};
int nums6[4];
//下面这步是非法的
nums6 = {
    
    1, 2, 3, 4};

如上,数组的初始化是在声明之初就该进行了的,而且初始化接受部分的初始化,这种初始化是从数组第一个元素开始初始化直到用完常量值,剩下的由编译器自动赋以类型零值。上面这种用大括号括起来的列表来对数组进行初始化,在c++里面被称为列表初始化,所以这里也这么称呼吧,方便点。如上面的错误例子,当数组在初始化时初始化列表大于数组容量时,编译器直接报错,而且声明以后,在后面的语句进行列表初始化也是非法的,同样会报错,但可以声明固定大小的数组而不初始化。还有如上面nums4这样不指定size的,声明初始化以后,其大小就是初始化的列表长度,也就是说nums4的长度是5。

数组的使用

数组是一个集合,数组的使用往往就是数组内元素的读取和写入,而数组内元素的调用可以通过数组下标来索引该元素,而数组的下标索引从0开始。下面是几个应用方式:

//1
int nums[4] = {
    
    1, 2, 3, 4};
scanf("%d", &nums[0]);      //"5"
printf("%d\n", nums[0]);    //"5"

//2
int nums1[4] = {
    
    [3] = 10};
printf("%d, %d\n", nums1[0], nums1[3]); //"0, 10"
scanf("%d", &nums[0]);                  //"8"
printf("%d, %d\n", nums1[0], nums1[3]); //"8, 10"

//3
int nums2[4];
printf("%d, %d, %d, %d\n", nums2[0], nums2[1], nums2[2], nums2[3]); //"8, 0, 20, 0"
scanf("%d%d%d%d", &nums2[0], &nums2[1], &nums2[2], &nums2[3]);  //"0 1 2 3"
printf("%d, %d, %d, %d\n", nums2[0], nums2[1], nums2[2], nums2[3]); //"0, 1, 2, 3"

如上面的三个例子所示,三个整型数组,三种状态下(完全初始化、部分初始化,只声明)的读取和写入。其实,数组的使用,和for这种计数循环天然适配,如下:

int nums[4], i;
//写入数据
for(i = 0;i < 4;i++)
    scanf("%d", &nums[i]);  //逐行输入"0"、"1"、"2"、"3"
//读取
for(i=0 ;i < 4; i++)
    printf("%d, ", nums[i]);
//"0, 1, 2, 3,"

调用数组元素的另一种方式

上面使用数组元素的方式是基于下标来进行,但也有另一种方式进行调用。数组名的值,本身就是一个指针常量,它是数组第一个元素的地址,所以,数组元素的调用也可以用指针来进行。如下:

int nums[4] = {
    
    0, 1, 2, 3};
printf("%d, %d\n", *nums, *(nums + 2)); //"0, 2"

int *p = nums, i;
for(i = 0;i < 4; i++)
    printf("%d, ", *(p + i));           //"0, 1, 2, 3, "

如上使用都是可以的,不过使用指针要注意的就是*取值符和&取址符,指针本身是指向某地址的变量,而*取值符的作用就是用来取指针指向地址的值,而取址符对指针本身往往没有多大使用,因为使用场景往往更关注指针指向地址的值,这里是想提醒不能把指针当做寻常变量那样使用取址符&。

要注意的,数组的使用和指针有共通之初,但并非等同于指针。

1.2 多维数组

在实际生活中,数据集有集合形式,也有矩阵形式,针对这种种数据的处理,C中往往都用数组进行,只是数组的形式有所不同。集合列表用一维数组,矩阵用二维数组,乃至有三维数组、四维数组应付更复杂的数据结构。这部分进行的就是对数组的学习解读。

二维数组

数组的所谓二维三维方面,在这里的体现,用下标展示会更加直观,如下声明定义一个二维数组:

//完全初始化
int matrix[2][3] = {
    
    
    {
    
    0, 1, 2},
    {
    
    3, 4, 5}
};

//部分初始化
int matrix1[2][3] = {
    
    
    {
    
    0, 1},
    {
    
    3, 4}
};

//不指定数组大小的声明初始化
int matrix2[][3] = {
    
    
    {
    
    0, 1, 2},
    {
    
    3, 4, 5}
};

回顾一下,数组是相同类型元素的列表,一维数组是一个简单列表,里面存放着同类型的一个个常量值,那二维数组呢?它则是存放着一个个一维数组的另类列表,所以,不去深究数组内的元素,其实二维数组和一维数组乃至多维数组都是一样的,它们都是一个有序列表。

就着上面的结论来看上面的例子就简单多了,matrix是一个存放着两个数组的列表,内层数组则存放着三个整型数据。因此,可以在数组大小范围已定的情况下,用不足其大小的列表去对其进行初始化,比如matrix1,还有不明确外层数组数量的情况用符合内层大小的一定个数的数组去对数组初始化,比如matrix2(还是有点拗口)。上面的例子也可以改成下面的样子:

//完全初始化
int matrix[2][3] = {
    
     {
    
    0, 1, 2},{
    
    3, 4, 5} };
int matrix[2][3] = {
    
    0, 1, 2, 3, 4, 5};

//部分初始化
int matrix1[2][3] = {
    
     {
    
    0, 1},{
    
    3, 4} };
int matrix1[2][3] = {
    
    0, 1, 3, 4};

//不指定数组大小的声明初始化
int matrix2[][3] = {
    
     {
    
    0, 1, 2},{
    
    3, 4, 5} };
int matrix2[][3] = {
    
    0, 1, 2, 3, 4, 5};

这样来看,就比较直观,实际上二维数组的元素也是顺序存放的。针对二维数组元素的访问,还是使用下标的方式比较直观,但使用指针的方式进行访问也是可以的,不过相对来说,就形式来看,比较麻烦,一层叠一层的。

int matrix[2][3] = {
    
    0, 1, 2, 3, 4, 5};
int *p = matrix;        //GNU中会有警告,没有在vs尝试
printf("%d\n", *(p+4)); //"4"
printf("%d\n", *((p+1)+1)); //"2",这里被计算器理解成p+2的取值
int (*p1)[3] = matrix;       //定义一个int [3]类型的指针,初始化其值使其指向matrix
printf("%d, %d\n", *(*p1 + 1), *(*(p1+1)+1));      //读取二维数组第一行第二列和第二行第二列的值,输出"1, 4"

总的来说,二维数组的指针调用方式有两种,一种是当做一维数组的正常偏移调用,第二种就是声明一个指向内层数组类型的指针,并初始化为指向matrix指向地址。因为*运算符的优先度要低于[]运算符,所以为了表明指针身份,需要把变量名和*运算符括起来;另外,需要重点说明的是上面的p1是一个int [4]类型的指针,(指针,是指向某地址的变量,具体来说是某种类型值的地址),所以,实际上类型对于指针就是束缚,防止它访问的存储越界从而得到期望以外的值,而int [4]类型也是束缚,可以把这样的4个整型连起来的存储空间看做一个单位,现在的指针就指向这么一种单位的地址,当它初始化成matrix的地址后进行偏移,它实际上就是以matrix的内层数组为单位进行偏移。顺便说一句,有确定类型的指针也有空类型的指针。

和for循环配合使用的二维数组

因为二维数组可以展开成一维数组,所以用循环调用就有嵌套和不嵌套的使用,如下:

int matrix[2][3] = {
    
    
    {
    
    0, 1, 2},
    {
    
    3, 4, 5}
};
int i, j, *p = matrix;

//嵌套循环
for(i = 0;i< 2;i++) {
    
    
    for(j = 0;j < 3;j++)
        printf("%d, ", matrix[i][j]);
    printf("\n");
}
//"0, 1, 2, "
//"3, 4, 5, "

//不嵌套循环,编译器报警系列
for(i = 0;i < 6; i++)
    printf("%d, ", *(p + i));
//"0, 1, 2, 3, 4, 5, "

//嵌套循环
int (*p1)[3] = matrix;
for(i = 0;i<2;i++) {
    
    
	for(j=0;j<3;j++)
		printf("%d, ", *(*(p + i) + j));
    printf("\n");
}
//"0, 1, 2, "
//"3, 4, 5, "

多维数组和二维数组共通,只不过是集合往更深一层嵌套,这里就不展开了。

1.3 变长数组和动态数组

在日常应用中,数组的长度往往是固定的,固定的方式各不一,比较常见的,就是使用宏定义定下数组长度,而这种数组往往存储的就是一个个常量数据,是日常生活中基本不动的数据,比如year数组就该有12个数,每个数存放每个月的天数;week数组就该存着周日到周一这么几个数值,具体是字符串还是整型数就看需要了。如下:

#define MONTH 12
#define WEEK 7

//const限定符
const int leap_year[MONTH] = {
    
    31, 28, 31, 30, 31, 30, 31, 30, 31, 31, 30, 31};
const char week[WEEK] = {
    
    "Sunday", "Monday", "Tuesday", "Wedsday", "Thursday", "Friday", "Saturday"};

除此以外,还可以用整型变量或者整型表达式来确定数组长度,在C99之前,规定的标准是以整形常量表达式为数组确定大小,而不是整型表达式,这是C99后添加的新特性。其实就个人来看,无非就是一句语句和两句语句的区别。不过这里也说明相对一词的重要性,变量,不确定的,但在运行程序中它是确定的,也就是相对程序运行来说,它是固定的。这个就是变长数组–VLA

int sum, a, b;
//输入a、b值
//c99之前
sum = a * b;
int num[sum];
//c99以后
int num[a*b];

相较于一开始就用常量给定区域的数组而言,变长数组也算是动态数组了,这种数组长度在程序运行时确定的数组就是动态数组,反过来说,运行以前确定的,就是静态数组了(因为常量就是运行前就确定了的)。除此以外,还有一种动态数组,这种数组随程序需要而确定大小,内存空间也是自己申请,使用完毕自己释放。这种申请的内存, 来自于堆,由于数组是可以逐层嵌套的,对于这种数组就需要自外向里,逐层创建,而释放则是反过来,由里向外逐层释放。而这种,才是常说的动态数组

堆区和栈区
对于一个运行程序来说,内存常被分为栈区、堆区、常量区、静态区和代码区。初始,程序以可执行代码的形式存放在磁盘中,操作系统在运行程序的时候就会把代码和静态数据(如初始化变量)加载到内存中,加载完毕后,分配内存给运行时栈(存放局部变量、函数参数和返回地址,main也是函数),除此以外,还有着堆内存由程序显式请求分配,对于数组、一些数据结构(链表、散列表、树等)都需要堆区存储,随程序运行的时候逐渐变化。

在C语言中,内存的显式请求和显式释放都有两个专门的函数–malloc函数和free函数,而c++中则是new运算符和delete运算符申请和释放。上面两函数原型如下:

#include <stdlib.h>
void *malloc(int num);
void free(void *address);
//另外几个相关函数
void *calloc(int num, int size);
//在内存中动态地分配 num 个长度为 size 的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了 num*size 个字节长度的内存空间,并且每个字节的值都是0。
void *realloc(void *address, int newsize);
//该函数重新分配内存,把内存扩展到 newsize。

malloc函数的用法,申请num指定字节的内存并返回指向该内存的空指针,一般在赋值给特定指针前需要强制类型转换;free则是把address指向的堆内存进行释放,没有返回值。常见的配合数组的使用如下:

#include <stdlib.h>
int *p;
p = (int *)malloc(4* sizeof(int));
for(i = 0;i < 4; i++)
	p[i] = i;
for(i = 0;i < 4;i++)
	printf("%d, ", p[i]);
free(p);

如上,申请一个4整型大小的内存空间,在强制类型转换后赋值给整形指针p,这时候就可以针对p指针做数组操作了(指针操作也行),重点,用完后记得free。

动态二维数组

上面已经有二维数组的学习,所以这里主要是一个嵌套malloc和free的调用例子

#include <stdio.h>
#include <stdlib.h>

int main() {
    
    
    
    int **p, i, j;
    //定义一个指向指针数组的指针,并指向能存下3个整型指针的连续内存
    p = (int **)malloc(3*sizeof(int *));
    //逐层分配空间并赋值
    for (i = 0;i < 3; i++){
    
    
        p[i] = (int *)malloc(4*sizeof(int));
        for(j = 0;j < 4;j++)
            p[i][j] = i * 3 + j + 1;
    }
    //从数组末开始读取值,一行读取完毕就释放那一行的内存
    for(i = 2;i >= 0;i--){
    
    
        for(j=3;j>= 0;j--)
            printf("%d, ", p[i][j]);
        printf("\n");
        free(p[i]);
    }
    //释放最外层的指针存放内存
    free(p);
	return 0;
}
/*输出
10, 9, 8, 7,
7, 6, 5, 4,
4, 3, 2, 1,
*/

如上,针对动态二维数组的内存申请和释放就更能体现二维数组的性质,外层数组存放内层数组地址,通过地址访问到该数组内容,而内数组也用下标把信息拆分得更加细致。嗯,虽说上面的例子是从数组最深处开始访问并逐渐向外面开始释放,但我正常从第一行内层数组开始释放也没有出现问题,暂时没有出现什么问题,但还是能倒着释放就倒着来吧。

1.4 指针的更多形式

指针就一个功能,指向某个地址,然后根据其指向的地址,它就有了不同的称呼:

  • 简单指针,指向简单的同类型变量,如简单数组;
  • 二重指针,指向简单指针的指针;
  • 指针数组,存放指针的数组,其数组可以赋值给二重指针;
  • 函数指针,指向函数的指针,就使用上来看有点像别名,其实是为了方便调用的;
  • 结构体指针,指向某个数据结构的指针

1.5 数组的边界

数组是持续内存,在访问内部元素的时候一旦越界,就会出现意料之外的行为。比如有一个元素长度为4的数组a,但在访问a[4]这种行为的时候,就属于明显的越界,但编译器并不会报错,这种问题的爆发会出现在程序运行的时候,有时候会输出期望值以外的值,有时候则会意外停止。所以这里是做一个警告,在进行下标访问的时候,要注意不能越界,另外像字符串这种字符数组,要习惯性地在数组末尾放置一个’\0’符。

1.6 数组和指针的异同

  • 从内存存储上看,数组是同类型元素的集合,是一大块固定数量类型的存储;指针是存储同类型地址的对象,根据系统的不同,其存储也不同,但其不大于8字节内存
  • 从声明定义上看,数组的整体定义要在声明之时,否则就只能逐一赋值,而指针可以拆开进行,而且也可以指向不同的地址
  • 从使用上看,数组使用下标进行元素访问,指针用*运算符取值,配合数组可以进行地址偏移

###数组和指针相互区别相互联系,不可分割也不可等同,两者应用场景和使用角度不一样。

二、结构体

作为复合类型的重要组成,结构体可以由其他各种数据类型组成,共同构成一个表达特殊意义的数据结构,也是面向对象编程的一个重要出发点。场景切入,当我们要描述一个对象的时候,比如一个学生,那我们可以用哪几个数据?成绩、身高、体重、年级、班级,等等皆可,针对我们需求的场景,抽取重要属性成结构体,程序需要处理的,就是结构体的属性数据。当我们需要做一个成绩系统,就需要学生的各科成绩,当我们需要做健康检测,就需要学生的身高、体重、视力等数据,需求不同,使用的属性不同。当我们确定好一个数据结构以后,就是确定了一个自定义类型,我们可以用这个类型来声明定义需要的变量。结构体的通用形式如下:

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

上面上面中,member-list就是成员列表,用字符型、整型、浮点型来填充,其变量名就是结构体tag的属性名,variable-list就是成员列表,一般来说如果不是声明为函数内局部变量,都不会在声明结构体时就声明这种类型的变量,所以一般全局的tag这里都是空留一个分号收尾。
结构体的声明,其实是用户自定义了一个类型,它告诉了编译器这种类型由哪些数据构成。

2.1 初始化结构体

结构体是不同类型的集合,数组是同类型的集合,但类型其实从内存角度上来说,就是划分字节和读取规则不一样,两者都是一大块内存,所以数组的初始化,很多可以参考数组。
要初始化一个结构体,先要声明一个结构体,现在就抽取学生这一对象作为一个结构体,抽象其姓名、年龄和成绩作为基本属性,如下:

struct student {
    
    
    char name[10];
    int age;
    float total_score;
};
struct student a = {
    
    "Xiao Ming", 14, 89.7};

如上为student结构体的一个声明(这个声明一般放在所有函数外面作为全局变量使用),声明student类型的a对象,用的也是括号初始化。需要注意,C和C++针对结构体对象的声明有点不同,C中进行声明需要添加struct突出结构体类型,c++中则是可选项,可加可不加。
当然也可以进行声明,然后逐个元素进行初始化,不过student的name属性是数组,所以它只能在声明之时初始化,或者使用strcpy等方法进行赋值。

//只声明不初始化
struct student temp;
temp.name = "Siri";                 //error: assignment to expression with array type
strcpy(temp.name, "Siri");          //使用前要引入头文件string.h
temp.age = 10;                      //合法
printf("%f\n", temp.total_score);   //"0.000000",编译器自动初始化为float类型零值

另外初始化的方法还有:

struct student b = {
    
    .name = "Xiao Fei",
            .age = 15,
            .total_score = 98.6};

如上面这种方式就是C99和C11后为结构体添加的指定初始化器(designated initializer),在大括号内用点运算符和属性名标识特定元素,等号赋值初始化。当然也可以部分初始化,这样的话,没有初始化的部分就会被编译器自动初始化为类型零值。

2.3 访问结构体

目前,关于结构体属性的访问,可以使用点运算符来进行,如上面的指定初始化器中的用法,点运算符加属性名就可以访问对应属性,这就有点类似数组下标。实际上,一般的结构体对象只有这种访问方式,而结构体指针有着另外的方式来访问结构体属性。

struct student *p = &a;
printf("%f\n", p->total_score); //"98.6"

如上,作为指向结构体的指针,适用的访问结构体属性的方法就是箭头,突出指针的存在。

2.4 结构体类型衍生

结构体是自定义类型,c中任何类型都可以产生指针和数组,所以就也有结构体数组和结构体指针,它们的声明定义和具体元素的访问没有特殊之处,只不过凑在一起会让人一下子有点不适。

struct student stu[3] = {
    
    
    {
    
    "Xiao Ming", 14, 89.7},
    {
    
    "Siri", 11, 80},
    {
    
    "Xiao Fei", 88}
}//错误,这种声明只能在结构体类型声明的时候进行,如下

struct student {
    
    
    char name[10];
    int age;
    float total_score;
} stu[3] = {
    
    
    {
    
    "Xiao Ming", 14, 89.7},
    {
    
    "Siri", 11, 80},
    {
    
    "Xiao Fei", 8}
};
printf("%d\n", stu[2].age); //"8"

//只声明,后续可以像数组只声明那样进行类似的初始化
struct student stu[4];

上面就是结构体数组的声明定义,关于结构体指针的声明比较简单,就简单的类型加*标识即可,然后用同类型普通变量的地址给予赋值就算初始化了。另外结构体数组的元素调用用的是下标访问,而结构体指针用的是箭头访问,但当指针指向数组时,它用的,还是下标访问方法。

复合嵌套

上面从一开始,结构体的属性就纳入了数组这一复合类型,所以结构体也是可以内嵌指针的,而且把数组改换成指针,也会方便很多。当然,在C语言中,方便与危险等同,越是简单的地方,越容易出事,不过这里不讨论。作为描述对象的结构体,对象内部也可以有对象属性,所以结构体同样可以嵌套结构体,比如给student拆解名字属性,名字属性就可以作为一个对象,内部填充姓和名,如下:

struct Name {
    
    
    char *surname;
    char *name;
};

struct student {
    
    
    struct Name name;
    int age;
    float total_score;
};

本来是想直接把student放前面,Name搞个前置声明的,发现不行,而且结构体的声明也不能进行初始化,还有各种各样的特性。。。。。。还是c++用多了,总把c的struct当做类class。
声明还是比较简单的,下面介绍一下其初始化和使用:

//main内
int i;
//普通student对象
struct student a = {
    
    {
    
    "Jack", "Chen"}, 25, 100.0};
//student数组
struct student stu[2] = {
    
    
    {
    
    {
    
    "Hong", "Pan"}, 26, 88},
    {
    
    {
    
    "Jin", "Jiang"}, 27, 98}
};
//student指针
struct student *p = &a;
printf("name: %s %s\n", a.name.surname, a.name.name);
for(i=0;i<2;i++)
	printf("name: %s %s, score: %.2f\n", stu[i].name.surname, stu[i].name.name, stu[i].total_score);
printf("age: %d\n", p->age);

//声明student对象但不初始化
struct student temp;
temp.name.surname="Siri";
temp.name.name="MI";
scanf("%d", &temp.age);         //80
printf("age: %d\n", temp.age);  //"80"

算是比较简单的了,具体的实验进行,就看个人经验了。当这些个简单的概念组合在一起变得庞大的时候,就大而化之,关注最底层的那一部分的特性,往往能更好理解和使用。

2.5 从堆内存分配的结构体

上面有介绍过动态数组,它是由malloc显式向堆内存申请特定大小的空间并由free显式释放的;同样的,结构体也可以,应该说,它往往是这么使用的,当它作为一个重要的数据结构的时候。

#include <stdlib.h>

struct student *p;
p = (struct student *)malloc(2*sizeof(struct student));
/*一堆应用操作*/
free(p);

其实这里并不复杂,但也只是一个概念的记录,复杂的是引入数据结构以后的事,这里篇幅也不够,只做记录。

重要的字节对齐

先举一个例:

struct Name {
    
    
    char *surname;
    char *name;
};

struct A {
    
    
    int age;
    struct Name name;
    float total_score;
};
struct B {
    
    
    int age;
    float total_score;
    struct Name name;
};
printf("A: %d, B: %d\n", sizeof(struct A), sizeof(struct B));
//输出:A: 32, B: 24
printf("ptr: %d, int: %d, float: %d, Name: %d\n", sizeof(char *), sizeof(int), sizeof(float), sizeof(struct Name));
//辅助数据:"ptr: 8, int: 4, float: 4, Name: 16"

上面这个例子,指的是有着同样属性的A、B两个结构体,却因为属性的先后声明顺序不一样,使得sizeof得到的占用内存不一样。出现这种情况的原因,就是这里要介绍的内容–字节对齐。

首先,说明一下字节对齐是如何对齐。在C中,一个变量,一块内存,一个复合变量,一大块内存,比如数组,但结构体就不一样,因为它存的是不同类型的变量,不像数组那样内部元素一致整齐。所以,字节对齐,就是针对结构体而言的一种调整了。所谓对齐,就是结构体中所有成员在分配内存时都要向成员中最大那个对齐,最大的那个作为一个标准,当第一个成员没有超出这个标准,后面紧跟着的也没超出这个标准,就会加在一起,看看是否到了这个标准,到了就可以新开一块最高标准的内存,没到就继续叠,最后凑起来的内存块就是最大那块的倍数(这里考虑的都是基本类型,不包括复合类型)。如下:

struct A {
    
    
    char a;
    int i;
};

struct B {
    
    
    char a;
    char b;
    int i;
};

struct C {
    
    
    char a;
    char b;
    char c;
    char d;
    char e;
    int i;
};

struct D {
    
    
    char a;
    int i;
    double db;
};

struct E {
    
    
    char a;
    int i;
    int i2;
    double db;
};

printf("char: %d, int: %d, double: %d\n", sizeof(char), sizeof(int), sizeof(double));
//"char: 1, int: 4, double: 8"
printf("A: %d, B: %d, C: %d, D: %d\n", sizeof(struct A), sizeof(struct B), sizeof(struct C), sizeof(struct D));
//"A: 8, B: 8, C: 12, D: 16, E: 24"

上面的例子的空间结构大概可以参考如下:
struct A

a
i i i i
占了8字节

struct B
a b
i i i i
占了8字节

struct C
a b c e
e
i
占了12字节

struct D
a
---- ----
db
占了16个字节


struct E
|a|空|空|空|i|i|i|i|
|----|----|—|—|—|-|-|-|-|-|
|i2|i2|i2|i2|空|空|空|空|
|db||||||||||
占了24个字节


上面都是针对结构体由简单的基本类型进行的解读,如果是复合类型呢?加进去数组或者其他结构体呢?这种存在就比较特殊,属于好好的内存块里面凸出去的大头,就像脸上的粉刺,让脸看得没有那么整洁雅观。
其实加进去数组也没什么,结构体也是基本类型的集合,虽说不至于拆开数组留空,但填补空缺还是够的,最要命的还是内嵌结构体。如下:

struct Name {
    
    
    char *surname;
    char *name;
};

struct A {
    
    
    int a;
    struct Name name;
    float f;
}sa;
struct B {
    
    
    int a;
    float f;
    struct Name name;
}sb;
struct C {
    
    
    struct Name name;
    int a;
    float f;
}sc;
printf("ptr: %d, int: %d, float: %d, Name: %d\n", sizeof(char *), sizeof(int), sizeof(float), sizeof(struct Name));
//辅助信息:"ptr: 8, int: 4, float: 4, Name: 16"
printf("A: %d, B: %d, C: %d\n", sizeof(struct A), sizeof(struct B), sizeof(struct C));
//"A: 32, B: 24, C: 24"
printf("A: %p, %p\nB: %p, %p\nC: %p, %p\n", &sa.a, &sa.name, &sb.f, &sb.name, &sc.name, &sc.a);
/*辅助性地址信息
A: 00000000004079C0, 00000000004079C8
B: 00000000004079A4, 00000000004079A8
C: 0000000000407980, 0000000000407990
*/

上面的例子就是自定义了一个存放两个指针的算是规整的结构体,然后把它作为大头放进三个同样存放基本类型的结构体中,分不同顺序来进行实验,直观点的内存结构如下:

struct A

a a a a
name
name
f f f f
占据32位字节,name独占16字节,分成8字节,a和f被name分开,所以各自占了8字节,但类型限制,实占4字节,其余字节位空置


struct B

a a a a f f f f
name
name
占24字节,a和f合在一起凑足8字节,name独占两个8字节,同理,struct C也是一样构造


struct C

name
name
a a a a f f f f
和上面的大B同理。上面就很明白了,哪怕是复合类型如结构体,也不是简单看其整体内存,在这部分其实要有个上限,那就是系统限定的字节对齐最大单位,当所有成员中的最大成员超出这个单位,那就按系统的来。64位系统限定最大对齐为8字节,32则是限定为4字节。

字节对齐的意义


在没有字节对齐的时候,结构体的内存分配和数组一样,都贴在一起的,这样一来就会出现那么几个数据凑成奇数内存块,而CPU读取数据是根据数据总线来读取的,16位数据总线可以一次读取两字节的数据,32位数据总线一次能读取4字节数据,64位系统则是8字节数据。当char型成员和int型成员凑在一起时为5字节内存,32位系统就没办法一次读取正确,需要读取第二次,所以拓展开,char成员占4字节内存的一字节,其余置空,这样对于CPU就能每次读取正确。这算是空间内存换取时间的意思了。字节对齐的存在是针对CPU的读取效率问题。

手动字节对齐


当两台机器进行通信,它们需要使用一种数据结构传输信息,那它们采取的字节对齐规则一致就显得很有必要,这种时候就可以进行手动的字节对齐规则调整。方法有两种,一种是预编译中设定规则,另一种就是数组声明时设定对齐规则,如下:

//设定其后的代码中字节对齐以n为准
#pragma pack(n)
struct A {
    
    
    ...//balabala
};
//设定其后的代码中字节对齐根据默认进行
#pragma pack()


struct B {
    
    
    ...
}__attribute__((packed));
//上面的__attribut__是一个给编译器看的设置,重点关注里面的参数
//packed参数,表示按照实际来进行字节对齐,也就是不对齐

末言

以上仅为个人参考书籍博客等资料而后实验所得,一家之言。

猜你喜欢

转载自blog.csdn.net/weixin_44948269/article/details/127711012