阅读 宋劲杉 老师的 Linux C 编程一站式学习 总结 C 语言的一些特性。
声明和定义
声明:变量声明、函数声明、类型声明。
分配存储空间的声明同时也是定义,不分配存储空间的声明不是定义。
凡是被多次声明的变量或函数,必须有且只有一个声明是定义的,如果有多个定义,或者一个定义都没有,链接器就无法完成链接。
定义一个变量,就是分配一块存储空间并给它命名; 给一个变量赋值,就是把一个值保存到这块存储空间。
初始化是一种特殊的声明,而不是一种赋值语句。
表达式
任何表达式都用值和类型两个基本属性。
左值右值
有些表达式可以表示存储位置和值,有些只能表示值。表达式所表示的存储位置称为左值 (lvalue),允许放在等号左边,表达式的值称为右值 (rvalue) ,只能放在等号右边。如下 error :
(a = b) = c
minute + 1 = hour;
返回值
函数返回一个值相当预定义一个和返回值类型相同的临时变量并用 return 后面的表达式来初始化。
函数返回值不是左值,或者说函数调用表达式不能做左值。
is_even(20) = 1; /* error */
转义序列 \n
是在编译时处理,而转换说明在运行时调用 printf
函数处理的。
printf("character: %c\n", '}');
整数类型
整型:char、int 型
副作用 (Side Effect)
改变计算机存储单元⾥的数据或者做输⼊输出操作都算 Side Effect,包括如下几个:
-
函数有副作用,与数学函数在概念上的根本区别。比如
printf
通常不关心它的返回值,只是利用它所产生的打印的副作用。或者在函数中修改某全局变量也是一种 Side Effect。 -
赋值运算符,把表达式
a = b
看做函数,返回值既是 a 又是 b 的值,副作用是 a 的值被改变。 -
前缀或后缀运算符 (++ --),把表达式
++i
看作函数调用,传入一个参数返回值等于参数加 1,副作用是 i 的值加 1。 -
复合赋值运算符 (*= /= %= += -= <<= >>= &= ^= |=),
a += 1
相当于a = a + 1
,但有⼀点细微的差别,前者对表达式 a 只求值⼀次,⽽后者求值两次。a[foo()] += 1; a[foo()] = a[foo()] + 1; // 如果 foo() 函数调⽤有Side Effect,⽐如会打印⼀条消息,那么前者只打印⼀次,⽽后者打印两次。
Sequence Point
C标准规定代码中的某些点是 Sequence Point,当执⾏到⼀个 Sequence Point 时,在此之前的 Side Effect 必须全部作⽤完毕,在此之后的 Side Effect 必须⼀个都没发⽣。⾄于两个 Sequence Point 之间的多个 Side Effect 哪个先发⽣哪个后发⽣则没有规定,编译器可以任意选择各 Side Effect 的作⽤顺序。如下例子结果 undefined,跟编译器实现相关。
int a = 0;
a = (++a)+(++a)+(++a)+(++a);
printf("%d\n", i++ * i++);
包含如下几种 Sequence Point:
-
调⽤⼀个函数时,在所有准备⼯作做完之后、函数调⽤开始之前是Sequence Point。
foo(f(), g()); // f() g()执行顺序未知
-
条件运算符 ?:、逗号运算符、逻辑与 &&、逻辑或 || 的第⼀个操作数求值之后是 Sequence Point。
-
在⼀个完整的声明末尾是Sequence Point,所谓完整的声明是指这个声明不是另外⼀个声明的⼀部分。
int a[10], b[20]; // 在 a[10] 末尾是Sequence Point,在 b[20] 末尾也是
-
在⼀个完整的表达式末尾是Sequence Point,所谓完整的表达式是指这个表达式不是另外⼀个表达式的⼀部分。
f(); g(); // f()在g()之前先执行
-
在库函数即将返回时是Sequence Point。
写表达式应遵循的原则:
- 在两个 Sequence Point 之间,同⼀个变量的值只允许被改变⼀次 。
- 如果在两个 Sequence Point 之间既要读⼀个变量的值⼜要改它的值,只有在读写顺序确定的情况下才可以这么写。
a = (++a)+(++a)+(++a)+(++a); // error
a[i++] = i; // error
i = i + 1; // 读写顺序确定,ok
void 设计原因
从语法上规定没有返回值的函数调用表达式是 void 类型,有一个 void 类型的值,这样任何表达式都有值,不必考虑特殊情况,编译器的语法解析比较容易; 然后从语义上规定 void 类型的表达式不能参与运算,从而兼顾类语法上的一致和语义上的不矛盾。
运算符优先级
后缀运算符包括后缀++、后缀–、结构体取成员.、 数组取下标[],单目(或前缀)运算符包括前缀++、前缀–、正号+ 、负号-、逻辑非!。后缀运算符优先级最高,单目运算符仅次于后缀,比其它运算符都高。
++count[2]
是对 count[2]
做前缀 ++ 运算。
数组
数组类型做右值使用时,自动转换成指向数组首元素的指针。下面的写法是错误的。
int a[5] = { 4, 3, 2, 1 };
int b[5] = a; /* error; incompatible types in assignment */
在函数原型中,如果参数写成数组的形式,则该参数实际上是指针类型。
define 和枚举
define 不仅可以定义常量,也可以定义更复杂的语法结构,宏定义,define 定义在预处理阶段处理,枚举在编译阶段处理。
结构体的成员名和变量名不在同一命名空间,枚举的成员名和变量名在同一命名空间。
#include <stdio.h>
enum coordinate_type { RECTANGULAR = 1, POLAR };
int main(void)
{
int RECTANGULAR;
printf("%d %d\n", RECTANGULAR, POLAR); /* 2 0 */
return 0;
}
Old Style C 风格
并非所有函数声明包含完整的函数原型,如
void threelines(); /* 没有指出参数类型和个数 */
void main() {}
sizeof
sizeof
是⼀个特殊的运算符,有两种形式:sizeof 表达式
和 sizeof(类型名)
。
sizeof 表达式
中的⼦表达式并不求值,⽽只是根据类型转换规则求得⼦表达式的类型,然后把这种类型所占的字节数作为整个表达式的值。 sizeof(表达式)
形式⾥的括号和 return(1);
的括号⼀样,不起任何作⽤。
sizeof(类型名)
的括号是必须写的,整个表达式的值也是这种类型所占的字节数。
int a[12];
printf("%d\n", sizeof a/sizeof a[0]);