C++内存模型与名称空间概念讲解
青山渺渺 人气:0程序可分为三部分:
头文件:包含结构声明和使用这些结构的函数的原型
源代码文件:包含与结构有关的函数的代码
源代码文件:包含调用与结构相关的函数代码。
头文件中常包含的内容:
函数原型、使用#define或const定义的符号常量、结构声明、类声明、模板声明、内联函数。
文件在编译时可以解释为翻译单元。
1、存储持续性与作用域及链接性
存储类别如何影响信息在文件间的共享呢?C++使用三种不同的方案来存储数据,这些方案的区别在于数据保留在内存中的时间:
自动存储特性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的,他们在程序开始执行其所属的函数或代码块时被创建,在执行完函数或代码块时,他们使用的内存被释放
静态存储特性:在函数定义外定义的变量和使用关键字static定义的变量的存储持续性都为静态,他们在程序整个运行过程中都存在
线程存储持续性:如果变量使用关键字thread_local声明,其声明周期和所属线程一样长
动态存储特性:用new运算符分配的内存将一直存在,直到使用delete运算符释放或者程序结束。
2、作用域和链接
作用域描述了名称在文件(翻译单元)的多大范围内可见。链接性描述了名称如何在不同单元间共享,链接性为外部的名称可在文件间共享,链接性为内部的名称只能由一个文件中的函数共享,自动变量的名称没有链接性,因为他们不能共享。
3、静态持续变量
静态存储持续性变量有三种链接性:外部、内部和无链接性,这三种链接性都在整个程序执行期间存在,它们的寿命更长,编译器将分配固定的内存。另外如果没有显式初始化静态变量,编译器将把它设置为0。
int global = 100; // 外部链接 static int one_file = 10; // 内部链接 extern double up = 0; // 外部链接 int func(int n) { static int cnt = 0; // 无链接,只在代码块内使用 return 0; }
4、静态持续性和外部链接性
链接性为外部的变量称为外部变量,他们的存储性为静态,作用域为整个文件。
C++提供有两种变量声明,一种是定义声明,它给变量分配存储空间;另一种是引用声明,它不给变量分配存储空间,而是引用已有的变量。
double d; // 定义 extern int a; // 引用声明 extern char c = 'a'; 定义声明
如果要在多个文件中使用外部变量,只需要在一个文件中包含该变量的定义,在使用该变量的其他所有文件中,都必须使用关键字extern声明它。
// a.h #pragma once int getGlobalNum() // a.cpp #include "a.h" extern int global; int getGlobalNum() { return global; } // b.cpp #include "a.h"; int global = 100; int main() { cout << getGlobalNum() << endl; // 100 getchar(); }
定义与全局变量同名的局部变量后,局部变量将隐藏全局变量。C++中提供了作用域解析运算符(::),放在变量名前面是,该运算符表示使用变量的全局版本。
// a.cpp #include "a.h" extern int global; int getGlobalNum() { int global = 10; return ::global; } // b.cpp #include "a.h"; int global = 100; int main() { cout << getGlobalNum() << endl; // 100 getchar(); }
5、静态持续性与内部链接性
将static限定符用于作用域为整个文件的变量时,该变量的链接性为内部的,链接性为内部的变量只能在其所属的文件中使用。
常规外部变量具有外部链接性,即可以在其他文件中使用,如果要在其他文件中使用相同的名称来表示其他变量,需要使用static。(如果在两个文件中有两个相同名称的外部变量,那么第三个文件引用时就不能确定引用哪一个)
// a.cpp extern int a = 10; // b.cpp extern int a = 30; // error static int a = 30; // OK
6、静态存储性与无链接性
在代码块中使用static时,将导致局部变量的存储持续性为静态的,该变量在代码块不在活动时仍存在。两次函数调用之间,静态局部变量的值将保持不变。初始化了静态局部变量,程序只在启动时进行一次初始化,以后再调用时将不会再被初始化。
void add2() { static int value = 0; cout << value++ << " "; // 0 1 2 } for (size_t i = 0; i < 3; i++) add2();
7、const
默认情况下全局变量的链接性为外部的,但是const全局变量的链接性为内部的,也就是说C++中全局const定义就像使用了static说明符一样。因为有该特性,const修饰的常量可以放在头文件中,并且可以在多个文件中使用该头文件(如果const声明是外部的,根据单定义规则将出错,即只有一个文件可以使用const声明,其他需要用extern来提供引用声明)。
extern const int a = 10; // 外部声明的常量 const int b = 10; // 内部声明的常量
如果希望某个常量的链接性是外部的,可以使用extern来覆盖默认的内部链接性。
8、函数和链接性
C++不允许在一个函数中定义另一个函数,所以所有的函数的存储持续性都自动为静态的,即整个程序执行期间都存在。
默认情况下,函数的链接性为外部的,可以使用extern来指出函数实在另一个文件中定义的(而不用去加头文件);
extern int pub();
可以使用static将函数的链接性设置为内部的,使之只能在一个文件中使用,必须同时在原型和定义中使用该关键字。
static int private(); static int private() { }
使用static修饰函数并不仅意味着该函数只在当前文件中可见,还意味着可以在其他文件中定义同名的函数。
// a.cpp int pub() { return 0; } // b.cpp int pub() // error { return 0; } static int pub() // OK { return 0; }
在定义静态函数的文件中,静态函数将覆盖外部定义。
对于每个非内联函数,程序只能包含一个定义(如果两个文件中包含相同名称的外部函数,那么第三个文件使用外部函数时将不能确定使用哪个定义)
内联函数不会受到单定义规则约束,所以可以放在头文件中,这样包含有头文件的每个文件都有内联函数的定义。C++要求同一个函数的所有内联定义都必须相同。
9、语言的链接性
链接程序要求每个不同的函数都有不同的符号名。C语言中一个名称只能对应一个函数,这很容易实现,C语言编译器可能将spiff翻译为_spiff。但是在C++中,同一个名称可能对应多个函数,必须将这些函数翻译为不同的符号名称,可能将spiff(int)翻译为_spiff_i,将spiff(double)翻译为_spiff_d。
链接程序寻找与C++函数调用匹配的函数时,使用的方法与C语言不同,要在C++程序中使用C库中预编译的函数可以在声明时指定链接性说明符,比如下面第一种指定用C语言的链接方式去链接spiff方法
extern "C" void spiff(int); // 使用C语言链接性 extern void spoff(int); // 默认使用C++链接性 extern "C++" void spaff(int); // 显式指定C++链接性
假设有一个C库libC,里面有一个函数spiff,如果我们在C++程序中直接引用头文件,并且调用函数,那么会出现找不到函数定义的情况。这时候我们可以用extern "C"将头文件包裹起来,表示用C语言的连接方式去链接方法:
extern "C" { #include "a.h" }
10、命名空间
一个命名空间中的名称不会与另一个命名空间中的相同名称发生冲突,命名空间可以是全局的,也可以位于另一个命名空间中,但不能位于代码中。默认情况下,命名空间中声明的名称的链接性为外部的。
除了用户定义的命名空间外,还有一个全局命名空间,它对应于文件及声明区域,因此前面所说的全局变量现在被描述为位于全局名称中间中。
namespace A { int a = 10; void printk() { } } namespace B { int a = 20; void printk() { } }
名称空间是开放的,可以把名称加入到已有的名称空间中:
namespace B { void printkk(); }
C++提供using声明和using编译两种机制来简化对名称空间中名称的使用:
using声明使特定的标识符可用,在函数外使用using声明,可以把名称添加到全局名称空间中
using B::printkk; int main() { using B::printk; printk(); }
using编译指令使整个名称空间可用,在全局声明区域中使用using编译指令,使得该名称空间中的名称全局可用:
using namespace B; int main() { // using namespace B; printk(); }
如果名称空间和声明区域定义了相同的名称,如果使用using声明来导入,则两个名称会发生冲突:
namespace B { int a = 20; } int main() { using B::a; // int a = 10; // error cout << a; getchar(); return 0; }
如果用using编译指令将名称空间的名称导入,则局部版本将隐藏名称空间版本:
namespace B { int a = 20; } int a = 100; int main() { using namespace B; int a = 10; cout << a << " " << ::a << " " << B::a << endl; // 10 100 20 }
一般来说,使用using声明比使用using编译指令更安全,如果名称和局部名称发生冲突,编译器将发出指示。using编译导入所有名称可能包括不需要的名称。
命名空间的声明可以进行嵌套:
namespace element { namespace fire { } } using namespace element::fire;
可以在名称空间中使用using编译指令和using声明:
namespace spaceA { int a = 10; } namespace spaceB { using namespace A; }
可以给命名空间创建别名,来简化嵌套命名空间的使用:
namespace spaceA { namespace B { int a = 10; } } namespace spaceX = spaceA::spaceB; spaceX::a = 100;
命名空间的使用指导原则:
使用在已命名的名称空间中声明的变量,而不是使用外部全局变量;
使用在已命名的名称空间中声明的变量,而不是使用静态全局变量;
如果开发了一个函数库或者一个类库,将其放在一个命名空间中;
不要在头文件中使用using编译指令,如果非要使用将其放在所有预处理指令之后;
导入名称时,首选使用作用域解析运算符或using声明的方法;
对于using声明,首选将其作用域设置为局部而不是全局。
加载全部内容