指针引用
# 第 8 章 C++ 指针引用
# 目录介绍
- 8.1 指针概念
- 8.2 指针操作
- 8.3 指针使用
- 8.4 指针高级用法
- 8.5 引用
- 8.6 指针VS引用
- 8.7 总结
- 8.8 综合案例
- 8.9 指针与引用底层原理
- 8.10 指针与引用训练题
- 8.11 综合思考题
# 8.1 指针概念
# 8.1.1 什么是指针
指针的作用: 可以通过指针间接访问内存
- 内存编号是从0开始记录的,一般用十六进制数字表示
- 可以利用指针变量保存地址
指针,它存储了一个内存地址。指针可以指向其他变量或对象的内存地址,通过指针,可以直接访问和操作内存中的数据。
# 8.1.2 指针声明
指针变量定义语法:
数据类型 *指针变量名;
数据类型:指针指向的变量的类型(如int、double等)。*:表示这是一个指针变量。指针变量名:指针的名称。
# 8.1.3 指针初始化
指针在使用前必须初始化,否则会指向一个未知的内存地址,可能导致程序崩溃。
int *ptr = nullptr; // 初始化为空指针
看如下案例所示:
int main() {
int a = 10; //定义整型变量a
//1、指针的定义
//指针定义语法: 数据类型 * 变量名 ;
int * p;
//初始化,指针变量赋值
p = &a; //指针指向变量a的地址
cout << &a << endl; //打印数据a的地址
cout << p << endl; //打印指针变量p
//2、指针的使用
//通过*操作指针变量指向的内存
cout << "*p = " << *p << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
指针变量和普通变量的区别
- 普通变量存放的是数据,指针变量存放的是地址
- 指针变量可以通过" * "操作符,操作指针变量指向的内存空间,这个过程称为解引用
- 总结1:我们可以通过 & 符号 获取变量的地址
- 总结2:利用指针可以记录地址
- 总结3:对指针变量解引用,可以操作指针指向的内存
# 8.1.4 指针占用空间
提问:指针也是种数据类型,那么这种数据类型占用多少内存空间?示例:
int main() {
int a = 10;
int * p;
p = &a; //指针指向数据a的地址
cout << *p << endl; //* 解引用
cout << sizeof(p) << endl;
cout << sizeof(char *) << endl;
cout << sizeof(float *) << endl;
cout << sizeof(double *) << endl;\
return 0;
}
//10
//8
//8
//8
//8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在 C++ 中,指针的大小取决于编译器和操作系统的位数。通常情况下,指针在 C++ 中也会占用一定的内存空间,这个空间大小与系统的位数相关。
在大多数现代计算机系统中,指针的大小通常如下:
32 位系统:在 32 位系统中,指针通常占用 4 个字节(32 位)的内存空间。
64 位系统:在 64 位系统中,指针通常占用 8 个字节(64 位)的内存空间。
# 8.1.5 综合案例与思考
综合案例:指针的声明、初始化与基本操作
#include <iostream>
using namespace std;
int main() {
// 1. 指针的声明和初始化
int a = 42;
int* p1 = &a; // 指向a的指针
int* p2 = nullptr; // 空指针(安全初始化)
cout << "=== 指针基础 ===" << endl;
cout << "a的值: " << a << endl;
cout << "a的地址: " << &a << endl;
cout << "p1存储的地址: " << p1 << endl;
cout << "p1指向的值: " << *p1 << endl;
// 2. 通过指针修改值
*p1 = 100;
cout << "\n修改后a的值: " << a << endl; // 100
// 3. 指针的大小(与类型无关)
cout << "\n=== 指针大小 ===" << endl;
cout << "int*: " << sizeof(int*) << " 字节" << endl;
cout << "double*: " << sizeof(double*) << " 字节" << endl;
cout << "char*: " << sizeof(char*) << " 字节" << endl;
// 4. 空指针检查
cout << "\n=== 空指针检查 ===" << endl;
if (p2 == nullptr) {
cout << "p2是空指针,不能解引用" << endl;
}
// *p2 = 10; // 危险!解引用空指针会崩溃
// 5. 野指针的危险
cout << "\n=== 野指针警告 ===" << endl;
int* wild; // 未初始化!指向未知地址
// *wild = 10; // 极其危险!未定义行为
cout << "永远不要使用未初始化的指针!" << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
案例知识融合:这个案例覆盖了指针概念的核心知识——指针的声明与初始化、取地址&和解引用*、通过指针修改变量值、指针大小与系统位数相关(而非类型)、空指针nullptr的安全检查,以及野指针的危险性。
思考题:
int* p和int *p和int * p三种写法有区别吗?在多变量声明int* p, q;中,q是什么类型?nullptr(C++11)比NULL和0有什么优势?为什么C++11要引入新关键字?- 野指针和空指针哪个更危险?为什么?如何养成良好的指针使用习惯?
# 8.2 指针基本操作
# 8.2.1 取地址运算符
& 用于获取变量的内存地址。
int num = 10;
int *ptr = # // ptr 指向 num 的地址
2
# 8.2.2 解引用运算符
* 用于访问指针指向的内存地址中的值。
int num = 10;
int *ptr = #
cout << *ptr; // 输出 10
2
3
# 8.2.3 综合案例与思考
综合案例:指针运算与地址算术
#include <iostream>
using namespace std;
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* p = arr;
// 1. 指针算术:p+1不是加1字节,而是加一个元素大小
cout << "=== 指针算术 ===" << endl;
for (int i = 0; i < 5; ++i) {
cout << "*(p+" << i << ") = " << *(p + i)
<< " 地址: " << (p + i) << endl;
}
// 两个相邻元素的地址差 = sizeof(int) = 4字节
cout << "地址差: " << (long)((p + 1)) - (long)(p) << " 字节" << endl;
// 2. 指针比较
cout << "\n=== 指针比较 ===" << endl;
int* begin = arr;
int* end = arr + 5; // 指向数组尾后位置
cout << "数组范围: [" << begin << ", " << end << ")" << endl;
cout << "begin < end? " << (begin < end) << endl;
// 3. 指针递增遍历
cout << "\n=== 指针遍历 ===" << endl;
for (int* it = begin; it != end; ++it) {
cout << *it << " ";
}
cout << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
案例知识融合:这个案例演示了取地址&和解引用*之外的指针操作——指针算术(p+i跳过i个元素而非i字节)、指针差值(地址差等于元素大小×元素个数)、指针比较(用于判断范围)、以及用指针递增实现类似迭代器的数组遍历。
思考题:
p + 1实际上在地址上增加了sizeof(int)字节,这种"类型感知"的指针算术是如何实现的?如果p是void*能做算术吗?- 指向数组尾后位置的指针
arr + n是合法的,但解引用它是未定义行为。为什么C++允许创建这样的指针? - 两个不相关的指针(指向不同数组)可以比较大小吗?C++标准对此怎么说?
# 8.3 指针使用
# 8.3.1 指针和数组
数组名本身就是一个指针,指向数组的第一个元素。示例
#include <iostream>
using namespace std;
int main() {
int arr[3] = {10, 20, 30};
int *ptr = arr; // ptr 指向数组的第一个元素
for (int i = 0; i < 3; i++) {
cout << "Element " << i << ": " << *(ptr + i) << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
输出:
Element 0: 10
Element 1: 20
Element 2: 30
2
3
# 8.3.2 指针和函数
指针可以作为函数的参数或返回值,用于传递或返回内存地址。
1.指针作为函数参数
#include <iostream>
using namespace std;
void increment(int *ptr) {
(*ptr)++; // 修改指针指向的值
}
int main() {
int num = 10;
increment(&num); // 传递 num 的地址
cout << "Incremented value: " << num << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
输出:
Incremented value: 11
2.指针作为函数返回值
#include <iostream>
using namespace std;
int* getMax(int *a, int *b) {
return (*a > *b) ? a : b;
}
int main() {
int x = 10, y = 20;
int *maxPtr = getMax(&x, &y);
cout << "Max value: " << *maxPtr << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
输出:
Max value: 20
# 8.3.3 指针与常量
指针可以与 const 关键字结合,表示指针指向的值或指针本身不可修改。
const修饰指针有三种情况
- const修饰指针 --- 常量指针。常量指针(Constant Pointer):在这种情况下,const 修饰指针本身,表示指针本身是常量,不能通过该指针修改指向的地址。
- const修饰常量 --- 指针常量。指向常量的指针(Pointer to Constant):在这种情况下,const 修饰指针所指向的值,表示指针指向的值是常量,不能通过该指针修改所指向的值。
- const即修饰指针,又修饰常量。指向常量的常量指针(Constant Pointer to Constant):结合上述两种情况,指针本身和指针所指向的值都是常量,既不能通过指针修改所指向的值,也不能修改指针本身指向的地址。
1.指向常量的指针
const int *ptr; // ptr 指向的值不可修改
2.常量指针
int *const ptr = # // ptr 本身不可修改
3.指向常量的常量指针
const int *const ptr = # // ptr 和 ptr 指向的值都不可修改
技巧:看const右侧紧跟着的是指针还是常量, 是指针就是常量指针,是常量就是指针常量
int main() {
int a = 10;
int b = 10;
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; //正确
//*p1 = 100; 报错
//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
//const既修饰指针又修饰常量
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 8.3.4 指针和结构体
作用:通过指针访问结构体中的成员
利用操作符 ->可以通过结构体指针访问结构体属性
结构体指针是指向结构体实例的指针,允许你通过指针来访问和操作结构体的数据成员。
//结构体定义
struct student4 {
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
};
int main() {
struct student4 stu = {"张三", 18, 100,};
struct student4 *p = &stu;
p->score = 80; //指针通过 -> 操作符可以访问成员
cout << "姓名:" << p->name << " 年龄:" << p->age << " 分数:" << p->score << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
总结:结构体指针可以通过 -> 操作符 来访问结构体中的成员
# 8.3.5 综合案例与思考
综合案例:指针在数组、函数和结构体中的综合应用
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int scores[3];
};
// 用const指针保护数据不被修改
void printStudent(const Student* s) {
cout << s->name << ": ";
for (int i = 0; i < 3; ++i)
cout << s->scores[i] << " ";
cout << endl;
}
// 指针遍历数组找最大值
int findMax(const int* arr, int size) {
const int* maxPtr = arr;
for (int i = 1; i < size; ++i) {
if (*(arr + i) > *maxPtr)
maxPtr = arr + i;
}
return *maxPtr;
}
// 用指针实现swap
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
// 1. 指针与数组
int arr[] = {15, 42, 8, 23, 51};
cout << "最大值: " << findMax(arr, 5) << endl;
// 2. 指针与函数(swap)
int x = 10, y = 20;
swap(&x, &y);
cout << "交换后: x=" << x << ", y=" << y << endl;
// 3. 指针与const
const int* cp = arr; // 指向常量的指针
// *cp = 100; // 错误!不能通过cp修改值
cp = arr + 2; // 可以改变指向
cout << "cp指向: " << *cp << endl;
int* const pc = arr; // 常量指针
*pc = 100; // 可以修改值
// pc = arr + 2; // 错误!不能改变指向
// 4. 指针与结构体
Student s = {"张三", {90, 85, 92}};
Student* sp = &s;
sp->scores[1] = 95; // 通过指针修改
printStudent(sp);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
案例知识融合:这个案例综合运用了指针与数组(遍历找最大值)、指针与函数(swap交换)、指针与const(常量指针 vs 指向常量的指针)、指针与结构体(->运算符访问成员)四个核心知识点。
思考题:
const int*和int* const的区别是什么?有一个记忆技巧叫"const在左是底层const,在右是顶层const",请解释这个规则。- 数组作为函数参数时会退化为指针,丢失了长度信息。有什么方法可以保留数组长度信息?(提示:考虑模板或
std::array) - 通过指针实现的swap函数和通过引用实现的swap函数有什么区别?C++标准库的
std::swap用的是哪种方式?
# 8.4 指针高级用法
# 8.4.1 指针的指针
指针可以指向另一个指针。
int num = 10;
int *ptr = #
int **ptr2 = &ptr; // ptr2 指向 ptr
2
3
# 8.4.2 函数指针
函数指针是指向函数的指针变量,可以用于动态调用函数。
#include <iostream>
using namespace std;
// 函数
int add(int a, int b) {
return a + b;
}
int main() {
// 声明函数指针
int (*funcPtr)(int, int) = add;
// 使用函数指针调用函数
int result = funcPtr(3, 5);
cout << "Sum: " << result << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
输出:
Sum: 8
# 8.4.3 综合案例与思考
综合案例:函数指针实现简易计算器
#include <iostream>
using namespace std;
// 四则运算函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divOp(int a, int b) { return b != 0 ? a / b : 0; }
// 函数指针类型别名
using Operation = int(*)(int, int);
// 用函数指针数组实现计算器
void calculate(int a, int b, Operation op, const string& name) {
cout << a << " " << name << " " << b << " = " << op(a, b) << endl;
}
int main() {
// 1. 基本函数指针
cout << "=== 函数指针 ===" << endl;
int (*funcPtr)(int, int) = add;
cout << "通过函数指针调用: " << funcPtr(3, 5) << endl;
// 2. 函数指针数组(跳转表)
cout << "\n=== 函数指针数组 ===" << endl;
Operation ops[] = {add, sub, mul, divOp};
string names[] = {"+", "-", "*", "/"};
for (int i = 0; i < 4; ++i) {
calculate(10, 3, ops[i], names[i]);
}
// 3. 指针的指针
cout << "\n=== 指针的指针 ===" << endl;
int val = 42;
int* p = &val;
int** pp = &p;
cout << "val = " << val << endl;
cout << "*p = " << *p << endl;
cout << "**pp = " << **pp << endl;
**pp = 100;
cout << "修改后val = " << val << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
案例知识融合:这个案例展示了指针的高级用法——函数指针的声明和调用、函数指针数组(跳转表模式)实现计算器分发、using类型别名简化复杂指针类型声明、以及指针的指针(二级指针)的读写操作。
思考题:
- 函数指针数组(跳转表)在性能上比switch语句有什么优势?游戏引擎和解释器中为什么常用这种模式?
- C++11的
std::function和传统函数指针相比有什么优势?它能存储哪些函数指针不能存储的可调用对象? - 二级指针(指针的指针)在实际编程中有哪些应用场景?为什么C函数有时需要
char**参数?
# 8.5 引用
# 8.5.1 什么是引用
在 C++ 中,引用 是一种别名机制,它为已存在的变量提供了一个新的名称。引用与指针类似,但更安全且易于使用。
引用是一个变量的别名,它必须在声明时初始化,并且一旦绑定到一个变量后,就不能再绑定到其他变量。
# 8.5.2 引用声明
作用: 给变量起别名
数据类型 &引用名 = 变量名;
数据类型:引用绑定的变量的类型。&:表示这是一个引用。引用名:引用的名称。变量名:引用绑定的变量。
示例
void test1() {
int a = 10;
int &b = a; // b 是 a 的引用
cout << "a = " << a << endl;
cout << "b = " << b << endl;
b = 100; //修改 b 等同于修改 a
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
int main() {
test1();
return 0;
}
// a = 10
// b = 10
// a = 100
// b = 100
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 8.5.3 引用必须初始化
引用在声明时必须绑定到一个变量,否则会编译错误。
int num = 10;
int &ref = num; // 正确
int &ref2; // 错误:引用必须初始化
2
3
引用一旦绑定到一个变量后,就不能再绑定到其他变量。
int num1 = 10, num2 = 20;
int &ref = num1;
ref = num2; // 这是赋值操作,不是重新绑定
2
3
# 8.5.4 引用访问数据
通过引用可以访问和修改原变量的值。
int num = 10;
int &ref = num;
ref = 20; // 修改 ref 的值
cout << num; // 输出 20
2
3
4
# 8.5.5 引用和函数
1.引用作为函数参数
函数可以返回引用,但必须确保返回的引用指向的变量在函数调用结束后仍然有效。示例
void increment(int& num) {
num++;
}
int main() {
int a = 10;
increment(a); // 传递引用
std::cout << a; // 输出 11
return 0;
}
2
3
4
5
6
7
8
9
10
2.引用作为函数返回值
函数返回值 引用可以作为函数的返回值,但必须确保返回的引用指向有效的内存。
int& getMax(int& a, int& b) {
return (a > b) ? a : b;
}
int main() {
int x = 10, y = 20;
cout << "y = " << getMax(x,y) << endl; // 输出 20
getMax(x, y) = 30; // 修改较大的值
cout << "y = " << y << endl; // 输出 30
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 8.5.6 常量引用
作用: 常量引用(const 引用)用于防止通过引用修改原变量,同时避免拷贝开销。
在函数形参列表中,可以加==const修饰形参==,防止形参改变实参
示例:
//引用使用的场景,通常用来修饰形参
void showValue(const int & v) {
//v += 10; //常量不能做新的赋值。会直接编译报错
cout << v << endl;
}
void test() {
//int& ref = 10;//引用本身需要一个合法的内存空间,因此这行错误
//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
const int& ref = 10;
//ref = 100; //加入const后不可以修改变量
cout << ref << endl;
//函数中利用常量引用防止误操作修改实参
int a = 10;
showValue(a);
}
int main() {
test();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 8.5.7 避免返回局部变量引用
避免返回局部变量的引用:局部变量在函数结束后会被销毁,返回其引用会导致未定义行为。
int& badFunction() {
int a = 10;
return a; // 错误:返回局部变量的引用
}
2
3
4
# 8.5.8 综合案例与思考
综合案例:引用在实际编程中的最佳实践
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// 1. const引用做参数(最常用的函数参数方式)
double average(const vector<int>& scores) {
double sum = 0;
for (int s : scores) sum += s;
return sum / scores.size();
}
// 2. 引用做返回值(允许链式调用)
class StringBuilder {
string data_;
public:
StringBuilder& append(const string& s) {
data_ += s;
return *this; // 返回自身引用
}
const string& str() const { return data_; }
};
// 3. 引用 vs 拷贝的性能差异
void processByValue(string s) { /* 拷贝 */ }
void processByRef(const string& s) { /* 零拷贝 */ }
int main() {
// const引用做参数
vector<int> scores = {85, 92, 78, 96, 88};
cout << "平均分: " << average(scores) << endl;
// 引用返回实现链式调用
StringBuilder sb;
sb.append("Hello").append(", ").append("World!");
cout << sb.str() << endl;
// 常量引用绑定临时对象
const int& ref = 42; // 合法!const引用延长临时对象生命周期
cout << "const引用绑定字面量: " << ref << endl;
// 引用必须初始化且不能重新绑定
int a = 10, b = 20;
int& r = a;
r = b; // 这是赋值,不是重新绑定!a变成了20
cout << "a = " << a << " (被赋值为b的值)" << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
案例知识融合:这个案例展示了引用的最佳实践——const引用作为函数参数(避免拷贝+保护数据)、返回*this引用实现链式调用、常量引用可以绑定字面量和临时对象、以及引用赋值和重新绑定的区别。
思考题:
const int& ref = 42;是合法的,编译器在背后做了什么?为什么非const引用不能绑定字面量?- 引用在底层通常是用指针实现的,那为什么说"引用不占额外内存"?这是否矛盾?
- C++11引入了右值引用(
int&&),它和左值引用(int&)有什么区别?为什么需要右值引用?
# 8.6 指针VS引用
# 8.6.1 两者对比
| 特性 | 引用 | 指针 |
|---|---|---|
| 语法 | 类型& 引用名;比如 int& ref | 数据类型 * 变量名;比如 int* ref |
| 初始化 | 必须初始化 | 可以不初始化 |
| 重新绑定 | 不能重新绑定 | 可以重新指向其他变量 |
| 空值 | 不能为空 | 可以为 nullptr |
| 内存占用 | 不占用额外内存 | 占用额外内存(存储地址) |
| 语法 | 更简洁(直接使用变量名) | 需要解引用(* 操作符) |
# 8.6.2 两者转换
指针可以隐式转换为引用:
int a = 10;
int* p = &a;
int& r = *p; // 将指针解引用为引用
2
3
引用不能直接转换为指针,但可以获取引用的地址:
int a = 10;
int& r = a;
int* p = &r; // 获取引用的地址
2
3
# 8.6.3 综合案例与思考
综合案例:指针与引用的选择策略
#include <iostream>
#include <string>
using namespace std;
struct Node {
int value;
Node* next; // 链表必须用指针(可能为nullptr)
};
// 场景1:必须用指针(可能为空)
Node* findNode(Node* head, int target) {
while (head != nullptr) {
if (head->value == target) return head;
head = head->next;
}
return nullptr; // 没找到返回空
}
// 场景2:优先用引用(一定存在)
void doubleValue(int& val) {
val *= 2;
}
// 场景3:只读大对象用const引用
void printInfo(const string& info) {
cout << info << endl;
}
int main() {
// 指针场景:链表操作
Node n3 = {30, nullptr};
Node n2 = {20, &n3};
Node n1 = {10, &n2};
Node* found = findNode(&n1, 20);
if (found) cout << "找到: " << found->value << endl;
found = findNode(&n1, 99);
if (!found) cout << "未找到99" << endl;
// 引用场景:修改变量
int x = 5;
doubleValue(x);
cout << "翻倍后: " << x << endl;
// 选择建议总结
cout << "\n=== 选择策略 ===" << endl;
cout << "1. 可能为空 -> 用指针" << endl;
cout << "2. 一定存在且不重新绑定 -> 用引用" << endl;
cout << "3. 只读大对象 -> 用const引用" << endl;
cout << "4. 需要重新指向 -> 用指针" << endl;
cout << "5. 动态内存管理 -> 用智能指针" << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
案例知识融合:这个案例通过实际场景说明了指针和引用的选择策略——链表等可能为空的场景用指针、确定存在的参数传递用引用、只读大对象用const引用。明确了两者的使用边界。
思考题:
- Google C++风格指南建议"输入参数用const引用,输出参数用指针",这个规则背后的理由是什么?你同意吗?
- 在哪些C++特性中必须使用引用(不能用指针替代)?(提示:考虑运算符重载、拷贝构造函数等)
- C++的智能指针(
unique_ptr、shared_ptr)结合了指针的灵活性和引用的安全性,它们是否完全替代了裸指针?什么场景下仍需要使用裸指针?
# 8.8 综合案例
# 8.8.1 遍历数组案例
作用:利用指针访问数组中元素。数组名本身是一个指针,指向数组的第一个元素。
示例:
int main() {
int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = arr; //指向数组的指针。看解释1
cout << "第一个元素: " << arr[0] << endl;
cout << "指针访问第一个元素: " << *p << endl; //指向数组第一个元素的指针。看解释2
for (int i = 0; i < 10; i++) {
//利用指针遍历数组。看解释3
cout << *p << endl;
p++;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
- 解释1,数组名是指针:在 C++ 中,数组名可以被隐式转换为指向数组第一个元素的指针。这意味着p可以将数组名视为指向数组的第一个元素的指针。
- 解释2,指针和数组的关系:指针可以用来访问数组中的元素。通过指针算术运算,可以遍历数组中的元素。
- 解释3,指针和数组的传递:当传递数组给函数时,实际上传递的是数组的地址,即数组名被隐式转换为指向数组第一个元素的指针。
# 8.8.2 指针vs普通对象
值语义 vs 指针语义,核心概念理解:
// 值语义:操作副本
Account copy = original; // 创建副本
copy.deposit(100); // 修改副本,原对象不变
// 指针语义:操作原对象
Account* ptr = &original; // 指向原对象
ptr->deposit(100); // 修改原对象
2
3
4
5
6
7
性能影响分析,内存使用对比
// Account对象大小估算
class Account {
std::string accountNumber; // ~24字节(小字符串优化)
std::string name; // ~24字节
double balance; // 8字节
// 总计:约56字节
};
// 使用指针:8字节(64位系统)
Account* ptr;
// 使用对象:56字节 + 复制开销
Account obj;
2
3
4
5
6
7
8
9
10
11
12
13
时间复杂度对比:
- 指针方式:O(1) 赋值操作
- 对象方式:O(n) 复制操作(n为对象大小)
# 8.8.3 交换两个变量值
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10, y = 20;
swap(x, y); // 传递引用
std::cout << x << " " << y; // 输出 20 10
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# 8.9 指针与引用底层原理
# 8.9.1 指针的内存模型与寻址
指针变量本质上就是一个存储内存地址的整数。在x86-64系统中,指针占8字节,存储的是目标变量在虚拟地址空间中的64位地址。
指针变量在内存中的布局:
假设 int a = 42; 存放在地址 0x7ffd1234
int* p = &a; p本身存放在地址 0x7ffd1240
内存布局:
地址 内容 说明
0x7ffd1234 2A 00 00 00 a的值42(小端序)
...
0x7ffd1240 34 12 fd 7f p的值(存的是a的地址,小端序)
00 00 00 00 (地址高4字节)
2
3
4
5
6
7
8
9
取地址与解引用的汇编实现:
int a = 42;
int* p = &a;
int b = *p;
2
3
对应的x86-64汇编(GCC -O0):
; int a = 42;
mov DWORD PTR [rbp-12], 42 ; 将42存入栈上a的位置
; int* p = &a;
lea rax, [rbp-12] ; LEA(Load Effective Address)取a的地址
mov QWORD PTR [rbp-8], rax ; 将地址存入p(8字节)
; int b = *p;
mov rax, QWORD PTR [rbp-8] ; 将p的值(地址)加载到rax
mov eax, DWORD PTR [rax] ; 间接寻址:从rax指向的地址读取4字节
mov DWORD PTR [rbp-16], eax ; 存入b
2
3
4
5
6
7
8
9
10
11
关键指令解析:
LEA(Load Effective Address):计算地址但不访问内存,用于取地址&操作MOV reg, [reg]:间接寻址,即解引用*操作,CPU通过寄存器中的地址去访问内存- 取地址是编译期计算(栈变量的偏移在编译时已知),解引用是运行期的内存访问
空指针的底层:
int* p = nullptr; // 等价于 p = 0
*p = 10; // 段错误(Segmentation Fault)
2
nullptr在底层就是地址0x0。操作系统将虚拟地址空间的第0页标记为不可访问,任何对地址0的读写都会触发页错误(Page Fault),内核向进程发送SIGSEGV信号,程序崩溃。这是一种硬件级别的保护机制。
# 8.9.2 指针运算的汇编实现
指针算术的本质——类型感知的地址计算:
int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;
int val = *(p + 2); // 访问arr[2]
2
3
汇编:
; int* p = arr;
lea rax, [rbp-32] ; 取数组首地址
; *(p + 2) 的计算过程:
; 编译器知道p指向int(4字节),所以p+2 = p + 2*sizeof(int) = p + 8
mov eax, DWORD PTR [rax+8] ; 直接地址 + 8字节偏移
2
3
4
5
6
编译器在编译期就将p + 2翻译为地址 + 2 × sizeof(int),这就是指针算术"类型感知"的底层实现——编译器根据指针的类型自动计算偏移量。
数组下标 vs 指针偏移:
arr[i] // 等价于 *(arr + i)
*(p + i) // 等价于 p[i]
2
两者在汇编层面完全等价,编译器生成相同的指令。C/C++标准定义a[b]等价于*(a+b),因此理论上2[arr]也是合法的(等价于*(2+arr))。
void 为什么不能做指针运算*:
void* vp = arr;
// vp + 1; // 错误!编译器不知道sizeof(void)是多少
2
指针运算需要知道元素大小,void*没有类型信息,编译器无法计算偏移量。GNU扩展将sizeof(void)视为1,但这不是标准行为。
# 8.9.3 引用的底层本质
引用在汇编层面等价于const指针:
int a = 42;
int& ref = a; // 引用
int* const p = &a; // 常量指针
2
3
两者生成的汇编完全相同:
; int& ref = a; 和 int* const p = &a; 生成相同代码:
lea rax, [rbp-12] ; 取a的地址
mov QWORD PTR [rbp-8], rax ; 存储地址
2
3
通过引用修改值:
ref = 100; // 通过引用赋值
*p = 100; // 通过指针赋值
2
; 两者汇编完全相同:
mov rax, QWORD PTR [rbp-8] ; 加载存储的地址
mov DWORD PTR [rax], 100 ; 间接寻址写入
2
3
结论:引用在底层就是一个自动解引用的const指针。编译器隐藏了取地址和解引用的语法,让引用看起来像变量的别名。
引用"不占内存"的说法辨析:
| 说法 | 真相 |
|---|---|
| "引用不占内存" | 作为局部变量时可能不占——编译器可能优化掉引用,直接用原变量地址 |
| "引用占内存" | 作为类成员或函数参数时占内存——底层需要存储地址(8字节) |
struct Ref {
int& r; // sizeof = 8,确实占用内存
};
// 编译器优化后的局部引用
int a = 10;
int& r = a;
r = 20;
// 优化后等价于:
// a = 20; 编译器直接消除了引用
2
3
4
5
6
7
8
9
10
引用传参 vs 值传参的汇编对比:
void byValue(int x) { x = 100; }
void byRef(int& x) { x = 100; }
void byPtr(int* x) { *x = 100; }
2
3
byValue:
mov DWORD PTR [rbp-4], edi ; 参数拷贝到栈上
mov DWORD PTR [rbp-4], 100 ; 修改的是副本
byRef: ; 和byPtr生成完全相同的代码!
mov DWORD PTR [rdi], 100 ; rdi存放的就是原变量地址,直接写入
byPtr:
mov DWORD PTR [rdi], 100 ; 和byRef完全一样
2
3
4
5
6
7
8
9
# 8.9.4 const指针与const引用的编译器处理
const的底层实现——编译期检查,不产生运行时开销:
const int* p1 = &a; // 底层const:不能通过p1修改a
int* const p2 = &a; // 顶层const:不能修改p2本身
const int& r = a; // const引用:不能通过r修改a
2
3
这三种const修饰在汇编层面没有任何区别——它们和非const版本生成相同的机器码。const是纯粹的编译期约束,编译器在编译阶段检查是否有违规的写操作,通过后const信息就被丢弃了。
const_cast的危险:
const int a = 42;
const int& r = a;
int& evil = const_cast<int&>(r);
evil = 100; // 未定义行为!
2
3
4
因为编译器可能将const int a = 42优化为编译期常量,直接在使用处替换为42,修改evil实际修改的是一个已经不被读取的内存位置。
常量引用绑定临时对象的底层:
const int& r = 42;
// 编译器生成:
// int __temp = 42;
// const int& r = __temp; // __temp的生命周期延长到r的作用域
2
3
4
编译器会创建一个匿名临时变量,并将其生命周期延长(lifetime extension)到引用的生命周期。这是C++标准的特殊规定。
# 8.10 指针与引用训练题
训练题1:实现安全的动态数组
要求:用指针和动态内存实现一个简化版的整型动态数组类,支持添加元素、按下标访问、获取大小、自动扩容。
#include <iostream>
#include <cstring>
using namespace std;
class IntArray {
int* data_; // 指向堆上数组的指针
int size_; // 当前元素个数
int capacity_; // 当前容量
public:
IntArray() : data_(nullptr), size_(0), capacity_(0) {}
~IntArray() {
delete[] data_; // 释放堆内存
data_ = nullptr;
}
void push_back(int val) {
if (size_ >= capacity_) {
int newCap = (capacity_ == 0) ? 4 : capacity_ * 2;
int* newData = new int[newCap];
if (data_) {
memcpy(newData, data_, size_ * sizeof(int));
delete[] data_;
}
data_ = newData;
capacity_ = newCap;
}
data_[size_++] = val;
}
// 返回引用,支持读写
int& operator[](int index) {
if (index < 0 || index >= size_) {
throw out_of_range("Index out of range");
}
return data_[index]; // 返回引用
}
// const版本,只读
const int& operator[](int index) const {
if (index < 0 || index >= size_) {
throw out_of_range("Index out of range");
}
return data_[index];
}
int getSize() const { return size_; }
// 用指针实现迭代
int* begin() { return data_; }
int* end() { return data_ + size_; }
};
int main() {
IntArray arr;
for (int i = 0; i < 10; ++i) {
arr.push_back(i * 10);
}
// 下标访问(通过引用修改)
arr[3] = 999;
// 指针迭代
cout << "数组内容: ";
for (int* p = arr.begin(); p != arr.end(); ++p) {
cout << *p << " ";
}
cout << endl;
// 输出: 0 10 20 999 40 50 60 70 80 90
cout << "大小: " << arr.getSize() << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
练习重点:指针管理堆内存、operator[]返回引用、指针迭代器模式、扩容策略(2倍增长)。
训练题2:用指针实现链表基本操作
要求:使用指针实现单链表的创建、插入、查找、删除和遍历。
#include <iostream>
using namespace std;
struct Node {
int data;
Node* next;
Node(int val) : data(val), next(nullptr) {}
};
class LinkedList {
Node* head_;
public:
LinkedList() : head_(nullptr) {}
~LinkedList() {
Node* curr = head_;
while (curr) {
Node* temp = curr;
curr = curr->next;
delete temp;
}
}
// 头插法
void pushFront(int val) {
Node* newNode = new Node(val);
newNode->next = head_;
head_ = newNode;
}
// 尾插法(需要遍历到末尾)
void pushBack(int val) {
Node* newNode = new Node(val);
if (!head_) {
head_ = newNode;
return;
}
Node* curr = head_;
while (curr->next) {
curr = curr->next;
}
curr->next = newNode;
}
// 查找:返回指针(可能为nullptr)
Node* find(int val) {
Node* curr = head_;
while (curr) {
if (curr->data == val) return curr;
curr = curr->next;
}
return nullptr;
}
// 删除:使用指向指针的指针(二级指针思维)
bool remove(int val) {
Node** pp = &head_; // 指向"指向当前节点的指针"
while (*pp) {
if ((*pp)->data == val) {
Node* toDelete = *pp;
*pp = (*pp)->next; // 修改前驱的next指针
delete toDelete;
return true;
}
pp = &((*pp)->next);
}
return false;
}
void print() const {
for (Node* p = head_; p; p = p->next) {
cout << p->data;
if (p->next) cout << " -> ";
}
cout << " -> NULL" << endl;
}
};
int main() {
LinkedList list;
list.pushBack(10);
list.pushBack(20);
list.pushBack(30);
list.pushFront(5);
cout << "初始链表: ";
list.print(); // 5 -> 10 -> 20 -> 30 -> NULL
Node* found = list.find(20);
if (found) cout << "找到: " << found->data << endl;
list.remove(20);
cout << "删除20后: ";
list.print(); // 5 -> 10 -> 30 -> NULL
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
练习重点:指针链接节点、二级指针简化删除逻辑(Linus推荐的技巧)、指针遍历、动态内存管理。
训练题3:探究引用与指针的汇编等价性
要求:编写以下代码,用g++ -S -O0生成汇编,观察引用和指针的底层实现。
#include <iostream>
using namespace std;
void swapByPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
void swapByRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 观察:结构体中引用占多少空间?
struct RefHolder {
int& ref;
RefHolder(int& r) : ref(r) {}
};
struct PtrHolder {
int* ptr;
PtrHolder(int* p) : ptr(p) {}
};
int main() {
int x = 10, y = 20;
// 对比两种swap
swapByPtr(&x, &y);
cout << "指针swap后: x=" << x << ", y=" << y << endl;
swapByRef(x, y);
cout << "引用swap后: x=" << x << ", y=" << y << endl;
// 验证引用在结构体中的大小
cout << "sizeof(RefHolder) = " << sizeof(RefHolder) << endl; // 8
cout << "sizeof(PtrHolder) = " << sizeof(PtrHolder) << endl; // 8
cout << "两者大小相同!引用底层就是指针" << endl;
// 验证引用的地址
int a = 42;
int& r = a;
cout << "&a = " << &a << endl;
cout << "&r = " << &r << endl; // 和&a完全相同!
cout << "引用没有自己的地址,&r就是&a" << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
编译并查看汇编:
g++ -S -O0 -o test.s test.cpp
# 在test.s中搜索swapByPtr和swapByRef
# 你会发现两者的函数体汇编指令完全相同
2
3
练习重点:通过实验验证引用=const指针的结论、理解sizeof反映底层实现、掌握用汇编分析C++特性的方法。
# 8.11 综合思考题
指针与虚拟内存:程序中的指针存储的是虚拟地址而非物理地址。同一个指针值
0x7ffd1234在不同进程中指向不同的物理内存。请思考:如果两个进程中的指针值相同,它们指向的数据相同吗?操作系统是如何通过**页表(Page Table)**实现虚拟地址到物理地址的映射的?智能指针的底层实现:
unique_ptr的大小通常和裸指针一样(8字节),而shared_ptr却是16字节。请分析:shared_ptr多出的8字节存储了什么?引用计数是如何实现线程安全的(提示:原子操作)?为什么unique_ptr能做到零开销抽象?指针别名(Pointer Aliasing)问题:编译器在优化时需要假设两个指针是否可能指向同一块内存。C99引入了
restrict关键字(C++中非标准但编译器支持__restrict),请思考:指针别名为什么会阻碍编译器优化?restrict如何帮助编译器生成更高效的代码?移动语义与指针:C++11的移动语义(
std::move)本质上是转移资源的所有权,而资源通常通过指针持有。请思考:std::vector的移动构造函数做了什么(提示:只是拷贝了内部指针)?为什么移动比拷贝快?移动后源对象的指针变成了什么状态?
# 8.12 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | 悬挂指针 / 悬挂引用 | 指向已销毁对象;一律 RAII + 智能指针 |
| 2 | 把同一裸指针交给两个 shared_ptr | 双重 free;只通过 make_shared 获取 |
| 3 | unique_ptr 按值传递 | 触发所有权转移,调用方不持有;按 & 或 T* 传 |
| 4 | 用 delete 释放 new[] | UB;规则:方括号配方括号 |
| 5 | 函数返回局部变量引用/指针 | 栈对象已销毁;返回值或 unique_ptr |