第七章 类Part2

深拷贝和浅拷贝

浅拷贝

  1. 同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝.
  2. 一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。
    image
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;


class Person
{
public:
	
	Person()
	{
	}
	//构造函数就是初始化属性的
	Person(char *name, int age)
	{
		m_Name = (char *)malloc(strlen(name) + 1);//malloc函数返回的是 void *
		strcpy(m_Name, name);

		m_age = age;//为啥不用开辟空间啊?因为name传过来的是pointer,而age是基本数据类型,传参的时候就开辟空间了
	}

	//拷贝构造,系统会默认提供拷贝构造,而且是简单的值拷贝

	~Person()
	{
		cout << "析构函数调用" << endl;
		if (m_Name != NULL)
		{
			free(m_Name);
			m_Name = NULL;//防止野指针
		}
	}

	//姓名
	char * m_Name;
	//年龄
	int m_age;

};

void test01()
{
	Person p1("敌法", 10);//调用了有参构造,
	Person p2(p1);//调用了拷贝构造
	
}

int main()
{
	test01();

	system("pause");
	return 0;
}

image
image

深拷贝

4.3.4.2 深拷贝
当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。
image

class Person{
public:
	Person(char* name,int age){
		pName = (char*)malloc(strlen(name) + 1);
		strcpy(pName,name);
		mAge = age;
	}
	//增加拷贝构造函数
	Person(const Person& person){
		pName = (char*)malloc(strlen(person.pName) + 1);
		strcpy(pName, person.pName);
		mAge = person.mAge;
	}

	~Person(){
		if (pName != NULL){
			free(pName);
		}
	}
private:
	char* pName;
	int mAge;
};

void test(){
	Person p1("Edward",30);
	//用对象p1初始化对象p2,调用c++提供的默认拷贝构造函数
	Person p2 = p1;
}

如果属性里有指向堆空间的数据,那么简单的浅拷贝会导致重复释放内存的异常,
解决上述问题,需要我们自己提供拷贝构造函数,进行深拷贝。

示例代码:

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;


class Person
{
public:
	
	Person()
	{
	}
	//构造函数就是初始化属性的
	Person(char *name, int age)
	{
		m_Name = (char *)malloc(strlen(name) + 1);//malloc函数返回的是 void *
		strcpy(m_Name, name);

		m_Age = age;//为啥不用开辟空间啊?因为name传过来的是pointer,而age是基本数据类型,传参的时候就开辟空间了
	}

	//拷贝构造,系统会默认提供拷贝构造,而且是简单的值拷贝
	//自己提供拷贝构造,原因是:简单的浅拷贝会释放堆区空间两次,导致程序挂壁
	//深拷贝
	Person(const Person &p)
	{
		m_Age = p.m_Age;
		m_Name = (char *)malloc(strlen(p.m_Name) + 1);// + 1是因为,结尾要有一个 \0
		strcpy(m_Name, p.m_Name);
		m_age = person.m_age;
	}

	~Person()
	{
		cout << "析构函数调用" << endl;
		if (m_Name != NULL)
		{
			free(m_Name);//开辟了空间,就要释放掉
			m_Name = NULL;//防止野指针
		}
	}

	//姓名
	char * m_Name;
	//年龄
	int m_Age;

};

void test01()
{
	Person p1("敌法", 10);//调用了有参构造,
	Person p2(p1);//调用了拷贝构造
	
}

int main()
{
	test01();

	system("pause");
	return 0;
}

先释放拷贝构造的,再释放原本的
image


explicit关键字

4.3.5 explicit关键字
C++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用。

[explicit注意]

  • explicit用于修饰构造函数,防止隐式转化。
  • 是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。
    代码示例:
点击查看代码
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
//#include<string>
using namespace std;

class MyString
{
public:
	MyString(const char * str)
	{
		//
	}

	explicit MyString(int a)
	{

	}

	char * mStr;

};

void test01()
{
	MyString str = "abc";
	MyString str2 = 10;//这行代码做什么用?
	//不明确!
	//隐式类型转换,相当于: MyString str2 = MyString(10);
	//意思一:str2字符串为 10
	//意思二:或者字符串的长度申请为10
	//如果不想这么做,可以使用关键字explicit
	//作用:
	MyString str3(10);//明确
}

int main()
{
	test01();

	system("pause");
	return 0;
}

image


动态创建对象

  • 当我们创建数组的时候,总是需要提前预定数组的长度,然后编译器分配预定长度的数组空间,在使用数组的时,会有这样的问题,数组也许空间太大了,浪费空间,也许空间不足,所以对于数组来讲,如果能根据需要来分配空间大小再好不过。
  • 所以动态的意思意味着不确定性。
  • 为了解决这个普遍的编程问题,在运行中可以创建和销毁对象是最基本的要求。当然C早就提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。
  • 然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。

当创建一个c++对象时会发生两件事:

  1. 为对象分配内存
  2. 调用构造函数来初始化那块内存
    第一步我们能保证实现,需要我们确保第二步一定能发生。c++强迫我们这么做是因为使用未初始化的对象是程序出错的一个重要原因。

C动态分配内存方法

为了在运行时动态分配内存,c在他的标准库中提供了一些函数,malloc以及它的变种calloc和realloc,释放内存的free,这些函数是有效的、但是原始的,需要程序员理解和小心使用。为了使用c的动态内存分配函数在堆上创建一个类的实例,我们必须这样做:

C动态分配内存方法
class Person{
public:
	Person(){
		mAge = 20;
		pName = (char*)malloc(strlen("john")+1);
		strcpy(pName, "john");
	}
	void Init(){
		mAge = 20;
		pName = (char*)malloc(strlen("john")+1);
		strcpy(pName, "john");
	}
	void Clean(){
		if (pName != NULL){
			free(pName);
		}
	}
public:
	int mAge;
	char* pName;
};
int main(){

	//分配内存
	Person* person = (Person*)malloc(sizeof(Person));
	if(person == NULL){
		return 0;
	}
	//调用初始化函数
	person->Init();
	//清理对象
	person->Clean();
	//释放person对象
	free(person);

	return EXIT_SUCCESS;
}

这样做的问题:

  1. 程序员必须确定对象的长度。
  2. malloc返回一个void指针,c++不允许将void赋值给其他任何指针,必须强转。
  3. malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。
  4. 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。

new operator

1.c的动态内存分配函数太复杂,容易令人混淆,是不可接受的,c++中我们推荐使用运算符new 和 delete。
2.C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。
3.当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。

Person* person = new Person;
相当于:
Person* person = (Person*)malloc(sizeof(Person));
	if(person == NULL){
		return 0;
	}
person->Init();

New操作符能确定在调用构造函数初始化之前内存分配是成功的,所以不用显式确定调用是否成功。
现在我们发现在堆里创建对象的过程变得简单了,只需要一个简单的表达式,它带有内置的长度计算、类型转换和安全检查。这样在堆创建一个对象和在栈里创建对象一样简单。

delete operator

1.new表达式的反面是delete表达式。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
2.delete只适用于由new创建的对象。
3.如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。因为大多数new和delete的实现机制都使用了malloc和free,所以很可能没有调用析构函数就释放了内存。
4.如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。

delete操作符
class Person{
public:
	Person(){
		cout << "无参构造函数!" << endl;
		pName = (char*)malloc(strlen("undefined") + 1);
		strcpy(pName, "undefined");
		mAge = 0;
	}
	Person(char* name, int age){
		cout << "有参构造函数!" << endl;
		pName = (char*)malloc(strlen(name) + 1);
		strcpy(pName, name);
		mAge = age;
	}
	void ShowPerson(){
		cout << "Name:" << pName << " Age:" << mAge << endl;
	}
	~Person(){
		cout << "析构函数!" << endl;
		if (pName != NULL){
			delete pName;
			pName = NULL;
		}
	}
public:
	char* pName;
	int mAge;
};

void test(){
	Person* person1 = new Person;
	Person* person2 = new Person("John",33);

	person1->ShowPerson();
	person2->ShowPerson();

	delete person1;
	delete person2;
}

用于数组的new和delete

使用new和delete在堆上创建数组非常容易。

//创建字符数组
char* pStr = new char[100];
//创建整型数组
int* pArr1 = new int[100]; 
//创建整型数组并初始化
int* pArr2 = new int[10]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

//释放数组内存
delete[] pStr;
delete[] pArr1;
delete[] pArr2;

当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。

点击查看代码
class Person{
public:
	Person(){
		pName = (char*)malloc(strlen("undefined") + 1);
		strcpy(pName, "undefined");
		mAge = 0;
	}
    //深拷贝构造
	Person(char* name, int age){
		pName = (char*)malloc(sizeof(name));
		strcpy(pName, name);
		mAge = age;
	}
	~Person(){
		if (pName != NULL){
			delete pName;
		}
	}
public:
	char* pName;
	int mAge;
};

void test(){
	//栈聚合初始化
	Person person[] = { Person("john", 20), Person("Smith", 22) };
	cout << person[1].pName << endl;
    //创建堆上对象数组必须提供构造函数
	Person* workers = new Person[20]; //自动调用20次无参构造函数
}

delete void*可能会出错

如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的, 因为它将不执行析构函数.以下代码未调用析构函数,导致可用内存减少。

delete void*
class Person{
public:
	Person(char* name, int age){
		pName = (char*)malloc(sizeof(name));
		strcpy(pName,name);
		mAge = age;
	}
	~Person(){
		if (pName != NULL){
			delete pName;
		}
	}
public:
	char* pName;
	int mAge;
};

void test(){
	void* person = new Person("john",20);
	delete person;
}

问题:malloc、free和new、delete可以混搭使用吗?也就是说malloc分配的内存,可以调用delete吗?通过new创建的对象,可以调用free来释放吗?


使用new和delete采用相同形式

	Person* person = new Person[10];
	delete person;

以上代码有什么问题吗?(vs下直接中断、qt下析构函数调用一次)
使用了new也搭配使用了delete,问题在于Person有10个对象,那么其他9个对象可能没有调用析构函数,也就是说其他9个对象可能删除不完全,因为它们的析构函数没有被调用。
我们现在清楚使用new的时候发生了两件事: 一、分配内存;二、调用构造函数,那么调用delete的时候也有两件事:
一、析构函数;二、释放内存。
那么刚才我们那段代码最大的问题在于:person指针指向的内存中到底有多少个对象,因为这个决定应该有多少个析构函数应该被调用。
换句话说,person指针指向的是一个单一的对象还是一个数组对象,由于单一对象和数组对象的内存布局是不同的。
更明确的说,数组所用的内存通常还包括“数组大小记录”,使得delete的时候知道应该调用几次析构函数。单一对象的话就没有这个记录。单一对象和数组对象的内存布局可理解为下图:
image

本图只是为了说明,编译器不一定如此实现,但是很多编译器是这样做的。

  1. 当我们使用一个delete的时候,我们必须让delete知道指针指向的内存空间中是否存在一个“数组大小记录”的办法就是我们告诉它。
  2. 当我们使用delete[],那么delete就知道是一个对象数组,从而清楚应该调用几次析构函数。
结论:

如果在new表达式中使用[],必须在相应的delete表达式中也使用[].如果在new表达式中不使用[], 一定不要在相应的delete[]表达式中使用[].

posted @ 2023-02-14 00:05  nullptrException  阅读(70)  评论(0)    收藏  举报