变量和基本类型

认识 C++ 的数据类型家族,会声明并初始化变量,理解类型如何影响内存布局和运算规则

学习目标

  • 能用自己的话解释类型系统在 C++ 程序中的角色——类型决定了变量占多大内存、能存什么值、能做什么运算
  • 能正确声明并初始化不同类型的变量,区分声明、定义、初始化、赋值的差别
  • 能回答:如果把 unsigned int 赋值为 -1 会怎样?代码块内层和外层声明了同名变量,内层用哪个?

为什么 C++ 中要给每个数据贴一张"标签"?

想象你面前有一排不同大小的盒子:火柴盒、鞋盒、收纳箱、搬家纸箱。你要放的是一颗纽扣——扔进搬家纸箱也行,但太浪费;要放的是一床被子——硬塞火柴盒肯定不行。

在 C++ 里,数据也有「盒子规格」:每种数据都得放进合适大小、合适样式的盒子里。你把一个整数放进一个小盒子,它干干净净刚好装下;把一个带小数的数字放进另一种盒子,它就按那种盒子的规矩摆放。要是所有东西都往同一种「万能麻袋」里塞呢? 那电脑就帮不了你检查错误,空间要么白白浪费、要么根本装不下。

这一章就来认识 C++ 里这些「盒子」的种类、贴标签的规矩,以及怎么让数据在该出现的地方出现、在该消失的时候消失。

类型——给内存里的每个格子划定规矩

在 C++ 中, 是程序最基本的「规矩」。任何一个变量在被使用之前,它的类型必须是确定的——编译器要在编译阶段就搞清楚:这个变量得分配几字节的内存、它的值能有多大范围、能对它做加减乘除还是不能。

类型不是运行时才起作用的概念——它从编译器看到你代码的第一刻起就参与工作。所以它处在程序编写的第一关口:在你敲完代码、点编译的那一刻,类型系统就开始逐行检查你的变量声明是否合法、运算是否合理。

基本算术类型——C++ 里有哪几种盒子

C++ 内置了以下几种

  • 整型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 字节)——只能存 truefalse

下面这张对照表帮你直观感受各类型在内存里的"个头":

类型字节符号最小值最大值bool1无符号falsetruetrue/false 不可做算术char1有符号-128127unsigned char1无符号0255wchar_t2/4无符号0依平台宽字符short2有符号-32,76832,767unsigned short2无符号065,535int4有符号≈ -2.1×10⁹≈ 2.1×10⁹unsigned int4无符号0≈ 4.3×10⁹long4/8有符号依平台依平台≥ intlong long8有符号≈ -9.2×10¹⁸≈ 9.2×10¹⁸float4有符号~1.2×10⁻³⁸~3.4×10³⁸7位有效数字double8有符号~2.2×10⁻³⁰⁸~1.8×10³⁰⁸15位有效数字注:字节数为常见平台典型值。long 在 64-bit Windows 为 4 字节,Linux/macOS 为 8 字节。float 约 7 位有效数字,double 约 15 位。wchar_t 在 Windows 为 2 字节,Linux/macOS 为 4 字节。
C++ 基本算术类型一览:各类型所占字节数、符号属性及典型平台的取值范围。 无符号类型只能存 ≥0 的值,有符号类型一半给正数一半给负数。

类型决定了三件事

  1. 内存大小int 占 4 字节,short 只占 2 字节——就像选盒子大小
  2. 取值范围unsigned char 只能存 0255,signed char 是 -128127
  3. 能做什么运算float 能算小数除法但可能有舍入误差,bool 不能做加减

带符号与无符号——同一段二进制,两种解读

当你写 int,它默认就是 signed int——能存正数、负数、零。如果你前面加一个 unsigned,比如 unsigned int,那它就只能存 ≥0 的整数

两者的核心差别在于最高位的解读方式。C++ 使用 (two's complement)来表示有符号整数——在补码里,最高位为 1 就代表负数。下面这张图直观对比了同一个 8-bit 位模式在两种解读下的不同含义:

8-bit 位模式unsigned 值signed 值(补码)000000000000000001110111111112712710000000128−12811111111255−1同一个 8-bit 位模式,用 unsigned 看最大值 255,用 signed(补码)看是 −128 ~ 127。关键规律:高位是 1 时 signed 解读为负数,是 0 时非负;最高位是「符号位」。unsigned 范围 [0, 2ⁿ−1];signed 补码范围 [−2ⁿ⁻¹, 2ⁿ⁻¹−1]
同一个二进制位模式,unsigned 解读全是 ≥0,signed(补码)把高位为 1 的解读为负数。 理解这一差别是避免 signed/unsigned 混用导致 bug 的关键。

无符号类型常用来表示那些不可能为负数的量:数组下标、大小(sizeof 的返回值)、位掩码等。但在一般算术中,默认用 signed int 即可。

字面值常量——写在代码里的"裸值"

你写在程序里的那些直接写出来的值——423.14'a'"Hello"——叫作 。编译器从字面值的写法就能推断出它的类型,不同的书写形式对应不同的类型:

几条记忆关键点:

  • 整数 42 默认是 int;加 u 后缀变 unsigned、加 Llong、加 LLlong long
  • 浮点数 3.14 默认是 double;加 f 后缀才能变成 float
  • 字符用单引号 'a'、字符串用双引号 "a"(末尾暗藏一个 \0 结束符)
  • 相邻字符串会被自动拼接:"Hello" " World" 等价于 "Hello World"

变量——给内存格子起个名字

就是给一块内存格子起的名字,加上一个类型标牌。在 C++ 里,变量必须先声明再使用

几个容易混淆的概念必须分清:

概念含义例子
告诉编译器名字和类型extern int x;(仅声明,不在本文件定义)
分配内存空间int x = 0;(声明 + 定义)
创建时给第一个值int x = 0;int x{0};(推荐!)
把已有变量换个新值x = 5;(不是初始化,是赋值)

C++11 引入的 是推荐的初始化方式——用花括号 {}

int x = 0;    // 老写法(拷贝初始化)
int x{0};     // 列表初始化——更安全!会拒绝截断的窄化转换
double d{3.14};      // ✅
int i{d};            // ❌ 警告或报错:double → int 会丢失信息!
int i = d;           // ⚠️ 不会报错,悄悄截断为 3(危险!)

{} 花括号不会允许你"悄悄地"把 double 截成 int——这是列表初始化比传统 = 写法更安全的核心原因。

作用域——变量该在哪儿"出生"、在哪儿"死掉"

决定了变量在哪里可以看见、在哪里已经不存在了。C++ 的作用域由花括号 {} 划定边界。

全局作用域(整个文件可见)int global = 42;← 整个程序都能看到它main 函数作用域int local = 10;← main 范围内可见内层代码块 {} 作用域int local = 20;← 遮蔽了外层的 local!int innerOnly = 30;← 只在这里面存在此作用域内可以访问:✓ global(全局) ✓ local=20(当前块,遮蔽了外层的 10)✓ innerOnly(本块) ✗ 无法访问外层的 local=10(被同名遮蔽了)同名遮蔽内层作用域可以看见外层的变量,但同名声明会遮蔽(shadow)外层的。出了内层块,innerOnly 就销毁了。规则:从声明点到所在块结束的花括号 } 就是它的「寿命」区间,越界即消失。
C++ 嵌套作用域:全局 → main 函数 → 内层代码块。每层花括号 {...} 定义一个作用域, 内层可访问外层变量;同名声明在内层会遮蔽外层;变量仅在声明它的块内存在。

嵌套作用域的核心规则

  1. 外层可见内不会:外层不能访问内层声明的变量——innerOnly 出了内层花括号就销毁了
  2. 内层可见外层:内层可以访问外层的变量(前提是同名变量没有被遮蔽)
  3. 同名遮蔽(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 Typesconst QualifierDealing with TypesDefining Our Own Data Structures。它们共同回答:一个名字是否拥有对象、能否修改对象、类型从哪里来,以及怎样定义领域类型。

中,引用是已有对象的别名,定义时必须绑定且通常不能改绑;指针是独立对象,保存地址,可以改指向、比较或取空值。int& ref = value; 修改 ref 就修改 valueint* ptr = &value;ptr*ptr 分别是地址对象和被指对象。

不等于编译期常量。const int n = read(); 的值可在运行时初始化;constexpr int width = 8 * 8; 则要求 constant expression。指针要分别读“指针本身 const”与“指向 const”:int* const p 不能改地址,const int* p 不能经 p 改对象。

包括 autodecltypeauto 通常忽略顶层 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&,仍是引用

struct Sales_data 开始。定义应放在头文件并使用 include guard,保证所有 translation units 看到同一定义;对象定义则在需要存储的位置出现。Chapter 7 会继续加入封装、成员函数和构造函数,本章先建立“类型定义是共享契约”的观念。

动手:跟踪一个变量从出生到死亡

下面用一个最简程序,分步追踪变量的**出生(定义)→ 使用(运算/输出)→ 遮蔽(内层同名)→ 死亡(离开作用域)**的完整生命周期。

猜一猜:在内层代码块里把 local 改成 20 并输出,出了内层 local 又变成 10。变量到底是"被改回来了"还是"根本没被改"?通过下面 Stepper 一步步看解释。

分步1 / 5

① 全局变量出生

程序启动后,最先创建全局作用域里的变量。int global = 42; 在程序整个运行期间都活着——它在 main 之前就已经分配好内存、赋予了初始值 42。全局变量存在「静态存储区」,生命周期 = 整个程序的运行时间。在跟踪它的去向前,先借下面这张对照表认识一下 int 这类变量在内存里占多大——后面每一步栈上的格子都按这个尺寸来分配。

类型字节符号最小值最大值bool1无符号falsetruetrue/false 不可做算术char1有符号-128127unsigned char1无符号0255wchar_t2/4无符号0依平台宽字符short2有符号-32,76832,767unsigned short2无符号065,535int4有符号≈ -2.1×10⁹≈ 2.1×10⁹unsigned int4无符号0≈ 4.3×10⁹long4/8有符号依平台依平台≥ intlong long8有符号≈ -9.2×10¹⁸≈ 9.2×10¹⁸float4有符号~1.2×10⁻³⁸~3.4×10³⁸7位有效数字double8有符号~2.2×10⁻³⁰⁸~1.8×10³⁰⁸15位有效数字注:字节数为常见平台典型值。long 在 64-bit Windows 为 4 字节,Linux/macOS 为 8 字节。float 约 7 位有效数字,double 约 15 位。wchar_t 在 Windows 为 2 字节,Linux/macOS 为 4 字节。
C++ 基本算术类型一览:各类型所占字节数、符号属性及典型平台的取值范围。 无符号类型只能存 ≥0 的值,有符号类型一半给正数一半给负数。

代码速查:声明与初始化的写法

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;      // ⚠️ 通过但截断成 3

auto 关键字能让编译器自动推导变量类型(从初始值推断),在你不关心具体类型或类型名很长时很方便——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)

数据的一张"规格标签"。它告诉编译器:这个数据占多大内存(几字节)、能存什么范围的值(比如 0255 还是 -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 还要求编译期常量表达式。

类型推断

由编译器从初始化式或表达式按规则确定类型的机制,主要包括 autodecltype,二者保留 const 和引用的规则不同。

自定义数据结构

structclass 定义的领域类型。本章以 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 图示。

资料与写作方式声明

本章以C++ Primer, Fifth Edition, Chapter 2: Variables and Basic Types权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…