C++初学者的学习进程(指针)
对于C++新手来说,指针无疑是入门路上的“拦路虎”。很多人刚接触时会被“*”“&”搞得晕头转向,甚至觉得指针“反人类”。但实际上,指针的核心逻辑非常简单——它就是一个“存储内存地址的变量”。今天这篇文章,我们就从最基础的概念开始,一步步拆解C++指针的基础知识,搭配大量可直接运行的代码示例,让你彻底搞懂指针的本
一、先搞懂核心:指针到底是什么?
在讲解指针之前,我们先回顾一个基础概念:变量在内存中的存储。
当我们在C++中定义一个变量(比如int a = 10;)时,计算机会在内存中开辟一块“存储空间”来存放这个变量的值(10)。而这块存储空间有一个唯一的“编号”,这个编号就是内存地址(类似我们的身份证号,唯一标识一块内存区域)。
而指针,本质上就是一个专门用来存储内存地址的变量。普通变量存储的是“数据本身”(比如a存储10),指针变量存储的是“数据所在的内存地址”(比如指针p存储a的地址)。
用一个通俗的比喻理解:
- 普通变量(a=10):相当于一个“快递盒子”,里面装的是“快递物品”(10);
- 指针变量(p=&a):相当于一个“纸条”,上面写着“快递盒子”(a)的存放地址,通过这个地址就能找到对应的盒子。
质和使用方法。
二、指针的基础语法:定义、取地址、解引用
指针的核心操作有三个:定义指针变量、获取变量的内存地址、通过指针访问变量的值。对应的语法符号分别是:*(定义指针、解引用)和&(取地址)。
2.1 语法1:取地址符 &——获取变量的内存地址
取地址符&的作用是“获取变量所在的内存地址”。语法格式:&变量名。
示例代码:
#include <iostream>
using namespace std;
int main()
{ int a = 10;
// 定义普通变量a,存储值10
cout << "变量a的值:" << a << endl;
// 输出:10
cout << "变量a的内存地址:" << &a << endl;
// 输出a的内存地址(十六进制,比如0x6ffe14) return 0; }
运行结果说明:不同电脑、不同运行次数,a的内存地址可能不一样(因为内存分配是动态的),但格式都是十六进制(以0x开头),这是内存地址的标准表示方式。
2.2 语法2:定义指针变量——存储内存地址
指针变量的定义格式:数据类型 * 指针变量名;
注意要点:
- “数据类型”:指针变量存储的是“某个变量的地址”,这个“某个变量”的数据类型就是指针的“基类型”。比如存储int变量的地址,指针类型就是int*;存储char变量的地址,指针类型就是char*。
*:表示这是一个指针变量,不能省略(否则就成了普通变量)。
示例代码(定义指向int变量的指针):
#include <iostream> using namespace std;
int main() { int a = 10;
// 普通int变量a int * p;
// 定义int*类型的指针变量p(专门存储int变量的地址) p = &a;
// 把a的内存地址赋值给指针p(p现在存储a的地址)
cout << "指针p存储的地址:" << p << endl;
// 输出:和&a一样的地址(比如0x6ffe14)
cout << "指针p本身的地址:" << &p << endl;
// 输出p自己的内存地址(指针也是变量,也占内存)
return 0;
}
关键提醒:指针变量必须“先定义,再赋值”,且赋值的地址必须和指针的基类型匹配(不能把char变量的地址赋值给int*指针,否则会编译报错或出现未知错误)。
2.3 语法3:解引用符 *——通过指针访问变量的值
我们定义指针、存储地址的最终目的,是为了通过地址找到对应的变量,进而操作它的值。这时候就需要用到“解引用符”*(和定义指针时的*是同一个符号,但作用不同)。
解引用的语法:*指针变量名,表示“通过指针变量存储的地址,找到对应的变量”,等价于“指针指向的变量”。
示例代码:
#include <iostream>
using namespace std;
int main() { int a = 10; int * p = &a;
// 定义指针p并直接赋值(推荐写法,简洁)
// 通过指针访问变量a的值
cout << "通过指针p访问a的值:" << *p << endl;
// 输出:10(等价于cout << a << endl;)
// 通过指针修改变量a的值 *p = 20;
// 等价于 a = 20;
cout << "修改后a的值:" << a << endl;
// 输出:20
return 0; }
这里一定要区分清楚*的两个作用:
- 定义指针时:
int * p;中的*表示“p是指针变量”; - 使用指针时:
*p = 20;中的*表示“解引用”,即“通过p的地址找到对应的变量”。
三、指针的重要特性:类型匹配
前面我们提到过,指针的“基类型”必须和它指向的变量类型一致,否则会出现错误。这是新手最容易踩的坑之一,我们用代码示例说明:
#include <iostream>
using namespace std;
int main()
{ int a = 10; char b = 'A'; int * p1 = &a;
// 正确:int*指针指向int变量
// int * p2 = &b;
// 错误:int*指针不能指向char变量(类型不匹配)
char * p3 = &b;
// 正确:char*指针指向char变量
return 0; }
为什么必须类型匹配?因为不同类型的变量占用的内存空间大小不同(比如int占4字节,char占1字节),指针解引用时需要知道“读取多少字节的内存”——int*指针解引用时读取4字节,char*指针读取1字节。如果类型不匹配,就会读取错误的内存数据,导致程序异常。
四、空指针与野指针:新手必须避开的坑
指针变量如果未被正确赋值,就可能成为“野指针”,这是程序崩溃的常见原因之一。另外,还有一种“空指针”,是C++中专门用来表示“指针未指向任何有效内存”的特殊值。
4.1 野指针(危险!)
野指针:指针变量未被初始化,或者指向的内存地址已经被释放(无效地址)。访问野指针会导致程序崩溃(段错误)。
错误示例(野指针):
#include <iostream>
using namespace std;
int main()
{ int * p;
// 未初始化的指针,存储的是随机的垃圾地址(野指针)
// *p = 10;
// 错误:访问野指针,程序会崩溃
return 0;
}
规避方法:定义指针时,要么直接赋值(指向有效变量),要么先赋值为nullptr(空指针)。
4.2 空指针(安全的“无指向”状态)
空指针是C++11引入的关键字nullptr,它表示“指针未指向任何有效内存地址”。空指针的核心作用是“避免野指针”——当我们暂时不知道指针该指向哪里时,就把它初始化为nullptr。
注意:不能解引用空指针(因为它指向无效内存),所以使用前需要判断指针是否为空。
正确示例(空指针的使用):
#include <iostream>
using namespace std;
int main()
{ int * p = nullptr;
/ 初始化指针为nullptr(空指针)
int a = 10;
// 使用指针前先判断是否为空
if (p == nullptr)
{ p = &a; // 为空时,赋值为有效地址 } *p = 20;
// 现在可以安全使用了
cout << a << endl;
// 输出:20 return 0; }
补充:C++98中曾用NULL表示空指针(本质是宏定义,值为0),但NULL可能会和整数0混淆,因此C++11推荐使用nullptr(专门表示空指针,类型更明确)。
五、指针与数组:天生一对
在C++中,数组名和指针有着非常紧密的联系——数组名本质上是一个“指向数组首元素的常量指针”(不能修改数组名的值,即不能让数组名指向其他地址)。
用代码理解:
#include <iostream>
using namespace std;
int main() { int arr[5] = {1, 2, 3, 4, 5};
// 定义数组arr
// 数组名arr是指向首元素arr[0]的常量指针
cout << "数组名arr的值(首元素地址):" << arr << endl;
cout << "arr[0]的地址:" << &arr[0] << endl;
// 和arr的值完全相同
// 通过指针访问数组元素(两种等价写法)
int * p = arr;
// 指针p指向数组首元素(无需&,因为arr本身就是地址)
cout << "p指向的元素(arr[0]):" << *p << endl;
// 输出:1 cout << "p+1指向的元素(arr[1]):" << *(p+1) << endl;
// 输出:2 cout << "p+2指向的元素(arr[2]):" << *(p+2) << endl; // 输出:3 // 遍历数组(指针方式)
for (int i = 0; i < 5; i++)
{ cout << *(p + i) << " "; // 等价于 arr[i] 或 p[i] }
return 0;
}
核心结论:
arr[i]等价于*(arr + i)(数组名是常量指针);p[i]等价于*(p + i)(指针变量可以像数组一样使用);- 指针
p+1表示“指向当前元素的下一个元素”,偏移的字节数由指针类型决定(int*指针偏移4字节,char*指针偏移1字节)。
六、指针的基础应用场景:函数传参(地址传递)
我们知道,C++函数的参数传递默认是“值传递”——函数内部修改参数值,不会影响函数外部的变量(因为函数会拷贝一份参数的副本)。但如果我们想通过函数修改外部变量的值,就可以使用“地址传递”(用指针作为参数)。
示例代码(用指针修改外部变量):
#include <iostream>
using namespace std;
// 函数参数是int*指针,接收外部变量的地址
void changeValue(int * p)
{ *p = 100;
// 解引用指针,修改外部变量的值
}
int main()
{ int a = 10;
cout << "修改前a的值:" << a << endl;
// 输出:10
// 传递a的地址给函数(用&a)
changeValue(&a);
cout << "修改后a的值:" << a << endl;
// 输出:100(外部变量被修改了)
return 0; }
原理:函数接收的是外部变量的地址(指针p存储a的地址),通过解引用*p,可以直接操作a所在的内存空间,因此修改的是外部变量本身,而不是副本。这是指针最常用的场景之一。
七、新手必记的指针基础要点
- 指针是“存储内存地址的变量”,核心是“地址”,不是“值”;
- 定义指针时必须指定基类型(int*、char*等),且基类型要和指向的变量类型匹配;
- &是“取地址符”,
*是“定义指针”或“解引用”,注意区分两个作用; - 指针必须初始化(要么指向有效变量,要么赋值为
nullptr),避免野指针; - 数组名是“指向首元素的常量指针”,可以直接赋值给同类型指针;
- 通过指针传参,可以实现函数对外部变量的修改(地址传递)。