[笔记]C Primer Plus 第16章:C预处理器和C库

16.1 翻译程序的第一步

在预处理之前,编译器会进行一些翻译处理:

  1. 处理多字节字符和“三字符序列”(目前已很少使用),映射到源字符集;

  2. 将两个物理行(physical line)转换为一个逻辑行(logical line):

转换前:

printf("Wonder\
ful");

转换后:printf("Wonderful");

  1. 用空格替换注释,用1个空格替换所有空白字符(不包括换行符)。

16.2 明示常量:#define

有类对象宏(object-like macro)、类函数宏(function-like macro)。

每行#define由3部分组成——#define指令、宏和替换体:#define PX printf("x == %d\n", x),从宏替换为最终文本的过程称为宏展开(macro expansion)

双引号中的宏不会被展开:

#define TWO 2
printf("TWO");     //输出TWO
printf("%s", TWO); //输出2

宏可以用来指定const对象的初始值、const对象数组的大小等(而const却无法指定,见下例):

#define DEF_LIMIT 20
const int CONST_LIM = 50;
static int data1[DEF_LIMIT];  //有效
static int data2[CONST_LIM];  //无效
const int VAL1 = 2 * DEF_LIMIT;  //有效
const int VAL2 = 2 * CONST_LIM;  //无效

16.2.1 记号

宏的替换体可以看作记号(token)型字符串。比如#define SIX 2*2中有1个记号(2*2);#define SIX 2 * 3中有3个记号(2*3)。

16.2.2 重定义常量

在ANSI标准中,只有新定义和旧定义完全相同才允许重定义:

#define SIX 2 * 3
#define SIX 2 * 3 //允许
#define SIX 2*3   //不允许,token数量不同

如果需要重定义,使用#undef指令。

16.3 在#define中使用参数

类函数宏:#define MEAN(X, Y) ((X)+(Y)/2)

直接替换会产生一些运算上的问题,比如:

#define SQUARE(X) X*X

SQUARE(x+2) //预期为(x+2)^2,实际被替换为"x+2*x+2",即3x+2
//解决方法:参数带括号,如#define SQUARE(X) (X)*(X)

100/SQUARE(x) //预期为100/x^2,实际被替换为"100/x*x",即100
//解决方法:替换体带括号,如#define SQUARE(X) ((X)*(X))

//所以在#define MEAN(X, Y) ((X)+(Y)/2)中,参数和替换体都带了括号

SQUARE(++x) //预期为(x+1)^2,被替换为++x*++x,即(x+1)(x+2)
//暂无解决方法,不要在类函数宏中使用递增或递减运算符

16.3.1 用宏参数创建字符串:#运算符

C字符串的串联特性:printf("a" "b" "c");的输出是abc。

预处理运算符#可以将记号替换为字符串(称为“字符串化”(stringizing)):

#define PRINT_SQUARE(X) printf("The square of " #x "is %d\n", ((x)*(x)))

y=2;
PRINT_SQUARE(y);     //宏展开将#x替换为y,    输出"The square of y is 4"
PRINT_SQUARE(2 + 4); //宏展开将#x替换为2 + 4,输出"The square of 2 + 4 is 36"

16.3.2 预处理器粘合剂:##运算符

将2个记号组合成1个记号,如:#define XNAME(n) x ## n,则XNAME(4)将展开为x4,可以用于变量命名。

16.3.3 变参宏:...和__VA_ARGS__

以三个点"..."的方式表示,使类函数宏的参数数量可变:

#define PRINT(...) printf(__VA_ARGS__)

PRINT("ABC");
PRINT("VAL=%d, str=%s", 2, "Helloworld");

16.4 宏和函数的选择

多次调用时,函数只产生1份副本,节省空间,然而需要多次跳转,耗费时间;宏需要插入多次,耗费空间,然而无需跳转,节省时间。
简单的函数一般用宏定义。调用次数少,宏的效果不明显;调用次数多(如嵌套循环中),用宏有助于提高效率。

16.5 文件包含:#include

在头文件中,只声明不定义!

extern的作用是使一个文件中的全局变量或函数可以在其他文件中使用,实现共享。

步骤:

  1. 在file1.c中定义全局变量和函数(可供其他文件使用):
int val = 5;
int fun(){ return 0; }
  1. 在header.h中声明相关变量和函数:
extern int val = 5;
extern int fun();
  1. 在file2.c文件中包含.h头文件:#include "header.h"

  2. 编译时,只需要编译连接.c文件:gcc -o file file1.c file2.c

原理:

#include的原理是将头文件整个地复制到#include的位置,所以可以理解,编译连接时只用到.c文件(不用.h)。所以extern会使程序在其他.c文件中寻找定义。

img

16.6 其他指令

16.6.1 #undef

取消之前的#define定义。即使之前没有定义过,#undef也有效,所以当想使用宏定义却不确定之前是否定义过时,为了安全,仍可使用#undef

16.6.3 条件编译

1. #ifdef#else#endif

#ifdef#else#endif,可用于调试程序代码:

#define DEBUG

int main(){
    int total = 0;

    for (int i=0;i<10;i++){
        total += 1;
#ifdef DEBUG
        printf("current value:%d", total);
#endif
    }

    printf("total value:%d", total);
}

不调试时去掉#define DEBUG,调试时加上即可,从而不用去掉printf()调试代码。

2. #ifndef

①防止重复包含

如果.c文件同时包含了A.h和B.h,B.h又包含了A.h,则造成重复包含。为了避免重复包含,需要在.h文件开头加上#ifndef HEADER_NAME_H#endif(HEADER_NAME_H可任意更改,只要是唯一的能标识当前.h文件即可,一般用头文件名+_H)。(补充说明:在.h文件开头添加的原因是,包含.h文件相当于整个地复制到.c文件中)

②便于调试代码

#ifndef也可以用于方便代码调试,比如header.h中有:

#ifndef LIMIT
    #define LIMIT 100
#endif

则在包含了该头文件的.c文件中,可通过如下方式,测试较小的LIMIT数值:

#define LIMIT 10     //临时测试
#include "header.h"

或者用#undef

#include "header.h"
#undef LIMIT       //临时测试
#define LIMIT 10   //临时测试

这样就无需在header.h中修改LIMIT的值。

3. #if#elif

#if#elif后接整型常量表达式,示例:

#if SYS == 1
#include "ibm.h"
#elif SYS == 2
#include "vax.h"
#else
#include "general.h"
#endif

16.6.4 预定义宏

预定义宏 含义
DATE 当前日期,格式为 "MMM DD YYYY"
TIME 当前时间,格式为 "HH:MM:SS"
FILE 当前源文件的文件名
LINE 当前代码行号
STDC 如果编译器遵循 ANSI 标准,则为 1
__cplusplus 如果编译器用于 C++,则定义为一个数字

__func__是预定义标识符,展开为代表函数名的字符串。

16.6.5 #line#error

#line:重置行号和文件名。

#line 100
#line 10 "cool.c"

#error:发出一条错误消息。

#if __STDC_VERSION__ != 201112L
#error Not C11
#endif

16.6.6 #pragma(编译器设置)

16.6.7 泛型选择(C11)

泛型编程(generic programming)的代码没有特定类型,可以转换为指定类型。C11引入了泛型选择表达式,可以根据表达式的类型选择1个值。示例:

_Generic(x, int: 0, float: 1, double: 2, default: 3)

结合#define使用示例:

#define MYTYPE(X) _Generic((X), int: "int", float: "float", double: "double", default: "others")

当X为int类型,宏展开为字符串"int";X为float类型,宏展开为字符串"float";等等。

16.7 内联函数

编译器可能会将函数调用直接替换为内联代码。内联函数应当比较短小。如果多个文件需要用到同一内联函数,可以在头文件中定义内联函数(一般头文件只声明、不定义,内联函数是特例)。

16.8 _Noreturn

16.9 C库

16.10 数学库

16.11 通用工具库

16.11.1 exit()atexit()

16.11.2 qsort()(快速排序)

16.12 断言库

16.13 string.h库中的memcpy()memmove()

函数原型:

void* memcpy(void* restrict s1, const void* restrict s2, size_t n);
void* memmove(void* s1, const void* s2, size_t n);

从s2指向的位置拷贝到s1指向的位置。memcpy()不允许s1和s2内存区域有重叠;memmove()则没有这样的要求。

16.14 可变参数:stdarg.h

可变参数的函数,参数列表以"..."结尾,"..."之前的1个参数是可变参数的数量(如下面例子中的num)。

#include <stdarg.h>

int sum(int num, ...){
    va_list ap;            //初始化va_list对象ap
    va_list ap_copy;
    va_start(ap, num);     //传入va_list对象ap和可变参数的数量num,使ap初始化
    va_copy(ap_copy, ap);  //将ap拷贝给ap_copy

    int sum=0;
    for (int i=0;i<num;i++)
        sum+=va_arg(ap, double);  //传入va_list对象ap和当前参数的类型
        //va_arg的第n次调用返回第n个可变参数
        //由于不支持退回之前的参数,所以需要ap_copy进行备份

    va_end(ap);      //清理工作
    va_end(ap_copy);
    return sum;
    }
posted @   Digitzh  阅读(21)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
· 零经验选手,Compose 一天开发一款小游戏!
点击右上角即可分享
微信分享提示