C-08\变量类别和名称粉碎机制


全局变量

定义:在所有函数外部定义的变量称为全局变量,一般以g_开头,如

char g_szBuf[100];  //	全局变量g_szBuf

int main()
{
    printf("%s\r\n",g_szBuf);
    return 0;
}

作用范围(作用域):从声明变量的位置开始到源程序结束,即全局变量可以被在其定义之后的其他函数所共享

  • 当扩展名为.c时全局变量不能以未知值进行初始化,不然会报错:initializer is not a constant(初始化式不是常量)

  • 当扩展名为.cpp时全局变量可以以未知值初始化,如函数的返回值(运行时才能确定返回值)

int GetInt()
{
    return 0x87654093;
}

int g_nTest = GetInt();

int main()
{
    return 0;
}

如果以函数的返回值初始化全局变量,则该函数会在main函数之前运行(这是抢在main函数之前执行的方法之一),如上面的GetInt()函数会在main函数之前运行

以常量初始化全局变量,常量在链接阶段会直接被写进文件中,因此在模块创建时就会在内存中存在这个全局变量,模块卸载才会消失,如图所示:

所以如果想修改全局变量可以直接在文件中修改
已初始化的全局变量放在已初始化数据区,未初始化的全局变量放在未初始化数据区

如果想要文件小一点,全局变量使用未初始化(或初始化为0),这样链接时基本不会增加生成的可执行文件的大小,如下图

如果全局变量初始化且不全为0,链接的时候就会在生成的可执行文件中预留足够的空间,从而使可执行文件大小变大

编译器会将所有未初始化数据归纳起来,求对齐后的总值,然后在链接(link)做可执行程序时记录下这个总值,最后运行时操作系统根据这个预设值再提供足够的空间供未初始化数据使用

void __cdecl _cinit (
        void
        )
{
        /*
         * initialize floating point package, if present
         */
#if defined (_M_MRX000) || defined (_M_ALPHA) || defined (_M_PPC)
        /*
         * MIPS compiler doesn't emit external reference to _fltused. Therefore,
         * must always force in the floating point initialization.
         */
        _fpmath();
#else  /* defined (_M_MRX000) || defined (_M_ALPHA) || defined (_M_PPC) */
    	
    	// 如果存在浮点数
        if ( _FPinit != NULL )
            (*_FPinit)();	// 初始化浮点协处理器
    
#endif  /* defined (_M_MRX000) || defined (_M_ALPHA) || defined (_M_PPC) */

        /*
         * do initializations
         */
    
    	// 初始化官方对象,如 cin、cout等
        _initterm( __xi_a, __xi_z );

        /*
         * do C++ initializations
         */
    
    	// 我们自己的全局变量在该函数进行初始化
        _initterm( __xc_a, __xc_z );

}
#ifdef CRTDLL
void __cdecl _initterm (
#else  /* CRTDLL */
static void __cdecl _initterm (
#endif  /* CRTDLL */
        _PVFV * pfbegin,
        _PVFV * pfend
        )
{
        /*
         * walk the table of function pointers from the bottom up, until
         * the end is encountered.  Do not skip the first entry.  The initial
         * value of pfbegin points to the first valid entry.  Do not try to
         * execute what pfend points to.  Only entries before pfend are valid.
         */
        while ( pfbegin < pfend )
        {
            /*
             * if current table entry is non-NULL, call thru it.
             */
            if ( *pfbegin != NULL )
                (**pfbegin)();
            ++pfbegin;
        }
}

inline修饰符

  • 告诉编译器,直接把该函数的机器码插入到调用它的地方
  • 不是强制性的,编译器有可能会置之不理,如:递归函数通常不会被编译成内联函数
  • 会检查参数是否合法(编译器级别的检查)

extern修饰符

  • extern 修饰符用来告诉编译器,该函数或变量在外部.obj文件中有定义。链接阶段会在其它文件中找,如果链接阶段在其它文件中找不到,则链接不会通过

    // 告诉编译器在别的文件中查找 g_szBuf 数组
    // 注意 extern 后面的变量不能有实质性动作(不能赋值,不能定义)
    extern char g_szBuf[];
    
    // 错误的。  不能赋值
    extern char g_szBuf[] = "hello world"
    
    • 声明:没有额外的机器行为,只是知会一下编译器,该变量或函数存在
    • 定义:要说明这个事怎么做,如定义一个函数,那就要定义函数的流程,定义函数的结构。那就要产生机器行为
  • extern "C" 用来标识函数或变量,使用标准的C编译器符号粉碎规则

  • extern 用来实现全局变量和函数的跨文件访问

Bug(vc++6.0)

  • 下面程序会崩溃,不会显示1234。原因是编译器认为下面程序没有浮点运算,所以没有初始化浮点协处理器,而scanf()函数中有浮点运算,所以导致了崩溃。后面高版本解决的办法是:不管程序有没有浮点运算都初始化浮点协处理器

    int main()
    {
        float m = 0.0f; // 加上这句编译器就会初始化浮点协处理器,程序可以正常运行
        float f;
        scanf("%f",&f);
        printf("1234");
        return 0;
    }
    
  • 下面程序会进入死循环,不会显示1234。早期版本的编译器(VC++6.0),因为goto编译器发现return 0是永远不会抵达的代码段,因此这段代码段不会参与编译,即删除 return 0 (将后面代码的行数往上推,不小心多推了一下,循环多了一次),就变成了NEXT: goto NEXT;自己跳自己,就会一直循环下去

    int fun()
    {
        goto NEXT;
        return 0;
    NEXT:
        ;
    }
    
    int main()
    {
        fun();
        printf("1234");
        return 0;
    }
    
  • vc6.0watch窗口使用C风格名称粉碎查找变量,导致.cpp文件中的静态局部变量不能使用watch窗口查看,解决办法将.cpp文件改为.c文件或者借用全局变量在内存窗口定位

静态变量

定义:变量前加static修饰符

作用:

  • 相当于限制了作用域的全局变量

  • 修饰全局变量或函数,则将该变量或函数的作用域限制在其定义的文件,其余文件无法访问,即使在其余文件中使用了extern修饰,也无法使用

    这是因为编译器在编译的时候虽然依然会编译生成全局变量(在.obj有静态全局变量),但是编译器没有为其生成外部符号的导出,所以外部访问不到。导致链接器找不到该静态全局变量符号,无法链接通过

  • 修饰局部变量,则将该变量的作用域限制在定义的函数,其余函数无法访问。

    • 只会初始化一次,如果以常量进行初始化,则不会产生任何代码,编译链接的时候直接将常量写入文件中(已初始化数据区)

      void fun(int n)
      {
          static int stcnTest = 0x789;
      }
      
    • 如果以变量进行初始化,为了实现只会初始化一次,会有一个全局的标志。早期在多线程中使用静态局部变量会导致多次初始化的问题(在全局标志修改前变量已经被多个线程初始化)。vs2013以后的版本设置了线程安全(将该标志保证在TLS中),解决了这个问题

      int g_isStcnTestInit = 0;
      void fun(int n)
      {
          if(!g_isStcnTestInit)
          {
              static int stcnTest = n;
              g_isStcnTestInit = 1;
          }   
      }
      
  • 面向对象的萌芽期,类似于私有

    私有的函数和变量使用static限制在文件中,一人负责一个文件互不干涉

检查的标准程度(编译链接阶段检查)
全局变量 跨文件
静态全局变量 不能跨文件
静态局部变量 不能跨函数

名称粉碎机制

  • 一般会有作用域,原先名称和层级三项信息(每个编译器的名称粉碎规则是不一样的)
  • extern "C":不管函数参数类型,所以也就不支持函数重载。而C++中函数的参数类型也会参与到名称粉碎,这也是其支持函数重载的原理
void fun()
{
    // _?stcTest@?1??fun@@YAXXZ@4HA
    static int stcTest = 0x3838438;
    printf("%d\r\n",stcTest);
}

int main(int argc, char *argv[])
{
    // _?stcTest@?1??main@@9@4HA
    static int stcTest = 0x1314520;

    printf("%d\r\n",stcTest);

    fun();

    return 0;
}

寄存器变量

  • 定义时加上类型修饰register
  • 只限于int型、char型和指针类型
  • 如果寄存器使用饱和时,程序将寄存器变量自动转换为自动变量处理,不会通知你
  • debug版本无论声明与否都不会使用寄存器变量(为了方便调试)
  • release版本无论声明与否编译器都会上寄存器变量(有空闲时)