02. 模块系统
02. 模块系统
简概
概念、底层原理(C++)
C++库函数
- C++库函数存储在库文件中,编译程序时会自动在库文件搜索你使用的函数(但好像还是要写头文件?)
- 即库文件包括很多头文件咯。库文件是工具仓库,头文件(库)是工具箱,头文件函数是工具
模块系统的作用:
提供函数原型的作用:简单来讲,头文件包括了库的函数原型的声明
为什么C/C++中有标准库头文件
这也是由于C/C++相较于其他一些语言的一个不同所导致的,C/C++使用函数前需要先声明函数原型
而头文件除了提供模块以外,另一个作用是提高声明函数原型
而像Python这种不需要写函数原型声明的语言,其直接使用解释器内嵌的内置函数,就不需要标准库的存在了
只有使用非内置函数、使用其他模块时,才需要声明
C/C++没有编译时自动加入标准库的操作,说是“标准库”,但其实不存在真正意义上的标准库
stdio/iostream里最重要的其实还是输入输出流,但并不必须。有的文件并不需要引入
C++中不使用头文件cmath,也能运行sqrt函数?
- MSVC STL在实现iostream的数值输入/输出时用了
<cmath>
中的一些函数(比如ldexp
),所以<iostream>
间接包含了<cmath>
- 不过这是实现细节,随时可能会改
- MSVC STL在实现iostream的数值输入/输出时用了
头文件
头文件声明
有两种
- 使用
<>
包裹,如<coo din.h>
- 编译器会在存储标准头文件的主机系统的文件系统中查找
- 使用
""
包裹,如"<coordin.h"
- 编译器首先查找当前的工作目录或源代码目录(或编译器指定其他目录) 如果没有找到头文件,则编译器再在标准位置查找
- 在包含自己的头文件时,应使用引号而不是尖括号
头文件内容
头文件可以放的东西
- 可放
- 结构声明
- 函数模板声明
- 内联函数定义:原因:会被预处理掉
- 类声明中的函数定义:原因:就算不加
inline
也会被视作内联函数 - const常量:原因详见内存管理一章的底层原理,其第一次使用时才分配内存
- enum枚举:原因:未知
- priveta中函数定义(不用加inline为内联函数):原因:这种函数并不会在客户程序中定义,当多个文件引用一个头文件时当然也并不会起冲突
- 不可放
- 普通函数定义
- 变量声明(定义)
应该放的东西
- 类声明头文件
- 函数/类模板声明:不能放实现文件,其不能被独立编译
- 内联函数定义:不能放实现文件,其不能被独立编译
- 类实现文件
- 客户代码文件
不可放的原因
- 头文件的引用
#include
使用的是预处理机制 - 在头文件中定义会导致多个文件定义同一样东西,违反C++的单一定义原则
类与头文件
常见问题:
可以在实现代码中定义变量而不在头文件中声明吗?
可以,但这样定义出来的变量的作用域是局部的,即无法被其他类函数使用,只能使用函数传参(使用起来不方便,放了相当于有全局变量方便点)
一般临时变量可以这样做。即使是new出来的变量,指向他的指针也会在头文件中声明
头文件 —— 宏
常用宏
其他
- 宏定义,
#define
- 文件包含,
#include
- 条件编译,
#if
QT的宏
Q_OBJECT,Qt元对象系统
QT_BEGIN_NAMESPACE,命名空间
QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE
QT_FORWARD_DECLARE_CLASS,和头文件#include作用类似
C++常用宏
#undef XXX
为解除定义#error XXXX
条件编译宏
条件编译宏
#ifdef XXX - #endif
,如果定义#ifndef XXX - #endif
,如果没定义,if not defined的缩写。用途:避免重复包含头文件#if !defined XXX - #endif
,如果没定义#if defined XXX - #endif
,如果定义。用途:是根据你是否定义了XXX这个宏,而使用不同的代码#elif
#else
- 区别
- #ifdef只能判断单一的宏是否定义,而#if defined()可以组成复杂的判别条件
- 对于单一的宏AAA来说,#ifdef AAA和#if defined(AAA)是完全相同的
- 而要组成复杂的判别条件,用#if defined()就灵活方便了,比如:
#if defined(AAA) && (BBB >= 10)
以#ifndef
为例
简概
- 同一个文件中只能将同一个头文件包含一次
- 但对于嵌套包含头文件或者其他情况,可能会包含多次
- 标准C/C++的一种技术可以避免多次包含同一头文件,其基于预处理编译指令
#ifndef
(if not defined)
使用
举例:(写在头文件中,而不是引用头文件的调用函数中)
#ifndef COORDIN_H_ // 如果没有定义 #define COORDIN_H_ // 那么就定义(预编译定义常量) // ... #endif // 否则跳到此处、编译器不读取中间的内容(已经定义的话) /* ... * ... * ... */ #ifndef COORDIN_H_ // 当定义两次时,这里因为前面已经定义了,所以跳过这一块 #define COORDIN_H_ // ... #endif
原理
编译器过程
编译器首次遇到该文件时,名称
COORDIN_H_
没有定义 (根据include文件名来选择名称,并加上一些下划线,以创建一个其他地方不会被定义的名称)在这种情况下,编译器将查看
#ifndef
和#endif
之间的内容,并读取定义COORDIN_H_
的一行如果在同一个文件遇到其他包含coordin.h的代码,编译器将知道
COORDIN_H_
已经被定义了,从而跳到#endif
后面的一行上
原理总结
- 这种方法不防止编译器将文件包含两次,只是让它忽略除第一次包含之外的所有内容 (大多数标准C和C++头文件使用这种防护方案,在同一个文件中定义同一个结构两次不会导致编译错误)
两种防止头文件重复包含的方式
为了避免同一个头文件被包含(include)多次,C/C++中有两种宏实现方式:一种是#ifndef方式;另一种是#pragma once方式
#ifndef
方式
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
// ...
#endif // MAINWINDOW_H
#pragma once
方式
#pragma once
// ...
区别:在能够支持这两种方式的编译器上,二者并没有太大的区别。但两者仍然有一些细微的区别
#ifndef
#ifndef
的方式受C/C++语言标准支持。它不仅可以保证同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含- 缺点:如果不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,但编译器却硬说找不到声明的状况——这种情况有时非常让人郁闷
#pragma once
- 一般由编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是指物理上的一个文件,而不是指内容相同的两个文件
- 你无法对一个头文件中的一段代码作pragma once声明,而只能针对文件
- 好处:你不必再担心宏名冲突了,当然也就不会出现宏名冲突引发的奇怪问题。大型项目的编译速度也因此提高了一些
- 缺点:如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名冲突引发的“找不到声明”的问题,这种重复包含很容易被发现并修正
- 缺点2:这种方式不支持跨平台
模块设计
编译和链接
- C++孤立将组建单独编译,然后在链接成可执行的程序(C++编译器既编译程序,也管理链接器)
- 这种做法的优点:如果只修改了一个文件,只需要重新编译该文件再链接。而无需所有文件一起重新编译
程序结构:可以分成三部分(原型、定义、调用,三者分离),这种组织方法模块设计与OOP设计一致
- 头文件:包含结构声明和使用这些结构的函数的原型
- 源代码文件1:包含与结构有关的函数的定义代码
- 源代码文件2:包含调用与结构相关的函数的代码
名称空间
名称空间
简概
- 名称空间原理:通过定义一种新的声明区域(声明区域详见内存管理思想一章)来创建命名的名称空间
- 名称空间与文件的关系
- 文件中可有不同的名称空间
- 不同文件可以有相同的名称空间
- 即两者没有定性的包含关系
- 叫法
- 未被装饰的名称称为
未限定的名称
(unqualified name),如cout
- 包含名称空间名的名称称为
限定的名称
(qualigied name),如std::cout
- 未被装饰的名称称为
使用
声明
namespace Jack{ int n; // ... } namespace Jill { // ... } /* 与类结合使用 */ namespace VECTOR { class Vector { // 与类的使用 } } /* 在QT中的使用 */ QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE
使用(三种方法)
- 作用域解析运算符:
名称空间名
+作用域解析运算符::
+里面的成员
,举例:Jack::n;
、std::cout <<
- using编译指令:
using namespace 名称空间名;
,举例:usign namespace std;
- using声明方法:
using namespace
- 作用域解析运算符:
其他特性
注意项
- 导入名称是,首选作用域解析运算符和using声明方法
- 而对于using声明,首选使用将其作用域设置为局部而不是全局
- 不要在头文件中使用using编译指令,否则可能会掩盖了要让那些名称可用,而且包含头文件的顺序可能会影响程序行为
名称空间可以嵌套:例如
namespace nna{namespace nnb{int n;}}
,nna::nnb::n;
可以使用匿名名称空间:例如
namespace {int spn;}
- 这种用法等价于后面跟着using指令一样
- 原理:其潜在作用域为:从声明点到声明区域结尾
- 使用场景:提供了链接性为内部的静态变量的代替品(可以批量定义静态变量),其他文件无法连接访问
使用场景
- 取代静态全局变量、以及外部全局变量的用法
- 如果开发了一个函数库或者类库,应该放在一个名称空间中
- 作为将旧代码转换为使用名称空间代码的权宜之计
单独编译
先单独编译,后链接
- C++提倡这种做法,能节省编译的效率。但这会引发一些问题(多个库的链接)
多个库的链接
多个库链接 x 名称修饰(可能存在的问题)
- 名称修饰对于二进制模块链接的影响
- C++标准运行每个
编译器设计人员
以他认为的何时方式实现名称修饰,两个编译器将为同一个函数生成不同的修饰名 - 所以它们各自编译的二进制模块可能无法正确链接
- C++标准运行每个
- 解决方案
- 确保所有对象文件或库都是同一个编译器生成的
- 如果有源代码,则可以用自己的编译器重新编译源代码来消除链接错误
特殊操作
“跨工程”调用头文件
参考:【CSDN】error LNK2019: 无法解析的外部符号....该符号在函数 ...中被引用
问题描述
在Qt中实验时发现了一个问题 两个项目:A.h、A.cpp、B.h、B.cpp,其中项目B要使用项目A中的类 _ B.h:#include A.h 报错:error: LNK2019: 无法解析的外部符号......中引用了该符号 _ B.h:#include A.h、#include A.cpp 报错 _ B.h:#include A.h B.cpp:#include A.cpp 正确运行 _ 然后我懵了......这是种什么操作?我都不知道怎么在搜索引擎描述这个问题了
原理
首先,#include是“预处理机制”机制对吧,除去条件编译等操作,大概相当于把被include的头文件拷贝了一份到当前头文件 那么,#include cpp他也是同理的
如果是在同一个工程内,不需要include cpp这一步。是否可以理解为,如果是分别在两个工程内,两个工程已经被分别编译了,你B工程没办法知道A工程的源代码,所以include头文件无效
而include cpp的工程相当于是将A工程的源代码拷贝到B工程里面
然后看上去B工程使用了A工程里面的类,但事实上只是一份拷贝
总结
这种操作也就实验一下,耦合很高