网站域名注册服务商,怎么做移动端网站,coding部署wordpress,wordpress复古三栏主题CPP编程-CPP11中的内存管理策略模型与名称空间管理探幽
CPP的四大内存分区模型 在 C 中#xff0c;**内存分区是一种模型#xff0c;用于描述程序运行时内存的逻辑组织方式#xff0c;但在底层操作系统中#xff0c;并不存在严格意义上的内存分区。**操作系统通常将内存分…CPP编程-CPP11中的内存管理策略模型与名称空间管理探幽
CPP的四大内存分区模型 在 C 中**内存分区是一种模型用于描述程序运行时内存的逻辑组织方式但在底层操作系统中并不存在严格意义上的内存分区。**操作系统通常将内存分配给进程并管理这些内存块的分配和释放但不会像内存分区模型那样将内存划分为堆、栈、全局/静态存储区等。这些概念是 C 中用来理解和管理内存的模型有助于开发者编写高效且可靠的代码。 在 C11 之前的版本C 的内存模型通常被描述为包含四个主要区域栈、堆、全局/静态存储区和常量存储区。这些区域用于存储不同类型的数据并在程序执行过程中发挥不同的作用
栈Stack用于存储局部变量和函数调用的上下文信息。每次函数调用时都会在栈上创建一个新的栈帧用于存储函数的参数、局部变量和返回地址等信息。当函数执行完毕时其对应的栈帧会被销毁释放其占用的内存空间。堆Heap用于动态分配内存。程序员可以使用 new 和 delete 运算符来在堆上分配和释放内存。堆上分配的内存生存期由程序员控制直到显式释放为止。全局/静态存储区Global/Static Storage Area用于存储全局变量、静态变量和静态常量。全局变量和静态变量在程序整个执行过程中都存在而静态常量的值在程序生命周期内保持不变。常量存储区Constant Storage Area用于存储常量值如字符串常量。这部分内存通常位于只读内存段防止程序意外修改常量值。
不过需要强调的是虽然操作系统中不存在严格意义上的内存分区模型**但在 C 编写的程序运行过程中会根据内存分区模型来管理内存。**在程序运行时操作系统会为程序分配内存而 C 的内存分区模型描述了这些内存的逻辑组织方式包括堆、栈、全局/静态存储区等 需要注意的是某些文章会提出一个代码区的概念代码区Code Area也称为文本区Text Area或者只读代码区Read-Only Code Area。这个区域用于存储程序的机器指令即编译后的程序代码。代码区通常是只读的因为程序运行时不应该修改其包含的指令。**代码区的内存分配是由操作系统和编译器共同管理的。编译器负责将源代码转换为机器指令并将这些指令存储在代码区。**操作系统则负责将程序加载到内存中并确保代码区是只读的以防止程序意外修改代码。 需要注意的是代码区不是 C 标准中明确定义的术语而是在描述内存模型时用于说明程序代码存储的区域之一。在一些文献或讨论中可能会将代码区归类到全局/静态存储区或者提到它作为一个独立的区域 CPP11新增内存模型特性
CPP11相对于传统内存模型进行了许多有用的内存模型拓展主要集中在以下方面
原子操作Atomic OperationsC11引入了原子操作的支持允许程序员使用std::atomic模板来创建原子变量以实现多线程间的安全访问。原子操作确保对共享数据的操作是不可分割的避免了竞态条件Race Condition的发生。线程内存模型Threading Memory ModelC11定义了一套严格的内存模型它明确了在多线程环境下对共享内存的访问规则。内存模型定义了内存操作的顺序和可见性确保多线程程序的正确性和可移植性。智能指针Smart PointersC11引入了智能指针如std::shared_ptr和std::unique_ptr用于管理动态分配的内存。智能指针可以自动管理内存的生命周期避免了内存泄漏和悬空指针的问题。内存管理工具Memory Management ToolsC11引入了一些新的内存管理工具如std::allocator和std::pointer_traits用于更灵活地管理内存分配和释放。新的内存分配函数New Memory Allocation FunctionsC11引入了一些新的内存分配函数如std::allocator_traits用于支持对齐内存分配和自定义内存分配器。 不过新特性并不是此次重点内容因为篇幅实在过大我们着重讨论传统内存模型的划分 CPP的翻译单元特性
为了提高程序的可维护性和模块功能独立化我们一般会将大型程序拆分成多个模块那么此时会出现一个问题如果一段代码需要在多个文件中进行复用我们总不可能在每段代码中都复制一份吧翻译单元则可以完美地解决这个问题。
在编译单元中通常情况下每份CPP代码将会被认为是一个独立的翻译单元编译器将每个翻译单元分别编译然后进行链接组建最后得到我们的可执行文件
include预处理指令及其作用
对于翻译单元特性有个问题是无法规避的既然进行了分离那么我们怎样才能够在当前翻译单元中获得其他翻译单元中声明的代码段呢?
答案是使用#include预处理指令该指令会将引入的头文件内容在预处理时拷贝一份到当前翻译单元之中然后再进行后续操作藉此实现同一份代码段在不同翻译单元之间的共享。即实现了一个典型的三角形结构
编写头文件xxx.h内部包含函数原型、使用#define或const定义的符号常量、结构声明、类声明、模板声明、内联函数。编写xxx.h的实现文件即头文件声明的代码段的真正实现xxx.cpp编写引入xxx.h的主程序文件
include指令作用
声明函数和类头文件包含了函数和类的声明使得其他文件可以使用这些函数和类而不需要知道其实现细节。引入外部代码头文件可以引入外部库或模块的代码使得当前文件可以使用外部代码提供的功能。共享常量和宏定义头文件可以包含常量和宏定义使得这些常量和宏可以在多个文件中共享。提高代码可读性通过将相关的声明放在一起头文件可以提高代码的可读性和维护性。减少编译时间使用头文件可以减少编译时间因为只有头文件或其对应的编译单元发生了变化才需要重新编译相应的文件。
避免二义性的机制
为什么在一个程序的多个部分引入同一头文件不会引起二义性
实质上头文件虽然与翻译单元关系紧密**但是其本质上并不是一个翻译单元其利用的是CPP中的声明语义即声明与实现相分离**。真正的翻译单元实质上是其对应的xxx.cpp文件因为xxx.cpp文件包含有该头文件声明内容的真正实现链接时链接器操作的实质上也是该cpp文件编译后产生的文件。
为什么在头文件中定义的宏和const常量内联函数也可以加入多个翻译单元不会引起二义性
对于这个问题我们需要明白include指令的拷贝对于每一个翻译单元来讲都是独立的即当前翻译单元中引入的头文件内容对于其他翻译单元是不可见的这里涉及到翻译单元的静态存储持续性稍后我们将进行解释。定义宏就更好理解了毕竟它就是个单纯的文本替换内联函数待后文函数部分细说。
关于include指令的误区
需要强调的是include指令处理头文件时并不会为编译器指明该头文件实现的cpp文件它仅仅是将头文件拷贝一份到当前的编译单元中。你可能会问那编译器怎样找到我们头文件中声明的结构实现呢实质上编译器会尝试寻找代码中每一个声明对应的实现否则将会抛出一个未定义错误这一机制与头文件管理在一定程度上共同保证了编译、链接、组建的正常进行。而要为编译器指明对应编译单元路径一般有两种方式 在编译指令中指明所有翻译单元的路径g file1.cpp file2.cpp -o output 借助构建系统如CmakeMakefile等工具进行组建在对应的构建列表加入翻译单元即可
定义头文件示例
#ifndef UNTITLED_STUDENTSINFOCONTROLLER_H
#define UNTITLED_STUDENTSINFOCONTROLLER_H
#include iostream
#include stringclass StudentsInfoController {private:std::string studentID;std::string studentName;std::string stuTeacherName;std::string stuProfession;
public:StudentsInfoController();std::string getStuInfo();std::string getName();~StudentsInfoController();};#endif //UNTITLED_STUDENTSINFOCONTROLLER_H在定义头文件时需要注意以下几点
在头文件中可以使用include引入其他所需要的库或头文件但是引入的库文件必须是与当前的编译单元相同的编译器编译出的因为不同的编译器实现同样的代码段可能结果是不一样的注意#ifndef ... #define ... #endif宏编程结构其表示检测该头文件是否被定义用于在第一次处理时进行引入UNTITLED_STUDENTSINFOCONTROLLER_H是Clion自动生成的头文件名表示更常见的是头文件名_H_注意不要引入头文件对应的编译单元即xxx.cpp文件避免造成多重定义
使用自定义头文件
#include StudentsInfoController.h这里只需要注意一点
使用尖括号表示包含的是标准库头文件或系统头文件编译器会在系统目录中查找这些头文件。使用双引号表示包含的是用户自定义的头文件编译器会先在当前源文件所在目录中查找如果找不到再去系统目录中查找
内存模型基石-作用域与链接性
作用域的定义与案例
作用域描述的是在翻译单元中该名称在多大的范围内是可见的例如一个函数中定义的变量在另一个函数中是不能使用的但是在整个翻译单元中在所有函数定义之前的变量是可以被所有函数使用的一般情况下某名称的定义处就是其作用域起始点排除一些特殊声明并且局部代码块中的相同名称具有隐藏全局名称的特性
#includeiostreamconst int FLAG_STR 114514;void varCheckoutFuncOne() {int varOfFuncOne 996;std::cout FLAG_STR varOfFuncOne std::endl;
}void varCheackoutFuncTwo() {int varOfFuncTwo 007;std::cout FLAG_STR varOfFuncTwo std::endl;
}int main() {varCheckoutFuncOne();varCheackoutFuncTwo();return 0;
}普通变量作用域只在其定义的代码块中又被称为自动变量即从其定义处开始到代码块结束处例如上述中的int varOfFuncOne 996;作用域为全局的变量在定义位置起始处到文件尾部都可以使用所以又称文件作用域例如上述中的const int FLAG_STR 114514;静态变量作用域取决于其定义的方式稍后细谈在类中声明的成员其作用域为整个类在名称空间中声明的名称作用域为整个名称空间。函数的形参列表中的变量名在函数外部是不可见的函数原型作用域只在包含参数列表的括号中可见函数定义的代码块与函数声明部分的参数列表函数的作用域可以是整个类或整个名称空间甚至于是整个文件作用域不过却不是全局的因为不能在代码块中声明定义函数
链接性的定义与案例
外部链接性案例
在多文件程序中如果一个名称在连接时可以与其他编译单元交互那么该名称就具有外部链接具有外部链接的名称将被引入到目标翻译单元中并且经由连接程序处理同时这个名称必须是唯一的:
// file1.cpp
#includeiostream
extern int global_var;int main(int argc, char const *argv[])
{std::cout global_var ;return 0;
}在file1.cpp中我们使用了extern关键字声明该变量来自于外部的其他编译单元编译器将会自动查找其定义接下来我们直接在file2.cpp中输入以下内容
// file2.cpp
#includeiostream
int global_var 2333;接下来我们以指令进行生成并运行发现可以成功访问
D:\Code\CPP g file1.cpp file2.cpp -o out
D:\Code\CPP .\out.exe
2333内部链接性案例
在程序中一个名称对于它自身所在的翻译单元是可见的进行连接时不可能与其他编译单元中的相同名称相冲突即其他单元不可见则称为具有内部链接性。具有内部连接性的名称不会被链接到目标翻译单元中也就是不能够与其他翻译单元交互如以下示例
// file1.cpp
#includeiostream
extern int inner_var;int main(int argc, char const *argv[])
{std::cout inner_var ;return 0;
}在file1.cpp中我们使用了extern关键字声明该变量来自于外部的其他编译单元编译器将会自动查找其定义接下来我们直接在file2.cpp中输入以下内容
// file2.cpp
#includeiostream
static int inner_var 233;接下来我们以指令进行生成并运行发现抛出错误 undefined reference to inner_var
D:\Code\CPP g file1.cpp file2.cpp
ccWJirC7.o:file2.cpp:(.rdata$.refptr.inner_var[.refptr.inner_var]0x0): undefined reference to inner_var
collect2.exe: error: ld returned 1 exit status
D:\Code\CPP 无链接性则是体现在自动变量上其生命周期和作用域决定了其无链接性 内存模型管理-四大存储策略 注意CPP11标准才拥有这四种存储策略以前只有三种策略 自动存储持续性策略
自动存储持续性指的是在程序执行到包含变量定义的块时自动创建在代码块执行结束时自动销毁的变量的存储策略。这种变量通常称为自动变量也就是我们在函数中定义的变量对应前文提到的普通变量作用域只在其定义的代码块中又被称为自动变量即从其定义处开始到代码块结束处为了更加清晰我这里使用一个类对象来进行演示
#include iostream
using namespace std;class LocalVarTest{public:LocalVarTest() { cout A Test object is created endl; }~LocalVarTest() { cout A Test object is deleted endl; }
};void LocalVarCheckoutFunc() { cout LocalVarCheckoutFunc is running endl; LocalVarTest functionObject;
}int main(int argc, char const *argv[]) {cout endl;LocalVarCheckoutFunc(); cout endl;return 0;
}得到运行结果
LocalVarCheckoutFunc is running
A Test object is created
A Test object is deleted
可以看到函数中的对象存在的生命周期非常的短函数的代码块一结束内存就被回收了在这种情况下作用域为局部且不具备链接性
CPP11中auto的复用
在C语言与CPP之前的版本中auto关键字用于显式声明对应变量为自动变量几乎没有使用场景但是CPP中对其进行了复用和Java的var类似拥有了自动推断的作用使得其拥有了更多场景使用例如对传入的某一个可迭代对象实现遍历时就非常方便
std::vectorint myTestVector(5, 233);for (auto _ : myTestVector)std::cout _ ; 还有个关键字register由C语言引入它显式声明编译器使用寄存器储存该变量它和早期的auto用途实质上是相同的现在许多编译器都会自动优化没删的原因是防止老架构中的代码出现错误 自动变量与栈的关系
由于自动变量的数目随函数的开始和结束而增减因此程序必须在运行时对自动变量进行管理。常用的方法是留出一段内存并将其视为栈以管理变量的增减。之所以被称为栈是由于新数据被象征性地放在原有数据的上面(也就是说在相邻的内存单元中而不是在同一个内存单元中)。当程序使用完后将其从栈中删除。栈的默认长度取决于实现但编译器通常提供改变栈长度的选项。程序使用两个指针来跟踪栈一个指针指向栈底即栈的开始位置另一个指针指向栈顶下一个可用内存单元。当函数被调用时其自动变量将被加入到栈中栈顶指针指向变量后面的下一个可用的内存单元。函数结束时栈顶指针被重置为函数被调用前的值从而释放新变量使用的内存然后继续跟踪新的自动变量。这便是CPP的内存分区栈区的来源我们可以做个实验来进行测试
#includeiostreamint checkAddressFunc(){int funcTestVarOne 233;int funcTestVarTwo 233;std::cout funcTestVarOne std::endl;std::cout funcTestVarTwo std::endl;return 233;
}int main(int argc, char const *argv[])
{int mainTestVar 233;std::cout mainTestVar std::endl;int varOffuncRe checkAddressFunc();std::cout varOffuncRe std::endl;
}得到运行结果
0x45755ffb9c
0x45755ffb5c
0x45755ffb58
0x45755ffb98可以看到返回值在最后储存在了0x45755ffb98即0x45755ffb9c处两者为连续地址符合栈的特征。地址值逐渐减小是因为采取了小端程序表示法
静态存储持续性策略 相较于作用域与生命周期短的多的自动变量静态存储的变量自然更需要明确的链接性管理 在函数定义外定义的变量与使用 static修饰的变量的存储持续性均为静态他们作用时间是整个程序的存活周期并且他们有三种链接性外部链接性内部链接性无链接性并且他们在运行期间是唯一的所以程序无需使用栈来管理它并且在未显式声明它时编译器将会把它默认设置为0。
#include iostreamint globalVar 233; // 链接性为外部
static int theVarOfFiel 344; // 链接性为内部void showNoLinkVar(){static int theVarOfFunc 455; // 无链接性
}静态变量初始化方式 零初始化在未初始化状态下的静态持续性变量系统会默认采取零初始化这种初始化对于标量类型将会进行转化如空指针结构体他们的表示可能是0但是其内部表示却有可能不是0。如指针的NULL与结构体填充位被置为零 常量初始化对于已有的静态持续性变量我们采用常量对其进行初始化如上述代码段中的 int global 233;就是使用的常量初始化同时常量表达式也可用于其初始化constexpr也增加创建常量表达式的方式 动态初始化即使用静态变量去接收某些方法的返回值来初始化 #include iostreamint function();int data function();静态变量与外部链接
拥有静态存储性的变量常被简称为外部变量但是因为其定义所有函数之外在同一工程中只要是使用了extern声明了该外部变量并且没有二义性就可以被声明了的翻译单元正常使用不过值得注意的是它也是可以被自动变量所掩盖的此时需要使用作用域控制符来指定访问全局命名空间中的变量等因此它也得名全局变量
#include iostreamint globalVar 233; // 链接性为外部
static int theVarOfFiel 344; // 链接性为内部void showNoLinkVar(){static int theVarOfFunc 455; // 无链接性
}int main(){int globalVar 2333;std::cout ::globalVar;
}静态变量与内部链接
在同一项目中如果在当前翻译单元中定义一个和声明为外部链接性的全局变量在另一个翻译单元定义一个同名的外部链接性全局变量将会导致二义性。此时我们可以将某个编译单元中的变量声明为内部链接性的全局变量使得其只能够在当前翻译单元中可见。即达成以下效果
使用外部链接性的静态变量让多个不同的翻译单元共享数据使用内部链接性的静态变量让当前翻译单元中的函数共享数据
// file1.cpp
#includeiostream
static int globalVar 233;int main(int argc, char const *argv[])
{std::cout Static-glabalVar: globalVar std::endl;return 0;
}// file2.cpp
#includeiostream
int globalVar 233;组建后运行可执行文件得到如下结果
D:\Code\CPP g file1.cpp file2.cpp -o out
D:\Code\CPP .\out.exe
Static-glabalVar: 233去掉file1.cpp中的static则会出现编译错误
D:\Code\CPP g file1.cpp file2.cpp -o out
ccII6LwP.o:file2.cpp:(.data0x0): multiple definition of globalVar;
ccf8fxCm.o:file1.cpp:(.data0x0): first defined here
collect2.exe: error: ld returned 1 exit status静态变量与无链接性
在代码块中创建的静态变量无链接性在代码块中使用static修饰的变量具有静态持续性它的存活周期贯穿整个程序存活周期在代码块不活跃时仍然存在于内存之中在两次代码块调用期间仍然保持不变并且作用域仅有该代码块作用时间也仅有代码块运行时间并且它仅在程序启动时进行一次初始化不受到循环结构等的干扰经典应用就是传值求和
#include iostreamvoid sum(int x){static int sum 0;sum x;std::cout sum ;
}int main(int argc, char const *argv[]){for(int i0; i5; i)sum(i);return 0;
}
深入const全局常量
前文提到头文件中定义的const类型常量在同一翻译单元中只定义了一次该名称时不会引起二义性原因是因为其作为const全局常量来讲是静态存储持续性的它对于外部翻译单元不可见不过这里要提出的是extern不仅仅是用来声明外部变量的它还可以使得当前编译单元中的const常量对其他编译单元可见
// file1.cpp
#includeiostream
extern int globalVar;int main(int argc, char const *argv[]) {std::cout Static-glabalVar: globalVar std::endl;return 0;
}// file2.cpp
#includeiostream
extern const int globalVar 233;组建运行得到结果发现该const常量已对其他编译单元可见
D:\Code\CPP g file1.cpp file2.cpp -o out
D:\Code\CPP .\out.exe
Static-glabalVar: 233线程存储持续性策略
线程存储持续性指变量在线程的生命周期内保持其值的持续性。在多线程编程中每个线程都有自己的线程栈Thread Stack用于存储局部变量和函数调用信息。线程存储持续性描述了变量在线程栈中的生命周期使用thread_local关键字声明变量使其具有线程存储持续性。这种变量类似于具有静态存储持续性的变量但每个线程都有自己的副本该副本会随着线程的回收而回收所谓的线程存储持续性实质上是指的该副本的生命周期
#include iostream
#include threadthread_local int galobalThreadVar 233;
int normalThreadVar 2333;void checkThreadVarFunc(){std::cout The address of galobalThreadVar in the thread: galobalThreadVar std::endl;std::cout The address of normalThreadVar in the thread: normalThreadVar std::endl;galobalThreadVar 233;normalThreadVar 2333;
}int main(int argc, char const *argv[]){std::cout The address of galobalThreadVar in the main: galobalThreadVar std::endl;std::cout The address of galobalThreadVar in the main: normalThreadVar std::endl;std::thread thread1(checkThreadVarFunc);thread1.join();std::cout The value of galobalThreadVar in the end: galobalThreadVar std::endl The value of normalThreadVar in the end: normalThreadVar std::endl;return 0;
}
我们会得到如下结果
The address of galobalThreadVar in the main: 0x1caedbd1c28
The address of galobalThreadVar in the main: 0x7ff681d39020
The address of galobalThreadVar in the thread: 0x1caedbd41a8
The address of normalThreadVar in the thread: 0x7ff681d39020
The value of galobalThreadVar in the end: 233
The value of normalThreadVar in the end: 4666可以发现线程中的galobalThreadVar与外部定义的galobalThreadVar地址是不一致的而normalThreadVar却是一致的。线程中的galobalThreadVar的地址是0x1caedbd41a8在线程结束后其内存被回收。 注意每个线程拥有自己独立的线程栈即在线程中运行的函数所创建的一般变量仍然是自动变量存在于线程栈上其符合的是自动存储持续性而非线程存储持续性。堆区全局/静态区常量存储区为当前程序中所有线程共享 动态存储持续性策略
CPP中的动态存储性策略涉及到动态内存分配和释放主要通过new和delete操作符来实现。动态存储的特点是对象的生存期不由其所在作用域的开始和结束决定而是由程序员在运行时显式地控制即new与delete的时机所决定也就是我们习惯上称之为堆区的部分
#include iostreamint main(int argc, char const *argv[]) {int *aIntPtr new int(233); // 申请一个int大小的空间 初始化为233int *blockIntPtr new int[3]{2333, 2333, 2333}; // 申请三个int大小的空间 初始化为2333int *currentIntPtr blockIntPtr; // 使用一个新指针来遍历数组while (currentIntPtr ! blockIntPtr 3) {std::cout *currentIntPtr ;currentIntPtr;}delete[] blockIntPtr;delete aIntPtr;return 0;
}需要注意的是在某些操作系统环境中如果等待程序结束系统自动回收内存可能会出现回收不及时的问题建议还是记得手动释放下面我们介绍几个相关内容
std::size_t类型
std::size_t是CPP标准库中定义的一个类型用于表示对象的大小或数组的索引。通常情况下std::size_t被用作无符号整数类型其大小足以容纳任何对象的大小。在使用std::size_t时通常用来表示数组的大小、循环的计数器等。例如在遍历数组时可以使用std::size_t来表示循环计数器
#include iostreamint main() {int arr[] {1, 2, 3, 4, 5};for (std::size_t i 0; i sizeof(arr) / sizeof(arr[0]); i) {std::cout arr[i] ;}return 0;
}这个类型出现在new操作符的实现位置这些函数又叫分配函数存在于全局命名空间中
void * operator new(std::size_t)
void * operator new[](std::size_t)而delete操作符则没有使用它
void operator delete(void *);
void operator delete[] (void *);std::bad_alloc类型
std::bad_alloc是CPP标准库中定义的一个异常类用于表示内存分配失败的情况。当使用new表达式进行内存分配时如果无法分配所请求的内存大小就会抛出std::bad_alloc异常。
#include iostream
#include newint main() {try {int* arr new int[1000000000000]; // 尝试分配一个非常大的数组// 使用arrdelete[] arr;} catch (const std::bad_alloc e) {std::cerr Failed to allocate memory: e.what() std::endl;}return 0;
}nullptr与NULL常量
在CPP中NULL通常被定义为整数常量0或者被定义为nullptr。在较早的C标准中NULL通常被定义为整数常量0用于表示空指针。然而从CPP11开始推荐使用nullptr来表示空指针因为nullptr具有更好的类型安全性和可读性。在使用NULL或nullptr时可以将它们赋值给指针变量用于表示该指针不指向任何有效的对象
int* ptr NULL;
int* ptr nullptr;在CPP中空指针表示指针不指向任何有效的对象因此在解引用空指针或者尝试访问空指针指向的对象时会导致未定义的行为。因此在使用指针之前应该始终检查指针是否为空以避免潜在的错误。这里需要注意的是在平常遇到了内存越界的情况并不表示越界的内存是一个空指针在进行判断时需要注意一般空指针都是编写人员主动声明的
new操作符定位内存
new操作符不仅仅只能够将数据定义到堆空间它还可以修改其内存定位处
#includeiostream
#includenewstruct MemoryBlock{int intVar;char charVar;
};int globalBuffer[3];void getAddress(int *block, int len){for(int i0; ilen; i)std::cout block[i] ;std::cout std::endl;
}int main(int argc, char const *argv[]){int localBuffer[3];getAddress(globalBuffer, 3);getAddress(localBuffer, 3);MemoryBlock *ptrOfGlobal new (globalBuffer) MemoryBlock;MemoryBlock *ptrOfLocal new (localBuffer) MemoryBlock;std::cout ptrOfGlobal ptrOfLocal std::endl;ptrOfGlobal-~MemoryBlock();ptrOfLocal-~MemoryBlock();return 0;
}得到如下运行结果
0x7ff6057e8040 0x7ff6057e8044 0x7ff6057e8048
0xa97e1ffd84 0xa97e1ffd88 0xa97e1ffd8c
0x7ff6057e8040 0xa97e1ffd84需要注意的是这里需要手动调用编译器自动提供的析构函数因为内存不在堆区中 函数的存储持续与链接
在默认情况下函数的存储持续性都是静态的即在整个程序的生命周期中都存在链接性也是外部链接性不过在使用其他文件中的函数时需要对其进行声明
// file1.cpp
#includeiostream
extern void greetingForWorld();int main(int argc, char const *argv[]){greetingForWorld();return 0;
}// file2.cpp
#includeiostreamvoid greetingForWorld(){std::cout Hello World!!! std::endl;
}得到运行结果
D:\Code\CPP g file1.cpp file2.cpp -o out
D:\Code\CPP .\out.exe
Hello World!!!和全局变量一样我们也可以使用static来将其链接性转为内部链接性并且掩盖外部名称我们修改file1.cpp演示
// file1.cpp
#includeiostream
static void greetingForWorld(){std::cout Hello World!!! and CPP std::endl;
}int main(int argc, char const *argv[]){greetingForWorld();return 0;
}加上static组建后运行得到
D:\Code\CPP g file1.cpp file2.cpp -o out
D:\Code\CPP .\out.exe
Hello World!!! and CPP否则你将得到一个错误
ccSUrq13.o:file2.cpp:(.text0x0): multiple definition of greetingForWorld();
ccM6iYYT.o:file1.cpp:(.text0x0): first defined here内联函数的特殊地位
内联函数在 CPP中具有内部链接性与静态存储持续性这意味着每个包含该内联函数定义的编译单元都会有其自己的副本。但是需要注意的是内联函数并不符合CPP的单定义原则内联函数可以在任何需要插入它的地方生成一份自己的定义但是CPP规定同一个内联函数的所有定义必须完全相同。这意味着如果**在多个包含了相同头文件的源文件中都定义了相同的内联函数那么这些函数的定义必须完全一致否则会导致链接错误。**在程序编译时内联函数的定义必须在每个调用点展开所以它的链接性是不能够被extern转换的 单定义原则Single Definition RuleSDR是指在C中每个非内联函数或对象只能在程序中定义一次。如果违反了单定义原则会导致链接错误。单定义原则的目的是确保每个函数或对象在程序中只有一个定义避免重复定义导致的冲突和错误。 特殊链接性之语言链接性
在C/CPP程序中链接器要求每一个名称都必须是唯一的于是编译器会对翻译单元中的内容进行一定的转义操作这就是语言链接性。在实际情况下C与CPP的二进制文件中命名协议大概率是不一致的不排除部分编译器的实现此时如果CPP想要使用C库的内容就必须知道C的命名协议于是CPP有了以下外部原型声明来表明命名协议
显式声明为CPP协议
extern void checkoutFunction();
extern C void checkoutFunction();显式声明为C协议
extern C void checkoutFunction();其他内存模型控制关键字
constexpr关键字
constexpr 是 CPP11 引入的关键字用于声明可以在编译时求值的常量表达式。constexpr 可以用于变量、函数以及构造函数上。
变量 可以使用 constexpr 来声明变量以使其成为编译时常量。如下声明要求 x 在编译时被赋值并且不能修改。
constexpr int x 5;函数 可以使用 constexpr 来声明函数以指示该函数可以在编译时求值。在调用 add 函数时如果参数是编译时常量那么它会在编译时被求值而不是在运行时。
constexpr int add(int a, int b) {return a b;
}构造函数 可以在构造函数上使用 constexpr以使得对象在编译时就被视为常量。如下代码中obj 被声明为 constexpr因此在编译时就被视为常量对象。
class MyClass {
public:constexpr MyClass(int x) : value(x) {}int getValue() const { return value; }
private:int value;
};int main() {constexpr MyClass obj(42);std::cout obj.getValue();return 0;
}constexpr 与普通常量的区别在于它的值必须在编译时就能确定并且能用于编译时计算。这使得 constexpr 常量可以在编译时进行优化和检查而普通常量则只是在运行时保持不变。另一个区别是constexpr 变量可以作为数组的长度、枚举的值等编译时常量的位置使用而普通常量则不能。
举例来说对比以下两种情况
constexpr int x 5;
int arr[x]; // 合法x 在编译时就能确定const int y 5;
int arr[y]; // 错误y 是运行时才能确定的常量volatile关键字
volatile 是 CPP中的早期关键字用于告诉编译器不要对其所修饰的对象进行优化。它通常用于修饰那些可能被意外修改的变量例如硬件寄存器或多线程共享的变量。volatile 告诉编译器不要对被修饰的变量进行任何缓存、寄存器优化或者重排序等操作因为这些操作可能会导致与程序预期不符的行为。但是可能会影响程序的性能。在大多数情况下应该尽量避免使用 volatile 多线程共享的变量当一个变量被多个线程访问并且可能被修改时应该将其声明为 volatile以确保每次访问都是从内存中读取而不是从缓存中读取。 中断服务程序中的变量在中断服务程序中某些变量可能会被中断处理程序修改因此这些变量应该声明为 volatile。 存储器映射的硬件寄存器当一个变量代表一个硬件寄存器的值时应该将其声明为 volatile以确保编译器不会对读取或写入该变量的代码进行优化。
mutable关键字
mutable 在较早的 CPP 标准中就已经存在了。mutable 关键字用于声明类的成员变量可以在 const 成员函数中被修改。也可以用于指定在const结构中的某一属性可以被修改。
const成员函数修改
class MyClass {
private:int value;mutable int mutableValue;public:int getValue() const {return value;}void setValue(int newValue) const {mutableValue newValue; // mutable 成员可以在 const 成员函数中被修改}
};const结构中可修改
#includeiostreamstruct MyStruct{int intVar;mutable char charVar;
};int main(int argc, char const *argv[]){const MyStruct data1 MyStruct{233, a};data1.charVar b;std::cout data1.intVar data1.charVar std::endl;return 0;
}名称空间模型的定义与使用 注意老式头文件 iostream.h并不支持使用名称空间 声明与定义可分离语义
声明Declaration声明告诉编译器某个实体的存在但不为其分配内存或定义其具体实现。在编译器看来声明是一个承诺它告诉编译器某个实体将在程序的其他地方定义或实现。声明可以包括函数声明、变量声明和类声明等。在代码块外声明的是全局名称声明区域为当前文件反之为代码块内的局部名称声明区域为当前代码块
// 函数声明
int add(int a, int b);// 类声明
class MyClass;// 变量声明
extern int globalVar;定义Definition定义为实体分配内存并指定其实现。定义还可以包含声明的信息因此它既是声明也是实现。在CPP中变量和函数需要在使用之前进行定义
// 函数定义
int add(int a, int b) {return a b;
}// 类定义
class MyClass {
public:void myMethod();
};// 变量定义
int globalVar 10;名称空间层次规则定义
在解释名称空间层次前需要明确一个概念即潜在作用域潜在作用域指的是某一名称从声明位置到自身声明区域结束处之间的部分。而某一名称包括名称空间中的名称的实际作用域还可能受到局部名称的遮盖作用即会出现以下情况 名称空间层次实质上就是指的在声明区域和作用域范围内某个名称对于该翻译单元的可见关系保证在某一层次中的某个名称是唯一的。而于此对应的就出现了全局命名空间即当前翻译单元所对应的文件级区域全局变量/常量就定义于全局变量空间之中
自定义显式名称空间
我们只需要使用namespace直接定义一个名称空间即可
namespace SelfDefineSpace{int intVarOfSpace 233;double doubleVarOfSpace 3.14;struct SpaceStruct{int theVarOfSpaceStruct 23333;};void placehoderFnuc(){std::cout abc std::endl;}
}名称空间中常见内容
内容描述变量名称空间可以包含变量的声明和定义。函数名称空间可以包含函数的声明和定义。类和结构体名称空间可以包含类和结构体的声明和定义。嵌套命名空间名称空间可以嵌套在其他命名空间中形成层级结构。命名空间别名可以使用 namespace 别名语法来为名称空间定义别名方便引用。引入其他命名空间可以使用 using namespace 来将其他名称空间内容引入
访问名称空间中的名称
利用作用域解析符访问对应名称即可
直接使用作用域解析符来访问名称空间中的成员不会引入名称空间中的所有成员到当前作用域并且对于当前名称也是临时的。
int main(int argc, char const *argv[])
{std::cout SelfDefineSpace::doubleVarOfSpace std::endl;return 0;
}
使用using namespace编译指令进行访问名称空间
using namespace 编译指令用于引入一个名称空间中的所有成员到当前作用域使得可以直接使用该名称空间中的成员而无需使用作用域解析符这种方式可能造成名称冲突通常不建议使用
int main(int argc, char const *argv[])
{using namespace SelfDefineSpace;std::cout doubleVarOfSpace std::endl;return 0;
}使用using声明访问此方法会将对应名称导入当前编译单元持续存在
使用这种方法需要注意不要引入多个名称空间中的同名名称或者在当前的声明区域中定义相同名称否则将会导致二义性
int main(int argc, char const *argv[])
{using SelfDefineSpace::doubleVarOfSpace;std::cout doubleVarOfSpace std::endl;return 0;
}名称空间嵌套的使用
语法非常简单直接在内部嵌套即可并且相同名称不会冲突使用时多加一层作用域解析符即可或者使用作用域解析符和using指令一起将嵌套名称空间引入翻译单元
namespace SelfDefineSpace{int intVarOfSpace 233;double doubleVarOfSpace 3.14;struct SpaceStruct{int theVarOfSpaceStruct 23333;};namespace SelfInnerSpace{int intVarOfSpace 233;}
}int main(int argc, char const *argv[])
{// using SelfDefineSpace::SelfInnerSpace::intVarOfSpace;// std::cout intVarOfSpace std::endl;std::cout SelfDefineSpace::SelfInnerSpace::intVarOfSpace std::endl;return 0;
}int main(int argc, char const *argv[])
{using namespace SelfDefineSpace::SelfInnerSpace;std::cout intVarOfSpace std::endl;return 0;
}名称空间别名语法
namespace SDS SelfDefineSpace;
namespace SIS SDS::SelfInnerSpace;匿名名称空间语法
由于匿名名称空间无法通过using指令来被其他翻译单元使用所以其只能在当前编译单元中可见类似于定义于全局命名空间中可以用于简化定义大量静态变量的过程
#include iostream
namespace {int intVarOfSpace 233;
}int main(int argc, char const *argv[])
{std::cout intVarOfSpace std::endl;return 0;
}
类与名称空间的链接性
谈完了名称空间的概念与使用终于可以来细说这一最复杂的部分联系类的定义和名称空间的定义你会发现它们的链接性实质上是相当复杂的。
从上文来看名称空间的链接性一般来讲是外部链接性的在名称空间中无论常量还是函数等它们的名称都是外部链接性的但是有一点例外匿名名称空间不是外部链接性的而是内部链接性的因为其对于外部不可见。
而类的链接性则要稍加注意类的名称确实是外部链接性的但是其内部的数据可就不一定了对于其中的公共访问权限部分则是外部链接性的可以通过对象来从外部访问对于私有或者保护成员来讲则是内部链接性的因为只有当前编译单元中的成员方法可以访问。
CPP11中的其他内存模型特性
std::atomi原子操作 原子操作是不可被中断的操作要么完全执行要么完全不执行不存在部分执行的情况。在多线程环境中原子操作可以确保对共享数据的操作是线程安全的即使有多个线程同时访问该数据。 对于多线程访问共享资源时如果不加以调整两个线程可能会因为竞争资源而导致资源调度出现错误例如如下代码
#include iostream
#include threadint counter 0;void addToCounter() {for (int i 0; i 10000; i) {counter;}
}int main() {std::thread t1(addToCounter);std::thread t2(addToCounter);t1.join();t2.join();std::cout Counter value: counter std::endl;return 0;
}
当两个线程同时增加公共资源的值时可能导致竞态条件Race Condition的发生因为两个线程可能同时读取counter的值然后分别递增后再写回这样就会导致最终的结果不确定。在多次运行后可能会出现 Counter value: 18803的非预期结果
使用std::atomic来创建原子类型的变量支持的原子操作包括加载、存储、交换、递增、递减等。使用原子操作可以避免出现竞态条件Race Condition保证数据的一致性和正确性。修改上述程序
#include iostream
#include thread
#include atomicstd::atomicint counter(0);void addToCounter() {for (int i 0; i 10000; i) {counter.fetch_add(1, std::memory_order_relaxed);// std::memory_order_relaxed// 它是最轻量级的内存顺序表示对其他线程的操作顺序没有严格要求// 只要最终结果是正确的即可。}
}int main() {std::thread t1(addToCounter);std::thread t2(addToCounter);t1.join();t2.join();std::cout Counter value: counter std::endl;return 0;
}std::share_ptr智能指针
#include iostream
#include memoryclass MyClass {
public:MyClass(int value) : m_value(value) {std::cout Constructor called with value: m_value std::endl;}~MyClass() {std::cout Destructor called for value: m_value std::endl;}void printValue() {std::cout Value: m_value std::endl;}private:int m_value;
};int main() {std::shared_ptrMyClass ptr1(new MyClass(1));std::shared_ptrMyClass ptr2 std::make_sharedMyClass(2);ptr1-printValue();ptr2-printValue();return 0;
}
make_share的优越性 make_shared 的方式会在一次内存分配中同时分配对象和控制块用于跟踪引用计数等信息因此 make_shared 通常更高效。 make_shared 在一定程度上可以提高代码的安全性因为它可以避免直接使用 new 来创建对象从而避免了可能的内存泄漏和资源管理问题。 make_shared 更具可读性因为它明确地显示了正在创建一个 shared_ptr并且不需要指定删除器因为它会使用默认的删除器。
指定删除器
在使用 std::shared_ptr 创建时可以指定一个删除器deleter用于在智能指针的引用计数归零时释放资源。指定删除器的方法是通过构造函数或 reset 方法中的额外参数来实现
std::shared_ptrMyClass ptr(new MyClass(42), [](MyClass* p) {std::cout Deleting MyClass object with value: p-value std::endl;delete p;});std::unique_ptr智能指针
std::unique_ptr 是一个独占所有权的智能指针它不能被复制只能通过移动move来转移所有权即CPP移动语义实现。因此它不需要引用计数器来追踪多个指针共享一个对象的情况。当 std::unique_ptr 被析构时它所管理的对象会被自动释放
#include iostream
#include memoryclass MyClass {
public:MyClass(int val) : value(val) {}void print() { std::cout Value: value std::endl; }
private:int value;
};int main() {std::unique_ptrMyClass ptr(new MyClass(42));ptr-print();// 编译错误std::unique_ptr 不支持复制构造// std::unique_ptrMyClass ptr2 ptr;// 移动所有权给另一个 std::unique_ptrstd::unique_ptrMyClass ptr2 std::move(ptr);// ptr 此时为空指针if (!ptr) {std::cout ptr is nullptr std::endl;}// ptr2 指向 MyClass 对象ptr2-print();return 0;
}代码中我们先实例化一个 std::unique_ptr 对象 ptr它指向一个 MyClass 对象。然后复制 ptr 给另一个 std::unique_ptr ptr2导致编译错误因为 std::unique_ptr 不支持复制构造。相反我们使用 std::move 将 ptr 的所有权转移给了 ptr2这样 ptr 就变成了空指针。
std::allocator与std::pointer_traits介绍
std::allocator 是 CPP标准库提供的一个用于分配和释放内存的模板类也被称之为分配器。它是标准库容器如 std::vector、std::list 等的默认分配器类型用于在容器内部分配元素的内存空间主要提供以下功能
步骤方法描述分配内存std::allocator::allocate分配一块内存并返回指向该内存起始位置的指针。构造对象std::allocator::construct在分配的内存空间上构造对象。销毁对象std::allocator::destroy销毁对象但不释放内存。释放内存std::allocator::deallocate释放先前分配的内存。
使用 std::allocator 可以使容器更加灵活因为它可以替换为自定义的分配器类型以满足特定需求例如使用内存池来优化内存分配。但在大多数情况下使用默认的 std::allocator 即可满足需求。 分配器allocator是用于管理内存分配和释放的对象。它是标准库容器的一个重要组成部分负责为容器分配和释放内存。分配器可以自定义以满足特定的需求和优化内存管理。 std::pointer_traits 是 C 标准库提供的一个模板类用于提供与指针相关的属性和操作。它定义了一组类型和函数用于在编译时获取指针类型的属性而不需要实际的指针实例。
方法描述std::pointer_traits::element_type获取指针指向的元素类型。std::pointer_traits::pointer获取指针的指针类型。std::pointer_traits::reference获取指针的引用类型。std::pointer_traits::difference_type获取指针的差值类型。std::pointer_traits::rebind将指针类型重新绑定到另一个类型返回新类型的指针类型。
std::allocator_trait介绍
std::allocator_traits 是一个模板类用于提供对分配器allocator的统一访问接口。它提供了一组模板函数用于管理和操作分配器std::allocator_traits 可以用于自定义容器使其能够与不同类型的分配器一起工作而无需直接操作底层分配器。
std::allocator_traits 主要用于处理分配器的底层细节使得分配器更容易与标准库的容器一起使用同时也提高了代码的可读性和可移植性。
属性描述allocator_type获取分配器类型。value_type获取分配器分配的对象类型。pointer获取指向分配器分配的对象类型的指针类型。const_pointer获取指向分配器分配的对象类型的常量指针类型。void_pointer获取指向未指定类型的指针类型。const_void_pointer获取指向未指定类型的常量指针类型。difference_type获取指针的差值类型。size_type获取分配器的大小类型。propagate_on_container_copy_assignment确定分配器是否应该在容器拷贝赋值时传播。propagate_on_container_move_assignment确定分配器是否应该在容器移动赋值时传播。propagate_on_container_swap确定分配器是否应该在容器交换时传播。