最近在学操作系统的时候,接触到了Linux的部分基础函数,比如与进程相关的fork
和exec
等。其中exec
是一个系列的函数,有一个名字相似的函数族,比如execv
、execl
等等。根据之前看到的某篇博客,叙述了exec
后跟不同字母的含义:
l
(可能代指list
?)表示参数可以通过C/C++函数调用时的可变参数的形式调用exec,只需最后传递一个NULL
表示参数结束即可;
v
(可能代指variable
?)表示参数以一个字符串数组的形式将参数传递给待调用进程,同样要求最有一个元素为NULL
;
p
表示允许带PATH
的相对路径搜索;
e
表示传递环境变量。
其中这个execvp
,在MSVC中的thread.h
中的定义是这样的:
_DCRTIMP intptr_t __cdecl execvp(
_In_z_ char const* _FileName,
_In_z_ char const* const* _Arguments
);
然后就看到了这么一个神奇的参数类型定义:char const* const*
,在IntelliCode里给出的是另一个定义:const char* const*
。这两者一样吗?分别代表的又是什么类型的数据?
然后就涉及到了最基本的问题,const修饰符与变量类型申明关键字,以及指针之间的关系。
两个在网上随便找找就能见到的例子:
char* const s1 = ...;
const char* s2 = ...;
两者分别定义了什么?有区别吗?
答案是,两者都定义了一个char*
类型的指针,都具有某些值不能被修改的限制(const
修饰符)。但是,第一个限制的是,s1
本身不能被修改,而第二个限制的是不允许通过s2
修改*s2
的值。换句话说,可以对s1
进行的相关写操作是修改*s1
的内容,而可以对s2
进行的相关写操作是修改s2
本身。相应地,对s2
不要求初始化,而s1
一定要初始化,否则会报错,因为后续就不允许修改了,而使用没有初始化过的变量,结果又是不可预测的。
其中对声明的性质起到关键作用的是指针符号*
的位置。
来自StackOverflow的一个答案:
int * mutable_pointer_to_mutable_int;
int const * mutable_pointer_to_constant_int;
int * const constant_pointer_to_mutable_int;
int const * const constant_pointer_to_constant_int;
从这几个声明可以看出,若是*
在const
前面,则指针本身不可修改;否则指向的数据不可修改。可以结合指针的使用进行理解:定义的时候声明了const *p
,则p
可修改,*p
不可修改;若是*const p
,则*p
可修改,p
不可修改,此时const
的作用域仅仅在p
上。若是在*
前后都加上const
,则两者的作用复合,形成一个无论如何都无法修改的指针。
此外,由于const
和数据类型的声明符的顺序没有影响,所以const int*
和int const*
的作用是一样的。
然后就回到了char const* const*
和const char* const*
的关系上。显然,由于char
和第一个const
在所有*
的同侧,交换顺序不影响定义,所以两者是等价的。
那它定义了个啥呢?
首先去除所有的const
,因为它仅仅是对于读写属性的限制,则实际的类型定义为:char **
,也就是一个char
类型的二级指针,即char*
指针的指针。也就是说,定义的东西是一个传统的字符串数组,但是由于是使用指针而不是数组定义的,所以每个一维串的长度可以不一致,整个字符串数组的内存分配也不一定连续。然后再补回const
属性,假设指针名为p
,则该声明限定了*p
和**p
是常量,不允许通过该指针进行修改。也就是说,char const* const* p
定义了一个char
类型的二级指针,且不允许修改该指针指向地址的值,这个值是一个直接指向char
类型数据的指针;也不允许修改这个值作为指针,所指向的目标内存地址的值。所以重新把视角拉高,整个声明到底定义了啥?一个指向不允许内容被修改的字符串数组。从功能上看,也就像是Java编程时在某些地方声明的final
对象一样,用于确保在执行该函数过程时,数据不会被有意或无意地修改。