“学习不是生活的一部分,而是构建生活的全部。”——约翰·杜
在这一章中,我们逐步构造了一个自己的简易版String类,这个类在构造和析构函数中频繁使用new和delete运算符,因此可以引出一些在类中使用动态内存分配所隐含的问题。
Stringbad类使用动态内存分配完成了构造和析构操作,但也引发了一些问题。下面是Stringbad类声明:
#include
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
class StringBad
{
private:
char* str;//pointer to string
int len;
static int num_strings;//number of objects
public:
StringBad(const char* s);
StringBad();
~StringBad();
friend std::ostream& operator<<(std::ostream& os, const StringBad& st);
};
#endif
注意,在声明中加入了一个静态成员变量,这种变量独立于所有对象存储,被所有对象所共享,它表示已经创建的对象的个数。
以下是StringBad类的定义:
#include
#include "stringbad.h"
using namespace std;
//初始化静态类成员
int StringBad::num_strings = 0;
//类方法
StringBad::StringBad(const char* s)
{
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
num_strings++;
cout << num_strings << ":\"" << str << "\"object created\n";
}
StringBad::StringBad()
{
len = 4;
str = new char[4];
strcpy(str, "C++");
num_strings++;
cout << num_strings << ":\"" << str << "\"object created\n";
}
StringBad::~StringBad()
{
cout << "\"" << str << "\" object deleted, ";
--num_strings;
cout << num_strings << "left\n";
delete[] str;
}
std::ostream& operator<<(std::ostream& os, const StringBad& st)
{
os << st.str;
return os;
}
以下是对StringBad类的测试程序:
#include
using std::cout;
#include"stringbad.h"
void callme1(StringBad&);//引用传参
void callme2(StringBad);//值传参
int main(void)
{
using std::endl;
{
cout << "Starting an inner block.\n";
StringBad headline1("Celery Stalks at Midnight");
StringBad headline2("Letture Prey");
StringBad sports("Spinach Leaves Bowl for Dollars");
cout << "headline1: " << headline1 << endl;
cout << "headline2: " << headline2 << endl;
cout << "sports: " << sports << endl;
callme1(headline1);
cout << "headline1: " << headline1 << endl;
callme2(headline2);
cout << "headline2: " << headline2 << endl;
cout << "Initialize one object to another:\n";
StringBad sailor = sports;
cout << "sailor: " << sailor << endl;
cout << "Assign one object to another:\n";
StringBad knot;
knot = headline1;
cout << "knot: " << knot << endl;
cout << "Exiting the block.\n";
}
cout << "End of main()\n";
return 0;
}
void callme1(StringBad& rsb)
{
cout << "String passed by reference:\n";
cout << " \"" << rsb << "\"\n";
}
void callme2(StringBad sb)
{
cout << "String passed by value:\n";
cout << " \"" << sb << "\"\n";
}
输出结果:
注意最后几行的输出信息,很明显出现了问题,初步判断是在调用析构函数时字符串的内容出了问题,而且num_strings最后的值是-2,也不对。
哪里出错了?
StringBad类的错误是由特殊成员函数引起的。如果编译器判断程序使用对象时需要这些函数且程序员没有写,C++就会自动提供。C++中的特殊成员函数有:
这个程序的问题就在于编译器自动提供了复制构造函数和赋值运算符,但提供的不对。
注意下面的两段节选代码:
callme2(headline2)
对于这条语句,调用了callme2函数,以值传参的方式传入了headline2。我们都知道,值传参时隐式调用复制构造函数构造一个临时对象。但编译器自动提供的复制构造函数显然会是这样的:
StringBad(StringBad s)
{
str=s.str;
len=s.len;
}
这会导致两个问题:
我们称编译器自动提供的拷贝构造函数为浅拷贝,只复制了字符串地址而没有复制字符串本身,因此我们需要提供一个深拷贝的构造函数:
StringBad(StringBad s)
{
num_strings++;//解决第一个问题
len=s.len;
str=new char[len+1];
std::strcpy(str,st.str);//进行字符串本身的复制,解决第二个问题
cout<<num_strings<<":\""<<str<<"\" object created\n";
}
StringBad sailor = sports;
knot = headline1;
这两条语句调用了编译器自动提供的赋值运算符重载函数。第一条语句可能有两种实现,第一种是直接进行赋值运算符;第二种是先拷贝构造一个sports的临时对象,然后再使用赋值运算符,这两种解决方式显然都会出问题,因为都调用了错误的函数。
编译器自动提供的赋值运算符重载显然应该是这样:
StringBad operator=(StringBad s)
{
str=s.str;
len=s.len;
return *this;//返回被赋值的类对象
}
这犯了同一个毛病,直接将字符串的地址进行赋值,而没有复制整个字符串进行赋值。注意,赋值运算符并没有新增一个StringBad类对象,不需要递增num_strings。
需要提供一个自己写的赋值运算符重载:
StringBad operator=(StringBad s)
{
len=s.len;
str=new char[len+1];
std::strcpy(str,st.str);//进行字符串本身的复制
return *this;//返回被赋值的类对象
}
将这两个函数加入后,StringBad类就可以正常使用。
在使用new运算符时,有两种方法:
str=new char;
str=new char[1];
这两种方法分配的内存量一样,区别在于前者要用delete运算符释放内存,后者用delete[]运算符释放内存。注意,任何隐式使用new运算符的方法都调用new而非new[]。
delete str;//兼容new运算符
delete[] str//兼容new[]运算符
delete[]运算符也兼容空指针,因此以下的代码能通过编译:
str=0;//将str置空
delete[] str;
解决了这两个问题后,我们可以完善String类。下面是String类的头文件:
#include
#ifndef STRING_H_
#define STRING_H_
class String
{
private:
char* str;//对string的指针
int len;//string的长度
static int num_strings;//字符串的数量,由变量由所有对象共享
static const int CINLIM = 80;//设置输入缓冲区限制
public:
String(const char* s);
String();
String(const String& st);
~String();
int length()const//返回被存储的字符串的长度
{
return len;
}
//下面三个友元函数对字符串进行比较
friend bool operator<(const String& st, const String& st2);
friend bool operator>(const String& st1,const String& st2);
friend bool operator==(const String& st, const String& st2);
//提供简单的输入功能
friend std::istream& operator>>(std::istream& is, String& st);
//下面两个函数提供用中括号访问字符串中各个字符的功能
char& operator[](int i);
const char& operator[](int i)const;
static int HowMany();//展示静态类数据成员num_string
friend std::ostream& operator<<(std::ostream& os, const String& st);
String& operator=(const String& st);
String& operator=(const char*);
};
#endif
cout<<const_string[1];//如果这个类对象是const类型的,那么就必须用const版本的函数
static int HowMany();//展示静态类数据成员num_string
这是一个和num_strings相似的静态函数,这个函数只能由类调用,不能通过对象调用,也因为这个原因它只能调用类的静态数据成员:
String::HowMany();//通过String类直接调用
以下是String类的实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include "string.h"
using namespace std;
//初始化静态类成员
int String::num_strings = 0;
//类方法,从C的String构造String
String::String(const char* s)
{
len = strlen(s);//设置长度
str = new char[len + 1];//分配存储空间
strcpy(str, s);//初始化指针
num_strings++;//设置字符串数量
}
String::String()
{
len = 0;
str = new char[1];
str[0] = '\0';
num_strings++;
}
String::String(const String& st)
{
num_strings++;//设置静态变量
len = st.len;//设置相同长度
str = new char[len + 1];//开辟内存
strcpy(str, st.str);//将字符串复制到新的地址
}
String::~String()
{
--num_strings;//required
delete[]str;//required
}
bool operator<(const String& st1, const String& st2)
{
return(strcmp(st1.str, st2.str) < 0);
}
bool operator>(const String& st1, const String& st2)
{
return st2 < st1;
}
bool operator==(const String& st1, const String& st2)
{
return(strcmp(st1.str, st2.str) == 0);
}
istream& operator>>(istream& is, String& st)
{
char temp[String::CINLIM];
is.get(temp, String::CINLIM);
if (is)
st = temp;
while (is && is.get() != '\n')
continue;
return is;
}
char& String::operator[](int i)
{
return str[i];
}
const char& String::operator[](int i)const
{
return str[i];
}
int String::HowMany()
{
return num_strings;
}
ostream& operator<<(ostream& os, const String& st)
{
os << st.str;
return os;
}
String& String::operator=(const String& st)
{
if (this == &st)//对象指向自己
return *this;//如果不额外声明一个if,那么给对象重新赋值时,释放内存操作可能删除对象的内容
delete[]str;//释放旧的字符串,以将新的字符串复制到地址
//进行新的赋值操作
len = st.len;
str = new char[len + 1];//为新字符串开辟内存空间
strcpy(str, st.str);
return *this;//返回对调用对象的引用
}
String& String::operator=(const char* s)
{
delete[]str;
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
return *this;
}
其他的看一看也就行了,自己写都能写出来,主要是>>运算符的重载,有一个get函数:
is.get(temp, String::CINLIM);
get函数读取CINLIM个字符到字符串temp中。读取失败则将is置为false。
我是霜_哀,在算法之路上努力前行的一位萌新,感谢你的阅读!如果觉得好的话,可以关注一下,我会在将来带来更多更全面的知识讲解!