我们先来看两组代码:
#include
int main()
{
char* p1 = "abcdef";
printf("%s", p1);
return 0;
}
#include
int main()
{
char arr[] = "abcdef";
printf("%s", arr);
return 0;
}
第二组是我们平时常用的写法,那么第一组为什么会能和第二组打印出相同的效果呢?
我们看第一组的代码:我么把常量字符串 “abcdef” 赋给了 char* 型指针变量 p1 ,那么 p1 里面存的地址是什么?我们可以通过下面这个代码来求解:
#include
int main()
{
char* p1 = "abcdef";
printf("%c", *p);
return 0;
}
这串代码的意思是,我把常量字符串 “abcdef” 赋给了 char* 型指针变量 p1 ,然后直接对其解引用打印,屏幕上显示 a。
由此我们得出结论:把常量字符串直接赋给指针变量,指针变量里面存放的是常量字符串的首元素地址。这与第二组的第二条语句十分相似(数组名表示首元素地址)。
好了,现在我们可以理解那两组代码了:哦,原来啊,printf后面写的都是首元素地址啊!
但是,为什么拿到首元素地址,并且还不用解引用就能直接打印出字符串了?这就与 %s 自身有关了,其原理大概就是,%s 会对通过你提供的首元素地址找到字符串,然后一直遍历到 "\0" 。
上面我经常提到一个名词——常量字符串。
什么是常量字符串?
我们先定一下这个“常量字符串”的属性——常量,既然它是常量,那它可以被更改吗?
#include
int main()
{
//常量不能更改,我们用const保护
const char* p1 = "abcdef";
const char* p2 = "abcdef";
char ch1[] = "abcdef";
char ch2[] = "abcdef";
if (p1 == p2)
printf("p1 == p2\n");
else
printf(" p1 != p2\n");
if (ch1 == ch2)
printf("ch1 == ch2\n");
else
printf("ch1 != ch2\n");
return 0;
}
大家可以猜一猜运行结果,真正的结果会令你大跌眼镜。
可以发现运行结果,p1 == p2 ;ch1 != ch2 。
这是什么原因?
事实上,p1,p2存放的都是常量字符串 “abcdef” 的首元素地址,这个字符串是放在只读数据区的。我们只要在内存中找到这个字符串,把它的首元素地址分别拿给 p1 ,p2 就好了。
但是对于数组来说,每次创建都是独立开辟一块空间,每块空间的地址是不一样的,所以虽然空间里面的东西一样,但是空间(即地址)不一样。
指针数组,我们需要注意,它的本质是一个数组。
好,本质是一个数组,那么我们可以回想一下数组的定义:同一种类型元素的集合叫做数组。
指针数组指针数组,它的元素类型应该是指针类型,即用来存放指针的数组。
#include
int main()
{
//指针数组的定义例子
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* p[3] = { arr1,arr2,arr3 };
return 0;
}
我们强调过很多次了,数组名表示的是首元素地址。可以看到代码段的第四条语句,我把每个数组的首元素地址都放在一个 int* 类型的数组中。数组里面的元素类型都是 int* 的。
接下来我们谈谈指针数组的用法。
#include
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* p[3] = { arr1,arr2,arr3 };
int i = 0;
for (i = 0; i < 3; i++)
{
printf("%d ", *p[i]);
}
return 0;
}
这组代码我们是可以打印数组里面的元素的,而数组里面的各个元素又是每个数组的首元素地址,那么运行结果是:
但如果仅仅是这样,我们何必大费周章的用指针的形式来打印呢?所以这并不是指针数组的正常用法。
其实,我们可以运用指针数组来实现二维数组。
#include
int main()
{
int arr1[5] = { 1,2,3,4,5 };
int arr2[5] = { 2,3,4,5,6 };
int arr3[5] = { 3,4,5,6,7 };
int* p[3] = { arr1,arr2,arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", *(p[i] + j));
}
printf("\n");
}
return 0;
}
这组代码运行的结果是:
可以发现我们只是加了一层循环,并对指针做了一些改动,就能实现二维数组的打印。
现在我们对 *(p[i]+j) 作出解释:
那 p[i]+j 又是什么意思?因为是两层循环,所以当 i 为 0 的时候,j会的值会从 0 到 4 之间++ 。
那我们知道指针的运算:指针加减一个数,能够跳过指针类型对应的字节大小。
我们把 p[i]+j 用括号括起来 (p[i]+j)再对其解引用 *(p[i]+j) 就可以访问到每个数组的元素。
我们先来谈一谈它的属性。
首先我们举几个例子:
int* 整型指针
char* 字符指针
float* 浮点型指针
那数组指针?对啦,数组指针的实质是指针,是指向数组的指针。
但是,上面我们一直再说,数组名是首元素的地址,这怎么会是数组呢?
这时候我们就忽略了一个符号,那就是 & 。
也就是 arr 和 &arr 两个性质不一样,那我们怎么证明?
我们有一组代码:
#include
int main()
{
int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%p\n", arr1);
printf("%p\n", &arr1);
printf("%p\n", arr1 + 1);
printf("%p\n", &arr1 + 1);
return 0;
}
我们对数组其地址打印,分别观察arr打印什么地址,&arr打印什么地址;再观察arr1+1打印什么地址,&arr1+1打印什么地址。
我们对前两个打印结果分析:arr1 和 &arr1 的地址一样,证明这两个表示的都是首元素的地址。
我们再对后两个打印结果分析:
arr1+1后跳过 0C-08=4 个字节,跳过了一个元素。
&arr1+1后跳过 30-08=28,转化为十进制为 40,也就是跳过了40个字节,40个字节整个数组的大小,意思就是跳过了整个数组。
由此可见,arr1 仅仅是首元素的地址,而&arr1 表示的是整个数组的地址,只是用其首元素地址来表示而已。
好,了解了区别之后我们看一组代码:
#include
int main()
{
int arr[2][3] = { 1,2,3,3,2,1 };
int arr[][] * p = &arr;//这个表达式正确码?
return 0;
}
这个表达式正确吗?按照我们以前所学的 整型指针(int*),字符指针(char*)等等,我们主观意识认为这个写法是没有错误的,不是数组指针吗?那我就给你写个数组类型。其实不然,这是不符合语法规定的。
正确的写法应该是:
#include
int main()
{
int arr[3] = { 1,2,3};
int (*p)[3] = &arr;
printf("%d", *(*p+1));
return 0;
}
这个写法是C语言标准规定的。
所以在代码中的printf语句中,*(*p+1)代表:
p 解引用 *p 得到数组名,即首元素地址,*p+1 则代表往后跳过一个整数类型,即指向了数组元素中的 2,再对其解引用 *(*p+1) 就能得到 2。
我们从这个代码的角度出发:
#include
void print1(int(*p)[3], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
void print2(int(*p)[3], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[2][3] = { 1,2,3,3,2,1 };
print1(arr,2,3);
print2(&arr, 2, 3);
return 0;
}
可以看到,print1和print2用了不同的方法传参,但都用了同一种方法接收参数。这不得不提到二维数组的特性,但因为本篇文章是专门这对指针的,这里不再赘述。关于数组的博客我会跟进。
我们需要总结的是,当我们传递参数的时候,传递的是什么类型,对应的形参参数也应该是同种类型。就像我们这一组代码,传递的是形参分别是 arr数组首元素地址(实际上也是一个数组)、arr数组地址。