文件"/>
程序与文件
本文介绍代码的管理,而不是代码的编写
- 多个程序文件之间是如何交互
- 头文件和源文件如何相互关联
- 如何管理和控制它们的内容
- 代码量多 易出错,如何调试
1. 可执行代码的产生
1.1 C/C++代码文件
- 源代码(源文件 + 头文件)
- 源文件
.c
.cpp
:包含头文件中声明的所有非内联函数 和 变量的定义 - 头文件
.h
:包括源文件需要的定义和声明,
还可包含模板和类型定义、枚举、常量、函数声明、内联函数和变量定义,
以及已命名的名称空间
- 源文件
- 对象代码
用于标识对象文件的拓展名在不同的机器环境中是不同的- Windows ——
.obj
- Unix ——
.o
- Windows ——
- 可执行程序
.exe
1.2 创建可执行代码的三个步骤
-
预处理指令
将所有
#include
头文件的完整内容复制到 源文件中 -
编译
编译器把每个源文件转换为对象文件,
其中包含了与源文件内容对应的机器码
-
链接
链接器把程序的对象文件合并到包含完整可执行程序的文件中
无论采用哪种编译方式,编译器都把每个源文件看作一个独立的实体,
为每个源文件 .cpp
生成一个对象文件。
然后在链接步骤中,把程序的对象文件和必要的库函数组合到一个可执行文件中。
-
编译是一个迭代的过程
编译链接发现错误 -> 修改 -> 编译链接
-----直到----> 程序按照期望的那样执行
不断反馈迭代,直至符合期望目标
-
程序总是会有错误
一般认为,如果程序超过一定规模,就总是包含错误。
追求简洁实用的代码,不要太复杂 步骤太多
2. 实现程序文件之间的关联
一般来说,基本思想是 让头文件包含函数声明和类型定义,
源文件实现头文件中已声明的函数,且可进一步利用它们来创建额外的函数定义。
头文件的内容通过 #include
预处理指令可用于源文件。
2.1 三个概念
转换单元
每个源文件及其所包含头文件的内容,称为一个转换单元
.cpp
文件是转换单元的基础,编译器会处理转换单元,以生成对象文件
- 编译器独立处理程序中的每个转换单元来生成对象文件。
- 对象文件包含机器码和实体引用的信息。
- 完整程序的对象文件集由链接程序处理,链接程序在对象文件之间建立必要的连接,
以生成可执行程序模块。 - 如果对象文件包含对外部实体的引用,但是在其他任何对象文件中都找不到该实体,
就不可生成可执行程序模块,链接程序也就会生成错误提示信息。
编译和链接转换单元的过程合称为 “转换”
单一定义规则
One Definition Rule,简称 ODR
ODR实际上并不是一个规则,而是一套规则:程序中定义的每个实体类型对应一个规则。
ODR的一般思想,可帮助我们理解如何在多个文件中组织程序的代码,
以及如何解读和解决在违反ODR原则时遇到的编译和链接错误。
在给定的转换单元中,每个变量、函数、类类型、枚举类型 或 模板都只能定义一次。
如果程序中允许存在多个定义,那这些定义也必须完全相同。
例如,变量或函数可有多个声明,但只能有一个定义来确定它是什么,并创建它。
所有的定义都是声明,但并非所有的声明都是定义。
这并不难理解——
我们每个人在这个世界上都是独一无二的,但却有着不同的身份。
在家里,我可以是孩子或父母;在学校,我也可以是学生或老师;
但 他们都是我,我却不完全是他们。
在整个程序内,大部分函数和变量都必须定义一次,且只能定义一次。
不允许有两个定义,即使这两个定义完全相同,且在不同的转换单元内定义。
- 内联函数和内联变量是此规则的例外
- 注意定义头文件内容的方式,确保不会在同一转换单元中出现重复的定义
链接属性
转换单元中的名称在 编译/链接 过程中 处理的方式由 链接属性 确定。
链接属性 指定了由一个名称表示的实体可在代码的什么地方使用。
程序中使用的每个名称要么有链接属性,要么则没有。(看起来好像是废话 哈哈哈)
当某个名称用于在声明该名称的作用域,外部可访问程序的内容时,就有链接属性,
反之,则没有。
当某个名称有链接属性时,就可有 内部链接 和 外部链接。
-
内部链接
该名称表示的实体可在同一转换单元的任何地方访问
即,可在整个转换单元中访问
-
外部链接
具有外部链接属性的名称除了可在定义它的转换单元中访问以外,
其他的转换单元也可访问。即,可在整个程序中访问
-
无链接属性
该名称表示的实体就只能在应用于该名称的作用域中访问。
在代码块中定义的所有名称(局部名称)都没有链接属性。
即,只能在定义它的代码块中访问
无论名称时在头文件中声明,还是在源文件中声明。
应用于名称的链接属性都不受影响。
在转换单元中,每个名称的链接属性都是在 .cpp
文件中插入头文件的内容后才确定的。
2.2 管理和控制
预处理指令
-
预处理是编译器把源文件编译为机器指令之前执行的过程
-
预处理根据预处理指令指定的说明,准备并修改源代码,以进入编译阶段
-
所有的预处理指令都以
#
开头 -
预处理阶段会分析、执行源文件中所有的预处理指令,然后删除它们,
生成一个仅包含之后要编译的C++语句的转换单元
#define 定义一个预处理宏
#undef 取消宏的定义#if 编译预处理中的条件命令, 相当于C语法中的if语句
#ifdef 判断某个宏是否被定义, 若已定义, 执行随后的语句
#ifndef 与#ifdef相反, 判断某个宏是否未被定义#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足, 则执行#elif之后的语句, 相当于C语法中的else-if#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足, 则执行#else之后的语句, 相当于C语法中的else#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
defined 与#if, #elif配合使用, 判断某个宏是否被定义#include 包含文件命令
#include_next 与#include相似, 但它有着特殊的用途#line 标志该语句所在的行号
# 将宏参数替代为以参数值为内容的字符串常量
## 将两个相邻的标记(token)连接为一个单独的标记
#pragma 说明编译器信息#warning 显示编译警告信息
#error 显示编译错误信息
预处理指令有很多实用的功能
按作用分类:
-
定义预处理宏
#define
- ``#`指令指定了所谓的宏,宏是一条重写规则,文本替代
- 在C++中不要用
#define
来定义常量,const
更实用 extern const double pi = 3.1415926
extern
声明之后,可在容易转换单元中使用它#define VALUE
代表删除此指令后面的 VALUE
替代标识符的字符序列没有任何限制,甚至可以不存在,字符串为空
甚至也可以替代关键字- 在C++中,
#define
的主要作用是管理头文件
-
参数宏
- 定义类似于函数的宏【不建议】
-
条件编译【管理和控制 头文件和源文件之间的内容】
- 用此方法可阻止 相同的定义在同一转换单元中多次出现 ODR
- 确保头文件的内容在一个转换单元中没有重复
#ifndef MYHEADER_H //默认大写 #define MYHEADER_H //宏定义//myheader.h is placed here #endif /*#pragma 指令可达到与上述模式相同的效果eg, #pragma once 可防止重复包含头文件的内容 */
-
逻辑预处理指令【可用来测试代码】
#if condition-1#define operate-1 #elif condition-2#define operate-2 #else#define operate-3 #endif
包含头文件
- 包含标准库头文件
#include <iostream>
- 包含自定义头文件
#includee "myheader"
查找头文件的过程取决于 是 < >
尖括号中指定文件名 还是 " "
双引号中指定文件名
< >
编译器只搜索包含标准库头文件的默认目录" "
编译器先搜索当前目录,之后搜索包含标准库头文件的目录
名称空间
名称空间,是用来解决 不同程序员可能给不同的元素使用相同的名称,使程序出错的问题。
名称空间定义了一个作用域——
在这个作用域内声明的所有名称都附加了名称空间的名称。
- 名称空间类似于姓氏,置于该命名空间声明的所有名称的前面
标准库的名称空间 都在 std
命名空间中定义,
std::cout
std::endl
这是它们的全称,
其中 ::
双冒号 称为 作用域解析运算符
mian( )
函数不能定义在名称空间中- 未在名称空间中定义的内容都存在于全局名称空间中,
全局名称空间没有名称 - 不在显示的名称空间作用域内声明的名称都在全局名称空间中
- 一个名称空间可由几个独立的同名名称空间组成
- 在不同名称空间中声明的相同名称是不同的
- 在某名称空间中声明的名称,在这个名称空间中使用时可不加限定符
- 只有当某个同名的局部声明隐藏了全局名称时,才真正需要显式访问全局名称
- 使用名称空间,可把代码分成逻辑组,更加有条理
- 每段代码都是自包含的,从而防止了名称冲突
namespace myRegion{/*code you want to have in the namespaceincluding function definitions and declarationsglobal variables, enum types, templates, etc*/
}
using namespace std;
using namespace myRegion;//在程序中使用名称空间namespace{//未命名的名称空间 更好的限制可访问性//未命名的名称空间 不在/不属于 全局名称空间中//未命名的名称空间在转换单元中是唯一的//code in the namespace
}//嵌套的名称空间
namespace outer{namespace inner{//coding in the second level namespacevoid func();}
}
void outer::inner::func(){}//名称空间的别名
/*确保不发生命名的错误当然要尽量避免使用长名称或多层嵌套的名称
*/
namespace shortName = outer::inner;
void shortName::func(){}
3. 程序调试
源代码,在编译、链接 和 运行过程中,难免出现错误,
查找并修改错误的过程,称为 程序调试。
俗称,
Debug
3.1 程序错误的分类
-
编译时错误:由编译器发现的错误
一般指的是,语法不正确
例如,关键词拼写不正确、括号不匹配等等
-
链接时错误:由链接器发现的错误
例如,调用了并不存在的函数
-
运行时错误/逻辑错误:程序运行时才发现的错误
例如,把
a = a + 1
写成a = a * 5
导致运行结果不符合预期,这就是典型的逻辑错误最难调试的是运行时错误/逻辑错误,因为一般没有任何错误提示
在程序调试的过程中,必须是从第一个错误开始查找和修改,且每改一个错误都要重新编译。因为后面的错误可能是前面的错误引起的。
3.2 调试工具和方法
调试程序的时间占程序开发总时间的一大部分。
程序越大越复杂,可能包含的错误就越多,使其正常运行所需要的时间和精力就越多。
例如,操作系统或复杂的应用程序,都需要定期大补丁和更新。
商业产品中最严重的 bug 一般是安全问题。
3.2.1 调试工具
-
IDE <=> Integrated Development Environment
-
IDE = 图形用户界面 + 编辑器 + 编译器 + 链接器 + 运行 + 程序调试
挖个坑吧,我还是小白,调试工具这部分有经验后,再来专门写个文章
3.2.2 方法
编程程序的方法 / 程序的质量 会影响测试和调试的难度。
- 结构合理的程序
- 每个函数有明确的目的
- 变量名和函数名通俗易懂
- 简洁明了的注释
- 清晰的代码排版 “缩进格式 和 语句布局”
集成调试器
-
跟踪程序流
在执行程序时,一次执行一个语句来执行源代码。
按下特定的键后,执行下一个语句。
-
设置断点
断点标识了程序中选定的语句,每次到达这些断点时,
程序都会暂定,允许查看程序的状态。
按下特定的键后,程序会继续执行到下一个断点
-
设置观察窗口
观察窗口标识了要在程序执行过程中跟踪的特定变量值
-
检查程序元素
在程序执行暂停时,可检查各个程序组件。
例如,在断点处,查看函数的详细信息,返回类型、参数
指针的地址等等
添加跟踪代码
在开发过程中,写单元测试代码
- 条件预处理指令
assert( )
static_assert( )
诊断代码仅在测试程序时才包含在内。
一旦程序可正常工作,就要删除它们。
更多推荐
程序与文件
发布评论