C++ 基础知识汇总 持续更新
摘录一些C++面试常考问题,写一些自己的理解,花了挺长时间的,作图是真的累,欢迎来摘果子。
static关键字
用于声明静态对象;
静态函数只在本文件可见。(默认是extern的)
全局静态对象:全局静态对象,存储在全局/静态区,作用域整个程序,在程序结束才销毁;
局部静态对象:在函数内部加上static声明的变量,在首次调用时初始化,然后一直驻留在内存,作用域是该函数,可用于函数调用计数(primary有例子),程序结束释放;
静态数据成员:归属于类,类对象共享,类外初始化,类对象可访问;
静态函数成员:归属于类,只能访问静态数据成员。
const 关键字
核心功能:限定只读
const T var; 声明常量,存储在常量区;
const T* p : 不可通过p指针修改对象值;T * const p : 常量指针,指针不可被赋值/地址不可改变。
const T function(const T, const T*, T* const, const T&) const &/&& {…;}
限定返回值为常量、常量形参、指针常量、常量指针、常引用、防止对象属性被改变(对象只读)STL源码大量使用
如何突破const的限制?mutable 在const函数中修改成员变量
View Code1 #include <iostream> 2 using namespace std; 3 4 class Test 5 { 6 public: 7 mutable int a; //突破限制 8 int change(int b) const; 9 }; 10 11 int Test::change(int b) const 12 { 13 a = b; 14 return a; 15 } 16 17 int main() 18 { 19 Test t; 20 t.a = 10; 21 cout << t.change(20) << endl; 22 return 0; 23 }
C/C++区别
C:面向过程语言、编译型(也可以实现对象化,但没有C++那样顺畅强大)。
C++:面向对象语言、编译型、拥有封装继承多态三大特性、支持泛型编程、模板。
引用指针区别
指针是对象的地址,而引用是对象的别名,均可改变对象值;
指针可以被重新赋值,引用初始化后不可以改变,就像私家车与共享汽车。
指针可以不知指向对象,引用必须初始化;
在实现层面,指针和引用完全相同,都占空间,汇编代码看一下就知道;
传指针和传引用的区别在于,会否发生拷贝,指针会发生拷贝,会降低效率,使用引用直接访问该对象,提高效率。
内存泄漏以智能指针,智能指针的内存泄漏,怎么解决?
动态申请的内存,未进行释放,比如new的空间,没有适时delete掉。导致内存泄漏,可用智能指针解决。
Auto_ptr可以实现部分shared_ptr的功能,但已经被弃用。
Shared_ptr:多指针共享对象,存在引用计数,赋值给其他指针,计数增加;被赋值,计数减少;当引用计数为0,自带的销毁函数调用类析构函数析构对象;当循环引用时,仍然会发生内存泄漏;靠weak_ptr解决。
Weak_ptr:弱引用,被复制shared_ptr对象不会引发引用计数,能很好的解决shared循环引用内存泄露的问题。
Unique_ptr:任意时刻只能由一个指着对象,禁止复制和拷贝,实现形式“=delete”。计数为0,析构对象。
数组和指针,野指针
C++内置数组是一块连续的内存空间,数组名其实是一个指针,指向数组第一个元素地址。野指针指向已删除对象地址或是其他未申请地址的指针,访问野指针很危险。
静态函数与虚函数(多态),虚析构
静态函数在编译时绑定;
虚函数由virtual标识,采用动态绑定方式。派生类覆盖基类虚函数,基类指针指向派生类对象,实现动态多态。在运行时判断指针指向的对象类型从而调用该对象的函数版本。
底层的实现是虚表。
基类析构函数必须为虚。否则若有基类指针指向派生类对象进行析构时函数调用基类析构函数,会出现错误,发生内存泄漏。
可以不为虚,只要你保证不会发生动态绑定。
函数指针
顾名思义,指向函数的指针。相当于拿到函数的访问地址,可以方便地将函数作为参数传入其他函数。。
定义方法:int (*fp)(int, int);
赋值:fp = &max;
直接调用:fp(1, 2)
作为参数使用 int cal(int a, int b, int (*fp)(int, int))
函数重载二义性
二义性是在编译阶段编译器进行函数匹配(匹配函数名、参数种类和个数)的时候出现的错误。多种情景会导致函数重载二义性。
默认参数与无参函数;
隐式类型转换造成的。比如:double, int, ßlong
类类型的转换,比如派生类对象指针/引用可以传给基类指针/引用,当出现派生类指针和基类指针两个函数时,编译器无法匹配,出现错误。
解决方法:①编程时注意;②使用explicit关键字声明函数,避免隐式类型转换的出现。
重载和覆盖和隐藏
重载指的是函数名相同而参数个数/类型不同从而实现调用同名函数,给定不同参数实现静态多态。
覆盖多用再类继承中,派生类继承基类,然后实现自己的函数版本,如果是虚函数一般在后面加上关键字override(可以提示编译器检查覆盖语法是否正确)。
隐藏是指派生类实现了与基类同名的函数,导致基类函数在派生类内不可见的现象。
隐式类型转换
种类较多。
① Int, double, float, unsigned_int, unsigned_long等内置类型可以相互转换;
② 整型提升,在计算时位数小于int的类型都会提升成为int进行计算;
③ 在表达式中,先将各个类型转换成类型中最宽的类型再进行计算,计算完成时将结果(右值)再转换成为左值变量的类型,然后抛弃将亡右值。
④ 基类与派生类之间存在隐式类型转换关系。向上(未理解)、向下、横向转换。派生类向基类进行转换,派生类向派生类进行转换。
四种cast类型转换
static_cast:用于一般的强制类型转换
const_cast:专用于除去const属性
dynamic_cast:RTTI的内容,用于运行时类型检查,将派生类对象指针/引用转换成基类指针/引用(upcast);或者将指向派生来对象的指针/引用转换成派生类对象指针/引用(downcast)。
interpret_cast:强制重新解释,即显示声明该变量/数据由该类来解释。
New/delete VS malloc/free
new/delete是C++关键字,操作的是对象,其实调用的是构造和析构函数。
malloc/free是C库函数,较为直接的操作内存,需要指定类型和空间大小。
new 有类型检查,malloc没有。
new返回内存地址,malloc返回void 指针,需要显示转换成我们需要的类型。
malloc函数原型:void* malloc(size_t) 头文件stdlib
RTTI
Runtime Type Identification,运行时类型识别,能够在运行时准确判断对象的类型,引入了type_info对象。通过typeid().name()来查看,头文件type_info.h。
另外类转换函数dynamic_cast<T>(expression),可以判断expression is a T问题。
可以进行upcast和downcast转换
upcast: 向上转换,即把派生类对象指针/引用转换为基类指针/引用;
downcast:向下转换,把指向派生类的基类指针/引用转换成为派生类指针/引用。
比较:type_info对象会增加开销。
虚函数的实现---虚表,怎么实现了多态?
虚表vftable,编译器为每个拥有虚函数的类都建有一张虚函数表,里面存有虚函数的入口指针(地址)。在类对象的内存布局中,先是一个vfptr虚表指针,指向虚表首地址,而后通过偏移量的形式来访问虚表中的地址。
发生单继承时,派生类内存布局,先是复制一份基类内存布局,然后是自己的布局(注意内存对齐)。虚表指针指向自己的虚表,派生类虚函数地址如果自己未覆盖,那么就是基类的,否则是自己的函数地址。
发生多继承时:先按照继承顺序,从左到右排布基类的布局包括虚标指针,然后排布自己的指针和数据;派生类虚表排布形式是按照继承顺序,是继承来的虚函数,如果有覆盖则换成自己的函数地址;然后是下一个基类,直至基类排布完毕。多张虚表各自独立。
发生虚继承时:无论是对象内存排布还是虚表,虚基类的部分都被放到最后排布。
STL map 与 set区别与实现
都是关联式容器,
① map是映射,存储的是pair<type, type>(key, value),默认有序,可以根据key修改value;set只存储key,key就是它的值,保持有序;元素的key都不可以重复,插入调用的都是红黑树的insert_unique函数,防止重复。
② map支持下标访问,set不支持,map下标查找不到对应key,会插入一条,要注意。
二者底层的实现都是红黑树,他们的函数只是对红黑树做了简单的封装。
红黑树要点:一种平衡二叉搜索树
节点红黑二色;根节点黑;红色节点子节点必须是黑;任意节点到叶子节点经过的黑色节点数相同;
插入和查找复杂度:O(logn)
维持自身的平衡,需要进行旋转(最多进行三次旋转可以平衡)和变色。
AVL树,另一种平衡二叉搜索树
维持较为严格的平衡条件:左右子树高度差最大为1;
在高度上会小于红黑树,但是在插入和删除时要进行复杂度旋转(单双旋转),比红黑树要复杂。统计性能红黑树要优于AVL树所以STL采用RB-tree。
STL 迭代器 与指针的区别,迭代器怎么删除元素
迭代器是STL的关键所在,STL中心思想是把容器和算法分离开发,独立设计成泛型(类模板和函数模板),再用一种粘合剂将二者结合起来,迭代器就是该角色。迭代器按照容器内部次序访问元素,而避免了暴露/考虑容器内不得结构。毕竟容器内部的实现各异,map/set族使用RB-tree,unordered族使用hashtable,vector使用内置数组,通过三个核心指针实现,queue和list使用双端队列deque实现。举个栗子:map的第一个元素是mostleft,最左节点,而vector第一个就是vec[0]使用指针必须要了解各个容器内部的实现,使用迭代器map.begin()/end(), vec.begin()/end()即可。
STL里resize和reserve的区别
resize进行容器大小重定义,如果重定义的size大于原始size,则增大原始size,并对扩充空间调用构造函数初始化;若小,则析构尾部多出的元素。对于某些容器如vector、unordered_map,capacity增长规律可能会不同。
reserve为容器预定空间,即它改变capacity大小。若大于原始容量扩充,若小,不做处理。
vector和list的区别,应用,越详细越好
vector是一个动态数组,可以动态的扩充容量,扩容规律,每当要超过当前容量,就扩容成为原始两倍,如果不够就扩充至需要的大小。(重新申请内存空间,原始数据搬移/拷贝构造,新数据初始化);使用连续的内存地址,支持随机访问O(1)、删除和插入耗时O(n);
list底层实现使用deque,使用非线性地址,不支持随机访问,查找耗时O(n),插入删除快速O(1);
类成员访问权限
三种public、private、protect
private成员只能在类内访问,类外不可访问,派生类内不可见,对象不可访问;
protect成员类内可访问,派生类内可访问,对象不可访问;
public:类内、派生类内、对象也可访问。
struct和class区别
struct是从C继承过来的,完全可以实现class的功能;但是无法实现访问控制,默认public,class默认private;
另外,class可用于声明模板,而struct不可以。
声明一个模板函数和一个模板类
templete<typename T> ret-type func-name(param list)
{ statement-list;}
如:templete <class T> void add(T a, T b)
{ return static_cast<T>(a+b);}
templete<type-name T>
class class-name{
T var1;
int var2;
public:
T add(T &a, T &b);
};
右值与左值,右值引用?用法?
左值和左值引用我们很熟悉了。一般定义的变量/对象都是左值,可以被赋值,直到作用域结束才释放。左值引用相当于左值的别名,可以通过它修改左值。可以被赋值
右值不同于左值,它是临时值(比如表达式结果-汇编里是立即数),用完就被抛弃释放,又称将亡值,我觉得很是贴切。不可被赋值。
右值引用就是右值的引用,可以实现右值的持久化,可以通过它访问右值。
主要的用法在于移动语义,,即移动构造和移动赋值函数。构造函数形参设置成右值引用,传进来的对象成为右值,要初始化的对象将它的内存空间“偷”过来(它的指针地址赋给该对象,再将它的指针置空),这样一来,就避免了申请新的空间以及释放右值对象的空间,这一招“借尸还魂”可以有效地提高效率。
include <> 和 “”的区别
<>用于引用编译器提供的头文件,在编译器指定include目录找不到则报错;
“”主要用于项目中自己实现的头文件引用,先在项目目录寻找,找不到会去编译器目录中找,找不到报错。(至少VS2017是这样)
C++内存管理/分配
栈区
一些局部变量
堆区
动态申请的内存,如new出来的地址;
全局/静态区
全局变量、static静态变量
文字常量区
存储字符串等常量,const 修饰变量
代码区
存储函数内容
堆与栈
① 管理方式:栈由编译器管理,堆由程序员自己申请和释放;
② 大小限制:程序栈大小一般固定较小(几M),堆较大(几G);
③ 分配方式:堆动态分配,栈一般是静态分配(如局部变量的定义)也有动态分配(不常用)
④ 生长方向:从上图可看出,栈向低地址端扩展;堆向高地址端扩展;
⑤ 分配效率:栈由系统提供,甚至有专门的寄存器和指令负责,而堆有C++程序库实现,有复杂的堆算法,故栈效率要高于堆;
⑥ 此外,堆的分配还会产生内碎片,影响存储内存的使用,栈先进后出结构,不会产生碎片。
段错误
一般由访问受保护内存、堆栈溢出、数组越界造成。
//数组越界
int a[10] = {0};
cout << a[20] << endl;
//访问受保护内存
int *p = nullptr;
*p = 10;
解决办法:检查是否有上述问题存在;gcc/g++ -g 编译, 使用gdb调试r
收到信号:SIGSEGV SEGV-segmentation violation 段违例。
STL allocator 的实现
STL空间配置器,SGI的是实现形式是std::alloc,所有的容器都实际使用alloc作为空间配置器,不接受反驳(这里的空间不只是内存还有外存,如磁盘)。alloc并不符合标准,他也实现了一个符合部分标准的std::allocate配置器,但只是对new和delete做了简单封装,效率太低,它自己都没用过。
一般的,我们使用new和delete运算符配置对象。新建对象涉及两个步骤:①使用::operator new 配置内存,②用构造函数构造对象内容;销毁对象也涉及两步骤:①使用析构函数析构函数内容;②使用::operator delete 释放内存。
SGI的实现则是:内存配置使用alloc:allocate(), 对象构造使用::construct();对象析构使用::destroy(),内存释放使用alloc:deallocate()。
为什么要实现alloc? 怎么实现的?
二级配置器将128bytes下空间分成16个等级来分配空间,显著的减少了内部碎片,提高了内存的使用效率。
下图中第二级忘记画内存块的回收了,是存在回收的!
看到这里,足以证明你是一个有耐心的人了,💪!
进阶知识
组合还是继承?
1.拿两个类来说,如果存在is A关系,意思是如果一个类是另一个类的“一种”,那么使用继承,而后修改一些属性,以体现自己的个性。
2.如果一个类需要其他多个类对象的参与来组成,及A has a B & C & D,那么,使用组合。
如果说,还没明白的话,拿现象级游戏《王者农药》来说。英雄有不同的身份,那么我们有一个总的“英雄类”,而其他不同的角色,如法师,战士,射手,都是英雄的一种,所以他们都可继承自“英雄”。再说组合:只要你氪金,就能拥有闪瞎眼的皮肤和特效,而这些皮肤和特效也是不同的类,一个英雄,有头、身子、脚、皮肤、技能、特效,都是has a的关系,那么由他们来组成一个英雄。
std::move和std::forward
move:主要用来获取一个左值的右值引用。(无条件将实参转换成为右值)。
forward:用于在模板中将一个函数或者函数包,连同实参的类型都传递给其他函数。(传入右值,返回右值,否则返回左值)。
二者都用到了---引用折叠。
当有一系列引用出现时,即 T & &, T && & , T & &&, T&& &&,除了第四种折叠成为右值外,其余都折叠成为左值。
C++的访问控制
public:类内、类对象、友元内
protected:类内、友元内
private:类内、友元内
发生继承时:
public:基类private不可访问,public成为public,protect成为private;
protected:基类private不可访问,public成为protected,protected还是protected
private:基类private不可访问,public和protected都成为private
continue...