C++面试重点

1.STL 容器相关实现
2.C++新特性的了解
3.多态和虚函数的实现
4.指针的使用

文章目录

1.私有属性和保护属性

(1)​​public​​公有属性:外部可进行调用

(2)​​protected​​保护属性:外部不能调用,只有继承的子类可以进行调用

(3)​​private​​私有属性:外部不能进行调用,继承的子类也不能进行调用,只有基类自身可进行调用

  • 成员属性设置为私有的优点:
  • 将所有成员属性设置为私有,可以自己控制读写权限;
  • 对于写权限,我们可以检测数据的有效性。

继承(子类自动地共享基类中定义的数据和方法)也有以上的3种方式,如公有继承:子类会把父类的公有成员和保护成员都继承下来,而且成员属性也是要继承下来,注意私有的不能继承过来。
​​​javascript:void(0)​

2.C++程序的组成

(1)预处理部分:主要是文件包含、宏定义等。
(2)类的定义:对具有相同属性和行为的一个或多个对象的描述。
定义一个类(相当于进行了一个类型说明)。具体的说法格式如下:
Class 类名称
{类各成员定义}
(3)主函数:主函数是程序的主线,决定着程序的开始与结束。主函数外部代码是否执行完全取决于主函数是否调用它。

3.C++程序的编写思路

(1)预处理部分
判断整个程序将用到哪些函数,而这些函数都包含在哪些头文件。
根据需要把头文件写在预处理部分。

(2)类的定义
A.类中包含哪些数据成员?
B.数据成员是什么类型?
C.这些成员是共有还是私有?
D.包含哪些成员函数?
E.这些成员函数需要完成什么功能?
F.函数的名称、参数、函数体应该怎么写?

(3)主函数
A.如何让类中的成员函数执行;
B.哪些数据是需要从键盘键入,哪些可以直接赋值;
C.调用函数涉及到参数传递,所调用的函数实参应该怎么写。

4.const说明

C++中常用的类型修饰符。
const作用:
(1)定义const常量,具有不可变性。 例如:const int Max = 100; Max++; 会报错
(2)便于进行类型检查,使编译器对处理内容有更多的了解,消除了一些隐患。 例如:void f(const int i){ } 编译器就会知道i是一个常量,不允许修改。
(3)可以避免意义模糊的数字出现,同样很方便地进行参数的调整和修改
同宏定义一样,可以做到不变则已,一变都变!如(1)中,如果想修改Max的内容,只需要:Const int Max = you want;即可。
(4)可以节省空间,避免不必要的内存分配。

#define PI 3.14159;//宏常量
const double Pi = 3.14159; //此时并未将Pi放入RAM中
double i = Pi; //此时为Pi分配内存,以后不再分配
double I = PI; //编译期间进行宏替换,分配内存
double j = Pi; //没有内存分配
double J = PI; //再进行宏替换,又一次分配内存

const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,
所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干份拷贝。
(5)提高了效率。编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也提高

5.OOP的基本概念

(1)对象
一个对象往往是一组属性和一组行为构成。C++中每个对象都是由数据和操作代码(如函数)组成。即将类实例化。

(2)
类和对象之间的关系是抽象和具体的关系。
类是对多个对象进行综合抽象的结果,对象又是类的个体事物(一个对象是类的一个实例)。
在OOP中,总是先声明类,再由类生成其对象(通常成为实例)。

(3)消息与方法
消息传递:对象之间的交互。
消息:一个对象向另一个对象发出的请求。当对象接收到发现它的消息时,就调用有关的方法,执行相应的操作。

方法:包括界面和方法体。
方法的界面给出方法名和调用协议(相当于C++中成员的函数的函数名和参数表)
方法体:实现某种操作的一系列计算步骤,即一段程序(相当于C++中成员函数的函数体)

消息和方法的关系
对象根据接收到的消息,调用相应的方法。

消息具有以下3个性质:
(1)同一个对象可以接收不同形式的多个消息,做出不同的响应;
(2)相同形式的消息可以传递给不同的对象,所做出的响应可以是不同的;
(3)对消息的响应并不是必需的,对象可以响应消息,也可以不响应。

6.参数传递方式

(1)值传递:单向传递数值;
(2)地址传递:双向传递地址;
——在函数调用时使实参对象和形参对象指针变量指向同一内存地址,在函数调用过程中,对形参对象指针所指对象值得改变也同样影响着实参对象的值。
(3)引用传递:形参和实参是同一个变量。
使用对象指针作为函数参数:“传址调用”,即(开辟了新指针)
使用对象引用作为函数参数:不但具有对对象指针用作函数参数的优点,而且用对象引用作函数参数将更简单、更直接。(形参实参一体,未开辟新空间)

7.OOP的基本特征

(1)抽象
通过特点的实例抽取性质后形成概念的过程。
类是对象的抽象,对象是类的实例。

(2)封装
把数据和实现操作的代码集中放在对象内部,并尽可能隐藏对象的内部细节。
使用一个对象时,只需知道它向外界提供的接口而不需知道它的数据结构的细节和实现操作的算法。封装使代码能重用。
对象中某些数据和操作代码对外隐蔽,有利于数据安全,防止无关人员访问和修改数据。

(3)继承
对象类之间相关的关系,这种关系使得某一类可以继承另一个类的特征和能力。

1)若类之间具有继承关系,则它们之间具有下列几个特性:
A.类间具有共享特征(包括数据和操作代码的共享);
B.类间具有差别或新增部分(包括非共享的数据和操作代码);
类间具有层次结构。

2)继承的作用:
A.避免公用代码的重复开发,减少代码和数据冗余;
B.通过增强一致性来减少模块间的接口和界面。
单继承:指每个派生类只直接继承了一个基类的特征;
多继承:指多个基类派生出一个派生类的继承关系。

(4)多态
指不同的对象收到相同的消息时执行不同的操作。
C++支持两种多态性,即编译时的多态性和运行时的多态性。
1)编译时的多态性:通过函数重载(包括运算符重载)来实现的,
2)运行时的多态性:通过虚函数来实现的。

8.辨析重载

(1)重载:名称相同,执行不同任务
(2)函数重载:函数名称相同,对应多个函数体
——可以定义多个相同名字的函数,只要它们的形参个数或类型不完全一致即可,编译程序根据实参与形参的类型及个数自动确定调用哪个函数。这些同名函数称为重载函数。

(3)运算符重载:运算符相同,对应的运算功能不同
——通过创建运算符重载函数实现,运算符重载函数可以是在类外定义的普通函数,也可以是类的成员函数或友元函数。在C++中允许重新定义已有的运算符,使其能够按照用户的要求完成一些特定的操作。

9.对象数组和对象指针

对象数组:每一个数组元素都是对象的数组。
对象指针:用于存放对象地址的变量。

10.继承:公有、私有、保护

(1)公有继承:
基类的私有成员不能被派生类的函数成员访问;
基类的公有成员和保护成员能被派生类访问

回顾:
​​​public​​​公有属性:外部可进行调用
​​​protected​​保护属性:外部不能调用,只有继承的子类可以进行调用
​​​private​​私有属性:外部不能进行调用,继承的子类也不能进行调用,只有基类自身可进行调用

(2)私有继承
基类的私有成员不能被派生类的函数成员访问;
基类的共有成员和保护成员在派生类中的访问权限变为私有类型。

(3)保护继承

基类的私有成员不能被派生类函数成员访问;

基类中的公有成员和保护成员在派生类中的访问权限都变为保护型。

C/C++基础知识整理_派生类

11.this指针

自引用指针。
每当创建一个对象时,系统就把this指针初始化为指向该对象,
即this指针的值是当前调用成员函数的对象的起始地址

12.面向对象的机制:

对象的特点:以数据为中心、对象是主动的、数据封装,并行性、模块独立性。

A.抽象数据类型;
B.封装与信息隐藏;
C.以继承方式实现程序的重用;
D.以函数重载、运算符重载和虚函数来实现多态性;
E.以模板来实现类型的参数化。

面向对象设计准则:
模块化、抽象、信息隐藏、弱耦合、强内聚、可重用

13.派生类与继承

所谓继承就是从先辈处得到属性和行为特征。
类的继承就是新的类从已有类那里得到已有的特性。
从另一个角度来看这个问题,从已有类产生新类的过程就是类的派生。
类的继承和派生机制使程序员无需修改已有类,从而较好地解决代码重用的问题。
由已有类产生新类时,新类便包含了已有类的特征,同时也可以加入自己的新特性。

14.static静态成员(数据&函数)

(1)静态数据成员
每个类中只有一个静态数据成员的拷贝,该类的所有对象共同维护和使用。

(2)静态成员函数
前面有static说明的成员函数。
静态成员函数属于整个类,是该类所有对象共享的成员函数,而不属于类中的某个对象。

注意:这里很可能也问​​static​​​关键字不在类里用的情况,参考​​C++中static关键字作用总结​​。

15.const常引用:

如果在说明引用时用​​const​​修饰,则被说明的引用为常引用。如果用常引用作形参,便不会产生对实参的不希望的更改。

16.派生类对基类成员的其他访问方法:

(1)可见成员:也就是派生类的公有成员:派生类从基类继承的公有成员和派生类新增的成员。
(2)不可见成员:
就是派生类的私有成员和保护成员(派生类从基类继承的私有保护成员和派生类新增的私有保护成员)

17.构造函数

一种特殊的成员函数,用于为对象分配空间,进行初始化。
(1)构造函数的函数名必须与类名相同;
(2)不能定义构造函数的类型;
(3)构造函数应该声明为公有函数;
(4)构造函数不能在程序中调用,在对象创建时,构造函数被系统自动调用。
——就是这个构造函数是这个类自己的东西,别的类不能用,构造函数是给它自己这个类中的属性做初始化工作的。在创建这个类的对象的时候,系统会自动调用这个类的构造函数。就比如​​Student stu=new Student()​​;这后半部分就是Student类的空参的构造方法。

18.析构函数

一种特殊的成员函数。
操作和构造函数相反,用于执行一些清理任务,在对象删除前被自动调用,
特点:
(1)析构函数名与类名相同,但它前面必须加一个波浪号(~);
(2)析构函数不返回任何值,没有参数;
(3)一个类只能定义一个析构函数,所以析构函数不能重载;
(4)撤销对象时,编译系统会自动地调用析构函数。

19.C++的结构体与C的不同:

(1)增加了函数成员(变量,即数据成员)。
数据成员->属性;函数成员->行为。
封装性:把每个对象的数据(属性)和操作(行为)包装在一个类中。
(2)增加成员的访问权限的设定机制 private私有成员 public公有成员

20.友元:

(1)友元提供了不同类或对象的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制,友元可以访问私有成员
(2)友元破坏了数据的封装性和安全机制;
(3)友元包括友元函数和友元类。

21.类的说明

(1)除了private和public之外,类中的成员还可以用另一个关键字protected来说明。
被protected说明的数据成员和成员函数称为保护成员。
保护成员可以由本类的成员函数访问;也可以由本类的派生类的成员函数访问,而类外的任何访问都是非法的
(2)不能在类声明中给数据成员赋初值(类似于不能在定义结构体类型过程中给成员赋值)

22.派生新类的过程

(1)吸收基类成员:首先将基类成员全盘吸收;
(2)改造基类成员:依靠派生类的继承方式来控制基类成员的访问
(3)对基类数据成员或成员函数进行覆盖,添加新成员。

派生关系的特点:
(1)派生类没有独立性,不能脱离基类而存在
(2)继承方式决定了基类成员在派生类当中的访问权限
(3)基类的私有成员在派生类中无法直接访问
(4)无论能否直接访问基类成员,继承的基类成员都存在派生类

23.虚继承的作用:

为什么要引入虚基类:
如果一个类有多个直接基类,而这些直接基类又有一个共同的基类,则在最低层的派生类中会保留这个间接的共同基类数据成员的多份同名成员。
在访问这些同名的成员时,必须在派生类对象名后增加直接基类名,使其惟一地标识一个成员,以免产生二义性。

为解决多继承(从多个直接基类中产生派生类)时的命名冲突,虚继承在派生类中只保留一份间接基类的成员。

即虚继承让某个类做出声明,承若愿意共享他的的基类(该被共享的基类称为虚基类,如下图的A类)。这样不论虚基类在继承体系中出现多少次,在派生类中都只包含一个虚基类的成员

C/C++基础知识整理_c++_02

24.基类和派生类指针问题

(1)基类指针不可以指向私有派生类对象;
(2)派生类指针不可以指向基类对象;
(3)基类指针指向派生类对象时,只能访问从基类中继承下来的成员。

另外:在公有派生(继承)的情况下,一个派生类的对象可用于基类对象适用的地方:
(1)派生类对象可赋值给基类的对象;
(2)派生类的对象可以初始化基类的引用;
(3)基类的指针可以指向派生类。
有一个游戏,里面有100种怪物,他们继承自同一个怪物基类Mob;

class Mob{
virtual attack(Mob*);
}//现在每种怪物都需要实现一个攻击任何怪物的方法。
//没有多态的时候可以这么写:
class Mob1 : public Mob{
bool attack(Mob1& mob1);
bool attack(Mob2& mob2);
bool attack(Mob3& mob3);
bool attack(Mob4& mob4);
.....
bool attack(Mob100& mob100);}
//有了多态以后:
class Mob1 : public Mob{
bool attack(Mob* pMob); //以前有99个成员函数,现在变成了1个,因为pMob可以有多种形态。
}

25.运算符重载规则

(1)只能对已有的C++运算符进行重载,不允许用户自己定义新的运算符,也不能改变原有运算符的含义
(2)重载不能改变运算符原有的优先级,C++语言已经预先规定了每个运算符的优先级,以决定运算次序,结合性和操作对象的个数不能改变
(3)重载的运算符函数参数不能有默认参数
(4)绝大部分重载的运算符都能被派生类继承,赋值运算符除外
(5)关系运算符“.”、作用域分辨符“::”、成员指针运算符“*”、sizeof运算符和三目运算符“?:”这五个运算符不能被重载

26.多态性

不同的对象收到相同的消息时执行不同的操作
C++支持两种多态性,即编译时的多态性和运行时的多态性。

在c++中,多态性的实现和联编(也叫绑定)这一概念有关。

其中在运行之前就完成的联编称为静态联编,又叫前期联编;
而在程序运行时才完成的联编叫动态联编,也称后期联编。
(1)静态联编:是指系统在编译时就决定如何实现某一动作。
函数调用速度快,效率高,但灵活性差;
(2)动态联编:指系统在运行时动态实现某一动作。
采用这种联编方式,一直要到程序运行时才能确定调用哪个函数,提供了更好的灵活性、问题抽象性和程序易维护性。但效率低。

(1)静态联编支持的多态性称为编译时多态性,也称静态多态性
编译时多态性是通过函数重载(包括运算符重载)和模板实现的。
(2)动态联编所支持的多态性称为运行时多态性,也称动态多态性
在C++中,运行时多态性是通过虚函数来实现的。

多态的栗子

比如说ATM,你要插借记卡,信用卡,VISA之类的,各种各样的银行卡。这些卡都是银行卡,所以银行卡是父类。而各种各样的卡是子类。
当ATM要取钱的时候,他不需要为每一种卡都写一个函数 借记卡.getMoney(),信用卡.getMoney()。
有了多态之后,他只需要写一个银行卡的getMoney()函数就行了,将这个函数设置为virtual。
多态的意义就是能够用父类指针来指向子类对象
不同种类的卡(子类)只需要实现自己的getMoney().当使用银行卡调用getMoney函数时,程序会因为多态性,自动寻找子类的getMoney函数执行。非常方便。

27.虚函数

是指在基类当中加上​​virtual​​的成员函数,它提供了一种接口界面,可以在一个或多个派生类中被重定义
(1)使用虚函数时,一定会有一个指向基类的指针或指向基类类型的引用
(2)在派生类中重新定义虚函数时,必须保证该函数的值和参数与基类中的说明完全一致(函数的类型、参数的个数和类型)
(3)若在派生类中没有重新定义虚函数,则该类的对象将使用其基类的虚函数代码
(4)虚函数必须是成员函数,不能是友元,不能是静态成员。
(5)析构函数可以是虚函数,但是构造函数不能
(6)一个类的虚函数仅对派生类中重定义的函数起作用,对其他函数没有影响。

说明

(1)将成员函数声明为虚函数,在函数原型前加关键字virtual,如果成员函数的定义直接写在类中,也在前面加virtual。
(2)将成员函数声明为虚函数后,再将基类指针指向派生类对象,在程序运行时,就会根据指针指向的具体对象来调用各自的虚函数,称之为动态多态
(3)如果基类的成员函数为虚函数,在其派生类中,原型相同的函数自动成为虚函数。

28.函数重载&虚函数

(1)重载函数要求函数有相同返回值类型和函数名称,并有不同参数序列;
虚函数要求函数名、返回值类型和参数序列完全相同。

(2)重载函数可以成员函数或者友元函数;
虚函数只能是成员函数。

(3)重载函数的调用时以所传递参数序列的差别作为调用不同函数的依据;
虚函数是根据对象的不同去调用不同类的虚函数。

(4)虚函数在运行时表现出多态(C++的精髓)——动态多态性(运行时多态性)。
重载函数在编译时表现出多态——静态多态性(编译时多态性)。

29.抽象类

纯虚函数:不必定义函数体的特殊虚函数。

抽象类:含有纯虚函数的类称为抽象类,常常用作基类。
(1)由于抽象类中至少包含一个没有定义功能的纯虚函数,因此抽象类只能用作其他类的基类,不能建立抽象类对象。
(2)抽象类不能用作参数类型、函数返回类型或显示转换的类型。
但可以声明指向抽象类的指针变量,此指针可以指向它的派生类,进而实现多态性。

如果在抽象类的派生类中没有重新说明纯虚函数,则该函数在派生类中仍然为纯虚函数,而这个派生类仍然还是一个抽象类,直到纯虚函数被重新定义。

30.vector的扩容原理

Vector通过一个连续的数组存放元素,如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,在插入新增的元素;

对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了 ;

不同的编译器实现的扩容方式不一样,VS2015中以1.5倍扩容,GCC以2倍扩容。

31.C#和C++指针的区别

(1)C#没有指针这个概念,只有引用和数值之分。
int等内部数据类型和struct定义的类型是数据类型,拷贝时做深度拷贝
而string和用class定义的类型是引用类型,拷贝时做浅拷贝——与深度拷贝对应,它通过引用计数来实现对象和内存管理。

这里的浅拷贝和OS里文件系统学的软链接差不多。

32.STL和泛型编程

STL是标准模板库,是一组处理各种容器对象的模板。
STL演示了一种编程模式——泛型编程。
STL容器、迭代器、算法都是泛型编程的例子。模板是泛型编程的基础

STL六大组件:
(1)算法:如vector的sort可进行排序
(2)容器:如vector、deque、set、map、pair
(3)迭代器:像指针,迭代器也可以是一个对其执行类似指针的操作,如接触引用operator*()和递增Operator++()的对象,
(4)仿函数:即函数对象,重载了操作符的struct
(5)容器适配器:以序列式容器为底层结构,STL提供3种适配器(stack、queue、priority_queue)
(6)空间配置器:主要工作——(1)对象的创建与销毁(2)内存的获取与释放

有哪些顺序容器,哪2种是C++11新添的

vector、list、forward_list、deque、array、string。
forward_list和array是新增的。

关联容器中,哪些是稳定排序的

map和set是稳定排序,multimap和multiset不是稳定排序。

33.vector和list区别,原理

(1)vector和数组类似,有一段连续的内存空间,便于随机访问O(1)。
当vector数组内存空间不足时,会采取扩容,通过重新申请一块更大的内存空间进行内存拷贝。

(2)list底层是双向链表实现,即内存空间不是连续的。
由链表知,list查询效率低,时间复杂度为O(n),但链表的插入和删除效率高。

迭代器支持不同

异:vector中,iterator支持 ”+“、”+=“,”<"等操作。而list中则不支持。
同:vector< int >::iterator和list< int >::iterator都重载了 “++ ”操作。

34.函数指针作用,应用场景

通过函数指针,可以通过赋值将函数的地址赋值给函数指针(使得函数指针指向该函数)。
举个栗子:函数名Func代表函数的首地址

int Func(int x);
int (*p)(int x);//定义一个函数指针
p=Func;//将Func函数的首地址赋值给指针变量p

函数指针的使用:

#include<stdio.h>
#include<iostream>
using namespace std;
int Max(int,int);//函数声明
int main(void){
int(*p)(int,int);//定义一个函数指针
int a,b,c;
p=Max;//把函数Max赋给指针变量p,使p指向Max函数
cout<<"please enter a and b:";
cin>>a>>b;
c=(*p)(a,b);//通过函数指针调用Max函数
cout<<"a="<<a<<endl<<"b="<<b<<endl<<"max="<<c<<endl;
system("pause");
}
int Max(int x,int y){
int z;
if(x>y){
z=x;
}else{
z=y;
}
return z;
}

​函数指针应用场景​​​:
(1)将函数指针作为参数(本质:调用函数在不同的场景需要不同的函数指针作为参数)
(2)引用不在代码段中的函数

35.智能指针

智能指针主要用于管理在堆上分配的内存,它将普通的指针封装为一个栈对象。当栈对象的生存周期结束后,会在析构函数中释放掉申请的内存,从而防止内存泄漏

  • ​auto_ptr​​智能指针:当对象拷贝或者赋值后,前面的对象就悬空了。
  • ​unique_ptr​​智能指针:防止智能指针拷贝和复制。
  • ​shared_ptr​​智能指针:通过引用计数的方式来实现多个​​shared_ptr​​对象之间共享资源。
  • ​weak_ptr​​​智能指针:可以从一个​​shared_ptr​​​或另一个​​weak_ptr​​对象构造,它的构造和析构不会引起引用记数的增加或减少。

c++11出来之前,只有1种智能指针,就是​​auto_ptr​​,c++11出来之后补充了3个智能指针(上面的另外3个)。

强调一点,不是每一种智能指针都可以增加内存的引用计数。智能指针分为两类,一种是可以使用多个智能指针管理同一块内存区域,每增加一个智能指针,就会增加1次引用计数,另一类是不能使用多个智能指针管理同一块内存区域,通俗来说,当智能指针2来管理这一块内存时,原先管理这一块内存的智能指针1只能释放对这一块指针的所有权。按照这个分类标准,auto_ptr unique_ptr weak_ptr属于后者,shared_ptr属于前者。

对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。

更多参考:​​javascript:void(0)​

参考阅读:
1、​​​javascript:void(0)​​​ 2、​​javascript:void(0)​​ 3、​​javascript:void(0)​​ 4、​​javascript:void(0)​​ 5、​​智能指针的视频​

36.C++虚函数常考面试题

(1)在有继承关系的父子类中,构建和析构一个子类对象时,父子构造函数和析构函数的执行顺序分别是怎样的?
(2)在有继承关系的类体系中,父类的构造函数和析构函数一定要申明为 virtual 吗?如果不申明为 virtual 会怎样?
(3)什么是 C++ 多态?C++ 多态的实现原理是什么?
(4)什么是虚函数?虚函数的实现原理是什么?
(5)什么是虚表?虚表的内存结构布局如何?虚表的第一项(或第二项)是什么?
(5)菱形继承(类D同时继承B和C,B和C又继承自A)体系下,虚表在各个类中的布局如何?如果类B和类C同时有一个成员变了m,m如何在D对象的内存地址上分布的?是否会相互覆盖?

37.memcpy和memmov函数原型和区别

C 库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。该函数返回一个指向目标存储区 str1 的指针。

void *memcpy(void *str1, const void *str2, size_t n)

// 将字符串复制到数组 dest 中
#include <stdio.h>
#include <string.h>
int main (){
const char src[50] = "http://www.baidu.com";
char dest[50];
memcpy(dest, src, strlen(src)+1);
printf("dest = %s\n", dest); //输出dest = http://www.baidu.com
return(0);
}

1.memmove

函数原型:void *memmove(void *dest, const void *source, size_t count)
返回值说明:返回指向dest的void *指针
参数说明:dest,source分别为目标串和源串的首地址。count为要移动的字符的个数
函数说明:memmove用于从source拷贝count个字符到dest,如果目标区域和源区域有重叠的话,memmove能够保证源串在被覆盖之前将重叠区域的字节拷贝到目标区域中。

2.memcpy

函数原型:void *memcpy(void *dest, const void *source, size_t count);
返回值说明:返回指向dest的void *指针
函数说明:memcpy功能和memmove相同,但是memcpy中dest和source中的区域不能重叠,否则会出现未知结果。

更多参考(实现过程)​

38.内存管理

内存中按照用途被划分的5个区域

int x=0;int *p=NULL;//存储在栈
int *p=new int[20];//存储在堆区,注意这里的数组名为p,而不是int(关键字int)
存储全局变量和静态变量//全局区
string str="hello";//常量区
存储逻辑代码的二进制//代码区

栈区:内存由系统进行控制(程序员不需要关心其分配和回收)
堆区:​​​new​​​分配一段内存(会分配在堆区),需要我们自己用​​delete​​​回收这段内存。
全局区:全局变量、静态变量
常量区:字符串、常量
代码区:存储编译后的二进制代码

39.指针常量和常量指针

【指针常量】是值不可变的地址变量。

#include <iostream>
using namespace std;
int main(){
int a = 3;
int m[2] = { 1, 2 };
int *const c = &a;//指针常量
cout << "c的(值):" << c << " " << "c指针指向的值:" << *c << endl;
c[0]=m[0];//改变地址的内容是合法的
cout << "c的(值):" << c << " " << "c指针指向的值:" << *c << endl;
system("pause");
}

C/C++基础知识整理_虚函数_03


【常量指针】和普通指针类似,可以改变指向地址(即该指针的值),但不能改变内容,这个内容比如​​*c​​。

#include <iostream>
using namespace std;
int main(){
int a = 3;
int m[6] = {0,2,3,6,9,5};
int const *c = &a;//const int*c = &a;常量指针
cout << "c的(值):" << c << " " << "c指针指向的值:" << *c << endl;
c = &m[3];//可以改变指向地址,不能改变内容(*c=m[3]操作错误)
cout << "c的(值):" << c << " " << "c指针指向的值:" << *c << endl;
system("pause");
}

C/C++基础知识整理_面试_04

40.深拷贝和浅拷贝

浅拷贝:如果一个类拥有资源,该类的对象进行复制时,如果资源不需要重新分配,则是浅拷贝;如果资源需要重新分配,则是浅拷贝。

41.实现一个类成员函数,要求不允许修改类的成员变量?

如果想达到一个类的成员函数不能修改类的成员变量,只需用 const 关键字来修饰该函数即可。

#include <iostream>

using namespace std;

class A{
public:
int var1, var2;
A(){
var1 = 10;
var2 = 20;
}
void fun() const // 不能在 const 修饰的成员函数中修改成员变量的值,除非该成员变量用 mutable 修饰
{
var1 = 100; // error: assignment of member 'A::var1' in read-only object
}
};

int main(){
A ex1;
return 0;
}

42.友元函数

通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。

1.普通函数定义为友元函数,使普通函数能够访问类的私有成员

#include <iostream>

using namespace std;

class A
{
friend ostream &operator<<(ostream &_cout, const A &tmp); // 声明为类的友元函数

public:
A(int tmp) : var(tmp)
{
}

private:
int var;
};

ostream &operator<<(ostream &_cout, const A &tmp)
{
_cout << tmp.var;
return _cout;
}

int main()
{
A ex(4);
cout << ex << endl; // 4
return 0;
}

2.友元类:类之间共享数据

#include <iostream>

using namespace std;

class A
{
friend class B;

public:
A() : var(10){}
A(int tmp) : var(tmp) {}
void fun()
{
cout << "fun():" << var << endl;
}

private:
int var;
};

class B
{
public:
B() {}
void fun()
{
cout << "fun():" << ex.var << endl; // 访问类 A 中的私有成员
}

private:
A ex;
};

int main()
{
B ex;
ex.fun(); // fun():10
return 0;
}

43.C++的引用

作用:给变量起别名,新别名指向的是同一块内存。
语法:​​​数据类型 &别名 = 原名​

#include<iostream>
using namespace std;

//引用,给变量起别名
int main(){
int a = 10;
int &b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
//a = 10
//b = 10

b = 100;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
//a = 100
//b = 100
system("pause");
return 0;
}

44.C++11的特性

(1)类型推导 auto和decltype

auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用,对 auto 的语义变更也就非常自然了。

使用 auto 进行类型推导的一个最为常见而且显著的例子就是迭代器。在以前我们需要这样来书写一个迭代器,并且加了​​auto​​后的写法:

for(vector<int>::const_iterator itr = vec.cbegin(); itr != vec.cend(); ++itr)

//auto类型推导的写法
// 由于 cbegin() 将返回 vector<int>::const_iterator
// 所以 itr 也应该是 vector<int>::const_iterator 类型
for(auto itr = vec.cbegin(); itr != vec.cend(); ++itr);

(2)初始化列表

C++11 提供了统一的语法来初始化任意的对象,例如:

struct A {
int a;
float b;
};
struct B {
B(int _a, float _b): a(_a), b(_b) {}
private:
int a;
float b;
};

A a {1, 1.1}; // 统一的初始化语法
B b {2, 2.2};

C++11 还把初始化列表的概念绑定到了类型上,并将其称之为 ​​std::initializer_list​​,允许构造函数或其他函数像参数一样使用初始化列表,这就为类对象的初始化与普通数组和 POD 的初始化方法提供了统一的桥梁,例如:

#include <initializer_list>

class Magic {
public:
Magic(std::initializer_list<int> list) {}
};

Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};

(3)Lambda 表达式

实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

Lambda 表达式的基本语法如下:

[ caputrue ] ( params ) opt -> ret { body; };
  • capture是捕获列表;
  • params是参数表;(选填)
  • opt是函数选项;可以填mutable,exception,attribute(选填)
  • mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
  • exception说明lambda表达式是否抛出异常以及何种异常。
  • attribute用来声明属性。
  • ret是返回值类型(拖尾返回类型)。(选填)
  • body是函数体。

(4)其他

如第56点C++的4种新类型转换、第57点的C++的左值引用和右值引用、第58点C++并发多线程的头文件、第59点的智能指针解决交叉引用、第60点的​​functional​​头文件等,都是属于C++11的内容。

45.map和unordered_map的区别

map

unordered_map

实现方法

红黑树,平衡二叉树

hash_table,一般是一个大​​vector​​​,每个​​vector​​元素节点挂链表来解决冲突

时间复杂度

能够O(log n)完成查找、插入和删除

查询几乎为常数级别

缺点

——

消耗较多内存(用空间换时间)

改进

——

可设置合适的hash方法,获得更好的性能

使用场景

在需要有序性或者对单次查询(能够确保良好的最坏运行时间)有时间要求的应用场景下,应使用map

其余情况(如多次查询)应使用unordered_map

46.new和malloc的区别

特征

new/delete

malloc/free

分配内存的位置

自由存储区


内存分配成功的返回值

完整类型指针,如​​new​​返回对象类型的指针

​void*​​,再通过强制类型转换成所需类型

内存分配失败的返回值

默认抛出异常

返回NULL

分配内存的大小

由编译器根据类型计算得出

必须显式指定字节数

处理数组

有处理数组的new版本new[]

需要用户计算数组的大小后进行内存分配

已分配内存的扩充

无法直观地处理

使用realloc简单完成

是否相互调用

可以,看具体的operator new/delete实现

不可调用new

分配内存时内存不足

客户能够指定处理函数或重新制定分配器

无法通过用户代码进行处理

函数重载

允许

不允许

构造函数与析构函数

调用

不调用

47.如何实现多态

  • 多态:指不同的对象收到相同的消息时执行不同的操作。无论发送消息的对象属于什么类,它们均能发送具有同一形式的消息,对消息的处理方式可能随接收消息的对象而变。C++通过虚函数实现多态,即在基类中用​​virtual​​声明,并在派生类中定义。虚函数:“虚假”的函数,父类引用子类对象,子类成员函数重写父类方法(函数)。
  • 虚函数一旦被定义后,在同一类族的类中,所有与该虚函数具有相同参数和返回值类型的同名函数都将自动成为虚函数(无论是否加上关键字​​virtual​​)。如基类的函数是虚函数时,则所有子类、子类中的子类的对应函数都是虚拟的。
  • C++支持两种多态性,即编译时的多态性和运行时的多态性。
  • 编译时的多态性:通过函数重载(包括运算符重载)来实现的,
  • 运行时的多态性:通过虚函数来实现的。

48.虚函数表是啥

虚函数表。C++通过虚函数实现多态。比如场景中:写代码时不能在一开始确定调用的是基类的函数,还是派生类的成员函数,就在基类中用​​virtual​​关键字声明。

【纯虚函数的实现原理】

纯虚函数也一定是某个类的成员函数,含有纯虚函数的类叫做抽象类。在C++中,抽象类无法实例化对象。如果我们定义了​​Shape​​​这样的类,那么,​​Shape​​类当中,因为有虚函数和纯虚函数,所以它一定有一个虚函数表,也就一定有一个虚函数表指针。在虚函数表当中,如果是纯虚函数,那么虚函数表中的函数指针值为0;如果是普通的虚函数,那就肯定是一个有意义的值。

C/C++基础知识整理_面试_05


【阿里面试】问题:成员变量,虚函数表指针的位置是怎么排布?

如果一个类带有虚函数,那么该类实例对象的内存布局如下:

  • 首先是一个虚函数指针
  • 接下来是该类的成员变量,按照成员在类当中声明的顺序排布,整体对象的大小由于内存对齐会有空白补齐。
  • 其次如果基类没有虚函数但是子类含有虚函数:
  • 此时父类是没有虚函数表指针的;
  • 此时内存子类对象的内存排布也是先虚函数表指针再各个成员。
  • 虚函数表中也有一个指向计算面积函数的指针,这个指针一开始的值是从基类继承而来,但在实例化子类对象Circle之后,这个值就会被Circle类中定义的计算面积函数的首地址所覆盖

【代码栗子】
如果将子类指针转换成基类指针此时编译器会根据偏移做转换。在visual studio,x64环境下测试,下面的​​​Parent p = Child();​​是父类对象,由子类来实例化对象。

#include <iostream>
using namespace std;

class Parent{
public:
int a;
int b;
};

class Child:public Parent{
public:
virtual void test(){}
int c;
};

int main() {
Child c = Child();
Parent p = Child();
cout << sizeof(c) << endl;//24
cout << sizeof(p) << endl;//8

Child* cc = new Child();
Parent* pp = cc;
cout << cc << endl;//0x7fbe98402a50
cout << pp << endl;//0x7fbe98402a58
cout << endl << "子类对象abc成员地址:" << endl;
cout << &(cc->a) << endl;//0x7fbe98402a58
cout << &(cc->b) << endl;//0x7fbe98402a5c
cout << &(cc->c) << endl;//0x7fbe98402a60
system("pause");
return 0;
}

结果如下:

24
8
0000013AC9BA4A40
0000013AC9BA4A48

子类对象abc成员地址:
0000013AC9BA4A48
0000013AC9BA4A4C
0000013AC9BA4A50
请按任意键继续. . .

分析上面的结果:
(1)第一行24为子类对象的大小,首先是虚函数表指针8B,然后是2个继承父类的​​​int​​​型数值,还有1个是该子类本身的​​int​​​型数值,最后的4是填充的。
(2)第二行的8为父类对象的大小,该父类对象由子类初始化,含有2个​​​int​​​型成员变量。
(3)子类指针​​​cc​​​指向又​​new​​​出来的子类对象(第三个),然后父类指针​​pp​​​指向这个子类对象,这两个指针的值:
父类指针​​​pp​​​值:0000013AC9BA4A48
子类指针​​​cc​​​值:0000013AC9BA4A40
即发现如之前所说的:如果将子类指针转换成基类指针此时编译器会根据偏移做转换。我测试环境是64位,所以指针为8个字节。转换之后pp和cc相差一个虚表指针的偏移。
(4)​​​&(cc->a)​​​的值即 0000013AC9BA4A48,和​​pp​​值是一样的,注意前面的 0000013AC9BA4A40到0000013AC9BA4A47其实就是子类对象的虚函数表指针了。

49.inline内联函数

  • 只有当函数只有 10 行甚至更少时才将其定义为内联函数。内联函数是为了解决程序中函数调用的效率问题,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的i节省。
  • 定义: 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
  • 优点: 当函数体比较小的时候, 内联该函数可以令目标代码更加高效. 对于存取函数以及其它函数体比较短, 性能关键的函数, 鼓励使用内联.
  • 缺点: 滥用内联将导致程序变慢. 内联可能使目标代码量或增或减, 这取决于内联函数的大小. 内联非常短小的存取函数通常会减少代码大小, 但内联一个相当大的函数将戏剧性的增加代码大小. 现代处理器由于更好的利用了指令缓存, 小巧的代码往往执行更快。
  • 几点结论:
  • 一个较为合理的经验准则是, 不要内联超过 10 行的函数. 谨慎对待析构函数, 析构函数往往比其表面看起来要更长, 因为有隐含的成员和基类析构函数被调用!
  • 另一个实用的经验准则:内联那些包含循环或 switch 语句的函数常常是得不偿失 (除非在大多数情况下, 这些循环或 switch 语句从不被执行).
  • 有些函数即使声明为内联的也不一定会被编译器内联。比如虚函数和递归函数(递归调用堆栈的展开并不像循环那么简单, 比如递归层数在编译时可能是未知的, 大多数编译器都不支持内联递归函数)就不会被正常内联。虚函数内联的主要原因则是想把它的函数体放在类定义内, 为了图个方便, 抑或是当作文档描述其行为, 比如精短的存取函数。

50.C++宏定义

(1)其实C语言中就有宏定义了,​​#define​​将一个标识符(该标识符被称为宏名)定义为一个字符串(被称为替换文本)。

C/C++基础知识整理_派生类_06


(2)在一个集成的开发环境如Turbo C中将编写好的源程序进行编译时,实际经过了预处理、编译、汇编和链接过程,其中在预处理阶段就有一步,叫宏展开(单纯的展开和替换)。

#define <宏名>  <字符串>
#define PI 3.1415926

51.内存泄漏是啥

内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存。比如,指针本身作为局部变量(不同于它指向的内存),当声明指针的函数返回时,指针将不再在作用域中,因此被丢弃,而​​new​​运算符分配的内存不会自动释放,所以这些内存将不可用。

  • 内存泄漏常指 堆内存泄漏,因为堆是动态分配的,由用户来控制,如果使用不当,则会产生内存泄漏。比如使用​​malloc​​​、​​calloc​​​、​​realloc​​​、​​new​​​ 等分配内存时,使用完后要调用相应的​​free​​​ 或​​delete​​ 释放内存。
  • 3类内存泄漏:
  • 堆内存泄漏:new/malloc分配内存,未使用对应的delete/free回收
  • 系统资源泄漏, Bitmap, handle,socket等资源未释放
  • 没有将基类析构函数定义称为虚函数,(使用基类指针或者引用指向派生类对象时)派生类对象释放时将不能正确释放派生对象部分。

举个简单栗子:指针重新赋值

char * p = (char *)malloc(10);
char * np = (char *)malloc(10);

其中,指针变量 p 和 np 分别被分配了 10 个字节的内存。

C/C++基础知识整理_虚函数_07


如果执行​​p=np;​​后,指针变量 p 被 np 指针重新赋值,其结果是 p 以前所指向的内存位置变成了孤立的内存。它无法释放,因为没有指向该位置的引用,从而导致 10 字节的内存泄漏。

C/C++基础知识整理_面试_08

52.如何应对内存泄漏

内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。

#include <iostream>
#include <cstring>

using namespace std;

class A{
private:
char *p;
unsigned int p_size;

public:
A(unsigned int n = 1) // 构造函数中分配内存空间
{
p = new char[n];
p_size = n;
};
~A() // 析构函数中释放内存空间
{
if (p != NULL)
{
delete[] p; // 删除字符数组
p = NULL; // 防止出现野指针
}
};
char *GetPointer()
{
return p;
};
};
void fun()
{
A ex(100);
char *p = ex.GetPointer();
strcpy(p, "Test");
cout << p << endl;
}
int main()
{
fun();
return 0;
}

但这样做并不是最佳的做法,在类的对象复制时,程序会出现同一块内存空间释放两次的情况,如下程序:

void fun1()
{
A ex(100);
A ex1 = ex;
char *p = ex.GetPointer();
strcpy(p, "Test");
cout << p << endl;
}

对于 ​​fun1​​​ 这个函数中定义的两个类的对象而言,在离开该函数的作用域时,会两次调用析构函数来释放空间,但是这两个对象指向的是同一块内存空间,所以导致同一块内存空间被释放两次(在VS中是报错​​block_type_is_valid​​),可以通过增加计数机制来避免这种情况,或者使用智能指针 or 内存泄漏检测工具valgrind:

C/C++基础知识整理_虚函数_09

54.sizeof(vector)大小

​sizeof(vec)​​​只取决于​​vector​​里面存放的数据类型,与元素个数无关。该值与编译器相关的。

#include<iostream>
#include<vector>
#include<stdlib.h>
using namespace std;

int main(){
vector<int>vec;
for(int i = 0; i < 100; i++){
vec.push_back(i);
cout << "vector的大小:" << sizeof(vec) << endl;
cout << "vector内的元素个数:" << vec.size() << endl;
}
system("pause");
return 0;
}

结果为:

vector的大小:24
vector内的元素个数:1
vector的大小:24
vector内的元素个数:2
......
vector内的元素个数:99
vector的大小:24
vector内的元素个数:100

虽然 ​​std :: vector​​ 由标准定义,可以有不同的实现。上面结果的24个大小可以解释为3个指针在64位体系结构上的大小,即有3 x 8 = 24字节)。这些指针可以是:向量开头;向量结尾;向量的保留内存结尾(即向量的容量)

55.搜索框字条的数据结构

字典,树。

56.C++的新类型转换

C++将强制类型转换分为四种不同的类型,分别是static_cast、const_cast、dynamic_cast、reinterpret_cast,其用法为xxx_cast(Expression)。

(1)static_cast

使用要点:

  • 用于基本类型之间的转换;(基本类型:int、float、double……)
  • 不能用于基本类型指针间的转换;
  • 用于有继承关系类对象之间的转换和类指针之间的转换;

(2)const_cast

使用要点:

  • 用于去除变量的只读属性;
  • 强制转换的目标类型必须是指针或引用;

(3)reinterpret_cast

使用要点:

  • 用于指针类型间的强制转换;
  • 用于整数和指针类型间的强制转换;

(4)dynamic_cast

使用要点:

  • 用于有继承关系的类指针间的转换;
  • 用于有交叉关系的类指针间的转换;
  • 具有类型检查的功能;
  • 需要虚函数的支持;

参考资料:​​javascript:void(0)​

57.C++的左值引用和右值引用,右值引用解决啥问题

  • 简单说,在赋值表达式中,出现在等号左边的就是左值,等号右边的就是右值。另一种说法,可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。
  • 用右值引用变量声明,就会少一次对象的析构及一次对象的构造。避免无谓的复制,提高程序性能。
  • 引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名

58.C++多线程用过吗

  • 多线程指的是在一个程序进程中处理控制流的多路并行通道,它在所有操作系统上为运行该程序提供了相同程度的并发性。
  • C++ 11 之后添加了新的标准线程库​​std::thread​​​,​​std::thread​​​ 在​​<thread>​​ 头文件中声明。

(1)并行和并发的概念:

  • 并行:同时执行,计算机在同一时刻,在某个时间点上处理两个或以上的操作。(如下图)
    判断一个程序是否并行执行,只需要看某个时刻上是否多两个或以上的工作单位在运行。一个程序如果是单线程的,那么它无法并行地运行。利用多线程与多进程可以使得计算机并行地处理程序(当然 ,前提是该计算机有多个处理核心)。

C/C++基础知识整理_c++_10

  • 并发:同一时间段内可以交替处理多个操作。如下图。每个队列都有自己的安检窗口,两个队列中间没有竞争关系,队列中的某个排队者只需等待队列前面的人安检完成,然后再轮到自己安检。

C/C++基础知识整理_虚函数_11

如果我们将程序的结构设计为可以并发执行的,那么在支持并行的机器上,我们可以将程序并行地执行。因此,并发重点指的是程序的设计结构,而并行指的是程序运行的状态。并发编程,是一种将一个程序分解成小片段独立执行的程序设计方法。

(2)并发的基本方法

  • 多进程:
  • 如你和小伙伴要开发一个项目,但是已经放假回家了,所以通过wechat联系,各自工作时互不干扰。这里的小伙伴代表线程,工作地点代表一个处理器,这个场景的每个小伙伴是一个单线程的进程(拥有独立的处理器)。
  • 多个进程独立地运行,它们之间通过进程间常规的通信渠道传递讯息(信号,套接字,文件,管道等),这种进程间通信不是设置复杂就是速度慢,这是因为为了避免一个进程去修改另一个进程,操作系统在进程间提供了一定的保护措施,当然,这也使得编写安全的并发代码更容易。运行多个进程也需要固定的开销:进程的启动时间,进程管理的资源消耗。
  • 多线程:
  • 上学时,你和小伙伴在实验室共同讨论问题,头脑风暴,更有效地沟通项目。这里的场景即只有一个处理器,所有小伙伴都是属于同一进程的线程。
    C++11 标准提供了一个新的线程库,内容包括了管理线程、保护共享数据、线程间的同步操作、低级原子操作等各种类。标准极大地提高了程序的可移植性,以前的多线程依赖于具体的平台,而现在有了统一的接口进行实现。
  • 在当个进程中运行多个线程也可以并发。线程就像轻量级的进程,每个线程相互独立运行,但它们共享地址空间,所有线程访问到的大部分数据如指针、对象引用或其他数据可以在线程之间进行传递,它们都可以访问全局变量。进程之间通常共享内存,但这种共享通常难以建立且难以管理,缺少线程间数据的保护。因此,在多线程编程中,我们必须确保每个线程锁访问到的数据是一致的。

(3)C++11中的并发与多线程

C++11 新标准中引入了几个头文件来支持多线程编程:(所以我们可以不再使用 CreateThread 来创建线程,简简单单地使用 std::thread 即可。)

​< thread >​​​ :包含std::thread类以及std::this_thread命名空间。管理线程的函数和类在 中声明.
​​​< atomic >​​​ :包含std::atomic和std::atomic_flag类,以及一套C风格的原子类型和与C兼容的原子操作的函数。
​​​< mutex >​​​ :包含了与互斥量相关的类以及其他类型和函数
​​​< future >​​​ :包含两个Provider类(std::promise和std::package_task)和两个Future类(std::future和std::shared_future)以及相关的类型和函数。
​​​< condition_variable >​​ :包含与条件变量相关的类,包括std::condition_variable和std::condition_variable_any。

59.智能指针怎么解决交叉引用,造成的内存泄漏

结论:创建对象时使用​​shared_ptr​​​强智能指针指向,其余情况都使用​​weak_ptr​​弱智能指针指向。

(1)交叉引用的栗子:

当A类中有一个指向B类的​​shared_ptr​​​强类型智能智能,B类中也有一个指向A类的​​shared_ptr​​​强类型智能指针。
​​​main​​函数执行后有两个强智能指针指向了对象A,对象A的引用计数为2,B类也是:

#include <iostream>
#include <memory>
using namespace std;

class B;
class A{
public:
shared_ptr<B> _bptr;
};

class B{
public:
shared_ptr<A> _aptr;
};

int main(){
shared_ptr<A> aptr(new A());
shared_ptr<B> bptr(new B());
aptr->_bptr = bptr;
bptr->_aptr = aptr;
return 0;
}

C/C++基础知识整理_派生类_12


而当主函数​​main​​​的​​return​​​返回后,对象​​A​​​的引用计数减一变为1(​aptr没指向​A​对象了),​​B​​对象也是,引用计数不为0,即不能析构2个对象释放内存,造成内存泄漏。

(2)解决方案

将类A和类B中的​​shared_ptr​​​强智能指针都换成​​weak_ptr​​弱智能指针;

class A{
public:
weak_ptr<B> _bptr;
};
class B{
public:
weak_ptr<A> _aptr;
};

​weak_ptr​​弱智能指针,虽然有引用计数,但实际上它并不增加计数,而是只观察对象的引用计数。所以此时对象A的引用计数只为1,对象B的引用计数也只为1。

C/C++基础知识整理_派生类_13

60.头文件functional(C++11特性)

  • ​<functional>​​是C++标准库中的一个头文件,定义了C++标准中多个用于表示函数对象(function object)的类模板,包括算法操作、比较操作、逻辑操作;
  • 以及用于绑定函数对象的实参值的绑定器(binder)。这些类模板的实例是具有函数调用运算符(function call operator)的C++类,这些类的实例可以如同函数一样调用。不必写新的函数对象,而仅是组合预定义的函数对象与函数对象适配器(function object adaptor),就可以执行非常复杂的操作。

61.零拷贝

62.多线程和epoll有关系吗

(1)epoll的引出

当需要读两个以上的I/O的时候,如果使用阻塞式的I/O,那么可能长时间的阻塞在一个描述符上面,另外的描述符虽然有数据但是不能读出来。

(2)解决方案

1.使用多进程或者多线程
但是这种方法会造成程序的复杂,而且对与进程与线程的创建维护也需要很多的开销。(Apache服务器是用的子进程的方式,优点可以隔离用户)

2.轮询
用一个进程,但是使用非阻塞的I/O读取数据,当一个I/O不可读的时候立刻返回,检查下一个是否可读,这种形式的循环为轮询(polling),这种方法比较浪费CPU时间,因为大多数时间是不可读,但是仍花费时间不断反复执行read系统调用。

3.异步I/O(asynchronous I/O)
当一个描述符准备好的时候用一个信号告诉进程,但是由于信号个数有限,多个描述符时不适用。

4.I/O多路转接(I/O multiplexing)(多路复用)
先构造一张有关描述符的列表(epoll中为队列),然后调用一个函数,直到这些描述符中的一个准备好时才返回,返回时告诉进程哪些I/O就绪。select和epoll这两个机制都是多路I/O机制的解决方案,select为POSIX标准中的,而epoll为Linux所特有的。

63.C和C++结构体的区别

64.C怎么调用C++函数

65.函数在内存的什么位置

66.static关键字作用

  • 隐藏:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
  • 保持变量内容的持久:存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
  • 默认初始化为0(static变量)

67.一千万个整数里面快速查找某个整数

  • 散列表结构方案
  • 布尔数组结构方案
  • 使用bitmap来进行存储

68. 如何解决菱形继承的二义性

class Base;

class SubA : public Base;

class SubB : public Base;

class AB : public SubA, public SubB;
  • 在AB调用Base的fun时,指明继承链路,如: AB ab;ab.SubA::fun(); 这样就祛除了二义性
  • 第一种方法能够祛除二义性,但基类的副本仍然是多个,这时就需要用到“虚继承”

69.死锁的4个必要条件

对于永久性资源,产生死锁有四个必要条件:

  • 互斥条件。一个资源只能被一个进程占用
  • 不可剥夺条件。某个进程占用了资源,就只能他自己去释放。
  • 请求和保持条件。某个进程之前申请了资源,我还想再申请资源,之前的资源还是我占用着,别人别想动。除非我自己不想用了,释放掉。
  • 循环等待条件。一定会有一个环互相等待。

A推出B”="如果A成立,那么B成立"=“A是B的充分条件”=“B是A的必要条件”

70.sizeof和strlen的区别

  • ​sizeof​​是运算符,而不是函数。它的功能是: 获得保证能容纳实现所建立的最大对象的字节大小。由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。
  • ​strlen(...)​​是函数,要在运行时才能计算。参数必须是字符型指针(char*), 且必须是以’\0’结尾的。当数组名作为参数传入时,实际上数组就退化成指针了。
eg1、char arr[10] = "Hello";
int len_one = strlen(arr);
int len_two = sizeof(arr);
cout << len_one << " and " << len_two << endl;
//输出结果为:5 and 10

71.share_ptr和unique_ptr区别

72.C++模板和多态关系

73.函数指针

74.系统调用write的用法

每个运行中的程序被称为进程,它有一些与之关联的文件描述符,通常我们定义他们为fd。这是一些小值整数。你可以通过它们访问打开的设备或者文件。有多少文件描述符可用取决于系统的配置情况。当一个程序开始运行的时,它一般会有3个已经打开的文件描述符:

0:标准输入

1:标准输出

2:标准错误

除此之外我们可以通过调用open,把其他文件描述符与文件和设备相关联。一个自动或者手动打开的文件描述符就可以使用write系统调用来创建一些简单的程序了。

(1)write函数原型

write:系统调用write的作用是把缓冲区buf的前n bytes 个字节写入与文件描述符fd相关联的文件中。

write函数在​​<unistd.h>​​头文件中定义。

原型是:​​ssize_t write(int fd, const void *buf, size_t count);​

(2)说明

write()函数尝试从buf开头的缓冲区中读取count个字节,并将其写入到文件描述符fd中。

写入的字节数可能会小于count。比如:底层的物理截止没有足够的空间,或者遇到​​RLIMIT_FSIZE​​ 资源限制,或者函数调用的时候被信号打断,没有写完。

对于可搜索文件(意思是可以使用​​lseek​​​函数的文件,一般常规文件都可以),写入的位置是当前的文件偏移量上,写完之后文件偏移量按字节递增。如果这个文件的打开方式是使用open函数,参数是“​​O_APPEND​​”,那么文件的偏移量就被设置到文件的末尾。这时候写入到文件,就属于在最后追加。

END:面经

(1)​​字节跳动阿秀C++面经​​​ (2)​​如何系统学习C++ 编程指北​​ (3)​​两万字总结《C++ Primer》要点​​ (4)​​2022届校招C++后端服务器开发/实习​

附:C++基础中的基础

1.构造函数

#include<iostream>
#include<string>
using namespace std;

class Car{
public:
string Name;
string Color;
double Price;
};
int main(){
Car myCar;
myCar.Name="Ford";
myCar.Color="red";
myCar.Price=50000;
cout<<"Name : "<<myCar.Name<<endl;
system("pause");
}

为了让赋值更简单,可以使用构造函数,其中构造函数有2个原则:
(1)有一个返回类型
(2)构造函数与类同名
所以可以按照下面这样赋值了:

#include<iostream>
#include<string>
using namespace std;

class Car{
public:
string Name;
string Color;
double Price;

Car(string name,string color,double price){
Name=name;
Color=color;
Price=price;
}
};
int main(){
Car myCar("Ford","red",50000);
cout<<"Name : "<<myCar.Name<<endl;
system("pause");
}

2.封装

#include<iostream>
#include<string>
using namespace std;

class Car{
private:
string Name;
string Color;
double Price;
bool IsBroken;
public:
Car(string name,string color,double price){
Name=name;
Color=color;
Price=price;
IsBroken=false;
}
void getInfo(){//类的一个方法
cout<<"Name: "<<Name<<endl;
cout<<"Color:"<<Color<<endl;
cout<<"Price:"<<Price<<"$"<<endl;
}
void crashCar(){
cout<<Name<<" crashed"<<endl;
IsBroken=true;
}
void repairCar(){
cout<<Name<<" repaired"<<endl;
IsBroken=false;
}
void move(){
if(IsBroken)
cout<<Name<<" is broken"<<endl;
else
cout<<Name<<" is driving"<<endl;
}
};
int main(){
Car ford("Ford","red",50000);
Car volvo("Volvo","black",70000);

ford.move();
ford.crashCar();
ford.move();
ford.repairCar();
ford.move();
//ford.getInfo();
//volvo.getInfo();
//cout<<"Name : "<<myCar.Name<<endl;
system("pause");
}

上面这个栗子就是类的变量设为私有属性了,只有几个方法是共有的(外部能访问/调用)。

3.继承

#include<iostream>
#include<string>
using namespace std;

class Car{
private:
string Name;
string Color;
double Price;
bool IsBroken;
public:
Car(string name,string color,double price){
Name=name;
Color=color;
Price=price;
IsBroken=false;
}
void getInfo(){//类的一个方法
cout<<"Name: "<<Name<<endl;
cout<<"Color:"<<Color<<endl;
cout<<"Price:"<<Price<<"$"<<endl;
}
void crashCar(){
cout<<Name<<" crashed"<<endl;
IsBroken=true;
}
void repairCar(){
cout<<Name<<" repaired"<<endl;
IsBroken=false;
}
void move(){
if(IsBroken)
cout<<Name<<" is broken"<<endl;
else
cout<<Name<<" is driving"<<endl;
}
};

class FlyingCar:public Car{
public:
FlyingCar(string name,string color,double price):Car(name,color,price){

}
};
int main(){
Car ford("Ford","red",50000);
FlyingCar flyingCar("Sky Fury","black",50000);
flyingCar.getInfo();
system("pause");
}

上面栗子中FlyingCar继承了Car类,注意构造函数FlyingCar是要在public下,不然默认是私有属性(构造函数不可访问)

4.多态

多态的提前:①用父类引用子类对象;②子类重写父类方法。
1.什么是多态性?
多态性,之所以叫多态,就是一种事物可以有多种表现的形态,在java中就是一个类(一种事物)的一个方法表现出多种输出结果(多种形态)
阿猫阿狗继承Animal的例子来所吧(具体代码就不写了),
Animal animal1= new Cat();
Animal animal2= new Dog();
同时调用Cat和Dog中重写Animal中的eat()方法
animal1.eat();
animal2.eat();
输出结果为:猫吃鱼,狗吃骨头
从这里我们就可以看出同一个事物Animal类的一个方法表现出多种结果,这就是多态

2.什么时候用到多态?
当多个有共同父类的子类调用同一需要传入子类对象的方法时,我们可以用子类共有的父类作为传入对象,用多态的思想父类引用子类对象可以少些好多代码

#include<iostream>
#include<string>
using namespace std;

class Car{
private:
string Color;
double Price;
protected:
string Name;
bool IsBroken;
public:
Car(string name,string color,double price){
Name=name;
Color=color;
Price=price;
IsBroken=false;
}
void getInfo(){//类的一个方法
cout<<"Name: "<<Name<<endl;
cout<<"Color:"<<Color<<endl;
cout<<"Price:"<<Price<<"$"<<endl;
}
void crashCar(){
cout<<Name<<" crashed"<<endl;
IsBroken=true;
}
void repairCar(){
cout<<Name<<" repaired"<<endl;
IsBroken=false;
}
void move(){
if(IsBroken)
cout<<Name<<" is broken"<<endl;
else
cout<<Name<<" is driving"<<endl;
}
};

class FlyingCar:public Car{
public:
FlyingCar(string name,string color,double price):Car(name,color,price){

}
void move(){
if(IsBroken)
cout<<Name<<" is broken"<<endl;
else
cout<<Name<<" is flying"<<endl;
}
};
class UnderwaterCar:public Car{
public:
UnderwaterCar(string name,string color,double price):Car(name,color,price){

}
void move(){
if(IsBroken)
cout<<Name<<" is broken"<<endl;
else
cout<<Name<<" is diving"<<endl;
}
};
int main(){
Car ford("Ford","red",50000);
FlyingCar flyingCar("Sky Fury","black",50000);
UnderwaterCar underwaterCar("Sea Storm","blue",60000);

ford.move();
flyingCar.move();
underwaterCar.move();

cout<<endl;

Car* car1=&flyingCar;
Car* car2=&underwaterCar;
car1->crashCar();
car2->crashCar();

cout<<endl;

ford.move();
flyingCar.move();
underwaterCar.move();

cout<<endl;

car1->repairCar();
car2->repairCar();

cout<<endl;

ford.move();
flyingCar.move();
underwaterCar.move();

system("pause");
}

上面的例子就满足多态的提前:①用父类引用子类对象;②子类重写父类方法。

子类飞车和水车重写了move方法,父类也应用了子类对象。

输出如下:

C/C++基础知识整理_派生类_14


​多态的其他介绍​​。

Reference

[1] ​​指针常量和常量指针​​​

[2] ​​malloc和new的十点区别​​​

[3] ​​C++11新特性快速一览​​​

[4] ​​从4行代码看右值引用​​​

[5] ​​C++覆盖(虚函数的实现原理)​​​

[6] ​​面试被问,一千万个整数里面快速查找某个整数​

[7] ​​多重继承——菱形继承(二义性)​​​​

[8] ​​死锁的4个必要条件​​​

[9] ​​底层文件访问之一:write系统调用​