加载中...
为指针取别名,为何影响const属性?
第1节:代码改善:一个“坑爹”的文字类冒险游戏
第2节:在禁止多重继承的情况下,如何设计“直立智慧猩猩”类?
第3节:C++多线程代码中的“乱序”执行现象
第4节:C++中函数指针有什么作用呢?
第5节:为什么我用c++写的游戏那么简陋?
第6节:多线程读写socket导致的数据混乱的原因是什么?
第7节:WebSocket 是什么原理?为什么可以实现持久连接?
第8节:怎样在c++中实现instanceof?
第9节:一个函数多处 return 是好风格吗?
第10节:C++中虚函数相比非虚函数的优势
第11节:为什么 C::C::C::C::foo() 能编译成功?
第12节:如何静态反射C++枚举的名字
第13节:看C++大叔如何拥 java 妹子入怀……
第14节:坨——理解递归实现“汉诺塔”代码的关键
第15节:C++编译器如何实现 const(常量)?
第16节:C++如何为断言加上消息
第17节:初学C++到什么水平,算是合格的初级开发工程师?
第18节:C++编程要避免使用单例模式吗?
第19节:学习C++要学boost库吗?
第20节:C++的继承就是复制吗?
第21节:C++构造函数失败,如何中止创建对象?
第22节:C++学完多线程后,学什么呢?
第23节:string_view 适合用做函数的返回值类型吗?
第24节:为指针取别名,为何影响const属性?
第25节:std::enable_shared_from_this 的存在意义?
第26节:C++模板可变参数如何一次性解包?
第27节:Linux下的c++开发,平时是怎么调试代码的呢?
课文封面

同样是为类型取别名,用宏定义和using或typedef有何区别?

(零)问题

网友问:项目中遇到的,指针的别名会在函数签名中把顶层const变成底层const。
简化了的代码大概是下面的情况。

using pInt = int*; int i = 0; pInt pi= &i; //这里我认为我声明的是 const int* &ptr,但是编译器认为是int* const &ptr bool func(const pInt &ptr) { ptr = pi;//error: assignment of read-only reference 'ptr' return true; } bool func1(const int* &ptr){ //这个函数就没有问题 ptr = pi; return true; } int main(){ const int *p; func(p); }

有人可以解释一下为什么么,以及如果我想继续使用指针的别名要怎么书写这个参数?


唉,都是历史债。
建议前四点都不看,只看最后一点“真正有用的知识”。


一、在 C 语言里:

int *p;

C语言的老前辈解读的超啰嗦版本,是下面四步:

  • (1)*p 是个 int 。
  • (2)* 在这里是 “ dereference ”操作符,所以 p 是一个指针 ;
  • (3)p 是个指针,且解引用得到一个 int ……
  • (4) 所以,p 是一个指向整数的指针。

注意,基于以上解读的第一步,不难猜出,在C风格的代码中,定义一个指针时,那个星号是习惯靠在变量名称这一边的。

学C++或教C++,我们就基本不在这里用 “dereference”,因为C++是真的有引用。

二、用这种方法解读一下:

const int *p;

只说第一步就够了:*p 是一个整数常量……最终结果就是 p 是指向一个整数常量的指针。

再来看这个:

int *const p;

也只说第一步就够了,但这回第一步要解读的是这部分 “const p”,解读结果当然也超级简单: p 是个常量。

然后呢?然后就是划掉已经解过的const,改为继续解读 “int *p”,过程见上……最终结果就是 p 是个指向整数的指针常量。

这种人肉解析方法,当你在遇到比较复杂的,但在C 语言里基本是必修课的各种奇奇怪怪的函数指针时,真的是救星。

三、再看typedef

typedef 在 C 中既有,using 是C引入的更强大的为类型取别名的方法,但对本问题,二者并无二致,我们就仅讲 typedef。

首先,typedef 是有编译意义的。

有“编译意义”的意思就是:每当碰上某个符号,发现不是一个关键字,并且无法借助前后文的某个关键字来直接推理出这个符号的身份时,就得借助其它规则,继续往前推——此时,一种规则,就是看看这个符号是不是某个已存的 typedef 的结果。

很多其他答案已经指出这一点了: typedef 和 define 于此的重大不同: define 是预编译的事,而预编译的结果相当直接修改了源代码,真正的编译过程,是完全看不到,也不会去看原来的预编译过程了。

给个例子:

typedef int XXX; typedef XXX YYY; void foo(YYY y);

如果走 define ,那么编译器根本看不到 YYY 和 XXX 这两个奇怪的符号;但因为走的是 typedef,所以,一个走“纯朴”的编译过程的“纯朴”的编译器,至少要懵B两次。第一次是“这YYY是什么鬼?”,第二次是“这XXX是个什么鬼?”

然后来看 typedef 如何为指针取类型别名:

typedef int *IntPtr;

没错,大量现存的C的规格说明里,这里的星号,又被靠在 IntPtr 身上了——虽然靠哪边编译结果都一致,但它是不是在向我暗示着什么?总之,一场推理大戏又在上演,只不过这次嫌疑人是“类型”,而不是“数据 ”——

  • 问: IntPtr 是个什么鬼?
  • 答:大人,从引领的 typedef 这个关键字看,IntPtr 是个类型!
  • 问:混蛋!我还不知道它是个类型?我是问它是个什么类型!
  • 答:小的也不知道 IntPtr 是什么类型,不过从它额前沾了一颗星来看,这个类型(的数据)解引用后,将得到一个 int……

拙劣的台词完全不应该成为你此刻的关注重点,刻意设计成一问一答的“破案”对话,是为了像拙劣广告一样,强行植入你的记忆:每个会被用到的 typedef 语句,都需参与后续的编译过程中的相关推导(作为线索存在)。

那么——

void foo(const IntPtr p);

你应该理解 :当编译器碰到 foo 函数定义入参表中的 IntPtr 时,它是懵B的,而为了找到答案,它现在急需一次推导,而为了推出答案,它需要找之前的定义——一切顺利的情况下——它找到的,有效“破案线索”,就是前面的那个“类型定义/ type-def”,也就是:

typedef int *IntPtr;

而当它在解读这个“线索”时,同学们,看仔细:哪有 foo ?哪有 p?最重要的,哪有 const ?相反,熟悉的 typedef,熟悉的 int ,熟悉的额头贴着星号的 IntPtr,一切都是前面说过的内容。肯定不用我再重复一次——我们只说结果:IntPtr 是指向int的指针类型。

案中案告破!现在继续眼前代码:

const 指向int的指针 p

现在是不是很一目了然:p 是个 const 指针。即,const 修饰的是指针自身,因此 p 不能再改变指向,而无关 p 当前所指向的值能不能被修改。

如果不嫌啰嗦,这里又分两步:

  1. 先看右边:p 是指向int的指针。(先是指针!)
  2. 再看左边:p 还是个常量。(然后这个指针是常量!)

结论: p 是个指针,并且是个常量——全然没有提到解引用的事。

注:在和同事交流的过程中,包括你的设计文档,永远不要出现“指针常量”或“常量指针”这样的中文表达。

四、如果我就只学C++呢?

如果,你们碰到像我这样又帅又优秀的老师,你今天提的问题,一定是在破口大骂“使用 define 为什么造成一个指针自身变成 const 的啊!!!!!”

我会一脸淫笑地对你说——但通常也分三步:

  • “我早就说过,别轻易用 宏定义,你们偏要用!偏要用!!”
  • “我在讲 using 时,你是在想女人吗? typedef 如此直白的关键字你是不懂英文吗”
  • “都是活刻,还有脸来问为师!自己上网搜索去吧!”

有隔壁班的同学说,老师,这样只是教的人爽,学的人哪有爽?

你得学会情景代入:学的人由于没有受宏定义的影响(宏定义几乎是最后“附加”学习的),所以你了不管是在用 using 还是用 typedef ,都是:一基于直觉反应,二得到正确结果。怎么会不爽?

五、真正有用的知识

不管是用 typedef 还是 using 来为类型取一个别名,都是先解读 typdef 或 using 这行语句本身,然后再套用回现场。

对了,你还问到:

以及如果我想继续使用指针的别名要怎么书写这个参数?
好吧,真正有用的知识在这里:

  • (1) 其实没事儿还是不要为指针类型取别名。为什么呢?因为百分之99,你的指针别名都是 XXXXPtr,或XXXXPointer —— 为“指针”取一个 Ptr 或 Pointer 于表达是毫无意义的——你不可能给一个需要用 Pointer 或 Ptr 来帮助理解这里是一个指针的同行写代码——通常你这样写会让读你代码的人感受到侮辱,虽然不多。真正有别名意义的,是 XXXX 部分,因为它们的名字往往涉及到业务的理解,比如说 int 必要时你可以给个别名: Age 。

  • (2) 如果是多级(多维)指针,为指针的指针,或指针的指针的指针取别名,就不是毫无意义,而是充满了搞笑的意义。int** p => IntPtrPtr ?PointerPointToIntPtr????哦,你可能喜欢 ppInt……此处我不怕你踩我,我要给的忠告是: pInt 就已经不是可笑的问题,而是可恶,ppInt 简直可恨。

  • (3) 如果非要为(一级)指针类型取别名,并且又常量和非常量又都用得到,那最好把三种(无,const , const* )都加上(这可以解决你上面那个问题),但,万一是你是写C++,或许你还得考虑 mutable 的组合也考虑上,这很可能会让你迅速破防——结论就是回到第1点。