第十二章.类和动态内存分配(P426-479)

本文详细探讨了C++中类的动态内存分配,特别是在构造函数和析构函数中的使用。强调了动态内存分配与释放的一致性,以及在复制构造函数和赋值运算符重载中的重要性。通过示例代码解释了动态内存可能导致的问题,如析构函数在释放未正确分配的空间时的错误。同时,提到了定位new运算符的使用及其内存管理注意事项,并讨论了析构函数的调用规则。文章还提醒在处理包含类成员的类时,应确保所有成员都有相应的复制构造函数。
摘要由CSDN通过智能技术生成

动态内存与类

本章讲述类的动态分配内存问题,如果类中包含一个字符串,那该字符串内容空间大小的声明不能太大也不能太小,因此动态分配内存最为合理。并且通常在构造函数中使用new分配空间,在析构函数中使用delete释放空间。

下面是一个简单的示例:

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

class Stringbad{
  private:
  	char * str;
  	int len;
  	static int numbers;
  public:
  	Stringbad();
  	Stringbad(const char *);
  	~Stringbad();
  	friend ostream &operator<<(ostream &, const Stringbad &);
};
void fuction1(Stringbad &);
void fuction2(Stringbad);
int Stringbad::numbers = 0;

Stringbad::Stringbad(){
  len = 4;
  str = new char[4];
  strcpy(str,"c++");
  numbers++;
}

Stringbad::Stringbad(const char * sp){
  len = strlen(sp);
  str = new char[len+1];   //动态开辟空间
  strcpy(str,sp);
  numbers++;
  cout<<numbers<<" : "<<str<<endl;
}
Stringbad::~Stringbad(){
  cout<<str<<" is deleting: "<<numbers<<endl;
  delete []str;  //析构函数释放空间
  --numbers;
}

ostream &operator<<(ostream & os,const Stringbad &st)
{
  os<<"the "<<st.numbers<<" strings is "<<st.str<<endl;
  return os;
}

void fuction1(Stringbad &s){
  cout<<s;
}

void fuction2(Stringbad s){
  cout<<s;
}


int main(){
  Stringbad s1("hello world");
  Stringbad s2("moring");

  cout<<"------------------"<<endl;
  fuction1(s1);
  fuction2(s2);
  return 0;
}

对于上述代码有几点注意。
第一,我们类中是char*指针,所以存储的是字符串的地址,并不是字符串本身,因为我们传入的是个常量字符串“hello world”,因此在相应构造函数参数应该定义为const类型,在dev中如果没有定义为const,会自动转换,并且警告。但在vs中没有定义会直接报错,因此这点需要注意。

第二,numbers变量定义为static类型,因此它在程序中只有一个,无论定义多少个对象,他们共用一个numbers常量,因此将该变量单独在类外定义。

其次就是该程序有个错误,在定义的两个正常函数中,fuction1参数为引用,fuction2参数为值参数。fuction1函数是正常的,但fuction2有问题。该函数是值传递,相当于:

Stringbad s1 = s2;

相当于将一个类对象赋予另一个类对象,定义对象肯定使用构造函数,因此与下面代码等价:

Stringbad s1 = Stringbad(s2);

但没有使用默认构造函数,也不是我们定义的构造函数,这是编译器自动生成的构造函数,简称复制构造函数
但在该程序中,我们构造函数中使用new动态分配空间,但是自动生成的复制构造函数中却没有动态分布空间。结果导致三个对象,两个对象是正常new分配空间的,另外一个没有动态分配。但三个对象都会调用析构函数,此时,利用复制构造函数定义的对象在调用析构函数时便会出现问题,不知道delete释放哪些空间,因为他没有new分配空间。在不同编译器该错误显示不同的,但在调用析构函数时一定是有问题的。

在这里插入图片描述这是在dev中编译的,可以明显的发现,第二个析构函数出现了问题。
上述错误我们知道,因为默认复制构造函数没有new分配空间,所以,我们在原有程序上进行修改,增加一个复制构造函数即可:

Stringbad(const Stringbad &);

Stringbad::Stringbad(const Stringbad &st){
	len = st.len;
	str = new char[len+1];
	strcpy(str,st.str);
}

这样结果就没有错误了:
在这里插入图片描述有个细节,这里调用了三次析构函数,但我们只定义了两个构造函数,是因为我们在fuction2值传递时调用了复制构造函数,一共三次构造函数,所以有三次析构函数。
到此该问题看似解决,但还有一点疑惑,我们知道,下面两行代码是等价的:

Stringbad s1 = s2;
Stringbad s1 = Stringbad(s2);

我们是推测在复制构造函数中出现了问题,导致错误,但是,这里还有一个过程,是将一个对象赋值给另一个对象,也就是赋值运算符,上一章说过,类中赋值运算符是已经默认重载过的,但默认重载函数中也没有new分配内存,所以会不会是赋值运算符出现问题呢?到这里,就会发现,该问题的产生有两种原因:赋值运算符或者复制构造函数。因为编译器不同,原因可以也不同,所以为了程序完整性,应把所有情况都考虑进去,因此,要在源代码的基础上重载赋值运算符:

Stringbad &operator=(const Stringbad &);

Stringbad &Stringbad::operator=(const Stringbad &st){
	if(this==&st)    
		return *this;
	delete []str;
	len = st.len;
	str = new char[len+1];
	strcpy(str,st.str);
	return *this;
}

有几点说明:

  1. 赋值运算符左侧是类对象,因此函数重载类型是成员函数
  2. 因为有可能出现连等的情况,所以需要返回类对象
  3. 有一步delete操作,因为定义一个对象,根据我们构造函数代码,都会有一个new分配内存的操作,所以赋值前需要将已有的内存空间释放
  4. 中间有一个if语句,因为,有可能是将自己赋值给自己,这样两个new空间是一样的,如果delete,则表示两个空间都没了,那么再赋值就会有问题,因此加一个判断语句,如果是自己赋值给自己,则直接返回类对象。

在构造函数中使用new注意的问题

  • 在构造函数中使用new初始化指针成员,析构函数中应使用delete
  • new和delete必须互相兼容。new对应于delete。new[]对应delete[]
  • 如果有多个构造函数,使用new的形式必须一致,因为它们匹配同一个析构函数
  • 根据前面的Stringbad实例,构造函数中使用new时,最好定义一个复制构造函数和复制运算符重载函数,防止析构函数在释放空间时出现问题。

包含类成员的类的逐成员复制

class String{
private:
	Stringbad s1;
	string str1;
	...
}

Stringbad 和string都是类,此时,声明的类成员是已经定义或者标准库中的类,也就是嵌套。string是标准库的类,有相应地复制构造函数,Stringbad我们前面已经定义过复制构造函数。如果新定义的String中成员只包含这两个,那么String类不需要定义新的复制构造函数,在复制该类时,会采取成员逐一复制的原则,只要每个成员都有其对应的复制构造函数,那么大的类就不需要定义新的复制构造函数。但如果成员还包括一个新的类,没有复制构造函数的,则大的String类需要定义复制构造函数

返回对象的说明

返回对象的值还是引用,可供选择,但返回引用的效率高,因为返回值含需要调用复制构造函数。

1.返回const对象的引用

使用const引用是为了提高效率,但有一些限制,某些情况不能这样使用。如果传入两个参数,返回较大的一个,此时,传入的参数可以声明为const,因为不做修改,因为两个参数都是const,所以返回值也应该声明为const,这样更匹配

2.返回值非const的对象引用

常见的两种情况是重载赋值运算符(提高效率)和重载<<运算符(必须这样,因为ostream类没有复制构造函数)。

3.返回对象

被返回的对象为局部对象,则不能使用引用

4.返回const对象

使用指向对象的指针

析构函数的调用:

  • 如果对象是动态变量,则在执行完所属的代码块时,调用其析构函数。
  • 如果是静态变量,在整个程序结束后自动调用析构函数
  • 如果是new创建的,只有在显式使用delete删除对象时,才会调用析构函数

声明指向类的指针:

String *pr;

将指针初始化为已有的对象:

String * pt = &string[0];//string为类数组

使用new和默认构造函数对指针初始化:

String *pt = new String;

使用new和接受一个参数的构造函数进行初始化

String *pt = new String(const char *);

使用new和接受一个类对象为参数的构造函数进行初始化:

String * pt = new String(string[0]);

其中内存关系,如下:
在这里插入图片描述

定位new运算符

#include<iostream>
using namespace std;
const int IM = 512;
class Student{
	private:
		string name;
		int age;
	public:
		Student(const string & s= "none", int n = 0){
			name = s;
			age = n;
			cout<<name<<" is constructed!"<<endl;
		}
		~Student(){
			cout<<name<<" is destoryed!"<<endl;
		};
		void show() const{
			cout<<name<<" -------is-------- "<<age<<endl;
		}
};
int main(){
	char * ch = new char[IM];
	Student *p1,*p2;
	p1 = new(ch) Student;
	p2 = new Student("jack",15);
	p1->show();
	p2->show();
	Student *p3,*p4;
	p3 = new(ch+sizeof(Student)) Student("kyle",20);
	p4 = new Student("mark",19);
	p3->show();
	p4->show();
	delete p2;
	delete p4;
	p3->~Student();
	p1->~Student();
	delete []ch;
	return 0;
}

new运算符是为变量动态分配内存,而定位new运算符可以指定分配空间的位置,对上述实例做几点说明:

  • ch是提前创建的字符数组内存,为后续定位new运算符指定内存位置。
  • p1,p3为指向定位new运算符创建的指针。p2,p4为指向普通new运算符的指针
  • p1指向ch数组的开始,p3则往后移动一个对象的位置,不然将会覆盖p1的对象,引起错误。
  • 程序调用四次构造函数,也应该伴随着四次析构函数。p2,p4在使用delete清除内存的时候会调用其析构函数。但p1,p3是定位new运算符,不能使用delete清除内存,所以不能伴随着隐式调用析构函数。因此,必须显式的调用析构函数,调用顺序与构造函数的顺序相反。
  • 最后需要delete清除掉最开始创建的动态字符数组内存。

本章最后有一个队列,等复习数据结构时再补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值