主要学习内容:
- 对类成员使用动态内存分配。
- 隐式显式复制构造函数。
- 隐式显式重载赋值运算符。
- 在构造函数中使用new所必须完成的工作。
- 使用静态类成员。
- 将定位new运算符用于对象。
- 使用指向对象的指针。
- 实现队列抽象数据类型。(像第(7)篇中的stack实现一样)
动态内存和类
首先说一下类中的静态类成员。类似如下声明:
class StringBad
{
private:
char * str;
int len;
static int num_strings; // 静态类成员
public:
...
}
静态类成员有一个特点:无论创建了多少对象,程序都只创建一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员。所以上面定义的num_strings成员可以记录所创建的对象数目。另外,静态类成员在类声明中声明,但需要在类声明之外使用单独的语句进行初始化。初始化的形式如下:
// initializing static class member
int StringBad::num_strings = 0;
然后就说一下隐式显式的构造函数,C++会自动提供下面这些成员函数:
- 默认构造函数,如果没有定义构造函数;
- 默认析构函数,如果没有定义;
- 复制构造函数,如果没有定义;
- 赋值运算符,如果没有定义;
- 地址运算符,如果没有定义。
最后一个先不讨论,这就说明了如果自己没有定义上面这些函数,那么C++会自己生成这些函数,但是当使用对应的类方法时,这些自动生成的函数却不一定会是我们想要的方法。由此就会带来一些错误。接下来说一下这些成员函数以及它们被调用的一些情况。
- 默认构造函数
如果没有提供任何构造函数,C++就会创建一个空构造函数。例如,假如定义了一个Klunk类,但没有提供任何构造函数,则编译器将提供下面的构造函数:
这种构造函数会在创建对象是调用,最好自己定义一个构造函数来初始化类。Klunk::Klunk() {} //implicit default constructor
- 复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中。也就是说,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中。类的复制构造函数原型通常如下:
例如:Class_name(const Class_name &);
新建一个对象并将其初始化为同类现有对象时,复制构造函数都将被调用。最常见的情况时将新对象显式地初始化为现有的对象。下面4种声明都将调用复制构造函数:StringBad(const StingBad &);
其中中间的2种声明可能会使用复制构造函数直接创建metoo和also,也可能使用复制构造函数生成一个临时对象,然后将临时对象的内容赋给metoo和also,这取决于具体的实现。最后一种声明使用motto初始化一个匿名对象,并将新对象的地址赋给pstring指针。每当程序生成了对象副本时,编译器都将使用复制构造函数。具体的说,当函数按值传递对象或函数返回对象时,都将使用复制构造函数。因为按值传递意味着创建原始变量的一个副本。而编译器生成临时对象时,也将使用复制构造函数。StringBad ditto(motto); // calls StringBad(const StringBad &) StringBad metoo = motto; // calls StringBad(const StringBad &) StringBad also = StringBad(motto); // calls StringBad(const StringBad &) StringBad * pStringBad = new StringBad(motto); // calls StringBad(const StringBad &)
默认的复制构造函数只会逐个复制非静态成员(成员复制叶称为浅复制),复制的是成员的值。所以也应该定义一个显式的复制构造函数防止出现问题。 - 赋值运算符
当使用赋值运算符时,默认的赋值运算符函数也是只进行浅复制,所以如果类成员中有指针的话会直接给指针赋值,这样两个类对象内的类成员指针指向同一块内存,一旦释放其中一个类,那么这块内存就会被释放,会造成内存泄漏。
解决问题的方法就是自己重新定义赋值运算符函数:
len+1是因为len方法或者strlen函数都是只计算字符串长度,不计算最后一个空字符\0,所以要加这个空字符的空间。StringBad & StringBad::operator=(const StringBad & st) { if (this == &st) return *this; delete [] str; // free old string len = st.len; str = new char [len+1]; // get space for new string std::strcpy(str, st.str); // copy the string return *this; }
构造一个String类
这个类是仿照C++的string写的一个类,一方面可以巩固和练习之前的知识,另一方面可以了解一个string类的实现方法。
// string1.h -- fixed and augmented string class definition
#ifndef STRING1_H_
#define STRING1_H_
#include <iostream>
using std::ostream;
using std::istream;
class String
{
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
String(const char * s); // constructor
String(); //default constructor
String(const String &); // copy constructor
~String(); //destructor
int length() const { return len;}
// overloaded operator methods
String & operator=(const String &);
String & operator=(const char *);
char & operator[](int i);
const char & operator[](int i) const;
// overloaded operator friends
friend bool operator<(const String &st, const String &st2);
friend bool operator>(const String &st1, const String &st2);
friend bool operator==(const String &st, const String &st2);
friend ostream & operator<<(ostream & os, const String & st);
friend istream & operator>>(istream & is, String & st);
// static function
static int HowMany();
}
#endif
方法定义:
// string1.cpp -- String class method
#include <cstring> // string.h for some
#include "string1.h" // includes <iostream>
using std::cin;
using std::cout;
//intializing static class member
int String::num_strings = 0;
// static method
int String::HowMany()
{
return num_strings;
}
// class methods
String::String(const char * s) // construct String from C string
{
len = std::strlen(s); // set size
str = new char[len + 1]; // allot storage
std::strcpy(str, s); // initialize pointer
num_strings++;
}
String::String() // default constructor
{
len = 4;
str = new char[1];
str[0] = '\0';
num_strings++;
}
String::String(const Stirng & 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 string to new location
}
String::~String()
{
--num_strings;
delete [] str;
}
// overloaded operator methods
// assign a String to a String
String & String::operator=(const String & st)
{
if (this == &st)
return *this;
delete [] str;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
return *this;
}
// read-write char access for non-const String
char & String::operator[](int i)
{
return str[i];
}
// read-only char access for const String
const char & String::operator[](int i) const
{
return str[i];
}
// overloaded operator friends
bool operator<(const String &st1, const String &st2)
{
return (std::strcmp(st1.str, st2.str) < 0);
}
bool operator>(const String &st1, const String &st2)
{
return st2 < st1;
}
bool operator==(const String &st1, const String &st2)
{
return (std::strcmp(st1.str, st2.str) == 0);
}
// simple String output
ostream & operator<<(ostream & os, const String & st)
{
os << st.str;
return os;
}
// quick and dirty String input
istream & operator>>(ostream & is, String & st)
{
char temp[String::CINLIM];
is.get(temp, String::CINLIM);
if (is)
st = temp;
while (is && is.get() != '\n')
continue;
return is;
}
构造函数中使用new的注意事项
使用new初始化对象的指针成员时应该这样做:
- 如果在构造函数中使用new来初始化指针成员,则应在析构函数中使用delete。
- new和delete必须相互兼容。new对应于delete,new[]对应于delete[]。
- 如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。
- 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。
- 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。
具体的说,该方法应完成这些操作:检查自我赋值的情况,释放成员指针以前指向的内存,复制数据而不仅仅是数据的地址,并返回一个指向调用对象的引用。
有关返回对象的说明
当成员函数或独立的函数返回对象时,有三种返回方式:返回指向对象的引用、返回指向对象的const引用或返回const对象。
- 返回指向const对象的引用
最好如下编写函数:
const Vector & Max(const Vector & v1, const Vector & v2)
{
if (v1.magval() > v2.magval())
return v1;
else
return v2;
}
- 返回指向非const对象的引用
两种常见的返回非const对象情形是重载运算符以及重载与cout一起使用的<<运算符。前者为了效率,后者必须要这么做。所以这两种情况也返回引用。 - 返回对象
如果被返回对象是被调函数中的局部变量,则不应该按引用方式返回它,因为在被调函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象而不是引用。通常,被重载的算术运算符属于这一类。 - 返回const 对象
在上面的那种情况中如果担心一些误操作会将返回的对象值改变,则应该返回const对象。
使用指向对象的指针
使用new初始化对象:
通常,如果Class_name是类,value的类型为Type_name,则下面的语句:
Class_name * pclass = new Class_name(value);
将调用如下构造函数:
Class_name (const Type_name &);
另外,如果不存在二义性,则将发生由原型匹配导致的转换(如从int到double)。下面的初始化方式将调用默认构造函数:
Class_name * ptr = new Class_name;
如果使用如下语句来创建对象:
String * favorite = new String(sayings[choice]);
这里使用new来为整个对象分配内存,之后如果不再需要该对象就用delete删除:
delete favorite;
这里的释放只是释放保存这个对象的指针,使用这句话后会自动调用析构函数来释放对象的内容。
另外指向对象的指针可以使用->运算符来访问类方法,也可以对对象指针应用运算符(*)来获得对象。(也就是说和基本标准变量指针一样的用法)。
队列模拟(queue)
queue.h
// queue.h -- interface for a queue
#ifndef QUEUE_H_
#define QUEUE_H_
// this queue will contain Customer items
class Customer
{
private:
long arrive; // arrival time for customer
int processtime; // processing time for customer
public:
Customer() { arrive = precesstime = 0; }
void set(long when);
long when() const { return arrive; }
int ptime() const { return processtime; }
};
typedef Customer Item;
class Queue
{
private:
// class scope definitions
// Node is a nested structure definition local to this c
struct Node { Item item; struct 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 & q) : 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.cpp
// queue.cpp -- Queue and Customer methods
#include "queue.h"
#include <cstdlib> // (or stdlib.h) for rand()
// Queue methods
Queue::Queue(int qs) : qsize(qs)
{
front = rear = NULL; // or nullptr
items = 0;
}
Queue::~Queue()
{
Node * temp;
while (front != NULL) // 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
// on failure, new throws std::bad_alloc exception
add->item = item; // set node pointers
add->next = NULL; // or nullptr
items++;
if (front == NULL) // 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 == NULL)
return false;
item = front->item; // set item to first item in queue
items--;
Node * temp = front; //save location of first item
front = front->next; // reset front to next item
delete temp; // delete former first item
if (items == 0)
rear = NULL;
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;
}
这个程序中涉及到的一些点:
- 嵌套结构和类
在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类中使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。例如,如果Node是在Queue类的公有部分声明的,则可以在类的外面声明Queue::Node类型的变量。 - 成员初始化列表
成员初始化列表是C++提供的一种特殊语法,用来对于const数据成员在执行到构造函数体之前,即创建对象时进行初始化。成员初始化列表由逗号分隔的初始化列表组成(前面带冒号)。它位于参数列表的右括号之后、函数体左括号之前。例如:
通常,初值可以是常量或构造函数的参数列表中的参数。但这种方法并不限于初始化常量。例如:Queue:: Queue(int qs) : qsize(qs) // initialize qsize to qs { front = raer = NULL; items = 0; }
只有构造函数可使用这种初始化列表语法。对于const类成员,必须使用这种语法。另外,对于被声明为引用的类成员,也必须使用这种语法:Queue:: Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0) { }
因为引用和const数据类似,只能在被创建时进行初始化。对于简单数据成员,使用成员初始化列表和在函数体种使用赋值没有什么区别。然而,对于本身就是类对象的类成员来说,使用成员初始化列表的效率更高。class Agency {...}; calss Agent { private: Agency & belong; // must use initializer list to initialize ... }; Agent::Agent(Agency & a) : belong(a) {...}
- C++11的类内初始化
C++11种可以使用更加直观的方式进行初始化:
这与在构造函数中使用成员初始化列表等价:class Classy { int mem1 = 10; // in-class initialization const int mem2 = 20; // in-class initialization //... };
Classy::Classy() : mem1(10), mem2(20) {…}
成员mem1和mem2将分别被初始化为10和20,除非调用了使用成员初始化列表的构造函数,在这种情况下,实际列表将覆盖这些默认初始值:
Classy::Classy(int n) : mem1(n) {…}
在这里,构造函数将使用n来初始化mem1,但mem2仍被设置为20。