9.内存模型与名称空间

00 日志

2022/04/15 起笔
2022/04/18 完善#ifndef笔记

0.程序规范

<文件名> ——编译器在存储标准头文件的主机系统的文件系统中查找
“文件名”——在当前工作目录或源代码目录(或其他目录,取决于编译器)查找

1. 多文件程序

1)程序的分层:

  • 头文件:函数原型、符号常量 、结构|类|模板声明、内联函数
    (注意不要加入函数定义与变量声明
  • 源代码文件:结构实现
  • 源代码文件:结构调用

2)如何避免包含多次头文件

#ifndef COORDIN_H_  只要将#define用于名称就足以完成该名称的定义
#define COORDIN_H_  
(通常使用#define语句来创建符号常量,#define MAX 1096 
但只要将#define用于名称就足以完成该名称的定义)
(通常根据include文件名来选择名称,并加上一些下划线。
以创建一个在其他地方不太可能被定义的名称)
...
#endif

一般根据include文件名来选择名称,并加上一些下划线
编译器首次遇到该文件时,名称没有被定义,查看中间内容,后面再遇到则执行#endif后的内容

这是一种常见的防护方案

2.存储持续性、作用域与链接性

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

  • 一块用于静态变量(可能再细分)
  • 一块用于自动变量
  • 一块用于动态存储

1) C++的存储方案

  • 自动存储连续性:函数定义中声明的变量(包括函数参数)——在程序开始执行函数时被创建,函数执行结束后被释放
  • 静态存储连续性: 函数定义之外的变量+使用static定义的变量——整个程序运行过程中都存在
  • 线程存储持续性(C++11):并行编程
  • 动态存储持续性:用new运算符分配的内存——已知存在到delete释放或和程序结束——有时称为自由存储或堆

2) 作用域与链接性

  • 作用域:描述多大范围内可见
  • 链接性:外部或者内部,文件间是否可共享

2.1 自动存储持续性

1) 自动变量与栈

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

函数结束后的栈: 新值没有被删除但是不在标记,栈顶指针重新指向以前的位置,它们所占据的空间将被下一个将值加入到栈中的函数调用所使用

若函数中存在两个同名变量(位于内外代码块中):新的定义会隐藏旧的定义

2) 寄存器变量

register int count_fast

建议编译器使用CPU寄存器来存储自动变量表示变量用得很多
目的在于提高访问变量的速度
目前C++11中用于显式地指出变量是自动的,而使用它的唯一原因在于这个变量的名称可能与外部变量相同(与auto以前的用途相同)
但保留该关键字的原因是——避免使用了该关键字的现有代码非法

2.2 静态持续变量

  • 所有静态持续变量在整个程序运行期间都存在
  • 编译器分配固定的内存块来存储所有的静态变量
  • 如果没有显示初始化静态变量,编译器将它设置为0(每个位)(零初始化)
    过程:首先,所有静态变量被零初始化(不管是否显式),接下来执行常量初始化,如果没有足够的信息,变量则进行动态初始化(变量在编译后初始化)
    -结合前面内容,变量一共有五种储存方式

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

链接性为外部——可在其他文件访问——代码块外部声明

单定义规则——即变量只能有一次定义

为此C++提供了两种变量声明:

  • 定义声明/定义——给变量分配存储空间
  • 引用声明/声明——不分配,仅引用——使用extern且不初始化
    如果要在多个文件中使用外部变量,只需在一个文件中包含定义,但在其他使用文件必须用关键字extern声明
  • 自动变量会隐藏全局变量——使用C++作用域解析运算符来访问
2.2.1.1 全局变量与局部变量的选择?

全局变量:易于访问但程序不可靠
程序越能避免对数据进行不必要的访问,就越能保持数据的完整性
故通常情况下应该使用局部变量,应在需要知晓时才传递数据

OOP数据隔离性

外部存储很适合常量数据(结合关键字const来防止数据被修改)

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

链接性为内部——只能在当前文件访问——代码块外部声明+static

场景:两个文件使用的相同的名称来表示其他变量,并且一起编译?-违反单定义原则
解决:静态变量隐藏常规外部变量

2.2.3 静态持续性、无链接性

无链接性的局部变量——只能在当前函数或代码块访问
代码块内部声明+static
特点:变量在该代码块不活跃状态仍然存在
如果初始化了静态局部变量,则程序只在启动时进行一次初始化,以后再调用函数时,不会像自动变量那样再次被初始化

2.3 说明符与限定符

2.3.1 存储说明符

  • auto :以前在声明中指出变量为自动变量,现在用于自动类型推断
  • register:以前指示寄存器存储,现在显示指出变量自动
  • static:见上面讲解
  • extern:表示引用声明
  • thread_load(C++11新增): 指出变量持续性与所属线程持续性相同
  • mutable: 用来指出即使结构(或类)变量为const,其某个成员也可以被修改

2.3.2 cv-限定符

  • const:表明内存被初始化后,程序不能再对它进行修改
  • volatile:表明即使程序代码没有对内存单元进行修改,其值也可能发生变化
    目的是为了改善编译器的优化能力(假设编译器发现程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中,这种优化假设变量的两次使用之间不会变化)——如果不将变量声明为volatile,则编译器将进行这种优化
2.3.2.1 const对默认存储类型的影响?

默认情况下全局变量的链接性为外部的,但const全局变量的链接性为内部的
意味着:每个文件都有自己的一组常量,而不是所有文件共享一组常量,故可以将常量定义放在头文件中
如果程序员希望某个常量的链接性为外部,使用extern关键字来覆盖默认的内部链接性:

extern const int states = 50;  
  • 必须在所有使用该常量的文件用extern关键字来声明它
  • 不过单个const在多个文件之间共享,因此只有一个文件可以对其进行初始化

2.4 函数和链接性

  • 所有函数的存储连续性都自动为静态的(不允许在也给函数中定义另外一个函数)
  • 默认情况下,函数链接性为外部
  • 可以使用static将函数链接性置为内部——必须同时在原型和函数定义中使用static
  • 在定义静态函数的文件中,静态函数将覆盖外部定义
  • 非内联函数也适用单定义规则

2.5 语言链接性

链接程序要求每个不同的函数都有不同的符号名

  • C语言中:一个名称只对应一个函数,C语言编译器可能将spiff这样的函数名翻译为-spiff,这种方法即C语言链接性
  • C++中:同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称,故C++编译器要执行名称矫正或名称修饰,为重载函数生成不同的符号名称,这种方法即C++语言链接

问题:在C++程序中使用C库预编译的函数
解决:使用函数原型来指出要使用的约定

extern "C" void spiff(int);
extern void spiff(int);
extern "C++" void spaff(int)

2.6 动态分配的存储方案

动态内存由运算符new 与 delete 控制,而不是由作用域与链接性规则控制
其分配与释放顺序取决于它们在何时以何种方式被使用
虽然存储方案概念不适用于动态内存,但适用于用来跟踪动态内存的自动和静态指针变量

2.6.1使用new运算符初始化

C++98: 在内置标量类型名后面加上初始值,并将其用括号括起
C++11: 常规结构或变量,使用大括号的列表初始化,也可以用于单值变量

 int *pi = new int (6);
 int *pt = new int {7};
 struct where { double x; double y; double z; };
 where * one = new where {2.5, 5.3 ,7.2}; 

2.6.2 new失败时

引发异常std::bad_alloc

2.6.3 new:运算符、函数和替换函数

 void * operator new(std::size_t); 括号里是一个typedef
 void * operator new[](std::size_t);
 
 void * operator delete(std::size_t);
 void * operator delete[](std::size_t);

使用new\new[]时调用以上函数

运算符重载语法

这些函数都是可替换的

2.6.4 定位new运算符

通常,new负责在堆中找到一个足以满足要求的内存块
但new还有另一种变体,即定位new运算符,能指定要使用的位置

  • 工作原理:只是返回传递给它的地址,并将其强制抓u能换为void * , 以便能够赋给任何指针类型
  • 不可替换但可以重载,至少需要接受两个参数,其中一个总是std:size_t,指定了请求的字节数
  • 是否使用delete来释放内存:delete只能用于指向常规new运算符分配的堆内存
 int * pi = new int ;
 int * p2 = new(buffer) int ;
 int * p3 = new(buffer) int[40] ; invoke new(40*sizeof(int),buffer)

3.名称空间

3.1 传统

已有的名称空间属性

  • 声明区域 declaration region :可以在其中进行声明的区域
  • 潜在作用域 potential scope : 变量的潜在作用域从声明点开始,到其声明区域的结尾(变量必须定义后才能使用)
  • 作用域:变量对程序而言可见的范围(局部变量将隐藏在同一个文件声明的全局变量

3.2 新的名称空间特性

通过定义中新的声明区域来创建命名的名称空间

  • 提供一个声明名称的区域,一个名称空间的名称不会与另外一个名称空间的相同名称发生冲突
  • 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中
  • 默认情况下,名称空间中声明的名称的链接性为外部的(除非引用了 常量)
  • 名称空间是开放的,可以把名称加入到已有的名称空间中
  • 访问:通过作用域解析运算符::,未被装饰的名称称为未限定的名称,包含名称空间的名称称为限定的名称
 namespace Jack{
 	double pail;
 	void fetch;
 	int pal;
 	struct Well{
 		...
	 };
 }
 Jack::pail = 12.34;

3.2.1 using声明与using编译指令

用来简化对名称空间中名称的使用
1)using 声明——使特定的标识符可用,将特定的名称添加到它所属的声明区域中

  • 函数中的using声明
using 名称空间::成员名字;

如果名称已经在函数中声明了就不能再声明

使用时会覆盖同名的全局变量,访问同名全局变量需要要::

namespace Jill{
	...
	double fetch;
} 
double fetch;
int main(){
 	using Jill::fetch;
 	double fetch;  不能这么定义,前面已经有了
	cin>>fetch;
	cin>>::fetch;  指示全局变量 
 }
  • 函数外面的using声明,将名称添加到全局名称空间

2)using编译指令——使整个名称空间可用

using namespace 名称空间名;
  • 如果using编译指令导入一个已经在函数中声明的名称,则局部名称将隐藏名称空间名
  • 在函数中使用,其中的名称 在函数中可用
  • 在全局中使用,则全局可用

3)比较

  • 一般来说,使用using声明比using编译指令更安全
  • 导入特定的名称,如果发生冲突,编译器会浸膏
  • 名称空间的开放性意味着名称空间的名称可能分散在多个地方,难以准确知道添加了哪些名称

3.2.2 命名空间其他特性

  • 名称空间声明可以嵌套
  • 可以在名称空间中使用using 编译指令和using声明
  • using编译指令是可传递的
  • 可以给名称空间创建别名
namespace a = b::c;
  • 未命名的名称空间,提供了链接性为内部的静态变量的替代品
static int counts ;
等同于
namespace
{
int counts;
}

3.3 名称空间及其前途

使用名称空间的主旨——简化大型编程项目的管理工作

  • 使用在已命名的名称空间中声明的变量,而不是使用外部全局变量
  • 使用在已命名的名称空间中声明的变量,而不是使用静态全局变量
  • 如果开发了一个函数库或者类库,放在一个名称空间中
  • 仅将编译指令using作为一种将旧代码转换成使用名称空间的权益之计
  • 不要在头文件中使用using编译指令(会掩盖要让哪些名称可用)
  • 导入名称时,首选使用域作用解析符或using声明的方法
  • 对于using声明,首选将其作用域设置为局部而非全局
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值