10 学习指针
1 指针核心定义与本质
1.1 指针与指针变量
1、指针即地址,指针变量是存放地址的变量,其大小与操作系统位数相关:64 位系统中占 8 字节,32 位系统中占 4 字节。
2、指针的核心功能是通过地址间接访问目标变量,实现灵活的内存操作。
1.2 指针类型的关键作用
1、承载地址信息与内存解析规则,基类型必须与指向数据类型严格一致。
2、决定内存访问步长:char*
偏移 1 字节,int*
偏移 4 字节,double*
偏移 8 字节。
1.3 核心要求
指针变量本身及指向的内存空间必须确定,严禁使用野指针(未初始化)或悬垂指针(指向已释放空间)。
2 指针的基本操作规则
2.1 地址获取与传递
1、通过&
(取地址运算符)可获取变量在内存空间中的首地址,只有左值(可被赋值的变量)能进行&
操作,常量、表达式和寄存器变量(register
修饰)不能。
2、例如int Num = 0; &Num
可得到Num
在内存中 4 字节空间的首地址,其类型为int *
(由基类型int
升级而来)。
3、在函数传参中,使用&
传递变量地址,能实现在被调函数中修改主调函数的变量值,突破值传递 “单向传递” 的限制(典型应用:函数返回多个值)。
2.2 指针访问与解引用
1、通过*
运算符可获得指针指向的空间或对应空间中的值,*
连接的内容必须为指针类型(否则编译报错)。
2、若直接使用*
对应的表达式,其值为指针指向空间中的值,类型为指针类型降级后的基类型,如int *p; *p
的类型为int
。
3、变量有两种访问形式:直接访问(通过变量名,如Num = 5
)和间接访问(通过指针,如*p = 5
)。
3 字符串函数与指针应用(基于指针实现的库函数)
字符串操作函数的底层实现依赖指针对字符数组的遍历与修改,核心是通过char*
指针访问内存中的字符序列(以'\0'
结尾)。
函数 | 功能 | 核心实现逻辑 | 关键点 |
---|---|---|---|
strlen | 计算字符串长度 | 指针遍历至'\0' ,返回指针差值 | const char* 保证只读,不包含'\0' |
strcpy | 复制字符串 | 逐个字符复制(含'\0' ) | 需保证目标空间足够,禁止地址重叠 |
strcat | 拼接字符串 | 先移动指针至目标末尾,再复制源字符串 | 目标需足够大,源和目标均需'\0' 结尾 |
strcmp | 比较字符串 | 按 ASCII 值逐个比较,遇不同字符返回差值 | 非长度比较,一旦不同立即返回 |
3.1 strlen
:计算字符串长度
1、功能:返回字符串中'\0'
前的字符个数(不包含'\0'
)。
2、底层实现:
size_t strlen(const char *str)
{const char *p = str; // 用指针遍历字符串while (*p != '\0') { // 遍历至结束符p++;}return p - str; // 指针差值即长度(字符个数)
}
3、关键点:const char*
保证不修改原字符串;通过指针自增遍历,通过指针相减计算长度。
3.2 strcpy
:字符串复制
1、功能:将源字符串(src
)复制到目标空间(dest
),包括'\0'
,返回目标地址。
2、底层实现:
char *strcpy(char *dest, const char *src)
{char *p = dest; // 保存目标首地址(用于返回)while ((*p++ = *src++) != '\0'); // 逐个复制字符,包括'\0'return dest;
}
3、注意事项
需保证dest
空间足够大,否则会导致缓冲区溢出。
源字符串必须以'\0'
结尾,否则会复制随机数据。
禁止源地址与目标地址重叠(如strcpy(a, a+1)
会导致未定义行为)。
3.3 strcat
:字符串拼接
1、功能:将源字符串(src
)追加到目标字符串(dest
)的末尾(覆盖dest
原有的'\0'
),返回目标地址。
2、底层实现:
char *strcat(char *dest, const char *src)
{char *p = dest;// 1. 移动指针到dest的末尾('\0'位置)while (*p != '\0') {p++;}// 2. 复制src到dest末尾,同strcpy逻辑while ((*p++ = *src++) != '\0');return dest;
}
3、注意事项:
dest
必须有足够空间容纳拼接后的字符串。
dest
和src
都必须以'\0'
结尾。
3.4 strcmp
:字符串比较
1、功能:按 ASCII 值比较两个字符串,返回差值(0
表示相等,正数表示str1
大于str2
,负数表示str1
小于str2
)。
2、底层实现:
int strcmp(const char *str1, const char *str2)
{// 遍历字符,直到遇到不同字符或'\0'while (*str1 != '\0' && *str2 != '\0' && *str1 == *str2) {str1++;str2++;}// 返回对应字符的ASCII差值return (unsigned char)*str1 - (unsigned char)*str2;
}
3、关键点:
比较的是字符的 ASCII 值,而非字符串长度。
一旦遇到不同字符立即返回差值,不继续比较后续字符。
4 指针偏移与内存访问
1、指针偏移大小由基类型大小决定:char *
偏移 1 字节,int *
偏移 4 字节,double *
偏移 8 字节,结构体指针偏移整个结构体大小。
2、两个同类型指针相减的结果为地址间相差的数据类型元素个数(非字节数),例如int *p1 = a; int *p2 = a+3; p2-p1
结果为 3(表示相差 3 个int
元素)。
3、指针不能与非指针类型运算,也不能跨类型相减(编译报错)。
5 野指针与空指针
1、野指针成因:未经初始化的指针(如int *p;
)、指向已释放空间的指针(如free(p);
后未置空)、越界访问的指针(如数组越界)。
2、空指针:指向内存地址0x0
的指针,用NULL
(本质为(void*)0
)表示,该空间为系统保留的只读区域,对空指针解引用(*p = 10
)会导致程序崩溃。
3、预防野指针:未使用的指针初始化为NULL
;释放内存后立即置空(free(p); p = NULL;
);避免指针越界访问。
6 指针赋值与修改
1、对指针变量本身赋值(如p = q;
):改变指针的指向,使其指向新的地址。
2、对指针解引用赋值(如*p = *q;
):改变指针指向空间的值,指针指向不变。
3、示例:int a=1, b=2; int *p=&a, *q=&b; p=q;
后p
指向b
,*p
结果为 2;*p=*q;
后a
的值变为 2,p
仍指向a
。
7 动态内存分配与管理
7.1 核心函数
函数 | 功能 | 特点 |
---|---|---|
malloc(size_t size) | 申请size 字节的连续内存 | 未初始化,内容为随机值;返回void* ,需强转;失败返回NULL |
calloc(size_t n, size_t size) | 申请n 个size 字节的连续内存 | 自动初始化为 0;效率略低于malloc |
realloc(void *ptr, size_t size) | 调整已分配内存的大小 | 可能在原地址扩展或重新分配;失败返回NULL ,原内存不变 |
free(void *ptr) | 释放动态分配的内存 | 仅释放ptr 指向的空间,不改变指针值;不能重复释放或释放非动态内存 |
7.2 内存管理规则
1、动态内存必须手动释放,否则导致内存泄漏(程序运行中内存占用持续增长)。
2、释放后指针需置空(p = NULL;
),避免成为野指针。
3、申请内存后必须检查返回值是否为NULL
(防止内存不足导致崩溃):
int *p = (int*)malloc(10*sizeof(int));
if (p == NULL) { /* 内存申请失败处理 */ }
4、避免 “内存碎片”:频繁申请和释放小块内存会导致内存碎片,可通过内存池优化。
8 指向函数的指针与指针函数
类型 | 定义 | 特点 |
---|---|---|
指针函数 | 返回指针的函数(类型* 函数名(参数) ) | 不可返回局部变量地址,可返回动态内存、全局变量地址 |
函数指针 | 指向函数的指针(返回类型 (*指针名)(参数列表) ) | 需匹配函数返回类型、参数类型和个数,用于回调函数(如qsort 比较器) |
8.1 指针函数
1、定义:返回值为指针的函数,格式为类型 *函数名(参数列表)
。
2、注意:不能返回局部变量的地址(局部变量在函数结束后释放,返回后成为野指针),可返回动态分配内存、全局变量或静态变量的地址。
3、示例:
int *createArray(int n)
{int *arr = (int*)malloc(n*sizeof(int));return arr; // 返回动态内存地址,需外部释放
}
8.2 函数指针
1、定义:指向函数的指针,格式为返回类型 (*指针名)(参数类型列表)
,需严格匹配函数的返回类型、参数类型和个数。
2、函数名本质是函数入口地址,可直接赋值给函数指针(无需&
)。
3、应用:实现回调函数(如排序函数qsort
的比较器)、函数接口封装,降低模块耦合性。
4、示例:
int add(int a, int b)
{return a + b;
}
int (*funcPtr)(int, int) = add; // 函数指针指向add
int result = funcPtr(3, 4); // 调用函数,结果为7
9 指针与数组的关系
9.1数组名的特殊性
1、数组名是指向首元素的指针常量(不可修改指向),如int a[5]; a
等价于&a[0]
,类型为int *
。
2、例外情况:
sizeof(数组名)
:获得数组总字节数(如int a[5]; sizeof(a) = 20
)。
&数组名
:类型为指向整个数组的指针(如int (*)[5]
),偏移量为整个数组大小。
数组名作为sizeof
参数或取地址时,不退化为首元素指针。
9.2 数组作为函数参数
1、三种传递形式等价:int fun(int a[5]);
、int fun(int a[]);
、int fun(int *a);
,函数内均按指针处理,丢失数组长度信息。
2、必须显式传递数组长度:int fun(int *a, int len)
,避免越界访问。
9.3 字符数组与字符串
1、字符串本质是'\0'
结尾的字符数组,传参时可直接传递数组名(即char *
指针)。
2、遍历字符串:while (*p != '\0') { printf("%c", *p++); }
。
3、字符串常量存储在只读区,不能通过指针修改(如char *p = "hello"; *p = 'H';
会崩溃)。
10 const 指针
10.1 三种形式及特性
定义 | 含义 | 指针值是否可改 | 指向空间是否可改 | 必须初始化 |
---|---|---|---|---|
const int *p; 或 int const *p; | const 修饰*p | 是 | 否 | 否 |
int *const p; | const 修饰p | 否 | 是 | 是 |
const int *const p; 或 int const *const p; | const 修饰p 和*p | 否 | 否 | 是 |
10.2 应用场景
1、const int *p
:保护指向的数据不被修改(如函数参数传递只读数据)。
2、int *const p
:确保指针始终指向同一变量(如硬件寄存器地址)。
3、const int *const p
:既固定指针指向,又保护数据(如常量配置参数)。
11 指针数组与数组指针
11.1 概念区分
类型 | 本质 | 定义形式 | 内存占用(64 位) | 示例 |
---|---|---|---|---|
指针数组 | 数组,元素为指针 | int *a[5]; | 5×8=40 字节 | char *strs[] = {"apple", "banana"}; |
数组指针 | 指针,指向数组 | int (*a)[5]; | 8 字节 | int (*p)[3] = &arr; (arr 为int[3] 数组) |
11.2数组指针与二维数组
1、二维数组名是指向第一行的数组指针,类型为int (*)[列数]
,如int a[2][3];
中a
的类型为int (*)[3]
。
2、访问元素的方式:a[i][j]
、*(a[i] + j)
、*(*(a + i) + j)
、(*(a + i))[j]
。
3、遍历二维数组:
int a[2][3] = {{1,2,3}, {4,5,6}};
int (*p)[3] = a; // p指向第一行
for (int i=0; i<2; i++)
{for (int j=0; j<3; j++) {printf("%d ", *(*(p+i) + j));}
}
12 二级指针
12.1 定义与本质
1、二级指针是指向一级指针变量的指针,格式为类型 **p
,64 位系统中占 8 字节,用于存储一级指针的地址。
2、示例:int a=10; int *p=&a; int **q=&p;
中q
是二级指针,*q
等价于p
,**q
等价于a
。
12.2 使用场景
1、函数内部修改外部指针的指向:
void allocMemory(int **p, int size)
{*p = (int*)malloc(size); // 修改外部指针p的指向
}
int *arr;
allocMemory(&arr, 10*sizeof(int)); // 传递一级指针的地址
2、指针数组传参:指针数组的数组名是二级指针,如char *strs[] = {"a", "b"};
传参时类型为char **
。
3、动态二维数组:int **arr = (int**)malloc(3*sizeof(int*));
用于创建行长度可变的二维数组。
13 指针作为函数参数
13.1 传递方式对比
传递方式 | 特点 | 适用场景 |
---|---|---|
值传递 | 形参是实参的副本,修改形参不影响实参 | 函数仅使用参数值,不修改原变量 |
地址传递(一级指针) | 形参指向实参地址,可修改实参的值 | 函数需修改原变量的值(如交换两个变量) |
二级指针传递 | 形参指向一级指针的地址,可修改一级指针的指向 | 函数需为外部指针分配内存或改变其指向 |
13.2典型应用:交换两个变量
void swap(int *a, int *b)
{int temp = *a;*a = *b;*b = temp;
}
int x=1, y=2;
swap(&x, &y); // 调用后x=2, y=1
14 学习总结
1、野指针操作:对未初始化、已释放或越界的指针解引用,导致程序崩溃或数据损坏。
解决:初始化指针为NULL
,释放后立即置空,避免越界。
2、类型不匹配:用char*
指针访问int
变量(如char *p = (char*)&a;
)可能导致数据截断或解析错误。
解决:严格保证指针类型与指向数据类型一致。
3、内存泄漏:动态分配的内存未释放,长期运行导致系统内存耗尽。
解决:遵循 “谁申请谁释放” 原则,使用智能指针(C++)或内存池管理。
4、重复释放内存:对同一指针多次调用free
,导致内存管理混乱。
解决:释放后立即置空,释放前检查是否为NULL
。
5、字符串函数使用错误:
(1)使用strcpy
时目标空间不足,导致缓冲区溢出(如char dest[3]; strcpy(dest, "hello");
)。
解决:使用strncpy
限制复制长度,或确保目标空间足够。
(2)对非'\0'
结尾的字符序列使用strlen
,导致遍历越界(如未初始化的字符数组)。
解决:确保字符串以'\0'
结尾,或手动指定长度。
6、const 指针违规操作:对const int *p
尝试修改指向空间的值(*p = 5
),编译报错。
解决:明确const
修饰的对象,避免违规修改。
7、混淆指针数组与数组指针:错误定义(如int (*a)[5]
写成int *a[5]
)导致内存访问错误。
解决:记住优先级:[]
高于*
,数组指针需加括号。
8、返回局部变量地址:函数返回栈上变量的地址(如int *func() { int a=1; return &a; }
),返回后地址失效。
解决:返回全局变量、静态变量或动态分配内存的地址。