本章介绍如何对类使用new和delete以及如何处理由于使用动态内存而引起的一些微妙的问题。
1. 动态内存和类为了让程序在运行时决定内存分配,C++使用new和delete运算符来动态控制内存。但是在类中使用这些运算符将导致许多问题。
1.1静态类成员静态类成员特点:无论创建多少个对象,都只有一个静态类成员。也就是说,同一个类中所有对象都使用一个静态类成员。
**注意:**静态类成员是在类声明中声明,在类方法定义中初始化,初始化要在变量名前使用类+作用运算符::指向所属的类,另外不要static修饰。但是如果静态成员时const整数类型或枚举型,则可以在类声明中初始化。
**注意:**在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete释放内存。如果是使用new[]来分配内存,则应使用delete[]来释放内存。
1.2 特殊成员函数C++会自动提供下面成员函数:
- 默认构造函数:如果没有提供任何构造函数,则C++会自动创建默认构造函数。注意:如果定义了构造函数,则C++将不会定义默认构造函数。另外,只能有一个默认构造函数。
- 复制构造函数:用于将一个对象复制到新创建的对象中。它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。
Class_name(const Class_name &);//类的复制构造函数原型
- 何时调用复制构造函数:新建一个对象并将其初始化为同类现有对象时(包括按值传递参数)
- 默认的复制构造函数的功能:逐个复制非静态成员的值(浅复制)
如果类中一些成员是使用new初始化的、指向数据的指针(例如指向字符串指针),另外,如果类中有静态类成员复制时需要更新,则应当定义一个显式复制构造函数,以复制指向的数据所用的空间创建副本,这称为深度复制。
1.4 赋值运算符C++允许类对象进行赋值,这是通过自动为类重载赋值运算符实现。注意赋值运算符只能由类成员函数重载。运算符原型:
Class_name & Class_name::operator=(const Class_name &); //接受并返回一个类对象的引用
功能:与复制构造函数相似,对对象中成员逐个复制,静态类成员不受影响。
何时使用:将已有对象赋给另一个对象,将自动使用重载的赋值运算符。
由于默认赋值运算符会直接复制指向同一块内存的指针,导致释放同一内存块两次,所以可以自定义赋值运算符进行深度复制。
自定义赋值运算符特点:
- 目标对象可能引用以前分配的数据,所以函数应使用delete[]释放这些数据
- 函数应当避免将对象赋给自身。否则,释放内存操作会删除原对象的内容。所以应首先判断操作数是否是本身。
- 函数应返回一个指向调用对象的引用,这样就可以连续赋值。
str = new char[1]; str = new char; //上面两种方式分配的内存量相同,但是前者能够与类析构函数中delete []兼容,而后者不行 //字面值0可以表示数值0,也可以表示空指针。空指针可以使用NULL表示,C++11引入新关键字nullptr表示空指针2.2 比较友元函数
bool operator==(const String &st1, const String &st2)
{
return (std::strcmp(st1.str, st2.str) == 0);
}
//将比较函数作为友元,有助于将Stirng对象与常规的C字符串进行比较
2.3 重载[]访问运算符
//对于中括号运算符,一个操作数在中括号前面,另一个在括号中间
char &String::operator[] (int i) //返回指定字符的引用,没有保证是否修改数据
{
return str[i];
}
const char &String::operator[](int i) const
{
return str[i]; //确保不会修改数据
}
//在重载时,C++会区分常量和非常量函数的特征标,例如上面两个函数
2.4 静态类成员函数
静态类成员函数:在成员函数声明前加上关键字static就可以声明静态成员函数。但是如果函数定义是独立的,则不能声明为静态类成员函数。声明时加上static,定义时就不需要了。
注意:
- 不能通过对象调用静态成员函数,也就不能使用this指针
- 如果静态成员函数在公有部分声明,则可以使用类名加上作用域解析运算符来调用它
- 由于静态成员函数不与特定对象相关联,所以静态成员函数只能使用静态数据成员
- 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
- new和delete必须相互兼容。new对应于delete,new[]对应于delete[]
- 如果有多个构造函数,则必须以相同方式使用new,因为只有一个析构函数,所有的构造函数都必须与它兼容
- 应定义一个复制构造函数
- 应定义一个赋值运算符
- 返回指向const对象的引用:注意返回对象将调用复制构造函数,而返回引用不会。其次,引用指向的对象应该在调用函数执行时存在。返回类型应该与const &匹配。
- **返回指向非const对象的引用:**常见用于重载赋值运算符以及重载<<运算符。允许对对象进行更改
- **返回非const对象:**如果返回的对象是被调用函数中的局部变量,则采用这种方式,返回语句将隐式调用复制构造函数
- 返回const对象:相对于返回非const对象,加上const是为了防止对对象修改
析构函数被调用的情况:
- 如果对象是自动变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
- 如果对象是静态变量,则当程序结束时将调用对象的析构函数。
- 如果对象是new创建的,则仅当显式使用delete删除对象时,将调用对象的析构函数。
要想使用对象指针,可以:
- 使用常规表示法声明指向对象的指针
- 可以将指针初始化为指向已有对象
- 可以使用new初始化指针,可以同时对new对象初始化
- 可以使用->运算符调用指针访问类方法
- 可以对对象指针使用解除引用运算符*来获得对象
使用定位new运算符,我们知道定位new运算符是为了在分配内存时能够指定内存位置。但是我们使用定位new分配对象空间应注意:
- 如果不想新对象覆盖原位置对象的内存单元,应对内存位置进行相应的偏移
- 如果想为对象的销毁调用析构函数,应显式调用对象的析构函数来说明对象的消亡
- 对于使用定位new创建的对象,应以与创建顺序相反的顺序进行删除,因为晚创建的可能依赖于早创建的对象
- 仅当所有对象都被销毁后,才能释放用于存储这些对象的缓冲区
可以在类中嵌套结构或者类声明,这是C++的一项特性
如果数据成员是非静态const成员或引用,这需要在创建对象前进行初始化,可以在创建构造函数时使用成员列表初始化,例如:
queue(int qs) : qsize(qs), item(0) //在创建对象时初始化数据成员qsize = qs,item = 0
{
//函数体
}
//也可以将这种方式用于普通变量初始化
int p1(1); //等价于int p1 = 1;



