一. 预处理器

编译一个 C 程序涉及很多步骤,其中第一个步骤被称为预处理阶段。C 预处理器在源代码编译之前对其进行一些文本性质的操作。

它的主要内容包括:

  1. 删除注释
  2. 插入被 【#include】指令包含的文件内容
  3. 定义和替换由【#define】指令定义的符号
  4. 确定代码的部分内容是否应该根据一些条件编译指令进行编译

1.1 预定义符号

符号 类型 含义
__FILE__ %s 进行编译的源文件名
__LINE__ %d 文件当前行的行号
__DATE__ %s 文件被编译的日期
__TIME__ %s 文件被编译的时间

1.2 define

【#define】常用来定义符号常量,但它还有许多其它用途

#define ROW 10
#define COL 10

1.3 宏

【#define】机制包括一个规定,允许把参数替换到文本中,这种实现通常称为

下面是宏的声明方式

#define name(parameter-list) stuff

parameter-list 由逗号分隔,其左括号必须与 name 紧邻。如果两者之间有任何空白存在,参数列表就会被解释为 stuff 的一部分

#include <stdio.h>

#define SQUARE(x) ((x)*(x))
#define DOUBLE(x) ((x)+(x))

int main(void)
{
    printf("%d\n", 2*SQUARE(5+1)); 
    printf("%d\n", 2*DOUBLE(5+1));  
    return 0;
}
#include <stdio.h>
#define MAX(x, y) ((x) > (y) ? (x) : (y))

int main(void)
{
    printf("%d\n", MAX(3, 4));    
    return 0;
}

1.3.1【#undef】

#undef name

如果一个现存的名字需要被重新定义,那么首先必须用【#undef】移除它的旧定义

1.4 条件编译

在编译一个程序时,如果可以翻译或忽略选定的某条语句或某组语句,会很方便

条件编译可以实现这个目的。使用条件编译,可以选择代码的一部分是被正常编译还是完全忽略。

#if constant-expression
    statements
#endif

constant-expression 由预处理器进行求值,如果值为真,那么 statements 部分被正常编译,否则预处理器就删除它们

#include <stdio.h>

#define DEBUG 0

int main(void)
{  
#if DEBUG
    printf("%s, %s, %s\n", __FILE__, __DATE__, __TIME__);
#endif
    return 0;
}

1.4.1 是否被定义

测试一个符号是否已经被定义也是可能的

#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol

上面每对定义的语句是等价的

#include <stdio.h>
#define DEBUG 1

int main(void)
{  
#ifdef DEBUG
    printf("%s, %s, %s\n", __FILE__, __DATE__, __TIME__);
#endif
    return 0;
}

1.4.2 文件包含

预处理器处理【#include】指令,它会删除这条指令,并用文件的内容取而代之。这样,一个头文件如果被包含到 10 个源文件中,它实际上被编译了 10 次

看下面的代码:

#include "a.h"
#include "b.h"

看上去没什么问题,如果 a.h 和 b.h 都包含一个嵌套的【#include “x.h”】,那么 x.h 在此处就被编译了两次

可以使用条件编译解决头文件被多次编译的情况

#ifndef _HEADERNAME_H
#define _HEADERNAME_H
/*
    All the stuff that you want in the header file
*/
#endif

注意:

  1. 符号 _HEADERNAME_H 应按照被包含头文件的文件名进行取名,以避免由于其它头文件使用相同的符号而引起的冲突
  2. 必须记住,预处理器仍将读入整个头文件,只是这个文件的所有内容将被忽略。由于这种处理将拖慢编译速度,因此如果可能,应避免出现多重包含。

1.5 assert(断言)

断言就是声明某种东西应该为真,assert 是一个宏,在调试程序时很有用

原型如下

void assert(int expression)

这个宏在执行时,会对表达式参数进行测试。如果它的值为假,程序就会被终止,并在控制台打印一条诊断信息。如果为真,不打印任何东西,程序继续执行。

#include <stdio.h>
#include <assert.h>

int main(void){
    int num = 1;
    assert(num != 2);
    printf("num = %d\n", num);
    return 0;
}

当程序被完整地测试完毕后,可以在编译时通过定义 NDEBUG 消除所有断言,在包含头文件 assert.h 之前增加下面这个定义

#define NDEBUG

这样,预处理器将丢弃所有断言,这样就消除了这方面的开销,而不必从源文件中把所有断言实际删除

二. 函数指针

请看下面一个例子

int *f(void);
int (*f)(void);

第二个声明有两个括号,每对的含义各不相同。第 2 对括号是函数调用操作符,第一对括号只起到聚组的作用。它迫使间接访问在函数调用之前进行,使 f 成为一个函数指针,它指向的函数没有形参,返回一个整形

函数指针?Yes,程序中的每个函数都位于内存中的某个位置,所以存在指向那个位置的指针是完全可能的

int *(*f)(void)
int (*f[4])(int)
int *(*f[5])(int, double)
#include <stdio.h>

int sumNum(int num_1, int num_2){
    return num_1 + num_2;
}

int main(void){
    int (*func_p)(int,int) = sumNum;
    printf("%d\n", func_p(1, 2));  
    return 0;
}

2.1 void*

void * 表示一个指向未知类型的指针,也因此它可以接收任意类型的指针

但也是因为可以接收任意数据类型的指针,void * 指针是不能解引用的,因为它不知道在解引用的时候,我到底要访问几个字节

也是因为上述类型,void * 指针不能进行 + - 整数的操作

#include <stdio.h>

int main(void)
{  
    int num_1 = 1;
    int *int_p = &num_1;
    void *void_p = int_p;
    printf("%d\n", *(int*)void_p);
    return 0;
}

作业

编写函数,给你一个按非递减顺序排序的整数数组 nums,返回每个数字的平方组成的新数组,要求也按非递减顺序排序。

示例 1

输入:nums = [-4,-1,0,3,10]

输出:[0,1,9,16,100]

解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100]

示例 2

输入:nums = [-7,-3,2,3,11]

输出:[4,9,9,49,121]