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++會自己生成這些函式,但是當使用對應的類方法時,這些自動生成的函式卻不一定會是我們想要的方法。由此就會帶來一些錯誤。接下來說一下這些成員函式以及它們被呼叫的一些情況。
- 預設建構函式
如果沒有提供任何建構函式,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。