• C++_基础5-内存模型


    C++为在内存中存储数据提供了多种选择:

    • 可以选择数据保留在内存中的时间长度(存储持续性);
    • 程序的哪一部分可以访问数据(作用域和链接);
    • 可以使用new来动态地分配内存;定位new运算符提供了这种技术的变种;
    • C++名称空间是另一种控制访问权的方式;
    • 通常大型程序都由多个源代码文件组成,这些文件可能共享一些数据,这样的程序涉及到程序文件的单独编译。

    =========================================

    单独编译

           C++鼓励程序员将组件函数放在独立的文件中。可以单独编译这些文件,然后将它们链接成可执行的程序。(通常C++编译器既编译程序,又管理链接器)如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接。这使得大程序的管理更便捷。(大程序有多个源代码文件,头文件是预处理的范畴,函数原型是给编译器看的,它告诉编译器该怎么处理函数)

           例如:UNIX和Linux系统都具有make程序 ,可以跟踪程序依赖的文件以及这些文件的最后修改时间。运行make时,如果它检测到上次编译后修改了源文件,make程序将记住重新构建程序所需的步骤。很多IDE在Project菜单中都提供了类似的工具。

           举个案例:将一个程序放在多个文件中将引出新的问题。多个函数都使用了某种结构声明。谁希望出现更多的问题呢?C和C++开发人员都不希望,因此他们提供了#include 来处理这种情况。与其将结构声明加入到每个文件中。不如统一放到头文件中。然后在每个源文件代码中包含该头文件。这样就可以实现结构声明共享了。另外,也可以将函数原型放到头文件中。因此我们可以将原来的程序分成三个部分:

    l  头文件:包含结构声明和使用这些结构的函数的原型;

    l  源代码文件:包含于结构有关的函数的代码;

    l  源代码文件:包含调用与结构相关的函数的代码;

           这是一个非常有用的组织程序的策略。例如:如果编写另一个程序时,也需要使用这些函数,则只需包含头文件,并将函数文件添加到项目列表或make列表中即可。

           但是注意不能把函数定义放到头文件中,这会引起麻烦。因为这样做的话,可能有多个文件include这个头文件,这就导致多次定义了同一个函数,除非函数是内联的,否则将出现错误。

           通常头文件中常包含的内容:

    l  函数原型

    l  使用#define或const定义的符号常量

    l  结构声明

    l  类声明

    l  模板声明

    l  内联函数

    结构声明放在头文件中很常见。因为结构声明不创建变量,而只是在源代码中创建声明的结构变量时,告诉编译器如何创建该结构变量。同样,模板声明也不被编译,而是告诉编译器如何生成与源代码中的函数调用相匹配的函数定义。

           被声明的const的数据和内联函数由特殊的链接属性,因此可以放到头文件中。

     

           注: “coordin.h” <coordin.h>,前者表示编译器将首先查找当前的工作目录或源代码目录;后者表示编译器将在存储标准头文件的主机系统的文件系统中查找。

     

           编译器:命令行编译器,

     

           不要使用#include来包含源代码文件,这样将导致多重声明。

     

    头文件管理

           在同一个文件中只能将同一个头文件包含一次。为了预防在不知情的情况下将头文件包含多次。可能使用了包含了另外一个头文件的头文件。所以可以使用一种标准的C/C++技术可以避免多次包含同一个头文件。

           #ifndef COORDIN_H_

     

           #endif

     

    多个库的连接

           连接编译模块时,要确保所有对象文件和库都是由同一个编译器生成的。如果有源代码,通常可以用自己的编译器重新编译源代码来消除链接错误。

           C++标准允许每个编译器的设计人员以他认为合适的方式实现名称修饰 。因此不同编译创建的二进制模块可能无法正确地链接。

           翻译单元和文件的关系。

    =========================================

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

    存储类别如何影响信息在文件中的共享。C++使用不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间

           自动存储持续性 :在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。即这种变量其产生和销毁都是自动进行的。即存储持续性为自动的变量。C++中有两种

           静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在整个程序的运行过程中都存在。C++中有三种该变量。

           线程存储持续性:(C++11)多核处理器很常见,这些CPU可同时处理多个执行的任务。这让程序能够将计算机放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其声明周期与所属的线程一样长。

           动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。

    作用域和链接

           作用域(scope),描述了名称在文件(翻译单元)的多大范围内可见。

           函数中定义的变量只能在该函数中使用,函数中可见。

           在文件中函数定义之前定义的变量,可在所有函数中使用。

          

           链接性:描述了名称如何在不同单元间共享。

                  内链接:只能在一个文件中的函数共享;

                  外链接:可在文件间共享;

           自动变量的名称没有链接性,因为他们不能共享。

     

           C++变量的作用域局部的变量是只在定义它的代码块中可用。代码块是由花括号括起来的一系列语句。代码块可以嵌套。全局作用域(文件作用域)作用的范围是从定义变量的位置开始都文件尾。自动变量是局部作用域。静态变量的作用域是全局还是局部,取决于它是如何被定义的。

     

           C++函数的作用域:不能在函数中定义函数。所以函数的作用域是整个类或整个名称空间。如果函数的作用域是局部的,则函数只对自己可见,这样的函数就无法调用。函数生而为调用。

          

           存储方式是通过:持续性(时间维度)、作用域和链接性(空间维度)来描述的。

     

    自动存储持续性

           作用域是局部的,没有链接性。因为自动变量是定义在函数或代码块中。生命期(持续性)是自动的。

           自动变量和栈的关系

           深入理解自动变量

           了解C++编译器如何实现自动变量。由于自动变量的数目岁函数的开始和结束而增减的。所以程序必须对自动变量进行管理。常用的方法是留出一段内存,将其视为栈。栈是先进后出的。最后一个被加到栈中的变量被首先弹出。栈的结构是逻辑上,象征性的。而不是真的有块连续的内存单元做栈。栈的实现原理应该是靠指针链表。栈的长度通常是默认的,编译器也提供改变栈长度的选项。

     

           寄存器变量 register

    静态持续变量

           函数中的静态变量------作用域仅为局部

           内链接的静态变量------加static修饰,作用域仅限于文件

           外链接的静态变量------作用域是多个文件

          

           静态说明了这些变量的生命期是整个程序存在的时期。程序不需要使用特殊的装置来管理这些变量。静态变量的数目在程序运行期间是不变的。

     

           静态变量初始化

           默认0初始化,还可以对静态变量进行常量表达式和动态初始化。

           默认初始化和常量表达式初始化就是静态初始化,就是在编译前初始化好了。

           动态初始化,意味着编译后初始化。

          

           int x;

           int y =5;

           long z = 13*13;

           const double pi =4.0 * atan(1.0);  //动态初始化,必须调用函数atan(),要等到函数被链接且程序执行时。

    静态持续性、外部链接性

    链接性为外部的变量通常称为外部变量,它们的存储持续性为静态。外部变量也相当于全局变量

     

    单定义规则

           声明有两种:引用式声明(extern)、定义式声明;

           只能定义一次;

    全局变量和局部变量

           全局变量的作用域很大,易于访问,但是代价很大,就是程序不可靠。

    静态持续性、内部链接性

           Static限定符修饰作用域为整个文件的变量时,该变量的链接性将为内部。单文件内部

    静态存储持续性、无链接性

           就是在函数内部创建变量,加上static修饰符,这样的变量的周期还是静态的,但是作用域是局部的。很奇葩吧。这种变量的目的是如果函数多次调用时,该静态局部变量将保持不变,可以用来记录一些变化的值。不会像自动变量那样被初始化掉。

    说明符和限定符

           存储说明符

                  Auto(C++11不再是说明符);

                  Register;

                  Static;

                  Extern;

                  Thread_local (C++11新增)

                  Mutable;

           规则:同一个声明不能使用多个说明符;

          

    限定符

           Const

           Volatile:这个关键字表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。该关键字的作用是为了改善编译器的优化能力。编译器会对变量进行优化,它会假定变量在两次使用之间不会变化。如果不用volatile的话,编译器就会进行这样的优化。使用voltile则相当于告诉编译器,不要这样优化,会出问题的。因为这个变量在两次使用之间已经被改了。修改它的是硬件。

          

           关于const有个大坑:

           根据单定义规则,就是一个变量只能被定义一次,假设这个变量在一个源文件中被定义了。其他源文件只能使用extern关键字来链接这个变量。

           还有一种情况就是有一个变量,会被多个源文件使用怎么办,但是不会去修改这个变量。它是在一个文件中定义,然后在其他源文件中extern。这样很麻烦,也不利于代码的移植,和维护。

           所以现在有个办法,就是使用const,在头文件中用const定义这个变量,即const变量。然后其他源文件都include这个头文件。这样会造成对单定义规则的破坏吗?并不会,这是因为const关键字有个隐含属性,就是对全局变量的链接性的影响。就是全局变量具有内链接性。这意味着每个文件都有自己的一组const变量(常量),而不是所有文件共享一组常量。这就很好的解决了单定义与常量共享的矛盾了。编程中经常会遇到很多常量,把其定义到头文件中,就会被多个源文件共享。这些常量都是内链接性的。

           假如一个文件有一个const常量如下:

                  cnst int fingers;

           另一个文件想引用该常量,则可以使用extern关键字,来进行引用式声明:

                  extern const int fingers;

     

    函数和链接性

           函数的持续性默认都是静态的,函数生而为调用,所以默认情况下链接性也是外链接的,即可以在文件中共享。

           所以可以再函数原型中使用extern关键字来表明引用另一个文件中定义的函数。

           当然在一个文件中的函数定义前面加上static就可以表明该函数时内链接性的了。

     

           内联函数不接受单定义规则。所以内联函数可以放到头文件中。

    语言链接性

           链接程序要求不同的函数由不同的符号名。这在C语言中很容易实现。一个名称对应一个函数。

           但是C++有函数重载的概念。同一个名称可能对应不同的函数。所以必须将这些函数翻译成不同的符号名称。因此C++编译器会实行名称矫正或符号修饰。为重载函数生成不同的符号名称。这种方法称为语言的链接性。

    存储方案和动态分配

    动态内存由:运算符new和delete控制,而不是由作用域和链接性规则控制。

    通常:编译器使用三块独立的内存:静态变量、自动变量、动态存储;

     

    动态内存需要被跟踪和访问,手段就是指针变量。

    存储方案不适用与动态内存,但是适用于指针变量。

     

    如果指针销毁了,这块动态内存就失去控制了,无法访问。这就是内存泄漏的来源。要避免这种情况。

    new和free是要成对使用的。

    float * p_fees = new float [20];

     

    1   使用new运算符初始化

     

    1        new失败时

     

    2        new:运算符、函数和替换函数

     

    3        定位new运算符

     

    4        定位new的其他形式

     


    前提:大程序由多个源代码文件组成;

           源代码文件编译后成中间文件(目标文件)

           链接器把这些目标文件链接在一起。

    如果一个源文件修改了,只要重新编译该文件,然后把它与其他中间文件重新链接即可。这样就不需要重新编译其他没有被修改的源文件。这就节省了编译时间。

    make程序,帮助编译管理;

           跟踪程序依赖的文件

           检测编译后源文件的修改

    #include 就是一条预处理命令,预处理就是编译前的处理。预处理就是把这条预处理指令进行替换,合成新的源文件。

    预处理简单讲就是替换;

     

  • 相关阅读:
    spring学习总结009 --- 重复id或name的bean定义允许覆盖allowBeanDefinitionOverriding
    spring学习总结008 --- IOC流程图
    spring学习总结007 --- IOC容器级生命周期接口
    spring学习总结006 --- Bean级生命周期接口
    spring学习总结005 --- IOC容器启动源码(事件机制)
    字体图标
    pycharm永久激活
    Linux常用命令
    Android Studio 更新后导入旧项目Bug解决
    Ubuntu更新源
  • 原文地址:https://www.cnblogs.com/grooovvve/p/10467714.html
Copyright © 2020-2023  润新知