前言:
- 每个C应用程序只有一个,且必须有一个main()主函数。无论主函数写在什么位置,C程序总是从main()函数开始执行。main()函数可以调用其它的子函数,子函数之间可以相互调用任意多次。
 - 库函数:使用库函数,必须包含 
#include对应的头文件。 
函数定义的格式:

- 返回值类型:
无返回值的类型:针对函数无返回值或明确不需返回值的情况,使用void(即空类型)表示。有返回值的类型:需要指明具体的类型。比如, int、float、char 等。如果省略,默认为int类型。- 对于有返回值类型的,则需要在函数体内写上
return 返回值,搭配使用,返回值需要与返回值类型一致。 
 - 函数名:
- 函数名,属于标识符。要遵循标识符的命名规则,同时要
见名知意,以增强程序的可读性。 
 - 函数名,属于标识符。要遵循标识符的命名规则,同时要
 - 参数列表:
- 函数名后面的圆括号里面,可以声明参数的类型和参数名,表示完成函数体功能时需要外部提供的数据列表。
 - 根据是否有参数,函数可以分为:
 无参函数:在调用无参函数时,主调函数不向被调用函数传递数据。但函数名后的()不能省略。- 举例:abort();不接受任何形参。
 
有参函数:在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据。- 函数参数为多个参数时,其间用逗号隔开。
 - 举例:add(int m,int n)
 
 - 函数体:
- 函数体:是要写在大括号{}里面,是函数被调用后要执行的代码。
 
 - return语句:
- return语句的作用:① 结束函数的执行 ②将函数运算的结果返回。
 - return语句后面就不能再写其它代码了,否则会报错。(与break、continue情况类似)
 - 下面分两种情况讨论:
- 情况1:返回值类型不是void时,函数体中必须保证一定有 
return 返回值;语句,并且要求该返回值结果的类型与声明的返回值类型一致或兼容。 - 情况2:返回值类型是void时,函数体中可以没有return语句。如果要用return语句提前结束函数的执行,那么return后面不能跟返回值,直接写
return;就可以。 
 - 情况1:返回值类型不是void时,函数体中必须保证一定有 
 
 
函数定义注意事项:
- C程序中的所有函数都是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。
 - 同一个程序中函数不能重名,
函数名是用来标识唯一一个函数。即在标准的 C 语言中,并不支持函数的重载。 
形参、实参:
形参(formal parameter):在定义函数时,函数名后面括号()中声明的变量称为形式参数,简称形参。实参(actual parameter):在调用函数时,函数名后面括号()中使用的值/变量/表达式称为实际参数,简称实参。举例:
1
2
3
4
5
6
7
8
9
10
11
12
13int add(int x, int y) { //x,y是add()中的形参
int z;
z = x + y;
return (z);
}
int main() {
int c;
int a = 10,b = 20;
c = add(a, b); //此时将实参a,b赋值给add()的形参
printf("sum is %d\n", c);
return 0;
}
说明:
1、实参与形参的类型应相同或赋值兼容,个数相等、一一对应。
2、形参只是一个形式,在调用之前并不分配内存。函数调用时,系统为形参分配内存单元,然后将主调函数中的实参传递给被调函数的形参。被调函数执行完毕,通过return语句返回结果,系统将形参的内存单元释放。
函数的调用:
调用函数时,需要传入实际的参数值。如果没有参数,只要在函数名后面加上圆括号就可以了。
无参函数调用:函数名();
有参函数调用:函数名(参数列表);
说明:
- 1、调用时,参数个数必须与函数声明里的参数个数一致,参数过多或过少都会报错。
 - 2、函数间可以相互调用,但不能调用main函数,因为main函数是被操作系统调用的,作为程序的启动入口。反之,main() 函数可以调用其它函数。
 - 3、函数的参数和返回值类型,会根据需要进行自动类型转换。
 
main()函数:
- C 语言规定, main() 是程序的入口函数,即所有的程序一定要包含一个 main() 函数。程序总是从这个函数开始执行,如果没有该函数,程序就无法启动。
 - main()函数可以调用其它函数,但其它函数不能反过来调用main()函数。main()函数也不能调用自己。
 - 正常情况下,如果 main() 函数里面省略 return 0 这一行,编译器会自动加上,即 main() 的默认返回值为0。
 - 在main()函数中C 语言约定:返回值 0 表示函数运行成功;返回其它非零整数值,表示运行失败,代码出了问题。系统根据 main() 的返回值,作为整个程序的返回值,确定程序是否运行成功。
 
exit()函数:
exit() 函数用来终止**
整个程序的运行**。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件stdlib.h里面。exit() 可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数,这两个常量也是定义在 stdlib.h 里面:
- EXIT_SUCCESS (相当于 0)表示程序运行成功,正常结束;
 - EXIT_FAILURE (相当于 1)表示程序异常中止。
 
在main()函数结束时也会隐式地调用exit()函数,exit() 等价于使用 return 语句。其它函数使用 exit() ,就是终止整个程序的运行,没有其它作用。
C 语言还提供了一个 atexit() 函数,用来登记 exit() 执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件
stdlib.h。
函数的声明:
函数必须先声明,后使用。由于程序总是先运行 main() 函数,所以其它函数都必须在main()之前声明。
主调函数和被调函数在同一个
.c文件当中时:被调函数在上,主调函数在下(不需要声明)
1
2
3
4
5
6void fun(){
printf("123");
}
int main(){
fun();//不需要声明
}被调函数在下,主调函数在上(需要声明)
1
2
3
4
5
6int main(){
fun();//需要声明
}
void fun(){
printf("123");
}**直接声明法:**将被调用的函数的第一行拷贝过去加分号
1
2
3
4
5
6
7void fun();//声明fun()函数
int main(){
fun();
}
void fun(){
printf("123");
}**间接声明法:**将函数的声明放在头文件中(文件命名为.h),.c 程序包含头文件即可
1
2
3
4
5
6
7
8
9
10//这是头文件a.h编写的代码,也就是在头文件里面声明函数
extern void fun();
int main(){
fun();
}
void fun(){
printf("123");
}
主调函数和被调函数不在同一个
.c文件当中时:(必须声明)**直接声明法:**将被调用的函数的第一行拷贝过去,后面加分号,前面加
extern主调函数:假设main.c文件
1
2
3
4
5
6extern int Max(int x,int y);
int main(){
int num=Max(3,7);
printf("%d",num);
return 0;
}被调函数:假设max.c文件
1
2
3
4
5
6int Max(int x,int y){
if(x>y)
return x;
else
return y;
}
**间接声明法:**将函数的声明放在头文件中(文件命名为.h),.c 程序包含头文件即可
头文件:han.h
1
extern int Max(int x,int y);
主调函数:han.c
1
2
3
4
5
6
int main(){
int num=Max(3,7);
printf("%d",num);
return 0;
}**注意:**如在Max.c中也用到了其他函数,则主调函数han.c也需要声明相应的函数(或包含相应的头文件),方便的办法是把用到的函数全声明在自己的头文件当中,只样在用的时候只需要包含自己的头文件即可
参数的传递:
值传递:
- 概念:是将实参的值传递给被调用的形参
 - 值传递,是
单向传递,修改形参的值,不会影响到实参 - **默认传递值的类型:**基本数据类型 (整型类型、浮点类型,字符类型)、结构体、共用体、枚举类型。
 - 体会:
形参、实参各占独立的存储空间。函数在被调用时,给形参动态分配临时存储空间,函数返回释放。 
地址传递:
概念:实参将地址传递给形参,二者地址值相同
地址传递,是
双向传递,修改形参则会影响到实参**默认传递地址的类型:**指针、数组
指针作为形参:
- 当函数的形参类型是指针类型时,使用该函数时,需要传递指针,或者地址,或者数组给该形参。函数内以指针的方式操作变量(*指针)。
 - 因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部变量的值。
 
数组作为形参:
- 数组名本身就代表该数组首地址,传数组的本质就是传地址。因此,把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组。
 
说明:
void f(int x[], int n){
 …………;
}
**参数1:**数组的定义,只需写出中括号即可,
不需要限定数组长度。**参数2:**数组作为参数的习惯操作。将函数中要操作的数组元素的长度传入(并不是指数组的总长度)。
由于数组名就是一个指针,如果只传数组名,那么函数只知道数组开始的地址,不知道结束的地址,所以才需要把数组长度也一起传入。
- 对于二维数组做形参:
 
int maxValue(int array[][4], int n)
那么除了第一维的长度可以当作参数传入函数,其他维的长度需要写入函数的定义。也就是说,在定义二维数组时,必须指定列数(即一行中包含几个元素) ,由于形参数组与实参数组类型相同,所以它们是由具有相同长度的一维数组所组成的。所以必须指定第2维(列数)。
**注意:**在传递时列数必须一致,而行数可以不一样
1
2
3
4void f(int x[5][5]){}
//1、int a[3][5]; 可以传给f函数吗? 可以!
//2、int b[10][3];可以传给f函数吗? 不可以!
而行数要保证实参不能比形参的大,不然会丢失数据指针和数组作形参小结:

字符串(字符指针)作为形参:
- 字符串(或字符指针)作为函数的参数,与数组或指针作为函数参数没有本质的区别,传递的都是地址值,所不同的仅是指针指向对象的类型不同而已。
 
指针数组作为形参:
- 指针数组的元素是指针变量,用指针数组能够实现一组字符串的处理。
 
函数的高级应用:
递归函数:
- 递归函数 调用:函数自己调用自己的现象就称为递归。
 - 直接递归:函数自身调用自己。
 - 间接递归:可以理解为A()函数调用B()函数,B()函数调用C()函数,C()函数调用A()函数。
 - 解释说明:
- 递归函数包含了一种
隐式的循环。 - 递归函数会
重复执行某段代码,但这种重复执行无须循环控制。 - 递归一定要向
已知方向递归,否则这种递归就变成了无穷递归,停不下来,类似于死循环。最终发生栈内存溢出。 - C语言支持函数的递归调用。
 
 - 递归函数包含了一种
 - 总结:
- 使用递归函数大大简化了算法的编写。
 - 递归调用会占用大量的系统堆栈,内存耗用多,在递归调用层次多时速度要比循环
慢的多,所以在使用递归时要慎重。 - 在要求高性能的情况下尽量避免使用递归,递归调用既
花时间又耗内存。考虑使用循环迭代 
 
可变函数:
有些函数的参数数量是不确定的,此时可以使用C语言提供的
可变参数函数(Variadic Functions)。声明可变参数函数的时候,使用省略号 … 表示可变数量的参数。
1
int printf(int form, ...);
这里的 … 表示可以传递任意数量的参数,但是它们都需要与
form的格式化标志相匹配。注意:… 符号必须放在
参数序列的结尾,否则会报错可变参数的使用:
为了使用可变参数,你需要引入
<stdarg.h>头文件。在函数中,需要声明一个
va_list类型的变量来存储可变参数。它必须在操作可变参数时,首先使用。使用
va_start函数来初始化va_list类型的变量。它接受两个参数,参数1是可变参数对象,参数2是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。使用
va_arg函数来逐个获取可变参数的值。每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,参数1是可变参数对象,参数2是当前可变参数的类型。使用
va_end函数来结束可变参数的处理。举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 可变参数函数,计算多个整数的平均值
//count是表示需要输入几个,如count=4,也就是他后面有4个数
double average(int count, ...) {
va_list args; // 声明一个va_list变量,存储可变参数
va_start(args, count); // 初始化va_list,指向可变参数的位置
double sum = 0;
for (int i = 0; i < count; i++) {
int num = va_arg(args, int); // 逐个获取整数参数
sum += num;
}
va_end(args); // 结束可变参数的处理
return sum / count;
}
int main() {
// 调用可变参数函数输入5,表示后面跟5个数
double avg = average(5, 10, 20, 30, 40, 50);
printf("Average: %lf\n", avg);
return 0;
}总结:
- 可变参数函数,在编写各种工具函数和格式化输出函数时非常有用。但要小心确保传递的参数数量和类型与函数的预期相匹配,以避免运行时错误。
 
指针函数:(返回值是指针)
C语言允许函数的返回值是一个指针(地址),这样的函数称为
指针函数。指针函数是指 返回值是指针的函数。它的本质是一个函数,只不过返回值是一个指针。定义格式:
1
2
3返回值类型 *函数名(形参列表) {
函数体
}- 函数体中的 return 命令须返回一个地址。
 
特点:
- 返回值是一个指针。
 - 通常用于动态内存分配、返回数组或字符串等场景。
 
函数指针:(指向函数的指针)
函数指针是指向函数的指针。它的本质是一个指针,只不过这个指针指向的是函数。C 语言规定,函数名本身就是指向函数代码的指针,通过函数名就能获取函数地址。也就是说, print 函数和 &print 是一回事。
定义形式:
- 返回类型 (*指针变量名)(参数列表);
 - 如:int (*max)(参数列表);
 - 其中,参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称。
 - int (* max)(int i);或int (* max)(int);
 
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 普通函数
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
// 定义函数指针
int (*funcPtr)(int, int);
// 让函数指针指向 add 函数
funcPtr = &add;//funcPtr = add;是一样的
printf("Add: %d\n", funcPtr(10, 5)); // 输出:15
// 让函数指针指向 subtract 函数
funcPtr = subtract;
printf("Subtract: %d\n", funcPtr(10, 5)); // 输出:5return 0;
}- 变量 funcPtr 是一个函数指针,它可以指向函数返回值类型为 int 且有2个整型参数的函数
 - funcPtr 指向函数 add() 的地址。函数 add() 的地址可以用 & 获得,也可以用add函数名获得
 - (*funcPtr)的小括号一定不能省略
 - 有了函数指针,通过它也可以调用函数:
 - (*funcPtr)(10,5)等同于add(10,5)以及funcPtr(10,5)是一样的
 
特点:
- 函数指针是一个变量,存储的是函数的地址。
 - 可以通过函数指针调用不同的函数,常用于回调函数、函数表等场景。
 
注意:
- 对指向函数的指针变量不能进行算术运算,如p+n,p++,p–等运算是无意义的。
 - 用函数名调用函数,只能调用所指定的一个函数,而通过指针变量调用函数比较灵活,可以根据不同情况先后调用不同的函数。
 
回调函数:
指向函数a的指针变量(函数指针)的一个重要用途是把函数a的入口地址作为参数传递到其它函数b中,此时的函数b就称为
回调函数。在此基础上,我们就可以在回调函数b中使用实参函数a。原理解释:有一个函数(假设函数名为fun),它有两个形参(x1和x2),定义x1和x2为指向函数的指针变量。在调用函数fun时,实参为两个函数名f1和f2,给形参传递的是函数f1和f2的入口地址。这样在函数fun中就可以调用f1和f2函数了。

**例子:**有两个整数a和b,由用户输入1,2或3。如输入1,程序就给出a和b中的大者,输入2,就给出a和b中的小者,输入3,则求a与b之和。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
int fun(int x, int y, int (*p)(int, int)); //fun函数声明
int max(int, int); //max函数声明
int min(int, int); //min函数声明
int add(int, int); //add函数声明
int main() {
int a = 10, b = 20, n;
printf("please choose 1,2 or 3:");
scanf("%d", &n); //输入1,2或3之一
switch(n){
case 1:
fun(a, b, max); //输入1时调用max函数
break;
case 2:
fun(a, b, min); //输入2时调用min函数
break;
case 3:
fun(a, b, add); //输入3时调用add函数
break;
}
return 0;
}
int fun(int x, int y, int (*p)(int, int)){ //定义fun函数
int result;
result = (*p)(x, y);
printf("%d\n", result); //输出结果
}
int max(int x, int y){ //定义max函数
int z;
if (x > y)
z = x;
else
z = y;
printf("max=");
return z; //返回值是两数中的大者
}
int min(int x, int y){ //定义min函数
int z;
if(x < y)
z = x;
else
z = y;
printf("min=");
return z; //返回值是两数中的小者
}
int add(int x, int y){ //定义add函数
int z;
z = x + y;
printf("sum=");
return z; //返回值是两数之和
}
函数说明符:
内部函数:(静态函数)static关键字修饰
如果在一个源文件中定义的函数只能被
本文件中的函数调用,而不能被同一源程序其他文件中的函数调用,这种函数称为内部函数。此时,内部函数需要使用 static 修饰。定义形式:static 返回值类型 函数名(形参){}
举例:
1
2
3static int f(int a,int b){
…
}说明:f()函数只能被本文件中的函数调用,在其他文件中不能调用此函数。- 但此处 static 的含义并
不是指存储方式,而是指对函数的调用范围只局限于本文件。因此在不同的源文件中定义同名的内部函数互不影响。 
外部函数:
外部函数在整个源程序中都有效,只要定义函数时,在前面加上
extern关键字即可。C语言规定,可以省略extern,默认函数前面加上 extern 关键字
定义形式:extern 返回值类型 函数名(形参表){}
举例:
1
2
3extern int f(int a,int b){
…
}- 如果定义为外部函数,则它不仅可被定义它的源文件调用,而且可以被其他文件中的函数调用,即其作用范围不只局限于其源文件,而是整个程序的所有文件。
 - 在一个源文件的函数中调用其他源文件中定义的外部函数时,通常使用extern说明被调函数为外部函数。
 
字符串常用函数:
这些函数都在
#include <string.h>头文件当中**strlen()**函数:
作用:返回字符串的长度,不包括末尾的空字符 ‘\0’ 。
1
2char str[10] = "China";
printf("%d\n", strlen(str));//5
**sizeof:**这是个关键字运算符,并不是函数
作用:计算数据类型或变量所占用的内存大小(以字节为单位)包含“\0”
1
2
3
4int a;
sizeof(a);//4
char a[10]="huan";
sizeof(a);//输出10,返回的是整个数组的大小
**strcpy()**函数:
格式:strcpy(字符数组1, 字符数组2)
作用:字符串的复制,直接将字符数组2的字符串复制到字符数组1中。
1
2
3
4
5
6char str1[10]="111111234", str2[] = "China";
strcpy(str1, str2);
printf("%s\n",str1); //China 遇到\0停止输出
for(int i = 0;i < 10;i++){
printf("%c",str1[i]); //China\0234\0
}**注意:**复制时将字符串China和其后的′\0′一起复制到字符数组str1中,取代字符数组str1中前面的字符,未被取代的字符保持原有内容。
**strncpy()**函数:
- 格式:strncpy(字符串1, 字符串2, n);
 - 作用:将字符串2中前面n个字符复制到字符数组1中去。
 
**strcat()**函数:
格式:strcat(字符数组1, 字符数组2)
作用:把两个字符数组中的字符串连接起来,把字符串2接到字符串1的后面,结果放在字符数组1中,函数调用后得到一个函数值——字符数组1的地址。
注意:连接前两个字符串的后面都有′\0′,连接时将字符串1后面的′\0′取消,只在新字符串最后保留′\0′。
1
2
3
4char str1[30] = {"People′s of "};
char str2[] = {"China"};
printf("%s\n", strcat(str1, str2)); //People′s of China
printf("%s\n", str1); //People′s of China
**strncat()**函数:
- 格式:strncat(str1, str2, n);
 - 作用:将字符串2中前面n个字符连接到字符数组1中去
 
**strcmp()**函数:
格式:strcmp(字符串1, 字符串2)
作用:比较字符串1和字符串2。
**字符串比较的规则是:**将两个字符串自左至右逐个字符相比(按ASCII码值大小比较),直到出现不同的字符或遇到′\0′为止。
(1) 如全部字符相同,则认为两个字符串相等。返回值为0
(2) 若出现不相同的字符,如果返回值为正数,则字符串1大;反之,返回值为负数,则字符串2大。
1
2
3
4
5
6char *str1 = "abxy";
char *str2 = "abmn";
printf("%d\n",strcmp(str1, str2)); // 1
int compare1 = strcmp("China", "Korea");
printf("%d\n",compare1); // -1
strlwr()/strupr():
strlwr(字符串):将字符串中大写字母换成小写字母。
strupr(字符串):将字符串中小写字母换成大写字母。
1
2
3
4
5
6char str[] = "HelloWorld";
strlwr(str);
puts(str); //helloworld
strupr(str);
puts(str); //HELLOWORLD
基本数据类型和字符串的转换函数:
基本数据类型 -> 字符串:
**sprintf()**函数可以将其他数据类型转换成字符串类型。此函数声明在
stdio.h头文件中。sprintf()和平时我们常用的printf()函数的功能相似,只是sprintf()函数输出到字符串中,而printf()函数输出到屏幕上。
1
2
3
4
5char str[20];
int a = 111;
char c = 'a';
sprintf(str, "%d%c", a, c);//把a c 加入到字符数组str中
printf("str=%s\n", str); //111a
字符串 -> 基本数据类型:
调用头文件
<stdlib.h>的函数atoi()或atof()即可。1
2
3
4
5
char str1[10] = "123456";
char str2[4] = "111";
int i = atoi(str1);//123456
int j = atof(str1);//111
日期和时间相关函数:
- 头文件是  
<time.h> - 举例说明:
 
- 返回一个值,即格林尼治时间1970年1月1日00:00:00到当前时刻的时长,时长单位是秒。
 
1  | time_t time(time_t *t)  | 
- 获取当前时间,返回一个表示当地时间的字符串(当地时间是基于参数timer的)。
 
1  | char *ctime(const time_t *timer)  | 
- 计算time1和time2之间相差的秒数(time1-time2)
 
1  | double difftime(time_t time1, time_t time2)  | 
例子举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 运行test函数,看看执行花费时间
void test() {
int i = 0;
int sum = 0;
int j = 0;
for (i = 0; i < 10000000; i++) {
sum = 0;
for (j = 0; j < 100; j++) {
sum += j;
}
}
}
int main() {
printf("程序启动...\n");
time_t start_t;
//先得到执行test前的时间
time(&start_t); //获取当前时间
test(); //执行test
time_t end_t;
//再得到执行test后的时间
time(&end_t); //获取当前时间
double diff_t; //存放时间差
diff_t = difftime(end_t, start_t); //时间差,按秒 ent_t - start_t
//然后得到两个时间差就是耗用的时间
printf("%d\n",start_t); //1697026306
printf("%d\n",end_t); //1697026308
printf("执行test()函数 耗用了%.2f 秒\n", diff_t); //执行test()函数 耗用了2.00 秒
//获取时间对应的字符串的表示
char * startTimeStr = ctime(&start_t);
printf("%s\n",startTimeStr); //Wed Oct 11 20:11:48 2023
return 0;
}
数学运算相关函数:
math.h头文件定义了各种数学函数。在这个库中所有可用的功能都带有一个 double 类型的参数,且都返回 double 类型的结果。
double exp(double x) :返回 e 的 x 次幂的值。
double log(double x) :返回 x 的自然对数(基数为 e 的对数)
double pow(double x, double y) :返回 x 的 y 次幂。
double sqrt(double x) :返回 x 的平方根。
double fabs(double x) :返回 x 的绝对值。
1
2
3
4
5
6
7
8
9
10
11
12
int main() {
double d1 = pow(2.0, 3.0);
double d2 = sqrt(5.0);
printf("d1=%.2f\n", d1); //d1=8.00
printf("d2=%f\n", d2); //d2=2.236068
return 0;
}
内存管理相关函数:
C程序的内存分配:
- 内存从大到小排序:栈–堆–静态区–常量区–代码区
 - 非静态的局部变量——内存中的动态存储区:stack 栈
 - 临时使用的数据——建立动态内存分配区域,需要时随时开辟,不需要时及时释放——heap 堆
 - 全局变量和静态局部变量——内存中的静态存储区/全局区
 - const修饰常量、字符串常量——常量区
 - 自己编写的代码、指令等——代码区
 - 根据需要
向系统申请所需大小的空间,由于未在声明部分定义其为变量或者数组,不能通过变量名或者数组名来引用这些数据,只能通过指针来引用) 
void指针(无类型指针)
C 语言提供了一种不定类型的指针,叫做 void 指针。它只有内存块的
地址信息,没有类型信息,等到使用该块内存的时候,再向编译器补充说明,里面的数据类型是什么。void 指针可以
指向任意类型的数据,但是不能解读数据。void 指针与其他所有类型指针之间是
互相转换关系,任一类型的指针都可以转为 void 指针,而 void 指针也可以转为任一类型的指针。1
2
3
4
5
6int x=10;
int *p=&x;
void *q=&x;
q=p;//无类型的指针变量可以与其他类型的指针变量相互转换
int y=*q;//无法解读出void类型指针指向的数据
int z=*((int *)q);//可以先转换为int *类型的,再取值由于不知道 void 指针指向什么类型的值,所以不能用
*运算符取出它指向的值。
内存动态分配函数:
头文件
<stdlib.h>声明了四个关于内存动态分配的函数。所谓动态分配内存,就是按需分配,申请才能获得。掌握:malloc()
函数原型:void *malloc(unsigned int size); //size的类型为无符号整型
作用:在内存的
动态存储区(堆区)中分配一个长度为size的连续空间。并将该空间的首地址作为函数值返回,即此函数是一个指针函数。由于返回的指针的基类型为 void,应通过显式类型转换后才能存入其他基类型的指针变量,否则会有警告。如果分配不成功,返回空指针(NULL)。
举例1:
1
2int *p;
p=(int *)malloc(sizeof(int));//因为返回的是void *,所以转换为int *类型举例2:动态申请数组空间
1
2
3
4int *p;
p = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++)
p[i] = i * 5;得到一个元素类型为int型,长度为n的数组。取元素方式与之前相同,如获取第2个元素:p[1]。
举例3:动态申请结构体空间
1
2struct node *p;
p = (struct node *) malloc(sizeof(struct node)); //(struct node*)为强制类型转换关于返回值为NULL:
- malloc() 分配内存有可能分配失败,这时返回常量 NULL。Null 的值为0,是一个无法读写的内存地址,可以理解成一个不指向任何地方的指针。
 - 由于存在分配失败的可能,所以最好在使用 malloc() 之后检查一下,是否分配成功。
 
掌握:**free()与malloc()**联用
函数原型:**void free(void *p);**函数无返回值
作用:释放指针变量p所指向的内存空间,使这部分内存能重新被其它变量使用。否则这个内存块会一直占用到程序运行结束。
1
2
3
4
5
6int *p;
p=(int *)malloc(sizeof(int));
//...各种操作...
free(p); //千万不要忘了使用free()释放内存!注意:
1、指针 p 必须是经过动态分配函数 malloc 成功后返回的首地址。
2、分配的内存块一旦释放,就不应该再次操作已经释放的地址,也不应该再次使用 free() 对该地址释放第二次。
3、如果忘记调用free()函数,同时p所在的函数调用结束后p指针已经消失了,导致无法访问未回收的内存块,构成内存泄漏。
了解:calloc()
函数原型:void *calloc(unsigned int n,unsigned int size);
作用:在内存的**动态存储区(堆区)*中分配n个,单位长度为size的连续空间,这个空间一般比较大,总共占用nsize 个字节。其内容初始化为0,并将该空间的首地址作为函数的返回值。如果函数没有成功执行,返回NULL。
calloc()函数适合为
一维数组开辟动态存储空间,n为数组元素个数,每个元素长度为size。1
2
3
4
5
6
7int *p;
p = (int *)calloc(10,sizeof(int)); //开辟空间的同时,其内容初始化为零
//等同于
int* p;
p = (int *)malloc(10 * sizeof(int));//未初始化
memset(p, 0, sizeof(int) * 10);//使用memset函数初始化上面示例中, calloc() 相当于 malloc() + memset() 。
了解:realloc()
函数原型:void realloc(void p, unsigned int size)
作用:重新分配malloc()或calloc()函数获得的动态空间大小,即调整大小的内存空间。将先前开辟的内存块的指针p指向的动态空间大小改变为size,单位字节。返回值是一个全新的地址(数据也会自动复制过去),也可能返回跟原来一样的地址。分配失败返回NULL。
realloc() 优先在原有内存块上进行缩减,尽量不移动数据,所以通常是返回原先的地址。
如果新内存块小于原来的大小,则丢弃超出的部分;如果大于原来的大小,则不对新增的部分进行初始化(程序员可以自动调用 memset() )。
1
2
3int* b;
b = (int *)malloc(sizeof(int) * 10);
b = (int *)realloc(b, sizeof(int) * 2000);指针 b 原来指向10个成员的整数数组,使用 realloc() 调整为2000个成员的数组。
动态分配内存的基本原则:
- 避免分配大量的小内存块:分配堆上的内存有一些系统开销,所以分配许多小的内存块比分配几个大内存块的系统开销大
 - 仅在需要时分配内存:只要使用完堆上的内存块,就需要及时释放它,否则可能出现内存泄漏。这里需要遵守原则:谁分配,谁释放。
 - 总是确保释放以分配的内存:在编写分配内存的代码时,就要确定在代码的什么地方释放内存。
 
常见的内存错误及其对策:
内存分配未成功,却使用了它:
- 解决办法是,在使用内存之前检查指针是否为NULL。比如,如果指针p是函数的参数,那么在函数的入口处应该用
if(p==NULL)或if(p!=NULL)进行防错处理。 
- 解决办法是,在使用内存之前检查指针是否为NULL。比如,如果指针p是函数的参数,那么在函数的入口处应该用
 内存分配虽然成功,但是尚未初始化就引用它:
1
2
3
4
5int * p = NULL;
p = (int*)malloc(sizeof(int));
if (p == NULL){/*...*/}
/*初始化为0*/
memset(p, 0, sizeof(int));题外话,无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
内存分配成功并且已经初始化,但操作时提示内存越界:
- 在使用数组时经常发生下标“+1”或者“-1”的操作,特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
 
忘记了释放内存,造成内存泄漏:
- 动态内存的申请与释放必须配对,程序中
malloc()与free()的使用次数一定要相同,否则肯定有错误。 
- 动态内存的申请与释放必须配对,程序中
 未正确的释放内存,造成内存泄漏:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void getMemory(int *p) {
p = (int *) malloc(sizeof(int)); // 在这里修改的是局部指针 p,不会影响 main 函数中的原始指针 ptr
//....
}
int main() {
int *ptr = NULL;
getMemory(ptr); // 将 ptr 的值传递给 getMemory,但是在函数内部修改的是 p,而不是 ptr
printf("ptr = %d\n", *ptr); // 这里的 *ptr 是未定义行为,因为 ptr 没有指向有效的内存
free(ptr); // 这里试图释放未分配的内存,会导致问题
}在本例中,getMemory()中的p申请了新的内存,只是把 p所指的内存地址改变了,但是ptr丝毫未变。getMemory()中的p也始终没有进行内存的释放。事实上,因为没有用free释放内存,每执行一次getMemory()就会泄漏一块内存。
释放了内存却继续使用它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18long *p;
void addr() {
long k;
k = 0;
p = &k;
}
void port() {
long i, j;
j = 0;
for (i = 0; i < 10; i++) {
(*p)--;
j++;
}
}
int main() {
addr();
port();
}由于addr函数中的变量k在函数返回后就已经不存在了,但是在全局变量p中却保存了它的地址。在下一个函数port中,试图通过全局指针p访问一个不存在的变量,进而出错。