【C++进阶篇】C++11新特性(中篇)

一. 右值引用

右值引用本质就是移动(挪动)资源,原因在于右值一般是马上要被销毁的资源,同时在函数返回值中可以减少拷贝问题。

1.1 什么是右值与左值

右值一般包括字面值常量,创建临时对象,如10,fmin(x,y),iterator(cur,nullptr)等,具有临时性。
左值是一个变量名,解引用指针如int a=1,*p=10等,一般具有持久性。

  • 不同点:左值可以取地址,右值不可以取地址。

1.2 左值引用和右值引用

两个引用都是取别名,前者是给左值取别名,后者是给右值取别名,左值引用不可以直接引用右值,原因:右值具有常性,涉及权限放大,可以使用const左值引用可以引用右值。
右值引用不可以直接引用左值,右值引用可以引用move(左值)。
结论:引用和指针在汇编实现都是一样的。

1.2.1 move

move是一个函数模版,本质就是类型强制转换。

template <class T> 
typename remove_reference<T>::type&& move (T&& arg);

在这里插入图片描述

虽然右值引用引用是右值,但&rr是允许的,因为右值引用本身是左值,左值是允许取地址的,右值是可以对该临时资源进行修改的。

int a = 10;

//左值引用 引用 右值
const int& a = 20;

//右值引用 引用 左值
int&& rr = move(a);

在这里插入图片描述
给右值加上const 就不允许修改它了。
在这里插入图片描述
注意:使用move左值语句后,可能会导致左值中的资源被挪动,所以在使用该move前须确保左值的资源不再使用。

string str = "hello C++!";
cout << "str: " << str << endl;

string rr = move(str);
cout << "str: " << str << endl;

输出结果:
在这里插入图片描述

1.3 右值使用场景

右值分为:纯右值和将亡值(说白了就是马上要被销毁的资源,与其它被销毁,不如二次回收,将所要被销毁的资源转移给需要的其它系统等)。将亡值的意义很大,尤其体现在拷贝场景下。
下面通过场景分析一下右值的用处。

下面是自己模拟实现的string类,未实现移动构造及移动赋值。

class string
{
public:
	typedef char* iterator;
	typedef const char* const_iterator;
	iterator begin()
	{
		return _str;
	}
	iterator end()
	{
		return _str + _size;
	}
	const_iterator begin() const
	{
		return _str;
	}
	const_iterator end() const
	{
		return _str + _size;
	}
	string(const char* str = "")
		:_size(strlen(str))
		, _capacity(_size)
	{
		cout << "string(char* str)-构造" << endl;
		_str = new char[_capacity + 1];
		strcpy(_str, str);
	}
	void swap(string& s)
	{
		::swap(_str, s._str);
		::swap(_size, s._size);
		::swap(_capacity, s._capacity);
	}

	string(const string& s)
		:_str(nullptr)
	{
		cout << "string(const string& s) -- 拷贝构造" << endl;
		reserve(s._capacity);
		for (auto ch : s)
		{
			push_back(ch);
		}
	}

	string& operator=(const string& s)
	{
		cout << "string& operator=(const string& s) -- 拷贝赋值" <<
			endl;
		if (this != &s)
		{
			_str[0] = '\0';
			_size = 0;
			reserve(s._capacity);
			for (auto ch : s)
			{
				push_back(ch);
			}
		}
		return *this;
	}
	~string()
	{
		cout << "~string() -- 析构" << endl;
		delete[] _str;
		_str = nullptr;
	}

	char& operator[](size_t pos)
	{
		assert(pos < _size);
		return _str[pos];
	}

	void reserve(size_t n)
	{
		if (n > _capacity)
		{
			char* tmp = new char[n + 1];
			if (_str)
			{
				strcpy(tmp, _str);
				delete[] _str;
			}
			_str = tmp;
			_capacity = n;
		}
	}

	void push_back(char ch)
	{
		if (_size >= _capacity)
		{
			size_t newcapacity = _capacity == 0 ? 4 : _capacity *
				2;
			reserve(newcapacity);
		}
		_str[_size] = ch;
		++_size;
		_str[_size] = '\0';
	}

	string& operator+=(char ch)
	{
		push_back(ch);
		return *this;
	}
	const char* c_str() const
	{
		return _str;
	}

	string operator+(char ch)
	{
		string tmp(*this);
		tmp += ch;
		return tmp;
	}

	size_t size() const
	{
		return _size;
	}
private:
	char* _str = nullptr;
	size_t _size = 0;
	size_t _capacity = 0;
};

main.cpp

int main()
{
	A::string str= "hello!";

	//str为左值
	A::string s = str;

	//str + '\n'为右值
	A::string s1 = str + '\n';
	return 0;
}

输出结果:
在这里插入图片描述
发生了三次构造,一次是"hello"构造,一次是s的构造,另一次是s1的构造。

给上述代码假如移动构造。

// 移动构造
		string(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

在运行代码,结果如下:
在这里插入图片描述

1.4 右值引用意义

右值引用是个非常奇妙的想法,它可以利用临时资源,避免低效率拷贝问题。

  • 左值引用:直接引用对象资源,本身该对象资源仍然存在。
  • 右值引用:间接减少拷贝,将临时资源等将亡值的资源通过 移动构造 进行转移,减少拷贝

二. 完美转发

2.1 模版中的万能引用

泛型编程的核心在于 模版根据传入参数类型推导函数,当我们分别传入左值引用,右值引用时,模版是否能正确推导?
下面这段代码的含义是 分别传入 左值、const 左值、右值、const 右值,并设计对应参数的回调函数,将参数传给模板,看看模板是否能正确回调函数。

void func(int& a)
{
	cout << "func(int& a) 左值引用" << endl;
}

void func(const int& a)
{
	cout << "func(const int& a) const 左值引用" << endl;
}

void func(int&& a)
{
	cout << "func(int&& a) 右值引用" << endl;
}

void func(const int&& a)
{
	cout << "func(const int&& a) const 右值引用" << endl;
}

template<class T>
void perfectForward(T&& val)
{
	// 调用函数
	func(val);
}

int main()
{
	int a = 10;
	const int b = 10;

	// 左值
	perfectForward(a);
	perfectForward(b);

	// 右值
	perfectForward(move(a));
	perfectForward(move(b));

	return 0;
}

  • 输出结果:

在这里插入图片描述
在这里插入图片描述
从结果可以看出全是左值相关的func函数,模版出问题了吗???实则不然。
模版是不会出现问题的,问题必出现在模版传入参数的属性上。val不管是左值,还是右值,传给func函数后都是左值属性了,左值传给func不言而喻了,为啥右值传给func后,属性变成左值???右值引用后的变量是具有指向资源和地址的,它本质就是左值,因为右值不可以取地址,左值可以取地址。这样就说的通了。如何保持保持值得属性,不改变它,这就需要使用完美转发,完美转发本质是函数模版。即forward函数。

template<class T>
void perfectForward(T&& val)
{
	// 调用函数
	func(forward<T>(val));
}

运行结果:
在这里插入图片描述
可以发现结果与预期相同。
注意:forward为函数模版,需制定函数模版类型T,确保参数正确传递。

2.2 实际应用

完美转发实际应用中较为广泛,特别是在链表中,可以规避较多无意义的拷贝行为。
下面来看看它的实际应用。

#pragma once
#include<assert.h>

namespace A
{
	template<class T>
	struct list_node
	{
		list_node<T>* _next;
		list_node<T>* _prev;
		T _data;

		list_node(const T& x = T())
			:_next(nullptr)
			, _prev(nullptr)
			, _data(x)
		{}
	};

	template<class T, class Ref, class Ptr>
	struct __list_iterator
	{
		typedef list_node<T> node;
		typedef __list_iterator<T, Ref, Ptr> self;
		node* _node;

		__list_iterator(node* n)
			:_node(n)
		{}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &_node->_data;
		}

		self& operator++()
		{
			_node = _node->_next;

			return *this;
		}

		self operator++(int)
		{
			self tmp(*this);
			_node = _node->_next;

			return tmp;
		}

		self& operator--()
		{
			_node = _node->_prev;

			return *this;
		}

		self operator--(int)
		{
			self tmp(*this);
			_node = _node->_prev;

			return tmp;
		}

		bool operator!=(const self& s)
		{
			return _node != s._node;
		}

		bool operator==(const self& s)
		{
			return _node == s._node;
		}
	};

	template<class T>
	class list
	{
		typedef list_node<T> node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		iterator begin()
		{
			return iterator(_head->_next);
		}

		const_iterator begin() const
		{
			return const_iterator(_head->_next);
		}

		iterator end()
		{
			return iterator(_head);
		}

		const_iterator end() const
		{
			return const_iterator(_head);
		}

		void empty_init()
		{
			_head = new node(T());
			_head->_next = _head;
			_head->_prev = _head;
		}

		list()
		{
			empty_init();
		}

		template <class Iterator>
		list(Iterator first, Iterator last)
		{
			empty_init();

			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		void swap(list<T>& tmp)
		{
			std::swap(_head, tmp._head);
		}

		list(const list<T>& lt)
		{
			empty_init();

			list<T> tmp(lt.begin(), lt.end());
			swap(tmp);
		}

		list<T>& operator=(list<T> lt)
		{
			swap(lt);
			return *this;
		}

		~list()
		{
			clear();
			delete _head;
			_head = nullptr;
		}

		void clear()
		{
			iterator it = begin();
			while (it != end())
			{
				//it = erase(it);
				erase(it++);
			}
		}

		void push_back(const T& x)
		{
			insert(end(), x);
		}

		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		void pop_back()
		{
			erase(--end());
		}

		void pop_front()
		{
			erase(begin());
		}

		void insert(iterator pos, const T& x)
		{
			node* cur = pos._node;
			node* prev = cur->_prev;

			node* new_node = new node(x);

			prev->_next = new_node;
			new_node->_prev = prev;
			new_node->_next = cur;
			cur->_prev = new_node;
		}

		iterator erase(iterator pos)
		{
			assert(pos != end());

			node* prev = pos._node->_prev;
			node* next = pos._node->_next;

			prev->_next = next;
			next->_prev = prev;
			delete pos._node;

			return iterator(next);
		}
	private:
		node* _head;
	};
}

主函数中只需创建一个 list 对象,,查看 移动构造是否被正确调用

测试移动构造是否被正确调用

int main()
{
	A::list<A::string> l;
	l.push_back("Hello World!");

	return 0;
}

运行结果:为两次深拷贝
在这里插入图片描述
第一次是构造本身构造,第二次是插入时传参的构造。

// 右值引用版
void push_back(T&& x)
{
	// 完美转发
	insert(end(), std::forward<T>(x));
}

// 右值引用版
void insert(iterator pos, T&& x)
{
	node* cur = pos._node;
	node* prev = cur->_prev;

	// 完美转发
	node* new_node = new node(std::forward<T>(x));

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = cur;
	cur->_prev = new_node;
}

list_node(T&& x)
	:_next(nullptr)
	, _prev(nullptr)
	, _data(x)
{}

在这里插入图片描述
要想让我们之前模拟实现的 list 成功进行 移动构造,需要增加:一个移动构造、两个右值引用版本的函数、三次完美转发,并且整个 完美转发 的过程是层层递进、环环相扣的,但凡其中有一层没有进行 完美转发,就会导致整个传递链路失效,无法触发 移动构造。但凡任何一层有左值属性,直接诶调用拷贝构造。

三. 新增类功能

3.1 默认的移动构造和移动赋值

原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重
载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器
会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
生成条件:

  • 如果没有显示实现移动构造函数,且没有显示实现析构函数,拷贝构造函数,拷贝赋值重载任意一个。那么编译器会自动生成一个移动构造函数,默认生成的移动构造函数对于内置类型成员会逐成员按字节拷贝,自定义类型成员,看该成员是否实现移动构造,如果实现调用移动构造,没有就调用拷贝构造。
  • 如果没有显示实现移动赋值重载函数,且没有实现析构函数,拷贝构造函数,拷贝赋值重载中任意一个函数,那么编译器会自动生成一个默认移动赋值函数,默认生成的移动赋值函数对于内置类型逐成员按字节拷贝,自定义类型成员,看该成员是否实现移动赋值函数,如果实现调用移动赋值重载,没有就调用拷贝赋值。
  • 显示实现移动构造或移动赋值,编译器不再主动生成拷贝构造和拷贝赋值。

如何实现移动语义函数呢???

// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	swap(s);
}

// 移动赋值
string& operator=(string&& s)
{
	swap(s);
	return *this;
}

  • 移动语义是否能延长临时对象(将亡值)的生命周期?

移动语义本身不会直接延长临时对象(将亡值)的生命周期,但通过特定机制(如绑定到右值引用)可以间接实现生命周期的延长。

  • const引用延长生命周期问题
#include<iostream>
using namespace std;

int Add(int& x, int& y)
{
	int m = x + y;
	return m;
}
int main()
{
	int a = 10, b = 30;
	const int& ret = Add(a, b);//临时对象被绑定到了ret对象上,临时对象与ret对象共存亡。
	//被const修饰的临时对象不可修改
	//ret += 2;//error
	return 0;
}

const引用不可以延长局部对象的生命周期,局部对象在函数早就被销毁了,不可访问。

3.2 新增关键字

default 关键字

功能:显式要求编译器生成默认的特殊成员函数(如默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符等)。因为一些特殊原因导致默认函数没有生成,我们的确想要它,就可以使用default关键字,强制编译器生成。

class MyClass {
public:
    MyClass() = default; // 显式要求编译器生成默认构造函数
    MyClass(int x) {}    // 自定义构造函数
    // 编译器会自动生成析构函数、拷贝构造函数等(除非被删除)
};

delete关键字

功能:禁用类的某些特殊成员函数(如禁用拷贝构造、赋值运算符等),或禁用特定函数重载。在Linux中就见过单例类模式,只允许生成有一个类对象,此时我们需要将拷贝构造,赋值等具有生成对象功能的函数禁用,就可以使用delete关键字。

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;       // 禁用拷贝构造
    NonCopyable& operator=(const NonCopyable&) = delete; // 禁用拷贝赋值
};

// 禁止将int隐式转换为类类型
void func(double) {}
void func(int) = delete; // 调用func(1)会编译错误

额外:声明成员变量时,是可以给缺省值的,不给则是随机值。

手动简单实现一个Date类,此时成员变量没有给缺省值。

class Date
{
public:
	Date()
	{
		//cout << "Date()" << endl;
	}

	~Date()
	{
		//cout << "~Date()" << endl;
	}
	int GetYear()
	{
		return _year;
	}

private:
	int _year;
};

int main()
{
	Date d1;
	cout << "d1.GetYear() = " << " " << d1.GetYear() << endl;
	return 0;
}

输出结果:
在这里插入图片描述
可以看出是随机值。
此时我们给成员变量 _year 一个缺省值,设置为2025。再看下结果。

	int _year = 2025;

输出结果:
在这里插入图片描述
随机值的危害:导致逻辑错误代码维护性下降 等。建议:声明成员变量时,给缺省值。

四. 可变参数

可变参数允许程序员传递任意个参数,函数都可以接收和作出相应的行为。C语言的scanf,printf函数就设计可变参数,需要用户指定数据类型及参数数量,这样使用相当复杂和繁琐,假如我有100个甚至上万个参数呢,都显示写吗???显然太烦杂了,此时C++11引用可变参数模版,将上述繁琐工作交给编译器去完成。

4.1 可变参数包

求参数包个数:sizeof…(参数包名)

//sizeof...运算符去计算参数包中参数的个数
template <class ...Args>
void Print(Args&&... args)
{
	cout << sizeof...(args) << endl;
}
int main()
{
	double x = 2.2;
	Print(); // 包⾥有0个参数
	Print(1); // 包⾥有1个参数
	Print(1, string("xxxxx")); // 包⾥有2个参数
	Print(1.1, string("xxxxx"), x); // 包⾥有3个参数
	return 0;
}

在这里插入图片描述
可变参数包使用万能引用模版,既可以引用左值,又可以引用右值。

4.2 可变参数包解析

通过递归展开参数包,逐个处理每个参数,直至参数包为空时终止递归。解析可变参数包的核心方法是模板递归和折叠表达式(C++17)。


void ShowList()
{
	// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
	cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
	cout << x << " ";
	// args是N个参数的参数包
	// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
	ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}
int main()
{
	Print();
	Print(1);
	Print(1, string("xxxxx"));
	Print(1, string("xxxxx"), 2.2);
	return 0;
}

输出结果:
在这里插入图片描述
图解:
在这里插入图片描述
注意:不可以递归这样操作:

if (args[0] == 0) return;

4.3 emplace系列函数

emplace系列函数增加参数为可变参数模版的版本函数。
emplace系列函数通过完美转发(Perfect Forwarding)将参数直接传递给元素的构造函数,在容器内存中直接构造对象,而非先创建临时对象再拷贝/移动到容器中。
在这里插入图片描述

总之:emplace函数功能比push_back函数更高效。
最佳实践:合理使用emplace系列函数可显著提升性能,但需权衡代码可读性与构造参数的明确性。在元素构造简单或需兼容旧代码时,insert系列函数可能更直观。

五. 最后

本文深入解析C++11核心特性:右值引用通过移动语义减少拷贝开销,结合move实现资源所有权转移;完美转发利用模板万能引用与std::forward保留参数值类别,优化链表等场景性能;新增类功能如默认移动语义、default/delete关键字提升代码健壮性;可变参数包与emplace函数通过模板递归和折叠表达式实现类型安全的高效参数处理。这些特性共同推动C++向零开销抽象迈进,适用于高性能库开发与资源敏感场景。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值