1.谈谈你对C++内存分配的理解
1.1 还是的先看看C++对内存分为哪几个区?
1、栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。想知道为什么效率高吗?因为关于栈的操作如push集成在处理器的指令集中,效率很高,但是分配的内存容量有限。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
4、文字常量区—常量字符串就是放在这里的。
5、程序代码区—存放函数体的二进制代码。
3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
4、文字常量区—常量字符串就是放在这里的。
5、程序代码区—存放函数体的二进制代码。
1.2 再给面试官谈谈new/delete和malloc/free的区别?
1、像我们的new/delete和malloc/free就是在上面所说的堆区上操作。程序员申请了内存,用完要记得释放,否则就内存泄露了。而且多次申请释放,会造成堆区碎片,这个需要注意下。
2、new/delete是操作符,而malloc/free是函数。前者可以被重载。前者可以有构造函数和析构函数。前者返回的某种类型对象的指针,后者返回VOID指针。
2.基于问题1,深入谈谈堆和栈
1、申请方式:
栈:函数内的局部变量:int a = 10;
堆:new / malloc
2、申请后的系统响应:
栈:只要申请的空间大小<栈的剩余空间,栈就分配。
堆:因为new,malloc都是C++封装的,里面做了这样的事:首先明白windows有这么一个记录空闲空间地址的链表,C++遍历该链表,先找到第一个空闲空间大小大于程序员申请的空间大小的地址(堆节点),将该堆节点从链表中删除,把该节点的空间分配给程序。对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
3、申请大小:
栈:默认是1M?还是2M?
堆:看系统的虚拟内存有多大了。(请记住堆是一个节点为空闲内存空间的链表。。)堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
4、申请效率:
栈:速度快,push,pop等指令都内置在处理器了,能不快吗?
堆:相对慢(为什么慢,可以看2,就知道它在申请的时候干了多少事),还容易产生内存碎片。不过容量大,操作方便。
5、存储内容:
栈(依次是):
主函数中后的下一条指令地址
然后是函数的各个参数(在大多数的C编译器中,参数是由右往左入栈的)
函数中的局部变量
堆:程序员随意安排。
6、存取效率:
先看看这段代码
int main(){ char a = 1; char c[] = "1234567890"; char *p ="1234567890"; a = c[1]; a = p[1]; return 0; }
这是在函数内,char数组c,是在栈上。
char *p = "1234567890" ,其中虽然*p也是在栈上,但是"1234567890"在常量区。
再看看它们对应的汇编代码:
10: a = c[1]; 00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh] 0040106A 88 4D FC mov byte ptr [ebp-4],cl 11: a = p[1]; 0040106D 8B 55 EC mov edx,dword ptr [ebp-14h] 00401070 8A 42 01 mov al,byte ptr [edx+1] 00401073 88 45 FC mov byte ptr [ebp-4],al
可以看到在栈上的c[]只要两行就能取到内容,而p要先去把常量区的内容放到edx中,再存取。说明纯栈操作,效率是要高一些的。
7、总结
看看一个经典代码就什么都懂了:
- //main.cpp
- int a = 0; //全局初始化区
- char *p1; //全局未初始化区
- main()
- {
- int b; //栈
- char s[] = "abc"; //栈
- char *p2; //栈
- char *p3 = "123456"; //123456\0在常量区,p3在栈上。
- static int c =0; //全局(静态)初始化区
- p1 = (char *)malloc(10);
- p2 = (char *)malloc(20);
- //分配得来得10和20字节的区域就在堆区。
- strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
- }
- //main.cpp
- int a = 0; //全局初始化区
- char *p1; //全局未初始化区
- main()
- {
- int b; //栈
- char s[] = "abc"; //栈
- char *p2; //栈
- char *p3 = "123456"; //123456\0在常量区,p3在栈上。
- static int c =0; //全局(静态)初始化区
- p1 = (char *)malloc(10);
- p2 = (char *)malloc(20);
- //分配得来得10和20字节的区域就在堆区。
- strcpy(p1, "123456"); //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
- }
3.谈谈这strcpy,memcpy和sprintf三者的区别?
共同点:都是拷贝东西。
效率排行: memcpy > strcpy > sprintf
操作对象:memcpy是两块内存之间的操作,strcpy 是两个字符串对象之间的操作,sprintf是任意类型转到字符串的操作。
4.C++多态机制
先上代码:
- #include <iostream.h>
- class animal
- {
- public:
- void sleep()
- {
- cout<<"animal sleep"<<endl;
- }
- void breathe()
- {
- cout<<"animal breathe"<<endl;
- }
- };
- class fish:public animal
- {
- public:
- void breathe()
- {
- cout<<"fish bubble"<<endl;
- }
- };
- int main()
- {
- fish fh;
- animal *pAn=&fh;
- pAn->breathe();
- return 0;
- }
- #include <iostream.h>
- class animal
- {
- public:
- void sleep()
- {
- cout<<"animal sleep"<<endl;
- }
- void breathe()
- {
- cout<<"animal breathe"<<endl;
- }
- };
- class fish:public animal
- {
- public:
- void breathe()
- {
- cout<<"fish bubble"<<endl;
- }
- };
- int main()
- {
- fish fh;
- animal *pAn=&fh;
- pAn->breathe();
- return 0;
- }
内存模式:
子类fish的内存模型是,先animal,然后再自己增加的部分。因为:
animal *pAn=&fh;
fish对象被强制更改成父类animal对象,指针指向fish的上半部分(那不就是一个animal对象了吗?),所以pAn->breathe()就是父类的breathe()。
更深入的了解机制(早绑定和晚绑定):
早绑定:C++在编译的时候,要确定每个对象调用的函数的地址,这就是早绑定。
晚绑定:编译的时候先不决定,等到运行的时候,再根据情况确定要调用的函数地址,这就是晚绑定。
什么东西支持晚绑定这个机制呢?虚函数!
虚函数机制,每个类对象有个虚表指针:vptr,这个指针指向类所属的虚表:vtable。
当我们把父类animal的breathe()设置为虚函数,子类fish即使被强制转换成animal对象,也是没关系的。
因为pAn实际指向的就是fish对象!
因此vptr指向的是fish的vtable!
所以vptr指向的vtable中的breathe()函数就是fish对象的breathe()函数。
正确的多态代码:
- #include <iostream.h>
- class animal
- {
- public:
- void sleep()
- {
- cout<<"animal sleep"<<endl;
- }
- virtual void breathe()
- {
- cout<<"animal breathe"<<endl;
- }
- };
- class fish:public animal
- {
- public:
- void breathe()
- {
- cout<<"fish bubble"<<endl;
- }
- };
- int main()
- {
- fish fh;
- animal *pAn=&fh;
- pAn->breathe();
- return 0;
- }
- #include <iostream.h>
- class animal
- {
- public:
- void sleep()
- {
- cout<<"animal sleep"<<endl;
- }
- virtual void breathe()
- {
- cout<<"animal breathe"<<endl;
- }
- };
- class fish:public animal
- {
- public:
- void breathe()
- {
- cout<<"fish bubble"<<endl;
- }
- };
- int main()
- {
- fish fh;
- animal *pAn=&fh;
- pAn->breathe();
- return 0;
- }
运行结果:
再一次深入了解vptr和vtable:
每个含有virtual function的类中都隐式包含着:
一个静态虚指针vfptr指向该类的静态虚表vtable
vtable中的表项指向类中的每个virtual function的入口地址
每个类内部都有一个虚表,无论类型怎么被转换,虚表指针vptr都是固定的。(fish被转换成animal也无所谓,vptr指向的永远是fish的vtable)
关于虚表:
1、每一个类都有虚表。
2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
5.C++的虚函数和纯虚函数
1、后者不可实例化:虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类(抽象类不可实例化)。
2、后者不可被直接使用:虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
3、两者都是可以被重载,体现多态:虚函数和纯虚函数都可以在子类中被重载(虚函数可以不被重载,纯虚函数必须在子类实现),以多态的形式被调用。
4、形式上:virtual{};纯虚函数的定义形式:virtual { } = 0;在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。(vitrual 和 static 就是死对头)
5、意义上:定义纯虚函数就是为了让基类不可实例化(没意义),因为实例化这样的抽象数据结构本身并没有意义或者给出实现也没有意义。
个人感觉,很多时候,基类,它能够实例化对象就是个不合理的存在,比如:动物,你给我实例化一个动物看看?
6.引用ref
1、什么是“引用”?声明和使用“引用”要注意哪些问题?
引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。声明一个引用的时候,切记要对其进行初始化。
引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。
声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
2、将“引用”作为函数参数有哪些特点?
(1)传递引用给函数与传递指针的效果是一样的。
这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;(无需副本)
而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
(3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。(阅读障碍啊)
3、什么时候用“常引用"?
const 类型标识符 &引用名=目标变量名;
既要保证效率,又要保证传递的数据不能被改变。
- int a ;
- const int &ra=a;
- ra=1; //错误
- a=1; //正确
- int a ;
- const int &ra=a;
- ra=1; //错误
- a=1; //正确
- string foo( );
- void bar(string & s);
- bar(foo( )); //错误
- bar("hello world");//错误
- string foo( );
- void bar(string & s);
- bar(foo( )); //错误
- bar("hello world");//错误
4、将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?
(1)不能返回局部变量的引用。
主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2)不能返回函数内部new分配的内存的引用。
虽然不存在局部变量的被动销毁问题,可是有出现了其他问题:
被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
(3)流操作符重载返回值声明为引用 【必须用引用】
估计就是C++引入”引用“这个概念主要原因吧。
流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << "hello" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。
为什么不是流对象呢?
每次返回值都是一个流对象的话,都要一次拷贝构造函数,每次的<< 都是不同的流对象,从开销和效率上看,这显然不可取。
为什么不是流对象指针呢?
指针能<<连续两次吗?如果针对流对象能的话,要修改整个指针机制,这显然不可取。
所以流对象引用,才是唯一可取的方法!
(4)赋值操作符 = 返回值也是声明为引用【必须用引用】
因为它和流操作符一样,都可以连续操作: x = y = 1。用引用,很科学。理由如(3)。
(5)四则运算符 + - * / 的返回值不能使引用【必须不能用引用】
主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:
返回一个对象、
返回一个局部变量的引用、
返回一个new分配的对象的引用、
返回一个静态对象引用。
根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。
所以可选的只剩下返回一个对象了。
5、引用与多态的关系?
引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。
- Class A;
- Class B:Class A{...};
- B b;
- A& ref = b;
- Class A;
- Class B:Class A{...};
- B b;
- A& ref = b;
6、引用与指针的区别?
指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。
程序中使用指针,程序的可读性差;
而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
此外,就是上面提到的对函数传ref和pointer的区别。
参考资料:http://blog.csdn.net/wfwd/archive/2006/05/30/763551.aspx
7.实现strcpy
- char *strcpy(char *strDest, const char *strSrc)
- {
- if ( strDest == NULL || strSrc == NULL)
- return NULL ;
- if ( strDest == strSrc)
- return strDest ;
- char *tempptr = strDest ;
- while( (*strDest++ = *strSrC++) != ‘\0’)
- ;
- return tempptr ;
- }
- char *strcpy(char *strDest, const char *strSrc)
- {
- if ( strDest == NULL || strSrc == NULL)
- return NULL ;
- if ( strDest == strSrc)
- return strDest ;
- char *tempptr = strDest ;
- while( (*strDest++ = *strSrC++) != ‘\0’)
- ;
- return tempptr ;
- }
8.实现String类
一直都有这么一个印象:拷贝构造函数和赋值函数是一对好基友。
- class String
- {
- public:
- String(const char *str = NULL); // 通用构造函数
- String(const String &another); // 拷贝构造函数
- ~ String(); // 析构函数
- String & operater =(const String &rhs); // 赋值函数
- private:
- char *m_data; // 用于保存字符串
- };
- String::String(const char *str)
- {
- if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
- {
- m_data = new char[1] ;
- m_data[0] = '\0' ;
- }
- else
- {
- m_data = new char[strlen(str) + 1];
- strcpy(m_data,str);
- }
- }
- String::String(const String &another) //拷贝构造函数
- {
- m_data = new char[strlen(another.m_data) + 1];
- strcpy(m_data,other.m_data);
- }
- String& String::operator =(const String &rhs) //赋值函数
- {
- if ( this == &rhs)
- return *this ;
- delete []m_data; //删除原来的数据,新开一块内存
- m_data = new char[strlen(rhs.m_data) + 1];
- strcpy(m_data,rhs.m_data);
- return *this ;
- }
- String::~String()
- {
- delete []m_data ;
- }
- class String
- {
- public:
- String(const char *str = NULL); // 通用构造函数
- String(const String &another); // 拷贝构造函数
- ~ String(); // 析构函数
- String & operater =(const String &rhs); // 赋值函数
- private:
- char *m_data; // 用于保存字符串
- };
- String::String(const char *str)
- {
- if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
- {
- m_data = new char[1] ;
- m_data[0] = '\0' ;
- }
- else
- {
- m_data = new char[strlen(str) + 1];
- strcpy(m_data,str);
- }
- }
- String::String(const String &another) //拷贝构造函数
- {
- m_data = new char[strlen(another.m_data) + 1];
- strcpy(m_data,other.m_data);
- }
- String& String::operator =(const String &rhs) //赋值函数
- {
- if ( this == &rhs)
- return *this ;
- delete []m_data; //删除原来的数据,新开一块内存
- m_data = new char[strlen(rhs.m_data) + 1];
- strcpy(m_data,rhs.m_data);
- return *this ;
- }
- String::~String()
- {
- delete []m_data ;
- }
9.#include<file.h> 与 #include "file.h"的区别?
答:前者是从Standard Library的路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h。
10.C++用C编译器编译后的函数为什么要加入extend "C"?
两者编译机制不一样,为了能够和谐工作,extend "c"告诉C++编译器,这里按照C编译器的处理方式编译链接就行了。
一句话:实现C++与C及其它语言的混合编程。
(我在JNI的时候用到这玩意~)
为什么?
作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。
函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。
void foo( int x, int y );
该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。
现在假设模块A是C语言编译后的,模块B是C++模块。B中调用A的函数。
这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;
它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
11.面向对象的三个基本特征,并简单叙述之?
1. 封装:
2. 继承:
广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。
3. 多态:
是将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。
简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。
12. 重载(overload)和重写(overried)的区别?
从定义上来说:
重写:是指子类重新定义父类虚函数的方法。
从实现原理上来说:
重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。
13. 如何判断一段程序是由C 编译程序还是由C++编译程序编译的?
- #ifdef __cplusplus
- cout<<"C++";
- #else
- cout<<"c";
- #endif
- #ifdef __cplusplus
- cout<<"C++";
- #else
- cout<<"c";
- #endif