1、程序员面试-5 及答案解析(总分:100.00,做题时间:90 分钟)一、论述题(总题数:28,分数:100.00)1.C+中的多态种类有哪几种 (分数:3.00)_2.什么函数不能声明为虚函数 (分数:3.00)_3.是否可以把每个函数都声明为虚函数 (分数:3.00)_4.C+中如何阻止一个类被实例化 (分数:3.00)_5.当 while()的循环条件是赋值语句时会出现什么情况 (分数:3.00)_6.不使用 if/:?/switch 及其他判断语句如何找出两个 int 型变量中的最大值和最小值 (分数:3.00)_7.C 语言获取文件大小的函数是什么 (分数:3.00)_8.表达式 a
2、bc 是什么意思 (分数:3.00)_9.如何打印自身代码 (分数:3.00)_10.如何实现一个最简单病毒 (分数:3.00)_11.如何只使用一条语句实现 x 是否为 2 的若干次幂的判断 (分数:3.00)_12.如何定义一对相互引用的结构 (分数:3.00)_13.什么是逗号表达式 (分数:4.00)_14./n 是否与/n/r 等价 (分数:4.00)_15.什么是短路求值 (分数:4.00)_16.已知随机数函数 rand7(),如何构造 rand10()函数 (分数:4.00)_17.printf(“%p/n“,(void*)x)与 printf(“%p/n“, class Ba
3、se public: Base() f(); virtual void f() cout“base“endl; ; class Derived:public Base public: Derived(); void f() cout“Derived“endl; ; int main() Base*p=new Derived; p-f(); return 0; 程序输出结果: base Derived base Derived 上例中,Base*p=new Derived,基类的指针生成了一个派生类对象,将会隐式调用 base 的构造函数,尽管对象是 Derived,但构造基类部分时,还只是个
4、Base,所以会调用基类的虚函数 f()。 析构函数可以是虚函数,而且有的时候是必需的,基类指针指向派生类,用基类指针 delete 时,如果不定义成虚函数,派生类中派生的那部分无法析构。 析构函数执行时先调用派生类的析构函数,然后才调用基类的析构函数。如果析构函数不是虚函数,而程序执行时又要通过基类的指针去销毁派生类的动态对象,那么用 delete 销毁对象时,只调用了基类的析构函数,未调用派生类的析构函数。这样会造成销毁对象不完全。程序示例如下: #ineludeiostream using namespace std; class CPerson public: virtual CPer
5、son(); protected: char * m_lpszName; char * m_lpszSex; ; class CStudent:public CPerson public: CStudent(); protected: int m_iNumjber; ; CPerson:CPerson() cout“CPerson!“endl; CStudent:CStudent() cout“CStudent!“endl; int main() CPerson * poCPerson=new CStudent; if(NULL=poCPerson) exit(0); delete poCPe
6、rson; cout“CStudent 对象已经完成析构“endl; CStudent oCSmdent; return 0; 程序输出结果: CStudent! CPerson! CStudent 对象已经完成析构 CStudent! CPerson!3.是否可以把每个函数都声明为虚函数 (分数:3.00)_正确答案:()解析:虽然虚函数很有效,但是不可以把每个函数都声明为虚函数。因为使用虚函数是要付出代价的。由于每个虚函数的对象在内存中都必须维护一个虚函数表,因此在使用虚函数时,尽管带来了使用的方便,却会额外产生一个系统开销。如果仅是一个很小的类,且不想派生其他类,那么根本没有必要使用虚函
7、数。4.C+中如何阻止一个类被实例化 (分数:3.00)_正确答案:()解析:C+中可以通过使用抽象类,或者将构造函数声明为 private 阻止一个类被实例化。抽象类之所以不能被实例化,是因为抽象类不能代表一类具体的事物,它是对多种具有相似性的具体事物的共同特征的一种抽象。例如,声明一个抽象类车,但是却不能用这个类来创造某个具体的事物来,只能派生一个汽车,才可以产生出来。 引申: 1)一般在什么时候将构造函数声明为 private? 例如,要阻止编译器生成默认的复制构造函数的时候。 2)什么时候编译器会生成默认的复制构造函数? 只要自己没写,而程序需要,都会生成。 3)如果已经写了一个构造函
8、数,编译器还会生成复制构造函数吗? 会。5.当 while()的循环条件是赋值语句时会出现什么情况 (分数:3.00)_正确答案:()解析:在 while()的循环条件里面定义一个变量并赋值为 0,程序代码如下: while(int i=0) print if(“%d/n“,i); i-; 以上代码不执行任何动作,相当于执行了 while(0)操作,循环结束,while 循环体不执行。而在 while 的循环条件里面定义一个变量并赋值为非 0 时,相当于执行了 while(1),程序进入无限循环。 while(int i=1) printf(“%d/n“,i); i-; 需要注意的是,上述代码
9、之所以不停地输出为 1,而不是执行 i-,是因为在 while 循环条件里,重新定义了一个局部变量 i,对其进行了重新赋值,所以 i 的初始值一直为 1,而不是自减。6.不使用 if/:?/switch 及其他判断语句如何找出两个 int 型变量中的最大值和最小值 (分数:3.00)_正确答案:()解析:寻找两个变量中的较大值,一般可以用判断型语句进行比较,如 if(ab),按照题目中的要求,不能采用判断语句,所以可以利用绝对值的方法或是移位的方法。 方法一: int max=(a+b)+abs(a-b)/2 如果 ab,则 max=a;如果 ab,则 max=b。 int min=(a+b)
10、-abs(a-b)/2 如果 ab,则 min=b;如果 ab,则 min=a。 上例中,abs 是 C 语言中的用于求绝对值的函数,C 语言中还有一个类似的函数,函数名为 fabs。 方法二:对变量的差值进行移位操作,通过其是否为非 0 值确定两个变量的大小。 #includestdio.h int main() int a=1,b=4; int c=a-b; int MAX=(unsigned)c(sizeof(int)*8-1); if(!MAX) printf(“%d/n“,a); else printf(“%d/n“,b); return(0) 程序的输出结果: 4 方法三:通过移位
11、操作来判断。程序示例如下: void compare(int a,int b) static char* op=“=“,“,“; int i=(unsigned(a-b)31)+(unsigned(b-a)31)*2; printf(“%d%s%d/n“,a,opi,b); 上例中,满足: 1)如果 ab,那么(unsigned(a-b)31)=0,(unsigned(b-a)31)*2=2。 2)如果 a=b,那么(unsigned(a-b)31)=0,(unsigned(b-a)31)*2=0。 3)如果 ab,那么(unsigned(a-b)31)=1,(unsigned(b-a)31)
12、*2=0。 方法四:通过加减运算与移位运算结合的方式实现。 void compare3(int a,int b) int min=a+(b-a)31) int max=a-(a-b)31) coutminendl; coutmaxendl; 注意:正数的补码和原码相同。负数的补码是正数的补码按位取反,末位加 1。7.C 语言获取文件大小的函数是什么 (分数:3.00)_正确答案:()解析:某些标识符是预定义的,扩展后将生成特定的信息,它们同预处理器表达式运算符#define 一样,不能取消定义或重新定义。预定义标识符见下表。 预定义标识符 函数描述_FILE_ 包含当前程序文件名的字符串,包含
13、了详细路径,如G:/program/study/c+/test1.c _LINE_ 包含当前源文件行数的十进制常量_包含DATE_ 编译日期的字符串字面值_STDC_ 如果编译器遵循ANSI C标准,它就是个非零值_TIME_ 包含编译时间的字符串字面值_FUNC_(在有些编译系统中为_FUNCTION_) 当前所在函数名,在编译器的较高版本中支持其中,标识符_LINE_和_FILE_通常用来调试程序,标识符_DATE_和_TIME_通常用来在边以后的程序中加入一个时间标志,以区分程序的不同版本。当要求程序严格遵循 ANSIC 标准时,标识符_sTDC_就会被赋值为 1。8.表达式 abc 是
14、什么意思 (分数:3.00)_正确答案:()解析:在弄清这个问题前,先看如下代码: #includestdio.h int main() int a=5,b=4,c=3; printf(“%d/n“,abc); return 0; 程序输出结果: 0 在上例中,abc 到底是如何执行的呢?对于这种连续运算,根据优先级,首先进行 ab 的比较判断,本例中 ab 为真,所以返回值为 1,接着比较该返回值与 c 的大小。因为 c 的值为 3,1c 表达式为假,所以返回值为 0。最终的输出为 0。 对于赋值运算符,结果又如何呢?以如下程序为例。 #includestdio.h int main() i
15、nt b,c; int a=(b=(c=020) printf(“%d%d%d/n“,a,b,c); return 0; 程序输出结果: 0 0 16 在赋值语句中,c=020,因为以 0 开头的数字一般表示的都是八进制的数值,所以折合成十进制的数为16。根据优先级关系,b 的值为(c=020)%c printf(p,10,10,10,34,p,34,10,10,10);%c return 0;%c“; printf(p,10,10,10,34,p,34,10,10,10); return 0; 上面代码的实现需要注意以下几个方面的内容: 1)写好一个程序。 2)定义一个字符串 str 把原来
16、的代码抄进去;不能显示的字符和特殊字符都用%c 替换,如换行、引号等。用一个输出语句 printf 打印 str。注意这里,格式控制时,10 表示换行,34 表示”,92 表示/,110 表示 n,9 表示/t。10.如何实现一个最简单病毒 (分数:3.00)_正确答案:()解析:可以把最简单的病毒理解为一个无限运行的恶意程序,无限运行可以通过无限循环实现,而恶意可以通过申请内存空间来实现,所以可以用如下代码来实现一个最简单的病毒。 while(1) int *p=new int 10000000; 该代码首先新建一个无限循环,然后在循环内执行一个内存申请操作,最终系统内存会被该程序占用完,导
17、致系统出现宕机的情况。 引申:嵌入式系统中经常要用到无限循环,怎样用 C 语言编写无限循环? 有多种执行无限循环的方式,第一种是使用 while,将 while 条件置为 1,具体使用方式如下: while(1) 除了使用 while 以外,也可以使用 for 循环,for 语句为空,因为没有循环区间,没有循环条件,也没有条件语句,所以能够执行无限循环。具体使用方式如下: for(;) 使用 for 循环的上述方式表示无限循环时,也可以将循环条件设置为 1。方式如下: for(;1;) 除了以上两种方法以外,还有一种不推荐使用的方法使用 goto 语句,程序示例如下: Loop: . goto
18、 Loop; goto 语句一般不推荐使用,但是在必要的嵌入式环境下也不失为一种不错的解决方案。11.如何只使用一条语句实现 x 是否为 2 的若干次幂的判断 (分数:3.00)_正确答案:()解析:如果一个数是 2 的若干次幂,那么其二进制表示中最高位为 1,其他位为 0,该数减去 1 之后的数的二进制表示为全 1,所以将两数进行与操作,判断其最终结果是否为 0,可以只用一语句实现判断该数是否是 2 的若干次幂的功能。 程序示例如下: int i=512; coutboolalpha(i12.如何定义一对相互引用的结构 (分数:3.00)_正确答案:()解析:用常规的定义相互引用的结构一般容
19、易出现类似于死锁一样的问题。例如,A 引用 B 的成员,B 也引用 A 的成员,由于 C 语言需要先声明后使用,所以常规的方法会引起编译器错误。 程序示例如下: typedef struct int afield; BPER bpointer; *APTR; typedef struct int bfield; APTR apointer; *BPTR; 上例中 BPER 没有被定义就已经被使用了,所以错误,需要采取其他的方式来实现。指针就是一种不错的方式,以下方式都可以实现定义一对相互引用的结构。 struct b;此处使用结构体的不完整声明 struct a int afield; str
20、uct b*bpointer;此处结构体虽然未定义,但是编译器却可以接受 ; struct b int bfield; struct a* apointer; ; 需要注意一个区别:结构体的自引用(self reference)就是在结构体内部,包含指向自身类型结构体的指针。结构体的相互引用(mutual reference)即在多个结构体中,都包含指向其他结构体的指针。 struct a int b; int c; char d; struct a z; struct a *p; 上述结构有问题,因为结构中不能定义结构本身的非指针变量,如果编译器支持则会导致无限嵌套,因此一般编译器都会认为
21、struct a 是未定义的类型,即使提前声明也不会有任何用处。13.什么是逗号表达式 (分数:4.00)_正确答案:()解析:关于二维数组赋值的一个陷阱: int a32=(0,1),(2,3),(4,5); int *p; p=a0; printf(“%d/n“,p0) 程序的输出结果: 1 程序示例如下: int a32=0,1,2,3),4,5); int *p; p=a0; printf(“%d/n“,p0); 程序的输出结果: 0 上述两段代码很类似,为什么输出却有这么大的不同?两者的区别在于二维数组的初始化问题,第一种情况由于是逗号表达式,所以数组的元素等价于1,3,5,0),0
22、,0,与第二种情况不一样。 C 语言提供一种特殊的运算符逗号运算符,优先级别最低,它将两式连接起来。例如,(3+4,2+8)称为逗号表达式,其求解过程是先求表达式 1 的运算结果,然后求解表达式 2 的结果,而整个表达式的值为表达式 2 的值,如(3+4,2+8)的值是 10。(a=3*5,a*4)的值为 60。 在使用逗号表达式时,首先需要弄清楚其表示形式,其表示形式如下: 表达式 1,表达式 2,表达式 3表达式 n 使用逗号表达式,需要注意以下 3 个方面的事项: 1)逗号表达式的运算过程为从左往右逐个计算表达式。 2)逗号表达式作为一个整体,它的值为最后一个表达式(也即表达式 n)的值
23、。 3)逗号运算符的优先级别在所有运算符中最低。 例如,语句 i=1,2;虽然是逗号表达式,但是赋值符的优先级更高,所以 i 的值为 1,接着执行常量 2 的运算,运算结果会被丢弃。在编译器中,虽然这种语句的写法是合法的,但是是不提倡。再例如,语句(a=3*5,a*4)和语句 b=(a=3*5,a*4)意义不一样,第一个语句中 a 的值为 15,第二个语句 a 的值为 15,但b 的值为 60。 引申:int i=(j=4,k=8,1=16,m=32),则 i 的值是多少? 输出结果为 32。当一个语句是由多个被逗号运算符隔开的表达式组成时,此语句的值为最后一个表达式的值。 首先看一个例子,i
24、nt i=(j=4),它等价于 i=j=4 语句,即 int j=4 与 int i=i。而inti=(j=4,1(=8,l=16,m=32)则等价于:int j=4,k=8,l=16,m=32,i=m=32,所以输出为 32。例如,int a=3,则执行 a+=a-a+=a*a 运算后,a 的值变为 0,因为这个连续运算语句可以转换为以下 3 个语句:首先计算 a+=a*a,把 a 的值 3 带入,a+=3+3*3,则 a 的值变为 12,然后计算 a-=a,此时 a 的值变为 0,最后执行 a+=a 操作,a 的值最终变为 0。14./n 是否与/n/r 等价 (分数:4.00)_正确答案
25、:()解析:换行(/n)就是光标下移一行却不会移到这一行的开头,回车(/r)就是回到当前行的开头却不向下移一行。 按(Enter)键后会执行“/n/r“,这样就是看到的一般意义的回车了,所以在用十六进制文件查看方式看一个文本,就会在行尾发现“/n/r“。 Tab 是制表符,就是“/t“,作用是预留 8 个字符的显示宽度,用于对齐。 引申:“/n“与“/n“是否有区别? 两者存在区别,“/n“是一个字符串,该字符串以“/0“结束,即它实际包含了两个字符,而“/n“只是一个简单的字符而已,所以两者不相等。15.什么是短路求值 (分数:4.00)_正确答案:()解析:短路求值是常见的计算机问题,所为
26、短路求值即对于(条件 1 if(i0(j+)0) ; printf(“%d/n“,j); return 0; 程序输出结果: 1 输出为什么不是 2 而是 1 呢?其实,这里就涉及一个短路计算的问题。由于 if 语句是一个条件判断语句,里面是有两个简单语句进行或运算组合的复合语句,因为或运算中,只要参与或运算的两个表达式的值都为真,则整个运算结果为真,而由于变量 i 的值为 6,已经大于 0 了,而该语句已经为 true,则不需要执行后续的 j+操作来判断真假,所以后续的 j+操作不执行,j 的值仍然为 1。 程序示例如下: #includestdio.h int main() int a=5
27、,b=6,c=7,d=8,m=2,n=2; (m=ab) printf(“%d/n“,n); return 0; 程序的输出结果: 2 因为短路计算的问题,对于 int rand7() return rand()%7+1; int rand10() int x=0; do x=(rand7()-1)*7+rand7(); while(x40); return x%10+1; int main() srand(unsigned(time(0); for(int i=0;i!=10;+i) coutrand10()“; coutendl; return 0; 程序输出结果: 9 7 10 8 7
28、8 9 4 5 817.printf(“%p/n“,(void*)x)与 printf(“%p/n“,语句打印 x 被转为指针的地址,就是它的值。printf(“%p/n“,将打印变量 x 的地址。 当整型变量 x 的值为 0x87654321 时,两者的输出分别为 87654321 0012FF6018.printf()函数是否有返回值 (分数:4.00)_正确答案:()解析:有。printf()函数的一般形式为 int printf(const char* format,argument),它返回一个 int值,表示被打印的字符数。 程序示例如下: #includeiostream usi
29、ng namespace std; int main() int i=4321; printf(“%d/n“,printf(“%d/n“,printf(“%d/n“,i); return 0; 程序输出结果如下: 4321 5 2 程序之所以首先输出为 4321,是因为 printf 打印的目标为整型变量 i 的值,由于 i 的值包含 4 个字符数,而“/n“占据 1 个字符,所以一共占据了 5 个字符,所以第二行打印为 5,5 与“/n“合计为两个字符,所以第三行输出为 2。19.不能使用任何变量,如何实现计算字符串长度函数 strlen() (分数:4.00)_正确答案:()解析:递归是一
30、种自己调用自己的方式,有点像死循环的嵌套。如果题目没有要求,最容易想到的方法是使用一个 for 循环,对字符串进行遍历,当遇到“/0“结束,最终记录字符串的长度,程序示例如下: #includestdio.h int Strlen(const char *str)/*使用了一个 int 型变量 len*/ int len:0; if(str=NULL) return 0; for(;*str+!=“/0“;) len+; return len; int main() printf(“%d/n“,Strlen(“amc“); return 0; 程序的输出结果: 3 上例中,使用了临时变量,因为
31、题目要求不能使用任何变量,所以上述方法不可取,可以采用递归的思想来实现。程序示例如下: #includestdio.h int Strlen(const char* s) if(*s!=“/0“) return 1+Strlen(+s); else return 0; int main() printf(“%d/n“,Strlen(“amc“); return 0; 程序的输出结果: 3 还可以将上述递归方式简化为如下所示的表现形式: int Strlen(const char* s) return *s=“/0“?0:(1+Strlen(+s); 20.负数除法与正数除法的运算原理是否一样
32、(分数:4.00)_正确答案:()解析:在 C 语言中,负数除法运算与正数除法运算不一样,主要遵循以下规则:除号的正负取舍和一般的算数一样,符号相同为正,相异为负,求余符号的正负取舍和被除数符号相同。 以-3/16,16/-3;-3%16,16%-3 为例分析,-3/16=0,16/-3=-5。-3%16=-3,16%-3=1。21.main()主函数执行完毕后,是否可能会再执行一段代码 (分数:4.00)_正确答案:()解析:可以。例如,可以用 onexit 注册一个函数,它会在 main 之后执行 int fn(void)。需要注意的是,使用 onexit()函数需要添加头文件 stdli
33、b.h,否则会报编译错误。 程序示例如下: #includestdio.h #includestdlib.h int fn() printf(“next/n“); return 0; int main() _onexit(fn); printf(“This is executed first/n“); return 0; 程序输出结果: This is executed first next 需要注意的是,在 C 语言中,用户代码也可以调用 main()函数,程序示例如下: #includestdio.h #includestdlib.h void main(); void test() ma
34、in(); void main() printf(“test“); test(); 上例中,程序运行一段时间会中断,递归却没有终止。22.进程与线程有什么区别 (分数:4.00)_正确答案:()解析:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,它是系统进行资源分配和调度的一个独立单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O 设备等,然后该进程被放入到进程的就绪队列,进程调度程序选中它,为它分配 CPU 及其他相关资源,该进程就被运行起来。 线程是进程的一个实体,是 CPU 调度和分配的基本单位,线程自己基本上不拥
35、有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器、一组寄存器和栈),但是它可以与同属一个进程的其他的线程共享进程所拥有的全部资源。 在没有实现线程的操作系统中,进程既是资源分配的基本单位,又是调度的基本单位,它是系统中并发执行的单元。而在实现了线程的操作系统中,进程是资源分配的基本单位,但线程是调度的基本单位,是系统中并发执行的单元。 引入线程主要有以下 4 个方面的优点: 1)易于调度。 2)提高并发性。通过线程可以方便有效地实现并发。 3)开销小。创建线程比创建进程要快,所需要的开销也更少。 4)有利于发挥多处理器的功能。通过创建多线程,每个线程都在一个处理器上运行,从而实现应用
36、程序的并行,使每个处理器都得到充分运行。 需要注意的是,尽管线程与进程很相似,但两者也存在着很大的不同,区别如下: 1)一个线程必定属于也只能属于一个进程;而一个进程可以拥有多个线程并且至少拥有一个线程。 2)属于一个进程的所有线程共享该线程的所有资源,包括打开的文件、创建的 Socket 等。不同的进程互相独立。 3)线程又被称为轻量级进程。进程有进程控制块,线程也有线程控制块。但线程控制块比进程控制块小得多。线程间切换代价小,进程间切换代价大。 4)进程是程序的一次执行,线程可以理解为程序中一段程序片段的执行。 5)每个进程都有独立的内存空间,而线程共享其所属进程的内存空间。引申:程序、进
37、程与线程的区别是什么?程序、进程与线程的区别见下表。 程序、进程与线程区别 名 称 描 述 程序 一组指令的有序结合 进程 具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立 单元 线程 进程的一个实体,是CPU调度和分派的基本单元,是比进程更小的能独立运行的基本单元。本身基本上不 拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈)。一个线程可以创 建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行 简而言之,一个程序至少有一个进程,一个进程至少有一个线程。23.线程同步有哪些机制 (分数:4.00)_正确答案:()解
38、析:现在流行的进程线程同步互斥的控制机制,其实是由最原始、最基本的 4 种方法(临界区、互斥量、信号量和事件)实现的。 1)临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。 2)互斥量:为协调对一个共享资源的单独访问而设计,只有拥有互斥量的线程才有权限去访问系统的公共资源,因为互斥量只有一个,所以能够保证资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。 3)信号量:为控制一个具有有限数量的用户资源而设计。它允许多个线程在同一个时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。 4)事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。24.内核线程和用户线程的区别 (分数:4.00)_正确答案:()解析:根据操作系统内核是