深度解析C语言:语法细节杂谈(番外篇)

C语言中有不少有趣的特性,这些东西不知道也无妨,若知道了则能够锦上添花,提升生产力或减小bug。数组

因为这些东西比较杂,内容也很少,不值得专门去介绍他们,也没有合适的地方在其余文章内安插这些,因此就以番外篇的形式作个大杂烩。安全

申明位置

在C89(C90)以及以前,块内的申明(好比函数内)必须放在块开头(即全部申明在语句以前)。而从C99开始,申明能够放在任何位置。好比如下代码,在C99中能够经过编译,而在C89(C90)中不行。app

int main(void) {
    int var1 = 0;
    var1 = 5;
    int var2 = 3;   // 不在块开头
}
复制代码

虽然标准这么规定,但在实现中仍然采用C89(C90)的方法,“能够放在任何位置”只是编译器对代码进行重排的结果。若是用调试器跟踪变量,会发如今进入函数时两个变量就已经申明,只是在各自申明的位置进行初始化。编辑器

main函数

受早期没有规范以及谭浩强的影响,不少人喜欢写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 语句

switch语句大概是C语言中最使人讨厌的语句了,不只几乎用不到,语法还特别麻烦。(因此某些语言删除了switch语句)

swith语句的原理其实就是匹配标签与跳转,跟goto语句相似,能够当作是goto语句的加强魔改版。

首先,switch语句只支持整数匹配,好比intchar,不支持floatdouble等浮点数。由于浮点数在计算机中使用二进制表示,并不能彻底准确地表示十进制小数,尤为在进行计算后尾数很不肯定,几乎不可能按照预期匹配。

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语句的存在是为了简化循环,跟三元运算符 ? : 相似。

for语句紧跟的括号内是由分号;分隔的三个表达式(是的,此处的;是分隔符而不是语句结尾,三个份量都是表达式而不是语句),三个表达式均可以省略(直接空着就行,不能写void),若是中间的表达式为空,默认为1。

PS:ifwhile等语句括号内的判断条件不能省略。

虽然申明函数时在函数名前加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字符fake_string\0(注意最后补了个0),而后返回char数组。与数组同样,字符串常量也能够当指针来使用,类型为char *,且这个指针同时有底层和顶层const属性。

既然能够当指针来用,那么指针运算在字符串常量上也适用。

"string"[0] == 's';
"string"[6] == 0;

char *ch = "string";
复制代码

字符串常量也能够像其余指针同样做为ifwhile等的判断条件。

不过,和数组同样,对字符串常量使用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;
};
复制代码

尽管在第二例中,S2S1内部定义,但其做用域与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为了提升效率,采用了内存对齐,是一种牺牲空间换时间的方案。

内存对齐能够简述为:

  1. 结构体的第一个成员的偏移量为0
  2. 每一个成员相对于结构体起始地址的偏移量是该成员大小的整数倍
  3. 结构体的最终大小是体积最大成员的整数倍
  4. 结构体的最终大小是知足上述条件的最小值

关于结构体的内存对齐,网上有不少很详细的说明,我就再也不赘述。

其实,在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;
复制代码

此外,咱们熟知的getcharfgetc等函数也是返回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类型只有01两个值,任何非0数值赋值给_Bool变量都转换为1

由于并无原生支持布尔类型,因此在C中条件表达式如1 < 2、逻辑表达式如1 && 0的返回值都是int类型。事实上,通常状况下,在C中使用布尔类型没有实际意义。

PS:C++有对布尔类型的完整支持

auto

注意:C中的auto与C++中的auto彻底不一样

C中的auto表示动态生存期。

auto大概是C中最没有存在感的东西了,事实上,它确实没什么用。

局部变量默认为动态生存期,无需指定auto;而全局变量默认静态生存期,不能指定auto

正因如此,C++才将auto的做用改成了自动判断类型。

函数申明

C中有两种申明函数的方式。传统的“函数申明”以及“函数原型”。

函数申明仅需指定返回值类型和函数名,参数列表为空。

函数原型还需指定参数类型(参数名可选)。

int fn1();              // 传统申明
int fn2(int, double) // 函数原型 int fn3(void) // 函数原型 复制代码

因为在C中函数参数列表为()表示不肯定参数,因此编译器不会在函数调用时检查参数。

建议在申明函数时使用函数原型,最好连参数名都加上。这样可让编译器来检查函数调用的正确性,若是使用比较智能的编辑器,还能够在编写代码时得到正确的参数提示。

相关文章
相关标签/搜索