声明与定义分离

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/lca1826/p/6503194.html

[2] https://blog.csdn.net/u012679707/article/details/80188124

[3] C++ Primer

[C++]变量声明与定义的规则的更多相关文章

  1. switch语句下的变量声明和定义

    switch语句下的变量声明和定义的问题: switch...case...语句中存在声明和定义会出现一些问题.这个由switch语法特性决定的, switch中每个case都是平等的层次,区别于一般 ...

  2. c++变量声明、定义,const变量

    变量声明和定义的主要区别: 声明不分配存储空间,定义分配存储空间. 变量可以声明多次,但只能定义一次(一个变量只能在一个源文件中定义) 声明通常放在头文件(.h)中,定义放在源文件(.cpp)中 变量 ...

  3. PHP中变量声明和定义的区别

    先记录一下(不知道PHP是不是一样,但是C语言是这样的):把建立空间的声明称之为“定义”,而把不需要建立存储空间的声明称之为“声明”.声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用 ...

  4. 变量声明和定义及extern 转载

    在讨论全局变量之前我们先要明白几个基本的概念: 1. 编译单元(模块):    在IDE开发工具大行其道的今天,对于编译的一些概念很多人已经不再清楚了,很多程序员最怕的就是处理连接错误(LINK ER ...

  5. 内存四个领域,变量声明和定义,注册,c内联汇编,auto,堆,不变,静态变量

     1.内存四大区域 2.在程序中,变量的声明能够有多份,定义仅仅能有一份 watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdG90b3R1enVvcXVh ...

  6. 变量声明和定义的关系------c++ primer

    为了允许把程序分成多个逻辑部分来编写,c++语言支持分离式编译机制 为了支持分离式编译,c++语言把声明和定义区分开来.声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名 ...

  7. C++11类内static成员变量声明与定义

    众所周知,将一个类内的某个成员变量声明为static型,可以使得该类实例化得到的对象实现对象间数据共享. 在C++中,通常将一个类的声明写在头文件中,将这个类的具体定义(实现)写在cpp源文件中. 因 ...

  8. C++ 变量的声明与定义的区别

    变量声明和定义的区别 我们在程序设计中,时时刻刻都用到变量的定义和变量的声明,可有些时候我们对这个概念不是很清楚,知道它是怎么用,但却不知是怎么一会事,下面我就简单的把他们的区别介绍如下:(望我的指点 ...

  9. C语言中声明和定义详解(待看。。

    变量声明和变量定义 变量定义:用于为变量分配存储空间,还可为变量指定初始值.程序中,变量有且仅有一个定义. 变量声明:用于向程序表明变量的类型和名字. 定义也是声明,extern声明不是定义 定义也是 ...

随机推荐

  1. 区块链项目NGK未来价值几何?

    没有人可以预知NGK未来会涨到多少钱,就像比特币只有10美分时,也无法预测它会涨到现在的价格⼀样.那时候人们把CPU超频挖矿只作为⼀种爱好和娱乐.所以,人们也没有办法预知NGK未来的价格.但可以知道的 ...

  2. Dyno-queues 分布式延迟队列 之 生产消费

    Dyno-queues 分布式延迟队列 之 生产消费 目录 Dyno-queues 分布式延迟队列 之 生产消费 0x00 摘要 0x01 前情回顾 1.1 设计目标 1.2 选型思路 0x02 产生 ...

  3. Python算法_整数反转(02)

    给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转. 示例 1: 输入: 123输出: 321 示例 2: 输入: -123输出: -321 示例 3: 输入: 120 输出: 2 ...

  4. iOS写在定制相机之前

    问题 不是所有的拍照UIImagePickerController都能搞定,理由如下: 1.产品不整点幺蛾子,哪来体验创新 2.设计不整点幺蛾子,怎能体现用心 3.运营:这体验跟某宝某信咋不一样??? ...

  5. Java操作Excel工具类(poi)

    分享一个自己做的poi工具类,写不是很完全,足够我自己当前使用,有兴趣的可以自行扩展 1 import org.apache.commons.lang3.exception.ExceptionUtil ...

  6. SpringBoot整合Mongodb4.0

    本品文章只做学习使用: 安装mongodb推荐博客:https://www.jianshu.com/p/a75e26e5f635 1:如何在外网环境下开放mongodb 服务器版本:centos7.6 ...

  7. React Context 理解和使用

    写在前面 ​ 鉴于笔者学习此内容章节 React官方文档 时感到阅读理解抽象困难,所以决定根据文档理解写一篇自己对Context的理解,文章附带示例,以为更易于理解学习.更多内容请参考 React官方 ...

  8. 痞子衡嵌入式:盘点国内RISC-V内核MCU厂商

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是国内RISC-V内核MCU厂商. 虽然RISC-V风潮已经吹了好几年,但2019年才是其真正进入主流市场的元年,最近国内大量芯片公司崛起 ...

  9. macOS命令行切换Python版本

    目录 brew安装anaconda3 anaconda3环境变量设置 安装双版本 命令后切换python环境 pip ide vscode set 参考 brew安装anaconda3 brew ca ...

  10. AQS源码解读(ReentrankLock的公平锁和非公平锁)

    构建Debug代码: 1 package com.hl.interview.lock; 2 3 import java.util.Scanner; 4 import java.util.concurr ...