美文网首页想法简友广场C++学习笔记
第九章 内存模型和名称空间(2)存储连续性,作用域和链接性

第九章 内存模型和名称空间(2)存储连续性,作用域和链接性

作者: 阿厉a_li | 来源:发表于2019-12-28 14:25 被阅读0次

    (二)存储连续性,作用域和链接性

    1.内存方案,存储连续性

        c++使用四种不同的存储方式来存放数据,区别是数据在内存中保存的时间

    (1)自动存储持续性。函数内或代码块内(包括函数的参数)定义的变量,在函数或代码块运行完毕后就会被释放。

    (2)静态存储持续性函数外或者用关键字static标记的变量,在程序的整个运行过程中都存在。

    (3)线程存储持续性(c++11的特性)。在多核系统中很常见,用thread_local来声明,它的生命周期与所属的线程一样长

    (4)动态存储持续性。new运算符分配的内存将一直存在,直到使用delete运算符将内存释放或者到程序结束的时候为止。new运算符分配的内存在自由存储区(free store,或称为堆heap),而自动变量分配的内存在内。

    2.作用域和链接性

        作用域(scope)描述了名称在文件的多大范围内可见,也就是可见性;而链接性(linkage)描述了名称如何在不同的单元间共享,链接性为外部的名称可以在文件间共享,而链接性为内部的变量只能被一个文件中的函数使用。不同的存储方式是通过存储持续性,作用域和链接性来描述的。

        其中函数外声明的变量(一般在所有函数的前面来定义,也叫作全局变量作用域是全局,从定义位置到文件结尾,也叫作文件作用域自动变量的作用域是局部,而静态变量的作用域是局部还是全局,取决于它是如何被定义的。类中声明的成员变量的作用域是整个类,而名称空间中声明的变量的作用域是整个名称空间全局作用域是名称空间作用域的特例)。

    3.自动存储持续性变量

    (1)默认情况下,在函数中声明的函数参量和变量的存储连续性为自动,作用域为局部,没有链接性。如果在代码块中定义了变量,那么它的存在时间和作用域将被限制在该代码块内,如果代码块内定义了和代码块外相同的变量,那么在代码块内部将只有内部定义的变量可见。main()函数不具有特殊性,main()函数中调用其他函数时,main()开始定义的变量在其他函数中也是不可见的。

    (2)自动变量的初始化。可以使用任何在声明时其值为已知的表达式初始化自动变量。比如int a=2;

    (3)自动变量和栈。栈是后进先出的存储形式,程序用两个指针来追踪栈,一个指向栈顶的后一个位置一个指向栈底。程序使用栈来管理自动变量,自动变量创建时利用栈顶指针来存储,同时栈顶指针指向下一个位置;自动变量被销毁时,内存并没有删除这些值,只是调整了栈顶指针的位置,对这些变量存储的内存单元不再标记了而已。

    (4)寄存器变量。c++中不再使用。register,但是并不非法,只是没有作用。

    4.静态存储持续性变量

    (1)静态存储变量的寿命是整个程序运行期间,不需要销毁,因此不需要使用栈来管理它们,所以静态存储变量会被分配固定的内存块来存储它们。有三种链接性:外部链接性内部链接性无链接性如果没有显式地初始化静态变量,编译器将会把它们初始化为0

    (2)外部连接性就是在函数外(一般是开头位置)声明的变量。它可以被其他文件访问(当然这种访问也是需要一些特殊要求,如使用关键字extern)。内部链接性也是在函数外声明的,但是有关键字static,它可以在本文件中任何一个函数中使用,但是不能被其他文件使用无链接性就是在函数内部或代码块中定义的,并使用static来声明,它只能在本代码块中可见,但是在整个程序生存期间它都存在

    (3)具有外部链接性的变量称为外部变量,它们的存储持续性为静态,作用域为整个程序所有文件,外部变量也称为全局变量。外部变量具有单定义规则,就是说这个变量只能定义一次(如果只是声明,定义的时候不能加extern,如果定义的时候还进行初始化,那么可以加extern也可以不加,加的话可以一目了然知道这个变量需要在其他文件中使用),如果在其他文件中想要使用它的话,需要进行引用声明,引用声明不会给变量分配空间,而定义声明的话会分配空间。引用声明用法:extern int cats;。虽然程序中可能存在同名的多个变量,但是每个变量都只有一个定义。这样的话,自动变量将隐藏同名的全局变量。使用全局变量的时候,在一个文件中定义这个全局变量(进行初始化,分配内存空间),而在其它文件中只要进行引用声明(不分配内存空间)就可以使用这个已经进行定义了的全局变量了。

    (4)作用域解析运算符::

        ::warning(warning是一个全局变量)表示使用warning全局变量,即使我定义了一个同名的局部自动变量。这是作用域解析运算符的一种用法,这说明,全局作用域相当于某种形式的名称空间(因为名称空间和类也是这种用法)。这种用法只能用于全局变量(链接性为外部)或者静态内部链接性变量,不能用于两个局部变量(比如代码块中又包含代码块)。

        在函数中使用关键字extern来表明某个变量使用的是外部变量(也就是全局变量,当然如果不声明的话也可以直接使用,只要在函数外面已经进行了引用声明),如果不加ertern,程序将会创建一个局部变量来隐藏同名的外部变量,但是我们依然可以使用作用域解析运算符来重新找到被隐藏的外部变量。如果不是在函数中,而是在函数外定义一个已经定义过的外部变量会怎样,是否可以隐藏另一个外部变量?这是不允许的,因为它违背了单定义原则,如果想定义另一个同名的全局变量,则可以使用static声明全局变量链接性为内部

        使用全局变量会很容易造成数据的不可靠更改,数据的隔离性差,因此,我们能用局部变量的时候就不要使用全局变量通常全局变量声明为const常量,这样可以避免对全局变量的更改,又可以方便访问某些通用的数据

    (5)具有内部链接性的静态变量。使用关键字static,可以与其他文件声明的外部变量同名,因为他们类型不同。这种静态外部变量隐藏常规外部变量,这时候不可以使用::来找回常规外部变量,因为::作用域解析运算符是用来显示文件作用域的变量,而这种静态外部变量也是文件作用域(只不过链接性是内部)。在多文件程序中,可以在一个文件中定义一个外部变量,在其他需要使用这个外部变量的程序中使用关键字extern来声明这个外部变量即可,如果在其他文件中需要使用一个同名的外部静态变量,可以使用static声明全局变量,这将使它的链接性为内部链接性,不会违反单定义原则。一般我们如果要使一个文件中的变量成为全局变量(多文件),那么我们在声明的时候使用extern,并进行初始化即可(这时候如果另一个文件定义了同名的链接性为内部的静态变量,编译器会发出警告)。

    (6)静态局部变量。这种变量在代码块中使用,加关键字static,表示虽然只能在这个代码块中可见,但是生命周期是整个程序运行期间,因此我两次调用这个代码块(比如函数),这个静态局部变量的值具有连续性(保持不变)。如果初始化了静态局部变量,则只会在程序运行一开始初始化这个局部变量,其他时间即使再调用也不会初始化。

    5.说明符和限定符

    (1)存储说明符

        存储说明符目前(在c++11)起作用的有:static:静态变量,用于局部变量表示局部静态变量,用于全局变量表示链接性为内部;extern:表示使用的是在另一个文件中定义的全局变量的引用;thread_local:表示变量的持续性和所属的线程相同thread_local之于线程,就如同static之于程序,thread_local是唯一可以与extern或static一起使用的说明符;mutable:它的含义需要根据const来解释。另外,auto,不再是说明符,现在表示自动类型推断或函数后置返回类型的占位符;

    (2)cv限定符。

        cv表示const和volatile(变化无常的)。const表示常量,即其被初始化后,就不能对它的值进行修改const还会对默认全局类型产生影响,比如const int a;就表示static const int a;a从默认的全局变量变成了内部链接性。而volatile标记的变量,表示它的值可能不是程序修改的,也可能被硬件修改。volatile表示变量是可能时刻变化的,因此程序不会做一些会导致错误的优化(比如一般两次调用一个变量的值,程序会将第一次调用的缓存拿来使用而不是再去查看变量的存储位置,使用了volatile之后,程序每次都会去查看变量位置的值)。

    (3)存储说明符解释:

        static,extern,thread_local,mutable。mutable表示即使用const声明了某个结构,类等其成员用mutable声明之后,依然可以改变这个成员,即这个成员从const限制中解除

    (4)再谈const变量。

        也就是常量,前面说明了如果在函数外定义const常量默认的链接性为内部,这样每个文件的常量都是私有的。如果我要使用外部const常量,则必须在定义它的文件里用extern来声明并初始化,其他位置使用extern来声明即可。在函数或代码块中声明const的时候,作用域是函数或代码块内部

        const变量的这种性质,也就是const变量可以在头文件中声明的原因,其他的外部变量(全局变量)不要在头文件中声明和初始化,应该在源码文件中加以定义,在使用它的另外的文件中用extern关键字加以声明。

    6.函数和链接性

        跟变量一样,函数也具有链接性默认的函数是外部链接,静态的(也就是函数的生命周期是整个程序存续的时间)(因为函数内部不能定义函数,因此函数只能是静态的)。如果我要改变函数的外部链接性,可以使用static来指明只在本文件中使用(此时函数的声明和定义都需要static关键字)单定义规则也适用于函数,这说明我们只能在一个文件中对函数进行定义,在其他文件中没有定义,但在不同的文件中我们要有函数原型的声明,这通常要通过包含头文件来完成。从这点来看,函数原型的声明,其实跟extern声明外部变量是类似的。

        内联函数不受这些规则的影响,因此内联函数可以包含在头文件中。

    7.语言链接性。

         C语言的链接性非常简单,因为C语言只允许有一个函数名称,而c++函数的多态可以让不同的函数有共同的函数名(只要它们的特征标不同就可以),因此,C语言和c++会对函数进行不同的修饰,来区分不同的函数。也就是不同语言(比如C和c++)对函数的修饰是不同的,因此如果要在c++当中使用c库预编译的函数,我们需要做一些更改,需要对不同语言对函数名的修饰进行说明,可以用函数原型来指出要使用的约定

        比如:extern “C” void spiff(int);和extern “C++” void spaff(int);分别是C语言链接性和C++语言链接性。也就是说要用c编译器编译的函数,需要在原型中加入语言链接性说明extern "C" 函数原型;。

    8.存储方案和动态分配

    (1)动态内存分配

        动态分配内存是用new运算符来进行的,它分配的内存不受存储连续性、作用域和链接性等规则限制,而是由new和delete运算符来控制。因此,原则上可以在一个函数中用new来分配内存,而在另一个函数中delete来释放这个内存,但是我们一般不这样做,这样极有可能因为忘记释放内存而造成内存泄漏。

        虽然存储方案不适用于动态内存,但是用来追踪动态内存指针存储方案的限制,因此要注意不要让追踪内存的指针被销毁从而找不回内存的错误发生。

    (2)使用new运算符初始化

        new运算符的初始化有两种方式,第一种是用括号的形式,主要是内置的标准类型,也可以用于有合适构造函数的类,比如:int *pi=new int(6);

        另一种方法的初始化可以用大括号(要求使用c++11标准),可以使用如下的方式:int *a=new int{12};其他类型类似,比如数组int *a=new int[2]{2,3};。

         !!!!!小括号的初始化主要用于标准类型和类对象;而对于结构和数组,一般要使用大括号括起来的列表初始化,而目前标准类型也可以使用列表初始化,因此,小括号的初始化一般只用于类对象。

        new运算符分配内存失败的时候,会引起std::bad_alloc的异常,从而为系统捕捉。

    (3)定位new运算符

        通常,new运算符在内存heap(堆)中找到一个满足要求的内存块,返回的是一个地址,一般可以用new来给指针赋值,比如int *a=new int[4];或double *a=new double;

        new运算符还有一种功能,就是定位new运算符,也就是指定内存地址的new运算,返回的是特定的内存地址。要使用定位new特性,需要在头文件中包含#include <new>。使用方法是:int *p1=new (buffer)int;或者int *p2=new(buffer) int[20];表示在buffer中分配int内存,buffer是一个地址,指向我们想要存放新数据的地方。

        定位new运算符将在我们传递给它的地址处创建一个内存区块来存放数据,因为这个地址一般是我们已经分配内存的地址,或者是硬件地址,因此定位new运算符不能使用delete来释放内存。再者,定位new运算符返回的地址就是我们传递给它的地址,不同的是对地址上存储内容的解释不相同。最后,可以利用定位new运算符和初始化来将信息放在特定的硬件地址处。

    相关文章

      网友评论

        本文标题:第九章 内存模型和名称空间(2)存储连续性,作用域和链接性

        本文链接:https://www.haomeiwen.com/subject/hzmggctx.html