变量和基本类型
认识 C++ 的数据类型家族,会声明并初始化变量,理解类型如何影响内存布局和运算规则
学习目标
- 能用自己的话解释类型系统在 C++ 程序中的角色——类型决定了变量占多大内存、能存什么值、能做什么运算
- 能正确声明并初始化不同类型的变量,区分声明、定义、初始化、赋值的差别
- 能回答:如果把
unsigned int赋值为-1会怎样?代码块内层和外层声明了同名变量,内层用哪个?
为什么 C++ 中要给每个数据贴一张"标签"?
想象你面前有一排不同大小的盒子:火柴盒、鞋盒、收纳箱、搬家纸箱。你要放的是一颗纽扣——扔进搬家纸箱也行,但太浪费;要放的是一床被子——硬塞火柴盒肯定不行。
在 C++ 里,数据也有「盒子规格」:每种数据都得放进合适大小、合适样式的盒子里。你把一个整数放进一个小盒子,它干干净净刚好装下;把一个带小数的数字放进另一种盒子,它就按那种盒子的规矩摆放。要是所有东西都往同一种「万能麻袋」里塞呢? 那电脑就帮不了你检查错误,空间要么白白浪费、要么根本装不下。
这一章就来认识 C++ 里这些「盒子」的种类、贴标签的规矩,以及怎么让数据在该出现的地方出现、在该消失的时候消失。
类型——给内存里的每个格子划定规矩
在 C++ 中,↡编程语言里给数据规定的类别,决定了一个数据占多大内存、能存什么范围的值、能对它做哪些运算。C++ 是强类型语言——每个变量的类型在编译时就定下来了。 是程序最基本的「规矩」。任何一个变量在被使用之前,它的类型必须是确定的——编译器要在编译阶段就搞清楚:这个变量得分配几字节的内存、它的值能有多大范围、能对它做加减乘除还是不能。
类型不是运行时才起作用的概念——它从编译器看到你代码的第一刻起就参与工作。所以它处在程序编写的第一关口:在你敲完代码、点编译的那一刻,类型系统就开始逐行检查你的变量声明是否合法、运算是否合理。
基本算术类型——C++ 里有哪几种盒子
C++ 内置了以下几种 ↡语言自带的基础类型,如 int、float、char 等,不需要 include 任何头文件就能直接用。与之相对的是标准库提供的高级类型(如 string、vector)。:
- 整型:
short(2 字节)、int(4 字节)、long(4 或 8 字节)、long long(8 字节)——存整数 - 浮点型:
float(4 字节,约 7 位有效数字)、double(8 字节,约 15 位有效数字)、long double(更大) - 字符型:
char(1 字节)、wchar_t(宽字符,2 或 4 字节) - 布尔型:
bool(1 字节)——只能存true或false
下面这张对照表帮你直观感受各类型在内存里的"个头":
类型决定了三件事:
- 内存大小:
int占 4 字节,short只占 2 字节——就像选盒子大小 - 取值范围:
unsigned char只能存 0255,127signed char是 -128 - 能做什么运算:
float能算小数除法但可能有舍入误差,bool不能做加减
带符号与无符号——同一段二进制,两种解读
当你写 int,它默认就是 signed int——能存正数、负数、零。如果你前面加一个 unsigned,比如 unsigned int,那它就只能存 ≥0 的整数。
两者的核心差别在于最高位的解读方式。C++ 使用 ↡计算机表示有符号整数的一种编码方式。在补码系统中,最高位为 1 表示负数:0→所有位全 0,-1→所有位全 1(11111111)。(two's complement)来表示有符号整数——在补码里,最高位为 1 就代表负数。下面这张图直观对比了同一个 8-bit 位模式在两种解读下的不同含义:
无符号类型常用来表示那些不可能为负数的量:数组下标、大小(sizeof 的返回值)、位掩码等。但在一般算术中,默认用 signed int 即可。
字面值常量——写在代码里的"裸值"
你写在程序里的那些直接写出来的值——42、3.14、'a'、"Hello"——叫作 ↡写在代码里的原始值本身,如 42、3.14、'a'、'Hello'。编译器从它的书写形式就能推断出它的类型。。编译器从字面值的写法就能推断出它的类型,不同的书写形式对应不同的类型:
几条记忆关键点:
- 整数
42默认是int;加u后缀变unsigned、加L变long、加LL变long long - 浮点数
3.14默认是double;加f后缀才能变成float - 字符用单引号
'a'、字符串用双引号"a"(末尾暗藏一个\0结束符) - 相邻字符串会被自动拼接:
"Hello" " World"等价于"Hello World"
变量——给内存格子起个名字
↡程序中一个有名字的、有类型的、可以存值并在生命周期内值可以被改变的内存空间。变量必须先声明(告诉编译器名字和类型)才能使用。 就是给一块内存格子起的名字,加上一个类型标牌。在 C++ 里,变量必须先声明再使用。
几个容易混淆的概念必须分清:
| 概念 | 含义 | 例子 |
|---|---|---|
| ↡告诉编译器「这个名字、这个类型」存在,但不一定分配空间。声明可以出现多次。 | 告诉编译器名字和类型 | extern int x;(仅声明,不在本文件定义) |
| ↡在声明的同时分配了内存空间、给了初始值(或不给)。在一个程序里,每个变量只能有一处定义。 | 分配内存空间 | int x = 0;(声明 + 定义) |
| ↡变量定义时给它第一个值。C++ 允许多种初始化方式:传统 = 号、花括号列表初始化(推荐)、括号初始化。 | 创建时给第一个值 | int x = 0; 或 int x{0};(推荐!) |
| ↡变量已经存在、已经初始化过的前提下,把一个新的值覆盖进去。不要跟初始化混为一谈——初始化是诞生时的事,赋值是之后的事。 | 把已有变量换个新值 | x = 5;(不是初始化,是赋值) |
C++11 引入的 ↡用花括号 {} 包围初始值的写法,如 int x{0}。它额外做了安全检查:不允许隐式截断的类型转换(如 double→int 花括号包围会报错或警告)。 是推荐的初始化方式——用花括号 {}:
int x = 0; // 老写法(拷贝初始化)
int x{0}; // 列表初始化——更安全!会拒绝截断的窄化转换
double d{3.14}; // ✅
int i{d}; // ❌ 警告或报错:double → int 会丢失信息!
int i = d; // ⚠️ 不会报错,悄悄截断为 3(危险!){} 花括号不会允许你"悄悄地"把 double 截成 int——这是列表初始化比传统 = 写法更安全的核心原因。
作用域——变量该在哪儿"出生"、在哪儿"死掉"
↡变量在程序中的可见范围——从它的声明点开始,到它所在那对花括号 { } 结束为止。出了这个范围,变量就被销毁,再也不能访问它。 决定了变量在哪里可以看见、在哪里已经不存在了。C++ 的作用域由花括号 {} 划定边界。
嵌套作用域的核心规则:
- 外层可见内不会:外层不能访问内层声明的变量——
innerOnly出了内层花括号就销毁了 - 内层可见外层:内层可以访问外层的变量(前提是同名变量没有被遮蔽)
- 同名遮蔽(shadowing):内层声明了和外部同名的变量时,内部用内层的,外层的在内部暂时看不见
#include <iostream>
int global = 42; // 全局作用域
int main() {
int local = 10; // main 函数作用域
{
int local = 20; // 遮蔽了外层的 local!
int innerOnly = 30; // 只在这个块里存在
std::cout << local; // 输出 20(内层的)
std::cout << global; // 输出 42(全局的,能看见)
std::cout << innerOnly; // 输出 30(本块定义的)
}
std::cout << local; // 输出 10(外层又可见了!)
// std::cout << innerOnly; // ❌ 编译错误!innerOnly 不存在了
return 0;
}Chapter 2 的完整类型契约
前面的内置类型、变量与作用域只覆盖 Chapter 2 的一部分。官方目录还要求掌握 Compound Types、const Qualifier、Dealing with Types 和 Defining Our Own Data Structures。它们共同回答:一个名字是否拥有对象、能否修改对象、类型从哪里来,以及怎样定义领域类型。
↡由其他类型组合而成并增加声明符语义的类型;本章重点是引用和指针。中,引用是已有对象的别名,定义时必须绑定且通常不能改绑;指针是独立对象,保存地址,可以改指向、比较或取空值。int& ref = value; 修改 ref 就修改 value;int* ptr = &value; 中 ptr 与 *ptr 分别是地址对象和被指对象。
↡把对象或访问路径标记为不可修改的类型约束;它还区分顶层 const 与底层 const。不等于编译期常量。const int n = read(); 的值可在运行时初始化;constexpr int width = 8 * 8; 则要求 constant expression。指针要分别读“指针本身 const”与“指向 const”:int* const p 不能改地址,const int* p 不能经 p 改对象。
↡让编译器按规定从初始化式或表达式推得类型的机制;auto 与 decltype 的引用和 const 保留规则不同。包括 auto 与 decltype。auto 通常忽略顶层 const 和引用后推导变量类型;decltype(expr) 按表达式类别决定是否得到引用,decltype((name)) 对左值通常是引用,而 decltype(name) 对未加括号变量得到声明类型。
int value{42};
int& ref = value;
const int limit{64};
const int* pointerToConst = &limit;
auto copy = ref; // int,复制 value
decltype(ref) alias = value; // int&,仍是引用↡由程序员声明的新类型,把相关数据成员放进同一个命名和生命周期边界;本章以 Sales_data 为例。从 struct Sales_data 开始。定义应放在头文件并使用 include guard,保证所有 translation units 看到同一定义;对象定义则在需要存储的位置出现。Chapter 7 会继续加入封装、成员函数和构造函数,本章先建立“类型定义是共享契约”的观念。
动手:跟踪一个变量从出生到死亡
下面用一个最简程序,分步追踪变量的**出生(定义)→ 使用(运算/输出)→ 遮蔽(内层同名)→ 死亡(离开作用域)**的完整生命周期。
猜一猜:在内层代码块里把
local改成 20 并输出,出了内层local又变成 10。变量到底是"被改回来了"还是"根本没被改"?通过下面 Stepper 一步步看解释。
① 全局变量出生
程序启动后,最先创建全局作用域里的变量。int global = 42; 在程序整个运行期间都活着——它在 main 之前就已经分配好内存、赋予了初始值 42。全局变量存在「静态存储区」,生命周期 = 整个程序的运行时间。在跟踪它的去向前,先借下面这张对照表认识一下 int 这类变量在内存里占多大——后面每一步栈上的格子都按这个尺寸来分配。
代码速查:声明与初始化的写法
C++ 变量声明和初始化有几种不同写法,下面是在你需要时快速对照的关键例子:
// 定义 + 初始化
int a = 0; // 拷贝初始化(C 风格写法)
int b{0}; // 列表初始化(C++11,安全!拒绝窄化转换)
int c(0); // 直接初始化
int d; // 仅定义,未初始化(值是垃圾!不推荐)
// 不同后缀决定字面值类型
auto x = 3.14; // x 是 double
auto y = 3.14f; // y 是 float
auto z = 42u; // z 是 unsigned int
// 列表初始化的安全检测
double pi = 3.14159;
// int bad{pi}; // ❌ 报错:窄化转换
int good = pi; // ⚠️ 通过但截断成 3auto 关键字能让编译器自动推导变量类型(从初始值推断),在你不关心具体类型或类型名很长时很方便——auto 会根据初始值来推断变量的类型。
容易踩的坑
小结
- C++ 是强类型语言——每个变量在编译时就确定了类型、内存大小、取值范围和允许的运算
- 基本算术类型有整型(short/int/long/long long)、浮点(float/double/long double)、字符(char/wchar_t)、布尔(bool)
- 有符号和无符号类型的差别在于最高位的解读方式——混用会导致隐式转换和意料之外的结果
- 列表初始化
int x{0};比传统=写法更安全——它会拒绝窄化转换 - 作用域由花括号
{}划定——变量从声明点活到所在块}结束;内层同名变量会遮蔽外层但不会修改它
练习
练习 1(改代码型) 下面这段代码有三处问题。找出它们并解释每处会出什么错,然后给出修正后的完整代码。
#include <iostream>
int main() {
unsigned int x = -1;
int y;
std::cout << y << std::endl;
double pi = 3.14159;
int rounded{pi};
std::cout << rounded << std::endl;
return 0;
}练习 2(问答型) 读下面这段程序,写出每一行 cout 的输出结果,并解释原因:
#include <iostream>
int main() {
int x = 10;
{
int x = 100;
std::cout << x << std::endl; // ① 输出?
}
std::cout << x << std::endl; // ② 输出?
{
x = 50;
std::cout << x << std::endl; // ③ 输出?
}
std::cout << x << std::endl; // ④ 输出?
return 0;
}练习 3(综合型) 把 char 类型变量分别赋值为 65、'A'、'\x41',用 cout 输出。它们输出一样吗?为什么?再试试 int i = 'A'; std::cout << i; 输出多少?解释这几种写法之间的关系。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 类型(type)
数据的一张"规格标签"。它告诉编译器:这个数据占多大内存(几字节)、能存什么范围的值(比如 0
255 还是 -128127)、能对它做哪些运算(比如整数能加减、字符串能拼接)。C++ 是强类型语言——每个变量在编译时就必须确定类型,编译器会帮你检查类型是否匹配。详见本章「类型」一节。- 算术类型(arithmetic type)
C++ 内置的基础数字类型,包括整型(int/short/long/long long)、浮点型(float/double/long double)、字符型(char/wchar_t)、布尔型(bool)。它们不需要 include 任何头文件就能直接用。详见本章「基本算术类型」一节和 TypeSizeDiagram 图示。
- 补码
计算机里表示有符号整数的方式。在补码里,最高位是"符号位"——为 1 表示负数、为 0 表示 ≥0。0 是所有位全 0,-1 是所有位全 1(11111111),-128 是 10000000。unsigned 把同一个位模式当纯正数解读,所以同样的 8-bit 位模式 11111111,unsigned 看是 255,signed(补码)看是 -1。详见本章「带符号与无符号」一节和 SignedUnsignedDiagram 图示。
- 复合类型
在其他类型上附加声明符形成的类型。本章核心是引用和指针:引用绑定已有对象,指针保存对象地址,两者的对象身份和可改绑能力不同。
- const 限定符
限制通过某个对象或访问路径修改值的类型约束;指针声明中必须区分指针本身 const 和指向对象 const,
constexpr还要求编译期常量表达式。- 类型推断
由编译器从初始化式或表达式按规则确定类型的机制,主要包括
auto与decltype,二者保留 const 和引用的规则不同。- 自定义数据结构
用
struct或class定义的领域类型。本章以Sales_data组织 ISBN、销量和收入,并强调定义放在有 include guard 的头文件中。- 字面值常量(literal)
直接写在代码里的"裸值"——比如 42、3.14、'a'、"Hello"。编译器从写法就能推断类型:没有小数点的整数是 int,带小数点的数是 double,单引号括起来的是 char,双引号括起来的是字符串。后缀可以改变默认类型:42u 是 unsigned int,3.14f 是 float。详见本章「字面值常量」一节和 LiteralTypesDiagram 图示。
- 变量(variable)
程序中一个有名字、有类型、可以存值的内存空间。变量必须先声明(告诉编译器"有这么个东西、它是什么类型")然后才能使用。创建变量时给它第一个值叫"初始化",之后给它换新值叫"赋值"——两者不一样。出了它所在的花括号,变量就自动被销毁。详见本章「变量」一节。
- 声明(declaration)
告诉编译器「这个名字、这个类型存在」,但不一定分配内存空间(如
extern int x;)。声明可以多次出现——只要不冲突。而"定义"一定是声明的一种,同时分配了内存。- 定义(definition)
声明 + 分配内存空间。
int x = 0;既声明了变量 x 的类型和名字,又给它分配了 4 字节内存并写入初始值。在一个程序里,每个变量只能有一处定义(否则链接器报重定义错误)。- 初始化(initialization)
变量出生时(定义时)给它第一个值。不是之后改值(那叫赋值)。C++ 有多种初始化写法:传统
int x = 0;(拷贝初始化)、推荐的int x{0};(列表初始化——更安全,会拒绝窄化转换)、int x(0);(直接初始化)。如果不初始化就使用,变量里是上一段程序留下的随机"垃圾值"。详见本章「变量」一节。- 赋值(assignment)
变量已经存在、已经有初始值之后,把一个新的值覆盖进去的操作。
x = 5;是赋值(没有类型名在前)。不要和初始化混淆——初始化是变量出生那一刻的事,赋值是之后随时可以做的。- 列表初始化(list initialization)
C++11 引入的初始化写法:用花括号
{}包围初始值,如int x{0};。它比传统=写法更安全——会自动拒绝窄化转换(比如int x{3.14}编译器会报错,因为 double→int 会丢失小数部分)。详见本章「变量」一节。- 作用域(scope)
变量在程序里的"可见范围"——从它声明的地方开始,到包含它的那对花括号
}结束。出了作用域,变量自动销毁、内存还给系统。嵌套作用域的核心规则:内层可以看见外层的变量,但同名声明会遮蔽外层;出了内层,内层变量消失、外层重新可见。详见本章「作用域」一节和 ScopeNestingDiagram 图示。