点击蓝字关注我哦
以下是本期干货视频 视频后还附有文字版本哦 ▼ 《名企高频考点之-谈谈你所理解的vector数据插入(一)》 ▼ ps:请在WiFi环境下打开,如果有钱任性请随意1. vector插入面试官更想听到 在面试中,vector的插入经常会被考到,可能会被面试官问到,也有可能在OJ笔试中遇到,操作不当可能就会导致程序效率低下而不能完全通过测试用例,或者不能给出程序简洁的书写方式。 那面试时被问到:vector是如何插入的?诸如此类问题,大家一定要去琢磨面试官更想听到什么答案,说白了就是想听到:同学是否掌握vector的不同插入方式
不同插入方式背后又会发生什么事情,效率怎么样,如何选择合适的插入方式
插入后可能会引起什么后果
有没有分类作答,回答条理是否清晰
- 老实巴交的push_back尾插
- 专横霸气的insert任意位置的插入
2. C++98中的push_back尾插
2.1 内置类型元素插入
内置类型的元素尾插非常简单,定义好了vector之后,便可以直接插入,而且该容器在插入时可以动态增长,用户只管插入数据,一般不用担心数据放不下的问题,降低了用户的使用成本。// 验证:1. push_back是尾插 2. 在空间允许范围内,可以插入任意多个元素void TestPushBack1(){ vector v; for(int i = 0; i < 100/*1000*/; ++i) v.push_back(i); for(auto e : v) cout<< e << endl; cout<}// 验证:在插入元素期间,vector会进行扩容void TestPushBack2(){ vector v; size_t sz = v.capacity(); for(int i = 0; i < 100; ++i) { v.push_back(i); if(sz != v.capacity()) { sz = v.capacity(); cout<< sz << endl; } }}
2.2 自定义类型元素插入
自定义类型元素在插入时,实际插入的是对象的一份拷贝。class Date{public: Date(int year = 1970, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day){ std::cout << "Date(int,int,int): " << this << std::endl;} Date(const Date& d) : _year(d._year) , _month(d._month) , _day(d._day){ std::cout << "Date(const Date&): " << this << std::endl;}private: int _year; int _month; int _day;};// 验证:自定义类型元素push_back的是该元素的拷贝void TestPushBack3(){ Date d(2020, 4, 17); vector v; v.push_back(d);}输出结果:Date(int,int,int): 00AFFBB4Date(const Date&): 00CEBD88
从打印结果可以看出,对于Date类型对象d在push_back()时,插入的确实是对象的拷贝。
如果想要提高程序运行效率,不考虑兼容性的情况下,可以使用C++11提供的emplace_back进行就地构造。
void TestPushBack4(){ vector v; v.emplace_back(2020, 4, 17); cout << &v[0] << endl;}程序输出结果:Date(int,int,int): 00865AE8 调用构造函数直接在vector中构造对象00865AE8 可以看到vector中0好位置对象与构造函数打印对象地址相同
那如果用户没有显式定义拷贝构造函数时,还能进行插入吗?
class Date{public: Date(int year = 1970, int month = 1, int day = 1) : _year(year) , _month(month) , _day(day){ std::cout << "Date(int,int,int): " << this << std::endl;}private: int _year; int _month; int _day;};// 验证:如果没有定义拷贝构造函数,能否正常进行尾插void TestPushBack5(){ Date d(2020, 4, 17); vector v; for(int i = 0; i < 100; ++i) v.push_back(d); std::cout << v.size() << std::endl;}程序输出:100
结论:对于Date类而言,即使用户没有显式定义拷贝构造函数,仍旧可以正常进行插入。
问题:那能否说明对于没有显式拷贝构造函数的任意类型,都可以进行尾插呢?
class String{public: String(const char* str = ""){ if (nullptr == str) str = ""; _str = new char[strlen(str) + 1]; strcpy(_str, str); std::cout << "String(const char*):" << this << std::endl;} ~String(){ if (nullptr != _str) { delete[] _str; _str = nullptr; } std::cout << "~String():" << this << std::endl;}private: char* _str;};void TestPushBack6(){ String s("hello"); std::vector v; v.push_back(s);}
上述程序在运行期间,并不能够正常运行,发生崩溃。
同样是没有拷贝构造函数,为什么Date可以直接插入,而String类则不行呢?原因分析如下:
1.如果用户没有显式定义拷贝构造函数时,编译器会按照浅拷贝方式生成一个默认拷贝构造函数
2.浅拷贝:将一个对象中内容原封不动的拷贝到新对象中
如果类中未涉及到资源管理时,不会出现任何问题,比如Date类
如果类中涉及到资源管理时,浅拷贝会让多个对象共享同一份资源,在对象销毁时同一份资源释放多次而导致程序崩溃。
在销毁时,s2对象调用其析构函数会先将0x12345678空间释放掉,但s1并不知道,其在销毁时会再次使用0x12345678空间,而造成程序崩溃。
解决方式:给String类添加深拷贝的拷贝构造函数完成拷贝
// 深拷贝就是让每个对象都拥有自己独立的资源,在释放时就不会产生影响String(const String& s) : _str(new char[strlen(s._str)+1]){ strcpy(_str, s._str); }
深拷贝也是面试中常考问题,如果同学们对深拷贝感兴趣,可以在后台留言。
3. C++11新增右值引用push_back尾插
3.1 传统插入的问题分析
class String{public: String(const char* str = ""){ if (nullptr == str) str = ""; _str = new char[strlen(str) + 1]; strcpy(_str, str); std::cout << "String(const char*):" << this << std::endl;} // 深拷贝构造函数 String(const String& s) : _str(new char[strlen(s._str)+1]) { strcpy(_str, s._Str); } ~String(){ if (nullptr != _str) { delete[] _str; _str = nullptr; } std::cout << "~String():" << this << std::endl;}private: char* _str;};int main(){ vector v; v.push_back(String("hello")); return 0;}
通过前文分析指导,vector在push_back时尾插的是对象的拷贝,但是对于v.push_back(String("hello"))方式,是构建一个匿名对象向vector中尾插,尾插结束后该对象被销毁,初听之下没有问题,细细品味有不足之处:
3.2 C++11提供移动语义的push_back方式来提高效率
如果想要支持移动语义,String类中必须提供移动构造:// 移动构造:将参数s中资源转移给当前对象,然后让s中资源指向空,因为s马上要被销毁// 注意:参数必须是右值引用类型的,此处不能添加const,否则不能转移资源String(String&& s) : _str(s._str){ s._str = nullptr; }
再次运行程序时可以看到不会再调用拷贝构造函数,而是调用移动构造,少了拷贝构造中新空间的申请,提高程序运行效率。
4. 总结
本节主要对push_back进行了详细讨论,以及插入背后所涉及到的问题及其优化,希望对同学们的日后使用以及面试能有所帮助,谢谢。
作者:时亮益
审核:王海斌
编辑:小丸子
好看,就要点个"在看"