前几天,又复习了一点关于c++的内容。觉得收获的还是挺多的。所以,将此次的收获记录下来。
就像侯捷老师所说,c++中的类可以分为两种,一种是成员变量没有指针的,还有一种就是成员变量中含有指针的。
如果你的类中有指针,那么是一定得自己重写拷贝赋值函数(赋值运算符的重载),拷贝构造函数,析构函数。因为,难免你在操作类的时候会让编译器自动调用这些函数。但是你如果不重写的话,使用编译器自带的这三个函数,就可能带来一些麻烦。
先贴出代码,然后我细细道来。
//这里是String.h
#ifndef _STRING_H_
#define _STRING_H_
#include<iostream>
using namespace std;
class String
{
private:
char *m_data;
public:
String();
String(const char *str);
String(const String& str);
String& operator=(const String&str);
~String();
char* get_char() const{return m_data;}
};
#endif
//这里是String,c
#include<iostream>
#include<string.h>
#include"String.h"
using namespace std;
/*
因为编译的原因,最好在vs上面也测试一哈,比较保险。
*/
int main()
{
String A("hello");
String B(A);
String C("world");
String* D=new String("hahaha");
C=B;
cout<<"i am string A: "<<A.get_char()<<endl;
cout<<"i am string B: "<<B.get_char()<<endl;
cout<<"i am string C: "<<C.get_char()<<endl;
delete D;
return 0;
}
String::String()
{
//这一步也是十分重要的 (如果不要的话,就会导致你在想要输出m_data时遇到麻烦!)
m_data=NULL;
cout<<"default constructer function"<<endl;
}
String::String(const char* str=0)
{
cout<<"i am construct function"<<endl;
if(str){
m_data=new char[strlen(str)+1];
strcpy(m_data,str);
}
else
{
m_data=new char[1];
m_data[0]='\0';
}
}
//拷贝赋值函数
String& String::operator=(const String& str)
{
cout<<"copy assignment function"<<endl;
if(this==&str)
{
return *this;
}
delete[] m_data;
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
return *this;
}
//拷贝构造函数
String::String(const String& str)
{
cout<<"copy construct function"<<endl;
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
}
//析构函数
String::~String()
{
cout<<"disconstruct function"<<m_data<<endl;
delete[] m_data;
}
这里谈谈关于几个函数的手法:
- 拷贝赋值函数(赋值运算符重载)
首先检查时候是自我赋值。这个是侯捷老师说的。写出这条语句的都是一些老手。我们得锻炼成这种有大家风范的代码。(特别喜欢老师说这个话的时候的表情)大家的第一感觉就是不写的话不就是会多做几次复制而已吧,但是的情况不是这样的。原因我下面说。
其次,释放自己的赋值运算符左边的内容。因为左值是要被修改的,所以,先释放自己的空间,避免内容混乱
然后根据赋值运算符的右值,去计算要给m_data分配多少的空间。然后实现复制内容
注意到第二步,我们是要释放自己的空间的。如果你一开始没有做自我赋值的判断,然后程序运行先释放了自己的空间,之后的strlen(str.m_data)就会报错的。
这个是运行结果:
这个结果相信大家都是很清楚怎么来的,并且这个代码看起来也是健壮性十分强大的。但是我这里提出几个问题,看看程序是否会运行出错。
如果在主函数中增加如下代码:
//这里假设上面的代码中主函数什么都没有写(不然变量名就重复了)
String A;
String B;
String C("hello");
C=A; //①
B=C; //②
A=B; //③
String *p=new String[3]; //④
delete[] p; //④
大家觉得会报错吗(当然会报错,不然我写出来干嘛)?
语句①:显然,这里会调用我们的拷贝赋值函数。但是看到函数内部是会计算字符串的长度,但是我的A调用的是无参构造函数,m_data=NULL;所以,程序报错。
语句②:发现其实他是没有问题的。这里我想要强调一点的是:
int *p=NULL;delete p;
是没有问题的,此时编译器看到是delete NULL;就什么都不会做。因为我一开始是认为他会报错的。
语句③:原理和①是一样的,也会报错。我之后将修改后的函数补上去。
语句④:这个我主要是想要强调一个delete *p此时应该会做的一些事情。
首先,在书本,可能会说:这个时候,delete关键字会做两件事情:一:调用析构函数;二:释放你new出来的空间。我画一个图就好理解了。
可以看到,其实这条语句在分配了两次堆区域的内容。因为new在堆分配空间,所以String[0],String[1],String[2]都在堆的位置,因为String[0],String[1],String[2]的成员变量m_data也是使用new关键字,所以分配的内存也在堆内存中。
这里还涉及到一个问题,为什么new []后要配合delete[]释放内存空间。就像侯捷老师说的一样,很多仅仅知道不这样搭配会造成内存泄漏,但是可能不是你所想的内存泄漏。不管你使用的是delete[]还是delete都是会将0x1122这个地址的内存释放,因为,p指向的这个位置。但是你如果使用的是delete,那么就仅仅是String[0]指向的内存hello\0会被释放,后面的都不会被释放。仅仅调用一次析构函数。(如图一)但是你写的是delete[]话,编译器就会知道,后面还有需要释放的内存,所以,将会调用三次析构函数。(如图二)
注意:这里我使用的是不同的编辑器,因为我发现dev c++对于你写的delete p还是会调用了三次析构函数。应该编译器自己做出了优化。但是,理论上分析是没有问题的。
这里给出优化后的代码:
#include<iostream>
#include<string.h>
#include"String.h"
using namespace std;
/*
因为编译的原因,最好在vs上面也测试一哈,比较保险。
*/
int main()
{
/*
String A("hello");
String B(A);
String C("world");
String* D=new String("suliangkuan");
C=B;
cout<<"i am string A: "<<A.get_char()<<endl;
cout<<"i am string B: "<<B.get_char()<<endl;
cout<<"i am string C: "<<C.get_char()<<endl;
delete D;
*/
String *p=new String[3];
delete p;
p=NULL;
cout<<p;
/*
String *p=new String[3]{String("hello"),String("world"),String("su")};
delete []p;
*/
/*
String A;
String B("hello");
A=B;
*/
/*
String a;
String b;
a = b;
*/
return 0;
}
String::String()
{
//这一步也是十分重要的 (如果不要的话,就会导致你在想要输出m_data时遇到麻烦!)
m_data=NULL;
cout<<"default constructer function"<<endl;
}
String::String(const char* str=0)
{
cout<<"i am construct function"<<endl;
if(str){
m_data=new char[strlen(str)+1];
strcpy(m_data,str);
}
else
{
m_data=new char[1];
m_data[0]='\0';
}
}
/*
//拷贝赋值函数
String& String::operator=(const String& str)
{
cout<<"copy assignment function"<<endl;
if(this==&str)
{
return *this;
}
delete[] m_data;
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
return *this;
}
*/
//最完美的拷贝赋值函数
String& String::operator=(const String& str)
{
cout<<"copy assignment function"<<endl;
if(this==&str)
{
return *this;
}
delete[] m_data;
//String A("hello");String B; A=B;
if(str.m_data==NULL)
{
m_data=new char[1];
m_data[0]='\0';
}
//String A("hello");String B("wolrd");A=B; or String A; A=B;
else
{
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
}
return *this;
}
//拷贝构造函数
String::String(const String& str)
{
cout<<"copy construct function"<<endl;
m_data=new char[strlen(str.m_data)+1];
strcpy(m_data,str.m_data);
}
//析构函数
String::~String()
{
cout<<"disconstruct function";
/*
因为调用默认初始化的时候,m_data赋值为了NULL后
在这里就十分容易判断了。如果m_data==NULL
你直接cout<<m_data<<endl;就是会报错的
*/
if(m_data!=NULL)
{
cout<<m_data<<endl;
}
else
{
cout<<endl;
}
if (m_data != NULL)
{
delete[] m_data;
}
}
后面的就是一些小细节了,我记录下来以便之后查看:
一般来说,因为自己肯定要写一个构造函数的,所以为了避免无参对象的无法构建,所以,自己一定得还写一个无参的构造函数。而且里面的指针一定得赋值为NULL。这个是一个好习惯。至于为什么,可以去看自己那篇有关指针的文章。
在show()这种输出函数的时候,如果有涉及的指针数据的输出,一定记得先去判断这个指针是否为空,因为cout<<m_data<<endl;如果m_data==NULL的话。但是你直接cout<<NULL; int *p=NULL; cout<<p;都是不会报错,输出为0.应该是编译器做了隐士转化。
这篇文章可以和指针那篇文章一起看。