2、变量与基本数据类型:从内存到代码的第一次握手
各位同学,今天我们来聊聊C++里最基础、也最核心的东西——变量和数据类型。说实话,这部分内容看起来简单,但我在面试过不少自称“精通C++”的候选人时,发现很多人其实栽在了这里。嗯,咱们从头捋一遍。
2.1 变量定义与初始化:给数据一个“家”
变量是什么?说白了,就是内存里一块有名字的空间。你给它起个名字,它帮你存数据。
int age; // 定义了一个整型变量,叫age
age = 25; // 赋值,把25放进去
int score = 90; // 定义+初始化,一步到位
我个人习惯是:定义变量时尽量初始化。为什么?因为未初始化的变量里存的是“垃圾值”——就是那块内存之前留下的随机数据。我曾经在项目里排查过一个bug,查了整整两天,最后发现是一个局部变量没初始化,在某些条件下读到了脏数据。从那以后,我写代码必初始化。
核心原则:定义变量时立即初始化,不要留到后面再赋值。
C++11之后,还多了一种初始化方式——花括号初始化(也叫列表初始化):
int x{10}; // 安全,不会发生窄化转换
int y = {20}; // 也可以
int z{}; // 初始化为0
花括号初始化的好处是:类型不匹配时会报错,而不是悄悄截断。比如 int a{3.14} 会编译失败,而 int a = 3.14 只会把小数部分丢掉。你想想看,哪个更安全?
2.2 整型家族:int、short、long、long long
整型就是存整数的。但C++给了你好几个选择,每个占的内存大小不一样:
| 类型 | 常见大小 | 取值范围(以32位系统为例) |
|---|---|---|
| short | 2字节 | -32768 ~ 32767 |
| int | 4字节 | -21亿 ~ 21亿 |
| long | 4或8字节(看平台) | 至少和int一样大 |
| long long | 8字节 | 非常大,9e18级别 |
我的建议:日常用 int 就够了。如果确定数值不会超过32767,可以用 short 省内存(比如嵌入式开发)。如果数值可能很大,直接用 long long,别纠结。
我记得有一次做金融交易系统,需要存储毫秒级时间戳,数值轻松超过21亿。有人用了 int,结果上线后数据溢出,交易记录全乱了。嗯,这种坑踩一次就够了。
2.3 浮点型:float 与 double
浮点型用来存小数。两个选择:
- float:4字节,精度约7位有效数字
- double:8字节,精度约15位有效数字
float pi = 3.14159f; // 注意后面的f,表示float字面量
double e = 2.718281828; // 默认就是double
避坑指南:千万不要用 float 做精确计算,比如金额、坐标。我曾经在图形学项目里用 float 累加坐标,跑了10万帧之后,位置漂移了半个屏幕。换成 double 就好了。
另外,浮点数比较时别用 ==,因为精度问题。应该用差值小于某个极小值来判断:
if (fabs(a - b) < 1e-6) {
// 认为a和b相等
}
2.4 字符型:char
char 存单个字符,占1字节。本质上它是个小整数:
char ch = 'A'; // 实际存的是65(ASCII码)
char digit = 48; // 存的是字符'0'
char newline = '\n'; // 转义字符,换行
这里有个小细节:char 是有符号还是无符号,取决于编译器。如果你需要存0~255的值,最好明确写成 unsigned char。我在做网络协议解析时,经常用 unsigned char 来操作二进制数据,避免符号扩展带来的麻烦。
2.5 布尔型:bool
bool 只有两个值:true 和 false。占1字节。
bool isReady = true;
bool isDone = false;
有意思的是,C++里 bool 可以跟整数互相转换:0是false,非0是true。但我不建议你这么用——代码可读性会变差。你想想看,if (flag) 比 if (flag != 0) 清爽多了。
2.6 sizeof 运算符:问编译器“这个类型占多大?”
sizeof 是个运算符,不是函数。它返回类型或变量占用的字节数:
cout << sizeof(int) << endl; // 4
cout << sizeof(double) << endl; // 8
int arr[10];
cout << sizeof(arr) << endl; // 40(10个int,每个4字节)
实用技巧:用 sizeof(arr) / sizeof(arr[0]) 可以算出数组元素个数。但注意,这只对真正的数组有效,对指针无效。我见过有人把数组传给函数后,在函数里用这个公式算长度,结果永远得到1——因为参数退化成指针了。
2.7 类型转换:隐式与显式
类型转换分两种:编译器偷偷帮你做的(隐式),和你明确告诉编译器要做的(显式)。
隐式转换
发生在你“不小心”混用了不同类型时:
int a = 3.14; // double转int,小数被截断,a=3
double b = 10; // int转double,b=10.0
bool c = 42; // int转bool,c=true
隐式转换虽然方便,但容易出问题。比如 int a = 3.14 不会报错,但你可能没意识到小数丢了。这就是为什么我前面推荐花括号初始化——它会阻止这种不安全的转换。
显式转换(强制类型转换)
当你明确知道要转换时,用C++风格的强制转换:
double pi = 3.14159;
int approx = static_cast<int>(pi); // 3,丢掉小数
// 其他场景
const int* p = &someInt;
int* q = const_cast<int*>(p); // 去掉const属性(慎用!)
// 不相关类型之间的转换
void* ptr = &someDouble;
double* dptr = reinterpret_cast<double*>(ptr);
我的忠告:尽量少用 reinterpret_cast 和 const_cast。它们绕过了类型系统的保护,容易写出难以调试的bug。我见过一个同事用 reinterpret_cast 把int指针转成float指针来读数据,结果因为字节序问题,读出来的值完全不对——嗯,调试了整整一个下午。
2.8 知识体系总览
下面这张图把本章的核心内容串起来了:
2.9 本章小结
好了,这一章的内容就这些。总结几个关键点:
- 变量定义时一定要初始化,花括号初始化更安全
- 整型选 int 或 long long,别在大小上纠结
- 浮点用 double,除非你明确知道 float 够用
- char 本质是整数,处理二进制数据用 unsigned char
- sizeof 是运算符,不是函数,注意数组和指针的区别
- 类型转换用 static_cast,少用 reinterpret_cast 和 const_cast
这些基础东西,写多了自然就熟了。刚开始可能会觉得啰嗦,但相信我——打好基础,后面学指针、引用、类的时候会轻松很多。