浅析二级指针、二维数组及其他
C 语言这种比较底层且大量操作指针的语言,常常需要开发者对程序对应进程的内存状态保持关注,例如一个变量存储在内存哪个区,地址范围是多少,指针指向的是什么。尤其当涉及数组和指针时,稍不注意就会犯错。本文试图对相关的几个概念和用法做解释,没有看《C专家编程》之类的书,理解可能不深或有误,请读者指正。
为了便于解释和验证,我提前写了点程序,先全部贴出来:
<pre><code>
//// main.cpp// testcpp//// Created by moxie on 15/8/25.// Copyright (c) 2015年 moxie. All rights reserved.//#include <iostream>#include <stdio.h>using namespace std;// 二维数组传参void func1(char (*a)[2]){ // or char a[][2] cout << a[1][1] << endl; cout << sizeof(a) << endl;}// 指针数组传参void func2(char **c){ // or char *c[] cout << c[0][2] << endl; cout << c[1][1] << endl; cout << sizeof(c) << endl;}// 二级指针void func3(char p){ p = new char[2]; p = 'b';}void func4(char p){ p = new char[2]; p = 'b';}// 二维数组地址void func5(){ // reference: http://c.biancheng.net/cpp/html/79.html int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11}; printf("%d,",a); printf("%d,",a); printf("%d,",a[0]); printf("%d,",&a[0]); printf("%d\n",&a[0][0]); printf("%d,",a+1); printf("%d,",(a+1)); printf("%d,",a[1]); printf("%d,",&a[1]); printf("%d\n",&a[1][0]); printf("%d,",a+2); printf("%d,",(a+2)); printf("%d,",a[2]); printf("%d,",&a[2]); printf("%d\n",&a[2][0]); printf("%d,",a[1]+1); printf("%d\n",(a+1)+1); printf("%d,%d\n",(a[1]+1),((a+1)+1));}int main(int argc, const char * argv[]) { // insert code here... // 二维数组 cout << "二维数组" << endl; char a[2][2] = {'a', 'b', 'c', 'd'}; char b[][2] = {'b','b','b'}; func1(a); func1(b); cout << endl; // 指针数组 cout << "指针数组" << endl; char c1[] = {'a','b','c'}; char c2[] = {'d','e'}; char *c[2] = {c1, c2}; func2(c); cout << endl; // 二级指针 cout << "二级指针" <<endl; char *p = new char[2]; *p = 'a'; cout << *p << endl; func3(p); cout << *p << endl; func4(&p); cout << *p << endl; if(p){ delete p; p = NULL; } cout << endl; // 二维数组地址 cout << "二维数组地址" <<endl; func5(); cout << endl; // 初始化 cout << "初始化" << endl; int dd[2] = {2}; cout<<dd[0]<<endl; char e[4] = "ab"; cout << e <<endl; cout << endl; return 0;
</code></pre>
output:
<pre><code>
二维数组d88指针数组ce8二级指针aab二维数组地址1606415872,1606415872,1606415872,1606415872,16064158721606415888,1606415888,1606415888,1606415888,16064158881606415904,1606415904,1606415904,1606415904,16064159041606415892,16064158925,5初始化2ab
</code></pre>
** 指针变量的参数传递 **
请看程序中 “二级指针” 部分以及 func3, func4。
对照结果,我们可以看到 func3 并没有改变主函数中 p 的指向,为什么呢?
我们知道 C++ 中对象的拷贝构造函数主要发生在三个地方:
- 对象实例化时
- 函数中进行对象参数的值传递时
- 函数返回对象时
其中第二种情况,就对应现在这种,进行值传递时,会对实参拷贝出一个临时变量(形参),函数中使用的都是这个临时变量。
假如这里拷贝的是 _p,它也是指针类型变量,与 p 的值是一样的,也就是说它俩指向同一块内存。在 func3 中,对 _p 的值做了修改,它指向了另外一块内存。p 和 _p 是两个不同的变量,放在内存的不同地方,_p 的值变了,对 p 有影响吗?当然没有。因此经过 func3 后,p 的指向没变。
这时候有人可能就会迷惑了,那我们平时做的指针传递是咋回事?以下面的代码为例:
<pre><code>
void func(int *_p){ *_p = 2;}int a = 1;int *p = &a;cout << *p << endl;func(p);cout << *p << endl;
</code></pre>
输出结果是 1 2。
这里为啥能修改呢?这里本质上还是做的值传递,_p 和 p 是两个不同变量,值相同。不同的是,函数里面是对指针指向的内存做了修改,而 p 也指向同一块内存啊,_p自然也会变化。那么如果希望执行 func3 中的操作,并且能影响主函数中的 p,怎么办呢?可以使用二级指针,用法如 func4.二级指针就是指向指针的指针。我们把指针 p 的地址传进去,函数中复制出临时变量 _p,它的指向自然也是指针 p。通过 *_p,这里就直接操作 p 进行修改了,它指向了一段新的内存。通过 **_p就能改变 _p 指向的内容。
** 指针数组与二维数组 **
注意,只要是数组,它的元素必定在一段连续内存中存放。指针数组的定义形式如 char *p[10],表示一个一位数组,每个元素是一个 char *类型的变量,也就是一个字符指针。我们知道,一个字符指针可以指向一个一维的指针数组,因此可以用这个指针数组存放 10 个字符串。请看程序中 “指针数组” 部分。这里还有两点需要注意:
由于指针数组每个元素只是指针,因此可以指向不同长度的一维数组。
还是由于它只是指针,不含有数组相关信息,因此 sizeof(c[1]) 还是指针的大小。
此处针对字符数组再多说两句,char *p = "abc"中指针p指向的是常量,不能执行类似 p[1] = 'c' 的修改,但可以执行 ++p,因为 p 是指针变量。 char p[] = "abc" 则在函数栈区开辟了内存,可以修改字符的值,但 ++p 不行,因为 p 是数组名,是个常量。另外,分别对 p 执行 sizeof 也会不同,前者是指针,后者是数组名,含有数组对象的信息,如大小。
二维数组就很熟悉了,它每行的长度都一样,不同行连续存放。其定义方式请参考程序中 “二维数组” 部分。
** 指针数组的参数传递 **
请看程序中 func2。首先回忆一下普通一维数组,它可以通过 char a或 char a[] 方式传参,都是自动退化成同类型的指针。指针数组本质上还是个一维数组嘛,参数类型也像上面一样定义。不同的是,它每个元素不是 char类型,而是 char类型,那么只需要将 char替换为 char*即可。还记得 C++ 中主函数的类型声明吗?
int main(int argc, char **argv);
可以看到第二个参数也是用的二级指针的形式,当然,用 char *argv[]也是完全可以的。
此外,搞 Unix 编程的肯定对 env变量不陌生吧,它也是这个类型。现在我们就知道这样的变量存储的都是啥东西了。
指针数组由于允许每个元素指向的一位数组长度不同,更加灵活,因此用的比较多。
** 二维数组的参数传递 **
请看程序中的 func1。由于前面说的二维数组存放方式,我们对它的行长最关心,参数传递时不能丢掉。
二维数组可以看成一个一位数组,每个元素也是一个一维数组类型 char [2]。那么仿照上面,它的形参类型可以写成 char a[] [2],也可以写成 char *a [2],但由于 []的优先级比 高,因此我们需要用括号包裹一下 char (a)[2]。后者a是一个数组指针,指针的类型是一个长度为2的一位数组,++a会移动 2 个字节。
** 二维数组的地址问题 **
最后看一下程序 “二维数组地址” 部分和 func5。稍微解释下:
a是二维数组第一个元素的地址。*a,我们知道传递 a 时用的是数组指针,对它取对象就是一维数组,还是第一个元素的地址。a[0]就是第一行这个一维数组。&a[0]又变成那个数组指针了。a+1是数组指针的移位,前面说过,移动的是整行大小。后面以此类推。还有一点就是数组的初始化,已初始化的元素自然初始化了,后面的元素默认初始化为 0,对于 char 类型,则是 ‘\0’, ascii 码是 0,注意和 0 作区别。