预处理器

C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。

文中代码皆在文末github地址上面。

所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:

指令

描述

#define

定义宏

#include

包含一个源代码文件

#undef

取消已定义的宏

#ifdef

如果宏已经定义,则返回真

#ifndef

如果宏没有定义,则返回真

#if

如果给定条件为真,则编译下面代码

#else

#if 的替代方案

#elif

如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码

#endif

结束一个 #if……#else 条件编译块

#error

当遇到标准错误时,输出错误消息

#pragma

使用标准化方法,向编译器发布特殊的命令到编译器中

预处理器实例

分析下面的实例来理解不同的指令。

#define MAX_ARRAY_LENGTH 20

这个指令告诉 CPP 把所有的 MAX_ARRAY_LENGTH 替换为 20。使用 #define 定义常量来增强可读性。

#include

#include "myheader.h"

这些指令告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。

#undef FILE_SIZE

#define FILE_SIZE 42

这个指令告诉 CPP 取消已定义的 FILE_SIZE,并定义它为 42。

#ifndef MESSAGE

#define MESSAGE "You wish!"

#endif

这个指令告诉 CPP 只有当 MESSAGE 未定义时,才定义 MESSAGE。

#ifdef DEBUG

/* Your debugging statements here */

#endif

这个指令告诉 CPP 如果定义了 DEBUG,则执行处理语句。在编译时,如果您向 gcc 编译器传递了 -DDEBUG 开关量,这个指令就非常有用。它定义了 DEBUG,您可以在编译期间随时开启或关闭调试。

预定义宏

ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。

描述

DATE

当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。

TIME

当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。

FILE

这会包含当前文件名,一个字符串常量。

LINE

这会包含当前行号,一个十进制常量。

STDC

当编译器以 ANSI 标准编译时,则定义为 1。

让我们来尝试下面的实例:

void macros_predefined() {

printf("File :%s\n", __FILE__);

printf("Date :%s\n", __DATE__);

printf("Time :%s\n", __TIME__);

printf("Line :%d\n", __LINE__);

printf("ANSI :%d\n", __STDC__);

}

File :E:\EclipseWorkspace\DailyCode\CCodes\com.ming.code\support.c

Date :Dec 14 2018

Time :11:28:35

Line :626

ANSI :1

预处理器运算符

C 预处理器提供了下列的运算符来帮助您创建宏:

宏延续运算符(\)

一个宏通常写在一个单行上。但是如果宏太长,一个单行容纳不下,则使用宏延续运算符(\)。例如:

#define message_for(a, b) \

printf(#a " and " #b ": We love you!\n")

标记粘贴运算符(##)

宏定义内的标记粘贴运算符(##)会合并两个参数。它允许在宏定义中两个独立的标记被合并为一个标记。例如:

#include

#define tokenpaster(n) printf ("token" #n " = %d", token##n)

int main(void)

{

int token34 = 40;

tokenpaster(34);

return 0;

}

当上面的代码被编译和执行时,它会产生下列结果:

token34 = 40

这是怎么发生的,因为这个实例会从编译器产生下列的实际输出:

printf ("token34 = %d", token34);

这个实例演示了 token##n 会连接到 token34 中,在这里,我们使用了字符串常量化运算符(#)和标记粘贴运算符(##)。

defined() 运算符

预处理器 defined 运算符是用在常量表达式中的,用来确定一个标识符是否已经使用 #define 定义过。如果指定的标识符已定义,则值为真(非零)。如果指定的标识符未定义,则值为假(零)。下面的实例演示了 defined() 运算符的用法:

#if !defined (MESSAGE)

#define MESSAGE "You wish!"

#endif

Here is the message: You wish!

参数化的宏

CPP 一个强大的功能是可以使用参数化的宏来模拟函数。例如,下面的代码是计算一个数的平方:

int square(int x) {

return x * x;

}

我们可以使用宏重写上面的代码,如下:

#define square(x) ((x) * (x))

在使用带有参数的宏之前,必须使用 #define 指令定义。参数列表是括在圆括号内,且必须紧跟在宏名称的后边。宏名称和左圆括号之间不允许有空格。例如:

#define MAX(x,y) ((x) > (y) ? (x) : (y))

使用#define含参时,参数括号很重要,如上例中省略括号会导致运算错误:

#include

#define square(x) ((x) * (x))

#define square_1(x) (x * x)

int main(void)

{

printf("square 5+4 is %d\n", square(5+4));

printf("square_1 5+4 is %d\n", square_1(5+4));

return 0;

}

输出结果为:

square 5+4 is 81

square_1 5+4 is 29

原因:

square 等价于 (5+4)*(5+4)=81

square_1 等价于 5+4*5+4=29

头文件

头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享。有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。

在程序中要使用头文件,需要使用 C 预处理指令 #include 来引用它。前面我们已经看过 stdio.h 头文件,它是编译器自带的头文件。

引用头文件相当于复制头文件的内容,但是我们不会直接在源文件中复制头文件的内容,因为这么做很容易出错,特别在程序是由多个源文件组成的时候。

A simple practice in C 或 C++ 程序中,建议把所有的常量、宏、系统全局变量和函数原型写在头文件中,在需要的时候随时引用这些头文件。

引用头文件的语法

使用预处理指令 #include 可以引用用户和系统头文件。它的形式有以下两种:

#include

这种形式用于引用系统头文件。它在系统目录的标准列表中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

#include "file"

这种形式用于引用用户头文件。它在包含当前文件的目录中搜索名为 file 的文件。在编译源代码时,您可以通过 -I 选项把目录前置在该列表前。

引用头文件的操作

#include 指令会指示 C 预处理器浏览指定的文件作为输入。预处理器的输出包含了已经生成的输出,被引用文件生成的输出以及 #include 指令之后的文本输出。例如,如果您有一个头文件 operatorhead.h,如下:

void hello(void) {

printf("hello world\n");

}

和一个使用了头文件的主程序 main.c,如下:

#include "operatorhead.h"

int main (void)

{

hello ();

}

只引用一次头文件

如果一个头文件被引用两次,编译器会处理两次头文件的内容,这将产生错误。为了防止这种情况,标准的做法是把文件的整个内容放在条件编译语句中,如下:

#ifndef HEADER_FILE

#define HEADER_FILE

#include "operatorhead.h"

#endif

这种结构就是通常所说的包装器 #ifndef。当再次引用头文件时,条件为假,因为 HEADER_FILE 已定义。此时,预处理器会跳过文件的整个内容,编译器会忽略它。

有条件引用

有时需要从多个不同的头文件中选择一个引用到程序中。例如,需要指定在不同的操作系统上使用的配置参数。您可以通过一系列条件来实现这点,如下:

#if SYSTEM_1

# include "system_1.h"

#elif SYSTEM_2

# include "system_2.h"

#elif SYSTEM_3

...

#endif

但是如果头文件比较多的时候,这么做是很不妥当的,预处理器使用宏来定义头文件的名称。这就是所谓的有条件引用。它不是用头文件的名称作为 #include 的直接参数,您只需要使用宏名称代替即可:

#define SYSTEM_H "system_1.h"

...

#include SYSTEM_H

SYSTEM_H 会扩展,预处理器会查找 system_1.h,就像 #include 最初编写的那样。SYSTEM_H 可通过 -D 选项被您的 Makefile 定义。

在有多个 .h 文件和多个 .c 文件的时候,往往我们会用一个 global.h 的头文件来包括所有的 .h 文件,然后在除 global.h 文件外的头文件中 包含 global.h 就可以实现所有头文件的包含,同时不会乱。方便在各个文件里面调用其他文件的函数或者变量。

#ifndef _GLOBAL_H

#define _GLOBAL_H

#include

#include

#include

#include

强制类型转换

强制类型转换是把变量从一种类型转换为另一种数据类型。例如,如果您想存储一个 long 类型的值到一个简单的整型中,您需要把 long 类型强制转换为 int 类型。您可以使用强制类型转换运算符来把值显式地从一种类型转换为另一种类型,如下所示:

(type_name) expression

请看下面的实例,使用强制类型转换运算符把一个整数变量除以另一个整数变量,得到一个浮点数:

当上面的代码被编译和执行时,它会产生下列结果:

void convert_data() {

int sum = 17, count = 5;

double mean;

mean = (double) sum / count;

printf("Value of mean : %f\n", mean);

}

Value of mean : 3.400000

这里要注意的是强制类型转换运算符的优先级大于除法,因此 sum 的值首先被转换为 double 型,然后除以 count,得到一个类型为 double 的值。

类型转换可以是隐式的,由编译器自动执行,也可以是显式的,通过使用强制类型转换运算符来指定。在编程时,有需要类型转换的时候都用上强制类型转换运算符,是一种良好的编程习惯。

整数提升

整数提升是指把小于 int 或 unsigned int 的整数类型转换为 int 或 unsigned int 的过程。请看下面的实例,在 int 中添加一个字符:

void convert_data() {

int sum = 17, count = 5;

double mean;

mean = (double) sum / count;

printf("Value of mean : %f\n", mean);

int i = 17;

char c = 'c'; /* ascii 值是 99 */

int sume;

sume = i + c;

printf("Value of sume : %d\n", sume );

}

当上面的代码被编译和执行时,它会产生下列结果:

Value of sum : 116

在这里,sum 的值为 116,因为编译器进行了整数提升,在执行实际加法运算时,把 'c' 的值转换为对应的 ascii 值。

常用的算术转换

常用的算术转换是隐式地把值强制转换为相同的类型。编译器首先执行整数提升,如果操作数类型不同,则它们会被转换为下列层次中出现的最高层次的类型:

image

常用的算术转换不适用于赋值运算符、逻辑运算符 && 和 ||。让我们看看下面的实例来理解这个概念:

void convert_data() {

int sum = 17, count = 5;

double mean;

mean = (double) sum / count;

printf("Value of mean : %f\n", mean);

int i = 17;

char c = 'c'; /* ascii 值是 99 */

int sume;

sume = i + c;

printf("Value of sume : %d\n", sume );

int j = 17;

char k = 'c'; /* ascii 值是 99 */

float sumes;

sumes = j + k;

printf("Value of sumes : %f\n", sumes );

}

当上面的代码被编译和执行时,它会产生下列结果:

Value of sum : 116.000000

在这里,c 首先被转换为整数,但是由于最后的值是 double 型的,所以会应用常用的算术转换,编译器会把 i 和 c 转换为浮点型,并把它们相加得到一个浮点数。

如果一个运算符两边的运算数类型不同,先要将其转换为相同的类型,即较低类型转换为较高类型,然后再参加运算,转换规则如下图所示。

img

错误处理

C 语言不提供对错误处理的直接支持,但是作为一种系统编程语言,它以返回值的形式允许您访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。您可以在 errno.h 头文件中找到各种各样的错误代码。

所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0,这是一种良好的编程习惯。0 值表示程序中没有错误。

errno、perror() 和 strerror()

C 语言提供了 perror() 和 strerror() 函数来显示与 errno 相关的文本消息。

perror() 函数显示您传给它的字符串,后跟一个冒号、一个空格和当前 errno 值的文本表示形式。

strerror() 函数,返回一个指针,指针指向当前 errno 值的文本表示形式。

让我们来模拟一种错误情况,尝试打开一个不存在的文件。您可以使用多种方式来输出错误消息,在这里我们使用函数来演示用法。另外有一点需要注意,您应该使用 stderr 文件流来输出所有的错误。

//

// Created by Lenovo on 2018/12/14.

//

#include

#include

#include

#include

#include

extern int errno;

void error_deal() {

FILE *pf;

int errnum = 0;

pf = fopen("unexist.txt", "rb");

if (pf == NULL) {

errnum = errno;

fprintf(stderr, "error num : %d\n", errno);

perror("perror output errors");

fprintf(stderr, "file open error: %s\n", strerror(errnum));

} else {

fclose(pf);

}

}

error num : 2

perror output errors: No such file or directory

file open error: No such file or directory

被零除的错误

在进行除法运算时,如果不检查除数是否为零,则会导致一个运行时错误。

为了避免这种情况发生,下面的代码在进行除法运算前会先检查除数是否为零:

#include

#include

main()

{

int dividend = 20;

int divisor = 0;

int quotient;

if( divisor == 0){

fprintf(stderr, "除数为 0 退出运行...\n");

exit(-1);

}

quotient = dividend / divisor;

fprintf(stderr, "quotient 变量的值为 : %d\n", quotient );

exit(0);

程序退出状态

通常情况下,程序成功执行完一个操作正常退出的时候会带有值 EXIT_SUCCESS。在这里,EXIT_SUCCESS 是宏,它被定义为 0。

如果程序中存在一种错误情况,当您退出程序时,会带有状态值 EXIT_FAILURE,被定义为 -1。所以,上面的程序可以写成:

exit(EXIT_SUCCESS);

exit(EXIT_FAILURE);

c语言字符串强制类型转换,C语言学习九 —头文件强制类型转换错误处理相关推荐

  1. c语言 字符串 枚举类型,C语言入门 — 枚举类型

    1.C语言入门 - 枚举类型,枚举类型的关键字是enum, enum是用来定义一组整型数值,其实定义模型如下: enum { 常数名称1 = 0, //起始值取0,这里的值可以为0,正数,负数. 常数 ...

  2. c语言字符串文库总结,C语言字符串.ppt

    C语言字符串.ppt ACM程序设计,福州大学至诚学院 冯新,第四讲,字符串处理,常用函数介绍,复制,char* strcpy char *s1, const char *s2; 将字符串s2复制到s ...

  3. C语言再学习 -- 常用头文件和函数(转)

    参看:C/C++常用头文件及函数汇总 linux常用头文件如下: POSIX标准定义的头文件 <dirent.h>        目录项 <fcntl.h>         文 ...

  4. c语言字符串型函数是,C语言字符/字符串相关函数收藏大全

    字符处理函数 int tolower(char ch)若ch是大写字母('A'-'Z')返回相应的小写字母('a'-'z') int toupper(char ch)若ch是小写字母('a'-'z') ...

  5. C语言semaphore头文件,C语言再学习 -- 常用头文件和函数

    Linux常用头文件如下: POSIX标准定义的头文件 < dirent.h>        目录项 < fcntl.h>         文件控制 < fnmatch. ...

  6. c语言字符串怎么退位,C语言第五六次作业.ppt

    C语言作业解析 第四弹 原来真正变态的是这两作业哇 原本是做完第五次作业等大家数分期中考完以后给大家 结果直接出了第六次 就顺便做了 买一送一还包邮哦亲 虽然这两次作业比较难 理解起来困难无比 不过考 ...

  7. c语言 字符串切片重组,C语言实现分割字符串

    背景 遇到一个将字符串分割场景.以前从没有用c语言实现,都是使用python的split()函数,python处理起来很简单. split()方法语法: str.split(str="&qu ...

  8. c语言 字符串切片重组,c语言 字符串的拼接和分割实例

    1.字符串的拼接 使用c的函数char *strcat(char *str_des, char *str_sou); 将字符串str_sou接在字符串str_des后面(放在str_des的最后字符和 ...

  9. C语言高级技巧-在Makefile中引用你的头文件

    在Makefile中添加头文 代码仓库:Makefile中添加头文件引用 我们常这样写C程序: #inlcude <stdio.h>int main(int argc, char *arg ...

最新文章

  1. 【Sql Server】数据库的3大服务
  2. DDD分层架构最佳实践
  3. 京东某女程序员求助:刚入职就意外怀孕,纠结还能不能过试用期?网友:职场女性太难!...
  4. [软件工程基础]结对项目 数独程序扩展
  5. 为踏实上进的【飞鸽传书】开发者而感动
  6. cuda stream
  7. 学习Haskell的一些资料
  8. 织梦根目录感染abc.php,织梦SEO优化:织梦dedecms根目录下robots.txt文件设置详解! - 张俊SEO...
  9. [加密]SSL/TLS原理详解
  10. Java---设计【高校教师信息管理系统】
  11. 计算机上没有保存任何数据源,Excel数据表找不到链接莫着急——三点操作重建数据的源文件-查看源文件...
  12. Unity中扫描二维码将电脑照片保存在手机中
  13. linux 电驴,开源电驴 MLDonkey 3.0.7 发布
  14. java 取磁盘阵列容量_硬盘阵列 Raid 的区别及容量计算方式
  15. 计算机一级插入页码,计算机一级WPS辅导:用WPSOffice2007插入特色页码
  16. vb3.0 升级vb6.0_将VB6升级到VB.NET(性能改进)
  17. 【面试题】闭包是什么?this 到底指向谁?
  18. Mentor_丝印检查——手工绘制丝印线条(标注)到丝印位号距离的检查
  19. 19级爪哇程序设计新手赛(题解)
  20. VMware虚拟机的安装,并编写简单的C程序

热门文章

  1. redux之reducer 为什么必须是纯函数?
  2. 牛客竞赛数据结构专题班树状数组、线段树练习题
  3. 打造属于自己的量化投资系统5——利用backtrader创建平滑异同移动平均线MACD策略
  4. 为什么公司里普遍存在内斗现象?
  5. ftp协议主动模式与被动模式
  6. 软raid5创建删除与配置
  7. Win10 - 隐藏此电脑中的文件夹
  8. Stackelberg博弈问题双层模型转化为MPEC模型的三种方法
  9. 做企业,就要做的象长虹,创新不止
  10. Verilog——双向IO口的FPGA实现