资源描述
水滴石穿C语言之C语言的底层操作
概述 C语言的内存模型基本上对应了现在von Neumann(冯·诺伊曼)计算机的实际存储模型,很好的达到了对机器的映射,这是C/C++适合做底层开发的主要原因,另外,C语言适合做底层开发还有另外一个原因,那就是C语言对底层操作做了很多的的支持,提供了很多比较底层的功能。
下面结合问题分别进行阐述。
问题:移位操作
在运用移位操作符时,有两个问题必须要清楚:
(1)、在右移操作中,腾空位是填 0 还是符号位;
(2)、什么数可以作移位的位数。
答案与分析:
">>"和"<<"是指将变量中的每一位向右或向左移动, 其通常形式为:
右移: 变量名>>移位的位数
左移: 变量名<<移位的位数
经过移位后, 一端的位被"挤掉",而另一端空出的位以0 填补,在C语言中的移位不是循环移动的。
(1) 第一个问题的答案很简单,但要根据不同的情况而定。如果被移位的是无符号数,则填 0 。如果是有符号数,那么可能填 0 或符号位。如果你想解决右移操作中腾空位的填充问题,就把变量声明为无符号型,这样腾空位会被置 0。
(2) 第二个问题的答案也很简单:如果移动 n 位,那么移位的位数要不小于 0 ,并且一定要小于 n 。这样就不会在一次操作中把所有数据都移走。
比如,如果整型数据占 32 位,n 是一整型数据,则 n << 31 和 n << 0 都合法,而 n << 32 和 n << -1 都不合法。
注意即使腾空位填符号位,有符号整数的右移也不相当与除以 。为了证明这一点,我们可以想一下 -1 >> 1 不可能为 0 。
问题:位段结构
struct RPR_ATD_TLV_HEADER
{
ULONG res1:6;
ULONG type:10;
ULONG res1:6;
ULONG length:10;
};
位段结构是一种特殊的结构, 在需按位访问一个字节或字的多个位时, 位结构比按位运算符更加方便。
位结构定义的一般形式为:
struct位结构名{
数据类型 变量名: 整型常数;
数据类型 变量名: 整型常数;
} 位结构变量;
其中: 整型常数必须是非负的整数, 范围是0~15, 表示二进制位的个数, 即表示有多少位。
变量名是选择项, 可以不命名, 这样规定是为了排列需要。
例如: 下面定义了一个位结构。
struct{
unsigned incon: 8; /*incon占用低字节的0~7共8位*/
unsigned txcolor: 4;/*txcolor占用高字节的0~3位共4位*/
unsigned bgcolor: 3;/*bgcolor占用高字节的4~6位共3位*/
unsigned blink: 1; /*blink占用高字节的第7位*/
}ch;
位结构成员的访问与结构成员的访问相同。
例如: 访问上例位结构中的bgcolor成员可写成:
ch.bgcolor
位结构成员可以与其它结构成员一起使用。 按位访问与设置,方便&节省
例如:
struct info{
char name[8];
int age;
struct addr address;
float pay;
unsigned state: 1;
unsigned pay: 1;
}workers;
上例的结构定义了关于一个工从的信息。其中有两个位结构成员, 每个位结构成员只有一位, 因此只占一个字节但保存了两个信息, 该字节中第一位表示工人的状态, 第二位表示工资是否已发放。由此可见使用位结构可以节省存贮空间。
注意不要超过值限制
问题:字节对齐
让偶们先来看下面这个结构体:
struct stu1
{
int a;
char b;
};
来看看sizeof(stu)的结果为多少?
怎么是8啊?
你先别急,再来看下一个例子:
struct stu2
{
char b;
int a;
}
这个sizeof(stu2)是多少?
怎么还是8啊?
现在创建一个结构体变量 stu2 s2 {" a ", 0x12345678h}; stu1 s1 {0x12345678, " a "}
运行DEGUG,怎么样发现了什么?
在第一个结构体中char b的后面内存有三个字节是添了数据的.也就是这样 78 56 34 12 61 cc cc cc
而在第二个结构体中CHAR B的后面内存中也添加了数据.61 cc cc cc 78 56 34 12
这又是怎么回事呢?
需要字节对齐当然有设计者的考虑了,原来这样有助于加快计算机的存取速度,否则就得多花指令周期了。所以,编译器通常都会对结构体进行处理,让宽度为2的基本数据类型(short等)
都位于能被2整除的地址上,让宽度为4的基本数据类型(int等)都位于能被4整除的地址上。正是因为如此两个数中间就可能需要加入填充字节,所以结构体占的内存空间就增长了。
其实字节对齐的细节和具体编译器实现相关,但一般而言,满足三个准则:
1) 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2) 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;例如上面第二个结构体变量的地址空间。
3) 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。例如上面第一个结构体变量。(哎呀!知道!真多嘴!)
现在就可以解释上面的问题了,第一个结构体变量中成员变量最宽为4(SIZEOF(INT) = 4),所以S1变量首地址必须能被4整除。(不信你试试!)S1的大小也应该为4的整数倍。但是现在s1中有 4 + 1 的空间,所以为了满足第三个条件就在char b的后面在加上三个字节的空间以凑够8个字节空间。第二个结构体变量S2中 成员变量最大宽度为4,而且按照以前的理解int a 的地址和s2的地址相差5个字节,但是为了满足第而个条件(相差的距离------偏移地址必须是4的整数倍)所以在char b的后面添加了三个字节的空间以保证int a的偏移地址是4的整数倍即为4。至于涉及到结构体嵌套的问题,你也可以用上述方法总结的,只不过你把被嵌套的结构体在原地展开就行了,不过在计算偏移地址的时候被嵌套的结构体是不能原地展开的必须当作整体。嘿嘿!偶申明一点,上述三条建议不是偶说的,是做编译器的工程师总结出来的,偶只是借用而已。我在使用VC编程的过程中,有一次调用DLL中定义的结构时,发觉结构都乱掉了,完全不能读取正确的值,后来发现这是因为DLL和调用程序使用的字节对齐选项不同,那么我想问一下,字节对齐究竟是怎么一回事?
答案与分析:
为了能使CPU对变量进行高效快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”。例如对于4字节的int类型变量,其起始地址应位于4字节边界上,即起始地址能够被4整除。
关于字节对齐:
1、 当不同的结构使用不同的字节对齐定义时,可能导致它们之间交互变得很困难。
2、 在跨CPU进行通信时,可以使用字节对齐来保证唯一性,诸如通讯协议、写驱动程序时候寄存器的结构等。
三种对齐方式:
1、 自然对齐方式(Natural Alignment):与该数据类型的大小相等。
2、 指定对齐方式 :
#pragma pack(8) //指定Align为 8;
#pragma pack() //恢复到原先值
3、 实际对齐方式:
Actual Align = min ( Order Align, Natual Align )
对于复杂数据类型(比如结构等):实际对齐方式是其成员最大的实际对齐方式:
Actual Align = max( Actual align1,2,3,…)
编译器的填充规律:
1、 成员为成员Actual Align的整数倍,在前面加Padding。
成员Actual Align = min( 结构Actual Align,设定对齐方式)
2、 结构为结构Actual Align的整数倍,在后面加Padding.
例子分析:
#pragma pack(8) //指定Align为 8
struct STest1
{
char ch1;
long lo1;
char ch2;
} test1;
#pragma pack()
现在
Align of STest1 = 4 , sizeof STest1 = 12 ( 4 * 3 )
test1在内存中的排列如下( FF 为 padding ):
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
01 FF FF FF 01 01 01 01 01 FF FF FF
ch1 -- lo1 -- ch2
#pragma pack(2) //指定Align为 2
struct STest2
{
char ch3;
STest1 test;
} test2;
#pragma pack()
现在 Align of STest1 = 2, Align of STest2 = 2 , sizeof STest2 = 14 ( 7 * 2 )
test2在内存中的排列如下:
00 -- -- -- 04 -- -- -- 08 -- -- -- 12 -- -- --
02 FF 01 FF FF FF 01 01 01 01 01 FF FF FF
ch3 ch1 -- lo1 -- ch2
注意事项:
1、 这样一来,编译器无法为特定平台做优化,如果效率非常重要,就尽量不要使用#pragma pack,如果必须使用,也最好仅在需要的地方进行设置。
2、 需要加pack的地方一定要在定义结构的头文件中加,不要依赖命令行选项,因为如果很多人使用该头文件,并不是每个人都知道应该pack。这特别表现在为别人开发库文件时,如果一个库函数使用了struct作为其参数,当调用者与库文件开发者使用不同的pack时,就会造成错误,而且该类错误很不好查。
3、 在VC及BC提供的头文件中,除了能正好对齐在四字节上的结构外,都加了pack,否则我们编的Windows程序哪一个也不会正常运行。
4、 在 #pragma pack(n) 后一定不要include其他头文件,若包含的头文件中改变了align值,将产生非预期结果。
5、 不要多人同时定义一个数据结构。这样可以保证一致的pack值。
问题:按位运算符
C语言和其它高级语言不同的是它完全支持按位运算符。这与汇编语言的位操作有些相似。 C中按位运算符列出如下:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
操作符 作用
────────────────────────────
& 位逻辑与
| 位逻辑或
^ 位逻辑异或
- 位逻辑反
>> 右移
<< 左移
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
注意:
1、按位运算是对字节或字中的实际位进行检测、设置或移位, 它只适用于字符型和整数型变量以及它们的变体, 对其它数据类型不适用。
2、关系运算和逻辑运算表达式的结果只能是1或0。 而按位运算的结果可以取0或1以外的值。 要注意区别按位运算符和逻辑运算符的不同, 例如, 若x=7, 则x&&8 的值为真(两个非零值相与仍为非零), 而x&8的值为0。
3、 | 与 ||,&与&&,~与! 的关系
&、| 和 ~ 操作符把它们的操作数当作一个为序列,按位单独进行操作。比如:10 & 12 = 8,这是因为"&"操作符把 10 和 12 当作二进制描述 1010 和 1100 ,所以只有当两个操作数的相同位同时为 1 时,产生的结果中相应位才为 1 。同理,10 | 12 = 14 ( 1110 ),通过补码运算,~10 = -11 ( 11...110101 )。<以多少为一个位序列> &&、|| 和!操作符把它们的操作数当作"真"或"假",并且用 0 代表"假",任何非 0 值被认为是"真"。它们返回 1 代表"真",0 代表"假",对于"&&"和"||"操作符,如果左侧的操作数的值就可以决定表达式的值,它们根本就不去计算右侧的操作数。所以,!10 是 0 ,因为 10 非 0 ;10 && 12 是 1 ,因为 10 和 12 均非 0 ;10 || 12也是 1 ,因为 10 非 0 。并且,在最后一个表达式中,12 根本就没被计算,在表达式 10 || f( ) 中也是如此。
水滴石穿C语言之extern声明辨析
1 基本解释
extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
另外,extern也可用来进行链接指定。
2 问题:extern 变量
在一个源文件里定义了一个数组:
char a[6];
在另外一个文件里用下列语句进行了声明:
extern char *a;
请问,这样可以吗?
答案与分析:
1)、不可以,程序运行时会告诉你非法访问。原因在于,指向类型T的指针并不等价于类型T的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]。
2)、例子分析如下,如果a[] = "abcd",则外部变量a=0x61626364 (abcd的ASCII码值),*a显然没有意义,如下图:
显然a指向的空间(0x61626364)没有意义,易出现非法内存访问。
3)、这提示我们,在使用extern时候要严格对应声明时的格式,在实际编程中,这样的错误屡见不鲜。
4)、extern用在变量声明中常常有这样一个作用,你在*.c文件中声明了一个全局的变量,这个全局的变量如果要被引用,就放在*.h中并用extern来声明。
3 问题:extern 函数1
常常见extern放在函数的前面成为函数声明的一部分,那么,C语言的关键字extern在函数的声明中起什么作用?
答案与分析:
如果函数的声明中带有关键字extern,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。即下述两个函数声明没有明显的区别:
extern int f(); 和int f();
当然,这样的用处还是有的,就是在程序中取代include “*.h”来声明函数,在一些复杂的项目中,我比较习惯在所有的函数声明前添加extern修饰。
4 问题:extern 函数2
当函数提供方单方面修改函数原型时,如果使用方不知情继续沿用原来的extern申明,这样编译时编译器不会报错。但是在运行过程中,因为少了或者多了输入参数,往往会照成系统错误,这种情况应该如何解决?
答案与分析:
目前业界针对这种情况的处理没有一个很完美的方案,通常的做法是提供方在自己的xxx_pub.h中提供对外部接口的声明,然后调用方include该头文件,从而省去extern这一步。以避免这种错误。
宝剑有双锋,对extern的应用,不同的场合应该选择不同的做法。
5 问题:extern “C”
在C++环境下使用C函数的时候,常常会出现编译器无法找到obj模块中的C函数定义,从而导致链接失败的情况,应该如何解决这种情况呢?
答案与分析:
C++语言在编译的时候为了解决函数的多态问题,会将函数名和参数联合起来生成一个中间的函数名称,而C语言则不会,因此会造成链接时找不到对应函数的情况,此时C函数就需要用extern “C”进行链接指定,这告诉编译器,请保持我的名称,不要给我生成用于链接的中间函数名。
下面是一个标准的写法:
//在.h文件的头上
#ifdef __cplusplus
#if __cplusplus
extern "C"{
#endif
#endif /* __cplusplus */
…
…
//.h文件结束的地方
#ifdef __cplusplus
#if __cplusplus
}
#endif
#endif /* __cplusplus */
水滴石穿C语言之static辨析
1、概述
static 声明的变量在C语言中有两方面的特征:
1)、变量会被放在程序的全局存储区中,这样可以在下一次调用的时候还可以保持原来的赋值。这一点是它与堆栈变量和堆变量的区别。
2)、变量用static告知编译器,自己仅仅在变量的作用范围内可见。这一点是它与全局变量的区别。
2、问题:Static的理解
关于static变量,请选择下面所有说法正确的内容:
A、若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
B、若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
C、设计和使用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题;
D、静态全局变量过大,可那会导致堆栈溢出。
答案与分析:
对于A,B:根据本篇概述部分的说明b),我们知道,A,B都是正确的。
对于C:根据本篇概述部分的说明a),我们知道,C是正确的(所谓的函数重入问题,下面会详细阐述)。
对于D:静态变量放在程序的全局数据区,而不是在堆栈中分配,所以不可能导致堆栈溢出,D是错误的。
因此,答案是A、B、C。
3、问题:不可重入函数
曾经设计过如下一个函数,在代码检视的时候被提醒有bug,因为这个函数是不可重入的,为什么?
unsigned int sum_int( unsigned int base )
{
unsigned int index;
static unsigned int sum = 0; // 注意,是static类型的。
for (index = 1; index <= base; index++)
{
sum += index;
}
return sum;
}
答案与分析:
所谓的函数是可重入的(也可以说是可预测的),即:只要输入数据相同就应产生相同的输出。
这个函数之所以是不可预测的,就是因为函数中使用了static变量,因为static变量的特征,这样的函数被称为:带“内部存储器”功能的的函数。因此如果我们需要一个可重入的函数,那么,我们一定要避免函数中使用static变量,这种函数中的static变量,使用原则是,能不用尽量不用。
将上面的函数修改为可重入的函数很简单,只要将声明sum变量中的static关键字去掉,变量sum即变为一个auto 类型的变量,函数即变为一个可重入的函数。
当然,有些时候,在函数中是必须要使用static变量的,比如当某函数的返回值为指针类型时,则必须是static的局部变量的地址作为返回值,若为auto类型,则返回为错指针。
水滴石穿C语言之typedef的问题
1. 基本解释
typedef为C语言的关键字,作用是为一种数据类型定义一个新名字。这里的数据类型包括内部数据类型(int,char等)和自定义的数据类型(struct等)。
在编程中使用typedef目的一般有两个,一个是给变量一个易记且意义明确的新名字,另一个是简化一些比较复杂的类型声明。
至于typedef有什么微妙之处,请你接着看下面对几个问题的具体阐述。
2. typedef & 结构的问题
当用下面的代码定义一个结构时,编译器报了一个错误,为什么呢?莫非C语言不允许在结构中包含指向它自己的指针吗?请你先猜想一下,然后看下文说明:
typedef struct tagNode
{
char *pItem;
pNode pNext;
} *pNode;
答案与分析:
1、typedef的最简单使用
typedef long byte_4;
给已知数据类型long起个新名字,叫byte_4。
2、 typedef与结构结合使用
typedef struct tagMyStruct
{
int iNum;
long lLength;
} MyStruct;
这语句实际上完成两个操作:
1) 定义一个新的结构类型
struct tagMyStruct
{
int iNum;
long lLength;
};
分析:tagMyStruct称为“tag”,即“标签”,实际上是一个临时名字,struct 关键字和tagMyStruct一起,构成了这个结构类型,不论是否有typedef,这个结构都存在。
我们可以用struct tagMyStruct varName来定义变量,但要注意,使用tagMyStruct varName来定义变量是不对的,因为struct 和tagMyStruct合在一起才能表示一个结构类型。
2) typedef为这个新的结构起了一个名字,叫MyStruct。
typedef struct tagMyStruct{ } MyStruct;
因此,MyStruct实际上相当于struct tagMyStruct,我们可以使用MyStruct varName来定义变量。
答案与分析
C语言当然允许在结构中包含指向它自己的指针,我们可以在建立链表等数据结构的实现上看到无数这样的例子,上述代码的根本问题在于typedef的应用。
根据我们上面的阐述可以知道:新结构建立的过程中遇到了pNext域的声明,类型是pNode,要知道pNode表示的是类型的新名字,那么在类型本身还没有建立完成的时候,这个类型的新名字也还不存在,也就是说这个时候编译器根本不认识pNode。
解决这个问题的方法有多种:
1)、
typedef struct tagNode
{
char *pItem;
struct tagNode *pNext;
} *pNode;
2)、
typedef struct tagNode *pNode;
struct tagNode
{
char *pItem;
pNode pNext;
};
注意:在这个例子中,你用typedef给一个还未完全声明的类型起新名字。C语言编译器支持这种做法。
3)、规范做法:
struct tagNode
{
char *pItem;
struct tagNode *pNext;
};
typedef struct tagNode *pNode;
3. typedef & #define的问题
有下面两种定义pStr数据类型的方法,两者有什么不同?哪一种更好一点?
typedef char *pStr;
#define pStr char *;
答案与分析:
通常讲,typedef要比#define要好,特别是在有指针的场合。请看例子:
typedef char *pStr1;
#define pStr2 char *;
pStr1 s1, s2;
pStr2 s3, s4;
在上述的变量定义中,s1、s2、s3都被定义为char *,而s4则定义成了char,不是我们所预期的指针变量,根本原因就在于#define只是简单的字符串替换而typedef则是为一个类型起新名字。
#define用法例子:
#define f(x) x*x
main( )
{
int a=6,b=2,c;
c=f(a) / f(b);
printf("%d \n",c);
}
以下程序的输出结果是: 36。
因为如此原因,在许多C语言编程规范中提到使用#define定义时,如果定义中包含表达式,必须使用括号,则上述定义应该如下定义才对:
#define f(x) (x*x)
当然,如果你使用typedef就没有这样的问题。
4. typedef & #define的另一例
下面的代码中编译器会报一个错误,你知道是哪个语句错了吗?
typedef char * pStr;
char string[4] = "abc";
const char *p1 = string;
const pStr p2 = string;
p1++;
p2++;
答案与分析:
是p2++出错了。这个问题再一次提醒我们:typedef和#define不同,它不是简单的文本替换。上述代码中const pStr p2并不等于const char * p2。const pStr p2和const long x本质上没有区别,都是对变量进行只读限制,只不过此处变量p2的数据类型是我们自己定义的而不是系统固有类型而已。因此,const pStr p2的含义是:限定数据类型为char *的变量p2为只读,因此p2++错误。
(注:关于const的限定内容问题,在本系列第二篇有详细讲解)。
#define与typedef引申谈
1) #define宏定义有一个特别的长处:可以使用 #ifdef ,#ifndef等来进行逻辑判断,还可以使用#undef来取消定义。
2) typedef也有一个特别的长处:它符合范围规则,使用typedef定义的变量类型其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置),而宏定义则没有这种特性。
5. typedef & 复杂的变量声明
在编程实践中,尤其是看别人代码的时候,常常会遇到比较复杂的变量声明,使用typedef作简化自有其价值,比如:
下面是三个变量的声明,我想使用typdef分别给它们定义一个别名,请问该如何做?
>1:int *(*a[5])(int, char*);
>2:void (*b[10]) (void (*)());
>3. doube(*)() (*pa)[9];
答案与分析:
对复杂变量建立一个类型别名的方法很简单,你只要在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头就行了。
(注:如果你对有些变量的声明语法感到难以理解,请参阅本系列第十篇的相关内容)。
>1:int *(*a[5])(int, char*);
//pFun是我们建的一个类型别名
typedef int *(*pFun)(int, char*);
//使用定义的新类型来声明对象,等价于int* (*a[5])(int, char*);
pFun a[5];
>2:void (*b[10]) (void (*)());
//首先为上面表达式蓝色部分声明一个新类型
typedef void (*pFunParam)();
//整体声明一个新类型
typedef void (*pFun)(pFunParam);
//使用定义的新类型来声明对象,等价于void (*b[10]) (void (*)());
pFun b[10];
>3. doube(*)() (*pa)[9];
//首先为上面表达式蓝色部分声明一个新类型
typedef double(*pFun)();
//整体声明一个新类型
typedef pFun (*pFunParam)[9];
//使用定义的新类型来声明对象,等价于doube(*)() (*pa)[9];
pFunParam pa;
水滴石穿C语言之编译器引出的问题
本节主要探讨C编译器下面两方面的特点所引发的一系列常见的编程问题。
· 对C文件进行分别编译:
C程序通常由几个小程序(.c文件)组成,编译器将这几个小程序分别编译,然后通过链接程序将它们组合在一起形成一个目标代码。由于编译器每次只能编译一个文件,因此它不能立即检查需要几个源文件配合才能发现的错误。
· 对函数的参数和返回值建立临时变量
C编译器会对函数的参数建立临时参数,也可能会对函数的返回值隐含传递一个指针。因为这些临时变量的隐含性存在,使得在某些情况下,特别是有指针存在的时候,会引发一系列的问题。
· C文件中所包含的头文件会和C语言一同编译
C语言中被包含的头文件是和.c文件一起编译的,头文件中的问题会反映到.c文件的编译中。
问题:C文件的分别编译
我有一个数组a定义在f1.c中,但是我想在f2.c中计算它的元素个数,用sizeof可以达到这个目的吗?
答案与分析:
答案是否定的,你没有办法达到目的,本质原因是sizeof操作符只是在“编译时(compile time)”起作用,而C语言的编译单位是每次单个.c文件进行编译(其它语言也都如此)。因此,sizeof可以确定同一个源文件中某个数组的大小,但是对于定义在另一个源文件中的数组它无能为力了,因为那已经是“运行时(run time)”才能确定的事情了。
一件事情要想做,总会有办法的,下面提供有三种可选的办法来解决这个问题:
1)、定义一个全局变量,让它记住数组的大小,在另外一个.c文件中我们通过访问这个全局变量来得到数组的大小信息(好像有点小题大做得不偿失^_^)。
2)、在某个.h文件中用宏定义数组的大小,例如#define ARRAY_SIZE 50,然后在两个源文件中都包含这个.h文件,通过直接访问ARRAY_SIZE来得到定义在不同.c文件中的数组的大小。
3)、设置数组的最后一个元素为特殊值,例如0,-1,NULL等,然后我们通过遍历数组来寻找这个特殊的结尾元素,从而判断数组的长度(这个办法效率低,也是笨笨的)。
问题:函数返回值隐含传递指针
下面的代码可以正常工作,但是在程序结束时会有一个致命错误产生。究竟是什么原因呢?
struct list
{
char *item;
struct list *next;
}
main (argc, argv)
{
...
}
答案与分析:
原因很简单,稍微注意一点不难发现,在定义结构list的右花括弧后面加一个分号就可以解决这个问题:
struct list
{
char *item;
struct list *next;
};//缺了这个分号可不行!
好了,问题是解决了,但,你知道这个错误究竟导致了什么致命问题吗?问题不是表面上那么简单的,OK,让我们来看看事情背后的真相。
首先看一看下面这段代码:
VOID Func ( struct my_struct stX)
{
.......
}
struct my_struct stY = {...};
Func (stY);
当调用函数Func的时候,是把结构变量stY的值拷贝一份到调用栈中,从而作为参数传递给函数FUNC的,这个叫做C语言的参数值传递。我相信这个你一定很清楚,那么,你应该知道:如果函数的返回值是结构变量的话,函数应该如何将值返回给调用者呢?且看下面这段代码:
struct my_structFunc (VOID)
{
.......
}
struct my_struct stY = Func();
此时函数Func的返回值是一个结构类型的值,这个返回值被放在内存中一个阴暗恐怖的地方,同时安排了一个指针指向这个地方(暂时称为“神秘指针”),而这个指针会由C语言的编译器作为一个隐藏参数传递给函数Func。当函数Func返回时,编译器生成的代码将这个由隐藏指针指向的内存区的值拷贝到返回结构stY中,从而完成将结构变量值返回给调用者。
你明白了上述所讲的东东,那么今天问题的真正原因也就呼之欲出了:
因为struct list {...}的定义后面没有加分号,导致主函数main (argc, argv)被编译器理解为是一个返回值为结构变量的函数,从而期望得到除了argc和argv以外的第三个参数,也就是我们上面提到的那个隐含传入的“神秘指针”。可是,大家知道,这里函数是main函数,main函数的参数是由程序中的启动代码(startup code)提供的。而启动代码当然认为main()天生就应该只得到两个参数,要“神秘指针”,当然没有,如此一来, main()在返回时自作主张地去调用栈中访问它的那个并不存在的第三个参数(即神秘指针),这样导致非法访问,产生致命问题。这才是这个问题的真正根源。
建议:
1)、尽量将结构变量的指针而不是结构本身作为函数参数,否则函数调用时内存拷贝的开销可不小,尤其是对那些调用频繁、结构体大的情况。
2)、结构定义的后面一定要加分号,经过上面我的大段讲述,我相信你不会犯相同的错误
问题:编译器会给函数的参数隐含制造临时副本
请问运行下面的Test函数会有什么样的结果?
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
答案与分析:
这是林锐的《C/C++高质量编程指南》上面的例子,拿来用一下。
这样调用会产生如下两个后果:
1)、能够输出hello
2)、内存泄漏
另一个相关问题:
请问运行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");
程序必然崩溃了事。
原因分析:
C编译器总是会为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西,如果想要输出动态内存,请使用指向指针的指针,或者,使用指向引用的指针。
问题:头文件和包含它的.c文件一同编译问
下面的代码非常短小,看起来毫无问题,但编译器会报告一个错误
展开阅读全文