【C++11】C++11类与模板语法的完善

目录

一,新的类功能

1-1,默认成员函数

1-2,强制生成关键字

二,可变参数模板

2-1,模板参数包

2-2,STL容器empalce的相关接口


一,新的类功能

1-1,默认成员函数

        C++11之前的类中有6个默认成员函数(默认成员函数就是我们不写编译器会生成一个默认的):1. 构造函数。2. 析构函数。3. 拷贝构造函数。4. 拷贝赋值重载。5. 取地址重载。6. const 取地址重载。这六个之中重要的是前4个,后两个取地址的用处不大。

        C++11新增了两个:移动构造函数移动赋值运算符重载。此两种默认成员函数与其它的默认成员函数有所不同,需说明以下三点。

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

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

        3,如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值,这时需自己实现拷贝构造和拷贝赋值。因为当你自己编写移动构造或移动赋值后,说明你可能想要完全控制对象的拷贝行为,且有了移动语义通常也需要深拷贝,而默认的拷贝操作都是浅拷贝,所以为了确保对象的拷贝行为符合预期,有了移动语义后编译器不会默认生成拷贝构造和拷贝赋值。

        这里的代码演示需借助类结构演示,我们先简单设计String类,如下:

#include <cstring>
#include <iostream>
using namespace std;
namespace bit
{
    class string
    {
    public:
        //构造函数
        string(const char* str = "")
            :_size(strlen(str))
            , _capacity(_size)
        {
            cout << "string(char* str)" << endl;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        //拷贝构造
        string(const string& s)
            :_str(nullptr)
        {
            cout << "string(const string& s) -- 深拷贝" << endl;
            string tmp(s._str);
            swap(tmp);
        }
        //赋值重载
        string& operator=(const string& s)
        {
            cout << "string& operator=(string s) -- 深拷贝" << 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;
        }
    private:
        void swap(string& s)
        {
            std::swap(_str, s._str);
            std::swap(_size, s._size);
            std::swap(_capacity, s._capacity);
        }
        char* _str;
        size_t _size;
        size_t _capacity; 
    };
}

        上面string类中自己实现了移动构造和移动赋值,编译器不会默认生成拷贝构造和拷贝赋值,若这里不自己实现拷贝构造和拷贝赋值,将不能实现有关拷贝构造和拷贝赋值的语句。下面来用以上类观察默认移动构造和默认移动赋值。

#include "String.h" //设计string的头文件
class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name) , _age(age)
    {    }
private:
    bit::string _name; 
    int _age;
};

/*自定义string类中实现了移动语义,不会生成默认拷贝构造和默认拷贝赋值,若自己不实现的话这里将会出错*/
int main()
{
    Person s1;
    cout << endl;
    //拷贝构造
    Person s2 = s1;
    cout << endl;
    
    //移动构造

    /*Preson满足默认移动构造的所有条件,它的自定义成员_name将会调用_name自己的移动构造,若没有移动构造将调用拷贝构造*/
    Person s3 = move(s1);  
    cout << endl;
    
    Person s4;
    //移动赋值

    /*与移动构造同理,若定义了_name的移动赋值将调用它移动赋值,若没有定义将调用它的拷贝赋值*/
    s4 = move(s2); 
    cout << endl;
    return 0;
}

        若在以上Person类中实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,将会输出不一样的结果。

class Person
{

    //.......
    ~Person() {    }  //在以上代码上加上了析构函数

    //.......
};

//main函数都一样

        这里可观察到默认移动语义比其它默认函数的条件更为苛刻,需满足两个条件:1,没有自己实现移动语义。2,没有自己实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。

        第一个条件好理解,第二个条件就不知所措了。其实这里之所以要这样设计还是跟析构函数 、拷贝构造、拷贝赋值重载三者的作用有关。系统默认生成的它们三个函数都是浅拷贝,当我们自己实现时说明需要深拷贝,这时说明我们需要控制对象的拷贝复制和移动行为,因此这里就不再默认生成,若有需要可自己实现。

1-2,强制生成关键字

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

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

#include "String.h" //头文件
class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name) , _age(age)
    {    }

    ~Person() {    }

    //上面自己编写了析构函数将不会有默认移动语义,这里强制生成默认移动语义
    Person(Person&& p) = default;
    Person& operator=(Person && p) = default;

    //上面强制生成了移动语义将不会有默认拷贝和默认赋值,这里强制生成默认拷贝和默认赋值
    Person(const Person& p) = default; 
    Person& operator=(const Person& p) = default;
private:
    bit::string _name; 
    int _age;
};
int main()
{
    //默认构造
    Person s1;
    cout << endl;

    //拷贝构造
    Person s2 = s1;
    cout << endl;

    //拷贝赋值
    s1 = s2;
    cout << endl;
    
    //移动构造
    Person s3 = move(s1); 
    cout << endl;
    
    //移动赋值
    Person s4;
    s4 = move(s1);
    cout << endl;

    return 0;
}

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

        有些类有时我们可能不希望有个别的默认函数,如拷贝构造、拷贝赋值等。如果想要限制这些默认函数的生成,C++98的做法是自己只声明该函数,且将该函数设置成private(防止别人在类外实现),这样就可做到万无一失,只要调用就会报错。C++11新增关键字delete对此做出处理,只需在该函数声明加上 “=delete” 即可,该语法指示编译器不生成对应函数的默认版本。“=delete”修饰的函数为删除函数。

        下面以禁止Person类进行拷贝和赋值为例。

class Person
{
public:
    Person(const char* name = "", int age = 0)
        :_name(name), _age(age)
    {    }

    ~Person() {    };

    //C++11的做法,delete关键字直接解决
    Person(Person&& p) = delete;
    Person& operator=(Person && p) = delete;
private:
    //以下是C++98的做法,只声明不实现,且放到私有
    //Person(const Person& p);
    //Person& operator=(const Person & p);

    bit::string _name; 
    int _age;
};


二,可变参数模板

2-1,模板参数包

        C++11的新特性可变参数模板能够让我们创建可以接受任意数量和类型的模板包。模板参数包是将传入任意数量和类型的模板参数打包形成的一个整体。相比 C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变参数模版无疑是一个巨大的改进。可变参数模板的知识运用比较广泛,这里不纠结过于复杂的结构,掌握一些基础的可变参数模板特性就足够使用了。

        下面就是一个基本可变参数的函数模板,并尝试使用sizeof来输出参数个数。具体细节和说明如下:

/*Args是一个模板参数包,args是一个函数形参参数包。这里可对比template<class Args> void func(Args args);运用时将传入的模板参数打包,形成Args,然后通过Args参数包中的类型定义具有Args包中模板参数类型的函数形参,将其打包形成args函数参数包*/
template <class... Args> //模板参数包Args,具有0到N个类型的模板参数
void func(Args... args)    //函数参数包args,具有0到N个类型的函数参数
{
    cout << sizeof...(Args) << " ";   //输出模板参数包所包含的参数数量  
    cout << sizeof...(args) << endl; //输出函数参数包所包含的参数数量(它们实际上大小是一样的)
    /*注意: 无法使用sizeof(Args或args),因为都是参数包。参数包的整体使用前面要加上“...”说明*/
}

int main()
{
    func();  //无参形式
    func(1, 1, 2); //同类型参数形式
    func(1, 1.2, "bit", 'a');  //不同类型参数形式
    return 0;
}

        上面的参数Args前面有省略号,表示它是一个参数表,函数形参args同理。它们里面包含了0到N(N>=0)个模版参数,当函数实例化后,以上面func(1, 1.2, "bit", 'a');为例,编译器将会在编译时生成void func<int,double,const char*,char>(其它形式同理)。

        由于参数个数和类型不定,所以使用sizeof输出的是参数的个数。下面的问题是又该如何使用包里面的参数?我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。参数包中的个数可以使用sizeof获取,有些人可能会使用数组的形式使用,但C++11语法并不支持使用args[i]这样的方式获取可变参数。

        C++11使用递归函数方式展开参数包,具体形式类似于以下代码:

#include <iostream>
using namespace std;
//编译时递归返回条件,即递归终止函数
void _func() { cout << endl; }
//注意: 递归返回条件不能使用下面的,因为出现无参函数时将会出错,即出现func()的情况
//template <class T>
//void _func(const T& data) { cout << data << endl; }

/*编译时递归解析,依次拿到参数包args里面的参数传递给T类型的data,直到参数包args为空时调用递归终止函数_func()*/
template <class T, class... Args>
void _func(const T& data, Args ...args)
{
    cout << data << " ";
    _func(args...);  //递归遍历
}

//参数包args中有0-N个参数
template <class ...Args>
void func(Args ...args)
{
    _func(args...); //传入参数包
}


int main()
{
    func(); 
    func(1);
    func(1, 1, 1);
    func(1, 1.2, "bit", 'a'); 
    return 0;
}

        模板在编译阶段实例化,编译器实例化后将会把参数包展开,这里的递归过程可理解为参数的诼渐展开过程,以func(1, 1.2, 'a');为例,展开过程如下:

//编译阶段中,func(1, 1.2, 'a')在主函数传入模板被实例化后,将会推演生成以下类似的代码
void func(int val1, double ch, char s)
{
    _func(val1, ch, s);
}

//诼渐往下抽象递归解包参数,直到遍历到递归终止函数
void _func(const int& data, double ch, char s)
{
    cout << data << " ";
    _func(ch, s);  //往下调用void _func(const double& data, char s)
}

void _func(const double& data, char s)
{
    cout << data << " ";
    _func(s);  //往下调用void _func(const char& data)
}

void _func(const char& data)
{
    cout << data << " ";
    _func(); //递归终止函数
}

        明白了这一原理后这里会发现一个潜在的问题,当传入的参数数量过多,即参数包过大,递归层数过多,系统会支撑不住这么大的开销,发生错误,实际应用中应注意。类似于以下代码:

template<size_t N>
void fun()
{
    cout << N << " ";
    fun<N - 1>();
}

template<>
void fun<1>()
{
    cout << 1 << endl;
}

int main()
{
    fun<15000>();  //传入参数数量过大,递归层数过多,系统支撑不下
    return 0;
}

2-2,STL容器empalce的相关接口

        C++中的emplace是一个常用的技术,主要用于在容器中就地构造元素,而不是先创建一个元素,然后将其复制到容器中。这种方法通常比先创建再复制(或移动)更高效,因为它减少了不必要的临时对象的创建和销毁。

   emplace通常在标准模板库(STL)的容器中看到,如vectorlistmapset等。每个这些容器都可能有一个或多个名为emplace_backemplace_frontemplace等的成员函数。

示例:

        假设我们有一个vector<string>,并且我们想要添加一个新的字符串。使用传统的push_back插入方法类似于以下:

vector<string> v;

//使用v.push_back("bit");内部形式如同以下

string str = "bit"; //先构造出string的临时对象

v.push_back(str); //然后将此对象移动拷贝到v容器中

        若使用emplace我们可以直接在v中构造字符串,从而避免了不必要的复制(或移动)。

vector<string> vec;
vec.emplace_back("bit");  //直接在容器中构造新的string对象,没有创建临时的string对象

        这里不难发现,若是直接传递对象,emplace将不会做出任何改变。emplace的主要作用是为了减少不必要的拷贝,如同string str("bit")的优化,将拷贝+拷贝构造直接优化成拷贝构造,若是没有不必要的构造,emplace与其它功能相同的函数的使用没有任何区别。还有就是emplace不能使用"{}"(初始化列表)。主要是因为emplace函数不是通过列表初始化来构造对象的,而是使用直接初始化或拷贝初始化,因为多参数要进行隐式类型转换,若进行隐式类型转换就必须要构造出临时对象,与底层底层结构不匹配。当出现多参数时这里可直接使用“()”。

        下面我们使用上面模拟实现的string类来观察这一现象。

#include <list>
#include "String.h"
int main()
{
    list<bit::string> lt1;
    bit::string s1("xxxx");
    lt1.push_back(s1); //调用拷贝构造bit::string在容器中创建string对象
    cout << endl;
    lt1.push_back(move(s1)); //调用移动拷贝bit::string将资源转移到容器string对象中
    cout << "=============================================" << endl;
    //对象传递,没有临时对象的不必要拷贝,emplace_back的调用输出跟push_back一样
    bit::string s2("xxxx");
    lt1.emplace_back(s2);
    cout << endl;
    lt1.emplace_back(move(s2));
    cout << "=============================================" << endl;
    //先将"xxxx"构造成bit::string对象,然后拷贝到lt1容器中
    lt1.push_back("xxxx");
    cout << endl;
    //直接在lt1容器中构造数据是"xxxx"的bit::string对象
    lt1.emplace_back("xxxx");
    cout << "=============================================" << endl;
    list<pair<bit::string, bit::string>> lt2;
    //下面输出两个重复的信息是因为pair是两个指令
    pair<bit::string, bit::string> kv1("xxxx", "yyyy");
    lt2.push_back(kv1);
    lt2.push_back(move(kv1));
    cout << "=============================================" << endl;

    pair<bit::string, bit::string> kv2("xxxx", "yyyy");
    //对象的传递,即便存储类型是pair键值对也是同理,与push_back输出一样
    lt2.emplace_back(kv2);
    lt2.emplace_back(move(kv2));
    cout << "=============================================" << endl;
    lt2.emplace_back("xxxx", "yyyy");
    cout << endl;
    lt2.push_back({ "xxxx", "yyyy" });
    cout << "=============================================" << endl;
    return 0;
}

        通过以上可发现,由于emplace只接受非对象时的改进,而非对象在这方面的运用是构造+移动语义,emplace的改进只是省去了移动语义。移动语义只是移动资源,内部消耗很小,也就是说emplace的改善在实现有移动语义类的情况下效率没有多大提高,但是在没有实现有移动语义类的情况下效率将大大提升,如push_back内部是构造+拷贝构造,emplace_back内部是构造。

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;
    }
    Date(const Date& d) //拷贝构造,没有移动语义
        :_year(d._year), _month(d._month), _day(d._day)
    {
        cout << "Date(const Date& d)" << endl;
    }
private:
    int _year = 1;
    int _month = 1;
    int _day = 1;
};

int main()
{
    list<Date> lt1;
    lt1.push_back({ 2024,3,30 });
    cout << endl;
    lt1.emplace_back(2024, 3, 30);
    cout << endl << endl;

    //匿名对象。匿名对象也是对象,emplace与push的实现相同
    lt1.push_back(Date(2023, 1, 1));
    cout << endl;
    lt1.emplace_back(Date(2023, 1, 1));
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/681709.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SparkSQL概述

1.1. SparkSQL介绍 SparkSQL&#xff0c;就是Spark生态体系中的构建在SparkCore基础之上的一个基于SQL的计算模块。SparkSQL的前身不叫SparkSQL&#xff0c;而是叫做Shark。最开始的时候底层代码优化、SQL的解析、执行引擎等等完全基于Hive&#xff0c;总是Shark的执行速度要比…

深入理解Django:中间件与信号处理的艺术

title: 深入理解Django&#xff1a;中间件与信号处理的艺术 date: 2024/5/9 18:41:21 updated: 2024/5/9 18:41:21 categories: 后端开发 tags: Django中间件信号异步性能缓存多语言 引言 在当今的Web开发领域&#xff0c;Django以其强大的功能、简洁的代码结构和高度的可扩…

设计必备!六款免费平面图设计软件大盘点

平面设计是一种迷人而多样化的艺术形式&#xff0c;它结合了颜色、形状、排版和创造力&#xff0c;通过图像和文本传达信息。市场上有各种各样的平面设计软件&#xff0c;选择合适的设计软件是成为优秀设计师的重要一步。为了降低软件成本&#xff0c;大多数设计师会优先使用免…

一套C语言开发的 PACS影像系统源码 PACS系统的基本概念、系统业务流程

PACS系统基本概念 PACS&#xff0c;全称 Picture Archiving and Communication Systems&#xff0c;中文意为影像归档和通信系统。它是应用于医院影像科室的一种系统&#xff0c;主要任务是把日常产生的各种医学影像&#xff08;包括核磁&#xff0c;CT&#xff0c;超声&#…

nginx的应用部署nginx

这里写目录标题 nginxnginx的优点什么是集群常见的集群什么是正向代理、反向代理、透明代理常见的代理技术正向代理反向代理透明代理 nginx部署 nginx nginx&#xff08;发音同enginex&#xff09;是一款轻量级的Web服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&…

TMS320F280049 CLB模块--总览(0)

CLB模块是可配置的逻辑块&#xff0c;和FPGA的CLB有些不同。 下图是CLB模块在系统中的交互&#xff0c;图中CLB XBAR和TILE是CLB。从049中有4个CLB&#xff0c;也就是TILE1-4。 下图是CPU和CLB交互的示意图。 下图是CLB的时钟。 参考文档&#xff1a; TMS320F28004x Real-Tim…

【C++】零钱兑换的始端---柠檬水找零

欢迎来CILMY23的博客 本篇主题为 零钱兑换的始端---柠檬水找零 个人主页&#xff1a;CILMY23-CSDN博客 个人专栏系列&#xff1a; Python | C | C语言 | 数据结构与算法 感谢观看&#xff0c;支持的可以给个一键三连&#xff0c;点赞关注收藏。 前言&#xff1a; 柠檬水找…

万兆以太网MAC设计(13)主机与FPGA之间进行PING

文章目录 前言&#xff1a;一、ICMP校验和计算二、上板效果1、终端命令行1、wireshark捕捉 前言&#xff1a; 在上板尝试进行PING操作的时候&#xff0c;发现一直是请求超时的情况&#xff0c;结果排查发现是首部校验和没有计算的问题。在UDP层&#xff0c;我们不进行校验和是…

ROS 2边学边练(45)-- 构建一个能动的机器人模型

前言 在上篇中我们搭建了一个机器人模型(其由各个关节&#xff08;joint&#xff09;和连杆&#xff08;link&#xff09;组成)&#xff0c;此篇我们会通过设置关节类型来实现机器人的活动。 在ROS中&#xff0c;关节一般有无限旋转&#xff08;continuous&#xff09;,有限旋转…

【每日力扣】543. 二叉树的直径与101. 对称二叉树

&#x1f525; 个人主页: 黑洞晓威 &#x1f600;你不必等到非常厉害&#xff0c;才敢开始&#xff0c;你需要开始&#xff0c;才会变的非常厉害 543. 二叉树的直径 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的…

芋道源码的Springboot 项目打包,配置和依赖包分开

Springboot 项目&#xff0c;把依赖包和开发的应用都打在一个jar 里很简单&#xff0c;但有个问题是&#xff0c;修改点东西就要再次全量更新。 这里介绍如何用assembly 来实现不打依赖包。 1、 在主模块中&#xff0c;需要引入 assembly.xml配置&#xff1a; src/main/asse…

DUX 主题 版本:8.2 WordPress主题优化版

主题下载地址&#xff1a;DUX 主题优化版.zip 支持夜间模式、快讯、专题、百度收录、人机验证、多级分类筛选&#xff0c;适用于垂直站点、科技博客、个人站&#xff0c;扁平化设计、简洁白色、超多功能配置、会员中心、直达链接、自动缩略图