概述
函数分类
C 程序是由函数组成的, 我们写的代码都是由主函数 main() 开始执行的. 函数是 C 程序的基本模块, 是用于完成特定任务的程序代码单元.
从函数定义的角度看, 函数可分为系统函数和用户自定义函数两种:
- 系统函数, 即库函数: 标准 C库, libc, 这是由编译系统提供的, 用户不必自己定义这些函数, 可以直接使用它们, 如我们常用的打印函数 printf().
- 1. 引入头文件 -- 声明函数
- 2. 根据函数原型调用
- 用户定义函数: 用以解决用户的专门需要.
函数的作用
封装
提高代码的复用率
提高程序模块化组织性
函数的定义
包含
- 函数原型 (返回值类型、函数名、形参列表) 和
- 函数体 (大括号一对, 具体代码实现)
int add (int a, int b, int c) { return a + b + c; }
函数的调用
包含 函数名(实参列表);
int ret = add(10, 4, 28);
函数在调用时, 传参必须严格按照形参填充. (参数的个数、类型、顺序必须一样)
函数的声明
要求在函数调用之前, 编译必须见过函数定义. 否则, 需要函数声明.
函数声明包含 函数原型 (返回值类型、函数名、形参列表) + ‘;’
~ cat test.c #include <stdio.h> int add(int a, int b) { return a + b; } int main(void) { printf("%d\n", add(3, 5)); return 0; }
~ gcc test.c -o test && ./test 8
~ cat test.c #include <stdio.h> // 函数声明 int add(int, int); int main(void) { printf("%d\n", add(3, 5)); return 0; } int add(int a, int b) { return a + b; }
~ gcc test.c -o test && ./test 8
~ cat test.c #include <stdio.h> int main(void) { printf("%d\n", add(3, 5)); return 0; } int add(int a, int b) { return a + b; }
~ gcc test.c -o test && ./test test.c:5:20: error: implicit declaration of function 'add' is invalid in C99 [-Werror,-Wimplicit-function-declaration] printf("%d\n", add(3, 5)); ^ 1 error generated.
main 函数与 exit 函数
return: 返回当前函数调用
exit() 函数: 退出当前程序
指针和函数
栈帧
p先有栈, 后有栈帧
当函数调用时, 系统会在 stack 空间上申请一块内存区域, 用来供函数调用, 主要存储形参和局部变量.
当函数调用结束, 这块在 stack 空间中申请的内存区域会自动被释放.
传值和传址
传值: 函数调用期间, 实参将自己的值, 拷贝一份给形参.
传址: 函数调用期间, 实参将自己的地址, 拷贝一份给形参.
传值
~ cat test.c
#include <stdio.h>
int swap(int a, int b);
int main()
{
int m = 23;
int n = 57;
printf("--before-- m = %d, n = %d\n", m, n);
// 函数调用
swap(m, n);
printf("--after-- m = %d, n = %d\n", m, n);
return 0;
}
// 函数定义
int swap(int a, int b)
{
int tmp = 0;
tmp = a;
a = b;
b = tmp;
return 0;
}
~ gcc test.c -o test && ./test --before-- m = 23, n = 57 --after-- m = 23, n = 57
程序从上往下执行:
- main 函数是入口函数, 执行 main 函数时, 会在栈空间中为 main 函数创建一个栈帧, 随着代码的执行, 局部变量 m 和 n 会被存入到 main 函数的栈桢中.
- 当执行到 swap 函数时, 会紧随 main 函数栈帧之后创建 swap 函数的栈帧, 首先会将形参 a 和 b 存储在 swap 函数的栈帧中, 然后通过值拷贝的方式, 将实参 m 和 n 的值分别拷贝给形参 a 和 b. 随着 swap 函数的顺序执行, 将局部变量 tmp 也存储到 swap 函数的栈帧中. 然后根据函数逻辑修改 tmp、a、b 的值, 最后返回 0 退出 swap 函数, swap 函数退出后, swap 函数的栈帧空间也随之被释放.
- 程序又回到 main 函数中继续向下执行.
由于在 swap 函数中, m 和 n 的值没有被修改, 所以 m 和 n 的值在 swap 函数执行前后没有发生改变.
传址
~ cat test.c
#include <stdio.h>
int swap(int *, int *);
int main()
{
int m = 23;
int n = 57;
printf("--before-- m = %d, n = %d\n", m, n);
// 函数调用
swap(&m, &n);
printf("--after-- m = %d, n = %d\n", m, n);
return 0;
}
// 函数定义
int swap(int *a, int *b)
{
int tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
return 0;
}
~ gcc test.c -o test && ./test
--before-- m = 23, n = 57
--after-- m = 57, n = 23
程序从上往下执行:
- main 函数是入口函数, 执行 main 函数时, 会在栈空间中为 main 函数创建一个栈帧, 随着代码的执行, 局部变量 m 和 n 会被存入到 main 函数的栈桢中.
- 当执行到 swap 函数时, 会紧随 main 函数栈帧之后创建 swap 函数的栈帧, 首先会将形参 a 和 b 存储在 swap 函数的栈帧中, 然后通过地址拷贝的方式, 将实参 m 和 n 的地址分别拷贝给形参 a 和 b. 随着 swap 函数的顺序执行, 将局部变量 tmp 也存储到 swap 函数的栈帧中. 然后根据函数逻辑修改 tmp、a、b 的值, 最后返回 0 退出 swap 函数, swap 函数退出后, swap 函数的栈帧空间也随之被释放.
- 程序又回到 main 函数中继续向下执行.
由于在 swap 函数中, m 和 n 的值被修改, 所以 m 和 n 的值在 swap 函数执行前后发生改变.
一级指针做函数参数时的输入输出特性
指针做函数参数
函数调用时, 传递有效的地址值
数组做函数参数
函数传参传递的不再是整个数组, 而是数组的首地址 (一个指针).
所以, 当整型数组做函数参数时, 我们通常在函数定义中封装两个参数, 一个表示数组首地址, 一个表示数组元素个数.
语法:
void bubble_sort(int arr[10]) == void bubble_sort(int arr[]) == void bubble_sort(int *arr)
~ cat test.c #include <stdio.h> void bubble_sort(int arr[], int n) // void bubble_sort(int *arr, int n) { // sizeof(arr) 是函数首地址的大小, 也就是指针的大小, 在 64 位系统中大小为 8 // sizeof(arr[0]) 是函数首地址中存储的数据的大小, 数据是 int 类型, 所以为 4 int len = sizeof(arr) / sizeof(arr[0]); printf("bubble_sort: len = %d\n", len); printf("bubble_sort: sizeof(arr) = %lu\n", sizeof(arr)); printf("bubble_sort: sizeof(arr[0]) = %lu\n", sizeof(arr[0])); for (int i=0; i<n-1; i++) { for (int j=0; j<n-1-i; j++) { if (arr[j] > arr[j+1]) { int temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } } int main() { int arr[] = {5,12,6,15,23,65,1,7}; printf("main: sizeof(arr) = %lu\n", sizeof(arr)); int n = sizeof(arr) / sizeof(arr[0]); bubble_sort(arr, n); for (int i=0; i<n; i++) { printf("%d ", arr[i]); } printf("\n"); return 0; }
~ gcc test.c -o test && ./test test.c:5:21: warning: sizeof on array function parameter will return size of 'int *' instead of 'int []' [-Wsizeof-array-argument] int len = sizeof(arr) / sizeof(arr[0]); ^ test.c:3:22: note: declared here void bubble_sort(int arr[], int n) // void bubble_sort(int *arr, int n) ^ test.c:5:27: warning: 'sizeof (arr)' will return the size of the pointer, not the array itself [-Wsizeof-pointer-div] int len = sizeof(arr) / sizeof(arr[0]); ~~~~~~~~~~~ ^ test.c:3:22: note: pointer 'arr' declared here void bubble_sort(int arr[], int n) // void bubble_sort(int *arr, int n) ^ test.c:8:54: warning: sizeof on array function parameter will return size of 'int *' instead of 'int []' [-Wsizeof-array-argument] printf("bubble_sort: sizeof(arr) = %lu\n", sizeof(arr)); ^ test.c:3:22: note: declared here void bubble_sort(int arr[], int n) // void bubble_sort(int *arr, int n) ^ 3 warnings generated. main: sizeof(arr) = 32 bubble_sort: len = 2 bubble_sort: sizeof(arr) = 8 bubble_sort: sizeof(arr[0]) = 4 1 5 6 7 12 15 23 65
指针做函数返回值
指针作函数返回值时, 不能返回函数中局部变量的地址值.
~ cat test.c #include <stdio.h> int m = 100; int *test_func() { return &m; } int main() { int *ret = NULL; ret = test_func(); printf("%d\n", *ret); return 0; }
~ gcc test.c -o test && ./test 100
局部变量在函数调用结束后, 会随着函数栈帧的消失而被释放掉.
~ cat test.c #include <stdio.h> int *test_func() { int n = 20; return &n; } int main() { int *ret = NULL; ret = test_func(); printf("%d\n", *ret); return 0; }
~ gcc test.c -o test && ./test test.c:6:13: warning: address of stack memory associated with local variable 'n' returned [-Wreturn-stack-address] return &n; ^ 1 warning generated. 20
数组做函数返回值
C 语言中, 不允许.
只能写成指针形式.
二级指针函数参数的输入输出特性
输入特性
~ cat test.c
#include <stdio.h>
#include <stdlib.h>
void printArray(int **pArray, int len)
{
for (int i=0; i<len; i++)
{
printf("%d\n", *pArray[i]);
}
}
int main()
{
int **pArray = malloc(sizeof(int *) * 5);
// 在栈上创建 5 个数据
int a1 = 10;
int a2 = 20;
int a3 = 30;
int a4 = 40;
int a5 = 50;
pArray[0] = &a1;
pArray[1] = &a2;
pArray[2] = &a3;
pArray[3] = &a4;
pArray[4] = &a5;
// 打印数组
printArray(pArray, 5);
if (pArray != NULL)
{
free(pArray);
pArray = NULL;
}
return 0;
}
~ gcc test.c -o test && ./test 10 20 30 40 50
~ cat test.c #include <stdio.h> #include <stdlib.h> void printArray(int **pArray, int len) { for (int i=0; i<len; i++) { printf("%d\n", *pArray[i]); } } void freeSpace(int **pArray, int len) { for (int i=0; i<len; i++) { free(pArray[i]); pArray[i] = NULL; } } int main() { // 创建在栈区 int *pArray[5]; for (int i=0; i<5; i++) { pArray[i] = malloc(4); *(pArray[i]) = 10 + i; } // 打印数组 printArray(pArray, 5); // 释放堆区 freeSpace(pArray, 5); return 0; }
~ gcc test.c -o test && ./test 10 11 12 13 14
输出特性
被调函数分配内存, 主调函数使用
~ cat test.c #include <stdio.h> #include <stdlib.h> void allocateSpace(int **p) { int *temp = malloc(sizeof(int) * 10); for (int i=0; i<10; i++) { temp[i] = 100 + i; } *p = temp; } void printArray(int **p, int len) { for (int i=0; i<len; i++) { printf("%d\n", (*p)[i]); } } void freeSpace(int *p) { if (p != NULL) { free(p); p = NULL; } } int main() { int *p = NULL; allocateSpace(&p); printArray(&p, 5); // 同级指针不能将 p 修改为空指针 freeSpace(p); if (p == NULL) { printf("空指针\n"); } else { printf("野指针\n"); } p = NULL; return 0; }
~ gcc test.c -o test && ./test 100 101 102 103 104 野指针
~ cat test.c #include <stdio.h> #include <stdlib.h> void allocateSpace(int **p) { int *temp = malloc(sizeof(int) * 10); for (int i=0; i<10; i++) { temp[i] = 100 + i; } *p = temp; } void printArray(int **p, int len) { for (int i=0; i<len; i++) { printf("%d\n", (*p)[i]); } } void freeSpace(int **p) { if (*p != NULL) { free(*p); *p = NULL; } } int main() { int *p = NULL; allocateSpace(&p); printArray(&p, 5); // 高级指针可以将 p 修改为空指针 freeSpace(&p); if (p == NULL) { printf("空指针\n"); } else { printf("野指针\n"); } return 0; }
~ gcc test.c -o test && ./test 100 101 102 103 104 空指针
const 修改函数参数
调用函数时, 参数的传递方式如果是值传递, 会重新从实参中拷贝一份给形参
如果同时调用 n 次, 就会在内存中存在 n 份实参的拷贝
~ cat test.c #include <stdio.h> #include <stdlib.h> struct Person { char name[64]; // 0 - 63 int age; // 64 - 67 int id; // 68 - 71 double score; // 72 - 79 }; void showPerson(struct Person p) { printf("name: %s, age: %d, id: %d, score: %f\n", p.name, p.age, p.id, p.score); } int main() { struct Person p = {"point", 18, 12, 100}; showPerson(p); return 0; }
~ gcc test.c -o test && ./test name: point, age: 18, id: 12, score: 100.000000
将 struct Person p 改为 struct Person *p 节省资源
调用函数时, 如果传引用, 则只会拷贝实参的地址给形参, 而指针的大小是固定的 (4 bytes)
如果同时调用 n 次, 就会在内存中存在 n 份实参地址的拷贝, 相对于实参数据很大的场景来说, 内存得到优化
~ cat test.c #include <stdio.h> #include <stdlib.h> struct Person { char name[64]; // 0 - 63 int age; // 64 - 67 int id; // 68 - 71 double score; // 72 - 79 }; // 将 struct Person p 改为 struct Person *p 节省资源 // void showPerson(struct Person p) void showPerson(struct Person *p) { //printf("name: %s, age: %d, id: %d, score: %f\n", p.name, p.age, p.id, p.score); printf("name: %s, age: %d, id: %d, score: %f\n", p->name, p->age, p->id, p->score); } int main() { struct Person p = {"point", 18, 12, 100}; //showPerson(p); showPerson(&p); return 0; }
~ gcc test.c -o test && ./test name: point, age: 18, id: 12, score: 100.000000
传引用给函数, 若函数内存修改了引用的数据, 函数外部的原始数据也会被修改
~ cat test.c #include <stdio.h> #include <stdlib.h> struct Person { char name[64]; // 0 - 63 int age; // 64 - 67 int id; // 68 - 71 double score; // 72 - 79 }; // 将 struct Person p 改为 struct Person *p 节省资源 // void showPerson(struct Person p) void showPerson(struct Person *p) { p -> age = 100; //printf("name: %s, age: %d, id: %d, score: %f\n", p.name, p.age, p.id, p.score); printf("name: %s, age: %d, id: %d, score: %f\n", p->name, p->age, p->id, p->score); } int main() { struct Person p = {"point", 18, 12, 100}; printf("call showPerson before: p.age = %d\n", p.age); //showPerson(p); showPerson(&p); printf("call showPerson after: p.age = %d\n", p.age); return 0; }
~ gcc test.c -o test && ./test call showPerson before: p.age = 18 name: point, age: 100, id: 12, score: 100.000000 call showPerson after: p.age = 100
const 修饰形参, 防止误操作
将 struct Person *p 改为 const struct Person *p 节省资源, 且可以防止数据被修改
~ cat test.c #include <stdio.h> #include <stdlib.h> struct Person { char name[64]; // 0 - 63 int age; // 64 - 67 int id; // 68 - 71 double score; // 72 - 79 }; // 将 struct Person p 改为 struct Person *p 节省资源 // 将 struct Person *p 改为 const struct Person *p 节省资源, 且可以防止数据被修改 // void showPerson(struct Person p) // void showPerson(struct Person *p) void showPerson(const struct Person *p) { p -> age = 100; //printf("name: %s, age: %d, id: %d, score: %f\n", p.name, p.age, p.id, p.score); printf("name: %s, age: %d, id: %d, score: %f\n", p->name, p->age, p->id, p->score); } int main() { struct Person p = {"point", 18, 12, 100}; printf("call showPerson before: p.age = %d\n", p.age); //showPerson(p); showPerson(&p); printf("call showPerson after: p.age = %d\n", p.age); return 0; }
~ gcc test.c -o test && ./test test.c:18:14: error: cannot assign to variable 'p' with const-qualified type 'const struct Person *' p -> age = 100; ~~~~~~~~ ^ test.c:16:38: note: variable 'p' declared const here void showPerson(const struct Person *p) ~~~~~~~~~~~~~~~~~~~~~^ 1 error generated.