第五章:C++新特性(1)

目录

一、新特性

        1、语法改进

        2、标准库扩充(STL中新增加一些模板类,比较好用)

二、C++中智能指针和指针的区别

        1、智能指针

        2、指针

        3、区别

三、C++中的智能指针

        1、智能指针有4种

        2、使用智能指针的原因

        3、四种指针分别解决的问题及各自的特性 


一、新特性

        C++新特性主要包括语法改进和标准库扩充两个方面,主要为以下11点:

        1、语法改进

        (1)统一的初始化方法;

        C++98/03可以使用初始化列表进行初始化:

int i_arr[3] = {1,2,3};
long l_arr[] = {1,2,3,4};

struct A
{
    int x;
    int y;
} a = {1,2};

         但是这种初始化方式的适用性狭窄,只有上面提到的这两种数据类型可以使用初始化列表。在C++11中,初始化列表的适用性被大大增加了。它现在可以用于任何类型对象的初始化,实例如下:

class Foo
{
public:
    Foo(int) {}
private:
    Foo(const Foo &);
};

int main(void)
{
    Foo a1(123);
    Foo a2 = 123; //error: 'Foo::Foo(const Foo &)' is private
    Foo a3 = { 123 };
    Foo a4 { 123 };
    int a5 = { 3 };
    int a6 { 3 };
    return 0;
}

        在上例中,a3、a4使用了新的初始化方式来初始化对象,效果如同 a1 的直接初始化。a5、a6则是基本数据类型的列表初始化方式。可以看到,它们的形式都是统一的。这里需要注意的是,a3虽然使用了等于号,但它仍然是列表初始化,因此,私有的拷贝构造并不会影响到它。a4和a6 的写法,是C++98/03 所不具备的。在 C++11 中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。

        (2)成员变量默认初始化;

        好处:构建一个类的对象不需要用构造函数初始化成员变量

//程序实例
#include <iostream>
using namespace std;

class B
{
public:
    int m = 1234;
    int n;
};

int main()
{
    B b;
    cout << b.m << endl;
    return 0;
}

        (3)auto关键字用于定于变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化);

//程序实例
#include <vector>
using namespace std;

int main(){
    vector< vector<int> > v;
    vector< vector<int> >::iterator i = v.begin();
    return 0;
}

        可以看出来,定义迭代器i的时候,类型书写比较冗长,容易出错。然而有了auto类型推导,我们大可不必这样,只写一个auto即可。

        (4)decltype 求表达式的类型;

        decltype是C++11新增的一个关键字,它和auto的功能一样,都用来在编译时期进行自动类型推导。

        ①为什么要有decltype?

        因为auto并不适用于所有的自动类型推导场景,在某些特殊情况下auto用起来非常不方便,甚至压根无法使用,所以decltype关键字也被引入到C++11中。

        auto和decltype关键字都可以自动推导出变量的类型,但它们的用法是有区别的:

auto varname = value;
decltype(exp) varname = value;

其中,varname表示变量名,value表示赋给变量的值,exp表示一个表达式。auto根据“=”右边的初始值value推导出变量的类型,而decltype根据exp表达式推导出变量的类型,与“=”右边的value没有关系。另外,auto要求变量必须初始化,而decltype不要求。这很容易理解,auto是根据变量的初始值来推导变量类型的,如果不初始化,变量的类型就无法推导了。decltype可以写成下面的形式:

decltype(exp) varname;

        ②代码示例:

//decltype用法举例
nt a = 0;
decltype(a) b = 1;  //b 被推导成int
decltype(10.8) x = 5.5;   //x 被推导成double
decltype(x+100) y;   //y 被推导成double

        (5)智能指针 shared_ptr

        和unique_ptr、weak_ptr不同之处在于:多个shared_ptr智能指针可以共同使用同一块堆内存。并且由于该类型智能指针在实现上采用的是引用计数机制,即使有一个shared_ptr指针放弃了堆内存的“使用权”(引用计数减1),也不会影响其它指向同一堆内存的shared_ptr指针(只有引用为0时,堆内存才会被自动释放)。

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    //构建 2 个智能指针
    std::shared_ptr<int> p1(new int(10));
    std::shared_ptr<int> p2(p1);
    //输出 p2 指向的数据
    cout << *p2 << endl;
    p1.reset();//引用计数减 1,p1为空指针
    if (p1) {
        cout << "p1 不为空" << endl;
    }
    else {
        cout << "p1 为空" << endl;
    }
    //以上操作,并不会影响 p2
    cout << *p2 << endl;
    //判断当前和 p2 同指向的智能指针有多少个
    cout << p2.use_count() << endl;
    return 0;
}


/* 程序运行结果:
    10
    p1 为空
    10
    1
*/

        (6)空指针 nullptr(原来是NULL);

        nullptr是nullptr_t类型的右值常量,专用于初始化空类型指针。nullptr_t是C++11新增加的数据类型,可称为“指针空值类型”。也就是说,nullptr仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要,我们完全可以定义出多个同nullptr完全一样的实例对象。nullptr可以被隐式转换成任意的指针类型。例如:

int * a1 = nullptr;
char * a2 = nullptr;
double * a3 = nullptr;

        显然,不同类型的指针变量都可以使用nullptr来初始化,编译器分别将nullptr隐式转换成int、char、double指针类型。另外,通过将指针初始化为nullptr,可以很好的解决NULL遗留问题。如:

 

#include <iostream>
using namespace std;

void isnull(void *c){
    cout << "void*c" << endl;
}
void isnull(int n){
    cout << "int n" << endl;
}

int main() {
    isnull(NULL);
    isnull(nullptr);
    return 0;
}


/* 程序运行结果:
    int n
    void*c
*/

        (7)基于范围的for循环

        如果要用for循环语句遍历一个数组或容器,只能套用如下结构:

for(表达式1;表达式2;表达式3){
    //循环体
}
//程序实例
#include <iostream>
#include <vector>
#include <string.h>
using namespace std;

int main() {
    char arc[] = "www.123.com";
    int i;
    //for循环遍历普通数组
    for (i = 0; i < strlen(arc); i++) {
        cout << arc[i];
    }
    cout << endl;
    vector<char>myvector(arc,arc+3);
    vector<char>::iterator iter;
    //for循环遍历 vector 容器
    for (iter = myvector.begin(); iter != myvector.end(); ++iter) {
        cout << *iter;
    }
    return 0;
}


/* 程序运行结果:
    www.123.com
    www
*/

         (8)右值引用和move语义 让程序员有意识减少进行深拷贝操作,节约内存空间

        ①右值引用

        C++98/03标准中就有引用,使用“&”表示。但这种引用方式有一个缺陷,即正常情况下只能操作C++中的左值,无法对右值添加引用。举例如下:

int num = 10;
int &b = num;    //正确
int &c = 10;     //错误

        如上所示,编译器允许我们为num左值建立一个引用,但不可以为10这个右值建立引用。 因此,C++98/03标准中的引用又称为左值引用

        注意虽然C++98/03标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值。例如:

int num = 10;
const int &b = num;
const int &c = 10;

        我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。为此,C++11标准新引入了另一种引用方式,称为右值引用,用“&&”表示

        需要注意的是,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化。如:

int num = 10;
//int && a = num; //右值引用不能初始化为左值
int && a = 10;

 和常量左值引用不同的是,右值引用还可以对右值进行修改,如:

int && a = 10;
a = 100;
cout << a << endl;


/* 程序运行结果:
100
*/

        另外,C++语法上是支持定义常量右值引用的,如:

const int &&a = 10; //编译器不会报错

         但这种定义出来的右值引用并无实际用处。一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。

        ②move语义

        move本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制 转化为右值。基于move()函数特殊的功能,其常用于实现移动语义。move()函数的用法也很简单,其语法格式如下:

move(arg)   //其中,arg表示指定的左值对象。该函数会返回arg对象的右值形式。
//程序实例
#include <iostream>
using namespace std;

class first{
public:
    first():num(new int(0)){
        cout << "construct!" <<endl;
    }
    //移动构造函数
    first(first &&d):num(d.num){
        d.num = NULL;
        cout << "first move construct!" <<endl;
    }
public:    //这里应该是private,使用public是为了更方便说明问题
    int *num;
}

class second{
public:
    second():fir(){}
    //用first类的移动构造函数初始化fir
    second(second &&sec):fir(move(sec.fir)){
        cout << "second move construct" <<endl;
    }
public:
    first fir;
}

int main()
{
    second oth;
    second oth2 = move(oth);
    //cout << "oth.fir.num <<endl;     //程序报运行时错误
    return 0;
}



/* 程序运行结果:
    construct!
    first move construct!
    second move construct
*/

        2、标准库扩充(STL中新增加一些模板类,比较好用)

        (9)无序容器(哈希表)用法和功能同map一模一样,区别在于哈希表的效率更高;

        用法和功能同map一模一样,区别在于哈希表的效率更高。

        ①无序容器具有两个特点:a.无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键;b.和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1))。但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。

        ②和关联式容器一样,无序容器只是一类容器的统称,其包含有4个具体容器,分别为 unordered_map、unordered_multimap、unordered_set 以及 unordered_multiset。功能如下表:

无序容器功能
unordered_map存储键值对<key, value>类型的元素,其中各个键值对键的值不允许重复,且该容器中存储的键值对是无序的
unordered_multimap和unordered_map唯一的区别是该容器允许存储多个键相同的键值对
unordered_set不再以键值对的形式存储数据,而是直接存储数据元素本身(可以理解为,该容器存储的全部都是键key和值value相等的键值对,正因为它们相等,因此只存储value即可)。另外,该容器存储的元素不能重复,且容器内部存储的元素是无序的
unordered_multiset和unordered_set唯一的区别在于该容器允许存储值相同的元素

        ③程序实例(以unordered_map容器为例)

#include <iostream>
#include <string>
#include <unordered_map>
using namespace std;

int main()
{
    //创建并初始化一个 unordered_map 容器,其存储的 <string,string> 类型的键值对
    std::unordered_map<std::string, std::string> my_uMap{
        {"教程1","www.123.com"},
        {"教程2","www.234.com"},
        {"教程3","www.345.com"} 
    };
    //查找指定键对应的值,效率比关联式容器高
    string str = my_uMap.at("C语言教程");
    cout << "str = " << str << endl;
    //使用迭代器遍历哈希容器,效率不如关联式容器
    for (auto iter = my_uMap.begin(); iter != my_uMap.end(); ++iter)
    {
        //pair 类型键值对分为 2 部分
        cout << iter->first << " " << iter->second << endl;
    }
    return 0;
}


/* 程序运行结果:
    教程1 www.123.com
    教程2 www.234.com
    教程3 www.345.com
*/

 

        (10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串,常用符号的意义如下:

符号意义
^匹配行的开头
$匹配行的结尾
.匹配任意单个字符
[…]匹配[]中的任意一个字符
(…)设定分组
\转义字符
\d匹配数组[0-9]
\D\d取反
\w匹配字母[a-z],数字,下划线
\W\w取反
\s匹配空格
\S\s取反
+前面的元素重复1次或多次
*前面的元素重复任意次
?前面的元素重复0次或1次
{n}前面的元素重复n次
{n,}前面的元素重复至少n次
{n,m}前面的元素重复至少n次,至多m次
|逻辑或

        (11)Lambda表达式。

               所谓匿名函数,简单的理解就是没有名称的函数,又常被称为lambda函数或者lambda表达式。

        ①定义

        lambda 匿名函数很简单,可以套用如下的语法格式:

        [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型

        {

        函数体;

        };

        其中各部分的含义分别为:

        a. [外部变量方位方式说明符]

        [ ] 方括号用于向编译器表明当前是一个lambda表达式,其不能被省略。在方括号内部,可以注明当前lambda函数的函数体中可以使用哪些“外部变量”。

        b.(参数)

        和普通函数的定义一样,lambda匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略。

        c.mutable

        此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在lambda表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用mutable关键字。

       注意:对于以值传递方式引入的外部变量,lambda表达式修改的是拷贝的那一份,并不会修改真正的外部变量。

        d.noexcept/throw()

        可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda函数的函数体中可以抛出任何类型的异常。而标注noexcept关键字,则表示函数体内不会抛出任何异常;使用throw()可以指定lambda函数内部可以抛出的异常类型。

        e.->返回值类型

        指明lambda匿名函数的返回值类型。值得一提的是,如果lambda函数体内只有一个 return语句,或者该函数返回void,则编译器可以自行推断出返回值类型,此情况下可以直接省略"-> 返回值类型"。

         f.函数体

        和普通函数一样,lambda匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。

        ②程序实例 

#include <iostream>
#include <algorithm>
using namespace std;

int main()
{
    int num[4] = {4, 2, 3, 1};
    //对 a 数组中的元素进行排序
    sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
    for(int n : num){
        cout << n << " ";
    }
    return 0;
}


/* 程序运行结果:
    1 2 3 4
*/

二、C++中智能指针和指针的区别

        1、智能指针

        如果在程序中使用new从堆(自由存储区)分配内存,等到不需要时,应该使用delete将其释放。C++引用了智能指针auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL)表明,需要有更精致的机制。基于程序员的编程体验和BOOST库提供的解决方案,C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr和wak_ptr。所有新增的智能指针都能与STL容器和移动语义协同工作。

        2、指针

        C语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存单元。指针变量不同于整型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为“指针类型”。

        3、区别

        智能指针和普通指针的区别在于智能指针实际上是对普通指针增加了一层封装机制,区别是它负责自动释放指针所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命周期

三、C++中的智能指针

        1、智能指针有4种

         shared_ptrunique_ptrweak_ptrauto_ptr。(其中auto_ptr被C++11弃用)

        2、使用智能指针的原因

        申请的空间(即new出来的空间),在使用结束时需要delete掉,否则会形成内存碎片。在程序运行期间,new出来的对象,在析构函数中delete掉,但是这种方法不能解决所有问题,因为有时候new发生在某个全局函数里面,该方法会给程序员造成精神负担。此时,智能指针就派生了用场。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以,智能指针的作用原理是在函数结束时自动释放内存空间,避免了手动释放内存空间。

        3、四种指针分别解决的问题及各自的特性 

        ①auto_ptr(C++98的方案,C++11已经弃用)

        采用所有权模式。

auto_ptr<string> p1(new string("I reigned loney as a cloud."));
auto_ptr<string> p2;
p2=p1; //auto_ptr不会报错

        此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1会报错。所以auto_ptr的缺点是存在潜在的内存崩溃问题

        ②unique_ptr(替换auto_ptr)

        unique_ptr实现独占式拥有或严格拥有概念,保证同一时间只有一个智能指针可以指向该对象。它对于避免资源泄露,例如,以new创建对象后因为发生异常而忘记调用delete时的情形特别有用。

        采用所有权模式,和上面例子一样。

auto_ptr<string> p3(new string("I reigned loney as a cloud."));
auto_ptr<string> p4;
p4=p3; //此时不会报错

        编译器任务p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。另外unique_ptr还有更聪明的地方:当程序试图将一个unique_ptr赋值给另一个时,如果源 unique_ptr是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁 止这么做,比如:

unique_ptr<string> pu1(new string ("hello world"));
unique_ptr<string> pu2;
pu2 = pu1; // #1 not allowed
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string ("You")); // #2 allowed

        其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用unique_ptr的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr优于允许两种赋值的auto_ptr 。

注意:如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准 库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;

        ③shared_ptr

        shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release() 时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

        shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

        成员函数

        use_count 返回引用计数的个数

        unique 返回是否是独占所有权( use_count 为 1)

        swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

        reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

      get 返回内部对象(指针),由于已经重载了()方法,因此和直接使用对象是一样的。如 shared_ptr sp(new int(1));sp 与 sp.get()是等价的。

        ④weak_ptr

         weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少weak_ptr是用来解决shared_ptr相互引用时的死锁问题如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引 用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它, 它可以通过调用lock函数来获得shared_ptr。

class B;
class A
{
public:
    shared_ptr<B> pb_;
    ~A()
{
    cout<<"A delete\n";
}
};

class B
{
public:
    shared_ptr<A> pa_;
    ~B()
{
    cout<<"B delete\n";
}
};

void fun()
{
    shared_ptr<B> pb(new B());
    shared_ptr<A> pa(new A());
    pb->pa_ = pa;
    pa->pb_ = pb;
    cout<<pb.use_count()<<endl;
    cout<<pa.use_count()<<endl;
}

int main()
{
    fun();
    return 0;
}

        可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的 shared_ptr pb_;改为weak_ptr pb;运行结果如下,这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减 一,那么A的计数为0,A得到释放。

注意:我们不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),我们不能这样访问,pa->pb->print(),英文pb是一个weak_ptr,应该先把它转化为shared_ptr,如:shared_ptr p = pa->pb_.lock();p->print();

  • 25
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿何试Bug个踌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值