C++动态内存和类

C++动态内存和类

一、使用

C++使用new和delete运算符来动态控制内存。

先上一段示例代码分析:

二、示例代码

// StringBad.h
class StringBad{
private:
    char* str;
    int len;
    static int num_strings;
public:
    StringBad();
    StringBad(const char* s);
    ~StringBad();

    // friend function 友元函数
    friend std::ostream& operator<<(std::ostream& os, const StringBad& st);

};

// StringBad.cpp
// 初始化静态类成员变量, 不能在类声明中初始化静态成员变量,
// 因为声明只是描述了如何分配内存,并不是分配内存
int StringBad::num_strings = 0;

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

StringBad::StringBad(const char *s)
{
    len = std::strlen(s);
    str = new char[len + 1];
    std::strcpy(str, s);
    num_strings++;
    std::cout << num_strings << ": \"" << str << "\" object created" << std::endl;
}

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

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

三、代码分析

3.1 StringBad.h成员变量声明分析

(1)此类使用char指针,而不是char数组来表示名称,意味着类声明没有为字符串本身分配存储空间,而是需要在构造函数或初始化函数中使用new来为字符串分配空间,这样避免了在类声明中预先定义字符串长度

(2)此类将num_strings成员声明为静态存储类型。而静态类成员有一个特点:无论创建了多少个对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。

(3)静态成员初始化:不能再类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来进行初始化,这是因为静态类成员是单独存储的,而不是对象的组成部分。还需注意,静态成员初始化指出了类型,并使用了作用域运算符,但没有使用关键字static。

(4)初始化位置:初始化是在源文件中,而不是在类声明文件中进行的,这是因为类声明位于头文件中,程序可能将头文件包含在其它几个文件中。如果在头文件中进行初始化,将出现多个初始化语句副本,从而引发错误。

(5)可在类声明中初始化的静态数据成员:静态数据成员为const整数类型或枚举类型

3.2 析构函数delete

创建一个StringBad对象自动删除时,删除对象可以释放对象本身占用的内存,但并不能自动释放属于对象成员的指针指向的内存。

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

3.3 strlen()函数

strlen()函数返回字符串长度,但不包括末尾的空字符。

四、特殊成员函数

上述代码示例中是存在很多问题的,比方说当使用一个对象来初始化另一个对象时,编译器将自动生成默认复制构造函数(创建一个对象的副本,即临时对象),如下的初始化(sports是一个StringBad对象):

StringBad sailor = sports;

4.1 特殊成员函数

StringBad的问题是由特殊成员函数引起的。这些成员函数是自动定义的,未显示定义会造成使用过程的其它问题,C++提供了下面这些特殊成员函数 :

<1> 默认构造函数,如果没有定义构造函数

<2> 默认析构函数,如果没有定义

<3> 复制构造函数, 如果没有定义

<4> 赋值运算符, 如果没有定义

<5> 地址运算符, 如果没有定义

更准确的说,编译器将生成后面三个函数的定义—如果程序使用的对象的方式要求这样做。例如,如果将一个对象赋给另一个对象,编译器将提供赋值运算符的定义

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

隐式地址运算符返回调用对象的地址,即this指针。

C++还提供了另外两个特殊成员函数,移动构造函数与移动赋值运算符,后续再讨论。

4.2 默认构造函数

(1)如果没有提供任何构造函数,C++将创建默认构造函数;也就是说,编译器将提供一个不接受任何参数,也不执行任何操作的构造函数

(2)如果定义了构造函数,C++将不会定义默认构造函数。

(3)带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。例如,Test类可以包含下述内联构造函数:

Test(int n = 0) 
{
    test_ct = n;
}

(4)但只能有一个默认构造函数。不能出现如下情况,此情况存在二义性:

Test()
{
    test_ct = 0;
}

Test(int n = 0)
{
    test_ct = n;
}

4.3 复制构造函数

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

Class_name(const Class_name &);

(2) 何时调用复制构造函数:

<1> 新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。4种举例如下:

// motto是一个StringBad对象
StringBad ditto(motto);					// 调用StringBad(const StringBad &)
StringBad metoo = motto;				// 调用StringBad(const StringBad &)
StringBad also = StringBad(motto);	 	 // 调用StringBad(const StringBad &)
StringBad *strb = new StringBad(motto);	 // 调用StringBad(const StringBad &)

<2> 每当程序生成了对象副本时,编译器都将使用复制构造函数。具体讲,当函数按值传递参数函数返回对象时,都将使用复制构造函数。

根据第二种使用情况建议: 由于按值传递对象将调用复制构造函数,因此应该使用按引用传递对象。这样可以节省调用构造函数的时间及存储新对象的空间。

(3) 默认复制构造函数功能:

默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。

五、第二章节示例代码问题分析

5.1 复制构造函数引起的问题

异常1: 运行程序后的输出表明,调用析构函数的次数比构造函数的调用次数多2次,原因可能是程序使用默认复制构造函数另外创建了两个对象。

异常2: 大概有两处症状,症状一是输出的字符串有乱码,症状二是试图释放内存两次可能导致程序异常停止。

5.2 复制构造函数引起问题解决方式

解决方式是显示定义一个复制构造函数,并对对象成员进行深度复制,即内存拷贝。复制指针指向的数据,并将副本地址赋给指针。这样调用析构函数的时候将释放指针指向的不同内存,而不会试图去释放已经释放的指针。显示复制构造函数
如下:

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

注: 如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。成员复制(浅复制)只是复制指针值。

5.3 赋值运算符

(1)赋值运算符

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

// 接受并返回一个指向类对象的引用
Class_name & Class_name::operator=(const Class_name &)

(2)赋值运算符何时使用

将已有的对象赋给另一个对象时,将使用重载的赋值运算符,举例如下:

Stringbad head("Hello World");
StringBad knot;
knot = head;

// 注:对如下情况进行解释
StringBad metoo = knot;
// 解释:使用复制构造函数创建一个临时对象,然后通过赋值将临时对象的值复制到新对象中。这就是说初始化时总是会调用复制构造函数,而使用=运算符也允许调用赋值运算符。

(3)赋值运算符功能

与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制(成员复制或浅复制)。如果成员本身就是类对象,则程序将使用为这个类定义的赋值运算符来复制该成员,但静态成员不受影响。

5.4 赋值运算符引起的问题

如示例代码中将headline1赋给knot:

knot = headline1;

引起的问题: 数据受损。导致knot.str与headline1.str指向相同的地址,当对knot调用析构函数时,将释放knot.str指向的内存;再对headline1调用析构函数时,将试图释放knot.str已将释放的内存。正如前面已经说明的一样,当试图删除已经
删除的数据时导致的结果是不确定的,因此可能改变内存的内容,导致程序异常停止。

5.5 赋值运算符引起的问题解决方式

解决方式: 对于由于默认赋值运算符不合适而导致的问题,解决方法是显示地定义赋值运算符,对成员进行深度复制。

赋值运算符的特点:

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

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

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

显式赋值运算符定义如下:

// 赋值运算符是只能由类成员函数重载的运算符之一
// 首先,检查自我复制,通过查看赋值运算符右边的地址(&st)是否与接收对象(this)的地址是否相同,相同时则直接返回*this
// 第二,如果第一步地址不同,函数将释放str指向的内存
// 第三,为新字符串分配足够的内存空间(new char[len + 1]),并进行深度复制
// 最后,返回*this
StringBad & StringBad::operator=(const StringBad& st)
{
    if (this == &st)
        return *this;

    delete[] str;
    str = nullptr;
    len = st.len;
    str = new char[len + 1];
    strcpy(str, st.str);
    return *this;
}

你可能感兴趣的:(#,C++,Primer,Plus,c++)