文章目录
1.面向对象基础知识
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgeDuQaE-1619187545734)(C:\Users\Chen\AppData\Roaming\Typora\typora-user-images\1619146299805.png)]
1.1 static关键字
static 修饰的数据存放在全局数据区,限定了作用域,生命周期是直到程序结束。
C++中的用法(包含C中的用法):
- static修饰类成员变量 静态成员变量
属于整个类
,在类实例之间共享
,也就是说无论创建多少个类的对象,该成员变量都只有一个副本。 同时由于静态成员变量属于整个类,所以只能在类内申明,在类外初始化
。如果在类内就初始化,那么会导致每一个实例化的对象都拥有一个该成员变量,这是矛盾的。 - static修饰类成员函数 静态成员函数属于整个类,
由于没有this指针,所以只能调用静态成员变量
;
1.2指针与引用的区别?
- 引用是变量的别名,本身不具有单独的内存空间,属于直接访问;指针是指向地址的变量,有单独的内存空间,属于间接访问。
- 引用在创建时就必须初始化,且不能更改绑定;指针可以不初始化,可以更改指向。
总的来说,引用既有指针的效率,同时也更加方便直观。
1.3简述extern关键字?
关键字 extern 对变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。
1.4堆和栈的区别?
- 栈是由编译器自动分配释放的,存放在高地址处,往下进行存储,通常存放局部变量,形参,函数调用等,其操作方式类似于数据结构中的栈,不会产生外部碎片。
- 堆是由程序员手动分配释放的,存放在低地址处,往上进行存储,通常为new malloc 等申请的内存块,其操作方式类似于数据结构中的链表,会产生外部碎片。
- 堆属于动态分配,没有静态分配的堆;栈由静态分配与动态分配两种方式,但是栈的动态分配由编译器控制。
注: 静态内存分配在编译时期完成;动态内存分配在程序运行时期完成。
1.5深拷贝与浅拷贝
- 浅拷贝 只是对指针的拷贝,拷贝后会有两个指针指向同一个内存空间;
- 深拷贝 对指针指向的内容进行拷贝,拷贝后会有两个指针指向不同的内存空间;
浅拷贝可能会出现问题,因为两个指针指向同一块内存区域,一个指针的修改会造成另一个指针错误,如出现两个对象析构,两次delete内存的情况。
1.6内联函数(inline)是什么?
- 我们知道如果频繁的调用一个函数,那么函数多次压栈会消耗过多的栈空间。 当函数被申明为内联函数之后,编译器编译时会把该函数的代码副本放置在每个调用该函数的地方 ,而不是按照普通的函数调用机制进行压栈调用;少了多次的压栈操作,栈空间的消耗就减小了。 避免了函数跳转和保护现场的开销 。
另外注意:
-
内联函数一般是不超过10行的小函数,内联函数中不允许使用循环和开关语句,因为如果内联函数过长或者过于复杂,那么内联展开之后同样会消耗许多栈空间,便得不偿失了。所以滥用内联函数反而可能会导致程序变慢。
-
类成员函数默认加上inline,但具体是否进行内联由编译器决定,类函数声明前加上inline是无效写法,只有在类函数定义前加上inline才有效。
1.7 struct 与 class 的区别?
struct和class的区别主要在于 默认访问级别 和 默认继承级别。
- 默认访问级别:struct中的成员默认是public,class中的成员默认是private。
- 默认继承级别:struct默认public继承,class默认private继承。
除了这两点外,struct 和 class 完全相同。
引申:C++和C的struct区别
C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成
员函数的定义,(C++中的struct能继承,能实现多态)
1.8 内存对齐是什么?(字节对齐)
现代计算机中的内存空间都是按照字节划分的,CPU实际读取内存时,是按照k字节进行读取而不是一个字节一个字节读取,这就是内存对齐;有了内存对齐之后,CPU可以一次性读取k字节的数据,变得更加高效。
注意:
- k通常为最大成员数据类型的大小,结构体的大小也应该为k的整数倍。
- 在union,class,struct中均有内存对齐;但是也可以通过
#pragma push(k)
,#pragma pop()
来设置内存对齐的方式。
类所占空间 = 非静态成员变量 + 指向虚函数的指针(如果有虚函数) ;
-
要考虑内存对齐问题,指针:32bit 目标平台寻址空间是4Byte(32bit),所以指针是 4Byte的;64bit 的指针是 8Byte
-
不管类里面有多少个虚函数,类内部只要保存虚表的起始地址即可,虚函数地址都可以通过偏移等算法获得。(32位:4Byte、64位:8Byte)
-
静态成员变量是在编译阶段就在静态区分配好内存的,所以静态成员变量的内存大小不计入类空间
-
C++编译系统中,数据和函数是分开存放的(函数放在代码区;数据主要放在栈区和堆区,静态/全局区以及文字常量区也有),实例化不同对象时,只给数据分配空间,各个对象调用函数时都都跳转到(内联函数例外)找到函数在代码区的入口执行,可以节省拷贝多份代码的空间
-
类的静态成员变量编译时被分配到静态/全局区,因此静态成员变量是属于类的,所有对象共用一份,不计入类的内存空间
1.9 C++ 函数调用的压栈过程
当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、mian函数中的变量、进行依次压栈;
当main函数开始调用func()函数时,编译器此时会将main函数的运行状态进行压栈,再将func()函数的
返回地址、func()函数的参数从右到左、func()定义变量依次压栈;
从代码的输出结果可以看出,函数f(var1)、f(var2)依次入栈,而后先执行f(var2),再执行f(var1),最后
打印整个字符串,将栈中的变量依次弹出,最后主函数返回。
1.9 陈述一下面向对象吧
面向对象:将需求或者业务进行抽象化
三大特性:继承、封装和多态
1.9.1 继承
继承就是子类继承父类,拥有父类的一些属性和方法。在无需重新编写原来的类的情况下对这些功能进行扩展 ,使得程序具有很灵活的扩展性。
1.9.2 封装
封装就是把客观事物封装成抽象的类,对数据和方法设置访问权限,避免外界干扰和不确定性访问。在日常使用中的链接库(.so/dll文件)、第三方api,都是封装起来的。
1.9.3 多态
多态是一个接口,多种方法。
C++中虚函数的唯一用处就是构成多态。
虚函数:虚函数是带有 virtual 关键字的 类成员函数。实现多态需要进行 虚函数重写,也就是派生类有一个和基类 函数名,参数,返回值完全相同 的成员函数,也就称为虚函数重写(覆盖)。这就是虚函数实现多态的方式。
虚函数表:有虚函数的类在编译时期都会生成一个 虚函数表,虚函数表实质上是一个 指针数组,存放 指向虚函数的指针,通过该指针我们可以调用虚函数;另外虚函数表是类对象之间共享的,在各个类对象之间不会存储整个虚函数表,只存放一个指向该虚函数表的指针,该指针通常是放在类内存的最前面。这就是虚函数表。该虚函数表存放在全局静态数据区的DATA段。
在生成派生类过程中,对虚函数表的操作有三个步骤:
- 将基类中的虚函数表指针拷贝到派生类中;
- 派生类对基类虚函数进行覆盖(重写);
- 派生类将自己新增的虚函数依次添加在虚函数表后。
延伸1:虚函数表中的虚函数指针是如何实现偏移的?
通常编译器会将虚函数表指针放在对象内存的最前面,我们可以通过取该对象的地址得到该虚函数表指针,进而得到虚函数表,也就是一个指针数组(指针属于函数指针,指向虚函数),遍历这个数组,就可以得到我们想要的虚函数。
延伸2:每个实例化对象的虚函数表是否相同?
相同,因为虚函数表属于类对象之间共享的,只会有一个,每一个实例化对象都只会存放一个虚函数表指针,指向共享的虚函数表。
1.10 C++中哪些函数不能是虚函数?
- 构造函数不能是虚函数
如果将构造函数设置为虚函数,那么派生类将无法创建,因为无法调用基类的构造函数。
- inline内联函数不能是虚函数
因为内联函数会在编译时内联展开,而虚函数需要在运行时动态联编。
- 友元函数不能是虚函数
因为友元函数不属于类成员函数,虚函数必须是类成员函数。
- 静态成员函数不能是虚函数
因为静态成员函数是整个类共享的,虚函数无法进行覆盖。
1.12 重载,隐藏,覆盖的区别?
- 重载 在
同一个类
中,函数名相同
,参数类型(或个数) 不同
则为函数重载;如果只是返回值不同
则不能称为重载。重载也称为编译时多态。 - 隐藏 若派生类的函数名与基类的
函数名相同
,派生类的函数则会吧基类的函数隐藏起来。 - 覆盖 派生类中的函数与基类中的虚函数完全相同(函数名,参数,返回值均相同),那么称为覆盖。覆盖也称为运行时多态。
1.12.1 继承时的名字遮蔽
基类成员和派生类成员的名字一样时会造成遮蔽(包含参数不同),这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
1.13 构造函数与析构函数能否为虚函数?
- 构造函数不能为虚函数
若构造函数为虚函数,那么派生类生成的过程中将会无法调用基类的构造函数。
- 析构函数可以为虚函数
如果不将析构函数设置为虚函数,只能调用基类的析构函数,将无法调用派生类的析构函数从而造成内存泄漏。
1.14 析构函数可以抛出异常吗?
析构函数不能抛出异常,原因如下:
- 如果析构函数抛出异常,那么异常点之后的程序并不会执行,那么就会造成内存泄漏的问题。
- 严格来说,析构函数也是处理异常的一部分;如果之前发生异常,调用析构函数来释放内存,若是析构函数也抛出异常,将会让程序崩溃。
1.15 空类中自带哪些函数?
六个函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符
- 取址运算符
- 取址运算符const。
1.16 简述智能指针?
智能指针的本质也是指针,只是它可以帮助我们自动释放空间,避免了内存泄漏和野指针的情况。
目前常用的智能指针有三种(auto_ptr已经淘汰):
- unique_ptr 一个对象只能由一个unique_ptr引用,当指针不再引用该对象时,该对象自动析构并释放内存。
- shared_ptr 一个对象可以由多个shared_ptr引用,对象的被引数量可以用引用计数(
use_count
)来表示,当对象的引用计数为0时,将该对象自动析构并释放内存。 - weak_ptr weak_ptr是一种弱引用,不会增加对象的引用计数,是用来打破shared_ptr相互引用时的死锁问题。 例如现在有两个类,一个类A,一个类B,类A里面有一个shared_ptr指向B,B里面有一个shared_ptr指向A,这样的话就算程序结束了这两个类也不会进行析构,因为他们的引用计数都还是1,这样会造成内存泄漏。
- 延伸:智能指针是线程安全的吗?
首先智能指针的引用计数使用的是atomic原子操作,所以智能指针本身是线程安全的;但是对于智能指针托管的对象,在多线程环境下则需要加锁操作才行。
1.16 C++ 的emplace函数
性emplace向容器中添加新元素,在容器管理的内存空间中构造新元素,与insert相比,省去了构造临时对象,属于零拷贝,减少了内存开销。