文章目录
字符指针变量
我们知道有一种指针类型为字符指针:char*
我们原来对字符指针的用法:存放一个字符的地址
现在我们再学习一种,字符指针可以指向一个常量字符串
我们写段代码:
int main()
{
//char ch = 'w';
//char* pc = &ch;
此时pc就是一个指针变量,可以存放一个字符的地址
char arr[10] = "abcdef";
char* p = arr;
return 0;
}
我们将这段代码的图画出来:
我们再写一种代码:
int main()
{
//char ch = 'w';
//char* pc = &ch;
此时pc就是一个指针变量,可以存放一个字符的地址
//char arr[10] = "abcdef";
//char* p = arr;
char* p = "abcdef";
return 0;
}
写出这段代码我们肯定会疑问:
这句代码是把这个字符串完整的赋值给p
吗?
我们对这段代码进行编译,发现编译器并没有发出警告:
说明这种写法是正确的。
那么这种写法的意思是什么呢?
注意:这个字符串是常量字符串,上面那个是字符数组。
我们敲出来的这个常量字符串在内存中也有属于它自己的空间。
char* p = "abcdef";
这条代码是将它首字符的地址放进指针变量p
里面去:
因为常量字符串在内存中也是连续存放的,所以找到首字符的地址,也就可以找到其他字符了。
我们可以看到,这两种形式非常的像,那么字符数组与常量字符串的区别是什么呢?
区别就在于:
第一个p
指向的是数组,数组的内容是可以被改变的。
但是第二个p
指向的是常量字符串,常量字符串的内容是不可以被改变的。
我们不妨试一下:
int main()
{
char arr[10] = "abcdef";
char* p1 = arr;
*p1 = 'w';
//试着将a改为w
char* p2 = "abcdef";
*p2 = 'w';
//也试着将a改为w
return 0;
}
写这样一段代码进行调试:
首先查看arr
数组里的内容:
确实放着10个元素,接着往下:
当我们执行完*p1 = 'w'
后第一个元素确实被改变为w
了:
接着往下调试:
接着往下调试:
执行到*p2 = 'w'
时却报出警告:
这是因为abcdef
是个常量字符串,常量字符串是不能被修改的,一旦修改它,就会报错。
所以:数组的内容是可以被修改的,而常量字符串是不能被修改的。
此时这段代码应该这样写:
在*
的左边加上const
,这样就限制了p2
指向的内容,一旦内容出现修改,编译器就会报错,可能出现的错误就直接被扼杀在编译期间了。
正确写法:
int main()
{
//char arr[10] = "abcdef";
//char* p1 = arr;
//*p1 = 'w';
//试着将a改为w
const char* p2 = "abcdef";
//也试着将a改为w
return 0;
}
所以const char* p2 = "abcdef"
这种写法并不是将整个字符串都放进指针变量里,而是将首字符的地址放进指针变量里。
接下来我们将它们打印出来:
这里为什么写一个p1
,p2
就能打印出来呢?
因为我们打印字符串的时候只提供一个起始地址就可以了,就像之前一直写的printf("%s\n",arr)
当我们用
%s
打印字符串的时候,只需要提供起始地址就行了。
不需要解引用,用了解引用是错的。
因为*p1
是a
,一个字符不能用%s
打印。
我们接下来看《剑指offer》中收录的一道和字符串相关的笔试题:
int main()
{
char str1[] = "hello bit.";
char str2[] = "hello bit.";
const char* str3 = "hello bit.";
const char* str4 = "hello bit.";
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
,然后再用"hello bit."
初始化了这两个数组:
因为两个数组是在不同的内存空间里,所以str1
所对应的h
的地址与str2
对应的h
的地址不同,并不是同一个地址,所以str1
不等于str2
,打印str1 and str2 are not same
那为什么str3
与 str4
相等呢?
因为hello bit.
在这里是个常量字符串:
常量字符串是不能被修改的,既然不能被修改,所以在内存空间里只有一个字符串。
因为这里str3
和str4
指向的是一个同一个常量字符串:"hello bit."
。
既然是同一个常量字符串,那么str3
和str4
中的地址都是同一块空间,所以str3==str4
。
那为什么指向的是同一个常量字符串呢?
因为常量字符串是不能被改变的,相同的常量字符串没必要保存两份(只有能被改变的才有必要存原本的字符串进行备份),所以在内存中只有一份这样的常量字符串,即str3
和str4
指向的都是同一个常量字符串,所以str3
和str4
里面存的地址相同。
我们可以进行调试看看str3
和str4
存的地址是否像我们说的相同:
数组指针变量
数组指针变量是什么?
之前我们学习了指针数组,指针数组是一种数组,数组中存放的是地址(指针)。
那么数组指针变量是指针变量、还是数组呢?
答案是:指针变量。
我们已经熟悉:
- 整型指针变量:
int* pint;
存放的是整型变量的地址,能够指向整型数据的指针。 - 浮点型指针变量:
float* pf;
存放的是浮点型变量的地址,能够指向浮点型数据的指针
所以,数组指针变量就应该是:存放的是数组的地址,能够指向数组的指针变量。
例如:
我们取出整个数组的地址,将数组的地址放进指针变量p
中去,这个p
就是数组指针变量
注意:我们要将数组指针与指针数组区分开
指针数组:是数组,是存放指针的数组
数组指针:指向数组的指针(变量)
那么这个p
的类型是什么呢?
我们首先来分析一下这段代码,看p1
是指针还是p2
是指针呢?
-
对
p1
的分析:
我们可以看到,p1
左边有个*
,右边放着[10]
,它首先是与方块结合的。
既然是与方块结合的,那么p1
就是一个数组名,方块里表示的就是这个数组里有几个元素。
最后前面的int*
表示p1
数组里面的10个元素都是int*
类型的
结论:p1
是个指针数组——存放指针的数组 -
对
p2
的分析:
我们可以看到p2
与前面的*
被括号括起来了,所以它没办法与后面的方块结合了。
我们在深入理解指针(1)中学习过,变量左边放个*
就表示这个变量是指针变量:
所以p2
前面放个*
就表示了这个p2
是个指针变量。
既然我们说p2
是指针变量,那么它指向什么呢?
我们看到括号的后面有个[10]
,就说明这个指针变量指向的应该是个数组,这个数组有10个元素,又因为最前面有个int
,所以数组的每个元素都应该是int
类型的。
我们之前学过数组是有类型的,数组名去掉就是这个数组的类型:
所以p2
是个指针变量,指向数组类型为int [10]
的数组
结论:p2
是个指针变量,指向的是数组
既然p2
是个指针变量,指向的是数组,所以p2
就应该是个数组指针变量。
而p
也是数组指针变量,所以p
类型的写法就与p2
类型的写法相似:
p2
指向10个元素都为int
的数组,写成int (*p2)[10]
p
也指向10个元素都为int
的arr
数组,所以写成:int (*p)[10]=&arr;
拆解:
首先p
得是个指针,所以要在p
前面加个*
说明:
*p=&arr;
下一步加括号来保证它俩先结合:
(*p)=&arr;
强调:括号一定不要掉!掉了会与后面的方块结合,此时p
就会变为数组名,成为指针数组,而不是指针变量
int* p[10]=&arr;
//err
最后:让它指向10个元素都为int
的数组:
int (*p)[10]=&arr;
解释::p
先和*
结合,说明p是一个指针变量,然后指针指向的是一个大小为10个整型的数组。
最后形态:
即p
是⼀个指针,指向⼀个数组,叫做数组指针。
数组指针变量怎么初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?
这就是我们之前学习的:&数组名
int arr[10]={
0};
&arr;//得到数组的地址
如果要存放数组的地址,就得存在数组指针变量中,数组指针的初始化:
int (*p)[10]=&arr;
这里我们再梳理一些知识:
由这段代码:
int main()
{
int arr[10]={
0};
int* p1=arr;
int(*p2)[10] = &arr;
return 0;
}
我们可知p1
的类型为int*
那么数组指针变量p2
的类型是什么呢?
去掉变量名就是该数组指针变量的类型了,p2
的类型为:int(*)[10]
我们让这两个指针分别+1,再将地址打印出来观察:
可以观察到p1
+1跳过了4个字节,p2
+1跳过了40个字节
我们之前学过:指针类型决定了指针进行+1或-1操作的时候,一次跳过多少个字节。
例如:
字符指针+1跳过1个字节;整型指针+1跳过4个字节…这里的数组指针+1就跳过一整个数组的字节大小。
int arr[10]
这个数组共有10个元素,每个元素4个字节,所以共跳过了40个字节。
这与深入理解指针(2)(数组与指针)中的这段笔记相似:
当p2
跳过一整个数组时指向哪里呢?
我们画个图:
当p2
跳过一个数组的大小时,我们并不知道这个指针最后指向哪里了(此时这个指针指向的位置是不可知的)这样会不会造成野指针呢?
答案是不会
总结:p2 + 1
本身不是野指针,因为它指向的是一个确定的内存地址(arr数组后的那个地址)。然而,对 p2 + 1
进行解引用操作是危险的,可能会引发程序异常,因为它超出了数组 arr 的有效范围。
那么数组指针到底有什么用呢?
它非常可能应用错场景。
例如,我们写段代码用指针遍历打印出数组内容:
那我们今天学过数组指针变量后,那我们能不能这样写呢?
我们将整个数组的地址取出来放进数组指针变量p
中:
int main()
{
int arr[10] = {
1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
return 0;
}
用数组指针变量打印出数组所有的元素:
对(*p)[i]
该如何理解呢?
我们在深入理解指针(1)中学习过,&
与*
可以相互抵消:
*&arr=arr
这条语句就是取出arr
的地址,再对这个地址进行解引用,其实最后找到的就是arr
例如:
所以在这段代码中,p
就是&arr
。再在p
前面加上*
后得到的就是个数组名
(*p)==(*&arr)==arr
此时要打印出所有元素就得使用下标打印
arr[i]
虽然这种写法最终也可以打印出正确结果,但是这是个不好的示范,不推荐这样使用数组指针。
数组指针可以使用在二维数组的传参上。
二维数组传参的本质
在过去,我们有一个二维数组需要传参给一个函数时,是这样写的:
我们创建个二维数组,再写个函数将这个数组打印出来
学一维数组时我们说:数组名是数组首元素的地址,那么对于二维数组来说是否也一样呢?
答案:对于二维数组来说,数组名也是数组首元素的地址。
那么对于这个二维数组来说,到底谁是首元素呢?真的是数字1吗?
我们之前学习二维数组的时候,说过:把一维数组做为元素,这个数组就是二维数组
所以,二维数组的每个元素是一个一维数组,二维数组的首元素就应该是第一行,首元素的地址就是第一行的地址。
而第一行的地址就是一个一维数组的地址,所以形参接收时的指针类型应该是数组指针类型
虽然二维数组在我们眼里是有行有列的,但是在内存中是连续存放的。
所以:arr
是指向第一行,让它+1就会跳过整个一维数组来到第二行…
完整代码:
void print(int(*arr)[5],