第九章 内存空间和名称空间

9.1 单独编译

将程序分为三部分


  • 头文件:包含结构声明和使用这些结构的函数的原型
  • 源代码文件:包含与结构有关的函数的代码
  • 源代码文件:包含调用与结构相关的函数的代码

头文件中常包含的内容:
- 函数原型
- 使用#define或const定义的符号常量
- 结构声明
- 类声明
- 模板声明
- 内联函数

==在包含头文件时,我们使用“coordin.h,而不是

9.2 存储持续性、作用域和连接性

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

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

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

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

9.2.1 作用域和链接

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

链接性(linkage): 描述了名称如何在不同单元间的共享。链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。自动变量的名称没有链接性,因为它们不能共享。

9.2.2 自动存储持续性

在默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性

特殊情况:如果有两个同名变量Same,一个位于外部代码块中,另一个位于内部代码块中;程序执行内部代码块中的语句时,将Same解释为局部代码块变量。

也就是说,新的定义隐藏了以前的定义,新定义可见,旧定义暂时不可见。在程序离开该代码块时,原来的定义又重新可见。

自动变量和栈

由于自动变量的数目随函数的开始和结束而增减,因此程序必须在运行时对自动变量进行管理。

常用的方法是留出一段内存,并将其视为栈,以管理变量的增减:当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元,函数结束后,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。    
寄存器变量

关键字 register 最初由C语言引入,它建议编译器使用CPU寄存器来存储自动变量。
这旨在提高访问变量的速度。
不过,现在没什么用了,保留它的原因只是为了避免使用了该关键字的现有代码非法。

9.2.3 静态持续变量

5种变量存储方式
存储描述| 持续性|作用域|链接性|如何声明
—|—|—|—|—|
自动 | 自动|代码块|无|在代码块中
寄存器 | 自动|代码块|无|在代码块中,使用关键字register
静态,无链接性|静态|代码块|全局|无|在代码块中,使用static
静态,外部链接性|静态|文件|外部|不在任何函数内
静态,内部链接性|静态|文件|内部|不在任何函数内,使用关键字static

  • 由于静态变量的数目在程序运行期间是不变的,因此程序不需要使用特殊的装置(如:栈)来管理它们。
  • 编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。
  • 另外,如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0.
//for example:
int global=100;             //static duration, external linkage
static int one_file=50;     //static duration, internal linkage
int main(){
...
}
void funct1(int n){
    static int count=.0;    //static duration, no linkage
    int llama=0;
...
}

静态变量的初始化:

除默认的零初始化外,还可对静态变量进行常量表达式初始化和动态初始化。

  • 零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。
  • 动态初始化意味着变量将在编译后初始化。

9.2.4 静态持续性、外部链接性

链接性为外部的变量通常简称为外部变量,它们的存储持续性为静态,作用域为整个文件。外部变脸是在函数外部定义的,因此对所有函数而言都是外部的。(也称全局变量)

单定义规则(One Deriction Rule)

  • 定义声明(defining declaration)或简称为定义(definition),它给变量分配存储空间;
  • 引用声明(reference declaration)或简称为声明(declaration),它不给变量分配存储空间,因为它引用已有的变量。
//引用声明使用关键字external,且不进行初始化;否则,声明为定义,导致分配存储空间;

//如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义(ODR),但在使用该变量的其他所有文件中,都必须使用关键字extern声明它

//file01.cpp
extern int cats=20  //definition because of initialization
int dogs = 22;      //also a definition

//file02.cpp
extern int cats;    //they're reference declaration because the use extern and have 
extern int dags;    //no initialization

作用域解析运算符(::): 放在变量名前面时,该运算符表示使用变量的全局版本。

全局变量和局部变量
- 计算经验表明,程序越能避免对数据进行不必要的访问,就越能保持数据的完整性。通常情况下,应使用局部变量,应在需要知晓时才传递数据,而不应不加区分地使用全局变量来使数据可用。
- 外部存储尤其适于表示常量数据,因为这样可以使用关键字const来防止数据被修改。

9.2.5 静态持续性、内部链接性

  • 将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。
  • 链接性为内部的变量只能在其所属的文件中使用;但常规外部变量都具有外部链接性,即可以在其他文件中使用。

  • 如果文件定义了一个静态外部变量,其名称与另一个文件中声明的常规外部变量相同,则在该文件中,静态变量将隐藏常规外部变量。

  • 在多文件程序中,可以在一个文件(且只能在一个文件中)定义一个外部变量。使用该变量的其他文件必须使用关键字extern声明它。

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

无链接性的局部变量:

将static限定符用于在代码块中定义的变量。在代码块中使用static时,将导致局部变量的存储持续性为静态的。
which means: 虽然该变量只在该代码块中可用,但他在该代码块不处在活动状态时,仍然存在。

9.2.7 说明符和限定符

存储说明符:storage class specifier
cv-限定符: cv-qualifier

存储说明符:
- auto: 自动类型推断
- register: 用于在声明中指示寄存器存储
- static:
1. 用于在作用域为整个文件的声明中时,表示内部链接性
2. 被用于局部声明中,表示局部变量的存储持续性为静态的
- extern: 引用声明,即声明引用在其他地方定义的变量
- thread_local: 指出变量的持续性与所属线程的持续性相同
- mutable: 即使结构(或类)变量为const,其某个成员也可以被修改。

cv-限定符:

  • const: 内存被初始化后,程序便不能再对它进行修改。
  • volatile: 即使程序代码没有对内存单元进行修改,其值也可能发生变化。(作用:改善编译器的优化能力)

再谈const

在默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的(就像使用了static说明符一样)

假设将一组常量放在头文件中,并在同一个程序的多个文件中使用该头文件。那么,预处理器将头文件的内容包含到每个源文件中后,所有的源文件都将包含类似下面这样的定义:

const int fingers = 10;
const char *warning = "Wak!";

注意:如果全局const声明的链接性像常规变量那样是外部的,则根据单定义规则,这将出错。也就是说,只能由一个文件可以包含前面的声明,而其他文件必须使用extern关键字来提供引用声明。另外,只有使用extern关键字的声明才能进行初始化。

//extern wounld be required if const had external linkage
extern const int fingers;   //can't be initialized
extern const char* warning; 

因此需要为某个文件使用一组定义,而其他文件使用另一组声明。然而,由于外部定义的const数据的链接性为内部的,因此可以在所有文件中使用相同的声明。

内部链接性还意味着,每个文件都有自己的一组常量,而不是所有文件共享一组常量。

如果出于某种原因,程序员希望某个常量的链接性是外部的,则可以使用extern关键字来覆盖默认的内部链接性:

extern const int states = 50//definition with external linkage

在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。这与常规外部变量不同,定义常规外部变量时,不必使用extern关键字,但在使用该变量的其他文件中必须使用extern。然而,请记住,鉴于单个const在多个文件中共享,因此只有一个文件可对其进行初始化。

在函数或代码块中声明const时,其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量才是可用的。这意味着在函数或代码块中创建常量时,不必担心其名称与其他地方定义的常量发生冲突。

9.2.8 函数和链接性

C++不允许在一个函数中定义另外一个函数,因此所有函数的存储持续性都自动为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。

  • 可以在函数原型中使用关键字extern来指出函数是在另一文件中定义的,不过这是可选的(要让程序在另一个文件中查找函数,该文件必须作为程序的组成部分被编译,或者是由链接程序搜索的库文件)。
  • 还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和函数定义中使用该关键字

    这意味着该函数只在这个文件中可见,还意味着可以在其他文件中定义同名的函数。和变量一样,在定义静态函数的文件中,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。

    单定义规则也适用于非内联函数,因此对于每个非内联函数,程序只能包含一个定义。对于链接性为外部的函数来说,这意味着在多文件程序中,只能有一个文件(该文件可能是库文件,而不是我提供的)包含该函数的定义,但使用该函数的每个文件都应包含其函数原型。

9.2.9 语言链接性

链接程序要求每个不同的函数都有不同的符号名。
为满足内部需求,

C语言编译器可能将spiff这样的函数名翻译为_spiff。这种方法被称为C语言链接性(C language linkage)。

C++编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。例如:可能将spiff(int)转换为_spiff_i,而将spiff(double,double)转换为_spiff_d_d。这种方法被称为C + +语言链接 (C + + language linkage)

链接程序寻找与C ++ 函数调用匹配的函数时,使用的方法与C语言不同。但如果要在C ++程序中使用C 库中预编译的函数,可以用函数原型来指出要使用的约定

extern "C" void spiff(int); //use C protocol for name look-up
extern void spoff(int);     //use C++ protocol for name look-up
extern "C++" void spaff(int); //use C++ protocol for name look-up

C和C ++ 链接性是C ++标准指定的说明符,但实现可提供其他语言链接性说明符

9.2.10 存储方案和动态分配

使用C ++运算符new(或C函数malloc())分配的内存,被称为动态内存。

动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放

与自动内存不同,动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用于自动变量,另外一块用于动态存储。

存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量。


1. 使用new运算符初始化

如果要为内置的标量类型(如int或double)分配存储空间并初始化,可在类型名后面加上初始值,并将其用括号括起。

int *pi = new int (6);  //*pi set to 6
double *pd = new double (99.99); //*pd set to 99.99

要初始化常规结构或数组,需要使用大括号的列表初始化:

struct where{double x;double y;double z;};
where *one = new where {2.5,5.3,7.2}; //C++11
int *ar = new int[4] {2,4,6,7};      //C++11

//列表初始化也可用于单值变量,


2. new失败时

new可能找不到请求的内存量。在最初的10年中,C++在这种情况下让new返回空指针,但现在将引发异常std::bad_alloc.


3. new:运算符、函数和替换函数

介绍库内部的知识:
运算符new和new[]分别调用如下函数:

void * operate new (std::size_t);   //used by new
void * operate new[] (std::size_t); //used by new[]

这些函数被称为分配函数(alloction function),它们位于全局名称空间中。同样,也有delete和delete[]调用的释放函数(deallocation function);

void * operate new (std::size_t);   //used by new
void * operate new[] (std::size_t); //used by new[]
//std::size_t是一个typedef,对应于合适的整型。

私人订制 ^ _ ^

C++将这些函数称为可替换的(replaceable)。这意味着如果有足够的知识和意愿,可为new和delete提供替换函数,并根据需要对其进行定制。例如,可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求。在代码中,仍将使用new运算符,但它将调用我自己定义的new()函数。


4. 定位new运算符

通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能够指定要使用的位置。程序员可能使用这种特性来设置其内存管理规程、处理需要通过特定地址进行访问的硬件或在特定位置创建对象。

要使用定位new特性,首先需要包含头文件new,它提供了这种版本的new运算符的原型;然后,将new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同。

定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块。这将一些内存管理的负担交给了程序员。

定位new运算符的另一种用法是,将其与初始化结合使用,从而将信息放在特定的硬件地址处。

基本上,它只是返回传递给它的地址,并将其强制转换为void*,以便能够赋给任何指针类型。


5. 定位new的其他形式

就像常规new调用一个接受一个参数的new()函数一样,标准定位new调用一个接受两个参数的new()函数:

int *pi = new int;  //invokes new(siziof(int));
int *p2 = new (buffer) int; //invokes new(sizeof(int),buffer);
int *p3 = new (buffer) int[40]; //invokes new(40*sizeof(int),buffer);

定位new函数不可替换,但可重载。它至少需要接收两个参数,其中第一个总是std::size_t,指定了请求的字节数。这样的重载函数都被称为定义new,即使额外的参数没有指定位置。

9.3 名称空间

9.3.1 传统的C++名称空间

声明区域(declaration region): 声明区域是可以在其中进行声明的区域。

  • 可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。
  • 对于函数中声明的变量,其声明区域为其声明所在的代码块。

潜在作用域(potential scope): 变量的潜在作用域从声明点开始,到其声明区域的结尾。

因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用的。

作用域: 变量对程序而言可见的范围被称为作用域。

C++关于全局变量和局部变量的规则定义了一种名称空间层次。
- 每个声明区域都可以声明名称,这些名称独立于在其他声明区域中声明的名称。
- 在一个函数中声明的局部变量不会与在另一个函数中声明的局部变量发生冲突。

9.3.2 新的名称空间特性

C++新增了一项功能:通过定义一种新的声明区域来创建命名的名称空间。这样做的目的之一是提供一个声明名称的区域。

  • 一个名称空间中的名称不会与另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。

  • 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。因此,在默认情况下,在名称空间中声明的名称的链接性为外部的(除非它引用了常量)。

  • 除了用户定义的名称空间外,还存在另一个名称空间————全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间中。
  • 任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。
  • 名称空间中的声明和定义规则同全局声明和定义规则相同。
  • 名称空间是开放的,即可以把名称加入到已有的名称空间中。
  • 访问给定名称空间中的名称的方法: 通过作用域解析运算符::,使用名称空间来限定该名称。
Jack::pail = 12.34;     //use a variable
Jill::Hill mole;        //create a type of Hill structure
Jack::fetch();          //use a function

未被装饰的名称(如pail)称为未限定的名称(unqualified name);包含名称空间的名称(如Jack::pail)称为限定的名称(qualified name)。


1. using声明和using编译指令

C++提供了两种机制(using声明和using编译指令)来简化对名称空间中名称的使用。

  • using声明使特定的标识符可用
  • using编译指令使整个名称空间可用
//using声明由被限定的名称和它前面的关键字using组成:
using Jill::fetch;      //a using declaration
//using声明将特定的名称添加到它所属的声明区域中。

using编译指令由名称空间名和它前面的关键字using namespace组成,它使名称空间中的所有名称都可用,而不需要使用作用域解析运算符。

using namespace Jack; //make all the name in Jack available 

它们增加了名称冲突的可能性。有点危险


2. using声明和using编译指令之比较

  • 使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。
  • 名称空间为全局的。如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全名变量一样。
  • 使用using声明时,就好像声明了相应的名称一样。如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。


    总结:假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空本。

一般来说,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译指令导入所有名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发生警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称


3. 名称空间的其他特性

可以将名称空间进行嵌套处理

using编译指令是可传递的。如果A op B且B op C, 则A op C,则说明操作op是可传递的。

//可以使用这种技术来简化对嵌套名称空间的使用:
namespace MEF = myth::elements::fire;
using MEF::flame;


4. 未命名的名称空间

可以通过省略名称空间的名称来创建未命名的名称空间:

namespace       //unnamed namespace
{
    int ice;
    int bandycoot;
}

在该名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾。具体地说,不能在未命名名称空间所属文件之外的其他文件中,使用该名称空间中的名称。这提供了链接性为内部的静态变量的替代品。

9.3.4 名称空间及其前途

指导原则:
- 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量
- 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量
- 如果开发了一个函数库或类库,将其放在一个名称空间中。
- 仅将编译指令using作为一种将旧代码转换为使用名称空间的权宜之计
- 不要在头文件中使用using编译指令。首先,这样做掩盖了要让那些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后。
- 导入名称时,首选使用作用域解析运算符或using声明的方法。
- 对于using声明,首选将其作用域设置为局部而不是全局。

9.4 总结

//啊,好累 ~ _ ~

1. C++鼓励程序员在开发程序时使用多个文件。

一种有效的组织策略是:
- 使用头文件来定义用户类型,为操纵用户类型的函数提供函数原型;
- 将函数定义放在一个独立的源代码文件中。头文件和源代码文件一起定义和实现了用户定义的类型及其使用方式。
- 最后,将main()和其他使用这些函数的函数放在第三个文件中。

2. C++的存储方案决定了变量保留在内存中的时间(存储持续性)以及程序的哪一部分可以访问它(作用域和链接性)。

  • 自动变量是在代码块(如函数体或函数体中的代码块)中定义的变量,仅当程序执行到包含定义的代码块时,它们才存在,并且可见。
  • 自动变量可以通过使用存储类型说明符register或根本不使用说明符来声明,没有使用说明符时,变量将默认为自动的。register说明符提示编译器,该变量的使用频率很高,但C++11摒弃了这种用法。

3. 静态变量在整个程序执行期间都存在。

  • 对于在函数外面定义的变量,其所属文件中位于该变量的定义后面的所有函数都可以使用它(文件作用域),并可在程序的其他文件中使用(外部链接性)。另一个文件要使用这种变量,必须使用extern关键字来声明它。
  • 对于文件间共享的变量,应在一个文件中包含其定义声明(无需使用extern,但如果同时进行初始化,也可使用它),并在其他文件中包含引用声明(使用extern且不初始化)。
  • 在函数的外面使用关键字static定义的变量作用域为整个文件,但是不能用于其他文件(内部链接性)。
  • 在代码块中使用关键字static定义的变量被限制在该代码块内(局部作用域,无链接性),但在整个程序执行期间,它都一直存在并且保持原值。
  • 在默认情况下,C++函数的链接性为外部,因此可在文件间共享;但使用关键字static限定的函数的链接性为内部的,被限制在定义它的文件中。

4.动态内存分配和释放是使用new和delete进行的,它使用自由存储区域或堆来存储数据。调用new占用内存,而调用delete释放内存。程序使用指针来跟踪这些内存单元。

5.名称空间允许定义一个可在其中声明标识符的命名区域。这样做的目的是减少名称冲突,尤其当程序非常大,并使用多个厂商的代码时。可以通过使用作用域解析运算符、using声明或using编译指令,来使名称空间中的标识符可用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值