c语言预处理和可变参数的使用

概述

预处理名称意 义
#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的定义就够了,也便于程序的移植.

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

weixin_44245323

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

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

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

打赏作者

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

抵扣说明:

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

余额充值