嵌入式C进阶一 —— 关键字(1)

  突然想复习一下C语言,我是从看谭浩强的《C语言程序设计》入门的,虽然网上有很多人在评论书中的各种错误,但这本书毕竟是很多院校选择的C语言教材,所以普及面还是很广的,但说实话对于C语言的真正了解和这本书真的没啥关系。

  与其要整理C语言的基础知识,不如重拾一下这些年来实践中的总结,一方面健全自己C语言的知识体系,另一方面也能给那些刚刚学习完C语言课程的人提供一个进阶的途径。

  说到编程语言,无论C还是jave,亦或是python,我想都要从他的基础关键字开始说起,每种语言对应的数据类型,作用域修饰符,数据结构等都会有一些自己关键字,所以我们先来看一下C语言中基础的32个关键字。

  • auto     声明自动变量,缺省时编译器默认为auto  
  • static                   声明静态变量
  • register               声明寄存器变量
  • const                   声明只读变量(常变量)
  • volatile                说明变量在程序执行中可能会被隐式地改变
  • char                    声明字符型变量  
  • short                   声明短整型变量
  • int                       声明整形变量
  • long                    声明长整形变量
  • float                    声明浮点型变量
  • double                声明双精度变量
  • signed                声明有符号类型变量
  • unsigned            声明无符号类型变量
  • struct                  声明结构体变量
  • union                  声明联合体(联合数据)变量
  • enum                  声明枚举变量
  • switch                 用于开关语句  
  • case                    开关语句的分支
  • default                开关语句中的“默认”分支
  • break                  跳出当前循环
  • continue              结束当前循环并开启下一轮循环
  • typedef                给数据类型取别名
  • extern                  声明变量是从其他文件中引用来的
  • return                  子程序返回语句(参数可有可无)
  • void                     声明函数无返回值或无参数,声明空类型指针
  • do                        循环语句的循环体
  • while                    循环语句的循环条件
  • if                          条件语句
  • else                      条件语句的否定分支
  • for                        循环语句
  • goto                     无条件跳转语句  
  • sizeof                   计算对象所占内存空间大小

  既然是进阶篇,肯定是要讲一下重点突出的内容,分别列举一下重点关键字进行介绍和举例。

register   

  被register修饰的变量叫做寄存器变量,顾名思义,该变量是直接存储在CPU的内部寄存器中的,访问速度要比内存中的数据快很多。这个在Embedded C中最常用的方法是修饰单片机中的外设寄存器地址,可以查看STM32固件库中对于GPIO,TIM等寄存器的定义。

  其他的应用场景比如有一个很大的循环,其中有几个需要频繁操作的变量,这时使用register进行修饰将大大提高效率。   

  但是即使强如register也是有着自己的限制的,必须是寄存器所接受的类型才能被修饰,即register变量必须是一个单个的值,而且他的长度也要小于等于整形的长度。register变量虽说并不是每时每刻都在寄存器中,最后也是要写入内存的,但是我们也并不能保证某个时刻它就在内存里,所以我们也不能对register变量取地址。

static  圈起来,这个要考 

静态关键字static在c语言中主要有两个作用,而在C++中获得了扩展(第三个作用),关于C++中的扩展,我们在C++进阶中介绍。 

作用一:static可修饰得变量分为局部变量全局变量,修饰后也就对应两种静态变量。他们都存储在内存的静态区。   

  • 静态局部变量:在函数中定义的静态局部变量则只有在该函数程序块内可以使用,其他函数是不可见的,不得使用。同时由于它和静态全局变量都存在于内存的静态区中而非栈中,所以函数结束之后值也不会销毁,函数下次运行时会继续使用这个值,这是非常重要的特性。   
  • 静态全局变量:static修饰全局变量的作用域仅限于被定义的文件中(从定义之处开始到当前文件结尾),而且在其他文件中即使使用extern关键字也不能使用。在当前文件中,静态全局变量定义之处之前的代码行也同样不能直接使用它,需要使用extern关键字才能使用。所以一般我们都将静态全局变量定义在文件的顶端。 

作用二:static修饰函数,在函数前面加上static使得函数成为静态函数。这时该函数的作用域则仅限于本文件中,所以又成为内部函数。这样做的好处是不同文件中的函数命名可以重名,不用担心编译器报错。

const 这个非常重要,要考

  这个关键字非常重要,有许多人对它的理解都有错误。先来看一句话:被const修饰的变量叫做常量。这句话对吗?

  加上const确实不能直接修改它的值了,这不是和常量一样吗?这么说是不准确的。该值在编译的时候不能被使用,因为这时编译器并不知道它存储的内容。因此和常量还是有小小的区别的,也许叫做只读变量比较好。

具体验证:请看如下代码:

const int MAX = 100;
int Array[MAX];

  这串代码在.cpp文件中可以顺利运行,但是在.c文件中不可行。证明了这个MAX它其实还是一个变量,绝非常量。由于c++对const进行了扩展使得在.cpp文件中这些代码成为可行。前面我们说到它叫做只读变量是因为编译期间它的值不被编译器所知道,这是怎么回事呢?编译器通常不给只读变量分配存储空间,而是直接将他们保存在符号表里。它就成为了一个编译期间的值,没有了存储和读内存的操作,效率变得很高。

关于const修饰对象的问题看下下面经典的考题:

const int *p;        //p的指向可以改变,但是指针p指向的对象的内存不能改变。
int const *p;        //同上
int *const p;        //p的指向不可以改变,但是指针p指向的对象的内存可以改变。
const int* const p;    //p的值和p指向的对象都不可改变。

  分辨这些情况,只需要首先将数据类型屏蔽(把int不看,编译器解析的时候也是这么做的),我们看const离哪个近他就修饰谁,*表示指针指向的对象或内存。p表示指针本身。

volatile   重点,必考

  和const类似,它也是一种类型修饰符,在Embedded C中很常见,它主要是用来处理编译器的优化,告诉编译器不要修饰我的变量。该关键字表示他所修饰的变量是一个极易被未知因素所改变的,编译器则对访问该代码不再进行优化,从而提供对特殊地址的稳定访问。

具体例子如下:

int i = 10;  int j = i;          //(1)    
int k = i;         //(2)

  通常情况编译器会对代码进行优化:在(1)(2)这两条语句中,i并未做左值。此时编译器就认为i的值没有改变,所以在语句(1)将i从内存中取出赋给j之后这个值并未被丢掉,而是直接继续给k赋值了。编译器不会再生成从内存中再次取出i的值的代码,这样就提高了效率,也就是所谓的优化。接下来再看下面的代码:

volatile i = 10;int j = i;            //(3)
int k = i;           //(4)

  这样的话,(4)语句则会再次从内存中i的地址中读取一次值赋给k。省去了优化的步骤,实现了稳定的访问。

  通常在中断服务函数,多线程变成中我们需要使用volatile修饰全局变量,防止编译器优化导致全局变量的变化不被中断或者其他线程感知。

char short int long float double

  需要注意的是在32位系统上,short占2个字节,int和long都是4个字节,char是1个字节,只有double和longlong是8字节。当然在不同平台系统上也有差异,需要自己用sizeof进行测试。

sizeof

  首先有一个问题:sizeof是函数还是关键字呢?有人可能看见sizeof后面加上了一对括号,认为它是函数。其实sizeof(i) 和 sizeof i (竟然可以这样用?)效果是一样的,结果也是一样没有任何问题。

  那么sizeof(int) 和 sizeof int也是一样的吗?答案是否定的,因为sizeof int 根本就无法通过编译。从前面一个例子我们了解了没有括号也能完成功能说明它并不是一个函数!毫无疑问它是一个关键字,那么sizeof int是什么意思?在int数据类型前加一个关键字,类比下来就好像double int类型的变量一样,这是个什么变量?所以一般来说,sizeof在计算变量所占空间大小的时候可以去掉括号,但计算类型大小的时候不能省略。在此处省略也只是为了让大家明白它不是一个函数,平常我们使用的时候还是乖乖的把括号写上吧,能用括号讲明白的就不要炫技了,常在河边走哪有不湿鞋!

signed unsigned

  它们主要涉及到变量的正负问题。我们都知道一个变量的最高位是符号位,unsigned类型则是将这个最高位也用于表示数据了,所以unsigned类型都是从0开始到某个正数的范围。signed类型和auto类似,也是平常我们看不到但是默认缺省情况都认为一个变量是signed类型的。

goto

  该关键字建议禁用。虽然它可以灵活跳转,但它明显破坏了结构化的设计风格,有时候我们会在函数中需要他来实现根据不同的结果进行跳转,此部分可以使用do while() 来实现,详见其他章节

void

  首先问大家一个问题:在C语言中如果定义一个函数,不给返回类型,例如:

add (int a, int b)
{    
    return a+b;
}

  这么做的结果是什么呢?返回值是不是void类型?还是说根本无法通过编译呢?答案是,编译可以通过,但返回值类型并不是你所想的void而是int。 

  还有另一个问题:定义一个void类型的变量的话,编译器会给他分配多大的内存呢?如果你接触过OOP语言(例如java c#等)一定对抽象类这个概念不陌生。抽象类能定义一个实例吗?明显不能,因为它强调的是一个“抽象”的概念,那必然是不能存在的,void也是同理,是不允许定义void类型的变量的。

extern

  extern可以置于变量和函数之前,来表示这个函数或者变量有可能是来自别的文件的,在这里进行了引用(当然有一种特殊情况,就是我们在上面讲过的本文件中在静态全局变量前的代码行若想使用这个变量,也得使用extern来引用)

  OK,我们已经重点讲述了大部分关键字,而且举了一些容易犯错的例子,其他的如数据结构相关的struct,union 等相关的内容我们在下一篇 嵌入式C进阶一 —— 关键字(2)中继续总结。

You may also like...

发表评论

电子邮件地址不会被公开。