第2章:变量和基本类型(Variables and Basic Types)
一、导读
1.1 本章学习目标
第2章深入讲解C++的类型系统,这是编写正确、高效C++代码的基础。C++的类型系统比C更加严格和安全,理解这些规则能帮助你编写更健壮的代码。本章的核心学习目标包括:
- 掌握C++的算术类型及其大小:整型、浮点型、字符型在不同平台上的表现
- 理解引用的概念和使用:引用是C++的重要特性,与指针有何不同?
- 掌握const限定符的正确使用:顶层const和底层const的区别
- 学会使用auto和decltype:C++11的类型推导机制
- 理解类型转换的规则:隐式转换和显式转换的安全性
- 掌握typedef和using的用法:如何定义类型别名
1.2 为什么本章如此重要
类型系统是C++的核心。与Python等动态类型语言不同,C++是强类型语言,每个变量都有明确的类型,类型错误在编译时就会被发现。理解类型系统不仅能帮助你避免bug,还能让你写出更高效的代码——因为类型直接决定了数据的内存布局和操作的执行方式。
1.3 核心问题引导
为什么C++的整型大小在不同平台上可能不同?这会导致什么问题?
引用和指针有什么本质区别?为什么引用必须初始化?
const int *p 和 int *const p 有什么区别?如何快速区分?
auto推导类型时遵循什么规则?什么时候auto会推导错误?
为什么C++推荐用const引用传递参数,而不是值传递?
隐式类型转换在什么情况下会导致bug?
二、核心概念详解
2.1 算术类型
C++的算术类型分为整型和浮点型两大类:
2.1.1 整型
// 基本整型
bool // 布尔类型,true或false,通常占1字节
char // 字符类型,至少8位(1字节)
short // 短整型,至少16位
int // 整型,至少16位(通常32位)
long // 长整型,至少32位
long long // 长长整型,至少64位
// 有符号和无符号
signed char // -128 到 127
unsigned char // 0 到 255
unsigned int // 0 到 4294967295(32位时)重要:C++标准只规定了各类型的最小大小,实际大小取决于编译器和平台。使用 <climits> 头文件可以查看具体实现的大小:
#include <climits>
#include <iostream>
int main() {
std::cout << "int: " << sizeof(int) << " bytes" << std::endl;
std::cout << "long: " << sizeof(long) << " bytes" << std::endl;
std::cout << "INT_MAX: " << INT_MAX << std::endl;
std::cout << "INT_MIN: " << INT_MIN << std::endl;
return 0;
}2.1.2 浮点型
float // 单精度浮点,通常32位,约7位有效数字
double // 双精度浮点,通常64位,约15位有效数字
long double // 扩展精度浮点,通常80位或128位浮点数的精度是有限的,比较浮点数时不能直接使用 ==:
double a = 0.1 + 0.2;
double b = 0.3;
// a == b 可能为false!
// 正确做法:
#include <cmath>
bool equal = std::abs(a - b) < 1e-9;2.2 引用
引用(reference)是C++的重要特性,它是变量的别名,必须初始化且不能重新绑定:
int i = 42;
int &ref = i; // ref是i的引用(别名)
ref = 100; // 修改ref就是修改i
std::cout << i << std::endl; // 输出100
int j = 200;
ref = j; // 这不是重新绑定!而是将j的值赋给i
std::cout << i << std::endl; // 输出200引用 vs 指针:
| 特性 | 引用 | 指针 |
|---|---|---|
| 必须初始化 | 是 | 否 |
| 可重新绑定 | 否 | 是 |
| 可以为空 | 否 | 是 |
| 语法 | 直接使用 | 需要解引用 |
2.3 const限定符
const用于声明不可修改的变量:
const int MAX = 100;
// MAX = 200; // 错误:不能修改const变量
const double PI = 3.14159;2.3.1 const与引用
int i = 42;
const int &cref = i; // 常量引用
// cref = 100; // 错误:不能通过常量引用修改
// 常量引用可以绑定到非const对象
int &ref = i;
ref = 100; // OK:通过非常量引用修改
std::cout << i << std::endl; // 1002.3.2 const与指针
int i = 42;
int j = 100;
// 指向常量的指针(底层const)
const int *p1 = &i;
// *p1 = 100; // 错误:不能修改指向的值
p1 = &j; // OK:可以指向另一个变量
// 常量指针(顶层const)
int *const p2 = &i;
*p2 = 100; // OK:可以修改指向的值
// p2 = &j; // 错误:不能指向另一个变量
// 常量指针指向常量(两者都是const)
const int *const p3 = &i;
// *p3 = 100; // 错误
// p3 = &j; // 错误记忆技巧:从右往左读——const int *p 是"指向const int的指针",int *const p 是"const指针指向int"。
2.3.3 constexpr
constexpr是C++11引入的,用于声明常量表达式(编译时就能确定值的常量):
constexpr int MAX = 100;
constexpr double PI = 3.14159;
// 可以用于数组大小
int arr[MAX]; // OK
// constexpr函数
constexpr int square(int x) {
return x * x;
}
constexpr int result = square(5); // 编译时计算2.4 auto和decltype
2.4.1 auto类型推导
auto让编译器根据初始化表达式推导变量类型:
auto x = 42; // int
auto y = 3.14; // double
auto z = "hello"; // const char*
auto b = true; // bool
// auto与引用
int i = 42;
auto &ref = i; // int&
auto val = i; // int(忽略引用)
// auto与const
const int ci = 42;
auto v1 = ci; // int(忽略顶层const)
auto &v2 = ci; // const int&(保留底层const)2.4.2 decltype
decltype从表达式推导类型,但不求值:
int i = 42;
decltype(i) x; // x的类型是int
decltype(i + 0) y; // y的类型是int
// decltype保留引用
int &ref = i;
decltype(ref) z = i; // z的类型是int&
// decltype与括号
decltype((i)) w = i; // int&(加括号总是得到引用)
decltype(i) v; // int(不加括号不是引用)2.5 类型转换
2.5.1 隐式类型转换
C++会在某些情况下自动进行类型转换:
int i = 42;
double d = i; // int -> double(安全)
double pi = 3.14;
int n = pi; // double -> int(截断,可能丢失精度)
char c = 65; // int -> char
int x = c; // char -> int
// 危险的隐式转换
unsigned int u = 10;
int s = -1;
if (s < u) {
// 这个分支不会执行!
// 因为s被转换为unsigned int,变成很大的正数
}2.5.2 显式类型转换(C++风格)
C++提供了4种显式类型转换运算符:
// static_cast:安全的类型转换
double d = 3.14;
int i = static_cast<int>(d); // double -> int
// const_cast:去除const属性
const int ci = 42;
int &ref = const_cast<int&>(ci); // 危险!
// reinterpret_cast:底层位模式转换
int *p = nullptr;
long addr = reinterpret_cast<long>(p); // 指针 -> 整数
// dynamic_cast:安全的向下转型(运行时检查)
// 用于多态类型最佳实践:优先使用C++风格的类型转换,避免C风格的 (int)x。C++风格的转换更容易被搜索到,也更明确地表达了意图。
三、实战案例
3.1 类型大小和范围测试
#include <iostream>
#include <climits>
#include <cfloat>
int main() {
std::cout << "=== Integer Types ===" << std::endl;
std::cout << "sizeof(bool): " << sizeof(bool) << std::endl;
std::cout << "sizeof(char): " << sizeof(char) << std::endl;
std::cout << "sizeof(short): " << sizeof(short) << std::endl;
std::cout << "sizeof(int): " << sizeof(int) << std::endl;
std::cout << "sizeof(long): " << sizeof(long) << std::endl;
std::cout << "sizeof(long long): " << sizeof(long long) << std::endl;
std::cout << "\n=== Float Types ===" << std::endl;
std::cout << "sizeof(float): " << sizeof(float) << std::endl;
std::cout << "sizeof(double): " << sizeof(double) << std::endl;
std::cout << "sizeof(long double): " << sizeof(long double) << std::endl;
std::cout << "\n=== Float Precision ===" << std::endl;
std::cout << "float digits: " << FLT_DIG << std::endl;
std::cout << "double digits: " << DBL_DIG << std::endl;
return 0;
}3.2 引用和指针的对比
#include <iostream>
void swapByValue(int a, int b) {
int temp = a;
a = b;
b = temp;
// 不会交换原始变量
}
void swapByRef(int &a, int &b) {
int temp = a;
a = b;
b = temp;
// 交换原始变量
}
void swapByPtr(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 10, y = 20;
swapByValue(x, y);
std::cout << "After swapByValue: x=" << x << ", y=" << y << std::endl;
// x=10, y=20(没有变化)
swapByRef(x, y);
std::cout << "After swapByRef: x=" << x << ", y=" << y << std::endl;
// x=20, y=10(交换成功)
swapByPtr(&x, &y);
std::cout << "After swapByPtr: x=" << x << ", y=" << y << std::endl;
// x=10, y=20(再次交换回来)
return 0;
}3.3 const引用的实际应用
#include <iostream>
#include <string>
#include <vector>
// 使用const引用传递大对象(避免拷贝)
void printVector(const std::vector<int> &v) {
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
}
// 使用const引用传递字符串
std::string greet(const std::string &name) {
return "Hello, " + name + "!";
}
// auto和decltype的使用
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
printVector(nums);
std::string msg = greet("Alice");
std::cout << msg << std::endl;
// auto推导
auto it = nums.begin(); // std::vector<int>::iterator
auto size = nums.size(); // std::vector<int>::size_type
// 范围for中的引用
for (auto &x : nums) {
x *= 2; // 修改原始元素
}
printVector(nums);
return 0;
}四、本章小结
第2章深入讲解了C++的类型系统:
算术类型:C++提供了丰富的算术类型,大小取决于平台。使用 <climits> 查看具体实现。
引用:变量的别名,必须初始化且不能重新绑定。与指针相比更安全、更方便。
const:声明不可修改的变量。理解顶层const和底层const的区别对于正确使用指针和引用至关重要。
auto和decltype:C++11的类型推导机制,简化了复杂类型的声明。
类型转换:隐式转换可能导致意外行为,推荐使用显式转换(static_cast等)。
五、思考与练习
思考题
为什么 sizeof(char) 总是1?这是否意味着char在所有平台上都是8位?
引用在底层是如何实现的?它和指针在汇编层面有区别吗?
为什么C++推荐用const引用传递参数?值传递在什么情况下更合适?
auto推导时如何处理顶层const和引用?
隐式类型转换在什么场景下会导致难以发现的bug?
编程练习
编写程序测试并打印所有算术类型在当前平台上的大小和范围。
编写一个函数,使用const引用参数实现字符串的反转。
使用auto和decltype简化一段包含复杂类型的代码。