2.11 switch、case组合
2.11.1 基本语法
switch(整型变量/常量/整型表达式) { case var1: break; case var2: break; case var3: break; default: break; }
2.11.2 不要拿青龙偃月刀去削苹果
已经有if else为何还要switch case
switch语句也是一种分支语句,常常用于多分支的情况。这种多分支,一般指的是很多分支,而且判定条件主要以整型为主,如:
输入1,输出星期一
输入2,输出星期二
输入3,输出星期三
输入4,输出星期四
输入5,输出星期五
输入6,输出星期六
输入7,输出星期日
如果写成 if else 当然是可以的,不过比较麻烦
#include <stdio.h> int main() { int day = 1; switch (day){ case 1: printf("星期一\n"); break; case 2: printf("星期二\n"); break; case 3: printf("星期三\n"); break; case 4: printf("星期四\n"); break; case 5: printf("星期五\n"); break; case 6: printf("星期六\n"); break; case 7: printf("星期日\n"); break; default: printf("bug!\n"); break; } return 0; }
2.11.3 规则:
把 default 子句只用于检查真正的默认情况。
有时候,你只剩下了最后一种情况需要处理,于是就决定把这种情况用 default 子句来处理。这样也许会让你偷懒少敲几个字符,但是这却很不明智。这样将失去 case 语句的标号所提供的自说明功能,而且也丧失了使用 default 子句处理错误情况的能力。所以,奉劝你不要偷懒,老老实实的把每一种情况都用 case 语句来完成,而把真正的默认情况的处理交给default 子句
在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。
建议 for 语句的循环控制变量的取值采用“半开半闭区间”写法
半开半闭区间写法和闭区间写法虽然功能是相同,但相比之下,半开半闭区间写法写法更加
直观。循环次数明显,便于进行个数计算
半开半闭区间写法 | 闭区间写法 |
for(n = 0; n < 10; n++) { …… } |
for(n = 0; n <= 9; n++) { …… } |
不能在 for 循环体内修改循环变量,防止循环失控(会导致死循环什么的)
for (n = 0; n < 10; n++) { … n = 8;//不可,很可能违背了你的原意 … }
2.11.4 case在switch的作用是什么?
case本质是进行判断功能
2.11.5 break在switch中的作用是什么?
break本质其实是进行分支功能
2.11.6 没有break会有什么问题?
#include <stdio.h> int main() { int day = 1; switch (day){ case 1: printf("星期一\n"); case 2: printf("星期二\n"); case 3: printf("星期三\n"); case 4: printf("星期四\n"); break; case 5: printf("星期五\n"); break; case 6: printf("星期六\n"); break; case 7: printf("星期日\n"); break; default: printf("bug!\n"); break; } return 0; } //如果多个不同case匹配,想执行同一个语句,推荐做法: #include <stdio.h> int main() { int day = 6; switch (day){ case 1: case 2: case 3: case 4: case 5: printf("周内\n"); break; case 6: case 7: printf("周末\n"); break; default: printf("bug!\n"); break; } return 0; }
结论:case之后,如果没有break,则会依次执行后续有效语句,直到碰到break
这里补充一个知识点:case后面不能是const修饰的只读变量
2.11.7 default语句相关问题
default可以出现在switch内的任何部分
尽管如此,我们依旧强烈推荐default应该放在case语句的最后
如果放在了最后面之后,前面的case语句中均没有 break,那么会一直执行,还要执行default子句
2.12 goto关键字
一般来说,编码的水平与 goto 语句使用的次数成反比。有的人主张慎用但不禁用 goto语句,但是在《C语言深度解剖》这本书中,作者却主张禁用
自从提倡结构化设计以来, goto 就成了有争议的语句。首先,由于 goto 语句可以灵活跳转,如果不加限制,它的确会破坏结构化设计风格;其次, goto 语句经常带来错误或隐患。它可能跳过了变量的初始化、重要的计算等语句。如果我们使用得当,goto也会带来巨大的便利,如大名鼎鼎的linux系统就涉及到了goto语句。
struct student *p = NULL; … goto state; p = (struct student *)malloc(…); //被 goto 跳过,没有初始化 { ⋯ state: //使用 p 指向的内存里的值的代码 ⋯ }
如果编译器不能发觉此类错误,每用一次 goto 语句都可能留下隐患
2.13 void关键字
2.13.1 色即是空
void 有什么好讲的呢?如果你认为没有,那就没有;但如果你认为有,那就真的有。有点像“色即是空,空即是色”。
2.13.2 void a
void 真正发挥的作用在于:
(1) 对函数返回的限定;
(2) 对函数参数的限定。
众所周知, 如果指针 p1 和 p2 的类型相同, 那么我们可以直接在 p1 和 p2 间互相赋值;如果 p1 和 p2 指向不同的数据类型,则必须使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。
例如:
float *p1; int *p2; p1 = p2;
其中 p1 = p2 语句会编译出错,提示“'=' : cannot convert from 'int *' to 'float *'”,必须改为:
p1 = (float *)p2;
而 void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
void *p1; int *p2; p1 = p2;
但这并不意味着, void *也可以无需强制类型转换地赋给其它类型的指针。因为“空类型”可
以包容“有类型”,而“有类型”则不能包容“空类型”。有些语句在编译时就可能会出错
2.13.3 void 修饰函数返回值和参数
如果函数没有返回值,那么应声明为 void 类型
在 C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为 void 类型。为了避免混乱,我们在编写 C 程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没有返回值,一定要声明为 void 类型。这既是程序良好可读性的需要。也是编程规范性的要求。另外,加上 void 类型声明后,也可以发挥代码的“自注释”作用。所
谓的代码的“自注释”即代码能自己注释自己
如果函数无参数,那么应声明其参数为 void
在 C 语言中,可以给无参数的函数传送任意类型的参数,若函数不接受任何参数,一定要指明参数为 void
2.13.4 void指针
千万小心又小心地使用 void 指针类型
按照 ANSI(American National Standards Institute)标准,不能对 void 指针进行算法操作,
ANSI 标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。也就是说必须知道内存目的地址的确切值。
那么我们该怎么做呢?
可以进行强制类型转化,把void*型指针类型转化为其他类型指针,然后再进行对应的操作
如果函数的参数可以是任意类型指针,那么应声明其参数为 void *
典型的如内存操作函数 memcpy 和 memset 的函数原型分别为:
void * memcpy(void *dest, const void *src, size_t len); void * memset ( void * buffer, int c, size_t num );
2.13.5 void不能代表一个真实的变量
void 不能代表一个真实的变量。
因为定义变量时必须分配内存空间,定义 void 类型变量,编译器到底分配多大的内存呢?
void简单不?现在你觉得它到底是色,还是空呢?
2.14 return 关键字
2.14.1 内存管理
首先在介绍这个关键字之前,我们先去认识一下“栈”的概念。
内存分成5个区,他们分别是堆,栈,自由存储区,全局/静态存续区,常量存续区。
(1)栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数,函数调用后返回的地址。(为运行函数而分配的局部变量、函数参数、函数调用后返回地址等存放在栈区)。栈运算分配内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(2)堆:内存使用new进行分配,使用delete或delete[]释放。如果未能对内存进行正确的释放,会造成内存泄漏。但在程序结束时,会由操作系统自动回收。
(3)自由存储区:使用malloc进行分配,使用free进行回收。
(4)全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的(全局变量、静态数据 存放在全局数据区)
(5)常量存储区:存储常量,不允许被修改。
更多的关于内存管理方面的知识大家可以去看看LoveMIss-Y这位前辈的文章
一文详解堆栈(二)——内存堆与内存栈_LoveMIss-Y的博客-CSDN博客_内存堆栈
我在这里就直接说一个概念了:
调用函数,形成栈帧
函数返回,释放栈帧
知道了上面概念之后我们再看一下return
2.14.2 return相关
函数调用时先给main函数一次性在栈上开辟足够的空间,我们称之为“栈帧”。关于如何估算我们整个程序所需相应的空间大小,那么就要回到我们文章刚开始时关于变量定义那里了,实质上是根据我们定义的变量类型以及核算关键字来估计我们程序所需的空间大小。
所有的一般临时变量基本都是在一个栈帧结构中定义的,需要使用栈帧中的其他临时变量,是要以我们的栈帧为依托来开辟自己的栈帧空间。栈帧结构都被释放掉了,那么给该变量的或该栈帧内的变量开辟的空间也就不存在了,他也应该被释放掉了,这里所谓的释放就是允许被覆盖。
但是我们栈帧空间有限,所以我们要主动的去释放掉空间,return就是用来终止一个函数(释放空间)并返回其后面跟着的值
return (Val); //此括号可以省略。但一般不省略,尤其在返回一个表达式的值时。return 可以返
回些什么东西呢?看下面例子:
char * Func(void) { char str[30]; … return str; }
str 属于局部变量,位于栈内存中,在 Func 结束的时候被释放,所以返回 str 将导致错误。
2.14.3 拓展:
return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁
函数的返回值,就是通过寄存器的方式返回给函数调用方!
2.15 const 关键字
2.15.1 const关键字也许应该被替换为readonly
//习惯格式 const int a = 10;
const修饰的变量不可直接被修改,正常情况是只读,可以通过指针的形式间接去修改。例如:
int *p = &a; //之后再通过对指针p进行修改来改变a的值
那么就有人会有疑问,既然能够被改变,那么我们要const何用?难不成是看C语言太简单了,给我们增加点难度?请看下去。
2.15.2 const价值
所有的报错都是在我们编译代码时报错,而不是把程序编过来运行起来报错,所以我们就要想办法早点发现代码错误,以减少成本。const修饰的变量就是不想让别人或者自己忘了,对这个变量进行了修改,让编译器直接进行修改式检查。所以const不能称为真正的常量(见下面代码),程序只有在运行的时候才能知道它的大小。
#include<stdio.h> int main() { const int n = 100; int arr[n]; //vs中是不通过的 //因为数组空间开辟时,他的元素个数是一定的,中括号里的必须是常量 //这就间接的说明了const修饰的不能算是真正的常量,我们叫它常变量 return 0; }
而相对的,真正不可被修改的是字符串常量,大家下去可以自己动手定义一个字符型指针,将字符串赋给这个指针,让后再对指针进行解引用外加赋值,这时候编译器就会无情的发出告警!
2.15.3 const修饰数组
定义或说明一个只读数组可采用如下格式:
int const a[5]={1, 2, 3, 4, 5}; const int a[5]={1, 2, 3, 4, 5};
2.15.4 const修饰指针
const int *p;
int const *p;
int *const p;
const int *const p;
// p 可变, p 指向的对象不可变
// p 可变, p 指向的对象不可变
// p 不可变, p 指向的对象可变
//指针 p 和 p 指向的对象都不可变
2.15.5 记忆和理解的方法:
先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。 “近水楼台先得月”,离谁近就修饰谁。
2.15.6 const修饰函数参数
const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使用。例如:
void Fun(const int i);
告诉编译器 i 在函数体中的不能改变, 从而防止了使用者的一些无意的或错误的修改。
2.15.7 const修饰函数返回值
#include <stdio.h> //告诉编译器,告诉函数调用者,不要试图通过指针修改返回值指向的内容 const int* test() { static int g_var = 100; return &g_var; } int main() { int *p = test(); //有告警 //const int *p = test(); //需要用const int*类型接受 *p = 200; //这样,【在语法/语义上】,限制了,不能直接修改函数的返回值 printf("%d\n", *p); return 0; } //一般内置类型返回,加const无意义
2.16 最易变的关键字----volatile
这个关键字是最不为人知,但确实最考察一个程序员C语言学习深度一个关键字。
volatile 关键字和 const 一样是一种类型修饰符, 用它修饰的变量表示可以被某些编译器
未知的因素更改,比如操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编
译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
int i=10; int j = i; //(1)语句 int k = i; //(2)语句
这时候编译器对代码进行优化,因为在(1)、(2)两条语句中, i 没有被用作左值。这时候
编译器认为 i 的值没有发生改变,所以在(1)语句时从内存中取出 i 的值赋给 j 之后,这个值并没有被丢掉,而是在(2)语句时继续用这个值给 k 赋值。编译器不会生成出汇编代码重新从内存里取 i 的值,这样提高了效率。但要注意: (1)、(2)语句之间 i 没有被用作左值才行。
int x; x = 100; //x的空间,变量的属性,左值 int y = x; //x的内容,数据的属性,右值 //同时也可以说明,任何一个变量名在不同的应用场景中代表不同的含义! int *p =&a; p = &b; //左值就是空间,我们将右值内容放到左值(空间)里去 q = p; //右值是内容:就是一个地址 //so,指针就是地址 //指针变量是一个变量,只不过里面保存的是地址数据
我们再看一个例子:
volatile int i=10; int j = i; //(3)语句 int k = i; //(4)语句
volatile 关键字告诉编译器 i 是随时可能发生变化的,每次使用它的时候必须从内存中取出 i 的值,因而编译器生成的汇编代码会重新从 i 的地址处读取数据放在 k 中。
这样看来,如果 i 是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问
const要求你不要进行写入就可以。volatile意思是你读取的时候,每次都要从内存读。两者并不冲突。
虽然volatile就叫做易变关键字,但这里仅仅是描述它修饰的变量可能会变化,要编译器注意,并不是它要求对应变量必须变化!这点要特别注意。
2.17 最会带帽子的关键字----extern
我们在写代码的时候并不是只在一个文件里面写,只在一个文件里面写会提高我们维护成本,所以我们要尽量使用多文件,关于多文件,我们在static那里提到过,忘记的可以回头瞅两眼再回来。我们使用多文件时,要保证我的文件之间是可以交互的(可以互相传递信息),那么这个时候就需要extern来帮我们告诉编译器:这个变量,这个函数……来自其它文件。
extern 可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,下面的代码用到的这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义。就好比在本文件中给这些外来的变量或函数带了顶帽子,告诉本文件中所有代码,这些家伙不是土著。下面是《C语言深度解剖》中的例子,大家可以看看
A.c 文件中定义: B.c 文件中用 extern 修饰: int i = 10; extern int i; //写成 i = 10;行吗? void fun(void) extern void fun(void); //两个 void 可否省略? { //code } C.h 文件中定义: D.c 文件中用 extern 修饰: int j = 1; extern double j; //这样行吗?为什么? int k = 2; j = 3.0; //这样行吗?为什么
2.18 struct 关键字
2.18.1 概念:
struct是一个神奇的关键字,我叫它“打包员”,因为它将一些相关联的数据打包成一个整体,方便使用。它就是我们C语言后面说的结构体。
在网络协议、通信控制、嵌入式系统、驱动开发等地方,我们经常要传送的不是简单的字节流(char 型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。经验不足的开发人员往往将所有需要传送的内容依顺序保存在 char 型数组中,通过指针偏移的方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序就要进行非常细致的修改,非常容易出错。这个时候只需要一个结构体就能搞定。平时我们要求函数的参数尽量不多于 4 个,如果函数的参数多于 4 个使用起来非常容易出错(包括每个参数的意义和顺序都容易弄错), 效率也会降低。这个时候,可以用结构体压缩参数个数。
2.18.2 空结构体大小
结构体所占的内存大小是其成员所占内存之和,这里绝不是简单的各个结构体成员大小相加,而是涉及到了另外一个概念:结构体的内存对齐,现在主要讨论空结构体的问题,之后再专门对“内存对齐”进行讲解,这里不多赘述。
struct student { }stu;
这个空结构体的内存是多大呢,我们再不同的编译器下,编译结果是不同的:
(1)VS下,我们的代码是不能被编译通过的
(2)gcc它允许我们的代码被编过,编译结果为0
(3)VC++ 6.0下也让编过,编译结果为1byte
编译器认为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。既然如此,编译器就理所当然的认为任何一个结构体都是有大小的,哪怕这个结构体为空。那万一结构体真的为空,它的大小为什么值比较合适呢?假设结构体内只有一个 char 型的数据成员,那其大小为 1byte(这里先不考虑内存对齐的情况) .也就是说非空结构体类型数据最少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占
的空间大吧。所以我们VC就选了1做空结构体大小。
2.18.3 柔性数组
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。 柔性数组成员允许结构中包含一个大小可变的数组。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct st_type { int i; int a[0]; }type_a; 有些编译器会报错无法编译可以改成: typedef struct st_type { int i; int a[]; }type_a;
这样我们就可以定义一个可变长的结构体,用 sizeof(type_a)得到的只有 4,就是sizeof(i)=sizeof(int)。那个 0 个元素的数组没有占用空间,而后我们可以进行变长操作了。通
过如下表达式给结构体分配内存:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
2.19 union 关键字
2.19.1 union相关规则
union 关键字的用法与 struct 的用法非常类似,但在本质上确实截然相反的。
union 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,在 union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。
union StMa{ char character; int number; char *str; double exp; };
一个 union 只配置一个足够大的空间以来容纳最大长度的数据成员,以上例而言,最大
长度是 double 型态,所以 StMa 的空间大小就是 double 数据类型的大小。
2.19.2 大小端模式对于union类型数据的影响
请看下面的代码片段:
union { int i; char a[2]; }*p, u; p = &u; p->a[0] = 0x39; p->a[1] = 0x38; //p.i 的值应该为多少呢?
大小端在前面signed、unsigned关键字那里已经讲过了,这里就不再多说了。
union 型数据所占的空间等于其最大的成员所占的空间。对 union 型的成员的存取都是相对于该联合体基地址的偏移量为 0 处开始, 也就是联合体的访问不论对哪个变量的存取都是从 union 的首地址位置开始。如此一解释,相信大家心里都已经有了答案了吧?大家可以把自己的答案打在评论区
2.20 enum 关键字
2.20.1 枚举
enum就是C语言里面的枚举类型,枚举不是像很多人说的那样没什么用,相反,它是很重要的
//定义方式 enum enum_type_name { ENUM_CONST_1, ENUM_CONST_2, ... ENUM_CONST_n } enum_variable_name;
enum_type_name 是自定义的一种数据数据类型名,而 enum_variable_name 为enum_type_name类型的一个变量, 也就是我们平时常说的枚举变量。 实际上enum_type_name
类型是对一个变量取值范围的限定,而花括号内是它的取值范围,即 enum_type_name 类型的变量 enum_variable_name 只能取值为花括号内的任何一个值,如果赋给该类型变量的值不在列表中,则会报错或者警告。enum存的都是常量,,所以枚举类型几乎等价于整型,简单理解enum的本质就是制作一组具有强相关性的常量。
枚举具有自描述性,不用做过多的解释
2.20.2 枚举与#define 宏的区别
1), #define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
2),一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
3),枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个。
枚举显然是可以用宏常量来代替的,但是如果常量多且相关性强,最好使用枚举,而且如果我们需要大量的相关性强的常量,用宏定义就要定义很多次,这会让我们的精力白白浪费在定义上。还有就是枚举有语法检查
2.21 伟大的缝纫师----typedef 关键字
2.21.1 历史的误会----也许应该是 typerename
顾名思义,type 是数据类型的意思; def(ine)是定义的意思,合起来就是定义数据类型啦!可是这种理解是不正确的,我们或许应该叫它typerename,因为typedef的这种意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别名,而非定义一个新的数据类型。
在实际项目中,为了方便,可能很多数据类型(尤其是结构体之类的自定义数据类型),需要我们重新取一个适用实际情况的别名。这时候 typedef 就可以帮助我们。例如:
typedef struct student { //code }Stu_st,*Stu_pst; //涉及到的命名规则前面已经提过了
struct student stu1和 Stu_st stu1是相同的
struct student *stu2;和 Stu_pst stu2;和 Stu_st *stu2;没有区别(如果看不明白可以去看看结构体再过来)
2.21.2 扩展:
用typedef定义一种新类型时,不能使用该新类型再拼上其他的关键字 / 不能再引入其他关键字来修饰类型或者变量,比如:
typedef int int32; unsigned int 32 b; //这种修饰方式很明显就是错的 //不符合语法规范
3. 关键字分类
目前,已经把 C89(C90) 的所有关键字全部介绍完了。下面就要对关键字进行一下分类,方便大家理解。
3.1 数据类型关键字(12个)
char :声明字符型变量或函数
short :声明短整型变量或函数
int : 声明整型变量或函数
long :声明长整型变量或函数
signed :声明有符号类型变量或函数
unsigned :声明无符号类型变量或函数
float :声明浮点型变量或函数
double :声明双精度变量或函数
struct :声明结构体变量或函数
union :声明共用体(联合)数据类型
enum :声明枚举类型
void :声明函数无返回值或无参数,声明无类型指针
3.2 控制语句关键字(12个)
3.2.1 循环控制(5个)
for :一种循环语句
do :循环语句的循环体
while :循环语句的循环条件
break :跳出当前循环
continue :结束当前循环,开始下一轮循环
3.2.2.条件语句(3个)
if : 条件语句
else :条件语句否定分支
goto :无条件跳转语句
3.2.3. 开关语句 (3个)
switch :用于开关语句
case :开关语句分支
default :开关语句中的“其他”分支
3.2.4. 返回语句(1个)
return :函数返回语句(可以带参数,也看不带参数)
3.3.存储类型关键字(5个)
auto :声明自动变量,一般不使用
extern :声明变量是在其他文件中声明
register :声明寄存器变量
static :声明静态变量
typedef :用以给数据类型取别名(但是该关键字被分到存储关键字分类中,虽然看起来没什么相关性)
存储关键字,不可以同时出现,也就是说,在一个变量定义的时候,只能有一个
3.4.其他关键字(3个)
const :声明只读变量
sizeof :计算数据类型长度
volatile :说明变量在程序执行中可被隐含地改变
结语:
好了,C语言关键字部分已经全部讲完了,如果其中有问题的地方请大家指出来;如果觉得有用的千万不要忘了点赞、关注、收藏哦!我们下期再见!
●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!