前言:
本章主要阐述对类成员使用动态内存分配的技术细节,并复习前面提到的复制构造函数,赋值运算符,静态类成员,定位new运算符,并动手实现string类;
下面这个文件提供了一个不太好的string类实现方式:
#include <iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
class StringBad
{
private:
char * str;//指向字符串的指针
int len;//字符串的长度
static int num_strings;//string类对象的个数,但这个变量对类功能的实现来说不是必须的
public:
StringBad(const char * s);//使用字符串初始化对象的构造函数
StringBad();//不使用参数的构造函数
~StringBad();//析构函数
friend std::ostream & operator<<(std::ostream & os, const StringBad & st);//友元函数,作用是重载<<运算符
};
#endif
注意:无论创建了多少对象,程序都只创建一个静态的类变量副本!
下面这个文件为上面的头文件提供了类函数的定义:
#include<cstring>
#include "stringbad.h"
using std::cout;
StringBad::StringBad(const char * s)
{
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str,s);
num_strings++;
}
StringBad()
{
len = 4;
str = new char[4];
std::strcpy(str,"C++");
num_strings++;
}
~StringBad()
{
--num_strings;
delete [] str;
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os<<st.str;
return os;
}
注意:不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存;
另外,初始化类中的静态变量时不使用static关键字:
int StringBad::num_strings = 0;
//关键字static只在变量声明中出现
例外情况是:静态数据成员为整型或枚举类型,可以在类定义中提供声明;
当定义一个类时,C++自动提供这些类方法:
- 默认构造函数,如果没有定义构造函数
- 默认析构函数,如果没有定义
- 复制构造函数,如果没有定义
- 赋值运算符,如果没有定义(执行浅复制)
- 地址运算符,如果没有定义
更准确的说,编译器会生成后面三个函数定义;
复制函数用于将一个对象复制到新创建的对象中;函数原型是这样的:
Class_name(const Class_name &);
//新建一个类对象并初始化为同类现有对象时,复制构造函数会被使用
//每当程序生成了对象副本时,编译器都将使用复制构造函数
默认的复制构造函数逐个复制非静态成员,但只进行浅复制;如果类成员本身也是一个类对象,程序会智能调用相应的复制函数,默认也执行浅复制;
要进行深复制,唯一的方法是提供一个显式复制构造函数:
StringBad::StringBad(const String & s)
{
num_strings++;
...//进行显示复制,申请内存空间并复制内容
}
**必须进行深层复制的构造函数的原因是:**一些类成员是使用new初始化的,指向数据的指针,而不是数据本身;**当使用delete释放原来的对象时,会导致被赋值对象的值发生设计之外的变化;
除了复制运算符外,还有赋值运算符,函数原型是:
Class_name & Class_name::operator=(const StringBad &);
在自定义赋值构造函数时应当遵守这些规则:
- 由于目标对象可能引用了以前的数据,所以构造函数应使用delete[]来释放这些数据
- 函数应当避免将对象赋值给自身;否则,在给对象重新赋值前,释放内存操作可能删除对象的内容
- 函数返回一个指向调用对象的引用
下面的代码说明了如何为StringBad类编写赋值运算符:
StringBad & StringBad::operator=(const StringBad & st)
{
if (this == &st)
return *this;
delete [] str;
len = st.len;
str = new char [len + 1];
std::strcpy(str,st.str);
return *this;
}
现在我们可以丰富StringBad这个类了,向其中添加以下方法:
int length() const {return len}
friend bool operator<(const String &st, const String &st2);
friend bool operator>(const String &st, const String &st2);
friend bool operator==(const String &st, const String &st2);
friend operator>>(istream & is, String & st);
char & operator[](int i);
const char & operator[](int i) const;
static int HowMany();
这是修订后的默认构造函数:
String::String()
{
len = 0;
str = new char[1];
str[0] = '\0';
}
//注意到new使用了中括号,这是为了和使用delete []的析构函数兼容
//delete[]对使用new []的构造函数创建的内存空间和空指针都兼容,但对new不兼容
**表示空指针的三种方法:NULL,0,nullptr(这是C++11独有的特性,也是最建议使用的表示方法);
可以将类成员函数声明为静态的,但这样做不能通过对象调用静态成员函数,而是使用类的作用域解析运算符调用;并且由于静态函数本身不和特定的类对象相关联, 因此只能使用静态数据成员;
现在让我们完成String类的设计。
下面是类声明:
#ifndef STRING1_H_
#define STRING1_H_
#include <iostream>
using std::ostream;
using std::istream;
class String
{
private:
char * str;
int len;
static int num_strings;
static const int CINLIM = 80;
public:
String(const char * s);
String();
String(const String &);
~String();
int length() const {return len;}
String & operator=(const String &);
String & operator=(const * char);
char & operator[](int i);
const char & operator[](int i);
friend bool operator<(const String & st, const String & st2);
friend bool operator>(const String & st, const String & st2);
friend bool operator==(const String & st, const String & st2);
friend ostream & operator<<(ostream & os, const String & st);
friend istream & operator>>(istream & is,String & st);
static int HowMany();
}
上面的文件给出了类定义,下面文件给出了类方法的定义:
#include<cstring>
#include "string1.h"
using std::cin;
using std::cout;
int String::num_strings = 0;
int String::HowMany()//返回String对象的总数
{
return num_strings;
}
String::String(const char * s)//构造函数
{
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str,s);
num_strings++;
}
String::String()//构造函数
{
len = 4;
str = new char[1];
str[0] = '\0';
num_strings++;
}
String::String(const String & st)//构造函数
{
num_strings++;
len = st.len;
str = new char[len + 1];
std::strcpy(str,st.str);
}
String::~String()
{
--num_strings;
delete [] str;
}
String & String::operator=(const String & st)
{
if (this == &st)
return *this;
delete [] str;
len = st.len;
str = new char[len + 1];
std::strcpy(str,st.str);
return *this;
}
String & String::operator(const char * s)
{
delete [] str;
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str,s);
return *this;
}
char & String::operator[](int i)
{
return str[i];
}
const char & String::operator[](int i) const
{
return str[i];
}
bool operator<(const String & st1, const String & st2)
{
return (std::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 (std::strcmp(st1.str, st2.str) == 0);
}
ostream & operator<<(ostream & os, const String & st)
{
os<<st.str;
return os;
}
istream & operator>>(istream & is, String & st)
{
chat temp[String::CINLIM];
is.get(temp,String::CINLIM);
if (is)
st = temp;
while (is && is.get() != '\n')
continue;
return is;
}
下面是在构造函数中使用new的注意事项:
- 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
- new和delete必须相互兼容,new对应delete而new []对应delete []
- 如果有多个构造函数,则必须以相同的方式使用new,因为只有一个析构函数,所有构造函数都要和它兼容,不过可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空,因为delete无论带不带中括号都可以用于空指针
- 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象
- 应定义一个赋值运算符,通过深度复制将一个对象赋值给另一个对象
默认的逐成员复制和赋值有一定的智能,会在合适的场景调用合适的复制或赋值函数;
定位new运算符
当在一段已经创建了一个内容的内存空间中,再创建第二个内容会导致对象被覆盖;程序员可以手动管理内存:
pc1 = new (buffer) JustTesting;
pc3 = new (buffer + sizeof (JustTesting)) JustTesting("Better Idea", 6);
注意:delete可以和new搭配使用,但是不能和定位new搭配使用!要释放内存,只能通过释放内存段本身的方式实现;另一种解决办法是:显式的为使用定位new运算符创建的对象调用析构函数:
pc3->~JustTesting();
pc1->~JustTesting();
//注意删除顺序应当和创建顺序相反,因为后创建的对象可能是依赖于早创建的对象
//仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区