C++快餐——C++11(2)

如期待奇迹发生,那唯有不停伸手去抓紧,去把握才行。

文章目录


类成员变量缺省值

C++11引入了成员变量的初始缺省值的特性。在类的定义中,可以为成员变量提供初始值。当对象被创建时,如果没有显式提供初始值,编译器会使用这些初始缺省值进行初始化。如下示例:

cpp 复制代码
class MyClass 
{
public:
    int myInt = 0; // 成员变量的初始缺省值为0
    float myFloat = 3.14f; // 成员变量的初始缺省值为3.14

    // 构造函数
    MyClass() 
    {
        // 可以在构造函数中访问初始缺省值
        cout << "myInt: " << myInt << endl;
        cout << "myFloat: " << myFloat << endl;
    }
};

int main()
{
    MyClass obj;
	return 0;
}

上述示例中,myInt和myFloat是具有初始缺省值的成员变量。如果没有提供显式初始值,它们将被分别初始化为0和3.14。构造函数中的输出语句将显示这些初始缺省值。

default关键字

在C++11及以后的版本中,可以使用关键字default来显式强制生成默认函数。默认函数包括默认构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。使用default关键字可以告诉编译器使用默认实现生成这些函数,而不需要显式定义函数体。这在某些情况下很有用,特别是当需要手动定义某些函数后,仍希望保留其他默认函数的行为。例如:

cpp 复制代码
class MyClass 
{
public:
    // 默认构造函数
    MyClass() {}
    // 拷贝构造函数
    MyClass(const MyClass& other) = default;
    // 拷贝赋值运算符
    MyClass& operator=(const MyClass& other) = default;
    // 析构函数
    ~MyClass() = default;
};

示例中,通过将函数定义为default,编译器将自动生成默认函数的实现。但是需要注意的是,使用default关键字生成默认函数的前提是该函数在编译器默认情况下是可生成的。如果类中存在不可复制的成员或基类,或者有其他原因导致默认函数无法自动生成,那么不能使用default关键字。

delete关键字

与default相对应的有delete关键字,C++11引入了关键字delete,可以用于显式删除函数,从而禁止生成默认函数。通过将函数声明为delete,编译器将不再生成该函数的默认实现。例如:

cpp 复制代码
class NonCopyable 
{
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete; // 删除拷贝构造函数
    NonCopyable& operator=(const NonCopyable&) = delete; // 删除拷贝赋值运算符
};

示例中,通过将拷贝构造函数和拷贝赋值运算符声明为delete,编译器将禁止生成默认的拷贝构造函数和拷贝赋值运算符。禁止生成默认函数可以用于防止类的对象进行拷贝操作,从而实现不可复制的类。这对于某些情况下的资源管理或设计意图是有用的。但是需要注意的是,当使用delete关键字删除某个函数时,需要确保在使用该函数时会导致编译错误,以避免意外调用被删除的函数。

final关键字

在C++中,final关键字用于继承和多态的语境中,用于限制派生类的进一步继承或虚函数的重写。例如:

cpp 复制代码
class Base final 
{
  	// 类定义
};

class Derived : public Base 
{  
	// 错误!无法继承final类
  	// 类定义
};

在上述示例中,Base类被标记为final,这意味着它不能被其他类继承。因此,尝试从Base派生一个名为Derived的类将导致编译错误。

cpp 复制代码
class Base 
{
public:
  virtual void foo() const final 
  {
    // 函数定义
  }
};

class Derived : public Base 
{
public:
  void foo() const override 
  {  // 错误!无法重写final虚函数
    // 函数定义
  }
};

在上述示例中,Base类中的虚函数foo()被标记为final,这意味着它不能在派生类中被重写。因此,尝试在Derived类中重写foo()函数将导致编译错误。

使用final关键字可以提供编译时的限制性,以确保某些类或虚函数不会被继承或重写。这对于需要确保特定类或函数的行为不被改变的情况下很有用。

可变参数模板

可变参数模板是C++11引入的一种特性,它允许函数或类模板接受可变数量的参数。使用可变参数模板可以编写接受任意数量参数的函数或类模板,并对每个参数进行处理,这适合于需要处理未知数量参数的情况。由于无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数。展开参数包通常使用以下两种方式:

  1. 递归函数方式展开参数包
cpp 复制代码
// 可变参数模板的递归终止条件
void print()
{
	cout << "end" << endl;
}
// 可变参数模板的递归调用
template <class T, class... Args>
void print(const T& x, Args... args)
{
	cout << "size : " << sizeof...(args) << " x = " << x << endl;
	print(args...);
}

int main()
{
	print(1, 'y', 5.5, "abcd");
}

示例中,print()函数是一个可变参数模板函数。它的终止条件是当参数列表为空时,在这种情况下它只打印换行符。递归调用中,它首先打印第一个参数,然后通过递归调用print(args...)打印剩余的参数。在main()函数中,我们调用print()函数传递了不同类型和数量的参数。编译器会根据传递的参数自动生成对应的函数实例,并输出这些参数。

  1. 逗号表达式展开参数包
cpp 复制代码
template <class T>
void print(const T& t)
{
	cout << t << endl;
}

template <class... Args>
void print(Args... args)
{
	int arr[] = { (print(args), 0)... };
}

int main()
{
	print(1, 'y', 5.5, "abcd");
}

在这个示例中,我们定义了两个重载的print()函数。第一个函数用于打印单个参数,只接受一个参数并输出到标准输出。第二个函数使用可变参数模板template <class... Args>来接受任意数量的参数。

在第二个函数中,我们使用逗号表达式和参数包展开来依次调用print()函数并打印每个参数。逗号表达式(print(args), 0)首先调用print(args)打印参数,然后返回0。这样,我们实际上只是在每个参数之后插入了一个0,并将它们放入一个整数数组中。这个数组的目的只是为了正确展开参数包,并不会被使用。

在main()函数中,我们调用了print()函数,并传递了四个不同类型的参数:整数1,字符'y',浮点数5.5和字符串"abcd"。这些参数会被正确展开并传递给相应的print()函数进行打印。

总的来说,可变参数模板在实现各种通用函数和类模板时非常有用,例如日志记录、格式化输出、数据结构等,它提供了一种灵活而强大的方式来处理可变数量的参数。

STL容器中empalce相关接口函数

STL提供了一些emplace相关的接口函数,用于在容器中就地构造元素,避免了额外的拷贝或移动操作。下面是一些常见的emplace相关接口函数:

  1. emplace_back():在容器的末尾就地构造一个元素,并将其插入容器中。它接受构造元素所需的参数,并直接在容器内构造元素,而不是先构造一个临时对象再进行拷贝或移动。
cpp 复制代码
vector<int> arr;
arr.emplace_back(1);
  1. emplace():在容器中指定位置就地构造一个元素,并将其插入容器中。它接受一个迭代器参数和构造元素所需的参数,直接在容器内指定位置构造元素。
cpp 复制代码
vector<int> arr;
vector<int>::iterator it = arr.emplace(arr.begin(), 2);

优点

可以看到的emplace系列的接口都支持模板的可变参数,还是万能引用,相对于insert系列接口它具有如下优势:

优势 说明
避免了额外的拷贝或移动操作 emplace系列接口允许在容器中就地构造元素,而不是通过拷贝或移动构造函数构造一个临时对象再进行插入。这样可以避免不必要的拷贝或移动操作,提高了性能和效率。
减少对象构造的开销 使用insert系列接口时,需要先构造一个临时对象,然后将该对象拷贝或移动到容器中。而emplace系列接口直接在容器内部构造元素,减少了额外的构造开销。特别是对于大型或复杂的对象,避免了多次构造和析构的开销。
支持可变参数和万能引用 emplace系列接口可以接受可变数量的参数,并支持引用折叠,即万能引用。这使得我们可以直接将构造元素所需的参数传递给emplace函数,而不需要显式地构造一个临时对象或进行类型转换。这样可以提高代码的简洁性和灵活性。
提高代码的可读性和维护性 使用emplace系列接口可以更直观地表达我们的意图,即在容器中就地构造元素。这样可以使代码更易于理解和维护,减少了不必要的中间步骤和临时对象的引入。

总的来说,emplace系列接口相对于insert系列接口在性能、效率和代码简洁性方面具有优势。它们通过就地构造元素,减少了不必要的拷贝或移动操作,并支持可变参数和万能引用,提供了更高效和灵活的方式来插入元素到容器中。

lambda表达式

Lambda表达式是C++11引入的一种匿名函数的语法特性,它允许我们在需要函数对象的地方编写简洁的、临时的函数定义。Lambda表达式的语法形式如下:

cpp 复制代码
[capture list] (parameters)mutable -> return_type 
{
    // 函数体
}

其中capture list是捕获列表,用于在Lambda表达式中捕获外部变量。可以为空,也可以包含一个或多个变量,以逗号分隔。捕获列表指定了Lambda表达式所使用的外部变量的方式。

parameters是参数列表,用于指定Lambda表达式的参数。和普通函数一样,可以为空或包含一个或多个参数,以逗号分隔。

mutable是Lambda表达式的一个关键字,用于指示Lambda函数体中的捕获变量可以被修改。默认情况下,Lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

return_type为返回类型,用于指定Lambda表达式的返回类型。可以省略,编译器可以自动推导返回类型。最后是Lambda表达式的函数体,用于实现具体的功能。

除了捕获列表和函数体外,其他都可以省略,因此可以写出最简单的Lambda函数,[]{},但是该表达式并没有什么作用,没有意义。可以写一个有意义的Lambda,通过Lambda表达式实现求和功能,如下:

cpp 复制代码
int sum = [](int x, int y){return x + y; }(5, 6);

捕获列表

捕获列表位于Lambda表达式的方括号[]中,用逗号分隔捕获的变量。捕获列表有以下几种形式:

  1. 值捕获:通过值方式捕获外部变量,可以在Lambda函数体内部以只读方式使用这些变量。变量的值在Lambda创建时被复制,后续对外部变量的修改不会影响Lambda内部的值。
cpp 复制代码
int main()
{
	int x = 10, y = 20;
	auto sum = [x, y]()->int{return x + y; };
	cout << sum() << endl;
	return 0;
}
  1. 引用捕获:通过引用方式捕获外部变量,可以在Lambda函数体内部以读写方式使用这些变量。变量的引用在Lambda创建时被捕获,后续对外部变量的修改会影响Lambda内部的值。
cpp 复制代码
int main()
{
	int x = 10, y = 20;
	auto sum = [&x, &y]()->int
	{
		x = 1;
		y = 2;
		return x + y;
	};
	int ret = sum();
	cout << "x = " << x << " y = " << y << " sum = " << ret << endl;
	return 0;
}
  1. 隐式捕获:通过自动推导的方式捕获外部变量。Lambda表达式可以根据函数体内部是否使用外部变量来自动推导需要捕获的变量。使用隐式捕获时,可以通过=表示以值方式捕获所有外部变量,或通过&表示以引用方式捕获所有外部变量。
cpp 复制代码
int main()
{
	int x = 10, y = 20;

	auto sum = [=]()->int
	{
		return x + y;
	};
	int ret1 = sum();
	cout << "x = " << x << " y = " << y << " sum = " << ret1 << endl;
	
	auto dif = [&]()->int
	{
		x *= 10;
		y *= 5;
		return x - y;
	};
	int ret2 = dif();
	cout << "x = " << x << " y = " << y << " dif = " << ret2 << endl;

	return 0;
}
  1. 显式捕获:通过显式指定捕获的变量来捕获外部变量。可以使用逗号分隔来指定多个捕获的变量。
cpp 复制代码
int main()
{
	int x = 10, y = 20;
	auto sum = [x, &y]()->int
	{
		y += 5;
		return x + y;
	};
	int ret = sum();
	cout << "x = " << x << " y = " << y << " sum = " << ret << endl;
	return 0;
}

需要注意的是,在Lambda表达式中可以同时使用不同的捕获方式,根据需要选择合适的捕获方式。捕获列表的选择取决于Lambda函数体内部对外部变量的访问需求和修改需求。总的来说捕获列表是Lambda表达式中非常重要的一部分,它决定了Lambda函数体内部可以访问和使用的外部变量。合理使用捕获列表可以使Lambda表达式更加灵活和功能强大。

注意!!!

lambda表达式之间不能相互赋值。在C++中,Lambda表达式是一种匿名函数对象,每个Lambda表达式都有其自己的唯一类型。即使两个Lambda表达式在语法上看起来具有相同的参数列表和返回类型,但是它们实际上是不同的类型,不能相互赋值。这是因为C++编译器会为每个Lambda表达式生成一个独特的闭包类型,该闭包类型包含Lambda表达式的函数体和捕获的变量。即使两个Lambda表达式的函数体和捕获变量完全相同,它们也不会共享相同的闭包类型。例如:

cpp 复制代码
#include <iostream>

int main() {
    auto lambda1 = [](int x) 
    {
        return x * 2;
    };

    auto lambda2 = [](int x) 
    {
        return x * 2;
    };

    lambda1 = lambda2;  // 错误:Lambda表达式之间不能相互赋值

    return 0;
}

在上述示例中,lambda1和lambda2是两个具有相同函数体的Lambda表达式。尽管它们函数体相同,但由于它们是不同的闭包类型,所以不能相互赋值。编译器将会报告错误。但如果需要在不同的Lambda表达式之间共享函数体,可以考虑将函数体提取为一个可调用对象(例如函数指针、std::function等),然后将该可调用对象分配给不同的Lambda表达式,这样可以实现Lambda表达式之间的函数体共享。如下:

cpp 复制代码
#include <iostream>
#include <functional>

int main() 
{
    auto function = [](int x) {
        return x * 2;
    };

    auto lambda1 = function;
    auto lambda2 = function;

    std::cout << lambda1(2) << std::endl;  
    std::cout << lambda2(3) << std::endl;  

    return 0;
}

将Lambda表达式的函数体提取为一个可调用对象function,然后将其分配给lambda1和lambda2。现在lambda1和lambda2共享相同的函数体,就可以独立的调用它们。

底层实现

在底层编译器实现中,Lambda表达式通常被转换为函数对象的方式来处理。编译器将Lambda表达式转换为一个匿名的、自动生成的类,并为该类生成一个重载了函数调用运算符的成员函数。该成员函数包含了Lambda表达式的函数体,并对捕获的变量进行处理。

这个自动生成的类可以被视为函数对象,它具有与Lambda表达式相同的行为和语义。编译器会为每个Lambda表达式生成一个唯一的类类型,并为每个捕获的变量生成相应的成员变量。编译器还会根据Lambda表达式的捕获列表,将需要捕获的变量作为该类的成员变量,并在构造函数中进行初始化。对于值捕获的变量,编译器会在类的构造函数中将其复制或移动到成员变量中。对于引用捕获的变量,编译器会将其作为成员变量的引用。

cpp 复制代码
class Sub
{
public:
	int operator()(int x, int y)
	{
		return x / y;
	}
};

int main()
{
	auto sum = [](int x, int y) 
	{
		return x + y;
	};
	Sub sub;
	cout << sub(30, 7) << endl;
	cout << sum(18, 7) << endl;  
	return 0;
}

需要注意的是,编译器的具体实现可能会有所不同,不同的编译器可能采用不同的优化策略和实现方式。但是,从语义上讲,Lambda表达式可以被看作是被转换为函数对象的形式来处理。

总结

文章介绍了C++11中的default、delete和final关键字,对可变参数模板也进行了详细的介绍。在C++11中STL容器新增加了emplace相关接口,就接口的用法以及特点也进行了详细的介绍,并与insert接口进行比对,阐述emplace接口的优点。最后对lambda表达式的用法、捕获列表以及底层实现都进行了详细的介绍。

如果文章对你有帮助的话就来一个三连呗,谢谢!🌹🌹🌹🌹

相关推荐
学步_技术1 分钟前
Python编码系列—Python抽象工厂模式:构建复杂对象家族的蓝图
开发语言·python·抽象工厂模式
BeyondESH23 分钟前
Linux线程同步—竞态条件和互斥锁(C语言)
linux·服务器·c++
wn53125 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
豆浩宇32 分钟前
Halcon OCR检测 免训练版
c++·人工智能·opencv·算法·计算机视觉·ocr
Hello-Mr.Wang37 分钟前
vue3中开发引导页的方法
开发语言·前端·javascript
救救孩子把40 分钟前
Java基础之IO流
java·开发语言
WG_1741 分钟前
C++多态
开发语言·c++·面试
宇卿.1 小时前
Java键盘输入语句
java·开发语言
Amo Xiang1 小时前
2024 Python3.10 系统入门+进阶(十五):文件及目录操作
开发语言·python
friklogff1 小时前
【C#生态园】提升C#开发效率:深入了解自然语言处理库与工具
开发语言·c#·区块链