Chapter 9 内存模型和名称空间



本章内容:

  • 单独编译
  • 存储持续性、作用域和链接性
  • 定位(placement)new运算符
  • 名称空间

C++为在内存中存储数据方面提供了多种选择。可以选择数据保留在内存中的时间长度(存储连续性)以及程序的哪一部分可以访问数据(作用域和链接)等。

9.1 单独编译

可以将一个程序分为三个部分

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

头文件包含的内容:

  • 函数原型
  • 使用#define或const定义的符号常量
  • 结构声明
  • 类声明
  • 模板声明
  • 内联函数
  • 函数声明

头文件管理
在同一个文件只能将同一个头文件包含一次
为了避免包含多次,可以使用基于预处理器编译指令#ifndef

#ifndef COORDIN_H_
...
#endif

该方法可以让编译器忽略第一次包含之外的内容。
多个库的链接,链接编译模块时,确保所有对象文件或库都是由同一个编译器生成的。

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

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

  • 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储连续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,它们使用的内存被释放。C++有两种存储连续性为自动的变量。
  • 静态存储持续性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态。它们在程序的整个运行过程中都存在。C++有3种存储持续性为静态的变量。
  • 线程存储持续性:(C++11):当前,多核处理器很常见,这些cpu可同时处理多个执行任务。这些程序能够将计算放在可并行处理的不同线程种。如果变量是使用关键字thread_local声明的,则其生命周期与所属的线程一样长。本书不探讨并行编程。
  • 动态存储持续性:用new运算符分配的内存将一直存在,直到使用delete运算符将其释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储或堆。

9.2.1 作用域和链接

作用域(scope)描述了名称在文件的多大范围内可见。
链接性(linkage)描述了名称如何在不同的单元间共享。

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

9.2.2 自动存储持续性

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

  1. 自动变量的初始化
    可以用人设在声明时其值为已知的表达式来初始化自动变量。

  2. 自动变量和栈

  3. 寄存器变量
    关键字register建议编译器用cpu寄存器来存储自动变量:
    旨在提高访问变量的速度。

9.2.3 静态持续变量

C++为静态存储持续性变量提供了3种链接性:外部链接性(可在其他文件中访问)、内部链接性(只能在当前文件种访问)和无链接性(只能在当前函数或代码块中访问)。

  • 链接性为外部的静态持续变量:在代码块的外面声明它;
  • 链接性为内部的静态持续变量:在代码块的外面声明它,并使用static限定符;
  • 无链接性的静态持续变量:在代码块的内部声明它,并使用static限定符。

未被初始化的静态变量的所有位都被设置位0。
5种变量储存方式

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

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

#include<cmath>
int x;								// zero-initialization
long z = 13 * 13; 					// constant-expression
const double pi = 4.0 * atan(1.0); 	// dynamic-initialization

最后一个值要等到函数被链接且程序执行时。

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

链接性为外部的变量简称外部变量,也成为全局变量。

  1. 单定义规则
    在每个使用外部变量的文件中,都必须声明它;变量只能有一次定义。
    C++提供了两种变量声明:
  • 定义声明,给变量分配空间
  • 引用声明,不给变量分配存储空间,引用声明使用关键字extern且不进行初始化,否则声明为定义,导致分配存储空间。

定义与全局变量同名的局部变量后,局部变量将隐藏全局变量。

C++提供了作用域解析运算符::,该运算符表示使用变量的全局版本。

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

将static限定符用于作用域为整个文件的变量时,该变量的链接性将为内部的。
可以使用外部变量在多文件程序的不同部分之间共享数据;
可以使用链接性为内部的静态变量在同一个文件中的多个函数之间共享数据。

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

无链接性的局部变量。将static用于代码块中定义的变量。
静态无链接性的变量意味着该变量只在该代码块中可用,但它在该代码块不活动状态仍然存在,因此,在两次函数调用之间,静态局部变量的值将保持不变。
注:static声明的静态局部变量只会初始化一次

9.2.7 说明符和限定符

存储说明符:

  • auto (在C++11中不再是说明符);
  • register;
  • static;
  • extern;
  • thread_local(C++11新增的);
  • mutable。
    thread_local指出变量的持续性与线程的持续性相同
    mutable的含义将根据const来解释。
  1. cv限定符
  • const;
  • volatile。
    volatile表明,即使程序代码没有对内存单元进行修改,其值也可能发生变化。可以将一个指针指向某个硬件位置,硬件可能修改其中的内容。
    该关键字的作用是为了改善编译器的优化能力。编译器对几条语句中两次使用了某个变量的值,则编译器可能不让程序查找这个值两次,而是将这个值缓存到寄存器中,该优化假设变量的值在两次使用之间不会变化,将变量声明为volatile,告诉编译器不要进行这种优化。
  1. mutable
    用它来指出,即使结构或类变量为const,其某个成员也可以被修改。

  2. 再谈const
    默认情况下,全局变量的链接性时外部的,
    const全局变量的链接性为内部的;
    如果希望某个常量的链接性为外部,则可以使用extern关键字来覆盖默认的内部链接性。

9.2.8 函数和链接性

默认情况下,函数的存储持续性默认自动为静态的,链接性为外部的,可以在文件间共享。
可以用static将链接性设置为内部的,使之只能在一个文件中使用。必须同时在原型和定义中使用static
单定义规则适用于非内联函数;内联函数不受该规则约束,这允许程序员可以将内联函数的定义放在头文件中
C++在哪里查找函数
原型指出函数是静态的,编译器将在文件内查找该函数定义;
否则,编译器将在所有的程序文件中查找,找到两个定义,将发出错误消息;
灭有找到,编译器将在库中搜索,这意味着如果定义了一个与库函数同名的函数,编译器将使用程序员定义的版本。

9.2.9 语言链接性

语言链接性对函数有影响,链接程序要求每个不同的函数都有不同的符号名。要想使用C库中预编译的函数,用函数原型来指出要使用的约定:

extern "C" void spiff(int);
extern void spiff(int);			// default
extern "C++" void spiff(int);

9.2.10 存储方案和动态分配

通常,编译器使用三块独立的内存:

  • 静态变量(可能细分)
  • 自动变量
  • 动态存储

float *p_fees = new float[20]
如果希望将另一个函数能够使用这80个字节的内容,则必须将其地址传递或返回给该函数。
如果将p_fees的链接性声明为外部的,则该声明后面的所有函数都可以使用它,
在另一个文件中使用,需要:
extern float * p_fees;

  1. 使用new运算符初始化
  • 内置的标量类型(int或double)
    int * pi = new int (6); // set pi to 6
  • 常规结果或数组
struct where{double x; double y; double z;};
where one = new where {2.5, 3.5, 7.2};
int * ar = new int[4]{2,4,6,8};
  1. new失败时
    new找不到请求的内存量,刚开始C++让new返回空指针,但现在将引发异常std::bad_alloc。
  2. new:运算符、函数和替换函数
    运算符new和new[]分别调用如下函数:
void * operator new(std::size_t);
void * operator new[](std::size_t);

这些函数称为分配函数,它们位于全局名称空间中。
同样,也有由delete和delete[]调用的释放函数:

void operator delete(void *);
void operator delete[](void *);
  1. 定位new运算符
    通常,new负责在堆(heap)中找到一个足以能够满足要求的内存块。new运算符还有另一种变体,被称为定位(placement)new运算符,它让您能指定要使用的位置。
    使用定位new特性:
  • 包含头文件new;
  • 将new运算符用于提供了所需地址的参数;
#include<new>
struct chaff
{
	char dross[20];
	int slag;
};
char buffer1[50];
char buffer2[500];
int main()
{
	chaff *p1, *p2;
	int *p3, *p4;
// first, the regular forms of new
	p1 = new chaff;
	p23 = new int[20];
// now, the two forms of placement new
	p2 = new(buffer1) chaff;
	p4 = new(buffer2) int[20];
...
}

定位new运算符使用传递给它的地址,它不跟踪哪些内存单元已被使用,也不查找未使用的内存块,这将内存管理的负担交给了程序员。
buffer指定的内存是静态内存,delete只能释放动态内存,buffer位于delete的管理辖区之外。
5. 定位new的其他形式
标准定位new调用一个接受两个参数的new()函数:

int * p1 = new int;				//invokes new(sizeof(int))
int * p2 = new (buffer) int;	//invokes new(sizeof(int),buffer)
int * p2 = new (buffer) int[40];	//invokes new(40 * sizeof(int),buffer)

定位new函数不可替换,但可重载;

9.3 名称空间

在C++中,名称可以是变量、函数、结构、枚举、类以及类和结构的成员。
C++标准提供了名称空间工具,以便更好的控制名称的作用域。

9.3.1 传统的C++名称空间

名称空间的几个术语:

  • 声明区域(declaration region)是可以在其中进行声明的区域
  • 潜在作用域(potential scope)变量的潜在作用域从声明点开始,到其声明区域的结尾。潜在作用域比声明区域小。
    变量并非在其潜在的作用域内的任何位置都是可见的,例如,他可能被另一个在嵌套声明区域中声明的同名变量隐藏。
  • 作用域(scope)变量对于程序而言可见的范围

9.3.2 新的名称空间特性

定义一种新的声明区域来创建命名的名称空间,提供一个声明名称的区域。
一个名称空间中的名称不会与另外一个名称空间的相同的名称发生冲突。

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

名称空间是开放的是开放的,即可以把名称加入到已有的名称空间中。样例如下:

namspace Jill{
	char * goose(const char *);
}

因此在原来的名称空间为函数提供了原型,可以在该文件后面(或另一个文件夹中)再次使用该名称空间来提供函数的定义。
使用作用域解析运算符::来访问空间的名称。
C++/提供了两种机制来简化对名称空间中名称的使用:

  • using声明
    using Jill::fetch;
    完成该声明后,可以用fetch代替Jill::fetch;
    在函数外使用using声明,将把名称添加到全局名称空间中;

  • using编译指令
    using namespace std;
    使名称空间内的所有名称可用;

using编译指令和using声明增加了名称冲突的可能性,导致二义性

  1. using编译指令和using声明之比较
    using编译指令导入一个名称空间中的所有名称与使用多个using声明不一样,更像使大量使用作用域解析运算符。

通常来说,使用using声明更安全,名称与局部名称发生冲突时,编译器将发出提示。
2. 名称空间的其他特性
可以将名称空间进行嵌套:

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

using编译指令是可传递的;
可以给名称空间创建别名:

namespace my_favourite_things{...};
namespace mft = my_favourite_things;

该技术可以简化对嵌套名称空间的使用。
3. 未命名的名称空间
该名称空间中声明的名称的潜在作用域为:从声明点到该声明区末尾。该空间提供了链接性为内部的静态变量的替代品;

9.3.3 名称空间及其用途

指导原则:

  • 使用在已命名的名称空间中声明的变量,而不是用外部全局变量。
  • 使用在已命名的名称空间中声明的变量,而不是用静态全局变量。
  • 如果开发了一个函数库或类库,将其放在一个名称空间中。事实上,C++当前提倡将标准函数库放在名称空间std中,
  • 仅将编译指令using作为一种将旧代码转换为使用名称空间的权益之计。
  • 不要在头文件中使用using编译指令。如果非要使用using编译指令,放在所有预处理器编译指令之后
  • 导入名称时,首选使用作用域解析运算符或using声明的方法
  • 对于using声明,首选将作用域设置为局部而不是全局

9.4 复习题

  1. 对于下面的情况,应该用那种存储方案?
    a. homer是函数的形参
    自动存储持续性、作用域为局部、无链接性的存储方案。
    b. secret变量由两个文件共享
    静态存储持续性,作用域为全局,外部链接性的存储方案。
    c. topsecret变量由一个文件中的所有函数共享,但对于其他文件来说是隐藏的。
    静态存储持续性,作用域为全局,内部链接性的存储方案。
    d. beecalled记录包含它的函数被调用的次数。
    静态存储持续性,作用域为局部,无链接性的存储方案。

  2. using声明和using编译指令有何区别?
    using声明可以使用名称空间的一个名称其作用域与using所在的声明区域相同;using编译指令可以使用该名称空间的所有名称,与作用域运算符::名称类似使用using编译指令时,就像在一个包含using声明和名称空间本身的最小声明区域中声明了这些名称一样。

  3. 重新编写下面的代码,使其不使用using声明和using编译指令。

#include<iostream>
using namespace std;
int main()
{
	double x;
	cout << "Enter value: ";
	while(!(cin >> x))
	{
		cout << "Bad input.Please enter a number: ";
		cin.clear();
		while(cin.get() != '\n')
			continue;
	}
	cout << "Value = " << x << endl;
	return 0;
}
#include<iostream>
int main()
{
	double x;
	std::cout << "Enter value: ";
	while(!(std::cin >> x))
	{
		std::cout << "Bad input.Please enter a number: ";
		std::cin.clear();
		while(std::cin.get() != '\n')
			continue;
	}
	std::cout << "Value = " << x << std::endl;
	return 0;
}
  1. 重新编写下面的代码,使之使用using声明,而不是using编译指令。
#include<iostream>
using namespace std;
int main()
{
	double x;
	cout << "Enter value: ";
	while(!(cin >> x))
	{
		cout << "Bad input.Please enter a number: ";
		cin.clear();
		while(cin.get() != '\n')
			continue;
	}
	cout << "Value = " << x << endl;
	return 0;
}
#include<iostream>
int main()
{
	using std::cout;
	using std::cin;
	using std::endl;
	double x;
	cout << "Enter value: ";
	while(!(cin >> x))
	{
		cout << "Bad input.Please enter a number: ";
		cin.clear();
		while(cin.get() != '\n')
			continue;
	}
	cout << "Value = " << x << endl;
	return 0;
}
  1. 在一个文件中调用average(3,6)函数时,它返回两个int参数的平均值,在同一个程序的另一个文件中调用时,它返回两个int参数的double平均值。应如何实现?
    在该文件中使用static将函数的链接性定义为内部链接性。
    static double average(int a, int b)

  2. 下面的程序由两个文件组成,该程序显示什么内容?

// file1.cpp
#include<iostream>
using namespace std;
void other();
void another();
int x = 10;
int y;

int main()
{
	cout << x << endl;
	{
		int x = 5;
		cout << x << endl;
		cout << y << endl;
	}
	other();
	another();
	return 0;
}

void other()
{
	int y = 1;
	cout << "Other: " << x << ", " << y << endl;
}
// file2.cpp
#include<iostream>
using namespace std;
extern int x;
namespace
{
	int y = -4;
}
void another()
{
	cout << "another(): " << x  <<", " << y << endl;
}

输出:
10
4
0 //没有定义时是0
Other: 10, 1
Another: 10, -4

  1. 下面的代码将显示什么内容?
#include <iostream>
using namespace std;
void other();
namespace n1
{
	int x = 1;
}
namespace n2
{
	int x = 2;
}

int main()
{
	using namespace n1;
	cout << x << endl;
	{
		int x = 4;
		cout << x << ", " << n1::x << ", " << n2::x << endl;
	}
	using n2::x;
	cout << x << endl;
	other();
	return 0;
}

void other()
{
	using namespace n2;
	cout << x << endl;
	{
		int x = 4;
		cout << x << ", " << n1::x << ", " << n2::x << endl;
	}
}

输出:
1
4, 1, 2
2
2
4, 1, 2
2

posted @ 2021-12-22 23:41  Fight!GO  阅读(44)  评论(0编辑  收藏  举报