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

时间:2022-12-10 05:44:33

第3部分

C++世界众生相

在听过了HelloWorld.exe的自我介绍,完成了与C++世界的第一次亲密接触后,大家是不是都急不可待地想要一试身手,开始编写C++程序了呢?程序的两大任务是描述数据和处理数据。那么,接下来我们将面临的第一个问题就是:如何在C++中描述数据?

3.1  C++中的数据类型

编程就是使用程序设计语言来描述和表达现实世界。现实世界中有很多客观存在的事物,例如,电脑、人、汽车等。我们总是用各种数据来描述这些事物的不同属性,比如,我们用一个字符串“ChenLiangqiao”来描述某个人的名字;用一个数字“175”来描述他的身高。而其中的某些数据往往可以归结为同一类型,比如,描述人的身高和电脑屏幕尺寸的数据都是数值数据,而描述人的名字和汽车牌照的数据都是字符串数据。对应的,在C++中,为了描述这些数据,我们将相同类型的数据抽象成某一数据类型,然后使用这一数据类型定义的变量来表达这类数据。例如,我们将现实世界中的各种整数(表示身高的175,表示屏幕尺寸的21)抽象成C++中的int这种数据类型,然后用int定义的变量来描述某个具体的整数数据。比如,我们可以定义一个int类型的变量nHeight来表示某个人的身高;定义另外一个int类型的变量nSize来表示某台电视机的尺寸。

这里我们也可以看到,相同的数据类型(int)可以定义多个不同的变量(nHeight、nSize),分别用于表示多个不同的具体事物(身高、尺寸)。反过来,表示不同事物的多个变量(nHeight、nSize),也可以是同一数据类型(int)。这就像现实世界中的百家姓一样,姓陈(int)的可以有好多人(nHeight、nSize),而好多人(nHeight、nSize)也都可以姓陈(int)。一个数据的数据类型,决定了这个数据是哪一家的人,而既然是同一家的人,那么这一家人都有着某些相同的特征。比如,它们都占用相同的内存字节数,所能够表示的数据范围都相同等等。如图3-1所示。

在C++中,按照所能够表达数据的复杂程度,数据类型可分为基本数据类型和构造数据类型。

1. 基本数据类型

在现实世界中有很多简单的数据,比如某个数字、某个字符等。为了表达这些简单数据,C++将这些数据分门别类地抽象成了多种基本数据类型。比如,表示人身高的175、表示电视机尺寸的21都是整数,C++将这些整数抽象成int数据类型;表示选择题选项的A、B、C、D都是字符,C++将字符抽象成char数据类型。基本数据类型是C++中最基础的数据类型,是C++世界最底层的民众,都具有自我说明的特点,不再具有可分性。

2. 构造数据类型

现实世界是复杂的,只使用C++所提供的基本数据类型还不能够完全描述复杂的现实世界。比如,我们无法用一个基本数据类型的数据来描述一个矩形,因为矩形有长和宽两个属性需要描述。但是我们发现,复杂事物都是由简单事物组成的,一个复杂的事物可以分解成多个简单事物,而将多个简单事物进行组合,也就构成了一个复杂事物。与现实世界相对应,C++中也提供了结构体和类等组合机制,可以将多个基本数据类型组合起来,构成一个比较复杂的构造数据类型以描述更加复杂的事物。例如,可以将两个简单的基本数据类型int组合起来形成一个新的数据类型Rect,用来描述更复杂的矩形。在C++中,可以使用struct关键字来创建一个新的构造数据类型:

// 创建描述矩形的数据结构Rect
struct Rect
{
int m_nLength; // 表示矩形的长
int m_nWidth; // 表示矩形的宽
}; // 使用Rect构造数据类型定义一个表示矩形的变量r
Rect r;
r.nLength = ; // 让矩形的长为4
r.nWidth = ; // 让矩形的宽为3

一个构造数据类型可以分解成若干个“成员”或“元素”。每个“成员”都是一个基本数据类型或另一个构造数据类型。如果我们将基本数据类型看成是化学中的原子的话,那么,构造数据类型就可以看成是由原子(int)组合而成的分子(Rect)。按照组合的形式不同,C++中的构造数据类型可以分为4种:①数组类型;②结构类型;③联合类型;④枚举类型。这些构造数据类型我们将在稍后的章节中详细介绍。

3.2  变量和常量

C++世界住满了各种数据量。从本质上讲,它们都是保存在某个内存位置上的数据。其中的某些数据需要在程序的运行过程中发生变化。比如,表示一个人身高的数据175,有可能在程序运行过程中变为180。同时,程序中还有另外一类数据量,它们在整个程序的运行过程中始终保持不变。比如表示圆周率的3.14159,在程序运行的任何时刻都不会变化。我们将那些在程序运行过程中可能会发生变化的数据量称为变量,而将那些始终保持不变的数据量称为常量。

3.2.1  变量的定义与初始化

为了保存数据,我们首先需要为它开辟合适的内存空间。同时,对于程序中的变量而言,我们往往需要对其进行多次读写访问。为了便于访问变量,我们往往需要给变量一个名字,然后通过变量名访问它所代表的数据。如果我们想要表达现实世界中的某个具体的可变化的数据,就可以使用这个数据所对应的数据类型,按照如下的语法格式来定义一个变量:

数据类型说明符  变量名;
// 同时定义相同类型的多个变量
// 不推荐的形式 ,多个变量容易让人混淆,代码缺乏可读性
数据类型说明符 变量名1,变量名2,变量名n;

变量定义由数据类型说明符和变量名两部分构成。数据类型是对变量类型的说明,用于指定这个变量是整型、浮点型还是自定义数据类型等,因而它也决定了这个变量的一些基本特征,比如占用的内存字节数、取值范围等。变量名是用来标记变量的符号,就相当于变量的名字一样,我们可以通过变量名对变量所表示的数据进行读写访问。例如,我们想要在程序中表示一个人的可变化的身高数据175,而175这个数据的类型是整数,与之相对应的C++数据类型是int,所以我们选择int作为变量的数据类型。又因为这个变量所表示的是人的身高,所以我们选择nHeight(n表示这是一个整数,Height表示这是身高数据)作为变量名:

// 定义一个int类型的变量nHeight,用来表示身高
int nHeight;

完成这样的变量定义后,就相当于为即将保存的175数据开辟了4个字节的内存空间(int类型的变量在内存中占有4个字节的空间),同时指定了这个变量的名字是nHeight,进而可以通过nHeight这个变量名将175身高数据保存到内存或者是对其进行读写访问:

// 通过nHeight写入数据
// 将175这个身高数据保存到内存
nHeight = ;
// 通过nHeight读取数据
// 将nHeight变量代表的身高数据175显示到屏幕
cout<<"身高:"<<nHeight<<endl;
// 通过变量名将身高数据修改为180
nHeight = ;
// 输出修改后的身高数据
cout<<"修改后的身高:"<<nHeight<<endl;

定义变量时,应注意以下几点。

l  不能用C++关键字作为变量名。比如常见的bool、for、do、case等关键字(在IDE中显示为蓝色等特殊颜色)都不能作为变量名;变量名不能以数字开始。例如,以下变量定义都是错误的:

int  case;  // 错误:case是关键字
int 2member; // 错误:变量名以数字开始

知道更多:C++中到底有多少关键字?

关键字(keyword)又称保留字,是C++在整个语言范围内预先保留的标识符。每个C++关键字都有特殊的含义,用以完成某项特定的功能。比如,int表示整数数据类型;for表示定义一个循环语句;class表示定义一个类等等。因为关键字已经拥有预先定义的含义,所以无法用做标识符(变量名、函数名或类名等)。C++的关键字如下表所示。

表3-1 C++中的关键字

  alignas (C++11 启用)
  alignof
(C++11 启用)
  and
  and_eq
  asm
  auto
  bitand
  bitor
  bool
  break
  case
  catch
  char
  char16_t
(C++11 启用)
  char32_t
(C++11 启用)
  class
  compl
  const
  constexpr
(C++11 启用)
  const_cast
  continue
  decltype
(C++11 启用)
  default
  delete
  do
  double
  dynamic_cast
  else

  enum
  explicit
  export
  extern
  false
  float

for
  friend
  goto
  if
  inline
  int
  long
  mutable
  namespace

new
  noexcept
(C++11 启用)
  not
  not_eq
  nullptr
(C++11 启用)
  operator
  or
  or_eq
  private
  protected
  public
  register
  reinterpret_cast

  return
  short
  signed
  sizeof
  static
  static_assert
(C++11 启用)
  static_cast
  struct
  switch
  template
  this
  thread_local
(C++11 启用)
  throw
  true
  try
  typedef
  typeid
  typename
  union
  unsigned

using
  virtual
  void
  volatile
  wchar_t
  while
  xor
  xor_eq

这里列出了C++中所有的84个关键字,但其中的大部分关键字我们很少用到。对于这些关键字,我们没有必要全部掌握。我们只需要掌握其中最常用的一、二十个,至于其他的关键字,需要用到的时候再去查资料就可以了。

l  允许在一个数据类型说明符后同时定义多个相同类型的变量。各变量名之间用逗号(这里的逗号必须是英文逗号)间隔。例如:

// 同时定义三个int类型的变量,
// 分别表示学生的ID(nStuID),年龄(nAge)和身高(nHeight)
int nStuID,nAge,nHeight;

l  数据类型说明符与变量名之间至少要有一个空格间隔。

l  最后一个变量名之后必须以“;”结尾,表示语句的结束。

l  变量定义必须放在变量使用之前。换句话说,也就是变量必须先定义后使用。

最佳实践:变量定义应尽可能地靠近变量使用的位置

我们知道,变量在定义之后才可使用,也就是变量在使用之前必须先被定义。那么,这个“使用之前”到底“前”到什么程度合适呢?是“使用之前”的1行代码恰当还是100行代码合适?面对这个问题,我们并没有一个固定的标准答案,但是我们有一个应当遵循的原则:变量定义应尽可能地靠近变量使用的位置。

如果变量定义的位置和变量实际使用的位置相距太远,则这中间可能会发生很多事情。比如,程序可能中途退出,定义的变量并没有得到使用而白白浪费;也可能在中间被错误地使用而给程序带来难以发现的问题。另外一方面,如果两者相距太远,我们在使用一个变量的时候,却难以找到它定义的位置,从而无法轻易地得知这个变量的数据类型等基本信息,影响我们对变量的使用。所以,为了避免这些可能存在的麻烦,一个简单而有效的方法是,尽可能地推迟变量定义的时机,尽可能地靠近其实际使用的位置。

在定义变量的时候,除了确定变量的数据类型之外,另外一个重要的工作就是给变量取一个好名字。一个人如果有个好名字,就很容易给人留下良好而深刻的印象,而变量的名字也一样。合适的变量名包含跟变量相关的信息,可以自我解释,让人更容易理解和使用,从而提高代码的可读性。那么如何给变量取一个合适的名字呢?比较下面这四个变量名:

// 记录学生数量的变量
int nStuNum;
int N;
int theNumberofStudent;
int xssl;

这四个变量都是用来表示学生数量的。如果要问这四个变量名哪个最好,大家肯定会说第一个变量最好,因为第一个变量名一看就知道是用来表示学生数量的。而其他几个,都有各自的缺点:第二个太短,不知道这个变量的具体含义;第三个太长,书写繁琐;第四个使用汉语拼音的首字母缩写,更是让人一头雾水。

好的变量名可以恰当地解释变量所表示的意义,无需过多的注释或文档,整个代码就清晰可读,可以做到“望文生义”。要为变量取一个好名字,通常要遵循某种命名规则。目前业界比较流行的命名规则当属微软公司提倡的“匈牙利命名法”。在匈牙利命名法中,一个变量名主要由三部分构成:

变量名 = 属性 + 类型 + 对象描述

其中,属性通常用来对这个变量的一些附加信息进行说明。例如,我们通常用“m_”前缀表示这个变量是某个类的成员(member)变量,而使用“g_”前缀表示这是一个全局(global)变量;类型表示这个变量的数据类型,通常用各种数据类型的缩写表示,例如我们通常用n表示int类型,用f表示float类型等;而对象描述就是对这个变量含义的说明了,它通常是一个名词。将这三个部分组合起来,就构成了一个完整的变量名,可以表达丰富的关于这个变量的信息。例如,某个变量的名字是“m_unAge”,一看变量名就知道这个变量表达的意义就是:这是某个类的成员变量(m_),它的数据类型是unsigned int(un),而它是用于描述这个类的年龄(Age)属性的。

知道更多:匈牙利命名法中的“匈牙利”是怎么来的?

匈牙利命名法是由一位叫 Charles Simonyi 的匈牙利程序员首先提出的,后来他在微软公司工作了数年,因为这种命名法用很少的文字很好地概括了变量的最重要信息,因而受到微软公司的认同并逐渐在微软内部流行起来。又因为微软在业界的强大影响力,匈牙利命名法也开始通过微软的各种产品和文档资料向全世界传播开,逐渐成为业界最为流行的变量命名方法。对于大部分程序员而言,无论自己使用的是何种开发语言,或多或少都会使用这种变量命名法。

这种命名法之所以被称为“匈牙利命名法”,就是为了纪念这位发明者所来自的国家。

使用匈牙利命名法,可以在一个变量名内表达丰富的信息,在一定程度上提高了代码的可读性。但是它也有一个最大的缺点——繁琐。有时候,一些过长而又意义不大的前缀为变量名增加了额外的负担。而这也是它并没有受到全世界所有程序员全都使用的原因。世界上并无所谓最好的命名规则。在实践中,可以根据业界流行的一些共性规则,再结合项目的实际情况来制定一种令大多数项目成员都满意的命名规则,并在项目中贯彻实施。“最合适”的规则就是“最好”的规则了。经过实践的检验,业界流行的一些共性命名规则主要有以下几点。

1. 简单易懂

变量名应当直观,方便拼读,可望文而生义。变量名最好采用英文单词或组合,便于记忆和阅读;切忌使用汉语拼音来命名,因为这样的变量名,只有你一个人能看懂;程序中的英文单词不宜太过复杂,用词应当尽量做到地道、准确。例如,把表示“当前数值”的变量命名为“fNowVal”,虽然能够表达变量的含义,但远没有“fCurVal”来得地道。

2. 最短长度,最大信息量

通常,编译器对变量名的长度没有限制。一般来说,长名字能更好地表达变量的含义,所以C++中的变量名长达十几个字符也不足为奇。既然没有限制,那么变量的名字是不是越长越好呢?不见得。只要能够完整地表达变量的含义,变量名应该越简单越好。例如,同样是表示最大值,变量名“max”就要比“maxValueUntilOverflow”好用,因为它用最短的长度表达了最大的信息量。

3. 作用范围越大,变量名越长

关于变量名的长度,我们还可以记住这样一条简单的规则:变量名(或者后文将介绍的函数名等)的长度与它的作用域的大小成正相关。所谓的作用域,也就是某个标识符(变量名或者函数名)发生作用的代码范围,具体介绍可以参考后继的7.3.3小节。换句话说,也就是如果一个变量的作用域比较大,那么在这个作用域内的变量就会比较多,为了避免冲突便于区分,变量名就应该比较长。反之亦然。例如,在一个函数内部,我们可以用一个简单的i来给一个局部变量命名,而在全局范围内,再使用i来给一个全局变量命名就不太合适了。

4. 变量名由名词构成

变量表示的是现实世界中的一个具体事物,其本质是一个数据实体,所以变量名的核心应该是一个名词,因而它应当使用单独的一个“名词”或者“形容词+名词”的组合形式。例如:

float fWeight;            // 名词,表示某个人的体重
float fLastWeight; // 形容词 + 名词,表示上一次的体重
float fCurWeight; // 形容词 + 名词,表示当前体重

4. 不要使用数字编号

尽量避免变量名中出现数字编号,如“Value1”、“Value2”等,除非逻辑上的确需要编号。

5. 常量大写

常量是某一类特殊的变量,它的特殊性就在于它不可修改。这种特殊性体现在命名上,就是我们通常用大写字母表示变量名,如果常量名中有多个单词,则用下划线加以分割。例如:

const float PI = 3.14159;  // 用const关键字定义一个float类型的常量PI
const int MAX_LEN = ; // 用下划线分割常量名

6. 使用约定俗成的前缀

约定俗成的一些变量名前缀,可以很好地解释变量的某些属性,让变量的含义一目了然。例如:变量加前缀s_,表示静态(static)变量;变量加前缀g_,表示全局(global)变量;变量加前缀m_,表示成员(member)变量。

当完成变量的定义后,系统会为这个变量分配内存空间,进而我们可以通过变量名对这块内存进行读写访问,将数据保存到内存或者是从内存读取数据。但是,在真正使用变量之前,我们往往还需要对其进行合理的初始化。这是因为变量定义后,如果不进行初始化,则系统会给定一个不确定的随机值作为其初始值。而根据编译器和目标平台的不同,这个随机值则可能有所不同。这样就可能导致同一程序在不同平台上行为的不一致,带来移植问题。同时,如果不小心使用了这个随机值进行操作,则可能导致程序运行结果出错,甚至程序崩溃,那就是一场灾难了。而变量初始化会给变量一个合理的初始值,可以很好地避免上面的这些问题。所以,在学习C++的一开始,就应该养成“在定义变量的同时进行初始化”的好习惯。

那么,我们该如何进行变量的初始化呢?

第一种方式,可以在定义变量的同时,使用“=”赋值符将合适的初始值赋值给这个变量。例如:

// 定义一个int类型的变量nHeight,并利用“=”将其值初始化为175
int nHeight = ;

第二种方式,就是在定义变量时在变量名之后用“()”给出初始值,系统会用这个初始值完成变量的创建,从而完成初始化工作。例如:

// 通过“()”将其值初始化为175
int nHeight();

除了以上两种方式之外,在 C++11标准中,我们还可以利用一对大括号“{}”表示的初始化列表(initializer list)在定义变量时完成变量的初始化工作。例如:

//通过初始化列表将其值初始化为175
int nHeight{};

最佳实践:为什么要使用初始化列表?

到这里,大家很自然地会提出这样一个问题:C++中已经有“=”和“()”可以完成变量的初始化了,为什么还要使用初始化列表来进行变量的初始化?

初始化列表是C++11标准新引入的一个特性,除了统一变量初始化的形式之外,它还带来另外一个好处:它可以预防变量初始化时的数据类型截断,防止数据精度丢失。所谓的数据类型截断,简而言之,就是在使用某种精度较高的数据类型(例如,double)的数据对另一种精度较低的数据类型(例如,int)的变量进行赋值时,C++会进行隐式的类型截断以满足类型转换的需要。例如:

int x = 7.3;    // 一个double类型的数据7.3被截断成int类型的数据7

在编译上面的代码时,虽然在这个过程中丢失了0.3这个数据,但编译器不会给出任何错误或者警告信息。但是,在C++11中,如果使用初始化列表“{}”来进行初始化,编译器则会对这种数据类型截断发出警告,提示用户数据精度的丢失。例如:

// 警告:用double类型的数据初始化int类型的变量会产生类型截断,丢失数据精度
int x1 = {7.3}; // 正确:虽然7是一个int类型的数据,但是可以使用char类型精确地表达,
// 因而不会导致数据类型截断而丢失精度错误
char x2{};

在C++中,如果一个初始值可以被精确地表达为目标类型,那么就不存在数据类型截断。但请注意,double类型至int类型的转换通常都会被认为是数据类型截断,会产生精度的丢失,即使是从7.0转换至7。初始化列表对于类型转换的处理增强了C++静态类型系统的安全性。传统的依赖于编程人员的初始化类型安全检查,在 C++11中,通过初始化列表由编译器实施,这样会减轻编程人员的负担,也更加安全。所以,如果可以,应该尽可能地使用初始化列表来完成变量的初始化。

这里需要特别指出的是,我们不能使用初始化列表对auto 类型(一种特殊的数据类型,它相当于一种数据类型的占位符。用它作为数据类型定义变量时,它并不具体地指定变量的数据类型,变量的真实数据类型将在编译时根据其初始值自动推断而得。在稍后的3.5.2小节中,我们将详细加以介绍)的变量进行初始化。如果那样的话,这个变量的类型会被编译器检测为初始化列表类型,而不是初始化列表中真正的初始值的类型。

知道更多:用户自定义数据标识(User-defined literals)创建特殊数据

在表示数据的时候,C++提供了许多内建的数据类型的数据标识供我们使用,这样我们在表达数据的时候,加上相应的数据标识,将使得我们要表达的数据更加准确而直观。例如:

1.2    // 默认double双精度浮点型
1.2F // F指定float单精度浮点型
19821003ULL // ULL表示unsigned long long 64位无符号长整型

除了这些内建的数据标识之外,C++11还通过在变量后面加上一个用户自定义的后缀来标定所需的数据类型以支持“用户自定义数据标识”,例如:

// 定义表示英寸的数据标识inch
constexpr double operator"" _inch(const long double in)
{
return in*0.0254; // 将英寸单位换算成米单位
}

在这里,我们实际上定义了一个操作符(必须以“_”开始),它以操作符之前的数据为参数,参数类型就是相应的数据取值范围最大的类型。例如,表示整型数的是unsigned long long、表示浮点数的是long double。当然,也可以将整个数据作为一个字符串,以const char*的参数形式传入。传入的参数在经过一定的处理后,返回的就是这个数据加上数据标识后表示的真实数据。这样,我们就可以直接在程序中使用“_inch”这个用户自定义的数据标识,直接定义以英寸为单位的数据。例如:

// 定义一台电视机的尺寸为54英寸,
// 然后换算成以米为单位的数据赋值给变量tvsize
double tvsize = .0_inch;
cout<<"54 inch = "<<tvsize<<" m"<<endl;

在上面这段代码中,我们使用“_inch”数据标识直接定义了电视机的尺寸。当编译器在编译这段代码时,首先会分析为变量赋值数据的后缀,然后将后缀之前的数据作为参数,调用定义数据标识的函数,并将函数的返回值作为这个数据的真实数值。用户自定义数据标识机制只是简简单单的允许用户制定一个新的后缀,并决定如何对它之前的数据进行处理。在其中,我们可以进行单位的换算,例如将英寸换算成米,也可以将其构造成新的数据类型,例如将一个二进制数构造成十进制数等等。

因为我们在定义数据标识的时候使用了constexpr关键字,constexpr关键字的作用是实现在编译时期进行预处理计算,所以在编译的时候,编译器会直接将这个以英寸为单位的数据换算成以米为单位的数据,并赋值给相应的变量。

通过这样的方式,我们可以直接在代码中表示各种不同类型的数据,例如英寸长度、角度、二进制数等等,使得我们在使用不同类型的数据的时候将更加直观,也更加方便,更加人性化。