基础C语言知识串串香11☞宏定义与预处理、函数和函数库

六、C语言宏定义与预处理、函数和函数库

6.1 编译工具链

  • 源码.c ——> (预处理)——>预处理过的.i文件——>(编译)——>汇编文件.S——>(汇编)——>目标文件.o->(链接)——>elf可执行程序

  • 预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链(gcc就是一个编译工具链)。

  • gcc中各选项的使用方法:

    • -E:只预处理不编译(执行头文件的扩展、宏替换、条件编译筛选、去掉注释等),生成.i文件
    • -S:只编译不汇编(将C/C++语言程序翻译成汇编语言),生成.S文件
    • -c:只汇编不链接(将汇编语言翻译成机器指令),生成.o文件
    • -o:链接(将目标文件和库文件进行链接,得到可执行文件),生成elf可执行文件

预处理的意义

  • 编译器本身的主要目的是编译源代码(将C的源代码转化成.S的汇编代码)。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。预处理器帮编译器做一些编译前的杂事。如:
    • gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不连接,仅生成.o目标文件。
    • gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。
    • 链接器:链接的时候是把目标文件(二进制)通过有序的排列组合起来,如star.smain.cled.c这三个源文件,分别被编译成三个目标文件,每个目标文件有很多函数集合。链接的时候会根据运行思路把这些杂乱的函数给排列组合起来,不是把目标文件简单的排列组合。
    • 可执行程序含有很多符号信息(符号表,里面的符号与一个个地址相对应,如函数名max对应一个地址),但在执行时有套程序会把这些符号信息给过滤掉,得到纯净的二进制代码,最后把他们加载到内存中去。debug版本就是有符号信息,而Release版本就是纯净版本的。
    • strip工具:strip是把可执行程序中的符号信息给拿掉,以节省空间。
    • objcopy工具:可将可执行程序转变成可烧录的bin文件。

6.2 预处理

6.2.1 #define的用法

  • #define 是一个预处理指令,这个预处理执行可以定义宏。与所有预处理指令一样,预处理指令#define用#符号作为行的开头。预处理指令从#开始,到其后第一个换行符为止。也就是说,指令的长度限于一行代码。如果想把指令扩展到几个物理行,可使用反斜线后紧跟换行符的方法实现,该出的换行符代表按下回车键在源代码文件中新起一行所产生的字符,而不是符号 \n 代表的字符。在预处理开始钱,系统会删除反斜线和换行符的组合,从而达到把指令扩展到几个物理行的效果。可以使用标准C注释方法在#define行中进行注释。
//使用反斜线+回车
#define OW "hello\
world!" /*注意第二行要左对齐*/
  • 每一个#define行由三部分组成:

    • 第一部分,指令#deine自身。
    • 第二部分,所选择的缩略语,这些缩略语称为宏(分为对象宏和函数宏)。宏的名字中不允许有空格,而且必须遵循C变量命名规则:只能使用字母、数字和下划线(_),第一个字符不能为数字。习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
    • 第三部分,(#define行的其余部分)称为替换列表或主体。 注意,结尾没有分号*
  • 下面来看一个例子:

    #include <stdio.h> 
    #define OW 2 * 2
    #define OW 2  *  2
    //#undef OW  需要先取消宏定义
    #define OW 2*2 
    
    int main (void)
    {	
    	printf ("%d\n", OW);	
    	return 0;
    }
    输出结果:
    define.c:5:0: 警告: “OW”重定义 [默认启用]
    define.c:4:0: 附注: 这是先前定义的位置
    
    • 相同定义意味着主体具有相同顺序的语言符号。因此,下面两个定义相同(两者都有三个相同的语言符号,而且额外的空格不是主体的一部分):
      #define OW 2 * 2  
      #define OW 2  *  2
      
    • 下面的定义则被认为是不同的(只有一个(而非三个)语言符号,因此与前面两个定义不同):
      #define OW 2*2
      
    • 可以使用#undef指令重新定义宏。
6.2.1.1 宏所代表的数字可以在编译命令中指定(使用-D选项)
/* 宏演示 */
#include <stdio.h>
int main()
{	
	int num=0;	
	int arr[SIZE]={};   //使用gcc -D可以宏定义这个数字	
	for(num = 0;num <= SIZE - 1;num++){		
		arr[num]=num;		
		printf("%d ",arr[num]);	}	
		printf("\n");	return 0;
	}
	/*
	gcc -DSIZE=4 define.c
	输出结果:
	0 1 2 3
	*/
6.2.1.2 函数宏
  • 通过使用参数,可以创建外形和作用都与函数相似的类函数宏。宏的参数也用圆括号括起来。类函数宏的定义中,用圆括号括起来一个或多个参数,随后这些参数出现在替换部分。

    #include <stdio.h>
    #define SQUARE(X) X*X
    #define PR(X) printf ("The result is %d\n", X) 
    
    int main (void)
    {	
    	int x = 4;	
    	int z; 	
    	printf ("x = %d\n", x);	z = SQUARE(x);	
    	printf ("Evaluating SQUARE(x): ");	
    	PR(z); 
    		
    	z = SQUARE(2);	
    	printf ("Evaluating SQUARE(2): ");	
    	PR(z); 	
    	
    	printf ("Evaluating 100/SQUARE(2): ");	
    	PR(100/SQUARE(2)); 	
    	
    	z = SQUARE(x+2);	
    	printf ("Evaluating SQUARE(x+2): ");	
    	PR(z);		
    	
    	printf ("x is %d\n", x);	
    	z = SQUARE(++x);	
    	printf ("Eavluating SQUARE(++x): ");	
    	PR(SQUARE (++x));	
    	
    	printf ("After incrementing, x is %x\n", x); 	
    	return 0;
    	}
    	/*
    	输出结果:
    	x = 4
    	Evaluating SQUARE(x): The result is 16Evaluating 
    	SQUARE(2): The result is 4
    	Evaluating 100/SQUARE(2): The result is 100
    	Evaluating SQUARE(x+2): The result is 14x is 4
    	Eavluating SQUARE(++x): The result is 36
    	After incrementing, x is 6
    	*/
    
    • SQUARE(x+2) 输出结果是14,而不是想要的66 = 36。这是因为预处理器不进行计算,而只进行字符串替换。在出现x的地方,预处理都用字符串 x+2进行替换。xx 变为 x+2*x+2 根据运算符优先级,则结果为 14
    • 100/SQUARE(2)输出结果是 100,而不是想要的 25。因为,根据优先级规则,表达式是从左到右求值的,100/2*2 = 100。
    • 要处理前面两个示例中的情况,需要如下定义: #define SQUARE(x) ((x) * (x))从中得到的经验是使用必须的足够多的圆括号来保证以正确的顺序进行运行和结合。
    • SQUARE(++x) 根据编译器的不同会出现两种不同的结果。解决这个问题的最简单的方法是:避免在宏的参数中使用++x。一般来说,在宏中不要使用增量或减量运算符。

    参看:C 语言再学习 – 运算符与表达式

6.2.1.3 利用宏参数创建字符串:#运算符
  • 在类函数宏的替换部分中,#符号用作一个预处理运算符,它可以把语言符号转化为字符串。例如:如果x是一个宏参量,那么#x可以把参数名转化为相应的字符串。该过程称为字符串化

    #include <stdio.h>
    #define PSQR(x) printf("The square of "#x" is %d\n", ((x)*(x))) 
    int main (void)
    {	
    	int y = 2;	
    	PSQR (y);	
    	PSQR (2 + 4);	
    	return 0;
    }
    /*
    输出结果:
    The square of y is 4
    The square of 2 + 4 is 36
    */
    
    #include <stdio.h>
    #include <string.h>
    #define VEG(n) #n
    
    int main()
    {	
    	char str[20];	
    	strcpy(str,VEG(num));	//num 	
    	printf("%s\n",str);		//拷贝	
    	return 0;
    }
    /*
    输出结果:
    num
    */
    
6.2.1.4 预处理器的粘合剂:##运算符
  • 和#运算符一样,##运算符可以用于类函数宏的替换部分。另外,##还可用于类对象宏的替换部分。这个运算符把两个语言符号组合成单个语言符号。

    #include <stdio.h>
    #define XNAME(n) x##n
    #define PRINT_XN(n) printf ("x"#n" = %d\n", x##n) 
    
    int main (void)
    {	
    	int XNAME (1) = 14; //变为 int x1 = 14;	
    	int XNAME (2) = 20; //变为 int x2 = 20;	
    	PRINT_XN (1);       //变为 printf ("x1 = %d\n", x1);	
    	PRINT_XN (2);       //变为 printf ("x2 = %d\n", x2);	
    	return 0;
    }
    /*
    输出结果:
    x1 = 14
    x2 = 20
    */
    
6.2.1.5 宏用于简单函数
#include <stdio.h>
#define MAX(x,y) ((x)>(y) ? (x) : (y))  /*比较大小*/
#define ABS(x) ((x) < 0 ? -(x) : (x))   /*绝对值*/
#define ISSIGN(x) ((x) == '+' || (x) == '-' ? 1 : 0)  /*正负号*/ 

int main()
{	
	printf ("较大的为: %d\n", MAX(5,3));	
	printf ("绝对值为: %d\n", ABS (-2));	
	printf ("正负号为: %d\n", ISSIGN ('+'));	
	return 0;
}
/*
输出结果:
较大的为: 5
绝对值为: 2
正负号为: 1
*/
  • 下面是需要注意的几点:

    • 宏的名字中不能有空格,但是在替代字符串中可以使用空格。ANSI C 允许在参数列表中使用空格。
    • 用圆括号括住每个参数,并括住宏的整体定义
    • 大写字母表示宏函数名,便于与变量区分。
    • 有些编译器限制宏只能定义一行。即使你的编译器没有这个限制,也应遵守这个限制。
    • 宏的一个优点是它不检查其中的变量类型,这是因为宏处理字符型字符串,而不是实际值。
  • 面试:用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)

    • #define SEC (60*60*24*365)UL
    • 考察内容:
      • 1、懂得预处理器将为你计算常量表达式的值,因此,可直接写出你是如何计算一年中有多少秒而不是计算出实际的值,这样更清晰而没有代价。
      • 2、意识到这个表达式将使一个16 位机的整形数溢出,因此要用到长整形符号 L ,告诉编译器这个常数是长整形数。
  • 面试:写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个

    • #define MIN(A,B) ((A) <= (B) ? (A) : (B))
    • 考察内容:
      • 1、三目表达式的使用
      • 2、使用必须的足够多的圆括号来保证以正确的顺序进行运行和结合
      • 3、进一步讨论,在宏中不要使用增量或减量运算符

参看:宏名必须用大写字母吗?

  • 研究:C语言中用宏定义(define)表示数据类型和用typedef定义数据类型有什么区别?
    • 宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。
      #define P1 int *
      typedef (int *) P2
      
      • 从形式上看这两者相似,但在实际使用中却不相同:
        • P1 a, b; 在宏代换后变成: int *a, b; 表示 a 是指向整型的指针变量,而 b 是整型变量。
        • P2 a, b; 表示a,b都是指向整型的指针变量。因为PIN2是一个类型说明符。
      • 总结,typedef和#define的不同之处:
        • 1、与#define不同,typedef 给出的符号名称仅限于对类型,而不是对值。
        • 2、typedef 的解释由编译器,而不是是处理器执行。
        • 3、虽然它的范围有限,但在其受限范围内,typedef 比 #define 更灵活。
6.2.1.6 用于定义字符串,尤其是路径
  • A)定义路径:#define ENG_PATH_1 E:\English\listen_to_this\listen_to_this_3
  • B)定义字符串:#define ENG_PATH_2 "E:\English\\listen_to_this\\listen_to_this_3"

6.2.2 #undef 指令

  • 取消定义一个给定的 #define

    • 例如有如下宏定义:#define LIMIT 40,则指令#undef LIMIT会取消该定义。现在就可以重新定义LIMIT,以使它有一个新的值。
    • 即使开始没有定义LIMIT,取消LIMIT的定义也是合法的。如果想使用一个特定名字,但又不能确定前面是否已经使用了该名字,为安全起见,就可以取消该名字的定义。
    • 注意:#define 宏的作用域从定义点开始,直到用 #undef 指令取消宏为止,或直到文件尾为止(由二者中最先满足的那个结束宏的作用域)。还应注意,如果用头文件引入宏,那么,#define 在文件中的位置依赖 #define 指令的位置。
    #include <stdio.h>
    #define X 3
    #define Y X*3
    #undef X
    #define X 2 
    int main (void)
    {	
    	printf ("Y = %d\n", Y);	
    	printf ("X = %d\n", X);	
    	return 0;
    }
    
    /*
    输出结果:
    Y = 6
    X = 2
    */
    
    #include <stdio.h>
    #define X 3
    #define Y X*3
    #define X 2   //不可重复定义  
    
    int main (void)
    {
    	int z = Y;	
    	printf ("Y = %d\n", z);	
    	printf ("X = %d\n", X);	
    	return 0;
    }
    
    /*
    输出结果:
    test.c:4:0: 警告: “X”重定义 [默认启用]
    test.c:2:0: 附注: 这是先前定义的位置
    */
    

6.2.3 文件包含:#include

  • 预处理器发现#include指令后,就会寻找后跟的文件名并把这个文件的内容包含但当前文件中。被包含文件中的文件将替换源代码文件中的#include指令,就像你把被包含文件中的全部内容键入到源文件中的这个特定位置一样。
  • #include 指令有两种使用形式:
    1. 文件名放在尖括号中#include <filename.h>
      • 在UNIX系统中,尖括号告诉预处理器在一个或多个标准系统目录中寻找文件。/usr/include 、kernel/include
    2. 文件名放在双引号中#include "filename.h"
      • 在UNIX系统中,双引号告诉预处理器现在当前目录(或文件名中指定的其他目录)中寻找文件,然后在标准位置寻找文件。

扩展:C语言再学习 – 常用头文件和函数(转)

Lniux的文件系统中有一个大分组,它包含了文件系统中所有文件,这个大的分组用一个专门的目录表示,这个目录叫做根目录,根目录可以使用“/”表示
路径可以用来表示文件或者文件夹所在的位置,路径是从一个文件夹开始走到另一个文件夹或者文件位置中间的这条路。把这条路经过的所有文件夹名称按顺序书写出来的结果就可以表示这条路。
路径分为绝对路径和相对路径 :
绝对路径:起点必须是根目录,如 /abc/def 所有绝对路径一定是以“/”作为开头的
相对路径:可以把任何一个目录作为起点,如…/…/abc/def 相对路径编写时不应该包含起点位置 。相对目录中“…”表示上层目录,用“.”表示当前目录。
终端窗口里的当前目录是所有相对路径的起点,当前目录的位置是可以修改的。
pwd 命令可以用来查看当前目录的位置
cd 命令可以用来修改当前目录位置
ls 命令可以用来查看一个目录的内容

6.2.4 条件编译

参看:条件编译#ifdef的妙用详解_透彻

#if:表示如果…
#ifdef: 表示如果定义…
#ifndef:表示如果没有定义…
#else:表示否则…,与#ifdef、#ifndef搭配使用。
#elif:表示否则如果…,与#if、#ifdef、#ifndef搭配使用。==没有#elseif ==
#endif:表示结束判断,与#if、#ifdef、#ifndef搭配使用

  • 注意:#if 和 if 区别

    • #if=>主要用于编译期间的检查和判断
    • if=>主要用于程序运行期间的检查和判断
  • (1)最常见的形式:

    #ifdef 标识符 
    	程序段1 
    #else 
    	程序段2 
    #endif 
    
    • 当"标识符"已经被定义过(一般用#define命令定义),则对程序段1进行编译,否则编译程序段2。
    • #else部分也可以没有,即:
      #ifdef 标识符
      	程序段1
      #endif
      
      • 这里的“程序段”可以是语句组,也可以是命令行。这种条件编译可以提高C源程序的通用性。
      • 如果一个C源程序在不同计算机系统上运行,而不同的计算机又有一定的差异。例如,我们有一个数据类型,在Windows平台中,应该使用long类型表示,而在其他平台应该使用float表示,这样往往需要对源程序做必要的修改,这就降低了程序的通用性。可以用以下的条件编译:
        #ifdef WINDOWS 
        #define MYTYPE long 
        #else 
        #define MYTYPE float
        #endif 
        
        • 如果在Windows上编译程序,则可以在程序的开始加上#define WINDOWS,这样则编译#define MYTYPE long
        • 如果在这组条件编译命令之前曾出现#define WINDOWS 0,则预编译后程序中的MYTYPE都用float代替。
      • 这样,源程序可以不必任何修改就可以用于不同类型的计算机系统。当然以上介绍的只是一种简单的情况,可以根据此思路设计出其他的条件编译。
        • 例如,在调试程序时,常常希望输出一些所需的信息,而在调试完成后不再输出这些信息。可以在源程序中插入以下的条件编译段:
          #ifdef DEBUG 
          	print ("device_open(%p)\n", file); 
          #endif 
          
          • 如果在它的前面有#define DEBUG,则在程序运行时输出file指针的值,以便调试分析。调试完成后只需将这个define命令行删除即可。
          • 有人可能觉得不用条件编译也可以达到此目的,即在调试时加一批printf语句,调试后再将prntf语句全部删除。的确,这是可以的。但是,当调试时加的printf语句比较多时,修改的工作量是很大的。用条件编译,则不必一一删除printf语句。只需删除前面的一条#define DEBUG 命令即可,这时所有的用DEBUG 作标识符的条件编译段都使其中的printf语句不起作用,起到统一控制的作用,如同一个“开关”一样。
  • (2)有时也采用下面的形式

    #ifndef 标识符 
    程序段1 
    #else 
    程序段2 
    #endif 
    
    • 只是第一行与第一种形式不同:将#ifdef改为#ifndef。它的作用是,若标识符未被定义则编译程序段1,否则编译程序段2。这种形式与第一种形式的作用相反。

    • 一般地,当某文件包含几个头文件,而且每个头文件都可能定义了相同的宏,使用#ifndef可以防止该宏重复定义

      /*test.h*/
      #ifndef SIZE
      #define SIZE 100
      #endif
      
      • #ifndef 指令通常用于防止多次包含同一文件,也就是说,头文件可采用类似下面几行的设置:

        //头文件卫士
        #ifndef THINGS_H_
        #define THINGS_H_
        #endif
        
  • (3) #if 后面跟一个表达式,而不是一个简单的标识符

    #if 表达式 
    程序段1 
    #else 
    程序段2 
    #endif 
    
    • 当指定的表达式为真(非零)时就编译程序段1,否则编译程序段2。可以事先给定一定条件,使程序在不同的条件下执行不同的功能。例如:

      #include <stdio.h>
      #define LETTER 1
      int main (void)
      {	
      #if LETTER	
      	printf ("111\n");	
      #else	
      	printf ("222\n");	
      #endif	
      	return 0;
      }
      /*
      输出结果:
      111
      */
      
    • 这种形式也可以用作注释用:#if 1 和 #if 0

      #include <stdio.h>
      int main (void)
      {	
      #if 0	
      	printf ("111\n");	
      #endif		
      	printf ("222\n");	
      	return 0;
      }
      /*
      输出结果:
      222
      */
      
  • (4)最后一种形式

    #if 标识符
    程序段
    #elif
    程序段1
    #elif
    程序段2
    ...
    #else
    程序段n
    #endif
    
    • #if...#elif(任意多次)...#else...#endif,以上结构可以从任意逻辑表达式选择一组编译,这种结构可以根据任意逻辑表达式进行选择。
      /* 	条件编译演示 */
      #include <stdio.h>
      #define SAN
      
      int main()
      {
      #if	defined(YI)  		//布尔值	
      	printf("1\n");
      #elif	defined(ER)			//布尔值	
      	printf("2\n");
      #elif	defined(SAN)		
      	printf("3\n");
      #else	
      	printf("4\n");
      #endif	
      	return 0;
      }
      /*
      输出结果:
      3
      */
      
      • 这里,define是一个预处理运算符。如果 define 的参数已用#define定义过,那么define返回1,否则返回 0 。这种方法的优点在于它可以和#elif一起使用。
  • 应用示例

    • 假设我们已在程序首部定义:
      #define DEBUG
      #define TEST
      
    1. 利用#ifdef 和 #endif 将程序功能模块包括进去,以向某用户提供该功能

      //在程序首部定义#define HNLD:
      #ifdef HNLD
      include"n166_hn.c"
      #endif
      
      • 如果不许向别的用户提供该功能,则在编译之前将首部的HNLD加下划线即可。
    2. 在每一个子程序前加上标记,以便追踪程序的运行

      #ifdef DEBUG
      printf(" Now is in hunan !");
      #endif
      
    3. 避开硬件的限制。有时一些具体应用环境的硬件不一样,但限于条件,本地缺乏这种设备,于是绕过硬件,直接写出预期结果。具体做法是:

      #ifndef TEST
      i=dial();	//程序调试运行时绕过此语句
      #else
      i=0;
      #endif
      //调试通过后,再屏蔽TEST的定义并重新编译,即可发给用户使用了。
      
    4. 确保使用的标识符在其他任何地方都没有定义过

      • 通常编译器提供商采用下述方法解决这个问题:用文件名做标识符,并在文件名中使用大写字母、用下划线代替文件名中的句点字符、用下划线(可能使用两条下划线)做前缀和后缀。例如,检查头文件read.h,可以发现许多类似的语句:

        #ifndef  __READ_H__     //防止被重复定义
        #define __READ_H__    
        extern int num=0;
        #endif
        

参看:C语言再学习 – 标识符

6.2.5 扩展:extern “C”

  • 通过 extern "C" 可以要求 C++ 编译器按照 C方式处理函数接口,即不做换名,当然也就无法重载。
    1. C 调 C++在 C++ 的头文件如下设置

      extern "C" int add (int x, int y);
      extern "C" {
      	int add (int x, int y);
      	int sub (int x, int y);
      }
      
      //示例 add.h
      #ifndef _ADD_H
      #define _ADD_H
      
      #ifdef __cplusplus
      extern "C" {
      #endif
      
      int add (int ,int );
      
      #ifdef __cplusplus
      }
      #endif
      
      #endif
      
    2. C++ 调 C在C++ 的主函数如下设置

      extern "C" {
      #include "chead.h"
      }
      
      //示例 main.cpp
      #include <iostream>
      using namespace std;
      
      extern "C" {
      #include "05sub.h"
      }
      
      int main (void) 
      {	
      	int x=456,y=123;	
      	cout << x << "+" << y << "=" << sub(x, y) << endl;	
      	return 0;
      }
      

6.2.6 预定义宏

__DATE__:进行预处理的日期(“mm dd yyyy”形式的字符串文字)

__FILE__:代表当前源代码文件名的字符串

__BASE_FILE__:获取正在编译的源文件名

__LINE__:代表当前源代码文件中的行号

__TIME__:源文件编译时间,格式为“hh: mm: ss”

__STDC__:设置为 1时,表示该实现遵循 C标准

__STDC_HOSTED__:为本机环境设置为 1,否则设为 0

__STDC_VERSION__:为C99时设置为199901L,C11为201112L,C18、C17为201710L

__FUNCTION__或者 __func__:获取所在的函数名(预定义标识符,而非预定义宏)

//part.c
#include <stdio.h>
int main (void)
{	
	printf ("The file is %s\n", __FILE__);	
	printf ("The base_file is %s\n", __BASE_FILE__);	
	printf ("The line is %d\n", __LINE__);	
	printf ("The function is %s\n", __FUNCTION__);	
	printf ("The func is %s\n", __func__);	
	printf ("The date is %s\n", __DATE__);	
	printf ("The time is %s\n", __TIME__);	
	return 0;
}
	/*
	输出结果:
	The file is part.c
	The base_file is part.c
	The line is 6
	The function is main
	The func is main
	The date is Nov 22 2016
	The time is 15:46:30
	*/

6.2.7 常用的新指令

#line 整数n =>表示修改代码的行数/指定行号 插入到程序中表示从行号n开始执行,修改下一行的行号为n
#error 字符串 => 表示产生一个错误信息
#warning 字符串 => 表示产生一个警告信息

//#line 预处理指令的使用
#include <stdio.h>
#line 200 
int main(void)
{	
	printf("The line is %d\n",__LINE__);	
	return 0;
}
/*
输出结果:
The line is 203
*/
//#error和#warning的使用
#include <stdio.h> 
#define VERSION 4
#define VERSION 2
#define VERSION 3

#if(VERSION < 3)	
#error "版本过低"
#elif(VERSION > 3)
#warning "版本过高"
#endif 

int main(void){	
	printf("程序正常运行\n");	
	return 0;
}
/*
输出结果:
警告: #warning "版本过高"
//错误: #error "版本过低"
//程序正常运行
*/

6.2.8 #pragma

  • #pragma GCC dependency 文件名

    • 表示当前文件依赖于指定的文件,如果当前文件的最后一次修改的时间早于依赖的文件,则产生警告信息
    #include <stdio.h>	
    #pragma GCC dependency "01print.c"	//当前程序依赖于01print.c文件
    int main(void){	
    	printf("Good Good Study,Day Day Up!\n");	
    	return 0;
    }
    /*
    输出结果:
    致命错误: 01print.c:没有那个文件或目录编译中断。
    */
    
  • #pragma GCC poison 标示符

    • 表示将后面的标示符设置成毒药,一旦使用标示符,则产生错误或警告信息

      //毒药的设置
      #include <stdio.h>
      //#define GOTO goto
      //将goto设置为毒药
      #pragma GCC poison goto  
      int main(void){	
      	//GOTO ok;	
      	goto ok;		
      	printf("main函数开始\n");	
      ok:	printf("main函数结束\n");	
      	return 0;
      }
      /*
      输出结果:
      错误: 试图使用有毒的“goto”  
      */
      
  • #pragma pack(n)

    • 表示按照整数n倍进行补齐和对齐

      //设置结构体的对齐和补齐方式
      #include <stdio.h> 
      //设置结构体按照2的整数倍进行对齐补齐
      #pragma pack(2) 	//8
      //#pragma pack(1) 	//6 
      //#pragma pack(3) 	//error 
      //char short int long int  long long 
      int main(void){	
      	struct S{		
      		char c1;		
      		int i;		
      		char c2;	
      	};	
      	printf("sizeof(struct S) = %d\n",sizeof(struct S));		//12	
      	return 0;
      }
      /*
      输出结果:
      sizeof(struct S) = 8
      */
      
  • #pragma message(“string”)

    • message 参数: message 参数是我最喜欢的一个参数,它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常重要的。其使用方法为:

      #pragma message(“消息文本”)
      
    • 当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。假设我们希望判断自己有没有在源代码的什么地方定义了_X86 这个宏可以用下面的方法.

      #define _X86
      #ifdef _X86
      #pragma message ("_X86 macro activated!")
      #endif
      /*
      输出结果:
      附注: #pragma message:_X86 macro activated!
      */
      
  • #pragma code_seg

    • 另一个使用得比较多的 pragma 参数是 code_seg,它能够设置程序中函数代码存放的代码段,格式如:#pragma code_seg( ["section-name"[,"section-class"] ] ) 当我们开发驱动程序的时候就会使用到它。
  • #pragma once

    • 比较常用,只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在Visual C++6.0 中就已经有了,但是考虑到兼容性并没有太多的使用它。
  • #pragma hdrstop

    • 表示预编译头文件到此为止,后面的头文件不进行预编译。 BCB 可以预编译头文件以加快链接的速度,但如果所有头文件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。
    • 有时单元之间有依赖关系,比如单元 A 依赖单元 B,所以单元 B 要先于单元 A 编译。你可以用#pragma startup 指定编译优先级,如果使用了#pragma package(smart_init) , BCB就会根据优先级的大小先后编译。
  • #pragma resource

    • *.dfm表示把*.dfm 文件中的资源加入工程。 *.dfm 中包括窗体外观的定义。
  • #pragma warning

    • #pragma warning( disable : 4507 34; once : 4385; error : 164 ) 等价于:
      #pragma warning(disable:4507 34) 	// 不显示 4507 和 34 号警告信息  
      #pragma warning(once:4385) 			// 4385 号警告信息仅报告一次  
      #pragma warning(error:164) 			// 把 164 号警告信息作为一个错误。  
      
    • 同时这个 pragma warning 也支持如下格式:
      #pragma warning( push [ ,n ] )  	//n 代表一个警告等级(1-4)
      #pragma warning( pop )  
      #pragma warning( push )		//保存所有警告信息的现有的警告状态。  
      #pragma warning( push, n)	//保存所有警告信息的现有的警告状态,并且把全局警告等级设定为 n。  
      #pragma warning( pop )		//向栈中弹出最后一个警告信息,在入栈和出栈之间所作的一切改动取消。例如:  
      #pragma warning( push )  
      #pragma warning( disable : 4705 )  
      #pragma warning( disable : 4706 )  
      #pragma warning( disable : 4707 )  
      //.......  
      #pragma warning( pop )  
      在这段代码的最后,重新保存所有的警告信息(包括 470547064707)
  • #pragma comment()

    • 将一个注释放入一个对象文件或可执行文件中。
    • 常用的 lib 关键字,可以帮我们连入一个库文件。 比如:#pragma comment(lib, "user32.lib") 将 user32.lib 库文件加入到本工程中。
    • linker:将一个链接选项放入目标文件中,你可以使用这个指令来代替由命令行传入的或者在开发环境中设置的链接选项,可以指定/include 选项来强制包含某个对象,例如: #pragma comment(linker, "/include:__mySymbol")

6.3 string.h和math.h

字符串处理函数库

  • 在ubuntu系统中,在/usr/include中字符串函数有:
    • memcpy(dest, src):内存字符串复制,它直接将源空间内容复制到目标内存空间,所以使用它的前提是确定src和dst不会overlap重复,执行效率高。
    • memmove(dest, src):内存字符串复制,它先将源空间内容复制到一个临时内存空间,然后再复制到目标空间。
    • memset()
    • strncmp()
    • memcmp()
    • strdup()
    • memchr()
    • strndup()
    • strcpy()
    • strchr()
    • strncpy()
    • strstr()
    • strcat()
    • strtok()
    • strncat()
    • strcmp()

数学函数库

  • 在编译时需要加-lm告诉链接器到libm.so中去查找用到的函数。
  • 在链接时可以用-lxxx来指示链接器去到libxxx.so中去查找这个函数。

6.4 自制链接库

6.4.1 制作静态链接库

静态链接库就是商业公司将自己的函数库源代码经过只编译不连接形成.o的目标文件,然后用ar工具将.o文件归档成.a的归档文件(.a的归档文件又叫静态链接库文件)。商业公司通过发布.a库文件和.h头文件来提供静态库给客户使用;客户拿到.a和.h文件后,通过.h头文件得知库中的库函数的原型,然后在自己的.c文件中直接调用这些库文件,在连接的时候链接器会去.a文件中拿出被调用的那个函数的编译后的.o二进制代码段,链接进去形成最终的可执行程序。

  1. 使用gcc -c只编译不连接,生成.o文件;
  2. 使用ar工具进行打包成.a归档文件。
    • 库名不能随便乱起,一般是"lib+库名",后缀名是.a表示是一个归档文件
    • 制作完静态库后,发布时需要发布.a文件和.h文件。

6.4.2 使用静态链接库

  1. .a.h都放在自己引用的文件夹下;
  2. .c文件中包含库的.h
  3. 此时就可以引用静态库里面的函数啦。
  • 备注
    • 命名.a文件时,前缀一定要加lib,如libzf.a
    • 链接属性-l(小L)后跟库名(不要lib和.a)
    • 属性-L表示库的路径
    • 属性-I表示库的头文件路径
    • 例如:gcc cdw.c -o cdw -lzf -L./include -I(大i)./include

6.4.3 自制动态链接库

动态链接库本身不将库函数的代码段链接入可执行程序,只是做个标记。当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。也就是在运行时,会把库函数代码放入内存中,然后多个程序要用到库函数时,就从这段内存去找,而静态链接对于多程序就是重复使用库函数,比较占内存。

  • 动态链接库的后缀名是.so(对应windows系统中的.dll),静态库的扩展名是.a
  • gcc aston.c -o aston.o -c -fPIC-fPIC表示设置位置无关码。
  • gcc -o libaston.so aston.o -shared-shared表示使用共享库
  • 发布库时,发布libxxx.so和xxx.h。

6.4.4 使用动态链接库

  • gcc cdw.c -o cdw -lmax.so -L./动态链接库路径 -I./动态链接库头文件路径

上述命令虽然可以编译成功,但是在执行时会报错。原因是采用动态链接时,只是在可执行文件中做了一个标记(标记是使用了哪个函数库的哪个函数),但并没有将库函数加载到源文件中,所以可执行文件很小。但在执行时,需要立即从系统里面找到使用到的函数库,然后加载到内存中,在linux系统中会先从环境变量的路径,然后再从系统默认的路径/usr/lib中寻找,所以我们可以用两种办法解决运行的问题:
一是:将动态库libmax.so复制到/usr/lib下面,但是如果以后所有的库都这样放的话,会越来越臃肿,导致运行速度变慢(系统会一个一个查找)
二是新添加一个环境变量:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/share/include,然后将库libmax.so复制到这个路径下面就可以了。

6.4.5 知识拓展

  • 头文件用双引号""括起来表示该头文件从自定义目录下找。如果没加路径属性,默认当前路径找,如果在其他文件夹下,必须用-I(大i)路径。
  • 用尖括号<>括起来表示该头文件从编译器的默认库目录下找,或者是自定义的库目录下找,但是要明确其路径。
  • nm ./include/libmax.a:可以查看max库中有哪些.o文件,各自含有哪些函数。
  • 使用ldd命令判断可执行文件是否能运行成功
    ldd cdw
    linux-gate.so.1=>(0xb77a8000)
    libmax.so=>notfound			#notfound意思就是没有找到对应的函数库
    libc.so.6=>/lib/i386-linux-gnu/libc.so.6(0xb75e2000)
    /lib/ld-linux.so.2(0xb77a9000)
    
  • gcc中,编译链接程序默认是使用动态库的,要想静态链接需要显式-static来强制静态链接。
  • 库函数的使用需要注意4点:
    1. 包含相应的头文件;
    2. 调用库函数时注意函数原型;
    3. 有些库函数链接时需要额外用-lxxx来指定链接;
    4. 如果是动态库,要注意-L指定动态库的地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Leon_George

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值