前言
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了 C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞 进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。 从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于 C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中 约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言, C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更 强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个 重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程 主要讲解实际中比较实用的语法。
一、关键字及其语法介绍
1.1. auto 关键字介绍
auto又称为范围for 主要用于自动推导,常用于迭代器遍历 auto作为函数返回值时,只能用于定义函数,不能用于声明函数
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局 部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将 其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初 始化值的类型。
#include<iostream>
#include<string>
#include<vector>
using namespace std;
int main()
{
//第一中用法自动推导变量
int i = 1;
auto j = 12;
cout << endl;
cout << j << endl;
//第二种用法用于迭代器的推导
vector<int> v{ 1,2,2,3 };
for (auto ch : v)
{
cout << ch << endl;
}
vector<int>::iterator itt = v.begin(); //可以看到常规迭代器定义比较麻烦
auto it = v.begin();//使用auto推导比较省事
return 0;
}
1.2. nullptr 关键字及其用法
nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,由于NULL实际上代表的是0
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
1.3. 统一的列表初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
c++ 11扩大了 大括号括起的列表初始化,基本上是“ 万物皆可列表初始化” 可以省略 =
int i = 1;
int j{ 2 };//怎末说呢个人不太喜欢这种用法,感觉都不像是c++了
//下面介绍列表初始化好的一面
vector<int> v{ 1,2,3,4 };
//几乎所用的容器在c++ 11里都支持了列表初始化,好处是不用在一个个push了
auto x = { 1, 2, 3, 4, 5, 6 };
cout << typeid(x).name() << endl;
//构造这边也可以直接构造
map<string, string> dict = { {"sort","排序"},{ "left","左"} };
for (auto ch : dict)
{
cout << ch.first << ch.second;
cout << endl;
}
1.4.std::initializer_list
http://www.cplusplus.com/reference/initializer_list/initializer_list/ 文档介绍
std::initalizer_list的类型
int main()
{
auto il = { 10, 20, 30 };
cout << typeid(il).name() << endl;
return 0;
}
使用场景:一般是作为构造函数的参数,c++11 中stl 不少容器就增加了
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值。
int main()
{
vector<int> v{ 1,2,3,5 };
list<int> lt = { 1,2 };
//这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//使用大括号对容器赋值
v = { 10, 20, 30 };
for (auto ch : v)
{
cout << ch;
cout << endl;
}
return 0;
}
总结一下:c++11 以后 一切对象都可以列表初始化,但是建议普通对象还是按照以前的来初始化,容器如果有需要再说。
容器内部的变化:
1.都支持了initalizer_list构造,用来支持列表初始化
2.比较鸡肋的接口: cbegin() cend()系列
1.5. c++11 一些关键字介绍
decltype介绍
关键字decltype将变量的类型声明为表达式指定的类型
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p; // p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
return 0;
}
二、新增的STL容器介绍
在c++11 中新增了几个容器
<array> <forward_list> <unordered_map> <unorderred_set>
2.1.1 std::array介绍
array听名字就知道是个数组,c++大佬弄这个玩意就是为了取代c语言的数组,
c数组的缺点:越界访问的检查是随机抽检,如果你是读越界可能不报错,写有时候越界也不会报错,随机编译器抽查。
std::array除了有传统数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能.
int main()
{
int a[10];
cout << a[15];
return 0;
}
//可以看到越界读没检查出来 虽然读写的是随机值
c数组和array对比
int main()
{
const size_t N = 100;
int a[N];
//c语言数组越界检查,越界读基本检查不出来,越界写抽查
a[N];
/*a[N] = 1;*/ //这个放开会崩
a[N + 5] = 1;//这个越界没被检查出来
// // 越界读写都可以被检查出来
// 实际情况:array用得很少,一方面大家用c数组用惯了
// 用array不如用vector + resize去替代c数组
array<int, N> a2;
a2[N];
a2[N] = 1;
a2[N + 5] = 1;
return 0;
}
2.2. std::forward_list介绍
forward_list 是c++11 新增的容器他是一个单列表我们在学习数据结构的时候都知道,链表在对数据进行插入和删除是比顺序存储的线性表有优势,因此在插入和删除操作频繁的应用场景中,使用list和forward_list比使用array、vector和deque效率要高很多。
int main()
{
forward_list<int> num = { 1,2,3,4,5 };
for (auto ch : num)
{
cout << ch << endl;
}
cout << endl;
num.push_front(12);
for (auto ch : num)
{
cout << ch << endl;
}
cout << endl;
//介绍个remove接口吧基本上学了容器其他接口都差不多
num.remove(4);
for (auto ch : num)
{
cout << ch << endl;
}
return 0;
}
2.3.unordred_map unroderd_set介绍
std::unordered_map与std::map用法基本差不多,但STL在内部实现上有很大不同,std::map使用的数据结构为二叉树,而std::unordered_map内部是哈希表的实现方式,哈希map理论上查找效率为O(1)。但在存储效率上,哈希map需要增加哈希表的内存开销。
std::unordered_set的数据存储结构也是哈希表的方式结构,除此之外,std::unordered_set在插入时不会自动排序,这都是std::set表现不同的地方。
int main()
{
unordered_set<int> un_set;
un_set.insert(7);
un_set.insert(5);
un_set.insert(3);
un_set.insert(4);
un_set.insert(1);
//没有排序
for (auto ch : un_set)
{
cout << ch << endl;
}
cout << endl << endl;
//排序
set<int> set{7,5,3,4,1};
for (auto ch : set)
{
cout << ch << endl;
}
return 0;
}
2.4左值引用和右值引用以及移动语义
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们 之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用? 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋 值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左 值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
2.4.1.左值引用的介绍
void swap(int& left, int& right)
{
int temp = left;
left = right;
right = temp;//引用做参数
}
//这是对左值引用的介绍
int val = 1024;
int& b = val;
b = 1000;
cout << val<<endl;
cout << b;
//可以看到b引用了val并同时改变了值
//左值引用的使用场景做参数,做返回值
//引用和指针从底层来看,并没有什么区别,引用在底层实现上与指针相同。引用只是c++语法
//可以将其看作编译器自动完成取地址、解引用的常量指针。
再来说一下左值引用和指针的区别吧
在语法概念上,引用只是定义了对象的别名,并未新空间;指针存储对象的地址。
2、引用必须初始化,而指针并无要求。
3、有空指针,无空引用。
4、引用与被引用对象绑定,一个对象可被引用多次,但一个引用不可引用多个对象;指针可指向不同的对象。
5、自增自减意义不同,引用自增自减即对被引用对象进行修改;指针则指指针运算,表明指针的偏移。
6、引用的使用比指针安全。
7、引用没有顶层const修饰,如 int& const。因为引用本身就与被引用对象绑定不可变,顶层const就失去了其意义;指针既有顶层const又有底层const修饰。
8、访问实体方式不同,指针需要显式解引用,引用则由编译器自动完成取地址、解引用。
9、在sizeof中含义不同,引用结果为被引用对象类型的大小,而sizeof指针的大小则为4或8byte。*/
右值引用的介绍
-
右值也是一个数据表达式,右值是字面常量或者是求值过程种创建的临时对象,右值的生命周期是短暂的,如:字面常量,表达式返回值,函数返回值(不是左值引用的返回值),临时变量,匿名对象等等。
-
右值引用是给右值取别名,所有的右值引用是不能引用左值。
-
右值是不能取出地址的,但是当右值去别名后,这个右值会被存到特定的位置,且可以取到该值的地址,也就是说右值引用值是一个左值。
-
右值引用会开辟一块空间去存右值,其中普通的右值引用是可以被修改这块空间的,const的右值引用时不可以被修改的
-
-
2.5.左值引用与右值引用比较
- 左值引用总结:
- 1.左值只能引用左值,不能引用右值
- 2.const左值引用即可引用左值,也可以引用右值
右值引用总结:
1.右值引用只能引用右值,不能引用左值
2.但是右值引用可以引用move以后的值
2.6.引用的价值
左值引用解决的问题:
1.做参数 a,减少拷贝,提高效率 b 做输出型参数
2.做返回值。 a 减少拷贝 提高效率 b.引用返回 ,可以修改对象 比如operate 【】
左值引用的短板:
当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。
namespace zdc
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
//string tmp(s._str);
//swap(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s);//str将亡 资源转移
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
bit::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
bit::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
int main()
{
zdc::string str1("hello");
zdc::string str2(str1);//调用拷贝构造
zdc::string str3(move(str1));//调用移动构造
return 0;
}
这是自己写了一个string类,to_string那个地方本来应该是两次拷贝构造,但是新一点的编译器会优化,变成一次拷贝构造。
其实两次拷贝构造 在一些普通的场景还可以,但是如果是string map这些深拷贝代价较大的地方就需要用到右值引用弄出来的移动构造,移动赋值。
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
//string tmp(s._str);
//swap(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s);//str将亡 资源转移
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
移动构造:本质是将参数右值的资源窃取过来,占为己有,那么就不用做深拷贝了。所以它叫做移动构造。就是窃取别人的资源来构造自己。 注意资源必须是将亡值。
移动赋值和移动构造本质一样,就不多做介绍了
2.7.完美转发
完美转发又称 引用折叠 万能引用
c++标准库,很多容器为了解决拷贝问题,都会定义一个移动拷贝构造,移动赋值函数
但是在上面调用参数是右值引用的insert接口存在一个问题,如果右值引用x去接受一个右值,那么这个x就会退化成为一个左值,调用的是拷贝构造,不是移动构造,为了保持x是一个右值,那么我们可以用库里面的 std::forward<T>(x) 这个函数可以在传参的时候保持原生右值属性。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include<vector>
#include<map>
#include<list>
#include<array>
#include<forward_list>
#include<unordered_map>
#include<unordered_set>
#include<set>
#include<cassert>
using namespace std;
namespace zdc
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷贝构造(深拷贝)" << endl;
//string tmp(s._str);
//swap(s);
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s);//str将亡 资源转移
}
// 拷贝赋值
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string s) -- 移动赋值(资源移动)" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
zdc::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
zdc::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
//
//int main()
//{
//
// zdc::string str1("hello");
// zdc::string str2(str1);//调用拷贝构造
// zdc::string str3(move(str1));//调用移动构造
//
//
// return 0;
//}
namespace zdc
{
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& x = T())
:_data(x)
, _next(nullptr)
, _prev(nullptr)
{}
list_node(T&& x)
:_data(std::forward<T>(x))
, _next(nullptr)
, _prev(nullptr)
{}
};
// typedef __list_iterator<T, T&, T*> iterator;
// typedef __list_iterator<T, const T&, const T*> const_iterator;
// 像指针一样的对象
template<class T, class Ref, class Ptr>
struct __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> iterator;
typedef bidirectional_iterator_tag iterator_category;
typedef T value_type;
typedef Ptr pointer;
typedef Ref reference;
typedef ptrdiff_t difference_type;
Node* _node;
__list_iterator(Node* node)
:_node(node)
{}
bool operator!=(const iterator& it) const
{
return _node != it._node;
}
bool operator==(const iterator& it) const
{
return _node == it._node;
}
// *it it.operator*()
// const T& operator*()
// T& operator*()
Ref operator*()
{
return _node->_data;
}
//T* operator->()
Ptr operator->()
{
return &(operator*());
}
// ++it
iterator& operator++()
{
_node = _node->_next;
return *this;
}
// it++
iterator operator++(int)
{
iterator tmp(*this);
_node = _node->_next;
return tmp;
}
// --it
iterator& operator--()
{
_node = _node->_prev;
return *this;
}
// it--
iterator operator--(int)
{
iterator tmp(*this);
_node = _node->_prev;
return tmp;
}
};
template<class T>
class list
{
typedef list_node<T> Node;
public:
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//typedef __reverse_iterator<iterator, T&, T*> reverse_iterator;
//typedef __reverse_iterator<const_iterator, const T&, const T*> //const_reverse_iterator;
const_iterator begin() const
{
return const_iterator(_head->_next);
}
const_iterator end() const
{
return const_iterator(_head);
}
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return iterator(_head);
}
/*reverse_iterator rbegin()
{
return reverse_iterator(end());
}
reverse_iterator rend()
{
return reverse_iterator(begin());
}*/
void empty_init()
{
// 创建并初始化哨兵位头结点
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
template <class InputIterator>
list(InputIterator first, InputIterator last)
{
empty_init();
while (first != last)
{
push_back(*first);
++first;
}
}
list()
{
empty_init();
}
void swap(list<T>& x)
//void swap(list& x)
{
std::swap(_head, x._head);
}
// lt2(lt1)
list(const list<T>& lt)
{
empty_init();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// lt1 = lt3
list<T>& operator=(list<T> lt)
{
swap(lt);
return *this;
}
~list()
{
clear();
delete _head;
_head = nullptr;
}
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
void push_back(const T& x)
{
//Node* tail = _head->_prev;
//Node* newnode = new Node(x);
_head tail newnode
//tail->_next = newnode;
//newnode->_prev = tail;
//newnode->_next = _head;
//_head->_prev = newnode;
insert(end(), x);
}
void push_back(T&& x)
{
insert(end(), std::forward<T>(x));
}
void push_front(const T& x)
{
insert(begin(), x);
}
iterator insert(iterator pos, const T& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(x);
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* newnode = new Node(std::forward<T>(x));
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
void pop_back()
{
erase(--end());
}
void pop_front()
{
erase(begin());
}
iterator erase(iterator pos)
{
assert(pos != end());
Node* cur = pos._node;
Node* prev = cur->_prev;
Node* next = cur->_next;
prev->_next = next;
next->_prev = prev;
delete cur;
return iterator(next);
}
private:
Node* _head;
};
}
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
// 完美转发:保持t引用对象属性
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
bit::list<bit::string> lt;
bit::string s1("hello");
lt.push_back(s1);
cout << "----------------------------------" << endl;
//lt.push_back(bit::string("world"));
lt.push_back("world");
return 0;
}
这是一段实现完美转发的代码,完美转发在右值的版本必须有模板,需要一层一层往下用std::forward来进行转右值不然会退化成为左值。
看下面这张图,我有一个位置没有完美转发就退化成为左值了
三.新的类功能
3.1.新增的默认成员函数
在原先c++中有6个默认成员函数:
1.构造函数 2.析构函数 3.拷贝构造函数 4.拷贝构造函数 5.取地址重载函数 6. const取地址重载
其实这里重要的是前四个,后两个用处不大,默认成员函数不写编译器会自动生成一个默认的,在c++11中 新增了两个 前面提前介绍了 分别是 移动构造和移动赋值。
如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,目没有实现析构函数。接贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
这是没有实现移动构造和移动赋值 ,编译器自动生成的
类成员变量初始化:C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原 因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以 使用default关键字显示指定移动构造生成。
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁 已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
可变参数模板:
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改 进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的.
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
//省略号就是可变参数模板
3.2.lambda表达式
lambda表达式c++好像是借鉴了别的语言,这个语法设计是为了代替c语言的函数模板,在c++里面是为了方便仿函数。
lambda表达式各部分说明
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
//写一个最简单的landaa
int main()
{
//由于landa表达式没有结果所以用auto推导
auto add = [](int a, int b) {return a + b; };
cout << add(3, 5);
return 0;
}
2.捕捉列表示例:
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
int main()
{
int x = 3;
int y = 5;
//捕捉列表 但是由于是函数调用 swap函数无法影响外面的
auto swap1 = [&x, &y]()mutable//[x,y]捕捉x y
{
//加上引用 就是传引用捕捉就可以改变了
int tmp = x;
x = y;
y = tmp;
};
swap1();
cout << x << y << endl;
return 0;
}
在编译器的底层 会把landa表达式通过算法转换成uuid 数字的仿函数
3.3.包装器(了解)
function包装器 function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。
使用说明:function<返回值(参数值)> f=绑定的对象
//介绍一下基本包装器使用场景 其实landa也可以使用包装器
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int add(int x, int y)
{
return x + y;
}
int main()
{
function<int(int, int)> f1 = add;
//<要包装的类型(需要传递的类型)> 2=需要包装的函数或者仿函数
cout<<f1(2, 4);
function<double(Plus, double, double)> f4 = &Plus::plusd;
//如果绑定的是对象需要加上对象名
// f4(Plus(), 1.1, 2.2);
return 0;
}
3.4.绑定
bind std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可 调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而 言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺 序调整等操作
可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对 象来“适应”原对象的参数列表。 调用bind的一般形式:auto newCallable = bind(callable,arg_list); 其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中 的参数。 arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示 newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对 象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int add(int x, int y)
{
return x + y;
}
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,
placeholders::_2);
//auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
//func2的类型为 function<void(int, int, int)> 与func1类型一样
//表示绑定函数 plus 的第一,二为: 1, 2
auto func2 = std::bind(Plus, 1, 2);
cout << func1(1, 2) << endl;
cout << func2() << endl;
return 0;
}
3.5.线程库(这个下次在补充)
四.智能指针
4.1.什么是智能指针
在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
4.2.内存泄漏
什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现 内存泄漏会导致响应越来越慢,最终卡死。
如何避免内存泄漏:
1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps: 这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智 能指针来管理才有保证。
2. 采用RAII思想或者智能指针来管理资源。 } return 0; } void MemoryLeaks() { // 1.内存申请了忘记释放 int* p1 = (int*)malloc(sizeof(int)); int* p2 = new int;
// 2.异常安全问题 int* p3 = new int[10]; Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放. delete[] p3; }
3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
4. 出问题了使用内存泄漏工具检测。
4.3.智能指针的使用及其原理(重要)
RAII:(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做 法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。
智能指针是一个类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源
通俗的来说:
1.获取到资源马上初始化,把自己构造和析构交给别的类帮忙释放
2.像指针一样 。
3.拷贝问题
//利用RAII思想模拟的 smartptr
// 1、利用RAII思想设计delete资源的类
// 2、像指针一样的行为
// 3、拷贝问题
智能指针//
template<class T>
class smartPtr
{
public:
smartPtr(T* ptr)
:_ptr(ptr)
{}
~smartPtr()
{
cout << "delete调用" << _ptr << endl;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
4.4.c++ 98 auto_ptr 介绍
主要是为了解决“有异常抛出时发生内存泄漏”的问题 。因为发生异常而无法正常释放内存。
auto_ptr有拷贝语义,拷贝后源对象变得无效,这可能引发很严重的问题;而unique_ptr则无拷贝语义,但提供了移动语义,这样的错误不再可能发生,因为很明显必须使用std::move()进行转移。
auto_ptr不支持拷贝和赋值操作,不能用在STL标准容器中。STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,所以不能在STL中使用。
这个智能指针设计 要注意 可能会造成空指针问题
class A
{
public:
~A()
{
cout << "~a ()" << endl;
}
//private:
int _a1 = 0;
int _a2 = 0;
};
int main()
{
// C++98
// auto_ptr 资源管理权转移,不负责任的拷贝,会导致被拷贝对象悬空
//不提倡使用auto_ptr因为可能发生空指针解引用
std::auto_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
std::auto_ptr<A> ap2(ap1);
ap2->_a1++;
ap2->_a2++;
return 0;
}
模拟实现auto_ptr
namespace zdc
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr=nullptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)//拷贝构造
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T> ap)
{
if (this != &ap)
{
//自己给自己赋值
if (_ptr)
{
//如果指针不为空
//指向新资源
cout << "调用delete " << _ptr << endl;
delete _ptr;
}
//ap2 转移给ap1
//置空
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
//析构
~auto_ptr()
{
if (_ptr)
{
cout << " delete析构" << _ptr << endl;
delete _ptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
};
}
4.5.uniqe_ptr(唯一的指针)
unique_ptr采用的是独享所有权语义,一个非空的unique_ptr总是拥有它所指向的资源。转移一个unique_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空;所以unique_ptr不支持普通的拷贝和赋值操作,不能用在STL标准容器中;局部变量的返回值除外(因为编译器知道要返回的对象将要被销毁);如果你拷贝一个unique_ptr,那么拷贝结束后,这两个unique_ptr都会指向相同的资源,造成在结束时对同一内存指针多次释放而导致程序崩溃
这个指针设计思路:禁止拷贝
看图片画红色波浪线的位置:编译器直接禁止拷贝
模拟实现 uniqe_ptr这个思路就是把拷贝禁用了
template<class T>
class uniqe_ptr
{
public:
uniqe_ptr(T* ptr=nullptr)
:_ptr(ptr)
{
}
//uniqe_ptr 禁止拷贝
uniqe_ptr(uniqe_ptr<T>& ap) = delete;
uniqe_ptr<T>& operator=(uniqe_ptr<T>& ap) = delete;
~uniqe_ptr()
{
if (_ptr)
{
delete _ptr;
}
}
T& operator*()
{
return _ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
4.6. shared_ptr
实现原理:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。
- 智能指针将一个计数器与类指向的对象相关联,引用计数器跟踪共有多少个类对象共享同一指针
- 每次创建类的新对象时,初始化指针并将引用计数置为1
- 当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数
- 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
- 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)
template<typename T>
class SharedPtr
{
public:
SharedPtr(T* ptr = NULL):_ptr(ptr), _pcount(new int(1))
{}
SharedPtr(const SharedPtr& s):_ptr(s._ptr), _pcount(s._pcount){
(*_pcount)++;
}
SharedPtr<T>& operator=(const SharedPtr& s){
if (this != &s)
{
if (--(*(this->_pcount)) == 0)
{
delete this->_ptr;
delete this->_pcount;
}
_ptr = s._ptr;
_pcount = s._pcount;
*(_pcount)++;
}
return *this;
}
T& operator*()
{
return *(this->_ptr);
}
T* operator->()
{
return this->_ptr;
}
~SharedPtr()
{
--(*(this->_pcount));
if (*(this->_pcount) == 0)
{
delete _ptr;
_ptr = NULL;
delete _pcount;
_pcount = NULL;
}
}
private:
T* _ptr;
int* _pcount;//指向引用计数的指针
};
4.7.weak_ptr
weak_ptr:弱引用。 引用计数有一个问题就是互相引用形成环(环形引用),这样两个指针指向的内存都无法释放。需要使用weak_ptr打破环形引用。weak_ptr是一个弱引用,它是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前使用函数lock()检查weak_ptr是否为空指针。
4.8.智能指针的循环引用
循环引用是指使用多个智能指针share_ptr时,出现了指针之间相互指向,从而形成环的情况 指针往往不能正常调用对象的析构函数,从而造成内存泄漏
#include <iostream>
using namespace std;
template <typename T>
class Node
{
public:
Node(const T& value)
:_pPre(NULL)
, _pNext(NULL)
, _value(value)
{
cout << "Node()" << endl;
}
~Node()
{
cout << "~Node()" << endl;
cout << "this:" << this << endl;
}
shared_ptr<Node<T>> _pPre;
shared_ptr<Node<T>> _pNext;
T _value;
};
void Funtest()
{
shared_ptr<Node<int>> sp1(new Node<int>(1));
shared_ptr<Node<int>> sp2(new Node<int>(2));
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
sp1->_pNext = sp2; //sp2的引用+1
sp2->_pPre = sp1; //sp1的引用+1
cout << "sp1.use_count:" << sp1.use_count() << endl;
cout << "sp2.use_count:" << sp2.use_count() << endl;
}
int main()
{
Funtest();
system("pause");
return 0;
}
//输出结果
//Node()
//Node()
//sp1.use_count:1
//sp2.use_count:1
//sp1.use_count:2
//sp2.use_count:2
从上面shared_ptr的实现中我们知道了只有当引用计数减减之后等于0,析构时才会释放对象,而上述情况造成了一个僵局,那就是析构对象时先析构sp2,可是由于sp2的空间sp1还在使用中,所以sp2.use_count减减之后为1,不释放,sp1也是相同的道理,由于sp1的空间sp2还在使用中,所以sp1.use_count减减之后为1,也不释放。sp1等着sp2先释放,sp2等着sp1先释放,二者互不相让,导致最终都没能释放,内存泄漏。
智能指针出现循环引用怎末解决:弱指针用于专门解决shared_ptr循环引用的问题,weak_ptr不会修改引用计数,即其存在与否并不影响对象的引用计数器。循环引用就是:两个对象互相使用一个shared_ptr成员变量指向对方。弱引用并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。