C++11学习笔记

C++11是C++编程语言的一个重要标准版本,发布于2011年。这个版本引入了许多新特性和改进,使得C++更加现代化和高效。本文对部分特性做介绍。

统一的列表初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定,而C++11扩大了初始化列表的使用范围,使其可用于所有内置类型和用户自定义的类型,使用初始化列表时可添加等号(=),也可不添加 。

下面代码演示列表初始化:

cpp 复制代码
struct Friend
{
Friend(const string& name, int age) :_name(name)
	, _age(age) {}
private:
	string _name;
	int _age;
};

void test1() {
	Friend oldsport{ "老王",30 };
	vector<Friend>{{"老王", 30}, { "老张",40 }, { "老李",46 }};
	for (auto i : Friendset) {
		i.Print_Information();
	}
	cout << typeid(int).name();
}

在C++11之前为多个对象动态分配内存要分步初始化:

cpp 复制代码
Friend* Bro = new Friend[2];
// 必须手动一个一个赋值
Bro[0].name = "老王";
Bro[0].age = 30;

现在可以用初始化列表统一初始化 ,其实本质也是调用了构造函数 ,这正是多参数的隐式类型转换

cpp 复制代码
Friend* Bro = new Friend[2]{"老王", 30}, { "老张",40 }};

explict修饰构造函数就不支持隐式类型转换了,也就不支持列表初始化了:

cpp 复制代码
explicit Friend(string&& name, int age) :_name(name)//不支持隐式类型转换
	, _age(age) {}

std::initializer_list

cpp 复制代码
vector<Friend>{{"老王", 30}, { "老张",40 }, { "老李",46 }};

对于前面这句代码,如果花括号{}是初始化列表,那"{"老王", 30}, { "老张",40 }, { "老李",46 }"理应被识别为构造vector的参数,也就意味着vector要有多个构造函数以适应构造时不同的参数个数。这显然是不合理的。

实际上"{"老王", 30}, { "老张",40 }, { "老李",46 }"先被当做初始化列表的初始值,用于构initializer_list 这个容器,再用该容器去构造vector 。initializer_list结构很简单,它的成员只有两个分别指向数据头尾的指针。C++11中很多容器都添加了initializer_list类型成员,当容器构造时,只需从initializer_list的头指针开始,将数据拷贝下来,到尾指针结束。

所以不论初始化容器时传入多少参数,构造函数只需接收initializer_list再进行构造。这个过程本质是容器把栈上的数据拷贝(或移动)到堆上的新内存中,initializer_list起到中转作用。

decltype

typeid关键字可以推导变量的类型,但不能将推导出来的类型拿来用 。而C++11新增的decltype既可以推导,其返回值也可用于定义其它变量

cpp 复制代码
void test2() {
    int a = 10;
    decltype(a) b = 10;
	Friend A{ "A",18 };
	Friend B(decltype(A));
	return;
}

右值引用

C++11引入右值的概念,右值可以理解用之即弃的变量或常量。右值无法取地址,无法被赋值和用于赋值,无法直接修改 。比如函数的返回值,字面量,将亡值

**将亡值:**生命周期即将结束的值,可对左值进行move操作,将左值转换为将亡值

右值引用和左值引用

注意,右值和左值引用本身都是左值 ,因为声明引用的格式是创建变量的格式,并不是用之即弃的;右值本身不可以被修改,但可以通过右值引用来修改右值 ,所以右值不需要加const,而右值引用根据情况加const修饰。

右值引用示例:

cpp 复制代码
//和左值引用相比,右值引用多了一个&
int&& A = 10;
A = 1;//可以通过引用来修改右值

左右值的互相引用

普通左值引用不可以引用右值,而const左值引用可以引用右值 。所以之前实现接口都强调左值引用如果允许尽量加const,就是为了兼容右值 。如果要使用右值引用去引用左值,必须使用move函数将左值变为将亡值

右值引用的价值

虽然const左值引用可以接收右值,但不能对右值进行修改 ,而右值本就要被销毁,如果想利用这个值只能先进行拷贝,开销大。使用右值引用可以将右值的资源直接拿来用,从而减少了拷贝的开销。这种转移资源所有权的策略 被称之为移动语义,下面介绍移动语义的一些应用场景。

移动构造

移动构造不是构造函数的改版,而是拷贝构造函数的改版。下面是string的移动构造:

cpp 复制代码
// 移动构造
string(string&& s)
    : _str(s._str)      // 1. 直接窃取指针(极快,只是赋值)
    , _size(s._size)    // 2. 直接窃取大小
    , _capacity(s._capacity) // 3. 直接窃取容量
{
    // 4. 把源对象掏空,防止它析构时释放内存,因为这里的s是一个右值,意味着他过会会被销毁,自然会调用析构函数,而我刚到手的str不能就这样被删了
    s._str = nullptr;
    s._size = 0;
    s._capacity = 0;
}

如果是普通的拷贝构造,就要把传入对象的字符串指针指向的字符串逐一拷贝到新的字符串数组中,再用当前对象的字符串指针来指向该数组。

现已知传入对象即将被销毁,莫不如把传入对象的字符串指赋值给当前对象的字符串指针 ,这样就没有拷贝开销了 。注意"s._str = nullptr"这行代码的作用是切断源对象与内存的联系,否则移动构造完成后将销毁当前对象的字符串指针指向的对象。

移动赋值

和移动构造同理,右值引用还可以用于改造赋值运算符重载,从而提高效率。

cpp 复制代码
// 移动赋值
string&operator=(string&& s)
{
    if(_str)
    delete[]_str;//释放原有资源
    
    _str = s._str;
    _size = s._size ;
    _capacity = s._capacity;

    s._str = nullptr;
    s._size = 0;
    s._capacity = 0;

    return *this;
}

上面两种场景被称为移动语义,这种方式在特定情况下减小了很多开销,所以C++11移动构造和移动赋值也成为了默认成员函数。

移动赋值有一种情景值得注意:编译器的返回值优化

cpp 复制代码
string to_string(int input){
    string str;
    //...略,将input转为string并赋值给str
    return str;//这里str将亡,调用移动构造
}

int main(){
    int a=10;
    string b=to_string(a);//这里的返回值将亡,调用移动构造,按理有两次移动构造,但是编译器的返回值优化机制使其变为了一次
}

在插入和删除场景中的应用:

右值引用还可以用在插入元素的场景中,以list的insert函数为例:

cpp 复制代码
原来的Insert函数
iterator insert(iterator pos, const T& value) {
	node* newnode=new node(value);//假如这里的value是一个右值,这里的拷贝是不必要的
	node* temp = pos._it;
	node* prev = temp->_prev;
	
	prev->_next = newnode;
	newnode->_prev = prev;
	temp->_prev = newnode; 
	newnode->_next = temp;
	_size++;
	return newnode;
}

//新增移动语义的Insert函数
iterator insert(iterator pos, T&& value) {
	node* newnode=new node(move(value);//调用移动构造
	node* temp = pos._it;
	node* prev = temp->_prev;
	
	prev->_next = newnode;
	newnode->_prev = prev;
	temp->_prev = newnode; 
	newnode->_next = temp;
	_size++;
	return newnode;
}

为什么这里要用move转换value呢?前文已经提到过,右值引用本身是左值,所以必须对这里的value做转换,才能够调用到node的移动构造。

注意:右值引用和移动语义诞生的初衷,是为了避免深拷贝 带来的巨大开销,所以右值引用对内置类型意义不大。

禁止生成默认成员函数delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁 已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即 可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

cpp 复制代码
struct Friend
{
Friend(const string& name, int age) :_name(name)
	, _age(age) {}

Friend(const Friend& f) = delete;//禁止生成默认拷贝构造
private:
	string _name;
	int _age;
};

万能引用和完美转发

在C++11中,如果把模版参数T写成T&&的形式并满足下面两个条件,就能触发完美引用。

  1. 形式必须是 T&& (不能加 const 等修饰)。
  2. 必须发生模板类型推导T 是未知的,需要编译器去猜)。

完美引用根据传入参数是右值/左值变为相应引用,所以左值和右值都可以被引用。但是,一个右值引用的格式"&&"为什么能引用左值呢?实际上这里触发了引用折叠机制,假如传入的是一个左值,T&&就会折叠成T&,从而实现左值引用。

cpp 复制代码
template <typename T>
void func(T&& param); //万能引用

辨析:能不能让成员函数只保留右值引用版本,进而实现构造函数的万能引用?比如下面这样:

cpp 复制代码
template <typename T>
class MyClass {
public:
    void push(T&& val); 
};

push不能触发万能引用,因为当MyClass<int> 实例化时,T已经确定是 int 了,再去调用成员函数时,编译器不需要推导就能知道val的类型是int,于是乎这里的val就变成普通右值引用。下面演示如何让成员函数触发万能引用。

cpp 复制代码
template <typename T>
class MyClass {
public:
    // 这才是万能引用
    template <typename U>
    void push(U&& val); 
};
cpp 复制代码
Myclass<int> myclass1;
myclass1.push<int>(10);

在这里,U是独立的模板参数,每次调用push时,U会重新推导。

完美转发:

前文已经提到过,无论实参是左值或右值,只要被对应引用接收,由于引用本身是左值,在函数内部形参都成了左值,如果再用该形参做其它函数实参,就无法保持最初的右值属性。

在普通函数中,我们需要手动move保持右值属性,而在万能引用的情境下需使用完美转发来解决

下面代码对比示例完美转发如何保持属性:

可以看到,最后Fun匹配的都是左值引用版本,和预期不符,而下面完美转发版本解决了此问题

这里可以看到调用Fun的时候用forward<T>对t进行转换,就能保持原属性了。

完美转发(std::forward):在一个函数模板中,将接收到的参数原封不动地转发给另一个函数。

这里的"原封不动"包含两层含义:

  1. 值类别不变:传入的是左值,转发后仍是左值;传入的是右值,转发后仍是右值。
  2. 类型属性不变 :参数的 constvolatile 等类型修饰符也会被保留。

可变参数模板

C++11可变参数模板能创建可变参数的函数模板和类模板

lambda表达式

定义

lambda表达式是一种以仿函数为底层的语句,像容器比较器、键提取器、哈希函数都要用到仿函数,但有时仿函数只需简短的语句实现,这时可以考虑使用lambda表达式

语法:

[捕捉列表](参数)->返回值{函数体}

也可以是**[捕捉列表](参数){函数体}**,让编译器自行推导返回值

其实除了捕捉列表和正常的函数体区别不大,但它不需要额外定义。

lambda表达式底层仿函数 ,只要是仿函数就会有类名 ,lambda表达式的类名是由系统自动生成的uid,所以用户无法主动接收,要用auto自动推导。

即便两个lambda表达式的定义一样,系统分配的uid也是不同的,而由于类名各不相同,lambdab表达式之间不能互相赋值。

捕捉列表描述了上下文中哪些数据可以被lambda使用,以及使用的方式是传值还是传引用。下面列举捕捉列表可填入项:

[var]:表示值传递方式捕捉变量var

[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

[&var]:表示引用传递捕捉变量var

[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

[this]:表示值传递方式捕捉当前的this指针

下面演示lambda表达式做比较器

cpp 复制代码
struct Commodity {
	string _name;
	double _price;
	double _eval;
};

void test6() {
    vector<Commodity>table = { {"钢笔",11.2,8} ,{ "铅笔",1.5,7}, { "圆珠笔",3,10 } };
    auto PriceLess = [](Commodity x, Commodity y)->bool {return x._price < y._price; };
    sort(table.begin(), table.end(),PriceLess);//lambda底层本质是仿函数所以可以作为sort的参数
    return;
}

下面演示捕捉列表的使用:

cpp 复制代码
Commodity crayon = { "蜡笔",0.5,7 };
Commodity eraser = { "橡皮",2,9 };
auto EvalLess = [=]() {cout << (crayon._eval < eraser._eval); };//捕获父作用域中所有的值,如果不写'='Evaless就无法获取crayon和eraser
EvalLess();//调用lambda表达式

包装器

总共有三种可调用对象:函数指针、lambda表达式、仿函数,如果某个函数的形参可能会接收到这三种类型,那就必须定义形参为函数指针、仿函数版本,同时无法接受lambda表达式,而包装器正适合这种场景。

cpp 复制代码
int func1(int a,int b) {
	cout << "被调用" << endl;
	return a + b;
}	

class Myfunc {
public:
	int operator()(int a,int b) {
		func1(11, 12);
		
	}
};

void recept1(int (*func1)(int a,int b)) {//这个函数用来接收函数指针
	func1(11,12);
}

void recept2(Myfunc input) {//这个函数用来接收仿函数
	func1(11, 12);
}

//可以看到,参数同样是要调用func1,却因为类型不同必须写两个不同的接收函数
//下面函数用包装器做参数,只要返回值是int,参数是两个int的可调用参数,
void Function(function<int(int,int)> myfunction) {
	myfunction(11,12);
}

void test7() {
	//不使用包装器需要用两个不同的函数
	recept1(func1);
	recept2(Myfunc());
	//现在只需要用同一个函数就可以解决问题
	Function(func1);
	Function(Myfunc());
}

function底层是用仿函数实现的,所以function也可以作为容器的存储类型,用于存储可调用对象。

应用案例:逆波兰表达式求解

逆波兰表达式求值

在这道题目中,最繁琐的是判断条件:如果在给定数组中读取到运算符,要用swithcase语句为加减乘除分别写操作方法,这里用一map,以运算符为键,以lambda表达式为值,先用map的count函数判断i是否为运算符,如果是运算符,调用与该运算符匹配的lambda表达式,返回值入栈;若不是,i入栈。

cpp 复制代码
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        stack<int> mystack;
        //使用包装器进行封装
        //定义一个map,以包装器作为值,以srtring为键
        //包装器包装的应该是lambda表达式,表达式为符号服务
        //如果是符号,把当前和现在的数字进行符号计算
        map<string,function<int(int,int)>> opt ={
            {"+",[](int a,int b){return a+b;}},//这里需要传pair
            {"-",[](int a,int b){return a-b;}},
            {"*",[](int a,int b){return a*b;}},
            {"/",[](int a,int b){return a/b;}}
        };
        
        for(auto i:tokens){
            if(opt.count(i)){
                int a=mystack.top();
                mystack.pop();
                int b=mystack.top();
                mystack.pop();
                mystack.push(opt[i](b,a));//结果重新入栈
            }
            else{
                mystack.push(stoi(i));//转换
            }
        }  
        return mystack.top();//最后的计算结果
    }
};

绑定(std::bind)

绑定std::bind是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来"适应"原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作

绑定的语法:

auto 新的函数名字=bind(原函数名,placeholders::_1,placeholders::_2...placeholders::_n)

下面代码演示给默认值:

cpp 复制代码
int PLUS(int a,int b,int c) {
	cout << "PLUS被调用" << " " << "操作数为" << a << " " << b<< endl;
	return a + b +c ;
}

auto func1=bind(PLUS, 1, placeholders::_1,2);//得到int PLUS(1,int b,2) {}

注意:``placeholders::_n 永远代表"新生成的函数"的第n个参数 ,func1中因为ac都有了默认值,只剩下b没被绑定。所以这里的"placeholders::_1"等同于原来的"b"。

下面代码演示利用绑定调换参数顺序:

cpp 复制代码
int PLUS(int a,int b,int c) {
	cout << "PLUS被调用" << " " << "操作数为" << a << " " << b<< endl;
	return a + b +c ;
}

// 绑定规则:a=新函数的第2个参数, b=新函数的第3个参数, c=新函数的第1个参数
auto func2 = bind(PLUS, placeholders::_2, placeholders::_3, placeholders::_1);

// 调用
func2(100, 10, 20);
// 解析:
// _1 (100) -> 传给 c
// _2 (10)  -> 传给 a
// _3 (20)  -> 传给 b
// 实际执行:PLUS(10, 20, 100);

如果绑定的是类的成员函数,bind需要增加其它参数:

cpp 复制代码
template<class T>
class Testsum {
public:
	int member1(int k,string name) {
		cout << name << "编号为" << k << endl;
		return k;
	}
};
//第一个参数是类中指定的成员函数,第二个参数是要传类对象或者类对象地址
Testsum<int> Instantiation;
auto func5= bind(&Testsum<int>::member1, &Instantiation, placeholders::_1, placeholders::_2);
auto func6 = bind(&Testsum<int>::member1, Testsum<int>(), 10, placeholders::_2);
相关推荐
寒秋花开曾相惜1 小时前
(学习笔记)4.2 逻辑设计和硬件控制语言HCL(4.2.3 字级的组合电路和HCL整数表达式)
android·网络·数据结构·笔记·学习
70asunflower1 小时前
C/C++ 自定义函数的常用规范:从入门到工程实践
c语言·c++
谭欣辰1 小时前
C++ DFS 与 BFS 剪枝方法详解
c++·算法·剪枝
c++之路1 小时前
C++ 预处理器
开发语言·c++
2301_809049421 小时前
WSL无法打开gui界面时,以及安装东西分两种
学习
CN-Dust1 小时前
【C++专题】格式化输出与输入
开发语言·c++·算法
Titan20241 小时前
C++位图学习笔记
c++·笔记·学习
念恒123061 小时前
Python(运算与操作)
python·学习
6Hzlia1 小时前
【Hot 100 刷题计划】 LeetCode 148. 排序链表 | C++ 归并排序自顶向下
c++·leetcode·链表