C++知識點雜談

C++知識點雜談

  • C++中的三種賦值方式:

    • 按值傳遞。一個參數的值會複製給另一個參數:

      int a, b;
      a = b;  
    • 按地址值傳遞。一個地址值複製給一個指針:

      int a;
      int *p;
      p = &a;
    • 按引用傳遞。不開闢新的內存,相當於給參數起個別名:

      int a;
      int& b = a;
  • C++的左值和右值:

    • 左值:可以被賦值,可以出現在賦值號的左邊或右邊。
    • 右值:不能被賦值,只能出現在賦值號的右邊。
  • C++的常量定義方式:

    • #define:#define identifier value,如:
        #define LENGTH 10   
        #define WIDTH  5
        #define NEWLINE '\n'
    • const : const type variable = value; 如:
        const int  LENGTH = 10;
        const int  WIDTH  = 5;
        const char NEWLINE = '\n';
  • cin>>、cin.get()、cin.getline()三者區別

    • cin>>:會忽略開始部分的空白符,從第一個非空白符開始讀取,直到下個空白符的前一個字符爲止。所以cin>>總會在輸入緩衝區殘留一個空白符。
    • cin.get(數組名,長度,結束符):碰到結束符時數組補上空字符’\0’,該結束符不會讀進數組中,會在輸入緩衝區中被保留下來。
    • cin.getline(數組名,長度,結束符):碰到結束符時數組補上空字符’\0’,該結束符不會讀進數組中,會在輸入緩衝區中被捨棄。
  • C++的臨時對象是由編譯器自動創建並自動銷燬的,它們並不在代碼中出現,更沒有名字。臨時對象的生存期:
    1、當表達式中存在臨時對象時,直到該表達式的結果被用來賦值給另一個變量爲止,臨時對象纔會被銷燬。

string s = "hello ";
string t = "world!";
string v = s + t;//s+t的結果保存在一個臨時對象string中,該臨時對象沒有名字。在用臨時對象初始化了v之後,該臨時對象才被銷燬。

2、將臨時對象初始化一個指針或者常量引用。

string s = string("hello");//這裏調用了string(const char* s)構造函數,用指針s指向了臨時對象“hello”。注意即使指針不是常量指針也可以。

const string &v = "hello";//這裏用臨時對象初始化了一個常量引用。注意,這裏的v必須是常量才行。

//這兩種方法相當於給臨時對象起了一個名字,該臨時對象的生存期就延遲至變量名的作用域結束爲止
  • C++中接口與實現分離
    C++中接口與實現分離最典型的情況就是在頭文件中聲明接口,在源文件中實現接口。這時實現接口的源文件必須在開頭include頭文件才行,然後在另外一個要調用接口的源文件中include頭文件,最後調用接口。所以接口和實現分離的本質就是在一個源文件中既要聲明接口又要實現接口,在另外一個要調用接口的源文件中必須先聲明接口才能調用接口,這時就會調用到接口的實現代碼了。
  • IO類的對象的引用一般都是普通引用,而不能是常量引用。因爲讀取和寫入操作會改變流的內容,所以對流的引用都是非常量引用。
  • 常量的對象、常量指針、常量引用都只能調用常量函數
  • 類內初始值,即在類中聲明一個成員變量的同時爲該成員變量賦上初始值。當我們提供一個類內初始值時,必須以符號=或者花括號表示。這裏着重強調爲只帶含參構造函數的類類型成員賦上初始值的情況。
class Player {
    public:
        Player(int type);
        virtual ~Player();
    private:
        int type;
}
class Game {
    public:
        Game();
        virtual ~Game();
    private:
        Player playerA = Player(0);//#1 正確
        Player playerB{1};//#2 正確
        //Player playerC(2);#3 錯誤,因爲編譯器會將其視爲函數聲明,其中Player被看成返回值,playerC被看成函數名,2被看成參數,顯然這樣的參數聲明有誤。所以編譯器認爲這樣的函數聲明,即Player playerC(2),是有問題的。
}

//也可以使用構造函數的初始化列表
Game::Game():playerA(0), playerB(1) {}//#4 正確
  • 一個const成員函數如果以引用的形式返回*this,那麼它的返回類型將是常量引用。
  • 類類型只被聲明,未被定義的話,那麼該類型的用途很有限,只能完成以下2件事:
    1、聲明指向這種類型的指針和引用
    2、可以聲明(但不可以定義)該類型作爲參數或返回類型的函數
    因爲只有當類全部完成後類纔算是被定義了,所以一個類的成員類型不能是該類自己。然而,一旦類名出現後,就認爲該類被聲明過了,所以類允許包含指向它自身類型的引用或指針。
class A {
    A a1; //#1 錯誤
    A& a2; //#2 正確
    A* a3; //#3 正確
    static A a4; //#4 正確
}
  • 當成員函數定義在類的外部時,若返回類型是某個類的類型成員,則需要單獨爲該返回類型指明它是哪個類的成員
class Screen;
class Window {
    public:
        using ScreenIndex = std::vector<Screen>::size_type;
        ScreenIndex addScreen(const Screen&);
}
//#1 下面的代碼是錯誤的,因爲“Window::”位於返回類型ScreenIndex的後面,
//編譯器不知道ScreenIndex是哪個類的
ScreenIndex Window::addScreen(const Screen& s) {
    //...
}
//#2 下面的代碼纔是對的
Window::ScreenIndex Window::addScreen(const Screen& s) {
    //...
}
  • 構造函數體一開始執行時,成員就已經初始化完成了。如果成員是const、引用,或者屬於某種未提供默認構造函數的類類型,我們必須通過構造函數初始值列表爲這些成員提供初值。
  • 在C++中被雙引號括起來的字符串(如:”Hello world!”),其實都是C風格的字符串,它們只能賦值給常量字符指針或者字符數組。
char greeting[] = "Hello world!"; // #1 正確
const char* p = "Hello world!"; // #2 正確
string str = "Hello world!"; //#3 正確,這裏實際上是隱式的類類型轉換,也就是隱式的調用了構造函數string (const char* s);
  • 靜態成員函數不能聲明成const,也不能在靜態成員函數體內使用this指針。
  • 靜態成員變量可以作爲成員函數的默認實參,而非靜態成員變量則不能。
  • 函數的返回類型不能是數組類型或者函數類型,但可以是指向數組或函數的指針。
  • 如果局部靜態變量沒有顯式的初始值,它將執行默認初始化。
  • 以內置數組作爲形參時的三種等價形式:
void print(const int*);
void print(const int[]);
void print(const int[10]);//這個維度表示我們期望數組含有多少元素,但實際不一定
//實參數組的大小對上面三種函數的調用沒有影響
  • 數組大小 = end(數組名) - begin(數組名)
  • 數組引用形參
void print(int (&arr)[10]);//&arr兩端的括號必不可少,arr是具有10個整數的整型數組的引用,它要求實參的數組大小必須爲10
  • 返回數組指針
typedef int arrT[10]; //arrT是一個類型別名,它表示的是一個含有10個整數的數組
using arrT = int[10]; //arrT的等價聲明
arrT* func(int i); //func返回一個指向含有10個整數的數組的指針

int arr[10]; //arr是一個含有10個整數的數組
int *p1[10]; //p1是一個含有10個指針的數組
int (*p2)[10] = &arr; //p2是一個指針,它指向含有10個整數的數組
  • main函數不能重載
  • const對函數重載的影響:
void do(A);
void do(const A); //重複聲明瞭void do(A);

void do(A*);
void do(A* const); //重複聲明瞭void do(A*);

void do(A&); //函數作用於引用
void do(const A&); //新函數,作用於常量引用

void do(A*); //函數作用於指針
void do(const A*); //新函數,作用於常量指針

當我們傳遞一個非常量對象或指向非常量對象的指針時,編譯器會優先選擇非常量版本的函數

  • 一旦某個函數形參被賦予了默認值,它後面的所有形參都必須有默認值
  • 函數的默認實參負責填補函數調用缺少的尾部實參。
string screen(int height = 24, int width = 80, char background = ' ');

string window;
window = screen(, , '?'); //#1 錯誤,只能省略尾部的實參
window = screen('?'); //#2 實際上調用的是screen('?', 80, ' ')

當函數有默認實參時,應該將不常用的默認值安排在函數前面,常用的默認值安排在函數的後面

  • 常量表達式是指值不會被改變並且在編譯時就能得到計算結果的表達式。
const int a = 10; // a是常量表達式
const int b = a + 1; //b是常量表達式
int c = 27; //c不是常量表達式,雖然c的初始值是常量,但是c是普通int型,c可以被改變
const int d = get_size(); // d不是常量表達式,儘管d本身是常量,但它的具體值要到運行時才能獲取得到
  • constexpr類型允許編譯器來驗證變量的值是否是一個常量表達式。聲明爲constexpr的變量一定是一個常量,而且必須用常量表達式初始化
  • constexpr函數是指能用於表示常量表達式的函數,constexpr不一定返回常量表達式
  • 內聯函數和constexpr函數通常定義在頭文件中
  • 指向不同函數類型的指針間是不存在轉換規則的,也就是說只有當形參和返回值精確匹配時才能給函數指針賦值
  • 函數類型只有在作爲形參時纔會被編譯器自動轉換爲指針類型。但作爲返回值時,編譯器不會自動轉換,所以必須顯式地加上*以表明返回的是指向函數的指針。
using F = int(int*, int); //F是函數類型,不是指針
using PF = int(*)(int*, int); //PF是指針類型

PF f1(int); //正確:PF是指向函數的指針,f1返回指向函數的指針
F f1(int); //錯誤:F是函數類型,f1不能返回一個函數
F* f1(int); // 正確:顯式指定返回類型是指向函數的指針
  • static_cast用於強制轉換A、B兩個不同的類型
int i, j;
double slope = static_cast<double>(j)/i; //#1 將int類型的j強制轉換成double類型

double d;
void* p = &d;
double *dp = static_cast<double*>(p); //#2 將void*轉換成初始的指針類型
  • const_cast添加或去除變量的const屬性,如果變量本來就是非常量的,則去除const屬性是合法的。但是如果變量本來是常量的,則去除const屬性就是非法的了。
//比較兩個string對象的長度,返回較短的那個引用
const string& shorterString(const string &s1, const string &s2) {
    return s1.size() <= s2.size() ? s1 : s2;
}

string& shorterString(string &s1, string &s2) {
    auto &r = shorterString(const_cast<const string&>(s1), const_cast<const string&>(s2)); //#1 這裏參數s1和s2是兩個非常量引用,首先將它們強制轉換成常量引用,接着調用const版的shorterString。
    //返回值r是常量引用,但r實際上是初始的非常量實參s1和s2中的一個。
    return const_cast<string&>(r); //#2 因爲r本來就是非常量引用,所以將r去除const屬性,是安全的。
}
  • 對數組執行sizeof運算得到整個數組所佔空間的大小,等價於對數組中所有的元素各執行一次sizeof運算並將所得結果求和。所以sizeof可以用來計算數組的大小。
int a[10];
constexpr size_t sz = sizeof(a)/sizeof(*a);
int b[sz]; //正確,sizeof返回一個常量表達式 
  • auto用來聲明變量時會忽略頂層的const而保留底層的const,用來聲明引用時會保留頂層的const
const int a = 7;
auto b = a;//#1 b是int
auto &c = a;//#2 c是const int&
auto *d = &a;//#3 d是const int*
  • decltype保留頂層const
const int a = 0, &b = a;
decltype(a) x = 0;//x類型是const int
decltype(b) y = x;//y類型是const int &
  • 如果decltype使用的表達式不是一個變量,則decltype返回表達式結果對應的類型。
int i = 1, *p = &i, &r = i;
decltype(r+0) b; //正確:加法的結果是int,因此b是一個未初始化的int
decltype(*p) c; //錯誤:c是int&,必須初始化
decltype((i)) d; //d是int&,必須初始化
//decltype((variable))(注意是雙括號)的結果永遠是引用
  • 類型修飾符(&和*)書寫時應該靠近變量
int* p;
int *p2;
//上面兩種寫法都正確,但是第一種容易引發誤導
int* p, p2;//這裏只有p是指針,而p2只是int
  • const始終只用來修飾緊跟在後面的修飾符或變量
int *const p1; //#1 const修飾p1,所以p1本身是常量,但指向的對象是int,非常量的
const int *p2; //#2 const修飾int,所以p2本身非常量,但指向的對象是const int, 常量的
const int *const p3; //#3 第一個const修飾int,第二個const修飾p3,所以p3本身是常量,指向的對象是const int,也是常量的
int a = 0;
const int b = 1;
int *p;
p1 = &a; //正確
p2 = &a;//正確
p2 = &b;//正確
p = p3;//錯誤,p3指向的對象是常量,但p指向的對象是非常量的,如果可以將p3賦值給p,那就意味着可以通過p改變p3所指向對象的值了
p2 = &a; //正確,int *能轉換成const int *
p2 = p3; //正確
p3 = p2; //正確
  • 合成的拷貝構造函數(即默認的拷貝構造函數)從給定的對象中依次將每個非static成員拷貝到正在創建的對象中。每個成員類型決定了它如何拷貝:對類類型的成員,會使用其拷貝構造函數來拷貝;內置類型的成員則直接拷貝。雖然我們不能直接拷貝一個數組,但合成拷貝構造函數會逐元素地拷貝一個數組類型的成員。如果,數組元素是類類型,則使用元素的拷貝構造函數來進行拷貝。
//等價於合成拷貝構造函數
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), //使用string的拷貝構造函數
units_sold(orig.units_sold),//拷貝orig.units_sold的int值
revenue(orig.revenue)//拷貝orig.revenue的double值
{}//空函數體
  • 重載賦值運算符通常應該返回一個指向其左側運算對象的引用。另外,標準庫要求容器中的元素類型要具有賦值運算符,且其返回值是左側運算對象的引用。
  • 合成拷貝賦值運算符(即默認的賦值運算符)會將右側運算對象的每個非static成員賦予左側運算對象的對應成員,這過程是通過成員類型的拷貝賦值運算符來完成的。對於數組類型的成員,逐個賦值數組元素。合成拷貝賦值運算符返回一個指向其左側運算對象的引用。
//等價於合成拷貝賦值運算符
Sales_data &Sales_data::operator=(const Sales_data &rhs) {
    bookNo = rhs.bookNo; //調用string::operator=
    units_sold = rhs.units_sold; //使用內置的int賦值
    revenue = rhs.revenue; //使用內置的double賦值
    return *this; //返回一個此對象的引用
} 
  • 拷貝構造函數和拷貝賦值運算符都是複製對象,但是拷貝構造函數是用於初始化一個對象,而拷貝賦值運算符則是給一個已經初始化了的對象賦值
  • 構造函數初始化對象的非static成員,析構函數銷燬對象的非static成員
  • 析構函數沒有返回值,也不接收參數。因爲不接收參數,所以析構函數不能被重載。對於給定的一個類,只有唯一的一個析構函數。
  • 如同構造函數有初始化部分和函數體,析構函數也有一個函數體和析構部分。在一個構造函數中,成員的初始化是在函數體執行之前完成的,且按照它們在類中出現的順序進行初始化。在一個析構函數中,先執行函數體,再銷燬成員,銷燬的順序是初始化的逆序。
  • 析構函數體自身不直接銷燬成員,成員是在析構函數體之後的隱式的析構階段中被銷燬的。銷燬類類型的成員時會執行成員自己的析構函數,銷燬內置類型的成員時什麼也不需要做。
  • 當指向一個對象的引用或指針離開作用域時,析構函數不會被執行。
  • 當一個類未定義自己的析構函數時,編譯器會爲它定義一個合成析構函數(類似合成構造函數,合成拷貝構造函數,合成拷貝賦值運算符),一般情況下,合成析構函數體就爲空。
//等價於合成析構函數
class Salas_data {
    public:
        ~Salas_data(){}
}
  • 如果需要爲一個類自定義一個析構函數,則必須爲它自定義拷貝構造函數和自定義拷貝賦值運算符,不可使用合成拷貝構造函數和合成拷貝賦值運算符。
class PStr {
    public:
        string *p;
        PStr(const string &s = string()):p() {new string(s)}
        ~PStr() {delete p;}
        //錯誤,需要自定義拷貝構造函數和自定義拷貝賦值運算符,
        //因爲合成拷貝構造函數和合成拷貝賦值運算符只會簡單的拷貝指針成員,
        //使得多個PStr對象的p指針指向了同一個對象
}

PStr f(PStr param) {//值傳遞,所以參數被拷貝
    PStr ret = param;//拷貝給定的PStr
    return ret;//param和ret被銷燬
}

PStr pStr("hello world");
f(pStr);//當f結束時,pStr.p指向內存被釋放
PStr q(pStr);//現在pStr和q都指向了無效內存
  • 一個類自定義了拷貝構造函數則也必須自定義拷貝賦值運算符,反之亦然。但該類未必需要自定義析構函數。
  • 我們只能對具有合成版的成員函數使用=default(即默認構造函數或拷貝控制成員)。
  • ==delete與==default的區別:
    1、==delete必須出現在函數第一次聲明的時候
    2、我們可以對任何函數指定==delete
  • 不能用==delete修飾析構函數,如果一個類型的析構函數是==delete的,則編譯器將不允許定義該類型的變量或臨時對象。如果一個成員的析構函數是==delete的,則該成員所在的類型也不會被編譯器允許定義變量和臨時對象。
  • 如果類的一個成員不能使用無參構造函數、拷貝、賦值或銷燬,則該類對應的成員函數若無顯式定義則編譯器會將這些函數的合成版設置成==delete。
  • 如果一個成員有刪除的(即==delete)或不可訪問的(如:是private的)析構函數,則該成員所在類的合成析構函數、合成構造函數和合成拷貝構造函數都是==delete的。
  • 若類具有的引用成員或const成員(該const成員的類型沒有無參構造函數)沒有類內初始化,則編譯器合成的構造函數是==delete的。若非如此,調用合成構造函數時,該引用成員和const成員將無法被初始化。
  • 若類具有const或引用成員,則編譯器的合成拷貝賦值運算符是==delete的,若非如此,在使用合成拷貝賦值運算符時就會將一個新值賦予一個已經初始化過的const或引用成員,顯然是不對的。
  • 如果一個函數永遠也不會被我們用到,那麼它可以只有聲明沒有定義。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章