> 文档中心 > C++面试常问基础总结梳理

C++面试常问基础总结梳理

本文对C++在使用中、面试中经常会被问到的点做了梳理,可当作日常开发过程中的工具书,亦可当作面试准备资料,欢迎大家交流~~

1. 友元函数

(1) 友元概念?

友元是C++提供的一种破坏数据封装和数据隐藏的机制。

通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本是被隐藏的信息。

可以使用友元函数和友元类。

为了确保数据的完整性,及数据封装与隐藏的原则,建议尽量不使用或少使用友元。

(2) 如何使用?

友元函数是在类声明中由关键字friend修饰说明的非成员函数,在它的函数体中能够通过对象名访问 private 和 protected成员。

(3) 作用

增加灵活性,使程序员可以在封装和快速性方面做合理选择。

访问对象中的成员必须通过对象名。

2. const用法

常类型的对象必须进行初始化,而且不能被更新。

常引用:被引用的对象不能被更新。

const 类型说明符 &引用名

常对象:必须进行初始化,不能被更新。

类名 const 对象名

常数组:数组元素不能被更新。

类型说明符 const 数组名[大小]…

常指针:指向常量的指针。

3. 调用规则

构造函数和析构函数的构造规则:

① 派生类可以不定义构造函数的情况

当具有下述情况之一时,派生类可以不定义构造函数:

基类没有定义任何构造函数。

基类具有缺省参数的构造函数。

基类具有无参构造函数。

② 派生类必须定义构造函数的情况

当基类或成员对象所属类只含有带参数的构造函数时,即使派生类本身没有数据成员要初始化,它也必须定义构造函数,并以构造函数初始化列表的方式向基类和成员对象的构造函数传递参数,以实现基类子对象和成员对象的初始化。

③ 派生类的构造函数只负责直接基类的初始化

C++语言标准有一条规则:*如果派生类的基类同时也是另外一个类的派生类,则每个派生类只负责它的直接基类的构造函数调用*

这条规则表明当派生类的直接基类只有带参数的构造函数,但没有默认构造函数时(包括缺省参数和无参构造函数),它必须在构造函数的初始化列表中调用其直接基类的构造函数,并向基类的构造函数传递参数,以实现派生类对象中的基类子对象的初始化。

这条规则有一个例外情况,当派生类存在虚基类时,所有虚基类都由最后的派生类负责初始化。

总结:

(1)当有多个基类时,将****按照*它们在继承方式中的*声明次序调用****,与它们在构造函数初始化列表中的次序无关。当基类A本身又是另一个类B的派生类时,则先调用基类B的构造函数,再调用基类A的构造函数。

(2)当有多个对象成员时,将按它们在派生类中的****声明次序调用****,与它们在构造函数初始化列表中的次序无关。

(3)当构造函数初始化列表中的基类和对象成员的构造函数调用完成之后,才执行派生类构造函数体中的程序代码。

4. 构造函数与析构函数

(1) 类对象成员的构造

先构造成员、再构造自身(调用构造函数)

(2) 派生类构造函数

派生类可能有多个基类,也可能包括多个成员对象,在创建派生类对象时,派生类的构造函数除了要负责本类成员的初始化外,还要调用基类和成员对象的构造函数,并向它们传递参数,以完成基类子对象和成员对象的建立和初始化。

*派生类只能采用构造函数初始化列表的方式向基类或成员对象的构造函数传递参数*,形式如下:

派生类构造函数名(参数表):基类构造函数名(参数表),成员对象名1(参数表),…{  //……}

(3) 构造函数和析构函数调用次序

派生类对象的构造: 先构造基类、再构造成员、最后构造自身(调用构造函数)。

基类构造顺序由派生层次决定:*最远的基类最先构造*

成员构造顺序和定义顺序符合。

析构函数的析构顺序与构造相反。

5. 基类与派生类关系

基类对象与派生类对象之间存在赋值相容性。包括以下几种情况:

(1) 把派生类对象赋值给基类对象;

(2) 把派生类对象的地址赋值给基类指针;

(3) 用派生类对象初始化基类对象的引用。

反之则不行,即****不能把基类对象赋值给派生类对象****;不能把基类对象的地址赋值给派生类对象的指针;也不能把基类对象作为派生对象的引用。

6. 继承访问权限

(1) 公有继承

① 基类中protected的成员

类内部:可以访问

类的使用者:不能访问

类的派生类成员:可以访问

② 派生类不可访问基类的private成员

③ 派生类可访问基类的protected成员

④ 派生类可访问基类的public成员

(2) 私有继承

派生类不可访问基类的任何成员与函数。

(3) 保护继承

派生方式为protected的继承称为保护继承,在这种继承方式下,基类的public成员在派生类中会变成protected成员,基类的protected和private成员在派生类中保持原来的访问权限。

注意点:当采用保护继承的时候,由于public成员变为protected成员,因此类的使用者不可访问!而派生类可访问!通俗点,即:基类中protected的成员:

类内部:可以访问;

类的使用者:不能访问;

类的派生类成员:可以访问。

(4) 派生类对基类成员的访问形式

① 通过派生类对象直接访问基类成员

② 在派生类成员函数中直接访问基类成员

③ 通过基类名字限定访问被重载的基类成员名

7. 虚拟继承

多继承下的二义性:在多继承方式下,派生类继承了多个基类的成员,当两个不同基类拥有同名成员时,容易产生名字冲突问题。

虚拟继承引入的原因:重复基类,派生类间接继承同一基类使得间接基类(Person)在派生类中有多份拷贝,引发二义性。

(1) 虚拟继承virtual inheritance的定义

语法

class derived_class : virtual […] base_class

虚基类virtual base class

被虚拟继承的基类

在其所有的派生类中,仅出现一次

(2) 虚拟继承的构造次序

虚基类的初始化与一般的多重继承的初始化在语法上是一样的,但构造函数的调用顺序不同;若基类由虚基类派生而来,则派生类必须提供对间接基类的构造(即在构造函数初始列表中构造虚基类,无论此虚基类是直接还是间接基类)

调用顺序的规定:

先调用虚基类的构造函数,再调用非虚基类的构造函数。若同一层次中包含多个虚基类,这些虚基类的构造函数按它们的说明的次序调用;若虚基类由非基类派生而来,则仍然先调用基类构造函数,再调用派生类构造函数。

(3) 虚基类由最终派生类初始化

在没有虚拟继承的情况下,每个派生类的构造函数只负责其直接基类的初始化。但在虚拟继承方式下,虚基类则由最终派生类的构造函数负责初始化。

在虚拟继承方式下,若最终派生类的构造函数没有明确调用虚基类的构造函数,编译器就会尝试调用虚基类不需要参数的构造函数(包括缺省、无参和缺省参数的构造函数),如果没找到就会产生编译错误。

8. 虚函数、纯虚函数和抽象类

(1) 多态性

多态性:多态就是在同一个类或继承体系结构的基类与派生类中,用同名函数来实现各种不同的功能。

*静态绑定又称静态联编*,是指在编译程序时就根据调用函数提供的信息,把它所对应的具体函数确定下来,即在编译时就把调用函数名与具体函数绑定在一起。

*动态绑定又称动态联编*,是指在编译程序时还不能确定函数调用所对应的具体函数,只有在程序运行过程中才能够确定函数调用所对应的具体函数,即在程序运行时才把调用函数名与具体函数绑定在一起。

编译时多态性: —静态联编(连接)----系统在编译时就决定如何实现某一动作,即对某一消息如何处理.静态联编具有执行速度快的优点.在C++中的编译时多态性是通过函数重载和运算符重载实现的。

运行时多态性: —动态联编(连接)----系统在运行时动态实现某一动作,即对某一消息在运行过程实现其如何响应.动态联编为系统提供了灵活和高度问题抽象的优点,在C++中的运行时多态性是通过继承和虚函数实现的。

(2) 虚函数

虚函数的意义

① 基类与派生类的赋值相容

派生类对象可以赋值给基类对象。

派生类对象的地址可以赋值给指向基类对象的指针。

派生类对象可以作为基类对象的引用。

赋值相容的问题:

不论哪种赋值方式,都只能通过基类对象(或基类对象的指针或引用)访问到派生类对象从基类中继承到的成员, 不能借此访问派生类定义的成员。

② 虚函数使得可以通过基类对象的指针或引用访问派生类定义的成员。

③ Virtual关键字其实质是告知编译系统,被指定为virtual的函数采用动态联编的形式编译。

④ 虚函数的虚特征:基类指针指向派生类的对象时,通过该指针访问其虚函数将调用派生类的版本。

- 一旦将某个成员函数声明为虚函数后,它在继承体系中就永远为虚函数了

- 如果基类定义了虚函数,当通过基类指针或引用调用派生类对象时,将访问到它们实际所指对象中的虚函数版本。

- 只有通过基类对象的指针和引用访问派生类对象的虚函数时,才能体现虚函数的特性。

- 派生类中的虚函数要保持其虚特征,必须与基类虚函数的函数原型完全相同,否则就是普通的重载函数,与基类的虚函数无关。

- 派生类通过从基类继承的成员函数调用虚函数时,将访问到派生类中的版本。

- 只有类的非静态成员函数才能被定义为虚函数,类的构造函数和静态成员函数不能定义为虚函数。原因是虚函数在继承层次结构中才能够发生作用,而构造函数、静态成员是不能够被继承的。

- 内联函数也不能是虚函数。因为内联函数采用的是静态联编的方式,而虚函数是在程序运行时才与具体函数动态绑定的,采用的是动态联编的方式,即使虚函数在类体内被定义,C++编译器也将它视为非内联函数。

⑤ 5.基类析构函数几乎总是为虚析构函数。

why?

假定使用delete和一个指向派生类的基类指针来销毁派生类对象,如果基类析构函数不为虚,就如一个普通成员函数,delete函数调用的就是基类析构函数。在通过基类对象的引用或指针调用派生类对象时,将致使对象析构不彻底!

(3) 纯虚函数和抽象类

① 纯虚函数概念?

仅定义函数原型而不定义其实现的虚函数。

Why pure function?

实用角度:占位手段place-holder

方法学:接口定义手段,抽象表达手段

How?

class X{virtual ret_type func_name (param) = 0;}

② 抽象类概念?

What is an abstract class?

包含一个或多个纯虚函数的类

Using abstract class

*不能实例化抽象类*

但是可以定义抽象类的指针和引用

Converting abstract class to concrete class

定义一个抽象类的派生类

定义所有纯虚函数

③ C++对抽象类具有以下限定

- 抽象类中含有纯虚函数,由于纯虚函数没有实现代码,所以不能建立抽象类的对象。

- 抽象类只能作为其他类的基类,可以通过抽象类对象的指针或引用访问到它的派生类对象,实现运行时的多态性。

- 如果派生类只是简单地继承了抽象类的纯虚函数,而没有重新定义基类的纯虚函数,则派生类也是一个抽象类。

9. 运算符重载

运算符重载是C++的一项强大功能。通过重载,可以扩展C++运算符的功能,使它们能够操作用户自定义的数据类型,增加程序代码的直观性和可读性。

本章主要介绍 类成员运算符重载与友元运算符重载, 二元运算符与一元运算符重载, 运算符++、–、[]、()重载, this指针与运算符重载及 流运算符<>的重载。

(1) 重载二元运算符

① 二元运算符的调用形式与解析

aa@bb 可解释成 aa.operator@(bb)、或解释成 operator@(aa,bb)

如果两者都有定义,就按照重载解析。

class X{public:void operator+(int);X(int);};void operator+(X,X);void operator+(X,double);

② 类运算符重载形式

a. 非静态成员运算符重载

以类成员形式重载的运算符参数比实际参数少一个,第1个参数是以this指针隐式传递的。

class Complex{double real,image;public:Complex operator+(Complex b){……}......};

b. 友元运算符重载

如果将运算符函数作为类的友元重载,它需要的参数个数就与运算符实际需要的参数个数相同。比如,若用友元函数重载Complex类的加法运算符,则形式如下:

class Complex{……friend Complex operator+(Complex a,Complex b);//声明//......};Complex  operator+(Complex a,Complex b){……}   //定义

(2) 重载一元运算符

① 一元运算符

一元运算符只需要一个运算参数,如取地址运算符(&)、负数(?)、自增加(++)等。

② 一元运算符常见调用形式为:

@a 或 a@ //隐式调用形式

a.operator@() // 显式调用一元运算符@

其中的@代表一元运算符,a代表操作数。

@a代表前缀一元运算,如“++a”;

a@表示后缀运算,如“a++”。

③ @a将被C++解释为下面的形式之一

a.operator@()、operator@(a)

④ 一元运算符作为类成员函数重载时不需要参数,其形式如下:

class X{……T operator@(){……};}

T是运算符@的返回类型。从形式上看,作为类成员函数重载的一元运算符没有参数,但实际上它包含了一个隐含参数,即调用对象的this指针。

⑤ 前自增(减)与后自增(减)

C++编译器可以通过在运算符函数参数表中是否插入关键字int 来区分这两种方式

//前缀

operator – ();

operator – (X & x);

//后缀

operator – (int);

operator – (X & x, int);

(3) 重载赋值运算符=

赋值运算符“=”的重载特殊性:

赋值运算进行时将调用此运算符;

只能用成员函数重载;

如果需要而没有定义时,编译器自动生成,该版本进行bit-by-bit拷贝。

(4) 重载赋值运算符[]

① [ ]是一个二元运算符,其重载形式如下:

class X{……X& operator[](int n);};

② 重载[]需要注意的问题

- []是一个二元运算符,其第1个参数是通过对象的this指针传递的,第2个参数代表数组的下标。

- 由于[]既可以出现在赋值符“=”的左边,也可以出现在赋值符“=”的右边,所以重载运算符[]时常返回引用。

- []只能被重载为类的非静态成员函数,不能被重载为友元和普通函数。

(5) 重载( )

① 运算符( )是函数调用运算符,也能被重载。且只能被重载为类的成员函数。

② 运算符( )的重载形式如下:

class X{……X& operator( )(参数表);};

其中的参数表可以包括任意多个参数。

③ 运算符( )的调用形式如下:

X Obj; //对象定义

Obj()(参数表); //调用形式1

Obj(参数表); //调用形式2

10. 模板

模板(template)是C++实现代码重用机制的重要工具,是泛型技术(即与数据类型无关的通用程序设计技术)的基础。

模板是C++中相对较新的语言机制,它实现了与具体数据类型无关的通用算法程序设计,能够提高软件开发的效率,是程序代码复用的强有力工具。

本章主要介绍了函数模板和类模板两类,以及STL库中的几个常用模板数据类型。

(1) 模板

① 模板概念

模板是对具有相同特性的函数或类的再抽象,模板是一种参数多态性的工具,可以为逻辑功能相同而类型不同的程序提供一种代码共享的机制。

一个模板并非一个实实在在的函数或类,仅仅是一个函数或类的描述,是参数化的函数和类。

② 模板分类

函数模板、类模板。

③ 函数模板与模板函数

函数模板提供了一种通用的函数行为,该函数行为可以用多种不同的数据类型进行调用,编译器会据调用类型自动将它实例化为具体数据类型的函数代码,也就是说函数模板代表了一个函数家族。

与普通函数相比,函数模板中某些函数元素的数据类型是未确定的,这些元素的类型将在使用时被参数化;与重载函数相比,函数模板不需要程序员重复编写函数代码,它可以自动生成许多功能相同但参数和返回值类型不同的函数。

  1. 函数模板

a. 函数模板的定义

template 返回类型 函数名(参数表){  …… //函数模板定义体}

template是定义模板的关键字;写在一对中的T1,T2,…是模板参数,其中的class表示其后的参数可以是任意类型。

模板参数常称为类型参数或类属参数,在模板实例化(即调用模板函数时)时需要传递的实参是一种数据类型,如int或double之类。

函数模板的参数表中常常出现模板参数,如T1,T2

b. 使用函数模板的注意事项

① 在定义模板时,不允许template语句与函数模板定义之间有任何其他语句。

template int x;  //错误,不允许在此位置有任何语句T min(T a,T b){…}

② 函数模板可以有多个类型参数,但每个类型参数都必须用关键字class或typename限定。此外,模板参数中还可以出现确定类型参数,称为非类型参数。例:

template T1 fx(T1 a, T 2 b, T3 c){…}

在传递实参时,非类型参数T4只能使用常量

③ 不要把这里的class与类的声明关键字class混淆在一起,虽然它们由相同的字母组成,但含义是不同的。这里的class表示T是一个类型参数,可以是任何数据类型,如int、float、char等,或者用户定义的struct、enum或class等自定义数据类型。

④ 为了区别类与模板参数中的类型关键字class,标准C++提出?了用typename作为模板参数的类型关键字,同时也支持使用class。比如,把min定义的template 写成下面的形式是完全等价的:

template  T min(T a,T b){…}

c. 函数模板的实例化

- 实例化发生的时机

模板实例化发生在调用模板函数时。当编译器遇到程序中对函数模板的调用时,它才会根据调用语句中实参的具体类型,确定模板参数的数据类型,并用此类型替换函数模板中的模板参数,生成能够处理该类型的函数代码,即模板函数。

- 当多次发生类型相同的参数调用时,只在第1次进行实例化。假设有下面的函数调用:

int x=min(2,3);   int y=min(3,9);int z=min(8.5);

编译器只在第1次调用时生成模板函数,当之后遇到相同类型的参数调用时,不再生成其他模板函数,它将调用第1次实例化生成的模板函数。

- 实例化的方式:

隐式实例化:

编译器能够判断模板参数类型时,自动实例化函数模板为模板函数

template  T max (T, T);…int i = max (1, 2); float f = max (1.0, 2.0);char ch = max (‘a’, ‘A’);…

隐式实例化,表面上是在调用模板,实际上是调用其实例。

显示实例化explicit instantiation:

编译器不能判断模板参数类型或常量值,需要使用特定数据类型实例化

语法形式::

模板名称 (参数)

template  T max (T, T);…int i = max (1, ‘2’); // error: data type can’t be deducedint i = max (1, ‘2’);…

d. 函数模板的特化

- 特化的原因

但在某些情况下,模板描述的通用算法不适合特定的场合(数据类型等)

比如:如max函数

char * cp = max (“abcd”, “1234”);

实例化为:char * max (char * a, char * b){return a > b ? a : b;}

这肯定是有问题的,因为字符串的比较为:

char * max (char * a, char * b){return strcmp(a, b)>0 ? a : b;  }

- 特化

所谓特化,就是针对模板不能处理的特殊数据类型,编写与模板同名的特殊函数专门处理这些数据类型。

模板特化的定义形式:

template 返回类型 函数名(参数表) {

​ ……

}

说明:

① template 是模板特化的关键字,中不需要任何内容;

② 函数名后的中是需要特化处理的数据类型。

e. 说明

① 当程序中同时存在模板和它的特化时,特化将被优先调用;

② 在同一个程序中,除了函数模板和它的特化外,还可以有同名的普通函数。其区别在于C++会对普通函数的调用实参进行隐式的类型转换,但不会对模板函数及特化函数的参数进行任何形式的类型转换。

f. 调用顺序

当同一程序中具有模板与普通函数时,其匹配顺序如下:

完全匹配的非模板函数

完全匹配的模板函数

类型相容的非模板函数

  1. 类模板

a. 类模板的概念

类模板可用来设计结构和成员函数完全相同,但所处理的数据类型不同的通用类。

如栈,存在:

双精度栈:

class doubleStack{  private:  double data[size];  ……};

字符栈:

class charStack{  private:  char data[size];  ……};

这些栈除了数据类型之外,操作完全相同,就可用类模板实现。

b. 类模板的声明

template

class 类名{

……// 类成员的声明与定义

}

其中T1、T2是类型参数

类模板中可以有多个模板参数,包括类型参数和非类型参数

c. 非类型参数

非类型参数是指某种具体的数据类型,在调用模板时只能为其提供用相应类型的常数值。非类型参数是受限制的,通常可以是整型、枚举型、对象或函数的引用,以及对象、函数或类成员的指针,但不允许用浮点型(或双精度型)、类对象或void作为非类型参数。

在下面的模板参数表中,T1、T2是类型参数,T3是非类型参数。

template

在实例化时,必须为T1、T2提供一种数据类型,为T3指定一个整常数(如10),该模板才能被正确地实例化。

d. 类模板的成员函数的定义

方法1:在类模板外定义,语法:

template
返回值类型 类模板名::成员函数名 (参数列表)
{
……
};

方法2:成员函数定义,与常规成员函数的定义类似,另外

“模板参数列表”引入的“类型标识符”作为数据类型使用

“模板参数列表”引入的“普通数据类型常量”作为常量使用

e. 类模板特化

特化,即用与该模板相同的名字为某种数据类型专门重写一个模板类。

类模板有两种特化方式:一种是特化整个类模板,另一种是特化个别成员函数

特化成员函数的方法:

template 返回类型 类模板名::特化成员函数名(参数表){

…… //函数定义体

}

template  void Array::Sort(){  for(int i=0;i<Size-1;i++){    int p=i;    for(int j=i+1;j<Size;j++)      if(strcmp(a[p],a[j])<0) p=j;   char* t=a[p];    a[p]=a[i];    a[i]=t;  }}

11. 异常处理

(1) catch捕获异常时,不会进行数据类型的默认转换。

(2) 限制异常的方法

- 当一个函数声明中不带任何异常描述时,它可以抛出任何异常。例如:

int f(int,char);  //函数f可以抛出任何异常

- 在函数声明的后面添加一个throw参数表,在其中指定函数可以抛出的异常类型。例如:

int g(int,char)  throw(int,char);  //只允许抛出int和char异常。

- 指定throw限制表为不包括任何类型的空表,不允许函数抛出任何异常。如:

int h(int,char) throw();//不允许抛出任何异常

(3) 捕获所有异常

在多数情况下,catch都只用于捕获某种特定类型的异常,但它也具有捕获全部异常的能力。其形式如下:

catch(…) {  ……      //异常处理代码}

(4) 再次抛出异常

如是catch块无法处理捕获的异常,它可以将该异常再次抛出,使异常能够在恰当的地方被处理。再次抛出的异常不会再被同一个catch块所捕获,它将被传递给外部的catch块处理。要在catch块中再次抛出同一异常,只需在该catch块中添加不带任何参数的throw语句即可。

(5) 异常的嵌套调用

try块可以嵌套,即一个try块中可以包括另一个try块,这种嵌套可能形成一个异常处理的调用链。

参考链接:
推荐一个零声学院后台服务器免费公开课,个人觉得老师讲得不错,分享给大家:
后台服务器开发
https://course.0voice.com/v1/course/intro?courseId=5&agentId=0

Python学习手册