C语言 超详细 零基础入门 结构体数组 结构体指针 数据结构

C语言 超详细 零基础入门 结构体 的 基本介绍-CSDN博客

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 <stdio.h>

#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 <stdio.h>

#include <string.h>

#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 <stdio.h>

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 <stdio.h>
#include <string.h>

// 定义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 <stdio.h>
#include <string.h>

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

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

练习3:景区门票

一个景区根据游客的年龄收取不同价格的门票。

(1)请编写游客结构体(Visitor),包含姓名,年龄,应付票价

(2)编写函数ticket(),根据年龄段决定能够购买的门票价格并输出。

规则:年龄>=18,门票为20元,其它情况免费。

(3)可以循环从控制台输入名字和年龄,打印门票收费情况,如果名字输入n,则退出程序。

#include <stdio.h>

#include <string.h>


//定义结构体
struct Visitor {
    char *name;  //姓名
    int age;     //年龄
    double pay;  //应付票价
};

//编写函数处理业务
void ticket(struct Visitor *visitor) {
    //判断
    if ((*visitor).age >= 18) {
        (*visitor).pay = 20;
    } else {
        (*visitor).pay = 0;
    }
}

int main() {
    //创建结构体变量(创建一个游客)
    struct Visitor visitor;
    //循环的输入名字和年龄
    while (1) {
        printf("请输入游客名字(输入n退出程序):");
        scanf("%s", visitor.name);
        //判断如果名字输入 n ,则退出程序
        if (!strcmp("n", visitor.name)) {
            break;
        }
        printf("请输入游客年龄:");
        scanf("%d", &visitor.age);
        //调用函数 ticket,获取应付的票价
        ticket(&visitor);
        printf("该游客应付票价=%.2lf\n", visitor.pay);
    }

    printf("退出程序");

    return 0;
}

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 <stdio.h>

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 <stdio.h>
#include <malloc.h>
#include <string.h>

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;
}

构成如下图的链表:

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

猜你喜欢

转载自blog.csdn.net/m0_67184231/article/details/134873777