C++ :String类中的操作及浅拷贝,深拷贝,写时拷贝

一、String类的定义及其操作

1.出现的原因

    C语言中,字符串通常都是以'\0'结尾得一些字符的集合,为了操作简单,方便,C标准库提供了一系列的库函数,但是这样使得字符串与处理这些字符串的函数是分开的,不符合OOP思想,而且底层还需要自己去维护管理,稍不注意还会出现越界访问。因此,在C++中对这个进行了优化。

2.特性

   C中 char* 是一个指针,而C++中string是一个类,string对char*进行封装 ,string封装了许多实用的方法,不用考虑内存释放与越界问题,string管理char* 所分配的内存,每一次都是string复制,取值都由string类来维护,不用担心复制越界与取值越界。为了在程序中使用string类型,必须包含头文件 <string>。string类是一个模板类,位于名字空间std中,通常为方便使用还需要增加: using namespace std;

string类的意义有两个,第一个是为了处理char类型的数组,并封装了标准C中的一些字符串处理的函数。而当string类进入了C++标准后,它的第二个意义就是一个容器。

3.总结

  •  string是表示字符串的字符串类
  • 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
  • string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator>string;
  •  不能操作多字节或者变长字符的序列。
  • 在使用string类时,必须包含头文件以及using namespace std;

4.String类简单实现代码


class String;  //String类的声明
 
///
class Iterator    //迭代器类
{
public:
	Iterator(String& str, int idx) :pstring(&str), index(idx){}   //传入参数String类字符串和下标位置

	bool operator!=(const Iterator& rhs)
	{
		return index != rhs.index;
	}

	char& operator*();

	const Iterator operator++(int)
	{
		const Iterator tmp(*this);
		index++;
		return tmp;
	}
private:
	String* pstring;
	int index;
};

char& Iterator::operator*()    //解引用
{
	return (*pstring)[index];
}

int main()
{
	String str("hello world!");
	String::iterator it = str.begin();
	while (it != str.end())
	{
		std::cout << *it;
		it++;
	}
	std::cout << std::endl;
	return 0;
}

///

class String    ///String类

{
public:
	typedef Iterator iterator;
	String()
	{
		pstr = new char[1] ( );
	}
	String(char* str)
	{
		pstr = new char[strlen(str) + 1] ( );
		strcpy(pstr, str);
	}
	String& operator=(const String& rhs)
	{
		if (this != &rhs)
		{
			delete[] pstr;
			pstr = new char[strlen(rhs.pstr) + 1];
			strcpy(pstr, rhs.pstr);
		}
		return *this;
	}
	String(const String& rhs)
	{
		pstr = new char[strlen(rhs.pstr) + 1]();
		strcpy(pstr, rhs.pstr);
	}
	~String()
	{
		delete[] pstr;
		pstr = NULL;
	}

	iterator begin()
	{
		return iterator(*this, 0);
	}
	iterator end()
	{
		return iterator(*this, strlen(pstr));
	}

	const String operator+(const char* prhs)
	{
		int len = strlen(pstr) + strlen(prhs) + 1;
		char* ptmp = new char[len]();
		strcat(ptmp, pstr);
		strcat(ptmp, prhs);
		String tmp(ptmp);
		delete[] ptmp;
		return tmp;
	}
	bool operator<(const String& rhs)
	{
		return strcmp(pstr, rhs.pstr) < 0;
	}
	bool operator!=(const String& rhs)
	{
		return strcmp(pstr, rhs.pstr) != 0;
	}
	char& operator[](int index)
	{
		return pstr[index];
	}
private:
	char* pstr;
	friend const String operator+(const char*, const String&);
	friend std::ostream& operator<<(std::ostream&, const String&);
};

const String operator+(const char* plhs, const String& rhs)
{
	int len = strlen(plhs) + strlen(rhs.pstr) + 1;
	char* ptmp = new char[len]();
	strcat(ptmp, plhs);
	strcat(ptmp, rhs.pstr);
	String tmp(ptmp);
	delete[] ptmp;
	return tmp;
}

std::ostream& operator<<(std::ostream& out, const String& rhs)
{
	out << rhs.pstr;
	return out;
}
std::istream& operator>>(std::istream& in, String& rhs)
{
	char ptmp[1000] = {0};
	in >> ptmp;
	rhs = String(ptmp);
	return in;
}




int main()
{
	String str1;
	String str2("hello");
	str1 = str2 + " world";
	str1 = "hi " + str2;
	if (str1 < str2)
	{
		std::cout << str1 << std::endl;
	}
	if (str1 != str2)
	{
		std::cout << str1[0] << std::endl;
	}
	std::cin >> str1;
	std::cout << str1 << std::endl;
	return 0;
}

二、浅拷贝

1.定义

  所谓浅拷贝,是指原对象与拷贝对象公用一份实体,仅仅是对象名字不同而已(类似引用,即对原对象起别名),指向同一块内存地址,其中任何一个对象改变都会导致其他的对象也跟着它变。当一个对象将这块内存释放掉之后,另一些对象不知道该块空间是否已经归还给系统,以为还有效,所以在对这段内存进行操作的时候,发生了访问违规。

2.针对进行代码分析

class String
{
public:
	String(const char *pStr = " ")//构造函数
	{
		if(pStr != NULL)//字符串不为空
		{
			_pStr = new char[strlen(pStr) + 1];
			strcpy(_pStr,pStr);
		}
		else//字符串为空
		{
			_pStr = new char[1];
			_pStr = '\0';
		}
	}

	String(const String& s)//拷贝构造函数
	{
		_pStr = s._pStr;
	}
	
	
    String& operator=(const String& s)     //重载赋值运算符=号
       {
	    if(this != &s)                 //判断是否自己给自己赋值
	    {
		_pStr = s._pStr;
	    }
	    return *this;
      }
private:
	char *_pStr;
};

void FunTest()
{
	String s1("Hello world");
	String s2(s1);
	String s3 = s2;
	String s4 = s3;
}
int main()
{
	FunTest();
	system("pause");
	return 0;
}

3. 具体原因

4.浅拷贝问题总结

①浅拷贝只是拷贝了指针,使得两个指针指向同一个地址,这样在对象块结束,调用函数析构的时候,会造成同一份资源析构多次,即delete同一块内存多次,造成程序崩溃。
②浅拷贝使得S1、S2和S3指向同一块内存,任何一方的变动都会影响到另一方。
③在释放内存的时候,会造成原有的内存没有被释放也不走默认构造函数,走的是默认的拷贝构造函数,造成内存泄露。
 

三、String类加入引用计数的浅拷贝的分析与实现

1.引用计数原理

  当类里面有指针对象时,进行简单赋值的浅拷贝,两个对象指向同一块内存,存在崩溃的问题!为了解决这个问题,我们可以采用引用计数。在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源

从上我们可以看出浅拷贝存在一定的问题,那么怎样对它进行改进防止一个对象被多次释放呢,你可能会这样想。

2.浅拷贝第一次尝试(引用计数(_count作为普通成员变量)error)

代码:

class String    //浅拷贝(引用计数(_count作为普通成员变量)error)
{
public:
	String(const char* pStr = ""):_pStr(new char[strlen(pStr)+1]),_count(0) //构造函数,_count初值赋值为0
		
	{
		if(0 == *pStr)
		{
			*_pStr = '\0';
		}
		else
		{
			strcpy(_pStr,pStr);
		}
		_count++;    //每创建一个对象计数器加1
	}

	String(String& s)//拷贝构造
		:_count(s._count)//将已存在的对象s的计数器赋给当前对象
	{
		_pStr = s._pStr;
		s._count++;//将原对象的计数器加1
		_count = s._count;//将原对象的计数器加1后赋值给当前对象
	}
	~String()//析构函数
	{
		if(NULL == _pStr)
		{
			return;
		}
		else
		{
			if(--_count == 0)//如果计数器为0,说明无对象指向该空间,可以释放
			{
				delete []_pStr;
				_pStr = NULL;
			}
		}
	}
	String& operator=(String& s)//赋值运算符重载
	{
		if(_pStr != s._pStr)
		{
			_pStr = s._pStr;
			s._count++;//将原对象的计数器加1
			_count = s._count;//将已存在的对象s的计数器赋给当前对象
		}
		return *this;
	}
private:
	int _count;//给一个计数器控制析构函数
	char* _pStr;
};
 
 
void Funtest()
{
	String s1("abcd");
	String s2(s1);
	String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
	String s4;//s4对象已经存在了
	s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
	Funtest();
	system("pause");
	return 0;
}

4个对象创建后: 

 调用4次析构函数之后:

???????  我们知道,这四个对象本来指向同一块空间,计数器本来都应为4,可是现在的结果却是计数器只能控制与它相邻对象的计数器,对象创建完成后,计数器并不统一。

     其次,调用4次析构函数之后,本来应该四个对象同时被释放,可是结果却是没有一个对象的计数器为0,也就是这块空间并没有被释放,内存又泄露了呗。为了保持计数器的统一,我们决定把计数器设置为类的静态成员函数,

 

2.浅拷贝第二次尝试(引用计数(_count作为静态成员变量)error)

代码:

//浅拷贝(引用计数(_count作为静态成员变量))
class String
{
public:
	String(const char* pStr = "")//构造函数
		:_pStr(new char[strlen(pStr)+1])
	{
		if(0 == *pStr)
		{
			*_pStr = '\0';
		}
		else
		{
			strcpy(_pStr,pStr);
		}
		_count++;
	}

	String(const String& s)//拷贝构造
	{
		_pStr = (char*)s._pStr;
		s._count = _count;
		_count++;
	}

	~String()//析构函数
	{
		if(NULL == _pStr)
		{
			return;
		}
		else
		{
			if(--_count == 0)
			{
				delete []_pStr;
				_pStr = NULL;
			}
		}
	}

	String& operator=(String& s)//赋值运算符重载
	{
		if(_pStr != s._pStr)
		{
			_pStr = s._pStr;
			s._count = _count;
			_count++;
		}
		return *this;
	}
private:
	static    int _count;
	char* _pStr;
};
 
int String::_count = 0;
 
void Funtest()
{
	String s1("abcd");
	String s2(s1);
	String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
	String s4;//s4对象已经存在了
	s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
	Funtest();
	system("pause");
	return 0;
}

结果:

分析:

1.我们一共只创建了4个对象,可是计数器却为5,那是因为静态成员变量为所有对象共享,任何对象都可以对它进行修改,每创建一个对象我们都对计数器加1,却忽略了创建的新对象是否与已存在的对象占同一块空间

2.调用4次析构函数后,计数器值为1,导致空间又没有被释放。很奇怪的一个现象是,在调用4次构造函数之后,计数器count居然等于1,也就是说调用4次析构函数并没有释放掉我们的空间。抛开失败的阴霾不说,原来我们创建的静态成员变量为所有对象所共有,任何对象都可以修改它,当我们每次创建对象计数器+1的时候,却忽视了创建的新对象是否与己存在的对象占用同一块空间,导致我们定义4个对象却要析构5次的尴尬。

3.浅拷贝第二次尝试(引用计数(_count作为指针)error)

1. 为什么要引入指针?

静态成员变量的引入最终带来的是析构调用次数的增加,那么指针的引入无疑是为了克服这一难关。但是指针的引入却增加了空间的开销,大部分程序员都知道引入指针也就相当于让程序存在了安全隐患,各种资源的释放以及释放之后防止野指针的问题同样重要。

2.代码:

//浅拷贝(引用计数(指针实现计数))
class String
{
public:
	String(const char* pStr = "")//构造函数
		:count(new int(0))
		,_pStr(new char[strlen(pStr)+1])
	{
		if(NULL == pStr)
	    {
			*_pStr = '\0';
		}
		else
		{
			strcpy(_pStr,pStr);
		}
		*count = 1;
	}
	String(const String& s)//拷贝构造
		:count(s.count)
	{
		_pStr = (char*)s._pStr;
		count = s.count;
		(*count)++;
	}
	~String()//析构函数
	{
		if(NULL == _pStr)
		{
			return;
		}
		else
		{
			if(--(*count) == 0)
			{
				delete[]count;//勿忘了释放计数器指针
				delete[]_pStr;
				_pStr = NULL;
				count = NULL;
			}
		}
	}
	String& operator=(String& s)//赋值运算符重载
	{
		if(_pStr != s._pStr)
		{
			_pStr = s._pStr;
			count = s.count;
			(*count)++;
		}
		return *this;
	}
private:
	int* count;
	char* _pStr;
};
 
 
void Funtest()
{
	String s1("abcd");
	String s2(s1);
	String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
	String s4;//s4对象已经存在了
	s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
int main()
{
	Funtest();
	system("pause");
	return 0;
}

3.结果:

给s4赋值后,_cout被置为4,调用4次析构函数之后,空间被准确释放。但是指针计数有一个缺陷缺陷:每个对象都得为它多创建一个指针,浪费空间。还有释放麻烦,很有可能我们只记得释放_pStr,却忘了释放计数器指针造成内存泄漏,得不偿失。

接下来,对浅拷贝再次进行优化。

四、写时拷贝(写时拷贝完美诠释String类的浅拷贝)

1.浅拷贝的艰辛历程

从简单的赋值浅拷贝(内存被多次析构释放)->加入引用计数的浅拷贝实现(计数器难以统一)->利用引用计数及静态成员变量浅拷贝实现(忽略了静态成员变量为所有成员所共享)->引用计数利用指针的浅拷贝实现(功能实现了但开辟了存放指针的空间,且不安全)->写时拷贝。一代一代的过渡终于可以完美的实现Strng类的浅拷贝了。

2.定义

写时拷贝就是一种拖延症,是在浅拷贝的基础之上增加了引用计数的方式来实现的。使用了引用计数,在开辟空间时会多开辟4个字节用来保存引用计数的值。当第一个对象构造时,String的构造函数会根据传入的参数从堆上分配内存,当有其他对象需要这块内存时,计数器++,当有对象进行析构释放时,引用计数--,直到最会一个对象被析构,此时计数器为0。只有这样程序才会真正的释放掉这块从堆上分配的内存。

参考:Linux下写时拷贝

          写时拷贝详解

           写时拷贝缺陷详解

3.代码:

//写时拷贝(仿照new[]实现)
class String
{
public:
	String(const char* pStr = "")//构造函数
	     :_pStr(new char[strlen(pStr) + 4 + 1])//每次多创建4个空间来存放当前地址有几个对象
	{
		if(NULL == pStr)
		{
			(*(int*)_pStr) = 1;//将前4个字节用来计数
			_pStr += 4;//指针向后偏移4个字节
			*_pStr = '\0';
		}
		else
		{
			(*(int*)_pStr) = 1;//将前4个字节用来计数
			_pStr += 4;//指针向后偏移4个字节
			strcpy(_pStr,pStr);//将pStr的内容拷贝到当前对象的_pStr中
		}
	}
	String(const String& s)//拷贝构造
		:_pStr(s._pStr)
	{
		++(*(int*)(_pStr-4));//向前偏移4个字节将计数加1
	}
	~String()//析构函数
	{
		if(NULL == _pStr)
		{
			return;
		}
		else
		{
			if(--(*(int*)(_pStr - 4)) == 0)//向前偏移4个字节判断计数是否为0,是0则释放
			{
				delete (_pStr-4);
				_pStr = NULL;
			}
		}
	}
	String& operator=(const String& s)//赋值运算符重载
	{
		if(_pStr != s._pStr)
		{
			if(--(*(int*)(_pStr - 4)) == 0)//释放旧空间
			{
				delete (_pStr-4);
				_pStr = NULL;
			}
		    _pStr = s._pStr;//指向新空间
			++(*(int*)(_pStr - 4));//计数加1
		}
		return *this;
	}
	char& operator[](size_t index)//下标访问操作符重载
	{
		assert(index>=0 && index<strlen(_pStr));
		if(*((int*)(_pStr-4)) > 1)//说明有多个对象指向同一块空间
		{
			char* temp = new char[strlen(_pStr) + 4 + 1];//新开辟一块空间
			temp += 4;//先将开辟的空间向后偏移4个字节
			strcpy(temp,_pStr);//将_pStr的内容拷贝到temp中
			--(*(int*)(_pStr-4));//将原来空间的计数器减1
 
			_pStr = temp;//将当前对象指向临时空间
			*((int*)(_pStr-4)) = 1;//将新空间的计数器变为1
		}
		return _pStr[index];
	}
private:
	char* _pStr;
};
 
void Funtest()
{
	String s1("abcd");
	String s2(s1);
	String s3 = s2;//调用拷贝构造函数(编译器会s2直接初始化s3)
	s3[2] = 'g';
	String s4;//s4对象已经存在了
	s4 = s3;//编译器会调用赋值运算符重载将s3的值赋给s4
}
 
int main()
{
	Funtest();
	system("pause");
	return 0;
}

 

五、深拷贝

所谓深拷贝,就是为新对象开辟一块新的空间,并将原对象的内容拷贝给新开的空间,释放时就不会牵扯到多次析构的问题。

 

        /*1 
	String& operator=(const String &s)//赋值运算符重载
	{
		if(_pStr != s._pStr)
		{
			String temp(s._pStr);//如果不给出临时变量,交换后s的值将为NULL
		    std::swap(_pStr,temp._pStr);
		}
		return *this;
	}

         */	
        /* 2
	String& operator=(const String& s)
	{
	if (this != &s)
	{
	String temp(s);
	std::swap(_pStr, temp._pStr);
	}
	return *this;
	}*/
 

	/* 3
	String& operator=(String temp)
	{
		std::swap(_pStr, temp._pStr);
		return *this;
	}*/

 

 

 

https://blog.csdn.net/snow_5288/article/details/52901528

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值