之前有总结指针数组,但是现在看来总结的太简单了。好多重要的知识点都是一带而过的。本想在后面添加后来想想算了,还是再写一篇文章来详细介绍数组和指针这对冤家吧。
之前总结的,参看:C语言再学习 – 数组和指针
一开始觉得C语言再学习专栏都写了五十篇了,现在的C语言水平至少可以说熟练掌握吧。有点洋洋得意的感觉,但是总结这章的时候,我有点急躁了。忽然觉得自己还是什么都不明白,之前的对C语言的认知只是冰山一角。迫切想着赶紧把这章总结完,部分内容复制粘贴过度了,好多内容都没有吃透。自责! 这不应该是我现在的状态。静下心来、从新梳理,再论数组和指针!!
一、指针
int n = 5;
int *ptr = &n;
*ptr = 5;
所以,可以看出指针是一个其数值为地址的变量。
2、指针内存布局
int *p;
char *c;
double * d;
上述都为指针,在 32 位系统下,不管什么样的指针类型,其大小都为 4byte。与“ *”号前面的数据类型无关,
“ *”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。
这部分可,参看:C语言再学习 – 关键字sizeof与strlen
3、应避免未初始化和非法的指针
下面这个代码说明了一个极为常见的错误:
这部分可,参看:C语言再学习 – 段错误(核心已转储)(重点)
4、指针表达式
现在让我们观察各种不同的指针表达式,并看看当它们分别作为左值和右值时是如何进行求值的。
我们以下面例子为例展开讨论:
当它作为右值使用时,表达式的值为 ‘a’; 但当这个表达式作为左值使用时,它是这个内存的地址而不是该地址所包含的值。
2)表达式:&ch
作为右值,这个表达式的值是变量 ch 的地址。作为左值是非法的。一方面,优先级表格显示&操作符的结果是个右值,它不能作为左值使用。另一方面,当表达式&ch进行求值时,你不知道它的结果应该存储在计算机的什么地方,这个表达式并未标识任何机器内存的特定位置,所以它不是一个合法的左值。
3)表达式:cp
它作为右值为 cp的值,它作为左值就是cp所处的内存位置。
4)表达式:&cp
它作为右值为指针变量的地址,也就是字符的指针的指针。它作为左值是非法的。
5)表达式:*cp
它等同于ch,它作为右值表达式的值为’a’,但当这个表达式作为左值使用时,它是这个内存的地址而不是该地址所包含的值。
6)表达式:*cp + 1
这里有两个操作符, * 的优先级高于 +,所以首先执行间接访问操作。我们取得这个值的一份拷贝并把它与 1 相加,表达式的最终结果为字符 ‘b’。这个表达式的最终结果的存储位置并未清晰定义,所以它不是一个合法的左值。优先级表格证实 + 的结果不能作为左值。
7)表达式:*(cp + 1)
这个括号表达式先执行加法运算,就是把 1 和 cp 中所存储的地址相加。这样,这个表达式作为右值就是这个位置的值,而它的左值是这个位置本身。
但是,这个表达式所访问的是ch后面的那个内存位置,一般而言,我们无法得知,所以像这样的表达式是非法的。
我们增加了指针变量cp的值,表达式的结果是增值后的指针的一份拷贝。因为前缀++先增加它的操作数的值再返回这个结果。这份拷贝的存储位置并未清晰定义,所以它不是一个合法的左值。
9)表达式:cp++
后缀++操作符同样增加cp的值,但它先返回cp值的一份拷贝然后增加cp的值。这样,这个表达式的值就是cp原来的值的一份拷贝。当然这个表达式也不是一个合法的左值。
10)表达式:*++cp
这里,间接访问操作符作用域增值后的指针的拷贝上,所以它的右值时ch后面那个内存的地址的值,而它的左值就是那个位置本身。
使用后缀++操作符所产生的结果不同:它的右值和左值分别是变量ch的值和ch的内存位置,也就是cp原先所指。
在这个表达式中,由于这两个操作符的结合性都是从右向左,且两个操作符都不做左值。所以首先执行的是间接访问操作。然后,cp所指向的位置的值增加1,表达式的结果是这个增值后的值的一份拷贝,它的左值不合法。
使用后缀++操作符,我们必须加上括号,使它首先执行间接访问操作。这个表达式的计算过程与前一个表达式相似,但它的值是ch增值前的原先值。它的左值为非法。
这个表达式共有3个操作符,记住这些操作符的结核性都是从右向左,所以首先执行的是++cp。接下来对这个拷贝值进行间接访问。最后在这个位置执行++操作,也就是增加它的值。
这个表示和前面一个表达式的区别在于这次第一个++操作符是后缀形式而不是前缀形式。由于它的优先级较高,所以先执行它。间接访问操作所访问的是cp所指向的位置而不是cp所指向的位置后面的那个位置。
二、数组
1、首先要搞清楚什么是数组?
我们把变量a称为数组,因为它是一些值的集合。下标和数组名一起使用,用于标识集合中某个特定的值。例如,a[0]表示数组a 的第一个值,a[4]表示第5个值。而在C中,在几乎所有使用数组名的表达式中,数组名的值是一个指针常量,也就是数组第 1 元素的地址。它的类型取决于数组元素的类型:如果它们是int类型,那么数组名的类型就是“指向int的常量指针”;如果它们是其他类型,那么数组名的类型就是“指向其他类型的常量指针”。
注意,这个值是指针常量,而不是指针变量。也就是说,数组名可以做右值,不可以做左值。
2、下标引用
1) ap 等价于 arr + 2。另外&arr[2]也是与它等价的表达式
3) ap[0] 等价于 *(ap + (0)),除去 0 和括号,其结果与 *ap 相同,因此等价于 arr[2]。
也可以写成 *(arr + 2)。记住,C 的下标引用和间接访问表达式是一样的。
4) ap +2,如果 ap 指向 arr[2],这个加法运算产生的指针所指向的元素是 arr[2] 向后移动 2个整数位置的元素。与它对应的表达式是 arr + 4 或者 &arr[4]。
但是注意:表达式ap++可以通过编译,但是arr++却不行,因为arr的值是一个常量。
5) *ap + 2 小心,这里有两个操作符。这个表达式相当于表达式 arr[2] + 2
6) *(ap + 2)括号迫使加法运算首先执行,所以我们这次得到的值是arr[4]。
7) ap[2] 把这个下标表达式转为与其对应的间接访问表达式 *(ap + 2),发现和上面的答案相同,得到的值是 arr[4]
9)ap[-1], 负值的下标,下标引用就是间接访问表达式,它对应的间接表达式为 *((arr + 2) - 1),也就是arr[1]。
三、数组和指针并不相同
首先要明白,数组和指针之间没有任何关系!
指针就是指针,指针变量在 32 位系统下,永远占 4 个 byte,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。
数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存任何类型的数据,但不能存函数。
区别在于下面几点:
1、左值和右值
简单说明什么是左值什么是右值:
X = Y;
===========
在这个上下文环境里,符号 X的含义是X所代表的地址,这被称为左值。
左值在编译时可知,左值表示存储结果的地方。
在这个上下文环境里,符号Y的含义是Y所代表的地址的内容,这被称为右值。
右值直到运行时才知。如无特别说明,右值表示“Y”的内容。
参看:C语言再学习 – 运算符与表达式
C语言引入了“可修改的左值”这个术语。它表示左值允许出现在赋值语句的左边。这个奇怪的术语是为与数组名区分,数组名也用于确定对象在内存中的位置,也是左值,但它不能作为赋值的对象。因此,数组名是个左值,但不是可修改的左值。标准规定赋值符必须用可修改的左值作为它左边一侧的操作数。用通俗的话讲,只能给可以修改的东西赋值。
根据上面讲解指针和数组部分可以看出,指针是可以当做左值和右值的;而数组被看做是一个指针常量,只能做右值,不能做左值。
//指针
//数组
#include 2、指针和数组的定义和声明
1)定义为数组,声明为指针
参看:C语言再学习 – 存储类型关键字
#include在一个源文件里定义了一个数组:char a[100]; 在另外一个文件里用下列语句进行了声明:extern char *a;
=======================================
这样是不可以的,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型 T 的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]。
但是,extern char a[]与 extern char a[100]等价。 因为这只是声明,不分配空间,所以编译器无需知道这个数组有多少个元素。这两个声明都告诉编译器 a 是在别的文件中被定义的一个数组, a 同时代表着数组 a 的首元素的首地址,也就是这块内存的其实地址。数组内任何元素的地址都只需要知道这个地址就可以计算出来。但是,当你声明 extern char *a 时,编译器理所当然的认为 a 是一个指针变量,在 32 位系统下,占 4 个字节。这4个字节里保存了一个地址,这个地址上存的是字符类型数据。虽然在文件 1 中,编译器知道 a 是一个数组,但是在文件 2 中,编译器并不知道这点,大多数编译器是按文件分别编译的,编译器只按照本文件中声明的类型来处理。所以,虽然 a 实际大小为 100 个字节,但是在文件 2 中,编译器认为 a 只占 4 个字节。
2)定义为指针,声明为数组
在一个源文件里定义了一个指针:char *p = "abcdefg"; 在另外一个文件里用下列语句进行了声明:extern char p[];
========================================
在文件 1 中, 编译器分配 4 个 byte 空间, 并命名为 p。 同时 p 里保存了字符串常量 “ abcdefg”的首字符的首地址。这个字符串常量本身保存在内存的静态区,其内容不可更改。在文件 2中,编译器认为 p 是一个数组,其大小为 4 个 byte,数组内保存的是 char 类型的数据。3、分配内存
参看:C语言再学习 -- 关键字sizeof与strlen
1)存储大小 在 32 位系统下,不管什么样的指针类型,其大小都为 4byte。与“ *”号前面的数据类型无关,“ *”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。#include
int main (void) { short *p = NULL; int i = sizeof (p); int j = sizeof (*p); printf ("i = %d, j = %d\n", i, j); return 0; } 输出结果: i = 4, j = 2 关于数组,看下面几个例子:
int a[100]; sizeof (a) 的值是多少? sizeof(a[100])呢? //请尤其注意本例。 sizeof(&a)呢? sizeof(&a[0])呢?#include
int main (void) { int a[100]; printf ("sizeof (a) = %d\n", sizeof (a)); printf ("sizeof (a[100] = %d\n", sizeof (a[100])); printf ("sizoef (&a) = %d\n", sizeof (&a)); printf ("sizeof (&a[0] = %d\n)", sizeof (&a[0])); return 0; } 输出结果: sizeof (a) = 400 sizeof (a[100] = 4 sizoef (&a) = 4 sizeof (&a[0] = 4 sizeof (a) = 400; 因为 a是类型为整型、有100个元素的数组,所占内存为400个字节
sizeof (a[100]) = 4; 因为 a[100] 为数组的第100元素的值该值为 int 类型,所占内存为4个字节。
sizeof (&a) = 4; 因为 &a 为数组的地址即指针,32位系统 指针所占字节为 4个字节
sizeof (&a[0]) = 4; 因为&a[0] 为数组的首元素的地址即指针,32位系统 指针所占字节为 4个字节
2)分配内存
数组和指针都可以在它们的定义中用字符串常量进行初始化。尽管看上去一样,底层的机制却不相同。
定义指针时,编译器并不为指针所指向的对象分配空间,它只是分配指针本身的空间,除非在定义时同时赋值给指针一个字符串常量进行初始化。
例如,下面的定义时创建一个字符串常量(为其分配了内存):
注意,只有对字符串常量才是如此。不能指望为浮点数之类的常量分配空间:char *p = "abcdefg";
再有,非关联化一个空指针总是导致段错误,但野指针和悬空指针指向的内存,可能会或可能不会存在,而且可能或不可能是可读的还是可写的,因此会导致瞬态错误。float *p = 3.14; /*错误,无法通过编译*/
现在,非关联化这些变量可能导致段错误:非关联化空指针通常会导致段错误,阅读时从野指针可能导致随机数据但没有段错误,和阅读从悬空指针可能导致有效数据,然后随机数据覆盖。#include
int main (void) { int *ptr = NULL; *ptr = 0; return 0; } 输出结果: 段错误(核心已转储) 另外,在ANSI C中,初始化指针时所创建的字符串常量被定义为只读。如果试图通过指针修改这个字符串的值,程序就会出现未定义的行为。在有些编译器中,字符串常量被存放在只允许读取的文本段中,以防止它被修改。
- #include
- #include
- int main (void)
- {
- char *ptr = "test";
- strcpy (ptr, "TEST");
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
- #include
- int main (void)
- {
- char *ptr = "hello";
- *ptr = 'H';
- return 0;
- }
- 输出结果:
- 段错误(核心已转储)
可以纠正这个代码使用一个数组而不是一个字符指针,这个栈上分配内存并初始化字符串的值:
- #include
- int main (void)
- {
- char ptr[] = "hello";
- ptr[0] = 'H';
- return 0;
- }
而定义数组时,编译器会为数组所指向的对象分配空间。
指针字符串和数组字符串区别
复制和比较时,都需要使用字符函数,不同的是指针字符串需要动态分配内存。
数组:
#include
#include
#include 4、以指针的形式访问和以下标的形式访问
1)以指针的形式访问和以下标的形式访问
下面我们就详细讨论它们之间似是而非的一些特点。例如,函数内部有如下定义:
以指针的形式访问和以下标的形式访问 指针
例子 A) 定义了一个指针变量 p,p本身在栈上占 4 个字节,p 里存储的是一块内存的首地址。这块内存在静态区,其空间大小为 7 个字节,这块内存也没有名字。对这块内存的访问完全是匿名的访问。比如现在需要读取字符 ‘e’,我们有两种方式:
1)以指针的形式:*(p + 4)。先取出 p 里存储的地址值,假设为 0x0000FF00,然后加上 4 个字符的偏移量,得到新的地址 0x0000FF04。然后取出 0x0000FF04 地址上的值。
2)以下标的形式:p[4]。编译器总是把以下标的形式的操作解析为以指针的形式操作。p[4]这个操作会被解析成:先取出 p 里存储的地址值,然后加上中括号中 4 个元素的偏移量,计算出新的地址,然后从新的地址中取出值。也就是说以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了。
以指针的形式访问和以下标的形式访问 数组
例子 B) 定义了一个数组 a, a 拥有7 个char类型的元素,其空间大小为 7。数组 a 本身在栈上面。对 a 的元素的访问必须先根据数组的名字 a 找到数组首元素的首地址,然后根据偏移量找到相应的值。这是一种典型的“具名 + 匿名”访问。比如现在需要读取字符‘5’,我们有两种方式:
1)以指针的形式:*(a + 4)。这时候代表的是数组首元素的首地址,假设为 0x0000FF00,然后加上 4
个字符的偏移量,得到新的地址 0x0000FF04。然后取出0x0000FF04地址上的值。
2)以下标的形式:a[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操作。a[4]这个操作会被解析成:a 作为数组首元素的首地址,然后加上中括号中 4 个元素的偏移量,计算出新的地址,然后从新的地址中取出值。
由上面的分析,我们可以看到,指针和数组根本就是两个完全不一样的东西。只是它们都可以“以指针形式”或“以下标形式”进行访问。一个完全的匿名访问,一个典型的具名 + 匿名访问。一定要注意的是这个“以XXX的形式的访问”这种表达式方式。
另外一个需要强调的是:上面所说的偏移量 4 代表的是 4 个元素,而不是 4 个字节。只不过这里刚好是 char 类型数据 1 个字符 的大小就为 1 个字节。记住这个偏移量的单位是元素的个数而不是字节数,在计算新地址时千万别弄错了。
注意,*a + 4 和 *(a + 4) 的不同
既然,可以用下标也可以用指针,那么两者效率相同吗? 分两种情况:
1、在下面的情况下,指针比下标的效率要高
5、其他区别
指针 |
数组 |
保存数据的地址,任何存入指针变量 p 的数据都会被当作地址来处理。 p 本身的地址由编译器另外存储,存储在哪里,我们并不知道。 |
保存数据,数组名 a 代表的是数组首元素的首地址而不是数组的首地址。 &a 才是整个数组的首地址。 a 本身的地址由编译器另外存储,存储在哪,我们并不知道。 |
间接访问数据,首先取得指针变量 p 的内容,把它作为地址,然后从这个地址提取数据或向这个地址写入数据。指针可以以指针的形式访问*(p+i);也可以以下标的形式访问 p[i]。但其本质都是先取 p 的内容然后加上i*sizeof(类型)个 byte 作为数据的真正地址。 |
直接访问数据,数组名 a 是整个数组的名字,数组内每个元素并没有名字。只能通过“具名+匿名”的方式来访问其某个元素,不能把数组当一个整体来进行读写操作。数组可以以指针的形式访问*(a+i); 也可以以下标的形式访问 a[i]。但其本质都是 a 所代表的数组首元素的首地址加上 i*sizeof(类型)个 byte 作为数据的真正地址。 |
通常用于动态数据结构 |
通常用于存储固定数目且数据类型相同的元素。 |
相关的函数为 malloc 和 free。 |
隐式分配和删除 |
通常指向匿名数据(当然也可指向具名数据) |
自身即为数组名 |
四、什么时候数组和指针是相同的
C语言标准对此作了如下说明:
规则1:表达式中的数组名(与声明不同)被编译器当做一个指向该数组第一元素的指针。
理解为,对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。
#include其实就是,C 的 下标引用 和 间接访问表达式 是一样的。例如,arr[2]也可以写成 *(arr + 2)。
参看上面讲解数组下表引用部分:#include
规则2:下标总是与指针的偏移量相同。
上面有强调,所说的偏移量 4 代表的是 4 个元素,而不是 4 个字节。只不过这里刚好是 char 类型数据 1 个字符 的大小就为 1 个字节。记住这个偏移量的单位是元素的个数而不是字节数,在计算新地址时千万别弄错了。
还有,指针比下标的效率要高。
规则3:在函数参数的声明中,数组名被编译器当做指向该数组第一个元素的指针。
首先,我们介绍一下形参和实参:
形参:它是一个变量,在函数定义或函数声明的原型中定义。又称“形式参数”。例如:
实参:在实际调用一个函数时所传递给函数的值。又称“实际参数”。例如:int p (int b int n); //b 和 n 都是形参
i = p (10, j); //10 和 j 都是实参。在同一个函数的多次调用时,实参可以不同。
标准规定作为“类型的数组”的形参的声明应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改为指向数组第一个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。现在让我们重点观察一下数组,隐性转换意味着三种形式是完全等同的。因此,在my_function的调用上,无论实参是数组还是真的指针都是合法的。my_function (int *t) {....} my_function (int t[]) {....} my_function (int t[200]) {....}
数组/指针实参的一般用法:
调用时的实参 |
类型 |
通常目的 |
Fun (&my_int); |
一个整型数的地址 |
一个 int 参数的传址调用 |
Fun (my_int_ptr); |
指向整型数的指针 |
传递一个指针 |
Fun (my_int_array); |
整型数组 |
传递一个数组 |
Fun (&my_int_array[i]); |
一个整型数组某个元素的地址 |
传递数组的一部分 |
1、一维数组参数
- //示例一
- #include
- int sum (int arr[], int size)
- {
- int num = 0, ret = 0;
- for(num = 0; num <= size - 1; num++)
- {
- ret += arr[num];
- }
- return ret; //注意返回值
- }
- int main (void)
- {
- int arr[7] = {1,2,3,4,5,6,7};
- int res = sum(arr,7);
- printf ("求和结果为 %d\n", res);
- return 0;
- }
- 输出结果:
- 求和结果为 28
示例一说明:处理数组时,函数必须知道数组的地址和元素的个数。数组地址直接传递给函数,而数组元素的个数信息需要内建于函数内部或被作为独立的参数传递给函数。 示例二说明:C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。
- //示例二
- #include
- void fun (int b[100]) //指针做形参
- {
- printf ("sizeof (b) = %d\n", sizeof (b));
- }
- int main (void)
- {
- int a[10];
- fun (a);
- return 0;
- }
- 输出结果:
- sizeof (b) = 4
参看: C语言再学习 -- 值传递,址传递,引用传递
问题:为什么C语言把数组形参当做指针?
#include把作为形参的数组和指针等同起来是出于效率原因考虑的。在 C 语言中,所有非数组形式的数据实参均以传值形式(对实参做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在空间上还是在时间上,其开销都是非常大的。更重要的是,在绝大部分情况下,你其实并不需要整个数组的拷贝,你只想告诉函数在那一刻对哪个特定的数组感兴趣。要达到这个目的,可以考虑的方法是在形参上增加一个存储说明符,表示它是传值调用还是传址调用。如果采用“所有的数组在作为参数传递时都转换为指向数组起始地址的指针,而其他的参数均采用传值调用”的约定,就可以简化编译器。类似的,函数的返回值绝不能是一个函数数组,而只能是指向数组或函数的指针。这里要明确的一个概念就是:函数本身是没有类型的,只有函数的返回值才有类型。玩转数组/指针实参:
//示例1
//示例2
#include #include注意、字符串函数第一个参数一般为字符数组
参看:C语言再学习 -- 字符串和字符串函数
/* 字符串函数演示 */
2、一级指针参数
在这个程序中用指针变量作参数,虽然 传送的是变量的地址,但实参和形参之间的数据传递依然是单向的“值传递”,即调用函数不可能改变实参指针变量的值。但它不同于一般值传递的是,它可以通过指针间接访问的特点来改变指针变量所指变量的值,即最终达到了改变实参的目的。
- #include
- void swap (int *px, int *py)
- {
- int temp=*px;
- *px=*py;
- *py=temp;
- printf("*px = %d, *py = %d\n", *px, *py);
- }
- int main(void)
- {
- int a=4;
- int b=6;
- swap (&a,&b);
- printf("a = %d,b = %d\n", a, b);
- return 0;
- }
- 输出结果:
- *px = 6, *py = 4
- a = 6,b = 4
无法把指针变量本身传递给一个函数
参看: C语言再学习 -- 值传递,址传递,引用传递
请问运行 Test 函数会有什么样的结果?
- void GetMemory( char *p )
- {
- p = (char *) malloc( 100 );
- }
- void Test( void )
- {
- char *str = NULL;
- GetMemory( str );
- strcpy( str, "hello world" );
- printf( str );
- }
答:程序崩溃。
因为 GetMemory 并不能传递动态内存,Test 函数中的 str 一直都是 NULL。strcpy(str, "hello world");将使程序崩溃。
在函数内部修改形参并不能真正的改变传入形参的值,执行完
char *str = NULL;
GetMemory( str );
后的str仍然为NULL;第一种方法:使用二级指针作为函数的形式参数,可以让被调用函数使用其他函数的指针类型存储区第二种方法:使用返回值
- #include
- #include
- #include
- void fa(char** p) //主要还是指针的问题
- {
- *p=(char* )malloc(100);
- if(*p)
- {
- return;
- }
- }
- int main()
- {
- char* str=NULL;//这块没问题的
- fa(&str);
- strcpy(str,"hello");
- printf("%s\n",str);
- free(str);
- str=NULL;
- return 0;
- }
- #include
- #include
- #include
- char* fa(char* p) //主要还是指针的问题
- {
- p=(char* )malloc(100);
- return p;
- }
- int main()
- {
- char* str=NULL;//这块没问题的
- str = fa(str);
- strcpy(str,"hello");
- printf("%s\n",str);
- free(str);
- str=NULL;
- return 0;
- }
- 输出结果:
- hello
3、二维数组参数与二维指针参数
我们把二维数组参数和二维指针参数的等效关系整理一下:
数组参数 |
等效的指针参数 |
数组的数组: char a[3][4] |
数组的指针: char (*p)[10] |
指针数组: char *a[5] |
指针的指针: char **p |
这里需要注意的是: C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。这条规则并不是递归的,也就是说只有一维数组才是如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维再也不可改写。比如: a[3][4][5]作为参数时可以被改写为( *p) [4][5]。
一个常见的例子就是函数main的第二个参数:
这种写法与下面的写法完全等价:main (int argc, char *argv[]) { .... }
需要注意的是,前一种写法强调的重点在于 argv 是一个指向某数组的起始元素的指针,该数组的元素为字符指针类型。因为这两种写法等价的,所以读者可以任选一种最能清楚反映自己意图的写法。main (int argc, char **argv) { .... }
数组和指针可交换行的总结:
1、用 a[i] 这样的形式对数组进行访问总是被编译器“改写”或解释为想 *(a + 1) 这样的指针访问。
2、指针始终都是指针。它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。
3、在特定的上下文中,也就是它作为函数的参数(也只有这种情况),一个数组的声明可以看作是一个指针。作为函数参数的数组(就是在一个函数调用中)始终会被编译器修改成为指向数组第一个元素的指针。
4、因此,当把一个数组定义为函数的参数时,可以选择把它定义为数组,也可以定义指针。不管选择哪种方法,在函数内部事实上获得的都是一个指针。
5、在其他所有情况中,定义和声明必须匹配。如果定义了一个数组,在其他文件对它进行声明时也必须把它声明为数组,指针也是如此。
五、再论数组和指针
1、指针数组和数组指针
#define M 2 #define N 3 int main() { int a[M][N]={1,2,3,4,5,6}; int *start=&a[0][0]; int * const end=start+M*N; for(;start!=end;start++) printf("%-5d",*start); putchar('\n'); return 0; } 输出结果: 1 2 3 4 5 6指针数组:首先它是一个数组,数组的元素都是指针,例如:int *ptr1[10];
数组指针:首先它是一个指针,它指向一个数组,例如:int (*ptr2)[10];
这里需要明白一个符号之间优先级的问题,"[ ]"的优先级比"*"要高。p1 先与“ []”结合,构成一个数组的定义,数组名为 p1, int *修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含 10 个指向 int 类型数据的指针,即指针数组。
至于 p2 就更好理解了,在这里"( )"的优先级比"[ ]"高,"*"号和 p2 构成一个指针的定义,指针变量名为 p2, int 修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚 p2 是一个指 针,它指向一个包含 10 个 int 类型数据的数组,即数组指针。
参看:数组指针和指针数组数组指针详解:
(1)数组在内存中的表示 创建一个数组就是在内存里面开辟一块连续的空间,比如int a[4];就是在内存里面开辟了一个大小为4*sizeof(int)字节的内存空间。二维数组是特殊的一维数组。 先来看一段代码:
- void main()
- {
- int a[2][2]={1,2,3,4};//这是一个2*2的二维数组
- int (*p)[2];//数组指针
- p=a;//令p指向数组a
- }
注意到代码中这句话:int (*p)[2];这里的p是一个数组指针变量。(2)理解数组名和数组指针变量a是一个数组名,类型是指向一维数组的指针,不是变量,a的值是指针常量,即不能有a++或者a=p这些操作。a指向这块连续空间的首地址,值是&a[0][0]。 a[0]是一维数组名,类型是指向整型的指针,值是&a[0][0],这个值是一个常量。 a[1]是一维数组名,类型是指向整型的指针,值是&a[1][0],这个值是一个常量。 p是一个数组指针变量,指向一维数组的指针变量,值是&a[0][0]。可以执行p++;p=a等操作。 a+1是取出第一行的首地址。 *(a+1)表示指向下一行元素,也可以理解为指向下一个一维数组。 a[0]+1是指向第0行第1个元素,也可以理解为指向一维数组a[0]的第一个元素。 p+1同a+1 *(p+1)同*(a+1) 虽然a跟a[0]值是一样,但类型不一样,表示的意义不一样。通过分析就不难理解为什么*(*(a+i)+j)和a[i][j]等效了。
- #include
- int main()
- {
- int a[2][2] = {1, 2, 3, 4};
- int (*p)[2];
- p = a;
- printf ("a = %p, p = %p, &a[0][0] = %p\n", a, p, &a[0][0]);
- return 0;
- }
- 输出结果:
- a = 0xbf90112c, p = 0xbf90112c, &a[0][0] = 0xbf90112c
(3)指针是数组的迭代器#include
理解这段代码,用指针遍历一个二维数组,是不是很像C++标准库里面vector的迭代器。注意这里只用了一个for循环,这也可以说明二维数组其实就是特殊的一维数组。
(4)数组名与数组指针变量的区别 从(2)中的分析中得出数组名是指针,类型是指向元素类型的指针,但值是指针常量,声明数组时编译器会为声明所指定的元素数量保留内存空间。数组指针是指向数组的指针,声明指针变量时编译器只为指针本身保留内存空间。 看看这个代码:
- #include
- void main()
- {
- int a[2][2]={1,2,3,4};//这是一个2*2的二维数组
- int (*p)[2];//数组指针
- p=a;//令p指向数组a
- printf("%d\n%d\n",sizeof a,sizeof p);
- }
- 输出结果:
- 16
- 4
当sizeof用于变量时返回这个变量占用的实际空间的大小。当sizeof用于数组名时,返回整个数组的大小(这里的大小指占用的字节数)。p是一个指针变量,这个变量占用四个字节。而a是数组名,所以sizeof a返回数组a中的全部元素占用的字节数。
#include从结果中看出,a在做+运算时是转化成了指针变量,此时a+i的类型是一个指针变量,而不是一个数组名。但a[i]是一个一维数组的数组名,sizeof(a[0])的值是8。
- #include
- void main()
- {
- int a[2][2]={1,2,3,4};//这是一个2*2的二维数组
- int (*p)[2];//数组指针
- p=a;//令p指向数组a
- printf("%d\n%d\n",sizeof(a+1),sizeof(p+1));
- printf("%d\n%d\n",sizeof(a+0),sizeof(p+0));
- }
- 输出结果:
- 4
- 4
- 4
- 4
现在再来看一段代码:是不是又有点困惑呢? 解释:这是因为传参的时候数组名转化成指针变量,注意到函数f中f(int a[][2])这里并不需要指定二维数组的长度,此处可以改为int (*a)[2]。所以传过来的就是一个数组指针变量。这样明白了吧!
- #include
- void f(int a[][2])
- {
- printf("%d\n",sizeof a);
- }
- void main()
- {
- int a[2][2]={1,2,3,4};//这是一个2*2的二维数组
- printf("%d\n",sizeof a);
- f(a);
- }
- 输出结果:
- 16
- 4
总结:数组名的类型是指向元素类型的指针,值是指针常量。(a+1)的类型是一个指针变量。把数组名作为参数传递的时候实际上传递的是一个指针变量。sizeof对变量和数组名操作时返回的结果会不一样。数组指针是指向数组的指针,其值可以是变量。指针数组详解:
(1)认识指针数组 一个存放int类型的数组称为整型数组,那么存放指针的数组就叫指针数组。
- #include
- void main()
- {
- int i=1,j=2;
- //p先跟[]结合,然后再跟*结合
- int *p[2];//指针数组,存放指针的数组
- p[0]=&i;
- p[1]=&j;
- printf("%d",sizeof(p));
- }
此例数组p就两个元素,p[0]是指向i的指针,p[1]是指向j的指针。这两个指针都是int型指针,所以p是存放int型指针的数组。sizeof(p)返回数组占用的总空间,所以程序输出是8。2、多维数组与多级指针
3、函数指针1)二维数组
上面这个例子很简单,结果为 0。 而且说明了二维数组,元素可以嵌套花括号,也可以将花括号去掉。#include
int main (void) { int a[3][2] = {{0, 1}, {2, 3}, {4, 5}}; // int a[3][2] = {0, 1, 2, 3, 4, 5}; int *p; p = a[0]; printf ("%d\n", p[0]); return 0; } 输出结果: 0 再看下面的例子:
为什么结果为 1? 仔细看花括号里嵌套的是小括号。这里是花括号里面嵌套了逗号表达式!其实这个赋值就相当于 int a [3][2]={ 1, 3, 5};#include
int main (void) { int a[3][2] = {(0, 1), (2, 3), (4, 5)}; int *p; p = a[0]; printf ("%d\n", p[0]); return 0; } 输出结果: 1
所以,在初始化二维数组的时候一定要注意,别不小心把应该用的花括号写成小括号了。这里了解一下逗号的用法:
#include
int main (void) { int a = (1, 3); printf ("%d\n", a); return 0; } 输出结果: 3 考虑存储大小:#include
int main (void) { int a[3][2] = {0, 1, 2, 3, 4, 5}; int *p; p = a[0]; printf ("%d\n", p[0]); printf ("sizeof (a) = %d, sizeof (a[1]) = %d, sizeof (a[2][1]) = %d\n", sizeof (a), sizeof (a[1]), sizeof (a[2][1])); return 0; } 输出结果: 0 sizeof (a) = 24, sizeof (a[1]) = 8, sizeof (a[2][1]) = 4 2)二级指针
#include
int main (void) { char *p = "abcdef"; char **p1 = &p; return 0; } 1)函数指针的定义
顾名思义,函数指针就是函数的指针,它是一个指针,指向一个函数。看例子:
A), char * (*fun1)(char * p1,char * p2); B), char * *fun2(char * p1,char * p2); C), char * fun3(char * p1,char * p2);
看上面三个表达式分别是什么意思?
C): 这很容易, fun3 是函数名, p1, p2 是参数, 其类型为 char *型, 函数的返回值为 char *类型。 B):也很简单,与 C)表达式相比,唯一不同的就是函数的返回值类型为 char**,是个二级指针。 A): fun1 是函数名吗?回忆一下前面讲解数组指针时的情形。我们说数组指针这么定义或许更清晰: int (*)[10] p;
再看看 A)表达式与这里何其相似!明白了吧。这里 fun1 不是什么函数名,而是一个指针变量,它指向一个函数。这个函数有两个指针类型的参数,函数的返回值也是一个指针。同样,我们把这个表达式改写一下: char * (*)(char * p1,char * p2) fun1; 这样子是不是好看一些呢?只可惜编译器不这么想。
2)函数指针的使用
参看:C语言再学习 -- 字符串和字符串函数
#include
void (*p)();#include
void Function() { printf("Call Function!\n"); } int main(void) { void (*p)(); *(int*)&p=(int)Function; (*p) (); return 0; } 输出结果: Call Function!
这行代码定义了一个指针变量 p, p 指向一个函数,这个函数的参数和返回值都是 void。
&p 是求指针变量 p 本身的地址,这是一个 32 位的二进制常数( 32 位系统)。
(int*)&p 表示将地址强制转换成指向 int 类型数据的指针。
(int)Function 表示将函数的入口地址强制转换成 int 类型的数据。
*(int*)&p=(int)Function;表示将函数的入口地址赋值给指针变量 p。
那么(*p) ();就是表示对函数的调用。
使用函数指针的好处在于,可以将实现同一功能的多个模块统一起来标识,这样一来更容易后期的维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦合度以及使接口与实现分开。
举例,函数指针数组:
#include#include
#include#include
4、const修饰指针、数组
const定义的变量具有只读性,const修饰的只读变量必须在定义的时候初始化。
1)修饰数组
定义或说明一个只读数组可采用如下格式: int const a[5]={1, 2, 3, 4, 5};或 const int a[5]={1, 2, 3, 4, 5};
const int numbers[] = {1, 2, 3, 4, 5}; numbers[1] = 10; // 错误,数组被const修饰,因此,数组内容不可修改
2)修饰指针
这里给出一个记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。“近水楼先得月”,离谁近就修饰谁。int arr[5];
constint*p = arr; //const 修饰*p,p 是指针,可变; *p 是指针指向的对象,不可变。intconst *p = arr; //const 修饰*p,p 是指针, 可变;*p 是指针指向的对象,不可变。int*const p = arr; //const 修饰 p, p 是指针,不可变; p 指向的对象可变。
constint*const p= arr; //前一个 const 修饰*p,后一个 const 修饰 p,指针 p 和 p 指向的对象都不可变。//示例一 int a = 10; int b = 20; const int *p = &a; //等同 int const *p = &a; p = &b; // 正确 *p = 20; // 错误,指针变量p所指向的地址中的内容不能通过指针变量修改 a = 20; // 正确,变量a并没有被const关键字修饰;
//示例二 int a = 10; int b = 20; int * const p = &a; p = &b; // 错误,指针p只能指向同一个地址; *p = 20; // 正确
//示例三 int a = 10; int b = 20; const int * const p = &a; p = &b; // 错误 *p = 20; // 错误
5、a 和 &a 的区别
#include#include
1)表达式:&arr + 1
取数组 arr 的首地址,该地址的值加上 sizeof (arr) 的值,即 &arr + 5 * sizeof (int),也就是下一个数组的首地址,显然当前指针已经超越了数组的界限。
2)表达式:(int*)(&arr + 1)
则是把上一步计算出来的地址,强制转换为 int * 类型,赋值给ptr
3)表达式:*(arr + 1)
arr,&arr 的值是一样的,但是意思不一样,arr是数组首元素的首地址,也就是 arr[0]的首地址。
&arr 是数组的首地址,arr + 1 是数组下一个元素的首地址,即 arr[1]的首地址。
&arr + 1 是下一个数组的首地址。
4)表达式:*(ptr -1)
因为 ptr 是指向 a[5],并且 ptr 是 int * 类型,所以 *(ptr - 1) 是指向 a[4]。
#include
int main (void)
{
char a[5] = {'A', 'B', 'C', 'D'};
char (*p3)[5] = &a;
// char (*p4)[5] = a; 警告: 从不兼容的指针类型初始化 [默认启用]
return 0;
}
毫无疑问, p3 和 p4 都是数组指针,指向的是整个数组。 &a 是整个数组的首地址, a是数组首元素的首地址,其值相同但意义不同。在 C 语言里,赋值符号“ =”号两边的数据类型必须是相同的,如果不同需要显示或隐式的类型转换。 p3 这个定义的“ =”号两边的数据类型完全一致,而 p4 这个定义的“ =”号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的数据类型是指向单个字符的指针。
扩展:地址的强制转换
ptr1:将&a+1 的值强制转换成 int*类型,赋值给 int* 类型的变量 ptr, ptr1 肯定指到数组 a 的下一个 int 类型数据了。 ptr1[-1]被解析成*(ptr1-1),即 ptr1 往后退 4 个 byte。所以其值为 0x4。#include
int main (void) { int a[4] = {1, 2, 3, 4}; int *ptr1 = (int*)(&a + 1); int *ptr2 = (int*)((int)a + 1); printf ("%x, %x\n", ptr1[-1], *ptr2); return 0; } 输出结果: 4, 2000000
ptr2:按照上面的讲解, (int)a+1 的值是元素 a[0]的第二个字节的地址。然后把这个地址强制转换成 int*类型的值赋给 ptr2, 也就是说*ptr2 的值应该为元素 a[0]的第二个字节开始的连续 4 个 byte 的内容。