9.1 单独编译
9.1.1 为什么需要单独编译?
大型程序通常分成多个文件,每个文件单独编译,最后链接在一起。这样做的好处:
-
修改一个文件,只需重新编译该文件,不影响其他文件
-
团队协作:不同成员负责不同模块
-
代码复用:公共功能放在独立文件中
典型的多文件项目结构:
┌─────────────────────────────────────────┐
│ coordin.h(头文件) │
│ - 结构体定义 │
│ - 函数原型 │
└──────────┬──────────────────────────────┘
│ #include
┌──────┴──────┐
▼ ▼
coordin.cpp main.cpp
(函数实现) (主程序)
│ │
└──────┬──────┘
▼
链接器
│
▼
可执行程序
9.1.2 头文件的内容规范
cpp
// coordin.h -- 头文件示例
// 头文件保护(防止重复包含)
#ifndef COORDIN_H_
#define COORDIN_H_
// ✅ 头文件中应该包含:
// 1. 函数原型
// 2. 使用 #define 或 const 定义的符号常量
// 3. 结构体声明
// 4. 类声明
// 5. 模板声明
// 6. 内联函数
struct Polar {
double distance; // 距离
double angle; // 角度(弧度)
};
struct Rect {
double x; // 横坐标
double y; // 纵坐标
};
// 函数原型
Polar rectToPolar(Rect xypos);
void showPolar(Polar dapos);
#endif // COORDIN_H_
cpp
// coordin.cpp -- 函数实现文件
#include <iostream>
#include <cmath>
#include "coordin.h" // 包含自定义头文件(用引号)
Polar rectToPolar(Rect xypos)
{
Polar answer;
answer.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
answer.angle = atan2(xypos.y, xypos.x);
return answer;
}
void showPolar(Polar dapos)
{
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "距离 = " << dapos.distance << endl;
cout << "角度 = " << dapos.angle * Rad_to_deg << " 度" << endl;
}
cpp
// main.cpp -- 主程序文件
#include <iostream>
#include "coordin.h"
int main()
{
using namespace std;
Rect rplace;
Polar pplace;
cout << "请输入直角坐标 x 和 y:" << endl;
while (cin >> rplace.x >> rplace.y)
{
pplace = rectToPolar(rplace);
showPolar(pplace);
cout << "继续输入(或 Ctrl+Z 退出):" << endl;
}
return 0;
}
9.1.3 头文件保护
cpp
// 方式1:传统的 #ifndef 保护(最通用)
#ifndef MYHEADER_H_
#define MYHEADER_H_
// 头文件内容...
#endif
// 方式2:#pragma once(现代编译器支持,更简洁)
#pragma once
// 头文件内容...
⚠️ 为什么需要头文件保护?
如果
main.cpp同时包含了a.h和b.h,而b.h也包含了a.h,没有保护的话
a.h的内容会被包含两次,导致重复定义错误。
9.2 存储持续性、作用域和链接性
9.2.1 四种存储持续性
| 存储类型 | 持续时间 | 存储位置 | 示例 |
|---|---|---|---|
| 自动存储 | 代码块执行期间 | 栈(Stack) | 局部变量、函数参数 |
| 静态存储 | 程序整个运行期间 | 静态区 | 全局变量、static 变量 |
| 线程存储 | 线程存在期间 | 线程局部存储 | thread_local 变量(C++11) |
| 动态存储 | 手动控制 | 堆(Heap) | new 分配的内存 |
9.2.2 自动存储持续性
cpp
// auto_storage.cpp -- 自动存储示例
#include <iostream>
void func()
{
int local = 10; // 自动存储:函数调用时创建,返回时销毁
std::cout << "local = " << local << std::endl;
local++;
} // local 在这里销毁
int main()
{
func(); // local = 10
func(); // local = 10(每次调用都重新创建)
func(); // local = 10
// 代码块作用域
{
int block_var = 100; // 只在这个代码块内有效
std::cout << "block_var = " << block_var << std::endl;
}
// block_var 在这里已经销毁
// std::cout << block_var; // ❌ 错误:block_var不在作用域内
return 0;
}
9.2.3 静态存储持续性
静态变量在程序启动时创建,程序结束时销毁,只初始化一次。
cpp
// static_storage.cpp -- 静态存储示例
#include <iostream>
// 1. 静态外部变量(全局变量):整个程序可见
int globalCount = 0;
// 2. 静态内部变量(static全局):只在本文件可见
static int fileCount = 0;
void counter()
{
// 3. 静态局部变量:只在函数内可见,但持续整个程序
static int callCount = 0; // 只初始化一次!
int localCount = 0; // 每次调用都重新初始化
callCount++;
localCount++;
globalCount++;
std::cout << "调用次数(static):" << callCount
<< " 局部计数(auto):" << localCount
<< " 全局计数:" << globalCount << std::endl;
}
int main()
{
counter(); // 调用次数:1 局部计数:1 全局计数:1
counter(); // 调用次数:2 局部计数:1 全局计数:2
counter(); // 调用次数:3 局部计数:1 全局计数:3
return 0;
}
输出:
调用次数(static):1 局部计数(auto):1 全局计数:1
调用次数(static):2 局部计数(auto):1 全局计数:2
调用次数(static):3 局部计数(auto):1 全局计数:3
💡 静态局部变量的典型应用:计数器、单例模式、缓存等需要"记住上次状态"的场景。
9.2.4 作用域与链接性
作用域(Scope):变量在哪里可以被访问
链接性(Linkage):变量在多个文件间的可见性
┌─────────────────────────────────────────────┐
│ 链接性类型 │
├──────────────┬──────────────────────────────┤
│ 外部链接性 │ 可在多个文件中访问(全局变量) │
│ 内部链接性 │ 只在本文件中访问(static全局) │
│ 无链接性 │ 只在代码块中访问(局部变量) │
└──────────────┴──────────────────────────────┘
cpp
// file1.cpp -- 演示链接性
#include <iostream>
// 外部链接性:其他文件可以用 extern 访问
int sharedVar = 100;
// 内部链接性:只有本文件可以访问
static int privateVar = 200;
// 外部链接性的函数
void showShared()
{
std::cout << "sharedVar = " << sharedVar << std::endl;
}
cpp
// file2.cpp -- 访问其他文件的变量
// extern 声明:告诉编译器变量在其他文件中定义
extern int sharedVar; // 声明,不是定义
// extern int privateVar; // ❌ 错误:privateVar是内部链接性
void modifyShared()
{
sharedVar = 999; // 可以访问和修改
}
9.2.5 变量的作用域规则
cpp
// scope_rules.cpp -- 作用域规则示例
#include <iostream>
int x = 1; // 全局变量
void func()
{
int x = 2; // 局部变量,遮蔽全局变量
std::cout << "func内 x = " << x << std::endl; // 2
{
int x = 3; // 更内层的局部变量
std::cout << "内层块 x = " << x << std::endl; // 3
std::cout << "全局 x = " << ::x << std::endl; // 1(::访问全局)
}
std::cout << "func内 x = " << x << std::endl; // 2(内层x已销毁)
}
int main()
{
std::cout << "main前 x = " << x << std::endl; // 1
func();
std::cout << "main后 x = " << x << std::endl; // 1(全局未改变)
int x = 10; // 局部变量遮蔽全局
std::cout << "局部 x = " << x << std::endl; // 10
std::cout << "全局 x = " << ::x << std::endl; // 1
return 0;
}
💡 作用域解析运算符
:::::变量名可以访问被遮蔽的全局变量。
9.3 说明符和限定符
9.3.1 存储说明符
cpp
// storage_specifiers.cpp -- 存储说明符示例
#include <iostream>
// auto(C++11前):自动存储,现在auto用于类型推断
// register:建议编译器将变量存入寄存器(现代编译器会忽略)
// static:静态存储
// extern:外部链接性声明
// mutable:允许const对象的成员被修改
struct Config {
int id;
mutable int accessCount; // 即使Config是const,也可以修改
void access() const
{
accessCount++; // ✅ mutable成员可以在const函数中修改
}
};
int main()
{
using namespace std;
const Config cfg = {1, 0};
cfg.access();
cfg.access();
cout << "访问次数:" << cfg.accessCount << endl; // 2
// static局部变量
auto countCalls = []() {
static int count = 0;
return ++count;
};
cout << countCalls() << endl; // 1
cout << countCalls() << endl; // 2
cout << countCalls() << endl; // 3
return 0;
}
9.3.2 cv 限定符(const 和 volatile)
cpp
// cv_qualifiers.cpp -- const和volatile示例
#include <iostream>
int main()
{
using namespace std;
// const:值不能被修改
const int MAX_SIZE = 100;
// MAX_SIZE = 200; // ❌ 错误
// const指针的四种形式
int a = 10, b = 20;
int* p1 = &a; // 普通指针:指向和值都可改
const int* p2 = &a; // 指向const的指针:值不可改,指向可改
int* const p3 = &a; // const指针:指向不可改,值可改
const int* const p4 = &a; // 指向const的const指针:都不可改
*p1 = 15; // ✅ 可以修改值
p1 = &b; // ✅ 可以改变指向
// *p2 = 15; // ❌ 不能修改值
p2 = &b; // ✅ 可以改变指向
*p3 = 15; // ✅ 可以修改值
// p3 = &b; // ❌ 不能改变指向
// *p4 = 15; // ❌ 不能修改值
// p4 = &b; // ❌ 不能改变指向
cout << "a = " << a << endl;
// volatile:告诉编译器变量可能被外部因素改变,不要优化
// 常用于硬件寄存器、多线程共享变量
volatile int hardware_reg = 0; // 每次都从内存读取,不缓存
return 0;
}
const指针总结:
| 声明 | 指向可改 | 值可改 | 记忆方法 |
|---|---|---|---|
int* p |
✅ | ✅ | 普通指针 |
const int* p |
✅ | ❌ | const在*左边:值不变 |
int* const p |
❌ | ✅ | const在*右边:指向不变 |
const int* const p |
❌ | ❌ | 两个const:都不变 |
9.4 名称空间(namespace)
9.4.1 为什么需要名称空间?
cpp
// 问题:不同库中可能有同名函数/变量,导致命名冲突
// 例如:两个库都定义了 fetch() 函数
// 库A
void fetch() { /* 从数据库获取 */ }
// 库B
void fetch() { /* 从网络获取 */ }
// ❌ 冲突!编译器不知道调用哪个fetch()
名称空间通过将名称封装在独立的区域来解决命名冲突。
9.4.2 定义和使用名称空间
cpp
// namespace_basic.cpp -- 名称空间基础
#include <iostream>
// 定义名称空间
namespace Jack {
double pail; // 变量
void fetch(); // 函数原型
int pal;
struct Well { int depth; };
}
namespace Jill {
double bucket(double n) { return n * 2; }
double fetch; // 与Jack::fetch不冲突
int pal;
struct Hill { int height; };
}
// 在名称空间外定义成员函数
void Jack::fetch()
{
std::cout << "Jack::fetch() 被调用" << std::endl;
}
int main()
{
using namespace std;
// 方式1:作用域解析运算符(最明确)
Jack::pail = 12.34;
Jill::fetch = 56.78;
Jack::fetch();
cout << "Jack::pail = " << Jack::pail << endl;
cout << "Jill::fetch = " << Jill::fetch << endl;
// 方式2:using声明(引入单个名称)
using Jill::bucket;
cout << "bucket(5) = " << bucket(5) << endl;
// 方式3:using编译指令(引入整个名称空间)
using namespace Jack;
cout << "pail = " << pail << endl; // Jack::pail
return 0;
}
9.4.3 名称空间的嵌套
cpp
// namespace_nested.cpp -- 嵌套名称空间
#include <iostream>
namespace Outer {
int x = 10;
namespace Inner {
int x = 20; // 与Outer::x不冲突
void show()
{
std::cout << "Inner::x = " << x << std::endl;
std::cout << "Outer::x = " << Outer::x << std::endl;
}
}
void show()
{
std::cout << "Outer::x = " << x << std::endl;
}
}
int main()
{
Outer::show(); // Outer::x = 10
Outer::Inner::show(); // Inner::x = 20, Outer::x = 10
// using简化嵌套访问
using namespace Outer::Inner;
show(); // 调用Inner::show()
return 0;
}
9.4.4 using 声明 vs using 编译指令
cpp
// using_comparison.cpp -- using声明与编译指令对比
#include <iostream>
namespace A {
int x = 1;
void func() { std::cout << "A::func()" << std::endl; }
}
namespace B {
int x = 2;
void func() { std::cout << "B::func()" << std::endl; }
}
void demo_using_declaration()
{
// using声明:引入特定名称,如有冲突会报错
using A::x;
using A::func;
// using B::x; // ❌ 错误:x已经被A::x占用
std::cout << "x = " << x << std::endl; // 1
func(); // A::func()
}
void demo_using_directive()
{
// using编译指令:引入整个名称空间
// 如有冲突,局部变量优先,否则需要显式指定
using namespace A;
using namespace B;
// x; // ❌ 二义性:A::x 还是 B::x?
std::cout << "A::x = " << A::x << std::endl; // 必须显式指定
std::cout << "B::x = " << B::x << std::endl;
}
int main()
{
demo_using_declaration();
demo_using_directive();
return 0;
}
using声明 vs using编译指令对比:
| 特性 | using A::name |
using namespace A |
|---|---|---|
| 引入范围 | 单个名称 | 整个名称空间 |
| 冲突处理 | 编译错误 | 需显式指定 |
| 推荐程度 | ✅ 推荐 | ⚠️ 谨慎使用 |
| 适用场景 | 精确控制 | 快速原型/小程序 |
9.4.5 名称空间的其他特性
cpp
// namespace_features.cpp -- 名称空间高级特性
#include <iostream>
// 1. 开放性:可以分多次添加成员
namespace MyLib {
int version = 1;
}
// 在其他地方继续添加
namespace MyLib {
void hello() { std::cout << "Hello from MyLib v" << version << std::endl; }
double pi = 3.14159;
}
// 2. 匿名名称空间(替代static全局变量)
namespace {
int secret = 42; // 只在本文件可见,相当于static
void helper() { std::cout << "内部辅助函数" << std::endl; }
}
// 3. 名称空间别名
namespace VeryLongNamespaceName {
void func() { std::cout << "长名称空间的函数" << std::endl; }
}
namespace VLN = VeryLongNamespaceName; // 别名
int main()
{
using namespace std;
// 使用开放名称空间
MyLib::hello();
cout << "MyLib::pi = " << MyLib::pi << endl;
// 使用匿名名称空间(直接访问,无需前缀)
cout << "secret = " << secret << endl;
helper();
// 使用别名
VLN::func();
return 0;
}
9.4.6 std 名称空间的正确使用
cpp
// std_namespace.cpp -- std名称空间的使用建议
#include <iostream>
#include <string>
#include <vector>
// ❌ 不推荐:在头文件中使用 using namespace std
// 会污染所有包含该头文件的文件
// ✅ 推荐方式1:在函数内部使用 using namespace std
void func1()
{
using namespace std;
cout << "在函数内使用 using namespace std" << endl;
}
// ✅ 推荐方式2:using声明(只引入需要的名称)
void func2()
{
using std::cout;
using std::endl;
using std::string;
string s = "Hello";
cout << s << endl;
}
// ✅ 推荐方式3:直接使用 std:: 前缀(最规范)
void func3()
{
std::string s = "World";
std::cout << s << std::endl;
}
int main()
{
func1();
func2();
func3();
return 0;
}
9.5 综合示例:多文件项目模拟
以下模拟一个完整的多文件项目(在单文件中演示概念):
cpp
// multi_file_simulation.cpp -- 多文件项目综合示例
#include <iostream>
#include <string>
#include <vector>
// ===== 模拟 math_utils.h =====
namespace MathUtils {
const double PI = 3.14159265358979;
const double E = 2.71828182845905;
double circleArea(double r);
double sphereVolume(double r);
bool isPrime(int n);
}
// ===== 模拟 string_utils.h =====
namespace StringUtils {
std::string toUpper(std::string s);
std::string toLower(std::string s);
int countChar(const std::string& s, char c);
std::string repeat(const std::string& s, int n);
}
// ===== 模拟 math_utils.cpp =====
namespace MathUtils {
double circleArea(double r)
{
return PI * r * r;
}
double sphereVolume(double r)
{
return (4.0 / 3.0) * PI * r * r * r;
}
bool isPrime(int n)
{
if (n < 2) return false;
for (int i = 2; i * i <= n; i++)
if (n % i == 0) return false;
return true;
}
}
// ===== 模拟 string_utils.cpp =====
#include <cctype>
namespace StringUtils {
std::string toUpper(std::string s)
{
for (char& c : s) c = toupper(c);
return s;
}
std::string toLower(std::string s)
{
for (char& c : s) c = tolower(c);
return s;
}
int countChar(const std::string& s, char c)
{
int count = 0;
for (char ch : s)
if (ch == c) count++;
return count;
}
std::string repeat(const std::string& s, int n)
{
std::string result;
for (int i = 0; i < n; i++)
result += s;
return result;
}
}
// ===== 模拟 main.cpp =====
int main()
{
using namespace std;
// 使用 MathUtils
cout << "===== 数学工具 =====" << endl;
cout << "PI = " << MathUtils::PI << endl;
cout << "半径5的圆面积:" << MathUtils::circleArea(5) << endl;
cout << "半径3的球体积:" << MathUtils::sphereVolume(3) << endl;
cout << "\n100以内的质数:";
for (int i = 2; i <= 100; i++)
if (MathUtils::isPrime(i))
cout << i << " ";
cout << endl;
// 使用 StringUtils
cout << "\n===== 字符串工具 =====" << endl;
using namespace StringUtils; // 局部using
string s = "Hello, World!";
cout << "原字符串:" << s << endl;
cout << "转大写:" << toUpper(s) << endl;
cout << "转小写:" << toLower(s) << endl;
cout << "'l'的个数:" << countChar(s, 'l') << endl;
cout << "重复3次:" << repeat("Ha", 3) << endl;
// 静态局部变量示例
cout << "\n===== 静态变量示例 =====" << endl;
auto makeCounter = [](int start = 0) {
static int count = start;
return ++count;
};
for (int i = 0; i < 5; i++)
cout << "计数:" << makeCounter() << endl;
return 0;
}
输出:
===== 数学工具 =====
PI = 3.14159
半径5的圆面积:78.5398
半径3的球体积:113.097
100以内的质数:2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
===== 字符串工具 =====
原字符串:Hello, World!
转大写:HELLO, WORLD!
转小写:hello, world!
'l'的个数:3
重复3次:HaHaHa
===== 静态变量示例 =====
计数:1
计数:2
计数:3
计数:4
计数:5
📝 第9章知识点总结
| 知识点 | 核心要点 |
|---|---|
| 单独编译 | 头文件(声明)+ 实现文件(定义)+ 主文件,用 #ifndef 防止重复包含 |
| 头文件内容 | 函数原型、结构体/类声明、const常量、模板、内联函数 |
| 自动存储 | 局部变量,代码块结束时销毁,存储在栈上 |
| 静态存储 | 全局变量和 static 变量,程序运行期间一直存在,只初始化一次 |
| 动态存储 | new/delete 管理,存储在堆上,手动控制生命周期 |
| 外部链接性 | 全局变量,多文件可见,用 extern 声明 |
| 内部链接性 | static 全局变量,只在本文件可见 |
作用域解析 :: |
访问被遮蔽的全局变量或名称空间成员 |
| const指针 | const int*(值不变)vs int* const(指向不变) |
| volatile | 告诉编译器不要优化该变量,每次从内存读取 |
| mutable | 允许 const 对象的特定成员被修改 |
| namespace | 解决命名冲突,用 :: 访问,可嵌套、可开放、可设别名 |
| 匿名namespace | 替代 static 全局变量,限制文件内可见性 |
| using声明 | using A::name,引入单个名称,冲突时报错 |
| using编译指令 | using namespace A,引入全部,谨慎使用 |