第十二章 類和動態內存分配
本章內容包括:
- 對類成員使用動態內存分配
- 隱式和顯式複製構造函數
- 隱式和顯式重載重載賦值運算符
- 在構造函數中使用new所必須完成的工作
- 使用靜態類成員
- 將定位new運算符用於對象
- 使用指向對象的指針
- 實現隊列抽象數據類型
動態內存和類:
C++使用new和delete運算符來動態控制內存。遺憾的是,在類中使用這些運算符將導致許多新的編程問題。在這種情況下,析構函數是必不可少的。有時候,還必須重載賦值運算符,以保證程序正常運行。
複習示例和靜態類成員:
stringbad.h
#ifndef D1_STRINGBAD_H
#define D1_STRINGBAD_H
#include <iostream>
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);
};
#endif //D1_STRINGBAD_H
stringbad.cpp
#include "stringbad.h"
#include <cstring>
using std::cout;
int stringbad::num_strings = 0;
stringbad::stringbad(const char *s) {
len = std::strlen(s);
str = new char[len + 1];
std::strcpy(str,s);
num_strings++;
cout << num_strings << ": \"" << str << "\" object created\n";
}
stringbad::stringbad() {
len = 3;
str = new char[4];
std::strcpy(str,"C++");
num_strings++;
cout << num_strings << ": \"" << str << "\" default object created\n";
}
stringbad::~stringbad() {
cout << "\"" << str << "\" object deleted, ";
num_strings--;
cout << num_strings << " left\n";
delete[] str;
}
std::ostream &operator<<(std::ostream &os, const stringbad &st) {
os << st.str;
return os;
}
首先:
int stringbad::num_strings = 0;
這條語句將靜態成員num_strings的值初始化爲零。注意,不能在類聲明中初始化靜態變量,這是因爲聲明中描述瞭如何分配內存,但並不分配內存。對於靜態成員變量,可以在類聲明之外使用單獨的語句來進行初始化,這是因爲靜態類成員是單獨存儲的,而不是對象的組成部分。
初始化是在方法文件中,而不是在類聲明文件中進行的,這是因爲類聲明位於頭文件中,如果頭文件被多個文件包含,變量就會被多次初始化,引發錯誤。
對於不能在類聲明中初始靜態數據成員的一種例外情況是const
static const int num_strings = 12;
main.cpp
#include <iostream>
#include "stringbad.h"
using std::cout;
void callme1(stringbad &);
void callme2(stringbad);
int main(){
using std::endl;
{
cout << "Starting an inner block.\n";
stringbad headline1("Celerty Stalks at Midnight");
stringbad headline2("Lettuce Prey");
stringbad sports("Spinach Leaves Boel for Dollars");
cout << "headline1: " << headline1 << endl;
cout << "headline2: " << headline2 << endl;
cout << "sports: " << sports << endl;
callme1(headline1);
cout << "headline1: " << headline1 << endl;
callme2(headline2);
cout << "headline2: " << headline2 << endl;
cout << "Initialize one object to another:\n";
stringbad sailor = sports;
cout << "sailor: " << sailor << endl;
cout << "Assign one object to another:\n";
stringbad knot;
knot = headline1;
cout << "knot: " << knot <<endl;
cout << "Exiting the block.\n";
};
cout << "End of main().\n";
}
void callme1(stringbad & rsb){
cout << "String passed by reference:\n";
cout << " \"" << rsb << "\"\n";
}
void callme2(stringbad sb){
cout << "String passed by value:\n";
cout << " \"" << sb << "\"\n";
}
結果
Starting an inner block.
1: "Celerty Stalks at Midnight" object created
2: "Lettuce Prey" object created
3: "Spinach Leaves Boel for Dollars" object created
headline1: Celerty Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Boel for Dollars
String passed by reference:
"Celerty Stalks at Midnight"
headline1: Celerty Stalks at Midnight
String passed by value:
"Lettuce Prey"
"Lettuce Prey" object deleted, 2 left (在這之後異常了)
headline2:
Initialize one object to another:
sailor: Spinach Leaves Boel for Dollars
Assign one object to another:
3: "C++" default object created
knot: Celerty Stalks at Midnight
Exiting the block.
"Celerty Stalks at Midnight" object deleted, 2 left
"Spinach Leaves Boel for Dollars" object deleted, 1 left
" �" object deleted, 0 left
*** Error in `/home/luslin/c++/d1/cmake-build-debug/d1': double free or corruption (fasttop): 0x00000000011a9080 ***
在callme2(headline2)時,按值傳遞headline2,結果表明,delete [] 兩次刪除一個內存區域。
3: “C++” default object created 這句話表明 stringbad sailor = sports; 這句話調用了stringbad::stringbad()
特殊成員函數:
stringbad 類的問題是由特殊的成員函數引起的,這些成員函數是自動定義的。就stringbad而言,這些函數的行爲與類設計不符。具體來說,C++中,提供了下面這些成員函數:
- 默認夠構造函數,如果沒有定義構造函數。
- 默認析構函數,如果沒有定義。
- 複製構造函數,如果沒有定義。
- 賦值運算符,如果沒有定義。
- 地址運算符,如果沒有定義。
更準確地說,編譯器將生成上述最後三個函數的定義。例如,如果將一個對象賦值給另一個對象,編譯器將提供賦值運算符的定義。
結果表明,stringbad類中的問題是由隱式複製構造函數和隱式複製構造函數和隱式賦值運算符引起的。
隱式地址運算符返回調用對象的地址。
默認構造函數:
如果沒有提供任何構造函數,c++將創建默認構造函數。例如,定義了一個Klunk類,但沒有提供任何構造函數,則編譯器提供下面默認構造函數:
Klunk::Klunk(){}
編譯器提供了一個不接受任何參數,也不執行任何操作的構造函數。這是因爲創建對象時總會調用構造函數。
默認構造函數使Klunk類似於一個常規的自動變量,也就是說,它的值在初始化時是未知的。
如果定義了構造函數,C++將不會定義默認構造函數。如果希望在創建對象時不顯式地對它進行初始化,則必須顯式地定義構造函數。這種構造函數沒有參數。
Klunk::Klunk(){ ct = 0;}
帶參數的構造函數也可以是默認構造函數,只要所有參數都有默認值
Klunk(int n = 12){ ct = n;}
但,只能有一個默認構造函數。防止二義性
Klunk(){ct=0;}
Klunk(int n =0) {ct = n;}
不可同時存在
複製構造函數:
複製構造函數用於將一個對象複製到新創建的對象中。也就是說,它用於初始化過程中(包括按值傳遞),而不是常規的賦值過程中,類的複製構造原型如下:
class_name(const class_name &);
它接受一個指向類對象的常量引用作爲參考
何時調用(motto是一個stringbad對象):
- stringbad ditto(motto);
- stringbad * pstringbad = new stringbad(motto)
- stringbad metoo = motto;
- stringbad also = stringbad(motto)
其中,後兩種聲明可能會使用複製構造函數直接創建metoo和also,也可能使用複製構造函數生成一個臨時對象,然後將臨時對象的內容賦給metoo 和also,這取決於具體實現。第二種使用motto初始化一個匿名對象,並將新對象的地址賦給pstringbad 指針。
每當程序生成對象副本時,編譯器都將使用賦值構造函數。具體地說,當函數按值傳遞對象或返回對象。記住,按值傳遞意味着創建原始變量的一個副本。編譯器生成臨時對象時,也將使用複製構造函數。例如,將3個Vector對象相加時,編譯器可能生成臨時的Vector對象來保存中間結果。何時生成臨時對象隨編譯器而異。
由於按值傳遞對象將調用複製構造函數,因此應該按引用傳遞對象。這樣可以節省調用構造函數時間及存儲新對象的空間
默認的複製構造函數功能:
默認的構造函數逐個複製非靜態成員(成員複製也稱爲淺複製),複製的是成員的值。在上面的程序中,下面的語句
stringbad sailor = sports;
與
stringbad sailor;
sailor.str = sports.str;
sailor.len = sports.len;
是等效的
如果成員本身就是類對象,則將使用這個類的複製構造函數來複制該成員對象。
在上面的程序中,由於複製構造函數將地址複製,是淺拷貝,導致對象釋放時調用了兩次delete[],導致失敗。
可以定義一個複製構造函數來解決這個問題
stringbad::stringbad(const stringbad &sd) {
num_strings++;
str = new char[sd.len + 1];
std::strcpy(str,sd.str);
len = sd.len;
cout << num_strings << ": \"" << str << "\" object copy created\n";
}
其他問題:賦值運算符:
並不是程序中所有問題都可以歸咎於默認的複製構造函數,還有默認賦值運算符。默認原如下:
class_name & class_name::operator=(const class_name &)
在上面程序中加上
headline1 = headline2;
也會導致運行失敗。原因是默認賦值運算符使用的也是淺拷貝,導致同一地址被釋放兩次
解決賦值問題:
- 由於目標對象可能引用了以前分配的數據,所以函數應使用delete[]先釋放這些數據。
- 函數應當避免將對象賦給自身;否則,給對象重新賦值前,釋放內存操作可能刪除對象的內容。
- 函數返回一個指向調用對象的引用
因此,賦值運算符函數可以這樣:
stringbad & stringbad::operator=(const stringbad & st) {
if (this == &st){
return *this;
}
delete[] str;
len = st.len;
str = new char[len + 1];
strcpy(str,st.str);
return *this;
}
靜態成員函數:
可以將成員函數聲明爲靜態的(函數聲明必須包含關鍵字static,但如果函數定義是獨立的,則其中不能包含關鍵字static),這樣有兩個重要後果:
- 不能通過對象調用靜態成員函數;實際上,靜態成員函數沒有this指針。如果靜態成員函數是在公有部分聲明的,則可以使用類名和作用域解析運算符來調用它。
- 靜態成員函數不與特定的對象關聯,只能使用靜態數據成員。
進一步重載賦值運算符:
假設要將常規字符串複製到string對象中。例如,假設使用getline()讀取了一個字符串,並要將這個字符串放置到stringbad 對象中,前面的定義的類方法可以讓我們這樣寫
stringbad name;
char temp[40];
cin.getline(temp,40);
name = temp;
但如果經常這麼做,這將不是一個理想的解決方案,因爲最後一條語句這樣執行:
- 使用構造函數stringbad(const char *s) 創建一個臨時對象
- 使用stringbad & operator=(const stringbad &st) 賦值對象
- 調用析構函數刪除臨時對象
爲了減去創建和刪除臨時對象,可以定義賦值char * s的賦值函數
stringbad & stringbad::operator=(char *s) {
delete[] str;
len = std::strlen(s);
str = new char[len + 1];
strcpy(str,s);
return *this;
}
還可以實現operator>>()重載來直接讀到對象中
改進後的String類:
String.h
#ifndef D1_STRING_H
#define D1_STRING_H
#include <iostream>
class String {
private:
char * str;
int len;
static int nums_strings;
static const int CINLIM = 80;
public:
String(const char *s);
String();
String(const String &s);
~String();
int length() const { return len; };
String &operator=(const char *s);
String &operator=(const String & s);
char &operator[](int n);
const char &operator[](int n) const;
friend std::ostream &operator<<(std::ostream &os, const String &string);
friend bool operator<(const String &s1,const String &s2);
friend bool operator>(const String &s1,const String &s2);
friend bool operator==(const String &s1,const String &s2);
friend std::istream &operator>>(std::istream &is, String &s);
static int HowMany();
};
#endif //D1_STRING_H
String.cpp
#include "String.h"
#include <cstring>
int String::nums_strings = 0;
int String::HowMany() {
return nums_strings;
}
String::String(const char *s) {
len = strlen(s);
str = new char[len+1];
strcpy(str,s);
nums_strings++;
}
String::String() {
len = 0;
str = new char[1];
str[0] = '\0';
nums_strings++;
}
String::String(const String &s) {
len = s.len;
str = new char[len+1];
strcpy(str,s.str);
nums_strings++;
}
String::~String() {
--nums_strings;
delete [] str;
}
String &String::operator=(const char *s) {
len = strlen(s);
delete[] str;
str = new char[len+1];
strcpy(str,s);
return *this;
}
String &String::operator=(const String &s) {
if (*this == s){
return *this;
}
len = s.len;
delete[] str;
str = new char[len+1];
strcpy(str,s.str);
return *this;
}
char &String::operator[](int n) {
return str[n];
}
const char &String::operator[](int n) const {
return str[n];
}
std::ostream &operator<<(std::ostream &os, const String &s) {
os << s.str;
return os;
}
bool operator<(const String &s1, const String &s2) {
return (strcmp(s1.str,s2.str) < 0);
}
bool operator>(const String &s1, const String &s2) {
return s2 < s1;
}
bool operator==(const String &s1, const String &s2) {
return (strcmp(s1.str,s2.str) == 0);
}
std::istream &operator>>(std::istream &is, String &s) {
char temp[String::CINLIM];
is.get(temp,String::CINLIM);
if (is){
s = temp;
}
while (is && is.get() != '\n') continue;
return is;
}
main.cpp
#include <iostream>
#include "String.h"
const int ArSize = 10;
const int MaxLen = 81;
int main(){
using std::cout;
using std::cin;
using std::endl;
String name;
cout << "Hi, what's your name?\n";
cin >> name;
cout << name << ",please enter up to " << ArSize << " short sayings<empty line to quit>:\n";
String saying[ArSize];
char temp[MaxLen];
int i;
for (i =0;i<ArSize;i++){
cout << i + 1 << ":";
cin.get(temp,MaxLen);
while (cin && cin.get() != '\n') continue;
if (!cin || temp[0] == '\0') {
break;
} else {
saying[i] = temp;
}
}
int total = i;
if (total > 0){
cout << "Here are your sayings:\n";
for (i=0;i<total;i++){
cout << saying[i][0] << ": " << saying[i] << endl;
}
int shortest = 0;
int first = 0;
for (i=1;i<total;i++){
if (saying[i].length() < saying[shortest].length()) shortest = i;
if (saying[i] < saying[first]) first = i;
}
cout << "Shortest saying: " << saying[shortest] << endl;
cout << "First alphabetically: " << saying[first] << endl;
cout << "This program used: " << String::HowMany();
}
return 0;
}
在構造函數中使用new時應注意的事項:
使用new初始化對象的指針成員時必須特別小心:
- 如果在構造函數中使用new來初始化指針成員,應該在析構函數中使用delete。
- new和delete必須相互兼容。new對應於delete,new[] 對應於delete[]。
- 如果有多個構造函數,則必須使用相同的方式使用new,要麼都帶中括號,要麼都不帶。因爲只有一個析構函數,所有的構造函數都必須與它兼容。然而,可以在一個構造函數中使用new初始化指針,而在另一個構造函數中將指針初始化爲空,這是因爲delete(或delete[])都可用於空指針。
- 應定義一個複製構造函數,通過深度複製將一個對象初始化爲另一個對象。
- 應當定義一個賦值運算符,通過深度複製將一個對象複製給另一個對象。
應該與不該:
下面包含了兩個不正確的示例以及一個良好的構造函數示例:
String::String(){
str = "default string"; // err:no new[]
len = std::strlen(str);
}
String::String(const char *s){
len = std::strlen(str);
str = new char; // err: no new[]
std::strcpy(str,s); // err: no room
}
String::String(const char *s){
len = std::strlen(str);
str = new char[len+1];
std::strcpy(str,s);
}
包含類成員的類逐成員複製:
class Magazine{
private:
String title;
string publisher;
...
}
String 和 string 類都使用動態內存分配,這是否意味着需要爲Magazine類編寫複製構造函數和賦值運算符?不,至少對這個類來說是不需要的默認的逐成員複製和賦值行爲有一定的智能。如果將一個Magazine類對象複製或賦值給另一個Magazine類對象,逐成員複製將使用成員類型定義的複製或賦值函數。
有關返回對象的說明:
當成員函數或獨立的函數返回對象時,有幾種返回方式可供選擇。可以返回指向對象的引用、指向對象的const引用或const對象
返回指向const對象的引用:
使用const引用的常見原因是旨在提高效率,但對於何時可以採用這種方式存在一些限制。如果函數返回(通過調用對象的方法或將對象作爲參數)傳遞給它的對象,可以通過返回引用來提高其效率。例如
Vector force1(50,60);
Vector force2(10,70);
Vector max;
max = Max(forc1,forc2);
下面兩種實現都是可行的:
Vector Max(const Vector &v1,const Vector &v2){
if (v1.magval() > v2.maagval()){
return v1;
} else {
return v2;
}
}
const Vector & Max(const Vector &v1,const Vector &v2){
if (v1.magval() > v2.maagval()){
return v1;
} else {
return v2;
}
}
首先,返回對象對象調用復構造函數,而返回引用不會。因此,第二個版本做的工作更少,效率更高。其次,引用指向force1或force2,它們都是在調用函數中定義的,因此滿足這種條件。第三,v1,v2都被聲明爲const引用,因此返回類型必須爲const,這樣才匹配。
返回指向非const對象的引用:
兩種常見的返回非const對象情形是,重載賦值運算符以及重載與cout一起使用的<<運算符。前者這樣旨在提高效率,而後者必須這樣做。
返回對象:
如果返回的對象是被調用函數中的局部變量,則不應按引用的方式返回它,因爲在被調用函數執行完畢時,局部對象將調用其析構函數。因此,當控制權回到調用函數時,引用指向的對象將不再存在
使用指向對象的指針:
C++程序經常使用指向對象的指針,改寫上面main.cpp中使用數組索引獲取最長和首字母最小的方式,改爲使用兩個指針指向:
int total = i;
if (total > 0){
cout << "Here are your sayings:\n";
for (i=0;i<total;i++){
cout << saying[i][0] << ": " << saying[i] << endl;
}
String * shortest = &saying[0];
String * first = &saying[0];
for (i=1;i<total;i++){
if (saying[i].length() < shortest->length()) shortest = &saying[i];
if (saying[i] < *first ) first = &saying[i];
}
cout << "Shortest saying: " << *shortest << endl;
cout << "First alphabetically: " << *first << endl;
cout << "This program used: " << String::HowMany();
}
指針和對象小結:
-
使用常規表示法來聲明指向對象的指針:
String * glamor;
-
可以將指針初始化指向已有對象:
String * first = &saying[0];
-
可以使用new來初始化指針,這將創建一個新的對象
String * favorite = new String(saying[0])
-
對類使用new將調用相應的類構造函數來初始化新創建的對象
String *gleep = new String; // default constructor String *glop = new String("12")
-
可以使用->運算符通過指針訪問類方法
-
可以通過對對象指針應用解除引用運算符*來獲取對象。
再談定位new運算符:
定位運算符能夠在分配內存時指定內存位置。
placenew1.cpp
#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF = 512;
class JustTesting{
private:
string words;
int number;
public:
JustTesting(const string &s="Just Testing", int n = 0){words = s;number=n;cout<<words<<" constructed\n";}
~JustTesting(){ cout << words << " destroyed\n";}
void show() const { cout << words << ", " << number << endl;}
};
int main(){
char * buffer = new char[BUF];
JustTesting *pc1, *pc2;
pc1 = new(buffer) JustTesting;
pc2 = new JustTesting("Heap1",20);
cout << "Memory block address:\n" << "buffer: " << (void *)buffer << " heap: " << pc2 << endl;
cout << "Memorry contents:\n";
cout << pc1 << ": "; pc1->show();
cout << pc2 << ": "; pc2->show();
JustTesting *pc3, *pc4;
pc3 = new(buffer)JustTesting("Bad Idea", 6);
pc4 = new JustTesting("Heap2",10);
cout << "Memorry contents:\n";
cout << pc3 << ": "; pc3->show();
cout << pc4 << ": "; pc4->show();
delete pc2;
delete pc4;
delete[] buffer;
cout << "Done\n";
return 0;
}
結果
Just Testing constructed
Heap1 constructed
Memory block address:
buffer: 0x224ec20 heap: 0x224f240
Memorry contents:
0x224ec20: Just Testing, 0
0x224f240: Heap1, 20
Bad Idea constructed
Heap2 constructed
Memorry contents:
0x224ec20: Bad Idea, 6
0x224f270: Heap2, 10
Heap1 destroyed
Heap2 destroyed
Done
程序在使用定位符時存在兩個問題,首先,在創建第二個對象時,定位new運算符使用一個新對象來覆蓋用於第一對象的內存單元。顯然,如果類動態地爲其成員分配內存,這將引發問題。
其次,delete 用於pc2和pc4時,將自動調用爲pc2和pc4指向的對象調用析構函數;然而,將delete[]用於buffer時,不會爲使用定位new運算符創建的對象調用析構函數。
在buffer中使用不同的內存單元。程序員需要提供兩個位於緩衝區不同的地址,並確保兩個內存單元不會重疊。例如,可以這樣定義
pc3 = new(buffer + sizeof(*pc1))JustTesting("Bad Idea", 6);
第二個教訓是,如果使用定位new運算符來爲對象分配內存,必須保證其析構函數被調用。但如何確保呢?對於在堆中創建的對象,可以:delete pc2 但是不能這樣做 delete pc1; delete pc3;
原因在於delete可與常規new運算符配合使用,但不能與定位new運算符配合使用。例如,指針pc3沒有收到new運算符返回的地址,因此delete pc3將導致運行階段錯誤。在另一方面,指針pc1指向的地址與buffer相同,但buffer是使用new[]初始化的,因此必須使用delete[]而不是delete來釋放。即使buffer是使用new[]而不是new初始化的,delete pc1也將釋放buffer,而不是pc1。這是因爲new/delete系統知道已分配的512字節塊buffer,但對定位new運算符對該內存塊做了何種處理一無所知。
delete[] buffer釋放了使用常規new運算符分配的整個內存塊,但它沒有爲定位new運算符在該內存塊中創建的對象調用析構函數,解決方法是,顯式地調用析構函數
pc3->~JustTesting();
pc1->~JustTesting();
需要注意的是刪除順序,對於使用定位new運算符創建的對象,應以與創建順序相反的順序進行刪除。原因在於,晚創建的對象可能依賴於早創建的對象。另外,僅當所有對象銷燬後才能釋放用於存儲這些對象的緩存區。
隊列模擬:
Queue.h
#ifndef D1_QUEUE_H
#define D1_QUEUE_H
template <class Item>
class Queue {
enum {Q_SIZE = 10};
struct Node {Item item; struct Node *next;};
Node *front;
Node *end;
int items;
const int qsize;
public:
Queue(const Queue &queue);
Queue &operator=(const Queue &queue);
Queue(int qs= Q_SIZE);
~Queue();
bool isempty() const;
bool isfull() const;
int queuecount() const;
bool add(const Item &item);
bool get(Item &item);
};
template<class Item>
bool Queue<Item>::get(Item &item) {
if (front == nullptr) return false;
item = front->item;
items--;
Node * temp = front;
front = front->next;
delete temp;
if (items == 0) end = nullptr;
return true;
}
template<class Item>
bool Queue<Item>::add(const Item &item) {
if (isfull()) return false;
Node *node = new Node;
node->item = item;
node->next = nullptr;
items++;
if (front == nullptr){
front = node;
} else {
end->next = node;
}
end = node;
return true;
}
template<class Item>
int Queue<Item>::queuecount() const {
return items;
}
template<class Item>
bool Queue<Item>::isfull() const {
return items == qsize;
}
template<class Item>
bool Queue<Item>::isempty() const {
return items == 0;
}
template<class Item>
Queue<Item>::~Queue() {
Node * temp;
while (front != nullptr){
temp = front;
front = front->next;
delete temp;
}
}
template<class Item>
Queue<Item>::Queue(int qs) :qsize(qs){
front = end = nullptr;
items = 0;
}
template<class Item>
Queue<Item>::Queue(const Queue &queue) :qsize(queue.qsize) {
front = end = nullptr;
items = 0;
Node *temp = queue.front;
while (temp != nullptr){
this->add(temp->item);
temp = temp->next;
}
}
template<class Item>
Queue<Item> &Queue<Item>::operator=(const Queue &queue) {
delete front;
delete end;
front = end = nullptr;
items = 0;
Node *temp = queue.front;
while (temp != nullptr){
this->add(temp->item);
temp = temp->next;
}
}
#endif //D1_QUEUE_H