网络知识 娱乐 C语言之结构体就这样被攻克了

C语言之结构体就这样被攻克了



写在前面:本文两万五千多字,预计阅读时间8分钟。同时,含金量也是非常高的,建议收藏!


有的时候,我们所遇到的数据结构,不仅仅是一群数字或者是字符串那么简单。比如我们每一个人的学籍信息,学号是一个长整数,名字却是字符;甚至有更复杂的情况,这种问题在现实生活中并不少见。我们之前学过一种叫数组的数据结构,它可以允许我们把很多同类型的数据集中在一起处理。相对于之前,这已经是一次极大的进步。但是,新的问题,往往又会出现,这个时候,我们就得上更高端的装备——结构体。

相比于数组,结构体有以下的更强大的优势:

  • 批量存储数据
  • 存储不同类型的数据
  • 支持嵌套

结构体的声明与定义

声明

结构体的声明使用struct关键字,如果我们想要把我们的学籍信息组织一下的话,可以这样表示:

struct Infon{n unsigned long identifier;//学号,用无符号长整数表示n char name[20];//名字,用字符数组表示n unsigned int year;//入学年份,用无符号整数表示n unsigned int years;//学制,用无符号整数表示n}n

这样,我们就相当于描绘好了一个框架,以后要用的话直接定义一个这种类型的变量就好了。

定义

我们刚刚申请了一个名叫Info的结构体类型,那么理论上我们可以像声明其他变量的操作一样,去声明我们的结构体操作,但是C语言中规定,声明结构体变量的时候,struct关键字是不可少的。

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

不过,你可以在某个函数里面定义:

#include <stdio.h>nnstruct Infon{n unsigned long identifier;//学号,用无符号长整数表示n char name[20];//名字,用字符数组表示n unsigned int year;//入学年份,用无符号整数表示n unsigned int years;//学制,用无符号整数表示n};nnint main(void)n{n /**n *在main函数中声明结构体变量n *结构体变量名叫infon *struct关键字不能丢n */n struct Info info;n ...n}n

也可以在声明的时候就把变量名定义下来(此时这个变量是全局变量):

#include <stdio.h>nnstruct Infon{n unsigned long identifier;//学号,用无符号长整数表示n char name[20];//名字,用字符数组表示n unsigned int year;//入学年份,用无符号整数表示n unsigned int years;//学制,用无符号整数表示n} info;n/**n *此时直接定义了变量n *该变量是全局变量n *变量名叫infon */nnint main(void)n{n ...n}n

访问结构体成员

结构体成员的访问有点不同于以往的任何变量,它是采用点号运算符.来访问成员的。比如,info.name就是引用info结构体的name成员,是一个字符数组,而info.year则可以查到入学年份,是个无符号整型。

比如,下面开始录入学生的信息:

//Example 01n#include <stdio.h>nnstruct Infon{n unsigned long identifier;//学号,用无符号长整数表示n char name[20];//名字,用字符数组表示n unsigned int year;//入学年份,用无符号整数表示n unsigned int years;//学制,用无符号整数表示n};nnint main(void)n{n struct Info info;nn printf("请输入学生的学号:");n scanf("%d", &info.identifier);n printf("请输入学生的姓名:");n scanf("%s", info.name);n printf("请输入学生的入学年份:");n scanf("%d", &info.year);n printf("请输入学生的学制:");n scanf("%d", &info.years);nn printf("n数据录入完毕nn");nn printf("学号:%dn姓名:%sn入学年份:%dn学制:%dn毕业时间:%dn", n info.identifier, info.name, info.year, info.years, info.year + info.years);n return 0;n}n

运行结果如下:

//Consequence 01n请输入学生的学号:20191101n请输入学生的姓名:Harrisn请输入学生的入学年份:2019n请输入学生的学制:4nn数据录入完毕nn学号:20191101n姓名:Harrisn入学年份:2019n学制:4n毕业时间:2023n

初始化结构体

像数组一样,结构体也可以在定义的时候初始化,方法也几乎一样:

struct Info info = {n 20191101,n "Harris",n 2019,n 4n};n

在C99标准中,还支持给指定元素赋值(就像数组一样):

struct Info info = {n .name = "Harris",n .year = 2019n};n

对于没有被初始化的成员,则「数值型」成员初始化为0,「字符型」成员初始化为‘0’。

对齐

下面这个代码,大家来看看会发生什么:

//EXample 02 V1n#include <stdio.h>nnint main(void)n{n struct An {n char a;n int b;n char c;n } a = {'a', 10, 'o'};n n printf("size of a = %dn", sizeof(a));n n return 0;n}n

我们之前学过,char类型的变量占1字节,int类型的变量占4字节,那么这么一算,一个结构体A型的变量应该就是6字节了。别急,我们看运行结果:

//COnsequence 02 V1nsize of a = 12n

怎么变成12了呢?标准更新了?老师教错了?都不是。我们把代码改一下:

//EXample 02 V2n#include <stdio.h>nnint main(void)n{n struct An {n char a;n char c;n int b;n } a = {'a', 'o', 10};n n printf("size of a = %dn", sizeof(a));n n return 0;n}n

结果:

//Consequence 02 V2nsize of a = 8n

实际上,这是编译器对我们程序的一种优化——内存对齐。在第一个例子中,第一个和第三个成员是char类型是1个字节,而中间的int却有4个字节,为了对齐,两个char也占用了4个字节,于是就是12个字节。

而在第二个例子里面,前两个都是char,最后一个是int,那么前两个可以一起占用4个字节(实际只用2个,第一个例子也同理,只是为了访问速度更快,而不是为了扩展),最后的int占用4字节,合起来就是8个字节。

关于如何声明结构体来节省内存容量,可以阅读下面的这篇文章,作者是艾瑞克·雷蒙,时尚最具争议性的黑客之一,被公认为开源运动的主要领导者之一:

英文原版,中文版

结构体嵌套

在学籍里面,如果我们的日期想要更加详细一些,精确到day,这时候就可以使用结构体嵌套来完成:

#include <stdio.h>nnstruct Daten{n unsigned int year;n unsigned int month;n unsigned int day;n};nnstruct Infon{n unsigned long identifier;//学号,用无符号长整数表示n char name[20];//名字,用字符数组表示n struct Date date;/*---入学日期,用结构体Date表示---*/n unsigned int years;//学制,用无符号整数表示n};nnint main(void)n{n ...n}n

如此一来,比我们单独声明普通变量快多了。

不过,这样访问变量,就必须用点号一层层往下访问。比如要访问day这个成员,那就只能info.date.day而不能直接info.date或者info,day

//Example 03n#include <stdio.h>nnstruct Daten{n unsigned int year;n unsigned int month;n unsigned int day;n};nnstruct Infon{n unsigned long identifier;//学号,用无符号长整数表示n char name[20];//名字,用字符数组表示n struct Date date;/*---入学日期,用结构体Date表示---*/n unsigned int years;//学制,用无符号整数表示n};nnint main(void)n{n struct Info info;n printf("请输入学生的学号:");n scanf("%d", &info.identifier);n printf("请输入学生的姓名:");n scanf("%s", info.name);n printf("请输入学生的入学年份:");n scanf("%d", &info.date.year);n printf("请输入学生的入学月份:");n scanf("%d", &info.date.month);n printf("请输入学生的入学日期:");n scanf("%d", &info.date.day);n printf("请输入学生的学制:");n scanf("%d", &info.years);nn printf("n数据录入完毕nn");nn printf("学号:%dn姓名:%sn入学时间:%d/%d/%dn学制:%dn毕业时间:%dn",n info.identifier, info.name,n info.date.year, info.date.month, info.date.day,n info.years, info.date.year + info.years);n return 0;n}n

运行结果如下:

//Consequence 03n请输入学生的学号:20191101n请输入学生的姓名:Harrisn请输入学生的入学年份:2019n请输入学生的入学月份:9n请输入学生的入学日期:7n请输入学生的学制:4nn数据录入完毕nn学号:20191101n姓名:Harrisn入学时间:2019/9/7n学制:4n毕业时间:2023n

结构体数组

刚刚我们演示了存储一个学生的学籍信息的时候,使用结构体的例子。那么,如果要录入一批学生,这时候我们就可以沿用之前的思路,使用结构体数组。

我们知道,数组的定义,就是存放一堆相同类型的数据的容器。而结构体一旦被我们声明,那么你就可以把它看作一个类型,只不过是你自己定义的罢了。

定义结构体数组也很简单:

struct 结构体类型n{n 成员;n} 数组名[长度];nn/****或者这样****/nnstruct 结构体类型n{n 成员;n};nstruct 结构体类型 数组名[长度];n

结构体指针

既然我们可以把结构体看作一个类型,那么也就必然有对应的指针变量。

struct Info* pinfo;n

嵌入式物联网需要学的东西真的非常多,千万不要学错了路线和内容,导致工资要不上去!


无偿分享大家一个资料包,差不多150多G。里面学习路线、面经、项目都比较新也比较全面!某鱼上买估计至少要好几十。

点击这里找小助理0元领取:嵌入式物联网学习资料(头条)

C语言之结构体就这样被攻克了

C语言之结构体就这样被攻克了



但是在指针这里,结构体和数组就不一样了。我们知道,数组名实际上就是指向这个数组第一个元素的地址,所以可以将数组名直接赋值给指针。而结构体的变量名并不是指向该结构体的地址,所以要使用取地址运算符&才能获取地址:

pinfo = &info;n

通过结构体指针来访问结构体有以下两种方法:

  1. (*结构体指针).成员名
  2. 结构体指针->成员名

第一个方法由于点号运算符比指针的取值运算符优先级更高,因此需要加一个小括号来确定优先级,让指针先解引用变成结构体变量,在使用点号的方法去访问。

相比之下,第二种方法就直观许多。

这两种方法在实现上是完全等价的,但是点号只能用于结构体变量,而箭头只能够用于指针。

第一种方法:

#include <stdio.h>n...nint main(void)n{n struct Info *p;n p = &info;n n printf("学号:n", (*p).identifier);n printf("姓名:n", (*p).name);n printf("入学时间:%d/%d/%dn", (*p).date.year, (*p).date.month, (*p).date.day);n printf("学制:n", (*p).years);n return 0;n}n

第二种方法:

#include <stdio.h>n...nint main(void)n{n struct Info *p;n p = &info;n n printf("学号:n", p -> identifier);n printf("姓名:n", p -> name);n printf("入学时间:%d/%d/%dn", p -> date.year, p -> date.month, p -> date.day);n printf("学制:n", p -> years);n return 0;n}n

传递结构体信息

传递结构体变量

我们先来看看下面的代码:

//Example 04n#include <stdio.h>nnint main(void)n{n struct Testn {n int x;n int y;n }t1, t2;nn t1.x = 3;n t1.y = 4;n t2 = t1;nn printf("t2.x = %d, t2.y = %dn", t2.x, t2.y);n return 0;n}n

运行结果如下:

//Consequence 04nt2.x = 3, t2.y = 4n

这么看来,结构体是可以直接赋值的。那么既然这样,作为函数的参数和返回值也自然是没问题的了。

先来试试作为参数:

//Example 05n#include <stdio.h>nstruct Daten{n unsigned int year;n unsigned int month;n unsigned int day;n};nnstruct Infon{n unsigned long identifier;n char name[20];n struct Date date;n unsigned int years;n};nnstruct Info getInput(struct Info info);nvoid printInfo(struct Info info);nnstruct Info getInput(struct Info info)n{n printf("请输入学号:");n scanf("%d", &info.identifier);n printf("请输入姓名:");n scanf("%s", info.name);n printf("请输入入学年份:");n scanf("%d", &info.date.year);n printf("请输入月份:");n scanf("%d", &info.date.month);n printf("请输入日期:");n scanf("%d", &info.date.day);n printf("请输入学制:");n scanf("%d", &info.years);nn return info;n}nnvoid printInfo(struct Info info)n{n printf("学号:%dn姓名:%sn入学时间:%d/%d/%dn学制:%dn毕业时间:%dn", n info.identifier, info.name, n info.date.year, info.date.month, info.date.day, n info.years, info.date.year + info.years);n}nnint main(void)n{n struct Info i1 = {};n struct Info i2 = {};n printf("请录入第一个同学的信息...n");n i1 = getInput(i1);n putchar('n');n printf("请录入第二个学生的信息...n");n i2 = getInput(i2);nn printf("n录入完毕,现在开始打印...nn");n printf("打印第一个学生的信息...n");n printInfo(i1);n putchar('n');n printf("打印第二个学生的信息...n");n printInfo(i2);nn return 0;n}n

运行结果如下:

//Consequence 05n请录入第一个同学的信息...n请输入学号:20191101n请输入姓名:Harrisn请输入入学年份:2019n请输入月份:9n请输入日期:7n请输入学制:4nn请录入第二个学生的信息...n请输入学号:20191102n请输入姓名:Joyn请输入入学年份:2019n请输入月份:9n请输入日期:8n请输入学制:5nn录入完毕,现在开始打印...nn打印第一个学生的信息...n学号:20191101n姓名:Harrisn入学时间:2019/9/7n学制:4n毕业时间:2023nn打印第二个学生的信息...n学号:20191102n姓名:Joyn入学时间:2019/9/8n学制:5n毕业时间:2024n

传递指向结构体变量的指针

早期的C语言是不允许直接将结构体作为参数直接传递进去的。主要是考虑到如果结构体的内存占用太大,那么整个程序的内存开销就会爆炸。不过现在的C语言已经放开了这方面的限制。

不过,作为一名合格的开发者,我们应该要去珍惜硬件资源。那么,传递指针就是一个很好的办法。

将刚才的代码修改一下:

//Example 06n#include <stdio.h>nstruct Daten{n unsigned int year;n unsigned int month;n unsigned int day;n};nnstruct Infon{n unsigned long identifier;n char name[20];n struct Date date;n unsigned int years;n};nnvoid getInput(struct Info *info);nvoid printInfo(struct Info *info);nnvoid getInput(struct Info *info)n{n printf("请输入学号:");n scanf("%d", &info->identifier);n printf("请输入姓名:");n scanf("%s", info->name);n printf("请输入入学年份:");n scanf("%d", &info->date.year);n printf("请输入月份:");n scanf("%d", &info->date.month);n printf("请输入日期:");n scanf("%d", &info->date.day);n printf("请输入学制:");n scanf("%d", &info->years);n}nnvoid printInfo(struct Info *info)n{n printf("学号:%dn姓名:%sn入学时间:%d/%d/%dn学制:%dn毕业时间:%dn", n info->identifier, info->name, n info->date.year, info->date.month, info->date.day, n info->years, info->date.year + info->years);n}nnint main(void)n{n struct Info i1 = {};n struct Info i2 = {};n printf("请录入第一个同学的信息...n");n getInput(&i1);n putchar('n');n printf("请录入第二个学生的信息...n");n getInput(&i2);nn printf("n录入完毕,现在开始打印...nn");n printf("打印第一个学生的信息...n");n printInfo(&i1);n putchar('n');n printf("打印第二个学生的信息...n");n printInfo(&i2);nn return 0;n}n

此时传递的就是一个指针,而不是一个庞大的结构体。

动态申请结构体

结构体也可以在堆里面动态申请:

//Example 01n#include <stdio.h>n...nint main(void)n{n struct Info *i1;n struct Info *i2;n n i1 = (struct Info *)malloc(sizeof(struct Info));n i2 = (struct Info *)malloc(sizeof(struct Info));n if (i1 == NULL || i2 == NULL)n {n printf("内存分配失败!n");n exit(1);n }n n printf("请录入第一个同学的信息...n");n getInput(i1);n putchar('n');n printf("请录入第二个学生的信息...n");n getInput(i2);nn printf("n录入完毕,现在开始打印...nn");n printf("打印第一个学生的信息...n");n printInfo(i1);n putchar('n');n printf("打印第二个学生的信息...n");n printInfo(i2);n n free(i1);n free(i2);n n return 0;n}n

实战:建立一个图书馆数据库

实际上,我们建立的数组可以是指向结构体指针的数组。

代码实现如下:

//Example 02n#include <stdio.h>n#include <stdlib.h>nn#define MAX_SIZE 100nnstruct Daten{n int year;n int month;n int day;n};nnstruct Bookn{n char title[128];n char author[48];n float price;n struct Date date;n char publisher[48];n};nnvoid getInput(struct Book* book);//录入数据nvoid printBook(struct Book* book);//打印数据nvoid initLibrary(struct Book* lib[]);//初始化结构体nvoid printLibrary(struct Book* lib[]);//打印单本书数据nvoid releaseLibrary(struct Book* lib[]);//释放内存nnvoid getInput(struct Book* book)n{n printf("请输入书名:");n scanf("%s", book->title);n printf("请输入作者:");n scanf("%s", book->author);n printf("请输入售价:");n scanf("%f", &book->price);n printf("请输入出版日期:");n scanf("%d-%d-%d", &book->date.year, &book->date.month, &book->date.day);n printf("请输入出版社:");n scanf("%s", book->publisher);n}nnvoid printBook(struct Book* book)n{n printf("书名:%sn", book->title);n printf("作者:%sn", book->author);n printf("售价:%.2fn", book->price);n printf("出版日期:%d-%d-%dn", book->date.year, book->date.month, book->date.day);n printf("出版社:%sn", book->publisher);n}nnvoid initLibrary(struct Book* lib[])n{n for (int i = 0; i < MAX_SIZE; i++)n {n lib[i] = NULL;n }n}nnvoid printLibrary(struct Book* lib[])n{n for (int i = 0; i < MAX_SIZE; i++)n {n if (lib[i] != NULL)n {n printBook(lib[i]);n putchar('n');n }n }n}nnvoid releaseLibrary(struct Book* lib[])n{n for (int i = 0; i < MAX_SIZE; i++)n {n if (lib[i] != NULL)n {n free(lib[i]);n }n }n}nnint main(void)n{n struct Book* lib[MAX_SIZE];n struct Book* p = NULL;n int ch, index = 0;nn initLibrary(lib);nn while (1)n {n printf("请问是否要录入图书信息(Y/N):");n don {n ch = getchar();n } while (ch != 'Y' && ch != 'N');nn if (ch == 'Y')n {n if (index < MAX_SIZE)n {n p = (struct Book*)malloc(sizeof(struct Book));n getInput(p);n lib[index] = p;n index++;n putchar('n');n }n elsen {n printf("数据库已满!n");n break;n }n }n elsen {n break;n }n }nn printf("n数据录入完毕,开始打印验证...nn");n printLibrary(lib);n releaseLibrary(lib);nn return 0;n}n

运行结果如下:

//Consequence 02n请问是否要录入图书信息(Y/N):Yn请输入书名:人类简史n请输入作者:尤瓦尔·赫拉利n请输入售价:32.25n请输入出版日期:2016-3-4n请输入出版社:中信出版集团nn请问是否要录入图书信息(Y/N):Nnn数据录入完毕,开始打印验证...nn书名:人类简史n作者:尤瓦尔·赫拉利n售价:32.25n出版日期:2016-3-4n出版社:中信出版集团n

单链表

我们知道,数组变量在内存中,是连续的,而且不可拓展。显然在一些情况下,这种数据结构拥有很大的局限性。比如移动数据的时候,会牵一发而动全身,尤其是反转这种操作更加令人窒息。那么,需要需要一种数据结构来弄出一种更加灵活的“数组”,那么这,就是「链表」

本节我们只讲讲单链表。

所谓链表,就是由一个个「结点」组成的一个数据结构。每个结点都有「数据域」「指针域」组成。其中数据域用来存储你想要存储的信息,而指针域用来存储下一个结点的地址。如图:

单链表

当然,链表最前面还有一个头指针,用来存储头结点的地址。

这样一来,链表中的每一个结点都可以不用挨个存放,因为有了指针把他们串起来。因此结点放在哪都无所谓,反正指针总是能够指向下一个元素。我们只需要知道头指针,就能够顺藤摸瓜地找到整个链表。

因此对于学籍数据库来说,我们只需要在Info结构体中加上一个指向自身类型的成员即可:

struct Infon{n unsigned long identifier;n char name[20];n struct Date date;n unsigned int years;n struct Info* next;n};n

在单链表中插入元素

头插法

这种每次都将数据插入单链表的头部(头指针后面)的插入法就叫头插法。

如果要把学生信息加入到单链表,可以这么写:

void addInfo(struct Info** students)//students是头指针n{n struct Info* info, *temp;n info = (struct Info*)malloc(sizeof(struct Info));n if (info == NULL)n {n printf("内存分配失败!n");n exit(1);n }n n getInput(info);n n if (*students != NULL)n {n temp = *students;n *students = info;n info->next = temp;n }n elsen {n *students = info;n info->next = NULL;n }n}n

由于students存放的是头指针,因此我们需要传入它的地址传递给函数,才能够改变它本身的值。而students本身又是一个指向Info结构体的指针,所以参数的类型应该就是struct Info**

往单链表里面添加一个结点,也就是先申请一个结点,然后判断链表是否为空。如果为空,那么直接将头指针指向它,然后next成员指向NULL。若不为空,那么先将next指向头指针原本指向的结点,然后将头指针指向新结点即可。

那么,打印链表也变得很简单:

void printStu(struct Info* students)n{n struct Info* info;n int count = 1;n n info = students;n while (book != NULL)n {n printf("Student%d:n", count);n printf("姓名:%sn", info->name);n printf("学号:%dn", info->identifier);n info = info->next;n count++;n }n}n

想要读取单链表里面的数据,只需要迭代单链表中的每一个结点,直到next成员为NULL,即表示单链表的结束。

最后,当然还是别忘了释放空间:

void releaseStu(struct Info** students)n{n struct Info* temp;n n while (*students != NULL)n {n temp = *students;n *students = (*students)->next;n free(temp);n }n}n

尾插法

与头插法类似,尾插法就是把每一个数据都插入到链表的末尾。

void addInfo(struct Info** students)n{n struct Info* info, *temp;n info = (struct Info*)malloc(sizeof(struct Info));n if (info == NULL)n {n printf("内存分配失败!n");n exit(1);n }n n getInput(info);n n if (*students != NULL)n {n temp = *students;n *students = info;n //定位到链表的末尾的位置n while (temp->next != NULL)n {n temp = temp->next;n }n //插入数据n temp->next = info;n info->next = temp;n }n elsen {n *students = info;n info->next = NULL;n }n}n

这么一来,程序执行的效率难免要降低很多,因为每次插入数据,都要先遍历一次链表。如果链表很长,那么对于插入数据来说就是一次灾难。不过,我们可以给程序添加一个指针,让它永远都指向链表的尾部,这样一来,就可以用很少的空间换取很高的程序执行效率。

代码更改如下:

void addInfo(struct Info** students)n{n struct Info* info, *temp;n static struct Info* tail;//设置静态指针n info = (struct Info*)malloc(sizeof(struct Info));n if (info == NULL)n {n printf("内存分配失败!n");n exit(1);n }n n getInput(info);n n if (*students != NULL)n {n tail->next = info;n info->next = NULL;n }n elsen {n *students = info;n info->next = NULL;n }n}n

搜索单链表

单链表是我们用来存储数据的一个容器,那么有时候需要快速查找信息就需要开发相关搜索的功能。比如说输入学号,查找同学的所有信息。

struct Info *searchInfo(struct Info* students, long* target)n{n struct Info* info;n info = students;n while (info != NULL)n {n if (info->identifier == target)n {n break;n }n info = info->next;n }n n return book;n};nnvoid printInfo(struct Info* info)n{n ...n}n...nnint main(void)n{n ...n printf("n请输入学生学号:");n scanf("%d", input);n info = searchInfo(students, input);n if (info == NULL)n {n printf("抱歉,未找到相关结果!n");n }n elsen {n don {n printf("相关结果如下:n");n printInfo(book);n } while ((info = searchInfo(info->next, input)) != NULL);n }n n releaseInfo(...);n return 0;n}n

插入结点到指定位置

到了这里,才体现出链表真正的优势。

设想一下,如果有一个有序数组,现在要求你去插入一个数字,插入完成之后,数组依然保持有序。你会怎么做?

没错,你应该会挨个去比较,然后找到合适的位置(当然这里也可以使用二分法,比较节省算力),把这个位置后面的所有数都往后移动一个位置,然后将我们要插入的数字放入刚刚我们腾出来的空间里面。

你会发现,这样的处理方法,经常需要移动大量的数据,对于程序的执行效率来说,是一个不利因素。那么链表,就无所谓。反正在内存中,链表的存储毫无逻辑,我们只需要改变指针的值就可以实现链表的中间插入。

//Example 03n#include <stdio.h>n#include <stdlib.h>nnstruct Noden{n int value;n struct Node* next;n};nnvoid insNode(struct Node** head, int value)n{n struct Node* pre;n struct Node* cur;n struct Node* New;nn cur = *head;n pre = NULL;nn while (cur != NULL && cur->value < value)n {n pre = cur;n cur = cur->next;n }nn New = (struct Node*)malloc(sizeof(struct Node));n if (New == NULL)n {n printf("内存分配失败!n");n exit(1);n }n New->value = value;n New->next = cur;nn if (pre == NULL)n {n *head = New;n }n elsen {n pre->next = New;n }n}nnvoid printNode(struct Node* head)n{n struct Node* cur;nn cur = head;n while (cur != NULL)n {n printf("%d ", cur->value);n cur = cur->next;n }n putchar('n');n}nnint main(void)n{n struct Node* head = NULL;n int input;nn printf("开始插入整数...n");n while (1)n {n printf("请输入一个整数,输入-1表示结束:");n scanf("%d", &input);n if (input == -1)n {n break;n }n insNode(&head, input);n printNode(head);n }nn return 0;n}n

运行结果如下:

//Consequence 03n开始插入整数...n请输入一个整数,输入-1表示结束:4n4n请输入一个整数,输入-1表示结束:5n4 5n请输入一个整数,输入-1表示结束:3n3 4 5n请输入一个整数,输入-1表示结束:6n3 4 5 6n请输入一个整数,输入-1表示结束:2n2 3 4 5 6n请输入一个整数,输入-1表示结束:5n2 3 4 5 5 6n请输入一个整数,输入-1表示结束:1n1 2 3 4 5 5 6n请输入一个整数,输入-1表示结束:7n1 2 3 4 5 5 6 7n请输入一个整数,输入-1表示结束:-1n

删除结点

删除结点的思路也差不多,首先修改待删除的结点的上一个结点的指针,将其指向待删除结点的下一个结点。然后释放待删除结点的空间。

...nvoid delNode(struct Node** head, int value)n{n struct Node* pre;n struct Node* cur;n n cur = *head;n pre = NULL;n while (cur != NULL && cur->value != value)n {n pre = cur;n cur = cur->next;n }n if (cur == NULL)n {n printf("未找到匹配项!n");n return ;n }n elsen {n if (pre == NULL)n {n *head = cur->next;n }n elsen {n pre->next = cur->next;n }n free(cur);n }n}n

内存池

C语言的内存管理,从来都是一个让人头秃的问题。要想更自由地管理内存,就必须去堆中申请,然后还需要考虑何时释放,万一释放不当,或者没有及时释放,造成的后果都是难以估量的。

当然如果就这些,那倒也还不算什么。问题就在于,如果大量地使用mallocfree函数来申请内存,首先使要经历一个从应用层切入系统内核层,调用完成之后,再返回应用层的一系列步骤,实际上使非常浪费时间的。更重要的是,还会产生大量的内存碎片。比如,先申请了一个1KB的空间,紧接着又申请了一个8KB的空间。而后,这个1KB使用完了,被释放,但是这个空间却只有等到下一次有刚好1KB的空间申请,才能够被重新调用。这么一来,极限情况下,整个堆有可能被弄得支离破碎,最终导致大量内存浪费。

那么这种情况下,我们解决这类问题的思路,就是创建一个内存池。

内存池,实际上就是我们让程序创建出来的一块额外的缓存区域,如果有需要释放内存,先不必使用free函数,如果内存池有空,那么直接放入内存池。同样的道理,下一次程序申请空间的时候,先检查下内存池里面有没有合适的内存,如果有,则直接拿出来调用,如果没有,那么再使用malloc

其实内存池我们就可以使用单链表来进行维护,下面通过一个通讯录的程序来说明内存池的运用。

普通的版本:

//Example 04 V1n#include <stdio.h>n#include <stdlib.h>n#include <string.h>nnstruct Personn{n char name[40];n char phone[20];n struct Person* next;n};nnvoid getInput(struct Person* person);nvoid printPerson(struct Person* person);nvoid addPerson(struct Person** contects);nvoid changePerson(struct Person* contacts);nvoid delPerson(struct Person** contacts);nstruct Person* findPerson(struct Person* contacts);nvoid displayContacts(struct Person* contacts);nvoid releaseContacts(struct Person** contacts);nnvoid getInput(struct Person* person)n{n printf("请输入姓名:");n scanf("%s", person->name);n printf("请输入电话:");n scanf("%s", person->phone);n}nnvoid addPerson(struct Person** contacts)n{n struct Person* person;n struct Person* temp;nn person = (struct Person*)malloc(sizeof(struct Person));n if (person == NULL)n {n printf("内存分配失败!n");n exit(1);n }nn getInput(person);nn //将person添加到通讯录中n if (*contacts != NULL)n {n temp = *contacts;n *contacts = person;n person->next = temp;n }n elsen {n *contacts = person;n person->next = NULL;n }n}nnvoid printPerson(struct Person* person)n{n printf("联系人:%sn", person->name);n printf("电话:%sn", person->phone);n}nnstruct Person* findPerson(struct Person* contacts)n{n struct Person* current;n char input[40];nn printf("请输入联系人:");n scanf("%s", input);nn current = contacts;n while (current != NULL && strcmp(current->name, input))n {n current = current->next;n }nn return current;n}nnvoid changePerson(struct Person* contacts)n{n struct Person* person;nn person = findPerson(contacts);n if (person == NULL)n {n printf("找不到联系人!n");n }n elsen {n printf("请输入联系电话:");n scanf("%s", person->phone);n }n}nnvoid delPerson(struct Person** contacts)n{n struct Person* person;n struct Person* current;n struct Person* previous;nn //先找到待删除的节点的指针n person = findPerson(*contacts);n if (person == NULL)n {n printf("找不到该联系人!n");n }n elsen {n current = *contacts;n previous = NULL;nn //将current定位到待删除的节点n while (current != NULL && current != person)n {n previous = current;n current = current->next;n }nn if (previous == NULL)n {n //若待删除的是第一个节点n *contacts = current->next;n }n elsen {n //若待删除的不是第一个节点n previous->next = current->next;n }nn free(person);//将内存空间释放n }n}nnvoid displayContacts(struct Person* contacts)n{n struct Person* current;nn current = contacts;n while (current != NULL)n {n printPerson(current);n current = current->next;n }n}nnvoid releaseContacts(struct Person** contacts)n{n struct Person* temp;nn while (*contacts != NULL)n {n temp = *contacts;n *contacts = (*contacts)->next;n free(temp);n }n}nnint main(void)n{n int code;n struct Person* contacts = NULL;n struct Person* person;nn printf("| 欢迎使用通讯录管理程序 |n");n printf("|--- 1:插入新的联系人 ---|n");n printf("|--- 2:查找现有联系人 ---|n");n printf("|--- 3:更改现有联系人 ---|n");n printf("|--- 4:删除现有联系人 ---|n");n printf("|--- 5:显示当前通讯录 ---|n");n printf("|--- 6:退出通讯录程序 ---|n");nn while (1)n {n printf("n请输入指令代码:");n scanf("%d", &code);n switch (code)n {n case 1:addPerson(&contacts); break;n case 2:person = findPerson(contacts);n if (person == NULL)n {n printf("找不到该联系人!n");n }n elsen {n printPerson(person);n }n break;n case 3:changePerson(contacts); break;n case 4:delPerson(&contacts); break;n case 5:displayContacts(contacts); break;n case 6:goto END;n }n }nnEND://此处直接跳出恒循环n releaseContacts(&contacts);nn return 0;nn}n

运行结果如下:

//Consequence 04 V1n| 欢迎使用通讯录管理程序 |n|--- 1:插入新的联系人 ---|n|--- 2:查找现有联系人 ---|n|--- 3:更改现有联系人 ---|n|--- 4:删除现有联系人 ---|n|--- 5:显示当前通讯录 ---|n|--- 6:退出通讯录程序 ---|nn请输入指令代码:1n请输入姓名:HarrisWilden请输入电话:0101111nn请输入指令代码:1n请输入姓名:Jackn请输入电话:0101112nn请输入指令代码:1n请输入姓名:Rosen请输入电话:0101113nn请输入指令代码:2n请输入联系人:HarrisWilden联系人:HarrisWilden电话:0101111nn请输入指令代码:2n请输入联系人:Miken找不到该联系人!nn请输入指令代码:5n联系人:Rosen电话:0101113n联系人:Jackn电话:0101112n联系人:HarrisWilden电话:0101111nn请输入指令代码:3n请输入联系人:HarrisWilden请输入联系电话:0101234nn请输入指令代码:5n联系人:Rosen电话:0101113n联系人:Jackn电话:0101112n联系人:HarrisWilden电话:0101234nn请输入指令代码:6n

下面加入内存池:

//Example 04 V2n#include <stdio.h>n#include <stdlib.h>n#include <string.h>nn#define MAX 1024nnstruct Personn{n char name[40];n char phone[20];n struct Person* next;n};nnstruct Person* pool = NULL;nint count;nnvoid getInput(struct Person* person);nvoid printPerson(struct Person* person);nvoid addPerson(struct Person** contects);nvoid changePerson(struct Person* contacts);nvoid delPerson(struct Person** contacts);nstruct Person* findPerson(struct Person* contacts);nvoid displayContacts(struct Person* contacts);nvoid releaseContacts(struct Person** contacts);nvoid releasePool(void);nnvoid getInput(struct Person* person)n{n printf("请输入姓名:");n scanf("%s", person->name);n printf("请输入电话:");n scanf("%s", person->phone);n}nnvoid addPerson(struct Person** contacts)n{n struct Person* person;n struct Person* temp;nn //如果内存池不是空的,那么首先从里面获取空间n if (pool != NULL)n {n person = pool;n pool = pool->next;n count--;n }n //内存池为空,则直接申请n elsen {n person = (struct Person*)malloc(sizeof(struct Person));n if (person == NULL)n {n printf("内存分配失败!n");n exit(1);n }n }nnn getInput(person);nn //将person添加到通讯录中n if (*contacts != NULL)n {n temp = *contacts;n *contacts = person;n person->next = temp;n }n elsen {n *contacts = person;n person->next = NULL;n }n}nnvoid printPerson(struct Person* person)n{n printf("联系人:%sn", person->name);n printf("电话:%sn", person->phone);n}nnstruct Person* findPerson(struct Person* contacts)n{n struct Person* current;n char input[40];nn printf("请输入联系人:");n scanf("%s", input);nn current = contacts;n while (current != NULL && strcmp(current->name, input))n {n current = current->next;n }nn return current;n}nnvoid changePerson(struct Person* contacts)n{n struct Person* person;nn person = findPerson(contacts);n if (person == NULL)n {n printf("找不到联系人!n");n }n elsen {n printf("请输入联系电话:");n scanf("%s", person->phone);n }n}nnvoid delPerson(struct Person** contacts)n{n struct Person* person;n struct Person* current;n struct Person* previous;n struct Person* temp;n {nn };nn //先找到待删除的节点的指针n person = findPerson(*contacts);n if (person == NULL)n {n printf("找不到该联系人!n");n }n elsen {n current = *contacts;n previous = NULL;nn //将current定位到待删除的节点n while (current != NULL && current != person)n {n previous = current;n current = current->next;n }nn if (previous == NULL)n {n //若待删除的是第一个节点n *contacts = current->next;n }n elsen {n //若待删除的不是第一个节点n previous->next = current->next;n }nn //判断内存池中有没有空位n if (count < MAX)n {n //使用头插法将person指向的空间插入内存池中n if (pool != NULL)n {n temp = pool;n pool = person;n person->next = temp;n }n elsen {n pool = person;n person->next = NULL;n }n count++;n }n //没有空位,直接释放n elsen {n free(person);//将内存空间释放n }n }n}nnvoid displayContacts(struct Person* contacts)n{n struct Person* current;nn current = contacts;n while (current != NULL)n {n printPerson(current);n current = current->next;n }n}nnvoid releaseContacts(struct Person** contacts)n{n struct Person* temp;nn while (*contacts != NULL)n {n temp = *contacts;n *contacts = (*contacts)->next;n free(temp);n }n}nnvoid releasePool(void)n{n struct Person* temp;n while (pool != NULL)n {n temp = pool;n pool = pool->next;n free(temp);n }n}nnint main(void)n{n int code;n struct Person* contacts = NULL;n struct Person* person;nn printf("| 欢迎使用通讯录管理程序 |n");n printf("|--- 1:插入新的联系人 ---|n");n printf("|--- 2:查找现有联系人 ---|n");n printf("|--- 3:更改现有联系人 ---|n");n printf("|--- 4:删除现有联系人 ---|n");n printf("|--- 5:显示当前通讯录 ---|n");n printf("|--- 6:退出通讯录程序 ---|n");nn while (1)n {n printf("n请输入指令代码:");n scanf("%d", &code);n switch (code)n {n case 1:addPerson(&contacts); break;n case 2:person = findPerson(contacts);n if (person == NULL)n {n printf("找不到该联系人!n");n }n elsen {n printPerson(person);n }n break;n case 3:changePerson(contacts); break;n case 4:delPerson(&contacts); break;n case 5:displayContacts(contacts); break;n case 6:goto END;n }n }nnEND://此处直接跳出恒循环n releaseContacts(&contacts);n releasePool();nn return 0;nn}n

typedef

给数据类型起别名

C语言是一门古老的语言,它是在1969至1973年间,由两位天才丹尼斯·里奇和肯·汤普逊在贝尔实验室以B语言为基础开发出来的,用于他们的重写UNIX计划(这也为后来UNIX系统的可移植性打下了基础,之前的UNIX是使用汇编语言编写的,当然也是这两位为了玩一个自己设计的游戏而编写的)。天才就是和咱常人不一样,不过他俩的故事,在这篇里面不多啰嗦,我们回到话题。

虽然C语言诞生的很早,但是却依旧不是最早的高级编程语言。目前公认的最早的高级编程语言,是IBM公司于1957年开发的FORTRAN语言。C语言诞生之时,FORTRAN已经统领行业数十年之久。因此,C语言要想快速吸纳FORTRAN中的潜在用户,就必须做出一些妥协。

我们知道,不同的语言的语法,一般来说是不同的,甚至还有较大的差距。比如:

C:

int a, b, c;nfloat i, j, k;n

而FORTRAN语言是这样的:

integer :: a, b, c;nreal :: i, j, k;n

如果让FORTRAN用户使用原来的变量名称进行使用,那么就能够快速迁移到C语言上面来,这就是typedef的用处之一。

我们使用FORTRAN语言的类型名,那就这么办:

typedef int integer;ntypedef float real;nninteger a, b, c;nreal i, j, k;n

结构体的搭档

虽然结构体的出现能够让我们有一个更科学的数据结构来管理数据,但是每次使用结构体都需要struct...,未免显得有些冗长和麻烦。有了typedef的助攻,我们就可以很轻松地给结构体类型起一个容易理解的名字:

typedef struct daten{n int year;n int month;n int day;n} DATE;//为了区分,一般用全大写nnint main(void)n{n DATE* date;n ...n}n

甚至还可以顺便给它的指针也定义一个别名:

typedef struct daten{n int year;n int month;n int day;n} DATE, *PDATE;n

进阶

我们还可以利用typedef来简化一些比较复杂的命令。

比如:

int (*ptr) [5];n

我们知道这是一个数组指针,指向一个5元素的数组。那么我们可以改写成这样:

typedef int(*PTR_TO_ARRAY)[3];n

这样就可以把很复杂的声明变得很简单:

PTR_TO_ARRAY a = &array;n

取名的时候要尽量使用容易理解的名字,这样才能达到使用typedef的最终目的。

共用体

共用体也称联合体。

声明

和结构体还是有点像:

union 共用体名称n{n 成员1;n 成员2;n 成员3;n};n

但是两者有本质的不同。共用体的每一个成员共用一段内存,那么这也就意味着它们不可能同时被正确地访问。如:

//Example 05n#include <stdio.h>n#include <string.h>nnunion Testn{n int i;n double pi;n char str[9];n};nnint main(void)n{n union Test test;nn test.i = 10;n test.pi = 3.14;n strcpy(test.str, "TechZone");nn printf("test.i: %dn", test.i);n printf("test.pi: %.2fn", test.pi);n printf("test.str: %sn", test.str);nn return 0;n}n

执行结果如下:

//Consequence 05ntest.i: 1751344468ntest.pi: 3946574856045802736197446431383475413237648487838717723111623714247921409395495328582015991082102150186282825269379326297769425957893182570875995348588904500564659454087397032067072.00ntest.str: TechZonen

可以看到,共用体只能正确地展示出最后一次被赋值的成员。共用体的内存应该要能够满足最大的成员能够正常存储。但是并不一定等于最大的成员的尺寸,因为还要考虑内存对齐的问题。

共用体可以类似结构体一样来定义和声明,但是共用体还可以允许不带名字:

unionn{n int i;n char ch;n float f;n} a, b;n

初始化

共用体不能在同一时间存放多个成员,所以不能批量初始化

union datan{n int i;n char ch;n float f;n};nnunion data a = {520}; //初始化第一个成员nunion data b = a; //直接使用一个共用体初始化另一个共用体nunion data c = {.ch = 'C'}; //C99的特性,指定初始化成员n

枚举

枚举是一个基本的数据类型,它可以让数据更简洁。

如果写一个判断星期的文章,我们当然可以使用宏定义来使代码更加易懂,不过:

#define MON 1n#define TUE 2n#define WED 3n#define THU 4n#define FRI 5n#define SAT 6n#define SUN 7n

这样的写法有点费键盘。那么枚举就简单多了:

enum DAYn{n MON=1, TUE, WED, THU, FRI, SAT, SUNn};n

**注意:**第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

枚举变量的定义和声明方法和共用体一样,也可以省略枚举名,直接声明变量名。

//Example 06n#include <stdio.h>n#include <stdlib.h>nnint main()n{nn enum color { red = 1, green, blue };nn enum color favorite_color;nn printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): ");n scanf("%d", &favorite_color);nn //输出结果n switch (favorite_color)n {n case red:n printf("你喜欢的颜色是红色");n break;n case green:n printf("你喜欢的颜色是绿色");n break;n case blue:n printf("你喜欢的颜色是蓝色");n break;n default:n printf("你没有选择你喜欢的颜色");n }nn return 0;n}n

执行结果如下:

//Consequence 06n请输入你喜欢的颜色: (1. red, 2. green, 3. blue): 3n你喜欢的颜色是蓝色n

也可以把整数转换为枚举类型:

//Example 07nn#include <stdio.h>n#include <stdlib.h>nnint main()n{n enum dayn {n saturday,n sunday,n monday,n tuesday,n wednesday,n thursday,n fridayn } workday;nn int a = 1;n enum day weekend;n weekend = (enum day) a; //使用强制类型转换n //weekend = a; //错误n printf("weekend:%d", weekend);n return 0;n}n

运行结果如下:

//Consequence 07nweekend:1n

位域

C语言除了开发桌面应用等,还有一个很重要的领域,那就是「单片机」开发。单片机上的硬件资源十分有限,容不得我们去肆意挥洒。单片机使一种集成电路芯片,使采用超大规模集成电路技术把具有数据处理能力的CPU、RAM、ROM、I/O、中断系统、定时器/计数器等功能(有的还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在工控领域使用广泛。

对于这样的设备,通常内存只有256B,那么能够给我们利用的资源就十分珍贵了。在这种情况下,如果我们只需要定义一个变量来存放布尔值,一般就申请一个整型变量,通过1和0来间接存储。但是,显然1和0只用1个bit就能够放完,而一个整型却是4个字节,也就是32bit。这就造成了内存的浪费。

好在,C语言为我们提供了一种数据结构,称为「位域」(也叫位端、位字段)。也就是把一个字节中的二进制位划分,并且你能够指定每个区域的位数。每个域有一个域名,并允许程序中按域名进行单独操作。

使用位域的做法是在结构体定义的时候,在结构体成员后面使用冒号(:)和数字来表示该成员所占的位数。

//Example 08n#include <stdio.h>nnint main(void)n{n struct Testn {n unsigned int a : 1;n unsigned int b : 1;n unsigned int c : 2;n } test;n n test.a = 0;n test.b = 1;n test.c = 2;nn printf("a = %d, b = %d, c = %dn", test.a, test.b, test.c);n printf("size of test = %dn", sizeof(test));nn return 0;n}n

运行结果如下:

//Consequence 08na = 0, b = 1, c = 2nsize of test = 4n

如此一来,结构体test只用了4bit,却存放下了0、1、2三个整数。但是由于2在二进制中是10,因此占了2个bit。如果把test.b赋值为2,那么:

//Consequence 08 V2na = 0, b = 0, c = 2nsize of test = 4n

可以看到,b中的10溢出了,只剩下0。

当然,位域的宽度不能够超过本身类型的长度,比如:

unsigned int a : 100;n

那么就会报错:

错误tC2034t“main::test::a”: 位域类型对位数太小n

位域成员也可以没有名称,只要给出类型和宽度即可:

struct Testn{n unsigned int x : 1;n unsigned int y : 2;n unsigned int z : 3;n unsigned int : 26;n};n

无名位域一般用来作为填充或者调整成员的位置,因为没有名称,所以无名位域并不能够拿来使用。

C语言的标准只说明unsigned int和signed int支持位域,然后C99增加了_Bool类型也支持位域,其他数据类型理论上是不支持的。不过大多数编译器在具体实现时都进行了扩展,额外支持了signed char、unsigned char以及枚举类型,所以如果对char类型的结构体成员使用位域,基本上也没什么问题。但如果考虑到程序的可移植性,就需要谨慎对待了。另外,由于内存的基本单位是字节,而位域只是字节的一部分,所以并不能对位域进行取地址运算。

虽然科技发展日新月异,但是秉承着节约成本这个放之四海而皆准的原则,还是要注意使用!毕竟5毛钱可能是小钱,但是乘以5000万呢?



原文链接:C语言之结构体就这样被攻克了!

转载自:单片机爱好者

原文链接:https://mp.weixin.qq.com/s/fgAZ3zL8ikaqKMB7wydepw


版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除