首页 C++ Primer Plus-第12章 动态内存和类
文章
取消

C++ Primer Plus-第12章 动态内存和类

12.1 动态内存和类

  • 不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存;

  • 初始化在方法文件中,而不是在类声明文件中进行,因为类声明文件位于头文件中,程序可能将头文件包括在其他几个文件中,如果 在头文件初始化,将出现多个初始化副本,引发错误;

    • 当然,也有例外:静态成员为整型或枚举型const
  • 特殊成员函数:如果没有定义,C++自动提供了下面这些成员函数

    • 默认构造函数

    • 默认析构函数

    • 复制构造函数

    • 赋值运算符

    • 地址运算符

  • C++11还提供了另外两个特殊成员函数:移动构造函数和移动赋值运算符

  • 默认构造函数:

    • 带参数的构造函数也可以是默认构造函数,只要所有的参数都有默认值,但只能有一个默认参数,不然会产生二义性
  • 复制构造函数:将一个对象复制到新创建的对象中

    • 用于初始化过程中(包括按值传递参数),而不是常规的赋值

    • 类的复制构造函数原型:Class_name(const Class_name &);

    • 假设motto是一个StringBad对象,下面4种情况都调用复制构造函数:

      1
      2
      3
      4
      5
      6
      
      //calls StringBad(const StringBad &);
      StringBad ditto(motto);
      StringBad metoo = motto;
      StringBad also = StringBad(motto);
      //使用motto初始化了一个匿名对象,并将新对象的地址赋给pstring指针
      StringBad * pStringBad = new StringBad(motto);
      
    • 默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象

    • 字符出现乱码:隐式复制构造函数是按值复制的,复制字符串(char*)复制的是指针,也就是说两个对象指向同一个字符串,一旦其中一个销毁了,另一个就用不了这个字符串了,所以在显式复制构造函数种需要新建一个字符串

    • 必须定义复制构造函数(深度复制)的原因在于,一些类成员是使用new初始化的、指向数据的指针,而不是数据本身

  • 赋值运算符:

    • 运算符原型:Class_name & Class_name::oprator=(const StringBad &);

    • 将已有的对象赋给另一个对象时,将使用重载运算符:

      1
      2
      3
      
      StringBad headline1("ABC");
      StringBad knot;
      knot = headline1;
      
    • 初始化对象使用=,不一定会使用赋值运算符(比如还使用了复制构造函数),那么为什么说不一定呢,这是因为实现时也有可能分两步来完成,先使用复制构造函数生成一个临时对象,在把临时对象赋值给那个要初始化的对象。也就是说,初始化总会调用复制构造函数,而使用=运算符时也可能调用赋值运算符

    • 函数应当避免将对象赋给自身,否则给对象重新赋值前,释放内存操作可能删除对象的内容(所以要先检查自我复制if(this == &st) return *this;

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

    • 函数返回一个指向调用对象的引用

    • 我认为一定要返回一个类的引用,是因为会有连等的情况比如A = B = C;这样的的话,B = C,然后返回一个类的引用也就是B,接着就会执行A = B;

12.2 改造后的新String类

  • 在C++98中,字面值0有两个含义:可以表示数字值0,也可以表示空指针,有些程序员使用(void *) 0来表示空指针或者NULL;C++11引入新关键字nullptr,表示空指针

  • 静态成员函数:函数声明必须包含static,但如果函数定义是独立的,则其中不能包含static

    • 不能通过对象调用静态成员函数,静态成员函数甚至不能使用this指针

    • 只能使用静态数据成员

    • 可以使用静态成员函数设置类级(classwide)标记,以空值某些类接口的行为

    • 如果是在公有部分声明的,则可以使用类名和作用域解析运算符调用:

      1
      2
      
      static int HowMany() {return num_strings;}
      int count = String::HowMany();
      

12.3 在构造函数中使用new时应注意的

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

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

  • 如果有多个构造函数,必须以相同的方式使用new,要么都到中括号要么都不带,因为只有一个析构函数

  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象

    • 具体来说,复制构造函数应分配足够的空间来存储复制的数据,并复制数据,而不仅仅是数据的地址,另外,还应该更新所有受影响的静态类成员
  • 应当定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象

    • 具体来说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,返回一个指向调用调用对象的引用我

12.4 有关返回对象的说明

  • 当成员函数或独立的函数返回对象时,可以返回指向对象的引用、指向对象的const引用或const对象

  • 返回指向const对象的引用,旨在提高效率

    • 返回对象将调用复制构造函数,而返回引用不会

    • 引用指向的对象在调用函数执行时存在(也就是说不能是字面值,字面值的话就说明这对象之前没创建,开始调函数了,给了个字面值)

    • 参数被声明为const引用,那么返回类型必须为const,这样才匹配

      1
      2
      3
      4
      5
      6
      7
      
      const Vector & Max(const Vector &v1, const Vector &v2)
      {
          if(v1.magval() > v2.magval())
              return v1;
          else
          return v2;
      }
      
  • 返回指向非const对象的引用

    • 两种常见情形:重载赋值运算符以及重载与cout一起使用的<<运算符。前者这样做旨在提高效率(因为传非引用的对象也行,就是慢点),后者必须这样做,因为要用于串接输出cout<<a<<b<<c;,如果不返回引用而返回ostream,将要求调用复制构造函数,但ostream类没有公有的复制构造函数
  • 返回对象

    • 如果被返回的对象是被调用函数的局部变量,则不应该按引用方式返回它,因为执行完了函数会销毁的
  • 返回const对象

    • 其实就是增强代码鲁棒性,防止一些情况发生,比如a+b=c,如果没有const修饰,那么这句话是对的,只是产生了一个临时对象,然后销毁,但加上const,这句话就是不对的了

12.5 使用指向对象的指针

  • 使用new初始化对象:Class_name * pclass = new Class_name(value);

  • 如果不存在二义性,则将发生由原型匹配导致的转换

  • 在下述情况下析构函数被调用

    • 对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数

    • 如果对象是静态变量,则在程序结束时调用

    • 如果是new创建的,则仅当您显式使用delete删除对象时调用

  • 定位new运算符:能够在分配内存时能够指定内存位置

    • 比如:

      1
      2
      3
      4
      5
      
      const int BUF = 512;
      char * buffer = new char[BUF];
      JustTesting *pc1, *pc2;
      pc1 = new(buffer)JustTesting;
      pc2 = new(buffer + sizeof(JustTesting))JustTesting;
      
    • delete可以跟常规new配合使用,但不能和定位new搭配使用,比如指针pc2没有收到new返回的地址,因此delete会导致运行阶段错误;pc1指向的buffer是new[]初始化的,就算delete也要加[]

12.6 复习各种技术

  • 重载<<运算符

  • 转换函数

  • 构造函数使用new的类

  • 这些前面都有,包括第11章,直接翻前面就行了

12.7 队列模拟

  • 栈是后进先出LIFO(last-in, first-out)

  • 队列是先进先出FIFO

  • 就是模拟了下ATM,写了个queue类和customer类,详情p460

本文由作者按照 CC BY 4.0 进行授权

C++ Primer Plus-第11章 使用类

C++ Primer Plus-第13章 类继承