1.C与C++区别
C是面向过程语言,C++是面向编程语言。
面向过程:
概念: 分析出解决问题所需的步骤,再把步骤一步一步的实现,在使用时依次调用。
优点: 性能比面向对象高,因为类的调用一般需要实例化,开销大。
缺点: 没有面向对象易维护,易复用,易扩展
面向对象:
概念: 将问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描述某个事物在整个解决问题的步骤中的行为。
优点: 易维护,易复用,易扩展,由于面向对象有封装,继承,多态的特性,可以设计出低耦合的系统,使系统更加灵活,更加易于维护。
缺点: 性能比面向过程低
其他区别:
C中无参函数可以接受任意多个参数,C++中参数检测严格,不支持
C中不支持缺省参数
C中不支持函数重载
2. 指针
(1) 指针与引用的区别
定义:
指针是一个变量,有自己的地址,且存储的内容是也是一段内存地址
引用是一个变量的别名,其地址是其初始化的变量的地址,且不可变更,作用上相当于指针常量 *const。
初始化:
指针在声明时可以不初始化,而引用必须初始化,且初始化后不可变更
嵌套:
指针可以有多级,如指向指针的指针,但引用不能有多级,比如引用的引用。
空值:
指针可以指向null,引用不可引用null。因此,用指针和引用传参时,引用不需要判空。
大小(sizeof):
64位指针长度位8字节,int类型是4字节
32位指针长度是4字节,int类型也是4字节
自增:
指针自增是指针向后偏移一个类型大小,相当于数组操作,引用自增就是内容的自增。
关键字:
-
指针常量: int * const p;
可以解引用修改内容,但不能修改指针指向。 -
常量指针: const int *p 或 int const *p;
不能解引用修改内容,但是可以修改指针指向。 -
指向常量的指针常量: const int * const p;
不能修改内容,也不能修改指向。 -
引用: 本质上是*const,const int & p == const int * const p
使用常量(字面量)对const引用进行初始化后,C++编译器会为常量值分配空间,生成一个只读变量,并 将引用名作为这段空间的别名。且常引用的 值和指向都不能修改。This指针是一个指针常量,可修改值,但不能 修改指向。
(2) 访问速度
定义一个数组 int a[10]和一个指针 int* p = a。问这两个访问数组哪个速度更快?
一般直接访问数组更快,因为访问指针p时,需要先访问p存储的a地址,再对a进行访问。但是如果硬件对指针自增有支持,则可以直接对指针自增,然 后依次遍历数组,则指针会更快
(3) 野指针和悬空指针
- 悬空指针:
一个指向一块内存的指针,如果该内存稍后被回收或者释放,但指针仍然指向这块内存,则该指针就是“悬空指针”。 - 野指针:
野指针是指不确定具体指向的指针,通常是由于指针未初始化,野指针可能指向任意内存段,因此其可能损坏正常数据,或引发未知错误。-
野指针成因:
-
指向内存已被释放,但并没有将指针设为null。
-
指针没有及时初始化,指向了未知内存。
-
指针操作超越了变量的作用域范围,如数组越界。
-
返回了栈内存的指针,如函数内的局部变量,在返回时变量即被释放掉了。
-
-
3.函数
函数的基本使用原则
先定义,后使用
函数的三种定义方式
有参函数,无参函数,空函数
函数的三种调用方式
常规调用: 正常调用。
嵌套调用: 在一个函数中调用另一个函数。
递归调用: 在一个函数体内调用其自身。
递归特点:
-
递归函数一定要有明确的终止条件
-
递归算法通常代码简洁,但难读懂
-
递归调用需要建立大量函数副本,尤其是函数的参数,每一层递归调用时参数都是单独的占据内存空间,他们的地址是不同的,因此递归会消耗大量的 时间和内存。
-
递归函数分为调用和回退节点,回退的顺序一般是调用顺序的逆序。
函数的另外三种调用方式
_cdecl: C/C++的默认函数调用方式。函数参数由右向左入栈,主调函数负责栈平衡。
_stdcall: Windows API 函数的调用方式。函数参数由右向左入栈,被调函数负责栈平衡。
_fastcall: 快速调用方式,参数优先从寄存器传入,剩下的参数再由右向左从栈传入。由于栈位于内存,寄存器位于CPU,故存取方式快于内存,称为快速调用。
函数的参数传递方式
-
值传递:
直接传递值,该值传递给函数时,函数会创建该值的副本,并在函数内部使用这个副本。对其的修改不会影响原值。
其需要额外内存来存储副本,可能会导致内存消耗过大,且传递大型对象时,也会导致性能下降。 -
指针传递:
指针传递是拷贝传递了一个指针给函数来实现的,函数可通过对其解引用来访问与修改原始值,但修改指针指向的行为并不会影响到原指针,需要加一个引用。
指针传递只需赋值一份地址,而无需将大型对象复制进来,比值传递更加高效,但是需要注意内存的分配与释放,且指针可能指向无效内存,需要进行有效性检查。 -
引用传递:
引用传递是将原始的参数地址传给了函数,函数可以通过对引用的访问直接访问原变量。
引用传递同样不会创建对象的副本,且引用不能修改指向,使用更加方便简洁,不能传递空值,因而也无需验证引用的有效性。
函数内联
- 概念:
- 指在函数的返回值前再加上“inline”关键字(Tips:加了内联不一定就是内联,不加内联也不一定不是内联,太伟大了自动优化)
- 相当于把内联函数里面的内容写在调用内联函数处;
- 相当于不用执行进入函数的步骤,直接执行函数体;
- 相当于宏,却比宏多了类型检查,真正具有函数特性;
- 编译器一般不内联包含循环、递归、switch 等复杂操作的内联函数;
- 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
- 编译器处理步骤:
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
- 为什么要内联:
- 最初使用内联函数的目的是代替部分#define宏定义和部分普通函数,提高程序的运行效率
- 处理时机: 宏是预处理指令,在预处理的时候把所有的宏名用宏体来替换;内联函数是函数, 在编译阶段把所有调用内联函数的地方用其函数体直接插入。
- 安全性: 宏没有类型检查,无论对错都是直接替换;内联函数在编译时进行安全检查。
- 编写: 宏的编写只能写一行,且不能使用return控制流程等。
- 特性: 宏无法操作类的私有数据成员
- 内联函数与重定义:
- 多次定义内联函数不会报重定义错误,因为inline是一个弱符号
强弱符号
- 强符号: 默认函数和初始化了的全局变量,拥有确切的数据,变量有值
- 弱符号: 未初始化的全局变量,没有确切的数据,inline,声明等。
- 强弱符号处理规则:
- 不允许强符号被多次定义,也即不同的目标文件中不能有同名的强符号;如果有多个强符号,那么链接器会报符号重复定义错误。
- 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
- 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
-
内联函数优缺点:
优点:
- 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度。
- 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
- 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
- 内联函数在运行时可调试,而宏定义不可以。
缺点:
- 代码膨胀。内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。
- inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译。
- 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。
4.类
(1)Class与Struct的区别:
-
成员函数:
在面向过程的C中,struct被认为是一种数据类型,因而不能定义任何成员函数,否则编译会报错。
而在面向对象的C++中,数据和数据操作是一个整体,因而其结构体可以拥有成员函数,可以实现继承,多态。并且和类一样拥有构造与析构函数。 -
访问权限:
结构体的默认访问权限是public,类的默认访问权限是private。
二者都可以使用访问控制符来控制其可见性与访问权限。 -
继承上:
Class和struct可以混合继承,在不加继承关键字的情况下,默认为
Struct B : A //公有继承
Class C : A //私有继承 -
泛型:
Class和typename一起,用于模板类的声明中,而struct不行。 -
使用场景:
-
结构体的使用场景:
- 用于存储一组相关的数据,但没有复杂的操作和逻辑。
- 当数据的封装比行为更重要时,例如在处理图形、坐标、日期等数据时。
- 当你需要将数据序列化/反序列化为二进制或其他格式时。
- 作为轻量级的数据容器,适用于性能要求较高的情况。
-
类的使用场景:
- 当你需要封装数据并附加操作和行为时,类更适合,因为它允许你将数据和操作封装在一起。
- 在面向对象编程中,用于建模现实世界的对象,例如人、车辆、银行账户等。
- 当你需要使用继承和多态来实现代码的扩展和重用。
- 为了实现更复杂的数据结构,如链表、树、图等。
(2)Union和struct区别
- Struct的各成员拥有自己的内存,各自使用互不干涉,遵循内存对齐原则,其大小取决于所有的成员变量的大小,内存对齐后可能会更大。
- Union的各成员共用一块内存空间和内存首地址,并且同时只有一个成员可以得到这块内存的使用权,其大小取决于成员变量中最大的变量大小,且需要是所有变量大小的整数倍。
(3)类的生命周期相关函数:
新建的任何空类,C++都会创建6个默认函数:
-
构造函数:
- 创建时保证每个数据成员都有一个合适的初始值,并且在整个对象的生命周期中只调用一次。
- 名称与类名相同;
- 无返回值;
- 对象使用new实例化时编译器自动调用对应构造函数;
- 构造函数可以重载;
-
析构函数:
-
拷贝构造函数:
-
赋值符号重载:
-
移动构造函数:
-
移动赋值函数:
-
默认函数生成规律:
- 一旦指定一个要求传参的构造函数,就会阻止编译器生成默认构造函数
- 两种复制操作是彼此独立的,即显式声明其中一个,不会阻止编译器生成另外一个
- 两种移动操作并不彼此独立,显示声明其中一个,就会阻止编译器生成另外一个
- 一旦显式声明了复制操作, 就会阻止编译器默认生成移动操作
- 一旦显式声明了移动操作, 就会阻止编译器默认生成复制操作
- 一旦显式申明了析构函数, 就会阻止编译器默认生成移动操作
Tips:如果编译器默认生成的上述函数能满足你的需求, 但由于各个规则被抑制生成的话, 可以通过 = default (使用方法同纯虚函数的=0)来显式表达这个想法
- 大三律(Rule Of Three)
- 如果你声明了复制构造函数, 复制赋值运算符, 或析构函数的任何一个, 你就得同时声明所有这三个。这个思想源于: 如果有改写复制操作的需求, 往往意味着该类需要执行某种资源管理, 而这就意味着:
- 在一种复制操作中进行的任何资源管理, 也极有可能在另一种复制操作中也需要进行
- 该类的析构函数也会参与到该资源的管理中(通常是释放)
- 如果你声明了复制构造函数, 复制赋值运算符, 或析构函数的任何一个, 你就得同时声明所有这三个。这个思想源于: 如果有改写复制操作的需求, 往往意味着该类需要执行某种资源管理, 而这就意味着:
(4)深拷贝与浅拷贝
- **浅拷贝:**在执行默认的拷贝构造函数时,若对象有动态分配的内存,则在拷贝时,不会拷贝分区,只会拷贝指针,最后会导致两个指针指向相同的内存区,在释放其中一个后,另一个一旦访问或者重复释放都会发生错误。
- **深拷贝:**在拷贝时,会在堆区重新申请空间,进行拷贝操作,两个对象的指针会指向不同的内存,但这些内存的内容是相同的。
(5)多态的意义与实现:
-
静态多态与动态多态:
-
静态多态(编译时多态,也称为函数重载或模板多态):
-
静态多态是通过函数重载或模板特化来实现的。
-
在编译时确定调用哪个函数或模板的版本。
-
编译器根据函数或模板的参数类型进行静态绑定(静态解析),决定使用哪个函数或模板。
-
静态多态性在编译时确定,因此效率较高。
-
示例:函数重载、模板函数。
-
-
动态多态(运行时多态,也称为虚函数多态):
-
动态多态是通过基类和派生类之间的继承关系以及虚函数的使用来实现的。
-
在运行时确定调用哪个函数的版本。
-
通过基类指针或引用调用虚函数,根据指针或引用指向的实际对象的类型,决定调用哪个派生类的方法。
-
动态多态性在运行时确定,因此具有更大的灵活性。
-
示例:虚函数、纯虚函数、虚函数重写(覆盖)。
-
-
区别:
-
静态多态在编译时确定调用的函数版本,而动态多态在运行时确定调用的函数版本。
-
静态多态使用函数重载或模板特化,而动态多态使用虚函数和继承关系。
-
静态多态性具有较高的效率,因为在编译时就确定了调用的函数版本。而动态多态性提供了更大的灵活性,可以根据实际对象的类型在运行时选择调用的函数版本。
-
静态多态是通过静态绑定实现的,而动态多态是通过动态绑定(动态解析)实现的。
-
-
-
多态构成条件:
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数是虚函数,且必须完成对基类虚函数的重写
-
虚函数:
使用virtual关键字修饰,且虚函数只能是类中非静态的成员函数
子类重写父类虚函数,virtual,override -
协变:
子类的虚函数可以和父类的虚函数返回值不同,但子类返回值和父类返回值要构成继承关系 -
虚析构:
析构函数如不声明为虚函数,则在用父类指针析构时,会调用父类析构函数而忽略子类析构函数。因此需要将析构声明为虚析构,从而在析构上实现多态。 -
C++11:
- 在类声明后面加上final代表该类无法再被继承
- 在虚函数后面加上final代表该虚函数无法再被重写
- 使用override的虚函数编译器会检查该虚函数是否重写,如果父类没有这个虚函数会提示报错
-
重载,重写(覆盖),重定义:
- 重载: 函数名相同,参数列表不相同,满足前面的条件下返回值也可不相同。
- 重写(覆盖): 必须是虚函数,且是子类重写父类的虚函数,返回值,参数列表,函数名必须相同(协变除外)
- 重定义: 子类和父类的成员变量或者函数名相同,子类会隐藏父类对应成员。
-
抽象类:
在虚函数后面加上= 0变成纯虚函数,有纯虚函数的类就是抽象类,也叫做接口类。抽象类无法实例化,其子类不重写虚函数也不能实例化。
(6)虚函数
① 虚函数能否是内联函数:
- 虚函数可以是内联函数,inline可以修饰虚函数,但是如果虚函数表现多态性的时候不能内联
- 因为内联发生在编译期间,编译器会自主选择内联,而虚函数的多态性在运行期间,编译器无从得知运行期调用哪个代码。 Inline
- virtual唯一可以内联的时候是,编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或者引用时才会发生。
- 如果inline了之后虚函数还是有多态性,那就说明编译器阻止了内联,从而使虚函数仍然具有多态性。
② 虚函数表:
-
概念
-
相当于一个函数指针数组,C++中一个类存在虚函数,编译器就位为类生成一个虚函数表,存放的是这个类所有虚函数的地址,类对象的前四个字节会变成虚函数表vfptr的指针。
-
虚函数表是全局共享的元素,即全局仅有一个.
-
虚函数表类似一个数组,类对象中存储vptr指针,指向虚函数表。即虚函数表不是函数,不是程序代码,不肯能存储在代码段。
-
虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不再堆中。
-
-
虚函数表存在哪里:
虚函数表长度和内容编译期即确定,运行时全局不修改,不实例化,因此应存在于只读常量区;
定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表。- 扩展:
-
c/c++的内存分配
- 栈(stack):
又称堆栈,栈是由编译器自动分配释放,存放函数的参数值,局部变量的值等(但不包括static声明的变量,static意味着在数据段中存放变量)。除此之外,在函数被调用时,栈用来传递参数和返回值。由于栈的先进后出的特点,所以栈特别方便用来保存/恢复调用数据。其操作方式类似于数据结构中的栈。 - 堆(heap):
堆是用于存放进程运行中被动态分配的内存段,它的大小,并不固定,可动态扩张或缩放。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被提出(堆被缩减)。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。 - 全局数据区(静态区)(static):
全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域。 - 文字常量区:
常量字符串就是放在这里,程序结束后由系统释放。 - 程序代码区:
存放函数体的二进制代码
- 栈(stack):
-
C语言在编译和连接后,将分成代码段(Text)、只读数据段(ROData)和读写数据段(RWData)。在运行时,除了以上三个区域外,还包括未初始化数据段(BSS)区域和堆(Heap)区域和栈(Stack)区域。
- .bss BSS段(bss segment):
通常是指用来存放程序中**未初始化**的全局变量的一块内存区域。BSS段属于静态内存分配。 - .RW data数据段(data segment):
通常是指用来存放程序中**已初始化**的全局变量的一块内存区域。数据段属于静态内存分配。 - .RO data只读数据段:
只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。 - .text代码段(code segment/text segment):
通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行之前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。程序段为程序代码在内存中的映射,一个程序可以在内存中有多个副本。
- .bss BSS段(bss segment):
-
- 扩展:
-
类不实例化也会有虚函数表吗?
- 若类存在虚函数,编译器就会在类的头部使用四个字节创建一个虚函数表的指针vfptr,指向该类的虚函数表。
- 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以在编译时期确定。
- 虚函数表是属于类的,因此无论是否实例化对象,类都会拥有一份虚函数表。
- Base1类中有fun1和fun2两个虚函数,因此其会生成一张大小为2的虚函数表。
- 当实例化其对象后,所有对象的前四个字节都会指向该虚函数表。
-
虚函数的类的子类的虚函数表是什么样的
-
不重写父类虚函数,单纯继承的情况下的子类内存分布:
虚函数表指针永远会在最前端,并有一份和其父类相同的虚函数表 -
重写父类虚函数后:
其虚函数表内会将指向其父类的虚函数指针重新指向子类虚函数 -
添加了父类没有的虚函数后:
因为父类虚函数的索引只到其父类虚函数数量为止,因此在其后增加虚函数其也不知情,子类添加自己虚函数会直接加到继承的虚函数表后 -
多继承两个拥有虚函数的基类时
不同基类的虚函数指针都会保存在各自类的指针开头
子类自己的虚函数将会保存在第一个拥有虚函数的基类的虚函数表的后面 -
多继承两个,但只有第二个基类拥有虚函数时
Base2在内存中的布局将会提到前面,以保证虚函数表指针在最前
子类自己的虚函数也会相应的添加到该虚函数表后面 -
多继承两个,但只有子类拥有虚函数时
类成员顺序不改变,但虚函数表指针仍放置在最前,保存的是子类的虚函数
-
④ 虚基表:
- 虚继承的意义:
- 虚继承是解决C++多重继承的一种手段,若A派生了B,C两个类,而D又多继承了B,C,则在D内会存在两个A类的拷贝,不仅浪费了存储空间,还造成了在D中A的变量的二义性。
- 虚继承会将其继承的类放在本类的末尾,并在本类中放置一个虚基指针,指向其虚继承的类的开始位置。若有其他类继承多个类似的本类,则在其类中只会有一个本类的虚继承的父类,且每一个本类都会有一个虚基指针负责记录与父类的内存偏移。
⑤ 虚继承的内存分布:
- 普通继承的内存分布:
class A {
public:
int a1, a2;
};
class B : A {
public:
int b1, b2;
};
class C : A {
public:
int c1, c2;
};
class D : B, C {
public:
int d1, d2;
};
- 虚继承的内存分布:
class A {
public:
int a1, a2;
};
class B : virtual A {
public:
int b1, b2;
};
class C : virtual A {
public:
int c1, c2;
};
class D : B, C {
public:
int d1, d2;
};
- 拥有虚函数的虚继承的内存分布:
class A
{
public:
int a1, a2;
virtual void Func_a1() {};
virtual void Func_a2() {};
};
class B : virtual public A
{
public:
int b1, b2;
virtual void Func_b1() {};
virtual void Func_b2() {};
virtual void Func_a1() override {};
};
class C : virtual public A
{
public:
int c1, c2;
virtual void Func_c1() {};
virtual void Func_c2() {};
virtual void Func_a2() override {};
};
class D : public B, public C
{
public:
int d1, d2;
virtual void Func_d1() {};
virtual void Func_d2() {};
};
⑥ 构造函数调用虚函数:
在构造子类时调用父类的构造函数,而父类的构造函数调用了虚函数,此时即使该虚函数在子类中被重写,也不会发生多态行为。
一般来说,在构造父类对象时,子类数据往往还未初始化,如果调用子类的虚函数,可能会造成各种错误。
⑦ 虚析构函数的作用:
虚析构函数使得在删除指向子类对象的基类指针时,会先调用子类的析构函数来达到释放子类中堆内存的目的,防止了内存泄漏。
⑧ 虚函数表指针生命周期:
- 构造函数不可以是虚函数。因为类的虚函数表指针是在构造函数中初始化的,在虚表指针没有被正确初始化之前,我们不能调用虚函数。
- 构造函数和析构函数也不能调用虚函数,前者是因为虚表指针还没有被初始化,后者是因为虚表指针可能已经被析构了。
- 存在虚函数的类都有一个一维的虚函数表,简称虚表。类的每个对象都有一个指向虚表开始的虚表指针。虚表是和类对应的,虚表指针是和对象对应的。
(7)类的大小受什么影响:
-
有关因素:
普通成员变量,虚函数,继承(单一继承,多重继承,重复继承,虚继承) -
无关因素:
静态成员变量,静态成员函数和普通成员函数 -
空类:
空类理论大小为0,但空类可以实例化,必须在内存中占有一个位置,因此编译器为其优化为一个字节大小 -
一般类(内存对齐):
类内成员变量的不同声明顺序,会导致不同的内存构造模型,如下: -
虚函数单一继承:
会在类的顶部追加8个字节的虚函数表指针。 -
虚函数多继承:
会在每个有虚函数的父类的顶部追加8个字节的虚函数表指针,并且如果只有一个虚函数表指针,则一定在整个类的最前端。 -
虚继承:
会在普通继承的基础上多一个8字节的虚基指针,其派生类多继承时内部每个本类都会有这个指针,但基类只会有一个。 -
虚继承且有虚函数:
派生类中多继承的类和虚基类都会存有一个虚函数表指针(如果其有虚函数),同时多继承的类还会有虚基表指针。
5.内存管理
(1)内存分区
-
栈
存放函数的局部变量,函数参数,返回地址等,由编译器自动分配和释放 -
堆
动态申请的内存空间,由malloc/free和new/delete进行分配的内存块,由程序员控制分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。 -
全局区/静态存储区(.bss段和.data段)
存放全局变量和静态变量,程序运行结束操作系统自动释放,在C中,未初始化的放在.bss段中,初始化的放在.data段中,C++中不再区分。 -
常量存储区(.data段)
存放的是常量,不允许修改,程序运行结束自动释放。 -
代码区(.text段)
存放代码,不允许修改,但可以执行,编译后的二进制文件存放在这里。
(2)堆和栈的区别
-
申请方式上:
栈是系统自动分配释放,堆是程序员主动申请和释放。 -
申请后系统响应(分配过程):
- 分配栈空间,如果剩余空间大于申请空间则分配成功,否则分配失败(栈溢出)
- 分配堆空间,堆在内存中呈现的方式类似链表(记录空闲地址空间的链表),在链表上寻找一块大于申请空间的节点分配给程序,将该节点从链表中移除,大多数系统中该块空间的首地址是本次分配空间的大小,便于释放,若空间大于申请空间,则吧剩余空间再次链接到空闲链表上
-
大小空间不同:
栈是向低地址扩展的数据结构,是一块连续的内存区域,其栈顶地址和栈最大容量是系统预先规定好的,如果申请空间超出大小就会导致栈溢出。
堆是向高地址扩展的数据结构,是不连续的内存区域,因为系统是用链表来存储空闲内存地址的。堆的大小受限于计算机中有效的虚拟内存,因此堆获得的空间更灵活,且更大 -
申请和访问效率不同:
- 栈分配算法简单,结构紧凑,且其分配和释放都很高效,由编译器完成。而堆在分配和释放时都要使用malloc/free或者new/delete,在分配时在堆空间寻找足够大的空间,这些都会花费一定时间。
访问时间上,访问堆的一个具体单元,需要两次访问内存,第一次取得指针,第二次才是取得真正数据,而栈只需要访问一次。 - 栈有专门的寄存器,压栈出栈指令效率高,而堆需要操作系统动态调度,堆内存可能被调度在非物理内存中,或着申请不连续,造成内存碎片的问题。
- 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,而堆是C++函数库提供的,机制比较复杂,所以栈的分配效率比堆的效率高。
- 栈分配算法简单,结构紧凑,且其分配和释放都很高效,由编译器完成。而堆在分配和释放时都要使用malloc/free或者new/delete,在分配时在堆空间寻找足够大的空间,这些都会花费一定时间。
-
存放内容不同:
栈中存放的是局部变量,函数参数等。
堆中存放的是程序员动态申请的内存空间。
6.关键字
(1) 怎么把对象创建在堆上和栈上:
在函数内直接创建变量即在栈上,在函数调用结束时被自动释放。
通过new/delete或者malloc/free创建的对象在堆上,需要手动进行释放。
(2) New/delete和malloc/free的区别与底层原理
New底层:先调malloc申请堆内存空间,然后调用构造函数初始化内存空间,最后返回该类的指针
Delete底层:先执行析构函数,然后调用free释放堆内存空间
Malloc底层:申请分配一块堆内存,返回void指针
malloc:申请一块内存空间,且不会初始化
calloc:两个参数,元素数目和每个元素大小,会对空间进行初始化
realloc:更改已分配的内存空间,即更改由malloc函数分配的内存空间的大小
alloca:向栈申请内存,因此无需释放,alloc和afree以栈的方式进行存储空间的管理
Free底层:释放指定的堆内存空间
- 区别:
- New/delete比其malloc/delete除了分配空间外还会调用构造函数和析构函数对类对象进行初始化和清理。
- Malloc/free需要手动计算类型大小且返回值是void*,new/delete可以自己计算类型大小,并返回类的指针。
- 使用new创建对象数组,只能使用对象的无参构造函数,如 Obj* objects = new Obj[100];使用delete时,也需要加上[],如delete[] objects;因为数组会额外开辟四个字节大小来存储数组大小,delete只会从首地址直接开始删除,且只会删除一个指针大小。
- 共同之处:
他们都只把指针指向的内存释放掉了,并没有把指针本身释放,因此free/delete之后,都应将指针设为nullptr。
(3)New/delete和malloc/free混用:
New会对对象进行初始化,而free不会调用析构函数,如果有需要释放的内存会造成内存泄漏
Malloc不会对对象进行初始化,调用delete可能会释放一些没有被初始化的指针,造成野指针错误。
(4)用new将对象创建在栈上(只允许在栈上创建对象):
如果只允许在栈上创建可以简单的将new和delete运算法重载并设为私有即可。
如果要用new也创建出栈对象则可以用alloca在栈上申请一个内存,然后调用new(对象指针) 构造函数();语句来创建一个栈对象。(即使用placement new操作符)
(5)只允许在堆上创建对象:
将构造函数声明为私有,然后使用一个静态方法来构建并返回一个堆上的对象,一切创建该对象的行为都要访问该静态成员函数即可。
将析构函数声明为私有,因为编译器需要管理对象的整个生命周期,所以若编译器发现该栈对象析构函数无法访问,就不会在栈上为其分配内存。如果要释放,可以新建一个函数作为释放其的接口。
(6)new了一个int类型的数组,new的前后发现进程中的内存占用是没有变化的,可能是什么原因导致的?
在某些编译器或者优化设置下,如果没有使用该数组或者没有对其进行任何操作,编译器可能会进行优化,将其视为无效代码,并在编译过程中将其删除,从而在运行时不再分配内存空间,进程中内存占用也不会有变化。
其次如果申请的内存大小无法满足导致申请失败,也有可能出现进程中内存占用没有变化。
(7)Delete和delete[]不对应会有什么错误
使用了new[]但是使用delete进行释放,会导致实际只释放了数组中的第一个元素,造成内存泄漏
使用了new 但是使用delete[]进行释放,new[]的前四个字节存放数组大小,delete[]根据前4个字节的数据,来决定调用几次delete,new的前4个字节数据存储的并非数组大小,因而delete[]会随机调用几次,delete掉不该delete的区域就会报异常。
(8)Sizeof
- 普通数据:
数据类型 | 32位 | 64位 |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 8 |
float | 4 | 4 |
long long | 8 | 8 |
double | 8 | 8 |
long double | 10/12 (有效10位,对齐为12位) | 10/16 (有效10位,对齐为16位) |
各种指针(如 char*) | 4 | 8 |
-
数组数据
直接传数组名会返回数组元素数量*元素大小
对数组名解引用会返回第一个数组元素的大小
内存对齐
1个字节的变量可以放在任意地址上 char
2个字节的变量放在2的整数倍地址上 short
4个字节的变量放在4的整数倍的地址上 float,int
8个字节的变量放在8的整数倍的地址上 long long,double -
影响因素:
变量声明顺序(变量排列顺序)
__attribute__((packed)):取消变量对齐(仅gcc支持)
#pragma pack (n):让变量强制按照 n 的倍数进行对齐,并会影响到结构体结尾地址的补齐
位域占位
7.头文件
(1)头文件重复包含解决办法:
-
使用
#ifndef [头文件名]
#define [头文件名]
[头文件内容]
#endif -
使用#pragma once
** (2)两个头文件互相引用解决办法 **
- 使用前置声明,如果需要在两个类中互相持有对方的指针可使用前置声明
- 如果又需要依赖对方类内的具体行为,则将一些东西抽象为接口或者组件等等,让对方类去实现或拥有,此时只需要借助获取接口或者组件进行实现而不需要获取对方的类,这样就可以无需扩展基类来保证不需要引用对方的类