C++ 教学课件 - 第三次课
C++ 第三节课:深入理解内存与地址——指针与引用
大家好,欢迎来到我们的第三节 C++ 课程!在前两节课中,我们学习了 C++ 的基本数据类型、运算符等基础知识。今天,我们将深入探索 C++ 中一个非常重要且强大的概念:指针和引用。理解了它们,你才能真正掌握 C++ 的精髓,并为后续学习动态内存管理、数据结构等高级主题打下坚实的基础。
一、指针 (Pointer)
指针是 C++ 中最强大的特性之一,但也常常让新手感到困惑。 别担心,我们会一步步地理解它。
1. 指针的概念:内存地址的艺术
对于高手: 指针是驾驭内存的灵活缰绳,能够实现直接的内存操作,构建复杂的数据结构,并优化程序性能。
对于新手: 指针就像一把双刃剑,强大但稍有不慎就会割伤自己。理解指针需要时间和实践,但它是成为 C++ 高手的必经之路。错误使用指针可能导致程序崩溃或产生不可预测的行为,所以务必谨慎。
C# 的权衡: C# 作为 C++ 的“近亲”,为了提高开发效率和安全性,牺牲了部分底层控制权,限制了指针的使用。这降低了学习门槛,但也限制了某些高级操作的可能性。理解 C++ 的指针,能帮助你更好地理解其他语言的内存管理机制。
🤔 思考: 想象你的电脑内存是一栋栋排列整齐的房子,每个房子都有一个唯一的地址。变量就像住在这房子里的人,而指针就像记录着某个房子地址的纸条。
2. 指针的基本操作:寻址和取值
&
(取址运算符 - Address-of Operator): 就像获取一个人的家庭住址。它返回变量在内存中的起始位置。- 例如:c++
int a = 10; int *pa = &a; // pa 存储了变量 a 的内存地址
&a
运算的结果就是变量a
在内存中的地址。
- 例如:
内存地址的表示: 内存地址通常以十六进制形式显示,例如
0x7ffee3a1b9dc
。这只是一个编号,不必深究其具体数值。*
(声明指针变量 - Declaration): 在声明变量时,*
表明你声明的是一个指针变量,它用来存储内存地址。- 例如:
int *pa;
声明了一个名为pa
的指针变量,它可以存储一个int
类型变量的内存地址。 注意: 这里的*
仅仅是类型声明的一部分,表示pa
是一个指向int
的指针。
- 例如:
*
(解引用运算符 - Dereference Operator): 就像拿着地址纸条找到对应的房子,然后看看里面住着谁。它访问指针所指向的内存地址中的值。- 例如:c++
int a = 10; int *pa = &a; std::cout << *pa << std::endl; // 输出 10,即 pa 指向的内存地址中存储的值
*pa
运算的结果就是指针pa
所指向的内存地址中存储的值,也就是变量a
的值。
- 例如:
指针的大小: 指针变量本身也需要内存来存储地址。这个大小取决于你的计算机系统架构。
- 32 位系统: 指针的大小通常是 4 个字节(足以表示 232 个不同的地址)。
- 64 位系统: 指针的大小通常是 8 个字节(足以表示 264 个不同的地址)。
- 你可以使用
sizeof(指针变量)
来查看指针的大小。
✍️ 练习: 写一段代码,声明一个 float
类型的变量 f
并赋值,然后声明一个指向 float
的指针 pf
,将 f
的地址赋给 pf
,并打印出 pf
的值(内存地址)和 *pf
的值(f
的值)。
3. 指针的初始化和空指针:避免野指针
- 未初始化的风险: 就像拿到一个没有写任何地址的纸条,你不知道它指向哪里。未初始化的指针可能包含随机的内存地址,对其进行解引用操作会导致程序崩溃或未定义的行为,这种指针被称为野指针 (Wild Pointer)。
nullptr
的意义:nullptr
是 C++11 引入的空指针常量,表示指针不指向任何有效的内存地址。将指针初始化为nullptr
是一种良好的编程习惯。- 例如:
int *pp = nullptr;
明确地表示指针pp
当前没有指向任何有效的int
型变量。
- 例如:
- 检查空指针: 在使用指针之前,尤其是来自函数返回值或动态分配的内存时,最好检查指针是否为
nullptr
,以避免访问无效内存。c++int *ptr = get_some_pointer(); // 假设 get_some_pointer 可能返回 nullptr if (ptr != nullptr) { // 指针有效,可以安全使用 std::cout << *ptr << std::endl; } else { // 指针为空,处理错误情况 std::cout << "指针为空!" << std::endl; }
4. 多级指针:指向指针的指针
- 概念: 指针本身也是变量,它也有自己的内存地址。因此,我们可以用另一个指针来存储一个指针变量的地址,这就是二级指针。同理,可以有三级、四级甚至更多级的指针。
- 二级指针: 指向指针的指针。
- 例如:c++
int a = 10; int *pp = &a; // pp 指向 a 的地址 int **pp2 = &pp; // pp2 指向 pp 的地址
pp2
存储的是指针pp
的内存地址。
- 例如:
- 三级指针: 指向二级指针的指针。
- 例如:
int ***pp3 = &pp2;
- 例如:
- 应用场景: 多级指针主要用于更复杂的数据结构(如图、树等)和一些高级编程技巧中,对于初学者来说,理解二级指针就足够了。
5. void
指针:通用的指针
- 可以指向任何类型:
void *
是一种特殊的指针类型,它可以指向任何类型的变量的内存地址。- 例如:c++
int n = 10; float f = 3.14; void *vp; vp = &n; // void 指针可以指向 int vp = &f; // void 指针也可以指向 float
- 例如:
- 不能直接解引用: 由于
void
指针不知道它指向的数据类型,因此不能直接使用*
解引用。需要先将其强制转换为具体的指针类型才能访问其指向的值。- 例如:c++
int n = 10; void *vp = &n; int *ip = static_cast<int*>(vp); // 强制转换为 int* std::cout << *ip << std::endl; // 现在可以解引用了
- 例如:
void
作为函数返回类型: 表示函数不返回任何值。这与void
指针是不同的概念。
二、引用 (Reference)
引用是 C++ 中另一个重要的概念,它提供了一种更安全、更方便的方式来间接操作变量。
1. 引用的概念:变量的别名
- 别名: 引用就像是给变量取了一个“绰号”或者“外号”。它并非新的变量,而是现有变量的另一个名字,它们指向相同的内存地址。
- 声明引用: 使用
&
符号声明引用类型。- 例如:c++
int a = 10; int &ra = a; // ra 是 a 的引用
ra
现在是变量a
的一个别名,对ra
的任何操作都会直接影响到a
。
- 例如:
- 引用的权限: 引用不能扩大被引用变量的访问权限。 例如,你不能创建一个指向
const int
变量的非常量引用。
2. 引用的特点:一旦绑定,终生不变
- 必须初始化: 引用在声明时必须立即被初始化,并且一旦初始化,就永远绑定到最初引用的变量,不能再指向其他变量。这保证了引用的有效性。
- 不可重新赋值指向: 引用一旦初始化,其指向的内存地址就不可改变。赋值操作会改变引用所指向变量的值,而不是改变引用的指向。c++
int a = 10; int b = 20; int &ra = a; // ra 引用 a ra = b; // 相当于 a = b; ra 仍然引用 a,只是 a 的值变成了 20
- C 语言没有引用: 引用是 C++ 特有的特性,C 语言中只有指针。
3. 指针和引用的区别:选择合适的工具
特性 | 指针 | 引用 |
---|---|---|
定义 | 是一种变量,存储的是内存地址。拥有自己的内存空间。 | 是一个已存在变量的别名,不占用额外的内存空间。 |
初始化 | 可以不初始化,可以在任何时候指向不同的变量(修改存储的地址)。 | 必须在声明时初始化,并且一旦初始化,就不能再指向其他变量。 |
空值 | 可以为空指针 (nullptr ),表示不指向任何有效的内存地址。 | 不存在空引用。引用在声明时必须绑定到一个有效的对象。 |
语法 | 使用 * 解引用来访问指向的值,使用 & 取地址。 | 直接使用引用名,就像使用原变量一样。 |
效率 | 间接访问,需要通过地址来找到值,效率略低于直接访问。 | 本质上是被引用变量的另一个名字,编译器通常会将其优化为直接访问,效率更高。 |
应用场景 | 动态内存分配、函数参数传递(需要修改实参)、实现链表等数据结构。 | 函数参数传递(避免拷贝,直接操作实参)、提高代码可读性、作为函数返回值。 |
本质 | 存储内存地址的变量。 | 变量的别名,是对内存地址的封装。 |
💡 记忆技巧: 可以把引用想象成一个人的“外号”,一旦有了这个外号,你就不能再把它给其他人了。而指针就像一个房子的地址,你可以修改地址指向的房子。
三、变量与常量 (Variables and Constants)
在讨论指针和引用时,理解变量和常量的概念至关重要。
1. 变量 (Variable)
- 可变性: 变量的值在程序运行过程中可以被修改和读取。
- 声明: 例如
int age = 20;
2. 常量 (Constant)
- 不可变性: 常量的值在声明后不能被修改。
- 声明: 使用关键字
const
声明常量。- 例如:
const int MAX_VALUE = 100;
- 例如:
- 作用: 常量用于表示程序中不应该被改变的值,提高代码的可读性和安全性。
四、常量指针和指针常量 (Const Pointers and Pointer Constants)
这是初学者常常混淆的一个概念,让我们仔细区分。
1. 常量指针 (Pointer to const)
- 定义: 指针指向的内存地址中的值是常量,不能通过该指针修改指向的值,但指针本身可以指向其他地址。
- 声明:
const 数据类型 * 指针变量名;
- 例如:c++
int a = 10; int b = 20; const int *pa1 = &a; // pa1 是一个指向常量整数的指针 // *pa1 = 30; // 错误!不能通过 pa1 修改 a 的值 pa1 = &b; // 正确!pa1 可以指向其他地址
- 例如:
- 理解:
const
修饰的是*pa1
,即指针指向的内容。
2. 指针常量 (Const Pointer)
- 定义: 指针本身是一个常量,一旦初始化后,它所指向的地址不能改变,但可以通过该指针修改指向的内存地址处的值。
- 声明:
数据类型 * const 指针变量名;
- 例如:c++
int a = 10; int b = 20; int *const pa2 = &a; // pa2 是一个指向整数的常量指针 *pa2 = 30; // 正确!可以通过 pa2 修改 a 的值 // pa2 = &b; // 错误!pa2 不能指向其他地址
- 例如:
- 理解:
const
修饰的是pa2
,即指针本身。
3. 指向常量的指针常量 (Const Pointer to const)
- 定义: 指针本身是常量,并且它指向的内存地址中的值也是常量,两者都不能被修改。
- 声明: `const 数据类型 * const 指const int * const pa3 = &a; // pa3 是一个指向常量整数的常量指针 // *pa3 = 30; // 错误!不能通过 pa3 修改 a 的值 // pa3 = &b; // 错误!pa3 不能指向其他地址 ```
- 理解: 两个
const
各司其职,第一个const
修饰*pa3
,表示不能通过pa3
修改指向的值;第二个const
修饰pa3
,表示pa3
的指向不能修改。
🤔 记忆技巧: 从右向左读有助于理解。
const int * pa
:pa
是一个指针,指向const int
(常量整数)。int * const pa
:pa
是一个const
指针,指向int
(整数)。const int * const pa
:pa
是一个const
指针,指向const int
(常量整数)。
五、auto
自动类型推断 (Automatic Type Deduction)
auto
关键字是 C++11 引入的非常方便的特性,可以简化代码并提高可读性。
- 编译器自动推断:
auto
允许编译器根据变量的初始化值自动推断变量的类型,而无需显式指定。- 例如:c++
auto x = 10; // x 被推断为 int auto y = 3.14; // y 被推断为 double auto z = "hello"; // z 被推断为 const char* std::vector<int> myVector = {1, 2, 3}; auto it = myVector.begin(); // it 被推断为 std::vector<int>::iterator
- 例如:
- 适用场景:
- 类型名过长或复杂: 例如在使用标准库容器时,迭代器的类型通常很长,使用
auto
可以简化声明。 - 类型不明确: 某些情况下,表达式的返回类型可能比较复杂或不直观,使用
auto
可以让代码更简洁。
- 类型名过长或复杂: 例如在使用标准库容器时,迭代器的类型通常很长,使用
- 提高代码可读性: 在某些情况下,
auto
可以让代码更专注于变量的含义,而不是具体的类型。 - 注意事项:
auto
必须初始化:编译器需要根据初始值来推断类型。auto
的推断结果是明确的,不是简单的“万能类型”。- 过度使用
auto
可能会降低代码的可读性,尤其是在不清楚变量类型的情况下。应该在提高代码可读性和维护性之间找到平衡。
六、常用转义字符 (Escape Sequences)
转义字符用于在字符串中表示一些特殊的字符,这些字符不能直接输入或具有特殊的含义。
\n
: 换行符 (Newline) - 将光标移动到下一行开头。\t
: 水平制表符 (Horizontal Tab) - 将光标移动到下一个制表位。\'
: 单引号 (Single Quote) - 用于在单引号包围的字符常量中表示单引号自身,例如'\''
。\"
: 双引号 (Double Quote) - 用于在双引号包围的字符串字面量中表示双引号自身,例如"abc\"def"
。\\
: 反斜杠 (Backslash) - 用于表示反斜杠自身,例如"C:\\Windows"
。\?
: 问号 (Question Mark) - 用于避免某些编译器将??
解释为三字符序列。\0
: 空字符 (Null Character) - 表示字符串的结尾,用于 C 风格的字符串。: 空格 - 虽然不是转义字符,但空格在字符串中也是常用的字符。
示例:
#include <iostream>
int main() {
std::cout << "第一行\n第二行" << std::endl;
std::cout << "Name:\tJohn\tAge:\t30" << std::endl;
std::cout << "单引号:\',双引号:\",反斜杠:\\" << std::endl;
return 0;
}
作业是写一个文本文件,将之前你理解的关键的概念加入总结:
- 指针是什么?和引用是什么?指针和引用之间的区别是什么?