《程序员面试宝典第四版》笔记2

7.1 指针基本问题
面试例题1:指针和引用的差别?

答案:
(1)非空区别。 在任何情况下都不能使用指向空值的引用。 一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针要高。
(2)合法性区别。 在使用引用之前不需要测试它的合法性。 相反,指针则应该总是被测试,防止其为空。
(3)可修改区别。 指针与引用的另一个重要的区别是指针可以被重新赋值以指向另一个不同的对象。 但是引用则总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。
(4)应用区别。 总的来说,在以下情况下应该使用指针:一是考虑到存在不指向任何对象的可能(在这种情况下,能够设置指针为空),二是需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。 如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么应该使用引用。

7.2 传递动态内存
面试例题3:这个函数有什么问题?该如何修改?

char *strA()
{
	char str[] = "hello word";
	return str;
}
解析:这个str里存的地址是函数strA栈帧里“hello word ”的首地址。 函数调用完成,栈帧恢复到调用strA之前的状态,临时空间被重置,堆栈“回缩”,strA栈帧不再属于应该访问
的范围。 存于strA栈帧里的“hello word”当然也不应该访问了。 这段程序可以正确输出结果,但是这种访问方法违背了函数的栈帧机制。

如果想获得正确的函数,改成下面这样就可以:

const char *strA()
{
	char *str = "hello word";
	return str;
}

首先要搞清楚char *str和char str[]:
char c[]="hello word"是分配一个局部数组;
char *c="hello word"是分配一个全局数组;
局部数组是局部变量,它所对应的是内存中的栈。 全局数组是全局变量,它所对应的是内存中的全局区域。 字符串常量保存在只读的数据段,而不是像全局变量那样保存在普通数据段(静态存储区),如:
char *c="hello word";
*c='t';//false
c占用一个存储区域,但是局部区的数据是可以修改的:
char c[]="hello word";
c[0]='t';//ok
另外要想修改,也可以这样:

const char *strA()
{
	static char str[] = "hello word";
	return str;
}
通过static开辟一段静态存储空间。
答案:因为这个函数返回的是局部变量的地址,当调用这个函数后,这个局部变量str就释放了,所以返回的结果是不确定的且不安全,随时都有被收回的可能。

面试例题6:下列代码的运行结果是什么?

#include <iostream>
using namespace std;
int main()
{

	int *ptr;
	ptr=(int*)0x8000;
	*ptr=0xaabb;
	cout<<*ptr<<endl;

}
解析:指针问题。
答案:这样做会导致运行时错误,因为这种做法会给一个指针分配一个随意的地址,这是非常危险的。 不管这个指针有没有被使用过,这么做都是不允许的。

7.3 函数指针
面试例题2:找出下面程序的错误,并解释它为什么是错的。

#include <iostream>
using namespace std;
int MaxNum(int a,int b)
{
	return a>b?a:b;
}
int main()
{
	int MaxNum();//int MaxNum(int x,int y);
	int *p=&MaxNum;//int (*p)(int,int)=&MaxNum;
	int a,b,c;
	cin>>a>>b>>c;
	int res=(*p)((*p)(a,b),c);
	cout<<res<<endl;
}
7.7 this指针

this指针易混的几个问题如下。
(1)This指针本质是一个函数参数,只是编译器隐藏起形式的,语法层面上的参数。this只能在成员函数中使用,全局函数、 静态函数都不能使用this。实际上,成员函数默认第一个参数为T* const this。
如:
class A
{
public:
int func(int p);
}
其中,func的原型在编译器看来应该是:int func(A*const this,int p);
(2)this在成员函数的开始前构造,在成员的结束后清除。 这个生命周期同任何一个函数的参数是一样的,没有任何区别。 当调用一个类的成员函数时,编译器将类的指针作为函数的this参数传递进去。 
如:A a;a.func(10);
此处,编译器将会编译成:A::func(&a,10);
看起来和静态函数没差别,不过,区别还是有的。 编译器通常会对this指针做一些优化,因此,this指针的传递效率比较高,如VC通常是通过ecx寄存器传递this参数的。
(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获得),也可以直接使用它。
8.1 递归基础知识
面试例题1:递归函数mystrlen(char *buf, int N)是用来实现统计字符串中第一个空字符前面字符长度。举例来说:char buf[]={'a,''b','c','d','e','f','\0','g'}字符串buf,当输入N=10或者20,期待输出结果是6;当输入N=3或5,期待输出结果是3或5.

#include <iostream>
using namespace std;
int mystrlen1(char *buf,int n)
{
	static int count=0;
	if(buf==NULL)
		return 0;
	if(buf[0]=='\0'||count==n)
		return count;
	count++;
	return mystrlen1(buf+1,n);
}
int main()
{
	char buf[]={'a','b','c','d','e','f','\0','g'};
	int count=mystrlen1(buf,4);
	cout<<count<<endl;
}
8.2 典型递归问题
面试例题1:If we define F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2) (n>=2), what is the result ofF(1025) mod 5?
A.0
B.1
C.2
D.3
E.4
解析:这里是对递归函数取余,所以我们最好先做一下拆项:
F(5n)=F(5n-1)+F(5n-2)
=2*F(5n-2)+F(5n-3)
=3*F(5n-3)+2*F(5n-4)
=5*F(5n-4)+3*F(5n-5)
所以F(1025)mod 5=3 * F(1020)mod 5;
依次类推:
F(1020)mod 5=3*F(1015)mod 5;
...
F(10)mod 5=3*F(5)mod 5;
F(5)为5,mod后值为0。 所以F(1025)mod 5=0;
答案:A
面试例题2:请给出此题的非递归算法:
f(m,n)=
if(m==1) n;
else if n==1 m;
else if(m>1,n>1) f(m-1,n)+f(m,n-1);

#include <iostream>
using namespace std;
int f(int m,int n)
{
	if(m==1)
		return n;
	if(n==1)
		return m;
	int **p=new int *[m];
	for(int i=0;i<m;i++)
		p[i]=new int[n];
	//===
	for(int i=0;i<m;i++)
	{
		p[i][0]=i+1;
		if(i==0)
		{
			for (int j=0;j<n;j++)
			{
				p[i][j]=j+1;
			}
		}
		else
		{
			for (int j=1;j<n;j++)
			{
				p[i][j]=p[i-1][j]+p[i][j-1];
			}
		}
	}
	int res=p[m-1][n-1];
	for (int i=0;i<m;i++)
	{
		delete []p[i];
	}
	return res;
}
int main()
{
	int m=3;
	int n=4;
	int res=f(3,4);
	cout<<res<<endl;
}

8.4 螺旋队列问题
面试例题1:看清以下数字排列的规律,设1点的坐标是(0,0),x方向向右为正,y方向向下为正。 例如,7的坐标为(-1,-1),2的坐标为(0,1),3的坐标为(1,1)。 编程实现输入任意一点坐标(x,y),输出所对应的数字。
 

#include <iostream>
#include<iomanip>
#include<cmath>
using namespace std;
int f(int x,int y)
{
	if(x==0&&y==0)
		return 1;
	int a=abs(x)>abs(y)?abs(x):abs(y);
	if(0<x&&(abs(y)<x||y==x))
		return (2*a-1)*(2*a-1)+a+y;
	
	if(0<y&&(abs(x)<y||y==-x)) 
		return (2*a-1)*(2*a-1)+3*a-x;
	
	if(x<0&&(x<y||y==x))
		return (2*a-1)*(2*a-1)+5*a-y;
	
	if(y<0&&(y<x||y==-x))
		return (2*a-1)*(2*a-1)+7*a+x;
}
int main()
{
	for(int j=-3;j<=3;j++)
	{
		for (int i=-3;i<=3;i++)
		{
			int res=f(i,j);
			cout<<setw(5)<<res;
		}
		cout<<endl;
	}
}

扩展知识
如矩阵:

1 2 3

8 9 4

7 6 5

找出规律,并打印出一个N×N的矩阵;规律就是从首坐标开始顺时针依次增大,代码如下:

#include <iostream>
#include<iomanip>
#include<cmath>
using namespace std;
void f(const int n)
{
	int a[100][100];
	int half=n/2;
	int count=0;
	for (int i=0;i<half;i++)
	{
		//===
		for(int j=i;j<n-1-i;j++)
			a[i][j]=++count;
		for(int j=i;j<n-1-i;j++)
			a[j][n-1-i]=++count;
		for(int j=n-1-i;j>i;j--)
			a[n-1-i][j]=++count;
		for(int j=n-1-i;j>i;j--)
			a[j][i]=++count;
	}
	if(n%2)
	{
		a[half][half]=n*n;
	}
	for (int i=0;i<n;i++)
	{
		for (int j=0;j<n;j++)
		{
			cout<<setw(4)<<a[i][j];
		}
		cout<<endl;
	}
}
int main()
{
	const int n=6;
	f(n);
}

面试例题1:解释一下什么是泛型编程,泛型编程和C++及STL的关系是什么?并且,你是怎么在C++环境里进行泛型编程的?
答案:泛型编程是一种基于发现高效算法的最抽象表示的编程方法。 也就是说,以算法为起点并寻找能使其工作且有效率工作的最一般的必要条件集。 令人惊讶的是,很多不同的算法都需要相同的必要条件集,并且这些必要条件有多种不同的实现方式。 类似的事实在数学里也可以看到。 大多数不同的定理都依赖于同一套公理,并且对于同样的公理存在多种不同的模型。 泛型编程假定有某些基本的法则在支配软件组件的行为,并且基于这些法则有可能设计可互操作的模块,甚至还有可以使用此法则去指导我们的软件设计。 STL就是一个泛型编程的例子。 C++是我可以实现令人信服的例子的语言。

面试例题2:重载和覆盖有什么不同?
答案:虚函数总是在派生类中被改写,这种改写被称为“override”(覆盖)。override是指派生类重写基类的虚函数,就像我们前面在B类中重写了A类中的foo()函数。 重写的函数必须有一致的参数表和返回值(C++标准允许返回值不同的情况,但是很少有编译器支持这个特性)。 Override这个单词好像一直没有什么合适的中文词汇来对应。 有人译为“覆盖”,还贴切一些。overload约定成俗地被翻译为“重载”,是指编写一个与已有函数同名但是参数表不同的函数。 例如一个函数既可以接收整型数作为参数,也可以接收浮点数作为参数。 重载不是一种面向对象的编程,而只是一种语法规则,重载与多态没有什么直接关系。

面试例题4:虚函数的入口地址和普通函数有什么不同?
答案:每个虚函数都在vtable中占了一个表项,保存着一条跳转到它的入口地址的指令(实际上就是保存了它的入口地址)。 当一个包含虚函数的对象(注意,不是对象的指针)被创建的时候,它在头部附加一个指针,指向vtable中相应的位置。 调用虚函数的时候,不管你是用什么指针调用的,它先根据vtable找到入口地址再执行,从而实现了“动态联编”。而不像普通函数那样简单地跳转到一个固定地址。
面试例题5:
1.C++中如何阻止一个类被实例化?
2.一般在什么时候构造函数被声明成private呢?
3.什么时候编译器会生成默认的copy constructor呢?
4.如果你已经写了一个构造函数,编译器还会生成copy constructor吗?

答案:
1.使用抽象类,或者构造函数被声明成private。
2.比如要阻止编译器生成默认的copy constructor的时候。
3.只要自己没写,而程序中需要,都会生成。
4.会生成。

面试例题1:求下列程序的输出结果。
int main()
{
printf("%f",5);
printf("%d",5.01);


}
解析:首先参数5为int型,32位平台中为4字节,因此在stack中分配4字节的内存,用于存放参数5。然后printf根据说明符“%f”,认为参数应该是个double型(在printf函数中,float会自动转换成double),因此从stack中读了8个字节。很显然,内存访问越界,会发生什么情况不可预料。 如果在printf或者scanf中指定了“%f”,那么在后面的参数列表中也应该指定一个浮点数,或者一个指向浮点变量的指针,否则不应加载支持浮点数的函数。于是("%f",5)有问题,而("%f",5.0)则可行。
答案:
第一个答案是0.000000。
第二个答案是一个大数。

面试例题4:关键字volatile有什么含意?并给出3个不同的例子。 

答案:一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。 精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。 下面是volatile变量的几个例子:
并行设备的硬件寄存器(如状态寄存器)。
一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)。
多线程应用中被几个任务共享的变量

面试例题5:一个参数可以既是const又是volatile吗?一个指针可以是volatile吗?解释为什么。
答案:
第一个问题:可以。 一个例子就是只读的状态寄存器。 它是volatile,因为它可能被意想不到地改变;它又是const,因为程序不应该试图去修改它。
第二个问题:可以。 尽管这并不很常见。 一个例子是当一个中断服务子程序修改一个指向一个buffer的指针时。


面试例题1:关键字static的作用是什么?
答案:在C语言中,static关键字至少有下列几个作用:
函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值。
在模块内的static全局变量可以被模块内所有函数访问,但不能被模块外其他函数访问。
在模块内的static函数只可被这一模块内的其他函数调用,这个函数的使用范围被限制在声明它的模块内。
在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝。
在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。

2012!末尾有几个零?
解析:
2乘5=10,4乘25=100,一个2和一个5的积末尾有1个0,所以只用用看1~2012这2012个数中,共有几个因数2和5,因数2多于5的个数,只用看有几个因数5。
5,10,15,20等等都有因数5,这样的数共有2012/5=402个,
25,50,75等等有两个5,这样的数有2012/25=80个,
125,250等等有3个5,这样的数有2012/125=16个,
625等等有4个5,这样的数有2012/625=3个,
所以共有因数5,402+80+16+3=501个,
从1开始2012个连续自然数的积的末尾有501个连续的零。
代码如下:

int main()
{
	int n = 2012;
	int count = 0;
	int n0 = 5;
	while (n/n0)
	{
		count += n / n0;
		n0 *= 5;
	}
	cout << count << endl;
}


面试题13:对于方程x1+x2+x3+x4=30,有多少满足x1≥2,x2≥0,x3≥-5,x4≥8的整数解?
答案:稍加调整改写原方程如下:(x1-1)+(x2+1)+(x3+6)+(x4-7)=29。 相当于29个1分成4组(28个空挑3个),有 =28*27*26/(1*2*3)=3276组解

面试题10:100个灯泡排成一排,第1轮将所有灯泡打开;第2轮每隔1个灯泡反转1个,即排在偶数的灯泡被关掉;第3轮每隔2个灯泡反转一次,将开着的灯泡关掉,关掉的灯泡打开。
依次类推,第100轮结束的时候,还有几盏灯泡亮着。 
解析:本题显然不是期待面试者用循环套循环的傻办法来解答,找一下规律,不妨用10个灯先做一下模拟(1为亮0为灭)
规律如下:当灯号的约数为奇数(即完全平方数1,4,9…)时,灯亮;反之,灯灭。所以现在问题转化为寻找100以内完全平方数个数的问题了。
代码如下:
int main()
{
	int sum = 0;
	int N = 100;
	for (int i = 1; i <= N; i++)
	{
		if (int(sqrt(i))*int(sqrt(i)) == i)
		{
			sum += 1;
		}
	}
	cout << sum << endl;
}


面试题2:一所监狱,关着N个人。 每人都被关在单独的房间里,房门外面标着从1开始的号码,表示犯人的号码。 突然有一天,国家发生了战争。 监狱长想我是应该释放这些犯人呢?
还是应该放弃他们离开这里呢?如果是一群无知的犯人,释放他们反而会给居民们带来更大的危险。 如果是一群有脑子的犯人,说不定还能对抗敌人,为国家做点贡献。 最后,监狱长决定用一个智力题考验监狱里的所有犯人。
问:给每个犯人的房间门外侧涂上颜色,颜色要么是“白色”,要么是“黑色”。 从1号人开始,一次只能一个人,出来看一下其他犯人的门是什么颜色。 但是看不见自己的门,因
为自己的门当时是翻开的,背面靠墙了。 然后犯人要猜测自己的门是什么颜色的,并告诉监狱长,然后再回到自己的房间。 监狱长把每个人的答案记下来,并且统计有多少人猜对了。如果有一半的人猜对了,就把监狱里的所有人全部释放。 如果猜对的人不到一半,监狱长就放弃这些犯人,让他们饿死。
1号犯人是被关的时间最长的犯人,也是这个监狱犯人里的带头大哥。 监狱长给1号犯人一个机会,让他出门后看完其他犯人的门后,可以使用监狱广播1分钟,把他的“逃命方
案”告诉所有犯人,然后再回房间。
假设你是这个1号犯人,你会如何设计你的“逃命方案”呢?







































  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值