C++ 面试题
本文《C++ 面试题》由小宇宙从原站整理搬运,详细讲解相关技术要点。
大家好,我是小宇宙。 说实话,C++ 的面试是所有语言里我觉得最难准备的,因为它考的东西实在太杂了。你以为把语法背熟就够了,结果面试官上来就问你虚函数表的内存布局、智能指针的引用计数实现,或者直接让你手写一个 。 这篇文章把 C++ 面试中高频出现的问题都整理下来了,基本上覆盖了从基础语法到内存管理、从面向对象到 STL、再到 C++11 新特性这一整条线。内容不算少,但我尽量把每个知识点都讲清楚,不只是给你一个结论,而是把背后的原理也说明白,因为很多面试官就是喜欢追着问"为什么"。 这里面有几块内容特别值得重点关注: 指针和内存管理 :这是 C++ 的核心,悬空指针、野指针、malloc/new 的区别、内存对齐,面试几乎必问。 虚函数和多态 :虚函数表的结构、纯虚函数、为什么析构函数要写成虚函数,这些是面向对象部分的重点。 STL 容器 :各种容器的底层实现、迭代器失效的场景, 和 的区别,别只会用,要知道原理。 智能指针 : 、 、 的使用和循环引用问题,现在几乎是标配考点。 C++11 新特性 :右值引用、移动语义、lambda 表达式、 推导,这些在大厂面试里问得越来越多。 如果你是刚开始准备,建议先把 C++ 基础和面向对象的部分搞透,再去看 STL 和新特性。遇到不理解的地方多结合代码去看,光靠背是很难真正掌握的。 C++基础 什么是指针?指针的大小及用法? 指针: 指向另外一种类型的复合类型。 指针的大小: 在 64 位计算机中,指针占 8 个字节空间。 指针的用法: 指向普通对象的指针: 指向常量对象的指针:常量指针 指向函数的指针:函数指针 指向对象成员的指针,包括指向对象成员函数的指针和指向对象成员变量的指针。 特别注意:定义指向成员函数的指针时,要标明指针所属的类。 this 指针:指向类的当前对象的指针常量。 什么是野指针和悬空指针? 悬空指针: 若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。 “野指针”是指不确定其指向的指针,未初始化的指针为“野指针”。 指针和引用的区别是什么? 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变) 指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间。(是否占内存) 指针可以为空,但是引用必须绑定对象。(是否可为空) 指针可以有多级,但是引用只能一级。(是否能为多级) 常量指针和指针常量的区别是什么? 常量指针 :常量指针本质上是个指针,只不过这个指针指向的对象是常量。也就是说,不可以通过对指针解引用修改指针指向的内容,而可以修改指针的指向。 特点:const 的位置在指针声明运算符 \ 的左侧。只要 const 位于\ 的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。(可以这样理解,\ 左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针。) 注意 1:指针指向的对象不能通过这个指针来修改,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值。 注意 2:虽然常量指针指向的对象不能变化,可是因为常量指针本身是一个变量,因此,可以被重新赋值。例如: 指针常量 的本质上是个常量,只不过这个常量的值是一个指针,和上面那个恰恰相反,不可以修改指针的指向,但可以通过对指针解引用修改指针指向的内容。 特点:const 位于指针声明操作符右侧,表明该对象本身是一个常量, 左侧表示该指针指向的类型,即以 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质。 注意 1:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变。 注意 2:指针的内容可以改变。 函数指针和指针函数的区别是什么? 指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。 函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。举例: 两者区别: 函数指针是个指针,专门指向函数的指针变量,比如能存某个函数的地址,通过它能调用对应的函数。 指针函数则是个函数,只是这个函数的返回值是指针类型,调用后会得到一个指针结果。 关键看名字最后俩字,“函数指针” 核心是指针,用来指向函数;“指针函数” 核心是函数,特点是返回指针。别被名字顺序迷惑,重点在结尾,一个是指针,一个是函数,这就是两者最根本的不同。 指针和引用值传递的区别是什么? 值传递是指将参数的值复制一份,传递给函数或方法进行操作。在值传递中,函数或方法对参数进行修改不会影响到原始的变量值。 指针引用是指将参数的内存地址传递给函数或方法,使得函数或方法可以直接访问和修改原始变量的值。在指针引用中,函数或方法对参数的修改会直接反映在原始变量上。 函数传参用引用的作用是什么? 可以避免避免拷贝,使用引用传参可以避免对大型对象进行复制。如果传递一个对象作为值参数,会触发对象的拷贝构造函数,造成额外的开销。而使用引用传参,可以直接在函数中操作原始对象,避免了拷贝操作。 参数传递时,值传递、引用传递、指针传递的区别? 参数传递的三种方式: 值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。 指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。 引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。 说明:从上述代码的运行结果可以看出,只有在值传递时,形参和实参的地址不一样,在函数体内操作的不是变量本身。引用传递和指针传递,在函数体内操作的是变量本身。 C++全局变量、局部变量、静态全局变量、静态局部变量的区别? C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。 从作用域看: 全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用extern 关键字再次声明这个全局变量。 静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被static 关键字修饰过的变量具有文件作用域。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。 局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。 静态局部变量:具有局部作用域。它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。 从分配内存空间看: 静态存储区:全局变量,静态局部变量,静态全局变量。 栈:局部变量。 全局变量定义在头文件中有什么问题? 如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能再头文件中定义全局变量。 extern C 的作用是什么? 当 C++ 程序 需要调用 C 语言编写的函数,C++ 使用链接指示,即 extern “C” 指出任意非 C++ 函数所用的语言。 举例: sizeof(1==1) 在 C 和 C++ 中分别是什么结果? C 语言代码: C++ 代码: C 和 C++ struct 的区别? 在 C 语言中 struct 是用户自定义数据类型;在 C++ 中 struct 是抽象数据类型,支持成员函数的定义。 C 语言中 struct 没有访问权限的设置,是一些变量的集合体,不能定义成员函数;C++ 中 struct 可以和类一样,有访问权限,并可以定义成员函数。 C 语言中 struct 定义的自定义数据类型,在定义该类型的变量时,需要加上 struct 关键字,例如:struct A var;,定义 A 类型的变量;而 C++ 中,不用加该关键字,例如:A var; C++ 中 struct和Class区别是什么? 在 C++ 中, 和 是两种用于定义自定义数据类型的关键字,它们的核心功能相似(都能包含成员变量和成员函数),但存在一些关键区别,主要体现在 默认访问权限 和 默认继承方式 上: 默认访问权限不同: 默认访问权限为 , 默认访问权限为 。比如: 默认继承方式不同:当使用继承时,两者的默认继承权限也不同, 默认继承方式为 继承, 默认继承方式为 继承。比如: 为什么有了 class 还保留 struct? C++ 是在 C 语言的基础上发展起来的,为了与 C 语言兼容,C++ 中保留了 struct。 struct 和 union 的区别是什么? 说明:union 是联合体,struct 是结构体。 区别: 联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。 对联合体的不同成员赋值,将会对覆盖其他成员的值,而对于结构体的对不同成员赋值时,相互不影响。 联合体的大小为其内部所有变量的最大值,按照最大类型的倍数进行分配大小;结构体分配内存的大小遵循内存对齐原则。 class 和 struct 的异同是什么? struct 和 class 都可以自定义数据类型,也支持继承操作。 struct 中默认的访问级别是 public,默认的继承级别也是 public;class 中默认的访问级别是 private,默认的继承级别也是 private。 当 class 继承 struct 或者 struct 继承 class 时,默认的继承级别取决于 class 或 struct 本身,class(private 继承),struct(public 继承),即取决于派生类的默认继承级别。 举例: class 可以用于定义模板参数,struct 不能用于定义模板参数。 C 和 C++ static 的区别是什么? 在 C 语言中,使用 static 可以定义局部静态变量、外部静态变量、静态函数 在 C++ 中,使用 static 可以定义局部静态变量、外部静态变量、静态函数、静态成员变量和静态成员函数。因为 C++中有类的概念,静态成员变量、静态成员函数都是与类有关的概念。 C++ static作用是什么? static 定义静态变量,静态函数。 保持变量内容持久:static 作用于局部变量,改变了局部变量的生存周期,使得该变量存在于定义后直到程序运行结束的这段时间。 隐藏:static作用于全局变量和函数,改变了全局变量和函数的作用域,使得全局变量和函数只能在定义它的文件中使用,在源文件中不具有全局可见性。(注:普通全局变量和函数具有全局可见性,即其他的源文件也可以使用。) static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。 static 在类中使用的注意事项有哪些? static 静态成员变量: 静态成员变量是在类内进行声明,在类外进行定义和初始化,在类外进行定义和初始化的时候不要出现static关键字和private、public、protected 访问规则。 静态成员变量相当于类域中的全局变量,被类的所有对象所共享,包括派生类的对象。 静态成员变量可以作为成员函数的参数,而普通成员变量不可以。 静态数据成员的类型可以是所属类的类型,而普通数据成员的类型只能是该类类型的指针或引用。 static 静态成员函数: 静态成员函数不能调用非静态成员变量或者非静态成员函数,因为静态成员函数没有 this 指针。静态成员函数做为类作用域的全局函数。 静态成员函数不能声明成虚函数(virtual)、const 函数和 volatile 函数。 static 全局变量和普通全局变量的异同是什么? 相同点: 存储方式:普通全局变量和 static 全局变量都是静态存储方式。 不同点: 作用域:普通全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,普通全局变量在各个源文件中都是有效的;静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其他源文件中引起错误。 初始化:静态全局变量只初始化一次,防止在其他文件中使用。 C++ 静态变量的使用场景是什么?未初始化的全局静态变量呢? 静态变量(包括全局静态、局部静态、类静态成员)的核心特点是 生命周期贯穿程序运行始终,且作用域受限定 ,常见使用场景如下: 全局静态变量( 修饰的全局变量) 作用:限制变量仅在当前文件内可见(避免不同文件中同名变量冲突),但生命周期是整个程序运行期间。 场景:当多个文件需要独立使用同名变量(如统计各模块的内部计数),但不希望被其他文件访问或修改时。例: (仅当前 文件可访问,其他文件即使声明 也无法使用)。 局部静态变量(函数内的 变量) 作用:变量在函数第一次调用时初始化,后续调用不再重新初始化,值会被保留(生命周期全局,作用域仅限函数内)。 场景:记录函数被调用的次数(如 );单例模式中,确保全局只存在一个实例(如函数内返回静态对象的指针);避免频繁创建销毁临时对象(如工具函数中复用的缓冲区)。 类静态成员变量( 修饰的类成员) 作用:属于整个类而非某个对象,所有对象共享该变量,生命周期全局,需在类外单独初始化。 场景:统计类的实例数量(如 ,在构造函数中 ,析构函数中 );存储类级别的常量或共享配置(如 )。 未初始化的全局静态变量 未初始化的全局静态变量(如 )有两个关键特性: 1. 自动初始化 :编译器会将其默认初始化为 0 (包括数值类型为 0,指针类型为 等)。 这是因为全局静态变量存放在内存的 BSS 段 (未初始化数据段),程序启动时系统会自动将该段所有数据清零。 2. 作用域限制 :和初始化的全局静态变量一样,仅在当前文件内可见,不影响其他文件的同名变量。 例: 未初始化的全局静态变量本质上是 “带文件作用域的零初始化全局变量”,适合需要跨函数(但仅限当前文件)共享、且初始值为 0 的场景。 介绍const 作用及用法? 作用: const 修饰成员变量,定义成 const 常量,相较于宏常量,可进行类型检查,节省内存空间,提高了效率。 const 修饰函数参数,使得传递过来的函数参数的值不能改变。 const 修饰成员函数,使得成员函数不能修改任何类型的成员变量(mutable 修饰的变量除外),也不能调用非 const 成员函数,因为非 const 成员函数可能会修改成员变量。 在类中的用法: const 成员变量: const 成员变量只能在类内声明、定义,在构造函数初始化列表中初始化。 const 成员变量只在某个对象的生存周期内是常量,对于整个类而言却是可变的,因为类可以创建多个对象,不同类的 const 成员变量的值是不同的。因此不能在类的声明中初始化 const 成员变量,类的对象还没有创建,编译器不知道他的值。 const 成员函数: 不能修改成员变量的值,除非有 mutable 修饰;只能访问成员变量。 不能调用非常量成员函数,以防修改成员变量的值。 define 和 const 的区别是什么? 区别: 编译阶段:define 是在编译预处理阶段进行替换,const 是在编译阶段确定其值。 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的常量是有类型的,是要进行判断的,可以避免一些低级的错误。 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的空间;const 定义的常量占用静态存储区的空间,程序运行过程中只有一份。 调试:define 定义的宏常量不能调试,因为在预编译阶段就已经进行替换了;cons定义的常量可以进行调试。 const 的优点: 有数据类型,在定义式可进行安全性检查。 可调式。 占用较少的空间。 define 和 typedef 的区别是什么? 原理: define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。 功能:typedef 用来定义类型的别名,方便使用。 define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。 作用域: define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。 指针的操作:typedef 和 define 在处理指针时不完全一样 volatile 的作用?是否具有原子性,对编译器有什么影响? volatile 的作用:当对象的值可能在程序的控制或检测之外被改变时,应该将该对象声明为 violatile,告知编译器不应对这样的对象进行优化。 volatile不具有原子性。 volatile 对编译器的影响:使用该关键字后,编译器不会对相应的对象进行优化,即不会将变量从内存缓存到寄存器中,防止多个线程有可能使用内存中的变量,有可能使用寄存器中的变量,从而导致程序错误。 什么情况下一定要用 volatile, 能否和 const 一起使用? 使用 volatile 关键字的场景: 当多个线程都会用到某一变量,并且该变量的值有可能发生改变时,需要用 volatile 关键字对该变量进行修饰; 中断服务程序中访问的变量或并行设备的硬件寄存器的变量,最好用 volatile 关键字修饰。 volatile 关键字和 const 关键字可以同时使用,某种类型可以既是 volatile 又是 const ,同时具有二者的属性。 为什么一般将析构函数设置为虚函数? 析构函数被设为虚函数主要是为了解决基类指针指向派生类对象时的资源释放问题。 如果我们有一个基类指针,它实际上指向一个派生类对象,当我们删除这个基类指针时,如果析构函数不是虚函数,那么就只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致派生类对象的一些资源没有被正确释放,从而引发内存泄漏等问题。 如果我们将析构函数设置为虚函数,那么在删除基类指针时,会首先调用派生类的析构函数,然后再调用基类的析构函数,从而确保所有的资源都能被正确释放。 析构函数为什么通常是会做成一个虚函数呢? 如果一个类有虚函数,就应该为其定义一个虚析构函数。这是因为在使用delete操作符释放一个指向派生类对象的基类指针时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数,这样就会导致内存泄漏和未定义行为的问题。通过将析构函数定义为虚函数,可以确保在释放派生类对象时,先调用派生类的析构函数,再调用基类的析构函数,从而避免内存泄漏和未定义行为的问题。 为什么析构函数一般写为虚函数? 如果析构函数不被声明成虚函数, 则编译器实施静态绑定 ,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。 所以在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。 为什么构造函数不写为虚函数? 从存储空间角度:虚函数对应一个vtable,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。 从使用角度:虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。 什么是内联函数? 在C++中,使用关键字"inline"可以声明一个内联函数。声明为内联函数的函数会在编译时被视为候选项,编译器会尝试将其展开,将函数体直接插入到调用点处。这样可以避免函数调用的开销,减少了函数调用的栈帧等额外开销,从而提高程序的执行效率。 宏定义(define)和内联函数(inline)的区别是什么? 内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。 内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。 宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。 内联函数有什么缺点? 内联函数的缺点主要有以下几点: 代码膨胀:内联函数会在每个调用它的地方进行代码替换,这可能导致代码膨胀。如果内联函数体非常大或者被频繁调用,会增加可执行文件的大小,可能导致缓存不命中,影响性能。 编译时间增加:内联函数需要在每个调用点进行代码替换,这会增加编译时间。特别是当内联函数被广泛使用时,编译时间可能会显著增加。 头文件膨胀与维护成本增加:由于内联展开需要在编译期就能看到函数定义,内联函数(尤其是跨文件使用的)通常需要把完整定义写在头文件中,而不是只放声明。这会暴露实现细节、使头文件变得臃肿,并且一旦修改实现,所有包含该头文件的源文件都要重新编译,增加了维护成本。 include “ “ 和 < 的区别是什么? include<文件名 和 include”文件名” 的区别: 查找文件的位置:include<文件名 在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找; include”文件名” 在当前源文件所在目录中进行查找,如果没有;再到系统目录中查找。 使用习惯:对于标准库中的头文件常用 include<文件名 ,对于自己定义的头文件,常用 include”文件名” void\ 是什么? 是一种通用的指针类型,被称为"无类型指针"。它可以用来表示指向任何类型的指针,因为 指针没有指定特定的数据类型。 由于 是无类型的,它不能直接进行解引用操作,也不能进行指针运算。在使用 指针时,需要将其转换为具体的指针类型才能进行操作。 指针常用于需要在不同类型之间进行通用操作的情况,例如在函数中传递任意类型的指针参数或在动态内存分配中使用。 malloc的参数列表 void\ 怎么转化为int\ 的? 可以使用类型转换将 指针转化为 指针。以下是将 指针转化为 指针的示例代码: 在上述示例中,使用 函数分配了存储一个 类型数据所需的内存,并返回了一个 指针。然后,通过将 指针转换为 指针,将其赋值给 变量。现在,可以通过 指针访问和操作 类型的数据。 sizeof 和 strlen 的区别是什么? strlen 是头文件 中的函数,sizeof 是 C++ 中的运算符。 strlen 测量的是字符串的实际长度(其源代码如下),以 \0 结束。而 sizeof 测量的是字符数组的分配大小。 若字符数组 arr 作为函数的形参,sizeof(arr) 中 arr 被当作字符指针来处理,strlen(arr) 中 arr 依然是字符数组,从下述程序的运行结果中就可以看出。 strlen 本身是库函数,因此在程序运行过程中,计算长度;而 sizeof 在编译时,计算长度; sizeof 的参数可以是类型,也可以是变量;strlen 的参数必须是 char\ 类型的变量。 explicit 的作用是什么? 作用:用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。 隐式转换: 上述代码中,A ex = 10; 在编译时,进行了隐式转换,将 10 转换成 A 类型的对象,然后将该对象赋值给 ex,等同于如下操作: 为了避免隐式转换,可用 explicit 关键字进行声明: memcpy 函数的底层原理是什么? memcpy 函数的底层原理简单说就是直接操作内存块的二进制数据。它会从源地址开始,逐个字节(或按更高效的块)复制数据到目标地址,直到复制完指定的字节数。 底层实现通常会做优化,比如对对齐的内存块用更大的单位(如 4 字节、8 字节)批量复制,比单字节循环更快;对未对齐的部分先用单字节处理到对齐位置,再用块复制。整个过程不关心数据类型,纯粹按字节搬运,所以复制后目标内存和源内存的二进制内容完全一致,但不会处理像字符串结束符这类特殊情况。 strcpy 函数有什么缺陷? strcpy 函数的缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。 说明:从上述代码中可以看出,变量 var 的后六位被字符串 “hello world!” 的 “d!\0” 这三个字符改变,这三个字符对应的 ascii 码的十六进制为:\0(0x00),!(0x21),d(0x64)。 原因:变量 arr 只分配的 10 个内存空间,通过上述程序中的地址可以看出 arr 和 var 在内存中是连续存放的,但是在调用 strcpy 函数进行拷贝时,源字符串 “hello world!” 所占的内存空间为 13,因此在拷贝的过程中会占用 var 的内存空间,导致 var的后六位被覆盖。 C++编译 C++的编译过程介绍一下? C++的编译过程经过了预处理、编译、汇编和链接四个主要阶段: 预处理:预处理阶段会对源代码进行处理,主要包括展开宏定义、处理条件编译指令(如 include、 define、 ifdef等)以及删除注释等。预处理的结果是生成一个经过宏展开和条件处理后的纯C++源代码文件。 编译(Compilation):编译阶段将预处理后的源代码翻译为汇编语言,生成汇编代码。编译器会进行词法分析、语法分析和语义分析,检查代码的正确性,并生成中间代码表示。 汇编:汇编阶段将汇编代码转换为机器可以执行的目标文件。汇编器会将汇编代码转化为机器指令,并生成与机器硬件平台相关的目标文件(通常以".obj"或".o"为扩展名)。 链接:链接阶段将目标文件与其他必要的库文件链接在一起,生成可执行程序。链接器会解析目标文件中的符号引用,将其与其他目标文件或库文件中的符号定义进行匹配,最终生成一个完整的可执行文件。在链接阶段,还会进行地址重定位、符号解析、符号表生成等操作,确保程序的正确执行。 静态链接库和动态链接库有什么区别? 链接方式:静态链接库在编译链接时会被完整地复制到可执行文件中,成为可执行文件的一部分;而动态链接库在编译链接时只会在可执行文件中包含对库的引用,实际的库文件在运行时由操作系统动态加载。 文件大小:静态链接库会使得可执行文件的大小增加,因为库的代码被完整地复制到可执行文件中;而动态链接库不会增加可执行文件的大小,因为库的代码在运行时才会被加载。 内存占用:静态链接库在运行时会被完整地加载到内存中,占用固定的内存空间;而动态链接库在运行时才会被加载,可以在多个进程之间共享,减少内存占用。 可扩展性:动态链接库的可扩展性更好,可以在不修改可执行文件的情况下替换或添加新的库文件,而静态链接库需要重新编译链接。 C++面向对象 什么是面向对象?面向对象的三大特性 面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。 面向对象的三大特性: 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。 继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。 C++特性介绍一下? 封装是将一些数据和函数封装到类中,这样外层调用类只会调用到设计者想让他调用的方法; 继承的话,我常是设计一个基类,然后分别设置子类去继承基类的一些方法,尤其是虚函数,针对不同子类的特点对虚函数进行重写。 继承还有公有和私有两种方法,公有继承是将基类的成员都原封不动的继承下来,私有继承则会将其改为私有部分;多态的话,是有函数重载和之前提到的虚函数,函数重载是可以使得相同的函数面对不同的参数个数或者类型进行不同的方式实现。 如何理解 C++ 是面向对象编程? 说明:该问题最好结合自己的项目经历进行展开解释,或举一些恰当的例子,同时对比下面向过程编程。 面向过程编程:一种以执行程序操作的过程或函数为中心编写软件的方法。程序的数据通常存储在变量中,与这些过程是分开的。所以必须将变量传递给需要使用它们的函数。缺点:随着程序变得越来越复杂,程序数据与运行代码的分离可能会导致问题。例如,程序的规范经常会发生变化,从而需要更改数据的格式或数据结构的设计。当数据结构发生变化时,对数据进行操作的代码也必须更改为接受新的格式。查找需要更改的所有代码会为程序员带来额外的工作,并增加了使代码出现错误的机会。 面向对象编程(Object Oriented Programming, OOP):以创建和使用对象为中心。一个对象(Object)就是一个软件实体,它将数据和程序在一个单元中组合起来。对象的数据项,也称为其属性,存储在成员变量中。对象执行的过程被称为其成员函数。将对象的数据和过程绑定在一起则被称为封装。 面向对象编程进一步说明: 面向对象编程将数据成员和成员函数封装到一个类中,并声明数据成员和成员函数的访问级别(public、private、protected),以便控制类对象对数据成员和函数的访问,对数据成员起到一定的保护作用。而且在类的对象调用成员函数时,只需知道成员函数的名、参数列表以及返回值类型即可,无需了解其函数的实现原理。当类内部的数据成员或者成员函数发生改变时,不影响类外部的代码。 重载、重写、隐藏的区别是什么? 重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。 说明:上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。 重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。 重写和重载的区别: 范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。 参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。 virtual 关键字:重写的函数基类中必须有 virtual关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。 隐藏和重写,重载的区别: 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。 C++的多态是什么?怎么通过虚函数实现? C++中的多态性是指的是同一个操作作用于不同的对象时,可以产生不同的行为。多态性主要通过虚函数实现,能够让你以父类的指针或引用调用子类的实现,从而在运行时决定使用哪个函数。 C++中的多态通常分为两种主要类型: 编译时多态(静态多态) :通过函数重载和运算符重载实现,在编译时确定调用哪个函数。 运行时多态(动态多态) :通过虚函数实现,在运行时根据对象的实际类型决定调用的函数。实现原理是,每个包含虚函数的类都有一个虚函数表,这个表记录了该类对象可以调用的虚函数指针,每个对象也有一个隐式的虚函数指针,指向其类的虚函数表。在运行时,调用虚函数时,程序通过虚函数指针查找正确的函数。 虚函数是通过在基类中声明一个函数为 来实现的。这标志着这个函数可以被派生类重写(override)。当通过基类指针或引用调用虚函数时,C++会查找实际对象的类型,调用对应的子类实现,而不是基类的实现。 下面是一个简单的示例,展示了如何使用虚函数实现多态。 1. 定义基类和派生类 2. 使用虚函数 创建一个函数,接受基类指针作为参数,利用多态性调用不同的派生类方法。 C++中的多态怎么实现的? C++中的多态主要通过虚函数和继承来实现。多态分为两种:编译时多态和运行时多态。 编译时多态:也称为静态多态或早绑定。这种多态是通过函数重载和模板来实现的。 运行时多态:也称为动态多态或晚绑定。这种多态是通过虚函数和继承来实现的。当基类的指针或引用指向派生类对象时,调用的虚函数将是派生类的版本,这就实现了运行时多态。 C++多态特性是什么? 多态是面向对象编程(OOP)的重要特性之一,它允许不同的对象对同一消息(函数调用)做出不同的响应。在 C++ 中,多态主要通过虚函数来实现。 多态有静态多态和动态多态两种: \ \ 静态多态(编译时多态):\ \ 主要通过函数重载来实现。 函数重载 是指在同一个作用域内,可以有多个同名函数,但是它们的参数列表(参数个数、参数类型或者参数顺序)不同。例如: 当调用 函数时,编译器会根据传入的参数类型和个数在编译时期就确定调用哪个版本的 函数。 \ \ 动态多态(运行时多态):\ \ 基于虚函数和继承来实现。它允许在运行时根据对象的实际类型来调用相应的函数。当一个类包含虚函数时,编译器会为这个类创建一个虚函数表。虚函数表是一个函数指针数组,其中存储了这个类的虚函数的地址。每个包含虚函数的类的对象中都会包含一个虚函数指针,这个指针指向该类的虚函数表。当通过基类指针或引用调用虚函数时,程序会根据虚函数指针找到对应的虚函数表,然后在虚函数表中查找要调用的虚函数的实际地址,从而实现根据对象的实际类型来调用函数。 C++的函数对象是什么?跟普通函数的区别? 函数对象 是指一个重载了 的类或结构体实例。函数对象可以像普通函数一样被调用,但它们实际上是对象,具有状态和行为。 普通函数与函数对象的区别: 定时方式:普通函数是用 语法定义的,而函数对象是一个类或结构体,并重载了 。 状态:普通函数无状态,而函数对象可以有内置的状态(成员变量)。 调用方式:普通函数被直接调用,函数对象需要先创建实例,然后用实例调用。 灵活性:函数对象可以重载多个操作符或添加更多功能,普通函数则只能定义一个函数。 普通函数: 函数对象: C++ 类相关 clas