EffectiveC++筆記 第4章

来源:https://www.cnblogs.com/1Kasshole/archive/2018/05/27/9096980.html
-Advertisement-
Play Games

我根據自己的理解,對原文的精華部分進行了提煉,併在一些難以理解的地方加上了自己的“可能比較準確”的「翻譯」。 Chapter4 設計與聲明 Designs and Declarations 條款18: 讓介面容易被正確使用,不易被誤用 欲開發一個“容易被使用,不容易被誤用”的介面,首先必須考慮客戶可 ...


我根據自己的理解,對原文的精華部分進行了提煉,併在一些難以理解的地方加上了自己的“可能比較準確”的「翻譯」。

Chapter4 設計與聲明 Designs and Declarations

條款18: 讓介面容易被正確使用,不易被誤用

欲開發一個“容易被使用,不容易被誤用”的介面,首先必須考慮客戶可能做出什麼樣的錯誤。

假設我們要設計一個表示日期的class:

class Data{
public:
    Date(int month,int day,int year);
    ...
};

事實上使用它的客戶很容易犯錯誤:

以錯誤的次序傳參:

Date d(30,3,1995); //喔喲! 應該是“3,30”而不是"30,3"

傳遞無效的參數:

Date d(2,30,1995); //2月有30號????

我們可以引入類型系統(type system)和外覆類型(wrapper types)
現以外覆類型來區別天數,月份,年份,然後再Date中使用:

struct Day{                      struct Month{
    explicit Day(int d)              explicit Month(int m)
        :val(d) {}                       :val(m) {}
    int val;                         int val;
};                               };

struct Year{
    explicit Year(int y)
        :val(y) {}
    int val;
};
class Date{
public:
    Date(const Month& m,const Day& d,const Year& y);
      ...
};
Date d(30,3,1995); //not ok
Date d(Day(30),Month(3),Year(1995)); //not ok
Date d(Month(3),Day(30),Year(1995)); //ok

其實你也可以用更成熟的class來封裝外覆類型,但這裡的struct已經很好了。

類型確定後,通常要對值進行限制,比如一年只有12個月。你可以用enum來表現月份。但是enum不具備類型安全性,比如enums可以被拿來當一個ints使用。

比較安全的解法是預先定義所有有效的Months:

class Month{
public:
    static Month Jan() { return Month(1); }
    static Month Feb() { return Month(2); }
    ...
    static Month Dec() { return Month(12); }
    ...
private:
    explicit Month(int m);
    ...
};
Date d(Month::Mar(),Day(30),Year(1995));

常見的預防客戶出錯的辦法是限制類型內的許可權。例如加上const。

另外,::應儘量讓你的包裝types與內置types行為一致::。客戶已知道int這樣的type會有什麼行為,所以你應讓你包裝的types也有相同表現。<避免無端與內置類型不相容>

記住,任何介面如果試圖讓客戶「必須記得做某些事情」,就是有著「不正確使用」的傾向。

還記得條款13嗎,tr1::shared_ptr接受了func()返回的指針,這將發揮智能指針的威力。
但是如果客戶自己就忘記了要用到智能指針呢?較佳介面的設計原則是先發制人,也就是這樣寫func():

std::tr1::shared_ptr<xx> func();

這實質上強迫客戶將返回值儲存於一個tr1::shared_ptr內,讓介面設計者得以阻止一大群客戶犯下資源泄漏的錯誤。

還有一種特殊情況。假設作為class設計者的你想讓那些“從func()取得xx*指針”的客戶將該指針傳遞給一個名為getRidOfxx()的函數,而不是粗暴地直接對此指針使用delete。可能你這樣設計有出於你的考慮,但是客戶還是可能忘記並仍使用delete。所以func()的設計者可先發制人,不僅返回一個tr1::shared_ptr,併在它身上綁定刪除器(deleter) getRidOfxx()。

事實上tr1::shared_ptr有一個重載的構造函數接受兩實參:一個是被管理的指針,另一個是當引用次數變為0時被調用的“刪除器”:

std::tr1::shared_ptr<xx> pInv(0,getRidOfxx);
//企圖創建一個null智能指針,但是無法通過編譯。

這個構造函數堅持第一個參數必須是指針,而不是int型的值0———雖然它能被轉換為指針。所以轉型可解決:

std::tr1::shared_ptr<xx> pInv(static_cast<xx*>(0),
    getRidOfxx);

//static_cast以後提到.

而作為func()的內部實現:

std::tr1::shared_ptr<xx> func()
{
    std::tr1::shared_ptr<xx> retVal(static_cast<xx*>(0),
                                            getRidOfxx);
      retVal = ...; //令retVal指向正確對象
    return retVal;
}

當然,若將被pInv接管的原始指針已經在建立pInv之前確定了,那麼直接傳此指針給pInv構造函數是更佳選擇。

條款19: 設計class猶如設計type

實際上,你定義一個新class時,可理解為你定一個了新type。

這意味著你不僅是class設計者,還是type設計者,重載(overloading)函數和操作符、控制記憶體的分配和歸還、定義對象的初始化和終結……都在你手上

考慮以下問題,你的回答往往影響你的設計規範:

  1. 新class,或者說type該如何被創建和銷毀?

這將影響你的構造、析構、記憶體分配與釋放函數:
(operator new, operator new[], operator delete, operator delete[])

前提是你打算撰寫它們

  1. 對象初始化和賦值該有怎樣的差別?

決定了你的構造函數和賦值操作符的行為。別混淆“初始化”和“賦值”,它們對應不同的函數調用(條款4)。

  1. 新type對象若被passed by value(以值傳遞),意味著什麼?

考慮copy構造函數用來定義一個type的pass-by-value該如何實現

  1. 什麼是新type的“合法值”?

對於你設計的class成員變數,你必須考慮它們取值的範圍以及規範(約束條件),這決定了你的成員函數必須進行的錯誤檢查工作。它也影響函數拋出的異常。

  1. 你的type需要配合某個繼承圖系嗎?

如果你的type繼承自現有的classes,就會受到設計約束。特別是受到“它們的函數是virtual或non-virtual”的影響。若你允許其它classes繼承你的class,這要考慮你的函數是否為virtual。

  1. 你的type需要什麼樣的轉換?

如果你希望你的type T1能隱式轉換為T2,就必須在class T1內寫一個類型轉換函數( operator T2 )或在class T2內寫一個non-explicit-one-argument(可被單一實參調用)的構造函數。若你只允許顯式轉換,就得寫出專門負責執行轉換的函數。

  1. 什麼樣的標準函數應駁回? 那些是你應聲明為private的成員(條款6)

  2. 誰改取用新type的成員?

這將幫助你決定哪個成員為public、private、proteced。也幫你決定哪個class,functions應該是友元,以及它們的嵌套是否合理。

條款20: 寧以pass-by-reference-to-const替換pass-by-value

C++預設是以by value方式(繼承自C)傳遞對象至函數(或來自函數)。這樣一來,函數參數都是以實參的副本為初值,而調用端獲得的亦是函數返回值的副本。這些副本是由對象的copy構造函數產出,會成為費時的操作。

考慮以下代碼:

class Person{
public:
    Person();
    virtual ~Person();
    ...
private:
    std::string name;
    std::string address;
};

class Student: public Person{
public:
    Student();
    ~Student();
    ...
private:
    std::string schoolName;
    std::string schoolAddress;
};

假設我們有這樣的代碼:

bool validateStudent(Student s); //by value
Student plato;
bool platoIsOk = validateStudent(plato);

無疑,會以plato為藍本初始化s,返回後s被銷毀。
你會發現,在這裡,以by value傳遞一個Student對象會導致調用一次Student copy構造函數、一次Person copy構造函數、四次string copy構造函數。

但如果以pass by reference-to-const的方式,效率會高得多:

bool validateStudent(const Student& s);

這時,沒有任何構造函數或析構函數被調用,因為沒有任何新對象被創建。這個const修飾符很重要,它可以保證函數不會修改源頭的Student。

另外,by reference方式可避免slicing(對象切割)。假設base class為A,derived class為B,有這種代碼:

void func(A obj)..

而你傳參的操作:

B tmp = new B();
func(tmp);

此時A的copy構造函數被調用,但是屬於B的特質化成員會被無視掉,只剩A對象的框架。此時解決方案即為以by reference-to-const方式傳遞參數。

當應用於C++內置類型,如int之類,pass-by-value可能會更高效,這同樣適用於STL迭代器和函數對象。

條款21: 必須返回對象時,別妄想返回其reference

在嘗到傳引用的甜頭後,你可能從此一發不可收拾。但是你總有一次會犯下致命錯誤:開始傳遞一些referencce指向司機不存在的對象。

現在假設有一個表達有理數(Rational Number)的Class:

class Rational{
public:
    Rational(int numerator = 0,
             int denominator = 1); //分別表示分子和分母
    ...
private:
    int n,d; //分子和分母的內部儲存
    friend const Rational operator*(const Rational& lhs
                                    const Rational& rhs);
    //將*操作符的重載函數定義為友元
};

然後我們在主調函數中有下麵的操作:

Rational a(1,2); //a = 1/2
Rational b(3,5); //b = 3/5
Rational c = a * b; //c算得3/10

第三條語句相當於 Rational c = operator*(a,b); ,這時函數會返回適當的「值」賦給c。

現在看第一個版本的*運算重載函數:

const Rational operator*(const Rational& lhs
                         const Rational& rhs)
{
    Rational result(las.n*rhs.n , lhs.d*rhs.d);
    return result;  //返回一份copy
}

經過之前學習,我們知道這樣開銷較大。

現在考慮考慮返回引用的版本,即將細節改成 return &result; ,並將返回類型改成const Rational&

這有嚴重問題,不用new來構造對象的話,對象只是一個local本地對象,它將在函數退出後被銷毀。這會導致你得到的引用指針將會指向一個不明的「殘骸」

看看另一種版本,由new構造的對象儲存在heap堆上:

const Rational& operator*(const Rational& lhs
                          const Rational& rhs)
{
    Rational* result = new result(las.n*rhs.n , lhs.d*rhs.d);
    return* result;  //返回指針,被&加工為產量指針
}

沒有啥卵用,因為new的過程還是要構造對象。其實這個版本更糟,因為你需要考慮delete。

還有一種坑爹的版本,就是將函數內部的Rational對象聲明為靜態的,並返回它的引用。這裡雖然解決了被銷毀的問題,但是對於C++多線程它是不安全的。

假設我們已經寫好了==重載函數,且完全正確:

bool operator==(const Rational& lhs, const Rational& rhs);

假設有下麵的操作:

Rational a,b,c,d;
...
if((a*b)==(c*d)){...}
else ...

估計你也想到了,兩個*運算都返回一個指向同一處static對象地址的引用,所以這個式子的比較結果永遠為true。

抱歉,說了這麼多,我們還是回到了起點————對於*運算的重載,我們幾乎只能採用返回一個新對象的方法:

//第一個版本的精簡
const Rational operator*(const Rational& lhs
                         const Rational& rhs)
{
    
    return Rational(las.n*rhs.n , lhs.d*rhs.d);  
    //返回一份copy
}

總結:

  • 絕不要返回指向local stack對象的pointer或reference / 返回指向heap-allocated對象的reference / 返回指向local static對象的pointer或reference,而且可能同時需要多個這樣的對象

條款22: 將成員變數聲明為private

這個建議適用於protected成員

  1. 首先,獲取私有成員的渠道大部分是函數,所以客戶訪問成員不需要考慮究竟是否要加小括弧,因為全是函數,他們照做就是。

  2. 其次,你可以通過函數精確控制各種訪問許可權:

class AccessLevels{
private:
    int noAccess; //無任何訪問動作
    int readOnly; //read-only access
    int readWrite; //read-write access
    int writeOnly; //write-only
public:
    ...
    int getReadOnly() const { return readOnly; }
    void setReadWrite(int v) { readWrite = v; }
    int getReadWrite() const { return readWrite; }
    void setWriteOnly(int v) { writeOnly = value; }
};

一般來說,每個成員變數都需要getter和setter的情況實屬罕見,所以這樣的控制很有必要。

將成員變數隱藏在函數介面的背後,可以為“所有可能的實現”提供彈性。

現在我問你,protected成員的封裝性是否高於public?答案是不盡如人意。

我們知道,public的訪問一般要求客戶自己寫代碼來實現,一旦public的成員函數被取消,所有使用它的客戶代碼都會被破壞。而protected被取消掉的話,它的所有dervied classes都會被破壞。因此protected和public一樣缺乏封裝性。

所以從封裝的角度來看,其實只有兩種訪問許可權:private(提供封裝)和其它(不提供封裝)。

寧以non-member、non-friend替換member函數

假設有一個Class代表網頁瀏覽器。有幾個成員函數,提供了清除緩存、清除歷史記錄、清除cookies:

class WebBrowser{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    void clearEverything(); //調用上述三個函數。
    ...
};

這些功能也可由一個non-member函數實現,只需傳入一個WebBrowser對象引用就行:

void clearBrowser(WebBrowser& wb)
{
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

那麼,哪一個比較好呢?member函數clearEverything還是non-member clearBrowser?

根據面向對象守則,數據以及操作數據的函數應捆綁在一塊兒,這意味member函數是更好的選擇。然而這是一個誤解。

和你直覺相反的是,non-member函數clearBrowser封裝性實際上比member版本的clearEverything還要高。

通常來說,member函數不僅能訪問Class里的private成員,還能取用enums、typedefs等。我們說高封裝性是指應有儘可能少的代碼能夠直接「看到」私有成員變數,這時non-member函數的優越性就體現出來了,能完成同樣的機能,但又和Class的私有成員保持了絕對的距離。

所以如果只考慮封裝性的話,選擇的關鍵在於member和non-member、non-friend之間。(friend的許可權和member一樣大)

我們甚至可以將函數clearBrowser作為某工具類的一個static member函數,給其它Class用時,再變成non-member。

在C++,你以後可能比較自然的做法是,將clearBrowser成為一個non-member函數並位於WebBrowser所在同一個namespace里:

namespace WebBrowserStuff{
    class WebBrowser{...};
    void clearBrowser(WebBrowser& wb);
}

條款24: 若所有參數都需類型轉換,請採用non-member函數

令class支持隱式轉換通常會有風險。但常見的例外是建立「數值類型」。假設我們又設計一個有理數Class,允許整數“隱式轉換”為有理數很合理。假設我們這樣構造有理數Class:

class Rational{
public:
    Rational(int numerator = 0,
             int denominator = 1);   
    //刻意不為explicit,允許int-to-Rational隱式轉換
    int numerator() const;   //分子訪問
    int denominator() const;   //分母訪問
private:
    ...
};

假設此時你想讓Class支持算術運算,比如讓它能作乘法運算。你不確定要用member、non-member還是non-member friend函數。你的直覺告訴你要用member版本的operator*重載:

class Rational{
public:
...
const Rational operator*(const Rational& rhs) const;
};

這個設計能讓相乘很自然:

Rational oneEight(1,8);
Rational oneHalf(1,2);
Rational result = oneHalf * oneEight;
result = result * oneEight;

你不滿足,你希望Rationals能和ints相乘:

result = oneHalf * 2; //Good
result = 2 * oneHalf; //Bad!

Wait,乘法應該滿足交換律啊!

問題出在哪?我們翻譯一下上述代碼:

result = oneHalf.operator*(2);  //Good
result = 2.operator*(oneHalf);  //Bad!

語句一中,將int型2傳入操作符函數後,發生了隱式轉換(原參數是一個Rational引用)。有點類似於:

const Rational temp(2); //編譯器建立一個臨時對象
result = oneHalf.operator*(temp); //傳參

這裡成功的原因是我們沒有將構造函數聲明為顯式的,這為上面的操作提供了支持。

然而第二個語句呢?2作為一個int型,並沒有class,更別說operator* 成員函數。編譯器會試著在namespace或global域內尋找是否有一個non-member operator*。然而並沒有。

當然,如果構造函數是explicit,沒有一個語句會通過編譯。

結論是,當參數位於參數列(parameter list)內,才有資格參與隱式轉換。這就是為啥第一個語句能夠通過編譯。

但是我們想支持混合運算啊喂!!也就是能讓Rational和其它類型數據相運算!!

現在考慮non-member operator* :

class Rational{
...
};
const Rational operator*(const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs.denominator());
} //變成non-member函數

執行:

Rational oneFourth(1,4);
Rational result;
result = oneFourth * 2;
result = 2 * oneFourth;     //全部通過編譯,恭喜!!!!

這都很好。但要不要考慮將operator* 變為一個friend函數呢?答案是否定的。因為我們可以從上面的操作中看出,完全可以只靠Rational的public介面完成operator* 的任務。這導出一個重要的觀察:

member函數的方面就是non-member,而不是friend。

無論何時,可以避免使用friend就避免。

必須告訴你的是,這些不是「真理」。因為從Object-Oriented C++跨入Template C++後,你會考慮將Rational設計為一個class template而非class,這將引入很多新考慮,以後會提到。

Remember :

  • 若你要為某函數的所有參數(包括this隱指針所指參數)進行類型轉換,這個函數必須設計為non-member

條款25: 考慮寫出不拋異常的swap函數

swap原本是STL一部分,實現了兩個數據對象的交換。後來成為異常安全性編程的脊柱(exception-safe programming)。

以下是swap在標準程式庫中的典型實現:

namespace std{
    template<typename T>
    void swap(T& a,T& b)
    {
        T temp(a);
        a = b;
        b = temp;
    }
}
//只要T類型支持copying(copy構造函數和=賦值符),以上代碼即可幫你自動置換。

這種default swap實現很平庸,特別對於某些類型,它的效率會顯得較低。

現在我們討論這種類型,也就是“以指針指向一個對象,內含真正數據”的類型,也就是“pimp”手法(pointer to implementation)。

現在我們試著用pimp來設計一個Widget class:

class WidgetImpl{  //Widget類的數據實現
public:
    ...
private:
    int a, b, c;
    std::vector<double> v; //可能會有很多數據,複製時間長
    ...
};
class Widget{  //使用pimp手法
public:
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs) //複製Widget時,令它複製其WidgetImpl對象
    {
        ...
        *pImpl = *(rhs.pImpl);
        ...
    }
    ...
private:
    WidgetImpl* pImpl;//指向對象內含Widget數據
};

如果我們交換兩個Widget時,只希望置換其中的pImpl指針;然而預設的swap演算法不知道。在swap的三條置換語句中,不只複製了三個Widget,還複製三個WidgetImpl對象。這很缺乏效率,一點不令人興奮!

所以我們應告訴swap該怎麼做:將 std::swap 針對Widget特化。下麵進行基本構思,但是暫時通不過編譯:

namespace std{
    template<>
    void swap<Widget>(Widget& a,Widget& b) //"T為Widget"的特化版本
    {
        swap(a.pImpl, b.pImpl); //僅置換Widgets內部指針
    }
}

First,此函數開頭 template<> 表明它是std::swap的一個全特化(total template specialization)版本。 函數名後的 <Widget> 表明這一特化版本系針對”T是Widget”而設計。 所以當你將swap施行於Widget對象身上便會自動調用此版本。

我們通常不被允許改變std空間里的任何東西,但被允許為標準templates(比如此處的swap)製造特化版本。

之前說通不過編譯的原因是,pImpl指針是私有的。可以考慮將此特化版本聲明為friend;但和以往規矩不同,這次我們在Widget內部聲明一個swap的公共函數進行真正的置換工作,再特化 std::swap ,令它調用該member function:

class Widget{  
public:
    ...
    void swap(Widget& other)
    {
        using std::swap;
        swap(pImpl, other.pImpl);
    }
    ...
};
namespace std{
    template<>
    void swap<Widget>(Widget& a,Widget& b)
    {
        a.swap(b);
    }
}

實際上,這也是類似STL容器的寫法,它們都提供public swap成員函數和std::swap特化版本。

另一種情況:假設Widget和WidgetImpl都是class templates而非classes:

template<typename T>
class WidgetImpl { ... };

template<typename T>
class Widget { ... };

在Widget內寫一個swap函數依舊簡單,但是特化 std::swap 時會遇到麻煩:

namespace std{
    template<typename T>
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) //invalid!
    { a.swap(b); }
}

這麼寫不合法的原因是,我們正企圖偏特化(partially specialize)一個function template(std::swap)。然而C++僅允許對class templates偏特化。

所以慣常做法是手動添加一個重載的版本:

namespace std{
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)//註意"swap"後沒有"<...>"
    { a.swap(b); }  //其實這也不合法,稍後提出
}

在C++中,重載function templates沒問題。然而std是特殊的命名空間,C++標準委員會禁止膨脹已經寫好的東西,因為可能會發生不明行為。所以問題出在我們的重載版本正在做這樣的事。

繞了一大圈,我們沒有前功盡棄。要提供一個高效的template swap特定版本,可以聲明一個non-member swap讓它調用member swap,而不再特化 std::swap 或在std里重載它。

為了簡化,將Widget相關機能一併置入命名空間WidgetStuff內:

namespace WidgetStuff{
    ...           //模版化的WidgetImpl等等
    template<typename T>
    class Widget { ... };   //內含swap成員函數
    ...
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b)
    {
        a.swap(b);
    }
}

從現在開始,任何地點的代碼若打算置換倆Widget對象而調用swap。C++的名稱查找法則(name lookup rules)會找到WidgetStuff空間內的Widget專屬豪華版本。

以上做法適用於classes和class templates。不幸的是,有一種情況使我們不得不為classes特化 std::swap ———只要你想讓你的專屬swap能在儘可能多語境被調用,你需要寫一個該class命名空間內的non-member版本和一個 std::swap 特化版本。(稍後解釋)

< 事實上你可以不採用namespace的方式,但global空間里漫天飛的東西真的好看嗎?>

現在開始解釋: 假設你在寫一個function template,需置換兩個對象值:

template<typename T>
void doSomething(T& obj1, T& obj2)
{
    ...
    swap(obj1,obj2);
    ...  
}

該調用哪種swap呢,也許有一種可能存在的T專屬版本此時棲身於某namespace中?(當然不可以在std內) 所以你希望如果存在專屬版本就調用它;不存在就用預設的 std::swap 吧:

template<typename T>
void doSomething(T& obj1, T& obj2)
{
    using std::swap;  //令std::swap在此函數內可用
    ...
    swap(obj1,obj2);  //為T型調用最佳swap版本
    ...  
}

之後C++會在global域和T所在namespace里搜索可能存在的T專屬版swap,若沒有則調用預設 std::swap

這裡有一個小trick,如果你這麼寫: std::swap(obj1, obj2); ,語意會截然不同,這相當於強迫使用std內的swap ————— 你get到了嗎,這就是我們要寫特化std::swap 的動機!這使得類型專屬的swap實現也能被這些蠢代碼所用。


OVER


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

-Advertisement-
Play Games
更多相關文章
  • 使用繼承時,基類必須保證存在預設構造器(無參構造器),子類在實例化時,會首先自動調用隱式調用父類的無參構造器,允許子類與父類的構造器列表不一致,子類使用有參構造器實例化對象時,最好顯式調用父類構造器,防止出錯。 ...
  • 每一個class位元組碼文件都唯一對應一個類或介面,class文件中記錄中類或介面的基本信息,但反之不成立,不是每一個類或介面都有一個唯一對應的位元組碼文件,首先類或介面的位元組碼可以不以文件的方式存儲,可以直接從記憶體中生成位元組碼,而不產生.class文件,動態代理的原理就是直接記憶體中生成位元組碼流,根據加 ...
  • 簡介 抖音,是一款可以拍短視頻的音樂創意短視頻社交軟體,該軟體於2016年9月上線,是一個專註年輕人的15秒音樂短視頻社區。用戶可以通過這款軟體選擇歌曲,拍攝15秒的音樂短視頻,形成自己的作品。此APP已在Android各大應用商店和APP Store均有上線。 今天咱們就用Python爬取抖音視頻 ...
  • 一.列表及元組 1.首先我們先來看一下列表: 列表是我們最常用的數據類型之一,通過列表可以對數據實現最方便的存儲、修改等操作 創建類表的兩種方式: (1.)l1=[1,2,3,4] (2.)l1=list((1,2,3,4)) (1.)和(2.)是等價的都是創建列表的方式 列表常用的方法: 我們可以 ...
  • 題目: 給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。 設計一個演算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。 註意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。 示例 1: 輸入: [7,1,5,3,6,4] 輸出: 7 解釋 ...
  • 一:Hash結構集合 Hash結構的特點是無序和唯一,無序即添加元素的順序和輸出元素的順序不一致,唯一是指元素不重覆。那是什麼來保證Hash結構元素唯一的呢? 元素所在類的HashCode()和equals()方法來保證元素的唯一性的,所以自定義的類用Hash結構集合存儲元素時,需要重寫這兩個方法。 ...
  • 登錄百度 先清理瀏覽器緩存,打開Charles,登錄一次百度主頁,抓取到登錄過程。 參數分析 確定了需要分析的參數,從哪個開始分析呢?隨意吧 一般有些參數之間是有關係的,比如token的請求參數里需要gid參數 這裡我就不一一去分析參數間的關係了,直接來了啊 參數 gid 一方面其他參數需要它,另外 ...
  • 資料庫就是存儲數據的倉庫,其本質是一個文件系統,數據按照特定的格式將數據存儲起來,用戶可以對資料庫中的數據進行增加,修改,刪除及查詢操作。 mysql的dos視窗啟動關閉命令:net start mysql和net stop mysql 登錄命令:(1)mysql -u用戶名 -p密碼 (2)mys ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...