0、c++11新特性
- auto关键字:编译器可以根据变量的初始值自动推导出其类型,这在编写模板代码或涉及复杂类型时特别有用。然而,需要注意的是,auto不能用于函数传参以及数组类型的推导。
- nullptr:这是一个新的空指针常量,用于替代C++98中的NULL。与NULL相比,nullptr具有更强的类型安全性,可以避免一些潜在的错误。
- lambda表达式:lambda表达式允许定义匿名函数对象,可以捕获其所在作用域的变量,并用于各种需要函数对象的场合,如算法、事件处理等。
- 右值引用和移动语义:C++11引入了右值引用的概念,并基于它实现了移动语义。这可以消除不必要的对象拷贝,提高性能,特别是在处理临时对象或大型对象时。
- 智能指针:C++11新增了std::shared_ptr、std::unique_ptr和std::weak_ptr等类型的智能指针,用于自动管理动态分配的内存,减少内存泄漏的风险。
- 范围for循环:新的for循环语法简化了对容器或数组的遍历,使得代码更加简洁易读。
- 初始化列表:可以使用初始化列表来对类进行初始化,提高了初始化的效率和安全性。
- override和final关键字:override用于明确表示某个成员函数是重写基类的虚函数,而final用于禁止某个类被继承或某个虚函数被重写。
1、new 和 malloc 的区别
- new 是操作符,而 malloc 是函数;
- 使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而 malloc 则需要显式地指出所需内存的尺寸;
- new 分配失败的时候会直接抛出异常,malloc 分配失败会返回 NULL;
- 对于非简单类型,new 在分配内存后,会调用构造函数,而 malloc 不会;
- new 分配成功后会返回对应类型的指针,而 malloc 分配成功后会返回 void * 类型;
- malloc 可以分配任意字节,new 只能分配实例所占内存的整数倍数大小;
- new 可以被重载,而 malloc 不能被重载;
- new 操作符从自由存储区上分配内存空间,而 malloc 从堆上动态分配内存;
- 使用 malloc 分配的内存后,如果在使用过程中发现内存不足,可以使用 realloc 函数进行内存重新分配实现内存的扩充,new 没有这样直观的配套设施来扩充内存。
2、面向对象的三大特征
面向对象的三大特征是:封装、继承、多态。
1. 封装 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。 C++通过 private、protected、public 三个关键字来控制成员变量和成员函数的访问权限。封装的好处:隐藏实现细节,提供公共的访问方式;提高了代码的复用性;提高了安全性。
2. 继承 C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。继承的好处:提高代码的复用性;提高代码的拓展性;是多态的前提。
3. 多态 在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。
3、浅拷贝和深拷贝
1. 浅拷贝 浅拷贝又称为值拷贝,将源对象的值拷贝到目标对象中,如果对象中有某个成员是指针类型数据,并且是在堆区创建,则使用浅拷贝仅仅拷贝的是这个指针变量的值,也就是在目标对象中该指针类型数据和源对象中的该成员指向的是同一块堆空间。这样会带来一个问题,就是在析构函数中释放该堆区数据,会被释放多次。默认的拷贝构造函数和默认的赋值运算符重载函数都是浅拷贝。
2. 深拷贝 深拷贝在拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样指针成员就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了拷贝的目的,还不会出现问题,两个对象先后去调用析构函数,分别释放自己指针成员所指向的内存。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。
4、 C++ 中的多态
多态分为静态多态和动态多态。
1. 静态多态 是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等。
2. 动态多态 是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。动态多态的实现原理 当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构, 虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。
5、虚函数的实现原理
1. 虚函数的作用主要是实现了动态多态的机制。动态多态,简单的说就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
2. 虚函数实现原理 编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表。虚函数表中存储的是类中的虚函数的地址。如果派生类重写了基类中的虚函数,则派生类对象的虚函数表中保存的是派生类的虚函数地址,如果派生类没有重写基类中的虚函数,则派生类对象的虚函数表中保存的是父类的虚函数地址。
6、什么是纯虚函数,有什么作用
1. 纯虚函数是一种特殊的虚函数,它的格式是:虚函数不给出具体的实现,也就是后面没有大括号实现体,而在后面加上 "=0"
2. 很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。
3. 如果一个类中有纯虚函数,那么这个类也被称为抽象类。这种类不能实例化对象,也就是不能创建该类的对象。除非在派生类中完全实现基类中所有的纯虚函数,否则派生类也是抽象类,不能实例化对象。
7、虚函数和纯虚函数的区别
1.虚函数可以有具体的实现,纯虚函数没有具体的实现。 对于虚函数来说,父类和子类都有各自的版本,由多态方式调用的时候动态绑定。 有纯虚函数的类称为抽象类,有纯虚函数的类不能实例化,派生类必须实现纯虚函数才可以实例化,否则也是抽象类。
2.虚函数是 C++ 中用于实现动态多态的机制。 很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。
8、虚析构函数有什么作用
虚析构函数的主要作用是为了防止遗漏资源的释放,防止内存泄露。如果基类中的析构函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则这时就会造成内存泄露。
9、重载,复写,隐藏的区别
1. 重载:在同一作用域中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载,与返回值类型无关。
2. 重写:指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部函数。
3. 派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。
10、什么情况会调用拷贝构造,什么时候会调用赋值操作
1. 拷贝构造函数的调用时机 - 用一个对象初始化另外一个对象 - 对象以值传递的方式传递给函数参数 - 函数局部对象以值传递的方式从函数返回
2. 赋值操作的调用时机 - 将一个对象赋值给另外一个对象
11、虚函数可以是内联函数吗
1. 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
2. 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联。
3. inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
12、 C++ 中的四种类型转换
上行转换(把派生类的指针或引用转换成基类表示) 下行转换(把基类指针或引用转换成派生类表示)
1. static_cast 静态转换 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换, 进行上行转换是安全的 - 进行下行转换时,由于没有动态类型检查,所以是不安全的。
2. dynamic_cast 动态转换 用于类层次间的上行转换和下行转换 ,进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的 在进行下行转换时,dynamic_cast 具有类型检查的功能,比 static_cast 更安全。
3. const_cast 常量转换 用来修改类型的const属性 常量指针被转化成非常量指针,并且仍然指向原来的对象 常量引用被转换成非常量引用,并且仍然指向原来的对象
4. reinterpret_cast 重新解释转换 这是最不安全的一种转换机制,最有可能出问题 主要用于将一种数据类型从一种类型转换为另一种类型,它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针
13、STL 中有哪些常见的容器
STL 中容器分为顺序容器、关联式容器、容器适配器三种类型
1. 顺序容器 容器并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list。
- vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
- deque:双向队列 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能。
-list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
2. 关联式容器 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、multiset、map、multimap。
- map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。
3. 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue、priority_queue。
- stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出。
- queue:队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出。
- priority_queue:优先级队列 内部维持某种有序,然后确保优先级最高的元素总是位于头部,最高优先级元素总是第一个出列。
14、STL 容器用过哪些,查找的时间复杂度是多少,为什么?
容器底层实现方式及时间复杂度分别如下:
1. vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
2. deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
3. list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1)
4. map、set、multimap、multiset 采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN)
5. unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1),最坏情况O(N) 注意:容器的时间复杂度取决于其底层实现方式。
15、vector 和 list 的区别,分别适用于什么场景?
1. 区别
- vector 底层实现是数组,list 是双向链表
- vector 支持随机访问,list 不支持
- vector 是顺序内存,list 不是
- vector 在中间节点进行插入删除会导致内存拷贝,list 不会
- vector 一次性分配好内存,不够时才进行扩容,list 每次插入新节点都会进行内存申请
- vector 随机访问性能好,插入删除性能差,list 随机访问性能差,插入删除性能好
2. 适用场景
- vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用 vector。
- list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。
16、迭代器失效原因,有哪些情况
1. 序列式容器迭代器失效 对于序列式容器,例如 vector、deque,由于序列式容器是组合式容器,当当前元素的迭代器被删除后,其后的所有元素的迭代器都会失效,这是因为 vector、deque都是连续存储的一段空间,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。解决:erase 返回下一个有效的迭代器。
2. 关联式容器迭代器失效 对于关联容器,例如如 map、 set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。
17、vector 的扩容机制,扩容以后,它的内存地址会变化吗?
当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间; 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。
18、deque 的实现原理
deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。 既然 deque 是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。
deque 采取一块所谓的 map(不是 STL 的 map 容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是 deque的存储空间的主体。
19、set 的实现原理
set 底层使用红黑树实现,一种高效的平衡检索二叉树。 set 容器中每一个元素就是二叉树的每一个节点,对于 set 容器的插入删除操作,效率都比较高,原因是二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。 对 set 进行插入删除操作 都不会引起迭代器的失效,因为迭代器相当于一个指针指向每一个二叉树的节点,对 set的插入删除并不会改变原有内存中节点的改变。 set 中的元素都是唯一的,而且默认情况下会对元素进行升序排列。不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。
20、map 的实现原理
map 实现原理 map 内部实现了一个红黑树,红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。
21、红黑树的特性,为什么要有红黑树
特性:
1、具有二叉查找树的特点;
2、根节点是黑色的;
3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据;
4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。
平衡树要求每个节点的左子树和右子树的高度差相等,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣。
23、C 语言和 C++ 语言的区别
1. C 语言是面向过程的语言,而 C++ 支持面向对象,所以 C 语言自然没有面向对象的封装、继承、多态等特性,也不支持面向对象的一些语法;
2. C++ 支持函数重载,C 语言不支持;
3. C 程序中如果函数没有任何参数需要将参数定义为 void 以此来限定函数不可传递任何参数,如果不进行限定让参数表默认为空其意义是可以传递任何参数,在 C++ 中,不带参数的函数表示函数不能传递任何参数;
4. C 语言 struct 中不能有函数,而 C++ 语言 struct 中可以有函数;
5. C 语言函数参数不支持默认值,而 C++ 语言支持参数默认值;
6. C++ 语言支持内联函数,而 C 语言不支持;
7. C++ 语言支持引用,而 C 语言不支持;
8. C 语言采用 malloc 和 free 函数动态申请和释放内存,而 C++ 使用 new 和 delete 运算符;
9. C 语言中只有局部和全局两个作用域,而 C++ 中有局部、全局、类、名称空间作用域。
24、C++ 和 C 中 struct 的区别以及和 class 的区别
C++ 和 C 中 struct 的区别: 1. C 的结构体不允许有函数存在,C++ 的结构体允许有内部成员函数,并且允许该函数是虚函数 2. C 的结构体内部成员不能加权限,默认是 public,而 C++ 的结构体内部成员权限可以是 public、protected、private,默认 public 3. C 的结构体是不可以继承,C++ 的结构体可以从其它的结构体或者类继承 4. C 中的结构体不能直接初始化数据成员,C++ 中可以 5. C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名后直接使用,而 C++ 中使用结构体可以省略
struct 和 class 的区别: 1. struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装 2. struct 中默认访问控制权限是 public,而 class 中默认的访问控制权限是 private。 3. 在继承关系中,struct 默认是公有继承,而 class 是私有继承 4. class 关键字可以用于定义模板参数,而 struct 不能
25、static 关键字的作用
可以用来修饰局部变量、全局变量、成员变量、函数和成员方法。主要作用有:限制数据的作用域、延长数据的生命周期、修饰成员可以被该类所有对象共享。
1. 限制数据的作用域(隐藏) 所有没有加 static 的全局变量和函数都具有全局可见性,其它源文件中也可以访问。被 static 修饰的全局变量和函数只能在当前源文件中访问,其它源文件访问不了,
2. 延长数据的生命周期 普通的局部变量出了作用域就会释放,而静态变量存储在静态区,知道程序运行结束才会释放。
3. 静态成员被该类所有对象共享, static 关键字可以修饰类中的成员变量和成员方法,被称为静态成员变量和静态成员方法,静态成员拥有一块单独的存储区,不管创建多少个该类的对象,所有对象都共享这一块内存。静态成员本质上属于类,可以通过类名直接访问。
4.静态成员函数中不能访问普通的成员变量,只能访问静态成员变量,并且在静态成员函数中没有 this 指针。
26、const 的用法
1. 用在变量身上,表示该变量只读,不能对它的值进行修改
2. 结合指针一起使用 const int * p 是常量指针,表示指针变量 p 所指向的内容不能修改,指针变量 p 的内容可以修改; int * const p 是指针常量,表示指针变量 p 的内容不能修改,指针变量 p 所指向的内容可以修改; const int * const p 表示指针变量 p 的内容和所指向的内容都不可以修改。
3. const 用于函数参数 const 用于形参时说明形参在函数内部不能被改变
4. 在类中修饰成员方法,防止在方法中修改非 static 成员
5. const 修饰类的成员变量 如果 const 修饰的是非静态的成员变量,可以在构造函数中对该变量进行初始化;如果 const 修饰的是静态的成员变量,则需要在类外对该变量进行初始化。
27、const 和 define 的区别
1. const 生效于编译阶段,而 define 生效于预处理阶段;
2. define只是简单的字符串替换,没有类型检查,而 const 有对应的数据类型,编译器要进行判断的,可以避免一些低级的错误;
3. 用 define 定义的常量是不可以用指针变量去指向的,用 const 定义的常量是可以用指针去指向该常量的地址的;
4. define 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大,const 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝;
5. 可以对 const 常量进行调试,但是不能对宏常量进行调试。
28、指针和引用的区别
1.指针是一种数据类型,用于保存地址类型的数据,而引用可以看成是变量的别名。
2. 引用不可以为空,当被创建的时候必须初始化,而指针变量可以是空值,在任何时候初始化;
3. 指针可以有多级,但引用只能是一级;
4. 引用使用时无需解引用(*),指针需要解引用;
6. 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了;
7. sizeof 引用得到的是所指向的变量(对象)的大小,而 sizeof 指针得到的是指针变量本身的大小;
8. 指针作为函数参数传递时传递的是指针变量的值,而引用作为函数参数传递时传递的是实参本身,而不是拷贝副本;
9. 指针和引用进行++运算意义不一样。
29、C++ 的内存管理
1. 代码区 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
2. 未初始化数据区 加载的是可执行文件 BSS 段,位置可以分开也可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
3. 已初始化数据区(全局初始化数据区/静态数据区) 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
4. 栈区 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
5. 堆区 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序,用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
30、堆和栈的区别
堆 | 栈 | |
管理方式 | 堆中资源由程序员控制(容易产生memory leak) | 栈资源由编译器自动管理,无需手工控制 |
内存管理机制 | 系统有一个记录空闲内存地址的链表,当系统收到程序申请时,遍历该链表,寻找第一个空间大于申请空间的堆结点,删 除空闲结点链表中的该结点,并将该结点空间分配给程序(大多数系统会在这块内存空间首地址记录本次分配的大小,这样delete才能正确释放本内存空间,另外系统会将多余的部分重新放入空闲链表中) | 只要栈的剩余空间大于所申请空间,系统为程序提供内存,否则报异常提示栈溢出。(这一块理解一下链表和队列的区别,不连续空间和连续空间的区别,应该就比较好理解这两种机制的区别了) |
空间大小 | 堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit 系统理论上是4G),所以堆的空间比较灵活,比较大 | 栈是一块连续的内存区域,大小是操作系统预定好的,windows下栈大小是2M(也有是1M,在 编译时确定,VC中可设置) |
碎片问题 | 对于堆,频繁的new/delete会造成大量碎片,使程序效率降低 | 对于栈,它是有点类似于数据结构上的一个先进后出的栈,进出一一对应,不会产生碎片。(看到这里我突然明白了为什么面试官在问我堆和栈的区别之前先问了我栈和队列的区别) |
生长方向 | 堆向上,向高地址方向增长。 | 栈向下,向低地址方向增长。 |
分配方式 | 堆都是动态分配(没有静态分配的堆) | 栈有静态分配和动态分配,静态分配由编译器完成(如局部变量分配),动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。 |
分配效率 | 堆由C/C++函数库提供,机制很复杂。所以堆的效率比栈低很多。 | 栈是其系统提供的数据结构,计算机在底层对栈提供支持,分配专门 寄存器存放栈地址,栈操作有专门指令。 |
31、-内存泄露,如何检测
1. 内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
2. 避免内存泄露的方法主要就是要有良好的编码习惯,动态开辟内存空间,及时释放内存。也可以采用智能指针来避免内存泄露。
3. 可以采用静态分析技术、源代码插装技术等进行检测。常见的一些检测工作有:LCLink、ccmalloc、Dmalloc、Electric Fence、Leaky、LeakTracer、MEMWATCH、Valgrind、KCachegrind等等。