1.类机制支持新类型的设计,其实,在面向对象的思想里,任何事物都是对象。包括一种类型,比如int型,也是一种对象。所以,我们应该把类和int型这样的类型辩证联系的看待。
2.本节以String类(注意,是大写的,标准库是string)为例(这个类是我们自己写的),介绍它的抽象的设计与实现。实现将着重说明C++对操作符重载(运算符重载)的支持。
3.String类需要完成的任务,或者说它需要具有的功能:
初始化
赋值,包括用字符串文字(string)和C风格字符串,或者是另一个String对象进行初始化或者赋值
支持用索引访问String中的单个字符
确定String长度(size())
两个String对象的相等比较,或者String同C风格字符串,或者string字符串比较。
读写一个String对象
访问底层的C风格字符串。
接下来我们便来一步步实现这个类。
4.一般的,一个类包括共有的(public)的操作部分和私有的(private)数据部分。这些操作被称为该类的成员函数(member function)或方法(method),他们定义了类的公有接口(public interface)——即,用户可以在该类对象上执行的操作的集合。
5.我们的String类的私有数据包括:_string,一个只想动态分配的字符数组的char*类型的指针。_size,记录String中字符串长度的int型变量。
现在我们来初步写下这个类(注意,我们目前为止还没出现过String这个类,所以我们有很多操作要借助于已有的char*字符串和string字符串。
class String
{
public:
//三个构造函数,提供了自动初始化的功能
String();
String(const char*);
String(const String&);
//析构函数,自动析构
~String();
//一组重载的赋值操作符
String & operator=(const String&);
String & operator=(const char*);
//一组重载的等于操作符
bool operator==(const String&);
bool operator==(const char*);
//重载的下标操作符
char &operator[]{(int);
//成员访问函数
int size(){return _size};
char* c_str(){return _string};
private:
int _size;
char *_string;
}
6.接下来我们来详细解释一下这个函数的功能
首先是构造函数,第一个构造函数是缺省构造函数。他不需要做任何显式的初始值。当我们写下String str1;的时候,我们调用的解释缺省的构造函数。
第二个构造函数采用char*类型的字符来初始化。第三个构造函数被称为拷贝构造函数。因为它用另一个对象的拷贝来初始化一个对象。
被重载的操作符采用下面的一般形式:
return_type operator op(parameter_list); 即返回类型+operator关键字+被重载的符号+参数列表。
类的成语那函数可以被定义在类中,也可以定义在类的外面(但是都必须先在类之中声明)。在类定义之外定义的成员函数不但要告诉编译器它们的名字,返回类型,参数表,而且还要说明它们所属的类。一般情况下,我们应该把类成员函数的定义放到一个程序文本文件中(汝String.cpp),并且把含有该类定义的头文件(String.h)包含进来。比如我们写一下重载==的函数
#include “String.h"
#include<cstring>
//以下是==重载函数的具体代码
bool String::operator==(const String &rhs)//其实就是在返回类型后面,函数名字前面加上了类名以及::符号。
{
if(_size!=rhs._size)
return false;
return strcmp(_string,rhs._string)?false:true;
}
此处先注意?:表达式,前面是判断语句,前面为true则返回第一个选项,前面的语句为false,则返回第一个选项。
在注意strcmp()函数,他比较两个字符串(C风格)若相等,返回0(也就是false),否则返回true.所以,当这两个字符相等的时候,因为返回的是0(false),所以会使得问号表达式选择后面的选项,也就是true.反之亦然。
7、因为等于操作符是个可能要频繁调用的小函数,所以我们把它声明为内联(inline)函数是个好办法。内联函数会在每个调用点上被展开,因此,这样做可以消除函数调用相关的额外消耗。只要该函数被调用足够多次,内联函数能显著的提高性能,在类外定义的成员函数如果想声明为内联函数,必须显示的生命为inline。在类内定义的函数本来就是被缺省的定义为内联函数了。
inline bool
String::operator==(const String &rhs)
{
// 如前
}
8.在类体外定义的内联函数,应该被写在包含有该类定义的头文件中,而不是.cpp文件中。也就是说,这个函数没有写在类里面,但是写在了类定义的那个文件中(.h文件)。并且要在之前加上inline字样。
9.构造函数的名字与类名相同。但是,注意我们不能在它的声明或构造函数体重指定返回值。它的一个或多个实例都可以被声明为inline。
比如,我们把三个构造函数的代码写出来。
// 缺省构造函数
inline String::String()
{
_size = 0;
_string = 0;
}
inline String::String( const char *str )
{
if ( ! str ) {
_size = 0; _string = 0;
}
else {
_size = strlen( str );//此处注意strlen测的是char*字符串的实际有用的长度,不包含最后的结尾字符\0。所以这也是下面需要+1的原因,否则无法复制。
_string = new char[ _size + 1 ];
strcpy( _string, str );
}
}
// 拷贝构造函数
inline String::String( const String &rhs )
{
_size = rhs._size;
if ( ! rhs._string )
_string = 0;
else {
_string = new char[ _size + 1 ];
strcpy( _string, rhs._string );
}
}
9.由于我们用了new表达式动态的分配内存来保留字符串。所以当不再需要该字符串对象的时候,我们必须用delete表达式释放该内存区。这就要用到析构函数了。
inline String::~String() { delete [] _string; }//注意,此处的【】与new[]相对应。这是对于数组而言的。
10.接下来我们写出剩下的赋值操作符的重载函数
inline String&
String::operator=( const char *s )
{
if ( ! s ) {
_size = 0;
delete [] _string;
_string = 0;
}
else {
_size = strlen( s );
delete [] _string;//注意这里,在赋值之前都把现有的对象先释放掉
_string = new char[ _size + 1 ];
strcpy( _string, s );
}
return *this;//这句是关键,返回本身,也就是返回这个被赋值的String类型对象。this是个指针,指向当前操作的对象
}
//另一种赋值操作(用一个String给另一个String对象赋值)需要考虑避免着两个对象不是同一个对象,这时候this指针又起到作用
inline String&
String::operator=( const String &rhs )
{
if ( this != &rhs )
{
delete [] _string;
_size = rhs._size;
if ( ! rhs._string )
_string = 0;
else {
_string = new char[ _size + 1 ];
strcpy( _string, rhs._string );
}
}
11.接下来是下标操作符的重载
#include <cassert>
inline char&
String::operator[]( int elem )
{
assert( elem >= 0 && elem < _size );
return _string[ elem ];//因为char*类型是支持下标操作符的。
}