(零)问题
网友问:项目中遇到的,指针的别名会在函数签名中把顶层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 当前所指向的值能不能被修改。
如果不嫌啰嗦,这里又分两步:
- 先看右边:p 是指向int的指针。(先是指针!)
- 再看左边: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点。