8.内存模型与命名空间

128 阅读12分钟

8.内存模型与命名空间


一个CPP程序从编码到执行

  1. 预处理,产生.ii文件

    预处理用于将所有的#include头文件及宏定义替换成其真正的内容。预处理后得到的依然是文本文件,但是文件体积会大很多。

    gcc -E -I./inc test.c -o test.i
    # 上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。
    
  2. 编译, 产生汇编文件,.s文件

    将预处理之后的程序转换成特定汇编代码的过程。

    gcc -S -I./inc test.c -o test.s
    
  3. 汇编,产生目标文件,.o或.obj文件

    将上一步的汇编代码转换成机器码,产生的文件叫目标文件,为二进制文件。

    gcc -c test.s -o test.o
    

    这一步会为每一个源文件产生一个目标文件。因此mymath.c也需要产生一个mymath.o文件

  4. 链接,产生可执行文件,.out或.exe文件

    将多个目标文件以及所需要的库文件(.so等)链接成最终的可执行文件

    ld -o test.out test.o inc/mymath.o ...libraries...
    

为什么C++ 不提倡使用宏定义而是使用 constconstexpr

  1. 类型安全性:宏定义是简单的文本替换,在编译器处理之前,它们不会进行任何类型检查。这意味着宏定义可能会导致类型错误,而 const 变量是具有类型的,可以进行类型检查。
  2. 作用域:宏定义是全局的,它们在整个程序中都是可见的。而 const 变量可以在特定的作用域内定义,使得程序更加模块化和可读性更好。
  3. 调试和错误信息:使用宏定义时,编译器生成的错误信息可能会很难理解,因为宏定义会直接展开到代码中。而 const 变量会在编译器进行类型检查时产生更好的错误消息。
  4. 更好的代码维护:宏定义往往不够直观,且很难调试。而使用 const 变量可以更清晰地表达程序员的意图,使得代码更易于维护。
  5. 支持调试器:宏定义生成的代码可能会使得调试器无法准确地跟踪变量的值,而 const 变量则可以正常地被调试器跟踪。

C++内存管理

内存分布

image-20240501222904500

  • Text Segment 代码区 文本区

    • 存放可执行程序的机器码
    • 存放只读数据
  • Data Segment 数据区 静态存储

    • 存放已初始化的全局、静态变量

    • BSS (block started by symbol)静态存储

      • 存放未初始化的全局和静态变量。默认设置为0
    • 存放常量数据

  • Heap 堆区 动态存储

    • 从低地址到高地址增长。用于动态内存分配
    • 由new、malloc实现内存的管理
  • Stack 栈区 自动存储

    • 从高地址向低地址增长。
    • 由编译器自动分配管理。用于局部变量(自动变量),函数参数值,返回变量等

函数栈

函数栈是用来管理函数调用和局部变量的一种数据结构,用于存储函数调用的上下文信息。

当调用函数时,一块内连续内存(栈区的一段内存)压入栈帧,函数返回时,弹出栈帧。

栈帧:

  • 每个函数调用都会在函数栈上创建一个称为栈帧的数据结构,用来存储函数的一些信息。

  • 通常包括

    • 局部变量:函数中声明的变量,在函数被执行时,局部变量(就是在函数内创建的自动变量)就被添加到栈中了。
    • 参数:传递给函数的参数
    • 返回地址:函数执行完需要返回的地址
    • 其他调用相关信息:如函数调用者的栈帧指针等。

栈指针:

  • 栈指针式一个指向函数栈顶部的指针,它指向当前函数栈帧的顶部

全局变量的管理

  • 当全局/静态变量未初始化时,他们被存放在BSS段,在程序执行时,被初始化为0。
  • 当全局/静态变量被初始化,存在数据区。
int x;
int z = 5;
const int q = 6;
void func()
{
     static int y;
}
int main()
{
    return 0;
}

这里的z一直存储在数据区。x、y被存储在BSS,q被存储在常量区。但总的来说全部存在数据区。

单独编译

  1. 将不同功能的程序分开放置,可以集中管理,单独编译。

  2. 一个程序分为三部分:

    • 头文件
    • 头文件的实现方法
    • 主文件
  3. 头文件应该包含哪些文件?

    不是具体实现,仅是告诉编译器如何做的各种量。

    1. 各种声明

      • 函数原型;
      • 类声明;
      • 结构声明;
      • 模板声明
    2. 在项目中多个文件用到的符号常量

      • #define 定义的符号常量
      • const 定义的符号常量
    3. 内敛函数

      内敛函数不是被调用,而是会添加到被调用处。而且经常忽略声明,而直接定义。

  4. #include <> 和" "的区别

    1. <>尖括号经常被用来包裹标准库文件。编译器将在计算机的标准头文件库中查找。
    2. ""引号常用来包裹自己编写的头文件。编译器首先在当前的工作目录或项目目录中查找,如果找不到再去标准位置查找。
  5. 如何使用预编译命令防止编译器将文件预编译两遍?

    #ifndef 全大写项目名_H_
    #define 全大写项目名_H_// 头文件内容#endif//ex:
    #ifndef CPP_LEARNING_H_
    #define CPP_LEARNING_H_#endif
    

存储持续性、作用域和链接性

概念

持续性:存在范围,指他们从被分配内存到被释放的持续过程

作用域:名称的作用范围。描述名称在文件的多大范围内可见。

链接性:描述了名称如何在不同单元间共享链接性的内外之分是指文件内外。为外部的名称可以在文件间共享,为内部的只能由一个文件中的函数共享。

不能在函数内定义函数,函数的作用域应是整个类或者整个命名空间,但不能是局部的。局部的毫无意义。

自动存储持续性

  1. 拥有自动存储持续性的变量称为自动变量

  2. 在函数中声明的函数参数和变量的持续性为自动持续性,作用域为局部。无链接性。

  3. 它们在开始执行其所属代码块时被创建(所以其在代码块开始执行就被分配内存),在代码块执行结束后被销毁(所占内存被释放)。

  4. 编译器使用栈来管理自动变量。

    程序使用两个指针来跟踪栈。一个指向栈底--栈开始的位置;一个指向栈顶--下一个可用单元(注意,这里是超顶)。当函数被调用时,其自动变量被放入栈中,栈顶指针指向下一个可用内存单元。当程序执行结束,栈顶指针被重置为函数被调用前的值(释放内存)。局部变量的值没有被删除,只不过不再被标记,他们所占据的空间将被下一个添加到栈中的函数所调用。

静态存储持续性

静态存储持续性变量:在整个程序的执行期间存在,注意这包括函数内部的静态变量。 在程序被执行时分配内存,程序结束后释放。被放在数据区(已初始化的正常放,未初始化的放BSS且被自动设置为0)。

注意: 静态存储持续性变量不是静态变量,或者不只是。其包括全局变量和静态持续性变量

  1. 静态存储,外部链接性 (全局变量,或称外部变量)

    1. 在代码块的外部声明。

    2. C++提供了两种变量的声明:

      1. 定义声明,他给变量分配存储空间。应该在定义中初始化变量。
      2. 引用声明,他不给变量分配空间,而是引用已有的变量。使用关键字extern 修饰。
    3. 单定义原则: (同持续性的)变量只能有一次定义。但应该在每一个使用外部变量的文件中都声明(引用声明)它。如果对变量进行多次定义,即对变量声明且没有使用extern,或者多个地方进行了初始化(进行初始化,编译器将自动忽略extern关键字,为其分配空间),那么程序将会产生链接(ld)错误。所以单独用一个文件来定义声明外部变量是一个好的选择。

    4. 在函数内部可以再次定义,但此时定义的是同名的自动变量。如果不进行指明,那么此函数中使用的变量将会是这个自动变量。可以使用域解析运算符(::)来访问隐藏的外部变量(放在变量名前面,表示使用全局版本)。

  2. 静态存储,内部链接性 (静态变量)

    在代码块外部声明,并使用static限定符修饰。仅在其所在的文件中使用。

  3. 静态存储,无链接性 (静态变量)

    在代码块内部证明,并使用static限定符修饰。

总结变量的存储方式

存储描述持续性作用域链接性如何声明
自动自动代码块在代码块中
寄存器自动代码块在代码块中,以关键字register
静态,外部链接性静态文件外部函数外
静态, 内部链接性静态文件内部函数外,使用static
静态,无链接性静态代码块代码块内,使用static
...;
int global = 1000;  // 全局变量,静态持续性,外部链接性
static int one_file = 50;  // 静态变量,静态持续性,内部链接性int main()
{
    ...;
}
​
// 与llama不同的是,count在fc1没有被执行时,也留在内存中
void fc1(int n)
{
    static int count = 0;  // 静态变量,静态持续性,无链接性
    int llama = 0;  // 自动变量
    ...;
    
}
​
void fc2(int q)
{
    ...;
    
}
  1. 静态持续性变量的初始化过程:(以下变量均指静态持续性变量)

    1. 首先所有的变量都被零初始化,无论他们有无显式地被初始化。
    2. 如果使用了常量表达式,那么编译器执行常量表达式初始化。
    3. 如果没有足够的信息,如引用了某个函数,那么变量将被动态的初始化。这需要等到用到的信息被链接且程序执行。

注意:如果没有显式初始化,编译器将会把他们设置为0(0,null)。

#include <cmath>int x;  // 0 init
int y = 5;  // 常量表达式
int z = 13 *13;  // 常量表达式
const double pi = 4.0 * atan(1.0);  // 动态初始化
  1. 说明符和限定字

    • 说明符:

      • register:用于在声明中指示寄存器存储,在C++11中,只是用于显式指出变量是自动的

      • thread_local:用于线程

      • mutable:用于指出结构或类即使被声明为const,其某个成员也可以被修改。

        struct data
        {
            char name[30];
            mutable int accesses;
            
        }
        
        const data veep = {"Claybourne Clodde", 0};
        strcpy(veep.name, "Joye");  // 不被允许
        veep.accesses++;  // 被允许
        
    • 限定符:

      • const:常量,在C++中,常量的链接性是内部的。这意味着可以将其放到头文件中,然后包含到每个使用它的地方。
      • volatile:表明即使程序代码没有对内存单元进行修改,其值也可能发生变化。(硬件、共享数据。)将变量声明为volatile,则编译器不会对这个变量进行缓存优化。(如程序两次使用了某个值,编译器可能对程序进行优化,将此值 存入寄存器中,这种优化假设这个变量在这两次使用之间不会发生变化。)
  2. 函数和链接性

    1. 所有函数的存储持续性都自动为静态的。可以使用static将函数的链接性设置为内部的。static vint private(int x);
    2. 单定义规则也适用于函数,如果想要使用函数,就要使用这个函数的每一个文件包含其函数原型。内联函数不受此限制,但c++要求同一个函数的所有内联定义都相同。
    3. C++首先在程序文件中查找函数,如果找不到则找库函数。这意味同名函数覆盖。

动态存储持续性

通过new与delete管理动态内存。

  1. 为new运算符初始化

    答案使在类型名后面加上初始值,并用括号(小括号,大括号,但不能是中括号,因为它是声明数组)括起来。其实就是列表初始化。

    int *pi = new int(6);  // *p1被设置为6;
    int *pi = new int{6};
    
    double *ar = new int [4] {2, 3, 4, 5};
    
    struct where{double x, double y, double z};
    where *one = new where{2.5, 3.6, 9.8};
    
  2. new失败该如何做?

    之前会让new返回空指针,现在将会引发std::bad_alloc异常。

  3. 定位new运算符

    指定要使用的内存。要包含头文件new。

    #include <new>
    
    char buffer1[50];
    char buffer2[500];
    
    // 从 buffer中分配空间
    p2 = new (buffer1) chaff;  // chaff is a struct
    p3 = new (buffer2) int [20];  
    

    是否要delete来释放空间,需要看指定的空间在哪。自动、静态、全局不需要。使用new指定的空间,可以使用常规delete来释放整个内存块。

线程存储持续性

名称空间(命名空间)

  1. 潜在作用域、作用域。

  2. 如何声明一个命名空间

    使用namespace创建命名空间。命名空间可以是全局的,也可以位于另一个命名空间中,但不能位于代码块中。因为默认情况下其名称链接性是外部的。

    // 创建命名空间
    namespace Jack
    {
        double pail;
        void fetch();
        struct Well {};
        
        namespace Jill
        {
            double pal;
        }
    }
    
    // 将名称添加到已有的命名空间中
    namespace Jack
    {
        char * goose{const char *};
    }
    
    // 为在命名空间中声明的函数原型提供实现。
    namespace Jack
    {
        void fetch()
        {
            ...;
        }
    }
    
    // 使用御解析符使用命名空间中的名称
    Jack::pail = 12.254;
    Jack::Well {...};
    
  3. using声明与using编译指令

    using Jack::pail;  // 此后pail被添加到他所属的声明区域
    
    using namespace Jack;  // 使Jack的所有名称都可用。
    

ref