C++ 笔记6

第12章 类和动态内存分配

GitHub

建议下载下来用Typora软件阅读markdown文件

12类和动态内存分配

  • 动态内存和类
  • –>让程序运行时决定内存分配,而不是在编译时决定。
  • —->使用new和delete运算符来动态控制内存
  • ——>在类中使用这些运算符将导致许多新的编程问题。这种情况下,析构函数将是必不可少的,而不再是可有可无的。
  • ——–>有时候还必须重载赋值运算符。

C++为类自动提供了下面这些成员函数

  • 1.默认构造函数,如果没有定义构造函数;记得自己定义了构造函数时,编译器不会再提供默认构造函数,记得自己再定义一个默认构造函数。
    带参数的构造函数也可以是默认构造函数,只要所有参数都有默认值。

  • 2.默认析构函数,如果没有定义;用于销毁对象

  • 3.复制(拷贝)构造函数,如果没有定义;用于将一个对象赋值到新创建的对象中(将一个对象初始化为另外一个对象)。用于初始化的过程中,而不是常规的赋值过程。

    • 每当程序生成对象副本时(函数按值传递对象,函数返回对象时),编译器都将使用复制构造函数
    • 编译器生成临时对象是,也将使用复制构造函数
      默认的复制构造函数的功能--->逐个复制非静态成员(成员复制也叫浅复制,给两个对象的成员划上等号),复制的是成员的值;如果成员本身就是类对象,则将使用这个类的复制构造函数复制成员对象,静态成员变量不受影响,因为它们属于整个类,而不是各个对象
  • 浅复制面对指针时会出现错误,在复制构造函数中浅复制的等价于sailor.len=sport.len;sailor.str=sport.str;前一个语句正确,后一个语句错误,因为成员char* str是指针,得到的是指向同一字符串的指针!!!

  • 当出现动态内存分配时,要定义一个现实复制构造函数—>进行深度复制(deep copy)

    1
    2
    3
    4
    5
    6
    StringBad::StringBad(const StringBad & st)
    {
    len=st.len;
    str=new char[len+1];
    std::strcpy(str,st.str);
    }
  • 4.赋值运算符,如果没有定义;赋值运算符只能由类成员函数重载的运算符之一。将已有的对象赋值给另外一个对象时(将一个对象复制给另外一个对象),使用赋值运算符。
    原型:class_name & class_name::operator==(const class_name &);接受并返回一个指向类对象的引用。

    • 与复制构造函数相似,赋值运算符的隐式实现也对成员进行逐个复制
  • 解决:提供赋值运算符(进行深度复制)定义,其实现业余复制构造函数相似,但有一些差别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //代码1:先申请内存,再delete
    CMyString& CMyString::operator=(const CMyString& str)
    {
    if(this==str)
    {
    char *temp_pData=new char[strlen(str.m_pData)+1)];
    delete[]m_pData;
    m_pData=temp_pData;
    strcpy(m_pData,str.m_pData);
    }
    return *this;
    }
    //代码2:调用复制构造函数
    CMyString& CMyString::operator=(const CMyString& str)
    {
    if(this==str)
    {
    CMyString strTemp(str);//复制构造函数创建临时对象,临时对象失效时会自动调用析构函数
    char* pTemp=strTemp.m_pData;//创建一个指针指向临时对象的数据成员m_pData
    strTemp.m_pData=m_pData;//交换
    m_pData=pTemp;//交换
    }
    return *this;
    }
  • 5.地址运算符,如果没有定义;

  • 6.移动构造函数(move construtor),如果没有定义;

  • 7.移动赋值运算符(move assignment operator)

静态类成员函数

  • 1.静态函数:静态函数与普通函数不同,它只能在声明它的文件中可以见,不能被其他文件使用。定义静态函数的好处:静态函数不会被其他文件使用。其他文件中可以定义相同名字的函数,不会发生冲突。
  • 2.静态类成员函数:与静态成员数据一样,我们可以创建一个静态成员函数,它为类的全部服务,而不是为某一个类的具体对象服务。静态成员函数与静态成员数据一样,都是在类的内部实现,属于类定义的一部分。普通成员函数一般都隐藏了一个this指针,this指针指向类的对象本身。
  • 3.静态成员函数由于不是与任何对象相联系,因此不具有this指针,从这个意义上讲,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数

    静态成员函数总结:

    • 1.出现在类体外的函数不能指定关键字static;
    • 2.静态成员之间可以互相访问,包括静态成员函数访问静态数据成员和访问静态成员函数;
    • 3.非静态成员函数可以任意地访问静态成员函数和静态数据成员;
    • 4.静态成员函数不能访问非静态成员函数和非静态数据成员
    • 5.由于没有this指针的额外开销,因此静态成员函数与类的全局函数相比,速度上会有少许的增长
    • 6.调用静态成员函数,可以用成员访问操作符(.)和(->)为一个类的对象或指向类对象的指调用静态成员函数。

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

  • 1.如果构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
  • 2.new和delete必须相互兼容,new对应于delete,new[]对应于delete[]
  • 3.如果有多个构造函数,则必须以相同的方式使用new,要么中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new来初始化指针,而在另外一个构造函数中初始化为空(0或nullptr),这是因为delete(无论是带括号,还是不带括号)可以用于空指针。
  • 4.应定义一个复制构造函数,通过深度复制将一个对象初始化为另外一个对象。
  • 5.应当定义一个赋值运算符,通过深度复制将一个对象复制给另外一个对象。

有关返回对象的引用

  • 1.首先,返回对象将调用复制构造函数(给新创建的临时对象复制(初始化)),而返回引用不会

  • 2.其次,返回引用指向的对象因该在调用函数执行时存在。

  • 3.返回作为参数输入的常量引用,返回类型必须为const,这样才匹配。

    使用指向对象的指针

    Class_name* ptr = new Class_name;调用默认构造函数

    定位new运算符/常规new运算符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //使用new运算符创建一个512字节的内存缓冲区
    char* buffer =new char[512];//地址:(void*)buffer=00320AB0
    //创建两个指针;
    JustTesting *pc1,*pc2;
    //给两个指针赋值
    pc1=new (buffer)JustTesting;//使用了new定位符,pc1指向的内存在缓冲区 地址:pc1=00320AB0
    pc2=new JustTesting("help",20);//使用了常规new运算符,pc2指向的内存在堆中
    //创建两个指针;
    JustTesting *pc3,*pc4;
    //给两个指针赋值
    pc3=new (buffer)JustTesting("Bad Idea",6);//使用了new定位符,pc3指向的内存在缓冲区 地址:pc3=00320AB0
    //创建时,定位new运算符使用一个新对象覆盖pc1指向的内存单元。
    //问题1:显然,如果类动态地为其成员分配内存,该内存还没有释放,成员就没了,这将引发问题。
    pc4=new JustTesting("help",10);//使用了常规new运算符,pc4指向的内存在堆中
    //释放内存
    delete pc2;//free heap1
    delete pc4;//free heap2
    delete[] buffer//free buffer
  • 解决问题1,代码如下:

    1
    2
    pc1=new (buffer)JustTesting;
    pc3=new (buffer+sizeof(JustTesting))("Bad Idea",6);
  • 问题2:

    将delete用于pc2,pc4时,将自动调用为pc2和pc4指向的对象调用析构函数;问题2:然而,将的delete[]用于buffer时,不会为使用定位new运算符创建的对象调用析构函数

  • 解决问题2:显式调用析构函数

    1
    2
    pc3->~JustTesting;
    pc1->~JustTesting;

嵌套结构和类

  • 在类声明的结构、类或枚举被认为是被嵌套在类中,其作用域为整个类

  • 这种声明不会创建数据对象,而是指定了可以在类中使用的类型。

    • 1.如果声明是在类的私有部分进行的,则只能在这个类中使用被声明的类型。
    • 2.如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型
      例如,如果Node是在Queue类的公有部分声明的,则可以在外部声明Queue::Node类型的变量。

    默认初始化

    a.内置类型的变量初始化

    如果定义变量时没有指定初始值,则变量被默认初始化。默认值由变量类型和定义变量的位置决定。

  • 如果是内置类型的变量未被显示初始化,它的值由定义位置决定。定义于任何函数体外的变量被初始化为0。

    1
    2
    //不在块中
    int i;//正确,i会被值初始化为0,也称为零初始化
  • 定义于函数体内部的内置类型变量将不被初始化(uninitialized)。一个未被初始化的内置类型变量的值是未定义的,如果试图拷贝或其他形式的访问此类型将引发错误

    1
    2
    3
    4
    5
    1 {//在一个块中
    2 int i;//默认初始化,不可直接使用
    3 int j=0;//值初始化
    4 j=1;//赋值
    5 }

b.类类型的对象初始化

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员。
由编译器提供的构造函数叫(合成的默认构造函数)。
合成的默认构造函数将按照如下规则初始化类的数据成员。

  • 如果存在类内初始值(C++11新特性),用它来初始化成员。

    1
    2
    3
    4
    5
    6
    7
    8
    class CC
    {
    public:
    CC() {}
    ~CC() {}
    private:
    int a = 7; // 类内初始化,C++11 可用
    }
  • 否则,没有初始值的成员将被默认初始化。

    成员列表初始化

  • 使用成员初始化列表的构造函数将覆盖相应的类内初始化

  • 对于简单数据成员,使用成员初始化列表和在函数体中使用复制没什么区别

  • 对于本身就是类对象的成员来说,使用成员初始化列表的效率更高

    如果Classy是一个类,而mem1,mem2,mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员。

    1
    2
    3
    4
    Classy::Classy(int n,intm):mem1(n),mem2(0),men3(n*m+2)
    {
    //...
    }
  • 1.这种格式只能用于构造函数

  • 2.必须用这种格式来初始化非静态const数据成员(至少在C++之前是这样的);

  • 3.必须用这种格式来初始化引用数据成员

  • 数据成员被初始化顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关


# C++

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×