第12章 类和动态内存分配
静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态变量副本
静态数据成员在类声明中声明,在包含类方法的文件中初始化。初始化时使用作用域运算符来指出静态成员所属的类。但如果静态成员是const整数类型或枚举类型,则可以在类声明中初始化。
在构造函数中使用new来分配内存时,必须在相应的析构函数中使用delete来释放内存。如果使用new[](包括中括号)来分配内存,则应使用delete[](包括中括号)来释放内存。
特殊成员函数
C++提供了下面这些函数:
- 默认构造函数,如果没有定义构造函数;
- 默认析构函数,如果没有定义;
- 复制构造函数,如果没有定义;
- 赋值运算符,如果没有定义;
- 地址运算符,如果没有定义。
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程。
Class_name(const Class_name&);
它接受一个指向类对象的常量引用作为参数
何时调用复制构造函数
最常见的情况是将新对象显示转化为现有对象
每当程序生成了对象副本时,编译器都将使用复制构造函数。(当函数按值传递对象或函数返回对象时,都将使用复制构造函数)
默认复制构造函数
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
如果类中包含使用了new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。
StringBad::StringBad(const StringBad& st)
{
num_strings++; //handle static member update
len = st.len; //same length
str = new char[len + 1]; //allot space
std::strcpy(str, st.str); //copy sting to new location
cout << num_strings << ": \"" << str
<< "\" object created\n"; //For Your Information
}
赋值运算符
原型如下:
Class_name & Class_name::operator=(const Class_name&);
解决赋值问题
对于由于默认赋值运算符不合适而导致的问题,解决办法是提供赋值运算符(进行深度复制)定义。
- 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[]来释放这些数据。
- 函数应当避免将对象赋给自身;否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
- 函数返回一个指向调用对象的引用。
StringBad& StringBad::operator=(const StringBad& st)
{
if (this == &st) //object assigned to itself
return *this;
delete[] str;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
return *this;
}
赋值操作并不创建新的对象,因此不需要调整静态数据成员num_strings的值。(没有num_strings++)
静态类成员函数
可以将成员函数声明为静态的(函数声明必须包含关键字static),但如果函数定义时独立的,则其中不能包含关键字static。
不能通过对象调用静态成员函数(静态成员函数甚至不能使用this指针),如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。例如,可以给String类添加一个名为HowMany()的静态成员函数,方法是在类声明中添加如下原型/定义:
static int HowMany() {return num_strings;}
调用它的方式如下:
int count = String::HowMany();
静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
为提高处理效率,最简单的方法是重载赋值运算符,使之能够直接使用常规字符串,这样就不用创建和删除临时对象了
在构造函数中使用new时应注意的事项
- 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete
- new和delete必须互相兼容。new对应于delete,new[]对应于delete[]。
- 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。
包含类成员的类逐成员复制
逐成员复制将使用成员类型定义的复制构造函数和赋值运算符
有关返回对象的说明
当成员函数或独立的函数返回对象时,有几种返回方式可供选择。可以返回指向对象的引用、指向对象的const引用或const对象。
返回对象将调用复制构造函数,但返回引用不会。
返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它。
返回const对象
返回const对象,能够清楚地指出误用及滥用的方法
总之,如果方法或函数要返回局部对象,则应返回对象,而不是指向对象的引用。在这种情况下,将使用它的复制构造函数来生成对象。如果方法或函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回一个指向这种对象的引用。引用效率高。
使用指向对象的指针
再谈定位new运算符
定位new运算符让您能够在分配内存时能够指定内存位置,要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保两个内存单元不重叠。
delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。
需要显示调用析构函数,必须指定要销毁的对象
pc3->~JustTesting(); // destory object pointed to by pc3
pc1->~JustTesting();
//对于使用定位运算符创建的对象,应以与创建顺序相反的顺序进行删除。
复习各种技术
转换函数
要将单个值转换为类类型时,需要创建原型如下所示的类构造函数:
c_name(type_name value);
其中c_name为类名,type_name是要转换的类型的名称。
要将类转换为其他类型,需要创建原型如下的类成员函数值:
operator type_name(); //应返回所需类型的值
其构造函数使用new的类
如果使用new运算符来分配类成员指向的内存
- 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete,该运算符将释放分配的内存。
- 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。
- 构造函数中要么使用new[],要么使用new,而不能混用。如果构造函数使用的是new[],则析构函数应使用delete[];如果构造函数使用new,则析构函数使用delete。
- 应定义一个分配内存(而不是指针指向已有内存)的复制构造函数。这样程序将能够将类对象初始化为另一个类对象。这种构造函数的原型通常如下:
className(const className&)
- 应定义一个重载赋值运算符的类成员函数,其函数定义如下:
c_name& c_name::operator=(const c_name& cn)
{
if (this == &cn)
return *this;
delete[] c_pointer;
c_pointer = new type_name[size];
...
return *this;
成员初始化列表的语法
如果Classy是一个类,而mem1、mem2和mem3都是这个类的数据成员,则类构造函数可以使用如下的语法来初始化数据成员:
Classy::Classy(int n, int m) :mem1(n), mem2(0), mem3(n*m + 2)
{
//...
}
上述代码将mem1初始化为n,将mem2初始化为0,将mem3初始化为n*m + 2。从概念上说,这些初始化工作是在对象创建时完成的,此时还未执行括号中的任何代码。
- 这种格式只能用于构造函数;
- 必须用这种格式来初始化非静态const数据成员
- 必须用这种格式来初始化引用数据成员。
数据成员被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。
不能将成员初始化列表语法用于构造函数之外的其他类方法。