面向对象的三大特性

封装
封装是指的是将数据和操作数据的方法封装在类中,使得外部不能直接访问数据和方法,只能通过类的公共接口进行访问和修改。
1.隐藏实现细节:封装可以将类的实现细节隐藏起来,使得其他代码只能通过公共接口访问和操作数据。
2.提高安全性:封装可以保护数据的完整性,防止未经授权的访问和修改。
3.提高可维护性:封装使得修改类的内部实现时只需修改类内部的代码,而不会影响到其他代码。
继承
继承是指的是一个类(子类)通过继承另一个类(父类)的属性和方法,从而实现代码的扩展。继承是一种代码复用的方式,可以通过继承现有类来创建新类,而无需重新编写原来的类。
继承的三种方式也可以说下
多态
多态指的是同一个函数名可以调用不同类型的函数,从而实现不同对象之间的差异性行为。多态有两种形式:静态多态和动态多态。静态多态是通过函数重载和运算符重载来实现的,而动态多态则是通过虚函数来实现的。多态的目的是实现代码的灵活性和可扩展性,使得程序可以更好地适应不同的应用场景。
多态
多态指的是不同对象在调用同一个函数时表现出不同的行为。
多态分为两种,
静态多态和动态多态,
静态多态在编译阶段进行绑定,通过函数重载和运算符重载进行实现
动态多态在运行阶段进行绑定,动态多态的产生需要有继承关系,且子类对象需要重写父类的虚函数.
动态多态的使用就是使父类指针或引用指向子类对象。
多态的目的是实现代码的灵活性和可扩展性,使得程序可以更好地适应不同的应用场景。
哪些函数不能是虚函数
构造函数,
构造函数初始化对象,派⽣类必须知道基类函数⼲了什么,才能进⾏构造;当有虚函数时,每⼀个类有⼀个虚表,每⼀个对象有⼀个虚表指针,虚表指针在构造函数中初始化;
内联函数,
内联函数在编译阶段进⾏函数体的替换操作,⽽虚函数在运⾏期间进⾏类型确定,所以内联函数不能是虚函数;
静态函数(其实这个想说的静态成员函数),
静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
友元函数,
友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
普通函数,
普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。

内存分区模型
C++程序在执行时,将内存大方向划分为4个区域
代码区:存放函数体的二进制代码,由操作系统进行管理的
全局区:存放全局变量和静态变量以及常量
栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
堆区:由程序员使用new或者malloc进行分配,使用delete或者free进行释放
内存四区意义:
不同区域的数据,有不同的生命周期, 编程时有更大的灵活性
值传参,指针传参,引⽤传参,
值传递: 形参是实参的拷贝,改变形参的值并不会影响外部实参的值。
指针传递: 形参与实参指向同一块内存,当对形参进行操作时,就相当于对数据本身进行操作。
引用传递: 形参相当于是实参的“别名”,对形参的操作其实就是对实参的操。
引用
引用就是给变量起别名
引用必须初始化
引用在初始化后,不可以改变


引用做函数返回值
用引用作为返回值最大的好处就是在内存中不产生被返回值的副本。
但是有以下的限制:
不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁

引用的本质
本质:引用的本质在c++内部实现是一个指针常量.
指针常量的的指向是不可以被修改的,这也说明了为什么引用不可被更改

C++ 中 const 和 static 关键字(定义,⽤途)
static

const
有道云笔记:
const修饰变量
const修饰指针
const既修饰指针又修饰变量

常量引用:常量引用主要用来修饰形参,防止误操作

const修饰成员函数
const修饰成员函数就是在成员函数名后面加上const关键字,也叫常函数
常函数中只能访问成员变量,不能修改成员变量,如果想要在常函数中修改成员变量,在成员变量定义的时候加上mutable关键字就可以。
const修饰对象
声明对象前加const称该对象为常对象
常对象只能调用常函数,
常对象和常函数一样只能访问成员变量,不能修改成员变量,如果想要修改成员变量,在成员变量定义的时候加上mutable关键字就可以。

因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。

重载,重写,重定义?
运算符重载

函数重载
函数名可以相同,提高复用性
函数重载满足条件:

同一个作用域下
函数名称相同
函数参数类型不同 或者 个数不同 或者 顺序不同
注意: 函数的返回值不可以作为函数重载的条件
函数重载注意事项:
引用作为重载条件???
函数重载碰到函数默认参数(容易产生歧义,需要避免)

重写
当有继承关系的时候,子类从父类中继承过来的虚函数,无法满足子类的需求,那么子类就可以重写父类的虚函数,重写要求函数的返回值,函数名,参数类型和个数都与父类中该函数相同,且重写时,可以任意修改子类中重写函数的访问权限。

重定义
就是将父类中的非虚函数在子类中进行编写,隐藏掉从父类中继承过来的同名函数,
重定义只要求函数名与父类相同即可,其他不做要求。
(函数的返回值和参数列表可以和父类中的不同。)
c++所有的构造函数
类的对象被创建时,编译系统为对象分配内存空间,并自动调用构造函数,由构造函数完成成员的初始化工作。
即构造函数的作用:初始化对象的成员。

默认构造函数
一般构造函数
拷贝构造函数

移动构造函数
将其他对象的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。
2、移动构造函数的特点
(1)移动构造函数的第一个参数必须是自身类型的右值引用(不需要const,为啥?右值使用const没有意义),若存在额外的参数,任何额外的参数都必须有默认实参
(2)移动构造函数构造对象时不再分配新内存,而是接管源对象的内存,移动后源对象进入可被销毁的状态,所以源对象中如果有指针数据成员,那么它们应该在移动构造函数中应该赋值为置为空
(3)因为移动操作不分配内存,所以不会抛出任何异常,因此可以用noexcept指定。

C++ 的四种强制转换
C++ 的四种强制转换包括:static_cast, dynamic_cast, const_cast, reinterpret_cast
1.const_cast
去除 const 性质,或增加 const 性质, 是四个转换符中唯⼀⼀个可以操作常量的转换符
2.static_cast
于基本数据类型之间的转换,也可以用于指针或引用类型的转换,但需要保证转换后的类型是安全的。
(因为没有动态类型检查,向上转换 (派⽣类->基类)安全,向下转换(基类->派⽣类) 不安全,所以主要执⾏⾮多态的转换操作; )
3.dynamic_cast
用于多态类型之间的转换,主要用于基类和派生类之间的转换。如果转换成功,会返回指针或者引用,如果转换失败,会返回 nullptr(对于指针类型)或抛出异常(对于引用类型)。
向上转换:指子类向基类转换。    
         向下转换:指基类向子类转换。
4. reinterpret_cast
            reinterpret_cast可以做任何类型的转换,不过不保证转换结果,容易出问题。

C++中指针和引用的区别
1.指针是存储地址的,而引用只是一个别名;
2.指针可以被初始化为 NULL,并且指针指向可以被更改,而引用必须在定义时被初始化,且引用的对象不能改变;
3.使用 sizeof 查看大小,指针的大小为4字节(32位,如果要是64位的话指针为8字节),引用的大小是被引用对象的大小;
4.作为参数传递时,指针需要被解引用才可以进行相应操作; 而引用可以直接进行操作
5.指针可以是多级,而引用没有分级;
6.不能返回局部变量的引用。因为函数返回以后局部变量就会被销毁
6.如果返回动态分配内存的对象或者内存,必须使用指针,引用可能引起内存泄漏。(不理解)
空指针和野指针
空指针:指针变量指向内存中编号为0的空间,空指针指向的内存是不可以访问的
野指针:指针变量指向非法的内存空间,指向不可知
释放内存后指针不及时置空(野指针),那么可能出现非法访问的错误。
避免办法:指针释放后置NULL

说⼀下 const 修饰指针如何区分?
(叫法截然相反,但是具体的含义是一样的,回答的时候,不说具体的名称就可以)

什么是函数指针,如何定义函数指针,有什么使用场景
概念:
函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该函数的入口地址就是函数指针所指向的地址。
定义
形式如下:
int func(int a);
int (*f)(int a);
f = &func;

函数指针的应用场景:
回调(callback)。我们调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫Callback。

说说静态变量什么时候初始化?(没有找到确切答案)
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。

静态全局变量的创建过程是在编译阶段完成的,它们会被分配内存空间并保留在程序的整个生命周期内。而初始化过程则是在程序启动时进行的,即在main函数执行之前。

静态局部变量的创建过程是在编译阶段完成的,它们会被分配内存空间并保留在程序的整个生命周期内。而初始化过程则是在第一次调用包含静态局部变量的函数或代码块时进行的。
需要注意的是,静态局部变量的初始化只发生一次,即在第一次调用包含它的函数或代码块时。之后每次调用该函数或代码块时,静态局部变量将保持其值,并且不会被重新初始化。

1.静态局部变量:在第一次调用包含它们的函数或进入包含它们的作用域时初始化。
2.静态成员变量:
在C++中,静态成员变量在类定义的时候不会被初始化,而是在首次使用时进行初始化。这是因为静态成员属于类作用域,不与任何类对象关联,它在程序中只有一份拷贝。
在C++中,静态成员变量是在类定义时被创建,
在程序开始时初始化,与任何对象的存在无关。

3.静态全局变量是在编译时分配内存的()

答案解析
作用域
:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。
静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
类静态成员变量:类作用域。
所在空间
:都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
生命周期
15、堆和栈区别(c++八股文)(未背)
new , delete 和 malloc,free 异同
相同点:都是用于内存的动态申请与释放
不同点:
1.属性:new / delete 是c++关键字,需要编译器支持。 malloc/free是库函数,需要c/c++的头文件支持。
2.参数:使用new操作符申请内存分配时无须制定内存块的大小。而mallco则需要显式地指出所需内存的大小
3.返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,故new是符合类型 安全性的操作符。而malloc内存成功分配返回的是void *,需要通过类型转换将其转换为我们需要的类型。
4.在为自定义类型申请内存时:new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。 然后调用自定义类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调 用operator delete函数释放内存(通常底层使用free实现)。 malloc/free是库函数,只能动态的申请和释放 内存,不能对自定义类型对象进行构造和析构。
5.重载:C++允许重载 new/delete 操作符。而malloc为库函数不允许重载。
6.内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分 配内存。其中自由存储区为:C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内 存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C 语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new 就可以不位于堆中。

extern
在C语言中,修饰符 extern 用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用。

define 和 const 区别(编译阶段、安全性、内存占用等)
对于 define 来说,宏定义实际上是在预编译阶段进行处理,没有类型,也就没有类型检查,仅仅做的是遇到宏定义进行字符串的展开,遇到多少次就展开多少次,而且这个简单的展开过程中,很容易出现边界效应,达不到预期的效果。因为 define 宏定义仅仅是展开,因此运行时系统并不为宏定义分配内存,但是从汇编 的角度来讲。define 却以立即数的方式保留了多份数据的拷贝。
对于 const 来说,const是在编译期间进行处理的,const有类型,也有类型检查,程序运行时系统会为 const 常量分配内存,而且从汇编的角度讲,const 常量在出现的地方保留的是真正数据的内存地址,所以只保留了一份数据的拷贝,省去了不必要的内存空间。而且,有时编译器不会为普通的 const 常量分配内存,而是直接将 const 常量添加到符号表中,省去了读取和写入内存的操作,效率更高。
计算下⾯⼏个类的⼤⼩

23、虚函数相关(虚函数表,虚函数指针),虚函数的实现原理
24、编译器处理虚函数表应该如何处理
(上面这两个还是挺重要的)
25、析构函数⼀般写成虚函数的原因
(本质问的还是虚析构函数)

直观的讲:是为了降低内存泄漏的可能性。
举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么父类指针指向的对象的析构函数地址就被早绑定为基类的析构函数,仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄漏。
如果基类的析构函数定义成虚函数,那么父类指针指向的对象的析构函数地址就被晚绑定为子类的析构函数,那么编译器就可以执行派生类的析构函数,再执行基类的析构函数,成功释放内存。

直观的讲:是为了降低内存泄漏的可能性。
举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数 (该对象的析构函数的函数地址早就被绑定为基类的析构函数),仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄漏。
如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存。

构造函数和析构函数的作⽤
构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。
29、构造函数的执⾏顺序?析构函数的执⾏顺序?
构造函数顺序
1.基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
(基类构造函数。如果有多个基类,则构造函数的调用顺序是父类被继承的顺序,先被继承的父类,其构造函数先被调用)
2.成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
3.派生类构造函数。
析构函数顺序
1.调用派生类的析构函数;
2.调用成员类对象的析构函数
3.调用基类的析构函数。

31、静态绑定和动态绑定的介绍
说起静态绑定和动态绑定,我们首先要知道静态类型和动态类型,静态类型就是它在程序中被声明时所采用的类型,在编译期间确定。动态类型则是指“目前所指对象的实际类型",在运行期间确定。
静态绑定,又名早绑定,绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期间
动态绑定,又名晚绑定,绑定的是动态类型,所对应的函数或属性依赖于动态类型,发生在运行期间。
比如说,virtual 函数是动态绑定的,非虚函数是静态绑定的,缺省参数值也是静态绑定的。这里呢,就需要注意我们不应该重新定义继承而来的缺省参数,因为即使我们重定义了,也不会起到效果。因为一个基类的指针指向一个派生类对象,在派生类的对象中针对虚函数的参数缺省值进行了重定义,但是缺省参数值是静态绑定的,静态绑定绑定的是静态类型相关的内容,所以会出现一种派生类的虚函数实现方式结合了基类的缺省参数值的调用效果,这个与所期望的效果不同。

浅拷贝和深拷贝
浅拷贝:浅拷贝执行的是简单的赋值,当被拷贝的对象中有动态分配的内存,也只是将该内 存地址进行拷贝,并没有开辟新的内存空间。那么在析构的时候就会对同一块内存 析构两次,造成错误。
深拷贝:当被拷贝的对象中有动态分配的内存,就会开辟一块新的内存用来存储拷贝过来的 数据。这样不会造成内存的重复释放。

拷贝构造函数调用时机
C++中拷贝构造函数调用时机通常有三种情况
使用一个已经创建完毕的对象来初始化一个新对象
把对象以值传递的方式给函数参数传值
以值方式返回局部对象
34、为什么拷⻉构造函数必需使用引⽤传递,不能是值传递?
35.结构体内存对⻬⽅式和为什么要进⾏内存对⻬?
⾸先我们来说⼀下结构体中内存对⻬的规则:
第一个数据成员的起始地址与结构体的起始地址相同,即偏移量为0,以后的每个数据成员的偏移量为数据成员本身⻓度的整数倍。
在所有的数据成员完成各⾃对⻬之后,结构体或联合体本身也要进⾏对⻬,整体⻓度是最⻓的数据成员的⻓度的倍数。
那么内存对⻬的作⽤是什么呢?
经过内存对⻬之后,可以提高CPU 的内存访问的速度。
不是所有的硬件平台都能访问任意地址上的任意数据,只能在某些地址处取某些特定类型的数据。所以内存对⻬还有利于平台移植。

(经过内存对⻬之后,CPU 的内存访问速度⼤⼤提升。因为 CPU 把内存当成是⼀块⼀块的,块的⼤⼩可以是2,4,8,16 个字节,因此 CPU 在读取内存的时候是⼀块⼀块进⾏读取的,块的⼤⼩称为内存读取粒度。⽐如说 CPU 要读取⼀个 4 个字节的数据到寄存器中(假设内存读取粒度是 4),如果数据是从 0 字节开始的, 那么直接将 0-3 四个字节完全读取到寄存器中进⾏处理即可。
如果数据是从 1 字节开始的,就⾸先要将前 4 个字节读取到寄存器,并再次读取 4-7 个字节数据进⼊寄存器,接着把 0 字节,5,6,7 字节的数据剔除,最后合并 1,2,3,4 字节的数据进⼊寄存器,所以说,当内存没有对⻬时,寄存器进⾏了很多额外的操作,⼤⼤降低了 CPU 的性能。 )

1.1.7 简述C++从代码到可执行二进制文件的过程
参考回答
        C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
答案解析

预编译:这个过程主要的处理操作如下:

(1) 将所有的#define删除,并且展开所有的宏定义

(2) 处理所有的条件预编译指令,如#if、#ifdef

(3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。

(4) 过滤所有的注释

(5) 添加行号和文件名标识。

编译:这个过程主要的处理操作如下:

(1) 词法分析:将源代码的字符序列分割成一系列的记号。

(2) 语法分析:对记号进行语法分析,产生语法树。

(3) 语义分析:判断表达式是否有意义。

(4) 代码优化:

(5) 目标代码生成:生成汇编代码。

(6) 目标代码优化:

汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。

链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。

链接分为静态链接和动态链接。

静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。

而动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。

虚基类,虚继承???
子类,父类,爷类
子类,父类的大小以及原因??

友元

lambda表达式
Lambda表达式 | 爱编程的大丙 (subingwen.cn)
lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。
capture opt -> ret {body;};
其中capture是捕获列表,params是参数列表,opt是函数选项,ret是返回值类型,body是函数体。

智能指针
智能指针是存储 指向动态分配(堆)对象的指针 的类,用于生存期的控制,能够自动地销毁动态分配的对象,防止内存泄露。
智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件
std::shared_ptr:共享的智能指针
std::unique_ptr:独占的智能指针
std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。
std::shared_ptr:共享的智能指针
共享智能指针 | 爱编程的大丙 (subingwen.cn)

初始化
通过构造函数、std::make_shared辅助函数以及reset方法

不要使用一个原始指针初始化多个shared_ptr。
int *p = new int;
shared_ptr p1(p);
shared_ptr p2(p);

使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。

对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。

共享智能指针调用get()方法,获取原始指针,然后访问数据
也可以直接使用智能指针访问数据

删除器
当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。
删除器函数也可以是lambda表达式,因此代码也可以写成下面这样:
int main()
{
shared_ptr ptr(new int(250), [](int* p) {delete p; });
return 0;
}
在上面的代码中,lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址之后函数体内部就可以完成删除操作了。

在C++11中使用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象(后面就支持删除数组对象了),具体的处理代码如下:
int main()
{
shared_ptr ptr(new int[10], [](int* p) {delete[]p; });
return 0;
}
在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:
int main()
{
shared_ptr ptr(new int[10], default_delete<int[]>());
return 0;
}