八股文之C++(持续更新,如有错误欢迎指正)

C++

面向对象的理解

把事物给对象化,包括其属性和行为,根据功能来划分问题

把构成问题的事物分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述事物在整个解决问题的步骤中的行为

C++源文件处理过程

预处理:进行简单的宏替换等,生成预编译文件

编译:将预编译文件转换成特定汇编代码,生成汇编文件

汇编:将编译文件转化成机器码,生成可重定位文件

链接:将多个目标文件和所需要的库连接成最终的可执行目标文件

const和#define的区别

const是保护变量不被修改,define是宏定义

const是在编译运行阶段起作用,而define是在预处理阶段起作用

const有对应的数据类型,要进行判断,可以避免一些低级错误,define只是进行简单的替换

const常量可以进行调试,define不行,因为在预处理阶段就替换了

const定义的只读变量在程序运行过程中只有一个备份,define定义的宏常量在内存中有若干备份

动态库静态库

静态库:gcc进行链接时,会把静态库代码打包到可执行文件中

动态库:链接时只保存动态库的信息,在程序启动后,动态库会被动态分配到内存中,ldd命令可以查看动态库依赖关系

静态库优缺点:

​ 优:打包到应用程序中,加载执行速度快

​ 发布程序时无需提供静态库,移植方便

​ 缺:消耗系统资源,浪费内存

动态库优缺点:

​ 优:可以实现进程间内存共享,共享库,多个进程使用相同的资源时只需要加载一次

​ 可以动态控制何时加载动态库

​ 缺:加载速度比静态库慢

​ 发布程序时需要提供依赖的静态库

sizeof和strlen的区别

sizeof是运算符,strlen是库函数

sizeof求的是字符串在内存中的长度,包含结束标志位;strlen是不加结束标志位‘\0’,表示字符的长度

sizeof可以用类型、函数做参数,strlen只能用char*做参数

sizeof在编译时就能计算,strlen只能在运算时才计算

resize和reserve

resize():改变当前容器内含有元素的数量(size),会新增0补元素

reserve():改变当前容器的最大容量(capacity),它不生成元素,只是确定这个容器允许放入多少对象。如果reserve的值大于当前,会重新分配一块能存放下的空间,然后把之前里面的对象都拷贝过来,再销毁之前的内存

字节对齐、作用及原因

计算机在访问特定类型的变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐

原因及作用:

1、减少cpu访问变量的次数,cpu可以更快的读取数据

2、合理的使用字节对齐,可以节省内存的大小。

3、减少 cpu 访问数据的出错性(有些 cpu 必须内存对齐,否则指针访问会出错),不同硬件平台进行数据通信,数据对齐可能会不一致,需要加入伪指令进行操作,防止灾难性性bug。

指针常量和常量指针

指针常量:指针类型的常量,指向的地址不可修改,但是指向的内容可以修改, * const

常量指针:指向常量的指针,本质上是一个指针,指针指向的内容是不可修改的,const *

指针函数和函数指针

指针函数是一个函数,返回值是指针类型 int * f(int x,int y) 首先是一个函数,返回一个指针

函数指针是一个指针,指针指向的是一个函数 int (*f) (int x, int y) 首先f是一个指针,指向一个函数

深拷贝和浅拷贝

浅拷贝:创建一个对象用另一个现成的对象初始化它的时候,只复制了成员(简单赋值),但是不拷贝分配给成员的资源

深拷贝:一个对象创建时,如果分配了资源,就需要定义自己的拷贝构造函数,不仅拷贝成员也拷贝函数

struct和class

都可以用来定义类,都可以继承

struct默认的访问和继承权限是public,class默认的访问和继承权限是private

class还可以用来定义类模板形参

封装

什么是封装?如何实现?

通过特性和行为的组合来创建新数据类型,让接口与具体实现隔离

C++通过类来实现,尽量避免某个模块的行为干扰同一系统中其他模块,应让模块仅仅公开必须让外界知道的接口

封装控制

public:既可以在类内引用修改,也可以在类外引用修改

private:类内及友元可以访问,类外不能访问。想要访问此类数据成员,要在类内定义一个与类外对接数据的接口

​ 类内对数据进行赋值的时候只能用构造函数,public和private里的赋值无效。若没有用构造函数,编译器默认执行空的构造函数,此函数会给所有的数据成员赋值为0

protected:protected的数据成员只能被类内成员函数,子类及友元函数访问,不能被其他访问

private和protected的区别:继承时,protected权限下的数据成员可以被子类的成员函数访问,private完全封闭了类外访问的所有权限

继承

组合和继承的不同点

两者都可以减少重复代码

组合通过在其他类中定义对象来使用类中的方法和属性,不能访问父类的任何接口

继承不仅得到方法和属性,还能获得父类的全部接口并加以调用

多继承

多继承:可以看作是单继承的扩展。多继承是指子类有多个父类,子类与每个父类之间仍可以看作是单继承

优点:子类可以调用多个父类的接口

缺点:容易出现继承向上的二义性

解决二义性:

加上全局符确定调用哪一份拷贝

使用虚继承,使得多重继承类只有祖先类的一份拷贝

虚继承

普通多重继承的构造顺序:构造序列与继承列表中父类的排列顺序一致,而不是与构造函数的初始化列表一致

虚继承构造顺序:存在虚继承时,系统碰到多重继承的时候就会自动先加入一个虚基类的拷贝,即首先调用虚基类的构造函数,然后再调用派生类的构造函数,最后再调用自己的构造函数

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

构造函数:先基后派 析构函数:先派后基

将可能会被继承的父类的析构函数设置为虚函数,可以保证当new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放子类的空间,防止内存泄漏。

虚函数需要占额外的空间,因为有虚函数表和虚函数指针。如果一个类不会被继承,设置为虚函数就会浪费内存。

多态

多态性:不同功能的函数有相同的函数名,可以用一个函数名调用不同内容的函数。可以理解为多态是一个接口,多种方法

分为静态多态动态多态

静态多态

静态多态又叫编译时多态,依靠重载和函数模板来实现

覆盖、重载、重写的区别

覆盖(override):子类函数覆盖父类函数,弗雷函数必须有virtual关键字

重载(overload):可以将语义、功能相似的几个函数共用一个函数名,但是参数不同,可以是类型、数量、顺序不同

重写(overwrite):子类函数屏蔽同名的父类函数

动态多态

动态多态又叫运行时多态,依靠虚函数来实现

虚函数原理:虚函数表、虚函数指针

当一个类声明或继承了虚函数,这个类就会有自己的虚函数表

虚函数按照声明顺序存放在虚函数表中

如果子类覆盖了父类的虚函数,将会覆盖虚函数表中原来父类的位置

纯虚函数

纯虚函数:在父类中为子类保留一个名字,以便子类根据需求对他定义。作为接口存在,不具备函数的功能,一般不能直接调用

指针和引用的区别

指针是指向目标的地址,引用只是对象的别名;

指针可以为空,引用不能为空,必须要初始化;

指针需要解引用才能对目标修改,引用可以直接修改;

指针可以多级嵌套,引用不行;

指针大小是地址的大小,引用是引用对象的大小;

返回动态内存分配的对象或者内存必须用指针,引用可能会引起内存泄漏;

Static关键字

初值:修饰的变量若未赋初值,默认赋值为0;

存储位置:默认存储在全局变量区

全局静态变量:作用域为从定义到文件结束,从定义到文件结束才被释放

局部静态变量:作用域为函数作用域,函数结束后不会被销毁,下次访问时值不变

类的静态成员:多个对象之间数据共享

类的静态函数:不能直接引用非静态成员,可以引用类中说明的静态成员,引用非静态成员时可通过对象来调用

C++的内存分布

栈区:存放局部变量和函数参数,由系统分配和释放,使用完毕后自动释放,自上而下分配;

堆区:用户通过new/malloc手动申请内存,自定义申请内存大小,需要进行手动释放,否则会内存泄漏,自下而上分配;

全局/静态区:存放全局变量,静态变量等,程序结束后由系统释放;

常量区:存放字符串常量等;

代码区:存放程序的二进制代码;

内存分配方式

从静态存储区分配:编译时期就分配完毕,程序结束后释放,如全局变量;

在栈上创建:函数的局部变量等都可以在栈上创建,函数结束时会由系统自动释放。栈的效率高但是内存大小有限,一般是2M;

从堆上分配:由用户进行手动申请和释放,生存周期由程序员决定,使用灵活,但是容易产生内存泄漏等问题

new/delete和malloc/free的区别

两者都可以用来进行动态分配和释放内存

new/delete是C++的关键字,而malloc/free是系统库函数

malloc申请内存时必须指定内存大小,new不需要

malloc/free不能调用构造函数和析构函数

内存泄漏和段错误

内存泄漏:通常是由于未使用delete/free释放申请的内存

内存泄漏判断:

Linux下可以使用内存泄漏检测工具Valgrind

写代码时添加内存申请和释放的统计功能,统计申请释放是否一致来判断

内存泄漏的分类

堆内存泄漏:未及时释放申请的内存

系统资源泄漏:程序使用系统分配的资源没有进行释放,如socket,会导致系统的资源浪费,效率降低

没有将基类的析构函数定义为虚函数:基类的析构函数不是虚函数,调用子类对象后,子类的析构函数不会被调用,因此子类的资源没有释放

段错误

通常发生在访问非法内存地址的时候,如使用野指针、试图修改字符串常量的内容等

野指针

指向了一个已删除对象或指向未申请访问受限区域

拷贝构造函数参数为const&的原因

拷贝构造函数的参数必须是引用,参数传递的方式有两种,值传递和地址传递。值传递就是拷贝原对象的一个副本作为实参,即参数传递过程中也调用了拷贝构造函数函数,若拷贝构造函数不是引用的话,会造成无穷递归的调用拷贝构造函数。而引用是直接操作原对象。

const是作为限制,防止引用对原对象的修改

C++11新特性

auto关键字:可以根据初始值自动推导出类型。不能用于函数传参及数组类型的推导

nullptr关键字:一种特殊类型的字面值,可以被转换成任意其他的指针类型。NULL一般宏定义为0,遇到重载时可能会出现问题

引入智能指针:新增三个智能指针,用来解决内存管理问题

初始化列表:使用初始化列表来对类进行初始化

右值引用:基于右值引用可实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率

增加容器:array、tuple等

Lambda表达式:Lambda表达式定义一个匿名函数,并且可以捕获一定范围内的变量

可变参数模板:对参数进行高度泛化,可以表示任意数目、任意类型参数

(移动语义:对于一个包含指针成员变量的类,编译器默认的拷贝构造函数是浅拷贝,所以需要进行深拷贝来为指针成员分配新内存并进行内容拷贝,避免悬挂指针的问题)

(完美转发:在函数模板中,完全按照模板的参数类型,将参数传递给函数模板中调用的另一个函数)

智能指针

unique_ptr:实现独占式拥有,保证同一时间内只能有一个智能指针指向该对象

shared_ptr:实现共享式拥有,多个指针可以同时指向相同对象,该对象和相关资源会在最后一个指针被销毁时进行释放。内部使用计数机制来表明资源被几个指针共享。

weak_ptr:一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象,weak_ptr只是提供了对管理对象的一个访问手段。设计目的是为了协助shared_ptr工作,只能从weak_ptr或shared_ptr构造。它的构造和析构不会引起计数器的增加或减少。weak_ptr是用来解决shared_ptr互相引用时的死锁问题。如果两个shared_ptr相互引用,那么两个指针的引用计数器用于不可能将为0,资源就得不到释放。

四种cast转换

const_cast:用于进行去除const属性的转换,值针对指针,引用,this指针

static_cast:支持各种隐式转换,如非const转const、void*(万能指针,不可直接使用,一般用来中转)转指针等

dynamic_cast:用来动态类型转换,只能用于含有虚函数的类,用于类层次间的向上或向下转化。只能转指针或引用。使父类可以访问子类中的成员函数

向上转换:子类向父类的转化

向下转换:父类向子类的转换

reinterpret_cast:指针类型之间转换(啥都能转,尽量少用)

迭代器

iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果

迭代器删除元素:

vector、deque:使用erase后,后面每个元素的迭代器都会失效,后面每个元素都会往前移动一个位置,但是erase会返回一个新的有效的迭代器

map、set:使用erase后,当前元素迭代器失效,但其结构是红黑树,删除当前元素不会影响下一个元素迭代器,所以调用erase前,系统会提前保存下一个有效迭代器并返回

List:它使用的是不连续分配的内存,并且它的erase也会返回下一个有效的迭代器,所以以上两种方法都可以

vector和list的区别

vector底层是用数组实现的,list是用双向链表实现的

vector支持随机访问,list不支持

vector是顺序存储,list不是

vector在中间节点进行插入删除时会导致内存拷贝,list不会

vector查找效率高,但是插入删除效率低

list插入删除效率高,但是查找效率低

vector会一次性分配好内存,当空间不足时,会进行二倍扩容,list每次插入新节点都会进行内存申请

volatile关键字

与const相对应,用来修饰变量,用于告诉编译器该变量值是不稳定的,可能被更改

作用:防止编译器优化把变量从内存装入CPU寄存器中。volatile的意思是让编译器每次操作该变量时一定要从内存中取出,而不是使用已存在寄存器中的值

应用场景:中断服务程序中访问到的变量最好带上volatile

​ 并行设备的硬件寄存器的变量最好带上volatile

explict关键字及使用场景

指定构造函数或转换函数为显式,它不能用于隐式转换和复制初始化

避免构造函数的参数自动转换成类对象的标识符

explict只能用在类内部的构造函数声明上

explict用作单个参数的构造函数

explict用来修饰类的构造函数,被修饰的构造函数的类,不能发生隐式转换

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值