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修饰的变量不可直接被修改,正常情况是只读,可以通过指针的形式间接去修改。例如:
1. int *p = &a; 2. //之后再通过对指针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修饰指针
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));
这样我们为结构体指针 p 分配了一块内存。用 p->item[n]就能简单地访问可变长元素,但是这时候我们再用 sizeof(*p)测试结构体的大小,发现仍然为 4。这是为什么?其实柔性数组就相当于模子,在定义这个结构体的时候,模子的大小就已经确定不包含柔性数组的内存大小。它与结构体没有什么关系,只是一个编外人员,不占结构体的编制。
上面既然用 malloc 函数分配了内存,肯定就需要用 free 函数来释放内存:free(p);
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语言关键字部分已经全部讲完了,如果其中有问题的地方请大家指出来;如果觉得有用的千万不要忘了点赞、关注、收藏哦!我们下期再见!