參考部落格
Chatgpt
C++ 基礎 - 知識點
修飾符
const
在 C++ 中,const
關鍵字用於定義不可修改的變數、指標、函式引數和返回值等。它可以增強程式碼的安全性和可讀性,防止意外修改資料。
1. 常量變數
使用 const
定義的變數是不可更改的常量。一旦賦值,就不能再修改。
const int x = 10;
// x = 20; // 錯誤,x 是一個常量,不能修改
2. 常量指標
指標可以與 const
結合,來控制指標或指標指向的資料是否可以被修改:
-
指向常量的指標
指標指向的值不能改變,但指標本身可以改變。const int* ptr = &x; // 指標指向一個常量 // *ptr = 20; // 錯誤,不能修改指標指向的值 int y = 15; ptr = &y; // 可以修改指標本身指向不同的變數
-
常量指標
指標本身不能改變,但指標指向的值可以改變。int* const ptr = &x; // 常量指標 *ptr = 20; // 可以修改指標指向的值 // ptr = &y; // 錯誤,不能修改指標本身
-
指向常量的常量指標
指標本身和指標指向的值都不能改變。const int* const ptr = &x; // 指向常量的常量指標 // *ptr = 20; // 錯誤,不能修改指標指向的值 // ptr = &y; // 錯誤,不能修改指標本身
3. 常量成員函式
在類中,使用 const
修飾成員函式表示該函式不會修改類的成員變數。常量成員函式只能呼叫其他常量成員函式。
class MyClass {
public:
int getValue() const { // 常量成員函式
return value;
}
private:
int value = 10;
};
名稱空間
名稱空間(namespace)是一種用於組織程式碼和避免命名衝突的機制。
名稱空間為識別符號(如變數名、函式名、類名等)提供了一個上下文,以便相同名字的識別符號可以在不同的名稱空間中共存,而不會產生衝突。
在 C++ 中,名稱空間(namespace)是一種用於組織程式碼和避免命名衝突的機制。名稱空間為識別符號(如變數名、函式名、類名等)提供了一個上下文,以便相同名字的識別符號可以在不同的名稱空間中共存,而不會產生衝突。
為什麼需要名稱空間?
隨著程式碼庫的增長和多個庫的整合,可能會出現命名衝突的情況,比如不同的庫中可能會有相同名字的函式、類或者變數。如果沒有名稱空間的存在,這些名字就會發生衝突,導致編譯錯誤。名稱空間可以解決這個問題。
1. 定義名稱空間
名稱空間透過 namespace
關鍵字定義,它可以包含變數、函式、類、結構體、列舉等。
示例:
namespace MyNamespace {
int value = 42;
void display() {
std::cout << "Value: " << value << std::endl;
}
}
在這個例子中,MyNamespace
是一個名稱空間,包含了一個整數變數 value
和一個函式 display()
。如果要訪問這些變數和函式,就必須透過名稱空間的名稱來引用。
2. 使用名稱空間中的內容
你可以使用名稱空間的成員透過以下幾種方式:
2.1 使用名稱空間字首
要使用名稱空間中的變數或函式,你可以在名稱前加上名稱空間的名稱作為字首。
示例:
int main() {
MyNamespace::display(); // 使用名稱空間字首
std::cout << MyNamespace::value << std::endl;
}
2.2 使用 using
宣告
透過 using
關鍵字,你可以將名稱空間中的特定成員引入當前作用域,從而不需要每次都使用名稱空間字首。
示例:
int main() {
using MyNamespace::display;
display(); // 直接使用函式,不需要字首
}
2.3 使用 using namespace
引入整個名稱空間
你還可以透過 using namespace
將整個名稱空間引入到當前作用域,從而無需使用字首來訪問名稱空間中的所有成員。
示例:
int main() {
using namespace MyNamespace;
display(); // 不需要字首
std::cout << value << std::endl;
}
注意:
using namespace
可能會引發命名衝突,尤其是在大範圍中使用多個庫時。因此,最好在區域性作用域中使用,或者避免在全域性範圍內大量引入名稱空間。
3. 標準名稱空間 std
C++ 標準庫中的所有庫函式、類和物件都被定義在 std
名稱空間中。因此,使用標準庫的內容時,通常需要使用 std::
字首,或者透過 using namespace std;
引入。
示例:
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl; // 使用 std 名稱空間
}
或者:
#include <iostream>
using namespace std;
int main() {
cout << "Hello, World!" << endl; // 不需要 std 字首
}
使用案例:
有時,區域性作用域中會有與全域性作用域同名的變數、函式或類。在這種情況下,使用 :: 可以訪問全域性作用域中的符號。
例如:
int value = 10; // 全域性變數
void func() {
int value = 20; // 區域性變數
std::cout << value << std::endl; // 輸出 20
std::cout << ::value << std::endl; // 使用 :: 訪問全域性變數,輸出 10
}
在 func() 中,value 是區域性變數,而 ::value 訪問的是全域性變數。
當你使用 C++ 標準庫或者使用者定義的名稱空間中的某些符號時,你需要用 :: 來指定該符號屬於哪個名稱空間。例如:
std::string name = "Alice"; // 使用 std 名稱空間中的 string 類
使用 std:: 明確指出 string 是標準庫名稱空間中的類,而不是全域性作用域或其他名稱空間中的同名符號。
物件導向
在 C++ 中,類(class) 是物件導向程式設計的核心概念之一。類是一種使用者定義的型別,它封裝了資料(成員變數)和行為(成員函式),並提供了對這些資料和行為的訪問控制。
類的基本概念
- 類的宣告:類是使用者定義的型別,它定義了物件的屬性(成員變數)和行為(成員函式)。
- 物件:類的例項化物件代表類的具體實現,每個物件都有自己的成員變數副本。
- 封裝:類透過將資料和函式封裝在一起來實現封裝性。類可以控制哪些資料對外部可見(透過訪問控制修飾符)。
- 繼承:類可以透過繼承其他類,複用已有類的功能,同時可以新增新的功能或重寫已有功能。
- 多型性:透過基類指標或引用,動態呼叫不同派生類的實現,利用虛擬函式實現執行時多型。
類的定義與物件的使用
#include <iostream>
#include <string>
class Person {
private:
// 私有成員變數
std::string name;
int age;
public:
// 建構函式
Person(std::string n, int a) : name(n), age(a) {}
// 成員函式
void introduce() const {
std::cout << "Hello, my name is " << name << " and I am " << age << " years old." << std::endl;
}
// 獲取年齡(只讀的成員函式)
int getAge() const {
return age;
}
// 修改年齡
void setAge(int a) {
age = a;
}
};
int main() {
// 建立物件
Person person1("Alice", 30);
// 使用物件的成員函式
person1.introduce(); // 輸出: Hello, my name is Alice and I am 30 years old.
person1.setAge(31); // 修改年齡
std::cout << "New age: " << person1.getAge() << std::endl; // 輸出: New age: 31
return 0;
}
建構函式與解構函式
- 建構函式(Constructor):建構函式是一個與類名相同的特殊函式,用於在建立物件時初始化成員變數。它可以有引數或沒有引數。
- 解構函式(Destructor):解構函式是一個用於在物件生命週期結束時清理資源的特殊函式。它的名字是類名前加上
~
符號,例如~Person()
。解構函式通常不接受引數。
建構函式和解構函式示例:
class Example {
private:
int* data;
public:
// 建構函式
Example(int value) {
data = new int(value); // 動態分配記憶體
std::cout << "Constructor called!" << std::endl;
}
// 解構函式
~Example() {
delete data; // 釋放動態分配的記憶體
std::cout << "Destructor called!" << std::endl;
}
};
這裡的建構函式寫法是使用成員初始化列表的方式來初始化成員變數:
Person(std::string n, int a) : name(n), age(a) {}
而你提到的另一種方式是透過在建構函式體內賦值:
Person(std::string n, int a) { name = n; age = a; }
雖然這兩種方式在效果上類似,都能初始化物件的成員變數,但使用成員初始化列表通常是更推薦的方式,原因如下:
- 更高效
對於內建型別(如int
、double
),兩種方式的區別可能不大。然而,對於非內建型別的成員變數,使用成員初始化列表可以避免不必要的預設構造和賦值操作。
-
在使用成員初始化列表的情況下,成員變數在物件構造時直接被初始化:
Person(std::string n, int a) : name(n), age(a) {}
這裡,
name
和age
直接在初始化時被賦值為n
和a
,沒有多餘的步驟。 -
如果你在建構函式體內賦值:
Person(std::string n, int a) { name = n; age = a; }
在這種情況下,
name
和age
首先會透過它們的預設建構函式初始化(對於內建型別可能無影響,但對於類物件會多一步預設構造),然後再透過賦值操作來更新它們的值。這會導致不必要的效能開銷,尤其是在類的成員變數是複雜型別(如std::string
、自定義類)時。
- 不可重新賦值的成員(
const
或引用
)
成員初始化列表是初始化const
成員或引用成員的唯一方式。因為const
或引用型別的成員變數在物件建立時就必須被初始化,不能透過賦值進行修改。
例如:
class Person {
private:
const int id;
std::string name;
public:
// 必須使用成員初始化列表初始化 const 成員
Person(int i, std::string n) : id(i), name(n) {}
};
這裡 id
是 const
,只能在成員初始化列表中被初始化,不能在建構函式體內進行賦值。
訪問控制修飾符
private
:私有成員只能在類的內部訪問,不能在類的外部訪問。通常用於隱藏物件的內部實現。public
:公有成員可以在類的外部訪問,是類的對外介面。protected
:受保護的成員只能在類的內部以及派生類中訪問,不能在類的外部訪問。
繼承(Inheritance)
繼承允許一個類從另一個類繼承屬性和方法。透過繼承,子類可以擴充套件或修改父類的行為。繼承的方式有三種:
- 公有繼承(public inheritance):父類的
public
和protected
成員在子類中保持原有的訪問級別。 - 私有繼承(private inheritance):父類的所有成員在子類中都變為
private
訪問級別。 - 保護繼承(protected inheritance):父類的
public
和protected
成員在子類中變為protected
。
繼承示例:
class Animal {
public:
void eat() {
std::cout << "I am eating." << std::endl;
}
};
class Dog : public Animal {
public:
void bark() {
std::cout << "I am barking." << std::endl;
}
};
虛擬函式
虛擬函式(Virtual Function) 是 C++ 中用於實現執行時多型性的一種機制。它允許在繼承體系中,透過基類的指標或引用,呼叫子類的重寫函式。
虛擬函式的主要目的是在程式執行時,根據物件的實際型別決定呼叫哪個函式版本,而不是在編譯時決定。
- 定義:在基類中用 virtual 關鍵字宣告的函式就是虛擬函式。
- 多型性:虛擬函式支援動態繫結(也稱為後期繫結),這意味著函式呼叫會在執行時解析,而不是在編譯時確定。
- 重寫:子類可以透過重寫基類的虛擬函式來提供不同的實現。
- 基類指標/引用:透過基類的指標或引用呼叫虛擬函式時,程式會根據實際的物件型別選擇正確的重寫版本。
class Animal {
public:
virtual void sound() const {
std::cout << "Animal makes a sound." << std::endl;
}
};
class Dog : public Animal {
public:
void sound() const override {
std::cout << "Dog barks." << std::endl;
}
};
inline 和 類
當你在類的宣告中定義了一個成員函式,除了虛擬函式之外,編譯器會自動將這些函式視為行內函數(inline
),即使你沒有顯式地使用 inline
關鍵字。這意味著編譯器會嘗試將這些函式作為行內函數處理。
示例:
class MyClass {
public:
int getValue() { return value; } // 隱式行內函數
void setValue(int v) { value = v; } // 隱式行內函數
virtual void display() { // 虛擬函式,不會隱式內聯
std::cout << "Value: " << value << std::endl;
}
private:
int value;
};
在這個例子中,getValue()
和 setValue()
是在類的宣告中定義的。根據這句話的意思,它們被隱式地視為行內函數。因此,編譯器可能會將這些函式的程式碼直接插入到呼叫這些函式的地方,類似於手動使用 inline
關鍵字的效果。
解釋:
-
類宣告中的函式定義:如果你在類的宣告中直接定義了函式(即在類的標頭檔案中直接寫了函式體),那麼這些函式會被自動認為是內聯的。
class MyClass { public: int getValue() { return value; } // 隱式內聯 private: int value; };
-
虛擬函式的例外情況:虛擬函式不會被自動內聯。虛擬函式是透過虛擬函式表(vtable)進行動態呼叫的,因此它們不適合內聯最佳化。行內函數的優勢是消除函式呼叫開銷,而虛擬函式的呼叫機制本身就是為了支援動態多型性,這會導致函式呼叫必須在執行時決定。
class MyClass { public: virtual void display() { std::cout << "Hello" << std::endl; } // 不會隱式內聯 };
內聯的自動化與限制:
-
自動內聯:類宣告中的非虛擬函式會被編譯器自動標記為行內函數,不需要顯式寫出
inline
關鍵字。 -
編譯器的自由裁量權:雖然函式在類宣告中定義時會自動成為行內函數,但編譯器仍然有權決定是否實際將這些函式內聯到呼叫點。如果函式過於複雜或體積過大,編譯器可能不會內聯它。
優點與使用場景:
-
提高效能:自動行內函數可以減少函式呼叫開銷,因為函式的程式碼可能會被直接插入到呼叫點,省去了壓棧、跳轉和返回的開銷。
-
簡單函式:這種內聯機制非常適用於簡單且頻繁呼叫的函式,例如訪問器(
getters
和setters
)以及其他短小的操作函式。