通过“你好,C++.exe”程序的演示,与C++完成了第一次亲密接触后,读者是不是迫不及待地想要一试身手,开始编写C++程序呢?程序的两大任务是描述数据和处理数据。本章面临的第一个问题就是:如何在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所示。
图3-1 C++的和谐大家庭
在C++中,按照所能够表达的数据的复杂程度,数据类型可分为基本数据类型和构造数据类型。
在现实世界中有很多简单的数据,例如数字、字符等。为了表达这些简单数据,C++将这些数据分门别类地抽象成多种基本数据类型。例如,表示人身高的175、表示屏幕尺寸的21都是整数,C++将这些整数抽象成int数据类型;表示选择题选项的A、B、C、D都是字符,C++将字符抽象成char数据类型。基本数据类型构成了C++中最基础的数据结构,它们具有自描述性,即类型名称直接反映了其存储的数据种类,并且这些类型是不可分割的,是C++数据类型体系中的最简单形式。
现实世界的复杂性意味着,仅使用C++提供的基本数据类型往往不足以完全描述它。例如,一个矩形具有长度和宽度两个属性,这无法通过单一的基本数据类型来表达。然而,正如复杂事物可以分解为多个简单事物一样,C++提供了结构体(struct)和类(class)等构造机制,允许将多个基本数据类型组合起来,以创建更复杂的数据类型。
这些构造类型可以捕捉到现实世界实体的多个方面。例如,我们可以将两个int类型的基本数据组合起来,形成一个新的数据类型Rect,用以描述矩形的复杂性。在C++中,我们使用struct关键字来定义这样的构造数据类型:
一个构造数据类型可以分解成若干“成员”。每个成员可以是基本数据类型,或者另一个构造数据类型。如果我们将基本数据类型看作化学中的原子,那么构造数据类型就可以看作由这些原子(如int)组合而成的分子(如Rect)。
在C++中,存在着各种各样的数据项。从本质上讲,它们都是存储在内存中的值。根据它们在程序执行过程中的特性,可以区分为变量和常量两种类型。
变量是那些在程序运行过程中可能会发生变化的数据项。例如,表示一个人体重的值可能从60变化到65。变量允许在程序中被重新赋值,以反映状态的变化。
常量则是那些在程序的整个运行过程中始终保持不变的数据项。例如,圆周率π的近似值3.14159是一个常量,它在程序的任何时刻都不会改变。常量用于表示那些不应该被修改的值,提供了一种确保数据不变的机制。
为了保存数据,首先需要为它开辟合适的内存空间。同时,我们往往还需要对数据进行多次读写访问。为了便于访问这些数据,我们需要给这些内存空间起一个名字,这就是变量名。通过变量名,我们可以访问它所代表的数据。如果我们想要表达现实世界中的某个具体可变的数据,可以使用该数据所对应的数据类型,并按照如下的语法格式来定义一个变量:
数据类型说明符 变量名; // 同时定义相同类型的多个变量 // 不推荐的形式,多个变量容易让人混淆,代码缺乏可读性 数据类型说明符 变量名1,变量名2,变量名n;
变量定义由数据类型说明符和变量名两部分构成。数据类型是对变量类型的说明,用于指定这个变量是整型、浮点型还是自定义数据类型等,因而它决定了这个变量的一些基本特征,例如占用的内存字节数、取值范围等。变量名是用来标记变量的符号,相当于变量的名字一样,我们可以通过变量名对变量所表示的数据进行读写访问。
例如,我们想要在程序中表示一个人的可变化的体重数据60,而60是整数,与之对应的C++数据类型是int,所以我们选择int作为变量的数据类型。又因为这个变量表示的是人的体重,所以选择nWeight(n表示这是一个整数,Weight表示这是体重数据)作为变量名:
// 定义一个int类型的变量nWeight,用来表示体重 int nWeight;
完成这样的变量定义后,就相当于为即将保存的60数据开辟了4字节的内存空间(因为int类型的变量在内存中占据4字节的空间)。同时指定了这个变量的名字是nWeight,因此可以通过nWeight这个变量名将60这个数据保存到内存中,也可以对其进行读写访问:
// 通过nWeight写入数据 // 将60这个体重数据保存到内存中 nWeight = 60; // 通过nWeight读取数据 // 将nWeight变量代表的体重数据60显示到屏幕上 cout<<"体重:"<<nWeight<<endl; // 通过变量名将体重数据修改为65 nWeight = 65; // 输出修改后的体重数据 cout<<"修改后的体重:"<<nWeight<<endl;
定义变量时,应该注意以下几点。
● 不能使用C++关键字作为变量名,例如常见的bool、for、do、case等关键字都不能作为变量名。另外,变量名不能以数字开始。以下是一些错误的变量定义示例:
int case; // 错误:case是关键字 int 2member; // 错误:变量名以数字开始
● 允许在一个数据类型说明符后同时定义多个相同类型的变量。各变量名之间用逗号(这里的逗号必须是英文逗号,即半角逗号)间隔。例如:
// 同时定义三个int类型的变量 // 分别表示学生的ID(nStuID)、年龄(nAge)和身高(nHeight) int nStuID,nAge,nHeight;
● 数据类型说明符与变量名之间至少要有一个空格间隔。
● 最后一个变量名之后必须以分号“;”结尾,表示语句的结束。
● 变量定义必须放在变量使用之前。换句话说,就是变量必须先定义后使用。
我们明白,变量在被定义之后才能使用,也就是说,在使用变量之前必须先进行定义。但是,“使用之前”具体到什么程度才合适呢?是提前一行代码还是一百行代码呢?对于这个问题,我们并没有一个标准的答案,但我们应遵循一个原则:变量的定义应尽可能地接近其使用的位置。
如果变量的定义位置与实际使用位置距离太远,可能会引发一些问题。例如,程序可能在中途退出,导致已定义的变量未被利用而浪费;或者在中间阶段被错误地使用,给程序带来难以发现的问题。另一方面,如果两者相距太远,我们在使用变量时可能难以找到其定义的位置,从而无法方便地获取该变量的数据类型等基本信息,影响我们对变量的使用。
因此,为了避免这些可能出现的问题,一种简单有效的方法是尽可能推迟变量的定义,使其尽可能地靠近实际使用的位置。
在定义变量时,除要确定变量的数据类型外,另一个关键任务就是为变量取一个恰当的名称。就像一个人拥有一个好名字能够轻易地给人留下良好且深刻的印象一样,一个好的变量名同样至关重要。一个合适的变量名可以自我解释,包含与变量相关的信息,使得其他人更容易理解其用途,从而提高代码的可读性。那么,如何为变量取一个合适的名字呢?请比较以下4个变量名:
// 记录学生数量的变量 int nStuNum; int N; int theNumberofStudent; int xssl;
这4个变量均用于表示学生数量。若询问哪个变量名最为适宜,显然,大多数人会倾向于第一个变量名,因其直观地表明了它是一个用于存储学生数量的整型(int)变量。相比之下,其他变量名各有瑕疵:第2个变量名过短,难以明确其具体含义;第3个变量名过于冗长,导致书写烦琐;第4个变量名采用汉语拼音首字母缩写,使人难以理解。
好的变量名应当能够恰当地表达变量的含义,无须额外的注释或文档,使代码易于阅读,实现“望文生义”的效果。为变量命名时,通常应遵循一定的命名规则,目前广泛采用的是微软公司推荐的“匈牙利命名法”。依据匈牙利命名法,变量名主要由以下三部分组成:
变量名=属性+类型+对象描述
其中,属性通常用来对这个变量的一些附加信息进行说明。例如,我们通常用m_前缀表示这个变量是某个类的成员(member)变量,而使用g_前缀表示这是一个全局(global)变量。类型表示这个变量的数据类型,通常用各种数据类型的缩写表示,例如我们通常用n表示int类型,用f表示float类型等。对象描述就是对这个变量含义的说明,通常是一个名词。将这三个部分组合起来,就构成了一个完整的变量名,可以表达丰富的关于这个变量的信息。例如,某个变量的名字是m_unAge,一看变量名就知道这个变量表达的意义是:这是某个类的成员变量(m_),它的数据类型是unsigned int(un),而它用于描述这个类的年龄(Age)属性。
匈牙利命名法由来自匈牙利的程序员Charles Simonyi首创。Simonyi后来在微软公司工作了多年,由于这种命名法能够用较少的字符精确地描述变量的主要信息,因此得到了微软的认可,并在公司内部得到广泛应用。借助微软在软件行业中的强大影响力,匈牙利命名法通过微软的多个产品和文档逐渐传播至全球,成为一种广受欢迎的变量命名标准。对于许多程序员来说,无论他们使用哪种编程语言,都或多或少会接触到这种命名方法。
这种命名法之所以被称为“匈牙利命名法”,是为了向其发明者Charles Simonyi的祖国匈牙利致敬。
匈牙利命名法通过在变量名中包含丰富的信息,能够在一定程度上提高代码的可读性。然而,这种方法也有其明显的缺点—烦琐。一些情况下,过长的前缀可能会使变量名变得复杂,增加了理解和维护的负担。这种烦琐性也是它没有被全世界所有程序员普遍采用的原因之一。
世界上并没有所谓的“最好”的命名规则。在实践中,我们可以根据业界的一些通用规则,并结合项目的具体需求来制定一种命名规则。这种规则应当能够获得项目团队成员的广泛认可,并在整个项目中得到一致的执行。“最合适”的规则,考虑到团队的偏好和项目的特点,往往就是“最好”的规则。
经过实践的检验,业界流行的一些共性命名规则主要包括以下几点。
变量名应当直观,方便拼读,可望文而生义。变量名最好采用英文单词或组合,便于记忆和阅读。切忌使用汉语拼音来命名,因为这样的变量名,只有编写代码的人才能看懂。程序中的英文单词不宜太过复杂,用词应当尽量做到地道、准确。例如,把表示“当前数值”的变量命名为fNowVal,虽然能够表达变量的含义,但是远没有fCurVal地道。
通常,编译器对变量名的长度没有限制。一般来说,长名字能更好地表达变量的含义,所以C++中的变量名长达十几个字符也不足为奇。既然没有限制,那么变量的名字是不是越长越好呢?不见得。只要能够完整地表达变量的含义,变量名应该越简单越好。例如,同样是表示最大值,变量名max就要比maxValueUntilOverflow好用,因为它用最短的长度表达了最大的信息量。
关于变量名的长度,有一条简单的规则可供参考:变量名的长度通常与其作用域的大小成正比。所谓作用域,是指某个标识符(如变量名或函数名)在代码中起作用的范围。简而言之,如果一个变量的作用域较广,那么在该作用域内可能存在的变量数量也较多,为了避免命名冲突并便于区分,变量名应当相对较长。相反,如果作用域较小,则可以使用较短的变量名。例如,在函数内部,可以使用简单的字母n作为局部变量名;而对于全局变量,若仍使用n作为名称就可能不太适宜,因为全局变量的作用域覆盖了整个程序,更易于与其他变量名冲突。
变量表示的是现实世界中的一个具体事物,其本质是一个数据实体,所以变量名的核心应该是一个名词,因而它应当使用单独的一个“名词”或者“形容词+名词”的组合形式。例如:
float fWeight; // 名词,表示某个人的体重 float fLastWeight; // 形容词 + 名词,表示上一次的体重 float fCurWeight; // 形容词 + 名词,表示当前体重
尽量避免变量名中出现数字编号,如Value1、Value2等,除非逻辑上的确需要编号。
常量是某一类特殊的变量,它的特殊性在于它的数值在程序执行期间不可修改。在命名上,通常使用大写字母作为常量名,如果常量名由多个单词组成,一般使用下画线进行分隔。例如:
const float PI = 3.14159; // 用const关键字定义一个float类型的常量PI const int MAX_LEN = 1024; // 用下画线分隔常量名
约定俗成的一些变量名前缀可以很好地解释变量的某些属性,让变量的含义一目了然。例如,变量名加前缀s_,表示静态(static)变量;变量名加前缀g_,表示全局(global)变量;变量名加前缀m_,表示成员(member)变量。
当完成变量的定义后,系统会为这个变量分配内存空间,使我们能够通过变量名对这块内存进行读写访问,将数据保存到内存或者从内存读取数据。然而,在真正使用变量之前,我们往往还需要对其进行合理的初始化。这是因为,如果变量定义后未进行初始化,则系统会给定一个不确定的随机值作为其初始值。根据编译器和目标平台的不同,这个随机值会有所差异,可能导致同一程序在不同平台上的行为不一致,从而带来程序的移植性问题。如果不小心使用了这个随机值进行操作,可能会导致程序运行结果出错,甚至导致程序崩溃,那就是一场灾难了。相反,通过变量初始化,我们可以为变量赋予一个合理的初始值,有效地避免上述问题。因此,在学习C++时,应养成“在定义变量的同时进行初始化”的良好习惯。
那么,我们应该如何进行变量的初始化呢?
第一种方式,在定义变量的同时,使用“=”赋值符将合适的初始值赋给这个变量。例如:
// 定义一个int类型的变量nHeight,并利用“=”将其值初始化为175 int nHeight = 175;
第二种方式,在定义变量时,在变量名之后使用初始化列表“()”给出初始值,系统会使用这个初始值来创建变量并完成初始化工作。例如:
// 通过“()”将其值初始化为175 int nHeight(175);
除以上两种方式外,在最新的C++标准C++23中,我们还可以利用一对花括号“{}”表示的初始化列表(initializer list)在定义变量时完成变量的初始化工作。例如:
//通过初始化列表将其值初始化为175 int nHeight{175};
到这里,大家很自然地会提出这样一个问题:C++中已经有“=”和“()”可以完成变量的初始化了,为什么还要使用初始化列表“{}”来进行变量的初始化?
C++11标准引入的初始化列表特性,不仅统一了变量初始化的形式,还带来了一个额外的好处:它有助于防止在变量初始化过程中可能出现的数据类型截断和精度丢失问题。
数据类型截断指的是在将一个精度较高的数据类型(如double)赋值给一个精度较低的数据类型(如int)时,隐式地丢弃超出目标类型范围的数值部分。这种截断可能会导致数据精度的损失。例如:
int x = 9.22; // 一个double类型的数据9.22被截断成int类型的数据9
在编译上面的代码时,尽管小数部分0.22被丢失,但编译器通常不会给出任何错误或者警告信息。然而,在C++11中,如果使用初始化列表“{}”来进行初始化,编译器会对这种数据类型截断发出警告,提示用户数据精度的丢失。例如:
// 警告:用double类型的数据初始化int类型的变量会产生类型截断,丢失数据精度 int x1 = {9.22}; // 正确:虽然9是一个int类型的数据,但是可以使用char类型精确地表达 // 因而不会导致数据类型截断而丢失精度的错误 char x2{9};
在C++中,如果一个初始值可以被精确地表达为目标类型,则不存在数据类型截断的问题。但请注意,从double类型到int类型的转换通常会导致精度丢失,即使是从9.0转换到9。
初始化列表对于类型转换的处理时增强了C++静态类型系统的安全性。传统的初始化类型安全检查依赖于程序员,但在C++11中,通过初始化列表由编译器实施,这样可以减轻程序员的负担,提高代码的安全性。因此,在可能的情况下,推荐使用初始化列表来完成变量的初始化。需要指出的是,不能使用初始化列表对auto类型进行初始化。Auto类型是一种特殊的数据类型,相当于一种数据类型的占位符。作为数据类型定义变量时,不具体地指定变量的数据类型,而是在编译时根据其初始值自动推断它的数据类型。在稍后的3.5.2节中,将详细介绍这一内容。这样变量的类型会被编译器检测为初始化值的类型,而不是初始化列表中初始值的类型。
与变量用于表示程序中可能改变的数据不同,C++中使用常量来表示那些一旦定义就不变的数据。常量包括直接使用的数值、字符、字符串以及使用const关键字声明的变量。通常情况下,常量是一次性读取的,因此它们不需要名称,也无须事先定义就可以直接使用。由于常量值一旦设置就不能更改,它们经常用于为变量赋值或参与运算。例如:
// 用常量180对变量nHeight赋值 nHeight = 180; // 直接使用常量3.14159进行计算 fArea = fR * fR * 3.14159;
这里的180和3.14159就是两个常量,分别用来对变量nHeight进行赋值和参与乘法运算。这样的常量只能使用一次,当完成赋值操作和乘法运算后,这两个常量也就不再被引用了。
C++中的常量主要包括数值常量(整型常数、浮点型常数等)、字符常量以及字符串常量。
整型常数是以文字形式出现的整数。整型常数的表示形式最常见的是十进制,也可以根据需要采用八进制或十六进制表示。在程序中,我们可以根据数字的前缀来区分各种进制的整数。
● 十进制整数:没有前缀,例如0、123、-1等。
● 八进制整数:以0为前缀,数字只包含0~7(不包含8和9),例如0123、-022等。
● 十六进制整数:以0x或0X为前缀,数字包含0~9十个数字,还包括A~F六个英文字母(大小写均可),例如0x123、-0X2A等。
在程序中,我们可以直接使用以下三种方式来表示某个整数数值:
nHeight = 175; // 十进制常数 nHeight = 0257; // 八进制常数 nHeight = 0xAF; // 十六进制常数
上面的代码分别采用不同进制形式的常数对一个变量赋值。虽然这些常数的表现形式不同,但它们所代表的数值都是175。
浮点型常数是以文字形式出现的浮点数,也就是我们通常所说的小数。浮点数有两种表示形式:小数形式和指数形式。小数形式就是我们通常使用的小数书写形式,由数字和小数点构成,如1.0、0.1、.123等。而指数形式则是用科学记数法,将一个浮点数表示为一个小数与10的多少次方的乘积的形式。当一个浮点数较大或者较小时,使用指数形式表示浮点数更加方便。例如,1.3e9表示1.3×10 9 ,也就是1300000000,0.123E-4表示0.123×10 -4 等。
字符常量就是程序中使用的单个字符,如“a”“A”“!”等。在C++中,我们使用单引号(' ')来表示一个字符常量。例如:
// 用一个字符常量对变量aMark赋值 char aMark = 'A'; // 输出一个字符常量‘!’ cout<<'!'<<endl;
除上述常见的可在屏幕上显示的字符外,C++还允许使用一类特殊的字符常量。这些字符无法通过键盘直接输入,也不能直接输出显示到屏幕上,但是可以用来表示一些特殊的控制意义,例如计算机响铃(\a)、换行(\n)、回车(\r)等。这些字符都以“\”开始,表示将“\”符号后的字符转换成其他的含义,所以这些字符也被称为转义字符。表3-1列出了C++中常用的转义字符。
表3-1 C++中常用的转义字符
(续表)
转义字符的使用方法与可显示字符相似,可以把转义字符放到一个字符串中,让它完成相应的控制功能,也可以单独输出某个转义字符,例如:
字符串常量就是由一对双引号(" ")作为起止符的字符序列,如"你好,C++!"。注意,因为双引号是字符串的界限符,所以想在字符串中使用双引号,就要使用转义字符来表示。另外,值得提醒的是,这里的双引号必须是英文的字符(""),即半角字符。因为英文双引号(" ")在形式上与中文双引号(“”)非常相似,所以常常被初学者误用而导致错误。例如:
在字符串常量中,我们可以使用反斜杠(\)这个转义字符来引入一些特殊字符,实现特殊的输出目的。然而,这却给正则表达式的书写带来了麻烦,因为在正则表达式中,反斜杠被用于表示字符的特殊符号,并且使用非常频繁。如果想在正则表达式中表示反斜杠这个字符,我们不得不使用两个反斜杠来表示一个反斜杠字符。例如,我们要表达“被反斜杠(\)分隔开的两个单词”这样一个模式(\zeng\\\mei),在C++代码中就成了如下形式:
string s = "\\zeng\\\\\\mei"; // 这样的表示很不直观,且容易出错
我们注意到,在正则表达式中,反斜杠字符被表达为两个反斜杠的组合。为了表示一个反斜杠,我们必须在正则表达式中使用两个反斜杠来表示。第一个反斜杠表示这是一个转义字符,第二个反斜杠才表示真正的反斜杠。这样的表达方式会让字符串变得非常复杂烦琐,很不直观,即使是经验丰富的程序员也很容易出错。
为了解决这个问题,C++的最新标准引入了原生字符串的机制,并使用原生字符串标识符R来表示一个原生字符串。在原生字符串中,每个字符都代表其最原始的字符意义,所见即所得。换句话说,也就是反斜杠(\)不再具有转义字符的作用,一个反斜杠仅用一个反斜杠字符就可以表示。因而,上述例子可以简化为:
string s = R"(\zeng\\mei)"; // 使用R"()"表示的原生字符串
原生字符串的R"(...)"记法相比普通字符串的"..."记法稍显冗长,但它的意义在于可以让字符串中的转义规则无效,所写即所得,所见即所得。当我们需要在字符串中频繁地表示各种特殊符号(如反斜杠、引号等)时,原生字符串将非常简便,而这一点点书写上的冗长也是值得的。
无论是数值常量还是字符串常量,它们都像C++世界的“雷锋”,只做好事而不留名字。然而,这也带来了一个麻烦:当我们在程序中需要重复多次使用某个常量时,就不得不一遍又一遍地在代码中书写相同的常量。例如,要编写一个有关圆面积和周长的计算程序,无疑会多次用到3.14159这个浮点常数:
这样的代码不仅在书写时非常困难(多次重复书写同一个小数,难以保证正确性和一致性),而且在后期维护时也非常麻烦(如果需要改变这个常数,就必须修改所有用到这个常数的地方)。这些问题都是因为常量在程序中无名无份,每次直接使用所致。解决办法是给常量取一个名字,这样我们可以通过这个名字方便地多次重复使用同一个常量。在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进行相应的计算。例如,上面的代码可以简化为:
这里,使用PI代替原本应该使用的3.14159同样可以完成计算。那么,宏是如何做到这一点的呢?宏PI并不是真正地具有了它所代表的3.14159这个常数的值,它本质上只是一种替换。在编译器对代码进行编译之前,会将代码中所有出现的PI替换为它所代表的内容,换言之,上面代码中的PI会被替换为3.14159,最终参与编译的代码实际上仍旧是:
从这里可以看到,宏的使用并没有减少代码中常数出现的次数,但是它用一种巧妙的方法减少了重复输入某个常数的烦琐操作,同时避免了可能发生的书写错误。
除减少代码重复和避免书写错误外,宏的使用还会给我们带来以下额外好处。
1.让代码更简洁明了,更具可读性
一个意义明确的宏名称往往比一个复杂而无意义的常数数字包含更丰富的信息,可以提高代码的可读性和可维护性;同时,使用宏可以比使用常数更简单,使代码更简洁。对比下面两段代码:
代码段1:
代码段2:
通过对比我们可以发现,虽然两段代码实现的功能是一样的,但是代码阅读者获取的信息却不大相同。第一段代码只是表示这个循环在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常变量的值 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关键字修饰的常变量来表示程序中的常数。
从每天早上醒来的那一刻起,我们就开始不断地与数字打交道:从清晨闹钟响起的6点30分,到乘坐编号为800的公交车上班;从新闻报道的房价跌至每平方米100元,到回家时购买的西红柿3.5元一斤,我们的日常生活与数字息息相关。程序作为现实世界的一种抽象和表达方式,自然也需要能够处理和表达这些数字。为此,C++提供了多种数值数据类型,从表示整数的int类型到表示小数的float类型,从表示数值范围较小的short类型到数值范围较大的double类型。凭借这些数据类型,我们可以在程序中定义变量来精确地表达和操作现实世界中的数字信息。
在现实世界中,最常见的数字应该是各种大大小小的整数了,而在C++中,我们用整型数值类型来表示现实世界中的整数。根据这些数据类型所占用的内存资源的多少和取值范围的不同,整型数值类型又被分为以下4种。
整数类型(简称整型)的说明符为int(integer),在内存中占4字节,其取值范围为-2147483648~2147483647。这个范围基本涵盖包含了常见的整数,能够满足我们在程序中表示整数的大部分需求。因为int是CPU原生支持的整数类型,所以处理起来最快。因此,int是我们最为常用的整数数值类型。
数据类型的字节数确实可能因编译器和操作系统的不同而有所变化。通常所说的数据类型所占用的内存字节数,是在特定环境下的典型值,例如在32位操作系统和使用32位编译器的情况下。
然而,在其他环境下,如64位操作系统或使用不同编译器时,这些数据类型所占用的字节数可能会有所不同。例如,在64位系统上,指针和其他一些数据类型可能会占用更多的字节。
为了确定特定环境下某种数据类型所占用的确切字节数,推荐使用sizeof运算符来动态计算。sizeof是C++中的一个关键字,用于在编译时确定类型或对象所占用的内存大小。具体使用方法可以参考后文对sizeof的介绍。
有时,我们只需要表达一个相对比较小范围内的整数,例如学生的成绩,最小为0,最大为100。如果仍然使用int这种取值范围比较大的数据类型来表示,就显得有点浪费资源了。现在正处于倡导节约型社会的时代,C++世界也不例外。为了表示这种取值范围相对较小的整数,C++提供了短整型数据类型,其类型说明符为short或short int,所占内存字节数只有int类型的一半,即2字节。相应地,其取值范围也缩小为-32768~32767,足以表达生活中常见的整型数值。然而,相对于int类型,short类型的处理速度要慢一些。考虑到现代计算机中的内存成本相对较低,所以我们往往为了性能而牺牲空间,较多使用int类型而不是使用short类型。
我们有时会遇到整数取值范围不确定的情况,这些数值可能很小,也可能非常大,达到数千亿。在这种情况下,为了确保程序的健壮性,我们通常希望使用平台支持的最大整型数值类型,以避免因超出取值范围而导致的溢出错误。C++中提供了长整型(long或long int),以适应这种需求。
在32位系统中,长整型通常占用4字节,其取值范围与普通的整型(int)相同。然而,在64位系统中,长整型占用8字节,取值范围显著扩大,从-9223372036854775808到9223372036854775807,这一范围足以满足绝大多数应用场景对整数大小的需求。
在C++中,为了表示一些特别大的整数值,如星系中星球的总数,我们使用取值范围更大的长长整型(long long或long long int)。这种类型在内存中占用8字节,其取值范围非常大,足以应对包括天文计算在内的特殊科学计算需求。
此外,C++还允许我们使用关键字unsigned或signed来修饰这些整型数据类型,从而构成无符号或有符号的整型数据类型。这两个关键字指明了数值在内存中的第一位是用于表示正负符号还是用于表示数值本身。默认情况下,我们提到的整型数据类型都是有符号的。如果要使用无符号版本,只需在相应的类型说明符前加上unsigned关键字即可。
各种无符号类型和相应的有符号类型占用的内存空间是相同的。但由于无符号类型省去了符号位,因此不能表示负数。相应地,它所能表示的正数范围会扩大到原来的两倍。当我们事先知道要表示的整数不会出现负数时,可以使用unsigned关键字修饰某个整型数值类型,使其成为一个无符号的数据类型,从而扩展其在正数上的取值范围。
表3-2列出了在典型的32位平台上,C++中各种整型数值数据类型、取值范围、位数和使用规则。
表3-2 整型数值类型
在现实世界中,人们根据体重来决定穿多大尺寸的衣服。同样地,C++中的各种数据类型也都有一个“体重”,即它们占用的内存字节数。了解数据类型的“体重”非常重要,因为它决定了程序运行时所需的内存资源。
为什么我们需要知道数据类型的“体重”?当我们通过指针直接操作内存时,需要知道数据类型所占用的内存字节数,以便为这些数据分配合适的内存资源。例如,如果我们想动态地申请一段内存来保存1024个long类型的数据,就需要根据每个long类型数据占用的内存字节数来计算一共需要申请多少字节的内存资源来存放这些数据。虽然大多数情况下long类型的字节数是4字节,但我们不能在程序中直接使用数字4,因为这个数字可能会因平台而异。为了提高程序的可维护性和可移植性,我们需要在代码中动态地获得数据类型在当前平台上的字节数,并计算出所需的内存资源总数。
C++提供了sizeof牌体重计(运算符),它可以帮助我们轻松地获取数据类型或变量(包括数组)实际占用的内存字节数。例如,在使用memset()函数清零某个数组时,可以使用sizeof牌体重计来计算这个数组的字节数:
#include <cstring> // 包含memset()函数所在的头文件 // ... // 定义数组 int res[1024]; // 用sizeof计算res占用的字节数 memset(res,0,sizeof(res));
其中,第一个参数是数组名,也就是数组的首地址;第二个参数是初始值,通常是0;第三个参数是用sizeof运算符计算出来的res数组所占用的字节数。幸亏有sizeof运算符,不然这里要写成更复杂的1024*4。
sizeof牌体重计不仅可以获得内建数据类型(例如int、long和double等)的“体重”,还可以获取自定义结构体或类所占用的字节数(包含因字节对齐而添加的字节数)。例如:
sizeof牌体重计如图3-2所示,它可以应用在任何需要知道某个数据类型或者变量(包括数组)所占用内存空间的地方。例如,使用memset()函数对数组进行清零操作;根据某个基本数据类型占用的字节数来判断当前硬件平台是32位还是64位等,以此来避免人为地指定数据类型的字节数可能带来的可维护性和可移植性问题。
图3-2 sizeof牌体重计
现实生活中的数字,除表示公共汽车路数的“800路”这种整数外,更常见的是表示西红柿价格的“3.5元/斤”这种小数。在C++中,我们使用浮点数类型来表示小数。根据取值范围的不同,C++中的浮点数类型可以分为单精度型、双精度型和长双精度型三种。
单精度浮点型的类型说明符为float。单精度浮点型占4字节的内存空间,其取值范围为-3.4E+38~+3.4E+38。需要注意的是,因为浮点型(包括后面的double和long double类型)无法精确地表示零值,所以其取值范围实际上并不连续,在中间接近零值的地方,被分为正负两部分。由于受到计算机存储浮点数机制的限制,使用float类型表示浮点数时,可以保证精确到小数点前后至少6位有效数字,最多可以达到7位有效数字。例如:
float fPrice = 3.5; // 用float类型的变量fPrice表示西红柿3.5元/斤
在C++中,我们将小数称为浮点数,而将表示小数的数据类型称为浮点数类型。大家可能会问:为什么小数被称为浮点数?其中的“浮”又是什么意思呢?这一切都与小数在C++中的表达方式有关系。所谓浮点,是相对于定点而言的。例如,我们要在C++中表达这样两个小数:
100000000000.0 0.000000000001
如果采用定点(小数点固定)表达方式,需要保存成如下形式:
100000000000.000000000000 000000000000.000000000001
采用这种方式,我们不得不将每一位数据都原原本本地保存下来,这其中的某些数据对于小数的数值和精度都毫无意义,反而浪费了宝贵的存储资源。为了解决这个问题,C++采用了一种新的保存方式:将数字表示成指数形式,保存每个数字的有效数字和指数。按照这种方式,上面的两个数可以保存成如下形式:
小数 1 指数 11(小数点往左移动了11位) 小数 1 指数 -12(小数点往右移动了12位)
通过小数点位置的移动,我们只需要保存小数的有效数字和小数点移动的位置,就可以以更加简洁的方式保存整个数字。因为这种表达方式中的小数点是浮动(float)的,所以小数也被称为浮点数。
双精度浮点型的类型说明符为double。双精度浮点型占8字节的内存空间,是单精度浮点型的两(double)倍,所以双精度浮点型不仅取值范围更大,可以达到-1.7E+308~1.7E+308,同时其精度也更高,可以精确到小数点前后15位有效数字,最多可以达到16位。例如:
double fD = 0.0000003; // 用double类型的变量表示支原体细胞的直径
长双精度浮点型的类型说明符为long double。长双精度浮点型占12字节的内存空间,其数值范围可以达到天文数字级别的-1.2E+4932~1.2E+4932。因此,这种类型更多地用于科学计算中,日常开发较少用到。
表3-3列出了在典型的32位平台上,浮点型数值的数据类型、位数、有效数字位数、取值范围和使用规则。
表3-3 浮点型数值类型
所谓随机数,通俗地讲,是由计算机通过一定的随机数算法生成的数字,按照某种规律分布(如均匀分布或正态分布)在某个指定范围内。在程序设计中,随机数被广泛应用于测试、游戏、仿真以及安全等领域。因此,掌握各种随机数的生成方式成为我们使用C++进行开发的一个必备技能。
在C++中,生成随机数需要使用随机引擎(engine)对象和分布(distribution)对象两部分共同完成。分布对象负责定义随机数的取值范围和分布。例如,使用uniform_int_distribution分布表示引擎生成的随机数字均匀分布在指定范围内;而使用normal_distribution分布则表示随机数字按正态分布方式生成。引擎对象根据分布对象的要求生成相应的随机数字。一旦确定了程序中随机数生成所需要的引擎对象和分布对象,就可以通过引擎对象调用分布对象的函数获取所需要的随机数。例如,生成网站登录验证码就需要使用随机数:
在这段代码中,首先引入了C++标准库中关于随机数的头文件<random>,然后定义相应的随机数引擎对象(reng)和分布对象(uni_dist)。在定义分布对象时,我们以构造函数参数的形式确定了随机数的取值范围。有了它们,就可以用引擎对象reng作为参数,调用uni_dist分布对象这个函数对象,最后得到的就是我们需要的在0~25范围内均匀分布的随机数。在这里,我们还利用了ASCII表中字母字符呈现连续分布的特性,在字符‘A’的基础上,加上一个0~25的随机数,就可以得到了我们最终想要的随机的字母字符。需要注意的是,在生成随机数之前,必须用引擎对象的seed()函数设置随机种子,否则每次运行生成的随机数序列都将相同,失去了随机性的意义。
另外,该程序仅利用随机数生成了验证码字符。接下来,我们需要接收用户的输入,并将它与当前的验证码进行比较,以判断用户输入是否正确。这部分工作可以作为学习后续内容(如各种控制结构、字符串处理等)的练习,读者可以自己动手完成。相信读者很快就能实现这一功能。
在日常生活中,我们经常需要表示不同类型的数据。例如,我们可能需要使用int类型的变量来表示800路公交车的编号;使用float类型的变量来表示西红柿每斤3.5元的价格。除此之外,有时我们需要表示逻辑状态,例如:
“这次的C++考试你过了没有?”
“他到底爱不爱我?”
这些问题中的“过了没有”和“爱不爱”都涉及逻辑判断。与数值数据类型不同,逻辑状态具有“非此即彼”的特性。例如,考试结果只能是“过了”或“没过”,没有其他选项。为了表示这种逻辑状态,在C++中我们使用布尔类型(bool)。
布尔类型的变量只能被赋予两个值:true表示真或肯定的状态,false表示假或否定的状态。C++标准并没有规定布尔类型的长度,但在Visual C++中,布尔类型通常占用1字节。以下是布尔类型使用的一个简单示例:
// 布尔类型变量bPass,表示考试是否通过 // 赋值为true,表示考试通过 bool bPass = true;
与int等数值类型数据主要用于计算不同,布尔类型的数据主要用来保存逻辑判断的结果,或者在条件结构或者循环结构中(这些将在稍后的第4章详细介绍),用于控制程序的执行流程。例如:
在示例代码中,bPass这个布尔类型的变量用于存储一个逻辑判断的结果:它记录了输入分数nScore是否大于或等于60。如果nScore大于或等于60,bPass被赋值为true,表示考试通过;如果nScore小于60,则bPass保持其初始值false,表示考试未通过。bPass变量因此保存了对nScore是否满足特定条件的逻辑判断结果。
布尔类型的变量不仅用于存储逻辑判断的结果,它们更多地用于控制程序的执行流程,特别是在条件语句或循环结构中。例如,在上述代码中的if条件语句里,bPass作为条件判断的依据:如果bPass为true,则程序会输出“考试通过”的提示;如果为false,则输出“考试未通过”的提示。这样,基于bPass变量的值,程序能够选择不同的执行路径,产生相应的输出结果。
布尔类型变量在C++中只有两个可能的值:true和false。然而,当布尔变量用于需要数值的环境时,它们会被隐式转换为整数值:false转换为0,而true转换为1。这种隐式转换同样适用于将数值赋给布尔变量:数值0转换为false,任何非0数值转换为true。以下是这种隐式转换的示例:
bool a = 4; // 4被转换为true,a的值为true int b = a; // a被转换为1,b的值为1 int c = a + b; // a被转换为1参与运算,c的值为2
这种隐式转换可能非常隐蔽,某些编译器甚至不会发出警告。隐蔽的行为可能导致难以察觉的错误。因此,我们应该避免使用数值给布尔变量赋值,或者避免在算术运算中使用布尔变量,以防止这种隐式转换带来的潜在问题。
“你的车牌号是多少?”
“陕A-82103”。
我们知道,程序设计语言本质上是用来抽象、描述和表达现实世界的。面对现实世界中的各种数值数据(例如表示公交车路线的800路,表示西红柿价格的3.5元/斤),我们可以用之前介绍的数值数据类型定义变量(int nNo,float fPrice)来抽象和表达。
除此之外,现实世界中还有文字数据,如车牌号“陕A-82103”。为了抽象和表达这类数据,C++提供了字符类型(char)和字符串类型(string)两种数据类型。字符类型用于表达单个字符,例如“A”“0”等,而字符串类型则用于表示由多个字符组成的序列。
在学习英语的时候,我们总是从ABC单个字母开始学习,然后将其串联起来形成一句完整的话。同样地,在C++中,要表达一个比较复杂的文字数据,我们也需要从构成字符串的单个字符开始学习。在C++中,我们用字符类型来抽象和表达单个常见的字符,其类型说明符是char。示例如下:
虽然字符类型的变量在赋值和输出时总是以字符形式出现,但从本质上讲,它可以被视为一种占用内存空间更少、取值范围更小的整数数据类型。字符类型只占用1字节的内存空间,相应地,其取值范围缩小为-128~127。当我们需要表达一个取值比较小的整数时,为了节省内存资源,也可以使用char类型。与整数数据类型类似,字符类型也可以通过signed或unsigned关键字修饰,形成有符号或无符号的字符类型。这使得char类型也可以参与算术运算。例如:
由于C++中的char类型主要用于表示ASCII字符集中的字符,其取值范围有限,通常只能涵盖基本的英文字母和控制字符。然而,对于更广泛的字符集,如中文字符,char类型则不足以应对。
为了解决这一问题,C++引入了wchar_t类型,它是一个宽字符类型,占用2字节的内存空间,可以表示更广泛的字符集,包括中文字符。以下是使用wchar_t类型输出中文字符的示例:
使用wchar_t类型时,需要注意以下几点:
● 代码文件应使用UTF-8编码格式保存,以确保正确处理中文字符。
● 宽字符常量或字符串常量需要使用L前缀,这表示它们采用宽字符集(通常是Unicode字符集)进行编码。例如,L'陕'表示一个宽字符常量。
● 如果不使用L前缀,则字符或字符串常量将使用多字节字符集进行编码,适用于char类型。
在C++中,使用char类型的变量可以表示单个字符。要表示由多个字符组成的字符串,我们可以使用字符数组。字符数组是一种将多个char类型元素组织成序列的数据结构。为了标识字符串的结束,我们在数组的末尾添加一个空字符'\0'。
以下是使用字符数组表示字符串的示例:
// 定义一个字符数组,用以保存字符串 char msg[32]; // 将各个字符依次保存到数组相应位置上 msg[0] = 'G'; // 第一个字符,保存到数组的第一个位置,后面的以此类推 msg[1] = 'o'; msg[2] = 'o'; msg[3] = 'd'; // 在最后的位置保存一个 '\0' 字符,表示字符串结束 msg[4] = '\0'; // 输出msg数组中的字符串 cout<<msg<<endl;
尽管使用字符数组是一种简单直接的方法,但它存在一些使用上的不便。因此,在C++中,我们更倾向于使用标准模板库(STL)中的string类型来表示字符串。string类型不仅封装了字符数组,还提供了许多方便的字符串操作功能,如获取字符串长度、查找特定字符等。
以下是使用string类型的示例程序:
string类型不仅包装了字符数组,可以存储字符串中的各个字符,还提供了很多与字符串相关的操作,例如可以获得一个字符串的长度,或者在字符串中查找某个特定的字符等,极大地方便了对字符串的处理。例如:
在上面这段代码中,我们首先定义了一个string类型的变量strName,用以保存用户输入的用户名字符串,然后用cin获取用户输入的字符串并保存到strName变量中。接着利用string类型的length()函数获取字符串的长度,也就是strName中的字符个数。最后用if条件结构将其与我们要求的字符串长度6进行比较,如果不符合条件,则显示错误提示。
在前面的章节中,我们介绍了C++中的多种数据类型:有表示整数的int类型,也有表示小数的float类型;有表示单个字符的char类型,也有表示字符串的string类型。这些意义不同、用途各异的数据类型为我们定义变量来表示现实世界中的数据提供了丰富的选择。然而,这些数据类型在使用上有一个共同的要求,那就是在定义变量表示数据时,我们必须事先知道所要表示的数据是什么类型,到底是一个小数还是一串字符,然后才能据此确定到底是该使用float还是string来定义变量。
不过,在开发实践中,有时我们并不能轻易确定一个变量应该具有的数据类型。例如,将某个复杂表达式作为初始值赋给一个新定义的变量时,我们往往很难确定这个表达式的数据类型,从而无法确定变量应有的数据类型。为了解决这个问题,C++的最新标准提供了auto关键字,使用它作为某个变量定义的数据类型,编译器会根据这个变量的初始值,自动推断出这个变量合理的数据类型,而无须人为指定。例如:
auto x = 1; // 使用整数1对变量x进行初始化,x被推断为int类型 auto y = 1.982; // 使用小数1.982对变量y进行初始化,y被推断为double类型 Handler GetHandler(); // 使用GetHandler()函数的返回值对变量handler进行初始化 // handler被推断为Handler类型 auto handler = GetHandler();
这里,我们在定义变量x时,并没有指定其具体的数据类型,而是使用auto作为代替。因此,编译器在编译这段代码时,会根据1这个初始值自动推断x的实际数据类型为int。同理,使用小数1.982进行初始化的变量y会被编译器自动推断为double类型;而最后的一个变量handler会被初始化为GetHandler()函数的返回值,其数据类型会被推断为Handler类型。虽然auto关键字会根据初始值自动推断变量的数据类型,但它的使用并不会增加额外的编译时间。使用auto关键字的好处显而易见,就像商场的免费大促销一样,这样的优惠谁不喜欢呢?
实际上,可以把auto关键字看作是变量定义中的数据类型占位符,它取代了原来应指定具体数据类型的位置。在编译时,编译器会根据变量的初始值推断出变量应有的具体数据类型,然后用该具体数据类型替换掉auto关键字,使其成为一个普通的带有具体数据类型的变量定义。使用auto关键字定义变量的形式与一般的变量定义形式并无差异,唯一的差别在于,使用auto关键字定义变量时必须有初始值:
auto变量名 = 初始值表达式; // 赋值形式 // 或 auto变量名{初始值表达式}; // 初始化列表形式
这样,编译器将根据初始值表达式计算结果的数据类型推断出变量的数据类型。通常情况下,当我们难以准确地确定变量的数据类型,或者这个变量的数据类型书写复杂时,就可以使用auto作为变量的数据类型来定义变量,交由编译器根据变量的初始值推断出该变量的真正数据类型。相比人脑做推断这种“苦力活”,计算机要比人脑快多了。这样做不仅省去了我们自己推断数据类型的麻烦,避免了可能的人为错误,还达到了简化代码的目的。例如:
为了在程序表达同样的意义,如果没有auto关键字的帮忙,我们将不得不写成下面这种烦琐的代码形式:
除简化代码外,auto关键字有时甚至能够帮助我们完成一些在C++11之前不可能完成的任务,成为一种必需。例如,在模板函数中,当一个变量的数据类型依赖于模板参数时,如果不使用auto关键字,根本无法确定变量的数据类型,因为我们无法提前预知用户使用何种数据类型作为模板参数来调用这个模板函数,从而无法确定这个变量的数据类型。但是使用auto关键字之后,一切难题都将迎刃而解。例如:
在这里,变量tmp的数据类型应该与模板参数T和U相乘结果的数据类型相同,也就是依赖于T和U的数据类型。对于程序员来说,在编写这个模板函数时,模板参数T和U的类型尚未确定,这样变量tmp的类型也就无法确定。所以,我们用auto关键字作为占位符,占据数据类型的位置,而真正的数据类型留待编译器在最终编译的时候,根据具体给定的模板参数T和U的类型进行推断来确定。这样,就把一件原来不可能的事情变成了可能。
使用auto关键字可以根据变量的初始值自动推断其数据类型,极大地方便了复杂数据类型变量的定义。不过,这种方式好是好,却有一个缺点,那就是每次推断得到的数据类型只能在定义变量时使用一次,无法保留下来继续多次使用。好不容易推断得到的数据类型只能使用一次,显得有点不够“低碳环保”。有时,我们需要推断得到的数据能够保留下来,并可以重复使用,用于定义多个相同类型的变量。为了弥补这个缺点,C++还提供了decltype关键字。它的使用语法形式如下:
typedef decltype(表达式) 用户数据类型;
其中,decltype(表达式)表示这个表达式的推断数据类型(declared type),也就是这个表达式计算结果的数据类型。而typedef是将这个推断得到的数据类型定义为用户自定义的数据类型。换句话说,typedef为这个推断数据类型取了一个名字,使得我们可以把它作为一个新的数据类型,用在定义变量、创建对象等任何需要数据类型的地方。例如,我们可以用decltype关键字来改写上面的示例程序:
auto和decltype的作用有些相似,都可以推断某个表达式的具体数据类型。但是,两者的使用还是稍有差别。如果我们只想根据初始值确定一个变量合适的数据类型,那么auto是最佳选择。只有当我们需要推断某个表达式的数据类型,并将其作为一种新的数据类型重复使用(例如定义多个相同类型的变量)或者单独使用(例如作为函数的返回值类型)时,才真正需要用到decltype。
学习了前面的基本数据类型之后,我们现在可以定义单个变量来表示单个数据。例如,用int类型定义变量nBusNo来表示公交车的800路,用float类型定义变量fPrice来表示西红柿每斤3.5元。然而,除单个孤立的数据外,现实世界中还有另一类数据:批量数据。例如,一个公司中所有员工的工资,这些数据的数据类型相同(都是int类型),性质相同(都表示员工的工资),数量庞大(成千上万员工的工资),并且往往形成一个有意义的数据集合(所有员工的工资)。针对这类批量数据,单独地定义一个个变量显然是行不通的。为此,C++提供了数组这种构造型数据类型来存储批量数据,它将这些数据组织起来形成一个数据序列,极大地方便了对批量数据的处理。
在C++中,定义数组的方法与定义变量非常相似。不同之处在于变量名变成了数组名,并且在数组名之后使用方括号“[]”来指定数组中数据元素的个数。其具体语法形式如下:
数据类型 数组名[个数常量][个数常量]…;
其中,数据类型表示这一系列批量数据的类型。例如,我们要定义一个可以保存多名员工工资的数组,而每名员工的工资数据都可以用int类型数据表示,那么整个数组的数据类型就是int类型;数组名通常是一个表明数组中数据含义的标识符。在这里,数组中的数据都是员工的工资,所以我们可以用arrSalary作为数组名。其中,arr前缀表示这是一个数组(array),而Salary则表示数组中的数据都是工资数据;数组名后方括号中的常数表示这一系列批量数据的个数。例如,这个公司有100名员工,我们需要在数组中保存100名员工的工资数据,那么这个常数自然就是100。需要注意的是,这个常数必须大于0,并且必须是常数。根据上面的介绍,我们可以这样定义一个用来保存100名员工的工资数据的数组:
// 保存100名员工的工资数据的数组 int arrSalary[100];
在定义数组的同时,也可以利用“{}”对数组进行初始化。例如:
// 定义数组并进行初始化 int nArray[5] = { 1,2,3,4,5 };
这行代码在定义一个长度为5的整型数组nArray的同时,用“{}”把1、2、3、4、5分别赋值给数组中的5个元素,以此来完成数组的初始化。当然,如果不需要对数组中的所有数据都赋初始值,也可以仅对数组的前面部分元素赋值,而剩余的未指定初始值的数据会被赋值为0或该数据类型的默认初始值。例如:
// 给定数组中前6个元素的初始值,剩下94个数据被赋值为0 int nBigArray[100] = { -10, 23, 542, 33, 543, 87 };
虽然我们可以利用“{}”在定义数组的同时对数组元素赋初始值,但由于数组的数据元素往往较多,使用“{}”为所有数组元素赋值并不现实。更多情况下,我们利用“{}”将数组中的所有元素都赋值为0,以完成数组使用前的清零操作。例如:
// 将nBigArray数组中的所有元素赋值为0 int nBigArray[1024] = {};
数组定义中的方括号“[ ]”用于确定数组的维数。在数组名后有几个“[ ]”就表示这是几维数组,一个数组的维数通常代表了数据的分类次数。例如,我们要用一个数组来保存一个学校所有学生的成绩,首先可以把所有学生的成绩按年级分成3个年级,然后每个年级再按班级分成10个班级,而每个班级又有30名学生。这样,经过3次分类,我们就可以用一个三维数组来保存一个学校所有学生的成绩:
// 记录学生成绩的三维数组 int arrScore[3][10][30]; // 一年级一班第一名学生的成绩是90分 arrScore[0][0][0] = 90;
定义好数组后,就相当于拥有了多个变量。要访问数组中的各个数据,我们可以通过在数组名后的方括号中给定数组下标来实现。数组下标表示要访问的数据在数组中的位置。需要注意的是,这个下标从0开始记数。例如,在前面定义的记录员工工资的arrSalary数组中,第一个数据是老板的工资,就可以通过如下方式来读写访问第一个数据:
我们可以看到,通过在数组名之后的方括号中给定0这个下标,可以像读写普通变量一样来读写数组中的第一个数据。以此类推,要想访问数组中的第二个数据,数组下标就应该是1,要想访问第n个数据,数组下标就应该是n-1。例如:
在使用数组下标时,需要注意的是,下标必须大于或等于0且小于数组定义时的个数。简单来说,一个长度为n的数组,其下标的取值范围是[0,n-1]。如果下标的取值超出这个范围,就会访问到数组以外的内存区域,导致数组访问越界错误。轻则造成数据读写错误,重则可能导致程序崩溃。并且,这种错误极具隐蔽性,往往很难发现。因此,在使用数组时,必须时刻注意防止数组访问越界。数组下标与数组中数据元素的对应关系如图3-3所示。
图3-3 数组中的数据元素与下标的对应关系
通过下标对数组中的元素进行读写访问,就像使用单独的变量一样简单。有了数组的帮忙,在描述大量性质相同的数据时,无须单独定义多个相同类型的变量,只需定义一个能容纳所需数据的数组。然后,通过不同的下标访问不同的数据,就像同时拥有多个单独的变量一样方便。
除之前介绍的数值数据和文字数据外,在现实世界中常常遇到这样的数据:一道单选题的答案只能是A、B、C、D四个选项中的某一个;红绿灯的颜色只能是红色、绿色和黄色中的一种;一个人的性别只能是男性或者女性。这些数据都只有有限的几种可能值,且值也只能是这个范围中的某一个。为了抽象和表达这种特殊数据,C++提供了枚举(enum)机制。
使用C++的枚举机制,可以通过列举某种数据的所有可能取值来定义一种新的数据类型,这种数据类型通常被称为枚举类型。当使用枚举类型来定义变量时,该变量的取值就限定在枚举类型所列举的可能取值范围内。定义枚举类型的语法格式如下:
其中,enum是定义枚举类型的关键字;枚举类型名是要创建的新数据类型的名称。完成枚举类型的定义后,我们可以将它作为数据类型来定义变量。在枚举类型的定义中,可以逐个列出该枚举类型的所有可能值。例如,可以将描述交通灯颜色的数据抽象成一个枚举类型:
// 交通灯颜色 enum TrafficLight { RED, // 红 GERRN, // 绿 YELLOW // 黄 };
有了这样的TrafficLight枚举类型的定义,我们就可以将它作为数据类型来定义一个变量,用以表示交通灯的颜色:
// 定义一个变量light表示交通灯的颜色 // 将枚举值RED赋值给它,表示当前交通灯是红色的 TrafficLight light = RED;
因为枚举类型所表达数据的特殊性,所以我们在应用枚举类型时需要注意以下几点。
本质上,枚举类型的数据是整型数值,每个枚举类型的可选值其实是一个整数值。默认情况下,第一个枚举值对应的是整数值为0,第二个枚举值对应的是整数值是1,以此类推。例如,上面TrafficLight枚举类型中的RED可选值其实就是整数值0,而GREEN对应1,YELLOW自然就对应2。如果认为某些枚举值默认的整数值不合适,例如,在某些情况下,我们希望某个枚举值拥有特殊的整数值来表示特殊含义,那么可以在定义时单独指定各个枚举值对应的整数值。例如:
经过人为地指定枚举值对应的数值后,RED对应的整数值变为1,其后的GREEN增加1,变为2。对于最后的YELLOW,因为其特殊意义,人为指定其整数值为0。
如果变量是枚举类型,那么只能使用该枚举类型中的某个枚举值对其进行赋值。例如:
换句话说,如果希望把某个变量的取值范围限定在几个可选值之内,则可以把该变量定义为枚举类型变量。
在定义枚举类型时,已经确定了枚举值及其对应的整数值。这些整数值要么是默认值,要么是指定的特殊数值。一旦定义完成,各个枚举值的整数值就成为常量,必须按照常量对待,不能再对其进行赋值或修改。也就是说,在完成定义后,不能改变任何枚举值对应的整数值。例如,下面的语句是错误的:
RED = 4; // 尝试改变枚举值,导致编译错误
枚举类型实际上定义了一组整型常量。在上面的例子中,我们可以使用3个整型常量来表示交通信号灯的3种颜色。然而,枚举类型允许我们使用描述性的名称来表示有限范围内的整数值,而不是直接使用含义不明确的整数值。这种做法有助于确保变量接受合法且易于理解的预期值,使得代码含义明确,更具可读性,也更易于维护。因此,当需要表达的数据“只有有限的几种可能值”时,我们应该优先选择使用枚举类型。
虽然枚举类型可以方便地定义某个范围内的枚举值,但由于传统的枚举类型本质上是一个int类型的整数,因此在使用中常常会遇到各种各样的问题。例如,传统枚举类型的所有枚举值在其定义后的整个代码范围内都可以使用,而这可能会造成名字污染或冲突,使得后续代码无法再将该枚举值名称用作他途。此外,传统枚举类型无法指定底层数据类型,这可能会造成内存资源的浪费,并且使得枚举类型无法进行前向声明。所谓前向声明,是指在某个元素(函数或者类)尚未定义时,为了提前使用它而进行的声明,向编译器表明源文件中将会有这个元素的定义,从而可以安心使用,具体的定义会稍后提供。
为了解决传统枚举类型存在的这些问题,C++的最新标准引入了新的作用域枚举类,它允许指定底层数据类型。定义作用域枚举类的语法形式与定义传统枚举类型的语法形式十分相似:
enum class枚举类名:数据类型 { // 可能的枚举值列表 };
枚举类的定义以enum class开始,后面跟着枚举类名,其后可以用冒号“:”指定枚举类的底层数据类型。枚举类的底层数据类型必须是有符号或无符号的整型数,如果不指定,其默认的数据类型是int类型。例如:
虽然枚举类的定义与传统的枚举类型的定义十分相似,但由于两者内在机制的不同,它们已经是两个完全不同的概念。枚举类具有作用域,其枚举值只在其作用域内可见,这有效地解决了可能由枚举值引起的名字污染问题。例如:
枚举类除解决名字污染的问题外,还有另一个显著的优势:它允许开发者指定底层数据类型,这使得前向声明成为可能。此外,根据枚举值的数量选择合适的底层数据类型,也可以在一定程度上避免资源浪费。例如:
在这段代码中,完成了枚举类TrafficLight的前向声明后,就可以直接使用它了。最后,只需要补充具体的定义即可。另外,在这里指定了枚举值的底层数据类型为char,这比默认的int更加节省资源。
从上面的例子可以看出,枚举类的使用有效地解决了传统枚举类型在使用中遇到的各种问题,所以,在今后的编程实践中,我们应该优先选择使用枚举类。
利用C++提供的基本数据类型定义的变量通常只能表达简单事物。例如,我们可以用int定义nAge变量来表示一个人的年龄,用string定义strName变量来表示一个人的姓名。然而,现实世界极其复杂,仅使用基本数据类型定义的变量无法充分描述这个复杂的现实世界。例如,仅用单一的基本数据类型无法描述人这样复杂的事物,因为人不仅具有姓名,还有身高、年龄、性别等多个属性需要描述。
我们认识到,即使是最复杂的事物,也是由多个简单事物构成的。既然我们可以使用基本数据类型来描述简单事物,那么将这些基本数据类型组合起来,形成一个可以描述同一事物多个不同属性的构造型数据类型,就可以用来描述复杂事物了。正是基于这样的想法,C++提供了结构体机制。
在C++中,struct关键字来将多个基本数据类型打包成结构体类型,其语法格式如下:
其中,struct关键字表示要创建一个结构体;结构体名是所创建结构体的名字,通常使用结构体描述所代表事物的名词作为结构体的名字。例如,如果我们要创建一个用于描述“人”这个复杂事物的结构体,那么结构体的名字可以使用Human。在结构体定义的内部,可以使用多个不同数据类型的变量来表示复杂事物的各个属性。例如,我们可以用string类型的m_strName变量来描述人的姓名,用bool类型的m_bMale变量来描述人的性别,等等。因为这些变量共同组成了结构体,所以它们也被称为结构体的成员变量。
例如,我们可以按照如下方式来定义表示“人”这个复杂事物的Human结构体:
通过将描述一个复杂事物的多个属性的变量打包在一个结构体中后,我们就可以用这个结构体定义变量来描述这个复杂事物(见图3-4):
// 定义一个Human结构体变量描述“陈良乔”这个人 // 这个结构体包含陈良乔的姓名、性别和年龄等信息 Human chenlq;
图3-4 将复杂事物打包成结构体
结构体由多个基本数据类型组成,相应地,结构体变量自然包含结构体中定义的所有成员变量。一旦定义了一个结构体变量,我们就拥有了它的所有成员变量。进一步地,我们可以使用点操作符“.”来访问结构体变量中的各个成员变量,利用它们来描述一个具体事物的各个属性。
例如,使用前面定义的Human结构体来创建一个变量,以描述“一个名叫陈良乔,年龄43岁,身高170厘米,体重61.5千克的男人”这一复杂事物:
// 定义一个Human类型变量chenlq // 用来描述人这种复杂事物 Human chenlq; // 这个人的姓名是“陈良乔” chenlq.m_strName = "陈良乔"; // 年龄43岁 chenlq.m_nAge = 43; // 身高170厘米 chenlq.m_nHeight = 170; // 体重61.5千克 chenlq.m_fWeight = 61.5; // 男人 chenlq.m_bMale = true;
这里我们注意到,如果想用结构体变量来描述一个复杂事物,只需要用“.”符号引用这个结构体变量中的各个成员变量,然后分别将这个复杂事物的各个属性对应地赋值给相应的成员变量即可。例如,我们要表示这个人的姓名是“陈良乔”,只需要将这个字符串数据赋值给“.”引出的m_strName成员变量。这个成员变量正是在chenlq结构体变量中表示姓名的成员变量。其他属性的处理以此类推。
需要注意的是,使用“.”操作符引用的结构体成员变量,与直接使用一个独立变量并没有太大差别,我们可以像对待普通变量一样对它进行读写操作。例如:
有了结构体机制,我们就可以描述更加复杂的事物,解决更加复杂的问题。在前面的例子中,我们创建了一个数组arrSalary来保存员工的工资。现在,我们希望程序还能处理员工的姓名、性别、年龄等附加的人事信息。如果没有结构体的帮助,我们不得不创建多个数组来分别保存这些附加信息,代码应该是这样的:
以上代码虽然能够解决问题,但有一个显著的缺点,就是描述同一个事物的各种数据是相互分割独立的,丢失了它们之间的紧密联系。如果各个数组不是按照相同的顺序保存员工的各项数据,想要知道名字为“陈良乔”这名员工的工资,就成了一件不可能完成的任务。
而有了结构体,我们可以把描述同一名员工的所有相关信息,包括姓名、性别、年龄和工资等多个属性,打包成一个结构体类型。然后,用这个结构体类型来定义保存员工数据的数组,这样不仅代码简单,而且上面那些不可能完成的任务也可以轻松完成:
在这段代码中,通过使用结构体,我们将描述员工信息的多个变量打包成一个Employee结构体。仅用一个Employee类型的数组就代替了原来的4个数组,这正体现了结构体在C++中的作用—将有联系的多个简单事物打包成一个复杂事物,化繁为简。
一天,两个变量在街上遇到了:
“老兄,你家住哪儿啊?改天找你玩儿去。”
“哦,我家在静态存储区的0x0049A024号,你家呢?”
“我家在动态存储区的0x0022FF0C号。有空来我家玩儿啊。”
在前面的章节中,我们学会了用int等数值数据类型定义变量表达各种数字数据,用char等字符数据类型定义变量表达文字数据,我们甚至还可以用结构体将多个基本数据类型组合形成新的数据类型,然后用它定义变量以表达更加复杂的事物。除这些现实世界中常见的数据外,在程序设计中,我们还有另外一种数据经常需要处理,那就是变量或者函数在内存中的地址数据。例如,上面对话中的0x0049A024和0x0022FF0C就是两个变量在内存中的地址。而就像对话中所说的那样,我们可以通过变量在内存中的地址便捷地对其进行读写操作,因而内存地址数据在C++中经常被用到。在C++中,表示内存地址数据的变量被称为指针变量,简称指针。
指针是C++从C语言中继承过来的,它提供了一种简便、高效地直接访问内存的方式。特别是当要访问的数据量比较大时,例如某个占用内存比较多的结构体变量,通过指针直接访问变量所在的内存,要比通过移动复制变量来对其进行访问要快得多,可以起到四两拨千斤的效果。合理正确地使用指针,可以写出更加紧凑、高效的代码。但是需要注意的是,如果指针使用不当,就很容易产生严重的错误,并且这些错误还具有一定的隐蔽性,极难发现与修正,因而它也成为程序员们痛苦的根源。爱恨交织,是程序员们对指针的最大感受,而学好指针,用好指针,也成为每个C++程序员的必修课。
指针专门用来表示内存地址,它的使用与内存访问密切相关。为了更好地理解指针,我们先来看看C++中内存空间的访问方式。
在C++程序中,有两种途径可以对内存进行访问。一种是通过变量名间接访问。为了保存数据,我们通常会先定义变量。定义变量意味着系统会分配一定的内存空间用于存储数据,而变量名则成为这块内存区域的标识。通过变量名,我们可以间接地访问到这块内存区域,在其中进行数据的读取或者写入。
另一种方式是直接通过这些数据所在内存的地址,即通过指针来访问这个地址上的数据。
这两种都是C++中访问内存的方式,只是一个是间接的,一个是直接的。打个比喻,我们可以将数据和内存的关系想象成送包裹的场景。按照第一种方式,我们会说:“送到亚美大厦(变量名)”。而按照第二种方式,我们会说:“送到科技路81号(内存地址)”。虽然这两种方式的表达不同,但实际上说的是同一件事。
在典型的32位计算机平台上,可以把内存空间看成由许多连续的小房间构成,每个房间就是一个小存储单元,大小是一个字节(byte)。数据们就住在这些房间里。有的数据比较小,例如一个char类型的字符,只需要一个房间就够了。而有的数据比较大,就需要占用多个房间。例如一个int类型的整数,其大小是4字节,因此需要4个房间来存储。为了方便找到住在这些房间中的数据,房间按照某种规则进行了编号,这个编号就是我们通常所说的内存地址。这些编号通常用一个十六进制数表示,例如上面例子中的0x0049A024和0x0022FF0C,如图3-5所示。
图3-5 住在内存中的数据
一旦知道某个数据所在的房间编号,就可以直接通过这个编号对相应房间中的数据进行读写操作。就像上面的例子中把包裹直接送到科技路81号一样,我们也可以把数据直接存储到0x0022FF0C标识的内存位置。
指针作为一种表示内存地址的特殊变量,其定义形式也有一定的特殊性:
数据类型* 指针变量名;
其中,数据类型指的是指针所表示地址上存储的数据的类型。例如,如果我们要定义一个指针来表示某个int类型数据的地址,那么指针定义中的数据类型就是int。这个数据类型由指针所指向的数据决定,可以是int、string和double等基本数据类型,也可以是自定义的结构体等复杂数据类型。简而言之,指针所指向的数据是什么类型,就用这种类型作为指针变量定义时的数据类型。数据类型之后的“*”符号表示定义的是一个指针变量,而“指针变量名”则是给这个指针指定的名字。例如:
// 定义指针变量p,它可以记录某个int类型数据的地址 int* p; // 定义指针变量pEmp,它可以记录某个Employee类型数据的地址 Employee* pEmp
实际上,下面两种定义指针变量的形式都是合乎C++语法的:
int* p; int *p;
这两种形式都可以通过编译,并表示相同的语法含义。然而,这两种形式所反映的编程风格和所强调的意义稍有不同。
int* p强调的是“p为一个指向int类型整数的指针”,这里可以把int*视为一种特殊的数据类型,而整个语句强调的是p是这种数据类型(int*)的一个变量。
int *p是把*p当作一个整体,强调的是“这个指针指向的是一个int类型的整数”,而p就是指向这个整数的指针。
这两种形式没有对错之分,可根据个人编程风格来选择。本书推荐第一种形式,因为它把指针视为一种数据类型,使得定义指针变量的语句更加清晰明了,可读性更强。
需要注意的是,当我们用第一种形式在一条语句中定义多个指针变量时,可能会产生混淆,例如:
// p是一个int类型的指针变量,而q实际上是一个int类型的变量 // 可能会让人误认为p和q都是int类型的指针 int* p, q; // 以下形式更清楚一些:*p是一个整数,p是指向这个整数的指针,q也是一个整数 int *p, q; // 定义两个指向int类型数据的指针p和q int *p, *q;
在开发实践中,有一条编码规范:一条语句只完成一件事情。按照这条规范,我们可以通过分开定义p和q来很好地避免上述问题。
如果确实需要定义多个相同类型的指针变量,也可以使用typedef关键字将指针类型定义为新的数据类型,然后用这个新的数据类型来定义多个指针变量:
// 将Employee指针类型定义成新的数据类型EMPointer typedef Employee* EMPointer; // 用EMPointer类型定义多个指针变量,这些变量都是Employee*类型 EMPointer pCAO,pCBO,pCCO,pCDO;
在定义一个指针变量之后,指针变量的初始值通常是一个随机值。它可能所指向某个无关紧要的数据,也可能指向重要的数据或者程序代码。如果直接使用这些未初始化的指针,后果可能是不可预期的。虽然有时可能啥事儿都没有,但也可能因此引发严重的问题。因此,在使用指针之前,务必对其赋值以进行初始化,将其指向某个有意义且合法内存位置。对指针变量进行赋值的语法格式如下:
指针变量 = 内存地址;
可以看到,对指针变量的赋值实际上是将这个指针指向某一内存地址,而该内存地址上存放的就是指针想要指向的数据。数据通常是用变量来表示的,因此获得变量的内存地址相当于获得数据所在的内存地址,从而可以用这个地址给指针变量赋值。在C++中,我们可以利用“&”取地址运算符,将该运算符放在某个变量的前面,以获得该变量的内存地址。例如:
这里,我们用“&”符号取得整型变量N的内存地址,即存储1003这个整数所在的内存地址,然后将这个地址赋值给整型指针变量pN,也就是将指针pN指向存储1003这个数据的内存地址,如图3-6所示。
图3-6 指针和指针所指向的数据
指针的初始化赋值最好是在定义指针时同时进行。例如,在上面的例子中,我们可以在定义指针pN的同时获取变量N的内存地址并赋值给该指针,从而使得指针在一开始就有一个合理的初始值,避免未初始化的指针被错误地使用。如果在定义指针时确实没有合理的初始值,我们可以将其赋值为nullptr(C++ 11标准中引入的关键字),表示这个指针没有指向任何内存地址,是一个空指针(null pointer),此时指针还不能使用。例如:
我们可以用“&”运算符获得一个变量(数据)的内存地址,反过来,也可以用“*”运算符表示一个内存地址上的变量(数据)。“*”被称为指针运算符,或称为解析运算符。它所执行的是与“&”运算符完全相反的操作。如果把“*”放在一个指针变量的前面,就可以取得这个指针所指向的内存地址中的数据。例如:
通过“*”运算符可以取得pN这个指针所指向的数据变量N,虽然N和*pN的形式不同,但是它们都代表内存中的同一份数据,都可以对这个数据进行读写操作,并且是等效的。
特别地,如果一个指针指向的是一个结构体类型的变量,与结构体变量使用“.”符号引用成员变量不同的是,如果是指向结构体的指针,则应该使用“->”符号引用成员变量。这个符号很像一个指针。例如:
当指针变量被正确赋值指向某个变量后,它就会成为一个有效的内存地址,也可以用它对另一个指针赋值。这样,两个指针指向相同的内存地址,指向同一份数据。例如:
// 定义一个整型变量 int a = 1982; // 得到变量a的内存地址并赋值给指针pa int* pa = &a; // 使用pa对另一个指针pb赋值 int* pb = pa;
在这里,我们用已经指向变量a的指针pa对指针pb赋值,这样pa和pb的值相同,都是变量a的地址,也就是说,两个指针指向了同一个变量。
需要特别说明的是,虽然两个指针指向同一个变量在语法上是合法的,但在实际开发中应当尽量避免。因为稍有不慎,这种代码就会给人带来困扰。继续上面的例子:
// 输出pa指向的数据,当前为1982 cout<<*pa<<endl; // 通过pb修改它所指向的数据,修改为1003 *pb = 1003; // 再次输出pa指向的数据,已经变为1003 cout<<*pa<<endl;
如果我们只看这段程序的输出,一定会感到奇怪:为什么没有通过pa进行任何修改,前后两次输出的内容却不同?结合前面的代码就会明白,pa和pb指向的是同一个变量a。当我们通过指针pb修改变量a后,再通过pa来获得变量a的数据,自然就是更新过后的数据了。表面上看,似乎没有通过pa对变量a进行修改,但实际上,pb早已暗渡陈仓,将变量a的数据修改了。在程序中,最忌讳这种“偷偷摸摸”的行为,因为一旦这种行为导致程序运行错误,将很难被发现。因此,应尽量避免两个指针指向同一个变量,就如同一个人最好不要取两个名字一样。
1.编写一个C++程序,接收用户输入的字符串,计算并输出字符串的长度。
2.编写一个数组求和程序,接收用户输入的整数个数n和n个整数,将这些整数存储在一个数组中,然后计算并输出数组中所有整数之和。
3.编写一个图书管理程序,定义一个包含书名、作者和出版年份的结构体Book。程序应接收用户输入的若干图书信息,并将这些信息存储在一个Book类型的数组中,然后输出所有图书的信息。
4.编写一个C++程序,接收用户输入的整数个数n,然后使用new操作符动态分配内存来创建一个整数数组。程序应接收用户输入的n个整数,并将它们存储在数组中。最后,程序应输出数组中所有整数的和。