一、 C++代码,内存等问题
1. C++的内存分区
C/C++程序编译时内存分为5大存储区
- 栈区(stack): 主要存放函数参数以及局部变量,由系统自动分配释放。其操作方式类似于数据结构中的栈。
- 堆区(heap): 由用户通过malloc/new手动申请,手动释放。分配方式类似于链表。
- 全局/静态区(static): 全局变量和静态变量存储是放在一起的,在程序编译时分配,程序结束后由系统释放。
- 字符串常量区: 存放常量字符串,程序结束后由系统释放。
- 程序代码区: 存放函数体(类成员函数、全局函数)的二进制代码。
int a = 0; //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b;//栈
char s[] = "bb"; //栈
char *p2; //栈
char *p3 = "123"; //其中"123\0"是常量区, p3在栈区
static int c = 0; //全局区
p1 = (char*)malloc(10); //10个字节区域在堆区
strcpy(p1, "123"); //"123\0"在常量区,编译器 可能 会优化为和p3的指向同一块区域
}
C/C++内存分配有三种方式:
(1) 从静态存储区域分配。内存在程序编译时已经分配好,这块内存在程序整个运行期间都存在。如全局变量,static变量。
(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,在函数执行结束时,这些存储单元自动被释放。
(3)从堆上分配, 亦称动态内存分配。程序在运行时用malloc或new申请任意多少内存,程序员自己负责何时用free或delete释放内存。
动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间而没有手动释放,则运行程序会出现内存泄漏。
另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
2. 堆和栈的区别
- 空间大小
栈的空间是连续的,空间大小通常是预先规定好的,即栈顶地址和最大空间是确定的
堆的内存空间是不连续的,由一个记录空间的链表负责管理,因此内存空间几乎没有限制。 - 管理方式
栈由编译器自动分配和释放,而堆需要程序员来手动分配和释放,若忘记释放,容易产生内存泄漏。 - 生长方向
栈向着内存地址减小的方向生长,这也是为什么栈的内存空间是有限的
堆是向着内存地址增大的方向生长。堆的大小受限于计算机系统中有效的虚拟内存。 - 碎片问题
由于栈内存空间是连续的,先进后出的方式保证不会产生零碎的空间
而堆分配方式是每次在空闲链表中遍历到的第一个大于申请空间的节点,每次分配的空间大小一般不会正好等于申请的内存大小,频繁的new操作势必会产生大量的空间碎片。 - 分配效率
-栈由系统自动分配,速度较快。但程序员是无法控制的。
堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
3. malloc/free 、new/delete区别
- new 是运算符, malloc()是一个库函数
- new 会调用构造函数, malloc 不会
- new 返回指定类型指针, malloc 返回void*指针,需要强制类型转换
- new 会自动计算需分配的内存空间, malloc 不行
- new 可以被重载, malloc不能
- C 中只能用malloc/free 管理动态内存
4. 常见的内存错误及对策
(1) 内存尚未分配成功,却使用了它;
解决
: 在使用内存之前检查指针是否为NULL。 如果指针p是函数的参数,那么在函数入口使用assert(p!=NULL)
进行检查。
assert: assert宏的原型定义在<assert.h>中,其作用是如果它的条件返回错误,则终止程序执行。在调试结束后,可以通过在包含#include <assert.h>的语句之前插入
#define NDEBUG
来禁用assert调用
如果是用malloc或者new申请的,应该用if(p == NULL)
或者if(p != NULL)
来进行防错处理。
(2) 内存分配虽然成功,但是尚未初始化就引用它;
错误原因: 一是没有初始化的观念, 二是误以为内存的缺省初值全为0, 导致引用初值错误(如数组)。
解决
: 尽量赋初值。
(3) 内存分配成功并初始化, 但是超过了内存的边界; 常为数组越界。
(4) 忘记释放内存,造成内存泄漏;
含有这一类错误的函数每次被调用都会丢失一块内存,开始时内存充足,看不到错误, 但终有一次程序死掉,报告内存耗尽。
(5) 释放了内存却继续使用它
产生原因:
- 程序中的对象调用关系过于复杂,难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构, 从根本上解决对象管理混乱局面。
- 函数return语句写错了,注意不要返回指向"栈内存"的指针或者引用, 因为该内存在函数体结束时理论上被自动销毁。
- 使用free或者delete释放内存后,没有将指针设置为NULL,导致产生了野指针。所以, 用free或者delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
5. 内存泄漏怎么产生的?如何避免?
- 内存泄露一般是指堆内存的泄露, 也就是程序在运行过程中动态申请内存空间,不再使用后没有及时释放内存,导致那块内存不能再次使用。
- 更广义的内存泄露还包括未对系统资源的及时释放, 比如句柄、socket等没有使用响应的函数释放掉,导致系统资源浪费。
6. 32位,64位系统中,各种常用内置数据类型占用的字节数?
- 32位
类型 | 字节数 |
---|---|
char | 1 |
*(指针变量) | 4 |
short int | 2 |
int | 4 |
unsigned int | 4 |
float | 4 |
double | 8 |
long | 4 |
unsigned long | 4 |
long long | 8 |
- 64位
类型 | 字节数 |
---|---|
char | 1 |
*(指针变量) | 8 |
short int | 2 |
int | 4 |
unsigned int | 4 |
float | 4 |
double | 8 |
long | 8 |
unsigned long | 8 |
long long | 8 |
二、 一些操作问题
7. 当i是一个整数的时候i++和++i哪个更快?它们的区别是什么
基本区别:
i++
: 先在i所在的表达式中使用i的当前值,然后再让i+1
++i
: 先让i+1, 然后在i所在的表达式中使用i的新值
本质区别:
++i
操作除i之外不涉及新的(隐含的)操作数, 而i++
则在i之外还涉及一个新的(隐含的)操作数。 所以后缀式产生了额外的开销,因此引起效率低。 所以尽可能使用前缀式自增自减。
8. 防止头文件被重复包含
- 问题重现
某个文件引用了A.h和B.h, 而在B.h中也引用了A.h, 则A.h就被重复包含了。 - 解决方案
- 采用条件编译
#ifndef XXXXX
#define XXXXX
…
#endif
- 添加杂注
#pragma once
- 采用条件编译
- 两者比较
#ifnde
方式依赖于宏名字不能冲突, 会覆盖宏名字相同的头文件,导致编译器找不到,#pragma once
则由编译器提供保证, 仅保证该文件被包含一次,但如果该头文件存在多个拷贝, 不能保证它们不被重复包含。- 方法一由语言支持所以移植性好, 方法二可以避免名字冲突
三、 容器,vector问题
9. vector的size(), resize(), reserve() 和capacity() 的区别?
size: 容器目前实际存在元素个数
resize: 重新确定容器大小,若resize之后的大小比原来小,相当于删除后面的元素
capacity: 容量, 说明至少添加多少元素才会使容器重新分配内存
reverse: 重新分配空间, 会在必要的时候使容器内部缓冲区扩充至一个更大的容量, 以确保至少能满足你所指出的空间。
10. vector、map、multimap、unordered_map、unordered_multimap的底层数据结构,以及几种map容器如何选择
- 底层数据结构
vector基于数组, map, multimap基于红黑树, uordered_map, unordered_multimap基于哈希表。
- 根据应用场景进行选择
- map/unordered_map 不允许重复元素
- multimap/unordered_multimap 允许重复元素
- map/multimap 底层基于红黑树,元素自动有序,且插入、删除效率高
- unordered_map/unordered_multimap 底层基于哈希表,故元素无序,查找效率高。
11. C++vector与list区别
- vector: 和数组类似,拥有一段连续空间,并且起始地址不变,能高效随机存取,时间复杂度为O(1). 但因为内存空间连续,在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为O(n). 当数组中内存空间不够时, 会重新申请一块内存空间进行内存拷贝。
- list : 由双向链表实现,因此内存空间是不连续的,只能通过指针访问数据,所以list随机存取非常没有效率,时间复杂度为O(n), 但由于链表的特点,能高效地进行插入和删除。
12. vector扩容原理说明
- 新增元素: vector通过一个连续的数组存放元素, 如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新的元素。
- 对vector的任何操作,一旦引起空间重新配置,指向原vector的所有迭代器就都失效了。
- 初始时刻vector的capacity为0, 插入第一个元素后capacity增加为1
- 不同编译器实现的扩容方法不一样, VS2015中以1.5倍扩容, GCC以2倍扩容
1) vector在push_back以成倍增长可以在均摊后达到O(1)的事件复杂度,相对于增长指定大小的O(n)时间复杂度更好。2) 为了防止申请内存的浪费, 现在使用较多的有2倍和1.5倍的增长方式,而1.5倍的增长方式可以更好的实现对内存的重复利用。
四、 结构体问题
13. struct与class的区别?
- 默认继承权限: struct默认为公有继承(public), class默认为私有继承(private)
- 默认访问权限: struct默认为public, class默认为private
- 大括号初始化:
- 在C中struct是一种数据类型,只能定义数据成员, 不能定义函数, 所以C中struct可以直接使用大括号初始化。
- 在C++中, struct可以被继承,可以包含成员函数,也可以实现多态。
- 当stuct和class中都定义了构造函数,就不能用大括号初始化
- 若没有定义构造函数, struct可以使用大括号初始化, 而class只有当所有成员及函数为public时,可以用大括号初始化。
- 所以struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。
14. 结构体内存对齐问题?结构体/类大小的计算?
注: 内存对齐是看类型而不是看总的字节数
结构体内存对齐规定: 结构体的总大小为结构体最宽基本类型成员大小的整数倍。
1. 首先找结构体中基本类型字节数最大为多少
2. 对各个元素逐个进行补位操作, 若上一个元素补位字节数大于等于下一个元素类型的字节数,则下一个类型不用补位
对于结构体中包含联合体的:
1. 首先判断联合体的大小:
大小足够容纳最宽的成员; 大小能够被其包含的所有基本数据类型的大小所整除。
2. 找出结构体中基本类型字节数最大为多少
3. 对每个元素进行补位操作,如上。 对于联合体也要进行补位,使其大小为结构体中最大基本类型字节数的倍数
五、 关键字或函数的区别
15. inline, decltype,volatile,static关键字的作用?使用场景?
-
inline : 为解决一些频繁调用的小函数大量消耗栈空间(栈内存)问题,引入了inline修饰符,表示为内联函数。
- 内联函数与普通函数的区别:
内联函数和普通函数最大的区别在于内部的实现方面,当普通函数在被调用时,系统首先跳跃到该函数的入口地址,执行函数体,执行完成后,再返回到函数调用的地方,函数始终只有一个拷贝; 而内联函数则不需要进行一个寻址的过程,当执行到内联函数时,此函数展开(很类似宏的使用),如果在 N处调用了此内联函数,则此函数就会有N个代码段的拷贝。
- 内联函数与普通函数的区别:
-
decltype : 从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的值类型。
-
volatile : volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
-
static : static函数和普通函数的最大的区别在于作用域方面,static函数限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数,默认是extern的,也就是说,可以被其它代码文件调用该函数。同时static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。
如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。
16. const与#define的区别
#define | const | |
---|---|---|
编译预处理 | 在预处理阶段进行替换 | 在编译时确定其值 |
类型检查 | 无类型, 不进行类型安全检查, 可能会产生意想不到的错误 | 有数据类型, 编译时会进行类型检查 |
内存空间 | 不分配内存, 给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大 | 在静态存储区中分配空间, 在程序运行过程中内存中只有一个拷贝 |
17. 关键字static、const、extern作用,类里面static和const可以同时修饰成员函数吗?
18. 深拷贝与浅拷贝的区别?
-
什么时候用到拷贝函数
a. 一个对象以值传递的方式传入函数体
b. 一个对象以值传递的方式从函数体返回
c. 一个对象需要通过另外一个对象进行初始化 -
什么叫深拷贝,什么叫浅拷贝
a. 浅拷贝: 浅拷贝类似于定义了一个别名,仍然指向该块内存, 与被拷贝者指向同一块内存
b. 深拷贝: 深拷贝就是 开辟了一块新的内存,把值复制过来, 与被拷贝者指向不同内存
c. 浅拷贝问题: 若是对一个被new的指针进行浅拷贝,则在释放(delete)时可能会对这块内存重复释放。
当对象中存在指针成员时,除了在复制对象时需要考虑自定义拷贝构造函数,还应该考虑以下两种情形:
1.当函数的参数为对象时,实参传递给形参的实际上是实参的一个拷贝对象,系统自动通过拷贝构造函数实现;
2.当函数的返回值为一个对象时,该对象实际上是函数内对象的一个拷贝,用于返回函数调用处。
3.浅拷贝带来问题的本质在于析构函数释放多次堆内存,使用std::shared_ptr,可以完美解决这个问题。