《c语言深度剖析》笔记3

1.

先看下面的例子:
int *p;

现在用sizeof 测试一下(32 位系统):sizeof(p)的值为4。嗯,这说明咔出p的这个模子大小为4个byte。显然,这个模子不是“int”,虽然它大小也为4。既然不是“int”那就一定是“int *”了。好,那现在我们可以这么理解这个定义:

 

一个“int *”类型的模子在内存上咔出了4个字节的空间,然后把这个4个字节大小的

空间命名为p,同时限定这4个字节的空间里面只能存储某个内存地址,即使你存入别的任

何数据,都将被当作地址处理,而且这个内存地址开始的连续4个字节上只能存储某个int

类型的数据。

这是一段咬文嚼字的说明,我们还是用图来解析一下:


如上图所示,我们把p 称为指针变量,p 里存储的内存地址处的内存称为p 所指向的内存。

指针变量p 里存储的任何数据都将被当作地址来处理。

我们可以简单的这么理解:一个基本的数据类型(包括结构体等自定义类型)加上“*”

号就构成了一个指针类型的模子。这个模子的大小是一定的,与“*”号前面的数据类型无

关。“*”号前面的数据类型只是说明指针所指向的内存里存储的数据类型。所以,在32位

系统下,不管什么样的指针类型,其大小都为4byte。可以测试一下sizeof(void *)。


int *p = NULL 和*p = NULL 有什么区别?

很多初学者都无法分清这两者之间的区别。我们先看下面的代码:

int *p= NULL;

这时候我们可以通过编译器查看p的值为0x00000000。这句代码的意思是:定义一个指针

变量p,其指向的内存里面保存的是int类型的数据;在定义变量p的同时把p 的值设置为

0x00000000,而不是把*p的值设置为0x00000000。这个过程叫做初始化,是在编译的时候

进行的。



明白了什么是初始化之后,再看下面的代码:

int *p;

*p =NULL;

同样,我们可以在编译器上调试这两行代码。第一行代码,定义了一个指针变量p,其指向

的内存里面保存的是int 类型的数据;但是这时候变量p 本身的值是多少不得而知,也就是

说现在变量p 保存的有可能是一个非法的地址。第二行代码,给*p 赋值为NULL,即给p

指向的内存赋值为NULL;但是由于p指向的内存可能是非法的,所以调试的时候编译器可

能会报告一个内存访问错误。这样的话,我们可以把上面的代码改写改写,使p指向一块合

法的内存:

int i =10;

int *p= &i;

*p =NULL;

在编译器上调试一下,我们发现p指向的内存由原来的10变为0了;而p 本身的值, 即内

存地址并没有改变。

另外还有初学者在使用NULL的时候误写成null或Null等。这些都是不正确的,C语

言对大小写十分敏感啊。当然,也确实有系统也定义了null,其意思也与NULL没有区别,

但是你千万不用使用null,这会影响你代码的移植性



2.

先看下面的例子:

int a[5];

所有人都明白这里定义了一个数组,其包含了5个int型的数据。我们可以用a[0],a[1]

等来访问数组里面的每一个元素,那么这些元素的名字就是a[0],a[1]…吗?看下面的示意

图:


如上图所示,当我们定义一个数组a时,编译器根据指定的元素个数和元素的类型分配确定

大小(元素类型大小*元素个数)的一块内存,并把这块内存的名字命名为a。名字a一旦

与这块内存匹配就不能被改变。a[0],a[1]等为a的元素,但并非元素的名字。数组的每一个

元素都是没有名字的。那现在再来回答第一章讲解sizeof关键字时的几个问题:

sizeof(a)的值为sizeof(int)*5,32位系统下为20。

sizeof(a[0])的值为sizeof(int),32位系统下为4。

sizeof(a[5])的值在32位系统下为4。并没有出错,为什么呢?我们讲过sizeof是关键字

不是函数。函数求值是在运行的时候,而关键字sizeof求值是在编译的时候。虽然并不存在

a[5]这个元素,但是这里也并没有去真正访问a[5],而是仅仅根据数组元素的类型来确定其

值。所以这里使用a[5]并不会出错。

sizeof(&a[0])的值在32位系下为4,这很好理解。取元素a[0]的首地址。

sizeof(&a)的值在32位系统下也为4,这也很好理解。取数组a的首地址。

 

省政府和市政的区别----&a[0]和&a的区别

这里&a[0]和&a到底有什么区别呢?a[0]是一个元素,a是整个数组,虽然&a[0]和&a

的值一样,但其意义不一样。前者是数组首元素的首地址,而后者是数组的首地址。举个

例子:湖南的省政府在长沙,而长沙的市政府也在长沙。两个政府都在长沙,但其代表的

意义完全不同。这里也是同一个意思

 

数组名a 作为左值和右值的区别

简单而言,出现在赋值符“=”右边的就是右值,出现在赋值符“=”左边的就是左值。

比如,x=y。

左值:在这个上下文环境中,编译器认为x的含义是x所代表的地址。这个地址只有

编译器知道,在编译的时候确定,编译器在一个特定的区域保存这个地址,我们完全不必

考虑这个地址保存在哪里。

右值:在这个上下文环境中,编译器认为y的含义是y所代表的地址里面的内容。这

个内容是什么,只有到运行时才知道。

C 语言引入一个术语-----“可修改的左值”。意思就是,出现在赋值符左边的符号所代

表的地址上的内容一定是可以被修改的。换句话说,就是我们只能给非只读变量赋值。

既然已经明白左值和右值的区别,下面就讨论一下数组作为左值和右值的情况:

当a 作为右值的时候代表的是什么意思呢?很多书认为是数组的首地址,其实这是非常

错误的。a 作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组

的首地址。这是两码事。但是注意,这仅仅是代表,并没有一个地方(这只是简单的这么

认为,其具体实现细节不作过多讨论)来存储这个地址,也就是说编译器并没有为数组a

分配一块内存来存其地址,这一点就与指针有很大的差别。


&a 代表数组的首地址

a代表数组首元素的地址和&a[0]一样



a 作为右值,我们清楚了其含义,那作为左值呢?

a不能作为左值!这个错误几乎每一个学生都犯过。编译器会认为数组名作为左值代表

的意思是a 的首元素的首地址,但是这个地址开始的一块内存是一个总体,我们只能访问数

组的某个元素而无法把数组当一个总体进行访问。所以我们可以把a[i]当左值,而无法把a

当左值。其实我们完全可以把a当一个普通的变量来看,只不过这个变量内部分为很多小块,

我们只能通过分别访问这些小块来达到访问整个变量a的目的



3.

以指针的形式访问和以下标的形式访问
下面我们就详细讨论讨论它们之间似是而非的一些特点。例如,函数内部有如下定义:
A),char *p = “abcdef”;
B),char a[] = “123456”;


以指针的形式访问和以下标的形式访问指针
例子A)定义了一个指针变量p,p 本身在栈上占4 个byte,p 里存储的是一块内存的首
地址。这块内存在静态区,其空间大小为7 个byte,这块内存也没有名字。对这块内存的访
问完全是匿名的访问。比如现在需要读取字符‘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)。a 这时候代表的是数组首元素的首地址,假设为0x0000FF00,
然后加上4 个字符的偏移量,得到新的地址0x0000FF04。然后取出0x0000FF04 地址上的
值。
2),以下标的形式:a[4]。编译器总是把以下标的形式的操作解析为以指针的形式的操
作。a[4]这个操作会被解析成:a 作为数组首元素的首地址,然后加上中括号中4 个元素的
偏移量,计算出新的地址,然后从新的地址中取出值。


由上面的分析,我们可以看到,指针和数组根本就是两个完全不一样的东西。只是它们
都可以“以指针形式”或“以下标形式”进行访问。
一个是完全的匿名访问,一个是典型
的具名+匿名访问。一定要注意的是这个“以XXX 的形式的访问”这种表达方式。
另外一个需要强调的是:上面所说的偏移量4 代表的是4 个元素,而不是4 个byte。只
不过这里刚好是char 类型数据1 个字符的大小就为1 个byte。记住这个偏移量的单位是元
素的个数而不是byte 数,在计算新地址时千万别弄错了。


通过上面的分析,相信你已经明白数组和指针的访问方式了,下面再看这个例子:
void main()
{

int a[5]={1,2,3,4,5};

int *ptr=(int *)(&a+1);

printf("%d,%d",*(a+1),*(ptr-1));

}

打印出来的值为多少呢? 这里主要是考查关于指针加减操作的理解。
对指针进行加1 操作,得到的是下一个元素的地址,而不是原有地址值直接加1。所以,
一个类型为T 的指针的移动,以sizeof(T) 为移动单位。因此,对上题来说,a 是一个一
维数组,数组中有5 个元素; ptr 是一个int 型的指针。

&a + 1: 取数组a 的首地址,该地址的值加上sizeof(a) 的值,即&a + 5*sizeof(int),也
就是下一个数组的首地址,显然当前指针已经越过了数组的界限。
(int *)(&a+1): 则是把上一步计算出来的地址,强制转换为int * 类型,赋值给ptr。
*(a+1): a,&a 的值是一样的,但意思不一样,a 是数组首元素的首地址,也就是a[0]的
首地址,&a 是数组的首地址,a+1 是数组下一元素的首地址,即a[1]的首地址,&a+1 是下一
个数组的首地址。所以输出2

*(ptr-1): 因为ptr 是指向a[5],并且ptr 是int * 类型,所以*(ptr-1) 是指向a[4] ,
输出5。

---------------------

注意:

int a[3] = {1,2,3};

sizeof(a) = 12;

sizeof(&a) = 4;

sizeof(&a[0]) = 4;


a和&a数值相同,代表的意义不同

a代表数组首元素地址,即&a[0],a+1指向数组第二个元素

而&a代表数组首地址,&a+1则指向下一个数组的首地址


-------------------


4.

文件1 中定义如下:
char a[100];
文件2 中声明如下(关于extern 的用法,以及定义和声明的区别,请复习第一章):
extern char *a;
这里,文件1 中定义了数组a,文件2 中声明它为指针。这有什么问题吗?平时不是总说数
组与指针相似,甚至可以通用吗?但是,很不幸,这是错误的。通过上面的分析我们也能
明白一些,但是“革命尚未成功,同志仍需努力”。你或许还记得我上面说过的话:数组就
是数组,指针就是指针,它们是完全不同的两码事!他们之间没有任何关系,只是经常穿
着相似的衣服来迷惑你罢了。

当你声明为extern char *a 时,编译器理所当然的认为a 是一个指针变量,在32 位系
统下,占4 个byte。这4 个byte 里保存了一个地址,这个地址上存的是字符类型数据。虽
然在文件1 中,编译器知道a 是一个数组,但是在文件2 中,编译器并不知道这点。大多数
编译器是按文件分别编译的,编译器只按照本文件中声明的类型来处理。所以,虽然a 实际
大小为100 个byte,但是在文件2 中,编译器认为a 只占4 个byte。


显然,按照上面的分析,我们把文件1 中定义的数组在文件2 中声明为指针会发生错误。
同样的,如果在文件1 中定义为指针,而在文件中声明为数组也会发生错误:
文件1
char *p = “abcdefg”;
文件2
extern char p[];


5.

指针和数组的对比
通过上面的分析,相信你已经知道数组与指针的的确确是两码事了。他们之间是不可
以混淆的,但是我们可以“以XXXX 的形式”访问数组的元素或指针指向的内容。以后一
定要确认你的代码在一个地方定义为指针,在别的地方也只能声明为指针;在一个的地方
定义为数组,在别的地方也只能声明为数组。切记不可混淆。
下面再用一个表来总结一下
指针和数组的特性:





6.

初学者总是分不出指针数组与数组指针的区别。其实很好理解:
指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身
决定。它是“储存指针的数组”的简称。
数组指针:首先它是一个指针,它指向一个数组。在32 位系统下永远是占4 个字节,
至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。


下面到底哪个是数组指针,哪个是指针数组呢:
A),int *p1[10];
B),int (*p2)[10];
每次上课问这个问题,总有弄不清楚的。这里需要明白一个符号之间的优先级问题。
“[]”的优先级比“*”要高。p1 先与“[]”结合,构成一个数组的定义,数组名为p1,int *
修饰的是数组的内容,即数组的每个元素。那现在我们清楚,这是一个数组,其包含10 个
指向int 类型数据的指针,即指针数组。至于p2 就更好理解了,在这里“()”的优先级比
“[]”高,“*”号和p2 构成一个指针的定义,指针变量名为p2,int 修饰的是数组的内容,
即数组的每个元素。数组在这里并没有名字,是个匿名数组。那现在我们清楚p2 是一个指
针,它指向一个包含10 个int 类型数据的数组,即数组指针


再论a 和&a 之间的区别
既然这样,那问题就来了。前面我们讲过a 和&a 之间的区别,现在再来看看下面的代
码:
int main()
{

char a[5]={'A','B','C','D'};

char (*p3)[5] = &a;

char (*p4)[5] = a;

return 0;

}
上面对p3 和p4 的使用,哪个正确呢?p3+1 的值会是什么?p4+1 的值又会是什么?
毫无疑问,p3 和p4 都是数组指针,指向的是整个数组。&a 是整个数组的首地址,a
是数组首元素的首地址,其值相同但意义不同。在C 语言里,赋值符号“=”号两边的数据
类型必须是相同的,如果不同需要显示或隐式的类型转换。p3 这个定义的“=”号两边的数
据类型完全一致,而p4 这个定义的“=”号两边的数据类型就不一致了。左边的类型是指
向整个数组的指针,右边的数据类型是指向单个字符的指针。在Visual C++6.0 上给出如下
警告:warning C4047: 'initializing' : 'char (*)[5]' differs in levels of indirection from 'char *'。还好,
这里虽然给出了警告,但由于&a 和a 的值一样,而变量作为右值时编译器只是取变量的值,
所以运行并没有什么问题。不过我仍然警告你别这么用。

既然现在清楚了p3 和p4 都是指向整个数组的,那p3+1 和p4+1 的值就很好理解了。
但是如果修改一下代码,会有什么问题?p3+1 和p4+1 的值又是多少呢?
int main()
{

char a[5]={'A','B','C','D'};

char (*p3)[3] = &a;//vs2008不过编译

char (*p4)[3] = a;

return 0;

}
甚至还可以把代码再修改:
int main()
{

char a[5]={'A','B','C','D'};

char (*p3)[10] = &a;//vs2008不过编译

char (*p4)[10] = a;

return 0;

}
这个时候又会有什么样的问题?p3+1 和p4+1 的值又是多少?
上述几个问题,希望读者能仔细考虑考虑。


7.

地址的强制转换
先看下面这个例子:
struct Test
{

int Num;

char *pcName;

short sDate;

char cha[2];

short sBa[4];

}*p;
假设p 的值为0x100000。如下表表达式的值分别为多少?
p + 0x1 = 0x___ ?

int Num:0-3

char* pcName:4-7

short sDate:8-9

char cha[2]:10-11//有数组的话,对齐应该按照其元素类型的大小来对齐,这里按1对齐

short sBa[4]:12-20//这里按2对齐

sizeof(Test) = 20

(unsigned long)p + 0x1 = 0x___?
(unsigned int*)p + 0x1 = 0x___?
我相信会有很多人一开始没看明白这个问题是什么意思。其实我们再仔细看看,这个知识点
似曾相识。一个指针变量与一个整数相加减,到底该怎么解析呢?
还记得前面我们的表达式“a+1”与“&a+1”之间的区别吗?其实这里也一样。指针变
量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。这个整数的单位不是
byte 而是元素的个数。所以:
p + 0x1 的值为0x100000+sizof(Test)*0x1。至于此结构体的大小为20byte,前面的章
节已经详细讲解过。所以p +0x1 的值为:0x100014。
(unsigned long)p + 0x1 的值呢?这里涉及到强制转换,将指针变量p 保存的值强制转换
成无符号的长整型数。任何数值一旦被强制转换,其类型就改变了。所以这个表达式其实就
是一个无符号的长整型数加上另一个整数。所以其值为:0x100001。
(unsigned int*)p + 0x1 的值呢?这里的p 被强制转换成一个指向无符号整型的指针。所
以其值为:0x100000+sizof(unsigned int)*0x1,等于0x100004。



上面这个问题似乎还没啥技术含量,下面就来个有技术含量的:

在x86 系统下,其值为多少?
int main()
{

int a[4]={1,2,3,4};

int *ptr1=(int *)(&a+1);

int *ptr2=(int *)((int)a+1);

printf("%x,%x",ptr1[-1],*ptr2);

return 0;

}
这是我讲课时一个学生问我的题,他在网上看到的,据说难倒了n 个人。我看题之后告诉他,
这些人肯定不懂汇编,一个懂汇编的人,这种题实在是小case。下面就来分析分析这个问题:
根据上面的讲解,&a+1 与a+1 的区别已经清楚。
ptr1:将&a+1 的值强制转换成int*类型,赋值给int* 类型的变量ptr,ptr1 肯定指到数
组a 的下一个int 类型数据了。ptr1[-1]被解析成*(ptr1-1),即ptr1 往后退4 个byte。所以其
值为0x4。
ptr2:按照上面的讲解,(int)a+1 的值是元素a[0]的第二个字节的地址。然后把这个地址
强制转换成int*类型的值赋给ptr2,也就是说*ptr2 的值应该为元素a[0]的第二个字节开始的
连续4 个byte 的内容。
其内存布局如下图:



好,问题就来了,这连续4 个byte 里到底存了什么东西呢?也就是说元素a[0],a[1]里面
的值到底怎么存储的。这就涉及到系统的大小端模式了,如果懂汇编的话,这根本就不是问
题。既然不知道当前系统是什么模式,那就得想办法测试。大小端模式与测试的方法在第一
章讲解union 关键字时已经详细讨论过了,请翻到彼处参看,这里就不再详述。我们可以用
下面这个函数来测试当前系统的模式。
int checkSystem( )
{

union check

{

int i;

char ch;

} c;

c.i = 1;

return (c.ch ==1);

}
如果当前系统为大端模式这个函数返回0;如果为小端模式,函数返回1。
也就是说如果此函数的返回值为1 的话,*ptr2 的值为0x2000000。
如果此函数的返回值为0 的话,*ptr2 的值为0x100。


8.

二级指针是经常用到的,尤其与二维数组在一起的时候更是令人迷糊。例如:
char **p;
定义了一个二级指针变量p。p 是一个指针变量,毫无疑问在32 位系统下占4 个byte。
它与一级指针不同的是,一级指针保存的是数据的地址,二级指针保存的是一级指针的地
址。下图帮助理解


我们试着给变量p 初始化:
A),p = NULL;
B),char *p2; p = &p2;
任何指针变量都可以被初始化为NULL(注意是NULL,不是NUL,更不是null),二
级指针也不例外。也就是说把指针指向数组的零地址。联想到前面我们把尺子比作内存,
如果把内存初始化为NULL,就相当于把指针指向尺子上0 毫米处,这时候指针没有任何内
存可用。
当我们真正需要使用p 的时候,就必须把一个一级指针的地址保存到p 中,所以B)的
赋值方式也是正确的。
给p 赋值没有问题,但怎么使用p 呢?这就需要我们前面多次提到的钥匙(“*”)。
第一步:根据p 这个变量,取出它里面存的地址。
第二步:找到这个地址所在的内存。
第三步:用钥匙打开这块内存,取出它里面的地址,*p 的值。
第四步:找到第二次取出的这个地址。
第五步:用钥匙打开这块内存,取出它里面的内容,这就是我们真正的数据,**p 的值。
我们在这里用了两次钥匙(“*”)才最终取出了真正的数据。也就是说要取出二级指针
所真正指向的数据,需要使用两次两次钥匙(“*”)。
至于超过二维的数组和超过二维的指针一般使用比较少,而且按照上面的分析方法同
样也可以很轻松的分析明白,这里就不再详细讨论。读者有兴趣的话,可以研究研


9.

能否向函数传递一个数组?
看例子:
void fun(char a[10])
{

char c = a[3];

}

int main()
{

char b[10] = “abcdefg”;

fun(b[10]);

return 0;

}
先看上面的调用,fun(b[10]);将b[10]这个数组传递到fun 函数。但这样正确吗?b[10]
是代表一个数组吗?

这是一个内存异常,我们分析分析其原因。其实这里至少有两个严重的错误。
第一:b[10]并不存在,在编译的时候由于没有去实际地址取值,所以没有出错,但是
在运行时,将计算b[10]的实际地址,并且取值。这时候发生越界错误。
第二:编译器的警告已经告诉我们编译器需要的是一个char*类型的参数,而传递过去
的是一个char 类型的参数,这时候fun 函数会将传入的char 类型的数据当地址处理,同样
会发生错误。(这点前面已经详细讲解)


无法向函数传递一个数组
我们完全可以验证一下:
void fun(char a[10])
{

int i = sizeof(a);

char c = a[3];

}
如果数组b 真正传递到函数内部,那i 的值应该为10。但是我们测试后发现i 的值竟然
为4!为什么会这样呢?难道数组b 真的没有传递到函数内部?是的,确实没有传递过去,
这是因为这样一条规则:
C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元
素首地址的指针。

这么做是有原因的。在C 语言中,所有非数组形式的数据实参均以传值形式(对实参
做一份拷贝并传递给被调用的函数,函数不能修改作为实参的实际变量的值,而只能修改
传递给它的那份拷贝)调用。然而,如果要拷贝整个数组,无论在空间上还是在时间上,
其开销都是非常大的。更重要的是,在绝大部分情况下,你其实并不需要整个数组的拷贝,
你只想告诉函数在那一刻对哪个特定的数组感兴趣。这样的话,为了节省时间和空间,提
高程序运行的效率,于是就有了上述的规则
。同样的,函数的返回值也不能是一个数组,
而只能是指针。这里要明确的一个概念就是:函数本身是没有类型的,只有函数的返回值
才有类型。很多书都把这点弄错了,甚至出现“XXX 类型的函数”这种说法。简直是荒唐
至极!


既然如此,我们完全可以把fun 函数改写成下面的样子:
void fun(char *p)
{

char c = p[3];//或者是char c = *(p+3);

}
同样,你还可以试试这样子:
void fun(char a[10])
{

char c = a[3];

}

int main()
{

char b[100] = “abcdefg”;

fun(b);

return 0;

}
运行完全没有问题。实际传递的数组大小与函数形参指定的数组大小没有关系。既然
如此,那我们也可以改写成下面的样子:
void fun(char a[ ])
{

char c = a[3];

}
改写成这样或许比较好,至少不会让人误会成只能传递一个10 个元素的数组。


能否把指针变量本身传递给一个函数
我们把上一节讨论的列子再改写一下:
void fun(char *p)
{

char c = p[3];//或者是char c = *(p+3);

}
int main()
{

char *p2 = “abcdefg”;

fun(p2);

return 0;

}

我们知道p2 是main 函数内的一个局部变量,它只在main 函数内部有效。这里需要
澄清一个问题:main 函数内的变量不是全局变量,而是局部变量,只不过它的生命周期和
全局变量一样长而已。全局变量一定是定义在函数外部的。初学者往往弄错这点。)
既然它
是局部变量,fun 函数肯定无法使用p2 的真身。那函数调用怎么办?好办:对实参做一份
拷贝并传递给被调用的函数。即对p2 做一份拷贝,假设其拷贝名为_p2。那传递到函数内
部的就是_p2 而并非p2 本身。


10.

无法把指针变量本身传递给一个函数
这很像孙悟空拔下一根猴毛变成自己的样子去忽悠小妖怪。所以fun 函数实际运行时,
用到的都是_p2 这个变量而非p2 本身。如此,我们看下面的例子:
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);//free 并没有起作用,内存泄漏

return 0;

}

在运行strcpy(str,”hello”)语句的时候发生错误。这时候观察str 的值,发现仍然为NULL。
也就是说str 本身并没有改变,我们malloc 的内存的地址并没有赋给str,而是赋给了_str。
而这个_str 是编译器自动分配和回收的,我们根本就无法使用。

所以想这样获取一块内存是
不行的。那怎么办? 两个办法:
第一:用return。

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;

}

这个方法简单,容易理解。
第二:用二级指针。
void GetMemory(char ** p, int num)
{

*p = (char *)malloc(num*sizeof(char));

return p;

}
in tmain()
{

char *str = NULL;

GetMemory(&str,10);

strcpy(str,”hello”);

free(str);

return 0;

}


注意,这里的参数是&str 而非str。这样的话传递过去的是str 的地址,是一个值。在函
数内部,用钥匙(“*”)来开锁:*(&str),其值就是str。所以malloc 分配的内存地址是真正
赋值给了str 本身。
另外关于malloc 和free 的具体用法,内存管理那章有详细讨论。


11.

二维数组参数与二维指针参数
前面详细分析了二维数组与二维指针,那它们作为参数时与不作为参数时又有什么区
别呢?看例子:
void fun(char a[3][4]);

我们按照上面的分析,完全可以把a[3][4]理解为一个一维数组a[3],其每个元素都是一
个含有4 个char 类型数据的数组。上面的规则,“C 语言中,当一维数组作为函数参数的时
候,编译器总是把它解析成一个指向其首元素首地址的指针。”在这里同样适用,也就是说
我们可以把这个函数声明改写为:
void fun(char (*p)[4]);
这里的括号绝对不能省略,这样才能保证编译器把p 解析为一个指向包含4 个char 类
型数据元素的数组,即一维数组a[3]的元素。
同样,作为参数时,一维数组“[]”号内的数字完全可以省略:
void fun(char a[ ][4]);
不过第二维的维数却不可省略,想想为什么不可以省略?
注意:如果把上面提到的声明void fun(char (*p)[4])中的括号去掉之后,声明“void fun
(char *p[4])”可以改写成:
void fun(char **p);
这是因为参数*p[4],对于p 来说,它是一个包含4 个指针的一维数组,同样把这个一维数
组也改写为指针的形式,那就得到上面的写法。


这里需要注意的是:C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析
成一个指向其首元素首地址的指针。这条规则并不是递归的,也就是说只有一维数组才是
如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维
再也不可改写。
比如:a[3][4][5]作为参数时可以被改写为(*p)[4][5]。


12.

顾名思义,函数指针就是函数的指针。它是一个指针,指向一个函数。看例子:
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; 这样子是不
是好看一些呢?只可惜编译器不这么想

我们看看下面的例子:
void Function()
{

printf("Call Function!\n");

}
int main()
{

void (*p)();

*(int*)&p=(int)Function;

(*p) ();

return 0;

}
这是在干什么?*(int*)&p=(int)Function;表示什么意思?
别急,先看这行代码:
void (*p)();
这行代码定义了一个指针变量p,p 指向一个函数,这个函数的参数和返回值都是void。
&p 是求指针变量p 本身的地址,这是一个32 位的二进制常数(32 位系统)。
(int*)&p 表示将地址强制转换成指向int 类型数据的指针。
(int)Function 表示将函数的入口地址强制转换成int 类型的数据。
分析到这里,相信你已经明白*(int*)&p=(int)Function;表示将函数的入口地址赋值给指
针变量p。
那么(*p) ();就是表示对函数的调用。
讲解到这里,相信你已经明白了。其实函数指针与普通指针没什么差别,只是指向的内
容不同而已。
使用函数指针的好处在于,可以将实现同一功能的多个模块统一起来标识,这样一来更
容易后期的维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦
合度以及使接口与实现分开。

函数指针数组
现在我们清楚表达式“char * (*pf)(char * p)”定义的是一个函数指针pf。既然pf 是一
个指针,那就可以储存在一个数组里。把上式修改一下:
char * (*pf[3])(char * p);
这是定义一个函数指针数组。它是一个数组,数组名为pf,数组内存储了3 个指向函数的
指针。这些指针指向一些返回值类型为指向字符的指针、参数为一个指向字符的指针的函
数。这念起来似乎有点拗口。不过不要紧,关键是你明白这是一个指针数组,是数组。
函数指针数组怎么使用呢?这里也给出一个非常简单的例子,只要真正掌握了使用方法,
再复杂的问题都可以应对。如下:
#include <stdio.h>
#include <string.h>
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 * p);

pf[0] = fun1; // 可以直接用函数名

pf[1] = &fun2; // 可以用函数名加上取地址符

pf[2] = &fun3;

pf[0]("fun1");

pf[0]("fun2");

pf[0]("fun3");

return 0;

}

函数指针数组的指针
看着这个标题没发狂吧?函数指针就够一般初学者折腾了,函数指针数组就更加麻烦,
现在的函数指针数组指针就更难理解了。
其实,没这么复杂。前面详细讨论过数组指针的问题,这里的函数指针数组指针不就是
一个指针嘛。只不过这个指针指向一个数组,这个数组里面存的都是指向函数的指针。仅
此而已。
下面就定义一个简单的函数指针数组指针:
char * (*(*pf)[3])(char * p);
注意,这里的pf 和上一节的pf 就完全是两码事了。上一节的pf 并非指针,而是一个数组名;
这里的pf 确实是实实在在的指针。这个指针指向一个包含了3 个元素的数组;这个数字里
面存的是指向函数的指针;这些指针指向一些返回值类型为指向字符的指针、参数为一个
指向字符的指针的函数。这比上一节的函数指针数组更拗口。其实你不用管这么多,明白
这是一个指针就ok 了。其用法与前面讲的数组指针没有差别。下面列一个简单的例子:
#include <stdio.h>
#include <string.h>
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 * p);

char * (*(*pf)[3])(char * p);

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;

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值