我们知道,全局变量时C++语言语法和语义中一个很重要的知识点,首先它的存在意义需要从三个不同角度去理解。
- 对于程序员来说,它是一个记录内容的变量(variable)
- 对于编译/链接器来说,它是一个需要解析的符号 (symbol)
- 对于计算机来说,它可能是具有地址的一块内存(memory)
其次从语法/语义上说:
从作用域上看:带static关键字的全局变量范围只能限定在文件里,否则会外联到整个模块和项目中。
从生存期看:即使不带static关键字,它的存储在静态全局区,因为它是静态的,并且贯穿整个程序或模块运行期间(Notice : 正是跨单元访问和持续生存周期这两个特点使得全局变量往往成为一段受攻击代码的突破口,这一点十分重要)
从空间分配上看:定义且初始化的全局变量在编译期,会在数据段(.data)上分配空间,定义但未初始化的全局变量(相当于仅仅是声明)暂存(tentative definition)在.bss段,编译时自动清零,而仅仅是声明的全局变量只能算个符号,寄存在编译器的符号表内,不会分配空间,直到链接或者运行时再冲定向到相应的地址上。
下面我们从 两个方面介绍:变量初始化问题
1:全局变量初始化带来的变量重复定义
2:变量初始值默认值问题
1:全局变量初始化带来的重复定义问题。
t.h
#pragma once
int a;
foo.cpp
#include<iostream>
#include<string>
#include "t.h"
using namespace std;
struct
{
int b1;
} b = {4};
void foo() {
cout << "全局变量 &a: " << &a
<< " ,&b: " << &b
<< " ,sizeof b: " << sizeof(b)
<< " ,struct b.b: " << b.b1 << endl;
}
main.cpp
#include<iostream>
#include<string>
#include "foo.cpp"
#include "t.h"
using namespace std;
int c;
int main()
{
foo();
cout << "全局变量 &a: " << &a
<< " ,c: " << c << endl;
}
很显然,
1:由于t.h 头文件中 声明且定义 int a (默认初始值为0),当 foo.cpp 和 main.cpp 同时引入 t.h头文件时,当main.o 和foo.o 两个文件链接时,就会发生 变量a 重复定义。
2:同样由于 foo.cpp 声明并定义 void foo() 函数,当main.cpp 引入了 foo.cpp 源文件,在main.o和foo.o 两个文件链接时,也会发生:函数重复定义。
修改方案
解决变量的重定义:可以引入 static 关键字,这个关键字的作用就是
1:全局变量范围只能限定在文件里,不会外联到整个模块和项目中。
2:变量只在源文件中有效。
解决函数重定义:引入 inline关键字修饰函数
1:inline 相当于在函数定义的地方,将函数展开,这样就可以避免函数符号在符号表中被多次找到。
// t.h
#pragma once
static int a;
// foo.cpp
#include<iostream>
#include<string>
#include "t.h"
using namespace std;
struct
{
int b1;
} b = {4};
inline void foo() {
cout << "global varial &a: " << &a << " ,a value: "<< a
<< " ,&b: " << &b
<< " ,sizeof b: " << sizeof(b)
<< " ,struct b.b: " << b.b1 << endl;
}
// main.cpp
#include<iostream>
#include<string>
#include "foo.cpp"
#include "t.h"
using namespace std;
int c;
int main()
{
foo();
cout << "global varial &a: " << &a << " ,a value: " << a
<< " ,c: " << c << endl;
}
// 打印结果
global varial &a: 0x407028 ,a value: 0 ,&b: 0x404004 ,sizeof b: 4 ,struct b.b: 4
global varial &a: 0x407028 ,a value: 0 ,c: 0
2:局部/全局变量初始值或默认值问题
在C语言中的全局变量和静态变量都是会自动初始化为0,堆和栈中的局部变量不会初始化,造成拥有不可预测的值。
C++保证了所有对象与对象成员都会初始化,但其中基本数据类型的初始化还需要依赖构造函数。下面我们讨论一下C++ 中成员变量的初始化规则
2.1 初始化语法
在C语言中在声明时用 = 即可完成初始化操作,C++偏向于使用圆括号()来完成。
// C语言风格
int i = 3;
int arr[] = {1,2,3};
// C++风格
int i (3);
int i = int(4)
int* p = new int(3)
int* arr = new int[3] {1,2,3}
在C语言中: int a ----》表示声明了整型变量a但未初始化,但是C++中总是会初始化(无论是否写了圆括号或者是否写了参数列表)
int basic_var // 未初始化,应用 “默认初始化机制”
Cperson person // 初始化,以空的参数列表调用构造函数。
(可以参考C语言为什么没有默认初始化机制)
C语言令人抓狂的一面——全局变量
2.2:默认初始化机制规则 (局部变量/全局变量/静态变量)
定义基本数据类型变量(单个值,数组)的同时可以指定初始值,如果未指定C++会去执行默认初始化(default-initialization) , 那么这个规则是怎样的了?
栈:中的变量和堆中的变量(动态内存),会保有不确定的值
全局变量和静态变量(包括静态局部变量):会初始化为零。
那么为啥会有这个区别了 ?
首先,未初始化的和初始化为零的静态/ 全局变量编译器是同样对待的,把他们存储在 进程 BSS段(这是全零的一段内存空间)中,所以他们会被 “默认初始化”为零。
#include<iostream>
#include<string>
using namespace std;
int g_var;
int* g_pointer;
static int g_static;
int main() {
int l_var;
int* l_pointer;
static int l_static;
cout << g_var << endl << g_pointer << endl << g_static << endl;
cout << l_var << endl << l_pointer << endl << l_static << endl;
};
很显然:局部变量没有初始化,编译器直接报错。
2.3:成员变量的初始化
成员变量分为成员对象和内置类型成员,其中成员对象总会被初始化,而我们要做的就是在构造函数中初始化其中的内置类型成员
#include<iostream>
#include<string>
using namespace std;
class A {
public:
int v;
};
A g_var;
int main() {
A l_var;
static A l_static;
cout << g_var.v << ' ' << l_var.v << ' ' << l_static.v << endl;
return 0;
}
打印结果: 0 6422280 0
从打印结果可知:内置类型的成员变量的 “默认初始化”行为取决于所在对象的存储方式,而存储方式对应的默认初始化规则是不变的。(即:全局对象变量 g_var 和局部静态变量对象 l_static存储在静态区,所以对象的 内置类型也存储在静态区,所以这个内置类型变量就可以默认初始化,而局部变量对象 l_var 存储在 栈中,所以它的 内置类型也在栈中,所以它就不能默认初始化,因而也就存在不确定值) 。
所以:
在 Effective C++ :item4 中,讨论了如何正确地在构造函数中初始化数据成员,这里不过多讨论,直接给出初始化的写法
class A{
public:
int v;
A(): v(0); // 直接在圆括号中初始化变量 v
};