C++11的简单介绍(上)

简介: C++11的简单介绍(上)

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增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本篇博文主要讲解实际中比较实用的语法。

这里我给出一个官方的C++11的介绍,大家可以自行阅读学习:

https://en.cppreference.com/w/cpp/11

C++11名字的由来:

1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也

完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为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 };//开辟出4个整形int的空间并且初始化为0
 return 0;
}

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

例如我们之前学习的日期类时创建日期类的对象时我们就可以用这种列表初始化方法来进行初始化,但实际上他是调用类的构造函数

class Date
{
public:
 Date(int year, int month, int day)
 :_year(year)
 ,_month(month)
 ,_day(day)
 {
 cout << "Date(int year, int month, int day)" << endl;
 }
private:
 int _year;
 int _month;
 int _day;
 };
int main()
{
 Date d1(2022, 1, 1); // old style
 // C++11支持的列表初始化,这里会调用构造函数初始化
 Date d2{ 2022, 1, 2 };
 Date d3 = { 2022, 1, 3 };
 return 0;
}
2.2std::initializer_list

std::initializer_list的中文翻译就是初始化列表,例如:

il就是一个std::initializer_list

il = { 10, 20, 30 }

我们可以用typeid.name来打印他的类型

int main()
{
  auto il = { 10, 20, 30 };
  cout << typeid(il).name() << endl;
  return 0;
}

输出结果如下:

std::initializer_list使用场景:

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值

例如:

int main()
{
 vector<int> v = { 1,2,3 };
// 使用大括号对容器赋值
v = {10, 20, 30};
 return 0;

我们还可以用它来使模拟实现的vector用{}进行初始化和赋值

namespace jh
{
template<class T>
class vector {
public:
     typedef T* iterator;
     //initializer_list作为vector构造函数的参数
     vector(initializer_list<T> l)
     {
         _start = new T[l.size()];
         _finish = _start + l.size();
         _endofstorage = _start + l.size();
         iterator vit = _start;
         typename initializer_list<T>::iterator lit = l.begin();
         while (lit != l.end())
         {
             *vit++ = *lit++;
         }
         //for (auto e : l)
         //   *vit++ = e;
     }
     vector<T>& operator=(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;
 };
}

3.声明

3.1 auto

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

例如:

int main()
{
  int i = 10;
  auto p = &i;
  auto pf = strcpy;
  cout << typeid(p).name() << endl;
  cout << typeid(pf).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将变量的类型声明为表达式指定的类型。用来推导表达式的类型的,并且用这个类型实例化模板参数。

例如:

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的类型是const 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,用于表示空指针。

4.STL中一些变化

4.1 新容器

用橘色圈起来是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。这两个我们前面已经进行了非常详细的讲解,其他的大家了解一下即可。

这里面的bitset就是我们的位图

而array这个容器可以用来代替静态数组,它的越界检查比较严格

至于forward_list,他的头插和头删的效率相对于list要高,并且它比list节省空间

4.2 容器中的一些新方法

如果我们再细细去看会发现基本每个容器中都增加了一些C++11的方法,但是其实很多都是用得比较少的。

比如说:

C++11的容器就同意增加了initializer_list的构造函数

比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本,稍后我们会有提到

5.右值引用和移动语义

5.1左值引用和右值引用

传统的C++语法中就有引用的语法,而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 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

5.2左值引用和右值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  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;
}

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以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;
}
5.2右值引用使用场景和意义

左值引用的使用场景:

做参数和做返回值都可以提高效率。

void func1(jh::string s)
{}
void func2(const jh::string& s)
{}
int main()
{
 jh::string s1("hello world");
 // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
 func1(s1);
 func2(s1);
 // string operator+=(char ch) 传值返回存在深拷贝
 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
 s1 += '!';
 return 0;
}

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:jh::string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

我们就可以用右值引用解决这个问题,右值引用和移动语义解决上述问题:

jh::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己

移动构造个数如下:

// 移动构造
string(string&& s)
 :_str(nullptr)
  ,_size(0)
 ,_capacity(0)
{
 cout << "string(string&& s) -- 移动语义" << endl;
 swap(s);
}
int main()
{
 jh::string ret2 =jh::to_string(-1234);
 return 0;
}

不仅仅有移动构造,还有移动赋值:

在jh::string类中增加移动赋值函数,再去调用jh:to_string(1234),不过这次是将jh::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}

这里运行后,我们看到调用了一次移动构造和一次移动赋值。**因为如果是用一个已经存在的对象接收,编译器就没办法优化了。jh::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。**然后在把这个临时对象做为jh::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

但其实,在移动构造中还进行了一次拷贝构造,只是C++11将一次移动构造和一次拷贝构造直接优化成了一次移动构造

STL中的容器都是增加了移动构造和移动赋值:

http://www.cplusplus.com/reference/string/string/string/

http://www.cplusplus.com/reference/vector/vector/vector/

其实左值引用的效率高的原因就是因为它减少了拷贝,而右值引用也是减少拷贝,但是右值引用是在拷贝构造的时候减少了拷贝转而进行了移动构造从而提高了效率,不然就要进行两次拷贝构造!

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

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

5.4 完美转发

模板中的&& 万能引用

模板中的**&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力**,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发

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)
{
 Fun(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 右值
 return 0;
}

其实完美转发又名折叠引用,因为当引用对象为左值是&就会进行折叠

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

例如:

下面这段代码中在模板中没有使用完美转发

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<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
  Fun(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 右值
  return 0;
}

输出结果如下:

然而使用完美转发后:

可以看到最后一次的输出结果不一样,完美转发后完美地保留了对象的原生类型属性

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

如果你使用的函数的是调用其他的函数来实现的,那么你调用的函数也要进行完美转发的修饰:

例如:

下面代码中的头插pushfront和pushback是调用insert,那么insert中的右值引用版本的插入值的语句也要用完美转发

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<jh::string> lt;
 lt.PushBack("1111");
 lt.PushFront("2222");
 return 0;
}

6.新的类的功能

6.1默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

例如:

下面代码中的person类我们没有实现它的移动构造,析构函数,拷贝构造和拷贝赋值中的任意一个age是内置类型,name是自定义类型,string类中我们实现了移动构造,所以会调用string类的移动构造

class Person
{
public:
  Person(const char* name = "", int age = 0)
    :_name(name)
    , _age(age)
  {}
  
private:
  jh::string _name;
  int _age;
};
int main()
{
  Person s1;
  Person s2 = s1;
  Person s3 = std::move(s1);
  Person s4;
  s4 = std::move(s2);
  return 0;
}

6.2强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p)
 :_name(p._name)
 ,_age(p._age)
 {}
Person(Person&& p) = default;
private:
 jh::string _name;
 int _age;
};
int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

6.3禁止生成默认函数的关键字delete:

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数

class Person
{
public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}
 Person(const Person& p) = delete;
private:
jh::string _name;
 int _age;
};
int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

6.可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。

下面就是一个基本可变参数的函数模板:

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。

递归函数方式展开参数包

// 递归终止函数
template <class T>
void ShowList(const T& t)
{
 cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
 cout << value <<" ";
 ShowList(args...);
}
int main()
{
 ShowList(1);
 ShowList(1, 'A');
 ShowList(1, 'A', std::string("sort"));
 return 0;
}

好了,今天的分享到这里就结束了,感谢大家的支持!

相关文章
|
7月前
|
存储 Linux 编译器
vim编辑器和gcc/g++编辑器的使用讲解
vim编辑器和gcc/g++编辑器的使用讲解
159 2
mitt.js:小型事件发布订阅库
mitt.js:小型事件发布订阅库
1605 0
|
7月前
|
存储 数据采集 数据挖掘
【软件设计师备考 专题 】数据仓库和分布式数据库基础知识
【软件设计师备考 专题 】数据仓库和分布式数据库基础知识
264 0
|
7月前
|
Java 程序员 API
【深入探究 Qt 线程】一文详细解析Qt线程的内部原理与实现策略
【深入探究 Qt 线程】一文详细解析Qt线程的内部原理与实现策略
605 0
|
7月前
|
机器学习/深度学习 数据采集 PyTorch
【单点知识】基于实例讲解PyTorch中的ImageFolder类
【单点知识】基于实例讲解PyTorch中的ImageFolder类
183 0
|
7月前
|
机器学习/深度学习 人工智能 计算机视觉
2023年12月 论文推荐
12月已经过了一半了,还有2周就是2024年了,我们来推荐下这两周我发现的一些好的论文,另外再推荐2篇很好的英文文章。
174 1
|
7月前
|
域名解析 缓存 网络协议
【域名解析DNS专栏】DNS解析过程深度解析:一次完整的域名查询旅程
【5月更文挑战第21天】DNS系统将人类友好的域名(如www.example.com)转化为IP地址,涉及递归和迭代查询。当用户输入域名,浏览器查询本地DNS缓存,未命中则向本地DNS服务器发起请求。本地服务器向根域名服务器查询,根服务器指引到对应顶级域名的权威DNS,权威DNS提供IP地址。Python示例代码展示了这一过程。了解DNS解析有助于理解互联网运作并优化网络资源管理。
216 2
【域名解析DNS专栏】DNS解析过程深度解析:一次完整的域名查询旅程
|
7月前
栈和队列的实现(详解+图解!文末附完整代码)
栈和队列的实现(详解+图解!文末附完整代码)
225 2
|
7月前
|
C语言
猜数字游戏(详解+代码)
猜数字游戏(详解+代码)
272 1
|
7月前
|
存储 编译器
数据结构之顺序表的实现(详解!附完整代码)
数据结构之顺序表的实现(详解!附完整代码)
131 1