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 = &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个字节,赋值为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 = &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]);
        }
    }
}
数组指针可以用于函数之间传递二维数组:
函数之间传递二维数组的方式:
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 *));

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

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

posted @ 2024-05-05 20:32  sleeeeeping  阅读(54)  评论(1)    收藏  举报