有基础,进阶用,个人查漏补缺
第十五章的内容之前学过,跳过
预处理之前,编译器必须对该程序进行一些翻译处理
首先把源代码中出现的字符映射到原字符集
其次编译器定位每个反斜杠后面跟着换行符的实例,并删除它们(把由于写代码时,一行太长,会用反斜杠\把一行逻辑行变成两个物理行)
然后编译器把文本划分为预处理记号序列、空白序列和注释序列。此处需要注意的是,编译器将用一个空格字符替换每一条注释,如
int/* 注释*/fox;
//将变成
int fox;//中间的注释变成了一个空格
C预处理器在程序执行之前查看程序,故称之为预处理器。根据程序中的预处理器指令,预处理器把符号缩写替换成其表达的内容。
明示常量:#define
指令从#开始运行,到第1个换行符结束,针对的是一个逻辑行(可以用\进行物理换行)
类对象宏定义的组成:宏的名称中不允许有空格,需要遵循C变量的命名规则
#define PX printf("x is %d\n", x)
//预处理指令 宏 替换体
预处理器不做计算,不对表达式求值,只进行替换
记号:可以把宏的替换体看作是记号(token)型字符串
#define FOUR 2*2 //有一个记号2*2,但对于C编译器来说是3个记号
#define six 2 * 3 //有三个记号2、*、3,额外的空格也是替换体的一部分
重定义常量:假设先把LIMIT定义为20,稍后在该文件中又把它定义为25。
//ANSI标准在新定义和旧定义完全相同时才允许重定义
#define six 2 * 3
#define six 2 * 3
在#define中使用参数,即类函数宏:
为保证运算顺序,要多使用圆括号
#define SQUARE(X) X*X
#define SQUARE1(X) (X*X)
#define SQUARE2(X) (X)*(X)
int x = 5;
z = SQUARE(x);//z=25
z = SQUARE(x+2);//z= x+2*x+2 = 5+2*5+2 = 17
z = 100 / SQUARE(2);//z = 100/2*2 = 100/2*2 = 100
z = SQUARE1(2);//z = 100 / (2*2) = 25
z = SQUARE2(x+2);//z = (x+2)*(x+2)
避免使用++x等这种递增或者递减
用宏参数创建字符串:#运算符
#define PSQR(X) printf("The square of X is %d.\n", ((X)*(X)))
PSQR(8);//输出The square of X is 64.**注意双引号中的X被视为普通文本,不是记号**
#define PSQR(x) printf("The square of " #x " is %d.\n", ((x)*(x)))
int y = 5;
PSQR(y);//输出The square of y is 25
PSQR(2 + 4);//输出The square of 2 + 4 is 36
预处理黏合剂:##运算符
#define XNAME(n) x ## n
int XNAME(1) = 14;//变成int x1 = 14
变宏参:…和__VA_ARGS__
#define PR(...) prinf(__VA_ARGS__)
pr("Hoedy");//等于 prinf("Hoedy")
#define PR(X, ...) prinf("Message " #X ": " __VA_ARGS__)
int x = 2;
PR(1, "x = %d\n", x);//即prinf("Message " "1" ": " "x = %d\n", x)
//输出Message 1: x = 2
宏和函数的选择
宏生成内联代码,即在程序中生成语句,调用20次宏就生成20行代码。而调用函数20次,在程序中只有一份函数语句的副本,节省空间
调用函数时,程序控制必须跳转到函数内,再返回主调程序,比内联代码更费时间
宏不用担心变量类型
在嵌套循环中使用宏更有助于提高效率
对于简单的函数,通常使用宏
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define ABS(X,Y) ((X) < 0 ? -(X) : (X))
#DEFINE ISSIGN(X) ((X) == '+' || (X) == '-' ? 1 : 0)
文件包含:#include
当预处理器发现#include指令时会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令。这相当于把被包含文件的全部内容输入到源文件#include指令所在的位置。
#include指令有两种形式:文件名在尖括号或者双引号中
//在unix系统中
#include //查找系统目录
#include "mystuff.h" //查找当前工作目录
#include "/usr/biff/mystuff.h" //查找/usr/biff/目录
头文件实例
names_st.h————names_st结构的头文件
// 常量
#include
#define SLEN 32
// 结构声明
struct names_st
{
char first[SLEN];
char last[SLEN];
};
// 类型定义
typedef struct names_st names;
// 函数原型
void get_names(names *);
void show_names(const names *);
char * s_gets(char * st, int n);
names_st.c————定义names_st.h中的函数
#include
#include "names_st.h" //包含头文件
//函数定义
void get_names(names * pn)
{
printf("Please enter your first name: ");
s_gets(pn->first, SLEN);
printf("Please enter your last name: ");
s_gets(pn->last, SLEN);
}
void show_names(const names * pn)
{
printf("%s %s", pn->first, pn->last);
}
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n'); //查找换行符
if (find) //如果地址不是NULL,
*find = '\0'; //在此处放一个空字符
else
while (getchar() != '\n')
continue; //处理输入行中的剩余字符
}
return ret_val;
}
useheader.c————使用names_st结构
#include
#include "names_st.h"
//记得链接names_st.c
int main(void)
{
names candidate;
get_names(&candidate);
printf("Let's welcome ");
show_names(&candidate);
printf(" to this program!\n");
return 0;
}
注意:
使用头文件
头文件中最常用的形式如下:
#undef 指令
用于取消已定义的 #define 指令
#define LIMIT 40
#undef LIMIT //可以移除上面的定义
现在可以将LIMIT重新定义为一个新值,即使原来没有定义LIMIT,该取消也依旧有效;
如果想使用一个名称,又不确定是否之前已经用过,为安全起见,可以使用 #undef 取消该名称的定义
条件编译——#ifdef、#else、#endif
预处理器不识别用于标记块的花括号{}
缩进与否看个人风格
#ifdef MAVIS //如果已经用#define定义了MAVIS,则执行下面的指令
#include "horse.h"
#define STABLE 5
#else //如果没有用#define定义了MAVIS,则执行下面的指令
#include "cow.h"
#define STABLE 5
#endif //必须存在
也可以用这些指令标记C语句块
#include
#define JUST_CHECKING
#define LIMIT 4
int main(void)
{
int i;
int total = 0;
for (i = 1; i <= LIMIT; i++)
{
total += 2*i*i + 1;
#ifdef JUST_CHECKING
printf("i=%d, running total = %d\n", i, total);
#endif
}
printf("Grand total = %d\n", total);
return 0;
}
输出:
i=1, running total = 3
i=2, running total = 12
i=3, running total = 31
i=4, running total = 64
Grand total = 64
如果省略JUST_CHECKING定义(把#define JUST_CHECKING放在注释中,或者使用#undef指令取消它的定义),并重新编译该程序,只会输出最后一行。该方法可用来调试程序。
条件编译——#ifndef 指令
#ifndef 和#ifdef 用法类似,也是和#else、#endif一起使用,只是它们的逻辑相反
有arrays.h
#ifndef SIZE
#define SIZE 100
#endif
有代码
#define SIZE 10
#include "arrays.h" //当执行到该行时,跳过了#define SIZE 100,故SIZE为10
故可以使用#ifndef 技巧避免重复包含
#ifndef NAMES_H_
#define NAMES_H_
// constants
#define SLEN 32
// structure declarations
struct names_st
{
char first[SLEN];
char last[SLEN];
};
// typedefs
typedef struct names_st names;
// function prototypes
void get_names(names *);
void show_names(const names *);
char * s_gets(char * st, int n);
#endif
用以下代码进行测试,是没有问题的。但是如果把上面的.h中的#ifndef 删除,程序会无法通过编译
#include
#include "names.h"
#include "names.h" //不小心第2次包含头文件
int main()
{
names winner = {"Less", "Ismoor"};
printf("The winner is %s %s.\n", winner.first, winner.last);
return 0;
}
条件编译——#if 和 #elif
类似于if语句。#if 后面跟着整型常量表达式
#if SYS == 1
#include "a.h"
#elif SYS == 2
#include "b.h"
#elif SYS == 3
#include "c.h"
#else
#include "d.h"
#endif
另一个新的用法测试名称是否已经定义
#if defined (ABC)
#include "a.h"
#elif defined (DE)
#include "b.h"
#elif defined (FG)
#include "c.h"
#else
#include "d.h"
#endif
预定义宏
#line 和 #error
#1ine 指令重置__LINE__和__FILE__宏报告的行号和文件名。可以这样使用Iine:
#line 1000 11 //把当前行号重置为1000
#line 10 "cool.c" //把行号重置为10,把文件名重置为 cool.c
#error 指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断。可以这样使用#error 指令:
#if __STDC_VERSION__!= 201112L
#error Not C11
#endif
//编译以上代码生成后,输出如下:
$ gcc newish.c
newish.c:14:2: error: #error Not C11
$ gcc -std=c11 zewish.c
$
如果编译器只支持旧标准,则会编译失败,如果支持 CI1 标准,就能成功编译。
#pragma
在现在的编译器中,可以通过命令行参数或 IDE 菜单修改编译器的一些设置。#pragma 把编译器指令放入源代码中。例如,在开发 C99 时,标准被称为 C9X,可以使用下面的编译指示 (pragma)让编译器支持 C9X:
#pragma c9x On
泛型选择(C11)
在程序设计中,泛型编程(generic programming)指那些没有特定类型,但是一旦指定一种类型,就可以转换成指定类型的代码。
例如,C++在模板中可以创建泛型算法,然后编译器根据指定的类型自动使用实例化代码。C没有这种功能。然而,C11 新增了一种表达式,叫作泛型选择表达式(generic selection expression),可根据表达式的类型(即表达式的类型是int、double 还是其他类型)选择一个值。泛型选择表达式不是预处理器指令,但是在一些泛型编程中它常用作#define 宏定义的一部分。
下面是一个泛型选择表达式的示例:
_Generic (x, int: 0, float: 1, double: 2, default: 3)
_Generic 是C11 的关键宇。_Generic 后面的國括号中包含多个用逗号分隔的项。
#include
#define MYTYPE(X) _Generic((X),\
int: "int",\
float : "float",\
double: "double",\
default: "other"\
)
int main(void)
{
int d = 5;
printf("%s\n", MYTYPE(d)); //d是int类型,输出int
printf("%s\n", MYTYPE(2.0*d)); //2.0*d是double类型,输出double
printf("%s\n", MYTYPE(3L)); //3L是long类型,输出other
printf("%s\n", MYTYPE(&d)); //&d是int * 类型输出other
return 0;
}
内联函数(C99)
通常,函数调用都有一定的开销,因为函数的调用过程包括建立调用、传递参数、跳转到函数代码并返回。使用宏使代码内联,可以避免这样的开销。
C99还提供另一种方法:内联函数(inline function)。把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用。
标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。
因此,最简单的方法是使用函数说明符inline和存储类别说明符static。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。如下所示:
#include
inline static void eatline () // 内联函数定义/原型
{
while (getchar() != '\n')
continue;
}
int main()
{
...
eatline(); //函数调用
...
}
编译器查看内联函数的定义(也是原型),可能会用函数体中的代码替换 eatline)函数调用。也就是说,效果相当于在西数调用的位置输入函数体中的代码:
#include
inline static void eatline () // 内联函数定义/原型
{
while (getchar() != 'In')
continue;
}
int main()
{
...
//函数调用之处相当于插入代码块
while (getchar() != "\n')
continue;
...
}
由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。
内联函数应该比较短小。把较长的函数变成内联并未节约多少时间,因为执行函数体的时间比调用函数的时间长得多。
编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。鉴于此,一般情况下内联函数都具有内部链接。
因此,如果程序有多个文件都要使用某个内联函数,那么这些文件中都必须包含该内联函数的定义。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。
一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联两数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。
与C++不同的是,C还允许混合使用内联函数定义和外部函数定义(具有外部链接的两数定义)。例如,一个程序中使用下面了个文件:
// file1.c
#include
inline static double square(double x) { return x*x; }
void spam(double);
void masp(double);
int main()
{
double q = square(1.3);
printf("%.2f\n", q);
spam(12.6);
masp(1.6);
return 0;
}
// file2.c
#include
double square(double x) { return (int) (x*x); }
void spam(double v)
{
double kv = square(v);
printf("%.2f\n", kv);
return;
}
// file3.c
#include
inline double square(double x) { return (int) (x * x + 0.5); }
void masp(double w)
{
double kw = square(w);
printf("%.2f\n", kw);
return;
}
如上述代码所示,3 个文件中都定义了square() 函数。
关于库以及一些函数的使用跳过