c++ 复制之 类和动态内存分配(9)

本章介绍如何对类使用new和delete以及如何处理由于使用动态内存而引起的一些微妙的问题。
一个具体的例子——c++如何增加内存负载。假设要创建一个类,其一个成员表示某人的姓,最简单的方法是使用字符数组来保存姓,但初始化多大呢,40位?2000位?所有这时需要动态的在运行过程中创建合适长度的数组。通常的c++方法是,在类构造函数中使用new运算符在程序运行时分配所需的内存,还需要执行一些额外的操作:扩展类析构函数、使所有的构造函数与new析构函数协调一致、编写额外的类方法来帮助正确完成初始化和赋值。

12.1 动态内存和类

代码来源
类的声明,其中有一个成员是静态存储类 num_strings。静态类有一个特点,无论创建了多少对象,程序都只创建一个静态类变量副本(图片见书P427)。

//stringbad.h
#include<iostream>
#ifndef STRINGBAD_H
#define STRINGBAD_H

class StringBad
{
private:
    char * str;
    int len;
    static int num_strings;//无论创建多少个StringBad对象,程序之创建一个静态类变量副本
    //这样可以方便地说明数据成员属性,比如用来记录所创建对象的数目
public:
    StringBad(const char * s);
    StringBad(const StringBad & st);
    StringBad();
    ~StringBad();
    StringBad & StringBad::operator=(const StringBad & st);
    friend std::ostream & operator<<(std::ostream & os, const StringBad & st);
};
#endif

该对象使用char指针(而不是char数组)来表示姓名,意味着类声明中没有为字符串本分分配存储空间,而是在构造函数中使用new来为字符串分配空间,这避免了在类声明中预先定义字符串的长度。

#include"stdafx.h"
#include<cstring>
#include"stringbad.h"
using std::cout;

int StringBad::num_strings = 0;
//静态数据成员在类声明中声明,在包含类方法的文件中初始化,初始化时使用作用域运算符来
//指出静态成员所属的类,但如果静态成员是const整数类型或者是枚举类型,则可以在声明中初始化


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\n";
}
StringBad::StringBad(const char * s)
{
    len = std::strlen(s);
    //strlen()返回字符串长度,但是并不包含末尾的空字符,因此分配内存长度为len+1
    str = new char[len + 1];
    std::strcpy(str, s);
    num_strings++;
    cout << "num_strings " << num_strings << ": " << str << "created!\n\n";

}

StringBad::StringBad()
{
    len = 4;
    str = new char[4];
    std::strcpy(str, "C++");
    num_strings++;
    cout << num_strings << " : " << str << "default object created!\n\n";
}

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

std::ostream & operator<<(std::ostream & os, const StringBad &st)
{
    os << st.str;
    return os;
}

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

在由类生成对象时,构造函数分配足够的内存(通过传入的字符串的长度)来存储字符串,然后将字符串复制到内存中。这里要注意的是:这里生成的对象中,只保留了字符串的地址信息,字符串本身并不保存在对象中(而保存在由new开辟的堆内存中heap),之后即使你删除了对象,但字符串所占用的内存并不会自动释放,这就需要由析构函数中的delete来释放由new所申请的内存。

/ StringBad.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>
using std::cout;

#include"stringbad.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("Lecttuce Prey");
        StringBad sports("Spinach Leaves Bow1 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\n";
        StringBad sailor = sports;
        //相当于运行了StringBad sailor = StringBad(sports);
        //这里=sport无法调用正常的构造函数,因为sport不是char*也不是null,是一个String对象,这涉及到了**复制构造函数**
        //在这里,编译器会自动调用复制构造函数StringBad(const StringBad &),从而使得
        //结果运行异常,结绝的方法是定义复制构造函数StringBad(const StringBad &)

        cout << "sailor: " << sailor << endl;
        cout << "Assign one object to another:\n\n";
        StringBad knot;
        knot = headline1;
        cout << "knot: " << knot << endl;
        cout << "Exiting the block.\n\n";
        return 0;
    }

    cout << "End of main() \n\n";
}

void callme1(StringBad & rsb)
{
    cout << "String passed by reference:\n\n";
    cout << "     \"" << rsb << "\"\n\n";
}

void callme2(StringBad sb)
{
    cout << "String passed by value: \n\n";
    cout << "      \"" << sb << "\"\n\n";
}

以上程序有一些错误,导致无法编译通过,主要的原因在于

StringBad sailor = sports;
StringBad headline2("Lecttuce Prey");
void callme2(StringBad sb)
{
    cout << "String passed by value: \n\n";
    cout << "      \"" << sb << "\"\n\n";
}

sport不是默认构造中的数据类型,而是一个StringBad对象,实现的是通过复制来赋值的操作,这里涉及到一个构造方法(也可能涉及一个对象赋值的操作),叫做 复制构造函数(系统有默认的,是浅层复制,当我们涉及到new来申请内存时,需要对复制构造函数进行修改)

12.1.2特殊成员函数

在定义一个类时,有一些成员函数是自动定义的,c++提供了下面的成员函数:

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

结果表明,StringBad类中的问题是由隐式复制函数和隐式赋值运算符引起的。

复制构造函数

复制构造函数用于将一个对象复制到新创建的对象中,用于初始化过程中所以才可以叫做构造函数)(包括按值传递参数和函数返回对象而非对象引用),而不是常规的赋值过程中。
类的复制构造函数原型通常如下,它接受一个指向类对象的常量引用作为参数。

StringBad(const StringBad & st);
  1. 何时调用复制构造函数

新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用,最常见的情况是将新对象显式地初始化为现有的对象(用按值传递时从实参到形参的复制过程也属于复制构造,构造调用函数中的形参的值;函数返回对象而非引用也会调用复制构造函数)

StringBad ditto(motto);
StringBad merro = metto;
StringBad also = StringBad(metto);
StringBad *p = new StringBad(metto);

第二种和第三种还可能涉及到赋值运算符的重载问题(也是造成浅层复制的原因之一),这两种过程可能直接调用了复制构造函数,或者可能使用复制构造函数先生成一个临时对象,然后将临时对象的内容赋值给merro或also。不过无论如何复制构造函数都将调用。

每当程序生成对象副本时,编译器都将使用复制构造函数。按值传递时,形参的初始化会调用复制构造函数;函数返回对象而非引用时,返回的对象将会调用复制构造函数将结果初始化到自己身上。

callme2(headline);

这里程序使用复制构造函数初始化sb——callme2()函数的StringBad型形参。

  1. 复制构造函数的功能

默认的构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。当值是数组时就完蛋了,由于只是复制值,两个变量会同时指向同一块地址,当第一个对象被析构时,第二个对象的数组就消失了。这就是浅复制造成复制构造函数出错的原因。我们对于带有地址的变量的复制需要开辟新的内存来复制,用这个思想来编写新的复制构造函数。

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

赋值运算符的重载

默认的赋值运算符也是浅复制的,因此对其的修改如下,由于不涉及新建对象,所以num_strings不需要++

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

这样所有的问题就解决了。

12.2改进string类

编辑了一天的没保存- =天哪!!

猜你喜欢

转载自blog.csdn.net/sangohan77/article/details/79129172