現代C++學習指南-具體類

来源:https://www.cnblogs.com/honguilee/archive/2023/06/21/17478637.html
-Advertisement-
Play Games

> 類作為C++中重要的概念之一,有著眾多的特性,也是最迷人的部分! 類是一個加工廠,開發者使用C++提供的各種材料組裝這個工廠,使得它可以生產出符合自己要求的數據,通過對工廠的改造,可以精細控制對象從出生到死亡的各種行為,真正達到我的代碼我做主的境界。 ### 類 我們經常說的面向對象三大特征:封 ...


類作為C++中重要的概念之一,有著眾多的特性,也是最迷人的部分!

類是一個加工廠,開發者使用C++提供的各種材料組裝這個工廠,使得它可以生產出符合自己要求的數據,通過對工廠的改造,可以精細控制對象從出生到死亡的各種行為,真正達到我的代碼我做主的境界。

我們經常說的面向對象三大特征:封裝,繼承和多態,其實說的是一種抽象維度。最簡單的就是具體類,它將數據打包在一起,提供操作數據的函數,使得開發者不再需要通過傳參的形式傳遞數據。它實現了事物的抽象,也就是所謂的封裝。第二層是在一堆數據中提取出共性的部分作為基類,然後將特性作為子類,充分利用繼承的優點,實現代碼復用。它不僅追求數據抽象,也追求行為上的相似性。而更進一步,一套演算法不關心實際的數據,只關心它可以用來完成什麼工作,甚至相互都不知道對方的存在,唯一的共同點就是都繼承自某個類,都能完成那個類指定的操作,至於細節都不關心,這就是多態,類只是一種規範流程。從第一層到第三層,抽象的事物從具體轉向抽象,重心也從數據轉向行為,只是為了更好的可維護性和解耦性。三者的關係可能是下圖這樣的:

為了能將跟高級的繼承和多態講明白,本篇我們將著重介紹他們的第一形態:封裝,也就是具體類。

類的基本組成

類是一種自定義類型,主要由兩部分組成:成員變數保存類管理的數據,成員函數操作數據。
和普通變數相比,類中的成員變數最大的不同是其生命周期。成員變數在類實例化後才占用空間,構造函數完成其初始化工作,在構造完成後,成員函數就可以無限制地使用成員變數,直到析構函數被調用。
成員函數和普通函數的不同之處是成員函數有個隱含的this指針,這個指針指向成員變數的存儲位置,也就是可以很方便地完成成員變數的訪問。
由此可見,具體類研究的主體是數據。接下來我將圍繞著數據的生命周期完成對類特性的解析。

對象的創建和銷毀

類的第一大作用就是控制類怎麼生成和銷毀。和Java不同,不需要用new也會涉及到構造函數的調用,哪怕只是個普通的局部變數,出了變數的作用範圍,對象就會被銷毀,記憶體就會被釋放。

class Sample{
 public:
    Sample(){
        std::cout<<"Creating a Sample object"<<std::endl;
    }
    
    ~Sample(){
        std::cout<<"Destoring a Sample object"<<std::endl;
    }
};

int main(){
    // Sample的構造函數被調用
    Sample a; 
    {
        // 大括弧創建了一個局部作用域,對象b只存在大括弧範圍內,出了大括弧後,b就會被銷毀,調用Sample的析構函數
        Sample b;
    }
    // 此時只有對象a還存活
}
// 輸出
// Creating a Sample object
// Creating a Sample object
// Destoring a Sample object
// Destoring a Sample object

上面的Sample是最簡單的類定義,我們只創建了類的構造函數和析構函數,在main函數中,創建了兩個變數。通過檢查輸出,我們可以確定類的構造函數和析構函數都被調用了。
上面那個類從功能上毫無用處,我們只能創建一個它的對象,然後看著它死去,什麼也幹不了。接下來,我們來改造下Sample類,讓它能在構造的時候告訴我們,哪一個對象在構造。

class Sample{
    Sample(const std::string name){
            std::cout<<"Creating a Sample object:name = "<<name<<std::endl;
        }
    //其餘不變
};

int main(){
    // 由於創建對象a時,用到了string對象,所以要先創建一個string對象
    std::string str{"a"};
    // 此時構造類需要一個名字了,我們已經控制了類的初始化狀態
    Sample a{str}; 
    {
        // Sample唯一給構造函數需要一個string的對象,但是編譯器推測出傳遞給Sample構造函數的參數類型是字元串常量
        // 參數不匹配,但這還沒達到編譯失敗的條件,因為編譯器還沒檢查是否存在一種從字元串常量生成字元串對象的構造函數,
        // 答案是有的,string類提供了這樣的構造函數
        // 接下來編譯器用字元串常量構造出了string對象,自動完成了string對象的創建
        // 並傳遞給Sample的構造函數,條件滿足,編譯順利完成
        Sample b{"b"};
    }
}
// 輸出
// Creating a Sample object:name = a
// Creating a Sample object:name = b
// Destoring a Sample object
// Destoring a Sample object

上面的例子有一個值得註意的地方,那就是對象b直接從字元串常量創建出來了,省略了中間字元串對象,其實這一步是編譯器為我們完成了,它的創建過程和a是完全一樣的,這種行為稱為隱式轉換。
這時的Sample類還是什麼也做不了,甚至連哪一個對象被銷毀了我們都不知道。析構函數是函數,那麼給析構函數添加參數行不行呢?答案是不行,因為析構函數是編譯器自動幫我們調用的,它不知道調用時需要什麼參數,所以就只能是無參。那麼有什麼辦法能正確標記出是哪個對象被銷毀了呢,答案是成員變數。
成員變數和對象是同生共死的,它和對象使用同一塊記憶體。對象創建就為成員變數也分配了空間,但是沒有初始化,需要開發者在構造函數或者其他函數使用前初始化。在析構函數調用時,記憶體尚未被回收,這時候是使用成員變數的最後時機。成員變數還有另一個重要的特點,在類中定義的所有非static函數都能使用它,不需要通過函數參數傳遞。這也是類設計的初衷之一,用類管理數據。
所以,接下來的析構函數可以這樣寫

class Sample {
private:
    // 第一步,創建一個成員變數
    std::string name;
public:
    // 第二步,在構造函數中初始化成員變數
    Sample(const std::string name) :name{ name } {
        std::cout << "Creating a Sample object:name = " << name << std::endl;
    }

    ~Sample() {
        //第三步,使用成員變數
        std::cout << "Destoring a Sample object:name = " << name << std::endl;
    }
};

int main() {
    std::string str{ "a" };
    Sample a{ str };
    {
        Sample b{ "b" };
    }
}
// 輸出
// Creating a Sample object:name = a
// Creating a Sample object:name = b
// Destoring a Sample object:name = b
// Destoring a Sample object:name = a

可以看到,創建成員變數也很簡單,關鍵在於第二步,這和Java又不一樣。第二步中,初始化成員變數使用了特殊的語法,在構造函數小括弧後面添加了:,然後普通變數初始化的語法,稱之為成員變數初始化。這樣寫的關鍵原因在於,對象創建需要先申請記憶體,記憶體申請後使用:後面的初始化方式初始化成員變數,最後才調用構造函數完成對象的創建,每一步都有它對應的位置和作用。假如像Java一樣寫在構造函數裡面,就相當於將第二步放到了第三步,打亂了它本來的順序。
為了說明成員函數確實在對象的整個生命周期都可以使用,我們再個它添加一個成員函數吧。

class Sample{
    void print() {
        std::cout << "Invoke print name = " << name << std::endl;
    }
    //其餘不變
}
int main() {
    std::string str{ "a" };
    Sample a{ str };
    {
        Sample b{ "b" };
        b.print();
    }
    a.print();
}
// 輸出
// Creating a Sample object:name = a
// Creating a Sample object:name = b
// Invoke print name = b
// Destoring a Sample object:name = b
// Invoke print name = a
// Destoring a Sample object:name = a

我們添加了一個成員函數print它沒有參數,但是它的函數體使用了成員變數name,可以看到,它也能正常工作。
至此,對象的創建和銷毀就說得差不多了。還沒說到的是構造函數可以有很多個,在創建對象的時候可以選擇使用哪種方式創建,編譯器會根據傳遞的參數來推導出實際使用的構造函數,開發者需要考慮的是提供的構造函數都能完成成員函數的正確初始化,以便在調用成員函數時,成員函數都能按預期工作。如Sample,我們還可以提供一個無參的構造函數,然後將name初始化為空字元串,這樣print和析構函數也能正常工作。
總結一下,類是管理數據的容器,它的數據隨著對象的創建而創建,併在對象存在的整個生命周期都可用。構造函數需要保證數據的初始化,並可以控制它構造的方式,成員函數可以隨時使用,析構函數是對象銷毀時最後一個調用,它需要保證數據到此都被清理。

數據的轉移和共用

數據拷貝

數據創建之後,不僅可以供成員函數使用,還可能被轉移到其他對象中去。或者和其他對象共用。複製構造函數可以控制數據以怎樣的方式和其他對象共用。

class Sample {
private:
    int value;
public:
    Sample(const int value) :value{ value } {
        std::cout << "Create value = " << value << std::endl;
    }

    // 以Sample命名,是構造函數,函數參數是自己的類型,說明是複製構造函數
    // 這個複製構造函數選擇用賦值的形式共用value數據
    Sample(const Sample& sample) :value{ sample.value } {
        std::cout << "Copy create object" << std::endl;
    }

};

void use(Sample sample) {
    //函數返回,sample對象被銷毀
}

int main() {
    Sample a{ 1 };
    // a的數據被分享給一個臨時對象了,此時出現了兩個對象,它們的value都是1
    use(a);
}
// 輸出
// Create value = 1
// Copy create object

複製構造函數有以下幾個特征

  1. 會出現至少兩個同類型的對象。因為複製需要先有一個存在的對象,再用這個存在的對象數據初始化另一個正在創建的對象的成員變數。這也是複製構造函數參數是自己的原因。
  2. 存在變數從無到有初始化的情況都會調用複製構造函數。函數調用,形參需要初始化為實參,參數本來不存在,調用函數會傳遞一個已存在的對象,就會調用到複製構造函數。這也是為什麼複製構造函數參數是引用的類型。假如是普通變數,調用複製構造的時候需要產生臨時變數,臨時變數又需要調用複製構造函數,程式就會陷入無限遞歸中。
  3. 除了函數調用,函數返回值,用對象初始化新變數的情況也會調用到複製構造函數。函數返回後,函數體中所有的局部變數都會被銷毀,返回值也屬於一種局部變數肯定也要被銷毀,但是返回後的值卻需要被 外部使用,它們的生命周期是不一樣的,由此我們就知道肯定創建了一個新的對象,這個對象被局部返回值初始化,但是有著和外部一樣的生命周期。用對象初始化變數就更直觀了,初始化的對象是從無到有創建的。符合構造函數出現的特點。

我們可以來驗證一下

//其餘不變
Sample returnSample() {
    // 用普通構造函數初始化的
    Sample sample{ 2 };
    return sample;
}
int main() {
    Sample a{ 1 };
    std::cout << "init local variable" << std::endl;
    // b是新對象,用a初始化的,所以調用了複製構造函數
    Sample b = a;
    // use的形參被用來初始化
    std::cout << "Use Sample as parameter" << std::endl;
    use(a);
    //返回的sample被用來初始化c
    std::cout << "return sample" << std::endl;
    Sample c = returnSample();
}
// 輸出
// Create value = 1
// init local variable
// Copy create object
// Use Sample as parameter
// Copy create object
// return sample
// Create value = 2
// Copy create object

可以看到,這三種情況都會造成複製構造函數的調用。

數據移動

數據拷貝雖然簡單易行,但是還是有個小瑕疵。考慮下麵這種場景:

void swap(Object& left,Object& right){
    // 有新對象產生,拷貝構造,目前記憶體中有兩份一模一樣的left
    Object temp=left;
    // 賦值操作,生成了一個right的臨時對象
    left=right;
    // 賦值操作,生成了一個temp的臨時對象
    right=temp;
    // 三個臨時對象都被銷毀
}

int main(){
    Object a;
    Object b;
    swap(a,b);
    return 0;
}

一個簡單的交換邏輯,我們就生成了很多的臨時對象,假如我們操作的是列表,大對象,短時間內大量創建並銷毀對象,就會造成記憶體抖動,嚴重影響系統的穩定性,而且,我們的真正目的只是將兩個變數的值交換一下而已。所以相較於拷貝,我們還有更好的選擇:移動。

左值和右值

說起移動,就不得不提到左值和右值。這裡的左和右是相對於=來說的。
我們知道=是用來賦值的,這下麵隱藏著三個動作:生,取,寫。在記憶體中生成一個臨時數據,讀取變數保存位置,將臨時變數內容寫入保存位置。生就是指的右值,它保存在我們不知道的記憶體位置,在寫動作完成後,它就被回收了。而取對應的就是左值,我們用變數名保存了它的記憶體位置,在它作用域內可以反覆讀寫。所以右值最大的特點就是不知道地址,如i=i+1就會先生成一個i+1的臨時對象,我們不知道地址,所以它是右值。與之相對的左值,是可以通過&讀到地址的。
接下來我們再來談一談引用。我們通常是用別名來理解引用的,但是可能會忽略一個小細節,別名也是需要有歸屬的,也就是它代表的地址在哪裡。基於這個前提,我們就可以推導出凡是存在記憶體中的數據,理都是有地址的,而右值是存在記憶體中的,它也應該需要一種方式來獲得地址,稱之為右值引用,相對的一般變數的引用就稱為左值引用。
說回到移動,前面的複製構造函數雖然能將數據和其他對象共用,但是大部分情況下,數據其實不需要共用的,只需要轉移,也就是將數據的所有權移動到另一個對象上,原始對象就不再有效。所以C++提供了移動構造函數來完成這個操作。

class Sample {
private:
	int* value;

public:
	Sample(const int value) :value{ new int{value} } {
		std::cout << "Create value = " << value << std::endl;
	}

	Sample(const Sample& sample) :value{ new int {*sample.value} } {
		std::cout << "Copy create object" << std::endl;
	}

	Sample(Sample&& sample) :value{ sample.value } {
		sample.value = nullptr;
		std::cout << "Move create object" << std::endl;
	}

	~Sample() {
		delete value;
		std::cout << "destory sample" << std::endl;
	}

	friend std::ostream& operator<<(std::ostream& os, const Sample& sample) {
		os << "Sample value is " << sample.value;
		return os;
	}

};

void use(Sample sample) {
	std::cout << "Use sample " << sample << std::endl;
}


int main() {
	// 普通變數,1被使用後馬上銷毀了
	int a = 1;
	//左值引用
	int& b = a;
	//右值引用,引用的就是1那個暫存的地址
	int&& c = 1;
	//可以修改引用的值
	c = 2;

	Sample sample{ 1 };

	use(std::move(sample));

	std::cout << sample << std::endl;
}
// 輸出
// Create value = 1
// Move create object
// Use sample Sample value is 009B8E90
// destory sample
// Sample value is 00000000
// destory sample

在上面的代碼里,我們真正使用sample對象的是函數useuse執行完後,sample就沒用了。所以我們用std::move將數據轉移到了函數實參中,外部的sample不再擁有那塊記憶體的占用。很多場景其實都是類似的情況:外部配置參數後,傳遞給某個函數使用,所以這種情況下就沒必要構造一個新的對象出來,假如業務很長的話,sample對象就會一直占用記憶體,但是它是早就沒用了的。所以移動構造函數就發揮了大作用。

數據共用

除了通過複製構造函數和成員函數共用數據外,還可以通過友元類和友元函數。它們都是一種特殊的訪問數據的形式,可以直接訪問到數據,不經過成員函數的調用。所以在有些時候友元能幫助減少函數調用的花銷,有些時候則會引入不可預期的行為。

class FriendClass {

public:
    void useSample(const Sample& sample) {
        std::cout << "Sample value is " << sample.value << std::endl;
    }
};

上面的例子,如果按照常規是無法通過編譯的,因為samplevalue是私有的。前面我們知道,成員函數是可以訪問私有變數的,但是這個類是定義在Sample外的,這個函數是另一個類的成員函數,完全沒辦法完成這種訪問。當然,這種情況下,我們可以修改Sample類的定義,添加一個成員函數就解決了。但是假如FriendClass有多個成員函數都需要訪問Sample的私有成員呢,這個時候添加成員函數的方式就不再適用,所以出現了友元類。
實現友元類很簡單,簡單到只需要添加一條聲明。首先友元類需要至少兩個類,一個類是想要訪問私有成員的源類,另一個是含有私有成員的目標類,然後我們把友元聲明放在目標類里,源類就可以順利訪問到目標類的私有成員了。在上面的例子FriendClass想要訪問Sample的私有成員,所以它是源類,是普普通通的類。Sample含有FeiendClass想訪問的私有成員value,所以它是目標類,聲明需要添加到它的類定義裡面。

Class Sample{
    private:
    int value;
    friend class FriendClass;
    
    //其餘不變
}

加上這一條之後,前面的FriendClass就可以正常通過編譯了。這一句的威力很大,大到FriendClass的所有成員函數都能訪問到value。假如這不是你的期望,但是還是想要直接訪問到value,那麼就可以適用友元函數。
友元函數是普通的函數,雖然它聲明在類里,但是不能直接訪問到類的私有成員,而是通過函數參數的形式。為了和普通的成員函數區分開來,它的聲明最前面需要添加關鍵字friendfriend仿佛像打開了許可權控制的開關,可以使函數訪問到參數的私有成員。

class Sample{
    friend std::ostream& operator<<(std::ostream& os,const Sample& sample) {
        os << "Sample value is " << sample.value << std::endl;
        return os;
    }
    //其餘不變
}

int main() {
    Sample a{ 1 };
    std::cout << a << std::endl;
}

// 輸出
// Create value = 1
// Sample value is 1

函數<<是友元函數,因為函數聲明有關鍵字friend。友元函數不是成員函數,想在函數體訪問到成員變數,需要添加函數參數。那麼函數參數有很多個,怎樣確定參數私有成員的可訪問性呢,這就得看這個友元函數聲明在哪個類裡面了,友元函數的聲明位置直接確定了它訪問私有成員的範圍。

特殊的成員函數

C++的類有極大的定製性,這種定製性不僅僅表現在數據上,還表現在成員函數上。我們知道一般的成員函數都是使用.來調用的,但是出於特殊的場景,有些情況下這種調用形式不僅僅不直觀,還效率不高。所以C++提出了運算符的概念。之所以稱為運算符,是因為函數的調用和傳參形式和普通的成員函數不一樣。定義良好的運算符可大大提高代碼的可讀性。如

  • []操作符是下標運算符,有了它的幫助,我們就可以像obj[2]這樣取容器中的元素了。
  • ()則可以把對象當成函數一樣直接調用,實現函數式編程的效果。
  • ->可以返回另外的對象,使得它可以表現出另一個對象的行為。

還有其他的諸如++--等操作符,在定義特定類型的類時,提供合適的運算符函數能使我們的類更簡潔、好用。

總結

總的來說,類是一個數據管理器,構造函數控制數據生成,來源可以使其他類型,也可以是相同類型。用相同類型生成新數據的時候,有複製和移動兩種選擇。複製構造函數控制相同類型的數據共用行為,其主要目標就是實現兩個類型在構造函數完成那一刻,在記憶體中的數據是完全一致的。移動構造函數的目標則是將現有的數據轉移到當前構造的對象上來,然後使現有的數據失效,從而達到減少對象創建、銷毀,增加記憶體利用率的目的。除此之外,還能使用成員函數改變或者訪問數據,最終在析構函數中結束數據的生命。此外友元類或者友元函數也是一種數據訪問途徑。
具體類的主要矛盾是數據,設計類的關鍵還是要弄清數據流向。數據自身在內部能有什麼狀態,能實現什麼行為,是成員函數該完成的工作。此外還要考慮相同類型相互構造時數據的共用情況,是完全隔離,還是相互影響,這些都是應該考慮的問題。畢竟確保數據從創建到銷毀的可靠性和有效性是一個具體類應該完成的基本功能。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 本文介紹了一種新的HTML元素搜索方法,並提供了一個實用的工具來幫助開發者快速找到所需的元素。這對於那些需要處理大量HTML元素的開發者來說是非常有用的。文章還通過提供一些常見元素的用法示例,幫助開發者更好地理解和應用這些元素。在眾多元素 ...
  • 前不久,在網上看到這麼一張非常有趣的圖: ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4edc3cef70d744d381eabb604ef089cb~tplv-k3u1fbpfcp-watermark.image?) 想必很多同學都看 ...
  • Stencil 組件使用 `JSX` 渲染,這是一種流行的聲明式模板語法。每個組件都有一個渲染函數,它返回在運行時渲染到 DOM 的組件樹。 ## 基礎用法 `render` 函數用於輸出將繪製到屏幕上的組件樹。 ```ts class MyComponent { render() { return ...
  • 註冊表是Windows中的一個重要的資料庫,用於存儲系統和應用程式的設置信息,註冊表是一個巨大的樹形結構,無論在應用層還是內核層操作註冊表都有獨立的API函數可以使用,而在內核中讀寫註冊表則需要使用內核裝用API函數,如下將依次介紹並封裝一些案例,實現對註冊表的創建,刪除,更新,查詢等操作。 ...
  • 工業互聯網項目開發全流程V3.0 工業互聯網項目開發工作流程及核心問題 一、需求分析 1、共用平臺需求分析 這個平臺要解決什麼問題?這個平臺的用戶群體是誰?這個平臺應該具備哪些主要功能?這個平臺的使用場景是什麼?這個平臺如何與現有的系統集成? 2、需求文檔設計 需求文檔是否詳細、明確,且可以量化?是 ...
  • > 本文首發於公眾號:Hunter後端 > 原文鏈接:[celery筆記五之消息隊列的介紹](https://mp.weixin.qq.com/s/fw7b1Gha0XpTYuCg3aZcWA) 前面我們介紹過 task 的處理方式,將 task 發送到隊列 queue,然後 worker 從 qu ...
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第21面: > 面試官:用過STL嗎? > > 二師兄:(每天都用好嗎。。)用過一些。 > > 面試官:你知道STL是什麼? > > 二師兄:STL是指標準模板庫(`Standard Template Library`),是C++區別於C語言的特征之 ...
  • # 泛型的定義 ```Scala object _11_泛型 { def main(args: Array[String]): Unit = { //[A] 這個代表的就是泛型 ==》 在創建對象的時候,可以指定需要傳進去的類型 //作用就是在創建對象的時候,可以對傳進去的參數一個約束,當設置泛型位 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...