【维生素C语言】第十七章 - C语言预处理(下)

简介: 本文为C语言预处理的下篇,本文将进一步讲解预处理的基本知识,对命令行定义进行讲解。对条件编译的语句进行逐个讲解,理解两种文件包含的方式。

前言


本文为C语言预处理的下篇,本文将进一步讲解预处理的基本知识,对命令行定义进行讲解。对条件编译的语句进行逐个讲解,理解两种文件包含的方式。

一、命令行编译


❓ 什么是命令行编译?


💡 在编译的时候通过命令行的方式对其进行相关的定义,叫做命令行编译。


📚 介绍:许多C的编译器提供的一种能力,允许在命令行中定义符号。用于启动编译过程。当我们根据同一个源文件要编译出不同的一个程序的不同版本的时,可以用到这种特性,增加灵活性。


💬 例子:假如某个程序中声明了一个某个长度的数组,假如机器甲内存有限,我们需要一个很小的数据,但是机器丙的内存较大,我们需要一个大点的数组。


#include <stdio.h>
int main() {
    int arr[ARR_SIZE];
    int i = 0;
    for (i = 0; i < ARR_SIZE; i++) {
        arr[i] = i;
    }
    for (i = 0; i < ARR_SIZE; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
    return 0;
}

🚩 gcc 环境下测试:(VS 里面不太好演示)


gcc test.c -D ARR_SIZE=5
ls
a.out  test.c
./a.out
0 1 2 3 4 5
gcc test.c -D ARR_SIZE=20
./a.out
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20


二、条件编译


0x00 介绍

📚 在编译一个程序时,通过条件编译指令将一条语句(一组语句)编译或者放弃是很方便的。


💬 调试用的代码删除了可惜,保留了又碍事。我们就可以使用条件编译来选择性地编译:


#include <stdio.h>
#define __DEBUG__ // 就像一个开关一样
int main(void)
{
    int arr[10] = {0};
    int i = 0;
    for (i = 0; i < 10; i++) {
        arr[i] = i;
        #ifdef __DEBUG__ // 因为__DEBUG__被定义了,所以为真
        printf("%d ", arr[i]); // 就打印数组    
        #endif // 包尾
    }
    return 0;
}


🚩 运行结果:1 2 3 4 5 6 7 8 9 10


❗  如果不想用了,就把 #define __DEBUG__ 注释掉:


#include <stdio.h>
// #define __DEBUG__ // 关
int main(void)
{
    int arr[10] = {0};
    int i = 0;
    for (i = 0; i < 10; i++) {
        arr[i] = i;
        #ifdef __DEBUG__ // 此时ifdef为假
        printf("%d ", arr[i]);      
        #endif
    }
    return 0;
}


🚩 (代码成功运行)


0x01 条件编译之常量表达式

42a047d1fcb5914fdec929c98ef8d687_watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5Yaw5qON6KKL5a2Q,size_20,color_FFFFFF,t_70,g_se,x_16.png


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


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


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

🚩 运行结果:Hello,World!


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


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

🚩 (代码成功运行)


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


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

0x02 多分支的条件编译

c6d3b00a9250703970bd46cbc156250a_watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5Yaw5qON6KKL5a2Q,size_20,color_FFFFFF,t_70,g_se,x_16.png

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


💬 代码演示:


#include <stdio.h>
int main(void) {
#if 1 == 2 // 假
    printf("rose\n");
#elif 2 == 2 // 真
    printf("you jump\n");
#else 
    printf("i jump\n")
#endif
    return 0;
}

🚩 代码运行结果:you jump


0x03 条件编译判断是否被定义

1a870467a31c24e1465200910cb0bed1_watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5Yaw5qON6KKL5a2Q,size_20,color_FFFFFF,t_70,g_se,x_16.png


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


💬 代码演示:


#include <stdio.h>
#define TEST 0
// #define TEST2 // 不定义
int main(void) {
/* 如果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;
}


0x04 条件编译的嵌套

📚 和 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

三、文件包含


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


0x00 头文件被包含的方式

2ac7ad621cf663ca4134ff28eb083eb2_watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5Yaw5qON6KKL5a2Q,size_20,color_FFFFFF,t_70,g_se,x_16.png


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


① " " 的查找策略:先在源文件所在的目录下查找。如果该头文件未找到,则在库函数的头文件目录下查找。(如果仍然找不到,就提示编译错误)


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


/usr/include

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


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

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


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


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


💬 代码演示:


①  add.h


int Add(int x, int y);

②  add.c


int Add(int x, int y) {
  return x + y;
}

③  test.c


#include <stdio.h>
#include "add.h"
int main(void) {
  int a = 10;
  int b = 20;
  int ret = Add(a, b);
  printf("%d\n", ret);
  return 0;
}

🚩 运行结果:30


0x01 嵌套文件的包含

❗  头文件重复引入的情况:

940c94730252e1a102bd790f30151ee6_watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5Yaw5qON6KKL5a2Q,size_20,color_FFFFFF,t_70,g_se,x_16.png


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

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


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

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


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


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


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


答:尖括号是包含库里面的头文件的,双引号是包含自定义头文件的。它们在查找策略上不同,尖括号直接去库目录下查找。而警号双引号是现去自定义的代码路径下查找,如果找不到头文件,则在库函数的头文件目录下查找。


相关文章
|
1月前
|
编译器 C语言
C语言的预处理指令
C语言的预处理指令
|
1月前
|
编译器 Linux PHP
C语言从入门到实战——预处理详解
C语言预处理是C语言编译过程的一个阶段,它在编译之前对源代码进行一系列的处理操作,包括宏替换、文件包含、条件编译等,最终生成经过预处理的代码,然后再进行编译。
45 0
|
1月前
|
存储 编译器 程序员
【程序环境和预处理】C语言
【程序环境和预处理】C语言
|
1月前
|
存储 机器学习/深度学习 自然语言处理
【进阶C语言】编译与链接、预处理符号详解
【进阶C语言】编译与链接、预处理符号详解
23 0
|
1月前
|
存储 自然语言处理 编译器
【C语言进阶】程序环境和预处理
【C语言进阶】程序环境和预处理
|
1月前
|
编译器 Linux C语言
程序环境和预处理(含C语言程序的编译+链接)--2
程序环境和预处理(含C语言程序的编译+链接)--2
30 5
|
1月前
|
存储 编译器 程序员
程序环境和预处理(含C语言程序的编译+链接)--1
程序环境和预处理(含C语言程序的编译+链接)--1
24 0
|
2月前
|
编译器 Linux C语言
C语言:预处理详解
C语言:预处理详解
|
3月前
|
编译器 C语言 C++
C语言——预处理
C语言——预处理
|
3月前
|
编译器 C语言
C语言基础专题 - 预处理
C语言基础专题 - 预处理
13 1