从C到C++___类和动态内存分配(一)设计一个String类

类和动态分配(一)设计一个String

对类成员使用动态内存分配会产生一系列问题。我们需要对原有类体系进行扩充。

1. 实例:StringBad

#include<iostream>
#ifndef STRNGBAD_H_
#define STRNGBAD_H_
class StringBad
{
private:
    char * str;      //指向字符串的指针
    int len;        //字符串的长度
    static int num_strings;    //对象个数
public:
    StringBad(const char * s);   //构造函数
    StringBad();                 //默认构造函数
    ~StringBad();                //析构函数
    //友元函数
    friend std::ostream & operator<<(std::ostream & os,const StringBad & st);
};
#endif // !STRNGBAD_H_
#include<cstring>
#include"类动态分配1.h"
using std::cout;
 
//初始化静态类成员
int StringBad::num_strings = 0;
 
//类方法
//以C字符型构造StringBad
StringBad::StringBad(const char * s)
{
    len = std::strlen(s);        //设置长度
    str = new char[len + 1];     //分配内存
    std::strcpy(str, s);         //初始化指针
    num_strings++;               //设置对象数量
    cout << num_strings << ": \"" << str
        << "\" object created\n";
}
StringBad::StringBad()            //默认构造函数
{
    len = 4;
    str = new char[4];
    std::strcpy(str, "C++");      //默认字符串
    num_strings++;
    cout << num_strings << ": \"" << str << "\" default 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是个不太完整的类。这个类并没有什么错误,但是忽略了一些不明显却必不可少的东西。
首先这个类中的str成员是一个指针,它指向一个内存块,这就意味著这个类本身根本没有存储字符串,而是在构造函数中使用new分配了一块内存,类本身保持的只是这块内存的地址和字符串的其他信息。
这里的num_strings成员是静态类成员
静态类成员无了创建了多少个对象,程序都不会创建静态变量副本,类的所有对象共享一个静态成员。
我们发现,在类方法定义的文件中,可以对私有成员初始化,这是不可思议的,因为私有成员不能直接访问。
int StringBad::num_strings = 0;
我们在类声明中是无法初始化静态成员变量,(ISO C++ forbids in-class initialization of non-const static member 'StringBad::num_strings'static int num_strings=0;),对于静态类成员,我们可以再类声明外进行初始化,而且我们发现初始化语句并没有static关键词。
C++允许我们在类声明中初始化成员变量,但是静态非const成员除外

静态数据成员在类声明中声明,在类方法中初始化。如果静态成员是const或者枚举类,则可以在类声明中初始化,(因为这种数据相当于是宏)。
strlen()是计算字符串长度的,但是不会加上字符串末尾的空字符,所以分配内存时,要+1。
在构造函数中,我们使用了动态内存分配,而隐式默认析构函数,是不会做任何操作的,所以我们必须自己写一个析构函数释放动态内存。
在构造函数中使用new分配内存时,必须在相应的析构函数中使用delete来释放内存。
下面我们看看这个StringBad类的缺陷

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using std::cout;
#include"类动态分配1.h"
 
void callme1(StringBad &);    //传递引用
void callme2(StringBad);      //按值传递
 
int main()
{
    using std::endl;
    {
        cout << "Starting an inner block.\n";
        StringBad headline1("Celery Stalks at Midnight");
        StringBad headline2("Lettuce 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 onject 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";
    std::cin.get();
    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";
}

这段代码是有问题的,不同编译器编译后,运行的结果不太一样。
具体有问题的语句是
callme2(headline2);
StringBad sailor = sports;
knot = headline1;
为什么会出错?实际上,类中不止有构造函数、析构函数,还有很多编译器自动写的默认成员函数,出错的地方就是编译器给的默认成员函数对动态内存分配的不配合。

2. 特殊成员函数

C++自动提供了下面这些成员函数

  • 默认构造函数,如果没有定义构造函数
  • 默认析构函数,如果没有定义
  • 复制构造函数,如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符,如果没有定义
  • 移动构造函数,如果没有定义
  • 移动赋值运算符,如果没有定义

默认构造函数和默认析构函数之前都说过,他们两啥事都不会干。重载地址运算符,就是对象的值就是这个对象的this指针的值,也很简单。移动构造函数和移动赋值运算符也不说了,这涉及移动语义语法,以后再说。

2.1 复制构造函数

复制构造函数用于初始化和按照传递参数,它的原型是:
Class_name(const Class_name&);
它接受一个const对象的引用参数,例如StringBad类的赋值函数原型是:
StringBad(const StringBad & );

  • 何时调用复制构造函数?

    StringBad ditto(motto);
    StringBad metto=motto;
    StringBad also =StringBad(metto);
    StringBad *pStringBad=new StringBad(motto);

当将新对象显式初始化成现有的对象时会直接调用复制构造函数,上面这4个声明都会使用复制构造函数之间创建对象。早期的编译器,可能对中间两种声明做一些复杂的操作,但是也是会使用复制构造函数的
当程序生成了对象副本时,编译器都会使用复制构造函数。具体的,函数按值传递对象(例如 callme2())或者按值返回对象时,都会使用复制构造函数。按值传递意味著创建原始变量的一个副本。
编译器生成临时变量时,也会使用复制构造函数,例如将三个对象相加,编译器会生成临时变量保存中间结果。不过现在编译器很智能,临时变量的生成时越来越少了。
总之,复制构造函数的调用会出现在初始化、按值传递、对象返回、生成临时对象时

  • 默认复制构造函数的功能

默认的复制构造函数只会进行潜复制,即逐个复制非静态成员,复制的时成员的值。
StringBad sailor=sports;
相当于
StringBad sailor;
sailor.str=sports.str;
sailor.len=sports,len;
当然了私有成员是无法直接访问的,上面只是演示。
静态成员是不会复制的,因为静态成员都是所有对象共享的,所以不需要复制。

  • StringBad类中默认复制构造函数出错了!

callme2(headline2);
调用时,默认复制构造函数会用来创建一个原始对象的副本,而且默认构造函数不会更新num_strings的值。
如果类中需要一个静态成员需要在创建对象时更新,那么一定要提供一个显式复制构造函数用来处理计数问题
还有一个更致命的问题,当callme2()调用结束后,原始变量的副本就会被删除,即调用析构函数。但是副本对象的str成员和原始对象的str成员是一样的,那么如果调用析构函数删除副本,那么原始对象的动态内存也会被释放,从而导致乱码。

StringBad sailor = sports;
这段代码也会直接让sailor.strsports.str完全相同,从而调用析构函数释放sports时也会把,sailor的那块动态内存释放掉,等到退出代码块后,sailorsports都会调用析构函数,相当于把同一块地址delete[]两次,从而出错。

  • 定义一个显式复制构造函数解决问题

解决这一问题的关键是**深复制(deep copy)**应该定义一个复制构造函数,用来复制指向的数据,而不是指针。
可以这样编写显式复制构造函数:

StringBad::StringBad(const StringBad & st)
{
    num_strings++;
    len=st.len;
    str=new char[len+1];
    std::strcpy(str,st.str);
    cout << num_strings << ": \"" << str
        << "\" object created\n";
}

总之,如果类中包含了使用new初始化的指针成员,应该定义一个复制构造函数,用来复制指向的数据,而不是指针,这就是深复制。浅复制仅复制指针信息,而不会深入挖掘指针指向的数据

2.2 赋值运算符

C++会自动给类写一个重载=的函数,默认赋值运算符。
它的原型是:
Class_name & Class_name::operator=(const Class_name&);
例如,StringBad的默认赋值运算符函数原型是:
StringBad & StringBad::operator=(const StringBad &);
这里返回类型是引用,因为返回的就是*this或者说允许连续赋值操作。(一般来说,能用引用传参或返回,优先考虑引用传参或返回,尤其是允许把传入的引用变量返回的函数。主要是,引用不能指向局部变量,所以我们有时候不得不使用按值传递。)

  • 赋值运算什么时候使用?

把已有的对象赋给另一个对象时,会使用赋值运算符。但是,一般来说,初始化对象时,不会调用赋值运算符,例如:
StringBad sailor = sports;
优秀的编译器,执行上面这一步会直接调用复制构造函数。
而落后的编译器它会这样做:
调用复制构造函数,创建一个临时对象(sports的副本),然后调用赋值运算符,把临时对象赋值给调用对象(即把sports的副本赋值给sailor)。

  • 默认赋值运算符的功能

默认赋值符的功能是,把传入的对象的逐个成员浅复制到调用对象的逐个成员。
a=b;
相当于
a.len=b.len;
a.str=b.str;
当然了,它也不会复制静态成员。

  • 默认赋值运算符出错了!

knot = headline1;
会使得knot.strheadline.str相同,则调用析构函数时,同一块动态内存会被释放两次。

  • 解决赋值问题

解决问题的关键还是:深复制
但是赋值运算符和复制构造函数有区别:

  1. 调用对象得将原来分配的数据delete掉,防止内存泄漏。
  2. 应该避免把自己赋值给自己;否则你已经把自己数据删除掉了,还怎么赋值。
  3. 返回值是指向调用对象的引用。

通过返回引用就可以使用连续赋值,(按值传递的其实也可以完成连续赋值,只是引用传递更高效)
s0=s1=s2;
相当于s0.operator=(s1.operator=(s2));

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;
}

注意这里if(this==&st)千万不能写成if(*this==st)因为地址相同和内容相同概念完全不一样。
赋值操作不会创造新的对象,所以num_strings不需要调整。

3. 改进后的新String

3.1 一些新的想法

我们在StringBad的基础上,添加一些功能,使他更像C++中的string
如下:

//string1.h
#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;//cin输入限制
    public:
        String (const char*s);//字符串常量构造函数
        String();//默认构造函数
        String(const String &st);//复制构造函数
        ~String();//析构函数
        int length()const {return len;}

        String& operator=(const String&);//赋值运算符
        String& operator=(const char*);//专门为C风格字符串常量设计的赋值运算
        char& operator[](int n);//中括号运算符的重载
        const char& operator[](int n)const;//专门为const string 设计的中括号运算符
        friend bool operator<(const String & st1,const String & st2);
        friend bool operator>(const String & st1,const String & st2);
        friend bool operator==(const String & st1,const String & st2);
        friend ostream & operator<<(ostream &os,const String &st);
        friend istream & operator>>(istream &is,String& st);

        static int HowMany();

};
#endif
//string1.cpp
#include<cstring>
#include"string1.h"
using std::cin;
using std::cout;

int String::num_strings=0;//初始化静态类成员
int String::HowMany()//静态类方法
{
    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=0;
    str=nullptr;
    num_strings++;
}
String::String(const String &st)//复制构造函数
{
    len=st.len;
    str=new char[len+1];
    std::strcpy(str,st.str);
    num_strings++;
}
String::~String()//析构函数
{
    delete[] str;
    num_strings--;
}


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)//专门为C风格字符串常量设计的赋值运算
{
    delete[] str;
    len=std::strlen(s);
    str=new char[len+1];
    str:strcpy(str,s);
    return *this;
}
char& String::operator[](int n)//中括号运算符的重载
{
    return str[n];
}
const char& String::operator[](int n)const//专门为const string 设计的中括号运算符
{
    return str[n];
}
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)
{
    char temp[String::CINLIM];
    is.get(temp,String::CINLIM);
    if(is)
    {
        st=temp;
    }
    while (is && is.get()!='\n')
        continue;
    return is;
}
  • 默认构造函数

我们看默认构造函数:

String::String()//默认构造函数
{
    len=0;
    str=nullptr;
    num_strings++;
}

构造函数的关键就在于,它能和析构函数匹配,这里str=nullptr;就是把指针置空,delete[]是可以对空指针操作的。你可以可以写成这样子str=new char[1]{'\0'};,相当于是给个空字符.

  • 比较成员函数

我们使用strcmp()函数对String对象做比较,strcmp(char*s1,char*s2)函数的返回值是,如果s1>s2即第一个参数在第二个参数后面,则返回正数;若s1<s2,则返回负数;如果s1==s2两个字符串相同,则返回0.

bool operator<(const String & st1,const String & st2)
{
    return (std::strcmp(st1.str,st2.str)<0);
}

而且我们把比较成员函数作为友元函数,是因为友元函数重载运算符更适应类类型自动转换,我们的String类的构造函数String::String (const char*s)它只接受一个字符串常量的参数,那么对于这个String类,它可以将字符串常量自动类型转换成String类类型.而使用友元函数重载运算符就是方便String对象和C中字符串常量做比较.
具体来说,如果使用类成员函数来实现<重载,则"name"<object;这句话就无法执行。因为"name"不是对象它无法调用类成员函数;
如果使用友元函数实现<重载,那么"name"<object;就可以执行,因为bool operator<(const String & st1,const String & st2)的第一个参数是String类,而"name"是字符串常量,他会调用构造函数自动转换成
String对象以匹配这个友元函数.

  • 重载[]运算符

[]也是一个运算符,它是一个二元运算符.a[n]a是第一个操作数,n是第二个操作数.
我们希望,String对象能够像数组一样使用[]运算符,但是我们无法直接访问私有成员,那么得重载运算符了.我们采用的是类成员函数方式:

char& String::operator[](int n)//中括号运算符的重载
{
    return str[n];
}
const char& String::operator[](int n)const//专门为const string 设计的中括号运算符
{
    return str[n];
}

可以看懂我们写了两个重载函数,这是因为我们调用[]时,如果是const对象我们不能对它的私有数据做修改,所以返回值时是const引用(或者采用按值传递);如果我们期待修改str[n]数据,我们就得返回它的引用,以确保object[0]='a';这样的赋值语句可以执行.
在重载解析时,会区别const和非const,以确保调用正确的成员函数.

  • 静态成员函数

我们可以将成员函数声明成静态的
静态成员函数不能使用对象调用,这也就意味着它没有this指针.
正因它和对象无关,静态成员函数只能使用静态数据成员.

静态成员函数的作用: 一般来说,我们只能通过对象才能调用成员函数,但是静态成员函数是个例外,
它放在公有部分,那么可以直接用作用域解析符访问它.

static int HowMany();//函数原型
int String::HowMany()//函数定义
{
    return num_strings;
}
String::HowMany()//函数调用

可以看出来,静态成员函数的一种用法:私有静态数据num_strings无法直接访问,那么就用公有的静态成员函数的方式来访问.还有一种用法是,使用静态成员函数设置类级标记,以控制某些接口的行为.

  • 进一步重载=运算符
String& operator=(const String&);//赋值运算符
String& operator=(const char*);//专门为C风格字符串常量设计的赋值运算

可以看出来,我们使用了两个函数来重载赋值运算符,这是因为我们经常会用C风格的字符串常量来给String类赋值.如果只有第一个函数,那么字符串常量就会先自动类型转换成String类对象,然后再调用赋值运算符.
但是这样做的开销是:
首先,调用构造函数创建一个String临时对象.
其次,对临时对象使用重载赋值运算
最后,调用析构函数删除临时对象.

所以为了减少开销,那就再写一个专门给字符串常量设计的赋值运算.

  • 重载>>运算符

我们希望能够使用cin>>s;的方式直接给String类赋值.

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;
}

首先说明一点String::CINLIM,这种方式直接访问私有静态数据,友元函数和成员函数有相同的访问权限,可以用作用域解析符来直接访问私有静态数据,st.CINLIM的使用对象调用的方式访问静态数据也是可行的,总之静态数据由于是所有对象共享的数据,可以用作用域解析符来访问,但是私有静态数据只允许成员函数和友元访问.
关于cin.get(array_name, ArSize);,从缓冲区读取数据,达到行尾或者读取了 ArSize - 1 个字符为止,且超过规定的字符数不会出现错误,会直接截断,并且不对此换行符进行处理,并将其留在缓冲区。如果直接输入\n,那么array_name[0]就会变成\0,并且换行符仍留在缓冲区.
cin.get();读入一个字符后结束读取,而且它会读取换行符.cin.get();多是用来处理换行符的.

所以上面那段代码,首先读取最多CINLIM-1个字符,然后if(is)是为了判断是否直接输入空行,如果不是空行就赋值,如果是空行的话直接返回;while (is && is.get()!='\n')是用is.get()吸收掉多余字符,并且它会吸收换行符.
或者这么写也行

istream & operator>>(istream &is,String& st)
{
    char temp[String::CINLIM];
    is.get(temp,String::CINLIM);
    if(is)
    {
        st=temp;
    }
    is.ignore();
    return is;
}

直接使用is.ignore();清除缓冲区所有字符.

3.2 一个例子

//类_sayings1.cpp
#include<iostream>
#include"string1.h"
const int ArSize=10;
const int MaxLen=81;
int  main()
{
    using std::cin;
    using std::cout;
    using std::endl;
    String name;
    cout<<"Hi,what is your name?\n>>";
    cin>>name;

    cout<<name<<", please enter up to "<<ArSize<<" short sayings <empty line to quit>:\n";
    String sayings[ArSize];
    char temp[MaxLen];
    int i;
    for(i=0;i<ArSize;i++)
    {
        cout<<i+1<<": ";
        cin.get(temp,MaxLen);
        while (cin && cin.get()!='\n')
            continue;
        if(!cin)//检查空行
            break;
        else
            sayings[i]=temp;
    }
    int total=i;
    if(total>0)
    {
        cout<<"Here are your sayings:\n";
        for(i=0;i<total;i++)
            cout<<sayings[i][0]<<": "<<sayings[i]<<endl;
        
        int shortest=0;
        int first=0;
        for(i=1;i<total;i++)
        {
            if(sayings[i].length()<sayings[shortest].length())
                shortest=i;
            if(sayings[i]<sayings[first])
                first=i;
        }
        cout<<"Shortest saying:\n"<<sayings[shortest]<<endl;
        cout<<"First sating:\n"<<sayings[first]<<endl;
        cout<<"This program used "<<String::HowMany()<<" String objects. Bye!\n";
    }
    else
        cout<<"No input! Bye.\n";
    return 0;
}

PS D:\study\c++\path_to_c++> g++ -I .\include\ -o 类_sayings1 .\类_sayings1.cpp .\string1.cpp
PS D:\study\c++\path_to_c++> .\类_sayings1.exe
Hi,what is your name?
>>Mr. Loong
Mr. Loong, please enter up to 10 short sayings <empty line to quit>:
1: eeeeeee
2: ff f f f f f f f f  f f f
3: a    a a a a a a a a a a  a a a a a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  a a a aaaaaaaaa
4: b
5: cc
6:                  dd   dd
7:
Here are your sayings:
e: eeeeeee
f: ff f f f f f f f f  f f f
a: a    a a a a a a a a a a  a a a a a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  a a
b: b
c: cc
 :                  dd   dd
Shortest saying:
b
First sating:
                 dd   dd
This program used 11 String objects. Bye!
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值