C++11(上)

目录

[{ } 列表初始化](#{ } 列表初始化)

[initializer_list 容器](#initializer_list 容器)

auto

decltype

[nullptr && 范围for](#nullptr && 范围for)

STL容器的变化

lambda表达式

可变模版参数


{ } 列表初始化

C++98中,标准允许使用大括号 { } 对数组或者结构体元素进行统一的列表初始值设定。

cpp 复制代码
struct stu
{
	int id;
	const char* name;
	double score;
};

int main()
{
	int a[] = { 1, 2, 3 };
	struct stu s = { 1, "zhangsan", 20.5 };
	return 0;
}

C++11扩大了用大括号括起来的列表 {初始化列表} 的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号,也可不添加。比如:

cpp 复制代码
struct stu
{
	int id;
	const char* name;
	double score;
};

int main()
{
	int a = { 1 }; //可以添加等号
	int b{ 1 }; //可以不添加等号
	int c[] = { 1, 2, 3 }; //可以添加等号
	int d[]{ 1, 2, 3 }; //可以不添加等号
	struct stu s1 = { 1, "zhangsan", 20.5 }; //可以添加等号
	struct stu s2{ 1, "zhangsan", 20.5 }; //可以不添加等号
	int* p = new int[3] {1, 2, 3};
	//int* p = new int[3] = {1, 2, 3}; //err, 不可添加等号
	return 0;
}

{ } 也用于创建对象时调用构造函数完成对对象的初始化

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
    //虽然都是调用构造函数, 但通常写法是直接调用构造函数, 而C++11新增写法本质是多参数构造函数的隐式类型转换
	Date d1(2025, 8, 30); //通常写法
    //C++11新增写法
	Date d2 = { 2025, 8, 30 }; //可以添加等号
	Date d3{ 2025, 8, 30 }; //也可以不添加等号
	return 0;
}

initializer_list 容器

initializer_list 本质就是一个大括号括起来的列表,如果用 auto 关键字定义一个变量来接收一个大括号括起来的列表,然后以 typeid(变量名).name() 的方式查看该变量的类型,就可以看到该变量的类型就是 initializer_list

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
	auto l = { 1, 2, 3 };
	cout << typeid(l).name() << endl; //class std::initializer_list<int>
	return 0;
}

initializer_list 是C++11新增的一个容器(类模板),该容器并没有提供过多的成员函数只,只有容器大小,以及迭代器访问接口,可以认为 initializer_list 底层是使用了 _start / _finish 两个指针维护区间元素

cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
	initializer_list<int> l = { 1, 2, 3 };
	initializer_list<int>::iterator it = l.begin();
	while (it != l.end())
	{
		cout << *it << " "; //1 2 3
		++it;
	}
	return 0;
}

因此 initializer_list 最大的用途是用于其他容器的初始化工作的,有了initializer_list,很多容器就支持列表初始化了!

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
#include <map>
using namespace std;
int main()
{
	//列表初始化
	vector<int> v = { 1, 2, 3 };
	list<int> l = { 1, 2, 3 };
	map<string, string> mp = { {"insert", "插入"}, {"left", "左边"} }; //外面的{}是 mp 的initializer_list构造, 内部的{}是pair参数的隐式类型转换
	return 0;
}

现在我们就可以给之前自己模拟实现的 vector 添加上列表初始化的构造函数了,赋值重载也加上 initializer_list 版本

cpp 复制代码
namespace dck
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;
		vector(initializer_list<T> il)
		{
			reserve(il.size);
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		vector<T>& operator=(initializer_list<T> il)
		{
			vector<T> tmp(il);
			swap(_start, tmp._start);
			swap(_finish, tmp._finish);
			swap(_endofstorage, tmp._endofstorage);
			return *this;
		}
	private:
		iterator _start;
		iterator _finish;
		iterator _endofstorage;
	};
}

int main()
{
	dck::vector<int> v = { 1, 2, 3 }; //initializer_list 构造
	v = { 4, 5, 6, 7, 8 }; //赋值重载
}

auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。

C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。这样可以简化代码,或者当我们不知道表达式结果到底是啥类型,就可以使用auto,让编译器自动推导

cpp 复制代码
int main()
{
	unordered_map<string, int> mp;
	mp.insert({ "apple",  2 });
	mp.insert({ "orange", 3 });
	mp.insert({ "banana", 1 });
	//unordered_map<string, int>::iterator it = mp.begin();
	auto it = mp.begin(); //简化代码
	int a = 10;
	double b = 20.5;
	auto c = a + b; //防止类型不对而导致精度损失
}

decltype

通过 typeid(变量名).name()的方式可以获取一个变量的类型,但无法用获取到的这个类型去定义变量,而 decltype 则可以将变量的类型声明为表达式指定的类型。

cpp 复制代码
template<class T1, class T2>
void func(T1 x1, T2 x2)
{
	decltype(x1 * x2) x;
	cout << typeid(x).name() << endl;
}

int main()
{
	int a = 10;
	double c = 15.3;
	decltype(a * c) d; //d的类型为a * c这个表达式运算结果的类型
	decltype(&c) e;

	cout << typeid(d).name() << endl; //double
	cout << typeid(e).name() << endl; //double*
	func(10, 5); //int
	func(10, 5.5); //double
}

decltype 除了能够推演表达式的类型,还能推演函数类型以及函数返回值的类型。

cpp 复制代码
int* func(int x)
{
	int* p = new int[10];
	return p;
}

int main()
{
	decltype(func) d1; //不带参数, 推导的是函数类型
	cout << typeid(d1).name() << endl; //int * __cdecl(int)

	decltype(func(10)) d2; //带参数, 推导的是函数返回值类型
	cout << typeid(d2).name() << endl; //int * 
}

decltype还可以指定函数返回值的类型

cpp 复制代码
template<class T1, class T2>
auto func(T1 x1, T2 x2)->decltype(x1 + x2)
{
	return x1 + x2;
}

int main()
{
	cout << typeid(func(1, 1.4)).name() << endl; //double
}

nullptr && 范围for

详见 C++基础语法

STL容器的变化

C++11中新增了四个容器,分别是 array、forward_list、unordered_map、unordered_set。

array

array容器本质就是一个静态数组,即固定大小的数组。

array容器有两个模板参数,第一个模板参数代表的是存储的类型,第二个模板参数是一个非类型模板参数,代表的是数组中可存储元素的个数。

cpp 复制代码
#include <array>
int main()
{
	array<int, 5> a = {1, 2, 3};
	array<double, 5> a;
}

array容器与C语言普通数组不同之处就是,array容器用一个类对数组进行了封装,并且在访问array容器中的元素时会进行越界检查。用 [ ]访问元素时采用断言检查,调用 at 成员函数访问元素时采用抛异常检查;而对于普通数组来说,一般只有对数组进行写操作时才会检查越界,如果只是越界进行读操作可能并不会报错。

但array容器与其他容器不同的是,array容器的对象是创建在栈上的,因此array容器不适合定义太大的数,负责会有栈溢出的风险

forward_list

forward_list 底层是单链表,但用的很少,forward_list 只支持头插头删,不支持尾插尾删,因为尾插尾删的还要找尾,时间复杂度为O(N),并且没有提供size函数,且只能单向遍历,功能受限,绝大多数场景下还是使用 list

C++11还提供了各种各样的字符串转化函数,可以将内置类型转化成字符串,也可以将字符串转化为内置类型

容器中的一些新方法

C++11为每个容器都增加了一些新方法,比如:

• 提供了initializer_list构造函数,支持列表初始化

• 提供了cbegin和cend方法,用于返回const迭代器(很少用到,因为begin和end方法就有普通版本和const版本)。

• 提供了emplace系列方法,并在容器原有插入方法的基础上重载了一个右值引用版本的插入函数,用于提高向容器中插入元素的效率

lambda表达式

前置知识:STL(三) list基本用法

在 list 基本用法 这篇博客里面我们介绍了 算法库中 sort 的基本用法 和 仿函数

cpp 复制代码
struct Goods
{
	string name;
	double price;
	int num;
};
int main()
{
	vector<Goods> v = { {"苹果", 12.2, 10}, {"香蕉", 3.3, 100}, {"橘子", 20, 30}};
	return 0;
}

我们自定义了一个Goods类,现在我们要对自定义类型的数组进行sort排序,有两种方式解决:

  1. sort 的第三个参数默认是 less仿函数,仿函数中对两个Goods对象进行 < 比较,因此我们可以在Goods类中重载 < 运算符,实现比较
cpp 复制代码
struct Goods
{
	bool operator<(const Goods& g)
	{
		return name < g.name;
	}

	string name;
	double price;
	int num;
};

这种方式不太好,因为直接把比较规则写死了,目前只能按照名字排序,要按照价格或者数量排序,就需要修改 operator< 重载函数的内部逻辑了

  1. 我们自定义仿函数,重载(),自定义仿函数逻辑,完成元素的比较
cpp 复制代码
class CmpName
{
public:
    bool operator()(const Goods& g1, const Goods& g2)
    {
        return g1.name < g2.name;
    }
};

class CmpPrice
{
public:
    bool operator()(const Goods& g1, const Goods& g2)
    {
        return g1.price < g2.price;
    }
};

class CmpNum
{
public:
    bool operator()(const Goods& g1, const Goods& g2)
    {
        return g1.num < g2.num;
    }
};

sort如下:

cpp 复制代码
sort(v.begin(), v.end(), CmpName());
sort(v.begin(), v.end(), CmpPrice());
sort(v.begin(), v.end(), CmpNum());

这种方式也不太好,比如除了名字价格数量,商品还有很多其他属性,每个属性都要来一个仿函数,代码非常的冗余,其次如果仿函数名字起的不太好,那么代码可读性比较差,不太直观**,此时我们引入lambda表达式**

lambda表达式使用

lambda表达式书写格式:

[capture-list](parameters)mutable->return-type{statement}

• [capture-list]:捕捉列表。该列表总是出现在lambda函数的开始位置,编译器根据 [ ] 来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。

• (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。

• mutable:传值捕捉的变量具有const属性,不可被修改,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

• -> return-type:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可以省略。返回值类型明确情况下也可省略,由编译器对返回类型进行推导。

• {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

上述商品数字排序使用lambda表达式写法如下:

cpp 复制代码
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1.name < g2.name; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1.price < g2.price; });
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1.num < g2.num; });

lambda 表达式的细节

  1. lambda表达式底层本质就是仿函数,只写一个 lambda 表达式并没有调用仿函数内的operator(),需要通过()调用,或者将该表达式存储起来,然后去调用
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    []() {cout << "haha" << endl; }; //没有调用
    []() {cout << "haha" << endl; }(); //调用
    auto f = []() {cout << "haha" << endl; }; 
    f(); // 调用
    return 0;
}
  1. 捕捉变量有两种方式,传值捕捉 & 传引用捕捉,传值捕捉到的变量相当于原变量的拷贝,不可修改,传引用捕捉到的变量就是本身,可以修改
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    int a = 10;
    //1.传值捕捉
    [a]()
    {
        //a = 20; //err,不可以修改a变量 
        cout << a << endl; //10
    }();

    //2.传引用捕捉
    [&a]()
    {
        a = 20; //可以修改a变量
        cout << a << endl; //20 
    }(); 
    return 0;
}
  1. 传值捕捉的变量默认不能被修改,但被mutable修饰后const属性被取消,可以被修改,但lambda表达式内修改不会影响到外部的变量,因为是传值捕捉
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    int a = 10;
    [a]() mutable 
    {
        a = 20; //可以修改 
        cout << a << endl; //20
    }();
    cout << a << endl; //10
    return 0;
}
  1. 局部变量或者局部函数对象需要捕捉后才能使用,但全局变量和全局函数对象无需捕捉,可以直接使用
cpp 复制代码
#include <iostream>
using namespace std;
int a = 20;
int main()
{
    auto Add = [](int a, int b) {return a + b; };
    [Add]() 
    {
        cout << a << endl; //20, 全局无需捕捉
        cout << Add(10, 20) << endl; //30, 局部需要捕捉
    }();
    return 0;
}
  1. 捕捉时可以一次性将上文所有的变量都进行传值捕捉或传引用捕捉,使用 "=" 传值捕捉,使用 "&" 传引用捕捉
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    int a = 10, b = 20, c = 30;
    auto Add = [](int a, int b) {return a + b; };
    [=]() {
        cout << a + b + c << endl; //60
    }();

    [&]() {
        a = 100, b = 200, c = 300;
        cout << a + b + c << endl; //600
    }();
    return 0;
}
  1. 传值捕捉和传引用捕捉可以混搭,比如a变量传引用捕捉,其余变量传值捕捉
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
    int a = 10, b = 20, c = 30;
    auto Add = [](int a, int b) {return a + b; };
    [=, &a]() //a变量传引用捕捉, 其余变量传值捕捉
    {
        a = 100;
        cout << a + b + c << endl; //150
    }();

    [&, b]() //b变量传值捕捉, 其余变量传引用捕捉
    {
        a = 100, c = 300;
        cout << a + b + c << endl; //420
    }();
    return 0;
}
  1. 捕捉列表不可以重复使用同一种方式捕捉,例如 [=,a] 或者 [&,&a],都会编译报错
cpp 复制代码
int a = 10, b = 20, c = 30;
[=, a]() {}(); //err
[&, &a]() {}(); //err
  1. 完全相同的两个lambda表达式,他们的类型都是不同的,不能相互赋值,并且 lambda 表达式的底层就是仿函数
cpp 复制代码
int main()
{
    auto f1 = [](){};
    auto f2 = [](){};
    cout << typeid(f1).name() << endl;  //class `int __cdecl main(void)'::`2'::<lambda_1>
    cout << typeid(f2).name() << endl;  //class `int __cdecl main(void)'::`2'::<lambda_2>
    return 0;
}

可变模版参数

cpp 复制代码
int printf ( const char * format, ... );

最早在学习printf的时候,我们就已经接触过可变参数了,printf 可以格式化输出多个参数

可变模版参数概念

相比于C++98/03中函数模板和类模板只能接收并使用固定数量的模板参数,c++11的可变参数模板可以让我们创建可以接收可变参数的函数模板和类模板

如下是可变函数模版的基本模样:

cpp 复制代码
//Args是可变函数模版参数包, args是函数形参参数包(可以是0个参数)
template <class... Args>
void func(Args ...args)
{}

可变模版参数使用

cpp 复制代码
#include <iostream>
using namespace std;

template <class... Args>
void func(Args ...args)
{
    cout << sizeof...(args) << endl; //固定写法
}

int main()
{
    func(1); //1
    func(1, 2.2); //2
    func(1, 2.2, string("xxx")); //3
    return 0;
}

注意: 我们无法直接在func函数中采用 [ ] 的方式去获取args参数包的具体内容

cpp 复制代码
template <class... Args>
void func(Args ...args)
{
    for (int i = 0; i < sizeof...(args); i++)
    {
        cout << args[i] << endl; //err
    }
}

只能通过展开参数包的形式来获取参数包中的具体参数,递归展开和逗号表达式展开两种方式:

递归展开(编译时的递归推演)

我们给func函数多带一个模版参数,这样在递归过程中参数包每次会被拆除一个参数,然后将带有剩余参数的参数包继续往下传,我们还需要一个递归出口函数,当参数包中没有参数时,就会匹配到该函数,来结束整个递归。

cpp 复制代码
#include <iostream>
using namespace std;

//递归结束
void func()
{
    cout << endl;
}

template<class T, class... Args>
void func(T val, Args ...args)
{
    cout << val << " ";
    func(args...);
}

int main()
{
    func(1);
    func(1, 2.2);
    func(1, 2.2, string("xxx"));
    return 0;
}

上述写法有一个问题,就是当func不带参数时,就会直接匹配到递归出口函数,和我们期望不一样,我们可以在外面套一层函数,这个函数只有可变模版参数

cpp 复制代码
void _func()
{
    cout << endl;
}

template<class T, class... Args>
void _func(T val, Args ...args)
{
    cout << val << " ";
    _func(args...);
}

template<class... Args>
void func(Args ...args)
{
    _func(args...);
}

int main()
{
    func();
    func(1);
    func(1, 2.2);
    func(1, 2.2, string("xxx"));
    return 0;
}

递归出口函数可以写成无参的,也可以带一个参数,但弊端是调用func时必须带参了!

cpp 复制代码
//递归结束
template<class T>
void func(T val)
{
    cout << val << endl;
}

template<class T, class... Args>
void func(T val, Args ...args)
{
    cout << val << " ";
    func(args...);
}

int main()
{
    func(1);
    func(1, 2.2);
    func(1, 2.2, string("xxx"));
    return 0;
}

逗号表达式展开:

如下代码所示,我们在初始化数组时列表内容为参数包,但由于数组每个元素类型必须相同,所以参数包中的参数类型都要和数组元素类型一致。

cpp 复制代码
template<class... Args>
void func(Args ...args)
{
    int arr[] = {args...};
    for (auto e : arr)
    {
        cout << e << " ";
    }
    cout << endl;
}

int main()
{
    func(1);
    func(1, 2);
    func(1, 2, 3);
    return 0;
}

而我们现在要做的是要借助逗号表达式展开参数包,逗号表达式的特点是从左向右运算每个表达式,最后一个表达式的值是整个逗号表达式的最终结果,因此我们可以将打印参数的函数作为逗号表达式第一个元素,随便一个整数作为最后一个元素,编译器在编译阶段要确定数组大小,就会去展开参数包,从而达到我们想要的效果。

cpp 复制代码
void ShowList()
{
    cout << endl;
}

template<class T>
void PrintArgs(const T& val)
{
    cout << val << " ";
}

//展开函数
template<class... Args>
void ShowList(Args... args)
{
    int arr[] = {(PrintArgs(args), 0)...};
    cout << endl;
}

int main()
{
    ShowList();
    ShowList(1);
    ShowList(1, 2.2);
    ShowList(1, 2.2, string("xxx"));
    return 0;
}

事实上,我们也可以不使用逗号表达式,直接将函数返回值设置成整形即可

cpp 复制代码
#include <iostream>
#include <functional>
using namespace std;

void ShowList()
{
    cout << endl;
}

template<class T>
int PrintArgs(const T& val)
{
    cout << val << " ";
    return 0;
}

//展开函数
template<class... Args>
void ShowList(Args... args)
{
    int arr[] = { (PrintArgs(args))... };
    cout << endl;
}

int main()
{
    ShowList();
    ShowList(1);
    ShowList(1, 2.2);
    ShowList(1, 2.2, string("xxx"));
    return 0;
}
相关推荐
会周易的程序员4 小时前
多模态AI 基于工业级编译技术的PLC数据结构解析与映射工具
数据结构·c++·人工智能·单例模式·信息可视化·架构
lixzest6 小时前
C++上位机软件开发入门深度学习
开发语言·c++·深度学习
苦藤新鸡7 小时前
4.移动零
c++·算法·力扣
hetao17338377 小时前
2026-01-04~06 hetao1733837 的刷题笔记
c++·笔记·算法
liulilittle8 小时前
XDP VNP虚拟以太网关(章节:一)
linux·服务器·开发语言·网络·c++·通信·xdp
Ralph_Y8 小时前
多重继承与虚继承
开发语言·c++
bkspiderx9 小时前
C++虚析构函数:多态场景下的资源安全保障
c++·析构函数·虚函数表·虚析构函数
White_Can9 小时前
《C++11:列表初始化》
c语言·开发语言·c++·vscode·stl
White_Can9 小时前
《C++11:右值引用与移动语义》
开发语言·c++·stl·c++11
Z1Jxxx9 小时前
字符串翻转
开发语言·c++·算法