参考http://blog.csdn.net/touch_2011/article/details/6966980
1、概述
- 指针数组:指针数组可以说成是”指针的数组”,首先这个变量是一个数组,其次,”指针”修饰这个数组,意思是说这个数组的所有元素都是指针类型,在32位系统中,指针占四个字节。
- 数组指针:数组指针可以说成是”数组的指针”,首先这个变量是一个指针,其次,”数组”修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址。
根据上面的解释,可以了解到指针数组和数组指针的区别,因为二者根本就是种类型的变量。
2、数组指针(指向数组的指针)
(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是一个数组指针变量。
a中各个元素在内存中是这样存放的:
(2)理解数组名和数组指针变量
现在我们思考a,a[0],a[1],p,a+1,a[0]+1,p+1到底是什么?
分析:
1. a是一个数组名,类型是指向一维数组的指针,不是变量,a的值是指针常量,即不能有a++或者a=p这些操作。a指向这块连续空间的首地址,值是&a[0][0]。
2. a[0]是一维数组名,类型是指向整型的指针,值是&a[0][0],这个值是一个常量。
3. a[1]是一维数组名,类型是指向整型的指针,值是&a[1][0],这个值是一个常量。
4. p是一个数组指针变量,指向一维数组的指针变量,值是&a[0][0]。可以执行p++;p=a等操作。
5. a+1表示指向下一行元素,也可以理解为指向下一个一维数组。
6. *(a+1)是取出第一行的首地址。
7. a[0]+1是指向第0行第1个元素,也可以理解为指向一维数组a[0]的第一个元素。
8. p+1同a+1
9. *(p+1)
和*(a+1)
10. 虽然a跟a[0]值是一样,但类型不一样,表示的意义不一样。通过分析就不难理解为什么((a+i)+j)和a[i][j]等效了。
既然pa是一个指针,存放一个数组的地址,那么在我们定义一个数组时,数组名称就是这个数组的首地址,那么这二者有什么区别和联系呢?
int a[2];
a是一个长度为2的整型数组,a是这个数组的首元素首地址。既然a是地址,p是指向数组的指针,那么能将a赋值给p吗?答案是不行的!因为a是数组首元素首地址,p存放的却是数组首地址,a是int*类型,a+1,a的值会实实在在的加1,而p是int[2]类型的,p+1,p则会加2,虽然数组的首地址和首元素首地址的值相同,但是两者操作不同,所以类型不匹配不能直接赋值,但是可以这样:p = &a,p相当与二维数组的行指针,现在它指向a[2]的地址。
(3)指针是数组的迭代器
#include<stdio.h>
#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;
}
理解这段代码,用指针遍历一个二维数组,是不是很像C++标准库里面vector的迭代器。注意这里只用了一个for循环,这也可以说明二维数组其实就是特殊的一维数组。
(4)数组名与数组指针变量的区别
从(2)中的分析中得出数组名是指针,类型是指向元素类型的指针,但值是指针常量,声明数组时编译器会为声明所指定的元素数量保留内存空间。数组指针是指向数组的指针,声明指针变量时编译器只为指针本身保留内存空间。
看看这个代码:
#include<stdio.h>
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);
}
注意当sizeof用于变量时返回这个变量占用的实际空间的大小。当sizeof用于数组名时,返回整个数组的大小(这里的大小指占用的字节数)。p是一个指针变量,这个变量占用四个字节。而a是数组名,所以sizeof a返回数组a中的全部元素占用的字节数。了解了sizeof,看下面这段代码输出什么
#include<stdio.h>
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));
}
从结果中看出,a在做+运算时是转化成了指针变量,此时a+i的类型是一个指针变量,而不是一个数组名。但a[i]是一个一维数组的数组名,sizeof(a[0])的值是8。
现在再来看一段代码:
#include<stdio.h>
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);
}
解释:这是因为传参的时候数组名转化成指针变量,注意到函数f中f(int a[][2])这里并不需要指定二维数组的长度,此处可以改为int (*a)[2]。所以传过来的就是一个数组指针变量。
总结: 数组名的类型是指向元素类型的指针,值是指针常量。(a+1)的类型是一个指针变量。把数组名作为参数传递的时候实际上传递的是一个指针变量。sizeof对变量和数组名操作时返回的结果会不一样。数组指针是指向数组的指针,其值可以是变量。
2、指针数组(存放指针的数组)
(1)认识指针数组
一个存放int类型的数组称为整型数组,那么存放指针的数组就叫指针数组。
#include<stdio.h>
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)深入了解
首先先定义一个指针数组,既然是数组,名字就叫arr。
char *arr[4] = {"hello", "world", "shannxi", "xian"};
//arr就是我定义的一个指针数组,它有四个元素,每个元素是一个char *类型的指针,这些指针存放着其对应字符串的首地址。
当一个变量出现左右都出现一个运算符时,没有记住运算符优先级的人就会纠结arr变量到底跟哪一个运算符先结合。如果是自己定义一个指针数组,搞不清楚运算符的优先级,那么就加上小括号(),比如定义一个指针数组,可以写成char *(arr[4]),不过在定义之前一定要清楚自己定义的变量,如果目的是一个数组,那就把arr[4]括起来,如果是一个指针,就把*arr括起来。如果是看到一段这样的代码,可以从他的初始化来分别它是数组还是指针,很明显,我这定义的是一个数组,如果是指针,会用NULL来初始化。
这个指针数组有多大呢?答案是16个字节,因为它是一个指针数组。每当出现这些问题时,脑子里一定要第一时间反应出内存映像图。
这里最左侧一列是一个很简陋但能说明意思的内存图,一般情况下,从栈区到代码区,是从高地址到低地址。栈向下增长,堆向上增长。
arr[4]是一个在主函数定义的数组。把它对应到对应到内存中,arr是一个在栈区,有四个元素的数组,而每一个数组又是一个指针,所以说它的四个元素各占四个字节,所以变量arr的大小是16个字节。
初始化arr的{“hello”, “world”, “shannxi”, “xian”};的这四个存在在内存中,只是跟arr这个变量不在同一段空间,它们被分配在只读数据区,数组arr[4]的四个指针元素,分别存放着这四个字符串的首地址,想象一下,从栈区有四只无形的手指向数据区的空间。arr+1会跳过四个字节,。也就是一个指针的大小
这就相当与定义char *p1 = “hello”,char *p1 = “world”,char *p3 = “shannxi”, char *p4 = “xian”,这是四个指针,每个指针存放一个字符串首地址,然后用arr[4]这个数组分别存放这四个指针,就形成了指针数组。
(3)指针数组排序
指针数组的排序非常有趣,因为这个数组中存放的是指针,通过比较指针指向的字符串的字典序,排序这些指针。函数实现如下:
void sort(char **pa, int n)//冒泡排序
{
int i, j;
char *tmp = NULL;
for(i = 0; i < n-1; i++){
for(j = 0; j < n-1-i; j++){
if(strcmp(*(pa+j), *(pa+j+1)) > 0){
tmp = *(pa + j);
*(pa + j) = *(pa + j + 1);
*(pa + j + 1) = tmp;
}
}
}
}
3、声明返回数组指针的函数
1、数组不能被拷贝,函数不能返回数组,只能返回数组的指针或者引用。
typedet int arr[10]; //arr是类型别名,表示的类型含有10个整数的数组
using arr=int[10]; //与上面等效
int arr[10]; //arr是一个含有10个整数的数组
int *p1[10]; //p1和[ ]的结合度要高,所以p1首先是一个10个大小的数组,即这里的p是一个数组名,其次是与*结合,表示数组中每个元素都是整型指针
int (*p)[10]=&arr; //由于括号p首先与*结合,即p是一个指针,然后才与[ ]结合,说明p指针指向大小为10的一个数组,且p指针初始化,p是一个数组指针
2、声明返回数组的指针
①返回函数指针的函数形式如下:
Type(*function(parameter_list))[dimension]
int (*func(int i))[10];
- func(int i) 意味着调用func函数时需要一个int类型的实参
- (*func(int)) 意味着对函数调用的结果是一个指针
- (*func(int i))[10] 意味着对函数调用结果的指针是一个指向数组,数组大小为10
- int (*func(int))[10] 意味着对函数调用结果的指针指向的数组中,数组元素都是整型的。
但如果我们使用类型别名,声明一个返回数组指针的函数的函数就看起来要优雅多了
using arrT=int[10]; //arr是大小为10的数组的一个别名,即相当于把int[10]看成是一种类型,该类型是一个整型数组,大小为10
arrT* func(int i);
3、使用尾置返回类型
任何函数的定义都能使用尾置返回,它在形参列表后以一个 -> 符号开头,为表示函数真正的返回类型在形参列表之后,我们在最前放置一个auto,比如:
auto func(int i)->int(*)[10]; //func接受一个int类型的实参,返回一个指针,指针指向10个整数的数组
于是现在就可以清楚地看到func函数返回的是一个指针,该指针指向10个整数的数组。
4、使用decltype
如果知道函数返回的指针指向哪个数组还可以使用decltype关键字声明返回类型,比如一个典型的例子:
#include<iostream>
int odd[] = { 1, 3, 5, 7, 9 };
int even[] = { 0, 2, 4, 6, 8 };
decltype(odd)* func(int i)
{
return (i & 1) ? &odd : &even;
}
int main()
{
std::cout << (*func(1))[2]<< std::endl;
std::cout << (*func(2))[2] << std::endl;
}
deltype的作用只是负责推导出表达式的类型,并不负责将数组类型转换成对应的指针,在这里是推导出odd的类型是一个大小为5的数组,但只是数组这种类型,要让成为一个指向这个类型的的指针,和正常一样加上*即可。