第09章 内存模型与名称空间
<c++ primer plus>第六版
9 内存模型和名称空间
9.1 单独编译
单独编译组件函数文件, 然后将它们链接成可执行程序. (c++编译器既编译程序, 也管理链接器)
程序分为三部分:
- 头文件: 包含结构声明, 使用这些结构的函数原型.
- 源代码文件1: 包含与结构有关的函数的代码.
- 源代码文件2: 包含调用函数的代码(这些函数要用到前面的结构).
头文件包含的内容(主要是不被编译的代码):
- 函数原型.
- 使用#define或const定义的符号常量.
- 结构声名.
- 类声明.
- 模板声明.
- 内联函数.
注意, 不要将函数定义和变量声名放到头文件中.
例:
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中是四种), 这些方案的区别在于数据保留在内存中的时间.
- 自动存储, 在函数内部声明的变量(包括参数), 函数执行完, 内存被释放.
- 静态存储, 在函数外部定义的变量+static定义的变量, 在程序整个运行过程中都存在.
- 线程存储(c++11), 程序能将计算放在不同线程中, 如果变量使用关键字thread_local声明, 则其生命周期与所属线程一样长.
- 动态存储, 也称为自由存储(free store)或堆(heap), 使用new运算符分配的内存将一直存在, 直到使用delete运算符将其释放或程序结束.
接下来讨论两点:
- 各种变量何时在作用域内可见(可被程序使用).
- 链接性, 哪些信息可在文件间共享.
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语言一样):
- 外部链接性: 可在其它文件中访问.
- 内部链接性: 只能在当前文件中访问.
- 无链接性 : 只能在当前函数或代码块中访问.
静态变量的数目在程序运行期间不变, 所以编译器分配固定的内存块来存储所有的静态变量, 默认值为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 静态持续性, 外部链接性
外部变量: 链接性为外部的变量简称为外部变量, 也称为全局变量.
基于如下现状:
- 在每个使用外部变量的文件中, 都必须声明它;
- c++单定义规则(One Definition Rule, ODR)指出, 变量只能有一次定义;
c++提供了两种变量声明: - 定义声明(defining declaration), 简称为定义(definition), 该语句给变量分配存储空间.
- 引用声明(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).
- 使用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};
-
new失败时
之前返回空指针, 现在引发异常std::bad_alloc. -
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).
- 定位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;
}
- 定位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).
- using声明和using编译指令.
using声明: 使特定的标识符可用, 将特定的名称添加到它所属的声明区域.
using编译指令: 使整个名称空间可用.
using Jill::fetch; //using声明, 将Jill::fetch添加到当前声明区域(比如main()函数).
using namespace Jack; //使Jack中的所有名称可用.
- 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; //正确
}
- 名称空间其它特性
嵌套
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 名称空间及前途
当前的一些指导原则
- 使用在已命名的名称空间中声明的变量, 而不是使用外部全局变量.
- 使用在已命名的名称空间中声明的变量, 而不是使用静态全局变量.
- 如果开发了一个函数库或类库, 将其放在一个名称空间中.
- 仅将编译指令using作为旧代码转换为使用名称空间的权宜之计. 即不使用using namespace std, 而是使用using std::cout.
- 不要在头文件中使用using编译指令.
- 导入名称时, 首选使用作用域解析运算符或using声明的方法.
- 对于using声明, 首选将其作用域设置为局部而不是全局.