第一篇:编程知识总结C难点总结
《编程知识总结》—C#难点总结
1.编写菜单中对应子菜单项的消息响应函数,考虑的方面很多,例 如,当前打开一个文件,此文件已经被修改,如果用户需要新建 或者打开另外一个文件,程序要询问用户是否保存当前的文件。
问题的解决:先用纸张写一份详细的业务流程图,在之后的编写 过程中,按照业务流程的规定进行功能的逐步实现。
2.编写判断是否要提醒用户保存的程序,判断是否提醒保存的根据 是什么。我的判断根据验证文件内容是否被修改,如果被修改,提醒用户保存。如果被保存的文件没有文件路径,调用另存为的 响应函数进行执行。
3.无法关闭窗体,当我响应文件菜单下退出菜单项的时候,如果使 用 Close()函数的话,可以就直接退出,但是当我需要以同样的 方式响应右上角关闭按钮的话,事件信息就会进入死循环,而且 永远不会结束。所以,在关闭窗体的时候,目前我用的就是 Application.exit();这个函数,强制性终结这个窗口
4.保存文件,我们打开文件和直接输入信息的文件的情况,对于保 存来说是不一样的,因为打开的时候,应该是直接保存到指定的 文件中,而直接输入的应该提示保存,通过另存为的方式保存。5.文件的删除。C#的 textBox 没有直接为我们提供文本删除的函数,我弄了许久,后来,无意间突然发现通过已经选择的字符串的下 标来操作,就可以删除文件了,具体操作就是,先获取被选择文 件的前半部分,在获取后半部分,而略过中间被选中的部分即可。6.开始的时候没有使用 Using System.IO,系统报错;
7.由于在 MessageBox.Show 后没有写 return 导致错误;
8.初步创建简易记事本后点击打开文件选项并不显示文件的内容,且编译器报错了,是由于没有判断文件名为空,且文件的完整路 径名获取的不准确,将文件名为空的情况及其文件的完整路径名 获取之后,读取文件正确;
9.首先,我在添加单击“新建”响应的代码时,单击进去,结果代 码全部不能运行,必须是双击。
10.获取文件名时,文件名不需要特别处理,程序内部直接处理好的,我在这里画蛇添足了。
11.操作对象必须指代明确,不如要出错。
12.在用 WinForm 做文本文档的时候,在“新建文本文档”项的代码 中,需要在项目中新建一个 new 窗体作为“新建文本文档”,在 form1 的代码中写如下代码: Form frm = new NewForm();frm.Show();this.Hide();然后在 Form2 中下如同样的代码即可。
13.还发现“保存”与“另存为”项的代码编写没有任何差异,考虑 并修改程序无果。代码如下: private void 保存SToolStripMenuItem_Click(object sender, EventArgs e)// { DialogResult save = saveFileD
ialog1.ShowDialog();
if
(save
== DialogResult.OK){ string filepath = saveFileDialog1.FileName;//FileInfo fi = new FileInfo(filepath);string content = richTextBox1.Text;File.WriteAllText(filepath, content, Encoding.Default);} } 14.在保存的时候必须输入保存路径才能正常执行,没有实现文本文 档的基本保存功能。并且在打开一个原本有内容的文本文档之后,加以修改再保存就不能保存新的内容。
15.获取 TextBox 中获取一行的值,使用 TextBox 的 Lines 属性来获 取
16.做删除操作时,由 MessageBoxv.show 来弹出一个确认窗体,把返 回的值赋给一个 DialogResult 类型的变量
17.在复制文件时,CopyTo 函数将第二个参数置为 TRUE 则在复制时,如果文件存在会被覆盖掉。
18.在向文件写入数据时,使用 Encoding 的 Default 的属性可避免输 入中文乱码问题。19.关于上午的文件编程,刚开始对程序要执行的具体操作不够明确,在编程时,功能实现的矛盾性问题很多,其问题在于编程前分析 不够到位;
20.文件编程中起先将打开文件定位为文件夹,导致弹出对话框显示 “不存在此文件”; 21.File.WriteAllLines(),无法将临时修改后的文件信息进行正 确保存,文本没有实现正确的回车换行;File.WriteAllText()能实现;
22.this.Close()无法实现程序的结束 Close()方法是关闭 Form 23.当创建了控件的响应事件后,修改控件的 name 属性,响应事件 名为改变,但并不影响进程的进行,要修改方法名应删除该响应 方法,双击控件,重新获得响应事件。
24.代码经常会出现命名不规范的情况,同时没有写注释的习惯
25.经常会丢掉方法后的括号,如 savedialog1.showdialog()26.一 些 方 法 中 的 细 微 差 别 如 File.Write.AllLine 27.对于异常的处理经常忽略,导致程序的容错处理比较差。
28.当编辑框改变时有两种情况,一种是打开,一种是编辑,当打开 时不应该记录改变标志。解决的办法:做一个打开标志,当打开操作时,赋值为 true,在 文本框的改变函数中判断,如果是打开操作,将打开操作之位 false,同时文本改变标志值为 false
29.此处如何把长整形变量赋值给一个 string 类型? 使用 ToString()30.如何在文本框上添加滚动条。设置 TextBox.ScrollBars
31.textbox 控件大小跟窗体一起变化:刚开始想通过相应窗体大小 改变事件的办法解决,后来通过其他同学得知可以通过更改 textbox 的 Dock 属性(改为 Fill)来解决。
32.当 textbox 空间的文本改变时,进行新建、打开、退出等操作时 应提示用户是否保存更改,通过设置文件更改标志(flag)并相 File.WriteAllText 和 响 textbox 的 textchanged 事件来解决。
33.由于需要提示用户保存更改,刚
开始时代码控制结构写的很复杂,连自己也有点难理解。后来用调用“保存”事件的响应函数(刚 开始一位不能这样)后才解决这一问题。
34.由于刚开始做,命名方面不是很规范,导致了变量、方法和类的 名字不易区分,不能一目了然,间接增加了编写程序所需的时间。解决方法是在编写程序中时刻遵守命名规范。
35.在对文件操作时,文件的路径经常出错,原因是少了转换为绝对 路径的@。
36.对文件注释太少,经常导致前面编过的代码过一段时间就完全看 不懂了,所以,在编程中也要养成随时加注释的习惯,其实这样 做表面上是增加了时间,统筹来看,这样做既方便了自己日后修 改程序,又为阅读我们程序的人提供了便利。
37.文件操作经常出错,程序无法运行,要因此要异常处理,比如说 用 try,catch 对异常处理。
38.在昨天上午的文件操作中,第一个问题就是路径的合法性检测。由于不知道 c#是否存在像 c++ access()一样的方法。最后使用 OpenFileDialog 来限定用户输入。可以使用正则表达式验证 39.再有就是文件操作异常处理。C#的异常处理还是很方便的,昨天 的经验是只要使用 try catch 语句,在 catch 中不作任何处理程 序也不会当掉。
40.MessageBox.Show(“你还没有保存上次的修改,是否保存修改?”, “保存”, MessageBoxButtons.YesNoCancel);选项的判断 问 同 学 得 : 返 回 值 为 DialogResult.No DialogResult.Yes DialogResult.Cancel 41.2 . private
void
Form1_FormClosing(object
sender, FormClosingEventArgs e)中如何取消关闭 问同学得:e.Cancel = true;42.如何实现修改后 “新建”“打开”等提示保存 点打开保存后记 录存储路径
43.新建 2 个成员记录:private string Pathname;private bool savetxt=true;
44.如何在两个窗体间传递值。通过向同学和老师询问,知道传值有 很多种方法,我用的就是调用构造函数,可是老师也说了如果值 比较多的时候,这种方法还是有缺陷的。使用构造函数可以,只是也用对象会好一些。
45.当汉字改成英文时,函数中仍然是汉字,如果直接在 program.cs 中改容易出问题,怎么改? 利用 VS 的代码重构
46.如何改窗体的默认主题样式 可以使用第三方控件完成
47.TextBox 中文本值改变时的响应事件(可以添加事件响应函数)。
48.文 件 打 开 中 将 文 件 写 到 textbox 中 : 定 义 string[] 调 用 File.ReadAllLines 函数将文件内容逐行读入到字符串数组中,然后利用 for 循环将文件内容逐行写出到控件中。
49.填写邮箱和文件路径的时候需要有正确的规格,因为之前接触过 c#不知道该如何完成,后来在经过询问他人和网上资料查询得知 用正则表达式来规范正确
的格式,便避免了这方面因格式而导致 的问题。
50.当输入多行字符串,保存过后进行查看时,当有换行就会出现黑 乎乎的正方形。换行:rn
51.假设某个事件已经做好,可以作为一个模块使用,那么如何在其 他代码中调用此模块
52.记事本的查找、替换模块不知道如何实现 需要使用字符串函数
53.打开 txt 文件的时候中文没有乱码但是保持了之后在打开就有了 乱码 实现:原先是因为保持的时候没有使用 encoding.default 54.保存时文件不能自动保存文件 ?自动保存文件
55.在做简易记事本的途中,创建快捷键的时候遇到一点问题,当在 杂项的 Shortcutkeys 中选择 shift 的时候,它会报属性值无效的 错误,后来无奈之下选择了 Ctrl,居然就正确了
56.还有在 TextBox 的属性 Dock 选择了 fill 的时候,依然没有变成 全部填充,真是百思不得其解!需要将 TextBox 设置为多行文本
57.发现了一种比较简便的方法: 已经做完的某个控件出发的事件,可以作为一个法方来使用。例: private void SaveMenu_Click(object sender, EventArgs e){ //判断文件是否已有路径,若无,选定路径,若有,保存在最新操作的文件路径下 if(path == string.Empty){ if(SolgSave.ShowDialog()== DialogResult.OK){ path = SolgSave.FileName;File.AppendAllText(path, txtEdit.Text, Encoding.Default);} } else { File.AppendAllText(path, txtEdit.Text, Encoding.Default);} } private void ExitMenu_Click(object sender, EventArgs e){ if(txtEdit.Text!= string.Empty){ DialogResult dr = MessageBox.Show(“是否保存当前文本至
”+path,“
记
事
本
”, MessageBoxButtons.OKCancel,MessageBoxIcon.Warning);if(dr == DialogResult.OK){ //使用保存文本方法 this.SaveMenu_Click(sender,e);//SaveMenu 的单击事件 } } //退出程序 Application.Exit();}
58.RichTextBox 与 TextBox 的换行区别问题。在 TextBox 中是以rn 为换行标志,而在 RichTextBox 中是以n 为换行标志。我在 C#程序中使用 RichTextBox 将其文本内容保存为文本文件,即(*.txt)类型的文件时,文本不能正常的换行,且出现一些乱码。这是由于文本文件是以rn 作为换行符,当我改用 TextBox 时,就 没有这样的问题了。
59.文本的选中问题。SelectedText 这一属性可以指向当前控件中选中的文本,60.通常我们会发现,保存操作在文本没有保存的情况下时才会出现 保存文件对话框,当文本已经保存过时,新增的文本会保存到原 文件中,这两个操作需要对文本是否保存过进行判断。我采用了 判断路径的方法对我文本是否保存过进行了判断,当文本路径存 在时,表明文本已经保存过,反之,则没有保存过。
第二篇:《C专家编程》总结
《C专家编程》总结
开始读《C专家编程》之前,有一个很担心的问题:94年出的讲语言的书,在现在(2012)还有多少是适用的。因此,一边读,一边用VS2010做实验。最后发现大部分内容都还在用。读完后,觉得最精彩的部分有二:一是讲解如何理解声明,二是深入地讲解数组名与指针。下文是将看书过程中所做的笔记进行的整理。
p.s: 以下代码均在VS2010测试过
1.使用无符号数时要特别注意(不推荐使用无符号数)当无符号数与有符号数在同一条表达式中出现时,有符号数会被转换为无符号数。e.g:
int feng =-1;unsigned int yan = 5;bool result =(feng < yan)? true : false;//最后的结果会是false 原因是C语言在计算含有不同类型的表达式时,会将类型向上提升。在本例中,int被提升了unsigned int,从而使-1的补码被解析为很大的整数 2.相邻的字符串常量会被自动合并成一个字符串 e.g:
char *str[] = {“feng” “yan”, “zero”};//“feng”和“yan”被合并成一个了:“fengyan” 3.易出错的优先级
.高于* e.g: *p.f 正确的理解:*(p.f)
[]高于* e.g: int *ap[] 正确的理解:int *(ap[])ap是个数组,其元素为int* 函数()高于* e.g: int *fp()正确的理解:int* fp()fp是返回int*的函数
==和!=高于位操作符 e.g: val & mask!= 0 正确的理解:val &(mask!= 0)==和!=高于赋值符 e.g: c = getchar()!= EOF 正确的理解:c =(getchar()!= EOF)
算术运算高于移位运算 e.g: msb<<4 + lsb 正确的理解:msb <<(4 + lsb)逗号运算符优先级最低 e.g: i = 1, 2 正确的理解:(i = 1), 2 4.理解声明,定义,typedef语句的步骤
a.找标识符
b.找被括号括起来的部分
c.找后缀操作符,如果是(),则表示是函数;如果是[],则表示是数组 d.找前缀操作符,如果是*,则表示“指向XX的指针”
e.找const和volatile,如果const,volatile后面紧跟类型(如int,long),那么它作用于类型,其它情况下,作用于它左边紧邻的项 e.g:
int const * zero;//zero是一个指针,指向一个常量整形
char(*zero)[20];//zero是一个指针,指向一个有20个char元素的数组
typedef void(*ptr_to_func)(int);//ptr_to_func是新类型名,这种类型是一个函数指针,指向接收一个int参数,并返回void的函数
char* const *(*zero)();//zero是一个函数指针,该函数无参数,并返回一个指针,返回的指针指向一个常量指针
char*(*zero[10])(int **p);//zero是一个数组,元素是函数指针,其指向的函数授受一个二维指针,并返回一个指向char的指针 void(*signal(int sig, void(*func)(int)))(int);//signal是一个函数,该函数接收一个int,一个函数指针,并返回一个函数指针
5.左值与右值
左值通常表示存储结果的地方(地址),其值在编译时可知 右值通常表示地址的内容,其值通常要到运行时才知道
6.指针与数组名不等同的情况(定义为数组,却声明为指针,或者反过来)前提知识(假设有定义:int array[10], *ptr;):
a.使用数组名下标访问(如:array[1]),会直接将数组名的地址加上偏移值作为变量的地址(即array[1]的地址)
b.使用指针下标访问(如:ptr[1]),会先取指针指向的内容,然后将这个内容加上偏移值作为变量的地址(即ptr[1]的地址)
不等同的原因:
当定义为数组,却声明为指针时,相当于用指针下标访问的方法来解析一个数组名下标,即先取数组第0号元素的内容,然后将这个内容加上偏移值作为变量的地址,从而访问了不该访问的东西。反之亦然。7.指针与数组等同的情况
a.编译器将表达式中的数组名当作指向该数组第0号元素的指针,下标当作指针的偏移量,即array[i]会被当作*(array + i)
b.编译器将函数参数声明中的数组名当作指向该数组第0号元素的指针,即在函数内部得到的是指针,而不是数组名
基于a情况,可知这条谣言是假的(至少在一维数组中一定不成立): 用指针迭代数组比用下标迭代数组更快
基于b情况,可解释为什么在传递数组后,不能用以下方法计算数组长度
int ArrayLength(int arr[]){ return sizeof(arr)/ sizeof(arr[0]);//返回值必定是1,因为此时的arr是一个指针,而不是数组名 } 注意b情况的将数组改写为指针并不是递归定义的,e.g:
实参 char zero[10][10] 被改写为 char(*zero)[10],这里将数组的数组改写为数组的指针
实参 char *zero[10] 被改写为 char **zero,这里将指针数组改写为指针的指针
实参 cahr(*zero)[10] 不改变,因为此时的zero是指针,而不是数组 8.interposition interposition指用户定义的函数取代库中声明完全相同的函数,注意这不是指重载,而是指下面这种:
void zero();//user defined function void zero();//library function 出现interposition时,要特别注意以下情况:
void zero();//user defined function int main(){ zero();//调用用户定义的函数zero,而不是库函数zero
FengYan();//假设这是另一个库函数,并且函数内调用库函数zero,此时由于interposition,变成调用用户定义的zero return 0;} 备注:
出现interposition时,在VS2010会出现warning: inconsistent dll linkage 9.堆栈段作用
a.存储局部变量
b.函数调用时,存储有关的维护信息
c.用作暂时存储区。e.g: 计算一个很长的表达式时,会把部分结果先压到堆栈中
第三篇:C语言编程自我总结
1.编译器选择8级优化时,可能会出现错误。刚写好的程序,建议先用0级优化看能否正常运行,再用更高的优化等级进行优化。
2.a、写中断程序一定要用using语句指定寄存器组。第1、2、3组都可以,不能是0,否则可能会main()函数冲突。从一个中断程序中调用函数必须和中断使用相同的寄存器组(摘自《Keil Cx51 编译器用户手册中文版》P129)。建议把原本中断函数需要调用的函数直接写在中断函数里,无须调用。
b、51单片机的中断有两个优先级。一个中断不会打断另一个相同优先级的中断。这样相同级别中断可以使用同一个组。比如:低优先级的中断函数都用 using 1,高优先级的中断都用 using 2。这样不会冲突。
3.C语言无符号数容易犯的错误。若定义成有符号数char,则不会陷入死循环。
main(){ unsigned char i;for(i = 2;i>=0;i--){ printf(“%d”,i);} }
4.C51忌讳使用绝对定位_at_,因为只要定义变量和变量的作用域,编译器就会把一个固定地址给这个变量,无须人工将其绝对定位,这样可能引发其他问题。
5.bit与sbit的区别:bit定义的位标量的地址是随机的,而sbit定义的位标量的地址是确定的。bit只能访问芯片内部RAM中的可寻址位20H-2FH,而sbit可以访问芯片内部RAM中的可寻址位和特殊功能寄存器中的可寻址位。注意不能直接在程序里用P1^0等位变量,需要经过sbit定义才可以使用。例如:
bit
tem;sbit led=P1^0;tem的地址是随机分配的,而led的地址则固定为0x90.0。sbit变量后面需要跟等号=。6.为了避免由于使用参数宏而带来意外的错误,需要注意以下几点:
6.1 宏的参数必须带括号,例如 #define CIRCLE_SQUARE(R)3.141*(R)*(R)6.2 对所使用的参数宏进行简单地展开检查;
6.3 使用简单表达式、对参数加括号、避免节外生枝的使用方式(例如“++”、“--”一类都属于不必要的附件运算);
6.4 在参数宏定义时,对于运算顺序通过括号进行明确的限定,只要遵循以上几点,就可以避免大多数应用场合的意外错误。
手把手教你写程序
内容:从最简单的程序入手,手把手教你写程序,让同学们拿到一个复杂的程序或者任务,能快速找到切入点,写出程序,再在此基础上优化程序。当拿到一个单片机任务时,不要急于动手写程序,先仔细分析它的以下几个点:
1、它要单片机整体实现什么功能
2、功能细分(模块化),先干什么,再干什么,最后干什么
3、画初步流程图,(把几个模块画出即可)
4、模块之间的分析:一个模块到另一个模块之间,怎么变换,怎么连接(优化流程图)
5、单个模块分析:每个模块要做什么(流程图细化)
6、所有模块结合连接,细化所有流程图
7、分析单个模块每步要用到的方法或者指令
8、总流程图定型
9、纸上写程序,对照流程图分析其可行性,若不可行则返回
10、上机调试,加注释
11、从小到大,一个功能一个功能地调试;
以上十一步,缺一不可(小程序例外)切记:流程图的确定很重要,需反复修改
大忌:拿到任务,不仔细分析就写程序。即使是小程序,我们也要养成良好的编程习惯,不要一味的追求结果。写小程序可能比别人快,若是大程序,一旦出现思维混乱,或者出现程序调试不出结果,那么你花在调试上的时间,要比别人的多。!!!磨刀不误砍柴工!!!程序的优化:属于后期工作,只有调试出来后,才去优化,如果一开始优化和写程序同时进行,一是加重你的思考量,二是出现问题无从下手。无疑增加了写程序的难度。对于一个初学者,写一个程序,本身头脑就处于紧张的状态,思考的问题就很多,如果此时把优化程序也考虑进去,你脑袋的负荷无疑加重,若你头脑精明,你可以把优化的地方,先在纸上记下来,等到调试结果正常,再把你想到的,优化的地方加进去。
7、如果在中断程序中改变了多字节类型的变量,那么中断程序以外的程序中(主程序,子函数)要使用该多字节类型变量的话,读写前要关中断,读写后再开中断。否则会导致偶尔读写错误。(实质为资源冲突)举一反三:
其他的数据类型也可能有这种影响。例如:长整型、浮点型。例如:
unsigned int ms_counter;void T0(){ //定时器程序每100毫秒中断一次,程序略 if(ms_counter<1000)ms_counter++;} void main(void){ //初始化定时器程序每100毫秒中断一次,程序略 unsigned char tt;ms_counter=0;tt=0;//用tt控制只响一次 while(1){ if(ms_counter<400){ if(tt==0){ tt=1;Sound_on();
} } else { Sound_off();} //其他程序 } }
8、sbit变量不能使用extern关键字,使其在不同的文件中被使用,如要在led.c和main.c文件中使用同一个变量led0,有以下下两种办法:
1.在各种文件中重复定义变量,如在led.c中定义sbit led0=P1^0;同样在main.c中定义sbit led0=P1^0;这样,led0就变成了全局变量,可以在两个文件中使用。
2.将sbit led0=P1^0定义到led.h头文件中,均在led.c和main.c中包含led.h这个头文件。
9、在多文件的程序中声明外部变量(extern和)
如果一个程序包含两个文件,在两个文件中都要用到同一个外部变量Num,不能分别在两个文件中各自定义一个外部变量Num,否则在进行程序的连接时会出现“重复定义”的 错误。正确的做法是:在任一个文件中定义外部变量Num,而在另一个文件中用extern对Num作“外部变量声明”。即extern Num;注意若Num为uchar类型,应当写为“extern uchar Num”,否则会当为int,而导致出错。
当使用static声明变量和函数时,需要在定义变量和函数的基础上加上此关键字,而不能单独使用。例如:
static int a;//定义性声明,需要时,直接使用变量a即可 a = 0x01;
static int funA(int a, int b);//声明,且static不起作用 int funA(int a ,int b)//定义,即使funA有static关键字修饰,但由于static不能单独使用,//故funA仍为外部函数。
{ …… } extern对变量进行声明时,如没有初始化,则为引用性声明,不含定义,如需使用此变量,需要进行定义。例如:
extern int a;//引用性声明,不含定义
extern int a = 0x01;//定义性声明,需要时,直接使用变量a即可 int a;//定义
extern对函数进行声明时,如没有函数体,则为引用性声明,不含定义。
extern int funB(int a ,int b);//引用性声明,不含定义,且extern声明可以省略
extern int funC(int a, int b)//定义性声明 { …… }
10、一般的,要尽量减少中断服务程序的内容和长度。因为在主程序中可以还需要随时响应其他的中断或事件。如果一个中断服务程序过程,很可能会影响到主程序对外部信号的检测和响应。通常,在中断程序中只是改变一些变量或标志位,在主程序中再根据变量或标志位的值进行判断,处理相应的事件。
11、在A/D和D/A转换电路中,电源电压和基准电压的稳定性,对转换的精度影响很大。另外,A/D和D/A转换电路中要特别注意地线的正确连接,否则转换结果将是不正确的,干扰影响将很严重。
12、根据C语言标准,左移“<<”和右移“>>”运算要求操作数至少是int,如果不满int,自动转换成int(C语言整型提升)。因此 uchar a=0x01;a<<8;实际运算,并不是8位数左移8位,而是int型左移8位。
13、在中断里调用其他函数,且要进行参数传递时,必须保证被调用函数所使用的寄存器组与中断函数一样,否则会产生不正确的结果。为了保证被调用的函数与中断函数使用的寄存器一致,可对被调用函数使用using,不过此函数只能被中断函数调用。
14、函数不使用using 时,所使用寄存器组保持与此函数被调用前相同,不对RS0和RS1的值进行修改;当使用了using 关键字后,此函数所使用的寄存器组与using所定义的一样。
15、当指定中断程序的工作寄存器组时,保护工作寄存器的工作就可以被省略。使用关键 字using 后跟一个0 到3 的数对应着4 组工作寄存器当指定工作寄存器组的时候默 认的工作寄存器组就不会被推入堆栈这将节省32 个处理周期,因为入栈和出栈都需要2 个处理周期。为中断程序指定工作寄存器组的缺点是所有被中断调用的过程都必须使用 同一个寄存器组否则参数传递会发生错误。
16、如何使用pdata 类型的变量?当要使用到pdata 类型的变量,如下: void main(void){ uchar pdata a;a=0x01;}
则需要进行如下设置,否则pdata 的变量a则会无效。
a、修改STARTUP.A51的内容。默认时,PPAGEENALBE为0,表示不允许pdata类型的变量,须将其值改为1;PPAGE表示pdata类型的变量存储在哪一页,01H表示存放在外部存储器的第1页,地址范围100H至1FFH,此时P2经STARTUP.A51处理后的值为0x01;此项设置需和BL51连接器的设置一致。
b、修改BL51连接器。根据STARTUP.A51中PPAGE所设置的值来填写Pdata的值,如下图。图中Pdata的值可以填写100H至1FFH中任意一个,表示pdata类型的变量从所填
写的值开始存储。例如,当Pdata填写的值为108H时,表示pdata类型的变量从108H开始存储,因此,存储范围变为了108H至1FFH。
另外,存储模式Compact的作用是将没有指定存储类型的变量定义为pdata类型,对uchar pdata a;变量的定义没有影响,但对uchar a;则有影响。
17、XBYTE的用法。XBYTE存在于#include
XBYTE[0x000F]=data; // 此语句表示将data写到外部RAM中的0x000F data=XBYTE[0x000F] // 此语句表示读取外部RAM中0x000F的数据 以下语句与上面的语句等效:
#define EX_RAM XBYTE[0x000F] //将EX_RAM定义为外部RAM的地址0x000F EX_RAM=data;// 此语句表示将data写到外部RAM中的0x000F data=EX_RAM // 此语句表示读取外部RAM中0x000F的数据
18、如何在keil中用汇编实现51中没有的指令
部分MCU与8051兼容,但会增加8051中没有的指令,如华邦的W77E58和N79E352等芯片,具有8051中没有的指令DEC DPTR。如何才Keil中实现此指令呢? 方法1:
在需要执行该指令的地方放置相应的机器码 MAIN:
MOV DPTR,#02H DB 0A5H;由于从数据手册上得知,DEC DPTR的机器码为0A5H,故此处相当于执行了DEC DPTR指令。
AJMP $ END
方法2:
使用宏定义的方法
/*宏定义,表示用DEC_DPTR代替MACRO与ENDM之间的内容*/ DEC_DPTR MACRO
DB 0A5H;此处不能与MACRO同一行 ENDM
MAIN: MOV DPTR,#02H DEC_DPTR;放置机器码0A5H,相当于执行DEC DPTR AJMP $ END
通过将以上两种方法生成的hex文件调入到编程器中,发现代码一样。经测试,同样可以用以上两种方法代替8051中已有的指令。
例如,从数据手册可知,MOV A,#0FH的长度为2字节,机器码的值为74H,0FH。因此,经验证,以下三个程序等效,产生的HEX文件一样 MAIN: MOV A,#55H DB 74H DB 0FH MOV P1,A AJMP $ END
MAIN: MOV A,#55H MOV A,#0FH MOV P1,A AJMP $ END
TEST MACRO DB 74H DB 0FH ENDM MAIN: MOV A,#55H TEST MOV P1,A AJMP $ END
18、汇编中包含头步骤:
例如,T2CON为定时器2的特殊功能寄存器,地址为0C8H,要对此寄存器赋值01H,除了
MOV 0C8H,#01H 和
T2CON EQU 0C8H MOV T2CON,#01H 外,还有用包含头文件的方法 #include
19、指针
C51 提供一个3 字节的通用存储器指针。通用指针的头一个字节表明指针所指的存储 区空间,另外两个字节存储16 位偏移量。对于DATA IDATA 和PDATA 段只需要8 位偏移量。Keil 允许使用者规定指针指向的存储段,这种指针叫具体指针。使用具体指针的好处是节省了存储空间编译器不用为存储器选择和决定正确的存储器操作指令产生代码这样就使代码更加简短但你必须保证指针不指向你所声明的存储区以外的地方否则会产生错误而且很难调试。
由于使用具体指针能够节省不少时间所以我们一般都不使用通用指针。
20、EEPROM存放开关机(复位)次数方法:每次开机(复位)读取EEPROM存放开关机的数据,并加1后重新写入EEPROM。
21、C51中,将printf函数与串口输出结合注意事项:
a、关串口中断;
b、初始化串口,并使TI=1;
c、KEIL里扩展出了b(8位),h(16位),l(32位)来对输入字节宽的设置
在Keil C51中用printf输出一个单字节变量时要使用%bd,若使用%d,则默认为双字节宽度,输出可能会出错。如
unsigned char counter;printf(“Current count: %bdn”, counter);而在标准C语言中都是使用%d: printf(“Current count: %dn”, counter);d、输出数据类型的长度应与定义的数据类型长度一致,如:
uint tem2=97;
printf(“%c,%bdn”,tem2,tem2);第一个输出会出错。
22、我一般不刻意的注意这个,都是从软件自身找问题的。
我写程序时对于软件抗干扰都是在程序状态上考虑意外情况的,例如:
if(a == 1){...} else if(a == 2){....} else{//这个else 一定得加的,即使自己认为不可能出现的情况也要加上
..//经过好多程序走飞的情况发现:大多情况都是缺少这个语句条件的,这 //语句可以写成重新初始化a } 还有程序出现堆栈比较深的运算(例如浮点乘除法后)或中断比较深,我加2个_nop_();
23、STC12C5410AD外部RAM使用方法:
a.在Keil中设置外部RAM的起始地址和大小,如下图
b.将变量定义为xdata即可。
24、中断嵌套
当有外部中断0时,中断标志位IE0由硬件自动置1,进入中断服务程序后,IE0被自动清0。若外部中断0触发信号在执行完中断服务程序后仍没有撤除,就会再次使已经变0的中断标志位IE0置1,再次进入中断服务程序;若在响应中断服务程序期间,再次产生外部中断0触发信号时,此中断不能被识别,因为CPU在响应中断时会自动关闭同一中断。
如果外部中断0比外部中断1的优先级高,当在响应外部中断0期间产生外部中断1时,如果执行完外部中断0后,外部中断1的中断请求标志位IE1仍没有清除的话,将会响应外部中断1的请求;但是如果在响应外部中断0期间,外部中断1的触发信号产生后又撤除的话,IE1也会自动清除,也就是说,执行完外部中断0后,不会去响应外部中断1。
当多个中断源同时向CPU请求中断时,CPU就可以通过中断优先权电路率先响应中断优先权高的中断请求,而把中断优先权低的中断请求暂时搁置起来,等到处理完优先权高的中断请求后再来响应优先权低的中断。
如果某一中断源提出中断请求后,CPU不能立即响应,只要该中断请求标志位不被软件人为清除,中断请求的状态就将一直保持,直到CPU响应中断为止。但是对于串行口中断,即使CPU响应了中断,其中断标志位RI/TI也不会自动清零,而必须在中断服务程序中设置
清除RI/TI的指令后,才会再一次地提出中断请求。
25、在满足应用要求的前提下,选择配较低的单片机,较小的RAM/ROM、较低的ADC分辨率、较低的ADC速率,较少的IO管脚都可以降低单片机的整体功耗。当然了,这个得能满足你产品需求的前提下。
26、对于一个数字系统而言,其功耗大致满足公式:P=CV2f。其中C为系统的负载电容,V为电源电压,f为系统工作频率[2]。功耗与电源电压的平方成正比,因此电源电压对系统的功耗影响最大,其次是工作频率,再次就是负载电容。负载电容对设计人员而言,一般是不可控的,因此设计一个低功耗系统,在不影响系统性能的前提下,尽可能地降低电源的电压和工作频率。对于大多数低功耗单片机来说,工作频率越低,意味着消耗的电流也越小,但是不能认为频率越低,系统整体功耗越小,因为工作频率降低,意味着需要更长的处理时间,其他外围电路消耗的电能就越多。目前有很多单片机都允许有两个或者两个以上的时钟源,低频时钟作为如UART、定时器等外围功能器件的时钟源,高频时钟作为系统的主时钟。在不需要高速运行的场合下,低频时钟也可以作为系统主时钟使用。对于需要在工作状态与空闲状态之间频繁切换的应用,在考虑单片机本身低功耗的同时,应该考虑切换时间和切换电流。考虑到有些场合单片机的工作特点,选择单片机不光要关注工作电流,更应该关注单片机休眠时的静态电流。单片机丰富的低功耗模式和极低的静态电流,在满足特定应用功能的同时,有效降低系统的功耗。尽量关闭MCU内部不用的资源,比如ATmega8内部的模拟比较器,默认是开着的,还有ATmega88内部的大多数资源都可以在不用的时候用软件关闭。
27、定时/ 计数器的实时性
定时/ 计数器启动计数后,当计满回0 溢出向主机请求中断处理,由内部硬件自动进行。但从回0 溢出请求中断到主机响应中断并作出处理存在时间延迟,且这种延时随中断请求时的现场环境的不同而不同,一般需延时3 个机器周期以上,这就给实时处理带来误差差。大多数应用场合可忽略不计,但对某些要求实时性苛刻的场合,可采用动态补偿措施。
所谓动态补偿,即在中断服务程序中对THx、TLx 重新置计数初值时,应将THx、TLx 从回0 溢出又重新从0 开始继续计数的值读出,并补偿到原计数初值中去进行重新设置。可考虑如下补偿方法: CLR EA ;禁止中断
MOV A,T L x ;读TLx 中已计数值 ADD A,#LOW ;LOW 为原低字节计数初值 MOV T L x,A ;设置低字节计数初值 MOV A,#HIGH ;原高字节计数初值送A ADDC A,T H x ;高字节计数初值补偿 MOV T H x,A ;置高字节计数初值 SETB EA ;开中断
28、动态读取运行中的定时器/计数值
在动态读取运行中的定时/ 计数器的计数值时,如果不加注意,就可能出错。这是因为不可能在同一时刻同时读取THx 和TLx 中的计数值。比如,先读TLx 后读THx,因为定时/ 计数器处于运行状态,在读TLx 时尚未产生向THx 进位,而在读THx 前已产生进位,这时读得的THx 就不对了;同样,先读THx 后读TLx 也可能出错。
一种可避免读错的方法是:先读THx,后读TLx,将两次读得的THx 进行比较;若两次读得的值相等,则可确定读的值是正确的,否则重复上述过程,重复读得的值一般不会再错。此法的软件编程如下:
RDTM: MOV A,THx ;读取THx 存A 中 MOV R0,TLx ; 读取TLx 存R0 中
CJNE A,THx,RDTM ;比较两次THx 值,若相等,则读得的值正确,否则重读 MOV R1,A ;将THx 存于R1 中
29、掉电及空闲模式
掉电方式
当PCON中的第二位PD为1时,进入掉电模式,不会执行任何指令,外部时钟停振,片内所有功能部件停止工作,如定时器,串行口,外部中断(部分增强型8051的外部中断可以工作),但片内RAM和SFR的内容保持不变。标准8051从掉电状态退出的惟一方法是硬件复位(部分增强型8051还可以通过外部中断来退出掉电状态),复位后,SFR被重新初始化,但RAM的内容不变。因此,若要使得8051在供电恢复正常后继续执行掉电前的程序,那就必须在掉电前预先把SFR中的内容保护到片内RAM,并在供电恢复正常后为SFR恢复到掉电前的状态。
当PCON的第一位IDEL为1时,进入空闲模式,CPU停止工作,不会执行任何指令,但中断、串行口和定时器可以继续工作。此时,CPU现场(即SP、PC、PSW和ACC等)、片内RAM和SFR中其他寄存器内容均维持不变。退出空闲模式有两种方法:
一、被允许中断的中断源发出中断请求;
二、硬件复位。30、看门狗应用
将喂狗操作(取反指令,如 CPL P1.0)分成两步,放在主程序和中断里执行。如将SETB P1.0放在主程序中,将CLR P1.0放在中断里,这样可以避免主程序跑飞,中断功能正常或者主程序正常,而中断跑飞的情况导致看门狗失效。
31、volatile作用
如果将将变量加上volatile修饰,则编译器保证对此变量的读写操作都不会被优化(肯定执行)。此例中i也应该如此说明。
一般说来,volatile用在如下的几个地方:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义;
另外,以上这几种情况经常还要同时考虑数据的完整性(相互关联的几个标志读了一半被打断了重写),在1中可以通过关中断来实现,2中可以禁止任务调度,3中则只能依靠硬件的良好设计了。32、51准双向口读取:
只有1条指令:
MOV A,P1为读端口寄存器 有两条指令: MOV A,#0FFH MOV A,P1为读引脚
33、采用C语言和汇编语言混合编程是最佳的选择;
34、系统投入运行的最初时刻,应对系统进行自检和初始化。开机自检在初始化前执行,如果自检无误,则对系统进行正常初始化,通常包括硬件初始化和软件初始化两个部分。硬件初始化是指对系统中的各种硬件资源设定明确的初始状态,如对各种可编程芯片进行编程、对各I/O端口设定初始状态和为单片机的硬件资源分配任务等。软件初始化包括,对中断的安排、对堆栈的安排、状态变量的初始化、各种软件标志的初始化、系统时钟的初始化和各
种变量储存单元的初始化等。
35、按键连击速度一般为3-4次/s。
36、复合键利用两个以上按键同时按下时产生的按键效果,但实际情况下,不可能做到真正的“同时按下”,它们的时间差可以长达50ms左右,故当检测到有KEY1按键按下时,还要等待超过50ms,再判断是否还有其他按键按下,再解析按键。
37、数码管显示闪烁效果时,一般闪烁速度为1-4次/s。
38、大电流和高电压设备的启动和关闭都是由软件指令来完成,这些指令执行后,必然引起强烈的干扰,这些干扰不能算随机干扰,它们与软件完全相关;可以在最后才执行这些可能引起强烈干扰的I/O操作,之后立即进入睡眠状态,这样就不会干扰到CPU,等CPU醒来后,干扰的高峰也基本过去。
39、用积分时间为20ms整数倍的双积分型A/D转换方式,能有效地抑制50Hz工频干扰。40、掉电检测电路必须在电压下降到CPU最低工作电压之前就提出中断申请,提前时间为几百us到数ms,以便掉电中断程序进行掉电保护。
41、用定时器作看门狗:当为专职看门狗时,在主程序中周期性清0定时器计数值,以使定时器中断不能产生,当产生定时器中断时,表明看门狗溢出,此时应执行出错处理程序或者进行复位。当为兼职看门狗时,可以在定时器中断程序对计数值进行加1,若计数值大于某值时,表明看门狗溢出,而主程序中应周期性地对计数值进行清0。
42、中断中,冲突发生的条件:
1)某一资源为中断程序和主程序所使用;该资源可以为1个变量,也可以为1个数组或者1个缓冲区。
2)中断程序或主程序对该资源进行了写操作;
3)主程序不能用一条指令对资源完成读或者写操作。(这条不对,参考深入浅出AVR单片机P100的例子)
当这三个条件均满足时,即有可能发生资源冲突,导致程序偶然运行不正常。为了避免发生冲突,可以在主程序中先关中断,再对资源进行读或写,结束后再开中断。
当主程序对资源的访问比较费时,长期关中断可能影响系统的实时性;解决的办法是尽可能缩短关中断的时间,将一边访问,一边处理的工作方式改为集中访问,分批处理。如果是读该资源,则关中断迅速将该资源的内容转移到缓冲区,再开中断,然后再对缓冲区中的信息进行处理;如果是写该资源,则先边运算边写缓冲区,全部写好后再关中断,然后迅速将缓冲区中的内容复制到该资源中,边可以开中断了。
43、A/B*C的运算方案不如(A*C)/B的运算方案精度高。因此,应尽可能将出现偏差的运算往后排,先进行无偏差或偏差小的运算。在定点运算系统中,加减法只要不超限,是没有偏差的,乘法运算的结果会使字长增加,如双字节乘双字节,积为四字节,如果保留全部结果,则没有偏差的。乘法运算的结果会使字长增加,如双字节乘双字节,积为四字节,如果保留全部结果,则没有偏差;如果受字长限制,则要舍去低位字节,从而产生舍入偏差。除法几乎都是有偏差的能够刚好整除的情况是很少的。在浮点运算系统中,加减法由于要进行对阶操作,当两操作数的阶码相差较大时,绝对值大的数有可能将绝对值小的数淹没,使运算的结果仍为绝对值大的数,一点儿也看不出绝对值小的数对结果的影响。相比之下,浮点乘法和浮点除法引起的偏差就比较小,它们能够保持一个比较稳定的运算精度。另外,不管在定点系统中还是在浮点系统中,都要尽可能避免两个数值相近的数过早相减,因为他们都可能是近似值,相减以后,差的有效数值大大减少,必然带来更大的相对误差。经过后续运算之后,结果可能离真实值相差甚远。再有,尽可能不要用绝对值小的数作分母,否则引起的误差也是很大的。
44、要对软件标志位的使用进行说明;对于全局定义的软件标志,它有惟一的定义;对于局
部定义的软件标志,必须注明其有效范围。
45、软件理论已经证明:任何一个程序(除某些短小的子程序外)都存在错误(缺陷),人们可以通过合理的测试来证明它仍然存在错误,却无法证明它已经没有错误。软件测试应该把发现错误作为目的,而不能把“程序调通”作为目的。
1.P0口能驱动8个TTL电路意思: 8051单片机P0口驱动8个TTL电路的意思,TTL电路输入悬浮时相当于输入高电平,因此P0口输出高电平驱动TTL电路几乎不需输出电流。TTL电路输入为低电平时最少要释放1mA电流,因此P0口输出低电平时吸收的电流大于8mA。TTL输出高电平最大1.6mA,输出低电平时吸收的最大电流 16mA。51输出最好用低电平有效,推动PNP管,因为51复位后IO为高电平,如果用高电平有效推N管的话上电复位后会先让外部电路动做。
2.在51里,有一条指令没有写进书本,机器码为A5,执行操作:将下一个字节跳过而不管它是单字节指令还是双字节或三字节指令的一部分.如果反汇编工具不识别A5指令的话,你在A5以后的程序反汇编后就错乱无章.当成个数据,用db a5 即可
3.有些51系统容易复位,一般是电路设计上的问题。很多电路介绍的复位电路都是10u和8.2k,但是在实践过程中我们发现该电路在电源不稳时很容易复位,特别是附近有大干扰时,如继电器动作等。我建议使用22u和1k的复位电路,有许多电路改为该数值后就工作稳定了。当然,最好的办法还是使用专用复位电路或三极管电路,但是那样要增加成本和体积。
4.电路中的滤波电容一定要注意加上,最好每个芯片都再加一个约0.1uf的电容,这样对电路的稳定性很有好处。如果使用了看门狗电路,就有可能是软件问题,程序工作到某些环节时忘记了复位看门狗,结果计数满了就复位了。
5.如果在中断程序中改变了多字节类型的变量,那么中断程序以外的程序中(主程序,子函数),读写前要关中断,读写后再开中断。举一反三:
其他的数据类型也可能有这种影响。例如:长整型、浮点型。
上面的例子是中断里写,主程序中读。相反主程序写,中断里读也可能出错。
6.教你一招,别说我损。。
写一个测试代码,反复向EEPROM中的某几个不用的空位字节写入0x55,直到把它干到寿命终结不能写为止,如果按照10MS写一个字节计算的话,大约只需要20分钟就能干掉它。然后向这个芯片中烧入你的正常代码,当然了,这个代码中应该有一段上电检测EEPROM这几个字节的代码,先尝试向它写入0Xaa,然后再读出来看看是否写入成功,如果没写入则再来两次,如果始终不能写入,这当作检查通过,如果就判断为检查失败,这个时候代码要装着‘不知情’继续执行正确代码,下面的‘破坏’行为应该如何做就不要我讲了把? 破坏行为要装的掩蔽一点,例如调一段代码檫除FLASH的代码,嘿嘿,那对方肯定以为CHIP质量不好容易出现FLASH数据丢失,如果对方使用了AD什么的,可以偶尔人为让它波动大一点,这样对方一般只会怀疑PCB和硬件电路弄的不好,而不会想到是代码动手脚了,长久以后他的用户肯定也会认为他们的产品质量不好,你这个时候就可以向他的客户推广你的产品了。。
上电写EEPROM的次数要在你自己的产品质量承诺的寿命时间之内,否则你自己的产品也
可能增加维修。。
这个方法特别适合在外接单挣钱的工程师,你可能给了对方几个CHIP做测试,对方测试通过偏说不行,就是不给你余款,然后把CHIP拿去CRACK,妄想省掉这个钱,NND,让他们见鬼去把,俺这招已经对付了不少不良分子。。
7.AD键盘
8.防解密高招
高招, 解密
使用一些带内部晶振和内部EEPROM的单片机,如PIC16F913和ATMEGA8等,带内部晶振的单片机有一个寄存器OSCTUNE(或OSCCAL),这个是芯片厂家用来校准内部晶振的,范围从0-31,出厂时同型号的单片机这个寄存器的值是不一样.我们可以利用一些隐藏功能,将OSCTUNE寄存器的值存入内部的EEPROM中,开机时读取EEPROM的值,再与OSCTUNE的值相比较, 若二者相同系统正常工作,若不
相同则不正常工作.解密者将解密的程序烧写进单片机中后,会发大部分的芯片不能正常工作,因为他们不知道这个隐藏的功能.举例说明: 芯片为PIC16F913,这个厂品有4个按键(KEY0、KEY1、KEY2、KEY3),内部我们可以设定这样子一个隐藏的功能,如果KEY0与KEY1同时按下3秒钟以上,会将OSCTUNE寄存器存入单片机的EEPROM中。
开机复位后,读取EEPROM中的数据,与OSCTUNE寄存器相比较,若二者相同系统正常工作,若不相同则不正常工作。以上有三个重点:
1、对于OSCTUNE寄存器不要进行写的操作,只进行读的操作,因为写了一次以后,就一直是你写的这个数据的。
2、刚才介绍的KEY0、KEY1同时按下3秒钟这个功能,可不能让解密者(包括产品的用户)知道,当然大家可以用别的隐藏的功能。
3、单片机中的OSCTUNE寄存器(或OSCCAL)的值,同一种型号的单片机不是每一个都是一样的,有32个数据,也就是说32个芯片中有一个是与解密的单片机是一样的。这样子造成的后果是:解密者解密了你的程序以后,却发现有些单片机可以正常工作,可有些单片机不能正常工作,可以说是大部分的单片机不能正常工作。
不过需要注意一下:要是遇到强干扰把EEPROM中的数据改变了看客户怎么收拾你!
9.PIC16F887A中,要求SLEEP指令后的下一条指令为NOP;不知51和AVR的芯片是否需要注意这一点。经查,AVR的datasheet无此要求,可能是其唤醒时,存在启动延时。
10.中断随时随刻都有可能产生,故编写程序时,需要时刻注意中断的影响。
11.注意以下语句在某些编译器下,结果可能出错:
unsigned char a,b;
unsigned int sum;
a=0x80;
b=0x80;
sum=a+b;
12.编程序最重要是好维护。几个执行时间和程序的可读性比,和开发时间比,我认为是不用考虑的。为了几个机器周期而把程序搞得很复杂,是非常愚蠢的行为。可是很多人多乐此不疲啊。总体系统的算法是要考虑优化的问题的,这点我是赞同的。天天在技术上对着几行程序去优化,而导致开发速度减慢,是非常愚蠢的行为。
13.串口通信协议:引导码/识别码+长度+命令字+data+校验
通过引导码/识别码、长度、校验三步检测 每当出错则丢弃当前数据并还原接收状态和空间…………
14.当准备调试一块板的时候,一定要先认真的做好目视检查,检查在焊接的过程中是否有可见的短路和管脚搭锡等故障,检查是否有元器件型号放置错误,第
一脚放置错误,漏装配等问题,然后用万用表测量各个电源到地的电阻,以检查是否有短路,这个好习惯可以避免贸然上电后损坏单板。调试的过程中要有平和的心态,遇见问题是非常正常的,要做的就是多做比较和分析,逐步的排除可能的原因,要坚信“凡事都是有办法解决的”和“问题出现一定有它的原因”,这样最后一定能调试成功。
做一个硬件设计人员要锻炼出良好的沟通能力,面对压力的调节能力,同一时间处理多个事务的协调和决断能力和良好平和的心态,还有细心和认真等等。
对初学者来说,在新的领域面前一穷二白,要学的东西太多太多,一味机械地试图学习这些“高深的语法”和“看不懂的技巧”,胡乱模仿些别人的“优秀风格”,不能说完全无所得,只能说会学得很累,而且往往事倍功半---久而久之,信心丧失殆尽,半途而废。学习编程,做自己便好。学习眼前能够看懂的内容,多写自己会写的程序。对于已经学到的东西,仔细地体会、思考,发掘其中的发展;学会用一种研究的心态去考察你的每一个疑问;不可以轻易地人云亦云,在网上看了别人无责任的经验,甚至是写错了,丢在一边,懒得改的东西之后,就放弃了自己的探索;要学会坚持自我,遇到别人先进的做法,在自己还没有体会到自己当前这种做法的劣势之前,不要轻易盲从;同样,使用了先进的方法,在没有同时理解这种做法的优点和缺点之前,请不要轻易地感叹“这种方法好,跟帖顶一下!”“大师高手宁有种乎?”坚持“体会到了才是学到了”的态度,最终形成自己的风格,形成自己的技巧。
第四篇:多线程编程知识总结
多线程编程
一、问题的提出
1.1问题的引出
编写一个耗时的单线程程序:
新建一个基于对话框的应用程序SingleThread,在主对话框IDD_SINGLETHREAD_DIALOG添加一个按钮,ID为IDC_SLEEP_SIX_SECOND,标题为“延时6秒”,添加按钮的响应函数,代码如下:
void CSingleThreadDlg::OnSleepSixSecond(){ Sleep(6000);//延时6秒 } 编译并运行应用程序,单击“延时6秒”按钮,你就会发现在这6秒期间程序就象“死机”一样,不在响应其它消息。为了更好地处理这种耗时的操作,我们有必要学习——多线程编程。
1.2多线程概述
进程和线程都是操作系统的概念。进程是应用程序的执行实例,每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成,进程在运行过程中创建的资源随着进程的终止而被销毁,所使用的系统资源在进程终止时被释放或关闭。
线程是进程内部的一个执行单元。系统创建好进程后,实际上就启动执行了该进程的主执行线程,主执行线程以函数地址形式,比如说main或WinMain函数,将程序的启动点提供给Windows系统。主执行线程终止了,进程也就随之终止。
每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。一个进程中的所有线程都在该进程的虚拟地址空间中,共同使用这些虚拟地址空间、全局变量和系统资源,所以线程间的通讯非常方便,多线程技术的应用也较为广泛。
多线程可以实现并行处理,避免了某项任务长时间占用CPU时间。要说明的一点是,对于单处理器(CPU)的,为了运行所有这些线程,操作系统为每个独立线程安排一些CPU时间,操作系统以轮换方式向线程提供时间片,这就给人一种假象,好象这些线程都在同时运行。由此可见,如果两个非常活跃的线程为了抢夺对CPU的控制权,在线程切换时会消耗很多的CPU资源,反而会降低系统的性能。这一点在多线程编程时应该注意。
Win32 SDK函数支持进行多线程的程序设计,并提供了操作系统原理中的各种同步、互斥和临界区等操作。Visual C++中,使用MFC类库也实现了多线程的程序设计,使得多线程编程更加方便。1.3 Win32 API对多线程编程的支持
Win32 提供了一系列的API函数来完成线程的创建、挂起、恢复、终结以及通信等工作。下面将选取其中的一些重要函数进行说明。
1、HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,DWORD dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId);该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄,其中各参数说明如下:
lpThreadAttributes:指向一个 SECURITY_ATTRIBUTES 结构的指针,该结构决定了线程的安全属性,一般置为 NULL;
dwStackSize:指定了线程的堆栈深度,一般都设置为0;
lpStartAddress:表示新线程开始执行时代码所在函数的地址,即线程的起始地址。一般情况为(LPTHREAD_START_ROUTINE)ThreadFunc,ThreadFunc 是线程函数名;
lpParameter:指定了线程执行时传送给线程的32位参数,即线程函数的参数;
dwCreationFlags:控制线程创建的附加标志,可以取两种值。如果该参数为0,线程在被创建后就会立即开始执行;如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用;
lpThreadId:该参数返回所创建线程的ID;
如果创建成功则返回线程的句柄,否则返回NULL。
2、DWORD SuspendThread(HANDLE hThread);该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。
3、DWORD ResumeThread(HANDLE hThread);该函数用于结束线程的挂起状态,执行线程。
4、VOID ExitThread(DWORD dwExitCode);该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。其中参数dwExitCode用来设置线程的退出码。
5、BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
一般情况下,线程运行结束之后,线程函数正常返回,但是应用程序可以调用TerminateThread强行终止某一线程的执行。各参数含义如下: hThread:将被终结的线程的句柄;
dwExitCode:用于指定线程的退出码。
使用TerminateThread()终止某个线程的执行是不安全的,可能会引起系统不稳定;虽然该函数立即终止线程的执行,但并不释放线程所占用的资源。因此,一般不建议使用该函数。
6、BOOL PostThreadMessage(DWORD idThread,UINT Msg,WPARAM wParam,LPARAM lParam);该函数将一条消息放入到指定线程的消息队列中,并且不等到消息被该线程处理时便返回。idThread:将接收消息的线程的ID;
Msg:指定用来发送的消息;
wParam:同消息有关的字参数;
lParam:同消息有关的长参数;
调用该函数时,如果即将接收消息的线程没有创建消息循环,则该函数执行失败。
1.4.Win32 API多线程编程例程
例程1 [MultiThread1] 一个简单的线程。注意事项:
Volatile:关键字:
volatile是要求C++编译器不要自作聪明的把变量缓冲在寄存器里.因为该变量可能会被意外的修改。(多个线程或其他原因)
如从串口读数据的场合,把变量缓冲在寄存器里,下次去读寄存器就没有意义了.因为串口的数据可能随时会改变的.加锁访问用于多个线程的场合.在进入临界区时是肯定要加锁的.volatile也加上,以保证从内存中读取变量的值. 终止线程:
Windows终止线程运行的四种方法 终止线程运行
若要终止线程的运行,可以使用下面的方法:
• 线程函数返回(最好使用这种方法)。
• 通过调用 ExitThread 函数,线程将自行撤消(最好不要使用这种方法)。
• 同一个进程或另一个进程中的线程调用 TerminateThread 函数(应该避免使用这种方法)。
• 包含线程的进程终止运行(应该避免使用这种方法)。
下面将介绍终止线程运行的方法,并且说明线程终止运行时会出现什么情况。
线程函数返回
始终都应该将线程设计成这样的形式,即当想要线程终止运行时,它们就能够返回。这是确保所有线程资源被正确地清除的唯一办法。
如果线程能够返回,就可以确保下列事项的实现:
• 在线程函数中创建的所有 C++ 对象均将通过它们的撤消函数正确地撤消。
• 操作系统将正确地释放线程堆栈使用的内存。
• 系统将线程的退出代码(在线程的内核对象中维护)设置为线程函数的返回值。
• 系统将递减线程内核对象的使用计数。 使用 ExitThread 函数
可以让线程调用 ExitThread 函数,以便强制线程终止运行:
VOID ExitThread(DWORD dwExitCode);
该函数将终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源。但是,C++ 资源(如 C++ 类对象)将不被撤消。由于这个原因,最好从线程函数返回,而不是通过调用 ExitThread 来返回。
当然,可以使用 ExitThread 的 dwExitThread 参数告诉系统将线程的退出代码设置为什么。ExitThread 函数并不返回任何值,因为线程已经终止运行,不能执行更多的代码。 使用 TerminateThread 函数
调用 TerminateThread 函数也能够终止线程的运行:
BOOL TerminateThread(HANDLE hThread, DWORD dwExitCode);
与 ExitThread 不同,ExitThread 总是撤消调用的线程,而 TerminateThread 能够撤消任何线程。hThread 参数用于标识被终止运行的线程的句柄。当线程终止运行时,它的退出代码成为你作为 dwExitCode 参数传递的值。同时,线程的内核对象的使用计数也被递减。
注意 TerminateThread 函数是异步运行的函数,也就是说,它告诉系统你想要线程终止运行,但是,当函数返回时,不能保证线程被撤消。如果需要确切地知道该线程已经终止运行,必须调用 WaitForSingleObject 或者类似的函数,传递线程的句柄。
设计良好的应用程序从来不使用这个函数,因为被终止运行的线程收不到它被撤消的通知。线程不能正确地清除,并且不能防止自己被撤消。
注意 当使用返回或调用 ExitThread 的方法撤消线程时,该线程的内存堆栈也被撤消。但是,如果使用 TerminateThread,那么在拥有线程的进程终止运行之前,系统不撤消该线程的堆栈。Microsoft故意用这种方法来实现 TerminateThread。如果其他仍然正在执行的线程要引用强制撤消的线程堆栈上的值,那么其他的线程就会出现访问违规的问题。如果将已经撤消的线程的堆栈留在内存中,那么其他线程就可以继续很好地运行。
此外,当线程终止运行时,DLL 通常接收通知。如果使用 TerminateThread 强迫线程终止,DLL 就不接收通知,这能阻止适当的清除,在进程终止运行时撤消线程。当线程终止运行时,会发生下列操作:
• 线程拥有的所有用户对象均被释放。在 Windows 中,大多数对象是由包含创建这些对象的线程的进程拥有的。但是一个线程拥有两个用户对象,即窗口和挂钩。当线程终止运行时,系统会自动撤消任何窗口,并且卸载线程创建的或安装的任何挂钩。其他对象只有在拥有线程的进程终止运行时才被撤消。
• 线程的退出代码从 STILL_ACTIVE 改为传递给 ExitThread 或 TerminateThread 的代码。
• 线程内核对象的状态变为已通知。
• 如果线程是进程中最后一个活动线程,系统也将进程视为已经终止运行。
• 线程内核对象的使用计数递减 1。
当一个线程终止运行时,在与它相关联的线程内核对象的所有未结束的引用关闭之前,该内核对象不会自动被释放。
一旦线程不再运行,系统中就没有别的线程能够处理该线程的句柄。然而别的线程可以调用 GetExitcodeThread 来检查由 hThread 标识的线程是否已经终止运行。如果它已经终止运行,则确定它的退出代码:
BOOL GetExitCodeThread(HANDLE hThread, PDOWRD pdwExitCode);退出代码的值在 pdwExitCode 指向的 DWORD 中返回。如果调用 GetExitCodeThread 时线程尚未终止运行,该函数就用 STILL_ACTIVE 标识符(定义为 0x103)填入 DWORD。如果该函数运行成功,便返回 TRUE。
线程的定义:
例程2[MultiThread2] 传送一个一个整型的参数到一个线程中,以及如何等待一个线程完成处理。
DWORD WaitForSingleObject(HANDLE hHandle,DWORD dwMilliseconds);
hHandle:为要监视的对象(一般为同步对象,也可以是线程)的句柄;
dwMilliseconds:为hHandle对象所设置的超时值,单位为毫秒;
当在某一线程中调用该函数时,线程暂时挂起,系统监视hHandle所指向的对象的状态。如果在挂起的dwMilliseconds毫秒内,线程所等待的对象变为有信号状态,则该函数立即返回;如果超时时间已经到达dwMilliseconds毫秒,但hHandle所指向的对象还没有变成有信号状态,函数照样返回。参数dwMilliseconds有两个具有特殊意义的值:0和INFINITE。若为0,则该函数立即返回;若为INFINITE,则线程一直被挂起,直到hHandle所指向的对象变为有信号状态时为止。
例程3[MultiThread3] 传送一个结构体给一个线程函数,可以通过传送一个指向结构体的指针参数来完成。补充一点:如果你在void CMultiThread3Dlg::OnStart()函数中添加/* */语句,编译运行你就会发现进度条不进行刷新,主线程也停止了反应。什么原因呢?这是因为WaitForSingleObject函数等待子线程(ThreadFunc)结束时,导致了线程死锁。因为WaitForSingleObject函数会将主线程挂起(任何消息都得不到处理),而子线程ThreadFunc正在设置进度条,一直在等待主线程将刷新消息处理完毕返回才会检测通知事件。这样两个线程都在互相等待,死锁发生了,编程时应注意避免。
例程4[MultiThread4] 测试在Windows下最多可创建线程的数目。
二、MFC中的多线程开发
2.1 MFC对多线程编程的支持
MFC中有两类线程,分别称之为工作者线程和用户界面线程。二者的主要区别在于工作者线程没有消息循环,而用户界面线程有自己的消息队列和消息循环。
工作者线程没有消息机制,通常用来执行后台计算和维护任务,如冗长的计算过程,打印机的后台打印等。用户界面线程一般用于处理独立于其他线程执行之外的用户输入,响应用户及系统所产生的事件和消息等。但对于Win32的API编程而言,这两种线程是没有区别的,它们都只需线程的启动地址即可启动线程来执行任务。
在MFC中,一般用全局函数AfxBeginThread()来创建并初始化一个线程的运行,该函数有两种重载形式,分别用于创建工作者线程和用户界面线程。两种重载函数原型和参数分别说明如下:
(1)CWinThread* AfxBeginThread(AFX_THREADPROC pfnThreadProc,LPVOID pParam,nPriority=THREAD_PRIORITY_NORMAL,UINT nStackSize=0,DWORD dwCreateFlags=0,LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);
PfnThreadProc:指向工作者线程的执行函数的指针,线程函数原型必须声明如下: UINT ExecutingFunction(LPVOID pParam);请注意,ExecutingFunction()应返回一个UINT类型的值,用以指明该函数结束的原因。一般情况下,返回0表明执行成功。
pParam:传递给线程函数的一个32位参数,执行函数将用某种方式解释该值。它可以是数值,或是指向一个结构的指针,甚至可以被忽略;
nPriority:线程的优先级。如果为0,则线程与其父线程具有相同的优先级;
nStackSize:线程为自己分配堆栈的大小,其单位为字节。如果nStackSize被设为0,则线程的堆栈被设置成与父线程堆栈相同大小; dwCreateFlags:如果为0,则线程在创建后立刻开始执行。如果为CREATE_SUSPEND,则线程在创建后立刻被挂起;
lpSecurityAttrs:线程的安全属性指针,一般为NULL;
(2)CWinThread* AfxBeginThread(CRuntimeClass* pThreadClass,int nPriority=THREAD_PRIORITY_NORMAL,UINT nStackSize=0,DWORD dwCreateFlags=0,LPSECURITY_ATTRIBUTES lpSecurityAttrs=NULL);
pThreadClass 是指向 CWinThread 的一个导出类的运行时类对象的指针,该导出类定义了被创建的用户界面线程的启动、退出等;其它参数的意义同形式1。使用函数的这个原型生成的线程也有消息机制,在以后的例子中我们将发现同主线程的机制几乎一样。下面对CWinThread类的数据成员及常用函数进行简要说明。
m_hThread:当前线程的句柄;
m_nThreadID:当前线程的ID;
m_pMainWnd:指向应用程序主窗口的指针
virtual BOOL CWinThread::InitInstance();重载该函数以控制用户界面线程实例的初始化。初始化成功则返回非0值,否则返回0。用户界面线程经常重载该函数,工作者线程一般不使用InitInstance()。
virtual int CWinThread::ExitInstance();在线程终结前重载该函数进行一些必要的清理工作。该函数返回线程的退出码,0表示执行成功,非0值用来标识各种错误。同InitInstance()成员函数一样,该函数也只适用于用户界面线程。
2.2 MFC多线程编程实例
例程5 MultiThread5 为了与Win32 API对照,使用MFC 类库编程实现例程3 MultiThread3。
例程6 MultiThread6[用户界面线程] 创建用户界面线程的步骤:
1.使用ClassWizard创建类CWinThread的派生类(以CUIThread类为例)class CUIThread : public CWinThread { DECLARE_DYNCREATE(CUIThread)protected: CUIThread();// protected constructor used by dynamic creation
// Attributes public: // Operations public:
// Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CUIThread)public: virtual BOOL InitInstance();virtual int ExitInstance();//}}AFX_VIRTUAL // Implementation protected: virtual ~CUIThread();// Generated message map functions //{{AFX_MSG(CUIThread)
// NOTE-the ClassWizard will add and remove member functions here.//}}AFX_MSG
DECLARE_MESSAGE_MAP()};
2.重载函数InitInstance()和ExitInstance()。BOOL CUIThread::InitInstance(){ CFrameWnd* wnd=new CFrameWnd;wnd->Create(NULL,“UI Thread Window”);wnd->ShowWindow(SW_SHOW);wnd->UpdateWindow();m_pMainWnd=wnd;return TRUE;}
3.创建新的用户界面线程 void CUIThreadDlg::OnButton1(){
}
请注意以下两点:
A、在UIThreadDlg.cpp的开头加入语句: #include “UIThread.h” B、把UIThread.h中类CUIThread()的构造函数的特性由 protected 改为 public。CUIThread* pThread=new CUIThread();pThread->CreateThread();
用户界面线程的执行次序与应用程序主线程相同,首先调用用户界面线程类的InitInstance()函数,如果返回TRUE,继续调用线程的Run()函数,该函数的作用是运行一个标准的消息循环,并且当收到WM_QUIT消息后中断,在消息循环过程中,Run()函数检测到线程空闲时(没有消息),也将调用OnIdle()函数,最后Run()函数返回,MFC调用ExitInstance()函数清理资源。
你可以创建一个没有界面而有消息循环的线程,例如:你可以从CWinThread派生一个新类,在InitInstance函数中完成某项任务并返回FALSE,这表示仅执行InitInstance函数中的任务而不执行消息循环,你可以通过这种方法,完成一个工作者线程的功能。
三、线程间通讯
3.1通讯方式
一般而言,应用程序中的一个次要线程总是为主线程执行特定的任务,这样,主线程和次要线程间必定有一个信息传递的渠道,也就是主线程和次要线程间要进行通信。这种线程间的通信不但是难以避免的,而且在多线程编程中也是复杂和频繁的,下面将进行说明。
3.1.1使用全局变量进行通信
由于属于同一个进程的各个线程共享操作系统分配该进程的资源,故解决线程间通信最简单的一种方法是使用全局变量。对于标准类型的全局变量,建议使用volatile 修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。如果线程间所需传递的信息较复杂,可以定义一个结构,通过传递指向该结构的指针进行传递信息。
3.1.2使用自定义消息
可以在一个线程的执行函数中向另一个线程发送自定义的消息来达到通信的目的。一个线程向另外一个线程发送消息是通过操作系统实现的。利用Windows操作系统的消息驱动机制,当一个线程发出一条消息时,操作系统首先接收到该消息,然后把该消息转发给目标线程,接收消息的线程必须已经建立了消息循环。
3.2例程
例程GlobalObjectTest 该例程演示了如何利用全局变量进行通信
例程7[MultiThread7] 该例程演示了如何使用自定义消息进行线程间通信。首先,主线程向CCalculateThread线程发送消息WM_CALCULATE,CCalculateThread线程收到消息后进行计算,再向主线程发送WM_DISPLAY消息,主线程收到该消息后显示计算结果。步骤:
四、线程的同步
4.1基本概念
虽然多线程能给我们带来好处,但是也有不少问题需要解决。例如,对于像磁盘驱动器这样独占性系统资源,由于线程可以执行进程的任何代码段,且线程的运行是由系统调度自动完成的,具有一定的不确定性,因此就有可能出现两个线程同时对磁盘驱动器进行操作,从而出现操作错误;又例如,对于银行系统的计算机来说,可能使用一个线程来更新其用户数据库,而用另外一个线程来读取数据库以响应储户的需要,极有可能读数据库的线程读取的是未完全更新的数据库,因为可能在读的时候只有一部分数据被更新过。
使隶属于同一进程的各线程协调一致地工作称为线程的同步。MFC提供了多种同步对象,下面只介绍最常用的四种:
临界区(CCriticalSection)
事件(CEvent)
互斥量(CMutex)
信号量(CSemaphore)
通过这些类,可以比较容易地做到线程同步。
4.2使用 CCriticalSection 类
当多个线程访问一个独占性共享资源时,可以使用“临界区”对象。任一时刻只有一个线程可以拥有临界区对象,拥有临界区的线程可以访问被保护起来的资源或代码段,其他希望进入临界区的线程将被挂起等待,直到拥有临界区的线程放弃临界区时为止,这样就保证了不会在同一时刻出现多个线程访问共享资源。
CCriticalSection类的用法非常简单,步骤如下:
1.定义CCriticalSection类的一个全局对象(以使各个线程均能访问),如CCriticalSection critical_section;
2.在访问需要保护的资源或代码之前,调用CCriticalSection类的成员Lock()获得临界区对象: critical_section.Lock();3.在线程中调用该函数来使线程获得它所请求的临界区。如果此时没有其它线程占有临界区对象,则调用Lock()的线程获得临界区;否则,线程将被挂起,并放入到一个系统队列中等待,直到当前拥有临界区的线程释放了临界区时为止。
4.访问临界区完毕后,使用CCriticalSection的成员函数Unlock()来释放临界区:critical_section.Unlock();通俗讲,就是线程A执行到critical_section.Lock();语句时,如果其它线程(B)正在执行critical_section.Lock();语句后且critical_section.Unlock();语句前的语句时,线程A就会等待,直到线程B执行完critical_section.Unlock();语句,线程A才会继续执行。
例程8 MultiThread8 4.3使用 CEvent 类
CEvent 类提供了对事件的支持。事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象。例如在某些网络应用程序中,一个线程(记为A)负责监听通讯端口,另外一个线程(记为B)负责更新用户数据。通过使用CEvent 类,线程A可以通知线程B何时更新用户数据。每一个CEvent 对象可以有两种状态:有信号状态和无信号状态。线程监视位于其中的CEvent 类对象的状态,并在相应的时候采取相应的操作。
在MFC中,CEvent 类对象有两种类型:人工事件和自动事件。一个自动CEvent 对象在被至少一个线程释放后会自动返回到无信号状态;而人工事件对象获得信号后,释放可利用线程,但直到调用成员函数ReSetEvent()才将其设置为无信号状态。在创建CEvent 类的对象时,默认创建的是自动事件。CEvent 类的各成员函数的原型和参数说明如下:
1、CEvent(BOOL bInitiallyOwn=FALSE,BOOL bManualReset=FALSE,LPCTSTR lpszName=NULL,LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);bInitiallyOwn:指定事件对象初始化状态,TRUE为有信号,FALSE为无信号;
bManualReset:指定要创建的事件是属于人工事件还是自动事件。TRUE为人工事件,FALSE为自动事件;
后两个参数一般设为NULL,在此不作过多说明。
2、BOOL CEvent::SetEvent();
将 CEvent 类对象的状态设置为有信号状态。如果事件是人工事件,则 CEvent 类对象保持为有信号状态,直到调用成员函数ResetEvent()将 其重新设为无信号状态时为止。如果CEvent 类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,CEvent 类对象由系统自动重置为无信号状态。
如果该函数执行成功,则返回非零值,否则返回零。
3、BOOL CEvent::ResetEvent();
该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置,故自动事件不需要调用该函数。如果该函数执行成功,返回非零值,否则返回零。一般通过调用WaitForSingleObject函数来监视事件状态。前面已经介绍了该函数。由于语言描述的原因,CEvent 类的理解确实有些难度,只要通过下面例程,多看几遍就可理解。例程9 MultiThread9 仔细分析这两个线程函数, 就会正确理解CEvent 类。线程WriteD执行到 WaitForSingleObject(eventWriteD.m_hObject,INFINITE);处等待,直到事件eventWriteD为有信号该线程才往下执行,因为eventWriteD对象是自动事件,则当WaitForSingleObject()返回时,系统自动把eventWriteD对象重置为无信号状态。
4.4使用CMutex 类
互斥对象与临界区对象很像.互斥对象与临界区对象的不同在于:互斥对象可以在进程间使用,而临界区对象只能在同一进程的各线程间使用。当然,互斥对象也可以用于同一进程的各个线程间,但是在这种情况下,使用临界区会更节省系统资源,更有效率。
4.5使用CSemaphore 类
当需要一个计数器来限制可以使用某个线程的数目时,可以使用“信号量”对象。CSemaphore 类的对象保存了对当前访问某一指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程的数目。如果这个计数达到了零,则所有对这个CSemaphore 类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零时为止。一个线程被释放已访问了被保护的资源时,计数值减1;一个线程完成了对被控共享资源的访问时,计数值增1。这个被CSemaphore 类对象所控制的资源可以同时接受访问的最大线程数在该对象的构建函数中指定。
CSemaphore 类的构造函数原型及参数说明如下:
CSemaphore(LONG lInitialCount=1,LONG lMaxCount=1,LPCTSTR pstrName=NULL,LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);lInitialCount:信号量对象的初始计数值,即可访问线程数目的初始值;
lMaxCount:信号量对象计数值的最大值,该参数决定了同一时刻可访问由信号量保护的资源的线程最大数目;
后两个参数在同一进程中使用一般为NULL,不作过多讨论;
在用CSemaphore 类的构造函数创建信号量对象时要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时,则说明当前占用资源的线程数已经达到了所允许的最大数目,不能再允许其它线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源数加1。例程10 MultiThread10 为了文件中能够正确使用同步类,在文件开头添加: #include “afxmt.h” 定义信号量对象和一个字符数组,为了能够在不同线程间使用,定义为全局变量:CSemaphore semaphoreWrite(2,2);//资源最多访问线程2个,当前可访问线程数2个
在信号量对象有信号的状态下,线程执行到WaitForSingleObject语句处继续执行,同时可用线程数减1;若线程执行到WaitForSingleObject语句时信号量对象无信号,线程就在这里等待,直到信号量对象有信号线程才往下执行。
第五篇:C++编程知识总结
1.数组
1.1数组定义时的注意点
1在C++中不提供可变化大小的数组,○即数组定义中的常量表达式不能包含变量。(来源:C++书6.1.1)
int n;cin>>n;float t[n];上例在定义数组t时,变量n没有确定的值,即在程序执行之前,无法知道数组t的元素个数,所以这种声明不被允许。但是可以用new动态分配,如: int n;cin>>n;float *t;t=new float[n];
2在定义数组时,可以不直接指定数组的大小,由C++编译器根据初值表中元素的个数来自○动确定数组元素的个数。例如: int z[]={0,1,2,3,4,5,6,7,8} 3C++语言规定只能对数组中的元素进行赋值或引用,不能把整个数组作为一个整体进行赋○值或引用。(2.3是一个实例)(来源:C++书4同类型的数组之间不能相互赋值 ○如int a[5],b[5];a=b;//错误
strcpy(b,a);//正确
6.1.1)
1.2数组和指针的关系(来源:C++书8.2节8.2.1)
char s[5];在C++中说明了一个数组后,数组名可以作为一个指针来使用,因此s可作为一个指针使用(但它不同于指针,不能赋值运算、算术运算等)。
2.字符数组
2.1输入字符数据 char c;cin>>c;// cin不能将输入的空格赋给字符型变量。
cin.get();//可获得键盘上输入的每一个字符,包括空格和回车键。
2.2字符数组的输入/输出(来源:C++书6.2.4)2.2.1逐个字符输入 char c[10];for(int i=0;i<10;i++)cin>>c[i];2.2.2字符串输入 方法1 char c[10];cin>>c;//即在输入输出时只给数组名
此法在输入字符串时,遇到空格和回车就认为一个字符结束。方法2 cin.getline(字符数组名,允许输入的最大字符个数)此法可把输入的一行作为一个字符串送到字符数组中。
2.3字符数组和字符指针的初始化 2.3.1字符数组初始化 char tx[5]=“";2.3.2字符指针初始化 char *ptx=new char[5];ptx[0]=' ';2.4字符串赋值
方法1 char tx[4]=”abcd“;方法2 char tx[4];//tx=”abcd“;//错误,tx是数组名,不分配内存空间,不可以进行赋值操作;但是数组名可当指针使用(C++书8.2.1)。strcpy(tx,”abcd“);以上两种方法是数组
方法3是指向数组的指针方法 方法3 char *tx;tx=new char[4];tx=”abcd“ 方法4 char *tx=”abcde“;//这相当于根据数组元素的个数,确定数组的大小。tx指针指向这个数组。
//下面实例告诉我们,不仅字符数组与字符指针有区别,用new给定内存空间大小的字符指针与没给定内存空间大小的字符指针也是有区别的 voidmain(){
/*char s[6]=”“;
strcpy(s,”abcd“);cout<
/*char *s=new char[5];strcpy(s,”abcd“);
} cout<
char *s=”“;//分配了内存空间,但不知道大小 strcpy(s,”abcd“);//错误,使用时要注意!!cout<
3.指针
3.1指针可执行的运算
指针可以进行赋值运算、算术运算、关系运算。
1可以将与指针变量同类型的任一变量的地址赋给指针○2在C++中,可以(1)赋值运算:○
3同类型的指针变量之间可以将0赋给任一指针变量,其含义是初始化指针变量,使其为空○相互赋值,不同类型的经强制转换后也可以,通常也没意义。(2)算术运算:指针变量执行“++”或“——”,其含义是使指针变量指向下一个或上一个元素
3.2指针和数组(同1.2)3.3指向数组的指针变量
char(*p)[10];(*p)指明p是一个指针变量,再与[10]结合,表示该指针变量所指向的数据是一个一维数组,该数组由10个元素组成。3.4指针数组
由若干个同类型的指针所组成的数组称为指针数组,数组的每个元素都是一个指针变量。定义指针数组的格式:如char *p[10];由于“[]”的优先级比“*”高,p[10]构成一个数组,再与“*”结合,指明是一个指针数组。3.5指向指针的指针变量 char **pp;3.6 new运算符
注意点:
用new运算符分配的内存空间的指针值必须保存起来,以便于delete运算符归还已动态分配的内存,否则会出现不可预测的错误。3.6.1指向数组的指针 char* m_p1;m_p1=new char[10];//指针m_p1指向含有10个元素的数组空间。for(i=0;i<10;i++)m_p2[i]表示这10个数组元素。
voidmain(){ char *p1;//char b;p1=newchar[5];
//p1=”abcde“;//直接给p1赋字符串,下面for循环中是给每个元素赋值。for(inti=0;i<5;i++){ //b='c';
} p1[i]='a';//p1[i]是数组元素,不是指针
cout<
3.6.2指向指针的指针变量 char**m_p2;//指向指针的指针
m_p2=new char*[10];//指针m_p2指向含有10个元素的指针数组。for(i=0;i<10;i++)m_p2[i]表示这10个指针。
void main(){ char *p1;char **pp;p1=new char[5];pp=new char*[5];for(int i=0;i<5;i++){
p1[i]='a';=&p1[i];//pp[i]是指针
cout<
cout<
3.7 delete运算符
delete释放的不是指针本身,而是指针所指的对象。
4.容器类std::string #include
int main(int argc, char * argv[]){
std::string str=”abc“;
std::string::iterator cit=str.begin();
for(;cit!=NULL;++cit)//NULL比较,我估计肯定不对,虽然你说是可以通过编译
{
std::cout<<*cit< } return 0;} 4.容器类Vector 4.1迭代器和指针的区别 有时需要使用指向vector的一个指针,我们可以这样来做。 vector 表达式v[0]生产一个指向vector中第一个元素的引用,所以,&v[0]是指向那个首元素的指针。vector中的元素被C++标准限定为存储在连续内存中,就像是一个数组。 如果你在一个不好的环境中,他们会告诉你说可以用v.begin()代替&v[0],因为(这些讨厌的家伙将会告诉你)begin返回指向vector内部的迭代器,而对于vector,其迭代器实际上是指针。那经常是正确的,但正如条款50所说,并不总是如此,你不该依赖于此。 begin的返回类型是iterator,而不是一个指针,当你需要一个指向vector内部数据的指针时绝不该使用begin。如果你基于某些原因决定键入v.begin(),就应该键入&*v.begin(),因为这将会产生和&v[0]相同的指针。 这表明迭代器的内容*v.begin()才是vector中第一个元素。 4.2 容器vector的函数clear() 清空vector里所有的元素。因此,如AMProcessList析构函数里一个个删除vector中所有的元素是多此一举。 5.关键字operator 它是说明符,用于重载运算符。 6.函数可以将一个处理的结果值通过函数的Return语句返回,也可以通过参数将处理的多个结果带给调用者。 C++语言在处理函数调用时,参数是自右向左依次入栈的 7.类的前置声明 8.Const char* Bjarne在他的The C++ Programming Language里面给出过一个助记的方法: 把一个声明从右向左读。 char * constcp;(* 读成 pointer to)cp is a const pointer to char--->cp是一个指向字符char的固定指针 const char * ptr;ptr is a pointer to const char;--->ptr是一个指向固定字符char的指针 char const * p;--->无此形式 也就是说,cp和ptr都是指针,cp的值是不可改变的cp指向的内容是可变的;而ptr的值是可以改变的,ptr指向的内容是不可变的 9.String转化为Constchar*,Const char*转化为char* 1.在string里面string.c_str()函数把string转换为了const char*.○代码如下: stringa=”abcd“;const char*p=a.c_str();2const_cast 10.初始化 char *p;char *s=”“;char *t=NULL;p没分配内存,s分配了内存,t为空,11.变量的初始化 1指针需要初始化; ○2基本数据类型声明的变量需要初始化;如double m_dvalue;m_dvalue=0; ○3类声明的对象不需要初始化。○ 12.派生类中的一般成员函数和虚函数 classA { public: voidsolid(){cout<<”基类实函数“<<'n';} virtualvoidvir(){cout<<”基类虚函数“<<'n';} };classAA:publicA { public: voidsolid(){cout<<”派生类实函数“<<'n';} virtualvoidvir(){cout<<”派生类虚函数“<<'n';} }; voidmain(){ A* a=newA;AA* aa=newAA;a=aa; a->vir();//vir()是虚函数。它是运行时的多态性,即在程序运行时,根据具体的执行情况来动态的确定。因此输出”派生类虚函数“,而不是“基类虚函数” a->solid();//solid()是一般成员函数。它是编译时的多态性,即程序编译时就觉得会调用哪个函数。因为a是A类对象的指针,即使派生类AA对象的指针aa赋给a,在编译是已经觉得调用基类A的solid函数,因此输出“基类虚函数”而不是“派生类虚函数” aa->vir();aa->solid(); aa->A::solid();aa->A::vir();} Button newBtn = new Button();newBtn.Location = new System.Drawing.Point(128, 110);newBtn.Name = ”newBtn“;newBtn.Size = new System.Drawing.Size(75, 23);newBtn.Text = ”button2";newBtn.UseVisualStyleBackColor = true;this.Controls.Add(newBtn); ///清除新生成的Btn this.Controls.Remove(newBtn); CButton* CTextEditorView::NewMyButton(int nID,CRect rect,int nStyle){ CString m_Caption;m_Caption.LoadString(nID);//取按钮标题 CButton *p_Button = new CButton();ASSERT_VALID(p_Button);p_Button->Create(m_Caption, WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | nStyle, rect, this, nID);//创建按钮 return p_Button;}