C语言的深入了解.docx
- 文档编号:9320532
- 上传时间:2023-02-04
- 格式:DOCX
- 页数:31
- 大小:34.17KB
C语言的深入了解.docx
《C语言的深入了解.docx》由会员分享,可在线阅读,更多相关《C语言的深入了解.docx(31页珍藏版)》请在冰豆网上搜索。
C语言的深入了解
嵌入式C语言的深入了解
本文并不一本详细C语言教材,只是重点分析与嵌入式相关的各种C语言要求,这些要求构成了一般C程序与嵌入式C程序开发者之间重大区别.本课程是作为嵌入式开发的前导课程来准备
C语言类型修饰符.
C语言支持可以见到6种类型修饰符,分别是:
auto,const,register,static,volatile,extern.
?
局部变量除非显式指明为static,否则默认为auto,所以一般不会在代码中使用类型修饰符auto.
?
extern用于声明全局变量或全局函数,使用者要注意extern的后的语句并不能产生实际定义.
?
Register是通知编译器,尽可能让所修饰的变量以寄存器的形式使用.在较新的编译器,优化器可以合理的分配寄存器,所以一般不会在代码中使用类型修饰符register.但是在单片机或嵌入式开发,有时强制使用register还是有实际含意.
?
而const,static,volatile有一些特定的需求
Static
static用于声明局域变量或局域函数,一个具有模式化编程的程序员应该多使用static,将私有数据和私有函数用static保护起来,不被外界访问.
这一关键字起的作用类似于C++的private.
1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2)在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。
它是一个本地的全局变量。
3)在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。
那就是,这个函数被限制在声明它的模块的本地范围内使用。
在一个大型项目里,static被广泛使用,如果一个嵌入式C程度员很少使用static,说明他至少对于数据保护的好处认识不够.
Const
Const也是应该广泛的关键字,表示后面定义有可能是一个不变的常量,但是跟各种数据类型定义组合,能演变出复杂含义.
Const主要有如下应用情况
1.声明后面是一个常量
2.如果跟指针组合,将形成复杂的组合情况
3.作函数参数数的修饰符.表示不能修改这一参数的值.主要是指针
4.作函数返回值,表示调用函数不能修改这一参数数的值.主要是指针
Const让人感到复杂是的,他的位置可以与被修饰的数据类型互换!
其它的修饰符没有这样用法.这样换一般情况下是等效的,
如constintc1=5;等效于intconstc1=5;但是,我说是但是,如果数据类型是一个指针,互换一样位置表示完全不同含义,所以const这
一个用法经常是面试官用来折磨嵌入式C面试者的法宝之一.
参看如下定义
intb=500;
constint*a=&b;[1]
intconst*a=&b;[2]
int*consta=&b;[3]
constint*consta=&b;[4]
这种定义要看const的位置来确定他的用法.
const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
因此,[1]和[2]的情况相同,都是指针所指向的内容为常量(const放在变量声明符的位置无关),这种情况下不允许对指针指向内容进行更改操作,如不能*a=3;[3]为指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如a++是错误的;[4]为指针本身和指向的内容均为常量。
const的初始化
Const在运行时不能修改值,所以只能在定义时进行初始化.
作为参数和返回值的const修饰符
其实,不论是参数还是返回值,道理都是一样的,参数传入时候和函数返回的时候,初始化const变量
1修饰参数的const,如voidfun0(constA*a);表示a的指向内容不准修改的.
2修饰返回值的const,如constA*fun2();表示a指南内容不准备修改
C++中函数整体不准修改
如果const位于参数列表后面,表示整个函数里不准修改任何值,全部是只读的
intvoidgetValue()const
{
//inti;
//i++;错误,不能在const修饰的函数修改值
returnvalue;
}
Const的好处是
?
关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。
如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。
(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。
)
?
通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
?
合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。
简而言之,这样可以减少bug的出现。
Const一大副作用是,在函数调用时,你必须要把参数调成const所要求的形式才能被编译调用.有时这样花很大功夫才能做到
Volatile
volatile关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如
操作系统、硬件或者其它线程等。
遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行
优化,从而可以提供对特殊地址的稳定访问。
volatile区分C程序员和嵌入式系统程序员的最基本的问题。
搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。
不懂得volatile的内容将会带来灾难。
intvolatilenVint;
当要求使用volatile声明的变量的值时,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。
而且读取的数据立刻被保存。
由于访问寄存器的速度要快过RAM,所以编译器一般都会作减少存取外部RAM的优化,对于变量,编译器发现两次从i读数据的代码之间的代码没有对i进行过操作,它会自动把上次读的数据放在b中。
而不是重新从i里面读取.这样以来,如果i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile可以保证对特殊地址的稳定访问。
volatile指出i是随时可能发生变化的,每次使用它的时候必须从i的地址中读取.
?
;存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义,如并行设备的硬件寄存器(如:
状态寄存器)
?
;中断服务程序中修改的供其它程序检测的变量需要加volatile,如一个中断服务子程序中会访问到的非自动变量(Non-automaticvariables)
?
;多任务环境下各任务间共享的数据,如多线程应用中被几个任务共享的变量
复杂数据定义声明
用变量a给出下面的定义
a)一个整型数(Aninteger)
b)一个指向整型数的指针(Apointertoaninteger)
c)一个指向指针的的指针,它指向的指针是指向一个整型数(Apointertoapointertoanintege)r
d)一个有10个整型数的数组(Anarrayof10integers)
e)一个有10个指针的数组,该指针是指向一个整型数的。
(Anarrayof10pointerstointegers)
f)一个指向有10个整型数数组的指针(Apointertoanarrayof10integers)
g)一个指向函数的指针,该函数有一个整型参数并返回一个整型数(Apointertoafunctionthattakesanintegerasanargumentandreturnsaninteger)
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数(Anarrayoftenpointerstofunctionsthattakeanintegerargumentandreturnaninteger)
a)inta;//Aninteger
b)int*a;//Apointertoaninteger
c)int**a;//Apointertoapointertoaninteger
d)inta[10];//Anarrayof10integers
e)int*a[10];//Anarrayof10pointerstointegers
f)int(*a)[10];//Apointertoanarrayof10integers
g)int(*a)(int);//Apointertoafunctionathattakesanintegerargumentandreturnsaninteger
h)int(*a[10])(int);//Anarrayof10pointerstofunctionsthattakeanintegerargumentandreturnaninteger
这是考虑数据定义的基础题
混合编程
C与汇编的混合编程
C语言通过_asm{}来直接在代码嵌入汇编,有底层驱动时,为了效率,经常直接嵌入式汇编,一但使用ASM,你程序的可移植性就大大减弱了,这是开发者要注意问题
C++引用C的函数库
C++引用C的库函数,必须要对C的库函数用,extern"C"对函数数声明进行修饰.
这一样,任何一个写得比较正规的库函数头文件都是形如下结构
#ifndef__MY_LIB_H__
#define__MY_LIB_H__
#ifdef__cplusplus
extern"C"{
#endif
/*这一段是函数声明的*/
#ifdef__cplusplus
}
#endif
#endif/*__MY_LIB_H__*/
其中,#ifndef……这一段是防止头文件被重复使用
#ifdef__cplusplus
extern"C"{
#endif
#ifdef__cplusplus
}
#endif
表示{}之间的函数是可以供C++函数直接引用.而_cplusplus宏是编译内置宏,表示是C++编译器,这样C编器可以跳过这样的定义的.以防止发误解
extern"C"包含双重含义,从字面上即可得到:
首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
C的位域(bitfields)
C语言位域的最大作用是简化位操作,可以让开发者以直观的方式来操作某一位.如下列定义
structbs
{
inta:
7;
intb:
2;
intc:
1;
};
表示用一个整数的前8位表示a,用一个整数的2位表示b,用一个整数的1位的来表示c,
位域定义不能超过数据定义类型的最大位,
如struct{
chara:
9;//char最大值为8位
intb:
33;//int的最大值为32,不能超过其中定义值
}
位域有如下特殊定义,
1)只要不超过数据定义最大值,多个位域可以定义一个数据单位里,如下是合法,定义,也是常用定义
StructPC_PIN{
Charbit0:
1,
bit1:
1,
Bit2:
1,
Bit3:
1,
Bit4:
1,
Bit5:
1,
Bit6:
1,
Bit7:
1;
}
2)位域可以采用’匿名',定义,这样程度就不能使用这些位,这样定义纯粹是起占位用.
structfoo1{
inta:
1;
int:
2;
shortc:
1;
};
上例中,在a和c中有一个2位的匿名占位
structbs
{
unsigneda:
4
unsigned:
0/*空域*/
unsignedb:
4/*从下一单元开始存放*/
unsignedc:
4
}
在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
位域占位计算有点复杂
1.定义位域不足一个数据位的,按一个完整数据算
StructtagA{
Inta:
3;
Intb;
};
Sizeof()值是8,因为a仍然按4位来核算.
2.如果连续定义几点相同类型.而位域总合不超过类型总位数长度的,会被编译器设为一个合并为一个位域定义
如structtagA{
inta:
1;
intb:
2;
inc;3
}
等同于structtagB{
Inta:
1,
b:
2,
c:
3;
};
Sizeof()的长度都是4,因为tagA的各个成员加起长度都没有超过32,所以仍然为4
3.aaa
位域的被广泛应用于8位单片机编程中.因为一个8位寄存器刚好是一个char的宽度,因为可以定义一个8个位位域来对寄存器的各个位进行存取.这样程序比较简单并且好理解.但在32位CPU应用反而不广泛,因为32CPU的寄存器是为32位宽度,正好是一个int的宽度,但int在不同CPU中的表示顺序位是不一致的.在不同字节序CPU里定义的位域,有一些不样,换句话说,定义这样位域需要定义两套类型.如ip的头定义.
以下取自Linux中,linux/ip.h
structiphdr{
#ifdefined(__LITTLE_ENDIAN_BITFIELD)
__u8ihl:
4,
version:
4;
#elifdefined(__BIG_ENDIAN_BITFIELD)
__u8version:
4,
ihl:
4;
#else
#error"Pleasefix
#endif
__u8tos;
__u16tot_len;
__u16id;
__u16frag_off;
__u8ttl;
__u8protocol;
__u16check;
__u32saddr;
__u32daddr;
/*Theoptionsstarthere.*/
};
使用反而不如位操作定义方便,因此32位CPU使用位域有一些麻烦
字节对齐
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但为为了CPU访问数据的快速,通常都要求数据存放的地址是有一定规律的.比如在32位CPU上,一般要求变量地址都是基于4位,这样可以保证CPU用一次的读写周期就可以读取变量.不按4位对齐,如果变量刚好跨4位的吗,这样需要CPU两个读写周期.效率自然低下.因此,在现代的编译器都会自动把复合数据定义按4位对齐,以保证CPU以最快速度读取,如下例
(gccversion3.2.2编译器(32位x86平台))
structA{
inta;
charb;
shortc;
};
结构体A中包含了4字节长度的int一个,1字节长度的char一个和2字节长度的short型数据一个。
所以A用到的空间应该是7字节。
但是因为编译器要对数据成员在空间上进行对齐。
所以使用sizeof(strcutA)值为8。
现在把该结构体调整成员变量的顺序。
structB{
charb;
inta;
shortc;
};
这时候同样是总共7个字节的变量,但是sizeof(structB)的值却是12,因为b+a已经超过32,编译无法合并在一个空间,干脆分配了4个byte空间.
但这样分配空间一个不好结果就是,各个成员的相对起移地址,的位移是不确定的.在不同CPU的有不同结果.这样对于经常处理网络包的嵌入式C程序员是一个大问题
如下例,假设某人设计不一不按4位对齐的包协议,如下前一个1位表示版本号,第2-5位表示,包序号,第6-7位表示端口号.
这时发来一个网络包{0x01,0x12,0x23,0x10,0x10,0xFF,0xFF}
如果你设计一个结构,去记取
Structprotocol{
Charversion;
Intserialno;
Shortport;
};
用这个结构去强制读取前面包buffer,会得出一个错误的结果!
包的serial号和port号都不会取到0x10101223和0xFFFF.为什么?
因为编译器已经把这个结构优化,造成包结构成员的相对位移发生变化.对应位的值就不是原来的值.
为此,有必要告诉编译器,我这个结构是不需要进行字节对齐优化的.在不同编译器,有不同方法来优化.
一般是采用#pragmapack(n)编译指令来告诉编译器,后面的结构采用n字节对齐,如果n=1表示,不对齐,强制要原始地址排列.网络包通常需要这样定义
用#pragmapack()取消上一次字节对齐指令,恢复成缺省对齐编译.
#pragmapack
(2)/*指定按2字节对齐*/
structC{
charb;
inta;
shortc;
};
#pragmapack()/*取消指定对齐,恢复缺省对齐*/
在gcc还有一种特殊编译属性__attribute__((packed)
可以强制让结构不对齐,即采用1字节对齐模式.
以下是两种不对齐的方法.
1)结构内部成员的pack
structfoo
{
chara;
intb__attribute__((packed));
};
2)整个结构的pack
structfoo
{
chara;
intb;
}__attribute__((packed));
数字字节序
数字字节序是指一个4字节整数或2字节的短整数内存的存储字节顺序.CPU自发明以来,存储数字的顺序就分为两大流派.两派典型代表分别是
那就是Motorola的PowerPC系列CPU和Intel的x86系列CPUPowerPC系列采用bigendian方式存储数据,而x86系列则采用littleendian方式存储数据。
ARM系统则是骑墙派,两种模式都可以实现,当然一次只能实现一种.
bigendian是指低地址存放最高有效字节(MSB),而littleendian则是低地址存放最低有效字节(LSB),即常说的低位在先,高位在后。
.
数字0x12345678在两种不同字节序CPU中的存储顺序如下所示:
BigEndian
低地址高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|12|34|56|78|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
LittleEndian
低地址高地址
----------------------------------------->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|78|56|34|12|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
从上面两图可以看出,采用bigendian方式存储数据是符合我们人类的思维习惯的。
而littleendian则反其道而行之.
C/C++语言编写的程序里数据存储顺序是跟编译平台所在的CPU相关的,而JAVA编写的程序则唯一采用bigendian方式来存储数据。
用如下程序可以检测数字字节序
intam_big_endian()
{
longone=1;
return!
(*((char*)(&one)));
}
这一段代码原理如下,&one是取自长整型的地址,这时将长整型看一个BUFFER,强制转换成一个char*指针.并对第一个字节取值.
如果正常顺序,即big_endian,1应该在最低位,这时取第一个字符值必然是0,即
*((char*)(&one)))==0,
但是是反序的Littlenendian,第一位就是1,
这时*((char*)(&one)))==1,
网络字节序与本机字节序
为了防止在不同字节序的机器传数字发生混乱,TCP/IP各层协议将字节序定义为统一定义为bigendian,这称为网络字节序,而相对的,CPU本身采用字节存储顺序称本机序,在发包前,TCP/IP要求必须转换成网络序,无论这个CPU采用何种字节序来存储.在收包后,也必段将网络序转本机序,方可对数字进行正常处理.为此,每个socket库要求实现如下四个函数(或宏)
htons():
将16位无符号整数从本地字节序转换成网络字节序
htonl():
将32位无符号整数从本地字节序转换成网络字节序
ntohs():
将16位无符号整数从网络字节序转换成本地字节序
ntohl():
将32位无符号整数从网络字节序转换成本地字节序
C程序的内存分区
一个C程序运行后,内存分成5个区,他们分别是代码区,数据区,未初始化数据区,栈.堆,,用英文术语分别是text,data(initialized),bss,stack,heap
一般程序员无需了解哪各部分是如何构造.但做为嵌入式程序员很多时候经常要与一个特殊程序打交道,如引导程序,或汇编代码.之类,一般要手工调整入口地址之类活动,因此必段要对C程序的分段有清晰的了解.也是面试者根其喜欢考的话题之一.
1)代码段(textsegment):
存放CPU执行的机器指令(machineinstructions)。
也就是存储你的程序代码编译后的机器代码,在内存中,这一段是只读的.
2)初始化数据段/数据段(initializeddatasegment/datasegmen,包含静态初始化的数据,所以有初值的全局变量和static变量在data区。
如全局变量intmaxcount=100就分配在这一区.
3)未初始化数据段/bss段,,bss是英文BlockStartedbySymbol的简称,通常是指用来存放程序中未初始化的全局变量的一块内存区域,在程序载入时由内核清0
4)栈段(stack),保存函数的局部变量和参数。
是一种“后进先出”(LastInFirstOut,LIFO)的数据结构,这意味着最后放到栈上的数据,将会是第一个从栈上移走的数据。
对于哪些暂时存贮的信息,和不需要长时间保存的信息来说,LIFO这种数据结构非常理想。
在调用函数或过程后,系统通常会清除栈上保存的局部变量、函数调用信息及其它的信息。
栈另外一个重要的特征是,它的地址空间“向下减少”,即当栈上保存的数据越多,栈的地址就越低。
栈(s
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 语言 深入 了解
![提示](https://static.bdocx.com/images/bang_tan.gif)