在学习C/C++的过程中,指针常常让初学者感到困惑。其实,指针并没有那么复杂,理解了它的基本原理和使用方法之后,你会发现它不过是一个存储地址的变量而已。
一、变量的本质
实际上,所有的变量都是一个地址,并不是小白理解的int a
是一个变量而int* a
是一个地址。实际上所有的变量都是一个地址。
变量的作用是为了指向一块存储空间,修改和读取内存中的数据。而指向存储空间需要的是一段真实内存地址
,而操作系统为了避免真实内存冲突,所以每个进程必须使用虚拟内存地址
,由硬件内存管理单元和操作系统共同决定虚拟地址和真实地址的映射。
因此,所有的变量实际上都是一个虚拟内存地址。而int a
这个变量的虚拟内存地址指向的是一个可以存储一个int格式数据的内存空间,而int* a
这个变量的虚拟内存地址指向的是一个虚拟内存地址,这个虚拟内存地址指向的是一个可以存储一个int格式数据的内存空间。
你会发现上面这段话,有两段一模一样的描述。如果你看懂了上面这段话,就可以理解int* a
储存的其实是一个int类型变量的原型。实际上int** a
储存的就是一个int*型变量的原型。
你始终要知道的是,所有变量都是一个虚拟内存地址,区别就是指向的内存空间作用不同而已,int
和int*
没有任何本质上的不同。
二、指针的基本原理
1. 什么是指针?
在C/C++中,指针是一种变量,它存储的是另一个变量的地址。简单来说,普通变量存储的是数据本身,而指针存储的是数据所在的内存地址。
2. 指针的定义和使用
指针变量的定义需要指定指针所指向的数据类型,语法如下:
数据类型* 指针变量名;
例如:
int a = 10; // 定义一个整型变量
int* p = &a; // 定义一个指向整型的指针变量,并将a的地址赋值给它
在上面的代码中:
-
&a
表示取变量a
的地址。 -
p
是一个指针变量,它存储了变量a
的地址。
你可以通过*p
来访问指针指向的变量的值:
cout << *p << endl; // 输出10,表示访问指针p指向的变量a的值
3. 指针的本质
指针本质上是一个普通变量,只不过它存储的是一个地址。理解了这一点,就不难理解为什么指针可以和地址、内存操作联系起来。
例如:
cout << p << endl; // 输出p存储的地址(a的地址)
cout << *p << endl; // 输出p指向地址中存储的值(a的值)
4. 解引用运算符和取址符
int a = 10;
int* p = &a;
cout << *p << endl;
*
是一个运算符,可以读取p这个变量储存的地址指向的值。
在非形参的定义中,&
是取地址符上面的&a
实际上就是拿到变量a的原型,文章开头说过所有的变量其实都是一个地址(注意a
虽然原型是个地址但a
代表的地址是不可修改的,它被赋值给了一个指针p
,而指针储存的地址是可以修改的,他是指针类型变量的值,修改了这个值并不会影响变量a
)。
二、指针的使用场景
指针不仅是C/C++的核心,也是程序设计中非常重要的一部分。以下是指针的常见使用场景:
1. 动态内存分配
在C++中,可以使用指针动态分配内存:
int* p = new int(42); // 动态分配一个整数并初始化为42
cout << *p << endl; // 输出42
delete p; // 释放内存
动态分配内存时,必须用delete
释放,否则可能会导致内存泄漏。
2. 通过指针修改变量的值
指针可以用来间接修改变量的值:
void modify(int* p) {
*p = 20; // 修改指针指向的变量的值
}
int main() {
int a = 10;
modify(&a); // 将a的地址传递给函数
cout << a << endl; // 输出20
return 0;
}
3. 指针与函数
指针可以用来实现函数参数的传址调用:
void swap(int* x, int* y) {
int temp = *x;
*x = *y;
*y = temp;
}
int main() {
int a = 5, b = 10;
swap(&a, &b); // 通过指针交换a和b的值
cout << a << " " << b << endl; // 输出10 5
return 0;
}
三、C++中的引用
1. 什么是引用?
引用是C++中一种更高级的功能,可以看作是指针的“语法糖”。引用本质上是一个变量的别名,它提供了一种更安全、更简洁的方式来操作变量。
引用的定义语法:
数据类型& 引用名 = 变量名;
例如:
int a = 10;
int& ref = a; // 定义一个引用ref,作为变量a的别名
此时,ref
和a
是同一个实体,修改ref
的值会直接影响a
:
ref = 20; // 修改ref的值
cout << a << endl; // 输出20
2. 引用的使用场景
- 作为函数参数:引用可以实现函数参数的传址调用,同时避免使用指针的繁琐语法。
void modify(int& ref) {
ref = 30; // 修改引用ref的值
}
int main() {
int a = 10;
modify(a); // 直接将变量传递给引用参数
cout << a << endl; // 输出30
return 0;
}
- 作为函数返回值:引用可以用来返回一个变量的引用,从而直接操作原变量。
int& getValue(int& ref) {
return ref;
}
int main() {
int a = 10;
int& b = getValue(a);
b = 40; // 修改b的值,等同于修改a
cout << a << endl; // 输出40
return 0;
}
四、数组与指针的关系
1. 数组的内存布局
数组在内存中是连续存储的,数组名本质上是一个指向数组第一个元素的"指针"。很多教程会把数组和指针划上关系,实际上我们前文已经论证过,实际上数组也就是一种变量类型,所有的变量名实际上都是指向第一个元素的"指针",只不过不是数组的话,它本身就只有一个元素而已。
int arr[3] = {1, 2, 3};
cout << arr << endl; // 输出数组首元素的地址
cout << &arr[0] << endl; // 同样输出数组首元素的地址
2. 用指针遍历数组
指针可以用来遍历数组。很多教程会把下面的代码来表示数组和指针的关系,实际上那是因为数组申请的时候,虚拟内存地址是连续的,所以可以通过运算符来指向下一个单元。如果这不是一个数组,你一样可以修改p
中存储的地址,只不过那样就脱离和原变量的关系甚至造成程序异常。
int arr[3] = {1, 2, 3};
int* p = arr; // 指针指向数组首元素
for (int i = 0; i < 3; i++) {
cout << *(p + i) << " "; // 使用指针访问数组元素
}
3. 指针与数组名的区别
虽然数组名可以看作指针,但它与普通指针有一些区别:
- 数组名是常量指针,不能修改。这一点我们之前也提过,变量本身所代表的地址,是不能修改的
- 普通指针可以动态指向其他地址,而数组名始终指向数组的起始地址。
例如:
int arr[3] = {1, 2, 3};
int* p = arr; // 正确
p++; // 正确,可以移动指针
arr++; // 错误,数组名是常量,不能修改
4. 二维数组与指针
二维数组的每一行可以看作是一个一维数组,指针可以用来访问二维数组:
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*p)[3] = arr; // 定义一个指向含有3个元素的数组的指针
cout << p[0][1] << endl; // 输出2
cout << p[1][2] << endl; // 输出6
这里int (*p)[3]
实际上可以重新赋值其它数组,比如再声明一个int arr2[9][3]
一样可以赋值给p
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*p)[3] = arr; // 定义一个指向含有3个元素的数组的指针
cout << p[0][1] << endl; // 输出2
cout << p[1][2] << endl; // 输出6
// 定义一个新数组
int arr2[9][3] = {
{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}
};
// 让 p 指向 arr2
p = arr2;
// 验证 p 是否正确指向 arr2
cout << p[0][1] << endl; // 输出11
cout << p[8][2] << endl; // 输出36
五、总结
指针是C/C++中非常强大的工具,理解了它的本质——存储地址的变量,就能更好地掌握它的用法。C++中的引用作为指针的更高层次封装,提供了更简洁的语法和更高的安全性。
关键点总结:
- 任何变量名实际上都代表一个虚拟地址,无论是整数,指针,数组,复合结构,本质上都是一样的。
- 指针是存储地址的变量,
*
和&
是操作指针的关键。 - C++中的引用是指针的语法糖,更易用且更安全。
- 指针和数组可以结合使用进行灵活操作。