3.4 解读 C 的声明(续)
3.4.1 const修饰符
const
是在 ANSI C 中追加的修饰符,它将类型修饰为“只读”。
名不副实的是,const
不一定代表常量。const
主要被用于修饰函数的参数。将一个常量传递给函数是没有意义的。无论怎样,使用 const
修饰符(变量名),只意味着使其“只读”。
/* const 参数的范例 */
char *strcpy(char *dest, const char *src);
strcpy
是持有被 const
修饰的参数的范例。此时,所谓的“只读”是如何表现的呢?
做个实验应该很快就会明白,上面例子中的 src
这个变量没有定义为只读。
char *my_strcpy(char *dest, const char *src)
{
src = NULL; ←即使对src 赋值,编译器也没有报错
}
此时,成为只读的不是 src
,而是 src
所指向的对象。
char *my_strcpy(char *dest, const char *src)
{
*src = 'a'; ←ERROR!!
}
如果将 src
自身定义为只读,需要写成下面这样:
char *my_strcpy(char *dest, char * const src)
{
src = NULL; ←ERROR!!
}
如果将 src
和 src
指向的对象都定义为只读,可以写成下面这样:
char *my_strcpy(char *dest, const char * const src)
{
src = NULL; ← ERROR!!
*src = 'a'; ← ERROR!!
}
在现实中,当指针作为参数时,const
常用于将指针指向的对象设定为只读。
通常,C 的参数都是传值。因此,无论被调用方对参数进行怎样的修改,都不会对调用方造成任何影响。如果想要影响调用方的变量(通过函数参数将函数内的一些值返回),可以将指针作为参数传递给函数。
可是,在上面的例子(my_strcpy
)中,传递的是 src
这个指针。其本来的意图是想要传递字符串(也就是 char
的数组)的值,由于在 C 中数组是不能作为参数传递的,情非得已才不得不将指向初始元素的指针传递给函数(因为数组可能会很大,所以传递指针有益于提高程序的效率)。
产生的问题是,为了达到从函数返回值的目的,需要向函数传递一个指针,这种方式让人感觉有些混乱。
此时,考虑在原型声明中加入 const
,
尽管函数接受了作为参数的指针,但是指针指向的对象不会被修改。
也就是说:
函数虽然接受了指针,但是并不意味着要向调用方返回值。
strcpy()
的意图就是——src
是它的输入参数,但是不允许修改它所指向的对象。
可以通过以下的规则解读 const
声明:
遵从 3.1.2 节中提到的规则,从标识符开始,使用英语由内向外顺序地解释下去。
一旦解释完毕的部分的左侧出现了
const
,就在当前位置追加 read-only。如果解释完毕的部分的左侧出现了数据类型修饰符,并且其左侧存在
const
,姑且先去掉数据类型修饰符,追加 read-only。在翻译成中文的过程中,英语不好的同学请注意:
const
修饰的是紧跟在它后面的单词。
因此,
char * const src
可以解释成:
src
is read-only pointer to char
src
是指向 char
的只读的指针
char const *src
可以解释成:
src
is pointer to read-only char
src
是指向只读的 char
的指针
此外,容易造成混乱的是,
char const *src
和
const char *src
的意思完全相同。
3.4.2 如何使用const?可以使用到什么程度?
很多人习惯在函数注释的参数说明部分,使用(i)
、(o)
、(i/o)
等标记1。
1 (i)
指用于输入的参数,(o)
指用于输出的参数,(i/o)
指用于输入输出的参数。——译者注
这里举一个有些矫揉造作的例子。
/*********************************************************
* void search_point(char *name, double *x, double *y)
*
* 功能:将名称作为key,检索“点”,返回坐标
* 参数:(i) name 名称(检索key)
* (o) x x 坐标
* (o) y y 坐标
*********************************************************
唉,每次要对函数做这样的注释,是不是有点麻烦?于是有很多人会将别的函数的注释一成不变地复制过来,事后还鬼话连篇说自己忘了修改了。其实这些人就没把注释当回事儿,他们认为直接看代码就什么都可以明白,老是揪着注释的问题不放,简直就是没事找事。
在这里的注释中,虽然标记了各参数是(i)
还是(o)
,但编译器可不会注意到这些。
对此,search_point
原型使用下面的方式进行声明:
- void search_point(char const *name, double *x, double *y);
当你错误地向 name[i]
赋值的时候,编译器会很负责任地向我们提出警告。因此,比起在注释中标记什么(i)
或者(o)
,使用 const
可靠性会提高很多*。
* 如果不用 const
,而是使用 (i)
并且很放心地将指针就这样传递给了函数,之后即使变量被改写了,你也很难马上发现。
对于上面的 char const *name
,是不能将它赋予 char*
类型的变量的(除非强制转型)。其中的理由显而易见:如果单纯地将它赋予 char*
类型的变量,之后就可以改写 name
所指向的对象的内容,const
的意义就丧失殆尽了。
同理,将 char const *
类型的指针传递给使用 char*
作为参数的函数,也是不允许的。因此,一旦给指针类型的参数设定了 const
,当前层次以下的函数就必须全部使用 const
*。
* 在一些通用函数中,如果本应该是 const
的参数却没有加上 const
,会经常导致调用方无法使用 const
。
假设有这样一个结构体:
typedef struct {
char *title; /*标题*/
int price; /*价格*/
char isbn[32]; /*ISBN*/
┊
} BookData;
将上面这个结构体作为输入参数的函数原型,可以写成下面这样:
/*注册书的数据*/
void regist_book(BookData const *book_data);
因为使用了 const
,所以 book_data
所指向的对象是禁止改写的。好吧,现在可以放心地将 BookData
传递给这个函数了……
不幸的是,我们发现被传递的数据中,书的标题(book_data->title
)所指向的内容是可以被改写的。
之所以发生这样的事情,是因为根据指定的 const
而成为“只读”的对象只是“book_data
所指向的对象自身”,而不包括“book_data
所指向的对象再向前追溯到的对象”(参照图 3-16)。
图 3-16 const 的边界
因此,如果将结构体 BookData
修改成这样:
这一次,就算是上帝来了也改不了 title
所指向的对象了*。
* Java 的 String
类也是不可变(immutable)的。这种方式当然有它的益处,但是有时候它也会给你带来麻烦。
正因为如此,很多人对 const
究竟能为现实中的编程提供多少便利持怀疑态度。
补充 const 可以代替#define 吗?
通常,C 语言使用预处理器的宏功能定义常量,就像下面这样:
- #define HOGE_SIZE (100)
- ┊
- int hoge[HOGE_SIZE];
可是,预处理器是独立于 C 语言语法的,因此在调试的时候时常会出现一些问题。由宏定义自身的问题造成的错误往往爆发在使用它的地方,这给纠错工作带来很大的困难。
惹不起,总躲得起吧?大不了不使用“坑爹”的宏,是不是可以写成下面这样:
- const int HOGE_SIZE = 100;
- int hoge[HOGE_SIZE];
亲,写成这样还是不行!!
尽管在 C 中,数组的元素个数必须为常量∗,但无论怎样,
const
修饰的标识符不是常量,它只是“只读”而已。因此,上面的写法还是错误的∗。但是,ISO C99 没有这样的规定。
C++另当别论。
3.4.3 typedef
typedef
用于给某类型定义别名。
比如,
typedef char *String;
通过以上的声明,以后对于“指向 char
的指针”可以使用“String
”这个别名。
可以按照普通的变量声明的顺序来解释 typedef
。对于上面的“String
”,如果像对待变量名一样用英语的顺序进行解释,应该是下面这句话:
String
is pointer tochar
String
是指向char
的指针
由此,String
作为“指向 char
的指针”这个类型的别名被声明。
之后,在使用 String
时,你可以写成这样:
String hoge[10];
它的意思是:
hoge
is array(元素个数10
)of String;
hoge
是String
的数组(元素个数10
)
如果将 String
和被定义成 String
类型的指向 char
的指针机械地进行置换,就会产生下面的解释:
hoge is array
(元素个数10
)of pointer tochar
;
hoge
是指向char
的指针的数组(元素个数10
)
语法上,typedef
属于“存储类型修饰符”(参照 2.2.1 节的补充内容)。可是无论怎么看也看不出 typedef
指定了“存储类别”。其实与此无关,typedef
之所以被划分为存储类型修饰符,应该是由于指定类型的语法沿用了通常声明标识符的语法规则。
要 点
typedef 使用和通常的标识符声明相同的方式进行解释。
可是,被声明的不是变量或者函数,而是类型的别名。
平时在声明结构体的时候,我肯定会指定 typedef
。顺便提一下,此时我会尽可能省略 tag
*。
* 也有人鄙视这样的风格……
typedef struct {
┊
} Hoge;
这个声明没有什么特别的地方,关于结构体,假设写成下面这样:
struct Hoge_tag {
┊
} hoge;
由于可以声明 struct Hoge_tag
类型的变量 hoge
,如果将和这个变量名对应的部分置换成类型的名称,就变成了 typedef
的声明了。
此外,在声明变量的时候,可以像下面这样一次性声明多个变量:
int a, b;
同样地,typedef
也可以一次声明类型的多个别名。
可是这么做,除了让声明难以阅读之外,你得不到任何好处。偶尔也会见到下面这样的声明:
typedef struct {
┊
} Hoge, *HogeP;
这个声明其实和下面的声明效果相同:
typedef struct {
┊
} Hoge;
typedef Hoge *HogeP;