《C++回顾笔记》声明、定义与初始化


声明(Declaration)

  所谓声明,是告诉编译器某个东西的名称和类型,但略去某些细节。在程序中,允许重复声明多次。下面的都是声明式:

extern int x;                         /// 声明一个变量

class Widget;                         /// 声明一个类

std::size_t numDigits(int number);    /// 声明一个函数

class A{
public:
    static int a;                     /// 声明一个静态变量
}
  • 任何包含了显式初始化的声明即成为定义,可以给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用,也就不再是声明,而变成定义了:
    extern int x = 0;                   /// 定义了一个变量,其类型为int
  • 此外,只有当extern声明位于函数外部时,才能执行赋值操作。

定义(Definition)

  定义的任务是,为编译器提供声明所遗漏的细节。对于对象而言,定义的任务是为此对象分配内存;对函数或者模板函数而言,定义提供了函数的实现;而对类或者模板类来说,定义列出其成员变量和成员函数。在程序中,只允许定义一次。下面的都是定义式:

int x;                                /// 定义一个变量

class Widget                          /// 定义一个类
{
public:
    Widget();
    ~Widget();
}

std::size_t numDigits(int number)     /// 定义一个函数
{
    std::size_t digit = 1;
    return digit;
}    

int A::a = 0;                         /// 定义一个静态变量并初始化

大多数情况下,定义也完成了声明的工作,不需要再额外声明。但以下两种情况,定义只是定义:

  • 在类定义之外,定义并初始化一个静态数据成员
  • 在类外定义非内联成员函数

声明和定义的关系

  为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separate compilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。为了支持分离式编译,C++语言将声明和定义区分开来。声明告诉程序一个名字,一个文件如果想使用别处定义的变量则必须包含对那个名字的声明,而定义负责创建与名字关联的实体。

  • 程序使用到的每个名字都会指向一个特定的实体:变量函数类型
  • 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。

  声明和定义的区别看起来也许微不足道,但实际上却非常重要。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。


初始化(Initialization)

  初始化是指给予已定义的对象初始值的过程。如果定义变量时候没有初始化,则变量将会被默认初始化(隐式初始化,由编译器按照一定的规则自动完成)。一般来说,默认初始化的规则如下:

  • 栈中的变量(函数内定义的变量)和堆中的变量(动态内存)会有不确定的值
  • 全局变量和静态变量(包括局部静态变量)会被初始化为该变量的类型中0这个概念对应的值

    C++11: If no initializer is specified for an object, the object is default-initialized; if no initialization is performed, an object with automatic or dynamic storage duration has indeterminate value. Note: Objects with static or thread storage duration are zero-initialized.

  之所以对不同类型的变量,默认初始化的行为会不一致,是因为编译器存储它们的内存空间不同。关于编译器存储变量的内存空间分配,可以参阅进程的内存空间分配

  值得注意的是,如果变量的类型是自定义的类,那么该变量在被默认初始化的时候,将会调用其默认构造函数。倘若该类找不到默认构造函数,或者没有带缺省参数的构造函数,编译器将会报错。

对于自定义的类来说,其没有初始化的成员变量的值是否为一个不确定的值,取决于其所在对象的存储方式(对象也没被初始化)

  1. 如果对象为全局变量或者静态变量(存于BSS段),那么其成员变量的值将会被初始化为0
  2. 如果对象为动态对象或者局部变量(存于堆或者栈上),那么其成员变量的值将是一个不确定的值
#include 
#include 

using namespace std;

class Test 
{
public:
    Test() 
    {
        cout << "Test(): a = " << a << endl;
    }

    int a;
};

class StaticTest
{
public:
    StaticTest()
    {
        cout << "StaticTest(): a = " << a << endl;
    }

    int a;
};

class TestPtr
{
public:
    TestPtr()
    {
        cout << "TestPtr(): a = " << a << endl;
    }

    int a;
};

Test GlobalTest;                                          /// 对象存于BSS段,成员变量a的值会被初始化为0,并调用默认构造函数
static StaticTest StaticGlobalTest;                       /// 对象存于BSS段,成员变量a的值会被初始化为0,并调用默认构造函数

TestPtr* GlobalTestPtr = new TestPtr();                   /// 对象存于堆上,成员变量a的值为不确定值,不会被初始化为0,并调用默认构造函数
TestPtr* GlobalTestPtr2;                                  /// 指针GlobalTestPtr2存于BSS段,因此其值会被初始化为0,也就是nullptr

int main()
{

    cout << "--------------------Initialization of local variable --------------------" << endl;

    Test LocalTest;                                         /// 对象存于栈上,成员变量a的值为不确定值,不会被初始化为0,并调用默认构造函数

    static StaticTest StaticLocalTest;                      /// 对象存于BSS段,成员变量a的值会被初始化为0,并调用默认构造函数

    TestPtr* LocalTestPtr = new TestPtr();                  /// 对象存于堆上,成员变量a的值为不确定值,不会被初始化为0,并调用默认构造函数
    TestPtr* LocalTestPtr2;

    cout << "GlobalTestPtr2 = " << GlobalTestPtr2 << endl;  
    // cout << "LocalTestPtr2 = " << LocalTestPtr2 << endl; /// 将会报错,不允许使用未初始化的变量

    system("pause");
    return 0;
}

  根据默认初始化的规则,不难得到上述事例程序的输出为:

Test(): a = 0
StaticTest(): a = 0
TestPtr(): a = -842150451
--------------------Initialization of local variable --------------------
Test(): a = -858993460
StaticTest(): a = 0
TestPtr(): a = -842150451
GlobalTestPtr2 = 00000000
LocalTestPtr2 = 00000000
请按任意键继续. . .

赋值与初始化

  在C++中,可以使用=来完成初始化,因此这很容易让人认为初始化是赋值的一种。然而,事实上,在C++语言中,赋值和初始化是两个完全不同的操作,只是它们两者的区别在很多情况下可以忽略不计。需要强调的是,初始化不是赋值,初始化的含义是创建变量的时候赋予其一个初始值,而赋值的含义则是把变量的当前值擦除,并使用一个新值来替代。


文章作者: RainbowCyan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 RainbowCyan !
 上一篇
《C++回顾笔记》进程的内存空间分配 《C++回顾笔记》进程的内存空间分配
  现代操作系统会为每个进程都分配一个虚拟内存地址空间,虚拟内存技术使得每个进程都可以独占整个内存空间,地址从零开始,直到内存上限。进程会把整个地址空间(从低地址到高地址)分为不同的区间用于不同的用途:  &e
下一篇 
《重构--改善既有代码的设计》读书笔记(一)重构的原则 《重构--改善既有代码的设计》读书笔记(一)重构的原则
前言  在前段时间,我被安排去负责一个重要模块的重构工作。惭愧的是,在开始这项重要的重构工作之前,虽然我曾花费过一些时间仔细翻阅本书,但后面重构的过程依旧十分坎坷,而且在最后进行测试的时候,发现了很多重构时新引入的bug
  目录