目录
C++
面向对象和面向过程
面向过程
面向对象
三大特性?
C语言和C++的区别?
C++编译过程
多态
是什么?
分类?
虚函数
是什么?
底层?
解决的问题?
构造函数不能设置为虚函数?
重载 重写 隐藏
引用
是什么?
好处
为什么不能初始化为空?
引用与指针的区别?
内存分区
堆和栈的区别?
指针常量和常量指针
NULL在C语言中是(void *)0在C++中是0?
C++用nullptr代指空指针?
构造函数
是什么?
拷贝构造
调用时机
拷贝构造参数不是引用行吗?
深浅拷贝的区别?
析构函数
是什么?
内存分配和销毁用什么?
new和malloc
区别?
new delete malloc free?
new会先调用malloc再调用构造函数 delete先析构再free?
delete先析构再free?
new可以重载吗?
C++11
lambda表达式
是什么?
内联函数
是什么?
可以是虚函数吗?
智能指针
unique_ptr
shared_ptr
weak_ptr
函数模板
是什么?
C++
面向对象和面向过程
面向过程
面向过程编程更关注的是过程,也就是一系列的步骤,把程序设计成一步一步解决问题的方式。这种编程方式把程序看作是由一组函数或过程组成的,每个函数完成一个具体的任务,数据则在这些函数之间传递。
面向对象
面向对象编程则更多地关注“对象”,即如何将现实中的事物抽象成程序中的对象。对象既包含数据(称为属性),也包含操作这些数据的方法(称为方法)。面向对象的思想是将数据和操作封装在一起,通过对象之间的交互来实现程序的功能。
三大特性?
封装:把数据和操作封装在对象内部,外部只能通过定义好的接口(方法)来访问数据。
继承:可以从已有的类派生出新的类,新的类可以继承和扩展原有类的属性和方法。
多态:不同对象可以对同一个方法做出不同的响应,这增强了程序的灵活性。
简单来说,面向过程是把程序看作是由函数组成的一步步过程,而面向对象则是将程序中的元素视为“对象”,
C语言和C++的区别?
(1)头文件C语言有.h,C++无
(2)bool: C语言标准(C99)之前没有为布尔值单独设置一个类型,所以在判断真假时,使用整数 0 表示假,所有非0表示真。C99标准新增_Bool表示布尔值,即逻辑true和false,。C99还提供了一个头文件stdbool.h,文件中定义了bool、true、falses三个我们用到的宏
(3)C++的编译器效率比C编译器低.C++不是更好的C,而是基于C的另一种编程语言。
(4)c++不是一个完全面向对象的语言,它是基于面向对象的语言,因为我们的c++语言中还包含C语言的东西,而我们的C语言是一个面向过程的语言。C语言没有面向对象易于维护、易复用、易扩展。C语言标准中不包括图形处理。
(5)重载->是静态多态
C语言不能重载,而C++可以。重载的定义是在同一个作用域下,函数名相同,参数类型或者顺序或者个数不同则为函数重载,因为编译器对两种语言的处理方式不同,C++编译器编译后会在原函数名的基础上加上参数类型来识别重载函数,而C语言编译后还是原函数名就会出现重定义错误,故C语言不能重载。重载还会发生与默认值冲突产生二义性。
C++编译过程
预处理 编译 汇编 链接
预处理:主要处理#开头的指令,将头文件插入到程序中,将全部的#define宏展开进行宏替换。处理全部的预处理指令,#if #ifdef #else等处理#include指令,这个过程是递归的,即被包括的文件可能还包括其它文件。删除全部注释// /**/。加入行号和文件标识。经过预处理后的 .i 文件不包括任何宏定义,由于全部的宏已经被展开。而且包括的文件也已经被插入到 .i 文件里。
编译:将源代码由文本形式转换成机器语言,生成汇编代码文件.s。
汇编:将汇编代码.s翻译成机器指令的.o或.obj目标文件,.o文件是纯二进制文件。
链接:产生的.out或.exe可运行文件。
汇编程序生成的目标文件,即 .o 文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp文件中定义的符号或者调用了某个库文件中的函数。那链接的目的就是将这些文件对应的目标文件连接成一个整体,从而生成可执行的程序 .exe文件。
链接是将所有的.o文件和库(动态库、静态库)链接在一起,得到可以运行的可执行文件(Windows的.exe文件或Linux的.out文件)等
多态
是什么?
面向对象的三大特征之一,简单来说就是不同对象调用同一行为表现出的不同形式和结果。多态可以理解成是一个接口多种实现。
分类?
多态分为静态多态和动态多态。
像函数重载 运算符重载 函数模板这种在编译期间确定的就是静态多态,就是编译期间确定绑定的是哪一个函数。
动态多态是通过虚函数重写实现的,是在运行期间确定的多态,是一种晚绑定机制,在运行期间才能确定调用哪一个函数。
虚函数
是什么?
在基类函数名前面加上virtual关键字的就是被声明为虚函数。虚函数是动态多态,在运行期间父类指针指向实际对象的类型,在运行期间根据对象类型确定调用的是哪个虚函数的,运行时在虚函数表中寻找调用的虚函数地址。
底层?
从底层实现来看,C++ 中的虚函数是通过虚函数表和虚函数指针来实现的。假如这个类中有虚函数,那么会给这个类提供一个虚函数表,这个表中存储了该类中所有虚函数的地址,每个对象都有一个指向虚表的虚表指针,然后通过虚表指针找到虚表,进而通过找到函数在表中的位置实现函数的正确调用。然后当我们子类继承父类的话,它会也会把这个父类这个虚函数表给继承下来。如果我们子类重写父类的虚函数的话,它会把父类那个虚函数表中的虚函数地址进行替换,从而达到运行时实现多态。然后确定调用子类的函数。
(虚表指针是在创建对象时候初始化指向对应的虚函数表)
解决的问题?
把父类的析构函数设置为虚析构,解决了就是能够正确释放子类的资源。
构造函数不能设置为虚函数?
因为在创建对象时,首先要调用构造函数来初始化对象,而虚函数机制依赖于虚函数表指针,在构造函数执行之前对象还未完全构建,虚函数表指针还不存在,所以构造函数不能是虚函数。
重载 重写 隐藏
函数重载是指在同一个类里,定义多个名字相同但参数不同的函数。通常用来处理类似的问题或任务,但输入的参数类型、个数或者顺序三者有一个不同。
重写是指在派生类中重新定义基类中的虚函数,相当于提供基类虚函数的新版本,名字和参数都一样。重写是为了在派生类中提供基类函数的新实现,以便通过基类指针或引用调用时,执行派生类中的实现。想象你继承了一个老爸的公司,你要接管他的工作,但是你觉得有些地方要改进,所以你用你自己的方式重新做了一遍。这就是重写。
隐藏是指在派生类中定义了一个与基类中同名的函数,但参数列表不同,或者基类中的函数不是虚函数。这样,基类的函数在派生类中被隐藏了。就像你有个同名的兄弟,你们都在同一个公司工作,但你们负责的工作内容不同。客户要找你时,会直接找你而不会去找你的兄弟。这就是隐藏。
引用
是什么?
引用呢是给变量起一个别名,在创建引用时就要初始化绑定一个变量。
好处
引用不占内存,使得引用在处理大对象时提高性能,避免了复制整个对象的内存和时间开销。
通过引用传递参数,可以避免使用指针的复杂性和潜在错误,还能保持代码的高可读性
函数返回值本身是右值 想返回左值时需要引用
还可以避免拷贝构造(引用与原始对象共享相同的内存地址,因此对引用的操作实际上是在操作原始对象本身,而不是创建对象的副本。)
为什么不能初始化为空?
因为他的定义就是这样,然后引用还不同于指针,引用一旦创建就不能更改其指向,所以初始化引用的时候必须指定一个有效的对象。
引用与指针的区别?
指针呢是一个变量只不过他存储的是另一个变量的地址,可以改变指向。而引用是是已经存在的变量起一个别名,在创建引用时就要初始化绑定一个变量,不能改变引用关系绑定其他对象了。
想要访问一块内存,指针呢需要进行进行星解引用操作,而引用直接可以进行访问。当指针作为函数参数时,函数内部通过解引用指针来访问和修改所指向的对象。引用作为函数参数时,函数内部直接对引用进行操作就是对实参本身进行操作。
如果函数返回引用,这个引用可以被用作左值。当函数返回引用时,实际上返回的是对象本身的别名,而不是对象的副本,避免了不必要的拷贝。
(补充:如果函数返回一个局部变量的引用(非静态局部变量),当函数结束时,这个局部变量就被销毁了,再使用这个引用就会出错。如果返回一个局部变量的指针(非静态局部变量),当函数结束后,该局部变量的内存被
释放,返回的指针就会成为野指针,使用野指针会导致未定义行为。)
指针可以有多级指针,而引用没有多级的概念。当需要在函数内部修改指针本身时,可以使用多级指针作为函数参数。
引用不可以为空 指针可以
引用++是值++ 而指针++是地址偏移
内存分区
堆和栈的区别?
堆是由程序员手动申请和释放的,可以使用new和malloc申请,释放使用delete和free.
指针
指针常量和常量指针
1)指向常量的指针 (**const**
在类型前)
指针指向的对象是常量,不能通过这个指针修改该对象的值,但指针本身可以改变。
(2)常量指针 (**const**
在*****
号后)
指针本身是常量,不能改变指针指向的地址,但可以通过它修改指向的对象。
(3)指向常量的常量指针 (**const**
在两处)
指针指向的对象和指针本身都不能修改。
NULL在C语言中是(void *)0在C++中是0?
在C语言中,NULL
通常定义为 (void *)0
,因为C允许将 void*
类型的指针隐式转换为任何其他类型的指针。这样,NULL
可以赋值给任何指针类型,编译器不会报错。
在C++中,NULL
被定义为 0
。C++比C对类型转换的检查更严格,不允许 void*
自动转换为其他类型的指针。用 0
来表示 NULL
是为了避免这些类型转换问题,因为 0
可以被看作是任何指针类型的空指针。
C++用nullptr代指空指针?
类型安全:nullptr
有一个专门的类型 std::nullptr_t
,不会被误用为整数或其他类型的指针。
清晰明确:使用 nullptr
可以明确表示这是一个空指针,而不是整数 0
,提高代码的可读性和维护性。
构造函数
是什么?
函数名与类名相同 无返回值也不写void 创建对象时自动调用构造函数 没有实现构造函数时编译器会提供一个默认的无参构造函数。构造函数是给成员变量赋值的不是初始化 初始化参数列表是给成员变量初始化的。
拷贝构造
调用时机
用一个已经存在的对象初始化一个新对象时
对象以值的形式作为函数的参数和返回值
拷贝构造参数不是引用行吗?
拷贝构造的参数不是引用会导致无限递归 加const是为了避免通过形参修改实参
深浅拷贝的区别?
浅拷贝是简单的赋值操作
深拷贝是拷贝相同大小相同内容到堆区
析构函数
是什么?
析构函数名与类名相同前面加~ 无参数 无返回值 无void 销毁对象时自动调用 未实现编译器会提供默认的析构 析构函数是释放对象里面成员变量所指向的堆区内存的
内存分配和销毁用什么?
new运算符申请内存会先调用malloc在调用构造函数 释放对象申请的内存空间用delete,delete会先调用析构函数在调用free。delete[]用于释放数组对象占用的内存。它会依次调用数组中每个对象的析构函数来清理资源
new和malloc
区别?
new不需要传入具体的申请字节数,会自动机选要分配的内存空间,返回类型正确的指针,malloc需要手动计算需要分配的内存大小,返回void*类型的指针。
new是运算符可以重载,malloc是库函数不能重载。(库函数不能重载的原因主要是因为库函数的实现已经被固定在编译器或者库文件中,这些函数通常是用来执行特定的任务和操作的,比如内存分配、文件操作等。如果允许对这些函数进行重载,可能会导致程序运行时出现不可预测的行为,因为编译器无法确定到底使用哪个版本的函数实现。)
new的返回值不需要强转 malloc需要
malloc申请失败 例如申请负数会返回空 而new会抛出异常
给一个类分配堆区内存时 会先调用malloc在调用构造函数给成员变量赋值
new delete malloc free?
对于 new
和 delete
:
在 C++ 中,new
和 delete
是用于动态内存分配和释放的运算符。new
用于创建对象时,会自动调用对象的构造函数进行初始化,而 delete
则用于释放 new
分配的内存,并调用对象的析构函数。这样可以确保对象的生命周期管理是安全和正确的。
对于 malloc
和 free
:
在 C 语言中,我们使用 malloc
和 free
来进行动态内存管理。malloc
分配一块指定大小的内存空间,并返回一个指向该内存的 void*
指针,而 free
则用于释放 malloc
分配的内存。需要注意的是,使用 malloc
分配的内存需要手动管理对象的构造和析构,因为它不会自动调用构造函数或析构函数。
new会先调用malloc再调用构造函数 delete先析构再free?
构造函数需要有一块内存来初始化对象的状态,所以先调用malloc在堆区中找到一个足够大的未初始化的内存,一旦malloc成功分配了内存,new运算符会再这块内存上构造对象,也就是调用对象的构造函数来初始化成员。
delete先析构再free?
析构函数负责清理对象的资源和状态的。对象的状态存储在内存中,如果先调用free释放了这块内存,析构函数无法找到对象的数据和成员(访问已经被释放的内存),因此无法正确释放内存。所以必须在释放之前调用析构函数清理对象。
new可以重载吗?
new运算符可以被重载。这种重载允许程序员自定义 new
运算符的行为,以满足特定的需求或添加额外的功能。通过重载 new
运算符,可以指定特定的内存分配策略,如使用自定义的内存池、分配器或者记录内存分配信息等。
C++11
lambda表达式
是什么?
Lambda 表达式是一种在代码中直接定义匿名函数的方法 也是一种匿名的内联函数,与传统的函数定义方式不同,lambda 表达式可以在一行代码中定义函数,而无需显式地为其命名。这使得 lambda 表达式特别适合在需要临时函数或者回调函数的场景下使用。
通常我们在定义一个函数后,需要在其他地方调用它,这个过程可能会涉及到函数声明、调用等步骤。而使用 lambda 表达式,我们可以直接在需要调用的地方定义这个函数,这样不仅节省了代码量,也让代码更加直观易读。Lambda 表达式一般只作用于局部作用域,用完即释放,不会占用额外的资源。
Lambda 表达式的另一个优点是,它能够捕捉外部变量并使用它们,使得在局部范围内使用外部数据更加方便。
然而,lambda 表达式也有其局限性。由于其简洁性,lambda 表达式的功能相对有限,只适用于定义简单的、单行的逻辑。如果逻辑过于复杂,使用 lambda 表达式可能会导致代码难以理解和维护。在这种情况下,传统的函数定义方式可能更为合适。
内联函数
是什么?
内联函数是一种让编译器在调用时把函数的代码直接插入到调用处的方法,而不是像普通函数那样在运行时去跳转到函数的地址执行。这么做的好处是可以减少一些函数调用的开销,比如省掉了压栈、出栈这些步骤。对一些简单、频繁调用的函数来说,用内联能提高程序的执行效率。
在C++中,可以通过在函数定义前加上 inline关键字来建议编译器将其内联。需要注意的是,inline 只是一个建议,编译器可以根据实际情况选择是否进行内联。通常情况下,编译器会对那些函数体较小、逻辑简单的函数进行内联处理,而对于那些复杂的、包含循环或递归调用的函数,编译器可能会选择忽略inline建议。
虽然内联函数在某些情况下可以提高性能,但它也有一定的局限性。首先,内联函数会增加代码体积,因为每次调用内联函数时,都会将函数体复制到调用点。如果一个内联函数被多次调用,这将导致代码的冗余,可能增加可执行文件的大小。其次,对于一些复杂的函数,内联可能并不能带来性能提升,甚至会适得其反,因为编译器可能无法有效地优化这些代码。
此外,需要注意的是,递归函数通常不适合作为内联函数,因为递归函数本质上需要多次调用自身,内联化可能会导致代码膨胀和不必要的复杂性。
可以是虚函数吗?
通常情况下,内联函数不应该是虚函数。
这是因为内联函数是在编译时直接将函数代码插入到调用点,而虚函数的特性是在运行时通过虚函数表进行动态绑定来确定调用哪个函数。这两个特性在本质上是冲突的:内联函数希望在编译时确定代码,而虚函数则依赖于运行时的动态决策。
虽然技术上可以声明虚函数为内联函数,但是这样做通常是无意义的。因为当你通过基类指针调用一个虚函数时,编译器无法内联它,因为调用的具体函数要到运行时才能确定。因此,这样的虚函数通常不会被内联,而是通过常规的虚函数调用机制来执行。
智能指针
C++中不像java自带垃圾回收机制,他必须要释放掉分配到内存。因此引入了智能指针。智能指针是C++中用于自动管理内存的工具,可以有效防止内存泄漏和一些手动管理内存时容易出错的问题。传统的指针在动态分配内存后,我们必须记得手动释放,这很容易出错,比如忘记释放就会导致内存泄漏。而智能指针通过在对象生命周期结束时自动释放资源。智能指针的核心是引用计数,每使用它一次内部的引用计数会+1,每析构一次引用计数会-1,引用计数减为0时会删除原始指针指向的堆区内存。使用只能指针需要引入头文件
<memory>
我了解的智能指针有三种
unique_ptr
unique_ptr
[juˈniːk] 代表独占所有权的指针,意味着内存在某一时刻只能被一个 unique_ptr
拥有不允许其他智能指针共享其内部的指针,通过构造函数初始化,不允许一个unique_ptr
复制给另一个unique_ptr
, 因为那会违反 unique_ptr
的独占所有权特性,这样可以明确地知道谁在管理这块内存,不会有多个指针混乱地操作同一块内存。如果你想把资源从一个 unique_ptr
转移到另一个 unique_ptr
时候,通过 std::move
转移内存的所有权,而原来的 `unique_ptr* 就不再持有这个资源了。
shared_ptr
shared_ptr
则是用来共享所有权的,多个 shared_ptr
可以同时管理同一块有效内存,它们通过引用计数来管理对象的生命周期,只有当最后一个 `shared_ptr 被销毁时引用计数变为0时,内存才会被释放。这个很适合在需要多个地方共享同一个对象的场景。
不过要注意的是,不要使用一个原始指针初始化多个**shared_ptr**
。因为释放时,会释放多次内存导致内存泄漏。(一个原始指针初始化两个智能指针 会导致内存重复释放的问题 )
{正确初始化 shared_ptr
的方法是直接通过将原始指针赋值给一个 shared_ptr,然后通过拷贝构造或移动构造来共享内存管理,而不是用同一个原始指针初始化多个 shared_ptr
。
weak_ptr
如果两个 shared_ptr 互相引用,会导致循环引用的问题,这时候就需要用到
weak_ptr。(如果两个对象互相持有对方的 shared_ptr,就会出现循环引用的问题。具体来说,两个 shared_ptr 相互引用彼此时,它们的引用计数都不会归零,这意味着无论这两个对象是否超出了它们的作用域,引用计数都无法减到零,导致这两个对象永远无法被销毁,进而导致内存泄漏。)
这时候就需要用到 weak_ptr了。是一个弱引用。weak_ptr 是一种不增加引用计数的智能指针,它提供了一种观察而不拥有对象的方法 主要是监视share_ptr中管理的资源是否存在。。通过将其中一个对象的 `**shared_ptr 改为 weak_ptr,我们可以打破这个循环引用。当对象超出作用域时,引用计数能够正常减少到零,从而释放对象,避免内存泄漏。
智能指针让我们不再需要手动管理内存,避免了很多常见的错误。不过在使用时也要注意,比如 shared_ptr
的引用计数会有一些性能开销,所以要根据实际需求选择合适的智能指针
函数模板
是什么?
函数模板允许我们编写一个通用的代码,避免了为不同类型编写相似的函数。拿比大小来说,通常情况下要将int,float 等数据类型都编写一个函数很麻烦,而使用函数模板会节省时间和灵活很多,只需要写一次,然后通过传入的参数类型,编译器会自动帮你生成适当的函数版本。
关键字:template
C++中还有模板特化的概念,这允许我们为某些特定类型提供特殊的实现。有时候,你可能需要对某种类型做一些特殊处理,而不想影响其他类型的模板实现,这时候就可以用模板特化。
虽然函数模板提供了很大的灵活性,但它也有一定的局限性。例如,模板代码在编译时会生成具体类型的代码,这可能导致代码膨胀,特别是在处理大量不同类型的情况下。此外,模板的语法和错误信息有时比较复杂,调试可能不太直观。
函数模板广泛应用于C++标准库中,例如STL(标准模板库)中的各种容器和算法都依赖模板。通过函数模板,STL能够提供通用的接口来处理不同的数据类型,使得C++程序更具通用性和灵活性。