面试总结:C++11新特性

对于C++11的特性你了解多少?简单说说

- 在语法层面引入统一初始化(即列表初始化),那么C++11的初始化就可以分为列表初始化和字面值初始化

列表初始化就是使用{}(花括号)来进行对象、内置基本类型等的初始化
int d = 1;
int a = {d};
但是这种初始化方法需要确保类型安全和精度不丢失(我们知道,相比于C语言,C++是一种类型安全的编程语言,有着严格的类型检查机制)

比如说,这种情况C++17以后会报错

//error
double d = 3.1415926;
int a = {d};

这是由于这段代码试图将一个double类型的变量用于初始化一个int类型的变量,这是类型不安全的,同时,int 占用4字节,而double占用8字节,这是有精度丢失的风险的

像这种大精度转小精度,大整型转小整型又被称为窄化转换,在C++的列表初始化中是不允许的,由于精度丢失风险和类型不安全等问题出警告或是直接报编译错误

需要说明的是例如:

double d = 3.1415926;
int a = d;

//像字面值初始化(即直接赋值这种初始化形式)
//C++编译器会默认发生内置基本类型的隐式转换,这
//时候不会因为类型不安全报错,但是精度是会丢失的

- 类的成员变量默认初始化

我们知道,static修饰的内置数据类型的成员变量会默认初始化为0,但是对于非static成员变量来说,类内的内置数据类型成员变量(char,short,int等),如果没有显式初始化,仍然会初始化一个不定值;类内的对象类型成员变量,如果没有显式初始化,就会调用默认构造函数

- auto关键字和decltype进行数据类型推导

auto关键字借助编译器进行数据类型推导,decltype可以对变量或表达式进行数据类型推导。

实际上,二者都是C++11中的类型推导方式,但是使用方式和推导规则有所不同

使用方式角度:
auto关键字:编译器通过表达式的值来推断auto关键字应该对应什么数据类型
decltype: 是通过表达式本身来推断数据类型,而不是值

int a = 0;
auto d = a;            // int d =a;
decltype(a) d = a;     // int d =a;

//类型推导一般用在类型复杂,关系不好辨认的情况下,
//如果是正常情况下一般不建议增加不必要开销

//比如给一个例子:
auto lam = [](int a,int b)->int{return a+b};
auto res = lam(1,2);
//像这种情况,用lambda匿名表达式作为一个变量传递的时候

- 智能指针(4种智能指针 auto_ptr,unique_ptr,share_ptr和weak_ptr

auto_ptr是一种已经被弃用的智能指针,作用其实和unique_ptr差不多,被弃用的主要原因是auto_ptr存在一些问题。
我们知道,智能指针是RALL技术的一种应用。RALL:资源获取即初始化,这个技术是借用类的生命周期来管理动态内存的申请与释放。在创建RALL类对象时调用构造函数托管资源,对象生命周期结束调用析构函数时释放动态内存空间,达到一个对动态内存分配和回收的自动化处理。

那么在使用RALL技术在智能指针对象中托管资源时,我们往往不希望看到多个指针指向同一个对象,这样会有double free的问题
这个情况类似于:

String * strPtr = new String("data");
auto_ptr<String> ap1(strPtr);
auto_ptr<String> ap2(ap1);

//假设后续对ap1进行操作,会发生段错误

本处首先是动态内存分配构造一个自定义的类String,使用String类的有参构造函数,在堆上分配空间,然后创建一个auto_ptr智能指针,在构造智能指针对象ap1时托管资源,也就是把strPtr指向的内存空间纳入托管,后续又定义了一个auto_ptr对象ap2,使用已存在的ap1对象去初始化ap2。

这时候按理来说是调用拷贝构造函数,但是auto_ptr的拷贝构造函数的参数传入的不是常规的const &,而是&类型,由于不希望多个指针同时指向托管对象的double free问题,这里的拷贝构造函数不是一个深拷贝的复制,而是在内部悄悄做了一个移动语义的操作,也就是直接把ap2的资源指针指向ap1的资源指针指向的位置,然后将ap1的指向赋值为NULL。因此,就有了假设后续对ap1进行操作,会发生段错误的问题。

这时候,unique_ptr作为auto_ptr的替代被提出来了,unique_ptr是一个独占所有权的指针,也就是说,unique_ptr在实现上避免了多个指针对象指向同一个托管资源的问题。主要的做法就是禁用了拷贝构造和赋值运算符函数(需要说明的是,在C++11之前,禁用拷贝构造和赋值运算符函数一般可以使用private访问权限声明的办法,在C++11以后,直接在形参后 ”=delete“即可)

但是有人可能要问禁用了拷贝构造和赋值运算符函数的话会不会使得智能指针无法放入vector这些容器中了,当然不会,虽然拷贝构造和赋值运算符函数被禁用,但是移动语义的相关函数没有被禁止,也就是说:使用std::move将左值的智能指针对象转为右值,优先使用移动语义即可

String * strPtr = new String("data");
unique_ptr<String> ap3(strPtr);
vector<unique_ptr<String>> vec;
vec.push_back(ap3);  //error 拷贝构造和赋值运算符函数被禁用
vec.push_back(std::move(ap3));   //使用移动拷贝构造

share_ptr:
在某些使用场景下,unique_ptr并不是完美适配,比如多线程共享的资源,这个资源本身就可以被多个线程共享,无需独占。这时候share_ptr,这样一个共享所有权的智能指针就被提出来了,每次托管资源时都在计数器上+1,释放对应资源时-1,计数器值为0时意味着当前托管的资源将被释放

在这里插入图片描述
share_ptr对象分为两部分,资源指针管理和计数器控制块,资源指针部分多个对象同时指向同一片托管的资源,并且多个对象的计数器控制块也同时指向堆上一个定义开辟出来的计数器。share_ptr资源指针管理时,并非线程安全,计数器的控制操作是线程安全的

share_ptr有一个循环引用的问题,也就是
在这里插入图片描述
假设当前栈区有share_ptr asp和share_ptr bsp,他们又各自托管了A类对象和B类对象。A类对象内部有一个share_ptr _b指针,B类对象内部有一个share_ptr _a指针,这时候假设_b指针和_a指针分别指向对方的对象空间,也就是说当前有两个share_ptr智能指针指向同一个A类对象,有两个share_ptr智能指针指向同一个B类对象,引用计数都为2。在某个时刻,asp和bsp的生命周期到达,编译器自动调用他们的析构函数,由于共享指针的特性,这时候引用计数2 - 1 = 1,并未释放托管的资源。后续A,B对象将死锁,一直无法释放。

为了解决这个循环引用的问题,我们又引入了weak_ptr,这个弱引用的智能指针托管资源时将不会改变引用计数,想一下,假如A,B对象里面是weak_ptr(weak_ptr _b 和 weak_ptr _a )是不是在栈区的asp和bsp生命周期到达时托管的对象也跟着释放了。(weak_ptr使用lock()方法可以暂时提升权限为share_ptr,通常用来查看这个share_ptr是否已经被释放了)

- 右值引用和移动语义

左值和右值:
一般我们认为左值就是可以取地址的值,右值是不能取地址的值
左值是有确切地址的,右值只是一个临时的变量

//左值
&"hello";   //显式的字符串是左值
&++i;       //前置++返回的是自身的引用,不是临时对象
int * p = nullptr;
&p;         //指针变量是左值

//右值
&i++;  //error 后置++返回的是临时对象,内部调用了前置++对自身做了一个 ++*this
&std::move(p); //error std::move(p)把左值p转为右值

在使用拷贝构造和赋值运算符函数时,我们通常是一个深拷贝的写法:

class String{
public:
     //构造和析构之类的实现略过了奥
     String();
     String(const char * str);
     ~String();
     
     String(const String & object){
        int length = strlen(object._data);
        _data = new char[length+1];
        strcpy(_data,object._data);
     }
private:
    char * _data;  //资源指针     
}

由于使用了const &作为形参传递,无论是左值还是右值,由于引用折叠的特性,到最后都可以成功调用拷贝构造函数,例如下面的实现:这里就是先调用有参构造,创建了一个临时String对象,然后作为参数传入vector的push_back方法,符合拷贝构造调用的时机,调用拷贝构造函数

vector<String> vec;
vec.push_back(new String("hello"));

但是我们发现,如果是对于临时对象使用深拷贝是一件意义不大的事情,因为临时对象的生命周期很短,对应的内存空间要在短时间内分配和回收,假如有大量的临时变量做拷贝构造,开销将会很不理想。这时候如果采用移动语义,将临时对象的内存空间直接由初始化的对象资源指针指向,临时对象的资源指针直接为NULL,就无需额外申请内存空间了(但是这种移动语义的处理只能对右值来做,对左值意义不大)

在这里插入图片描述
(图源百度,包浆的好图咯)

但是问题来了,拷贝构造函数里面的const &参数并不能识别出谁是左值,谁是右值,这时候就无法针对右值来做一个移动语义的处理。C++11引入了一个右值引用,类似于 int && ref = std::move(p);右值引用只能绑定右值,我们只需要实现移动拷贝构造和移动赋值函数,这时候,如果传入临时对象时,就会优先调用移动语义函数

String(String && str){
    _data = str._data;
    str._data = nullptr;
}

String & operator=(String && str){
    if(this != & str){
       delete[] _data;
       _data = nullptr;
       _data = str._data;
       str._data = nullptr;
    }
    return *this;
}

需要注意的是,右值引用不一定是左值

//左值ref
int  p = 0;
int && ref = std::move(p);

//若果函数返回值为右值引用
int && getData(){
    int p = 0;
    return std::move(p);
}

&getData();  //error 返回值是右值引用,也就是说,右值引用不一定是左值

- 范围for循环

范围性的迭代写法

vector<int> vec(n,0);
for(auto i : vec){
   //范围for循环:循环体内的i为vec容器的各个元素的拷贝
}
for(auto & i : vec){
   //范围for循环:循环体内的i为vec容器的各个元素的引用,可以修改
}

- 完美转发(std::forward

函数模板的参数传递的时候参数的左值右值属性传入函数体内部不改变
主要是使用了std::forward()完成实现
在这里插入图片描述
std::forward()的底层就主要是做了一个static_cast 转换为右值引用,结合函数模板万能引用(使得C++函数既能接受左值又可以接受右值)的知识理解:

templete<type_name T>
void Func(T && a){
    return;
}

int data = 10;
Func(1123);  //函数模板中T 理解为 int &&
Func(data);  //函数模板中T 理解为 int &

在这里插入图片描述

- 空指针nullptr

在C语言中,NULL用于表示 ((void *)0) ,此时C语言中为指针赋值NULL,实际上是做了一个下行转换,把void *自动转换为其他的数据类型了,而且需要注意的时C语言中的NULL是一个宏定义

在C++中,NULL只用于表示0,这是因为C++中的void *不能随意转换为其他数据类型,(也即C++不支持void *的隐式数据类型转换)只能把NULL表示为0来使用,但是这个时候又会出现新的问题,假如有:

void func(int p){}
void func(int * p){}

int * p = NULL;
func(p);  //这个时候会产生二义性,由于NULL等同于0,在上述重载中有二义性的问题

所以C++11中引入了nullptr,表示空指针,nullptr可以像C语言的void *一样可以随意转换为其他数据类型

- lambda表达式(允许在代码中定义匿名函数)

lambda表达式 [ “捕获列表” ] ( “形参列表” )->typename{ “函数体” }

lambda表达式相当于是一个未命名类的未命名对象,在代码中定义匿名函数,尤其是在和STL函数配合时有很好的效果

vector<int> vec;
sort(vec.begin(),vec.end(),[](const int & a,const int & b)->bool{return a > b});

//lambda匿名函数的返回值不一定要说明,可以由编译器自己推断

同时lambda表达式可以作为一个变量传递

auto lam = [](const int & a,const int & b)->bool{return a > b};
lam(1,2);

- 无序哈希表,如unordered_map这些

像unordered_map,unordered_set这些都是哈希表为底层实现的容器,查找的效率为O(1),当然由于哈希冲突等问题,在某些情况下最坏时间复杂度时O(n)

像map,set这些序列化容器的底层则是由红黑树实现的,所谓红黑树就是不严格平衡的二叉搜索树,其中:根节点和外部节点为黑色,不能有连续的两个红色节点,所有根节点到外部节点的路径上黑色节点的数目一致,搜索增删的时间复杂度为O(logn)

  • 47
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值