本文讲解C语言中的指针概念、指针变量、指针数组、数组指针、指针函数、函数指针等等、并且有应用与示例,篇幅较长,如果时间有限可以通过目录快速定位需要了解的部分,或者可以从头到尾系统的学习,有条件最好做示例实验能够加固印象;通过本文学习相信一定会对你有帮助。
二、概念 2.1 什么是指针:指针就像是一个“路标”,它指向内存中某个位置。具体来说,指针存储的是内存地址,而不是具体的数值或字符。通过指针,我们可以找到并操作这些内存位置中的数据。
2.2 内存地址与值:当我们声明一个变量时,计算机会给这个变量分配一块内存空间。这块内存有一个唯一的地址,就像房子有门牌号一样。而变量本身存储的是实际的数据(比如数值、字符等)。指针的作用就是保存这个内存地址,让我们可以通过地址找到并访问变量中的值。
2.3 声明与初始化:声明指针变量时,我们告诉编译器这个变量将用于存储另一个变量的地址。例:声明一个指针变量并让这个指针变量指向变量a的地址,如下:
int *p; // 声明一个指向整型的指针变量p int a = 10; // 声明一个变量并赋予初始值 p = &a; // 让p指向变量a的地址 2.4 解引用操作符 *:解引用操作符 * 用来获取指针所指向的内存位置中的值。换句话说,它帮助我们从指针指向的地方读取或修改数据。如下:
int a = 10; // 声明一个变量并赋予初值 int *p = &a; // p指向a printf("a的值是:%d\r\n", *p); // 输出a的值,即10 *p = 20; // 修改a的值为20 printf("a的值是:%d\r\n", a); // 输出a的值,即20 2.5 地址操作符 &:地址操作符 & 用来获取变量的内存地址。通过这个操作符,我们可以得到变量的地址,并将其赋值给指针变量。如下:
int a = 10; //声明一个变量 int *p = &a; // 获取a的地址并赋值给p printf("a的地址是:%p\r\n", p); // 输出a的地址 2.6 空指针 (NULL):空指针 (NULL) 是一个特殊的指针值,表示该指针不指向任何有效的内存地址。使用空指针可以避免程序试图访问无效的内存位置,从而防止出现错误。在声明指针但尚未赋值时,最好将其初始化为 NULL,以确保安全。如下:
int *p = NULL; // 初始化为NULL,表示p当前不指向任何地方 三、指针变量这里做一个指针变量与变量指针的一个说明,实际上它们的区别就是指针变量是用来存储指向别人的地址,因为它就是个变量;而变量指针就是一个有值的变量&出来的指针,它是指向自己的内容的首地址,这里比较容易理解混淆。
3.1 定义与声明:在概念那我们已经知道,指针变量用于存储内存地址,指针变量是一种特殊的变量,它的值是另一个变量的地址。换句话说,它存储的是内存地址。这里继续举例不同类型的声明,根据指针所指向的数据类型,我们可以声明不同类型的指针变量。声明时,需要在变量类型前加上 * 号。
int *ap; // 声明一个指向"整型"的指针变量 char *bp; // 声明一个指向"字符型"的指针变量 float *cp; // 声明一个指向"浮点型"的指针变量 struct Person { char name[50]; int age; }; struct Person *p; // 声明一个指向结构体Person的指针变量p int *p1, *p2, *p3; // 声明三个指向整型的指针变量 3.2 指针运算:指针可以进行算术运算,最常见的是递增和递减操作,在处理数组和字符串时特别有用。注意:下面的递增与递减实际上是递增类型的宽度,在3.4会详细说明。
/****************递增操作 (++) STA************************/ int arr[5] = {1, 2, 3, 4, 5};//定义一个数组 int *p = arr; // p指向数组的第一个元素 printf("%d\r\n", *p); // 输出1 p++; // p现在指向第二个元素 printf("%d\r\n", *p); // 输出2 /****************递增操作 (++) END************************/ /****************递减操作 (--) STA**********************/ p--; // p回到第一个元素 printf("%d\r\n", *p); // 输出1 /*****************递减操作 (--) END**********************/ /****************加减法操作 (+-) STA**********************/ p = arr; // p指向数组的第一个元素 p = p + 3; // p现在指向第4个元素 printf("%d\r\n", *p); // 输出4 p = p - 3; // p回到第一个元素 printf("%d\r\n", *p); // 输出1 /****************加减法操作 (+-) END**********************/ 3.3 指针比较:指针可以像普通变量一样进行比较,通常用于检查指针是否相等或是否为空。
/******************判断指针是否为空 STA********************/ int *p=NULL; if (p == NULL) { printf("指针p为空\r\n"); } else { printf("指针p不为空\r\n"); } /******************判断指针是否为空 END********************/ /************比较两个指针是否指向同一地址 STA***************/ int a = 10; int b = 20; int *p1 = &a; int *p2 = &b; if (p1 == p2) { printf("p1和p2指向同一个地址\r\n"); } else { printf("p1和p2指向不同的地址\r\n"); } /************比较两个指针是否指向同一地址 END***************/ /************比较指针大小 STA***************/ /*注意这仅适用于指向同一数组内的指针,数组内的成员地址是连续的*/ char str[] = "Hello"; char *p3 = str; char *p4 = str + 2; if (p3 < p4) { printf("p3的地址小于p4的地址\r\n"); } /************比较指针大小 STA***************/ 3.4 不同类型的指针:指针可以指向各种不同的数据类型,如整型、字符型、浮点型等。每个指针的类型决定了它能指向的数据类型以及如何解释所指向的数据。
int a = 10; int *pInt = &a; // pInt 是指向整型数据的指针 char c = 'A'; char *pChar = &c; // pChar 是指向字符型数据的指针 float f = 3.14f; float *pFloat = &f; // pFloat 是指向浮点型数据的指针 void *pVoid; // pVoid 可以指向任何类型的数据 3.5 类型转换与强制类型转换:不同类型的指针之间是可以互相转换的,但是这种转换必须谨慎进行,以避免未定义行为或数据损坏。
/*******************强行转换为任意类型 STA********************/ int a = 10; int *pInt = &a; void *pVoid = (void *)pInt; // 强制类型转换为 void*类型 printf("%d\r\n",*(int *)pVoid); /*******************强行转换为任意类型 END********************/ /*******************强行转换int类型 STA********************/ /*注意:这是危险操作!!!会造成数据丢失与错误*/ char a1 = 10; int *pInt1 = (int *)&a1; //强制转换为int*类型 printf("%d\r\n",*pInt1); /*输出1090457610 已经造成数据错误 pInt1 实际上指向了一块比 a1 占用内存更大的区域, 这可能会覆盖或涉及相邻的内存位置*/ /*******************强行转换为int类型 END********************/ /*******************强行转换char类型 STA********************/ /*注意:这是危险操作!!!会造成数据丢失与错误*/ int a2 = 10; char *pInt2 = (char *)&a2;//强制转换为char*类型 printf("%d\r\n",*pInt2); //输出10 a2 = 300; pInt2 = (char *)&a2; printf("%d\r\n",*pInt2); //输出44 已经造成数据错误 300-256=44 /*******************强行转换为char类型 END********************/ 3.6 多级指针:多级指针是指向其他指针的指针。最常见的是二级指针,即“指针的指针”,它存储了一个指针的地址。三级指针则进一步存储了二级指针的地址,以此类推。
/**************************二级指针 STA***********************************/ int a = 10; int *pInt = &a; // pInt 是指向整型的指针 int **ppInt = &pInt; // ppInt 是指向 pInt 的指针,也就是二级指针 printf("%d\r\n",**ppInt); // 输出 10 /**************************二级指针 END***********************************/ /**************************三级指针 STA***********************************/ int ***pppInt = &ppInt; // pppInt 是指向 ppInt 的指针,即三级指针 printf("%d\r\n",***pppInt);// 输出10 /**************************三级指针 END***********************************/ 四、指针数组 4.1 定义与声明:指针数组的声明方式类似于普通数组,但每个元素的数据类型是某种类型的指针。
int *ptrArray[10]; // 声明一个包含10个整型指针的数组 char *strArray[] = {"Hello", "World", "C", "Programming"}; // 字符串指针数组 4.2 访问指针数组中的元素:访问指针数组中的元素和访问普通数组类似,使用方括号[]操作符来指定索引位置。由于每个元素本身是一个指针,因此需要解引用(通过*)这些指针来获取实际的数据值。
char *strArray[] = {"Hello", "World", "C", "Programming"}; // 字符串指针数组 printf("%s\r\n", strArray[0]); //访问并打印第一个字符串 输出: Hello /*如果是指向整数的指针数组,则可以通过解引用获取值*/ int values[] = {1, 2, 3}; int *ptrArray[] = {values, values + 1, values + 2}; // 初始化指针数组 printf("%d\r\n", *ptrArray[0]); // 输出: 1 4.3 指针数组与函数参数:指针数组作为函数参数传递时,实际上传递的是数组第一个元素的地址。函数可以通过这个指针访问整个数组的内容。
void printStrings(char *strArray[], int size) //声明打印函数 { for (int i = 0; i < size; ++i) //遍历 { printf("%s\r\n", strArray[i]); //输出数组内容 } } char *strings[] = {"First", "Second", "Third"}; //声明数组并初始化 printStrings(strings, 3); //放入数组首地址与长度 五、数组指针 5.1 指向数组的指针:声明一个指向特定类型数组的指针。这不同于普通的指针,它实际上指向的是整个数组对象。
int (*ptr)[3]; // 声明一个指向包含3个整数的数组的指针 int arr[3] = {1, 2, 3}; // 创建一个数组并初始化 ptr = &arr; // 让 ptr 指向 arr printf("%d\n", (*ptr)[0]); // 使用 ptr 访问数组元素 输出: 1 5.2 多维数组与指针:多维数组本质上是一维数组的数组。对于二维数组来说,每个元素都是一个一维数组;对于三维数组,则是二维数组的数组,以此类推。
int matrix[2][4]={{1,2,3,4},{4,3,2,1}}; // 二维数组,包含2个一维数组,每个一维数组有4个整数 int (*pMatrix)[4] = matrix; // pMatrix 是指向包含4个整数的一维数组的指针。 printf("%d\r\n", pMatrix[0][1]); // 等同于matrix[0][1],输出值为2 int matrix2[2][2][3] = {{{1,2,3},{3,2,1}},{{1,2,3},{3,2,1}}}; // 三维数组,包含2个二维数组,每个二维数组有2行,每行3个整数 int (*pMatrix2)[2][3] = matrix2; // pMatrix2 是指向 [2][3] 类型数组(即包含2行3列的二维数组)的指针 printf("%d\r\n", pMatrix2[0][0][1]); // 等同于matrix2[0][0][1],输出值为2 六、指针函数指针函数是指返回值为指针类型的函数,以及接受指针作为参数的函数。
6.1 返回指针的函数:返回指针的函数实际就是它的返回值是一个指针,就这么简单。
/*定义一个返回指向整数的指针的函数*/ int* getIntPtr() { static int num = 42; // 使用静态变量保证num生命周期超过函数调用 return # //返回指针 } 6.2 指针作为函数参数:将指针作为参数传递给函数是一种实现按引用传递的方式,函数可以直接修改传递给它的变量或数据结构。
/*修改外部变量的函数*/ void increment(int *ptr) { (*ptr)++; } int main() { int x = 5; increment(&x); // x 的值现在变为 6 printf("%d\r\n", x); return 0; } 七、函数指针函数指针,它允许将函数作为参数传递给其他函数、存储在变量中或数组里,甚至可以返回函数,它真的很好用!!!
7.1 定义与声明:声明一个函数指针,需要知道目标函数的特性(即返回类型和参数列表),格式如下:
/* 返回类型 函数指针变量名 形参列表 */ return_type (*pointer_name)(parameter_list); 7.2 初始化与赋值和调用:声明了函数指针,就可以将其初始化为指向某个具体函数,或者稍后通过赋值操作来改变它所指向的函数。
/*一个简单加法函数*/ int add(int a, int b) { return a + b; } int main() { #if 1 //方式一 int (*funcPtr)(int, int) = add;//声明一个指向返回 int 并接受两个 int 参数的函数的指针并直接赋值 printf("%d\r\n",funcPtr(5,6)); //打印函数运算返回值 return 0; #else //方式二 int (*funcPtr)(int, int); funcPtr = add; //函数指针赋值 int a = funcPtr(7,8); //调用函数指针指向的函数并赋值运算后的返回值到变量a printf("%d\r\n",a); return 0; #endif } 7.3 函数指针数组:函数指针数组是一个包含多个函数指针的数组,每个元素都是一个指向不同函数的指针,对应菜单选择,多级菜单很有帮助。
/*简单定义几个数学运算函数*/ int add(int a, int b) { return a + b; } int subtract(int a, int b) { return a - b; } int multiply(int a, int b) { return a * b; } int main() { typedef int (*MathFunc)(int, int); //做一个函数指针声明 typedef:可定义别名 MathFunc mathOps[3] = {add, subtract, multiply}; // 定义一个函数指针数组 /*使用函数指针数组中的函数*/ int result = mathOps[0](5, 3); // 调用 add 函数 printf("%d\r\n",result); // 结果为 8 result = mathOps[1](5, 3); // 调用 subtract 函数 printf("%d\r\n",result); // 结果为 2 result = mathOps[2](5, 3); // 调用 multiply 函数 printf("%d\r\n",result); // 结果为 15 } 7.4 回调函数:回调函数是指将一个函数作为参数传递给另一个函数,在特定事件发生时,由后者调用前者。
// 定义一个执行回调的函数 void executeCallback(void (*callback)(void)) { printf("执行回调函数\r\n"); if (callback != NULL) // 如果不为空 { callback(); // 调用回调函数 } } // 定义一个简单的回调函数 void myCallback() { printf("这是一个很简单的回调函数!\n"); } int main() { executeCallback(myCallback); // 传递 myCallback 作为回调函数 return 0; } 八、指针与结构体 8.1 指向结构体的指针与引用:声明一个指向结构体的指针,首先需要定义结构体类型,然后声明一个指向该类型的指针变量,这可以通过指针间接访问结构体成员。
// 定义一个结构体类型 typedef struct { int id; char name[50]; } Person; Person *personPtr; // 声明指向结构体的指针 int main() { Person person = {1, "xiaomin"}; //初始化成员 personPtr = &person; // 将 person 的地址赋给 personPtr /*访问方式一: 箭头运算符*/ printf("%d\r\n",personPtr->id); //使用箭头运算符打印ID printf("%s\r\n",personPtr->name); //使用箭头运算符打印name /*访问方式二: 解引用*/ printf("%d\n", (*personPtr).id); // 使用解引用后再访问成员ID printf("%s\n", (*personPtr).name); // 使用解引用后再访问成员name return 0; } 8.2 嵌套结构体与指针:嵌套结构体是指在结构体内包含另一个结构体作为成员,可以通过多级指针来访问内部结构体的成员。
// 定义两个结构体类型 typedef struct { int streetNumber; char streetName[50]; } Address; typedef struct { int id; char name[50]; Address address; // 嵌套结构体 } Employee; int main() { Employee emp; //声明结构体类型变量 emp.id = 1; //初始化成员ID strcpy(emp.name, "Charlie"); //初始化成员name emp.address.streetNumber = 123; //初始化成员 address结构体的成员 strcpy(emp.address.streetName, "Main St");//初始化成员 address结构体的成员 /*打印结构体成员信息*/ printf("Employee Name: %d\r\n", emp.id); printf("Street Number: %s\r\n", emp.name); printf("Street Name: %d\r\n", emp.address.streetNumber); printf("Street Name: %s\r\n", emp.address.streetName); /*使用指向嵌套结构体的指针访问成员 */ Employee *empPtr = &emp; //声明指针并指向结构体首地址 printf("Employee Name: %s\r\n", empPtr->name); printf("Street Number: %d\r\n", empPtr->address.streetNumber); printf("Street Name: %s\r\n", empPtr->address.streetName); return 0; } 九、指针与内存管理指针是C语言中用于直接操作内存的关键工具,有效的内存管理对于编写高效、安全的程序至关重要。
9.1 动态内存分配:动态内存分配允许程序在运行时根据需要分配和释放内存,C语言提供了几个标准库函数来实现这一点:
#include "stdio.h" #include "malloc.h" //使用内存管理函数需要增加此头文件引用 int main() { /**************************malloc函数使用 STA***********************************/ int *arr = (int *)malloc(5 * sizeof(int)); // 分配5个整数的空间 if (arr != NULL) { for (int i = 0; i < 5; ++i) { arr[i] = i; printf("%d\r\n",arr[i]); //打印 0-4 } free(arr); // 使用完毕后释放内存 arr = NULL; // 避免悬挂指针 } /*************************malloc函数使用 END************************************/ /**************************calloc函数使用 STA***********************************/ int *arr2 = (int *)calloc(5, sizeof(int)); // 分配5个整数的空间,并初始化为0 if (arr2 != NULL) { for (int i = 0; i < 5; ++i) { printf("%d\r\n",arr2[i]); //打印 5个0 } } /*************************calloc函数使用 END************************************/ /*************************realloc函数使用 STA************************************/ arr2 = (int *)realloc(arr2, 10 * sizeof(int)); // 尝试扩展到10个整数的空间 if (arr2 != NULL) { for (int i = 0; i < 10; ++i) { arr2[i] = i; //初始化值 printf("%d\r\n",arr2[i]); //打印0-9 } } else { printf("错误\r\n"); } /*************************realloc函数使用 END************************************/ free(arr2); // 释放内存 arr2 = NULL; // 避免悬挂指针 return 0; } 9.2 堆与栈的区别:①栈:由编译器自动管理,用于存储局部变量、函数参数和返回地址等,栈上的数据在其作用域结束时自动销毁释放,因为它是连续的内存区域,访问效率高,原理类似子弹夹详细可以看到。
②堆:程序员负责显式地分配和释放内存,可以在整个程序运行期间存在,相比栈,堆上的内存访问稍慢,因为它不是连续的。
③它们对指针的影响:栈上的指针指向的是局部变量或函数参数,这些变量的作用域仅限于当前函数调用。堆上的指针可以指向通过 malloc 或 calloc 分配的内存,这些内存块可以在程序的不同部分之间共享,并且必须手动释放。
9.3 内存泄漏与悬挂指针:①内存泄漏:当程序分配了内存但没有正确释放时,会导致内存泄漏。这会逐渐消耗系统资源,最终可能导致性能下降甚至崩溃。
②悬挂指针:当指针指向的内存已经被释放或重新分配,但是指针仍然持有旧地址时,就叫悬挂指针,访问悬挂指针会导致未定义行为。
#include "stdio.h" #include "malloc.h" //使用内存管理函数需要增加此头文件引用 int main() { int *arr = (int *)malloc(5 * sizeof(int)); // 分配5个整数的空间 if (arr != NULL) { for (int i = 0; i < 5; ++i) { arr[i] = i; printf("%d\r\n",arr[i]); //打印 0-4 } free(arr); // 使用完毕后释放内存 (可以尝试注释此句进行下方打印观察) } /*释放后我们继续打印*/ for (int i = 0; i < 5; ++i) { printf("%d\r\n",arr[i]); //打印输出值错误 } free(arr); // 使用完毕后释放内存 arr = NULL; // 避免悬挂指针 return 0; } 十、指针的安全性与实践指针是C语言中强大但是潜在危险的特性。为了确保代码的安全性和稳定性,我们需要遵循一些最规则实践,继续学习如何避免悬空指针、检查空指针、使用 const 修饰指针、保证指针算术的安全性。
10.1 避免悬空指针:笔者在9.3已经说到并给出示例,这里不在赘述。
10.2 检查空指针:在使用任何指针之前,都应该检查它是否为 NULL,以避免尝试解引用无效指针。
int *ptr = (int *)malloc(sizeof(int)); if (ptr == NULL) { printf("分配失败\r\n"); return 0; } 10.3 使用 const 修饰指针:const 关键字可以用来声明不可变的指针或指针指向的数据,就是使用它来修饰的变量就变成了常量不可修改了。
#include "stdio.h" int main() { /*********************指针本身不可变 STA***********************************/ /*可以理解为固定指向,只能修改指向的数据,不能切换指向*/ int a = 5, b = 10; int * const p = &a; // p 不能指向其他对象 // p = &b; // 错误:p 是常量指针 a = b; //但是可以修改a的值 因为它不是指针本身 printf("%d\r\n",*p); //打印输出10 *p=12; //指针指向的数据可变 本质是和上面 a = b是一样的 printf("%d\r\n",*p); //打印输出12 /*********************指针本身不可变 END***********************************/ /*********************指针指向的数据不可变 STA*****************************/ /*可以理解为非固定指向,只能切换指向不能修改指向的数据*/ const int *p2 = &a; // 不能通过 *p2 修改 a 的值 // *p2 = 10; // 错误:*p2 是只读的 p2 = &b; //但是指针的指向可以修改 printf("%d\r\n",*p2); //打印输出10 /*********************指针指向的数据不可变 END*****************************/ /*********************以上两者结合 STA*****************************/ const int * const p3 = &a; // p 和 *p 都是只读的 // *p3 = 10; // 错误:*p3 是只读的 // p3 = &b; // 错误:*p3 是只读的 printf("%d\r\n",*p3); //打印输出12 (在指针本身不可变 STA已经修改值) a=10; //只能修改源本身了 printf("%d\r\n",*p3); //打印输出10 /*********************以上两者结合 END*****************************/ return 0; } 十一、总结通过以上学习我们知道了指针的作用非常大,但是也需要注意指针应用的风险,如果有地方还有缺漏或者不正确,期望读者可以指出谢谢大家!!!