《C++ Primer Plus》12. 类和动态内存分配

动态内存和类

书上的代码:

// main.h
#ifndef MAIN_H
#define MAIN_H

#include <iostream>
class StringBad {
    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 // MAIN_H

// main.cpp
#include "main.h"
#include <cstring>

int StringBad::num_strings = 0;

StringBad::StringBad(const char* s) {
    len = static_cast<int>(std::strlen(s));
    str = new char[len + 1];
    std::strcpy(str, s);
    num_strings++;
}

StringBad::StringBad() {
    len = 3;
    str = new char[3 + 1];
    std::strcpy(str, "C++");
    num_strings++;
}

StringBad::~StringBad() {
    --num_strings;
    delete [] str;
}

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


int main() {
    StringBad s1("Hello");
    StringBad s2;
    std::cout << s1 << std::endl;
    std::cout << s2 << std::endl;
    return 0;
}

静态成员变量需要在类声明外进行初始化,因为类的声明描述如何分配内存但是不会分配内存

如果是 static const,那么只有类型是整型(intchar 等)或者枚举时才能在类的声明内部进行初始化,因为编译器必须确保它能在编译期计算出来(double 也不行),编译器会做一些优化

上面的代码运行没问题,但是如果我们改一下 main 函数:

// main.cpp
...

int main() {
    StringBad s1("Hello");

    StringBad s2 = s1;                          // 1
    StringBad s3(s1);                           // 2
    StringBad s4 = StringBad(s1);               // 3

    StringBad s5;
    s5 = s1;                                    // 4
    return 0;
}

1、2 和 3 都表示将 s1 拷贝给 s2(1 不一定,要看编译器的实现),4 表示将 s1 赋值给 s5

这几个语句都有问题,不管运行哪个,运行代码都会出现错误:

Process finished with exit code -1073740940 (0xC0000374)
  • 对于拷贝操作,s1 被赋值给了 s2,而 C++ 仅仅执行了浅拷贝(这是默认的拷贝构造函数),s1s2 最后依次被析构,而同一个指针被 delete 了两次
  • 对于赋值操作,同样地,会析构旧的值,并且拷贝新的值(这是默认的赋值运算符),s1 指向的指针同样会被析构两次

为了解决这个问题,需要自行定义拷贝构造函数和赋值运算符。

特殊成员函数

C++ 自动提供了下面的成员函数:

  • 默认构造函数,如果没有定义构造函数
  • 默认析构函数,如果没有定义
  • 拷贝构造函数,如果没有定义
  • 赋值运算符,如果没有定义
  • 地址运算符(&),如果没有定义

拷贝构造函数

拷贝构造函数的函数原型是:

ClassName(const ClassName &);

默认的拷贝构造函数会拷贝所有的成员,如果成员是个类,那么执行这个类的拷贝构造。

下面的四种情况会调用拷贝构造:

StringBad s2(s1);
StringBad s3 = s1;
StringBad s4 = StringBad(s1);
StringBad * ps5 = new StringBad(s1);

其中,第三种情况可能会直接拷贝构造,也可能创建一个临时变量,然后赋值给 s4,这取决于编译器

赋值运算符

赋值运算符的原型是:

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

默认的赋值运算符与拷贝构造相似,逐个复制所有的成员,如果成员是个类对象,那么执行这个类的赋值运算符

下面的情况会调用赋值运算符:

StringBad s2;
s2 = s1;

在构造函数中使用 new 的注意事项

  • newdelete 搭配,new[]delete[] 搭配,不要混用
  • 对于同一个成员变量,不同的构造函数应该用同一种 new 的方式,因为析构函数只有一个
  • 应该定义拷贝构造函数、赋值运算符

关于返回对象的说明

  • 返回指向 const 对象的引用:提高效率,不会调用复制构造函数
  • 返回指向非 const 对象的引用:提高效率,并且可以更改
  • 返回对象:如果返回的是一个局部变量
  • 返回 const 对象:防止写出 if (s1 + s2 = s3) 这样的代码(原本应该是 if (s1 + s2 == s3)),因为 const 对象不能被赋值,如果不是 const 对象,那么会将 s3 赋值给一个临时变量

定位 new 运算符

可以使用定位 new 运算符来在 buffer 中分配内存:

// main.cpp
...

const int BUF = 512;
int main() {
    char* buffer = new char[BUF];
    
    StringBad* p1 = new(buffer) StringBad;
    StringBad* p2 = new(buffer + sizeof StringBad) StringBad;
    
    p1->~StringBad();
    p2->~StringBad();
    
    return 0;
}

new 后面的括号表示对象的地址

定位 new 运算的本质其实就是把传递给他的地址强制转换成 void * 类型然后返回以便能够赋值给任何指针类型

使用定位 new 运算符时,必须确保析构函数被调用,需要手动调用析构函数,不能直接 delete