2C++ Primer(第5版)

变量和基本类型

数据的表示

基本类型变量const引用

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 整型

cpp
// 基本整型
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> 头文件可以查看具体实现的大小:

cpp
#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 浮点型

cpp
float       // 单精度浮点,通常32位,约7位有效数字
double      // 双精度浮点,通常64位,约15位有效数字
long double // 扩展精度浮点,通常80位或128位

浮点数的精度是有限的,比较浮点数时不能直接使用 ==:

cpp
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++的重要特性,它是变量的别名,必须初始化且不能重新绑定:

cpp
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用于声明不可修改的变量:

cpp
const int MAX = 100;
// MAX = 200;  // 错误:不能修改const变量

const double PI = 3.14159;

2.3.1 const与引用

cpp
int i = 42;
const int &cref = i;  // 常量引用
// cref = 100;  // 错误:不能通过常量引用修改

// 常量引用可以绑定到非const对象
int &ref = i;
ref = 100;  // OK:通过非常量引用修改
std::cout << i << std::endl;  // 100

2.3.2 const与指针

cpp
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引入的,用于声明常量表达式(编译时就能确定值的常量):

cpp
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让编译器根据初始化表达式推导变量类型:

cpp
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从表达式推导类型,但不求值:

cpp
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++会在某些情况下自动进行类型转换:

cpp
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种显式类型转换运算符:

cpp
// 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 类型大小和范围测试

cpp
#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 引用和指针的对比

cpp
#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引用的实际应用

cpp
#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简化一段包含复杂类型的代码。


2/12变量和基
0%