口试
目录

- C++ 底子知识
- C和C++的区别是什么?
- 说说关键字static、const、extern的作用分别是什么?
- sizeof和strlen有什么区别 ?
- 指针和引用的区别有哪些?请详细阐明。
- 请表明指针数组、数组指针和函数指针的概念,并举例阐明。
- 描述一下C++的内存结构,栈区、堆区、全局/静态区、笔墨常量区和程序代码区分别存放什么内容?
- 堆和栈的区别是什么 ?
- malloc/free 和 new/delete 的区别是什么?
- 常见的内存错误有哪些?怎样避免和办理这些错误?
- 字节对齐的原则是什么?为什么必要字节对齐?
- 怎样举行"0"的比较判断?浮点数在计算机中是怎样存储的 ?
- 内联函数有什么优势?内联函数和宏定义的区别是什么?
- 函数的调用惯例有哪些?printf的变参是怎样实现的 ?
- 覆盖、重载、隐藏的区别是什么 ?
- C++有哪四种强制类型转换?它们的使用场景分别是什么?
- C/C++程序的编译过程是怎样的 ?
- 构造函数和析构函数可以是虚函数吗?为什么?
- 浅拷贝和深拷贝的区别是什么?在什么环境下必要使用深拷贝 ?
- 在跨平台举行指针通报时,应该用int类型照旧long类型?为什么 ?
- 请先容一下C++11的新特性,你在项目中用过哪些,是怎样应用的 ?
- 智能指针的实现原理是什么?C++11中有哪些智能指针?它们的使用场景分别是什么 ?
- 面向对象的特性有哪些?请详细论述多态的原理 ?
- 先容一下虚函数,虚函数是怎样实现多态的 ?
- 除了多态和继续,面向对象还有哪些特性 ?
- 一个对象赋值给另一个对象时,会发生什么?(涉及赋值构造函数的概念)
- 假如new了之后出了题目直接return,会导致什么题目?怎样办理 ?
- weak_ptr的使用场景是什么?在什么环境下会产生循环引用,怎样避免 ?
- 函数重载的机制是什么?重载是在编译期照旧运行期确定 ?
- vector的原理是什么?它是怎样扩容的 ?
- 请先容一下const引用和指针的区别 ?
- 右值引用的概念是什么?函数参数可以传右值引用吗 ?
- 怎样实现本身的堆栈?要求不能用STL容器,说说你的底层实现思路 ?
- vector、数组、map分别是基于什么底层结构实现的 ?
- 完美转发的概念是什么?去掉std::forward会怎样 ?
- 在C代码中引用C++代码偶然候会报错,为什么?怎样办理 ?
- 静态多态有哪些实现方式 ?
- 虚表是什么时间建立的?为什么要把析构函数设置成虚函数 ?
- map为什么用红黑树而不消AVL树 ?
- inline失效的场景有哪些 ?
- C++中struct和class有什么区别 ?
- 怎样防止一个头文件被多次include ?
- lambda表达式的理解,它可以捕获哪些类型 ?
- 友元friend的作用是什么 ?
- move函数的作用是什么 ?
- 模板类的作用是什么?模板和泛型的区别是什么 ?
- new可以重载吗?可以改写new函数吗 ?
- C++中的map和unordered_map的区别和使用场景分别是什么?它们是线程安全的吗 ?
- C++标准库里优先队列是怎么实现的 ?
- extern "C"的作用是什么 ?
- 数据结构与算法
- 数组和链表的区别和优缺点分别是什么 ?
- 快速排序、堆排序、冒泡排序的原理和实现过程是怎样的 ?
- 二分查找的原理和复杂度是多少 ?
- hash表的原理是什么?当数据很大,rehash的代价很高,怎么办 ?
- 二叉树的前序遍历、中序遍历、后序遍历的非递归实现方法是什么 ?
- 怎样反转链表 ?
- 怎样输出二叉树每一层最右边的节点 ?
- 对于千万级数组,怎样求最大的k个数 ?
- 当数据范围有限(如0到1000),且有许多重复数据,怎样按频率排序 ?
- 计算二叉树层高的方法有哪些 ?
- 给一个连续非空子数组,怎样找到它乘积最大的子数组 ?
- 常见的排序算法中,哪些是稳固的,哪些是不稳固的 ?
- 树的深度和高度的概念是什么?怎样计算 ?
- 布隆过滤器的原理和应用场景是什么 ?
- 哈希冲突的办理方法有哪些(如开放定址法、链地址法、再哈希法、建立公共溢出区)?请详细描述这些方法的过程和思路,以及它们的实用场景和底层数据结构 ?
- 链表和数组的底层结构设计、关联、区别和应用场景分别是什么 ?
- 死锁的概念是什么?历程调理算法有哪些?怎样办理死锁题目 ?
- GDB/GCC/G++
- 什么是GDB?它用于做什么?常用的GDB命令有哪些 ?
- 怎样在GDB中设置断点?怎样查看变量的值 ?
- 怎样使用GDB举行程序调试时定位内存走漏题目 ?
- GCC和G++都可以编译C++代码吗?假如可以,它们有什么不同之处 ?
- GCC和G++的常用编译选项有哪些 ?
- 怎样在GCC/G++中指定特定版本的标准(如C++11或C++14)举行编译 ?
- 当程序出现题目时,怎样使用GDB举行调试?请描述详细的步调和方法 ?
- 怎样分析C++的core文件 ?
- 在Linux下,除了GDB,还有哪些常用的调试工具和方法 ?
- 设计模式
- 什么是设计模式?为什么要使用设计模式 ?
- 常见的设计模式有哪些分类?请简要描述每个分类中的几个详细设计模式 ?
- 表明单例模式的概念和用途,以及怎样实现单例模式 ?
- 什么是工厂方法模式和抽象工厂模式?它们的区别和应用场景是什么 ?
- 表明装饰器模式和适配器模式的概念,并举例阐明它们的应用场景 ?
- 什么是观察者模式?怎样实现观察者模式 ?
- 表明策略模式和状态模式之间的区别,以及在什么环境下使用它们 ?
- 什么是迭代器模式和组合模式 ?
- 表明桥接模式和适配器模式之间的区别,并阐明在哪种环境下选择使用桥接或适配器 ?
- 先容责任链模式和命令模式的概念,并阐明它们怎样解耦发送者和接收者 ?
- 在你的项目中,有没有使用过设计模式?请举例阐明是怎样应用的,带来了哪些长处 ?
- 操纵体系
- 线程和历程的区别是什么?它们的应用场景分别是什么 ?
- 多线程中有哪些常见的锁(如读写锁、互斥锁)?它们的作用和使用场景是什么 ?
- 内存池的概念是什么?它有什么作用 ?
- 内存管理的方式有哪些?在C++中怎样举行有用的内存管理 ?
- 假如频仍举行内存的分配开释会有什么题目?假如频仍分配开释的内存很大(>128k),怎么处置处罚 ?
- 假造内存的概念是什么?堆栈溢出是怎么发生的,如那边理 ?
- 分段和分页的区别是什么 ?
- 历程间通讯的原理和方式有哪些 ?
- fork()函数的作用是什么?它的返回值是怎么实现的 ?
- 读时共享写时拷贝的原理是什么 ?
- 互斥锁和条件变量怎样共同使用 ?
- 假如非堆内存一直在增长,大概是哪个区域的内存出了题目 ?
- 堆和栈的区别是什么?在什么环境下数据会被放到堆里 ?
- 用户级线程和内核级线程的区别是什么 ?
- 线程池的概念是什么?为什么要用线程池?线程池中的线程是怎样运作的 ?
- 生产者消费者模子的原理是什么?在实现过程中,信号量是怎样使用的 ?
- 当队列空时,消费者和生产者会发生什么 ?
- 线程池请求队列一般用什么数据结构实现,为什么 ?
- 线程同步的方法有哪些?怎样实现线程间的共享和互斥同步 ?
- 信号量和自旋锁的区别是什么 ?
- 在Linux体系中,查看磁盘、CPU占用、内存占用的命令分别是什么 ?
- 请描述Linux假造地址空间结构 ?
- 怎样使用top命令排查高占有率历程?top命令中的占用率是怎样计算的 ?
- 谈谈历程创建后在Linux中的内存分布 ?
- 在Linux体系下,使用for循环一直举行new操纵,会发生heap - overflow吗?为什么?假如是在Java中呢 ?
- 死锁的概念是什么?历程调理算法有哪些?怎样办理死锁题目 ?
- 请讲讲历程管理的相干知识 ?
- 项目履历
- 请详细先容一下你简历中提到的[详细项目名称],你在项目中担任什么角色,重要负责哪些工作 ?
- 在项目中碰到了哪些技术困难,你是怎样办理的 ?
- 项目中用到了哪些技术栈和工具,为什么选择这些 ?
- 请描述项目的架构设计,以及这样设计的长处和考虑因素 ?
- 在项目开发过程中,怎样举行团队协作和沟通 ?
- 项目是否有性能优化的需求?你采取了哪些优化措施,结果怎样 ?
- 项目上线后,是否出现过题目?你是怎样举行排查和办理的 ?
- 从项目中学到了什么?对今后的工作有什么帮助 ?
- 假如重新做这个项目,你会在哪些方面举行改进 ?
- 其他
- 你对加班的看法是什么 ?
- 未来的职业规划是怎样的 ?
- 你有什么题目想要问我的吗 ?
- 你寻常是怎样学习新技术的,有没有什么学习方法可以分享 ?
- 请举例阐明你是怎样办理一个复杂题目的,体现你的题目办理本领 ?
- 在团队中,你更倾向于担任领导者照旧跟随者的角色,为什么 ?
- 你怎样包管本身写的代码的质量和可维护性 ?
- 有没有关注过行业内的新技术发展趋势,近来相识到哪些新技术 ?
- 假如团队成员之间出现了技术分歧,你会如那边理 ?
1. C++ 底子知识
C和C++的区别是什么?
- 设计范式:C 是面向过程的编程语言,重要关注功能的实现和步调的执行;C++ 支持面向对象编程,具有类、对象、继续、多态等特性,也支持泛型编程。
- 语法特性:C++ 有更丰富的语法,如引用、函数重载、运算符重载、模板等;C++ 引入了命名空间,避免命名冲突。
- 内存管理:C 使用 malloc、free 举举措态内存分配和开释;C++ 除了兼容这些,还提供了 new 和 delete 运算符,并且支持构造函数和析构函数来主动管理对象的生命周期。
- 标准库:C 有标准 C 库;C++ 有标准模板库(STL),包罗各种容器(如 vector、map)和算法(如 sort),大大提高了开发服从。
说说关键字 static、const、extern 的作用分别是什么?
- static
- 局部变量:修饰局部变量时,该变量只在第一次进入函数时初始化,之后再次调用函数时,该变量保留上一次调用结束时的值。
- 全局变量:修饰全局变量时,该变量的作用域仅限于当前文件,其他文件无法通过 extern 引用。
- 函数:修饰函数时,该函数的作用域仅限于当前文件,其他文件无法调用。
- 类成员:修饰类的成员变量时,该变量被全部类的对象共享;修饰类的成员函数时,该函数不依靠于详细的对象,可以直接通过类名调用。
- const
- 常量:用于定义常量,一旦初始化后,其值不能被修改。
- 指针:可以修饰指针本身或指针所指向的内容,如 const int* p 表现指针所指向的内容不能被修改,int* const p 表现指针本身不能被修改。
- 函数参数:用于包管函数内部不会修改传入的参数。
- 类成员函数:修饰类的成员函数时,表现该函数不会修改类的成员变量。
- extern
- 变量和函数声明:用于声明在其他文件中定义的全局变量和函数,使得当前文件可以使用这些变量和函数。
sizeof 和 strlen 有什么区别 ?
- 本质:sizeof 是一个运算符,用于计算数据类型或变量所占用的内存字节数;strlen 是一个库函数,用于计算字符串的实际长度(不包括字符串结束符 '\0')。
- 参数:sizeof 可以接受数据类型、变量名等作为参数;strlen 的参数必须是字符指针,指向以 '\0' 结尾的字符串。
- 计算结果:sizeof 在编译时就确定告终果;strlen 在运行时遍历字符串,直到碰到 '\0' 为止,计算出字符串的长度。
指针和引用的区别有哪些?请详细阐明。
- 定义和初始化:指针是一个变量,存储的是另一个变量的地址,可以在定义时不初始化,之后再赋值;引用是一个变量的别名,必须在定义时初始化,且一旦初始化后,就不能再引用其他变量。
- 内存占用:指针本身必要占用一定的内存空间,用于存储地址;引用不占用额外的内存空间,它和所引用的变量共享同一块内存。
- 空值:指针可以指向空值(nullptr);引用不能引用空值,必须有一个有用的对象与之关联。
- 使用方式:指针使用 * 运算符来访问所指向的对象;引用可以直接使用,就像使用被引用的对象一样。
- 多级:可以有多级指针(如 int**);但没有多级引用。
请表明指针数组、数组指针和函数指针的概念,并举例阐明。
- 指针数组:是一个数组,数组的每个元素都是指针。比方:int* arr[5]; 定义了一个包罗 5 个 int 指针的数组。
- 数组指针:是一个指针,指向一个数组。比方:int (*p)[5]; 定义了一个指向包罗 5 个 int 元素的数组的指针。
- 函数指针:是一个指针,指向一个函数。比方:int (*func)(int, int); 定义了一个指向返回值为 int,接受两个 int 类型参数的函数的指针。可以用它来调用函数,如:
- int add(int a, int b) { return a + b; }
- func = add;
- int result = func(1, 2);
复制代码 描述一下 C++ 的内存结构,栈区、堆区、全局/静态区、笔墨常量区和程序代码区分别存放什么内容?
- 栈区:由编译器主动分配和开释,存放函数的参数值、局部变量等。函数调用时,栈会为这些变量分配内存;函数结束时,这些内存会被主动开释。
- 堆区:由程序员手动分配和开释(使用 new 和 delete 或 malloc 和 free),用于存储动态分配的对象。假如程序员没有精确开释,会导致内存走漏。
- 全局/静态区:存放全局变量和静态变量。全局变量和静态变量在程序启动时分配内存,在程序结束时开释内存。
- 笔墨常量区:存放常量字符串,如 "hello world" 这样的字符串常量就存放在这里。该区域的内容在程序运行期间不可修改。
- 程序代码区:存放程序的二进制代码,也就是 CPU 执行的机器指令。
堆和栈的区别是什么 ?
- 内存分配方式:栈由编译器主动分配和开释;堆必要程序员手动分配和开释。
- 内存空间大小:栈的空间通常比较小,一般由操纵体系限定;堆的空间相对较大,理论上可以使用体系剩余的全部可用内存。
- 数据存储顺序:栈是后进先出(LIFO)的数据结构;堆没有固定的存储顺序。
- 内存碎片:栈不会产生内存碎片;堆在频仍的分配和开释操纵后,大概会产生内存碎片,导致内存利用率低落。
- 分配服从:栈的分配和开释速度快,因为只必要移动栈指针;堆的分配和开释必要更复杂的内存管理算法,速度相对较慢。
malloc/free 和 new/delete 的区别是什么?
- 语法:malloc 和 free 是 C 语言的标准库函数,new 和 delete 是 C++ 的运算符。
- 对象构造和析构:new 在分配内存后会主动调用对象的构造函数举行初始化;delete 在开释内存前会主动调用对象的析构函数。而 malloc 和 free 只是简朴地分配和开释内存,不会调用构造函数和析构函数。
- 返回值类型:malloc 的返回值是 void*,必要举行显式的类型转换;new 直接返回指定类型的指针。
- 非常处置处罚:new 在内存分配失败时会抛出 std::bad_alloc 非常;malloc 在内存分配失败时返回 NULL。
常见的内存错误有哪些?怎样避免和办理这些错误?
- 内存走漏:程序在动态分配内存后,没有精确开释,导致这部分内存无法再被使用。避免方法是在使用 new 或 malloc 分配内存后,确保使用 delete 或 free 开释内存;也可以使用智能指针来主动管理内存。
- 悬空指针:指针所指向的内存已经被开释,但指针仍旧存在。避免方法是在开释内存后,将指针置为 nullptr。
- 越界访问:访问数组或指针时超出了其正当范围。避免方法是在访问数组或指针时,举行边界检查。
- 重复开释:对同一块内存举行多次开释。避免方法是确保只开释一次内存。
字节对齐的原则是什么?为什么必要字节对齐?
- 原则
- 数据成员对齐规则:结构体或类的第一个成员放在偏移量为 0 的位置;之后的每个成员的起始偏移量必须是该成员大小的整数倍。
- 结构体团体对齐规则:结构体的总大小必须是其最大成员大小的整数倍。
- 原因
- 提高访问服从:处置处罚器访问对齐的数据时,速度更快。因为处置处罚器通常按字(如 4 字节或 8 字节)来访问内存,假如数据没有对齐,大概必要多次访问才能获取完整的数据。
- 硬件限定:某些硬件平台要求数据必须对齐,否则会产生硬件非常。
怎样举行"0"的比较判断?浮点数在计算机中是怎样存储的 ?
- 整数和指针:对于整数和指针,可以直接使用 == 运算符与 0 举行比较,如 if (num == 0) 或 if (ptr == nullptr)。
- 浮点数:由于浮点数在计算机中是以二进制的形式近似存储的,存在精度误差,不能直接使用 == 运算符与 0 举行比较。通常的做法是定义一个很小的误差范围(如 1e-6),然后判断浮点数的绝对值是否小于这个误差范围,如 if (fabs(num) < 1e-6)。
- 浮点数存储:浮点数在计算机中通常采取 IEEE 754 标准举行存储,分为符号位、指数位和尾数位三部分。符号位表现正负,指数位表现阶码,尾数位表现小数部分。
内联函数有什么优势?内联函数和宏定义的区别是什么?
- 内联函数优势
- 提高执行服从:内联函数在编译时会将函数体的代码直接插入到调用处,避免了函数调用的开销(如保存和规复寄存器、跳转等)。
- 类型安全:内联函数是真正的函数,会举行类型检查,而宏定义只是简朴的文本更换,不举行类型检查。
- 区别
- 处置处罚阶段:内联函数在编译阶段处置处罚;宏定义在预处置处罚阶段处置处罚。
- 类型检查:内联函数有类型检查;宏定义没有类型检查。
- 调试:内联函数可以举行调试;宏定义无法举行调试。
- 作用域:内联函数有作用域限定;宏定义没有作用域限定,只要在宏定义之后的代码都可以使用。
函数的调用惯例有哪些?printf 的变参是怎样实现的 ?
- 调用惯例
- __cdecl:C/C++ 默认的调用惯例,参数从右到左入栈,由调用者负责清理栈。
- __stdcall:参数从右到左入栈,由被调用者负责清理栈,常用于 Windows API 函数。
- __fastcall:部分参数通过寄存器通报,其余参数从右到左入栈,由被调用者负责清理栈。
- printf 变参实现:printf 函数使用了可变参数列表来实现变参功能。在 C 语言中,通过 <stdarg.h> 头文件中的 va_list、va_start、va_arg 和 va_end 宏来实现。根本步调如下:
- 定义一个 va_list 类型的变量,用于存储可变参数列表。
- 使用 va_start 宏初始化 va_list 变量,使其指向第一个可变参数。
- 使用 va_arg 宏依次获取可变参数的值。
- 使用 va_end 宏结束可变参数的处置处罚。
覆盖、重载、隐藏的区别是什么 ?
- 覆盖(重写):发生在基类和派生类之间,当派生类中定义了一个与基类中虚函数原型完全相同的函数时,就会覆盖基类的虚函数。覆盖的函数必须是虚函数,并且函数名、参数列表和返回值类型都要相同(协变返回类型除外)。
- 重载:发生在同一个作用域内,函数名相同,但参数列表不同(参数个数、类型或顺序不同)。重载函数的返回值类型可以不同,但不能仅靠返回值类型来区分重载函数。
- 隐藏:也是发生在基类和派生类之间,当派生类中定义了一个与基类中同名的函数(无论是否为虚函数),基类的同名函数就会被隐藏。假如派生类要调用基类的被隐藏函数,必要使用作用域分析运算符 ::。
C++ 有哪四种强制类型转换?它们的使用场景分别是什么?
- static_cast
- 用于根本数据类型之间的转换,如 int 转 double。
- 用于类层次结构中基类和派生类指针或引用的转换,但不举行运行时类型检查,必要程序员本身确保转换的安全性。
- 用于把 void* 指针转换为其他类型的指针。
- dynamic_cast
- 重要用于类层次结构中基类和派生类指针或引用的转换,举行运行时类型检查。假如转换失败,对于指针返回 nullptr,对于引用抛出 std::bad_cast 非常。常用于实现多态。
- const_cast
- 用于去除或添加 const 或 volatile 修饰符。比方,将一个 const 指针转换为非 const 指针,但必要注意这种转换大概会导致未定义行为,因为修改本来 const 的对象是不安全的。
- reinterpret_cast
- 用于任意指针或引用之间的转换,也可以将指针转换为整数类型,或者将整数类型转换为指针。这种转换是最不安全的,因为它只是简朴地重新表明内存中的二进制数据,不考虑数据的实际类型。
C/C++ 程序的编译过程是怎样的 ?
- 预处置处罚:处置处罚以 # 开头的预处置处罚指令,如 #include、#define 等。将头文件内容插入到源文件中,举行宏更换,删除表明等。使用 gcc -E 命令可以只举行预处置处罚。
- 编译:将预处置处罚后的源文件翻译成汇编代码。编译器会举行语法分析、语义分析、代码优化等操纵。使用 gcc -S 命令可以只举行编译,天生汇编文件。
- 汇编:将汇编代码翻译成机器代码,天生目标文件(.o 文件)。使用 gcc -c 命令可以只举行汇编。
- 链接:将多个目标文件和库文件链接成一个可执行文件。链接过程会办理符号引用题目,将不同文件中的符号(如函数、变量)关联起来。使用 gcc 命令可以完成整个编译和链接过程。
构造函数和析构函数可以是虚函数吗?为什么?
- 构造函数:不能是虚函数。因为虚函数的调用必要通过虚函数表来实现,而虚函数表是在对象创建后才初始化的。在构造函数执行时,对象还没有完全创建好,虚函数表还未初始化,所以无法通过虚函数表来调用构造函数。
- 析构函数:可以是虚函数,而且在基类中通常将析构函数声明为虚函数。当通过基类指针删除派生类对象时,假如基类的析构函数不是虚函数,只会调用基类的析构函数,而不会调用派生类的析构函数,导致派生类对象的资源无法精确开释,产生内存走漏。将基类的析构函数声明为虚函数后,在删除基类指针时,会先调用派生类的析构函数,再调用基类的析构函数,确保对象的资源被精确开释。
浅拷贝和深拷贝的区别是什么?在什么环境下必要使用深拷贝 ?
- 浅拷贝:只是简朴地复制对象的成员变量的值,假如成员变量是指针,只复制指针的值,而不复制指针所指向的内容。多个对象大概会共享同一块内存,当此中一个对象开释这块内存时,其他对象的指针就会变成悬空指针。
- 深拷贝:除了复制对象的成员变量的值,还会为指针成员变量分配新的内存,并将指针所指向的内容复制到新的内存中。这样每个对象都有本身独立的内存空间,不会相互影响。
- 使用场景:当类的成员变量包罗指针,并且指针指向动态分配的内存时,必要使用深拷贝。否则,在对象赋值或拷贝构造时,会出现浅拷贝的题目,导致内存走漏或悬空指针。
在跨平台举行指针通报时,应该用 int 类型照旧 long 类型?为什么 ?
- 都不发起直接使用 int 或 long 类型来通报指针。因为不同平台上 int 和 long 的长度大概不同,而指针的长度也大概不同。比方,在 32 位体系上,int、long 和指针通常都是 32 位;但在 64 位体系上,int 通常是 32 位,long 大概是 32 位或 64 位,指针通常是 64 位。
- 应该使用 std::intptr_t 或 std::uintptr_t 类型,它们是 C++ 标准库中定义的整数类型,其长度与指针类型相同,包管了在不同平台上可以安全地存储和通报指针。
请先容一下 C++11 的新特性,你在项目中用过哪些,是怎样应用的 ?
- 智能指针:std::unique_ptr、std::shared_ptr 和 std::weak_ptr 用于主动管理动态分配的内存,避免内存走漏。比方,在一个必要动态分配对象的类中,可以使用 std::unique_ptr 来管理对象的生命周期,当对象不再使用时,会主动开释内存。
- Lambda 表达式:用于创建匿名函数,使代码更加简洁。比方,在使用 std::sort 函数对容器举行排序时,可以使用 Lambda 表达式来定义排序规则:
- #include <algorithm>
- #include <vector>
- #include <iostream>
- int main() {
- std::vector<int> v = {3, 1, 4, 1, 5, 9};
- std::sort(v.begin(), v.end(), [](int a, int b) { return a < b; });
- for (int num : v) {
- std::cout << num << " ";
- }
- std::cout << std::endl;
- return 0;
- }
复制代码
- 右值引用和移动语义:右值引用用于绑定到临时对象,移动语义可以避免不必要的拷贝操纵,提高性能。比方,在容器的插入操纵中,假如使用移动语义,可以直接将临时对象的资源转移到容器中,而不必要举行深拷贝。
- 范围 for 循环:简化了对容器和数组的遍历操纵。比方:
- #include <vector>
- #include <iostream>
- int main() {
- std::vector<int> v = {1, 2, 3, 4, 5};
- for (int num : v) {
- std::cout << num << " ";
- }
- std::cout << std::endl;
- return 0;
- }
复制代码
- 主动类型推导(auto):编译器可以根据初始化表达式主动推导变量的类型,使代码更加简洁。比方:
- auto num = 10; // 编译器自动推导 num 为 int 类型
复制代码
- nullptr:用于表现空指针,比 NULL 更安全,避免了 NULL 在某些环境下大概出现的二义性。
智能指针的实现原理是什么?C++11 中有哪些智能指针?它们的使用场景分别是什么 ?
- 实现原理:智能指针是一个类模板,它封装了一个原始指针,并在其析构函数中主动开释所指向的内存。通过重载 * 和 -> 运算符,使得智能指针可以像平凡指针一样使用。
- C++11 中的智能指针
- std::unique_ptr:独占式智能指针,同一时间只能有一个 std::unique_ptr 指向同一个对象。当 std::unique_ptr 被销毁时,它所指向的对象也会被销毁。实用于管理独占资源的场景,如动态分配的数组。
- std::shared_ptr:共享式智能指针,多个 std::shared_ptr 可以指向同一个对象。使用引用计数来管理对象的生命周期,当引用计数为 0 时,对象被销毁。实用于多个对象共享同一个资源的场景。
- std::weak_ptr:弱引用智能指针,它不拥有对象的全部权,只是对 std::shared_ptr 所指向的对象的一种弱引用。重要用于办理 std::shared_ptr 大概出现的循环引用题目。
面向对象的特性有哪些?请详细论述多态的原理 ?
- 面向对象的特性
- 封装:将数据和操纵数据的函数封装在一起,形成一个类。通过访问控制符(如 private、protected、public)来控制对类成员的访问,提高了数据的安全性和可维护性。
- 继续:允许一个类(派生类)继续另一个类(基类)的属性和方法,从而实现代码的复用和扩展。派生类可以添加本身的成员,也可以重写基类的方法。
- 多态:允许不同的对象对同一消息做出不同的响应。多态分为静态多态(编译时多态)和动态多态(运行时多态)。
- 多态的原理(动态多态):动态多态通过虚函数来实现。当一个类中包罗虚函数时,编译器会为该类创建一个虚函数表(vtable),虚函数表中存储了该类全部虚函数的地址。每个包罗虚函数的对象都会有一个指向虚函数表的指针(vptr)。当通过基类指针或引用调用虚函数时,会根据对象的实际类型,通过 vptr 找到对应的虚函数表,然后从虚函数表中找到要调用的虚函数的地址,从而实现动态绑定。
先容一下虚函数,虚函数是怎样实现多态的 ?
- 虚函数:在基类中使用 virtual 关键字声明的函数。派生类可以重写(覆盖)基类的虚函数,以实现不同的行为。
- 实现多态的原理:如上述动态多态原理所述,当基类指针或引用指向派生类对象时,调用虚函数时会根据对象的实际类型,通过虚函数表来确定要调用的函数。比方:
- #include <iostream>
- class Base {
- public:
- virtual void print() {
- std::cout << "Base::print()" << std::endl;
- }
- };
- class Derived : public Base {
- public:
- void print() override {
- std::cout << "Derived::print()" << std::endl;
- }
- };
- int main() {
- Base* basePtr = new Derived();
- basePtr->print(); // 调用 Derived::print()
- delete basePtr;
- return 0;
- }
复制代码 在这个例子中,Base 类的 print 函数是虚函数,Derived 类重写了该函数。当通过基类指针 basePtr 调用 print 函数时,会根据 basePtr 实际指向的对象类型(Derived 类对象),从虚函数表中找到 Derived::print 函数的地址并调用,从而实现了多态。
除了多态和继续,面向对象还有哪些特性 ?
除了多态和继续,面向对象还有封装和抽象两个重要特性:
- 封装:是将数据(属性)和操纵数据的方法(行为)捆绑在一起,并对外部隐藏内部实现细节的机制。通过使用访问修饰符(如 private、protected、public),可以控制类的成员(变量和函数)的访问权限,提高了代码的安全性和可维护性。比方,一个类可以将其内部的数据成员声明为 private,并提供 public 的成员函数来访问和修改这些数据,这样可以防止外部代码直接访问和修改数据,从而避免数据被意外修改。
- 抽象:是指从详细的事物中提取出共性和本质特性,忽略非本质的细节,形成抽象的概念或模子。在面向对象编程中,抽象通常通过抽象类和接口来实现。抽象类是一种不能被实例化的类,它包罗至少一个纯虚函数,用于定义一组接口规范,派生类必须实现这些纯虚函数。接口则是一种特殊的抽象类,它只包罗纯虚函数,没有数据成员和平凡成员函数。抽象可以帮助我们更好地理解和设计体系,提高代码的可扩展性和可维护性。
一个对象赋值给另一个对象时,会发生什么?(涉及赋值构造函数的概念)
当一个对象赋值给另一个对象时,会调用赋值运算符重载函数(也称为赋值构造函数)。赋值运算符重载函数用于将一个对象的状态复制到另一个对象。其执行过程和详细行为取决于赋值运算符重载函数的实现,一般来说大概会有以下环境:
1. 默认赋值运算符
假如类没有显式定义赋值运算符重载函数,编译器会为类天生一个默认的赋值运算符。默认赋值运算符会执行浅拷贝,即逐个复制对象的每个成员变量。对于根本数据类型的成员变量,会直接复制其值;对于指针类型的成员变量,只会复制指针的值(即地址),而不会复制指针所指向的对象,这大概会导致多个对象共享同一块内存,从而引发悬空指针和内存走漏等题目。
示例代码:
- #include <iostream>
- class MyClass {
- public:
- int* data;
- // 默认赋值运算符(编译器自动生成)
- };
- int main() {
- MyClass obj1;
- obj1.data = new int(10);
- MyClass obj2;
- obj2 = obj1; // 调用默认赋值运算符
- std::cout << *obj2.data << std::endl;
- delete obj1.data;
- // obj2.data 现在是悬空指针,因为它和 obj1.data 指向同一块内存
- // delete obj2.data; // 错误:重复释放内存
- return 0;
- }
复制代码 2. 显式定义赋值运算符
为了避免浅拷贝带来的题目,通常必要显式定义赋值运算符重载函数,实现深拷贝。深拷贝会为指针类型的成员变量分配新的内存,并将原对象指针所指向的内容复制到新的内存中。
示例代码:
- #include <iostream>
- class MyClass {
- public:
- int* data;
- MyClass() : data(nullptr) {}
- // 显式定义赋值运算符
- MyClass& operator=(const MyClass& other) {
- if (this != &other) {
- if (data) {
- delete data; // 释放原有的内存
- }
- if (other.data) {
- data = new int(*other.data); // 深拷贝
- } else {
- data = nullptr;
- }
- }
- return *this;
- }
- ~MyClass() {
- delete data;
- }
- };
- int main() {
- MyClass obj1;
- obj1.data = new int(10);
- MyClass obj2;
- obj2 = obj1; // 调用显式定义的赋值运算符
- std::cout << *obj2.data << std::endl;
- delete obj1.data;
- // obj2.data 指向不同的内存,不会成为悬空指针
- delete obj2.data;
- return 0;
- }
复制代码 假如 new 了之后出了题目直接 return,会导致什么题目?怎样办理 ?
假如在使用 new 运算符动态分配内存后,程序在开释该内存之前因为某些原因(如非常或直接 return)而提前退出,会导致内存走漏。这是因为动态分配的内存没有被精确开释,操纵体系无法回收这部分内存,随着程序的运行,内存走漏会渐渐积聚,终极大概导致体系内存耗尽。
示例代码:
- #include <iostream>
- void func() {
- int* ptr = new int(10);
- // 模拟出现问题
- if (/* 某个条件 */ true) {
- return; // 直接返回,没有释放 ptr 指向的内存
- }
- delete ptr;
- }
- int main() {
- func();
- return 0;
- }
复制代码 办理方法
- 使用智能指针:智能指针是一种 RAII(资源获取即初始化)技术的实现,它会在对象生命周期结束时主动开释所管理的资源。在 C++ 中,std::unique_ptr、std::shared_ptr 和 std::weak_ptr 是常用的智能指针。使用智能指针可以避免手动管理内存带来的题目。
示例代码:
- #include <iostream>
- #include <memory>
- void func() {
- std::unique_ptr<int> ptr = std::make_unique<int>(10);
- // 模拟出现问题
- if (/* 某个条件 */ true) {
- return; // ptr 离开作用域时会自动释放内存
- }
- }
- int main() {
- func();
- return 0;
- }
复制代码
- 使用 try-catch 块:在使用 new 分配内存后,使用 try-catch 块来捕获大概出现的非常,并在非常处置处罚代码中开释内存。
示例代码:
- #include <iostream>
- void func() {
- int* ptr = nullptr;
- try {
- ptr = new int(10);
- // 模拟出现问题
- if (/* 某个条件 */ true) {
- throw std::runtime_error("Something went wrong");
- }
- delete ptr;
- } catch (const std::exception& e) {
- if (ptr) {
- delete ptr;
- }
- std::cerr << "Exception caught: " << e.what() << std::endl;
- }
- }
- int main() {
- func();
- return 0;
- }
复制代码 weak_ptr 的使用场景是什么?在什么环境下会产生循环引用,怎样避免 ?
weak_ptr 的使用场景
- 打破循环引用:当两个或多个对象通过 std::shared_ptr 相互引用时,会形成循环引用,导致引用计数永世不会变为 0,从而造成内存走漏。std::weak_ptr 是一种弱引用,它不会增加引用计数,因此可以用于打破循环引用。
- 观察对象的生命周期:std::weak_ptr 可以用于观察 std::shared_ptr 所管理的对象的生命周期。可以通过 std::weak_ptr 的 lock() 方法获取一个 std::shared_ptr,假如对象还存在,则获取成功;假如对象已经被销毁,则返回一个空的 std::shared_ptr。
示例代码:
- #include <iostream>
- #include <memory>
- class B;
- class A {
- public:
- std::shared_ptr<B> b_ptr;
- ~A() {
- std::cout << "A destroyed" << std::endl;
- }
- };
- class B {
- public:
- std::weak_ptr<A> a_ptr; // 使用 std::weak_ptr 打破循环引用
- ~B() {
- std::cout << "B destroyed" << std::endl;
- }
- };
- int main() {
- std::shared_ptr<A> a = std::make_shared<A>();
- std::shared_ptr<B> b = std::make_shared<B>();
- a->b_ptr = b;
- b->a_ptr = a;
- return 0;
- }
复制代码 循环引用的产生环境
当两个或多个对象通过 std::shared_ptr 相互引用时,就会产生循环引用。比方,对象 A 持有一个 std::shared_ptr 指向对象 B,而对象 B 也持有一个 std::shared_ptr 指向对象 A,这样它们的引用计数永世不会变为 0,即使没有其他外部引用指向它们,也无法开释内存。
避免循环引用的方法
- 使用 std::weak_ptr:如上述示例所示,将此中一个 std::shared_ptr 更换为 std::weak_ptr,可以打破循环引用,因为 std::weak_ptr 不会增加引用计数。当其他 std::shared_ptr 都开释后,对象的引用计数变为 0,对象会被正常销毁。
函数重载的机制是什么?重载是在编译期照旧运行期确定 ?
函数重载的机制
函数重载是指在同一个作用域内,可以有多个同名函数,但它们的参数列表(参数个数、类型或顺序)不同。编译器在编译时会根据函数调用时提供的参数列表来确定要调用的详细函数。编译器通过对函数名和参数列表举行编码(称为名称修饰或名称改编),天生唯一的内部名称,以便在链接时能够精确地识别和调用不同的重载函数。
示例代码:
- #include <iostream>
- // 函数重载示例
- void print(int num) {
- std::cout << "Printing int: " << num << std::endl;
- }
- void print(double num) {
- std::cout << "Printing double: " << num << std::endl;
- }
- int main() {
- print(10); // 调用 print(int)
- print(3.14); // 调用 print(double)
- return 0;
- }
复制代码 重载的确定时期
函数重载是在编译期确定的。编译器在编译时会根据函数调用的实参类型和数目,与全部重载函数的形参列表举行匹配,选择最匹配的函数举行调用。假如找不到匹配的函数,或者有多个函数都匹配但无法确定最佳匹配,则会产生编译错误。
vector 的原理是什么?它是怎样扩容的 ?
vector 的原理
std::vector 是 C++ 标准模板库(STL)中的一个动态数组容器,它可以存储任意类型的元素,并且可以根据必要动态调整大小。vector 的底层实现是基于数组的,它使用连续的内存空间来存储元素,因此可以通过下标快速访问元素。
扩容机制
当向 vector 中添加元素时,假如当前的容量(capacity)不足以容纳新元素,vector 会举行扩容操纵。扩容的根本步调如下:
- 分配新的内存:vector 会分配一块更大的连续内存空间,通常是原容量的两倍(不同编译器实现大概略有不同)。
- 复制元素:将原内存中的元素逐个复制到新的内存空间中。
- 开释原内存:开释原内存空间。
- 更新指针和容量:更新 vector 的内部指针,使其指向新的内存空间,并更新容量信息。
示例代码:
- #include <iostream>
- #include <vector>
- int main() {
- std::vector<int> vec;
- std::cout << "Initial capacity: "
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |