内存模型和名称空间

内存模型和名称空间

 

一、组织程序的策略

程序一般分为三个部分

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

  • 源代码文件:包含与结构有关的函数的代码

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

在编写另一程序,也需要使用这些函数时,只需要包含头文件,并将函数文件添加到项目列表或make列表中即可。

这种组织方式也与oop方法一致。一个文件(头文件)包含了用户定义类型的当以;另一个文件包含操作用户定义类型的函数的代码。这两个文件组成了一个软件包,可用于各种程序中。

Notes: 不要将函数定义或变量声明放在头文件中,因为 #include 可以理解为将头文件中的东西全部复制粘贴到当前文件中。如果在头文件中包含一个函数定义,然后再其他两个文件(属于同一程序)中包含该头文件,则同一程序中将包含同一个函数的两个定义,除非该函数是内联的,否则将会出错。

头文件中包含的内容

  • 函数原型

  • 使用#define或者const定义的符号常量

  • 结构声明(结构声明不创建变量,只是告诉编译器如何创建该结构变量)

  • 类声明

  • 模板声明(与结构声明类似,指示编译器如何生成与源代码中的函数调用相匹配的函数定义)

  • 内联函数

Notes:

  1. 在同一文件中只能将同一头文件包含一次,但是很容易在不知情的情况下将头文件包含多次。解决方法一般有两种。

    • 在头文件的开头一般会写上#program once 防止头文件被多次调用

    • 使用# ifndef 大写头文件名_+#define 大写头文件名 _+#endif。

    这两种方法并不能防止编译器将文件包含两次,而只是让它忽略除第一次包含之外的所有内容。

  2. 在程序中包含自己的头文件时一般使用“ ” 而不是<>,前者编译器将首先查找当前的工作目录或源代码目录,如果没有找到将在标准位置查找。后者则将在存储标准头文件的主机系统的文件系统中查找。

 

二、存储持续性、作用域和链接性

c++使用三种(c++11中是四种)不同的方案来存储数据,这些方案的区别就在数据保留在内存中的时间。

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

  • 静态存储持续性:在函数定义外定义的变量和关键字stasic定义的变量的存储持续性都为静态。他们在程序整个运行过程中都存在

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

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

作用域:描述了名称在文件(翻译单元)的多大范围内可见。即函数中定义的变量可在该函数中使用,但不能在其他函数中使用;而在文件中的函数定义之前定义的变量则可在所有函数中使用。

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

代码块:由花括号括起来的一系列语句。

Notes:

  1. 在函数原型作用域中使用的 名称只在包含参数列表的括号内可用(这就是为什么这些名称是什么以及是否出现都不重要的原因)。

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

不同的c++存储方式是通过存储持续性、作用域和链接性来描述的。

1、自动存储持续性

默认情况下,在函数中声明的函数参数和变量的存储持续性为自动,作用域为局部,没有链接性。即,自动变量只在包含他们的函数或代码块中可见

如果在代码块中定义了变量,则该变量的存在时间和作用域将限制在该代码块内。如,假设在main()的开口定义了一个名为teledeli的变量,然后在main()中开始一个新的代码块,并在其中定义了一个新的变量websight,则teledeli在内部代码块和外部代码块中都是可见的,而websight就只在内部代码块中可见,它的作用域是从定义它的位置到该代码块的结尾:

int main()
{
    int teledeli = 5;
    {
        std::cout << "Hello" << std::endl;
        int websight = -2;
        std::cout << websight << ' ' << teledeli << std::endl;
    }
    std::cout << teledeli << std::endl;
}

如果将内部代码块中的变量命名为teledeli,而不是websight,使得有两个同名的变量(一个位于外部代码块中,另一个位于内部代码块中),这种情况下,程序执行内部代码块中的语句时,将teledeli解释为局部代码块变量。我们说,新的定义隐藏了(hide)以前的定义,新定义可见,旧定义暂时不可见。在程序离开该代码块时,原来的定义重新可见。如下图所示。(注意本段中的隐藏这个概念,在后文中将会继续使用)

 

自动

变量的初始化

可以使用任何在声明时值为已知的表达式来初始化自动变量

自动变量和栈

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

常用的方法是留出一段内存,并将其视为栈,以管理变量的增减。

当函数被调用时,其自动变量将被加入到栈中,栈顶指针指向变量后面的下一个可用的内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

函数调用举例:

如图所示,函数fib()被调用时,传递一个2字节的int和4字节的long。这些值被加入到栈中。当fib()开始执行时,它将名称real和tell同这两个值关联起来。当fib()结束时,栈顶指针重新指向以前的位置。新值没有被删除,但不再被标记,他们所占据的空间将被下一个将值加入到栈中的函数调用所使用。

 

 

2、静态存储持续性

c++为静态存储持续性变量提供了三种链接性:

  • 外部链接性(可在其他文件中访问)

  • 内部链接性(只能在当前文件中访问)

  • 无链接性(只能在当前函数或代码块中访问,自动存储持续性就为无链接性)

与自动变量相比,它们的寿命更长。由于静态变量的数目在程序运行期间时不变的,因此程序不需要使用特使的装置(如栈)来管理他们。编译器将分配固定的内存块来存储所有的静态变量,这些变量在整个程序执行期间一直存在。另外,如果没有显式的初始化静态变量,编译器将把它设置为0。在默认情况下,静态数组和结构将每个元素或成员的所有位都设置为0。

静态存储的五种方式的总结如下:

 

静态变量的初始化

静态变量有三种初始化方式:

  1. 0初始化:即不对静态变量进行赋值时编译器将把他设置为0。例,int x;

  2. 常量表达式初始化:编译器仅根据文件内容(包括被包含的头文件)就可计算表达式。例,int y = 5;int z = 13 *13;

  3. 动态初始化:当表达式中没有足够的信息,变量将被动态初始化。例,const double pi = 4.0 * atan(1.0);此时必须调用函数atan(),这需要等到函数被链接且程序执行时。

1、2被统称为静态初始化,这意味着在编译器处理文件时初始化变量。动态初始化意味着变量将在编译后初始化。

静态持续性、外部链接性

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

单定义规则:变量只能有一次定义。

一方面,在每个使用外部变量的文件中,都必须声明它;另一方面,c++有单定义规则。为满足这种需求,c++提供了以下两种变量声明:

  1. 定义声明(defining declaration)简称定义:它给变量分配存储空间

  2. 引用声明(referencing declaration)简称声明:它不给变量分配存储空间,因为它引用已有的变量。

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

double up //definition ,up is 0
extern int blem; //blem defined elsewhere
extern char gr = 'z'; //definition because initialized 

Notes:

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

  2. 单定义规则并不意味着不能有多个变量的名称相同。例如,在不同函数中声明的同名自动变量是彼此独立的,他们都有自己的地址。另外,局部变量可能隐藏同名的全局变量。然而,虽然程序中可包含多个同名的变量,但每个变量都只有一个定义。

  3. 如果在函数中声明了一个与外部变量同名的变量,这种声明将被视为一个自动变量的定义,当程序执行自动变量所属的函数是,该变量将位于作用域内。(即,即使省略也没有影响)

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

全局变量和局部变量:所有的函数能访问全局变量,因此不用传递参数,但易于访问的代价是使得程序不靠谱。因此,程序越能避免对数据进行不必要的访问,就越能保证数据的完整性。通常情况下,应使用全局变量,应在需要知晓时才传递数据,而不应不加区分的使用全局变量来使数据可用。

静态持续性、内部链接性

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

Notes:如果将作用域为整个文件的变量变为静态的,就不必担心其名称与其他文件中的作用域为整个文件的变量发生冲突。

程序1:

#include <iostream>
​
using namespace std;
int tom = 3; //external variable definition
int dick = 30; //external variable definition
static int harry = 300; //static,internal linkage
​
void remote_access();
​
int main() {
   cout << "main() reports the following address:\n";
   cout <<  &tom << " = &tom, " << &dick << " = &dick, ";
   cout << &harry << " = &harry\n";
   remote_access();
​
    return 0;
}
​

程序2:

#include <iostream>
extern int tom; // tom defined elsewhere
static int dick = 10; //overrides external dick
int harry = 200; //external variable definition,no conflict with main() harry
​
void remote_access()
{
    using namespace std;
    cout << "remote_access() reports the following addresses:\n";
    cout <<  &tom << " = &tom, " << &dick << " = &dick, ";
    cout << &harry << " = &harry\n";
}

程序结果如下,从中我们可以看出,这两个文件使用了同一个tom变量,但使用了不同的dick和harry 变量。

 

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

这种变量的创建是将static限定符用于在代码块中定义的变量。在代码块中使用static时,将导致局部变量的存储持续性为静态的。这意味着虽然该变量只在该代码块中可用,但它在该代码块不处于活动状态时仍然存在(相比于自动变量的区别)。因此,在两次调用函数之间,静态局部变量的值将保持不变。另外,如果初始化了静态局部变量,则程序只在启动时进行一次初始化。以后再调用函数时,将不会像自动变量那样再次被初始化。程序三对该点进行了说明:

程序3:

const int ArSize = 10;
void strcount(const char* str);
​
int main()
{
    char input[ArSize];
    char next;
​
    cout << "Enter a line:\n";
    cin.get(input,ArSize);
    while(cin)
    {
        cin.get(next);
        while (next != '\n')
            cin.get(next);
        strcount(input);
        cout << "Enter next line (empty line to quit):\n";
        cin.get(input,ArSize);
    }
    cout << "Bye\n";
    return 0;
}
​
void strcount(const char* str)
{
    static int total = 0; //static local variable
    int count = 0;
​
    cout << "\"" << str << "\" contains ";
    while(*str++)
        count++;
    total += count;
    cout << count << " characters\n";
    cout << total << " characters total\n";
​
}

Notes:

  1. 该程序演示了一种处理行输入可能长于目标数组的方法。cin.get(input,ArSize)将一直读取输入,直到到达行尾或读取了ArSize-1个字符为止。它把换行符留在输入队列中。该程序使用cin.get(next)读取行输入后的字符。如果next 是换行符,则说明cin.get(input,ArSize)读取了整行;否则说明行中还有字符没有被读取。随后,程序使用一个循环来丢弃余下的字符

  2. 该程序还利用了这样一个事实,即试图使用get(char*,int)读取空行将导致cin为false。

cv限定符volatile:关键字volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。例如,可以将一个指针指向某个硬件位置,其中包含了来自串行端口的时间或信息,在这种情况下,硬件(而不是程序)可能修改其中的内容。或者两个程序可能相互影响,共享数据。该关键字的作用是为了改善编译器的优化能力。例如,假设编译器发现,程序在几条语句中使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化。如果不将变量声明为volatile,则编译器将进行这种优化;将变量声明为voatile,相当于告诉编译器,不要进行这种优化。

3、函数和链接性

c++不允许在一个函数中定义另外一个函数,因此,所有函数存储持续性都自动为静态的,即在整个程序执行期间都一直存在。在默认情况下,函数的链接性为外部的,即可以在文件间共享。实际上,可以在函数原型中使用关键字extern来指出函数实在另一个文件中定义的(可选的)。还可以使用static关键字将函数的链接性设置为内部的,使之只能在一个文件中使用,必须同时在原型和函数定义中使用该关键字

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

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

4、存储方案和动态分配

动态内存:使用c++运算符new分配的内存。动态内存由运算符new和delete控制,而不是由作用域和链接性规则控制。因此,可以在一个函数中分配动态内存,而在另一个函数中将其释放。与自动内存不同,动态内存不是LIFO,其分配和释放顺序要取决于new和delete在何时以何种方式被使用。通常,编译器使用三块独立的内存:一块用于静态变量(可能再细分),一块用用于自动变量,另外一块用于动态存储。

  1. 使用new运算符初始化

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

      如: int* pi = new int(6); double* pd = new double(99.99)

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

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

      where* one = new where{2.5,5.3,7.2}

  2. new失败时将引发异常std::bad_alloc

  3. 定位new运算符

    通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,他能够指定要使用的位置

    Notes: 要使用定位new特性,首先需要包含头文件new,它提供了这种版本的new运算符的原型;然后将new运算符用于提供了所需地址的参数。除需要指定参数外,句法与常规new运算符相同。具体的说,使用定位new运算符时,变量后面可以有方括号,也可以没有

    #include <iostream>
    #include <new>
    ​
    using namespace std;
    ​
    const int BUF = 512;
    const int N = 5;
    char buffer[BUF];
    int main()
    {
        double *pd1, *pd2;
        int i;
        cout << "Calling new and placement new:\n";
        pd1 = new double[N];
        pd2 = new (buffer) double[N];
    ​
        for (i = 0;i < N;i++)
            pd2[i] = pd1[i] = 1000 + 20.0 * i;
        cout << "Memory addresses:\n" << " heap: " << pd1 << " static: " << (void *) buffer << endl;
        cout << "Memory contents:\n";
        for (i = 0; i < N; i++)
        {
            cout << pd1[i] << " at " << &pd1[i] << "; ";
            cout << pd2[i] << " at " << &pd2[i] << endl;
        }
    ​
        cout << "\nCalling new and placement new a second time:\n";
        double *pd3, *pd4;
        pd3 = new double[N];
        pd4 = new (buffer) double[N];
    ​
        for (i = 0;i < N;i++)
            pd4[i] = pd3[i] = 1000 + 40.0 * i;
        cout << "Memory contents:\n";
        for (i = 0; i < N; i++)
        {
            cout << pd3[i] << " at " << &pd3[i] << "; ";
            cout << pd4[i] << " at " << &pd4[i] << endl;
        }
    ​
        cout << "\nCalling new and placement new a third time:\n";
        delete []pd1;
        pd1 = new double[N];
        pd2 = new(buffer + N * sizeof(double)) double[N];
    ​
        for (i = 0;i < N;i++)
            pd2[i] = pd1[i] = 1000 + 60.0 * i;
        cout << "Memory contents:\n";
        for (i = 0; i < N; i++)
        {
            cout << pd1[i] << " at " << &pd1[i] << "; ";
            cout << pd2[i] << " at " << &pd2[i] << endl;
        }
        delete [] pd1;
        delete [] pd3;
    ​
        return 0;
    }

Notes: 使用new定位运算符时,不能通过delete来释放内存,因为buffer指定的内存是静态内存。而delete只能用于指向常规new运算符分配的堆内存。即,buffer位于delete的管辖区之外(栈区)。

三、名称空间

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

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

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

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

全局名称空间(global namespace):对应于文件级声明区域。

任何名称空间中的名称都不会与其他名称空间中的名称发生冲突。同时名称空间是开放的,可以把名称加入到已有的名称空间中。

1、using声明和using编译指令

using声明使特定的标识符可用,using编译指令使整个名称空间可用。using声明将特定的名称添加到它所属的声明区域中。如, using Jill::fetch。完成该声明后可以使用名称fetch代替Jill::fetch。

namespace Jill
{
    double bucket(double n){...}
    double fetch;
    struct Hill{...};
}
char fetch;
int main()
{
    using Jill::fetch //put fetch into local namespace 
    double fetch; //Error!Already have a local fetch
    cin >> fetch; //read a value into Jill::fetch
    cin >> ::fetch; //read a value into global fetch
}

Note: fetch将覆盖同名的全局变量。

using声明使一个名称可用,而using编译指令使所有的名称都可用。如,using namespace std。

2、using编译指令和using声明指令的比较

使用using编译指令导入一个名称空间中所有的名称与使用多个using声明是不一样的,而更像是大量使用作用域解析运算符。使用using声明时,就好像声明了相应的名称空间一样。如果某个名称已经在函数中声明了,则不能用using声明导入相同的名称。然而,使用using编译指令时,将进行名称解析,就像在包含using声明和名称空间本身的最小声明区域中声明了名称一样。在下面的示例中,名称空间是全局的。如果使用using编译指令到日一个已经在函数中声明的名称,则局部名称将隐藏名称空间名,就像隐藏同名的全局变量一样,不过仍然可以像下面的示例中那样使用作用域解析运算符:

namespace Jill{
    double bucket(double n){...}
    double fetch;
    struct Hill{...};
}
char fetch;
int main()
{
    using namespace Jill; //import all namespace names
    Hill Thrill; //create a type Jill::Hill structure
    double water = bucket(2); //use Jill::bucket();
    double fetch; //not an error;hides Jill::fetch
    cin >> fetch; //read a value into the local fetch 
    cin >> ::fetch; //read a value into global fetch
    cin >> Jill::fetch; //read a value into Jill::fetch
    ...
}
​
int foom()
{
    Hill top; //ERROR
    Jill::Hill crest; //valid
}

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

一般来说,使用using声明比使用using编译指令更安全,这是由于它只导入指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using编译指令导入所有名称,包括可能并不需要的名称,如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器不会发出警告。

参考文献:C++ primer plus (第六版)中文

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值