动态内存与类
本章讲述类的动态分配内存问题,如果类中包含一个字符串,那该字符串内容空间大小的声明不能太大也不能太小,因此动态分配内存最为合理。并且通常在构造函数中使用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;
}
有几点说明:
- 赋值运算符左侧是类对象,因此函数重载类型是成员函数
- 因为有可能出现连等的情况,所以需要返回类对象
- 有一步delete操作,因为定义一个对象,根据我们构造函数代码,都会有一个new分配内存的操作,所以赋值前需要将已有的内存空间释放
- 中间有一个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清除掉最开始创建的动态字符数组内存。
本章最后有一个队列,等复习数据结构时再补充。