C++11特性(一)

C++11 简介

在C++98标准之后,C++标准委员会于2003年其实还提出过一份技术勘误表(TC1),使得C++03 这个名字已经取代了C++98为C++11之前的最新的C++标准名称。不过由于 TC1 主要是对 C++98 标准中的漏洞进行修复,语言的核心部分并没有改动,因此大多数人习惯将这两个标准合称为 C++98/03
相比于 C++98/03 ,C++11 带来了数量可观的变化,不仅增加了新的特性,并且对 C++03 的漏洞进行了一定的修正。相比而言,C++11 能更好地用于系统开发和库开发,语言更加泛化和简单化,更加稳定和安全,不仅功能更强大,而且能提高开发效率

列表初始化

C++98 中的初始化

在C++98标准中,允许使用花括号对数组元素进行统一的列表初始化值设定。如:

int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = {0};

但是,对于一些自定义类型却无法使用这种方式的初始化。如:

vector<int> v{ 1, 2, 3, 4, 5 };

如上述例子中,C++98 标准就无法通过编译。需要将 vector 首先定义出来,然后循环对其赋初始值,这样的话显得十分不方便…

** C++11 扩大了用花括号括起来的列表的适用范围,使其可以用于所有的内置类型 和 用户自定义的类型,使用初始化列表时,可以加“=”,也可以不加 **

C++11 的初始化

内置类型的列表初始化

int main()
{
    // 内置类型变量
    int a = {10};
    int b{10};
    int c = 10 + 20;
    int d = {10 + 20};
    int e{10 + 20};
    
    // 数组
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = {0};

    // 动态数组----C++11
    int* array3 = new  int[5]{ 1, 2, 3, 4, 5 };

    // STL容器----C++11
    std::vector<int> v{ 1, 2, 3, 4, 5 };
    std::map<int, int> m{{1, 1}, {2, 2}, {10, 20}};

    return 0;
}

自定义类型的列表初始化

1、对单个自定义类型对象的列表初始化

class person
{
public:
    person(std::string = "", int age = 0)
        : _name(name)
        , _age(age)
    {}
private:
    std::string _name;
    int _age;
};

int main()
{
    person p1{"AdamXi", 20};
    return 0;
}

2、对多个自定义类型对象的列表初始化
多个对象想要支持列表初始化,需要给该类(模板类)添加一个带有 initializer_list 类型参数的构造函数即可。
【注意】:initializer_list 是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法 size()

#include 

template<class T>
class Vector
{
public:
    // ...
    Vector(initializer_list<T> list)
        : _capacity(list.size())
        , _size(0)
    {
        _array = new T[_capacity];
        for(auto e  : list)
        {
            _array[_size++] = e;
        }
    }

    Vector<T>& operator=(std::initializer_list<T> list)
    {
        delete[] _array;
        size_t i = 0;
        for(auto e : list)
        {
            _array[i++] = e;
        }
        return *this;
    }
    // ...
private:
    T* _array;
    size_t _size;
    size_t _capacity;
};

initializer_list ---- C++11

其实 initializer_list 不单单只是上述代码中的对多个自定义类型对象的列表初始化,它还有更多的用处:

std::vector<int> v({ 1, 2, 3, 4, 5 });

在上述例子中,在实例化对象时候调用了构造函数,传进去的实参是 { 1, 2, 3, 4, 5 },这样就会创建一个 initializer_list 的临时对象,用来构造vector
在C语言中的printf、scanf等就是变长参数列表,可是在使用这一类的变长参数列表时,它们的共性就是第一个参数为格式化字符串,**C语言必须要通过第一个格式化字符串中的格式控制符来确定后面的参数个数。**这样的话,如果不遵守这个规则的话就会报错。那么,此时,initializer_list 它就可以起到作用:

void print(std::initializer_list<int> list)
{
	for (auto it = list.begin(); it != list.end(); it++)
	{
		std::cout << *it << " ";
	}
	std::cout << endl;
}

int main()
{
	printf("%d %d %d %d %d\n", 1, 2, 3, 4, 5);
	print({ 1, 2, 3, 4, 5 });

	return 0;
}

当然,这样的话,细心的朋友会发现,initializer_list 对象里面的话能放置相同类型的参数。当然同样也有办法放置不同类型的参数----C++17中引入了一个万能容器any,有兴趣的朋友有可以了解一下…

变量类型推导

为什么需要类型推导

在定义变量时候,需要先给出变量的实际类型,编译器才允许定义,但在有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂。如:

int main()
{
	short a = 32765;
	short b = 32766;
	// c如果给成short,会造成数据丢失,如果能让编译器根据 a+b 的结果推导出c的实际类型,就不会存在问题
	short c = a + b;

	std::map<std::string, int> stu{ { "AdamXi", 20 }, { "Sofency", 22 } };
	// 使用迭代器遍历容器,迭代器类型太过繁琐
	std::map<std::string, int>::iterator it = stu.begin();
	while (it != stu.end())
	{
		std::cout << it->first << "\t" << it->second << std::endl;
		++it;
	}
	return 0;
}

auto 类型推导

早在C++98 中就已经存在了 auto 关键字,那时的 auto 用于声明变量为自动变量,自动变量意为拥有自动的生命周期,只是多余的,因为即使没有用auto声明,变量依旧拥有自动的生命周期
C++11 中删除了auto的旧有语法,取而代之的是全新的auto关键字。其作用有如下几点:

  1. 用于代替冗长代码
    在上述代码中进行上述改动:
int main()
{
	short a = 32765;
	short b = 32766;
	// c如果给成short,会造成数据丢失,如果能让编译器根据 a+b 的结果推导出c的实际类型,就不会存在问题
	auto c = a + b;

	std::map<std::string, int> stu{ { "AdamXi", 20 }, { "Sofency", 22 } };
	// 使用迭代器遍历容器,迭代器类型太过繁琐
	auto it = stu.begin();
	while (it != stu.end())
	{
		std::cout << it->first << "\t" << it->second << std::endl;
		++it;
	}
	std::cout << typeid(c).name() << std::endl;
	std::cout << typeid(it).name() << std::endl;

	return 0;
}

运行结果如下:

AdamXi 20
Sofency 22
int
class std::_Tree_iterator const, int> > > >

  1. 在定义模板函数时,用于声明模板参数的变量类型
template <typename _Tx,typename _Ty>
void Multiply(_Tx x, _Ty y)
{
    auto v = x*y;
    std::cout << v;
}
  1. 模板函数依赖于模板参数的返回值
template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
    return x*y;
}

此时auto作用仅为占位符,真正的返回值是后面的 decltype(_Tx*_Ty)。那为什么要这样做呢?
原因:如果没有后置返回值,则函数声明时为:

decltype(_Tx*_Ty)multiply(_Tx x, _Ty y)

而此时_Tx 和 _Ty都还未曾声明过,编译自然无法通过

decltype 类型推导

有了上面的例子中 decltype 的应用,下面还是简单介绍一下:
auto 使用的前提是:必须要对 auto 声明的类型进行初始化,否则编译器无法推导出 auto 的实际类型

decltype 是根据表达式的实际类型推演出定义变量时所用的类型。如:

  1. decltype 是根据表达式类型作为变量的定义类型
int main()
{
    int a = 10;
    int b = 20;
    decltype(a + b) c;
    std::cout << typeid(c).name() << std::endl;
	
	return 0;
}
  1. 推演函数返回值的类型
void* GetMemory(size_t size)
{
	return malloc(size);
}

int main()
{
	// 如果没有带参数,推导函数的类型
	std::cout << typeid(decltype(GetMemory)).name() << std::endl;
	// 如果带参数列表,推导的是函数返回值的类型。注意:此处只是推演,不会执行函数
	std::cout << typeid(decltype(GetMemory(0))).name() << std::endl;
	
	return 0;
}

基于范围for的循环

一个类只要提供了分别获取起始和终止迭代器的begin和end函数,就可以支持基于范围的for循环

特点

  1. 基于范围的for循环
for(元素类型 元素对象:容器对象)
{
  循环体
}
  1. 如果循环体由单条语句或者单个结构块组成,可以省略花括号
  2. 用元素对象依次结合容器对象中的每一个元素,每结合一个元素,执行依次循环体,直至容器内的所有元素都被结合完为止.
  3. 不依赖于下标元素
  4. 不需要访问迭代器
  5. 不需要定义处理函数

【注意】

  • 对map和multimap容器使用范围循环,每次拿到的元素既不是键也不是值,而是由键和值组成的pair
  • 在使用基于范围的for循环时,不能违背容器本身的约束
  • 基于范围的for循环,无论循环体执行多少次,冒号后面的表达式永远只执行一次
  • 基于范围的for循环,其底层实现依然要借助于容器的迭代器

nullptr 空值指针

C中的NULL

在C语言中,使用NULL表示空指针。实际在C中,NULL通常被如下定义:

#define NULL ((void *)0)

也就是说NULL实际上是一个 void * 的指针,然后将 void * 指针赋值给对应类型的指针的时候,隐式转换成相应的类型。而如果换做一个C++编译器来编译的话是要出错的,因为C++是强类型的,void *是不能隐式转换成其他指针类型的,所以通常情况下,编译器提供的头文件会这样定义NULL:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

C++中的 nullptr

C++中不能将 void * 类型的指针隐式转换为其他指针类型,而又为了解决空指针的问题,所以C++中引入0来表示空指针,就有了类似上面的代码来定义NULL
但是实际上,用NULL代替0表示空指针在函数重载时会出现问题,程序执行的结果会与我们的想法不同。如:

void func(void* i)
{
	cout << "func1" << endl;
}
 
void func(int i)
{
	cout << "func2" << endl;
}
 
int main()
{
	func(NULL);
	func(nullptr);
	
	return 0;
}

函数运行结果:

func2
func1

上述代码中,运行结果与我们使用NULL的初衷是相违背的,因为我们本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数版本,所以是有问题的,这就是用NULL代替空指针在C++程序中的二义性

故此,C++11 中引入了 nullptr 这个关键字来代指空指针

默认成员函数的控制

C++语法上定义:如果程序员没有显式定义构造函数、拷贝构造函数、赋值运算符重载、析构函数、取地址运算符重载 和 const类型的取地址运算符重载 这6个默认成员函数,编译器会自动生成这些默认的成员函数

但是,实际上并不是所有编译器都会这样进行实现的!

实际情况:因为有些情况下编译器生成的默认函数意义并不大,而且如果生成就需要调用 会影响程序的运行效率。因此,编译器并没有严格地按照语法实现。本文通过调研 VS2013 会发现:编译器是否会生成构造函数取决于是否需要,如果需要编译器才会生成

编译器生成构造函数的场景

  • A类具有无参 或者 带有全缺省的构造函数,B类没有显式定义任何构造函数,但B类中包含A类的对象,编译器在编译阶段会给B类生成默认的构造函数
  • 继承体系下,如果基类具有无参 或者 带有全缺省的构造函数派生类没有显式定义任何构造函数,编译器在编译阶段会给派生类生成默认的构造函数
  • 虚拟继承体系中,需要在构造对象阶段将指向虚基表的指针放在对象的前4个字节
  • 如果类中具有虚函数,在创建对象期间需要将虚表的地址放在对象的前4个字节

default 与 delete

defalut:告诉编译器生成默认的成员函数

class Date
{
private:
	Date() = default;  // 即使用户定义了带有参数的构造函数,编译器也会生成无参的构造函数
}

delete:告诉编译器删除默认的成员函数,即不生成

class unique_ptr
{
private:
	unique_ptr(const unique_ptr& ) = delete;  // 禁止编译器生成拷贝构造函数
}    

final 与 override

1、final:修饰虚函数,表示该虚函数不能再被继承
final是C++11中定义的继承控制关键字,它可以修饰虚函数,表示该虚函数不能被继承
若直接修饰基类,无任何意义。

2、override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写,则编译报错

智能指针

C++11 之前的标准中提供了 auto_ptr 智能指针,但是,这种智能指针有着致命的缺陷,所以,C++11 标准中新增加了 unique_ptr 、shared_ptr 、weak_ptr 这几种智能指针。详情请参考:CSDN–AdamXi–C++智能指针

C++11 中引入的新容器

序列式容器

  1. forward_list:带头结点的循环单链表
  2. array:静态类型的顺序表

关联式容器

unordered_list 系列的容器,底层结构依赖哈希结构。之前C++部分文章都介绍过~~~

  1. unordered_map:键值对,key唯一
  2. unordered_set:键key,key唯一
  3. unordered_multimap:键值对,key不唯一
  4. unordered_multiset:键key,key不唯一

你可能感兴趣的:(C++)