C++-類和動態內存分配 大發彩_票平臺開發
地址一:【hubawl.com】狐霸源碼論壇
地址二:【bbscherry.com】
類和動態內存分配
- 動態內存和類
C++在分配內存時是讓程序在運行時決定內存分配,而不是在編譯時決定。 這樣,可根據程序的需要,而不是根據一系列嚴格的存儲類型規則來使用內存。C++使用new和delete運算符來動態控制內存。
1.1. 復習示例和靜態類成員
這個程序使用了一個新的存儲類型:靜態類成員。
//strngbad.h
#include<iostream>
#ifndef STRNGBADH
#define STRNGBADH
class StringBad
{
private:
char str; //指向字符串的指針
static int num_strings; //對象個數
public:
StringBad(const char s); //構造函數
StringBad(); //默認構造函數
~StringBad(); //析構函數
//友元函數
friend std::ostream & operator<<(std::ostream & os,
const StringBad & st);
};
#endif // !STRNGBADH
註意:
首先,它使用char指針(而不是char數組)來表示姓名。這意味著類聲明沒有為字符串本身分配存儲空間,而是在構造函數中使用new來為字符串分配空間。 這避免了在類聲明中預先定義字符串的長度。
其次,將num_strings成員聲明為靜態存儲類。 靜態類成員有一個特點:無論創建了多少對象,程序都只創建一個靜態類變量副本。 也就是說,類的所有對象共享同一個靜態成員。 假設創建了10個StringBad對象,將有10個str成員和10個len成員,但只有一個共享的num_strings成員。這對於所有類對象都具有相同值的類私有數據是非常方便的。 例如,num_strings成員可以記錄所創建的對象數目。
//strngbad.cpp
#include<cstring>
#include"strngbad.h"
using std::cout;
//初始化靜態類成員
int StringBad::num_strings = 0;
//類方法
//以C字符型構造StringBad
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 = 4;
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;
}
註意: 不能在類聲明中初始化靜態成員變量,這是因為聲明描述了如何分配內存,但並不分配內存。 對於靜態類成員,可以在類聲明之外使用單獨的語句來進行初始化,這是因為靜態類成員是單獨存儲的,而不是對象的組成部分。
析構函數:str成員指向new分配的內存。當StringBad對象過期時,str指針也將過期。 但str指向的內存仍被分配,除非使用delete將其釋放。 刪除對象可以釋放對象本身占用的內存,但並不能自動釋放屬於對象成員的指針指向的內存。因此,必須使用析構函數。 在析構函數中使用delete語句可確保對象過期時,由構造函數使用new分配的內存被釋放。
//vegnews.cpp
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using std::cout;
#include"strngbad.h"
void callme1(StringBad &); //傳遞引用
void callme2(StringBad); //按值傳遞
int main()
{
using std::endl;
{
cout << "Starting an inner block.\n";
StringBad headline1("Celery Stalks at Midnight");
StringBad headline2("Lettuce Prey");
StringBad sports("Spinach Leaves Bowl 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 onject 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";
std::cin.get();
return 0;
}
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";
}
程序開始時還是正常的,但逐漸變得異常,最終導致了災難性結果。
首先來看正常的部分。 構造函數指出自己創建了3個StringBad對象,並為這些對象進行了編號,然後程序使用重載運算符<<列出這些對象:
1: "Celery Stalks at Midnight"object created
2: "Lettuce Prey" object created
3: "Spinach Leaves Bowl forDollars" object created
headline1: Celery Stalks at Midnight
headline2: Lettuce Prey
sports: Spinach Leaves Bowl for Dollars
然後,程序將headline1傳遞給callme1()函數,並在調用後重新顯示headline1。 代碼如下:
callme1(headline1);
cout<< "headline1: " << headline1 << endl;
下面是運行結果:
String passed by reference:
"Celery Stalks at Midnight"
headline1: Celery Stalks at Midnight
這部分代碼看起來也正常。
但隨後程序執行了如下代碼:
callme2(headline2);
cout<< "headline2: " << headline2 << endl;
這裏,callme2()按值(而不是按引用)傳遞headline2,結果表明這是一個嚴重的問題!
String passed by value:
"Lettuce Prey"
"Lettuce Prey" object deleted, 2left
headline2: 葺葺葺葺葺葺葺葺軮
將headline2作為函數參數來傳遞從而導致析構函數被調用。
另外,下面的代碼:
StringBad sailor= sports;
這使用的是哪個構造函數呢?不是默認構造函數,也不是參數為const char*的構造函數。記住,這種形式的初始化等效於下面的語句:
StringBad sailor = StringBad(sports); //使用sports的構造函數
因為sports的類型為StringBad,因此相應的構造函數原型應該如下:
StringBad(const StringBad &);
當您使用一個對象來初始化另一個對象時,編譯器將自動生成上述構造函數(稱為復制構造函數,因為它創建對象的一個副本)。 自動生成的構造函數不知道需要更新靜態變量num_string,因此會將計數方案搞亂。 實際上,這個例子說明的所有問題都是由編譯器自動生成的成員函數引起的。
1.1. 特殊成員函數
StringBad類的問題是由特殊成員函數引起的。 這些成員函數是自動定義的,就StringBad而言,這些函數的行為與類設計不符。 具體地說,C++自動提供了這些成員函數:
默認構造函數,如果沒有定義構造函數;
默認析構函數,如果沒有定義;
復制構造函數,如果沒有定義;
賦值運算符,如果沒有定義;
地址運算符,如果沒有定義。
(1) 默認構造函數:
如果沒有提供任何構造函數,C++將創建默認構造函數。 例如,加入定義了一個Klunk類,但沒有提供任何構造函數,則編譯器將提供下述默認構造函數:
Klunk::Klunk() { } //隱式默認構造函數
Klunk lunk; //調用默認構造函數
默認構造函數使Lunk類似於一個常規的自動變量,也就是說,它的值在初始化時是未知的。
如果定義了構造函數,C++將不會定義默認構造函數。 如果希望在創建對象時不顯式地對它進行初始化,則必須顯式地定義默認構造函數。這種構造函數沒有參數,但可以使用它來設置特定地值:
Klunk::Klunk() //顯式默認構造函數
{
klunk_ct=0;
…
}
帶參數的構造函數也可以是默認構造函數,只要所有參數都有默認值。 例如,Klunk類可以包含下述內聯構造函數:
Klunk(int n = 0) {klunk_ct = n;}
但只能有一個默認構造函數。 也就是說,不能這樣做:
Klunk() {klunk_ct = 0;} //構造函數#1
Klunk(int n =0) {klunk_ct=n;} //具有二義性的構造函數#2
例如:
Klunk kar(10); //明確地與#1匹配
Klunk bus; //與兩個構造函數均可匹配
第二個聲明既與構造函數#1(沒有參數)匹配,也與構造函數#2(使用默認參數0)匹配。
(2) 復制構造函數
復制構造函數用於將一個對象復制到新創建的對象中。 類的復制構造函數的原型如下:
Class_name(const Class_name &);
它接受一個指向類對象的常量引用作為參數。 例如,StringBad類的復制構造函數原型如下:
StringBad(const StringBad &);
對於復制構造函數,需要知道兩點:何時調用和有何功能。
(3) 何時調用復制構造函數
新建一個對象並將其初始化為同類現有對象時,復制構造函數都將被調用。 這在很多情況下都可能發生,最常見的情況是將新對象顯式地初始化為現有地對象。例如,假設motto是一個StringBad對象,則下面4種聲明都將調用復制構造函數:
StringBadditto(motto); //調用StringBad(const StringBad &)
StringBad metoo =motto; //調用StringBad(const StringBad &)
StringBad also = StringBad(motto); //調用StringBad(const StringBad &)
StringBad*pStringBad = new StringBad(motto);
////調用StringBad(constStringBad &)
其中中間的兩種聲明可能會使用復制構造函數直接創建metoo和also,也可能使用復制構造函數生成一個臨時對象,然後將臨時對象的內容賦給metoo和also,這取決於具體的實現。 最後一種聲明使用motto初始化一個匿名對象,並將新對象的地址賦給pstring指針。
每當程序生成了對象副本時,編譯器都將使用復制構造函數。 由於按值傳遞對象將調用復制構造函數,因此應該按引用傳遞對象。 這樣可以節省調用構造函數的時間以及存儲新對象的空間。
(4) 默認的復制構造函數的功能
默認的復制構造函數逐個復制非靜態成員(成員復制也稱為淺復制),復制的是成員的值。
StringBad sailor = sports;
與下面代碼等價(由於私有成員是無法訪問的,因此這些代碼不能通過便於):
StringBad sailor;
sailor.str=sports.str;
sailor.len=sports.len;
如果成員本身就是類對象,則將使用這個類的復制構造函數來復制成員對象。 靜態函數(如num_strings)不受影響,因為它們屬於整個類,而不是各個對象。
1.2. 回到StringBad:復制構造函數的哪裏出了問題
當callme2()被調用時,復制構造函數被用來初始化callme2()的形參,還被用來將對象sailor初始化為對象sports。 默認的復制構造函數不說明其行為,因此它不指出創建過程,也不增加計數器num_strings的值。但析構函數更新了計數,並且在任何對象過期時都將被調用,而不管對象是如何被創建的。 這是一個問題,因為這意味著程序無法準確地記錄對象計數。 解決方法是提供一個對計數進行更新地顯式復制構造函數:
StringBad::StringBad(const StringBad & s)
{
num_strings++;
...
}
第二個異常之處更微妙,也更危險:
headline2: 葺葺葺葺葺葺葺葺軮
原因在於隱式復制構造函數是按值進行復制的。 隱式復制構造函數的功能相當於:
sailor.str = sports.str;
這裏復制的並不是字符串,而是一個指向字符串的指針。 也就是說,將sailor初始化為sports後,得到的是兩個指向同一個字符串的指針。 當operator<<()函數使用指針來顯示字符串時,這並不會出現問題。但當析構函數被調用時,這將引發問題。
(1) 定義一個顯式復制構造函數以解決問題
解決類設計種這種問題的方法時進行深度復制(deep copy)。 也就是說,復制構造函數應當復制字符串並將副本的地址賦給str成員,而不僅僅是復制字符串地址。 這樣每個對象都有自己的字符串,而不是引用另一個對象的字符串。 調用析構函數時都將釋放不同的字符串,而不會試圖去釋放已經被釋放的字符串。可以這樣編寫String的復制構造函數:
StringBad::StringBad(const StringBad & st)
{
num_strings++; //處理靜態成員更新
len= st.len; //相同長度
str= new char[len + 1]; //分配空間
std::strcpy(str,st.str); //將字符串復制到新位置
cout<< num_strings << ":\"" << str
<< "\" object created\n";
}
必須定義復制構造函數的原因在於,一些類成員是使用new初始化的、指向數據的指針,而不是數據本身。
1.3. StringBad的其他問題:賦值運算符
C++允許類對象賦值,這是通過自動為類重載賦值運算符實現的。 這種運算符的原型如下:
Class_name & Class_name::operator=(constClass_name &);
它接受並返回一個指向類對象的引用。 例如,StringBad類的賦值運算符的原型如下:
StringBad & StringBad::operator=(constStringBad &);
(1) 賦值運算符的功能以及何時使用它
StringBad headline1(“Celery Stalks atMidnight”);
…
StringBad knot;
Knot = headline1; //賦值運算符被調用
初始化對象時,並不一定會使用賦值運算符:
StringBad metoo=knot; //可能使用復制構造函數,也可能是賦值運算符
這裏,metoo是一個新創建的對象,被初始化為knot的值,因此使用復制構造函數。 然而,正如前面指出的,實現時也可能分兩步來處理這條語句:使用復制構造函數創建一個臨時對象,然後通過賦值將臨時對象的值復制到新對象中。這就是說,初始化總是會調用復制構造函數,而使用=運算符時也允許調用賦值運算符。
(2) 解決賦值的問題
對於由於默認賦值運算符不合適導致的問題,解決辦法時提供賦值運算符(進行深度復制)定義。 其實現與復制構造函數相似,但也有一些差別。
由於目標對象可能引用了以前分配的數據,所以函數應使用delete[]來釋放這些數據。
函數應當避免將對象賦給自身;否則,對對象重新賦值前,釋放內存操作可能刪除對象的內容。
函數返回一個指向調用對象的引用。
StringBad & StringBad::operator=(const StringBad & st)
{
if (this == &st) //對象賦值給自身
return *this; //結束
delete[] str; //釋放老字符串
len= st.len;
str= new char[len + 1]; //為新字符串開辟空間
std::strcpy(str,st.str); //復制字符串
return *this;
}
如果地址相同,程序將返回*this,然後結束。 如果地址不同,函數將釋放str指向的內存,這是因為稍後把一個新字符串的地址賦給str。 如果不首先使用delete運算符,則上述字符串將保留在內存中。賦值操作並不創建新的對象,因此不需要調整靜態數據成員num_strings的值。
C++-類和動態內存分配 大發彩_票平臺開發