最近在学习 c++
, 在编译与链接过程中遇到了一些定义与声明的问题, 经过多处查阅资料, 基本解惑. 现记录与此, 希望让后面人少走些弯路.
C++
的头文件应该用什么扩展名?目前业界的常用格式如下:
implementation file
*.cpp
*.cc
*.cc
*.c
header file
*.hpp
*.h++
*.hh
*.hxx
*.h
一句话: 建议 源文件使用 .cpp
, 头文件使用 .hpp
关于 implementation file
并没有什么说的, 使用 .cpp
/.cc
都是可以的. 但是 header file
需要注意.
c 的头文件格式是 .h
, 认为 h
代表 header
, 于是有很多人也喜欢在 c++ 用 .h
作为头文件扩展名. 其实扩展名并不影响编译结果, 对于编译器来说扩展名是不重要的 (甚至使用 .txt
也可以). 但是如果在一个 c
与 c++
混合使用的大型项目中, 你很难立刻分辨出这是一个 cpp
的 header file
或者是一个 c
的header file
; 此外, 在 vim
或者 vscode
的语法提示插件看来, .h
就是 c 语言的, 那么当你在 c 文件写了 cpp 的某些语法当然会提示不正确 (当然肯定还是可以编译通过的)
因此, 我认为最好的处理结果就是如果 header file
中涉及到了任何 c++
的语法, 那么这个头文件就应该以 .hpp
为后缀, 否则都已 .h
为后缀
implementation file
与 header file
写什么内容理论上来说 implementation file
与 header file
里的内容, 只要是 c++ 语言所支持的, 无论写什么都可以的, 比如你在 header file
中写函数体, 只要在任何一个 implementation file
包含此 header file
就可以将这个函数编译成 object
文件的一部分 (编译是以 implementation file
为单位的, 如果不在任何 implementation file
中包含此 header file
的话, 这段代码就形同虚设), 你可以在 implementation file
中进行函数声明, 变量声明, 结构体声明, 这也不成问题!!!
那为何一定要分成 header file
与 implementation file
呢? 为何一般都在 header file
中进行函数, 变量声明, 宏声明, 结构体声明呢? 而在 implementation file
中去进行变量定义, 函数实现呢?
原因如下:
如果在 header file
中实现一个函数体, 那么如果在多个 implementation file
中引用它, 而且又同时编译多个 implementation file
, 将其生成的 object file
连接成一个可执行文件, 在每个引用此 header file
的 implementation file
所生成的 object file
中, 都有一份这个函数的代码, 如果这段函数又没有定义成局部函数, 那么在连接时, 就会发现多个相同的函数, 就会报错.
如果在 header file
中定义全局变量, 并且将此全局变量赋初值, 那么在多个引用此 header file
的 implementation file
中同样存在相同变量名的拷贝, 关键是此变量被赋了初值, 所以编译器就会将此变量放入 DATA 段
, 最终在连接阶段, 会在 DATA 段
中存在多个相同的变量, 它无法将这些变量统一成一个变量, 也就是仅为此变量分配一个空间, 而不是多份空间, 假定这个变量在 header file
中没有赋初值, 编译器就会将之放入 BSS 段
, 连接器会对 BSS 段
的多个同名变量仅分配一个存储空间.
如果在 implementation file
中声明宏, 结构体, 函数等, 那么如果要在另一个 implementation file
中引用相应的宏, 结构体, 就必须再做一次重复的工作, 如果我改了一个 implementation file
中的一个声明, 那么又忘了改其它 implementation file
中的声明, 这不就出了大问题了, 如果把这些公共的东西放在一个头文件中, 想用它的 implementation file
就只需要引用一个就 OK 了!
在 header file
中声明结构体, 函数等, 当你需要将你的代码封装成一个库, 让别人来用你的代码, 你又不想公布源码, 那么人家如何利用你的库中的各个函数呢? ? 一种方法是公布源码, 别人想怎么用就怎么用, 另一种是提供 header file
, 别人从 header file
中看你的函数原型, 这样人家才知道如何调用你写的函数, 就如同你调用 printf
函数一样, 里面的参数是怎样的? 你是怎么知道的? 还不是看人家的头文件中的相关声明!
已知 header file
a.h
声明了一系列函数 (仅有函数原型, 没有函数实现), b.cpp
中实现了这些函数, 那么如果我想在 c.cpp
中使用 a.h
中声明的这些在 b.cpp
中实现的函数, 通常都是在 c.cpp
中使用 #include "a.h"
, 那么 c.cpp
是怎样找到 b.cpp
中的实现呢?
编译器预处理时, 要对 #include
命令进行 文件包含处理: 将 a.h
的全部内容复制到#include "a.h"
处. 这也正说明了, 为什么很多编译器并不 care 到底这个文件的后缀名是什么 - 因为 #include
预处理就是完成了一个 复制并插入代码 的工作.
程序编译的时候, 并不会去找 b.cpp
文件中的函数实现, 只有在 link
的时候才进行这个工作. 我们在 b.cpp
或 c.cpp
中用 #include "a.h"
实际上是引入相关声明, 使得编译可以通过, 程序并不关心实现是在哪里, 是怎么实现的. 源文件编译后成生成 obj file
, 在此文件中, 这些函数和变量就视作一个个符号. 在 link
的时候, 需要在 makefile
里面说明需要连接哪个 obj
文件 (在这里是 b.cpp
生成的 .obj
文件), 此时, 连接器会去 .obj
文件中找在 b.cpp
中实现的函数, 再把他们 build
到 makefile
中指定的那个可以执行文件中.
在 Clion
中, 一般情况下不需要自己写 makefile
, 只需要将需要的文件都包括在 project 中, Clion
会自动帮你把 makefile 写好.
通常, 编译器会在每个 .o
或 .obj
文件中都去找一下所需要的符号, 而不是只在某个文件中找或者说找到一个就不找了. 因此, 如果在几个不同文件中实现了同一个函数, 或者定义了同一个全局变量, 链接的时候就会提示redefined
.
是定义还是声明与其位于
header file
还是implementation file
无关.
根据以上规定, 我们可以有如下的结论:
extern int a; // 声明
int a; // 定义
int a = 0; // 定义
extern int a = 0; // 定义
许多程序员对定义变量和声明变量混淆不清, 定义变量和声明变量的区别在于定义会产生内存分配的操作, 是汇编阶段的概念; 而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量.
我们在编译模块中的任意一个文件中书写的变量/函数在此模块中其他文件中都可以被访问到, 但是其他编译模块的文件是没有访问此变量的权限的. 那么如何跨模块共享变量 / 函数呢?
答案就是使用 extern
. 在这里请在做的各位牢牢记住它的定义: 标示所修饰的变量或函数的可能位于其他模块.
一定要牢牢记住上面的定义, 带着定义我们就可以想明白以下问题
- 为什么在一个
implementation file
中使用一个外部变量要先extern
声明该变量 (或者导入该变量所在的header file
)?- 为什么
header file
中要使用 extern 声明一个变量?
这样当我们编译某个单元时, 编译器发现了使用 extern
修饰的变量, 如果正好本模块中有其相关定义, 那么就直接使用; 如果没有相关定义, 那么就挂起, 在编译后续其他模块的时候进行查找, 如果到最后还没有找到, 那么在链接阶段就会报错 ld: symbol(s) not found for architecture x86_64
;
test1.hpp
中声明 extern int a;
test1.cpp
中定义 int a = 10;
(或者使用 int a;
定义, 这样的话值是默认值 0)test2.cpp
中 #include "test1.hpp"
, 这样便可以在 test2.cpp
中直接使用 a 变量了.在头文件 test1.hpp
中直接 extern int a = 10;
这样属于在头文件中直接定义, 我们已经说了 一个变量可以被多处声明, 但只能定义在一处
, 在这种情况下如果有多个 implementation file
都 #include "test1.hpp"
, 那么会造成在 obj
文件的 链接
阶段发现多处存在同一个变量的定义, 这时会报错 ld: 1 duplicate symbol for architecture x86_64
同时, 在头文件中定义一个变量属于非常业余的做法, 请不要争相模仿
在头文件 test1.hpp
中 直接 extern int a = 10;
, 在 test2.cpp
中直接使用 extern int a;
(没有 #include test1.hpp
)
这样做可以避免多处重复定义的问题, 但是这样的话 test1.hpp
定义的其他变量与方法都不可以使用了, 必须全部使用 extern ***
的形式进行声明然后使用, 这样会及其得不偿失.
所以我们可以得出结论:
真理总是这么简单!
函数与变量类似, 也分为定义与声明. 但是与变量在声明时必须要包含 extern
不同, 由于函数的定义和声明是有区别的, 定义函数要有函数体, 声明函数没有函数体, 所以函数定义和声明时都可以将 extern
省略掉, 反正其他文件也是知道这个函数是在其他地方定义的, 所以不加 extern
也行.
所以在 cpp 中, 如果在一个函数前添加了 extern, 那么仅表示此函数可能在别的模块中定义; 或者也可以让我们在只使用了某个头文件的这个方法时不用
#include <***.hpp>
static
使用static
用于修饰类中的变量/函数是一个静态成员变量/函数
static
用于修饰类之外的变量/函数是一个普通的全局静态成员变量/函数
extern
声明也不可以), 不是真正意义的全局(普通的函数默认是 extern
的)static
, 比如函数中要返回一个数组, 不想让这个数组函数结束时被释放, 那么可以使用 static 修饰此局部变量static
使变量只在本编译模块内部可见, 这样的话如果两个编译模块各自都有一个 value
变量的话, 那么千万不要将两个编译模块内 static 修饰的变量认为是同一份内存, 他们实际上是两份内存, 修改其中一个不会影响另外一个
implementation file
及其所有的 #include ...
文件内所组成的一个编译模块中有多个 static int a = 0
, 那么会报错 error: redefinition of 'a'
test.hpp
有 static int a = 0
, test1.cpp
与 test2.cpp
分别都有 #include "test.hpp"
, 那么这就是两个编译模块各有一个 static int a
, 这时是 cpp 允许的, 可以顺利通过编译并运行的当 const
单独使用时它就与 static
相同, 而当与 extern
一起合作的时候, 它的特性就跟 extern
的一样了
#ifndef
能保证你的头文件在本编译模块只被编译一次(但是多个模块都编译此段代码的话则还是会有重复代码)
头文件
& 声明
& 定义
的规则header file
中是对于该模块接口的声明, 接口包括该模块提供给其它模块调用的外部函数及外部全局变量, 对这些变量和函数都需在 header file
中冠以 extern
关键字声明implementation file
开头冠以 static
关键字声明header file
中定义变量#include
其 header file
即可.如果工程很大, 头文件很多, 而有几个头文件又是经常要用的, 那么
- 把这些头文件全部写到一个
header file
里面去, 比如写到 preh.h- 写一个
preh.cpp
, 里面只一句话:#include "preh.h"
- 对于
preh.c
, 在 project setting 里面设置create precompiled headers
, 对于其他 c++ 文件, 设置use precompiled header file
文章浏览阅读657次,点赞5次,收藏12次。采用现有无人车导航系统进行农作物育种表型信息的监测与采集时,存在因育种小区数量较多导致的导航目标点人工测量工作量大的问题,且育种田块不同于大田作物以A-B 线为基线的路径规划方法,无人车需要根据育种材料编号自动行进到某一指定位置进行作物表型信息采集。因此,为提高导航效率和降低人工劳动强度,本文基于无人机遥感对无人车的导航系统进行了研究。通过无人机获取玉米育种田的遥感影像并进行拼接和位置矫正,生成正射影像和数字地表模型(Digital Surface Model,DSM)。
文章浏览阅读7.8k次。开发环境vs2010css:bootstrapjs:jquery bootstrap paginator原先只是想做个mvc的分页,但是一般的数据展现都需要检索条件,而且是多个条件,所以就变成了MVC多条件+分页查询因为美工不是很好,所以用的是bootstrap前端框架,自己懒得写前端的分页控件,用的是bootstrap paginator分页控件。方式: _.net core mvc+bootstrap 页面分页查询
文章浏览阅读2.5k次。Springboot +spring security,OAuth2 四种授权模式概念_springbootsecurity oauth2
文章浏览阅读78次。 在学习设计模式的过程中,实践需与理论相结合才能更好地举一反三,灵活运用。设计模式到底是什么?它是对整个软件系统的拆分,组装,并决定模块间关系以及如何互动、通信的某种模式。究其本质,设计模式就是以语言特性(面向对象三大特性)为硬件基础,再加持六大设计原则的灵魂组合而总结出的一系列套路,本篇要讲地就是灵魂。单一职责 我们知道功能完备的软件系统是复杂的,系统的拆分与模块化是不可或...
文章浏览阅读1.1k次。Token:票据,令牌。当用户尝试登录,将请求提交到服务器端,如果服务器端认证通过,会生成一个Token数据并响应到客户端,此Token是有意义的数据,此客户端在后续的每一次请求中,都应该携带此Token数据,服务器端通过解析此Token来识别用户身份!关于Session与Token:Session默认是保存在服务器的内存中的数据,会占用一定的服务器内存资源,并且,不适合集群或分布式系统(虽然可以通过共享Session来解决),客户携带的Session ID只具有唯一性的特点(理论上),不具备数据含义……而_token 与 jwt
文章浏览阅读944次。记一次数据库连接数据库报错报错提示:Loading class com.mysql.jdbc.Driver'. This is deprecated. The new driver class iscom.mysql.cj.jdbc.Driver’. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.原因:mysql驱动类D_the new driver class is `com.mysql.cj.jdbc.driver
文章浏览阅读1.1w次,点赞2次,收藏3次。前提:想要使现用chrome不更新,按照网上最多的教程是控制面板-管理工具-服务-找到“Google更新服务(Gupdate)"服务 项与“Google更新服务(Gupdatem)”服务项-选择“禁用”我安装的谷歌浏览器在服务中找不到googleupdate参考教程如下:手动方式:chrome禁止更新如何设置_怎么设置chrome不自动更新-win7之家命令行方式:https://jingyan.baidu.com/article/f3ad7d0f51c6bf09c3345bb0.htm_服务里面没有谷歌更新服务
文章浏览阅读822次。给大家带来的一篇关于人工智能相关的电子书资源,介绍了关于深度学习、人工智能算法方面的内容,本书是由人民邮电出版社出版,格式为PDF,资源大小30.8 MB,Ian Goodfellow编写,目前豆瓣、亚马逊、当当、京东等电子书综合评分为:8.7。内容介绍本书包括3 个部分:第1 部分介绍基本的数学工具和机器学习的概念,它们是深度学习的预备知识;第2 部分系统深入地讲解现今已成熟的深度学习方法和技术..._人工智能 算法 pdf
文章浏览阅读1.2k次。(一)VNC1 下载如下包 git clonehttps://github.com/Juniper/contrail-testhttps://github.com/Juniper/contrail-provisioninghttps://github.com/Juniper/contrail-fabric-utilshttps://github.com/Juniper/cont_centos6 ipfix
文章浏览阅读7.4k次,点赞3次,收藏39次。//用两个数码管显示0到99。十位数为0的时候,显示为空白。//用两个按键控制数值的加减,按一次K1数字加1,按一次K2数字减1,数值的范围是从0到99。//再用一个接近开关控制数字的减小,接近开关感应一次数字减1,减到0停止。// C语言程序如下。/*************************************************************** 文件 : -----..._两位数码管显示功能。具体功能描述如下:数码管可以显示0-99两位数字,按一下加按键
文章浏览阅读3w次,点赞7次,收藏12次。 在Android开发中,设置背景图片是一个既简单又常用的方法,由于本人现在需要同时做Android开发,所以Android的开发技能也要不断学习储备,不仅是为了给老板省钱,也是为了增加自己的竞争力。那么就来分享一下开发心得,给控件添加背景图的方法步骤,Android大牛请飘过,只分享给需要的人,今天分享在这里分享一下给添加背景图片的方法。 设置背景图片有两种方法,但是在..._c#andriod 背景图
文章浏览阅读3.8k次,点赞2次,收藏12次。网络通信技术1. 基本概念1.1 接口1.2 表项查询方法1.2.1 内容寻址存储器2. 二层技术3. 三层技术1. 基本概念1.1 接口1.2 表项查询方法1.2.1 内容寻址存储器内容寻址存储器(Content Addressable Memory)CAM,是在传统的存储技术的基础上实现的联想记忆存储器,主要有读、写、查询操作。读操作:输入地址,返回该地址上的数据,读取速度与RAM相同;写操作:2. 二层技术3. 三层技术..._网络通信技术