C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(下)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)

C语言进阶⑳(程序环境和预处理)(#define定义宏+编译+文件包含)(中):https://developer.aliyun.com/article/1513279

2.5.1条件编译之常量表达式

如果常量表达式为真,参加编译。反之如果为假,则不参加编译。

代码演示:常量表达式为真

 
#include <stdio.h>
int main()
{
#if 1//非0都是真,算式也行
    printf("Hello,World!\n");
#endif
    return 0;
}

代码演示:常量表达式为假

 
#include <stdio.h>
int main()
{
#if 0
    printf("Hello,World!\n");
#endif
    return 0;
}

当然也可以用宏替换,可以表示地更清楚:

 
#include <stdio.h>
#define PRINT 1
#define DONT_PINRT 0
int main() 
{
#if PRINT
    printf("Hello,World!\n");
#endif
    return 0;
}

2.5.2多分支的条件编译

介绍:多分支的条件编译,直到常量表达式为真时才执行。

代码演示:

 
#include <stdio.h>
int main()
{
#if 1 == 2 // 假
    printf("aaaa\n");
#elif 2 == 2 // 真
    printf("bbbb\n");
#else 
    printf("cccc\n")
#endif
        return 0;
}

2.5.3条件编译判断是否被定义

定义:ifdef 和 if defined() ,ifndef 和 if !defined() 效果是一样的,用来判断是否被定义。

代码演示:

 
#include <stdio.h>
#define TEST 0
// #define TEST2 // 不定义
int main() 
{
    /* 如果TEST定义了,下面参与编译 */
    // 1
#ifdef TEST
    printf("1\n");
#endif
    // 2
#if defined(TEST)
    printf("2\n");
#endif
 
    /* 如果TEST2不定义,下面参与编译 */
    // 1
#ifndef TEST2
    printf("3\n");
#endif
    // 2
#if !defined(TEST2)
    printf("4\n");
#endif
    return 0;
}

2.5.4条件编译的嵌套

if 语句一样,是可以嵌套的:

 
 #if defined(OS_UNIX)
    #ifdef OPTION1
        unix_version_option1();
    #endif
    #ifdef OPTION2
        unix_version_option2();
    #endif
#elif defined(OS_MSDOS)
    #ifdef OPTION2
        msdos_version_option2();
    #endif
#endif

2.6文件包含

我们已经知道,#include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。替换方式为,预处理器先删除这条指令,并用包含文件的内容替换。这样一个源文件被包含10次,那就实际被编译10次。

2.6.0头文件被包含的方式

< > 和 " " 包含头文件的本质区别:查找的策略的区别

① " " 的查找策略:先在源文件所在的目录下查找。如果该头文件未找到,

则在库函数的头文件目录下查找。(如果仍然找不到,就提示编译错误)

Linux环境 标准头文件的路径:

/usr/include

VS环境 标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

< > 的查找策略:直接去标准路径下去查找。(如果仍然找不到,就提示编译错误)

既然如此,那么对于库文件是否也可以使用 " " 包含?

当然可以。但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。

为了效率不建议这么做。

2.6.1嵌套文件的包含

头文件重复引入的情况:

comm.h 和 comm.c 是公共模块。

test1.h 和 test1.c 使用了公共模块。

test2.h 和 test2.c 使用了公共模块。

test.h 和 test.c 使用了 test1 模块和 test2 模块。

这样最终程序中就会出现两份 comm.h 的内容,这样就造成了文件内容的重复。


那么如何避免头文件的重复引入呢?

使用条件编译指令,每个头文件的开头写:

 
#ifndef __TEST_H__
#define __TEST_H__
// 头文件的内容
#endif

如果嫌麻烦,还有一种非常简单的方法:

1. 
2. #pragma once // 让头文件即使被包含多次,也只包含一份


笔试题:选自《高质量C/C++编程指南》

① 头文件中的 ifnde / define / endif 是干什么用的?

答:防止头文件被重复多次包含。

② #include <filename.h> 和 #include "filename.h" 有什么区别?

答:尖括号是包含库里面的头文件的,双引号是包含自定义头文件的。

它们在查找策略上不同,尖括号直接去库目录下查找。

而双引号是现去自定义的代码路径下查找,如果找不到头文件,则在库函数的头文件目录下查找。

注:还有其他预处理指令

 
#error
#pragma
#line
...
不做介绍,可以自己去了解。
#pragma pack()//在结构体部分介绍。

3. 笔试选择题

3.1环境

以下什么内容的作用是将源程序文件进行处理,生成一个中间文件,

编译系统将对此中间文件进行编译并生成目标代码。

A.编译预处理

B.汇编

C.生成安装文件

D.编译

解析:

题干中提到了“编译”,说明是编译的上一步,那自然是编译预处理。


3.2可执行程序的生成

由多个源文件组成的C程序,经过编辑、预处理、编译、链接等阶段会生成最终的可执行程序。

下面哪个阶段可以发现被调用的函数未定义?( )

A.预处理

B.编译

C.链接

D.执行

解析:

预处理只会处理#开头的语句,编译阶段只校验语法,链接时才会去找实体,

所以是链接时出错的,故选C。这里附上每个步骤的具体操作方式:

预处理: 相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件

(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),

没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。


编译: 将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,

产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,

不负责寻找实体。


链接: 通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。 链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。

3.3定义变量

test.c文件中包括如下语句:

 
#define INT_PTR int*
typedef int*int_ptr;
INT_PTR a,b;
int_ptr c,d;

文件中定义的四个变量,哪个变量不是指针类型?( )

A.a

B.b

C.c

D.d

解析:

预处理的#define是查找替换,所以替换过后的语句是“int*a,b;”,

其中b只是一个int变量,如果要让b也是指针,必须写成“int *a, *b;”。

而typedef没有这个问题,c、d都是指针。故选B。


3.4预处理指令

下面哪个不是预处理指令:( )

A.#define

B.#if

C.#undef

D.#end

解析:

#define执行查找替换,#if可以区分是否编译,#undef可以反定义,

也就是取消#define宏定义的东西,#end并没有这个东西,只有#endif。


3.5预定义符号

下面哪个不是预定义符号?( )

A.__FILE__

B.__TIME__

C.__DATE__

D.__MAIN__

解析:

前三个是常用宏,分别是:打印所在文件、打印编译时间、打印编译日期。

除此之外,还有__LINE__(行号)、__FUNCTION__(函数名)等宏,而__MAIN__并不存在。


3.6宏定义1

以下代码输出什么? ( )

 
#include<stdio.h>
#define N 4
#define Y(n) ((N+2)*n) /*这种定义在编程规范中是严格禁止的*/
int main()
{
    int z = 2 * (N + Y(5 + 1));
    printf("%d", z);
    return 0;
}

A.出错

B.60

C.48

D.70

解析:

 
#include<stdio.h>
#define N 4
#define Y(n) ((N+2)*n) /*这种定义在编程规范中是严格禁止的*/
int main()
{
    //N=4  所以Y(n)=((4+2)*n)
    //所以2 * (N + Y(5 + 1)) = 2 * (4 + (4 + 2) * 5 + 1 ) = 2 * 35 = 70
    int z = 2 * (N + Y(5 + 1));
    printf("%d", z);
    return 0;
}

3.7宏定义2

下面代码执行的结果是:( )

 
#include<stdio.h>
#define A 2+2
#define B 3+3
#define C A*B
int main()
{
    printf("%d\n", C);
    return 0;
}

解析:

宏C预处理后的代码是:2+2*3+3,即2+6+3,等于11


3.8条件编译指令

下面哪个是条件编译指令( )

A.#define

B.#ifdef

C.#pragma

D.#error

解析:

A是宏定义,C是一个比较复杂的预编译语句,但跟条件肯定扯不上关系,

D是报错用的,条件编译指令包括#if、#ifdef,#ifndef,#else,#elif、#endif等。

除此之外还有#if defined(xxx)的用法。选B


3.9头文件

以下关于头文件,说法正确的是( )

A.#include,编译器寻找头文件时,会从当前编译的源文件所在的目录去找

B.#include“filename.h”,编译器寻找头文件时,会从通过编译选项指定的库目录去找

C.多个源文件同时用到的全局整数变量,它的声明和定义都放在头文件中,是好的编程习惯

D.在大型项目开发中,把所有自定义的数据类型、函数声明都放在一个头文件中,各个源文件都

只需包含这个头文件即可,省去了要写很多#include语句的麻烦,是好的编程习惯。

解析:

AB说反了,尖括号是直接去库找,双引号是先从当前目录找,再去库里找。

C选项头文件不能定义全局变量,否则如果有多个文件,那链接时会冲突。故选D。

D也不是十全十美,在大型项目的开发中,这也并不是一个很好的编程习惯,

分类放在不同的头文件并根据特点命名是更好的选择,因为这样更加方便代码的管理和维护,

就目前而言,算是一个好习惯吧。


本篇完。

C语言知识点算结束了。


a7a9f6ce42724a77a2a99564bc340994.jpg


目录
相关文章
|
1月前
|
存储 自然语言处理 编译器
【C语言】编译与链接:深入理解程序构建过程
【C语言】编译与链接:深入理解程序构建过程
|
1月前
|
编译器 C语言
C语言--预处理详解(1)
【10月更文挑战第3天】
|
1月前
|
编译器 Linux C语言
C语言--预处理详解(3)
【10月更文挑战第3天】
|
21天前
|
C语言
【c语言】你绝对没见过的预处理技巧
本文介绍了C语言中预处理(预编译)的相关知识和指令,包括预定义符号、`#define`定义常量和宏、宏与函数的对比、`#`和`##`操作符、`#undef`撤销宏定义、条件编译以及头文件的包含方式。通过具体示例详细解释了各指令的使用方法和注意事项,帮助读者更好地理解和应用预处理技术。
22 2
|
1月前
|
存储 自然语言处理 编译器
|
1月前
|
自然语言处理 编译器 Linux
C语言中抽象的编译和链接原理
C语言中抽象的编译和链接原理
20 1
|
1月前
|
C语言
C语言--预处理详解(2)
【10月更文挑战第3天】
|
1月前
|
编译器 C语言
C语言预处理详解
C语言预处理详解
|
1月前
|
Linux C语言 iOS开发
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
MacOS环境-手写操作系统-06-在mac下通过交叉编译:C语言结合汇编
19 0
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3