目录
引入
C++11新特性
统一的初始化列表
一切皆可{}初始化
std::initializer_list
统一的声明
auto
decltype
nullptr
范围for
STL新增容器
STL新增容器接口
左值引用和右值引用
左值和右值
左值引用和右值引用
右值引用的优势(移动语义)
右值引用的使用场景
move谨慎使用
模板中的万能引用
完美转发
类型转换
C++类型转化
static_cast
reinterpret_cast
const_cast
dynamic_cast
引入
C++11是C++的一个大变革,增加了大约有140种新特性,其中有不少对于我们的代码编写效率帮助很大,也有一些特性很鸡肋;C++11是委员会经过了8年积淀才发行的,其中添了不少C++的“坑”,此篇文章将对C++11的新特性进行介绍和剖析。
C++11新特性
统一的初始化列表
一切皆可{}初始化
C++98规定了可以使用{}对内置类型或结构体元素进行初始化,在C++11后将{}的适用范围进行了扩充,允许{}对所有内置类型和自定义类型进行初始化,并且如果使用{}进行初始化可以不加赋值符号=。
struct Point
{int x;int y;
};int a = 1;
int a1 = { 1 };
int a2{ 1 };int arr[]{ 1,2,3,4 };Point p1 = { 1,1 };
Point p2{ 1,1 };
以上初始化的方式都是被允许的。
std::initializer_list
initializer_list是C++中引入的一个新特性,其保证了一切皆可{}初始化。可以理解为代码中的{}会被识别为initializer_list类型,然后通过该类型再去进行赋值或构造。
为了验证{}的类型是initializer_list类型,可以使用typeid(e).name()来打印类型。
auto e = { 1,2,3,4,5,6,7 };
cout << typeid(e).name() << endl;
C++11为每个容器的构造函数都增加了用initializer_list作为参数进行构造,这也是为什么容器可以使用{}进行初始化。
vector:
set:
map:
对于自定义类型用{}进行初始化,以及vector用{}进行初始化的原理是不一样的,vector的{}中元素个数是可以不固定的,而对于一些我们自己实现的自定义类型的{}中元素的个数是固定的;比如一下Point类型。
struct Point
{int x;int y;
};Point p1 = { 1,1 };
vector<int> v={1,2,3,4,5,6,7};
p1的{}是隐式类型转化,先用{}中的参数进行构造Point对象,再进行拷贝构造,编译器会将两次构造优化为一次;v是用initialiter_list进行构造的,不是使用拷贝构造。
统一的声明
auto
C++11对auto的作用进行了修改,auto可以实现类型推断,变量的类型需要显示的写,让编译器自己推。
不论是内置类型,自定义类型,还是函数返回值auto都能推导。但是注意:auto不能作为函数的参数。
auto a = new int(1);
auto b = &a;
auto c = strcpy;
decltype
decltype(expression)
decltype是C++11中新增的关键字,其能够获得表达式的类型。表达式可以是变量,也可以是表达式。
decltype(&x) aa; //aa的类型是int*
decltype(x * y) bb; //bb的类型是doouble
nullptr
C++11引入了nullptr来代替NULL;NULL实际上是0,是整形,这也就导致了在进行函数传参的时候NULL会匹配整形而不是指针。引入nullptr就不会出现分歧了,nullptr本质是(void*)0
范围for
C++引入范围for这一新概念,对于数组以及部分容器的遍历的方式进行了简化。
int arr[] = { 1,2,3,4,5,6,7 };
for (auto e : arr)
{cout << e << endl;
}vector<int> v = { 1,2,3,4,5,6,7,8,8 };
for (auto e : v)
{cout << e << endl;
}
范围for的底层还是依靠迭代器来实现的,所以没有实现迭代器的容器是不能使用范围for的。
STL新增容器
以上用红色框起来的都是新增容器,其中unordered_set和unordered_map底层是哈希表,关于哈希表的底层实现在《哈希表的实现》中已经进行了深度剖析,此处就不再赘述了。
哈希表(闭散列)的实现-CSDN博客文章浏览阅读921次,点赞43次,收藏44次。通过除留余数法确定数据位置,再根据闭散列的方法解决哈希冲突。本篇博客对哈希表闭散列的实现进行细致剖析,帮助读者理解哈希表性能的优缺点。https://blog.csdn.net/2401_87944878/article/details/146922720哈希表(开散列)的实现-CSDN博客文章浏览阅读518次,点赞24次,收藏26次。通过除留余数法确定数据位置,再根据开散列的方法解决哈希冲突。本篇博客对哈希表开散列的实现进行细致剖析,帮助读者理解哈希表性能的优缺点。
https://blog.csdn.net/2401_87944878/article/details/146923387array容器,其底层就是数组,只是对数组进行了封装。
int arr1[10] = { 0 };
array<int, 10> a2{ 0 };
以上arr1和arr2是完全一样的,唯一区别就是array对越界的检查更加严格,这个类在我们实际写代码中用的很少,没有数组的简洁,直观,也没有vector丰富的接口,array的使用很鸡肋。
forward_list容器,其底层是单链表,这个容器也很鸡肋,库中forward_list不支持尾插和任意位置前面插入,因为这两个函数实现的时间复杂度是O(N);forward_list的使用不如list,所以这个容器的使用也是比较少的。
STL新增容器接口
C++11为每个容器的迭代器增加了新接口:cbegin()和cend();
这俩个接口专门用来返回const迭代器,但是begin()中已经重载了返回const迭代器的接口,所以cbegin和cend的引入是完全没有必要的。
左值引用和右值引用
左值和右值
C++11引入了左值和右值的概念;
左值:可以获取地址的值被称为左值,左值可以在赋值符号=的左边也可以在右边;
右值:不能获取地址的值被称为右值,右值只能在赋值符号=的右边;右值通常有常量,表达式,函数返回值...
例外:常量字符串属于左值,其可以取地址。
const char* arr = "hello world";
左值引用和右值引用
我们通常使用的都是左值引用。
左值引用:在类型后加&即可。
int a = 10;
int& b = a;
int* p = new int(1);
int*& pp = p;
右值引用:在类型后加&&即可。
double x = 1.8, y = 2.3;
double&& sum = x + y;
左值引用可以引用右值:使用const&;
const int& a1 = 10;
double x = 1.8, y = 2.3;
const double& sum = x + y;
右值引用可以引用左值:move()仿函数,将左值转化为右值;
double x = 1.8, y = 2.3;
double&& x1 = move(x);
double&& y1 = move(y);
右值引用的优势(移动语义)
右值引用作为参数和返回值可以提高效率。
当需要将一个自定义类型的对象返回并接受的时候,使用右值引用可以减少拷贝。
以上代码编译器会优化:将ret视为右值,因为其是将亡值,即将被销毁。
根据以上图可以看出,如果对自定义类型进行返回的时候会先进行拷贝再去赋值。
上面代码中返回的ret即将被销毁,所以ret也被称为将亡值。ret是将亡值,能否直接用ret来进行赋值呢??不去创建临时对象,这样也能大大提高效率。
此时就可以使用右值引用,通过引用将亡值来避免再创建临时对象,而是直接进行赋值,这也种方法被称为移动赋值。
对赋值函数进行重载即可,将参数修改为右值引用,代码模拟实现如下。
//赋值重载函数
string& operator=(const string& s)
{cout << "赋值" << endl;string tmp(s);swap(tmp);return *this;
}//移动赋值
string& operator=(string&& s)
{cout << "移动赋值" << endl;swap(s);return *this;
}
有移动赋值,同时也有移动构造。
//拷贝构造
string(const string& s):_str(nullptr)
{cout << "深拷贝" << endl;string tmp(s._str);swap(tmp);
}//移动拷贝构造
string(string&& s):_str(nullptr)
{cout << "移动拷贝" << endl;swap(s);
}
左值引用的核心是:减少拷贝,提高效率;
右值引用的核心是:进一步减少拷贝,解决左值引用没有解决的传值返回问题。
右值引用的使用场景
自定义类型中需要深拷贝的类,且有对该类型进行船只返回的场景。
浅拷贝的类不需要实现移动语义,编译器会自动生成;而且浅拷贝的拷贝代价也不大,没有意义。
右值引用的核心在于:不进行深拷贝,而是进行已有资源的交换。
库中的容器里,基本上每一个插入,构造,赋值都会重载移动语义来提高效率;
move谨慎使用
库中会对移动语义进行重载,那使用move将左值变为右值不就可以提高效率吗??
理论上是这样的,但是被move且被使用的参数会自动销毁。
string s1 = "hello";
vector<string> l;
l.push_back(move(s1));
以上代码,确实提高了list插入的效率,但是插入后,s1会调用析构函数。
模板中的万能引用
template<typename T>
void Func(T&& val)
{cout << val << endl;
}
模板中的&&并不仅仅表示右值引用,其表示万能引用,既可以引用左值又能引用右值。
实参是左值,T&&实例化成左值引用,这一现象称为折叠;
实参是右值,T&&实例化成右值引用。
万能引用也包含const 引用。
完美转发
在C++11中规定了:右值引用变量的属性会被编译器识别为左值。
void Func1(int&& val)
{cout << val << endl;
}
意思就是,上面代码中的val不再是右值,而是左值。
如果希望保持val的右值属性,在使用val的时候,使用forward<T>(val)替代,这样向下一个函数传递的时候依旧保留的右值的属性。
完美转化常用于:在多个函数之间相互调用的时候,需要保留参数的右值属性。
比如:在list的插入中,如果传过去的是右值属性的自定义类型,进行构造的时候还需要保持其右值属性,此时就可以使用完美转化来实现。
//尾插
void push_back(T&& val)
{ListNode* newnode = new ListNode(std::forward<T>(val));newnode->_next = _phead;newnode->_prev = _phead->_prev;_phead->_prev->_next = newnode;_phead->_prev = newnode;
}
对ListNode进行构造时,仍然需要保留右值属性,从而调用string的移动构造。
//初始化节点
ListNode(T&& val):_val(std::forward<T>(val)), _next(this), _prev(this)
{}
tips:只要是右值变量需要往下传,且要保留右值属性就可以使用完美转发来实现。
类型转换
类型转换分为显式类型转化和隐式类型转化。
1)有关联的类型之间可以转化;
2)指针可以转为整形,地址也只是一个编号;
3)单参数构造支持隐式类型转化;
4)自定义类型能否隐式类型转换看构造函数,如果不希望进行隐式类型转化可使用explict进行修饰。
void test_07()
{const int a = 10;const int* pa = &a;int* paa = (int*)pa;*(paa) = 20;cout << *paa << endl;cout << a << endl;
}
上图是对const类型进行隐式类型转化,来实现对const类型变量的修改。
如图是代码运行的结果,并没有看到a发生了改变。这是因为a实际上改变了,但是编译器在第一次拿到a的时候认为其不会改变,将其放在寄存器中存储,每次也从寄存器中拿。
要先让编译器从a存放的地址处那,可以使用volatile对const类型进行修饰。
void test_07()
{volatile const int a = 10;volatile const int* pa = &a;int* paa = (int*)pa;*(paa) = 20;cout <<"*paa: " << *paa << endl;cout << "a: " << a << endl;
}
C++类型转化
C++对类型转换进行了分类,分为4种;每种使用一个关键字实现转化,使得类型转化有了可视性。
static_cast
static_cast用于非多态类型的转换(静态转换),编译器隐式执行的任何类型转换都可用static_cast,但它不能用于两个不相关的类型进行转换。
double d = 1.2;int x = static_cast<int>(d);
指针和整形不能进行static_cast转化。
reinterpret_cast
reinterpret_cast操作符通常为操作数的位模式提供较低层次的重新解释,用于将一种类型转换为另一种不同的类型。
可用于不相关类型的转化。
int a = 10;
int* pa = &a;
int b = reinterpret_cast<int>(pa);
const_cast
const_cast最常用的用途就是删除变量的const属性,方便赋值。
volatile const int r = 10;
int* pr = const_cast<int*>(&r);
dynamic_cast
dynamic_cast用于将一个父类对象的指针/引用转换为子类对象的指针或引用(动态转换)。
用派生类指针可以转化为基类指针,则被称为向上转化,遵循复制兼容原则。
用基类指针可以转化为派生类类指针,则被称为向下转化,只有当基类指针指向派生类的时候才能实现转化。
当对基类指针和派生类指针进行转化的时候,建议使用dynamic_cast。