3. C++存储方式(存储持续性、作用域和链接性)与名称空间


变量的存储方式对函数、模板、类等同样有意义。

翻译单元(translation unit):According to standard C++ (wayback machine link) : A translation unit is the basic unit of compilation in C++. It consists of the contents of a single source file, plus the contents of any header files directly or indirectly included by it, minus those lines that were ignored using conditional preprocessing statements.

A single translation unit can be compiled into an object file, library, or executable program.

The notion of a translation unit is most often mentioned in the contexts of the One Definition Rule, and templates.

在同一个翻译单元中的内容可视为是同一个文件中的。

3.1 存储持续性

  • 自动存储持续性:在函数中声明的变量的存储持续性为自动的。它们占用的内存会在执行完函数或代码块时被自动释放。C++中有两种存储持续性为自动的变量。

  • 静态存储持续性:在函数外定义或使用关键字static定义的变量的存储持续性都为静态的。它们在整个程序运行过程中都存在。C++中有三种存储持续性为静态的函数。

  • 动态存储持续性:使用new运算符分配的内存将一直存在,直到使用delete运算符将内存释放或程序结束为止。这种内存的存储持续性为动态,有时被称为自由存储(free store)或堆(heap)。

  • 线程存储持续性(C++):使用关键字thread_local生命的变量。

3.1.1 自动存储持续性

  • 自动变量与栈

C++中的自动变量储存在栈中。程序使用一个指向栈顶(最后添加的变量)和一个指向栈底(第一个变量)的指针来跟踪栈。
栈是一种LIFO(后入先出)的数据结构。在释放变量时并不会写入数据,而仅仅只是向下移动指针。

传统的K&R C不允许自动初始化自动存储的数组与结构,只允许初始化静态结构与变量。

  • 寄存器变量(register关键字)

在C语言中,register表示建议使用CPU寄存器来储存自动变量,以提高访问变量的速度。
在早期的C++,随着编译器与硬件越来越复杂,register表示提示某一自动变量使用频繁,编译器可以对其进行特殊处理。
到了C++11,register只是显式地指出变量是自动的(这个含义与C语言和早期的C++中的auto一样)。保留register的原因只是避免使用了register的早期代码非法。

3.1.2 静态存储持续性

静态存储变量有三种链接性,取决于变量的声明位置。

  • 外部链接性:名称在其他文件中可见。
  • 内部链接性:名称只在当前文件中可见。
  • 无连接性:名称只在当前代码块中可见。

静态变量的数目在程序运行过程中是不变的,所以不需要特殊的装置(如栈)来管理它们。编译器直接分配固定的内存块来储存静态变量。
另外,如果没有显式地初始化静态变量(包括数组和结构),编译器会将所有数据位都设置为0。

在全局作用域(不在任何函数和代码块中)声明的变量默认为内部链接性的静态变量。
要声明为外部链接性的静态变量,要在在全局作用域中声明的同时在声明前加上关键字static

以下为三种静态变量的声明示例。

int global = 1000;    // 静态变量, 内部链接性
static int one_file = 500;    // 静态变量, 外部链接性

int main()
{
    // ...
}

void func(int n)
{
    static const int count = 0;    // 静态变量, 无链接性
}

静态变量的初始化

  • 静态初始化:在编译期间就给变量分配内存。
    • 零初始化:见上文。
    • 常量表达式初始化:声明静态变量时用常量的表达式给给静态变量赋值。
  • 动态初始化:用非常量的表达式初始化,在程序运行期间进行。
3.1.2.1 静态存储持续性&外部链接性,extern关键字

C++中提供了两种变量声明。

  1. 定义声明(简称定义):在声明时给变量分配存储空间。
  2. 引用声明:引用已有的变量,不给变量分配存储空间。

若要使用在其他文件中的变量,需要在本文件的全局作用域中使用引用声明。

// file1.cpp
#include <iostream>

extern int i = 10;    // 这里的extern关键字不是必须的

int main()
{
    // ...
    return 0;
}
#include <iostream>

extern int i;    // 引用声明

int find(const char* str, const char ch)
{
    // ...
    return pos;
}

3.1.2.2 静态存储持续性&内部链接性,static关键字

在要被链接(link)在一起的文件中简单地声明同名变量违反单定义规则(One Definition Rule,ODR)。会导致编译错误。
使用static关键字可以指明变量的链接性为内部,而非要提供全局定义。

// file1.cpp
#include <iostream>

extern int i = 10;

int main()
{
    // ...
    return 0;
}
#include <iostream>

// int i = 50;    // error
static int i;    // valid

int find(const char* str, const char ch)
{
    // ...
    return pos;
}

3.1.2.3 静态存储持续性&内部链接性

3.1.3 动态存储持续性(new&delete, new[]&delete[])

  • 使用new/new[]运算符初始化

可以配合使用圆括号初始化单值变量,可以用花括号(C++11)初始化结构或数组及单值变量

// 圆括号
int* pi = new int(6);
// 花括号
int* arri = new int[4]{12, 124, 534, 653};
position* pos = new position{3, 4};
char* ch = new char{'c'};
  • new/new[]失败时

在最初的10年中,C++在这时返回空指针,但现在,将引发异常std::bad_alloc

  • 定位new(placement new)运算符

要使用定位new运算符,要包含头文件<new>。可以指定新分配内存的地址。
定位运算符的工作原理大致上只是将传递给它的地址转换为void*类型并返回。

char buffer[50];
int* arri = new(buffer) int[20];

C++不会追踪已分配的地址,所以可以在释放内存前再次分配已被分配的内存,不过这样做会覆盖原来的数据。如果再次分配时覆盖了已被分配的内存,那么在释放内存时就会重复释放内存,这将导致错误。

3.1.3.1 类与动态内存分配

如果使用定位new运算符将类的对象(数组)“放在”动态分配的内存上,有两点要注意。

  1. 不要用delete([])释放类的对象(数组),因为定位new运算符只是将指针的值强制转换为void*,并没有分配内存。
  2. 如果这个类中使用了动态内存分配,要先显式地调用析构函数(其中有delete([])语句)。

3.2 作用域与链接性

  • 作用域描述了名称在文件或翻译单元中的多大范围内可见。按作用域可将变量分为全局变量局部变量

    • 全局变量:在所有函数和代码块外定义的变量,也称外部变量
    • 局部变量:在函数定义或代码块中(包括函数定义或代码块前的括号)定义的变量。
    • 函数原型作用域(function prototype scope):函数声明中的名称只在包含参数列表的括号内可用。
  • 链接性

    • 内部链接性:从定义的位置到定义所在的文件的结尾。
    • 外部链接性:从定义的位置到文件的结尾。
    • 无链接性:只能在声明的代码块中使用,例如局部变量。
  • 存储持续性与链接性

    • 自动变量的链接性为局部。
    • 静态变量的作用域是全局还是局部取决于它是怎样被定义的。

C++中的函数的作用域可以是整个类或整个名称空间(包括全局作用域,全局作用域是名称空间作用域的特例),但不能是局部的。因为函数不能在代码块中定义函数(内联函数除外,内联函数是依靠预编译替换代码实现的,某种意义上不是函数而更像是宏)。

3.2.1 局部变量的"覆盖作用"

局部变量会覆盖同名的有链接性的变量(包括内部链接性和外部链接性)。
C++提供了作用域解析运算符::,能做到访问被覆盖的这些变量。

#include <iostream>
using namespace std;

const char* str = "extern";    // 外部链接性
// const stetic char* str = "extern";    // 内部链接性

int main()
{
    const char* str = "main()";
    const char* str1 = "main()";
    {
        const char* str = "block in main()";
        cout << "-- in block in main() --" << endl;
        cout << "str:\t" << str << endl;
        cout << "::str:\t" << ::str << endl;
    }
    cout << "------ in main() ------" << endl;
    cout << "str:\t" << str << endl;
    cout << "::str:\t" << ::str << endl;
    return 0;
}

输出:

-- in block in main() --
str:    block in main()
::str:  extern
------ in main() ------
str:    main()
::str:  extern

3.3 说明符与限定符

3.3.1 存储说明符

  • auto(C++11中不再是说明符)
  • register
  • static
  • extern
  • thread_local(C++11中新增)
  • mutable

在同一声明中不可以使用多个说明符,但thread_local除外,它可以与staticextern一起使用。
thread_local指出变量的生命周期与其所在的线程一样长。thread_local之于线程,就和常规静态变量之于整个程序。

mutable指出即使在const对象中,指定变量的值也是可以修改的。

3.3.2 cv-限定符

  • const
  • volatile

volatile指出即使程序代码不会修改变量的值,变量的值仍然可能改变(如其他程序或硬件就可能会修改某些变量的值)。
该关键字的作用旨在避免编译器做出错误的优化。例如,程序在几条语句中多次使用了某个变量的值,而且其中没有改变变量的值的代码。那么编译器就可能会将变量的值储存在寄存器中。在之后的使用中不会再此查找变量的值,而是直接使用寄存器中的值。

在C++中(但在C中不是),const限定符对变量的存储方式会造成影响。const全局变量的默认链接性为内部的
若要定义链接性为外部的conat变量,需要使用extern关键字。

// 以下两行代码等效,都是链接性为内部的const变量
const int i = 10;
static const int i = 10;
// 链接性为外部的conat变量
extern const int i = 10;

3.4 语言链接性

由于加入了函数重载,C++编译器在对函数执行名称修饰和名称矫正时生成的符号名称会包含函数的参数信息,而C语言中不会。
这两种特殊的链接性分别称为C++语言链接性C语言链接性

由于符号名称不同,C和C++之间在使用对方预先编译好的库等情况时会出现错误。
为解决这个问题可以在函数原型使用形如extern "C"的说明符来指定要使用的约定。

extern "C" void spiff(int);    // C语言链接性, 可能将spiff(int)转换为_spiff。
extern void spiff(int);        // C++语言链接性, 可能将spiff(int)转换为_spiff_i。另外,extern是可选的。
extern "C++" void spiff(int);  // 与第二句等效

C和C++语言链接性说明符是C++标准规定的,但具体实现可以有其他语言链接性说明符。

3.5 名称空间

声明区域:声明区域值变量声明所在的区域,包括声明语句前的区域。声明区域可以是某个代码块,也可以是整个文件。
潜在作用域:从声明语句开始,直到声明区域的终点。
作用域:变量对程序而言可见的地方,范围是潜在作用域除去被嵌套代码块中的同名变量隐藏的区域。

每个名称空间都对应一个声明区域。C++标准提供了名称空间工具来通过定义一个新的声明区域来创建命名的名称空间。

3.5.1 创建名称空间

3.5.1.1 创建语法

名称空间可以位于另一个名称空间中,但是不能位于代码块中。
除了用户定义的名称空间,还有全局名称空间,它对应文件级声明区域。
在全局名称空间外的名称空间内创建名称空间称为嵌套式名称空间

//jack_jill.h

namespace Jack{
    char* cake;
    float length;
    struct Student Tom;
    string object;
    int find(const char*, conat char);
}

namespace Jill{
    static int cake;
    double length;
    struct Student Tom;
    string object;
    char* str;
}
3.5.1.3 名称空间的开放性(open)

名称空间是开放(open)的,这意味着可以将新的名称添加到已有的名称空间中。

// 以下代码将student结构体的声明添加到名称空间Jill中
namespace Jill{
    struct student{
        string fullname;
        int id;
        int grade_num;
        int class_unm
    };
}

3.5.2 使用名称空间

3.5.2.1 作用域解析运算符 ::
#include "jack_jill.h"

char* str = "Still water runs deep";

Jack::length = 10.0;
Jack::find(str, 'u');
Jill::cake = 10;
Jill::length = 12.0;
3.5.2.2 using声明与using编译指令

using声明会引入特定的名称,using编译指令则会引入整个名称空间。
using声明与using编译指令都是可以在代码块或全局名称空间中使用,将名称导入到特定区域。

区别:using声明会与同名的变量冲突,而using编译指令不会。
目标作用域中的名称会隐藏用using编译指令导入的相同的名称。不过可以用作用域解析运算符解决这一问题。

一般来说,using编译指令比using声明要危险。

  • 由于名称空间的开放性,难以确定名称空间有哪些变量。
  • 当有同名变量时,编译器不会报错,可能会因此产生难以察觉的错误。

在名称空间中也可以使用using声明和using编译指令

3.5.2.3 嵌套式名称空间
namespace Jack{
    namespace myth{
        int n;
        }
}
#include "jack_jill.h"
...

Jack::myth::n;

...

《C++ Primer Plus》第六版中指出using编译指令是可传递的(using更内层的名称空间后会将其外层名称空间也导入),但在我测试时貌似并不是这样。可能之后C++标准又有了变化,也可能是我理解有误。

3.5.2.4 名称空间的别名

使用类似赋值语句的语句可以为名称空间设定别名。

// 假设存在名为my_very_favorite_things的名称空间,下面的语句为其设定了别名。
namespace mvft = my_very_favorite_things;
3.5.2.5 未命名的名称空间

创建没有名称的名称空间会像是在后面跟着using编译指令一样。这样来看,这就像是声明了全局变量。但是由于没有名称不能再其他文件中使用,因此该名称空间中的名称是内部链接性的。这提供了内部链接性的静态变量的替代品。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值