C++ 经典知识点面试题

29 篇文章 0 订阅

1、指针的优点和缺点

优点:灵活高效

(1)提高程序的编译效率和执行速度(数组下标往下移时,需要使用乘法和加法,而指针直接使用++即可)

(2)通过指针可使用主调函数和被调函数之间共享变量或数据结构,便于实现双向数据通讯。

(3)可以实现动态的存储分配。

(4)便于表示各种数据结构,如结构体,编写高质量的程序。

缺点:容易出错

(1)可能变成野指针,导致程序崩溃

(2)内存泄露

(3)可读性差

2、指针和引用的定义和区别

(1)指针和引用的定义

1)指针:指针是一个变量,存储一个地址,指向内存的一个存储单元;

2)引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。

(2)指针和引用的区别

<1> 从内存分配上来说:

1)指针是一个实体,而引用仅是个别名,即为指针分配内存,而不为引用分配内存空间;

<2> 从指向的内容来说:

2)引用只能在定义时被初始化一次,之后不可变;指针可变;

3)引用不能为空,指针可以为空;

4)const与指针搭配可以表示指针指向指针指向内容是否可变。const与引用搭配只有一种,即来修饰其内容的可读性。由于引用从一而终,不用修饰其指向。

5)指针可以有多级,但是引用只能是一级(int **p;合法,而int &&a是不合法的)

<3> 其他方面

6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

7)指针和引用的自增(++)运算意义不一样;

指针和引用在符号表中的形式:程序在编译时分别将指针和引用添加到符号表上。在符号表上记录的是变量名及变量所对应地址。在符号表上,指针变量对应的地址值为指针变量的地址值,而引用对应的地址值是引用对象的地址值。符号表生成后就不会再改,因此指针可以改变指向的对象(指针变量中的值可以改),而引用对象不能改。

3、malloc/free 和 new/delete相关的面试题

1):C++有了malloc/free 为什么还要new/delete?

(1)malloc/free只在申请空间时,它们只需要申请空间,无法对空间进行操作。

(2)而在创建C++的对象时,不仅仅是需要申请空间,还需要自动调用构造函数,以及在对象消亡之前要自动执行析构函数。

因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete

根据上面两点,我们可以知道malloc/free 是不能满足C++的需要的,因此需要new/delete。

即在创建对象时,能分配内存空间且初始化内存 + 销毁对象时,能回收空间且对内存进行清理的new/delete。

2)为什么malloc/free在申请空间时,它们只能申请空间,无法对空间进行操作?

因为:malloc/free 是库函数,使用它需要头文件,在一定程度上是独立于语言的。编译器在处理库函数时,编译器不需要知道它是做啥的,而仅仅需要对该函数进行编译,并且保证调用函数时的参数和返回值是合法的,并生成相应 call 函数的代码就ok了。编译器不会控制库函数做一些操作,比如调用构造函数和析构函数。因此malloc/free无法满足动态生成对象的要求。

3)为什么new/delete 在申请空间时,它们不仅能申请空间,还能调用构造函数或析构函数对对空间进行操作?

因为:new/delete是运算符,它与+-*/的地位 是一样的。编译器看到new/delete时,就知道只要它要做啥操作,并生成对应的代码。

4):malloc/free  和 new/delete 的相同点和不同点

相同点:它们都可以申请和释放空间。

不同点:

一、new/delete 在申请空间的时候能对空间进行操作,而malloc/free 不能。

(1)new :分配内存 + 调用类的构造函数 + 初始化  delete:释放内存 + 调用类的析构函数

(2)malloc:只分配内存,不会进行初始化类成员的工作   free只释放内存,不会调用析构函数

二、new/delete是C++运算符,能重载

(1)new、delete 是运算符,可以进行重载

(2)malloc,free是标准库函数,不可以进行重载,但可以覆盖。

三、new delete 更加安全,简单:

(1)不用计算类型大小:自动计算要分配存储区的字节数

(2)不用强制类型转换:自动返回正确的指针类型(二者返回值不同,一个为void*,一个是某种数据类型指针

四、new可以分配一个对象或对象数组的存储空间,malloc不可以

五、new和delete搭配使用,malloc和free搭配使用:混搭可能出现不可预料的错误

六、new后执行的三个操作:(某面试题目)

(1)new的类分配内存空间。

(2)调用类的构造方法。

(3)返回该实例(对象)的内存地址

4、构造函数、析构函数与虚函数的关系

(1)为什么构造函数不能是虚函数?

简单点说,构造函数的调用是发生多态的前提(多态有对象指针引发)。

(2)为什么在派生类中的析构函数常常为虚析构函数?

简单点说,发生多态时,防止漏掉调用派生类的析构函数。

(3)把所有的类的析构函数都设置为虚函数好吗?

简单点说,不好,会造成时间和空间浪费。

体答案在博客构造函数、析构函数与虚函数的关系中。

5、C++中,哪些函数不可以被声明为虚函数

总的来说,共有五种,普通函数(非成员函数)、构造函数、内联函数静态函数、友元函数。

首先说明两点:

(1)虚函数是为了实现多态,而多态是属于动态联编,在运行时确定调用哪个函数。

(2)虚函数调用时,类之间需要有公有继承 +继承关系基类指针引用调用

具体解释

(1)普通函数为啥不能是虚函数?

原因:多态是依托于类的,要声明的多态的函数前提必须是虚函数。

(2)构造函数为啥不能是虚函数?

原因:多态是依托于类的,多态的使用必须是在类创建以后,而构造函数是用来创建构造函数的,所以不行。

具体的原因:虚表指针的初始化时在构造函数进行的,而虚函数需要放到虚表中。在调用虚函数前,必须首先知道虚表指针,此时矛盾就出来了。

(3)内联函数为啥不能是虚函数?

原因:内联函数属于静态联编,即内联函数是在编译期间直接展开,可以减少函数调用的花销,即是编译阶段就确定调用哪个函数了。但是虚函数是属于动态联编,即是在运行时才确定调用哪一个函数。显然这两个是冲突的。

(4)静态函数为啥不能使虚函数?

原因:

<1>从技术层面上说,静态函数的调用不需要传递this指针。但是虚函数的调用需要this指针,来找到虚函数表。相互矛盾

<2>从存在的意义上说,静态函数的存在时为了让所有类共享。可以在对象产生之前执行一些操作。与虚函数的作用不是一路的。

(5)友元函数为啥不能是虚函数?

原因:C++不支持友元函数的继承,不能继承的函数指定不是虚函数。

6、能做switch()的参数类型是:

能自动转换为整形(int)且转换过程中不存在精度损失的类型

具体包括:byte,short,char,int. 但是不包括:long,string,double,float等。

7、堆栈溢出一般是由什么原因导致的?

(1)没有回收垃圾资源

(2)递归调用的层次太深

8、C++中的空类,默认产生哪些类成员函数?

(1)默认的构造函数

(2)默认的拷贝构造函数

(3)默认的赋值函数

(4)默认的析构函数

(5)默认的取地址运算符(不带const)

(6)默认的取地址运算符(带const)

  1. class Empty  
  2. {  
  3. public:  
  4.     Empty();                          // 缺省构造函数  
  5.     Empty( const Empty& );            // 拷贝构造函数  
  6.     ~Empty();                         // 析构函数  
  7.     Empty& operator=( const Empty& ); // 赋值运算符  
  8.     Empty* operator&();               // 取址运算符  
  9.     const Empty* operator&() const;   // 取址运算符 const  
  10. };  

9、面向对象的四个特性:

(1)抽象性:是对事物的抽象概括描述,继而将客观事物抽象成类,从而实现了客观世界向计算机世界的转化。

(2)封装性:把客观事物封装成抽象的类,并且只让可信的类活对象进行访问,而对其他成员进行信息隐藏。

(3)继承性:新产生的类无需重新编写现有类的代码,而直接使用现有类的功能,继而对新类进行扩展。

(4)多态性:允许基类指针指向各种各样的派生类对象,之后能够根据其指向的派生类对象,做出不同的反应(该反应为虚函数实现的)。即多态的出现,就是给出一个统一处理各种各样的派生类的方法。比如可以定义一个数组,数组类型为基类指针类型,放的可以是各种派生类,之后可以使用基类指针对数组元素统一处理。

注:有书上说是四个,有书上说是三个,不是很统一。

10、简述strcpy与memcpy的相同点和区别点:

相同点:strcpy与memcpy都可以实现拷贝的功能

不同点:

(1)实现功能不同,strcpy主要实现字符串变量间的拷贝,memcpy主要是内存块间的拷贝。

(2)操作对象不同,strcpy的操作对象是字符串,memcpy 的操作对象是内存地址,并不限于何种数据类型。

(3)执行效率不同,memcpy最高,strcpy次之。

11、内存分配方式

一个C、C++程序编译时内存分为5大存储区:全局区、栈区、堆区、文字常量区、程序代码区。

(1) 在静态存储区域分配

控制者:编译器

分配时间:在程序编译的时候分配内存

释放时间:在程序的整个运行期间都存在,程序结束后由OS释放

内容:全局变量,static变量

特点:

0、速度快,不易出错。

1、初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和静态变量在另一块区域

2、定义后,变量的值可以改变

(2) 在栈上创建

控制者:由编译器自动分配释放

分配时间:在程序运行(执行函数)的时候分配内存

释放时间:在函数执行结束时,释放存储单元自动被释放。

举例: 局部变量,函数参数

特点:

1、栈内存分配运算 内置于处理器的指令集中,分配效率很高,但是分配的内存容量有限。

2、定义后,变量的值可以改变

(3) 从堆上分配

控制者:程序员一般由程序员分配和释放,。

分配时间:在程序运行(遇见new或malloc)的时候分配内存。

释放时间:程序员自己决定,若程序员不释放,程序结束时可能由OS回收,但是程序运行期间不释放的内存属于内存泄露。

举例:使用new 和 malloc申请的空间

特点:

0、频繁地分配和释放不同大小的堆空间将会产生堆内碎块

1、程序员使用malloc 或new 申请任意多少的内存,自己负责在何时用free 或delete 释放内存,否则会造成内存泄露。

2、定义后,变量的值可以改变

(4) 文字常量区

控制着:编译器

分配时间:在程序编译的时候分配内存

释放时间:程序结束后由系统释放

举例:常量字符串

特点:定义后,变量的值不可以 改变,只读的

(5) 程序代码区

内容:存放函数体的二进制代码

12.堆内存与栈内存的比较

(1)申请方式

栈:由系统分配,

堆:有程序员显式分配,malloc和new

(2)申请大小的限制

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈内存的地址和大小是系统预先规定好的,而且能从栈申请的大小很小在WINDOWS下,栈的大小是2M,如果申请的空间超过栈的剩余空间时,将提示overflow

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

(3)申请后系统的响应

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

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

(4)申请效率的比较

栈:由系统自动分配,速度较快。但程序员是无法控制的。
堆:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

(5)堆和栈中的存储内容

栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的 下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
(6)存取效率的比较

栈:存取速度快

堆:存取速度慢

13、如何判断程序中堆和栈增长方向?

栈的生长方式是向下的,向着内存地址减小的方向增长。

堆的生长方式是向上的,向着内存地址增加的方向增长。

怎么判断堆和栈的增长方向呢,其实就是依次申请两个变量,判断两个变量的地址大小。

判断栈的增长方式:

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. void Func(int a)  
  5. {  
  6.     int b = 1;  
  7.     cout<<"&a = "<<&a<<endl; //0012FE8C  
  8.     cout<<"&b = "<<&b<<endl; //0012FE7C  
  9. }  
  10.   
  11. int main()  
  12. {  
  13.     int a = 1;  
  14.     Func(a);  
  15.     system("pause");  
  16.     return 1;  
  17. }  
分析:在函数Func中,变量a和b都是局部变量,都是在栈上申请的,而且肯定是先申请变量a的内存,后申请b的内存。

此时可以直接比较a和b的地址即可。

根据程序的注释可以知道a的地址大于b的地址,且a先于b申请,即在栈中,先申请的内存地址大,后申请的内存地址小。

从而得出,栈内存的增长方式是由高地址到低地址的。

判断堆的增长方式:

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. void Func(int* a)  
  5. {  
  6.     int* b = new int;  
  7.     cout<<"a = "<<a<<endl; //00395B80  
  8.     cout<<"b = "<<b<<endl; //00395BB0  
  9. }  
  10.   
  11. int main()  
  12. {  
  13.     int* a = new int;  
  14.     Func(a);  
  15.     system("pause");  
  16.     return 1;  
  17. }  
分析: 在函数Func中,变量a和b中存放的都是堆内存,而且肯定是先申请变量a的内存,后申请b的内存。

此时可以直接比较a和b的地址即可。

根据程序的注释可以知道a的地址小于b的地址,且a先于b申请,即在堆中,先申请的内存地址小,后申请的内存地址大。

从而得出,堆内存的增长方式是由低地址到高地址的。

13、class 与 struct的区别

C++对struct进行了扩充,使得struct不仅仅包含变量,还可以包含成员函数、能继承、能重载,能实现多态。

在c++中,struct和class的区别主要在于: 

(1)默认访问权限不同。

struct:变量和函数默认的访问权限是public,而class默认的访问权限为private。

(2)默认继承的访问权限不同。

struct:继承权限默认为public,而class默认的继承权限为private。

(3)class这个关键字可用于定义模板参数,但关键字struct不可以用于定义模板参数。

14、STL的set用什么实现的?为什么不用hash?

set是由红黑树实现的,红黑树是一个平衡二叉查找树,用来检测某个元素是否在集合中出现。

如果使用hash实现时,需要为不同类型的数据编写哈希函数,而利用红黑树只需要为不同类型重载operator<就可以了。

15、列举面向对象设计的三个基本要素和五种主要设计原则。

(1)三个基本要素:继承、封装、多态
(2)主要设计原则:单一职责原则、里氏代换原则、依赖倒置原则、接口隔离原则、迪米特原则、开放-封闭原则。

16、分析++it和it++的优劣 - 巨人网络2014校招

  1. 1、for(iterator it = V.begin(); it != V.end(); ++it)    
  2. 2、for(iterator it = V.begin(); it != V.end(); it++)   
分析:

(1)这两个式子的结果肯定是一样的。

(2)俩式子效率不同。++it返回的是对象引用,而it++返回的是临时的对象。由于it是用户自定义类型的,编译器无法对其进行优化,即无法直接不生成临时对象,进而等价于++it,所以每进行一次循环,编译器就会创建且销毁一个无用的临时对象。

(3)对于int i,i++ 和 ++i 在release下,二者效率是等价的,因为编译器对其进行优化了。

17、分析下面代码,回答问题

  1. #include <iostream>  
  2. using namespace std;  
  3.   
  4. class Test  
  5. {  
  6. public:  
  7.     void print()  
  8.     {  
  9.         cout<<"a::print null"<<endl;  
  10.     }  
  11.     void set(int v)  
  12.     {  
  13.         m_val = v;  
  14.         cout<<"a::set val = %d"<<endl;  
  15.     }  
  16. private:  
  17.     int m_val;  
  18. };  
  19.   
  20. int main()  
  21. {  
  22.     Test* a = NULL;  
  23.     a->print();  //这个函数的调用结果是什么?  
  24.     a->set(100); //这个函数的调用结果是什么?  
  25.     system("pause");  
  26.     return 1;  
  27. }  
分析:

a->print():输出a::print null

a->set():没有输出,程序直接崩溃。

原因:

(1)a只是一个类Test的指针,程序并未为其生成Test类型的内存。但是a仅仅调用print函数,该函数没有对内存进行读写,而且该函数属于类共享的,所以从编译器的角度上,调用也是没问题的。

(2)a只是一个类Test的指针,程序并未为其生成Test类型的内存,所以m_val是没有内存空间的,因此为其赋值就相当于为null赋值。由于地址为null的位置其实是0x0000,这是一个特殊的内存,只能读不能写,所以当为该内存赋值时,会崩溃。

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值