C++内存模型和名称空间 --- 详细知识点总结
1. 单独编译
1.1 什么是单独编译?
为什么需要单独编译?
假设你写了一个1000行的程序,每次修改一行代码,编译器都要重新编译全部1000行,非常浪费时间。单独编译允许你把程序拆成多个文件,哪个文件改了就只编译哪个文件,最后再"拼装"起来。
简单比喻: 就像做一本杂志,封面设计改了,只需要重新印封面,不需要把里面的广告页也重新印。
1.2 文件拆分的三类结构
| 文件类型 | 内容 | 扩展名 |
|---|---|---|
| 头文件 | 函数声明、类定义、常量、模板 | .h |
| 实现文件 | 函数的具体实现 | .cpp |
| 主文件 | main()函数,调用其他功能 |
.cpp |
1.3 具体案例
头文件 coordin.h:
cpp
#ifndef COORDIN_H // 防止重复包含
#define COORDIN_H
struct polar {
double distance;
double angle;
};
struct rect {
double x;
double y;
};
// 函数声明(告诉编译器:有这个函数,具体代码在别的文件里)
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);
#endif
实现文件 coordin.cpp:
cpp
#include "coordin.h" // 包含自己的头文件
#include <cmath>
// 函数的具体实现
polar rect_to_polar(rect xypos) {
polar result;
result.distance = sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
result.angle = atan2(xypos.y, xypos.x);
return result;
}
void show_polar(polar dapos) {
cout << "距离=" << dapos.distance
<< ", 角度=" << dapos.angle * 180 / 3.14159;
}
主文件 main.cpp:
cpp
#include <iostream>
#include "coordin.h" // 包含头文件就能用里面的函数
int main() {
rect rplace = {3.0, 4.0};
polar pplace = rect_to_polar(rplace);
show_polar(pplace);
return 0;
}
编译命令(用g++):
bash
g++ main.cpp coordin.cpp -o program
关键点:
#ifndef/#define/#endif叫做头文件保护符,防止同一个头文件被包含多次导致重复定义错误。
2. 存储持续性、作用域和链接性
2.1 三个核心概念
| 概念 | 含义 | 通俗解释 |
|---|---|---|
| 存储持续性 | 变量存在多长时间? | 变量的"寿命"------从出生到消亡 |
| 作用域 | 变量在哪里能被看见? | 变量的"可视范围" |
| 链接性 | 变量能否跨文件使用? | 变量的"社交能力"------能不能被别的文件认识 |
2.2 为什么要同时理解这三个概念?
一个实际问题: 你在一个文件里定义了变量 count,在另一个文件里也定义了 count,编译器怎么知道你用的是哪个?这就是链接性要解决的问题。如果不理解这些,就会遇到"未定义引用"、"重复定义"等编译错误却不知道怎么修。
3. 作用域和链接
3.1 作用域的种类
作用域 = 变量的有效范围
cpp
#include <iostream>
using namespace std;
int global_var = 100; // 全局作用域:整个程序都能看到
void funcA() {
int a = 10; // 局部作用域:只有funcA里能看到a
cout << a; // OK
// cout << b; // 错误!funcB的b在这里看不到
}
void funcB() {
int b = 20; // 另一个局部作用域
cout << global_var; // OK:全局变量哪里都能看到
}
int main() {
funcA();
funcB();
// cout << a; // 错误!a只在funcA里存在
return 0;
}
3.2 链接性的种类
| 链接性 | 含义 | 例子 |
|---|---|---|
| 外部链接 | 可以在其他文件中使用 | 普通的全局函数、不加static的全局变量 |
| 内部链接 | 只能在本文件使用 | 加了static的全局变量/函数 |
| 无链接 | 只在当前代码块内有效 | 局部变量 |
4. 自动存储持续性
4.1 什么是自动存储?
"自动" = 函数调用时自动创建,函数结束时自动销毁
这是最常见的变量类型。你在函数里写的局部变量,默认都是自动存储。
cpp
void demo() {
int x = 10; // 自动变量:进入函数时创建
double y = 3.14; // 自动变量:进入函数时创建
// 函数结束时,x和y自动被销毁(内存被回收)
}
int main() {
demo();
// demo()里的x和y已经不存在了
return 0;
}
比喻: 自动变量就像酒店房间------你入住时(函数调用)前台给你分配房间,退房时(函数返回)房间就清空了,别人可以住进去。
4.2 自动变量的初始化
cpp
void func() {
int x; // 未初始化:值是随机的("垃圾值")
int y = 5; // 显式初始化
int z(10); // 括号初始化
int a = x; // 用未初始化的x来初始化a,a也是随机值!
}
最佳实践: 局部变量一定要初始化,否则里面的值是不确定的,会导致莫名其妙的bug。
4.3 auto 关键字(C++11)
auto 让编译器自动推断变量类型:
cpp
auto x = 10; // 推断为int
auto y = 3.14; // 推断为double
auto z = "hello"; // 推断为const char*
auto flag = true; // 推断为bool
什么时候用auto? 当类型很长或者你懒得写的时候:
cpp
// 不用auto
std::vector<std::string>::iterator it = vec.begin();
// 用auto
auto it = vec.begin();
4.4 栈的工作原理
自动变量存储在栈 上。栈的特点是后进先出(LIFO):
cpp
void funcA() {
int a = 1; // 栈:[a]
}
void funcB() {
int b = 2; // 栈:[b]
funcA(); // 栈:[b, a]
// funcA结束,栈:[b]
} // funcB结束,栈:[]
5. 静态持续变量
5.1 什么是静态持续?
"静态" = 程序运行期间一直存在,不会自动销毁
静态变量在程序启动时分配内存,程序结束时才释放。和自动变量的区别:
| 对比项 | 自动变量 | 静态变量 |
|---|---|---|
| 寿命 | 函数执行期间 | 整个程序运行期间 |
| 内存位置 | 栈 | 静态/全局存储区 |
| 初始化 | 每次调用都会执行 | 只在程序启动时初始化一次 |
| 默认初值 | 无(随机值) | 0(或等价值) |
cpp
void counter() {
static int count = 0; // 只在第一次调用时初始化为0
count++;
cout << "调用次数: " << count << endl;
}
int main() {
counter(); // 输出:调用次数: 1
counter(); // 输出:调用次数: 2(count没有被重置!)
counter(); // 输出:调用次数: 3
return 0;
}
比喻: 自动变量像便利贴,撕掉就没了;静态变量像刻在石头上的字,一直在那里。
6. 静态持续性、外部链接性
6.1 什么是外部链接性的静态变量?
通俗解释: 这是"全局变量"------在一个文件里定义,其他文件也能用。
声明方式: 在所有函数外面 定义(不加static)。
6.2 具体案例
文件1 globals.cpp:
cpp
#include <iostream>
using namespace std;
int count = 0; // 外部链接性的静态变量
double total = 0.0; // 外部链接性的静态变量
void add_to_total(double amount) {
total += amount;
count++;
}
文件2 main.cpp:
cpp
#include <iostream>
using namespace std;
// 用extern告诉编译器:这个变量定义在别的文件里
extern int count;
extern double total;
void add_to_total(double amount); // 函数声明
int main() {
add_to_total(10.5);
add_to_total(20.3);
cout << "总数: " << total << ", 次数: " << count << endl;
// 输出:总数: 30.8, 次数: 2
return 0;
}
6.3 extern 的两种用法
cpp
extern int count; // 声明:告诉编译器"count在别的地方定义了"
extern int count = 5; // 定义并初始化:创建了一个新的全局变量count
注意: 如果在头文件里写
extern int count;并在多个cpp文件中包含它,每个文件用的都是同一个count,这就是"外部链接"的含义。
6.4 命名冲突问题
cpp
// 文件A.cpp
int x = 10; // 全局变量x
// 文件B.cpp
int x = 20; // 也定义了全局变量x → 链接时会报错!
// 因为两个x"撞名"了
解决方案: 用static关键字(见下一节)。
7. 静态持续性、内部链接性
7.1 什么是内部链接性?
加了 static 的全局变量/函数,只能在本文件中使用。
就像给变量上了一把锁,别的文件看不到它。
7.2 具体案例
文件1 module_a.cpp:
cpp
#include <iostream>
using namespace std;
static int secret = 42; // 内部链接:只有这个文件能看到secret
void show_secret() {
cout << "secret = " << secret << endl;
}
文件2 main.cpp:
cpp
void show_secret(); // 函数声明(show_secret是外部链接,可以用)
// extern int secret; // 如果取消注释,编译会报错!
// 因为secret是internal linkage,main.cpp看不到
int main() {
show_secret(); // OK:通过函数间接访问
// cout << secret; // 错误!secret在main.cpp里不可见
return 0;
}
7.3 static vs 没有 static
cpp
// 没有static → 外部链接 → 所有文件共享
int shared_var = 1; // 全局共享
// 有static → 内部链接 → 只有本文件能用
static int private_var = 2; // 本文件私有
7.4 为什么要用内部链接?
解决命名冲突。 假设你和同事各自写了一个 .cpp 文件,都想用变量 data:
cpp
// 同事的 file1.cpp
static int data = 100; // 同事的data
// 你的 file2.cpp
static int data = 200; // 你的data
// 两个data互不干扰!
比喻: 外部链接的变量像公共WiFi,谁都能连;内部链接的变量像你的手机热点,只有你自己能用。
8. 静态存储持续性、无链接性
8.1 什么是无链接的静态变量?
在函数内部用 static 声明的变量------函数结束后不销毁,但别的函数也看不到它。
这是之前讲过的"计数器"那个例子:
cpp
void funcA() {
static int count = 0; // 无链接的静态变量
count++;
cout << "funcA调用了 " << count << " 次" << endl;
}
void funcB() {
// count在这里看不到(无链接 = 别的函数不认识它)
static int local_count = 0;
local_count++;
cout << "funcB调用了 " << local_count << " 次" << endl;
}
int main() {
funcA(); // funcA调用了 1 次
funcA(); // funcA调用了 2 次
funcB(); // funcB调用了 1 次
funcA(); // funcA调用了 3 次
funcB(); // funcB调用了 2 次
return 0;
}
8.2 四种存储方案对比总表
| 存储方案 | 存储持续性 | 作用域 | 链接性 | 例子 |
|---|---|---|---|---|
| 自动变量 | 自动 | 局部 | 无链接 | int x = 1;(在函数内) |
| 静态+外部链接 | 静态 | 全局 | 外部链接 | int x = 1;(在函数外,不加static) |
| 静态+内部链接 | 静态 | 全局 | 内部链接 | static int x = 1;(在函数外) |
| 静态+无链接 | 静态 | 局部 | 无链接 | static int x = 1;(在函数内) |
9. 说明符和限定符
9.1 存储说明符(Storage Specifiers)
| 说明符 | 作用 | 示例 |
|---|---|---|
auto |
自动推断类型(C++11) | auto x = 5; |
register |
建议放入寄存器(现代编译器通常忽略) | register int x = 5; |
static |
静态存储,程序结束才释放 | static int x = 5; |
extern |
声明外部变量 | extern int x; |
mutable |
允许修改const成员 | 见下文 |
9.2 mutable --- 专门修改const对象
正常情况下,const 对象不能修改。但 mutable 可以打破这个限制:
cpp
class Logger {
mutable int access_count = 0; // 即使是const对象也能修改
string data;
public:
void show() const {
access_count++; // OK!因为access_count是mutable
cout << "数据: " << data << endl;
cout << "被查看了 " << access_count << " 次" << endl;
}
};
int main() {
const Logger log = {"重要信息"};
log.show(); // 查看第1次
log.show(); // 查看第2次 --- access_count变成了2
return 0;
}
9.3 CV限定符(const和volatile)
const --- 值不能修改
cpp
const int MAX = 100;
// MAX = 200; // 错误!const不能修改
const int* p1 = &x; // 指向const int:不能通过p1修改x的值
int* const p2 = &x; // const指针:p2不能指向别的地址
const int* const p3 = &x; // 两者都不能改
volatile --- 告诉编译器"这个值可能被外部改变"
cpp
// 比如一个硬件时钟,它的值在程序运行中随时会变
volatile long clock_time;
// 没有volatile的话,编译器可能优化掉重复读取:
// clock_time = read_clock();
// ... 做一些事 ...
// clock_time = read_clock(); // 编译器可能认为"clock_time没变"直接用缓存
// 加了volatile,编译器每次都重新读取
通俗比喻: const 说"别动我的东西";volatile 说"我的东西随时可能被别人动"。
10. 函数和链接性
10.1 函数默认是外部链接
cpp
// 所有函数默认可以在其他文件中使用(只要声明了)
void func(); // 外部链接(默认)
static void func2(); // 内部链接:只有本文件能调用func2
10.2 不能在一个文件中定义两次同一个函数
cpp
// file1.cpp
void hello() { cout << "hello from file1"; }
// file2.cpp
void hello() { cout << "hello from file2"; } // 错误!重复定义
10.3 C++函数没有"覆盖"机制
在C++中,如果两个文件定义了同名函数(且不是static),链接器会报错。这一点和某些脚本语言不同。
11. 语言链接性(Language Linkage)
11.1 什么是语言链接性?
语言链接性指的是函数名在链接时的"翻译规则"。C++和C编译函数名的方式不同:
| 语言 | 函数 int func(int) 的内部名称 |
|---|---|
| C | _func(可能带有参数编码) |
| C++ | _Z4funci(带参数类型信息,支持重载) |
11.2 为什么需要C语言链接?
当你调用C语言写的库(比如 math.h、Windows API),需要用C的命名规则:
cpp
extern "C" void call_C_function(); // 用C的命名规则链接
extern "C" {
void func1();
void func2(); // 大括号里的函数都用C规则
}
// 也可以在头文件里统一处理
#ifdef __cplusplus
extern "C" {
#endif
void c_library_func(); // 声明
#ifdef __cplusplus
}
#endif
实际场景: 你在C++项目里使用了一个C语言写的数据库库,就必须用 extern "C" 来匹配C的编译规则。
12. 存储方案和动态分配
12.1 三种内存分配方式
| 方式 | 存储位置 | 特点 | 管理方式 |
|---|---|---|---|
| 自动分配 | 栈 | 函数结束自动释放 | 编译器自动管理 |
| 静态分配 | 数据区 | 程序结束才释放 | 编译器自动管理 |
| 动态分配 | 堆 | 手动控制生命周期 | 程序员手动管理 |
12.2 new 和 delete
cpp
#include <iostream>
using namespace std;
int main() {
// 动态分配一个int
int* p = new int;
*p = 42;
cout << *p << endl; // 输出:42
delete p; // 释放内存!
// 动态分配数组
int* arr = new int[5]; // 分配5个int的空间
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
}
for (int i = 0; i < 5; i++) {
cout << arr[i] << " "; // 0 10 20 30 40
}
delete[] arr; // 释放数组!注意是 delete[]
return 0;
}
12.3 常见错误
错误1:内存泄漏(忘了delete)
cpp
void leak() {
int* p = new int(100);
// 忘了delete p!
// 函数返回后,p被销毁,但p指向的内存没人释放
// 这块内存就"丢了",无法再访问也无法再释放
}
错误2:使用已释放的内存(悬空指针)
cpp
int* p = new int(10);
delete p;
// *p = 20; // 错误!p指向的内存已经释放了,不能再用
p = nullptr; // 好习惯:释放后置为nullptr
错误3:重复释放
cpp
int* p = new int(10);
delete p;
// delete p; // 错误!同一块内存不能delete两次
12.4 C++11的智能指针(推荐使用)
手动管理 new/delete 容易出错,C++11提供了智能指针,自动释放内存:
cpp
#include <memory>
using namespace std;
int main() {
// unique_ptr:独占所有权
unique_ptr<int> p1 = make_unique<int>(42);
cout << *p1 << endl; // 42
// 不需要手动delete,函数结束自动释放
// shared_ptr:引用计数,最后一个引用消失时释放
shared_ptr<int> p2 = make_shared<int>(100);
shared_ptr<int> p3 = p2; // 引用计数=2
cout << *p2 << " " << *p3 << endl; // 100 100
// p2和p3都离开作用域后,内存自动释放
return 0;
}
最佳实践: 优先使用
unique_ptr和shared_ptr,尽量避免裸new/delete。
13. 名称空间
13.1 为什么需要名称空间?
问题场景: 假设你用了两个库,都提供了 rand() 函数:
cpp
// 库A有一个rand()函数
// 库B也有一个rand()函数
// 你的代码到底用哪个?→ 命名冲突!
解决方案: 用名称空间把函数"装进不同的房间":
cpp
namespace LibraryA {
void rand() { /* 库A的随机数 */ }
}
namespace LibraryB {
void rand() { /* 库B的随机数 */ }
}
// 使用时明确指定
LibraryA::rand(); // 用库A的
LibraryB::rand(); // 用库B的
比喻: 名称空间就像学校里不同班级的同名学生------都叫"张伟",但一个是一班的张伟,一个是二班的张伟,用"班级名::"就能区分。
13.2 using 声明和 using 编译指令
cpp
namespace MyLib {
void func() { cout << "MyLib::func" << endl; }
void helper() { cout << "MyLib::helper" << endl; }
}
// 方法1:using声明(推荐,精确引入)
using MyLib::func; // 只引入func
func(); // OK,可以直接调用
// helper(); // 错误!helper没被引入
// 方法2:using编译指令(不推荐,可能造成冲突)
using namespace MyLib; // 把MyLib里所有东西都引入
func(); // OK
helper(); // OK
最佳实践: 优先使用
using声明(引入个别名称),避免using namespace全局引入(容易造成命名冲突)。
14. 传统的C++名称空间
14.1 全局名称空间
没有放在任何namespace里的内容,都在全局名称空间中:
cpp
// 这些都在全局名称空间中
int global_var = 10;
void global_func() {
cout << "我在全局名称空间" << endl;
}
// 用 :: 访问全局名称空间
int main() {
int x = 10; // 局部变量x
::global_var = 20; // :: 表示"全局的",避免和局部冲突
return 0;
}
14.2 嵌套名称空间
cpp
namespace Company {
namespace Engineering {
namespace Backend {
void deploy() { cout << "部署后端服务" << endl; }
}
}
}
// 访问
Company::Engineering::Backend::deploy();
// 简化访问
using namespace Company::Engineering::Backend;
deploy(); // 直接调用
15. 新的名称空间特性(C++11)
15.1 名称空间别名
cpp
namespace VeryLongNamespaceNameForSomethingImportant {
void func() {}
}
// 起个别名
namespace VL = VeryLongNamespaceNameForSomethingImportant;
VL::func(); // 简洁多了!
15.2 using声明的改进
C++11中,using声明可以在名称空间中使用:
cpp
namespace A {
int x = 10;
void show() { cout << "A::show" << endl; }
}
namespace B {
using A::x; // 把A的x引入B中
using A::show; // 把A的show引入B中
}
// 现在可以通过B访问A的东西
cout << B::x; // 输出:10
B::show(); // 输出:A::show
15.3 匿名名称空间
cpp
namespace {
int secret = 42; // 只有本文件能看到
void helper() {} // 只有本文件能调用
}
// 等价于传统的 static
// static int secret = 42;
匿名名称空间的优势: 语义更清晰,是C++推荐的替代 static 的方式。
16. 名称空间示例
16.1 完整的多文件名称空间示例
头文件 my_math.h:
cpp
#ifndef MY_MATH_H
#define MY_MATH_H
namespace MyMath {
const double PI = 3.14159265;
double circle_area(double radius);
double rectangle_area(double width, double height);
}
#endif
实现文件 my_math.cpp:
cpp
#include "my_math.h"
namespace MyMath {
double circle_area(double radius) {
return PI * radius * radius;
}
double rectangle_area(double width, double height) {
return width * height;
}
}
主文件 main.cpp:
cpp
#include <iostream>
#include "my_math.h"
using namespace std;
int main() {
using MyMath::circle_area; // 只引入需要的函数
using MyMath::rectangle_area;
cout << "圆的面积: " << circle_area(5.0) << endl;
// 输出:圆的面积: 78.5398
cout << "矩形面积: " << rectangle_area(3.0, 4.0) << endl;
// 输出:矩形面积: 12
// using MyMath::PI; // 也可以只引入常量
cout << "PI = " << MyMath::PI << endl;
return 0;
}
16.2 名称空间嵌套示例
cpp
#include <iostream>
using namespace std;
namespace School {
namespace Grade3 {
struct Student {
string name;
int score;
};
void print_student(const Student& s) {
cout << "三年级 " << s.name << " 成绩: " << s.score << endl;
}
}
namespace Grade5 {
struct Student {
string name;
int score;
};
void print_student(const Student& s) {
cout << "五年级 " << s.name << " 成绩: " << s.score << endl;
}
}
}
int main() {
School::Grade3::Student s1 = {"小明", 95};
School::Grade5::Student s2 = {"大华", 88};
School::Grade3::print_student(s1); // 三年级 小明 成绩: 95
School::Grade5::print_student(s2); // 五年级 大华 成绩: 88
return 0;
}
注意:虽然两个namespace里都有
Student结构体和print_student函数,但因为它们在不同的名称空间中,所以不会冲突。
17. 名称空间及其前途
17.1 名称空间的发展趋势
- C++98: 引入名称空间,解决全局命名冲突
- C++11: 增加别名、匿名名称空间等便利特性
- C++17: 引入嵌套名称空间的简化语法
namespace A::B::C { }
cpp
// C++98写法
namespace A {
namespace B {
namespace C {
void func() {}
}
}
}
// C++17简化写法
namespace A::B::C {
void func() {}
}
17.2 关键结论
- 名称空间是管理大型项目的必备工具
using namespace std;在学习阶段可以使用,但正式项目中应避免 (只用using声明引入需要的具体名称)- 匿名名称空间是内部链接的现代替代方式
总结速查表
| 主题 | 核心要点 |
|---|---|
| 单独编译 | 拆分文件,改哪编译哪,用头文件保护符防止重复包含 |
| 自动存储 | 函数内局部变量,函数结束自动销毁,存在栈上 |
| 静态持续+外部链接 | 全局变量,所有文件可共享,用extern声明 |
| 静态持续+内部链接 | 加static的全局变量,只本文件可用 |
| 静态持续+无链接 | 函数内的static变量,值持久但只本函数可见 |
const / volatile |
const=不许改;volatile=值可能被外部改变 |
| 动态分配 | new/delete,用智能指针更安全 |
| 名称空间 | 避免命名冲突,优先用using声明而非using namespace |
extern "C" |
调用C库时使用,匹配C的命名规则 |