3.2.2  常量

与变量可以用在程序中表达那些可能会发生变化的数据量相对应地,在C++中,我们用常量来表达那些始终保持不变的数据量。简单来讲,就是程序中直接使用的数值、字符、字符串以及const关键字修饰的常变量等。大多数时候,常量只需要被读取一次,所以它没有名字,无须定义而可以直接使用。又因为其数据只能读取,不能修改,所以通常用来给一个变量赋值或者直接参与运算。例如:

// 用常量180对变量nHeight赋值
nHeight = ;
// 直接使用常量进行计算
fArea = fR * fR * 3.1415926;

这里的“180”和“3.1415926”就是两个常量,分别用来对变量nHeight进行赋值和参与乘法运算。这样的常量只能使用一次,当完成赋值操作和乘法运算后,这两个常量也就不再有意义了。

C++中的常量主要包括数值常量(整型常数、浮点型常数)、字符常量、字符串常量。

1. 整型常数

整型常数就是以文字形式出现的整数。整型常数的表示形式最常见的是十进制,也可以根据需要采用八进制或十六进制表示。在程序中,我们可以根据数字的前缀来区分各种进制的整数。

l  十进制整数:没有前缀,例如0、123、-1等。

l  八进制整数:以0为前缀,数字不包含8和9。例如0123、-022等。

l  十六进制整数:以0x或0X为前缀,数字除了0~9十个数字之外,还包括A~F六个英文字母字符。例如0x123、-0X2等。

在程序中,我们可以直接使用这三种方式来表示某个整数数值:

nHeight = ;         // 十进制常数
nHeight = ; // 八进制常数
nHeight = 0xAD; // 十六进制常数

上面的代码分别采用不同进制形式的常数对一个变量赋值。虽然这些常数的表现形式不同,但是它们所代表的数值都是173。

2. 浮点型常数

浮点型常数就是以文字形式出现的浮点数,也就是我们通常所说的小数。浮点数有两种表示形式:小数形式和指数形式。小数形式就是我们通常的小数书写形式,由数字和小数点构成,如1.0、0.1、.123等。而指数形式则是用科学记数法,将一个浮点数表示为一个小数与10的多少次方的乘积的形式。当一个浮点数较大或者较小时,使用指数形式来表示浮点数会更加方便。例如,1.3e9表示1.3×109,也就是1300000000,0.123E-4表示0.123×10-4等。

3. 字符常量

字符常量就是程序中使用的单个字符,如“a”、“A”、“!”等。在C++中,我们使用单引号(' ')来表示一个字符常量。例如:

// 用一个字符常量对变量aMark赋值
char aMark = 'A';
// 输出一个字符常量‘!’
cout<<'!'<<endl;

除了上述常见的可在屏幕显示的字符外,C++还允许使用一类特殊的字符常量。这些字符无法通过键盘直接输入,也不能直接输出显示到屏幕,但是可以用来表示一些特殊的控制意义,比如计算机响铃(/a)、换行(/n)、回车(/r)等。这些字符都以“\”开始,表示将“\”后的字符转换成其他的含义,所以这些字符也被称为转义字符。表3-2列出了C++的常用转义字符。

表3-2  C++中的常用转义字符

转义字符

意    义

'\a'

响铃,用cout直接输出该字符时,屏幕无显示,但计算机喇叭会发出“滴”声,常用来提示用户程序完成某项操作

'\n'

换行(n: line),如果在一个字符串中有这个字符,转义字符后的字符串将换行输出

(还记得在2.2.2[Chen1] 小节中我们曾用到过这个转义字符吗?回顾一下吧!)

'\t'

制表符,输出位置将横向移动一个Tab的位置

'\r'

回车(r:return)

'\\'

转义字符“\”本身

'\"'

双引号

'\''

单引号。以上这三个转义字符组合起来,可以输出一些含有特殊符号的字符串。例如,我们想输出:

这些字符都以“\”开始

这样一个字符串,就需要用到这些转义字符来输出其中的特殊符号:

cout<<"这些字符都以\"\\\"开始"<<endl;

转义字符的使用跟可显示字符的使用相似,可以把转义字符放到一个字符串中,让它完成相应的控制功能,也可以单独输出某个转义字符,例如:

// 将“\n“放到一个字符串中,它将控制这个字符串输出为两行
// 恭喜!
// 任务完成!
cout<<"恭喜!\n任务完成!"<<endl;
// 直接输出““\a”转义字符,发出一个计算机响铃,提示用户任务完成
cout<<'\a'<<endl;

4. 字符串常量

字符串常量就是由一对双引号(" ")括起来的字符序列,如"Hello World!"。注意,因为双引号是字符串的界限符,所以如果想在字符串中使用双引号,就要使用转义字符来表示。另外值得提醒的是,这里的双引号必须是英文的("")。因为与中文双引号(“”)在形式上非常相似,所以常常被初学者误用而导致错误。例如:

// 使用字符串常量对变量赋值
strName = "ZengMei";
// 输出字符串常量
// 在输出一些特殊符号(比如,双引号,斜杠等)时,我们必须使用相应的转义字符
// 这里使用了转义字符“\””来输出字符串中的双引号,最终输出结果如下
// 你的名字是:”ZengMei”
cout<<"你的名字是:\"ZengMei\""<<endl;

知道更多:原生字符串(raw string)标识

在字符串常量中,我们可以使用反斜杠(\)这个转义操作符来引入一些特殊字符,实现特殊的输出目的。然而,这却给正则表达式的书写带来了麻烦,因为在正则表达式中,反斜杠成了用于引入表示字符的特殊符号,并且使用非常频繁。如果想在正则表达式中表示反斜杠这个字符,我们不得不使用两个反斜杠来表示一个反斜杠字符。例如,我们要表达“被反斜杠(\)分隔开的两个单词”这样一个模式(\zeng\\\mei),在C++代码中就成了:

string s = "\\zeng\\\\\\mei";   // 这样的表示很不直观、且容易出错

我们注意到,在正则表达式中,反斜杠字符被表达为两个反斜杠的组合。为了表示一个反斜杠,我们必须在正则表达式中使用两个反斜杠来表示。第一个反斜杠表示这是一个转义字符,第二个才表示真正的反斜杠。这样的表达方式,会让我们的字符串变得非常复杂繁琐,很不直观,即使是经验丰富的程序员也很容易出错。

为了解决这个问题,C++11引入了原生字符串的机制,并使用原生字符串标识符R来表示一个原生字符串。在原生字符串中,每个字符都代表其最原始的字符意义,所见即所得。换句话说,也就是反斜杠(\)不再具有转义符的作用,一个反斜杠仅用一个反斜杠字符就可以表示。因而,上述的例子可以简化为:

string s = R"(\zeng\\mei)";        // 使用R"()"表示的原生字符串

原生字符串的R"(...)"记法相比于普通字符串的"..."记法会有一点点的冗长,但它的意义就在于它可以让字符串中的转义规则无效,所写即所得,所见即所得。当我们需要在字符串中频繁地表示各种特殊符号(反斜杠,引号等)的时候,原生字符串将非常简便,而这一点点书写上的冗长也是值得的。

无论是数值常量还是字符串常量,它们都像C++世界的“雷锋”,只做好事而不留名字。可是,这样也带来了一个麻烦:当我们在程序中需要重复多次地使用某个常量时,我们不得不在代码中一遍又一遍地书写同一个常量。例如,要编写一个有关圆的计算程序,无疑会多次用到3.14159这个浮点常数:

float fR = 19.82;  // 半径
// 用常数3.14159计算面积
float fArea = 3.14159 * fR * fR;
// 用常数3.14159计算周长
float fGirth = * 3.14159 * fR;

这样的代码,不仅书写起来非常困难(多次重复书写同一个小数,难以保证正确性和一致性),在后期也难以维护(如果想改变这个常数,我们不得不修改所有用到这个常数的地方)。这一切,都是因为常量在程序中无名无份,每次都是直接使用引起的。那么,解决的办法自然就是给常量取一个名字,让我们可以通过这个名字方便地重复多次使用同一个常量。在C++中,给常量取一个名字的方法有两种:

1.用#define预编译指令将数值或字符串定义成宏,然后用宏来代替常量的直接使用

2.用const关键字将一个变量修饰成常变量,然后用常变量来代替常量的直接使用

我们首先来看如何用宏来代替常量。所谓的宏,就是将某个无明确意义的数值(例如,3.14159,知道的认为是圆周率,不知道的认为只是某个奇怪的数字)定义为某个有明确意义的标识符(例如,PI,所有人都会认为是圆周率)。然后,就可以在代码中使用这个有意义的标识符来代替无明确意义的数值,从而使代码更具可读性。在C++中,可以使用#define预编译指令来定义一个宏:

#define 宏名称 宏值

其中,“宏名称”就是要定义的宏,通常用一个大写的有意义的名称来表示。“宏值”就是这个宏所代表的内容,它可以是一个常数、一个字符串,甚至是一个更加复杂的语句。比如,可以用下面的语句将3.14159定义为一个宏PI:

// 将3.14159定义成宏PI
#define PI 3.14159

有了常数3.14159所对应的宏PI之后,我们就可以在代码中直接使用PI来代替3.14159进行相应的计算。例如,上面的代码可以简化为:

// 将3.14159定义成宏PI
#define PI 3.14159 float fR = 19.82; // 半径
// 用PI计算面积
float fArea = PI * fR * fR;
// 用PI计算周长
float fGirth = * PI * fR;

这里,使用PI代替了原本应该使用的3.14159也同样可以完成计算。那么,宏是如何做到这一点呢?这里的宏PI并不是真正地具有了它所代表的3.14159这个常数的值,从本质上讲,宏只是一种替换。当编译器对代码进行预编译处理的时候,它会将代码中的宏替换为它所代表的内容,换而言之,也就是上面代码中的PI会被替换为3.14159,最终参与编译的代码实际上仍旧是:

// 宏PI被替换为常数3.14159计算面积
float fArea = 3.14159 * fR * fR; // 宏PI被替换为常数3.14159计算周长
float fGirth = * 3.14159 * fR;

从这里可以看到,宏的使用并没有减少代码中的常数,但是它用一种巧妙的方法,减少了重复输入某个常数的繁琐,避免了可能发生的书写错误。

最佳实践:使用宏提高代码的可读性与可维护性

除了减少代码重复避免书写错误之外,宏的使用还会给我们带来额外好处:

1. 让代码更简洁明了,更具可读性

一个意义明确的宏名称往往比一个复杂而无意义的常数数字包含了更加丰富的信息,可以增加代码的可读性;同时,宏比常数数字更简单,可以使代码更简洁。对比下面两段代码:

// 不使用宏的代码
for( int i = ; i < ; ++i )
{
// ...
} // 使用宏的代码
#define MIN 0
#define MAX 1024 for( int i = MIN; i < MAX; ++i )
{
// ...
}

通过对比我们可以发现,虽然两段代码实现的功能是一样的,但是给代码阅读者的信息却不大相同。第一段代码只是表示这个循环是从0到1024之间,至于为什么是从0到1024,只能让代码阅读者自己去猜测了。第二段代码则通过宏的使用,明确地告诉了我们这个循环是在最小值和最大值之间进行的,这样可以从代码本身获得更加丰富的信息,增加了代码的可读性。

2. 让代码更加易于维护

如果我们在代码中直接多次使用某个常数数字,而恰好这个数字需要修改,那么我们不得不修改代码中所有使用这个数字的地方。而如果是将这个常数定义成宏,并在代码中使用宏来代替这个常数,当我们需要修改这个常数时,只需要修改宏的定义就可以了,而无需修改代码中所有使用这个宏的地方。例如,我们将3.14159这个常数定义成PI这个宏并用它参与计算,当我们需要降低精度使用3.14进行计算时,只需修改PI的定义,将3.14定义成PI即可:

// 修改PI的定义
#define PI 3.14 // 使用3.14作为圆周率计算面积
float fArea = PI * fR * fR;

除了用#define定义的宏可以表示常数之外,C++还提供了const关键字,使用它可以将一个变量修饰成一个数值不可修改的常变量,也可以用来表示程序中的常数。

const关键字的使用非常简单,只需要在定义变量的时候,在数据类型前或后加上const关键字即可:

const 数据类型 常变量名 = 常量值;

这里的const关键字会告诉编译器,这个变量的数值不可修改(或者更严格地说,不可以通过这个变量名直接修改它所表示的数值,而通过其他方式间接地修改是可以的),这就使得这个变量具有了一个常数最基本的特征:不可修改。所以,经过const关键字的修饰,这个变量就成了一个常变量,可以用来表示程序当中的各种常数。需要特别注意的是,因为常变量的值在定义后便不可以修改,所以必须在定义常变量的同时完成它的赋值。例如:

// 定义常变量PI
const double PI = 3.14159; // … // 用常量PI计算面积
float fArea = PI * fR * fR;
// 用常量PI计算周长
float fGirth = * PI * fR;

而在定义之后,如果试图通过这个常变量名来修改它所表示的值,则会导致一个编译错误,以此来保证这个变量的数值不会被修改而成为一个常变量。例如,如果想在程序中降低PI的精度,偷工减料是不行的:

// 错误:不能修改const常变量的值
PI = 3.141;

既然宏和const关键字都可以用来给常数一个名分,那么该如何选择呢?要表示常数的时候,到底是用宏还是用const关键字?我们的回答是:应该更多地选择使用const关键字。比如,要想在程序中表示3.14159这个常数,可以采用以下两种方式:

// 宏方式
#define PI 3.14159 // const方式
const double PI = 3.14159;

这两种方式在语法上都是合法的,在使用上也并没有什么太大区别。但是第二种方式要比第一种方式好,因为如果使用#define将这个常数定义成宏PI,PI会在代码的预编译阶段被预编译处理器替换成3.14159这个常数本身,这样就没有了编译器的数据类型检查, 并且,宏的名称不会出现在符号表中,这样会给代码后期的调试带来麻烦,可能会遇到一个数字,却不知道它从何而来,这就是我们常说的Magic Number(像拥有魔力一样不知从何而来的数字)。而使用const将这个常数表示成一个常变量,它是拥有数据类型的,编译器可以对其进行数据类型检查,避免错误的发生。同时这个常变量名也会出现在程序的符号表中,便于程序的调试。所以,我们总是优先使用const关键字修饰的常变量来表示程序中的常数。


[Chen1]确认

你好,C++(8)如何表达那些始终保持不变的数据量?3.2.2 常量的更多相关文章

  1. 你好,C++(7)第三部分 C++世界众生相 3.2.1 变量的定义与初始化

    第3部分 C++世界众生相 在听过了HelloWorld.exe的自我介绍,完成了与C++世界的第一次亲密接触后,大家是不是都急不可待地想要一试身手,开始编写C++程序了呢?程序的两大任务是描述数据和 ...

  2. 地图POI类别标签体系建设实践

    导读 POI是“Point of interest”的缩写,中文可以翻译为“兴趣点”.在地图上,一个POI可以是一栋房子.一个商铺.一个公交站.一个湖泊.一条道路等.在地图搜索场景,POI是检索对象, ...

  3. Xtrabackup数据全备份与快速搭建从服务器

    Percona Xtrabackup可以说是一个完美的数据备份工具.特别是当数据库的容量达到了一定数量级的时候且存在单表达到几十G的数据量, 很难容忍一些逻辑备份的漫长时间.如单个数据库约200G,单 ...

  4. 洛谷P2556 [AHOI2002] 黑白图像压缩 [模拟]

    题目传送门 黑白图像压缩 题目描述 选修基础生物基因学的时候, 小可可在家里做了一次图像学试验. 她知道:整个图像其实就是若干个图像点(称作像素)的序列,假定序列中像素的个数总是 8 的倍数, 于是每 ...

  5. 洛谷——P2556 [AHOI2002]黑白图像压缩

    P2556 [AHOI2002]黑白图像压缩 题目描述 选修基础生物基因学的时候, 小可可在家里做了一次图像学试验. 她知道:整个图像其实就是若干个图像点(称作像素)的序列,假定序列中像素的个数总是 ...

  6. 数据分析之--log文件自动化分析

    https://mp.weixin.qq.com/s?__biz=MjM5NjE2MTIyMw==&mid=2257483803&idx=1&sn=efe24b040397cd ...

  7. 你好,C++(39)6.4.4 依葫芦画瓢:用C++表达设计结果(下)

    6.4.4  依葫芦画瓢:用C++表达设计结果 完成上面的分析与设计之后,小陈感觉已经成竹在胸胜利在望了.他知道,只要完成了程序中的类以及类之间关系的分析和设计,整个程序就相当于已经完成了一大半.接下 ...

  8. 你好,C++(12)怎样管理多个类型同样性质同样的数据?3.6 数组

    3.6  数组 学过前面的基本数据类型之后,我们如今能够定义单个变量来表示单个的数据.比如,我们能够用int类型定义变量来表示公交车的216路:能够用float类型定义变量来表示西红柿3.5元一斤. ...

  9. 你好,C++(12)如何管理多个类型相同性质相同的数据?3.6 数组

    3.6  数组 学过前面的基本数据类型之后,我们现在可以定义单个变量来表示单个的数据.例如,我们可以用int类型定义变量来表示公交车的216路:可以用float类型定义变量来表示西红柿3.5元一斤.但 ...

随机推荐

  1. Unity C# 游戏间物体间的访问

    脚本语言:C# 1.在Unity工程中新建两个物体:Cube和Sphere 2.分别为Cube和Sphere添加脚本CubeScript和SphereScript: 在SphereScript这两个定 ...

  2. bzoj3721

    不是说好的20s吗,怎么我19s都超时……逗我最后还得写成c++才能过……首先不难发现询问肯定是O(1)的复杂度我们先把奇数和偶数分开排序,不难发现几个性质1. 奇数的个数一定是奇数2. 奇数选取随k ...

  3. 树(最小乘积生成树,克鲁斯卡尔算法):BOI timeismoney

    The NetLine company wants to offer broadband internet to N towns. For this, it suffices to construct ...

  4. Hadoop core-site.xml 配置项列表

    core-default.xml与core-site.xml的功能是一样的,如果在core-site.xml里没有配置的属性,则会自动会获取core-default.xml里的相同属性的值 官方文档: ...

  5. UNIX环境下的共享内存

    好久没更新博客了,最近几个月一直在忙项目,现在终于有时间进一步学习了.这次记录的是unix环境中共享内存的使用方法.  在我理解,共享内存就是在内存中开辟一段空间,各个毫不相干的进程就可以通过访问这段 ...

  6. String拼接也有用加号更好的时候

    做String拼接时用StringBuilder(或StringBuffer)好还是直接用+号性能好?一般来说是前者,不过也有用加号略好的时候.首先我一直认为用+号有很好的可读性,而且当String拼 ...

  7. 【模拟赛】BYVoid魔兽世界模拟赛 解题报告

    题目名称(点击进入相关题解) 血色先锋军 灵魂分流药剂 地铁重组 埃雷萨拉斯寻宝 源文件名(.c/.cpp/.pas) scarlet soultap subway eldrethalas 输入文件名 ...

  8. 深入理解java垃圾回收算法

    Java虚拟机的内存区域中,程序计数器.虚拟机栈和本地方法栈三个区域是线程私有的,随线程生而生,随线程灭而灭:栈中的栈帧随着方法的进入和退出而进行入栈和出栈操作,每个栈帧中分配多少内存基本上是在类结构 ...

  9. Lucene实例教程

    Lucene是apache组织的一个用java实现全文搜索引擎的开源项目. 其功能非常的强大,api也很简单.总得来说用Lucene来进行建立 和搜索和操作数据库是差不多的(有点像),Document ...

  10. 再看C++引用类型

    之前弃用博客园的原因是其不支持markdown语法.到今天偶然进来试了一下,发现Markdown toggle原来是能支持的(不知道是不是因为它升级了),遂重新启用. 在一年前学C++的时候就对引用, ...