函数和指针

函数指针

函数类型

  • 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;

// 这里我们将运行后指针的地址直接赋值给函数指针,输出与之前一模一样
// 说明函数指针可以直接跳到某个固定的地址开始指针
// void(*pf)() = 0x55ea891ba17c;
// 三种写法本质一样的,只是写法不同
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));

/*
pf = 0x55ea891ba17c
f = 0x55ea891ba17c
&f = 0x55ea891ba17c
Call f()...
Call f()...
Function pointer call: 4
*/

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);

/*
Fight boss!
Knife attack: 1
Knife attack: 1
Knife attack: 1
Boss loss: 3
Fight boss!
Gun attack: 10
Gun attack: 10
Gun attack: 10
Gun attack: 10
Gun attack: 10
Boss loss: 50
*/

return 0;
}

小结

  • C 语言中的函数都有特定的类型
  • 可以使用函数类型定义函数指针
  • 函数指针是实现回调机制的关键技术
  • 通过函数指针可以实现在 C 程序中实现固定地址跳转

函数的意义

从高级角度来看 C 语言的组成,C 语言可以是,以不同函数之间的调用组成的;

函数的由来

程序 = 数据 + 算法(处理数据的算法)

C 程序 = 数据 + 函数

模块化程序设计

Cmokuaihuasheji

所谓的面向过程就是将一个复杂的问题分解为多个简单的问题;

面向过程的程序设计

  • 面向过程是一种以过程为中心的编程思想
  • 首先将复杂的问题分解为一个个容易解决的问题
  • 分解过后的问题可以按照步骤一步步完成
  • 函数是面向过程在 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; // (struct Test*)malloc(sizeof(struct Test));
// struct Test* p = (struct Test*)malloc(sizeof(struct Test));
// 这样将会报错,因为 sizeof(struct Test) 无法得到一个确定的大小信息
// 文件中仅仅有 struct Test 的声明而没有定义,所以 sizeof(struct Test) 无法得到一个大小信息
// 当编译 extern struct Test; 还没有编译到 Test 所在的文件时,当前文件是无法得到 Test 结构体大小的
// 所以我们不能依赖于编译器对于文件的编译顺序

printf("p = %p\n", p);

//g_var = 10;

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);

/*
2, 1
3
*/

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); // 4
printf("b = %d\n", b); // 5

// && 左右两边都是完成点,a-- = 0 所以不成立
if(a-- && a) {
printf("a = %d\n", a);
}

return 0;
}
  1. k 最初是 2。
  2. 第一个 k++ 会返回 2,然后 k 变为 3。
  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);

/*
2, 1 // 在 vs 编译器中这里的值为 1, 1
k = 3
*/

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)

//#i 是一个预处理器运算符,它将宏参数 i 转换为字符串。
#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;
}

递归

函数设计原则