文章

一:分解helloworld:头文件、预处理

一:分解helloworld:头文件、预处理

目录

前言

C++基本语法一篇markdown每个知识点都记录太长,也难以查看,将不同部分分开,每个点详细更新一篇会好一点。 关于内容:C++的语法和概念很多,想每个概念语法稍微深入一点,一般视频教程也要2、30个小时。 更新顺序并不会从变量等开始,因为了解C++多少都了解点c, 因此本篇想从分解helloworld.cpp开始,一段简单的输出helloworld代码反而是很多 新手朋友容易忽略的地方,而其中的命名空间、预处理#符等都值得更深入一点,这对理解一段cpp代码 是如何被编译、执行的会更透彻。

1
2
3
4
5
6
7
// hello.cpp
#include <iostream>
// using namespace std;
int main() {
	std::cout << "Hello World" << std::endl;
	return 0;
}

正文

头文件和预处理1:预处理意义

写代码是在组织一段文本交给编译器,让编译器看得懂,为了组织好代码,比如相同的部分想拿出来公用,那么不可能将所有的东西都放在一个文件中,将不同功能等代码分解成各个模块存在文件中,需要的时候引用即可,头文件也是这样,当然也不仅仅这么简单。

helloworld中用到了cout,实际上如若不引用#include <iostream>cout在代码中不存在或者说未声明,因此要在开头将 iostream引用,#符是预处理符号,还记得编译代码的四个过程吗,预处理、编译、汇编、链接,在预处理过程 引用头文件的作用便出来了,#include会把iostream的代码复制一份到hello.cpp的开头位置。

但也可以想到,这其中可能存在问题,无法保证变量名、函数名等不会重复,当然解决方法也简单,比如iostream中的命名 都用iostream_xxx下划线xxx组织起来,但显然这也不是特别好的方法,因此要引出下方命名空间规则。

命名空间

在一些输出helloworld例子中,开头有using namespace std, 这就是引入了一个叫std的命名空间,如果编写了这一行代码,在后续的调用cout中就不需要std::cout了, 即告诉了编译器接下来的coutstd这个空间中,实际上,查看iostream的源码会发现:

1
2
3
namespace std _GLIBCXX_VISIBILITY(default)
{
...

C++的源码将变量、函数等放到了一个叫std的命名空间中(其他文件也是如此)。

还记得gcc有一些参数可以仅做预处理、编译等四个过程吧, 假设在注释// using namespace std;的情况下只写cout显然会报错,试一试:

1
2
3
4
...
// using namespace std;
...
cout << "Hello World";

首先用gcc -E test.cpp -o test.i生成预处理文件 (注意.i结尾,gcc –help中说的很清楚,会根据文件名后缀名决定编译器要做什么), 这一步是不会报错的,因为预处理仅仅复制了一份代码,那接下来编译gcc -S test.i -o test.s, 这一步就会报错了:

1
2
3
...
error: ‘cout’ undeclared (first use in this function)
...

对编译器来说,cout是未声明的,因为它在不用std::cout显式的告诉编译器cout在哪的情况下, 它在预处理后的文件中找不到哪个地方声明了cout

然后是一些语法,using不一定非要写在开头,写在需要调用的地方也可以, 也不一定非要using整个命名空间,using std::cout仅需使用的函数也是可以的。

编程方面,其实不推荐using命名空间,需要什么用::符号引出更易读,或者在需要使用的地方using也是可以的, 如果func1函数内部需要cout,则在其函数体内引入,而不需要在全局引入,这也是较好的编程习惯。

值得注意的是:C中就没有命名空间的概念,因此C的一些库就是上文提到的规则,其中变量用库名下划线某某命名。

头文件和预处理2:如何防止重复引入

每个知识点都并不是单独存在的,因此描述完命名空间,可以继续说下头文件。

还是以cout为例,或者说,引入一些东西是想告诉编译器我声明了(不一定要定义即实现,定义只能定义一次), 请不要报错,这个东西你会在其他地方找到的。

问题include是复制粘贴文件,那如果不小心在一个问题中include一个头文件多次怎么办?预处理后岂不是有多个相同命名的变量或函数了。

在使用vs新建头文件会发现IDE给头文件加上了#pragma once指令,这这个指令在预处理中的意思便是说: 请不要把重复的头文件复制多次,但并不意味着你不能在不同的文件中引入相同的头文件,只是说在同一个文件中如果引入多个相同的头文件预处理不会复制多次。

创建一个log.h:

1
2
3
4
5
6
7
#pragma once

void log();

void log() {
	std::cout << "log" << std::endl;
}

main.h中这样写:

1
2
3
4
5
6
7
8
#include <iostream>
#include "log.h"
#include "log.h"

int main()
{
	return 0;
}

此时,IDE不会报错,因为log.h已经加上了上述指令,如果把log函数的定义放在main.h中,此时不用引入log.h也没关系,因为log函数在main.h中已经声明并且定义了,这仅仅是想做个例子而已。

如果不加#pragma once会怎么样?IDE会报错,告诉你这个函数已经有一个主体了,原因也很简单,因为被复制了两次, 函数可以被声明多次,但是定义只能在一个地方定义。

对于重复问题,解决方法当然也不仅pragma声明,预处理指令有很多,而且这个也比较新,不过现在编译器基本都支持pragma 这条指令,还可以利用一些预处理指令,例如if not define的缩写ifndef,以及配套的endif,搭配define指令, ifndef的意思就是说如果未定义就怎样,显然可以构造一个变量,顺序是如果未定义它,那就定义它,然后结束,如:

1
2
3
4
5
6
7
8
9
10
//#pragma once
#ifndef ONE
#define ONE

void logger();

void logger() {
	std::cout << "log" << std::endl;
}
#endif // !ONE

此时编译main.h也不会报错,哪怕log.h被引入多次。

相反的,ifdef指令便是if define的缩写。显然pragma要简洁很多。

但有意思的是,如果查看头文件源码,会发现C++头文件基本都用#ifndef来防止重复引用,每个如此,如vector

1
2
3
4
5
#ifndef _GLIBCXX_VECTOR
#define _GLIBCXX_VECTOR 1
#pragma GCC system_header
...
#endif /* _GLIBCXX_VECTOR */

头文件和预处理3:”“和<>引入 以及 一些预处理指令、预处理宏

引入头文件会发现有""符号和<>号,实际上,一般双引号可以包含一个路径,这意味着你可以在双引号中写: "/root/yourlib/yourhead.h",尖括号会去设置的路径下搜索,比如linux下/usr/include等。

至于为什么iostream没有.h后缀名,这在C++基本语法的大md中已经说过了,这只是C++库的一种命名, 通常c的拓展库带.h后缀名罢了。

对于预处理指令,了解为什么要用印象显然是更加深刻的, 比如上面就学习了pragmaifndefendif等用法,预处理指令也不仅仅包括这些,还有很多,例如#elseif联用,#line重新定义行号文件名等,除此之外,预处理指令的参数也是有不同的。

也可以联想到,既然有define宏定义,那cpp本身有没有定义一些基本的宏呢?也是有的,比如:

1
std::cout << __LINE__ << std::endl;

__LINE__便是一个已定义过的宏,意思是当前行号。除此之外还有,__FILE__文件名,日期时间__DATE____TIME__等。

指令和宏很多,这些东西要在使用中才会不断了解印象才会深刻。

本文由作者按照 CC BY 4.0 进行授权

热门标签