C day16 预处理器(二)

其他预处理指令

undef

在这里插入图片描述

注意这里说的已定义是预处理角度看的哈,预处理器看标识符也是遵循C变量命名准则,但是由于预处理器比编译器先看程序,且只看预处理指令,所以如果预处理指令中的标识符和程序中的有文件作用域的变量相同的话,预处理器仍然会认为这个标识符是未定义的。

只有预处理指令中有两个同名标识符,其中一个才会被认为是未定义的。

条件编译指令 condtional compilation

#ifdef #else #endif

注意ANSI C支持预处理器缩进格式哈,即#前面可以有tab符。

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

示例

#include <stdio.h>
#define JUST_CHECKING
#define LIMIT 4

int main()
{
    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 // JUST_CHECKING
    }
    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

如果注释掉#define JUST_CHECKING或者#undef JUST_CHECKING,就只会打印

Grand total = 64

#ifndef #else #endif

#ifndef 是如果没有定义,if not define的意思,也和#else, #endef结合使用,但是逻辑相反。

#ifndef SIZE
	#define SIZE 100
#endif

它很有用。

用处1:防止宏被重复定义

包含多个头文件时,这些头文件中可能有重复的宏定义,如果所有头文件中宏定义都用#idndef和#endif包裹起来,就不会重复定义宏了。一旦第一个头文件的该宏定义被激活,其他后续头文件中的该宏定义就会被忽略。

用处2:防止宏定义被篡改

比如,"array.h"头文件中定义了SIZE为100:

#ifndef SIZE
	#define SIZE 100
#endif

在自己的程序顶部写,

#define SIZE 10
#include "array.h"

则程序包含了array头文件,但不会把SIZE定义为100,可以用自己定义的10;如果不需要自己定义的SIZE,直接#undef或者注释掉,就把SIZE设置为100了

如果没有#ifndef,SIZE就是100,也许你还莫名奇妙呢,不知道是头文件里定义的

用处3:防止重复包含一个文件

你肯定不会在一个文件中显式包含一个头文件多次,但是很可能隐式包含多次,比如你包含了头文件A,B,而A包含了文件B,导致两次包含文件B。

所以用#ifndef和#endif把头文件中的定义包裹起来,比如结构,类型,就可以避免多次出现
在这里插入图片描述

自己不要用下划线或两条下划线作为前缀,避免和标准头文件定义的宏冲突

示例

主程序

#include <stdio.h>
#include "names.h"
#include "names.h"//不小心包含两次头文件

int main()
{
    names winner = {"Less", "Ismoor"};
    printf("The winner is %s %s.\n", winner.first, winner.last);

    return 0;
}

头文件,注意#endif放在头文件末尾

#ifndef NAMES_H_
#define NAMES_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);
#endif

输出

The winner is Less Ismoor.

如果去掉头文件开始的两句指令和最后一句指令,则报错,一是说重复定义struct names_st结构类型,二是所有的函数名已经是已定义的了。
在这里插入图片描述

#if #elif

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

前面说的#ifdef 可以用#if defined 代替,这样代替的好处是可以用#elif配套使用

#if defined (VAX)
	#include "vax.h"
#elif defined (IBMPC)
	#include "ibmpc.h"
#elif defined (MAC)
	#include "mac.h"
#else 
	#include "general.h"
#endif

defined是一个运算符,如果后面的标记已定义,则返回1,否则返回0

这种条件编译可以使得程序可移植,不同的系统会包含不同的文件。

预定义宏

在这里插入图片描述

示例

#include <stdio.h>
void why_me();

int main()
{
    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__);//当前行号,整型int
    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__);
}
The file is C:\Users\wulimmya\Documents\MyCCode\MyC\main.c.
The date is Feb 20 2020.
The time is 15:01:58.
The version is 201112.
This is line 10.
This function is main
This function is why_me.
This is line 20.

#line

可以重置行号,但是不知道用处是啥???

#include <stdio.h>
void why_me();

int main()
{
    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__);//版本是长整型!!!
    #line 50
    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__);
}
The file is C:\Users\wulimmya\Documents\MyCCode\MyC\main.c.
The date is Feb 20 2020.
The time is 15:07:26.
The version is 201112.
This is line 50.
This function is main
This function is why_me.
This is line 60.

#error

预处理器发出错误消息,注意不是编译器发

我的编译器支持的是C11标准
在这里插入图片描述

如果把代码改为

 #if __STDC_VERSION__ !=201112L

就不报错,成功编译

#pragma

这个指令是提供编译指示的,即是用于修改编译器设置的,平时我们可以用命令行参数或者IDE的菜单去修改编译器的设置,比如使用什么C标准,可以选C99或者C11,但是也可用#pragma指令在源代码中给编译器做出指示哦。

pragma这个单词本来就是编译指示的意思。

在这里插入图片描述在这里插入图片描述在这里插入图片描述

由于编译指示比较高阶,C primer没有深入描述,我以后再加深这方面的学习吧

_Pragma预处理器运算符(C99)

在这里插入图片描述在这里插入图片描述在这里插入图片描述

泛型选择表达式(C11)

generic selection expression

在这里插入图片描述在这里插入图片描述

重点:

  • C没有泛型编程的功能。
  • 泛型选择表达式不是预处理器指令。
  • 泛型选择表达式和泛型编程没有什么关系,只是都泛了类型,即可以允许多种类型,这里很不容易说清,看代码。
/*泛型选择表达式和宏定义结合*/
#include <stdio.h>
//这里每一行的反斜杠不可省略,否则不是一个逻辑行,报错
#define MYTYPE(X) _Generic((X),\
                           int: "int",\
                           double: "double",\
                           int *: "int *",\
                           default: "other type")
int main()
{
    int d = 4;

    printf("%s\n", MYTYPE(d));
    printf("%s\n", MYTYPE(&d));
    printf("%s\n", MYTYPE(d*2.0));
    printf("%s\n", MYTYPE(4L));

    return 0;
}
int
int *
double
other type

通过示例程序看到,实际上泛型选择表达式很像switch语句,只不过它的case是表达式的类型而已。

内联函数inline function(C99)

函数调用会产生一定开销,这里说开销主要是说在时间上,建立调用,主调函数压栈,跳转到被调函数的代码块,执行被调函数代码块,再跳转回到内存中存储主调函数的代码块。

如果函数体非常短小,在内存中跳来跳去的时间都比执行函数体时间长的话,实际上这个开销是很不划算的,所以当函数体很短时, 我们应该想办法去尽量减小这种函数调用产生的开销。(对于函数体很长的函数就不用管了,函数体执行时间远远长于调用,就算用后面说的方式优化,益处也不是很明显。所以说,要不要优化,主要是看函数调用时间和函数体执行时间的相对比例。)

那么函数体短小,调用函数的开销很大的这种情况,该怎么优化呢?

有两种办法,一是用宏函数,宏函数实际不是函数,只是调用和定义的形式很像函数而已,它通过内联代码避免函数调用,内联代码实际上就是替换的意思,不要觉得很难理解,就是文本替换,所以宏函数的函数调用根本没有涉及函数调用那一系列的压栈,跳转,执行,跳回,出栈·····,这就避免时间开销啦

另一种办法就是这一节的主角:内联函数。
它真的是函数,不像宏函数只是像函数,它本质上是货真价实的函数。但是相比于普通函数,他当然又有很多特殊之处:

  1. 把函数定义为内联函数,只需要在函数定义中使用inline关键字。并且内联函数不需要再单独写原型,它的定义就是原型,原型就是定义
  2. 内联函数必须具有内部链接,即他必须是在所有函数外部用static定义的,具有文件作用域的内部链接函数。(注意static是存储类别说明符,而inline是函数说明符)。正是由于内联函数的内部链接,多个文件都写相同的内联函数定义也不会相互影响。
  3. 内联函数的定义和调用必须在同一文件中,因为编译器优化内联函数时必须要能看到内联函数的定义。所以最方便的做法是把内联函数的定义写在头文件中,然后包含该头文件即可。内联函数是头文件中唯一一种可执行代码
  4. 编译器查看内联函数的定义或说成是原型时,可能会用它的函数体中的代码去替换内联函数的调用语句,即相当于在调用内联函数的地方插入内联函数的函数体,即内联函数的作用机制还是替换,但是编译器也可能不替换。(不替换的话,这个调用难道就不执行了?还是说就老老实实像函数调用那样跳来跳去呢,可是不是不给内联函数分配代码块吗,去哪里找他呢?)
  5. 编译器不会在内存中给内联函数分配单独的代码块,所以我们不能获取内联函数的地址。如果非要获取,那么编译器就只好给它分配一个单独代码块咯,则他也就不是内联函数而是普通函数了。所以说,内联函数不仅可以避免函数调用的时间开销,还可以节省内存呢,因为不用存它。
  6. 内联函数不能在调试器显示,因为编译器没有把它存在内存里,调试器没法找到它。
  7. 内联函数应该尽量短小。像上面说的,如果函数体很长,那么执行函数体花的时间多的多,那点调用开销就不算什么了。

示例

inline 和 static一定会同时出现

//eatline.h
#include <stdio.h>
#ifndef EATLINE_H_
    #define EATLINE_H_

inline static void eatline()
{
    while(getchar()!='\n')
        ;
}
#endif // EATLINE_H_

示例 内联函数和外部函数混合使用(C++不允许这么做)

//file1.c
#include <stdio.h>
inline static double square(double);
double square(double x){return x * x;}
extern void spam(double);
extern void masp(double);

int main()
{
    double q = square(1.3);//使用本文件定义的内联函数
    printf("file1  q = %.2lf\n", q);

    spam(1.3);
    masp(1.3);
}
//file2.c
#include <stdio.h>
//普通函数定义,外部链接
double square(double x){return (int)(x * x);}
void spam(double v)
{
    double kv = square(v);
    printf("file2  kv = %.2lf\n", kv);
}
//file3.c
#include <stdio.h>
//没写static,具有外部链接
inline double square(double x){return (int)(x*x + 0.5);}
void masp(double w)
{
    double kw = square(w);//可以调用自己文件定义的square函数,也可以调用file2定义的,因为file2定义的那个square函数是外部链接的
    printf("file3  kw = %.2lf\n", kw);
}

从结果看,file3中的square函数调用的是file2的square定义,并不是file3的,因为如果是file3的应该输出2.00

file1  q = 1.69
file2  kv = 1.00
file3  kw = 1.00

_Noreturn 函数(C11)

no return

顾名思义,用它告诉用户和编译器,该函数不会返回主调函数,而是直接退出程序。

_Noreturn 和 inline一样,都是函数说明符。(inline是C99出来的),即他俩只能用于函数。

而extern, static是存储类别说明符,他们可以用于函数,也可以用于数据对象。

之前用的exit()函数就是_Noreturn函数,其实main()函数也是用exit()函数退出系统的,因为操作系统调用了main函数,main执行结束后不需要返回操作系统,而是直接退出,但是返回值还是要返回给操作系统过目的 。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值