首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >收集飞花令碎片——C语言指针

收集飞花令碎片——C语言指针

作者头像
枫亭湖区
发布2025-11-18 16:16:57
发布2025-11-18 16:16:57
410
举报

暑假已尽,小编也开始进入肝文模式了

从今天起,我们开始进入C语言最难的环节————指针

  • 作为每位码农学习C语言时候的“拦路虎”,C语言指针的复杂性、多元化、思维深度大,让99%的代码萌新都顺利入坑

今天小编就用一片博文,带大家从基础、进阶到技巧、运用,层层进化,带大家打通C语言指针的任督二脉

内存的基本概念

我们知道计算机CPU(中心处理器)在处理数据,需要的数据是在内存中读取的,处理后的数据也会放回内存中

我们把内存划分为一个个内存单元,每个内存单元存储一个字节 每个内存单元一个字节空间里面能放8个比特位 1Byte = 8bit 1KB = 1024Byte 1MB = 1024KB 1GB = 1024MB 1TB = 1024GB 1PB = 1024TB

地址的基本概念

我们观察下面这一组代码

代码语言:javascript
复制
#include <stdio.h>
int main(){

	int a = 10;

return 0;
}

我们打开监视

我们再打开内存窗口

指针的基本概念

每个内存单元也都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。

在计算机中,我们把给内存空间起的编号称为地址

内存空间的编号=地址=指针

简单来讲,指针是一种变量,但它存储的不是普通的数据值,而是内存地址。通过指针,可以直接访问或修改该地址上存储的数据。

指针:(一)指针的定义与声明

代码语言:javascript
复制
数据类型 *指针变量名;
int *p;      // p 是一个指向 int 类型的指针
char *c;     // c 是一个指向 char 类型的指针
float *f;    // f 是一个指向 float 类型的指针

指针:(二)关键运算符

(1)取地址运算符&
  • 用于获取变量的内存地址
代码语言:javascript
复制
int num = 10;
int *p = &num;  // p 存储了 num 的地址
(2)解引用运算符*
  • 用于访问指针指向的内存地址中的值
代码语言:javascript
复制
int value = *p;  // value = 10(获取 p 指向地址的值)
*p = 20;         // 修改 p 指向地址的值,num 现在等于 20

我们观察下面的两段代码

代码语言:javascript
复制
#include <stdio.h>

//这段代码将n的四个字节全部改为0
int main() {

	int n = 0x11223344;
	int* p = &n;

	*p = 0;

	printf("%d\n", n);

	return 0;
}
代码语言:javascript
复制
#include <stdio.h>
int main() {

	int n = 0x11223344;
	printf("n的值为:%x\n");

	char *p = &n;			//&n类型指向int指针
							//p的类型是char *(指向char类型的指针)
	*p = 0;

	printf("n的值为:%x\n");

	return 0;
 }

调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第⼀个字节改为0

结论 :指针的类型决定了,对指针解引用的时候有多⼤的权限(一次能操作几个字节)。 比如: char* 的指针解引⽤就只能访问⼀个字节,而 int* 的指针的解引⽤就能访问四个字节。

指针:(三)指针初始化

  • 指针在使用前必须初始化,否则可能成为野指针(指向未知内存)。
  • 可以初始化为 NULL(空指针),表示不指向任何有效地址。

指针:(四)指针大小

  • 指针的大小取决于系统架构:
    • 32位系统:指针占 4字节
    • 64位系统:指针占 8字节
代码语言:javascript
复制
printf("指针的大小:%zu字节\n", sizeof(int*));

指针:(五)指针变量

我们通过取地址符&,可以获得一个地址的值(如:0x006FFD70) 这个数值也是需要存储起来的,方便后期能使用 那么这个数值是存储在哪里呢? 这边我们就要引入一个新概念:指针类型

我们要如何理解指针类型呢?

我们先观察下面的代码

代码语言:javascript
复制
int a = 10;
int * pa = &a;

pa左边的int *中,*是在说明pa是指针变量,而前面的 int 是在说明pa指向的是整型(int)类型的对象。

在32位平台下,指针变量大小是4个字节 在64位平台下,指针变量大小是8个字节

代码语言:javascript
复制
#include <stdio.h>
int main()
{
	printf("%zd\n", sizeof(char *));
	printf("%zd\n", sizeof(short *));
	printf("%zd\n", sizeof(int *));
	printf("%zd\n", sizeof(double *));
return 0;
}
在这里插入图片描述
在这里插入图片描述

指针:(六)指针运算

指针运算有三种形式:

  • 指针±整数
  • 指针-指针
  • 指针的关系运算
指针±整数

因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的所有元素。

代码语言:javascript
复制
#include <stdio.h>

int main() {

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = &arr[0];
	int sz = sizeof(arr) / sizeof(arr[0]);

	for (int i = 0; i < sz; i++) {
	
		printf("%d ", *(p + i));

	}
	return 0;
}

p+i 就是数组中下标为i元素的地址 *(p+i)就是下标为i的这个元素

代码语言:javascript
复制
#include <stdio.h>

int main() {

	char arr[] = "Hello World";       //这个数组真实的样貌是:"Hello World\0"
	//printf("%s \n", arr);

	char* p = &arr[0];
	while(*p != '\0')
	{ 
	
		printf("%c", *p);
		p++;
	
	}

	return 0;
}

arr[]数组的真实样貌是 Hello World\0

指针 - 指针

指针-指针:得到的是两个指针之间的元素个数 前提: 两个指针指向了同一块空间,否则不能相减

代码语言:javascript
复制
#include <stdio.h>

	int main(){
	
		int arr[10] = { 0 };
		printf("%lld\n", &arr[ 9 ] - &arr[ 0 ]);
		printf("%lld\n", &arr[ 0 ] - &arr[ 9 ]);
		//数组随着下标的增长,地址由低到高变化的
	

		return 0;
	}
在这里插入图片描述
在这里插入图片描述

数组随着下标的增长,地址由低到高变化的 所以&arr[ 9 ] - &arr[ 0 ] = 9

指针:(七)void* 指针

void*可以理解为无具体类型指针(或者称为泛型指针),这种类型的指针可以用来接受任意类型地址

void*指针不能进行指针±操作和解引用操作

观察下面的代码

在这里插入图片描述
在这里插入图片描述

将一个int类型的变量赋值给char类型的指针变量 编译器会因为类型不兼容而给出一个警告

在这里插入图片描述
在这里插入图片描述

而void指针就不会有这样的问题

利用void指针接受地址

代码语言:javascript
复制
#include <stdio.h>

int main(){

	int num = 0;
	void* p = &num;
	void* pc = &num;

	*p = 10;
	*pc = 10;

	return 0;
}

在这里插入图片描述
在这里插入图片描述

这里我们可以看到,void类型指针可以接收不同类型的地址,但是无法进行直接的计算

void指针的用处

void一般是使用在函数的参数部分,用来实现接收不同数据类型的地址,用来实现泛型编程的效果

指针:(八)指针与const

代码语言:javascript
复制
const int *ptr1;      // 指向常量的指针,指针可变,值不可变
int *const ptr2;      // 常量指针,指针不可变,值可变
const int *const ptr3;// 指向常量的常量指针,都不可变 
(1)指向常量的指针 (const int *ptr)

特点: 指针可以指向别的变量,但不能通过指针修改变量值

代码语言:javascript
复制
int a = 10;
const int *ptr = &a; // ptr指向a,但不能通过ptr修改a的值

// *ptr = 20; // 错误!不能通过ptr修改a
a = 20;      // 正确,可以直接修改a

int b = 30;
ptr = &b;    // 正确,可以改变指针指向
(2)常量指针(int *const ptr)

特点: 指针永远指向同一个变量,但可以通过指针修改变量值

代码语言:javascript
复制
int x = 10;
int *const ptr = &x; // ptr将永远指向x

*ptr = 20; // 正确,可以修改x的值

int y = 30;
// ptr = &y; // 错误!ptr不能指向别的变量
(3)指向常量的常量指针(const int *const ptr)

特点: 指针不能改变指向,也不能通过指针修改变量值

代码语言:javascript
复制
int m = 10;
const int *const ptr = &m; // ptr永远指向m,且不能通过ptr修改m

// *ptr = 20; // 错误!不能通过ptr修改m
// ptr = &n;  // 错误!不能改变指针指向

简单记忆法 看const和*的位置关系:

  • const在*左边:值不能改
  • const在*右边:指针不能改
  • 两边都有const:都不能改

指针:(九)野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)


野指针成因(1):指针未初始化
代码语言:javascript
复制
#include <stdio.h>

int main() {

	int *p;     //局部变量指针未初始化,默认值为随机值

	*p = 20;

	return 0;
}

野指针成因(2):指针越界访问
代码语言:javascript
复制
#include <stdio.h>

int main() {

	int arr[10] = { 0 };
	int* p = arr;

	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {

		printf("%d\n", *p);

		*p = i;
		p++;

	}

	return 0;
}

野指针成因(3):指针指向的空间释放
代码语言:javascript
复制
#include <stdio.h>
int* test()
{
	int n = 100;  //n是局部变量,函数结束则生命周期结束
	return &n;
}
int main()
{
	int* p = test();	//这时p指向的就是一个野指针
	printf、
	printf("%d\n", *p);
	return 0;
}

这时候我们可以使用静态变量static

代码语言:javascript
复制
#include <stdio.h>
int* test()
{
	static int n = 100;  // 静态变量,生命周期贯穿整个程序
	return &n;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}
规避野指针(1):指针初始化

如果明确指针指向哪里就直接赋值指针 如果不知道就直接给指针赋值NULL(空指针)

NULL指针是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。


规避野指针(2):注意越界访问

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。


规避野指针(3):及时将闲置指针设置成NULL
代码语言:javascript
复制
#include <stdio.h>

int main() {

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int* p = arr;
	int i = 0;
	
	for (i = 0; i < 5; i++) 
	{
		*p = 5;							//将指针p指向的当前地址的值改为5
		p++;
	}

	p = NULL;
	//现在又想用p
	p = arr;
	if (p == NULL) {

		printf("p是空指针\n");

	}
	return 0;
}
规避野指针(4):避免返回局部变量的地址
代码语言:javascript
复制
int* test()
{
	int n = 100;  
	return &n;
}

指针:(十)assert断言

assert头文件定义了assert 用来确保程序在运行时,符合指定条件;如果不符合条件,则终止运行

定义assert的规则
代码语言:javascript
复制
assert(条件表达式);

assert的工作原理
代码语言:javascript
复制
#include <assert.h>
void assert(int expression);
  • 如果expression的值为真(非0),assert什么都不做
  • 如果expression的值为假(0),assert会

输出错误信息(包含文件名、行号、失败的表达式)调用abort()函数终止程序如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号

如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前⾯,定义一个宏 NDEBUG

代码语言:javascript
复制
#define NDEBUG
#include <stdio.h>

assert的缺点

assert在调用的时候会引入额外的检查,增加程序运行的时间

一般我们可以在Debug中使用,在Release版本中选择禁⽤assert就行,在 VS 这样的集成开发环境中,在Release版本中,直接就是优化掉。这样在debug版本写有利于程序员排查问题,在Release版本不影响用户使用程序的效率。


指针:(十一)指针的使用和传值调用

(1)strlen的模拟实现

库函数strlen的原型是求字符串的长度 统计的字符串\0之前的个数


strlen函数的使用

代码语言:javascript
复制
#include <stdio.h>

int main() {
	char arr[] = "abcdefg";
	size_t len = strlen(arr);
	printf("%zu\n", len);

	return 0;
}

我们知道strlen只需要将字符串的起始地址传递给strlen就行


那我们能不能自己把strlen函数自己编写出来?


代码语言:javascript
复制
#include <stdio.h>
#include <assert.h>  // 需要包含assert.h头文件

// 计算字符串长度
// 参数:str - 指向以null结尾的字符串的指针
// 返回值:字符串的长度(不包括结尾的null字符)
// 使用size_t(无符号整型)作为返回类型是最合适的,因为长度不可能是负数
size_t my_strlen(const char* str) {
    size_t count = 0;  // 计数器,用于统计字符数量

    // 使用断言确保传入的指针不为NULL,避免对空指针进行解引用
    assert(str != NULL);

    // 遍历字符串,直到遇到字符串结束符'\0'
    while (*str != '\0') {
        count++;  // 计数器加1
        str++;    // 指针移动到下一个字符
    }

    return count;  // 返回字符串长度
}

int main() {
    char str[] = "abcdefg";     // 定义一个测试字符串
    size_t len = my_strlen(str); // 调用自定义的字符串长度函数
    printf("%zu\n", len);       // 使用%zu格式说明符打印size_t类型的值

    return 0;
}

(2)传值调用和传址调用

代码中什么问题是非指针解决不可的呢?


例如: 写一个函数,交换两个整型变量

在指针之前我们可能会写下面的代码

代码语言:javascript
复制
#include <stdio.h>
void Swap1(int x, int y)
{
	int tmp = x;
	x = y;
	y = tmp;
}
int main()
{
	int a = 0;
	int b = 0;
	scanf("%d %d", &a, &b);
	printf("交换前:a=%d b=%d\n", a, b);
	Swap1(a, b);
	printf("交换后:a=%d b=%d\n", a, b);
	return 0;
}

当我们运行代码,结果如下:

在这里插入图片描述
在这里插入图片描述

我们发现a、b并没有发生交换,为什么?


下面讲解一下传值调用就能明白

函数调用方法(传值调用)

通过调试我们发现:a和b的值并没有发生交换 不知道VS的调试技巧可以看这里

在这里插入图片描述
在这里插入图片描述

  1. 我们在main函数里面创建的两个变量:a和b,并给a与b分配了两个地址
  2. 在调用Swap函数时,将a与b作为实参传递过去。在Swap函数内部创建两个形参变量x和y负责接收a和b
  3. 但是,x、y、a、b的地址不一样,x和y确实接收了。但是x和y相当于是两个独立的空间,改变的也只是x和y的值,a和b自然就不会受影响了
  4. 当Swap函数调用结束后,回到main函数,a和b没办法发生交换

结论:实参传递给形参的时候,形参会单独创建一份单独空间来接收实参,对形参的修改不影响实参


函数调用方法(传址调用)

传值调用只是传递两个实参变量给Swap函数,但是函数交换的空间对应着形参的地址,与实参的地址不同 下面我们提供另外一种函数调用的方法:传址调用

观察下面的代码

代码语言:javascript
复制
#include <stdio.h>

// 交换两个整数的函数
// 参数:pa - 指向第一个整数的指针,pb - 指向第二个整数的指针
void Swap1(int* pa, int* pb)
{
    int tmp = 0;        // 定义临时变量用于交换
    tmp = *pa;          // 将pa指向的值赋给临时变量
    *pa = *pb;          // 将pb指向的值赋给pa指向的变量
    *pb = tmp;          // 将临时变量的值(原pa的值)赋给pb指向的变量
}

int main()
{
    int a = 0;          // 定义整型变量a并初始化为0
    int b = 0;          // 定义整型变量b并初始化为0

    // 从标准输入读取两个整数,分别存入a和b
    scanf_s("%d %d", &a, &b);

    // 打印交换前的a和b的值
    printf("交换前:a=%d b=%d\n", a, b);

    // 调用交换函数,传入a和b的地址
    Swap1(&a, &b);

    // 打印交换后的a和b的值
    printf("交换后:a=%d b=%d\n", a, b);

    return 0;           // 程序正常结束
}
在这里插入图片描述
在这里插入图片描述

我们发现Swap顺利完成了任务

传址调用,可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量

  • 所以未来函数中只是需要主调函数中的变量值来实现计算,就可以采用传值调用。
  • 如果函数内部要修改主调函数中的变量的值,就需要传址调用。

指针:(十二)数组与指针

数组名与数组首地址

数组名的地址 == 数组首元素的地址


代码语言:javascript
复制
#include <stdio.h>

int main() {

	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };

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

	return 0;
}
在这里插入图片描述
在这里插入图片描述

数组地址的特例
  • sizeof(数组名):代表的是整个数组的大小,代表的是整个数组的大小,单位是字节
  • &数组名:代表的是整个数组的地址 整个数组大小和数组首地址大小还是有区别的
代码语言:javascript
复制
#include <stdio.h>
int main()
{
    // 声明并初始化一个包含10个整数的数组
    int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
    
    // 打印第一个元素的地址
    // &arr[0] 获取数组第一个元素的地址
    printf("&arr[0] = %p\n", &arr[0]);
    
    // 打印第一个元素地址加1后的地址
    // 由于是int指针,+1会移动sizeof(int)个字节(通常是4字节)
    printf("&arr[0]+1 = %p\n", &arr[0]+1);
    
    // 打印数组名(数组名在大多数情况下会退化为指向第一个元素的指针)
    printf("arr = %p\n", arr);
    
    // 打印数组名加1后的地址(同样移动sizeof(int)个字节)
    printf("arr+1 = %p\n", arr+1);
    
    // 打印整个数组的地址(虽然值相同,但类型不同)
    // &arr 的类型是 int(*)[10](指向10个整数数组的指针)
    printf("&arr = %p\n", &arr);
    
    // 打印整个数组地址加1后的地址
    // 这里会移动整个数组的大小(10 * sizeof(int) = 40字节)
    printf("&arr+1 = %p\n", &arr+1);
    
    return 0;
}
在这里插入图片描述
在这里插入图片描述

  • &arr[0]arr指向的是数组的首元素地址,+1移动4字节
  • &arr和sizeof(arr)是指向整个数组的大小,+1移动40个字节

使用数组传递指针
代码语言:javascript
复制
#include <stdio.h>

int main()
{
    int arr[10] = { 0 };  // 声明并初始化一个包含10个整数的数组,所有元素初始化为0
    
    // 计算数组长度
    int sz = sizeof(arr) / sizeof(arr[0]);
    
    // 输入部分
    printf("请输入 %d 个整数:\n", sz);  // 添加提示信息,提高用户体验
    
    int* p = arr;  // 定义指针p指向数组首地址
    
    for (int i = 0; i < sz; i++)  // 将i的声明移到循环内部,限制作用域
    {
        printf("请输入第 %d 个数:", i + 1);  // 添加序号提示
        scanf_s("%d", p + i);  // 使用指针算术访问数组元素
        // 等价写法:
        // scanf_s("%d", &arr[i]);      // 使用数组下标
        // scanf_s("%d", arr + i);      // 使用数组名指针算术
    }
    
    // 输出部分
    printf("\n您输入的数组是:\n");
    
    for (int i = 0; i < sz; i++)
    {
        printf("%d ", p[i]);  // 使用指针下标表示法输出
        // 等价写法:
        // printf("%d ", *(p + i));     // 使用指针解引用
        // printf("%d ", arr[i]);       // 使用数组下标
    }
    printf("\n");  // 换行使输出更美观
    
    return 0;
}

我们可以使用arr[i]访问数组元素,也可以使用p[i]访问数组元素

arr[i]等价于*(arr+i) p[i]等价于*(p+i)


一维数组传参的本质

首先先从一个问题引入:能不能将一个数组传递给函数,在这个函数内部算出数组的个数呢?

代码语言:javascript
复制
#include <stdio.h>

void test(int arr[]) {

	int len2 = sizeof(arr) / sizeof(arr[0]);
	printf("len2 = %d\n", len2);

}

int main() {
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int len1 = sizeof(arr) / sizeof(arr[0]);

	printf("len1 = %d\n", len1);
	test(arr);

	return 0;
}

发现函数内部并没有正确得出数组元素个数

在这里插入图片描述
在这里插入图片描述

关键点:

  • 数组作为函数参数时会退化为指针
  • sizeof在编译时确定大小,无法在运行时获取通过指针传递的数组长度 在函数内部我们写sizeof(arr)实际上计算的是数组地址的大小,而不是数组的大小

总结:

  • 一维数组传参 形参可以是指针形式也可以是数组形式

指针数组

我们类比一下 整型数组是存放整型变量的数组 字符数组是存饭字符变量的数组 所以指针数组便是存放指针数据的数组

指针数组的每一个元素都是地址,并指向一块区域

在这里插入图片描述
在这里插入图片描述
指针数组模拟二维数组
代码语言:javascript
复制
#include <stdio.h>

int main() {
    // 完整初始化所有数组元素
    int arr1[3] = { 1, 2, 3 };
    int arr2[4] = { 11, 22, 33, 44 };    // 添加第4个元素
    int arr3[5] = { 111, 222, 333, 444, 555 }; // 添加第4、5个元素

    int* arr[3] = { arr1, arr2, arr3 };

    // 定义每个子数组的实际长度
    int lengths[3] = {
        sizeof(arr1) / sizeof(arr1[0]),  // 3
        sizeof(arr2) / sizeof(arr2[0]),  // 4  
        sizeof(arr3) / sizeof(arr3[0])   // 5
    };

    printf("指针数组模拟不规则二维数组:\n\n");

    for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) {
        printf("第%d行 (长度=%d):\n", i+1, lengths[i]);
        for (int j = 0; j < lengths[i]; j++) {
            printf("  arr[%d][%d] = %d\n", i, j, arr[i][j]);
        }
        printf("\n");
    }

    return 0;
}

arr[i]是访问arr数组的元素并指向整型一维数组 arr[][]就是访问整型一维数组中的元素


数组指针变量

指针数组是数组 那数组指针变量便是指针变量

数组指针变量存放的是数组的地址,能够指向数组的指针变量


数组指针变量的定义方式
代码语言:javascript
复制
// 指向整型数组的指针
int (*ptr)[10];  // ptr是指向包含10个整数的数组的指针

解释:ptr先和*结合,说明p是⼀个指针变量,然后指针指向的是⼀个大小为10个整型的数组。所以ptr是一个指针,指向一个数组,叫数组指针。

代码语言:javascript
复制
// 指向字符数组的指针  
char (*cptr)[20]; // cptr是指向包含20个字符的数组的指针

初始化数组指针
代码语言:javascript
复制
#include <stdio.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};  // 定义一个包含5个整数的数组
    
    // 定义并初始化数组指针
    // int (*ptr)[5] 表示ptr是一个指针,指向包含5个整数的数组
    // &arr 获取的是整个数组的地址,而不是第一个元素的地址
    int (*ptr)[5] = &arr;  // 注意:取整个数组的地址
    
    // 打印数组的首地址(整个数组的地址)
    printf("数组地址: %p\n", &arr);
    // 打印指针变量ptr存储的地址值(应该与&arr相同)
    printf("指针值: %p\n", ptr);
    // 解引用ptr得到数组本身,然后通过[0]访问第一个元素
    printf("第一个元素: %d\n", (*ptr)[0]);
    
    return 0;
}

我们通过调试也能发现&arrp的类型是完全一致的


数组指针与指针数组的区别
  • 数组指针 int (*p)[5] ————指向整个数组的指针
  • 指针数组 int *p[5]————包含5个整型指针的数组

特性

数组指针

指针数组

定义

int (*ptr)[n]

int *ptr[n]

本质

指向数组的指针

存储指针的数组

内存占用

1个指针的大小

n个指针的大小

指针运算

以整个数组为单位

以指针大小为单位

主要用途

处理多维数组

存储多个地址/字符串


二维数组传参本质

数组退化为指针 二维数组作为函数参数传递时,会退化为指向数组首元素的指针

代码语言:javascript
复制
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < r; i++)
	{
		for (j = 0; j < c; j++)
		{
			printf("%d ", *(*(p + i) + j));
		}
		printf("\n");
	}
}
int main()
{
	int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7} };
	test(arr, 3, 5);
	return 0;
}

总结:二维数组传参,形参部分可以是数组形式,也可以是指针形式


指针:(十三)二级指针

指针变量也是变量,也有自己的地址 二级指针变量是用来存放一级指针变量的地址的!

二级指针与一级指针的区别

一级指针

代码语言:javascript
复制
int a = 10;
int *pa = &a;    //pa是指针变量,pa是一级指针

pa指向的对象是int类型

二级指针

代码语言:javascript
复制
int **ppa = &pa;    //ppa是二级指针变量

ppa指向的对象是int*类型的

在这里插入图片描述
在这里插入图片描述

二级指针的运算
代码语言:javascript
复制
#include <stdio.h>

int main() {
    int a = 10;
    int *p = &a;
    int **pp = &p;
    
    printf("变量a的值: %d\n", a);
    printf("变量a的地址: %p\n", &a);
    
    printf("\n一级指针p:\n");
    printf("p的值(指向的地址): %p\n", p);
    printf("p的地址: %p\n", &p);
    printf("*p的值: %d\n", *p);
    
    printf("\n二级指针pp:\n");
    printf("pp的值(指向的地址): %p\n", pp);
    printf("pp的地址: %p\n", &pp);
    printf("*pp的值(即p的值): %p\n", *pp);
    printf("**pp的值(即a的值): %d\n", **pp);
    
    return 0;
}
在这里插入图片描述
在这里插入图片描述

指针:(十四)字符指针变量

在指针中,有一种指针类型叫做字符类型

定义方式:

代码语言:javascript
复制
char *str;  // 声明一个字符指针变量

字符指针的三种使用方式
  • 方式一:指向单个字符变量
代码语言:javascript
复制
char ch = 'w';
char *pc = &ch;

pc 指向字符变量ch 可以修改 *pc 的值(即修改 ch 的值) pc 存储的是变量 ch 的地址

  • 方式二:指向字符数组
代码语言:javascript
复制
char str = "abcdef";
char *ps = str;		//数组名就是数组首元素的地址

  • arr 是在栈上分配的字符数组,包含7个字符符:'a','b','c','d','e','f','\0'
  • pc 指向数组的首元素 arr[0]
  • 可以修改数组内容:pc[0] = 'A' 或 arr[0] = 'A'
  • 方式三:指向字符串字面量
代码语言:javascript
复制
char* pc = "abcdef";

  • "abcdef" 是字符串字面量,存储在只读内存区域
  • pc存储的是字符串首字符 ‘a’ 的地址
  • 不能修改内容:pc[0] = 'A' 会导致运行时错误

字符指针注意事项
代码语言:javascript
复制
int main()
{
 	const char* pstr = "hello bit.";//这⾥是把⼀个字符串放到pstr指针变量⾥了吗?
	printf("%s\n", pstr);    //%s读取的是地址
return 0;
}

代码 const char* pstr = "hello bit."; 特别容易让同学以为是把字符串hello bit放到字符指针pstr里了,但是本质是把字符串 hello bit. 首字符的地址放到了pstr中。

在这里插入图片描述
在这里插入图片描述

指针:(十五)函数指针变量

整数指针是用来存放整数数据类型的,数组指针是用来存放数组的,那函数指针呢?

代码语言:javascript
复制
#include <stdio.h>

void test(){};

int main(){

	printf("test: %p\n", test);
	printf("&test: %p\n", &test);
	return 0;

}
在这里插入图片描述
在这里插入图片描述

所以我们看到函数其实是有地址的,函数名就是函数的地址 我们可以通过&函数名来调用函数的地址


函数指针的声明与定义方式

声明格式:

代码语言:javascript
复制
返回值类型 (*指针变量名)(参数类型) = 函数名或者&函数名;

定义方式:

代码语言:javascript
复制
#include <stdio.h>

// 定义一个函数
int add(int a, int b) {
    return a + b;
}

int main() {
    // 定义函数指针并初始化
    int (*func_ptr)(int, int) = add;
    
    // 通过函数指针调用函数
    int result = func_ptr(3, 5);
    printf("3 + 5 = %d\n", result);  // 输出: 8
    
    return 0;
}

函数指针类型解析
代码语言:javascript
复制
int (* pf3) (int x, int y)
|       |       ------------
|       |            |
|       |            pf3指向函数的参数类型和个数的交代
|      函数指针变量名
pf3指向函数的返回类型

int (*) (int x, int y) //pf3函数指针变量的类型

函数指针使用示例
代码语言:javascript
复制
#include <stdio.h>  // 需要包含头文件以使用printf

// 声明一个无参数无返回值的函数
void test()
{
    printf("hehe\n");
}

// 函数指针的声明和初始化:
// 方式1:使用取地址运算符&(&是可选的,因为函数名会被隐式转换为函数地址)
void (*pf1)() = &test;  // 正确:使用&test显式获取函数地址
void (*pf2)() = test;   // 正确:函数名test会隐式转换为函数地址

// 声明一个带参数的函数
int Add(int x, int y)
{
    return x + y;
}

// 函数指针的声明和初始化:
// 方式1:不使用参数名(只有类型)
int (*pf3)(int, int) = Add;     // 正确:函数名隐式转换为函数地址

// 方式2:使用参数名(参数名会被编译器忽略,只有类型信息有效)
int (*pf4)(int x, int y) = &Add;  // 正确:使用&Add显式获取函数地址

// 注意:不能重复定义同名的函数指针变量(原代码中pf3被定义了两次)
// 因此将第二个改为pf4

int main()
{
    // 测试函数指针调用
    pf1();  // 输出: hehe
    pf2();  // 输出: hehe
    
    printf("%d\n", pf3(2, 3));   // 输出: 5
    printf("%d\n", pf4(5, 7));   // 输出: 12
    
    return 0;
}

函数指针的两段有趣的代码

查看blog


typedef关键字

查看blog


指针:(十六)函数指针数组

函数指针数组是存储多个函数指针的数组,常用于实现回调机制、状态机、命令模式等


基本语法和声明

使用 typedef 定义函数指针类型

代码语言:javascript
复制
typedef 返回类型 (*函数指针类型名)(参数列表);

声明函数指针数组

代码语言:javascript
复制
函数指针类型名 数组名[大小];

函数指针数组的用途:转移表

我们先看最简单的计算器的实现

代码语言:javascript
复制
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    
    do
    {
        printf("*************************\n");
        printf(" 1:add   2:sub \n");
        printf(" 3:mul   4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        
        switch (input)
        {
            case 1:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = add(x, y);
                printf("ret = %d\n", ret);
                break;
            case 2:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = sub(x, y);
                printf("ret = %d\n", ret);
                break;
            case 3:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = mul(x, y);
                printf("ret = %d\n", ret);
                break;
            case 4:
                printf("输入操作数:");
                scanf("%d %d", &x, &y);
                ret = div(x, y);
                printf("ret = %d\n", ret);
                break;
            case 0:
                printf("退出程序\n");
                break;
            default:
                printf("选择错误\n");
                break;
        }
    } while (input);
    
    return 0;
}

利用函数指针数组(转移表)制作

代码语言:javascript
复制
#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表
    
    do
    {
        printf("*************************\n");
        printf(" 1:add 2:sub \n");
        printf(" 3:mul 4:div \n");
        printf(" 0:exit \n");
        printf("*************************\n");
        printf("请选择:");
        scanf("%d", &input);
        
        if ((input <= 4 && input >= 1))
        {
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            ret = (*p[input])(x, y);
            printf("ret = %d\n", ret);
        }
        else if(input == 0)
        {
            printf("退出计算器\n");
        }
        else
        {
            printf("输入有误\n");
        }
    } while (input);
    
    return 0;
}

指针:(十七)回调函数

回调函数是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。 简单来讲:就是通过函数指针调用的函数 回调函数不是由该函数的实现方直接调⽤,⽽是在特定的事件或条 件发⽣时由另外的⼀⽅调⽤的,⽤于对该事件或条件进⾏响应。


多说无益,我们看一下下面这段代码

代码语言:javascript
复制
// 1. 定义回调函数类型
typedef void (*SimpleCallback)(int);

// 2. 具体回调函数实现
void print_number(int num) {
    printf("数字: %d\n", num);
}

void square_number(int num) {
    printf("%d的平方: %d\n", num, num * num);
}

// 3. 接收回调的函数
void process_number(int num, SimpleCallback callback) {
    printf("处理数字 %d...\n", num);
    callback(num); // 调用回调
}

int main() {
    process_number(5, print_number);
    process_number(5, square_number);
    return 0;
}

代码分析:

  1. main函数起手,调用process_number函数并传入两个参数
  2. 再看process_number函数,参数5用到了第一个输出语句,第二个形参传入了print_number函数指针(退化成指针),给print_number重命名,再调用callback函数
  3. 后面我们要在函数里面调用其他函数,只需要利用回调函数就可以了

下面我们用回调函数编写简易计算器

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>

// 定义函数指针类型,用于表示所有计算类型的函数
// 所有计算函数都要传入两个double类型的参数,返回一个double类型的结果
typedef double (*Callback_Function)(double, double);

// 加法函数
double add(double a, double b) {
    return a + b;
}

// 减法函数
double subtract(double a, double b) {
    return a - b;
}

// 乘法函数
double multiply(double a, double b) {
    return a * b;
}

// 除法函数
double divide(double a, double b) {
    // 检查除数是否为零,避免除零错误
    if (b != 0) {
        // 除数不为零,执行除法运算并返回结果
        return a / b;
    }
    else {
        // 除数为零,打印错误信息
        printf("Error: Division by zero!\n");
        // 终止程序执行,返回失败状态
        exit(EXIT_FAILURE);
    }
}

/**
 * 计算函数 - 通过回调函数执行具体运算
 * @param a 第一个操作数
 * @param b 第二个操作数
 * @param callback 指向具体计算函数的指针
 * @return double 计算结果
 */
double calculation(double a, double b, Callback_Function callback) {
    return callback(a, b);
}

/**
 * 显示计算器菜单
 */
void DisplayMenu() {
    printf("\n=== 简易计算器 ===\n");
    printf("1. 加法 (+)\n");
    printf("2. 减法 (-)\n");
    printf("3. 乘法 (*)\n");
    printf("4. 除法 (/)\n");
    printf("5. 退出\n");
    printf("请输入您的选择 (1-5): ");
}

int main() {
    int choice;
    double num1, num2, result; // 修正:改为double类型以匹配函数参数

    // 定义函数指针数组,用来存储所有可能的计算操作
    // 数组顺序与菜单选项顺序对应(加法=0,减法=1,乘法=2,除法=3)
    Callback_Function callbacks[] = { add, subtract, multiply, divide };

    while (1) {
        DisplayMenu();
        scanf_s("%d", &choice);

        // 清除输入缓冲区,避免后续输入问题
        while (getchar() != '\n');

        // 检查用户是否选择退出系统
        if (choice == 5) {
            printf("感谢使用计算器,下次再见!\n");
            break;
        }

        // 验证输入是否有效
        if (choice < 1 || choice > 4) {
            printf("您输入的选项无效,请重新输入。\n");
            continue;
        }

        // 获取用户输入的操作数
        printf("请输入第一个数字: ");
        scanf_s("%lf", &num1); // 使用%lf读取double类型

        printf("请输入第二个数字: ");
        scanf_s("%lf", &num2); // 使用%lf读取double类型

        // 清除输入缓冲区
        while (getchar() != '\n');

        // 使用回调函数执行计算
        // callbacks[choice-1] 根据用户选择获取相应的函数指针
        // 例如:选择1 → callbacks[0] (add函数)
        //        选择2 → callbacks[1] (subtract函数)
        result = calculation(num1, num2, callbacks[choice - 1]);

        // 显示计算结果,保留两位小数
        printf("计算结果: %.2lf\n", result);
    }

    return 0;
}

qsort函数

qsort是C标准库<stdlib.h>中提供的一个通用排序函数,它使用快速排序(QuickSort)算法(注意:C标准并不强制要求具体实现方式,但通常确实是高效的快速排序)来对任意类型的数据进行排序。

语法格式
代码语言:javascript
复制
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*));

参数列表: 参数1: void *base

  • 类型: void指针(通用指针)
  • 作用: 指向要排序数组的起始地址

参数2: size_t nitems

  • 类型: size_t(无符号整型,通常是unsigned long)
  • 作用: 指定数组中元素的数量

参数3: size_t size

  • 类型: size_t
  • 作用: 指定数组中每个元素的大小(字节数) 如何获取:使用sizeof运算符 如何获取:通常用sizeof(array) / sizeof(array[0])计算

参数4:int (*compar)(const void *, const void*)

  • 类型:函数指针
  • 作用:指向比较函数的指针

分解:

  • *compar:函数指针变量名
  • (const void *, const void*):比较函数的参数列表
  • int:比较函数的返回类型

qsort函数模拟实现

下面我们利用qsort函数对整型数据进行排列

代码语言:javascript
复制
#include <stdio.h>// 包含标准输入输出头文件,用于printf函数
#include <stdlib.h>// 包含标准库头文件,qsort函数实际上在这里定义(原代码缺失)

// qsort函数的使⽤者得实现⼀个⽐较函数
// int_cmp: 整型比较函数
// 参数:p1, p2 - 指向要比较的两个元素的void指针
// 返回值:负数(p1<p2),0(p1==p2),正数(p1>p2)
int int_cmp(const void* p1, const void* p2) {
    int a = *(int*)p1;
    int b = *(int*)p2;
    if (a < b) return -1;
    if (a > b) return 1;
    return 0;
    // 1. 将void*转换为int*类型指针
    // 2. 解引用获取实际整数值
    // 3. 用p1指向的值减去p2指向的值
    //    - 如果结果为负:p1 < p2,返回负数 → 升序排列
    //    - 如果结果为0:p1 == p2,返回0
    //    - 如果结果为正:p1 > p2,返回正数
}

int main() {
    // 定义并初始化一个整型数组
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;  // 循环计数器

    // 调用qsort函数对数组进行排序
    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), int_cmp);
    // 参数1: arr - 要排序的数组的起始地址
    // 参数2: sizeof(arr) / sizeof(arr[0]) - 计算数组元素个数
    //        sizeof(arr): 整个数组的字节大小 (10个int × 4字节 = 40字节)
    //        sizeof(arr[0]): 第一个元素的字节大小 (4字节)
    //        40 / 4 = 10个元素
    // 参数3: sizeof(int) - 每个元素的大小(4字节)
    // 参数4: int_cmp - 比较函数的指针(函数名就是函数指针)
   
    // 打印排序后的数组    
    for(i = 0;i<sizeof(arr)/sizeof(arr[0]);i++){
        
        printf("%d ", arr[i]);
        // 依次输出每个元素
    }

    printf("\n");// 换行

    return 0;// 程序正常结束
}

利用冒泡排序实现qsort函数
代码语言:javascript
复制
#include <stdio.h>

// 整型比较函数
// 参数:p1, p2 - 指向要比较的两个元素的void指针
// 返回值:负数(p1<p2),0(p1==p2),正数(p1>p2)
int int_cmp(const void *p1, const void *p2)
{
    // 将void指针转换为int指针,然后解引用获取整数值
    // 用p1指向的值减去p2指向的值实现升序排序
    return (*(int *)p1 - *(int *)p2);
}

// 通用交换函数
// 参数:p1, p2 - 指向要交换的两个元素的指针
//        size - 每个元素的大小(字节数)
void _swap(void *p1, void *p2, int size)
{
    int i = 0;
    // 逐字节交换两个元素的内容
    for (i = 0; i < size; i++)
    {
        // 将指针转换为char*类型进行字节级操作
        // 临时保存p1的第i个字节
        char tmp = *((char *)p1 + i);
        // 将p2的第i个字节复制到p1
        *((char *)p1 + i) = *((char *)p2 + i);
        // 将临时保存的字节复制到p2
        *((char *)p2 + i) = tmp;
    }
}

// 通用冒泡排序函数
// 参数:base   - 指向数组起始位置的指针
//        count  - 数组中元素的数量
//        size   - 每个元素的大小(字节数)
//        cmp    - 比较函数的指针
void bubble(void *base, int count, int size, int (*cmp)(void *, void *))
{
    int i = 0;
    int j = 0;
    
    // 外层循环:控制排序的轮数
    // 每完成一轮,最大的元素就会"冒泡"到末尾
    for (i = 0; i < count - 1; i++)
    {
        // 内层循环:进行相邻元素的比较
        // count-i-1:每轮后,末尾的i个元素已经有序,不需要再比较
        for (j = 0; j < count - i - 1; j++)
        {
            // 计算当前元素和下一个元素的地址:
            // (char *)base + j*size      → 第j个元素的地址
            // (char *)base + (j+1)*size  → 第j+1个元素的地址
            
            // 使用比较函数比较相邻元素
            if (cmp((char *)base + j * size, (char *)base + (j + 1) * size) > 0)
            {
                // 如果顺序错误,交换两个元素
                _swap((char *)base + j * size, (char *)base + (j + 1) * size, size);
            }
        }
    }
}

int main()
{
    // 定义并初始化测试数组
    int arr[] = {1, 3, 5, 7, 9, 2, 4, 6, 8, 0};
    int i = 0;
    
    // 计算数组元素个数
    int element_count = sizeof(arr) / sizeof(arr[0]);
    
    // 调用通用冒泡排序函数
    // 参数1: arr - 数组起始地址
    // 参数2: element_count - 元素个数 (10)
    // 参数3: sizeof(int) - 每个元素的大小 (4字节)
    // 参数4: int_cmp - 比较函数指针
    bubble(arr, element_count, sizeof(int), int_cmp);
    
    // 打印排序后的数组
    printf("排序后的数组: ");
    for (i = 0; i < element_count; i++)
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
    
    return 0;
}
文章总结

本文涵盖了C语言指针95%的内容,剩余的一些是项目实战问题。我也会出一篇文章来讲解数组与指针的笔试强训 如果你觉得这篇文章对你学习指针帮助,请给文章一个三连吧

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 内存的基本概念
  • 地址的基本概念
  • 指针的基本概念
    • 指针:(一)指针的定义与声明
    • 指针:(二)关键运算符
      • (1)取地址运算符&
      • (2)解引用运算符*
    • 指针:(三)指针初始化
    • 指针:(四)指针大小
    • 指针:(五)指针变量
    • 指针:(六)指针运算
      • 指针±整数
      • 指针 - 指针
    • 指针:(七)void* 指针
      • void指针的用处
    • 指针:(八)指针与const
      • (1)指向常量的指针 (const int *ptr)
      • (2)常量指针(int *const ptr)
      • (3)指向常量的常量指针(const int *const ptr)
    • 指针:(九)野指针
      • 野指针成因(1):指针未初始化
      • 野指针成因(2):指针越界访问
      • 野指针成因(3):指针指向的空间释放
    • 指针:(十)assert断言
      • 定义assert的规则
      • assert的工作原理
      • assert的缺点
    • 指针:(十一)指针的使用和传值调用
      • (1)strlen的模拟实现
      • (2)传值调用和传址调用
    • 指针:(十二)数组与指针
      • 数组名与数组首地址
      • 数组地址的特例
      • 使用数组传递指针
      • 一维数组传参的本质
      • 指针数组
      • 数组指针变量
      • 数组指针与指针数组的区别
      • 二维数组传参本质
    • 指针:(十三)二级指针
      • 二级指针与一级指针的区别
      • 二级指针的运算
    • 指针:(十四)字符指针变量
      • 字符指针的三种使用方式
      • 字符指针注意事项
    • 指针:(十五)函数指针变量
      • 函数指针的声明与定义方式
      • 函数指针类型解析
      • 函数指针使用示例
      • 函数指针的两段有趣的代码
      • typedef关键字
    • 指针:(十六)函数指针数组
      • 基本语法和声明
      • 函数指针数组的用途:转移表
    • 指针:(十七)回调函数
      • qsort函数
      • qsort函数模拟实现
      • 利用冒泡排序实现qsort函数
      • 文章总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档