sunwengang blog

C++头文件、作用域、内存模型和名称空间

字数统计: 3.1k阅读时长: 10 min
2019/09/01 Share

单独编译

C++提供#include语法,因而可以将程序划分,大致可以分成三部分:

  1. 头文件a.h:包含结构声明和使用这些结构的函数原型
  2. 源代码文件a.cpp:包含与结构相关的函数的代码
  3. 源代码文件b.cpp:包含调用与结构相关的函数的代码

头文件内容和引用

头文件常包含的内容:

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

头文件引用时,使用#include "coordin.h",而不是#include <coordin.h>
因为尖括号的文件名,C++编译器将在存储标准头文件的主机系统的文件系统中查找;而双引号的头文件,则编译器将首先查找当前的工作目录或源代码目录(或其他目录,取决于编译器)

头文件管理

在同一个文件中只能将同一个头文件包含一次。有时候会存在包含了另一个头文件的头文件。因而有一种方法基于预处理器编译指令#ifndef即if not defined可以忽略第一次包含之外的所有内容,但是这种方法不能防止编译器将文件包含两次。。

1
2
3
4
5
//仅当以前没有使用预处理器编译指令#define定义名称COORDIN_H_时,才处理#inndef和#endif之间的语句
#ifndef COORDIN_H_
#define COORDIN_H_ //完成该名称的定义
...
#endif

作用域scope

作用域描述名称在文件的多大范围可见。
C++变量的作用域有多种。

  • 局部变量只在定义他的代码块中可用。
  • 作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。
  • 自动变量的作用域为局部
  • 静态变量的作用域是全局还是局部取决于它如何被定义
  • 在函数原型作用域中使用的名称只包含参数列表的括号内可用
  • 在类中声明的成员的作用域为整个类
  • 在名称空间中声明的变量的作用域是整个名称空间
  • C++函数的作用域可以是整个类或者命名空间,但不能是局部的,不能只对自己可见,这样会导致不能被其他函数调用

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

数据内存的存储方式(存储持续性)

C++使用三种方案存储数据,见文章《C++内存分配方式和模板类vector,array》
C++11新增了线程存储持续性

线程存储持续性

在多核处理器很常见,这些CPU可以同时处理多个执行任务(可以使用SDK的systrace工具抓取一份trace查看)。这让程序能够将计算放在可并行处理的不同线程中。如果变量是使用关键字thread_local声明的,则其生命周期和所属的线程一样长。

自动存储持续性

默认情况下,在函数中声明的函数参数和变量的存储持续性是自动,作用域是局部,没有链接性。当函数结束时,这些变量将小时(执行到代码块时,将为变量分配内存,但是其作用域的起点是其声明位置)

如果在代码块中定义了变量,则该变量的存在时间和作用域将被限制在该代码块内。

通常存储在栈stack中,先进后出。之所以被称为栈,是由于新数据被象征性的放在原有数据的上面(相邻的内存单元),当程序使用完后,将其从栈中删除(栈的默认长度取决于实现,编译器通常提供改变栈长度的选项)。程序使用两个指针跟踪栈,一个指向栈底(开始位置),一个指向栈顶(下一个可用内存单元)。当函数被调用时,其自动变量被加入栈中,栈顶指针指向变量后的下一个可用内存单元。函数结束时,栈顶指针被重置为函数被调用前的值,从而释放新变量使用的内存。

有些自动变量存储在寄存器中,关键字register最初由C语言引入,C++11之前,它建议编译器使用CPU寄存器存储自动变量。这旨在提高访问变量的速度。
register int count_fast;

在C++11中,失去了这种提示作用,关键字register只是显式的指出变量是自动的。鉴于它只能用于原本就是自动的变量,使用他的唯一原因是指出这个变量的名称可能与外部变量相同,避免使用了该关键字的现有代码非法。

静态持续变量

C++未静态存储持续性变量提供三种链接性:

  1. 外部链接性(可在其他文件中访问)
  2. 内部链接性(只能在当前文件中访问)
  3. 无链接性(只能在当前函数或者代码块中访问)
1
2
3
4
5
6
7
8
9
10
11
12
...
int global = 1000; //外部链接,作用域是整个文件
static int one_file = 50; //内部链接,作用域是整个文件
int main(){
...
}

void fun1(int n) {
static int count = 0; //无链接
int ma = 0; //自动变量,两者区别在于静态无链接变量在函数没有被执行时,也会在内存中
...
}
存储描述 持续性 作用域 链接性 声明方法
自动 自动 代码块 代码块中
寄存器 自动 代码块 代码块中,使用关键字register
静态,无链接性 静态 代码块 使用关键字’static’
静态,外部链接性 静态 文件 外部 不再任何函数内
静态,内部链接性 静态 文件 内部 不再任何函数内,使用关键字static

所有静态持续变量的初始化特征:未被出的初始化的静态变量的所在位都被设置为0,这种变量被称为零初始化的

零初始化和常量表达式初始化被统称为静态初始化,这意味着在编译器处理文件(翻译单元)时初始化变量。动态初始化意味着变量将在编译后初始化。

1
2
int x;   //零初始化
int y = 5; //y先被零初始化,然后编译器计算常量表达式,初始化成5

静态持续性、外部链接性

链接性为外部的变量通常简称为外部变量,存储持续性是静态,作用域是整个文件。在函数外部定义,因此对所有函数都是外部的。例如可以在main()前面或者头文件中定义他们。可以在文件中位于外部变量定义后面的任何函数中使用它,因此也被称为全局变量(相对于局部的自动变量)

C++有“单定义规则”,每个变量只能有一次定义。因而C++提供两种变量声明,一种是定义声明,分配存储空间;另一种是引用声明,不给变量分配存储空间,引用已有的变量。

引用声明使用关键字extern,则不进行初始化。

1
2
3
double up;  //零初始化
extern int blem;; //引用声明,不进行初始化(此处可以引用其他文件已经声明的外部变量)
extern char gr = 'z'; //已经初始化了,所以导致分配存储空间

静态持续性、内部链接性

不同于外部变量,将static限定符用于作用域为整个文件的变量时,该变量的链接性是内部的。两者区别是 内部链接的变量只能在所属的文件中使用,外部变量具有外部链接性,可以在其他文件中使用。

静态持续性、无链接性

将static限定符用于在代码块中定义的变量,导致局部变量的存储持续性是静态的。这意味着虽然只能在该代码块使用,但是在该代码块不处于活动状态时仍然存在。因此在两次调用该函数时,静态局部变量的值将保持不变。此外如果初始化了静态局部变量,则程序只在启动时进行一次初始化,再次调用时不会初始化,即值保持上次的值


限定符和说明符

存储说明符

  • auto(在C++11不再是说明符)
  • register
  • static
  • extern
  • thread_local
  • mutable
  1. 同一个声明中不能使用多个说明符(除了thread_local可以和static或者extern结合使用)
  2. C++11之前auto指出变量是自动变量,但是在C++11中,auto用于自动类型推断
  3. register用于在声明中指示寄存器存储,在C++11中只是显式的指出变量是自动的
  4. static被用在作用域是整个文件的声明中时,表示内部链接性;被用于局部声明中,表示局部变量的存储持续性是静态的
  5. extern表明是引用声明,即声明引用在其他地方定义的变量
  6. thread_local指出变量的持续性和所属线程的持续性相同。thread_local变量之于线程,犹如常规静态变量之于整个程序。
  7. mutable的含义根据const来解释(查看下一小节

cv-限定符(const和volatile)

  • const
  • volatile
  1. const表示内存被初始化后,程序不能再对其进行修改
  2. volatile表明即使程序代码没有对内存单元进行修改,其值也可能发生变化。作用是为了改善编译器的优化能力。例如,如果编译器发现程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到既存区中。(这种优化假设变量的值在两次使用之间不发生变化)将变量声明为volatile,则编译器不会进行这种优化;否则将进行这种优化。
  3. 可以使用mutable指出,即使结构(或者类)变量为const,其某个成员也可以被修改。
1
2
3
4
5
6
7
8
9
struct data {
char name[30];
mutable int accesses;
...
};

const data veep = {"peter", 0, ...};
+ strcpy(veep.name, "nancy"); //not allowed,因为const
- veep.accesses++; //allow

veep的const限定符禁止程序修改veep的成员,但access成员的mutable说明符可以使的access不受这种限制。


名称空间

  1. 声明区域是可以在其中进行声明的区域。例如函数外面声明全局变量,其声明区域为所在的文件;
    函数中声明变量,声明区域为所在代码块;
  2. 潜在作用域:变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明作用域小,这是由于变量必须定义后才能使用。
  3. 关键字namespace通过定义一种新的声明区域来创建命名的空间,如此不会和另一个名称空间发生同名冲突。
1
2
3
4
5
6
7
8
9
10
11
namespace Javk {
double pail;
int pal;
...
}

namespace Jil {
double pail;
int pal;
...
}

作用域解析运算符::双冒号

1
2
3
Jil::pal = 3;   //限定的名称
int pal = 10; //未限定的名称
Javk::pail = 23.4;

using声明恶化using编译指令

using声明使得特定的标识符可用,using编译指令使得整个命名空间可用。

1
2
3
4
5
6
7
8
namespace Jil {
...
}

int main() {
using Jil::pail; //using声明
double a;
}

using编译指令由名称空间和关键字using namespace组成。

1
2
using namespace Jil;
using namespace std;

建议

  • 不要在头文件使用using编译指令,如果非要使用,应将其放在所有的预处理编译指令#include后面
  • 导入名称时,首选使用作用域解析运算符或者using声明的方法
  • 对于using声明,首选将其作用域设置为局部而不是全局
CATALOG
  1. 1. 单独编译
    1. 1.1. 头文件内容和引用
    2. 1.2. 头文件管理
  2. 2. 作用域scope
  3. 3. 数据内存的存储方式(存储持续性)
    1. 3.1. 线程存储持续性
    2. 3.2. 自动存储持续性
    3. 3.3. 静态持续变量
      1. 3.3.1. 静态持续性、外部链接性
      2. 3.3.2. 静态持续性、内部链接性
      3. 3.3.3. 静态持续性、无链接性
  4. 4. 限定符和说明符
    1. 4.1. 存储说明符
    2. 4.2. cv-限定符(const和volatile)
  5. 5. 名称空间
    1. 5.1. 作用域解析运算符::双冒号
    2. 5.2. using声明恶化using编译指令
    3. 5.3. 建议