C语言-指针

指针

计算机的内存长什么样子?

1、计算机中的内存就像一叠非常厚的 “便签”,一张便签就相当于一个字节的内存,一个字节有8个二进制位

2、每一张 “便签” 都有自然排序的一个编号,计算机是根据便签的编号来访问、使用 "便签"

3、CPU会有若干个金手指,每根金手指能感知高低电平,高电平转换成1,低电平转换成0,我们常说的32位CPU指的是CPU有32个金手指用于感知电平,并计算出“便签”的编号

便签的最小编号:
00000000 00000000 00000000 00000000 = 0
便签的最大编号:
11111111 11111111 11111111 11111111 = ‭4294967295‬
所以32CPU最多能使用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 = &num;
    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所代表的整数)/进步值 

​ 指针加减整数,就相当于以指针变量的进步值为单位前后移动,指针-指针可以计算出两个指针变量之间相隔多少个元素。

注意:指针-指针运算,它们的类型必须相同,否则编译器会报错。

数组名与指针:

数组名就是指针:

  1. 数组名就是数组内存块的首地址,它是个常量地址(特殊的指针),所以它作函数的参数时,才能蜕变成指针变量,因此需要额外传递数组的长度。
  2. 指针变量可以使用[]解引用,数组名也可以*遍历,它们是等价的。建议:当指针变量指向数组时,把指针当做数组用较为方便
    注意:如果定义<TYPE> arr[n]数组,数组名arr就是TYPE *类型的地址。

数组名与指针的相同点

  1. 它们都是地址
  2. 它们都使用[], *去访问一块连续的内存

数组名与指针的不同点

  1. 数组名是常量,而指针是变量
  2. 指针变量有它自己的存储空间,而数组名就是地址,它没有存储地址的内存。
  3. 指针变量与它的目标内存是指向关系,而数组名与它的目标内存是映射关系。

通用指针(万能指针):

​ 一些具备通用性的操作函数,它们的参数可能是任意类型的指针,但编译器规定不同类型的指针不能进行赋值,为了兼容各种类型的指针,C语言中设计了void类型的指针,它能与任意类型的指针互相转换,它能解决不同类型的指针参数的兼容性问题。

void *p1 = NULL; // void* 可以给任意类型的指针变量赋值
int *p2 = p1; // 任意类型的指针可以给void*类型的指针赋值
void *p3 = p2;

通用操作的函数:

void bzero(void *s, size_t n);
功能:把内存块s的n个字节,赋值为0void *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 = &num;	//	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 = &num;	p就无法改变指向
free(p)

二级指针:

一级指针存储的是普通变量的内存地址,二级指针存储的是指针变量内存地址。

定义二级指针:

​ 类型* 一级指针;
​ 类型** 二级指针;

注意:二级指针在使用方法上与一级指针有不同,所以一般以pp结尾,让使用者从变量名上就能区别一级指针与二级指针。

二级指针的赋值:

​ 二级指针 = &一级指针;
注意:给二级指针赋值的一级指针,它们的类型必须相同,否则编译时就会报错。

二级指针解引用:

​ 二级指针 = &一级指针;
​ *二级指针 此时它等价于一级指针
​ **二级指针 此时它等价于 *一级指针 等价于一级指针指向的内存

指针变量的内存地址

int num = 1024;
int *p = &num;    //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]);
        }
    }
}
数组指针可以用于函数之间传递二维数组:
函数之间传递二维数组的方式:
1void func(int arr[行数][列数]); // 行列数要固定
2void func(int arr[][列数],int x); // 列数不能省略
3void func(int (*arr)[列数],int x); // 一样缺乏泛用性
4void func(int *arr, int x, int y) {
    printf("%d ", *(arr + i * y + j));	//使用麻烦
	int arr[3][5];
	func((int *)arr, 3, 5);
}	
5void 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 *));

上一篇:内存、类型限定符

下一篇:内存管理、输入输出缓冲区、字符串

posted @   sleeeeeping  阅读(17)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
  1. 1 吹梦到西洲 恋恋故人难,黄诗扶,妖扬
  2. 2 敢归云间宿 三无Marblue
敢归云间宿 - 三无Marblue
00:00 / 00:00
An audio error has occurred, player will skip forward in 2 seconds.

敢归云间宿 - 三无Marblue

词:怀袖

曲:KBShinya

编曲:向往

策划:杨颜

监制:似雨若离RainJaded/杨颜

吉他:大牛

混音:三无Marblue

和声:雾敛

母带:张锦亮

映像:似雨若离RainJaded

美术:阿尼鸭Any-a/乙配/雨谷/今风/米可/Eluan

题字:长安

酒 泼去群山眉头

酒 泼去群山眉头

月 悬在人世沧流

空杯如行舟 浪荡醉梦里走

心 生自混沌尽头

心 生自混沌尽头

对 天地自斟自酬

诗随我 遍历春秋

行流水 走笔形生意动

见珠玉 淙淙落纸成诵

拾得浮名有几声 云深处 却空空

耳畔丝竹 清商如雾

谈笑间 却是心兵穷途

飞觞醉月无归宿 便是孤独

不如就化身为风

卷狂沙 侵天幕

吹醒那 泉下望乡 的战骨

昨日边关犹灯火

眼前血海翻覆

千万人跌落青史 隔世号呼

于是沸血重剑共赴

斩以雷霆之怒

肩背相抵破阵开路

万古同歌哭

纵他春风不度 悲欢蚀骨

此去宁作吾

挣过命途 才敢写荣枯

望 云际群龙回首

望 云际群龙回首

任 飘蓬争逐身后

叹冥顽之俦 好景尽付恩仇

收 江声随酒入喉

收 江声随酒入喉

来 提笔御风同游

不觉已 换了春秋

真亦假 泼墨腾烟沉陆

有还无 蝶影纷堕幻目

我与天地周旋久

写尽梦 便成梦

夜雨浇熄 往事残烛

生死间 谁尽兴谁辜负

管他醒来归何处 心生万物

也曾对电光火雨

抛酒樽 镇天枢

护住了 人间多少 朝与暮

烧尽了阴云冥府

烧尽了阴云冥府

且看星斗尽出

浩荡荡尘埃野马 忘怀命数

于是信步鸿蒙之轻

也领苍生之重

与诗与剑颠倒与共

沉眠斜阳中

纵他世事汹涌 万类争渡

此去宁作吾

醉得糊涂 才梦得清楚

潮水 带着叹息轻抚

潮水 带着叹息轻抚

像光阴 漫过大地上幽微草木

有情世 见众生明灭往复

天生自在 何必回顾

晦暗中双掌一拊

立此身 照前路

与某个 阔别的我 决胜负

渺渺兮身外无物

无喜无悲无怖

不过是大梦一场 各自沉浮

于是纵横万相穷通

也守心底灵通

合眼识得星沉地动

也岿然不动

敢令岁月乌有 逍遥长驻

敢归云间宿

遥祝远行人 有道不孤

点击右上角即可分享
微信分享提示