C/C++面经
C/C++
共同:
轻量、可移植多个平台、能直接访问底层硬件
区别:
C复用性、扩展性、维护性差
适用场景:
程序过程
编译
预处理:都是文本操作,条件编译,头文件展开,宏定义替换 .c->.i
编译:词法分析,语法分析,代码优化,存储分配,翻译成汇编语言 .i->.s
汇编:汇编代码转为二进制机器代码 .s->.o
链接:根据源文件创建符号表,里面记录了变量和函数的地址和大小,每个源文件有一个地址,将不同目标文件进行组装,组装中进行重定位,重定位表会把当前文件里找不到符号的定义记录下来,后续进行填充,这里的符号表和重定位表都是编译时生成的。总之就是把代码中调用别的文件(extern)或库连接起来,这些目标文件成为操作系统能装入执行的统一整体,链接器会在最后生成的可执行文件里创建一个全局符号表,所有的符号都有了自己的地址。
动态链接和静态链接?
在项目中引用了库函数,编译中的链接时会把引用的东西链接到可执行文件里,这种方式得到的可执行文件很大,并且有重复加载的可能消耗大量内存,但优点是不依赖当前环境,因为文件里包含了运行需要的内容。
动态编译时,在编译链接时不把依赖组装进可执行文件,当可执行文件真正运行时,再把依赖加载进内存,此时才完成链接,而且这时候加载进来的库能被多个可执行文件共享。
编译过程中局部变量怎么实现?
执行
装入内存
main函数
关键字
new & delete
new和malloc:
new是操作符,malloc是库函数
new会先malloc分配未初始化的内存空间,如果这一步出错,返回std::bad_alloc异常;之后根据对象的构造函数在这个内存空间上进行初始化,返回空间的首地址,如果构造出错,调用delete释放内存
new得到的是初始化完成后的内存空间,new一个对象;malloc得到未初始化的空间,malloc一个空间,delete和free同理
delete和free:
首先调用对象的析构函数;再free回收内存空间
为什么有malloc free还要new delete:
对于自定义类型(非内部数据类型),无法用malloc构造free析构
为什么不淘汰malloc free:
C还在用
extern
当你extern C时,就相当于是C语言写的代码能在C++里面用了,因为C++多态,函数名会被编译器根据参数重命名,但C不存在多态,编译器只会给函数多加一个下划线。
引入外部变量的方式:
extern 、 include
static
作用:改变存储方式和作用域
类中的成员变量:这个成员变量存储在全局数据区,这个类的所有对象共享这个成员变量,且大家都可以操作这个成员变量,所有对象共享一个拷贝;如果没有static修饰,那么这个成员变量这个类的每个对象都有一份拷贝
同全局变量相比,静态成员变量不存在和其他全局名字冲突,且存在public\private\protected的访问控制
类中的成员函数:只为类服务,而不为某个对象服务,因此不接受指向对象本身的this指针,这个成员函数只能调用static成员变量和static成员函数,不能被virtual修饰,因为vptr是通过this指针访问的
局部变量:被static修饰,意味着这个局部变量存储在进程的全局数据区,函数返回了,它的值还保留,但作用域还是局部的
全局变量:static与extern相反,原本不添加static时,在同一工程想要用这个变量,用extern就行,static后只能在本文件中使用。
全局变量与static 全局变量存储方式相同,不同在于作用域,static限制使用范围
函数:static后作用域为本文件,只能在本文件使用这个函数
static函数在内存中只有一份,普通函数每个调用中维持一个拷贝
const
变量:const可以在类型声明前后,不能改变常量的值,声明时赋值
函数参数:防止函数内意外修改该参数,保护
函数返回值:返回一个右值,不可被修改
函数体:void f(int a) const {} ,不能修改类中的成员变量,且只能调用const函数
常量指针
const int *p = &a;
int const *p = &a;
一个指针,指向一个常量,也就是说这个指针指向的东西是个常量,这个常量不能通过指针改,但我可以指别的,我也可以这个常量自己改自己,但就是我指谁谁就不能通过我来改
可以理解为*p是const的,*p的值不能改,改了就报错
不能通过指针修改这个对象的值,但指针可以指向其他对象
也就是p = &b没有问题,a = 1231没有问题;但*p = 1312会报错
指针常量
int * const p = &a;
一个指针,这个指针的值是个常量,还是指针,但我指向了这个地方那就永远只能指向这个地方,我不能指向别人,但我指向你,我可以通过指针来修改你的值
friend
实际使用中,如果设计得当,应该尽量不使用friend,因为friend破坏了封装性
比如只有在写单测需要访问私有成员时用friend,或者print的时候来访问某些成员
友元函数:
使用场景:
有时候需要定义一些函数,这些函数不属于这个类,但需要频繁访问类中的成员
在类中声明函数为自己的友元,这个函数即可调用类中的private、protected成员,这个函数中没有this指针,因为友元函数不属于任一个类
在类外声明函数,调用原理与普通函数一致
友元类:
友元类的所有成员函数都是友元函数,可以访问另一个类的private、protected成员
结构体
结构体大小:
空结构体为1,编译器默认分配一个char
其余大小计算按偏移量内存对齐相加
内存对齐
若未对齐,cpu要进行两次访问内存,花费额外时钟处理对齐
枚举
枚举一些常量符号,不赋值默认从0开始
枚举只存在赋值运算,不存在算数运算
编译器决定枚举大小,一般为4字节
面向对象
封装
把事物抽象成类,类里包含事物的属性和方法,并且对内部数据提供不同级别的保护
继承
让某个类获得其他类的属性或方法
多态
静态多态(编译多态)
函数重载
同名函数根据参数列表不同,会被重载成不同函数
为什么要有函数重载:
如果没有函数重载,那就要对功能相似但参数不同的函数一个个取名,很不友好
函数重载的实质是什么
编译器的name mangling机制
编译时重命名,函数重载只是语法层面的,实际上还是不同的函数
类成员函数,编译器会重命名为C-style函数,会extral一个this指针作为函数参数,这也就解释了为什么类成员函数在参数列表相同的情况下,只是返回值不同,但仍然可以重载
C不支持函数重载,因为C编译器在name mangling时不会考虑函数参数,只用函数名
模板
动态多态(运行多态)
虚函数
虚函数的实质是什么
一个接口,多种方法
如果你继承了虚函数,你是基类的指针指向继承类的对象,你的函数调用只和你的对象类型有关,在对象实例的最前面,有一个虚函数表,这里面实际上是指向了各个虚函数的指针的数组的指针,只要子类重写了虚函数,那虚函数表里子类的方法就会覆盖掉父类的方法
虚函数在运行时动态绑定,基类指针指向子类的对象时,调用的是子类对象的成员函数。
为什么父类析构函数是虚函数
如果不是虚函数,你用父类的指针操作一个继承类的成员,只是释放了父类的资源,而没有调用继承类的析构函数,这时候就会内存泄漏
而如果你父类的析构是虚函数,那在虚函数表里,子类会覆盖父类的析构函数,当你用父类的指针指向子类的对象时,你调用了析构函数,先释放子类的资源,再调用父类的析构函数,防止没析构子类,就避免了内存泄漏
纯虚函数
父类里的函数=0,此时父类变成抽象类,无法实例化,纯虚函数目的是提供一个统一的接口,
子类继承父类,必须要实现这个函数,如果不实现,那子类也是抽象类
构造与析构
构造时,如果有父类,先执行父类的构造函数,类中非静态的成员变量,按声明顺序构造,再执行子类的构造函数,析构相反
调用顺序
构造:抽象基类的构造、虚基类的构造、父类的构造、本类中的对象的构造、本类的构造
析构相反
异常
构造与析构内可以抛出异常吗?
构造内异常:
异常的一个存在意义就是解决构造函数失败怎么办,因此只要保证raii(C++构造的对象最终会被销毁的原则:使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源),在构造内抛异常没有问题,保证抛异常前,把资源释放掉就行
析构内异常:
可能会导致资源未被释放,程序过早结束
拷贝构造
一个对象以值传递的方式传入函数参数、一个对象以函数返回值的形式返回(不在return时,而在用一个新的对象接收这个返回值时)、一个对象由另一个对象进行初始化时会调用拷贝构造函数
默认拷贝构造函数是浅拷贝
类的大小
空类编译器优化为大小为1,因为空类实际上也能实例化
普通成员变量遵循内存对齐
成员函数、构造函数、析构函数不占内存
虚函数存在函数vptr,32位下4字节,64位下8字节,并且指针占用实例内存最头部的位置,其余变量根据vptr大小内存对齐
静态成员在全局静态数据区,不占类内存
有继承关系的,先分配父类内存,内存对齐值也按父类内存对齐值分配
存在虚继承关系的子类中会有一个指向虚基类表的指针,32位下4字节,64位下8字节
指针
指针是一个变量,指针的值是指向变量的地址,指针也有自己的地址
野指针
野指针就是你不知道指针指向了哪,但他却不为空,例如在栈区声明了一个int* p,或者是你指针指向了一个对象,但对象被delete了,你没有把指针置空,那这个指针变为野指针,使用野指针是很危险的,因为你不知道自己在操作什么
空指针
int *p=NULL;
指向0号内存地址,被系统占用,这个地址不能被调用,防止误调用
空指针会分配内存吗
函数指针
函数指针指向函数被加载到的内存的首地址,可用于调用函数
指针传递参数
指针传递是另外一种形式的“址传递”,他和“引用传递”的区别在于:如果认为引用传递稍微“智能”一些,即只需在函数定义的形参上加一个&表示要取的是地址就行,指针传递则更加“低级粗暴”,直接在函数定义时指明形参要的就是地址值,要求把地址传递进来,从而实现直接对地址进行操作
引用
变量的别名,引用拷贝实际上是操作同一个地址
用指针实现引用
深拷贝与浅拷贝
浅拷贝只是用了别人的引用,实际上大家都是同一个地址,如果被浅拷贝的对象变了,那拷贝方也要改变
深拷贝申请了新的内存,将对象复制过来,用指针指向这个地址,或者两个指针指向同一个地址,但把原指针置为NULL
C++11
nullptr
nullptr没有int类型,但C++里NULL为int类型0,而且还有函数重载,所以可以用nullptr避免函数形参错误,调用错函数
auto decltype
auto让编译器替我们推导数据类型,但auto不能定义一个没有初始化值的变量,另外,我们有时候不想用这个表达式的值作为初始化值
decltype是完全只推导类型,根据decltype()里的参数推导数据类型,只分析表达式的类型,而不计算表达式
智能指针
智能指针的实现:
智能指针是一个类,原理是离开了作用域会自动析构,有的对象我们要动态分配内存,这个类里就存储了指向这个对象的指针,
sharedptr
共享指针管理对象时,除了对象本身,还有一个引用计数管理区域,这里面的引用计数值通过原子操作保证被互斥地访问
当我们再构造一个对象时,这时候两个对象指向相同的资源,也就是类,两个对象共享一个引用计数管理区域,引用计数+1
uniqueptr
uniqueptr怎么实现独占性?
循环引用
两个共享指针互相互相引用,当析构时,引用计数永远为1,变造成了循环引用
解决办法:将其中一个变为weak_ptr,主要作用是观测,weak_ptr的构造不会引起引用计数增加