深入理解并打败C语言难关之一————指针(4)

前言:

我们在前面的几讲中已经讲了指针的很多内容了,现在我们开始层层递进,要探寻更多的指针喽,不多废话了,直接进入正题,开始今天的指针之旅喽!


目录:

1.字符指针变量

1.1常量字符串

1.2一个有趣的题目

2.数组指针变量

2.1数组指针变量是什么?

2.2我们如何对数组指针变量初始化

3.二维数组传参的本质

4.函数指针变量

4.1函数指针变量是什么?

4.2函数指针变量的创建和初始化

4.3函数指针变量的使用


正文:

1.字符指针变量

在开启文章之前,我先对字符串的打印提一嘴,对于字符串的打印,我们知道用到的占位符是%s,此时仅仅提供字符串首字符地址就可以了,可一定不要解引用字符指针,这样仅仅会打印第一个元素的地址,并且VS2022还会给你报警告!!!,一定要记住这个,小编在刚开始起草这篇文章的时候就忘记这部分知识点了,导致我自己重新复习了一遍知识,所以我们在学习的时候一定要温习以前的知识,温故而知新,可以为师矣!

我们知道,字符型的常量用char表示,那么根据指针的知识,我们知道便可以知道字符指针变量是char * 来表示,我们知道,字符串的创建可以通过一个数组进行创建,就类似:

cpp 复制代码
#include<stdio.h>
int main()
{
     char arr[20] = {0};
     arr[20] = {"hello world"};
     return 0;
}

在这里数组里面放置了字符串 ,这个我相信读者朋友们都是明白的,下面我给出一串代码,大家来思考一个问题(我放到题目里面了):

cpp 复制代码
​
#include<stdio.h>
int main()
{
	char* p = "hello world";   //猜一猜这里是把一个字符串放进指针变量里吗?
	printf("%s", p);
}

​

先自己思考一下,三,二,一------------------其实这里是把字符串第一个元素的地址放入了指针变量p里面,之后打印环节是通过第一个元素的地址开始往后打印,直到遇到\0停下,下面来看看这个字符串真正的样子:

通过这个图可以清晰的知道p指向的地方在哪里,我们把后面的字符串叫做常量字符串,下面我们来进入常量字符串的环节:

4.1常量字符串

我们把;类似上面的代码的后面的字符串,叫做常量字符串,这是我们写字符串的另一种形式,第一种是通过数组的方式来存放字符串,现在这种为通过数组指针来传递字符串,大家一定要记住这两种书写方式,接下来我们来讲一下常量字符串的一个特性,将之前请欣赏下面一组代码:

cpp 复制代码
int main()
{
	char* p = "abcdef";
	*p = 'a';
	printf("%c", *p);   //这个代码可以正常实现吗
	return 0;
}

大家觉得这个代码可以实现吗?下面我们来运行一下:

我们发现运行起来居然没有结果!那么这是为什么呢?对于我们代码中出现的未知错误,我们可以通过调试来检测一下代码的重要性(这里展示出了调试的重要性,我们在平常代码出错的时候记得自己调试,而不是一味的去查询网络,询问别人) :

我们发现我们无法修改字符串常量的值,由此我们可以知道字符串常量的一个性质:字符串常量是无法被改变,它是固定的,它是忠诚的,所以我们再平常使用字符串常量的时候一定要记得这个性质!既然我们已经了解了字符串常量了,下面来看一个有趣的题目,可以让我们更好的理解这部分的知识

1.2.一个有趣的题目:

下面请看下面的代码:

cpp 复制代码
#include<stdio.h>
int main
{ 
   char arr1[] = "nihao shijie";
   char arr2[] = "nihao shijie";
   const char* arr3 = "nihao shijie";
   const char* arr4 = "nihao shijie";
   if(arr1 == arr2)
      printf("arr1 and arr2 is same!\n");
   else
      printf("arr1 and arr2 is diffcule\n");  //猜一猜最终会打印出什么?
   if(arr3 == arr4)
     printf("arr3 and arr4 is same!\n");
   else 
     printf("arr3 and arr4 is diffcult!\n");
    return 0;
}​

​

大家先思考一下这个问题(一定要思考!),三,二,一 ------------ 这个题的答案是下图所展示的:

可能现在很多读者朋友会有疑惑:为什么arr1 和 arr2是不同的呢?arr3 和 arr4为什么是相同的呢?下面我来解释一下大家的疑惑:首先我们可以清晰的看出来,arr1 和 arr2是一个字符数组,虽然它们指向的都是同一个字符串,但是每一次数组的建立,都是开辟一块新的空间,所以二者的地址都不一样,所以这个是不相同的! 但是对于后面两个字符串,在C语言中,会把常量字符串固定在一个内存中,所以它们指向的内容是同一个内存,所以他们是相同的,这个题目一定要记住牢牢掌握,我当时学习这个题目的时候就出错了,所以我特地把它写到文章,读者朋友们一定要牢记!

小结:

大家一定要好好掌握常量字符串,至少做到看见它知道它是什么东西,而不是啥也不会,下面来进入下一篇章

2.数组指针变量

2.1.数组指针变量是什么

在讲这个之前,大家一定要把数组指针和指针数组区分开,虽然同样是四个字,但是位置一交换那整体的意思就不一样了,后者是一个数组,那前边的是什么呢?我们可以类比记忆,存放整形地址的叫整形指针,存放浮点型的是浮点型指针,那么结果显然意见了,数组指针就是存放数组地址的指针,它的本质是指针,指针数组本事是数组,这两个双胞胎一定要区分开?

那么我们如何写数组指针呢?下面给出两个代码,大家来看看二者各自是什么呢?

cpp 复制代码
int* p1[10];
int(*p2)[10];

我们之前就讲述了指针数组的创建,所以我们很显然的认识出了p1是指针数组,那么通过排除我们可以知道第二个指的是数组指针,那么为什么是这样创建呢?下面我们通过图文的方式来帮助大家进行理解:

(上面的图片有一句话出错课,[10]是指针所指向的数组元素有10个)

通过图文的形式我们可以很清晰的知道为什么数组指针是这样的创建的,我们进行完创建后就要初始化了,下面我们来进行初始化的环节:

2.2.我们如何对数组指针变量初始化

其实初始化是蛮简单的,我们以及了解到了数组指针是什么了,初始化就是把它翻译成代码就好了,下面来展示一下数组指针如何初始化:

cpp 复制代码
#include<stdio.h>
int main()
{
	int arr[5] = { 0 };
	int(*p)[5] = &arr;
}

其实这个初始化和整型的初始化本质都是一样的,都是取地址罢了,不过一个是取数的地址,一个是取数组的地址而已,下面小编来提问一个问题,数组指针的类型是什么呢?这个时候我们可以通过类比进行记忆,int *p的类型是int *,char *p的类型是char,那么数组指针的类型自然是int(*)[10],我们将名字去掉以后就是它的类型,我们可以通过调试窗口来验证我们的说法:

很显然,我们的说法是正确的,我们现在已经了解到了数组指针的创建和初始化了,那么在进入下一篇文章之前,不知道大家是否还记得我以前写的文章中,&arr代表的是整个数组,当时我并没有很详细的解释,现在我们学了数组指针,这个问题就好解决了,因为&arr的类型是int(*)[10],所以它代表的是整个元素的地址,所以我们让它加一的时候它会跳过一个数组的字节!所以说,知识都是环环相扣的,前面许多不懂得知识我们学到后面就迎刃而解了!

小结:

大家一定要把数组指针和指针数组区分好,以后我们会经常使用它们的!

3.二维数组传参的本质(差点忘记写这部分呢)

小编在之前的一篇文章中,我记着应该是讲指针(3)的时候就讲过一维数组传参的本质,光说一维数组的话,二维数组我们就白学了,它也是需要被宠幸(bushi)的,在说这个之前,我们也是需要说二维数组数组名代表的是什么:

3.1二维数组数组名

我们知道,一维数组的数组名是数组首元素的地址,那么二维数组的数组名也是数组首元素的地址吗?大家先来自己思考一下,我给出一段代码以及运行图,来解释一下二维数组的数组名到底是不是二维数组第一个元素的地址:

cpp 复制代码
int main()
{
	int arr[3][4] = { 0 };
	printf("%p\n", arr);
	printf("%p\n", &arr[0][0]);
	printf("%p\n", arr + 1);
	printf("%p\n", &arr[0][0] + 1);
	printf("%p\n", &arr[0][1]);
	printf("%p\n", &arr[1][0]);  //猜猜我为啥会写这个
	return 0;
}

我们会发现如果我们仅仅光展示前两行代码的时候,大家肯定认为此时二维数组的数组名就是二维数组首元素的地址,但是我们发现,当指针加一的时候,二者突然又不一样了!这里很多读者朋友就会疑惑了,这俩为啥不相同,这里为了让大家更好的理解,小编贴心的又打出了两个代码,这个时候我们就发现,&arr[0][0] + 1的地址和&arr[0][1]的地址是一样的,arr + 1和&arr[1][0]的地址是一样的,这似乎向我们反映了一个事情,arr似乎指的是第一行的地址,也就是首行的地址,那么事实是否就是这样呢?其实,这个就是正确的,二维数组中,数组名就是数组受行的地址,为了帮助读者朋友们更好的理解,小编用图文进行解释:

对于具体的解释我已经放到图文里面,读者朋友们先记住,二维数组的数组名就是数组首行元素的地址!现在我们已经明白了这个小的知识点,下面我们来进行中重要部分呢,二维数组进行传参的本质:

3.2.二维数组传参的本质

我们知道哦在一维数组传参的时候传过去的是数组名,是首元素的地址,我们在传参二维数组的时候,同样也是传的数组名,但是数组首行的地址,那么我们形参可以怎么写呢?这里就用到了我们刚学的一部分内容,数组指针,我们可以把传过去的首行元素看做成一个一维数组的地址,此时我们可以通过数组指针来接受它,具体的代码如下图:

cpp 复制代码
void suibian(int(*p)[2], int sz)  //是不是感觉到知识是换换相扣的呢?
{
	///......
}
int main()
{
	int arr[3][2] = { 0 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	suibian(arr, sz);
	return 0;
}

是不是感觉知识是环环相扣的呢》我么在数组指针讲完后趁热打铁,讲到了二维数组传参的本质,这有助于我们更好的理解数组指针,当然,对于二维数组的传参,我们也可以写成这样类型的:

cpp 复制代码
void suibian(int arr[3][2], int sz)  //是不是感觉到知识是换换相扣的呢?
{
	///......
}

其实这两种写法都是可以的,你想用什么就用什么!

4.函数指针变量

4.1.函数指针变量是什么?

这里我们同样也可以类比记忆,上面我们刚讲数组指针是存放数组地址的指针,整形指针是存放整形的指针,所以函数指针,就是存放函数地址的指针,这里向我们透露出了一个信息,函数也是有地址的,下面我们通过一串代码来看看函数的地址是什么:

cpp 复制代码
#include<stdio.h>
void add()
{
	//内容我就不写了
}
int main()
{
	printf("%p",&add);
	return 0;
}

可以看到函数确实会存在地址,这里我们也是get到了一个新的知识点,下面小编出个题考考大家,前面我们学习了数组名代表着数组首元素的地址,那么函数名是否也是一个地址呢?下面我们来进行代码展示:

cpp 复制代码
void add() {

}
int main()
{
	printf("%p\n", add);  //这里会不会也和数组名和取地址数组名一样,两者指的类型不同呢?
	printf("%p\n", &add);   
}

上图可以看出,函数名同样也是指的函数的地址,正如我在代码中提出的问题一样,函数名和&函数名指向的是一样吗?这里小编也不多废话了,其实函数名和&函数名是一模一样的,只不过根据个人的写法不同罢了,这里一定要记住,待会要用到!

4.2.函数指针变量的创建和初始化

4.2.1函数指针的创建

那么既然我们知道函数指针是什么了,下面我们要对它进行创建了 :

cpp 复制代码
int (*p)(int ,int) 

可能很多读者朋友对整个还是很疑惑的,看不懂这是什么东西,下面我们通过画图来进行进一步的解释:

通过上图我们可以知道函数指针到底是如何进行创建的,这个和数组指针的创建是有一点相似的,所以也可以类比记忆,同样的,我们也要了解函数指针的类型到底是什么,其实它和数组指针,整形指针的类型记忆方法一样,我们把指针的名字去掉就是代表的是什么类型了,所以函数指针的类型是:int(*)(),下面我们通过调试来证明我说的:

上面的代码证实了我说的正确性,所以我们也要记住函数指针的类型,这里也算是个小小的重点,我们现在已经讲了函数指针是如何进行创建的,下面我们来进行函数指针的初始化:

4.2.1函数指针的初始化

其实这部分的知识很简单,既然函数指针指的是存放函数地址的指针,那么我们在对其使用的时候,直接对函数进行取地址操作就好了,下面是代码的展示:

cpp 复制代码
	int(*p)(int, int) = &add; //这里的add是指的是一个add函数,记住

上面便是对函数指针进行初始化,这部分知识算是简单的 ,读者朋友们一定要掌握好,我们既然讲了函数指针,那么我们就要对函数指针进行使用,下面进入最后一个小节,对函数指针进行使用

4.3.函数指针变量的使用

我们在使用指针变量的时候,往往伴随着解引用操作符*的使用,所以我们在使用函数指针变量的时候,也需要用到解引用操作符,上面的图解解释了int * 是返回类型,所以我们是不需要写它的,我们仅仅使用剩下的就好了,下面是对函数指针使用时的代码:

cpp 复制代码
int add(int x,int y) {
	return x + y;
}
int main()
{
	int(*p)(int, int) = &add;
	int c = (*p)(3, 4); 
	printf("%d", c);
}

可以看出此时函数被正确的运用了,所以我们在使用函数指针的时候记住忽略前面的int *就好了,之后正常写就好,不知道你是还记得我们在前面说过函数名就是函数地址,这时候就要形成闭环了,我将前面的目的就是为了这里,请读者朋友们想想看,我们是否可以通过指针名直接访问函数呢?下面来看看代码展现

cpp 复制代码
int add(int x,int y) {
	return x + y;
}
int main()
{
	int(*p)(int, int) = &add;
	int c = p(3, 4); 
	printf("%d", c);
}

我们很快便可以发现,原来指针名就可以直接访问函数,回想一下,我们在之前使用函数的时候,是不是直接通过函数名就调用函数了?其实我们是通过地址来访问函数的,知识又再次形成了闭环,可能有些读者朋友们会想,为什么在使用函数指针的时候要需要括号呢?其实是很简单解释的,如果没有括号,p首先会和后面的括号结合,我们在之前就说了,这就是调用函数,解引用操作符操作的是地址,而不是常量,所以这么写是明显错误的锕,大家一定要记住正确的格式!


总结:

今天我们讲了许多重要的内容,大家一定要好好的理解并运用,现在指针的知识我们已经讲了一大半了,我感觉我指针已经忘记很多了,今天的博客还是我通过复习得来的,这更加的说明了我们一定要温故而知新,知识就是要这样的,我们需要重复的去记忆,才能有助于我们学习,我也不多废话了,如果文章有误,请您在评论区指出,我会认真倾听你们的意见,我们下一篇博客见喽!

相关推荐
IT技术分享社区12 分钟前
C#实战:使用腾讯云识别服务轻松提取火车票信息
开发语言·c#·云计算·腾讯云·共识算法
极客代码15 分钟前
【Python TensorFlow】入门到精通
开发语言·人工智能·python·深度学习·tensorflow
疯一样的码农21 分钟前
Python 正则表达式(RegEx)
开发语言·python·正则表达式
&岁月不待人&43 分钟前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
StayInLove1 小时前
G1垃圾回收器日志详解
java·开发语言
TeYiToKu1 小时前
笔记整理—linux驱动开发部分(9)framebuffer驱动框架
linux·c语言·arm开发·驱动开发·笔记·嵌入式硬件·arm
无尽的大道1 小时前
Java字符串深度解析:String的实现、常量池与性能优化
java·开发语言·性能优化
互联网打工人no11 小时前
每日一题——第一百二十四题
c语言
爱吃生蚝的于勒1 小时前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~1 小时前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio