1. 程式人生 > >C++基礎學習之類和動態記憶體分配(9)

C++基礎學習之類和動態記憶體分配(9)

主要學習內容:

  • 對類成員使用動態記憶體分配。
  • 隱式顯式複製建構函式。
  • 隱式顯式過載賦值運算子。
  • 在建構函式中使用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++會自己生成這些函式,但是當使用對應的類方法時,這些自動生成的函式卻不一定會是我們想要的方法。由此就會帶來一些錯誤。接下來說一下這些成員函式以及它們被呼叫的一些情況。

  1. 預設建構函式
    如果沒有提供任何建構函式,C++就會建立一個空建構函式。例如,假如定義了一個Klunk類,但沒有提供任何建構函式,則編譯器將提供下面的建構函式:
    Klunk::Klunk() {} 	//implicit default constructor
    
    這種建構函式會在建立物件是呼叫,最好自己定義一個建構函式來初始化類。
  2. 複製建構函式
    複製建構函式用於將一個物件複製到新建立的物件中。也就是說,它用於初始化過程中(包括按值傳遞引數),而不是常規的賦值過程中。類的複製建構函式原型通常如下:
    Class_name(const Class_name &);
    
    例如:
    StringBad(const StingBad &);
    
    新建一個物件並將其初始化為同類現有物件時,複製建構函式都將被呼叫。最常見的情況時將新物件顯式地初始化為現有的物件。下面4種宣告都將呼叫複製建構函式:
    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 &)
    
    其中中間的2種宣告可能會使用複製建構函式直接建立metoo和also,也可能使用複製建構函式生成一個臨時物件,然後將臨時物件的內容賦給metoo和also,這取決於具體的實現。最後一種宣告使用motto初始化一個匿名物件,並將新物件的地址賦給pstring指標。每當程式生成了物件副本時,編譯器都將使用複製建構函式。具體的說,當函式按值傳遞物件或函式返回物件時,都將使用複製建構函式。因為按值傳遞意味著建立原始變數的一個副本。而編譯器生成臨時物件時,也將使用複製建構函式。
    預設的複製建構函式只會逐個複製非靜態成員(成員複製葉稱為淺複製),複製的是成員的值。所以也應該定義一個顯式的複製建構函式防止出現問題。
  3. 賦值運算子
    當使用賦值運算子時,預設的賦值運算子函式也是隻進行淺複製,所以如果類成員中有指標的話會直接給指標賦值,這樣兩個類物件內的類成員指標指向同一塊記憶體,一旦釋放其中一個類,那麼這塊記憶體就會被釋放,會造成記憶體洩漏。
    解決問題的方法就是自己重新定義賦值運算子函式:
    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;
    }
    
    len+1是因為len方法或者strlen函式都是隻計算字串長度,不計算最後一個空字元\0,所以要加這個空字元的空間。

構造一個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物件。

  1. 返回指向const物件的引用
    最好如下編寫函式:
const Vector & Max(const Vector & v1, const Vector & v2)
{
	if (v1.magval() > v2.magval())
		return v1;
	else
		return v2;
}
  1. 返回指向非const物件的引用
    兩種常見的返回非const物件情形是過載運算子以及過載與cout一起使用的<<運算子。前者為了效率,後者必須要這麼做。所以這兩種情況也返回引用。
  2. 返回物件
    如果被返回物件是被調函式中的區域性變數,則不應該按引用方式返回它,因為在被調函式執行完畢時,區域性物件將呼叫其解構函式。因此,當控制權回到呼叫函式時,引用指向的物件將不再存在。在這種情況下,應返回物件而不是引用。通常,被過載的算術運算子屬於這一類。
  3. 返回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;
}

這個程式中涉及到的一些點:

  1. 巢狀結構和類
    在類宣告中宣告的結構、類或列舉被稱為是被巢狀在類中,其作用域為整個類。這種宣告不會建立資料物件,而只是指定了可以在類中使用的型別。如果宣告是在類的私有部分進行的,則只能在這個類中使用被宣告的型別;如果宣告是在公有部分進行的,則可以從類的外部通過作用域解析運算子使用被宣告的型別。例如,如果Node是在Queue類的公有部分宣告的,則可以在類的外面宣告Queue::Node型別的變數。
  2. 成員初始化列表
    成員初始化列表是C++提供的一種特殊語法,用來對於const資料成員在執行到建構函式體之前,即建立物件時進行初始化。成員初始化列表由逗號分隔的初始化列表組成(前面帶冒號)。它位於引數列表的右括號之後、函式體左括號之前。例如:
    Queue:: Queue(int qs) : qsize(qs)	// initialize qsize to qs
    {
    	front = raer = NULL;
    	items = 0;
    }
    
    通常,初值可以是常量或建構函式的引數列表中的引數。但這種方法並不限於初始化常量。例如:
    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) {...}
    
    因為引用和const資料類似,只能在被建立時進行初始化。對於簡單資料成員,使用成員初始化列表和在函式體種使用賦值沒有什麼區別。然而,對於本身就是類物件的類成員來說,使用成員初始化列表的效率更高。
  3. 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。