本文将介绍对类使用new和delete 以及如何处理由于 使用动态内存而引起的一些微妙问题,它将影响构造函数的析构函数的设计和运算符的重载。
注意:不要在类声明文件(.h文件)中初始化类静态成员。在包含类方法的文件(.cpp文件)中初始化。但是如果静态成员是整型或枚举类型const,则可以在类声明中初始化。(c因为onst 定义的常量属于局部作用域,不是静态变量所属的全局区)
下面代码在每个构造函数都有num++。使得每创建一个str对象,静态变量的值就+1记录对象总数。完整代码实现部分如下:
#ifndef STR_H #define STR_H class str { public: str(); str(const char *s); virtual ~str(); friend std::ostream & operator<<(std ::ostream & os,const str & st); str(const str& st); protected: private: char * str; int len; static int num; }; #endif // STR_H
#include "str.h" str::str() { //ctor int len=4; str =new char[4]; std::strcpy(str,"C++"); num++; } str::str(const char *s){ len = std::strlen(s); str = new char[len+1]; std::strcpy(str,s); num++; } str::~str() { //dtor --num; delete[] str; } std::ostream & operator<<(std::ostream & os,const str &st){ //重载一下<<输出str对象 os<<st.str; return os; } str & str::operator=( const str& st){ if(this == &st) return *this; //判断一下赋值的对象同一个则返回自身 delete[] str; //如果不同,把旧的字符串指的内存释放,如果此时不释放, //上面的字符串一直在内存中,因为str不指向它了,该内存浪费了,稍后把一个新的字符串地址赋给str, len = st.len; str = new char[len+1]; std::strcpy(str,st.str); return *this; //此次对象赋值会把旧的内存delete,而使str指向一个新new的字符串,不会创建新的对象因此num不会变化 } int str::num=0; str::str(const str& st){ num++; len = st.len; str =new char [len +1]; std::strcpy(str,st.str); } int main(){ str sport("dsdsss"); }
分析: 类的成员str是一个指针,指向字符串。所以构造函数必须提供内存来存字符串,初始化对象时,给构造函数传一个字符串指针,(字符串可变)
Str(“String”);
构造函数必须分配足够的内存来存储字符串,然后把字符串复制到内存中
首先,用strlen()函数计算字符串的大小,并对成员len初始化,接着,用new分配足够的空间来保存字符串,然后新的内存地址(new函数返回值就是地址char *)给str 成员
strlen()返回字符串长度,不包括末尾的空字符\0,所以得+1,使得分配的内存能存储包含了空字符的字符串。
构造函数用strcpy()将传递的字符串复制到新的内存,并更新对象计数num
必须知道字符串并不保存在对象中,单独保存在堆内存中(new的动态分配的内存都在堆区),对象中str成员指针仅保存了指出到哪里查找字符串的地址
默认构造函数,虽然无形参,但实现中strcpy 提供了一个默认字符串“C++”,复制到新建的内存中。
该析构函数成员str指向new分配的内存,Str对象过期了,指针也过期若无delete
但str指向new分配的内存仍被分配。删除对象可以释放对象本身占的内存,但并不能自动释放属于对象成员指针指向的内存。
所以必须用析构函数里确保对象过期了,由构造函数new分配的内存也被释放。)(两件事,不能用默认的析构函数)
总结:str成员是指向一块新建内存的指针。该内存长度运行时候确定,是传入字符串strlen()+1,构造函数中new一个内存,再用std空间的成员方法strcpy把形参的复制到新建内存中
若用默认析构函数只会使str对象消亡,不会使得成员str指向的堆内存的字符串释放空间
复制构造函数引出:
int main 中有如下语句:
str sport("dsdsss");
str sailor=sports; //句子1
上面是把一个对象赋值给另一个对象是赋值运算符的重载吗 (不对)
句子1 不是调用默认也不是调用有参构造函数。
句子1 与 str sailor =str(sports); //句子2 等效
句2把对象作为实参传入str中,是有参构造函数调用吗?(不对)
若是有参构造函数,形参应该是str &,因为实参是个对象
正解:句子2原型是str(const str &)
这就是复制构造函数
复制构造函数形参是该类的引用
用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程,而不是常规的赋值过程
它接收一个对象的常量引用作为参数
对复制构造函数需要知道什么时候调用,有啥作用。下面是会调用复制构造函数几种形式
- str ditto(motto); //该实参是一个对象,在实参传给形参过程中调用了复制构造函数
- str metoo =motto;
- str also =Str(motto); //该函数返回值是一个对象
4 str* ptring =new Str(motto); // new出了一个匿名对象,并把这个对象地址给了pting这个指针
作用:
每当程序产生了一个对象副本,编译器都将使用复制构造函数
具体就是:按值传递对象,或返回对象都将用复制构造函数。按值传递意味着创建原始变量的一个副本
介绍了复制构造函数,
再说如果用默认复制构造函数对str类复制
会有什么问题?
str类有个成员是指针类型。
这样 sailior.str = sports.str;是复制了指针还是字符串?
是指针,只是把内存地址复制了一遍。并没把指针指向的字符串内容复制下来。所有复制的对象副本str指针指向的仍然是原来的内存,并没有一个新的内存被他指向、所以当析构函数调用两次删除对象时,会两次delete同一片内存空间,而产生错误。
正确做法是:重新编写新的复制构造函数,不用默认的。使得str指向一片新的内存空间。
如下代码:
str::str(const str& st){ num++; len = st.len; //与有参构造函数这句有差别 str =new char [len +1]; std::strcpy(str,st.str); }
调用有参构造函数,会创建一个全新的对象。虽然看起来和复制构造函数形式有点类似(这体现引用的特点:指向一个对象,名称是该已有的对象的别名)。
有参构造函数
str::str(const char *s){ len = std::strlen(s); str = new char[len+1]; std::strcpy(str,s); num++; }
实参给一个字符串,用指针指向它,
str析构函数使用默认的会出问题,也会出现在
赋值运算符重载 中
函数未定义的话,c++会自动添加重载的赋值运算符,
若未重载赋值运算符,把对象赋值给对象是不允许的。
重载的原型是
str & str::operator=(const str &)
该函数接收一个指向类对象的引用
什么时候用赋值运算符?
在把一个已有的对象赋值给一个对象时:
如
str kont;(“sds”); Str head; Kont=head;
该处用赋值问题和用隐含的复制构造函数错误相同,成员复制问题。
=复制只会head.str与knot指向相同的地址。两次调用析构函数时,删除同一片内存出错;
初始化对象时,不一定会用上赋值运算符
str sailor=sports; //此处用复制构造函数,不是赋值重载
上面句sailor 是一个新创建的对象,被初始化为sport的值,所以用复制构造函数
实现时也可以使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值赋到新对象中去。
总结就是初始化一定会用上复制构造函数,用=也可能调用赋值运算符(不一定是初始化过程)
赋值运算符与复制构造函数过程 类似,赋值运算符也是对成员进行逐个复制
如果成员本身就是类对象,则程序将用为这个类定义的赋值运算符来赋值该成员
自己写个重载的赋值运算符可以避免出现上述问题。如下
str & str::operator=( const str& st){ if(this == &st) return *this; //判断一下赋值的对象同一个则返回自身 delete[] str; //如果不同,把旧的字符串指的内存释放,如果此时不释放, //上面的字符串一直在内存中,因为str不指向它了,该内存浪费了。稍后把一个新的字符串地址赋给str, len = st.len; str = new char[len+1]; std::strcpy(str,st.str); return *this; //此次对象赋值会把旧的内存delete,而使str指向一个新new的字符串,不会创建新的对象。因此num不会变化 }