C/C++编程笔记:C/C++ 的编译和链接
C/C++文件
C/C++程序文件包括 .h .c .hpp .cpp,其中源文件(.c .cpp)是基本的编译单元,头文件(.h .hpp)不会被编译器编译。
C/C++项目构建(build)过程,分为以下几个步骤 预处理 → 编译 → 链接。
预编译
预编译的过程可以理解为编译器(实际上是预处理器,这里统称为编译器就可以了)在正式编译之前处理C/C++文件中的预处理命令,即#开头的代码。
常用的几个预处理命令如下:
#include ......
#ifdef ...... #else......#endif
#define ......
#pragma ......
举个例子,下面是个很简单的类定义:
MyClass.h
MyClass.cpp
预编译完成后的样子:
可以看到编译器把.h文件替换到了.cpp文件中的#include 位置上,把DEFAULT_VALUE定义的值也替换到了相应的位置。
编译
预编译之后,编译器会编译每个源文件(.c .cpp),如果编译成功,会生成对应的目标文件,Linux为.o文件,Windows平台下为.obj文件。
以Linux平台为例,上面的MyClass.cpp编译完成后会生成MyClass.o文件
使用objdump可以看到目标文件MyClass.o的内容
编译器会把MyClass::Fun()的名字改成_ZN7MyClass3FunEv,这个过程叫Mangle,由于C++支持重载,覆盖等特性,所以编译器必须把函数用一个唯一的标识表示。这个字符串就是编译器生成的唯一标识。
这里还要单独说一下头文件,头文件的既然不是编译单元,那么它的作用是什么?
头文件就是负责”声明“,编译器在编译MyClass.cpp的时候,对于MyClass这个类以及Fun()这个成员函数,编译器必须找到它的声明,这个函数才能被正确编译。
如果有其他cpp需要使用MyClass这个类的时候,也需要它的的声明。例如
main.cpp
加上#include "MyClass.h" 编译器在编译main.cpp的时候才知道怎么编译MyClass这个类。MyClass.h里声明是不会真正被编译到main.o中,.h文件中的内容在目标文件中只是以列表的形式存在,这个表在后面链接时会用到。
当然,头文件不仅可以用来声明,还可以定义(定义全局变量,全局函数等),在头文件中的定义要小心,可能会引起链接错误。
链接
链接就是将一堆目标文件加静态库文件装配成可执行文件的过程。(或者是装配成静态/动态库的过程)
上面两个cpp分别被编译成了MyClass.o, 和main.o,我们要生成可执行程序的话,就必须经过链接的过程,把两个目标文件合成一个可执行文件。
main.o中,main函数会构造MyClass, 并且调用Fun()函数,那么main就根据MyClass.h生成的表,找到MyClass.o中的函数,这个就是链接器要做的工作。
常见错误
构建c/c++工程的时候,最常见的就是两种错误:
-- 编译错误,在编译过程中产生的错误,通常是语法错误,没有声明,重复声明导致编译目标文件错误
其中没有声明通常是由于没有#include相应的头文件,或者头文件缺少相应的声明。
而重复声明通常是#include了相同的头文件,比如 B.h 和 C.h 都包含 A.h,然后 main.h 包含了 B.h 和 C.h,这就导致A.h 在main中被包含了两次。
解决这个问题的方法是可以在所有.h文件的第一行加上
#pragmaonce
或者,使用#ifndef ... #define ... #endif 语句块
-- 链接错误,常见的错误也是两种,没有定义和重复定义,和上面的没有声明,重复声明类似。(这里定义指的就是函数实现)
先讨论没有定义(undefined reference to xxx)
通常是因为函数有声明,而且被使用了,但是没有被定义。比如上面MyClass.cpp中,如果Fun()没有被实现的话,MyClass.cpp和main.cpp编译时都不会报错,但是链接时会报告找不到Fun()。
当然,如果Fun()没被main.cpp调用的话,即使不实现它,整个构建过程也不会出错,因为链接器根本不会去找这个函数的定义。
然后是重复定义(multiple definition)
指的一份相同的定义在两个目标文件中都存在,链接的时候链接器不知道时用哪个了。这种问题通常由于全局函数,和全局变量定义在了头文件中。导致多个目标文件包含相同的全局函数和全局变量的定义。
解决方法就是在头文件中声明,定义放到cpp文件中,或者为定义加上const 或 static这样的修饰符,链接时会对这些带有const和修饰符的变量特殊处理的。
const只适用于定义常量变量,static定义的是静态全局变量,只在当前cpp有效,所以链接它也不会被别的目标文件链接,就不会有重复定义的问题了。
总之在头文件中定义变量和函数要特别主意,可能会导致链接错误。
当然也不是所有定义都不能放到头文件中,比如刚才说的const常量,static全局变量就是例外,还有内联函数,可以定义在.h文件中,因为内联函数会被拷贝到每个目标文件中,也不会参与链接的过程。
还有模板类必须放在头文件中定义,这个下面会讨论这个。
关于模板,静态成员变量
模板类模板函数必须声明和定义在头文件中,原因是什么,举个例子,假设MyClass是模板类
MyClass.h
MyClass.cpp
main.cpp
编译的时候没有问题,但是链接时会报错,main.cpp找不到MyClass<int>::Fun(),如下图
MyClass虽然定义了Fun函数,但是MyClass.o中存在MyClass<T>::Fun(),而根据MyClass.h文件,main.o中需要找到MyClass<int>::Fun()的定义
结果链接器哪都找不到,只好报错了。(实际上通过objdump查看MyClass.o,编译器都没有生成MyClass<T>::Fun(),因为编译器认为这个函数没人使用,就直接优化掉了)
如果非得在cpp中定义模板类的成员函数呢,有一种方法就是要显示的在cpp文件中声明,比如
MyClass.cpp
加上下面这行就不会有问题了,但是缺点就是开发MyClass的程序员无从知道其他类是怎么使用这个模板的,不可能把所有可能的模板参数全都一一的列在这里。
所以模板类的定义还是要写在.h文件中,
那么如果main.cpp使用到了MyClass<int>, 另外一个cpp也使用到了MyClass<int>,会不会产生重复代码导致重复定义呢,不会,编译器会处理好模板类的。
下面是静态成员变量,为什么静态成员变量的定义要放在cpp里,(模板类的静态成员变量除外)
静态成员变量和静态全局成员变量不同。
静态成员变量的作用域可以是整个工程,而静态全局变量的作用域只是当前的cpp。所以静态成员变量定义在.h中就会发生重定义错误。
希望对大家有帮助!
“我是一名从事了10年开发的老程序员,最近我花了一些时间整理关于C语言、C++,自己有做的材料的整合,一个完整的学习C语言、C++的路线和工具。如果感觉自学C/C++编程有困难的话可以湫湫扫下方二维码。这里是编程爱好者的聚集地,欢迎初学和进阶中的小伙伴。希望你也能凭自己的努力,成为下一个优秀的程序员。
C/C++编程笔记:C/C++ 的编译和链接相关推荐
- Java编程笔记7:内部类
Java编程笔记7:内部类 图源:PHP中文网 所谓的内部类,其实就是定义在类中的类.这和Java编程笔记6:接口 - 魔芋红茶's blog (icexmoon.xyz)中提到的接口嵌套的方式有点相 ...
- Java编程笔记2:初始化和清理
Java编程笔记2:初始化和清理 图源:Java Switch语句(用法详解)-java教程-PHP中文网 构造器 构造器,在编程领域也会被称作构造函数.事实上我觉得这个名称并不是很恰当,可能相当一部 ...
- Java编程笔记6:接口
Java编程笔记6:接口 图源:PHP中文网 在Java中,接口可能只是特指使用interface关键字声明的一种抽象类型,但实际上在UML或者OOP中,接口往往是作为一种底层抽象概念而言的,其具体实 ...
- Java编程笔记9:容器(下)
Java编程笔记9:容器(下) 图源:PHP中文网 本篇文章是Java编程笔记8:容器(上) - 魔芋红茶's blog (icexmoon.xyz)的下篇. Set Set也是一种常见的数据类型,很 ...
- 响应式编程笔记(二):代码编写
2019独角兽企业重金招聘Python工程师标准>>> 响应式编程笔记(二):代码编写 博客分类: 架构 原文:Notes on Reactive Programming Part ...
- NDK 高级编程(笔记)
Android 开发中针对 NDK 的书籍很少,<Pro Android C++ with the NDK>也是出版的比较早的一本书,有些内容可能对现在的开发并不适用.但是书中介绍的内容比 ...
- Java网络编程笔记5
在Java网络编程笔记4中我们看到了客户端与服务器通信的过程,只是在前面的程序只是单个客户端与服务器通信 的例子. 接下来我们看如何实现多个客户端与服务器通信,对于服务器来说,它要为每个客户端请求的S ...
- MSSQL编程笔记四 解决count distinct多个字段的方法
MSSQL编程笔记四 解决count distinct多个字段的方法 参考文章: (1)MSSQL编程笔记四 解决count distinct多个字段的方法 (2)https://www.cnblog ...
- status c语言_C/C++编程笔记:C语言编程风格个人总结,初学小白可借鉴
总结一下我个人的编程风格及这样做的原因吧,其实是为了给实验室写一个统一的C语言编程规范才写的.首先声明,我下面提到的编程规范,是自己给自己定的,不是c语言里面规定的. 一件事情,做成和做好中间可能隔了 ...
- Java TCP/IP Socket 编程 笔记
http://jimmee.iteye.com/blog/617110 http://jimmee.iteye.com/category/93740 Java TCP/IP Socket 编程 笔记( ...
最新文章
- HPAIC人类蛋白质图谱分类挑战赛金牌经验分享
- Netlib文件转化为mps文件
- 设置APP版本跟新提示
- hudo as3 网络通信框架
- php7的稳定性,探索PHP7(一)--性能
- spring项目概念-BeanFactoryApplicationContext
- (转)WIN7更改用户名访问共享文件夹
- 计算机网络实验二抓包协议分析,计算机网络实验-使用Wireshark分析TCP和UDP协议...
- python求完全平方数_【Python】【demo实验6】【练习实例】【完全平方数相关】
- Facebook 发布 PyTorch Hub:一行代码实现经典模型调用!
- 非零基础入门微信小程序
- Linux kernel SMP 中断机制
- 51单片机定时器问题总结
- log4cxx OutputDebugString DebugView dbgview
- 时钟倒计时html,jQuery倒计时/计时器/时间插件
- 结构化数据、半结构化数据和非结构化数据
- Web开发过程流程图
- 4个避免使用npm link的理由
- Java中的BigDecimal,你真的会用吗?
- 程序卡住了?教你如何调试已在运行的程序
热门文章
- oracle自带的sql语言环境变量,Oracle技术网—SQL*Plus系统环境变量有哪些?如何修改?...
- js文件里获取路由 vue_【源码拾遗】从vue-router看前端路由的两种实现
- 磁盘管理来安装linux,Linux_在CentOS系统中安装quota来管理磁盘配额,在网络管理的工作中,由于硬 - phpStudy...
- ajax保存乱码,Ajax 乱码详细
- c语言getline读取一行命令行,如何从文件的特定行中获取getline()? C ++
- java wps linux 安装_ubuntu安装Java开发环境
- 机器学习笔记(十二)——马尔科夫模型
- 公司为什么宁愿花11K月薪招新人,也不愿意花9K的月薪留住老员工?
- b站电脑客户端_B站(哔哩哔哩) 视频批量下载工具#电脑版##更新
- python拟合曲线的方式,Python实现曲线拟合操作示例【基于numpy,scipy,matplotlib库】...