Abstract
私认为这一章没有什么特别的东西需要写 down,都是自分自身在平时工作过程中遇到过的内容。主要介绍了内存变量的类型和 namespace 这个特性。
- 变量内存管理方式是老生常谈的内容,不过我还是决定记录一下关键内容。
- namespace 如果从使用的角度来说很简单,这是个 C++ 源码层面的概念,对于汇编层面来说没有太大的改变。
1.内存模型
1.1 变量
1.1.1 变量类型
As it knows to all,变量的使用场景大致有这么几种,栈中的局部变量,全局的静态变量和堆中的动态变量。C++11 中还更新了一种线程变量,用于并行编程。
下面来为这 4 种使用场景来引入专有名词:
- 自动存储持续性:这个对应的是栈中的数据,包括函数中定义的变量,函数的参数等。这些变量是通过栈来进行管理的,而一个程序中栈的分配和释放(虽然没有这个显式的概念)都是编译器在生成二进制代码时就已经决定好了的。
- 静态存储持续性:包含全局变量和 static 修饰的其他变量,不要看到名字里面有 静态 就把它与 static 等价起来了,这里说的是存储类型。所以这个 静态 的意思是从程序开始到程序结束这个变量都会一直存在。
- 动态存储持续性:这个对应的是堆中的数据,通过 malloc 和 free 或 new 和 delete 进行管理 by 程序员。
- 线程存储持续性:一般来说同一个 进程 中的 线程 是共享存储空间的,而如今的多核处理器已经非常常见了,可以使用 thread_local 关键字来进行创建线程变量,使得 CPU 可以胜任并行任务。
1.1.2 链接性
这里的链接性是针对的 静态存储持续性 变量,主要讨论外部全局变量(extern)和静态全局变量(static)。
我 want 记录的 eastwest 就是下面的这个例子,这里有三种不同的变量定义,代表了两种链接性和两种默认声明情况。
- 首先来看默认情况(extern or static):
- C/C++ 中对于全局变量默认是采用 extern,即一个变量的链接性默认为外部,可以在其他文件中使用 extern 关键字 declaration 后使用,链接时,linker 会将它们视为一个变量,放入相同的存储空间。
- 对于 extern 关键字进行 声明 时需要注意,不要对变量进行初始化就 like 例子中的 def 变量,这样会转化为 definition 而不是 declaration。
- 默认情况中存在一种例外,就是 like 例子中的 num 变量,它被默认使用 static 进行修饰。这是由于其通过了限制符 const 进行修饰,C++ 这样做就可以把形如 num 的定义放到头文件中,被其他文件包含而不必担心在链接时出现 重定义。当然很多编译器在进行优化时会将这部分的内容直接进行替换为表达式的右值,而不是开辟一块空间去存储,同じ 宏 一样。
int good; //same as "extern int good"
extern int decal; //a declaration of variable, its defination from other files
extern int def = 1; //a defination of variable.
static int very;
const int num = 1; //same as “static const int num = 1”
int main (){
...
}
- 再来看两种链接性
- 使用 extern 关键字进行修饰的变量为 外部链接性,可以与其他文件进行共享。在 COFF 文件(.obj)的符号表中的其 StorageClass 成员被设置为了 2。
- 使用 static 关键字进行修饰的变量为 静态链接性,只能被文件内部的函数进行访问,在 COFF 文件(.obj)的符号表中的其 StorageClass 成员被设置为了 3。
1.1.3 静态局部变量
下面的例子中有两个静态变量,一个是 Global 在函数之外进行的定义,一个是 Local 在函数内部进行的定义。将从几个层面来区分它们:
- 存储位置:存储位置是没有什么疑问的,它们都是 静态存储连续性 的变量,都会被程序放置在 .data or .rdata 中存储。当然这里的没有 const 修饰,会被放入到 .data 节中。
- 使用范围:静态全局变量 nGlobalStatic 可以在整个文件中被使用;while 静态局部变量 nLocalStatic 只能在定义它的函数中使用。
- 初始化:它们 都 只会被 初始化一次,only once。尽管函数每次都是从头开始运行,似乎 static int nLocalStatic = 1;这条语句每次都会被执行,但是编译器在生成代码时会将这部分设置为 if-else 模块保证其只被 初始化一次。
static int nGlobalStatic = 0;
int main(){
static int nLocalStatic = 0;
return 0;
}
1.2 限制符
1.2.1 cv限制符
cv 限制符其实就是两个关键字的缩写——const 和 volatile。对于 const 私很熟悉,不进行相关记录了。主要记录有关 volatile 的相关使用信息。
volatile 关键字表明即使程序 没有对于内存单元进行修改,其值也 可能发生改变。下面一个例子说明的这个关键字的作用,
- 当没有使用 volatile 对 a 进行修饰时,编译器会认为内存中的值没有发生改变,不需要重新读入寄存器中。
- 当使用 volatile 对 a 进行修饰时,编译器再每次使用前,都会重新将内存中的内容读入。
int main() {
int a = 1; //volatile int a = 1;
a++;
return a;
}
; don't use the volatile ; use the volatile
_main: _main:
push ebp push ebp
mov ebp, esp mov ebp, esp
sub esp, 4 sub esp, 4
lea eax, [esp] lea eax, [esp]
mov eax, 1 mov eax, 1
inc eax inc eax
mov eax, [esp]
mov esp, ebp mov esp, ebp
pop ebp pop ebp
ret ret
volatile 限制符的主要使用场景:
- 硬件相关的内存。
- 存在相互影响的两个程序之间的共享数据。
1.2.2 mutable
mutable 的作用是表明即使某个 struct or class 被 const 修饰其中的某个成员也是可以被修改的。例如:
struct data {
char name[30];
mutable int access;
};
const data veep = {"goodvery", 0};
strcpy(veep.name, "nothing"); //not allowed
veep.access = 1; //allowed
1.3 函数
1.3.1 comdat 属性
存在这样的一种场景,in which 一个函数被多个文件同时包含,链接时会出现重定义。这种情况多发生在使用 C 的标准库中,我们就拿 printf 作为说明。
为了解决这个在链接中可能会出现的问题,编译器会采取如下对策,将 printf 放入到一个单独的节中,并设置为 comdat 属性。当链接时遇到多个 同じ名称 时会根据策略选择其中的一个作为链接的目标,而忽略到其他的。这样就解决了链接中的问题。
1.3.2 C语言链接性
这里我 want 写下的东西只有一点对于函数 printf 在链接时的名称极有可能是 _printf。Like MessageBoxA 这类函数它在链接时的名称可能是_MessageBoxA@4。
1.3.3 C++语言链接性
C++链接的名称就比较复杂了,在 MSVC 编译器中叫作 Decorated Name ,而在 g++ 和 clang 编译器中则称为 Name Mangling。
- 如果需要获取名称粉碎,最好的方式就是写一个函数,然后通过命令行生成一个 .obj 文件,通过 Hex 工具查看即可。
- MSVC 的名称粉碎可以使用其提供的 undname 命令。
- 如果 需要在C++ 使用 C 的链接名称,可以使用 extern “C” 在函数前进行修饰。
下面的代码中就是一个实例,
namespace FW {
// declaration a function in a namespace
int good();
}
// use the C language link name
extern "C" void fishwheel_goodvery();
int main() {
FW::good();
fishwheel_goodvery();
return 0;
}
在 MSVC 控制台中输入如下命令来进行编译,得到 .obj 文件。通过 010 editor 打开之后就可以得到下面的内容,可以看到 C++ 链接的名称与 C 是截然不同的。
cl /c test.cpp
将之前得到的 ?good@FW@@YAHXZ 使用 undname 命令进行解析就可以得到原始的名称。
undname ?good@FW@@YAHXZ
1.4 new 运算符
这里我 want 记录下的内容是 new 的申请变量后的初始化方式,以及定位 new 运算符。
1.4.1 new 的初始化方式
// directly initialize
int *pi = new int(6); //set *pi = 6
double* pd = new double(1.0); //set *pd = 1.0
// use the form to initialize
struct where {double x; double y; double z;};
where* one = new where{1., 2., 0.5};
1.4.2 定位 new 运算符
new 在进行分配内存时可以指定从一块区域中进行分配,而不是从 堆 中分配,这样内存空间的管理就从 堆 转移到了 程序员。这样分配的内存空间是不需要使用 delete 进行释放的,从例子中可以看出分配是全局数据中的空间,这片空间是静态存在的。
char cBuf[512];
int main() {
// use the heap memory
double* pd1 = new double[3];
// use the memory hold by cBuf
double* pd2 = new(cBuf) double[3];
// before return heap memory should be release, while the located new don't need
delete[] pd1;
// it's error to delete the pd2, which are alloc by located new
//delete[] pd2;
return 0;
}
是不是感觉这样分配一块本来就可以使用的内存 十分的愚蠢,没错我也是这么 think 的。不过书中对于这种使用方式的场景是这样描述的,可以与初始化结合使用,从而实现将信息放入特定的硬件地址处。
2.命名空间 namespace
这部内容我感觉没有什么好 write down 的,从汇编层面来说没有与之对应的东西,命名空间的结果就是没有外部链接的静态数据名称不同。主要还是一些使用方式。
2.1 全局名称空间(global namespace)
我 only want 记录的就是 like ::good 这样,可以使用域名解析符号 :: 来使用全局变量,即使在有同名的局部变量的情况下。
int good = 0;
int main() {
int good;
// use local variable
good = 0;
// use global variable
::good = 0;
return 0;
}
2.2 using 声明和using 编译指令
using 声明使一个名称可用;而 using namespace 使所有名称都可用。
#include <iostream>
void test() {
int a;
// after use the using namespace all the name access by the name without region
using namespace std
cin >> a;
cout << 1;
}
int main() {
int a;
// use the using the use the std::cout
using std::cout;
cout << 1;
// there must be std::cin rather than cin
std::cin >> a;
return 0;
}
可以使用 namespace 关键字给某个域名起 alias。
namespace goodvery {
int a;
}
namespace gv = goodvery;
省略名称空间的名称可以创建未命名的名称空间。不能在未命名名称空间所属文件之外的文件中使用其中的名称,可以成为 内部静态变量 的替代品
static int counts; //static storage, internal linkage
int main() {
...
}
namespace {
int counts; //static sotrage, internal linkage
}
int main() {
...
}