C语言-指针
指针
计算机的内存长什么样子?
1、计算机中的内存就像一叠非常厚的 “便签”,一张便签就相当于一个字节的内存,一个字节有8个二进制位
2、每一张 “便签” 都有自然排序的一个编号,计算机是根据便签的编号来访问、使用 "便签"
3、CPU会有若干个金手指,每根金手指能感知高低电平,高电平转换成1,低电平转换成0,我们常说的32位CPU指的是CPU有32个金手指用于感知电平,并计算出“便签”的编号
便签的最小编号:
00000000 00000000 00000000 00000000 = 0
便签的最大编号:
11111111 11111111 11111111 11111111 = 4294967295
所以32位CPU最多能使用4Gb的内存
4、便签的编号就是内存的地址,是一种无符号的整数类型
什么是指针:
1、指针(pointer
)是一种特殊的数据类型,使用它可以用于定义指针变量,简称指针
2、指针变量中存储的是内存的地址,是一种无符号的整数类型,
3、通过指针变量中记录的内存地址,我们可以读取对应的内存中所存储的数据、也可以向该内存写入数据
4、可以通过%p
显示指针变量中存储的地址编号
如何使用指针:
定义指针变量
类型* 指针变量名;
int num;
char n;
double d;
int* nump; // 访问4字节
char* p; // 访问1字节
double* doublep; // 访问8字节
long* lp; // 访问4/8字节
1、一个指针变量冲只记录内存中某一个字节的地址,我们把它当做一块内存的首地址,当使用指针变量去访问内存时具体连续访问多少个字节,指针变量的类型来决定。
2、普通变量与指针变量的用法上有很大区别,为了避免混用,所以指针变量一般以p结尾,以示区分
3、指针变量不能连续定义,一个*只能定义一个指针变量
int n1, n2, n3; // n1 n2 n3都是int
int *p1, p2, p3; // int *p1,p2,p3 p1是int* p2 p3是int
int *p1, *p2, *p3; // p1 p2 p3都是int*
4、指针变量与普通一样,默认值是随机的(野指针),为了安全尽量给指针变量初始化,如果不知道该初始化为多少,可以先初始化为NULL(空指针)
int *p; // 野指针
int *p = NULL; // 空指针
给指针变量赋值:
指针变量 = 内存地址
所谓的给指针变量赋值,其实就是往指针变量中存储一个内存地址,如果该内存地址是非法的,当使用该指针变量去访问内存时会出现 段错误
// 存储堆内存地址
int *p = malloc(4);
// 存储指向num所在内存地址(stack\data\bss)
int num; // stack
int *p = #
注意:num变量的类型必须与p类型相同
指针变量解引用:
*指针变量名;
给指针变量赋值就是让指针指向某一个内存,对指针变量解引用就是根据指针变量中存储的内存编号,去访问该内存,具体连续访问多少个字节由指针变量定义时的类型决定
int num = 100;
int *p = NULL; // 定义指针变量
p = # // 给指针变量赋值
printf("%p\n", p); // 查看指针变量的值
printf("%d\n", *p + 10); // 对指针变量解引用
*p = 88;
printf("%d\n", num);
如果指针变量中存储的是非法的内存地址,当程序运行到该指针变量解引用时,会出现段错误
int* p = NULL;
*p = 100; // 非法访问内存 会段错误
为什么要使用指针:
1、函数之间需要共享变量
函数之间的命名空间是相互独立,并且是以赋值的方式进行单向值传递,所以无法通过普通类型形参传参来解决共享变量的问题
全局变量虽然可以在函数之间共享,但是过多地使用全局变量容易造成命名冲突和内存浪费
使用数组是可以共享,但是需要额外传递长度
因此,虽然函数之间的命名空间是相互独立的,但是所使用的是同一条内存,也就是说内存空间是同一个,所以使用指针可以解决函数之间共享变量的问题
#include <stdio.h>
void func(int *p) {
printf("func p = %p, *p = %d\n", p, *p);
*p = 88;
printf("func p = %p, *p= %d\n", p, *p);
}
int main() {
int num = 66;
func(&num);
printf("main &num = %p ", &num);
printf("main:%d\n", num);
}
当函数需要返回两个以上的数据时,光靠返回值满足不了,可以通过指针共享一个变量,借助该输出型参数,返回多个数据
// put_p输出型参数
int func(int *put_p) {
*put_p = 20;
return 10;
}
int main() {
int num = 0;
int ret1 = func(&num);
printf("ret1 = %d ret2=%d\n", ret1, num);
}
2、使用指针可以提高函数之间的传参效率
一个指针变量占内存 4 | 8 字节
函数之间传参是以内存拷贝的方式进行,当参数的内存字节数比较大(大于4字节时)的时候,传参的效率就会比较低下,此时使用指针传参可以提高传参效率
#include <stdio.h>
void func(long double *f) {}
int main() {
long double f = 3.14;
for (int i = 0; i < 1000000000; ++i) {
func(&f);
}
}
3、使用堆内存时,必须与指针变量配合
堆内存无法像栈、数据段、bss段那样给内存取名字,通过标准库、操作系统提供的管理堆内存的接口函数,来操作堆内存时,是直接返回堆内存的地址给调用者,因此必须使用指针变量配合才能访问堆内存
建议:指针就是一种工具,目的是完成任务,而使用指针是有危险性,所以除了以上三种情况需要使用指针以外,不要轻易使用指针
空指针:
指针变量中存储的NULL,那么它就是空指针
操作系统规定程序不能访问NULL指向的内存,只要访问必定段错误
当函数的返回值是指针类型时,函数执行出错时一般返回NULL,作为函数的错误标志
NULL也可以作为初始值给指针变量初始化
#include <stdio.h>
int* func(void) {
return NULL; //表示执行出错
}
int main() {
int* p = NULL;
int num= 10;
p = #
printf("%d\n,", *p); // 必定段错误
}
如何避免空指针产生的段错误?
1。对来历不明的指针进行解引用前先判断是否是空指针
2、当自己写的函数的参数中有指针类型时,在使用该参数时,需要先判断是否是空指针再使用
3、当使用别人提供的函数时,它的返回值类型是指针类型时,获取返回值后,也需要先判断是否是空指针再使用
注意:必须导入 stdio.h
后 NULL才可以使用
野指针:
指针变量中存储的地址,无法确定是哪个地址、是否是合法地址,此时该指针就称为野指针
对野指针解引用的后果:
1、一切正常,刚好指针变量中存储的是空闲且合法的地址
2、段错误,刚好指针变量中存储的是非法的地址
3、脏数据,存储的是其它变量的地址
野指针比空指针的危害性更大
1、空指针可以通过if (p == NULL)
判断出来,但是野指针一旦产生,无法通过代码判断,只能通过经验人为判断
2、野指针就算暂时不暴露问题,不代表没有问题,后期可能随时暴露
如何避免产生野指针:
所有的野指针都是人为造成的,因此想要避免野指针的危害,只能通过不人为制造野指针
1、定义指针变量时一定初始化
2、函数不要返回局部变量、块变量的地址,因为当函数执行结束后,该地址指向的内存就会被自动销毁回收,如果非要接收,就接受到了一个野指针
3、与堆内存配合的指针,当堆内存手动释放后,该指针要及时置空
指针的进步值与指针的运算:
指针变量里面存储的是整数,代表内存的编号(每个整数都对应一字节的内存)。
指针的进步值:
指针变量中存储的其实是一个内存块的首地址,内存块的具体大小由指针变量的类型决定,当使用指针变量解引用访问内存时,实际访问的内存字节数叫做指针变量的进步值,也就是指针变量+1
后的内存地址的变化。
#include <stdio.h>
int main() {
char *p1 = NULL;
short *p2 = NULL;
int *p3 = NULL;
long long *p4 = NULL;
long double *p5 = NULL;
printf("%p %p %p %p %p\n", p1, p2, p3, p4, p5);
printf("%p %p %p %p %p\n", p1 + 1, p2 + 1, p3 + 1, p4 + 1, p5 + 1);
int*p6 = p3 + 8;
printf("%p\n",p3);
printf("%d\n",p3 - p6);
}
指针的运算:
指针变量存储就是是整数,理论上整数能使用的运算符,指针变量都可以使用,但只有以下运算才有意义:
指针+n = 指针所代表的整数+进步值*n
指针-n = 指针所代表的整数-进步值*n
指针1-指针2 = (指针1所代表的整数-指针2所代表的整数)/进步值
指针加减整数,就相当于以指针变量的进步值为单位前后移动,指针-指针可以计算出两个指针变量之间相隔多少个元素。
注意:指针-指针运算,它们的类型必须相同,否则编译器会报错。
数组名与指针:
数组名就是指针:
- 数组名就是数组内存块的首地址,它是个常量地址(特殊的指针),所以它作函数的参数时,才能蜕变成指针变量,因此需要额外传递数组的长度。
- 指针变量可以使用[]解引用,数组名也可以*遍历,它们是等价的。建议:当指针变量指向数组时,把指针当做数组用较为方便
注意:如果定义<TYPE> arr[n]
数组,数组名arr
就是TYPE *
类型的地址。
数组名与指针的相同点:
- 它们都是地址
- 它们都使用
[]
,*
去访问一块连续的内存
数组名与指针的不同点:
- 数组名是常量,而指针是变量
- 指针变量有它自己的存储空间,而数组名就是地址,它没有存储地址的内存。
- 指针变量与它的目标内存是指向关系,而数组名与它的目标内存是映射关系。
通用指针(万能指针):
一些具备通用性的操作函数,它们的参数可能是任意类型的指针,但编译器规定不同类型的指针不能进行赋值,为了兼容各种类型的指针,C语言中设计了void类型的指针,它能与任意类型的指针互相转换,它能解决不同类型的指针参数的兼容性问题。
void *p1 = NULL; // void* 可以给任意类型的指针变量赋值
int *p2 = p1; // 任意类型的指针可以给void*类型的指针赋值
void *p3 = p2;
通用操作的函数:
void bzero(void *s, size_t n);
功能:把内存块s的n个字节,赋值为0。
void *memset(void *s, int c, size_t n);
功能:把内存块s的n个字节,赋值为c(0~255)
void *memcpy(void *dest, const void *src, size_t n);
功能:从src内存块拷贝n个字节的内容到dest内存块
int memcmp(const void *s1, const void *s2, size_t n);
功能:比较s1和s2内存块的n个字节
s1 > s2 返回1
s1 < s2 返回-1
s1 == s2 返回0
注意1:void类型的指针变量的进步值是1。
注意2:void类型的指针变量不能解引用 ,必须转换成其它类型的指针才能解引用。
void* p = NULL;
int num = 10;
p = # // p依然是void*
*p = 10; // 报错不允许
*(int*)p = 10; // 允许
const与指针:
遵循就近原则:看const右边是* 还是指针变量名
const int *p;
功能:保护指针变量所指向的内存不被修改(也就是说 不通过通过*p
访问内存了,*p
变成只读)
int const *p;
功能:同上
注意:如果此时int *p1 = p
,类型不匹配会有警告 (需要强转 int *p1 = (int *)p
)
int *const p;
功能:保护指针变量的值不被修改 (也就是说 指针的指向不能修改p
不能再赋其它地址,变成只读)
const int *const p;
功能:保护指针所指向的内存以及指针的指向都不能修改
int const *const p;
功能:同上
当使用指针变量作为函数的参数传递时,此时是函数之间共享内存,但是有些情况下不希望函数中修改该共享内存中的数据,可以通过const
与指针变量配合,防止内存被修改。
当希望与堆内存配合使用的指针变量的指向是从一而终的,可以通过 类型 *const 指针变量名
的方式保护指针的指向不被修改
int num = 10;
int* const p = malloc(4);
*p = 100; //p = # p就无法改变指向
free(p)
二级指针:
一级指针存储的是普通变量的内存地址,二级指针存储的是指针变量内存地址。
定义二级指针:
类型* 一级指针;
类型** 二级指针;
注意:二级指针在使用方法上与一级指针有不同,所以一般以pp结尾,让使用者从变量名上就能区别一级指针与二级指针。
二级指针的赋值:
二级指针 = &一级指针;
注意:给二级指针赋值的一级指针,它们的类型必须相同,否则编译时就会报错。
二级指针解引用:
二级指针 = &一级指针;
*二级指针 此时它等价于一级指针
**二级指针 此时它等价于 *一级指针 等价于一级指针指向的内存
指针变量的内存地址
int num = 1024;
int *p = # //p变量 p存储内存地址 同时它自己也有内存地址
int **pp = &p; //p类型为int * &p类型为 int **
pp == &p
*pp == *&p == p == &num
**pp == *p == *&num == num
int x = 10;
*pp = &x; //改变的是啥 改变了p的值
**pp = 8527; //改变的是啥 改变了x的值
二级指针的用处:只有一个情况适合使用二级指针,那就是跨函数共享一级指针变量。
指针数组:
由指针变量构成的数组,也可以说它的身份是数组,成员是指针变量。
定义指针数组:
类型 *数组名[n];
就相当于定义了n个类型相同的指针变量。
int *arr[10]; // 相当于定义了10个int*的指针变量
// 10个野指针
int *arr[10] = {}; // 全部初始化为NULL
指针数组的用处:
1、构建不规则二维数组。
#include <stdio.h>
int main() {
int arr1[] = {5, 2, 3, 5, 6, 1};
int arr2[] = {2, 2, 3};
int arr3[] = {9, 2, 3, 5, 6, 1, 4, 5, 5, 2};
int arr4[] = {4, 2, 3, 5, 1};
int arr5[] = {7, 2, 3, 5, 6, 1, 3, 4};
int *arr[] = {arr1, arr2, arr3, arr4, arr5};
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); ++i) {
for (int j = 1; j <= arr[i][0]; ++j) {
printf("%d%c", arr[i][j], " \n"[j == arr[i][0]]);
}
}
}
2、构建字符串数组。
char *str[] = {"hehehahaxixi", "xixihha",...};
数组指针:
专门指向数组的指针变量,它的进步值是整个数组的字节数。
定义数组指针:
类型 (*指针变量名) [n];
类型和n决定了 数组指针 指向的是什么样的数组。
int (*arrp)[5]; // arrp是一个指向 长度为5,类型为int的数组的指针
// 数组指针: 本质是指针,指针存储数组的内存地址(指针指向数组)
int arr[5] = {1,2,3,4,5};
int (*parr)[5] = &arr; //数组指针 parr 和 &arr
int brr[3][7] = {};
int (*pbrr)[7] = brr; //数组名即首元素内存地址 &brr[0] 数组指针
int (*pcrr)[3][7] = &brr; //对二维数组取地址 数组指针 二维数组指针
int main(int argc,const char* argv[]) {
int arr[5] = {188, 2, 3, 4, 50};
int (*p)[5] = &arr;
int *p1 = (int *)(p+1); //p+1 指向数组的末尾 往后移动20字节
printf("%d\n", *(p1 - 1)); // p1是int* p1-1往前移动4字节
}
数组指针的用处:
使用数组指针可以把一块连续的内存当作二维数组使用,特别是与堆内存配合效果更佳
#include <stdio.h>
int main() {
int arr[20] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20};
int (*p)[5] = (void*)arr; //void* 是为了不产生警告
for (int j = 0; j < 4; ++j) {
for (int i = 0; i < 5; ++i) {
// printf("%d%c", *(*(p + j) + i), " \n"[i == 4]);
printf("%d%c", p[j][i], " \n"[i == 4]);
}
}
}
数组指针可以用于函数之间传递二维数组:
函数之间传递二维数组的方式:
1、void func(int arr[行数][列数]); // 行列数要固定
2、void func(int arr[][列数],int x); // 列数不能省略
3、void func(int (*arr)[列数],int x); // 一样缺乏泛用性
4、void func(int *arr, int x, int y) {
printf("%d ", *(arr + i * y + j)); //使用麻烦
int arr[3][5];
func((int *)arr, 3, 5);
}
5、void func(int x, int y, int arr[x][y]);
数组名、指针、数组指针
int arr[n]; // 数组
arr 是 int* 类型
*arr 是 int 类型
&arr[0] 是 int* 类型
&arr 是 int (*)[n] 类型
int (*p)[n] <=> &arr
*p <=> arr
arr[i] <=> *(arr+i)
int arr2[row][col];
arr2 是 int (*)[col] 类型
*arr2 是 int* 类型
**arr2 是 int 类型 *(*(arr2 + i) + j) <=> arr2[i][j]
*(arr2[0]) 是 int 类型
*arr2[0] 是 int 类型
&arr2[0] 是 int (*)[col]\int** 类型
&arr2[0] <=> int (*)[col]
&arr2[0][0] 是 int* 类型
函数指针:
函数:函数就是一段具有某项功能的代码,它会被编译器编译成二进制指令存储在text
内存段,函数名就是它在text
内存段的首地址。编译器认为函数名就是一个地址(整数)
函数指针:专门存储函数地址的指针变量叫函数指针。
定义函数指针:
1、先确定指向的函数的格式(函数声明)。
2、照抄函数声明。
3、用小括号包含函数名。
4、在函数名前加*
5、在函数名末尾加_fp,防止命名冲突。
, 6、用函数名给函数指针赋值后,函数指针就可以当作函数调用了。
#include <stdio.h>
void func(void) {
printf("我是函数func,我被调用了...\n");
}
int main() {
void (*func_fp)(void) = func;
func_fp();
}
函数指针的用处:
函数指针可以让函数像数据一样在函数之间传递。
当我们实现一个数组的排序函数时,那么排序函数内部需要调用数组元素的比较函数,由于我们不知道待排序的数组是什么类型,也就无法自己实现数组元素的比较函数,那么我们可以在排序函数的参数列表中预留一个函数指针,当有人调我们的排序函数时,他就需要提供一个数组元素比较函数供我们调用,排序函数就可以为它的数组进行排序。
函数的这种调用模式就叫回调模式。
/**
* 功能:为数组进行排序
* @base:数组的首地址
* @nmemb:数组的长度
* @size:数组成员的字节数
* @compar:调用者需要提供的数组元素的比较函数 回调函数
*/
void qsort(void *base,
size_t nmemb,
size_t size,
int (*compar)(const void *, const void *));
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)