单元七 · 零基础路线图-第 1–3 周·变量、类型与字符串

专栏《Zero to C++》· 单元 07

阅读时间约 12 分钟。这三周学变量、类型和字符串,做出一个能读入文本并输出信息的小工具。


这三周的目标

第 0 周让程序跑起来了,但那个程序只会说一句写死的话。这三周要让程序接受你的输入 ,并回答你

做法是写一个文本小助手:你输入一行字,它告诉你这行字有多少个字符、开头是什么、重复两遍长什么样。到第 4--6 周会给它加上「数词数」「找子串」这类功能,这两周先搭好输入和文本处理的部分。

这三周仍然不碰循环和判断。先让一条直线执行到底的程序完全跑通,比一开始就塞进各种控制流更容易看清每一步在做什么。

变量:给数据起个名字

cpp 复制代码
int count = 0;        // 声明一个整数变量 count,初值是 0
double ratio = 0.5;   // 声明一个小数变量 ratio

intdouble类型countratio变量名 ,等号右边是初值

三条习惯,越早养成越好:

  1. 声明时就给初值。 int count; 语法上合法,但 count 里是什么由运气决定。写 int count = 0; 就没有这个问题。
  2. 名字说清用途。 int n; 不如 int word_count;
  3. 一个变量只在它需要的地方声明。 不要把变量堆在函数开头------那是 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 intconst doubleconst 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),和整数除法一样属于「不报错,只是算错」。

零基础阶段不用背各类型的精确范围,记住三件事就够:

  1. int 装不下超过约 21 亿的数;
  2. 循环里反复累加、连乘,最容易越界;
  3. 真需要更大的整数时用 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

n42x1.5name.size() 是 4。)

关于 auto 有两点要记住:

  1. auto 不会让类型消失 。变量的类型和你手写的时候一模一样,只是名字由编译器填上。写成 auto x = 1.5 之后,x 就是一个 double,整数除法、溢出那些规则照样对它生效。
  2. 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 周的验收

  • 能说出 intdouble 的区别,知道 7 / 27.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 --- 布尔类型,只有 truefalse 两个值。

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。本单元还没用到,下一单元会用它。

相关推荐
Benny_Tang42 分钟前
P7514 [省选联考 2021 A/B 卷] 卡牌游戏 题解
c++·算法
geovindu1 小时前
java: Strategy Pattern
java·开发语言·后端·设计模式·策略模式·行为模式
residual_fan1 小时前
经验模态重构:直向工业时间序列数据的数据扩增方法
人工智能·算法·重构·数据挖掘·数据分析
念越1 小时前
Python基础语法(一):变量、类型与程序控制流
开发语言·网络·python
程序猿编码1 小时前
LLM 技术迁移音频领域:基于 GGML 搭建 Roformer,端侧音乐人声分离实现详解
c++·llm·音视频·transformer·模型推理·ggml
wuminyu1 小时前
Kafka的写入延迟与磁盘IO抖动原理分析
java·linux·c语言·jvm·c++
buhuizhiyuci1 小时前
线程的互斥
linux·运维·服务器·c++
资讯综合1 小时前
2026全国云渲染网站排名 不同需求用户适配榜单
开发语言·前端·javascript
aramae1 小时前
模拟实现strcpy(字符串拷贝)(C语言)
java·c语言·开发语言·算法