C++避坑指南

2022-02-07 騰訊技術工程

 

導語:如果,將程式語言比作武功秘籍,C++無異於《九陰真經》。《九陰真經》威力強大、博大精深,經中所載內功、輕功、拳、掌、腿、刀法、劍法、杖法、鞭法、指爪、點穴密技、療傷法門、閉氣神功、移魂大法等等,無所不包,C++亦如是。

C++跟《九陰真經》一樣,如果使用不當,很容易落得跟周芷若、歐陽鋒、梅超風等一樣走火入魔。這篇文章總結了在學習C++過程中容易走火入魔的一些知識點。為了避免篇幅浪費,太常見的誤區(如指針和數組、重載、覆蓋、隱藏等)在本文沒有列出,文中的知識點也沒有前後依賴關係,各個知識點基本是互相獨立,並沒有做什麼鋪墊,開門見山直接進入正文。

目錄

1 函數聲明和對象定義

2 靜態對象初始化順序

3 類型轉換

3.1 隱式轉換

3.2 顯示轉換

4 inline內聯

5 名稱查找

5.1 受限名稱查找

5.2 非受限名稱查找

6 智能指針

6.1 std::auto_ptr

6.2 std::shared_ptr

6.3 std::unique_ptr

7 lambda表達式

對象定義寫成空的初始化列表時,會被解析成一個函數聲明。可以採用代碼中的幾種方法定義一個對象。

//這是一個函數聲明//不是一個對象定義string foo();

//函數聲明string foo(void);

//對象定義幾種方法string foo;string foo{ };string *foo = new string;string *foo = new string();string *foo = new string{ };

(左滑可以查看全部代碼,下同)

在同一個編譯單元中,靜態對象的初始化次序與其定義順序保持一致。對於作用域為多個編譯單元的靜態對象,不能保證其初始化次序。如下代碼中,在x.cpp和y.cpp分別定義了變量x和y,並且雙方互相依賴。

//x.cppextern int y;int x = y + 1;

x.cpp中使用變量y來初始化x

//y.cppextern int x;int y = x + 1;

y.cpp中使變量x來初始化y

//main.cppextern int x;extern int y;int main(){    cout << "x = " << x << endl;    cout << "y = " << y << endl;    return 0;}

如果初始化順序不一樣,兩次執行的結果輸出不一樣,如下所示:

g++ main.cpp x.cpp y.cpp./a.out x = 1y = 2

g++ main.cpp y.cpp x.cpp./a.out x = 2y = 1

如果我們需要指定依賴關係,比如y依賴x進行初始化,可以利用這樣一個特性來實現:函數內部的靜態對象在函數第一次調用時初始化,且只被初始化一次。使用該方法,訪問靜態對象的唯一途徑就是調用該函數。改寫後代碼如下所示:

//x.hextern int &getX();//x.cppint &getX(){    static int x;    return x;}

getX()函數返回x對象

//y.hextern int &getY();//y.cpp#include "x.h"int &getY(){    static int y = getX() + 1;    return y;}

y對象使用x對象進行初始化

//main.cppint main(){    cout << "x = " << getX() << endl;    cout << "y = " << getY() << endl;    return 0;}

列印x和y值。通過這種方式,就保證了x和y的初始化順序。

g++ main.cpp x.cpp y.cpp./a.out x = 0y = 1

g++ main.cpp y.cpp x.cpp./a.out x = 0y = 1

這裡只描述自定義類的類型轉換,不涉及如算數運算的類型自動提升等。

3.1 隱式轉換

C++自定義類型在以下兩種情況會發生隱式轉換:

1) 類構造函數只有一個參數或除第一個參數外其他參數有默認值;
2) 類實現了operator type()函數;

class Integer{public:    Integer() : m_value(0) {  }        Integer(int value) : m_value(value)    {        cout << "Integer(int)" << endl;    }

    operator int()    { cout << "operator int()" << endl; return m_value;     }    private: int m_value;};

上面定義了一個Integer類,Integer(int)構造函數可以將int隱式轉換為Integer類型。operator int()函數可以將Integer類型隱式轉換為int。從下面代碼和輸出中可以看出確實發生了隱式的類型轉換。

int main(){    Integer value1;    value1 = 10;    cout << "value1=" << value1 << endl;    cout << "*******************" << endl;    int value2 = value1;    cout << "value2=" << value2 << endl;    return 0;}

output:Integer(int)operator int()value1=10*******************operator int()value2=10

隱式類型轉換在某些場景中確實比較方便,如:

a、運算符重載中的轉換,如可以方便的使Integer類型和內置int類型進行運算

const Integer operator+(const Ingeter &lhs, const Ingeter &rhs){    return Integer(lhs.m_value + rhs.m_value);}Integer value = 10;Integer sum = value + 20;

b、條件和邏輯運算符中的轉換,如可以使智能指針像原生裸指針一樣進行條件判斷

template<typename T>class AutoPtr{public:    operator bool() const { return m_ptr; }private:    T *m_ptr;};AutoPtr<int> ptr(new int(10));if(ptr){    }

隱式類型轉換在帶來便利性的同時也帶來了一些坑,如下所示:

template<typename T>class Array{public:    Array(int size);    const T &operator[] (int index);    friend bool operator==(const Array<T> &lhs, const Array<T> &rhs);};Array<int> arr1(10);Array<int> arr2(10);if(arr1 == arr2[0]){    }

構造函數隱式轉換帶來的坑。上述代碼定義了一個Array類,並重載了operator==運算符。本意是想比較兩個數組,但是if(arr1 == arr2)誤寫成了f(arr1 == arr2[0]),編譯器不會抱怨,arr2[0]會轉換成一個臨時Array對象然後進行比較。

class String{public:    String(const char *str);    operator const char* () const{ return m_data; }private:    char *m_data;};const char *strcat(const char *str1, const char *str2){    String str(str1);    str.append(str2);    return str;}

operator type()帶來的坑。上述String類存在到const char *的隱式轉換,strcat函數返回時String隱身轉換成const char *,而String對象已經被銷毀,返回的const char *指向無效的內存區域。這也是std::string不提提供const char *隱式轉換而專門提供了c_str()函數顯示轉換的原因。

3.2 顯示轉換

正是由於隱式轉換存在的坑,C++提供explicit關鍵字來阻止隱式轉換,只能進行顯示轉換,分別作用域構造函數和operator(),如下所示:

1) explicit Ctor(const type &);
2) explicit operator type();

class Integer{public:    Integer() : m_value(0) {  }        explicit Integer(int value) : m_value(value)    {        cout << "Integer(int)" << endl;    }        explicit operator int()    {        cout << "operator int()" << endl;        return m_value;    }private:    int m_value;};

用explicit改寫Integer類後,需要進行顯示轉換才能與int進行運算,如下:

int main(){    Integer value1;        value1 = static_cast<Integer>(10);    cout << "value1=" << (int)value1 << endl;

int value2 = static_cast<int>(value1); cout << "value2=" << value2 << endl; return 0;}

為了保持易用性,C++11中explicit operator type()在條件運算中,可以進行隱式轉換,這就是為什麼C++中的智能指針如shared_ptr的operator bool()加了explicit還能直接進行條件判斷的原因。下面代碼來自shared_ptr源碼。

explicit operator bool() const _NOEXCEPT{ return (get() != nullptr);}

內聯類似於宏定義,在調用處直接展開被調函數,以此來代替函數調用,在消除宏定義的缺點的同時又保留了其優點。內聯有以下幾個主要特點:

a、內聯可以發生在任何時機,包括編譯期、連結期、運行時等;


b、編譯器很無情,即使你加了inline,它也可能拒絕你的inline;


c、編譯器很多情,即使你沒有加inline,它也可能幫你實施inline;


d、不合理的inline會導致代碼臃腫。

使用內聯時,需要注意以下幾個方面的誤區:

1)inline函數需顯示定義,不能僅在聲明時使用inline。類內實現的成員函數是inline的。

inline int add(int, int);

int add(int x, int y) { return x + y;}class Calculator{public:    static int add(int x, int y)     { return x + y; } static int sub(int x, int y);};int Calculator::sub(int x, int y) { return x - y;}

2)通過函數指針對inline函數進行調用時,編譯器有可能不實施inline

inline int add(int x, int y){    return x + y;}//定義函數指針int (*pfun)(int, int) = add;

add(3, 5); pfun(3, 5);

3)編譯器可能會拒絕內聯虛函數,但可以靜態確定的虛函數對象,多數編譯器可以inline

class Animal{public:    virtual void walk() = 0;};clas Penguin : public Animal{public:    virtual void walk(){ }}Animal *p1 = new Penguin();p1->walk(); 

Penguin *p2 = new Penguin();p2->walk();

4)inline函數有局部靜態變量時,可能無法內聯

inline void report(int code){    static int counter = 0;    doReport(code, ++counter);}report(-9998);

5)直接遞歸無法inline,應轉換成迭代或者尾遞歸。下面分別以遞歸和迭代實現了二分查找。

template<typename T>inline int recursionSearch(const vector<T> &vec, const T &val, int low, int high){    if(low > high) return -1;    int mid = (low + high) / 2;    if(val < vec[mid])    {        return recursionSearch(vec, val, low, mid - 1);    }    else if(val > vec[mid])    {        return recursionSearch(vec, val, mid + 1, high);    }    else    {        return mid;    }}

二分查找的遞歸方式實現。

template<typename T>inline int iterationSearch(const vector<T> &vec, const T &val){    int low = 0;    int high = vec.size() - 1;    while(low <= high)    {        int mid = (low + high) / 2;        if(val < vec[mid])        {            high = mid -1;        }        else if(val > vec[mid])        {            low = mid +1;        }        else        {            return mid;        }    }    return -1;}

二分查找的迭代方式實現。

分別調用二分查找的遞歸和迭代實現,開啟-O1優化,通過查看彙編代碼和nm查看可執行文件可執行文件符號,只看到了遞歸版本的call指令和函數名符號,說明遞歸版本沒有內聯,而迭代版本實施了內聯展開。

6)構造函數和析構函數可能無法inline,即使函數體很簡單

class Student : public Person{public:    Student() {  };    virtual ~Student() {  }private:    School m_school;};

Student::Student(){ try { Person::Person(); } catch(...) { throw; } try { m_school.School::School(); } catch(...) { Person::~Person(); throw; }}

表面上構造函數定義為空且是inline,但編譯器實際會生成如右側的偽代碼來構造基類成分和成員變量,從而不一定能實施inline。

C++中名稱主要分為以下幾類:

a) 受限型名稱:使用作用域運算符(::)或成員訪問運算符(.和->)修飾的名稱。
如:::std、std::sort、penguin.name、this->foo等。


b) 非受限型名稱:除了受限型名稱之外的名稱。
如:name、foo


c) 依賴型名稱:依賴於形參的名稱。
如:vector<T>::iterator


d) 非依賴型名稱:不屬於依賴型名稱的名稱。
如:vector<int>::iterator

5.1 受限名稱查找

受限名稱查找是在一個受限作用域進行的,查找作用域由限定的構造對象決定,如果查找作用域是類,則查找範圍可以到達基類。

class B{public:    int m_i;};class D : public B{}void foo(D *pd){    pd->m_i = 0; }

5.2 非受限名稱查找

5.2.1 普通查找:由內向外逐層查找,存在繼承體系時,先查找該類,然後查找基類作用域,最後才逐層查找外圍作用域

extern string name;  string getName(const string &name) {    if(name.empty())    {        string name = "DefaultName";         return getName(name);     }    return name + ::name;}

5.2.2 ADL(argument-dependent lookup)查找:又稱koenig查找,由C++標準委員會Andrew Koenig定義了該規則——如果名稱後面的括號裡提供了一個或多個類類型的實參,那麼在名稱查找時,ADL將會查找實參關聯的類和命名空間。

namespace ns{    class C{  };    void foo(const C &c)    {         cout << "foo(const C &)" << endl;     }}int main(){    ns::C c;    foo(c);    return 0;}

根據類型C的實參c,ADL查找到C的命名空間ns,找到了foo的定義。

了解了ADL,現在來看個例子,下面代碼定義了一個Integer類和重載了operator<運算符,並進行一個序列排序。

namespace ns{    class Integer    {     public:        explicit Integer(int value) : m_value(value){  }        int m_value = 0;     };}bool operator<(const Integer &lhs, const Integer &rhs){    return lhs.m_value < rhs.m_value;}int main(){    using ns::Integer;    std::vector<Integer> v = {Integer(1), Integer(5), Integer(1), Integer(10)};    std::sort(v.begin(), v.end());    for(auto const &item : v)    {        std::cout << item.m_value << " ";    }    std::cout << std::endl;    return 0;}

上面的代碼輸出什麼? 1 1 5 10嗎。上面的代碼無法編譯通過,提示如下錯誤

/usr/include/c++/4.8.2/bits/stl_heap.h:235:35: 錯誤:no match for 『operator<』 (operand types are 『ns::Integer』 and 『ns::Integer』)  if (*(__first + __secondChild) < *(__first + (__secondChild - 1)))

operator<明明在全局作用於有定義,為什麼找不到匹配的函數?前面的代碼片段,應用ADL在ns內找不到自定義的operator<的定義,接著編譯器從最近的作用域std內開始向外查找,編譯器在std內找到了operator<的定義,於是停止查找。定義域全局作用域的operator<被隱藏了,即名字隱藏。名字隱藏同樣可以發生在基類和子類中。好的實踐:定義一個類時,應當將其相關的接口(包括自由函數)也放入到與類相同的命名空間中

namespace ns{    class Integer    {    public:        explicit Integer(int value) : m_value(value){  }

int m_value = 0; }; bool operator<(const Integer &lhs, const Integer &rhs) { return lhs.m_value < rhs.m_value; }}

把operator<定義移到ns命名空間後運行結果正常

再來看一個名稱查找的例子。

template<typename U>struct B{    typedef int T;};template<typename T>struct D1 : B<int>{    T m_value;};int main(){    int value = 10;    D1<int *> d1;    d1.m_value = &value;    cout << *d1.m_value << endl;    return 0;}

這段代碼編譯時提示如下錯誤,我們用int *實例化D1的模板參數並給m_value賦值,編譯器提示無法將int *轉換成int類型,也就是m_value被實例化成了int而不是int *。

我們將代碼改動一下,將D2繼承B<int>改為B<T>,代碼可以順利編譯並輸出。

template<typename U>struct B{    typedef int T;};template<typename T>struct D2 : B<T>{    T m_value;};int main(){    int value = 10;    D2<int *> d2;    d2.m_value = &value;    cout << *d2.m_value << endl;

return 0;}

D1和D2唯一的區別就是D1繼承自B<int>,D2繼承自B<T>。實例化後,為何D1.m_value類型是int,而D2.m_value類型是int *。在分布式事務領域有二階段提交,在並發編程設計模式中二階段終止模式。在C++名稱查找中也存在一個二階段查找。

二階段查找(two-phase lookup):首次看到模板定義的時候,進行第一次查找非依賴型名稱。當實例化模板的時候,進行第二次查找依賴型名稱。


D1中查找T時,基類B<int>是非依賴型名稱,無需知道模板實參就確定了T的類型。


D2中查找T時,基類B<T>是依賴型名稱,在實例化的時候才會進行查找。

6.1 std::auto_ptr

std::auto_ptr是C++98智能指針實現,複製auto_ptr時會轉移所有權給目標對象,導致原對象會被修改,因此不具備真正的複製語義,不能將其放置到標準容器中。auto_ptr在c++11中已經被標明棄用,在c++17中被移除。

auto_ptr<string> ap1(new string("foo"));auto_ptr<string> ap2 = ap1;//內存訪問錯誤,ap1管理的指針已經被置位空string str(*ap1);

auto_ptr<string> ap3(new string("bar"));vector<auto_ptr<string>> ptrList;//ap2和ap3被複製進容器後其管理的指針對象為空//違反標準c++容器複製語義ptrList.push_back(ap2);ptrList.push_back(ap3);

6.2 std::shared_ptr

std::shared_ptr採用引用計數共享指針指向對象所有權的智能指針,支持複製語義。每次發生複製行為時會遞增引用計數,當引用計數遞減至0時其管理的對象資源會被釋放。但shared_ptr也存在以下幾個應用方面的陷阱。

1)勿通過傳遞裸指針構造share_ptr

{    string *strPtr = new string("dummy");    shared_ptr<string> sp1(strPtr);    shared_ptr<string> sp2(strPtr);}

這段代碼通過一個裸指針構造了兩個shared_ptr,這兩個shared_ptr有著各自不同的引用計數,導致原始指針被釋放兩次,引發未定義行為。

2)勿直接將this指針構造shared_ptr對象

class Object{public:    shared_ptr<Object> GetSelfPtr()    {        return shared_ptr<Object>(this);    }};shared_ptr<Object> sp1(new Object());shared_ptr<Object> sp2 = sp1->GetSelfPtr();

這段代碼使用同一個this指針構造了兩個沒有關係的shared_ptr,在離開作用域時導致重複析構問題,和1)是一個道理。當希望安全的將this指針託管到shared_ptr時,目標對象類需要繼承std::enable_shared_from_this<T>模板類並使用其成員函數shared_from_this()來獲得this指針的shared_ptr對象。如下所示:

class Object : public std::enable_shared_from_this<Object>{public:    shared_ptr<Object> GetSelfPtr()    {        return shared_from_this();    }};shared_ptr<Object> sp1(new Object());shared_ptr<Object> sp2 = sp1->GetSelfPtr();

3)請勿直接使用shared_ptr互相循環引用,如實在需要請將任意一方改為weak_ptr。

struct You;struct I{    shared_ptr<You> you;    ~I() { cout << "i jump" << endl; }};struct You{    shared_ptr<I> me;    ~You() { cout << "you jump" << endl; }};int main(){    shared_ptr<I> i(new I());    shared_ptr<You> you(new You());    i->you = you;    you->me = i;    return 0;}

代碼運行結果,沒有看到列印任何內容,析構函數沒有被調用。最終你我都沒有jump,完美的結局。但是現實就是這麼殘酷,C++的世界不允許他們不jump,需要將其中一個shared_ptr改為weak_ptr後資源才能正常釋放。

struct You{    weak_ptr<I> me;    ~You() { cout << "you jump" << endl; }};int main(){    shared_ptr<I> i(new I());    shared_ptr<You> you(new You());    i->you = you;    you->me = i;    return 0;}

4)優先使用make_shared而非直接構造shared_ptr。make_shared主要有以下幾個優點:

a、可以使用auto自動類型推導。

shared_ptr<Object> sp(new Object());

auto sp = make_shared<Object>();

 b、減少內存管理器調用次數。shared_ptr的內存結構如下圖所示,包含了兩個指針:一個指向其所指的對象,一個指向控制塊內存。

shared_ptr<Object> sp(new Object());

 這條語句會調用兩次內存管理器,一次用於創建Object對象,一次用於創建控制塊。如果使用make_shared會一次性分配內存同時保存Object和控制塊。

c、防止內存洩漏。

class Handler;string getData();int handle(shared_ptr<Handler> sp, const string &data);handle(shared_ptr<Handler>(new Handler()), getData());

這段代碼可能發生內存洩漏。一般情況下,這段代碼的調用順序如下:

new Handler()     ① 在堆上創建Handler對象

shared_ptr()        ②創建shared_ptr

getData()             ③調用getData()函數

但是編譯器可能不按照上述①②③的順序來生成調用代碼。可能產生①③②的順序,此時如果③getData()產生異常,而new Handler對象指針還沒有託管到shared_ptr中,於是內存洩漏發生。使用make_shared可以避免這個問題。

handle(make_shared<Handler>(), getData());

這條語句在運行期,make_shared和getData肯定有一個會先調用。如果make_shared先調用,在getData被調用前動態分配的Hander對象已經被安全的存儲在返回的shared_ptr對象中,接著即使getData產生了異常shared_ptr析構函數也能正常釋放Handler指針對象。如果getData先調用並產生了異常,make_shared則不會被調用。

但是make_shared並不是萬能的,如不能指定自定義刪除器,此時可以先創建shared_ptr對象再傳遞到函數中。

shared_ptr<Handler> sp(new Handler());handle(sp, getData());

6.3 std::unique_ptr

std::unique_ptr是獨佔型智能指針,僅支持移動語義,不支持複製。默認情況下,unique_ptr有著幾乎和裸指針一樣的內存開銷和指令開銷,可以替代使用裸指針低開銷的場景。

1)與shared_ptr不同,unique_ptr可以直接指向一個數組,因為unique_ptr對T[]類型進行了特化。如果shared_ptr指向一個數組,需要顯示指定刪除器。

unique_ptr<T []> ptr(new T[10]);//顯示指定數組刪除器shared_ptr<T> ptr(new T[10], [](T *p){delete[] p;});

 2)與shared_ptr不同,unique_ptr指定刪除器時需要顯示指定刪除器的類型。

shared_ptr<FILE> pf(fopen("data.txt", "w"), ::fclose);unique_ptr<FILE, int(*)(FILE *)> pf(fopen("data.txt", "w"), ::fclose);unique_ptr<FILE, std::function<int(FILE *)>> pf(fopen("data.txt", "w"), ::fclose);

1)捕獲了變量的lambda表達式無法轉換為函數指針。

using FunPtr = void(*)(int *);FunPtr ptr1 = [](int *p) { delete p; };FunPtr ptr2 = [&](int *p) { delete p; };//錯誤


2)對於按值捕獲的變量,其值在捕獲的時候就已經確定了(被複製到lambda閉包中)。而對於按引用捕獲的變量,其傳遞的值等於lamdba調用時的值。

int a = 10;auto valLambda = [a] { return a + 10; };auto refLambda = [&a] { return a + 10; };cout << "valLambda result:" << valLambda() << endl; cout << "refLambda result:" << refLambda() << endl; a += 10;cout << "valLambda result:" << valLambda() << endl; cout << "refLambda result:" << refLambda() << endl; 

3)默認情況下,lambda無法修改按值捕獲的變量。如果需要修改,需要使用mutable顯示修飾。這其實也好理解,lambda會被編譯器轉換成operator() const的函數對象。

auto mutableLambda = [a]() mutable { return a += 10; };

4)lambda無法捕捉靜態存儲的變量。

static int a = 10;auto valLambda = [a] { return a + 10; }; 

相關焦點

  • Google C++項目編程風格指南 (中文版) 分享
    因此今天分享一下Google開源的編程風格指南。源GitHub項目:https://github.com/google/styleguide中文翻譯:https://github.com/zh-google-styleguide/zh-google-styleguide如果下載有問題,可以在後臺回覆:「c++
  • 定製櫥櫃避坑指南,這樣設計省心又省錢!
    廚房裝修要是不順心,我猜你一定不想下廚房,送你一套定製櫥櫃避坑指南,省錢又省心的設計技巧你不想要嗎?定製櫥櫃避坑指南,這樣設計省心又省錢!你學會了嗎,定製家具都是有方法的,不要悶著自己想,可以多找一些案例看看,避坑我們是認真的。
  • 大一新生,《大學避坑+升級指南2020版》請收下
    大一新生,《大學避坑+升級指南2020版》請收下 > 學校陸續開學 各位萌新們即將進入新學校 因為疫情 今年父母可能不能陪大家進入校園 小編特地請來重量級嘉賓 為大家親身演示 《大學避坑指南
  • 實用的育兒避坑指南,讓你省時省力省錢
    育兒避坑指南——NO.1 衣是不是家裡小孩衣服太多,自己買的,親友送的,有些還來不及穿,衣服就小了?因為小孩長的太快了,尤其是前六個月內,一個碼的衣服穿不了一個月。家有小公主的,更是控制不住買買買的心。但是留在家裡又很佔空間,畢竟,房價不便宜。那該怎麼辦?
  • 哺乳期寶媽飲食避坑指南
    哺乳期寶媽飲食避坑指南酒精趕緊收下這份避坑指南吧。
  • 全屋定製衣櫃避坑指南,趕快收好!
    現在大家為了生活品質和裝修效果,裝修新家的時候很多人都會選擇全屋定製,既好用又美觀,還能量身定製自己喜歡的風格和款式,很多小夥伴沒有裝修經驗擔心自己會踩坑,今天小編就給大家分享關於定製衣櫃的避坑指南,快來學習!
  • 家庭英語分級閱讀避坑指南
    很多家長會留言問我們一些刷英語分級讀物的疑惑,在和家長的交流和溝通中,我們發現了幾個很多家長都會踩到的坑
  • 大一新生入學避坑指南
    大一新生入學避坑指南為了讓大家都能開開心心上大學,小編整理了一些開學注意事項
  • @大一新生,《大學避坑+升級指南2020版》請收下
    漫畫來源:吾皇萬睡學校陸續開學各位萌新們即將進入新學校因為疫情今年父母可能不能陪大家進入校園小編特地請來重量級嘉賓為大家親身演示《大學避坑指南
  • 報志願如何「避坑」?快收好這份高考志願填報指南
    報志願如何「避坑」?怎樣科學選擇心儀大學?選專業要注意些什麼?快收好這份高考志願填報指南!
  • VLOOKUP函數避坑指南
    例如隨便以財務為檢索詞搜到的一則崗位需求:但實際上新手在使用VLOOKUP時候往往會出現許多意想不到的錯誤,因此表哥給大家總結出了下面這些避坑指南。
  • 20英語避坑指南:這些「坑」一不留神你準掉!
    20英語避坑指南:這些坑一不留神你準掉!摘要:不知不覺,2019年暑假已經開始,自習室的考研黨們在英語這塊兒是在刷閱讀?刷閱讀?還是刷閱   2020考研英語避坑指南:這些「坑」一不留神你準掉
  • 避坑!千萬不要這樣做日本藝術留學作品集!
    今天和硯藝術留學小編就給大家盤點一波日本藝術留學作品集的正確姿勢,大家一定要避坑!【避坑指南1】千萬不要以為進了美術類語言學校就萬事大吉語言學校最主要的目的是學語言,相比最後拿籤證呆在日本其實後者明顯更重要一些,拿籤證呆日本可以擁有參加學校開放日和與教授面談的機會。
  • 河南省內自駕遊蠢蠢欲動的你,含攻略,花三分鐘看完這個避坑指南
    如果開始對城市疲倦,對光怪陸離都不再新鮮,嚮往天然的風景清新的空氣,那麼我們可以在周末安排一場自駕遊了,如果你對自駕遊蠢蠢欲動躍躍欲試,歡迎花三分鐘看完這個避坑指南,是我們多次國內外自駕遊之後總結出來的經驗,相信可以幫你少走冤枉路,省下不少冤枉錢!
  • 安卓源碼避坑指南3——撥打電話的SIM卡無效導致藍牙斷開連接
    撥打電話的SIM卡無效導致藍牙斷連它來了、它來了,它帶著BUG趕來了,歡迎大家查看本期的安卓源碼避坑指南。本期的問題場景比較特殊,電話SIM卡是無效的(欠費過期了,很是貧窮…)。本期是安卓源碼避坑指南系列的第三篇文章了,想了解更多安卓源碼bug的同學可以翻看以前的文章,也歡迎感興趣的小夥伴私信留言共同學習。
  • 值無不言:關於機械鍵盤的購買避坑指南,讓我用這篇告訴你
    本期話題:關於機械鍵盤的購買避坑指南。 不同鍵盤的敲擊手感有什麼分別? 購買機械鍵盤都有哪些坑? 什麼尺寸的機械鍵盤是最適合自己的? ......
  • 超級全的大一新生入學避坑指南,一定要避開這些坑
    開學季微博話題#大一新生入學避坑指南#熱搜持續居高不下各網友紛紛談起了當年踩的那些「坑」為了讓大家都能開開心心上大學小編在整理收集了很多開學注意事項經綜合整理,強烈推出《10.0版本的新生入學「避坑」指南》快接好了!
  • 育兒避坑指南——前言
    可是人生沒有後悔藥,我願意將經驗分享給大家,教你避坑,教你育兒。 我主要寫寶寶的早期教育和餵養,穿插一些孕期和產後抑鬱症的問題。大家也可以留言說說你們的問題和最關注的內容,我會優先寫。
  • 育兒避坑指南,這份就夠了
    很多媽媽在寶寶還沒有出生之前就飽覽了各種囤貨指南最好的避坑辦法就是不要喝。溼疹:2歲之前的小孩是溼疹易發群體,溼疹也成了坑娃的一個大坑。很多媽媽在寶寶出現溼疹後,都習慣去問一些「過來人」,有溼疹擦什麼好得快。
  • 新生避坑、認識自我 這個心理學課程有創意
    9月11日上午,在深受學生喜愛的武鋼三中《新生「避坑」指南》校本課上,高一新生們在心理教師的引導下認識和了解自我,了解新的環境,樹立生涯規劃意識,學習心理調適的技能。  《新生「避坑」指南》是武鋼三中今年面向高一新生開設的心理學系列課程。