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 就是一条预处理命令,预处理就是编译前的处理。预处理就是把这条预处理指令进行替换,合成新的源文件。
预处理简单讲就是替换;