除了对算法做一个系列的总结之外,我还算法对网络,操作系统,数据库等各方面的热点的考点进行一下总结,放在这里,还是想到哪说到哪,为了给自己一个总结,同时希望能够帮助各位网友。
我们主要采用引用链接的形式,贴出一些有用的,写得好的博文出来。
一、关于C++中new和delete,malloc和free的区别与联系:
1.
http://blog.csdn.net/hgy413/article/details/6074128
这篇博客就是告诉我们new会调用类型的构造函数,delete会调用类型的析构函数,而malloc和free则不会
2.
这篇博文水平比较高,虽然主要内容是翻译老外的
博文告诉了我们new和delete的详细过程,以及为什么用delete删除类对象数组时为什么会崩溃。主要的内容是说new的过程是先调用new函数,然后调用malloc函数,然后调用构造函数,返回指针;delete则是先析构函数,然后调用free释放掉内存。但是如果用new[size]申请了size个对象,然后用delete(应该用delete[])来删除的话会崩溃,这是因为new[size]申请的空间会比实际的空间多出4个字节来,这四个字节存放的是这个数组的大小,如果直接用delete删除,则会出现崩溃。
个人认为:new或者new[]会对某个内存块进行“格式化”,看到格式化可能众多像我一样的屌丝会有点心里一震,我说的格式化是指对这片内存加上格式的东西,就是说这块内存要放什么样的数据,以及数据怎么组合;但是你用malloc则不会,就是会直接返回给你一个荒芜的内存区,至于你要往这里面放什么,怎么放,它是不管的。这也就是说new会调用构造函数,构造函数的意思就是给一片一个对象大小的内存放入这个对象所属的类的成员初始化值,呵呵,有点拗口,不过我读了两遍,逻辑上是对的。
思考总结:平时认为理所应当的东西,如果深究起来还是很有意思的,一旦觉得某个东西有意思,你就不觉得无聊了,码农就有码农的乐趣了。
二、TCP三次握手,四次挥手的过程,原理总结
这也是一个经典的问题,我们好像对TCP三次握手还是有点印象的,但是四次挥手是什么东东恩?
详见:
1.这个博文里,通过截图,截数据包的形式,深入分析了三次握手四次挥手的过程
2.
http://blog.csdn.net/whuslei/article/details/6667471
这个博文里,对为什么只需要三次握手,而需要四次挥手进行了解释
四次挥手的原因是因为一方要终止连接,不代表另外一方也要终止连接,可能我还有数据要传呢
思考:在自己认为很简单的东西,别人一问一考,你可能不能完全答出来,这就是说明我们的知识是碎片化的,不系统,好像离开了网络我们就不知道怎么学习了,就不知道问谁了。比如这个题,直接google一下就出来了。但是问题就是考试的时候是不允许你google的,另外,知识的碎片化,导致的问题就是对好多东西都是一知半解,不求甚解,没有系统的对计算机领域的核心的几个方向有一个深入的认识,当然不要求我们对每个方向都有深入的认识,这也是不可能的,但是最起码要对一些常识的东西有所了解,不然的话会制约我们的发展。
三、不调用库函数,写出一个strcpy函数来
这是一个经典的题目,虽然看似简单,但是有很多东西可以考察出来:
1.
http://blog.163.com/zhang_231/blog/static/109599520076256259238/
这个博客应该也是转载的,但是不管怎样,博主的思维很清晰,博客设计的很好看,还是应该感谢博主。为了加深记忆,我们把代码贴出来:
-
char * strcpy(char * strDest,const char * strSrc)
-
{
-
if ((strDest==NULL)||(strSrc==NULL)) //[1]
-
throw "Invalid argument(s)"; //[2]
-
char * strDestCopy=strDest; //[3]
-
while ((*strDest++=*strSrc++)!='\0'); //[4]
-
return strDestCopy;
-
}
就八行代码,但是博主对每一行都进行深入的分析。
值得一看。
2.
http://blog.csdn.net/zzxian/article/details/6364745
博主分享了他面试的时候遇到这个问题的场景
思考:还是应该从基础做起。我目前能够一下就想到的几点是:源数据要保护,用const来完成;[1]处最好写成(NULL==strDest)||(NULL==strSrc),这样做的好处就是如果你把==写成了=(这是程序员常犯的错误)时编译器会饶不了你的
,所以养成将常量放在逻辑操作符前面的习惯吧!
四、迷之指针
关于指针,肯定是各大IT公司必考的东西
下面记录一下今天遇到的一个经典的关于指针的问题。先看代码:
-
void swap1(int *p,int *q)
-
{
-
int *temp;
-
temp=p;
-
p=q;
-
q=temp;
-
}
-
void swap2(int *p,int *q)
-
{
-
int temp;
-
temp=*p;
-
*p=*q;
-
*q=temp;
-
}
-
void swap3(int &p,int &q)
-
{
-
int temp=p;
-
p=q;
-
q=temp;
-
}
-
-
void swap4(int *p,int *q)
-
{
-
int *temp;
-
*temp=*p;
-
*p=*q;
-
*q=*temp;
-
}
这里定义了四个swap函数,第一个、第二个和第四个都是使用了指针,第三个是使用了引用。
那么你认为这四个函数哪个能够实现交换呢?
先看第一个,哦,有指针,老师说,只要是传递的是地址,对形参的改变将影响到实参。
可是仔细一想,是这样吗?我们看代码,swap1的功能是将两个指针交换了,函数结束之后,这两个指针就失效了。那么跟实参有半毛钱关系?是的,没关系
那么看swap2,函数体里面,通过指针交换了指针所指向的内容,这也就是说两个实参的内容实现了改变
再看swap3,通过引用,引用的实质就是别称,也是就给一个变量起一个外号,那么对这个外号的任何改变都将反应到原来的主人身上。所以swap3可以实现交换。
再看swap4,好像是没有问题的,但是一想,这个temp指针没有初始化啊,它的内容是一个随机数,如果这时莽撞的给这个地址赋值,可能会导致系统崩溃呢。所以要想使用这个方式应该先对temp指针赋值:int *temp=p;而后再进行替换
那么,总结一下,什么情况下指针的改变能够影响到实参呢?一般的是只要是有*p这样的操作,在函数体里的操作就将改变实参,因为这是对指针指向的内容的改变,而不是单纯的改变指针,另外一个要随时注意指针的初始值,如果不是系统分配给你的,你就要想办法对它进行初始化,否则会出现问题。
五、迷之指针二
再来看一组例子:
-
试题1:
-
-
void GetMemory( char *p )
-
{
-
p = (char *) malloc( 100 );
-
}
-
void Test( void )
-
{
-
char *str = NULL;
-
GetMemory( str );
-
strcpy( str, "hello world" );
-
printf( str );
-
}
-
-
试题2:
-
char *GetMemory( void )
-
{
-
char p[] = "hello world";
-
return p;
-
}
-
void Test( void )
-
{
-
char *str = NULL;
-
str = GetMemory();
-
printf( str );
-
}
-
-
试题3:
-
void GetMemory( char **p, int num )
-
{
-
*p = (char *) malloc( num );
-
}
-
void Test( void )
-
{
-
char *str = NULL;
-
GetMemory( &str, 100 );
-
strcpy( str, "hello" );
-
printf( str );
-
}
-
-
-
试题4:
-
void Test( void )
-
{
-
char *str = (char *) malloc( 100 );
-
strcpy( str, "hello" );
-
free( str );
-
... //省略的其它语句
-
}
那么以上四个题目,哪个对哪个错呢?说出原因。
其实都是有问题的。
第一个是的问题我们在上面一目中已经讲了。这个函数中没有出现*p这样的句子,显然p是一个局部变量,是不会把结果带出去的;第二题同样这问题,函数的退出导致局部变量的释放;第三题已经差不多对了,可以看到函数中出现了*p这样的句子,所以结果能够带出去,但是缺少了一步就是对申请的空间的检查,究竟申请成功没有呢?所以应该加上类似str!=NULL这样的句子;同理第四题也是这样的,要对申请的空间进行检查。
顺便,再对字符指针与字符数组进行一个对比,看代码:
-
char *GetString1(void)
-
{
-
char p[15] = "Hello world";
-
return p;
-
}
-
char *GetString2(void)
-
{
-
char *p = "Hello world";
-
return p;
-
}
-
int main()
-
{
-
char *c1=NULL;
-
c1=GetString1();
-
cout<
-
cout<
-
c1=GetString2();
-
cout<
-
cout<
-
}
这时编译一下,看到什么结果呢?
看到两个警告,第一个警告是说把局部变量p作为地址带出去了,第二个警告是说将常量字符串赋值给了一个变量字符指针。
那么只有警告没有错误,是可以运行的,那运行结果呢?
从结果可以看出,第一个返回的指针指向的内容是一堆乱码,但是这依然是一个指针,指针长度依然为8,注意我们这里是在64为操作系统下作的。第二个返回的指针指向的内容却是正确的输出了,这是为什么呢?因为在GetString1中,p作为一个字符数组,只存在于本函数中,出了本次函数,p 就将被delete掉,返回的这个指针就是一个随机数了,就没有任何法律效力了,我就不管了。但是在GetString2中,“hello world”却是作为一个常量存在的,在编译的时候,会在常量区给这个字符串分配空间并有地址,通过p返回的时候实际上就是返回了这个地址,虽然p也被delete掉了,但是这个地址还是被带出去了。那么我们输出的时候就可以找到这个常量区,将这个字符串输出了。
那么本着递进式学习的原则,我们对两个函数都做一下改动:
-
char *GetString1(void)
-
{
-
static char p[15] = "Hello world";
-
return p;
-
}
-
char *GetString2(void)
-
{
-
char *p = "Hello world";
-
return p;
-
}
-
int main()
-
{
-
cout<<"-----------------------------------------------------------------\n";
char *C1=NULL;
C1=GetString1();
cout<
cout<
C1[0]='a';
cout<
C1=GetString1();
cout<
static string s1=C1;
cout<
s1[1]='b';
C1=GetString1();
cout<
cout<
C1=GetString2();
cout<
cout<
-
}
先给出输出结果:
结果有点怪异,我们进行分析。
首先第一个输出。因为我们把GetString1中的p定义为了static,所以这时就在程序的静态变量区有了一片区域属于了p所指向的地址了。那么我们将这个地址的内容输出来没问题,指针的长度为8.
但是我们接着对这个指针所指向的内容进行改变,C1[0]='a',然后再输出,这时输出变了,可以理解。然后我们再次调用GetString1,输出结果,竟然还是 aello world.这不是逆天了吗?明明函数里说的是hello world啊,为何这里没有改变呢?这是因为静态区域的变量一旦申请,不管你调用多少次,地址是不会变的,也就是说假如“hello world”的地址为1000,那么经过我们的改变这个区域的内容变为了“aello world”,但是地址还是不变的,这时把地址赋给p,让p带出去,输出这个结果,当然还是“aello world”了。
那么,如果我用这个字符指针初始化一个string类型的变量,然后输出结果,这个可以理解,然后我通过这个string变量对其中的一个字符进行改变:s1[1]='b',然后我再次调用GetString1,输出结果发现不变!呵呵,这是为啥呢?这是因为这个s1,是一个局部变量,跟我静态区域的那个内容没有半毛钱的关系了,不知这样说您可以理解了吗?如果还是不可以明白,那么我们再做实验:
-
string s2="abcd";
-
s2[1]='c';
-
char c3[]="abcd";
-
c3[1]='c';
-
char *c4="abcd";
-
c4[1]='c';
这样我们定义了三个跟字符相关的东西,其中s2是一个string,c3是一个char数组,c4是一个char指针。对这段代码进行编译,结果是:Segmentation fault (core dumped)
为何呢?问题就出在c4上,对于s2和c3来说,都是变量,系统都要给他们分配临时的区域存放数据,这个区域的内容是可变的
也就是说对前两个记性赋值是没有任何问题的。针对c4来说,系统是不会给c4来单独的分配空间存放数据的,这个“abcd”是一个常量。
那么对它进行赋值,也就是想对常量进行赋值,如果能够执行,那也就是意味着常量能变了,那不就彻底的颠覆了价值观了吗?
所以为了保全我们的价值观,输出结果还是让你段错误吧!
总结:细节决定成败,面试官就是在这些细节中给你打分的。
六、迷之指针三
先看代码:
-
以下为WindowsNT下的32位C++程序,请计算sizeof的值
-
-
void Func ( char str[100] )
-
{
-
sizeof( str ) = ?
-
}
-
void *p = malloc( 100 );
-
sizeof ( p ) = ?
第一眼看到的时候我想说第一个输出应该是100,第二个输出应该是4,但是结果却是都是4,这是为什么呢?
我们来分析:
char c[10];
sizeof(c);
这时当然是输出实际的字节数10
char c[10]={'a','b','c','\0','d','e'};
cout<
cout<
cout<
char *c2=c;
cout<
输出什么呢?
abc
10
3
4
为何呢?cout输出时,遇到\0就停止了
sizeof实际上输出整个c占据的字节数,所以为10
strlen输出的是遇到第一个‘\0’之前的字符数
那么最后c2为什么是4呢?这是因为c2是一个指针,而指针的大小在32位机器下,是4个字节。你也许会问那c也是一个指针啊
不是说好的数组名就是指针吗?是的,这个不错,但是对于sizeof这个函数来说,它遇到数组时,会输出整个数组所占的字节数
这就是c、c++有时迷惑人的地方。
好了,铺垫了半天,我们初识的那个Func函数为何输出4呢?对啊 ,不是说对于数组来说,就输出数组的大小的吗?
这是因为对于参数为数组类型的(像这里),形参实际上不是一个数组变量,而是一个指针变量。为何这样啊?为何不这样呢?
比如你在定义函数的时候可以这样写:
-
void Func ( char str[100] )
-
{
-
sizeof( str ) = ?
-
}
但是你写成这样也是可以的。
-
void Func ( char str[] )
-
{
-
sizeof( str ) = ?
-
}
这就是说,对于形参为数组的函数来说,形参退化为了指针,而不是一个单纯的数组变量。
这就是解释了为什么输出为4了。
七、谈谈虚函数
相信,对于稍微有些C++经验的人,更别说大牛了,对于C++的虚函数都会如数家珍。我这个菜鸟秉着一个记录学习的态度在这里稍作总结,后面遇到还会继续谈到。
虚函数,我认识是为了实现C++的多态而存在的,所谓的多态,是指多种形态等。那么在C++中具体表现为什么呢?
具体表现就是可以用基类指针指向继承类对象,那么在程序运行的时候,可以根据这个指针指向的不同对象而采取不同的动作。
c++的三大特点:封装,继承,多态。
那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。
注意这个接口重用,这个很重要。不同的类可能继承至同一个基类中,而在这个基类中我们定义了公共的接口,并将这些接口声明为virtual的。这样在各个继承类中只要覆盖这写接口,我们就可以定义一个以基类指针或者基类引用为形参的函数,传递给这个函数的对象可以是继承至这个基类的各种对象。而这个函数中针对这个指针或者引用所做的所有操作都会映射到相应的对象。这样就不用针对不同类型的对象定义相同的函数了。这就是所谓的接口重用。也就是定义一个公共接口,大家都来继承覆盖。
看个例子:
-
#include<iostream>
-
using namespace std;
-
-
class A
-
{
-
public:
-
void foo()
-
{
-
printf("1\n");
-
}
-
virtual void fun()
-
{
-
printf("2\n");
-
}
-
};
-
class B : public A
-
{
-
public:
-
void foo()
-
{
-
printf("3\n");
-
}
-
void fun()
-
{
-
printf("4\n");
-
}
-
};
-
int main(void)
-
{
-
A a;
-
B b;
-
A *p = &a;
-
p->foo();
-
p->fun();
-
p = &b;
-
p->foo();
-
p->fun();
-
return 0;
-
}
第一个p->foo()和p->fuu()都很好理解,本身是基类指针,指向的又是基类对象,调用的都是基类本身的函数,因此输出结果就是1、2。
第二个输出结果就是1、4。p->foo()和p->fuu()则是基类指针指向子类对象,正式体现多态的用法,p->foo(),由于指针是个基类指针,指向是一个固定偏移量的函数,因此此时指向的就只能是基类的foo()函数的代码了,因此输出的结果还是1。而p->fun(),指针是基类指针,指向的fun是一个虚函数,由于每个虚函数都有一个虚函数列表,此时p调用fun()并不是直接调用函数,而是通过虚函数列表找到相应的函数的地址,因此根据指向的对象不同,函数地址也将不同,这里将找到对应的子类的fun()函数的地址,因此输出的结果也会是子类的结果4。
笔试的题目中还有一个另类测试方法。即
B *ptr = (B *)&a; ptr->foo(); ptr->fun();
问这两调用的输出结果。这是一个用子类的指针去指向一个强制转换为子类地址的基类对象。结果,这两句调用的输出结果是3,2。
并不是很理解这种用法,从原理上来解释,由于B是子类指针,虽然被赋予了基类对象地址,但是ptr->foo()在调用的时候,由于地址偏移量固定,偏移量是子类对象的偏移量,于是即使在指向了一个基类对象的情况下,还是调用到了子类的函数,虽然可能从始到终都没有子类对象的实例化出现。
而ptr->fun()的调用,可能还是因为C++多态性的原因,由于指向的是一个基类对象,通过虚函数列表的引用,找到了基类中fun()函数的地址,因此调用了基类的函数。由此可见多态性的强大,可以适应各种变化,不论指针是基类的还是子类的,都能找到正确的实现方法。
也就是说:不论指针是基类还是继承类,只要是通过这个指针来调用定义为virtual的函数,只要看这个指针指向的哪个对象,就调用相应的对象属于哪个类就行了。至于不是virtual的函数,就只要看这个指针的类型是什么就行了,如果是基类就调用基类的函数,如果是继承类就调用相应的继承类的函数就行了。记住这一点就可以了。
还有一点就是多态只能通过
指针或者引用来实现,而不能通过赋值来实现。如果上例中做:a=b,这样的话,来调用fun函数,还是只调用a所属的类的函数,没有实现多态。
需要
将基类的析构函数定义为virtual,这是因为,如果不定义为vitual,那么在某个函数中你利用了C++的多态,用一个基类指针指向了一个继承类,在函数退出或者是你手动的delete掉这个指针的时候,会发生什么呢?delete掉的只是只是基类部分,继承部分的成员变量并没有删除掉,而是发生了内存的泄漏。
所谓的抽象类,就是某个类中有一个函数声明为了纯虚函数。这样的类是不能实例化的。这样的类的存在的意义就是单纯的为了定义公共接口,然后让其它的类来继承它。
C++是多么的繁琐啊!但是这些繁琐都是工程师们在解决遇到的问题的时候慢慢的添加进去的,我们不可能都遇到这些情况,每个公司每个单位相信只是用到了其中的一部分而已,只要你把这一部分研究透了就可以了。
八、memcpy实现
今天去面试金山了,面试官很专业,职业的C++程序员,其中问到了几个关于const的问题,强指针与弱指针的问题,其中有一个题目记得是问如果一个函数能够返回指针也能够返回引用,问你怎么做出选择。为什么用指针为什么用引用?读者可以想想为什么。google了半天,发现老外在这个问题上也没有很好的解释。其实呢,往底层想,引用其实还是保存了对象的地址,不然怎么知道你引用的是谁呢?所以就空间效率来说,引用并没有优势。引用的优势是c++在C的基础上提出的改进,也就是不用频繁的使用取地址符&,但是引用是一个绑定,也就是说不能“朝秦暮楚”的改变要引用的对象,这个我还没有深刻的理解,这是为什么。
在博客:
http://blog.csdn.net/xiaobo620/article/details/7488827
中进行了很好的分析。
那么,为什么很多面试官都喜欢问这个题目呢?是一个习俗还是因为这个确实很重要?
最简单的一个想法是:
-
void *mymemcpy(void *dst,const void *src,size_t num)
-
{
-
assert((dst!=NULL)&&(src!=NULL));
-
//assert(des>=src+num||src>dst+num);
-
byte * psrc = (byte *)src;//byte 既为unsigned char类型
-
byte * pdst = (byte *)dst;
-
while(num-->0)*pdst++ = *psrc++;
-
return dst;
-
}
那么问题是什么呢?首先是没有考虑地址的重合问题,就是如果dest在source之后,dest如果overlap 了source,那么如果还是这样进行简单的赋值,会带来什么问题呢?问题是source会被破坏,想想是吧?
那么如何保证在有overlap的情况下也能安全的进行拷贝呢?我当时的回答是首先判断是否有overlap,如果没有就直接进行拷贝,如果有就重新申请一块区域来访dest,然后再进行拷贝,这个答案是对的,但是肯定不是面试官想要的答案。
那么看下面的代码:
-
void * mymemcpy(void *dest, const void *src, size_t count)
-
{
-
if (dest == NULL || src == NULL)
-
return NULL;
-
char *pdest = static_cast <char*>(dest);
-
const char *psrc = static_cast <const char*>(psrc);
-
int n = count;
-
-
if (pdest > psrc && pdest < psrc+count)
-
{
-
for (size_t i=n-1; i != -1; --i)
-
{
-
pdest[i] = psrc[i];
-
}
-
}
-
else
-
{
-
for (size_t i= 0; i < n; i++)
-
{
-
pdest[i] = psrc[i];
-
}
-
}
-
-
return dest;
-
}
网上的答案基本上就是这个,但是这样就解决了地址覆盖的问题了吗?如果存在overlap,不论是从后往前,还是从前往后,多会污染source啊,这个问题一直没有搞懂。
对于代码:
-
void *mymemcpy(void *dst,const void *src,size_t num)
-
{
-
assert((dst!=NULL)&&(src!=NULL));
-
int wordnum = num/4;//计算有多少个32位,按4字节拷贝
-
int slice = num%4;//剩余的按字节拷贝
-
int * pintsrc = (int *)src;
-
int * pintdst = (int *)dst;
-
while(wordnum--)*pintdst++ = *pintsrc++;
-
while (slice--)*((char *)pintdst++) =*((char *)pintsrc++);
-
return dst;
-
}
这样说进行了优化,这倒是真的,因为不是一个字符一个字符的进行拷贝了,而是一个字节一个字节的拷贝,这样速度肯定比一个字节一个字节的拷贝要快。