ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C/C++中const关键字与指针、引用的位置关系全解析

C/C++中const关键字与指针、引用的位置关系全解析 1. 从一段“诡异”的代码说起为什么const的位置让人头疼最近在带新人做代码Review看到一段关于字符串处理的函数声明大概长这样char const * const ProcessString(char const * const input);新人挠着头问我“哥这const一会儿在*前面一会儿在*后面到底谁修饰谁啊我每次看到这种都得停下来想半天有没有什么一劳永逸的‘口诀’”我笑了笑这几乎是每个C/C开发者从入门到进阶的必经之路。const这个关键字看似简单就六个字母但它与指针*、引用结合时位置的微妙变化直接决定了程序的行为、安全性和你的设计意图。很多人靠死记硬背“左定值右定向”之类的口诀但遇到复杂情况或者长时间不用很容易又混淆。实际上理解const的各种位置核心在于理解声明符declarator的解析规则和const的“就近修饰”原则。它不是一个孤立的语法点而是与你对内存模型、数据所有权、接口契约的理解深度绑定。搞清楚了你写出的代码会更具表达力更安全也更能体现你的设计水平。今天我们就抛开那些模糊的口诀从编译器的视角把const在各种位置下的含义掰开揉碎讲清楚。2. 基础概念扫盲const到底“定”住了什么在深入位置差异前我们必须统一对const基础语义的理解。很多混淆源于对基本概念的模糊。2.1 const的核心语义只读承诺const最直白的翻译是“常量”但在C/C中更准确的描述是**“只读”**。它向编译器和你代码的读者包括未来的你自己做出一个承诺“通过这个标识符变量名、指针等去访问这块内存时你不能修改其中的内容。”注意这个承诺是针对“访问路径”的而不是内存本身。这块内存本身可能是可写的只是通过这条加了const的路径访问时编译器会阻止你进行写操作。这是理解后续所有复杂情况的基础。2.2 声明与定义理解声明的“向右螺旋”规则C/C的声明语法被戏称为“顺时针/螺旋规则”虽然规则本身有点复杂但我们可以抓住一个关键思想从变量名标识符开始根据操作符的优先级和结合性像剥洋葱一样一层层向外解析。对于涉及指针的声明*和const谁先谁后至关重要。这里有一个简单有效的判断方法找到声明中的变量名然后看它左边的const和右边的const分别修饰谁。注意在标准的声明中const可以放在类型前或类型后如const int和int const在修饰普通变量时等价。但在与指针结合时我们通常将const视为类型限定符的一部分并关注其相对于*的位置。3. 单层指针下的const位置全解析这是最常见的战场也是所有混淆的源头。我们以一个int类型和指针为例。3.1 常量指针指针本身是常量int * const pint a 10, b 20; int * const p a; // p是一个常量指针指向int解析变量名是p。const在*的右边紧挨着p。根据“就近原则”const直接修饰p本身。所以p这个指针变量的值即它存储的地址不能被改变。什么能改什么不能改p b;//错误不能改变p指向的地址。*p 30;//正确可以通过p修改它所指内存a的值因为p指向的int不是常量。初始化要求由于p本身是常量必须在定义时初始化指定它指向哪里。类比这好比一个固定的望远镜p一旦架设好对准了某颗星星a就不能再转动去对准别的星星。但你可以通过这个望远镜去观察并描述那颗星星的状态修改*p。3.2 指向常量的指针指针指向的数据是常量const int *p或int const *pint a 10, b 20; const int *p a; // p是一个指针指向const int // 等价于: int const *p a;解析变量名是p。const在*的左边修饰的是int即指针所指向的类型是const int。所以不能通过指针p来修改它所指向的内存内容。什么能改什么不能改*p 30;//错误不能通过p修改a的值。p b;//正确可以改变p本身让它指向另一个地址b。注意b本身可以不是const但通过p这个“窗口”去看b时只能读不能写。初始化灵活性可以指向常量或非常量。指向非常量时只是限制了这个指针的“写权限”。类比这好比一个带了“只读滤镜”的望远镜p。你可以随意转动望远镜对准任何星星p b但通过这个滤镜看任何星星你都只能看不能动手去改变它*p 30错误。3.3 指向常量的常量指针指针本身和指向的数据都是常量const int * const pint a 10; const int * const p a; // p是一个常量指针指向const int解析这里有两个const。左边的const修饰int右边的const修饰p。所以p既不能改变指向p xxx错误也不能通过它修改所指内存的值*p xxx错误。这是最严格的限制。通常用于表示一个固定的、只读的数据访问句柄。初始化要求必须在定义时初始化。3.4 记忆技巧与常见误区技巧忽略最左边的类型如int直接看*和const。const在*左边指向的东西是常量*p不能变。const在*右边指针本身是常量p不能变。const在*两边都有两者都是常量。误区int const *p和const int *p完全等价。很多人只熟悉后者看到前者就发懵。记住const修饰它左边的类型如果左边没类型则修饰右边。在int const *p中const左边是int所以它修饰int含义不变。4. 进阶场景const与引用、多层指针、函数参数的纠缠掌握了单层指针我们来看更复杂的场景这些才是真正体现功力的地方。4.1 const与引用C中引用本质上是别名一旦绑定就不能再指向其他对象。所以引用本身天然就是“常量”的T类比于T* const。因此const在引用上的用法主要修饰的是引用的类型。int a 10; const int ref a; // 常引用通过ref不能修改a // int const ref a; // 错误引用本身不能是const因为引用天生不可重新绑定。const int 常引用。允许绑定到常量或非常量甚至字面量或临时对象是C中非常高效且安全的传递参数的方式避免了不必要的拷贝。重要用途作为函数参数void Func(const std::string str);表示函数承诺不会修改str同时避免了按值传递的拷贝开销。4.2 多层指针**下的const这才是真正的“噩梦模式”但原理和一层的相同。关键依然是从标识符开始由内向外解析明确每个const修饰的是哪一个*。int a 10; int *p a;const int **pp1 p;//危险可能允许非法修改pp1是指向const int*的指针。这本身没问题但如果你允许pp1 p;而p是指向非常量int的指针那么理论上可以通过*pp1一个const int*类型的指针去修改一个非const的inta这违反了const的承诺。因此编译器通常禁止这种隐式转换需要强制类型转换。这是一个经典的const正确性漏洞点。int * const *pp2 p;pp2是一个指针它指向一个int * const即一个常量指针指向int。意思是pp2可以指向不同的ppp2 another_p但不能通过*pp2来修改p的指向*pp2 somewhere错误但可以通过**pp2修改a的值**pp2 20正确。int const ** const pp3 p;pp3本身是一个常量指针最右边的const修饰pp3它指向一个const int*指向常量的指针。所以pp3不能指向别处并且通过*pp3得到的指针也不能用来修改数据。4.3 函数参数与返回类型中的const这里的const主要关乎API的设计契约和调用者的使用体验。值传递参数void Func(int x)。参数x是副本函数内修改x不影响实参。这里的constvoid Func(const int x)只对函数内部实现有意义它告诉编译器和你自己在函数体内不会修改x。对于调用者来说传常量和非常量进去都一样。这种用法可以防止函数体内的误操作是一种良好的编程习惯。指针/引用传递参数void Func(const int* ptr);或void Func(const std::string str);。这里的const是接口契约的一部分。它向调用者承诺“我绝不会通过这个指针或引用来修改你传进来的数据”。这极大地提高了接口的易用性和安全性调用者可以放心地传递常量对象、字面量或临时对象。返回类型为指针/引用const int* GetData();调用者得到一个指向常量的指针不能通过它修改数据。这常用于返回类内部数据的只读视图。const int GetData();返回常引用同样只读且无拷贝开销。注意不要返回局部变量的指针或引用无论是否const因为局部变量在函数结束后就被销毁了返回的指针/引用将指向无效内存。4.4 成员函数尾部的const这是C特有的极其重要。class MyClass { public: int GetValue() const; // 常量成员函数 void ModifyValue(); // 非常量成员函数 private: int value_; };int GetValue() const;这个const修饰的是隐式的this指针。它意味着在这个函数体内this的类型是const MyClass*因此你不能通过this修改类的任何非静态成员变量除非成员被mutable修饰。作用语义明确告诉调用者这个函数不会改变对象的状态。实用性常量对象const MyClass obj;只能调用其常量成员函数。这使得常量对象变得有用。重载可以根据函数是否为const进行重载。例如std::vector::operator[]就有const和非常量两个版本分别用于只读和读写访问。5. 实战中的“坑”与最佳实践理解了语法还要知道怎么用以及如何避开陷阱。5.1 类型转换与const的丢失这是违反const正确性的主要根源。const int a 100; int *p (int*)a; // 使用C风格强制转换去掉了const *p 200; // 未定义行为a原本是const修改它可能导致程序崩溃或产生奇怪结果。绝对不要用强制类型转换尤其是C风格转换和const_cast去掉一个原本定义为const的对象的常量性并修改它。对于通过指针/引用传递进来的、被承诺为const的数据也绝不应该去掉其常量性进行修改。这破坏了程序的逻辑契约是未定义行为的温床。const_cast的唯一合理使用场景是调用一个设计糟糕的、参数不是const但实际不会修改参数的旧式API而你手头只有const数据时。即便如此也要万分小心。5.2 顶层const与底层const这是一个有助于理解的概念模型顶层const (top-level const)表示对象本身是常量。适用于任何数据类型。如int * const p中的const修饰p指针本身const int a中的const修饰a。底层const (low-level const)表示指针或引用所指向的对象是常量。如const int *p中的const。在拷贝操作中顶层const不影响因为拷贝的是值但底层const必须匹配或者可以进行从非常量到常量的转换反之则不行。int a 0; const int *p1 a; // 正确非常量地址可以赋值给指向常量的指针添加底层const int *p2 p1; // 错误不能去掉底层const5.3 设计建议与最佳实践能用const就用const这是一种“按需索取权限”的设计思想。默认给变量、参数、返回值加上const除非你真的需要修改它。这能减少意外修改让代码意图更清晰也方便编译器优化。函数参数优先使用const T对于非内置类型的输入参数这几乎是标准做法。高效且安全。成员函数尽可能声明为const只要一个成员函数不修改对象状态就把它声明为const。这提高了类的常量可用性。警惕指针的指针和const的传递在设计涉及多层指针的API时要仔细思考每一层的const语义避免出现前面提到的const int**类型的漏洞。保持一致性在一个项目或模块中对const的用法比如const放在类型前还是后保持统一风格可以提高代码的可读性。我个人习惯将const放在类型后面如int const*因为它更符合“const修饰其左边类型”的语法逻辑但在业界const int*的写法更为流行两者皆可重在统一。6. 经典面试题深度剖析最后我们通过几个经典的、容易出错的例子来巩固一下这些也是面试中高频出现的问题。6.1 问题一以下声明的区别是什么const char *p; char const *q; char * const r; const char * const s;const char *p;和char const *q;完全等价。都是指向常量字符的指针。不能通过p或q修改它们指向的字符但p和q本身可以指向不同的地址。char * const r;常量指针指向字符。r一旦初始化就不能再指向别处但可以通过r修改它指向的字符。const char * const s;指向常量字符的常量指针。s不能变*s也不能变。6.2 问题二下面代码有什么问题void PrintString(const char **str) { printf(%s\n, *str); } int main() { char *data Hello; // C11起字符串字面量是const char[]这里用char*指向已deprecated PrintString(data); // 编译警告或错误 return 0; }问题PrintString期望一个const char**指向const char*的指针但data的类型是char**。在C/C中不能将char**隐式转换为const char**。原因就是我们4.2节提到的const正确性漏洞。如果允许可能会通过const char*这个“只读视图”去修改原本非const的数据。修正将data声明为const char* data Hello;。或者修改函数签名如果函数确实不修改str指向的字符串可以改为void PrintString(const char * const * str)表示“指向常量指针的指针”这里的常量指针指向const char。这样从char**到const char * const *的转换是安全的。6.3 问题三const在函数重载中的应用class Array { public: int operator[](size_t index) { // 用于读写 return data_[index]; } const int operator[](size_t index) const { // 用于只读 return data_[index]; } private: int data_[100]; };当你有Array arr;时arr[0]调用非常量版本返回的引用可以修改。 当你有const Array carr;时carr[0]调用常量版本返回常量引用不能修改。这是const用于成员函数重载的完美例子它让对象在常量和非常量语境下有不同的、恰当的行为。理解const的各种位置绝非简单的语法记忆。它是编写健壮、清晰、高效C/C代码的基石。下次当你再看到复杂的const声明时别慌静下心来找到变量名然后从左到右、由近及远地解析思考每个const究竟锁定了哪一层“访问权限”。当你习惯这种思考方式后const就不再是障碍而是你表达设计意图、提升代码质量的最得力助手之一。
返回列表