2.3. 变量
关键概念:强静态类型
C++ 是一门静态类型语言,在编译时会作类型检查。在大多数语言中,对象的类型限制了对象可以执行的操作。如果某种类型不支持某种操作,那么这种类型的对象也就不能执行该操作。
在 C++ 中,操作是否合法是在编译时检查的。当编写表达式时,编译器检查表达式中的对象是否按该对象的类型定义的使用方式使用。如果不是的话,那么编译器会提示错误,而不产生可执行文件。
什么是变量
变量提供了程序可以操作的有名字的存储区。C++ 中的每一个变量都有特定的类型,该类型决定了变量的内存大小和布局、能够存储于该内存中的值的取值范围以及可应用在该变量上的操作集。C++ 程序员常常把变量称为“变量”或“对象(object)”。左值和右值
-
左值(发音为 ell-value):左值可以出现在赋值语句的左边或右边。
-
右值(发音为 are-value):右值只能出现在赋值的右边,不能出现在赋值语句的左边。
-
变量是左值,因此可以出现在赋值语句的左边。数字字面值是右值,因此不能被赋值。给定以下变量:
int units_sold = 0; double sales_price = 0, total_revenue = 0;
下列两条语句都会产生编译错误:
// error: arithmetic expression is not an lvalue units_sold * sales_price = total_revenue; // error: literal constant is not an lvalue 0 = 1;
有些操作符,比如赋值,要求其中的一个操作数必须是左值。结果,可以使用左值的上下文比右值更广。左值出现的上下文决定了左值是如何使用的。例如,表达式
units_sold = units_sold + 1;
中,units_sold 变量被用作两种不同操作符的操作数。+ 操作符仅关心其操作数的值。变量的值是当前存储在和该变量相关联的内存中的值。加法操作符的作用是取得变量的值并加 1。
术语:什么是对象?
C++ 程序员经常随意地使用术语对象。一般而言,对象就是内存中具有类型的区域。说得更具体一些,计算左值表达式就会产生对象。
严格地说,有些人只把术语对象用于描述变量或类类型的值。有些人还区别有名字的对象和没名字的对象,当谈到有名字的对象时一般指变量。还有一些人区分对象和值,用术语对象描述可被程序改变的数据,用术语值描述只读数据。
我们遵循更为通用的用法,即对象是内存中具有类型的区域。我们可以自由地使用对象描述程序中可操作的大部分数据,而不管这些数据是内置类型还是类类型,是有名字的还是没名字的,是可读的还是可写的。
变量名
变量名,即变量的标识符,可以由字母、数字和下划线组成。变量名必须以字母或下划线开头,并且区分大小写字母:C++ 中的标识符都是大小写敏感的。下面定义了 4 个不同的标识符:
// declares four different int variables int somename, someName, SomeName, SOMENAME;
语言本身并没有限制变量名的长度,但考虑到将会阅读和/或修改我们的代码的其他人,变量名不应太长。
变量命名习惯
变量命名有许多被普遍接受的习惯,遵循这些习惯可以提高程序的可读性。
-
变量名一般用小写字母。例如,通常会写成 index,而不写成 Index 或 INDEX。
-
标识符应使用能帮助记忆的名字,也就是说,能够提示其在程序中的用法的名字,如 on_loan 或 salary。
-
包含多个词的标识符书写为在每个词之间添加一个下划线,或者每个内嵌的词的第一个字母都大写。例如通常会写成 student_loan 或studentLoan,而不写成studentloan
定义对象
每个定义都是以 类型说明符开始,后面紧跟着以逗号分开的含有一个或多个说明符的列表。分号结束定义。类型说明符指定与对象相关联的类型: int 、 double、 std::string 和 Sales_item 都是类型名。其中 int 和 double 是内置类型, std::string 是标准库定义的类型, Sales_item 是我们自定义使用的类型。类型决定了分配给变量的存储空间的大小和可以在其上执行的操作。多个变量可以定义在同一条语句中:
double salary, wage; // defines two variables of type double int month, day, year; // defines three variables of type int std::string address; // defines one variable of type std::string
初始化
变量定义指定了变量的类型和标识符,也可以为对象提供初始值。定义时指定了初始值的对象被称为是已初始化的。C++ 支持两种初始化变量的形式:复制初始化和直接初始化。复制初始化语法用等号(=),直接初始化则是把初始化式放在括号中:
int ival(1024); // direct-initialization int ival = 1024; // copy-initialization这两种情形中, ival 都被初始化为 1024。
初始化指创建变量并给它赋初始值,而赋值则是擦除对象的当前值并用新值代替。
使用多个初始化式
初始化内置类型的对象只有一种方法:提供一个值,并且把这个值复制到新定义的对象中。对内置类型来说,复制初始化和直接初始化几乎没有差别。
对类类型的对象来说,有些初始化仅能用直接初始化完成。要想理解其中缘由,需要初步了解类是如何控制初始化的。
也可以通过一个计数器和一个字符初始化string对象。这样创建的对象包含重复多次的指定字符,重复次数由计数器指定:
std::string all_nines(10, '9'); // all_nines= "9999999999"
本例中,初始化 all_nines 的唯一方法是直接初始化。有多个初始化式时不能使用复制初始化。
初始化多个变量
当一个定义中定义了两个以上变量的时候,每个变量都可能有自己的初始化式。 对象的名字立即变成可见,所以可以用同一个定义中前面已定义变量的值初始化后面的变量。已初始化变量和未初始化变量可以在同一个定义中定义。两种形式的初始化文法可以相互混合。
#include <string> // ok: salary defined and initialized before it is used to initialize wage double salary = 9999.99, wage(salary + 0.01); // ok: mix of initialized and uninitialized int interval, month = 8, day = 7, year = 1955; // ok: both forms of initialization syntax used std::string title("C++ Primer, 4th Ed."), publisher = "A-W";对象可以用任意复杂的表达式(包括函数的返回值)来初始化:
double price = 109.99, discount = 0.16; double sale_price = apply_discount(price, discount);
本例中,函数 apply_discount 接受两个 double 类型的值并返回一个 double 类型的值。将变量 price 和 discount 传递给函数,并且用它的返回值来初始化 sale_price。
变量初始化规则
当定义没有初始化式的变量时,系统有时候会帮我们初始化变量。这时,系统提供什么样的值取决于变量的类型,也取决于变量定义的位置。
内置类型变量的初始化
内置类型变量是否自动初始化取决于变量定义的位置。在函数体外定义的变量都初始化成 0,在函数体里定义的内置类型变量不进行自动初始化。除了用作赋值操作符的左操作数,未初始化变量用作任何其他用途都是没有定义的。未初始化变量引起的错误难于发现,永远不要依赖未定义行为。
警告:未初始化的变量引起运行问题
使用未初始化的变量是常见的程序错误,通常也是难以发现的错误。虽然许多编译器都至少会提醒不要使用未初始化变量,但是编译器并未被要求去检测未初始化变量的使用。而且,没有一个编译器能检测出所有未初始化变量的使用。有时我们很幸运,使用未初始化的变量导致程序在运行时突然崩溃。一旦跟踪到程序崩溃的位置,就可以轻易地发现没有正确地初始化变量。
但有时,程序运行完毕却产生错误的结果。更糟糕的是,程序运行在一部机器上时能产生正确的结果,但在另外一部机器上却不能得到正确的结果。添加代码到程序的一些不相关的位置,会导致我们认为是正确的程序产生错误的结果。
问题出在未初始化的变量事实上都有一个值。编译器把该变量放到内存中的某个位置,而把这个位置的无论哪种位模式都当成是变量初始的状态。当被解释成整型值时,任何位模式都是合法的值——虽然这个值不可能是程序员想要的。因为这个值合法,所以使用它也不可能会导致程序崩溃。可能的结果是导致程序错误执行和/或错误计算。
所以——————建议每个内置类型的对象都要初始化。虽然这样做并不总是必需的,但是会更加容易和安全,除非你确定忽略初始化式不会带来风险。
类类型变量的初始化
如果定义某个类的变量时没有提供初始化式,这个类也可以定义初始化时的操作。它是通过定义一个特殊的构造函数即默认构造函数来实现的。这个构造函数之所以被称作默认构造函数,是因为它是“默认”运行的。如果没有提供初始化式,那么就会使用默认构造函数。不管变量在哪里定义,默认构造函数都会被使用。
大多数类都提供了默认构造函数。如果类具有默认构造函数,那么就可以在定义该类的变量时不用显式地初始化变量。例如,string 类定义了默认构造函数来初始化string 变量为空字符串,即没有字符的字符串:
有些类类型没有默认构造函数。对于这些类型来说,每个定义都必须提供显式的初始化式。没有初始值是根本不可能定义这种类型的变量的。
声明和定义
C++ 程序通常由许多文件组成。为了让多个文件访问相同的变量,C++ 区分了声明和定义。变量的 定义用于为变量分配存储空间,还可以为变量指定初始值。在一个程序中,变量有且仅有一个定义。
声明用于向程序表明变量的类型和名字。定义也是声明:当定义变量时我们声明了它的类型和名字。可以通过使用extern关键字声明变量名而不定义它。不定义变量的声明包括对象名、对象类型和对象类型前的关键字extern:
extern int i; // declares but does not define i int i; // declares and defines iextern 声明不是定义,也 不分配存储空间。事实上,它只是说明变量定义在程序的其他地方。程序中变量可以声明多次,但只能定义一次。
《强调》:只有当声明也是定义时,声明才可以有初始化式,因为只有定义才分配存储空间。初始化式必须要有存储空间来进行初始化。如果声明有初始化式,那么它可被当作是定义,即使声明标记为extern:
extern double pi = 3.1416; // definition
虽然使用了 extern ,但是这条语句还是定义了 pi,分配并初始化了存储空间。只有当extern 声明位于函数外部时,才可以含有初始化式。
因为已初始化的 extern 声明被当作是定义,所以该变量任何随后的定义都是错误的:
extern double pi = 3.1416; // definition double pi; // error: redefinition of pi
同样,随后的含有初始化式的 extern 声明也是错误的:
extern double pi = 3.1416; // definition extern double pi; // ok: declaration not definition extern double pi = 3.1416; // error: redefinition of pi
声明和定义之间的区别可能看起来微不足道,但事实上却是举足轻重的。
《强调》:在 C++ 语言中,变量必须且仅能定义一次,而且在使用变量之前必须定义或声明变量。
名字的作用域
C++程序中,每个名字都与唯一的实体(比如变量、函数和类型等)相关联。尽管有这样的要求,还是可以在程序中多次使用同一个名字,只要它用在不同的上下文中,且通过这些上下文可以区分该名字的不同意义。用来区分名字的不同意义的上下文称为作用域。作用域是程序的一段区域。一个名称可以和不同作用域中的不同实体相关联。
C++ 语言中,大多数作用域是用花括号来界定的。一般来说,名字从其声明点开始直到其声明所在的作用域结束处都是可见的。例如:
#include <iostream> int main() { int sum = 0; // sum values from 1 up to 10 inclusive for (int val = 1; val <= 10; ++val) sum += val; // equivalent to sum = sum + val std::cout << "Sum of 1 to 10 inclusive is " << sum << std::endl; return 0; }
这个程序定义了三个名字,使用了两个标准库的名字。程序定义了一个名为 main 的函数,以及两个名为 sum 和 val 的变量。名字 main 定义在所有花括号之外,在整个程序都可见。定义在所有函数外部的名字具有全局作用域,可以在程序中的任何地方访问。名字sum 定义在main 函数的作用域中,在整个 main 函数中都可以访问,但在 main 函数外则不能。变量sum 有局部作用域。名字val 更有意思,它定义在for 语句的作用域中,只能在 for 语句中使用,而不能用在 main 函数的其他地方。它具有语句作用域。
C++ 中作用域可嵌套
定义在全局作用域中的名字可以在局部作用域中使用,定义在全局作用域中的名字和定义在函数的局部作用域中的名字可以在语句作用域中使用,等等。名字还可以在内部作用域中重新定义。理解和名字相关联的实体需要明白定义名字的作用域:
#include <iostream> #include <string> /* Program for illustration purposes only: * It is bad style for a function to use a global variable and then * define a local variable with the same name */ std::string s1 = "hello"; // s1 has global scope int main() { std::string s2 = "world"; // s2 has local scope // uses global s1; prints "hello world" std::cout << s1 << " " << s2 << std::endl; int s1 = 42; // s1 is local and hides global s1 // uses local s1;prints "42 world" std::cout << s1 << " " << s2 << std::endl; return 0; }
这个程序中定义了三个变量:string 类型的全局变量s1、string 类型的局部变量s2 和 int 类型的局部变量 s1。局部变量s1 的定义屏蔽了全局变量s1。
变量从声明开始才可见,因此执行第一次输出时局部变量s1 不可见,输出表达式中的s1 是全局变量 s1,输出“hello world”。第二条输出语句跟在s1 的局部定义后,现在局部变量s1 在作用域中。第二条输出语句使用的是局部变量 s1 而不是全局变量 s1,输出“42 world”。
<建议>:像上面这样的程序很可能让人大惑不解。在函数内定义一个与函数可能会用到的全局变量同名的局部变量总是不好的。局部变量最好使用不同的名字。
在变量使用处定义变量
一般来说,变量的定义或声明可以放在程序中能摆放语句的任何位置。变量在使用前必须先声明或定义。通常把一个对象定义在它首次使用的地方是一个很好的办法。
在对象第一次被使用的地方定义对象可以提高程序的可读性。读者不需要返回到代码段的开始位置去寻找某一特殊变量的定义,而且,在此处定义变量,更容易给它赋以有意义的初始值。
放置声明的一个约束是,变量只在从其定义处开始到该声明所在的作用域的结束处才可以访问。必须在使用该变量的最外层作用域里面或之前定义变量。
2.4. const 限定符
下列 for 循环语句有两个问题,两个都和使用 512 作为循环上界有关。
for (int index = 0; index != 512; ++index) { // ... }第一个问题是程序的可读性。比较 index 与 512 有什么意思呢?循环在做什么呢?也就是说 512 作用何在?[本例中,512 被称为 魔数(magic number),它的意义在上下文中没有体现出来。好像这个数是魔术般地从空中出现的。
第二个问题是程序的可维护性。假设这个程序非常庞大,512 出现了 100 次。进一步假设在这 100 次中,有 80 次是表示某一特殊缓冲区的大小,剩余 20 次用于其他目的。现在我们需要把缓冲区的大小增大到 1024。要实现这一改变,必须检查每个 512 出现的位置。我们必须确定(在每种情况下都准确地确定)哪些 512 表示缓冲区大小,而哪些不是。改错一个都会使程序崩溃,又得回过头来重新检查。
解决这两个问题的方法是使用一个初始化为 512 的对象:
int bufSize = 512; // input buffer size
for (int index = 0; index != bufSize; ++index) {
// ...
}
通过使用好记的名字如 bufSize,增强了程序的可读性。现在是对对象 bufSize 测试而不是字面值常量 512 测试:
index != bufSize
现在如果想要改变缓冲区大小,就不再需要查找和改正 80 次出现的地方。而只有初始化 bufSize 那行需要修改。这种方法不但明显减少了工作量,而且还大大减少了出错的可能性。
定义 const 对象
定义一个变量代表某一常数的方法仍然有一个严重的问题。即 bufSize 是可以被修改的。 bufSize 可能被有意或无意地修改。 const 限定符提供了一个解决办法,它把一个对象转换成一个常量。
const int bufSize = 512; // input buffer size
定义 bufSize 为常量并初始化为 512。变量 bufSize 仍然是一个左值,但是现在这个左值是不可修改的。任何修改bufSize 的尝试都会导致编译错误:
bufSize = 0; // error: attempt to write to const object
因为常量在定义后就不能被修改,所以定义时必须初始化: |
const std::string hi = "hello!"; // ok: initialized const int i, j = 0; // error: i is uninitialized const
const 对象默认为文件的局部变量
在全局作用域里定义非 const 变量时,它在整个程序中都可以访问。我们可以把一个非 const 变更定义在一个文件中,假设已经做了合适的声明,就可在另外的文件中使用这个变量:
// file_1.cc int counter; // definition // file_2.cc extern int counter; // uses counter from file_1 ++counter; // increments counter defined in file_1
与其他变量不同,除非特别说明,在全局作用域声明的 const 变量是定义该对象的文件的局部变量。此变量只存在于那个文件中,不能被其他文件访问。
通过指定 const 变更为 extern,就可以在整个程序中访问 const 对象:
// file_1.cc // defines and initializes a const that is accessible to other files extern const int bufSize = fcn(); // file_2.cc extern const int bufSize; // uses bufSize from file_1 // uses bufSize defined in file_1 for (int index = 0; index != bufSize; ++index) // ...
本程序中,file_1.cc 通过函数 fcn 的返回值来定义和初始化 bufSize。而bufSize 定义为extern,也就意味着 bufSize 可以在其他的文件中使用。file_2.cc 中extern 的声明同样是extern;这种情况下,extern 标志着 bufSize 是一个声明,所以没有初始化式。
《注解:》
非 const 变量默认为 extern。要使 const 变量能够在其他的文件中访问,必须地指定它为extern。