c 语言深度剖析之关键字的秘密
在c语言标准中定义了32个关键字,这32个关键字描述了有数据类型,或修饰数据类型,代码控制等。因此,深度学习c语言首先得把这32个关键字分析透彻,好现在就一个个来看这些关键字。首先看看数据类型。我们先要问为什么c语言中要加入int,float,char等数据类型,以及什么是数据类型?在汇编语言中是没有这个概念的。
1 基本数据类型
1.1 什么是数据类型?
答:数据类型是固定内存大小的别名,比如在32位机器中,int代表4个字节,char代表1个字节。数据类型是创建变量的模子1.2 什么要有数据类型?
答:数据类型是创建变量的模子。这就好比生活中的鞋子,小孩子脚小,那么作鞋使用的模具较小,大人脚大那么就需要较大的作鞋模具。同样,在实际编写代码过程中,数据对内存的大小需求不一样。数据小的话,一个字节就可以搞定。相反,如果是个大数据的话那么就要多个字节才能存储。为了方便使用,c语言中有了int 、char、 double等数据类型,它们 其实就是固定大小内存的别名,没有其它意义了。
1.3 变量的本质的是什么呢?
答 : 变量的本质是一段连续存储空间的别名,程序中通过变量来申请并命名存储空间,通过变量的名字可以使用存储空间。变量有点类型我们的门牌,在一栋楼房中,门牌就代表了具体的房间。同理,在一段内存中变量名就代表了一段内存。但是,这些基本的数据类型还不足以一个数据在内存中怎么存在?存在什么位置?虽然这些事情最后工作是链接器完成的,但是我们的c还是要显示的告诉编译器。那么就有了c中修饰数据类型的关键字
2 auto,static,register分析 (修饰变量)
2.1 在 定义 变量的时候可以加上“属性”关键字,来说明变量在内存的位置。这是因为可执行文件在内存中运行时,在内存中有三个段:一个是栈,另一个是堆,最后一个是静态存储区(这个在c在内存中分布在记录)。变量可以定义在三个段中任何一个。需要注意的是,这里指的是定义的时候! 如果你在声明一个结构体时用static修饰,那是错误的。因为声明一个结构体本身是不占用内存的,其次需要在定义的时候才能加上属性关键字,最后试想如果在声明的时候可以加上属性关键字,那么以后真正定义的时候,编译器会理解这些结构体都是static类型呢,那我们要定义不是static类型的时候,怎么办?所以,编译器干脆不允许我们这样做。因此,如果在代码中写下面这种类型的代码是错误的,可能你还不知道为什么?
static struct student { char *name; int number; }; struct student ludy;2.2 c 语言中默认的局部变量是auto类型,也就是分配在栈空间;全局变量的默认类型是分配在静态存储区;但是需要小心的是static 关键字指明变量的 “静态” 属性,对于静态的局部变量,只会被初始化一次,也就是不管调用定义这个变量的函数体次数,这个变量定义只会执行一次。比如在某个函数里面 static int len;那么可以这样理解不管调用定义这个len变量的函数体次数,这个定义语句只会执行一次。这样说的目的只是告诉自己这个变量只会初始化一次,下一次进来不会被初始化。这里要指出国嵌唐老师的一个错误,唐老师在讲课的时候说编译器在多次编译的时候,在第二次编译的时候发现len已经编译过,所以不在编译。事实上,函数的调用次数你在编译的怎么知道呢?其实,这个问题的关键在于编译器对静态的局部变量的处理应该是与全局变量差不多的;我们不会认为全局变量每次使用的时候都会被初始化吧。而事实是静态的局部变量和全局变量都是放在静态存储区,那么对他们的处理应该不会有多大的不同吧。当然,这里指的是变量的初始化,对于变量的可见性肯定不同。当然,这些只是我的猜测,并没有实际查阅编译器的处理过程。
2.3 static 关键同时具有“作用域限定符” 的意义。static修饰的全局变量表示该变量只在本文件可见。因此,不应该在别的文件中使用用static修饰的其他文件中的变量
2.4 register 关键字指明将变量存储于寄存器中,所以必须是局部变量。register 只是请求寄存器变量,但不一定请求成功;register 变量必须是cpu寄存器可以接受的值;不能用&运算符获取register变量的地址,因为寄存器本身不存在地址,& 符号是取内存中的地址,因此不在内存中的地址是不行的;对于实时性要求比较高的时候才使用这个register变量。注:并不是每个变量都可以用上面三个关键字修饰,这要看变量定义的位置,比如你不能在全局变量前面加上auto 或 register,这样做在内存的角度看是很可笑的。因为,全局变量本身是分配在静态内存区,用auto修饰是在分配在栈中。这样定义的话,编译器到底是分配在哪呢?当然,还有更深的矛盾,比如如果是分配在栈中,我们知道只要函数调用结束,那么在栈中的将销毁。而定义为全局变量,本文件或别的文件的函数都可以使用该变量,但是这个变量已经随函数销毁了,也就是不可使用,程序将崩溃。当然,这些都只是我的假设,事实也不存在这些假设,只是幻想下编译器如果允许这样做有什么后果。
有了加强的变量的定义,那么现在开始学怎么高效的控制代码 的执行,c语言中定义了if ,switch,do ,while ,for 来控制代码块的执行,它们也是c实现模块编程的基础
3 if ,esle ,switch,do ,while ,for 分析
3.1 分支语句分析 --if
3.1.1 if语句中零值的比较的注意点,什么这个要说呢?因为在c语言里面条件判断 0为假 ,非0为真,也就是 -1 也是真。
bool类型的比较:在c语言里面没有标准的bool类型,bool类型是编译器厂商自己定义的,对于TRUE可能定义不同的值,有的可能定义为-1,所以如果在代码中这样写 if(b == 1),如果条件为真b的值是-1,但是if语句是不成立的。bool类型变量应该直接出现在条件中或是与TRUE 或 FALESE比较。
普通变量和0值比较:0值应该出现比较负号左边 --- 这只是工程经验,这是防止“== ”写成“=”,这种错误是难以排出的。
float 型变量与0值比较:不能和零值字节相比较。因为浮点数在内存中存储是不精确的,0值可能是0.000030 或是-0.000030。因此,需要定义一个区间来表示0。比如,假设#defineZERO 0.000040 那么在条件中可以这样判断 if (-ZERO < value < ZERO )条件成立的话,则认为是0值。
3.1.2 if 与 else的搭配
# else 不能独立存在,并且总是与最近的if搭配。这里容易犯一个错误,看下面一例:
if ( 0 < n){ ... }; else{ ... }这里在if语句后面加了个分号,此时if将独立存在;else找不到匹配的if,由于else是不能单独存在,所以代码错误;解决这个问题的另一方法是按照linux的代码风格(也是c的创作人推荐的风格),如下:
if ( 0 < n ){ ... }else{ ... }这样写中不会犯错误了吧!
# else 语句后可以连接其它的if语句。
3.2 分支语句分析 --switch
3.2.1 对应单个条件对应多个分支的情形,每个case语句分支要有break,否则导致分支重叠;default语句有必要加上,以处理特殊情况。
case语句中的值只能是整型或字符型,不能够有浮点数。
case 语句排列的顺序 分析:
按字母或数字的顺序排列各条语句 ---- 这样就不会漏下某项了,即使漏了也容易找出来
正常情况下放在前面,异常情况放在后面 ---- 这样做程序将更有效率哦
default只能用在真正的处理默认情况 ---- 这样做是让别人觉的你没有忘记处理如果条件都不满足该怎么做
if 和 switch 对比:# if 语句实用于需要“按片”进行判断 的情形中 ---- 按片可以理解为按区间
# switch语句实用于需要对各个离散的值进行分别判断的情形中
# if 语句可以完全从功能上代替switch语句,但是switch语句不能代替if语句
# switch 语句对于多分支判断的情形更加简洁
3.3 循环语句
基本工作方式:通过条件表达式判断是否执行循环体,条件表达式遵循if语句表达式的原则
3.2.1 do、while、for语句的区别 :
# do 语句先执行后判断,循环体至少执行一次
# while语句先判断后执行,循环体可能不执行
# for 语句先判断后执行,相比while更简洁,可读性相对较高
3.2.2 break 和 continue的区别
# break 表示终止循环的执行
# continue 表示终止本次的循环体,进入下一次循环的执行
那么switch 能否用continue关键字呢 ? 为什么?
答:不能。switch本身就不是循环, break设计的本身是用来跳出一个块,而不单纯是跳出循环。
3.2.3 do while 的妙用
do{
语句块
break; // 注意这里的break 是跳出这个语句块
语句块
break;
}while(0);
作用:1 保证宏定义的使用者能够无编译无错误的使用它,它不对使用者做任何假设。 (将相当于我们的软件不能假设用户按正确的方式操作,事实你要用户那么做,用户偏没有那样做。所以我们写代码的时候要有足够的“容错”)
2 可以使程序只退出当前块,程序继续往下面执行。在这个块中可以检查分配的内存有没有成功,输入参数对不对,如果检测失败那么退出这个程序块,后面依赖这个这两个参数的代码块
不会执行
例 1:
#define SPFE_FREE(P) do{ free(p); p = NULL;} while(0) if ( NULL != p) SPFE_FREE(P) else /* do something*/会被展开为:
if ( NULL != p) do{ free(p); p = NULL;} while(0); else /* do something*/如果去掉do ... while(0),这会展开为:
if ( NULL != p) free(p); p = NULL; else /* do something*/展开的代码存在两个问题:
# if语句后有两个语句,这样else分支没有对应的if,编译失败
# 如果没有else分支,SAFE_FREE中的第二个语句无论if测试是否都会执行。
此时,我们可能想free(p); p = NULL;加上{};编程{free(p); p = NULL;}不就行了么。这样的结果是:
if ( NULL != P) SAPE_FREE(P) else /* do someting*/但是,在c语言中,每个语句后面加上;号是个约定俗成的习惯,加上;号后,代码展开为:
if ( NULL != P) { free(p); p = NULL}; else /* do something */这样else有没有if了。或许,你会想到在if 后面用户自己加上{}不就可以了么,确实是可以的。但是,宏的定义着不能假设每个代码编辑者都会这样做,只要有一个没这样,代码就错误的。所以,我们不能假设用户按我们的要求做,我们得假定用户会怎么用,这样代码才不会有问题。
do{}while(0);在linux内核代码中经常使用
例 2:
int func(int n) { int i = 0; int ret = 0; int* p = (int*)malloc(sizeof(int) * n); do { if( NULL == p ) break; if( n < 0 ) break; for(i=0; i<n; i++) { p[i] = i; printf("%d\n", p[i]); } ret = 1; }while(0); free(p); return ret; }这里假设 n如果小于0,那么代码将退出do块,接着释放分配的指针。如果不用do{}while(0),那么n如果小于0,你将使用return 退出,这样的结果是退出了整个函数,而分配的指针没有释放,这样的后果是大量调用该函数必然导致内存的溢出。
4 goto ,void ,extern
4.1 goto语句
高手潜规则:禁用goto
项目经验: 程序质量与goto的出现次数成反比
最后的判决:将goto打入冷宫
4.1.2 goto的副作用
跳过了本来应该执行 的语句,比如释放指针的操作。分析虽然如此,linux内核中却大量使用了goto语句,但是这些使用都是用在错误处理。
4.2 void
4.2.1 void的意义
如果函数没有返回值,那么应该声明为void
如果函数没有参数,那么应该声明为void
不存在void变量,c 语言没有定义void 对应对大的内存,不能是void定义变量
void 指针的意义
c语言规定只有相同类型的指针才可以相互赋值
void ×指针作为左值用于接收“任意”类型的指针
void ×指针作为右值赋值给其它指针需要强制类型转换
4.3 extern中隐藏的意义
extern 用于声明外部定义的变量 和 函数
extern 用于高速编译器用c方式编译 比如extern “c” {
int f(int a,intb)
{
}
}
这样f函数将按照标准c进行编译
4.4 为sizeof正名
sizeof是编译器内置的关键字,不是函数
sizeof 用于“计算”相应实体所占的内存的大小
sizeof的值在编译期就已经确定
5 volatile const
5.1 const
在c语言中const修饰的变量是只读的,其本质还是变量
const修饰的变量会在内存占用空间
本质上const只对编译器有用,在运行时无用。因此,可以通过指针修改其值。
const int × p ,int × const p区别:当const出现在×号左边时,指针指向的数据为常量
但const出现在×号右边时,指针本身为常量
5.1.2 const 修饰函数参数 表示在函数体内不希望改变参数的值
修饰的函数返回值表示返回值不可改变,多用于返回指针的情形
5.2 深藏不露的volatile -- 警告编译器每次去内存中去变量值
思考:const 和 volatile 是否可修饰同一个变量?例如const volatile int i = 0这个时候i具有什么属性?编译器如何处理这个变量。
可以同时修改
6 struct ,union
6.1 struct
对于一个空结构体的大小,不同编译器对这个处理不同,这是由于c 标准的灰色地带。
c 语言中结构体的最后一个元素可以是大小未知的数组
c 语言可以由结构体产生柔性数组
#include <stdio.h> #include <malloc.h> typedef struct _soft_array { int len; int array[]; }soft_array; int main(void) { int i = 0; printf(" %d \n",sizeof(soft_array)); soft_array *sa = (soft_array *)malloc(sizeof(soft_array) + sizeof(int)*10); sa->len = 10; for(i = 0; i<sa->len; i++){ sa->array[i] = i + 1; } for(i = 0; i<sa->len; i++){ printf(" %d \n",sa->array[i]); // sa->array[i] = i + 1; } free(sa); return 0; }
6.2 union
union只分配最大域的空间,所有域共享这个空间
7 enum,typedef
7.1 enum: 默认常量在前一个值的基础上依次加 1
变量只能取定义时的离散值
与#define 定义的常量的区别: #define宏常量只是简单的进行值替换,枚举是真正意义上的常量
#define宏常量无法被调试,枚举常量可以
#define宏无类型信息,枚举常量是一种特定类型的常量
7.2 typedef 的意义 typerename 命名更合适。
用于给一个已经存在的数据类型重命名,并没有产生新的类型,重定义的类型不能进行unsigned 和 signed扩展。