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声明,首选将其作用域设置为局部而非全局