Chapter 12 类的动态内存分配

动态内存和类

  • 类声明中,只能声明静态成员变量,但是不能初始化。静态数据成员在类声明hpp中声明,在类方法cpp文件中初始化,不需要使用关键字static

    声明:static int num_strings; 初始化:int StringBad::num_strings=0;

    初始化在在类声明hpp文件中进行。因为可能有多个文件包含头文件,如果在hpp中初始化,会出现多次初始化,引发错误

    静态成员是整型或枚举型const,则可以在类声明中初始化

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

  • 特殊成员函数:
    • 默认构造函数(如果没有定义构造函数)
    • 默认西沟函数(如没定义)
    • 复制构造函数(如没定义)
    • 赋值运算符(如没定义)
    • 地址运算符(如没定义)
  • 如果没提供任何构造函数,C++将自动创建默认构造函数(不接受任何参数,也不执行任何操作),为了解决 Klunk lunk; 这类问题;

    默认构造函数:

    • 没有参数
    • 所有参数都有默认值

    复制构造函数:

    • 用于初始化,而不是常规赋值
    • 表现为指向对象的引用左右参数,例如StringBad( const String & )
    • 将新对象显式地初始化现有对象,常使用赋值构造函数,例如:
      • String ditto(motto);
      • String metoo=motto;
      • String also=String(motto);
      • String *pStringBad=new String(motto);
    • 按值传递和返回对象,都将调用复制构造函数
    • 默认赋值函数,只是直接复制成员变量值,即只复制指针地址,并不复制指针内容;通过显式复制构造函数,StringBad::StringBad(const StringBad &st) {str=new char[len+1]; strcpy(str, st.str);}则复制指针内容,如果之前对象删除,也不会影响

改进的String类

  • C++11,nullptr空指针,即str=nullptr

  • 中括号运算符[],可通过operator来重载该运算符,一个操作数位于[之前,另一个操作数位于[]之间。例如,city[0]中,city是第一个操作数,[]是运算符,0是第二个操作数

  • String::show() const表示成员函数show()不改变成员变量;const String answer(“fut”)表示常量对象

  • 静态类成员函数,可用类名和作用域解析运算符来调用。例如, static int HowMany(){return num;},则调用方式为int count=String::HowMany();

    使用静态类成员函数,注意:

    • 不能通过对象调用静态成员函数,不能使用this指针(其为公共部分)
    • 只能使用静态数据成员,如num_thing,不能使用str与len

构造函数使用new注意

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

  • new []对应于delete []

  • 如果有多个构造函数,必须以相同的方式使用new,都带中括号,或者都不带。因为只有一个析构函数,所有构造函数必须与其兼容

  • 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。String::String( const String &st) { str=new char[len+1];strcpy(str, st.str);}。即复制构造函数应复制数据,而不仅仅是数据地址。

  • 应定义一个复制运算符,深度复制一个对象给另一个对象。功能:检查自我赋值,释放成员以前指针,复制数据非地址,并返回指向调用对象的引用。

返回对象

  • 如果函数要返回局部对象,应返回对象,不能是指向对象的引用。这种情况,将使用复制构造函数来生成返回的对象。

  • 函数返回一个没有公有复制构造函数的类(如ostream类)的对象,必须返回一个指向这种对象的引用。

  • 有些函数(如重载运算符)可以返回对象,也可以返回指向对象的引用。这种情况,首先使用引用,效率更高。

指向对象的指针

使用new初始化对象:

  • Class_name *pclass=new Class_name(value),其中Class_name是类,value类型为Type_name;随后使用构造函数Class_name(Type_name);

  • 默认构造函数,则Class_name *ptr=new Class_name;

  • 例如, String *favorite=new String(test);

  • Act *pt=new Act; delete pr; 对pt使用delete运算符时,将调用动态对象pt的析构函数

  • delete只释放对象占用空间,对于成员变量所指向的内存,使用析构函数完成

  • 如果对象是动态变量(普通成员变量,非静态),new,则当执行玩定义该对象的程序块时,将调用该对象的析构函数

  • 对象是用new创建的,则必须显式使用delete删除对象时,析构函数才会被调用。

  • 指针和对象:

    - 常规表示对象指针 String *glamour
    - 对象初始化指针 String *first=& saying[0];
    - 使用new初始化指针,创建新对象 String *favorite=new String(saying[choice])
    - 调用构造函数,初始化新对象 String *gleep=new String;String *glop=new String("my")
    
  • 定位new运算符:

    • delete可与常规new运算符配合使用,不能与定位new运算符配合使用

    • 显式使用定位new运算符创建的对象调用析构函数 pc3->~JustTesting();

    • 使用定位new运算符创建的独享,应与创建顺序相反,即晚创建的早释放

小结

  • 重载«运算符:ostream & operator«(ostream & os, const c_name & obj){ os « …; return os;}\

  • 转换函数:

    • 单个值转换为类类型 c_name(type_name value)
    • 类转换为其他类型,类成员函数 operator type_name()

队列

  • 在类声明中,结构、类或枚举被嵌套在类中,作用域为整个类。这种声明不会创建数据对象,只是指定了在类中可以使用的类型。

    • 声明在公有部分,可从类的外部通过作用域解析运算符使用被声明的类型
    • 声明在私有部分,只能在这个类中使用其类型
  • 成员初始化列表,只能用于构造函数,参数列表后,函数体前,。如Queue::Queue(int qs): qsize(qs){ ; }

    • 对于const的类成员与声明为引用的类成员,只能被创建时进行初始化,无法赋值,所以必须使用初始化列表
    • 初始化顺序与类声明顺序相同
  • 克隆或复制队列,必须提供复制构造函数和执行深度复制的赋值构造函数

总结

  • 对象包含成员指针,其指向内存由new分配,则释放对象,不会释放指针所指向的内存。因此,在类构造函数中使用new分配内存时,应在析构函数中使用delete

  • new可能为对象分配内存,也可能为类成员分配内存,两种情况不同:

    • 对象包含指向new分配内存的指针成员 时,如果将一个对象初始化为另一个对象,或一个对象赋给另一个对象,可能会出现问题(当最终释放这两个对象时,类的析构函数将删除同一内存两次)。可能有两种方式: String a(b);或 String a; a=b; 解决方式:定义一个特殊的复制构造函数来重新定义初始化,并重载赋值运算符。深度复制。
    • 用new为对象分配内存,将其地址赋给一个指针,当用delete释放指针时,将自动为对象调用析构函数。
    • 使用定位new运算符(非常规new运算符)为类对象分配内存,必须显式调用析构函数
  • 在类中包含结构、类和枚举定义,其嵌套类型的作用域为整个类,不会与其他地方定义的同名结构、类和枚举发生冲突

  • 初始化列表:由初始化成员的名称和包含初始值的括号组成。这些初始化操作是在对象创建时进行的,函数体还未执行(非静态const或引用成员,必须采用类内初始化)。C++11可将类内初始化用于非静态const成员 const int qsize=Q_SIZE; 成员初始化列表的构造函数将覆盖相应的类内初始化。