1. 程式人生 > >類和動態記憶體分配(1)

類和動態記憶體分配(1)

假設我們要建立一個類,其中有一個成員表示某人的姓,最簡單的就是用字串陣列來儲存,開始使用14個字元的陣列,發現太小,保險的方法是使用40個字元的陣列,但是當建立2000多個這個樣的物件時,必定會造成記憶體浪費。通常使用string類,該類有良好的記憶體管理細節。但是這樣就沒有機會深入的學習記憶體管理了。

c++在記憶體分配方面,採用這樣的策略,在程式執行時決定記憶體分配,而不是編譯時決定。使用new和delete運算子進行動態控制記憶體,但在類中使用這些運算子將導致新的程式設計問題,這時候解構函式是必不可少的,而不再是可有可無的。有時候還必須過載賦值運算子。

複習new、delete和靜態成員變數的工作原理:


首先我們設計一個string類stringbad,之所以bad是因為這個類存在比較多的問題。

//stringbad.h
#include <iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_
class StringBad 
{
private :
      char *str;
       int len;
       static int num_strings;
 public:
       StringBad();
       StringBad(const char * s);
       ~StringBad
(); //friend friend std::ostream &operator<<(std::ostream &os, const StringBad &st); }; #endif
//stingbad.cpp
#include "stringbad.h"

int StringBad::num_strings = 0;

StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	std::strcpy(str, "c++");
	num_strings++;
	std:
:cout << "string create:" << str << " , " << num_strings << " object created" << std::endl; } StringBad::StringBad(const char * s) { len = std::strlen(s); str = new char[len + 1]; std::strcpy(str, s); num_strings++; std::cout << "string create:"<<str<<" , "<<num_strings << " object created" << std::endl; } StringBad::~StringBad() { num_strings--; std::cout << "string delete:" << str << " , " << num_strings << " object left" << std::endl; delete[]str; } std::ostream & operator<<(std::ostream & os, const StringBad & st) { os << st.str; return os; }

有幾點需要注意:

  • 這個類使用了char指標成員,而不是char陣列,意味著類宣告沒有為字串本身分配儲存空間,而是在建構函式中使用new來為字串分配空間,這樣避免的在類宣告中預先定義了字串的長度。
  • num_strings宣告為靜態成員,靜態成員的特點是:無論建立多少物件,程式都只建立一個靜態成員類變數副本,即該類的所有物件共享一個靜態成員。
    int StringBad::num_strings = 0;
    這條語句將靜態成員num_strings初始化為0。注意,不能在類宣告中初始化靜態成員變數,因為宣告只是描述瞭如何分配記憶體,但並不分配記憶體。對於靜態成員,可以在類宣告之外使用單獨的語句進行初始化,這是因為靜態類成員是單獨儲存的,而不是物件的組成部分。初始化語句指明瞭型別,並使用了作用於運算子,但是沒有使用關鍵字static。
    靜態成員在類宣告中宣告,在包含類方法的檔案中初始化。初始化時使用作用域運算子來表示靜態成員所屬的類。但如果靜態成員是const整數型別或者列舉時,則可以在類宣告中初始化。
//mian.cpp
#include"stringbad.h"
void callme1(StringBad &s)
{
	std::cout << "按引用傳遞" << std::endl;
	std::cout << s << std::endl;
}
void callme2(StringBad s)
{
	std::cout << "按值傳遞" << std::endl;
	std::cout << s << std::endl;
}
void main()
{
	std::cout << "---begin---" << std::endl;
	{
		StringBad A("the first string");
		StringBad B("the second string");
		StringBad C("the third string");
		StringBad D;
		StringBad E=A;

		callme1(A);
		std::cout << A << std::endl;
		callme2(B);
		std::cout << B << std::endl;

	}
	std::cout << "---end---" << std::endl;
}

編寫上面的檔案對生成的類進行測試,執行結果如下:
在這裡插入圖片描述
程式碼執行到callme1(A);沒什麼問題,關鍵就是下面的callme2(B);這個callme2函式是按值傳遞的。
將B作函式為引數來傳遞,從而導致解構函式被呼叫。雖然按值傳遞 可以防止原始引數被修改,但實際函式已使原始字元無法識別導致一些非法字元。

首先分析下程式:

  • 建立了4個物件A B C D E ,其中
    StringBad E=A;
    沒有使用我們定義的那兩個建構函式,這時候上面的語句等效於下面的句子:
    StringBad E=StringBad(A);
    因此相應的建構函式的原型應是:
    StringBad(const StringBad &);
    當使用一個物件去初始化另一個物件時,自動成上述的建構函式,稱為複製建構函式,該函式建立了一個物件的副本。自動生成的複製建構函式不知道需要更新num_strings

  • 呼叫按引用傳遞的函式callme2(B),為了不修改B,通過預設複製建構函式建立了一個B的副本,這時,副本和B的指標成員指向的是同一個地址,這個副本的作用於只限於這個函式內,函式結束時,將析構這個副本物件,但同時也將B的字串指標也釋放了,導致後面B析構出現問題,試圖對釋放過的記憶體進行釋放操作是很危險的。

  • 物件的析構順序和建立順序是相反的,正常的刪除順序應該是 E D C B A ,刪除E D C是正常的,但是刪除B出現了錯誤。程式終止,A沒有析構成功,並沒有執行最後一句。

特殊成員函式:

  • 預設建構函式,如果沒有定義建構函式
  • 預設解構函式,如果沒有定義
  • 複製建構函式,如果沒有定義
  • 賦值運算子,如果沒有定義
  • 地址運算子,如果沒有定義

上述程式的問題就是由隱式複製建構函式和隱式賦值運算子引起的。
預設建構函式,不接受任何引數,也不進行任何操作。
隱式地址運算子返回呼叫物件的地址,即this指標的值,這正是我們需要的。

複製建構函式
用於複製一個物件到新建立的物件中,即用於初始化(包括按值傳遞引數),而不是常規的賦值過程中。
原型:Class_name (const Class_name &);//指向物件的常量引用作為引數
何時呼叫:
StringBad jack(luck);
StringBad jack=luck;
StringBad jack=StringBad(luck);
StringBad * jack=new StringBad(luck);
函式按值傳遞和返回物件.

預設複製建構函式
預設的賦值建構函式逐個賦值非靜態成員(成員複製也稱為淺複製)。
如果成員本身是其他類物件,則將使用這個類的複製建構函式來複製成員變數。
靜態成員和靜態變數不受影響,因為他們屬於整個類而不是各個物件。

定義一個顯式的複製建構函式和改進的解構函式解決上述問題:

StringBad::StringBad(const StringBad & s)
{
	num_strings++;
	len = s.len + 1;
	str = new char[len + 1];
	std::strcpy(str, s.str);
	std::cout << "string create:" << str << " , " 
			  << num_strings << " object created" << std::endl;
}

必須定義複製建構函式的原因在於:一些類成員是使用new初始化、指向指標的資料,而不是資料本身。
深複製和淺複製:深複製是複製指向的資料而不是指標,淺複製僅僅複製指標資訊,而不會深入“挖掘”以複製指標引用的結構。

賦值運算子:
過載賦值運算子:
原型:Class_name& Class_name ::operator =(const Class_name &);//接受並返回一個指向類物件的引用
何時呼叫:

  • 將已有的物件賦給另一個物件時:
    StringBad head(“this is a string”);
    StringBad k;
    k=head;
  • 初始化物件時,並不一定使用賦值運算子:
    StringBad met=k;
    新建立一個物件met,被初始化為k的值,因此使用複製建構函式。但是實現時可能分兩步來處理:使用複製建構函式建立一個臨時物件,然後通過賦值將臨時物件的值複製到新物件中。
    也就是說,初始化總是呼叫複製建構函式,而使用=運算子也允許呼叫賦值運算子。

與複製建構函式相似,賦值運算子的隱式實現也是對每個成員進行逐個複製。如果成員本身是類物件,這使用該類的賦值運算子來複制該成員,但靜態成員不受影響。

StringBad & StringBad::operator=(const StringBad & s)
{
	if (this == &s)
		return *this;

	delete[]str;//重置原來的字串
	len = s.len;
	str = new char[len + 1];
	std::strcpy(str, s.str);
	return *this;
}

與複製建構函式的一些差別:

  • 由於目標物件可能引用了以前分配的資料,因此必須使用delete[]進行重置,釋放這些資料
  • 函式應該避免將物件賦給自己;否則,給物件重新賦值前,釋放記憶體操作可能刪除物件的內容
  • 函式返回一個指向呼叫物件的引用,可以編寫s0=s1=s2;這樣的程式碼