第09章 内存模型与名称空间

<c++ primer plus>第六版

9 内存模型和名称空间

9.1 单独编译

单独编译组件函数文件, 然后将它们链接成可执行程序. (c++编译器既编译程序, 也管理链接器)

程序分为三部分:

  1. 头文件: 包含结构声明, 使用这些结构的函数原型.
  2. 源代码文件1: 包含与结构有关的函数的代码.
  3. 源代码文件2: 包含调用函数的代码(这些函数要用到前面的结构).

头文件包含的内容(主要是不被编译的代码):

  1. 函数原型.
  2. 使用#define或const定义的符号常量.
  3. 结构声名.
  4. 类声明.
  5. 模板声明.
  6. 内联函数.
    注意, 不要将函数定义和变量声名放到头文件中.

例:
coordin.h, 头文件, 结构声明, 函数原型等.

#ifndef COORDIN_H_ //防止在不知情的情况下将头文件包含多次.
#define COORDIN_H_

//structure templates
struct polar
{
    double distance;
    double angle;
};

struct rect
{
    double x;
    double y;
};

//prototypes
polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

#endif

file2.cpp, 函数定义.

#include <iostream>
#include <cmath>
#include "coordin.h"

polar rect_to_polar(rect xypos)
{
    using namespace std;

    polar answer;

    answer.distance = sqrt(xypos.x*xypos.x + xypos.y*xypos.y);
    answer.angle = atan2(xypos.y, xypos.x);

    return answer;
}

void show_polar(polar dapos)
{
    using namespace std;

    const double rad_to_deg = 57.29577951;

    cout << "distance=" << dapos.distance << endl;
    cout << "angle   =" << dapos.angle * rad_to_deg << endl;
}

file1.cpp, main函数.

#include<iostream>
#include "coordin.h"

using namespace std;
int main()
{
    rect rplace;
    polar pplace;

    rplace.x = 2;
    rplace.y = 2;

    pplace = rect_to_polar(rplace);
    show_polar(pplace);

    return 0;
}

运行方法

g++ coordin.h file1.cpp file2.cpp -o file1.o
file1.o

9.2 存储持续性, 作用域, 链接性

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

  1. 自动存储, 在函数内部声明的变量(包括参数), 函数执行完, 内存被释放.
  2. 静态存储, 在函数外部定义的变量+static定义的变量, 在程序整个运行过程中都存在.
  3. 线程存储(c++11), 程序能将计算放在不同线程中, 如果变量使用关键字thread_local声明, 则其生命周期与所属线程一样长.
  4. 动态存储, 也称为自由存储(free store)或堆(heap), 使用new运算符分配的内存将一直存在, 直到使用delete运算符将其释放或程序结束.

接下来讨论两点:

  1. 各种变量何时在作用域内可见(可被程序使用).
  2. 链接性, 哪些信息可在文件间共享.

9.2.1 作用域和链接

作用域(scope): 名称(变量)在文件(编译单元)的多大范围内可见.

函数中定义的变量可在该函数中使用, 不能在其它函数中使用.
文件中, 函数定义之前定义的变量, 可在所有函数中使用.

链接性(linkage): 描述名称如何在不同单元(文件)间共享.

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

9.2.2 自动存储连续性.

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

c++11中, auto用于自动类型推断, 但在c和以前的c++中, auto用于显式地指出变量为自动变量.
因为auto以前很少人用, 所以给它赋予了新含义.

c++11中, register只是显式地指出变量是自动的, 没什么用. 以前主要是建议编译器使用CPU寄存器来存储自动变量, 提高访问速度.

9.2.3 静态持续变量

c++为静态存储持续性变量提供了3种链接性(和c语言一样):

  1. 外部链接性: 可在其它文件中访问.
  2. 内部链接性: 只能在当前文件中访问.
  3. 无链接性 : 只能在当前函数或代码块中访问.

静态变量的数目在程序运行期间不变, 所以编译器分配固定的内存块来存储所有的静态变量, 默认值为0.

...
int global = 1000;        //外部链接性(代码块外声明, 不使用static)
static int one_file = 50; //内部链接性(代码块外声明, 使用static限定符).
int main()
{ ...  }

void func(int n)
{
    static int count=0;   //无链接性(代码块内声明, 使用static限定符)
}

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

外部变量: 链接性为外部的变量简称为外部变量, 也称为全局变量.

基于如下现状:

  1. 在每个使用外部变量的文件中, 都必须声明它;
  2. c++单定义规则(One Definition Rule, ODR)指出, 变量只能有一次定义;
    c++提供了两种变量声明:
  3. 定义声明(defining declaration), 简称为定义(definition), 该语句给变量分配存储空间.
  4. 引用声明(referencing declaration), 简称为声明(declaration), 它不给变量分配存储空间, 只引用已有变量.

引用声明使用关键字extern, 且不进行初始化.
double up; //定义变量, 默认初始化为0
extern int blem; //引用其它文件的外部变量
extern char gr = 'z'; //由于对变量进行了初始化, 本语句变成了定义声明, 而不是引用声明

如果要在多个文件中使用同一个外部变量, 只需要在一个文件中定义它, 其它使用它的文件中使用extern声明它.

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

将static限定符用于作用域为整个文件的变量时, 该变量的链接性将为内部的. 链接性为内部的变量只能在其所属的文件中使用.
常规外部变量(不起个头static限定符)具有外部链接性, 可以在其它文件中使用.

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

静态持续家族三个成员:
作用域为整个文件的外部链接性变量,
作用域为整个文件的内部链接性变量,
无链接性的局部变量.

无链接性的局部变量定义方法: 将static限定符用于代码块中定义的变量.
这样变量只在代码块中可用, 但在代码块不处于活动状态时仍然存在.
因此在两次函数调用之间, 静态局部变量的值将保持不变.
如果函数中初始化了静态局部变量, 则程序只在启动时进行一次初始化, 以后再调用函数时, 将不会像自动变量那样再次初始化.

9.2.7 说明符和限定符

存储说明符(storage class specifier)

auto        : 原来用于指出变量为自动变量, c++11中用于自动类型推断.(c++11中不再是说明符)
register    : 指示寄存器存储
static      : 
extern      : 表示是引用声明
thread_local: 指出变量的持续性与其所属线程的持续性相同(c++11中新增的)
mutable     : 即使结构(或类)变量为const, 其某个成员也可以被修改.

cv-限定符(cv-qualifier)
const : 初始化后不能再修改.
volatile: 即使程序代码没有对内存单元进行修改, 其值也可能发生变化.

9.2.8 函数和链接性

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

9.2.9 语言链接性

language linking

c语言中, 一个名称只对应一个函数. 比如函数spiff函数名翻译为_spiff.

c++中, 同一个名称可能对应多个函数, 所以c++编译器执行名称矫正或名称修饰, 为重载函数生成不同的符号名称.
如将spiff(int)转换为_spiff_i, 将spiff(double, double)转换为_spiff_d_d.
这种方法称为c++语言连接(c++ language linkage).

extern "C" void spiff(int); //使用c语言的查找符号
extern "C++" void spaff(int); //使用c++语言的查找符号

9.2.10 存储方案和动态分配

通常, 编译器使用三块独立内存:
一块用于静态变量(全局变量??).
一块用于自动变量(局部变量??).
一块用于动态存储(new, delete).

  1. 使用new运算符初始化

初始化内置标量类型: 类型后加上初始值, 并用括号括起来
int *pi = new int(6); //将pi指向的空间初始化为6.
double *pd = new double(99.9); // *pd设置为99.9.

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

struct where{
    double x;
    double y;
    double z;
};
where * one = new where {2.5, 5.3, 7.2};

int * ar = new int[4] {2,4,6,7}

c++11中还可以将列表初始化用于单值变量

int *pin = new int {6};
double *pdo = new double {99.9};
  1. new失败时
    之前返回空指针, 现在引发异常std::bad_alloc.

  2. new运算符, 函数, 替换函数

运算符new和new[] 分别调用如下函数:

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

这些函数称为分配函数.

对应的释放函数:

void * operator delete(void *);
void * operator delete[](void *);

例如:

int *pi = new int; 将被转换为这样:
int *pi = new(sizeof(int));

int *pa = new int[40]; 将被转换为这样:
int *pa = new(40*sizeof(int));

delete pi; 转换为如下函数调用
delete(pi);

c++将这些函数称为可替换的(replaceable).

  1. 定位new运算符

通常new负责在堆(heap)中找到一个足以满足要求的内存块.
new运算符还有一种变体, 称为定位(placement)new运算符, 程序员可以指定要使用的位置
placement new运算符另一种用法是, 将其与初始化结合使用, 从而将信息放在特定的硬件地址处.

#include<iostream>
#include<new>

const int BUF = 512;
const int N = 5;
char buffer[BUF];

int main()
{
    using namespace std;

    double *pd1, *pd2;
    int i;

    cout << "Calling new and placement new:" << endl;

    pd1 = new double[N];          //use heap
    pd2 = new (buffer) double[N]; //use buffer array

    for (i=0; i<N; i++)
    {
        double tmp = 1000 + 20.0*i;
        pd1[i] = tmp;
        pd2[i] = tmp;
    }

    cout << "Memory addresses:" << endl;
    cout << "    heap  : " << pd1 << endl;
    cout << "    static: " << (void *)buffer << endl;
    cout << endl;

    cout << "Memory contents:" << endl;
    for (i=0; i<N; i++)
    {
        cout << "    pd1[" << i << "]=" << pd1[i] << " at " << &pd1[i] << ", pd2[" << i << "]=" << pd2[i] << " at " << &pd2[i] << endl;
    }
    cout << endl;

    cout << "Calling new and placement new again:" << endl;
    double *pd3, *pd4;

    pd3 = new double[N];          //寻找新的地址
    pd4 = new (buffer) double[N]; //盖写旧数据

    for (i=0; i<N; i++)
    {
        double tmp = 1000 + 40.0*i;
        pd3[i] = tmp;
        pd4[i] = tmp;
    }

    cout << "Memory contents:" << endl;
    for (i=0; i<N; i++)
    {
        cout << "    pd3[" << i << "]=" << pd3[i] << " at " << &pd3[i] << ", pd4[" << i << "]=" << pd4[i] << " at " << &pd4[i] << endl;
    }
    cout << endl;

    cout << "Calling new and placement new third time:" << endl;
    delete [] pd1;

    pd1 = new double[N];          //??
    pd2 = new (buffer+ N*sizeof(double)) double[N]; //增加一个偏移量

    for (i=0; i<N; i++)
    {
        double tmp = 1000 + 60.0*i;
        pd1[i] = tmp;
        pd2[i] = tmp;
    }

    cout << "Memory contents:" << endl;
    for (i=0; i<N; i++)
    {
        cout << "    pd1[" << i << "]=" << pd1[i] << " at " << &pd1[i] << ", pd2[" << i << "]=" << pd2[i] << " at " << &pd2[i] << endl;
    }
    cout << endl;
    delete [] pd1; //可以用delete来释放new分配的内存, 但不可以用delete释放placement new指定的内存(它们是静态内存).
    delete [] pd3;

    return 0;
}
  1. 定位new运算符的其它形式
int * pi = new int            ; //调用 new(sizeof(int))
int * pi = new(buffer) int    ; //调用 new(sizeof(int), buffer)
int * pi = new(buffer) int[40]; //调用 new(40*sizeof(int), buffer)

9.3 名称空间

9.3.1 传统的c++名称空间

声明区域(declaration region): 可以在其中进行声明区域
潜在作用域(potential scope): 变量的潜在作用域从声明点开始, 到其声明匹配的结尾.

9.3.2 新的名称空间特性

可以使用关键字namespace创建命名的名称空间.

namespace Jack
{
    double pail; //声明变量
    void fetch();//声明原型
    int pal;     //声明变量
    struct Well {...}; //声明结构
}

namespace Jill
{
    double bucket(double n) {...} //定义函数
    double fetch;//声明变量
    int pal;     //声明变量
    struct Hill {...}; //声明结构
}

名称空间可以是全局的, 也可以位于另一个名称空间中, 但不能位于代码块中.
默认情况下名称空间中声明的名称的链接性为外部.

还有另一个名称空间, 是全局名称空间(global namespace), 它对应于文件级声明区域. 全局变量现在被描述为位于全局名称空间中.

访问给定名称空间中的名称: 使用作用域解析运算符::

Jack::pail = 12.34; //使用变量
Jack::fetch(); //调用函数

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

  1. using声明和using编译指令.

using声明: 使特定的标识符可用, 将特定的名称添加到它所属的声明区域.
using编译指令: 使整个名称空间可用.

using Jill::fetch; //using声明, 将Jill::fetch添加到当前声明区域(比如main()函数).
using namespace Jack; //使Jack中的所有名称可用.
  1. using声明和using编译指令比较.
namespace Jill
{
    double bucket(double n) {...}
    double fetch;
    struct Hill {...};
}

char fetch; //global namespace
int main()
{
    using namespace Jill; //导入Jill中的所有name
    Hill Thrill;          //创建一个Jill::Hill结构
    double water = bucket(2); //使用Jill::bucket

    double fetch;         //无错误, 声明一个local fetch, 将隐藏Jill::fetch和global fetch, 这时将存在三个fetch
    cin >> fetch;         //读取一个值放到local fetch中
    cin >> ::fetch;       //读取一个值放到global fetch中
    cin >> Jill::fetch;   //读取一个值放到Jill::fetch中

    //注意, 
    //1) 当前main函数中, 名称Jill::fetch被放在局部名称空间中, 但其作用域不是局部的, 因此不会覆盖全局fetch.
    //2) 局部声明的fetch将隐藏Jill::fetch和global fetch, 这时后两个fetch只能通过域解析运算符访问.
}

int foom()
{
    Hill top;         //错误, 没有找到Hill, 当前函数没有使用using namespace Jill, 所以不能直接访问Hill.
    Jill::Hill crest; //正确
}
  1. 名称空间其它特性

嵌套

namespace elements
{
    namespace fire
    {
        int flame;
        ...
    }
    float water;
}

这时flame是指element:🔥:flame.

4 未命名的名称空间

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

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

它的潜在作用域为: 从声明点到该声明区域末尾, 跟全局变量相似.
差别在于不能显式在使用using编译指令或using声明来使它在其它位置可用, 即不能在其它文件中使用它们, 提供了链接性为内部的静态变量的替代品.

上述代码等价于:

static int ice; //静态存储, 内部链接性.
static int bandycoot;

9.3.3 名称空间示例

9.3.4 名称空间及前途

当前的一些指导原则

  1. 使用在已命名的名称空间中声明的变量, 而不是使用外部全局变量.
  2. 使用在已命名的名称空间中声明的变量, 而不是使用静态全局变量.
  3. 如果开发了一个函数库或类库, 将其放在一个名称空间中.
  4. 仅将编译指令using作为旧代码转换为使用名称空间的权宜之计. 即不使用using namespace std, 而是使用using std::cout.
  5. 不要在头文件中使用using编译指令.
  6. 导入名称时, 首选使用作用域解析运算符或using声明的方法.
  7. 对于using声明, 首选将其作用域设置为局部而不是全局.
posted @ 2022-07-10 11:12  编程驴子  阅读(46)  评论(0编辑  收藏  举报