自学内容网 自学内容网

【c++复习--c++11新增语法第一节】——本内容会讲到c++11新增的比较实用的语法,比如智能指针、lambda表达式、新增加的容器、还有右值引用

1.c++11简介

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 增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节课程
主要讲解实际中比较实用的语法。
2.统一的列表初始化
2.1 {}初始化
在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扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

struct Point
{
 int _x;
 int _y;
};
int main()
{
 int x1 = 1;
 int x2{ 2 };
 int array1[]{ 1, 2, 3, 4, 5 };
 int array2[5]{ 0 };
 Point p{ 1, 2 };
 // C++11中列表初始化也可以适用于new表达式中
 int* pa = new int[4]{ 0 };
 return 0;
}

创建对象时也可以使用列表初始化方式调用构造函数初始化

#include<iostream>

class Date{
public:
    Date(int year,int month,int day)
    :_year(year),_month(month),_day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    Date d1(2022,1,1);
    //c++11支持列表初始化,这里会调用构造函数初始化
    Date d2{2022,1,2};
    Date d3 = {2022,1,2};
    return 0;
}

2.2 std::initializer_list 

我个人认为std::initializer_list 可以理解为一个结构体,通过花括号来初始化然后通过这个结构体来对相应的对象初始化,下面我通过代码将这个类型打印出来

std::initializer_list 一般是作为构造函数的参数,c++11对STL中的不少容器就增std::initializer_list作为参数的构造函数,这样就可以用大括号赋值

#include<vector>
#include<list>
#include<string>
#include<map>


int main()
{
    std::vector<int> v = {1,2,3,4};
    std::list<int> lt = {1,2};
    std::map<std::string, std::string> dict = { {"sort", "排序"}, {"insert", "插入"} };
    //使用大括号对容器赋值
    v = {10,20,30};
    return 0;
}

下面我们写的vector支持{}初始化,这里提一下typename std::initializer_list<T>这里的typename是用来告诉编译器T是一个类型,这属于模版进阶部分的内容,我后面会单独出一期来讲

namespace zkj
{
template<class T>
class vector {
public:
typedef T* iterator;
vector(std::initializer_list<T> l)
{
_start = new T[l.size()];
_finish = _start + l.size();
_endofstorage = _start + l.size();
iterator vit = _start;
typename std::initializer_list<T>::iterator lit = l.begin();
while (lit != l.end())
{
*vit++ = *lit++;
}
//for (auto e : l)
//   *vit++ = e;
}
vector<T>& operator=(std::initializer_list<T> l) {
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
private:
iterator _start;
iterator _finish;
iterator _endofstorage;
};
}

int main()
{
zkj::vector<int> arry{ 1,2,3,4 };
zkj::vector<int> arry2 = { 1,2,3,4 };
return 0;
}

3.声明

3.1 auto

在c++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没有什么价值了。c++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型

int main()
{
    int i = 10;
    auto p = &i;
    cout << typeid(p).name() << endl;
    map<string, string> dict = {{"sort", "排序"}, {"insert", "插入"}};
    // map<string, string>::iterator it = dict.begin();
    auto it = dict.begin();
    return 0;
}

3.2 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;
}

3.3 nullptr

用于c++中NULL被定义成字面量0,这样就可能会带来一些问题,因为0既能表示指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,c++11中新增了nullptr,用于表示空指针

因为智能指针和范围for在前面已经讲过了这里不做叙述

4 右值引用和移动语义

4.1 左值引用和右值引用

        在c++11以前并没有右值引用这种说法。不论是左值引用还是右值引用都是给对象取别名

什么是左值?什么是左值引用?

        左值是一个表示数据的表达式(如变量名或解引用的指针)我们可以获取它的地址+可以对它赋 值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左

值,不能给他赋值,但是可以取它的地址。

        左值引用就是给左值的引用,给左值取别名。

int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;

什么是右值?什么是右值引用?

        右值是一个表示数据的表达式,如字面常量、表达式返回值、函数返回值(函数返回值不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址

        右值引用就是对右值的引用,给右值取别名

int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}

        右值不能取地址,但是给右值取别名后,后导致被存储到特定的位置,且可以取到该位置的地址,也就是说:我们不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1,如果不想rr1被修改,可以用const int&& rr1取引用。

4.2 左值引用与右值引用比较

左值引用总结:

        左值引用只能引用左值,不能引用右值

        const左值引用即可以引用左值,也可以引用右值


int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a;   // ra为a的别名
//int& ra2 = 10;   // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}

右值引用总结

        右值引用只能引用右值,不能引用左值

        右值引用可以可以引用move以后的左值

int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;

// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}

4.3右值引用的场景和意义

#include<iostream>
//using namespace std;

namespace zkj
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{

//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
std::cout << "string(const string& s) -- 深拷贝" << std::endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
std::cout << "string& operator=(string s) -- 深拷贝" << std::endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
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)
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; // 不包含最后做标识的\0
};

}

zkj::string func()
{
zkj::string str("xxxxxxxxxxxxxxxxxxxxxxx");
return str;
}


int main()
{
zkj::string str1 = func(); //为什么没有掉拷贝构造
zkj::string str2;
str2 = func();

return 0;
}

上面这份代码的运行结果

        对于这个结果我个人还有疑惑,我的预期是str1那里调一次深拷贝(新一点的编译器会对连续的拷贝进行优化)str2哪里调两次深拷贝(因为我的赋值重载用到是现代写法所以会多一次深拷贝),但是我跑出来的结果竟然是上面这样,这里我认为可能编译器(vs2022)又做了一些优化,导致我的func这个传值返回没有发生深拷贝,所以就只打印了那个赋值重载又因为是现代的写法所以又掉用了一次拷贝构造也就打印了上面的两条信息。

        但不论怎么说多次的深拷贝都不是我们希望的,而且拷贝的对象在拷贝完成后就销毁了,那为什么我们不将他的内容直接拿过来,还要去拷贝这是一种浪费。这里讲一下对于内置类型的右值我们叫纯右值,自定义类型的右值我们叫将亡值。

        上面的func传值返回会发生拷贝产生一个临时对象用于赋值,其实这个临时对象就是一个将亡值,我们可以直接将他的资源转移,并把我们不需要的资源丢给他。基于这个思想我们在string这个类里提供移动构造(其实就是参数是右值的构造函数),和移动赋值(其实就是参数是右值的赋值重载)

#include<iostream>
using namespace std;

namespace zkj
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{

//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
std::cout << "string(const string& s) -- 深拷贝" << std::endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
std::cout << "string& operator=(string s) -- 深拷贝" << std::endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动构造
string(string && s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string && s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
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)
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; // 不包含最后做标识的\0
};

}

zkj::string func()
{
zkj::string str("xxxxxxxxxxxxxxxxxxxxxxx");
return str;
}


int main()
{
zkj::string str1 = func(); //为什么没有掉拷贝构造
zkj::string str2;
str2 = func();

return 0;
}

运行结果如下

这样我们就减少了拷贝,大大提高了代码的效率。其实这里还可以讲讲func的返回值str,它本身是一个左值但是经过编译器处理后认为是一个右值,这样在产生临时对象是也是通过移动构造来实现的,这样的目的也是为了提高代码的效率。

移动构造和移动赋值的本质试讲右值的资源窃取过来,占为己有,就可以避免做深拷贝而导致效率降低,所以它叫做移动构造、移动赋值。字面理解就是窃取别人(这个别人一般指将亡值)的资源来构造或赋值自己。

4.4 右值引用引用左值及其一些更深入的使用场景分析

        这里重温一下右值引用不一定只能引用右值,还可以引用move后的左值,move这个函数并不搬运任何东西,唯一的功能就是将一个左值强制转化为右值,然后实现移动语义

int main()
{
    bit::string s1("hello world");
    // 这里s1是左值,调用的是拷贝构造
    bit::string s2(s1);
    // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
    // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
    // 资源被转移给了s3,s1被置空了。
    bit::string s3(std::move(s1));
    return 0;
}

4.5 完美转发

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; }

上面的例子是一些常见的左值引用和右值引用。

但是在模板中&&并不代表左值引用,而是万能引用,其既能接收左值也能接受右值

模板的万能引用只是提供了能够同时接收左值引用和右值引用的功能

但是引用类型的唯一作用就是限制了接收的类型,万能引用在后续使用中都退化成了左值

我们希望能够在传递过程中保持它的左值或右值的属性,就需要用到完美转发(std::forward())

完美转发在传参过程中保留对象原生类型属性

完美转发实际中的使用场景

template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void PushFront(T&& x)
{
//Insert(_head->_next, x);
Insert(_head->_next, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
void Insert(Node* pos, const T& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = x; // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
int main()
{
List<zkj::string> lt;
lt.PushBack("1111");
lt.PushFront("2222");
return 0;
}

其实上面这个代码最精华的部分就是在&&这个万能引用的处理上,我们希望每个节点的_data的属性不发生改变我们通过函数重载和万能转发来保证_data的属性不发生改变。


原文地址:https://blog.csdn.net/2301_79644919/article/details/144173379

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!