C++面试重点整理
整理各大八股,夹杂自己理解,死记硬背效果差,搜索验证才记得牢,由于这里只是作为实习准备,所以不会记录太全,干不干程序员这行还说不定呢...
2023-09-26 20:07:54 星期二 三阳开泰五天没好难受至极
C++基础语法
1.内存管理
在 C++ 中,程序运行时,内存主要分成四个区,分别是栈、堆、数据段和代码段。
- 栈:存储局部变量、函数参数和返回值。
- 堆:存储动态开辟内存的变量(new malloc)。
- 数据段:存储全局变量和静态变量。
- 代码段:存储可执行程序的代码和常量(例如字符常量),此存储区不可修改。
栈和堆的主要区别:
- 1.管理方式不同:栈是系统自动管理的,在出作用域时,将自动被释放;堆需手动释放,若程序中不释放,程序结束时由操作系统回收。
- 2.空间大小不同:堆内存的大小受限于物理内存空间;而栈就小得可怜,一般只有8M(可以修改系统参数)。
- 3.分配方式不同:堆是动态分配;栈有静态分配和动态分配(都是自动释放)。
- 4.分配效率不同:栈是系统提供的数据结构,计算机在底层提供了对栈的支持,进栈和出栈有专门的指令,效率比较高;堆是由C++函数库提供的。
- 5.是否产生碎片:对于栈来说,进栈和出栈都有着严格的顺序(先进后出),不会产生碎片(栈顶指针地址,参数,局部变ᰁ);而堆频繁的分配和释放,会造成内存空间的不连续,容易产生碎片(空闲链表),太多的碎片会导致性能的下降。
- 6.增长方向不同:栈向下增长,以降序分配内存地址;堆向上增长,以升序分配内存地址!
总结:
- 先想像上面那张图,内存高到低分别是栈、堆、数据段、代码段,由于栈先进后出的特性往下扩展,堆由于它们中间有一个共享内存空间,所以向上扩展,对于这块内存空间的分配,栈释放内存顺序遵循作用域自动释放和出栈原则,堆手动释放,所以相较前者会产生内存碎片,并且栈系统内置指令,堆是c++专门语法来使用的,其他语言屏蔽,使用函数库实现,所以慢得多
2.C 和 C++ 区别 (函数/类/struct/class)
- C++是C的超集,基础语法差不多
- C++是面向对象的语言,其三大特性封装、继承、多态更加注重将大问题抽象化,模块化。抽象化指将问题用缩小用class创建对象的方式解决问题
class和strcut区别:
根本特性上: - class默认成员私有,有修饰符控制访问权限,还有成员函数,可以用来将成员变量私有用成员函数暴露给类外访问,有私又共,一切皆对象。struct默认公有,只有成员变量,通常用来包装数据,在项目多个相同功能数据的时候经常使用struct来传递数据
- 继承和多态(纯虚函数)则可以实现代码的复用,实现模块化,常见如面向对象的设计模式单例模式和工厂模式等等
功能上:
标准库很丰富,如STL模板库支持泛型编程等等
支持重载,C++ 函数名字的修饰会将参数加在后⾯
安全性上: - 命名空间防止命名冲突
- 将malloc free进一步封装成更适合面向对象更安全的new delete
- 指针加入引用更安全,更简洁,对于还有c++11的智能指针
- 四种强制类型转换
3.结构体内存对⻬问题
- 对于结构体中的各个成员,第⼀个成员位于偏移为 0 的位置,以后的每个数据成员的偏移必须是最小数据成员的倍数
- 结构体整体大小应该按最大数据成员的倍数
- 对于数据类型来说,只不过是在系统操作内存空间的方式而已,一次使用多少个字节
struct Student { char name[10]; // 对齐:10字节 // 填充字节:2字节 int score; // 对齐:4字节 // 总大小:16字节 };
4单个自己最大,要满足四的倍数,14不满足
内存对齐的作用
CPU读取内存是按字节块读取完然后放到寄存器里的,一次性读四个和一次读一个效率不同
另外有些跨平台需要某些特定类型的数据,有些地址访问不到
4.指针和引⽤的区别
指针:存放变量在内存的地址,指针变量存放变量在内存的首地址,指针变量占用地址(64位系统8字节足够了)指针是追踪可以加减地址,内存是访问,指针指向基类型的可以强制转换,可以为空,可以改变对象指向,sizeof 指针得到的是指针类型的⼤⼩
引用:
- 引用是已定义的变量的别名,必须初始化,无法改变对象,无法强制转换,实际上是指针常量(汇编层次理解),sizeof 引⽤得到代表对象的⼤⼩
- 引用的主要用途是用作函数的形参和返回值。
-传地址的意义如下: - 可以在函数中修改实参的值。
- 减少内存拷贝,提升性能。
本质怪: - 作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引⽤的实质是传地址,传递的是变ᰁ的地址
5.指针与const
-
常量指针
const 数据类型 *变量名;
不能通过解引用的方法修改内存地址中的值用原始的变量名是可以修改的
一般用于修饰函数的形参,表示不希望在函数里修改内存地址中的值。 -
指针常量
数据类型 * const 变量名;
指向的变量(对象)不可改变。在定义的同时必须初始化,否则没有意义。
可以通过解引用的方法修改内存地址中的值。
5.5值传递:形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
- 值传递:形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
- 指针传递:也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就
- 相当于对实参本身进⾏操作。
- 引⽤传递:实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯
6.在传递函数参数时,什么时候该使⽤指针,什么时候该使⽤引⽤呢
值很小使用值传递,实参是数组只能使用指针
实参是对象用引用(对象拷贝)编程规范
不要返回局部变量的引用,作用域自动销毁
指针作为参数进⾏传递时,也是将实参的⼀个拷⻉传递给形参,两者指向的地址相同,但不是同⼀个变量
- 除此之外,最好引用简洁好用
7.new / delete 与 malloc / free的异同
都可以⽤来在堆上分配和回收空间。new /delete 是重载操作符,malloc/free 是库函数。
new 实际上执⾏两个过程:
- 分配未初始化的内存空间(malloc);
- 使⽤对象的构造函数对空间进⾏初始化;返回空间的⾸地址。
- 如果在第⼀步分配空间中出现问题,则抛出std::bad_alloc 异常,或被某个设定的异常处理函数捕获处理;如果在第⼆步构造对象时出现异常,则⾃动调⽤ delete 释放内存。
执⾏ delete 实际上也有两个过程
- 使⽤析构函数对对象进⾏析构;
- 回收内存空间
new和malloc的区别:
- new 和 malloc 的区别,new 得到的是经过初始化的空间,⽽ malloc 得到的是未初始化的空间。所以 new 是 new ⼀个类型,⽽ malloc 则是malloc ⼀个字节⻓度的空间。delete 和 free 同理,delete 不仅释放空间还析构对象,delete ⼀个类型,free ⼀个字节⻓度的空间
为什么有了 malloc/free 还需要 new/delete?
- 因为对于⾮内部数据类型⽽⾔,光⽤ malloc
- free ⽆法满⾜动态对象的要求。对象在创建的同时需要⾃动执⾏构造函数,对象在消亡以前要⾃动执⾏析构函数。由于 mallo/free 是库函数⽽不是运算符,不在编译器控制权限之内,不能够把执⾏的构造函数和析构函数的任务强加于 malloc/free,
delete p、delete [] p、allocator都有什么作⽤
- delete p用于调用析构函数释放通过new运算符分配的单个对象的内存
- delete [] p用于逆序调用析构函数释放通过new[]运算符分配的数组对象的内存
- allocator适用和容器一起使用的模板类,allocator可以帮助我们在堆上分配内存,并在不再需要时释放内存。它提供了allocate和deallocate成员函数来分配和释放内存,以及construct和destroy成员函数来构造和销毁对象
std::allocator<T> alloc; // 创建一个 allocator 对象 T* p = alloc.allocate(1); // 分配单个对象的内存 alloc.construct(p, args); // 构造对象 // 使用对象 p alloc.destroy(p); // 销毁对象 alloc.deallocate(p, 1); // 释放内存
- new在内存分配上⾯有⼀些局限性,new的机制是将内存分配和对象构造组合在⼀起,同样的,delete也是将对象析构和内存释放组合在⼀起的。allocator将这两部分分开进⾏,allocator申请⼀部分内存,不进⾏初始化对象,只有当需要的时候才进⾏初始化操作
malloc、realloc、calloc的区别 - 自行初始化内存大小
- 默认初始化为0
- 用于扩充容量
8.变ᰁ声明和定义区别
- 变量的声明是引入变量的名称和类型,告诉编译器变量的存在;而变量的定义是为变量分配内存并初始化它。全局变量定义的位置要在使用他的函数之前,不然会报错
9.宏定义和函数和和typede const 内联函数有何区别?
- 宏定义预处理阶段进行全局文本替换,提高代码执行效,代码可读性差,没有类型检查和语法检查,容易出错,多分数据拷贝在内存
- 函数程序运行阶段在调用进行跳转的,可重用的代码块,具有明确定义的输入和输出类型,作用域是局部的,不易出错
- 宏主要⽤于定义常ᰁ及书写复杂的内容;typedef⽤于定义类型别名,type在编译类型检查。
- const编译期间可以对其进⾏类型安全检查,主要用于定义变量,形参(不改变内部),类的成员函数,指针(顶底层),引用(左右值),只有一份数据在内存
- 内联函数会在编译时类型检查,减少函数调用的开销,将函数的代码插入到调用处,而不是跳转到函数的地址执行。增加代码的大小,因为每个调用处都会插入函数体的副本。
总结:类型检查 预处理还是编译
10.a和&a有什么区别
- 指针变量(地址的值)加1后,增加的量等于它指向的数据类型的字节数,
- C++内部,用指针来处理数组。
*P = &a; *(P+1)(运算符优先级) = &a+1 = a[1]解释为 *(数组首地址+下标) &a = a;
- 数组是占用连续空间的一块内存,数组名被解释为数组第0个元素的地址。C++操作这块内存有两种方法:数组解释法和指针表示法,它们是等价的,指针指针是根据元素指向的,元素大小都是相同的
11.C++中const和static的作⽤
- const:用于变量、函数参数和函数返回类型,变量不可修改,函数参数不希望内部被改变,返回值不被修改
- static::修改(主要类) 变量、函数和类成员。属于类,调用通过类域名解析附,全局静态编译器初始化,局部在初次使用构造初始化,在静态存储区只有一份,全局包括对象共享static类对象必须要在类外进⾏初始化,static修饰的变ᰁ先于对象存在,不属于对象所以没有this指针也就无法访问非静态成员,
- 补充:和全局变量区别, 全局变量只有一份,但不同文件同名的时候链接器的特性会链接同名全局变量,会报错。而static一般在文件内,想要使用只能通过外部接口,不过接口调用是旧文件的拷贝副本。
12.final和override 区别
- override:纯虚函数必须在子类实现,不用担心。如果是虚函数,在子类实现的情况下会有三中情况:1.函数名正确能够重写虚函数 2.函数名错误当成新函数 3.函数名错误但是后缀添加override表明重写父类虚函数,会报错
- final:用于类、函数,声明后类不可以被派生类重写和继承。
13.拷⻉初始化和直接初始化和赋值
- 初始化声明变量指定一个初始值,保证变量没有垃圾值。赋值操作用于改变变量的当前值,并将其设置为新的值另外,如进行赋值运算符重载,以及拷贝构造
- 直接初始化直接调⽤与实参匹配的构造函数,拷⻉初始化⾸先使⽤指定构造函数创建⼀个临时对象,然后⽤拷⻉构造函数将那个临时对象拷⻉到正在创建的对象。
string str1("I am a string");//语句1 直接初始化 string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调⽤拷⻉构造函数 对str2进⾏初始化 string str3 = "I am a string";//语句3 拷⻉初始化,先为字符串”I am a string“创 建临时对象,再把临时对象作为参数,使⽤拷⻉构造函数构造str3 string str4 = str1;//语句4 拷⻉初始化,这⾥相当于隐式调⽤拷⻉构造函数,⽽不是调⽤ 赋值运算符函
14.extern"C"的⽤法(转移在19关键词部分)
15.野指针与空指针和函数指针
野指针
原因:
- 指针在定义的时候,如果没有进行初始化
- 内存被释放后,指针不会置空,但是,指向的地址已失效
- 让指针指向了函数的局部变量内存已释放
解决: - 指针初始化和内存被释放后都置位nullptr,不要返回局部变量的地址。
空指针: 访问置空指针,空指针的内存中分配了一块区域管理,但是无法进行读写操作,所以程序会奔溃,建议操作指针前先判空,有NULL nullptr 0S三种置空选择,一般选择nullptr是一个void*类型无法强制转换,其他两种会产生隐式转换的歧义
函数指针 - 函数指针是指向函数的指针变ᰁ,每⼀个函数都有⼀个⼊⼝地址,该⼊⼝地址就是函数指针所指向的地址。有了指向函数的指针变ᰁ后,可⽤该指针变ᰁ调⽤函数,
用途:调⽤函数和做函数的参数,⽐如回调函数
16.C++中的᯿载、᯿写(覆盖)和隐藏的区别
- 重载overload:同一作用域,同一函数名,不同函数参数,不限制返回值,调用根据参数个数选择,
- 重写overriide:基类和派生类中具有相同的函数名、参数列表和返回类型,使用 override 关键字明确指示对基类中虚函数的重写。
- 函数隐藏:函数隐藏是指在派生类中屏蔽了与基类中的非虚函数同名的函数,能够显式调用父类被屏蔽的函数,
17.C++有哪⼏种的构造函数
构造函数的作⽤:初始化对象的数据成员
默认构造函数:当用户没有显示定义构造函数的时候会调用
参数化构造函数:允许在创建对象时提供初始化参数,以便对象可以在构造时具有特定的状态。
拷贝构造函数:
-
拷贝构造函数用于通过已存在的对象创建一个新的对象,(通常是 const 引用),可以执行深拷贝或浅拷贝,默认拷贝是浅拷贝
为什么拷⻉构造函数必需时引⽤传递,不能是值传递
⽆限递归 -
浅拷⻉只是拷⻉⼀个指针,并没有新开辟⼀个地址,拷⻉的指针和原来的指针指向同⼀块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷⻉的指针的资源就会出现错误,如果拷贝资源没有指针就没事
-
深拷⻉不仅拷⻉值,还开辟出⼀块新的空间⽤来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷⻉得到的值。
调用时机: -
⽤类的⼀个实例化对象去初始化另⼀个对象
-
函数的参数是类的对象时
-
函数以值的方式返回对象时
给目标对象指针分配一块内存 //深拷⻉ //name = new char(20); //memcpy(name, s.name, strlen(s.name));
构造函数或析构函数中调⽤虚函数会怎样
构造函数一般是做一些初始化数据的操作,析构函数是做一些资源释放的操作,而题目描写的作法很邪门不符合规范
移动构造函数:将临时对象或右值引用的对象的资源转移到新创建的对象,而无需执行深拷贝构造
- 后续移动左右值会提到
析构函数:对象的生命周期结束时被调用,用于清理对象分配的资源
构造函数的执⾏顺序?析构函数的执⾏顺序
构造函数顺序
- 创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
- 销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。如果手工调用派生类的析构函数,也会调用基类的析构函数。
析构函数⼀般写成虚函数的原因
- ⼀个基类的指针指向⼀个派⽣类的对象,在使⽤完毕准备销毁时(该对象的析构函数的函数地址早就被绑定为基类的析构函数),如果基类的析构函数没有定义成虚函数,那 么编译器根据指针类型就会认为当前对象的类型是基类,调⽤基类的析构函数,派⽣类的⾃身内容将⽆法被析构
- 如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执⾏派⽣类的析构函数,再执⾏基类的析构函数,成功释放内存
构造函数析构函数可否抛出异常
- 构造函数可以抛出异常,这意味着在对象创建的过程中,可以通过抛出异常来通知调用者。当构造函数抛出异常时,对象的创建将被中止,需要使用异常处理机制来捕获和处理构造函数抛出的异常,以便进行适当的清理工作。在构造函数中抛出异常时,已经执行的构造函数的代码将被回滚,对象的析构函数也不会被调用。
- 析构函数也可以抛出异常,当析构函数抛出异常时,异常会逃逸出析构函数的范围,并且通常会导致程序终止(即调用std::terminate())。这如果调用栈上存在其他正在处理异常的函数,那么两个异常可能会相互冲突,导致程序处于不确定状态。
赋值运算符的᯿载:
- 注意,这个类似拷⻉构造函数,将=右边的本类对象的值复制给=左边的对象,它不属于构造函数,=左右两边的对象必需已经被创建。如果没有显示的写赋值运算符的᯿载,系统也会⽣成默认的赋值运算符,做⼀些基本的拷⻉⼯作
A a1, A a2; a1 = a2;//调⽤赋值运算符 A a3 = a1;//调⽤拷⻉构造函数,因为进⾏的是初始化⼯作,a3 并未存在
18.public,protected和private访问和继 限/public/protected/private的区别
- 类成员:public和protected在类外可以访问,private只能在类成员函数访问
- 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。继承权限会降级不会升级,
- 使用 using 关键字可以改变基类成员在派生类中的访问权限。using A::m_b; // 把m_b的权限修改为公有的。,可以通过基类的公有成员函数间接访问基类的私有成员。
19.volatile、mutable 和explicit extern"C"关键字的⽤法
volatile:
- 下⼀条语句不会直接使⽤上⼀条语句对应的volatile 变ᰁ的寄存器内容,⽽是᯿新从内存中读取,保证读写操作一致性如线程同步,为了保持volatile的顺序性,编译器不会优化
mutable: const 成员函数本身不应该修改对象的状态,在const函数⾥⾯修改⼀些跟类状态⽆关的数据成员,那么这个函数就应该被mutable来修饰,
explicit: 用于修饰类的构造函数,防止编译器进行隐式的类型转换,只允许显式地调用构造函数进行对象的创建。
extern"C" - 在c++源文件中告诉编译器这部分代码按照C语⾔进⾏编译,不要对 进行名称修饰,以保持与 C 语言的函数名一致。这样,在链接时就可以正确地找到并调用 C 语言中定义的函数。
总结:volatile 用于确保对变量的读写操作的可见性和一致性,mutable 用于在 const 成员函数中修改成员变量,explicit 用于控制构造函数的显式调用
20.C++的异常处理
- 数据越界,分配内存失败等等都会引发异常~略
21.形参与实参的区别
- 形参被调用时分配内存,调用结束释放内存,变量无法再使用,实参需要是以及被明确赋值的值(表达式 函数 数字 常变量),形参传递给实参,除了指针引用以外,形参的值是对实参的拷贝,互不影响。
22.类成员初始化⽅式?为什么⽤成员初始化列表会快些?什么情况必须使用
方式:
赋值初始化,通过在函数体内进⾏赋值初始化,数据成员分配内存后才在函数体内进行初始化,
列表初始化,在冒号后使⽤初始化列表进⾏初始化,数据成员分配内存的同时进行初始化,在函数体执行之前
必须使用:
- 初始化const成员变量,const成员变量在对象创建后不能再被修改,所以必须在对象创建时进行初始化,
- 引用类型成员变量必须在创建对象时进行初始化,并且只能通过成员初始化列表来完成
为什么会很快: - 成员初始化列表允许在对象构造时直接初始化成员变量,而不是先调用默认构造函数创建临时对象,然后再通过赋值操作符将值赋给成员变量。这样可以避免了不必要的临时对象的创建和赋值操作,提高了效率。
~~### 23.C++中新增了string,它与C语⾔中的 char 有什么区别吗?它是如何实现的?
~~区别: string封装子char,包含了char*数组,容ᰁ,⻓度等等属性。
实现: string可以进⾏动态扩展,在每次扩展的时候另外申请⼀块原空间⼤⼩两倍的空间(2^n),然后将原字符串拷⻉过去,并加上新增的内容
24.什么是内存泄露,如何检测与避免
原因:动态分配的内存空间没有被正确释放(没有delete或释放顺序错误)或者释放时出现错误(多次delete),导致这部分内存无法再被程序所使用,最终导致系统内存资源
解决: 养成良好习惯,使用智能指针,析构函数编写正确
25.介绍⾯向对象的三⼤特性,并且举例说明
封装:class(Encapsulation):封装是将数据和对数据的操作封装在一起,形成一个独立的实体。通过封装,对象的内部细节对外部是不可见的,外部只能通过对象提供的公共接口来访问和操作对象。封装可以隐藏实现细节,提高代码的可维护性和安全性。常见修饰符的使用。
什么是组合?
即内嵌其他类的对象作为⾃⼰的成员,先调⽤内嵌对象的构造函数,然后按照内嵌对象成员在组合类中的定义顺序,与组合类构造函数的初始化列表顺序⽆关。然后执⾏组合类构造函数的函数体,析构函数调⽤顺序相反
-继承:
- 冒号是指一个类可以派生出子类,子类可以继承父类的属性和方法。通过继承,子类可以重用父类的代码,并且可以在不修改父类的情况下增加新的功能或修改已有功能。继承可以建立类之间的层次关系,提高代码的可扩展性和复用性。
- 实现继承:实现继承是指直接使⽤基类的属性和⽅法⽽⽆需额外编码的能⼒。
- 接⼝继承:接⼝继承是指仅使⽤属性和⽅法的名称、但是⼦类必需提供实现的能⼒
多态: 是指同一类型的对象,允许将⼦类类型的指针赋值给⽗类类型的指针,使用基类指针来调用子类的成员,重载实现编译时多态,虚函数实现运⾏时多态)
多态本质 - 静态多态其实就是᯿载,因为静态多态是指在编译时期就决定了调⽤哪个函数,根据参数列表来决定;
- 动态多态是指通过⼦类᯿写⽗类的虚函数来实现的,因为是在运⾏期间决定调⽤的函数,所以称为动态多态
虚函数本质
- 编译器会为该类⽣成⼀个虚函数表,保存该类中虚函数的地址,同样,派⽣类继承基类,派⽣类中⾃然⼀定有虚函数,所以编译器也会为派⽣类⽣成⾃⼰的虚函数表。当我们定义⼀个派⽣类对象时,编译器检测该类型有虚函数,所以为这个派⽣类对象⽣成⼀个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的。⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态
纯虚函数
- ⼦类继承⽗类时, ⽗类的纯虚函数必须᯿写,否则⼦类也是⼀个虚类不可实例化。 定义纯虚函数是为了实现⼀个接⼝,起到⼀个规范的作⽤,规范继承这个类的程序员必须实现这个函数(接口继承)
编译器处理虚函数表应该如何处理
对于派⽣类来说,编译器建⽴虚函数表的过程其实⼀共是三个步骤:
- 拷⻉基类的虚函数表,如果是多继承,就拷⻉每个有虚函数基类的虚函数表
- 当然还有⼀个基类的虚函数表和派⽣类⾃身的虚函数表共⽤了⼀个虚函数表,也称为某个基类为派⽣类的主基类
- 查看派⽣类中是否有᯿写基类中的虚函数, 如果有,就替换成已经᯿写的虚函数地址;
- 查看派⽣类是否有⾃身的虚函数,如果有,就追加⾃身的虚函数到⾃身的虚函数表中。
26.C++的四种强制转换reinterpret_cast/const_cast/static_cast/dynamic_cast
- c语言的强制转换可能会出现精度丢失,跨平台错误,绕过编译器,而c++的强制转换分四种含义和作用更加明显专一
- static_cast: 用于基本内置数据类型之间的转换,不会提示警告信息,用于指针之间的转换不允许转换,必须借助void*类型作为中间介质。明确指出类型转换,⼀般建议将隐式转换都替换成显示转换,因为没有动态类型检查,上⾏转换(派⽣类->基类)安全,下⾏转换(基类->派⽣类) 不安全,所以主要执⾏⾮多态的转换操作;
- reinterpret_cast: 可以将整形转 换为指针,也可以把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换,
- const_cast:const_cast:专⻔⽤于 const 属性的转换,去除 const 性质,或增加 const 性质,经常用在引用上个转换符中唯⼀⼀个可以操作常ᰁ的转换符
- dynamic_cast : 是 C++ 中的一个类型转换运算符,用于在运行时进行动态类型检查和转换。它主要用于处理多态类型(基类指针或引用指向派生类对象)的类型转换。
27.C++函数调⽤的压栈过程
- 从栈空间分配存储空间,从实参的存储空间复制值到形参栈空间,进⾏运算
- 形参在函数未调⽤之前都是没有分配存储空间的,在函数调⽤结束之后,形参弹出栈空间,清除形参空间。
- 数组作为参数的函数调⽤⽅式是地址传递,形参和实参都指向相同的内存空间,调⽤完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。
- 当函数有多个返回值的时候,不能⽤普通的 return 的⽅式实现,需要通过传回地址的形式进⾏,即地址/指针传递
28.内存池是什么
- 重载new和delete运算符的目是为了自定义内存分配的细节。(内存池:快速分配和归还,无碎片)
- 内存池(Memory Pool) 是⼀种内存分配⽅式。通常我们习惯直接使⽤new、malloc 等申请内存,这样做的缺点在于:由于所申请内存块的⼤⼩不定,当频繁使⽤时会造成⼤ᰁ的内存碎⽚并进⽽降低性能。内存池则是在真正使⽤内存之前,先申请分配⼀定数ᰁ的、⼤⼩相等(⼀般情况下)的内存块留作备⽤。当有新的内存需求时,就从内存池中分出⼀部分内存块, 若内存块不够再继续申请新的内存。这样做的⼀个显著优点是尽ᰁ避免了内存碎⽚,使得内存分配效率得到提升
29.hello.c 程序的编译过程
源代码-->预处理-->编译-->优化-->汇编-->链接-->可执⾏⽂件-
- 预处理
读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进⾏处理。对源程序的“替代”⼯作 i - 编译阶段
编译程序所要作得⼯作就是通过词法分析和语法分析,符合语法规则之后, - 汇编阶段
汇编过程实际上指把汇编语⾔代码翻译成⽬标机器指令的过程。对 s - 链接阶段
链接程序的主要⼯作就是将有关的⽬标⽂件彼此相连接 o
30.静态绑定和动态绑定的介绍
- 静态类型就是它在程序中被
- 声明时所采⽤的类型,在编译期间确定。动态类型则是指“⽬前所指对象的实际类型”,在运⾏期间确定。
- virtual 函数是动态绑定的,⾮虚函数是静态绑定的
31.说⼀下 fork,wait,exec 函数
- fork⽗进程产⽣⼦进程使⽤ fork 拷⻉出来⼀个⽗进程的副本,此时只拷⻉了⽗进程的⻚表,两个进程都读同⼀块内存。
- waitfork 从⽗进程返回⼦进程的 pid,从⼦进程返回 0,调⽤了 wait 的⽗进程将会发⽣阻塞,直到有⼦进程状态改变,执⾏成功返回 0,错误返回 -1。
- 当有进程写的时候使⽤写实拷⻉机制分配内存,exec 函数可以加载⼀个 elf ⽂件去替换⽗进程,从此⽗进程和⼦进程就可以运⾏不同的程序了,exec 执⾏成功则⼦进程从新的程序开始运⾏,⽆返回值,执⾏失败返回 -1。
32.动态编译与静态编译
- 静态编译,编译器在编译可执⾏⽂件时,把需要⽤到的对应动态链接库中的部分提取出来,连接到可执⾏⽂件中去,无需再进行编译,主要优点是可执行文件独立、执行速度快,可执行文件相对较大,无法在运行时进行修改或优化。
- 动态编译,可执⾏⽂件需要附带⼀个动态链接库,需要调⽤其对应动态链接库的命
- 令,缩⼩了执⾏⽂件本身的体积,加快了编译速度,
- 如果其他计算机上没有安装对应的运⾏库,则⽤动态编译的可执⾏⽂件就不能运⾏
33.动态链接和静态链接区别
- 静态连接库就是把 (lib) ⽂件中⽤到的函数代码直接链接进⽬标程序,程序运⾏的时候不再需要其它的库⽂件;
- 动态链接就是把调⽤的函数所在⽂件模块(DLL)和调⽤函数在⽂件中的位置等信息链接进⽬标程序,程序运⾏的时候再从 DLL 中寻找相应函数代码
- 静态链接库与动态链接库都是共享代码的⽅式,如果采⽤静态链接库,lib中的指令都全部被直接包含在最终⽣成的 EXE ⽂件中了。但是若使⽤ DLL,该 DLL 不必被包含在最终 EXE ⽂件中,EXE ⽂件执⾏时可以“动态”地引⽤和卸载这个与 EXE 独⽴的 DLL ⽂件
补充: 动态库就是在需要调⽤其中的函数时,根据函数映射表找到该函数然后调⼊堆栈执⾏。如果在当前⼯程中有多处对dll⽂件中同⼀个函数的调⽤,那么执⾏时,这个函数只会留下⼀份拷⻉。但如果有多处对 lib ⽂件中同⼀个函数的调⽤,那么执⾏时该函数将在当前程序的执⾏空间⾥留下多份拷⻉,⽽且是⼀处调⽤就产⽣⼀份拷⻉
34.类如何实现只能静态分配和只能动态分配
前者是把 new、delete 运算符᯿载为 private 属性。
后者是把构造、析构函数设为 protected 属性,再⽤⼦类来动态创建
建⽴类的对象有两种⽅式:
- 静态建⽴,静态建⽴⼀个类对象,就是由编译器为对象在栈空间中分配内存;
- 动态建⽴,A *p = new A(); 动态建⽴⼀个类对象,就是使⽤ new 运算符为对象在堆空间中分配内存。这个过程分为两步,第⼀步执⾏ operator new() 函数,在堆中搜索⼀块内存并进⾏分配;第⼆步调⽤类构造函数构造对象;
- 只有使⽤ new 运算符,对象才会被建⽴在堆上,因此只要限制 new 运算符就可以实现类对象只能建⽴在栈上。可以将 new 运算符设为私有
35.哪些函数不能是虚函数
- 构造函数,构造函数初始化对象,派⽣类必须知道基类函数⼲了什么,才能进⾏构造;当有虚
- 函数时,每⼀个类有⼀个虚表,每⼀个对象有⼀个虚表指针,虚表指针在构造函数中初始化;
- 内联函数,内联函数表示在编译阶段进⾏函数体的替换操作,⽽虚函数意味着在运⾏期间进⾏
- 类型确定,所以内联函数不能是虚函数;
- 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚
- 函数没有任何意义。
- 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数
- 的说法。
- 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
C++11(移动语义 智能指针 绑定器)
1.空指针 nullptrr
- 出现的⽬的是为了替代 NULL,有些编译器会将 NULL 定 义为 ((void*)0),有些则会直接将其定义为 0。C++ 不允许直接将 void * 隐式转换到其他类型
2.Lambda 表达式
-
利⽤lambda表达式可以编写内嵌的匿名函数,⽤以替换ᇿ⽴函数或者函数对象;
-
每当你定义⼀个lambda表达式后,编译器会⾃动⽣成⼀个匿名类(这个类当然᯿载了()运算符),我们称为闭包类型(closure type)。那么在运⾏时,这个lambda表达式就会返回⼀个匿名的闭包实例,其实⼀个右值。所以,我们上⾯的lambda表达式的结果就是⼀个个闭包。闭包的⼀个强⼤之处是其可以通过传值或者引⽤的⽅式捕捉其封装作⽤域内的变ᰁ,前⾯的⽅括号就是⽤来定义捕捉模式以及变ᰁ,我们⼜将其称为lambda捕捉块。
-
lambda表达式的语法定义如下:
[capture] (parameters) mutable ->return-type {statement};
-
lambda必须使⽤尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包
-
含捕获列表和函数体;
3.左值、右值引⽤
- 左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束后就不再存在的临时对象。看能不能对表达式取地址,如果能,则为左值,否则为右值
右值:非引用返回的临时变量,运算表达式产生的结果,字面常量,将要被移动的对象、,T&&函数返回的值、std::move()的返回值
左值引用、右值引用: - 右值引用就是给右值取个名字。生命周期将与右值引用类型变量aa的生命周期一样,只要aa还活着,该右值临时变量将会一直存活下去。
- 引入右值引用的主要目的是实现移动语义。
- 左值引用只能绑定(关联、指向)左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。
- 常量左值引用,万能的引用类型,缺点是,只能读不能改。
语法:数据类型&& 变量名=右值
4.constexpr
- const关键字从功能上来说有双重语义:只读变量和修饰常量,为了解决const关键字的双重语义问题,保留了const表示“只读”的语义,而将“常量”的语义划分给了新添加的constexpr关键字。
5.统一的初始化
- 用大括号括起来的列表(统一的初始化列表)可以用于所有内置类型和用户自定义类型。
6.auto decltype
指示编译器在编译时推导auto声明的变量的数据类型。
禁止:
- auto声明的变量必须在定义时初始化,auto不能作为函数的形参类型。,auto不能直接声明数组。auto不能定义类的非静态成员变量。
- 使用:模板声明 lambada 冗长声明如迭代器
decltype - 希望从表达式中推断出要定义变量的类型,但却不想⽤表达式的值去初始化变量,希望从表达式中推断出要定义变量的类型,但却不想⽤表达式的值去初始化变量
7.智能指针‘
普通指针
智能指针
auto_ptr是c++98的标准,已经弃用了
unique_ptr:同时只有一个unique_ptr指向同一个对象,当这个unique_ptr被销毁时,指向的对象也随即被销毁。
- 含义: 模板参数AA表示要管理的指针是AA,p表示被要管理的指针是P,而p指向new出来的数据的地址
- 内部构造:
- 智能指针是类,内部有析构函数,重载了指针的常用操作,
- 照例禁用了拷贝赋值构造,防止对象之间释放指针,
- 禁止隐式转换,防止普通指针赋值给智能指针
- 类内部还有一个普通指针用来管理其管理的指针
- 不支持指针的运算,可以传引用不能传值(没有拷贝构造)
shared_ptr: shared_ptr使⽤引⽤计
数,每⼀个shared_ptr的拷⻉都指向相同的内存。每使⽤他⼀次,内部的引⽤计数加1,
每析构⼀次,内部的引⽤计数减1,减为0时,⾃动删除所指向的堆内存。shared_ptr内部的引⽤计数是线程安全的,但是对象的读取需要加锁
内部构造:
- 没有删除拷贝构造,需要对象共享,拷贝构造的时候引用技术加一
- 可以使用use_count来检查引用计数个数
循环引用 - shared_ptr的实现中我们知道了只有当引⽤计数减减之后等于0,析构时才会释放对象,可是双方指针都互相指向等着对方释放资源,就都无法释放,类似死锁
weak_Ptr: - 弱指针⽤于专⻔解决shared_ptr循环引⽤的问题,weak_ptr不会修改引⽤计数,即其存在与否并不影响对象的引⽤计数器。循环引⽤就是:两个对象互相使⽤⼀个shared_ptr成员变ᰁ指向对⽅。弱引⽤并不对对象的内存进⾏管理,在功能上类似于普通指针,然⽽⼀个⽐⼤的区别是,弱引⽤能检测到所管理的对象是否已经被释放,从⽽避免访问⾮法内存
使用牛客网牛客网面试对照
8.移动语义
-
如果一个对象中有堆区资源,需要编写拷贝构造函数和赋值函数,实现深拷贝。
-
深拷贝把对象中的堆区资源复制了一份,如果源对象(被拷贝的对象)是临时对象,拷贝完就没什么用了,这样会造成没有意义的资源申请和释放操作。如果能够直接使用源对象拥有的资源,可以节省资源申请和释放的时间。
实现移动语义要增加两个函数:移动构造函数和移动赋值函数。
移动构造函数的语法:
类名(类名&& 源对象)
移动赋值函数的语法:
类名& operator=(类名&& 源对象)
-
对于一个左值,会调用拷贝构造函数,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转义为右值,从而方便使用移动语义。它其实就是告诉编译器,虽然我是一个左值,但不要对我用拷贝构造函数,用移动构造函数吧。左值对象被转移资源后,不会立刻析构,只有在离开自己的作用域的时候才会析构,如果继续使用左值中的资源,可能会发生意想不到的错误。
-
如果没有提供移动构造/赋值函数,只提供了拷贝构造/赋值函数,编译器找不到移动构造/赋值函数就去寻找拷贝构造/赋值函数。
-
C++11中的所有容器都实现了移动语义,避免对含有资源的对象发生无谓的拷贝。
-
移动语义对于拥有资源(如内存、文件句柄)的对象有效,如果是基本类型,使用移动语义没有意义。
完美转发 -
所谓完美,即不仅能准确的转发参数的值,还能保证被转发参数的左、右值属性不变。能否实现完美转发,决定了该参数在传递过程使用的是拷贝语义还是移动语义。提供了模板函数std::forward(参数) ,用于转发参数,如果 参数是一个右值,转发之后仍是右值引用;如果参数是一个左值,转发之后仍是左值引用。
c++ STL
1.什么是STL(自我补充知识)
标准模板库(Standard Template Library,简称STL)简单说,就是一些常用数据结构和算法的模板的集合。
- 广义上讲,STL分为3类:Algorithm(算法)、Container(容器)和Iterator(迭代器),容器和算法通过迭代器可以进行无缝地连接。
- 详细的说,STL由6部分组成:容器(Container)、算法(Algorithm)、 迭代器(Iterator)、仿函数(Function object)、适配器(Adaptor)、空间配制器(Allocator)。
标准模板库STL主要由6大组成部分:
容器 - 是一种数据结构, 如list, vector, 和deques,以模板类的方法提供。为了访问容器中的数据,可以使用由容器类输出的迭代器。
算法 - 是用来操作容器中的数据的模板函数。例如,STL用sort()来对一 个vector中的数据进行排序,用find()来搜索一个list中的对象, 函数本身与他们操作的数据的结构和类型无关,因此他们可以用于从简单数组到高度复杂容器的任何数据结构上。
迭代器 - 提供了访问容器中对象的方法。例如,可以使用一对迭代器指定list或vector中的一定范围的对象。 迭代器就如同一个指针。事实上,C++ 的指针也是一种迭代器。 但是,迭代器也可以是那些定义了operator*()以及其他类似于指针的操作符方法的类对象;
仿函数 - 仿函数又称之为函数对象, 其实就是重载了操作符的struct,没有什么特别的地方。
适配器 - 简单的说就是一种接口类,专门用来修改现有类的接口,提供一中新的接口;或调用现有的函数来实现所需要的功能。主要包括3中适配器Container Adaptor、Iterator Adaptor、Function Adaptor。
空间配制器(Allocator) - 为STL提供空间配置的系统。其中主要工作包括两部分:
- (1)对象的创建与销毁;
- (2)内存的获取与释放。
2.请说说 STL 中常见的容器,并介绍一下实现原理
容器可以用于存放各种类型的数据(基本类型的变量,对象等)的数据结构,都是模板类,分为顺序容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下:
顺序容器
容器并非排序的,元素的插入位置同元素的值无关。包含vector、deque、list,具体实现原理如下:
vector头文件
- 动态数组。元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
deque头文件 - 双向队列。元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
list头文件
- 双向链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
关联式容器 - 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现。包含set、multiset、map、multimap,具体实现原理如下:
set/multiset头文件
- set 即集合。set中不允许相同元素,multiset中允许存在相同元素。
map/multimap头文件
- map与set的不同在于map中存放的元素有且仅有两个成员变,一个名为first,另一个名为second, map根据first值对元素从小到大排序,并可快速地根据first来检索元素。
注意:map同multimap的不同在于是否允许相同first值的元素。
容器适配器
封装了一些基本的容器,使之具备了新的函数功能,比如把deque封装一下变为一个具有stack功能的数据结构。这新得到的数据结构就叫适配器。包含stack,queue,priority_queue,具体实现原理如下:
stack 头文件
- 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项)。后进先出。
queue 头文件
- 队列。插入只可以在尾部进行,删除、检索和修改只允许从头部进行。先进先出。
priority_ueue 头文件
优先级队列。内部维持某种有序,然后确保优先级最高的元素总是位于头部。最高优先级元素总是第一个出列。
本文作者:Gal0721
本文链接:https://www.cnblogs.com/Gal0721/p/17728712.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步