第九章 - 内存模型和名称空间

一,单独编译

C++鼓励程序员将组件函数放在独立的文件中,可以单独编译这些文件,然后将他们连接成可执行程序。如果只修改了一个文件,则可以只重新编译该文件,然后将它与其他文件的编译版本链接,这使得大程序的管理更便捷。

1.1,常用的组织程序的策略

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

这是一种非常有用的组织程序的策略,同时,这种组织方式也与OOP方法一致。一个头文件包含了用户定义类型的定义,另一个文件包含了操作用户定义类型的函数的代码。这两个文件组成一个软件包,可用于各种程序中。

1.2,头文件常包含的内容

  1. 函数原型。
  2. 使用#define或const定义的符号常量。
  3. 结构声明。
  4. 类声明。
  5. 函数模板。
  6. 内敛函数。

1.3,使用头文件注意下面的问题

  1. 不要将函数定义或变量声明放到头文件中,这样做通常会导致编译错误。例如:如果在头文件包含函数定义,另外两个文件包含该头文件,则同一个程序将包含同一个函数的两个定义,除非函数是内敛的,否则将出错。
  2. 将结构声明放在头文件中是可以的,因为他们不创建变量,而只是在源代码文件中声明结构变量时,告诉编译器如何创建该结构变量。同样,模板声明不能单独编译,它们只是指示编译器如何生成函数定义。
  3. 被声明为const的数据与内敛函数有特殊的链接属性,因此,可以放在头文件中。
  4. 内敛函数不受单定义规则的约束,这允许程序员将内敛函数放在头文件中。这样,包含了头文件的每个文件都有内敛函数的定义。然而,C++要求同一个函数的所有内敛定义必须是相同的。

二,内存模型

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

2.1,四种不同的存储方案

  1. 自动存储持续性:在函数中声明的变量(包括函数的参数)的存储持续性为自动的。它们在程序执行其所属的函数时被创建,在执行完函数时,它们使用的内存被释放。C++中有两种存储持续性为自动的变量。
  2. 静态存储持续性:在函数外面定义的变量和使用关键字static定义的变量的存储持续性为静态的。它们在程序的整个执行的过程中都存在。C++中有三种存储持续性为静态的变量。
  3. 线程存储持续性(C++11):当前,多核处理器很常见,这些CPU可同时处理多个任务。这让程序可将计算放在可并行处理的不同的线程中。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。
  4. 动态存储持续性:使用new运算符动态分配的内存,在整个程序执行期间将一直存在,直到使用delete运算符将其释放掉或是程序执行结束。这种内存的持续性是动态的。

2.2,不同的存储方式是如何来描述的

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

  1. 存储持续性:数据在内存中保留的时间。
  2. 作用域:描述了名称(变量名)在文件的多大范围内可见。例如,在函数中定义的变量可以在该函数中使用不能再函数外面使用,而在函数外面定义的变量可以在所有的函数中使用。
  3. 连接性:描述了名称如何在不同文件间共享。连接性为外部的名称可以在文件间共享,连接性为内部的名称只能由一个文件中的函数共享。

2.3,自动存储持续性(有 2 种存储持续性为自动的变量)

2.3.1,自动变量

在函数中定义的变量或是函数参数的存储持续性为自动的,作用域为局部没有连接性,系统不会对自动的自动变量进行初始化。

int main(){
    int a;
    cout<<a;
    return 0;
}

2.3.2,寄存器变量

关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量,目的是为了提高访问变量的速度。

register int a = 10;

2.4,静态存储持续性(有 3 种存储持续性为静态的变量)

  1. 连接性为外部的静态存储变量,必须要在函数的外面声明。
  2. 连接性为内存的静态存储变量,必须要在函数的外面声明,并使用static关键字。
  3. 没有连接性的静态存储变量,必须要在函数里面声明,并使用static关键字。如果初始化了静态局部变量,则程序只在启动时进行一次初始化,以后再调用函数时,将不会像自动变量那样再次被初始化。

注意:
未被初始化的静态变量的值被初始化为0。

2.5,C++单定义规则

该规则指出,变量只能有一次定义。为了满足这种需求,C++提供了两种变量声明。一种是定义声明,它给变量分配存储空间;另一种是引用声明,它不给变量分配存储空间,因为它引用已有的变量。引用声明使用关键字extern,且不进行初始化,否则,声明为定义,导致分配存储空间。

2.6,C++对常量类型的规则的修改

在默认情况下,全局变量的连接性为外部的,但const全局变量的连接性为内部的,这样的修改让程序员很轻松。

//const.h
const int INF_MAX = 100;
const int MIN = 0;

例如,我们创建一个头文件const.h,将一组常量放到这个头文件中,并在同一个程序的多个文件中使用这个头文件。那么,预处理器将头文件的内容包含到每个源文件后,所有的源文件都下面的定义:

const int INF_MAX = 100;
const int MIN = 0;

如果全局声明的const变量的连接性像常规变量那样是外部的,则根据单定义规则,这将出错。也就是说只有一个文件可以包含前面的声明,而其他的关键字必须使用extern关键字来提供引用声明。另外,只用未使用extern关键字的声明才能进行初始化。全局定义的const变量的连接性为内部的,意味着每个文件都有自己的一组常量,而不是所有的文件共享一组常量。每个定义都是其所属文件私有的,这就是能把常量定义放在头文件中的原因。

2.7,函数的连接性

和变量一样函数也有连接性。和C语言一样,C++不允许在一个函数中定义另外一个函数,因此,所有函数的存储连接性为静态的,即在整个程序执行期间一直存在。在默认情况下函数的连接性为外部的,即可以在文件间共享。实际上可以使用extern关键字来指出函数是在另一个文件中定义的。可以使用static关键字将函数的连接性设置为内部的,使之只能在一个文件中使用,必须同时在函数原型与函数声明中使用该关键字:

//函数原型
static int display();

//函数声明
static int display(){    
    cout<<"hello world."<<endl;
}

2.8,编译器在哪里查找函数

假设在程序的一个文件中调用一个函数,编译器将到哪里去寻找函数的定义呢?如果该文件中的函数原型指出这个函数是静态的,则编译器只在该文件中查找函数定义;否则,编译器在所有的程序文件中查找;如果在程序文件中没有找到,编译器将在库中搜索。

2.9,动态内存分配

2.9.1,使用new动态分配内存

如果要为内置的类型分配空间并初始化,可在类型名称后面加上初始值,并将其用括号括起来。

int *p = new int(8);
cout<<*p<<endl;

注意:
当内存不足时,new可能会失败,最初的10年C++让new返回空指针,但现在引发异常std::bad_alloc。

2.9.2,new运算符与函数替换

运算符new与new[]分别调用以下函数:

void * operator new(std::size_t);  //use by new
void * operator new[](std::size_t);  //use by new[]

delete与delete[]调用的释放函数

void operator delete(void *);  //use by delete
void operator delete[](void *);  //use by delete[]

2.9.3,定位new运算符

通常,new负责在堆中找出一块满足需求的内存块。new运算符还有一种变体,被称为定位new运算符,它能让你指定要使用的位置。程序员可以使用这种特性来设置其内存管理规则或在特定的位置创建对象。

#include <iostream>
using namespace std;
char buffer[100];
int main(){    
    int *p = new (buffer)int[20];  //buffer即为要指定的位置    
    return 0;
}

三,名称空间

3.1,为什么要使用名称空间

在C++中的名称可以是变量,函数,类以及类的成员。随着项目的增大,名称相互冲突的可能性也在增大。使用多个厂商的类库时,可能导致名称冲突。例如,两个库都定义了名为List,Tree和Node的类,但是定义的方式不兼容。用户可能希望使用一个库的List类,而是用另一个库的Tree类,这种冲突称为名称空间问题。C++标准提供了名称空间工具,以便更好的控制名称的作用域。

3.2,名称空间的特性

  1. 可以使用名称空间,创建一个声明名称的区域。一个名称空间中的名称不会另外一个名称空间中的相同名称发生冲突。
  2. 名称空间可以是全局的,也可以位于另外一个名称空间中,但是不能位于代码块中。因此,在默认情况下,名称空间中声明的名称的连接性为外部的。
  3. 名称空间是开放的,即可以把名称加入到已有的名称空间中。
  4. 使用作用域限定符访问名称空间中的名称。

3.3,using声明与using编译指令

我们并不希望每次使用名称时都对它进行限定,C++提供了两种机制来简化对名称空间中名称的使用。

3.3.1,using声明

using声明将特定的名称添加到它所属的声明区域中,在函数的内部使用using声明,将一个名称添加到局部作用域中。

namespace Jack{
    string name;
    string number;
    int age;
}

string name = "global name";

int main(){
    //把Jack::name放到一个局部名称空间中
    using Jack::name;
    //Error,在局部空间中已经存在名称name
    string name;
    cin>>name;
    cout<<::name;
    return 0;
}

输出结果

local name
global name
Process returned 0 (0x0)   execution time : 9.672 s
Press any key to continue.

在函数的外部使用using声明,将一个名称添加到全局作用域中

namespace Jack{
    string name;
    string number;
    int age;
}

using Jack::name;

int main(){
    name = "global name";
    cout<<name;
    return 0;
}

输出结果

global name
Process returned 0 (0x0)   execution time : 0.011 s
Press any key to continue.

3.3.2,using编译指令

using编译指令使一个名称空间中的所有名称可用,using编译指令由名称空间和他前面的关键字using namespace组成。在全局声明区域中使用using指令,将使该名称空间中的所有名称在全局作用域中可用,在函数中使用using指令,将使该名称空间中的所有名称在函数中可用。

3.3.3,使用using声明与using指令,增加了名称冲突的可能性

namespace Jack{
    string name;
}

namespace Jess{
    string name;
}

int main(){
    using Jack::name;
    using Jess::name;
    return 0;
}

Jack::name与Jess::name是不同的标识符,表示不同的内存单元。如果像上面那样使用using声明,将导致二义性。

3.4,using声明与using编译指令的比较

3.4.1, 使用using声明将名称空间的名称导入局部声明区域,在这个区域存在相同的名称,编译时会出错

namespace Jack{
    string name;
}

int main(){
    using Jack::name;
    string name;
    return 0;
}

3.4.2,使用using编译指令,将该名称空间的名称导入局部区域,则局部版本将隐藏名称空间版本

namespace Jack{
    string name = "global name.";
}

int main(){
    using namespace Jack;
    string name = "local name.";
    cout<<name;
    return 0;
}

输出结果

local name.
Process returned 0 (0x0)   execution time : 0.007 s
Press any key to continue.

3.4.3,使用using声明比使用using编译指令更加的安全

这是由于它只导入指定的名称。如果这个名称与局部的名称发生冲突,编译器将发出指示。using指令导入所有的名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器不会发出警告。

3.5,自定义名称空间

自定义命名空间Person

//Person.cpp

#include <string.h>
namespace Person{
    //系统提供的标准库位于std命名空间中,要使用string就要导入这个名称
   using std::string;
    using std::cout;
    using std::endl;
    string name;
    int age;
    void display(const string &name, int age){
        cout<<"name: "<<name<<endl;
        cout<<"age: "<<age<<endl;
    }
}

在另外一个文件中使用前面定义的命名空间

//main.cpp

#include <iostream>
#include "Person.cpp"

int main(){
    using namespace Person;
    string name = "local name.";
    int age = 20;
    display(name, age);
    return 0;
}

输出结果

name: local name.
age: 20

Process returned 0 (0x0)   execution time : 0.017 s
Press any key to continue.

猜你喜欢

转载自blog.csdn.net/cloud323/article/details/80937201