C++知识整理 内存模型和命名空间
在读《C++ primer》的过程中整理一下知识点,做点笔记。
单独编译:
C++鼓励将组件函数放到独立的文件中,然后可以单独编译这些文件,将他们链接成可执行程序。在大型项目中,如果只改变了一个文件,则可以对这个文件进行单独编译,然后将它与其他文件的编译版本链接即可。
.h文件和.cpp文件中应该存放的内容:
1. 头文件 XXX.h文件:
a. 包含结构声明和使用这些结构的函数原型
b. 类声明
c. 模板声明
d. 内联函数
e. 使用#define 或者 const 定义的符号常量
2. 源代码文件, .cpp文件
a. 存放函数的具体实现
在程序中包含头文件时,需要注意:
1. 是自己定义的头文件,应该使用#include "xxx.h"的形式
2. 如果时c++中标准头文件,应该使用#include <xxx.h>的形式
对于C++编译器而言,如果头文件包含在尖括号中,则它将在存储标准头文件的主机系统的文件系统中查找。如果文件名包含在双引号中,则编译器会首先查找当前的工作目录或者源代码目录,如果没有找到,才会在存储标准头文件的文件系统中查找。
多个库的链接:
由不同编译器创建的二进制模块很可能无法正确的链接,这是应为c++标准允许每个编译器设计人员以他认为合适的方式实现名称修饰。名称的不同将使连接器无法将一个编译器生成的函数调用和另一个编译器生成的函数定义匹配。所以在链接时,要保证所有的对象文件或者库都是由同一个编译器生成的。如果游玩代码,完全可以自己重新编译。
关于名称修饰,可以参考一下这篇文章:《C++中的名称修饰》
名称修饰在C++中存在着广泛的使用。第一个C++编译器将C++代码转换为C代码,然后使用C编译器来编译从而得到目标代码;正因为如此,符号的名称需要符合C语言的标志符规则。即使后来,C++的编译器能够直接将将C++代码转换为机器码或者汇编代码,系统的连接器却通常不支持C++的符号,所以名称修饰仍然需要。
C++编程语言并未规定标准的名称修饰方案,所以每一种编译器按照自己的方法实现。C++由于具有一些复杂的语言特性,比如:类,模板,命名空间,运算符重载等,这会改变了特定符号在上下文或者使用中的含义。这些特性的元信息可以通过修饰名称的符号来消除。因为这种名称修饰系统在不同的编译器之间并没有标准化,几乎没有链接器能够链接不同的编译器产生的目标文件。
下面举一个简单的例子:
有下面的c++程序:
int f (void) { return 1; } int f (int) { return 0; } void g (void) { int i = f(); int j = f(0); }
这些是不同的函数,相互之间除了名称没有其他关联。如果这些函数不做任何改变而直接转换为C代码,会带来一个错误——C语言不允许存在两个同名的函数。所以C++编译器需要将函数的签名信息编码到函数的符号名称中,结果大概如下所示:
int __f_v (void) { return 1; } int __f_i (int) { return 0; } void __g_v (void) { int i = __f_v(); int j = __f_i(0); }
注意函数g() 的名称也被修饰了,即使不存在与函数g() 的名称相冲突的地方:名称修饰会应用到所用的符号上。
(来源:《C++中的名称修饰》)
存储持续性,作用域和链接性:
根据数据在内存中的保留时间的不同,c++中数据存储有三种不同的方案:
1. 自动存储持续性:
在函数定义中声明的变量的存储持续性为自动。在函数执行时被创建,函数执行完毕后销毁。c++有两种存储持续性为自动的变量。即,如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在该代码块内。
1.1 自动变量和栈:
对于典型的c++编译器,对于自动变量的管理按照如下的机制:自动变量的数目随函数的开始和结束而变化,程序对自动变量的管理方法是,留出一段内存,将其视为栈,程序使用两个指针来跟踪栈,指向栈底的指针和指向栈顶的指针。在函数调用过程中,函数中的变量被压入栈中。函数执行结束的时候,栈顶的指针重新指向开始的位置。
寄存器变量:
register关键字最初由C语言引进,他建议编译器使用cpu的寄存器来存储自动变量: register int count; 这是为了提高访问变量的速度。c++11之后,这个关键字的作用只是显示的指出变量是自动的。现在保留该关键字的作用只是为保证以前用了register关键字的代码是合法的。
2. 静态存储持续性:
在函数定义的外面定义的变量和使用关键字static定义的变量的存储持续性都为静态,他们在程序的整个运行过程中都存在。c++为存储持续性为静态的变量提供了三种链接:
a. 外部链接: 可在其他文件中访问
b. 内部链接: 只能在当前文件中访问
c. 无链接:只能在当前函数或者代码块中访问。
这三种连接性在程序执行的整个周期内都存在。对比自动变量,在程序运行的过程中,静态变量的数目是保持不变的,所以不需要特殊的装置(栈)来存储他们。编译器会为它们分配固定的内存来存储静态变量。并且如果没有显式的初始化,编译器将把他们设置为0.(这称为静态初始化)
例如:
int global = 100; // 静态变量,外部链接性 外部文件可以调用 static int onefile = 200; // 静态变量,内部链接性 int main() { } void func() { static int call = 0; // 静态变量 无链接性 }
静态变量的初始化方法:
a. 静态初始化:变量在编译器编译,处理文件的时候被初始化
// 静态初始化 int x; int y = 6; int z = 34*23; // 编译器会进行简单的计算
b. 动态初始化:变量在编译器编译,处理文件后被初始化:
// 动态初始化,变量的初始化必须等到函数被连接且程序执行的时候 const double pi = 4.0 * atan(1.0);
3.线程存储持续性:
如果变量的声明是使用关键字thread_local声明的,则其生命周期与所述的线程一样长。
4. 动态存储持续性:
用new运算符分配的内存将一直存在,直到使用delete将其释放或者程序结束为止。有时候称为自由存储(free storage)或者堆。
--------------------------------------------------------------------------------------------------------
外部链接性:
C++中的但定义规则(ODR: one defination rule), 变量只能有一次定义。对于具有外部链接性的变量,c++提供了两种声明方式:
1. 定义声明:
即定义,为变量分配实际的内存空间
2. 引用声明:
即在外部文件引用变量,使用extern关键字。指标是引用,且不能进行赋值。如果进行赋值,则表示的是定义变量。
举个例子:
// file1.cpp extern int cat = 10; // 定义,因为初始化了 int dog = 20; // file2.cpp extern int cat; // 引用 extern int dog; // 引用 // file3.cpp extern int cat; // 引用 extern int dog; // 引用 void func() { int cat = 30; // 局部变量 cout << cat << endl; // 输出30 // 定义同名的局部变量以后,会将全局隐藏 cout << ::cat << endl; // 通过这种方式调用被隐藏的全局变量 }
内部链接性:
可以使用链接性为内部的静态变量在同一文件中的的多个函数之间共享数据。如果将做作用域为整个个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的同名变量发生冲突。
无链接性:
子函数或者代码块的内部,使用static修饰的变量。这意味着即使该函数或者代码块没有被调用,其中的静态变量依然存在于内存中。因此在两次函数调用之间,静态局部变量的值保持不变。在函数中,静态变量知会初始化一次,即使再次调用函数时,也不会初始化。
函数的链接性:
c++中,所有函数的存储持续性都是静态的。默认情况下,函数的链接性为外部。即可以在不同文件之间进行共享。另外,可以使用static关键字将函数的链接型设置为内部的。必须在函数原型和函数定义中同时使用static关键字。
static int func(int a); // prototype static int func(int a) { ... }
-----------------------------------------------------------------------------------------------------------------------------
cv-限定符:
const: 内存初始化后,程序便不对它进行修改
volatile:即使代码没有对内存单元进行修改,它也可能发生变化。例如多线程中遇到的问题。
举个例子:
假设编译器发现,程序在几条语句中两次使用了某一变量,编译为了优化,可能不会两次让程序去查找这个值,而实将这个值缓存到寄存器中,但是,这种优化的前提条件是,这个变量的值在前后两次的使用中,其值不会发生变化。所以,将变量声明为volatile,相当于告诉编译器不要进行这种优化。
mutable限定符:
即使结构(类)变量为const,其某个成员也可以被修改,例如:
// 定义结构 struct data { char name[30]; mutable int access; }; const data cdata = {"hello", 23}; // const类型的结构变量 const strcpy(cdata.name, "world"); // 不允许 cdata.access = 89; // 允许修改
语言链接性:
连接程序要求每个函数有不同的符号名,在C语言中,一个名称只能对应一个函数,因此这很容易实现。例如,C比那一起可能将函数名func()翻译为func_()这样的形式。这种方法称为C语言的链接性。在C++中一个名称可能对应多个函数,编译器必须将这些函数翻译成不同的符号名。例如,func(int x)可能会翻译为func_i, func(double x, double)可能会翻译为func_d_d。这种方法就是c++语言的链接性。这个就是前面所提到的名称修饰的过程。
名称空间(namespace)
在程序中,可能会使用不同厂商提供的库,不同的库中可能定义了同名的函数,这样可能会导致冲突。所以C++提供了名称空间工具,可以更好的控制名称的作用域。
变量的声明区域:指可以在其中声明的区域,例如在函数外面声明全局变量,则声明区域位为其所在文件,在函数内声明变量,则其声明区域为所在的代码块。
变量的潜在作用域:从声明点开始,到其声明区域的结束。因此潜在作用域比声明区域小。
名称空间特性:
相当于提供一个声明名称的区域,不同的名称空间中,即使相同的名称也不会发生冲突,这就很好的解决了不同厂商提供的库文件如果有同名的函数,该如何调用的问题。例如:
namespace Space1 { int pail; void fetch(); double pi; } namespace Space2 { double fetch; double pail; }
名称空间可以是全局的。也可以位于另一个名称空间中,但不能在代码块中。所以它的链接性是外部的。调用方法如下:
Space2::fetch; // 作用域解析运算符 Space1::fetch();
using声明和 using编译指令:
为了避免每次都需要使用作用域解析运算符对名称空间中的名称进行调用,c++提供了两种机制:
1. using声明:使一个名称可用
using Space1::pail; // using声明
2. using编译指令:是整个名称空间可用
using namespace Space2; // using编译指令
例如:
namespace Space1 { int pail; void fetch(); double pi; } namespace Space2 { double fetch; double pail; } Space2::fetch; Space1::fetch(); int pail; int main() { using Space1::pail; // using声明 using namespace Space2; // using编译指令 using Space1::pail; // 名称空间中的变量 ::pail = 9; // 全局变量 return 0; }
一般推荐使用using声明:只导入指定的名称,如果导入的名称与局部的名称冲突时编译器会进行提示。如果使用using编译指令,则不会提示,如果有冲突,局部名称会覆盖掉导入的名称空间。
名称空间也可以实现嵌套。
也可以给名称空间创建别名:
namespace sp1 = Space1;
通过一个完整的程序说明名称空间
h文件
#pragma once #include "stdafx.h" #include <string> namespace pers // 定义名称空间 { struct Person { std::string lname; std::string fname; }; void getPerson(Person& ); void showPerson(const Person&); } namespace debts { using namespace pers; struct debt { Person name; double amount; }; void getDebt(debt&); void showDebt(const debt&); double sumDebts(const debt arr[], int n); }
.cpp文件:
#include "namesp.h" #include <iostream> #include "stdafx.h" // 定义和声明必须在同一个名称空间中 namespace pers { using std::cout; using std::endl; using std::cin; void getPerson(Person& p) { cout << "Enter first name: "; cin >> p.fname; cout << "Enter last name: "; cin >> p.lname; } void showPerson(const Person& p) { cout << "This is " << p.fname << "." << p.lname << endl; } } namespace debts { void getDebt(debt& d) { getPerson(d.name); cout << "Enter the debt: " << endl; cin >> d.amount; } void showDebt(const debt& d) { showPerson(d.name); cout << "The debt is " << d.amount << endl; } double sumDebts(const debt arr[], int n) { double sum_ = 0.0; for (int i = 0; i < n; i++) { sum_ += arr[i].amount; } return sum_; } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)