有基础,进阶用,个人查漏补缺
第十五章的内容之前学过,跳过
-
预处理之前,编译器必须对该程序进行一些翻译处理
-
首先把源代码中出现的字符映射到原字符集
-
其次编译器定位每个反斜杠后面跟着换行符的实例,并删除它们(把由于写代码时,一行太长,会用反斜杠\把一行逻辑行变成两个物理行)
-
然后编译器把文本划分为预处理记号序列、空白序列和注释序列。此处需要注意的是,编译器将用一个空格字符替换每一条注释,如
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 = 100z = SQUARE1(2);//z = 100 / (2*2) = 25z = 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 <stdio.h> //查找系统目录 #include "mystuff.h" //查找当前工作目录 #include "/usr/biff/mystuff.h" //查找/usr/biff/目录
-
头文件实例
-
names_st.h————names_st结构的头文件
// 常量 #include <string.h> #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 <stdio.h> #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'; //在此处放一个空字符elsewhile (getchar() != '\n')continue; //处理输入行中的剩余字符}return ret_val; }
-
useheader.c————使用names_st结构
#include <stdio.h> #include "names_st.h" //记得链接names_st.cint main(void) {names candidate;get_names(&candidate);printf("Let's welcome ");show_names(&candidate);printf(" to this program!\n");return 0; }
-
注意:
- 两个源文件.c都使用names_st类型结构,所以它们都必须包含names_st.h头文件
- 必须编译和链接names_st.c和useheader.c源代码文件
- 声明和指令放在names_st.h头文件中,函数定义放在names_st.c源代码文件中
-
-
使用头文件
头文件中最常用的形式如下:
- 明示常量
- 宏函数
- 函数声明
- 类型定义
- 使用头文件声明外部变量供其他文件共享
-
-
#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 <stdio.h> #define JUST_CHECKING #define LIMIT 4int main(void) {int i;int total = 0;for (i = 1; i <= LIMIT; i++){total += 2*i*i + 1; #ifdef JUST_CHECKINGprintf("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 <stdio.h> #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 后面的國括号中包含多个用逗号分隔的项。
- 第1个项是一个表达式,后面的每个项都由一个类型、一个冒号和一个值组成,如float:1
- 第1个项的类型匹配哪个标签,整个表达式的值是该标签后面的值。例如,假设上面表达式中× 是int 类型的变量,× 的类型匹配int :标签,那么整个表达式的值就是0。
- 如果没有与类型匹配的标签,表达式的值就是 default:标签后面的值。
- 泛型选择语句与 switch 语句类似,只是前者用表达式的类型匹配标签,而后者用表达式的值匹配标签。
#include <stdio.h> #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类型,输出intprintf("%s\n", MYTYPE(2.0*d)); //2.0*d是double类型,输出doubleprintf("%s\n", MYTYPE(3L)); //3L是long类型,输出otherprintf("%s\n", MYTYPE(&d)); //&d是int * 类型输出otherreturn 0; }
-
内联函数(C99)
通常,函数调用都有一定的开销,因为函数的调用过程包括建立调用、传递参数、跳转到函数代码并返回。使用宏使代码内联,可以避免这样的开销。
C99还提供另一种方法:内联函数(inline function)。把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但是也可能不起作用。
标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中。
因此,最简单的方法是使用函数说明符inline和存储类别说明符static。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。如下所示:
#include <stdio.h> inline static void eatline () // 内联函数定义/原型 {while (getchar() != '\n')continue; } int main() {...eatline(); //函数调用... }
编译器查看内联函数的定义(也是原型),可能会用函数体中的代码替换 eatline)函数调用。也就是说,效果相当于在西数调用的位置输入函数体中的代码:
#include <stdio.h> inline static void eatline () // 内联函数定义/原型 {while (getchar() != 'In')continue; } int main() {... //函数调用之处相当于插入代码块while (getchar() != "\n')continue; ... }
由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数)。另外,内联函数无法在调试器中显示。
内联函数应该比较短小。把较长的函数变成内联并未节约多少时间,因为执行函数体的时间比调用函数的时间长得多。
编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。鉴于此,一般情况下内联函数都具有内部链接。
因此,如果程序有多个文件都要使用某个内联函数,那么这些文件中都必须包含该内联函数的定义。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。
一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联两数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。
与C++不同的是,C还允许混合使用内联函数定义和外部函数定义(具有外部链接的两数定义)。例如,一个程序中使用下面了个文件:
// file1.c #include <stdio.h> 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 <stdio.h> double square(double x) { return (int) (x*x); } void spam(double v) {double kv = square(v);printf("%.2f\n", kv);return; }
// file3.c #include <stdio.h> 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() 函数。
- file1.c文件中是 inline static定义
- file2.c 文件中是普通的函数定义(因此具有外部链接)
- file3.c 文件中是 inline 定义,省路了static
- 3个文件中的函数都调用了 sguare() 函数,这会发生什么情况?
- file1.c文件中的main()使用square()的局部static 定义。由于该定义也是inline 定义,所以编译器有可能优化代码,也许会内联该函数。
- file2.c文件中spam函数使用该文件中square()函数的定义,该定义具有外部链接,其他文件也可见。
- file3.c 文件中,编译器既可以使用该文件中square()函数的内联定义,也可以使用file2.c文件中的外部链接定义。如果像 file3.c 那样,省路file1.c 文件 inline 定中的 static,那么该 inline 定义被视为可替换的外部定义。
-
关于库以及一些函数的使用跳过