21、函数初体验
C语言要求函数必须“先定义,再调用”,定义函数的格式如下/函数的定义:
类型名 函数名(参数列表)
{
函数体
}
- 类型名就是函数的返回值,如果这个函数不准备返回任何数据,那么需要写上 void(void 就是无类型,表示没有返回值)。
- 函数名就是函数的名字,一般我们根据函数实现的功能来命名,比如 print_C 就是“打印C”的意思,一目了然。
- 参数列表指定了参数的类型和名字,如果这个函数没有参数,那么这个位置直接写上小括号即可(())。
- 函数体就是指定函数的具体实现过程,是函数中最重要的部分。
函数的声明/声明函数的格式非常简单,只需要去掉函数定义中的函数体再加上分号(;)即可:
例如:
int max(int, int); // 声明可以只写参数的类型,不写名字
编写一个函数 max,接收两个整型参数,并返回它们中的较大的值。
#include <stdio.h>
int max(int, int); // 声明可以只写参数的类型,不写名字
int max(int x, int y)
{
if (x > y)
return x; // 程序一旦执行return语句,表明函数返回,后边的代码不会继续执行。
else
return y;
}
int main(void)
{
int x, y, z;
printf("请输入两个整数:");
scanf("%d%d", &x, &y);
z = max(x, y);
printf("它们中较大的值是:%d\n", z);
return 0;
}
补充:如果函数不需要参数,建议定义时在函数名后边的小括号中写上 void,明确表示该函数无参数。
22、函数和指针
定义一个子函数,交换两个整数:三种方式
方式一:作用域,单向传值
方式一:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void swap(int x, int y);
void swap(int x ,int y) {
int temp;
printf("In swap,互换前:x=%d,y=%d\n",x,y);
temp = x;
x = y;
y = temp;
printf("In swap,互换后:x=%d,y=%d\n", x, y);
}
int main() {
int x = 3, y = 5;
printf("In main,互换前:x=%d,y=%d\n", x, y);
swap(x, y);
printf("In main, 互换后:x = %d, y = %d\n", x, y);
return 0;
}
方式二:传递地址,单向改变
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void swap(int *x, int *y);
void swap(int *x, int *y)
{
int *temp;
printf("In swap, 互换前:x = %d, y = %d\n", *x, *y);
temp = x;
x = y;
y = temp;
printf("In swap, 互换后:x = %d, y = %d\n", *x, *y);
}
int main()
{
int x = 3, y = 5;
printf("In main, 互换前:x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("In main, 互换后:x = %d, y = %d\n", x, y);
return 0;
}
方式三:传递地址,双向改变
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
void swap(int *x, int *y);
void swap(int *x, int *y)
{
int temp;
printf("In swap, 互换前:x = %d, y = %d\n", *x, *y);
temp = *x;
*x = *y;
*y = temp;
printf("In swap, 互换后:x = %d, y = %d\n", *x, *y);
}
int main()
{
int x = 3, y = 5;
printf("In main, 互换前:x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("In main, 互换后:x = %d, y = %d\n", x, y);
return 0;
}
子函数,参数名为数组,传递的是地址:
即:子函数中,改变数组a[5]的值,但是在main函数中,重新打印数组a,发现a[5]=520;
#include <stdio.h>
void get_array(int a[10]);
void get_array(int a[10])
{
int i;
a[5] = 520;
for (i = 0; i < 10; i++)
{
printf("a[%d] = %d\n", i, a[i]);
}
}
int main()
{
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int i;
get_array(a);
printf("在main函数里边再打印一次...\n");
for (i = 0; i < 10; i++)
{
printf("a[%d] = %d\n", i, a[i]);
}
return 0;
}
通过sizeof 进行证明:
#include <stdio.h>
void get_array(int b[10]);
void get_array(int b[10])
{
printf("sizeof b: %d\n", sizeof(b));
}
int main()
{
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
printf("sizeof a: %d\n", sizeof(a));
get_array(a);
return 0;
}
请问下面这两个函数的定义等价吗?k
func(int a[], int n)
{
……
}
与
func{int *a, int n}
{
……
}
答:以上两种写法是等价的。因为函数不存在传递(拷贝)整个数组的情况,当用数组名做实参时,传递的实际上是数组第一个元素的地址。因此,形参是一个指向数组元素类型的指针。@
并不存在将整个数组作为参数传递的方式,传递的实际上是指向这个数组的第一个元素的地址而已。
23、指针函数 和 函数指针
1、指针函数/ int *p():使用指针变量作为函数的返回值; 是一个函数
示例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
char *getWord(char);
char *getWord(char c)
{
switch (c)
{
case 'A': return "Apple";
case 'B': return "Banana";
case 'C': return "Cat";
case 'D': return "Dog";
default: return "None";
}
}
int main()
{
char input;
printf("请输入一个字母:");
scanf("%c", &input);
printf("%s\n", getWord(input));
return 0;
}
不要返回局部变量的指针;注意函数中变量的生存期!如下:
#include <stdio.h>
char *getWord(char);
char *getWord(char c)
{
char str1[] = "Apple";
char str2[] = "Banana";
char str3[] = "Cat";
char str4[] = "Dog";
char str5[] = "None";
switch (c)
{
case 'A': return str1;
case 'B': return str2;
case 'C': return str3;
case 'D': return str4;
default: return str5;
}
}
int main()
{
char input;
printf("请输入一个字母:");
scanf("%c", &input);
printf("%s\n", getWord(input));
return 0;
}
2、函数指针/ int(*p)() : 是一个指针,指向函数名,即地址
示例:
#include <stdio.h>
int square(int);
int square(int num) {
return num * num;
}
int main() {
int num;
int(*fp)(int);
printf("请输入一个整数:");
scanf("%d",&num);
fp = square;// 函数名相当于函数的地址
printf("%d * %d = %d\n",num,num,(*fp)(num));
}
示例: [课后作业] S1E39:C语言的内存布局 | 课后测试题及答案
#include <stdio.h>
void (*array[3])();
void funcA(void) {
printf("I'm funcA\n");
}
void funcB(void) {
printf("I'm funcB\n");
}
void funcC(void) {
printf("I'm funcC\n");
}
int main() {
array[0] = funcA;
array[1] = funcB;
array[2] = funcC;
array[0](); //函数指针
array[1]();
array[2]();
}
0. 函数名在表达式中应该如何被解读?xE)}n"zgO
ZVgRsJO,4;c}>NXKQk{hi
答:函数名可以在表达式中被解读成“指向该函数的指针”。
1. 函数指针和指针函数有什么区别?6iMKx=
0LwPCBI^@a3c}o8*Ue)v2GY~J
答:函数指针是一个指向函数的指针;指针函数是一个返回指针变量的函数。`rE
[课后作业] S1E30:指针函数和函数指针 | 课后测试题及答案
24、局部变量 和 全局变量
C99 标准允许在 for 语句的第一个表达式部分声明变量,它的作用范围仅限于复合语句的内部。
#include <stdio.h>
int main() {
int i = 520;
printf("before i=%d\n",i);
/**/
for (int i = 0; i < 10; i++) {
printf("%d\n",i);
}
printf("after i=%d\n",i);
int j = 0;
printf("j=%d\n",j);
return 0;
}
运行结果:
C 语言允许在程序的任意位置声明变量
[课后作业] S1E31:局部变量和全局变量 | 课后测试题及答案
观察下面程序的执行结果,请问在什么情况下变量 a 和变量 b 可以存放在同一个地址而里面的数据却允许不同?
答:当变量 a 和变量 b 分别为两个函数的局部变量时,就可能出现上述情况。[_ak
25、作用域 和 链接属性
(1)C语言编译器有四种不同类型的作用域:
- 代码块作用域;所谓代码块,就是位于一对花括号之间的所有语句。
- 文件作用域;任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们的声明位置开始,到文件的结尾处都是可以访问的。另外,函数名也具有文件作用域,因为函数名本身也是在代码块之外。
- 原型作用域;
- 函数作用域;
(2)定义和声明:
- 当一个变量被定义的时候,编译器为变量申请内存空间并填充一些值。
- 当一个变量被声明的时候,编译器就知道该变量被定义在其他地方。
- 声明是通知编译器该变量名及相关的类型已存在,不需要再为此申请内存空间。
- 局部变量既是定义又是声明。
- 定义只能来一次,否则就叫做重复定义某个同名变量;而声明可以有很多次。
(3)链接属性
在 C 语言中,链接属性一共有三种:
- external(外部的)-- 多个文件中声明的同名标识符表示同一个实体
- internal(内部的)-- 单个文件中声明的同名标识符表示同一个实体
- none(无)-- 声明的同名标识符被当作独立不同的实体(比如函数的局部变量,因为它们被当作独立不同的实体,所以不同函数间同名的局部变量并不会发生冲突)
默认情况下,具备文件作用域的标识符拥有 external 属性。也就是说该标识符允许跨文件访问。对于 external 属性的标识符,无论在不同文件中声明多少次,表示的都是同一个实体。
使用 static 关键字可以使得原先拥有 external 属性的标识符变为 internal 属性。这里有两点需要注意:
- 使用 static 关键字修改链接属性,只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)
- 链接属性只能修改一次,也就是说一旦将标识符的链接属性变为 internal,就无法变回 external 了
26、生存期 和 存储类型
(1)生存期:
C 语言的变量拥有两种生存期,分别是静态存储期(static storage duration)和自动存储期(automatic storage duration)。
具有文件作用域的变量具有静态存储期(比如全局变量),函数名也拥有静态存储期。具有静态存储期的变量在程序执行期间将一直占据存储空间,直到程序关闭才释放;具有代码块作用域的变量一般情况下具有自动存储期(比如局部变量和形式参数),具有自动存储期的变量在代码块结束时将自动释放存储空间。
(2)存储类型
变量的存储类型其实是指存储变量值的内存类型,C 语言提供了 5 种不同的存储类型,分别是:
auto、register、static、extern 、 typedef。
- 自动变量(auto)
在代码块中声明的变量默认的存储类型就是自动变量,使用关键字 auto 来描述。所以函数中的形参、局部变量,包括复合语句的中定义的局部变量都具有自动变量。自动变量拥有代码块作用域,自动存储期和空连接属性。
- 寄存器变量(register)
寄存器是存在于 CPU 的内部的,CPU 对寄存器的读取和存储可以说是几乎没有任何延迟。
将一个变量声明为寄存器变量,那么该变量就有可能被存放于CPU的寄存器中。为什么我这里说有可能呢?因为CPU的寄存器空间是十分有限,所以编译器并不会让你将所有声明为register的变量都放到寄存器中。事实上,有可能所有的register关键字都被忽略,因为编译器有自己的一套优化方法,会权衡哪些才是最常用的变量。在编译器看来,它觉得它比你更了解程序。而那些被忽略的register变量,它们会变成普通的自动变量。
所以寄存器变量和自动变量在很多方面的是一样的,它们都拥有代码块作用域,自动存储期和空链接属性。
- 静态局部变量(static)
static 用于描述具有文件作用域的变量或函数时,表示将其链接属性从 external 修改为 internal,它的作用范围就变成了仅当前源文件可以访问。
但如果将 static 用于描述局部变量,那么效果又会不一样了。默认情况下,局部变量是 auto 的,具有自动存储期的变量。如果使用 static 来声明局部变量,那么就可以将局部变量指定为静态局部变量,同时初始化为0。static 使得局部变量具有静态存储期,所以它的生存期与全局变量一样,直到程序结束才释放。
static 和 extern
static 关键字使得默认具有 external 链接属性的标识符变成 internal 链接属性;extern 关键字是用于告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。
静态局部变量,示例函数:
#include <stdio.h>
void func(void); //声明函数
void func(void) //定义函数
{
static int count = 0; //静态局部变量,如果不初始化,系统也会初始化为0;
printf("count = %d\n", count);
count++;
}
int main(void)
{
int i;
for (i = 0; i < 10; i++)
{
func();
}
return 0;
}
但是,子函数中定义的局部变量用static修饰后,其作用范围仍是子函数内,只不过其数值得到保留:如下实例
#include <stdio.h>
void func(void); //声明函数
void func(void) //定义函数
{
static int count = 0; //静态局部变量,如果不初始化,系统也会初始化为0;
printf("count = %d\n", count);
count++;
}
int main(void)
{
int i;
for (i = 0; i < 10; i++)
{
func();
}
printf("count=%d\n",count); //main函数内,引用子函数的 ststic int count 无效
return 0;
}
编写一个程序,像下图一样打印出各种各样的地址并尝试搞清楚它们在内存中的位置关系。#
#include <stdio.h>
#include <stdlib.h>
int global_var1;
int global_var2;
static int file_static_var1;
static int file_static_var2;
void func1(int func1_param1, int func1_param2)
{
static int func1_static_var1;
static int func1_static_var2;
// 输出行参的地址
printf("addr of func1_param1: %010p\n", &func1_param1);
printf("addr of func1_param2: %010p\n", &func1_param2);
// 输出静态局部变量的地址
printf("addr of func1_static_var1: %010p\n", &func1_static_var1);
printf("addr of func1_static_var2: %010p\n", &func1_static_var2);
}
void func2(const int func2_const_param1, const int func2_const_param2)
{
int func2_var1;
int func2_var2;
// 输出const参数的地址
printf("addr of func2_const_param1: %010p\n", &func2_const_param1);
printf("addr of func2_const_param2: %010p\n", &func2_const_param2);
// 输出局部变量的地址
printf("addr of func2_var1: %010p\n", &func2_var1);
printf("addr of func2_var2: %010p\n", &func2_var2);
}
int main(void)
{
char *string1 = "I love FishC.com";
char *string2 = "very much";
// 输出函数的地址
printf("addr of func1: %010p\n", func1);
printf("addr of func2: %010p\n", func2);
// 输出字符串常量的地址
printf("addr of string1: %010p\n", string1);
printf("addr of string2: %010p\n", string2);
// 输出全局变量的地址
printf("addr of global_var1: %010p\n", &global_var1);
printf("addr of global_var2: %010p\n", &global_var2);
// 输出文件内的static变量的地址
printf("addr of file_static_var1: %010p\n", &file_static_var1);
printf("addr of file_static_var2: %010p\n", &file_static_var2);
// 输出函数内局部变量的地址
func1(1, 2);
func2(3, 4);
return 0;
}
运行结果:
27、递归
递归的实质:递归从原理上来说就是函数调用自身这么一个行为。
实现递归,满足的两个条件:
- 调用函数本身;
- 设置了正确的结束条件;
递归的优势和劣势
优势:递归的思考角度跟通常的迭代(你可以理解为 for 循环之类的)迥然不同,所以有时候使用迭代思维解决不了的问题,使用递归思维则一下子迎刃而解。
劣势:递归的执行效率通常比迭代低很多,所以递归程序要更消耗时间;由于递归函数是不断调用函数本身,在最底层的函数开始返回之前,程序都是一致在消耗栈空间的,所以递归程序要“吃”更多的内存空间;递归的结束条件设置非常重要,因为一旦设置错误,就容易导致程序万劫不复(崩溃)。
递归函数示例:请写出下面代码会输出啥?
#include <stdio.h>
void up_and_down(int n);
void up_and_down(int n)
{
printf("%d ", n);
if (n > 0)
{
up_and_down(--n);
}
printf("%d ", n);
}
int main(void)
{
int n;
printf("请输入一个整数:");
scanf("%d", &n);
up_and_down(n);
putchar('\n');
return 0;
}
结果:
编写一个程序,用递归实现将一个十进制的正整数转换成二进制的形式显示。=$i
#include <stdio.h>
void binary(unsigned long n);
void binary(unsigned long n)
{
int r;
r = n % 2;
if (n >= 2)
{
binary(n / 2);
}
putchar('0' + r); // '0' + 1 == '1'
}
int main(void)
{
unsigned long number;
printf("请输入一个正整数:");
scanf("%lu", &number);
binary(number);
putchar('\n');
return 0;
}
28、动态内存管理
malloc、free函数使用示例:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i, length;
char *buffer;
printf("请输入字符串的长度:");
scanf("%d", &length);
buffer = (char *)malloc(length + 1); // 还要存放'\0'字符
if (buffer == NULL)
{
printf("内存空间不足!\n");
exit(1);
}
printf("请输入%d个字符的字符串:", length);
getchar(); // 清除上一次输入残留的'\n'字符
for (i = 0; i < length; i++)
{
buffer[i] = (char)getchar();
}
//buffer[i + 1] = '\0';
buffer[i] = '\0';
printf("您输入的字符串是:%s\n", buffer);
free(buffer);
return 0;
}
[扩展阅读] malloc 内存分配原理及内存碎片产生的原因
free
free 函数释放 ptr 参数指向的内存空间。该内存空间必须是由 malloc、calloc 或 realloc 函数申请的。否则,该函数将导致未定义行为。如果 ptr 参数是 NULL,不执行任何操作。
注意:该函数并不会修改 ptr 参数的值,所以调用后它仍然指向原来的地方(变为非法空间)。
示例:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *ptr = NULL;
ptr = (int *)malloc(sizeof(int));
if (ptr == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
printf("Before free, ptr = %p\n", ptr);
free(ptr);
printf("After free, ptr = %p\n", ptr);
return 0;
}
运行结果:两次打印出来的地址是一样的!
有人可能会疑惑:”难道调用 free() 函数释放内存后,ptr 不是应该指向 NULL 的吗?“。这里务必要注意一点,free() 函数释放的是 ptr 指向的内存空间,但它并不会修改 ptr 指针的值。也就是说,ptr 现在虽然指向 0x8b23008,但该空间已经被 free() 函数释放了,所以对于该空间的引用已经失去了意义(会报错)。因此,为了防止后面再次对 ptr 指向的空间进行访问,建议在调用 free() 函数后随即将 ptr 赋值为 NULL。`
calloc [函数快查] calloc -- 申请并初始化一系列内存空间
calloc 函数在内存中动态地申请 nmemb 个长度为 size 的连续内存空间(即申请的总空间尺寸为 nmemb * size),这些内存空间全部被初始化为 0。如果 nmemb 或 size 参数的值为 0,那么返回值会因标准库实现的不同而不同,可能是 NULL,也可能返回一个指针值,稍后可以传递给 free 函数
备注:
calloc 函数与 malloc 函数的一个重要区别是:calloc 函数在申请完内存后,自动初始化该内存空间为零,而 malloc 函数不进行初始化操作,里边数据是随机的。
因此,下面两种写法是等价的:
// calloc() 分配内存空间并初始化
char *str1 = (char *)calloc(8, 10);
// malloc() 分配内存空间并用 memset() 初始化
char *str2 = (char *)malloc(80);
memset(str2, 0, 80);
realloc [函数快查] realloc -- 重新分配内存空间
内存泄漏:如果申请的动态内存没有及时释放会怎样?
(1)隐式内存泄漏:即用完内存块没有及时使用free函数释放
(2)丢失内存块地址:
示例:(针对丢失内存块地址)
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *ptr;
int num = 123;
ptr = (int *)malloc(sizeof(int));
if (ptr == NULL)
{
printf("分配内存失败!\n");
exit(1);
}
printf("请输入一个整数:");
scanf("%d", ptr);
printf("你输入的整数是:%d\n", *ptr);
ptr = #
printf("你输入的整数是:%d\n", *ptr);
free(ptr); //此处?
return 0;
}
如果,程序中,free(ptr),会报错:
是因为:指针ptr已经指向了局部变量num,用free(ptr),去释放一个局部变量!当然,malloc申请的动态内存的地址丢掉了……
memset 通常和 malloc 搭配使用:
malloc动态分配的内存,使用memset初始化为0;示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define NUM 100
int main(void)
{
int *ptr;
ptr = (int *)malloc(NUM * sizeof(int));
if (ptr == NULL)
{
exit(1);
}
memset(ptr, 0, NUM);
return 0;
}
注意,malloc、calloc、realloc、memcpy、memset都是以字节为单位进行操作!
示例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define NUM 10
int main(void)
{
int i;
int *ptr = NULL;
ptr = (int *)malloc(NUM * sizeof(int));
if (ptr == NULL)
{
exit(1);
}
memset(ptr, 1, NUM * sizeof(int));
printf("memset之后的存储空间的值是:");
for (i = 0; i < NUM; i++)
{
printf("0x%08x ", ptr[i]);
}
printf("\n");
free(ptr);
return 0;
}
运行结果:
以 mem 开头的函数比如 memcpy,memcmp 被编入字符串标准库(函数的声明包含在 string.h),那么请问它们与同在该标注库的 strncpy,strcnmp 函数有什么区别呢?Ef5j*RLGT
*?NXM_nCWp1wtIK,k}|lU02
答:从形式上看,str 开头的函数使用的是 char 类型的指针(char *)作为参数和返回值;而 mem 开头的函数使用的是 void 类型的指针(void *)作为参数和返回值。K7AWL[e<
w?POBUe%Z^g(p:Tb5tmk2]}1
从本质上看,str 开头的函数主要目的是提供字符串的拷贝,比较等操作;而 mem 开头的函数主要目的是提供一个高效的函数接口来处理内存空间的数据。Z
请问下面代码存在什么问题?a
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
static int *pi = (int *)malloc(sizeof(int));
*pi = 520;
printf("*pi = %d\n", *pi);
return 0;
}
比较好的一个示例:子函数 func中指针ptr,申请的动态内存,在main函数中释放:
#include "stdio.h"
#include <stdlib.h>
int *func(void) {
int *ptr = NULL;
ptr = (int *)malloc(sizeof(int));
if (ptr == NULL) {
exit(1);
}
*ptr = 520;
return ptr;
}
int main() {
int *ptr1 = NULL;
ptr1 = func();
printf("%d\n",*ptr1);
free(ptr1);
return 0;
}
[课后作业] S1E38:动态内存管理2 | 课后测试题及答案
29、C语言的内存布局
(1)如何定义一个字符串?什么是字符串?
所谓字符串本质上就是以'\0作为'结尾的特殊字符数组;
(2)定义字符串的过程中有哪些注意点?
由于字符串本质上其实就是以'\0作为'结尾的特殊字符数组,所以定义字符串时,必须保证字符串存储的最后一个元素为'\0'。
字符串的字面量是常量,比如"hello world"。
(3)实现方式:
char 字符串名称[] = {字符串所含元素}; 也叫数组形式
注意点:通过这种方式来定义字符串,则需要在{}中写上'\0';
char hi[] = {'h','e','l','l','o','\0'};
char 字符串名称[字符串长度] = {字符串所含元素}; 也叫直接赋首地址
注意点:如果字符串所含元素中我们没有加上'\0',那么字符串长度应该就为字符串实际长度+1(大于等于1都行);
char Str[5] = { 'h','e','l','l','o' };
char Str1[6] = { 'h','e','l','l','o' };
char Str2[7] = { 'h','e','l','l','o' };
char 字符串名称[] = "字符串所包含元素" 也叫省略长度
注意点:通过这种方式来定义字符串的底层原理其实就是将"TomHe",转换为了{'T', 'o', 'm', 'H', 'e', '\0'};
例如:char name[] = "TomHe"; <==> char name[] ={'T', 'o', 'm', 'H', 'e', '\0'};
字符指针:
通过字符指针来定义字符串 "hello",指针 hi 指向一个存放字符串 "hello" 的连续地址单元的首地址
char *hi = "hello";
参考:
C 语言的内存布局规律
可以看到局部变量的地址是占据高地,接着是 malloc 函数申请的动态内存空间,然后是全局变量和静态局部变量。
不过这两者都需要区分是否已经初始化,已经初始化的放一块,未初始化的放一块,并且未初始化的地址要比已经初始化的要更高一些。接着下来是字符串常量,最后是函数的地址。
典型 C 语言程序的内存空间划分
根据内存地址从低到高分别划分为:
- 代码段(Text segment)
- 数据段(Initialized data segment)
- BSS段(Bss segment/Uninitialized data segment)
- 栈(Stack)
- 堆(Heap)
示例程序:运行平台:WIN10 + vs2017
#include <stdio.h>
#include <stdlib.h>
// 三个全局变量,其中一个未初始化
int global_uninit_var;
int global_init_var1 = 520;
int global_init_var2 = 880;
// 函数地址
void func(void);
void func(void)
{
;
}
int main(void)
{
// main 函数内的局部变量
int local_var1;
int local_var2;
// main 函数内的静态局部变量,其中一个未初始化
static int static_uninit_var;
static int static_init_var = 456;
//字符串指针常量
char *str1 = "I love FishC.com!";
char *str2 = "You are right!";
// malloc 分配动态内存
int *malloc_var = (int *)malloc(sizeof(int));
printf("addr of func -> %p\n", func);
printf("addr of str1 -> %p\n", str1);
printf("addr of str2 -> %p\n", str2);
printf("addr of global_init_var1 -> %p\n", &global_init_var1);
printf("addr of global_init_var2 -> %p\n", &global_init_var2);
printf("addr of static_init_var -> %p\n", &static_init_var);
printf("addr of static_uninit_var -> %p\n", &static_uninit_var);
printf("addr of global_uninit_var -> %p\n", &global_uninit_var);
printf("addr of malloc_var -> %p\n", malloc_var);
printf("addr of local_var1 -> %p\n", &local_var1);
printf("addr of local_var2 -> %p\n", &local_var2);
return 0;
}
运行结果:和小甲鱼的分析有出入;
按照地址高低分别为:未初始化全局变量、未初始化静态变量、静态变量、全局变量、字符指针、函数、堆/malloc分配内存、局部变量、
[课后作业] S1E39:C语言的内存布局 | 课后测试题及答案
30、高级宏定义
宏定义的实质:机械的替换
编译器的工作流程:编译器的工作流程
- 预处理(Pre-Processing) 就是对各种预处理指令(#include、#define 和 #ifdef 这些以“#”开始的代码行)进行处理,并删除注释,添加行号以及文件名标识。预处理:宏定义、文件包含、条件编译
- 编译(Compiling)
- 汇编(Assembling)
- 链接(Linking)
(1) 请问下面的宏为什么无法比较 x 和 y 两个参数的大小?+
#define MAX (x, y) (((x) > (y)) ? (x) : (y))
答:这是由于在 MAX 和参数 (x, y) 之间存在空格导致的。8;:rnox#
8u!bP~=f}35mhV URI6d{W+Oy4i-
在宏定义的时候,宏的名字和参数列表之间不能有空格,否则会被当做是无参数的宏定义。sT
(2) 从初学者的角度来看,带参数的宏定义和函数十分相似,你作为渐入佳境的大牛,你能否从实现逻辑上指出它俩的不同之处?4
答:虽然宏定义也有所谓的”形参“和”实参“,但在宏定义的过程中,并不需要为形参指定类型。这是因为宏定义只是进行机械替换,并不需要为参数分配内存空间。e7T8Au
p(!Q2%GxO;<AYWk5sMJh&c
而函数不同,在函数中形参和实参是两个不同的变量,都有自己的内存空间和作用域,调用时是要把实参的值传递给形参。z
示例程序:默认将参数从右到左的顺序依次入栈,参数和局部变量都是存放在栈中的 ,介绍在上面的链接
即printf语句中,先执行square(i++),然后执行(i-1),最后是"%d的平方为%d\n"
#include <stdio.h>
int square(int x) {
return x * x;
}
int main() {
int i = 1;
while (i <= 5) {
printf("%d的平方为%d\n",i-1,square(i++));
}
return 0;
}
运行结果:
31、内联函数
(1)inline函数
普通函数调用是酱紫的:
内联函数调用是酱紫的:
不过内联函数也不是万能的,内联函数虽然节省了函数调用的时间消耗,但由于每一个函数出现的地方都要进行替换,因此增加了代码编译的时间。
另外,并不是所有的函数都能够变成内联函数。现在的编译器也很聪明,就算你不写 inline,它也会自动将一些函数优化成内联函数。
对比普通函数来说,内联函数的特点是什么??ql*U8&Nxu
:2)O*Utoh]SL"$ap8b=(zs
答:当函数被声明为内联函数之后,编译器会将其内联展开,而不是按通常的函数调用机制进行调用。
(2)# 和 ##
# 和 ## 是两个预处理运算符。
- 在带参数的宏定义中,# 运算符后面应该跟一个参数,预处理器会把这个参数转换为一个字符串。
- ## 运算符被称为记号连接运算符,可以使用它来连接多个参数
示例:
#include <stdio.h>
#define STR(s) # s
int main() {
printf("%s\n",STR(Hello World));
return 0;
}
运行结果:
示例代码:
#include <stdio.h>
#define STR(s) # s
int main(void) {
printf(STR(Hello %s num=%d\n),STR(World),520);
return 0;
}
示例代码:
#include <stdio.h>
#define TOGETHER(x,y) x##y
int main() {
printf("%d\n",TOGETHER(2,50));
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);
return 0;
}
运行结果:
可变参数是允许空参数的(如果可变参数是空参数,## 会将 format 后面的逗号“吃掉”,从而避免参数数量不一致的错误):
示例:
#include <stdio.h>
#define SHOWLIST(format,...) printf(#format, # __VA_ARGS__)
int main(void)
{
SHOWLIST(FishC, 520, 3.14\n);
return 0;
}
运行结果:
示例:
#include <stdio.h>
#define PRINT(format, ...) printf(#format, ##__VA_ARGS__)
int main(void)
{
PRINT(num = %d\n, 520);
PRINT(Hello FishC!\n);
return 0;
}
运行结果:
[课后作业] S1E41:内联函数和一些鲜为人知的技巧 | 课后测试题及答案
32、结构体
(1)结构体声明及定义变量:
示例代码:
struct Book {
char title[120];
char author[20];
float price;
unsigned int date;
char publisher[120];
}; //不要忘记冒号
struct Book book;
访问结构体成员变量:点号(.)
#include <stdio.h>
struct Book {
char title[120];
char author[20];
float price;
unsigned int date;
char publisher[120];
};
int main() {
struct Book book;
printf("请输入书名:");
scanf("%s", book.title);
printf("请输入作者:");
scanf("%s", book.author);
printf("请输入售价:");
scanf("%f", &book.price);
printf("请输入出版日期:");
scanf("%d", &book.date);
printf("请输入出版社:");
scanf("%s", book.publisher);
printf("\n====数据录入完毕!====\n");
printf("书名:%s\n", book.title);
printf("作者:%s\n", book.author);
printf("售价:%.2f\n", book.price);
printf("出版日期:%d\n", book.date);
printf("出版社:%s\n", book.publisher);
return 0;
}
结构体变量的初始化:
可以指定某一个元素进行初始化,也可以一下全部初始化:
struct Book book = {
"《带你学C带你飞》",
"小甲鱼",
48.8,
20171111,
"清华大学出版社"
};
//只初始化Book的price成员
struct Book book = {.price = 48.8};
//或者 不按结构体声明的成员顺序进行初始化
struct Book book = {
.publisher = "清华大学出版社",
.price = 48.8,
.date = 20171111
};
(2)结构体嵌套
#include <stdio.h>
struct Date {
int year;
int month;
int day;
};
struct Book {
char title[120];
char author[30];
float price;
struct Date date;
char publisher[40];
};
int main() {
struct Book book;
book = {
"《带你学C带你飞》",
"小甲鱼",
48.8,
{2017, 11, 11},
"清华大学出版社"
};
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;
}
(3)结构体数组
方法1:声明结构体的时候进行定义
struct 结构体名称
{
结构体成员;
} 数组名[长度];
方法2:先声明一个结构体类型,然后利用此类型定义一个结构体数组:
struct 结构体名称
{
结构体成员;
};
struct 结构体名称 数组名[长度];
初始化结构体数组:
struct Book book[3] = {
{"《零基础入门学习Python》", "小甲鱼", 49.5, {2016, 11, 11}, "清华大学出版社"},
{"《零基础入门学习Scratch》", "不二如是", 49.9, {2017, 10, 1}, "清华大学出版社"},
{"《带你学C带你飞》", "小甲鱼", 49.9, {2017, 11, 11}, "清华大学出版社"}
};
(4)结构体指针
其中 ,结构体指针,指向结构体变量时,结构体变量必须取地址,给指针变量;此处与数组名直接给指针变量有区别。
struct Book * pt;
pt = &book;
// pt=book; 这样写存在问题!
通过结构体指针访问结构体成员的方法:
- (*结构体指针).成员名
- 结构体指针->成员名
#include <stdio.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char title[128];
char author[40];
//double price;
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;
}
(5)结构体赋值
两个结构体变量的类型必须一致,可以直接赋值:示例 都是 Test 类型
#include <stdio.h>
int main(void)
{
struct Test
{
int x;
int y;
}t1, t2;
t1.x = 3;
t1.y = 4;
t2 = t1;
printf("t2.x = %d, t2.y = %d\n", t2.x, t2.y);
return 0;
}
(6)传递结构体变量
传递结构体(或其他int 、char等数据类型)变量到子函数中,需要初始化;(结构体变量作为参数传递)
传递指向结构体变量的指针,不需要对数据初始化;
两个示例程序,如下:
传递结构体变量时,需要初始化:
#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];
};
struct Book getInput(struct Book book);
void printBook(struct Book book);
struct Book 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);
return book;
}
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);
}
int main(void)
{
struct Book b1 = { 0 }, b2 = {0};
//struct Book b1 , b2 ;
printf("请录入第一本书的信息...\n");
b1 = getInput(b1);
putchar('\n');
printf("请录入第二本书的信息...\n");
b2 = getInput(b2);
printf("\n\n录入完毕,现在开始打印验证...\n\n");
printf("打印第一本书的信息...\n");
printBook(b1);
putchar('\n');
printf("打印第二本书的信息...\n");
printBook(b2);
return 0;
}
传递指向结构体变量的指针:
#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];
};
void getInput(struct Book *book);
void printBook(struct Book *book);
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);
}
int main(void)
{
struct Book b1, b2;
printf("请录入第一本书的信息...\n");
getInput(&b1);
putchar('\n');
printf("请录入第二本书的信息...\n");
getInput(&b2);
printf("\n\n录入完毕,现在开始打印验证...\n\n");
printf("打印第一本书的信息...\n");
printBook(&b1);
putchar('\n');
printf("打印第二本书的信息...\n");
printBook(&b2);
return 0;
}
使用 malloc 函数为结构体分配存储空间
#include <stdio.h>
#include <stdlib.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];
};
void getInput(struct Book *book);
void printBook(struct Book *book);
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);
}
int main(void)
{
struct Book *b1, *b2;
b1 = (struct Book *)malloc(sizeof(struct Book));
b2 = (struct Book *)malloc(sizeof(struct Book));
if (b1 == NULL || b2 == NULL)
{
printf("内存分配失败!\n");
exit(1);
}
printf("请录入第一本书的信息...\n");
getInput(b1);
putchar('\n');
printf("请录入第二本书的信息...\n");
getInput(b2);
printf("\n\n录入完毕,现在开始打印验证...\n\n");
printf("打印第一本书的信息...\n");
printBook(b1);
putchar('\n');
printf("打印第二本书的信息...\n");
printBook(b2);
free(b1);
free(b2);
return 0;
}
问题:构建一个图书馆:
要求采用 指针数组
代码如下:注意 指针数组中每个元素都是结构体指针,在程序的最后需要free释放!
#include <stdio.h>
#include <stdlib.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];
};
void getInput(struct Book *book);
void printBook(struct Book * book);
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("BookName: %s\n", book->title);
printf("BookAuthor: %s\n", book->author);
printf("BookPrice: %.2f\n", book->price);
printf("Book 出版日期: %d-%d-%d\n", book->date.year, book->date.month, book->date.day);
printf("Book 出版社: %s\n", book->publisher);
}
#define NUM 520
int main(void)
{
system("color 0a");//装B必备
struct Book *bookarray[NUM]; //指针数组
int number = 0;
int input;
do{
//bookarray[number] = (struct Book *)realloc(bookarray,(number+1)*sizeof(struct Book));
bookarray[number] = (struct Book *)malloc(sizeof(struct Book));
if (bookarray[number] == NULL) {
printf("Failure!!!");
exit(1);
}
printf("请输入第%d本书的信息\n", number + 1);
getInput(bookarray[number++]);
printf("\n输入1继续录入,输入其他整数退出录入:");
scanf("%d", &input);
} while (input == 1);
printf("录入完毕现在打印验证....\n\n");
for (int i = 0; i < number; i++) {
printf("第%d本书的信息\n", i + 1);
printBook(bookarray[i]);
putchar('\n');
}
for (int i = 0; i < number; i++) {
free(bookarray[i]); //注意free 方式
}
return 0;
}
运行结果:
部分网友答案(不一定对):[课后作业] S1E44:传递结构体变量和结构体指针 | 课后测试题及答案
33、链表
备注:如果主调函数和被调函数之间的,实参、形参是传递地址;
一定要明确的是:实参是地址,形参也是地址;
- 如果都表达成最外层的地址,那么子函数就是一个常见的函数,不存在指针函数之类的,就是看 形参是几重解引用!
- 如果都表达成最外层的地址,子函数 是指针函数,则形参就要少一重解引用!详情见下面示例:链表头插法(1)
传递的是谁的地址?子函数中,对形参做什么操作?两个要对应起来!
例如:传递的是指针变量的地址,要用指针的指针来接!
#include <stdio.h>
// 接收的是变量的地址,所以要用指針來接
void func1(int *pa)
{
*pa = 100;
}
// 接收的是指針变量的地址,所以要用指針的指針來接
void func2(int **ppb)
{
**ppb = 200;
// *ppb =pb = &b
}
int main(void)
{
int a = 10;
int b = 20;
int *pb = NULL;
pb = &b;
printf("before a = %d\n", a);
func1(&a); //把变量 a 的位址传进去
printf("after a = %d\n", a);
printf("before pb = %d, b = %d\n", *pb, b);
func2(&pb); //把指針变量的位址传进去
printf("after pb = %d, b = %d\n", *pb, b);
return 0;
}
(1)头插法
示例程序:对应两种 不同的方式:** 和 *
#include <stdio.h>
#include <stdlib.h>
struct Book
{
char title[128];
char author[40];
struct Book *next;
};
void getInput(struct Book *book);
void addBook(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);
}
#if 0
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;
}
else
{
*library = book;
book->next = NULL;
}
}
#else
struct Book *add_book(struct Book *library) //指针函数
{
struct Book *book = NULL, *temp;
book = (struct Book *)malloc(sizeof(struct Book));
if (book == NULL) exit(1);
getInput(book);
if (library != NULL)
{
temp = library;
library = book;
book->next = temp;
}
else
{
library = book;
book->next = NULL;
}
printf("插入成功\n");
return library;
}
#endif
void printLibrary(struct Book *library)
{
struct Book *book;
int count = 1;
book = library;
while (book != NULL)
{
printf("Book%d: ", count);
printf("书名: %s\n", book->title);
printf("作者: %s\n", book->author);
book = book->next;
count++;
}
}
void releaseLibrary(struct Book *library)
{
struct Book *temp;
while (library != NULL)
{
temp = library;
library = library->next;
free(temp);
}
}
int main(void)
{
struct Book *library = NULL;
int ch;
while (1)
{
printf("请问是否需要录入书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
//addBook(&library); //传进去的为指针变量的地址
library = add_book(library); //传进去的为指针的值(指针指向的地址)
}
else
{
break;
}
}
printf("请问是否需要打印图书信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
printLibrary(library);
}
releaseLibrary(library);
return 0;
}
运行结果 :
(2)尾插法
遍历方法示例代码:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
struct Book
{
char title[128];
char author[40];
struct Book *next;
};
void getInput(struct Book *book);
void addBook(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);
}
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;
// 定位单链表的尾部位置
while (temp->next != NULL)
{
temp = temp->next;
}
// 插入数据
temp->next = book;
book->next = NULL;
}
else
{
*library = book;
book->next = NULL;
}
}
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++;
}
}
void releaseLibrary(struct Book **library)
{
struct Book *temp;
while (*library != NULL)
{
temp = *library;
*library = (*library)->next;
free(temp);
}
}
int main(void)
{
struct Book *library = NULL;
int ch;
while (1)
{
printf("请问是否需要录入书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
addBook(&library);
}
else
{
break;
}
}
printf("请问是否需要打印图书信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
printLibrary(library);
}
releaseLibrary(&library);
return 0;
}
运行结果:
指针法尾插法:
#include <stdio.h>
#include <stdlib.h>
struct Book
{
char title[128];
char author[40];
struct Book *next;
};
void getInput(struct Book *book);
void addBook(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);
}
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; //tail 指针指向 当前book
}
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++;
}
}
void releaseLibrary(struct Book **library)
{
struct Book *temp;
while (*library != NULL)
{
temp = *library;
*library = (*library)->next;
free(temp);
}
}
int main(void)
{
struct Book *library = NULL;
int ch;
while (1)
{
printf("请问是否需要录入书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
addBook(&library);
}
else
{
break;
}
}
printf("请问是否需要打印图书信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
printLibrary(library);
}
releaseLibrary(&library);
return 0;
}
根据作者或者 书名,进行搜索;并输出!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Book
{
char title[128];
char author[40];
struct Book *next;
};
void getInput(struct Book *book);
void addBook(struct Book **library);
void printLibrary(struct Book *library);
struct Book *searchBook(struct Book *library, char *target);
void printBook(struct Book *book);
void releaseLibrary(struct Book **library);
void getInput(struct Book *book)
{
printf("请输入书名:");
scanf("%s", book->title);
printf("请输入作者:");
scanf("%s", book->author);
}
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;
}
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++;
}
}
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);
}
void releaseLibrary(struct Book **library)
{
struct Book *temp;
while (*library != NULL)
{
temp = *library;
*library = (*library)->next;
free(temp);
}
}
int main(void)
{
struct Book *library = NULL;
struct Book *book;
char input[128];
int ch;
while (1)
{
printf("请问是否需要录入书籍信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
addBook(&library);
}
else
{
break;
}
}
printf("请问是否需要打印图书信息(Y/N):");
do
{
ch = getchar();
} while (ch != 'Y' && ch != 'N');
if (ch == 'Y')
{
printLibrary(library);
}
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;
}
示例结果:
(3)插入节点 ,单链表
示例代码:
#define _CRT_SECURE_NO_WARNINGS
#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;
while (1)
{
printf("请输入一个整数(输入-1表示结束):");
scanf("%d", &input);
if (input == -1)
{
break;
}
insertNode(&head, input);
printNode(head);
}
return 0;
}
运行结果:
(4)删除节点 ,单链表
示例代码:
#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');
}
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);
}
}
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);
}
printf("开始测试删除整数...\n");
while (1)
{
printf("请输入一个整数(输入-1表示结束):");
scanf("%d", &input);
if (input == -1)
{
break;
}
deleteNode(&head, input);
printNode(head);
}
return 0;
}
运行结果:
34、typedef
typedef 实质为类型取别名!
- typedef 是对“类型”的封装;并且,可以一次性取多个别名;
- 宏定义是直接替换
示例1:
#include <stdio.h>
#define integer int //不需要加分号
typedef int INTEGER; //注意方式,最后加 分号
int main(void)
{
integer a;
int b;
a = 520;
b = a;
INTEGER c;
printf("a = %d\n", a);
printf("b = %d\n", b);
printf("size of a = %d\n", sizeof(a));
printf("size of c = %d\n", sizeof(c));
return 0;
}
运行结果:
示例2:#define 只是简单的替换
#include <stdio.h>
//typedef int integer;
#define integer int
int main(void)
{
unsigned integer a;
a = -1;
printf("a = %u\n", a);
return 0;
}
typedef 可以一次性取多个别名; 而 #define 不可以
示例3:
#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", &a);
printf("addr of a = %p\n", b);
printf("addr of a = %p\n", c);
return 0;
}
运行结果:
示例4:
typedef 与 struct 结构体
示例:
#include <stdio.h>
#include <stdlib.h>
#include "vld.h" //检测是否存在内存泄漏
typedef struct Date
{
int year;
int month;
int day;
} DATE, *PDATE;
int main(void)
{
system("color 0A"); //更改控制台窗口背景、前景颜色
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);
free(date);
return 0;
}
运行结果:
typedef 的进阶:
编程中使用typedef目的一般有两个:
- 给变量起一个容易记住且意义明确的别名
- 简化一些复杂的类型声明
(1)数组指针 声明
typedef int(*PTR_TO_ARRAY)[3]; //声明一个 数组指针
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include "vld.h" //检测是否存在内存泄漏
typedef int(*PTR_TO_ARRAY)[3]; //声明一个 数组指针
int main() {
system("color 0A"); //更改控制台窗口背景、前景颜色
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;
}
运行结果:
(2)函数指针 声明
typedef int(*PTR_TO_FUN)(void); //声明 函数指针
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <vld.h>
typedef int(*PTR_TO_FUN)(void); //声明 函数指针
int fun() {
return 520;
}
int main() {
system("color 0A");
PTR_TO_FUN ptr_to_fun = &fun; //定义一个 函数指针指向子函数
printf("%d\n",(*ptr_to_fun)()); //先解引用,然后取值
return 0;
}
运行结果:
(3)指针函数 声明
typedef int *(*PTR_TO_FUN)(int); //*()(int)指针函数,
int *(*array[3])(int); 分析过程如下:
(*array[3])就是一个指针数组
int *A(int);
typedef int *(*PTR_TO_FUN)(int);
PTR_TO_FUN array[3];
指针函数中含有 指针数组
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <vld.h>
typedef int *(*PTR_TO_FUN)(int); //*()(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() {
system("color 0A");
PTR_TO_FUN array[3] = {&funA,&funB,&funC};
int i;
for (i = 0; i < 3; i++) {
printf("add of num: %p\n", (*array[i])(i));
}
return 0;
}
运行结果:
复杂的typedef
示例代码:
#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;
}
35、UNION/共用体 和 枚举类型
共用体:
- 共用体成员共享同一个地址;
- 多个成员变量,最后的成员覆盖之前的变量;
- 共用体占用内存为:大于最大一个元素所占内存;
代码示例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <vld.h>
union Test {
int i;
double pi;
char str[10];
};
int main() {
system("color 0A");
union Test test;
test.i = 520;
test.pi = 3.14;
strcpy(test.str,"Fishc.com");
//打印共用体成员地址:
printf("addr of test.i:%p\n",&test.i);
printf("addr of test.pi:%p\n", &test.pi);
printf("addr of test.str:%p\n", &test.str);
//打印共用体成员值:
printf("test.i:%d\n", test.i);
printf("test.pi:%.2f\n", test.pi);
printf("test.str:%s\n", test.str);
//
printf("size of test:%d\n",sizeof(test));
return 0;
}
运行结果:
枚举:
time 函数:用于返回从标准计时点到当前时间的秒数,即从 1970 年 1 月 1 日的 UTC 时间 0 时 0 分 0 秒算起到现在所经过的秒数。
#include <time.h>
...
time_t time(time_t *t);
示例代码:
#include <stdio.h>
#include <time.h>
int main(void)
{
time_t seconds;
// 下面语句也可以写成:time(&seconds);
seconds = time(NULL);
printf("1970年1月1日零点到现在经过了%ld个小时!\n", seconds / 3600);
return 0;
}
运行结果:
localtime 函数 localtime -- 获取当前的本地时间和日期
首先用 函数 time 获取到目前为主的时间,然后通过结构体变量 tm,得到 当地时间和日期;
struct tm
{
int tm_sec; /* 秒,范围为 0~59,60 的话表示闰秒 */
int tm_min; /* 分,范围是 0~59 */
int tm_hour; /* 时,范围是 0~23 */
int tm_mday; /* 一个月中的第几天,范围是 1~31 */
int tm_mon; /* 一年中的第几个月,范围是 0~11 */
int tm_year; /* 自 1900 年往后的第几年 */
int tm_wday; /* 星期几,自星期天开始计算,范围是 0~6 */
int tm_yday; /* 一年中的第几天,范围是 0~365 */
int tm_isdst; /* 指定日光节约时间是否生效,正数表示生效,0 表示不生效,负数表示该信息不可用 */
};
示例代码:
#include <stdio.h>
#include <time.h>
#define SUN 0
#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
int main(void)
{
struct tm *p;
time_t t;
time(&t); //从起使时刻到目前为止的秒数;1970 年 1 月 1 日的 UTC 时间 0 时 0 分 0 秒算起到现在所经过的秒数
p = localtime(&t);
switch (p->tm_wday)
{
case MON:
case TUE:
case WED:
case THU:
case FRI:
printf("干活!T_T\n");
break;
case SAT:
case SUN:
printf("放假!^_^\n");
break;
default:
printf("Error!\n");
}
return 0;
}
运行结果:
如果一个变量只有几种可能的值,那么就可以将其定义为枚举类型;
枚举类型的声明:
enum 枚举类型名称 {枚举值名称,枚举值名称,……}
定义枚举变量:
enum 枚举类型名称 枚举变量1,枚举变量2
示例代码:
#include <stdio.h>
#include <time.h>
int main(void)
{
enum Week {sun, mon, tue, wed, thu, fri, sat};
enum Week today;
struct tm *p;
time_t t;
time(&t);
p = localtime(&t);
today = p->tm_wday;
switch(today)
{
case mon:
case tue:
case wed:
case thu:
case fri:
printf("干活!T_T\n");
break;
case sat:
case sun:
printf("放假!^_^\n");
break;
default:
printf("Error!\n");
}
return 0;
}
示例代码:
#include <stdio.h>
int main(void)
{
enum Color { red, green, blue = 10, yellow };
enum Color rgb;
printf("red = %d\n", red);
printf("green = %d\n", green);
printf("blue = %d\n", blue);
printf("yellow = %d\n", yellow);
return 0;
}
运行结果:
36、位域 和 (逻辑)位操作 和 移位运算符
(1)位域:
位域:在定义结构体时,在结构体成员后面使用冒号(:),和数字来表示该成员所占的位数。
无名位域:位域成员可以没有名称,只要给出数据类型
示例代码:
#include <stdio.h>
int main(void)
{
struct Test
{
unsigned int a : 1;
unsigned int b : 1;
unsigned int c : 2;
};
struct Test test;
test.a = 0;
test.b = 1;
test.c = 2;
printf("a = %d, b = %d, c = %d\n", test.a, test.b, test.c);
printf("size of test = %d\n", sizeof(test));
return 0;
}
运行结果:
(2)位操作:
逻辑位运算符:优先级从高到低为: ~ 、&、^、|
运算符 | 含义 | 优先级 | 举例 | 说明 |
~ | 按位取反 | 高 | ~a | 如果a为1,~a为0; 如果a为0,~a为1。 |
& | 按位与 | 中 | a & b | 只有a和b同时为1,结果才为1; 只要a和b其中一个为0,结果为0。 |
^ | 按位异或 | 低 | a ^ b | 如果a和b不同,其结果为1; 如果a和b相同,其结果为0。 |
| | 按位或 | 最低 | a | b | 只要a和b其中一个为1,结果为1; 只有a和b同时为0,结果为0。 |
这四个运算符,除了按位取反只有一个操作数之外,其它三个都可以跟赋值号(=)结合到一块,使得代码更加简洁!
示例代码:
#include <stdio.h>
int main(void)
{
int mask = 0xFF;
int v1 = 0xABCDEF;
int v2 = 0xABCDEF;
int v3 = 0xABCDEF;
v1 &= mask;
v2 |= mask;
v3 ^= mask;
printf("v1 = 0x%X\n", v1);
printf("v2 = 0x%X\n", v2);
printf("v3 = 0x%X\n", v3);
return 0;
}
运行结果:
(3)移位运算符
将某个变量中所有的二进制位进行左移或右移的运算符;
左移运算符:11001010 << 2
右移运算符:11001010 >> 2
示例代码:
#include <stdio.h>
int main(void)
{
int value = 1;
while (value < 1024)
{
value <<= 1; // value = value << 1; 左移一位
printf("value = %d\n", value);
}
printf("\n--------分割线---------\n\n");
value = 1024;
while (value > 0)
{
value >>= 2; //右移两位
printf("value = %d\n", value);
}
return 0;
}
运行结果:
左移 n 位,乘以 2^n;右移 n 位,除以 2^n;
左移、右移运算符右边的操作数如果是为负数,或者右边的操作数大于左边操作数支持的最大宽度,那么表达式的结果均是属于“未定义行为”。
左边的操作数是有符号还是无符号数其实也对移位运算符有着不同的影响。无符号数肯定没问题,因为这时候变量里边所有的位都用于表示该数值的大小。但如果是有符号数,那就要区别对待了,因为有符号数的左边第一位是符号位,所以如果恰好这个操作数是个负数,那么移动之后是否覆盖符号位的决定权还是落到了编译器上。
掩码:
打开位:或 |
关闭位:与 &
转置位:异或 ^
37、文件操作
(1)打开、关闭文件:
只有在一个文件被打开的时候,你才能够对其进行读写操作;“读”就是从文件里获取数据,“写”则相反,是将数据写入到文件里面;在完成对一个文件的读写操作之后,你必须将其关闭。
C语言的文件主要有两种,一种是你看得懂的,一种是让你懵逼的。
你看得懂的是文本文件,由一些字符的序列组成的;另一种让你懵逼的是二进制文件,通常是指除了文本文件以外的文件。
(2)格式化、二进制读写文件
格式化读写:所需函数
从文件中读取数据: fscanf 函数文档
将格式化字符串,输入到文件中: fprintf 函数文档
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void) {
system("color 0A");
FILE *fp;
struct tm *p;
time_t t;
time(&t);
p = localtime(&t);
if ((fp = fopen("date.txt", "w")) == NULL) {
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fprintf(fp,"%d-%d-%d",1900+p->tm_year,1+p->tm_mon,p->tm_mday);
fclose(fp);
int year, month, day;
if ((fp = fopen("date.txt", "r")) == NULL) {
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fscanf(fp,"%d-%d-%d",&year,&month,&day);
printf("%d-%d-%d\n",year,month,day);
}
运行结果:
二进制读写:所需函数
fread 从文件中读出 fread 函数文档
fwrite 写入文件中 fwrite 函数文档
示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Date
{
int year;
int month;
int day;
};
struct Book
{
char name[40];
char author[40];
char publisher[40];
struct Date date;
};
int main(void)
{
FILE *fp;
struct Book *book_for_write, *book_for_read;
book_for_write = (struct Book *)malloc(sizeof(struct Book));
book_for_read = (struct Book *)malloc(sizeof(struct Book));
if (book_for_write == NULL || book_for_read == NULL)
{
printf("内存分配失败!\n");
exit(EXIT_FAILURE);
}
strcpy(book_for_write->name, "《带你学C带你飞》");
strcpy(book_for_write->author, "小甲鱼");
strcpy(book_for_write->publisher, "清华大学出版社");
book_for_write->date.year = 2017;
book_for_write->date.month = 11;
book_for_write->date.day = 11;
if ((fp = fopen("file.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fwrite(book_for_write, sizeof(struct Book), 1, fp);
fclose(fp);
if ((fp = fopen("file.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fread(book_for_read, sizeof(struct Book), 1, fp);
printf("书名:%s\n", book_for_read->name);
printf("作者:%s\n", book_for_read->author);
printf("出版社:%s\n", book_for_read->publisher);
printf("出版日期:%d-%d-%d\n", book_for_read->date.year, book_for_read->date.month, book_for_read->date.day);
fclose(fp);
return 0;
}
运行结果:
用文本方式读写的文件是文本文件,用二进制读写的文件就是二进制文件?(错误)
答:C 语言的文本方读写与二进制读写的差别仅仅体现在回车换行符这类特殊字符的处理上,从本质上来看,文本文件存放到设备上也是以二进制的形式存储的(谁叫 CPU 这个 SB 只认识 0 和 1 嘛)。
(3)随机读写
ftell 函数 :返回stream(流)/文件指针 当前的文件位置,如果发生错误返回-1.
//函数声明
long int ftell(FILE *stream)
示例代码:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
if ((fp = fopen("hello.txt", "w")) == NULL)
{
printf("文件打开失败!\n");
exit(EXIT_FAILURE);
}
printf("%ld\n", ftell(fp));
fputc('F', fp);
printf("%ld\n", ftell(fp));
fputs("ishC\n", fp);
printf("%ld\n", ftell(fp));
//rewind(fp);
//fputs("Hello", fp);
fclose(fp);
return 0;
}
运行结果:
函数rewind() 把文件指针移到由stream(流)/文件指针 指定的开始处, 同时清除和流相关的错误和EOF标记.
示例代码:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE *fp;
if ((fp = fopen("hello.txt", "w")) == NULL)
{
printf("文件打开失败!\n");
exit(EXIT_FAILURE);
}
printf("%ld\n", ftell(fp));
fputc('F', fp);
printf("%ld\n", ftell(fp));
fputs("ishC\n", fp);
printf("%ld\n", ftell(fp));
rewind(fp); //将文件指针,移动到文件开始处,
fputs("Hello", fp);
fclose(fp);
return 0;
}
运行结果:
fseek() 函数 用于设置文件流/文件指针的位置
示例代码:
#include <stdio.h>
#include <stdlib.h>
#define N 4
struct Stu
{
char name[24];
int num;
float score;
}stu[N], sb;
int main(void)
{
FILE *fp;
int i;
if ((fp = fopen("sorce.txt", "wb")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
printf("请开始录入成绩(格式:姓名 学号 成绩)");
for (i = 0; i < N; i++)
{
scanf("%s %d %f", stu[i].name, &stu[i].num, &stu[i].score);
}
fwrite(stu, sizeof(struct Stu), N, fp);
fclose(fp);
if ((fp = fopen("sorce.txt", "rb")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fseek(fp, sizeof(struct Stu), SEEK_SET);
fread(&sb, sizeof(struct Stu), 1, fp);
printf("%s(%d)的成绩是:%.2f\n", sb.name, sb.num, sb.score); //从文件头,偏移一个结构体的位置
fclose(fp);
return 0;
}
运行结果:
38、I/O 缓冲区
exit 函数文档
exit 函数通过其参数传递关闭信息给操作系统,通常的约定的做法是:
正常终止的程序传递 0(就像 main 函数最后 return 0 一样);非正常终止的程序传递一个非 0 值。
不同的退出值可以用来标识导致程序退出的不同原因。但并非所有的操作系统都有共同的一份约定的退出值名单,所以 ANSI C 标准规定使用一个有限的最小范围来表示。该标准要求使用 0 或宏 EXIT_SUCCESS 来表示程序正常终止;而使用宏 EXIT_FAILURE 表示程序非正常终止。如果你的程序希望获得最大的可移植性,建议使用宏 EXIT_SUCCESS 和 EXIT_FAILURE 作为参数。