C++ 问题整理

2023-05-16

说一下C++和C的区别

● 设计思想上:C++ 是面向对象的语言,而 C 是面向过程的结构化编程语言
● C++具有封装、继承和多态三种特性
● C++支持范式编程,比如模板类、函数模板等
● C++相比C,增加多许多类型安全的功能,比如强制类型转换、


C++面向对象的特征

● 封装:将客观事物抽象成类,拥有自己的权限
● 继承:可以使用现有类的功能,并且在此基础上修改,不用重新编写原来的类,目的是代码的复用和实现多态
● 多态:同一接口,可以完成不同的功能。多态可以分为静多态和动多态。
①前者主要是指函数重载(包括运算符重载、函数模版),在编译时根据实参类型不同,确定调用哪个函数。
②后者则和继承、虚函数等概念有关,在运行时根据指针指向的对象类型不同,确定调用哪个函数。


结构体和类

C语言中的 struct 只能包含变量,而 C++ 中的 class 除了可以包含变量,还可以包含函数。display() 是用来处理成员变量的函数,在C语言中,我们将它放在了 struct Student 外面,它和成员变量是分离的;而在 C++ 中,我们将它放在了 class Student 内部,使它和成员变量聚集在一起,看起来更像一个整体。

class 继承默认是 private 继承,而 struct 继承默认是 public 继承
class 可以使用模板,而 struct 不能


内联函数

函数体替换函数调用,声明没用,要定义的时候内联,因此直接在声明处直接定义,作用一是消除函数调用时的开销,二是取代带参数的宏(坑 x + 1)。不过我更倾向于后者,取代带参数的宏更能凸显内联函数存在的意义。


类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。

两种创建对象的方式:一种是在栈上创建,形式和定义普通变量类似;另外一种是在堆上使用 new 关键字创建,必须要用一个指针指向它,读者要记得 delete 掉不再使用的对象。
通过对象名字访问成员使用点号.,通过对象指针访问成员使用箭头->,这和结构体非常类似。

编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码

编译器会根据函数所在的命名空间、它所属的类、以及它的参数列表等信息进行重新命名,形成一个新的函数名。成员函数最终被编译成与对象无关的全局函数。C++规定,编译成员函数时要额外添加一个参数,把当前对象的指针传递进去,通过指针来访问成员变量。这样通过传递对象指针就完成了成员函数和成员变量的关联。这与我们从表明上看到的刚好相反,通过对象调用成员函数时,不是通过对象找函数,而是通过函数找对象。

深拷贝和浅拷贝

● 浅拷贝:将 a 所在内存中的数据按照二进制位(Bit)复制到 b 所在的内存
● 深拷贝:复制的时候,开辟了新的内存区域存放数据
● 当类中有动态分配的内存、指向其他数据的指针等,浅拷贝就不能拷贝这些资源了,我们必须显式地定义拷贝构造函数,以完整地拷贝对象的所有数据。


拷贝构造函数和赋值运算符的区别

● 拷贝构造函数生成新的类对象
● 赋值运算符用已存在的对象来创建另一个对象


什么时候调用拷贝构造函数

① 初始化 string s1(s2);
② 赋值初始化 string s1 = s2;
③ 函数参数值传递 func(string s1);
④ 函数返回 return s1;

赋值运算符调用时机:s1 = s2; 必须是初始化后的!
当以拷贝的方式初始化一个对象时,会调用拷贝构造函数;当给一个对象赋值时,会调用重载过的赋值运算符。


什么时候不能使用默认析构,拷贝构造,赋值运算符

(深拷贝)如果一个类需要定义析构函数,那么几乎可以肯定这个类也需要一个拷贝构造函数和一个赋值运算符。
(唯一id)如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个赋值运算符;反之亦然


成员函数和非成员函数

● 成员函数在调用时会隐式地增加 this 指针,指向调用它的对象,从而使用该对象的成员。非成员函数没有 this 指针,编译器不知道使用哪个对象的成员。
● 非成员函数就是普通函数,不在类中定义的函数,非成员函数比较典型的是友元函数。


友元函数

主要功能是让普通函数或者其他类成员函数能够访问对象的所有成员,必须借助对象,因为友元函数没有 this 指针
● 声明在类的内部,函数名前加关键字 friend,定义在类的外部,函数名前不用加关键字,和普通和函数一样
● 作为非成员函数的友元函数,不能直接访问类的成员,必须要借助对象,可以访问对象的所有成员(包括私有成员)
● 友元函数可以在类外直接使用,在参数中需要指出要访问的对象
● 友元关系是单向的,而且不能继承和传递
● 可以声明一个友元类,类中的所有函数都是友元函数


初始化列表和赋值

赋值是删除原值,赋予新值。初始化列表开辟空间和初始化是同时完成的,直接给予一个值。因此有三种情况只能只用初始化列表
● 含有 const(常量)和 reference(引用)成员变量时
● 当基类没有构造函数时,要在派生类的构造函数调用基类的构造函数,实现派生类对基类成员的初始化
● 如果成员函数是没有默认构造函数的类,也只能使用初始化列表


类成员变量的初始化顺序

基类静态和全局变量 → 派生类静态和全局变量 → 基类成员变量 → 派生类成员变量


虚函数

当没有虚函数的时候,基类指针指向派生类对象时,使用的是派生类的的成员变量,但用的还是基类的成员函数。
当有虚函数的时候,基类指针指向派生类对象时,使用的是派生类的的成员变量和成员函数。

虚函数构成多态的三个条件
必须存在继承关系;
继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
存在基类的指针,通过该指针调用虚函数。


析构函数一般是不是虚函数?

不是虚函数的话,基类指针定义是哪个类,就用哪个类的函数,不管是什么对象,指向派生类对象可以用它的成员变量
是虚函数的话,基类指针指向哪个对象,就用对象类的函数
调用派生类析构函数时,会自动调用基类的析构函数

不是的后果:
基类指针指向派生类对象时,调用的是基类的析构函数,派生类对象的内存得不到释放,造成内存泄露
https://blog.csdn.net/heibao111728/article/details/80814313


为什么构造函数不可以是虚函数?

派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义。
对象没有创建,就没有虚表,不知道调用哪个虚函数
https://www.cnblogs.com/gcczhongduan/p/5059566.html


纯虚函数

包含纯虚函数的类称为抽象类,无法实例
继承纯虚函数的类必须实现全部纯虚函数,才可以被实例
抽象基类除了约束派生类的功能,还可以实现多态。


状态同步和帧同步的区别

帧同步:A告诉服务器放了技能,服务器告诉其他人A放了技能,客户端自己运算技能飞行轨迹和掉血(王者荣耀)
状态同步:A告诉服务器放了技能,服务器运算技能飞行轨迹和掉血,再告诉A和其他人应该怎么显示,因为客户端不能承载整张地图的信息(魔兽世界)
https://www.cnblogs.com/sanyejun/p/9069165.html


4种强制类型转换(和C的区别)

https://www.cnblogs.com/songhe364826110/p/11521589.html


new和malloc的区别

https://blog.csdn.net/nyist_zxp/article/details/80810742
https://blog.csdn.net/qq_40315987/article/details/79686283


面向过程和面向对象

https://www.cnblogs.com/qianxiaoruofeng/p/11561188.html


红黑树

二叉搜索树的时间复杂度是O(logN~N),红黑树的时间复杂度是O(logN),
红黑树解决了二叉搜索树的自平衡问题
插入红色节点
父节点是黑,直接插
父节点是黑,叔节点是红,变色
父节点是黑,叔节点是黑,旋转+变色
https://www.cnblogs.com/LiaHon/p/11203229.html


B+树(加强版多路平衡搜索树)

在这里插入图片描述
● B+树的特点:
非叶子节点存放键值信息(键值 id 和键值指针(指向磁盘块))
叶子节点存放数据
叶子节点之间有链指针

● B+树的优点
哈希:查找区间太慢
搜索树:不平衡的时候查找速度慢
平衡搜索二叉树:数据量大时,树会变高,导致 IO 操作变多
B树:每个节点有多个子节点,子节点存放键值,键值指针和数据,可以控制树高,减少IO,但查找需要索引,索引又和数据存放在一起,就很大,需要从硬盘读进内存,就慢炸了
https://www.bilibili.com/video/BV1Ka4y1t7ev?from=search&seid=15473693171804009833


static

static 的意思是静态,根据修饰的类型不同,作用也不一样:
● 函数内静态局部变量:①静态变量只会在第一次调用的时候被初始化,和全局变量一样存放在静态区,数据不会随函数结束消失,作用域为函数内。
● 函数外静态全局变量和函数:①限制作用域为当前文件。普通函数默认是被extern修饰的。
● 静态数据成员:①属于类,被所有对象共享和修改。②可以在类外不借助对象,直接访问,而且要在类外初始化,默认是0。
● 静态成员函数:①不能访问或者修改非静态数据成员,但可以修改静态数据成员。②没有 this 指针,可以在类外不借助对象,直接访问,非静态类成员函数含有指向对象的this指针,只有对象才能调用。
https://blog.csdn.net/yesyes120/article/details/79519753
https://blog.csdn.net/majianfei1023/article/details/45290467
● 例子:在力扣里,一般是在类内写程序,如果想要使用 sort 进行排序并且自定义 cmp 函数,因为 sort 本身是全局函数,想要传 cmp 函数参数给 sort ,在 cmp 函数前必须加 static 变成静态成员函数,sort 才能访问到。

const

● 变量——定义为常量,不分配空间,存储在常量区
● 函数的参数——在函数体内不可被修改,本质上仍为变量
● 函数的返回值——①指针,指针内容不能被修改 ②引用,函数体不能作为左值 ③变量,没有意义,返回后就挂掉
● 类内——①数据成员,在对象内是常量 ②成员函数 函数不能修改对象的成员(函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改。函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值)
● 类外——①对象,对象不能被更新
● 指针——4种情况,注释掉的代码是出错的语句,代表值不可被修改
https://blog.csdn.net/qq_33438361/article/details/105999447

const 和 constexpr 的不同

在定义类数据成员时,static const整型可以作为常量在类内部定义,但浮点型不行,constexpr是两者都可以
作为函数返回类型时,constexpr可以返回一个常量表达式,而 const 不行


程序内存分布

● 栈区(stack):是一片连续的内存区域,操作于类似栈,存放程序运行中被动态分配的内存段,由编译器自动分配与释放,存放函数局部变量、函数参数、返回数据、返回地址等。
● 堆区(heap):是一片不连续的内存区域,分配类似于链表,存放临时创建的局部变量,一般由程序员自动分配,如果程序员没有释放,程序结束时可能由操作系统回收,存放 new 对象。
● 全局区(静态区static):存放全局数据和静态数据,程序结束后由系统释放,存放全局变量、静态数据、常量。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
● 常量区(文字常量区):程序结束后由系统释放,存放常量字符串、虚函数表。
● 代码区:存放程序执行代码。存放函数体(类成员函数、虚函数和全局区)的二进制代码。


迭代器

作用:不需要暴露某个容器的内部表现形式情况下,遍历容器中存储的元素
实现:通过重载了指针的一些操作符,->,*,++ --等封装了指针,并模拟了指针的一些功能
和指针的区别:指针只能对序列式容器中的元素,进行依次访问
而迭代器可以对STL各种容器中的元素,进行依次访问


迭代器失效原因和情况

对容器的操作影响了元素的存放位置
vector:① 尾部插入元素,end迭代器失效
② 尾部插入元素,capcity发生变化,begin和end迭代器失效
③ 删除元素,指向删除元素的迭代器失效,指向删除元素后方元素的迭代器也失效
list/set/multiset/map/multimap:删除元素,指向该元素的迭代器失效。


什么是内存泄露?

● 因为内存的堆区是由程序员分配的,可能会由于程序员的错误,程序没有释放已经不再使用的内存,失去了对这段内存的使用权,造成了内存的浪费。
● 内存泄露会导致系统内存资源耗尽的严重后果。


C++11 decltype补位auto情况

auto 只能用于类的静态成员,不能用于类的普通成员,如果我们想推导非静态成员的类型,这个时候就必须使用 decltype 了
http://c.biancheng.net/view/7151.html

左值和右值

● 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值
● 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
● 右值引用:实现了移动语义和完美转发

对象 = 左值对象(拷贝构造)
对象 = 右值对象(优先移动构造,再拷贝构造)
对象 = move(左值对象)(移动构造)
移动构造是浅拷贝,使用时要注意指针的重置,避免“对同一块空间释放多次”

● 移动语义:通过移动构造将右值对象(临时对象)变成了左值,优点是没有内存拷贝。编译器会优先选择移动构造函数,再选择拷贝构造
● 完美转发:函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。

二叉树修改值又要保持连通

通过 return TreeNode* 在子节点修改,父节点接收 root->left = dfs(root->left);
直接在父节点中修改 root->left = nullptr;

map原理

底层存储结构是红黑树,根据键值排序。
插入:通过键值比较函数找到自己的位置,存储数据,不允许修改键值
取值:通过键值比较函数找到自己的位置


unordered_map原理

底层存储结构是哈希表,无序。
首先分配一大片内存,形成许多个存储区域,通过哈希函数将键值映射到一个存储区域(不同的键值可能会映射到同一个区域)
插入:通过哈希函数将键值映射到一个存储区域,存储数据
取值:通过哈希函数将键值映射到一个存储区域,如果键值相等取出数据


避免哈希冲突

对于可以避免的冲突:选取合适的哈希函数(直接定址法、数字分析法、平方取中法、折叠法、除留余数法和随机数法)
对于无法避免的冲突:
● 开放定址法:H(key) = (H(key) + d) % m,选取合适的方法(线性探测法:d=1,2,3,…,m-1
、二次探测法:d=12,-12,22,-22,32,…、伪随机数探测法:d=伪随机数),获取地址增量 d,直到冲突不再发生
● 再哈希法:使用另一个哈希函数计算,直到冲突不再发生
● 链地址法:将键值存储在一个线性链表中
在这里插入图片描述
● 建立一个公共溢出区:建立两张表,一张基本表,存储没有发生冲突的数据,一张溢出表,存储发生冲突的数据
http://c.biancheng.net/view/3437.html


智能指针

为什么需要:智能指针的出现主要是为了解决内存泄露的问题
怎么实现:C++ 智能指针底层是采用引用计数的方式实现的。智能指针在申请堆内存时,会为他配备一个整形值,初始值为 1。每当有新对象使用此堆内存时,该整形值 +1,每当使用此堆内存的对象被释放时,该整形值减 1。当堆内存对应的整形值为 0 时,该堆空间就会被释放掉。

● auto_ptr:在 C++11 已被废弃。
● shared_ptr(shared_pointer):①可以与其他指针共享堆内存,有拷贝构造函数和移动构造函数 ②有堆内存引用计数器,只有计数器 0 的时候,才会释放所指向的内存 ③在初始化智能指针时,可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则。
● unique_ptr:①不与其他指针共享堆内存,只有移动构造函数 ②在初始化智能指针时,可以自定义所指堆内存的释放规则,制定释放规则只能采用函数对象的方式。
● weak_ptr:①通常不单独使用,只能和 shared_ptr 类型指针搭配使用。该智能指针不会导致堆内存空间的引用计数增加或减少。


lambda

[]:外部变量,&引用传递,=值传递
():参数
-> T:返回值类型

sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
auto cmp = [=](int x, int y){
	return x < y;
}

多线程冲啊

https://www.cnblogs.com/z1014601153/p/11350631.html

(1)对于数据成员和普通数据可以使用static和const修饰 (2)对于成员函数不可同时使用static和const修饰,因为静态成员函数不属于类,没有this指针

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++ 问题整理 的相关文章

随机推荐