首页 > 编程语言 >深入探索 C++11 第一弹:现代 C++ 编程的基石与革新

深入探索 C++11 第一弹:现代 C++ 编程的基石与革新

时间:2024-11-15 14:44:42浏览次数:3  
标签:11 string 右值 int 编程 左值 C++ 引用 str

1、C++的发展历史

C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。C++11 对 C++ 语言的发展具有深远的影响,它使 C++ 语言更加现代化、高效、灵活和易于使用,为开发者提供了更强大的工具和更好的编程体验,推动了 C++ 在各个领域的广泛应用和持续发展。话不多说,下面我们具体为大家分析C++11都诞生了什么新鲜玩意。

2、列表初始化

<1>、C++98中传统的{ }

C++98中我们常用{ }来初始化结构体和数组。

struct Student
{
	string name;
	int age;
	char gender;
	string tele;
};

void Test()
{
	int arr[] = { 1,2,3,4,5 };
	Student s = { "张三",18,'男',"113565587" };
}

<2>、C++11中的{ }

C++11后,意在统一初始化方式,做到一切对象都可以用{ }来初始化,也叫做列表初始化内置类型支持,自定义类型也支持。代码中出现的移动构造我们后面会为大家仔细讲解

class Date
{
public:

	Date() = default;
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "构造函数:Date()" << endl;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	~Date()
	{
		cout << "析构函数:~Date()" << endl;
	}
	Date(const Date& d1)
	{
		_year = d1._year;
		_month = d1._month;
		_day = d1._day;
		cout << "拷贝构造:Date(const Date& d1)" << endl;
	}
	//右值引用 移动构造 下面会讲到
	Date(Date&& d2)
	{
		std::swap(_year, d2._year);
		std::swap(_month, d2._month);
		std::swap(_day, d2._day);
		cout << "移动构造:Date(Date&& d2)" << endl;
	}
private:
	int _year = 2024;
	int _month = 11;
	int _day = 11;
};

//C++11的列表初始化
void Test()
{
	//内置类型支持
	int a = { 1 };

	//自定义类型也支持本质上产生了临时对象 但编译器优化为直接构造,调用构造函数,后面我们都会讲到
	Date d1 = { 2024,11,12 };

	//这里d2引用的是临时变量,临时变量具有常性,需要加上const修饰
	 const Date& d2 = { 2024,11,13 };
}

从运行结果可以看出,d1和d2都是通过直接调用构造函数来初始化的。

C++11中使用{ }初始化时还可以省去=,这样写也是可以的

void Test()
{
	 //C++11使用{}构造时还可以省去=
	 int b{ 1 };
	 Student s{ "李四",19,'男',"129997564" };
	 Date d3{ 2024,11,14 };
	 const Date& d4{ 2024,11,15 };
}

使用{ }初始化在函数传参时会十分方便,编译器会走隐式类型转换。

void Test()
{
	vector<Date> v;
	Date d = { 2024,11,12 };
	//有名对象传参
	v.push_back(d);
	//匿名对象传参
	v.push_back(Date(2024, 11, 13));
	//直接使用{}传参
	v.push_back({ 2024,11,14 });
}

<3> C++11中的std::initializer_list

C++11库中提出了⼀个std::initializer_list的类,这个类的本质是底层开⼀个数组,将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。std::initializer_list支持迭代器遍历。

STL标准库中的容器支持⼀个用std::initializer_list的构造函数,就可以直接使用{ }进行构造。 下面我们来看一下initializer_list构造的使用
void Test_initializer_list()
{
	//v1是直接调用vector的initializer_list构造
	vector<int> v1({ 1,2,3,4,5 });

	//v2是先用initializer_list构造出临时对象,再用临时对象拷贝构造v2
	//编译器优化为直接构造
	vector<int> v2 = { 1,2,3,4,5 };
	
	//这里是pair<string,string>的{}初始化和map的initializer_list结合
	map<string, string> m = { {"apple","苹果"},{"hello","你好"},{"world","世界"} };

	//同时容器的赋值也支持initializer_list
	v1 = { 2,3,4,5,6 };
}

3、右值引用

<1>、左值和右值

左值是指表达式结束后依然存在的持久对象,存储在内存中,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。简单来说左值可以取地址&。

void Test()
{
	//指针是左值
	int* p = new int(1);
	//普通变量是左值
	int b = 2;
	//const修饰的变量同时是左值,不能被修改,但是可以取地址
	const int c = 5;
	//指针解引用得到的对象也是左值
	*p = 10;
	//string 对象是左值
	string s("helloworld");
	//字符串中的某个元素也是左值
	s[0] = 'a';
}

这些左值都可以取地址

右值是指表达式结束后就不再存在的临时对象,它没有持久的存储位置,通常只能出现在赋值语句的右边。右值一般是字面常量、临时对象或表达式求值产生的临时结果等,简单来说,右值无法取地址&。

void Test()
{
	double x = 1.5, y = 2.5;
	//字面量常量是右值
	10;
	//表达式的结果是右值
	x + y;
	//函数返回的临时对象是右值
	fmin(x, y);
	//构造的临时对象是右值
	string("helloworld");
}

这些右值都无法取地址,&操作符要求对象必须为左值!

<2>、左值引用和右值引用

我们都知道C++的特性之一就是有引用,本质就是给变量取别名,那么对左值的引用就是左值引用,用&来表示,我们之前见到的引用都是左值引用。

void Test()
{
	int* p = new int(1);
	int b = 2;
	const int c = 5;
	*p = 10;
	string s("helloworld");
	s[5] = 'a';

	//对左值的引用就是左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& r = *p;
	string& rs = s;
	char& rs5 = s[5];
}

那么同理,对右值的引用就叫做右值引用。用&&来表示。

void Test()
{
	double x = 1.5, y = 2.5;
	10;
	x + y;
	fmin(x, y);
	string("helloworld");

	//对右值的引用叫做右值引用
	int&& ra = 10;
	double&& rb = x + y;
	double&& rc = fmin(x , y);
	string&& rs = "helloworld";
}
左值引用不能直接引用右值,但是const左值引用可以引用右值 右值引用不能直接引用左值,但是右值引用可以引用move(左值) move是C++11提出的函数模板,可以将左值转换成右值,本质就是强制类型转换,还涉及一些引用折叠的知识,后面我们会讲到。
void Test()
{

	// 左值引用不能直接引用右值,但是const左值引用可以引用右值
	const int& rx1 = 10;
	const double& rx2 = x + y;
	const double& rx3 = fmin(x, y);
	const string& rx4 = string("helloworld");
	// 右值引用不能直接引用左值,但是右值引用可以引用move(左值)
	int&& rrx1 = move(b);
	int*&& rrx2 = move(p);
	int&& rrx3 = move(*p);
	string&& rrx4 = move(s);
	string&& rrx5 = (string&&)s;
}

<3>引用延长生命周期

右值引用可用于为临时对象延长生命周期,const 的左值引用也能延长临时对象生命周期期,但这些对象无法被修改。
void Test()
{
	//临时对象,生命周期只在当前行,程序执行过后就会销毁
	string ("helloworld");

	//右值引用临时对象,延长其生命周期
	string&& rs = "helloworld";

	string s1("hello");
	string s2("world");
	//const 左值引用 引用临时对象s1+s2 延长生命周期 但是不能修改
	const string& s3 = s1 + s2;
    //右值引用延长生命周期可以修改
	string&& s4 = s1 + s2;
	//s3[5] = 'x';  会报错,无法修改
	s4[5] = 'x';  //可以修改
}

<4>、左值和右值的参数匹配问题

在C++98中,我们使用const 左值引用作为函数的形参,那么实参既可以传递左值,也可以传递右值。 在C++11中实参左值会匹配左值引用,实参为const 左值会匹配const 左值引用,实参为右值则会匹配右值引用 这里我们需要注意, 右值引用变量在用于表达式时的属性是左值,为什么会这样设计?大家可能会匪夷所思,为什么右值引用的属性却是左值?在下面的右值引用的使用场景上我们解释这样设计的原因。
void Test_func()
{
	int i = 10;
	const int ci = 20;
	func(i); // 左值 调用 func(int&)
	func(ci); // const左值 调用func(const int&)
	func(30); // 右值调用 func(int&&),如果没有 func(int&&) 则会调用func(const int&)
	func(move(i)); // move后的左值属性变为右值 调⽤ func(int&&)

	// 右值引用变量在用于表达式时是左值
	int&& j = 1;
	func(j); // j是右值引用,但是j的属性是左值 调用 func(int& x)
	func(move(j)); // 被move改为右值 调用func(int&& x)
}

很显然我们的判断确实是正确的 ! !

4、右值引用和移动语义

<1>、左值引用的缺陷——传值返回拷贝问题

左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实 参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是还是存在一些场景无法解决,这里简单模拟实现了一个使用字符串进行数字加减的函数, 无法使用左值引用返回,因为str是一个局部对象,函数调用结束后,会随着函数栈帧一起销毁。只能传值返回进行拷贝。
//传值返回需要拷贝
string addStrings(string num1, string num2) 
{
    string str;
    int end1 = num1.size()-1, end2 = num2.size()-1;
    // 进位
    int next = 0;
    while(end1 >= 0 || end2 >= 0)
    {
        int val1 = end1 >= 0 ? num1[end1--]-'0' : 0;
        int val2 = end2 >= 0 ? num2[end2--]-'0' : 0;
        int ret = val1 + val2+next;
        next = ret / 10;
        ret = ret % 10;
        str += ('0'+ret);
    }
    if(next == 1)
        str += '1';
    reverse(str.begin(), str.end());
    return str;
}

有同学会说:拷贝一个string类型代价也不是很大呀。 但是如果函数返回的是一个二维数组呢,这样的话传值返回拷贝的代价是不是就太大了。这里还只是个整形数组,那如果里面存储的是自定义类型,这样的话拷贝的代价就更大了。那么这里使用右值引用是否可以解决拷贝问题呢?很显然也是不行的,右值引用也无法解决函数栈帧销毁,临时变量也随之销毁的问题。

//传值返回需要拷贝整个二维数组
vector<vector<int>> generate(int numRows)
{
	vector<vector<int>> vv(numRows);
	for (int i = 0; i < numRows; ++i)
	{
		vv[i].resize(i + 1, 1);
	}
	for (int i = 2; i < numRows; ++i)
	{
		for (int j = 1; j < i; ++j)
		{
			vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
		}
	}
	return vv;
}

<2>、C++98和输出型参数

C++98解决函数返回拷贝问题的方法是输出型参数。即在函数的形参列表加上需要返回类型的引用对象,在函数外部创建好对象,以左值引用的方式传入,在函数内部修改,最后作为返回值返回,因为传参的是引用,确实解决了拷贝的问题,但是这样的使用还是让我们很不舒服。

vector<vector<int>> generate(int numRows,vector<vector<int>>& vv)
{
	vector<vector<int>> vv(numRows);
	for (int i = 0; i < numRows; ++i)
	{
		vv[i].resize(i + 1, 1);
	}
	for (int i = 2; i < numRows; ++i)
	{
		for (int j = 1; j < i; ++j)
		{
			vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
		}
	}
	return vv;
}

<3>、C++11的移动构造和移动赋值

移动构造是⼀种构造函数,类似拷贝构造,移动构造函数要求参数是该类类型的右值引用, 移动构造不同于拷贝构造是将一个对象的资源 “移动” 而不是 “复制” 到另一个正在构造的对象中。这样可以避免深拷贝带来的性能开销。 移动赋值是⼀个赋值运算符的重载,跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函 数要求第⼀个参数是该类类型的右值引用。与移动构造类似,它避免了不必要的资源复制,而是直接转移资源的所有权,从而提高赋值操作的效率下面我们模拟一个string类,并实行移动构造和移动赋值。
namespace zwy
{
	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)
		{
			std::swap(_str, s._str);
			std::swap(_size, s._size);
			std::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(string&& s)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

        //拷贝赋值
		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& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			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;
		}
		size_t size() const
		{
			return _size;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

对于需要深拷贝的类型,比如string,移动构造和移动赋值可以提高效率,降低不必要的拷贝带来的开销。本质就是对于右值对象,因为生命周期短,出作用域就要销毁,就可以直接转移其资源,减少拷贝,提高效率。

<4>、具体使用场景

移动构造的使用

解决传值返回拷贝问题

还是我们之前写过的字符串相加的addStrings函数的传值返回问题

string addStrings(string num1, string num2)
	{
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		// 进位
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}
		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		return str;
	}

void Test()
{
	zwy::string ret = zwy::addStrings("123", "456");
}

这里我们addString函数返回的是个临时对象,临时对象属于右值,所以这里会调用zwy::string的移动构造来构ret对象。这里不同的编译器由于优化程度不同,实际运行结果也有差异,下面我们以vs2019和vs2022的实际运行结果给大家分析:

vs2019debug: 优化力度较小

我们根据程序的运行结果来分析,这里我使用的编译器是vs2019  前两个构造函数是分别用 "123" 和 "456"来构造zwy::string对象作为函数的实参,第三个构造函数是函数内部的临时对象string str的构造,第四个移动构造就是用str是右值作为移动构造的参数来构造ret。 vs2022debug和vs2019release:优化力度大 vs2022的前两个构造与vs2019相同都是对实参string对象的构造,不同的是vs2022识别到str是函数内部的临时对象,会作为右值移动构造ret  直接合二为一 优化为一个对象,在函数内部直接构造。底层是str作为ret的引用用指针的方式实现。 最后再给大家看下在Linux下关闭所有优化的场景 在Linux平台下可以使用 g++ test.cpp -fno-elide-constructors -std=c++11命令让编译器关闭所有优化Linux:无任何优化 这里就是关闭编译器的所有优化,与vs2019有所不同的是,前两个函数实参的直接构造,也被分为构造string对象,在移动构造函数实参,后面构造str  用str移动构造临时对象,str析构,再用临时对象移动构造ret。 

通过右值引用实现移动构造,我们就可以解决传值返回的深拷贝问题,移动构造的代价很小,不需要对数据进行拷贝,只是资源的转移,相比于拷贝构造,可以减少不必要的拷贝开销,能够很大的提高效率。

移动赋值的使用
	string addStrings(string num1, string num2)
	{
		string str;
		int end1 = num1.size() - 1, end2 = num2.size() - 1;
		// 进位
		int next = 0;
		while (end1 >= 0 || end2 >= 0)
		{
			int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
			int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
			int ret = val1 + val2 + next;
			next = ret / 10;
			ret = ret % 10;
			str += ('0' + ret);
		}
		if (next == 1)
			str += '1';
		reverse(str.begin(), str.end());
		return str;
	}

void Test()
{
	zwy::string ret;
	ret= zwy::addStrings("123", "456");
}

vs2019debug: 优化力度小

vs2022debug和vs2019release :优化力度大

Linux下无任何优化:

以上是关于移动赋值的解析,与移动构造大致相同,移动赋值也是实现资源的转移,相比于拷贝赋值效率上有很大提高,图片上解析十分清晰,我就不再过多赘述。

<5>总结-——未完待续

这篇文章为大家介绍了C++11中重点是关于右值引用的部分,以及移动构造和移动赋值在解决函数传值返回需要拷贝的问题,关于C++11的更多新特性,我会在后面的文章中给大家带来更详细的解析。

感谢各位大佬的观看,如果觉得对你有帮助的话,点赞+收藏支持下,关注博主,为你带来更多高质量的内容!欢迎大佬评论区指点纠正.

标签:11,string,右值,int,编程,左值,C++,引用,str
From: https://blog.csdn.net/bite_zwy/article/details/143723070

相关文章

  • 第16章 Shell企业编程基础
    说到Shell编程,很多从事Linux运维工作的朋友都不陌生,都对Shell有基本的了解,初学者可能刚开始接触Shell的时候,有各种想法,感觉编程非常困难,SHELL编程是所有编程语言中最容易上手,最容易学习的编程脚本语言。本章向读者介绍Shell编程入门、Shell编程变量、If、While、For、Case......
  • 1159. 市场分析 II
    目录题目链接(无VIP请直接看下面的需求)题目和题目代码1.读题(建议使用这种表结构和数据对比看阅读)2.答案代码以及图表解释题目链接(无VIP请直接看下面的需求)链接:15分钟没思路建议直接看答案题目和题目代码表:Users+----------------+---------+|Colu......
  • 仓颉原生应用编程语言教程(第5期)
    泛型视频:KCKCJY在现代软件开发中,泛型编程已成为提高代码质量、复用性和灵活性的关键技术。泛型作为一种参数化多态技术,允许开发者在定义类型或函数时使用类型作为参数,从而创建可适用于多种数据类型的通用代码结构。泛型带来的好处包括:代码复用:能够定义可操作多种类型的通用算法......
  • 11.15
    实验二:逻辑回归算法实现与测试 一、实验目的深入理解对数几率回归(即逻辑回归的)的算法原理,能够使用Python语言实现对数几率回归的训练与测试,并且使用五折交叉验证算法进行模型训练与评估。 二、实验内容(1)从scikit-learn库中加载iris数据集,使用留出法留出1/3的样......
  • 11/15
    #include<stdio.h>intmain(){ intN,i,j,M,count; unsignedintarr[1000],times[10]={0},maxvalue[10]; scanf("%d",&N); for(i=0;i<N;i++){ scanf("%d",&arr[i]); }// times[10]={0}; for(i=0;i<N;i++){ ......
  • c11智能指针
      普通指针的不足new和new[]的内存需要用delete和deletel]释放。程序员的主观失误,忘了或漏了释放。程序员也不确定何时释放。普通指针的释放类内的指针,在析构函数中释放。C++内置数据类型,如何释放?new出来的类,本身如何释放?C++11新增三个智能指针类型uniqu......
  • 打卡信奥刷题(239)用C++工具信奥P1866 [普及组/提高] 编号
    编号题目描述太郎有NNN只兔子,现在为了方便识别它们,太郎要给他们编号。兔子们向太郎表达了它们对号码的喜好,每个兔子i......
  • YOLOv11改进,YOLOv11结合DynamicConv(动态卷积),CVPR2024,二次创新C3k2结构
    摘要大规模视觉预训练显著提高了大规模视觉模型的性能。现有的低FLOPs模型无法从大规模预训练中受益。在本文中,作者提出了一种新的设计原则,称为ParameterNet,旨在通过最小化FLOPs的增加来增加大规模视觉预训练模型中的参数数量。利用DynamicConv动态卷积将额外的参......
  • 第21课-C++[set和map学习和使用]
    ......
  • ssm118亿互游在线平台设计与开发+vue(论文+源码)_kaic
    毕业设计(论文)  亿互游在线平台的设计与开发学生姓名   XXX                        学    号   XXXXXXXX          分院名称   XXXXXXXX          专业班级   XXXXX   ......