C++ Primer Plus (6th) Chap9 内存模型和名称空间 摘录

C++可以选择数据保留在内存中的时间长度(内存持续性)以及程序的那一部分可以访问数据(作用域与链接)等。

9.1 单独编译

C++允许也鼓励程序员将组件函数放在独立的文件中。

C++编译器即编辑程序,也管理链接器。如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件编译版本链接。

例如unix和linux系统的make程序,可以跟踪程序依赖的文件以及这些文件最后的修改时间。允许mak时,如果它检测到到上次编译后修改了源文件,make将记住重新构建程序所需的步骤。

在分解程序清单7.12,不能简单地以main()之后的虚线为界,将原来的文件分为两个。问题在于,main函数和其他两个函数使用了同一结构声明,因此两个文件都应包含该声明。简单地将它们输入进入无疑是自找麻烦。即使正确地复制了结构声明,如果以后要作修改,则必须记住对这两组声明都进行修改。简而言之,将一个程序放在多个文件中将引出新的问题。

C++提供了#include来处理上述情况,与其将结构声明加入每一个文件中,不如将其放在头文件中,然后在每一个源代码文件中包含该头文件。这样,若要修改结构声明时,只需在头文件修改依次即可。

另外,也可以将函数原型放在头文件中。因此,可以将原来程序分为三部分:

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

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

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

一个文件(头文件)包含了用户定义类型的定义;另一个文件包含操作用户定义类型的函数的代码。这两个文件组成了一个软件包,可用于各种程序。

请不要将函数定义或变量声明放到头文件中。例如,如果在一个头文件包含一个函数定义,然后在其他两个文件(属于同一程序)中包含该头文件,则同一个程序将包含同一个函数的两个定义,除非函数是内联的,否则这将出错。

头文件中常包含的内容:

        函数原型;

        使用#defien或const定义的符号常量;

        结构声明;

        类声明;

        模板声明;

        内联函数;

将结构声明放在头文件是可以的,因为它们不创建变量,而只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。同样,模板声明不是将被编译的代码,它们指示编译器如何生成与源代码中的函数调用相匹配的函数定义。被声明为const的数据和内联函数有特殊的链接属性。

如果文件名包含在尖括号中,则C++编译器将在存储标准头文件的主机系统的文件系统中查找;如果文件包含在双引号中,则编译器将查找当前的工作目录或源代码目录(或其他目录,这取决与编译器)。

注意,只需将源代码加入到项目中,而不用加入头文件。这是因为#include指令管理头文件。另外,不要使用#include来包含源代码文件,这样做将导致多重声明。

在IDE中,不要将头文件加入到项目列表中,也不要在源文件中使用#include来包含其他源代码。

// coordin.h
#ifndef COORDIN_H_
#define COORDIN_H_

void prin(int);


#endif
// coordin.cpp
#include"coordin.h"

void prin(int n)
{
    statements
}
// test.cpp
#include"coordin.h"

int main()
{
    ...
    prin(4);
    ...
}

有一种标准的C++技术可以避免多次包含同一个头文件,防护方案。它是基于预处理器编译指令#ifndef的。意味着仅当以前没有使用预处理器编译指令#include定义名称COORDIN_H_时,才处理#ifndef 和#endif之间的内容。

C++标准允许每个编译器设计人员以他认为合适的方式实现名称修饰,因此有不同编译器创建的二进制模块(对象代码文件)很可能无法正确地链接。在链接编译模块时,请确保所有对象文件或库都是由同一个编译器生成地。

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

        自动存储持续性:

        静态存储持续性:

        线程存储持续性:如果使用关键字thread_local声明的,则其生命周期与所属的线程一样长。

        动态存储持续性:

9.2.1 作用域和链接

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

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

链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享。

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

C++变量的作用域有多种。作用域为局部的变量只在定义它的代码块可见。

作用域为全局(文件作用域)的变量在定义位置到文件结尾之间都可见。

自动变量的作用域为局部,静态变量的作用域全局还是局部取决于是它如何被定义的。

在名称空间中声明的变量的作用域为整个名称空间。

C++函数的作用域可以是整个类或整个名称空间(包括全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域为局部,则只对它自己可见,因此不能被其他函数调用,这样的函数无法运行)。

9.2.2 自动存储持续性

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

注意,执行到函数的代码块时,将为变量分配内存,但其作用域的起点为其声明位置。

如果函数内部声明了一个与函数外声明的同名变量,当程序进入该代码块时,新定义可见,旧定义暂时不可见。在程序离开该代码块处,原来的旧定义又重新可见了。

在C++11之前,关键字auto用于显示地指出变量为自动存储。

由于自动变量地数目随函数地开始到结束而增减,因此程序必须在运行时对自动变量进行管理。常见的方法是留出一段内存,并将其视为栈,以管理变量的增减。当程序使用完变量后,将其从栈中删除。栈的默认长度取决于实现,但编译器通常提供改变栈长度的选项。程序使用两个指针来跟踪栈,一个指针指向栈底--栈开始的位置,另一个指针指向栈顶--下一个可用的内存单元。当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用内存单元。函数结束后,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

栈是后进先出(LIFO),最后加入栈中的变量被首先弹出。

9.2.3 寄存器变量

关键字register建议编译器使用CPU寄存器来存储自动变量。但在C++11中,这种提示失去作用了。

9.2.3 静态持续变量

C++也为静态持续变量提供3中链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件中访问)和无链接性(只能在当前函数或代码块中访问)。这3种链接性都在整个程序执行期间存在,与自动变量相比,它们的寿命更长。编译器将分配固定的内存块来存储所有的静态变量。

如果没有显式地初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个成员或元素地所有位设置为0。

K&R C不允许初始化自动数组和结构,但允许初始化静态数组和结构。

要想创建链接性为外部的静态持续变量,必须在代码块的外面声明它;

要想创建链接性为内部的静态持续变量,必须在代码块的外面声明它,并用static限定符;

要想创建没有链接性的静态持续变量,必须在代码块内声明它,并使用static限定符。

...
int val1  = 100; // static duration, external linkage
static int val2 = 20; // static duration, internal linkage

void func()
{
    static int val3 = 2000; // static duration, no linkage
}
...

所有的静态持续变量都有下述初始化特征:未被初始化的静态的所有位都被设置为0。这种变量被称为零初始化(zero-initialized)。

存储描述持续性作用域链接性如何声明
自动自动代码块在代码块
寄存器自动代码块在代码块,regsiter关键字
静态,无链接性静态代码块在代码块,static关键字
静态,外部链接性静态文件外部不在任何函数内
静态,内部链接性静态文件内部不在任何函数内,static关键字

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

零初始化和常量表达式被统称为静态初始化。

如果没有足够的信息,变量将被动态初始化。

const double x = 4.0 * atan(1); // 动态初始化,因为需要等到atan函数被链接且执行程序时

常量表达式并非只能使用字面常量的算术表达式。例如,它还能使用size'of运算符。

C++11 新增了关键字constexpr,这增加了创建常量表达式的方式。

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

链接性位外部的变量被称为外部变量,它们的存储持续性为静态,作用域为整个文件。也称为全局变量。

C++有”单定义规则(One Definition Rule, ODR)“,该规则指出,变量只能有一次定义。为满足该需求,C++提供了两种变量声明。一种是定义声明(defining declaration)或称定义(definition),它给变量分配存储空间;另一种是引用声明(referencing declaration)或称声明(declaration),它不给变量分配内存,因为它引用已有的变量。

引用声明使用关键字extern,且不进行初始化;否则,声明为定义,导致分配内存空间。

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

请注意,单定义规则并非意味着不能有多个变量的名称相同。然而,虽然程序可能包含多个同名的变量,但每个变量都只有一个定义。

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

全局变量很有吸引力--因为所有的函数能访问全局变量,因此不用传递参数。但代价是--程序不可靠。计算经验表明,程序越能避免对数据的不必要访问,就越能保持数据的完整性。通常情况下,应该需要知晓时才传递数据,而不应不加区分地使用全局变量来使数据可用。

外部存储尤其适于表示常量数据,因为这样可以使用关键字const来防止数据被修改。

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

将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。在多文件程序中,内部链接性和外部链接性的差别很有意义。

// file1
int errors = 20;
...



// file2
int errors = 30;    // ??know to file2 only??, 将与file1产生二义性! 错误!


// file2
static int errors = 30;    // uses errors defined in file2, rathan than in file1.

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

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

可使用外部变量在多文件的不同部分之间共享数据;可使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据(名称空间提供了另外一种共享数据的方法)。另外,如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件的作用域为整个文件的变量发生冲突。

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

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

9.2.7 说明符和限定符

有些被称为存储说明符(storage class specifier)或cv-限定符(cv-qualifier)的C++关键字提供了其他有关存储的信息。下面是存储说明符:

        auto(C++ 11后不在是说明符):指明自动变量;

        register:声明指示寄存器存储;

        static:作用域为整个文件的声明,内部链接性;作用域为局部,静态存储性;

        extern:引用声明在其他地方定义的变量;

        thread_local:指出变量的持续性与其所属线程的持续性相同;

        mutable:根据const限定符来解释

在同一个声明中不能使用多个说明符,但thred_local除外,它可与static与extern结合使用。

cv控制符:

        const:表明内存初始化后,程序便不能在对它进行修改。

        volatile:即使程序代码没有对内存单元进行修改,其值也可能发生变化。可能是硬件或两个互相影响,共享数据。该关键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会发生变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

        mutable指出,即使结构(类)变量为const,其某个成员也可以被修改。

struct data
{
    char name[10];
    mutable int accesses;
}

const data veep = {"weqs", 23}
veep.accesses = 12;    // OK!
strcpy(veep.name, "rseww");    //Wrong!

在C++中,const限定符对默认存储类型稍有影响。在默认情况下全局变量的链接性为外部的,但const全局链接性为内部的。

const int fingers = 10;    // same as static const int fingers = 10;

只能由一个文件可以包含前面的声明,而其他文件必须使用extern关键字来提供引用声明。另外,只有未使用extern关键字的声明才能进行初始化:

extern const int fingers;    // can't be initialized

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

每个定义都是其所属文件私有的,着就是能够将常量定义放在头文件中的原因。

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

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

在这种情况下,必须在所有使用该常量的文件中使用extern关键字来声明它。鉴于单个const在多个文件中共享,因此只有一个文件可对其进行初始化。

在函数或代码块中声明const时,其作用域为代码块,即仅当程序执行该代码块中的代码时,该常量才是可用的。

9.2.8 函数的链接性

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

实际上,可以用关键字extern指出函数是在另一个文件定义的,不过这是可选的。还可以使用关键字static将函数的链接性设置为内部的,使之只能在一个文件中使用。必须在原型和函数定义中使用该关键字。

static int private(double x);
...

static int private(double x)
{
    ...

}

和变量一样,在定义静态函数时,静态函数将覆盖外部定义,因此即使在外部定义了同名的函数,该文件仍将使用静态函数。

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

内联函数不受这项规则的约束,这允许程序员能够将内联函数的定义放在头文件中。这样,包含了头文件的每个文件都有内联函数的定义。然而,C++要求同一个函数的所有内联定义都必须相同。

C++在哪里查找函数?如果该文件中的函数原型指出该函数是静态的。则编译器只在该文件查找函数定义;否则,编译器(包括链接程序)将在所有的程序文件中查找。如果找到两个定义,编译器发出错误消息,因为每个外部函数只能有一个定义。如果在程序文件中没有找到,编译器将在库中搜索。这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本,而不是库函数。

9.2.9 语言的链接性

链接程序要求每个不同的函数都有不同的符号名。C++编译器执行名称矫正或名称修饰,为重载函数生成不同的符号名称。例如将spiff(int)转换为_spoff_i,而将spiff(double, double)转换为_spiff_d_d。这种方法被称为C++语言链接。

可以用函数原型来指出要使用的约定:

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

前一个原型通过默认方式指出使用C++语言链接性,后一个原型通过显式地指出这一点。

9.2.10 存储方案和动态分配

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

与自动内存不一样,动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。

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

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

在不那么健壮的操作系统中,用new请求大型内存块将导致该代码块在程序结束不会被自动释放,所以最好还是用delete来释放。

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

int* pi = new int (5);

然而要初始化常规结构和数组,需要适用大括号的列表初始化。这要求编译器支持C++11.

struct where
{
    double x;
    double y;
    double z;
};

where* one = new where {2.5, 3.2, 4.1};

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

new运算符调用以下函数;

void operator new(std::size_t);

该函数被称为分配函数(allocation function),它们位于全局名称空间中。也有对应的释放函数(deallocation function):

void operator delete(void*);

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

使用定位new运算符时,变量后面可以由方括号,也可以没有。

char buffer1[20];
int* p1 = new (buffer1) int[20];

定位new运算符的另一种用法是,将其与初始化结合使用,从而将信息放在特定的硬件地址处,并将其强制转化为void*,以便能够赋给任何指针类型。

9.3 名称空间

在C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。使用多个厂商的类库时,可能导致名称冲突。

9.3.1 传统的C++名称空间

声明区域(declaration region)是指可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数中声明的变量,其声明区域为其所在的代码块。

潜在作用域(potential scope)是指作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小。这是由于变量必须定义后才能使用。

然而,变量并非在其潜在作用域的任何位置都是可见的。例如,它可能被另一个在嵌套声明区域中声明的同名变量隐藏。

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

C++关于全局变量和局部变量规则定义了一种名称空间层次。

9.3.2 新的名称空间特性

C++通过定义一种新的声明区域来创建命名的名称空间,目的之一是提供一个声明名称的区域。一个名称空间中的名称不会域另外一个名称空间的相同名称发生冲突,同时允许程序的其他部分使用该名称空间中声明的东西。

namespace Jack
{
    double pail;        // variable declaration
    void fetch();       // function prototype
    int pal;            // variable declaration
    struct Well {...};  // struction declaration
}

// namespace 给出函数原型,名称空间外给出函数定义。
void Jack::fetch()
{
    ...
}

名称空间可以是全局的,也可以是位于另一个名称空间中,但不能位于代码块。

除了用户定义的名称空间外,还存在另一个名称空间--全局名称空间(global namespace)。它对应于文件级声明区域,因此前面所说的全局变量现在被描述为位于全局名称空间。

任何名称空间中的名称都不会与其他名称空间的名称发生冲突。

名称空间中的声明和定义规则同全局声明和定义规则相同。

名称空间是开放的(open),即可以把名称加入到已有的名称空间中。

例如:

namespace Jack
{
    char* goose(const char*);
}

当然,需要有一种办法来访问给定名称空间中的名称。最简单方法是作用域解析运算符::。

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

C++提供using声明和using编译指令来简化对名称空间中名称的使用。using声明使特定的标识符可用,using编译指令使整个名称可用。

using声明由被限定的名称和它前面的关键字using组成:

using Jack::pail;

using编译指令由名称空间名和它前面的关键字using namespace组成,它使名称空间中所有名称可用。

using namespace Jack;

事实上,编译器不允许您同时使用上述两个using声明,因为这将导致二义性。

using Jack::pail;
using Jill::pail;    // wrong!

如果使用using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样。

局部声明的Jill::fetch将会隐藏全局声明fetch。

局部声明的fetch将隐藏Jill::fetch和全局fetch。然而,如果使用作用域解析符,则均可用。

虽然函数中的using编译指令将名称空间的名称视为函数之外声明的,但它不会使得该文件中的其他函数能够使用这些名称。

最好只在使用名称空间中具体名称时前面加上作用域解析符,而不是使用using编译指令。如果经常使用该名称,则使用using声明该名称。

可用将名称空间嵌套。

namespace Jack
{
    ...
    namespace Jill
    {
        ...    
    }
    
}

也可以在名称空间内使用using声明或编译指令。

namespace
{
    ...
}

在未命名的名称空间中声明的名称的潜在作用域为:从声明点到该声明区域末尾。

9.3.3 名称空间的示例

// namespace.h
#inclde <string>

namespace pers
{
    struct Person
    {
        std::string fname;
    };

    void getPerson(Person&);
}


// namespace.cpp
#include "namespace.h"
#include <iostream>
namespace pers
{
    void getPerson(Person& rp);
    {
        std::cout << rp.fname << std::endl;
    }
}


// namespacesp.cpp
#include <iostream>
#include "namespace.h"

int main()
{
    using pers::getPerson;
    using pers::pers;
    pers Per1 = {Dilly};
    getPerson(Per1);  

    return 0;    
}

9.3.4 名称空间及其用途

名称空间当前的一些指导原则:

        使用在已命名的名称空间中声明的变量,而不是使用外部全局变量;

        使用在已命名的名称空间中声明的变量,而不是使用静态全局变量;

        如果开发了一个函数库或类库,将其放在一个名称空间中;

        仅在编译指令using作为一种将旧代码转换为使用名称空间的权宜之计;

        不要在头文件使用using编译指令。这样做掩盖了要让那些名称可用;另外,包含头文件的顺序可能影响程序的行为。如果非要使用编译指令using,应将其放在所有预处理器编译指令#include之后;

        导入名称后,首选使用作用域解析运算符或using声明的方法;

        对于using声明,首选将其作用域设置为局部而不是全局。

9.4 总结

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

C++存储方案决定了变量保留在内存的时间(存储持续性)以及程序的那一部分可用访问它(作用域和链接性)。自动变量是在代码块定义的变量。

静态变量在整个程序执行期间都存在,对于在函数外面定义的变量,其所属文件中位于该变量的定义后面的所有函数都可以使用它(文件作用域),并在程序的其他文件中使用(外部链接性)。另一个文件要使用这种变量,必须使用extern关键字来声明它。

对于文件间共享的变量,应在一个文件中包含其定义且不初始化。在函数的外面使用关键字static定义的变量的作用域为整个文件,但是不能用于其他文件(内部链接性)。在代码块中使用关键字static定义的变量被限制在该代码块中(局部作用域,无链接性),但在整个程序执行期间,它都一直存在并且保持原值。

在默认情况下,C++函数的链接性为外部,因此可在文件间共享;但使用关键字static限定的函数的链接性为内部的,被限制在定义它的文件中。

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值