轻松拿捏C语言——【保姆级·指针讲解】期末C语言<指针>急救包,全是干货,诚意满满!

爆更小哇 2024-06-11 13:05:13 阅读 55

🥰欢迎关注 轻松拿捏C语言系列,来和 小哇 一起进步!✊

🌈感谢大家的阅读、点赞、收藏和关注💕


目录👑

 一、指针与内存🪐

二、指针变量 、取地址操作符&和解引用操作符* 🌏

三、指针变量类型的意义🌔

1.指针变量类型不同决定了在解引用的时候能访问的字节数不同💛

2.指针变量类型决定了指针向前或向后的步长🧡

3.void*指针🩵

四、const修饰指针☀️

五、指针运算🌈

1、指针加减运算🌸

2、指针相减🌺

3、指针关系运算🌷

六、野指针🏵️

        1.指针变量没有初始化

        2.指针越界访问

         3.指针指向的空间被释放掉了

七、传值调用与传址调用🌻

八、一维数组与指针🌼

1.数组名的理解

2.用指针访问数组

3.一维数组传参本质

九、指针数组🌙

1.定义 

2.用指针数组模拟二维数组 

十、字符指针变量 💙

十一、数组指针变量💜

1.定义

2.数组指针变量的初始化:

3、二维数组传参的本质: 

十二、函数指针变量💗

1.定义

2.使用

3.两段代码

十三、函数指针数组💖

 1.定义

2.区分两个数组,两个指针变量 

3.函数指针数组用途——转移表


 

 一、指针与内存

        有一栋楼,里有200个房间,假如我们要去某个房间找某个人,然后他说他在C304,我们就能通过门牌号C304快速找到他所在房间。

        在计算机中内存划分为一个个内存单元,每个内存单元也有编号,每个内存单元占1字节的空间大小,1字节又等于8个比特位

       这相当于,内存就是一栋楼,每个内存单元就是一个房间,内存单元编号就是房间门牌号,房间里有8个床位。

        内存单元编号==地址==指针

二、指针变量 、取地址操作符&和解引用操作符* 

         int a = 10;

        这里创建了一个整型变量a,占四个字节,所以就会向内存申请四个字节大小的连续空间,每个字节的内存单元都有编号。

        通过取地址符& 我们可以得到a所占四个字节中 地址最小的内存单元 的地址,该地址就是变量a的地址

        因为这四个字节的空间连续,我们得到了这一个地址,就能挨着访问另外的地址

        拿到地址有什么用?

        我们可以将地址存储在一个变量中,用来存储地址的这个变量就叫做指针变量

       int a = 10;

       int* b = &a;

这里b就是一个指针变量,它的类型是int*类型。int*中*说明b是指针变量,int说明b指向的对象是整型(即b中存储的这个地址对应的变量a是整型)

        通过解引用操作符*,我们可以改变指针变量指向的内容

           int a = 10;

           int* b = &a;

           *b = 5;这样a中的值就从10变成了5。b中存放的是a的地址,*b就是找到b中存放的地址对应的空间,所以其实*b就是a了,*b=5就是把a变成了5。

        通过解引用操作符没有直接修改变量a,而是通过地址来间接修改

另外指针变量的大小与它的类型没有关系,在32位平台下(32个比特位),指针变量大小是4个字节;在64位平台下指针变量是8个字节。

总结一下关于指针p的三个值:

​​int a = 1;

int* p = &a;

①p        p中放着一个地址,这里是a的地址

②*p        p指向的对象,这里为a

③&p        表示变量p的地址 

        二级指针:存放一级指针变量地址的变量

int a = 10;

int* p =&a;

int** m = &p;

 

对*m = p,**m = *p = a。

三、指针变量类型的意义

1.指针变量类型不同决定了在解引用的时候能访问的字节数不同

例如,char*类型的指针解引用时只能访问一个字节,而int*类型的指针解引用能访问四个字节

int n = 0x11223344;

int *pi = &n;

*pi = 0;这里将变量n的四个字节空间的内容都改成0

int n = 0x11223344;

char *pc = (char *)&n;

*pc = 0;这里只将变量n四个字节中第一个字节的内容改为0

2.指针变量类型决定了指针向前或向后的步长

char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。

指针+1,其实跳过1个指针指向的元素。指针可以+1,那也可 以-1。

3.void*指针

void*是一种特殊的指针类型,也叫泛型指针(或无具体类型的指针)

优点:可以接收任何类型的指针

缺点:不能进行 指针+-整数的运算,不能进行 解引用操作

四、const修饰指针

        const修饰变量时,变量不能被修改

#include <stdio.h>int main(){ int m = 0; m = 20;//m是可以修改的 const int n = 0; n = 20;//n是不能被修改的 return 0;}

但是这里我们可以不直接修改变量n,可以通过它的地址来间接修改

但我们给n加上const的目的就是为了使它不能被修改,所以我们应该让p拿到n的地址后也不能间接修改n

我们可以在*p前面加上const      const int *p = &n;   或者  int const *p   这样就不能通过指针变量p来间接修改n的值了

const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。 但是指针变量本身的内容可变(就是他存储的地址可以改变)。

 const如果放在*的右边,修饰的是指针变量本身,保证了指针变量的内容不能修改(它存储的地址不能改变),但是指针指向的内容,可以通过指针改变。

五、指针运算

1、指针加减运算

        数组在内存中连续存放,找到第一个元素地址就能顺藤摸瓜找到所有元素

#include <stdio.h>int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); for(i=0; i<sz; i++) { printf("%d ", *(p+i));// p+i 这⾥就是指针+整数 } return 0;}

2、指针相减

前提:两个指针指向同一块空间

指针 - 指针的绝对值是指针间的元素个数

#include <stdio.h>int my_strlen(char *s)//s为字符串常量abc中a的地址{ char *p = s; while(*p != '\0' ) p++;当p指向\0,不再++ return p-s;指向\0的地址p减指向a的地址s,所以p-s为3}int main(){ printf("%d\n", my_strlen("abc"));//打印3 return 0;}

3、指针关系运算

地址大小比较

#include <stdio.h>int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]; int sz = sizeof(arr)/sizeof(arr[0]); while(p<arr+sz) //指针的⼤⼩⽐较 { printf("%d ", *p); p++; } return 0;}

六、野指针

指针指向的位置是未知的、不正确的、随机的,那么这个指针就是野指针。

        野指针成因:

        1.指针变量没有初始化

int *p;//局部变量指针未初始化,默认为随机值

*p = 20;

        规避方法,将指针初始化

        当不知道指针变量该指向哪里时,可以给指针赋值NULL. NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。 

        2.指针越界访问

#include <stdio.h>int main(){ int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0;}

         3.指针指向的空间被释放掉了

#include <stdio.h>int* test(){ int n = 100;//局部变量n return &n;//该函数结束后,创建的变量n会被销毁}int main(){ int*p = test(); printf("%d\n", *p); return 0;}

七、传值调用与传址调用

        通过一个题来感受一下什么是传值调用,什么是传址调用

写一个函数,交换整型变量的值

#include <stdio.h>void Swap(int x, int y){ int tmp = x; x = y; y = tmp;}int main(){ int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap(a, b); printf("交换后:a=%d b=%d\n", a, b); return 0;}

但是我们却发现a和b的值却没有交换

调试看一下:

通过调试我们发现,虽然a确实把值传给了x,b把值传给了y,但是a的地址和x的地址不是同一个地址,b的地址和y的地址也不是同一个地址。

这是因为变量x和y是在Swap函数内部创建的,变量x和变量y是两个独立的空间,因此x和y交换值对变量a和b是没有影响的。

像这样把变量的值传给函数,这就是传值调用。

把实际参数传递给形式参数时,形参会单独创建一个空间来接收实参,因此形参的改变对实参没有影响。

所以我们可以将a和b的地址传过去,通过地址将a和b的值交换。 

#include <stdio.h>void Swap(int* x, int* y){ int tmp = *x; *x = *y; *y = tmp;}int main(){ int a = 0; int b = 0; scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b); Swap(&a, &b); printf("交换后:a=%d b=%d\n", a, b); return 0;}

 交换成功。

像这样把变量的地址传递给函数,这就是传址调用。

所以在函数中需要改变主调函数中变量的值,我们可以采用传址调用;如果仅需要在函数内利用变量的值来计算,就采用传值调用。

八、一维数组与指针

1.数组名的理解

数组名是数组首元素的地址

#include <stdio.h>int main(){ int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0;}

 数组名和数组首元素打出的地址一样。

但是有两个例外:

       1、 sizeof(数组名),sizeof中单独放数组名,这的数组名表示整个数组,计算的是整个数组的大小, 单位是字节

       2、 &数组名,这的数组名表示整个数组,取出的是整个数组的地址

这里讲一下&arr和arr的区别:

可以看出它们三个打印出的一模一样,没区别呀?

这时就发现,&arr[0]和arr加1,它们地址都只加了 4,而&arr加1后,它的地址加了40。

这时因为&arr[0]和arr都是首元素的地址,它们加1,就是跳过一个元素

而&arr是整个数组的地址,它加1就是跳过整个数组

2.用指针访问数组

#include <stdio.h>int main(){ int arr[10] = {0}; //输⼊ int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); //输⼊ int* p = arr; for(i=0; i<sz; i++) { scanf("%d", p+i); //scanf("%d", arr+i);//也可以这样写 } //输出 for(i=0; i<sz; i++) { printf("%d ", *(p+i)); } return 0;}

将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i] 是等价于 *(p+i)。

同理arr[i] 应该等价于 *(arr+i) 。

3.一维数组传参本质

之前我们都是在主函数里计算数组元素的个数,那能在函数里计算吗?

#include <stdio.h>void test(int arr[]){ int sz2 = sizeof(arr)/sizeof(arr[0]); printf("sz2 = %d\n", sz2);}int main(){ int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz1 = sizeof(arr)/sizeof(arr[0]); printf("sz1 = %d\n", sz1); test(arr); return 0;}

这个代码看上去感觉sz1和sz2算出来是一样的,但并不是这样。 

上面讲过,arr表示数组首元素的地址,因此在形参中我们应该用一个int* 类型的指针变量来接受实参,所以形参中int arr[]只是写成了数组的形式,本质上还是一个指针变量。

所以在函数内部sizeof(arr)计算的是数组首元素的地址的大小,并不是整个数组的大小

(这里提一个点,在32位的环境下 指针变量占4字节,64位环境下 指针变量占8字节,所以不同环境下sz2可能算出来一个是1,一个是2)。

九、指针数组

1.定义 

        指针数组是一个存放指针的数组,是数组。

        类比,整型数组是存放整型的,字符数组是存放字符的数组。

所以指针数组的每个元素存储的都是地址,类型都为指针类型,每个元素又能通过指针指向一块空间。

一个指针数组arr,长度为5,元素类型为int*类型 即元素都是 整型指针变量 的地址。

int* arr[5] = {&a1,&a2,&a3,&a4,&a5};

那么这个数组arr的类型 为 int* [5]

2.用指针数组模拟二维数组 

#include <stdio.h>int main(){ int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中 int* parr[3] = {arr1, arr2, arr3}; int i = 0; int j = 0; for(i=0; i<3; i++) { for(j=0; j<5; j++) { printf("%d ", parr[i][j]); } printf("\n"); }

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。 

但这并非是二维数组,二维数组是连续的一块空间,但指针数组模拟的并不是连续的。

十、字符指针变量 

有一种指针类型是 char* 类型

        它是把一个字符的地址放进指针变量中

#include<stdio.h>{ char a = 'w'; char* p = &a; printf("%c\n",*P); return 0;}

请问下面这个代码是把⼀个字符串放到pstr指针变量里了吗

int main(){ const char* pstr = "hello Bao Gengxiaowa."; printf("%s\n", pstr); return 0;}

并不是!

 它是将字符串hello Bao Gengxiaowa.的首元素地址即h的地址放进指针变量中。

用%s打印字符串,只需要传首元素的地址。

 

现在来看一段代码:

#include <stdio.h>int main(){ char str1[] = "hello Bao Gengxiaowa."; char str2[] = "hello Bao Gengxiaowa."; const char *str3 = "hello Bao Gengxiaowa."; const char *str4 = "hello Bao Gengxiaowa."; if(str1 ==str2) printf("str1 and str2 are same\n"); else printf("str1 and str2 are not same\n"); if(str3 ==str4) printf("str3 and str4 are same\n"); else printf("str3 and str4 are not same\n"); return 0;}

❓ 你觉得程序允许后屏幕会打印哪些语句?

你对了吗😃

这是因为str1和str2是两个数组首元素的地址,是两个不同的地址。

        所以str1、str2 not same

而str3、str4都是字符指针变量,都存放的是字符串 hello Bao Gengxiaowa.的首元素h的地址。

        所以str3、str4 same

十一、数组指针变量

1.定义

        数组指针变量是一个指向数组的指针,存储的是数组的地址,它不是数组

类比一下:

        整型指针变量 int* p;存储的是一个int型变量的地址,指针类型是 int*。

        字符指针变量 char* p;存储的是一个char类型变量的地址,指针类型是 char*。

看看这两个分别是什么:

int *p1[10];

int (*p2)[10];

        第一个是 一个数组长度为10,数组元素类型为 int* 的 指针数组,存储的是指针(地址)。

        第二个是 一个指向的 数组长度为10 数组元素类型为 int 的 数组指针,这个数组指针变量中存储的是数组的地址。 这个指针变量的类型为 int (*)[10]

注意:[]的优先级要高于*号的,所以在数组指针变量中,必须加上()来保证p先和*结合,否则p和[]先结合,那就是一个指针数组了。

2.数组指针变量的初始化:

int arr[10] = {0};

int (*p)[10] = &arr;//数组的地址 

p和&arr的类型一致,都是int (*)[10]类型。

p是这个数组指针变量的变量名,10表示p指向的数组元素个数,int为数组元素的类型。

3、二维数组传参的本质: 

#include <stdio.h>void test(int a[3][5], int r, int c){ ………}int main(){ int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}}; test(arr, 3, 5); return 0;}

这里实参是二维数组,形参也是二维数组的形式 ,知道了数组指针后 参数还能有别的写法吗?

二维数组可以看成是一个 一维数组 的数组,每一行就是一个一维数组,那么二维数组首元素的地址就是第一行的地址

第一行数组元素类型int [5],所以第一行元素的地址的类型为 int (*)[5]

所以二维数组传参的本质是 传递了地址,传递的是第一行这个一维数组的地址。

所以形参也可以写成指针形式:

#include <stdio.h>void test(int (*p)[5], int r, int c){ int i = 0; int j = 0; for(i=0; i<r; i++) { for(j=0; j<c; j++) { printf("%d ", *(*(p+i)+j)); } printf("\n"); }}int main(){ int arr[3][5] = { {1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}}; test(arr, 3, 5); return 0;}

十二、函数指针变量

1.定义

        函数指针变量是存放函数地址的变量,能通过这个变量或地址来调用函数

        函数名 就是函数的地址,也可以在函数名的前面加上&来获取地址(加不加&都一样)

  写法:

void test(){ printf("hehe\n");}void (*pf1)() = &test;void (*pf2)()= test;int Add(int x, int y){ return x+y;}int(*pf3)(int, int) = Add;int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的

那int(*pf3)(int, int)举例,pf3是函数指针变量的变量名,

函数指针变量pf3的类型(就是函数地址的类型)是int(*)(int,int)

它指向的函数的参数有两个,类型都为int,函数的返回值类型为int

2.使用

#include <stdio.h>int Add(int x, int y){ return x+y;} int main(){ int(*pf3)(int, int) = Add; printf("%d\n", (*pf3)(2, 3)); printf("%d\n", pf3(3, 5)); return 0;}

这里printf中的*pf3和pf3都是函数Add地址,所以(*pf3)(2,3)和pf3(3,5)都是在调用函数Add。 

3.两段代码

来看两段代码

(*(void (*)())0)();

 这个代码中,void (*)()是一个函数指针类型,它指向的函数没有形参,返回值类型为void。

void (*)()放在整数0的前面表示强制类型转换,将整型的数字0转换成void (*)()类型的 地址0。

所以这是一次函数调用,调用0地址处放的那个函数,0地址处放的函数没有参数,返回值也是void

void (*signal(int , void(*)(int)))(int); 

这整个代码表示的是一个 函数的声明。

函数名字是signal,函数参数有两个,一个是int类型,一个是 void(*)(int)函数指针类型。

函数的返回值类型也是 void(*)(int)函数指针类型,也就是说函数signal的返回值是一个函数的地址。

但是 我们并没有写成 void(*)(int) signal(int , void(*)(int)),而是把函数名和参数放进返回值类型里面,所以就是void (*signal(int , void(*)(int)))(int); 

十三、函数指针数组

 1.定义

是一个用来存放函数指针的数组

定义:int (*p[3])();

p先和[3]结合表示数组,数组中存放的是int (*)()类型的函数地址。

我们可以这样使用

int func1() { return 1; } int func2() { return 2; } int func3() { return 3; } int (*p[3])() = {func1, func2, func3};

2.区分两个数组,两个指针变量 

我们来区分一下这几个是什么:

1、int (*p[3])();  //函数指针数组

2、int*(p[3]);      //指针数组

3、int (*p)();      //函数指针变量

4、int (*p)[3];    //数组指针变量

 第1个中p是一个数组,它包含 3 个元素,每个元素都是int (*)()类型

第2个中,p是一个数组,有3个元素,每个元素都是int*类型

第3个中,p是一个指针变量,它存储的是一个函数的地址,这个函数返回值为int型,没有形参

第4个中,p也是一个指针变量,存储的是一个数组的地址,数组有3个int型的元素。

3.函数指针数组用途——转移表

使用转移表比使用switch语句更加灵活,因为你可以动态地改变转移表的内容,而不需要修改调用转移表的代码。

举例:分别用switch和转移表来实现一个计算器功能

用switch:

#include<stdio.h>void menu(){printf("====================================\n");printf("********* 1.add 2.sub **********\n");printf("********* 3.mul 4.div **********\n");printf("********* 0.exit 退出 **********\n");printf("====================================\n");}int add(int x, int y){return x + y;}int sub(int x, int y){return x - y;}int mul(int x, int y){return x * y;}int div(int x, int y){return x / y;}int main(){int input = 0;int x = 0;int y = 0;int ret = 0;do{menu();printf("请选择:"); scanf("%d", &input); switch (input) { case 1: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = add(x, y); printf("ret = %d\n", ret); break; case 2: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = sub(x, y); printf("ret = %d\n", ret); break; case 3: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = mul(x, y); printf("ret = %d\n", ret); break; case 4: printf("输⼊操作数:"); scanf("%d %d", &x, &y); ret = div(x, y); printf("ret = %d\n", ret); break; case 0: printf("退出程序\n"); break; default: printf("选择错误\n"); break;} while (input);return 0;}

用函数指针数组(转移表):

#include<stdio.h>void menu(){printf("====================================\n");printf("********* 1.add 2.sub **********\n");printf("********* 3.mul 4.div **********\n");printf("********* 0.exit 退出 **********\n");printf("====================================\n");}int add(int x, int y){return x + y;}int sub(int x, int y){return x - y;}int mul(int x, int y){return x * y;}int div(int x, int y){return x / y;}int main(){int input = 0;int x = 0;int y = 0;int ret = 0; //函数指针数组int (*arr[5])(int, int) = {0,add,sub,mul,div };do{menu();printf("请选择:");scanf("%d", &input);if (input >= 1 && input <= 4){printf("请输入两个操作数");scanf("%d %d", &x, &y);ret = arr[input](x, y);printf("%d\n", ret);}else if (input == 0)printf("退出计算器\n");elseprintf("输出错误,请选择0-4\n");} while (input);return 0;}

 用转移表代码量大大减少,能提高程序效率。


 🎉🎉🎉本文内容结束啦,希望各位大佬多多指教!

🌹🌹感谢大家三连支持

💕敬请期待下篇文章吧~



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。