单片机原理与应用 C/C++在现代数字计算机上的实现
课程项目 安装并配置51单片机的软件开发环境 Code::Blocks+SDCC 实验报告模板有实验所需文件及详细实验步骤。从课程主页下载并逐步填写并提交。 严禁抄袭 提交截止:2015-10-20 23:59
C/C++的实现 C/C++在现代数字计算机上的实现 各种语法映射到实际硬件实现 高效地实现语言的各项特性 提供一定的资源管理能力 提供方便开发的工具、库
平坦地址空间模型 所有数据、代码共享一个地址空间 便于操作系统管理和分配内存 程序需要存储哪些信息? 如何实现这些功能和信息? 函数 数据 各个函数的指令 数据 全局变量,静态变量,局部(自动)变量 动态分配的存储空间,函数调用信息 如何实现这些功能和信息? 必须要清楚各自的功能和特点
局部(自动)变量 仅在定义变量的函数被调用时存在 当函数被递归调用时,可有多个不同变量 void main() { pow(0.5,3); } 局部(自动)变量 仅在定义变量的函数被调用时存在 当函数被递归调用时,可有多个不同变量 float pow(float x, int n) { if(n==1) return x; if(n==0) return 1; int t = n/2; return pow(x,t)*pow(x,n-t); } main() x=?? n=?? t=?? pow(0.5,3) x=0.5 n=3 t=1 x=0.5 n=1 t=?? x=0.5 n=2 t=1 pow(0.5,1) pow(0.5,2) pow(0.5,1)
同步地把Root换成this(多线程安全) volatile: 如一个变量可能被多个线程修改,应定义为volatile this: C++成员函数中指向当前对象的指针 全局变量 程序的整个生存周期都存在 常量 全局资源的管理数据 const char *FlagToName[]= { “create”, “open”, “append” }; 同步地把Root换成this(多线程安全)
静态变量 程序的整个生存周期内存在 当函数被递归调用时,也仅有一个变量 大多数静态变量可以用类变量代替 void main() { pow(0.5,3); } 静态变量 程序的整个生存周期内存在 当函数被递归调用时,也仅有一个变量 float pow(float x, int n) { static int entryCount=0; entryCount ++; …… main() entryCount=0 pow(0.5,3) entryCount=1 entryCount=2 entryCount=3 pow(0.5,1) pow(0.5,2) 大多数静态变量可以用类变量代替 pow(0.5,1)
程序的其它信息 动态分配的存储空间 函数调用信息(返回地址等) 函数 生存周期由程序员控制 与局部变量相同 程序的整个生存周期内存在 可能跨越函数,也可能仅跨越几条语句 函数调用信息(返回地址等) 与局部变量相同 函数 程序的整个生存周期内存在 与各种数据都很不相同 与常量有一定相似性
程序的各种信息总结 程序的整个生存周期内存在 仅在定义变量的函数被调用时存在 由程序员控制 各部分应分别管理,放置在地址空间的不同部位 函数,全局变量,静态变量 仅在定义变量的函数被调用时存在 局部变量,函数调用信息 由程序员控制 动态分配的存储空间 各部分应分别管理,放置在地址空间的不同部位
程序在内存中的映象 全局和静态数据的实现没有区别 防止空指针错误 00000000 未映射空间 文件头 函数 函数1 数据 开始执行位置 __startup 堆 函数在内存的顺序并不重要 call _main ~ main() call _exit 栈 函数n 操作系统的 代码、数据 FFFFFFFF
栈 管理函数调用和返回,分配局部变量 为什么用栈? 栈指针 栈框架 SP 函数可以递归调用 指向栈顶部的指针 未分配空间 SP printf() SF 管理函数调用和返回,分配局部变量 为什么用栈? 函数可以递归调用 栈指针 指向栈顶部的指针 栈框架 一个函数的调用信息、局部变量等所有的信息 Go() SF Test() SF main() SF 已分配空间 不能返回指向局部变量的指针或引用!
栈 SP SP ptr y (3) printf SP main main SA int * ptr () { int y=3; return &y; }; main () { int *SA,content; SA = ptr(); content = *SA; printf("%d", content); /*??*/ content = *SA; printf("%d", content); /*??*/ }; SP SP ptr y (3) printf SP main SA(??) main SA main SA See 3 at stackAddress, its equal to 3
栈 空间比较小:32位平台约1MB左右 如果函数调用太多,或者自动变量太多? Stack Overflow: 栈溢出
堆 堆:程序员主动管理的内存 返回值:地址(?) 如何使用? 叫啥名? size new char[16*1024*1024];
指针(pointer) 存储地址的变量 char *photo = new char[16*1024*1024]; 堆 00230002 00230003 00230004 栈 ffff0008 ffff000c ffff0010 ffff0014 photo 0023004
指针 指针存储的是地址 指针和整数的区别:没有! 一种情况是从堆分配的内存 合法的指针:(char*)12345 还有别的情况 指针和整数的区别:没有! 合法的指针:(char*)12345 指针的大小通常和整数的大小相同 32位平台,通常是32位,或4字节 与指针所指向的内容的大小无关
指针 指针是变量 指针的值可以被改变 变的是什么? char *photo=new char[16*1024*1024]; …… 0011004 也没变! 变了! 0023004 没变!
指针 访问指针指向的内容 指针可以如数组名一样使用 堆中的内存块就是数组 char *photo=new char[size]; for(int i=0;i<size;i++) photo[i] = 0; photo[eyePos] = …… faceColor = photo[facePos];
指针 易犯的错误 编译没有问题 typedef struct {…}TMyStruct; TMyStruct *sptr = (TMyStruct *)malloc(n*sizeof(TMyStruct *)); C++ C 这个*是不能有的 struct TMyStruct{…}; TMyStruct *sptr = new TMyStruct *; 编译报错
指针 易犯的错误 假设变量大小 尽量用sizeof计算变量的大小 int *dynArray = (int*)malloc(mySize*4); 尽量用sizeof计算变量的大小 特别是结构的大小 结构的大小在不同的编译选项下面可能有差异
指针 指针的作用域 指针指向的内容呢? 如其它的变量 void ShowPhoto(const char *FileName) { …… char *photo=new char[fileSize]; } 指针指向的内容呢?
指针 指针指向内容的作用域 由程序员控制 不再使用的时候,要释放 void ShowPhoto(const char *FileName) { …… char *photo=new char[fileSize]; delete []photo; }
指针 如果忘了释放? 0023004 还在! 内存泄漏(memory leak)!
指针 如果尚未分配内存就访问其指向的内容? 非法指针访问! char *photo; photo[facePos] = clPink; 别的变量 00132357 非法指针访问!
指针 如果访问的单元超出了分配的范围? 指针越界! char *name=new char[5]; name[5] = 0; Name[-1] = ‘L’; 00230002 00230003 00230004 00230005 00230006 00230007 00230008 00230009 00230004 指针越界!
指针 如何防止这些错误 小心? 开发工具? 良好的书写风格 人总是有犯错误的时候! Borland C++ Builder 6以上版本可查这些错误 运行缓慢,需要很长时间调试 良好的书写风格 可以避免一部分错误
指针 良好的书写风格 总是把指针初始化成空 NULL 可以通过调试检测非法指针访问错误 空:通常定义为(void*)0 地址为0的内存单元一般被定义为不能被访问,如果有对这个单元的访问,则一定是错误 一般操作系统把最前和最后64KB地址都设为不能访问 可以通过调试检测非法指针访问错误 如何做? char *photo = NULL;
指针 空指针错误
指针 空指针错误 实验代码:NULL.7z(从课程主页下载)
指针 用C++类防止内存泄漏 构造函数:在定义变量的一开始被自动调用 析构函数:在变量被释放前被自动调用 均由编译器在适当的位置自动插入指令完成 不需要程序员管理 可以“忘掉”这些事情! 可以方便地管理内存分配与释放
指针 class TCharArray { public: char *img; int len; TCharArray() 要管理的内存 img = NULL; len = 0; } …… 要管理的内存 分配的内存大小 构造函数 初始化为空状态
指针 ~TCharArray() { 析构函数 Close(); } 调用内存释放函数(很重要!) void Close() ……//接上页 ~TCharArray() { Close(); } void Close() if(img!=NULL) delete []img; img = NULL; len = 0; …… 析构函数 调用内存释放函数(很重要!) 内存释放函数 测试是否已经分配 如是,则释放,并恢复成空状态
指针 void Alloc(int size) { Close(); img = new char[size]; len = size; ……//接上页 void Alloc(int size) { Close(); img = new char[size]; len = size; } …… }; 内存分配函数 调用内存释放函数(很重要!) 分配内存,设置成合法状态 实现其它功能
指针 如何使用? 定义变量,分配内存 如何访问其指向的内容? TCharArray photo; faceColor = photo.img[facePos]; 书写很麻烦 希望:… = photo[facePos]; TCharArray photo; photo.Alloc(fileSize);
指针 操作符重载 重新定义操作符的含义 class TCharArray { …… char operator[](int inx) return img[inx]; } }; 前面的内容 重载下标操作符[]
指针 指针越界? … = photo[-1]; … = photo[fileSize]; class TCharArray { …… char operator[](int inx) return img[inx]; } }; if ( (inx<0) || (inx>=len) ) { ……//出错处理 }
指针 什么样的出错处理比较合适? 不知道!(程序逻辑上已经不可恢复) 用assert #include <_assert.h> class TCharArray { …… char operator[](int inx) assert( (i>=0) && (i<len) ); return img[inx]; } }; 两处都需要!
指针 如何对其内容赋值? photo[facePos] = clPink; Compiler Error: LValue Required! photo.img[facePos] = clPink; 麻烦!
指针 解决办法:返回引用 唯一的变化 char &operator[](int inx) { …… char &operator[](int inx) { assert( (i>=0) && (i<len) ); return img[inx]; } }; TCharArray photo; photo.Alloc(fileSize); …… photo[facePos] = clPink;
指针 返回引用和返回普通变量的区别 char &operator[](int inx) { assert( (i>=0) && (i<len) ); return img[inx]; } char operator[](int inx) { assert( (i>=0) && (i<len) ); return img[inx]; } 再返回 直接返回该变量的访问方法 复制一份 char tmp=img[inx]
指针 多种数据类型,为每个类型写一个类? class TFloatArray class TIntArray class TDoubleArray class TLongArray class TShortArray class TCharArray
模板 模板:管你是什么类型 TArray<int> IntArray; template<class T> class TArray { public: T *Data; …… void Alloc(int n) { Close(); Data = new T[n];} T &operator[](int n) { return Data[n];} TArray<int> IntArray; TArray<float> FloatArray; TArray<TMyStruct> SA; TArray<TArray<int> > AA;
资源泄漏 用类机制防止其它资源泄漏 与上面过程类似 class TMyFile { public: FILE *MyFile; ……
函数 高级语言写的函数必须翻译成机器语言 需要翻译的内容 可执行文件中函数即是机器语言指令串 参数传递 函数调用 局部变量 函数内在功能(算法) 返回
函数 调用函数需要进行的操作 SP SP SP SP ret_add var1 fmt count …… 第一步:参数传递(先不管顺序) void main() { int count = 10; printf(“%d times hello”,count); …… SP var1 SP fmt SP count …… 第一步:参数传递(先不管顺序) 第二步:记录返回值,转到函数入口
(ret_add到fmt之间的长度不定) 函数 调用函数需要进行的操作 SP i SP ret_add int printf(const char *fmt,...) { for(int i=0;fmt[i]!=0;i++) if(fmt[i]==‘%’) …… var1 fmt 从右到左传递参数 var1 fmt count …… 第三步:分配局部变量 函数如何知道fmt在哪里? (ret_add到fmt之间的长度不定)
函数 函数的参数传递 __cdecl, __stdcall: 从右到左顺序压栈 __pascal:从左到右顺序压栈 还有其它差别 __pascal:从左到右顺序压栈 __fastcall: 利用寄存器传递参数 速度稍快
函数 函数返回 SP SP SP i ret_add 第一步:清除局部变量 fmt 第二步:返回 var1 count 谁清除调用参数? …… __cdecl, __pascal : 调用函数 __stdcall: 被调用函数 __fastcall: 基本不用清除
函数 函数的参数传递与清除 __cdecl: 从右到左,调用函数清除 __stdcall: 从右到左,被调用函数清除 可以方便地支持可变参数个数 在参数表末尾添加不必要的参数不会导致问题 __stdcall: 从右到左,被调用函数清除 __pascal: 从左到右,调用函数清除 __fastcall: 利用寄存器传递参数
函数 函数重载的实现 函数名转换 int OpenFile(const char *FileName) { …… int OpenFile(const wchar_t *FileName) @@OpenFile$qpxc proc near …… @@OpenFile$qpxb proc near
函数 函数名转换 调用方式的区别 大小写 名修饰(name mangling, name decoration) __cdecl, __stdcall, __fastcall:区分 __pascal:不区分 名修饰(name mangling, name decoration) C:不修饰(但可选择头上加下划线) extern “C” {…} C++:修饰(可以重载) extern “C++” {…}
函数 函数名转换 不同编译器的名修饰是不同的! 不同编译器编译的C++库不能通用 C++DLL不能被不同编译器编译的执行文件调用 编写DLL应使用extern “C” {…}以避免名修饰 名字前面的下划线? Ansi C/C++:缺省加 VC:缺省不加(且加一个尾巴!) 如果DLL链接出错,看看是否相应选项正确
函数 函数的地址和函数指针 CPU不知道名字 编译器必须把所有名字翻译成地址 ...... E8 ???? ...... void main() { int count = 10; printf(“%d times hello”,count); …… ...... …… call ???? E8 ???? ......
函数 函数的地址和函数指针 编译器不知道别的模块的名字对应的地址 把需要地址的地方记录下来 ...... E8 00000000 0x1234abcd ...... 0x1234abcd: _printf 引入符号表
函数 函数的地址和函数指针 编译器知道本模块的名字的地址 要把这些名字的地址告诉后续工具 main() Usage() push ebp ...... ret push ebp ... 0x00000000 ...... 0x00001234 ...... 0x00000000: _main 0x00001234: _Usage ...... 引出符号表
函数 函数的地址和函数指针 编译器的功能 但是 如何让程序执行起来? .c/.cpp ----> .obj 高级语言函数“翻译”成机器语言函数 但是 .obj不能执行 尚未解决的地址 如何让程序执行起来?
函数 函数的地址和函数指针 连接器 一个程序要调用多个函数 编译器生成多个独立的函数 连接器把函数合并到一起成为程序
函数 函数的地址和函数指针 连接器 0x00010000 ? __startup ? __startup 0x00001234: _main 0x00005678: _exit
函数 函数的地址和函数指针 连接器 0x00010000 __startup 0x0001abcd _main ? 0x0001ef01 _exit _main 0x0000abcd: _printf _exit
函数 函数的地址和函数指针 连接器 0x00010000 __startup 0x0001abcd _main _printf …… 0x0001ef01 _exit _printf ? ……
函数 ?库文件一定叫.lib? 函数的地址和函数指针 经常使用的函数 库:收集经常使用的函数到一起 每次都重新编译? 放到哪里? _printf _scanf ...... Dictionary _printf: ...... _scanf: ...... ...... ?库文件一定叫.lib?
函数 函数的地址和函数指针 生成程序的全过程 中间临时输出路径 .c .cpp .pas .f .asm 编译器 汇编器 .h .hpp .inc ...... .obj .lib tlink.exe .exe tlib.exe 输出路径 库文件路径 源文件路径 头文件路径
函数 函数的地址和函数指针 函数地址 函数的执行入口在内存中的地址 函数指针 理论上:执行文件被加载到内存后才能确定函数地址 实际上:系统均有很强的约定,大多数函数地址可以在连接时确定 函数指针 指向函数的入口的指针 有什么用?
函数 函数指针 作用:调用所指向的函数 修改函数的代码? 获得函数的代码? 一般情况下不作这些用处 理论上可行 为什么用指针? 不用名?
函数 函数指针 当编译的时候无法确定函数名,则不能用名调用函数,只能用指针 例1:排序算法,编译时不能确定需要排序的数组的具体类型,就无法确定比较函数 例2:虚函数,编译时无法确定指向基类的指针的实际类型,就无法确定该用哪个函数 虚函数在编译器中是用函数指针实现的
函数 函数指针 定义和使用函数指针类型的语法 typedef int (*TCmp)(const void *,const void *); 可以不加名字 差异所在 基本上是函数的声明 函数指针可以如一般函数名一样使用 函数指针类型可以如一般变量类型使用 void qsort(void *base, size_t nelem, size_t width, TCmp cmp) {…… int cmpResult = cmp( (void*)(((int)base)+width*…);