“学习不是生活的一部分,而是构建生活的全部。”——约翰·杜
文章目录
- 第12章:类和动态内存分配
- 第1版:Stringbad类
- 错误
- 小知识点:new和delete的对应使用
- 第二版:String类
- 构造函数中使用new时的注意事项
第12章:类和动态内存分配
在这一章中,我们逐步构造了一个自己的简易版String类,这个类在构造和析构函数中频繁使用new和delete运算符,因此可以引出一些在类中使用动态内存分配所隐含的问题。
第1版:Stringbad类
Stringbad类使用动态内存分配完成了构造和析构操作,但也引发了一些问题。下面是Stringbad类声明:
#include<iostream>
#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<cstring>
#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;
}
- 在每个构造函数中,递增num_strings;每个析构函数中递减num_strings
- 以传入参数的构造函数为例,先用字符串s初始化len和str,初始化str时使用new运算符分配了足够存储字符串的内存。然后用strcpy函数进行字符串拷贝,递增num_strings,输出字符串信息。注意,不能直接用“str=s”,这样相当于拷贝了地址,而非字符串本身。
- 对于析构函数,则先输出要析构的字符串信息,递减num_strings,最后调用delete运算符进行内存释放。注意,当对象被析构时,str指针会被自动释放,但分配的内存不会,因此会造成内存泄露,所以在C++中要注意一个new对应一个delete,这点和java不同。
以下是对StringBad类的测试程序:
#include<iostream>
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;
}
这会导致两个问题:
- num_strings没有递增,但临时对象在析构时也要调用构造函数,会造成num_strings递减。
- 直接将s的字符串指针地址传给临时对象,这样的话临时对象析构时会将s的字符串释放掉,使其无法识别。
我们称编译器自动提供的拷贝构造函数为浅拷贝,只复制了字符串地址而没有复制字符串本身,因此我们需要提供一个深拷贝的构造函数:
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和delete的对应使用
在使用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类。下面是String类的头文件:
#include <iostream>
#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
- 仿照标准程序库中的String类重载了>,<,==三个运算符,以字典序比较两个字符串。
- 也是仿照String类重载了[]运算符,通过[]运算符能访问字符串中特定的元素。分别重载const版本和普通版本是为了访问const字符串,如果没有声明为const的函数,用[]运算符访问const字符串时将出错:
cout<<const_string[1];//如果这个类对象是const类型的,那么就必须用const版本的函数
- 注意这一个函数:
static int HowMany();//展示静态类数据成员num_string
这是一个和num_strings相似的静态函数,这个函数只能由类调用,不能通过对象调用,也因为这个原因它只能调用类的静态数据成员:
String::HowMany();//通过String类直接调用
- 增加参数为const char*的赋值运算符重载,使得程序能直接赋值=运算符给String对象,而不用先构造出临时对象再赋值。
- 静态数据成员CINLIM表示对输入(cin)的限制(limit),具体做法看实现。
以下是String类的实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include<cstring>
#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。
构造函数中使用new时的注意事项
- 构造函数中使用new,则析构函数中需要使用delete
- new对应delete,new[]对应delete[]
- 多个构造函数必须用同一种方式使用new,即要么都用new,要么都用new[],因为析构函数只能有一个,其中要么用delete,要么用delete[]。
- 当判断浅拷贝和浅赋值不够用时,需要自定义深度拷贝和深度赋值运算符重载函数。
我是霜_哀,在算法之路上努力前行的一位萌新,感谢你的阅读!如果觉得好的话,可以关注一下,我会在将来带来更多更全面的知识讲解!