深入C语言:探索结构体的奥秘

简介: 深入C语言:探索结构体的奥秘

一、什么是结构体

首先我们为什么要用到结构体?


我们都已经学了很多int char …等类型还学到了同类型元素构成的数组,以及取上述类型的指针,在一些小应用可以灵活使用,然而,在我们实际应用中,每一种变量进行一次声明,再结合起来显然是不太实际的。


类如一位学生的信息管理,他可能有,姓名(char),学号(int)成绩(float)等多种数据。如果把这些数据分别单独定义,就会特别松散、复杂,难以规划,因此我们需要把一些相关的变量组合起来,以一个整体形式对对象进行描述,这就是结构体的好处。


官方来说结构体就是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。说到集合,数组也是集合,但是不同的是数组只能是相同类型元素的集合。


二、结构体的使用

2.1 结构体的声明

struct tag
{
  member1;
  member2;
} variable-list;
  • struct是结构体关键字
  • tag是结构体的标签名,是自定义的,如book,student等。
  • struct tag就是结构体类型
  • member1 , member2 是结构体成员,是标准的变量定义,比如 int i; 或者 float f;,也可以定义数组char s[20]。
  • variable-list 结构变量,定义在结构的末尾,最后一个分号之前,您可以指定一个或多个结构变量,也可以省略。


2.2 结构体的基础结构和类型

2.2.1 普通结构体

(一)先定义结构体类型,再定义结构体变量


struct   student //结构体类型 或 结构体名
{       
 int num;
 char  name[20];     //结构体成员
 char sex;
 int age; 
 float score;
 char addr[30];
 };
 struct student stu1,stu2;       //结构体变量

(二)定义结构体类型的同时定义结构体变量


struct   data   // 结构体类型 或结构体名
 {      
  int day int month;   //结构体成员
  int year
 }time1,time2;   //结构体变量

2.2.2 嵌套结构体

结构体和函数一样可以嵌套使用,也就是说在一个结构体中可以使用另外一个结构体,也可以包含指向自己结构体类型的指针,而通常这种指针的应用是为了实现一些更高级的数据结构如链表和树等。


struct student
{
  int age;//年龄
  char sex[8];//性别
  int weight;//体重
  char tele[20];//电话
};
struct people
{
  int num;//序号
  struct student s;//学生
};
struct list
{
  int num;//序号
  struct list* next;//指向自己的结构体指针
};


但是结构体中不能包含一个同类型的结构体变量,因为这样结构体大小无法确定


struct node
{
  int num;
  struct node s;
  //错误定义
};

2.2.3 匿名结构体

匿名结构体是不定义结构体名称,而直接定义其成员的一种方式。这种结构体只能使用一次。并且两个匿名结构体的成员如果都相同的话,这两个匿名结构体也是不同的。


struct//匿名结构体
{
  int num;
  char name[20];
  //.....
};
struct
{
  int a;
  char b;
  float c;
}x;
struct
{
  int a;
  char b;
  float c;
}*p;
p = &x;//两种结构体不同无法赋值


2.3 结构体的初始化

(一)定义时初始化


#include <stdio.h>
#include <stdlib.h>
struct books    // 结构体类型
{
  char title[50];
  char author[50];    //结构体成员
  char subject[100];
  int book_id;
}book={"C 语言","xingaosheng","编程语言",12345};  //结构体变量的初始化
int main()
{
  printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
  return 0;
}

(二)先定义再进行初始化


typedef struct student
{
  int age;//年龄
  char sex[8];//性别
  int weight;//体重
  char tele[20];//电话
}stu;
struct people
{
  int num;//序号
  struct student s;//学生
};
int main()
{
  struct student s = { 20,"nan",50,"1233455" };//创建变量并初始化
  //struct student s;
  //s= { 20,"nan",50,"1233455" };错误
  stu t = { 18,"nan",45,"123444" };
  struct people p = { 1,{20,"nan",50,"1233455"} };
  //嵌套结构体的初始化
  return 0;
}


2.4 结构体的成员访问

2.4.1 直接访问

结构体成员的直接访问是通过点操作符(.)访问的。点操作符接受两个操作数。如下所⽰:


struct Point
{
  int x;
  int y;
};
int main()
{
  struct Point p = { 1,2 };
  printf("x: %d y: %d\n", p.x, p.y);
  return 0;
}

输出结果:


x:1  y:2  


2.4.2 间接访问

除了通过(.)操作符直接访问,我们也可以通过结构体地址,利用(->)操作符间接访问。


#include <stdio.h>
struct Point
{
  int x;
  int y;
};
int main()
{
  struct Point p = { 3, 4 };
  struct Point* ptr = &p;//结构体指针
  ptr->x = 1;
  ptr->y = 2;
  printf("x = %d y = %d\n", ptr->x, ptr->y);
  return 0;
}

输出结果:


x:1  y:2  


需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。


三、结构体数组

结构体数组:是指数组中的每一个元素都式结构体, 结构体数组常被用来表示一个拥有相同数据结构的群体。


struct stu
{
    char name[20];  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5];
//表示一个班有5个人

结构体数组在定义的时候也可以初始化


struct stu
{
    char name[20];  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5] = {
    {"Li ping", 5, 18, 'C', 145.0},
    {"Zhang ping", 4, 19, 'A', 130.5},
    {"He fang", 1, 18, 'A', 148.5},
    {"Cheng ling", 2, 17, 'F', 139.0},
    {"Wang ming", 3, 17, 'B', 144.5}
};

使用方法:


#include <stdio.h>
#include <stdlib.h>
struct stu
{
  char name[20];
  int num;
  int age;
  char group;
  float score;
} ban[5] = {
  {"xing",5,18,'c',145.0},{"ao",4,19,'a',130.5},
  {"sheng",1,18,'a',148.5},{"pei",2,17,'f',139.0},
  {"yuan",3,17,'b',144.5}
};      // 表示一个班有5个人
int main()
{
  int i, n = 0;
  float sum = 0;
  for (i = 0; i < 5; i++) {
  sum += ban[i].score;
  if (ban[i].score < 140) n++;
  }
  printf("sum=%.2f\naverage=%.2f\nn=%d\n", sum, sum / 5, n);
  return 0;
}

输出结果:


sum=707.50


average=141.50


n=2


🟥四、结构体指针(重点)

4.1 指向结构体变量的指针

4.1.1 结构体指针的定义

可以定义指向结构体的指针,方式与定义指向奇特类型变量的指针类似


定义方式:struct 结构体名*结构体指针名


struct books*struct_pointer

其中books是结构体名,struct_pointer为结构体指针名


定义之后可以在上述定义的指针变量中存储结构变量的地址

struct_pointer = &Book1;

为了使用指向该结构的指针访问结构的成员,必须使用->运算符

struct_pointer->title;

代码示例如下:


struct stu   // 结构体类型 或 结构体名
{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;

也可以定义结构体的同时定义结构体指针:


struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = { "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;

注意:


  • ▶ 结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&符号,所以给p赋值只能写成。
  • ▶ 结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,例如上面的stu,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量(例如stu1)才包含实实在在的数据,才需要内存来存储。不可能去取一个结构体名的地址,也不能将它赋值给其他变量。


4.1.2 结构体指针的成员访问

通过结构体指针可以获取结构体成员,一般形式为:


(*pointer).memberMane                  //pointer为结构体指针名
  pointer->memberName             // 或者
  • 第一种写法中, .  的优先级高于  * ,(*pointer)两边的括号不能少。如果去掉括号写成*pointer.memberName,那么就等效于*(pointer.memberName),这样意义就不对了。
  • 第二种写法中,-> 是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员,
  •     这也是 -> 在C语言中的唯一用途。

4.1.3 结构体指针的使用


前面两种写法是等效的,我们通常采用第二种写法,这样更加直观。


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct stu
{
  char name[20];
  int num;
  int age;
  char group;
  float score;
}stu1 = { "Tom",12,18,'A',136.5 }, * pstu = &stu1;
int main()
{
  // 读取结构体成员的值
  printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", (*pstu).name, (*pstu).num, (*pstu).age, (*pstu).group, (*pstu).score);
  printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", pstu->name, pstu->num, pstu->age, pstu->group, pstu->score);
  return 0;
}


运行结果如下:


14e218d74004e1811b224fc5c166d715_c3ab2ef66776f2e612fd999047f0bf59.png


4.2 指向结构体数组的指针

在我们想要用指针访问结构体数组的第n个数据时可以用:


struct Student
{ 
  char cName[20];
  int number;
  char csex;  
}student1;     //结构体变量
struct Student stu1[5];   //结构体数组
struct Student*p;        // 结构体指针
p=stu[n];
(++p).number//是指向了结构体数组下一个元素的地址

结构体指针与结构体数组的联合使用:


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct stu    //结构体类型 或结构体名
{
  char name[20];
  int num;
  int age;   //结构体成员
  char group;
  float score;
}stus[]{           //结构体数组
  {"Zhou ping", 5, 18, 'C', 145.0},
  {"Zhang ping", 4, 19, 'A', 130.5},
  {"Liu fang", 1, 18, 'A', 148.5},
  {"Cheng ling", 2, 17, 'F', 139.0},
  {"Wang ming", 3, 17, 'B', 144.5}
}, * ps;   //结构体指针
int main()
{
  //求数组长度 : sieof(结构体变量)/sizeof(结构体类型名)
  int len = sizeof(stus) / sizeof(struct stu);
  printf("Name\t\tNum\tAge\tGroup\tScore\t\n");
  for (ps = stus; ps < stus + len; ps++)
  {
  printf("%s\t%d\t%d\t%c\t%.1f\n", ps->name, ps->num, ps->age, ps->group, ps->score);
  }
  return 0;
}


输出结果如下:


6665629f559a65d2e8b23a1f289d67f9_6e45ec8fa16782450fa01018c2443827.png


4.3 结构体成员是指针类型

代码示例如下:


struct Student
{
  char* Name;//这样防止名字长短不一造成空间的浪费
  int number;
  char csex;  
}student1;

注意:在使用时可以很好地防止内存被浪费,但是注意在引用时一定要给指针变量分配地址,如果你不分配地址,结果可能是对的,但是Name会被分配到任意的一的地址,结构体不为字符串分配任何内存存储空间具有不确定性,这样就存在潜在的危险


代码改进如下:


struct Student
{
  char* Name;
  int number;
  char csex;  
}stu,*stu;
stu.name=(char*)malloc(sizeof(char));//内存初始化

如果我们定义了结构体指针变量,他没有指向一个结构体,那么这个结构体指针也是要分配内存初始化的,他所对应的指针类型结构体成员也要相应初始化分配内存


struct Student
{
  char* Name;
  int number;
  char csex;  
}stu,*stu;
stu = (struct student*)malloc(sizeof(struct student));./*结构体指针初始化*/
  stu->name = (char*)malloc(sizeof(char));/*结构体指针的成员指针同样需要初始化*/

五、结构体的内存对齐

在熟悉了结构体的基本应用之后,下面我们要深入讨论的就是结构体大小,如何计算结构体的大小,就需要知道它在内存中是如何储存的。


而结构体在内存中存在结构体对齐的现象。


我们先参考以下代码:


struct S1
  {
  char c1;
  int i;
  char c2;
  };
  printf("%d\n", sizeof(struct S1));
  struct S2
  {
  char c1;
  char c2;
  int i;
  };
  printf("%d\n", sizeof(struct S2));
  struct S3
  {
  double d;
  char c;
  int i;
  };
  printf("%d\n", sizeof(struct S3));
  struct S4
  {
  char c1;
  struct S3 s3;
  double d;
  };
  printf("%d\n", sizeof(struct S4));


输出结果是:


12


8


16


32


如果直接计算结构体成员的所占的内存之和显然比这小,这是为什么呢?


C语言分配结构体内存时,遵循的是内存对齐规则,那什么是内存对齐规则呢?


内存对齐规则:


  1. 结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处
  2. 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
  3. 对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。(VS 中默认的值为 8 ,Linux中gcc没有默认对齐数,对⻬数就是成员⾃⾝的⼤⼩)
  4. 结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。
  5. 如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

为什么会存在内存对齐呢?相信大部分人都会有这个疑问,其实主要有两个原因:


  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两个8字节内存块中。


上面的代代码图示如下:



六、结构体位段

6.1 什么是位段

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可,所以C 语言有一种特别的数据结构名为位段,允许我们按位对成员进行定义,指定其占用的位数,单位为比特位(bit)。一般是用来节约内存,与结构体有两个不同:


位段的实现和结构体类似,只不过位段的成员的类型只能是

unsigned int 或者int类型,char类型的也可以。

每个成员名后面要加上:和数字


代码示例如下:


struct stu
{
  int a : 4;
  int b : 2;
};

后面的数字表示bite位。位段不存在对齐。

位段不具有跨平台性:


  1. 位段中没有规定在内存使用的过程中,是从左使用还是从右使用。
  2. 不能满足下一个成员使用的空间是舍弃还是保留的问题没有规定。
  3. int位段中无符号还是有符号的问题没有规定


6.2 结构体实现位段的内存分配

那么位段的分配到底是怎么样的呢?


当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃剩余的位还是利用呢?


struct S
{
  char a : 3;
  char b : 4;
  char c : 5;
  char d : 4;
};
struct S s = { 0 };
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
  1. 假设位段在一个字节内部是从高地址到低地址分配。
  2. 假设当一个结构体包含两个位段,第二个位段比较大,无法容纳于第一个位段剩余的位时, 是舍弃。

0d634caf608b80ce17aa4af399552108_1408a053e849a44f77830770fbb389f3.png


七、结构体传参

我们知道函数传参分为两种,一种是直接传参:直接传变量;一种是间接传参:通过传变量地址间接访问。


struct S1
{
  int p;
  int num;
};
//结构体传参
void print1(struct S1 s)
{
  printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S1* ps)
{
  printf("%d\n", ps->num);
}
int main()
{
        struct S1 s = { 1,2 };
  print1(s);  //传整个结构体
  print2(&s);  //传地址
  return 0;
}

但在结构体传参的时候,最好选择传址调用,有两个好处:

1.可以减少对空间的浪费

2.可以对里面的数据进行修改


🟥八、typedef关键字与结构体、结构体指针(重点)

8.1 使用typedef定义结构体

typedef用来定义新的数据类型,通常typedef与结构体的定义配合使用。使用typedef的目的使结构体的表达更加简练(所以说typedef语句并不是必须使用的。)


  1. struct 是用来定义新的数据类型——结构体
  2. typedef是给数据类型取别名。


定义一个名字为TreeNode的结构体类型(现在并没有定义结构体变量,并不占用内存空间):

struct TreeNode   // 结构体类型
{
        int Element;
        struct TreeNode* LeftChild;   //结构体成员
        struct TreeNode* RightChild;
};

为结构体起一个别名Node,这时Node就等价于struct TreeNode

typedef struct TreeNode Node;

将结构体的定义和typedef语句可以连在一起写:

typedef struct TreeNode   //结构体类型
{
        int Element;              //结构体成员
        struct TreeNode* LeftChild;
        struct TreeNode* RightChild;   
}Node;     // Node 是 struct TreeNode 的别名


注意 :不要与“定义结构体类型的同时定义结构体类型变量”混淆:

使用typedef关键字定义结构体类型 定义结构体类型的同时定义结构体类型变量


typedef struct student
{
        int age;
        int height;
}std;
std std1, std2;
//std相当于struct student  
struct student
{
        int age;
        int height;
}std1,std2;
struct student std3, std4;
//定义了student数据类型的结构体和std1、std2、std3、std4结构体变量

8.2 使用typedef定义结构体指针

使用typedef关键字用一个单词Node代替struct TreeNode,并定义指向该结构体类型的指针PtrToTreeNode:


struct TreeNode
{
        int Element;
        struct TreeNode* LeftChild;
        struct TreeNode* RightChild;
};
typedef struct TreeNode Node;   //用Node代替struct TreeNode
Node *PtrToTreeNode;            //定义指针

也可以简化如下所示:


typedef struct TreeNode
{
        int Element;
        struct TreeNode* LeftChild;
        struct TreeNode* RightChild;
}Node;                          //定义结构体并用Node代替struct TreeNode
Node *PtrToTreeNode;            //定义指针
相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1519 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
503 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
457 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析