指针和数组
- 2021年3月13日完成初稿
本课程来自中国大学MOOC中《C语言程序设计精髓》 (哈尔滨工业大学)
或许乍看上去指针和数组是两个独立的数据类型,但如果意识到数组名代表数组首地址的话,或许可以尝试利用指针来代表数组或者数组中的某个元素。这一节就是讲述指针和数组之间的关系。
1. 指针的算术运算
指针是一个变量,其保存变量/函数的地址值,那么指针的算术运算是否有意义,指针的算术运算的规则又是什么?
正如所要讨论的那样,指针和数组有着密切的关系,并且当指针变量指向数组元素时,指针的算术运算才有意义,因为在内存中数组元素是紧挨着的,其地址是相邻的,那么保存地址的指针是否可以通过算术运算十分方便地对数组中的元素进行索引,答案是肯定的。接下来就要来叙述指针的算术运算。
1.1 指针加法/减法
如果一个指针变量p
指向某个数组元素a[i]
时,那么用指针变量方便地代替数组下标进行操作的想法是合理的,因此C语言中的确可以利用指针变量对数组元素进行索引,而且并不需要改变指针变量p
的值,只需要让指针变量加上/减去一个整数即可:
#include <stdio.h>
int main()
{
int i, array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} ;
int *p = array ;
for(i = 0; i < 10; i++)
{
printf("*(p+%d) = %d\n", i, *(p+i) ) ;
}
return 0;
}
*(p+0) = 0
*(p+1) = 1
*(p+2) = 2
*(p+3) = 3
*(p+4) = 4
*(p+5) = 5
*(p+6) = 6
*(p+7) = 7
*(p+8) = 8
*(p+9) = 9
上面的程序将指针变量p
指向数组array[]
首地址,通过*(p+i)
就可以访问元素array[i]
了。其原因是指针变量p
指向一个地址,而加上一个整数i
就相当于指针变量所指向元素的后面的第i
个元素了。这也就解释了为什么当指针变量指向数组元素是才是有效的访问。不过需要注意的是,像数组访问一样,指针变量访问数组元素也需要注意数组访问越界。
从数学上讲,定义了加法之后减法的定义是没有必要的,在此处也是如此,因为减去一个整数i
和加上一个整数i
的相反数是一致的。
1.2 两个指针相减
两个指针相加是没有意义的,而指向同一数组元素的两个指针相减是有意义的,以下面的例子为例:
#include <stdio.h>
#define MAXSIZE 5
int main()
{
int array[MAXSIZE+1] = {'a', 'b', 'c', 'd', 'e', '\0'} ;
int *p = array, *q = array;
while(*(q) != '\0')
{
q ++ ;
}
printf("len = %d\n", (int)(q - p)) ;
return 0;
}
len = 5
首先,指针的算术运算允许通过对指针变量重复自增来访问数组的元素,所以q++
是会改变指针变量q
的值,使其指向下一个地址块(一个指针变量所指向元素占用一个地址块)。需要注意的是,q++;
相当于q+=1;
,其会改变q
的值,因而不等价于q+1
。
另外当两个指针相减q-p
的结果为指针之间的距离,利用这一点可以用来计算数组中元素的个数,本程序的功能就是计算数组元素的个数。由此还可以扩展出指针的关系比较运算,因为p - q
是有意义的,那么p
和q
的大小关系也是判断的。
2. 指针和数组之间的关系
通过上面的讲述,指针和一维数组之间的关系是明确的,只要牢记a[i]
等价于*(a+i)
即可。因此下面讲述一下指针和二维数组的关系,而正确地看待二维数组是问题的关键:
2.1 将二维数组看成数组的数组
下面以数组int[2][3] a;
为例,来分析如何将二维数组看成数组的数组,在内存中数组a的存储可以看出下图所示:
正如上图所示,可以将二维数组a
看成一维数组,有2
个“int[3]
型”元素,即a包含2
个元素a[0],a[1]
,a[0],a[1]
又分别是一个
一维数组,包含3
个元素。a
代表二维数组的首地址,第0
行的地址,行地址,a+i
代表第i
行的地址。
如果这么理解二维数组,那么如果用指针去指向该数组时,就需要如下的定义:
/* 1. 2D arrays and pointers */
#include <stdio.h>
int main()
{
int a[2][3] = {0};
int (*p)[3] ; /* p is a pointer to int[3] */
a[1][1] = 1 ;
p = a + 1 ;
printf("a[1][1] = %d", p[0][1]) ;
return 0 ;
}
a[1][1] = 1
其中int (*p)[3] ;
定义了一个指针,指向int[3]
类型的元素,因此初始化p = a+1 ;
中使p
指向数组第1
行的地址,那么p[0]
就等价于*(p+0)
,其值是一个数组,等价于a[1]
,因此p[0][1]
和a[1][1]
相同。
2.2 将二维数组看成一维数组
换一个角度看待二维数组,可以到二维数组实质上是一维数组的排列:
如果这么理解二维数组,那么问题会变得方便,因为对于一维数组是容易理解的。以数组int[2][3] a;
为例,可以将二维数组a
看成一维数组,有6
个int
型元素,而且用指针指向该二维数组时也是简单的:
/* 2. 2D arrays and pointers */
#include <stdio.h>
int main()
{
int a[2][3] = {0} ;
int *p ; /* p is a pointer to int */
a[1][1] = 1 ;
p = a[0] ;
printf("a[1][1] = %d", *(p+1*3+1)) ;
return 0 ;
}
a[1][1] = 1
不过需要注意的是,在对指针变量p
初始化的时候有p = a[0] ;
因为a[0]
等价于*(a+0)
,是第0
行的地址,是一个int*
数据,而不能把a
直接赋值给p
,因为a
不是一个int*
型数据。不过访问元素的时候,应注意二维数组的行列,对于数组int[2][3] a
而言,p
指向指向第0
行第0
列的int
型元素,p[i*n+j]
即为a[i][j]
,这一点区别是特别需要注意的。
最后,指针与二维数组间的关系的关键在于理解二维数组的行指针和列指针,二维数组在内存中按行存储,但可以以两种方式看待它,始终牢记一个x
型的指针指向x
型的数据就不会出错了。
3. 指针数组及其应用
3.1 指针数组及其在字符串处理中的应用
在前面,可以利用二维数组存储多个字符串,每一行存储一个字符串,但是由于每个字符串都是不等长的,因而必须以最长的那个字符串来定义二维数组的列数,因此比较浪费空间。在C语言中,可以利用指针来指向一个字符串,那么用一个数组保存这些指向字符串的指针是十分自然的,则就是本节想讨论的字符指针数组,它的每个数组元素都是一个指向字符串的指针。下面看一个例子:
#include <stdio.h>
int main()
{
char *country[] = {"America", "England", "Australia", "China", "Finland"};
printf("%s", country[3]) ;
return 0 ;
}
China
在这里,用一个数组存储指向字符串的指针,然后通过指针对该字符串进行索引,在内存中有下图所示:
不过值得注意的是,字符串的存储位置在只读存储区,是不能修改字符串的内容的,但是可以修改指针的指向。另外char *country[]
和char(*country)[]
是不一样的,前者是一个指针数组,后者是一个指向数组的指针,其中的差别需要读者好好体会。
3.2 命令行参数
在C语言中,命令行是不常见的,命令行有一定的历史,曾经在各类操作系统中十分常见,但随着图形化界面的采用,命令行被认为是复杂的、只被专业人士使用的。不过了解命令行是有一定帮助的,毕竟实质上图形化界面的所有看似简单的操作都是基于命令行实现的,只不过这种命令行是被装入小黑盒里面了。
本节将用int main(int argc, char* argv[])
来介绍命令行参数的使用。对于下面的程序,大家或许有点熟悉又有点陌生。在这里,使用int main(int argc, char *argv[])
作为主函数正为了弄懂命令行参数的作用。
/* Command line parameters */
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
printf("The number of command line arguments is:%d\n", argc);
printf("The program name is:%s\n", argv[0]);
if (argc > 1)
{
printf("The other arguments are following:\n");
for (i = 1; i<argc; i++)
{
printf("%s\n", argv[i]);
}
}
return 0;
}
对于函数int main(int argc, char *argv[])
,其中的形参都是命令行参数,
-
argc
:命令行参数的数量(包括程序名本身) -
argv
:指向命令行参数的指针数组 -
argv[0]
为指向程序名的字符指针 -
argv[1]~argv[argc-1]
为指向余下的命令行参数的字符指针
在code::blocks中直接编译运行该程序,只有程序名本身被计算在内:
The number of command line arguments is:1
The program name is:D:\Documents\Desktop\myproject\Cstudy\Cprogram\bin\Debug\Cprogram.exe
不过在code::blocks中的project的set program’s arguments中的program argument窗口中输入其余的命令行参数,并编译执行则有:
/* input */
Hello, world!
The number of command line arguments is:3
The program name is:D:\Documents\Desktop\myproject\Cstudy\Cprogram\bin\Debug\Cprogram.exe
The other arguments are following:
Hello,
world!
或许在这里还不能体会命令行参数的作用,但是如果学习Linux的同学会明白,在linux中需要熟练地使用命令行来操作。
4. 往期回顾
1.【学习札记】C语言程序设计(第一周):变量和常量
2.【学习札记】C语言程序设计(第二周):C运算符和表达式
3.【学习札记】C语言程序设计(第三周):输入和输出
4.【学习札记】C语言程序设计(第四周):选择结构
5.【学习札记】C语言程序设计(第五周):循环结构
6.【学习札记】C语言程序设计(第五周补充):循环结构练习题
7.【学习札记】C语言程序设计(第六周):函数
8.【学习札记】C语言程序设计(第七周):函数递归和函数变量
9.【学习札记】C语言程序设计(第七周补充):函数练习题
10.【学习札记】C语言程序设计(第八周):数组
11.【学习札记】C语言程序设计(第八周补充):数组练习题
12.【学习札记】C语言程序设计(第九周):指针
13.【学习札记】C语言程序设计(第十周):字符串