c++学习笔记 类和动态内存分配与特殊成员函数

本文探讨了C++中如何使用动态内存分配创建可变大小的类,如Str,通过new关键字分配内存。同时讲解了构造函数、析构函数、复制构造函数、赋值运算符和地址运算符的原理及作用。强调了在涉及动态内存时,析构函数中正确使用delete的重要性,以及自定义复制构造函数以避免浅拷贝问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上一个例子中是仿string类Str使用的是char数组类储存,但是这样有大小限制(100),能不能让它动态的进行调整大小呢?

已定义的char数组行不通,但是让一个指针指向一块规定大小的内存是可以的,于是我可以使用内存分配来完成任意大小的Str类

这种方法叫动态内存分配

类型指针=new 类型名;

char *pstr=new char[n];
strcpy(pstr,"c++");
为pstr指针分配n个char大小的内存来保存数据,然后用strcpy把“c++”复制到pstr指针指向的地址。

new关键字会开辟一个大小为n个char类型大小的空间,然后让指针指向它。

于是可以修改一下类Str,使得更好用:

class Str
{
	char *str;
	int n;
	const static int SIZEMAX = 100;								//每次输入的字符串长度
public:
	Str(){
		str = NULL;
		n = 0;
	}
	Str(const char *pstr){
		str = new char[n = strlen(pstr) + 1];						// 使用new为分配n+1个char大小的内存,并用str指向
	}
	Str operator+(const Str &s)
	{
		Str temp;
		delete[]temp.str;
		temp.str = new char[temp.n = (strlen(str) + strlen(s.str)) + 1];
		return temp;
	}
	friend Str operator+(const char *pstr, const Str &s)
	{
		Str temp;
		delete[]temp.str;
		temp.str = new char[temp.n = (strlen(pstr) + strlen(s.str)) + 1];
		return temp;
	}
	Str &operator=(const Str &s)
	{
		delete[]str;
		str = new char[n = strlen(s.str) + 1];
		return *this;
	}
	Str &operator=(const char *pstr)							//当Str对象=字符串指针时的特别情况
	{
		delete[]str;
		str = new char[n = strlen(pstr) + 1];
		return *this;
	}
	friend ostream &operator<<(ostream &os, const Str &s)
	{
		os << s.str;
		return os;
	}
	friend istream &operator>>(istream &is, Str &s)					<span style="white-space:pre">	</span>//重载输入运算符,能够使用cin>>Str对象;
	{
		char temp[Str::SIZEMAX];							//最大100长度
		is.get(temp, Str::SIZEMAX);
		if (is)										//检查输入是否非法
			s = temp;
		while (is&&is.get() != '\n')							//清空流
			continue;
		return is;
	}
	operator int()
	{
		return n;
	}
	~Str(){ delete[]str; }									//使用delete释放str指向的内存
};


构造函数会为对象的str指针分配内存,大小是形参pstr指针指向的内容的大小:strlen(pstr);该函数返回一个int值,函数会计算pstr的大小,最后把n也设置为pstr的大小,因为n是用来记录Str对象内字符串大小用的。

先来看看析构函数,在对象生命结束时会自动调用析构函数,如果使用默认的析构函数进行清理,那样结果会如何?

默认析构函数会把对象的数据成员进行清理,比如int n会被清理、char *str也会被清理,但是有个严重的问题,str指向的内存怎么办?

于是可以在构造函数里加入delete来清理动态分配的内存,析构函数会先执行函数体的内容,再进行清理工作,所以会先运行delete[]str;再清理int n;和char *str;

关于重载运算符">>",该方法接受一个istream和Str对象作为参数,并返回一个istream对象,先来看看是怎么工作的:

Str s4;
cin>>s4;

用户输入后并按下回车键确认时,将调用operator(cin,s4);,而用户输入的内容将被保存在输入流中,在>>函数里is.get()就相当于cin.get(),能够读取到输入流的内容,如果用户输入错误,is对象里将设置错误信息,可以用if判断,现在可以先不用了解它是如何工作的,只需要知道当输入的内容不符合类型时,而且无法通过转换识别,那么cin将设置错误信息,所以可以用if(is)判断输入流是否正确。

然后使用一个循环来清空输入流,当获取的字符不是'\n\'和is获取没有出错时继续获取。



在类里有一些特殊成员函数,当用户没有为他们定义时将自动提供:

默认构造函数、默认析构函数、复制构造函数、赋值运算符、地址运算符。


如果没有提供任何构造函数,将自动创建默认构造函数

类名(){};

构造函数:类名(参数列表);

例如有一个A类,但是没有提供任何构造函数,c++将自动提供A(){}构造函数,它将A类对象里的数据成员初始化。
当显式地提供了构造函数,比如A(int n);,c++将不会定义默认构造函数,这种情况下定义对象时不进行初始化赋值就会发生错误:

A a1; //编译器将找不到A(){}构造函数,所以如果提供了构造函数则把默认构造函数也提供。


对象过期时,例如在fun()函数里定义的A a1作为临时变量,在执行完函数后a1就会自动调用a1对象的析构函数。

~类名();

如果用户没有显式地提供析构函数,c++将自动提供默认的析构函数:例如~A(){};

它会对对象里的数据成员进行清理,但是如果存在数据成员指针动态分配内存,则只会清理指针变量,而不会清理指向的内存,这时候就需要在析构函数里使用delete释放指向的内存。析构函数的执行顺序是先执行析构函数函数体内的内容,然后清理对象里的数据成员。


默认的复制构造函数是c++自动提供的,比如说类A,它的默认复制构造函数就是A(const A &)。

类名(const 类名 &形参名);

当没有显式地提供复制构造函数,c++会提供一个默认的构造函数,他的功能是把非静态的数据成员逐个复制到新对象中对应的数据成员:

假如类A:

class A
{
<span style="white-space:pre">	</span>int a;
<span style="white-space:pre">	</span>double d;
<span style="white-space:pre">	</span>static int NUM;
public:
<span style="white-space:pre">	</span>A(int n,double m){ a=n; d=m; };
}
A a1(3,2.5);
A a2(a1);//调用复制构造函数
A a2(a1)这个表达式将调用默认的复制构造函数把对象a1中,把a1中的a和d复制到a2中,而静态变量NUM不会被复制。

与下面代码等效:

a2.a=a1.a;
a2.d=a1.d;
注意:这只是说明,事实上是无法访问私有成员的!

这就是默认的复制构造函数,但是有些情况下它福安满足我们的需要,比如Str类对象,里面的数据成员是char指针和int整形,int整形很好办,但是char指针只会把char指针保存的地址复制过去,这样当被复制的对象s1调用了析构函数时,调用复制的s2的str指针指向的内容也会消失。

所以我们需要自定义复制构造函数:

Str(const Str &s)
{
<span style="white-space:pre">	</span>if(NULL!=str)
	delete[]str;					//如果不是在定义对象时复制,就先释放调用对象str指向的内存
	str=new char[n=strlen(s.str)+1];
	strcpy(str,s.str);
}
Str s4(s2);

Str s4=s2;

Str s4=Str(s2);

Str *p=new Str(s2);

上述情况都会调用复制构造函数。第一种情况是常见的,直接调用复制构造函数把s2的内容复制到s4。第二种和第三种会出现两种情况,一是直接使用复制构造函数创建s4,二是先使用复制构造函数先生成一个临时变量,然后再s2的内容赋给s4,这取决于编译器。第四种情况会生成一个对象,然后把对象的地址赋给指针p。


如果将已有的对象赋值给另一个对象,那就会调用赋值运算符,例如:

Str s5;

s5=s2;

这样进行赋值就会调用赋值构造函数,相当于调用了s5.operator=(s2);

赋值构造函数是通过重载"="来完成的:

返回值 operator=(参数列表);


有时会把复制构造函数与赋值构造函数混绕例如:

Str s1=s2;

他到底是使用赋值还是复制?若在定义变量时赋初始值,那就是使用复制构造函数。

若只是使用=号赋值,如s1=s2;那就是赋值构造函数。


地址运算符就是&,他会取得对象的地址,他不需要显式的提供,c++会默认地提供该功能。

但是有时候并不是我们希望的,例如&s1是si对象的地址,然而我想要的是s1对象里保存指针的地址,这时候就可以自己重载运算符&:

char *operator&()
{
	return str;
}
这样就可以吧Str对象的str地址直接使用&获取:

char *p=&s2;




下面谈谈成员初始化列表来初始化类:

Str() :str(NULL), n(0){}
这样当使用Str类定义对象时,默认构造函数会初始化里面的内容

在cpp文件里格式是:

类名::类名(参数列表):数据成员1(参数1),数据成员2(参数2)...数据成员n(参数n)

{

//函数体

}

这种格式只能用于构造函数,其他函数非法,必须用这种格式来初始化非静态的const数据成员,必须使用这种格式里初始化引用数据成员(类型名 &变量名)


c++11还允许在类里初始化数据成员:

class A
{
	int a=0;
	double b=0.0;
	static const int n=10;
public:
	...
};
相当于:A():a(0),b(0.0),n(10){}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值