12.1 动态内存和类
让程序在运行时决定内存分配,而不是在编译时决定。
复习示例和静态类成员
静态类成员的特点:无论创建了多少个对象,程序都只创建一个静态类变量副本。
不能在类声明中初始化静态成员变量,因为声明描述了如何分配内存,并不分配内存。
【提示】:如果静态成员是const整数类型或枚举型,则可以在类中初始化。
对于静态类成员,可以在类声明之外使用单独的语句进行初始化,这是因为静态类成员变量是单独存储的,而不是对象的组成成分。
初始化格式:
class StringBad
{
static int num_strings;
}
// initializing static class number
int StringBad::num_strings = 0;
【注意】:初始化是在方法文件中,而不是在类声明文件中进行的,因为类声明位于头文件中,程序可能将头文件包括在其他几个文件。如果在头文件中初始化,将出现多个初始化副本, 从而引发报错。
特殊成员函数
C++自动提供了下面这些成员函数,如果没有定义
- 默认构造函数
- 默认析构函数
- 复制构造函数
- 赋值运算符
- 地址运算符
- 移动构造函数和移动赋值运算符(C++11)
默认构造函数
创建对象时,如果没有提供任何构造函数,C++将创建默认构造函数。
【注意】:带参数的也可以使默认构造函数。注意**防止二义性**。
//function prototype
klunk(); #1 default constructor
klunk(int a = 10); #2
klunk kar(10); //与#2匹配
klunk bus; // 既与#1匹配,也与#2匹配,出现二义性
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。
// general function prototype
Class_name (const Class_name& );
下面4种声明将调用复制构造函数
//calls StringBad(const StringBad& );
StringBad ditto(motto); // motto is a StringBad class object
StringBad metoo = motto;
StringBad also = StringBad(motto);
StringBad* pStringBad = new StringBad(motto);
由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。
默认复制构造函数(浅复制)
默认的复制构造函数逐个复制非静态成员(成员复制也称为浅复制),复制的是成员的值。
调用默认复制构造函数
StringBad sailor = sports;
//与下面的代码等效
StringBad sailor;
sailor.str = sports.str;
sailor.len = sailor.len;
浅复制带来的问题
- 字符串内容出现乱码,原因在于浅复制是按值进行复制的
- 试图释放内存两次可能导致程序异常终止。
这里的复制是一个指向字符串的指针,两个对象指向同一个字符串。当析构函数被调用时就会引发这样的问题。
【注意】:指向的内存已经被析构函数释放、这将导致不确定的、可能有害的后果。
深度复制构造函数
深度复制代码如下:
StringBad::StringBad(const StringBad& st)
{
num_strings++;
len = st.len;
str = new char[len + 1]; //allot space
std::strcpy(str, st.str); // copy string to new location
}
必须定义深度复制构造函数的原因在于
一些类成员使用new初始化的、指向数据的指针,而不是数据本身。
赋值运算符
调用赋值运算符
StringBad headline1;
StringBad knot;
knot = headline1; // assignment operator invoked
赋值运算符的问题和浅复制的问题相同,数据受损。
提供赋值运算符(深度复制)定义
//assignment operator
StringBad& StringBad::operator=(const StringBad& st)
{
if (this == &st) // object assigned to itself
return *this;
delete[] str; // free old string
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
return *this;
}
【注意】:
- 返回为StringBad&,进行连续的赋值,同时不会生成匿名StringBad对象。
- 由于目标对象可能引用了以前分配的数据,所以函数应使用delete[] 来释放旧数据。
- 函数应当避免将对象赋给自身。否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
12.2 改进后的新 String 类
修正后的默认构造函数
修正后的默认构造函数为创建一个空字符串。
MyString::MyString
{
len = 0;
str = new char[1];
str[0] = '\0';
}
为啥不是
str = new char;
上述两种方式分配的内存量相同,区别在于前者与delete[] str 兼容,而后者不兼容。
【提示】:delete[] 与使用 new[] 初始化和空指针都兼容。
因此可修改为:
MyString::MyString
{
len = 0;
str = nullptr;
}
空指针
C++11,建议使用nullptr,来表示空指针。
代码示例:
#include<iostream>
using std::cout;
using std::endl;
void test(void*);
void test(int);
int main()
{
test(NULL);
test(0);
test(nullptr);
system("pause");
return 0;
}
void test(void*)
{
cout << " this is a point!" << endl;
}
void test(int)
{
cout << " this is int!" << endl;
}
运行结果:
0和NULL 优先显示为 整型0,而不是(void*)。因此,建议使用nullptr.
比较成员函数
可以使用标准的 strcmp()函数,
如果依照字母顺序,第一个参数位于第二个参数之前,则该函数返回一个负值,否则返回正值。如果两个字符串相同,则返回0。
代码示例:
//小于
bool operator<(const MyString& st1, const MyString& st2){
return (std::strcmp(st1.str, st2.str) < 0);
}
//大于
bool operator>(const MyString& st1, const MyString& st2){
return st2 < st1;
}
//等于
bool operator == (const MyString& st1, const MyString& st2){
return (std::strcmp(st1.str, st2.str) = 0);
}
将比较函数作为友元函数,有助于将MyString对象与常规C字符串进行比较。
使用中括号表示法访问字符
代码示例:
//可修改,可读写常规对象
char& MyString::operator[](int i){
return str[i];
}
// 不可修改,只读其数据
const char& MyString::operator[](int i){
return str[i];
}
// 调用
MyString text("Once upon a time");
const MyString answer("futile");
cout << text[2]; // ok
cout << answer[2]; // ok
cin >> text[1]; // ok
cin >> answer[2]; // compile-time error
返回为char&,可以给特定元素赋值。
静态类成员函数
函数声明加static,函数定义不能包含static
//function prototype
static int HowMany();
//function definition
int MyString::HowMany(){};
//function invoked
int count = MyString::HowMany();
【注意】:
- 如果静态成员函数为公有声明,可以通过类名或对象名来调用。例如MyString::HowMany()。而非静态成员函数,只可以通过对象名调用。
- 静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
- 不能使用this指针。
【技巧】:一切不需要实例化就可以有确定行为方式的函数都应该设计成静态的。
String 类程序
//MyString.h -- string class definition
#define _CRT_SECURE_NO_WARNINGS
#ifndef MYSTRING_H
#define MYSTRING_H
#include<iostream>
using std::ostream;
using std::istream;
class MyString
{
private:
char* str; //pointer to string
int len; // length of string
static int num_strings; // number of objects
static const int CINLIM = 80; // cin input limit
public:
// constructors and other methods
MyString(); // default constructor
MyString(const char* ); // constructor
MyString(const MyString& ); // copy constructor
~MyString(); // destructor
int length() const { return len; } //length of string
// overloaded operator methods
MyString& operator=(const MyString&);
MyString& operator=(const char*);
char& operator[](int);
const char& operator[](int) const;
// overloaded operator friends
friend bool operator<(const MyString&, const MyString&);
friend bool operator>(const MyString&, const MyString&);
friend bool operator==(const MyString&, const MyString&);
friend ostream& operator<<(ostream&, const MyString&);
friend istream& operator>>(istream&, MyString&);
// static function
static int HowMany();
};
#endif // !MYSTRING_H
//MyString.cpp -- MyString class methods
#include"MyString.h"
using std::cin;
using std::cout;
//initializing static class member
int MyString::num_strings = 0;
// static function
int MyString::HowMany()
{
return num_strings;
}
// default constructor
MyString::MyString()
{
num_strings++;
len = 4;
str = nullptr;
}
// constructor
MyString::MyString(const char* s)
{
num_strings++;
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
}
// copy constructor
MyString::MyString(const MyString& mstr)
{
num_strings++;
len = mstr.len;
str = new char[len + 1];
std::strcpy(str, mstr.str);
}
// destructor
MyString::~MyString()
{
num_strings--;
delete[] str;
str = nullptr;
}
// overloaded operator methods
MyString& MyString::operator=(const MyString& mstr)
{
if (this == &mstr)
return *this;
delete[] str;
len = mstr.len;
str = new char[len + 1];
std::strcpy(str, mstr.str);
return *this;
}
MyString& MyString::operator=(const char* s)
{
delete[] str;
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str, s);
return *this;
}
char& MyString::operator[](int i)
{
return str[i];
}
const char& MyString::operator[](int i) const
{
return str[i];
}
// overloaded operator friends
bool operator<(const MyString& mstr1, const MyString& mstr2)
{
return (std::strcmp(mstr1.str, mstr2.str) < 0);
}
bool operator>(const MyString& mstr1, const MyString& mstr2)
{
return mstr2 < mstr1;
}
bool operator==(const MyString& mstr1, const MyString& mstr2)
{
return (std::strcmp(mstr1.str, mstr2.str) == 0);
}
ostream& operator<<(ostream& os, const MyString& mstr)
{
return os << mstr.str;
}
istream& operator>>(istream& is, MyString& mstr)
{
char temp[MyString::CINLIM]{};
is.get(temp, MyString::CINLIM);
if (is)
{
mstr = temp;
}
while (is && is.get() != '\n')
continue;
return is;
}
12.3 在构造函数中使用new时应注意的事项
- 如果构造函数用new来初始化指针成员,则应在析构函数中使用delete
- 如果析构函数中有delete,则每个构造函数都应当使用new来初始化指针,或者设置为空指针(nullptr)。
- new和delete必须相互兼容。new对应着delete,new[] 对应着delete[].
- 如果有多个构造函数,必须以相同的方式new。因为只有一个析构函数,必须相互兼容。
- 可以通过在另一个构造函数中将指针初始化为空,delete(无论带不带中括号)可以用于空指针。
- 定义深度复制构造函数。
MyString::MyString(const MyString& mstr)
{
num_strings++;
len = mstr.len;
str = new char[len + 1];
std::strcpy(str, mstr.str);
}
- 定义深度赋值运算符。
检测自我赋值情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
MyString& MyString::operator=(const MyString& mstr)
{
if (this == &mstr)
return *this;
delete[] str;
len = mstr.len;
str = new char[len + 1];
std::strcpy(str, mstr.str);
return *this;
}
12.4 有关返回对象的说明
返回指向const对象的引用
旨在:调高效率
如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高其效率。
返回对象将调用复制构造函数,而返回引用不会,返回引用效率更高。
返回指向非const对象的引用
返回非const对象引用:
- 重载赋值运算符
返回对象引用可以提高效率,避免创建新的复制构造函数 - 重载与cout一起使用的<<运算符
返回的类型必须是ostream&, ostream没有公有的复制构造函数。
返回对象
被返回的对象时被调用函数中的局部变量,则返回对象。
返回const对象
对于不覆盖局部对象,可以返回const对象。
总结
如果方法或者函数要返回局部对象,则应返回对象,而不是指向对象的引用。将使用复制构造函数,来生成返回的对象。
如果方法或者函数要返回一个没有公有复制构造函数的类(如ostream类)的对象,它必须返回为对象的引用。
最后,有些方法和函数(如重载赋值运算符)可以返回对象也可以返回对象引用,应首选引用,其效率更高。
12.5 使用指向对象的指针
指针和对象小结
- 使用常规表示法来声明指向对象的指针
String* glamour;
- 可以将指针初始化为指向已有的对象
String* first = &sayings[0];
- 可以使用new来初始化指针,这将创建一个新的对象
String* favorite = new String(sayings[choice]);
- 对类使用new将调用相应的类构造函数来初始化新创建的对象
//invokes default constructor
String* first = new String;
- 可以使用->运算符通过指针访问类方法
first->length();
- 可以对对象指针解引用来获取对象
(*first).length();
定位new运算符创建对象的内存管理
class JustTest{};
char* buffer = new char[512];
JustTest * pc1 = new(buffer) JustTest; // placement new in buffer
JustTest * pc2 = new(buffer) JustTest; // placement new in buffer
delete[] buffer; // free buffer
上面代码存在的问题:
- 问题1:在创建第二个对象时,定位new运算符使用一个新对象覆盖第一个对象的内存。
解决办法:提供两个位于缓冲区的不同地址,并确保两个内存不会重叠。
例如:
JustTest * pc1 = new(buffer) JustTest;
JustTest * pc2 = new(buffer + sizeof(JustTest)) JustTest;
- 问题2:delete[] buffer ,释放了buffer的内存块,却不会调用使用定位new运算符创建对象(pc1,pc2)的析构函数。
原因:delete可与常规new运算符配合使用,不能与定位运算符配合使用。另一方面,pc1指向的地址与buffer相同。delete pc1,也将释放buffer。这是因为new/delete系统知道已分配的512字节块buffer。
解决办法:显示的使用定位运算符创建的对象调用析构函数。
例如:
pc2->~JustTest();
pc1->~JustTest();
【注意】:对于使用定位new运算符创建对象,应当与创建顺序相反进行删除。原因:晚创建的对象可能依赖于早创建的对象。另外,仅当所有的对象都被销毁后,才能释放用于存储这些对象的缓冲区(buffer)。
12.7 队列
成员初始化列表
调用构造函数时,对象将在括号中的代码执行之前被创建。即调用构造函数将给成员变量分配内存空间。
而对于非静态const成员变量必须在创建对象时进行初始化。
因此,C++提供成员初始化列表来完成上述工作。
//初始化列表格式
Queue::Queue(int qs):qsize(qs),front(nullptr), rear(nullptr), items(0)
{
}
- 这种格式只能用于构造函数
- 必须用这种格式来初始化非静态const数据成员
- 必须用这种格式来初始化引用数据成员
【警告】:不能将成员初始化列表语法用于构造函数之前的其他方法。
【建议】:多使用初始化列表,减少赋值运算符的步骤。
C++11类内初始化
// 类内直接初始化
class Queue
{
qsize = 0;
front = nullptr;
//...
}
C++11允许直接在类内初始化,与在构造函数中使用成员初始化列表等价。
再调用成员初始化列表的构造函数,实际列表将覆盖这些默认初始值。
成员函数私有化技巧
要克隆或者复制,必须提供深度复制构造函数和执行深度复制的赋值构造函数。如果不实现上述功能,可以将这些方法声明为私有方法。
所以,与其将来面对无法预料的运行故障,不如得到一个易与跟踪的编译错误,指出这些方法是不可访问的。
【注意】:C++11提供了另一种禁用方法的方式----使用关键字delete。(18章介绍,待补充)
队列的实现
//Queue.h -- interface for a queue
#ifndef QUEUE_H
#define QUEUE_H
// This queue will contain Customer items
class Customer
{
private:
long arrive;
int processtime;
public:
Customer():arrive(0),processtime(0){}
void set(long when);
long when() const { return arrive; }
int ptime() const { return processtime; }
};
typedef Customer Item;
class Queue
{
//class scope definitions
//Node is nested structure definition local to this c
struct Node
{
Item item;
Node* next;
};
enum{Q_SIZE = 10};
//private class members
Node* front; //pointer to front of Queue
Node* rear; //pointer to rear of Queue
int items; //current number of items in Queue
const int qsize; // maximum number of items in Queue
//preemptive definitions to prevent public copying 防止公共复制的抢先定义
Queue(const Queue&):qsize(0){}
Queue& operator=(const Queue& q) { return *this; }
public:
Queue(int qs = Q_SIZE); // create queue with a qs limit
~Queue();
bool isempty() const;
bool isfull() const;
int queuecount() const;
bool enqueue(const Item& item);// add item to end
bool dequeue(Item& item); //remove item from front
};
#endif // !QUEUE_H
// Queue.cpp -- Queue and Customer methods
#include"Queue.h"
#include<cstdlib> // for rand()
//Queue methods
Queue::Queue(int qs):
qsize(qs), front(nullptr), rear(nullptr), items(0){}
Queue::~Queue()
{
Node* temp{};
while (front != nullptr) // while queue is not yet empty
{
temp = front; // save address of front item
front = front->next; // reset pointer to next item
delete temp; //delete former front
}
}
bool Queue::isempty() const
{
return items == 0;
}
bool Queue::isfull() const
{
return items == qsize;
}
int Queue::queuecount() const
{
return items;
}
// add item to queue
bool Queue::enqueue(const Item& item)
{
if (isfull())
return false;
Node* add = new Node; // create node
add->item = item; //set node pointers
add->next = nullptr; // or nullptr
items++;
if (front == nullptr) //if queue is empty
front = add; //place item at front
else
rear->next = add; // else place at rear
rear = add; //have rear point to new node
return true;
}
//place front item into item variable and remove from queue
bool Queue::dequeue(Item& item)
{
if (front == nullptr)
return false;
item = front->item; // set item to first item in queue
items--;
Node* temp = front; // save location of first in queue
front = front->next; //reset front to next item
delete temp; //delete former first item
if (items == 0)
rear = nullptr;
return true;
}
//time set to a random value in the range 1-3
void Customer::set(long when)
{
processtime = std::rand() % 3 + 1;
arrive = when;
}