第 03 章

第 3 章:变量、数据类型与进制

本章对应长视频 3:00:43–4:16:37。先说明变量只是「一块有名字的内存」,再用二进制、八进制、十六进制解释内存里的 0 和 1 怎样变成人可读的数,然后逐一讲清整数、修饰符与浮点数的字节数、取值范围、三种初始化方式与窄化陷阱。

Chapter 3: Variables, Data Types and Number Systems

Covers 3:00:43–4:16:37 of the long video. It first establishes that a variable is a named piece of memory, then uses binary, octal and hexadecimal to explain how the zeros and ones become readable numbers, and finally gives the size, range, three initialisation forms and narrowing traps of integers, integer modifiers and floating-point types.

首发于 2022.02.17中 / EN
系统课程cppdata-typesintegersfloating-pointbinary
语言
字号
目录
篇内目录

这一章在讲什么

第 3 章覆盖 3:00:43–4:16:37,是整门课第一次正面讲"数据"。要回答三个问题:

  1. 变量到底是什么?——一块有名字的内存。
  2. 内存里只有 0 和 1,人怎么读?——用二进制、八进制、十六进制翻译。
  3. 不同的类型占多少字节、能表示多大范围?——整数、整数修饰符、浮点数各自的性质。

这一章的结论会一直用到指针和内存管理部分:所有数据最终都是内存中一组 0 和 1,类型决定这组位怎样被解释。

时间轴

时间内容
3:00:43回顾上一章;C++ 预定义的基本类型
3:03:26bit 与 byte;数据在内存中的表示
3:05:09进制:十进制、二进制、八进制、十六进制
3:16:15在 C++ 里写不同进制的字面量
3:21:36整数 int:变量是命名的内存
3:25:33三种初始化方式与窄化转换
3:27:01sizeof;变量命名规则
3:40:43整数修饰符:signed、unsigned、short、long
3:54:04浮点数:float、double、long double;精度与后缀
4:00:46无穷大、NaN 与科学计数法

变量:一块有名字的内存

C++ 预定义了一批基本类型:int、double、float、char、bool、void,以及用于类型推导的 auto。它们先在内存里划出固定大小的空间,再决定这些位怎样解释。

int age {27};

age 是那块内存在代码里的名字,int 决定它占 4 个字节、按整数解释,27 是写进去的位模式。变量不是抽象的"盒子",而是实实在在的一段地址。

bit、byte 与进制

计算机里每个 0 或 1 叫一个 bit;8 个 bit 组成一个 byte。8 位能表示 256 种组合(0–255),16 位能表示 65536 种。

十进制是"按权重展开"

数字 2371 可以写成:

2371=2×103+3×102+7×101+1×1002371 = 2\times10^3 + 3\times10^2 + 7\times10^1 + 1\times10^0

每一位的权重是 10 的幂。

二进制

二进制把权重换成 2 的幂。10010:

1×24+0×23+0×22+1×21+0×20=16+2=181\times2^4 + 0\times2^3 + 0\times2^2 + 1\times2^1 + 0\times2^0 = 16 + 2 = 18

100101 则是 32+4+1=3732+4+1=37。三位二进制能表示 0–7,四位能表示 0–15。一般地,nn 位能表示

0∼2n−1.0 \sim 2^n - 1 .

十六进制:四位一组

把二进制按 4 位分组,每组恰好对应一个十六进制符号(0–9、A–F)。例如

1111 0000 0011 1110 0110
  F    0    3    E    6      →  0xF03E6

32 位写成 8 个十六进制符号,比 32 个 0/1 好读得多。位数不是 4 的倍数时,在左边补 0(padding)再分组。

八进制:三位一组

八进制按 3 位分组,符号是 0–7。这种做法现在很少见,课程提它是为了完整。

在 C++ 里写字面量

int a = 15;        // 十进制
int b = 017;       // 八进制:前导 0,值是 15
int c = 0x0F;      // 十六进制:0x 前缀,值是 15
int d = 0b1111;    // 二进制:0b 前缀,C++14 起支持,值是 15

前导 0 是个陷阱:017 不是十七,而是八进制的十五。打印这四个变量,输出全是 15——它们在内存里是同一个数,只是写法不同。

整数:int

int 在大多数平台上占 4 个字节,用二进制存储。声明与初始化有三种写法:

int a = 10;     // 赋值初始化
int b {10};     // 花括号初始化(braced initialization)
int c (10);     // 函数式初始化(functional initialization)

未初始化会怎样

int elephants;   // 未初始化

这块内存里是垃圾值,读出来没有意义。花括号的空初始化 int count {}; 则保证得到 0。

窄化转换:三种写法的关键差别

int x {2.9};    // 编译错误:narrowing conversion
int y (2.9);    // 编译通过,静默截断为 2
int z = 2.9;    // 编译通过,静默截断为 2

C++20 里用花括号把浮点数塞进 int 会直接报错,这正是花括号初始化的价值:它把"悄悄丢数据"变成了"编译不过"。函数式与赋值式则会默默截断,2.9 变成 2,程序照样跑,结果却错了。

sizeof

std::cout << "sizeof(int): " << sizeof(int) << std::endl;       // 4
std::cout << "sizeof(x): "   << sizeof(x)   << std::endl;       // 4

既可以查类型,也可以查变量,单位是字节。int 在常见平台上就是 4 字节。

命名规则

  • 首字符必须是字母或下划线,不能是数字。
  • 之后可以是字母、数字、下划线。
  • 区分大小写:bikeCount 与 BikeCount 是两个变量。
  • 不能含空格或 +、- 这类符号。

整数修饰符

修饰符改变整数的符号性与宽度。它们只能用于整数类型,不能用于浮点数。

signed 与 unsigned

int a = 1;              // 默认 signed,可以存负数
signed int b = -300;    // 显式写法
unsigned int c = 4;     // 只能存非负数
unsigned int d = -5;    // 编译错误

两者占用的字节数相同(都是 4 字节),区别在于这 4 个字节怎样解释:有符号把最高位当符号位,无符号则全部当数值。因此范围不同:

unsigned:0∼2n−1,signed:−2n−1∼2n−1−1.\text{unsigned}: 0 \sim 2^n-1, \qquad \text{signed}: -2^{n-1} \sim 2^{n-1}-1 .

4 字节无符号约 0–42 亿,有符号约 −21 亿–21 亿。

short、long、long long

写法典型大小说明
short2 字节等价于 short int
int4 字节
long4 或 8 字节依赖平台,Windows 上通常是 4
long long8 字节范围极大

课程在 Windows + gcc 上实测:short 2 字节、int 4 字节、long 4 字节、long long 8 字节。换到 Linux,long 往往是 8 字节——所以不要假设 long 的大小,需要固定宽度时用 <cstdint> 里的 int32_t、int64_t。

short 能表示 −32768 到 32767,这个范围可以用上一节的公式验证:215=327682^{15}=32768。

浮点数

浮点数表示带小数点的数。C++ 有三个类型:

类型典型大小有效数字(十进制)
float4 字节约 7 位
double8 字节约 15–17 位
long double12 或 16 字节至少与 double 相当

"精度"指从第一个有效数字起能可靠表示多少位。数字 123456789012 有 12 位,float 存不下,double 可以。

后缀决定字面量的类型

float a = 1.23f;        // f 后缀
double b = 1.23;        // 默认就是 double
long double c = 1.23L;  // L 后缀

漏掉 f 是常见 bug:float x = 1.23456789; 里的字面量先按 double 解析,再转成 float,精度在转换时被截断。把变量类型改成 double 却没删 f 更隐蔽——变量是 double,存的却是 float 精度的值。

科学计数法

double big = 1.9e8;     // 1.9 × 10^8
double small = 3.498e-11; // 3.498 × 10^-11

e 后面的数字是 10 的幂次,正数向右移小数点,负数向左移。

无穷大与 NaN

浮点数可以除以零,结果不是崩溃,而是特殊值:

double a = 5.6;
double zero = 0.0;
double inf = a / zero;      // +inf
double nan = zero / zero;   // nan(not a number)
  • 正数除以 0 得到 inf,负数得到 -inf。
  • inf + 5 仍是 inf:无穷大加有限大还是无穷大。
  • 0.0 / 0.0 得到 nan,表示"这个运算没有有意义的数值结果"。nan 参与的大多数运算结果仍是 nan。

整数除以零是未定义行为,浮点数除以零有定义的结果——这是两类数在语义上的重要区别。

用 setprecision 观察精度

#include <iomanip>

std::cout << std::setprecision(20);
std::cout << 1.234567890123456789 << std::endl;

默认精度是 6 位。setprecision 只是"要求显示多少位",不能突破类型本身的精度上限:float 显示到第 8 位就开始出现垃圾数字,double 大约到第 17 位,再往后同样没有意义。

本章结论

  • 变量是一块有名字的内存;类型决定这块内存的大小和解释方式。
  • 内存里只有 0/1,二进制按 2 的幂展开,八进制三位一组,十六进制四位一组。
  • C++ 字面量:017 是八进制,0x0F 是十六进制,0b1111 是二进制。
  • 三种初始化方式中,花括号最安全:窄化会编译报错,另外两种会静默截断。
  • int 通常 4 字节;short 2 字节;long long 8 字节;long 的大小依赖平台。
  • signed/unsigned 改变符号性;short/long 改变宽度;修饰符只作用于整数。
  • 浮点数用 float/double/long double,精度分别约为 7、15、更高;字面量后缀 f、L 不能漏。
  • 浮点数除以零得到 inf,0.0/0.0 得到 nan,程序不会因此崩溃。

理解检验

1. 变量 int a = 017; 的值是多少?如果把它打印出来,为什么和 int b = 15; 的输出一样?

参考答案:017 是八进制字面量,值为 1×8+7=151\times8+7=15。它与 15 在内存里是同一个二进制数 0000 0000 0000 0000 0000 0000 0000 1111,只是源码里的写法不同;std::cout 默认按十进制输出,因此两者都打印 15。

2. int x {2.9}; 与 int y (2.9); 的行为差别是什么?为什么说前者更安全?

参考答案:int x {2.9}; 使用花括号初始化,从 double 到 int 属于窄化转换,C++20 会直接报 "narrowing conversion" 编译错误,程序无法生成。int y (2.9); 使用函数式初始化,编译器静默地把小数部分截掉,y 得到 2。前者把"丢数据"变成编译期可见的错误,后者把问题藏到运行时,所以前者更安全。赋值初始化 int z = 2.9; 与后者一样会静默截断。

3. 为什么 unsigned int 和 int 都占 4 字节,取值范围却不同?

参考答案:4 字节 = 32 位,两种类型能表示的状态数都是 2322^{32},区别在于如何解释最高位。signed 把最高位当符号位,剩下 31 位表示数值,范围是 −231-2^{31} 到 231−12^{31}-1,约 −21 亿到 21 亿;unsigned 把 32 位全部当数值,范围是 0 到 232−12^{32}-1,约 0 到 42 亿。字节数相同,解释方式不同。

4. 在 Windows 上 sizeof(long) 是 4,在 Linux 上常见是 8。这对写可移植代码意味着什么?

参考答案:long 的宽度由平台的数据模型决定(Windows 用 LLP64,Linux 用 LP64),因此同一段代码在两个平台上可能表现出不同的溢出行为。写可移植代码时不要假设 long 的大小:需要明确宽度就用 <cstdint> 的 int32_t/int64_t,或者改用 long long(C++ 标准保证至少 64 位)。涉及序列化或二进制文件格式时尤其不能依赖 long。

5. float x = 1.234567890123; 里漏写 f 会怎样?把变量类型改成 double 却不删 f 又会怎样?

参考答案:第一种情况下,字面量按 double 解析(精度约 15 位),再赋给 float(精度约 7 位),赋值时后 8 位左右被截掉,x 只能保留约 7 位有效数字。第二种情况下,变量是 double,但字面量带 f 后缀,先按 float 存储再提升为 double,看起来变量更宽,实际保存的仍是 float 精度的值,后面的数字是没有意义的噪声。两种情况都说明:字面量后缀与变量类型必须一致。

6. 整数 7 / 0 与浮点数 7.0 / 0.0 的结果有什么本质区别?

参考答案:整数除以零在 C++ 中是未定义行为,编译器不保证任何结果,程序可能崩溃或产生任意值。浮点数遵循 IEEE 754,7.0 / 0.0 有明确定义的结果 +inf,-7.0 / 0.0 得到 -inf,0.0 / 0.0 得到 nan;程序不会因此崩溃,但后续运算可能被 inf 或 nan 污染。前者是语言层面的错误,后者是数值层面的特殊值。

与前后章的关系

本章建立了"类型决定字节数与取值范围"的基本认识。第 4 章补齐剩下的基本类型:布尔、字符(ASCII)与 auto 类型推导,并演示 auto 配合无符号类型时的静默溢出陷阱。第 5 章开始对这些数据做运算:算术、优先级、自增自减、复合赋值与逻辑运算。

What this chapter covers

Chapter 3 spans 3:00:43–4:16:37 and is the first sustained look at data. It answers three questions:

  1. What is a variable? — a named piece of memory.
  2. Memory holds only zeros and ones; how do humans read it? — through binary, octal and hexadecimal.
  3. How many bytes does each type take, and what range can it hold? — integers, integer modifiers and floating-point numbers.

The conclusion carries all the way to pointers and memory management: every piece of data is ultimately a group of bits, and the type decides how those bits are interpreted.

Timeline

TimeTopic
3:00:43Recap; the fundamental types predefined by C++
3:03:26Bits, bytes and how data sits in memory
3:05:09Number systems: decimal, binary, octal, hexadecimal
3:16:15Writing literals in different bases
3:21:36int: a variable is named memory
3:25:33Three initialisation forms and narrowing conversions
3:27:01sizeof; variable naming rules
3:40:43Integer modifiers: signed, unsigned, short, long
3:54:04Floating point: float, double, long double; precision and suffixes
4:00:46Infinity, NaN and scientific notation

A variable is a named piece of memory

C++ predefines a set of fundamental types: int, double, float, char, bool, void, plus auto for type deduction. Each type reserves a fixed amount of memory and fixes how the bits are read.

int age {27};

age is the name in the source code for that memory; int says it occupies 4 bytes and is interpreted as an integer; 27 is the bit pattern written into it. A variable is not an abstract "box" but a real address.

Bits, bytes and number systems

Each 0 or 1 is a bit; eight bits make a byte. Eight bits give 256 combinations (0–255); sixteen give 65536.

Decimal is a weighted sum

2371=2×103+3×102+7×101+1×1002371 = 2\times10^3 + 3\times10^2 + 7\times10^1 + 1\times10^0

Each digit's weight is a power of ten.

Binary

Binary swaps the weights to powers of two. For 10010:

1×24+0×23+0×22+1×21+0×20=16+2=181\times2^4 + 0\times2^3 + 0\times2^2 + 1\times2^1 + 0\times2^0 = 16 + 2 = 18

100101 is 32+4+1=3732+4+1=37. Three bits cover 0–7, four cover 0–15. In general, nn bits cover

0∼2n−1.0 \sim 2^n - 1 .

Hexadecimal: groups of four

Split the bits into groups of four; each group is one hex digit (0–9, A–F). For example

1111 0000 0011 1110 0110
  F    0    3    E    6      →  0xF03E6

Thirty-two bits become eight hex symbols, far easier to read than 32 zeros and ones. When the length is not a multiple of four, pad with zeros on the left.

Octal: groups of three

Octal groups bits by three, with digits 0–7. It is rarely used today; the course mentions it for completeness.

Writing literals in C++

int a = 15;        // decimal
int b = 017;       // octal: leading 0, value 15
int c = 0x0F;      // hex: 0x prefix, value 15
int d = 0b1111;    // binary: 0b prefix, C++14 or later, value 15

The leading zero is a trap: 017 is not seventeen but fifteen. Printing all four variables yields 15 in every case — the same number in memory, written differently.

Integers: int

int is 4 bytes on most platforms and stores an integer in binary. There are three ways to declare and initialise:

int a = 10;     // assignment initialisation
int b {10};     // braced initialisation
int c (10);     // functional initialisation

Uninitialised variables

int elephants;   // uninitialised

That memory holds garbage; reading it is meaningless. Empty braces, int count {};, guarantee zero.

Narrowing: the decisive difference

int x {2.9};    // compile error: narrowing conversion
int y (2.9);    // compiles, silently truncates to 2
int z = 2.9;    // compiles, silently truncates to 2

In C++20, stuffing a floating-point value into an int with braces is an error. That is exactly the value of braced initialisation: it turns "quietly loses data" into "does not compile". Functional and assignment forms silently drop the fraction, 2.9 becomes 2, and the program runs on with a wrong value.

sizeof

std::cout << "sizeof(int): " << sizeof(int) << std::endl;   // 4
std::cout << "sizeof(x): "   << sizeof(x)   << std::endl;   // 4

sizeof accepts a type or a variable and reports bytes. On common platforms int is 4.

Naming rules

  • The first character must be a letter or underscore, never a digit.
  • The rest may be letters, digits or underscores.
  • Names are case-sensitive: bikeCount and BikeCount differ.
  • No spaces or symbols such as + or -.

Integer modifiers

Modifiers change signedness or width. They apply only to integer types, never to floating point.

signed and unsigned

int a = 1;              // signed by default; negatives allowed
signed int b = -300;    // explicit
unsigned int c = 4;     // non-negative only
unsigned int d = -5;    // compile error

Both occupy the same 4 bytes; the difference is interpretation. Signed treats the top bit as a sign bit, unsigned uses all 32 bits for magnitude. Hence:

unsigned:0∼2n−1,signed:−2n−1∼2n−1−1.\text{unsigned}: 0 \sim 2^n-1, \qquad \text{signed}: -2^{n-1} \sim 2^{n-1}-1 .

Four unsigned bytes cover roughly 0–4.2 billion; four signed bytes cover roughly −2.1 to 2.1 billion.

short, long, long long

FormTypical sizeNote
short2 bytessame as short int
int4 bytes
long4 or 8 bytesplatform dependent; usually 4 on Windows
long long8 bytesvery large range

The course measures on Windows + gcc: short 2, int 4, long 4, long long 8. On Linux long is often 8 — so never assume the size of long; when a fixed width matters, use int32_t or int64_t from <cstdint>.

short covers −32768 to 32767, which the formula confirms: 215=327682^{15}=32768.

Floating-point numbers

Floating-point types represent numbers with a fractional part. There are three:

TypeTypical sizeSignificant decimal digits
float4 bytesabout 7
double8 bytesabout 15–17
long double12 or 16 bytesat least as much as double

"Precision" means how many digits can be represented reliably, counted from the first significant digit. The number 123456789012 has 12 digits: too many for float, fine for double.

The suffix decides the literal's type

float a = 1.23f;        // f suffix
double b = 1.23;        // double by default
long double c = 1.23L;  // L suffix

A missing f is a common bug: in float x = 1.23456789; the literal is parsed as double and then converted to float, losing precision in the conversion. Worse is changing the variable to double but keeping the f — the variable is wide, the stored value is still float precision.

Scientific notation

double big = 1.9e8;       // 1.9 × 10^8
double small = 3.498e-11; // 3.498 × 10^-11

The exponent after e is a power of ten: positive moves the point right, negative moves it left.

Infinity and NaN

Floating-point division by zero does not crash; it produces special values:

double a = 5.6;
double zero = 0.0;
double inf = a / zero;      // +inf
double nan = zero / zero;   // nan (not a number)
  • A positive number over zero gives inf, a negative one gives -inf.
  • inf + 5 stays inf: a finite quantity added to infinity is still infinite.
  • 0.0 / 0.0 gives nan, meaning "this operation has no meaningful numeric result". Most arithmetic on nan yields nan.

Integer division by zero is undefined behaviour; floating-point division by zero has a defined result. That is a real semantic difference between the two families.

Watching precision with setprecision

#include <iomanip>

std::cout << std::setprecision(20);
std::cout << 1.234567890123456789 << std::endl;

The default precision is 6. setprecision only requests how many digits to display; it cannot exceed the type's own precision. A float starts producing noise around the eighth digit, a double around the seventeenth.

Chapter summary

  • A variable is named memory; its type fixes the size and the interpretation.
  • Memory holds bits; binary expands by powers of two, octal groups three bits, hex groups four.
  • Literals: 017 is octal, 0x0F hex, 0b1111 binary.
  • Of the three initialisation forms, braces are safest: narrowing is a compile error, the others truncate silently.
  • int is usually 4 bytes, short 2, long long 8; long depends on the platform.
  • signed/unsigned change signedness; short/long change width; modifiers apply only to integers.
  • Floating point comes as float/double/long double with roughly 7, 15 and more digits; never omit the f or L suffix.
  • Floating-point division by zero gives inf, 0.0/0.0 gives nan, and the program does not crash.

Comprehension check

1. What is the value of int a = 017; and why does it print the same as int b = 15;?

Reference answer: 017 is an octal literal, so its value is 1×8+7=151\times8+7=15. In memory it is the same 32-bit pattern as 15; only the source notation differs. std::cout prints in decimal by default, so both output 15.

2. How do int x {2.9}; and int y (2.9); differ, and why is the first safer?

Reference answer: Braced initialisation treats double → int as narrowing and, in C++20, reports a "narrowing conversion" compile error, so no binary is produced. Functional initialisation silently drops the fraction, leaving y == 2. The first turns data loss into a build-time error; the second hides it until run time. Assignment initialisation behaves like the second.

3. unsigned int and int are both 4 bytes — why do their ranges differ?

Reference answer: 32 bits give 2322^{32} states either way; the difference is the top bit. Signed uses it as a sign bit and the remaining 31 for magnitude, giving −231-2^{31} to 231−12^{31}-1 (about −2.1 to 2.1 billion). Unsigned uses all 32 bits for magnitude, giving 0 to 232−12^{32}-1 (about 0 to 4.2 billion). Same size, different interpretation.

4. sizeof(long) is 4 on Windows but often 8 on Linux. What does that mean for portable code?

Reference answer: The width of long follows the platform's data model (LLP64 on Windows, LP64 on Linux), so the same source can overflow differently on each. Portable code must not assume long's size: use int32_t/int64_t from <cstdint> when an exact width matters, or long long, which the standard guarantees is at least 64 bits. This matters especially for serialisation and binary file formats.

5. What happens with float x = 1.234567890123; when the f is missing? And when the variable becomes double but the f stays?

Reference answer: Without f, the literal is a double (about 15 significant digits) and the assignment to float keeps only about 7, truncating the rest. With double x = 1.234567890123f;, the literal is stored as a float first and then widened, so the variable holds float-precision data and the trailing digits are noise. In both cases the suffix and the variable type must agree.

6. What is the essential difference between integer 7 / 0 and floating-point 7.0 / 0.0?

Reference answer: Integer division by zero is undefined behaviour in C++; the language promises nothing and the program may crash or produce an arbitrary value. Floating point follows IEEE 754: 7.0 / 0.0 is +inf, -7.0 / 0.0 is -inf, and 0.0 / 0.0 is nan. The program does not crash, but the result can contaminate later arithmetic. The first is a language-level error, the second a numeric special value.

How it connects

This chapter establishes that a type fixes both the byte count and the range. Chapter 4 completes the fundamental types with booleans, characters (ASCII) and auto deduction, and shows the silent overflow that auto can produce when combined with unsigned types. Chapter 5 starts operating on this data: arithmetic, precedence, increment and decrement, compound assignment and logical operators.

来源