C++學習筆記——003

不是公子的小白發表於2024-03-18
  • malloc() 函式在 C 語言中就出現了,在 C++ 中仍然存在,但建議儘量不要使用 malloc() 函式。new 與 malloc() 函式相比,其主要的優點是,new 不只是分配了記憶體,它還建立了物件。
//一維陣列動態分配,陣列長度為 m
int *array = new int [m];
 
//釋放記憶體
delete [] array;


//二維陣列
int **array;


// 假定陣列第一維長度為 m, 第二維長度為 n
// 動態分配空間
array = new int *[m];
for(int i = 0; i < m; ++i)
{
    array[i] = new int [n];
}


//釋放
for(int i = 0; i < m; ++i)
{
    delete [] array[i];
}
delete [] array;

  • new 和 malloc 內部的實現方式有什麼區別?new 的功能是在堆區新建一個物件,並返回該物件的指標。所謂的“新建物件”的意思就是,將呼叫該類的建構函式,因為如果不構造的話,就不能稱之為一個物件。而 malloc 只是機械的分配一塊記憶體,如果用 malloc 在堆區建立一個物件的話,是不會呼叫建構函式的。嚴格說來用 malloc 不能算是新建了一個物件,只能說是分配了一塊與該類物件匹配的記憶體而已,然後強行把它解釋為“這是一個物件”。同樣的,用 delete 去釋放一個堆區的物件,會呼叫該物件的解構函式。用 free 去釋放一個堆區的物件,不會呼叫該物件的解構函式。做個簡單的實驗即可明瞭:
#include <iostream>
#include <malloc.h>


class TEST
{
private:
    int num1;
    int num2;
    
public:
    TEST()
    {
        num1 = 10;
        num2 = 20;
    }
    void Print()
    {
        std::cout << num1 << " " << num2 << std::endl;
    }
};


int main(void)
{
    // 用malloc()函式在堆區分配一塊記憶體空間,然後用強制型別轉換將該塊記憶體空間
    // 解釋為是一個TEST類物件,這不會呼叫TEST的預設建構函式
    TEST * pObj1 = (TEST *)malloc(sizeof(TEST));
    pObj1->Print();


    // 用new在堆區建立一個TEST類的物件,這會呼叫TEST類的預設建構函式
    TEST * pObj2 = new TEST;
    pObj2->Print();


    return 0;
}


/*
執行結果:


-----------------------------
-842150451 -842150451       |
10 20                       |
請按任意鍵繼續. . .         |
-----------------------------
我們可以看到pObj1所指的物件中,欄位num1與num2都是垃圾值
而pObj2所指的物件中,欄位num1與num2顯然是經過了構造後的值
*/

型別轉換

型別轉換是將一個資料型別的值轉換為另一種資料型別的值。C++ 中有四種型別轉換:靜態轉換、動態轉換、常量轉換和重新解釋轉換。

  • 靜態轉換(Static Cast):靜態轉換是將一種資料型別的值強制轉換為另一種資料型別的值。靜態轉換通常用於型別相似的物件之間的轉換,例如將 int 型別轉換為 float 型別。靜態轉換不進行任何執行時型別檢查,因此可能會導致執行時錯誤。
int i = 10;
float f = static_cast<float>(i); // 靜態將int型別轉換為float型別
  • 動態轉換(Dynamic Cast):動態轉換通常用於將一個基類指標或引用轉換為派生類指標或引用。動態轉換在執行時進行型別檢查,如果不能進行轉換則返回空指標或引發異常。
class Base {};
class Derived : public Base {};
Base* ptr_base = new Derived;
// 將基類指標轉換為派生類指標
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base);
  • 常量轉換(Const Cast):常量轉換用於將 const 型別的物件轉換為非 const 型別的物件。常量轉換隻能用於轉換掉 const 屬性,不能改變物件的型別。
const int i = 10;
int& r = const_cast<int&>(i); // 常量轉換,將const int轉換為int
  • 重新解釋轉換(Reinterpret Cast):重新解釋轉換將一個資料型別的值重新解釋為另一個資料型別的值,通常用於在不同的資料型別之間進行轉換。重新解釋轉換不進行任何型別檢查,因此可能會導致未定義的行為。
int i = 10;
float f = reinterpret_cast<float&>(i); // 重新解釋將int型別轉換為float型別

typedef 與 #define 的區別

  • 執行時間不同:關鍵字 typedef 在編譯階段有效,由於是在編譯階段,因此 typedef 有型別檢查的功能。#define 則是宏定義,發生在預處理階段,也就是編譯之前,它只進行簡單而機械的字串替換,而不進行任何檢查。
typedef unsigned int UINT;
 
void func()
{
    UINT value = "abc";  // error C2440: 'initializing' : cannot convert from 'const char [4]' to 'UINT'
    cout << value << endl;
}
  • 功能有差異:typedef 用來定義型別的別名,定義與平臺無關的資料型別,與 struct 的結合使用等。#define 不只是可以為型別取別名,還可以定義常量、變數、編譯開關等。
  • 作用域不同:#define 沒有作用域的限制,只要是之前預定義過的宏,在以後的程式中都可以使用。
void func1()
{
    #define HW "HelloWorld";
}
 
void func2()
{
    string str = HW;
    cout << str << endl;
}

而typedef有自己的作用域

void func1()
{
    typedef unsigned int UINT;
}
 
void func2()
{
    UINT uValue = 5;  //error C2065: 'UINT' : undeclared identifier
}

class A
{
    typedef unsigned int UINT;
    UINT valueA;
    A() : valueA(0){}
};
 
class B
{
    UINT valueB;
    //error C2146: syntax error : missing ';' before identifier 'valueB'
    //error C4430: missing type specifier - int assumed. Note: C++ does not support default-int
};

上面例子在B類中使用UINT會出錯,因為UINT只在類A的作用域中。此外,在類中用typedef定義的型別別名還具有相應的訪問許可權:

class A
{
    typedef unsigned int UINT;
    UINT valueA;
    A() : valueA(0){}
};
 
void func3()
{
    A::UINT i = 1;
    // error C2248: 'A::UINT' : cannot access private typedef declared in class 'A'
}

而給UINT加上public訪問許可權後,則可編譯透過。

class A
{
public:
    typedef unsigned int UINT;
    UINT valueA;
    A() : valueA(0){}
};
 
void func3()
{
    A::UINT i = 1;
    cout << i << endl;
}
  • 對指標的操作:二者修飾指標型別時,作用不同:
typedef int * pint;


#define PINT int *
 
int i1 = 1, i2 = 2;
 
const pint p1 = &i1;    //p不可更改,p指向的內容可以更改,相當於 int * const p;
const PINT p2 = &i2;    //p可以更改,p指向的內容不能更改,相當於 const int *p;或 int const *p;
 
pint s1, s2;    //s1和s2都是int型指標
PINT s3, s4;    //相當於int * s3,s4;只有一個是指標。
 
void TestPointer()
{
    cout << "p1:" << p1 << "  *p1:" << *p1 << endl;
    //p1 = &i2; //error C3892: 'p1' : you cannot assign to a variable that is const
    *p1 = 5;
    cout << "p1:" << p1 << "  *p1:" << *p1 << endl;
 
    cout << "p2:" << p2 << "  *p2:" << *p2 << endl;
    //*p2 = 10; //error C3892: 'p2' : you cannot assign to a variable that is const
    p2 = &i1;
    cout << "p2:" << p2 << "  *p2:" << *p2 << endl;
}

結果:

p1:00EFD094 *p1:1
p1:00EFD094 *p1:5
p2:00EFD098 *p2:2
p2:00EFD094 *p2:5
  • 還可以用 typedef 來定義與平臺無關的型別。比如定義一個叫 FALSE 的浮點型別,在目標平臺一上,讓它表示最高精度的型別為:
typedef long double FALSE;

在不支援 long double 的平臺二上,改為:

typedef double FALSE;

在連 double 都不支援的平臺三上,改為:

typedef float FALSE;

也就是說,當跨平臺時,只要改下 typedef 本身就行,不用對其他原始碼做任何修改。標準庫就廣泛使用了這個技巧,比如 size_t。

  • 另外,因為 typedef 是定義了一種型別的新別名,不是簡單的字串替換,所以它比宏來得穩健(雖然用宏有時也可以完成以上的用途)。為了方便列舉的使用,應該和 typedef 結合使用,例如:
typedef enum BAYER_PATTERN{
    BAYER_RG = 0,
    BAYER_BG,
    BAYER_GR,
    BAYER_GB
}BAYER_PATTERN;

使用的時候就不用再 enum BAYER_PATTERN color = BAYER_RG; 了,而可以直接用:

BAYER_PATTERN color = BAYER_RG;

  • 不帶初始化的定義:帶有靜態儲存持續時間的變數會被隱式初始化為 NULL(所有位元組的值都是 0),其他所有變數的初始值是未定義的。

  • C/C++ 編譯 cpp 檔案是從上往下編譯,所以 main 函式里面呼叫其他函式時,如果其他函式在 main 函式的下面,則要在 main 函式上面先宣告這個函式。或者把 main 函式放在最下面,這個不僅限於 main 函式,其他函式的呼叫都是如此。被呼叫的函式要在呼叫的函式之前宣告。
// 函式宣告
int func();


int main()
{
    // 函式呼叫
    int i = func();
}


// 函式定義
int func()
{
    return 0;
}

  • 若參與運算的型別不同,則先轉換成同一型別,然後進行運算。轉換按資料長度增加的方向進行,以保證精度不降低。如int型和long型運算時,先把int量轉成long型後再進行運算。若兩種型別的位元組數不同,轉換成位元組數高的型別。若兩種型別的位元組數相同,且一種有符號,一種無符號,則轉換成無符號型別。

  • 所有的浮點運算都是以雙精度進行的,即使僅含float單精度值運算的表示式,也要先轉換成double型,再作運算。

  • char型和short型參與運算時,必須先轉換成int型。

  • 在賦值運算中,賦值號兩邊量的資料型別不同時,賦值號右邊量的型別將轉換為左邊量的型別。如果右邊量的資料型別長度比左邊長時,將丟失一部分資料,這樣會降低精度:
int a=1;
double b=2.5;
a=b;
cout << a; //輸出為 2,丟失小數部分
int a = 1;
double b = 2.1;
cout << "a + b = " << a + b << endl;  //輸出為a + b = 3.1

掃碼關注公眾號,檢視更多精彩內容

相關文章