P40_高级宏定义
1、宏定义的实质
再牛逼的宏定义,说到底也是机械替换。
2、C 语言三大预处理功能
宏定义;文件包含;条件编译。
3、不带参数的宏定义
宏定义分为带参数和不带参数两种情况,不带参数的情况就是我们熟悉的直接替换操作。
例如:
#include <stdio.h>
#define R 6371
#define PI 3.14
// 嵌套宏定义
#define V PI * R * R * R * 4 / 3
int main(void) {
printf("地球的体积大概是:%.2f\n", V);
return 0;
}
这个宏定义的作用是把程序中出现的 PI 在预处理阶段全部替换成 3.14。
注意点:
-
为了和普通的变量进行区分,宏的名字通常我们约定是全部由大写字母组成。
-
宏定义只是简单地进行替换,并且由于预处理是在编译之前进行,而编译工作的任务之一就是语法检查,所以编译器不会对宏定义进行语法检查
-
宏定义不是说明或语句,在末尾不必加分号
-
宏定义的作用域是从定义的位置开始到整个程序结束
-
可以用 #undef 来终止宏定义的作用域
-
宏定义允许嵌套
4、带参数的宏定义
C 语言允许宏定义带有参数,在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数,这点和函数有些类似。
例如:
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
// 错误❌
#define MAX (x, y) (((x) > (y)) ? (x) : (y))
这个宏定义的作用是求出 x 和 y 两个参数中比较大的那一个。
演示:
#include <stdio.h>
#define MAX(x, y) (((x) > (y)) ? (x) : (y))
int main(void) {
int x, y;
printf("请输入两个整数:");
scanf("%d%d", &x, &y);
printf("%d 是较大的那个数!\n", MAX(x, y));
return 0;
}
演示:
- 注意添加括号
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main(void) {
int x;
printf("请输入一个整数:");
scanf("%d", &x);
printf("%d 的平方是:%d\n", x, SQUARE(x));
printf("%d 的平方是:%d\n", x+1, SQUARE(x+1));
return 0;
}
P41_内联函数和小技巧
1、内联函数
普通函数调用是酱紫的:
内联函数调用是酱紫的:
不过内联函数也不是万能的,内联函数虽然节省了函数调用的时间消耗,但由于每一个函数出现的地方都要进行替换,因此增加了代码编译的时间。
另外,并不是所有的函数都能够变成内联函数。现在的编译器也很聪明,就算你不写 inline,它也会自动将一些函数优化成内联函数。
2、# 和
# 和 ## 是两个预处理运算符。
在带参数的宏定义中,# 运算符后面应该跟一个参数,预处理器会把这个参数转换为一个字符串。
## 运算符被称为记号连接运算符,可以使用它来连接多个参数。
2.1. #的使用
case01
#include <stdio.h>
#define STR(s) # s
int main(void)
{
printf("%s\n", STR(FISHC)); // FISHC
return 0;
}
case02
#include <stdio.h>
#define STR(s) # s
int main(void)
{
printf(STR(Hello %s num = %d\n), STR(FishC), 520);
return 0;
}
2.2. ##的使用
case01
#include <stdio.h>
#define TOGETHER(x, y) x ## y
int main(void)
{
printf("%d\n", TOGETHER(2, 50)); // 250
return 0;
}
2.3. 可变参数列表
case01
#include <stdio.h>
#define SHOWLIST(...) printf(# __VA_ARGS__)
int main(void)
{
SHOWLIST(FishC, 520, 3.14\n); // FishC, 520, 3.14
return 0;
}
case02
#include <stdio.h>
#define PRINT(format, ...) printf(# format, ## __VA_ARGS__)
int main(void)
{
PRINT(num = %d\n, 520); // num = 520
PRINT(Hello FishC!\n); // Hello FishC!
return 0;
}
3、可变参数
之前我们学习了如何让函数支持可变参数,带参数的宏定义也是使用可变参数的:
#define SHOWLIST(…) printf(#__VA_ARGS__)
其中 … 表示使用可变参数,VA_ARGS 在预处理中被实际的参数集所替换。
#include <stdio.h>
#define SHOWLIST(...) printf(# __VA_ARGS__)
int main(void)
{
SHOWLIST(FishC, 520, 3.14\n); // FishC, 520, 3.14
return 0;
}
可变参数是允许空参数的(如果可变参数是空参数,## 会将 format 后面的逗号“吃掉”,从而避免参数数量不一致的错误):
#include <stdio.h>
#define PRINT(format, ...) printf(#format, ##__VA_ARGS__)
int main(void)
{
PRINT(num = %d\n, 520); // num = 520
PRINT(Hello FishC!\n); // Hello FishC!
return 0;
}
P42_结构体
1、结构体声明
在 C 语言中,可以使用结构体(structure)来组织不同类型的数据。
结构体声明(structure declaration)是描述结构体组合的主要方法,语法如下:
struct 结构体名称
{
结构体成员 1;
结构体成员 2;
结构体成员 3;
};
其中,结构体成员既可以是任何一种基本的数据类型,也可以是另一个结构体(相当于结构体的嵌套)。
注意:结构体声明你即可以放在所有函数的外面,也可以单独在一个函数里面声明。如果是后者,则该结构体只能在该函数中被定义。
2、定义结构体类型变量
结构体声明只是进行一个框架的描绘,它并不会在内存中分配空间存储数据,直到你定义一个结构体类型的变量。
定义结构体变量的语法如下:
struct 结构体名称 结构体变量名;
注意:这里的 struct 关键字不能丢。
3、访问结构体成员
要访问结构体成员,我们需要引入一个新的运算符——点号(.)运算符。
比如 book.title 就是引用 book 结构体的 title 成员;而 book.price 则是引用 book 结构体的 price 成员。
4、初始化结构体
可以在定义结构体变量的时候同时为其初始化:
struct Book book = {
"《带你学C带你飞》",
"小甲鱼",
48.8,
20171111,
"清华大学出版社"
};
C99 增加了一种新特性:支持初始结构体的指定成员值。
其语法和数组指定初始化元素类似,只不过结构体指定初始化成员使用点号(.)运算符和成员名(数组则是用方括号和下标索引值)。
比如我们可以让程序只初始化 Book 的 price 成员:
struct Book book = {.price = 48.8};
利用该特性,还可以不按结构体声明的成员顺序进行初始化:
struct Book book = {
.publisher = "清华大学出版社",
.price = 48.8,
.date = 20171111
};
注意:其它未初始化的数值型成员也将被自动初始化,其中数值型成员初始化为 0,字符型成员初始化为 ‘\0’。
5、推荐阅读
- 失传的 C 结构体打包技艺 -> 传送门
6、小结
在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:
struct 结构体名{
结构体所包含的变量或数组
};
结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。请看下面的一个例子:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
};
stu 为结构体名,它包含了 5 个成员,分别是 name、num、age、group、score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
注意大括号后面的分号;不能少,这是一条完整的语句。
结构体也是一种数据类型,它由程序员自己定义,可以包含多个其他类型的数据。
像 int、float、char 等是由C语言本身提供的数据类型,不能再进行分拆,我们称之为基本数据类型;而结构体可以包含多个基本类型的数据,也可以包含其他的结构体,我们将它称为复杂数据类型或构造数据类型。
结构体变量
既然结构体是一种数据类型,那么就可以用它来定义变量。例如:
struct stu stu1, stu2;
定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。
stu 就像一个“模板”,定义出来的变量都具有相同的性质。也可以将结构体比作“图纸”,将结构体变量比作“零件”,根据同一张图纸生产出来的零件的特性都是一样的。
你也可以在定义结构体的同时定义结构体变量:
struct stu{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
将变量放在结构体定义的最后即可。
如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:
struct{ //没有写 stu
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在学习小组
float score; //成绩
} stu1, stu2;
这样做书写简单,但是因为没有结构体名,后面就没法用该结构体定义新的变量。
理论上讲结构体的各个成员在内存中是连续存储的,和数组非常类似,例如上面的结构体变量 stu1、stu2 的内存分布如下图所示,共占用 4+4+4+1+4 = 17 个字节。
但是在编译器的具体实现中,各个成员之间可能会存在缝隙,对于 stu1、stu2,成员变量 group 和 score 之间就存在 3 个字节的空白填充(见下图)。这样算来,stu1、stu2 其实占用了 17 + 3 = 20 个字节。
关于成员变量之间存在“裂缝”的原因,我们将在《C语言内存精讲》专题中的《C语言内存对齐,提高寻址效率》一节中详细讲解。
成员的获取和赋值
结构体和数组类似,也是一组数据的集合,整体使用没有太大的意义。数组使用下标[ ]获取单个元素,结构体使用点号.获取单个成员。获取结构体成员的一般格式为:
结构体变量名.成员名;
通过这种方式可以获取成员的值,也可以给成员赋值:
#include <stdio.h>
int main(){
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1;
//给结构体成员赋值
stu1.name = “Tom”;
stu1.num = 12;
stu1.age = 18;
stu1.group = ‘A’;
stu1.score = 136.5;
//读取结构体成员的值
printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);
return 0;
}
运行结果:
Tom的学号是12,年龄是18,在A组,今年的成绩是136.5!
除了可以对成员进行逐一赋值,也可以在定义时整体赋值,例如:
struct{
char *name; //姓名
int num; //学号
int age; //年龄
char group; //所在小组
float score; //成绩
} stu1, stu2 = { “Tom”, 12, 18, ‘A’, 136.5 };
不过整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值,这和数组的赋值非常类似。
需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。
P43_结构体数组和结构体指针
1、结构体嵌套
结构体可以进行嵌套声明,比如:
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
};
要访问其结构体成员的话,就需要使用两层点号(.)运算符来进行操作。
因此,试图用 book.date 访问日期的做法是错误的,你只能通过 book.date.year,book.date.month 和 book.date.day 依次打印出年月日。
2、结构体数组
结构体数组跟之前我们学习过数组概念一致,只不过是每个数组元素不再是简单的基础类型,而是一个结构体类型的数据。
定义结构体数组和定义一个结构体变量的语法类似。
第一种方法是在声明结构体的时候进行定义:
struct 结构体名称
{
结构体成员;
} 数组名[长度];
第二种方法是先声明一个结构体类型(比如上面 Book),再用此类型定义一个结构体数组:
struct 结构体名称
{
结构体成员;
};
struct 结构体名称 数组名[长度];
3、结构体指针
指向结构体变量的指针我们称之为结构体指针:
struct Book * pt;
这里声明的就是一个指向 Book 结构体类型的指针变量 pt。
我们知道数组名其实是指向这个数组第一个元素的地址,所以我们可以将数组名直接赋值给指针变量。
但注意,结构体变量不一样,结构体的变量名并不是指向该结构体的地址,所以要使用取地址运算符(&)才能获取其地址:
pt = &book;
通过结构体指针访问结构体成员有两种方法:
- (*结构体指针).成员名
- 结构体指针->成员名
第一种方法由于点号运算符(.)比指针的取值运算符(*)优先级要高,所以要使用小括号先对指针进行解引用,让它先变成该结构体变量,再用点运算符去访问其成员。
相比之下,第二种方法更加方便和直观。不知道大家有没有发现,第二种方法使用的成员选择运算符(->)自身的形状就是一个箭头,箭头具有指向性,所以我们一下子就把它跟指针联系起来。
需要注意的是,两种方法在实现上是完全等价的,所以无论你习惯使用哪一种方法都可以访问到结构体的成员。
但切记,点号(.)只能用于结构体,而箭头(->)只能用于结构体指针,这两个就不能混淆。
4、案例演示
case01
#include <stdio.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
} book = {
"《带你学C带你飞》",
"小甲鱼",
48.8,
{2017, 11, 11},
"清华大学出版社"
};
int main(void)
{
printf("书名:%s\n", book.title);
printf("作者:%s\n", book.author);
printf("售价:%.2f\n", book.price);
printf("出版日期:%d-%d-%d\n", book.date.year, book.date.month, book.date.day);
printf("出版社:%s\n", book.publisher);
return 0;
}
case02
#include <stdio.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
} book = {
"《带你学C带你飞》",
"小甲鱼",
48.8,
{2017, 11, 11},
"清华大学出版社"
};
int main(void)
{
struct Book *pt;
pt = &book;
printf("书名:%s\n", pt->title);
printf("作者:%s\n", pt->author);
printf("售价:%.2f\n", pt->price);
printf("出版日期:%d-%d-%d\n", pt->date.year, pt->date.month, pt->date.day);
printf("出版社:%s\n", pt->publisher);
return 0;
}
P44_传递结构体变量和指针
1、传递结构体变量
两个相同结构体类型的变量可以直接进行赋值:
...
struct Test {
int x;
int y;
}t1, t2;
t1.x = 3;
t1.y = 4;
t2 = t1;
...
可以看到 t2 = t1; 语句将 t1 这个结构体变量所有成员的值都成功地赋值给了 t2。
那么同样的道理,结构体变量也是可以作为函数参数进行传递的:
#include <stdio.h>
struct Date {
int year;
};
struct Book {
char title[128];
float price;
struct Date date;
};
struct Book getInput(struct Book book);
void printBook(struct Book book);
struct Book getInput(struct Book book)
{
printf("请输入书名:");
scanf("%s", book.title);
printf("请输入售价:");
scanf("%f", &book.price);
printf("请输入出版日期:");
scanf("%d-%d-%d", &book.date.year);
return book;
}
void printBook(struct Book book)
{
printf("书名:%s\n", book.title);
printf("售价:%.2f\n", book.price);
printf("出版日期:%d-%d-%d\n", book.date.year);
}
int main(void)
{
struct Book b1;
printf("请录入第一本书的信息...\n");
b1 = getInput(b1);
printf("打印第一本书的信息...\n");
printBook(b1);
return 0;
}
2、传递指向结构体变量的指针
在最开始的时候,C语言是不允许直接将结构体作为参数传递给函数的。
当初有这么一个限制主要是出于对程序执行效率上的考虑。
因为结构体变量的尺寸可以是很大的,那么在函数调用的过程中将会导致空间和时间上的开销也相对是巨大的。
既然传递结构体变量可能导致程序的开销变大,那么应该如何做才好呢?
没错,使用万能的指针!
#include <stdio.h>
struct Date
{
int year;
};
struct Book
{
char title[128];
float price;
struct Date date;
};
struct Book getInput(struct Book book);
void printBook(struct Book book);
void getInput(struct Book *book)
{
printf("请输入书名:");
scanf("%s", book->title);
printf("请输入售价:");
scanf("%f", &book->price);
printf("请输入出版日期:");
scanf("%d-%d-%d", &book->date.year);
}
void printBook(struct Book *book)
{
printf("书名:%s\n", book->title);
printf("售价:%.2f\n", book->price);
printf("出版日期:%d-%d-%d\n", book->date.year);
}
int main(void)
{
struct Book b1, b2;
printf("请录入第一本书的信息...\n");
getInput(&b1);
printf("打印第一本书的信息...\n");
printBook(&b1);
return 0;
}
这次我们传递过去的就是一个指针,而不是整个庞大的结构体。
注意:这里由于传进来的实参是一个指针,所以要使用箭头(->)来访问结构体变量的成员。
3、动态申请结构体
还可以动态地在堆里面给结构体分配存储空间:
- 优化代码,指针。
int main(void)
{
struct Book *b1;
b1 = (struct Book *)malloc(sizeof(struct Book));
if (b1 == NULL )
{
printf("内存分配失败!\n");
exit(1);
}
printf("请录入第一本书的信息...\n");
getInput(b1);
printf("打印第一本书的信息...\n");
printBook(b1);
free(b2);
return 0;
}
4、构建图书馆
现在要求大家来构建一个图书馆,然后让用户将书籍的信息都录入到里面。
提示:“图书馆” 其实就是存放 Book 结构体变量的指针数组,每个数组元素存放的是指向一个动态申请的 Book 结构体变量的指针。
关系大概是这样子:
代码清单:
#include <stdio.h>
#include <stdlib.h>
#define MAX_SIZE 100
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
};
void getInput(struct Book *book);
void printBook(struct Book *book);
void initLibrary(struct Book *library[]);
void printLibrary(struct Book *library[]);
void releaseLibrary(struct Book *library[]);
void getInput(struct Book *book)
{
printf("请输入书名:");
scanf("%s", book->title);
printf("请输入作者:");
scanf("%s", book->author);
printf("请输入售价:");
scanf("%f", &book->price);
printf("请输入出版日期:");
scanf("%d-%d-%d", &book->date.year, &book->date.month, &book->date.day);
printf("请输入出版社:");
scanf("%s", book->publisher);
}
void printBook(struct Book *book)
{
printf("书名:%s\n", book->title);
printf("作者:%s\n", book->author);
printf("售价:%.2f\n", book->price);
printf("出版日期:%d-%d-%d\n", book->date.year, book->date.month, book->date.day);
printf("出版社:%s\n", book->publisher);
}
void initLibrary(struct Book *library[])
{
int i;
for (i = 0; i < MAX_SIZE; i++)
{
library[i] = NULL;
}
}
void printLibrary(struct Book *library[])
{
int i;
for (i = 0; i < MAX_SIZE; i++)
{
if (library[i] != NULL)
{
printBook(library[i]);
putchar('\n');
}
}
}
void releaseLibrary(struct Book *library[])
{
int i;
for (i = 0; i < MAX_SIZE; i++)
{
if (library[i] != NULL)
{
free(library[i]);
}
}
}
int main(void)
{
struct Book *library[MAX_SIZE];
struct Book *ptr = NULL;
int ch, index = 0;
initLibrary(library);
while (1)
{
printf("请问是否需要录入图书信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
if (index < MAX_SIZE)
{
ptr = (struct Book *)malloc(sizeof(struct Book));
getInput(ptr);
library[index] = ptr;
index++;
putchar('\n');
}
else
{
printf("该图书馆已满,无法录入新数据!\n");
break;
}
}
else
{
break;
}
}
printf("\n\n录入完毕,现在开始打印验证...\n\n");
printLibrary(library);
releaseLibrary(library);
return 0;
}
P45_单链表1
1、声明一个指向自身的结构体
不能这样写:❎
struct Test
{
int x;
int y;
struct Test test;
};
要这样写:✅
struct Test
{
int x;
int y;
struct Test *test;
};
2、单链表
单链表是最简单的一种链表实现方式,它包含两个域,一个信息域和一个指针域:
真正的单链表它还需要一个头指针,用于存放指向链表第一个节点的地址(这样你才能知道到它):
对于 Book 结构体来说,要把它变成链表的其中一个元素,我们只需要为其添加一个指向自身的成员即可:
struct Book
{
char title[128];
char author[40];
float price;
struct Date date;
char publisher[40];
struct Book *next;
};
show
遍历单链表,展示
void printLibrary(struct Book *library)
{
struct Book *book;
int count = 1;
book = library;
while (book != NULL)
{
printf("Book%d: \n", count);
printf("书名: %s\n", book->title);
printf("作者: %s\n", book->author);
book = book->next;
count++;
}
}
free
释放单链表
void releaseLibrary(struct Book **library)
{
struct Book *temp;
while (*library != NULL)
{
temp = *library;
*library = (*library)->next;
free(temp);
}
}
3、在单链表中插入元素(头插法)insert
在单链表中插入元素,事实上只需要修改指针的指向即可:
将书籍添加到单链表的代码我们这么可以写:
//*library是头指针
// **library是头指针参数
void addBook(struct Book **library) {
struct Book *book, *temp;
book = (struct Book *)malloc(sizeof(struct Book));
if (book == NULL) {
printf("内存分配失败!\n");
exit(1);
}
getInput(book);
if (*library != NULL) {
temp = *library; // 保存一下原本的第一个节点
*library = book; // 头指针指向新的节点
book->next = temp; // 新的节点的next指向原本的第一个节点(现在就是第二个节点了)
}
// 空链表的情况
else {
*library = book;
book->next = NULL;
}
}
单链表结构
单链表插入数据
P46_单链表2
1、在单链表中插入元素(尾插法)insert
上一节课我们演示了单链表的头插法,就是将数据插入到单链表的头部位置。
那么相对应的还有另一个种方法:尾插法 —— 将数据插入到单链表的尾部位置。
void addBook(struct Book **library)
{
struct Book *book;
static struct Book *tail;
book = (struct Book *)malloc(sizeof(struct Book));
if (book == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
getInput(book); // 用户输入数据
if (*library != NULL)
{
tail->next = book;
book->next = NULL;
}
else
{
*library = book;
book->next = NULL;
}
tail = book;
}
对比:
- 初级版
//尾插法相应的操作 每次调用函数都循环遍历找到尾指针效率很低,推荐用指针存储每次调用后的 尾部位置!!!1
if(*library==NULL)
{
*library=book;
book->next=NULL;
}
else
{
//设置一个中转变量,并循环遍历单链表直至找到 最后一个节点结束,所以temp此时存储的是最后一个节点地址
temp=*library;
while(temp->next!=NULL)
{
temp=temp->next;
}
temp->next=book;
book->next=NULL;
}
- 高级版
//高效版,用尾指针存储每次添加书籍之后的位置,所以定义一个尾指针tail来存储单链表尾部的地址。
static struct Book *tail; //这里很关键,必须是static类型的,不没有的话会造成每一次调用add函数时,tail重新初始化了。
……
if(*library==NULL)
{
*library=book;
book->next=NULL;
}
else
{
tail->next=book;
book->next=NULL;
}
tail=book;
2、搜索单链表 search
有时候我们可能会对单链表进行搜索操作,比如输入这个书名或者作者的名字,可以找到相关的节点数据。
struct Book *searchBook(struct Book *library, char *target)
{
struct Book *book;
book = library;
while (book != NULL)
{
if (!strcmp(book->title, target) || !strcmp(book->author, target))
{
break;
}
book = book->next;
}
return book;
}
void printBook(struct Book *book)
{
printf("书名:%s\n", book->title);
printf("作者:%s\n", book->author);
}
int main(void)
{
printf("\n请输入书名或作者:");
scanf("%s", input);
book = searchBook(library, input);
if (book == NULL)
{
printf("很抱歉,没能找到!\n");
}
else
{
do
{
printf("已找到符合条件的书籍...\n");
printBook(book);
} while ((book = searchBook(book->next, input)) != NULL);
}
releaseLibrary(&library);
return 0;
}
P47_单链表3
1、插入节点到指定的位置 insert
我们之前说单链表和数组相比较的话,最大的优势就是插入元素到指定位置的效率。
对于数组来说,插入一个元素到指定的位置,需要将其后面所有的元素都挨个儿移动一次,效率之低下可想而知。
相比之下,单链表的效率就要高很多了。因为对于单链表来说,只需要轻轻地改动一下指针即可。
代码实现如下:
#include <stdio.h>
#include <stdlib.h>
struct Node
{
int value;
struct Node *next;
};
void insertNode(struct Node **head, int value)
{
struct Node *previous;
struct Node *current;
struct Node *new;
current = *head;
previous = NULL;
while (current != NULL && current->value < value)
{
previous = current;
current = current->next;
}
new = (struct Node *)malloc(sizeof(struct Node));
if (new == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
new->value = value;
new->next = current;
if (previous == NULL)
{
*head = new;
}
else
{
previous->next = new;
}
}
void printNode(struct Node *head)
{
struct Node *current;
current = head;
while (current != NULL)
{
printf("%d ", current->value);
current = current->next;
}
putchar('\n');
}
int main(void)
{
struct Node *head = NULL;
int input;
printf("开始测试插入整数...\n");
while (1)
{
printf("请输入一个整数(输入-1表示结束):");
scanf("%d", &input);
if (input == -1)
{
break;
}
insertNode(&head, input);
printNode(head);
}
return 0;
}
程序实现如下:
[fishc@localhost s1e47]$ gcc test1.c && ./a.out
开始测试插入整数...
请输入一个整数(输入-1表示结束):5
5
请输入一个整数(输入-1表示结束):3
3 5
请输入一个整数(输入-1表示结束):8
3 5 8
请输入一个整数(输入-1表示结束):9
3 5 8 9
请输入一个整数(输入-1表示结束):1
1 3 5 8 9
请输入一个整数(输入-1表示结束):0
0 1 3 5 8 9
请输入一个整数(输入-1表示结束):2
0 1 2 3 5 8 9
请输入一个整数(输入-1表示结束):4
0 1 2 3 4 5 8 9
请输入一个整数(输入-1表示结束):7
0 1 2 3 4 5 7 8 9
请输入一个整数(输入-1表示结束):-1
我们重点分析一下这个 insertNode 函数:
while (current != NULL && current->value < value)
{
previous = current;
current = current->next;
}
while 函数用于找到符合条件的链表节点,也就是在有序的链表中找到比传入的value更大的值,然后停下来;如果没有,则在链表的尾部位置停止(current == NULL 时结束循环)。
由于单链表一旦指向下一个节点,就没办法回头了,所以我们使用 previous 变量来记录 current 节点的上一个节点。
最后判断一下 previous 变量,如果为 NULL 的话,说明 while 循环它压根儿就没进去过,有两种情况,要么是这是一个空链表(current == NULL),或者该值比当前链表中所有的节点的 value 成员都小,无论是哪一种情况,我们都将该值插入为单链表的第一个节点即可。
2、在单链表中删除元素 delete
我们的单链表应该支持删除某一个节点的数据。
删除某个节点的数据,其实也是修改指针的事儿,两个步骤搞定:
- 修改待删除节点的上一个节点的指针,将其指向待删除节点的下一个节点
- 释放待删除节点的内存空间
代码实现如下:
...
void deleteNode(struct Node **head, int value)
{
struct Node *previous;
struct Node *current;
current = *head;
previous = NULL;
while (current != NULL && current->value != value)
{
previous = current;
current = current->next;
}
if (current == NULL)
{
printf("找不到匹配的节点!\n");
return ;
}
else
{
if (previous == NULL)
{
*head = current->next;
}
else
{
previous->next = current->next;
}
free(current);
}
}
...
程序实现如下:
[fishc@localhost s1e47]$ gcc test2.c && ./a.out
开始测试插入整数...
请输入一个整数(输入-1表示结束):5
5
请输入一个整数(输入-1表示结束):3
3 5
请输入一个整数(输入-1表示结束):1
1 3 5
请输入一个整数(输入-1表示结束):9
1 3 5 9
请输入一个整数(输入-1表示结束):8
1 3 5 8 9
请输入一个整数(输入-1表示结束):7
1 3 5 7 8 9
请输入一个整数(输入-1表示结束):0
0 1 3 5 7 8 9
请输入一个整数(输入-1表示结束):-1
开始测试删除整数...
请输入一个整数(输入-1表示结束):0
1 3 5 7 8 9
请输入一个整数(输入-1表示结束):9
1 3 5 7 8
请输入一个整数(输入-1表示结束):7
1 3 5 8
请输入一个整数(输入-1表示结束):8
1 3 5
请输入一个整数(输入-1表示结束):-1
简单分析一下实现代码,当 current 指向 NULL 的情况有两种,要么这是一个空链表,要么在单链表的所有节点的 value 成员中找不到对应的数值,所以统一跟用户说找不到即可。
如果 current 不为 NULL,我们还要预防 previous 是否为 NULL,有一种情况会导致这种局面的发生,那就是当要删除的节点是单链表的第一个节点的时候,在这种情况下需要特殊处理:要将 head 指针指向该节点。
这个链表的知识,说白了就是考核你对结构体和指针的理解程度。
在编程学习的初期阶段,你不可避免会遇到比如“自己思前想后毫无头绪,但一看到代码思路立马就清晰了!”类似这样的体验,总的来说,就是你看得多,练得少导致的。
学习编程总的来说就是一个量变引发质变的过程,不要怕出错,不要吝啬于练习,你打的代码越多,你的功力就越深厚。
P48_内存池
内存碎片
产生原因:简单来说,比如我们使用malloc
申请了12kB的内存空间,我们使用完毕后调用free函数。但是这个空间依旧存在,只有下一次申请12kB以及以下的大小,才会重新拿来分配使用。在一些极端的环境下就会产生很大的问题。
malloc 内存分配原理及内存碎片产生的原因:https://fishc.com.cn/forum.php?mod=viewthread&tid=85239
时间上的消耗
内存池
具体流程:
简单来说,当用户申请内存的时候,在内存池(垃圾桶)找到可以使用的空间,就直接返回给用户。如果没有合适的空间,再向内存malloc申请空间。释放的时候就放在内存池。
使用单链表来维护一个简单地内存池。
只需要将没有用的内存空间地址依次用一个单链表记录下来;当再次需要的时候,从这个单链表中获取即可。
案例说明
通讯录程序,加入内存池管理空间。
代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX 1024
struct Person
{
char name[40];
char phone[20];
struct Person *next;
};
struct Person *pool = NULL;
int count;
void getInput(struct Person *person);
void printPerson(struct Person *person);
void addPerson(struct Person **contacts);
void changePerson(struct Person *contacts);
void delPerson(struct Person **contacts);
struct Person *findPerson(struct Person *contacts);
void displayContacts(struct Person *contacts);
void releaseContacts(struct Person **contacts);
void releasePool(void);
void getInput(struct Person *person)
{
printf("请输入姓名:");
scanf("%s", person->name);
printf("请输入电话:");
scanf("%s", person->phone);
}
void addPerson(struct Person **contacts)
{
struct Person *person;
struct Person *temp;
// 如果内存池非空,则直接从里面获取空间
if (pool != NULL)
{
person = pool;
pool = pool->next;
count--;
}
// 如果内存池为空,则调用malloc函数申请新的内存空间
else
{
person = (struct Person *)malloc(sizeof(struct Person));
if (person == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
}
getInput(person);
// 将person用头插法添加到通讯录中
if (*contacts != NULL)
{
temp = *contacts;
*contacts = person;
person->next = temp;
}
else
{
*contacts = person;
person->next = NULL;
}
}
void printPerson(struct Person *person)
{
printf("联系人:%s\n", person->name);
printf("电话:%s\n", person->phone);
}
struct Person *findPerson(struct Person *contacts)
{
struct Person *current;
char input[40];
printf("请输入联系人:");
scanf("%s", input);
current = contacts;
while (current != NULL && strcmp(current->name, input))
{
current = current->next;
}
return current;
}
void changePerson(struct Person *contacts)
{
struct Person *person;
person = findPerson(contacts);
if (person == NULL)
{
printf("找不到该联系人!\n");
}
else
{
printf("请输入新的联系电话:");
scanf("%s", person->phone);
}
}
void delPerson(struct Person **contacts)
{
struct Person *temp;
struct Person *person;
struct Person *current;
struct Person *previous;
// 先找到待删除的节点指针
person = findPerson(*contacts);
if (person == NULL)
{
printf("找不到该联系人!\n");
}
else
{
current = *contacts;
previous = NULL;
// current定位到待删除的节点
while (current != NULL && current != person)
{
previous = current;
current = current->next;
}
if (previous == NULL)
{
// 待删除的节点是第一个节点
*contacts = current->next;
}
else
{
// 待删除的节点不是第一个节点
previous->next = current->next;
}
// 判断内存池是不是有空位
if (count < MAX)
{
if (pool != NULL)
{
temp = pool;
pool = person;
person->next = temp;
}
else
{
pool = person;
person->next = NULL;
}
count++;
}
else
{
free(person);
}
}
}
void displayContacts(struct Person *contacts)
{
struct Person *current;
current = contacts;
while (current != NULL)
{
printPerson(current);
current = current->next;
}
}
void releaseContacts(struct Person **contacts)
{
struct Person *temp;
while (*contacts != NULL)
{
temp = *contacts;
*contacts = (*contacts)->next;
free(temp);
}
}
void releasePool(void)
{
struct Person *temp;
while (pool != NULL)
{
temp = pool;
pool = pool->next;
free(temp);
}
}
int main(void)
{
int code;
struct Person *contacts = NULL;
struct Person *person;
printf("| 欢迎使用通讯录管理程序 |\n");
printf("|--- 1:插入新的联系人 ---|\n");
printf("|--- 2:查找已有联系人 ---|\n");
printf("|--- 3:更改已有联系人 ---|\n");
printf("|--- 4:删除已有联系人 ---|\n");
printf("|--- 5:显示当前通讯录 ---|\n");
printf("|--- 6:退出通讯录程序 ---|\n");
printf("|- Powered by FishC.com -|\n");
while (1)
{
printf("\n请输入指令代码:");
scanf("%d", &code);
switch (code)
{
case 1: addPerson(&contacts); break;
case 2: person = findPerson(contacts);
if (person == NULL)
{
printf("找不到该联系人!\n");
}
else
{
printPerson(person);
}
break;
case 3: changePerson(contacts); break;
case 4: delPerson(&contacts); break;
case 5: displayContacts(contacts); break;
case 6: goto END;
}
}
END:
releaseContacts(&contacts);
releasePool();
return 0;
}
P49_基础typedef
1、问题引入
Fortran 在1957年由IBM开发出,是世界上第一个被正式采用并流传至今的高级编程语言。
C语言定义变量的语法:
- int a, b, c;
- float i, j, k;
Fortran语言定义变量的语法:
- integer :: a, b, c
- real :: i, j, k
如果能让C语言相同的关键字定义变量:
- integer a, b, c;
- real i, j, k;
2、案例演示
case01 宏定义实现
#include <stdio.h>
#define integer int
int main(void)
{
integer a;
int b;
a = 520;
b = a;
printf("a = %d\n", a); // a = 520
printf("b = %d\n", b); // b = 520
// warning: format specifies type 'int' but the argument has type 'unsigned long'
printf("size of a = %d\n", sizeof(a)); // size of a = 4
return 0;
}
case02 typedef实现
#include <stdio.h>
typedef int integer;
int main(void)
{
integer a;
int b;
a = 520;
b = a;
printf("a = %d\n", a); // a = 520
printf("b = %d\n", b); // b = 520
// size of a = 4
printf("size of a = %d\n", sizeof(a)); // warning: format specifies type 'int' but the argument has type 'unsigned long'
return 0;
}
case03 typedef与指针
#include <stdio.h>
typedef int INTEGER, *PTRINT;
int main(void)
{
INTEGER a = 520;
PTRINT b, c;
b = &a;
c = b;
printf("addr of a = %p\n", c); // addr of a = 0x7ff7b1c18fb8
return 0;
}
case04 宏定义指针
#include <stdio.h>
typedef int INTEGER;
#define PTRINT int*
int main(void)
{
INTEGER a = 520;
PTRINT b, c;
b = &a;
c = b; // warning
printf("addr of a = %p\n", c);
return 0;
}
case05 typedef与结构体
#include <stdio.h>
#include <stdlib.h>
typedef struct Date
{
int year;
int month;
int day;
} DATE, *PDATE;
int main(void)
{
struct Date *date;
date = (PDATE)malloc(sizeof(DATE));
if (date == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
date->year = 2017;
date->month = 5;
date->day = 15;
printf("%d-%d-%d\n", date->year, date->month, date->day);
return 0;
}
P50_进阶typedef
在编程中使用typedef目的一般有两个:
- 一个是给变量起一个容易记住且意义明确的别名;
- 另一个是简化一些比较复杂的类型声明;
1、案例说明
case01
首先PTR_TO_FUN是一个函数指针类型的变量,可以替换下列复杂的类型声明
#include <stdio.h>
typedef int (*PTR_TO_FUN)(int, int);
int add(int, int);
int sub(int, int);
int calc(PTR_TO_FUN, int, int);
PTR_TO_FUN select(char);
int add(int num1, int num2)
{
return num1 + num2;
}
int sub(int num1, int num2)
{
return num1 - num2;
}
int calc(int (*fp)(int, int), int num1, int num2)
{
return (*fp)(num1, num2);
}
int (*select(char op))(int, int)
{
switch(op)
{
case '+': return add;
case '-': return sub;
}
}
int main()
{
int num1, num2;
char op;
int (*fp)(int, int);
printf("请输入一个式子(如1+3):");
scanf("%d%c%d", &num1, &op, &num2);
fp = select(op);
printf("%d %c %d = %d\n", num1, op, num2, calc(fp, num1, num2));
return 0;
}
case02
PTR_TO_ARRAY是一个指针,int类型的数组指针。
#include <stdio.h>
typedef int (*PTR_TO_ARRAY)[3];
int main(void)
{
int array[3] = {1, 2, 3};
PTR_TO_ARRAY ptr_to_array = &array;
int i;
for (i = 0; i < 3; i++)
{
printf("%d\n", (*ptr_to_array)[i]);
}
return 0;
}
case03
PTR_TO_FUN是一个指针,返回值是int的函数指针,没有参数。
#include <stdio.h>
typedef int (*PTR_TO_FUN)(void);
int fun(void)
{
return 520;
}
int main(void)
{
PTR_TO_FUN ptr_to_fun = &fun;
printf("%d\n", (*ptr_to_fun)());
return 0;
}
case04
PTR_TO_FUN 是一个指针,返回值是int*类型,参数是int
#include <stdio.h>
typedef int *(*PTR_TO_FUN)(int);
int *funA(int num)
{
printf("%d\t", num);
return #
}
int *funB(int num)
{
printf("%d\t", num);
return #
}
int *funC(int num)
{
printf("%d\t", num);
return #
}
int main(void)
{
PTR_TO_FUN array[3] = {&funA, &funB, &funC};
int i;
for (i = 0; i < 3; i++)
{
printf("addr of num: %p\n", (*array[i])(i));
}
return 0;
}
P51_共用体
参考:https://www.runoob.com/cprogramming/c-unions.html
共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。
1、定义共用体
为了定义共用体,您必须使用 union 语句,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员。union 语句的格式如下
union [union tag]
{
member definition;
member definition;
...
member definition;
} [one or more union variables];
nion tag 是可选的,每个 member definition 是标准的变量定义,比如 int i; 或者 float f; 或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,您可以指定一个或多个共用体变量,这是可选的。下面定义一个名为 Data 的共用体类型,有三个成员 i、f 和 str:
union Data { int i; float f; char str[20]; } data;
现在,Data 类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。您可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。
共用体占用的内存应足够存储共用体中最大的成员。例如,在上面的实例中,Data 将占用 20 个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。下面的实例将显示上面的共用体占用的总内存大小:
实例
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main()
{
union Data data;
printf( "Memory size occupied by data : %d\n", sizeof(data));
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Memory size occupied by data : 20
2、初始化共用体
union data a = {520}; // 初始化第一个成员
union data b = a; // 直接用一个共用体初始化另一个共用体
union data c = {.ch = 'C'}; // C99新增特性,指定初始化成员
3、访问共用体成员
为了访问共用体的成员,我们使用成员访问运算符(.)。成员访问运算符是共用体变量名称和我们要访问的共用体成员之间的一个句号。您可以使用 union 关键字来定义共用体类型的变量。下面的实例演示了共用体的用法:
实例
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
data.f = 220.5;
strcpy( data.str, "C Programming");
printf( "data.i : %d\n", data.i);
printf( "data.f : %f\n", data.f);
printf( "data.str : %s\n", data.str);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
data.i : 1917853763
data.f : 4122360580327794860452759994368.000000
data.str : C Programming
在这里,我们可以看到共用体的 i 和 f 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。现在让我们再来看一个相同的实例,这次我们在同一时间只使用一个变量,这也演示了使用共用体的主要目的:
实例
#include <stdio.h>
#include <string.h>
union Data
{
int i;
float f;
char str[20];
};
int main( )
{
union Data data;
data.i = 10;
printf( "data.i : %d\n", data.i);
data.f = 220.5;
printf( "data.f : %f\n", data.f);
strcpy( data.str, "C Programming");
printf( "data.str : %s\n", data.str);
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
data.i : 10
data.f : 220.500000
data.str : C Programming
在这里,所有的成员都能完好输出,因为同一时间只用到一个成员。
P52_enum 枚举
参考:https://www.runoob.com/cprogramming/c-enum.html
定义
枚举是 C 语言中的一种基本数据类型,它可以让数据更简洁,更易读。
枚举语法定义格式为:
enum 枚举名 {枚举元素1,枚举元素2,……};
接下来我们举个例子,比如:一星期有 7 天,如果不用枚举,我们需要使用 #define 来为每个整数定义一个别名:
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7
这个看起来代码量就比较多,接下来我们看看使用枚举的方式:
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
这样看起来是不是更简洁了。
**注意:**第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。
可以在定义枚举类型时改变枚举元素的值:
enum season {spring, summer=3, autumn, winter};
没有指定值的枚举元素,其值为前一元素加 1。也就说 spring 的值为 0,summer 的值为 3,autumn 的值为 4,winter 的值为 5
枚举变量的定义
前面我们只是声明了枚举类型,接下来我们看看如何定义枚举变量。
我们可以通过以下三种方式来定义枚举变量
1、先定义枚举类型,再定义枚举变量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
enum DAY day;
2、定义枚举类型的同时定义枚举变量
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
3、省略枚举名称,直接定义枚举变量
enum
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
实例01
#include <stdio.h>
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
};
int main()
{
enum DAY day;
day = WED;
printf("%d",day);
return 0;
}
以上实例输出结果为:
3
在C 语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以按照 C 语言规范是没有办法遍历枚举类型的。
不过在一些特殊的情况下,枚举类型必须连续是可以实现有条件的遍历。
实例02
使用 for 来遍历枚举的元素:
#include <stdio.h>
enum DAY
{
MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;
int main()
{
// 遍历枚举元素
for (day = MON; day <= SUN; day++) {
printf("枚举元素:%d \n", day);
}
}
以上实例输出结果为:
枚举元素:1
枚举元素:2
枚举元素:3
枚举元素:4
枚举元素:5
枚举元素:6
枚举元素:7
以下枚举类型不连续,这种枚举无法遍历。
enum
{
ENUM_0,
ENUM_10 = 10,
ENUM_11
};
实例03
枚举在 switch 中的使用:
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum color { red=1, green, blue };
enum color favorite_color;
/* 用户输入数字来选择颜色 */
printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): ");
scanf("%u", &favorite_color);
/* 输出结果 */
switch (favorite_color)
{
case red:
printf("你喜欢的颜色是红色");
break;
case green:
printf("你喜欢的颜色是绿色");
break;
case blue:
printf("你喜欢的颜色是蓝色");
break;
default:
printf("你没有选择你喜欢的颜色");
}
return 0;
}
以上实例输出结果为:
请输入你喜欢的颜色: (1. red, 2. green, 3. blue): 1
你喜欢的颜色是红色
实例04
以下实例将整数转换为枚举:
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum day
{
saturday,
sunday,
monday,
tuesday,
wednesday,
thursday,
friday
} workday;
int a = 1;
enum day weekend;
weekend = ( enum day ) a; //类型转换
//weekend = a; //错误
printf("weekend:%d",weekend);
return 0;
}
以上实例输出结果为:
weekend:1