5、指针
1、指针概念
在计算机内部存储器中,每一个字节单元,都有一个编号,我们将内存中字节的编号称为地址(Address)或指针(Pointer)。
内存单元的地址称为指针。专门用来存放地址的变量,称为指针变量。
地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。
现在由于大多数计算机是32位的,也就是说地址的字宽是32位的,因此,指针也是32位的。可以看到,由于计算机内存的地址都是统一的宽度,而以内存地址作为变量地址的指针也就都是32位宽度。
所有数据类型的指针在32位机上面都是32位的,即4个字节;在64位机上面都是64位的,即8个字节。
(1)、一切都是地址
C语言用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。
数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。
CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错,或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃,程序员没有挽救的机会。
CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式:
0X3000 = (0X1000) + (0X2000);
( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存。
变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址,那场景简直让人崩溃。
需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。
2、指针的定义和使用
2.1、指针变量的定义
内存单元的地址称为指针,内存的每个字节都有相应的地址。指针变量就是用来存放内存单元的地址。
指针变量和其他变量一样,在使用之前要先定义,一般形式为:
类型说明符 *指针变量名;
指针变量可以连续定义,例如:
int *a, *b, *c; //a、b、c 的类型都是 int*,而不是int,它们是完全不同的类型
注意每个变量前面都要带*。如果写成下面的形式,那么只有 a 是指针变量,b、c 都是类型为 int 的普通变量:
int *a, b, c;
数据的地址是它的起始地址,内存由低地址向高地址。
在32位机中,所有数据类型的指针都是32位(四字节),在64位机中,所有数据类型的指针都是64位(八字节)
2^32=4294967296种状态
1GB=1024MB=1048576KB=1073741824Byte
4GB=4096MB
指针变量的定义:
格式:<数据的存储类型> <数据的数据类型> <*> <指针变量名>
注意:存储类型和数据类型表示的是指针所指向的数据的存储类型和数据类型,而不是指针的存储类型和数据类型。
一个指针变量只能指向同类型的数据变量。
2.2、指针变量的赋值
指针变量在使用前不仅要定义说明,而且要赋予具体的值。未经赋值的指针变量不能随便使用,否则将造成程序运行错误。指针变量的值只能是变量的地址,不能是其他数据,否则将引起错误。
在C语言中,变量的地址是由编译系统分配的,用户不知道变量的具体位置。C语言提供了地址运算符”&”来表示变量的地址,其一般形式为:&变量名;
指针变量只能存放一个地址,而不能将一个整形数据赋给指针(NULL除外)。
指针变量的赋值:
例: int * p; int a=20; p=&a; p:表示指针变量,存放某个整形数据的地址。 *p:表示指针所指向的地址上存储的值。 &p:表示指针变量自身的地址。 注意:指针变量的值只能是地址常量,不能是普通的整数。
2.3、指针变量的引用
指针指向的内存区域中的数据称为指针的目标。如果它指向的区域是程序中的一个变量的内存空间,,则这个变量称为指针的目标变量。指针的木标变量简称为指针的目标。对指针目标的控制,需要用到下面两个运算符:
&-----------取地址运算符,即指针变量本身的地址
*-----------指针运算符(间接存取运算符),指针变量所指向的内存区域上的值
&和*两个运算符互为逆操作。
假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a和&*pa分别是什么意思呢?
*&a可以理解为*(&a),&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。
&*pa可以理解为&(*pa),*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。
3、指针变量的运算
指针运算是以指针变量所存的值(地址量)作为运算量而进行的运算。因此,指针运算的实质就是地址的运算。
指针运算的种类是有限的,它只能进行算术运算、关系运算和赋值运算。
3.1、指针的算术运算
指针的算术运算只有加减算术运算,算术运算和指针指向的数据的数据类型有关。不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。
指针的运算和指针所指向的对象的数据宽度有关。也就是说,对于指向整形变量,加1操作就相当于向后移动4个字节;对于指向字符型变量的指针,加1操作就相当于向后移动一个字节。
对于数组而言,指针的相加就相当于依次指向数组中的下一个元素。
不同数据类型的两个指针实行加减整数运算是无意义的。
px+n表示的实际内存单元的地址量是:(px)+ sizeof(px的类型)*n
px-n 表示的实际内存单元的地址量是:(px)- sizeof(px的类型)*n
|
运算符 |
计算形式 |
意义 |
|
+ |
p+n |
指针向地址大的方向移动n个数据 |
|
- |
p-n |
指针向地址小的方向移动n个数据 |
|
++ |
p++或++p |
指针向地址大的方向移动1个数据 |
|
-- |
p--或--p |
指针向地址小的方向移动1个数据 |
|
- |
p-q |
两个指针之间相隔数据元素的个数 |
*p++=*(p++) *++p=*(++p)
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?
*p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素,上面已经进行了详细讲解。
*++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
3.2、指针的关系运算
指针变量除了可以参与加减运算,还可以参与比较关系运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。
|
运算符 |
说明 |
意义 |
|
> |
大于 |
p>q |
|
< |
小于 |
P<q |
|
>= |
大于等于 |
p>=q |
|
<= |
小于等于 |
p<=q |
|
!= |
不等于 |
p!=q |
|
== |
等于 |
p==q |
关于指针的关系运算,需要注意以下几个问题:
A、 具有不同数据类型的指针之间的关系运算没有意义,指向不同数据区域的数据的两指针之间,关系运算也没有意义。
B、 指针与一般整形变量之间的关系运算没有意义。但可以和零进行等于或不等于的关系运算,判断指针是否为空。
4、数组指针
4.1、指针与一维数组
int (*p) [5] = NULL; //该指针指向5个连续的int型数据
数组是有一定顺序关系的若干变量的集合,占用连续的存储空间。集合中的每个变量也被称作数组的元素。
数组元素的地址是指数组元素在内存中的起始地址。可以由各个元素加上取地址符“&”构成。
&a[0]就表示数组中第一个元素的地址,&a[1]就表示数组中第二个元素的地址,依次类推。
这里要特别强调的是,数组名就代表了数组的起始地址。显然,以下这两个表达式的值是相等的:
int a[10]; a 等效于 &a[0] //一维数组名代表数组中第一个元素的地址,但是数组名不等价与指针
数组指针是指向数组起始地址的指针,其本质为指针。一维数组的数组名为一维数组的指针。
“数组名本身就是一个指针”这种表述并不准确,严格来说应该是“数组名被转换成了一个指针”。
数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *。
引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。
(1) 使用下标
也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。
(2)使用指针
也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)。
不管是数组名还是数组指针,都可以使用上面的两种方式来访问数组元素。不同的是,数组名是常量,它的值不能改变,而数组指针是变量(除非特别指明它是常量),它的值可以任意改变。也就是说,数组名只能指向数组的开头,而数组指针可以先指向数组开头,再指向其他元素。
指针变量和数组名都是地址量,但数组名只能指向数组头一个元素。
注意:数组名是地址常量,不可更改 指针是地址变量,可以更改
一维数组a的第i个元素,有下标法和指针法。假设指针变量p指向数组的首元素。则有四种数组元素的表示方法:a[i] = p[i] = *(p+i) = *(a+i)
第i个元素的地址:&a[i] = &p[i] = p+i = a+i
需要特别说明的一点是,指针变量和数组在访问数组中元素时,一定条件下其使用方法具有相同的形式,因为指针变量和数组名都是地址量。但指针变量和数组的指针在本质上不同,数组在内存中的位置在程序的运行过程中是无法动态改变的。因此,数组名是地址常量,指针是地址变量。数组名可以在运算中作为指针参与,但不允许被赋值。
指针和数组的常见等价操作:
|
指针操作 |
数组操作 |
说明 |
|
array |
&array[0] |
数组首地址 |
|
*array |
array[0] |
数组首个元素 |
|
array+i |
&array[i] |
数组第i个元素的地址 |
|
*(array+i) |
array[i] |
数组的第i个元素 |
|
*array+b |
array[0]+b |
数组首元素的值加b |
|
*(array+i)+b |
array[i]+b |
数组第i元素的值加b |
|
*array++ |
array[i++] |
先取得第i个元素的值,i再加1 |
|
*++array |
array[++i] |
先将i加1,再取得第个元素的值 |
|
*array-- |
array[i--] |
先取得第i个元素的值,i再减1 |
|
*--array |
array[--i] |
先将i减1,再取得第i个元素的值 |
4.2、指针与多维数组
(1)列指针遍历二维数组
多维数组就是具有两个或两个以上下标的数组。实际上,在C语言中并没有多维数组的概念,多维数组就是一维数组的组合。
在C语言中,二维数组的元素连续存储,按行优先存,存储了第一行的元素,存第二行的,依次类推。基于这个特点,可以用一级指针来访问二维数组。
(2)行指针遍历二维数组
从内存管理的角度,二维数组的元素和一维数组的元素的存储是类似的,都是连续存储,因此,可以用一级指针循环遍历二维数组中的所有元素。 换一个角度来理解二维数组,把二维数组看作又多个一维数组组成。
二维数组名代表了数组的二维数组的首个元素的地址,即第一行一维数组的地址。加1表示下一行一维数组的地址。
在实际使用中,通常使用到二维数组就足够了。更多维的处理会导致程序的可读性及维护难度等加大。因此,建议尽量不要使用二维以上的数组。
存储行地址的指针变量,叫做行指针变量。其说明一般形式如下:
<存储类型> <数据类型> (*<指针变量名>)[表达式];
存储类型指的是auto,register,static,extern。若省略,相当于auto。
数据类型可以是任一种基本数据类型或构造数据类型。
二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。以下面的二维数组 a 为例:
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
为了更好的理解指针和二维数组的关系,我们先来定义一个两个指针变量 :
int *p1 = a; int (*p2)[4] = a;
p1是一个指向int数据的指针,p2是一个指向int数据类型数组的的指针
[ ]的优先级高于*,( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针。
对于指针p1加1,它就指向下一个数据,p2加1它就指向下一个数组:
p1+1 == &a[0][1] p2+1 == &a[1]
*p1表示第0个数组第0个元素的值,*(p1+1)表示第0个数组第1个元素的值。*p2表示第0行的数据元素的地址,也表示第0行第0个元素的地址,*p2+1表示第0行第一个元素的首地址,*(*(p2+1)+1)表示第一行第一个元素的值。
*p1 == a[0][0] *(p1+1) == a[0][1] *p2 == &a[0][0] *(p2+1) == &a[1][0] *p2+1 == &a[0][1] *(p2+1)+1 == &a[1][1] *(*(p2+1)+1) == a[1][1]
根据上面的结论,可以很容易推出以下的等价关系:
a+i == p2+i a[i] == p2[i] == *(a+i) == *(p2+i) a[i][j] == p2[i][j] == *(a[i]+j) == *(p2[i]+j) == *(*(a+i)+j) == *(*(p2+i)+j)
4.3、字符串指针
除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
char str[] = “hello word”; char *p = “hello word”;
这两者的区别是在内存中的存储区域不一样,第一种字符数组存储在全局数据区或栈区,第二种字符串存储在常量区。
全局数据区和栈区的字符串有读取和写入的权限,而常量区的字符串只有读取权限,没有写入权限。
在使用字符指针指向字符数组时,有以下两点需要注意:
A、虽然数组名也表示数组的首地址,但由于数组名为指针常量,其值是不能改变的(不能进行自加、自减、赋值等操作)。如果把字符数组的首地址赋给一个字符指针变量,就可以移动这个指针来访问或修改数组中的字符。
B、在使用scanf时,其参数前面要加上取地址符&,表明是一个地址。如果指针变量的值已经是一个字符数组的首地址,那么可以直接把指针变量作为参数而不需要再加上取地址符&。
字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *。
在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
数组指针和字符串指针最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
5、指针数组
5.1、指针数组的定义及初始化
所谓指针数组是指由若干个具有相同存储类型和数据类型的指针变量构成的集合,指针变量数组的一般说明形式如下:
<存储类型> <数据类型> * <指针变量数组名> [<大小>]
举例:
int * arr[5]={NULL}; //有5个元素,每个元素是个int *
int **parr = arr;
arr 是一个指针数组,它包含了 5 个元素,每个元素都是一个指针。
parr 是指向数组 arr 的指针,确切地说是指向 arr 第 0 个元素的指针,它的定义形式应该理解为int *(*parr),括号中的*表示 parr 是一个指针,括号外面的int *表示 parr 指向的数据的类型。arr 第 0 个元素的类型为 int *,所以在定义 parr 时要加两个 *。
arr[i] 表示获取第 i 个元素的值,该元素是一个指针,还需要在前面增加一个 * 才能取得它指向的数据,也即 *arr[i] 的形式。
parr+i 表示第 i 个元素的地址,*(parr+i) 表示获取第 i 个元素的值(该元素是一个指针),**(parr+i) 表示获取第 i 个元素指向的数据。
5.2、理解指针数组名
对于指针数组的数组名,也代表数组的起始地址。由于数组的元素已经是指针了,数组名就是数组首元素的地址,因此数组名是指针的地址,是多级指针了。
6、函数指针参数、指针返回值和函数指针
(1)函数的指针参数
用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。
像数组、字符串、动态分配的内存等都是一系列数据的集合,没有办法通过一个参数全部传入函数内部,只能传递它们的指针,在函数内部通过指针来影响这些数据集合。
(2)函数返回值
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。
用指针作为函数返回值时需要注意的一点是,函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。
(3)函数指针
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针的定义方法如下:
int max(int a,int b);
int (*pmax)(int, int) = max;
7、多级指针
多级指针的定义及引用
我们把一个指向指针变量的指针称为多级指针。对于指向处理数据的指针称为一级指针,简称一级指针。而把指向一级指针变量的指针称为二级指针,简称二级指针。
二级指针变量的说明形式如下:<存储类型> <数据类型> **<指针名>
二级指针用来存放以及指针变量的地址。
Int *p; ------------ p是指向int类型数据的指针
Int ** p; -------- p是指向int * 类型数据的指针
8、空指针、野指针、void指针和const指针
8.1、空指针
这里所说的空指针,指的是指针变量存了零号地址,即指向编号为0x00000000的空间,在程序中可以为指针赋0,很显然,可以用“*p=0”。
在C语言中指针常量只有NULL一个,NULL的含义是:C语言标准中定义了一个NULL指针,其就代表了0在实际编程中,NULL指针的使用是非常普遍的,因为他可以用来表明一个指针目前并未指向任何对象。
建议对没有初始化的指针赋值为 NULL,例如:
int *p; //P没有赋值,是野指针,会随意指向一个地址的
int *p = NULL; //空指针,表明该指针当前没有指向任何对象,指针初始化尽量赋值或者NULL
其实,NULL 是在stdio.h中定义的一个宏,它的具体内容为:
#define NULL ((void*) 0)
NULL 是“零值、等于零”的意思,在C语言中表示空指针。从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。
注意区分大小写,null 没有任何特殊含义,只是一个普通的标识符。
很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作,或者给出提示信息。
(void*)0 表示把数值0强制转换成void * 类型,最外层的( )把宏定义的内容括起来,防止发生歧义,从整体上来看,NULL指向了地址为0的内存,而不是不指向任何数据。
空指针指向的内存不可读和写,使用前必须重新赋值。
空指针是不可以进行访问的,也就是说不能给0地址上面赋值,即int *p = NULL; *p = 100;就是错误的,因为0~255之间的内存编号是系统占用的,因此不可以访问。
8.2、野指针
所谓野指针就是指针变量指向非法的内存空间。
例如:int *p = (int *)0x1100; //指针变量p指向内存地址编号为0x1100的空间(0x1100代表数字,在数字前面加上(int*)就表示地址),0x1100是未程序为开辟的地址,所以不能访问
8.3、void 指针
void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。
也就是说,void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。
void型的指针变量是一种不确定数据类型的指针变量,它可以通过强制类型转换让该变量指向任何数据类型的变量或数组。
一般形式为:
void *<指针变量名称>;
对于void类型的指针变量,实际使用时,一般需要通过强制类型转换才能使void型指针变量得到具体变量或数组地址,才能使void型指针变量进行指针的运算。在没有强制类型转换之前,void类型指针变量不能进行任何指针的算术运算。
8.4、const指针
const修饰指针有三种情况:
(1)const修饰指针 -----常量指针
常量指针的特点是指针的指向可以修改,但是指针指向的地址上的值不可以改。
int a = 10; int b = 20; const int * p = &a; //常量指针 *p = 50;//错误 p= &b;//正确
(2)const修饰常量 -----指针常量
指针常量的特点是指针的指向不可以修改,但是指针指向的地址上的值可以改。
int a = 10; int b = 20; int * const p = &a; *p = 50;//正确 p= &b;//错误
(3)const既修饰指针,又修饰常量
指针常量的特点是指针的指向不可以修改,指针指向的地址上的值也不能改。
int a = 10; int b = 20; const int * const p = &a; *p = 50;//错误 p= &b;//错误
9、数组名和指针不等价
数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针。
int a[6] = {0, 1, 2, 3, 4, 5}; int *p = a;
数组是一系列数据的集合,没有开始和结束标志,p 仅仅是一个指向 int 类型的指针,编译器不知道它指向的是一个整数还是一堆整数,对 p 使用 sizeof 求得的是指针变量本身的长度。也就是说,编译器并没有把 p 和数组关联起来,p 仅仅是一个指针变量,不管它指向哪里,sizeof 求得的永远是它本身所占用的字节数。
站在编译器的角度讲,变量名、数组名都是一种符号,它们最终都要和数据绑定起来。变量名用来指代一份数据,数组名用来指代一组数据(数据集合),它们都是有类型的,以便推断出所指代的数据的长度。
对,数组也有类型,这是很多读者没有意识到的,大部分C语言书籍对这一点也含糊其辞!我们可以将 int、float、char 等理解为基本类型,将数组理解为由基本类型派生得到的稍微复杂一些的类型。sizeof 就是根据符号的类型来计算长度的。
对于数组 a,它的类型是int [6],表示这是一个拥有 6 个 int 数据的集合,1 个 int 的长度为 4,6 个 int 的长度为 4×6 = 24,sizeof 很容易求得。
对于指针变量 p,它的类型是int *,在 32 位环境下长度为 4,在 64 位环境下长度为 8。
归根结底,a 和 p 这两个符号的类型不同,指代的数据也不同,它们不是一码事,sizeof 是根据符号类型来求长度的,a 和 p 的类型不同,求得的长度自然也不一样。
对于二维数组,也是类似的道理,例如int a[3][3]={1, 2, 3, 4, 5, 6, 7, 8, 9};,它的类型是int [3][3],长度是 4×3×3 = 36,读者可以亲自测试。
站在哲学的高度看问题
编程语言的目的是为了将计算机指令(机器语言)抽象成人类能够理解的自然语言,让程序员能够更加容易地管理和操作各种计算机资源,这些计算机资源最终表现为编程语言中的各种符号和语法规则。
整数、小数、数组、指针等不同类型的数据都是对内存的抽象,它们的名字用来指代不同的内存块,程序员在编码过程中不需要直接面对内存,使用这些名字将更加方便。
编译器在编译过程中会创建一张专门的表格用来保存名字以及名字对应的数据类型、地址、作用域等信息,sizeof 是一个操作符,不是函数,使用 sizeof 时可以从这张表格中查询到符号的长度。
与普通变量名相比,数组名既有一般性也有特殊性:一般性表现在数组名也用来指代特定的内存块,也有类型和长度;特殊性表现在数组名有时候会转换为一个指针,而不是它所指代的数据本身的值。
9.1、数组名在什么时候转换为指针
数组名的本意是表示一组数据的集合,它和普通变量一样,都用来指代一块内存,但在使用过程中,数组名有时候会转换为指向数据集合的指针(地址),而不是表示数据集合本身,这在前面的例子中已经被多次证实。
数据集合包含了多份数据,直接使用一个集合没有明确的含义,将数组名转换为指向数组的指针后,可以很容易地访问其中的任何一份数据,使用时的语义更加明确。
C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组时)、sizeof 或 & 的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)。
数组和指针的关系颇像诗和词的关系,它们都是一种文学形式,有不少共同之处,但在实际的表现手法上又各有特色。
数组下标[ ]
C语言标准还规定,数组下标与指针的偏移量相同。通俗地理解,就是对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。
假设现在有一个数组 a 和指针变量 p,它们的定义形式为:
int a = {1, 2, 3, 4, 5}, *p, i = 2;
读者可以通过以下任何一种方式来访问 a[i]:
p = a; ------> p[i];或*(p + i);
p = a + i; -----> *p;
对数组的引用 a[i] 在编译时总是被编译器改写成*(a+i)的形式,C语言标准也要求编译器必须具备这种行为。
取下标操作符[ ]是建立在指针的基础上,它的作用是使一个指针和一个整数相加,产生出一个新的指针,然后从这个新指针(新地址)上取得数据;假设指针的类型为T *,所产生的结果的类型就是T。
取下标操作符的两个操作数是可以交换的,它并不在意操作数的先后顺序,就像在加法中 3+5 和 5+3 并没有什么不一样。以上面的数组 a 为例,如果希望访问第 3 个元素,那么可以写作a[3],也可以写作3[a],这两种形式都是正确的,只不过后面的形式从不曾使用,它除了可以把初学者搞晕之外,实在没有什么实际的意义。
a[3] 等价于 *(a + 3),3[a] 等价于 *(3 + a),仅仅是把加法的两个操作数调换了位置。
使用下标时,编译器会自动把下标的步长调整到数组元素的大小。数组 a 中每个元素都是 int 类型,长度为 4 个字节,那么a[i+1]和a[i]在内存中的距离是 4(而不是 1)。
9.2、关于数组和指针可交换性的总结
(1)用 a[i] 这样的形式对数组进行访问总是会被编译器改写成(或者说解释为)像 *(a+i) 这样的指针形式。
(2)指针始终是指针,它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。
(3)在特定的环境中,也就是数组作为函数形参,也只有这种情况,一个数组可以看做是一个指针。作为函数形参的数组始终会被编译器修改成指向数组第一个元素的指针。
(4)当希望向函数传递数组时,可以把函数参数定义为数组形式(可以指定长度也可以不指定长度),也可以定义为指针。不管哪种形式,在函数内部都要作为指针变量对待。再谈数组下标[ ]
10、如何识别指针
C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。对,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!
对于初学者,有几种运算符的优先级非常容易混淆,它们的优先级从高到低依次是:
定义中被括号( )括起来的那部分。
后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
前缀操作符:星号*表示“指向xxx的指针”。
int *p1[6]; //指针数组 int *(p2[6]); //指针数组,和上面的形式等价 int (*p3)[6]; //二维数组指针 int (*p4)(int, int); //函数指针 char * (* c[10]) (int **p); //c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。 int (*(*(*pfunc)(int *))[5]) (int *); //pfunc 是一个函数指针(蓝色部分),该函数的返回值是一个指针,它指向一个指针数组(红色部分),指针数组中的指针指向原型为int func(int *);的函数(橘黄色部分)
11、main函数的指针参数用法
int main(); int main(int argc, char *argv[]); //argc 表示传递的字符串的数目,argv 是一个指针数组,每个指针指向一个字符串(一份数据)
12、指针总结
指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。
程序在运行过程中需要的是数据和指令的地址,变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符:在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址;程序被编译和链接后,这些名字都会消失,取而代之的是它们对应的地址。
| 定 义 | 含 义 |
|---|---|
| int *p; | p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组 |
| int **p; | p 为二级指针,指向 int * 类型的数据 |
| int *p[n]; | p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]); |
| int (*p)[n]; | p 为二维数组指针 |
| int *p(int); | p 是一个函数,它的返回值类型为 int *,参数为int |
| int (*p)(int); | p 是一个函数指针,指向原型为 int func(int) 的函数 |
(1)指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。
(2)给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃。
(3)使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL。
(4)两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。
(5)数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。

浙公网安备 33010602011771号