C++ primer 第12章 类和动态内存分配

不能在类声明中初始化静态成员变量。因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类,但如果静态成员是整型或枚举型const,则可以在类声明中初始化。

构造函数必须分配足够的内存来存储字符串。

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

对应的析构函数如下:(delete语句至关重要)

StringBad::~StringBad()
{
    cout<<"\""<<str<<"\"object deleted,";
    --num_strings;
    cout<<num_strings<<"left\n";
    delete [] str;
}

在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果new[](包括中括号)来分配内存,则应使用delete[](包括中括号)来释放内存。

1.复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中,也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:

Class_name(const Class_name &)

对于复制构造函数,需要知道两点:何时调用和有何功能。(1)何时调用:新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。每当程序生成了对象副本时,编译器都将使用复制构造函数。具体的说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。(按值传递都将调用复制构造函数,因此应该按引用传递对象,这样可以节省调用构造函数的时间或存储新对象的空间)。(2)默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员你的值。

隐式复制构造函数是按值进行复制的,这里复制的不是字符串,而是一个纸箱字符串的指针。在进行初始化时,可能得到两个指向同一个字符串的指针,而在试图释放内存两次可能导致程序异常终止。因此,需要定义一个显式复制构造函数以解决问题——深度复制。复制构造函数应当复制字符串并将副本的地址赋给str成员,而不是仅仅是赋值字符串的地址,这样每个对象都有自己的字符串,而不是引用另一个对象的字符串,调用析构函数的时候都将释放不同的字符串,不会试图去释放已经被释放的字符串。可以这样编写String的复制构造函数:

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初始化的、指向数据的指针,而不是数据本身。如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据。而不是指针,这样被称为深度复制,复制的另一种形式(成员复制或浅复制)只是复制指针值。

2.解决赋值运算符问题

C++允许类对象赋值,是通过自动为类重载赋值运算符实现的,原型如下:

Class_name & Class_name::operator=(const Class_name &);

将已有的对象赋给另一个对象,将使用重载的赋值运算符。使用赋值运算符时存在和复制构造函数相同的问题,解决方法如下:

(1)由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。

(2)函数应当避免将对象赋给自身,否则,给对象重新赋值前,释放内存操作可能删除对象的内容。

(3)函数返回一个指向调用对象的引用。

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

静态成员函数:不能通过对象调用静态成员函数,实际上,静态成员函数甚至不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。

3.在构造函数中使用new时应注意的事项

(1)如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。

(2)new和delete必须兼容。new对应于delete,new[]对应于delete[]。

(3)如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。

(4)应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。通常这类构造函数与下面类似。具体的说复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址。另外,还应该更新所有受影响的静态类成员。

String::String(const String & st)
{
    num_strings++;
    len=st.len;
    str=new char[len+1];
    std::strcpy(str,st.str);
   

(5)应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。通常,该类方法与下面类似,具体的说,该方法应完成这些操作,检查自我赋值的情况,释放成员指针指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。

String & String::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;
}

总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类的对象,它必须返回一个指向这种对象的引用。最后,有些方法或函数可以返回对象,也可以返回指向对象的引用。在这种情况下,应首选引用,因为效率更高。

4.队列

栈是后进先出(LIFO,last-in,first-out)的结构,队列是先进后出(FIFO,first-in,first-out)的。

默认情况下,队列最多可存储10个项目。链表相比于数据能更好的满足队列的要求。链表由节点序列构成。每个节点中都包含要保存到链表中的信息以及一个指向下一个节点的指针你。可以使用下面的结构来表示节点:

struct Node
{
    Item item;
    struct Node *next;
};

成员初始化列表:由都好分隔的初始化列表组成(前面带冒号)。它位于参数列表的右括号之后、函数体左括号之前。可以将Queue构造函数写成如下所示:(只有构造函数可以使用这种初始化列表)

Queue::Queue(int qs):qsize(qs),front(NuLL),rear(NULL),items(0)
{
    
}

将项目添加到队尾(入队)比较麻烦,如下:

bool Queue::enqueue(const Item &item)
{
    if(isfull())
        return false;
    add->item=item;
    add->next=NULL;
    items++;
    if(front==NULL)
    {
        front=add;
    }
    else
        rear->next=add;
    rear=add;
    return true;
}

删除队首项目(出队)

bool Queue::dequeue(Item &item)
{
    if(front==NULL)
        return false;
    item=front->item;
    items--;
    Node *temp=front;
    front=front->next;
    delete temp;
    if(items==0)
        rear=NULL;
    return true;
}

dequeue()方法确实可以清楚节点,但这并不能保证队列在到期时为空。因此,类需要一个显式析构函数——该函数删除剩余的所有节点。

Queue::~Queue()
{
    Node *temp;
    while(front!=NULL)
    {
        temp=front;
        front=front->next;
        delete temp;
    }
}

要克隆和复制队列,必须提供复制构造函数和执行深度复制的赋值构造函数。最好,还是提供复制构造函数和赋值运算符。正好伪私有方法可以解决这一问题:

class Queue
{
 private:
    Queue(const Queue &q):qsize(0){}
    Queue & operator=(const Queue &q)
        {
            return *this;
        }
};

作用:(1)避免了本来将自动生成的默认方法定义。(2)因为方法是私有的,所以不能被广泛使用。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/CHY1156957638/article/details/86007596