值、类型与对象:C++ 程序中的第一层现实 / Values, Types, and Objects
1. 先看一行代码背后的问题
int count{3};初学者通常会把它读成:
定义一个叫
count的整数,值是 3。
这个说法可以作为入口,但还不够精确。
编译器和运行时还要处理:
count 是名字
int 是类型
对象需要一段存储
3 是初始化值
对象有生命周期
程序通过名字访问对象本篇要建立的核心关系是:
名字
|
v
对象
|
+-- 类型:怎样解释它的存储,允许哪些操作
+-- 值:当前保存的状态
+-- 存储:对象占用的运行时空间
+-- 生命周期:何时开始存在,何时结束2. 本文基础词小注释
值:程序在某一时刻可以观察到的结果或状态。例如整数对象当前的值可以是 3,布尔对象当前的值可以是 true。
类型:对一类值、操作和表示规则的约束。类型不仅告诉我们“它是什么”,还告诉编译器哪些操作合法、如何解释存储和如何进行转换。
对象:一段在运行时承载某种类型状态的实体。C++ 中对象不等于变量名;变量名是访问对象的一个名字。
存储:承载对象的运行时空间。存储可以来自自动存储、动态分配、静态区域或其他实现机制。
初始化:对象开始生命周期时获得初始状态的过程。
赋值:一个已经存在的对象接收新值的过程。它不是对象第一次出现。
生命周期:对象从开始存在到结束存在的时间范围。对象还有地址,不代表它仍然可以被合法访问。
3. 名字、对象和存储
int count{3};可以画成:
源码名字 count
|
v
+------------------+
| int object |
| current value: 3 |
+------------------+
|
v
运行时存储count 是名字。
对象是运行时真正存在的 int 实体。
存储是承载这个对象的空间。
在常见实现中,局部对象可能位于线程调用栈中,但 ISO C++ 主要规定对象语义和存储期,不要求所有实现都用同一张物理布局图。
这一区分很重要:
名字失去作用域
!=
存储必然立刻归还给操作系统
对象生命周期结束
==
不能再按这个对象类型合法访问它4. int 不只是“整数类型”
int 用来表示离散的整数值,例如计数、索引和状态码。
int count{3};
int offset{-2};常见实现用固定宽度的二进制表示它。
因此它有表示范围:
位数有限
|
v
可表示的整数数量有限
|
v
超出范围时要考虑转换或溢出规则sizeof(int) 可以观察当前实现的大小:
#include <iostream>
int main() {
std::cout << sizeof(int) << '\n';
}但不要把某台机器的 sizeof(int) 当成所有平台的永恒答案。
语言标准规定最小能力和相对关系,具体宽度属于实现。
还要区分:
有符号 int
+-- 可以表示负数、零和正数
+-- 有符号溢出是危险边界
unsigned int
+-- 只表示非负范围
+-- 运算和比较可能触发隐式转换例如:
int negative{-1};
unsigned int size{1};
if (negative < size) {
// 结果可能与直觉不同
}问题不是“负数突然变成正数”这么简单,而是二者参与比较时会经过通常算术转换。
5. float 不只是“带小数的类型”
float 用有限位数近似表示实数。
float price{0.1f};常见实现把一个浮点值拆成大致几部分:
+--------+----------+----------------+
| sign | exponent | significand |
+--------+----------+----------------+这意味着它不是把十进制字符 0.1 原封不动放进内存。
很多十进制小数在二进制浮点中只能近似表示:
#include <iostream>
#include <iomanip>
int main() {
float value{0.1f};
std::cout << std::setprecision(20) << value << '\n';
}因此浮点数需要讨论:
- 有限精度。
- 舍入误差。
- 数值范围。
NaN和无穷。- 近似比较,而不是随意使用
==。
float 是否适合某个问题,不只取决于“能不能存小数”,还取决于误差预算。
6. 初始化不是赋值
int a{7}; // 定义并初始化
int b{}; // 值初始化为 0
a = b; // a 已经存在,现在赋值时间线:
a 的存储准备好
|
v
a 的生命周期开始
|
v
a 获得初始值 7
|
v
后续赋值改变 a 的当前值初始化决定对象从什么状态开始。
赋值改变已经存在对象的状态。
这一区分会影响:
const对象。- 引用成员。
- 类的构造函数。
- RAII。
- 异常安全。
例如:
const int limit{10};
// limit = 20; // 不能赋值因为 limit 从一开始就是一个不可修改的对象。
7. 初始化形式和窄化转换
int a = 3.9; // 可能丢失小数
int b(3.9); // 也可能发生转换
int c{3.9}; // 列表初始化通常拒绝窄化列表初始化的价值不是“写法更漂亮”。
它让某些可能丢失信息的转换更早暴露:
更宽的数值
|
v
更窄的目标类型
|
+-- 值可能改变
+-- 列表初始化倾向于在编译期拒绝这是 C++ 把一部分错误提前到编译期的例子。
8. const 的含义
const int limit{10};这里的 const 表示通过这个对象名不能修改对象的值。
它还会影响接口:
void inspect(const int& value);这个函数表达了一个意图:
调用者提供一个已经存在的 int
函数借用它
函数不应该通过这个参数修改它但 const 不等于线程安全,也不等于对象永远不会变化。
如果对象通过其他合法路径被修改,或者内部状态使用 mutable,具体语义仍要看完整类型和程序设计。
9. 类型如何影响机器执行
表达式:
int sum = left + right;编译器要根据类型决定:
读取多少字节
|
v
把比特解释成什么数值
|
v
选择什么算术指令或转换
|
v
把结果放在哪里类型不是运行时贴在对象上的文字标签。
它既是语言层面的约束,也是编译器生成代码时的重要信息。
不过不要把“类型影响代码生成”理解成“所有类型都能直接看出固定机器布局”。
优化、ABI 和目标架构都会影响最终实现。
10. 未初始化和不确定状态
int value;
std::cout << value << '\n';不要把它理解成“系统自动给了一个随机数”。
更准确地说,程序没有先建立一个可用的 int 初始状态,就尝试读取它。
应该写:
int value{};或者:
int value = 0;初始化不是装饰。
它是建立对象不变量的第一步。
11. 整数、浮点和字符的对比
| 类型 | 主要用途 | 关键限制 |
|---|---|---|
int | 离散整数、计数、索引 | 范围有限,转换和溢出要小心 |
unsigned | 非负范围、位操作等 | 混合比较可能产生意外转换 |
float | 有限精度数值计算 | 舍入误差、范围和特殊值 |
double | 更高精度的浮点计算 | 仍不是任意实数的精确表示 |
char | 字节或字符单元 | 是否有符号、编码语义需区分 |
bool | 真值状态 | 参与算术时会发生转换 |
选类型不是只问“能不能存这个值”。
还要问:
这个值的数学意义是什么?
精度要求是什么?
是否需要负数?
是否会和其他类型混合?
接口是否要表达状态?12. 编译期错误和运行时错误
int value{"42"};通常在编译阶段就会被拒绝,因为初始化类型不匹配。
而下面的问题可能要到运行时才暴露:
int values[2]{1, 2};
std::cout << values[5] << '\n';这说明不同阶段负责不同工作:
编译期
+-- 语法、类型、名字和部分规则
运行期
+-- 实际输入、索引、状态和执行路径编译器能检查的越多,程序越容易维护。
但 C++ 允许很多低层操作,所以仍然需要测试、调试器和 Sanitizer。
13. 最小实验
实验 1:比较 int、long、float、double 的 sizeof。
实验 2:观察有符号和无符号混合比较。
实验 3:比较 0.1f 的输出精度。
实验 4:比较 = 初始化、() 初始化和 {} 初始化。
实验 5:开启 -Wall -Wextra,观察未初始化警告。
实验 6:制造数组越界,使用 AddressSanitizer 观察。建议编译:
c++ -std=c++17 -Wall -Wextra -Wpedantic -g demo.cpp -o demo
c++ -std=c++17 -fsanitize=address,undefined -g demo.cpp -o demo-sanitized14. 常见误解
“变量就是一块内存”
不准确。变量是名字;对象是运行时实体;存储是承载对象的空间。
“初始化就是第一次赋值”
不准确。初始化建立对象,赋值改变已经存在的对象。
“float 就是精确的小数”
不对。它通常是有限精度的二进制近似。
“const 就是线程安全”
不对。const 主要表达访问和修改约束,不自动建立线程同步。
“编译通过就没有未定义行为”
不对。许多地址、生命周期、越界和并发问题只能在运行时路径中暴露。
15. 用类型描述程序意图
比较:
void set_count(int value);
void set_count(unsigned value);它们不只是能接受的数字范围不同,也表达了不同意图:
int
+-- 可能有负值
+-- 适合差值和部分状态码
unsigned
+-- 只表达非负范围
+-- 混合比较时要小心转换类型选择是一种接口设计。
如果参数表示文件大小、数组长度或资源数量,还要考虑明确宽度整数、std::size_t 或领域类型,而不是机械地把所有数字都写成 int。
16. 位、字节和对象表示
计算机最终保存的是比特:
bit
|
v
byte
|
v
object representation
|
v
value interpreted through a type同一组比特按不同类型解释,意义可能不同。
但“内存里有这些比特”和“程序可以按这种类型合法访问这些比特”是两个问题。
这也是指针、结构体、序列化和内存映射必须分开学习的原因。
17. 数值边界实验
#include <limits>
#include <iostream>
int main() {
std::cout << std::numeric_limits<int>::min() << '\n';
std::cout << std::numeric_limits<int>::max() << '\n';
std::cout << std::numeric_limits<float>::epsilon() << '\n';
}epsilon 不是所有浮点误差都相等。
它描述的是某个实现下与 1 附近相关的精度尺度。
真实数值算法还要考虑数量级、运算次数、舍入方向和误差传播。
18. 语言规则、实现和平台
问题:int 到底几字节?
|
+-- ISO C++:规定最小范围和类型关系
+-- 实现:选择具体宽度和表示
+-- ABI:规定跨目标文件如何传递
+-- 平台:决定编译器、运行库和目标架构遇到“在我的电脑上就是这样”,不要马上把观察提升为语言规则。
更好的回答方式是:
标准保证什么?
常见实现是什么?
当前平台观察到什么?
换平台后哪些内容需要重新验证?19. 总结与下一篇
本篇最重要的模型:
名字
|
v
对象
|
+-- 有类型
+-- 占用存储
+-- 有生命周期
+-- 保存值
+-- 受到访问规则约束下一篇进入表达式和控制流。
我们会继续追问:
表达式怎样产生值?
隐式转换在哪里发生?
为什么 && 可以短路?
if 和 for 最后如何改变 CPU 的控制流?