C语言中的符号重载
C语言非常的简洁, 以至于不愿意用太多的符号, 这样有很多符号在不同的地方有不同的含义
这样会让用户很困惑, 这是c的语言特性, 也是设计上的一些失误
static
在函数内部,表示该变量的值在各个调用间一直保持延续性;
对于函数,表示该函数只在本文件中可见
extern
用于变量,表示该变量在其它地方定义;
用于函数定义, 表示全局可见(属于冗余的)
void
用于参数列表中,表示该函数参数为空,如int main(void);
用于返回值,表示该函数返回void,即不返回任何值,相当与pascal中的过程;
在指针声明中,表示通用指针
*
乘法运算符;
用于指针前,表示对指针所指内容的间接引用;
用于声明中表示指针,如int *pi,表示指向整形的指针,char *strcpy(...)表示函数的返回值为指针
&
位与运算符;
取地址操作符
()
函数定义中,包围函数形式参数表,如int main(int argc, char **argv);
调用一个函数,如srand();
改变表达式的运算顺序,如 a * ( b + c );
类型转换, 如 (int*)x;
定义带参数的宏,如 #define MAX(a, b) ((a) > (b)) ? (a) : (b)
包围sizeof操作符的操作数(如果是类型名),如 sizeof(int)
数组和指针的本质区别
C编程新手最常听到的一句话是'数组和指针是相同的', 不幸的是, 这是一种危险的说法, 他并不正确.
最典型的一个错误是
file 1:
int mango[100];
file 2:
extern int *mango;
这样的声明是错的, 必须声明为extern int mango[];
关于声明的意义在上篇讲解extern时已经详细说明过
我们首先要区分"地址y"和"地址y的内容"之间的区别, 这是一个相对微妙之处, 因为大多数编程语言中我们用同一个符号来表示两样东西, 并由编译器根据上下文环境来判断它的具体含义.
int X, Y;
X = Y;
这边我们就来深入讨论一下这个赋值语句, 看上去X和Y是同一种东西, 都是int型变量
但对于编译器而言, 一个赋值语句两边的东西是不一样的 (以汇编的角度思考)
左值 = 右值;
左值是地址, 右值是数据, 是有本质区别的
所以上面的X是X代表的地址, Y是Y代表的地址里面的内容
所以对于编译器而言,
X=2, 这个很直接, 把2放到X代表的地址上
X=Y, 需要先从Y代表的地址上读出数据, 再放到X代表的地址上
需要注意, 左值是在编译时就可以明确知道的, 因为编译器会为每个变量分配一个地址
而右值往往只有在运行时才可知的
那么下面详细看看为什么大家会误认为两者相同
int a[10], *p, *q;
p = a ;
q = p+1 ;
q = a+1;
int i;
i = a[0];
i = (*a);
i = (*p);
如上的代码, 你发现可以象操作指针一样来操作数组变量名a, 貌似a和指针p时等价的
其实非也, a其实代表数组的首地址, 就是个常量, 进行q = a+1, 类似于X = 2+1
而p是指针变量, p=a后, p代表的地址里存放了a,即数组首地址
那么q = p+1, 需要从p代表的地址中读出a, 并放到q代表的地址中去, 类似于X = Y+1
同样对于*操作, 是取地址上的内容, *a直接取a地址上的内容, 而*p是要先从p代表的地址取出地址数据, 再取该地址数据上的内容
而且两者还有一个区别是时间上的差别
对于q = a+1, 因为a代表常量, 在编译时就可以算出q的值
而对于q = p+1, p是变量, 必须到运行时才能知道q的值
两者是有本质区别的, a代表常量, 有人把它称为'常量指针', 是不能赋值或改变的指针, 这绝对的是误导
a就不是指针, 根本就不能作为左值, 你有见把2作为左值的吗
此处可以认为a等同于&p
之所以大家会混淆, 完全是因为c语言的设计不规范导致的, 相同的语句却有着完全不同的操作
此处个人觉得是数组的设计欠缺妥当
int i, * p;
对于一般的变量, 无论是i还是p, 作为左值表示变量名所代表的地址, 作为右值表示变量名所代表的地址上存放的数据. 其实i和p从这个层面上来讲, 完全没有什么分别, 只不过p中存放的数据是地址而不是其他.
但是对于int a[];
就不一样了, 因为a只是作为一个数据集合的开始位置, a无法作为左值(设计者认为这样的不恰当). 同时a作为右值时, 也无法取a地址上的数据, 因为它只是个开始位置, 你不知道取多少. 所以设计者打破了普通变量的规则, 把a当作一个地址常量来看待, 即当a作为右值时, 直接取a代表的地址作为右值, 而不会象通常变量一样去取地址上的数据作为右值.
问题是, 根据c的语法, 其在使用时和指针又十分相似, 这样的设计是不严谨的. 其实如果这样设计会稳妥些,
将a[]作为一个整体, 不能单独对a做任何操作, 如需取地址则用&(a[])
不过这样应该不符合c语言的简单美学...c语言是灵活, 简单的语言, 但是很多设计太随意, 不够严谨, 所以导致很多地方不符合逻辑, 很难理解
再论数组
本书和很多讲解数组和指针的文章过于复杂, 我个人觉得那是把简单的问题复杂化了.
我在上面应该已经把数组和指针的关系讲清楚了, 下面补充几点
1. 书中指出数组和指针在作为函数参数时是一样的(equivalent), 这个说法来自"C Programming Language, 2nd Ed, Kernighan & Ritchie"
如下,
char my_array[10];
char * my_ptr;
...
i = strlen( my_array );
j = strlen( my_ptr );
在c语言中, 作为函数的参数都是传值的, 但是数组却是个例外, 确实对于大数组, 传值明显是相当低效的
所以当你自己把数组作为函数参数的时候, 编译器其实是生成一个指向数组首地址的指针, 并把这个指针作为参数传入函数.
其实如果想把整个数组传值传入函数, 象前面说的, 只要把数组封装在结构中, 即可, 不过不推荐这样, 甚至在把结构体对象当作参数的时候, 要check结构体内是否有数组, 如果有数组, 最好把地址作为参数.
所以在作为函数参数时, 数组首地址会被转化为指针作为参数, 这个只是编译器出于方便统一这样处理.
在这点上说两者equivalent, 我个人觉得是不妥当的
2. 书中说"表达式中的数组名就是指针", 这个说法是相当危险的, 会把刚有些明白的程序员再次绕晕
int a[100];
int *p;
p = a+1;
书中的意思是, 这个操作中, 编译器出于处理的方便, 会先把a封装成一个指针, 然后赋值给p, 所以可以把a看作是指针, 这个说法很不负责
编译器可以出于处理方便的需要把a封装成指针, 但是数组名a绝对不是指针, 如果a是指针, 就应该可以作为左值进行赋值, a = p
实际上是不行的, a就是地址常量
3. 数组中的[]符号就代表地址偏移
在编译的时候, a[i] 会被改写成 *(a+i)
对于+是没有前后顺序的
所以你这样写, i[a], 也是可以的, 因为在编译时, 也是被改写成 *(i+a)
所以虽然看着很bt, 但是其实效果是一样的, 这就是[]的真正意义.