C++类和动态内存分配

时间:2022-03-20 20:02:15

C++ Primer Plus读书笔记

1.类静态成员

  • 类中可以有一些静态变量,不管这个类创建了多少个对象,这个变量始终都是保持不变的,因为它是静态变量,这就给一些在所有对象中都有相同值的类提供了方便。

  • 在进行类的变量定义时,将数组定义成指针的形式,可以不在定义时就将大小分配,而是在构造函数中使用new对其分配空间。

2.析构函数的调用

  • 析构函数的调用时机:
    • 当在成员函数中创建对象时,该函数执行完毕之后会调用析构函数!
    • 当对象是动态变量时,执行完定义该对象的程序块时,将调用该对象的析构函数。
    • 当对象是静态变量时,程序结束时将调用对象的析构函数。
    • 当对象是用new创建的时,仅当显示使用delete删除对象时,析构函数才会被调用。

3.特殊成员函数

  • C++自动提供的成员函数:

    • 默认构造函数
      • 默认的构造函数里面是没有内容的
    • 默认析构函数
      • 类似与默认构造函数,是没有内容的
    • 复制构造函数

      • 用于将一个对象复制到新创建的对象中,它用于初始化过程中,而不是常规的赋值过程中。它的函数原型为:

        class_name(const class_name &)
        //ZhuMeng(const ZhuMeng &)
      • 新建一个对象并将其初始化为现有对象时,复制构造函数将被调用

      • 默认的复制构造函数复制的是成员的值

    • 赋值运算符

      • 这种运算符的原型为:
        class_name & class_name::operator=(const class_name &);
    • 地址运算符

4.这里介绍一种错误

  • 头文件
#ifndef ZHUMENG_H_
#define ZHUMENG_H

#include<iostream>

class ZhuMeng
{
private:
    int shi;
    int fen;
    int miao;
    static int numm;
public:
    ZhuMeng();

    ZhuMeng(int , int , int );
    explicit ZhuMeng(int );
    ~ZhuMeng();
    ZhuMeng operator+(ZhuMeng &) const;
    friend std::ostream & operator<<(std::ostream & o, const ZhuMeng & outp);
};

#endif
  • 实现文件
#include"ZhuMeng.h"

int ZhuMeng::numm = 0;

ZhuMeng::ZhuMeng()
{
    shi = 0;
    fen = 0;
    miao = 0;
    numm++;
}

ZhuMeng::ZhuMeng(int shi, int fen, int miao)
{ 
    this->shi = shi;
    this->fen = fen;
    this->miao = miao;
    numm++;
}

ZhuMeng::ZhuMeng(int shit)
{
    this->shi = shit;
    numm++;
}

ZhuMeng::~ZhuMeng()
{
    numm--;
}

ZhuMeng ZhuMeng::operator+(ZhuMeng & aaa) const 
{
    std::cout << this->shi << std::endl;
    ZhuMeng sum;
    sum.shi = this->shi + aaa.shi;
    sum.fen = this->fen + aaa.fen;
    sum.miao = this->miao + aaa.miao;
    return sum;
}

std::ostream & operator<<(std::ostream & o, const ZhuMeng & outp)
{
    o<<outp.shi<<std::endl<<outp.fen<<std::endl<<outp.miao<<std::endl;
    o<<"left num: "<<outp.numm;
    return o;
}
  • 执行文件
#include<iostream>
#include"ZhuMeng.h"
using namespace std;

ZhuMeng operator*(int all, ZhuMeng all_2)
{
    cout<<"success"<<endl;
}

void func(ZhuMeng a)
{
    cout<<a<<endl;
}

int main() 
{
    ZhuMeng default_zhumeng;
    ZhuMeng a1(13, 24, 10);
    ZhuMeng a2;//=a1; **** #1
    func(default_zhumeng);
    func(a1);
    func(default_zhumeng);
    func(a1);
    func(a1);
    return 0;
}

输出的结果为:

0
0
0
left num: 2
13
24
10
left num: 1
0
0
0
left num: 0
13
24
10
left num: -1
13
24
10
left num: -2
  • 下面从以下几个角度来分析

    1. 这里的numm在类中作为静态变量,初始值定义在cpp文件中,静态变量在内存中一直存在,作为某个类所有对象之间共享的一个变量。

    2. numm的值为什么会变成负数呢?

      • 在重载<<运算符的时候,将一个ZhuMeng的对象作为对象传给这个函数

      • 函数的参数传递方式是按值传递,因此编译器内部是将实参的值赋给形参,也就是用到了复制构造函数,这时候会调用实参的析构函数,以及赋值所用到的复制构造函数,并不是使用构造函数,而在C++中默认的赋值运算符是没有让numm++的,因此numm的值会-1。因此想要正确地执行所想的功能,这里应该是定义一个复制构造函数

        ZhuMeng(const ZhuMeng & s)
        {
            numm++;
        
        }
    3. 1部分的代码有什么问题呢?

      • 这部分代码,把等号加上,就是进行了赋值操作,而默认的赋值函数没有记性numm++操作,同时这个赋值也是不安全的,如果有使用new delete而创建删除的变量,则很有可能会出现错误,因为指针和内存区域也进行了复制,而每次把对象当做实参进行传递之后会调用析构函数,会将前面使用new的变量删除,同时后面如果使用两次的话,会重复删除同一块区域导致意想不到的错误。

      • 因此解决方法是显式定义赋值运算符(也就是重载 = 运算符)

        ZhuMeng & ZhuMeng::operator=(const ZhuMeng & a)
        {
            numm++;
            //这里重新定义那些使用new分配空间的变量,并将a的对应值赋给他们即可
            return *this;
        }
    4. 还有一个问题:如果在类的函数中,某些数据是使用new delete函数进行创建的话,则需要在复制构造函数和赋值运算符中创建新的内存区域。如果不这样做,则每一次调用析构函数的时候,删除的都是同一个内容(因为默认的复制和赋值函数都是这样处理的),当一个变量被delete两次的时候,就会发生未知的错误!!切记。

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

  • 如果在构造函数中使用new来初始化指针成员,则需要在析构函数中使用delete

  • new和delete必须相互兼容,new对应delete,new[]对应delete[]

  • 如果有多个构造函数,则必须以同样的方式使用new,要么都带[],要么都不带,因为只有一个析构函数,所有的构造函数都必须与他兼容。
  • 可以在构造函数中将指针初始化为0(C++11为nullptr),因为delete和delete[]都可以用于空指针。
  • 应当定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象
  • 应当定义一个赋值运算符,通过深度复制的方式将一个对象复制给另一个对象
  • 下面是几个错误的例子

    String String()
    {
    str = "123";//没有使用new
    }


    String String(const char * s)
    {
    str = new char;//因为是字符串,没有使用[]是错误的
    }


    String String(const char * s)
    {
    len = strlen(s);
    str = new char[len+1];//这个是正确的,在长度后面留一个\0的位置
    }

5.关于返回对象

  • 1 返回指向const对象的引用

    • 使用const引用的常见原因是旨在提高效率,但对于何时可以采用这种方式存在一些限制。如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高效率。

    • 注意:

      • 返回对象将调用复制构造函数,而返回引用则不会
      • 引用指向的对象应该在调用函数执行时存在
  • 2 返回指向非const对象的引用

    • 重载赋值运算符

      • 这个是为了提高效率
    • 重载<<运算符(与cout一起使用)

      • 这个是必须这样做
  • 3 返回对象

    • 如果返回的对象是被调用函数中的局部变量,则不应该按引用的方式返回它,因为在被调用函数执行完毕时,局部对象将执行其析构函数,因此,当控制权回到调用函数时,引用指向的对象将不再存在。
  • 4 返回const对象

    • 主要是为了防止某些函数的左值被用于计算,比如a+b=c,将c赋给加法的结果,当然这样是没有意义的,如果返回const对象则可能会避免发生这种错误。
  • 总结:如果方法或函数要返回局部对象,则应该返回对象,而不是指向对象的引用。在这种情况下,将使用复制构造函数来生成返回的对象。如果方法或函数要返回一个没有公有复制构造函数的类的对象,则必须返回一个指向这种对象的引用。如果返回引用和返回对象都可以的时候则最好使用返回引用的方法,效率会更高。

6.指向对象的指针

  • 使用new获取指向对象的指针,使用delete则会调用对象的析构函数
ZhuMeng * pointer = new ZhuMeng();//括号里针对不同构造函数有不同的写法
  • 定位new运算符
char * buffer = new char[512];

ZhuMeng *pc3, *pc4;
pc3 = new (buffer) ZhuMeng();
pc4 = new (buffer + sizeof(ZhuMeng)) ZhuMeng();
  • 使用定位new运算符创建对象时,是使用一个新的对象来覆盖用于第一个对象的内存单元。因此,如上面两行代码所示,必须在原有的地址上有一个偏移量,才能保证两个部分不重叠。如果类动态地为成员分配内存,则会出现错误。

  • 另外一点是使用delete,如果是用定位new运算符创建的对象,则在删除的时候,如果使用了delete pc3,则删掉的是buffer,因为new/delete系统知道已经分配的是512个字节的内存,而对定位new运算符的操作则是一无所知。由于pc3和buffer指向的内容是一样的,因此使用pc3删掉的也是buffer。这样的话,pc3和pc4的析构函数也不会执行,需要显式地调用析构函数。

  • 使用delete [] buffer的时候是将整个buffer删除(对应的new [])而不会为每一pc调用析构函数(pc3和pc4的析构函数都不会被调用)。

pc4->~ZhuMeng();//释放部分的内存
pc3->~ZhuMeng();

delete [] buffer;//将整个buffer都给释放掉

7.类的成员初始化列表

  • 成员初始化列表是放在构造函数定义的名称后面,用冒号指出
ZhuMeng::ZhuMeng(int qs):shi(qs)
{
    //......
}
  • 成员初始化列表就是上面代码片中的shi(qs),将qs赋给shi。

  • 打个比方,如果shi是一个const int型的常量,则只能给它初始化而不能给他赋值,如果将shi = qs写在构造函数中,则会报错,因为这是赋值,不是初始化,把这个类成员初始化列表写在构造函数后边,则会在执行到构造函数之前,也就是创建对象的时候进行初始化,而不是对其进行赋值,这样就会有很大的好处。

  • 只有构造函数可以使用这种初始化列表的方法,并且对于const类的成员,必须使用这种语法,否则,则必须要在类内进行初始化。另外,被声明为引用的类成员也必须要使用这种语法。因为引用与const类似,都是只能在声明的时候进行初始化。

  • 在类内进行初始化与使用初始化列表进行初始化是等价的,也就是下面这两种描述是相同的。

class ZhuMeng{
    const int quan = 20;
}
ZhuMeng::ZhuMeng():quan(20)
{
    //...
}