浅析C++预处理命令

1. 概述

预处理命令就是我们程序开头以#字符开头的命令。为什么叫预处理命令?因为这些命令是在编译时的第一步就执行了的,不会转为汇编码。

编译器编译代码的步骤:

  1. 预处理。处理#include,#define等命令并删除注释,所以无论怎么写都不会再第一步CE。
  2. 编译。真编译会分析代码语法(开了O2还会改一些)并生成汇编文件。
  3. 汇编。将汇编码转为机器码。
  4. 链接。根据电脑情况进行重定位,链接库等,生成可执行文件

使用-E-S-c可以选择只执行第1步,1~2步,1~3步。如果对本文的知识有疑惑,您可以选择使用g++ -E 1.cpp -o 1.i来获取预处理后的.i文件深刻体会。另外-S也可以用于获取汇编码。

绝大部分预处理命令在OI里用处不大,但也有功能强大的预处理命令。

#符号应该是这一行的第一个非空字符。不过,也可以打\把内容移到下一行,就跟注释一样。

#define pi 3.14159 26535
//This is an example

这样就把下一行内容上移了。

洛谷的编辑器不会这么显示,但本地编辑器上你能发现下一行也变成了注释或预处理样式。

常见的预处理命令如下:

#include 包含头文件
#ifdef 或 #if defined 如果定义了一个宏, 就执行操作
#ifndef 或 #if !defined 如果没有定义一个宏,就指执行操作
#define 定义一个宏
#undef 删除一个宏
#pragma 自定义编译器选项,指示编译器完成一些事

这里介绍3个最常用的预处理命令:#include#define#pragma

2. #include

这是最常见的文件包含命令。

无论你再厉害,什么东西可以手写,也需要#include <cstdio>

命令本质是把指定的文件中的函数,变量,宏等全部导入,可以理解成把那个文件全部内容复制粘贴到你的代码里了。

不过,如果是单纯的粘贴,#include两遍应该会有重复定义CE才对。但是标准库使用宏定义避免了这一点(参见后文)。自己写头文件时也要注意。

Question 0: #include必须接尖括号吗?

事实上,#include命令不一定要使用尖括号,使用引号也是完全可以的。

区别在于引号会优先在要编译的文件中找,没找到才会调用标准库里的文件。

当然对于OIer来讲,#include <cstdio>#include "cstdio"就没有任何区别了,但是此时尖括号更为规范。

在自己用C++开发小游戏时,为了便于管理,可以像标准库一样把用途相似的函数单独用一个文件保存。在需要时就将其包含,此时就需要用到引号了。

Question 1: 为什么引用标准库的头文件时不加.h?

在C语言中其实是要加的,只能写#include <stdio.h>或者#include <math.h>

C++里把这些文件的后缀名去掉并在前面加了一个c比如#include <cmath>

但是这些传统的库你如果使用老写法,仍然是可以过编译的,只是不规范。

但是对于C++的新内容(比如iostreamstack)就不能加.h了。

有人试了,会说#include <string.h>能用!但是string.h对应的是C语言里的cstring库而不是C++新增的那个string。使用前者是定义不了string类型的。cstring库是提供一些内存操作的函数和char数组的函数比如memset,memcpy,strlen。

Question 2: 万能头文件真的万能吗?

现在的NOI)(P已经支持万能头文件#include <bits/stdc++.h>

(注意是正斜杠不是反斜杠,写错了有可能CE)

事实上他包含的东西你是不可能记完的,但是您能用到的东西里面绝对都有。

C++11里还新包括了randomunordered_map等库。
详见stdc++.h原文件

虽然说不上万能,OI里的确完全够用了。

辟谣!!!万能头文件并不会减慢程序运行速度,内存上的增加几乎可以忽略。在编译时main里没有用到的东西就会被优化掉。

而且你随时带上十几个头文件,又在说万头不好,根本没说服力

当然有可能增加编译时间和源程序大小,然并卵

Question 3: 为什么我看到别人有在程序中途包含文件的神奇操作?

之前说过#include的本质是把指定文件复制进这一行,所以如果是在函数内写的这个命令,就只对这一个函数有效。

void func()
{
    #include "test.h"
    mmm();//可以使用test.h里的函数
}

int main()
{
    func();
    mmm()//CE。不能使用test.h里的函数。
}

但是OI里不能这么用,因为标准库还涉及到命名空间的问题。

Quetion 4: 自己写的头文件到底是怎么用的?

按照标准的话,.h用于存放大篇的宏定义和函数,变量的声明(也就是函数第一行的函数名和参数列表),而同名的.cpp则存放函数的具体实现。.h里写一个#include "test.cpp"。主程序只要包含test.h就可以使用库里的函数了。

不过为了节省工作量,我们可以在.h里就直接定义好函数,也可以选择在主程序里直接#include "test.cpp"。包含命令的本质是复制粘贴,这样写也是完全没有问题的。

使用万能头文件不要用的变量名:y1, next, time, rand

包括很多常见单词最好都不用,有些Windows可以,但是评测时会CE。

3. #define

命令#define 叫做宏定义,用于代码中的字符串替换。是最有用的预处理指令

1. 不带参数的宏

#define MAX 10000
if (9874 > MAX)
    return 0;

上述代码定义宏MAX,这句以后的"MAX"就代表10000。if中的式子为false。

该方法可用于替代const定义常量,而且只做了代码替换,运行时不占用空间。也可以用于简化标准库里名字超长的函数。

另外如果这个常量需要多次进行运算(比如模数),据说写成const是更快的,经过个人不完全测试的确是这样的,但是效率差别很小,所以也不必过多在意,还是看自己更喜欢哪种写法。

注意:

1. #define不会替换字符串和注释中的宏(废话)

2. 替换宏时需要完全匹配,如定义宏“super”后,“supermarket”不会被部分替换。

2. 带参数的宏

事实上,宏跟函数一样,可以带有参数。

例:用圆的半径求其周长和面积。

#define pi 3.14159
#define AREA(i) i*i*pi

double d;

int main()
{
    cin >> d;
    cout << AREA(d)<< endl ;
    return 0;
}

我们把宏写成AREA这种像函数的形式,之后出现AREA(i)时,
先发现括号里为2,即i=2,然后再做替换。

由于只做字符串替换,所以#define不仅可以定义常量,还可以定义表达式,函数,甚至代码段。

#define sum(a,b,c) (a)+(b)+(c)
#define max(a,b) (a>b)?(a):(b)
#define fors(a,b) for(int i=(a);i<=(b);i++)

利用宏定义可以使代码更加简洁易懂,同时用#define定义max等函数。速度快于函数,但也没快多少。

注意:

命令#define命令后第一个单词为宏,其余为宏体。

#define int long long
#define abc def ghi jkl
#define register

在第一句中,第一个int为替换体,即以后int代表long long。

在第二句中,只有abc作为宏体,之后的abc被替换为def ghi jkl,反斜杠只有换行作用。

在第三句中,程序里所有的register会被删除,可以用于调试。

特例(不是完全字符串替换,感谢@Black_white_Tony dalao):

我们都知道vector <pair<int,int>>会因为>>被识别为右移而CE所以必须补空格。但是如果这样写:

#define pii pair<int,int>
vector <pii> a;

却可以正常通过编译,这是因为如果define中的最后一个字符和后面第一个字符能构成新运算符时,就会自动加上空格。大家可以用g++ -E指令看得更透彻一些。

两个运算符构成新运算符加空格:<< >> -> ++ && += >=

这个特例也许就是为了STL套STL的问题设计的吧。

注:C++11里是可以直接写vector <pair<int,int>>的,但是你如果使用了宏定义,第一步预处理后的文件在这里仍会加上空格。

3. 宏的高级应用

##:连接左右两端的字符串

#: 把后面的参数变为一个字符串(即强行加上"")

#define a(x) p##x
#define b(x) #x

int p1 = 3, p2 = 4;

int main()
{
    printf("%d %d\n",a(1),a(2));
    puts(b(qwqwq));
}
//Output:
//3 4
//qwqwq

这个比较常见的就是用来缩写for,避免因b改变带来的问题。

#define F(i, a, b) for(int i=(a),end##i=(b); i<=end##i; i++)

#ifdef 如果定义了宏

#ifndef 如果没定义宏

#endif以上两句的终止句(相当于右括号)

在标准库中,每包含一个头文件,这个头文件里就会define一个表示这个文件已被包含的宏,如果这个文件第二次被包含,#ifndef为假不再执行,就会跳过文件,这样就可以避免重复包含导致CE。

有些宏是在不同编译环境里就定义好的,利用这些就可以做些趣事。

#ifndef ONLINE_JUDGE
    freopen("testdata.in","r",stdin);
    freopen("testdata.out","w",stdout);
#endif
//很多OJ(包括洛谷)都有这个宏

或者也可以在开头定义一个debug宏,把调试输出的语句用#ifndef括上,这样删除调试输出就只需要注释一行。

其他预定义的宏:(摘自cppreference)

__cplusplus //C++版本号
__FILE__ //文件名
__DATE__ //编译日期
__TIME__ //编译时间
__LINE__ //这一行的行号

4. 宏的撤销

能定义的宏就能取消,使用#undef直接接宏名就可以撤销宏(包括预定义的)。

#define sum(a,b) a+b
#define e 2.718
int a=sum(9,6);
double b=e*3;
#undef sum(a,b)
#undef e
#undef __cplusplus

5. 宏的缺点

宏虽然方便易用,但也有许多缺点。

I. 改变运算优先级
#define DEF 2+3
int a = DEF+5;
int b = DEF*7;

DEF以2+3的形式直接带入,没有转化为5

在A的定义中,a将被解释为“2+3+5”,其值为10.

但B将被解释为“2+3*7”,乘法先算,值为23,不是我们希望的35.

解决方法就是在参数左右加上括号

II. 没有固定的数据类型
#define MAX 1e6
int a[MAX];

此时会CE。因为1e6是一个double类型,数组大小只能用int,由于MAX是文本替换导致这里并不会转换类型。

这是可以在前面加上(int),或者使用const定义常量。

4. #pragma

在我们寻找一道题最优解的时候,最快的人(如果没打表)往往会有几十行的#pragma来卡常。那么这个命令有什么用?卡常的原理是什么呢?

#pragma命令可以指定编译选项,或者让编译器完成一些命令。功能非常强大,这里只做非常浅显的介绍。

部分内容摘自百度百科。

1. #pragma once

添加在头文件的开头,可以告诉编译器这个文件最多编译一次,也可以用于防止重复包含头文件。比前文#ifndef好用,只是标准库里没用这个。

2. #pragma message()

让编译器输出括号里的字符串,配合#ifdef,可以在编译时就输出一些特定的信息。

3. #pragma comment()

本身用于链接文件,OI里可以用来手动扩栈(但是不一定有用)

#pragma comment(linker,"/STACK:1024000000,1024000000")

4. #pragma GCC target()

这个找遍全网也没有准确定义,大概就是将括号里的东西识别为指令。指令的速度比函数更快,借此加速。

#pragma GCC target("popcnt")可以让内置函数__builtin_popcount()的速度提高一倍以上。

另外,如果你想使用指令集,也可以使用这条指令把指令集括上。

#pragma GCC target("avx,avx2,sse,sse2,sse3,sse4.1,sse4.2")

5. #pragma pack() & pop()

用于对齐结构体

//#pragma pack(4)
struct Node
{
    int a;
    long long b;
}x;

本来一个结构体的每个变量都会与最大的那个对齐,比如例子中int就与long long对齐了,字节数也为8。所以sizeof x = 16

但是如果有了那句#pragma,每个变量就会与4对齐,所以int字节数为4,long long由于本来就大于4就被忽略,sizeof x = 12。这样做一定程度上可以省空间。

但是对齐其实效率更高,所以x大一点好。

pop()可以用来取消pack()指令

6. #pragma GCC optimize()

将括号里的字符串带入编译参数,相当于可以自定义编译参数。

如果输入数字的话就会进行O1/O2/O3优化。用这个命令可以开启编译器自带的优化。

但是只能是编译优化方面的参数,比如-o指定文件名肯定不能加在里面。

最后附赠网络上广泛流传的40行优化:

#pragma GCC target("sse,sse2,sse3,sse4.1,sse4.2,popcnt,abm,mmx,avx")
#pragma comment(linker,"/STACK:102400000,102400000")
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")
#pragma GCC optimize("-fgcse")
#pragma GCC optimize("-fgcse-lm")
#pragma GCC optimize("-fipa-sra")
#pragma GCC optimize("-ftree-pre")
#pragma GCC optimize("-ftree-vrp")
#pragma GCC optimize("-fpeephole2")
#pragma GCC optimize("-ffast-math")
#pragma GCC optimize("-fsched-spec")
#pragma GCC optimize("unroll-loops")
#pragma GCC optimize("-falign-jumps")
#pragma GCC optimize("-falign-loops")
#pragma GCC optimize("-falign-labels")
#pragma GCC optimize("-fdevirtualize")
#pragma GCC optimize("-fcaller-saves")
#pragma GCC optimize("-fcrossjumping")
#pragma GCC optimize("-fthread-jumps")
#pragma GCC optimize("-funroll-loops")
#pragma GCC optimize("-fwhole-program")
#pragma GCC optimize("-freorder-blocks")
#pragma GCC optimize("-fschedule-insns")
#pragma GCC optimize("inline-functions")
#pragma GCC optimize("-ftree-tail-merge")
#pragma GCC optimize("-fschedule-insns2")
#pragma GCC optimize("-fstrict-aliasing")
#pragma GCC optimize("-fstrict-overflow")
#pragma GCC optimize("-falign-functions")
#pragma GCC optimize("-fcse-skip-blocks")
#pragma GCC optimize("-fcse-follow-jumps")
#pragma GCC optimize("-fsched-interblock")
#pragma GCC optimize("-fpartial-inlining")
#pragma GCC optimize("no-stack-protector")
#pragma GCC optimize("-freorder-functions")
#pragma GCC optimize("-findirect-inlining")
#pragma GCC optimize("-fhoist-adjacent-loads")
#pragma GCC optimize("-frerun-cse-after-loop")
#pragma GCC optimize("inline-small-functions")
#pragma GCC optimize("-finline-small-functions")
#pragma GCC optimize("-ftree-switch-conversion")
#pragma GCC optimize("-foptimize-sibling-calls")
#pragma GCC optimize("-fexpensive-optimizations")
#pragma GCC optimize("-funsafe-loop-optimizations")
#pragma GCC optimize("inline-functions-called-once")
#pragma GCC optimize("-fdelete-null-pointer-checks")

注意:

  1. 这类优化的效果玄学,因人而异,有时很猛有时一点用都没有,也与编译环境相关。但是最坏情况也就没有用,这些代码不会因为编译环境CE。
  2. 由于O2/O3/Ofast优化已经到达了改写循环,删除多余代码等毁天灭地的程度,很容易改变代码的原意导致玄学错误。使用这些优化的时候一定要保证自己的代码规范,否则就会有玄学问题出现。
  3. 并不知道NOI)(P能不能用,最好不用(你也不可能背下来)

5. Others

还有一些命令,这里花上几行介绍一下。

#error //在这一行显示一个CE信息,并中断编译
#warning //在这一行显示警告信息
#line //指定下一行的行号
#if //如果满足则执行,后面应接布尔表达式,以#endif结尾
#elif //#if语句的分支

完结撒花,感谢陪伴

原文地址:https://www.cnblogs.com/ofnoname/p/11621345.html

时间: 2024-10-14 05:34:51

浅析C++预处理命令的相关文章

#pragma预处理命令

#pragma预处理命令 #pragma可以说是C++中最复杂的预处理指令了,下面是最常用的几个#pragma指令: #pragma comment(lib,"XXX.lib") 表示链接XXX.lib这个库,和在工程设置里写上XXX.lib的效果一样. #pragma comment(linker,"/ENTRY:main_function") 表示指定链接器选项/ENTRY:main_function #pragma once 表示这个文件只被包含一次 #pra

C语言第十一回合:预处理命令的集中营

  [学习目标]   1.         宏定义 2.         文件包括"处理 3.         条件编译 预处理命令:能够改进程序设计的环境.提高编程效率. 其功能主要有三种:宏定义.文件包括.文件编译. ANSI标准定义的C语言预处理指令预览表 A: 宏定义 (a)不带參数的宏定义 格式:#define标识符 字符串 如:#define PI 3.1415926 *标识符被称为:宏名 *在预编译时将宏名替换成字符串的过程为:宏展开. *#define 是宏定义命令 //求圆周长

IOS使用C#预处理命令,多种SDK共存

当我们使用Unity接 91,XY助手等等SDK时候. 我们需要使用[DllImport("__Internal")] 来声明一个C++的方法调用. 不同的SDK总会有不同的方法. 我习惯是写成 XYSDK类,  Baidu91SDK类里面封装的各种[DllImport("__Internal")]声明的方法 问题就来了, 如果我们发布XY SDK,而91 SDK的方法并没有放在xcode工程里面. 所以造成编译不通过.我们就需要把91 C++封装好的方法放入工程当

Unity预处理命令

我们经常在代码里面写Debug.Log()调试代码,游戏后门代码.这些代码在发布时无意义的,我们就需要慢慢的删除掉它们(很痛苦),有什么办法让它们在编译的时候并不加入编译代码中呢?  预处理命令..比如游戏准备发布电脑和安卓分别控制角色鼠标移动,双手控制移动并不需要复制两份项目分别开发,可以使用预处理命令进行分开编程! 下面介绍几个常用的预处理命令: UNITY_EDITOR    只在编辑器中编译 UNITY_ANDROID  只在安卓下编译 UNITY_STANDALONE_OSX  只在苹

C++预处理命令

预处理语句是由一系列和预处理相关的命令符组成的.预处理语句以#作为起始标记,其后紧跟预处理命令关键字,之后是空格,空格之后是预处理命令的内容.C++提供多种预处理功能,如宏定义,文件包括,条件编译等. #define 在这个教程的开头我们已经提到了一种预处理指令: #define ,可以被用来生成宏定义常量(defined constantants 或 macros),它的形式是: #define name value 它的作用是定义一个叫做name 的宏定义,然后每当在程序中遇到这个名字的时候

JDBC简介,MySQL连接,PreparedStatement 预处理命令,通配符

何须浅碧轻红色,自是花中第一流. -李清照的<鹧鸪天·桂花> JDBC 简介 我实验的MySQL数据库 配置连接MySQL驱动 数据库连接工具类 JDBC API Driver 接口 Connection 接口 DriverManager 类 Statement 接口 PreparedStatement 接口 CallableStatement 接口 ResultSet 接口 JDBC 数据库操作 测试连接示例 添加数据 查询信息 修改数据 删除数据 批处理 调用存储过程 JDBC 简介 JD

程序猿之--C语言细节15(预处理命令细节#error、运算符#和##、__FILE__、__LINE__)

主要内容:预处理命令细节#error.运算符#和##.__FILE__.__LINE__ #include <stdio.h> /* 包含这个头文件,并不是将其所有函数都链接进程序*/ /* ##运算符 */ #define MK_ID(n) i##n /* 表示将两个记号连接 */ int MK_ID(1), MK_ID(2),MK_ID(3); /* 预处理后变成int i1,i2,i3;*/ /* 定义多个type##_max函数,函数返回类型和参数类型用define决定 * 如GENE

volatile,可变参数,memset,内联函数,宽字符窄字符,国际化,条件编译,预处理命令,define中##和#的区别,文件缓冲,位域

 1.volatile:要求参数修改每次都从内存中的读取.这种情况要比普通运行的变量需要的时间长. #include <stdio.h> #include <stdlib.h> #include <time.h> void main() { time_t start, end; double res = 0; time(&start);  //获取时间,传递给start //volatile强制每次从内存读取 volatile int i; for (i =

C语言预处理命令详解

一  前言 预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作.预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置. 预处理是C语言的一个重要功能,它由预处理程序负责完成.当对一个源文件进行编译时,系统将自动引用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译. C语言提供多种预处理功能,主要处理#开始的预编译指令,如宏定义(#define).文件包含(#include).条件编译(#ifdef)等.合理使用预处理功能编