目录

C++变量声明与定义

声明与定义关系

为了允许把程序拆分为多个逻辑部分来写,C++文件允许分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。

如果将程序分为多个文件,则需要有在文件间共享代码的方法。例如,一个文件的代码可能需要使用另一个文件中定义的变量。一个实际的例子是std::coutstd::cin,它们定义于标准库,却能被我们写的程序使用。

为了支持分离式编译,C++将声明和定义区分开,声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。

变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还是申请存储空间,也可能会为变量赋一个初始值。

如果想要单纯声明一个变量,就在变量名前加关键字extern,而不要显式地初始化变量:

所以,下面的是声明:

1
extern   int   a;

下面的是定义

1
2
3
int   a; //声明并定义a
int   a = 0; // 声明并定义初始化a  
extern int a = 0;//// 声明并定义初始化a    

在函数体内部,如果试图初始化一个extern关键字标记的变量,将引发错误。总结一下就是:

根据C++标准的规定,一个变量是声明,必须同时满足两个条件,否则就是定义:

① 声明必须使用extern关键字

② 不能给变量赋初值

函数或变量在声明时,并没有给它实际的物理内存空间,它有时候可保证你的程序编译通过;

函数或变量在定义时,它就在内存中有了实际的物理空间。

如果你在编译单元中引用的外部变量没有在整个工程中任何一个地方定义的话,那么即使它在编译时可以通过,在连接时也会报错,因为程序在内存中找不到这个变量。

函数或变量可以声明多次,但定义只能有一次。

编译单元

在VC或VS上编写完代码,点击编译按钮准备生成exe文件时,编译器做了两步工作:

第一步,将每个.cpp(.c)和相应的.h文件编译成obj文件;

第二步,将工程中所有的obj文件进行LINK,生成最终.exe文件。

那么,错误可能在两个地方产生:

一个,编译时的错误,这个主要是语法错误;

一个,链接时的错误,主要是重复定义变量等。

编译单元指在编译阶段生成的每个obj文件。

一个obj文件就是一个编译单元。

一个.cpp(.c)和它相应的.h文件共同组成了一个编译单元。

一个工程由很多编译单元组成,每个obj文件里包含了变量存储的相对地址等。

全局变量(extern)

在函数体外定义的变量,默认是全局变量。

针对上文提到的一个文件的代码可能需要使用另一个文件中定义的变量情况,推荐的做法是在头文件中声明,对应的源文件中定义的做法。因为声明可以多次,定义只能一次,这样头文件被其他许多文件包含时候,不会出现重复定义的错误。如果将全局变量的声明和定义放在一个头文件中,将不能使用头文件的方式在多个文件文件中共享变量,即使在头文件中加#pragma once,或#ifndef也会出现重复定义,因为每个编译单元是单独的,都会对它各自进行定义。这样的情况下,只能使用extern修饰的声明在其他文件中引用该变量。

举个例子:

1
2
// 1.h 头文件
extern int a; //声明一个全局变量
1
2
// 1.cpp 源文件
int a; // 定义该变量,但没有初始化
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 2.cpp 源文件
#include<iostream>
#include"1.h" // 以包含源文件形式引用变量
using namespace std;

//a = 1; //在全局中,语句的执行顺序是没有保证的,所以全局变量不能再函数体外赋值
extern void nm();
int main() {
    a = 2;
    nm();
    cout << ++a << endl;
    return 0;
}
1
2
3
4
5
6
7
8
// 3.cpp 源文件
#include<iostream>
using namespace std;

extern int a; // 以extern修饰形式引用变量
void nm() {
    cout << a;
}

上述程序结果输出为: 23

2.cpp采用包含头文件的形式引用全局变量,3.cpp采用extern 修饰的声明引用。两者都能正确共享变量。但是,如果在1.h中将声明改成定义,就不能使用头文件引用的形式了。

总之变量的使用前必须有声明,有定义。

另外,有一个问题是,在函数体之外,全局变量不能被赋值,因为函数体之外,代码块的执行顺序没有固定要求,视编译器而定,所以如果允许赋值的话,可能会造成先调用后赋值的情况,所以被禁止。

在上述例子中,2.cpp中引用1.h头文件,并且使用extern修饰的声明语句引用函数nm,在main函数体内,对变量a进行赋值,该变量在1.h中声明,1.cpp中定义,由于包含了1.h,所以2.cpp可以引用它。接着调用nm函数,nm函数来自3.cpp,功能是直接输出a,在3.cpp中是用extern关键字引用的该变量。在调用该函数之前,已经对a赋值过了,所以能够正确输出。实际上,在我的PC上用VS运行,假如在赋值之前调用nm,输出是:03,因为编译器给a默认初始化为0了吧。

静态全局变量(static)

注意使用static修饰变量,就不能使用extern来修饰,即static和extern不可同时出现。

static修饰的全局变量的声明与定义同时进行,即当你在头文件中使用static声明了全局变量,同时它也被定义了。

static修饰的全局变量的作用域只能是本身的编译单元。在其他编译单元使用它时,只是简单的把其值复制给了其他编译单元,其他编译单元会另外开个内存保存它,在其他编译单元对它的修改并不影响本身在定义时的值。即在其他编译单元A使用它时,它所在的物理地址,和其他编译单元B使用它时,它所在的物理地址不一样,A和B对它所做的修改都不能传递给对方。

多个地方引用静态全局变量所在的头文件,不会出现重定义错误,因为在每个编译单元都对它开辟了额外的空间进行存储。

全局常量(const)

const单独使用时,其特性与static一样(每个编译单元中地址都不一样,不过因为是常量,也不能修改,所以就没有多大关系)。

const与extern一起使用时,其特性与extern一样。

1
2
extern const char g_szBuffer[];      //写入 .h中
const char g_szBuffer[] = "123456"; // 写入.cpp中