程序员面试宝典(第4版)
欧立奇; 刘洋; 段韬
2.1 简历注意事项
2016-05-31
简历要尽量短。我们做过一个计算,一份中文简历压缩在2页左右就可以把所有的内容突出了。清楚、完整地把你的经历和取得的成绩表现出来。不要压缩版面,不要把字体缩小到别人难以阅读的程度。当你写履历时,试着问自己:“这些陈述会让我得到面试的机会吗?”然后,仅仅保留那些回答“是”的信息。
2016-05-31
求职简历一定要按照实际情况填写,任何虚假的内容都不要写。即使有的人靠含有水分的简历得到面试的机会,面试时也会露出马脚的。千万不要为了得到一次面试机会就编写虚假简历。被招聘方发现后,你几乎就再也没有机会进入这家公司了。而且对于应届生来说,出现这种情况后,还有可能影响到同校的其他同学。
2016-05-31
求职简历上一定要注明求职的职位。每份简历都要根据你所申请的职位来设计,突出你在这方面的优点,不能把自己说成是一个全才,任何职位都适合。不要只准备一份简历,要根据工作性质有侧重地表现自己。如果你认为一家单位有两个职位都适合你,可以向该单位同时投两份简历。
2016-05-31
作为求职的开始,我们要编写一份或者几份有针对性的简历,也就是按照对方的要求突出自己相关的经历。只要你的优势与招聘方的需要吻合,并且比其他应聘者突出的话,你就胜利了。
2016-05-31
简历上写上对工资的要求要冒很大的风险,最好不写。如果薪水要求太高,会让企业感觉雇不起你;如果要求太低,会让企业感觉你无足轻重。对于刚出校门的大学生来说,第一份工作的薪水不重要,不要在这方面费太多脑筋。
9.不要写太多个人情况不要把个人资料写得非常详细,姓名、电话是必需的,出生年月可有可无。如果应聘国家机关、事业单位,应该写政治面貌。如果到外企求职,这一项也可省去,其他都可不写。
10.不要用怪字怪体我见过一份简历,用中空字体,还有斜体字。这些都是很忌讳的。试想一个HR挑了一天的简历,很累了,还要歪着头看你的简历。你想你的胜算能有多大?其实用简单的宋体5号字就很好了,不用标新立异。
11.仔细措辞体现个人业绩有时候,简历上的几个字就足以“激怒”HR,使其停止阅读你的简历。招聘经理与招聘人员心中都有一张讨厌措辞表。他们厌烦那些在简历中尽可能多地堆砌动词、形容词或副词的人。“非常出色”、“做出很大的贡献”这些措词都是不合适的。最好能够改成“我完成了多少销售业绩,联系了多少家公司”。如果数字过于敏感不适宜表达,可以用百分比或企业的表彰来表达,还可以写上获得的证书。
2.2 简历模板
2016-06-01
下面是一份简历模板。
求职简历个人介绍:
姓名:柯小平性别:男
出生日期:1985/07/03学校及专业:西北大学计算机系软件与理论专业学历:硕士
移动电话:13096964884电子邮件:
[email protected]
IT &&英语技能:1.软件结构设计,需求分析能力
2.精通C/C++、C#,精通SQL3.熟悉Windows开发平台,精通.NET体系
4.熟悉Delphi开发工具,熟悉UML统一建模语言5.深入理解面向对象的思想,并能熟练地应用于具体的软件设计开发工作中
6.英语水平:国家六级项目经验(近期):
2010/1—2010/7与实验室人员合作,在基于ASP.NET+SQL 2005的平台下开发西北大学网络选课系统程序。负责帮助客户现场鉴定并解决有关网络技术及安全问题,保证客户网络畅通;整体安全服务解决方案项目的设计;网络培训课程的开发、设计;学校管理信息技术客户培训。
2009/9—2010/1与实验室人员合作,在基于Delphi+SQL 2005的平台下开发西北大学人事管理系统程序。该项目是西北大学基金项目,目的为完成西北大学教职员工信息的统一规范化管理。系统分为教师科、劳资科、人事科、人才交流中心等几部分,实现了各个部门之间的信息统一化协调管理。系统由Delphi、Power Designer、MS SQL开发完成。
工作经验:2009/9—2009/12
北大青鸟ACCP培训师2007/7—至今
就读于西北大学计算机系,在校期间,与实验室小组合作完成网上选课系统(ASP.NET+SQL 2000)和人事管理系统(Delphi+SQL 2000)的研发工作,以及教务管理系统(PowerBuilder 8+SQL 2000)的测试工作。应聘西北工业大学计算机三级网络任课老师,教授国家计算机三级相关内容。
奖学金:中国石油奖学金优秀学生一等奖。
其他特长:文学和美术功底较好,擅长网页制作,Photoshop和Dreamweaver水平较好。擅长表述,能够胜任教学工作。
个人评价:我无法掩饰对这份工作的渴望——一份有科研挑战的职位。
我一向认为理想分为两类,一类是实现自己的理想,另一类理想则通过自身得到实现。理想之于我则两者兼而有之,并稍稍倾向后者。作为老师,我喜欢传道、授业、解惑,形成一套自己的理论并潜移默化我的学生;同样,作为科研工作者,我也被C++的华贵多彩而吸引,那是真正的逻辑之美。此外,很多时候为了项目的完满,必须具备一种不破楼兰终不还的决心和不积跬步无以至千里的恒心。最后,我谦和、谨慎,富于团队精神。希望您能给我这样一个机会展示自己。谢谢。
柯小平2010.7.7
3.2 电话面试
2016-06-01
由于模式的限制,电话面试时间不会很长。在这个环节中,一定要表现得自信、礼貌、认真、严肃,这样会在声音上给对方一个良好的印象。如果声音慵懒、语气生硬,除非是技术题目及英文方面表现得足够好,否则很难予以平衡。
3.3 面试
2016-06-01
应聘初级职位,会针对你的编程能力和以往的项目经验进行重点的考查。如果面试官针对你做的某个项目反复提问,那么你就需要注意了,要么面试官在这个方面特别精通,要么就是未来的职位需要用到这方面的技术。我们应该抱着一种诚恳的态度来回答,对熟悉的技术点可以详细阐述,对于不熟悉的部分可以诚实地告诉面试官,千万不要不懂装懂。不过,我们认为可以引导与面试官的谈话,把他尽量引导到我们所擅长的领域。在SPSS公司面试时,在回答完面试官单链表逆置和复制构造函数问题之后,我把话题引入了我所擅长的设计模式方面,这是一种谈话的艺术。
应聘中级职位,不但会考查代码编写,而且会对软件架构或相关行业知识方面进行考查。代码编写方面,主要以考查某种编程技巧来判断你对代码的驾驭能力。比如某国际知名软件公司经常会让面试者编写malloc或atoi函数。越是简单的函数越能考验应聘者的编码能力。你不但要实现功能,而且还要对可能出现的错误编写防御性代码,这些经验都需要在实际编程过程中积累。应聘高级职位,应聘者肯定对技术或某个行业有相当程度的了解,这时主要是看你与职位的契合程度、企业文化的配比性(即将人力资源及成本配比作为服务体系的重要组成部分,将公司企业文化中核心理念及价值观作为客户服务的重要媒介)及整体感觉。应聘管理职位的话,考查的更多是管理技巧、沟通技巧和性格因素。架构师一般会考查行业背景与软件架构方面的知识,比如UML或建模工具的使用等;技术专家的职位则会针对相关技术进行深度考查,而不会再考查一般性的编码能力。
2016-06-01
建议准备一个日程本,记录每一次宣讲会、笔试和面试的时间,这样一旦公司打电话来预约面试,可以马上查找日程本上的空闲时间,不至于发生时间上的冲突。每投一份简历,记录下公司的职位和要求,如果一段时间以后(1个月或更长)有面试机会,可以翻出来看看,有所准备。根据不同的公司,准备不同的简历,千万不要一概而论,不同的公司care(在意)的东西不一样。每参加完一次笔试或面试,把题目回忆一下,核对一下答案,不会做的题目更要好好弄懂。同学们之间信息共享,总有人有你没有的信息。如果投了很多份简历,一点儿回音都没有,你得好好看看简历是否有问题,增加一些吸引HR眼球的东西。
3.4 签约
2016-06-01
这是签约前必然要谈的部分。这里面的内容非常多。待遇主要包括工资、奖金、补贴、福利、股票(期权)、保险、公积金。以下具体介绍各部分应注意的细节。● 工资:一定要问清楚是税前还是税后,这点不用多说。另外,还要问清楚,发多少个月。例如,税前工资7000,发13个月,则年收入7000×13=91000。很多单位有年底双薪,还有一些单位会发14~16个月不等。
● 奖金:很多单位的奖金占收入的很大一部分。例如,联想、百度、中航信都有季度奖、年终奖,另外还有项目奖,华为也有项目奖、年终奖,瞬联就没有奖金。不同的单位情况不同,奖金的数额也不一样,通常几千至数万不等,所以关于这一点,一定要问清楚,而且要问确定能拿到的奖金,取最低数。● 补贴:有些单位会有各种补贴,如通信补贴、住房补贴、伙食补贴等。例如,华为有800~1000的餐补。有些单位的补贴加在一起非常可观,也要问清楚。
● 福利:对于一些国企和事业单位来说,往往会有一些福利。例如,过节费、防暑降温费、取暖费、购物券、电影票、生活用品,等等。● 股票:对于很多公司来说,股票是他们提供的非常有诱惑力的福利。一般来说,已经上市的公司提供股票的可能性不大,反倒是一些即将上市的公司提供股票的可能性很大。对此,一定要看准机遇,不要轻易错过。
● 保险、公积金:即常说的“五险一金”。五险指的是养老保险,医疗保险,失业保险,人身意外伤害保险,生育保险,一金指的是住房公积金。这些是国家规定的,企业不得以任何理由拒绝为你缴纳,而且个人和企业出的比例是有规定的(但是也有一些企业不缴纳公积金的例子)。这里要注意的是缴费基数。很多单位在这上面做文章。例如,你的工资是5000,他们以2000为缴费基数,也就是说,用它去乘固定的比例给你缴纳五险一金,对此,一定要注意问清楚缴费基数。有些单位公积金比例上得非常高,所以你工资扣得也很多,那意味着公司交的钱更多,而一旦买房时,这些钱都是你自己的,所以,这部分收入不能忽视。此外,有些单位还会向你提供补充医疗保险、补充养老保险、补充意外保险、住房无息贷款或经济适用房等,也要问清楚。把这些收入加起来,得到年收入。然后再考虑工作地的工资水平和消费水平。例如,年薪8万在西安,无疑是比年薪10万在上海要高多了。● 年假:即每年除了法定节假日之外可以休息的天数,这个自然是高校最多(有寒、暑假),研究所、外企可能会少一些,比如PPFORM公司一年是15~20天年假,30天探亲假(不可以同时休);Nortel是第一年12天年假,然后每年递增,直到21天为止;华为没有年假,要靠每月最后一天周六加班来攒假期作为自己的年假。不上班的时候觉得假期无足轻重,上了班就会觉得假期弥足珍贵。
5.3 编程风格
2016-06-02
C中printf计算参数时是从右到左压栈的。
5.4 类型转换
2016-06-02
C++定义了一组内置类型对象之间的标准转换,在必要时它们被编译器隐式地应用到对象上。
隐式类型转换发生在下列这些典型情况下。1.在混合类型的算术表达式中
在这种情况下最宽的数据类型成为目标转换类型,这也被称为算术转换(Arithmetic Conversion),例如:2.用一种类型的表达式赋值给另一种类型的对象
在这种情况下目标转换类型是被赋值对象的类型。例如在下面第一个赋值中文字常量0的类型是int。它被转换成int*型的指针表示空地址。在第二个赋值中double型的值被截取成int型的值。3.把一个表达式传递给一个函数,调用表达式的类型与形式参数的类型不相同在这种情况下目标转换类型是形式参数的类型。例如:
4.从一个函数返回一个表达式的类型与返回类型不相同在这种情况下返回的表达式类型自动转换成函数类型。例如:
算术转换保证了二元操作符,如加法或乘法的两个操作数被提升为共同的类型,然后再用它表示结果的类型。两个通用的指导原则如下:(1)为防止精度损失,如果必要的话,类型总是被提升为较宽的类型。
(2)所有含有小于整型的有序类型的算术表达式在计算之前其类型都会被转换成整型。规则的定义如上面所述,这些规则定义了一个类型转换层次结构。我们从最宽的类型long double开始。
如果一个操作数的类型是long double,那么另一个操作数无论是什么类型都将被转换成long doubless。例如在下面的表达式中,字符常量小写字母a将被提升为long double,它的ASC码值为97,然后再被加到long double型的文字常量上:如果两个操作数都不是long double型,那么若其中一个操作数的类型是double型,则另一个就将被转换成double型。例如:
类似地,如果两个操作数都不是double型而其中一个操作数是float型,则另一个被转换成float型。例如:否则如果两个操作数都不是3种浮点类型之一,它们一定是某种整值类型。在确定共同的目标提升类型之前,编译器将在所有小于int的整值类型上施加一个被称为整值提升(integral promotion)的过程。在进行整值提升时类型char、signed char、unsigned char和short int都被提升为类型int。如果机器上的类型空间足够表示所有unsigned short型的值,这通常发生在short用半个字而int用一个字表示的情况下,则unsigned short int也被转换成int,否则它会被提升为unsigned int。wchar_t和枚举类型被提升为能够表示其底层类型(underlying type)所有值的最小整数类型。例如已知如下枚举类型:
相关联的值是0和1。这两个值可以但不是必须存放在char类型的表示中。当这些值实际上被作为char类型来存储时,char代表了枚举的底层类型,然后status的整值提升将它的底层类型转换为int。在下列表达式中:
在确定两个操作数被提升的公共类型之前,cval found和mval都被提升为int类型。一旦整值提升执行完毕,类型比较就又一次开始。如果一个操作数是unsigned long型,则第二个也被转换成unsigned long型。在上面的例子中所有被加到ulong上的3个对象都被提升为unsigned long型。如果两个操作数的类型都不是unsigned long而其中一个操作数是long型,则另一个也被转换成long型。例如:
long类型的一般转换有一个例外。如果一个操作数是long型而另一个是unsigned int型,那么只有机器上的long型的长度足以存放unsigned int的所有值时(一般来说,在32位操作系统中long型和int型都用一个字长表示,所以不满足这里的假设条件),unsigned int才会被转换为long型,否则两个操作数都被提升为unsigned long型。若两个操作数都不是long型而其中一个是unsigned int型,则另一个也被转换成unsigned int型,否则两个操作数一定都是int型。尽管算术转换的这些规则带给你的困惑可能多于启发,但是一般的思想是尽可能地保留多类型表达式中涉及的值的精度。这正是通过把不同的类型提升到当前出现的最宽的类型来实现的。
5.6 a、b交换与比较
2016-06-04
x&y是取相同的位与,这个的结果是x和y相同位的一半,x^y是取x和y的不同位,右移相当于除以2
5.7 C和C++的关系
2016-06-06
C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为void foo(int x, int y)。该函数被C编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。
C++提供了C连接交换指定符号extern "C"解决名字匹配问题。
2016-06-06
C是一种结构化语言,重点在于算法和数据结构。C程序的设计首先考虑的是如何通过一个过程,对输入(或环境条件)进行运算处理得到输出(或实现过程(事务)控制)。而对于C++,首先考虑的是如何构造一个对象模型,让这个模型能够契合与之对应的问题域,这样就可以通过获取对象的状态信息得到输出或实现过程(事务)控制。
对于大规模数值运算,C/C++和Java/.NET之间没有明显的性能差异。不过,如果运算设计向量计算、矩阵运算,可以使用FORTRAN或者MATLAB编写计算组件(如COM)。大规模用户界面相关的软件可以考虑使用.NET进行开发(Windows环境下),而且.NET同COM之间的互操作十分容易,同时.NET对数据库访问的支持也相当好。
6.2 const
2016-06-07
1)先看情况1。
如果const位于星号的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于星号的右侧,const就是修饰指针本身,即指针本身是常量。
2016-06-07
const与#define相比有什么不同?
答案:C++语言可以用const定义常量,也可以用#define定义常量,但是前者比后者有更多的优点:● const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查,而对后者只进行字符替换,没有类型安全检查,并且在字符替换中可能会产生意料不到的错误(边际效应)。
● 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试。在C++程序中只使用const常量而不使用宏常量,即const常量完全取代宏常量。
2016-06-07
默认const是外部连接的,C++默认const是内部连接的,这样,如果在C++中想完成与C中同样的事情,必须用extern把内部连接改成外部连接
6.3 sizeof
2016-06-07
在const成员函数中,用mutable修饰成员变量名后,就可以修改类的成员变量了。
2016-06-07
在VC中,我们可以用pack预处理指令来禁止对齐调整。例如,下面的代码将使得结构尺寸更加紧凑,不会出现对齐到4字节问题:
对于这个pack指令的含义,大家可以查询MSDN。请注意,除非你觉得必须,否则不要轻易做这样的调整,因为这将降低程序的性能。目前比较常见的用法有两种,一是这个结构需要被直接写入文件;二是这个结构需要通过网络传给其他程序。
2016-06-08
通过对sizeof与strlen的深入理解,得出两者区别如下:
(1)sizeof操作符的结果类型是size_t,它在头文件中的typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。(2)sizeof是运算符,strlen是函数。
(3)sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以“\0”结尾的。sizeof还可以用函数做参数,比如:输出的结果是sizeof(short),即2。
(4)数组做sizeof的参数不退化,传递给strlen就退化为指针。(5)大部分编译程序在编译的时候就把sizeof计算过了,是类型或是变量的长度。这就是sizeof(x)可以用来定义数组维数的原因:
(6)strlen的结果要在运行的时候才能计算出来,用来计算字符串的长度,而不是类型占内存的大小。(7)sizeof后如果是类型必须加括号,如果是变量名可以不加括号。这是因为sizeof是个操作符而不是个函数。
(8)当使用了一个结构类型或变量时,sizeof返回实际的大小。当使用一静态的空间数组时,sizeof返回全部数组的尺寸。sizeof操作符不能返回被动态分配的数组或外部的数组的尺寸。(9)数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,如fun(char [8])、fun(char [])都等价于fun(char *)。在C++里传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小。如果想在函数内知道数组的大小,需要这样做:进入函数后用memcpy将数组复制出来,长度由另一个形参传进去。代码如下:
(10)计算结构变量的大小就必须讨论数据对齐问题。为了使CPU存取的速度最快(这同CPU取数操作有关,详细的介绍可以参考一些计算机原理方面的书),C++在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。这样做可能会浪费一些内存,但在理论上CPU速度快了。当然,这样的设置会在读写一些别的应用程序生成的数据文件或交换数据时带来不便。MS VC++中的对齐设定,有时候sizeof得到的与实际不等。一般在VC++中加上#pragma pack(n)的设定即可。或者如果要按字节存储,而不进行数据对齐,可以在Options对话框中修改Advanced Compiler选项卡中的“Data Alignment”为按字节对齐。(11)sizeof操作符不能用于函数类型、不完全类型或位字段。不完全类型指具有未知存储大小数据的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等。
6.4 内联函数和宏定义
2016-06-08
内联函数和宏的差别是什么?
答案:内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。而宏只是一个简单的替换。内联函数要做参数类型检查,这是内联函数跟宏相比的优势。
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说inline增加空间消耗换来的是效率提高,这方面和宏是一模一样的,但是inline在和宏相比没有付出任何额外代价的情况下更安全。至于是否需要inline函数,就需要根据实际情况来取舍了。inline一般只用于如下情况:
(1)一个函数不断被重复调用。(2)函数只有简单的几行,且函数内不包含for、while、switch语句。
2016-06-08
一般来说,我们写小程序没有必要定义成inline,但是如果要完成一个工程项目,当一个简单函数被调用多次时,则应该考虑用inline。
宏在C语言里极其重要,而在C++里用得就少多了。关于宏的第一规则是绝不应该去使用它,除非你不得不这样做。几乎每个宏都表明了程序设计语言里、程序里或者程序员的一个缺陷,因为它将在编译器看到程序的正文之前重新摆布这些正文。宏也是许多程序设计工具的主要麻烦。所以,如果你使用了宏,就应该准备只能从各种工具(如排错系统、交叉引用系统、轮廓程序等)中得到较少的服务。宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。关键字inline必须与函数定义体放在一起才能使函数成为内联,仅将inline放在函数声明前面不起任何作用。如下风格的函数Foo不能成为内联函数:
而如下风格的函数Foo则成为内联函数:所以说,inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。内联能提高函数的执行效率,至于为什么不把所有的函数都定义成内联函数?如果所有的函数都是内联函数,还用得着“内联”这个关键字吗?内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜使用内联:1 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。2 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如“偷偷地”执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。一个好的编译器将会根据函数的定义体,自动地取消不值得的内联(这进一步说明了inline不应该出现在函数的声明中)。
7.1 指针基本问题
2016-06-12
指针和引用的差别?
答案:(1)非空区别。在任何情况下都不能使用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针要高。
(2)合法性区别。在使用引用之前不需要测试它的合法性。相反,指针则应该总是被测试,防止其为空。(3)可修改区别。指针与引用的另一个重要的区别是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
(4)应用区别。总的来说,在以下情况下应该使用指针:一是考虑到存在不指向任何对象的可能(在这种情况下,能够设置指针为空),二是需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么应该使用引用。
7.2 传递动态内存
2016-06-12
首先要搞清楚char *str和char str[]:
是分配一个局部数组:是分配一个全局数组:
局部数组是局部变量,它所对应的是内存中的栈。全局数组是全局变量,它所对应的是内存中的全局区域。字符串常量保存在只读的数据段,而不是像全局变量那样保存在普通数据段(静态存储区),如:c占用一个存储区域,但是局部区的数据是可以修改的:
这里c不占存储空间。
7.5 迷途指针
2016-06-13
malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
对于非内部数据类型的对象而言,只用malloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
2016-06-13
因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。new/delete不是库函数,而是运算符。
7.6 指针和句柄
2016-06-14
句柄是一个32位的整数,实际上是Windows在内存中维护的一个对象(窗口等)内存物理地址列表的整数索引。因为Windows的内存管理经常会将当前空闲对象的内存释放掉,当需要时访问再重新提交到物理内存,所以对象的物理地址是变化的,不允许程序直接通过物理地址来访问对象。程序将想访问的对象的句柄传递给系统,系统根据句柄检索自己维护的对象列表就能知道程序想访问的对象及其物理地址了。
句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是驻留在内存的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。但是,如果真的这样认为,那么就大错特错了。我们知道,Windows是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,以此来满足各种应用程序的内存需要。对象被移动意味着它的地址变化了。如果地址总是如此变化,我们该到哪里去找该对象呢?为了解决这个问题,Windows操作系统为各应用程序腾出一些内存地址,用来专门登记各应用对象在内存中的地址变化,而这个地址(存储单元的位置)本身是不变的。Windows内存管理器移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。这样我们只需记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。这个地址是在对象装载(Load)时由系统分配的,当系统卸载时(Unload)又释放给系统。句柄地址(稳定)→记载着对象在内存中的地址→对象在内存中的地址(不稳定)→实际对象。但是,必须注意的是,程序每次重新启动,系统不能保证分配给这个程序的句柄还是原来的那个句柄,而且绝大多数情况下的确不一样。假如我们把进入电影院看电影看成是一个应用程序的启动运行,那么系统给应用程序分配的句柄总是不一样,这和每次电影院售给我们的门票总是不同的座位是一样的道理。HDC是设备描述表句柄。CDC是设备描述表类。用GetSafeHwnd和FromHandle可以互相转换。
答案:句柄和指针其实是两个截然不同的概念。Windows系统用句柄标记系统资源,隐藏系统的信息。你只要知道有这个东西,然后去调用就行了,它是个32bit的uint。指针则标记某个物理内存地址,两者是不同的概念。
7.7 this指针
2016-06-14
关于This指针,有这样一段描述:当你进入一个房子后,你可以看见桌子、椅子、地板等,但是房子你是看不到全貌了。对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?this指针是这样一个指针,它时时刻刻指向这个实例本身。
this指针易混的几个问题如下。(1)This指针本质是一个函数参数,只是编译器隐藏起形式的,语法层面上的参数。
this只能在成员函数中使用,全局函数、静态函数都不能使用this。实际上,成员函数默认第一个参数为T* const this。
如:其中,func的原型在编译器看来应该是:(2)this在成员函数的开始前构造,在成员的结束后清除。这个生命周期同任何一个函数的参数是一样的,没有任何区别。当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。如:此处,编译器将会编译成:
看起来和静态函数没差别,不过,区别还是有的。编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,如VC通常是通过ecx寄存器传递this参数的。
2016-06-14
(3)this指针并不占用对象的空间。
this相当于非静态成员函数的一个隐函的参数,不占对象的空间。它跟对象之间没有包含关系,只是当前调用函数的对象被它指向而已。所有成员函数的参数,不管是不是隐含的,都不会占用对象的空间,只会占用参数传递时的栈空间,或者直接占用一个寄存器。
(4)this指针是什么时候创建的?this在成员函数的开始执行前构造,在成员的执行结束后清除。
但是如果class或者struct里面没有方法的话,它们是没有构造函数的,只能当做C的struct使用。采用TYPE xx的方式定义的话,在栈里分配内存,这时候this指针的值就是这块内存的地址。采用new方式创建对象的话,在堆里分配内存,new操作符通过eax返回分配的地址,然后设置给指针变量。之后去调用构造函数(如果有构造函数的话),这时将这个内存块的地址传给ecx。(5)this指针存放在何处?堆、栈、还是其他?this指针会因编译器不同而有不同的放置位置。可能是堆、栈,也可能是寄存器。
C++是一种静态的语言,那么对C++的分析应该从语法层面和实现层面两个方面进行。语法上,this是个指向对象的“常指针”,因此无法改变。它是一个指向相应对象的指针。所有对象共用的成员函数利用这个指针区别不同变量,也就是说,this是“不同对象共享相同成员函数”的保证。
而在实际应用的时候,this应该是个寄存器参数。这个不是语言规定的,而是“调用约定”,C++的默认调用约定是__cdecl,也就是C风格的调用约定。该约定规定参数自右向左入栈,由调用方负责平衡堆栈。对于成员函数,将对象的指针(即this指针)存入ecx中(有的书将这一点单独分开,叫做thiscall,但是这的确是cdecl的一部分)。因为这只是一个调用约定,不是语言的组成部分,不同编译器自然可以自由发挥。但是现在的主流编译器都是这么做的。(6)this指针是如何传递给类中的函数的?绑定?还是在函数参数的首参数就是this指针?那么,this指针又是如何找到“类实例后函数”的?
大多数编译器通过ecx寄存器传递this指针。事实上,这也是一个潜规则。一般来说,不同编译器都会遵从一致的传参规则,否则不同编译器产生的obj就无法匹配了。(7)我们只有获得一个对象后,才能通过对象使用this指针。如果我们知道一个对象this指针的位置,可以直接使用吗?this指针只有在成员函数中才有定义。因此,你获得一个对象后,也不能通过对象使用this指针。所以,我们无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以通过&this获得),也可以直接使用它。
第9章 STL模板与容器
2016-06-15
STL有以下优点:
● 可以方便、容易地实现搜索数据或对数据排序等一系列的算法。● 调试程序时更加安全和方便。
● 即使是人们用STL在UNIX平台下写的代码,也可以很容易地理解(因为STL是跨平台的)。STL中一些基础概念的定义如下。
● 模板(Template):类(及结构等各种数据类型和函数)的宏(macro)。有时叫做甜饼切割机(cookie cutter),正规的名称应叫做泛型(generic)。一个类的模板叫做泛型类(generic class),而一个函数的模板也自然而然地被叫做泛型函数(generic function)。● STL标准模板库:一些聪明人写的一些模板,现在已成为每个人所使用的标准C++语言中的一部分。
● 容器(Container):可容纳一些数据的模板类。STL中有vector、set、map、multimap和deque等容器。● 向量(Vector):基本数组模板,这是一个容器。
● 游标(Iterator):这是一个奇特的东西,它是一个指针,用来指向STL容器中的元素,也可以指向其他的元素。
9.1 向量容器
2016-06-15
:C++的一个新特性就是采用了标准模板库(STL)。所有主要编译器销售商现在都把标准模板库作为编译器的一部分进行提供。标准模板库是一个基于模板的容器类库,包括链表、列表、队列和堆栈。标准模板库还包含许多常用的算法,包括排序和查找。
标准模板库的目的是提供对常用需求重新开发的一种替代方法。标准模板库已经经过测试和调试,具有很高的性能并且是免费的。最重要的是,标准模板库是可重用的。当你知道如何使用一个标准模板库的容器以后,就可以在所有的程序中使用它而不需要重新开发了。容器是包容其他对象的对象。标准C++库提供了一系列的容器类,它们都是强有力的工具,可以帮助C++开发人员处理一些常见的编程任务。标准模板库容器类有两种类型,分别为顺序和关联。顺序容器可以提供对其成员的顺序访问和随机访问。关联容器则经过优化关键值访问它们的元素。标准模板库在不同操作系统间是可移植的。所有标准模板库容器类都在namespace std中定义。
9.2 泛型编程
2016-06-15
泛型编程是一种基于发现高效算法的最抽象表示的编程方法。也就是说,以算法为起点并寻找能使其工作且有效率工作的最一般的必要条件集。令人惊讶的是,很多不同的算法都需要相同的必要条件集,并且这些必要条件有多种不同的实现方式。类似的事实在数学里也可以看到。大多数不同的定理都依赖于同一套公理,并且对于同样的公理存在多种不同的模型。泛型编程假定有某些基本的法则在支配软件组件的行为,并且基于这些法则有可能设计可互操作的模块,甚至还有可以使用此法则去指导我们的软件设计。STL就是一个泛型编程的例子。C++是我可以实现令人信服的例子的语言。
9.3 模板
2016-06-15
数据结构本身十分重要。当程序中存在着对时间要求很高的部分时,数据结构的选择就显得更加重要。
经典的数据结构数量有限,但是我们常常重复着一些为了实现向量、链表等结构而编写的代码。这些代码都十分相似,只是为了适应不同数据的变化而在细节上有所不同。STL容器就为我们提供了这样的方便,它允许我们重复利用已有的实现构造自己的特定类型下的数据结构。通过设置一些模板类,STL容器对最常用的数据结构提供了支持。这些模板的参数允许我们指定容器中元素的数据类型,可以将许多重复而乏味的工作简化。容器部分主要由头文件、、、、
10.3 成员变量
2016-06-15
必须使用静态成员变量在一个类的所有实例间共享数据。如果想限制对静态成员变量的访问,则必须把它们声明为保护型或私有型。不允许用静态成员变量去存放某一个对象的数据。静态成员数据是在这个类的所有对象间共享的。
10.4 构造函数和析构函数
2016-06-15
由于在生成CChild对象c时,实际上在调用CChild类的构造函数之前必须首先调用其基类CBase的构造函数,所以当撤销c时,也会在调用CChild类析构函数之后,调用CBase类的析构函数(析构函数调用顺序与构造函数相反)。也就是说,无论析构函数是不是虚函数,派生类对象被撤销时,肯定会依次上调其基类的析构函数。
那么为什么CObject类要搞一个虚的析构函数呢?因为多态的存在。
仍以上面的代码为例,如果main()中有如下代码:那么在pBase指针被撤销时,调用的是CBase的析构函数还是CChild的呢?显然是CBase的(静态联编)析构函数。但如果把CBase类的析构函数改成virtual型,当pBase指针被撤销时,就会先调用CChild类构造函数,再调用CBase类构造函数。
答案:在这个例子里,所有对象都存在于栈框中,当离开其所处的作用域时,该对象会被自动撤销,似乎看不出什么大问题。但是试想,如果CChild类的构造函数在堆中分配了内存,而其析构函数又不是virtual型的,那么撤销pBase时,将不会调用CChild::~CChild(),从而不会释放CChild::CChild()占据的内存,造成内存泄露。将CObject的析构函数设为virtual型,则所有CObject类的派生类的析构函数都将自动变为virtual型,这保证了在任何情况下,不会出现由于析构函数未被调用而导致的内存泄露。这才是MFC将CObject::~CObject()设为virtual型的真正原因。
2016-06-15
虚函数采用一种虚调用的办法。虚调用是一种可以在只有部分信息的情况下工作的机制,特别允许我们调用一个只知道接口而不知道其准确对象类型的函数。但是如果要创建一个对象,你势必要知道对象的准确类型,因此构造函数不能为虚。
2016-06-15
:如果虚函数是非常有效的,我们是否可以把每个函数都声名为虚函数?
答案:不行,这是因为虚函数是有代价的:由于每个虚函数的对象都必须维护一个v表,因此在使用虚函数的时候都会产生一个系统开销。如果仅是一个很小的类,且不想派生其他类,那么根本没必要使用虚函数。
10.6 多态的概念
2016-06-15
什么是多态?
答案:开门,开窗户,开电视。在这里的“开”就是多态!
多态性可以简单地概括为“一个接口,多种方法”,在程序运行的过程中才决定调用的函数。多态性是面向对象编程领域的核心概念。多态(Polymorphisn),按字面的意思就是“多种形状”。多态性是允许你将父对象设置成为和它的一个或更多的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单地说就是一句话,允许将子类类型的指针赋值给父类类型的指针。多态性在Object Pascal和C++中都是通过虚函数(Virtual Function)实现的。
2016-06-15
虚函数就是允许被其子类重新定义的成员函数。而子类重新定义父类虚函数的做法,称为“覆盖”(override),或者称为“重写”。这里有一个初学者经常混淆的概念。上面说了覆盖(override)和重载(overload)。覆盖是指子类重新定义父类的虚函数的做法。而重载,是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。其实,重载的概念并不属于“面向对象编程”。重载的实现是编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是int_func,str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的(记住:是静态)。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关。真正与多态相关的是“覆盖”。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态(记住:是动态)地调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。结论就是重载只是一种语言特性,与多态无关,与面向对象也无关。
引用一句Bruce Eckel的话:“不要犯傻,如果它不是晚绑定,它就不是多态。”那么,多态的作用是什么呢?我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了代码重用。而多态则是为了实现另一个目的—接口重用!而且现实往往是,要有效重用代码很难,而真正最具有价值的重用是接口重用,因为“接口是公司最有价值的资源。设计接口比用一堆类来实现这个接口更费时间,而且接口需要耗费更昂贵的人力和时间”。其实,继承为重用代码而存在的理由已经越来越薄弱,因为“组合”可以很好地取代继承的扩展现有代码的功能,而且“组合”的表现更好(至少可以防止“类爆炸”)。因此笔者个人认为,继承的存在很大程度上是作为“多态”的基础而非扩展现有代码的方式。
10.7 友元
2016-06-15
类具有封装和信息隐藏的特性。只有类的成员函数才能访问类的私有成员,程序中的其他函数是无法访问私有成员的。非成员函数可以访问类中的公有成员,但是如果将数据成员都定义为公有的,这又破坏了隐藏的特性。另外,应该看到在某些情况下,特别是在对某些成员函数多次调用时,由于参数传递、类型检查和安全性检查等都需要时间开销,而影响程序的运行效率。
为了解决上述问题,提出一种使用友元的方案。友元是一种定义在类外部的普通函数,但它需要在类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字friend。友元不是成员函数,但是它可以访问类中的私有成员。友元的作用在于提高程序的运行效率,但是,它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。
11.2 私有继承
2016-06-16
一个私有的或保护的派生类不是子类,因为非公共的派生类不能做基类能做的所有的事。
2016-06-16
公有继承(public)、私有继承(private)和保护继承(protected)是常用的3种继承方式。
1.公有继承方式基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。这里保护成员与私有成员相同。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员可见,基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态;基类的私有成员不可见,基类的私有成员仍然是私有的,派生类不可访问基类中的私有成员。基类成员对派生类对象的可见性对派生类对象来说,基类的公有成员是可见的,其他成员是不可见的。
所以,在公有继承时,派生类的对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有成员和保护成员。2.私有继承方式
基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员是可见的,基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的,派生类不可访问基类中的私有成员。
基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。所以,在私有继承时,基类的成员只能由直接派生类访问,而无法再往下继承。
3.保护继承方式这种继承方式与私有继承方式的情况相同。两者的区别仅在于对派生类的成员而言,基类成员对其对象的可见性与一般类及其对象的可见性相同,公有成员可见,其他成员不可见。
基类成员对派生类的可见性对派生类来说,基类的公有成员和保护成员是可见的,基类的公有成员和保护成员都作为派生类的保护成员,并且不能被这个派生类的子类所访问;基类的私有成员是不可见的,派生类不可访问基类中的私有成员。基类成员对派生类对象的可见性对派生类对象来说,基类的所有成员都是不可见的。
所以,在保护继承时,基类的成员也只能由直接派生类访问,而无法再往下继承。C++支持多重继承,从而大大增强了面向对象程序设计的能力。多重继承是一个类从多个基类派生而来的能力。派生类实际上获取了所有基类的特性。当一个类是两个或多个基类的派生类时,必须在派生类名和冒号之后,列出所有基类的类名,基类间用逗号隔开。派生类的构造函数必须激活所有基类的构造函数,并把相应的参数传递给它们。派生类可以是另一个类的基类,这样,相当于形成了一个继承链。当派生类的构造函数被激活时,它的所有基类的构造函数也都会被激活。在面向对象的程序设计中,继承和多重继承一般指公共继承。在无继承的类中,protected和private控制符是没有差别的。在继承中,基类的private对所有的外界都屏蔽(包括自己的派生类),基类的protected控制符对应用程序是屏蔽的,但对其派生类是可访问的。
保护继承和私有继承只是在技术上讨论时有其一席之地。
11.4 多重继承
2016-06-16
多重继承在语言上并没有什么很严重的问题,但是标准本身只对语义做了规定,而对编译器的细节没有做规定。所以在使用时(即使是继承),最好不要对内存布局等有什么假设。此类的问题还有虚析构函数等。为了避免由此带来的复杂性,通常推荐使用复合。但是,在《C++设计新思维》(Andrei Alexandrescu)一书中对多重继承和模板有极为精彩的运用。
(1)多重继承本身并没有问题,如果运用得当可以收到事半功倍的效果。不过大多数系统的类层次往往有一个公共的基类,就像MFC中的Cobject,Java中的Object。而这样的结构如果使用多重继承,稍有不慎,将会出现一个严重现象——菱形继承,这样的继承方式会使得类的访问结构非常复杂。但并非不可处理,可以用virtual继承(并非唯一的方法)及Loki库中的多继承框架来掩盖这些复杂性。(2)从哲学上来说,C++多重继承必须要存在,这个世界本来就不是单根的。从实际用途上来说,多重继承不是必需的,但这个世界上有多少东西是必需的呢?对象不过是一组有意义的数据集合及其上的一组有意义的操作,虚函数(晚期绑定)也不过是一堆函数入口表,重载也不过是函数名扩展,这些东西都不是必需的,而且对它们的不当使用都会带来问题。但是没有这些东西行吗?很显然,不行。
(3)多重继承在面向对象理论中并非是必要的——因为它不提供新的语义,可以通过单继承与复合结构来取代。而Java则放弃了多重继承,使用简单的interface取代。多重继承是把双刃剑,应该正确地对待。况且,它不像goto,不破坏面向对象语义。跟其他任何威力强大的东西一样,用好了会带来代码的极大精简,用坏了那就不用说了。C++是为实用而设计的,在语言里有很多东西存在着各种各样的“缺陷”。所以,对于这种有“缺陷”的东西,它的优劣就要看使用它的人。C++不回避问题,它只是把问题留给使用者,从而给大家更多的自由。像Ada、Pascal这类定义严格的语言,从语法上回避了问题,但并不是真正解决了问题,而使人做很多事时束手束脚(当然,习惯了就好了)。
(4)多重继承本身并不复杂,对象布局也不混乱,语言中都有明确的定义。真正复杂的是使用了运行时多态(virtual)的多重继承(因为语言对于多态的实现没有明确的定义)。为什么非要说多重继承不好呢?如果这样的话,指针不是更容易出错,运行时多态不是更不好理解吗?因为C++中没有interface这个关键字,所以不存在所谓的“接口”技术。但是C++可以很轻松地做到这样的模拟,因为C++中的不定义属性的抽象类就是接口。
(5)要了解C++,就要明白有很多概念是C++试图考虑但是最终放弃的设计。你会发现很多Java、C#中的东西都是C++考虑后放弃的。不是说这些东西不好,而是在C++中它将破坏C++作为一个整体的和谐性,或者C++并不需要这样的东西。举一个例子来说明,C#中有一个关键字base用来表示该类的父类,C++却没有对应的关键字。为什么没有?其实C++中曾经有人提议用一个类似的关键字inherited,来表示被继承的类,即父类。这样一个好的建议为什么没有被采纳呢?这本书中说得很明确,因为这样的关键字既不必须又不充分。不必须是因为C++有一个typedef * inherited,不充分是因为有多个基类,你不可能知道inherited指的是哪个基类。很多其他语言中存在的时髦的东西在C++中都没有,这之中有的是待改进的地方,有的是不需要,我们不能一概而论,需要具体问题具体分析。
2016-06-16
比如,C继承自A和B,如果出现了相同的函数foo(),那么C.A::foo(),C.B::foo()就分别代表从A类中继承的foo函数和从B类中继承的foo函数。
11.5 检测并修改不适合的继承
2016-06-16
如果不指定public,C++默认的是私有继承。私有继承是无法继承并使用父类函数中的公有变量的。
11.6 纯虚函数
2016-06-16
因为Shape类中的Draw函数是一个纯虚函数,所以Shape类是不能实例化一个对象的。Shape s1;是不可以的,解决方法是把Draw函数修改成一般的虚函数。
2016-06-16
C++中如何阻止一个类被实例化?
2.一般在什么时候构造函数被声明成private呢?3.什么时候编译器会生成默认的copy constructor呢?
4.如果你已经写了一个构造函数,编译器还会生成copy constructor吗?[英国某著名计算机图形图像公司面试题]答案:
1.使用抽象类,或者构造函数被声明成private。2.比如要阻止编译器生成默认的copy constructor的时候。
3.只要自己没写,而程序中需要,都会生成。4.会生成。
11.7 运算符重载与RTTI
2016-06-16
C++引入的额外开销体现在以下两方面。
1.编译时开销模板、类层次结构、强类型检查等新特性,以及大量使用了这些新特性的C++模板、算法库都明显地增加了C++编译器的负担。但是应当看到,这些新机能在不增加程序执行效率的前提下,明显降低了广大C++程序员的工作量。
2.运行时开销运行时开销恐怕是程序员最关心的问题之一了。相对于传统C程序而言,C++中有可能引入额外运行时开销特性包括:
● 虚基类。● 虚函数。
● RTTI(dynamic_cast和typeid)。● 异常。
● 对象的构造和析构。虚基类,从直接虚继承的子类中访问虚基类的数据成员或其虚函数时,将增加两次指针引用(大部分情况下可以优化为一次)和一次整型加法的时间开销。定义一个虚基类表,定义若干虚基类表指针的空间开销。虚函数的运行开销有进行整型加法和指针引用的时间开销。定义一个虚表,定义若干个(大部分情况下是一个)虚表指针的空间开销。
RTTI的运行开销主要有进行整型比较和取址操作(可能还会有一两次整形加法)所增加的时间开销。定义一个type_info对象(包括类型ID和类名称)的空间开销。"dynamic_cast"用于在类层次结构中漫游,对指针或引用进行自由的向上、向下或交叉转化。"typeid"则用于获取一个对象或引用的确切类型。一般地讲,能用虚函数解决的问题就不要用"dynamic_cast",能够用"dynamic_cast"解决的就不要用"typeid"。关于异常,对于几乎所有编译器来说,在正常情况(未抛出异常)下,try块中的代码执行效率和普通代码一样高,而且由于不再需要使用传统上通过返回值或函数调用来判断错误的方式,代码的实际执行效率还会进一步提高。抛出和捕捉异常的开销也只是在某些情况下会高于函数返回和函数调用的开销。
关于构造和析构,开销也不总是存在的。对于不需要初始化/销毁的类型,并没有构造和析构的开销,相反对于那些需要初始化/销毁的类型来说,即使用传统的C方式实现,也至少需要与之相当的开销。实事求是地讲,RTTI是有用的。但因为一些理论上及方法论上的原因,它破坏了面向对象的纯洁性。
首先,它破坏了抽象,使一些本来不应该被使用的方法和属性被不正确地使用。其次,因为运行时类型的不确定性,它把程序变得更脆弱。第三点,也是最重要的一点,它使程序缺乏扩展性。当加入了一个新的类型时,你也许需要仔细阅读你的dynamic_cast或instanceof的代码,必要时改动它们,以保证这个新的类型的加入不会导致问题。而在这个过程中,编译器将不会给你任何帮助。很多人一提到RTTI,总是侧重于它的运行时的开销。但是,相比于方法论上的缺点,这点运行时的开销真是无足轻重的。
总的来说,RTTI因为它的方法论上的一些缺点,它必须被非常谨慎地使用。今天面向对象语言的类型系统中的很多东西就是产生于避免RTTI的各种努力。
2016-06-16
RTTI是Runtime Type Information的缩写,从字面上来理解就是执行时期的类型信息,其重要作用就是动态判别执行时期的类型。有的读者会认为设计类时使用虚函数就已经足够了,可是虚函数有本身的局限性,当涉及类别阶层时,需要判断某个对象所属的类别,而因为类别设计中大量使用了虚函数,所以使得这一工作难以实现,但又极其重要,于是使用RTTI的typeid运算符能使程序员确定对象的动态类型。
12.1 位制转换
2016-06-16
首先参数5为int型,32位平台中为4字节,因此在stack中分配4字节的内存,用于存放参数5。
然后printf根据说明符“%f”,认为参数应该是个double型(在printf函数中,float会自动转换成double),因此从stack中读了8个字节。很显然,内存访问越界,会发生什么情况不可预料。如果在printf或者scanf中指定了“%f”,那么在后面的参数列表中也应该指定一个浮点数,或者一个指向浮点变量的指针,否则不应加载支持浮点数的函数。
于是("%f",5)有问题,而("%f",5.0)则可行。
2016-06-16
“int z:33;”定义整型变量z为33位,也就是超过了4字节。这是不合法的,会造成越界,所以程序会报错。
2016-06-16
In which system(进制) expression 13*16=244 is true?(下面哪个进制能表述13*16=244是正确的?)[中国台湾某计算机硬件公司V2010年5月面试题]
A.5B.7
C.9D.11
解析:13如果是一个十进制的话,它可以用13=1*101+3*100来表示。现在我们不知道13是几进制,那我们姑且称其X进制。X进制下的13转化为十进制可以用13=1*X1+3*X0;表示;X进制下的16转化为十进制可以用16=1*X1+6*X0;表示;X进制下的244转化为十进制可以用244=2*X2+4*X1+4*X0;表示;因此X进制下的13*16=244可以转化为十进制下的等式:(1*X1+3*X0)* (1*X1+6*X0)=2*X2+4*X1+4*X0。整理得X*X+6*X+3*X+3*6=2*X*X+4*X+4;最后得出一元二次方程X*X-5*X-14=0。答案X=-2或者X=7。X=-2不合题意舍弃,所以X=7。
2016-06-17
C++有4个类型转换操作符,这4个操作符是static_cast、const_cast、dynamic_cast和reinterpret_cast。
例如,假设你想把一个int转换成double,以便让包含int类型变量的表达式产生出浮点数值的结果。你应该这样写:这样的类型转换不论是对人工还是对程序都很容易识别。在C++中,static_cast在功能上相对C语言来说有所限制。如不能用static_cast像用C风格的类型转换一样把struct转换成int类型,或者把double类型转换成指针类型;另外,static_cast不能从表达式中去除const属性,因为另一个新的类型转换操作符const_cast有这样的功能。
其他C++类型转换操作符被用在需要更多限制的地方。const_cast最普通的用途就是转换掉对象的const属性。通过使用const_cast,让编译器知道通过类型转换想做的只是改变一些东西的constness或者volatileness属性。这个含义被编译器所约束。如果你试图使用const_cast来完成修改constness或者volatileness属性之外的事情,你的类型转换将被拒绝。下面是一个const_cast的例子:static_cast和reinterpret_cast操作符修改了操作数类型。它们不是互逆的;static_cast在编译时使用类型信息执行转换,在转换执行必要的检测(诸如指针越界计算,类型检查),其操作数相对是安全的。另一方面,reinterpret_cast仅仅是重新解释了给出的对象的比特模型而没有进行二进制转换,编译器隐式执行任何类型转换都可由static_cast显示完成,reinterpret_cast通常为操作数的位模式提供较低层的重新解释。例子如下:上面的例子中,我们将一个变量从int转换到double。这些类型的二进制表达式是不同的。要将整数9转换到双精度整数9,static_cast需要正确地为双精度整数d补足比特位。其结果为9.0。而reinterpret_cast的行为却不同:
在进行计算以后,d包含无用值。这是因为reinterpret_cast仅仅是复制n的比特位到d,没有进行必要的分析。reinterpret_cast这个操作符被用于的类型转换的转换结果几乎都是实现时定义(implementation-defined)。因此,使用reinterpret_casts的代码很难移植。转换函数指针的代码是不可移植的,(C++不保证所有的函数指针都被用一样的方法表示),在一些情况下这样的转换会产生不正确的结果。所以应该避免转换函数指针类型,按照C++新思维的话来说,reinterpret_cast是为了映射到一个完全不同类型的意思,这个关键词在我们需要把类型映射回原有类型时用到它。我们映射到的类型仅仅是为了故弄玄虚和其他目的,这是所有映射中最危险的。reinterpret_cast就是一把锐利无比的双刃剑,除非你处于背水一战和火烧眉毛的危急时刻,否则绝不能使用。对于dynamic_cast要注意以下4点:
● dynamic_cast是在运行时检查的,dynamic_cast用于在继承体系中进行安全的向下转换downcast(当然也可以向上转换,但是没必要,因为完全可以用虚函数实现),即基类指针/引用到派生类指针/引用的转换。如果源和目标类型没有继承/被继承关系,编译器会报错;否则必须在代码里判断返回值是否为NULL来确认转换是否成功。● dynamic_cast不是扩展C++中style转换的功能,而是提供了类型安全性。你无法用dynamic_cast进行一些“无理”的转换。
● dynamic_cast是4个转换中唯一的RTTI操作符,提供运行时类型检查。● dynamic_cast不是强制转换,而是带有某种“咨询”性质的。如果不能转换,dynamic_cast会返回NULL,表示不成功。这是强制转换做不到的。
下面是一个例子:在本题中,MyItem与IGlyph是继承关系,可以适用dynamic_cast类型转换,而因为我们不知道确定的IWidgetSelector::Selection()返回的具体类型是什么,所以应用dynamic_cast“试探性”地进行类型转换是十分必要的。
2016-06-17
给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a的bit 3。在以上两个操作中,要保持其他位不变。
2016-06-17
面试官希望看到的几个要点为说明常数、“|=”和“&=~”操作。
12.2 嵌入式编程
2016-06-17
volatile问题。
当一个对象的值可能会在编译器的控制或监测之外被改变时,例如一个被系统时钟更新的变量,那么该对象应该声明成volatile。因此编译器执行的某些例行优化行为不能应用在已指定为volatile的对象上。volatile限定修饰符的用法与const非常相似——都是作为类型的附加修饰符。例如:display_register是一个int型的volatile对象;curr_task是一个指向volatile的Task类对象的指针;ixa是一个volatile的整型数组,数组的每个元素都被认为是volatile的;bitmap_buf是一个volatile的Screen类对象,它的每个数据成员都被视为volatile的。
volatile修饰符的主要目的是提示编译器该对象的值可能在编译器未监测到的情况下被改变,因此编译器不能武断地对引用这些对象的代码做优化处理。答案:
volatile的语法与const是一样的,但是volatie的意思是“在编译器认识的范围外,这个数据可以被改变”。不知什么原因,环境正在改变数据(可能通过多任务处理),所以,volatile告诉编译器不要擅自做出有关数据的任何假定——在优化期间这是特别重要的。如果编译器说:“我已经把数据读进寄存器,而且再没有与寄存器接触。”在一般情况下,它不需要再读这个数据。但是,如果数据是volatile修饰的,编译器则不能做出这样的假定,因为数据可能被其他进程改变了,编译器必须重读这个数据而不是优化这个代码。就像建立const对象一样,程序员也可以建立volatile对象,甚至还可以建立const volatile对象。这个对象不能被程序员改变,但可通过外面的工具改变。
2016-06-17
关键字const的作用是为读你代码的人传达非常有用的信息。实际上,声明一个参数为常量是为了告诉用户这个参数的应用目的。如果你曾花很多时间清理其他人留下的垃圾,你就会很快学会感谢这点儿多余的信息。当然,懂得用const的程序员很少会留下垃圾让别人来清理。通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
2016-06-17
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
● 并行设备的硬件寄存器(如状态寄存器)。● 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)。
● 多线程应用中被几个任务共享的变量。
12.3 static
2016-06-17
在C语言中,static关键字至少有下列几个作用:
● 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。● 在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其他函数访问。
● 在模块内的static函数只可被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内。● 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。
● 在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。
13.6 堆
2016-06-17
进行C/C++编程时,需要程序员对内存的了解比较精准。经常需要操作的内存可分为以下几个类别。
● 栈区(stack):由编译器自动分配和释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。● 堆区(heap):一般由程序员分配和释放,若程序员不释放,程序节束时可能由操作系统回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
● 全局区(静态区)(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序节束后由系统释放。● 文字常量区:常量字符串就是放在这里的。程序节束后由系统释放。
● 程序代码区:存放函数体的二进制代码。以下是一段实际说明的程序代码:堆和栈的理论知识如下。
1.申请方式栈:由系统自动分配。例如,声明在函数中的一个局部变量int b,系统自动在栈中为b开辟空间。
堆:需要程序员自己申请,并指明大小,在C中用malloc函数。如:
在C++中用new运算符,如:但是注意p1、p2本身是在栈中的。
2.申请后系统的响应栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确地释放本内存空间。另外,由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动地将多余的那部分重新放入空闲链表中。3.申请大小的限制
栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在Windows下,栈的大小是2MB(也有的说是1MB,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间,将提示overflow。因此,能从栈获得的空间较小。堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表存储空闲内存地址的,自然是不连续的。而链表的遍历方向是由低地址向高地址,堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。4.申请效率的比较
栈:由系统自动分配,速度较快。但程序员无法控制。堆:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
另外,在Windows下,最好的方式是用VirtualAlloc分配内存。不是在堆,也不是在栈,而是直接在进程的地址空间中保留一块内存,虽然用起来最不方便,但是速度最快,也最灵活。5.堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数。在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用节束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。6.存取效率的比较
aaaaaaaaaaa是在运行时刻赋值的,而bbbbbbbbbbb是在编译时就确定的。但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。比如:
对应的汇编代码如下:
2016-06-17
第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把edx指中,再根据edx读取字符,显然慢了。
7.小节堆和栈的区别可以用如下的比喻来描述。
使用栈就像我们去饭馆里吃饭,只管点菜(发出申请)、付钱和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作。好处是快捷,但是自由度小。使用堆就像是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。数据结构方面的堆和栈,这些都是不同的概念。这里的堆实际上指的就是(满足堆性质的)优先队列的一种数据结构,第一个元素有最高的优先权;栈实际上就是满足先进后出的性质的数学或数据结构。虽然“堆栈”的说法是连起来叫,但是它们还是有很大区别的。
2016-06-17
heap是堆,stack是栈。
stack的空间由操作系统自动分配/释放,heap上的空间手动分配/释放。stack空间有限,heap是很大的自由存储区。
C中的malloc函数分配的内存空间即在堆上,C++中对应的是new操作符。程序在编译期对变量和函数分配内存都在栈上进行,且程序运行过程中函数调用时参数的传递也在栈上进行。
2016-06-17
接触过编程的人都知道,高级语言都能通过变量名访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?
首先,来了解一下C语言的变量是如何在内存分布的。C语言有全局变量(Global)、本地变量(Local)、静态变量(Static)和寄存器变量(Register)。每种变量都有不同的分配方式。先来看下面这段代码:编译后的执行结果是:
输出的结果就是变量的内存地址。其中v1、v2、v3是本地变量,g1、g2、g3是全局变量,s1、s2、s3是静态变量。你可以看到这些变量在内存中是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部分:代码区、静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区。栈是一种线性节构,堆是一种链式节构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰的。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量,如下图所示。堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。Windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“_stdcall”和“_cdecl”前缀区分。先看下面这段代码:
编译后的执行结果是:下面详细解释函数调用的过程中堆栈的分布:
在堆栈中分布变量是从高地址向低地址分布,EBP指向栈底指针,ESP是指向栈顶的指针,根据__stdcall调用约定,参数从右向左入栈,所以首先,3个参数以从右到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”。栈内分布如下图所示。然后函数的返回地址入栈,栈内分布如下图所示。通过跳转指令进入函数。函数地址入栈后,EBP入栈,然后把当前的ESP的值给EBP,汇编下指令为:
此时栈底指针和栈顶指针指向同一位置,栈内分布为如下图所示。然后是int var1=param1;int var2=param2;int var3=param3;也就是变量var1,var2,var3的初始化(从左向右的顺序入栈),按声明顺序依次存储到EBP-4,EBP-8,EBP-12位置,栈内分布如下图所示。
2016-06-17
Windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存
13.8 排序
2016-06-18
按平均时间将排序分为以下4类。
● 平方阶(O(n2))排序:一般称为简单排序,例如直接插入、直接选择和冒泡排序。● 线性对数阶(O(nlgn))排序:如快速、堆和归并排序。
● O(n1+£)阶排序:£是介于0和1之间的常数,即0<£<1,如希尔排序。● 线性阶(O(n))排序:如桶、箱和基数排序。
简单排序中直接插入排序最好,快速排序最快。当文件为正序时,直接插入排序和冒泡排序均最佳。1.影响排序效果的因素因为不同的排序方法适应不同的应用环境和要求,所以选择合适的排序方法应综合考虑下列因素:
● 待排序的记录数目n。● 记录的大小(规模)。● 关键字的节构及其初始状态。
● 对稳定性的要求。● 语言工具的条件。
● 存储节构。● 时间和辅助空间复杂度等。
2.不同条件下排序方法的选择(1)若n较小(如n≤50),可采用直接插入或直接选择排序。
当记录规模较小时,直接插入排序较好。否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜。(2)若文件初始状态基本有序(指正序),则应选用直接插入排序、冒泡排序或随机的快速排序为宜。
(3)若n较大,则应采用时间复杂度为O(nlgn)的排序方法(快速排序、堆排序或归并排序)。快速排序被认为是目前基于比较的内部排序中最好的方法。当待排序的关键字随机分布时,快速排序的平均时间最短。
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况。这两种排序都是不稳定的。若要求排序稳定,则可选用归并排序。但本章介绍的从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序节合在一起使用。先利用直接插入排序求得较长的有序子文件,然后再两两归并之。因为直接插入排序是稳定的,所以改进后的归并排序仍是稳定的。
14.1 整数字符串转化
2016-06-18
整数转化成字符串,可以采用加'0',再逆序的办法,整数加'0'就会隐性转化成char类型的数。
16.1 进程
2016-06-21
作业:用户在一次解题或一个事务处理过程中要求计算机系统所做工作的集合。它包括用户程序、所需要的数据及控制命令等。作业是由一系列有序的步骤组成的。
进程:一个程序在一个数据集合上的一次运行过程。所以一个程序在不同数据集合上运行,乃至一个程序在同样数据集合上的多次运行都是不同的进程。线程:线程是进程中的一个实体,是被系统独立调度和执行的基本单位。
管程:管程实际上是定义了一个数据结构和在该数据结构上的能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。
2016-06-21
现在最常用的进程间通信的方式有信号、信号量、消息队列、共享内存。所谓进程通信,就是不同进程之间进行一些“接触”。这种接触有简单,也有复杂。机制不同,复杂度也不一样。通信是一个广义上的意义,不仅仅指传递一些message。它们的使用方法是基本相同的,所以只要掌握了一种使用方法,然后记住其他的使用方法就可以了。信号和信号量是不同的,它们虽然都可用来实现同步和互斥,但前者是使用信号处理器来进行的,后者是使用P、V操作来实现的。消息队列是比较高级的一种进程间通信方法,因为它真的可以在进程间传送message,连传送一个“I seek you”都可以。
一个消息队列可以被多个进程所共享(IPC就是在这个基础上进行的);如果一个进程的消息太多,一个消息队列放不下,也可以用多于一个的消息队列(不过可能管理会比较复杂)。共享消息队列的进程所发送的消息中除了message本身外还有一个标志,这个标志可以指明该消息将由哪个进程或者是哪类进程接受。每一个共享消息队列的进程针对这个队列也有自己的标志,可以用来声明自己的身份。
2016-06-21
在Windows编程中互斥器(mutex)的作用和临界区(critical section)类似,请说一下二者间的主要区别。[中国台湾某著名杀毒软件公司2005年面试题]
解析:多线程编程问题。答案:两者的区别是mutex可以用于进程之间互斥,critical section是线程之间的互斥。
2016-06-21
所谓deadlocks(死锁)是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。
产生死锁的4个必要条件如下。● 互斥条件:一个资源每次只能被一个进程使用。
● 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。● 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
● 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。这4个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的解除与预防方法如下:理解了死锁的原因,尤其是产生死锁的4个必要条件,就可以最大可能地避免、预防和解除死锁。所以,在系统设计、进程调度等方面注意如何不让这4个必要条件成立,如何确定资源的合理分配算法,避免进程永久占据系统资源。此外,也要防止进程在处于等待状态的情况下占用资源,在系统运行过程中,对进程发出的每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,若分配后系统可能发生死锁,则不予分配,否则予以分配。因此,对资源的分配要给予合理的规划。
根据产生死锁的4个必要条件,只要使其中之一不能成立,死锁就不会出现。为此,可以采取下列3种预防措施:● 采用资源静态分配策略,破坏“部分分配”条件。
● 允许进程剥夺使用其他进程占有的资源,从而破坏“不可剥夺”条件。● 采用资源有序分配法,破坏“环路”条件。
这里注意一点:互斥条件无法被破坏。死锁的避免不严格地限制死锁的必要条件的存在,而是系统在系统运行过程中小心地避免死锁的最终发生。避免死锁算法中最有代表性的算法是Dijkstra E.W于1968年提出的银行家算法,该算法需要检查申请者对资源的最大需求量,如果系统现存的各类资源可以满足申请者的请求,就满足申请者的请求。这样申请者就可很快完成其计算,然后释放它占用的资源,从而保证了系统中的所有进程都能完成,所以可避免死锁的发生。
2016-06-21
进程在运行中不断地改变其运行状态。通常,一个运行进程必须具有以下三种基本状态。1>就绪(Ready)状态:当进程已分配到除CPU以外的所有必要的资源,只要获得处理机便可立即执行,这时的进程状态称为就绪状态。2>执行(Running)状态:当进程已获得处理机,其程序正在处理机上执行,此时的进程状态称为执行状态。3>阻塞(Blocked)状态:正在执行的进程,由于等待某个事件发生而无法执行时,便放弃处理机而处于阻塞状态。引起进程阻塞的事件可以有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。
16.2 线程
2016-06-21
进程是程序的一次执行。线程可以理解为进程中执行的一段程序片段。在一个多任务环境中下面的概念可以帮助我们理解两者间的差别。
进程间是独立的,这表现在内存空间、上下文环境上;线程运行在进程空间内。一般来讲(不使用特殊技术),进程无法突破进程边界存取其他进程内的存储空间;而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。同一进程中的两段代码不能够同时执行,除非引入线程。
线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除。线程占用的资源要少于进程所占用的资源。进程和线程都可以有优先级。进程间可以通过IPC通信,但线程不可以。
2016-06-21
PE文件被称为可移植的执行体是Portable Execute的全称,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)
2016-06-21
目前以lib为后缀的库有两种,一种为静态链接库(Static Libary),另一种为动态链接库(DLL)的导入库(Import Libary,以下简称“导入库”)。虽然静态链接库和动态库的导入库都是.lib文件,但是区别很大,它们实质是不一样的东西。静态库本身就包含了实际执行代码、地址符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
静态链接库是一个或者多个obj文件的打包,所以有人干脆把从obj文件生成lib的过程称为Archive,即合并到一起。比如你链接一个静态库,如果其中有错,它会准确地找到是哪个obj有错,即静态lib只是壳子。当我们的应用工程在使用静态链接库的时候,静态链接库要参与编译,在生成执行文件之前的链接过程中,将静态链接库的全部指令直接链接入可执行文件中,故而,在可执行文件生成以后,静态链接库.lib文件即可以弃之不用。动态链接库(dll)是作为共享函数库的可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个.dll文件中,该dll包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。dll还有助于共享数据和资源。多个应用程序可同时访问内存中单个dll副本的内容。使用动态链接代替静态链接有若干优点。dll节省内存,减少交换操作,节省磁盘空间,更易于升级(不需要重链接和重编译),提供售后支持,提供扩展MFC库类的机制,支持多语言程序。
静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,lib中的指令都全部被直接包含在最终生成的exe文件中了。但是若使用dll(即动态链接库),该dll不必被包含在最终exe文件中,exe文件执行时可以“动态”地引用和卸载这个与exe独立的dll文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。动态链接库与静态链接库的使用不同之处在于它允许可执行模块(.dll文件或.exe文件)仅包含在运行时定位dll函数的可执行代码所需的信息。在静态链接库的使用中,链接器从静态链接库获取所有被引用的函数,并将库同代码一起放到可执行文件中。
16.3 内存管理
2016-06-21
Windows内存管理方式主要分为:页式管理、段式管理、段页式管理。
页式管理的基本原理是将各进程的虚拟空间划分成若干个长度相等的页(page);页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应的页表;并用相应的硬件地址变换机构来解决离散地址变换问题。页式管理采用请求调页或预调页技术来实现内外存存储器的统一管理。其优点是没有外碎片,每个内碎片不超过页的大小。缺点:程序全部装入内存,要求有相应的硬件支持。例如地址变换机构缺页中断的产生和选择淘汰页面等都要求有相应的硬件支持。这增加了机器成本,增加了系统开销。段式管理的基本思想就是把程序按内容或过程函数关系分成段,每段有自己的名字。一个用户作业或进程所包含的段对应一个二维线形虚拟空间,也就是一个二维虚拟存储器。段式管理程序以段为单位分配内存 然后通过地址影射机构把段式虚拟地址转换为实际内存物理地址。其优点是可以分别编写和编译,可以针对不同类型的段采取不同的保护,可以按段为单位来进行共享,包括通过动态链接进行代码共享。缺点是会产生碎片。
段页式管理:为了实现段页式管理,系统必须为每个作业或进程建立一张段表以管理内存分配与释放、缺段处理等。另外由于一个段又被划分成了若干页。每个段又必须建立一张页表以把段中的虚页变换成内存中的实际页面。显然与页式管理时相同,页表中也要有相应的实现缺页中断处理和页面保护等功能的表项。段页式管理是段式管理与页式管理方案结合而成的 所以具有它们两者的优点。但反过来说,由于管理软件的增加,复杂性和开销也就随之增加了。另外需要的硬件以及占用的内存也有所增加。使得执行速度下降。
2016-06-21
银行家算法是用来避免死锁的,该方法将系统的状态分为安全状态和不安全状态,只要使系统处于安全状态,便可避免死锁的发生。
17.1 数据库理论
2016-06-21
数据库模式的4个范式问题。
1NF:第一范式。如果关系模式R的所有属性的值域中每一个值都是不可再分解的值,则称R属于第一范式模式。如果某个数据库模式都是第一范式的,则称该数据库模式属于第一范式的数据库模式。第一范式的模式要求属性值不可再分裂成更小部分,即属性项不能是属性组合或由组属性组成。
2NF:第二范式。如果关系模式R为第一范式,并且R中每一个非主属性完全函数依赖于R的某个候选键,则称R为第二范式模式。如果某个数据库模式中每个关系模式都是第二范式的,则称该数据库模式属于第二范式的数据库模式。(注:如果A是关系模式R的候选键的一个属性,则称A是R的主属性,否则称A是R的非主属性。)3NF:第三范式。如果关系模式R是第二范式,且每个非主属性都不传递依赖于R的候选键,则称R是第三范式的模式。如果某个数据库模式中的每个关系模式都是第三范式,则称R为3NF的数据库模式。
BCNF:BC范式。如果关系模式R是第一范式,且每个属性都不传递依赖于R的候选键,那么称R为BCNF的模式。4NF:第四范式。设R是一个关系模式,D是R上的多值依赖集合。如果D中成立非平凡多值依赖X→→Y时,X必是R的超键,那么称R是第四范式的模式。
2016-06-21
存储过程是用户定义的一系列SQL语句的集合,涉及特定表或其他对象的任务,用户可以调用存储过程。而函数通常是数据库已定义的方法,它接收参数并返回某种类型的值,并且不涉及特定用户表。
2016-06-21
数据库事务是指作为单个逻辑工作单元执行的一系列操作,这些操作要么全做要么全不做,是一个不可分割的工作单位。
事务的开始与结束可以由用户显式控制。如果用户没有显式地定义事务,则由DBMS按默认规定自动划分事务。事务具有原子性、一致性、独立性及持久性等特点。● 事务的原子性是指一个事务要么全部执行,要么不执行。也就是说一个事务不可能只执行了一半就停止了。比如你从银行取钱,这个事务可以分成两个步骤(1)存折减款,(2)拿到现金。不可能存折钱少了,而钱却没拿出来。这两步必须同时完成,要么就都不完成。
● 事务的一致性是指事务的运行并不改变数据库中数据的一致性。例如,完整性约束了a+b=10,一个事务改变了a,那么b也应该随之改变。● 事务的独立性是指两个以上的事务不会出现交错执行的状态。因为这样可能会导致数据不一致。● 事务的持久性是指事务运行成功以后,就系统的更新是永久的。不会无缘无故的回滚。
2016-06-21
游标用于定位结果集的行。通过判断全局变量@@FETCH_ STATUS可以判断其是否到了最后。通常此变量不等于0表示出错或到了最后。
2016-06-21
事前触发器运行于触发事件发生之前,而事后触发器运行于触发事件发生之后。语句级触发器可以在语句执行前或后执行,而行级触发在触发器所影响的每一行触发一次。
2016-06-21
所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串中,欺骗服务器执行恶意的SQL命令。在某些表单中,用户输入的内容直接用来构造(或者影响)动态SQL命令,或作为存储过程的输入参数,这类表单特别容易受到SQL注入式攻击。
防范SQL注入式攻击闯入并不是一件特别困难的事情,只要在利用表单输入的内容构造SQL命令之前,把所有输入内容过滤一番就可以了。过滤输入内容可以按多种方式进行。● 替换单引号,即把所有单独出现的单引号改成两个单引号,防止攻击者修改SQL命令的含义。
● 删除用户输入内容中的所有连字符,防止攻击者顺利获得访问权限。● 对于用来执行查询的数据库账户,限制其权限。用不同的用户账户执行查询、插入、更新、删除操作。由于隔离了不同账户可执行的操作,因而也就防止了原本用于执行SELECT命令的地方却被用于执行INSERT、UPDATE或DELETE命令。● 用存储过程来执行所有的查询。SQL参数的传递方式将防止攻击者利用单引号和连字符实施攻击。此外,它还使得数据库权限可以被限制到只允许特定的存储过程执行,所有的用户输入必须遵从被调用的存储过程的安全上下文,这样就很难再发生注入式攻击了。
● 检查用户输入的合法性,确信输入的内容只包含合法的数据。数据检查应当在客户端和服务器端都执行。之所以要执行服务器端验证,是为了弥补客户端验证机制脆弱的安全性。在客户端,攻击者完全有可能获得网页的源代码,修改验证合法性的脚本(或者直接删除脚本),然后将非法内容通过修改后的表单提交给服务器。因此,要保证验证操作确实已经执行,唯一的办法就是在服务器端也执行验证。● 将用户登录名称、密码等数据加密保存。加密用户输入的数据,然后再将它与数据库中保存的数据比较,这相当于对用户输入的数据进行了“消毒”处理。用户输入的数据不再对数据库有任何特殊的意义,从而也就防止了攻击者注入SQL命令。
● 检查提取数据的查询所返回的记录数量。如果程序只要求返回一个记录,但实际返回的记录却超过一行,那就当做出错处理。
17.2 SQL语言
2016-06-21
临时表要在表名前面加“#”。
17.4 SQL语言主观题
2016-06-21
sp_renamedb命令可以更改数据库的名称。
语法如下:参数:
是数据库的当前名称。old_name为sysname类型,无默认值。是数据库的新名称。new_name必须遵循标识符规则。new_name为sysname类型,无默认值。
返回代码值:0(成功)或非零数字(失败)权限:
只有sysadmin和dbcreator固定服务器角色的成员才能执行sp_renamedb。答案:sp_renamedb命令。
18.1 网络结构
2016-06-21
OSI参考模型有7层,其分层原则如下:
● 根据不同层次的抽象分层。● 每层应当有一个定义明确的功能。
● 每层功能的选择应该有助于制定网络协议的国际标准。● 各层边界的选择应尽量节省跨过接口的通信量。
● 层数应足够多,以避免不同的功能混杂在同一层中,但也不能太多,否则体系结构会过于庞大。根据以上标准,OSI参考模型分为物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。
物理层涉及在信道上传输的原始比特流。数据链路层的主要任务是加强物理层传输原始比特流的功能,使之对应的网络层显现为一条无错线路。发送包把输入数据封装在数据帧,按顺序传送出去并处理接收方回送的确认帧。
网络层关系到子网的运行控制,其中一个关键问题是确认从源端到目的端如何选择路由。传输层的基本功能是从会话层接收数据而且把其分成较小的单元传递给网络层。
会话层允许不同机器上的用户建立会话关系。表示层用来完成某些特定的功能。
应用层包含着大量人们普遍需要的协议。
18.2 网络协议问题
2016-06-21
TCP是传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发、丢弃重复数据、检验数据、流量控制等功能,保证数据能从一端传到另一端。
UDP是用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快。
2016-06-21
服务器端程序编写:
(1)调用ServerSocket(int port)创建一个服务器端套接字,并绑定到指定端口上。(2)调用accept(),监听连接请求,则接收连接,返回通信套接字。
(3)调用Socket类的getOutStream()和getInputStream获取输出流和输入流,开始网络数据的发送和接收。(4)关闭通信套接字.Socket.close()。
客户端程序编写:(1)调用Socket()创建一个流套接字,并连接到服务器端。
(2)调用Socket类的getOutputStream()和fetInputStream获取输出流和输入流,开始网络数据的发送和接收。(3)关闭通信套接字.Socket.close()。
18.3 网络安全问题
2016-06-21
防火墙的优点:它能增强机构内部网络的安全性,用于加强网络间的访问控制,防止外部用户非法使用内部网的资源,保护内部网络的设备不被破坏,防止内部网络的敏感数据被窃取。防火墙系统决定了哪些内部服务可以被外界访问;外界的哪些人可以访问内部的哪些服务,以及哪些外部服务可以被内部人员访问。
防火墙的缺点:对于发生在内网的攻击无能为力;对于部分攻击,可以绕过防火墙,防火墙发现不了;防火墙的策略是静态的,不能实施动态防御;等等。入侵检测的优势:入侵监测系统扫描当前网络的活动,监视和记录网络的流量,根据定义好的规则来过滤从主机网卡到网线上的流量,提供实时报警。大多数的入侵监测系统可以提供关于网络流量非常详尽的分析。它们可以监视任何定义好的流量。很多系统对FTP、HTTP和Telnet流量都有默认的设置,还有其他的流量,如NetBus、本地和远程登录失败,等等。也可以自己定制策略。如果定义了策略和规则,便可以获得FTP、SMTP、Telnet和任何其他的流量。这种规则有助于追查该连接和确定网络上发生过什么,以及现在正在发生什么。这些程序在需要确定网络中策略实施的一致性情况时是非常有效的工具。
入侵检测的缺点:目前入侵检测技术的方法主要停留在异常检测统计方法和误用检测方法上,这两种方法都还存在这样或那样的问题。网络入侵技术在不断地发展,入侵的行为表现出不确定性、多样性等特点。网络应用的发展又带来新的安全问题。如高速网络技术出现流量大的特点,那么基于网络的入侵检测系统如何适应这种情况?基于主机审计数据怎样做到既减少数据量,又能有效地检测到入侵?入侵检测研究领域急需其他学科知识提供新的入侵检测解决方法。入侵检测只是仅仅试图发现计算机网络中的安全问题,要解决网络安全的问题还需要其他的网络安全技术。另外,入侵检测系统本身还存在安全问题。入侵检测系统也可能会受到攻击。终上所述,其实防火墙和入侵检测各有优劣。打个比方,防火墙就相当于一栋大楼外的门卫系统,而入侵检测就相当于大楼内的监控系统,两者缺一不可。应该将入侵检测系统与防火墙联动起来,当入侵检测系统发现到有入侵行为时,应及时报告防火墙,以阻断入侵。
2016-06-22
在网络技术中,端口(Port)大致有两种意思:一是物理意义上的端口,比如,ADSL MODEM、集线器、交换机、路由器用于连接其他网络设备的接口,如RJ-45端口、SC端口,等等;二是逻辑意义上的端口,一般是指TCP/IP协议中的端口,端口号的范围为0~65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口,等等。我们这里将要介绍的就是逻辑意义上的端口。
逻辑意义上的端口有多种分类标准,下面将介绍两种常见的分类。1)按端口号分布划分
①知名端口(Well-Known Ports)知名端口即众所周知的端口号,范围为0~1023,这些端口号一般固定分配给一些服务。比如21端口分配给FTP服务,25端口分配给SMTP(简单邮件传输协议)服务,80端口分配给HTTP服务,135端口分配给RPC(远程过程调用)服务,等等。
②动态端口(Dynamic Ports)动态端口的范围为1024~65535,这些端口号一般不固定分配给某个服务,也就是说许多服务都可以使用这些端口。只要运行的程序向系统提出访问网络的申请,那么系统就可以从这些端口号中分配一个供该程序使用。比如1024端口就是分配给第一个向系统发出申请的程序。在关闭程序进程后,就会释放所占用的端口号。
不过,动态端口也常常被病毒木马程序所利用,如冰河默认连接端口是7626,WAY 2.4是8011,Netspy 3.0是7306,YAI病毒是1024,等等。2)按协议类型划分
按协议类型划分,可以分为TCP、UDP、IP和ICMP(Internet控制消息协议)等端口。下面主要介绍TCP和UDP端口。①TCP端口
TCP端口,即传输控制协议端口,需要在客户端和服务器之间建立连接,这样可以提供可靠的数据传输。常见的包括FTP服务的21端口,Telnet服务的23端口,SMTP服务的25端口,以及HTTP服务的80端口,等等。②UDP端口
UDP端口,即用户数据包协议端口,无须在客户端和服务器之间建立连接,安全性得不到保障。常见的有DNS服务的53端口,SNMP(简单网络管理协议)服务的161端口,QQ使用的8000和4000端口,等等。
21.1 数字规律类题目
2016-06-22
数字规律题目一般包含以下10种情况:
(1)等差关系类;(2)等比关系类;(3)前项求和/差关系类;
(4)前项求积/商关系类;(5)隔项规律类;
(6)分组规律类;(7)平方规律类;
(8)质数规律;(9)整数+小数类;
(10)组合类。
21.4 应用数学类题目
2016-06-22
应用数学类题目包括如下几类:1.比例问题,2.路程问题,3.工程问题,4.植树问题,5.解方程问题,6.排列组合问题,7.利润问题,8.概率问题。