C语言中,结构体类型属于一种构造类型(其他的构造类型还有:数组类型,联合类型)。本文主要介绍关于结构体以下几部分。

1、概念

为什么要有结构体?

因为在实际问题中,一组数据往往有很多种不同的数据类型。例如,登记学生的信息,可能需要用到 char型的姓名,int型或 char型的学号,int型的年龄,char型的性别,float型的成绩。又例如,对于记录一本书,需要 char型的书名,char型的作者名,float型的价格。在这些情况下,使用简单的基本数据类型甚至是数组都是很困难的。而结构体(类似Pascal中的“记录”),则可以有效的解决这个问题。
结构体本质上还是一种数据类型,但它可以包括若干个“成员”,每个成员的类型可以相同也可以不同,也可以是基本数据类型或者又是一个构造类型。
结构体的优点:结构体不仅可以记录不同类型的数据,而且使得数据结构是“高内聚,低耦合”的,更利于程序的阅读理解和移植,而且结构体的存储方式可以提高CPU对内存的访问速度。

结构声明(structure declaration)

结构声明(也见有称做定义一个结构体)是描述结构如何组合的主要方法。
一般形式是:
struct 结构名{
成员列表
};
struct关键词表示接下来是一个结构。
如声明一个学生的结构:

struct Student{         //声明结构体char name[20];      //姓名int num;            //学号float score;        //成绩
};

上面的声明描述了一个包含三个不同类型的成员的结构,但它还没创建一个实际的数据对象,类似C++中的模板。每个成员变量都用自己的声明来描述,以分号结束。花括号之后的分号表示结构声明结束。结构声明可以放在函数外(此时为全局结构体,类似全局变量,在它之后声明的所有函数都可以使用),也可以放在函数内(此时为局部结构体,类似局部变量,只能放在该函数内使用,如果与全局结构体同名,则会暂时屏蔽全局结构体)。

要定义结构变量,则一般形式是:
struct 结构体名 结构体变量名;
如:

struct Student stu1;    //定义结构体变量


1)、结构体变量的定义可以放在结构体的声明之后:

struct Student{         //声明结构体char name[20];      //姓名int num;            //学号float score;        //成绩
};
struct Student stu1;    //定义结构体变量
  • <

2)、结构体变量的定义也可以与结构体的声明同时,这样就简化了代码:

struct Student{        char name[20];       int num;             float score;
}stu1;                  //在定义之后跟变量名

3)、还可以使用匿名结构体来定义结构体变量:

struct {                //没有结构名char name[20];       int num;            float score;
}stu1;

但要注意的是这样的方式虽然简单,但不能再次定义新的结构体变量了。

访问结构成员

虽然结构类似一个数组,只是数组元素的数据类型是相同的,而结构中元素的数据类型是可以不同的。但结构不能像数组那样使用下标去访问其中的各个元素,而应该用结构成员运算符点(.)。即访问成员的一般形式是:
结构变量名 . 成员名
如 stu1 . name 表示学生stu1的姓名。

但如果结构体中的成员又是一个结构体,如:

struct Birthday{                //声明结构体 Birthdayint year;int month;int day;
};
struct Student{                 //声明结构体 Studentchar name[20];              int num;                    float score;                 struct Birthday birthday;   //生日
}stu1;

则用 stu1.birthday.year 访问出生的年份。

结构体变量的初始化

1)、结构体变量的初始化可以放在定义之后:

可以对结构体的成员逐个赋值:

struct Student stu1, stu2;      //定义结构体变量
strcpy(stu1.name, "Jack");
stu1.num = 18;
stu1.score = 90.5;

注意:不能直接给数组名赋值,因为数组名是一个常量。如:

stu1.name = "Jack"; //…main.c:26:15: Array type 'char [20]' is not assignable

或者可以对结构体进行整体赋值:

stu2 = (struct Student){"Tom", 15, 88.0};

注意:此时要进行强制类型转换,因为数组赋值也是使用{},不转换的话系统无法区分!如:

int arr[5] = {1, 2, 3, 4, 5};       //数组的初始化
stu2 = {"Tom", 15, 88.0};           //…main.c:31:12: Expected expression

2)、结构体变量的初始化也可以与定义同时:

struct Student{                 //声明结构体 Studentchar name[20];               int num;                    float score;
}stu = {"Mike", 15, 91};        //注意初始化值的类型和顺序要与结构体声明时成员的类型和顺序一致

此时不需要强制类型转换
也可以部分初始化:

struct Student stu4 = {.name = "Lisa"};

也可以按照任意的顺序使用指定初始化项目:

    struct Student st = { .name = "Smith",.score = 90.5,.num = 18 };

3)、可以用一个已经存在的结构体去初始化一个新的相同类型的结构体变量,是整体的拷贝(每一个成员都一一赋值给新的结构体变量),而不是地址赋值。如:

stu3 = stu1;
printf("stu1 addr: %p\nstu3 addr: %p\n", &stu1, &stu3);
printf("stu1.num: %d\nstu3.num: %d\n", stu1.num, stu3.num);
printf("stu1.num addr: %p\nstu3.num addr: %p\n", &stu1.num, &stu3.num);
//输出结果:
stu1 addr: 0x10000104c
stu3 addr: 0x100001084
stu1.num: 18
stu3.num: 18
stu1.num addr: 0x100001060
stu3.num addr: 0x100001098

2、结构体变量的存储原理

1)结构体数据成员对齐的意义

内存是以字节为单位编号的,某些硬件平台对特定类型的数据的内存要求从特定的地址开始,如果数据的存放不符合其平台的要求,就会影响到访问效率。所以在内存中各类型的数据按照一定的规则在内存中存放,就是对齐问题。而结构体所占用的内存空间就是每个成员对齐后存放时所占用的字节数之和。
计算机系统对基本数据类型的数据在内存中存放的限制是:这些数据的起始地址的值要求是某个数K的倍数,这就是内存对齐,而这个数 K 就是该数据类型的对齐模数(alignment modulus)。这样做的目的是为了简化处理器与内存之间传输系统的设计,并且能提升读取数据的速度。
结构体对齐不仅包括其各成员的内存对齐(即相对结构体的起始位置),还包括结构体的总长度。

2)结构体大小的计算方法和步骤
i. 将结构体内所有数据成员的长度值相加,记为 sum_a ;
ii. 将各数据成员为了内存对齐,按各自对齐模数而填充的字节数累加到sum_a上,记为sum_b。
对齐模数是 #pragma pack 指定的数值与该数据成员自身长度相比较得到的数值较小者。该数据相对起始位置应该是对齐模数的整数倍。
iii. 将和 sum_b 向结构体模数对齐。
该模数则是 #pragma pack 指定的数值与结构体内最大的基本数据类型成员长度相比较得到的数值较小者。结构体的长度应该是该模数的整数倍。

数据类型自身对齐:

所谓“对齐在N上”,是指“存放的起始位置是%N = 0”.

3)在没有#pragma pack宏的情况下:
例子1:

内存分配状态为:

对于结构体的第一个成员 a,起始位置为0x…38 (也为 4 的倍数),所占内存为 0x…38 ~ 0x…3b,共占4个字节;
对于结构体的第二个成员 b,自身长度为1,对齐模数也为1,所以内存分配可以紧接着a的结尾位置 0x…3b,所以起始位置为 0x…3c,共占1个字节;
对于结构体的第三个成员 c,自身长度为2,对齐模数也为2,所以起始位置距离a 的起始位置应该是2的倍数,所以 0x…3d处只距离5,不符合要求,所以空着,继续往下找,而在 0x…3e处满足要求,所以可以作为c的起始位置,共占2个字节;
此时3个成员及其中间空着的位置所占内存单元总和为8,而结构体内最大的基本数据成员是 a,其长度为4,所以结构体模数为 4,而8是4的倍数,满足要求,故不再加内存。

例子2:
与例子1相比,三个类型的声明顺序变了:

内存分配状态为:

要注意的是,对 a而言,对齐模数为 4,所以当 b的起始位置在0x7f…830之后,0x7f…831、0x7f…832、0x7f…833的位置距离起始位置0x7f…830分别是1,2,3,都不是 4 的倍数,所以那三个位置都空着,直到0x7f…834才满足要求,所以作为 a 的起始位置。当最后 一个成员 c 占的内存末尾在0x7f…839时,所有数据成员及其之间的空位所占内存单元总和为10,而结构体模数为4,10不是4的倍数,所以要扩大到12才满足要求,此时又多了2个空位置,就是0x7f…83a和0x7f…83b。

例子3:
当结构体中有数组时:

内存分配状态为:

亦即相同类型数据的数组之间多分配的空间会被相邻数组的元素所占用。

4)在存在#pragma pack宏的情况下:
方法类似,只是模数可能会按上面说的规则而有所变化。

内存分配状态为:

注意,当没有#pragma pack(2)时,成员a要确定自身的q起始位置,是以自身的长度4为对齐模数,但有了#pragma pack(2),则将括号里的2与a的长度4比较,2为较小者,所以以2为a的对齐模数,即地址从0x7f…839往下找到0x7f…83a时,已经距离结构体的起始位置0x7f…838为2,是2的倍数,满足要求(虽然不是4的倍数),可以作为a的起始位置。而最后,所有数据成员及其之间的空位所占内存单元总和为8,因为2和4(结构体中最大的数据成员长度)的较小者为2,而8是2的倍数,所以刚好满足要求,不用在分配空位置,所以结构体总长度即为8。

3、结构体数组

结构类型作为一种数据类型,也可以像基本数据类型那样,作为数组的元素的类型。元素属于结构类型的数组成为结构型数组。如开头提出的问题,生活中经常用到结构数组来表示具有相同数据结构的一个群体,如一个班的学生的信息,一个书店或图书馆的书籍信息等。

1)结构数组定义
一般格式:
struct 结构名 {
成员列表
} 数组名[数组长度];
如:

struct Student{                 //声明结构体 Studentchar name[20];int num;float score;
}stu[5];                        //定义一个结构结构数组stu,共有5个元素

2)结构数组的初始化

定义结构数组的同时进行初始化

struct Student stu[2] = {{"Mike", 27, 91},{"Tom", 15, 88.0}};  

先定义,后初始化
整体赋值:

stu[2] = (struct Student){"Jack", 12, 85.0};

或者将结构体变量的成员逐个赋值:

strcpy(stu[3].name, "Smith");
stu[3].num = 18;
stu[3].score = 90.5;

输出结构体:

//结构体数组的长度:
int length = sizeof(stu) / sizeof(struct Student);
//逐个输出结构数组的元素
for (int i = 0; i < length; i++) {printf("姓名:%s  学号:%d  成绩:%f \n", stu[i].name, stu[i].num, stu[i].score);
}

//输出结果:

在这个例子中,要注意的是:

4、结构与指针

当一个指针变量用来指向了一个结构变量,这个指针就成了结构指针变量。
结构指针变量中的值是所指向的结构变量的首地址。可以通过指针来访问结构变量。

1)定义结构指针变量的一般形式:
struct 结构名 * 结构指针变量名
如:

struct Student *pstu;       //定义了一个指针变量,它只能指向Student结构体类型的结构体变量

结构指针变量的定义也可以与结构体的定义同时。而且它必须先赋值后使用。
数组名表示的是数组的首地址,可以直接赋值给数组指针。但结构变量名只是表示整个结构体变量,不表示结构体变量的首地址,所以不能直接赋值给结构指针变量,而应该使用 & 运算符把结构变量的的地址赋值给结构指针变量。即:

注意:结构名、结构变量名、结构体指针的区别。

2)通过结构指针间接访问成员值

访问的一般形式:
(*结构指针变量). 成员名 或 结构指针变量 -> 成员名
如:

(*pstu).name
pstu->name

注意(pstu)的小括号不能省略,因为成员符“.”优先级为1,取地址符“”优先级为2,去掉括号就相当于*(pstu.name)了。

5、结构体的嵌套

1)结构体中的成员可以又是一个结构体,构成结构体的嵌套:

struct Birthday{                //声明结构体 Birthdayint year;int month;int day;
};
struct Student{                 //声明结构体 Studentchar name[20];              int num;                    float score;                 struct Birthday birthday;   //生日
};


2)结构体不可以嵌套跟自己类型相同的结构体,但可以嵌套定义自己的指针。如:

struct Student{                 //声明结构体 Studentchar name[20];int num;float score;struct Student *friend;     //嵌套定义自己的指针
}

3)甚至可以多层嵌套:

struct Time{                    //声明结构体 Timeint hh;                     //时int mm;                     //分int ss;                     //秒
};
struct Birthday{                //声明结构体 Birthdayint year;int month;int day;struct Time dateTime        //嵌套结构
};
struct Student{                 //声明结构体 Studentchar name[20];int num;float score;struct Birthday birthday;   //嵌套结构
}
//定义并初始化
struct Student stud = {"Jack", 32, 85, {1990, 12, 3, {12, 43, 23}}};
//访问嵌套结构的成员并输出
printf("%s 的出生时刻:%d时 \n", stud.name, stud.birthday.dateTime.hh);
//输出结果:Jack 的出生时刻:12时 

注意如何初始化和对嵌套结构的成员进行访问。

6、结构与函数

结构体的成员可以作为函数的参数,属于值传递(成员是数组的除外)。如:

struct Student{                 //声明结构体 Studentchar name[20];int num;float score;
};
void printNum(int num){         //定义一个函数,输出学号printf("num = %d \n", num);
}struct Student student0 = {"Mike", 27, 91};printNum(student0.num);     //调用printNum 函数,以结构成员作函数的参数
//运行结果:num = 27 

注意,函数printNum并不知道也不关心实际参数是不是结构成员,它只要求实参是int类型的就可以了。

结构变量名也可以作为函数的参数传递,如:

void PrintStu(struct Student student){      //定义 PrintStu 函数,以结构变量作函数的形参student.num = 100;                      //修改学号printf("PrintStu 修改后:姓名: %s, 学号: %d, 内存地址: %p \n", student.name, student.num, &student);
}struct Student student0 = {"Mike", 27, 91};PrintStu(student0);                     //调用 PrintStu 函数,以结构变量名作函数的参数printf("           原来:姓名: %s, 学号: %d,  内存地址: %p \n", student0.name, student0.num, &student0);

//输出结果:

形参和实参的地址不一样,是在函数中创建了一个局部结构体,然后实参对形参进行全部成员的逐个传送,在函数中对局部结构体变量进行修改并不影响原结构体变量。这样传送的时间空间开销都比较大,特别是当成员有数组的时候,程序效率较低。所以可以考虑使用指针:

void PrintStu2(struct Student *student){      //定义 PrintStu2 函数,以结构指针作函数的形参student->num = 100;                       //修改学号printf("PrintStu2 修改后:姓名: %s, 学号: %d, 内存地址: %p \n", student->name, student->num, student);
}struct Student student0 = {"Mike", 27, 91};PrintStu2(&student0);                     //调用 PrintStu 函数,以结构变量的地址作函数的参数printf("           原来:姓名: %s, 学号: %d,  内存地址: %p \n", student0.name, student0.num, &student0);

//输出结果:

形参和实参的地址是一样的,所以是地址传递,在 PrintStu2 函数中,student 与&student0 指向同一块内存单元,用指针student修改结构变量会影响原结构变量。

参考:

https://www.cnblogs.com/magay/p/9561813.html
https://www.cnblogs.com/snowsad/p/11886462.html

C语言中的结构体(struct)详解相关推荐

  1. (15)System Verilog结构体struct详解

    (15)System Verilog结构体struct详解 1.1 目录 1)目录 2)FPGA简介 3)System Verilog简介 4)System Verilog结构体struct详解 5) ...

  2. C语言结构体struct详解与实例

    目录 1.定义 2.应用 2.1初始化 2.2使用 3.结构体对齐规则与存储 1.定义 C语言中结构体(struct关键字定义)是一种自定义数据类型.通过结构体的定义可以将多种不同类型数据形成一个组合 ...

  3. C语言中的结构体——struct

    结构体是C语言中很重要的知识点,结构体使得C语言有能力去描述复杂类型.         我们知道C语言中有许多类型,如:int.char.double······,但是现实世界中存在很多复杂的对象,不 ...

  4. Go语言中的结构体 (struct)

    Golang官方称Go语言的语法相对Java语言而言要简洁很多,但是简洁背后也灵活了很多,所以很多看似很简单的代码上的细节稍不注意就会产生坑.本文主要对struct结构体的相关的语法进行总结和说明. ...

  5. C语言结构体struct详解

    目录 一.struct的概念 1.类比: 2.含义详解: 二.结构体变量的定义 1.创建结构体类型的时候定义 2.具体使用的时候定义 三.如何得到结构体成员? 1.通过 . 点操作符得到结构体成员 2 ...

  6. C/C++结构体struct详解

    结构体定义 typedefstruct 用法详解和用法小结 typedef是类型定义的意思.typedef struct 是为了使用这个结构体方便. 具体区别在于: 若struct node{}这样来 ...

  7. C 结构体 struct 详解

    数组(Array),它是一组具有相同类型的数据的集合.但在实际的编程过程中,我们往往还需要一组类型不同的数据,例如对于学生信息登记表,姓名为字符串,学号为整数,年龄为整数,所在的学习小组为字符,成绩为 ...

  8. go面向对象编程:结构体struct详解、结构体实例的创建方式、结构体之间的转换(type取别名的使用)、方法的注意事项及与函数的区别

    入门示例 package main import "fmt" //定义老师结构体,将老师中的各个属性 统一放入结构体中管理: type Teacher struct{//变量名字大 ...

  9. 关于C语言中的结构体所占的字节数 sizeof(struct)

    C语言中的结构体所占的字节数 ,用sizeof求一个结构体的大小 C语言中用sizeof求结构体所占字节数的详细方法. 一.非嵌套结构体 struct one{char a; // 1(char所占字 ...

  10. C语言中的结构体,结构体中数组初始化与赋值

    最近写c语言中的结构体遇到了些问题,从网上找了些资料如下: 结构体是连续存储的,但由于结构体中成员类型各异,所以会存在内存对齐问题,也就是内存里面会有空档,具体的对齐方式这里 暂不讨论: 1.结构体的 ...

最新文章

  1. R语言生存分析COX回归分析实战:放疗是否会延长胰脏癌症患者的生存时间
  2. CentOS 6.2 下samba 服务的配置
  3. 北大核刊最新版2020目录_最新版CSSCI来源期刊目录及增减变化!
  4. 赋值后页面不渲染_第七节:框架搭建之页面静态化的剖析
  5. android 关闭软键盘_HTC官方社区明天正式关闭,收入持续下滑或是直接原因
  6. 最小生成树的Kruskal算法实现
  7. 全字段排序 VS rowid 排序
  8. Java应用程序中的内存泄漏和内存管理
  9. html中评论应该怎么写,HTML-评论
  10. 阿里云 ESSD 采用自研新一代存储网络协议,打造“超级高速”
  11. eclipse中项目内存溢出问题
  12. mysql合并多条纪录字段_mysql合并多条记录的单个字段去一条记录
  13. 学php应该怎么学习数学,数学是怎样学好的 零基础怎么自学数学
  14. arm 服务器优势,零的突破 戴尔正式宣布基于ARM架构服务器
  15. VB 提取TextBox 文本框中指定一行字符串
  16. c#和javascript分别轻松实现计算24点
  17. DBMS_ERRLOG记录DML错误日志(一)
  18. 7月最强书单丨博文视点新品畅销TOP10,让技术带你燃爆整个7月
  19. python图形编程基础知识_AI-图像基础知识-01|python基础教程|python入门|python教程
  20. 网络退化、过拟合、梯度消散/爆炸

热门文章

  1. 【STC15】 STC15W408AS SBUS通信例程
  2. 海外媒体推广,企业如何在YouTube做好海外网红KOL营销?
  3. 一分钟定制CocosCreator加载页面
  4. python 特殊方法是什么_Python类的特殊方法
  5. 甲基乙烯基硼酸 cas7547-97-9/异环己酰亚胺 cas4538-37-8
  6. 利用gdal的RasterIO进行最近邻、双线性、三次卷积重采样的重采样
  7. unordered_map详解
  8. vb.net 教程 11-1 打印组件 4 PrintDocument 1
  9. 同款蓝牙耳机为什么会串联_蓝牙耳机凭什么成网红明星标配时尚单品?揭秘三大原因...
  10. uboot命令实践:fat系列命令实践