C语言指针
引言
指针是C语言的核心机制之一,也是其区别于多数高级语言的关键特性。指针本质上是对内存地址的抽象,它提供了直接操作内存的能力,使得C语言能够实现高效的系统编程、硬件控制和动态内存管理。本文基于C语言标准,系统阐述指针的语法、语义及典型应用场景,涵盖从基础定义到函数指针的完整知识体系。
一、指针基础
1.1 内存、地址与指针的概念
计算机内存以字节为单位进行编址,每个字节对应一个唯一的数值标识,即地址。程序运行时,变量被分配在内存的特定位置,其起始地址称为该变量的首地址。指针即地址本身,而指针变量是用于存储地址的变量,其自身也占用内存空间(在64位系统中通常为8字节,32位系统中为4字节)。
1.2 指针变量的定义
指针变量的定义语法如下:
基类型*变量名;- 基类型:指明该指针所指向内存单元中存储的数据类型,可以是基本数据类型(
int、char、float、double)、构造数据类型或void。 *:类型说明符,表示定义的是指针变量。- 变量名:遵循标识符命名规则,通常以
p或ptr为前缀以增强可读性。
示例:
int*pint;// 指向int型数据的指针char*pch;// 指向char型数据的指针double*pd;// 指向double型数据的指针指针变量中存储的是其所指向数据的首地址。
1.3 指针变量的初始化与赋值
指针变量必须在使用前被明确赋值,否则其值为随机(野指针),可能导致不可预料的错误。推荐初始化为NULL(即0)或已有变量的地址。
intnum=10;int*p=NULL;// 初始化为空指针p=#// 赋值:获取num的地址野指针是指未初始化、已释放或指向无效内存的指针。避免野指针的核心原则:
- 定义时初始化为
NULL; - 仅将指针指向合法且生命周期可控的变量;
- 释放动态内存后将指针置为
NULL。
1.4 指针运算符
- 取地址运算符
&:获取变量的首地址。只能作用于变量,不能用于常量或表达式。 - 间接访问运算符
*(解引用):访问指针所指向的内存单元中的数据。解引用所访问的字节数由指针的基类型决定,例如char *解引用访问1字节,int *访问4字节。
inta=5;int*pa=&a;*pa=10;// 等价于 a = 10;1.5 指针的算术运算
指针支持有限的算术运算,主要包括自增(++)、自减(--)、加/减整数(p+n、p-n)以及指针相减(仅当指向同一数组时有效)。运算的步长由基类型大小决定,即p+1的地址偏移量为sizeof(基类型)字节。
intarr[5]={1,2,3,4,5};int*p=arr;// 指向arr[0]p++;// 现在指向arr[1],地址增加4字节(int为4字节)指针相减的结果为元素个数差,类型为ptrdiff_t(有符号整数)。
二、指针与一维数组
2.1 数组名的指针语义
在C语言中,数组名在绝大多数表达式中被解释为数组首元素的地址,即arr等价于&arr[0],其类型为基类型 *。例如:
inta[10];int*p=a;// 合法,a为int*类型2.2 通过指针访问数组元素
利用指针算术和间接访问,可以遍历数组:
for(inti=0;i<10;i++){*(p+i)=i;// 等价于 a[i] = i;}a[i]本质上被编译器转换为*(a+i),故指针和数组下标在访问层面等效。
2.3 数组作为函数参数
当数组作为函数参数传递时,实际传递的是首元素的地址,形参退化为指针。因此,以下两种函数原型等价:
voidprint_array(intarr[],intlen);voidprint_array(int*arr,intlen);在被调函数中,sizeof(arr)获得的是指针本身的大小(8字节),而非数组总字节数,故必须显式传递数组长度。
三、指针与一维字符型数组
字符型数组(C风格字符串)以'\0'作为结束标志。数组名同样退化为字符指针,指向字符串首字符。
charstr[32]="hello";char*p=str;// 等价于 char *p = &str[0];遍历字符串可通过指针递增直至遇到'\0':
intmystrlen(constchar*s){intlen=0;while(*s++)len++;returnlen;}由于字符串自带终止符,函数传参时无需额外传递长度,仅需传入指针即可。
四、数组指针(指向数组的指针)
数组指针是指向整个数组的指针,其定义语法为:
基类型(*指针名)[数组长度];例如,int (*p)[5]定义了一个指针,可以指向具有5个int元素的数组。它与普通指针的关键区别在于步长:p+1将偏移整个数组的大小(5 * sizeof(int))。
inta[5]={1,2,3,4,5};int(*pa)[5]=&a;// pa指向数组整体注意以下区别:
a(数组名)类型为int *,指向首元素;&a类型为int (*)[5],指向整个数组。
数组指针通常用于操作二维数组的行。
五、指针与二维数组
5.1 二维数组名的指针类型
二维数组int a[2][3]的数组名a在表达式中被解释为首元素(即第一行一维数组)的地址,其类型为int (*)[3],即一个数组指针,步长为3 * sizeof(int)。
5.2 指针访问二维数组元素
借助数组指针和偏移,可以访问任意元素:
inta[2][3]={{1,2,3},{4,5,6}};int(*p)[3]=a;// p指向第一行intval=*(*(p+1)+2);// 访问a[1][2],值为6通用公式:a[i][j]等价于*(*(a+i) + j)。
5.3 二维数组作为函数参数
形参可声明为二维数组形式(行数可省略,列数不可省略)或等价的数组指针形式:
voidprint_2D(intarr[][3],introws);voidprint_2D(int(*arr)[3],introws);// 完全等价调用时传递数组名及行数即可。
六、指针数组(保存指针的数组)
指针数组本质是数组,其元素均为指针类型。定义语法:
基类型*数组名[元素个数];例如,char *str_arr[5]定义了一个包含5个字符指针的数组,每个指针可指向一个字符串常量或字符数组。
初始化:
char*p[3]={"hello","world","how"};此时p[0]存储字符串常量"hello"的首地址,p[1]存储"world"的首地址,依此类推。指针数组常用于处理多个字符串,每个字符串的长度可不同,且各元素可独立重新指向。
指针数组与二维字符数组的区别
| 特性 | 二维字符数组 | 指针数组 |
|---|---|---|
| 存储内容 | 字符串数据本身(占全部空间) | 字符串的地址 |
| 内存分配 | 固定行列,浪费或不足 | 灵活,各指针独立指向 |
| 修改内容 | 可修改字符串内容 | 可改变指针指向,但字符串常量不可修改 |
| 典型用途 | 存储固定数量、固定长度的字符串 | 操作多个动态字符串或常量串 |
七、二级指针(指向指针的指针)
二级指针是存储指针变量的地址的指针。定义语法:
基类型**变量名;例如,int **pp表示指向int *类型变量的指针。
7.1 主要应用场景
- 在函数中修改主调函数的指针变量:若需在被调函数中改变主调函数中指针的指向,需传递该指针的地址,形参用二级指针接收。
voidset_pointer(int**pp,int*new_val){*pp=new_val;// 修改主调中的指针}调用:set_pointer(&p, &some_var);
- 指针数组的数组名:
char *p_arr[3]在表达式中退化为char **类型,可传递给期望二级指针的函数,用于遍历或操作指针数组。
voidprint_strs(char**arr,intn){for(inti=0;i<n;i++){puts(arr[i]);// arr[i]为char*}}八、指针函数(返回指针的函数)
指针函数是指返回值为指针类型的函数。定义时需将*与函数名结合:
基类型*函数名(形参表);例如,char *strcpy(char *dest, const char *src);返回char *。
8.1 合法返回的指针类型
- 静态变量或全局变量的地址:具有静态生命周期,程序结束时才释放。
- 字符串常量地址:位于只读数据段,生命周期贯穿程序。
- 通过参数传入的地址:可直接返回该地址(如
strcpy返回目标地址)。 - 堆区动态分配的内存(通过
malloc等),前提是未释放。
8.2 禁止返回局部变量的地址
局部变量(包括局部数组)在函数返回后其栈帧被销毁,返回其地址将导致悬垂指针,访问未定义行为。
int*bad_func(){intx=10;return&x;// 错误}九、函数指针(指向函数的指针)
9.1 定义与赋值
函数名代表函数的入口地址。函数指针的定义需指定返回类型和参数类型列表:
返回值类型(*指针名)(参数类型列表);例如,int (*pfun)(int, int);定义了一个可指向返回int、接受两个int参数的函数的指针。
赋值:
intadd(inta,intb){returna+b;}pfun=add;// 或 pfun = &add;9.2 调用
通过函数指针调用函数,可直接使用指针名(或解引用)加实参:
intresult=pfun(3,4);// 等价于 add(3,4)9.3 回调函数
回调函数是将函数指针作为参数传递给另一函数,使得被调函数能够在适当时候调用该函数。回调机制广泛应用于事件驱动、排序算法(如qsort的比较函数)和框架设计中,其核心优势在于解耦调用者与具体实现,提高代码复用性和灵活性。
示例(简化版排序比较):
intcompare_int(constvoid*a,constvoid*b){return(*(int*)a-*(int*)b);}voidsort(void*base,intn,intsize,int(*cmp)(constvoid*,constvoid*));调用时传递compare_int作为回调。
十、总结
指针是C语言的精髓,其核心思想可归纳为:
- 指针即地址,指针变量存储地址,通过基类型决定访问宽度。
- 数组名在表达式中退化为指针,函数参数传递数组时实际传递指针。
- 数组指针、指针数组、二级指针各具用途,分别用于指向数组整体、存储指针集合、修改指针变量。
- 指针函数与函数指针需严格区分:前者返回指针,后者指向函数。
- 指针与数组、字符串、函数、动态内存管理紧密关联,掌握指针是深入理解C语言内存模型和系统编程的基础。
正确使用指针能够显著提升程序效率,但不当使用(如野指针、越界访问)将导致严重错误。开发者应始终遵循“初始化、检查边界、避免悬垂”的原则,并善用const修饰符增强安全性与可读性。