第12章 类和动态内存分配
1. 动态内存和类
当类中有静态成员,则所有该类对象共享一个该静态成员。
class StringBad
{
private:
char * str;
int len;
static int num_strings;
public:
StringBad(const char * s);
StringBad();
~StringBad();
friend std::ostream & operator<<(std::ostream & os,
const StringBad & st);
};
若创建了10个StringBad对象,则有10个 str 和 len,只有一个 num_strings。有一个对象更改该静态变量,所有其他对象的该变量都会改变。不能在类声明时初始化静态变量,这是因为会在类声明时分配内存,不过C++11可以类内初始化了...
静态成员一般在类定义文件中初始化:
int StringBad:: num_strings = 0; // 省略关键字 static
在没有显示定义一些方法时, c++ 会自动生成一些成员函数,包括:默认构造函数,默认析构函数,复制(拷贝)构造函数,赋值运算符重载,地址运算符重载。隐式的地址运算符返回调用的对象的地址(即this指针)。c++ 11 有额外提供了移动构造函数和移动赋值运算两个方法。
默认构造函数的定义没有任何操作
Klun::Klun()
{}
它跟声明一个未初始化的变量的行为是一样的,该类对象成员的值在这种情况下时是未知的。
而一旦在类声明中显示声明了带参数的构造函数,则c++ 不会再生成默认构造函数,如需要默认构造函数,需要自己定义。带参数的构造函数,可以通过默认参数的形式,定义一个默认构造函数,但是它与不带参数的默认构造函数冲突,只能有一个。
Klun::Klun(int n = 0) { ct = 0;}
Klun::Klun() {ct = 0;} // 只能有一个,会报错
复制构造函数的参数是类对象,用于初始化类对象,而不是类对象间赋值。
StringBad::StringBad(const StringBad &)
{
...
}
StringBad ditto; // 调用默认构造函数
StringBad mtto(ditto); // 调用复制构造函数初始化 mtto
StringBad also = mtto; // 调用复制构造函数初始化 also
StringBad tee = StringBad(ditto); // 调用复制构造函数
StringBad * pstr = new StringBad(mtto); // 调用复制构造函数
中间两种声明可能会直接调用复制构造函数初始化mtto和also,也可能通过复制构造函数像创建临时对象,然后通过赋值运算符重载赋值。最后一种,会初始化一个匿名对象,然后将该对象的地址赋值给 pstr 指针。
默认的复制构造函数会逐个复制非静态成员的值。生成临时对象时,也会调用复制构造函数,对于类构造函数中使用 new 的情况下,需要显示定义一个复制构造函数,来避免因创建类临时对象产生的内存泄漏问题,显示定义的复制构造函数也就是深度复制。
StringBad::StringBad(const char * s)
{
len = strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
num_strings++;
}
StringBad::~StringBad()
{
num_strings--;
delete [] str;
}
当没显示定义复制构造函数时,创建一个类对象的临时对象(如按值将类对象作为函数参数),临时对象出作用域时会自动调用析构函数,这样原对象的 str 指针和临时对象的 str 指针指向相同的 new 地址,指向的 new 内存就会释放。复制构造函数不仅复制数据地址,也要复制数据。
StringBad::StringBad(const StringBad & s) // 显示定义复制构造函数,
{ // str 另 new 一块内存
num_strings++;
len = s.len;
str = new char [len + 1];
std::strcpy(str, s.str);
}
默认的赋值运算符重载同默认复制构造函数类似,逐值赋值非静态类成员,同样对于类构造函数中使用 new 的情况下,需要显示定义赋值运算符,来防止 new 的内存泄漏。
StringBad & StringBad::operator=(const StringBad & st)
{
if (this == &st)
return *this;
delete [] str;
len = st.len;
str = new char [len + 1];
std::strcpy(str, st.str);
return *this;
}
析构函数中 delete 的形式要和构造函数中 new 的形式对应,即 new int [],则 delete [];new int, 则 delete 。
// 当析构函数是 delete [] 时,构造函数可以这么定义
String::String()
{
len = 0;
str = new char [1];
str[0] = '/0';
}
delet 或者 delete [] 都可以作用于空指针,因此上面的定义等价于:
String::String()
{
len = 0;
str = nullptr;
}
可以将类成员函数声明为静态的(声明时必须包含关键字 static,定义时不能包含 static),不能通过类对象调用静态成员函数,实际上,静态成员函数不能使用 this 指针。若静态成员函数是公有的,可以通过类名和作用域解析符来调用它。静态成员函数只能只能使用类的静态成员,即 StringBad 类对象的静态成员函数只能使用 num_strings,而不能使用 str 和 len。
static int how() {return num_strings;}
cout << StringBad::how() << endl; // 通过类名和::调用它
2. 使用指向对象的指针
使用指向类对象的指针,如:
StringBad s;
StringBad *pt = new StringBad(s);
将调用复制构造函数初始化一个未命名的类对象,pt 指向这个对象的地址
StringBad * pt = new StringBad;
将调用默认构造函数。
在使用指向类 StringBad 对象的指针时,new 分配的内存负责储存 str 和 len的值,str 指向的字符串由构造函数分配内存,当类对象出作用域时,调用析构函数会 delete str 指向的字符串内存,因此还需要 delet pt;来释放类对象的内存(不带 [] ),当对象是通过 new 创建的,只有当显式调用 delete pt 时,才会调用该对象的析构函数。调用类方法时,使用 “ ->" 运算符代替 " . "。
当使用定位 new 运算符时创建对象时,会产生覆盖的情况,在构造函数有 new 的情况会产生问题:
char * buf = new char[512];
StringBad *pc1 = new (buf) StringBad;
StringBad *pc2 = new (buf) StringBad; // pc2指向的对象会覆盖pc1指向的对象
// 都是从buf[0]开始存储
当 delete [] buf 时,也不会为定位 new 运算符创建的对象调用析构函数。因此,在使用定位 new 运算符连续创建对象时,要提供不同的存储位置:
pc1 = new (buf) StringBad;
pc2 = new (buf + sizeof(StringBad)) StringBad; // 使pc2的对象存储在pc1对象的后面
在使用定位 new 运算符创建对象时,无法这样释放对象:delete pc2;这是因为pc2并没有收到 new 返回的地址,而当 delete pc1;时,也只会释放 buf 指向的那块512字节的内存块,这是因为pc1 与 buf 的地址相同。因此,对于定位 new 运算符创建对象的情况,要显示的调用析构函数来释放对象:
pc2->~StringBad();
pc1->~StringBad();
delete [] buf;
3. 队列模拟
在类中嵌套结构或类声明,声明的作用域是整个类,不会与其他类或者全局等同名声明冲突:
class Queue
{
struct Node { Item item; struct * node;};
enum { Q_SIZE = 10 };
Node * front;
const int qsize;
...
}
如果 Node 声明在公有部分,可以在类的外部,通过作用域解析运算符使用这个声明的类型:Queue::Node node;。
当 Queue 类的公有方法为私有的变量赋值时,qsize 会出现问题,它是 const 修饰的常量,常量只能初始化,而无法先声明再赋值。在调用构造函数时,如有一个构造函数 Queue(int qs) { qsize = qs;},对象将在代码执行前就被创建,这时候在为 qsize 赋值就会报错了。C++ 采用成员初始化列表解决这个问题:
Queue::Queue(int qs) : qsize(qs)
{
front = rear = nullptr;
items = 0;
}
这种方法不局限于初始化常量:
Queue::Queue(int qs) : qsize(qs), front(nullptr), rear(nullptr),
items(0)
{
}
只有构造函数可以使用成员初始化列表的方式,类中有 const 成员或者引用成员的时候,必须使用这种方法,引用也只能初始化,无法先声明后赋值。
C++ 11 开始允许在类内初始化变量:
class Classy
{
private:
int mem = 1;
const int mem2 = 20;
...
};
这与在构造函数中使用成员初始化列表等价。
数据成员被初始化的顺序与它们在类内声明的顺序相同,与初始化器的排列顺序无关。