【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]

简介: 【C++入门到精通】新的类功能 | 可变参数模板 C++11 [ C++入门 ]

引言

随着C++11标准的发布,引入了许多令人振奋的新特性,其中包括强大的类功能和可变参数模板。这些新增的功能为C++编程带来了更加灵活和高效的可能性,极大地丰富了语言的表达能力和应用范围。本文将重点探讨C++11中这些新特性的优势和用法,帮助读者更好地理解和运用现代C++编程的最新技术。😍

一、新的类功能

1. 默认成员函数

在C++11标准中,引入了两个重要的默认成员函数:移动构造函数和移动赋值运算符重载。这两个功能的引入极大地提升了C++语言的性能和效率。

⭕移动构造函数

移动构造函数允许对象通过移动资源而不是复制资源来进行构造。在传统的复制构造函数中,对象的构造是通过逐个复制成员变量来完成的,这可能导致资源的不必要拷贝和分配,从而降低程序的性能。而移动构造函数则允许对象直接获取资源的所有权,而无需进行复制,从而提高了程序的效率。移动构造函数通过使用右值引用来实现,可以显著减少资源的拷贝和内存分配,特别适用于管理大量数据的类对象。

⭕移动赋值运算符重载

移动赋值运算符重载与移动构造函数类似,它也通过移动资源而不是复制资源来实现对象之间的赋值操作。传统的赋值运算符重载会对已有的资源进行释放和重新分配,这样的操作可能会消耗大量的时间和系统资源。而移动赋值运算符重载则通过将资源的所有权转移给目标对象,避免了资源的拷贝和分配,提高了程序的性能和效率。

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

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

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

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

以下代码在vs2013中不能体现,在vs2019下才能演示体现上面的特性。

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& operator=(const Person& p)
  {
    if(this != &p)
    {
      _name = p._name;
      _age = p._age;
    }
    return *this;
  }*/
  /*~Person()
  {}*/
private:
  std::string _name;
  int _age;
};

int main()
{
  Person s1;
  Person s2 = s1;
  Person s3 = std::move(s1);
  Person s4;
  s4 = std::move(s2);
  
  return 0;
}

2. 类成员变量初始化

在C++11标准中,新增了一种方便的类成员变量初始化方式,即在类定义中直接对成员变量进行初始化。这种初始化方式使得在定义类的同时就可以为成员变量赋予初始值,而不需要依赖于构造函数来完成初始化操作

具体来说,我们可以在类定义的同时为成员变量提供默认值,例如:

class MyClass {
public:
    int x = 0;  // 直接对成员变量进行初始化
    double y = 3.14;
    std::string name = "C++";
};

在上面的示例中,成员变量x、y和name都在类定义中直接进行了初始化赋值,这样在创建对象时,如果没有显式地指定初始值,那么这些成员变量将会自动以指定的默认值进行初始化。


这种类成员变量的直接初始化方式简化了代码,使得类的定义更加清晰和简洁。同时,它也提供了对类成员变量进行默认值设置的便利途径,使得开发者可以更加方便地管理和维护类的成员变量初始化状态。

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

在C++11标准中,引入了关键字"default",它可以用来显式地指示编译器生成默认的特殊成员函数,例如默认构造函数、析构函数、拷贝构造函数、移动构造函数和赋值操作符等。使用"default"关键字可以方便地告诉编译器去生成这些函数,而不需要手动编写它们的定义。

例如,假设我们有一个类需要生成默认的构造函数和析构函数,我们可以这样使用"default"关键字:

class MyDefaultClass {
public:
    // 显式指示编译器生成默认构造函数和析构函数
    MyDefaultClass() = default;
    ~MyDefaultClass() = default;
};

在上面的示例中,我们使用"default"关键字来告诉编译器生成默认的构造函数和析构函数。这样做的好处在于,我们无需手动编写这些默认函数的定义,而是交由编译器自动生成,从而简化了代码并提高了代码的可读性。


"default"关键字的另一个重要应用是在移动构造函数和移动赋值操作符中。我们可以使用"default"关键字来告诉编译器生成默认的移动构造函数和移动赋值操作符,例如下面的代码使用"default"关键字来告诉编译器生成默认的移动构造函数

#include <iostream>
#include <utility>

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:
    std::string _name;
    int _age;
};

int main() {
    Person s1; // 调用默认构造函数
    Person s2 = s1; // 调用拷贝构造函数
    Person s3 = std::move(s1); // 调用移动构造函数

    return 0;
}

在这段代码中,我们定义了一个名为Person的类,包括默认构造函数、拷贝构造函数和移动构造函数。在main函数中,我们创建了三个Person对象s1、s2和s3,并展示了它们在不同情况下调用构造函数的过程。

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

在C++11标准中,可以使用关键字"delete"来显式地删除默认生成的特殊成员函数,例如默认构造函数、拷贝构造函数、移动构造函数和赋值操作符等。通过使用"delete"关键字,可以阻止特定的函数被默认生成或者调用。

下面是一个简单的示例,展示了如何使用"delete"关键字阻止默认构造函数的生成:

class NoDefault {
public:
    // 删除默认构造函数
    NoDefault() = delete;
};

int main() {
    NoDefault nd; // 这里会导致编译错误,因为默认构造函数已被删除
    return 0;
}

在上述示例中,类"NoDefault"的默认构造函数被使用"= delete"语法删除了,因此在main函数中尝试创建"NoDefault"类的实例将会导致编译错误。

类似地,你也可以使用"delete"关键字来删除其他默认生成的特殊成员函数,以满足特定的设计需求。这种方式通常用于禁用某些不希望被调用的函数,或者确保特定的行为不发生。

5. override 和 final

overridefinal 都是 C++11 中引入的关键字,用于标识和修饰虚函数的行为。

(1)override

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

示例:

class Base {
public:
    virtual void myFunction() {
        // 基类虚函数的默认实现
    }
};

class Derived : public Base {
public:
    void myFunction() override {
        // 派生类重写基类虚函数的实现
    }
};

在上述示例中,派生类 Derived 使用 override 关键字表明它重写了基类 Base 的虚函数 myFunction()。如果在派生类中意外地使用了错误的函数签名(参数列表或返回类型不匹配),编译器会发出错误提示。

(2)final

final 用于标识类、成员函数或虚函数,表示它们被声明为最终版本,禁止在派生类中进一步继承或重写。

示例:

class Base final {
public:
    virtual void myFunction() {
        // 基类虚函数的默认实现
    }
};

class Derived : public Base {
public:
    void myFunction() /* override 不可使用 */ {
        // 派生类重写基类虚函数的实现
    }
};

在上述示例中,基类 Base 使用 final 关键字标识它是最终类,不允许被继续派生。同时,派生类 Derived 中的 myFunction() 不能使用 override 关键字进行标识,因为基类已经被声明为最终类

二、可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大家如果有需要,再可以深入学习。

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

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

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

递归函数方式展开参数包

// 递归终止函数
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;
}

逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)…}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc… ),最终会创建一个元素值都为0的数组int arr[sizeof…(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包。

template <class T>
void PrintArg(T t)
{
  cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
  int arr[] = { (PrintArg(args), 0)... };
  cout << endl;
}
int main()
{
  ShowList(1);
  ShowList(1, 'A');
  ShowList(1, 'A', std::string("sort"));
  
  return 0;
}

温馨提示

感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!


目录
相关文章
|
2月前
|
存储 C++ UED
【实战指南】4步实现C++插件化编程,轻松实现功能定制与扩展
本文介绍了如何通过四步实现C++插件化编程,实现功能定制与扩展。主要内容包括引言、概述、需求分析、设计方案、详细设计、验证和总结。通过动态加载功能模块,实现软件的高度灵活性和可扩展性,支持快速定制和市场变化响应。具体步骤涉及配置文件构建、模块编译、动态库入口实现和主程序加载。验证部分展示了模块加载成功的日志和配置信息。总结中强调了插件化编程的优势及其在多个方面的应用。
374 67
|
16天前
|
算法 网络协议 数据挖掘
C++是一种功能强大的编程语言,
C++是一种功能强大的编程语言,
46 14
|
27天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
49 2
|
29天前
|
安全 编译器 C++
【C++11】可变模板参数详解
本文详细介绍了C++11引入的可变模板参数,这是一种允许模板接受任意数量和类型参数的强大工具。文章从基本概念入手,讲解了可变模板参数的语法、参数包的展开方法,以及如何结合递归调用、折叠表达式等技术实现高效编程。通过具体示例,如打印任意数量参数、类型安全的`printf`替代方案等,展示了其在实际开发中的应用。最后,文章讨论了性能优化策略和常见问题,帮助读者更好地理解和使用这一高级C++特性。
46 4
|
29天前
|
算法 编译器 C++
【C++】模板详细讲解(含反向迭代器)
C++模板是泛型编程的核心,允许编写与类型无关的代码,提高代码复用性和灵活性。模板分为函数模板和类模板,支持隐式和显式实例化,以及特化(全特化和偏特化)。C++标准库广泛使用模板,如容器、迭代器、算法和函数对象等,以支持高效、灵活的编程。反向迭代器通过对正向迭代器的封装,实现了逆序遍历的功能。
35 3
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
94 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
83 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
99 4
|
1月前
|
编译器 C++
【c++】模板详解(1)
本文介绍了C++中的模板概念,包括函数模板和类模板,强调了模板作为泛型编程基础的重要性。函数模板允许创建类型无关的函数,类模板则能根据不同的类型生成不同的类。文章通过具体示例详细解释了模板的定义、实例化及匹配原则,帮助读者理解模板机制,为学习STL打下基础。
32 0
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4