C语言从入门到熟悉------第六阶段

头文件

程序是如何编译生成可执行文件的

在编写程序时,可以将所有程序都放在一个.c文件中,然后通过编译器将这个.c文件编译成.obj目标文件。obj就是object的缩写,即“目标”的意思。一个.obj文件就是一个编译单元。一个程序可以由一个编译单元组成,也可以由多个编译单元组成。如果不希望源代码变得很难阅读的话,那么就使用多个编译单元,即将源程序放在多个.c文件中,然后在编译时每个.c文件都会产生一个.obj文件,最后通过链接器将所有的.obj文件链接起来,就组成了一个可执行的.exe文件。

如果一个.c文件要用到另一个.c文件中定义的函数怎么办? 只需要在该.c文件中写上它要用的在另一个.c文件中定义的函数的声明就可以了,其他工作链接器会自动帮助你完成。声明后就可以在这个.c文件中调用该函数了。

但是当链接器将所有的.obj链接起来的时候,如果碰巧有相同的函数或全局变量怎么办?一般来说在同一个程序中是不允许出现两个一样的函数名或全局变量名的。但是值得庆幸的是,C/C++可以通过一种叫作链接属性的关键字来限定某个函数是属于整个程序公用的,还是只能在一个编译单元里面使用。这些关键字就是extern和static,这个在前面已经讲过了。extern是外部链接的意思,即除了这个单元以外,其他单元也能访问这个函数。static是内部链接的意思,即只能在本文件中使用,其他文件不能访问这个函数。而且与全局变量不同的是,当1.c想要使用2.c中定义的函数时只需要将该函数的声明复制一份到1.c中即可,前面的extern可以省略,因为函数默认的就是extern。

概述

什么是头文件?

在C语言家族程序中,头文件被大量地使用。一般而言,每个C/C++程序通常都由头文件(header file)和定义文件(definition file)组成。头文件主要用于保存程序的声明(declaration),后缀为.h。而“定义文件”用于编写程序的逻辑实现(implementation),后缀为.c或.cpp。

头文件本身不需要包含程序的逻辑实现代码,它只起到描述的作用。用户程序只需要按照头文件中的接口声明(函数声明)来调用相关函数,链接器就会从库或其他“定义文件”(.c或.cpp文件)中寻找相应实际定义的代码。因此,头文件是用户应用程序和函数库之间的桥梁和纽带。在整个软件中,头文件不是最重要的部分,但它是C语言家族不可缺少的部分。

头文件是没有编译意义的,编译器只会对.c文件进行编译,从而生成.obj文件。但是头文件通过# include命令包含到.c中,预处理的时候编译器会将头文件中的内容机械性地复制和粘贴到包含它的.c文件中,从而组成一个完整的.c文件,然后再对该.c文件进行编译。所以头文件中的内容实则是.c文件的组成部分。如果你不喜欢这么写,你完全可以直接将头文件里面的内容写到.c文件中,而不使用头文件。头文件只是一个工具,但不是必需的。然而如果是写大型的项目,最好不要写在一起。比如程序中定义了100个函数,那么在程序开头就要写100个函数声明,这样程序就会显得很臃肿,未见main(),先见100个函数声明,这样明显很不好。而如果将这100个函数声明写在一个头文件中,那么就只需要在程序开头包含这个头文件即可。这就相当于将这100个函数声明用头文件像袋子一样“装”了起来,然后只需要使用这个头文件就行了。所以使用头文件会使程序的可读性更强,代码看起来更具有条理。而且如果不使用头文件,那么当有多个.c文件都要使用某个函数的时候,就必须要在每个.c中文件中都进行声明,这样很麻烦!而且一旦要修改,就必须在每个.c文件中都进行修改,更麻烦!可以说,头文件就是为了解决这个问题而诞生的!它包含了这些公共的内容,然后所有需要使用这些内容的.c文件,只需要用#include命令将相应的头文件包含进来即可。如果要修改,也只需修改头文件中的内容。

头文件的作用:

头文件的作用主要有两个:

1)第一个是方便同一个项目中的多个.c文件对该项目中的某个.c文件中的函数或变量进行重用。比如1.c中定义了一个全局变量a,现在2.c和3.c都想使用这个a,我们不能在2.c和3.c中重新定义这个a,不然编译的时候就会报“多次定义”的错误。正确的做法是只需在2.c和3.c中分别用extern对该变量进行声明即可:

    extern   int   a;

但是这样做有一个缺点,就是如果要进行修改的话,那么2.c和3.c中就都要修改。而如果将这个声明写到头文件中,然后在2.c和3.c中都包含这个头文件,那么此时如果要修改的话就只需要修改头文件中的内容即可。函数也是同样的道理,同一个项目中的不同.c文件中不能定义同名的函数,否则就会报“多次定义”的错误。比如1.c中定义了一个函数,现在2.c和3.c都想要使用这个函数,那么与全局变量一样,也只需要在2.c和3.c中分别对该函数进行声明即可。但与全局变量不同的是,2.c和3.c中函数声明的前面不需要加extern,因为函数默认的就是extern。但是我们说了,这样做有缺点。因为如果要修改的话,2.c和3.c中就都要修改。但是如果将函数的声明写在头文件中,然后2.c和3.c都包含这个头文件,那么此时如果要修改的话就只需要修改头文件中的内容就行了。

2)防止定义的冲突。这一点是针对同一个.c文件而言的,对于不同的.c文件它无法预防。之所以在同一个.c文件中能预防,是因为头文件中使用了宏定义#ifndef/#define/#endif;而之所以在不同.c文件中不能预防,是因为宏定义的作用范围只在本文件中。关于头文件中的宏定义及其使用一会儿就会讲到。此外,在同一个.c文件中的“防”也只针对“赋了值的全局变量”和“结构体类型的定义”。函数的声明是不需要预防的,因为即使在同一个.c文件中,同一个函数进行多次声明也是允许的,没有语法错误,但它们起作用的就只有一个。同样,同一个.c文件中的“另一个.c文件中的全局变量的外部声明”也可以写多个,语法上是允许的,但起作用的也只有一个。

什么样的内容适合放在头文件中?

1)用户构造的数据类型,如枚举型、结构体型。

2)函数声明。

3)对全局变量的外部声明。

4)宏定义。

5)对其他头文件的#include。

这里需要注意的是:只能在头文件中对全局变量进行外部声明,即用extern对在其他源程序文件中已经定义好的全局变量进行外部声明,扩展它的作用域。永远不要将全局变量直接定义在头文件中!在C语言中(在.c文件中)如果在头文件中只定义全局变量,并不给它赋值,那么当多个.c文件都包含该头文件的时候并不会出现重复定义的错误。即使在同一个.c文件中定义两个同名的全局变量,只要不给它们赋值,那么编译的时候就不会报错。因为如果不赋值的话,定义多个同名的全局变量时起作用的只有一个,其他的都相当于声明,反之就会出现重复定义的错误。而在C++中(在.cpp文件中),如果在头文件中定义全局变量,那么在同一个工程中,只要有多个.cpp文件包含该头文件,那么不管有没有给它赋值,都会报“重复定义”的错误。所以在头文件中定义全局变量是非常不好的编程习惯,可移植性很差。在头文件中只能用extern对全局变量的作用域进行扩展。而且前面也说了,不到万不得已,永远不要使用全局变量。

“编译的时候不是分开编译的吗?分开编译的话不应该是互不影响的吗?为什么还会出现重复定义的错误呢?”编译的时候确实是分开编译的,只选择编译是不会报错的,但链接的时候将合成一个.exe文件,这时它们被合到一起就会报错了。

“# include <>”和“# include ""”的区别

用include包含头文件主要有两种用法,一种是“#include <>”,另一种是“# include ""”。它们的区别如下:

1)“# include <>”主要用于包含系统库函数的头文件,不能用于包含用户自定义的头文件。而“# include ""”不仅可以用于包含系统库函数的头文件,也可以用于包含用户自定义的头文件,所以“# include ""”功能更强大,它可以代替“# include <>”。但通常情况下包含系统库函数时都习惯使用“# include <>”,包含用户自定义的头文件时才会使用“# include ""”。

2)“# include <>”和“# include ""”在寻找头文件时的寻找路径不同。使用“# include<>”时不需要加路径,系统会自动到系统头文件目录中寻找。而使用“# include ""”时,系统会先从用户指定的路径中查找该文件是否存在,如果不存在,再从系统头文件目录中查找。如果用户不指定路径,那么此时在不同的操作系统中就会有不同的情况:

① 在Windows操作系统中,如果不指定路径,那么系统会先到工程文件所在路径下查找,如果没有工程文件或者有工程文件但是在该工程文件路径下未找到,那么再到系统头文件目录中查找。

② 在Linux操作系统中,如果不指定路径,那么系统会先到当前用户主目录中查找(包括用户主目录下的所有其他目录)。如果未找到,再到系统头文件目录中查找。

所以不管在哪个操作系统中,#include <stdio.h>都可以写成# include "stdio.h"。但写成# include "stdio.h"时就多找了一个地方,浪费了一些时间。所以为了更快地找到头文件,加快编译速度,建议系统头文件用尖括号,自定义头文件用双引号。

但这也告诉我们,如果将自己定义的头文件放到系统头文件目录中,那么也可以使用“# include <>”,而且不需要加路径。但是我们一般都不会将自己定义的头文件放到系统头文件目录中,所以只能用“# include ""”,而且一定要指明头文件所在的路径。除非头文件是定义在工程路径下(Windows)或是定义在当前用户主目录中(Linux)的,这时就可以不指定路径。事实上这也是用得最多的一种情况。这一点非常重要,千万要记住。

那么系统头文件目录在哪呢?在不同的操作系统中,系统头文件所在的目录不一样。在Ubuntu Linux中,系统头文件目录在/usr/include/目录下,常用的头文件都在里面。而在Windows下VC++ 6.0中,头文件所在目录为VC++ 6.0安装目录即“C:\Program Files (x86)\Microsoft Visual Studio\VC98\Include”。而在VS 2008中,头文件所在目录为VS2008安装目录即“C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\include”。

如何自定义头文件

头文件一般由四部分组成:

1)头文件开头处的版权和版本声明。

2)预处理块。

3)inline内联函数的定义。

4)声明部分。

但是如果是自己定义的头文件,一般只用第2和第4两个部分就可以了。预处理块就是要提前处理的部分,比如宏定义和# include包含的其他头文件。声明部分比如全局变量外部声明、结构体类型声明、函数声明。在头文件中,一般将这两个部分写在#ifndef/#define/#endif结构之间。头文件的这种结构是利用C语言进行软件开发所通用的、公认的结构。

但是如果不用#ifndef/#define/#endif也可以,也就是说可以将需要放到.h头文件中的内容直接放进来,而不需要使用#ifndef/#define/#endif。但是这样做不好!#ifndef/#define/#endif的主要作用是防止头文件的重复引用,这点下面讲完后你就明白了。大家不要觉得这个好像很复杂,其实极其简单,就与if语句差不多,事实上比if语句简单多了!

在前面讲宏定义#define的时候说过,凡是以“#”开头的都是预处理指令。如#include <stdio.h>、# define NUM 10。在源程序中预处理指令都是放在函数之外。之所以称为预处理是因为它们比程序更早地被处理,即在编译之前处理。用#define进行宏定义在前面已经讲过、用过很多了,这里就不多说了。我们直接讲#ifndef和#endif。

#ifndef/#define/#endif:

ifndef即if not define,即“如果没有定义”的意思。#ifndef/#define/#endif的作用是防止一个源程序文件对同一个头文件的重复包含。如果重复包含的话,重复进行函数声明和外部变量声明不要紧,但头文件中的结构体类型就会被重复声明,这样就会报错。这时有人会说:“怎么可能有人会在同一个源程序文件中多次包含同一个头文件呢?谁会这么写呢?”如果直接是写在源程序文件中当然不会有人犯这种错误,但是假如一个源程序文件中包含了两个头文件,而这两个头文件中都包含了同一个头文件,这时就很难察觉了,这时编译的时候就会出错。所以如果不用#ifndef/#define/#endif的话,就要费心注意头文件的问题,而使用#ifndef/#define/#endif就可以完全避免和解决这种重复包含的问题。如果使用了#ifndef/#define/#endif,那么即使在一个源程序文件中多次包含了同一个头文件,那么该头文件也只会被处理一次。

#ifndef/#define/#endif的用法一般为:

    # ifndef   标识符
    # define  标识符
        ………   // include or define sth
    #endif

功能是:如果前面没有用#define定义过这个标识符,那么就定义这个标识符,然后处理其后到#endif之间的代码。如果前面用#define定义过这个标识符,则跳出#ifndef和#endif之间的代码,执行#endif之后的代码。如果#endif之后没有代码则直接返回,相当于头文件什么都没有执行。

其中标识符从理论上来说可以自由命名,但每个头文件的“标识符”都应该是唯一的,所以一般都命名成头文件名。命名规则一般是头文件名全部大写,前后加下划线,并把文件名中的“. ”也变成下划线,如stdio.h对应的就是:

    # ifndef   _STDIO_H_
    # define    _STDIO_H_
    ………   // include or define sth.
    # endif

#endif用于结束条件编译。#endif要与#ifndef成对使用,如果丢失#endif就会报错。所以编程时最好写完#ifndef随即就将#endif写上,然后再在它们之间写内容。

位操作运算符

位操作又称位运算,顾名思义就是以“位”为单位的操作和运算。即将参与运算的数全部转换成二进制,然后一位一位地按照一定的规则进行运算。这些运算规则就是位操作规则,常用的位操作主要有“按位与(&)”、“按位或(|)”、“按位异或(^)”、“左移运算符(<<)”、“右移运算符(>>)”。位运算是不同于逻辑运算的,逻辑运算不是一位一位运算的,所以一定要将位运算与逻辑运算区分开。因为它们的运算符类似,所以容易混淆。

按位与(&)

首先要提醒的是,千万不要混淆“按位与(&)”和“逻辑与(&&)”。“按位与”的运算规则是:全1才为1,有0则为0。如果参加运算的两个数为负数,则以其补码形式表示的二进制数来进行“与”运算。在实际的应用中,“与”操作经常被用于实现特定的功能。

按位或(|)

同样强调不要混淆“按位或(|)”和“逻辑或(||)”。运算规则为:有1就为1,全0才为0。

按位异或(^)

运算规则:相同为0,不同为1。按位异或是很有用的位运算:

1)自己同自己异或,清零。

2)同1异或则按位取反。

3)同0异或则保留原值。

例子:交换两个变量的值,而不用临时变量。

不知道从什么时候开始,有人想出了不使用临时变量来交换两个数的办法。即使用“异或”。如下所示:

void Swaxor(int *p, int *q) {
	*p = *p ^ *q;
	*q = *p ^ *q;
	*p = *p ^ *q;
}

下面写一个程序试试:

#include<stdio.h>

void Swaxor(int *, int *);  //函数声明

int main(void) {
	int a = 5;
	int b = 10;

	Swaxor(&a, &b);

	printf("a = %d, b = %d\n", a, b);

	return 0;
}

void Swaxor(int *p, int *q) {
	*p = *p ^ *q;
	*q = *p ^ *q;
	*p = *p ^ *q;
}
/*
在VC++ 6.0中的输出结果是:
--------------------------------------
a = 10, b = 5
--------------------------------------
*/

受某些书籍的误导,很多人认为使用这个“技巧”程序少用了一个变量,节省了内存空间,程序运行会更快。这种想法是不对的!用“异或”交换变量既不会加快运行速度,也不会节省空间。传统方法只是两次内存的读和写,但是这种方法是“六读三写加三次异或”,反而更慢。看上去好像是少用了一个中间变量,节省了内存空间,但实际上它使用的是寄存器。此外,这种方法有很大的限制:“异或”运算符两端只能是整数,它可以是正整数、0和负整数,但不能是实数。

左移运算符(<<)

运算规则:按二进制形式将所有数字向左移动对应的位数,高位移出(舍弃),低位的空位补零。在计算机系统中,数值都是用补码进行表示和存储的。原因在于,使用补码可以将符号位和数值域统一处理;同时加法和减法也可以统一处理。此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。

正整数的补码即其二进制形式,负整数的补码是其对应的正整数的二进制形式的所有位取反(包括符号位,0变1,1变0)后加1。所以正整数的最高位是0,负整数的最高位是1。最高位表示符号位。比如:

    int i = -7;

int为32位,所以-7的补码为:

    11111111111111111111111111111001

向左移动1至28位,i的最高位都是1, i的结果还是负数;移动29位或30位后,0就移到了最高位,此时i的结果就变成了正数;移动31位后,最后一个1移到了最高位,i又变成了负数。那么移动32位后i的值变成多少了?是不是所有位都被移出去了,i就变成0了呢?在C语言中,不管是左移还是右移,真实移动的位数都是程序中指定移动的位数和数据类型的最大位数取余,然后按余数进行移位。比如int型是32位,那么当移动位数N小于32时,N%32都等于N(商为0,余数为N)。而当N等于32时,N%32等于0(商为1,余数为0),所以不管是左移32位还是右移32位都相当于没有移位。而如果N等于33,那么N%32等于1,所以不管是左移33位还是右移33位都只相当左移或右移1位。其他的同理。

通过仔细观察发现,其实左移一位就相当于乘以2,左移两位相当于乘以4,左移N位相当于乘以2的N次方。但这个计算结果的正确性是建立在移动的位数不会将有效数据位移到最高位或移出去的基础上的。

右移运算符(>>)

右移运算和左移运算类似。右移运算的规则是按二进制形式将所有的数字向右移动对应的位数。与左移运算不同的是:右移运算时,低位移出,高位的空位补符号位。如果是正数,空位补0;如果是负数,空位补1。也就是说右移的时候,不管移动多少位正数都不会变成负数,负数也不会变成正数。

右移一位相当于除以2,右移两位相当于除以4,右移N位相当于除以2的N次方。但是同样,计算结果的正确性是建立在移动的位数不会将有效数据位移出去的基础上。

  • 51
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值