乐趣区

关于c++:C-变量声明与定义的各种规则

申明与定义拆散

Tips:变量能且仅能被定义一次,然而能够被屡次申明。

为了反对分离式编译,C++ 将定义和申明辨别开。其中申明规定了变量的类型和名字,定义除此性能外还会申请存储空间并可能为变量赋一个初始值。

extern

如果想申明一个变量而非定义它,就应用关键字 extern 并且不要显式地初始化变量:

extern int i;      // 申明 i 而非定义 i
extern int i = 1;  // 定义 i, 这样做对消了 extern 的作用 

static

当咱们在 C /C++ 用 static 润饰变量或函数时,次要有三种用处:

  • 部分动态变量
  • 内部动态变量 / 函数
  • 类内静态数据成员 / 成员函数

其中第三种只有 C ++ 中有,咱们后续在面向对象程序设计中再探讨,这里只探讨动态部分 / 全局变量。

1. 动态局部变量

在局部变量后面加上 static 说明符就形成动态局部变量,例如:

// 申明部分动态变量
static int a;
static int array[5] = {1, 2, 3, 4, 5};
  • 动态局部变量在函数内定义,但不像主动变量那样当函数被调用时就存在,调用完结就隐没,动态变量的生存期为整个源程序
  • 动态变量的生存期尽管为整个源程序,然而作用域与主动变量雷同,即只能在定义该变量的函数内应用该变量,退出函数后尽管变量还存在,但不可能应用它
  • 对根本类型的动态局部变量如果在申明时未赋初始值,则零碎主动赋 0 值;而对一般局部变量不赋初始值,那么它的值是不确定的

依据动态局部变量的特点,它的生存期为整个源程序,在来到定义它的函数(作用域)但再次调用定义它的函数时,它又可持续应用,而且保留了前次被调用后留下的值。因而,当屡次调用一个函数且要求在调用之间保留某些变量的值时,可思考采纳动态局部变量,尽管用全局变量也能够达到上述目标,但全局变量有时会造成意外的副作用,因而最好采纳部分动态变量。例如:

#include <iostream>

void foo() {
    int j = 0;         // 一般局部变量
    static int k = 0;  // 动态局部变量
    ++j;
    ++k;
    printf("j:%d, k:%d\n", j, k);
}

int main(void)
{for (int i = 1; i <= 5; i++) {foo();
    }
}

// 输入:
j:1, k:1
j:1, k:2
j:1, k:3
j:1, k:4
j:1, k:5

2. 动态全局变量(C++ 废除,用匿名命名空间代替)

Tips:对于全局变量,不论是否被 static 润饰,它的存储区域都是在动态存储区,生存期为整个源程序。只不过加上 static 后限度这个全局变量的作用域只能在定义该变量的源文件内。

全局变量(内部变量)的申明之前加上 static 就形成了动态的全局变量,全局变量自身就是动态存储变量,动态全局变量当然也是动态存储形式。这两者在存储形式上并无不同,这两者的区别在于 非动态全局变量的作用域是整个源程序。当一个源程序由多个源程序组成时,非动态的全局变量在各个源文件中都是无效的,而动态全局变量则限度了其作用域,即只在定义该变量的源文件内无效,在同一源程序的其余源文件中不能应用它。

这种在文件中进行动态申明的做法是从 C 语言继承而来的,在 C 语言中申明为 static 的全局变量在其所在的文件外不可见。这种做法曾经被 C ++ 规范勾销了,当初的代替做法是应用匿名命名空间。

匿名命名空间:指关键字 namespace 后紧跟花括号括起来的一系列申明语句,具备如下特点:

  • 在匿名命名空间内定义的变量具备动态生命周期
  • 匿名空间在某个给定的文件内能够不间断,然而不能逾越多个文件
  • 每个文件定义本人的匿名命名空间,不同文件匿名命名空间中定义的名字对应不同实体
  • 如果在一个头文件中定义了匿名命名空间,则该命名空间内定义的名字在每个蕴含该头文件的文件中对应不同实体
namespace {int i;  // 匿名命名空间内定义的变量具备动态生命周期, 作用域仅限于以后文件}

3. 总结

static 这个说明符在不同中央所起的作用域是不同的,比方把局部变量扭转为动态变量后是扭转了它的存储形式即扭转了它的生存期,把全局变量扭转为动态变量后是扭转了它的作用域,限度了它的应用范畴。

auto

1. C++98 中 auto 用法(C++11 已废除)

C++98 auto用于申明变量为主动变量(领有主动的生命周期),C++11曾经删除了该用法,取而代之的是“变量的主动类型推断办法”。

// c++ 98:
int a = 10;         // 领有主动生命期
auto int b = 20;    // 领有主动生命期(C++11 编译不过)
static int c = 30;  // 缩短了生命期

C++11 新规范引入了 auto 类型说明符,让编译器通过初始值来主动推断变量类型(这意味着通过auto 定义的变量必须有初始值)。

// c++ 11:
int a = 10;
auto auto_a = a;  // 主动类型推断为 int 类型

2. auto 会去除变量的援用语义

当援用对象作为初始值时,真正参加初始化的是援用对象的值,此时编译器会以援用对象的类型作为 auto 推算的类型:

int main(void) {
    int i = 10;
    int &ri = i;
    auto auto_i = ri;  // 去除援用语义, 主动推断为 int
}

如果心愿推断进去的 auto 类型蕴含援用语义,咱们须要用 & 明确指出:

int main(void) {
    int i = 10;
    auto &auto_i = i;  // 加上援用语义, 主动推断为 int&
}

3. auto 疏忽顶层 const

auto 个别会疏忽掉顶层 const,同时底层 const 会被保留下来:

int main(void) {
    const int ci = 10;    // 常量 int
    auto auto_ci = ci;    // auto_ci 被推断为 int 类型
    auto_ci = 20;         // 正确: auto_ci 十分量

    const int &cr = ci;   // cr 是指向常量 int 的常量援用
    auto auto_cr = cr;    // auto_cr 被推断为 int 类型: 去除了援用语义 + 去除了顶层 const
    auto_cr = 20;         // 正确: auto_cr 十分量

    const int *cp = &ci;  // cp 是指向常量 int(底层)的常量指针(顶层)
    auto auto_cp = cp;    // auto_cp 被推断为 const int* 类型(指向常量 int 的指针): 去除了顶层 const + 保留底层 const
    // *auto_cp = 10;     // 谬误: 不能批改 auto_cp 指向的常量
}

如果心愿推断进去的 auto 类型是一个顶层 const,咱们须要通过 const 关键字明确指出:

int main(void) {
    const int ci = 10;          // 常量 int
    const auto auto_ci = ci;    // auto_ci 被推断为 const int 类型
    // auto_ci = 20;            // 谬误: auto_ci 是一个常量, 禁止批改
}

const

有时咱们心愿定义一个不能被扭转值的变量,能够应用关键字 const 对变量类型加以限定。

1. const 对象必须初始化

因为 const 对象一经创立后其值就不能再扭转,所以 const 对象必须初始化,然而初始值能够是任意简单的表达式:

const int i = get_size();  // 正确: 运行时初始化
const int j = 42;          // 正确: 编译时初始化
const int k;               // 谬误: k 是一个未经初始化的常量

2. 默认状况下 const 仅在文件内无效

举个例子,咱们在编译时初始化一个 const 对象:

const int i = 10;

编译器会在编译过程把用到该变量的中央都替换为对应的值。为了执行这个替换,编译器必须晓得变量的初始值,如果程序蕴含多个文件,那么每个用了这个 const 对象的文件都必须得能拜访到它的初始值才行(即每个文件都要定义 const 对象)。为了防止对同一变量的反复定义,当多个文件中呈现同名的 const 对象时,其实等同于在不同文件中别离定义了独立的变量。

/*
 * 上面是非法的, 不存在变量 i 反复定义问题
 */

// foo.cpp
const int i = 10;

// bar.cpp
const int i = 5;

如果想在多个文件之间共享 const 对象,那么必须在变量的定义之前增加 extern 关键字:

/*
 * 上面是非法的, main.cpp 和 foo.cpp 中的 const int 对象是同一个
 */

// foo.cpp
extern const int i = 10;

// main.cpp
#include <iostream>

int main(void) {
    extern int i;
    std::cout << "i:" << i << std::endl;
}

3. 容许常量援用绑定十分量对象、字面值甚至个别表达式

一般而言,援用的类型必须与其所援用对象的类型统一,然而有两个例外:

  • 初始化常量援用时容许用任意表达式作为初始值,只有该表达式的后果能转换成援用类型即可,容许为一个常量援用绑定十分量的对象、字面值甚至是一个个别表达式(如下)
  • 能够将基类的指针或援用绑定到派生类对象上(后续面向对象章节再探讨)
int i = 10;

const int &ri1 = i;      // 非法: 绑定到十分量对象
const int &ri2 = 100;    // 非法: 绑定到字面值
const int &ri3 = 1 + 1;  // 非法: 绑定到个别表达式

4. 顶层 const 与底层 const

指针自身是一个对象,因而指针自身是不是常量与指针所指对象是不是常量是两个独立的问题,前者被称为顶层 const,后者被称为底层 const。

Tips:指针类型既能够是顶层 const 也能够是底层 const,其余类型要么是顶层常量要么是底层常量。

顶层 const 用于示意任意的对象是常量,包含算数类型、类和指针等,底层 const 用于示意援用和指针等复合类型的根本类型局部是否是常量。

int i = 10;

int *const p1 = &i;        // 顶层 const: 不能扭转 p1 的值
const int *p2 = &i;        // 底层 const: 不能通过 p2 扭转 i 的值
const int *const p3 = &i;  // 底层 const + 顶层 const

const int &r1 = i;         // 底层 const: 不能通过 r1 扭转 i 的值

constexpr

C++11 引入了常量表达式 constexpr 的概念,指的是值不会扭转并且在 编译期间 就能失去计算结果的表达式。

const int i = 10;          // 常量表达式
const int j = i + 1;       // 常量表达式
const int k = size();      // 仅当 size()是一个 constexpr 函数时才是常量表达式, 运行时能力取得具体值就不是常量表达式

在一个简单零碎中,咱们很难分辨一个初始值是否是常量表达式,通过 constexpr 关键字申明一个变量,咱们能够让编译器来验证变量的值是否是一个常量表达式。

1. 字面值是常量表达式

算术类型、援用和指针都属于字面值类型,自定义类则不属于字面值类型,因而也无奈被定义为 constexpr。

Tips:只管指针和援用都能被定义成 constexpr,但它们的初始值却受到严格限度。一个 constexpr 指针的初始值必须是 nullptr、0 或者是存储于某个固定地址中的对象。

2. constexpr 是对指针的限度

在 constexpr 申明中定义了一个指针,限定符 constexpr 仅对指针无效,与指针所指对象无关:

const int *pi1 = nullptr;      // 底层 const: pi1 是指向整型常量的一般指针
constexpr int *pi2 = nullptr;  // 顶层 const: pi2 是指向整型的常量指针

咱们也能够让 constexpr 指针指向常量:

constexpr int i = 10;
constexpr const int *pi = &i;  // 顶层 const + 底层 const 

Reference

[1] https://www.cnblogs.com/lca18…

[2] https://blog.csdn.net/u012679…

[3] C++ Primer

退出移动版