专栏《Zero to C++》· 单元 07
阅读时间约 12 分钟。这三周学变量、类型和字符串,做出一个能读入文本并输出信息的小工具。
这三周的目标
第 0 周让程序跑起来了,但那个程序只会说一句写死的话。这三周要让程序接受你的输入 ,并回答你。
做法是写一个文本小助手:你输入一行字,它告诉你这行字有多少个字符、开头是什么、重复两遍长什么样。到第 4--6 周会给它加上「数词数」「找子串」这类功能,这两周先搭好输入和文本处理的部分。
这三周仍然不碰循环和判断。先让一条直线执行到底的程序完全跑通,比一开始就塞进各种控制流更容易看清每一步在做什么。
变量:给数据起个名字
cpp
int count = 0; // 声明一个整数变量 count,初值是 0
double ratio = 0.5; // 声明一个小数变量 ratio
int 和 double 是类型 ,count 和 ratio 是变量名 ,等号右边是初值。
三条习惯,越早养成越好:
- 声明时就给初值。
int count;语法上合法,但count里是什么由运气决定。写int count = 0;就没有这个问题。 - 名字说清用途。
int n;不如int word_count;。 - 一个变量只在它需要的地方声明。 不要把变量堆在函数开头------那是 C89 限制留下的习惯,C++ 里不需要。
类型:数据分几种
入门阶段常用的就五种:
| 类型 | 装什么 | 例子 |
|---|---|---|
int |
整数 | int lives = 3; |
double |
小数 | double pi = 3.14159; |
bool |
真 / 假 | bool done = false; |
char |
单个字符 | char grade = 'A'; |
std::string |
一段文本 | std::string name = "C++"; |
类型告诉编译器「这个数据怎么存、能做哪些运算」。把一段文本存进 int,编译器会直接报错------这是类型系统在帮你。单元 04 讲过的 std::string 在这里第一次派上用场。
小数的类型叫 double,意思是「双精度浮点数」。日常代码用 double 就够,float 精度更低,除非有明确的省内存需求,否则不用管它。
让值保持不动:const
pi 这样的值有个特点:程序从头到尾都该是同一个数。如果某个地方不小心改了它,后面所有用到它的计算都跟着错,而且不会报错。
const 把这个约定交给编译器去守:
cpp
const double pi = 3.14159;
pi = 3.0; // 这一行编译不过
实测 g++ 的反应:
error: assignment of read-only variable 'pi'
const 写在类型前面,const int、const double、const std::string 都是同一个用法。
要判断一个名字该不该加 const,问一句就够了:它赋值之后还需要再改吗?不需要就加。加的代价是以后真想改的时候编译器会拦你一次;不加的代价是几个月后的某次改动里被别处的代码改掉,而那时你已经不记得它不该变。
单元 01 里 IntVec 那个「size 被改成 999」的例子,用的是另一件工具:private 挡住外部代码。const 管的是自己改自己------两者挡的是不同的手。
一个真实的坑:整数除法
看这行:
cpp
std::cout << 7 / 2 << '\n';
你可能期待 3.5,实际输出是 3。
两个整数相除,结果还是整数 ,小数部分被丢掉------注意是丢掉,不是四舍五入,7 / 2 得 3。
想让结果是小数,参与运算的数里至少有一个得是小数类型:
cpp
std::cout << 7.0 / 2 << '\n'; // 3.5
实测两行放在一起:
7 / 2 = 3
7.0 / 2 = 3.5
这个坑在新手代码里出现频率极高,而且不报错,只是算错。判断方法:看除号两边是不是都是整数。如果是,先想清楚你要的是整除还是小数除法。
如果两边本来就是整数,而你要小数结果,做法是把其中一个显式转成小数类型:
cpp
int a = 7, b = 2;
std::cout << a / b << '\n'; // 3
std::cout << static_cast<double>(a) / b << '\n'; // 3.5
static_cast<double>(a) 是一次类型转换 :先把 a 变成 double,再参与除法。因为除号一边已经是小数,另一边也跟着升级,结果就保住了小数部分。
C 里的写法是 (double)a。C++ 提供了四种带名字的转换写法,static_cast 是日常最常用的一个。它的好处是意图写在字面上------读代码的人在长表达式里不容易看漏一处转换。单元 01 的 IntVec::avg 里出现过同一个写法。
除号还有一个搭档 %,算的是相除之后的余数:
cpp
std::cout << 7 / 2 << '\n'; // 3
std::cout << 7 % 2 << '\n'; // 1
7 / 2 得 3,7 % 2 得 1,合起来 3 * 2 + 1 正好是 7。判断奇偶、判断能不能整除都靠它:i % 2 == 0 表示 i 是偶数,i % 7 == 0 表示 i 能被 7 整除。单元 08 会用这两个写法。
第三个会算错的坑:放不下
每种类型能表示的范围都是有限的。int 通常能装到约 21 亿,超出之后不会报错,会绕回去:
cpp
int big = 2000000000;
big = big + big;
std::cout << big << '\n';
实测输出:
-294967296
两个正数相加得到负数,程序不会中断,后面用到 big 的地方全部算错。这类问题叫溢出(overflow),和整数除法一样属于「不报错,只是算错」。
零基础阶段不用背各类型的精确范围,记住三件事就够:
int装不下超过约 21 亿的数;- 循环里反复累加、连乘,最容易越界;
- 真需要更大的整数时用
long long,范围和int的用法完全一样,只是更大。
字符串:std::string
std::string 是 C++ 的文本类型。它自己管理内存,你只管用。常用操作:
cpp
#include <iostream>
#include <string>
int main() {
std::cout << "输入一行文本:";
std::string text;
std::getline(std::cin, text); // 读入一整行
std::cout << "字符数:" << text.size() << '\n';
std::cout << "第一个字符:" << text[0] << '\n';
std::cout << "前 5 个字符:" << text.substr(0, 5) << '\n';
std::cout << "重复两遍:" << text + text << '\n';
}
| 写法 | 作用 |
|---|---|
text.size() |
有多少个字符 |
text[0] |
第 0 个字符。string 的下标从 0 开始 |
text.substr(0, 5) |
从第 0 个位置起取 5 个字符 |
text + text |
拼接。+ 用在字符串上是连接 |
text.empty() |
是不是空字符串 |
实测输出:
7 / 2 = 3
7.0 / 2 = 3.5
输入一行文本:字符数:18
第一个字符:C
前 5 个字符:C++ m
重复两遍:C++ makes me happyC++ makes me happy
(输入的文本是 C++ makes me happy,一共 18 个字符。)
这一组操作在 C 里对应什么,单元 04 的第五节有完整的对照:strlen 对应 size(),strcat 对应 +,strncpy 对应 substr。区别是 C 的每个操作都需要你确认目标缓冲区够不够大,而 string 自己会处理。你不需要再算缓冲区大小------这是这一节最省心的一点。
让编译器替你写类型:auto
写多了会发现,类型名经常在右边已经写过一遍:
cpp
std::string name = "Zero"; // 右边本来就是字符串
double x = 2.5; // 右边本来就是小数
auto 让你把左边的类型名省掉,由编译器从右边推出来:
cpp
auto n = 42; // 推成 int
auto x = 1.5; // 推成 double
auto name = std::string("Zero"); // 推成 std::string
三行一起跑,实测输出:
42 1.5 4
(n 是 42,x 是 1.5,name.size() 是 4。)
关于 auto 有两点要记住:
auto不会让类型消失 。变量的类型和你手写的时候一模一样,只是名字由编译器填上。写成auto x = 1.5之后,x就是一个double,整数除法、溢出那些规则照样对它生效。auto只看右边 。右边是什么类型就推成什么,所以auto x = 42;和auto x = 42.0;是两个不同的类型,后面算出来的结果也不同。
入门阶段用 auto 的场合有两个:右边类型名太长的时候(单元 10 会见到 std::vector<std::pair<std::string, int>>::iterator 这类写法),以及类型一眼能从右边看出来的时候。该写清楚的地方还是写清楚------函数的返回类型、类的成员,写全了更好读。
一个容易卡住的地方:getline 和 >>
读输入有两种写法:
cpp
std::cin >> word; // 读一个"词",遇到空格停
std::getline(std::cin, line); // 读一整行,遇回车才停
如果把这两种混用,会撞上一个经典的坑:std::cin >> n; 读完数字后,那个回车还留在输入流里,紧接着的 getline 会把空行读走,表现得像「我的输入被跳过了」。
这两周的建议很简单:要么全程用 >>,要么全程用 getline,不要混。 需要混用时的处理办法留到单元 11 的 FAQ。
做出工具:inspect.cpp
把上面这些拼起来,就是这三周的产物:
cpp
// inspect.cpp ------ 文本小助手 v1
// 编译:g++ -std=c++20 -Wall -Wextra inspect.cpp -o inspect
#include <iostream>
#include <string>
int main() {
// ① 整数除法:两个整数相除,结果还是整数
std::cout << "7 / 2 = " << 7 / 2 << '\n';
// ② 只要有一个是小数,结果就是小数
std::cout << "7.0 / 2 = " << 7.0 / 2 << '\n';
// ③ 文本
std::cout << "输入一行文本:";
std::string text;
std::getline(std::cin, text);
std::cout << "字符数:" << text.size() << '\n';
std::cout << "第一个字符:" << text[0] << '\n';
std::cout << "前 5 个字符:" << text.substr(0, 5) << '\n';
std::cout << "重复两遍:" << text + text << '\n';
}
运行:
$ g++ -std=c++20 -Wall -Wextra inspect.cpp -o inspect
$ ./inspect
程序会等你输入一行,输入完按回车,它就输出四项信息。
第 1--3 周的验收
- 能说出
int和double的区别,知道7 / 2和7.0 / 2为什么结果不同 - 能说出什么时候该给一个名字加
const - 能把两个整数相除的结果变成小数,并说清
static_cast<double>做了什么 - 能用
auto声明变量,并说出手写类型和auto的区别在哪 - 能用
std::string存一段文本,并用size()、[]、substr、+取信息 - 能用
std::getline读入一整行 -
inspect.cpp能编译运行,输入自己的名字能正确输出字符数
下一篇给这个工具加功能:让它能数出「这行有几个词」「出现没出现过某个子串」。这需要循环和判断,也是把它们引出来的合适时机。
附录 · 术语表
本单元出现的词。不要求记住,读的时候遇到不懂的回来查一眼。
变量与类型
变量(variable) --- 给一块数据起的名字。int count = 0; 声明了一个叫 count 的变量。
类型(type) --- 描述「这块数据是什么、能做哪些运算」。int 是整数类型,std::string 是文本类型。
声明(declaration) --- 语句 int count = 0; 的前半部分,告诉编译器「这里有个整数变量叫 count」。
初始化(initialization) --- 声明时给出的初值。声明时不写初值,变量里是未定义的内容。
int --- 整数类型。
double --- 双精度浮点数类型,用来存小数。
bool --- 布尔类型,只有 true 和 false 两个值。
char --- 单个字符的类型。字符字面量用单引号,例如 'A'。
std::string --- C++ 的文本类型,自己管理内存。
字符串字面量 --- 写在双引号里的文本,例如 "C++"。
long long --- 范围更大的整数类型。用法和 int 完全一样,需要装超过约 21 亿的数时换掉类型名即可。
修饰与推导
const --- 写在类型前面,表示这个值定下来之后不再改。改它会编译不过。
auto --- 让编译器从右边的初值推导变量的类型。auto x = 1.5; 里 x 就是 double。
运算
整数除法 --- 两个整数相除,结果还是整数,小数部分被丢弃。7 / 2 得 3。
浮点除法 --- 参与运算的数里至少有一个是小数类型时,结果保留小数。7.0 / 2 得 3.5。
类型转换(cast) --- 把一个值从一种类型转成另一种。C++ 的写法是 static_cast<目标类型>(值),C 的写法是 (目标类型)值。
static_cast<double>(a) --- 把 a 转成 double。两个整数相除想要小数结果时,用它转其中一个。
溢出(overflow) --- 计算结果超出类型能表示的范围,值会绕回来,不报错。int 里两个 20 亿相加会得到负数。
运算符 + --- 用在数字上是加法,用在字符串上是拼接。同一个符号,两套含义,由类型决定。
输入与输出
std::cin --- 标准输入,从键盘读数据。
std::cout --- 标准输出,把数据送到屏幕。
>> --- 输入运算符,从 std::cin 把数据读到右边的变量里。
std::getline(std::cin, text) --- 读入一整行(直到回车),存进 text。
\n --- 换行符。
字符串的操作
.size() --- 返回字符串的字符数。
下标 [i] --- 取第 i 个字符。下标从 0 开始。
.substr(pos, n) --- 从位置 pos 起取 n 个字符。
.empty() --- 返回这个字符串是不是空的。
.find(...) --- 找子串出现的位置。没找到时返回 std::string::npos。本单元还没用到,下一单元会用它。