前言

最近在写代码的过程中,发现一个大家容易忽略的知识点:深仿制和浅仿制

或许关于Java程序员来说,很少遇到深浅仿制问题,可是关于C++程序员来说可谓是又爱又恨。。

浅仿制:

  • 1.将原目标或许原目标的引证直接赋值给新目标,新目标,新数组仅仅原目标的一个引证。

  • 2.C++默许的仿制结构函数与赋值运算符重载都是浅仿制,能够节省必定空间,可是或许会引发同一块内存重复开释问题, 二次开释内存或许导致严重的反常溃散等状况。

  • 浅仿制模型:

    【重学C/C++系列(九)】:深拷贝和浅拷贝的那点事

深仿制:

  • 1.创立一个新的目标或许数组,将目标或许数组的特点值仿制过来,留意此刻新目标指向的不是原目标的引证而是原目标的值,新目标在堆中有自己的地址空间。

  • 2.浪费空间,可是不会引发浅仿制中的资源重复开释问题。

  • 深仿制模型

    【重学C/C++系列(九)】:深拷贝和浅拷贝的那点事

事例分析

下面运用一个事例来看下一个由于浅仿制带来的bug。

#include "DeepCopy.h"
#include <iostream>
#include <string>
using namespace std;
class Human {
public:
	Human(int age):_age(age) {
	}
	int _age;;
};
class String {
public:
	String(Human* pHuman){
		this->pHuman = pHuman;
	}
	~String() {
		delete pHuman;
	}
	Human* pHuman;
};
void DeepCopy::main() 
{	
	Human* p = new Human(100);
	String s1(p);
	String s2(s1);
}

这个程序从外表看是不会有啥问题的,运转后,呈现如下过错:

【重学C/C++系列(九)】:深拷贝和浅拷贝的那点事

先说下原因

这个过错便是由于代码 String s2(s1) 会调用String的默许仿制结构函数,而默许的仿制结构函数运用的是浅仿制,即仅仅仅仅对新的指针目标pHuman指向原指针目标pHuman指向的地址

在退出main函数效果域后,会回调s1和s2的析构函数,当回调s2析构函数后,s2中的pHuman内存资源被开释,此刻再回调s1,也会回调s1中的pHuman析构函数,可是此刻的pHuman指向的地址 已经在s2中被开释了,造成了二次开释内存,呈现了溃散的状况

所以为了避免呈现二次开释内存的状况,需求运用深仿制

深仿制需求重写仿制结构函数以及赋值运算符重载,且在仿制结构内部重新去new一个目标资源.

代码如下:

#include "DeepCopy.h"
#include <iostream>
#include <string>
using namespace std;
class Human {
public:
	Human(int age):_age(age) {
```
}
int _age;;
```
};
class String {
public:
	String(Human* pHuman){
		this->pHuman = pHuman;
	}
	//重写仿制结构,完成深仿制,避免二次开释内存引发溃散
	String(const String& str) {
		pHuman = new Human(str.pHuman->_age);
	}
	~String() {
		delete pHuman;
	}
	Human* pHuman;
};
void DeepCopy::main() 
{	
	Human* p = new Human(100);
	String s1(p);
	String s2(s1);
}

默许状况下运用:

String s2(s1)或许String s2 = s1 这两种办法去赋值,就会调用String的仿制结构办法,假如没有完成,则会履行默许的仿制结构,即浅仿制。

能够在仿制结构函数中运用new重新对指针进行资源分配,到达深仿制的要求、

说了这么多只要记住一点:假如类中有成员变量是指针的状况下,就需求自己去完成深仿制

虽然深仿制能够协助咱们避免呈现二次内存是否的问题,可是其会浪费必定空间,假如目标中资源较大,拿每个目标都包含一个大目标,这不是一个很好的设计,而浅仿制就没这个问题。

那么有什么办法能够兼容他们的长处么?即不浪费空间也不会引起二次内存开释

兼容优化方案:

  • 1.引证计数办法
  • 2.运用move语义搬运

引证计数

咱们对资源添加一个引证计数,在结构函数以及仿制结构函数中让计数+1,在析构中让计数-1.当计数为0时,才会去开释资源,这是一个不错的留意。

如图所示:

【重学C/C++系列(九)】:深拷贝和浅拷贝的那点事

对应代码如下:

#include "DeepCopy.h"
#include <iostream>
#include <string>
using namespace std;
class Human {
public:
	Human(int age):_age(age) {
	}
	int _age;;
};
class String {
public:
	String() {
		addRefCount();
	}
	String(Human* pHuman){
		this->pHuman = pHuman;
		addRefCount();
	}
	//重写仿制结构,完成深仿制,避免二次开释内存引发溃散
	String(const String& str) {
		////深仿制
		//pHuman = new Human(str.pHuman->_age);
		//浅仿制
		pHuman = str.pHuman;
		addRefCount();
	}
	~String() {
		subRefCount();
		if (getRefCount() <= 0) {
			delete pHuman;
		}	
	}
	Human* pHuman;
private:
	void addRefCount() {
		refCount++;
	}
	void subRefCount() {
		refCount--;
	}
	int getRefCount() {
		return refCount;
	}
	static int refCount;
};
int String::refCount = 0;
void DeepCopy::main() 
{	
	Human* p = new Human(100);
	String s1(p);
	String s2 = s1;
}

此刻的仿制结构函数运用了浅仿制对成员目标进行赋值,且只有在引证计数为0的状况下才会进行资源开释

可是引证计数的办法会呈现循环引证的状况,导致内存无法开释,产生内存走漏

循环引证模型如下:

【重学C/C++系列(九)】:深拷贝和浅拷贝的那点事

咱们知道在C++的智能指针shared_ptr中就运用了引证计数

类似java中目标垃圾的定位办法,假如有一个指针引证某块内存,则引证计数+1,开释计数-1.假如引证计数为0,则阐明这块内存能够开释了。

下面咱们写个shared_ptr循环引证的状况:

class A {
  public:
    shared_ptr<B> pa;
```
~A() {
    cout << "~A" << endl;
}
```
};
class B {
public:
    shared_ptr<A> pb;
```
~B() {
    cout << "~B" << endl;
}
```
};
void sharedPtr() {
    shared_ptr<A> a(new A());
    shared_ptr<B> b(new B());
    cout << "第一次引证:" << endl;
    cout <<"计数a:" << a.use_count() << endl;
    cout << "计数b:" << b.use_count() << endl;
    a->pa = b;
    b->pb = a;
    cout << "第2次引证:" << endl;
    cout << "计数a:" << a.use_count() << endl;
    cout << "计数b:" << b.use_count() << endl;
}
运转成果:
第一次引证:
计数a:1
计数b:1
第2次引证:
计数a:2
计数b:2

能够看到运转成果并没有打印出对应的析构函数,也便是没被开释。

指针a和指针b是栈上的,当退出他们的效果域后,引证计数会-1,可是其计数器数是2,所以还不为0,也便是不能被开释。你不开释我,我也不开释你,咱两耗着呗。

这便是标志性的由于循环引证计数导致的内存走漏.。所以咱们在设计深浅仿制代码的时分千万别写出循环引证的状况

move语义搬运

在C++11之前,假如要将源目标的状态搬运到目标目标只能通过仿制。 而现在在某些状况下,咱们没有必要仿制目标,只需求移动它们。

C++11引进移动语义

源目标资源的控制权全部交给目标目标。留意这儿说的是控制权,即运用一个新的指针目标去指向这个目标,然后将原目标的指针置为nullptr

模型如下:

【重学C/C++系列(九)】:深拷贝和浅拷贝的那点事

要完成move语义,需求完成移动结构函数 代码如下:

//移动语义move
class Human {
public:
	Human(int age) :_age(age) {
```
}
int _age;;
```
};
class String {
public:
```
String(Human* pHuman) {
	this->pHuman = pHuman;
}
//重写仿制结构,完成深仿制,避免二次开释内存引发溃散
String(const String& str) {
	////深仿制
	//pHuman = new Human(str.pHuman->_age);
	//浅仿制
	pHuman = str.pHuman;
}
//移动结构函数
String(String&& str) {
	pHuman = str.pHuman;
	str.pHuman = NULL;
}
~String() {
	if (pHuman != NULL) {
		delete pHuman;
	}		
}
Human* pHuman;	
```
};
void DeepCopy::main()
{
	Human* p = new Human(100);
	String s1(p);
```
String s2(std::move(s1));
String s3(std::move(s2));
```
}

该事例中,指针p的权限会由s1让渡给s2,s2再让渡给s3.

运用move语义搬运在C++中运用还是比较频频的,由于其能够大大缩小由于目标目标的创立导致内存吃紧的状况。比较引荐应用中运用这种办法来优化内存方面问题.

总结

本篇文章首要讲解了C++面向目标编程中的深仿制和浅仿制的问题,以及运用引证计数和move语义搬运的办法来优化深浅仿制的问题。

C++不像Java那样,JVM都给咱们处理好了资源开释的问题,没有二次开释导致的溃散状况,C++要懂的东西远非Java可比,这也是为什么C++程序员那么少的原因之一吧

这篇文章就讲解到这了,我是小余,咱们下期见。