函数指针 函数类型
C 语言中的函数有自己特定的类型
函数的类型由返回值 ,参数类型 和参数个数 共同决定
int add (int i, int j) 的类型为 int(int, int)
C 语言中通过 typedef 为函数类型重命名
typedef type name(parameter list)
例如:
typedef int f(int, int) // 将 int 类型重命名为 int f(int, int)
typedef void p(int);
函数指针
函数指针用于指向一个函数
函数名是执行函数体的入口地址
可通过函数类型定义函数指针:FuncType pointer; *
也可以直接定义:*type (pointer)(parameter list);
pointer 为函数指针变量名
type 为所指函数的返回值类型
parameter list 为所指函数的参数类型列表
如何使用 C 语言直接跳到某个固定的地址开始执行?
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 #include <stdio.h> typedef int (FUNC) (int ) ;int test (int i) { return i * i; } void f () { printf ("Call f()...\n" ); } int main () { FUNC* pt = test; void (*pf)() = &f; printf ("pf = %p\n" , pf); printf ("f = %p\n" , f); printf ("&f = %p\n" , &f); pf(); (*pf)(); printf ("Function pointer call: %d\n" , pt(2 )); return 0 ; }
回调函数
回调函数是利用函数指针实现的 一种调用机制
回调机制原理
调用者不知道 具体事件发生时需要调用的具体函数
被调函数不知道 和时被调用,只知道需要完成任务
当具体事件发生时,调用者通过函数指针调用具体函数
回调机制中的调用者和被调用者函数互不依赖
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 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 #include <stdio.h> typedef int (*Weapon) (int ) ;void fight (Weapon wp, int arg) { int result = 0 ; printf ("Fight boss!\n" ); result = wp(arg); printf ("Boss loss: %d\n" , result); } int knife (int n) { int ret = 0 ; int i = 0 ; for (i = 0 ; i < n; i++) { printf ("Knife attack: %d\n" , 1 ); ret++; } return ret; } int sword (int n) { int ret = 0 ; int i = 0 ; for (i = 0 ; i < n; i++) { printf ("Sword attack: %d\n" , 5 ); ret += 5 ; } return ret; } int gun (int n) { int ret = 0 ; int i = 0 ; for (i = 0 ; i < n; i++) { printf ("Gun attack: %d\n" , 10 ); ret += 10 ; } return ret; } int main () { fight(knife, 3 ); fight(gun, 5 ); return 0 ; }
小结
C 语言中的函数都有特定的类型
可以使用函数类型定义函数指针
函数指针是实现回调机制的关键技术
通过函数指针 可以实现在 C 程序中实现固定地址跳转
函数的意义 从高级角度来看 C 语言的组成,C 语言可以是,以不同函数之间的调用组成的;
函数的由来
程序 = 数据 + 算法(处理数据的算法)
C 程序 = 数据 + 函数
模块化程序设计
所谓的面向过程就是将一个复杂的问题分解为多个简单的问题;
面向过程的程序设计
面向过程是一种以过程为中心 的编程思想
首先将复杂的问题分解 为一个个容易解决的问题
分解过后的问题可以按照步骤一步步完成
函数是面向过程在 C 语言中的体现
解决问题的每个步骤可以用函数来实现
函数的声明和定义
声明的意义在于告诉编译器程序单元的存在
定义则明确指示程序单元的意义
C 语言中通过 extern 进行程序单元的声明
一些程序单元在声明时可以省略 extern
严格意义上来说声明和定义并不相同!
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 #include <stdio.h> #include <malloc.h> extern int g_var;extern struct Test ;int main () { extern void f (int i, int j) ; extern int g (int x) ; struct Test * p = NULL ; printf ("p = %p\n" , p); printf ("g_var = %d\n" , g_var); f(1 , 2 ); printf ("g(3) = %d\n" , g(3 )); free (p); return 0 ; }
在 C 语言中变量的定义必然伴随着内存的分配,如果只是声明则只是告诉编译器有这么个变量,并不会开辟新的空间;
函数的参数 顺序点
函数参数在本质 上与局部变量相同在栈上分配空间
函数参数的初始值 是函数调用时的实际值
在 C 语言中,函数参数的求值顺序依赖于编译器的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> int func (int i, int j) { printf ("%d, %d\n" , i, j); return 0 ; } int main () { int k = 1 ; func(k++, k++); printf ("%d\n" , k); return 0 ; }
在 C 语言中并没有规定函数参数的求值顺序,只是规定了必须将值求出来之后在进行函数调用
++ 在后先用后加,那么按照输出应该是 后面的 k 传递给函数后 ++ 变成了 2 ,前面这个参数传递给函数后又 ++ 变成了 3;
在 C 语言中,操作符(包括函数)的求值顺序是不固定的,依赖于编译器的实现; (大多数编译器是从右向左的)
程序的顺序点
程序中存在一定的顺序点
顺序点指的是执行过程中修改变量值的最晚时刻
在程序到达顺序点的时候 ,之前所作的一切操作必须完成
C 语言中的顺序点
每个完整表达式结束时 ,即分号处
&&,||,?:,以及逗号表达式 的每个参数计算后
函数调用时所有实参求值完成后(进入函数图之前)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> int main () { int k = 2 ; int b = 0 ; int c = 0 ; int a = 1 ; b = k++ + k++; printf ("k = %d\n" , k); printf ("b = %d\n" , b); if (a-- && a) { printf ("a = %d\n" , a); } return 0 ; }
k
最初是 2。
第一个 k++
会返回 2,然后 k
变为 3。
第二个 k++
会返回 3,然后 k
变为 4。
最终的结果是 b = 2 + 3 = 5
。因此,打印结果将是 b = 5
。
注意,这种用法在 C 标准中是未定义行为,实际结果可能依赖于编译器和优化设置。为了确保可预测的结果,建议避免在同一表达式中对同一变量进行多次修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> int func (int i, int j) { printf ("%d, %d\n" , i, j); return 0 ; } int main () { int k = 1 ; func(k++, k++); printf ("k = %d\n" , k); return 0 ; }
小结
函数的参数在栈上分配空间
函数的实参并没有固定的计算次序
顺序点 是 C 语言中变量修改的最晚时机
参数入栈的顺序 函数参数的计算次序是依赖编译器实现的 ,那么函数参数的入栈次序是如何确定 的呢?
调用约定
当函数调用 发声时
参数 会传递给被调用者的函数
而返回值 会被返回给函数调用者
调用约定 描述参数如何传递 到栈中以及栈的维护方式
调用约定是预定义的 可理解为调用协议
调用约定通常用于库调用 和库开发 的时候
1 2 从右到左依次入栈:__stdcall, __Cdecl, __thiscall(__Cdecl 是 C 语言默认的调用约定) 从左到右依次入栈:__pascal, __fastcall
在 C 语言中,函数的调用约定(calling convention)是指在函数调用时,如何传递参数、返回值,以及如何管理栈的约定。这些约定对于不同编译器、处理器架构以及操作系统之间的兼容性至关重要。
可变参数
C 语言可以定义参数可变的寒素
参数可变函数的实现依赖于 stdarg.h 头文件
va_list - 参数集合
va_arg - 取具体参数值
va_start - 标识参数访问的开始
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 24 25 #include <stdio.h> #include <stdarg.h> float average (int n, ...) { va_list args; int i = 0 ; float sum = 0 ; va_start(args, n); for (i=0 ; i<n; i++) { sum += va_arg(args, int ); } va_end(args); return sum / n; } int main () { printf ("%f\n" , average(5 , 1 , 2 , 3 , 4 , 5 )); printf ("%f\n" , average(4 , 1 , 2 , 3 , 4 )); return 0 ; }
va_list args:定义一个类型,用于存储可变参数的列表。
va_start(args, n):表示从 n 的下一个参数开始获取可变参数,而 n 本身是最后一个固定参数的名称。
va_arg(args, int):从 args
中获取下一个参数,并将其转换为 int
类型。
va_end(args):清理 args
,以避免内存泄漏。
可变参数的限制
可变参数必须从头到尾按照顺序逐个访问
参数列表中至少要村在一个确定的命名参数
可变参数函数无法确定实际存在的参数的数量
可变参数无法确定参数的实际类型
va_arg 中如果制定了错误的类型,那么结果是不可预测的
小结
调用约定指定了函数参数的入栈顺序 以及栈的清理方式
可变参数是 C 语言提供的一种函数设计技巧
可变参数的函数提供了一种更方便的函数调用方式
可变参数必须顺序点访问 ,无法直接访问中间的参数值
函数与宏
宏是由预处理器直接替换展开的 ,编译器不知道宏的存在
函数是由编译器直接编译 的实体,调用行为由编译器决定
多次使用宏会导致最终可执行程序的体积增大
函数时跳转执行 的,内存中只有一份函数体的存在
宏的效率比函数要高 ,因为是直接展开,无调用开销
函数调用时会创建活动记录,效率不如宏
注意
宏 的效率比函数稍高,但是其副作用巨大
宏是文本替换,参数无法进行类型检查
可以用函数完成的绝对不用宏
宏的定义中不能出现递归定义
宏的妙用
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 #include <stdio.h> #include <malloc.h> #define MALLOC(type, x) (type*)malloc(sizeof(type)*x) #define FREE(p) (free(p), p=NULL) #define LOG_INT(i) printf("%s = %d\n" , #i, i) #define LOG_CHAR(c) printf("%s = %c\n" , #c, c) #define LOG_FLOAT(f) printf("%s = %f\n" , #f, f) #define LOG_POINTER(p) printf("%s = %p\n" , #p, p) #define LOG_STRING(s) printf("%s = %s\n" , #s, s) #define FOREACH(i, n) while(1) { int i = 0, l = n; for(i=0; i < l; i++) #define BEGIN { #define END } break; } int main () { int * pi = MALLOC(int , 5 ); char * str = "D.T.Software" ; LOG_STRING(str); LOG_POINTER(pi); FOREACH(k, 5 ) BEGIN pi[k] = k + 1 ; END FOREACH (n, 5 ) BEGIN int value = pi[n]; LOG_INT(value); END FREE (pi) ; LOG_POINTER(pi); return 0 ; }
递归 函数设计原则