C语言学习(十二)C预处理器和C库

参考书:《C Primer Plus》第六版


C预处理器在程序执行之前查看程序,根据程序中的预处理器指令,把符号缩写替换成其表示的内容。
基本上它的工作是把一些文本转换成另一些文本。

1. 翻译程序的第一步

预处理之前,编译器必须对程序做一些翻译处理。首先编译器把源码中出现的字符映射到源字符集。
然后编译器定位每个斜杠后面跟着换行符的实例,并删除它们。
再然后编译器把文本划分成预处理记号序列、空白序列和注释序列。编译器会将一个空格字符替换每一条注释。它会用一个空格替换所有的空白字符序列。
最后,程序已经准备号进入预处理阶段,预处理器查找一行中以#号开始的预处理指令。

2. 明示常量:#define

可以在#define中使用参数,可以创建外形和作用和函数类似的类函数宏。

#define SQUARE(X) X*X
z=SQUARE(2);

其中SQUARE是宏标识符,其中的X是宏参数,X*X是替换列表。
但这个用法和函数是不同的,它可能会产生与我们预期不符的结果,所以我们一般不用这种用法。

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

#号作为一个预处理运算符,可以把记号转换成字符串。如x是一个宏形参,那么#x就是转换为字符串"x"的形参名。这个过程称为字符串化。
程序清单1:

#include"stdio.h"
#define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x)))

int main(void){
     
    int y=5;
    PSQR(y);
    PSQR(2 + 4);
    return 0;
}
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# ./list1
The square of y is 25.
The square of 2 + 4 is 36.

预处理器黏合剂:## 运算符

##运算符把两个记号组合成一个记号。
程序清单2

#include
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n",x ## n )

int main(void){
     
   int XNAME(1) =14;
   int XNAME(2) =20;
   int x3=30;
   PRINT_XN(1);
   PRINT_XN(2);
   PRINT_XN(3);
   return 0;
}
x1 = 14
x2 = 20
x3 = 30

变参宏:...__VA_ARGS__

可以把宏参数中的最后的参数写成...实现可变的参数
程序清单3

#include"stdio.h"

#define PR(x,...) printf("Message" #x ": " __VA_ARGS__)

int main(void){
     
    double x=48;
    double y;
    y=  33;
    PR(1,"x=%g\n",x);
    PR(2,"x=%.2f, y= %.4f\n",x,y);
    return 0;
}
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# ./list3
Message1: x=48
Message2: x=48.00, y= 33.0000
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# 

宏和函数的选择

使用宏要比使用函数复杂一些,也存在一些限制,一些编译器规定宏只能定义成一行。
宏和函数的选择实际上是时间和空间的权衡。宏生成内联代码,它会在程序中生成语句,调用多少次就生成多少次语句。而函数只有一份函数语句的副本,但另一方面,程序的控制需要跳转至函数中,之后再返回主调函数,自然比内联代码花的时间要多。
宏的一个好处是不用担心变量类型。
对于简单的函数,用宏好一点。

#define MAX(X,Y) ((X)>(Y)?(X) :(Y))
#define ABS(X) ((X)<0? -(X) : (X))
#define ISSIGN(x) ((x)=='+' || (x)=='-' ?1:0)

3. 文件包含:#include

预处理器发现#include指令时,会查看后面的文件名并把文案金的内容包含到当前文件中,即替换源文件中的#include指令。相当于把被包含的文件的全部内容输入到源文件#include指令所在的位置。
它有两种形式:

#include
#include"stdlib.h"

文件名放在尖括号中或者双引号中都行。
而在UNIX系统中,尖括号告诉预处理器在标准系统目录中查找文件,而双引号告诉预处理器首先在当前目录中查找该文件。
C语言通常用后缀.h表示头文件。头文件中经常包含一些预处理器指令。

头文件示例

程序清单4
list4.h头文件,

#include
#define SIZE 32
struct names_st{
     
    char first[SIZE];
    char last[SIZE];
};
typedef struct names_st names;
void get_names(names *);
void show_names(const names*);
char * s_gets(char *st,int n);

list4.c源文件

#include
#include"list4.h"
void get_names(names* pn){
     
    printf("Please enter your first name: ");
    s_gets(pn->first,SIZE);
    printf("Please enter your lase name: ");
    s_gets(pn->last,SIZE);
}
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)
            *find='\0';
        else
            while(getchar()!='\n')
                continue;
    }
    return ret_val;
}

list5.c程序

#include
#include"list4.h"
//需要链接list4.c
int main(void){
     
    names candidate;
    get_names(&candidate);
    printf("Let's welcome ");
    show_names(&candidate);
    printf(" to this program!\n");
    return 0;
}

在ubuntu环境下编译时,要将list4.clist5.c一起编译,最终才能生成可执行程序。
命令如下

gcc list4.c list5.c -o list5

输出如下

root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# gcc list4.c list5.c -o list5
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# ./list5
Please enter your first name: jack
Please enter your lase name: love
Let's welcome jack love  to this program!
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16#

一般声明和指令放在头文件list4.h中,相关的函数定义放在list4.c源文件中,这个虽然不是硬性规定,但这样做比较有条理。也可以函数定义也放在头文件中,这样就不需要链接额外的源文件了,也可以直接编译运行。这个就看个人风格了。
头文件中常用的形式如下:

  • 明示常量
  • 宏函数
  • 函数声明
  • 结构模版定义
  • 类型定义

4. 其它指令

undef指令

用于取消已定义的#define指令。
如:#define LIMIT 400,用指令undef LIMIT会移除前面的定义,然后就可以将LIMIT重新定义新值。它的一个用途是,有时需要用某个名称,不确定前面有没有用过,安全起见可以用这个指令。

条件编译

#ifdef#else#endif指令

示例如下:

#ifdef MAVIS
	#include "horse.h"
	#define STABLES 5
#else
	#include "cow.h"
	#define STABLES 15
#endif

这个指令类似于if-else语句,但预处理器不识别花括号。这些指令结构也可以嵌套。

#ifndef指令

#ifdef用法类似
但这个指令是非常常见的指令。它的一个重要用途是防止一个文件被重复包含。有时一个文件中包含的文件中又包含这一些文件,所以就不可以避免的会重复的包含某些文件,这时当预处理器第二次及以后访问这些文件时应该告诉预处理器跳过这个文件,所以这个指令的典型用法是在头文件的开头用#ifndef+某个下划线开头的基本不会用到的标识符,然后,然后用#define定义这个标识符,在头文件的代码末尾加上#endif结束。这样整个头文件的代码都包含在这个指令内部。
stdio.h头文件中就采用这个用法。

#ifndef _INC_STDIO // include guard for 3rd party interop
#define _INC_STDIO
#endif

#if#elif指令

这个就和if语句很像了。如

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

编译器提供另一种方法测试名称是否引进定义,即用#if definedn (VAX)代替#ifdef VAX。它的优点是可以配合#elif使用。

预定义宏

如下表所示:

含义
__DATE__ 预处理的日期
__FILE__ 表示当前源代码文件名的字符串字面量
__LINE__ 表示当前源代码文件中行号的整型常量
__STDC__ 设置为1,表明实现C标准
__STDC_HOSTED__ 本机环境设置为1;否则设置为0
__STDC_VERSION__ 支持C99标准,设置为199901L;支持C11标准,设置为201112L
__TIME__ 翻译代码的时间

还有一个预定义标识符:__func__,为一个代表函数名的字符串。
程序清单6

#include
void why_me();

int main(void){
     
    printf("The file is %s.\n",__FILE__);
    printf("The date is %s.\n",__DATE__);
    printf("The time is %s.\n",__TIME__);
    printf("The version is %ld.\n",__STDC_VERSION__);
    printf("This is line %d.\n",__LINE__);
    printf("This function is %s\n",__func__);
    why_me();
    return 0;
}
void why_me(){
     
    printf("This function is %s\n",__func__);
    printf("This is line %d.\n",__LINE__);
}
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# ./list6
The file is list6.c.
The date is Aug 29 2020.
The time is 21:47:38.
The version is 201710.
This is line 9.
This function is main
This function is why_me
This is line 16.

#line#error

#line指令重置__LINE____FILE__宏报告的行号和文件名。如下

#line 1000
#line 10 "cool.c"

#error让预处理器发出一条错误信息,该消息包含指令中的文本,可能的话,编译过程应该中断。
可以这样用

#if  __STDC_VERSION__!=201112L
#error Not C11

#endif

#pragma

现在的一些编译器中,可以通过命令行参数或者IDE菜单修改编译器的一些设置。#pragma把编译器指令放入源代码中。如,开发C99时,标准被称为C9X,可以使用下面的编译指示让编译器支持C9X

#pragma c9x on

一般编译器都有自己的编译指示集。如编译指示可用于控制分配给自动变量的内存量,或设置错误检查的严格程度,或启用非标准语言特性。
C99提供_Pragma预处理器运算符,将字符串转换为普通的编译指示。如

_Pragma("nonstandardtreatmenttypeB on")

等价于

#pragma nonstandardtreatmenttypeB on

由于不用#,可以把它作为宏展开的一部分:

#define PRAGMA(X) _Pragma(#X)
#define LIMRG(X) PRAGMA(STDC CX_LIMITED_RANGE X)

这样可以用这样的代码

LIMRG( ON )

泛型选择 (C11)

C++可以在模板中创建泛型算法,但C不能这样。C11新增了一种表达式:泛型选择表达式,根据表达式的类型选择一个值。它不是预处理指令,但在一些泛型编程中常作为#define宏定义的一部分。
如:

_Generic(x,int :0,float :1,double:2,default :3)
#define MYTYPE(x) _Generic ((x), int:"int",float :"float",double:"double",default:"other")

5. 内联函数

函数调用包括建立调用、传递函数、跳转到函数代码并返回。这个过程具有一定的开销的。使用宏让代码内联可以避免这样的开销。还有另一种方法:内联函数。但,把函数变成内联函数意味着尽可能快地调用该函数,具体效果由实现定义。
有很多方法创建内联函数的定义,标准规定具有内部链接的函数可以成为内联函数,同时规定内联函数的定义和调用该函数的代码必须在同一文件中。最简单的方法是用函数说明符inline和存储类别说明符static。通常内联函数定义在首次使用它的文件中。

#include
inline static void eatline(){
     
	while(getchar()!='\n'
		continue;
}
int main(){
     
	...
	eatline();
	...
}

编译器查看内联函数的定义,可能会用函数体中的代码替换eatline()函数调用。

6. _Noreturn函数

表明调用函数后不返回主调函数

7. C库

math.h中包含许多有用的数学函数。
tgmath.c中定义了泛型类型宏。

8. 通用工具类

exit()atexit()

atexit()函数接受一个函数指针作为参数,可以注册要在退出时调用的函数。注册函数列表中的函数在调用exit()时会执行这些函数,执行顺序与列表中的顺序相反,最后添加的先执行。

qsort()函数

原型如下:

void qsort(void *base,size_t nmemb,size_t size,int (*compar)(const void*, const void*));

程序清单7

#include
#include
#define NUM 40
void fillarray(double ar[],int n);
void showarray(const double ar[],int n);
int mycomp(const void *p1,const void *p2);
int main(void){
     
    double vals[NUM];
    fillarray(vals,NUM);
    puts("Random list:");
    showarray(vals,NUM);
    qsort(vals,NUM,sizeof(double),mycomp);
    puts("\nSorted list:");
    showarray(vals,NUM);
    return 0;
}
void fillarray(double ar[],int n){
     
    int ind;
    for(ind=0;ind<n;ind++)
        ar[ind]=(double)rand()/((double)rand()+0.1);
}
void showarray(const double ar[],int n){
     
    int i;
    for(i=0;i<n;++i){
     
        printf("%9.4f ",ar[i]);
        if(i%6==5)
            putchar('\n');
    }
    if(i%6!=0)
        putchar('\n');
}
int mycomp(const void *p1,const void *p2){
     
    const double *a1=(const double *)p1;
    const double *a2=(const double *)p2;
    if(*a1<*a2)
        return -1;
    else if(*a1==*a2)
        return 0;
    else 
        return 1;
}
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# ./list7
Random list:
   2.1304    0.9808    4.6147    0.4364    0.5014    0.7591 
   0.7105    1.0393    0.8863    0.2333    0.0671    0.1706 
   0.3908    1.1928    4.5768    0.6113    2.0695    1.2160 
   0.5074    0.3792    0.6842    0.4489    0.8038    0.8789 
   0.0735    6.1123    0.2898    2.5516    3.2049    7.2541 
   0.2455    1.0603    0.4940    0.4935    0.7676   13.5337 
   0.4697    1.2911    0.3849    1.8076

Sorted list:
   0.0671    0.0735    0.1706    0.2333    0.2455    0.2898 
   0.3792    0.3849    0.3908    0.4364    0.4489    0.4697 
   0.4935    0.4940    0.5014    0.5074    0.6113    0.6842 
   0.7105    0.7591    0.7676    0.8038    0.8789    0.8863 
   0.9808    1.0393    1.0603    1.1928    1.2160    1.2911 
   1.8076    2.0695    2.1304    2.5516    3.2049    4.5768 
   4.6147    6.1123    7.2541   13.5337

9. 断言库

assert.h头文件支持的断言库是一个用于辅助调试程序的小型库。由assert()宏组成,接受一个整型表达式作为参数,表达式为假,assert()宏就会在标准错误流中写入一条错误信息,并调用abort()函数终止程序。如果它终止了程序,它首先会显示失败的调试、包含测试的文件名和行号。

10. 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);

我们通常不能直接将一个数组赋值给另一个数组,但对于字符串,string.h中提供了strcpy()strnpy()函数来处理,其中也有memcpy()memmove()这两个函数可以处理其他任意类型的数组。
两个函数的区别是memcpy()假设两个区域之间没有重叠,memmove()不做这样的假设。

11. 可变参数: stdarg.h

程序清单8

#include
#include
double sum(int,...);
int main(void){
     
 double s,t;
 s=sum(4,1.1,2.5,3.3,22.2);
 t=sum(5,1.1,2.2,3.3,4.4,5.5);
 printf("s=%g,t=%g\n",s,t);
 puts("Bye.");
 return 0;
}
double sum(int lim,...){
     
 va_list ap;
 double tot=0;
 va_start(ap,lim);
 for(int i=0;i<lim;i++){
     
     tot+=va_arg(ap,double);
 }
 va_end(ap);
 return tot;
}
s=29.1,t=16.5
Bye.

12. 编程练习

  1. 用#define定义一个宏函数计算调和平均数
#include
#define MEAN(x,y) 2./(1./x+1./y)

int main(void){
     
    printf("%g, %g, ==>%g",4.,5.,MEAN(4.,5.));
    putchar('\n');
    return 0;
}
4, 5, ==>4.44444
  1. 编写一个程序读取向量的模和角度,显示 x x x轴和 y y y轴坐标
#include
#include
typedef struct{
     double x;double y;} pos;
pos convert(double len,double ang);
int main(void){
     
    double x=10.,ang=45.;
    pos p=convert(x,ang);
    printf("Coordinate is (%g, %g)\n",p.x,p.y);
    return 0;
}
pos convert(double len,double ang){
     
    pos p;
    double a=ang/90.*asin(1.);
    p.x=len*cos(a);
    p.y=len*sin(a);
    return p;
}
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# gcc prac2.c -lm -o prac2
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# ./prac2
Coordinate is (7.07107, 7.07107)

这里需要说明一下,Linux下编译C文件的指令是gcc main.cgcc后面加要编译的源文件名,这样会生成当前目录下的可执行文件a.out,用命令./a.out就会执行,可以用-o参数指定文件名:gcc main.c -o main
通常情况下这样就可以的,但用gcc prac2.c -o prac2时会编译失败,报下面的错:

root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# gcc prac2.c -o prac
/usr/bin/ld: /tmp/ccqxbyoO.o: in function `convert':
prac2.c:(.text+0xc0): undefined reference to `cos'
/usr/bin/ld: prac2.c:(.text+0xd8): undefined reference to `sin'
collect2: error: ld returned 1 exit status

说明编译器链接器没有找到数学库。这时要用-lm标记指示链接器搜索数学库。所以编译指令为:

gcc prac2.c -lm -o prac2
  1. 利用time.h中的clock()函数来计算程序的运行时间。
#include
#include
#include
int main(void){
     
    clock_t start=clock();
    long unsigned sum=0;
    for(int i=0;i<10;i++){
     
        for(int j=0;j<1000;j++)
            for (int z=0;z<1000;z++)
                for(int k=0;k<1000;k++)
                    sum=(sum+1)%100000;
        printf("i=%d\n",i);
    }
    printf("sum is %lu\n",sum);
    printf("The program has spend %ld second running.\n",(clock()-start)/CLOCKS_PER_SEC);
    return 0;
}
root@DESKTOP-624LRQ5:/mnt/d/Program Files/code/c16# ./prac3
i=0
i=1
i=2
i=3
i=4
i=5
i=6
i=7
i=8
i=9
sum is 0
The program has spend 45 second running.

用四重循环一共计算了 1 0 10 10^{10} 1010次表达式,一共花了45秒的时间,这个速度还是相当块的。
4. 用rand()初始化一个数组,从数组中随机抽取一些元素模拟抽奖。

#include
#include
#define LEN 100
void r_get(int* arr,int len,int num);
int main(void){
     
    int arr[LEN];
    for(int i=0;i<100;i++){
     
        arr[i]=rand()%100;
    }
    r_get(arr,LEN,10);
    return 0;
}
void r_get(int *arr,int len,int num){
     
    int ind;
    for(int i=0;i<num;i++){
     
        ind =rand()%len;
        while(arr[ind]==-1)
            ind=rand()%len;
        printf("%d,selected value is %d, index=%d\n",i+1,arr[ind],ind);
        arr[ind]=-1;
    }
}
1,selected value is 12, index=95
2,selected value is 24, index=70
3,selected value is 69, index=34
4,selected value is 43, index=78
5,selected value is 29, index=67
6,selected value is 86, index=1
7,selected value is 86, index=97
8,selected value is 77, index=2
9,selected value is 26, index=17
10,selected value is 76, index=92

你可能感兴趣的:(C/C++,字符串,linux)