存储持续性、作用域和链接性
C++使用三种(在C++11是四种)不同的方案来存储数据,这些方案的区别就在于数据保留在内存中的时间。
-
自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。他们在程序开始执行其所属的的函数或代码块时被创建,在执行完函数或代码块时,他们使用的内存被释放。C++有两种存储持续性为自动的变量。
-
静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。他们在程序整个运行过程中都存在。C++有三种存储持续性为静态的变量。
-
线性存储持续性(C++11):当前,多核处理器很常见,这些CPU可同时处理多个执行任务。这让程序能够计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属线程一样长。
-
动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束位置。这种内存的存储持续性为动态,有时被称为自由存储或堆(heap)。
链接性(linkage)描述了名称如何在不同单元间共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为他们不能共享。
自动存储持续性
在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。
当程序开始执行这些变量所属的代码块时,将为其的分配内存;当函数结束时,这些变量都将消失(**注意,执行到代码块时,将为变量分配内存,但其作用域的起点为其声明位置)。
如果内部代码块和外部都定义了一个同名的变量,程序执行内部代码块时,将该同名变量解释为局部代码块变量。
自动变量和栈
了解典型的C++编译器如何实现自动变量有助于更深入了解自动变量。由于自动变量的数目随函数的开始而增减,因此程序必须在运行时对自动变量进行管理。
常用的方法是留出一段内存,并将其视为栈,以管理变量的增减。之所以被称栈,是由于新数据被象征性的放在原有数据的上面(也就是说,在相邻的内存单元中,而不是在同一个内存单元),当程序使用完后,将其从栈中删除。栈的默认长度取决于实现,但编译器通常提供改变栈长度的选项。程序使用两个指针来跟踪栈,一个指针指向栈底--栈的开始位置,另一个指针指向栈顶--下一个可用的内存单元。当函数被调用时,其自动变量被加入到栈中,栈顶指针指向变量后面的下一个可用内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。
寄存器变量
关键字register最初是由C语言引入的,他建议编译器使用CPU寄存器来存储自动变量:
register int count_fast;这旨在提高访问变量的速度。
静态持续变量
和C语言一样,C++也为静态存储持续性变量提供了三种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。
这三种链接性都在整个程序执行期间存在,与自动变量相比,他们的寿命更长。
编译器将分配固定的内存块来存储所有的静态变量。这些变量在整个程序执行期间一直存在。另外,如果没有显示地初始化静态变量,编译器将把他设置为0.
在默认情况,静态数组和结构将每个元素或成员的所有位都设置为0.
下面介绍如何创建这三种静态持续变量,然后介绍他们的特点。
要想创建链接性位外部的静态持续变量,必须在代码块的外面声明他;
要想创建链接性位内部的静态持续变量,必须在代码块的外面声明它,并使用static限定符;
要创建没有链接性的静态持续变量,必须在代码块内声明他,并使用static限定符;
所有的静态持续变量都有下述初始化特征:未被初始化的静态变量的所有位都被设置为0.这种变量被称为零初始化的(zero-initialized)。
存储描述 | 持续性 | 作用域 | 链接性 | 如何声明 |
---|---|---|---|---|
自动 | 自动 | 代码块 | 无 | 在代码块中 |
寄存器 | 自动 | 代码块 | 无 | 在代码块中,使用关键字register |
静态,无链接性 | 静态 | 代码块 | 无 | 在代码块中,使用关键字static |
静态,外部链接性 | 静态 | 文件 | 外部 | 不在任何函数内 |
静态,内部链接性 | 静态 | 文件 | 内部 | 不在任何函数内,使用关键字static |
静态持续性、外部链接性
链接性为外部的变量通常简称为外部变量,他们的存储持续性为静态,作用域为整个文件。因此外部变量也成为全局变量
1. 单定义规则
一方面,在每个使用外部变量的文件中,都必须声明他;另一方面,C++有“单定义规则”,该规则指出,变量只能由一次定义。
C++提供了两个变量声明。
一种是定义声明或简称为“定义”,他给变量分配存储空间;
另一种是引用声明或简称为“声明”,他不给变量分配存储空间,因为它引用已有的变量。
引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配存储空间。
double up; extern int blem; extern char gr;如果要在多个文件中使用外部变量,只需在一个文件中包含改变量的定义,但在使用该变量的其他文件中,都必须使用关键字extern声明他。
C++提供了作用域解析符(::)。放在变量名前,该运算符使用变量的全局变量。
静态持续性、内部链接性
在多文件程序中,内部链接性和外部链接性之间的差别很有意义。链接性为内部的变量只能在其所属的文件中使用;但常规外部变量具有外部链接性,即可以在其他文件中hi用。
如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规变量和相同,则在该文件中,静态变量将隐藏常规外部变量。
注意:在多文件程序中,可以在一个文件(且只能再一个文件)中定义一个外部变量。使用该变量的其他文件必须使用关键字extern声明它。
静态存储持续性、无链接性
这种变量是这样创建的,将static限定符用于在代码块中定义的变量。在代码块中使用static时,将导致局部变量的存储持续性为静态。这意味着虽然该变量只在该代码块中可用,但他在该代码块不处于活动状态时仍然存在。因此在两次函数调用期间,静态局部变量的值将保持不变。(静态变量适用于再生——可以用他们将瑞士银行的密码账号传递到下一个要去的地方)。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用函数,将不会像自动变量那样再次被初始化。
说明符和限定符
有些被成为存储说明符*(storage class specifier)或cv-限定符(cv-qualifier)的C++关键字提供了其他有关存储的信息。下面是存储说明符:
-
auto
-
register
-
static
-
extern
-
thread_loacl
-
mutable
在同一个声明中不能使用多个说明符,但thread_loacl除外,它可与static或extern结合使用。
在C++11之前,可以在声明中使用关键字auto指出变量为自动变量;但在C++11中,auto用于自动类型推断。
关键字register用于在声明中指示寄存器存储,而在C++11中,它只是显示地指出变量是自动的、
关键字thread_loacl指出变量的持续性与其所属线程的持续性相同。thread_loacl变量之于线程,犹如常规静态变量之于整个程序。关键字mutable的含义将根据const来解释,因此先来介绍cv-限定符,然后解释他。
-
cv-限定符:
-
const;
-
volatile;
最常见的cv-限定符时const,它表明内存被初始化后,程序便不能对他进行修改。
关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。
例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在两次使用之间不会变化。如果不将变量声明为 volatile,则编译器将进行这种优化;将变量声明为 volatile,相当于告诉编译器,不要进行这种优化。
-
mutable
可以用它来指出,即使结构(或类)变量为const,其某个成员也可以被修改,例如:
struct data { char name[30]; mutable int accesses; ..... }; const data veep={"Claybourne Clodde",0,..}; strcpy(veep.name,"Joye Joux");// not allowed veep.accesses++;// allowed
veep的const限定符禁止程序修改veep的成员,但accesses成员的mutable说明符是的accesses不受这种限制。
存储方案和动态分配
动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。与自动内存不同,动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。
通常,编译器使用三块独立的内存:一块用于静态变量,一块用于自动变量,,另外一块用于动态存储。
例如,假设在一个函数中包含下面的语句:
float *p_fees=new float [20];由new分配的80个字节的内存一直保留在内存中,直到使用delete运算符将其释放。但当包含该声明的语句块执行完毕时,p_fees将消失。如果希望另一个函数能够使用这80个字节的内容,则必须将其地址传递或返回给该函数。另一方面,如果将p_fees的链接性声明为外部的,则文件中位于该声明后面的所有函数都可以使用它。另外,通过在另一个文件中使用下述声明,便可在其中使用该指针。
extern float * p_fees;
使用new运算符初始化
如果腰围内置的标量类型(如int或double)分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起来:
int *pi=new int (6); double *pd = new double (99.9);然而,要初始化常规结构或数组,需要使用大括号的列表初始化
struct where{double x;double y;double z;}; where * one =new where {2.5,5.3,7.2}; int *arr=new int [4] {2,4 6 7};在C++11中,还可将列表初始化用于单值变量:
int *pi = new int {}; double * pd =new double {99.9};
定位new运算符
通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。
new运算符还有另外一种变体,被称为定位new运算符,他让您能够指定要使用的位置。
程序员可能使用这种特性来设置其内存管理流程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。
要使用定位new特性,首先需要包含头文件new,他提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。出需要指定参数外,句法与常规new运算符相同。具体地说,使用定位new运算符时,变量后面可以有方括号,也可以有没有。
#include<new> char buffer[50]; int *p4; p4=new (buffer) int[20];这段代码的意思是从buffer中分配空间给一个包含20个元素的int数组。
buffer分配的空间位于静态内存,如果用delete操作这样的内存,则会出错。
名称空间
在C++中,名称可以是变量、函数、结构、枚举、类以及和结构的成员。当随着项目的增大,名称相互冲突的可能性也将增加。使用多个场上的类库时,可能导致名称冲突。例如,两个库可能都定义了名为List、Tree和Node的类,但定义的方式不兼容。用户可能希望希望使用一个库的List类,而使用另一个库的Tree类。这种冲突被称为名称空间问题。
传统的C++名称空间
术语概念:
声明区域:声明区域时可以在其中声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于函数中声明的变量,其声明区域为其声明所在的代码块。
潜在作用域:变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。
变量对程序而言可见的范围被称为作用域。
新的名称空间特性
C++新增了这样一种功能,即通过定义一种新的声明区域来创建命名的名称空间,这样做的目的之一是提供一个声明名称的区域。一个名称空间中的名称不会与另外一个名称空间发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。
例如:
namespace Jack { double pail; void fetch(); int pal; struct Well{...}; } namespace Jill { double bucket(double n){...} double fetch; int pal; struct Hill(...); }名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非他引用了常量)。
除了用户定义的名称空间外,还存在另一个名称空间——全局名称空间。它对应文件级声明区域,因此前面所说的全局变量现在被描述位于全局名称空间中。任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。因此,Jack中的fetch可以与Jill中的fetch共存,Jill中的Hill可以与外部Hill共存。名称空间中的声明和定义规则同全局声明和定义规则相同。
名称空间是开放的,即可以把名称加入到已有的名称空间中。例如,下面这条语句将名称goose添加到Jill中已有的名称列表中:
namespace Jill { char *goose(const char*); }同样,原来的Jack名称空间为fetch()提供了原型。可以在该文件后面(或另一个文件)再次使用Jack名称空间来提供该函数的代码。
namespace Jack { void fetch() { ... } }当然,需要由一种方法来访问给定的名称空间中的名称。最简单的方法,通过作用域解析符::,使用名称空间来限定该名称:
Jack::pail = 12.34; Jill::Hill mole; Jack::fetch();
using声明和using编译指令
using声明使特定的标识符可用,using编译指令使整个名称空间可用。
using声明由限定的名称和前面的关键字using组成:
using Jill::fetch;
using声明将特定的名称添加到它所属的声明区域中。完成该指令后,便可使用名称fetch代替Jill::fetch。
using声明使一个名称可用,而using编译指令使所有的名称都可用。using编译指令由名称空间和它钱买你的关键字using namespace组成,它使名称空间中的所有名称都可用。
jack::pal=3; jill::pal=10;
变量jack::pal和jill:pal是不同的标识符,表示不同的内存单元。然而,如果使用using声明,情况将发生变化:
using jack::pal; using jill::pal; pal=4;
事实上,编译器不允许您同时使用上述两个using声明。
using编译指令和using声明之比较
**假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间中的名称导入到该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间导入到该声明区域,则局部版本将隐藏名称空间版本。
名称空间的其他特性
可以将名称空间声明进行嵌套:
namespace elements { namespace fire { int flame; ... } float water; }
这里,flame指的是element:🔥:flame。
同样,可以使用下面的using编译指令使内部的名称可用:
using namespace elements::fire;另外,也可以在名称空间中使用using编译指令和using声明,如下所示:
namespace myth { using Jill::fetch; usiing namespace elements; using std::cout; using std::cin; }假设要访问Jill::fetch.由于Jill::fetch现在位于名称空间myth(在这里,他被叫做fetch)中,因此可以这样访问它:
std::cin>>myth::fetch;当然,由于它也位于Jill名称空间中,有哪次仍然可以称作Jill::fetch:
std::cout<<Jill::fetch;如果没有与之冲突的局部变量,则也可以这样做:
using namespace myth; cin>>fetch;现在考虑将using编译指令给用于myth名称空间的情况.using编译指令使可传递的.如果 A op B且B op C,则A op C ,则说明操作op使可传递的.例如,>运算符是可传递的.在这个情况下,下面的语句将导入名称空间myth和elements;
using namespace myth;这条编译指令与下面良好两条编译指令等价:
using namespace myth; using namespace elements;
可以给名称空间创建别名.例如:
namespace my_very_favorite_things{...};则可以使用下面的语句让mvft成为my_very_favorite_things的别名:
namespace mvft=my_very_favorite_things;可以使用这种技术来简化对嵌套名称空间的使用:
namespace MEF=myth::elements::fire; using MEF::flame;
未命名的名称空#过省略名称空间的名称来创建未命名的名称空间:
namespace { int ice; int bandycoot; }这就像后面跟着using编译指令一样,也就是说,在该名称空间中声明的名称的潜在作用域为:从声明点到到声明区域末尾.从这个方面看,他们与全局变量相似.然而,由于这种名称空间没有名称,因此不能显示地使用using编译指令或using声明来使它在其他位置都可用.具体地说,不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间的名称.这提供了链接性为内部的静态变量的替代品.