二、類與對象(二)

8 this指針

8.1 this指針的引入

我們先來定義一個日期的類Date

	#include <iostream>
using namespace std;
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;     // 年int _month;    // 月int _day;      // 日
};
int main()
{Date d1, d2;d1.Init(2022, 1, 11);d2.Init(2022, 1, 12);d1.Print();d2.Print();return 0;
}

對于上面一個類,有這樣一個問題:

Date類中有InitPrint兩個成員函數,函數體中并沒有關于不同對象的區分,那么當d1調用Init函數時,該函數是如何知道應該設置d1對象,而不是設置d2對象的呢?

C++通過引入this指針來解決這個問題。實際上,C++編譯器給每個非靜態的成員函數增加了一個隱藏的指針參數,讓該指針指向當前對象(函數運行時調用該函數的對象),在函數體中所有“成員變量” 的操作,都是通過該指針去訪問,只不過所有的操作對用戶是透明的,即用戶不需要來傳遞,編譯器自動完成。

8.2 this指針的特性

  1. this指針的類型:類的類型 const*,所以成員函數中,不能給this指針賦值。
  2. this指針只能在成員函數的內部使用。
  3. this指針本質上是成員函數的形參,所以this指針是存儲在中的。當對象調用成員函數時,函數將對象地址作為實參傳遞給this形參。所以對象中不存儲this指針。
  4. this指針是成員函數第一個隱含的指針形參,一般情況下由編譯器通過ecx寄存器自動傳遞,不需要用戶傳遞。

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

  1. this指針

例1:下面程序編譯運行的結果是什么?

#include <iostream>
using namespace std;
class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
void test1()
{A* p = nullptr;//空指針p->Print();
}
void test2()
{A* p = nullptr;//空指針(*p).Print();
}
int main()
{test1();test2();return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

從輸出結果可以看到,程序正常運行了,這是為什么呢?

這是因為成員函數Print實際上在公共的代碼段而并不在對象里面,所以雖然p是一個空指針,但p->Print()在這里并不代表解引用,而是直接去公共區域調用了函數Print(*p).Print()也同理。

如果是這樣的話,那能不能不用對象直接調用Print函數呢?

#include <iostream>
using namespace std;
class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
int main()
{Print();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

可以看到,編譯器報錯了。這是因為Print會受到類域的限制,如果不用對象直接調用Print函數那么編譯器將無法找到Print函數。

例2:下面程序編譯運行的結果是什么?

#include <iostream>
using namespace std;
class A
{
public:void PrintA(){cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

從輸出結果可以看到,程序崩潰了。這是因為PrintA函數體內部的cout << _a << endl語句等價于cout << this->_a << endl而此時PrintA函數的參數為空指針,那么對空指針進行解引用自然就會發生崩潰了。

9 C語言和C++實現Stack的對比

9.1 C語言實現

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
typedef int DataType;
typedef struct Stack
{DataType* array;int capacity;int size;
}Stack;void StackInit(Stack* ps)
{assert(ps);ps->array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == ps->array){assert(0);return;}ps->capacity = 3;ps->size = 0;
}void StackDestroy(Stack* ps)
{assert(ps);if (ps->array){free(ps->array);ps->array = NULL;ps->capacity = 0;ps->size = 0;}
}void CheckCapacity(Stack* ps)
{if (ps->size == ps->capacity){int newcapacity = ps->capacity * 2;DataType* temp = (DataType*)realloc(ps->array,newcapacity * sizeof(DataType));if (temp == NULL){perror("realloc申請空間失敗!!!");return;}ps->array = temp;ps->capacity = newcapacity;}
}void StackPush(Stack* ps, DataType data)
{assert(ps);CheckCapacity(ps);ps->array[ps->size] = data;ps->size++;
}int StackEmpty(Stack* ps)
{assert(ps);return 0 == ps->size;
}void StackPop(Stack* ps)
{if (StackEmpty(ps))return;ps->size--;
}DataType StackTop(Stack* ps)
{assert(!StackEmpty(ps));return ps->array[ps->size - 1];
}int StackSize(Stack* ps)
{assert(ps);return ps->size;
}int main()
{Stack s;StackInit(&s);StackPush(&s, 1);StackPush(&s, 2);StackPush(&s, 3);StackPush(&s, 4);printf("%d\n", StackTop(&s));printf("%d\n", StackSize(&s));StackPop(&s);StackPop(&s);printf("%d\n", StackTop(&s));printf("%d\n", StackSize(&s));StackDestroy(&s);return 0;
}

可以看到,在用C語言實現Stack時,Stack相關操作函數有以下共性:

  1. 每個函數的第一個參數都是Stack*
  2. 函數中必須要對第一個參數檢測,因為該參數可能會為NULL
  3. 函數中都是通過Stack*參數操作棧的.
  4. 調用時必須傳遞Stack結構體變量的地址。

結論:C語言中結構體只能定義存放數據的結構,而操作數據的方法不能放在結構體中,即數據和操作數據的方式是分離開的,而且實現上相對復雜,涉及到大量指針操作,稍不注意可能就會出錯。

9.2 C++實現

#include <iostream>
#include <stdlib.h>
using namespace std;
typedef int DataType;
class Stack
{
public:void Init(){_array = (DataType*)malloc(sizeof(DataType) * 3);if (NULL == _array){perror("malloc申請空間失敗!!!");return;}_capacity = 3;_size = 0;}void Push(DataType data){CheckCapacity();_array[_size] = data;_size++;}void Pop(){if (Empty())return;_size--;}DataType Top() { return _array[_size - 1]; }int Empty() { return 0 == _size; }int Size() { return _size; }void Destroy(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:void CheckCapacity(){if (_size == _capacity){int newcapacity = _capacity * 2;DataType* temp = (DataType*)realloc(_array, newcapacity *sizeof(DataType));if (temp == NULL){perror("realloc申請空間失敗!!!");return;}_array = temp;_capacity = newcapacity;}}
private:DataType* _array;int _capacity;int _size;
};
int main()
{Stack s;s.Init();s.Push(1);s.Push(2);s.Push(3);s.Push(4);printf("%d\n", s.Top());printf("%d\n", s.Size());s.Pop();s.Pop();printf("%d\n", s.Top());printf("%d\n", s.Size());s.Destroy();return 0;
}

在C++中,通過類可以將數據以及操作數據的方法進行完美結合,通過訪問權限可以控制哪些方法在類外可以被調用,即封裝。在使用時就像使用自己的成員一樣,更符合人對一件事物的認知。 而且和C語言相比,每個方法不需要傳遞Stack*的參數,編譯器在編譯之后會將該參數自動還原,即C++中Stack*參數是編譯器維護的,C語言中需要用戶自己維護。

10 類的默認成員函數

之前我們說過,如果一個類中什么成員都沒有,簡稱為空類。任何類在什么都不寫時,編譯器會自動生成以下6個默認成員函數。 默認成員函數指的就是用戶沒有顯式實現,但是編譯器會生成的成員函數。

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

11 構造函數

11.1 構造函數的概念

我們以下面一個描述日期的類Date為例:

#include <iostream>
using namespace std;
class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;d1.Init(2022, 7, 5);d1.Print();Date d2;d2.Init(2022, 7, 6);d2.Print();return 0;
}

對于Date類,可以通過公有方法Init給對象設置日期,但如果每次創建對象時都調用該方法設置信息,還是有點麻煩。那能否在對象創建時,就將信息設置進去呢? C++中,引入了構造函數來解決這個問題。

構造函數是一個特殊的成員函數,名字與類名相同,創建類類型對象時由編譯器自動調用,以保證每個數據成員都有一個合適的初始值,并且在對象整個生命周期內只調用一次

需要注意的是,構造函數雖然名稱叫構造,但是構造函數的主要任務并不是開辟空間創建對象,而是初始化對象

11.2 構造函數的特性

  1. 函數名與類名相同。
  2. 無返回值。
  3. 對象實例化時編譯器自動調用對應的構造函數。
  4. 構造函數可以重載,也就是說構造函數允許對象有多種初始化的方式。

例:

#include <iostream>
using namespace std;
class Date
{
public:// 1.無參構造函數Date(){}// 2.帶參構造函數Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1; // 調用無參構造函數Date d2(2015, 1, 1); // 調用帶參的構造函數Date d3();d1.Print();d2.Print();// 注意:如果通過無參構造函數創建對象時,對象后面不用跟括號,否則就成了函數聲明// 以下代碼的函數:聲明了d3函數,該函數無參,返回一個日期類型的對象//d3.Print(); // warning C4930: “Date d3(void)”: 未調用原型函數(是否是有意用變量定義的?)return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

  1. 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。

例:

#include <iostream>
using namespace std;
class Date
{
public:// 如果用戶顯式定義了構造函數,編譯器將不再生成/* Date(int year, int month, int day){_year = year;_month = month;_day = day;}*/void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{// 將Date類中構造函數屏蔽后,代碼可以通過編譯,因為編譯器生成了一個無參的默認構造函數// 將Date類中構造函數放開,代碼編譯失敗,因為一旦顯式定義任何構造函數,編譯器將不再生成// 無參構造函數,放開后報錯:error C2512: “Date”: 沒有合適的默認構造函數可用Date d1;d1.Print();return 0;
}

放開前運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

放開后運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

  1. 由于C++把類型分成內置類型(如:int/char等)和自定義類型(如使用class/struct/union等自己定義的類型),而C++的語法又規定編譯器生成的默認構造函數不會對內置類型進行處理,也就是說對于內置類型的成員,雖然調用了默認構造函數但是依舊是隨機值,而對于自定義類型的成員則會去調用它的默認構造函數。

注意:不傳參數就可以調用的構造函數就叫默認構造函數,一般建議每個類都提供一個默認構造函數。

例:

#include <iostream>
using namespace std;
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}void Print(){cout << _hour << "時" << _minute << "分" << _second << "秒" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}/*Date(){cout << "Date()" << endl;}*/
private://基本類型int _year;int _month;int _day;//自定義類型Time _t;
};
int main()
{Date d;d.Print();return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

從輸出結果可以看到,編譯器生成默認的構造函數會對自定類型成員_t調用的它的默認成員函數。

注意:C++11中針對內置類型成員不初始化的缺陷打了補丁,打了補丁后內置類型成員變量在類中聲明時可以給默認值。

例:

#define _CRT_SECURE_NO_WARNINGS	1
//構造函數缺陷
#include <iostream>
using namespace std;
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}void Print(){cout << _hour << "時" << _minute << "分" << _second << "秒" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}/*Date(){cout << "Date()" << endl;}*/
private://基本類型型(內置類型)int _year = 2023;int _month = 10;int _day = 3;//自定義類型Time _t;
};
int main()
{Date d;d.Print();return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

  1. 無參的構造函數和全缺省的構造函數都稱為默認構造函數,并且默認構造函數只能有一個。 也就是說,如果既寫了無參構造函數又寫了全缺省的構造函數,那么編譯的時候編譯器會報錯。

例:

#include <iostream>
using namespace std;
class Date
{
public://無參的構造函數Date(){_year = 1900;_month = 1;_day = 1;}//全缺省的構造函數Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};void Test()
{Date d1;
}
int main()
{Test();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

12 析構函數

12.1 析構函數的概念

通過前面構造函數的學習,我們知道一個對象是怎么來的,那一個對象又是怎么沒的呢?

與構造函數功能相反,析構函數不是完成對對象本身的銷毀,局部對象銷毀工作是由編譯器完成的,而對象在銷毀時會自動調用析構函數,完成對象中資源的清理工作。

12.2 析構函數的特性

  1. 析構函數名是在類名前加上字符~
  2. 無參數無返回值。
  3. 一個類只能有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數,也就是說,析構函數不能重載
  4. 對象生命周期結束時,C++編譯系統會自動調用析構函數。

例:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 3){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申請空間失敗!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};
void TestStack()
{Stack s;s.Push(1);s.Push(2);
}
int main()
{TestStack();return 0;
}
  1. 由于內置類型成員的銷毀不需要資源清理,是最后由系統直接將其內存回收,所以不需要調用析構函數;而對于自定義類型的成員則需要調用它的析構函數,不過這個自定義類型成員的析構函數不能被直接調用,而是由包含這個自定義類型成員的類的析構函數調用。換言之,如果類中沒有申請資源時,析構函數可以不寫,直接使用編譯器生成的默認析構函數即可;而有資源申請時,一定要寫,否則會造成資源泄漏,比如Stack類。

例:

#include <iostream>
using namespace std;
class Time
{
public:~Time(){cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
private://基本類型型(內置類型)int _year = 1970;int _month = 1;int _day = 1;//自定義類型Time _t;
};int main()
{Date d;return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

從輸出結果可以看到,在main函數中根本沒有直接創建Time類的對象,但是最后還是調用了Time類的析構函數,這就是因為main函數中創建了Date類對象d,而d中包含了4個成員變量,其中_year_month, _day三個是內置類型成員,銷毀時不需要資源清理,而_tTime類對象,所以在銷毀d時,要將其內部包含的Time類的_t對象銷毀,所以要調用Time類的析構函數。但是:main函數中不能直接調用Time類的析構函數,實際要釋放的是Date類對象,所以編譯器會調用Date類的析構函數,而Date沒有顯式提供,所以編譯器會給Date類生成一個默認的析構函數,目的是在其內部調用Time類的析構函數,也就是說當Date的對象銷毀時,要保證其內部每個自定義對象都能被正確銷毀。
總結:創建哪個類的對象則調用該類的析構函數,銷毀哪個類的對象則調用該類的析構函數。

13 拷貝構造函數

13.1 拷貝構造函數的概念

在現實生活中,可能存在一個與你一樣的自己,我們稱其為雙胞胎。

而在C++中,拷貝構造函數就可以實現創建一個與已存在對象一模一樣的新對象。

13.2 拷貝構造函數的特性

  1. 拷貝構造函數是構造函數的一個重載形式。
  2. 拷貝構造函數只有單個形參,該形參只能是對本類類型對象的引用(一般常用const修飾),而且在用已存在的類類型對象創建新對象時由編譯器自動調用,如果使用傳值方式進行傳參那么編譯器會直接報錯,因為C++規定自定義類型的傳值需要去調用拷貝構造函數,也就是在使用傳值方式進行傳參的過程中會調用拷貝構造函數,而由于這個拷貝構造函數是以傳值方式實現的受C++語法的限制會又調用拷貝構造函數,層層調用最終導致無窮遞歸調用。

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date d)       //錯誤寫法:編譯報錯,會引發無窮遞歸//{//    _year = d._year;//    _month = d._month;//    _day = d._day;//   cout << "Date(const Date d)" << endl;//}Date(const Date& d)      // 正確寫法{_year = d._year;_month = d._month;_day = d._day;cout << "Date(const Date& d)" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d1;Date d2(d1);return 0;
}

錯誤寫法運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

原因圖解:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

正確寫法運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

  1. 如果沒有顯式定義拷貝構造函數,那么編譯器會生成默認的拷貝構造函數。默認的拷貝構造函數對象按內存存儲按字節序完成拷貝,這種拷貝叫做淺拷貝,或者值拷貝。其中內置類型按照字節方式直接拷貝,而自定義類型則調用其拷貝構造函數完成拷貝

例:

#include <iostream>
using namespace std;
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}void Print(){cout << _hour << "時" << _minute << "分" << _second << "秒" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:void Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}
private://基本類型(內置類型)int _year = 1970;int _month = 1;int _day = 1;//自定義類型Time _t;
};int main()
{Date d1;// 用已經存在的d1拷貝構造d2,此處會調用Date類的拷貝構造函數// 但Date類并沒有顯式定義拷貝構造函數,則編譯器會給Date類生成一個默認的拷貝構造函數Date d2(d1);d2.Print();return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

既然編譯器生成的默認拷貝構造函數已經可以完成字節序的值拷貝,那么對于所有的類是不是都不需要自己來顯式實現呢?我們可以通過下面的類來感受一下:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2(s1);return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

可以看到,當我們以同樣的方式對Stack類的對象s2進行拷貝構造時,程序崩潰了,這是什么原因呢?

![![外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=C%3A%5CUsers%5CHackerKevin%5CAppData%5CRoaming%5CTypora%5Ctypora-user-images%5Cimage-](https://img-blog.csdnimg.cn/64088cfcaf1a48e4bbae1b869341bd97.png)
20231003203650522.png&pos_id=img-LibCRJDs-1700659738351)

我們可以通過上圖幫助我們理解崩潰的原因。在main函數中,s1對象通過調用構造函數創建,而在構造函數中,默認申請了10個元素的空間,然后將1、2、3、4存了進去。

在后續構造s2對象的過程中,由于s2對象使用s1拷貝構造,而Stack類沒有顯式定義拷貝構造函數,所以編譯器會給Stack類生成一份默認的拷貝構造函數,而又因為默認拷貝構造函數是按照值進行拷貝的,也就是說默認拷貝構造函數會將s1中的內容原封不動地拷貝到s2中,所以s1s2指向了同一塊內存空間。

當程序退出時,s2s1都要銷毀。而根據析構“后進先出”(即后創建的先銷毀)的原則,s2將先被銷毀,此時s2銷毀時調用析構函數已經將0x11223344的空間釋放了,但是s1中仍然指向0x11223344這塊空間,到s1銷毀時,會將0x11223344的空間再釋放一次,一塊內存空間多次釋放,必然會造成程序崩潰。

結論:類中一旦涉及到資源申請時,一定要寫拷貝構造函數,否則就是淺拷貝;而類中沒有涉及資源申請時,寫還是不寫拷貝構造函數都可以。

  1. 拷貝構造函數典型調用場景:
    1. 使用已存在對象創建新對象
    2. 函數參數類型為類類型對象
    3. 函數返回值類型為類類型對象

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int,int,int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};
Date Test(Date d)
{Date temp(d);return temp;
}
int main()
{Date d1(2022, 1, 13);Test(d1);return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

程序解讀:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

總結:為了提高程序效率,一般對象傳參時,盡量使用引用類型;返回時根據實際場景,能用引用盡量使用引用。

14 賦值運算符重載

14.1 運算符重載

C++為了增強代碼的可讀性引入了運算符重載,讓自定義類型對象也可以使用運算符。

運算符重載是具有特殊函數名的函數,也具有其返回值類型、函數名以及參數列表,其返回值類型與參數列表與普通的函數類似。

函數名為:operator + 需要重載的運算符符號

函數原型:返回值類型 + operator +(參數列表)

注意:

  1. 不能通過連接其他符號來創建新的操作符,比如operator@
  2. 重載操作符必須有一個類類型參數。
  3. 用于內置類型的運算符,其含義不能改變,例如:內置的整型+,不能改變其含義。
  4. 作為類成員函數重載時,其形參看起來比實際操作數數目少1,但是成員函數里還隱藏了一個this參數。
  5. 特別注意:.*::sizeof?:.這5個運算符不能重載,這個經常在筆試選擇題中出現。

例:用全局的operator==實現判斷Date類相等:

//全局的operator==
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
//private:int _year;int _month;int _day;
};
bool operator==(const Date& d1, const Date& d2)//第一個參數為左操作符,第二個參數為右操作符
{return d1._year == d2._year&& d1._month == d2._month&& d1._day == d2._day;
}
void Test()
{Date d1(2023, 9, 27);Date d2(2023, 9, 27);Date d3(2023, 9, 27);Date d4(2023, 9, 26);cout << (d1 == d2) << endl;//d1 == d2會被轉換成operator==(d1,d2)cout << (d3 == d4) << endl;
}int main()
{Test();return 0;
}

成員變量為私有時運行結果:外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

成員變量為公有時運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

這里會發現運算符重載成全局的就需要成員變量是公有的,但如果這樣的話封裝性就無法得到保證了。這里其實可以用我們后面學習的友元解決,或者干脆重載為成員函數。

例:

//重載為成員函數
#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)// 這里需要注意的是,成員函數都有一個默認的隱藏參數,即左操作數是this,指向調用函數的對象bool operator==(const Date & d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2023, 9, 27);Date d2(2023, 9, 27);Date d3(2023, 9, 27);Date d4(2023, 9, 26);cout << (d1 == d2) << endl;cout << (d3 == d4) << endl;
}
int main()
{Test();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

14.1.1 運算符重載的復用

剛才我們實現了判斷Date類相等的函數operator==,那當我們還想實現諸如operator>operator<operator>=這樣邏輯相似的函數時,如果每一個函數都要單獨寫一段代碼進行實現,那未免也太麻煩了,有沒有什么簡化的方法呢?

這里我們就可以通過對運算符重載的復用來實現,還是以Date類為例,要實現所有的比較關系的話,我們實際上只需在實現operator==的基礎上,再實現一個operator<或者operator>即可:

	bool operator==(const Date & d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}bool operator<(const Date& d){return _year < d._year|| (_year == d._year && _month < d._month)|| (_year == d._year && _month == d._month && _day < d._day);  }bool operator<=(const Date& d){return *this < d || *this == d;}bool operator>(const Date& d){return !(*this <= d);}bool operator>=(const Date& d){return !(*this < d);}bool operator!=(const Date& d){return !(*this == d);}

可以看到,上面的代碼只具體實現了operator==operator<,其他的關系直接通過這兩個函數的復用就實現了,以operator>為例,operator>就是通過復用operator<=,然后對它的判斷結果進行取反來進行實現的。

實際上,上面這一套判斷邏輯,對所有的類均適用

14.2 賦值運算符重載

以往賦值運算符=只能在內置類型之間使用,而如果要讓自定義類型也能通過=進行賦值,就需要對賦值運算符進行重載。

有了剛才實現運算符重載的經驗,那我們實現賦值運算符的重載實際上也沒有什么難度。

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void operator=(const Date& d)//賦值運算符重載{_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2023, 9, 27);Date d2;d1.Print();d2.Print();d2 = d1;d1.Print();d2.Print();
}
int main()
{Test();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

可以看到,我們設計的賦值運算符重載實現了它的功能,但實際上當前設計的還是存在缺陷的,比較突出的一點就是它不支持連續賦值,因為它的返回類型是void

要實現連續賦值,那么它應該返回當前被賦值的對象,也就是返回左操作數的值。除此之外,我們還應該考慮到自己給自己賦值的情況,尤其在需要深拷貝時,會降低程序運行的效率,所以遇到這種情況時,我們直接返回即可。那么對于剛才的operator=函數我們可以進行如下改造:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}/* void operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;}*/bool operator==(const Date& d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}bool operator!=(const Date& d){return !(*this == d);}Date& operator=(const Date& d)//支持連續賦值的重載賦值運算符{if (this != &d)//地址不一樣時才賦值{_year = d._year;_month = d._month;_day = d._day;return *this;} }void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2023, 9, 27);Date d2;Date d3;d1.Print();d2.Print(); d3.Print();d3 = d2 = d1;d1.Print();d2.Print();d3.Print();
}
int main()
{Test();return 0;
}

輸出結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

可以看到,改造后的operator=函數就支持連續賦值了。

需要注意的是,賦值運算符只能重載成類的成員函數而不能重載成全局函數,原因在于賦值運算符如果不顯式實現,那么編譯器就會生成一個默認的賦值運算符重載,此時如果用戶再在類外自己實現一個全局的賦值運算符重載,那么就和編譯器在類中生成的默認賦值運算符重載沖突了,所以賦值運算符重載只能是類的成員函數

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}int _year;int _month;int _day;
};
// 賦值運算符重載成全局函數,注意重載成全局函數時沒有this指針了,需要給兩個參數
Date& operator=(Date& left, const Date& right)
{if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;
}
void Test()
{Date d1(2023, 9, 27);Date d2;Date d3;d3 = d2 = d1;
}
int main()
{Test();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

這里還需要注意的是,由編譯器生成的默認賦值運算符重載,是以值的方式逐字節拷貝,也就是說,對于內置類型成員變量是直接賦值的,但是對于自定義類型成員變量則需要調用對應類的賦值運算符重載才能完成賦值。

例:

#include <iostream>
using namespace std; 
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1;}//Time& operator=(const Time& t)  ////{//	if (this != &t)//	{//		_hour = t._hour;//		_minute = t._minute;//		_second = t._second;//	}//	return *this;//}void Print(){cout << _hour << "時" << _minute << "分" << _second << "秒" << endl;}
private:int _hour;int _minute;int _second;
};
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << "年" << _month << "月" << _day << "日";this->_t.Print();}
private:// 基本類型(內置類型)int _year;int _month;int _day;// 自定義類型Time _t;
};
int main()
{Date d1;Date d2(2023, 10, 4);d1.Print();d2.Print();d1 = d2;d1.Print();d2.Print();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

所以,雖然編譯器生成的默認賦值運算符重載函數已經可以完成字節序的值拷貝了,但是對于一些涉及到資源管理的類,則必須要自己實現賦值運算符的重載,否則會出現無法預料的結果。

例:

#include <iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申請空間失敗");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType* _array;size_t _size;size_t _capacity;
};
int main()
{Stack s1;s1.Push(1);s1.Push(2);s1.Push(3);s1.Push(4);Stack s2;s2 = s1;return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

可以看到,當我們以同樣的方式對Stack類的對象s2進行拷貝構造時,程序崩潰了,原因就在于Stack類中涉及到了資源管理,而Stack的賦值運算符重載又是依靠編譯器實現的。

圖解:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

結論:

  1. 賦值運算符重載格式:
    1. 參數類型:const T&,傳遞引用可以提高傳參效率。
    2. 返回值類型:T&,返回引用可以提高返回的效率,有返回值目的是為了支持連續賦值。
    3. 檢測是否自己給自己賦值。
    4. 返回*this:要復合連續賦值的含義。
  2. 賦值運算符只能重載成類的成員函數而不能重載成全局函數。
  3. 用戶沒有顯式實現時,編譯器會生成一個默認賦值運算符重載,以值的方式逐字節拷貝。
  4. 如果類中未涉及到資源管理,那么賦值運算符是否實現都可以;一旦涉及到資源管理則必須要自行實現。
14.2.1 賦值運算符重載和拷貝構造之間的辨析

我們通過下面這段代碼來感受一下賦值運算符重載和拷貝構造的區別:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator==(const Date& d2){return _year == d2._year&& _month == d2._month&& _day == d2._day;}bool operator!=(const Date& d){return !(*this == d);}Date& operator=(const Date& d)//支持連續賦值的重載賦值運算符{if (this != &d)//地址不一樣時才賦值{_year = d._year;_month = d._month;_day = d._day;return *this;} }void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
void Test()
{Date d1(2023, 9, 27);Date d2 = d1;//拷貝構造d1.Print();d2.Print();Date d3;d3 = d1;//賦值重載cout << "--------------------------------" << endl;d1.Print();d2.Print();d3.Print();
}
int main()
{Test();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

由于賦值重載是在兩個已經定義好的對象之間進行的,雖然Date d2 = d1;這條語句中用了賦值重載運算符=,但是這條語句的意思是用d1來初始化d2,也就是用一個已經定義好的對象來初始化一個正在定義的對象,所以Date d2 = d1;這條語句實際上是拷貝構造,而d3 = d1;這條語句才是賦值重載。

14.3 前置++和后置++重載

前置++和后置++的重載之所以要單獨拎出來講,是因為它們和運算符+-相比,有需要注意的地方。

由于前置++和后置++都是一元運算符,為了讓前置++與后置++能正確重載,C++規定:后置++重載時多增加一個int類型的參數,但調用函數時該參數不用傳遞,編譯器自動傳遞。

例:

#include <iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// 前置++:返回+1之后的結果// 注意:this指向的對象函數結束后不會銷毀,故以引用方式返回提高效率Date& operator++(){_day += 1;return *this;}//后置++:返回+1之前的結果// 注意:后置++是先使用后+1,因此需要返回+1之前的舊值,故需在實現時需要先將this保存一份,然后給this + 1// 而temp是臨時對象,因此只能以值的方式返回,不能返回引用Date operator++(int){Date temp(*this);_day += 1;return temp;}void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}
private:int _year;int _month;int _day;
};
int main()
{Date d;Date d1(2022, 1, 13);d = d1++;d.Print();d1.Print();cout << "-------------------------" << endl;d = ++d1;d.Print();d1.Print();return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

15 const成員函數

在引出const函數之前,我們先來看下面這種情況:

#include <iostream>
using namespace std;
class A
{
public:void Print(){cout << _a << endl;}
private:int _a = 10;
};
int main()
{A aa;//const A aa;aa.Print();return 0;
}

const修飾aa前的運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

const修飾aa后的運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

可以看到,當對象aa沒有被const修飾時,它能夠順利運行,但是當aaconst修飾后再運行編譯器就報錯了。

之所以會報錯,是因為這里涉及到一個權限被放大的問題。

在這個例子中,當我們把aa傳進Print函數時,本質上傳的是aa的地址,aa在沒有被const修飾前,&aa的類型為A*,而Print的隱藏參數this的類型為A* const,也就是說當把aa傳進Print函數后this的指向是不能被改變的,這是個權限縮小的過程,所以編譯器允許;而當aaconst修飾后,&aa的類型就成了const A*,也就是說這個時候aa是不能被修改的,但傳進Print函數后卻反而可以被修改了,這個過程就把this的權限放大了,而這是不被編譯器所允許的。

又由于this是隱藏的參數,我們沒有辦法進行修改,所以我們只能對函數用const進行修飾,那么我們就將const修飾的成員函數稱為const成員函數,這個const實際修飾的是成員函數隱藏的this指針,修飾后this的類型就變成了const A*

雖然我們實際情況下很少在定義的時候用const修飾變量,但是像下面的情況卻并不少見:

#include <iostream>
using namespace std;
class A
{
public:void Print() const{cout << _a << endl;}
private:int _a = 10;
};
void Func(const A& x)
{x.Print();
}
int main()
{A aa;Func(aa);return 0;
}

const修飾Print函數前的運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

const修飾Print函數后的運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

可以看到,當我們把對象傳給某個參數被const修飾的函數,而這個函數的內部所調用的函數卻沒有被const修飾時,就容易出錯。

因此,只要函數內部不對成員變量進行改變一般都建議用const修飾一下,加上之后const對象和普通對象都可以調用。

16 取地址及const取地址操作符重載

對于這兩個操作符一般不需要重載,使用編譯器生成的默認取地址的重載即可。

例:

#include <iostream>
using namespace std;
class A
{
public:/*A* operator&(){cout << "My &:";return this;}const A* operator&() const{cout << "My const&:";return this;}*/
private:int _a = 10;
};
int main()
{A aa;const A bb;cout << &aa << endl;cout << &bb << endl;return 0;
}

使用編譯器默認生成的:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

使用自己寫的:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

只有特殊情況,才需要重載,比如想讓別人獲取到指定的內容:

#include <iostream>
using namespace std;
class A
{
public:A* operator&(){return nullptr;//拒絕取地址}const A* operator&() const{return nullptr;//拒絕取地址}
private:int _a = 10;
};
int main()
{A aa;const A bb;cout << &aa << endl;cout << &bb << endl;return 0;
}

運行結果:

外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/160180.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/160180.shtml
英文地址,請注明出處:http://en.pswp.cn/news/160180.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Python BDD之Behave測試報告

behave 本身的測試報告 behave 本身提供了四種報告格式&#xff1a; pretty&#xff1a;這是默認的報告格式&#xff0c;提供顏色化的文本輸出&#xff0c;每個測試步驟的結果都會詳細列出。plain&#xff1a;這也是一種文本格式的報告&#xff0c;但沒有顏色&#xff0c;并且…

電動汽車充放電V2G模型MATLAB代碼

微?關注“電氣仔推送”獲得資料&#xff08;專享優惠&#xff09; 主要內容&#xff1a; 本程序主要建立電動汽車充放電V2G模型&#xff0c;采用粒子群算法&#xff0c;在保證電動汽車用戶出行需求的前提下&#xff0c;為了使工作區域電動汽車盡可能多的消納供給商場基礎負荷…

【辦公常識】寫好的代碼如何上傳?使用svn commit

首先找到對應的目錄 找到文件之后點擊SVN Commit

五大資源之Service(可以固定IP)

Service可以看作是一組同類Pod對外訪問接口,借助Service應用可以方便的實現服務發現與負載均衡 創建集群內部可以訪問Service #暴露Service(也創建在了namespace dev下) [root@master ~]# kubectl expose deployment(pod控制器) nginx --name=svc-nginx1 --type=Cluste…

基于原子軌道搜索算法優化概率神經網絡PNN的分類預測 - 附代碼

基于原子軌道搜索算法優化概率神經網絡PNN的分類預測 - 附代碼 文章目錄 基于原子軌道搜索算法優化概率神經網絡PNN的分類預測 - 附代碼1.PNN網絡概述2.變壓器故障診街系統相關背景2.1 模型建立 3.基于原子軌道搜索優化的PNN網絡5.測試結果6.參考文獻7.Matlab代碼 摘要&#xf…

mysql 中 varchar 和 text 的區別

varchar 數據類型 如何理解 varchar(50) varchar(50) 中的 50 在 mysql5.0 及以上默認是存儲的字符數&#xff0c;5.0 以下&#xff0c;默認是字節長度。 varchar 占據的存儲空間 varchar 占據的存儲空間大概有以下幾部分組成&#xff1a; varchar 類型用來存儲【實際字符…

Python入門教學——輸入任意長度的int整型一維數組

使用python輸入一個任意長度的整型一維數組&#xff1a; nums input("請輸入整數數組&#xff0c;用空格分隔&#xff1a; ") nums [int(i) for i in nums.split( )] # 將每個數轉換為整型后輸出 運行結果&#xff1a; 【注】如果不強制轉換類型&#xff0c;數字…

功能測試進階建議,學習思路講解

1. 深入了解測試理論&#xff1a; 了解測試的原理、方法和最佳實踐&#xff0c;包括黑盒測試、白盒測試、灰盒測試等。可以閱讀相關的書籍或參加在線課程。 2. 學習相關測試工具&#xff1a; 掌握常用的測試工具&#xff0c;如缺陷發現工具、性能測試工具、安全測試工具等。可以…

matlab求矩陣的偽逆或者負二分之一次方

如果X不是滿秩矩陣的時候&#xff0c;那么不能使用inv()函數來求X的逆&#xff0c;因為X此時不存在逆&#xff0c;但是我們可以求X的偽逆。 &#xff08;1&#xff09;有以下兩種方法求X的偽逆&#xff1a; 假設 X[1 2 1;1 2 1;2 3 4]; 1, pinvXpinv(X); 2, [U,D,P]svd(X); r…

如何使用YOLOv8代碼框架中的RT-DETR

1. RT-DETR RT-DETR是由由此&#xff0c;百度推出了——RT-DETR (Real-Time DEtection TRansformer) &#xff0c;一種基于 DETR 架構的實時端到端檢測器&#xff0c;其在速度和精度上取得了 SOTA 性能。 RT-DETR開源的代碼在百度自己的飛槳paddlepaddle上&#xff0c;因此非…

基于天鷹算法優化概率神經網絡PNN的分類預測 - 附代碼

基于天鷹算法優化概率神經網絡PNN的分類預測 - 附代碼 文章目錄 基于天鷹算法優化概率神經網絡PNN的分類預測 - 附代碼1.PNN網絡概述2.變壓器故障診街系統相關背景2.1 模型建立 3.基于天鷹優化的PNN網絡5.測試結果6.參考文獻7.Matlab代碼 摘要&#xff1a;針對PNN神經網絡的光滑…

新手必看!!附源碼!!STM32通用定時器輸出PWM

一、什么是PWM? PWM&#xff08;脈沖寬度調制&#xff09;是一種用于控制電子設備的技術。它通過調整信號的脈沖寬度來控制電壓的平均值。PWM常用于調節電機速度、控制LED亮度、產生模擬信號等應用。 二、PWM的原理 PWM的基本原理是通過以一定頻率產生的脈沖信號&#xff0…

【5 樹與二叉樹】統計二叉樹結點值和。

typedef struct BiTNode{int data;struct BiTNode *lchild,*rchild; }*BiTree,BiTNode;int sum(BiTree T){if(Tnull)return 0;return T->datasum(T->lchild)sum(T->rchild); }

Android:Google三方庫之Firebase集成詳細步驟(二)

Analytics分析 1、將 Firebase 添加到您的 Android 項目&#xff08;如果尚未添加&#xff09;&#xff0c;并確保在 Firebase 項目中啟用了 Google Analytics&#xff08;分析&#xff09;&#xff1a; 如果您要創建新的 Firebase 項目&#xff0c;請在項目創建過程中啟用 G…

實時錯誤’-2147217887‘多步OLB DB 操作產生錯誤。如果可能,請檢查OLE DB狀態值

目錄 背景問題問題分析問題解決 錯誤解決與定位技巧總結 背景 仍舊是學生信息管理系統的問題&#xff0c;當時做的時候沒發現這么多問題呢&#xff0c;只能說明一件事&#xff0c;做的時候沒有站在用戶的角度考慮需求&#xff0c;設置了什么內容&#xff0c;就按照設置好的去測…

AIGC ChatGPT4總結SQL優化細節操作

數據庫SQL優化是一個復雜的過程,它通常涉及到許多不同的技術和方法。以下是一些常用的SQL優化策略: 1. **索引使用**:索引可以極大地加速查詢速度。但是,索引并不總是有好處的,因為它們需要額外的空間來存儲,并且在插入和更新數據時可能會減慢速度。因此,選擇正確的字段…

Unity中Shader紋理的過濾

文章目錄 前言一、為什么要過濾&#xff1f;二、過濾方式1、Point(no filter) 無過濾2、Bilinear 雙線性過濾3、Trilinear 三線性過濾 前言 Unity中Shader紋理的過濾 一、為什么要過濾&#xff1f; 事實上沒有一個紋理上的紋素是與屏幕上的像素是一一對應的。 屏幕上的 一個…

redis安裝(Windows和linux)

如何實現Redis安裝與使用的詳細教程 Redis 簡介 Redis是一個使用C語言編寫的開源、高性能、非關系型的鍵值對存儲數據庫。它支持多種數據結構&#xff0c;包括字符串、列表、集合、有序集合、哈希表等。Redis的內存操作能力極強&#xff0c;其讀寫性能非常優秀&#xff0c;且…

萬字解析:十大排序(直接插入排序+希爾排序+選擇排序+堆排序+冒泡排序+快速排序+歸并排序+計數排序+基數排序+桶排序)

文章目錄 十大排序排序算法復雜度及穩定性分析一、 排序的概念1.排序&#xff1a;2.穩定性&#xff1a;3.內部排序&#xff1a;4.外部排序&#xff1a; 二、插入排序1.直接插入排序2.希爾排序 三、選擇排序1.直接選擇排序方法一方法二直接插入排序和直接排序的區別 2.堆排序 四…

【藍橋杯省賽真題45】Scratch九宮格游戲 藍橋杯scratch圖形化編程 中小學生藍橋杯省賽真題講解

目錄 scratch九宮格游戲 一、題目要求 編程實現 二、案例分析 1、角色分析