概述
预处理名称 | 意 义 |
---|---|
#define | 宏定义 |
#undef | 撤销已定义过的宏名 |
#include | 使编译程序将另一源文件嵌入到带有#include 的源文件中 |
#if | 如果条件为真,则执行相应操作 |
#elif | 如果前面条件为假,而该条件为真,则执行相应操作 |
#else | 如果前面条件均为假,则执行相应操作 |
#endif | 结束相应的条件编译指令 |
#ifdef | 如果该宏已定义,则执行相应操作 |
#ifndef | 如果该宏没有定义,则执行相应操作 |
#ifdef | 用#ifdef 与#ifndef 命令分别表示“如果有定义”及“如果无定义”,是条件编译的另一种方法。 |
#ifndef | |
#line | 改变当前行数和文件名称,它们是在编译程序中预先定义的标识符命令的基本形式如下:#line number[“filename”] |
#error | 编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译 |
#pragma | 为实现时定义的命令,它允许向编译程序传送各种指令例如,编译程序可能有一种选择,它支持对程序执行的跟踪。可用#pragma 语句指定一个跟踪选择。 |
另外 ANSI 标准 C 还定义了如下几个宏:
预处理名称 | 意 义 |
---|---|
LINE | 表示正在编译的文件的行号 |
FILE | 表示正在编译的文件的名字 |
DATE | 表示编译时刻的日期字符串,例如: “25 Dec 2007” |
TIME | 表示编译时刻的时间字符串,例如: “12:30:55” |
STDC | 判断该文件是不是定义成标准 C 程序, |
NULL: | 表示空指针常量。 |
EOF: | 表示文件结束符。 |
true 和 false: | 表示布尔类型的真值和假值,C99引入了 <stdbool.h> 头文件来定义这两个符号。 |
宏定义(#define、#under)
用#define定义数值常量:
#define PI 3.141592654
…
// code
#undef PI //后面代码不能使用 PI 了
用#define定义字符串常量
#define ENG_PATH_1 E:\English\listen_to_this\listen_to_this_3
#define ENG_PATH_2 "E:\English\listen_to_this\listen_to_this_3"
ENG_PATH_1 定义时没使用双引号,在使用时就需要再加双引号,写成 "ENG_PATH_1" ,否则会报错;
ENG_PATH_2 定义时使用了双引号,可以直接使用ENG_PATH_2去表示字符串
用 #define 宏定义表达式
#define SEC_A_YEAR 60*60*24*365 // 这个可能会溢出
#define SEC_A_YEAR (60*60*24*365)UL
条件编译(#if、#else、#elif、#endif)
1
// 如果标识符已被 #define 命令定义过则对程序段 1 进行编译,否则对程序段 2进行编译。
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
2
// 如果标识符未被#define 命令定义过则对程序段 1 进行编译,否则对程序段 2 进行编译。
#ifndef 标识符
程序段 1
#else
程序段 2
#endif
3
// 如常量表达式的值为真(非 0),则对程序段 1 进行编译,否则对程序段 2 进行编译。
#if 常量表达式
程序段 1
#else
程序段 2
#endif
4
// 替换第一种方式
#if defined(标识符)
程序段 1
#else
程序段 2
#endif
5
// 替换第二种方式
#if !defined(标识符)
程序段 1
#else
程序段 2
#endif
6
// 其他方式
#if (defined(__SERVER) && (!defined(__CLIENT)))
程序段
#endif
7
// 与 #elif 一起使用
#if defined(_SS528V100) || defined(_SS626V100)
程序段 1
#elif defined(_SS928V100)
程序段 2
#endif
文件包含(#include)
#include <filename>
error 预处理
#if _SS528V100
程序段 1
#elif _SS626V100
程序段 2
#else
#error "没有相应宏处理"
#endif
#define(宏定义)----由预处理器来处理
typedef----在编译阶段由编译器处理
typedef:
如果放在所有函数之外,它的作用域就是从它定义开始直到文件尾
如果放在某个函数内,它的作用域就是从它定义开始直到该函数结尾
#define:
不管是在某个函数内,还是在所有函数之外,作用域都是从定义开始直到整个文件结尾 (不管是typedef还是define,其作用域都不会扩展到别的文件,即使是同一个程序的不同文件,也不能互相使用)
在写switch语句经常忘记写break 我们就可以使用宏定义把case break缩写成case在预处理阶段自动帮我们加break语句
#include<stdio.h>
#define CASE break;case
int main()
{
int x = 0;
scanf("%d", &x);
switch (x)
{
CASE 1:
printf("xxx");
CASE 2:
printf("yyyy");
CASE 3:
printf("zzz");
}
return 0;
}
define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首被替换。
2.替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复述处理过程。
注意:
1.宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
作用
“#”:它是用于将宏参数转换为字符串的预处理器运算符
#define STRINGIFY(x) #x
printf("%s", STRINGIFY(Hello)); // 打印 "Hello"
#define PRINT(n,format) printf("the value of "#n" is "format"\n",n)
int main() {
int a = 10;
printf("the value of a is %d\n", a);
PRINT(a,"%d");
int b = 15;
printf("the value of b is %d\n", b);
PRINT(b,"%d");
float f = 4.5f;
printf("the value of f is %f\n", f);
PRINT(f,"%f");
return 0;
}
##在宏定义的使用
#define unio(x,y) x##y
printf("%d\n",unio(3,4))
--->34
#define CAT(x,y) x##y
int main() {
int ab = 2024;
printf("%d", CAT(a, b)); //打印2024
}
#define PI 3.14159 // 定义一个常量宏
#undef PI // 取消定义宏
ifdef指令用于判断某个宏是否已定义,若已定义则编译 #ifdef 和 #endif 之间的代码。
#ifdef DEBUG
printf("Debug mode enabled.");
#endif
if !defined指令用于判断某个宏是否没有定义,与 #ifndef 相同的效果。
#if !defined(DEBUG)
printf("Not in debug mode. ");
#endif
可变参数的使用
int func(int arg1, …);
省略号…表示可变参数列表,需要注意的是:如果你想使用可变参数列表,则至少有一个固定参数
可变参数的提取
对于可变参数列表,我们最关心的还是怎么将可变参数提取出来,关于可变参数的提取主要依赖一个类型和四个宏函数:va_list,va_start,va_arg,va_copy,va_end,而这些类型和宏函数在C语言的头文件stdarg.h中
- 类型va_list本质是一个char*类型,我们要使用可变参数列表,必须首先定义一个va_list类型的变量
va_list ap;
- 给va_list类型的变量初始化的函数是va_start函数,初始化以后va_list类型的变量指向第一个可变参数的首地址,该函数的函数原型如下
void va_start(va_list ap, last);
ap: 这是一个 va_list 类型的对象。
last 是最后一个传递给函数的已知的固定参数,即省略号之前的参数
- 用来提取可变参数列表中的参数的函数是va_arg,使用一次提取一个,每次提取的参数是直接返回的并且该函数提取的同时会自动将ap指向下一个参数。
type va_arg(va_list ap, type);
ap: 这是一个 va_list 类型的对象。
type:要提取的参数的类型,如int , double,如果当前参数类型和type不统一,就会发生不可预知的错误
- 这个函数不是必须使用的函数,这时一个拷贝函数,初始化dest作为src(当前状态)的副本。
void va_copy(va_list dest, va_list src);
dest: 要作为副本的对象。
src: 原始值
- 销毁va_list类型变量的函数是va_end,其本质就是将指针置为NULL。
void va_end(va_list ap);
ap: 要销毁的变量。
#include <stdio.h>
#include <stdarg.h>
void PrintArg(int num, ...)
{
va_list ap;
// 1.进行初始化
va_start(ap, num);
for (int i = 0; i < num; i++)
{
// 不断取出可变参数
int a = va_arg(ap, int);
printf("%d ", a);
}
// 销毁
va_end(ap);
}
int main()
{
PrintArg(4, 1, 3, 4, 5);
return 0;
}
函数通过固定参数指定可变参数个数,打印所有变参值
#include <stdarg.h>
#include <stdio.h>
void parse_valist_by_num(int arg_cnt, ...);
int main(void)
{
parse_valist_by_num(4,1,2,3,4);
parse_valist_by_num(4,1,2,3);
parse_valist_by_num(4,1,2,3,4,5); //多余的变参被忽略
}
//第一个参数定义可变参数的个数
void parse_valist_by_num(int arg_cnt, ...)
{
va_list p_args;
va_start(p_args, arg_cnt);
int idx;
int val;
for(idx = 1; idx <= arg_cnt; ++idx){
val = va_arg(p_args, int);
printf("第 %d 个参数: %d\n", idx, val);
}
printf("---------------\n");
va_end(p_args);
}
函数定义一个结束标记(-1),调用时通过最后一个参数传递该标记,打印标记前所有变参值
#include <stdarg.h>
#include <stdio.h>
void parse_valist_by_flag(int num_1, ...);
int main(void)
{
parse_valist_by_flag(1,-1);
parse_valist_by_flag(1,2,3,5,-1);
parse_valist_by_flag(-1);
}
//函数定义一个结束标记(-1),调用时通过最后一个参数传递该标记,以结束变参的遍历打印。
//最后一个参数作为变参结束符(-1),用于循环获取变参内容
void parse_valist_by_flag(int num_1, ...)
{
va_list p_args;
va_start(p_args, num_1);
int idx = 0;
int val = num_1;
while(val != -1){
++idx;
printf("第 %d 个参数: %d\n", idx, val);
val = va_arg(p_args, int); //得到下个变参值
}
va_end(p_args);
printf("---------------\n");
}
C语言中可变参数的用法
我们在C语言编程中会遇到一些参数个数可变的函数,例如printf()
这个函数,它的定义是这样的:
int printf( const char* format, …);
它除了有一个参数format固定以外,后面跟的参数的个数和类型是
可变的,例如我们可以有以下不同的调用方法:
printf(“%d”,i);
printf(“%s”,s);
printf(“the number is %d ,string is:%s”, i, s);
究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实
现的呢?本文就这个问题进行一些探讨,希望能对大家有些帮助.会C++的
网友知道这些问题在C++里不存在,因为C++具有多态性.但C++是C的一个
超集,以下的技术也可以用于C++的程序中.限于本人的水平,文中如果有
不当之处,请大家指正.
写一个简单的可变参数的C函数
下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的
C函数要在程序中用到以下这些宏:
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
va在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个
头文件.下面我们写一个简单的可变参数的函数,该函数至少有一个整数
参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;
va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d/n", i, j);
return;
}
我们可以在我们的头文件中这样声明我们的函数:
extern void simple_va_fun(int i, …);
我们在程序中可以这样调用:
simple_va_fun(100);
simple_va_fun(100,200);
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变
量是指向参数的指针.
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第
一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个
参数是你要返回的参数的类型,这里是int型.
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使
用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获
取各个参数.
如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:
1)simple_va_fun(100);
结果是:100 -123456789(会变的值)
2)simple_va_fun(100,200);
结果是:100 200
3)simple_va_fun(100,200,300);
结果是:100 200
我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果
正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果
的原因和可变参数在编译器中是如何处理的.
可变参数在编译器中的处理
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,
由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下
面以VC++中stdarg.h里x86平台的宏定义摘录如下(’/’号表示折行):
typedef char * va_list;
#define _INTSIZEOF(n) /
((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) /
( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函
数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我
们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再
看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的
地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆
栈的地址,如图:
高地址 |
---|
函数返回地址 |
第n个参数(第一个可变参数) |
<–va_start后ap指向 |
第n-1个参数(最后一个固定参数) |
低地址<-- &v |
图( 1 )
然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我
们看一下va_arg取int型的返回值:
j= ( (int)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回
ap-sizeof(int)的int指针,这正是第一个可变参数在堆栈里的地址
(图2).然后用取得这个地址的内容(参数值)赋给j.
高地址 |
---|
函数返回地址 |
<–va_arg后ap指向 |
第n个参数(第一个可变参数) |
<–va_start后ap指向 |
第n-1个参数(最后一个固定参数) |
低地址<-- &v |
图( 2 )
最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再
指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不
会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.
在这里大家要注意一个问题:由于参数的地址用于va_start宏,所
以参数不能声明为寄存器变量或作为函数或数组类型.
关于va_start, va_arg, va_end的描述就是这些了,我们要注意的
是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
可变参数在编程中要注意的问题
因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,
可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能
地识别不同参数的个数和类型.
有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数
printf是从固定参数format字符串来分析出参数的类型,再调用va_arg
的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通
过在自己的程序里作判断来实现的.
另外有一个问题,因为编译器对可变参数的函数的原型检查不够严
格,对编程查错不利.如果simple_va_fun()改为:
void simple_va_fun(int i, …)
{
va_list arg_ptr;
char *s=NULL;
va_start(arg_ptr, i);
s=va_arg(arg_ptr, char*);
va_end(arg_ptr);
printf(“%d %s/n”, i, s);
return;
}
可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现
core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出
错,但错误却是难以发现,不利于我们写出高质量的程序.
以下提一下va系列宏的兼容性.
System V Unix把va_start定义为只有一个参数的宏:
va_start(va_list arg_ptr);
而ANSI C则定义为:
va_start(va_list arg_ptr, prev_param);
如果我们要用system V的定义,应该用vararg.h头文件中所定义的
宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以
用ANSI C的定义就够了,也便于程序的移植.