变量与类型

上一章展示的 "Hello World" 程序的用处相当有限。我们必须写好几行代码,编译它们,然后执行生成的程序,只为了在屏幕上显示一个简单的句子。当然,自己直接输入这个句子会快得多。

然而,编程并不仅限于在屏幕上打印简单的文本。为了更进一步,为了能够编写能真正节省我们工作的有用程序,我们需要引入变量(variable)的概念。

让我们想象一下,我让你记住数字5,然后我让你同时记住数字2。你刚刚在你的记忆中存储了两个不同的值(5和2)。现在,如果我让你把我说的第一个数字加上1,你应该在记忆中保留数字6(也就是5+1)和2。然后,我们可以,例如,将这些值相减并得到4作为结果。

上面描述的整个过程就像是计算机处理两个变量的比喻。同样的过程可以用C++中的以下语句来表达:

1
2
3
4
a = 5;
b = 2;
a = a + 1;
result = a - b;

显然,这是一个非常简单的例子,因为我们只用了两个小的整数值,但请考虑你的计算机可以同时存储数百万个这样的数字,并对它们进行复杂的数学运算。

我们现在可以将变量定义为内存中用于存储值的一部分。

每个变量都需要一个名称来标识它并将其与其他变量区分开来。例如,在前面的代码中,变量名是 abresult,但我们本可以给这些变量起任何我们能想到的名字,只要它们是有效的C++标识符。

标识符

一个有效的标识符是一个由一个或多个字母、数字或下划线字符(_)组成的序列。空格、标点符号和符号不能成为标识符的一部分。此外,标识符必须总是以字母开头。它们也可以以下划线字符(_)开头,但在大多数情况下,这类标识符被认为是为编译器特定的关键字或外部标识符保留的,以及任何位置包含两个连续下划线字符的标识符。在任何情况下,它们都不能以数字开头。

C++使用许多关键字来标识操作和数据描述;因此,程序员创建的标识符不能与这些关键字匹配。不能用于程序员创建的标识符的标准保留关键字是:

alignas, alignof, and, and_eq, asm, auto, bitand, bitor, bool, break, case, catch, char, char16_t, char32_t, class, compl, const, constexpr, const_cast, continue, decltype, default, delete, do, double, dynamic_cast, else, enum, explicit, export, extern, false, float, for, friend, goto, if, inline, int, long, mutable, namespace, new, noexcept, not, not_eq, nullptr, operator, or, or_eq, private, protected, public, register, reinterpret_cast, return, short, signed, sizeof, static, static_assert, static_cast, struct, switch, template, this, thread_local, throw, true, try, typedef, typeid, typename, union, unsigned, using, virtual, void, volatile, wchar_t, while, xor, xor_eq

特定的编译器也可能有额外的特定保留关键字。

非常重要: C++语言是“大小写敏感”(case sensitive)的语言。这意味着用大写字母写的标识符与用小写字母写的同名标识符不等价。因此,例如,变量 RESULT 与变量 result 或变量 Result 是不一样的。这是三个标识了三个不同变量的不同标识符。

基本数据类型

变量的值以0和1的形式存储在计算机内存中某个未指定的位置。我们的程序不需要知道变量存储的确切位置;它可以简单地通过它的名字来引用它。程序需要知道的是变量中存储的数据类型。存储一个简单的整数和存储一个字母或一个大的浮点数是不同的;尽管它们都用0和1表示,但它们的解释方式不同,而且在许多情况下,它们占用的内存量也不同。

基本数据类型是由语言直接实现的基本类型,代表了大多数系统原生支持的基本存储单元。它们主要可以分为:
  • 字符类型: 它们可以表示单个字符,例如 'A''$'。最基本的类型是 char,它是一个单字节的字符。还提供了用于更宽字符的其他类型。
  • 数值整数类型: 它们可以存储一个整数值,例如 71024。它们有多种大小,并且可以是有符号(signed)或无符号(unsigned),这取决于它们是否支持负值。
  • 浮点类型: 它们可以表示实数值,例如 3.140.01,具有不同级别的精度,具体取决于使用三种浮点类型中的哪一种。
  • 布尔类型: 布尔类型,在C++中称为 bool,只能表示两种状态之一:truefalse

以下是C++中基本类型的完整列表:
分组类型名称*关于大小/精度的说明
字符类型char大小恰好为一个字节。至少8位。
char16_t不小于 char。至少16位。
char32_t不小于 char16_t。至少32位。
wchar_t可以表示支持的最大字符集。
整数类型(有符号)signed charchar 大小相同。至少8位。
signed short int不小于 char。至少16位。
signed int不小于 short。至少16位。
signed long int不小于 int。至少32位。
signed long long int不小于 long。至少64位。
整数类型(无符号)unsigned char(与它们的有符号对应类型大小相同)
unsigned short int
unsigned int
unsigned long int
unsigned long long int
浮点类型float
double精度不低于 float
long double精度不低于 double
布尔类型bool
Void 类型void无存储空间
空指针decltype(nullptr)

* 某些整数类型的名称可以省略其 signedint 部分进行缩写——只有非斜体部分是必需的,斜体部分是可选的。例如,signed short int 可以缩写为 signed shortshort int 或干脆 short;它们都表示相同的基本类型。

在上述每个分组中,类型之间的区别仅在于它们的大小(即它们在内存中占用的空间):每个分组中的第一个类型是最小的,最后一个是最大的,每个类型至少与同一组中它前面的类型一样大。除此之外,同一组中的类型具有相同的属性。

请注意,在上面的面板中,除了 char(其大小恰好为一个字节)外,没有任何基本类型指定了标准大小(最多只有一个最小大小)。因此,该类型不被要求(并且在许多情况下不)恰好是这个最小大小。这并不意味着这些类型的大小不确定,而是意味着在所有编译器和机器上没有一个标准大小;每个编译器实现可以为这些类型指定最适合程序将要运行的架构的大小。这种对类型相当通用的尺寸规定为C++语言提供了很大的灵活性,使其能够适应在各种平台(无论是现在还是未来)上以最佳方式工作。

上面的类型大小以比特(bit)为单位表示;一个类型拥有的比特越多,它可以表示的不同值就越多,但同时在内存中消耗的空间也越多。

大小可表示的唯一值数量备注
8位256= 28
16位65 536= 216
32位4 294 967 296= 232 (约40亿)
64位18 446 744 073 709 551 616= 264 (约1800亿亿)

对于整数类型,拥有更多可表示的值意味着它们可以表示的值范围更大;例如,一个16位的无符号整数可以表示0到65535范围内的65536个不同值,而其有符号的对应类型在大多数情况下能够表示-32768到32767之间的值。请注意,与无符号类型相比,有符号类型的正值范围大约减半,因为16位中的一位用于表示符号;这是一个相对较小的范围差异,很少能成为仅仅因为可表示的正值范围而使用无符号类型的理由。

对于浮点类型,大小会影响其精度,因为它有更多或更少的位用于其有效数和指数。

如果类型的大小或精度不是问题,那么通常选择 charintdouble 来分别表示字符、整数和浮点值。它们各自组中的其他类型仅在非常特殊的情况下使用。

在特定系统和编译器实现中,基本类型的属性可以通过使用 numeric_limits 类来获取(参见标准头文件 <limits>)。如果出于某种原因需要特定大小的类型,库在头文件 <cstdint> 中定义了一些固定大小的类型别名。

上述类型(字符、整数、浮点和布尔)统称为算术类型。但还存在另外两种基本类型:void,它表示类型的缺失;以及类型 nullptr,它是一种特殊的指针类型。这两种类型都将在后面关于指针的章节中进一步讨论。

C++支持基于上述基本类型的各种类型;这些其他类型被称为复合数据类型,是C++语言的主要优势之一。我们将在未来的章节中更详细地看到它们。

变量的声明

C++是一种强类型语言,要求每个变量在首次使用前都必须声明其类型。这会通知编译器为该变量在内存中保留多大的空间以及如何解释其值。在C++中声明一个新变量的语法很简单:我们只需写下类型,后跟变量名(即其标识符)。例如:

1
2
int a;
float mynumber;

这是两个有效的变量声明。第一个声明了一个类型为 int、标识符为 a 的变量。第二个声明了一个类型为 float、标识符为 mynumber 的变量。一旦声明,变量 amynumber 就可以在程序中它们作用域的其余部分使用。
如果要声明多个相同类型的变量,可以在单个语句中全部声明,用逗号分隔它们的标识符。例如:

1
int a, b, c;

这声明了三个变量(abc),它们都是 int 类型,并且与以下写法的含义完全相同:

1
2
3
int a;
int b;
int c;

为了看看变量声明在程序中实际是什么样子,让我们看一下本章开头提出的关于你脑海记忆的例子的完整C++代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// operating with variables

#include <iostream>
using namespace std;

int main ()
{
  // declaring variables:
  int a, b;
  int result;

  // process:
  a = 5;
  b = 2;
  a = a + 1;
  result = a - b;

  // print out the result:
  cout << result;

  // terminate the program:
  return 0;
}
4

如果除了变量声明本身之外,还有其他东西让你觉得有点奇怪,不要担心。其中大部分内容将在接下来的章节中更详细地解释。

变量的初始化

在上面的例子中,当变量被声明时,它们的值是不确定的,直到它们第一次被赋值。但是,一个变量可以从它被声明的那一刻起就有一个特定的值。这被称为变量的初始化

在C++中,有三种方法可以初始化变量。它们都是等效的,也反映了该语言多年来的演变:

第一种,被称为C-like初始化(因为它继承自C语言),包括在变量名后附加一个等号,后跟变量被初始化的值:

类型 标识符 = 初始值;
例如,要声明一个名为 xint 类型变量,并在声明的同时将其初始化为零,我们可以这样写:

1
int x = 0;

第二种方法,被称为构造函数初始化(由C++语言引入),将初始值括在圆括号(())中:

类型 标识符 (初始值);
例如:

1
int x (0);

最后,第三种方法,被称为统一初始化,与上述方法类似,但使用花括号({})代替圆括号(这是由2011年C++标准修订版引入的):

类型 标识符 {初始值};
例如:

1
int x {0};

在C++中,这三种初始化变量的方式都是有效且等价的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// initialization of variables

#include <iostream>
using namespace std;

int main ()
{
  int a=5;               // initial value: 5
  int b(3);              // initial value: 3
  int c{2};              // initial value: 2
  int result;            // initial value undetermined

  a = a + b;
  result = a - c;
  cout << result;

  return 0;
}
6

类型推导:auto 和 decltype

当一个新变量被初始化时,编译器可以根据初始化表达式自动推断出变量的类型。为此,只需使用 auto 作为变量的类型说明符:

1
2
int foo = 0;
auto bar = foo;  // the same as: int bar = foo; 

在这里,bar 被声明为 auto 类型;因此,bar 的类型就是用来初始化它的值的类型:在这种情况下,它使用 foo 的类型,即 int

未初始化的变量也可以使用 decltype 说明符进行类型推导:

1
2
int foo = 0;
decltype(foo) bar;  // the same as: int bar; 

这里,bar 被声明为与 foo 具有相同的类型。

autodecltype 是最近添加到语言中的强大功能。但它们引入的类型推导功能旨在用于无法通过其他方式获取类型,或使用它能提高代码可读性的情况。上面的两个例子可能都不属于这两种用例。事实上,它们很可能降低了可读性,因为在阅读代码时,必须去查找 foo 的类型才能真正知道 bar 的类型。

字符串简介

基本类型代表了代码可能运行的机器所处理的最基本的类型。但C++语言的一大优势是其丰富的复合类型集,而基本类型仅仅是这些复合类型的构建块。

string 类就是一个复合类型的例子。这种类型的变量能够存储字符序列,例如单词或句子。这是一个非常有用的功能!

与基本数据类型的第一个区别是,为了声明和使用这种类型的对象(变量),程序需要包含定义该类型的标准库头文件(头文件 <string>):

// my first string
#include <iostream>
#include <string>
using namespace std;

int main ()
{
  string mystring;
  mystring = "This is a string";
  cout << mystring;
  return 0;
}
This is a string

正如你在上一个例子中看到的,字符串可以用任何有效的字符串字面量来初始化,就像数值类型变量可以用任何有效的数值字面量来初始化一样。与基本类型一样,所有初始化格式对字符串都有效:

1
2
3
string mystring = "This is a string";
string mystring ("This is a string");
string mystring {"This is a string"};

字符串也可以执行基本数据类型能执行的所有其他基本操作,比如在声明时不带初始值,以及在执行期间改变其值:

// my first string
#include <iostream>
#include <string>
using namespace std;

int main ()
{
  string mystring;
  mystring = "This is the initial string content";
  cout << mystring << endl;
  mystring = "This is a different string content";
  cout << mystring << endl;
  return 0;
}
This is the initial string content
This is a different string content

注意:插入 endl 操作符会结束行ends the line),即打印一个换行符并刷新流。

string 类是一种复合类型。正如你在上面的例子中所看到的,复合类型的使用方式与基本类型相同:声明变量和初始化它们使用相同的语法。

有关标准C++字符串的更多详细信息,请参见 string 类参考。
Index
目录