C语言指针常量与常量指针详解:语法、内存与应用场景 📅 发布时间:2026/9/1 22:11:05 👁 浏览次数: 这次我们来看一个C语言里绕不开的经典问题指针常量和常量指针。这俩概念名字相似但含义和用法天差地别是很多初学者甚至有一定经验的开发者容易混淆的“拦路虎”。搞不清楚轻则编译报错重则程序行为诡异内存访问出错。这篇文章不跟你绕弯子直接说重点指针常量Pointer to Constant和常量指针Constant Pointer的核心区别在于“谁不能变”。一个是“指针指向的值不能变”另一个是“指针本身存储的地址不能变”。理解了这个再复杂的声明也能拆解。本文会带你从语法、内存、使用场景三个维度彻底搞懂它们并通过大量代码示例和对比表格让你不仅能区分更能正确、安全地在项目中使用。无论你是正在准备考试、面试还是想在嵌入式、系统编程中写出更健壮的代码这篇文章都值得你仔细阅读并动手验证。1. 核心概念速览谁不能变在深入细节前我们先通过一个表格快速抓住两者的本质区别这是后续所有讨论的基础。特性指针常量 (Pointer to Constant)常量指针 (Constant Pointer)核心约束指向的值是常量不能通过该指针修改。指针本身是常量其存储的地址初始化后不能改变。声明语法const int *p;或int const *p;int * const p;“*”的位置const在*左边或类型左边。const在*右边。可变部分指针变量p本身可以指向别的地址。指针p指向的值*p可以被修改。初始化要求声明时可暂不初始化后续再赋值。必须在声明时初始化因为之后不能再改地址。类比理解一个“只读”视角的窗口。你可以换一扇窗看别处但透过这扇窗看到的东西不能动。一个“固定”的窗口。这扇窗焊死了不能移动但你可以改变窗外展示的东西。常见用途函数参数防止函数内部意外修改传入的数据。指向固定硬件寄存器、共享内存区或需要固定绑定某个对象的场景。一句话口诀左定值右定址。看const在*的左边还是右边。左边修饰值右边修饰指针本身。2. 指针常量指向常量的指针指针常量更准确的叫法是“指向常量的指针”。它的重点是你不能通过这个指针去修改它指向的那个内存单元的值。但指针变量本身存储的地址是可以改变的即它可以指向另一个地方。2.1 语法与声明有两种等价的声明方式const的位置不同但含义完全相同const int *p; // 方式一const 修饰 int int const *p; // 方式二const 修饰 *p 推荐更清晰对于编译器而言int const *p和const int *p是完全一样的。但int const *p的读法更符合“从右向左”的规则p是一个指针指向一个const int整型常量。我个人更推荐第二种写法因为它直接体现了“指向常量的指针”这一本质。2.2 代码示例与行为分析#include stdio.h int main() { int a 10; int b 20; // 声明一个指向常量的指针 p并让它指向 a int const *p a; // 1. 读取值完全没问题 printf(*p %d\n, *p); // 输出: *p 10 // 2. 试图通过 p 修改 a 的值编译错误 // *p 30; // 错误assignment of read-only location ‘*p’ // 3. 但是直接修改 a 本身是允许的 a 30; printf(a %d, *p %d\n, a, *p); // 输出: a 30, *p 30 // 注意p 指向 aa 变了*p 读到的值自然也变了。 // 约束是“不能通过 p 改”而不是“a 必须是常量”。 // 4. 让 p 指向另一个变量 b完全允许 p b; printf(*p %d (now points to b)\n, *p); // 输出: *p 20 // 5. 同样不能通过 p 修改 b // *p 40; // 错误 b 40; // 直接改 b 可以 printf(b %d, *p %d\n, b, *p); // 输出: b 40, *p 40 return 0; }关键点分析保护性int const *p提供了一种保护机制。当你把一个变量的地址交给这样一个指针时相当于承诺“我不会通过这个指针去改你的数据”。这常用于函数传参防止函数内部误操作。灵活性指针本身是变量可以重新指向其他符合类型要求的地址。底层非const它指向的目标如a,b本身可以是非常量。指针只是提供了一个“只读视图”。2.3 典型应用场景函数参数这是指针常量最常用、最重要的场景。用于传递“只读”数据提高代码的安全性和可读性。#include stdio.h #include string.h // 不良设计参数是普通指针函数内部可能意外修改 src void bad_print(const char *str) { // 虽然我们没改但函数签名没有约束调用者会担心。 printf(%s\n, str); } // 良好设计参数是指向常量字符的指针明确告知“我不会修改你的字符串” void good_print(const char *str) { // str[0] A; // 如果尝试修改编译器会直接报错防止bug。 printf(%s\n, str); } // 计算字符串长度标准库strlen的原型 size_t my_strlen(const char *str) { size_t len 0; while (str[len] ! \0) { // 只读访问安全 len; } return len; } int main() { char msg[] Hello, World; good_print(msg); printf(Length: %zu\n, my_strlen(msg)); return 0; }为什么这很重要安全性防止函数内部的代码意外修改调用者的数据这类 Bug 很难追踪。可读性函数原型void func(const T *ptr)清晰地表达了设计意图“这是一个输入参数我不会改它”。兼容性它可以接受常量数据如字符串字面量和非常量数据的地址而反过来非常量指针指向常量数据是不允许的。const char *literal immutable; // 正确指向常量字符串的指针 // char *p immutable; // 危险在C中错误在C中不推荐可能引发未定义行为3. 常量指针指针本身是常量常量指针意思是指针变量本身是常量。一旦它被初始化指向某个地址这个地址值就不能再改变了。但是通过这个指针去修改它所指向的内存的值是允许的前提是那个内存区域本身可写。3.1 语法与声明声明时const必须放在*的右边并且必须在声明的同时进行初始化。int * const p some_variable; // 正确声明并初始化 // int * const p; // 错误未初始化的常量 ‘p’3.2 代码示例与行为分析#include stdio.h int main() { int a 10; int b 20; // 声明一个常量指针 p并让它指向 a。必须初始化 int * const p a; // 1. 通过 p 读取和修改 a 的值允许 printf(*p %d\n, *p); // 输出: 10 *p 30; // 正确修改了 a 的值 printf(a %d\n, a); // 输出: a 30 // 2. 试图让 p 指向 b编译错误 // p b; // 错误assignment of read-only variable ‘p’ // 3. 直接修改 a 当然也可以 a 40; printf(*p %d\n, *p); // 输出: 40 // 4. 如果指向的目标是 const 呢 const int c 100; // int * const p2 c; // 错误初始化丢弃了‘const’限定符 // 因为 p2 是一个“指向int的常量指针”它承诺可以通过自己修改目标。 // 但 c 是一个 const int不允许修改。类型不兼容危险 const int * const p3 c; // 正确指向常量的常量指针双重const // *p3 200; // 错误不能通过 p3 改值 // p3 a; // 错误也不能改 p3 本身的地址 return 0; }关键点分析固定绑定常量指针像一个“固定挂钩”一旦挂上某个对象就不能再换对象了。这常用于指向硬件寄存器、内存映射的I/O地址或者需要在整个生命周期内与某个特定对象关联的场景。必须初始化因为之后无法赋值所以声明时就必须给出初始值。可修改目标只要目标对象本身不是const就可以通过这个指针修改它。3.3 典型应用场景固定资源访问// 假设有一个固定的硬件状态寄存器地址 #define STATUS_REG ((volatile unsigned int *)0x40021000) void hardware_init() { // 定义一个常量指针固定指向这个硬件地址 volatile unsigned int * const status_ptr STATUS_REG; // 在整个函数或模块生命周期中status_ptr 永远指向 STATUS_REG // 我们可以通过它读写寄存器 unsigned int current_status *status_ptr; // 读 *status_ptr current_status | 0x01; // 写设置某一位 // status_ptr (volatile unsigned int *)0x40022000; // 错误指针地址不可变。 }在嵌入式开发中这种用法非常普遍确保了指向关键硬件资源的指针不会被意外修改。4. 双重const指向常量的常量指针把前两种约束结合起来就得到了“指向常量的常量指针”。它既不能修改指向的值也不能修改自身的地址。这是最严格的约束。4.1 语法与声明const同时出现在*的两边。const int * const p some_constant_variable; // 推荐 int const * const p some_constant_variable; // 等价同样推荐读法p是一个常量指针指向一个整型常量。4.2 代码示例与行为分析#include stdio.h int main() { const int immutable_value 42; int normal_value 100; // 指向常量的常量指针通常用它来指向真正的常量 const int * const p1 immutable_value; // 以下操作全部非法 // *p1 50; // 错误不能修改指向的值 // p1 normal_value; // 错误不能修改指针本身 // 它可以指向非常量吗语法上可以但逻辑上要小心。 const int * const p2 normal_value; // 合法 // *p2 200; // 错误通过 p2 不能修改 normal_value // 但是 normal_value 本身可以通过其他方式修改 normal_value 200; printf(normal_value %d, *p2 %d\n, normal_value, *p2); // 输出: 200, 200 // p2 提供了一个对 normal_value 的只读、固定视图。 return 0; }应用场景通常用于定义模块内部的全局只读数据访问句柄或者作为函数参数传递一个既不允许修改内容也不允许改变指向的完整“只读输入对象”。5. 深入辨析从声明理解与“从右向左”规则面对复杂的指针声明比如const int * const * pp如何快速理解使用“从右向左”的阅读规则是最有效的方法。规则从变量名开始向右看遇到()或[]先解析它们是右结合然后向左看依次解释每个符号。让我们拆解几个例子int const *p;从p开始。向右没有()或[]。向左遇到*所以p是一个指针。继续向左遇到const所以这个指针指向的是常量。继续向左遇到int这个常量是int类型。结论pis a pointer to a constant integer. (指针常量)int * const p;从p开始。向右没有。向左遇到const所以p本身是常量。继续向左遇到*所以这个常量是一个指针。继续向左遇到int这个指针指向int。结论pis a constant pointer to an integer. (常量指针)const int * const p;从p开始。向右没有。向左遇到const所以p是常量。继续向左遇到*所以这个常量是一个指针。继续向左遇到int指针指向int。继续向左遇到const这个int是常量。结论pis a constant pointer to a constant integer. (指向常量的常量指针)更复杂的const int * const * pp;从pp开始。向右没有。向左遇到*所以pp是一个指针。继续向左遇到const所以这个指针指向的是常量。继续向左遇到*所以这个常量本身又是一个指针。继续向左遇到int这第二个指针指向int。继续向左遇到const这第二个指针指向的int是常量。结论ppis a pointer to a constant pointer to a constant integer.pp本身可以指向别的const int * const类型的指针。*pp是一个常量指针你不能修改*pp的值即它存储的地址。**pp是一个整型常量你不能修改**pp的值。多练习几次“从右向左”读法再复杂的声明也能迎刃而解。6. 常见混淆点与陷阱排查在实际编码和面试中以下几个点是高频出错区。6.1 陷阱一字符串字面量与指针char *str Hello; // 在C中合法但不安全在C中错误 str[0] h; // 未定义行为试图修改只读存储区的字符串字面量。字符串字面量如Hello通常存储在程序的只读数据段如.rodata。用一个非常量指针指向它然后试图修改会导致运行时错误如段错误。正确做法始终使用const char*来指向字符串字面量。const char *str Hello; // 安全明确表示只读 printf(%c\n, str[0]); // 可以读 // str[0] h; // 编译错误保护了程序6.2 陷阱二const 在函数返回值中的意义const int *get_value(void);这个函数返回一个指向常量int的指针。调用者不能通过返回的指针修改目标数据。这常用于返回指向内部静态数据或常量数据的指针防止调用者修改。int * const get_value(void); // 不常见这个函数返回一个指向int的常量指针。返回值本身指针是常量不能指向别处但可以通过它修改目标数据。这种用法较少。6.3 陷阱三类型转换与 const 丢弃将const T*强制转换为T*去掉 const 限定是极其危险的操作违反了语言的约定可能导致未定义行为。const int a 10; const int *p a; int *q (int *)p; // 强制类型转换去掉了const *q 20; // 未定义行为试图修改一个const对象。 printf(a %d\n, a); // 输出可能是10也可能是20取决于编译器优化。最佳实践除非你百分之百确定底层对象本身是非 const 的例如你正在实现一个像strchr这样的标准库函数它接收const char*但返回char*否则绝对不要去掉 const 限定符。6.4 陷阱四与数组名的混淆数组名在大多数表达式中会退化为指向其首元素的指针。但这个指针是“常量指针”。int arr[5] {1,2,3,4,5}; // arr 的类型可以看作是 int * const // arr some_other_int; // 错误不能给数组名赋值。 // arr // 错误 int *p arr; // 正确p 是一个普通的指针变量可以指向别处 p; // 正确p 现在指向 arr[1]7. 综合测试与代码验证最好的学习方式是动手。下面提供一个综合测试程序你可以复制到本地编译运行并尝试取消注释那些错误的行观察编译器的报错信息。#include stdio.h void test_pointer_to_constant() { printf( 测试1指针常量 (Pointer to Constant) \n); int x 5, y 10; const int *p x; // 或 int const *p printf(初始: x%d, *p%d\n, x, *p); // *p 6; // 取消注释将导致编译错误read-only location x 6; // 直接修改 x 是允许的 printf(修改x后: x%d, *p%d\n, x, *p); p y; // 指针本身可以指向别处 printf(指向y后: y%d, *p%d\n, y, *p); printf(\n); } void test_constant_pointer() { printf( 测试2常量指针 (Constant Pointer) \n); int a 1, b 2; int * const p a; // 必须初始化 printf(初始: a%d, *p%d\n, a, *p); *p 3; // 可以通过指针修改值 printf(通过*p修改后: a%d, *p%d\n, a, *p); // p b; // 取消注释将导致编译错误read-only variable printf(\n); } void test_constant_pointer_to_constant() { printf( 测试3指向常量的常量指针 \n); const int immutable 100; const int * const p immutable; printf(*p %d\n, *p); // *p 101; // 错误不能修改值 // p NULL; // 错误不能修改指针 printf(双重const两者皆不可变。\n); printf(\n); } void test_function_parameter(const int *input) { printf( 测试4函数参数中的指针常量 \n); // input 是指向常量的指针函数内部不能通过它修改数据 printf(传入的值: %d\n, *input); // *input 999; // 错误保护了外部数据 printf(函数内部无法修改*input。\n); printf(\n); } int main() { test_pointer_to_constant(); test_constant_pointer(); test_constant_pointer_to_constant(); int val 42; test_function_parameter(val); // 额外测试数组名作为常量指针 printf( 测试5数组名作为常量指针 \n); int arr[3] {10, 20, 30}; // arr 可以看作 int * const int *q arr; // 正确普通指针可以接收数组名 q; // q 可以移动 printf(*q (arr[1]) %d\n, *q); // arr; // 错误数组名不能作为左值 return 0; }编译与运行建议将代码保存为pointer_const.c。使用gcc -Wall -Wextra -o pointer_test pointer_const.c编译。-Wall -Wextra会打开更多警告帮助你发现不严谨的代码。运行./pointer_test查看输出。尝试有选择地取消注释那些标记为“错误”的行重新编译仔细阅读编译器给出的错误信息。这是理解规则最直接的方式。8. 在项目中的最佳实践与选择建议理解了语法关键是要在正确的地方使用它们。以下是一些实战建议函数参数传递优先使用const T*对于输入参数如果函数只需要读取数据而不修改它一律使用指向常量的指针。这是最重要的习惯。好处安全防止函数内误操作。清晰函数原型就是文档明确表达了参数用途。灵活可以接受常量或非常量实参。示例void print_array(const int *arr, size_t len);size_t find_item(const char *str, char target);固定资源绑定使用T* const当某个指针在初始化后其指向在整个作用域或对象生命周期内都不应改变时使用。示例嵌入式中的硬件寄存器指针、单例模式中获取的实例指针、类成员中指向某个固定缓冲区的指针。注意如果指向的目标也不该被修改则应结合使用const T* const。定义全局或静态常量数据使用const T* const用于定义模块内部的查找表、配置数据等同时防止指针被修改和指针指向的数据被修改。示例static const char * const days_of_week[] {Mon, Tue, ...};区分“底层const”与“顶层const”这是C标准中的概念但有助于理解。顶层const表示对象本身是常量如int * const p。底层const表示指针指向的对象是常量如const int *p。在拷贝赋值时顶层const不影响但底层const必须匹配。非常量可以转为常量反之则不行。简单记你可以给一个“指向常量的指针”赋予一个“普通指针”的值增加const但不能反过来去掉const。使用typedef简化复杂声明对于复杂的常量指针类型使用typedef可以大大提高代码可读性。typedef const char * ReadOnlyString; // 指向常量字符的指针 typedef char * const FixedString; // 指向字符的常量指针 void process(ReadOnlyString input) { // 一目了然输入字符串 // ... } FixedString config_path /etc/app.conf; // 路径固定不可变9. 总结与下一步回到最初的问题如何区分指针常量和常量指针记住这句口诀“左定值右定址”。看const相对于*的位置。const *(const在左)指向的值是常量指针常量Pointer to Constant。你有一个只读的视角。* const(const在右)指针本身是常量常量指针Constant Pointer。你有一个固定的挂钩。彻底掌握它们不仅仅是应付考试更是为了写出更安全、更清晰、更专业的C语言代码。它关乎你对程序数据访问权限的精确控制是理解更高级主题如函数指针、复杂数据结构的基石。下一步你可以结合函数指针尝试理解const在函数指针声明中的应用例如int (* const fp)(int)和int (*fp)(const int)的区别。探索C中的引用C的引用在行为上很像一个自动解引用的常量指针并且天然具有const属性不能重新绑定。阅读优秀源码查看Linux内核、标准C库glibc或一些著名开源项目的代码观察他们如何大量使用const来保证安全。在项目中实践从今天起在编写函数时有意识地为所有“输入”参数加上const限定。编译器会成为你最好的代码审查伙伴。理解并善用const是区分C语言新手和熟练者的重要标志之一。建议收藏本文并在下次遇到指针相关的编译警告或困惑时回来对照表格和示例进行排查。