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用来修饰类的构造函数,被修饰的构造函数的类,不能发生隐式转换