2018秋招面试问题(四、C++基础问题)

注:面试过程中整理的学习资料,如有侵权联系我即刻删除

目录

C++有哪些常见的内存错误?

C++中内存泄露的几种情况和解决方法。

默认的拷贝构造函数是怎么造成内存泄露的?

如何检测内存泄露?

如何避免内存泄露?

shared_ptr的风险

weak_ptr是如何辅助shared_ptr的?

weak_ptr 弱指针 弱在哪里?

using namespace std的作用

如何发现死循环?

四核cpu如何跑满?

具体说明STL是如何实现vector的?

vector中erase和remove的区别?

vector通过下标访问,越界访问会有什么现象?

C++ STL中vector内存用尽后,为啥每次扩容是两倍增长,而不是3倍或其他倍数?

map使用下标去访问不存在的key值,会有什么现象?

map和vector什么时候使用map,什么时候使用vector?

vector、map/mutimap、unordered_map/unordered_multimap的底层数据结构,以及几种map容器如何选择?

迭代器iterator是什么?

如何使用迭代器iterator来对容器进行删除?

迭代器失效的情况

关于容器和迭代器的用法和相关知识点


C++有哪些常见的内存错误?

  1. 内存分配未成功,却使用了它。这种时候最好加个if(p==NULL)的判断
  2. 内存分配虽然成功,但是尚未初始化就引用它。
  3. 内存分配成功并且已经初始化,但操作越过了内存的边界。
  4. 忘记了释放内存,造成内存泄露。
  5. 释放了内存却继续使用它。

C++中内存泄露的几种情况和解决方法。

内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况,从而造成内存的浪费。

  1.  没有匹配地调用new和delete。
  2.  释放对象数组时(new []),delete没有加[]。
  3. 缺少拷贝构造函数。
  4.  没有将基类的析构函数定义为虚函数。当基类指针指向子类对象,如果基类析构函数不为虚函数,那么子类的析构函数不会被调用,子类资源没有正确释放,造成内存泄露。

 野指针也会造成内存泄露。指针没有初始化;指针在free或者delete之后没有置null;返回指向栈内存的引用或者指针等等都是野指针。

默认的拷贝构造函数是怎么造成内存泄露的?

一个类里面如果含有指针成员变量,要么显式的写出拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符。不然可能会造成两个对象的指针变量指向同一个内存空间,同一块内存被释放两次。

拷贝构造函数只是在用一个已存在的对象去初始化新建立的对象时调用,而对象进行赋值时,拷贝函数将不被调用。(用常量初始化新的对象调用的是构造函数);默认的拷贝构造函数实现的只能是浅拷贝,即直接将原对象的数据成员值依次复制给新对象中对应的数据成员,并没有为新对象另外分配内存资源。这样,如果对象的数据成员中有指针,两个指针实际上指向的是同一块内存空间。

在某些情况下,浅拷贝会带来数据安全方面的隐患。当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是深拷贝构造函数。(深拷贝和浅拷贝)

如何检测内存泄露?

(1)在mfc中,每个cpp文件中都有以下语句,

#ifdef _DEBUG
#define new DEBUG_NEW
#endif
DEBUG_NEW 这个宏定义在afx.h文件中,就是它帮助我们定位内存泄漏。如果我们代码有发生内存泄露的话,那么停止程序的时候,vs的output窗口就会显示内存泄露的信息,会定位到那一行代码。

(2)在普通的c++程序中,可以用 _CrtDumpMemoryLeaks();这个函数是在头文件crtdbg.h。在调试的时候使用,当它执行时,所有未销毁的对象均会报内存泄漏。因此尽量让这条语句在程序的最后执行。声明一个这样的宏,#define _CRTDBG_MAP_ALLOC,则可以输出内存泄露的位置。

如果一个程序有多个出口退出,在每一个可能的出口处调用 _CrtDumpMemoryLeaks 肯定是不可取的,那么这时可以在程序开始处包含下面的调用:_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );这条语句无论程序在什么地方退出都会自动调用 _CrtDumpMemoryLeaks。注意:这里必须同时设置两个位域标志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。

(3)可以开任务管理器看一下程序的“内存使用”和”虚拟内存大小”两项,如果虚拟内存持续地增长,那么就是有内存泄露了。

如何避免内存泄露?

使用智能指针(auto_ptr、unique_ptr、shared_ptr和weak_ptr)来替代new和delete的任务,头文件是。其中auto_ptr是c++98中的,c++11将其抛弃,是为了避免潜在的内存崩溃的问题(当两个指针指向同一个内存的时候,会释放两次),替换为unique_ptr、shared_ptr则不会崩溃。unique_ptr会在编译时就报错,而shared_ptr则会计算引用的次数,计录有多少个指针指向同一个对象,如果引用次数为0,这个对象会被自动释放掉,以防止内存泄露。(new的空间就一个,多个指针指向这个空间,要断开指向用reset,reset之后该指针就自动析构了,等所有指针都reset了,这个new的空间就自动释放掉了。)weak_ptr可以辅助shared_ptr解决多线程访问带来的安全问题。

auto_ptr m (new int(5));

shared_ptr的风险

使用shared_ptr时,遇到循环引用的情况,容易导致内存泄露。通过shared_ptr创建的两个指针,同时它们的内部均包含shared_ptr指向对方如下图:

2018秋招面试问题(四、C++基础问题)_第1张图片

  • main函数退出之前,FatherSon对象的引用计数都是2
  • son指针销毁,这时Son对象的引用计数是1
  • father指针销毁,这时Father对象的引用计数是1
  • 由于Father对象和Son对象的引用计数都是1,这两个对象都不会被销毁,从而发生内存泄露。

所以,如果shared_ptr所表征的引用关系中出现了一个环,那么环上对象的引用次数肯定不可能减为0,对象也就无法析构,发生内存泄露。

weak_ptr是如何辅助shared_ptr的?

为了解决shared_ptr循环引用引起的内存泄露问题,引入了weak_ptr。用weak_ptr来指向shared_ptr所指向的对象,对象引用次数并不会增加。

2018秋招面试问题(四、C++基础问题)_第2张图片

把其中一个shared_ptr改为weak_ptr,就可以解决内存泄露的问题。

  • main函数退出前,Son对象的引用计数是2,而Father的引用计数是1
  • son指针销毁,Son对象的引用计数变成1
  • father指针销毁,Father对象的引用计数变成0,导致Father对象析构,Father对象的析构会导致它包含的son_指针被销毁,这时Son对象的引用计数变成0,所以Son对象也会被析构。

weak_ptr 弱指针 弱在哪里?

weak_ptr没有重载*、->操作符,所以不能通过*、->操作符操作智能指针的内部数据。

要操作内部数据,可通过weak_ptr::lock间接访问。

using namespace std的作用

C++标准程序库的名称都被封装在一个名为std的命名空间中。这样就可以省略std,直接调用其中的cout、cin。

过去我们用的全局变量可以理解为全局命名空间,不需要namespace声明,比如带.h的头文件iostream.h。而不带.h的头文件iostream,就必须使用using namespace std,这样才能省略掉命名空间名和::。

命名空间主要是为了防止命名冲突,指定一些有名字的命名空间,把一些全局量放到各个命名空间中,与其他全局量分隔开来。

如何发现死循环?

看任务管理器中程序占用的cpu是不是快百分之百了。

四核cpu如何跑满?

用四个线程写四个死循环。

具体说明STL是如何实现vector的?

vector的底层结构还是数组,vector的内部是使用动态数组的方式来实现的。只要没超过它的预留容量,vector就会自动扩充空间来容纳新的元素,如果超过了容量,vector就会把容量扩充为原来的两倍,动态的重新分配内存,把原数组的内容拷贝过去,再释放原来的空间。

vector中erase和remove的区别?

vector中erase方法真正删除了元素,迭代器不能访问了。remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为remove是algorithm中的函数,algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。

vector通过下标访问,越界访问会有什么现象?

程序不一定会崩溃,通过下标访问vector元素的时候是不会做边界检查的,而是返回这个地址中存储的值。如果想在访问vector中的元素时首先进行边界检查,可以使用vector中的at函数。通过使用at函数不但可以通过下标访问vector中的元素,而且在at函数内部会对下标进行边界检查。Sta.at(i)

C++ STL中vector内存用尽后,为啥每次扩容是两倍增长,而不是3倍或其他倍数?

VS2015是1.5倍的增长。

1, 2, 4, 8, 16, 32,……可以看到,每次需要申请的空间都无法用到前面释放的空间,成倍数增长是为了时间复杂度。不是三倍或者更大是为了防止堆内存的浪费。

map使用下标去访问不存在的key值,会有什么现象?

程序不会抛出异常,会自动插入那个key值,返回一个默认值null。

(想着map的结构就明白了,map的key是个红黑树)

所以map的插入方法有两个,一个是下标访问,一个insert。

map和vector什么时候使用map,什么时候使用vector?

vector是顺序容器,map是关联容器。

如果想快速通过一个值找到其对应的另一个值的时候就应该用Map。比如,你希望输入一个学生的名字,就可以快速得到他的学号,那么你应该建立一个Map。如果不用map,而改用vector或者list之类的数据结构的话,就只能用遍历的方式查找,在数据量很大的时候,效率会低很多;哈希表(散列表)查找也是用map。

vector是动态数组,当我们需要随机存取的时候用vector就很合适。

vector、map/mutimap、unordered_map/unordered_multimap的底层数据结构,以及几种map容器如何选择?

底层数据结构:

vector基于数组,map/multimap/set基于红黑树,unordered_map/unordered_multimap基于哈希表
根据应用场景进行选择:

map/unordered_map 不允许重复元素

multimap/unordered_multimap允许重复元素

map/multimap/set底层基于红黑树,元素自动有序,且插入、删除效率高

unordered_map/unordered_multimap底层基于哈希表,故元素无序,查找效率高。

迭代器iterator是什么?

迭代器iterator 提供了一种一般化的方法,可以对顺序或关联容器类型中的每个元素进行连续访问

例如,假设iter为任意容器类型的一个iterator,则++iter 表示向前移动迭代器使其指向容器的下一个元素,而*iter 返回迭代器指向元素的值,每种容器类型都提供一个begin()和一个end()成员函数。

begin()返回一个iterator 它指向容器的第一个元素。

end()返回一个iterator 它指向容器的末元素的下一个元素也就是空。

通过迭代器,我们可以用相同的方式来访问、遍历容器。

如何使用迭代器iterator来对容器进行删除?

typedef vector<int>::iterator VecIt;

typedef map::iterator MapIt;

VecIt iter;

MapIt iter2;

可以。

(1)对于序列式容器:vector,deque,序列式容器就是数组式容器,删除当前的iterator会使得后边所有元素的iterator都失效。这是因为其使用了连续分配的内存,删除一个元素导致后面所有的元素都会向前移动一个位置,所以不能使用erase(iter++)的方式。但是erase方法可以返回下一个有效的iterator。(对于vector来说,erase删除之后自动指向下一个迭代器)

2018秋招面试问题(四、C++基础问题)_第3张图片

迭代器失效的情况是:

2018秋招面试问题(四、C++基础问题)_第4张图片

这样是先删除迭代器iter,iter已经失效了,然后iter再++,这样就会程序崩溃。

iter =cont.erase(iter);来解决连续内存结构的迭代器失效

  1. 对于关联容器(如map, set,multimap,multiset)和链表,删除当前的迭代器,只会使当前迭代器失效,不会自动指向下一位,只要在erase的时候,递增当前的iterator迭代器即可。(关联式容器使用了红黑树来实现,插入、删除一个结点不会对其他结点造成影响)

erase(iter++)来解决不连续内存结构的迭代器失效。反正都不能在for中iter++。

2018秋招面试问题(四、C++基础问题)_第5张图片

在m.erase(it++);中,it++先执行,此时还没有erase,程序自然不会崩溃。当it++执行完后,已经指向了下一个元素了,但it++的返回值还是当前元素,此时再删除它,才是正确的。

迭代器失效的情况

2018秋招面试问题(四、C++基础问题)_第6张图片

  1. 经过erase(iter)之后的迭代器完全失效,该迭代器iter不能参与任何运算,包括iter++,*iter,都是错的。
  2. 这样是错的,迭代器已经失效,不能再加,正确方法是在失效之前就指向下一位。

关于容器和迭代器的用法和相关知识点

2018秋招面试问题(四、C++基础问题)_第7张图片2018秋招面试问题(四、C++基础问题)_第8张图片

  1. set,set的key结构是红黑树结构,所以打印set元素出来是一个有序的数组,比如题上输出1、33、45、86。而对于关联性容器set而言,删除掉中间元素45,并不会影响其他元素的位置,也不会影响迭代器的位置,所以最后输出*it依然是86。关联式容器删除一个元素,不会使后面的迭代器失效,所以it++可以放erase里面指向下一个迭代器。2018秋招面试问题(四、C++基础问题)_第9张图片
  2. vector这种内存连续的容器,删除了前面一个,会使后面的迭代器全部失效,只有erase返回的是一个有效的下一个迭代器。
  3. 不管是内存连续的容器,还是内存不连续的容器,删除了当前的迭代器,这个迭代器就不能再用了,cout << *++it这种操作都是错误的。

你可能感兴趣的:(2018秋招面试问题(四、C++基础问题))