深入C++05:運算子過載

booker 發表於 2022-06-08
C++

📕運算子過載

1.複數類

運算子過載目的:使物件運算表現得和編譯器內建型別一樣;

複數類例子

#include<iostream>

using namespace std;

class CComplex{
public:
    CComplex(int r = 0, int l = 0): mreal(r), mimage(l) {}
    void show() {
        cout << "實部:" << mreal << "虛部:" << mimage << endl;
    }
    CComplex operator+(const CComplex &temp) { // 一個引數 相當於xxx.operator+(temp);
        return CComplex(mreal + temp.mreal, mimage + temp.mimage);//物件優化,減少生成一個物件的構造和析構過程;
    }
    CComplex& operator++() {//先加,前置的
        mreal++;
        mimage++;
        return *this;
    }
    CComplex operator++(int) {//後加,後置的
        return CComplex(mreal++, mimage++);
    }
    void operator+=(const CComplex &temp) {
        mreal += temp.mreal;
        mimage += temp.mimage;
    }
private:
    int mreal;
    int mimage;
    friend CComplex operator+(const CComplex &other, const CComplex &temp);
    friend ostream& operator<<(ostream &out, const CComplex &temp);
    friend istream& operator>>(istream &in, CComplex &temp);
   
};
CComplex operator+(const CComplex &other, const CComplex &temp) { //兩個引數, ::operator+(other, temp);
    return CComplex(other.mreal + temp.mreal, other.mimage + temp.mimage);
}
ostream& operator<<(ostream &out, const CComplex &temp) { //全域性函式好
    out << temp.mreal << " " << temp.mimage << endl;
    return out;
}
istream& operator>>(istream &in, CComplex &temp) { //設定成全域性函式好,不能設定為const,因為需要改變
    in >> temp.mreal >> temp.mimage;
    return in;
}
int main() {
    CComplex comp1(10, 20);
    CComplex comp2(20, 30);
    CComplex comp3 = comp1 + comp2; //如果呼叫類方法:comp1.operator+(comp2); 如果呼叫全域性方法:::operator+(comp1, comp2);優先呼叫全域性方法
    comp3.show();
    CComplex comp4 = comp3 + 20;//呼叫全域性方法和類方法都可以,因為知道comp3是什麼類,然後會將20轉換成對應的類;
    comp4 = 20 + comp3;//只能通過呼叫全域性方法,呼叫類方法是不成功的,因為20不知道是什麼類;
    comp4.show();
    ++comp4;
    comp4.show();
    CComplex comp5 = comp4++;;
    comp5.show();
    cout << comp5 << comp4 << endl;
    return 0;
}

image-20220322194246008

2.實現string類

#include<iostream>
#include<string>

using namespace std;

class String {
public:
    String(const char* str = nullptr) {
        if (str != nullptr) {
            len = strlen(str);
            _data = new char[len + 1];
            strcpy(_data, str);

        }
        else {
            len = 0;
            _data = new char[1];
            _data[0] = '\0';
        }
    }
    ~String() {
        delete[]_data;
        _data = nullptr;
    }
    String(const String& other) { //拷貝構造
        _data = new char[other.len + 1];
        strcpy(_data, other._data);
        len = other.len;
    }
    String operator=(const String& other) { //賦值過載
        if (*this == other) return *this;

        delete[]_data;
        _data = new char[other.len + 1];
        strcpy(_data, other._data);
        return *this;
    }
    char operator[](const int index) { //可以改值
        if (index >= len) throw "已經越界啦";
        return _data[index];
    }
    const char operator[](const int index) const { //不可以改值
        if (index >= len) throw "已經越界啦";
        return _data[index];
    }
    size_t size() { return len; }
    String operator+=(const String& str) {
        len += str.len;
        char* newstring = new char[len + 1];
        strcpy(newstring, _data);
        strcat(newstring, str._data);
        delete[]_data;
        _data = newstring;
        return *this;
    }
    bool operator==(const String& str) {
        return strcmp(str._data, _data) == 0;
    }
private:
    char* _data;
    size_t len;
    friend String operator+(const String& temp, const String& other);
    friend ostream& operator<<(ostream& out, const String& other);
    friend istream& operator>>(istream& in, String& other);
};
String operator+(const String& temp, const String& other) {
    char* p = new char[temp.len + other.len + 1]; 
    strcpy(p, temp._data);
    strcat(p, other._data);//追加
    String res(p); //res 也要new和delete
    delete[]p;
    return res;
}
ostream& operator<<(ostream& out, const String& other) { //輸出
    out << other._data;//不用加*,按地址輸出全部
    return out;
}
istream& operator>>(istream& in, String& other) { //輸入,每次輸出都標記為重新輸入
    char temp[1000];
    in >> temp;
    delete[]other._data;
    other._data = new char[strlen(temp) + 1];
    strcpy(other._data, temp);
    return in;
}
int main() {
    String test = "abc";
    test += "d";
    cout << test;
    const char* p = "abc";
    cout << *p << " " << p << endl;//cout 從地址不斷輸入資料,而*p讀到的就是地址的值;
    cin >> test;
    cout << test << endl;
    cout << test[2] << endl;
    return 0;
}

存在問題:

在operator+的過載函式中,存在兩次new和兩次delete,浪費記憶體和時間,可以優化一下:

String operator+(const String& temp, const String& other) {
    //char* p = new char[temp.len + other.len + 1]; 
    String res; 
    res._data =  new char[temp.len + other.len + 1];
    strcpy(res._data, temp._data);
    strcat(res._data, other._data);
    return res; //只有一次的new和delete
}

3.實現string字串物件的迭代器iterator

為什麼需要迭代器:因為資料往往存在類的成員屬性中,比如上述的String類的char *_data,如果我想一個個訪問資料,我需要也用一個char* 的指標指向—_data,而事實是我們不可以訪問物件的private屬性!所以需要迭代器來實現;

迭代器:要訪問順序容器和關聯容器中的元素,需要通過"迭代器(iterator)"進行,提供一種統一的方式,來透明地遍歷容器

迭代器是一個變數,相當於容器和操縱容器的演算法之間的中介。迭代器可以指向容器中的某個元素,通過迭代器就可以讀寫它指向的元素。從這一點上看,迭代器和指標類似。

迭代器有正向迭代器(容器類名::iterator 迭代器名;)、常量正向迭代器(容器類名::const_iterator 迭代器名;)、反向迭代器(容器類名::reverse_iterator 迭代器名;)、常量反向迭代器(容器類名::const_reverse_iterator 迭代器名;)

遍歷例子:

string str1 = "hello world!";//str1叫容器嗎?,可以,因為str1其底層放了一組char
//iterator即容器的迭代器
string::iterator it = str1.begin();
for (; it!=str1.end(); ++it)
{
	cout << *it << " ";
}
cout << endl;

說明:

image-20220323210201662

泛型演算法:比如sort等;泛型演算法:給所有容器都可以使用,引數接受的都是容器的迭代器。

我們要知道:

  • 如上面所說,str1物件中存入了各種各樣的字元,字串型別底層的成員變數為私有的,我們無法看見。
  • 容器有一個begin()方法,begin()返回它底層的迭代器的表示,it迭代器指向容器的首元素位置。
  • 容器中還有一個end()方法,end()表示容器中最後一個元素的後繼位置,迴圈中it!=end(),++it,將其遍歷一遍。
  • 底層無論是陣列還是連結串列什麼的,如何從當前元素遍歷到下一個元素,我們不需要操心;容器底層元素真真正正從一個元素跑到下一個元素,不同資料結構,不同差異都封裝在迭代器++運算子過載函式中,我們一般採用前置++(後置++比前置++多一個臨時物件的構造和析構過程)
  • 迭代器還需要提供 * 運算子過載,訪問迭代器所迭代元素的值,迭代器解引用訪問的就是容器底層資料。

String 迭代器的實現:

class String {
 public:
    ..... //上面的程式碼
    class iterator {
    public:
        iterator(char *p = nullptr):_p(p) {}
        bool operator!=(const iterator& it) {
            return _p != it._p;
        }
        void operator++() { //前置++,減少臨時物件的構造;
            ++_p;
        }
        char& operator*() {
            return *_p;
        }
    private:
        char* _p;
    };
    iterator begin() { //不能採用引用,因為是臨時物件
        return iterator(_data);//構造臨時迭代器,指向首元素;
    }
    iterator end() { //不能採用引用,因為是臨時物件
        return iterator(_data + len);//構造臨時迭代器,指向末元素的後繼位置;
    }
private:
    .....
};
int main() {
    String test = "hello word";
    //String::iterator it = test.begin();
    auto it = test.begin();  //用自動推導遍歷auto 代替 String::iterator 
    for (; it != test.end(); ++it) { //
        cout << *it << " ";
    }
    cout << endl;
    for (char ch : test) { // foreach內部也是用iterator實現,如果沒有iterator會發生錯誤;
        cout << ch << " ";
    }
    return 0;
}

image-20220323213844737

4.實現vector容器的迭代器iterator

直接上手程式碼,注意👀多瞭解選擇const,&,這些加和不加的問題;

template<typename T>
class vector {//和之前的模板vector一樣
public:
    ....
    class iterator {
    public:
        iterator( T *p): _p(p) {} //不可以加const,因為加了const的話會有_p(T*) = p(const T*),這樣會型別轉換錯誤;

        //相信一句話:如果不改變值,就加const,引數為const,可接受const和非const,函式為const,可以常物件和普通物件呼叫;
        bool operator!=(const iterator &it)const { 
            return _p != it._p;
        }
        void operator++() {
            ++_p;
        }
        T& operator*() {
            return *_p;
        }
        const T& operator*()const { //常物件也能呼叫,並且不可以改變值;
            return *_p;
        }
    private:
        T* _p;
    };
    iterator begin() {
        return iterator(_first);
    }
    iterator end() {
        return iterator(_end);
    }
private:
    .....
}
int main() {
    vector<int> arr;
    for (int i = 0; i < 20; i++) {
        arr.push_back(i);
    }
    auto i = arr.begin();
    *i = 100;//呼叫的是普通方法,非常方法!
    for (; i != arr.end(); ++i) {
        cout << *i << " ";
    }
    return 0;
}

image-20220323225556649

5.容器的迭代器失效問題

①迭代器的失效問題:

對容器的操作影響了元素的存放位置,稱為迭代器失效

②失效情況:

  • 當容器呼叫erase()方法後,當前位置到容器末尾元素的所有迭代器全部失效。
  • 當容器呼叫insert()方法後,當前位置到容器末尾元素的所有迭代器全部失效。
  • 如果容器擴容,在其他地方重新又開闢了一塊記憶體。原來容器底層的記憶體上所儲存的迭代器全都失效了。
  • 不同容器的迭代器,是不能進行比較運算的。

例子:程式:把vec容器中所有的偶數全部刪掉;把vec容器中的偶數前驅位置插入 偶數值-1;

image-20220323231637455

③迭代器失效的解決辦法:對插入/刪除點的迭代器進行更新操作。(新的迭代器才是有效的)

image-20220323232706001

//對於刪除操作
auto it = vec.begin();
while (it!=vec.end())
{
	if (*it % 2 == 0)
	{
		it = vec.erase(it);//返回更新當前刪除位置迭代器,並且不進行++操作,因為元素已經往前移動;
        
	}
	else
	{
		++it;
	}
}

//給vec容器中所有的偶數前面新增一個小於偶數值1的數字
auto it = vec.begin();
for (; it!=vec.end(); ++it)
{
	if (*it % 2 == 0)
	{
		it = vec.insert(it, *it-1);//更新當前增加位置迭代器
		++it; // 這樣一共移動兩個位置
	}
}

④迭代器失效原理:

我們嘗試寫一下底層迭代器的程式碼:

在vector類中加一個迭代器連結串列(儲存每一個迭代器的資訊);在迭代器中加多一個當前的vector指標(看看該迭代器屬於誰);erase、insert操作都移動當前vector裡資料的位置,擴容操作搬移vector中的資料到另一塊新記憶體,將迭代器連結串列中資訊對應不上的迭代器 失效掉;步驟如下:

  • 在iterator私有成員下新增一個指向當前物件的指標,讓迭代器知道當前迭代的容器物件,不同容器之間不能相互比較

    vector<T, Alloc> *_pVec;
    
  • 增加一個結構體維護了一個連結串列。cur是指向某一個迭代器,迭代器中存有①該迭代器是哪個類物件②該迭代器指向哪個元素;又定義了一個指向下一個Iterator_Base節點的指標;外面定義了一個頭節點_head,記錄了使用者申請的迭代器,記錄在Iterator_Base連結串列中

image-20220324175933945

  • verify檢查有效性。在我們增加或刪除後,把我們當前節點的地址到末尾的地址,全部進行檢查,在儲存的迭代器連結串列上進行遍歷,哪一個迭代器指標指向的迭代器迭代元素的指標在檢查範圍內,就將相應迭代器指向容器的指標置為空,即為失效的迭代器
void verify(T *first, T *last)
{
	Iterator_Base *pre = &this->_head;
	Iterator_Base *it = this->_head._next;
	while (it != nullptr)
	{
		if (it->_cur->_ptr > first && it->_cur->_ptr <= last)
		{
			//迭代器失效,把iterator持有的容器指標置nullptr
			it->_cur->_pVec = nullptr;
			//刪除當前迭代器節點,繼續判斷後面的迭代器節點是否失效
			pre->_next = it->_next;
			delete it;
			it = pre->_next;
		}
		else
		{
			pre = it;
			it = it->_next;
		}
	}
}

  • !=運算子號過載前要檢查迭代器的有效性,即兩個迭代器比較之前檢查是否失效,或者是否是同一型別容器的迭代器。 ++、*運算子也需要檢驗其有效性

    bool operator!=(const iterator &it)const
    {
    	//檢查迭代器的有效性
    	if (_pVec == nullptr || _pVec != it._pVec)//迭代器為空或迭代兩個不同容器
    	{
    		throw "iterator incompatable!";
    	}
    	return _ptr != it._ptr;
    }
    //++、*也一樣;
    
  • pop_back中加入了verfiy, 自定義insert實現(未考慮擴容與ptr合法性,加入verify操作,還有往後移動資料),自定義erase實現(加入verify操作,還有往前移動資料);

void pop_back()//尾刪
{
	if (empty()) return;
	verify(_last - 1, _last);
	//不僅要把_last指標--,還需要析構刪除的元素
	--_last;
	_allocator.destroy(_last);
}
//自定義vector容器insert方法實現
iterator insert(iterator it, const T &val)
{
	//1.這裡我們未考慮擴容
	//2.還未考慮it._ptr指標合法性,假設它合法
	verify(it._ptr - 1, _last);
	T *p = _last;
	while (p > it._ptr)
	{
		_allocator.construct(p, *(p-1));
		_allocator.destroy(p - 1);
		p--;
	}
	_allocator.construct(p, val);
	_last++;
	return iterator(this, p);
}
//自定義vector容器erase方法實現
iterator erase(iterator it)
{
	verify(it._ptr - 1, _last);
	T *p = it._ptr;
	while (p < _last-1)
	{
		_allocator.destroy(p);
		_allocator.construct(p, *(p+1));
		p++;
	}
	_allocator.destroy(p);
	_last--;
	return iterator(this, it._ptr);
}

總結:vector會有維護迭代器的連結串列,在vector迭代器中,如果有插入和刪除操作,那麼與這些位置有關聯的迭代器(比如在插入刪除後面)會全部失效,因為會用verify函式將迭代器失效,失效後什麼操作都會失敗;

總程式碼:

#include<iostream>
#include<algorithm>

using namespace std;

//容器的空間配置器
template <typename T>
struct Allocator
{
	T* allocate(size_t size)//只負責記憶體開闢
	{
		return (T*)malloc(sizeof(T) * size);
	}
	void deallocate(void* p)//只負責記憶體釋放
	{
		free(p);
	}
	void construct(T* p, const T& val)//已經開闢好的記憶體上,負責物件構造
	{
		new (p) T(val);//定位new,指定記憶體上構造val,T(val)拷貝構造
	}
	void destroy(T* p)//只負責物件析構
	{
		p->~T();//~T()代表了T型別的解構函式
	}
};

//####1
template <typename T, typename Alloc = Allocator<T>>
class vector//向量容器
{
public:
	vector(int size = 10)//構造
	{
		_first = _allocator.allocate(size);
		_last = _first;
		_end = _first + size;
	}
	~vector()//析構
	{
		for (T* p = _first; p != _last; ++p)
		{
			_allocator.destroy(p);//把_first指標指向的陣列的有效元素析構
		}
		_allocator.deallocate(_first);//釋放堆上的陣列記憶體
		_first = _last = _end = nullptr;
	}
	vector(const vector<T>& rhs)//拷貝構造
	{
		int size = rhs._end - rhs._first;//空間大小
		_first = _allocator.allocate(size);
		int len = rhs._last - rhs._first;//有效元素
		for (int i = 0; i < len; ++i)
		{
			_allocator.construct(_first + i, rhs._first[i]);
		}
		_last = _first + len;
		_end = _first + size;
	}
	vector<T>& operator=(const vector<T>& rhs)//賦值運算子過載
	{
		if (this == &rhs)
		{
			return *this;
		}

		for (T* p = _first; p != _last; ++p)
		{
			_allocator.destory(p);//把_first指標指向的陣列的有效元素析構
		}
		_allocator.deallocate(_first);//釋放堆上的陣列記憶體

		int size = rhs._end - rhs._first;//空間大小
		_first = _allocator.allocate(size);
		int len = rhs._last - rhs._first;//有效元素
		for (int i = 0; i < len; ++i)
		{
			_allocator.construct(_first + i, rhs._first[i]);
		}
		_last = _first + len;
		_end = _first + size;
		return *this;
	}
	void push_back(const T& val)//尾插
	{
		if (full())
		{
			expand();
		}
		_allocator.construct(_last, val);//_last指標指向的記憶體構造一個值為val的物件
		_last++;
	}
	void pop_back()//尾刪
	{
		if (empty()) return;
		verify(_last - 1, _last); //呼叫verify
		//不僅要把_last指標--,還需要析構刪除的元素
		--_last;
		_allocator.destroy(_last);
	}
	T back()const//返回容器末尾元素值
	{
		return *(_last - 1);
	}
	bool full()const
	{
		return _last == _end;
	}
	bool empty()const
	{
		return _first == _last;
	}
	int size()const//返回容器中元素個數
	{
		return _last - _first;
	}
	T& operator[](int index)
	{
		if (index < 0 || index >= size())
		{
			throw "OutOfRangeException";
		}
		return _first[index];
	}

	//#####2
	//迭代器一般實現成容器的巢狀型別
	class iterator
	{
	public:
		friend class vector <T, Alloc>;
		//新生成當前容器某一個位置元素的迭代器
		iterator(vector<T, Alloc>* pvec = nullptr
			, T* ptr = nullptr)
			:_ptr(ptr), _pVec(pvec)
		{
			Iterator_Base* itb = new Iterator_Base(this, _pVec->_head._next); //this指向當前迭代器(在上面類裡面表示什麼);_pVec->_head表示當前物件的迭代器連結串列頭
			_pVec->_head._next = itb;
		}
		bool operator!=(const iterator& it)const
		{
			//檢查迭代器的有效性
			if (_pVec == nullptr || _pVec != it._pVec)//迭代器為空或迭代兩個不同容器
			{
				throw "iterator incompatable!";
			}
			return _ptr != it._ptr;
		}
		void operator++()
		{
			//檢查迭代器有效性
			if (_pVec == nullptr)
			{
				throw "iterator incalid!";
			}
			_ptr++; //只改變了當前迭代器指向的元素
		}
		T& operator*()
		{
			//檢查迭代器有效性
			if (_pVec == nullptr)
			{
				throw "iterator invalid!";
			}
			return *_ptr;
		}
		const T& operator*()const
		{
			if (_pVec == nullptr)
			{
				throw "iterator invalid!";
			}
			return *_ptr;
		}
	private:
		T* _ptr;//當前迭代器是哪個容器物件,找到當前迭代器的目標值
		vector<T, Alloc>* _pVec;//指向當前物件容器的指標
	};

	iterator begin()
	{
		return iterator(this, _first);
	}
	iterator end()
	{
		return iterator(this, _last);
	}
	//檢查迭代器失效
	void verify(T* first, T* last)
	{
		Iterator_Base* pre = &this->_head;
		Iterator_Base* it = this->_head._next;
		while (it != nullptr)
		{
			if (it->_cur->_ptr > first && it->_cur->_ptr <= last)
			{
				//迭代器失效,把iterator持有的容器指標置nullptr
				it->_cur->_pVec = nullptr;
				//刪除當前迭代器節點,繼續判斷後面的迭代器節點是否失效
				pre->_next = it->_next;
				delete it;
				it = pre->_next;
			}
			else
			{
				pre = it;
				it = it->_next;
			}
		}
	}

	//自定義vector容器insert方法實現
	iterator insert(iterator it, const T& val)
	{
		//1.這裡我們未考慮擴容
		//2.還未考慮it._ptr指標合法性,假設它合法
		verify(it._ptr - 1, _last); //呼叫verify
		T* p = _last;
		while (p > it._ptr)
		{
			_allocator.construct(p, *(p - 1));
			_allocator.destroy(p - 1);
			p--;
		}
		_allocator.construct(p, val);
		_last++;
		return iterator(this, p);
	}

	//自定義vector容器erase方法實現
	iterator erase(iterator it)
	{
		verify(it._ptr - 1, _last); //呼叫verify
		T* p = it._ptr;
		while (p < _last - 1)
		{
			_allocator.destroy(p);
			_allocator.construct(p, *(p + 1));
			p++;
		}
		_allocator.destroy(p);
		_last--;
		return iterator(this, it._ptr);
	}
private:
	T* _first;//起始陣列位置
	T* _last;//指向最後一個有效元素後繼位置
	T* _end;//指向陣列空間的後繼位置
	Alloc _allocator;//定義容器的空間配置器物件

	//容器迭代器失效增加程式碼
	struct Iterator_Base
	{
		Iterator_Base(iterator* c = nullptr, Iterator_Base* n = nullptr)
			:_cur(c), _next(n) {}
		iterator* _cur;
		Iterator_Base* _next;
	};
	Iterator_Base _head; //存迭代器的連結串列

	void expand()//擴容
	{
		int size = _end - _first;
		//T *ptmp = new T[2*size];
		T* ptmp = _allocator.allocate(2 * size);
		for (int i = 0; i < size; ++i)
		{
			_allocator.construct(ptmp + i, _first[i]);
			//ptmp[i] = _first[i];
		}
		//delete[]_first;
		for (T* p = _first; p != _last; ++p)
		{
			_allocator.destroy(p);
		}
		_allocator.deallocate(_first);
		_first = ptmp;
		_last = _first + size;
		_end = _first + 2 * size;
	}
};

int main() {
	vector<int> vec;
	for (int i = 0; i < 5; ++i)
	{
		vec.push_back(i);
	}

	auto it3 = vec.end();
	vec.pop_back(); // it3 失效
	auto it2 = vec.end();
	cout << (it3 != it2);

	auto it1 = vec.end();
	while (it1 != vec.end()) { //因為這條語句有end(),會構造迭代器,加多了vec迭代器連結串列中的元素;但是每次生成的都是最後一個元素的迭代器;
		++it1;
	}
	vec.pop_back();//verify(_last-1, _last)
	auto it2 = vec.end();
	cout << (it1 != it2) << endl;
	return 0;
}

6.深入理解new和delete的原理

new與delete實現原理進行剖析

malloc和new:

  • malloc按位元組開闢記憶體的;new開闢記憶體時需要指定型別;
  • malloc開闢記憶體返回的都是void * ,new相當於運算子過載函式,返回值自動轉為指定的型別的指標。
  • malloc只負責開闢記憶體空間,new不僅僅也有malloc功能,還可以進行資料的初始化(使用建構函式)。
  • malloc開闢記憶體失敗返回nullptr指標;new丟擲的是bad_alloc型別的異常。
  • malloc開闢單個元素記憶體與陣列記憶體 的記憶體計算是一樣的,都是給所有元素所需要的位元組數(比如一個int 4個位元組,int arr[2] 8個位元組);new開闢時,如果元素是編譯器內建型別,則和malloc一樣(malloc需要自己算,new不用);但是對於類物件,對單個元素記憶體後面不需要[],記憶體大小也和元素所需要的位元組數一樣(一個 int 4個位元組);陣列需要加上[]並給上元素個數,位元組數則是是 所有元素所需位元組數 + 4個位元組(記錄有多少個元素); 可以看最後的解析

delete和free

  • free不管釋放單個元素記憶體還是陣列記憶體,只需要傳入記憶體的起始地址即可。
  • delete釋放單個元素記憶體,不需要加中括號,但釋放資料記憶體時需要加中括號。中括號:[]
  • free只有一步,就是釋放記憶體;delete執行其實有兩步,先呼叫析構,再釋放記憶體

new與delete實現原理剖析

int* p = new int; delete p;

反彙編為:

image-20220325005827341

我們發現,其實new和delete的本質是有對new和delete過載函式的呼叫new -> operator new delete -> operator delete

實現一下operator new和 operator delete:

//new:先呼叫operator開闢記憶體空間,然後呼叫物件的建構函式
void* operator new(size_t size)
{
	void *p = malloc(size);
	if (p == nullptr)
	{
		throw bad_alloc();
	}
	cout << "operator new addr:" << p <<endl;
	return p;
}

//operator new[]實現
void* operator new[](size_t size)
{
	void *p = malloc(size);
	if (p == nullptr)
	{
		throw bad_alloc();
	}
	cout << "operator new[] addr:" << p <<endl;
	return p;
}

//delete p:呼叫p指向物件的解構函式,再呼叫operator delete釋放記憶體空間
void operator delete(void *ptr)
{
	cout << "operator delete addr:" << ptr <<endl;
	free(ptr);
}
//operator delete[]實現
void operator delete[](void *ptr)
{
	cout << "operator delete[] addr:" << ptr <<endl;
	free(ptr);
}

其中,bad_alloc()函式實際程式碼:

image-20220325010336014

測試:以Test類為例子:

class Test {
public:
	Test(int data = 1): ptr(new int(data)) { cout << "Test()" << endl; }
	~Test() { cout << "~Test()" << endl; }
private:
    int *ptr;
};
int main() {
	Test* ptr = new Test;
	delete ptr;
	cout << endl;
	Test *test = new Test[2];
	delete[]test;
	return 0;
}

image-20220325012721720

③重要問題:new 和delete能夠混用嗎?

我們看看c++編譯器內建型別:

以int為例子:

int *p = new int;
delete[]p;

int *q = new int[10];
delete q;

image-20220325013215094

是沒有問題的!!!因為對於整型來說,沒有建構函式與解構函式,針對於int型別,new與delete功能只剩下malloc與free功能,可以將其混用。

我們看看類 型別:

以上述的Test為例子:

Test *p1 = new Test();
delete[]p1;

Test *p2 = new Test[5];
delete p2;

image-20220325013640472

第一個會一直不斷死迴圈,第二個會觸發異常;

具體原因:

正常情況下,每一個test物件是隻有一個整型指標成員變數,只佔用四個位元組的;而Test[5]這裡分配了5個test物件。delete時先呼叫解構函式,this指標需要將正確的物件的地址傳入解構函式中,加了[]表示有好幾個物件,對陣列中的每一個物件都要進行析構。但delete真正執行指令時,底層是malloc按位元組開闢,並不知道是否開闢了5個test物件的陣列,因此還要再多開闢一個4位元組來儲存物件的個數,假設它的地址是0x100;但是new完之後p2返回的地址是0x104地址,是第一個元素的地址。當我們執行delete[]時,會到前四個4位元組來取一下物件的個數,將知道了是5個並將這塊記憶體平均分為5份,將其每一份物件起始地址傳給相應的解構函式,正常析構,最後將0x100開始的4位元組也釋放。
而上述中p2出錯是給使用者返回的第一個物件的起始地址,delete p2認為p2只是指向了一個物件,只將Test[0]物件析構,直接從0x104 free(p2),但底層實際是從0x100開闢的,因此崩潰存在異常;而p1出錯則很明顯:p1只是單個元素,從0x104開始開闢記憶體,但是delete[]p1,裡面並沒有那麼多元素,最後還釋放了4個位元組的儲存物件個數的記憶體(即從0x100釋放)因此崩潰。

image-20220325021928139

總結:1.對於普通的編譯器內建型別new/delete[]混用是可以的;new[]/delete混用也是可以的。 2.自定義的類型別,有解構函式,為了呼叫正確的解構函式,那麼開闢物件陣列時會多開闢4個位元組記錄物件的個數,不能混用。

④擴充套件問題:C++中如何設計一個程式檢查記憶體洩露問題?

核心是用new與delete運算子過載接管整個應用的記憶體管理,對記憶體開闢釋放都要記錄。 檢查記憶體洩露,在全域性中重寫new與delete,new操作中用對映表記錄一下都有哪些記憶體被開闢過了;delete時候,將相應的記憶體資源用記憶體地址標識,再刪除掉。如果整個系統執行完發現對映表中有一些記憶體還沒有被釋放則存在記憶體洩露;

7.new和delete過載實現的物件池引用

**物件池: ** 在一部分記憶體空間(池子)中事先例項化好固定數量的物件,當需要使用池中的物件時,首先判斷該池中是否有閒置(暫未使用)的物件,如果有,則取出使用,如果沒有,則在池中建立該物件。當一個物件不再被使用時,其應該將其放回物件池,以便後來的程式使用,最後程式結束的時候,再統一釋放;

為什麼需要物件池:因為大量的push和pop操作,會不斷申請和釋放空間,影響效能;拿Queue為例子:

#include<iostream>

using namespace std;

template<typename T>
class Queue {
public:
	Queue(){
		_front = _rear = new QueueItem(); //提供一個空節點
	}
	~Queue() {
		QueueItem* cur = _front;
		while (cur != nullptr) {
			_front = _front->next;
			delete cur;
			cur = _front;
		}
	}
	void push(const T &val) { //隊尾入隊
		QueueItem* item = new QueueItem(val);
		_rear->next = item;
		_rear = item;
	}
	void pop() { //隊頭出隊, 不用返回值,記住隊頭為空
		if (empty()) throw "佇列為空";
		QueueItem* item = _front->next;
		_front->next = item->next;
		if (_front->next == nullptr) { //說明只有一個元素;
			_rear = _front;
		}
		delete item;
	}
	T front()const { //只讀操作所以加const
		if (empty()) throw "佇列為空";
		return _front->next->_data; //物件才有點操作,指標是箭頭操作,有多個next因為有空的頭節點
	}
	bool empty()const { return _rear == _front; } //只讀操作

private:
	struct QueueItem //想做一個物件池(10000個QueueItem的節點),不用大量new和delete,用到就裡面拿,不用就歸還,程式設計師來記憶體管理
	{
		QueueItem(T data = T()): _data(data), next(nullptr) {} //留意一下,這個T data = T()操作
		T _data;
		QueueItem* next;
	};
	QueueItem* _front; //隊頭
	QueueItem* _rear; //隊尾
};

int main() {
	Queue<int> que;
	for (int i = 0; i < 1000000; ++i)
	{
		que.push(i); //這裡存在大量的new和delete
		que.pop(); //
	}
	cout << que.empty() << endl;
	return 0;
}

這裡我們可以看出:每次push都會執行一次new QueueItem,每次pop都會執行一次QueueItem的delete;而節點不同的是隻是結點中資料不同,短時間內大量對其進行呼叫,會影響我們程式的效能。所以採用物件池更佳,物件池模型如下:

image-20220325093445118

可知:第一次物件池生成的物件全部用完,會重新開闢新物件在物件池中,如果歸還第一次開闢的物件,也會連結到物件池空閒物件中

物件池實現:

#include<iostream>

using namespace std;

template<typename T>
class Queue {
public:
	Queue(){
		_front = _rear = new QueueItem(); //提供一個空節點
	}
	~Queue() {
		QueueItem* cur = _front;
		while (cur != nullptr) {
			_front = _front->next;
			delete cur;
			cur = _front;
		}
	}
	void push(const T &val) { //隊尾入隊
		QueueItem* item = new QueueItem(val); //先呼叫new在物件池中拿出物件,然後採用建構函式初始化物件;可以和之前new彙編做了什麼行為進行對比
		_rear->next = item;
		_rear = item;
	}
	void pop() { //隊頭出隊, 不用返回值,記住隊頭為空
		if (empty()) throw "佇列為空";
		QueueItem* item = _front->next;
		_front->next = item->next;
		if (_front->next == nullptr) { //說明只有一個元素;
			_rear = _front;
		}
		delete item;
	}
	T front()const { //只讀操作所以加const
		if (empty()) throw "佇列為空";
		return _front->next->_data; //物件才有點操作,指標是箭頭操作,有多個next因為有空的頭節點
	}
	bool empty()const { return _rear == _front; } //只讀操作

private:
	//想做一個物件池(100000個QueueItem的節點),不用大量new和delete,用到就裡面拿,不用就歸還,程式設計師來記憶體管理
	struct QueueItem
	{
		QueueItem(T data = T()): _data(data), next(nullptr) {} //留意一下,這個T data = T()操作!!!!!!!
		//QueueItem提供自定義記憶體管理
		void* operator new(size_t size){ //會自動變為static方法,這裡的size沒有用到
			if (_itemPool == nullptr) {
				_itemPool = (QueueItem*)new char[POOL_ITEM_SIZE * sizeof(QueueItem)]; //為什麼記憶體池用char?
				QueueItem* p = _itemPool;//指向首地址
				for (; p < _itemPool + POOL_ITEM_SIZE - 1; ++p) { //最後一個元素指向空,不需要遍歷到最後一個元素
					p->next = p + 1;
				}
				p->next = nullptr;
			}
			QueueItem* p = _itemPool; 
			_itemPool = _itemPool->next;
			return p;
		}

		void operator delete(void* ptr) { //使用void*,自動變為靜態方法
			QueueItem* p = (QueueItem*)ptr;
			p->next = _itemPool;
			_itemPool = p;
		}
		T _data;
		QueueItem* next;
		static QueueItem* _itemPool; //記憶體池指標
		static const int POOL_ITEM_SIZE = 100000;
	};
	QueueItem* _front; //隊頭
	QueueItem* _rear; //隊尾
};
template <typename T>
typename Queue<T>::QueueItem *Queue<T>::QueueItem::_itemPool = nullptr; //記住樣例

int main() {
	Queue<int> que;
	for (int i = 0; i < 1000000; ++i)
	{
		que.push(i); //用物件池處理
		que.pop(); //
	}
	cout << que.empty() << endl;
	return 0;
}

什麼是0構造