目录
一. 基础篇
1. 指针基础
指针是一个存储了内存地址的变量。它指向内存中的某个位置,允许程序直接访问那个位置的数据。在C语言中,声明指针使用"*"符号。
int *ptr; // 声明一个指向整数的指针
指针理解的2个要点:
1. 指针是内存中一个最小单元的编号,也就是地址
2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量
那我们就可以这样理解:
内存
指针变量:用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
指针基础拓展内容:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。
- 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
2. 指针与地址
指针存储的是内存地址,通过取地址操作符&
可以获取一个变量的地址。例如:
int num = 10;
int *ptr = # // 将ptr指向num的地址
3.指针和指针类型
我们都知道,变量有不同的类型,整形,浮点型等。那指针也是有类型的.
当有这样的代码时:
int num = 10;
p = #
要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们给指针变量相应的类型。
char *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
这里可以看到,指针的定义方式是: type + * 。 其实: char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。 int* 类型的指针是为了存放 int 类型变量的地址。
那么指针类型的意义是什么呢? 我们稍后再说,我们先来看看下一个知识点
4. 指针的解引用
解引用操作符"*"用于访问指针所指向地址的值。
int num = 10;
int *ptr = #
printf("%d", *ptr); // 输出10
//演示实例
#include <stdio.h>
int main()
{
int n = 0x11223344;
char *pc = (char *)&n;
int *pi = &n;
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}
5.指针运算
5.1指针+-整数
#define N_VALUES 5
float values[N_VALUES];
float *vp;
//指针+-整数;指针的关系运算
for (vp = &values[0]; vp < &values[N_VALUES];)
{
*vp++ = 0;
}
循环的主体部分设置当前指针所指向的数组元素为0,然后递增指针 vp
,使其指向下一个数组元素的地址。这样循环会在每次迭代中初始化一个数组元素,并且指针 vp
会逐渐移动到下一个元素。
5.2指针-指针
int my_strlen(char *s)
{
char *p = s; // 声明一个指针p,初始化为指向传入的字符串s的首地址
while (*p != '\0') // 当指针p所指向的字符不是字符串的结束符'\0'时执行循环
p++; // 指针p递增,指向下一个字符,直到遇到字符串结束符为止
return p - s; // 返回指针p指向的地址与传入字符串的首地址之间的偏移量,即字符串的长度
}
这个函数的核心思想是,通过移动指针 p
来遍历字符串,直到遇到字符串的结束符 '\0'
。然后,返回指针 p
指向的地址与传入字符串的首地址之间的偏移量,即字符串的长度。
5.3指针的关系运算
for(vp = &values[N_VALUES]; vp > &values[0];)
{
*--vp = 0;
}
可以简化为
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
*vp = 0;
}
-
vp = &values[N_VALUES-1];
:初始化指针vp
,使其指向数组values
的最后一个元素。 -
vp >= &values[0];
:循环条件,保证vp
不小于数组values
的第一个元素的地址。 -
vp--
:每次循环迭代时,递减指针vp
,使其指向前一个数组元素的地址。 -
*vp = 0;
:循环的主体部分,将当前指针vp
所指向的数组元素设置为0。
通过这段代码,数组 values
中的所有元素都会被初始化为0。
实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。
6.指针和数组
先来看一个例子:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
运行结果为
可见数组名和数组首元素的地址是一样的。
我们可以得出结论:数组名表示的是数组首元素的地址.(sizeof操作符与&+数组名两种方式除外)
那么这样写代码是可行的:
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。 例如:
#include <stdio.h>
int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr)/sizeof(arr[0]);
int i = 0;
for(i=0; i<sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p+i);
}
return 0;
}
运行结果为:
所以 p+i 其实计算的是数组 arr 下标为i的地址。 那我们就可以直接通过指针来访问数组。 如下:
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int *p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i<sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
7.二级指针
指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里?答: 存在二级指针(变量)里
对于二级指针的运算有:
- *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .
int b = 20;
*ppa = &b;//等价于 pa = &b;
- **ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a .
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
8.指针数组
指针数组是指针还是数组? 答案:是数组。是存放指针的数组。
数组我们已经知道整形数组,字符数组。
int arr1[5];
char arr2[6];
那我们来看看指针数组是什么样的呢?
int* arr3[5];//是什么?
arr3是一个数组,有五个元素,每个元素是一个整形指针。
二.进阶
1.字符指针
在指针的类型中我们知道有一种指针类型为字符指针 char*
一般使用:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
还有一种使用方式如下:
int main()
{
const char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
代码 const char* pstr = "hello world.";
特别容易让同学以为是把字符串 "hello world."放到字符指针 pstr 里了,但是本质是把字符串"hello world."首字符的地址放到了pstr中。
上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。
2.指针数组
2.1 数组指针的定义
数组指针是指针?还是数组? 答案是:指针。
我们已经熟悉: 整形指针: int * pint; 能够指向整形数据的指针。 浮点型指针: float * pf; 能够指向浮点型数据的指针。
那数组指针应该是:能够指向数组的指针。 下面代码哪个是数组指针?
int *p1[10];
int (*p2)[10];
//p1, p2分别是什么?
解释:
int (*p)[10];
//解释:p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。
//所以p是一个指针,指向一个数组,叫数组指针。
//这里要注意:[]的优先级要高于*号的,所以必须加上()来保证p先和*结合。
2.2 &数组名VS数组名
对于下面的数组:
int arr[10];
arr与&arr分别代表什么?
我们知道arr是数组名,数组名表示数组首元素的地址。
那&arr数组名到底是啥?
#include <stdio.h>
int main()
{
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
return 0;
}
运行结果为:
可见数组名和&数组名打印的地址是一样的。 难道两个是一样的吗?
我们再来看一串代码
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("arr = %p\n", arr);
printf("&arr= %p\n", &arr);
printf("arr+1 = %p\n", arr+1);
printf("&arr+1= %p\n", &arr+1);
return 0;
}
运行结果为:
根据上面的代码我们发现,其实&arr和arr,虽然值是一样的,但是意义应该不一样的。 实际上: &arr 表示的是数组的地址,而不是数组首元素的地址。(细细体会一下)
本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型 数组的地址+1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是40
2.3数组指针的使用
那数组指针是怎么使用的呢?
既然数组指针指向的是数组,那数组指针中存放的应该是数组的地址。 看代码:
#include <stdio.h>
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int (*p)[10] = &arr;//把数组arr的地址赋值给数组指针变量p
//但是我们一般很少这样写代码
return 0;
}
一个数组指针的使用:
#include <stdio.h>
void print_arr1(int arr[3][5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
void print_arr2(int (*arr)[5], int row, int col)
{
int i = 0;
for(i=0; i<row; i++)
{
for(j=0; j<col; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
print_arr1(arr, 3, 5);
//数组名arr,表示首元素的地址
//但是二维数组的首元素是二维数组的第一行
//所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
//可以数组指针来接收
print_arr2(arr, 3, 5);
return 0;
}
学了指针数组和数组指针我们来一起回顾并看看下面代码的意思:
int arr[5];
int *parr1[10];
int (*parr2)[10];
int (*parr3[10])[5];
留给大家当作思考
3.数组参数,指针参数
3.1一维数组传参
#include <stdio.h>
void test(int arr[])//ok? OK!
{}
void test(int arr[10])//ok? OK!
{}
void test(int *arr)//ok? OK!
{}
void test2(int *arr[20])//ok? OK!
{}
void test2(int **arr)//ok? OK!
{}
int main()
{
int arr[10] = {0};
int *arr2[20] = {0};
test(arr);
test2(arr2);
}
3.2二维数组传参
void test(int arr[3][5])//ok? OK!
{}
void test(int arr[][])//ok? NO!
{}
void test(int arr[][5])//ok? OK!
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。
//因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素。
//这样才方便运算。
void test(int *arr)//ok? NO!
{}
void test(int* arr[5])//ok? NO!
{}
void test(int (*arr)[5])//ok? OK!
{}
void test(int **arr)//ok? OK!
{}
int main()
{
int arr[3][5] = {0};
test(arr);
}
3.3一级指针传参
#include <stdio.h>
void print(int *p, int sz)
{
int i = 0;
for(i=0; i<sz; i++)
{
printf("%d\n", *(p+i));
}
}
int main()
{
int arr[10] = {1,2,3,4,5,6,7,8,9};
int *p = arr;
int sz = sizeof(arr)/sizeof(arr[0]);
//一级指针p,传给函数
print(p, sz);
return 0;
}
思考:当一个函数的参数部分为一级指针的时候,函数能接收什么参数?
例如:
void test1(int *p)
{}
//test1函数能接收什么参数?
void test2(char* p)
{}
//test2函数能接收什么参数?
3.4二级指针传参
#include <stdio.h>
void test(int** ptr)
{
printf("num = %d\n", **ptr);
}
int main()
{
int n = 10;
int*p = &n;
int **pp = &p;
test(pp);
test(&p);
return 0;
}
思考: 当函数的参数为二级指针的时候,可以接收什么参数?
例如:
void test(char **p)
{
}
int main()
{
char c = 'b';
char*pc = &c;
char**ppc = &pc;
char* arr[10];
test(&pc);
test(ppc);
test(arr);//Ok?
return 0;
}
4.函数指针
首先看一段代码:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出结果为:
输出的是两个地址,这两个地址是 test 函数的地址。 那我们的函数的地址要想保存起来,怎么保存? 下面我们看代码:
void test()
{
printf("hehe\n");
}
//下面pfun1和pfun2哪个有能力存放test函数的地址?
void (*pfun1)();
void *pfun2();
首先,能给存储地址,就要求pfun1或者pfun2是指针,那哪个是指针? 答案是:
pfun1可以存放。pfun1先和*结合,说明pfun1是指针,指针指向的是一个函数,指向的函数无参数,返回值类型为void。
5.函数指针数组
数组是一个存放相同类型数据的存储空间,那我们已经学习了指针数组, 比如:
int *arr[10];
//数组的每个元素是int*
在C语言中,函数指针数组是一个数组,其中的每个元素都是一个函数指针。函数指针指向一个函数,允许你通过调用指针来调用相应的函数。
下面是一个简单的示例,演示如何声明和使用函数指针数组:
#include <stdio.h>
// 定义两个简单的函数
void add(int a, int b) {
printf("Sum: %d\n", a + b);
}
void subtract(int a, int b) {
printf("Difference: %d\n", a - b);
}
int main() {
// 声明一个函数指针数组,包含两个函数指针
void (*func_ptr_arr[2])(int, int) = {add, subtract};
int choice, x, y;
printf("Enter 0 for addition, 1 for subtraction: ");
scanf("%d", &choice);
printf("Enter two numbers: ");
scanf("%d %d", &x, &y);
// 使用函数指针数组调用相应的函数
if (choice >= 0 && choice < 2) {
(*func_ptr_arr[choice])(x, y);
} else {
printf("Invalid choice\n");
}
return 0;
}
6.指向函数指针的数组
C语言中指向函数指针的数组是一个数组,其元素都是指向函数指针的指针。
这种数组中的每个元素指向一个函数指针,而函数指针又指向一个函数。这种结构可以用来实现多级间接调用函数的需求,通常在设计复杂的软件系统时会用到。
下面是一个示例,演示了如何声明和使用指向函数指针的数组:
#include <stdio.h>
// 定义两个简单的函数
void add(int a, int b) {
printf("Sum: %d\n", a + b);
}
void subtract(int a, int b) {
printf("Difference: %d\n", a - b);
}
int main() {
// 定义一个指向函数指针的数组
void (*func_ptr_arr[2])(int, int);
// 将函数的地址直接赋值给数组元素
func_ptr_arr[0] = add;
func_ptr_arr[1] = subtract;
int choice, x, y;
printf("Enter 0 for addition, 1 for subtraction: ");
scanf("%d", &choice);
printf("Enter two numbers: ");
scanf("%d %d", &x, &y);
// 使用指向函数指针的数组来间接调用函数
if (choice >= 0 && choice < 2) {
(*func_ptr_arr[choice])(x, y);
} else {
printf("Invalid choice\n");
}
return 0;
}
在这个例子中,我们定义了一个大小为2的函数指针数组 func_ptr_arr
。然后,我们直接将 add
和 subtract
函数的地址赋值给数组的两个元素。最后,我们使用用户的选择来间接调用相应的函数。
7.回调函数
回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当 这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调 用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。
回调函数通常用于事件处理、异步操作、错误处理和实现可重用性等方面。它们允许程序设计者编写更加灵活和模块化的代码,因为它们可以将特定的功能从主要的函数中分离出来,使得代码更易于维护和理解。
下面是一个简单的示例,演示了如何在C语言中使用回调函数:
#include <stdio.h>
// 声明一个回调函数类型
typedef void (*CallbackFunction)(int);
// 主函数,接受一个回调函数作为参数
void performOperation(int x, int y, CallbackFunction callback) {
int result = x + y;
// 调用回调函数,将结果传递给它
callback(result);
}
// 回调函数,用于打印结果
void printResult(int result) {
printf("Result: %d\n", result);
}
int main() {
int a = 5, b = 3;
// 调用主函数,传递回调函数作为参数
performOperation(a, b, printResult);
return 0;
}
在这个示例中,我们首先声明了一个回调函数类型 CallbackFunction
,它接受一个 int
类型的参数并返回 void
。然后我们定义了一个主函数 performOperation
,它接受两个整数和一个回调函数作为参数,在函数内部执行一些操作后调用回调函数。最后我们定义了一个回调函数 printResult
,用于打印结果。
在 main
函数中,我们调用了 performOperation
函数,传递了两个整数和 printResult
函数作为回调函数。在 performOperation
函数内部,它执行了加法操作,并将结果传递给回调函数 printResult
,这样结果就被打印出来了。
这就是回调函数在C语言中的基本用法。通过回调函数,我们可以实现函数之间的灵活交互,使得代码更加模块化和可维护。
三.结语
OK! 如果你看完整篇文章那么恭喜你, 你已经掌握了C语言指针的90%内容, 以下内容需要同学们在学习与工作中共同去探索, 书山有路勤为径, 学海无涯苦作舟 !
如果你感觉这篇文章对你有帮助, 请给一个免费的赞哦!