指针
指针是 C++ 中非常重要的一种数据类型,它专门用来存储其它变量的内存地址,通过指针,我们可以直接访问和操作存储在内存中的数据。
在讨论指针前,我们先来看看 C++ 中变量的内存地址这个概念。
内存地址
在 C++ 中,所有的变量、数据、甚至代码都是存储在内存中的,在这里我们不讨论具体的内存模型是怎么样的(想要深入了解可以自行查阅资料,我在后面也会给出一些参考资源),我们现在主要关心的是变量所在的内存地址。
假设我们有一个变量 a
,在变量的前面加上取地址符号 &
,&a
会得到这个变量所在的内存地址。
#include <iostream>using namespace std;
int main() { int a = 1, b = 2, c = 3;
cout << "a的地址:" << &a << endl; cout << "b的地址:" << &b << endl; cout << "c的地址:" << &c << endl;
return 0;}
运行该程序,输出结果为:
a的地址:0x7fff282891bcb的地址:0x7fff282891c0c的地址:0x7fff282891c4
你运行的结果应该大致看起来和我差不多,但具体的地址肯定不同,这是当程序在创建变量时由系统自行分配的。
另外,你每次运行程序时,都会得到不同的地址,这是因为每次运行程序都会重新创建变量,因此分配的内存地址也应该也是不同的。
这里的地址 0x7fff282891bc
看起来有点奇怪,有数字又有字母,实际上,前缀0x
代表十六进制,后面的一串数字则是由十六进制表示的实际内存地址。
细心的同学还会发现,这里三个变量的地址刚好差 4,实际上是指 4 个字节(byte),因为 int
变量刚好是占 4 个字节。
在 C++ 中,符号
&
根据上下文的不同有多种含义。
- 作为取地址运算符,例如:&a,表示取变量 a 的地址
- 作为按位与运算符,例如: a & b,表示对变量 a 和 b 进行与运算
- 用于声明引用的分隔符,例如:int& ref,表示声明了一个引用,在函数参数中用得较多
数组与地址
数组与指针是两种完全不同的对象,但又有着密不可分的关系,接下来我们来看看它们之间有哪些共同和不同的地方。
一个数组的名字,实际上就是一个指针,该指针指向该数组存放的起始地址,也就是数组第一个元素所在的地址,这一点使得数组与指针之间产生了密切的联系。
例如,有如下代码:
int q[5] = {1, 2, 3, 4, 5};cout << q << endl;cout << &q[0] << endl;
q
与 &q[0]
的输出结果是完全相同,因为他们代表的是同一个元素的地址。
【TODO】
- 地址的连续性
- 二维数组越界访问如何处理
指针变量
在 C++ 中,指针是一种特殊的变量,它是专门用来存储变量的内存地址的。直接看例子容易明白。
#include <iostream>using namespace std;
int main() { int a = 5;
// 定义指针 int* pointer_a;
// 为指针赋值 pointer_a = &a;
cout << "a: " << a << endl; cout << "&a: " << &a << endl; cout << "pointer_a: " << pointer_a << endl;
return 0;}
这里定义了一个指针变量 pointer_a
,该变量存储了变量 a
的地址。程序的运行结果如下:
a: 5&a: 0x7ffc0e3f045cpointer_a: 0x7ffc0e3f045c
可以看到,这里指针 pointer_a
的值与变量 a
的地址是完全一致的。
在定义指针变量时,除了正常的变量名称,需要在前面加上 *
表明这是一个指针变量,*
的位置可以加在变量名前,也可以紧跟在变量类型后面。
int *pointer_a; // 位置1: 加在变量名前int* pointer_a; // 位置2: 紧跟在变量类型后
推荐使用位置2,紧跟在变量类型后,这样就不会误解,以为 *pointer_a
这个整体是变量名,指针变量名是不包含 *
的。
实际上,point_a
和 *pointer_a
代表的完全不同的意思,在指针变量名前加星号,如 *pointer_a
,是直接获取该指针中存储的地址所指向的值。
// 通过指针访问变量cout << "pointer_a 指向的值: " << *pointer_a << endl;
下面的示意图显示了普通变量 a
,a 的地址 &a
,以及指针变量 pointer_a
这几者之间的关系。
更改指针变量所指向的值
既然指针变量存储的是变量的地址,我们就可以通过这个地址去修改变量的值。
下面是一个简单的例子:
#include <iostream>using namespace std;
int main() { int a = 5; int* pointer_a;
pointer_a = &a;
// 更改指针变量所指向的值 *pointer_a = 8;
cout << "a: " << a << endl; cout << "&a: " << &a << endl; cout << "pointer_a: " << pointer_a << endl; cout << "pointer_a 指向的值: " << *pointer_a << endl;
return 0;}
这里我们添加了一行,通过指针去修改它所指向的值,可以发现,原始的变量 a
的值也被修改了。
需要注意的是,指针变量也有大小,它是指针本身在内存中占用的空间,而不是它所指向的数据的大小,别弄混淆了。指针的大小是由操作系统和编译器决定的,通常在 32 位系统上是 4 个字节,在 64 位系统上是 8 字节。
指针与函数
在前面讲解函数的章节中,我们已经看到函数的参数传递分为值传递和引用传递两种方式。
引用传递实际上就是将实参变量的地址传递给函数,当时讲到一个交换两个变量的函数 swap
,大家还记得吗?
void swap(int &a, int &b) { int temp = a; a = b; b = temp;}swap(a, b); // 调用函数
在上面的代码中,我们通过引用传递实现了两个变量的交换。
既然指针又是存储内存地址的变量,我们也可以通过传递指针变量的方式来实现同样的功能。
void swap(int* a, int* b) { int t = *a; *a = *b; *b = t;}swap(&a, &b); // 调用函数
两者实现的效果一样,不同的是函数参数的定义和调用函数的方式有所区别,通过这两种不同的写法,可以更好地帮助理解指针与内存地址之间的关系。
指针的运算
如果指针 指向数组元素 ,那么
- 表达式 为指向元素 向后 个位置的元素的指针
- 表达式 为指向元素 向前 个位置的元素的指针
例如下面的代码:
int q[5] = {1, 2, 3, 4, 5};int* p = &q[2];cout << *p << endl; // 输出 3p = p + 2;cout << *p << endl; // 输出 5p = p - 1;cout << *p << endl; // 输出 4
前面有讲到,数组名本身就是数组所在的内存地址,所以我们可以使用 数组名 + i
来遍历数组中的元素。如下面的代码所示:
int q[5] = {1, 2, 3, 4, 5};for (int i = 0; i < 5; i++) { cout << *(q + i) << endl; // 利用指针运算来遍历数组元素}
动态分配内存
也可以使用指针来动态分配内存,例如:
// 动态分配了一个 int 类型的内存空间int* p = new int;
// 将值 10 存储到刚刚分配的内存空间中*p = 10;
这里的 new
运算符用于动态分配指定类型的内存空间,将分配的内存空间地址存储在指针变量 p
中,然后使用 *p
来设定该地址所指向的值。
需要注意的是,在使用完动态分配的内在后,需要使用 delete
运算符去释放对应的内存空间。
// 释放动态分配的内存空间delete p;
动态创建数组对象
上面是单个对象动态创建,同样的方式也可以用于动态创建数组,下面是一个动态创建数组的完整示例:
#include <iostream>using namespace std;
int main() { int size; cin >> size;
// 动态创建含有 size 个元素的数组对象 int* p = new int[size];
// 继续读入元素到数组 for (int i = 0; i < size; i++) { cin >> p[i]; }
// 测试:遍历输出数组每一个元素 for (int i = 0; i < size; i++) { cout << p[i] << " "; }
// 释放动态分配的内存空间 delete[] p;
return 0;}
可以发现,使用 new
运算符动态创建数组,数组元素的个数可以不用是常量(这里是通过外部读入的),也就是说数组元素的个数是在运行时确定的,相比普通的数组创建来说有更高的灵活度。
另外,销毁数组是用 delete[]
运算符,别忘了 []
符号。
使用指针的一些常见错误
指针可以直接操作内存地址,这是 C++ 强大的功能之一,但也正是因为它的强大与灵活,一旦没使用好,很容易造成程序崩溃、内存泄漏或逻辑错误等问题。在使用时要特别小心。
这里介绍几种使用指针时的常见错误用法。
错误 1
// 访问未初始化或无效的指针int a, *p;a = 5;*p = a + 5;cout << a << endl;
修改后:
int a, *p;a = 5;p = &a;*p = a + 5;cout << a << endl;
错误 2
// 同一行声明多个指针int* p1, p2;int a = 5;int b = 3;p1 = &a;p2 = &b; // 出错
运行代码,会遇到 int*
到 int
的类型转换错误。
当我们想在一行定义两个同类型的指针,初学者容易这样写,这是一个常见的声明指针的错误。
修改后:
int *p1, *p2; // 通过分开定义来修正错误int a = 5;int b = 3;p1 = &a;p2 = &b;
错误 3
int a = 5;int* p = &a;*p++; // 出错
cout << *p << endl;
写这段代码的本意是希望将 p 指针指向的值增加 1,但运行该程序得到的结果可能是一个很大或很小的,错误的整数。
修改后:
int a = 5;int* p = &a;(*p)++; // 修改的语句
cout << *p << endl;
小结
C++ 中的指针是一种强大的工具,它们提供了直接访问和操作内存的能力,使用得当,可以极大的简化一些编程任务的执行,但如果未使用好,容易造成一些不易调试的内存访问问题。因引在使用指针时需要谨慎,多积累一些内存管理的技巧,养成良好的编码实践习惯。