本文总结C语言中的核心内容——“指针和数组”的一些易错点和陷阱。
2010-10-25 wcdj
本文接:Dissection C Chapter 3
http://blog.csdn.net/delphiwcdj/archive/2010/10/21/5956723.aspx
1,指针
2,数组
3,指针和数组间的恩恩怨怨
4,指针数组和数组指针
5,多维数组与多级指针
6,数组参数与指针参数
7,函数指针
【问题】
[1] 什么是指针?
[2] 什么是数组?
[3] 指针和数组有什么关系?
1,指针
【1】指针的内存布局
定义了一个指针p,它占用多大的内存空间呢?用sizeof测试一下,32位系统下sizeof(p)==4。这4个字节的空间里面只能存储某个内存地址,即使你存入别的任何数据,都将被当作地址处理。
一个基本的数据类型(包括结构体等自定义类型)加上"*"号就构成了一个指针类型的模子,这个模子的大小是一定的,与"*"号前面的数据类型无关。"*"号前面的数据类型只是说明指针所指向的内存里存储的数据类型。所以,在32位系统下,不管什么样的指针类型,其大小都为4B。
#include <cstdio>
int main()
{
int val1=sizeof(int *);// 4
int val2=sizeof(void *);// 4
return 0;
}
【2】使用指针的时候,"*"号就是我们读写一块内存的一把“钥匙”。
【3】声明一个指针的时候要对其进行初始化。
int *p=NULL;
初始化过程(在编译的时候进行):定义一个指针变量p,其指向的内存里面保存的是int类型的数据,在定义变量p的同时把p的值设置为0x00000000,而不是设置*p的值。
// error
int *p;
*p=NULL;
// ok
int i=10;
int *p=&i;
*p=NULL;
【注意】#define NULL 0
【4】如何将数值存储到指定的内存地址
假设现在需要往内存0x12ff7c地址上存入一个整型数0x100,我们怎样才能做到呢?
我们知道可以通过一个指针向其指向的内存地址写入数据,这里的内存地址0x12ff7c本质就是一个指针。
#include <cstdio> int main() { int ival=1;// &ival==0x0012ff64 int *p=(int *)0x12ff7c; *p=0x100; return 0; }
【注意】将地址0x12ff7c赋值给指针变量p的时候必须强制类型转换。至于这里为什么选择内存地址0x12ff7c,而不选择别的地址,比如0xff00等。这仅仅是为了方便在VC6上测试而已。如果你选择了0xff00,也许在执行*p=0x100;这条语句的时候,编译器会报告一个内存访问错误,因为地址0xff00处的内存你可能并没有权力去访问。那么如何知道0x12ff7c是可以访问的呢?可以先定义一个变量ival,变量ival所处的内存肯定是可以被访问的,然后在编译器中调试即可观察到ival的内存地址,然后就可以给任意一个可以被合法访问的地址赋值。
其实还可以不用指针变量,直接对内存地址进行赋值。即,通过那把“钥匙 ”——"*"向内存中写入数据。
#include <cstdio> int main() { int ival=1;// &ival==0x0012ff64 // int *p=(int *)0x0012ff7c; // *p=0x100; *(int*)0x0012ff7c=0x100; return 0; }
2,数组
【1】数组的内存布局
#include <cstdio> int main() { int a[5]; int ival1=sizeof(a);// 5*4==20 int ival2=sizeof(a[0]);// 4 int ival3=sizeof(a[5]);// note, 4 int ival4=sizeof(&a[0]);// 4 int ival5=sizeof(&a);// note, 4 (vs2008, gcc) return 0; }
【思考】为什么sizeof(a[5])没有出错?
因为,sizeof是关键字不是函数,函数求值是在运行的时候,而关键字sizeof求值是在编译的时候。虽然并不存在a[5]这个元素,但是这里也并没有真正去访问a[5],而是仅仅根据数组元素的类型来确定其值的,所以这里使用a[5]不会出错。
【2】&a[0]和&a的区别——省政府和市政府的区别
a[0]是一个元素,a是整个数组,虽然&a[0]和&a的值一样,但其意义不一样。
&a[0]是数组首元素的首地址;
&a是数组的首地址;
【3】数组名a作为左值和右值的区别
x=y;
左值:在这个上下文环境中,编译器认为x的含义是x所代表的地。这个地址只有编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必考虑这个地址保存在哪里。
右值:在这个上下文环境中,编译器认为y的含义是y所代表的地址里面的内容,这个内容是什么,只有到运行时才知道。
可修改的左值:意思是,出现在赋值符左边的符号所代表的地址上的内容一定是可以被修改的。即,只能给非只读变量赋值。
【注意】
[1] 当a作为右值 的时候,其代表的意思是:数组首元素的首地址 ,而不是数组的首地址。其意义与&a[0]是一样 的。但是注意,这仅仅是代表,并没有一个地方来存储这个地址,也就是说,编译器并没有为数组a分配一块内存来存其地址。(这一点就与指针有很大的差别)
[2] 当a作为左值 的时候,可以吗?——a不能作为左值!编译器会认为数组名作为左值代表的意思是a的首元素的首地址,但是这个地址开始的一块内存是一个整体,我们只能访问数组的某个元素而无法把数组当一个整体进行访问。所以,我们可以把a[i]当作左值,而无法把a当作左值。其实,我们完全可以把a当作一个变量来看,只不过这个变量内部分为很多小块,我们只能通过分别访问这些小块来达到访问整个变量a的目的。
3,指针和数组间的恩恩怨怨
【问题】指针和数组到底有什么关系?——它们之间没有任何关系!
数组就是数组,指针就是指针,它们是完全不同的两码事!它们之间没有任何关系,只是经常穿着相似的衣服来迷惑你罢了。
【1】以指针的形式访问和以下标的形式访问
#include <cstdio> int main() { char *p="abcdef"; char a[]="123456"; printf("%c/n",*(p+4));// e printf("%c/n",p[4]);// e printf("%c/n",*(a+4));// 5 printf("%c/n",a[4]);// 5 return 0; }
【2】a和&a的区别
a和&a的值是一样的,但意思不一样。
a是数组首元素的首地址,所以,a+1是数组下一元素的首地址。
&a是数组的首地址,所以,&a+1是下一个数组的首地址。
【3】指针和数组的定义和声明
定义分配内存,而声明没有。
定义只出现一次,而声明可以出现多次。
【注意】以后一定要确认你的代码在一个地方定义为指针,在别的地方也只能声明为指针;在一个地方定义为数组,在别的地方也只能声明为数组。
【4】指针和数组的对比
指针 | 数组 |
保存数据的地址,任何存入指针变量p 的数据都会被当作地址来处理。p 本身的地址由编译器另外存储,存储在哪里,我们并不知道。 |
保存数据,数组名a 代表的是数组首元素的首地址而不是数组的首地址。&a 才是整个数组的首地址。( 区别:a+1 和&a+1) a 本身的地址由编译器另外存储,存储在哪里,我们并不知道。 |
间接访问数据,首先取得指针变量p 的内容,把它作为地址,然后从这个地址提取数据或向这个地址写入数据。 指针可以以指针的形式*(p+i) 访问;也可以以下标的形式p[i] 访问。其本质都是:先取p 的内容然后加上i*sizeof(type) 个byte 作为数据的真正地址。 |
直接访问数据,数组名a 是整个数组的名字,数组内每个元素并没有名字。只能通过“具名+ 匿名”的方式来访问其某个元素,不能把数组当一个整体来进行读写操作。 数组可以以指针的形式*(a+i) 访问;也可以以下标的形式a[i] 访问。其本质都是:a 所代表的数组首元素的首地址加上i*sizeof(type) 个byte 作为数据的真正地址。 |
通常用于动态数据结构。 |
通常用于存储固定数目且数据类型相同的元素。 |
相关的函数为malloc 和free 。 |
隐式分配和删除。 |
通常指向匿名数据( 当然也可指向具名数据) 。 |
自身即为数组名。 |
4,指针数组和数组指针
【1】指针数组和数组指针的内存布局
int *p1[10]; // [1]
int (*p2)[10]; // [2]
指针数组 :首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身决定。它是“储存指针的数组”的简称。
[1] "[]"的优先级比"*"高,p1先与"[]"结合,构成一个数组的定义,数组名为p1,int* 修饰的是数组的内容,即数组的每个元素。因此,这是一个数组,其包含10个指向int类型数据的指针,即指针数组。
数组指针 :首先它是一个指针,它指向一个数组。在32位系统下永远是占4个字节,至于它指向的数组占多少个字节,不知道。它是“指向数组的指针”的简称。
[2] "*"与p2构成一个指针的定义,指针变量名为p2,int修饰的是数组的内容,即数组的每个元素。数组在这里并没有名字,是个“匿名数组”。因此,p2是一个指针,它指向一个包含10个int类型数据的数组,即数组指针。
【2】再论a和&a之间的区别
下面代码在VC6和VS2008下测试均编译出错。
#include <cstdio> int main() { char a[5]={'a','b','c','d'}; char (*p1)[5]=&a; char (*p2)[5]=a;// error C2440: 'initializing' : cannot convert from 'char [5]' to 'char (*)[5]' return 0; }
&a是整个数组的首地址,a是数组首元素的首地址。
在C语言里,赋值符号"="两边的数据类型必须是相同的,如果不同需要显示或隐式的类型转换。p1这个定义的"="号两边的数据类型完全一致,而p2这个定义的"="号两边的数据类型就不一致了。左边的类型是指向整个数组的指针,右边的类型是指向单个字符的指针。在VC6和VS2008上给出如下的错误:error C2440: 'initializing' : cannot convert from 'char [5]' to 'char (*)[5]'。
修改一下呢:
#include <cstdio> int main() { char a[5]={'a','b','c','d'}; char (*p1)[3]=&a;// error C2440: 'initializing' : cannot convert from 'char (*)[5]' to 'char (*)[3]' return 0; }
【3】地址的强制转换
假设p的值为0x100000,下列表达式的值分别为多少?
struct Test
{
int num;
char *pcName;
short sDate;
char cha[2];
short sBa[4];
}*p;
[1] p+0x01=0x ?
[2] (unsigned long)p+0x01=0x ?
[3] (unsigned int *)p+0x01=0x ?
分析:
指针变量与一个整数相加减,并不是用指针变量里的地址直接加减这个数。这个整数的单位不是byte而是元素的个数。所以:
[1] p+0x01的值为0x100000+sizeof(Test)*0x01。此结构体的大小为20byte,所以,p+0x01的值为:0x100014。
[2] (unsigned long)p+0x01的值,首先将指针变量p保存的值强制类型转换成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以,这个表达式其实就是一个无符号的长整型数加上另一个整数,其值为:0x100001。
[3] (unsigned int *)p+0x01的值==0x100000+sizeof(unsigned int)*0x01,其值为:0x100004。
5,多维数组与多级指针
【1】二维数组
假想中的二维数组布局——“Excel表”。
内存与"尺子"的对比——内存不是表状的,而是线性的。(内存的最小单位为1个byte)
编译器总是将二维数组看成是一个一维数组,而一维数组的每一个元素又都是一个数组。
即,a[i][j]的首地址为:
&a[i]+j*sizeof(char) == a+i*sizeof(char)*4+j*sizeof(char)
用指针形式获取a[i][j]的值:*(*(a+i)+j)
【思考】输出的结果是多少?
#include <cstdio>
int main()
{
int a[3][2]={(0,1),(2,3),(4,5)};
int *p;
p=a[0];
printf("%d/n",p[0]);// 1
return 0;
}
分析:
答案不是0,注意,花括号里面嵌套的是小括号,而不是花括号!这里是花括号里面嵌套了逗号表达式,其实这个赋值就相当于int a[3][2]={1,3,5};。区别:
#include <cstdio>
int main()
{
int a[3][2]={{0,1},{2,3},{4,5}};// note
int *p;
p=a[0];
printf("%d/n",p[0]);// 0
return 0;
}
【思考】下面的值是多少?
#include <cstdio> int main() { int a[5][5]; int (*p)[4]; p=(int (*)[4])a; int ival=&p[4][2]-&a[4][2];// -4 return 0; }
分析:
在VS2008下测试,&p[4][2]-&a[4][2]==-4。
注意,当数组名a作为右值时,代表的是数组首元素的首地址,即&a[0][0],所以:
&p[4][2]==&a[0][0]+4*4*sizeof(int)+2*sizeof(int)
&a[4][2]==&a[0][0]+4*5*sizeof(int)+2*sizeof(int)
内存布局图如下:
【2】二级指针
char **p;
定义了一个二级指针变量p。p是一个指针变量,在32位系统下占4个byte。它与一级指针不同的是,一级指针保存的是数据的地址,二级指针保存的是一级指针的地址。
char c; char *p1=&c; char **p2=&p1;
6,数组参数与指针参数
【1】一维数组参数
【问题】能否向函数传递一个数组?——不能!
#include <cstdio> void fun(char a[10])// a 0x0012ff58 "12345" char * { int i=sizeof(a);// 4 char c=a[3]; } int main() { char a[10]={'1','2','3','4','5'};// a 0x0012ff58 "12345" char [10] fun(a); return 0; }
【规则】C语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元素首地址的指针。
原因是:在C语言中,所有非数组形式的数据实参均以传值形式(对实参做一份拷贝并传递给被调用的函数,函数不能修改作为实参变量的值,而只能修改传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在空间上还是在时间上,其开销都是非常大的。更重要的是,在绝大部分情况下,你其实并不需要整个数组的拷贝,你只想告诉函数在哪一时刻对哪个特定的数组感兴趣,所以就有了上面的规则。同样,函数的返回值也不能是一个数组,而只能是指针。
fun还可以改为下面的形式:
void fun(char *a)
void fun(char a[])
void fun(char a[100])// 实际传递的数组大小与函数形参指定的数组大小没有关系
【2】一级指针参数
【问题】能否把指针变量本身传递给一个函数?——不能!
下面的代码正确吗?
#include <cstdio> #include <cstdlib> #include <cstring> void GetMemory(char *p, int num) { p=(char*)malloc(num*sizeof(char)); } int main() { char *str=NULL; GetMemory(str, 10); strcpy(str,"hello"); free(str); return 0; }
测试在运行到strcpy这条语句出错,发现str的值为NULL,即没有发生改变。也就是说,malloc的内存地址并没有赋给str,而是赋给了一个临时变量_str,而这个_str是编译器自动分配和回收的。两种修改方法如下:
【第一种】用return。
#include <cstdio> #include <cstdlib> #include <cstring> char* GetMemory(char *p, int num) { p=(char*)malloc(num*sizeof(char)); return p; } int main() { char *str=NULL; str=GetMemory(str, 10); strcpy(str,"hello"); free(str); return 0; }
【第二种】用二级指针。
#include <cstdio> #include <cstdlib> #include <cstring> void GetMemory(char **p, int num) { *p=(char*)malloc(num*sizeof(char)); } int main() { char *str=NULL; GetMemory(&str, 10); strcpy(str,"hello"); free(str); return 0; }
注意,这里的参数是&str而非str。这样的话传递过去的是str的地址,是一个值。在函数内部用“钥匙”("*")来开锁:*(&str),其值就是str。所以malloc分配的内存地址是真正赋值给了str本身。
【3】二维数组参数与二维指针参数
【测试】
#include <cstdio> void fun(char a[2][3])// [1] { char c=a[1][2];// 6 } int main() { char a[2][3]={{'1','2','3'},{'4','5','6'}}; fun(a); return 0; }
下面的形式也是正确的:
void fun(char (*p)[3])// [2] { char c=p[1][2];// 6 } void fun(char a[100][3])// [3] { char c=a[1][2];// 6 } void fun(char a[][3])// [4] { char c=a[1][2];// 6 }
7,函数指针
【问题】解释下面的含义:
char * (*fun1)(char* p1, char* p2); // [1] char * *fun2(char* p1, char* p2); // [2] char *fun3(char* p1, char* p2); // [3]
[2]和[3]都是函数。
[1]是定义了一个指针变量,它指向一个函数,这个函数有两个指针类型的参数,函数的返回值类型为char*。
【1】函数指针的使用方法
#include <cstdio> #include <cstring> char *fun(char* p1, char* p2) { int i=0; i=strcmp(p1,p2); return 0==i ? p1 : p2; } int main() { char * (*pf)(char *p1, char* p2); pf=&fun;// pf=fun; (*pf)("aa","bb"); return 0; }
注意,给函数指针赋值时,可以用&fun或直接用函数名fun。这是因为函数名被编译后,其实就是一个地址,所以这两种用法没有本质的差别。
【2】下面形式如何解释?
#include <cstdio> void fun() { printf("call function/n"); } int main() { void (*p)(); *(int *)&p=(int)fun;// p=fun; or p=&fun; (*p)(); return 0; }
(int*)&p表示将地址强制转化成指向int类型数据的指针。
(int)fun表示将函数的入口地址强制转换成int类型的数据。
所以,*(int*)&p=(int)fun;表示将函数的入口地址赋值给指针变量p。
那么,(*p)();就是表示对函数的调用。
【使用函数指针的好处】
便于分层设计,利于系统抽象,降低耦合度以及使接口与实现分开。(理解)
【3】(*(void(*)())0)();——这是什么?
这是《C Traps and Pitfalls》这本经典书中的一个例子。
分析:
void(*)(),可以明白这是一个函数指针类型。这个函数没有参数,没有返回值。
(void(*)())0,这是将0强制转换为函数指针类型,0是一个地址,也就是说,一个函数存在首地址为0的一段区域内。
(*(void(*)())0),这是取0地址开始的一段内存里面的内容,其内容就是保存在首地址为0的一段区域内的函数。
(*(void(*)())0)(),就是函数调用。
【总结】类似的题目就如同上面的分析,一层一层的“分解”。
【4】函数指针数组
char* (*pf)(char* p); 定义的是一个函数指针pf。
char* (*pf[3])(char* p); 定义了一个函数指针数组,数组名为pf,数组内存储了3个指向函数的指针。
【函数指针数组的使用方法】
#include <cstdio> #include <cstring> char *fun1(char* p) { printf("%s/n",p); return p; } char *fun2(char* p) { printf("%s/n",p); return p; } char *fun3(char* p) { printf("%s/n",p); return p; } int main() { char* (*pf[3])(char*); pf[0]=fun1; // 可以直接使用函数名 pf[1]=&fun2; // 也可以函数名加上取地址符 pf[2]=&fun3; pf[0]("fun1"); pf[1]("fun2"); pf[2]("fun3"); return 0; }
【5】函数指针数组的指针
char* (*(*pf)[3])(char* p);
定义了一个函数指针数组的指针,它是一个指针,指向一个包含了3个元素的数组。这个数组里面存的是指向函数的指针,这些指针指向一些返回值类型为指向字符的指针,参数为一个指向字符的指针的函数。
【函数指针数组的指针的使用方法】
#include <cstdio> #include <cstring> char *fun1(char* p) { printf("%s/n",p); return p; } char *fun2(char* p) { printf("%s/n",p); return p; } char *fun3(char* p) { printf("%s/n",p); return p; } int main() { // 函数指针数组 char* (*a[3])(char*); // 函数指针数组的指针 char* (*(*pf)[3])(char*); pf=&a; a[0]=fun1;// 可以直接使用函数名 a[1]=&fun2;// 也可以函数名加上取地址符 a[2]=&fun3; pf[0][0]("fun1"); pf[0][1]("fun2"); pf[0][2]("fun3"); return 0; }