在上一篇文章里,介绍了一些指针的基本内容及其应用。接下来,会介绍指针和数组之间的关联以及应用。
9.1 数组名的理解
在之前数组的文章中,我们介绍过,数组名代表的是数组首元素的地址,但这有两个例外
• sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
c
#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);
printf("%d\n", sizeof(arr));
return 0;
}
运行结果
• &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)
c
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0] + 1);
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr + 1);
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr + 1);
return 0;
}
运行结果
通过上面的代码,可以观察到,&arr[0]和&arr[0]+1之间相差了4个字节,而&arr和&arr+1之间相差了40个字节。这就是&arr和&arr[0]之间的区别,虽然看上去取出来的都是地址,但在应用时是不一样的。
除此之外,任何地方使用数组名,都代表的是数组首元素的地址。
9.2 指针与一维数组
下面来介绍一下指针在一维数组中的应用,如何利用指针去对一维数组进行操作。
9.2.1 使用指针访问数组
由于数组名代表的是数组首元素的地址,可以直接将数组名作为地址赋值给一级指针变量,再通过指针之间的运算及解引用操作符,去对其进行访问
c
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
printf("%d ", *(p + i));
return 0;
}
运行结果
9.2.2 一维数组传参的本质
学完指针之后,可以来讨论一下一维数组传参的本质。在上文中提到,数组名的本质就是数组首元素的地址,那么在进行传参时,传入的也是数组首元素的地址。通过下面的代码可以观察到。在test函数中,由于传参传入的是数组首元素的地址,所以用sizeof并没有获取到整个数组的大小,而仅仅是数组首元素的大小
c
#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;
}
运行结果
总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。
9.3 指针数组
下面介绍一下指针数组的概念。指针数组其实通过它的名字可以看出,它的本质是一个数组,里面存放的内容是指针。定义方式如下
c
int* arr[5]; //数组中的5个元素都是指向整型变量的指针
char* str[10]; //数组中的10个元素都是指向字符变量的指针
通过下面的图可以更加直观的理解指针数组
下面我们使用指针数组来模拟实现一个二维数组。注意,这里是模拟实现二维数组,并不是二维数组在传递参数时的真正的形式。由于指针数组中每个变量都是指向同一变量类型的指针,所以我们可以让这些指针指向不同的数组,并将这些指针变量存到数组指针中,这样就模拟实现了一个二维数组,代码实现如下
c
#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[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
printf("%d ", parr[i][j]);
printf("\n");
}
return 0;
}
通过下面的图可以更加直观的理解上述代码
9.4 指针与二维数组
本节来窥探指针与二维数组之间的关系及应用。先介绍数组指针,并讲述其与二维数组的关系。
9.4.1 数组指针
数组指针与指针数组虽然名字上很相似,但两者有很大差别。上文说到,指针数组的本质是数组,而数组指针的本质是一个存放整个数组的地址的指针变量。它的定义格式如下,其中int代表指向数组的类型,(*p)表示p是一个指针变量,后面的[5]表示指向数组的变量元素个数
c
int arr[5] = { 1,2,3,4,5 };
int(*p)[5] = &arr; //指向arr数组的指针变量
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
int* parr[3] = { arr1,arr2,arr3 }; //指针数组,里面存放的都是指针变量
int* (*p2)[3] = parr; //指向指针数组的指针变量
使用数组指针遍历数组的方式如下,由于该变量存放的是整个数组的地址,对其解引用后就得到了数组,之后再利用下标访问的方式就可以遍历数组了,下面代码中的两种写法都是可以实现目标的
c
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int(*p)[10] = &arr;
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
//printf("%d ", *((*p) + i)); //这种写法也是可以的
printf("%d ", (*p)[i]);
return 0;
}
9.4.2 二维数组传参的本质
有了数组指针的知识,就可以窥探二维数组传参的本质了。在之前将二维数组作为参数传给一个函数时,写法是这样的
c
void test(int a[3][5], int row, int col);
但在本人数组那篇文章介绍二维数组时提到过,二维数组的本质其实就是将几个一维数组存放到一个数组中,就成了二维数组。上文提到过,数组名是数组首元素的地址,而二维数组的数组名也同样如此,那么二维数组的首元素是什么呢?其实它就是那个指向一维数组的数组指针,传参时的写法也可以写成下面这样
c
void test(int (*p)[5], int row, int col);
这种写法才是二维数组传参的本质,传入了二维数组首元素的地址,并通过这一地址可以进而访问其余元素,使用数组指针遍历二维数组的写法如下,其中arr+i表示二维数组中的数组指针,对其解引用后就找到了对应的一维数组,之后利用下标访问的方式即可遍历整个二维数组
c
#include <stdio.h>
int main()
{
int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
//printf("%d ", *(*(arr + i) + j)); 这种写法也是可以的
printf("%d ", (*(arr + i))[j]);
printf('\n');
}
return 0;
}
运行结果
9.5 字符指针与字符数组
字符指针用来存放字符变量的地址,并通过这一指针,可以完成对变量的各种修改操作,使用方式如下
c
#include <stdio.h>
int main()
{
char a = 'b';
char* pa = &a; //用字符指针存放变量a的地址
*pa = 'c'; //通过对指针变量解引用改变变量a的存储内容
printf("%c", a);
return 0;
}
除了这种使用方式之外,我们也可以让字符指针指向字符串或者字符数组。由于字符串中的每个字符都是连续存放的,我们可以使用字符指针指向该字符串的首字母,再根据其连续存放的特点,就可以找到整个字符串,直到'\0'为止。所以下面的代码写法并不是用字符指针存放了整个字符串,而是存放首字母的地址,再通过字符串连续存放的特点找到整个数组
c
#include <stdio.h>
int main()
{
char* p = "effective";
printf("%s\n", p); //以%s的方式打印整个字符串
//p[0] = 'a'; //不能对常量字符串进行任何的修改
return 0;
}
需要注意的是,这里的p变量默认存放的是常量字符串,也就是说,对这个字符指针指向的字符串只能进行"读"的操作,而不能进行任何修改操作,否则程序无法正常运行。所以严谨的写法如下
c
const char* p = "effective";
而使用字符数组存放字符串时,是可以进行任何的读写操作的。这也是字符指针存储字符串和使用字符数组存储字符串的区别。
下面来看一段代码,读者可以猜一猜它的运行结果
c
#include <stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char* str3 = "hello world.";
const char* str4 = "hello world.";
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;
}
根据上文的知识可以知道,字符指针存放的是常量字符串,str3和str4都指向了同一个常量字符串,没有必要开辟两块不同的空间。而每个字符数组是单独开空间存储相应的内容,只是在上面的程序中,存储的内容是一样的,但其实它们的地址是不一样的,运行结果如下
9.6 二级指针
指针变量也是变量,只要是一个变量,就有它自己的地址。存储指针变量的地址,需要用二级指针。具体的代码使用如下
c
#include <stdio.h>
int main()
{
int a = 5;
int* pa = &a;
int** ppa = &pa; //用二级指针变量存储一级指针的地址
**ppa = 10; //通过二级指针改变变量a
printf("%d\n", a);
return 0;
}
通过下面的图示可以更好的理解二级指针
由此可以衍生出三级指针,四级指针的概念,三级指针存放二级指针变量的地址,四级指针存放三级指针变量的地址。不过二级指针的使用范围就已经比较狭隘了,三级,四级指针使用的非常少,这里就不再赘述了。