C语言中有不少有趣的特性,这些东西不知道也无妨,若知道了则能够锦上添花,提升生产力或减小bug。数组
因为这些东西比较杂,内容也很少,不值得专门去介绍他们,也没有合适的地方在其余文章内安插这些,因此就以番外篇的形式作个大杂烩。安全
在C89(C90)以及以前,块内的申明(好比函数内)必须放在块开头(即全部申明在语句以前)。而从C99开始,申明能够放在任何位置。好比如下代码,在C99中能够经过编译,而在C89(C90)中不行。app
int main(void) {
int var1 = 0;
var1 = 5;
int var2 = 3; // 不在块开头
}
复制代码
虽然标准这么规定,但在实现中仍然采用C89(C90)的方法,“能够放在任何位置”只是编译器对代码进行重排的结果。若是用调试器跟踪变量,会发如今进入函数时两个变量就已经申明,只是在各自申明的位置进行初始化。编辑器
受早期没有规范以及谭浩强的影响,不少人喜欢写void main();
。其实这是一种彻底错误的写法。函数
从C89(C90)开始,C标准就规定main
函数必须返回int
类型,正常状况下应返回0
。优化
C语言代码从main
函数开始执行,为何返回值如此重要?ui
代码执行从main
开始,但编译后的二进制程序的执行却并非从main
开始。在进入main
以前,操做系统还有不少工做要作,如初始化环境和传递参数等,main
的返回值用于操做系统评估程序是否正常退出。(Windows常常在某些不规范程序退出后弹出“**程序是否正常退出”的询问)spa
若是main
函数忘记写return 0;
,那么编译时会自动补上,这也是C标准规定的。(可是其余本该有返回值的函数若是没有return
,其行为是未定义的)操作系统
说到传递参数,C标准规定了两种main
函数参数形式:指针
int main(void);
int main(int argc, char **argv); // int main(int argc, char *argv[]);
复制代码
参数能够为空或者两个参数,参数名能够任意,但类型必须为第一个int
,第二个char **
。
同时,C标准规定能够针对不一样平台扩展参数。(如下内容摘抄自维基百科:Entry_point)
好比在Unix和Windows中能够经过第三个参数指定运行环境:
int main(int argc, char **argv, char **envp);
复制代码
基于Darwin的操做系统(例如macOS)具备第四个参数,该参数包含操做系统提供的任意信息:
int main(int argc, char **argv, char **envp, char **apple);
复制代码
此外,int main();
也是能够经过编译的。在C++中int main();
与int main(void);
相同,但在C中是不一样的。C语言中若是函数的参数列表为()
,表示不肯定参数的数量,能够向函数传递任意数量的参数。若是要表示没有参数,须要添加关键词void
。
通常而言,第一个参数argc
表示操做系统调用此程序的参数计数,包括文件名。第二个参数argv
表示参数向量,指向一个指针数组,数组中的每一个指针指向一个字符串(此处的数组和字符串都不是严谨的意义)。第一个字符指针所指的字符串是文件名,具体内容与调用程序时终端的写法一致;最后一个字符指针是个空指针,也就是说argv[argc] == (char *)0
。
好比,调用一个名为a.out
的程序:
$ ./a.out I love C
复制代码
argc == 4;
argv[0] == "./a.out";
argv[1] == "I";
argv[2] == "love";
argv[3] == "C";
argv[4] == (char *)0;
复制代码
以0做为参数向量结尾与字符串末尾的0有相同的妙用。
有趣的是,即便main
写为int main(void);
,代表不接受参数,但在终端调用程序时仍然能够输入参数,而且这些参数也会传递给程序,只是咱们在程序内没法使用这些参数。
switch语句大概是C语言中最使人讨厌的语句了,不只几乎用不到,语法还特别麻烦。(因此某些语言删除了switch语句)
swith语句的原理其实就是匹配标签与跳转,跟goto语句相似,能够当作是goto语句的加强魔改版。
首先,switch语句只支持整数匹配,好比int
、char
,不支持float
、double
等浮点数。由于浮点数在计算机中使用二进制表示,并不能彻底准确地表示十进制小数,尤为在进行计算后尾数很不肯定,几乎不可能按照预期匹配。
switch语句首创了case
标签。在“语言结构”那篇我讲过,case
标签只能用在switch语句内,标签做用域只是当前switch语句而不是当前函数,而且case
标签容许且只容许标签名使用整数类型常量表达式。整数能够理解,上一段已经说了switch只支持整数匹配;由于要进行匹配,要求一个返回值,因此得是表达式;至于常量,case
标签毕竟是个标签,在编译时就必须获得肯定的值,而变量的值在运行时才能肯定,因此只能是常量表达式。
case
标签中的表达式在编译时进行计算,因此下面两个标签相同,若是在同一个swith语句中编译会报错。
case 0 + 2:
case 1 + 1:
复制代码
因为switch语句只是如goto般的匹配标签与跳转,跳转后就按照顺序结构继续执行了,再也不理会其余标签,因此才一般在switch语句内使用break跳出,只执行须要的部分。不过,也能够利用这个特性让多个匹配结果执行相同的操做。
switch (var) {
case 0:
case 1:
case 2:
printf("var < 3");
break;
default:
printf("var >= 3");
}
复制代码
此外,switch语句内的申明必须写在块内,以免跳过初始化。以下:
switch (var) {
case 0:
{
int a = 5; // 编译经过
printf("%d\n", a);
}
case 1:
int a = 5; // 编译报错
printf("%d\n", a);
}
复制代码
for语句的存在是为了简化循环,跟三元运算符 ? :
相似。
for语句紧跟的括号内是由分号;
分隔的三个表达式(是的,此处的;
是分隔符而不是语句结尾,三个份量都是表达式而不是语句),三个表达式均可以省略(直接空着就行,不能写void
),若是中间的表达式为空,默认为1。
PS:if
、while
等语句括号内的判断条件不能省略。
虽然申明函数时在函数名前加void
表示没有返回值,但函数调用表达式仍然会返回一个void
类型的值,但这个值不能被使用,只能丢弃。
for语句括号内的第二个表达式是循环的判断条件,若是此处调用了一个void
函数,那么void
值就被使用了,编译就会出错,除此以外,这个位置能够是任意表达式。至于另外两个表达式则没有任何限制。
从C99开始,for语句括号内的第一个表达式能够换成一个不彻底的申明(彻底的申明以分号结尾,而此处的分号是分隔符,不是申明的一部分)。
for语句括号内变量的做用域小于for所在的块,大于for语句内部的块。举例以下:
int iter = 0;
for (int iter = 0; iter < 5; iter++) {
int iter = 0;
printf("%d\n", iter++);
}
printf("%d\n", iter);
复制代码
在这段代码中申明了三个iter
变量,分别在for语句外,for语句的括号内,for语句的块内。
首先申明了最外层的iter
,接着进入for语句,又申明了一个iter
。
括号内的空间能够看做是外层的一个子域,因此能够申明和外层同名的变量,而且屏蔽掉了外层的iter
。
括号中另外两个iter
都是在括号内申明的那个iter
。
而后进入for语句内的块,又申明了一个iter
。
这个块内部能够看做是括号内空间的子域,因此此处申明的iter
又屏蔽掉了外层的两个iter
。
每次循环都会从新申明一个iter
并初始化为0,因此每次循环输出一个0
,自增操做没有实际做用。
因为块内的iter
屏蔽掉了括号内的iter
,因此控制条件不受块内代码的影响,循环执行5次。
跳出循环后,for语句的两个iter
都被销毁,打印的是最外层的的iter
。
因此程序的执行结果是输出6个0
。
若是for语句的块内有continue;
语句,那么在执行此语句后会跳过块内的剩余代码,并执行for语句括号内的第三个表达式,以后才是判断条件。
若是for语句的块内有break;
语句,那么在执行此语句后会直接跳出循环,不执行for语句括号内的第三个表达式。
因此在for语句内使用continue;
和break;
语句都是安全的。
所谓字符串常量,就是指"fake_string"
这种类型的东西。
众所周知,C语言没有真正意义上的字符串类型,只是以0
结尾的字符数组。
"fake_string"
并无它看上去那么简单,实际上它作了不少事。首先,申请一段连续的内存空间,依次放入char
字符f
、a
、k
、e
、_
、s
、t
、r
、i
、n
、g
、\0
(注意最后补了个0),而后返回char
数组。与数组同样,字符串常量也能够当指针来使用,类型为char *
,且这个指针同时有底层和顶层const
属性。
既然能够当指针来用,那么指针运算在字符串常量上也适用。
"string"[0] == 's';
"string"[6] == 0;
char *ch = "string";
复制代码
字符串常量也能够像其余指针同样做为if
、while
等的判断条件。
不过,和数组同样,对字符串常量使用sizeof
运算符获得的是所占内存空间的大小。
对字符串常量取地址获得的是一个行指针。
(两者都包括末尾的0)
sizeof("1234") == 5;
char (*pt)[5] = &"1234";
复制代码
首先,谁都知道但常常忽略的一点,定义结构体末尾必须加分号。
结构体定义由4部分组成,struct
关键字、结构体名、成员列表、结构体变量。
一个完整的结构体定义能够写为:
struct StructName {
int member1;
char member2;
} var, *pt;
复制代码
若是要使用此结构体来申明变量,能够写为:
struct StructName var2;
复制代码
其中struct StructName
是一个完整的类型名。
在结构体定义中,结构体名和结构体变量能够省略。若是省略结构体名,就没有办法在其余地方使用此结构体,因此不该该同时省略结构体名和结构体变量。
结构体定义仅仅是类型定义,不能对结构体成员初始化。(C++能够指定成员默认值)
除了定义,也能够申明一个结构体。仅需struct
关键字和结构体名。
struct StructName;
复制代码
申明的结构体甚至能够没有定义(只要不去使用它)。
结构体也能够和typedef
结合使用,该类型定义能够在结构体定义以前。一样,若是没有使用此类型,结构体也能够没有定义。
typedef struct StructName TypeName;
复制代码
结构体名,类型名分别属于不一样的名字空间,也就是说,这两个名称能够相同。以下:
typedef struct TreeNode TreeNode;
struct TreeNode {
int data;
TreeNode *left;
TreeNode *right;
};
复制代码
不过仍是建议使用不一样的名字,避免混淆。
结构体的成员能够是结构体变量。如:
struct S1 {
int member1;
int member2;
};
struct S2 {
struct S1 member1;
int member2;
};
复制代码
或者
struct S1 {
struct S2 {
int member1;
int member2;
} member1;
int member2;
};
复制代码
尽管在第二例中,S2
在S1
内部定义,但其做用域与S1
相同。(这点与C++不一样)
和数组同样,结构体也可使用列表初始化。列表按照成员的申明顺序依次初始化,没有指定的成员初始化为0。
对于包含结构体成员的结构体,可使用相似多维数组那样的初始化方法。
struct S1 {
struct S2 {
int member1;
int member2;
} member1;
int member2;
};
struct S2 var1 = { 1, 2 };
struct S1 var2 = { { 1, 2 }, 3 };
复制代码
结构体也支持指定初始化,以下:
struct S1 var = { .member1.member2 = 1, .member2 = 2 };
复制代码
PS:C++不支持指定初始化。
从C99开始,可使用类型加列表创造一个临时结构体。如:
struct S {
int member1;
int member2;
};
(struct S){ 1, 2 };
复制代码
临时结构体能够像通常结构体那样使用。
(struct S){ 1, 2 }.member1 == 1;
复制代码
临时结构体也可使用多维列表以及指定初始化。
PS:C++不支持临时结构体。
若是对一个结构体使用sizeof
运算符,会发现它的大小并不必定等于成员大小之和。
C为了提升效率,采用了内存对齐,是一种牺牲空间换时间的方案。
内存对齐能够简述为:
关于结构体的内存对齐,网上有不少很详细的说明,我就再也不赘述。
其实,在C中到处都有内存对齐,好比定义变量,只是咱们通常无需关心。
为了提升运算效率,C在运算时会将比int
小的整数类型提高为int
类型。
这是由于int
的大小通常就是处理器的字长。虽然内存按字节编址,但处理器是以字为单位处理数据的,这种提高是硬件层面的语言优化。
char a = 0, b = 0;
printf("%d\n", sizeof(a + b));
复制代码
上面的代码将输出4
。
事实上,除了在字符串里为了节省空间而使用char
类型之外,C几乎不会使用char
类型。
好比字符常量'a'
,实际上是int
类型。(C++中为char
)
sizeof('a') == 4;
复制代码
此外,咱们熟知的getchar
、fgetc
等函数也是返回int
类型。
C中的字符常量还有另一个特性,好比有这些写法:'ab'
、'abc'
、'abcd'
。
这些可都不是字符串,而是int
类型整数。由于int
类型的大小通常是char
类型的4倍,因此能够容纳最多4个ASCII
字符。
暂且无论大小端存储,只从逻辑上分析。以abcd
为例,将这4个字符的二进制码依次排列,再组合成一个数,就是abcd
的值。为了简便,我用十六进制表示二进制。
'a' == 0x61;
'b' == 0x62;
'c' == 0x63;
'd' == 0x64;
'abcd' == 0x61626364;
'abcd' == 1633837924;
复制代码
关于浮点数,C也几乎不使用float
,由于其精度过低。
咱们经常使用的浮点数常量如0.5
实际上是double
类型,math,h中接受或返回浮点数的函数也几乎都是double
类型。
C并不原生支持布尔类型,直到c99才引入了关键字及类型_Bool
。
而咱们经常使用的stdbool.h头文件的主要内容只有:
#define bool _Bool
#define true 1
#define false 0
复制代码
虽然理论上布尔类型只有1bit,但实际上它占用1个字节。
_Bool
类型只有0
和1
两个值,任何非0数值赋值给_Bool
变量都转换为1
。
由于并无原生支持布尔类型,因此在C中条件表达式如1 < 2
、逻辑表达式如1 && 0
的返回值都是int
类型。事实上,通常状况下,在C中使用布尔类型没有实际意义。
PS:C++有对布尔类型的完整支持。
注意:C中的auto
与C++中的auto
彻底不一样。
C中的auto
表示动态生存期。
auto
大概是C中最没有存在感的东西了,事实上,它确实没什么用。
局部变量默认为动态生存期,无需指定auto
;而全局变量默认静态生存期,不能指定auto
。
正因如此,C++才将auto
的做用改成了自动判断类型。
C中有两种申明函数的方式。传统的“函数申明”以及“函数原型”。
函数申明仅需指定返回值类型和函数名,参数列表为空。
函数原型还需指定参数类型(参数名可选)。
int fn1(); // 传统申明
int fn2(int, double) // 函数原型 int fn3(void) // 函数原型 复制代码
因为在C中函数参数列表为()
表示不肯定参数,因此编译器不会在函数调用时检查参数。
建议在申明函数时使用函数原型,最好连参数名都加上。这样可让编译器来检查函数调用的正确性,若是使用比较智能的编辑器,还能够在编写代码时得到正确的参数提示。