面试总结

来源:互联网 发布:试述数据库设计的步骤 编辑:程序博客网 时间:2024/05/29 11:52

一、C++中什么情况下用栈什么情况下用堆?

C++中堆和栈的完全解析

内存分配方面:

堆: 操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删 除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样代码 中的delete语句才能正确的释放本内存空间。我们常说的内存泄露,最常见的就是堆泄露(还有资源泄露),它是指程序在运行中出现泄露,如果程序被关闭掉的话,操作系统会帮助释放泄露的内存。

栈:在函数调用时第一个进栈的主函数中的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数 的各个参数,在大多数的C编译器中,参数是由右往左入栈,然后是函数中的局部变量。

 

一、预备知识程序的内存分配

一个由c/C++编译的程序占用的内存分为以下几个部分

1、栈区(stack— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

2、堆区(heap — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

3、全局区(静态区)(static,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放

4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放

5、程序代码区存放函数体的二进制代码。

 

有些说法,把34合在一起,也有的把3分成自由存储区(malloc/free)和全局/静态存储区。

这与编译器和操作系统有关。

 

二、例子程序

这是一个前辈写的,非常详细

//main.cpp

int a = 0; 全局初始化区

char *p1; 全局未初始化区

main()

{

int b; 

char s[] = "abc";  //更正:abc 分配在静态存储区,不是栈上

char *p2; 

char *p3 = "123456"; 123456\0在常量区,p3在栈上。

static int c =0 全局(静态)初始化区

p1 = (char *)malloc(10);

p2 = (char *)malloc(20);

分配得来得1020字节的区域就在堆区。

strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。

}

二、堆和栈的理论知识

2.1申请方式

stack:

由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间

heap:

需要程序员自己申请,并指明大小,在cmalloc函数

p1 = (char *)malloc(10);

C++中用new运算符

p2 = (char *)malloc(10);

但是注意p1p2本身是在栈中的。

2.2

申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,

 遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内 存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大 小,系统会自动的将多余的那部分重新放入空闲链表中。

2.3申请大小的限制

栈:在Windows,栈是向低地址扩展的数据结构,是 一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个 编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

转载来自:http://www.cnblogs.com/likwo/archive/2010/12/20/1911026.html

二、什么是内连接什么是外连接?

内连接:内连接也叫做连接,是一种很自然的连接,他的查询结果是删除与被连接的中没有匹配的行;他这样做丢失信息;

外链接:分为左向外连接,右向外连接,完整外部连接;

左向外链接:返回的结果包含了Left JOIN子句中指定的左表的所有行,而不仅联接列的所有的行,如果左表的某行在右表中没有,那在相关联的集中行右表的所有选择列表赋值为空;

右外向连接right join 或 right outer join

返回的结果是right join 子句中指定的右表的所有行,如果右表的某些行在左表中没有匹配,那么就在左表的相应行赋值为空,

完整的外部连接

返回左表和右表的所有行,当某行在另一个表中没有匹配时,则另一个表的选择列表列包含空值;如果表中有匹配行,则整个结果集行包含基表的数据值。完整连接等于左连接加有连接;

数据库中的内连接、自然连接、外连接

数据中的连接join分为内连接、自然连接、外连接,外连接又分为左外连接、右外连接、全外连接。
  
  当然,这些分类都是在连接的基础上,是从两个表中记录的笛卡尔积中选取满足连接的记录。笛卡尔积简单的说就是一个表里的记录要分别和另外一个表的记录匹配为一条记录,即如果表A有3条记录,表B也有三条记录,经过笛卡尔运算之后就应该有3*3即9条记录。如下表:


自然连接(natural join)
  自然连接是一种特殊的等值连接,他要求两个关系表中进行比较的必须是相同的属性列,无须添加连接条件,并且在结果中消除重复的属性列。
  sql语句:Select …… from 表1 natural join 表2
  结果:

   内连接(inner  join)
  内连接基本与自然连接相同,不同之处在于自然连接奥球是同名属性列的比较,而内连接则不要求两属性列同名,可以用using或on来指定某两列字段相同的连接条件。
  sql语句:Select …… from 表1 inner join 表 2 on 表1.A=表2.E
  结果:
 
  自然连接时某些属性值不同则会导致这些元组会被舍弃,那如何保存这些会被丢失的信息呢,外连接就解决了相应的问题。外连接分为左外连接、右外连接、全外连接。外连接必须用using或on指定连接条件。

 左外连接(left outer join)
  左外连接是在两表进行自然连接,只把左表要舍弃的保留在结果集中,右表对应的列上填null。
  sql语句:Select …… from 表1 left outer join 表2 on 表1.C=表2.C
结果:

  右外连接(rignt outer join)
  右外连接是在两表进行自然连接,只把右表要舍弃的保留在结果集中,左表对应的列上填null。
 Select …… from 表1 rignt outer join 表2 on 表1.C=表2.C
  结果:

  全外连接(full join)
  全外连接是在两表进行自然连接,只把左表和右表要舍弃的都保留在结果集中,相对应的列上填null。
 Select …… from 表1 full join 表2 on 表1.C=表2.C
  结果:

(二)怎么用程序测试一个机器是大端还是小端?

可以用union来测定;

代码如下:

#include <iostream>  using namespace std;   void checkSystemBigOrLittle(void);   int main(void) {     checkSystemBigOrLittle();     system("pause");     return 0; }   void checkSystemBigOrLittle(void) {     typedef union MyUnion     {         int i;         char c;     };     MyUnion mu;     mu.i = 1; //因为对于0x00000001 低地址-->高地址 大端
              //0x00000001高地址-》低地址 小端;
              //所以可以根据低地址的值判断是大端还是小端;
              //如果是大端 那么低地址mu.c ==0,如果是小端 那么低地址的值为 mu.c == 1;     if(mu.c == 1)     {         cout<<"小端存储模式。。。"<<endl;     }     else if (mu.c == 0)     {         cout<<"你的电脑是大端存储模式。。。"<<endl;     }     else    {         cout<<"很抱歉,出错了"<<endl;     } } 
(3)默认的构造函数起什么作用?

至今没有对这个又很深入的理解。它就是对对象的数据成员初始化和,普通定义一个数据干的事情一样;

(4)关于虚表的问题:子类的虚函数是在什么时候放入虚表的?

#include<iostream>using namespace std;class A{public:virtual void fun(){cout<<"A::fun()<<"<<endl;}private:int a;int *ptr;string str;};class B:public A{virtual void fun(){cout<<"B:fun()"<<endl;//在这个对象中只有一个虚表,
                                      //并且虚表里面子类重写父类的虚函数已经覆盖再虚表里面了;}};

(三)论述C/C++程序预处理,编译,连接到执行的过程

一、预处理

1.宏定义指令:预编译所要做的是将对代码中出现的宏定义做出相对应的替换;

2.条件编译指令:列如#ifdef,#ifndef,#else,#elif,#endif,这些伪指令的出现可以使得程序员定义不同的宏来决定编译程序对哪些代码进行处理;预编译程序根据有关文件,将那些不必要的代码过滤掉;

3,头文件包含指令:文件包含命令包含#include<stdio.h> #include"filename";

在头文件包含许多宏命令,还有许多外部符号的说明;采用头文件的主要作用是可以将这些定义的宏或者外部符号说明能够用在许多C源文件中,因为在用到这些定义的源文件中只需要包含该头文件即可,而不需要将这些定义在定义一遍。预编译程序将头文件中包含的所有定义加入到它所产生的文件中,以供编译程序对他进行处理

4.特殊符号:预编译可以识别源程序中的一些特殊符号,比如FILE被识别为源文件的文件名,预编译程序将源文件中这些特殊符号替换成为十进制数;

二、编译阶段

1.经过预编译的文件中只包含一些常量,以及C语言的关键字,编译做的事情是通过词法分析和语法分析,确认所有的指令都符合语法规则的情况下,将它翻译成等价代码或者汇编代码;

三、汇编阶段:

汇编阶段是把汇编语言代码翻译成为目标机器指令的过程;对于被系统翻译的源文件将这个过程处理得到目标文件,目标文件存放的是是与源文件等效的目标机器的机器语言代码

四、链接程序:

由汇编程序生成的目标的文件并不能立即执行,因为可能在某一个原文件中引用了另外一个源文件的某些变量,或者该文件调用库文件函数;

链接的主要目的是将有关系的的目标文件连接起来;

静态链接:这种链接是函数的代码将从其所在的静态链接库中拷贝到最终的可执行文件中,这样该程序被执行的时候,这些代码被装入到该进程的虚拟进程空间

动态链接:函数的代码被放到动态链接库或者是共享对象的某个目标文件中,链接程序做的工作是在最终可执行的程序中记录下共享对象的名字以及其他少量的登记信息。在可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间,动态链接程序将可执行的程序中记录的信息找到相应的函数代码;

动态链接会使最终的可执行文件的变小,当共享对象 被多个进程使用时,能够节省一些内存;

(四)单列模式(Singleton模式)

这个问题在一个游戏开发公司面试的时候,当时没写好,一方面在于我不会变通,人家给了一个纸我就写了,写的特别的挤;第二就是自己没有完全写好虽然明白,可是写的时候却没有考虑多线程的情况,以及是饿汉式还是懒汉式;在这里做个总结;

饿汉式:事先把实列new出来,用的时候直接用;

class Singleton{public:static Singleton * get_stance(){return my_stance;}private:Singleton();Singleton(Singleton & other);Singleton& operator=(const Singleton & other);~Singleton(){}private:static Singleton * my_stance;};Singleton *Singleton ::my_stance = new Singleton();Singleton::Singleton(){cout<<"Singleton"<<endl;}
#define _AFXDLL#include <afx.h>#include <process.h>#include "afxmt.h"#include "winbase.h"#include<assert.h>static CCriticalSection cs;

懒汉式class Singleton{public:static Singleton* get_stance(){if(my_stance == NULL){cs.Lock();if(my_stance  == NULL){my_stance == new Singleton();}cs.Unlock();}return my_stance;}static void freeInstance(){if(my_stance != NULL){delete my_stance;my_stance =NULL;}}private:Singleton();Singleton(Singleton & other);Singleton& operator = (const Singleton & other);~Singleton();private:static Singleton* my_stance ;};Singleton * Singleton::my_stance = NULL;Singleton::Singleton(){cout<<"Singleton"<<endl;}Singleton::~Singleton(){delete my_stance;}*/int main(){Singleton * ptr1 = Singleton::get_stance();Singleton * ptr2 = Singleton::get_stance();}


(五)static 有什么作用?

答:在C语言中,static 主要定义全局静态变量,定义局部静态变量,定义静态函数

一、定义全局静态变量:在全局变量前面加上关键字static,该全局变量就变成全局静态变量。

全局静态变量有以下特点:

(1)在全局数据区内分配内存

(2)如果没有初始化,其默认值为0;

(3)该变量在本文件内从定义开始到文件结束可见

二、定义局部静态变量:在局部静态变量前面加上关键字static,该静态变量变成了局部变量。

静态局部变量有以下特点:

(1)该变量在全局数据区分配内存

(2)如果不显示初始化,那么将被隐式初始化为0

(3)它始终驻留在全局数据区,直到程序运行结束;

(4)其作用域为局部作用域,当定义它的函数或语句块结束时,其作用域随之结束。

三、定义静态函数:在函数的返回类型加上static关键字,函数即被定义成静态函数。

(1)静态函数只能在本源文件中使用;

(2)在文件作用域中声明inline函数,默认认为static;

说明:静态函数只是在一个普通的全局函数,只不过受static限制,他只能在文件所在的编译单位内使用,不能在其他编译单位内使用。

在C++语言中新增加了俩个作用:定义静态数据成员或定义静态函数成员函数

(1)定义静态成员函数。静态数据成员有如下特点

(1)内存分配:在程序的全局数据区分配;

(2)初始化和定义:静态数据成员函数与类相联系,不与类的对象想联系。静态成员函数不能访问非静态数据成员。原因很简单,非静态数据成员属于特定的类实列,主要用于主要用于对静态数据成员的操作;

(4)静态成员函数和静态数据成员都没有this指针;

(六)static const 定义的变量可不可以定义在被多个.cpp文件引用的头文件中,如果可以?会有什么危害?

可以,但是在每一个源文件中这个变量的地址都不一样,在头文件中定义的static变量会造成变量的多次定义,造成内存空间的浪费,而且也不是真正的全局变量,一个源文件中修改,在另外一个源文件没有影响;

test.h

#pragma oncestatic int g_int =3;

func.h

void func1();void func2();

test1.cpp

#include<stdio.h>#include"test.h"void func1(){printf("test1 :%d\n",&g_int);printf("test1 %d\n",g_int);g_int =4;printf("test1 %d\n",g_int);}

test2.cpp

#include<stdio.h>#include"test.h"void func2(){printf("test2 :%d\n",&g_int);printf("test %d\n",g_int);}

main.cpp

#include"func.h"int main(){func1();func2();}

从这些代码可以验证以上的代码;

(七)讲一下管道

管道能在父,子进程间传递数据,利用的是fork调用之后俩个管道的文件描述符(fd[0]和fd[1]都保持打开)。一对这样的文件描述符只能保证父,子进程间一个方向的数据传输,父进程和子进程必须有一个关闭fd[0],另一个关闭fd[1]。

但是,如果要实现父,子进程之间的双向数据传输,就必须使用俩个管道,socket编程接口提供一个创建全双工管道的系统调用:socketpair。

(八)strtok函数的用法:函数原型:char * strtok(char * s,const char * delim);

(1)strtok函数用来将字符串分割成一个片段,参数s指向欲分割的字符串,参数delim则分割字符串中包含的所有字符。当strtok()在参数s的字符串中发现参数delim中包含的字符,则会将字符修改为'\0'.

(2)在第一次调用时strtok()必须给予参数s字符串,往后的调用则将字符串设置为NULL,每次调用成功则返回指向被分割出片段的指针。

(3)返回值:1.从s开头开始的一个个被分割的串,当s中的字符串查找到末尾时返回NULL。

                        2.如果查找不到delim中的字符时,返回当前strtok的字符串的指针;

                       3.所有delim中包含的字符都会被滤掉,并将滤掉的地方设置为一处分割的结点

(4)strtok会破坏被分解字符串的完整,调用前和调用后s已经不一样了。如果要保持原字符串的完整,可以使用strchr和sscanf的组合等;

(5)strtok来判断ip或者mac的时候务必要先用其他的方法判断'.'或':'的个数,因为用strtok截断的话,比如:"192..168.0...8..."这个字符串,strtok只会截取四次,中间的...无论多少都会被当作一个key。

(6)函数strtok保存string中标记后面的下一个字符的指针,并且返回当前标记的指针,后面调用srtok时,第一个参数为NULL,表示调用strtok继续从s中上次调用strtok时保存的位置开始标记化;

#include<iostream>#include<stdlib.h>#include<vector>using namespace std;int main(){vector<int> vc;char s[] = "192.168.0.12";char *delim=".";char * p = strtok(s,delim);while(p){cout<<p<<".";int n = atoi(p);//可以将字符串转化为整数;vc.push_back(n);p = strtok(NULL,delim);}for(int i =0;i<vc.size();++i)cout<<vc[i]<<" ";cout<<endl;return 0;}

补充:int sscanf ( const char * s, const char * format, ...)Read formatted 

从一个字符串中读入与指定格式相符的数据到指定的内存;

判断一个IP地址的合法性:

#include<iostream>
using namespace std;
bool IsValidlp(char * str)
{
if(str == NULL)
return false;
int a[7];
if(sscanf(str,"%d.%d.%d.%d",&a[0],&a[1],&a[2],&a[3]) != 4)
return false;
for(int i=0;i<4;++i)
{
//cout<<a[i]<<endl;
if(a[i]<0 || a[i] >255)
return false;
}
return true;
}
int main()
{
char * ptr ="127.0.0.1";
cout<<IsValidlp(ptr)<<endl;
}

 atoi(将字符串转换成整型数)
相关函数 atof,atol,atrtod,strtol,strtoul
表头文件 #include<stdlib.h>
定义函数 int atoi(const char *nptr);
函数说明 atoi()会扫描参数nptr字符串,当字符一开始就有非空格或者非数字时,转化得到的数字是0,如果是空格,跳过前面的空格字符,直到遇上数字或正负符号才开始做转换,而再遇到非数字或字符串结束时点:

补充:sscanf函数的用法;
(九)select,poll,epoll这三个函数的有什么区别?

(1)这三个系统调用都能同时监听多个文件描述符,他们将等待由timeout参数指定的起始时间内,一直到一个或者多个文件描述符上有事件发生时返回,返回值是就绪文件描述符的个数。

(2)他们都通过特定类型的结构体变量来告诉内核监听哪些文件买舒服上的哪些事件,并且用这种结构体变量来获取处理的结果;

select:

(1)select的参数类型没有将文件描述符和事件绑定,它只是一个文件描述符集合,select只能处理可读可写异常事件,内核是对fd_set集合的在线修改,应用程序在下次调用select必须重置fd_set集合;

(2)select每次返回的是整个用户注册的事件集合,所以应用程序得轮询的方式找到就绪文件描述符;

poll:

(1)poll的参数类型pollfd,它把文件描述符和事件绑定在一起,并且内核每次修改的是revents,而events成员保持不变,应用程序在下次调用poll时无需重置pollfd类型的参数;

(2)poll每次返回的是用户注册的事件集合,所以应用程序找到就绪文件描述符的时间复杂度也是O(n);

epoll:

(1)epoll在内核中维护一个事件表,每次epoll_wait调用都直接从该事件表中取得用户注册的事件,而无需反复去重置文件描述符,这与select不同。epoll_wait系统调用的events参数仅用于返回就绪的事件,这使得应用程序找到就绪文件描述的时间复杂度是O(1);

(十)函数重载

1、函数重载的定义:在同一个作用域中,可以有一组具有相同的函数名,但是参数列表不同的函数,这一组函数称为函数重载。重载函数通常用来命名一组功能相似的函数,这样能够减少函数名的使用,对程序的可读性有很大的好处。

#include<iostream>using namespace std;void print(int i){        cout<<"print a integer :"<<i<<endl;}void print(string str){        cout<<"print a string :"<<str<<endl;}int main(){        print(12);        print("hello world!");        return 0;}


2、为什么需要函数重载?

如果没有函数重载, 在C中,你必须要为print这个函数起很多名字,比如print_int,print_string,这样一来很多功能相似的函数就必须要起很多名字。在C中可以通过宏定义来实现这一功能;

3、编译器编译了重载函数以后,重载函数的函数名不再是原来的函数名而是根据一条规则:返回值类型+函数名+参数类型。

4.既然返回类型也考虑到映射机制中,这样不同的返回类型映射之后的函数名坑定不一样了,但是为什么不讲函数的返回值类型考虑到函数重载呢?-------这是为了保持解析操作符或函数调用时独立于上下文

5、重载函数变编译后的新名字

我们都知道很多语言都支持函数重载,那么编译器是怎么处理编译后它们的命名冲突的呢?

1、先给出几个正确的重载函数:

#include <iostream>using namespace std;int Add(int a, int b){    return a + b;}double Add(double a, double b){    return a + b;}double Add(double a, int b){    return a + b;}double Add(int a, double b){    return a + b;}void Add(void){    ;}int main(){    int a = 10;    int b = 20;    double d1 = 1.0;    double d2 = 2.0;    Add(a, b);    Add(d1, d2);    return 0;}

2、查看编译后重载函数新名字的方法

a. 在vs2010下面,直接把重载函数都屏蔽了,然后在主函数里调用这些函数,此时会报错,这时我们在错误信息里会看到这些函数的新名字。这种方法比较简单。

b. 在vs2013下面,,先在解决方案里右键你的项目(编译器界面没有解决方案的可以用以下方法调出:点击视图->解决方案资源管理器或直接Ctrl+Alt+L),接着依次点击:属性->配置属性->链接器->调试,找到映射导出一栏,把默认的否改为:是 (/MAPINFO:EXPORTS)即,然后重新编译程序。此时,找到你项目(工程)的位置(在你电脑硬盘上的物理位置),找到Debug文件夹里的后缀为.map的文件,用记事本或其他文档浏览软件打开,在里面可以用编辑->查找的方式,输入你的重载函数名,一直查找,直到找到几个连续的与原函数名字比较相似的新函数名即可,这些就是你的新的函数名。

这里写图片描述

这里写图片描述

这里写图片描述

c. 在Linux下把编译后的文件反汇编来查看

①建立一个.cpp文件,把代码拷贝进去

②编译该文件

③执行命令objdump -d a.out >log.txt反汇编并将结果重定向到log.txt文件中。

这里写图片描述

这里写图片描述

④对生成log.txt文件进行分析。

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

可以看到在log.txt里重载函数的名字变成了对应的这几个,我们很明显的发现在Linux里重载函数在反汇编之后的新名字可以很清楚的看出来。
而且我们可以总结出重载函数在反汇编之后出现的新名字的规律(Z3这里权当是一个作用域标识):
作用域+函数名+参数列表参数类型的首字母

 
原创粉丝点击