嵌入式C/C++语言精华文章集锦

嵌入式C/C++语言精华文章集锦
C/C+语言struct 深层探索............................................................................2
C++中extern "C"含义深层探索........................................................................7
C 语言高效编程的几招...............................................................................11
想成为嵌入式程序员应知道的0x10 个基本问题.........................................................15
C 语言嵌入式系统编程修炼...........................................................................22
C 语言嵌入式系统编程修炼之一:背景篇............................................................22
C 语言嵌入式系统编程修炼之二:软件架构篇........................................................24
C 语言嵌入式系统编程修炼之三:内存操作..........................................................30
C 语言嵌入式系统编程修炼之四:屏幕操作..........................................................36
C 语言嵌入式系统编程修炼之五:键盘操作..........................................................43
C 语言嵌入式系统编程修炼之六:性能优化..........................................................46
C/C++语言void 及void 指针深层探索.................................................................50
C/C++语言可变参数表深层探索.......................................................................54
C/C++数组名与指针区别深层探索.....................................................................60
C/C++程序员应聘常见面试题深入剖析(1) ..............................................................62
C/C++程序员应聘常见面试题深入剖析(2) ..............................................................67
一道著名外企面试题的抽丝剥茧......................................................................74
C/C++结构体的一个高级特性――指定成员的位数.......................................................78
C/C++中的近指令、远指针和巨指针...................................................................80
从两道经典试题谈C/C++中联合体(union)的使用......................................................81
基于ARM 的嵌入式Linux 移植真实体验................................................................83
基于ARM 的嵌入式Linux 移植真实体验(1)――基本概念...........................................83
基于ARM 的嵌入式Linux 移植真实体验(2)――BootLoader .........................................96
基于ARM 的嵌入式Linux 移植真实体验(3)――操作系统..........................................111
基于ARM 的嵌入式Linux 移植真实体验(4)――设备驱动..........................................120
基于ARM 的嵌入式Linux 移植真实体验(5)――应用实例..........................................135
深入浅出Linux 设备驱动编程.......................................................................144
1.Linux 内核模块..............................................................................144
2.字符设备驱动程序...........................................................................146
3.设备驱动中的并发控制.......................................................................151
4.设备的阻塞与非阻塞操作.....................................................................157
2
C/C+语言struct 深层探索
出处:PConline 作者:宋宝华
1. struct 的巨大作用
面对一个人的大型C/C++程序时,只看其对struct 的使用情况我们就可以对其编写者的编程经
验进行评估。因为一个大型的C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结
构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用
struct 是区别一个开发人员是否具备丰富开发经历的标志。
在网络协议、通信控制、嵌入式系统的C/C++编程中,我们经常要传送的不是简单的字节流(char
型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
经验不足的开发人员往往将所有需要传送的内容依顺序保存在char 型数组中,通过指针偏移的
方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序
就要进行非常细致的修改。
一个有经验的开发者则灵活运用结构体,举一个例子,假设网络或控制协议中需要传送三种报
文,其格式分别为packetA、packetB、packetC:
struct structA
{
int a;
char b;
};
struct structB
{
char a;
short b;
};
struct structC
{
int a;
char b;
float c;
}
优秀的程序设计者这样设计传送的报文:
struct CommuPacket
{
3
int iPacketType; //报文类型标志
union //每次传送的是三种报文中的一种,使用union
{
struct structA packetA; struct structB packetB;
struct structC packetC;
}
};
在进行报文传送时,直接传送struct CommuPacket 一个整体。
假设发送函数的原形如下:
// pSendData:发送字节流的首地址,iLen:要发送的长度
Send(char * pSendData, unsigned int iLen);
发送方可以直接进行如下调用发送struct CommuPacket 的一个实例sendCommuPacket:
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
假设接收函数的原形如下:
// pRecvData:发送字节流的首地址,iLen:要接收的长度
//返回值:实际接收到的字节数
unsigned int Recv(char * pRecvData, unsigned int iLen);
接收方可以直接进行如下调用将接收到的数据保存在struct CommuPacket 的一个实例recvCommuPacket 中:
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
接着判断报文类型进行相应处理:
switch(recvCommuPacket. iPacketType)
{
case PACKET_A:
… //A 类报文处理
break;
case PACKET_B:
… //B 类报文处理
break;
case PACKET_C:
… //C 类报文处理
break;
}
以上程序中最值得注意的是
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
中的强制类型转换:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再转化为char 型指针,
这样就可以直接利用处理字节流的函数。
利用这种强制类型转化,我们还可以方便程序的编写,例如要对sendCommuPacket 所处内存初始化为0,可以这
样调用标准库函数memset():
memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));
2. struct的成员对齐
Intel、微软等公司曾经出过一道类似的面试题:
#include <iostream.h>
4
#pragma pack(8)
struct example1
{
short a;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
#pragma pack()
int main(int argc, char* argv[])
{
example2 struct2;
cout << sizeof(example1) << endl;
cout << sizeof(example2) << endl;
cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;
return 0;
}
问程序的输入结果是什么?
答案是:
8
16
4
不明白?还是不明白?下面一一道来:
2.1 自然对界
struct 是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float 等)的变量,也可以是
一些复合数据类型(如array、struct、union 等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,
以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各
个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size 最大的成员对齐。
例如:
struct naturalalign
{
char a;
short b;
char c;
};
在上述结构体中,size 最大的是short,其长度为2 字节,因而结构体中的char 成员a、c 都以2 为单位对齐,
sizeof(naturalalign)的结果等于6;
如果改为:
struct naturalalign
5
{
char a;
int b;
char c;
};
其结果显然为12。
2.2 指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:
· 使用伪指令#pragma pack (n),编译器将按照n 个字节对齐;
· 使用伪指令#pragma pack (),取消自定义字节对齐方式。
注意:如果#pragma pack (n)中指定的n 大于结构体中最大成员的size,则其不起作用,结构体
仍然按照size 最大的成员进行对界。
例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
#pragma pack ()
当n 为4、8、16 时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。而当n 为2
时,其发挥了作用,使得sizeof(naturalalign)的结果为6。
在VC++ 6.0 编译器中,我们可以指定其对界方式(见图1),其操作方式为依次选择projetct >
setting > C/C++菜单,在struct member alignment 中指定你要的对界方式。
图1 在VC++ 6.0 中指定对界方式
6
另外,通过__attribute((aligned (n)))也可以让所作用的结构体成员对齐在n 字节边界上,但
是它较少被使用,因而不作详细讲解。
2.3 面试题的解答
至此,我们可以对Intel、微软的面试题进行全面的解答。
程序中第2 行#pragma pack (8)虽然指定了对界为8,但是由于struct example1 中的成员最大
size 为4(long 变量size 为4),故struct example1 仍然按4 字节对界,struct example1 的size
为8,即第18 行的输出结果;
struct example2 中包含了struct example1,其本身包含的简单数据成员的最大size 为2(short
变量e),但是因为其包含了struct example1,而struct example1 中的最大成员size 为4,struct
example2 也应以4 对界,#pragma pack (8)中指定的对界对struct example2 也不起作用,故19 行的
输出结果为16;
由于struct example2 中的成员以4 为单位对界,故其char 变量c 后应补充3 个空,其后才是
成员struct1 的内存空间,20 行的输出结果为4。
3. C 和C++间struct 的深层区别
在C++语言中struct 具有了“类” 的功能,其与关键字class 的区别在于struct 中成员变量
和函数的默认访问权限为public,而class 的为private。
例如,定义struct 类和class 类:
struct structA
{
char a;

}
class classB
{
char a;

}
则:
structA a;
a.a = 'a'; //访问public 成员,合法
classB b;
b.a = 'a'; //访问private 成员,不合法
许多文献写到这里就认为已经给出了C++中struct 和class 的全部区别,实则不然,另外一点
需要注意的是:
C++中的struct 保持了对C 中struct 的全面兼容(这符合C++的初衷——“a better c”),
因而,下面的操作是合法的:
//定义struct
struct structA
{
char a;
char b;
int c;
};
7
structA a = {'a' , 'a' ,1}; // 定义时直接赋初值
即struct 可以在定义的时候直接以{ }对其成员变量赋初值,而class 则不能,在经典书目
《thinking C++ 2nd edition》中作者对此点进行了强调。
4. struct 编程注意事项
看看下面的程序:
1. #include <iostream.h>
2. struct structA
3. {
4. int iMember;
5. char *cMember;
6. };
7. int main(int argc, char* argv[])
8.{
9. structA instant1,instant2;
10. char c = 'a';
11. instant1.iMember = 1;
12. instant1.cMember = &c;
13. instant2 = instant1;
14. cout << *(instant1.cMember) << endl;
15. *(instant2.cMember) = 'b';
16. cout << *(instant1.cMember) << endl;
17. return 0;
}
14 行的输出结果是:a
16 行的输出结果是:b
Why?我们在15 行对instant2 的修改改变了instant1 中成员的值!
原因在于13 行的instant2 = instant1 赋值语句采用的是变量逐个拷贝,这使得instant1 和
instant2 中的cMember 指向了同一片内存,因而对instant2 的修改也是对instant1 的修改。
在C 语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将2 个实例中的
指针型成员指向了同一片内存。
在C++语言中,当结构体中存在指针型成员时,我们需要重写struct 的拷贝构造函数并进行“=”
操作符重载。
C++中extern "C"含义深层探索
作者:宋宝华 e-mail:21cnbao@21cn.com 出处:太平洋电脑网
1.引言
C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C 语言的全局变量和函数
所采用的编译和连接方式与C 语言完全相同。作为一种欲与C 兼容的语言,C++保留了一部分过程式语
言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。
8
但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C
有明显的不同。
2.从标准头文件说起
某企业曾经给出如下的一道面试题:
面试题
为什么标准头文件都有类似以下的结构?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
#endif
#endif /* __INCvxWorksh */
分析
显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用
是防止该头文件被重复引用。
那么
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
的作用又是什么呢?我们将在下文一一道来。
3.深层揭密extern "C"
extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,
被它修饰的目标是“C”的。让我们来详细解读这两重含义。
(1)被extern "C"限定的函数或变量是extern 类型的;
extern 是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,
其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
extern int a;
仅仅是一个变量的声明,其并不是在定义变量a,并未为a 分配内存空间。变量a 在所有模块中作
为一种全局变量只能被定义一次,否则会出现连接错误。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern 声明。
例如,如果模块B 欲引用该模块A 中定义的全局变量和函数时只需包含模块A 的头文件即可。这样,
模块B 中调用模块A 中的函数时,在编译阶段,模块B 虽然找不到该函数,但是并不会报错;它会在
连接阶段中从模块A 编译生成的目标代码中找到此函数。
与extern 对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个
函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
(2)被extern "C"修饰的变量和函数是按照C 语言方式编译和连接的;
未加extern “C”声明时的编译方式
9
首先看看C++中对类似C 的函数是怎样编译的。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C 则不支持。函数被C++编译后在符
号库中的名字与C 语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C 编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int 之类
的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled
name”)。_foo_int_int 这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来
实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )
编译生成的符号是不相同的,后者为_foo_int_float。
同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成
员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,
也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。
未加extern "C"声明时的连接方式
假设在C++中,模块A 的头文件如下:
// 模块A 头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
int foo( int x, int y );
#endif
在模块B 中引用该函数:
// 模块B 实现文件 moduleB.cpp
#include "moduleA.h"
foo(2,3);
实际上,在连接阶段,连接器会从模块A 生成的目标文件moduleA.obj 中寻找_foo_int_int 这样
的符号!
加extern "C"声明后的编译和连接方式
加extern "C"声明后,模块A 的头文件变为:
// 模块A 头文件 moduleA.h
#ifndef MODULE_A_H
#define MODULE_A_H
extern "C" int foo( int x, int y );
#endif
在模块B 的实现文件中仍然调用foo( 2,3 ),其结果是:
(1)模块A 编译生成foo 的目标代码时,没有对其名字进行特殊处理,采用了C 语言的方式;
(2)连接器在为模块B 的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。
如果在模块A 中函数声明了foo 为extern "C"类型,而模块B 中包含的是extern int foo( int x,
int y ) ,则模块B 找不到模块A 中的函数;反之亦然。
所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生
都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么
做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):
实现C++与C 及其它语言的混合编程。
明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。
4.extern "C"的惯用法
10
(1)在C++中引用C 语言中的函数和变量,在包含C 语言头文件(假设为cExample.h)时,需进
行下列处理:
extern "C"
{
#include "cExample.h"
}
而在C 语言的头文件中,对其外部函数只能指定为extern 类型,C 语言中不支持extern "C"声明,
在.c 文件中包含了extern "C"时会出现编译语法错误。
笔者编写的C++引用C 函数例子工程中包含的三个文件的源代码如下:
/* c 语言头文件:cExample.h */
#ifndef C_EXAMPLE_H
#define C_EXAMPLE_H
extern int add(int x,int y);
#endif
/* c 语言实现文件:cExample.c */
#include "cExample.h"
int add( int x, int y )
{
return x + y;
}
// c++实现文件,调用add:cppFile.cpp
extern "C"
{
#include "cExample.h"
}
int main(int argc, char* argv[])
{
add(2,3);
return 0;
}
如果C++调用一个C 语言编写的.DLL 时,当包括.DLL 的头文件或声明接口函数时,应加extern "C"
{ }。
(2)在C 中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C 语言中不
能直接引用声明了extern "C"的该头文件,应该仅将C 文件中将C++中定义的extern "C"函数声明为
extern 类型。
笔者编写的C 引用C++函数例子工程中包含的三个文件的源代码如下:
//C++头文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++实现文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
11
{
return x + y;
}
/* C 实现文件 cFile.c
/* 这样会编译出错:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
如果深入理解了第3 节中所阐述的extern "C"在编译和连接阶段发挥的作用,就能真正理解本节
所阐述的从C++引用C 函数和C 引用C++函数的惯用法。对第4 节给出的示例代码,需要特别留意各个
细节。
C 语言高效编程的几招
编写高效简洁的C 语言代码,是许多软件工程师追求的目标。本文就工作中的一些体会和经验做相关的阐述,不对的地方

各位指教。
第1 招:以空间换时间
计算机程序中最大的矛盾是空间和时间的矛盾,那么,从这个角度出发逆向思维来考虑程序的效率问题,我们就有了解决
问题
的第1 招--以空间换时间。
例如:字符串的赋值。
方法A,通常的办法:
#define LEN 32
char string1 [LEN];
memset (string1,0,LEN);
strcpy (string1,"This is an example!!"
方法B:
const char string2[LEN]="This is an example!"
char*cp;
cp=string2;
(使用的时候可以直接用指针来操作。)
从上面的例子可以看出,A 和B 的效率是不能比的。在同样的存储空间下,B 直接使用指针就可以操作了,而A 需要调用
两个字符函数才能完成。B 的缺点在于灵活性没有A 好。在需要频繁更改一个字符串内容的时候,A 具有更好的灵活性;
如果采用方法B,则需要预存许多字符串,虽然占用了 大量的内存,但是获得了程序执行的高效率。
如果系统的实时性要求很高,内存还有一些,那我推荐你使用该招数。
12
该招数的边招--使用宏函数而不是函数。举例如下:
方法C:
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
int BIT_MASK (int_bf)
{
return ((IU<<(bw##_bf))-1)<<(bs##_bf);
}
void SET_BITS(int_dst,int_bf,int_val)
{
_dst=((_dst) & ~ (BIT_MASK(_bf)))I/
(((_val)<<<(bs##_bf))&(BIT_MASK(_bf)))
}
SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumb
er);
方法D:
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
#define bmMCDR2_ADDRESS BIT_MASK
(MCDR2_ADDRESS)
#define BIT_MASK(_bf)(((1U<<(bw##_bf))-1)<<
(bs##_bf)
#define SET_BITS(_dst,_bf,_val)/
((_dst)=((_dst)&~(BIT_MASK(_bf)))I
(((_val)<<(bs##_bf))&(BIT_MASK(_bf))))
SET_BITS(MCDR2,MCDR2_ADDRESS,RegisterNumb
er);
函数和宏函数的区别就在于,宏函数占用了大量的空间,而函数占用了时间。大家要知道的是,函数调用是要使用系统的
栈来保存数据的,如果编译器里有栈检查选项,一般在函数的头会嵌入一些汇编语句对当前栈进行检查;同时,CPU 也要
在函数调用时保存和恢复当前的现场,进行压栈和弹栈操作,所以,函数调用需要一些CPU 时间。而宏函数不存在这个问
题。宏函数仅仅作为预先写好的代码嵌入到当前程序,不会产生函数调用,所以仅仅是占用了空间,在频繁调用同一个宏
函数的时候,该现象尤其突出。
D 方法是我看到的最好的置位操作函数,是ARM 公司源码的一部分,在短短的三行内实现了很多功能,几乎涵盖了所有
的位操作功能。C 方法是其变体,其中滋味还需大家仔细体会。
第2 招:数学方法解决问题
现在我们演绎高效C 语言编写的第二招--采用数学方法来解决问题。
数学是计算机之母,没有数学的依据和基础,就没有计算机的发展,所以在编写程序的时候,采用一些数学方法会对程序
的执行效率有数量级的提高。
举例如下,求1~100 的和。
方法E
int I,j;
方法F
int I;
13
for (I=1; I<=100; I++){
j+=I;
}
I=(100*(1+100))/2
这个例子是我印象最深的一个数学用例,是我的饿计算机启蒙老师考我的。当时我只有小学三年级,可惜我当时不知道用
公式Nx(N+1)/2 来解决这个问题。方法E 循环了100 次才解决问题,也就是说最少用了100 个赋值、100 个判断、200
个加法(I 和j);而方法F 仅仅用了1 个加法、1 个乘法、1 次除法。效果自然不言而喻。所以,现在我在编程序的时候,
更多的是动脑筋找规律,最大限度地发挥数学的威力来提高程序运行的效率。
第3 招:使用位操作
实现高效的C 语言编写的第三招--使用位操作,减少除法和取模的运算。
在计算机程序中,数据的位是可以操作的最小数据单位,理论上可以用“位运算”来完成所有的运算和操作。一般的位操作
是用来控制硬件的,或者做数据变换使用,但是,灵活的位操作可以有效地提高程序运行的效率。举例台如下:
方法G
int I,J;
I=257/8;
J=456%32;
方法H
int I,J;
I=257>>3;
J=456-(456>>4<<4);
在字面上好象H 比G 麻烦了好多,但是,仔细查看产生的汇编代码就会明白,方法 G 调用了基本的取模函数和除法函数,
既有函数调用,还有很多汇编代码和寄存器参与运算;而方法H 则仅仅是几句相关的汇编,代码更简洁、效率更高。当然,
由于编译器的不同,可能效率的差距不大,但是,以我目前遇到的MS C,ARM C 来看,效率的差距还是不小。相关汇编
代码就不在这里列举了。
运用这招需要注意的是,因为CPU 的不同而产生的问题。比如说,在PC 上用这招编写的程序,并在PC 上调试通过,在
移植到一个16 位机平台上的时候,可能会产生代码隐患。所以只有在一定技术进阶的基础下才可以使用这招。
第4 招:汇编嵌入
高效C 语言编程的必杀技,第四招--嵌入汇编。
“在熟悉汇编语言的人眼里,C 语言编写的程序都是垃圾”。这种说法虽然偏激了一些,但是却有它的道理。汇编语言是效
率最高的计算机语言,但是,不可能靠着它来写一个操作系统吧?所以,为了获得程序的高效率,我们只好采用变通的方
法--嵌入汇编、混合编程。
举例如下,将数组一赋值给数组二,要求每一个字节都相符。char string1[1024], string2[1024];
14
方法I
int I;
for (I=0; I<1024; I++)
*(string2+I)=*(string1+I)
方法J
#int I;
for(I=0; I<1024; I++)
*(string2+I)=*(string1+I);
#else
#ifdef_ARM_
_asm
{
MOV R0,string1
MOV R1,string2
MOV R2,#0
loop:
LDMIA R0!,[R3-R11]
STMIA R1!,[R3-R11]
ADD R2,R2,#8
CMP R2, #400
BNE loop
}
#endif
方法I 是最常见的方法,使用了1024 次循环;方法J 则根据平台不同做了区分,在ARM 平台下,用嵌入汇编仅用128
次循环就完成了同样的操作。这里有朋友会说,为什么不用标准的内存拷贝函数呢?这是因为在源数据里可能含有数据为
0 的字节,这样的话,标准库函数会提前结束而不会完成我们要求的操作。这个例程典型应用于LCD 数据的拷贝过程。根
据不同的CPU,熟练使用相应的嵌入汇编,可以大大提高程序执行的效率。
虽然是必杀技,但是如果轻易使用会付出惨重的代价。这是因为,使用了嵌入汇编,便限制了程序的可移植性,使程序在
不同平台移植的过程中,卧虎藏龙、险象环生!同时该招数也与现代软件工程的思想相违背,只有在迫不得已的情况下才
可以采用。切记。
使用C 语言进行高效率编程,我的体会仅此而已。在此已本文抛砖引玉,还请各位高手共同切磋。希望各位能给出更好的
方法,大家一起提高我们的编程技巧。
摘自《单片机与嵌入式系统应用》2003.9
15
想成为嵌入式程序员应知道的0x10 个基本问题
-|endeaver 发表于 2006-3-8 16:16:00
C 语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织了许多这种测试,在这过程中我意识到这些测
试能为带面试者和被面试者提供许多有用信息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。
从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对ANSI 标准细节的知识而不是技术技巧而
设计吗?这个愚蠢的问题吗?如要你答出某个字符的ASCII 值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这标志着出题
者也许花时间在微机上而不上在嵌入式系统上。如果上述任何问题的答案是"是"的话,那么我知道我得认真考虑我是否应该去做这份工作。
从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解应试者C 语言的水平。不管怎么样,看一下这人如何
回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口呢,还是表现
出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们的测试成绩一样有用。
有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给正在找工作的人一点帮住。这些问题都是我这些年实
际碰到的。其中有些题很难,但它们应该都能给你一点启迪。
这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问
题的偏好,每个问题没有分配分数,如果选择这些考题为你所用,请自行按你的意思分配分数。
预处理器(Preprocessor)
1 . 用预处理指令#define 声明一个常数,用以表明1 年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
•; #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
•; 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
•; 意识到这个表达式将使一个16 位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
•; 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
2 . 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
这个测试是为下面的目的而设的:
•; 标识#define 在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C 的一部分,宏是方便产生嵌入代码的唯一方
法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
•; 三重条件操作符的知识。这个操作符存在C 语言中的原因是它使得编译器能产生比if-then-else 更优化的代码,了解这个用法是很重要的。
•; 懂得在宏中小心地把参数用括号括起来
•; 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
3. 预处理器标识#error 的目的是什么?
如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C 语言课本的附录去找出象
这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。
死循环(Infinite loops)
4. 嵌入式系统中经常要用到无限循环,你怎么样用C 编写死循环呢?
16
这个问题用几个解决方案。我首选的方案是:
while(1)
{
?}
一些程序员更喜欢如下方案:
for(;;)
{
?}
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们
这样做的基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。
第三个方案是用 goto
Loop:
...
goto Loop;
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN 程序员。
数据声明(Data declarations)
5. 用变量a 给出下面的定义
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10 个整型数的数组( An array of 10 integers)
e) 一个有10 个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10 个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument
and returns an integer)
h)一个有10 个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions t
hat take an integer argument and return an integer )
答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer
人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了
一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。
应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为
什么出准备呢?
17
Static
6. 关键字static 的作用是什么?
这个简单的问题很少有人能回答完全。在C 语言中,关键字static 有三个明显的作用:
•; 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
•; 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变
量。
•; 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然
不懂得本地化数据和代码范围的好处和重要性。
Const
7.关键字const 有什么含意?
我只要一听到被面试者说:"const 意味着常数",我就知道我正在和一个业余者打交道。去年Dan Saks 已经在他的文章里完全概括了const
的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const 能做什么和不能做什么.如果你从没有
读到那篇文章,只要能说出const 意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道
更详细的答案,仔细读一下Saks 的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:
下面的声明都是什么意思?
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
/******/
前两个的作用是一样,a 是一个常整型数。第三个意味着a 是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个
意思a 是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a 是一个指向常整
型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留
下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关
键字const 呢?我也如下的几下理由:
•; 关键字const 的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果
你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const 的程序员很少会留下的垃圾让别人来清
理的。)
•; 通过给优化器一些附加的信息,使用关键字const 也许能产生更紧凑的代码。
•; 合理地使用关键字const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug
的出现。
Volatile
8. 关键字volatile 有什么含意?并给出三个不同的例子。
一个定义为volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到
这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile 变量的几个例子:
18
•; 并行设备的硬件寄存器(如:状态寄存器)
•; 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
•; 多线程应用中被几个任务共享的变量
回答不出这个问题的人是不会被雇佣的。我认为这是区分C 程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、
RTOS 等等打交道,所有这些都要求用到volatile 变量。不懂得volatile 的内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile 完全的重要性。
•; 一个参数既可以是const 还可以是volatile 吗?解释为什么。
•; 一个指针可以是volatile 吗?解释为什么。
•; 下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}
下面是答案:
•; 是的。一个例子是只读的状态寄存器。它是volatile 因为它可能被意想不到地改变。它是const 因为程序不应该试图去修改它。
•; 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer 的指针时。
•; 这段代码有点变态。这段代码的目的是用来返指针*ptr 指向值的平方,但是,由于*ptr 指向一个volatile 型参数,编译器将产生类似下面
的代码:
int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr 的值可能被意想不到地该变,因此a 和b 可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:
long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}
位操作(Bit manipulation)
9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a 的bit 3,第二个清除a 的bit 3。
在以上两个操作中,要保持其它位不变。
19
对这个问题有三种基本的反应
•; 不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
•; 用bit fields。Bit fields 是被扔到C 语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可
重用的。我最近不幸看到Infineon 为其较复杂的通信芯片写的驱动程序,它用到了bit fields 因此完全对我无用,因为我的编译器用其它的方
式来实现bit fields 的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
•; 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:
#define BIT3 (0x1 << 3)
static int a;
void set_bit3(void) {
a |= BIT3;
}
void clear_bit3(void) {
a &= ~BIT3;
}
一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。
访问固定的内存位置(Accessing fixed memory locations)
10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9 的整型变量的值为0xaa6
6。编译器是一个纯粹的ANSI 编译器。写代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同
而不同。典型的类似代码如下:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;
A more obscure approach is:
一个较晦涩的方法是:
*(int * const)(0x67a9) = 0xaa55;
即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。
中断(Interrupts)
11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展―让标准C 支持中断。具代表事实是,产生了一个新的关键
字__interrupt。下面的代码就使用了__interrupt 关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。
__interrupt double compute_area (double radius)
{
20
double area = PI * radius * radius;
printf("/nArea = %f", area);
return area;
}
这个函数有太多的错误了,以至让人不知从何说起了:
•; ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
•; ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
•; 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在IS
R 中做浮点运算。此外,ISR 应该是短而有效率的,在ISR 中做浮点运算是不明智的。
•; 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,
那么你的被雇用前景越来越光明了。
*****
代码例子(Code examples)
12 . 下面的代码输出是什么,为什么?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
这个问题测试你是否懂得C 语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是
">6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。 因此-20 变成了一个非常大的正整数,所以
该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了
得不到这份工作的边缘。
13. 评价下面的代码片断:
unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */
对于一个int 型不是16 位的处理器为说,上面的代码是不正确的。应编写如下:
unsigned int compzero = ~0;
这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而P
C 机程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应
试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看
到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧...
21
动态内存分配(Dynamic memory allocation)
14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发
生的问题是什么?
这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP 杂志中被广泛地讨论过了(主要是 P.J.
Plauger, 他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一
个小节目:
下面的代码片段的输出是什么,为什么?
char *ptr;
if ((ptr = (char *)malloc(0)) ==
NULL)
else
puts("Got a null pointer");
puts("Got a valid pointer");
这是一个有趣的问题。最近在我的一个同事不经意把0 值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的
代码,该代码的输出是"Got a valid pointer"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的
答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。
Typedef
:
15 Typedef 在C 语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:
#define dPS struct s *
typedef struct s * tPS;
以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s 指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef 更好。思考下面的例子:
dPS p1,p2;
tPS p3,p4;
第一个扩展为
struct s * p1, p2;
.
上面的代码定义p1 为一个指向结构的指,p2 为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。
晦涩的语法
16 . C 语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?
int a = 5, b = 7, c;
c = a+++b;
22
这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作
者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:
c = a++ + b;
因此, 这段代码持行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编
写风格,代码的可读性,代码的可修改性的好的话题。
好了,伙计们,你现在已经做完所有的测试了。这就是我出的C 语言测试题,我怀着愉快的心情写完它,希望你以同样的心情读完它。如果是
认为这是一个好的测试,那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年,我就不做现在的工作,也需要找一个。
Nigel Jones 是一个顾问,现在住在Maryland,当他不在水下时,你能在多个范围的嵌入项目中找到他。 他很高兴能收到读者的来信,他的
email 地址是: NAJones@compuserve.com 。
References
•; Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.
•; Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66
C 语言嵌入式系统编程修炼
C 语言嵌入式系统编程修炼之一:背景篇
作者:宋宝华 更新日期:2005-08-30
来源:yesky.com
不同于一般形式的软件编程,嵌入式系统编程建立在特定的硬件平台上,势必要求其编程语言具备较强的硬件直接操作能
力。无疑,汇编语言具备这样的特质。但是,归因于汇编语言开发过程的复杂性,它并不是嵌入式系统开发的一般选择。
而与之相比,C 语言--一种"高级的低级"语言,则成为嵌入式系统开发的最佳选择。笔者在嵌入式系统项目的开发过程中,
一次又一次感受到C 语言的精妙,沉醉于C 语言给嵌入式开发带来的便利。
图1 给出了本文的讨论所基于的硬件平台,实际上,这也是大多数嵌入式系统的硬件平台。它包括两部分:
(1) 以通用处理器为中心的协议处理模块,用于网络控制协议的处理;
(2) 以数字信号处理器(DSP)为中心的信号处理模块,用于调制、解调和数/模信号转换。
本文的讨论主要围绕以通用处理器为中心的协议处理模块进行,因为它更多地牵涉到具体的C 语言编程技巧。而DSP
编程则重点关注具体的数字信号处理算法,主要涉及通信领域的知识,不是本文的讨论重点。
23
着眼于讨论普遍的嵌入式系统C 编程技巧,系统的协议处理模块没有选择特别的CPU,而是选择了众所周知的CPU 芯
片--80186,每一位学习过《微机原理》的读者都应该对此芯片有一个基本的认识,且对其指令集比较熟悉。80186 的字长
是16 位,可以寻址到的内存空间为1MB,只有实地址模式。C 语言编译生成的指针为32 位(双字),高16 位为段地址,低
16 位为段内偏移,一段最多64KB。
图1 系统硬件架构
协议处理模块中的FLASH 和RAM 几乎是每个嵌入式系统的必备设备,前者用于存储程序,后者则是程序运行时指令及
数据的存放位置。系统所选择的FLASH 和RAM 的位宽都为16 位,与CPU 一致。
实时钟芯片可以为系统定时,给出当前的年、月、日及具体时间(小时、分、秒及毫秒),可以设定其经过一段时间即
向CPU 提出中断或设定报警时间到来时向CPU 提出中断(类似闹钟功能)。
NVRAM(非易失去性RAM)具有掉电不丢失数据的特性,可以用于保存系统的设置信息,譬如网络协议参数等。在系统
掉电或重新启动后,仍然可以读取先前的设置信息。其位宽为8 位,比CPU 字长小。文章特意选择一个与CPU 字长不一致
的存储芯片,为后文中一节的讨论创造条件。
UART 则完成CPU 并行数据传输与RS-232 串行数据传输的转换,它可以在接收到[1~MAX_BUFFER]字节后向CPU 提出中
断,MAX_BUFFER 为UART 芯片存储接收到字节的最大缓冲区。
键盘控制器和显示控制器则完成系统人机界面的控制。
以上提供的是一个较完备的嵌入式系统硬件架构,实际的系统可能包含更少的外设。之所以选择一个完备的系统,是
为了后文更全面的讨论嵌入式系统C 语言编程技巧的方方面面,所有设备都会成为后文的分析目标。
嵌入式系统需要良好的软件开发环境的支持,由于嵌入式系统的目标机资源受限,不可能在其上建立庞大、复杂的开
发环境,因而其开发环境和目标运行环境相互分离。因此,嵌入式应用软件的开发方式一般是,在宿主机(Host)上建立开
发环境,进行应用程序编码和交叉编译,然后宿主机同目标机(Target)建立连接,将应用程序下载到目标机上进行交叉调
试,经过调试和优化,最后将应用程序固化到目标机中实际运行。
CAD-UL 是适用于 x86 处理器的嵌入式应用软件开发环境,它运行在Windows 操作系统之上,可生成x86 处理器的目标
24
代码并通过PC 机的COM 口(RS-232 串口)或以太网口下载到目标机上运行,如图2。其驻留于目标机FLASH 存储器中的
monitor 程序可以监控宿主机Windows 调试平台上的用户调试指令,获取CPU 寄存器的值及目标机存储空间、I/O 空间的内
容。
图2 交叉开发环境
后续章节将从软件架构、内存操作、屏幕操作、键盘操作、性能优化等多方面阐述C 语言嵌入式系统的编程技巧。软
件架构是一个宏观概念,与具体硬件的联系不大;内存操作主要涉及系统中的FLASH、RAM 和NVRAM 芯片;屏幕操作则涉及
显示控制器和实时钟;键盘操作主要涉及键盘控制器;性能优化则给出一些具体的减小程序时间、空间消耗的技巧。
在我们的修炼旅途中将经过25 个关口,这些关口主分为两类,一类是技巧型,有很强的适用性;一类则是常识型,在
理论上有些意义。
C 语言嵌入式系统编程修炼之二:软件架构篇
作者:宋宝华 更新日期:2005-07-22
模块划分
模块划分的"划"是规划的意思,意指怎样合理的将一个很大的软件划分为一系列功能独立的部分合作完成系统的需求。
C 语言作为一种结构化的程序设计语言,在模块的划分上主要依据功能(依功能进行划分在面向对象设计中成为一个错误,
牛顿定律遇到了>相对论),C 语言模块化程序设计需理解如下概念:
(1) 模块即是一个.c 文件和一个.h 文件的结合,头文件(.h)中是对于该模块接口的声明;
(2) 某模块提供给其它模块调用的外部函数及数据需在.h 中文件中冠以extern 关键字声明;
(3) 模块内的函数和全局变量需在.c 文件开头冠以static 关键字声明;
(4) 永远不要在.h 文件中定义变量!定义变量和声明变量的区别在于定义会产生内存分配的操作,是汇编阶段的概
念;而声明则只是告诉包含该声明的模块在连接阶段从其它模块寻找外部函数和变量。如:
/*module1.h*/
int a = 5; /* 在模块1 的.h 文件中定义int a */
/*module1 .c*/
#include "module1.h" /* 在模块1 中包含模块1 的.h 文件 */
25
/*module2 .c*/
#include "module1.h" /* 在模块2 中包含模块1 的.h 文件 */
/*module3 .c*/
#include "module1.h" /* 在模块3 中包含模块1 的.h 文件 */
以上程序的结果是在模块1、2、3 中都定义了整型变量a,a 在不同的模块中对应不同的地址单元,这个世界上从来不
需要这样的程序。正确的做法是:
/*module1.h*/
extern int a; /* 在模块1 的.h 文件中声明int a */
/*module1 .c*/
#include "module1.h" /* 在模块1 中包含模块1 的.h 文件 */
int a = 5; /* 在模块1 的.c 文件中定义int a */
/*module2 .c*/
#include "module1.h" /* 在模块2 中包含模块1 的.h 文件 */
/*module3 .c*/
#include "module1.h" /* 在模块3 中包含模块1 的.h 文件 */
这样如果模块1、2、3 操作a 的话,对应的是同一片内存单元。
一个嵌入式系统通常包括两类模块:
(1)硬件驱动模块,一种特定硬件对应一个模块;
(2)软件功能模块,其模块的划分应满足低偶合、高内聚的要求。
多任务还是单任务
所谓"单任务系统"是指该系统不能支持多任务并发操作,宏观串行地执行一个任务。而多任务系统则可以宏观并
行(微观上可能串行)地"同时"执行多个任务。
多任务的并发执行通常依赖于一个多任务操作系统(OS),多任务OS 的核心是系统调度器,它使用任务控制块(TCB)
来管理任务调度功能。TCB 包括任务的当前状态、优先级、要等待的事件或资源、任务程序码的起始地址、初始堆栈指
针等信息。调度器在任务被激活时,要用到这些信息。此外,TCB 还被用来存放任务的"上下文"(context)。任务的上
下文就是当一个执行中的任务被停止时,所要保存的所有信息。通常,上下文就是计算机当前的状态,也即各个寄存
器的内容。当发生任务切换时,当前运行的任务的上下文被存入TCB,并将要被执行的任务的上下文从它的TCB 中取出,
放入各个寄存器中。
嵌入式多任务OS 的典型例子有Vxworks、ucLinux 等。嵌入式OS 并非遥不可及的神坛之物,我们可以用不到1000
26
行代码实现一个针对80186 处理器的功能最简单的OS 内核,作者正准备进行此项工作,希望能将心得贡献给大家。
究竟选择多任务还是单任务方式,依赖于软件的体系是否庞大。例如,绝大多数手机程序都是多任务的,但也有
一些小灵通的协议栈是单任务的,没有操作系统,它们的主程序轮流调用各个软件模块的处理程序,模拟多任务环境。
单任务程序典型架构
(1)从CPU 复位时的指定地址开始执行;
(2)跳转至汇编代码startup 处执行;
(3)跳转至用户主程序main 执行,在main 中完成:
a.初试化各硬件设备;
b.初始化各软件模块;
c.进入死循环(无限循环),调用各模块的处理函数
用户主程序和各模块的处理函数都以C 语言完成。用户主程序最后都进入了一个死循环,其首选方案是:
while(1)
{
}
有的程序员这样写:
for(;;)
{
}
这个语法没有确切表达代码的含义,我们从for(;;)看不出什么,只有弄明白for(;;)在C 语言中意味着无条件循环才
明白其意。
下面是几个"著名"的死循环:
(1)操作系统是死循环;
(2)WIN32 程序是死循环;
(3)嵌入式系统软件是死循环;
(4)多线程程序的线程处理函数是死循环。
你可能会辩驳,大声说:"凡事都不是绝对的,2、3、4 都可以不是死循环"。Yes,you are right,但是你得不到鲜
27
花和掌声。实际上,这是一个没有太大意义的牛角尖,因为这个世界从来不需要一个处理完几个消息就喊着要OS 杀死它的
WIN32 程序,不需要一个刚开始RUN 就自行了断的嵌入式系统,不需要莫名其妙启动一个做一点事就干掉自己的线程。有
时候,过于严谨制造的不是便利而是麻烦。君不见,五层的TCP/IP 协议栈超越严谨的ISO/OSI 七层协议栈大行其道成为事
实上的标准?
经常有网友讨论:
printf("%d,%d",++i,i++); /* 输出是什么?*/
c = a+++b; /* c=? */
等类似问题。面对这些问题,我们只能发出由衷的感慨:世界上还有很多有意义的事情等着我们去消化摄入的食物。
实际上,嵌入式系统要运行到世界末日。
中断服务程序
中断是嵌入式系统中重要的组成部分,但是在标准C 中不包含中断。许多编译开发商在标准C 上增加了对中断的支持,
提供新的关键字用于标示中断服务程序 (ISR),类似于__interrupt、#program interrupt 等。当一个函数被定义为ISR
的时候,编译器会自动为该函数增加中断服务程序所需要的中断现场入栈和出栈代码。
中断服务程序需要满足如下要求:
(1)不能返回值;
(2)不能向ISR 传递参数;
(3) ISR 应该尽可能的短小精悍;
(4) printf(char * lpFormatString,…)函数会带来重入和性能问题,不能在ISR 中采用。
在某项目的开发中,我们设计了一个队列,在中断服务程序中,只是将中断类型添加入该队列中,在主程序的死循环
中不断扫描中断队列是否有中断,有则取出队列中的第一个中断类型,进行相应处理。
/* 存放中断的队列 */
typedef struct tagIntQueue
{
int intType; /* 中断类型 */
struct tagIntQueue *next;
}IntQueue;
IntQueue lpIntQueueHead;
__interrupt ISRexample ()
{
28
int intType;
intType = GetSystemType();
QueueAddTail(lpIntQueueHead, intType);/* 在队列尾加入新的中断 */
}
在主程序循环中判断是否有中断:
While(1)
{
If( !IsIntQueueEmpty() )
{
intType = GetFirstInt();
switch(intType) /* 是不是很象WIN32 程序的消息解析函数? */
{
/* 对,我们的中断类型解析很类似于消息驱动 */
case xxx: /* 我们称其为"中断驱动"吧? */

break;
case xxx:

break;

}
}
}
按上述方法设计的中断服务程序很小,实际的工作都交由主程序执行了。
硬件驱动模块
一个硬件驱动模块通常应包括如下函数:
(1)中断服务程序ISR
(2)硬件初始化
a.修改寄存器,设置硬件参数(如UART 应设置其波特率,AD/DA 设备应设置其采样速率等);
b.将中断服务程序入口地址写入中断向量表:
/* 设置中断向量表 */
m_myPtr = make_far_pointer(0l); /* 返回void far 型指针void far * */
m_myPtr += ITYPE_UART; /* ITYPE_UART: uart 中断服务程序 */
/* 相对于中断向量表首地址的偏移 */
29
*m_myPtr = &UART _Isr; /* UART _Isr:UART 的中断服务程序 */
(3)设置CPU 针对该硬件的控制线
a.如果控制线可作PIO(可编程I/O)和控制信号用,则设置CPU 内部对应寄存器使其作为控制信号;
b.设置CPU 内部的针对该设备的中断屏蔽位,设置中断方式(电平触发还是边缘触发)。
(4)提供一系列针对该设备的操作接口函数。例如,对于LCD,其驱动模块应提供绘制像素、画线、绘制矩阵、显示
字符点阵等函数;而对于实时钟,其驱动模块则需提供获取时间、设置时间等函数。
C 的面向对象化
在面向对象的语言里面,出现了类的概念。类是对特定数据的特定操作的集合体。类包含了两个范畴:数据和操作。
而C 语言中的struct 仅仅是数据的集合,我们可以利用函数指针将struct 模拟为一个包含数据和操作的"类"。下面的C
程序模拟了一个最简单的"类":
#ifndef C_Class
#define C_Class struct
#endif
C_Class A
{
C_Class A *A_this; /* this 指针 */
void (*Foo)(C_Class A *A_this); /* 行为:函数指针 */
int a; /* 数据 */
int b;
};
我们可以利用C 语言模拟出面向对象的三个特性:封装、继承和多态,但是更多的时候,我们只是需要将数据与行为
封装以解决软件结构混乱的问题。C 模拟面向对象思想的目的不在于模拟行为本身,而在于解决某些情况下使用C 语言编
程时程序整体框架结构分散、数据和函数脱节的问题。我们在后续章节会看到这样的例子。
总结
本篇介绍了嵌入式系统编程软件架构方面的知识,主要包括模块划分、多任务还是单任务选取、单任务程序典型架构、
中断服务程序、硬件驱动模块设计等,从宏观上给出了一个嵌入式系统软件所包含的主要元素。
请记住:软件结构是软件的灵魂!结构混乱的程序面目可憎,调试、测试、维护、升级都极度困难。
小力力力 2005-09-21 17:29
30
C 语言嵌入式系统编程修炼之三:内存操作
作者:宋宝华 更新日期:2005-07-22
数据指针
在嵌入式系统的编程中,常常要求在特定的内存单元读写内容,汇编有对应的MOV 指令,而除C/C++以外的其它编程
语言基本没有直接访问绝对地址的能力。在嵌入式系统的实际调试中,多借助C 语言指针所具有的对绝对地址单元内容的
读写能力。以指针直接操作内存多发生在如下几种情况:
(1) 某I/O 芯片被定位在CPU 的存储空间而非I/O 空间,而且寄存器对应于某特定地址;
(2) 两个CPU 之间以双端口RAM 通信,CPU 需要在双端口RAM 的特定单元(称为mail box)书写内容以在对方CPU 产
生中断;
(3) 读取在ROM 或FLASH 的特定单元所烧录的汉字和英文字模。
譬如:
unsigned char *p = (unsigned char *)0xF000FF00;
*p=11;
以上程序的意义为在绝对地址0xF0000+0xFF00(80186 使用16 位段地址和16 位偏移地址)写入11。
在使用绝对地址指针时,要注意指针自增自减操作的结果取决于指针指向的数据类别。上例中p++后的结果是p=
0xF000FF01,若p 指向int,即:
int *p = (int *)0xF000FF00;
p++(或++p)的结果等同于:p = p+sizeof(int),而p-(或-p)的结果是p = p-sizeof(int)。
同理,若执行:
long int *p = (long int *)0xF000FF00;
则p++(或++p)的结果等同于:p = p+sizeof(long int) ,而p-(或-p)的结果是p = p-sizeof(long int)。
记住:CPU 以字节为单位编址,而C 语言指针以指向的数据类型长度作自增和自减。理解这一点对于以指针直接操作
内存是相当重要的。
函数指针
首先要理解以下三个问题:
31
(1)C 语言中函数名直接对应于函数生成的指令代码在内存中的地址,因此函数名可以直接赋给指向函数的指针;
(2)调用函数实际上等同于"调转指令+参数传递处理+回归位置入栈",本质上最核心的操作是将函数生成的目标代
码的首地址赋给CPU 的PC 寄存器;
(3)因为函数调用的本质是跳转到某一个地址单元的code 去执行,所以可以"调用"一个根本就不存在的函数实体,
晕?请往下看:
请拿出你可以获得的任何一本大学《微型计算机原理》教材,书中讲到,186 CPU 启动后跳转至绝对地址0xFFFF0(对
应C 语言指针是0xF000FFF0,0xF000 为段地址,0xFFF0 为段内偏移)执行,请看下面的代码:
typedef void (*lpFunction) ( ); /* 定义一个无参数、无返回类型的函数指针类型*/
/* 定义一个函数指针,指向CPU 启动后所执行第一条指令的位置*/
lpFunction lpReset = (lpFunction)0xF000FFF0;
lpReset(); /* 调用函数 */
在以上的程序中,我们根本没有看到任何一个函数实体,但是我们却执行了这样的函数调用:lpReset(),它实际上起
到了"软重启"的作用,跳转到CPU 启动后第一条要执行的指令的位置。
记住:函数无它,唯指令集合耳;你可以调用一个没有函数体的函数,本质上只是换
一个地址开始执行指令!
数组vs.动态申请
在嵌入式系统中动态内存申请存在比一般系统编程时更严格的要求,这是因为嵌入式系统的内存空间往往是十分有限
的,不经意的内存泄露会很快导致系统的崩溃。
所以一定要保证你的malloc 和free 成对出现,如果你写出这样的一段程序:
char * function(void)
{
char *p;
p = (char *)malloc(…);
if(p==NULL)
…;
… /* 一系列针对p 的操作 */
return p;
}
在某处调用function(),用完function 中动态申请的内存后将其free,如下:
32
char *q = function();

free(q);
上述代码明显是不合理的,因为违反了malloc 和free 成对出现的原则,即"谁申请,就由谁释放"原则。不满足这个
原则,会导致代码的耦合度增大,因为用户在调用function 函数时需要知道其内部细节!
正确的做法是在调用处申请内存,并传入function 函数,如下:
char *p=malloc(…);
if(p==NULL)
…;
function(p);

free(p);
p=NULL;
而函数function 则接收参数p,如下:
void function(char *p)
{
… /* 一系列针对p 的操作 */
}
基本上,动态申请内存方式可以用较大的数组替换。对于编程新手,笔者推荐你尽量采用数组!嵌入式系统可以以博
大的胸襟接收瑕疵,而无法"海纳"错误。毕竟,以最笨的方式苦练神功的郭靖胜过机智聪明却范政治错误走反革命道路的
杨康。
给出原则:
(1)尽可能的选用数组,数组不能越界访问(真理越过一步就是谬误,数组越过界限就光荣地成全了一个混乱的嵌
入式系统);
(2)如果使用动态申请,则申请后一定要判断是否申请成功了,并且malloc 和free 应成对出现!
关键字const
const 意味着"只读"。区别如下代码的功能非常重要,也是老生长叹,如果你还不知道它们的区别,而且已经在程序
界摸爬滚打多年,那只能说这是一个悲哀:
const int a;
int const a;
const int *a;
int * const a;
33
int const * a const;
(1)关键字const 的作用是为给读你代码的人传达非常有用的信息。例如,在函数的形参前添加const 关键字意味着
这个参数在函数体内不会被修改,属于"输入参数"。在有多个形参的时候,函数的调用者可以凭借参数前是否有const 关
键字,清晰的辨别哪些是输入参数,哪些是可能的输出参数。
(2)合理地使用关键字const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改,这样
可以减少bug 的出现。
const 在C++语言中则包含了更丰富的含义,而在C 语言中仅意味着:"只能读的普通变量",可以称其为"不能改变的
变量"(这个说法似乎很拗口,但却最准确的表达了C 语言中const 的本质),在编译阶段需要的常数仍然只能以#define
宏定义!故在C 语言中如下程序是非法的:
const int SIZE = 10;
char a[SIZE]; /* 非法:编译阶段不能用到变量 */
关键字volatile
C 语言编译器会对用户书写的代码进行优化,譬如如下代码:
int a,b,c;
a = inWord(0x100); /*读取I/O 空间0x100 端口的内容存入a 变量*/
b = a;
a = inWord (0x100); /*再次读取I/O 空间0x100 端口的内容存入a 变量*/
c = a;
很可能被编译器优化为:
int a,b,c;
a = inWord(0x100); /*读取I/O 空间0x100 端口的内容存入a 变量*/
b = a;
c = a;
但是这样的优化结果可能导致错误,如果I/O 空间0x100 端口的内容在执行第一次读操作后被其它程序写入新值,则
其实第2 次读操作读出的内容与第一次不同,b 和c 的值应该不同。在变量a 的定义前加上volatile 关键字可以防止编译
器的类似优化,正确的做法是:
volatile int a;
volatile 变量可能用于如下几种情况:
(1) 并行设备的硬件寄存器(如:状态寄存器,例中的代码属于此类);
34
(2) 一个中断服务子程序中会访问到的非自动变量(也就是全局变量);
(3) 多线程应用中被几个任务共享的变量。
CPU 字长与存储器位宽不一致处理
在背景篇中提到,本文特意选择了一个与CPU 字长不一致的存储芯片,就是为了进行本节的讨论,解决CPU 字长与存
储器位宽不一致的情况。80186 的字长为16,而NVRAM 的位宽为8,在这种情况下,我们需要为NVRAM 提供读写字节、字
的接口,如下:
typedef unsigned char BYTE;
typedef unsigned int WORD;
/* 函数功能:读NVRAM 中字节
* 参数:wOffset,读取位置相对NVRAM 基地址的偏移
* 返回:读取到的字节值
*/
extern BYTE ReadByteNVRAM(WORD wOffset)
{
LPBYTE lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */
return *lpAddr;
}
/* 函数功能:读NVRAM 中字
* 参数:wOffset,读取位置相对NVRAM 基地址的偏移
* 返回:读取到的字
*/
extern WORD ReadWordNVRAM(WORD wOffset)
{
WORD wTmp = 0;
LPBYTE lpAddr;
/* 读取高位字节 */
lpAddr = (BYTE*)(NVRAM + wOffset * 2); /* 为什么偏移要×2? */
wTmp += (*lpAddr)*256;
/* 读取低位字节 */
lpAddr = (BYTE*)(NVRAM + (wOffset +1) * 2); /* 为什么偏移要×2? */
wTmp += *lpAddr;
return wTmp;
}
/* 函数功能:向NVRAM 中写一个字节
*参数:wOffset,写入位置相对NVRAM 基地址的偏移
* byData,欲写入的字节
*/
35
extern void WriteByteNVRAM(WORD wOffset, BYTE byData)
{

}
/* 函数功能:向NVRAM 中写一个字 */
*参数:wOffset,写入位置相对NVRAM 基地址的偏移
* wData,欲写入的字
*/
extern void WriteWordNVRAM(WORD wOffset, WORD wData)
{

}
子贡问曰:Why 偏移要乘以2?
子曰:请看图1,16 位80186 与8 位NVRAM 之间互连只能以地址线A1 对其A0,CPU 本身的A0 与NVRAM 不连接。因此,
NVRAM 的地址只能是偶数地址,故每次以0x10 为单位前进!
图1 CPU 与NVRAM 地址线连接
子贡再问:So why 80186 的地址线A0 不与NVRAM 的A0 连接?
子曰:请看《IT 论语》之《微机原理篇》,那里面讲述了关于计算机组成的圣人之道。
总结
本篇主要讲述了嵌入式系统C 编程中内存操作的相关技巧。掌握并深入理解关于数据指针、函数指针、动态申请内存、
const 及volatile 关键字等的相关知识,是一个优秀的C 语言程序设计师的基本要求。当我们已经牢固掌握了上述技巧后,
我们就已经学会了C 语言的99%,因为C 语言最精华的内涵皆在内存操作中体现。
我们之所以在嵌入式系统中使用C 语言进行程序设计,99%是因为其强大的内存操作能力!
如果你爱编程,请你爱C 语言;
如果你爱C 语言,请你爱指针;
36
如果你爱指针,请你爱指针的指针!
C 语言嵌入式系统编程修炼之四:屏幕操作
作者:宋宝华 更新日期:2005-07-22
汉字处理
现在要解决的问题是,嵌入式系统中经常要使用的并非是完整的汉字库,往往只是需要提供数量有限的汉字供必要的
显示功能。例如,一个微波炉的LCD 上没有必要提供显示"电子邮件"的功能;一个提供汉字显示功能的空调的LCD 上不需
要显示一条"短消息",诸如此类。但是一部手机、小灵通则通常需要包括较完整的汉字库。
如果包括的汉字库较完整,那么,由内码计算出汉字字模在库中的偏移是十分简单的:汉字库是按照区位的顺序排列
的,前一个字节为该汉字的区号,后一个字节为该字的位号。每一个区记录94 个汉字,位号则为该字在该区中的位置。因
此,汉字在汉字库中的具体位置计算公式为:94*(区号-1)+位号-1。减1 是因为数组是以0 为开始而区号位号是以1 为开
始的。只需乘上一个汉字字模占用的字节数即可,即:(94*(区号-1)+位号-1)*一个汉字字模占用字节数,以16*16 点阵字
库为例,计算公式则为:(94*(区号-1)+(位号-1))*32。汉字库中从该位置起的32 字节信息记录了该字的字模信息。
对于包含较完整汉字库的系统而言,我们可以以上述规则计算字模的位置。但是如果仅仅是提供少量汉字呢?譬如几
十至几百个?最好的做法是:
定义宏:
# define EX_FONT_CHAR(value)
# define EX_FONT_UNICODE_VAL(value) (value),
# define EX_FONT_ANSI_VAL(value) (value),
定义结构体:
typedef struct _wide_unicode_font16x16
{
WORD value; /* 内码 */
BYTE data[32]; /* 字模点阵 */
}Unicode;
#define CHINESE_CHAR_NUM … /* 汉字数量 */
字模的存储用数组:
Unicode chinese[CHINESE_CHAR_NUM] =
{
{
EX_FONT_CHAR("业")
EX_FONT_UNICODE_VAL(0x4e1a)
37
{0x04, 0x40, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0x44, 0x46, 0x24, 0x4c, 0x24, 0x48, 0x14, 0x50,
0x1c, 0x50, 0x14, 0x60, 0x04, 0x40, 0x04, 0x40, 0x04, 0x44, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00}
},
{
EX_FONT_CHAR("中")
EX_FONT_UNICODE_VAL(0x4e2d)
{0x01, 0x00, 0x01, 0x00, 0x21, 0x08, 0x3f, 0xfc, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08, 0x21, 0x08,
0x21, 0x08,
0x3f, 0xf8, 0x21, 0x08, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00}
},
{
EX_FONT_CHAR("云")
EX_FONT_UNICODE_VAL(0x4e91)
{0x00, 0x00, 0x00, 0x30, 0x3f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0xff, 0xfe, 0x03, 0x00,
0x07, 0x00,
0x06, 0x40, 0x0c, 0x20, 0x18, 0x10, 0x31, 0xf8, 0x7f, 0x0c, 0x20, 0x08, 0x00, 0x00}
},
{
EX_FONT_CHAR("件")
EX_FONT_UNICODE_VAL(0x4ef6)
{0x10, 0x40, 0x1a, 0x40, 0x13, 0x40, 0x32, 0x40, 0x23, 0xfc, 0x64, 0x40, 0xa4, 0x40, 0x28, 0x40,
0x2f, 0xfe,
0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40, 0x20, 0x40}
}
}
要显示特定汉字的时候,只需要从数组中查找内码与要求汉字内码相同的即可获得字模。如果前面的汉字在数组中以
内码大小顺序排列,那么可以以二分查找法更高效的查找到汉字的字模。
这是一种很有效的组织小汉字库的方法,它可以保证程序有很好的结构。
系统时间显示
从NVRAM 中可以读取系统的时间,系统一般借助NVRAM 产生的秒中断每秒读取一次当前时间并在LCD 上显示。关于时
间的显示,有一个效率问题。因为时间有其特殊性,那就是60 秒才有一次分钟的变化,60 分钟才有一次小时变化,如果
我们每次都将读取的时间在屏幕上完全重新刷新一次,则浪费了大量的系统时间。
一个较好的办法是我们在时间显示函数中以静态变量分别存储小时、分钟、秒,只有在其内容发生变化的时候才更新
其显示。
extern void DisplayTime(…)
{
38
static BYTE byHour,byMinute,bySecond;
BYTE byNewHour, byNewMinute, byNewSecond;
byNewHour = GetSysHour();
byNewMinute = GetSysMinute();
byNewSecond = GetSysSecond();
if(byNewHour!= byHour)
{
… /* 显示小时 */
byHour = byNewHour;
}
if(byNewMinute!= byMinute)
{
… /* 显示分钟 */
byMinute = byNewMinute;
}
if(byNewSecond!= bySecond)
{
… /* 显示秒钟 */
bySecond = byNewSecond;
}
}
这个例子也可以顺便作为C 语言中static 关键字强大威力的证明。当然,在C++语言里,static 具有了更加强大的威
力,它使得某些数据和函数脱离"对象"而成为"类"的一部分,正是它的这一特点,成就了软件的无数优秀设计。
动画显示
动画是无所谓有,无所谓无的,静止的画面走的路多了,也就成了动画。随着时间的变更,在屏幕上显示不同的静止
画面,即是动画之本质。所以,在一个嵌入式系统的LCD 上欲显示动画,必须借助定时器。没有硬件或软件定时器的世界
是无法想像的:
(1) 没有定时器,一个操作系统将无法进行时间片的轮转,于是无法进行多任务的调度,于是便不再成其为一个多
任务操作系统;
(2) 没有定时器,一个多媒体播放软件将无法运作,因为它不知道何时应该切换到下一帧画面;
(3) 没有定时器,一个网络协议将无法运转,因为其无法获知何时包传输超时并重传之,无法在特定的时间完成特
定的任务。
因此,没有定时器将意味着没有操作系统、没有网络、没有多媒体,这将是怎样的黑暗?所以,合理并灵活地使用各
种定时器,是对一个软件人的最基本需求!
在80186 为主芯片的嵌入式系统中,我们需要借助硬件定时器的中断来作为软件定时器,在中断发生后变更画面的显
示内容。在时间显示"xx:xx"中让冒号交替有无,每次秒中断发生后,需调用ShowDot:
39
void ShowDot()
{
static BOOL bShowDot = TRUE; /* 再一次领略static 关键字的威力 */
if(bShowDot)
{
showChar(’:’,xPos,yPos);
}
else
{
showChar(’ ’,xPos,yPos);
}
bShowDot = ! bShowDot;
}
菜单操作
无数人为之绞尽脑汁的问题终于出现了,在这一节里,我们将看到,在C 语言中哪怕用到一丁点的面向对象思想,软
件结构将会有何等的改观!
笔者曾经是个笨蛋,被菜单搞晕了,给出这样的一个系统:
图1 菜单范例
要求以键盘上的"← →"键切换菜单焦点,当用户在焦点处于某菜单时,若敲击键盘上的OK、CANCEL 键则调用该焦点
菜单对应之处理函数。我曾经傻傻地这样做着:
/* 按下OK 键 */
void onOkKey()
{
/* 判断在什么焦点菜单上按下Ok 键,调用相应处理函数 */
Switch(currentFocus)
{
case MENU1:
menu1OnOk();
break;
case MENU2:
menu2OnOk();
break;

}
}
40
/* 按下Cancel 键 */
void onCancelKey()
{
/* 判断在什么焦点菜单上按下Cancel 键,调用相应处理函数 */
Switch(currentFocus)
{
case MENU1:
menu1OnCancel();
break;
case MENU2:
menu2OnCancel();
break;

}
}
终于有一天,我这样做了:
/* 将菜单的属性和操作"封装"在一起 */
typedef struct tagSysMenu
{
char *text; /* 菜单的文本 */
BYTE xPos; /* 菜单在LCD 上的x 坐标 */
BYTE yPos; /* 菜单在LCD 上的y 坐标 */
void (*onOkFun)(); /* 在该菜单上按下ok 键的处理函数指针 */
void (*onCancelFun)(); /* 在该菜单上按下cancel 键的处理函数指针 */
}SysMenu, *LPSysMenu;
当我定义菜单时,只需要这样:
static SysMenu menu[MENU_NUM] =
{
{
"menu1", 0, 48, menu1OnOk, menu1OnCancel
}
,
{
" menu2", 7, 48, menu2OnOk, menu2OnCancel
}
,
{
" menu3", 7, 48, menu3OnOk, menu3OnCancel
}
41
,
{
" menu4", 7, 48, menu4OnOk, menu4OnCancel
}

};
OK 键和CANCEL 键的处理变成:
/* 按下OK 键 */
void onOkKey()
{
menu[currentFocusMenu].onOkFun();
}
/* 按下Cancel 键 */
void onCancelKey()
{
menu[currentFocusMenu].onCancelFun();
}
程序被大大简化了,也开始具有很好的可扩展性!我们仅仅利用了面向对象中的封装思想,就让程序结构清晰,其结
果是几乎可以在无需修改程序的情况下在系统中添加更多的菜单,而系统的按键处理函数保持不变。
面向对象,真神了!
模拟MessageBox 函数
MessageBox 函数,这个Windows 编程中的超级猛料,不知道是多少入门者第一次用到的函数。还记得我们第一次在
Windows 中利用MessageBox 输出 "Hello,World!"对话框时新奇的感觉吗?无法统计,这个世界上究竟有多少程序员学习
Windows 编程是从MessageBox ("Hello,World!",…)开始的。在我本科的学校,广泛流传着一个词汇,叫做"’Hello,World’
级程序员",意指入门级程序员,但似乎"’Hello,World’级"这个说法更搞笑而形象。
图2 经典的Hello,World!
图2 给出了两种永恒经典的Hello,World 对话框,一种只具有"确定",一种则包含"确定"、"取消"。是的,MessageBox
的确有,而且也应该有两类!这完全是由特定的应用需求决定的。
嵌入式系统中没有给我们提供MessageBox,但是鉴于其功能强大,我们需要模拟之,一个模拟的MessageBox 函数为:
42
/******************************************
/* 函数名称: MessageBox
/* 功能说明: 弹出式对话框,显示提醒用户的信息
/* 参数说明: lpStr --- 提醒用户的字符串输出信息
/* TYPE --- 输出格式(ID_OK = 0, ID_OKCANCEL = 1)
/* 返回值: 返回对话框接收的键值,只有两种 KEY_OK, KEY_CANCEL
/******************************************
typedef enum TYPE { ID_OK,ID_OKCANCEL }MSG_TYPE;
extern BYTE MessageBox(LPBYTE lpStr, BYTE TYPE)
{
BYTE keyValue = -1;
ClearScreen(); /* 清除屏幕 */
DisplayString(xPos,yPos,lpStr,TRUE); /* 显示字符串 */
/* 根据对话框类型决定是否显示确定、取消 */
switch (TYPE)
{
case ID_OK:
DisplayString(13,yPos+High+1, " 确定 ", 0);
break;
case ID_OKCANCEL:
DisplayString(8, yPos+High+1, " 确定 ", 0);
DisplayString(17,yPos+High+1, " 取消 ", 0);
break;
default:
break;
}
DrawRect(0, 0, 239, yPos+High+16+4); /* 绘制外框 */
/* MessageBox 是模式对话框,阻塞运行,等待按键 */
while( (keyValue != KEY_OK) || (keyValue != KEY_CANCEL) )
{
keyValue = getSysKey();
}
/* 返回按键类型 */
if(keyValue== KEY_OK)
{
return ID_OK;
}
else
{
return ID_CANCEL;
}
}
43
上述函数与我们平素在VC++等中使用的MessageBox 是何等的神似啊?实现这个函数,你会看到它在嵌入式系统中的
妙用是无穷的。
总结
本篇是本系列文章中技巧性最深的一篇,它提供了嵌入式系统屏幕显示方面一些很巧妙的处理方法,灵活使用它们,
我们将不再被LCD 上凌乱不堪的显示内容所困扰。
屏幕乃嵌入式系统生存之重要辅助,面目可憎之显示将另用户逃之夭夭。屏幕编程若处理不好,将是软件中最不系统、
最混乱的部分,笔者曾深受其害。
C 语言嵌入式系统编程修炼之五:键盘操作
作者:宋宝华 更新日期:2005-07-22
处理功能键
功能键的问题在于,用户界面并非固定的,用户功能键的选择将使屏幕画面处于不同的显示状态下。例如,主画面如
图1:
图1 主画面
当用户在设置XX 上按下Enter 键之后,画面就切换到了设置XX 的界面,如图2:
图2 切换到设置XX 画面
程序如何判断用户处于哪一画面,并在该画面的程序状态下调用对应的功能键处理函数,而且保证良好的结构,是一
个值得思考的问题。
让我们来看看WIN32 编程中用到的"窗口"概念,当消息(message)被发送给不同窗口的时候,该窗口的消息处理函数
44
(是一个callback 函数)最终被调用,而在该窗口的消息处理函数中,又根据消息的类型调用了该窗口中的对应处理函数。
通过这种方式,WIN32 有效的组织了不同的窗口,并处理不同窗口情况下的消息。
我们从中学习到的就是:
(1)将不同的画面类比为WIN32 中不同的窗口,将窗口中的各种元素(菜单、按钮等)包含在窗口之中;
(2)给各个画面提供一个功能键"消息"处理函数,该函数接收按键信息为参数;
(3)在各画面的功能键"消息"处理函数中,判断按键类型和当前焦点元素,并调用对应元素的按键处理函数。
/* 将窗口元素、消息处理函数封装在窗口中 */
struct windows
{
BYTE currentFocus;
ELEMENT element[ELEMENT_NUM];
void (*messageFun) (BYTE keyValue);

};
/* 消息处理函数 */
void messageFunction(BYTE keyValue)
{
BYTE i = 0;
/* 获得焦点元素 */
while ( (element [i].ID!= currentFocus)&& (i < ELEMENT_NUM) )
{
i++;
}
/* "消息映射" */
if(i < ELEMENT_NUM)
{
switch(keyValue)
{
case OK:
element[i].OnOk();
break;

}
}
}
在窗口的消息处理函数中调用相应元素按键函数的过程类似于"消息映射",这是我们从WIN32 编程中学习到的。编程
到了一个境界,很多东西都是相通的了。其它地方的思想可以拿过来为我所用,是为编程中的"拿来主义"。
45
在这个例子中,如果我们还想玩得更大一点,我们可以借鉴MFC 中处理MESSAGE_MAP 的方法,我们也可以学习MFC 定
义几个精妙的宏来实现"消息映射"。
处理数字键
用户输入数字时是一位一位输入的,每一位的输入都对应着屏幕上的一个显示位置(x 坐标,y 坐标)。此外,程序还
需要记录该位置输入的值,所以有效组织用户数字输入的最佳方式是定义一个结构体,将坐标和数值捆绑在一起:
/* 用户数字输入结构体 */
typedef struct tagInputNum
{
BYTE byNum; /* 接收用户输入赋值 */
BYTE xPos; /* 数字输入在屏幕上的显示位置x 坐标 */
BYTE yPos; /* 数字输入在屏幕上的显示位置y 坐标 */
}InputNum, *LPInputNum;
那么接收用户输入就可以定义一个结构体数组,用数组中的各位组成一个完整的数字:
InputNum inputElement[NUM_LENGTH]; /* 接收用户数字输入的数组 */
/* 数字按键处理函数 */
extern void onNumKey(BYTE num)
{
if(num==0|| num==1) /* 只接收二进制输入 */
{
/* 在屏幕上显示用户输入 */
DrawText(inputElement[currentElementInputPlace].xPos,
inputElement[currentElementInputPlace].yPos, "%1d", num);
/* 将输入赋值给数组元素 */
inputElement[currentElementInputPlace].byNum = num;
/* 焦点及光标右移 */
moveToRight();
}
}
将数字每一位输入的坐标和输入值捆绑后,在数字键处理函数中就可以较有结构的组织程序,使程序显得很紧凑。
整理用户输入
继续第2 节的例子,在第2 节的onNumKey 函数中,只是获取了数字的每一位,因而我们需要将其转化为有效数据,譬
如要转化为有效的XXX 数据,其方法是:
/* 从2 进制数据位转化为有效数据:XXX */
void convertToXXX()
{
46
BYTE i;
XXX = 0;
for (i = 0; i < NUM_LENGTH; i++)
{
XXX += inputElement[i].byNum*power(2, NUM_LENGTH - i - 1);
}
}
反之,我们也可能需要在屏幕上显示那些有效的数据位,因为我们也需要能够反向转化:
/* 从有效数据转化为2 进制数据位:XXX */
void convertFromXXX()
{
BYTE i;
XXX = 0;
for (i = 0; i < NUM_LENGTH; i++)
{
inputElement[i].byNum = XXX / power(2, NUM_LENGTH - i - 1) % 2;
}
}
当然在上面的例子中,因为数据是2 进制的,用power 函数不是很好的选择,直接用"<< >>"移位操作效率更高,我们
仅是为了说明问题的方便。试想,如果用户输入是十进制的,power 函数或许是唯一的选择了。
总结
本篇给出了键盘操作所涉及的各个方面:功能键处理、数字键处理及用户输入整理,基本上提供了一个全套的按键处
理方案。对于功能键处理方法,将LCD 屏幕与Windows 窗口进行类比,提出了较新颖地解决屏幕、键盘繁杂交互问题的方
案。
计算机学的许多知识都具有相通性,因而,不断追赶时髦技术而忽略基本功的做法是徒劳无意的。我们最多需要"精通
"三种语言(精通,一个在如今的求职简历里泛滥成灾的词语),最佳拍档是汇编、C、C++(或JAVA),很显然,如果你"精
通"了这三种语言,其它语言你应该是可以很快"熟悉"的,否则你就没有"精通"它们.
C 语言嵌入式系统编程修炼之六:性能优化
作者:宋宝华 更新日期:2005-07-22
使用宏定义
在C 语言中,宏是产生内嵌代码的唯一方法。对于嵌入式系统而言,为了能达到性能要求,宏是一种很好的代替函数
的方法。
47
写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个:
错误做法:
#define MIN(A,B) ( A <= B ? A : B )
正确做法:
#define MIN(A,B) ((A)<= (B) ? (A) : (B) )
对于宏,我们需要知道三点:
(1)宏定义"像"函数;
(2)宏定义不是函数,因而需要括上所有"参数";
(3)宏定义可能产生副作用。
下面的代码:
least = MIN(*p++, b);
将被替换为:
( (*p++) <= (b) ?(*p++):(b) )
发生的事情无法预料。
因而不要给宏定义传入有副作用的"参数"。
使用寄存器变量
当对一个变量频繁被读写时,需要反复访问内存,从而花费大量的存取时间。为此,C 语言提供了一种变量,即寄存
器变量。这种变量存放在CPU 的寄存器中,使用时,不需要访问内存,而直接从寄存器中读写,从而提高效率。寄存器变
量的说明符是register。对于循环次数较多的循环控制变量及循环体内反复使用的变量均可定义为寄存器变量,而循环计
数是应用寄存器变量的最好候选者。
(1) 只有局部自动变量和形参才可以定义为寄存器变量。因为寄存器变量属于动态存储方式,凡需要采用静态存储方
式的量都不能定义为寄存器变量,包括:模块间全局变量、模块内全局变量、局部static 变量;
(2) register 是一个"建议"型关键字,意指程序建议该变量放在寄存器中,但最终该变量可能因为条件不满足并未成
为寄存器变量,而是被放在了存储器中,但编译器中并不报错(在C++语言中有另一个"建议"型关键字:inline)。
48
下面是一个采用寄存器变量的例子:
/* 求1+2+3+….+n 的值 */
WORD Addition(BYTE n)
{
register i,s=0;
for(i=1;i<=n;i++)
{
s=s+i;
}
return s;
}
本程序循环n 次,i 和s 都被频繁使用,因此可定义为寄存器变量。
内嵌汇编
程序中对时间要求苛刻的部分可以用内嵌汇编来重写,以带来速度上的显著提高。但是,开发和测试汇编代码是一件
辛苦的工作,它将花费更长的时间,因而要慎重选择要用汇编的部分。
在程序中,存在一个80-20 原则,即20%的程序消耗了80%的运行时间,因而我们要改进效率,最主要是考虑改进那
20%的代码。
嵌入式C 程序中主要使用在线汇编,即在C 程序中直接插入_asm{ }内嵌汇编语句:
/* 把两个输入参数的值相加,结果存放到另外一个全局变量中 */
int result;
void Add(long a, long *b)
{
_asm
{
MOV AX, a
MOV BX, b
ADD AX, [BX]
MOV result, AX
}
}
利用硬件特性
首先要明白CPU 对各种存储器的访问速度,基本上是:
CPU 内部RAM > 外部同步RAM > 外部异步RAM > FLASH/ROM
49
对于程序代码,已经被烧录在FLASH 或ROM 中,我们可以让CPU 直接从其中读取代码执行,但通常这不是一个好办法,
我们最好在系统启动后将FLASH 或ROM 中的目标代码拷贝入RAM 中后再执行以提高取指令速度;
对于UART 等设备,其内部有一定容量的接收BUFFER,我们应尽量在BUFFER 被占满后再向CPU 提出中断。例如计算机
终端在向目标机通过RS-232 传递数据时,不宜设置UART 只接收到一个BYTE 就向CPU 提中断,从而无谓浪费中断处理时间;
如果对某设备能采取DMA 方式读取,就采用DMA 读取,DMA 读取方式在读取目标中包含的存储信息较大时效率较高,
其数据传输的基本单位是块,而所传输的数据是从设备直接送入内存的(或者相反)。DMA 方式较之中断驱动方式,减少了
CPU 对外设的干预,进一步提高了CPU 与外设的并行操作程度。
活用位操作
使用C 语言的位操作可以减少除法和取模的运算。在计算机程序中数据的位是可以操作的最小数据单位,理论上可以
用"位运算"来完成所有的运算和操作,因而,灵活的位操作可以有效地提高程序运行的效率。举例如下:
/* 方法1 */
int i,j;
i = 879 / 16;
j = 562 % 32;
/* 方法2 */
int i,j;
i = 879 >> 4;
j = 562 - (562 >> 5 << 5);
对于以2 的指数次方为"*"、"/"或"%"因子的数学运算,转化为移位运算"<< >>"通常可以提高算法效率。因为乘除运
算指令周期通常比移位运算大。
C 语言位运算除了可以提高运算效率外,在嵌入式系统的编程中,它的另一个最典型的应用,而且十分广泛地正在被
使用着的是位间的与(&)、或(|)、非(~)操作,这跟嵌入式系统的编程特点有很大关系。我们通常要对硬件寄存器进行
位设置,譬如,我们通过将AM186ER 型80186 处理器的中断屏蔽控制寄存器的第低6 位设置为0(开中断2),最通用的做
法是:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp &~INT_I2_MASK);
而将该位设置为1 的做法是:
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
outword(INT_MASK, wTemp | INT_I2_MASK);
判断该位是否为1 的做法是:
50
#define INT_I2_MASK 0x0040
wTemp = inword(INT_MASK);
if(wTemp & INT_I2_MASK)
{
… /* 该位为1 */
}
上述方法在嵌入式系统的编程中是非常常见的,我们需要牢固掌握。
总结
在性能优化方面永远注意80-20 准备,不要优化程序中开销不大的那80%,这是劳而无功的。
宏定义是C 语言中实现类似函数功能而又不具函数调用和返回开销的较好方法,但宏在本质上不是函数,因而要防止
宏展开后出现不可预料的结果,对宏的定义和使用要慎而处之。很遗憾,标准C 至今没有包括C++中inline 函数的功能,
inline 函数兼具无调用开销和安全的优点。
使用寄存器变量、内嵌汇编和活用位操作也是提高程序效率的有效方法。
除了编程上的技巧外,为提高系统的运行效率,我们通常也需要最大可能地利用各种硬件设备自身的特点来减小其运
转开销,例如减小中断次数、利用DMA 传输方式等。
C/C++语言void 及void 指针深层探索
1.概述
许多初学者对C/C++语言中的void 及void 指针类型不甚理解,因此在使用上出现了一些错误。本文将对void 关键字的深刻含义进
行解说,并详述void 及void 指针类型的使用方法与技巧。
2.void 的含义
void 的字面意思是“无类型”,void *则为“无类型指针”,void *可以指向任何类型的数据。
void 几乎只有“注释”和限制程序的作用,因为从来没有人会定义一个void 变量,让我们试着来定义:
void a;
这行语句编译时会出错,提示“illegal use of type ‘void‘”。不过,即使void a 的编译不会出错,它也没有任何实际意义。
void 真正发挥的作用在于:
(1) 对函数返回的限定;
(2) 对函数参数的限定。
我们将在第三节对以上二点进行具体说明。
众所周知,如果指针p1 和p2 的类型相同,那么我们可以直接在p1 和p2 间互相赋值;如果p1 和p2 指向不同的数据类型,则必须
使用强制类型转换运算符把赋值运算符右边的指针类型转换为左边指针的类型。
例如:
float *p1;
int *p2;
p1 = p2;
51
其中p1 = p2 语句会编译出错,提示“‘=‘ : cannot convert from ‘int *‘ to ‘float *‘”,必须改为:
p1 = (float *)p2;
而void *则不同,任何类型的指针都可以直接赋值给它,无需进行强制类型转换:
void *p1;
int *p2;
p1 = p2;
但这并不意味着,void *也可以无需强制类型转换地赋给其它类型的指针。因为“无类型”可以包容“有类型”,而“有类型”则
不能包容“无类型”。道理很简单,我们可以说“男人和女人都是人”,但不能说“人是男人”或者“人是女人”。下面的语句编译出
错:
void *p1;
int *p2;
p2 = p1;
提示“‘=‘ : cannot convert from ‘void *‘ to ‘int *‘”。
3.void 的使用
下面给出void 关键字的使用规则:
规则一 如果函数没有返回值,那么应声明为void 类型
在C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理。但是许多程序员却误以为其为void 类型。例如:
add ( int a, int b )
{
return a + b;
}
int main(int argc, char* argv[])
{
printf ( "2 + 3 = %d", add ( 2, 3) );
}
程序运行的结果为输出:
2 + 3 = 5
这说明不加返回值说明的函数的确为int 函数。
林锐博士《高质量C/C++编程》中提到:“C++语言有很严格的类型安全检查,不允许上述情况(指函数不加类型声明)发生”。可
是编译器并不一定这么认定,譬如在Visual C++6.0 中上述add 函数的编译无错也无警告且运行正确,所以不能寄希望于编译器会做严
格的类型检查。
因此,为了避免混乱,我们在编写C/C++程序时,对于任何函数都必须一个不漏地指定其类型。如果函数没有返回值,一定要声明
为void 类型。这既是程序良好可读性的需要,也是编程规范性的要求。另外,加上void 类型声明后,也可以发挥代码的“自注释”作
用。代码的“自注释”即代码能自己注释自己。
规则二 如果函数无参数,那么应声明其参数为void
52
在C++语言中声明一个这样的函数:
int function(void)
{
return 1;
}
则进行下面的调用是不合法的:
function(2);
因为在C++中,函数参数为void 的意思是这个函数不接受任何参数。
我们在Turbo C 2.0 中编译:
#include "stdio.h"
fun()
{
return 1;
}
main()
{
printf("%d",fun(2));
getchar();
}
编译正确且输出1,这说明,在C 语言中,可以给无参数的函数传送任意类型的参数,但是在C++编译器中编译同样的代码则会出错。
在C++中,不能向无参数的函数传送任何参数,出错提示“‘fun‘ : function does not take 1 parameters”。
所以,无论在C 还是C++中,若函数不接受任何参数,一定要指明参数为void。
规则三 小心使用void 指针类型
按照ANSI(American National Standards Institute)标准,不能对void 指针进行算法操作,即下列操作都是不合法的:
void * pvoid;
pvoid++; //ANSI:错误
pvoid += 1; //ANSI:错误
//ANSI 标准之所以这样认定,是因为它坚持:进行算法操作的指针必须是确定知道其指向数据类型大小的。
//例如:
int *pint;
pint++; //ANSI:正确
pint++的结果是使其增大sizeof(int)。
但是大名鼎鼎的GNU(GNU‘s Not Unix 的缩写)则不这么认定,它指定void *的算法操作与char *一致。
因此下列语句在GNU 编译器中皆正确:
53
pvoid++; //GNU:正确
pvoid += 1; //GNU:正确
pvoid++的执行结果是其增大了1。
在实际的程序设计中,为迎合ANSI 标准,并提高程序的可移植性,我们可以这样编写实现同样功能的代码:
void * pvoid;
(char *)pvoid++; //ANSI:正确;GNU:正确
(char *)pvoid += 1; //ANSI:错误;GNU:正确
GNU 和ANSI 还有一些区别,总体而言,GNU 较ANSI 更“开放”,提供了对更多语法的支持。但是我们在真实设计时,还是应该尽可
能地迎合ANSI 标准。
规则四 如果函数的参数可以是任意类型指针,那么应声明其参数为void *
典型的如内存操作函数memcpy 和memset 的函数原型分别为:
void * memcpy(void *dest, const void *src, size_t len);
void * memset ( void * buffer, int c, size_t num );
这样,任何类型的指针都可以传入memcpy 和memset 中,这也真实地体现了内存操作函数的意义,因为它操作的对象仅仅是一片内
存,而不论这片内存是什么类型。如果memcpy 和memset 的参数类型不是void *,而是char *,那才叫真的奇怪了!这样的memcpy 和
memset 明显不是一个“纯粹的,脱离低级趣味的”函数!
下面的代码执行正确:
//示例:memset 接受任意类型指针
int intarray[100];
memset ( intarray, 0, 100*sizeof(int) ); //将intarray 清0
//示例:memcpy 接受任意类型指针
int intarray1[100], intarray2[100];
memcpy ( intarray1, intarray2, 100*sizeof(int) ); //将intarray2 拷贝给intarray1
有趣的是,memcpy 和memset 函数返回的也是void *类型,标准库函数的编写者是多么地富有学问啊!
规则五 void 不能代表一个真实的变量
下面代码都企图让void 代表一个真实的变量,因此都是错误的代码:
void a; //错误
function(void a); //错误
void 体现了一种抽象,这个世界上的变量都是“有类型”的,譬如一个人不是男人就是女人(还有人妖?)。
void 的出现只是为了一种抽象的需要,如果你正确地理解了面向对象中“抽象基类”的概念,也很容易理解void 数据类型。正如
不能给抽象基类定义一个实例,我们也不能定义一个void(让我们类比的称void 为“抽象数据类型”)变量。
4.总结
小小的void 蕴藏着很丰富的设计哲学,作为一名程序设计人员,对问题进行深一个层次的思考必然使我们受益匪浅。
54
C/C++语言可变参数表深层探索
作者:宋宝华 e-mail:21cnbao@21cn.com
1.引言
C/C++语言有一个不同于其它语言的特性,即其支持可变参数,典型的函数如printf、scanf 等可
以接受数量不定的参数。如:
printf ( "I love you" );
printf ( "%d", a );
printf ( "%d,%d", a, b );
第一、二、三个printf 分别接受1、2、3 个参数,让我们看看printf 函数的原型:
int printf ( const char *format, ... );
从函数原型可以看出,其除了接收一个固定的参数format 以外,后面的参数用“…”表示。在C/C++
语言中,“…”表示可以接受不定数量的参数,理论上来讲,可以是0 或0 以上的n 个参数。
本文将对C/C++可变参数表的使用方法及C/C++支持可变参数表的深层机理进行探索。
2.可变参数表的用法
2.1 相关宏
标准C/C++包含头文件stdarg.h,该头文件中定义了如下三个宏:
void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */
type va_arg ( va_list arg_ptr, type );
void va_end ( va_list arg_ptr );
在这些宏中,va 就是variable argument(可变参数)的意思;arg_ptr 是指向可变参数表的指针;
prev_param 则指可变参数表的前一个固定参数;type 为可变参数的类型。va_list 也是一个宏,其定
义为typedef char * va_list,实质上是一char 型指针。char 型指针的特点是++、--操作对其作用
的结果是增1 和减1(因为sizeof(char)为1),与之不同的是int 等其它类型指针的++、--操作对其
作用的结果是增sizeof(type)或减sizeof(type),而且sizeof(type)大于1。
通过va_start 宏我们可以取得可变参数表的首指针,这个宏的定义为:
#define va_start ( ap, v ) ( ap = (va_list)&v + _INTSIZEOF(v) )
显而易见,其含义为将最后那个固定参数的地址加上可变参数对其的偏移后赋值给ap,这样ap 就
是可变参数表的首地址。其中的_INTSIZEOF 宏定义为:
#define _INTSIZEOF(n) ((sizeof ( n ) + sizeof ( int ) – 1 ) & ~( sizeof( int ) – 1 ) )
va_arg 宏的意思则指取出当前arg_ptr 所指的可变参数并将ap 指针指向下一可变参数,其原型为:
#define va_arg(list, mode) ((mode *)(list =/
(char *) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) &/
(__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1]
对这个宏的具体含义我们将在第3 节深入讨论。
而va_end 宏被用来结束可变参数的获取,其定义为:
#define va_end ( list )
可以看出,va_end ( list )实际上被定义为空,没有任何真实对应的代码,用于代码对称,与va_start
对应;另外,它还可能发挥代码的“自注释”作用。所谓代码的“自注释”,指的是代码能自己注释
自己。
下面我们以具体的例子来说明以上三个宏的使用方法。
55
2.2 一个简单的例子
#include <stdarg.h>
/* 函数名:max
* 功能:返回n 个整数中的最大值
* 参数:num:整数的个数 ...:num 个输入的整数
* 返回值:求得的最大整数
*/
int max ( int num, ... )
{
int m = -0x7FFFFFFF; /* 32 系统中最小的整数 */
va_list ap;
va_start ( ap, num );
for ( int i= 0; i< num; i++ )
{
int t = va_arg (ap, int);
if ( t > m )
{
m = t;
}
}
va_end (ap);
return m;
}
/* 主函数调用max */
int main ( int argc, char* argv[] )
{
int n = max ( 5, 5, 6 ,3 ,8 ,5); /* 求5 个整数中的最大值 */
cout << n;
return 0;
}
函数max 中首先定义了可变参数表指针ap,而后通过va_start ( ap, num )取得了参数表首地址(赋
给了ap),其后的for 循环则用来遍历可变参数表。这种遍历方式与我们在数据结构教材中经常看到
的遍历方式是类似的。
函数max 看起来简洁明了,但是实际上printf 的实现却远比这复杂。max 函数之所以看起来简单,是
因为:
(1) max 函数可变参数表的长度是已知的,通过num 参数传入;
(2) max 函数可变参数表中参数的类型是已知的,都为int 型。
而printf 函数则没有这么幸运。首先,printf 函数可变参数的个数不能轻易的得到,而可变参数的类
型也不是固定的,需由格式字符串进行识别(由%f、%d、%s 等确定),因此则涉及到可变参数表的更
复杂应用。
下面我们以实例来分析可变参数表的高级应用。
2.3 高级应用
下面这个程序是我们为某嵌入式系统(该系统中CPU 的字长为16 位)编写的在屏幕上显示格式字符串
56
的函数DrawText,它的用法类似于int printf ( const char *format, ... )函数,但其输出的目标
为嵌入式系统的液晶显示屏幕(LED)。
///
// 函数名称: DrawText
// 功能说明: 在显示屏上绘制文字
// 参数说明: xPos ---横坐标的位置 [0 .. 30]
// yPos ---纵坐标的位置 [0 .. 64]
// ... 可以同数字一起显示,需设置标志(%d、%l、%x、%s)
///
extern void DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )
{
BYTE lpData[100]; //缓冲区
BYTE byIndex;
BYTE byLen;
DWORD dwTemp;
WORD wTemp;
int i;
va_list lpParam;
memset( lpData, 0, 100);
byLen = strlen( lpStr );
byIndex = 0;
va_start ( lpParam, lpStr );
for ( i = 0; i < byLen; i++ )
{
if( lpStr[i] != '%' ) //不是格式符开始
{
lpData[byIndex++] = lpStr[i];
}
else
{
switch (lpStr[i+1])
{
//整型
case 'd':
case 'D':
wTemp = va_arg ( lpParam, int );
byIndex += IntToStr( lpData+byIndex, (DWORD)wTemp );
i++;
break;
//长整型
case 'l':
case 'L':
57
dwTemp = va_arg ( lpParam, long );
byIndex += IntToStr ( lpData+byIndex, (DWORD)dwTemp );
i++;
break;
//16 进制(长整型)
case 'x':
case 'X':
dwTemp = va_arg ( lpParam, long );
byIndex += HexToStr ( lpData+byIndex, (DWORD)dwTemp );
i++;
break;
default:
lpData[byIndex++] = lpStr[i];
break;
}
}
}
va_end ( lpParam );
lpData[byIndex] = '/0';
DisplayString ( xPos, yPos, lpData, TRUE); //在屏幕上显示字符串lpData
}
在这个函数中,需通过对传入的格式字符串(首地址为lpStr)进行识别来获知可变参数个数及各个可
变参数的类型,具体实现体现在for 循环中。譬如,在识别为%d 后,做的是va_arg ( lpParam, int ),
而获知为%l 和%x 后则进行的是va_arg ( lpParam, long )。格式字符串识别完成后,可变参数也就处
理完了。
在项目的最初,我们一直苦于不能找到一个好的办法来混合输出字符串和数字,我们采用了分别显示
数字和字符串的方法,并分别指定坐标,程序条理被破坏。而且,在混合显示的时候,要给各类数据
分别人工计算坐标,我们感觉头疼不已。以前的函数为:
//显示字符串
showString ( BYTE xPos, BYTE yPos, LPBYTE lpStr )
//显示数字
showNum ( BYTE xPos, BYTE yPos, int num )
//以16 进制方式显示数字
showHexNum ( BYTE xPos, BYTE yPos, int num )
最终,我们用DrawText ( BYTE xPos, BYTE yPos, LPBYTE lpStr, ... )函数代替了原先所有的输出
函数,程序得到了简化。就这样,兄弟们用得爽翻了。
3.运行机制探索
通过第2 节我们学会了可变参数表的使用方法,相信喜欢抛根问底的读者还不甘心,必然想知道如下
问题:
(1)为什么按照第2 节的做法就可以获得可变参数并对其进行操作?
(2)C/C++在底层究竟是依靠什么来对这一语法进行支持的,为什么其它语言就不能提供可变参数表
呢?
我们带着这些疑问来一步步进行摸索。
58
3.1 调用机制反汇编
反汇编是研究语法深层特性的终极良策,先来看看2.2 节例子中主函数进行max ( 5, 5, 6 ,3 ,8 ,5)
调用时的反汇编:
1. 004010C8 push 5
2. 004010CA push 8
3. 004010CC push 3
4. 004010CE push 6
5. 004010D0 push 5
6. 004010D2 push 5
7. 004010D4 call @ILT+5(max) (0040100a)
从上述反汇编代码中我们可以看出,C/C++函数调用的过程中:
第一步:将参数从右向左入栈(第1~6 行);
第二步:调用call 指令进行跳转(第7 行)。
这两步包含了深刻的含义,它说明C/C++默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺
序为从右至左,这种调用方式称为_cdecl 调用。x86 系统的入栈方向为从高地址到低地址,故第1 至n
个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,
让我们反汇编到max 函数的内部:
int max ( int num, ...)
{
1. 00401020 push ebp
2. 00401021 mov ebp,esp
3. 00401023 sub esp,50h
4. 00401026 push ebx
5. 00401027 push esi
6. 00401028 push edi
7. 00401029 lea edi,[ebp-50h]
8. 0040102C mov ecx,14h
9. 00401031 mov eax,0CCCCCCCCh
10. 00401036 rep stos dword ptr [edi]
va_list ap;
int m = -0x7FFFFFFF; /* 32 系统中最小的整数 */
11. 00401038 mov dword ptr [ebp-8],80000001h
va_start ( ap, num );
12. 0040103F lea eax,[ebp+0Ch]
13. 00401042 mov dword ptr [ebp-4],eax
for ( int i= 0; i< num; i++ )
14. 00401045 mov dword ptr [ebp-0Ch],0
15. 0040104C jmp max+37h (00401057)
16. 0040104E mov ecx,dword ptr [ebp-0Ch]
17. 00401051 add ecx,1
18. 00401054 mov dword ptr [ebp-0Ch],ecx
19. 00401057 mov edx,dword ptr [ebp-0Ch]
20. 0040105A cmp edx,dword ptr [ebp+8]
21. 0040105D jge max+61h (00401081)
59
{
int t= va_arg (ap, int);
22. 0040105F mov eax,dword ptr [ebp-4]
23. 00401062 add eax,4
24. 00401065 mov dword ptr [ebp-4],eax
25. 00401068 mov ecx,dword ptr [ebp-4]
26. 0040106B mov edx,dword ptr [ecx-4]
27. 0040106E mov dword ptr [t],edx
if ( t > m )
28. 00401071 mov eax,dword ptr [t]
29. 00401074 cmp eax,dword ptr [ebp-8]
30. 00401077 jle max+5Fh (0040107f)
m = t;
31. 00401079 mov ecx,dword ptr [t]
32. 0040107C mov dword ptr [ebp-8],ecx
}
33. 0040107F jmp max+2Eh (0040104e)
va_end (ap);
34. 00401081 mov dword ptr [ebp-4],0
return m;
35. 00401088 mov eax,dword ptr [ebp-8]
}
36. 0040108B pop edi
37. 0040108C pop esi
38. 0040108D pop ebx
39. 0040108E mov esp,ebp
40. 00401090 pop ebp
41. 00401091 ret
分析上述反汇编代码,对于一个真正的程序员而言,将是一种很大的享受;而对于初学者,也将使其
受益良多。所以请一定要赖着头皮认真研究,千万不要被吓倒!
行1~10 进行执行函数内代码的准备工作,保存现场。第2 行对堆栈进行移动;第3 行则意味着max
函数为其内部局部变量准备的堆栈空间为50h 字节;第11 行表示把变量n 的内存空间安排在了函数内
部局部栈底减8 的位置(占用4 个字节)。
第12~13 行非常关键,对应着va_start ( ap, num ),这两行将第一个可变参数的地址赋值给了指针
ap。另外,从第12 行可以看出num 的地址为ebp+0Ch;从第13 行可以看出ap 被分配在函数内部局部
栈底减4 的位置上(占用4 个字节)。
第22~27 行最为关键,对应着va_arg (ap, int)。其中,22~24 行的作用为将ap 指向下一可变参
数(可变参数的地址间隔为4 个字节,从add eax,4 可以看出);25~27 行则取当前可变参数的值赋给
变量t。这段反汇编很奇怪,它先移动可变参数指针,再在赋值指令里面回过头来取先前的参数值赋给
t(从mov edx,dword ptr [ecx-4]语句可以看出)。Visual C++同学玩得有意思,不知道碰见同样的
情况Visual Basic 等其它同学怎么玩?
第36~41 行恢复现场和堆栈地址,执行函数返回操作。
痛苦的反汇编之旅差不多结束了,看了这段反汇编我们总算弄明白了可变参数的存放位置以及它们被
读取的方式,顿觉全省轻松!
60
3.2 特殊的调用约定
除此之外,我们需要了解C/C++函数调用对参数占用空间的一些特殊约定,因为在_cdecl 调用协议中,
有些变量类型是按照其它变量的尺寸入栈的。
例如,字符型变量将被自动扩展为一个字的空间,因为入栈操作针对的是一个字。
参数n 实际占用的空间为( ( sizeof(n) + sizeof(int) – 1 ) & ~( sizeof(int) – 1 ) ),这就
是第2.1 节_INTSIZEOF(v)宏的来历!
既然如此,2.1 节给出的va_arg ( list, mode )宏为什么玩这么大的飞机就很清楚了。这个问题就留
个读者您来分析。
C/C++数组名与指针区别深层探索
作者:宋宝华 e-mail:21cnbao@21cn.com
1. 引言
指针是C/C++语言的特色,而数组名与指针有太多的相似,甚至很多时候,数组名可以作为指针使用。于是乎,很多
程序设计者就被搞糊涂了。而许多的大学老师,他们在C 语言的教学过程中也错误得给学生讲解:“数组名就是指针”。
很幸运,我的大学老师就是其中之一。时至今日,我日复一日地进行着C/C++项目的开发,而身边还一直充满这样的
程序员,他们保留着“数组名就是指针”的误解。
想必这种误解的根源在于国内某著名的C 程序设计教程。如果这篇文章能够纠正许多中国程序员对数组名和指针的误
解,笔者就不甚欣慰了。借此文,笔者站在无数对知识如饥似渴的中国程序员之中,深深寄希望于国内的计算机图书
编写者们,能以“深入探索”的思维方式和精益求精的认真态度来对待图书编写工作,但愿市面上多一些融入作者思
考结晶的心血之作!
2. 魔幻数组名
请看程序(本文程序在WIN32 平台下编译):
1. #include <iostream.h>
2. int main(int argc, char* argv[])
3. {
4. char str[10];
5. char *pStr = str;
6. cout << sizeof(str) << endl;
7. cout << sizeof(pStr) << endl;
8. return 0;
9. }
2.1 数组名不是指针
我们先来推翻“数组名就是指针”的说法,用反证法。
证明 数组名不是指针
假设:数组名是指针;
则:pStr 和str 都是指针;
因为:在WIN32 平台下,指针长度为4;
所以:第6 行和第7 行的输出都应该为4;
实际情况是:第6 行输出10,第7 行输出4;
所以:假设不成立,数组名不是指针
2.2 数组名神似指针
上面我们已经证明了数组名的确不是指针,但是我们再看看程序的第5 行。该行程序将数组名直接赋值给指针,这显
61
得数组名又的确是个指针!
我们还可以发现数组名显得像指针的例子:
1. #include <string.h>
2. #include <iostream.h>
3. int main(int argc, char* argv[])
4. {
5. char str1[10] = "I Love U";
6. char str2[10];
7. strcpy(str2,str1);
8. cout << "string array 1: " << str1 << endl;
9. cout << "string array 2: " << str2 << endl;
10. return 0;
11. }
标准C 库函数strcpy 的函数原形中能接纳的两个参数都为char 型指针,而我们在调用中传给它的却是两个数组名!
函数输出:
string array 1: I Love U
string array 2: I Love U
数组名再一次显得像指针!
既然数组名不是指针,而为什么到处都把数组名当指针用?于是乎,许多程序员得出这样的结论:数组名(主)是(谓)
不是指针的指针(宾)。
整个一魔鬼。
3. 数组名大揭密
那么,是揭露数组名本质的时候了,先给出三个结论:
(1)数组名的内涵在于其指代实体是一种数据结构,这种数据结构就是数组;
(2)数组名的外延在于其可以转换为指向其指代实体的指针,而且是一个指针常量;
(3)指向数组的指针则是另外一种变量类型(在WIN32 平台下,长度为4),仅仅意味着数组的存放地址!
3.1 数组名指代一种数据结构:数组
现在可以解释为什么第1 个程序第6 行的输出为10 的问题,根据结论1,数组名str 的内涵为一种数据结构,即一
个长度为10 的char 型数组,所以sizeof(str)的结果为这个数据结构占据的内存大小:10 字节。
再看:
1. int intArray[10];
2. cout << sizeof(intArray) ;
第2 行的输出结果为40(整型数组占据的内存空间大小)。
如果C/C++程序可以这样写:
1. int[10] intArray;
2. cout << sizeof(intArray) ;
我们就都明白了,intArray 定义为int[10]这种数据结构的一个实例,可惜啊,C/C++目前并不支持这种定义方式。
3.2 数组名可作为指针常量
根据结论2,数组名可以转换为指向其指代实体的指针,所以程序1 中的第5 行数组名直接赋值给指针,程序2 第7
行直接将数组名作为指针形参都可成立。
下面的程序成立吗?
1. int intArray[10];
2. intArray++;
读者可以编译之,发现编译出错。原因在于,虽然数组名可以转换为指向其指代实体的指针,但是它只能被看作一个
62
指针常量,不能被修改。
而指针,不管是指向结构体、数组还是基本数据类型的指针,都不包含原始数据结构的内涵,在WIN32 平台下,sizeof
操作的结果都是4。
顺便纠正一下许多程序员的另一个误解。许多程序员以为sizeof 是一个函数,而实际上,它是一个操作符,不过其
使用方式看起来的确太像一个函数了。语句sizeof(int)就可以说明sizeof 的确不是一个函数,因为函数接纳形参
(一个变量),世界上没有一个C/C++函数接纳一个数据类型(如int)为“形参”。
3.3 数据名可能失去其数据结构内涵
到这里似乎数组名魔幻问题已经宣告圆满解决,但是平静的湖面上却再次掀起波浪。请看下面一段程序:
1. #include <iostream.h>
2. void arrayTest(char str[])
3. {
4. cout << sizeof(str) << endl;
5. }
6. int main(int argc, char* argv[])
7. {
8. char str1[10] = "I Love U";
9. arrayTest(str1);
10. return 0;
11. }
程序的输出结果为4。不可能吧?
4,一个可怕的数字,前面已经提到其为指针的长度!
结论1 指出,数据名内涵为数组这种数据结构,在arrayTest 函数体内,str 是数组名,那为什么sizeof 的结果却
是指针的长度?这是因为:
(1)数组名作为函数形参时,在函数体内,其失去了本身的内涵,仅仅只是一个指针;
(2)很遗憾,在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被修改。
所以,数据名作为函数形参时,其全面沦落为一个普通指针!它的贵族身份被剥夺,成了一个地地道道的只拥有4
个字节的平民。
以上就是结论4。
4. 结论
本文以打破沙锅问到底的探索精神用数段程序实例论证了数据名和指针的区别。
最后,笔者再次表达深深的希望,愿我和我的同道中人能够真正以谨慎的研究态度来认真思考开发中的问题,这样才
能在我们中间产生大师级的程序员,顶级的开发书籍。每次拿着美国鬼子的开发书籍,我们不免发出这样的感慨:我
们落后太远了。
C/C++程序员应聘常见面试题深入剖析(1)
作者:宋宝华 e-mail:21cnbao@21cn.com 出处:软件报
1.引言
本文的写作目的并不在于提供C/C++程序员求职面试指导,而旨在从技术上分析面试题的内涵。文中的
大多数面试题来自各大论坛,部分试题解答也参考了网友的意见。
许多面试题看似简单,却需要深厚的基本功才能给出完美的解答。企业要求面试者写一个最简单的
strcpy 函数都可看出面试者在技术上究竟达到了怎样的程度,我们能真正写好一个strcpy 函数吗?我
们都觉得自己能,可是我们写出的strcpy 很可能只能拿到10 分中的2 分。读者可从本文看到strcpy
63
函数从2 分到10 分解答的例子,看看自己属于什么样的层次。此外,还有一些面试题考查面试者敏捷
的思维能力。
分析这些面试题,本身包含很强的趣味性;而作为一名研发人员,通过对这些面试题的深入剖析则可
进一步增强自身的内功。
2.找错题
试题1:
void test1()
{
char string[10];
char* str1 = "0123456789";
strcpy( string, str1 );
}
试题2:
void test2()
{
char string[10], str1[10];
int i;
for(i=0; i<10; i++)
{
str1[i] = 'a';
}
strcpy( string, str1 );
}
试题3:
void test3(char* str1)
{
char string[10];
if( strlen( str1 ) <= 10 )
{
strcpy( string, str1 );
}
}
解答:
试题1 字符串str1 需要11 个字节才能存放下(包括末尾的’/0’),而string 只有10 个字节的空
间,strcpy 会导致数组越界;
对试题2,如果面试者指出字符数组str1 不能在数组内结束可以给3 分;如果面试者指出strcpy(string,
str1)调用使得从str1 内存起复制到string 内存起所复制的字节数具有不确定性可以给7 分,在此基
础上指出库函数strcpy 工作方式的给10 分;
对试题3,if(strlen(str1) <= 10)应改为if(strlen(str1) < 10),因为strlen 的结果未统计’/0’
所占用的1 个字节。
剖析:
考查对基本功的掌握:
(1)字符串以’/0’结尾;
64
(2)对数组越界把握的敏感度;
(3)库函数strcpy 的工作方式,如果编写一个标准strcpy 函数的总分值为10,下面给出几个不同
得分的答案:
2 分
void strcpy( char *strDest, char *strSrc )
{
while( (*strDest++ = * strSrc++) != ‘/0’ );
}
4 分
void strcpy( char *strDest, const char *strSrc )
//将源字符串加const,表明其为输入参数,加2 分
{
while( (*strDest++ = * strSrc++) != ‘/0’ );
}
7 分
void strcpy(char *strDest, const char *strSrc)
{
//对源地址和目的地址加非0 断言,加3 分
assert( (strDest != NULL) && (strSrc != NULL) );
while( (*strDest++ = * strSrc++) != ‘/0’ );
}
10 分
//为了实现链式操作,将目的地址返回,加3 分!
char * strcpy( char *strDest, const char *strSrc )
{
assert( (strDest != NULL) && (strSrc != NULL) );
char *address = strDest;
while( (*strDest++ = * strSrc++) != ‘/0’ );
return address;
}
从2 分到10 分的几个答案我们可以清楚的看到,小小的strcpy 竟然暗藏着这么多玄机,真不是盖的!
需要多么扎实的基本功才能写一个完美的strcpy 啊!
(4)对strlen 的掌握,它没有包括字符串末尾的'/0'。
读者看了不同分值的strcpy 版本,应该也可以写出一个10 分的strlen 函数了,完美的版本为:
int strlen( const char *str ) //输入参数const
{
assert( strt != NULL ); //断言字符串地址非0
int len;
while( (*str++) != '/0' )
{
len++;
}
return len;
}
65
试题4:
void GetMemory( char *p )
{
p = (char *) malloc( 100 );
}
void Test( void )
{
char *str = NULL;
GetMemory( str );
strcpy( str, "hello world" );
printf( str );
}
试题5:
char *GetMemory( void )
{
char p[] = "hello world";
return p;
}
void Test( void )
{
char *str = NULL;
str = GetMemory();
printf( str );
}
试题6:
void GetMemory( char **p, int num )
{
*p = (char *) malloc( num );
}
void Test( void )
{
char *str = NULL;
GetMemory( &str, 100 );
strcpy( str, "hello" );
printf( str );
}
试题7:
void Test( void )
{
char *str = (char *) malloc( 100 );
strcpy( str, "hello" );
free( str );
... //省略的其它语句
}
66
解答:
试题4 传入中GetMemory( char *p )函数的形参为字符串指针,在函数内部修改形参并不能真正的改
变传入形参的值,执行完
char *str = NULL;
GetMemory( str );
后的str 仍然为NULL;
试题5 中
char p[] = "hello world";
return p;
的p[]数组为函数内的局部自动变量,在函数返回后,内存已经被释放。这是许多程序员常犯的错误,
其根源在于不理解变量的生存期。
试题6 的GetMemory 避免了试题4 的问题,传入GetMemory 的参数为字符串指针的指针,但是在
GetMemory 中执行申请内存及赋值语句
*p = (char *) malloc( num );
后未判断内存是否申请成功,应加上:
if ( *p == NULL )
{
...//进行申请内存失败处理
}
试题7 存在与试题6 同样的问题,在执行
char *str = (char *) malloc(100);
后未进行内存是否申请成功的判断;另外,在free(str)后未置str 为空,导致可能变成一个“野”指
针,应加上:
str = NULL;
试题6 的Test 函数中也未对malloc 的内存进行释放。
剖析:
试题4~7 考查面试者对内存操作的理解程度,基本功扎实的面试者一般都能正确的回答其中50~60 的
错误。但是要完全解答正确,却也绝非易事。
对内存操作的考查主要集中在:
(1)指针的理解;
(2)变量的生存期及作用范围;
(3)良好的动态内存申请和释放习惯。
在看看下面的一段程序有什么错误:
swap( int* p1,int* p2 )
{
int *p;
*p = *p1;
*p1 = *p2;
*p2 = *p;
}
在swap 函数中,p 是一个“野”指针,有可能指向系统区,导致程序运行的崩溃。在VC++中DEBUG 运
行时提示错误“Access Violation”。该程序应该改为:
swap( int* p1,int* p2 )
{
67
int p;
p = *p1;
*p1 = *p2;
*p2 = p;
}
C/C++程序员应聘常见面试题深入剖析(2)
作者:宋宝华 e-mail:21cnbao@21cn.com 出处:软件报
3.内功题
试题1:分别给出BOOL,int,float,指针变量 与“零值”比较的 if 语句(假设变量名为var)
解答:
BOOL 型变量:if(!var)
int 型变量: if(var==0)
float 型变量:
const float EPSINON = 0.00001;
if ((x >= - EPSINON) && (x <= EPSINON)
指针变量: if(var==NULL)
剖析:
考查对0 值判断的“内功”,BOOL 型变量的0 判断完全可以写成if(var==0),而int 型变量也可以写
成if(!var),指针变量的判断也可以写成if(!var),上述写法虽然程序都能正确运行,但是未能清晰
地表达程序的意思。
一般的,如果想让if 判断一个变量的“真”、“假”,应直接使用if(var)、if(!var),表明其为“逻
辑”判断;如果用if 判断一个数值型变量(short、int、long 等),应该用if(var==0),表明是与0
进行“数值”上的比较;而判断指针则适宜用if(var==NULL),这是一种很好的编程习惯。
浮点型变量并不精确,所以不可将float 变量用“==”或“!=”与数字比较,应该设法转化成“>=”
或“<=”形式。如果写成if (x == 0.0),则判为错,得0 分。
试题2:以下为Windows NT 下的32 位C++程序,请计算sizeof 的值
void Func ( char str[100] )
{
sizeof( str ) = ?
}
void *p = malloc( 100 );
sizeof ( p ) = ?
解答:
sizeof( str ) = 4
sizeof ( p ) = 4
剖析:
Func ( char str[100] )函数中数组名作为函数形参时,在函数体内,数组名失去了本身的内涵,仅
仅只是一个指针;在失去其内涵的同时,它还失去了其常量特性,可以作自增、自减等操作,可以被
修改。
数组名的本质如下:
68
(1)数组名指代一种数据结构,这种数据结构就是数组;
例如:
char str[10];
cout << sizeof(str) << endl;
输出结果为10,str 指代数据结构char[10]。
(2)数组名可以转换为指向其指代实体的指针,而且是一个指针常量,不能作自增、自减等操作,不
能被修改;
char str[10];
str++; //编译出错,提示str 不是左值
(3)数组名作为函数形参时,沦为普通指针。
Windows NT 32 位平台下,指针的长度(占用内存的大小)为4 字节,故sizeof( str ) 、
sizeof ( p ) 都为4。
试题3:写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。另外,当你写下面的代码时
会发生什么事?
least = MIN(*p++, b);
解答:
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
MIN(*p++, b)会产生宏的副作用
剖析:
这个面试题主要考查面试者对宏定义的使用,宏定义可以实现类似于函数的功能,但是它终归不是函
数,而宏定义中括弧中的“参数”也不是真的参数,在宏展开的时候对“参数”进行的是一对一的替
换。
程序员对宏定义的使用要非常小心,特别要注意两个问题:
(1)谨慎地将宏定义中的“参数”和整个宏用用括弧括起来。所以,严格地讲,下述解答:
#define MIN(A,B) (A) <= (B) ? (A) : (B)
#define MIN(A,B) (A <= B ? A : B )
都应判0 分;
(2)防止宏的副作用。
宏定义#define MIN(A,B) ((A) <= (B) ? (A) : (B))对MIN(*p++, b)的作用结果是:
((*p++) <= (b) ? (*p++) : (b))
这个表达式会产生副作用,指针p 会作两次++自增操作。
除此之外,另一个应该判0 分的解答是:
#define MIN(A,B) ((A) <= (B) ? (A) : (B));
这个解答在宏定义的后面加“;”,显示编写者对宏的概念模糊不清,只能被无情地判0 分并被面试官
淘汰。
试题4:为什么标准头文件都有类似以下的结构?
#ifndef __INCvxWorksh
#define __INCvxWorksh
#ifdef __cplusplus
extern "C" {
#endif
/*...*/
#ifdef __cplusplus
}
69
#endif
#endif /* __INCvxWorksh */
解答:
头文件中的编译宏
#ifndef __INCvxWorksh
#define __INCvxWorksh
#endif
的作用是防止被重复引用。
作为一种面向对象的语言,C++支持函数重载,而过程式语言C 则不支持。函数被C++编译后在symbol
库中的名字与C 语言的不同。例如,假设某个函数的原型为:
void foo(int x, int y);
该函数被C 编译器编译后在symbol 库中的名字为_foo,而C++编译器则会产生像_foo_int_int 之类的
名字。_foo_int_int 这样的名字包含了函数名和函数参数数量及类型信息,C++就是考这种机制来实现
函数重载的。
为了实现C 和C++的混合编程,C++提供了C 连接交换指定符号extern "C"来解决名字匹配问题,函数
声明前加上extern "C"后,则编译器就会按照C 语言的方式将该函数编译为_foo,这样C 语言中就可
以调用C++的函数了。
试题5:编写一个函数,作用是把一个char 组成的字符串循环右移n 个。比如原来是“abcdefghi”
如果n=2,移位后应该是“hiabcdefgh”
函数头是这样的:
//pStr 是指向以'/0'结尾的字符串的指针
//steps 是要求移动的n
void LoopMove ( char * pStr, int steps )
{
//请填充...
}
解答:
正确解答1:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN];
strcpy ( tmp, pStr + n );
strcpy ( tmp + steps, pStr);
*( tmp + strlen ( pStr ) ) = '/0';
strcpy( pStr, tmp );
}
正确解答2:
void LoopMove ( char *pStr, int steps )
{
int n = strlen( pStr ) - steps;
char tmp[MAX_LEN];
memcpy( tmp, pStr + n, steps );
memcpy(pStr + steps, pStr, n );
70
memcpy(pStr, tmp, steps );
}
剖析:
这个试题主要考查面试者对标准库函数的熟练程度,在需要的时候引用库函数可以很大程度上简化程
序编写的工作量。
最频繁被使用的库函数包括:
(1)strcpy
(2)memcpy
(3)memset
试题6:已知WAV 文件格式如下表,打开一个WAV 文件,以适当的数据结构组织WAV 文件头并解析WAV
格式的各项信息。
WAVE 文件格式说明表
偏移地址 字节数 数据类型 内 容
00H 4 Char "RIFF"标志
04H 4 int32 文件长度
08H 4 Char "WAVE"标志
0CH 4 Char "fmt"标志
10H 4 过渡字节(不定)
14H 2 int16 格式类别
16H 2 int16 通道数
18H 2 int16
采样率(每秒样本数),表示每个通道的
播放速度
1CH 4 int32 波形音频数据传送速率
20H 2 int16 数据块的调整数(按字节算的)
22H 2 每样本的数据位数
24H 4 Char 数据标记符"data"



28H 4 int32 语音数据的长度
解答:
将WAV 文件格式定义为结构体WAVEFORMAT:
typedef struct tagWaveFormat
{
char cRiffFlag[4];
UIN32 nFileLen;
char cWaveFlag[4];
char cFmtFlag[4];
char cTransition[4];
UIN16 nFormatTag ;
UIN16 nChannels;
UIN16 nSamplesPerSec;
UIN32 nAvgBytesperSec;
UIN16 nBlockAlign;
UIN16 nBitNumPerSample;
char cDataFlag[4];
71
UIN16 nAudioLength;
} WAVEFORMAT;
假设WAV 文件内容读出后存放在指针buffer 开始的内存单元内,则分析文件格式的代码很简单,为:
WAVEFORMAT waveFormat;
memcpy( &waveFormat, buffer,sizeof( WAVEFORMAT ) );
直接通过访问waveFormat 的成员,就可以获得特定WAV 文件的各项格式信息。
剖析:
试题6 考查面试者组织数据结构的能力,有经验的程序设计者将属于一个整体的数据成员组织为一个
结构体,利用指针类型转换,可以将memcpy、memset 等函数直接用于结构体地址,进行结构体的整体
操作。
透过这个题可以看出面试者的程序设计经验是否丰富。
试题7:编写类String 的构造函数、析构函数和赋值函数,已知类String 的原型为:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operate =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
解答:
//普通构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1]; // 得分点:对空字符串自动申请存放结束标志'/0'的空
//加分点:对m_data 加NULL 判断
*m_data = '/0';
}
else
{
int length = strlen(str);
m_data = new char[length+1]; // 若能加 NULL 判断则更好
strcpy(m_data, str);
}
}
// String 的析构函数
String::~String(void)
{
delete [] m_data; // 或delete m_data;
}
72
//拷贝构造函数
String::String(const String &other) // 得分点:输入参数为const 型
{
int length = strlen(other.m_data);
m_data = new char[length+1]; //加分点:对m_data 加NULL 判断
strcpy(m_data, other.m_data);
}
//赋值函数
String & String::operate =(const String &other) // 得分点:输入参数为const 型
{
if(this == &other) //得分点:检查自赋值
return *this;
delete [] m_data; //得分点:释放原有的内存资源
int length = strlen( other.m_data );
m_data = new char[length+1]; //加分点:对m_data 加NULL 判断
strcpy( m_data, other.m_data );
return *this; //得分点:返回本对象的引用
}
剖析:
能够准确无误地编写出String 类的构造函数、拷贝构造函数、赋值函数和析构函数的面试者至少已经
具备了C++基本功的60%以上!
在这个类中包括了指针类成员变量m_data,当类中包括指针类成员变量时,一定要重载其拷贝构造函
数、赋值函数和析构函数,这既是对C++程序员的基本要求,也是《Effective C++》中特别强调的条
款。
仔细学习这个类,特别注意加注释的得分点和加分点的意义,这样就具备了60%以上的C++基本功!
试题8:请说出static 和const 关键字尽可能多的作用
解答:
static 关键字至少有下列n 个作用:
(1)函数体内static 变量的作用范围为该函数体,不同于auto 变量,该变量的内存只被分配一次,
因此其值在下次调用时仍维持上次的值;
(2)在模块内的static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
(3)在模块内的static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明
它的模块内;
(4)在类中的static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
(5)在类中的static 成员函数属于整个类所拥有,这个函数不接收this 指针,因而只能访问类的
static 成员变量。
const 关键字至少有下列n 个作用:
(1)欲阻止一个变量被改变,可以使用const 关键字。在定义该const 变量时,通常需要对它进行初
始化,因为以后就没有机会再去改变它了;
(2)对指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指
定为const;
(3)在一个函数声明中,const 可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
(4)对于类的成员函数,若指定其为const 类型,则表明其是一个常函数,不能修改类的成员变量;
(5)对于类的成员函数,有时候必须指定其返回值为const 类型,以使得其返回值不为“左值”。例
73
如:
const classA operator*(const classA& a1,const classA& a2);
operator*的返回结果必须是一个const 对象。如果不是,这样的变态代码也不会编译出错:
classA a, b, c;
(a * b) = c; // 对a*b 的结果赋值
操作(a * b) = c 显然不符合编程者的初衷,也没有任何意义。
剖析:
惊讶吗?小小的static 和const 居然有这么多功能,我们能回答几个?如果只能回答1~2 个,那还真
得闭关再好好修炼修炼。
这个题可以考查面试者对程序设计知识的掌握程度是初级、中级还是比较深入,没有一定的知识广度
和深度,不可能对这个问题给出全面的解答。大多数人只能回答出static 和const 关键字的部分功能。
4.技巧题
试题1:请写一个C 函数,若处理器是Big_endian 的,则返回0;若是Little_endian 的,则返回1
解答:
int checkCPU()
{
{
union w
{
int a;
char b;
} c;
c.a = 1;
return (c.b == 1);
}
}
剖析:
嵌入式系统开发者应该对Little-endian 和Big-endian 模式非常了解。采用Little-endian 模式的CPU
对操作数的存放方式是从低字节到高字节,而Big-endian 模式对操作数的存放方式是从高字节到低字
节。例如,16bit 宽的数0x1234 在Little-endian 模式CPU 内存中的存放方式(假设从地址0x4000
开始存放)为:
内存地

0x4000 0x4001
存放内

0x34 0x12
而在Big-endian 模式CPU 内存中的存放方式则为:
内存地

0x4000 0x4001
存放内

0x12 0x34
32bit 宽的数0x12345678 在Little-endian 模式CPU 内存中的存放方式(假设从地址0x4000 开始存放)
为:
内存地0x4000 0x4001 0x4002 0x4003
74

存放内

0x78 0x56 0x34 0x12
而在Big-endian 模式CPU 内存中的存放方式则为:
内存地

0x4000 0x4001 0x4002 0x4003
存放内

0x12 0x34 0x56 0x78
联合体union 的存放顺序是所有成员都从低地址开始存放,面试者的解答利用该特性,轻松地获得了
CPU 对内存采用Little-endian 还是Big-endian 模式读写。如果谁能当场给出这个解答,那简直就是
一个天才的程序员。
试题2:写一个函数返回1+2+3+…+n 的值(假定结果不会超过长整型变量的范围)
解答:
int Sum( int n )
{
return ( (long)1 + n) * n / 2; //或return (1l + n) * n / 2;
}
剖析:
对于这个题,只能说,也许最简单的答案就是最好的答案。下面的解答,或者基于下面的解答思路去
优化,不管怎么“折腾”,其效率也不可能与直接return ( 1 l + n ) * n / 2 相比!
int Sum( int n )
{
long sum = 0;
for( int i=1; i<=n; i++ )
{
sum += i;
}
return sum;
}
所以程序员们需要敏感地将数学等知识用在程序设计中。
一道著名外企面试题的抽丝剥茧
宋宝华 21cnbao@21cn.com 软件报
问题:对于一个字节(8bit)的数据,求其中“1”的个数,要求算法的执行效率尽可能地高。
分析:作为一道著名外企的面试题,看似简单,实则可以看出一个程序员的基本功底的扎实程度。你或许已经
想到很多方法,譬如除、余操作,位操作等,但都不是最快的。本文一步步分析,直到最后给出一个最快的方
法,相信你看到本文最后的那个最快的方法时会有惊诧的感觉。
解答:
首先,很自然的,你想到除法和求余运算,并给出了如下的答案:
方法1:使用除、余操作
75
#include
#define BYTE unsigned char
int main(int argc, char *argv[])
{
int i, num = 0;
BYTE a;
/* 接收用户输入 */
printf("/nPlease Input a BYTE(0~255):");
scanf("%d", &a);
/* 计算1 的个数 */
for (i = 0; i < 8; i++)
{
if (a % 2 == 1)
{
num++;
}
a = a / 2;
}
printf("/nthe num of 1 in the BYTE is %d", num);
return 0;
}
很遗憾,众所周知,除法操作的运算速率实在是很低的,这个答案只能意味着面试者被淘汰!
好,精明的面试者想到了以位操作代替除法和求余操作,并给出如下答案:
方法2:使用位操作
#include
#define BYTE unsigned char
int main(int argc, char *argv[])
{
int i, num = 0;
BYTE a;
/* 接收用户输入 */
printf("/nPlease Input a BYTE(0~255):");
scanf("%d", &a);
/* 计算1 的个数 */
for (i = 0; i < 8; i++)
{
num += (a >> i) &0x01;
}
/*或者这样计算1 的个数:*/
/* for(i=0;i<8;i++)
{
if((a>>i)&0x01)
num++;
}
76
*/
printf("/nthe num of 1 in the BYTE is %d", num);
return 0;
}
方法二中num += (a >> i) &0x01;操作的执行效率明显高于方法一中的
if (a % 2 == 1)
{
num++;
}
a = a / 2;
到这个时候,面试者有被录用的可能性了,但是,难道最快的就是这个方法了吗?没有更快的了吗?方法二真
的高山仰止了吗?
能不能不用做除法、位操作就直接得出答案的呢?于是你想到把0~255 的情况都罗列出来,并使用分支操作,
给出如下答案:
方法3:使用分支操作
#include
#define BYTE unsigned char
int main(int argc, char *argv[])
{
int i, num = 0;
BYTE a;
/* 接收用户输入 */
printf("/nPlease Input a BYTE(0~255):");
scanf("%d", &a);
/* 计算1 的个数 */
switch (a)
{
case 0x0:
num = 0;
break;
case 0x1:
case 0x2:
case 0x4:
case 0x8:
case 0x10:
case 0x20:
case 0x40:
case 0x80:
num = 1;
break;
case 0x3:
case 0x6:
case 0xc:
case 0x18:
77
case 0x30:
case 0x60:
case 0xc0:
num = 2;
break;
//...
}
printf("/nthe num of 1 in the BYTE is %d", num);
return 0;
}
方法三看似很直接,实际执行效率可能还会小于方法二,因为分支语句的执行情况要看具体字节的值,如果a=0,
那自然在第1 个case 就得出了答案,但是如果a=255,则要在最后一个case 才得出答案,即在进行了255
次比较操作之后!
看来方法三不可取!但是方法三提供了一个思路,就是罗列并直接给出值,离最后的方法四只有一步之遥。眼
看着就要被这家著名外企录用,此时此刻,绝不对放弃寻找更快的方法。
终于,灵感一现,得到方法四,一个令你心潮澎湃的答案,快地令人咋舌,算法中不需要进行任何的运算。你
有足够的信心了,把下面的答案递给面试官:
方法4:直接得到结果
#include
#define BYTE unsigned char
/* 定义查找表 */
BYTE numTable[256] =
{
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3,
3, 4, 3, 4, 4, 5, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3,
4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4,
3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3,
4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6,
6, 7, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4,
5, 3, 4, 4, 5, 4, 5, 5, 6, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 2, 3, 3, 4, 3, 4, 4, 5, 3,
4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 3, 4,
4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6,
7, 6, 7, 7, 8
};
int main(int argc, char *argv[])
{
int i, num = 0;
BYTE a = 0;
/* 接收用户输入 */
printf("/nPlease Input a BYTE(0~255):");
scanf("%d", &a);
/* 计算1 的个数 */
/* 用BYTE 直接作为数组的下标取出1 的个数,妙哉! */
78
printf("/nthe num of 1 in the BYTE is %d", checknum[a]);
return 0;
}
这是个典型的空间换时间算法,把0~255 中1 的个数直接存储在数组中,字节a 作为数组的下标,checknum[a]
直接就是a 中“1”的个数!算法的复杂度如下:
时间复杂度:O(1)
空间复杂度:O(2n)
恭喜你,你已经被这家著名的外企录用!老总向你伸出手,说:“Welcome to our company”。
C/C++结构体的一个高级特性――指定成员的位数
宋宝华 21cnbao@21cn.com sweek
在大多数情况下,我们一般这样定义结构体:
struct student
{
unsigned int sex;
unsigned int age;
};
对于一般的应用,这已经能很充分地实现数据了的“封装”。
但是,在实际工程中,往往碰到这样的情况:那就是要用一个基本类型变量中的不同的位表示不同的含义。譬
如一个cpu 内部的标志寄存器,假设为16 bit,而每个bit 都可以表达不同的含义,有的表示结果是否为0,
有的表示是否越界等等。这个时候我们用什么数据结构来表达这个寄存器呢?
答案还是结构体!
为达到此目的,我们要用到结构体的高级特性,那就是在基本成员变量的后面添加:
: 数据位数
组成新的结构体:
struct xxx
{
成员1 类型成员1 : 成员1 位数;
成员2 类型成员2 : 成员2 位数;
成员3 类型成员3 : 成员3 位数;
};
基本的成员变量就会被拆分!这个语法在初级编程中很少用到,但是在高级程序设计中不断地被用到!
例如:
struct student
{
unsigned int sex : 1;
unsigned int age : 15;
};
上述结构体中的两个成员sex 和age 加起来只占用了一个unsigned int 的空间(假设unsigned int 为16 位)。
基本成员变量被拆分后,访问的方法仍然和访问没有拆分的情况是一样的,例如:
79
struct student sweek;
sweek.sex = MALE;
sweek.age = 20;
虽然拆分基本成员变量在语法上是得到支持的,但是并不等于我们想怎么分就怎么分,例如下面的拆分显然是
不合理的:
struct student
{
unsigned int sex : 1;
unsigned int age : 12;
};
这是因为1+12 = 13,不能再组合成一个基本成员,不能组合成char、int 或任何类型,这显然是不能“自圆
其说”的。
在拆分基本成员变量的情况下,我们要特别注意数据的存放顺序,这还与CPU 是Big endian 还是Little endian
来决定。Little endian 和Big endian 是CPU 存放数据的两种不同顺序。对于整型、长整型等数据类型,Big
endian 认为第一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节);而
Little endian 则相反,它认为第一个字节是最低位字节(按照从低地址到高地址的顺序存放数据的低位字节到
高位字节)。
我们定义IP 包头结构体为:
struct iphdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
__u8 ihl:4,
version:4;
#elif defined (__BIG_ENDIAN_BITFIELD)
__u8 version:4,
ihl:4;
#else
#error "Please fix <asm/byteorder.h>"
#endif
__u8 tos;
__u16 tot_len;
__u16 id;
__u16 frag_off;
__u8 ttl;
__u8 protocol;
__u16 check;
__u32 saddr;
__u32 daddr;
/*The options start here. */
};
在Little endian 模式下,iphdr 中定义:
__u8 ihl:4,
version:4;
其存放方式为:
第1 字节低4 位 ihl
80
第1 字节高4 位 version (IP 的版本号)
若在Big endian 模式下还这样定义,则存放方式为:
第1 字节低4 位 version (IP 的版本号)
第1 字节高4 位 ihl
这与实际的IP 协议是不匹配的,所以在Linux 内核源代码中,IP 包头结构体的定义利用了宏:
#if defined(__LITTLE_ENDIAN_BITFIELD)

#elif defined (__BIG_ENDIAN_BITFIELD)

#endif
来区分两种不同的情况。
由此我们总结全文的主要观点:
(1)C/C++语言的结构体支持对其中的基本成员变量按位拆分;
(2)拆分的位数应该是合乎逻辑的,应仍然可以组合为基本成员变量;
要特别注意拆分后的数据的存放顺序,这一点要结合具体的CPU 的结构。
C/C++中的近指令、远指针和巨指针
宋宝华 email:21cnbao@21cn.com sweek
在我们的C/C++学习生涯中、在我们大脑的印象里,通常只有指针的概念,很少听说指针还有远、近、巨之分
的,从没听说过什么近指针、远指针和巨指针。
可以,某年某月的某一天,你突然看到这样的语句:
char near *p; /*定义一个字符型“近”指针*/
char far *p; /*定义一个字符型“远”指针*/
char huge *p; /*定义一个字符型“巨”指针*/
实在不知道语句中的“near”、“far”、“huge”是从哪里冒出来的,是个什么概念!本文试图对此进行解答,解
除许多人的困惑。
这一点首先要从8086 处理器体系结构和汇编渊源讲起。大家知道,8086 是一个16 位处理器,它设定
了四个段寄存器,专门用来保存段地址:CS(Code Segment):代码段寄存器;DS(Data Segment):
数据段寄存器;SS(Stack Segment):堆栈段寄存器;ES(Extra Segment):附加段寄存器。8086 采
用段式访问,访问本段(64K 范围内)的数据或指令时,不需要变更段地址(意味着段地址寄存器不需修改),
而访问本段范围以外的数据或指令时,则需要变更段地址(意味着段地址寄存器需要修改)。
因此,在16 位处理器环境下,如果访问本段内地址的值,用一个16 位的指针(表示段内偏移)就可以
访问到;而要访问本段以外地址的值,则需要用16 位的段内偏移+16 位的段地址,总共32 位的指针。
这样,我们就知道了远、近指针的区别:
Ø 近指针是只能访问本段、只包含本段偏移的、位宽为16 位的指针;
Ø 远指针是能访问非本段、包含段偏移和段地址的、位宽为32 位的指针。
近指针只能对64k 字节数据段内的地址进行存取,如:
char near *p;
p=(char near *)0xffff;
远指针是32 位指针,它表示段地址:偏移地址,远指针可以进行跨段寻址,可以访问整个内存的地址。如定
81
义远程指针p 指向0x1000 段的0x2 号地址,即1000:0002,则可写作:
char far *p;
p=(char far *)0x10000002;
除了远指针和近指针外,还有一个巨指针的概念。
和远指针一样,巨指针也是32 位的指针,指针也表示为16 位段:16 位偏移,也可以寻址任何地址。它和远
指针的区别在于进行了规格化处理。远指针没有规格化,可能存在两个远指针实际指向同一个物理地址,但是
它们的段地址和偏移地址不一样,如23B0:0004 和23A1:00F4 都指向同一个物理地址23604!巨指针通过
特定的例程保证:每次操作完成后其偏移量均小于10h,即只有最低4 位有数值,其余数值都被进位到段地址
上去了,这样就可以避免Far 指针在64K 边界时出乎意料的回绕的行为。当然,一次操作必须小于64K。下
面的函数可以将远指针转换为巨指针:
void normalize(void far ** p)
{
*p=(void far *)(((long)*p&0xffff000f)+(((long)*p&0x0000fff00<<12));
}
从上面的函数中我们再一次看到了指针之指针的使用,这个函数要修改指针的值,因此必须传给它的指针的指
针作为参数。
讲到这里,笔者要强调的是:近指针、远指针、巨指针是段寻址的16bit 处理器的产物(如果处理器是16 位
的,但是不采用段寻址的话,也不存在近指针、远指针、巨指针的概念),当前普通PC 所使用的32bit 处理
器(80386 以上)一般运行在保护模式下的,指针都是32 位的,可平滑地址,已经不分远、近指针了。但是
在嵌入式系统领域下,8086 的处理器仍然有比较广泛的市场,如AMD 公司的AM186ED、AM186ER 等处理
器,开发这些系统的程序时,我们还是有必要弄清楚指针的寻址范围。
如果读者还想更透彻地理解本文讲解的内容,不妨再温习一下微机原理、8086 汇编,并参考C/C++高级编程书籍
的相关内容。
从两道经典试题谈C/C++中联合体(union)的使用
宋宝华 21cnbao sweek@21cn.com
试题一:编写一段程序判断系统中的CPU 是Little endian 还是Big endian 模式?
分析:
作为一个计算机相关专业的人,我们应该在计算机组成中都学习过什么叫Little endian 和Big endian。Little
endian 和Big endian 是CPU 存放数据的两种不同顺序。对于整型、长整型等数据类型,Big endian 认为第
一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节);而Little endian 则
相反,它认为第一个字节是最低位字节(按照从低地址到高地址的顺序存放数据的低位字节到高位字节)。
例如,假设从内存地址0x0000 开始有以下数据:
0x0000 0x0001 0x0002 0x0003
0x12 0x34 0xab 0xcd
如果我们去读取一个地址为0x0000 的四个字节变量,若字节序为big-endian,则读出结果为0x1234abcd;
若字节序位little-endian,则读出结果为0xcdab3412。如果我们将0x1234abcd 写入到以0x0000 开始的
内存中,则Little endian 和Big endian 模式的存放结果如下:
地址 0x0000 0x0001 0x0002 0x0003
82
big-endian 0x12 0x34 0xab 0xcd
little-endian 0xcd 0xab 0x34 0x12
一般来说,x86 系列CPU 都是little-endian 的字节序,PowerPC 通常是Big endian,还有的CPU 能通过
跳线来设置CPU 工作于Little endian 还是Big endian 模式。
解答:
显然,解答这个问题的方法只能是将一个字节(CHAR/BYTE 类型)的数据和一个整型数据存放于同样的内存
开始地址,通过读取整型数据,分析CHAR/BYTE 数据在整型数据的高位还是低位来判断CPU 工作于Little
endian 还是Big endian 模式。得出如下的答案:
typedef unsigned char BYTE;
int main(int argc, char* argv[])
{
unsigned int num,*p;
p = &num;
num = 0;
*(BYTE *)p = 0xff;
if(num == 0xff)
{
printf("The endian of cpu is little/n");
}
else //num == 0xff000000
{
printf("The endian of cpu is big/n");
}
return 0;
}
除了上述方法(通过指针类型强制转换并对整型数据首字节赋值,判断该赋值赋给了高位还是低位)外,还有没
有更好的办法呢?我们知道,union 的成员本身就被存放在相同的内存空间(共享内存,正是union 发挥作用、
做贡献的去处),因此,我们可以将一个CHAR/BYTE 数据和一个整型数据同时作为一个union 的成员,得出
如下答案:
int checkCPU()
{
{
union w
{
int a;
char b;
} c;
c.a = 1;
return (c.b == 1);
}
}
实现同样的功能,我们来看看Linux 操作系统中相关的源代码是怎么做的:
static union { char c[4]; unsigned long l; } endian_test = { { 'l', '?', '?', 'b' } };
83
#define ENDIANNESS ((char)endian_test.l)
Linux 的内核作者们仅仅用一个union 变量和一个简单的宏定义就实现了一大段代码同样的功能!由以上一段
代码我们可以深刻领会到Linux 源代码的精妙之处!(如果ENDIANNESS=’l’表示系统为little endian,
为’b’表示big endian )
试题二:假设网络节点A 和网络节点B 中的通信协议涉及四类报文,报文格式为“报文类型字段+报文内容的结
构体”,四个报文内容的结构体类型分别为STRUCTTYPE1~ STRUCTTYPE4,请编写程序以最简单的方式组
织一个统一的报文数据结构。
分析:
报文的格式为“报文类型+报文内容的结构体”,在真实的通信中,每次只能发四类报文中的一种,我们可以将四
类报文的结构体组织为一个union(共享一段内存,但每次有效的只是一种),然后和报文类型字段统一组织
成一个报文数据结构。
解答:
根据上述分析,我们很自然地得出如下答案:
typedef unsigned char BYTE;
//报文内容联合体
typedef union tagPacketContent
{
STRUCTTYPE1 pkt1;
STRUCTTYPE2 pkt2;
STRUCTTYPE3 pkt1;
STRUCTTYPE4 pkt2;
}PacketContent;
//统一的报文数据结构
typedef struct tagPacket
{
BYTE pktType;
PacketContent pktContent;
}Packet;
总结
在C/C++程序的编写中,当多个基本数据类型或复合数据结构要占用同一片内存时,我们要使用联合体(试题
一是这样的例证);当多种类型,多个对象,多个事物只取其一时(我们姑且通俗地称其为“n 选1”),我们也
可以使用联合体来发挥其长处(试题二是这样的例证)。
基于ARM 的嵌入式Linux 移植真实体验
基于ARM 的嵌入式Linux 移植真实体验(1)――基本概念
宋宝华 21cnbao@21cn.com 出处:dev.yesky.com
84
1.引言
ARM 是Advanced RISC Machines(高级精简指令系统处理器)的缩写,是ARM 公司提供的一种微处理
器知识产权(IP)核。
ARM 的应用已遍及工业控制、消费类电子产品、通信系统、网络系统、无线系统等各类产品市场。
基于ARM 技术的微处理器应用约占据了32 位RISC 微处理器75%以上的市场份额。揭开你的手机、MP3、
PDA,嘿嘿,里面多半藏着一个基于ARM 的微处理器!
ARM 内核的数个系列(ARM7、ARM9、ARM9E、ARM10E、SecurCore、Xscale、StrongARM),各自满
足不同应用领域的需求,无孔不入的渗入嵌入式系统各个角落的应用。这是一个ARM 的时代!
下面的图片显示了ARM 的随处可见:
有人的地方就有江湖(《武林外传》),有嵌入式系统的地方就有ARM。
构建一个复杂的嵌入式系统,仅有硬件是不够的,我们还需要进行操作系统的移植。我们通常在ARM
平台上构建Windows CE、Linux、Palm OS 等操作系统,其中Linux 具有开放源代码的优点。
下图显示了基于ARM 嵌入式系统中软件与硬件的关系:
日前,笔者作为某嵌入式ARM(硬件)/Linux(软件)系统的项目负责人,带领项目组成员进行了下述
85
工作:
(1)基于ARM920T 内核S3C2410A CPU 的电路板设计;
(2)ARM 处理下底层软件平台搭建:
a.Bootloader 的移植;
b.嵌入式Linux 操作系统内核的移植;
c.嵌入式Linux 操作系统根文件系统的创建;
d.电路板上外设Linux 驱动程序的编写。
本文将真实地再现本项目开发过程中作者的心得,以便与广大读者共勉。第一章将简单地介绍本ARM
开发板的硬件设计,第二章分析Bootloader 的移植方法,第三章叙述嵌入式 Linux 的移植及文件系统
的构建方法,第四章讲解外设的驱动程序设计,第五章给出一个已构建好的软硬件平台上应用开发的
实例。
如果您有良好的嵌入式系统开发基础,您将非常容易领会本文讲解地内容。即便是您从来没有嵌入式
系统的开发经历,本文也力求让您读起来不觉得生涩。您可以通过如下email 与作者联系:
21cnbao@21cn.com
2.ARM 体系结构
作为一种RISC 体系结构的微处理器,ARM 微处理器具有RISC 体系结构的典型特征。还具有如下增
强特点:
(l)在每条数据处理指令当中,都控制算术逻辑单元(ALU)和移位器,以使ALU 和移位器获得最大
的利用率;
(2)自动递增和自动递减的寻址模式,以优化程序中的循环;
(3)同时Load 和Store 多条指令,以增加数据吞吐量;
(4)所有指令都条件执行,以增大执行吞吐量。
ARM 体系结构的字长为32 位,它们都支持Byte(8 位)、Halfword(16 位)和Word(32 位)3 种数据类型。
ARM 处理器支持7 种处理器模式,如下表:
大部分应用程序都在User 模式下运行。当处理器处于User 模式下时,执行的程序无法访问一些被
保护的系统资源,也不能改变模式,否则就会导致一次异常。对系统资源的使用由操作系统来控制。
User 模式之外的其它几种模式也称为特权模式,它们可以完全访问系统资源,可以自由地改变模式。
其中的FIQ、IRQ、supervisor、Abort 和undefined 5 种模式也被称为异常模式。在处理特定的异常
时,系统进入这几种模式。这5 种异常模式都有各自的额外的寄存器,用于避免在发生异常的时候与
用户模式下的程序发生冲突。
还有一种模式是system 模式,任何异常都不会导致进入这一模式,而且它使用的寄存器和User 模式
下基本相同。它是一种特权模式,用于有访问系统资源请求而又需要避免使用额外的寄存器的操作系
统任务。
程序员可见的ARM 寄存器共有37 个:31 个通用寄存器以及6 个针对ARM 处理器的不同工作模式所设立
的专用状态寄存器,如下图:
86
ARM9 采用5 级流水线操作:指令预取、译码、执行、数据缓冲、写回。ARM9 设置了16 个字的数据缓
冲和4 个字的地址缓冲。这5 级流水已被很多的RISC 处理器所采用,被看作RISC 结构的“经典”。
3.硬件设计
3.1 S3C2410A 微控制器
电路板上的ARM 微控制器S3C2410A 采用了ARM920T 核,它由ARM9TDMI、存储管理单元MMU 和高速缓存
三部分组成。其中,MMU 可以管理虚拟内存,高速缓存由独立的16KB 地址和16KB 数据高速Cache 组成。
ARM920T 有两个内部协处理器:CP14 和CP15。CP14 用于调试控制,CP15 用于存储系统控制以及测试控
制。
S3C2410A 集成了大量的内部电路和外围接口:
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C/C+语言struct 深层探索............................................................................2 C++中extern "C"含义深层探索........................................................................7 C 语言高效编程的几招...............................................................................11 想成为嵌入式程序员应知道的 0x10 个基本问题.........................................................15 C 语言嵌入式系统编程修炼...........................................................................22 C 语言嵌入式系统编程修炼之一:背景篇............................................................22 C 语言嵌入式系统编程修炼之二:软件架构篇........................................................24 C 语言嵌入式系统编程修炼之三:内存操作..........................................................30 C 语言嵌入式系统编程修炼之四:屏幕操作..........................................................36 C 语言嵌入式系统编程修炼之五:键盘操作..........................................................43 C 语言嵌入式系统编程修炼之六:性能优化..........................................................46 C/C++语言 void 及void 指针深层探索.................................................................50 C/C++语言可变参数表深层探索.......................................................................54 C/C++数组名与指针区别深层探索.....................................................................60 C/C++程序员应聘常见面试题深入剖析(1) ..............................................................62 C/C++程序员应聘常见面试题深入剖析(2) ..............................................................67 一道著名外企面试题的抽丝剥茧 ......................................................................74 C/C++结构体的一个高级特性――指定成员的位数.......................................................78 C/C++中的近指令、远指针和巨指针...................................................................80 从两道经典试题谈 C/C++中联合体(union)的使用......................................................81 基于 ARM 的嵌入式Linux 移植真实体验................................................................83 基于 ARM 的嵌入式Linux 移植真实体验(1)――基本概念...........................................83 基于 ARM 的嵌入式Linux 移植真实体验(2)――BootLoader .........................................96 基于 ARM 的嵌入式Linu

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值