程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

C语言结构体

balukai 2025-02-13 11:03:57 文章精选 8 ℃

本章专题脉络

1、结构体(struct)类型的基本使用

1.1 为什么需要结构体?

C 语言内置的数据类型,除了几种原始的基本数据类型,只有数组属于复合类型,可以同时包含多个值,但是只能包含相同类型的数据,实际使用场景受限。

举例1:

现有一个需求,编写学生档案管理系统,这里需要描述一个学生的信息。该学生的信息包括学号、姓名、性别、年龄、家庭住址等,这些数据共同说明一个学生的总体情况。

显然,这些数据类型各不相同,无法使用数组进行统一管理。

举例2:

隔壁老王养了两只猫咪。一只名字叫小黄,今年2岁,橘色;另一只叫小黑,今年3岁,黑色。请编写一个程序,当用户输入小猫的名字时,就显示该猫的名字,年龄,颜色。如果用户输入的小猫名错误,则显示老王没有这只猫。

传统的解决方法

尝试1:单独定义多个变量存储,实现需求。但是,多个变量,不便于数据的管理。

尝试2:使用数组,它是一组具有相同类型的数据的集合。但在编程中,往往还需要一组类型不同的数据,例如猫的名字使用字符串、年龄是int,颜色是字符串,因为数据类型不同,不能用一个数组来存放。

尝试3:C语言提供了结构体。使用结构体,内部可以定义多个不同类型的变量作为其成员。

考研中见的最多的就是指针和结构体结合起来构造结点(如链表的结点、二叉树的结点等)。

1.2 结构体的理解

C 语言提供了 struct关键字,允许自定义复合数据类型,将不同类型的值组合在一起,这种类型称为结构体(structure)类型。

C 语言没有其他语言的对象(object)和类(class)的概念,struct 结构很大程度上提供了对象和类的功能。

1.3 声明结构体

构建一个结构体类型的一般格式:

 struct 结构体名{ 
     数据类型1 成员名1;   //分号结尾
     数据类型2 成员名2; 
     ……
     数据类型n 成员名n;
 }; //注意最后有一个分号

举例:学生

 struct Student{       // 定义结构体:学生
     int id;           // 学号
     char name[20];    // 姓名
     char gender;      // 性别
     char address[50]; // 家庭住址
 };  

举例:通讯录

 struct Contacts{            
     char name[50];          //姓名
     int year;               //年
     int month;              //月
     int day;                //日
     char email[100];        //电子邮箱
     char phone_number[15]; //手机号
 };

1.4 声明结构体变量并调用成员

定义了新的数据类型以后,就可以声明该类型的变量,这与声明其他类型变量的写法是一样的。

声明结构体变量格式1:

 struct 结构体类型名称 结构体变量名;

注意,声明自定义类型的变量时,类型名前面,不要忘记加上 struct 关键字。

举例:

 struct Student stu1;

调用结构体变量的成员:

 结构体变量名.成员名 [= 常量或变量值]

举例:

 #include 
 #include 
 int main() {
 
     struct Student stu1; //声明结构体变量
     
     //调用结构体成员
     stu1.id = 1001;
     //stu1.name = "Tom"; //报错,不能直接通过赋值运算符来给字符数组赋值
     strcpy(stu1.name, "Tony");
     stu1.gender = 'M';
     strcpy(stu1.address, "北京市海淀区五道口");
 
     printf("id = %d,name = %s,gender = %c,address = %s\n", 
            stu1.id, stu1.name, stu1.gender, stu1.address);
 
     return 0;
 }

说明:

1)先声明了一个 struct Student类型的变量 stu1,这时编译器就会为 stu1 分配内存,接着就可以为 stu1 的不同属性赋值。可以看到,struct 结构的属性通过点( . )来表示,比如 id 属性要写成 stu1.id。

2)字符数组是一种特殊的数组,直接改掉字符数组名的地址会报错,因此不能直接通过赋值运算符来对它进行赋值。你可以使用字符串库函数 strcpy() 来进行字符串的复制操作。

声明结构体变量格式2:

除了逐一对属性赋值,也可以使用大括号,一次性对 struct 结构的所有属性赋值。此时,初始化的属性个数最好与结构体中成员个数相同,且成员的先后顺序一一对应。格式:

 struct 结构体名 结构体变量={初始化数据};

举例:

 //声明结构体
 struct Car {
   char* name;
   double price;
   int speed;
 };
 
 //声明结构体变量
 struct Car audi = {"audi A6L", 460000.99, 175};

注意:如果大括号里面的值的数量少于属性的数量,那么缺失的属性自动初始化为 0 。

 struct Student {
     int id;
     char name[20];
     char gender;
     int score;  //学生成绩
 
 };
 
 int main() {
     struct Student stu = {1001, "songhk", 'M'};
     printf("Name: %s\n", stu.name);
     printf("Score: %d\n", stu.score);
 
     return 0;
 }

声明结构体变量格式3:

方式2中大括号里面的值的顺序,必须与 struct 类型声明时属性的顺序一致。此时,可以为每个值指定属性名。

格式:

 struct 结构体名 结构体变量={.成员1=xxx,.成员2=yyy,...};

举例:

 struct Car audi = {.speed=175, .name="audi A6L"};

同样,初始化的属性少于声明时的属性,剩下的那些属性都会初始化为 0 。

声明变量以后,可以修改某个属性的值。

 struct Car audi = {.speed=175, .name="audi A6L"};
 audi.speed = 185;  //将 speed 属性的值改成 185

声明结构体变量格式4: 声明类型的同时定义变量

struct 的数据类型声明语句与变量的声明语句,可以合并为一个语句。格式:

 struct 结构体名 {
     成员列表
 } 变量名列表;

举例:同时声明了数据类型 Circle 和该类型的变量 c1

 struct Circle {
     int id;
     double radius;
 } c1;

举例:

 struct Employee {
     char name[20];
     int age;
     char gender;
     char phone[11];
 } emp1, emp2;

声明结构体变量格式5: 不指定类型名而直接定义结构体类型变量

如果类型标识符(比如Student、Circle、Employee等)只用在声明时这一个地方,后面不再用到,那就可以将类型名省略。 该结构体称为匿名结构体。

格式:

 struct {
     成员列表;
 } 变量名列表;

举例:

 struct {
     char name[20];
     int age;
     char gender;
     char phone[11];
 } emp1, emp2;

struct 声明了一个匿名数据类型,然后又声明了这个类型的两个变量emp1、emp2 。与其他变量声明语句一样,可以在声明变量的同时,对变量赋值。

举例:

 struct {
     char name[20];
     int age;
     char gender;
     char phone[11];
 } emp1 = {"Lucy", 23, 'F', "13012341234"},
   emp2 = {"Tony", 25, 'M', "13367896789"};

上例在声明变量 emp1 和 emp2 的同时,为它们赋值。

声明结构体变量格式6:使用 typedef 命令

使用 typedef 可以为 struct 结构指定一个别名,这样使用起来更简洁。

举例:

 //声明结构体
 typedef struct cell_phone {
     int phone_no;              //电话号码
     double minutes_of_charge;  //每分钟费用
 } Phone;
 
 //声明结构体变量
 Phone p = {13012341234, 5};

上例中, Phone 就是 struct cell_phone 的别名。声明结构体变量时,可以省略struct关键字。

这种情况下,C 语言允许省略 struct 命令后面的类型名。进一步改为:

 //声明匿名结构体
 typedef struct {
     int phone_no;
     double minutes_of_charge;
 } Phone;
 
 //声明结构体变量
 Phone p = {13012341234, 5};

进一步,在考研中,还会出现如下的声明方式:

 typedef struct {
     int phone_no;
     double minutes_of_charge;
 } Phone,*pPhone;

这里多了个*pPhone,其实在定义一个结点指针p时,Phone *p; 等价于 pPhone p;,前者的写法类似于int *a、char *b等更方便记忆,不必再加个pPhone p来增加记忆负担。所以在考研中我们不采用这种方法,统一删掉 *pPhone的写法。

说明:

1、在创建一个结构体变量后,需要给成员赋值。在没有给成员赋值的情况下调用,打印的值是垃圾数据,可能导致程序异常终止。

2、不同结构体变量的成员是独立的,互不影响,一个结构体变量的成员更改,不影响另外一个。

1.5 举例

练习:盒子案例

(1)编程创建一个Box结构体,在其中定义三个成员表示一个立方体的长、宽和高,长宽高可以通过控制台输入。

(2)定义一个函数获取立方体的体积(volume)。

(3)创建一个结构体,打印给定尺寸的立方体的体积。

 #include 
 
 // 1. 定义Box结构体
 struct Box {
     double length;
     double width;
     double height;
 };
 
 // 2. 获取立方体体积的函数
 double getVolume(struct Box box) {
     return box.length * box.width * box.height;
 }
 
 int main() {
 
     // 3. 创建结构体实例
     struct Box box;
     printf("输入长度:");
     scanf("%lf", &box.length);
 
     printf("输入宽度:");
     scanf("%lf", &box.width);
 
     printf("输入高度:");
     scanf("%lf", &box.height);
 
     // 调用函数获取体积并打印
     printf("体积为: %.2lf\n", getVolume(box));
 
     return 0;
 }

1.6 小 结

区分三个概念:结构体、结构体变量、结构体变量的成员。

  • 结构体是自定义的数据类型,表示的是一种数据类型。
  • 结构体变量代表一个具体变量。类比:
  • int num1 ; // int 是数据类型, 而num1是一个具体的int变量

    struct Car car1; // Car 是结构体数据类型,而car1是一个Car变量
  • Car 就像一个“汽车图纸”,生成出来的具体的一辆辆汽车,就类似于一个个的结构体变量。这些结构体变量都含有相同的成员,将结构体变量的成员比作“零件”,同一张图纸生产出来的零件的作用都是一样的。

2、进一步认识结构体

2.1 结构体嵌套

结构体的成员也是变量,那么成员可以是基本数据类型,也可以是数组、指针、结构体等类型 。如果结构体的成员是另一个结构体,这就构成了结构体嵌套。

举例:

 struct Date {    //声明一个结构体类型 struct Date 
     int year;    //年
     int month;   //月
     int day;     //日
 };
 
 struct Employee { //声明一个结构体类型 struct Employee
     int id;
     char name[20];
     int age;
     struct Date birthday; //成员birthday属于struct Date类型
 };

声明结构体变量并调用成员:

 #include 
 #include 
 
 int main(){
     struct Employee emp1;
 
     emp1.id = 1001;
     strcpy(emp1.name,"Tony");
     emp1.age = 24;
 
     emp1.birthday.year = 2001;
     emp1.birthday.month = 3;
     emp1.birthday.day = 12;
 
     return 0;
 }

说明:如果成员本身又属一个结构体类型,则要用若干个点( . ),一级一级地找到最低的一级的成员。比如,emp1.birthday.year。

赋值的时候还有多种写法:

 #include 
 
 int main() {
     //方式1:
     struct Employee emp1 = {1001, "Tony", 24, {1999, 10, 11}};
 
     //方式2:
     struct Date birthday = {2001, 5, 6};
     struct Employee emp2 = {1002, "Tom", 22, birthday};
 
     //方式3:
     struct Employee emp3 = {
             .id = 1003,
             .age = 24,
             .name = "Jerry",
             .birthday = {2001, 3, 16}};
 
     //方式4:
     struct Employee emp4 = {
             .id = 1003,
             .age = 27,
             .name = "Jerry",
             .birthday.year = 1998,
             .birthday.month = 8,
             .birthday.day = 12};
 
     return 0;
 }

举例:自我嵌套

单链表结构的结点定义如下:

 struct Node {
     int data;            //这里默认的是int型,如需其他类型可修改
     struct Node* next;   //指向Node型变量的指针
 };
 //等同于
 typedef struct Node {
     int data;
     struct Node *next;
 } LNode;

二叉树结构的结点定义如下:

 typedef struct BTNode {
     int data;              //这里默认的是int型,如需其他类型可修改
     struct BTNode *lchild;        //指向左孩子结点指针,在后续的二叉树章节中讲解
     struct BTNode *rchild;        //指向右孩子结点指针,在后续的二叉树章节中讲解
 } BTNode;

2.2 结构体占用空间

结构体占用的存储空间,不是各个属性存储空间的总和。为了计算效率,C 语言的内存占用空间一般来说,都必须是 int 类型存储空间的整数倍。如果 int 类型的存储是4字节,那么 struct 类型的存储空间就总是4的倍数。

 struct A{
     char a;
     int b;
 } s;
 
 int main() {
     printf("%d\n", sizeof(s)); // 8
     return 0;
 }

变量 s 的存储空间不是5个字节,而是占据8个字节。a 属性与 b 属性之间有3个字节的“空洞”。

2.3 结构体变量的赋值操作

同类型的结构体变量可以使用赋值运算符( = ),赋值给另一个变量,比如

 student1 = student2; //假设student1和student2已定义为同类型的结构体变量

这时会生成一个全新的副本。系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过去,即原样生成了一份数据。

也就是说,结构体变量的传递机制是值传递,而非地址传递。这一点跟数组的赋值不同,使用赋值运算符复制数组,不会复制数据,只是传递地址。

举例1:

 struct Car {
     double price;
     char name[30];
 } a = {.name = "Audi A6L", .price = 390000.99};
 
 int main() {
     struct Car b = a;
 
     printf("%p\n", &a); //结构体a变量的地址 00007ff75a019020
     printf("%p\n", &b); //结构体b变量的地址 000000a6201ffcd0
 
     printf("%p\n", a.name);  //结构体a变量的成员name的地址  00007ff719199028
     printf("%p\n", b.name);  //结构体b变量的成员name的地址  000000c2565ffd88
 
     a.name[0] = 'B';
     printf("%s\n", a.name); // Budi A6L
     printf("%s\n", b.name); // Audi A6L
 
     return 0;
 }

上例中,变量 b 是变量 a 的副本,两个变量的值是各自独立的,修改掉 b.name 不影响 a.name 。

举例2:将结构体内的字符数组改为字符指针

上个例子有个前提,就是 struct 结构的属性必须定义成字符数组,才可以复制数据。如果属性定义成字符指针,结果就不一样了。

 struct Car {
     char *name;
     double price;
 } a = {"Audi A6L", 390000.99};
 
 int main() {
     struct Car b = a;
 
     printf("%p\n", &a); //结构体a变量的地址 00007ff75a019020
     printf("%p\n", &b); //结构体b变量的地址 000000a6201ffcd0
 
     printf("%p\n", a.name); //结构体a变量的成员name的地址  00007ff7d778a000
     printf("%p\n", b.name); //结构体b变量的成员name的地址  00007ff7d778a000
 
     return 0;
 }

上例中, name 属性变成了一个字符指针,这时 a 赋值给 b ,此时的b变量仍然是新开辟的内存空间。但是,a 和 b的 name 成员保存的指针相同,也就是说两个属性共享同一个"Audi A6L"。

在C语言中,相同的字符串常量通常只会保存一份,即这些字符串常量共享相同的内存。当你声明多个指针变量并让它们指向相同的字符串常量时,它们实际上都指向相同的内存地址。字符串常量的共享,有助于减小程序的内存占用。

注意:C 语言没有提供比较两个自定义数据结构是否相等的方法,无法用比较运算符(比如 == 和 != )比较两个数据结构是否相等或不等。

【武汉科技大学2019研】已知书籍结构体定义如下,则对结构体变量bk的正确赋值是( )。

struct BOOK {
struct {
int year, month, day;
} publish;
} bk;

A.bk.year=1998; bk.month=11; bk.day=11; B.publish.year=1998; publish.month=11; publish.day=11; C.year=1998; month=11; day=11; D.bk.publish.year=1998; bk.publish.month=11; bk.publish.day=11;

【答案】D

【解析】变量bk是结构体BOOK的一个结构体变量,该变量含有一个成员变量publish,publish也是一个结构体变量,该结构变量含三个成员变量,分别是year、month、day,结构体变量中的成员变量不可直接访问,必须以结构体变量名.成员变量名形式访问,所以只能通过bk.publish.year形式访问到最内层的变量并为其赋值,答案选D。

3、结构体数组

3.1 对比结构体与数组

 //定义一个结构体A
 typedef struct{
     int a ;
     char b;
     float c;
 } A;
 //定义一个结构体变量
 A a;
 
 //定义一个数组类型的变量
 int b[3];
 

语句int b[3];定义了一个数组,名字为b,由3个整型分量组成。而语句A a;可以类似认为定义了一个数组,名字为a,只不过组成a数组的3个分量是不同类型的。对于数组b,b[0]、b[1]、b[2]分别代表数组中第1、第2、第3个同为int类型的元素的值。而结构体a中,a. a、a. b、a. c分别对应于结构体变量a 中第1、第2、第3个元素的值,两者十分相似。

如果有3个结构体A类型的元素,如何存储呢?使用结构体数组,即:A a[3]

对比:A a[3]和int b[3][3]:

a数组中的每个元素都是结构型且每个元素都有3个分量,可以把它类比成一个二维数组。例如, int b[3][3]。

3.2 结构体数组的声明

结构体数组:数组元素是结构体变量而构成的数组。先定义结构体类型,然后用结构体类型定义数组变量。

方式1:先声明一个结构体类型,再用此类型定义结构体数组

 结构体类型 数组名[数组长度];

举例:

 struct Person{ 
     char name[20];
     int age;
 };
 
 struct Person pers[3]; //pers是结构体数组名

举例:

 struct Student{       // 定义结构体:学生
     int id;           //学号
     char name[20];    //姓名
     char gender;      //性别
     int age;          //年龄
 }; 
 
 struct Student stus[10]; //stus是结构体数组名

方式2:定义结构体类型的同时,定义数组变量。

 struct 结构体名{
     成员列表;
 } 数组名[数组长度];

举例:

 struct Person{ 
     char name[20];
     int age;
 } pers[3];

举例:

 struct Date{
     int year;
     int month;
     int day;
 }dates1[10],dates2[10];

3.3 初始化数组元素

对应前面的声明方式1:

举例:

 struct Student stus[3] = { {1001,"Tom",'M', 14},
                            {1002, "Jerry", 'M', 13},
                            {1003, "Lily",'F',12}};

对应前面的声明方式2:

举例:

 struct Person {
     char name[20];
     int age;
 } pers[3] = {{"Tom",   12},
              {"Jerry", 11},
              {"Lily",  10}};

或者:

 struct Person {
     char name[20];
     int age;
 } pers[] = {{"Tom",   12},
              {"Jerry", 11},
              {"Lily",  10}};

说明:初始化结构体数组元素时,也可以不指定结构体数组的长度。系统在编译时,会自动根据初始化的值决定结构体数组的长度。

3.4 结构体数组元素的成员的调用

方式1:使用数组角标方式

 结构体数组名[下标].成员名

如:

 stus[1].age = 23;

方式2:使用指向数组或数组元素的指针(下节讲)

 指针->成员名

如:

 p->age=24;  //p为指向某个数组元素的指针

举例1:输入一个班级的学生信息(包含id、name、gender、score),并把学习成绩超过全班平均成绩的学生找出来,输出这部分学生的姓名和成绩。

 #include 
 
 #define N 4
 #define MAX_NAME_LENGTH 20
 
 struct Student {
     int id;
     char name[MAX_NAME_LENGTH];
     char gender;
     int score;
 };
 
 int main() {
     struct Student stu[N];
     int sum = 0;
 
     for (int i = 0; i < N; i++) {
         printf("请输入学生信息 (ID, 姓名, 性别, 成绩): \n");
         scanf("%d %19s %c %d", &stu[i].id, stu[i].name, &stu[i].gender, &stu[i].score);
         sum += stu[i].score;
     }
 
     double avg = (double)sum / N; //计算平均成绩
     printf("平均成绩为: %.2lf\n", avg);
 
     printf("高于平均分的学生:\n");
     for (int i = 0; i < N; i++) {
         if (stu[i].score > avg) {
             printf("%-20s:%d\n", stu[i].name, stu[i].score);
         }
     }
     return 0;
 }

测试如下:

 请输入学生信息 (ID, 姓名, 性别, 成绩):
 1 Tom M 89
 请输入学生信息 (ID, 姓名, 性别, 成绩):
 2 Jerry F 99
 请输入学生信息 (ID, 姓名, 性别, 成绩):
 3 Lucy F 56
 请输入学生信息 (ID, 姓名, 性别, 成绩):
 4 Tony M 66
 平均成绩为: 77.50
 高于平均分的学生:
 Tom                 :89
 Jerry               :99
 
 Process finished with exit code 0

举例2:编写一个统计选票的系统,根据先后输入的候选人姓名,统计各人的得票数。

 #include 
 
 #include 
 
 #define N 3
 
 struct Person { //声明结构体类型struct Person
     char name[20]; //候选人姓名
     int count; //候选人得票数
 } leader[N] = {{"zhang3", 0},
                {"li4",    0},
                {"wang5",  0}}; //定义结构体数组并初始化
 
 int main() {
     char leader_name[20]; //定义字符数组
     for (int i = 1; i <= 10; i++) {
         printf("你要投票给谁?(zhang3、li4、wang5):");
         scanf("%s", leader_name); //输入所选的候选人姓名
         for (int j = 0; j < N; j++) {
             if (strcmp(leader_name, leader[j].name) == 0){
                 leader[j].count++;
                 break;
             }
         }
     }
     printf("\n统计结果:\n");
     for (int i = 0; i < N; i++)
         printf("%-10s:%d\n", leader[i].name, leader[i].count);
 
     return 0;
 }

【武汉科技大学2019研】对于以下定义,能打印出字母h的语句是( )。

struct person{
char title[20];
int code;
};

struct person book[5]={"Physics",17,"Math",18,"English",20,"History",18};

A.printf("%c",book[0].title[1]); B.printf("%c",book[1].title[4]); C.printf("%c",book[2].title[7]); D.printf("%c",book[3].title[6]);

【答案】A

【解析】person是一个自定义结构体类型,该结构体含有两个成员变量,分别是一个字符数组和一个int数据,BC选项打印出来的是'\0';D选项打印出来的是y,只有A打印出来的是h,答案选A。

4、结构体指针

4.1 结构体指针格式

结构体指针:指向结构体变量的指针 (将结构体变量的起始地址存放在指针变量中)

具体应用场景:①可以指向单一的结构体变量 ②可以用作函数的参数 ③可以指向结构体数组

定义结构体指针变量格式:

 struct 结构体名 *结构体指针变量名;
 
 //int num;
 //int *num;

举例:

 struct Book {
     char title[50];
     char author[10];
     double price;
 };
 
 struct Book *b1;

等价于

 struct Book {
     char title[50];
     char author[10];
     double price;
 } *b1;

说明:变量 b1 是一个指针,指向的数据是 struct Book 类型的实例。

4.2 结构体传参

如果将 struct 变量传入函数,函数内部得到的是一个原始值的副本。

 #include 
 
 struct Person {
     char *name;
     int age;
     char *address;
 };
 
 void addAge(struct Person per) {
     per.age = per.age + 1;
 }
 
 int main() {
     struct Person p1 = {"Tom", 20, "北京市海淀区"};
     addAge(p1);
     printf("age = %d\n", p1.age); // 输出 20
     return 0;
 }

函数 addAge() 要求传入一个 struct 变量 per,但实际上传递的是 struct 变量p1的副本,改变副本影响不到函数外部的原始数据。

通常情况下,开发者希望传入函数的是同一份数据,函数内部修改数据以后,会反映在函数外部。而且,传入的是同一份数据,也有利于提高程序性能。这时就需要将 struct 变量的指针传入函数,通过指针来修改 struct 属性。如下

 struct Person {
     char *name;
     int age;
     char *address;
 };
 
 void addAge(struct Person *per) {   //说明1
     (*per).age = (*per).age + 1;    //说明2
 }
 
 int main() {
     struct Person p1 = {"Tom", 20, "北京市海淀区"};
     addAge(&p1);                    //说明3
     printf("age = %d\n", p1.age);   // 说明4:输出 21
     return 0;
 }
  • 说明1:per 是 struct 结构的指针,调用函数时传入的是指针。
  • 说明2:函数内部必须使用 (*per).age 的写法,从指针拿到 struct 结构本身。因为运算符优先级问题,不能写成*per.age,会将per.age看成是一个指针,然后取其值。
  • 说明3:结构体类型跟数组不一样,类型标识符本身并不是指针,所以传入时,指针必须写成 &p1。
  • 说明4: addAge() 内部对 struct 结构的操作,就会反映到函数外部。

练习1:

(1)编写一个Dog结构体,包含name(char[10])、age(int)、weight(double)属性

(2)编写一个say函数,返回字符串,方法返回信息中包含所有成员值。

(3)在main方法中,创建Dog结构体变量,调用say函数,将调用结果打印输出。

写法1:

 #include 
 #include 
 
 // 定义Dog结构体
 struct Dog {
     char name[10];  //或者  char * name;
     int age;
     double weight;
 };
 
 // 定义say函数,返回包含所有成员值的字符串
 char* say(struct Dog dog) {
     static char info[100]; // 静态数组用于存储结果,此数组生命周期会持续到整个程序运行结束。
     sprintf(info, "Name: %s, Age: %d, Weight: %.2lf", dog.name, dog.age, dog.weight);
     return info;
 }
 
 int main() {
     // 创建Dog结构体变量
     struct Dog myDog;
     strcpy(myDog.name, "大黄");
     myDog.age = 3;
     myDog.weight = 12.5;
 
     // 调用say函数,打印结果
     char* result = say(myDog);
     printf("info = %s\n", result);
 
     return 0;
 }

其中,sprintf() 函数是C标准库中的一个函数,它用于将格式化的数据写入一个字符数组(字符串)。

 int sprintf(char *str, const char *format, ...);
 > str:是一个字符数组,用于存储格式化后的字符串。
 > format:是格式化字符串,包含了要输出的文本以及格式说明符,就像 printf() 函数中的格式字符串一样。
 > ...:是可变参数,用于提供要格式化的数据。

顺便看一个问题,如果say()函数如下声明,请问main()中打印dog.name会是多少呢?为什么?

 char* say(struct Dog dog) {
     static char info[100]; // 静态数组用于存储结果,此数组生命周期会持续到整个程序运行结束。
     sprintf(info, "Name: %s, Age: %d, Weight: %.2lf", dog.name, dog.age, dog.weight);
     strcpy(dog.name, "小花");
     return info;
 }
 
 int main() {
     // 创建Dog结构体变量
     struct Dog myDog;
     strcpy(myDog.name, "大黄");
     myDog.age = 3;
     myDog.weight = 12.5;
 
     // 调用say函数,打印结果
     char* result = say(myDog);
     printf("info = %s\n", result);
     printf("name = %s", myDog.name);  //大黄
     return 0;
 }

在C语言中,函数参数是按值传递的,这意味着 say 函数接受的是 dog 结构体的一个副本,而不是原始的 dog 结构体。因此,在 say 函数内部对 dog 结构体的修改不会影响到 main 函数中的原始结构体。

虽然在 say 函数内部将 dog.name 设置为 "小花",但这只会影响 say 函数内的副本,而不会影响 main 函数中的 dog 结构体。所以,最后打印 dog.name 时输出的是 "大黄",而不是 "小花"。

写法2:

 #include 
 #include 
 
 // 定义Dog结构体
 struct Dog {
     char name[10];  //或者  char * name;
     int age;
     double weight;
 };
 
 // 定义say函数,返回包含所有成员值的字符串
 char *say(struct Dog *dog) {
     static char info[100]; // 静态数组用于存储结果,此数组生命周期会持续到整个程序运行结束。
     sprintf(info, "Name: %s, Age: %d, Weight: %.2lf", (*dog).name, (*dog).age, (*dog).weight);
     return info;
 }
 
 int main() {
     // 创建Dog结构体变量
     struct Dog myDog;
     strcpy(myDog.name, "大黄");
     myDog.age = 3;
     myDog.weight = 12.5;
 
     // 调用say函数,打印结果
     char *result = say(&myDog);
     printf("info = %s\n", result);
 
     return 0;
 }
 

练习2:

 struct S {
     int data[100];
     int num;
 };
 struct S s = {{1, 2, 3, 4}, 100};
 
 //结构体传参
 void print1(struct S s) {
     printf("%d\n", s.num);
 }
 
 //结构体地址传参
 void print2(struct S *ps) {
     printf("%d\n", (*ps).num);
 }
 
 int main() {
     print1(s);     //传结构体
     print2(&s);    //传地址
     return 0;
 }

从性能开销角度考虑,上面的 print1 和 print2 函数哪个好些?

答案:print2函数。函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。(考研中定义的结点作为形参时一定要注意考虑此问题)

结论:结构体传参的时候,建议传结构体的地址。

4.3 -> 操作符

前面例子中,(*per).age 的写法很麻烦,C 语言就引入了一个新的箭头运算符( -> ),可以从结构体指针上直接获取属性,大大增强了代码的可读性。

 void addAge(struct Person * per) {
     per->age = per->age + 1;  //使用结构体指针访问指向对象的成员
 }

另例:

 struct Student {
     char name[20];
     int age;
     char gender;
 };
 
 int main() {
     //打印结构体信息
     struct Student s = {"张三", 20, 'M'};
 
     //方式1:.为结构成员访问操作符
     printf("name = %s,age = %d,gender = %c\n", s.name, s.age, s.gender);
 
     struct Student *ps = &s;
     //方式2:.为结构成员访问操作符
     printf("name = %s,age = %d,gender = %c\n", (*ps).name, (*ps).age, (*ps).gender);
 
     //方式3:->操作符
     printf("name = %s,age = %d,gender = %c\n", ps->name, ps->age, ps->gender);
 
     return 0;
 }

总结:如果指针变量p指向一个结构体变量stu,以下3种用法等价:

 ① stu.成员名    stu.num
 ② (*p).成员名   (*p).num
 ③ p->成员名     p->num

4.4 指向结构体数组的指针

举例:

 struct Person {
     int id;
     char name[20];
 };
 
 int main() {
 
     struct Person per;
     struct Person arr[5];
 
     struct Person *p,*q;
 
     p = &per;  //指向单个结构体变量
     q = arr;   //指向结构体数组
 
     return 0;
 }

举例:

 #include 
 
 struct Student {
     int id;
     char name[20];
     char gender;
 } stu[3] = {{1001, "Tom",   'M'},
             {1002, "Jerry", 'M'},
             {1003, "Lily",  'F'}};
 
 int main() {
 
     //方式1:
     for (int i = 0; i < 3; i++) {
         printf("%d%10s%3c\n", stu[i].id, stu[i].name, stu[i].gender);
     }
 
     //方式2:
     struct Student *p = stu;
     for (int i = 0; i < 3; i++) {
         printf("%d%10s%3c\n", p[i].id, p[i].name, p[i].gender);
     }
 
     //方式3:
     struct Student *q;
     for (q = stu; q < stu + 3; q++) {
         printf("%d%10s%3c\n", q->id, q->name, q->gender);
     }
 
     return 0;
 }

【中央财经大学2018研】若有以下说明和语句:

struct worker {
int no;
char *name;
} work, *p = &work;

则以下引用方式不正确的是( )。 A.work.no B.(*p).no C.p->no D.work->no

【答案】D

【解析】结构体变量访问成员变量的引用方式采用“.”,而结构体指针采用“->”,因此AC是正确的,B项中*p表示结构体变量,因此可以用“.”,所以答案选择D。

5、结构体在数据结构中的应用

5.1 声明结点的结构体

链表是一种动态的数据存储结构(非固定长度),链表的基本单位是结点(node),同一链表的所有结点具有相同的数据类型。而结点使用结构体类型进行定义。

一个链表结点包括数据域和指针域两部分:数据域存储需要处理的数据、指针域存储下一个结点的位置。

单链表结构的结点定义如下:

 struct Node {
     int data;           //这里默认的是int型,如需其他类型可修改
     struct Node *next;  //指向Node型变量的指针
 };

或者:

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

二叉树结构的结点定义如下:

 struct BTNode {
     int data;                     //这里默认的是int型,如需其他类型可修改
     struct BTNode *lchild;        //指向左孩子结点指针
     struct BTNode *rchild;        //指向右孩子结点指针
 };

或者

 typedef struct BTNode {
     int data;                     //这里默认的是int型,如需其他类型可修改
     struct BTNode *lchild;        //指向左孩子结点指针
     struct BTNode *rchild;        //指向右孩子结点指针
 } BTNode;

5.2 声明结点变量

这里不需要事先说明链表所包括的结点个数,新数据到达时创建结点变量即可。

以创建二叉树结点为例,方式①:

 BTNode bt1;

方式②:

 BTNode *bt;
 bt = (BTNode*) malloc(sizeof (BTNode));//此句要熟练掌握

方式①中只用一句就制作了一个结点,而方式②中需要两句,使用了系统已有的函数malloc()申请新结点所需内存空间,比①要烦琐。

②的执行过程为:先定义一个结点的指针bt,然后用函数malloc()来动态申请一个结点的内存空间,接着让指针 bt 指向这片内存空间,这样就完成了一个结点变量的创建。后续不需要数据时,删除结点,释放空间(使用free(bt)释放)即可。

5.3 两种方式对比

对比1:是否可以重新赋值

②中的bt是个指针型变量,用来存储刚创建好的结点的地址。因bt是变量,虽然现在bt指向了刚生成的结点,但是在以后必要的时候bt可以离开这个结点转而指向其他结点。而①则不行,①中的bt1就是某个结点的名字,一旦定义好,它就不能脱离这个结点了。

结论:②比①更灵活,因此②用得多。

对比2:①和②中的BT取分量的操作也是不同的。比如,想取其data 域的值赋给x。

对于①,用结构体变量直接取分量,其操作用“.”

 int x = bt1.data;

对于②,用指向结构体变量的指针来取分量,其操作用“->”

 int x = bt->data;
 //等同于
 int x = (*bt).data; //这里的()不要省略

考研数据结构中所有类型结点的内存分配中使用最多的就是方式② ,即使用函数malloc()来完成,模式固定,务必记忆。

注意点

可能会有人认可如下的两种简便写法。虽然这种写法简单,但是在一些纯C编译器中是不通过的,如果你所报考的目标学校严格要求用纯C语言来写程序,则不能这样写结构体定义。

 //链表结点:
 struct Node {
     int data;
     Node *next;
 };
 
 //二叉树结点:
 struct BTNode {
     int data;
     BTNode *lchild;
     BTNode *rchild;
 };
 

5.4 malloc()模板

模板:(当需要制作一个新结点时,只要把结点结构型的名称填入括号中的“类型”处即可)

 类型 *p;
 p = (类型 *)malloc(sizeof(类型));  //将=右边创建的结点的地址赋给p

举例:

 typedef struct BTNode {
     int data;
     struct BTNode *lchild;
     struct BTNode *rchild;
 }BTNode;
 
 int main() {
     BTNode *newNode;
     newNode = (BTNode *) malloc(sizeof(BTNode));
 
     return 0;
 }

此外,还可以一次申请一组结点,可以看做是动态申请数组空间的方法。如下:

 int *p;
 p = (int *) malloc(n * sizeof(int));

这样就申请了一个由指针p所指的( p指向数组中第一个元素的地址)元素为int 型的、长度为n 的动态数组。取元素时和一般的数组(静态数组)一样,如取第二个元素,则可写成p[1]。

5.5 举 例

在考研的数据结构中,只需要熟练掌握以上两种结点(链表、二叉树)的定义方法,其他结点都是由这两种衍生而来的。

举例:定义结构体,表示学生结点

 struct StudentNode {
     int id;
     char name[20];
     struct StudentNode *next;
 };

创建多个结点,彼此构成链表

 #include 
 #include 
 #include 
 
 typedef struct StudentNode {
     int id;
     char name[20];
     struct StudentNode *next;
 } StuNode;
 
 int main() {
 
     StuNode *head;
 
     // 生成一个三个节点的列表 {1001,"Tom"} -> {1002,"Jerry"} -> {1003,"Lily"}
     head = (StuNode *)malloc(sizeof(StuNode));
     head->id = 1001;
     strcpy(head->name, "Tom");
 
     StuNode *p = (StuNode *)malloc(sizeof(StuNode));
     p->id = 1002;
     strcpy(p->name, "Jerry");
     head->next = p;
 
     p = (StuNode *)malloc(sizeof(StuNode));
     p->id = 1003;
     strcpy(p->name, "Lily");
     head->next->next = p;
 
     //遍历链表
     StuNode *cur;
     for (cur = head; cur != NULL; cur = cur->next) {
         printf("id = %d,name = %s\n", cur->id, cur->name);
     }
 
     return 0;
 }

构成如下图的链表:

为了准确定位第一个结点,每个链表要有一个表头指针,从第一个结点开始,沿指针链遍历链表中的所有结点。

最近发表
标签列表