前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >轻松拿捏C语言——【保姆级·指针讲解】期末C语言<指针>急救包,全是干货,诚意满满!

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

作者头像
用户11162265
发布2024-06-14 14:48:15
840
发布2024-06-14 14:48:15
举报
文章被收录于专栏:C语言C语言

一、指针与内存

有一栋楼,里有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修饰变量时,变量不能被修改

代码语言:javascript
复制
#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、指针加减运算

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

代码语言:javascript
复制
#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、指针相减

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

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

代码语言:javascript
复制
#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、指针关系运算

地址大小比较

代码语言:javascript
复制
#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.指针越界访问

代码语言:javascript
复制
#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.指针指向的空间被释放掉了

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

七、传值调用与传址调用

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

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

代码语言:javascript
复制
#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的值交换。

代码语言:javascript
复制
#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.数组名的理解

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

代码语言:javascript
复制
#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.用指针访问数组

代码语言:javascript
复制
#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.一维数组传参本质

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

代码语言:javascript
复制
#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.用指针数组模拟二维数组

代码语言:javascript
复制
#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* 类型

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

代码语言:javascript
复制
#include<stdio.h>
{
    char a = 'w';
    char* p = &a;
    printf("%c\n",*P);
    return 0;
}

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

代码语言:javascript
复制
int main()
{
 const char* pstr = "hello Bao Gengxiaowa.";
 printf("%s\n", pstr);
 return 0;
}

并不是!

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

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

现在来看一段代码:

代码语言:javascript
复制
#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、二维数组传参的本质:

代码语言:javascript
复制
#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]

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

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

代码语言:javascript
复制
#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.定义

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

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

写法:

代码语言:javascript
复制
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.使用

代码语言:javascript
复制
#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 (*)()类型的函数地址。

我们可以这样使用

代码语言:javascript
复制
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:

代码语言:javascript
复制
#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;
}

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

代码语言:javascript
复制
#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");
		else
			printf("输出错误,请选择0-4\n");
	} while (input);
	return 0;
}

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

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

🌹🌹感谢大家三连支持

💕敬请期待下篇文章吧~

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-05-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、指针与内存
  • 二、指针变量 、取地址操作符&和解引用操作符*
  • 三、指针变量类型的意义
    • 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.函数指针数组用途——转移表
                                                领券
                                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档