C++头文件中定义static/const

温故知新! 网上查看到这篇文章,终结的很好,自己也修改了,这里记录一下:

背景

      看到一个头文件,是专门定义各种常量的,有一天发现这里的常量定义既使用了 static,又使用了 const,这个文件没有类,全部都是字符串的定义

// QVNDefine.h
#ifndef QVNDefine_h
#define QVNDefine_h
const string VN_LIST = "list";
const string VN_CELL = "cell";
const string VN_VIEWPAPER = "view-pager";

static vector<string> VN_MULTICELLTYPE = {VN_LIST, VN_VIEWPAPER};

#endif /* QVNDefine_h */

传统做法

应该在头文件里声明,在实现文件里定义会更好,即类似

// QVNDefine.h
#ifndef QVNDefine_h
#define QVNDefine_h

extern const string VN_LIST;

#endif /* QVNDefine_h */

 

// QVNDefine.cpp
#include "QVNDefine.h"

extern const string VN_LIST = "list";

      虽然是自己写的代码,但其实自己对这里的知识不太熟悉,习惯性就写了 const,又不知道为什么写了 static,所以找了一下资料系统学习一下

声明与定义

我们的问题是在 C++ 头文件中如何正确定义全局变量,首先要理解变量的声明和定义的区别是什么,翻了一下之前的笔记

extern int a;       // 声明一个全局变量a

int a;              // 定义一个全局变量a
extern int a = 0;   // 定义一个全局变量a并给初值
int a = 0;          // 定义一个全局变量a并给初值

注意一下在 C++ 中

以上是针对变量的,如果是函数,还有点微妙的区别

函数的定义和声明一样是有区别的,定义函数要有函数体,声明函数没有函数体

所以与变量的区别就是没有函数体的函数是声明,而不是定义,如下例

int a;              // 这是一个变量的定义
int fun(void);      // 这是一个函数的声明,省略了extern,完整些是extern int fun(void);

 

头文件定义全局变量几种方式的比较

    根据以上信息,我们有以下几种定义全局变量的方式

第一种方法,直接定义

// QVNDefine.h
#ifndef QVNDefine_h
#define QVNDefine_h
extern int a = 0;
#endif /* QVNDefine_h */

    这是危险的,相当于 int a = 0,变量存放在同一个地址,是全局变量,多个实现文件包含该头文件是会发生重复定义问题,违背了 ODR 规则!

第二种方法,使用 static

// QVNDefine.h
#ifndef QVNDefine_h
#define QVNDefine_h
static int a = 0;
#endif /* QVNDefine_h */

    这是可行的,在编译阶段,每个包含该头文件的 .cpp 会生成一个 static int a = 0,变量存放在不同的地址,不是全局变量

第三种方法,使用 const

// QVNDefine.h
#ifndef QVNDefine_h
#define QVNDefine_h
const int a = 0;
#endif /* QVNDefine_h */

    这是可行的,在编译阶段,每个包含该头文件的 .cpp 会生成一个 const int a = 0,变量存放在不同的地址,不是全局变量,与 static 效果一样

第四种方法,使用 extern const 声明 + 实现文件定义

// QVNDefine.h
#ifndef QVNDefine_h
#define QVNDefine_h
extern const int a;
#endif /* QVNDefine_h */
// QVNDefine.cpp
#include "QVNDefine.h"
extern const int a = 1;

      这是可行的,在编译阶段,其他包含该头文件的 .cpp 会生成一个 extern const int a,存放在同一个地址,是全局变量

      除了方法一,其他方法都是可行的。那么它们有什么区别呢,谁才是最佳方式?

      先说结论,大多数情况下,方法三最好。

先聊聊 static

      这里只阐述全局变量有无被 static 修饰的区别

      一个全局变量(无论是定义在 .h 还是 .cpp),如果没有被 static 修饰,那么它是全局性的,假如该头文件被多次 include,在编译时就会产生重复链接的报错

      而如果添加了 static,该全局变量就会变成静态全局变量,其作用域只在当前编译单元(比如 include 了该头文件的 .cpp)生效

      所以实现文件的全局函数一般都要添加 static,这样不同的人编写不同的实现文件时,不用担心自己定义的函数,是否会与其它文件中的函数同名

      根据这个特点,假如有一百个*.cpp实现文件包含了该头文件,那么这个全局变量就会被定义一百次,这个会造成内存空间的浪费,应该避免使用这种方式

    此时我们可以使用 extern 声明 + 实现文件定义的方法来解决多次定义的浪费问题

再聊聊 const

      c++引入了命名常量的概念,命名常量就像变量一样,只是它的值不能改变,如果试图改变一个const 对象,编译器将会产生错误。 const 和正常变量一样有作用域,所以函数内部的const也不会影响程序的其余部分。在c++中const可以取代预处理器#define来进行值替代, const有安全的类型检查,所以不用担心会像预处理器一样引入错误。

      在通常的情况下const同预处理器#define一样只是将所赋值保存入编译器的符号表中(符号表仅仅在编译时存在,在编译过程中编译器将程序中的名字与之在符号表中定义的数值作简单的替换),在使用的时候进行值替换,并不为const创建存储空间我们将const的定义放进头文件里,这样通过包含头文件,可以把const定义单独放在一个地方并把它分配给一个编译单元,const默认为内部连接(内部连接意味着只对正在编译的文件创建存储空间,别的文件可以使用相同的标示符和全局变量,编译器不会发现冲突,外部连接意味着为所有被编译过的文件创建一片单独的存储空间,一般全局变量和函数名的外部连接通过extern声明,可以通过其他的文件访问)也就是说const仅能被它所定义过的文件访问,在定义一个const时,必须赋一个值给它,除非用extern做出说明。

 

const 的最初动机是取代预处理器 #define 来进行值替代,后来还被用于指针、函数变量、返回类型、类对象以及成员函数——《C++ 编程思想》

      这里我们只阐述头文件中的 const 有什么特点

      const 在 C++ 中默认为内部链接(这一点与 C 不同,注意),即只对包含该定义的文件里是可见的,而不会被其他编译单元看到,故不是一个全局变量(与 static 类似),这个特点保证了不会有重复定义的错误

      既然 const 与 static 类似,那么是否一样会有多次定义的浪费问题呢?以及为什么比方法四(extern 声明 + 实现文件定义)好?答案都在书里

通常 C++ 编译器并不为 const 创建存储空间,相反它把这个定义保存在它的符号表里。大部分场合使用内部数据类型的情况,包括常量表达式,编译都能执行常量折叠——《C++ 编程思想》

不过以下情况,编译器会进行存储空间的分配:

  • extern 成为 const 变量定义的一部分
  • 取一个 const 的地址
  • const 修饰的是一个复杂的对象

     如果 extern 成为 const 变量定义的一部分的时候,那么编译器会为强制进行了存储空间分配,extern 意味着使用外部连接,因此必须分配存储空间,这也就是说有几个不同的编译单元应当能够引用它,所以它必须存储空间

常量折叠

      何为常量折叠?常量折叠(Constant folding)和常量传播(constant propagation)都是一种编译器最佳化技术

      常量折叠表面上的效果和宏替换是一样的,只是 “效果上是一样的”,而两者真正的区别在于,宏是字符常量,在预编译完宏替换完成后,该宏名字会消失,所有对宏的引用已经全部被替换为它所对应的值,编译器当然没有必要再维护这个符号

    而常量折叠发生的情况是,对常量的引用全部替换为该常量的值,但是,常量名并不会消失,编译器会把他放入到符号表中

i = 320 * 200 * 32;

      比如上面的代码中,编译器通常会在编译时直接计算出320 * 200 * 32的值,而不会在此生成2个乘法指令.

   例如
const int a = 8;
cout << a << endl;
  经过编译器扫描后,变为:   

cout << 8 << endl;

      编译器会为常量分配了地址,但是在使用常量的时候,常量会被一立即数替换(保护常量,防止被破坏性修改)

      在C++中对于基本类型的常量,编绎器并不为其分配存储空间,编译器会把它放到符号表当取符号常量的地址等操作时,将强迫编译器为这些常量分配存储空间,编译器会重新在内存中创建一个它的拷贝,通过地址访问到的就是这个拷贝而非原始的符号常量。
 

结论

      const int i=10;//这个类似宏替换,也就是说,它优化之后可能是放一个符号表里面。所有使用i的地方都用10代替但是当你对i取址后,没办法,编译器必须为i在常量区找个地方安身。这就是所谓的常量折叠

      所以如果定义的都是内部数据类型,我们只要保证不对 const 变量进行取址操作(事实上也很少这样做),那么使用 const 的方式是最佳的,因为可以借助编译器的力量进行优化。最后回归背景问题,我们需要将 static 替换为 const 即可

参考文章

   转自:

https://norcy.github.io/wiki/C++/C++%E5%A4%B4%E6%96%87%E4%BB%B6%E5%A6%82%E4%BD%95%E6%AD%A3%E7%A1%AE%E5%AE%9A%E4%B9%89%E5%85%A8%E5%B1%80%E5%8F%98%E9%87%8F/

 

  • 5
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值