欢迎回到现代C++

现在C++的岗位几乎都要求会使用C++11以后的标准,正好微软官方有一章就是讲的“Welcome Back to C++ (Modern C++)”,我这里主要在内存方面介绍下。具体请看看官方文档:https://docs.microsoft.com/en-us/cpp/cpp/welcome-back-to-cpp-modern-cpp?view=vs-2019

先来个总纲:

Modern C++ emphasizes:

  • Stack-based scope instead of heap or static global scope.

  • Auto type inference instead of explicit type names.

  • Smart pointers instead of raw pointers.

  • std::string and std::wstring types (see <string>) instead of raw char[] arrays.

  • C++ Standard Library containers like vectorlist, and map instead of raw arrays or custom containers. See <vector><list>, and <map>.

  • C++ Standard Library algorithms instead of manually coded ones.

  • Exceptions, to report and handle error conditions.

  • Lock-free inter-thread communication using C++ Standard Library std::atomic<> (see <atomic>) instead of other inter-thread communication mechanisms.

  • Inline lambda functions instead of small functions implemented separately.

  • Range-based for loops to write more robust loops that work with arrays, C++ Standard Library containers, and Windows Runtime collections in the form for ( for-range-declaration : expression ). This is part of the Core Language support. For more information, see Range-based for Statement (C++).

1. 基于栈的内存管理,而非基于堆或者静态全局的内存管理。

先说下由于我有嵌入式开发经历,所以之前的文章里都是属于基于堆或者静态全局的内存管理。

基于堆就是用指针操作new或者malloc的内存。优点:快,没有多余代码。缺点:需要自己管理内存的释放,以及对指针操作的不安全因素。

静态全局就是在某个cpp文件下声明一个静态的全局内存。优点:快,不需管理释放。缺点:无效占用,即便没使用的时候也会占用内存。

现代C++推荐的是基于栈的内存管理。栈内存的好处是什么?比如每个函数里面的局部变量,进入函数时一个个入栈,函数结束时一个个出栈,是一种安全,自动化的内存操作。但是栈并不是无限大的,所以比较大的内存一般都是放在堆上,比如保存一张图片的数据。而堆的缺点就是需要自己管理释放,这个恰恰是很多新手玩不转的东西。如果能让堆上的内存和栈的变量一样自动弹出释放,那就是新手也可以写出安全的内存代码。

基于栈的内存管理大概就是这样的操作:申请数据内存的同时,再申请一个RefCount(统计引用计数的内存),然后把两块内存的指针打包到一个只能用栈申请的数据结构里,称这个结构为Content。栈上每增加一个Content,引用计数就增加一个,每销毁一个就减一个计数,计数为0时销毁Content指向的数据内存和引用计数内存。这样堆上的内存就被栈管理了,同时也增加额外的结构。

先看看以上这三种内存管理方式在代码里的形式:

static char sName[256] = "Static Global Name"; //静态全局,该内存只能访问,全局存在
char* GetSName()
{
	return sName;
}
char* GetHeapName()
{
	const char* heapname = "Heap Name";
	char* hName = new char[256];//堆上内存,通过指针传递给外面使用
	strcpy_s(hName,256, heapname);
	return hName;
}
std::shared_ptr<char> GetRefName()
{
	const char* crefname = "Ref Name";
	char* temp = new char[256];
	std::shared_ptr<char> refname(temp);//这里本来应该用std::string,用这个只是用来和上面进行对比,理解栈上内存管理
	strcpy_s(&(*refname), 256, crefname);//同样,&(*)是为了强制使用char*指针,
	return refname;
}
#define FastPrint(p) printf("%s %s\n",#p,p)
void RefCountTest()
{
	char* sname = GetSName();
	char* hname = GetHeapName(); //这个char*指针是需要用户来维护的,如果项目很大,这个指针会被多个模块使用,模块之间如何协调
							     //指针内存的释放是个大问题
	//char* rname = &(*GetRefName()); //举个例子,return的临时变量会立刻出栈,所以获取的字符串指针是错误的
	std::shared_ptr<char> temp= GetRefName();//这个才是正确做法
	char* rname = &(*temp);//这里也是强制使用char*指针,不是好的用法
	FastPrint(sname);
	FastPrint(hname);
	FastPrint(rname);
}

注意上面的shared_ptr就是STL库提供的一个基于栈的内存管理,也就是上面提到的Content。千万别去new 这个Content。

2.如何理解基于栈的内存管理

a.一种类设计中的低级错误

先看一段新手在自己写的类中最容易出现的内存管理错误:

class CErr
{
public:
	CErr(int _size)
		:Size(_size)
	{
		printf("CErr Constructor %p\n", this);
		Buf = new char[Size];
	}
	~CErr()
	{
		printf("CErr DeConstructor %p\n", this);
		if (Buf)
		{
			delete[]Buf;
		}
	}
	char* Buf;
	int Size;
};

CErr GetErr()
{
	CErr test(256);
	strcpy_s(test.Buf,256, "Data Copy Test");
	return test;
}

void ErrorTest()
{
	CErr a= GetErr();
	printf("A:%s\n", a.Buf);
}

上面的例子中,写了一个CErr类来管理一块字符串的空间,构造的时候去创建内存,析构的时候释放内存,感觉好像没啥问题。但是一用就会崩溃。

在GetErr这个函数里面用test的临时变量申请的char数组,在退出函数的时候就会调用析构被释放掉。外部ErrorTest里面的 a变量虽然进行了Copy Constructor,但是这时内存已经释放了。函数ErrorTest结束后,会弹出栈上内存也就是a变量,这时在析构函数里会重复释放这块char数组。

如上所示,这么简单的一个类都会出这么大的问题。所以现代C++才会强调使用智能指针来进行堆上内存管理。

之所以会写出这种错误的类,主要是新手程序员缺失了一个重要的知识点:Copy Constructor和Copy Assignment,顺便一提的还有Move Constructor和Move Assignment。下面就讲讲这部分细节。

b. Copy和Move,以及LReference 和RReference

先解释下这几个词。Copy Constructor 拷贝构造,Copy Assignment 拷贝赋值。Move就是对应移动构造,移动赋值。LReference是左值引用,即&。RReference是右值引用,即&&。一个类只用拷贝就能正常工作,移动是因为有些情况下编译器不得已会产生一些临时对象。

关于临时对象和右值引用下面会带着讲一些,具体请看链接:

临时对象 https://docs.microsoft.com/en-us/cpp/cpp/temporary-objects?view=vs-2019 

引用https://docs.microsoft.com/en-us/cpp/cpp/references-cpp?view=vs-2019

下面直接看代码理解吧:

//A() ,A(const A&), ~A(), A& operator=(const A& a)这四个是编译器会默认生成的,一般都需要程序员自己来重写
//拷贝还是移动取决于传参是左值引用还是右值引用。
//但是如果没有对移动的实现,则都会使用拷贝
//其中std::move可以强制变成右值引用
//其他则看传参是否为一个临时对象由编译器决定
class A
{
public:
	A() { printf("Default Constructor %p\n", this); }
	A(const A& a) { printf("Copy Constructor %p a:%p\n", this, &a); }
	A(const A&& a)noexcept { printf("Move Constructor %p a:%p\n", this, &a); }
	A& operator=(const A& a) { printf("Copy Assignment %p a:%p\n", this, &a); return *this; }
	A& operator=(const A&& a)noexcept { printf("Move Assignment %p a:%p\n", this, &a); return *this; }
	~A()
	{
		printf("Decontructor %p\n", this);
	}
};

void PrintLine()
{
	static int index = 0;
	printf("=====================%02d===================\n", index++);
}

//先是在函数内的默认构造
//然后return的时候再进行移动构造得到return的临时对象
//最后函数外的赋值=号,调用的移动赋值
A GetA1()
{
	A temp;
	return temp;
	//如果return A(),那么A()构造的也是一个匿名的临时对象
	//则上面的移动构造就不需要了,直接用这个临时对象进行移动复制
}

//传参时先进行拷贝构造得到 a
//return的时候再进行移动构造得到临时对象
//最后还有外部对return的临时对象的移动赋值
A GetA3(A a)
{
	return a;
}
//传参时的拷贝构造被左值引用给取消了,同时也说明a不是一个临时变量
//只剩下对return的临时对象的拷贝构造
//最后同样外部=号的移动赋值
A GetA4(A& a)
{
	return a;
}
//这个只有一个拷贝赋值
A& GetA5(A& a)
{
	return a;
}
//这个是错误示范:先用拷贝构造出a1
//然后return这句前a1出栈 析构
//最后用已经析构的a1与外部进行拷贝赋值。
//注意这里不是移动赋值,因为return的对象是引用而非匿名对象
A& GetA6(const A& a)
{
	A a1(a);
	return a1;
}
//先拷贝构造出a1
//然后移动构造出return的临时对象
//最后return的临时对象和外部进行移动赋值
A GetA7(const A& a)
{
	A a1(a);
	return a1;
}

void ConstrcutorTest()
{
	A a;//默认构造
	PrintLine();
	A a1 = a;//拷贝构造
	PrintLine();
	A a2(a);//拷贝构造
	PrintLine();

	a1 = GetA1();//移动赋值
	PrintLine();

	a1 = a;//拷贝赋值
	PrintLine();
	a1 = std::move(a);//强行移动赋值
	PrintLine();

	a2 = GetA3(a);
	PrintLine();
	a2 = GetA4(a);
	PrintLine();
	a2 = GetA5(a);
	PrintLine();
	a2 = GetA6(a);
	PrintLine();
	a2 = GetA7(a);
	PrintLine();
	((A*)NULL)->operator=(a);//和别人说这个不能编译通过,结果可以的
}

假设你看懂了上面的代码,实际上就知道了我a.中设计的类没有拷贝构造和拷贝赋值的相关操作,所以造成了内存管理异常。

c.正确的类

class RefName
{
public:
	RefName()
	{
		printf("Default Constructor %p\n", this);
		MemCreate();
		const char* refname = "RefCount Name";
		strcpy_s(Buf, 256, refname);
	}

	~RefName()
	{
		printf("Decontructor %p\n", this);
		DecreseRef();

	}
	RefName(const RefName& other)
		:RefCount(nullptr)
	{
		printf("Copy Constructor %p other:%p\n", this, &other);
		CopyAll(other);
	}
	RefName(const RefName&& other) noexcept
	{
		printf("Move Constructor %p other:%p\n", this, &other);
		CopyContent(other);
	}
	RefName& operator=(const RefName& other)
	{
		printf("Copy Assignment %p other:%p\n", this, &other);
		CopyAll(other);
		return *this;
	}
	RefName& operator=(const RefName&& other) noexcept
	{
		printf("Move Assignment %p other:%p\n", this, &other);
		CopyContent(other);
		return *this;
	}
	char* Buf;
	int* RefCount;
private:
	void CopyContent(const RefName& other)
	{
		Buf = other.Buf;
		RefCount = other.RefCount;
		(*RefCount)++;
	}
	void CopyAll(const RefName& other)
	{
		if (RefCount!=nullptr&&(*RefCount)>0)
		{
			DecreseRef();
		}
		MemCreate();
		memcpy_s(Buf, 256,other.Buf,256);
	}
	void DecreseRef()
	{
		(*RefCount)--;
		if (*RefCount == 0)
		{
			MemRelease();
		}
	}

	void MemRelease()
	{
		printf("MemRelease %p Buf:%p RefCount:%p\n",this, Buf, RefCount);
		delete RefCount;
		delete[]Buf;
	}

	void MemCreate()
	{
		Buf = new char[256];
		RefCount = new int(1);
		printf("MemCreate %p Buf:%p RefCount:%p\n", this, Buf, RefCount);
	}
};

RefName TestGetRefName()
{
	RefName temp;
	strcat_s(temp.Buf, 256, "TestGetRefName");
	return temp;
}
RefName TestGetRefName1(RefName ref)
{
	strcat_s(ref.Buf, 256, "TestGetRefName1");
	return ref;
}
#define MyPrint(idx) printf("%s\n", test##idx.Buf)
void RefTest()
{
	RefName test1;
	strcat_s(test1.Buf, 256, " test1");
	PrintLine();
	RefName test2 = test1;
	strcat_s(test2.Buf, 256, " test2");
	PrintLine();
	RefName test3(test1);
	strcat_s(test3.Buf, 256, " test3");
	PrintLine();
	RefName test4 = TestGetRefName();
	strcat_s(test4.Buf, 256, " test4");
	PrintLine();
	RefName test5 = TestGetRefName1(test1);
	strcat_s(test5.Buf, 256, " test5");
	RefName test6 = std::move(test1);
	strcat_s(test6.Buf, 256, " test6");
	MyPrint(1);
	MyPrint(2);
	MyPrint(3);
	MyPrint(4);
	MyPrint(5);
	MyPrint(6);
	PrintLine();
}

这里的代码和上面关于copy和move的代码都差不多,我就不一一解释代码了。看代码的时候,先自己想想,RefTest中一共会new出多少char数组,然后test1到test6的输出各是什么。

上面的内容只是为了方便理解,栈和堆之间的协同原理。实际操作过程中请使用std::shared_ptr和std::vector这类标准库的类来实现代码。


最开始的总纲实际上已经说了大半,综合来讲就是尽量使用STL库。

欢迎回到现代C++!

猜你喜欢

转载自blog.csdn.net/luoyu510183/article/details/90679789
今日推荐