C++11新特性的了解

C++11更新了许多新的特性,更加方便,强大,语法更加泛化,因此C++11需要重点学习。

统一的列表初始化

{}初始化

在C++98中,允许使用过 {} 来对数组或者结构体进行初始化。

#include<iostream>
#include<vector>
using namespace std;

struct ST {
	int _a;
	int _b;
};

int main()
{
	vector<int> v = { 1,2,3 };
	ST s = { 1,2 };
}

而C++11则扩大了列表初始化的范围,所有内置类型和用户自定义的类型也能够采用列表初始化了。

我们可以加 "=",也可以不加。并且我们的自定义类型也可以通过列表初始化来调用构造函数

#include<iostream>
#include<string>

using namespace std;
class Date {
public:
	Date(int x, int y, int z)
		:_x(x),_y(y),_z(z)
	{

	}
private:
	int _x;
	int _y;
	int _z;
};
int main()
{
	Date d = { 2024,1,18 };
	return 0;
}

std::initializer_list

        这个类一般作为容器的构造函数的参数,C++11中的容器的构造函数重载了以该类为参数的构造函数,这样就能够采用 {} 赋值也可以用作operator=的参数

声明

auto

        原本的auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是在局部域中定义的变量本来就是局部自动存储类型,因此auto作用不大,C++11则废除auto原来的作用,用于实现自动判断类型,不过需要显示初始化。

decltype

        该关键字可以把变量的类型声明为表达式指定的类型。

int main()
{
	int x = 1;
	double y = 1.1;
	decltype(x * y) ret1;
	char z = 'a';
	decltype(x * z) ret2;

	cout << typeid(ret1).name() << endl;
	cout << typeid(ret2).name() << endl;

	return 0;
}

nullptr

        C++中,NULL本定义为字面量0,那么NULL既表示指针常量,又表示整形常量,可能出现一些问题,因而出现了 nullptr。 

智能指针

        智能指针是一个用于防止内存泄漏的机制,通过利用类成员的生命周期来将空间释放,这里不做详细叙述。

右值引用和移动语义

        C++中本来就有引用的语义,不过C++中新增了右值引用的语义,之前学习的都是左值引用。

左值和左值引用

        左值是一个表达数据的表达式,我们可以对它进行取地址,或者对他进行赋值,它能够出现在赋值符号=的左边或右边。

        const修饰的左值不能赋值,但是可以对它取地址。

        而对这些左值的引用就是左值引用。

int main()
{
	//这些就是左值
	int a = 1;
	int* b = &a;
	const int c = 1;
	//这些就是左值引用
	int& pa = a;
	int*& pb = b;
	const int& pc = c;
	return 0;
}

右值和右值引用

        右值也是一个表达数据的表达式,比如字面常量,表达式返回值,函数返回值(不能是左值引用返回)等,不过右值只能出现在赋值符号=的右边,因此称为右值。

        为了和左值引用区分,右值引用后面有两个&&。

注:右值虽然不能取地址,但是可以通过右值引用来引用右值,再对右值引用取地址。也可以采用const 右值引用,这样能取地址但是不能修改。

不过这不是右值引用的主要特性。 

 左值引用和右值引用的比较

左值引用

  • 左值引用只能引用左值,不能引用右值。
  • const 左值引用可以引用右值。

右值引用

  • 右值引用只能引用右值,不能引用左值
  • 右值引用可以引用move后的左值
  • move是C++的函数,可以将左值强行转换为右值

左值引用的短板

左值引用能够引用左值,const 左值左值右值都能引用,那为啥有个右值引用呢?

这就是因为左值引用的短板了,为了说明这个短板,我们先构建一个类。

#define _CRT_SECURE_NO_WARNINGS

#include<iostream>
#include<assert.h>

using namespace std;
namespace test {
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			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);
		}
		// s1.swap(s2)
		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;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移动构造
		string(string&& s) noexcept
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s) noexcept
		{
			cout << "string& operator=(string&& s) -- 移动语义" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			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];
				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)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};
}

这个类中包含了深拷贝,移动构造,移动赋值等方法(移动构造和移动赋值等下说明)

我们再写两个函数,然后分别调用它们。

void f1(test::string s)
{
	
}

void f2(test::string& s)
{

}

int main()
{
	test::string s("hello world");
	cout << "这是f1" << endl;
	f1(s);
	cout << "这是f2" << endl;
	f2(s);
	s += '!';
	return 0;
}

我们发现,没有使用左值引用作为参数的函数需要调用一次深拷贝,而使用了左值引用的函数则没有调用。 

这是因为当你没有使用左值引用来调用参数时,那么编译器需要通过深拷贝来拷贝一个新的string,而通过左值引用调用参数时,这个参数的地址依旧是原来的地址,只是名称不同了,因此不用调用深拷贝,作为一个函数返回对象时也是如此。

这是左值引用的使用场景——函数参数和函数返回对象。但是当函数返回对象是一个局部对象时无法使用左值引用。

test::string to_string(int value)
{
	test::string tmp;
	//...中间过程省略
	return tmp;
}

int main()
{
	test::string s;
	s = to_string(1234);
	return 0;
}

像这种状况下,左值引用就无法减少拷贝了,如果函数返回值是一个左值引用,那么当退出函数的时候,这个局部变量就被销毁了,因此这里只能使用普通的函数返回。(这里我将移动构造都给屏蔽了)

 这个时候就轮到右值引用出场了(将移动构造和移动赋值解除屏蔽)

我们发现此时只进行了一次移动赋值,那什么是移动赋值呢?移动构造又是什么呢?

移动构造和移动赋值

移动构造:将参数的右值的资源窃取过来,占为己有,不做深拷贝,因此叫它移动构造,就是窃取别人的资源来构造自己。

移动赋值:将右值对象赋值给返回对象,再以左值引用返回。

当遇上类似状况时,采用右值引用能够进行优化。

右值引用引用左值以及一些深入场景分析

虽然右值引用只能引用右值,但是在需要时,可以引用左值来调用移动构造和移动赋值。

使用move将左值变为右值。

int main()
{
	test::string s1("hello world");
	test::string s2 = s1;
	test::string s3 = (std::move(s1));
}

比如这样,我们通过move来使用移动语义,不过需要注意的是,移动构造会将右值的资源全部给搬走,上述代码中,s1的资源就全部给s3了,s1为空了。

  • s2深拷贝,s3 移动构造。 

当然这种用法一般不怎么使用。

不过STL里面增加了很多右值引用的版本,可以使用一下。

int main()
{
	vector<string> v;
	v.push_back("1111");
	string s("1234");
	v.push_back(std::move(s));
}

 比如这些,就是使用了移动语义的版本。

int main()
{
	vector<string> v;
	v.push_back("1111");
	string s("1234");
	v.push_back(std::move(s));
}

万能引用

万能引用是一种特殊的写法,它能够同时接受左值和右值。

#include<iostream>
using namespace std;

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

//下面的模板函数就是万能引用
//它能够同时接受左值和右值
//但是它唯一的作用就是限制了接受的类型
//后续使用都退化成了左值
template<class T>
void func1(T&& t)
{
	func(t);
}

int main()
{
	int a = 1;
	func1(a);
	func1(11);
	func1(std::move(a));
	return 0;
}

不过万能引用后续都退化成了左值,因此我们需要学习一下完美转发。

退化原因: 在 func 调用中,都是将数据作为 t 来传递,而编译器会将这里识别成左值

完美转发

#include<iostream>
using namespace std;

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

//在模板函数中的传参使用forward即是完美转发
template<class T>
void func1(T&& t)
{
	func(std::forward<T>(t));
}

int main()
{
	int a = 1;
	func1(a);
	func1(11);
	func1(std::move(a));
	return 0;
}

 

完美转发必须要传递模板参数,否则会出错。

新的类成员函数

由于右值的出现,成员函数中又出现了移动赋值和移动构造。

  • 如果类成员中没有移动构造函数时, 且析构、拷贝构造、拷贝赋值重载中的任意一个都没有实现时,编译器会生成一个默认的移动构造,内置类型则按字节拷贝,自定义类型成员则看该成员内部是否实现了移动构造,实现了调用移动构造,没实现就调用拷贝构造。
  • 如果类成员没有移动赋值运算符重载时,且析构、拷贝构造、拷贝赋值重载中的任意一个都没有实现时,后面就和移动构造函数一样了
  • 如果实现了就不会生成默认的函数了

强制生成默认函数default

C++提供了新的关键字default,可以强制生成默认函数。

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

这里就强制生成了移动构造。

禁止生成默认函数delete

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) = delete;
private:
 bit::string _name;
 int _age;
};

这里就禁止生成移动构造了。

可变参数模板

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

这个args是一个可变模板参数,带省略号的参数称为参数包,它包含了0~n个参数,不能直接获取参数,只能通过展开参数包来获取参数。

递归获取参数

#include<iostream>
using namespace std;

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

递归获取参数必须要有一个递归终止参数,当参数包只剩一个参数时,它就会调用这个终止参数,来终止递归。

逗号表达式获取参数

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;
}

通过逗号表达式获取参数无需终止函数,PrintArg实际上是处理参数的函数。

这里是通过创建一个数组 arr,并且通过C++的初始化列表,创建了一个变长数组,数组长度是 sizeof args ,数组内容是 (PrintArg(args1),0), (PrintArg(args2),0)...直到参数包的最后一个参数。

这里的逗号表达式会先调用 PrintArg(args),然后获取表达式结果0,也就是说arr实际上是一个长度为 sizeof(args) 的,内容全为0的数组,这个数组的目的是为了在创建数组的时候展开参数包。

lambada表达式

在以往,我们可以通过sort来构造顺序函数,但是如果数组类型是自定义类型就需要自己定义一个新的比较函数,这样比较麻烦,因此C++11提供了lambada表达式,用来简化操作。

#include<string>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct Goods
{
	string _name;
	double _price;
	Goods(string name, double price)
		:_name(name)
		,_price(price)
	{
	}
};

int main()
{
	vector<Goods> a = { {"pineapple",8} ,{"apple",10},{"banana",12} };
	sort(a.begin(), a.end(), [&](const Goods a, const Goods b) {
		return a._name < b._name;
	});
	for (auto t : a)
	{
		cout << t._name << " : " << t._price << endl;
	}
	sort(a.begin(), a.end(), [&](const Goods a, const Goods b) {
		return a._price < b._price;
		});
	for (auto t : a)
	{
		cout << t._name << " : " << t._price << endl;
	}
}

 

sort的第三个参数就是一个lambada表达式。

语法

[capture-list](paremeters)mutable->returntype{statement} 

  • capture-list:捕捉列表,编译器根据 [] 来判断接下来是否是lambada函数,能够捕捉上下文的变量供lambada使用。
  • paremeters: 参数列表,和普通的函数参数传递一样,不需要可以不传。
  • mutable : 默认情况下,lambda 是一个const 函数,mutable 可以取消其常量性,使用mutable时,参数列表不可为空。
  • ->returntype :返回值类型,可以由编译器推断。
  • {statement} :函数体,可以使用捕捉的变量和其参数
  • 注意:在lambada 中,参数列表和返回类型都是可选部分,而捕捉列表和函数体可以为空。最简单的lambada函数为 [] {}。

 了解了 lambada 表达式的语法后,还有几点需要说明。

捕捉列表的使用

  • [var] : 用值传递的方式捕捉变量 var
  • [=] : 用值传递的方式捕捉父作用域的所有变量,包括this指针。
  • [&var]:用引用的方式捕捉变量var
  • [&] : 用引用传递捕捉父作用域的所有变量,包括this
  • [this]: 用值传递的方式捕捉this指针

注意事项

  • 其中,父作用域指的是包含lambada表达式的语句块。
  • 而且捕捉列表可以由多个捕捉项一起使用,用逗号分隔:[a,this]之类。
  • 但是捕捉列表不可重复,否则会编译错误,如[a,=]这种。
  • 块作用域之外的lambada的捕捉列表必须为空。
  • 在块作用域之内的lambada只能捕捉父作用域的局部变量,捕捉其他变量会报错。
  • lambada表达式之间不可相互赋值。

函数对象和lambada

函数对象的使用方式和lambada几乎一样,实际上底层对于lambada表达式的处理方式和函数对象的处理方式都是一样的。

#include<string>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct Goods
{
	string _name;
	double _price;
	Goods(string name, double price)
		:_name(name)
		,_price(price)
	{
	}
};

int main()
{
	vector<Goods> a = { {"pineapple",8} ,{"apple",10},{"banana",12} };
	
	auto t1 = [&](const Goods a, const Goods b) {
		return a._name < b._name;
		};
	sort(a.begin(), a.end(),t1);
	for (auto t : a)
	{
		cout << t._name << " : " << t._price << endl;
	}
	auto t2 = [&](const Goods a, const Goods b) {
		return a._price < b._price;
		};
	sort(a.begin(), a.end(),t2);
	for (auto t : a)
	{
		cout << t._name << " : " << t._price << endl;
	}
}

包装器(function)

包装器可以将函数包装起来,更方便的使用。

我们先来看看这个场景。

template<class F, class T>
T useF(F f, T x)
{
 static int count = 0;
 cout << "count:" << ++count << endl;
 cout << "count:" << &count << endl;
 return f(x);
}
double f(double i)
{
 return i / 2;
}
struct Functor
{
 double operator()(double d)
 {
 return d / 3;
 }
};
int main()
{
 cout << useF(f, 11.11) << endl;
 // 函数对象
 cout << useF(Functor(), 11.11) << endl;
 // lamber表达式
 cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
 return 0;
}

我们发现,模板函数实例化了三份。

但是采用function就不会。

#include<iostream>
#include<functional>
using namespace std;

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;
	return f(x);
}
double f(double i)
{
	return i / 2;
}
struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};
int main()
{
	std::function<double(double)> f1 = f;
	// 函数名
	cout << useF(f1, 11.11) << endl;
	// 函数对象
	std::function<double(double)> f2 = Functor();
	cout << useF(f2, 11.11) << endl;
	// lamber表达式
	std::function<double(double)> f3 = [](double d)->double { return d / 4; };
	cout << useF(f3, 11.11) << endl;
	return 0;
}

我们发现function调用函数只实例化了一份。

 bind函数

bind函数是一个函数模板,它就像一个函数包装器,生成一个可调用对象来适应原对象的参数列表。

我们可以通过bind来把一个接受N个参数的函数fn,通过绑定参数,返回一个接受M个参数的新参数(M可以大于N,不过一般都是小于N,因为大于N没有意义)。

#include<iostream>
#include<functional>
using namespace std;

int sub(int a, int b)
{
	return a - b;
}
int main()
{
	function<int(int,int)> f = bind(sub,std::placeholders::_1,std::placeholders::_2);
	cout<<f(2,1);
	return 0;
}

使用bind绑定的函数的参数我们可以通过 std::placeholder::_n 来指定该位置的参数应该是原参数的什么位置。

#include<iostream>
#include<functional>
using namespace std;

int sub(int a, int b)
{
	return a - b;
}
int main()
{
	function<int(int,int)> f = bind(sub,std::placeholders::_2,std::placeholders::_1);
	cout<<f(2,1);
	return 0;
}

 比如这里我将 _2 和 _1 位置调换,结果就不同了。

线程库

C++11提供了线程库,供用户使用。 

函数名功能
thread()创造一个线程对象,没有关联任何函数,也就没有启动任何线程
thread(fn,arg1,arg2,...)构造一个线程对象,关联函数fn,参数为arg1,arg2...
get_id()获取线程对象的id
joinable()查看某个线程是否还在运行
join()该函数调用后会阻塞线程,当线程结束后,主线程继续执行
detach()分离线程与线程对象,分离的线程变为后台线程

注意点:

  • 通过线程对象可以控制一个线程或者获取线程状态。
  • 创建的线程对象有关联线程函数后,该线程就被启动,与主线程一起运行(函数可以是函数指针,函数对象或者lambada表达式)
  • thread类防拷贝以及赋值,但是可以移动构造和移动赋值
  • 可以通过joinable()查看线程是否有效,若线程对象是无参构造函数构造的,或者线程对象的状态转移给其他线程对象,或者线程已经调用join或detach结束了,就是无效的。

 总结

C++11提供了许多强大的功能,并且在C++98的基础上完善了许多。

新增的右值引用提高了C++的效率,lambada也方便了用户使用 algorithm 的函数,十分值得学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值