C语言C预处理器和C库
/*---------以下为commonheader.h文件--------*/
#pragma once //pragma用于设置编译器指令,修改编译器的一些设置,pragma once使这个文件只会被包含一次,但如果别的文件中有与这个文件相同的代码块依然会冲突,这个指令不受C标准支持,不可移植
// 预处理指令以#开头
#define CLEANINPUT() while(getchar()!='\n')continue //定义类函数宏,cleaninput()宏在预处理阶段会替换为while循环(内联代码),实际运行不会发生函数调用,但每个cleaninput()宏的替换都会产生这段相同的代码,使占用变大,而函数调用则只会有一份代码,但每次调用函数更耗时,只有在多次调用(循环)时类函数宏和函数才会有时间、空间区别
#ifndef _CRT_SECURE_NO_WARNINGS //ifndef如果没有定义后面跟着的宏/标识符 则执行下面直到#else或#endif(先遇到任意一个截止)之间的语句
#define _CRT_SECURE_NO_WARNINGS //常用方法,为避免重复宏名,先判断再定义,可以用缩进表示层级,缩不缩没有影响
#endif //预处理器的if必须用endif结束
#ifdef DEBUG //如果声明了DEBUG则执行下面内容
#define INFO() printf("%s %s %s %d",__DATE__, \
__TIME__, \
__FILE__, \
__LINE__\
) //预定义宏__DATE__替换为预处理器执行到这里时(预处理时)的日期字符串字面量,__TIME__替换为时分秒字符串字面量"hh:mm:ss",__FILE__替换为当前源代码文件字符串字面量,__LINE__替换为当前行号 整型 常量,注意这些预定义宏在每个调用的地方单独替换,比如xxx.c第20行使用了__FILE__和__LINE__则替换为xxx.c 20,yyy.c第13行使用则替换为yyy.c 13,由于预处理器在编译阶段之前,所以不会替换成最终可执行文件名以及实际打开文件时间
//预处理器按行处理,在行末尾使用\加换行(两者紧挨着)使编译器在预处理之前将多行物理行处理为一行逻辑行,上面五行会去掉\\n合并为一行,并且每行要和首行左对齐,如果有缩进会被视为空白符号
#else //if不满足走else
#define NODEBUG
#endif // DEBUG
#define AVG(X,Y) ( 1/( ( 1/(X)+1/(Y) )/2 ) ) //类函数宏传参,传入的实参会替换表达式中的XY,,因为预处理器不做计算,如果X为5+3则会直接代入表达式,既不会先计算处理成8也不外套括号,所以需要对每个参数手动加括号保证语义正确,同理整个替换体也需要括起来保证作为一个整体来执行
//宏的参数不要使用递增,由于预处理器不计算直接替换,所以当有多个替换时会产生多个x++,使变量的值不止+1
#define PI (4*atan(1.0)) //C数学库中没有定义PI,角度的180度对应弧度PI,arctan(1)是45度即PI/4,格式: #define 宏名 替换体 ,宏名遵循变量命名规则,替换体可以为空
#define DEGREE_TO_RADIAN (PI/180) //角度除以180乘以PI = 弧度 ,上面定义的PI会替换掉这行的PI,即((4*atan(1.0))/180)
#define STRINGPI "PI" //用双引号引起来的部分不会被宏替换,要表示PI对应的值的字符串需要使用格式化字符串进行转换
#define PR1(X,Y) printf(#X" = %.2f\n"#Y" = %.2f\n",(X),(Y)) //#X将参数X转化为字符串,相当于在X两侧加了双引号,编译时多段字符串会合并,如果直接写"X"会将其看做字符而不是参数,不进行替换
#if XYZ == 1 //后面跟整形常量表达式,非0为真,不能使用上面通过define定义的PI,因为替换体中的atan()函数需要到程序执行阶段才能计算值,预处理阶段得不到表达式PI的值
#define Z 1
#elif defined(XXMOD) //elif配合if使用,#if defined(...)等价于#ifdef ... ,好处是#if 可以使用#elif
#include "XX.h"
#endif // XYZ ==1
#ifndef COMMONHEADER_H //通常避免头文件重复包含的方法,将整个头文件包裹在ifndef内,用头文件的名字作为符号常量并用_代替. 头文件内定义的宏也可以拼上文件名防止名字冲突比如COMMONHEADER_H_PR()
#define COMMONHEADER_H //使用头文件名作为符号常量查看是否已被包含,没有定义过则执行包含,并且定义该符号常量
#endif // !COMMONHEADER_H
//预定义宏__STDC__设置为1时表明实现遵循C标准,__STDC_HOSTED__本机环境设置为1否则设置为0,__STDC_VERSION__设置为199901L以支持C99标准,设置为201112L以支持C11标准
#line 200 //把当前行号重置为200
#line 220 "cool.c" //把当前行号重置为220,文件名重置为cool.c
//#if __STDC_VERSION__ != 201112L
// #error Not C11 //error指令让预处理器发出一条错误消息,消息包含指令中的文本,并应该中断编译
//#endif
_Pragma("nonstandardtreatmenttypeB on") //等价于#pragma nonstandardtreatmenttypeB on 好处是没有使用#可以放在其他宏中
#define SHOWTYPE(X) printf("Type of "#X" is %s",_Generic((X),int:"int",double:"double",long:"long",default:"other")".\n") //在参数X前面加一个# (#X)将参数按字面转换为字符串,编译阶段将多个相邻的字符串串联起来
//_Generic()泛型选择(C11) 判断参数1的类型,不计算参数1的具体值,后面的参数为 类型名:值 的形式,参数1的类型匹配某一个类型名标签时,整个泛型选择表达式的值为标签后面的值,值也可以是表达式,该例中char类型会匹配到default,这时printf函数的参数2变为了"other"".\n"再合并为一个字符串
//_Noreturn void quit(void) //_Noreturn函数说明符(c11),告诉用户和编译器该函数不会将控制返回主调函数,exit()是_Noreturn函数的一个实例
#define XNAME(n) x##n //如果替换体写作xn则不会替换n为实参,预处理器会将xn作为一个标记看待,这和&n *n等使用运算符加参数名不同,预处理器能够识别运算符,所以如果想表示变量名x1,则要使用##运算符(预处理器黏合剂)连接x和参数n,即将x和n看作两个标记(使参数n能够替换),然后连在一起
inline static void eatline() //inline内联函数,编译器可能会用内联代码替换函数调用以尽可能快地调用该函数,可能会执行一些优化也可能不起作用,一般内联函数需要定义为static内部链接(即在同一个翻译单元内使用),内联函数不能获得函数地址,因为如果得到了函数地址,编译器会生成非内联函数
{
//将内联函数写在头文件时应写完整的函数定义,一般不在头文件中放置可执行代码,内联函数是特例
while (getchar() != '\n')
continue;
}
/*---------以下为preprocessor.c文件--------*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h> //尖括号指示编译器去标准库中查找文件
#include <ctype.h>
#include <math.h>
#include <time.h>
#include <stdlib.h>
#include <assert.h> //断言库,assert断言,用于辅助调试
#include <string.h> //include指令将头文件的代码替换至当前位置,但编译结果只保留实际调用的部分
#include <stdarg.h>
#define DEBUG //define用来定义明示常量/符号常量,
#include "commonheader.h" //双引号指示编译器从当前目录/源文件目录/IDE指定目录等位置寻找,找不到再去标准库中查找,双引号中实际为相对路径例如"/usr/biff/p.h"表示在当前目录的usr文件夹的biff文件夹里的p.h文件
//在预处理之前,编译器必须对程序进行翻译处理,首先映射字符集(utf-8 gbk等),第二定位每个反斜杠(\)后面跟着换行符的实例并删除他们,将两个物理行转换成一个逻辑行,第三编译器会将每一条注释替换为一个空格,将多个连续空白替换为1个空格(换行符除外)
struct xy //直角坐标
{
double x;
double y;
};
struct rA //极坐标
{
double r;
double A;
};
struct xy coordinate(struct rA* ra);
void testclock_t(void);
void random_choice_int(const int arr[], int size, int num);
void test_qsort(void);
int str_comp(const void* p1, const void* p2);
double* new_d_array(int n, ...);
void say_bye(void);
void other(void);
struct xy coordinate(struct rA* pra) //极坐标转换为直角坐标
{
struct xy pxy;
pxy.x = cos(pra->A) * pra->r;
pxy.y = sin(pra->A) * pra->r;
// sin cos tan asin acos atan均为接收1个参数,当xy都为负数,atan的参数是正数返回值有误,需要改为使用atan2(x,y)两个参数的版本
return pxy; //在被调函数中创建的变量/数据对象会随被调函数结束而释放,如需传给主调函数则要传回变量的值,或从主调函数接收一个地址来存储值,所以传回pxy的地址是错误的,因为pxy的内存已经释放了,该地址的值可能已经被改变了,而且该地址也已经不再注册为程序使用
}
#define MINUS(X,Y) ((X)-(Y)) //define对定义行往下的区域有效
void testclock_t(void)
{
clock_t cpu_time1 = clock(); //clock_t定义在time.h头文件中的类型,clock()返回clock_t类型的当前处理器时间(如果不可用返回-1)
double d = 3e20;
for (int i = 0; i < 100000; i++)
{
d = sqrt(d); //square root平方根
d = pow(d,2);
}
clock_t cpu_time2 = clock();
printf("time2-time1=%g", (double)MINUS(cpu_time2, cpu_time1) / CLOCKS_PER_SEC); //time.h中的常量CLOCKS_PER_SEC每秒钟的处理器时间单位的数量
}
#undef MINUS(X,Y) //undef 取消指定的宏,该宏在这一行之前有效(上面的函数可以使用),这一行之后无效(无法调用),在下面可再次定义该名称的宏,并可以赋予不同的操作
void random_choice_int(const int arr[], int size, int num) //从数组中随机选择num个元素
{
assert(size > num); //断言数组长度大于选取元素的数量,合法性检查,如果条件为false会调用abort()函数(stdlib.h定义的函数)中止程序并显示错误的文件、函数、行号、该行代码的内容
assert(num > 0); //也可以通过if(){abort();}来中止
//当使用了#define NDEBUG 会禁用文件中所有assert()语句,方便调试
_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed"); //_Static_assert在编译阶段检查,判断参数1整型常量表达式,如果false编译器会显示参数2字符串并且不编译程序
srand(time(0));
int choice;
int** pa = (int** )malloc(size * sizeof(int*));
for (int i = 0; i < size; i++)
{
pa[i] = arr + i;
}
for (int i = 0; i < num; i++)
{
choice = rand()%(size-i);
printf("%d ", *pa[choice]);
pa[choice] = pa[size - 1 - i];
}
putchar('\n');
free(pa);
}
struct names
{
char first[20];
char last[20];
};
#define LISTSIZE 10 //尽量对每一个有特殊意义的常量使用define以提高可读性
#define RANDOM_FROM1TO(X) (rand()%(X)+1)
#define GET_RANDOM_LETTER() "qwertyuiopasdfghjklzxcvbnm"[rand()%26]
#define SHOW_LIST() for(int i = 0; i<LISTSIZE;i++)printf("[%s %s] ",list[i].first,list[i].last)
void test_qsort(void)
{
srand(time(0));
struct names list[LISTSIZE];
int len;
for (int i = 0; i < LISTSIZE; i++)
{
len = RANDOM_FROM1TO(LISTSIZE-1);
for (int j = 0; j < len; j++)
{
list[i].first[j] = GET_RANDOM_LETTER();
}
list[i].first[len] = '\0';
len = RANDOM_FROM1TO(LISTSIZE-1);
for (int j = 0; j < len; j++)
{
list[i].last[j] = GET_RANDOM_LETTER();
}
list[i].last[len] = '\0';
}
SHOW_LIST();
putchar('\n');
qsort(list, LISTSIZE, sizeof(struct names), str_comp); //qsort()快速排序,参数1是数组地址,参数2是元素个数,参数3是元素大小,参数4是比较器(函数),需要程序员自定义比较器/比较规则,qsort使用比较器比较两个元素,如果返回正数则交换位置
//qsort根据参数3来确定每个元素的大小,或者说每次移动的字节数,将参数3指定的大小作为一个数据块,按比较器的结果从小到大排序
SHOW_LIST();
}
int str_comp(const void* ps1, const void* ps2) //自定义比较器,比较器的函数签名必须和qsort规定的比较器签名一致,所以即便要比较的是结构,也要设定形参为const void*,比较器要返回int,接收两个const void*参数
{
const struct names* psn1 = (const struct names*)ps1; //对const void*指针进行强转,强转也需要带上const关键字
const struct names* psn2 = (const struct names*)ps2; //对const void*指针进行强转,强转也需要带上const关键字
int res = strcmp(psn1->first, psn2->first); //借助strcmp完成字符串比较
if (res==0)
{
return strcmp(psn1->last, psn2->last);
}
return res; //返回正数即告诉qsort函数交换这两个元素
}
double* new_d_array(int n, ...) //可变参数的函数,在参数列表的最后添加...作为可变参数的标志 ,在可变参数前面添加int类型参数用来接收可变参数的数量,即需要从主调函数传递当前调用的实际可变参数的数量
{
va_list ap; //声明在stdarg.h中的va_list类型代表一种用于储存形参对应的形参列表中省略号...部分的数据对象,即使用va_list类型变量接收可变参数部分
va_start(ap, n); //va_start()宏,参数1为va_list变量,参数2为可变参数的数量(通过主调函数获得)
double* dynarr = malloc(n * sizeof(double)); //由于被调函数定义数组会随函数结束而释放,但动态分配的内存可以在函数之间传递,最终需要在某处使用free(动态内存地址)来释放,通常可以从主调函数接收一个数组用来存放元素,这样该数组为主调函数的自动变量,不需要手动释放
for (int i = 0; i < n; i++)
{
dynarr[i] = va_arg(ap, double); //va_arg()宏从参数1 va_list变量中逐个取出参数(按实参从前往后顺序),参数2为该参数的类型名,类型名必须正确,因为宏不会进行类型转换,类型不正确解读的结果可能不正确
}
va_end(ap); //清理工作,调用该函数后只有用va_start重新初始化ap才能使用,可以在初始化ap后使用va_copy(va_list apcopy,va_list ap)拷贝备份,va_copy会拷贝ap当前状态
//调用new_d_array(5, 1.0,2.0,3.0,4.0,5.0),省略号前面的int n称为parmN,需要和后面实际传参的数量对应,不包含前面的普通参数
return dynarr; //return会将dynarr的值返回,变量dynarr会释放,主调函数可以直接使用值(一次性),也可以声明一个变量接收值(保存多次使用)
}
void say_bye(void)
{
puts("bye");
}
#define PR(...) printf(__VA_ARGS__) //变参宏,在宏的参数列表中使用...(前面可以有普通参数,...必须放在最后一个参数的位置),在替换体中使用__VA_ARGS__表示变参部分的参数
void other(void)
{
atexit(say_bye); //atexit()函数用于注册程序退出时(exit())执行的动作,接收无返回值无参数的函数地址,并且遵循后进先出,后注册的先执行
//由于main函数return 0 即为隐式exit()所以即便没有显式exit(),注册过的动作也会执行,C标准规定注册列表最小为32个函数
// exit()执行完atexit()指定的函数后会完成一些清理工作:刷新所有输出流、关闭所有打开的流和由标准io函数tmpfile()创建的临时文件,然后把控制权返回主机环境并报告中止状态(EXIT_SUCCESS/EXIT_FAILURE)
//memcpy(dest,src,size)将src的size大小数据块拷贝至dest位置,由于拷贝不使用缓冲区,要求dest和src不能有重叠,比如从src+1拷贝到src应使用memmove(),会将数据先拷贝到创建的缓冲区再拷贝到dest
PR("%d\n%s\n", 20, "asd"); //预处理时变参分别接收一个字符串参数、一个int参数和一个字符串参数,将三个参数替换到__VA_ARGS__所在位置,并且用逗号分隔
exp(10); //math.h中的函数,返回e^参数,指数函数的值
log(20.0); //math.h,返回 参数的自然对数值
log10(100.0); //math.h,返回以10为底的对数值
//pow幂运算,sqrt平方根,cbrt立方根,fabs绝对值,ceil(x)返回大于等于x的最小整数,floor(x)返回小于等于x的最大整数
//sqrtf为sqrt的float版本,sqrtl为long double版本,在包含tgmath.h(c99)头文件时会将这三个函数组合成一个宏sqrt()自动匹配实参类型的函数,但这时宏sqrt()盖住了函数sqrt(),如果想要直接使用函数sqrt()可以写作(sqrt)(),用括号先运算sqrt,使其表示函数地址再接括号表示函数调用
}