1 变量
变量提供了一个具名的、可供程序操作的存储空间。C++ 中的每个变量都有其数据类型,数据类型决定着变量所占内存空间的大小和布局方式、该空间能存储的值的范围,以及变量能参与的运算。
1.1 变量的定义
变量定义的基本形式:首先是类型说明符,随后紧跟由一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。列表中每个变量名的类型都由类型说明符指定,定义时还可以为一个或多个变量赋初值。例如:
int sum = 0, value, units_sold = 0; // 3 个变量都是 int 类型的
std::string hello("你好"); // 变量 hello 通过一个字符串字面值初始化
1.1.1 初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化了。用于初始化变量的值可以是任意复杂的表达式。当一次定义了两个或多个变量时,对象的名字随着定义也就马上可以使用了。因此在同一条定义语句中,可以用先定义的变量值去初始化后定义的其它变量。例如:
double price = 109.99, discount = price * 0.16; // 正确:price 先被定义并赋值,随后被用于初始化 discount
很多程序员对于用等号=
来初始化变量的方式倍感困惑,这种方式容易让人认为初始化是赋值的一种。事实上,在 C++ 语言中,初始化和赋值是两个完全不同的操作。虽然很多时候这种区别无关紧要。初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来替代。
1.1.2 列表初始化
C++ 定义了几种不同形式的初始化。例如,要想定义一个名为units_sold
的int
变量并初始化为 0,有以下 4 种方法:
int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);
使用花括号来初始化的形式称为列表初始化。当用于内置类型的变量时,这种初始化形式有一个重要特点:如果我们使用列表初始化且初始值存在丢失信息的风险,即窄化,精度降低或造成数值变动,则编译器将报错,例如:
long double ld = 3.1415926536;
int a{ld}, b = {ld}; // 错误:转换未执行,因为存在丢失信息的危险
int c(ld), d = ld; // 正确:转换执行,且确实丢失了部分值
1.1.3 默认初始化
如果定义变量时没有指定初值,则变量被默认初始化,此时变量被赋予了“默认值”。默认值到底是什么由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定。定义于任何函数体之外的变量被初始化为 0。定义于函数体内部的内置类型变量将不被初始化。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其它形式访问此类值将引发错误。
绝大多数类都支持无须显式初始化而定义对象,这样的类提供了一个合适的默认值。一些类要求每个对象都显式初始化,此时如果创建了一个该类的对象而未对其做明确的初始化操作,将引发错误。
2 控制流语句
2.1 条件判断语句
2.1.1 if
语句
if
语句的作用是:判断一个指定的条件是否为真,根据判断结果决定是否执行另外一条语句。其语法形式有:
if (condition)
statement
if (condition)
statement
else
statement2
condition
必须用圆括号包围起来,其类型必须能转换成布尔类型。
2.2 循环语句
2.2.1 while
语句
while
语句反复执行一段代码,直至给定条件为假为止。while
语句的形式为:
while (condition)
statement
while
语句的执行过程是交替地检测condition
条件和执行关联的语句statement
,直至condition
为假时停止。所谓条件就是一个产生真或假的结果的表达式。
2.2.1.1 读取数量不定的输入数据
我们可以通过while
和istream
对象实现读取数量不定的输入数据。假设要对用户输入的一组数求和,我们预先不知道有多少个输入。这就需要不断读取数据直至没有新的输入为止:
#include <iostream>
int main()
{
int sum = 0, value = 0;
// 读取数据,直到遇到文件尾,计算所有读入的值的和
while (std::cin >> value)
sum += value;
std::cout << "Sum is: " << sum << std::endl;
return 0;
}
当我们使用一个istream
对象作为条件时,其效果是检测流的状态。如果流是有效的,即流未遇到错误,那么检测成功。当遇到文件结束符,或遇到一个无效输入时,istream
对象的状态会变为无效。处于无效状态的istream
对象会使条件变为假。
2.2.2 for
语句
2.2.2.1 传统的for
语句
每个for
语句都包含两部分:循环头和循环体。循环头控制循环体的执行次数,它由三部分组成:一个初始化语句、一个循环条件、一个表达式。循环体每次执行前都会先检查循环条件。
2.2.2.2 范围for
语句
范围for
语句遍历给定序列中的每个元素,并对序列中的每个值执行某种操作,其语法形式为:
for (declaration : expression)
statement
其中,expression
部分是一个对象,用于表示一个序列。declaration
部分负责定义一个变量,该变量将被用于访问序列中的基础元素。每次迭代,declaration
部分的变量会被初始化为expression
部分的下一个元素值。例如,string
对象表示一个字符的序列,因此string
对象可以作为范围for
语句中的expression
部分:
std::string s("some string");
for (char c : s) {
std::cout << c << std::endl; // 输出当前字符
}
2.3 跳转语句
跳转语句中断当前的执行过程。C++ 提供了 4 种跳转语句:break
、continue
、goto
、return
。
2.3.1 break
语句
break
语句负责终止离它最近的循环语句或switch
语句,并从这些语句之后的第一条语句开始继续执行。break
语句只能出现在循环语句或switch
语句的内部。
3 标准库类型string
标准库类型string
表示可变长的字符序列,使用string
类型必须首先包含头文件#include <string>
。作为标准库的一部分,string
定义在命名空间std
中。
3.1 定义和初始化string
对象
下表列出了初始化string
对象的方式。
表:初始化string | ||
---|---|---|
语句 | 描述 | |
1 | string s1 | 默认初始化,s1 是一个空字符串。 |
2 | string s2(s1) | s2 是s1 的副本。 |
3.2 读写string
对象
3.2.1 输入输出流
输入输出流也可以读写string
对象,例如:
string s; // 默认初始化空字符串
std::cin >> s; // 将 string 对象读入 s,遇到空白停止
std::cout << s << std::endl; // 输出 s
在执行读取操作时,string
对象会自动忽略开头的空白(即空格符、换行符、制表符等),并从第一个真正的字符开始读起,直到遇到下一处空白为止。因此,如果程序的输入是" Hello "
,则输出将是"Hello"
,输出结果中没有任何空格。
此外,也可以使用while
语句实现读取未知数量的字符串对象,例如:
string word;
while (std::cin >> word) { // 反复读取,直至到达文件末尾
std::cout << word << std::endl; // 逐个输出单词,每个单词后面紧跟一个换行
}
3.2.2 使用getline()
读取一整行
有时我们希望能在最终得到的字符串中保留输入时的空白符,这时应该用getline()
函数代替原来的>>
运算符。getline()
函数的参数是一个输入流和一个string
对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string
对象中去(注意不存在换行符)。getline()
只要一遇到换行符就结束读取操作并返回结果,哪怕输入的一开始就是换行符。如果输入真的一开始就是换行符,那么所得的结果是个空string
。
和输入运算符一样,getline()
也会返回它的流参数。因此既然输入运算符能作为判断的条件,我们也能用getline()
的结果作为条件。例如,我们要读取不定数量的行:
string line;
while (getline(std::cin, line)) { // 每次读入一整行,直至到达文件末尾
std::cout << line << std::endl;
}
因为line
中不包含换行符,所以我们手动地加上换行操作符。
3.3 字符串的运算
3.3.1 拼接
两个string
对象相加(+
)得到一个新的string
对象,其内容是把左侧的运算对象和右侧的运算对象串接而成。复合赋值运算符(+=
)负责把右侧string
对象的内容追加到左侧string
对象的后面。例如:
string s1 = "Hello, ", s2 = "World";
string s3 = s1 + s2; // s3 的内容是 "Hello World"
s1 += s2; // 等价于 s1 = s1 + s2
即使一种类型并非所需,我们也可以使用它,前提是该种类型可以自动转换成所需的类型。因为标准库允许把字符字面值和字符串字面值转换成string
对象,所以在需要string
对象的地方就可以使用这两种字面值来替代。当把string
对象和字符字面值及字符串字面值混在一条语句中使用时,必须确保每个加法运算符的两侧的运算对象至少有一个是string
对象。例如:
string s4 = s1 + ", "; // 正确:把一个 string 对象和一个字面值相加
string s5 = "Hello" + ", "; // 错误:两个运算对象都不是 string
string s6 = s1 + ", " + "World"; // 正确:每个加法运算符都有一个运算对象是 string
string s7 = "Hello" + ", " + s2; // 错误:不能把字面值直接相加
警告:因为某些历史原因,也为了与 C 兼容,所以 C++ 中的字符串字面值并不是标准库类型string
的对象。切记,字符串字面值与string
是不同的类型。
3.4 常用方法
3.4.1 长度
s.size()
返回字符串
s
中字符的个数,即对象的长度。
3.4.2 子字符串操作
s.substr(pos, n)
返回原始
string
对象的一部分或全部的拷贝。传递给substr()
的是可选的开始位置和计数值。例如:string s("Hello World"); string s2 = s.substr(0, 5); // s2 = Hello string s3 = s.substr(6); // s3 = World string s4 = s.substr(6, 11); // s4 = World string s5 = s.substr(12); // 抛出一个 out_of_range 异常
如果开始位置超过了
string
的大小,则会抛出一个out_of_range
异常。如果开始位置加上计数值大于string
的大小,则会调整计数值,只拷贝到string
的末尾。