C++开发面试基础知识点整理(超详细2)

c++中类成员的访问权限?

c++通过public,protected,private三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的,受保护的,私有的,被称为成员访问限定符。

  • public关键字,该访问属性下数据成员、成员函数是对所有用户开放的,所有用户都可以调用。
  • protected关键字,该访问属性下的成员,派生类和类内部都可以访问,但是对象不可以访问。
  • private,该访问属性下只有类自己可以访问,对象和派生类都不可以访问。

什么是继承?

类的继承就是新的类从已有类那里得到属性和方法。或从已有类产生新类的过程就是类的派生。原有的类称为基类或父类,产生的新类称为派生类。
三种继承方式:
**公有继承(public):**当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可访问。
私有继承(protected):当类的继承为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可访问。
保护继承(private):保护继承中,基类的公有成员以保护成员的方式出现在派生类中,而基类的私有成员不可访问。

继承的特点:
1.子类拥有父类的属性和方法。
2.子类可以拥有父类没有的属性和方法
3.子类可以当作父类对象来使用

哪些函数不能被继承?

构造函数和析构函数不能被继承。不然会造成派生类的成员冗余。

多重继承与虚继承:

一个类有多个直接的继承关系成为多继承。
多继承(菱形继承)存在二义性关系

  • 如果一个派生类从多个基类派生,而这些基类又有一个共同的基类为菱形继承模型,则在对该基类中声明的名字进行访问时,可能产生二义性,即子类会保存两份祖先类的成员。
  • 要解决数据的二义性问题,就要使这个祖先类在派生类只产生一个子对象,在所有子类继承祖先类时加上virtual关键字,祖先类称为虚基类,进行虚继承,指出它希望共享虚基类的状态,这样最终的子类只保存一份祖先类的成员。

类和类之间的关系有哪些?

1.has-A包含关系,即一个类的成员属性是另一个已经定义好的类。
2.use-A使用关系,一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式实现。
3.is-A,继承关系,关系具有传递性。

c++中struct和class的区别?

在c++中,可以用struct和class定义类,都可以继承。
区别在于:struct的默认继承权限和默认访问权限时public,而class的默认继承权限和默认访问权限是private。

struct和union的区别?

  • struct和union都是由多个不同的数据类型成员组成,但在任何同一时刻,union中存放了一个被选中的成员,而struct的所有成员都存在。
  • 内存:在struct中,各成员都占用内存空间,一个struct的变量总长度为所有成员长度之和。union中,所有成员不能同时占用内存空间,其长度为最长的成员长度。
  • 对于union的不同成员赋值,将会对其它成员重写,原来的值就不存在了,而对于struct的不同成员赋值不影响。

类默认的成员函数:

构造函数、拷贝构造函数、析构函数、赋值操作符重载、取地址操作符重载和const修饰的取地址操作符重载。

类什么时候会发生析构?

1.对象生命周期结束,被销毁时。
2.delete指向对象的指针时,或者delete指向对象的基类类型指针,而其基类虚析构函数是虚函数时。
3.对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。

必须在构造函数初始化式里进行初始化的数据成员有哪些?

  1. 常量成员,因为常量只能初始化不能赋值,所以必须放在初始化列表里面
  2. 引用类型,引用必须在定义的时候初始化,并且不能重新赋值,所以也要写在初始化列表里面
  3. 没有默认构造函数的类类型,因为使用初始化列表可以不必调用默认构造函数来初始化,而是直接调用拷贝构造函数初始化

如果一个类,没有任何可用的构造函数,那么c++便会有一个被隐式声明出来的trival(无用的)默认构造函数。那么什么情况下生成有用的默认构造函数?

  1. 带有默认构造函数的类成员对象。如果一个类没有任何构造函数,但它含有一个成员对象,而后者有默认构造函数,那么编译器会为该类合成出一个默认构造函数。不过这个合成操作只有在构造函数真正被需要的时候才会发生。
    2.带有默认构造函数的基类。如果一个没有默认构造函数的派生类派生自一个有着默认构造函数的基类,那么该派生类会合成一个构造函数调用上一层基类的默认构造函数。
    3.带有一个虚函数的类。
    4.带有一个虚基类的类。

说一下类的初始化方式?两者区别?

1.赋值初始化。通过在函数体内进行赋值初始化
2.列表初始化:在冒号后使用初始化列表进行初始化。
区别:
对于函数体内进行初始化工作,是在所有的数据成员被分配内存空间之后才进行的。
列表初始化是给数据成员分配内存空间时就进行初始化。
就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式,那么分配内存空间后在进入函数体之前就给数据成员赋值,此时函数体尚未执行。

为什么列表初始化会更快?

c++的赋值操作是会产生临时对象的,降低程序的而效率。列表初始化没有临时对象。

一个派生类的构造函数的执行顺序:

构造函数的顺序:
1.基类的构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化列表中的顺序。
2.成员类对象的构造函数。如果有多个类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在初始化成员列表中的顺序。
3.派生类的构造函数
析构函数与之相反。

如何让一个类不能实例化?

将类定义为抽象基类或者将构造函数声明为private

C++如何创建一个类,使得他只能在堆或者栈上创建?

1.只能在堆上生成对象:将析构函数设置为私有类型,再用子类来动态创建。原因:C++是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。

2.只能在栈上生成对象:将new 和 delete 设置为私有。
原因:在堆上生成对象,使用new关键词操作,其过程分为两阶段:第一阶段,使用new在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将new操作设置为私有,那么第一阶段就无法完成,就不能够再堆上生成对象。

拷贝构造函数为什么必须传引用而不能传值?

拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归。

拷贝构造函数为什么要用const?

如果在函数中不会改变引用类型的值,加不加const的效果是一样的。而不加const,编译器也不会报错。但是为了整个程序的安全,还是加上const,防止对引用类型的参数值意外修改。

什么是虚函数?

虚函数是指一个类中你希望重载的成员函数,当你用一个基类指针或者引用指向一个继承类对象的时候,调用一个虚函数时,实际调用的是继承类的版本。

什么是c++的多态性?

c++多态分为静态多条和动态多态。
静态多态是通过重载和模板技术实现,在编译时就确定,称为静态联编。
动态多态通过虚函数和继承关系实现,执行动态联编,在运行时才确定。
产生多态的三个条件:继承,要有虚函数重写,要有父类指针或引用指向子类 对象。
多态的实现原理:
(1)当类里声明虚函数时,编译器会在类中生成一个虚函数表
(2)虚函数表是一个存储函数指针的数据结构,由编译器自动生成和维护。
(3)每一个virtual成员函数会被编译器放入虚函数表中。
(4)每一个对象中都有一个存在指向虚函数表的vptr指针,虚函数表的地址在每个对象的首地址。当调用成员函数(虚函数)时,vptr指针查找该虚函数表中该类对应的函数指针进行调用。

多态的目的:

接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,实现代码重用,多态的目的就是为了接口重用。

虚表在什么时候生成?

在c++中,虚函数是在编译时期获知,之后规定不变的。由于程序在执行时,表格的大小和内容都不会改变,所以虚表是在编译时期生成的。类对象的vptr指针是在运行时期确定的。

虚表属于类还是属于对象?存在哪里?加virtual关键字后的内存布局?

虚表属于类中,存放虚函数指针,指针指向虚函数的地址。每个类对象的前4个字节存放着指向该表的虚表指针(在堆区)。虚表存放在常量段中。
在这里插入图片描述

构造函数为什么不能定义为虚函数,析构函数为什么可以?

1.从存储的角度来说,虚函数有着对应的一个vptr指针指向虚表,但是这个虚表指针是存储在对象的内存空间的。假设构造函数的是虚的,就需要用虚表来调用,但是对象还没有实例化,虚表指针没有构造出来,就实现不了虚函数的机制,所以构造函数不能是虚函数。
2.从实现的意义上看,构造函数就是为了提供初始化工作,在对象生命周期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。

析构函数是虚函数是为了防止内存泄漏问题。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。
假设基类是非虚析构函数,当基类指针指向子类对象时就不会触发动态绑定机制,因而只会调用基类的析构函数,而不会调用派生类的析构函数,那么派生类的资源就不会回收产生内存泄漏。
所以析构函数应采用虚析构函数。

构造函数和析构函数可以调用虚函数吗?为什么?

从语法上是可以的,不会报错。但是不提倡在构造函数和析构函数中调用虚函数。
理由:
1.构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或者析构函数中调用虚函数,运行的是为构造函数和析构函数自身类型定义的版本。
2.父类对象会在子类之前进行构造,此时子类部分数据成员还未进行初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编。
3.析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数再调用父类的析构函数。所以在调用基类的析构函数时,派生类的数据成员已经销毁,这个时候再调用子类的虚函数,程序的运行结构不可控。

为什么析构函数必须是虚函数?为什么c++默认的析构函数不是虚函数?

  • 将可能会被继承的父类的析构函数设置为虚函数,可以保证党我们new 一个子类,然后用基类指针指向子类对象,释放基类指针可以释放掉子类的空间,防止内存泄漏。
  • c++默认析构函数不是虚函数的原因是,虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。

构造函数析构函数可抛出异常?

在effective c++中有一条说“尽量不要让异常逃离析构函数”。所以我们应该尽可能的不让构造函数和析构函数抛出异常。
理由如下:
1.c++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。如果在构造函数中发生异常,控制权会转出构造函数之外。因此,在对象b的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。
2.我们可以用auto_ptr智能指针来取代指针类的成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源。
3.如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,c++会调用terminate函数让程序结束。
4.如果异常从析构函数抛出,而且没有在析构函数中进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全的话,就是没有完成他应该执行的每一件事情。

哪些函数不能声明为虚函数?

1.构造函数
2.内联函数。
虚函数用于实现运行时多态,或者成为动态绑定。
而内联函数是在编译器就进行展开,完成函数替换,是静态绑定的。两者是矛盾的,所以不行。
3.静态成员函数。
静态成员属于类,所有的对象都共享,所以与虚函数的意义不相同
4.友元函数和普通函数不能声明为虚函数。因为不能被继承,多态是建立在继承的基础上实现的。
只有类的成员函数才能声明为虚函数。

什么是重载,重写和重定义?

重载:在同一个作用域中,函数名字相同,但参数列表不同的函数。
重写(覆盖):在基类和派生类中,函数名字相同,参数列表相同,且基类必须有virtual关键字
重定义(隐藏):是指派生类的函数屏蔽了与其同名的基类函数:
(1)如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual关键字,基类的函数被隐藏。
(2)如果派生类的函数的名字与基类的函数名相同,参数列表也相同,但基类没有virtual关键字,基类的函数就会被隐藏。

什么是纯虚函数?什么是抽象类?

定义:在很多情况下基类生成对象是不和情理的。为了解决这个问题,引入纯虚函数。给函数名前加virtual 后加=0,是纯虚函数的形式。纯虚函数不能在基类中进行实现,只能在派生类中实现。
抽象类:含有纯虚函数的类称为抽象类。
作用是为一个继承体系提供一个公共的根,为派生类提供操作接口的通用语义。
特点:
1.抽象类不能生成对象。
2.只能作为基类使用,派生类 必须实现基类中的纯虚函数,否则派生类依然是一个抽象类。

抽象基类为什么不能创建对象?

1.因为抽象类的主要作用是将有关的操作作为接口组织在一个继承层次结构中,派生类将具体实现在其基类中作为接口的操作。
2.在很多的情况下,基类本身生成对象是不合情理的。例如:动物作为一个基类可以派生出老虎,孔雀等子类,但动物本身生成对象明显不合情理。

什么是类型萃取?

类型萃取就是在模板的基础上区分内置类型和其他类型。
主要原理就是将内置类型全部进行特化,然后再进行区分。通过区分内置类型和非内置类型,我们可以提高程序的执行效率。
比如对于内置类型我们在拷贝的时候可以使用memmove和memcopy进行拷贝。

C++模板是什么?底层是如何实现的?

模板是c++中泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者公式。
模板定义以template开始,后跟一个模板参数列表,这是一个逗号分隔的一个或多个模板参数的列表。
当实例化模板时,编译器用函数实参来为我们推断模板实参,用推断出的模板实参为我们实例化。生成的版本称为模板的实例。

实现:

1.编译器并不是把函数模板处理成能够处理任意类的函数;编译器从函数模板通过具体类型产生不同的函数; 编译器会对函数模板进行两次编译:在声明的地方对模板本身进行编译,在调用的地方对参数替换后的代码进行编译。 2.这是因为函数模板要被实例化之后才能成为真正的函数,在使用函数模板的源文件中包含模板的头文件,如果giant头文件只有声明,没有定义,那么编译器无法实例化模板,最终导致链接错误。