C++学习笔记——双重指针与二维数组
C++学习笔记:双重指针与二维数组
哈喽,同学们!今天我们来聊聊C++里两个听起来有点“绕”但实际很重要的概念:双重指针和二维数组。别担心,我会用最直白的方式,从学生的角度来给大家讲清楚!
一、双重指针(指针的指针)
1. 什么是双重指针?
我们知道,一个普通指针(比如 int* p
)是用来存储一个变量的地址的。那双重指针呢?顾名思义,它就是用来存储指针的地址的!
想象一下,你有一个房间(变量),房间里放着一个纸条,纸条上写着另一个房间的地址(普通指针)。现在,双重指针就是另一个纸条,上面写着你手里这个纸条的地址。是不是有点像“套娃”?
在C++中,我们用两个星号 **
来声明一个双重指针。
举个例子:
1 | int a = 10; // 定义一个整型变量 a |
2. 怎么理解双重指针?
我们来画个图,帮助大家理解内存中的关系:
1 | graph TD |
a
:存储值10
。p
:存储a
的地址。pp
:存储p
的地址。
3. 如何通过双重指针访问数据?
pp
:存储的是p
的地址。*pp
:通过pp
解引用一次,得到的是p
的值,也就是a
的地址。所以*pp
等价于p
。**pp
:通过pp
解引用两次,第一次得到p
的值(a
的地址),第二次再解引用a
的地址,就得到了a
的值。所以**pp
等价于*p
,也等价于a
。
代码验证:
1 |
|
4. 双重指针的常见用途
函数参数: 想象一下,你有一个空箱子(
int* myArr = nullptr;
),你希望一个函数(allocateMemory
)帮你把这个箱子装满(让它指向一块新分配的内存)。如果函数只接收
int* ptr
(普通指针),那么它只能修改ptr
指向的内容,而不能改变ptr
本身指向哪里。就像你把箱子里的东西拿出来给别人看,别人只能看,不能把你的箱子换成另一个箱子。但是,如果你把箱子的地址(
&myArr
)传给函数,函数就能通过这个地址找到你的箱子,然后把箱子里的“纸条”(myArr
存储的地址)换成新的“纸条”(新分配内存的地址)。所以,当函数需要修改外部指针变量本身(让它指向新的内存地址)时,就需要传入双重指针。
1
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
70void allocateMemory(int** ptr) { // ptr 是一个双重指针,它存储了外部指针的地址
// *ptr 表示通过 ptr 找到外部指针变量本身
// new int[5] 分配了一块新的内存,并返回这块内存的首地址
// 这一行代码的意思是:把新分配内存的首地址,赋值给外部指针变量
*ptr = new int[5];
}
int main() {
int* myArr = nullptr; // 声明一个空指针 myArr,它现在不指向任何有效的内存
std::cout << "调用 allocateMemory 前,myArr 的值 (指向的地址): " << myArr << std::endl; // 此时为 0x0 或 nullptr,表示空
allocateMemory(&myArr); // 传入 myArr 的地址,让函数能修改 myArr 本身的值(即它指向的地址)
std::cout << "调用 allocateMemory 后,myArr 的值 (指向的地址): " << myArr << std::endl; // 此时为新分配内存的地址
// 重点来了!修改空指针的地址有什么作用?我们继续用超市的例子来理解:
// 你有一个空的购物袋 (myArr),它现在是空的 (nullptr),不指向任何有用的东西。
// 你想让一个朋友 (allocateMemory 函数) 帮你去超市买 5 样东西 (new int[5] 分配内存)。
// 情况一:如果你只把购物袋本身给朋友 (void allocateMemory(int* ptr))
// 朋友拿到你的购物袋,他可以在里面装东西,但这个购物袋还是你原来的那个。
// 朋友不能把你的购物袋扔掉,然后给你换一个全新的、更大的购物袋。
// 也就是说,函数内部 `ptr = new int[5];` 只是修改了函数内部 `ptr` 这个局部变量的指向,
// 外部的 `myArr` 仍然是空的,没有指向新买到的东西。
// 这就是普通指针作为函数参数的局限性。
// 那么,双重指针(情况二)就是为了解决这个问题而存在的。
// 朋友通过“你放购物袋的那个位置”,就可以直接把你的旧购物袋(空指针)换成一个装满东西的新购物袋(新分配的内存地址),
// 这样你就能在函数外部使用这块新内存了。
// 情况二:你把“你放购物袋的那个位置”告诉朋友 (传入 &myArr,用双重指针 int** ptr 接收)
// 朋友通过你告诉他的“位置”(`&myArr`),找到了你放购物袋的地方。
// 朋友就可以直接在你放购物袋的位置,把旧的空购物袋(`nullptr`)拿走,
// 然后把一个装满 5 样东西的全新购物袋(`new int[5]` 返回的新内存地址)放到那个位置。
// 这样,当朋友回来后,你直接去那个位置拿购物袋,拿到的就是那个装满东西的新购物袋了!
// 所以,修改空指针的地址,就是为了让函数能够把在它内部动态分配的内存,
// “传递”给函数外部的指针变量,让外部的指针能够真正地“拥有”并使用这块新内存。
// 简单来说,就是通过修改 `myArr` 这个空指针的地址,让它从“空”变为“指向新分配的内存”,
// 从而 `main` 函数才能使用这块由 `allocateMemory` 函数分配的内存。
// 补充:如果这里的 `nullptr` 换成一个不为空的数组的指针,是不是也能成立,通过双重指针修改内存的大小?
// 答案是:**能成立!**
// 双重指针的作用是修改它所指向的那个“一级指针”的值(也就是地址)。
// 无论这个“一级指针”原来是 `nullptr`(空),还是已经指向了一块内存,
// 只要你把这个“一级指针”的地址传给双重指针,函数内部就可以通过双重指针来修改这个“一级指针”的指向。
//
// 但是,这里需要澄清一个概念:**不是直接修改“内存的大小”**。
// `new` 和 `delete` 是用来**分配**和**释放**内存的。如果你需要一块不同大小的内存,
// 通常的做法是:
// 1. 先 `delete[]` 释放掉旧的内存(如果 `myArr` 之前指向了内存)。
// 2. 再 `new` 一块新的、所需大小的内存。
// 3. 然后把新内存的地址赋值给 `myArr`。
//
// 就像超市的例子:朋友可以把你的旧购物袋(即使里面有东西)拿走,然后给你换一个全新的、更大或更小的购物袋。
// 但他不能直接把你的旧购物袋“变大”或“变小”。
//
// 这种“重新分配”的场景,双重指针同样非常有用,因为它允许函数在完成“释放旧内存”和“分配新内存”后,
// 将新内存的地址“更新”到外部的指针变量上。
// 在这里,myArr 已经指向了新分配的内存,我们就可以像操作普通数组一样使用它了:
myArr[0] = 100;
myArr[1] = 200;
std::cout << "myArr[0] = " << myArr[0] << std::endl; // 输出 100
std::cout << "myArr[1] = " << myArr[1] << std::endl; // 输出 200
// 记得释放内存!动态分配的内存用完一定要释放,否则会造成内存泄漏!
delete[] myArr;
myArr = nullptr; // 养成好习惯,释放后将指针置为 nullptr,避免成为“野指针”
return 0;
}管理指针数组: 双重指针经常用于管理“指针的数组”,最经典的例子就是
main
函数的参数char** argv
。int main(int argc, char** argv)
:argc
(argument count):表示命令行参数的数量。argv
(argument vector):这是一个双重指针。它指向一个数组,这个数组的每个元素都是一个char*
类型的指针(即指向字符串的指针)。
- 怎么理解
char** argv
?- 想象一下,你有一张清单(
argv
)。这张清单本身存储在内存中。 - 这张清单上,每一行都写着一个地址。
- 这些地址指向的,是一个个独立的字符串(比如你运行程序时输入的命令行参数)。
- 所以,
argv
就是一个指向“存放字符串地址的清单”的指针。
- 想象一下,你有一张清单(
- 内存布局描述:
在char** argv
的内存布局中,可以想象栈区有一个argc
变量存储命令行参数数量,以及一个argv
指针变量,它存储着一个“清单”的起始地址。这个“清单”通常位于堆区或数据区,它是一个指针数组,每个元素都是一个char*
类型的指针。这些char*
指针依次指向内存中实际存储的各个命令行参数字符串(例如./myprogram
、hello
、world
)。argv
数组的最后一个元素通常是nullptr
,表示参数列表的结束。- “清单”这个数组的每个元素(
argv[0]
,argv[1]
,argv[2]
)又都是一个char*
类型的指针。 - 这些
char*
指针分别指向内存中实际存储的字符串("./myprogram"
,"hello"
,"world"
)。 - 注意:
argv
数组的最后一个元素通常是nullptr
,表示参数列表的结束。
- “清单”这个数组的每个元素(
- 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 编译后运行:./myprogram arg1 arg2
int main(int argc, char** argv) {
std::cout << "命令行参数数量: " << argc << std::endl;
std::cout << "所有命令行参数:" << std::endl;
for (int i = 0; i < argc; ++i) {
std::cout << "参数 " << i << ": " << argv[i] << std::endl;
}
// 也可以通过 while 循环遍历,利用最后一个 nullptr 结束
// char** current_arg = argv;
// while (*current_arg != nullptr) {
// std::cout << "参数 (while): " << *current_arg << std::endl;
// current_arg++;
// }
return 0;
}- 如果你运行
myprogram hello world
,输出会是:1
2
3
4
5命令行参数数量: 3
所有命令行参数:
参数 0: ./myprogram
参数 1: hello
参数 2: world - 通过这个例子和图示,你可以看到
argv
如何让你方便地访问每一个命令行参数字符串。
- 如果你运行
二、二维数组
1. 什么是二维数组?
我们学过一维数组,它就像一排整齐的格子。二维数组呢,你可以把它想象成一个表格,有行有列。
在C++中,二维数组的声明方式是 类型 数组名[行数][列数];
。
举个例子:
1 | int matrix[3][4]; // 声明一个 3 行 4 列的整型二维数组 |
2. 二维数组在内存中如何存储?
虽然我们把它想象成表格,但计算机内存是线性的,一维的。二维数组在内存中是按行连续存储的。
比如 matrix[3][4]
,它会先存储 matrix[0][0]
到 matrix[0][3]
,然后是 matrix[1][0]
到 matrix[1][3]
,最后是 matrix[2][0]
到 matrix[2][3]
。所有元素紧密排列,形成一个连续的内存块。
3. 如何访问二维数组的元素?
通过 数组名[行索引][列索引]
来访问。
1 |
|
4. 二维数组与指针的关系
这是最容易混淆的地方,但理解了双重指针,这里就容易多了!
数组名作为指针:
matrix
:代表整个二维数组的首地址,它是一个指向“包含3个整型元素的一维数组”的指针。它的类型是int (*)[3]
。matrix[0]
:代表第一行的首地址,它是一个指向int
的指针,等价于&matrix[0][0]
。matrix[1]
:代表第二行的首地址,等价于&matrix[1][0]
。
通过指针访问二维数组:
*matrix
:解引用matrix
,得到的是第一行这个一维数组。所以*matrix
等价于matrix[0]
。**matrix
:解引用*matrix
,得到的是第一行的第一个元素,即matrix[0][0]
。*(matrix[i])
:等价于matrix[i][0]
。*(*(matrix + i) + j)
:这是通过指针算术来访问matrix[i][j]
的通用方式。matrix + i
:跳过i
行,得到第i
行的首地址。*(matrix + i)
:解引用得到第i
行这个一维数组。*(matrix + i) + j
:在第i
行的基础上,跳过j
个元素,得到matrix[i][j]
的地址。*(*(matrix + i) + j)
:最终解引用得到matrix[i][j]
的值。
代码验证:
1 |
|
5. 动态分配二维数组(重点!)
固定大小的二维数组在编译时就确定了大小,不够灵活。在实际开发中,我们经常需要动态分配二维数组。这通常会用到双重指针!
方法一:使用指针数组(每行是独立的一维数组)
1 |
|
这种方法的好处是每行可以有不同的列数(不规则数组),但内存不连续。
方法二:模拟二维数组(内存连续)
这种方法更接近静态二维数组的内存布局,所有元素在内存中是连续的。
1 |
|
这种方法内存连续,访问效率更高,但每行的列数必须相同。
总结
- 双重指针是存储指针地址的指针,通过
**
访问最终数据。它在需要修改函数外部指针指向时非常有用,也是动态分配二维数组的关键。 - 二维数组可以看作是表格,在内存中是按行连续存储的。数组名本身可以看作是指针,通过指针算术可以灵活访问元素。
- 动态二维数组的分配通常需要双重指针,有两种常见方法:一种是每行独立分配,另一种是分配一块连续内存再用指针数组指向各行。
希望这篇笔记能帮助大家更好地理解双重指针和二维数组!多敲代码,多画图,很快就能掌握它们啦!