本篇博客是对指针万字详解的补充,其中包含一些巧妙的理解,如果感觉这篇博客的内容看不太懂,可以参考上一篇指针博客😁😁
文章目录
🚩字符指针
字符数组本质上非常简单,但是有几个需要注意的点:
1.字符指针指向的是一个常量,常量存在于只读数据区,所以不能进行修改。
我们看以下代码,我们定义了一个字符指针,并让他指向一个常量,这时候你想通过解引用去修改它,最后的结果只能是程序崩溃。
char*a = "abcdef";
*a = "w";
可以理解为,在创建这个字符指针的时候,计算机先创建了一个常量字符串"abcdef",然后再让字符指针指向了这个字符串,这个字符串存储在计算机内无法修改的地方,所以程序会崩溃。
2.当你用两个字符指针指向相同的常量字符串时,存储的地址是一样的 我们进行以下操作:
char *a = "abcdef";
char *b = "abcdef";
printf("%p %p",a,b);
这也说明了一件事,系统不会创建相同的常量,当再次让一个字符指针指向相同的字符串时,系统会找到之前创建的常量,而不是重新创建一个重复的常量。
3.字符指针与数组的区别
虽然之前我们说指针和数组有相同之处,但是在字符串的世界里,他们有着显著的区别。
请看以下代码:
char a[] = "abcdef";
a[0] = 'o';
printf("%s", a);
char a[] = "abcdef";
char b[] = "abcdef";
printf("%p %p",&a,&b);
我们之前说不能对常量字符串进行修改,相同常量字符串地址相同好像都不成立了,但事实真的如此吗?
其实你现在创建的"abcdef"根本不是常量,它并不储存在系统的只读区域。
仔细观察上面的代码,我们是先申请了一片空间给字符数组,然后再把字符串初始化给字符数组,是对数组里的元素赋值,并不是先创建了一个字符常量,那当然可以修改了。
🚩指针数组与数组指针的使用
在上一篇文章中我们简单的介绍了一下数组指针与指针数组,现在我们深入了解一下。
♨️♨️♨️1.指针数组模拟二维数组
在模拟二维数组之前,我们先来补充一些二维数组的知识。假设定义了一个三行五列的二维数组,并赋上若干值。
按照我们平时的理解,这个二维数组是按照行和列存放的,就像上图写的那样,但是实际上,数组存储是一条线存储的,并没有换行。
通过监视,我们可以看到这个数组每个元素的地址都是相邻的,并没有达到真正的二维,我们可以说,这个二维数组仍然是一个一维数组,只不过元素只有三个,分别代表每个行,一行中有五个整数而已。
现在我们来用指针数组模拟实现这样一个二维数组,在上一篇对指针的讲解中,我们说指针数组是一个数组,只不过存储的变量是指针变量。我们想要模拟二维数组的形态,就要让一个数组元素包含五个整数,这时就用到了指针数组,具体操作如下:
可以看到,我们让指针数组中的三个指针变量指向了三个一维数组,模拟实现了一个二维数组,至于为什么在监视中三个数组只显示了首元素1?这是因为指针数组存储的是p[0],p[1],p[2],计算机会翻译为*(p+0),*(p+1) ,这样的形式。
p[0]中的指针指向的是整个a数组,也就是数组名,数组名相当于首元素的地址,首元素的地址存储的值就是1。
既然p[0]中存储的值是a的首元素的地址,那我们对p[0]再进行解引用*(p[0]),那得到的就是a[0]的值,也就是1,我们知道p[0] == *(p+0),我们现在进行的操作就可以用p[0][0]来表示,这样看起来是不是更像一个二维数组了?代码表述如下:
int a[5] = { 1,2,3,4,5, };
int b[5] = { 1,2,3,4,5, };
int c[5] = { 1,2,3,4,5, };
int* p[3] = { a,b,c };
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 5; ++j)
{
printf("%d ", p[i][j]);
}
printf("\n");
}
♨️♨️♨️2.数组指针的应用
无论是指针数组,或是数组指针,他们都与二维数组有密切的联系,数组指针是指向有固定数目元素数组的指针,而二维数组每一行的元素个数都是固定的,所以我们也可以用数组指针来打印一个二维数组。具体操作如下:
void ArrPrintf(int(*p)[5], int c, int r)
{
for (int i = 0; i < c; ++i)
{
for (int j = 0; j < r; ++j)
{
printf("%d ",*(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int a[3][5] = { {1,2,3,4,5},{1,2,3,4,5}, {1,2,3,4,5}, };
ArrPrintf(a, 3, 5);
return 0;
}
这里有一个有点复杂的表达式。*(*(p+i)+j)
,我们来单独解释一下他,我们先简单的分析一下:
p是我们函数的参数,是一个有五个元素的数组指针,而他指向就是二维数组的第一行,他就相当于是&a,(我们之前说过,只有两种特殊情况数组名是指整个数组的,一种就是&数组名,另一种是sizeof(数组名)),那么我们对p+1,字长是一整行,自然就跳跃到了第二行,加i就跳跃到跳跃到第i行,那对p进行解引用呢? p是&a,那解引用就是*&a也就是a,a是数组名,也就是&a[0],此时数组名的意义是数组首元素的地址,那么再次解引用,得到的就是数组首元素的值了。
有点绕,可以画图理解一下:
(图有点小了,见谅)
♨️小练习
我们来看以下三行程序,并说出他的含义:
int *a[10];
int (*a)[10];
int (*a[10])[5];
请思考完毕后查看答案:
- 指针数组,因为*号的结合力没有[ ]强,所以a先于[ ]结合,成为一个数组,int * 就是数组里每个元素的类型。
- 数组指针·,因为加了括号,所以a先与*结合,变成一个指针,这个指针指向的类型应该是 int [10],一个含有十个元素的数组。
- 这个看起来有点复杂,我们可以采用删减的思想来看他,首先看a与谁结合了,a先和[ ],结合了,说明他是一个数组,数组由的定义由数组名和数组元素的类型构成,我们把数组名删掉,剩下了一个int (*)[5],这就是数组元素的类型,所以这是存储数组指针的数组。
注意:无论是我们分析复杂的程序,还是类型,我们都可以用这种删减的思想,看原本应该有什么,我现在有了什么,把有的删去,剩下的就是我们想要的结果了。
🚩3.数组传参与指针传参
一维数组传参
首先我们来看一维数组传参
void ArrPrint(int a[5],int size)
{
for (int i=0; i < size; ++i)
{
printf("%d ", a[i]);
}
}
int main()
{
int a[5] = { 1,2,3,4,5, };
ArrPrint(a, 5);
return 0;
}
我们可以使用直来直去的传参方式,要传进去一个有五个元素的数组,那我们就在参数上写一个五个元素的数组,当然int a[5]中的5是没有用处的,他只是提醒你你传进去的是有五个元素的数组而已,你在里面填9,10,什么数都可以,因为他是没有实际意义的。
我们常说数组和指针是密切联系的,可以用数组就可以用指针,因为我们传进去的a,是数组首元素的地址,指针当然是可以接收地址的,指针形式如下:
void ArrPrint(int *a,int size)
{
int* q = a;
for (q = a; q < a+size; ++q)
{
printf("%d ", *q);
}
}
这里使用纯指针的形式打印一维数组,也是可以的。
♨️♨️二维数组传参
在之前我们说过,二维数组其实就是一维数组,而二维数组传参打印,我们在描述数组指针和指针数组时也已经写过了,这里不再赘述。
但是值得注意的是,二维数组与二级指针没有什么关系,二级指针是储存一级指针地址的指针,而二维数组,实质上是加长版一维数组,他的类型仍然是一级指针,只不过对于二维数组,它是由三个行元素组成的,每一个行元素可以用数组指针来表示:
可以看到,使用数组指针来接收二维数组是允许的,而二级指针则会出现问题。
🚩总结
这篇博客实质上是对指针基础内容的补充,谈不上指针的进阶,而在下一篇博客中,就要真正的增加难度了,有问题欢迎指正,分享不易,希望大家多多支持😎😎