news 2026/9/7 4:45:31

C语言指针常量与常量指针详解:语法、内存与应用场景

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言指针常量与常量指针详解:语法、内存与应用场景

这次我们来看一个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 *pconst 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 指向 a,a 变了,*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; }

为什么这很重要?

  1. 安全性:防止函数内部的代码意外修改调用者的数据,这类 Bug 很难追踪。
  2. 可读性:函数原型void func(const T *ptr)清晰地表达了设计意图:“这是一个输入参数,我不会改它”。
  3. 兼容性:它可以接受常量数据(如字符串字面量)和非常量数据的地址,而反过来(非常量指针指向常量数据)是不允许的。
    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,如何快速理解?使用“从右向左”的阅读规则是最有效的方法。

规则:从变量名开始,向右看,遇到()[]先解析(它们是右结合),然后向左看,依次解释每个符号。

让我们拆解几个例子:

  1. int const *p;

    • p开始。
    • 向右:没有()[]
    • 向左:遇到*,所以p是一个指针。
    • 继续向左:遇到const,所以这个指针指向的是常量。
    • 继续向左:遇到int,这个常量是int类型。
    • 结论pis a pointer to a constant integer. (指针常量)
  2. int * const p;

    • p开始。
    • 向右:没有。
    • 向左:遇到const,所以p本身是常量。
    • 继续向左:遇到*,所以这个常量是一个指针。
    • 继续向左:遇到int,这个指针指向int
    • 结论pis a constant pointer to an integer. (常量指针)
  3. const int * const p;

    • p开始。
    • 向右:没有。
    • 向左:遇到const,所以p是常量。
    • 继续向左:遇到*,所以这个常量是一个指针。
    • 继续向左:遇到int,指针指向int
    • 继续向左:遇到const,这个int是常量。
    • 结论pis a constant pointer to a constant integer. (指向常量的常量指针)
  4. 更复杂的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; }

编译与运行建议

  1. 将代码保存为pointer_const.c
  2. 使用gcc -Wall -Wextra -o pointer_test pointer_const.c编译。-Wall -Wextra会打开更多警告,帮助你发现不严谨的代码。
  3. 运行./pointer_test查看输出。
  4. 尝试有选择地取消注释那些标记为“错误”的行,重新编译,仔细阅读编译器给出的错误信息。这是理解规则最直接的方式。

8. 在项目中的最佳实践与选择建议

理解了语法,关键是要在正确的地方使用它们。以下是一些实战建议:

  1. 函数参数传递:优先使用const T*

    • 对于输入参数:如果函数只需要读取数据,而不修改它,一律使用指向常量的指针。这是最重要的习惯。
    • 好处
      • 安全:防止函数内误操作。
      • 清晰:函数原型就是文档,明确表达了参数用途。
      • 灵活:可以接受常量或非常量实参。
    • 示例void print_array(const int *arr, size_t len);size_t find_item(const char *str, char target);
  2. 固定资源绑定:使用T* const

    • 当某个指针在初始化后,其指向在整个作用域或对象生命周期内都不应改变时使用。
    • 示例:嵌入式中的硬件寄存器指针、单例模式中获取的实例指针、类成员中指向某个固定缓冲区的指针。
    • 注意:如果指向的目标也不该被修改,则应结合使用const T* const
  3. 定义全局或静态常量数据:使用const T* const

    • 用于定义模块内部的查找表、配置数据等,同时防止指针被修改和指针指向的数据被修改。
    • 示例static const char * const days_of_week[] = {"Mon", "Tue", ...};
  4. 区分“底层const”与“顶层const”

    • 这是C++标准中的概念,但有助于理解。顶层const表示对象本身是常量(如int * const p)。底层const表示指针指向的对象是常量(如const int *p)。
    • 在拷贝赋值时,顶层const不影响,但底层const必须匹配。非常量可以转为常量,反之则不行。
    • 简单记:你可以给一个“指向常量的指针”赋予一个“普通指针”的值(增加const),但不能反过来(去掉const)。
  5. 使用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语言代码。它关乎你对程序数据访问权限的精确控制,是理解更高级主题(如函数指针、复杂数据结构)的基石。

下一步你可以

  1. 结合函数指针:尝试理解const在函数指针声明中的应用,例如int (* const fp)(int)int (*fp)(const int)的区别。
  2. 探索C++中的引用:C++的引用(&)在行为上很像一个自动解引用的常量指针,并且天然具有const属性(不能重新绑定)。
  3. 阅读优秀源码:查看Linux内核、标准C库(glibc)或一些著名开源项目的代码,观察他们如何大量使用const来保证安全。
  4. 在项目中实践:从今天起,在编写函数时,有意识地为所有“输入”参数加上const限定。编译器会成为你最好的代码审查伙伴。

理解并善用const,是区分C语言新手和熟练者的重要标志之一。建议收藏本文,并在下次遇到指针相关的编译警告或困惑时,回来对照表格和示例进行排查。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 1:48:29

FaceFusion 3.8.1更新:处理器与视频底层重写,本地换脸更稳更快

FaceFusion 3.8.1 这次更新的关键信息很简单&#xff1a;重写了处理器架构和视频底层。对于长期跑本地换脸、做视频后期合并素材的人来说&#xff0c;这两个改动直接关系到“跑得快不快”和“稳不稳定”。这次我们就从版本定位、部署方式、功能验证、批量处理和性能观察几个角度…

作者头像 李华
网站建设 2026/9/7 4:43:49

STM32F411移植FreeModbus V1.6:Modbus RTU从站与DMA接收改造实战

简介&#xff1a;FreeModbus V1.6 是一份成熟的 Modbus 协议栈开源实现&#xff0c;支持 RTU 与 ASCII 两种传输模式&#xff0c;面向嵌入式开发者和工业自动化工程师&#xff0c;解决了 MCU 与上位机或 PLC 之间快速集成 Modbus 通信的问题。完整覆盖读写线圈、离散输入、保持…

作者头像 李华
网站建设 2026/9/4 7:52:29

基础平台研发岗笔试攻略:从算法到分布式的核心考点解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 3:36:58

基于Unity 3D + C#实现的昆虫博物馆系统

本项目为前几天收费帮学妹做的一个项目&#xff0c;在工作环境中基本使用不到&#xff0c;但是很多学校把这个当作编程入门的项目来做&#xff0c;故分享出本项目供初学者参考。 一、项目描述 基于Unity 3D C#实现的昆虫博物馆系统 地址&#xff1a;本地PC端运行&#xff08…

作者头像 李华
网站建设 2026/9/5 7:26:06

FPGA实现曼彻斯特编码:RTL设计、仿真与上板调试全攻略

简介&#xff1a;面向FPGA学习者的曼彻斯特编码完整工程包&#xff0c;基于Quartus II环境完成编码器与解码器设计&#xff0c;适合数字通信、以太网接口等方向的初学者或工程师参考。曼彻斯特编码在每个比特中间跳变&#xff0c;兼具时钟与数据传递&#xff0c;FPGA可灵活实现…

作者头像 李华
网站建设 2026/9/5 13:06:09

B站后端校招笔试复盘:题型考点与实战策略

去年秋招季&#xff0c;很多学弟学妹问我B站后端开发方向笔试到底考什么。说实话&#xff0c;B站这几年校招笔试的题目风格变化不小&#xff0c;2023届的笔试卷A更偏向“基础工程”的组合&#xff0c;而不是纯粹刷LeetCode。如果你只刷题不补基础&#xff0c;很容易在选择题上翻…

作者头像 李华