C语言——指针与函数指针

6 阅读8分钟

1.第一个 C 程序

这里以 Visual Studio 2013 为例,打开 Visual Studio 2013 后,选择 文件 > 新建 > 项目,选择 Visual C++ > Win32 控制台应用程序,项目名为 Hello,然后点击 “确定” ,再点击 “下一步”,选择 “空项目”,然后点击完成。点击 视图 > 解决方案资源管理器,选中 源文件,右击选择 添加 > 新建项,输入名称 main.c,选择 添加,代码如下所示:

#include <stdio.h>

int main() {  
	printf("Hello World");
	getchar();
	return 0;
}

你可以直接点击 “本地 Windows 调试器” 运行程序,也可依次执行如下步骤:

  • 1.右击项目并选择 “生成”,这将在项目所在的目录的 Debug 文件夹下生成可执行文件 Hello.exe。
  • 2.在命令提示符中,切换到可执行文件所属的文件夹(通常是项目文件夹中的 Debug 文件夹)。
  • 3.输入.\Hello.exe 运行它即可在控制台窗口上看到打印,打印如下:
Hello World

这里 #include <stdio.h> 是 C 语言中的一个预处理指令,它的作用是将标准输入输出库(Standard Input/Output Library)的头文件包含到你的程序中。没有这个头文件,你就无法使用如 printf()。getchar() 作用是让用户输入字符,在这里用于暂停程序的执行,否则控制台窗口打开后马上就消失了。

2. 指针

在 C/C++ 中万物皆指针,下面我们来学习指针的相关知识。修改代码如下:

#include <stdio.h> 

int main() {
    int number = 100;
    printf("number的内存地址是:%p\n", &number);  // %p 是地址输出的占位
    getchar();  // 控制台等带用户输入
    return 0;
}

运行后打印如下:

number的内存地址是:00CFFCCC

首先我们要明白,所有的变量都是存储在内存中,所有的变量都有对应的内存地址,我们可以通过这个内存地址拿到这个变量,反之,也可以通过这个变量拿到该变量的内存地址。

这里的 & 操作符是取地址符,用于获取 number 在内存中的地址,打印出来是一个十六进制的地址。

地址可以赋给指针,这样指针就指向了这个内存地址。修改代码如下:

#include <stdio.h> 

int main() {
	int number = 100;
	printf("number的内存地址是:%p\n", &number); 
	int * a = &number;
	printf("a的值为:%p\n", a);
	getchar();
	return 0;
}

打印如下:

number的内存地址是:00EFFB14
a的值为:00EFFB14

这里使用 int * a 定义了一个指针 a,然后把 number 的内存地址赋值给它, 通过打印可以看到 a 的值现在等于 number 的内存地址了。

再对代码进行修改,如下所示:

#include <stdio.h> 

int main() {
	int number = 100;
	printf("number的内存地址是:%p\n", &number); 
	int * a = &number;
	printf("a的值为:%p\n", a);
	*a = 102;
	printf("number的值为:%d\n", number);
	getchar();
	return 0;
}

打印如下:

number的内存地址是:00D6F8DC
a的值为:00D6F8DC
number的值为:102

注意第 8 行 * a 跟前面又不一样了,* 直接作用在变量 a 上,这里 a 就是一个内存地址,* a 表示取出这个内存地址所对应的值。也就是把 00D6F8DC 这个内存地址所对应的值改成了 102,也就等于把 number 的值改成了 102。

看下面的实例,使用指针实现两个变量的值的交换:

#include <stdio.h>

void changeAction(); // 由于 C 语言不支持函数重载,所以声明函数不需要写参数

int main() {
	int a = 100;
	int b = 200;
	changeAction(&a, &b);
	printf("交换后a=%d, b=%d\n", a, b);
	getchar();
}

void changeAction(int * a, int * b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

运行后打印如下:

交换后a=200, b=100

再来看一个实例:

#include <stdio.h>

void update(int m) { 
	printf(" update 函数中 m 的内存地址是:%p\n", &m); 
}

int main() {
	int m = 100; 
	update(m);

	printf(" main 函数中 m 的内存地址是:%p\n", &m);
	getchar();

	return 0;
}

打印如下:

 update 函数中 m 的内存地址是:00DDF6B8
 main 函数中 m 的内存地址是:00DDF78C

两个函数中 m 的内存地址是不一样的,因为 update() 函数中 m 是新建的,它跟 main() 函数中的 m 值是一样的,但是是两个变量。

把上面的代码改一下:

#include <stdio.h>

void update(int * m) { 
	printf(" update 函数中 m 的内存地址是:%p\n", m); 
}

int main() {
	int m = 100; 
	update(&m);

	printf(" main 函数中 m 的内存地址是:%p\n", &m);
	getchar();

	return 0;
}

打印出来两个内存地址就一样了,因为 update() 函数中的 m 实际是一个地址,它等于 main() 函数中的变量 m 的地址。

3. 数组

看下面的实例:

#include <stdio.h>

int main() {
	// int [] arr = {1,2,3,4} 是错误的写法
	int arr[] = { 1, 2, 3, 4 };

	int i = 0;
	for (i = 0; i < 4; ++i) {
     printf("%d\n", arr[i]);
	}

	printf("arr  =   %p\n", arr);
	printf("&arr  =   %p\n", &arr);
	printf("&arr[0]  =   %p\n", &arr[0]);

	int * arr_p = arr;
	printf("%d\n", *arr_p); 

	arr_p++; 
	printf("%d\n", *arr_p); 

	arr_p += 2;
	printf("%d\n", *arr_p); 

	arr_p -= 3; 
	printf("%d\n", *arr_p);

	arr_p += 2000;
	printf("%d\n", *arr_p);

	getchar();
	return 0;
}

打印如下:

1
2
3
4
arr  =   00AFF798
&arr  =   00AFF798
&arr[0]  =   00AFF798
1
2
4
1
0

可以看到 arr、&arr 和 &arr[0] 的值是一样的,事实上,这 3 个都可以用于表示该数组的内存地址。使用 int * arr_p = arr 将指针 arr_p 指向 arr 后,可以通过 *arr_p 拿到数组的第一个值 1,然后可以通过指针位移拿到其他的值。当位移超过了数组的范围的时候,拿到的是系统值,这里打印是 0,有些系统会打印一个无意义随机数,比如 547852156。

为什么可以通过指针位移来获取数组里面的每一个值呢,因为数组的内存是连续的,对于 int 数组,指针 + 1 表示指针往后挪动 4 个字节( 32 位系统)。

再来看一个例子:

#include <stdio.h>

int main() {

	int num = 100;
	int * int_p = &num;
	double * double_p = &num;
	long float * lf_p = &num;

	printf("%d\n", sizeof int_p);
	printf("%d\n", sizeof double_p);
	printf("%d\n", sizeof lf_p);

	getchar();

	return 0;
}

打印如下:

4
4
4

从打印可以看到指针占用的内存大小都是 4 个字节( 32 位系统),那为什么定义指针的时候使用 int * 来定义指针,这里的 int 有什么作用呢?

答案是为了告诉编译器指针所指向的数据类型,int* 类型的指针,编译器解引用时会读取4字节(int 的大小),double* 类型的指针,编译器解引用时会读取 8 字节(double 的大小)。还有是为了方便指针算数运算,比如对于 int* 类型的指针,数组取值的时候,调用 arr_p++ 会让地址每次增加 sizeof(int)。

4.函数指针

#include <stdio.h>

void add(int num1, int num2); // 先声明,后面再实现

void mins(int num1, int num2) { // 声明和实现一起
    printf("num1 - num2 = %d\n", (num1 - num2));
}
// 函数指针
void opreate(void(*method)(int, int), int num1, int num2) {
	method(num1, num2);
	printf(" opreate 函数的 method 指针是:%p\n", method);
}

int main() {  
    opreate(add, 1, 2);
    printf(" main 函数的 add 指针是:%p\n", add);
    printf("\n");
    opreate(mins, 100, 10);
    printf(" main 函数的 mins 指针是:%p\n", mins);
    printf("\n");
    printf("%p, %p\n", add, &add); 
    getchar();

    return 0;
}

// 再实现
void add(int num1, int num2) {
	printf("num1 + num2 = %d\n", (num1 + num2));
}

看 operate 函数的声明,是不是感觉函数指针用起来很像 Kotlin 里面的高阶函数,同样是把另一个函数作为参数传入,这样就可以在调用的时候再决定对 num1 和 num2 执行何种运算。

打印如下:

num1 + num2 = 3
 opreate 函数的 method 指针是:000710E6
 main 函数的 add 指针是:000710E6

num1 - num2 = 90
 opreate 函数的 method 指针是:0007119F
 main 函数的 mins 指针是:0007119F

000710E6, 000710E6

从打印可以看出来,在执行 opreate(add, 1, 2) 时, 函数指针 method 指向 add 函数,它们俩打印出来的地址是一样的,所以可以这样执行。

再来看一个例子:

#include <stdio.h>

void callBackMethod(char * fileName, int current, int total) {
	printf("%s图片压缩的进度是:%d/%d\n", fileName, current, total);
}

void compress(char * fileName, void(*callBackP)(char *, int, int)) {
	callBackP(fileName, 5, 100); 
}

int main() {
	void(*call) (char *, int, int);  // 声明函数指针
	call = &callBackMethod;  // 指针指向 callBackMethod 函数
	compress("123.png", call); 
	getchar();
	return 0;
}

运行后打印如下:

123.png图片压缩的进度是:5/100

可以看到还可以把 函数指针直接声明出来,并指向另外一个函数。