const 指针与指针 const:分清常量指针与指针常量
在C++指针编程中,const与指针的组合是高频易错点,尤其是"常量指针"(const 指针)与"指针常量"(指针 const),二者语法格式仅差const位置,含义与用法却天差地别。前文我们已掌握指针、二级指针及引用的核心逻辑,本文将聚焦这两种特殊指针,从语法规则、本质差异、内存特性、实战场景四个维度逐一拆解,帮你精准区分二者边界,规避编程中的常见错误,彻底吃透const与指针的组合用法。
一、前置回顾:const 修饰变量的核心逻辑
在讲解特殊指针前,先回顾const修饰普通变量的基础规则,为后续分析铺垫:
-
const修饰变量时,本质是将变量变为"只读常量",禁止通过该变量修改其存储的值,但变量内存地址仍可被引用。 -
语法格式:
const 数据类型 变量名或数据类型 const 变量名,二者等价(修饰普通变量时,const位置可互换)。 -
示例:
const int a = 10;与int const b = 20;均表示变量值不可修改。
关键提醒:当const与指针结合时,const的位置决定了其修饰对象(是指针指向的值,还是指针本身),这也是区分两种特殊指针的核心。
二、核心辨析:常量指针与指针常量的定义与本质
常量指针与指针常量的核心差异,在于const修饰的目标不同:常量指针修饰"指针指向的值",指针常量修饰"指针本身"。以下从语法、本质、特性三方面逐一拆解。
1. 常量指针(const 指针):指向常量的指针
(1)语法格式
cpp
// 两种等价写法,const 修饰指针指向的值
const 数据类型 *指针名;
数据类型 const *指针名;
示例:const int *p; 或 int const *p;,均为int类型的常量指针。
(2)本质与核心特性
常量指针的本质是"指针指向的目标值不可修改",但指针本身是变量,可修改其指向的地址。形象理解:指针是"可变的路标",但路标指向的"目的地内容"不可更改。
核心规则(必记):
-
禁止通过常量指针修改指向的值(只读特性),但可通过原变量修改值(若指向的是普通变量)。
-
指针本身可重新赋值,指向其他同类型变量(普通变量或常量均可)。
(3)实战示例与验证
cpp
#include <iostream>
using namespace std;
int main() {
int a = 10, b = 20;
const int *p = &a; // 常量指针p,指向变量a
// 特性1:禁止通过指针修改指向的值
// *p = 20; // 错误:常量指针指向的值不可修改
a = 20; // 合法:通过原变量修改值,指针指向的值同步更新
cout << *p << endl; // 输出20
// 特性2:指针本身可修改指向
p = &b; // 合法:指针p重新指向变量b
cout << *p << endl; // 输出20
// 特性3:可指向常量
const int c = 30;
p = &c; // 合法:常量指针可指向常量
cout << *p << endl; // 输出30
return 0;
}
2. 指针常量(指针 const):指针本身是常量
(1)语法格式
cpp
// 唯一写法,const 修饰指针本身,位置不可互换
数据类型 *const 指针名;
示例:int *const p;,为int类型的指针常量。
(2)本质与核心特性
指针常量的本质是"指针本身是常量,存储的地址不可修改",但指针指向的目标值可修改(若指向的是普通变量)。形象理解:指针是"固定的路标",始终指向同一个地址,但路标指向的"目的地内容"可更改。
核心规则(必记):
-
指针本身必须在定义时初始化,且初始化后不可重新赋值,无法修改指向的地址。
-
可通过指针修改指向的值(若指向的是普通变量,而非常量)。
(3)实战示例与验证
cpp
#include <iostream>
using namespace std;
int main() {
int a = 10, b = 20;
int *const p = &a; // 指针常量p,初始化指向变量a
// 特性1:指针本身不可修改指向
// p = &b; // 错误:指针常量地址不可修改
// 特性2:可通过指针修改指向的值(指向普通变量时)
*p = 20; // 合法:修改a的值
cout << a << endl; // 输出20
cout << *p << endl; // 输出20
// 特性3:若指向常量,不可修改值
const int c = 30;
const int *const p2 = &c; // 指向常量的指针常量
// *p2 = 40; // 错误:指向的值是常量,不可修改
return 0;
}
3. 终极区分技巧:看 const 与 * 的位置关系
面对const与指针的组合,无需死记名称,只需通过以下技巧快速判断修饰对象:
-
const 在 * 左侧 :修饰"指针指向的值",为常量指针(值不可改,指针可改)。
-
const 在 * 右侧 :修饰"指针本身",为指针常量(指针不可改,值可改,需初始化)。
-
const 在 * 两侧 :既修饰指针指向的值,又修饰指针本身(值和指针均不可改,需初始化),如
const int *const p;。
cpp
const int *p1; // const在*左 → 常量指针
int const *p2; // const在*左 → 常量指针(与p1等价)
int *const p3; // const在*右 → 指针常量(需初始化)
const int *const p4; // const在*两侧 → 值和指针均不可改(需初始化)
三、深度对比:常量指针与指针常量的核心差异
为进一步厘清边界,以下从语法格式、修饰对象、初始化要求、可修改性、使用场景五个核心维度,对二者进行全面对比,结合表格更直观呈现。
| 对比维度 | 常量指针(const 指针) | 指针常量(指针 const) |
|---|---|---|
| 语法格式 | const T* p; 或 T const* p; |
T* const p;(唯一写法) |
| 修饰对象 | 指针指向的值 | 指针本身(存储的地址) |
| 初始化要求 | 可先定义后赋值,无需初始化 | 必须在定义时初始化,否则编译报错 |
| 可修改性 | 1. 指向的值不可改(通过指针);2. 指针可重新指向其他地址 | 1. 指针地址不可改;2. 指向的值可改(指向普通变量时) |
| 指向对象 | 可指向普通变量或常量 | 优先指向普通变量(指向常量需搭配const修饰值) |
| 核心用途 | 保护指向的数据不被修改,灵活切换指向对象 | 固定指针指向,确保始终操作同一个地址的数据 |
补充:与常引用的关联对比
前文讲解的常引用(const T& ref)与常量指针有相似性,均为"保护指向/绑定的值不被修改",但二者本质不同:
-
常量指针是"指针变量",可修改指向;常引用是"变量别名",绑定关系不可改,无独立内存。
-
常量指针支持空值(
const T* p = nullptr;);常引用无空引用,必须绑定有效变量。
四、实战场景:两种特殊指针的选型逻辑
常量指针与指针常量的选型,核心取决于需求:是否需要保护数据不被修改,是否需要固定指针指向。以下是常见实战场景及选型建议,结合案例帮你落地应用。
1. 优先使用常量指针的场景
(1)函数参数传递,保护数据不被修改
当函数需接收指针参数,且仅需读取数据、无需修改时,用常量指针可防止函数内部误改外部数据,提升代码安全性。这是常量指针最常用的场景,尤其适用于传递大型对象地址(避免拷贝)。
cpp
#include <iostream>
#include <cstring>
using namespace std;
// 常量指针作为参数,保护字符串不被修改
void printString(const char *str) {
// strcpy(str, "hello"); // 错误:禁止修改指向的数据
cout << "字符串:" << str << endl;
}
int main() {
char arr[] = "world";
printString(arr); // 传递字符串地址,数据安全
return 0;
}
(2)灵活切换指向,同时限制数据修改
当需要遍历多个数据、切换指针指向,且不允许修改数据内容时,用常量指针既能满足指向灵活性,又能保护数据。
cpp
#include <iostream>
using namespace std;
int main() {
const int arr1[] = {1,2,3}, arr2[] = {4,5,6};
const int *p = arr1; // 常量指针,指向arr1
// 遍历arr1
for (int i = 0; i < 3; i++) {
cout << *(p+i) << " "; // 输出1 2 3
}
p = arr2; // 切换指向arr2,合法
for (int i = 0; i < 3; i++) {
cout << *(p+i) << " "; // 输出4 5 6
}
return 0;
}
2. 优先使用指针常量的场景
(1)固定指针指向,确保操作目标不变
当指针需始终指向同一个变量/对象,不允许修改指向,且需修改数据内容时,用指针常量固定指向,避免误改地址。
cpp
#include <iostream>
using namespace std;
int main() {
int a = 10;
int *const p = &a; // 固定指向a,不可修改
// 多次修改a的值,指针始终指向a
*p = 20;
cout << a << endl; // 输出20
*p = 30;
cout << a << endl; // 输出30
return 0;
}
(2)类成员指针,固定指向类成员
在类中,若需定义指向类成员的指针,且不允许修改指针指向(始终操作该成员),可用指针常量确保成员访问的稳定性。
cpp
#include <iostream>
using namespace std;
class Person {
public:
int age;
Person(int a) : age(a) {}
};
int main() {
Person p(20);
int Person::*const pMember = &Person::age; // 指针常量,固定指向age成员
// 修改成员值,指针指向不变
p.*pMember = 30;
cout << p.age << endl; // 输出30
Person p2(40);
cout << p2.*pMember << endl; // 仍指向age成员,输出40
return 0;
}
3. 双 const 指针场景(const T* const p)
当既需要固定指针指向,又需要保护数据不被修改时,用双const指针,适用于"只读且指向固定"的场景(如配置项地址、常量数据)。
cpp
#include <iostream>
using namespace std;
int main() {
const int a = 10;
const int *const p = &a; // 双const指针,值和指向均不可改
// *p = 20; // 错误:值不可改
// p = &b; // 错误:指向不可改
cout << *p << endl; // 仅可读取,输出10
return 0;
}
五、避坑指南:常见错误与规避方法
1. 混淆 const 修饰对象,误改不可修改内容
cpp
const int *p = &a;
*p = 20; // 错误:常量指针指向的值不可改
int *const p = &a;
p = &b; // 错误:指针常量指向不可改
规避方案:用"const与*位置"技巧快速判断修饰对象,牢记"左值右针"原则(左修饰值,右修饰针)。
2. 指针常量未初始化
cpp
int *const p; // 错误:指针常量必须初始化
int *const p = &a; // 正确:定义时绑定地址
规避方案:指针常量定义时立即初始化,绑定有效变量地址,不可先定义后赋值。
3. 常量指针指向常量后,试图通过原变量修改值
cpp
const int a = 10;
const int *p = &a;
a = 20; // 错误:a本身是常量,无论通过何种方式均不可改
规避方案:常量指针指向常量时,数据完全不可修改(既不能通过指针,也不能通过原变量),仅可读取。
4. 函数参数传递时,误将普通指针赋值给常量指针
cpp
void func(const int *p);
int *p = &a;
func(p); // 正确:普通指针可赋值给常量指针(权限缩小,安全)
void func(int *p);
const int *p = &a;
func(p); // 错误:常量指针不可赋值给普通指针(权限放大,不安全)
规避方案:指针赋值遵循"权限不放大"原则,普通指针可转为常量指针,反之则禁止。
六、总结
常量指针与指针常量的核心区别,本质是const修饰对象的不同------左修饰值,右修饰针。掌握二者的关键的是:无需死记名称,只需通过const与*的位置判断修饰目标,再结合场景需求选型:
-
需保护数据不被修改、灵活切换指向 → 用常量指针(
const T* p)。 -
需固定指针指向、允许修改数据 → 用指针常量(
T* const p),定义时必须初始化。 -
需既固定指向又保护数据 → 用双const指针(
const T* const p)。