4、聲明
聲明是將名字引入到cpp程序中,不是每條聲明都聲明實際的東西。定義是足以使該名字所標識的實體被使用的聲明。聲明包含以下幾種:
- 函數定義
- 模板聲明
- 模板顯式實例化
- 模板顯式特化
- 命名空間定義
- 鏈接說明
- 屬性聲明(C++11)
- 空聲明(;)
- 塊聲明(能在塊中出現的聲明)
- 匯編聲明
- 類型別名聲明(C++11)
- 命名空間別名定義
- using 聲明
- using 指令
- using enum聲明
- static_assert
- 不透明enum聲明
- 簡單聲明:引入、創建并可能會初始化一個或數個標識符的語句(典型地為變量)
說明符:聲明說明符是用于描述變量、函數或者類型等聲明屬性的關鍵字或者符號。它包含以下幾類:
- 存儲類說明符:像auto、register、static、extern、mutable等。
- 類型說明符:例如int、char、float、double、void等基本類型,還有struct、union、enum、自定義類型等。
- 類型限定符:比如const、volatile。
- 其他說明符:typdef、inline、friend、constexpr
4.1、存儲類說明符
4.1.1、存儲期
存儲期是對象的一種屬性,定義了對象的存儲的最短潛在生存期。存儲期由對象的創建方式決定,一共有如下幾個:
- 靜態存儲期:屬于命名空間作用域,或者聲明static或者extern
- 線程存儲期(C++11):聲明thread_local的變量,這些實體的存儲在創建它們的線程持續期間存在,每個線程都有一個不同的對象。
- 自動存儲期:屬于塊作用域,沒有static、thread_local、extern修飾,此類變量的存儲持續到它們創建時所在的塊退出時。
- 動態存儲期:new表達式創建的對象和隱式創建的對象
4.1.2、鏈接
名字可以具有外部鏈接、模塊鏈接、內部鏈接或者無鏈接:
-
無鏈接:無鏈接的標識符僅在其聲明的作用域內可見,在其他作用域里無法使用。它不具備跨翻譯單元引用的能力。常見的無鏈接標識符有局部變量、類的非靜態成員等
-
內部鏈接:具有內部鏈接的標識符在單個翻譯單元(也就是單個.cpp文件及其包含的頭文件)內可見,不同的翻譯單元可以擁有同名的內部鏈接標識符,且它們彼此獨立。通常使用static修飾的全局變量和函數,以及匿名命名空間中的標識符具有內部鏈接。
-
外部鏈接:具有外部鏈接的標識符可以在多個翻譯單元中被訪問,一個翻譯單元可以引用另一個翻譯單元中聲明的具有外部鏈接的標識符。普通的全局變量和函數默認具有外部鏈接。類以及其成員函數、靜態數據成員默認具有外部鏈接。
類本身具有外部鏈接,也就是說如果在一個翻譯單元定義了一個類,其他翻譯單元包含頭文件后可以直接使用。類的非內聯成員函數(無論是否是靜態函數)默認具有外部鏈接;內聯成員函數具有內部鏈接:
class MyClass {
public:void inlineFunc() { /* 內聯實現 */ }
};// 使用inline關鍵字在類定義外部定義
class MyClass {
public:void func() { ... } // 隱式內聯void inlineFunc();
};
inline void MyClass::inlineFunc() { /* 內聯實現 */ }
要注意的是訪問控制權限(private/protected/public)和鏈接屬性是沒有關系的。
在全局作用域中使用 static 修飾變量或函數時,會改變它們的鏈接屬性,使其從默認的外部鏈接變為內部鏈接。這意味著這些變量或函數僅在定義它們的翻譯單元(.cpp 文件)內可見,不同翻譯單元可以有同名的 static 全局變量或函數,且彼此獨立。
當在局部作用域(如函數內部)使用 static 修飾變量時,該變量會成為靜態局部變量。靜態局部變量與普通局部變量不同,它的生命周期會延長至整個程序的運行期間,而不是像普通局部變量那樣在函數調用結束后就銷毀。同時,靜態局部變量只會在首次執行到其聲明語句時進行初始化,后續函數調用時不會再次初始化。
在不同翻譯單元中,可以有多個名字相同的靜態變量。當使用 static 修飾全局變量時,該變量具有內部鏈接屬性,其作用域僅限于定義它的翻譯單元。因此,在不同的翻譯單元中定義同名的靜態變量是合法的,它們彼此相互獨立,不會產生沖突。每個翻譯單元中的靜態變量都有自己獨立的存儲空間,在各自的翻譯單元內可以被訪問和修改,而不會影響其他翻譯單元中的同名變量。
在整個程序中,不能擁有兩個名稱相同的具有外部鏈接的變量。具有外部鏈接的變量,其作用域可以跨越多個翻譯單元。如果在不同的翻譯單元中定義了兩個同名的具有外部鏈接的變量,鏈接器在鏈接階段會發現重復定義(違反ODR原則),從而導致鏈接錯誤。
以上這部分可以參考Modern C++(一)基本概念1.5節
- 靜態塊變量:靜態局部變量只會在首次執行到其聲明語句時進行初始化,后續函數調用時不會再次初始化。在其后所有的調用中,聲明都會被跳過。如果多個線程試圖同時初始化同一靜態局部變量,那么初始化嚴格發生一次(等價于std::call_once)。靜態局部變量的內存分配發生在程序加載時(main 之前),但初始化延遲到首次執行到其聲明處,靜態存儲期的塊變量的析構函數在程序退出時調用。
4.2、語言鏈接
所有函數類型、所有擁有外部鏈接的函數名,以及所有擁有外部鏈接的變量名,擁有一種稱作語言鏈接的性質。
C++支持函數重載、類成員函數等特性,為了保證這些特性能夠正常工作,編譯器會對函數名和變量名進行名稱修飾(Name Mangling)。名稱修飾會把函數的參數類型、返回類型等信息融入到修飾后的名稱中,從而讓鏈接器能夠區分不同的重載函數。
“C” 語言鏈接:當需要在C++代碼里定義能夠被C語言代碼調用的函數,或者要調用C語言編寫的函數時,需要使用"C" 語言鏈接。C 語言沒有函數重載等特性,所以 C 語言的函數名不會被修飾。使用 “C” 語言鏈接可以讓 C++ 編譯器按照 C 語言的規則處理函數名,避免名稱修飾。
namespace A
{extern "C" int x();extern "C" int y();
}extern "C" { int x; }
- 語言鏈接只能在命名空間作用域出現,意思是語言鏈接不能出現在除命名空間以外的作用域中
- 語言說明的花括號不建立作用域
- 當語言說明發生嵌套時,只有最內層的說明生效。
- 直接包含在語言鏈接說明之中的聲明,被處理為如同它含有 extern 說明符,用以確定所聲明的名字的鏈接以及它是否為定義。
extern "C" int x; // 聲明且非定義extern "C" { int x; } // 聲明及定義
- extern “C” 允許 C++ 程序中包含含有 C 庫函數的聲明的頭文件,但如果與 C 程序共用相同的頭文件,就必須以適當的 #ifdef 隱藏 extern “C”(C 中不允許使用),通常會用 __cplusplus:
#ifdef __cplusplus
extern "C" int foo(int, int); // C++ 編譯器看到的
#else
int foo(int, int); // C 編譯器看到的
#endif
要注意的是,extern “C” 的核心作用是告訴 C++ 編譯器:“對這些函數 / 變量使用 C 語言的命名和調用約定”,而不是禁止使用 C++ 特性。
以上是C++程序使用C庫的方法。如果要在C程序使用C++庫怎么辦呢?如果希望C代碼能夠直接調用C++庫中的函數,那么在編譯C++庫時,必須通過extern "C"明確標記需要暴露給C的函數,這樣編譯器就不會對這些函數進行名稱修飾,從而讓C代碼能夠正確鏈接和調用。
綜上,extern "C"有兩個作用
- 當C++調用C庫時:告訴C++編譯器:“這個函數是C風格的,不要用C++的名稱修飾去找它”
- 當編譯C++庫時:告訴C++編譯器:“這個函數要按C風格編譯,不要進行名稱修飾”。
4.3、命名空間
在命名空間塊以內聲明的實體會被放入一個命名空間作用域中,可以避免名字沖突。在命名空間塊以外聲明的實體屬于全局命名空間。全局命名空間屬于全局作用域,并且可以通過以 :: 開頭來顯式指代。多個命名空間塊的名字可以相同。
- 具名命名空間
namespace A {int a = 10;
}
- 內聯命名空間
內聯命名空間內的聲明將在它的外圍命名空間可見。內聯命名空間提供了一種方便的機制,使得我們可以在不改變現有代碼調用方式的情況下,對庫進行版本更新和擴展。
// 初始版本 (v1是內聯的)
namespace Graphics {inline namespace v1 {class Renderer { /*...*/ };}
}// 用戶代碼最初是這樣寫的
Graphics::Renderer r; // 實際使用的是v1// 新版本 (v2成為內聯的,但保留v1)
namespace Graphics {namespace v1 { // 不再是inlineclass Renderer { /*...*/ }; }inline namespace v2 { // 新的inline命名空間class Renderer { /*...*/ };}
}// 用戶代碼無需修改!
Graphics::Renderer r; // 現在自動解析為v2::Renderer
- 匿名命名空間
匿名命名空間內部成員的作用域是具有從聲明點到翻譯單元結尾,具有內部鏈接
namespace {int a = 0;
}//int a = 10; // 如果這個a被定義,那么匿名的a無法被使用int main() {a = 20;cout << ::a << endl; // a = 20return 0;
}
- 有限定查找
命名空間名可以出現在作用域解析運算符的左側,作為有限定的名字查找的一部分。具體的名字查找過程參考Modern C++(一)基本概念 1.6、名字查找。
namespace A {int a = 10;
}A::a = 20;
- using namespace 命名空間名
從using指令之后到指令出現的作用域結尾為止,來自命名空間名的任何名字可被無限定查找
namespace A {int a = 10;
}using namespace A;
a = 20;
- using 命名空間名::成員名
引入命名空間中的成員,注意只能引入該命名空間直接包含的成員
#include <iostream>namespace MyLibrary {void func1() { std::cout << "MyLibrary::func1()\n"; }void func2() { std::cout << "MyLibrary::func2()\n"; }
}int main() {using MyLibrary::func1;func1(); // 可以直接調用,無需限定// func2(); // 錯誤:func2不可見
}
如果有嵌套類是不能直接引入的:
namespace N {class Outer {public:class Inner {};};
}// using N::Outer::Inner 錯誤
替代方法是:
namespace N {class Outer {public:class Inner {};};using Inner = Outer::Inner; // 在外層命名空間中定義別名
}
或者定義別名
using NestedInner = N::Outer::Inner;
- 命名空間別名定義
#include <iostream>namespace VeryLongNamespaceName {void foo() {}
}int main() {namespace ShortName = VeryLongNamespaceName;ShortName::foo(); // 等同于VeryLongNamespaceName::foo()
}
- 嵌套命名空間定義
// 傳統方式 (C++11)
namespace A {namespace B {namespace C {void func() {}}}
}// C++17新語法
namespace A::B::C {void newFunc() {}
}
不能前向聲明嵌套命名空間
// namespace Outer::Inner; // 錯誤:不能單獨前向聲明嵌套命名空間// 正確方式
namespace Outer {namespace Inner {class A;}
}
4.4、引用聲明
引用表示已存在對象或函數的別名,引用有兩種:
- S& D:S類型的左值引用
- S&& D:S類型的右值引用
右值的定義參考Modern C++(三)表達式 3.1.2、右值。
引用有以下幾點使用要注意:
- 引用必須被初始化為指代一個有效的對象或函數
- 引用類型不能在頂層有 cv 限定
- 頂層 cv 限定:作用于對象本身
- 底層 cv 限定:作用于對象所指向或引用的類型。
- 所謂底層,就是它真實指向的內容,const修飾指針、引用類型。
- 所謂頂層,指的是變量自身,const在變量名前面
int tmp = 10;
const int* ptr = &tmp; // 底層
int tmp2 = 20;
ptr = &tmp2;
int* const ptr2 = &tmp; // 頂層
- 引用不是對象;它們不必占用存儲,但是編譯器仍會分配一個指針大小的內存
- 不存在引用的數組,不存在指向引用的指針,不存在引用的引用(有特殊情況,模板和typedef)
int& a[3]; // 錯誤
int&* p; // 錯誤
int& &r; // 錯誤
- 左值引用可用于建立既存對象的別名。
- 右值引用可用于為臨時對象延長生存期
- MyClass rref = createTempObject(); 會創建一個臨時對象的副本,臨時對象本身會在表達式結束后銷毀。實際測試只調用了一次構造函數,這是因為編譯器做了返回值優化(Return Value Optimization,RVO)
- const MyClass& ref = createTempObject(); 延長了臨時對象的生存期,但不能修改臨時對象。注意,如果不加const會編譯失敗:非常量引用的初始化必須是左值。示例中42是右值,無法被修改,使用非常量引用可能會導致非法修改。
int& invalid_ref = 42; // 錯誤:42是右值(字面量) const int& valid_ref = 42; // 合法:常量引用可綁定右值
- MyClass&& rref = createTempObject(); 既延長了臨時對象的生存期,又允許對臨時對象進行修改。
雖然右值引用變量在聲明時綁定到右值,但在后續的表達式中,它本身具有確定的存儲位置,可以取地址,因此表現得像左值。
// 右值引用變量在用于表達式時是左值
int&& x = 1;
f(x); // 調用 f(int& x)int i = 1;
f(std::move(i)); // 調用 f(int&&)
4.4.1、萬能引用、完美轉發與引用折疊
在模板中,T&&有兩種截然不同的含義:
- 右值引用:當T是明確類型時,如int&&,表示只能綁定右值
- 萬能引用:在模板函數中,當T是模板類型參數且使用T&&時,表示既可以綁定左值,也可以綁定右值。
void func(int& x) { std::cout << "左值" << std::endl; }
void func(int&& x) { std::cout << "右值" << std::endl; }template<typename T>
void wrapper(T&& arg) {func(std::forward<T>(arg)); // 完美轉發參數到func
}int main() {int x = 10;wrapper(x); // 轉發左值,調用func(int&)wrapper(20); // 轉發右值,調用func(int&&)
}
當向T&&傳遞左值時,T被推導為T&(左值引用),所以上例中傳遞x,T被推導為int&。
當向T&&傳遞右值時,T被推導為T(非引用類型),所以上例中傳遞20,T被推導為int。
要注意語法是不支持引用的引用!!但是有特殊情況。
引用折疊:通過模板或typedef中的類型操作可以構成引用的引用,此時適用引用折疊規則,右值引用的右值引用折疊成右值引用,所有其他組合均折疊成左值引用
typedef int& lref;
typedef int&& rref;
int n;lref& r1 = n; // r1 的類型是 int&
lref&& r2 = n; // r2 的類型是 int&
rref& r3 = n; // r3 的類型是 int&
rref&& r4 = 1; // r4 的類型是 int&&
上述例子中T被推導為T&后,函數參數類型為int& &&,被折疊為int&。
4.5、指針聲明
指針聲明包含兩種:
- 指針聲明符:聲明 S* D;
- 成員指針聲明符:聲明 S C:😗 D; 指向 C 類中 S 類型的非靜態數據成員的指針
不存在指向引用的指針和指向位域的指針。
由于存在數組到指針的隱式轉換,可以以數組類型的表達式初始化指向數組首元素的指針。
int a[2];
int* p1 = a; // 指向數組 a 首元素 a[0](一個 int)的指針int b[6][3][8];
int (*p2)[3][8] = b;
由于存在指針的派生類到基類的隱式轉換,可以以派生類的地址初始化指向基類的指針,這種指針可用于進行虛函數調用。如果原指針指向某多態類型對象中的基類子對象,則可用 dynamic_cast 獲得指向最終派生類型的完整對象的 void*
struct Base {};
struct Derived : Base {};Derived d;
Base* p = &d;
指向任意類型對象的指針都可以被隱式轉換成指向 void 的指針,逆向的轉換要求 static_cast 或顯式轉換,并生成它的原指針值。
int n = 1;
int* p1 = &n;
void* pv = p1;
int* p2 = static_cast<int*>(pv);
std::cout << *p2 << '\n'; // 打印 1
函數指針能以非成員函數或靜態成員函數的地址初始化,由于存在函數到指針的隱式轉換,取址運算符可以忽略。與函數或函數的引用不同,函數指針是對象,從而能存儲于數組、被復制、被賦值等。
void f(int);
void (*p1)(int) = &f;
void (*p2)(int) = f; // 與 &f 相同void (*a[10])(int); // OK:函數指針數組using F = void(int); // 用來簡化聲明的具名類型別名
F* a[10]; // OK:函數指針數組
要注意:
- using F = void(int); // F 是函數類型
- using FPtr = void(*)(int); // FPtr 是函數指針類型
- typedef void (*FuncPtr)(int); // C風格函數指針類型
4.5.1、成員指針聲明
成員指針是 C++ 中一種特殊的指針類型,它指向類的成員(成員變量或成員函數),而非對象本身。成員指針包含兩種:
- 數據成員指針:指向作為類 C 的成員的非靜態數據成員 m 的指針&C::m(其他形式不構成成員指針)
struct C { int m; };int main()
{int C::* p = &C::m; // 指向類 C 的數據成員 mC c = {7};std::cout << c.*p << '\n'; // 打印 7C* cp = &c;cp->m = 10;std::cout << cp->*p << '\n'; // 打印 10
}
- 成員函數指針:指向作為類 C 的成員的非靜態成員函數 f 的指針,能準確地以表達式 &C::f 初始化
struct C
{void f(int n) { std::cout << n << '\n'; }
};int main()
{void (C::* p)(int) = &C::f; // 指向類 C 的成員函數 f 的指針C c;(c.*p)(1); // 打印 1C* cp = &c;(cp->*p)(2); // 打印 2
}
成員指針有什么用?
- 實現通用的訪問邏輯:成員指針可以讓你編寫通用的代碼來訪問不同對象的相同成員,而不需要為每個對象類型編寫特定的訪問代碼。這在處理類層次結構或模板編程時特別有用。
#include <iostream>class MyClass {
public:int value;void printValue() {std::cout << "Value: " << value << std::endl;}
};// 通用函數,使用成員指針訪問對象的成員
void accessMember(MyClass& obj, int MyClass::* memberPtr) {std::cout << "Accessing member: " << obj.*memberPtr << std::endl;
}int main() {MyClass obj;obj.value = 10;// 定義成員指針int MyClass::* ptr = &MyClass::value;// 調用通用函數訪問成員accessMember(obj, ptr);return 0;
}
- 實現回調機制,允許運行時動態指定要調用的成員函數,在事件處理、狀態機等場景中非常有用
#include <iostream>class EventHandler {
public:void handleEvent1() {std::cout << "Handling event 1" << std::endl;}void handleEvent2() {std::cout << "Handling event 2" << std::endl;}
};// 回調函數類型
using Callback = void (EventHandler::*)();// 事件處理器類
class EventManager {
public:void setCallback(Callback cb) {callback = cb;}void triggerEvent(EventHandler& handler) {if (callback) {(handler.*callback)();}}private:Callback callback = nullptr;
};int main() {EventHandler handler;EventManager manager;// 設置回調函數manager.setCallback(&EventHandler::handleEvent1);// 觸發事件manager.triggerEvent(handler);return 0;
}
每個類型的指針都擁有一個特殊值,稱為該類型的空指針值。需要將指針初始化為空或賦空值給既存指針時,可以使用值為0的整數字面量,std::nullptr_t。
4.6、數組聲明
形式為T a[N];的聲明為數組聲明,a為由N個連續分配的T類型對象所組成的數組對象(注意,會調用N次T的構造函數,無論是否帶初始化器)。
class T {
public:T(int value) : data(value) {}
private:int data;
};// T arr[3]; // 錯誤:無法編譯,因為 T 沒有默認構造函數T arr[3] = {T(1), T(2), T(3)}; // 顯式調用帶參數的構造函數
T arr[3] = {1, 2, 3}; // 隱式轉換(如果構造函數不是 explicit)
要注意的是,如果類沒有默認構造函數,聲明數組時必須提供初始化器。
T arr[3] = {1, 2, 3}; // 棧上聲明數組
T *arr2 = new T[3] {1, 2, 3}; // 堆上聲明數組
數組元素不能具有未知邊界數組類型,所以多維數組只能在第一個維度中有未知邊界。
T arr3[] = { 1, 2, 3 };
T arr3[][2] = { {1, 2}, {4, 5} };
4.7、結構化綁定
結構化綁定聲明是 C++17 中一個非常實用的特性,它可以讓你更方便地處理聚合類型,提高代碼的可讀性和簡潔性。通過結構化綁定,你可以直接將聚合類型的元素綁定到命名變量上,避免了繁瑣的索引或成員訪問操作。
與引用類似,結構化綁定是既存對象的別名。
struct C { int x, y, z; };template <class T>
void now_i_know_my()
{auto [a, b, c] = C(); // OK: a, b, c 分別指代 x, y, zauto [d, ...e] = C(); // OK: d 指代 x; ...e 指代 y 和 zauto [...f, g] = C(); // OK: ...f 指代 x 和 y; g 指代 zauto [h, i, j, ...k] = C(); // OK: 包 k 為空// auto [l, m, n, o, ...p] = C(); // 錯誤: 結構化綁定大小太小
}int a[2] = {1, 2};
auto [x, y] = a; // 創建 e[2],復制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]
const auto& [xr1, yr1] = a;
要注意的是被綁定的對象初始化器必須是 = 、()、 {} 之一。
4.8、枚舉
枚舉是一種獨立的類型,它的值被限制在一個取值范圍內,它可以包含數個明確命名的常量(“枚舉項”)。
有以下幾種形式可以聲明枚舉:
- 枚舉關鍵詞 屬性(可選) 枚舉頭名(可選) 枚舉基(可選) { 枚舉項列表(可選) };
- 枚舉關鍵詞 屬性(可選) 枚舉頭名(可選) 枚舉基(可選) { 枚舉項列表, };
- 枚舉關鍵詞 屬性(可選) 枚舉頭名 枚舉基(可選);
枚舉關鍵字:enum、enum class、enum struct
枚舉頭名:所聲明的枚舉的名字,可以省略
枚舉基:冒號后面指名的某個整數類型
枚舉選項列表:標識符 = 常量表達式、簡單的獨一無二的標識符
4.8.1、無作用域枚舉
無作用域枚舉也就是傳統枚舉,每個枚舉項都成為該枚舉類型的一個具名常量,在它的外圍作用域可見,且可以用于要求常量的任何位置。無作用域枚舉的名字可以忽略,當無作用域枚舉是類成員時,它的枚舉項可以通過類成員訪問運算符 . 和 -> 訪問。
struct X
{enum direction { left = 'l', right = 'r' };
};
X x;
X* p = &x;int a = X::direction::left; // C++11 開始才能用
int b = X::left;
int c = x.left;
int d = p->left;
無作用域枚舉類型的值可以被提升或轉換為整數類型,示例中給d賦值時可以直接賦值。
無作用域枚舉的底層類型由編譯器決定(通常是int),也可以通過枚舉基來指定:
enum SmallEnum : short { A, B, C }; // 底層類型為short
若未顯式指定無作用域枚舉底層類型,無法前向聲明(因為編譯器需要知道大小)
enum E; // 錯誤:必須指定底層類型
enum E : int; // 正確
4.8.2、有作用域枚舉
有作用域枚舉也被稱為增強型枚舉,每個枚舉項都成為該枚舉的類型(即名字)的具名常量,它被該枚舉的作用域所包含,且可用作用域解析運算符訪問。沒有從有作用域枚舉項到整數類型的隱式轉換(可以用static_cast)。
enum class Color { red, green = 20, blue };
Color r = Color::blue;switch(r)
{case Color::red : std::cout << "紅\n"; break;case Color::green: std::cout << "綠\n"; break;case Color::blue : std::cout << "藍\n"; break;
}// int n = r; // 錯誤:不存在從有作用域枚舉到 int 的隱式轉換
int n = static_cast<int>(r); // OK, n = 21
std::cout << n << '\n'; // prints 21
int red = 10;
注意示例中的枚舉項只能用作用域解析符使用,受作用域的影響,最后定義red并不會出錯。
enum class/enum struct默認底層類型為int,也可以通過枚舉基來指定。
- using enum 聲明:引入它所指名的枚舉的枚舉項名字(C++20起),可直接前向聲明
enum class fruit { orange, apple };struct S
{using enum fruit; // OK:引入 orange 與 apple 到 S 中
};void f()
{S s;s.orange; // OK:指名 fruit::orangeS::orange; // OK:指名 fruit::orange
}
- 使用 enum:當需要與 C 代碼兼容或需要隱式轉換為整數時。
- 使用 enum class/enum struct:當需要避免命名沖突、增強類型安全時(推薦優先使用)
4.9、inline說明符
inline說明符將函數聲明為一個內聯函數。內聯函數或內聯變量(C++17)有以下性質:
- 內聯函數的定義必須在訪問它的翻譯單元中可見,否則鏈接時會報錯
// 翻譯單元 1:main.cpp
#include <iostream>// 聲明內聯函數
void useInlineFunction();int main() {useInlineFunction();return 0;
}// 內聯函數的定義
inline void useInlineFunction() {std::cout << "This is an inline function." << std::endl;
}
這里看到內聯函數的聲明并沒有加inline,這是為什么呢?inline是一個定義屬性,而非聲明屬性。它告訴編譯器 “這個函數可以在調用點直接展開”,因此只需要在定義時標記。
內聯實體允許多個翻譯單元中有相同的定義,但每個定義必須完全一致!如果各個定義不相同是未定義行為,可能出現非預期行為,具體調用哪一個實現由編譯器決定。
在內聯函數中,所有函數定義中的函數局部靜態對象在所有翻譯單元間共享,地址相同。
// 頭文件:inline_example.h
#ifndef INLINE_EXAMPLE_H
#define INLINE_EXAMPLE_H
// 內聯函數定義
inline int add(int a, int b) {return a + b;
}
// 內聯變量定義(C++17 起)
inline int globalValue = 10;
#endif// 翻譯單元 1:file1.cpp
#include "inline_example.h"
#include <iostream>void test1() {int result = add(2, 3);std::cout << "Result in file1: " << result << std::endl;std::cout << "Global value in file1: " << globalValue << std::endl;
}// 翻譯單元 2:file2.cpp
#include "inline_example.h"
#include <iostream>void test2() {int result = add(4, 5);std::cout << "Result in file2: " << result << std::endl;std::cout << "Global value in file2: " << globalValue << std::endl;
}
// 頭文件:address_example.h
#ifndef ADDRESS_EXAMPLE_H
#define ADDRESS_EXAMPLE_H// 內聯變量定義
inline int sharedValue = 20;#endif// 翻譯單元 1:address_file1.cpp
#include "address_example.h"
#include <iostream>void printAddress1() {std::cout << "Address of sharedValue in file1: " << &sharedValue << std::endl;
}// 翻譯單元 2:address_file2.cpp
#include "address_example.h"
#include <iostream>void printAddress2() {std::cout << "Address of sharedValue in file2: " << &sharedValue << std::endl;
}// 主程序:main.cpp
#include <iostream>
#include "address_example.h"void printAddress1();
void printAddress2();int main() {printAddress1();printAddress2();return 0;
}
4.10、cv(const 與 volatile)類型限定符
mutable 說明符的主要用途是讓 const 成員函數能夠修改類的特定數據成員。它在實現緩存機制、引用計數等場景中非常有用,但在使用時需要謹慎,確保不會破壞代碼的邏輯和可維護性。
4.11、constexpr 說明符
constexpr說明符聲明可以在編譯時對實體求值。這些實體(給定了合適的函數實參的情況下)即可用于需要編譯期常量表達式的地方。
constexpr int a = 10;
int main() {int arr[a] = {};return 0;
}
如果函數或函數模板的一個聲明擁有 constexpr 說明符,那么它的所有聲明都必須含有該說明符。
// 第一次聲明,使用 constexpr 說明符
constexpr int add(int a, int b);// 定義函數,也必須使用 constexpr 說明符
constexpr int add(int a, int b) {return a + b;
}int main() {constexpr int result = add(2, 3);return 0;
}
當在對象聲明中使用 constexpr 說明符時,意味著該對象是一個編譯期常量,其值在編譯時就已經確定。同時,constexpr 蘊含了 const 的語義,即該對象是只讀的,不能被修改。
// 定義一個編譯期可求值的函數
constexpr int square(int x) {return x * x;
}
int main() {// 使用 constexpr 聲明對象constexpr int num = square(5);// 嘗試修改 num,會導致編譯錯誤//num = 10; std::cout << "Square of 5: " << num << std::endl;return 0;
}
如果square函數不加constexpr關鍵字,會導致編譯錯誤。這是因為constexpr變量(如constexpr int num)必須在編譯期完全確定其值,而普通函數(非 constexpr)的調用通常在運行時進行。
函數或靜態數據成員(C++17 起)首個聲明中的 constexpr 說明符蘊含 inline。
- constexpr函數在首個聲明中使用constexpr說明符時,隱含inline性質,這意味著編譯器可以在多個翻譯單元中對該函數進行內聯展開,以提高程序的執行效率。同時,constexpr函數仍然保持其在編譯時求值的特性,能夠在常量表達式中被使用。
- 對于類的靜態數據成員,在其首個聲明中使用constexpr說明符時也隱含inline。這允許在多個翻譯單元中對該靜態數據成員進行定義和初始化,而不會引發重復定義錯誤。每個翻譯單元中都有該靜態數據成員的一份相同的拷貝,并且在編譯時就確定其值。
https://zh.cppreference.com/w/cpp/language/constexpr
4.12、decltype 說明符
檢查實體的聲明類型,或表達式的類型和值類別。
語法如下:
- decltype ( 實體 )
- decltype ( 表達式 )
decltype有幾個使用注意點:
- 如果decltype的實參是沒有括號的標識表達式,或沒有括號的類成員訪問表達式,那么decltype產生該表達式指名的實體的類型
#include <iostream>int main() {int num = 10;// num 是未加括號的標識表達式decltype(num) anotherNum = 20;std::cout << "Type of anotherNum: " << typeid(anotherNum).name() << std::endl;return 0;
}
- 如果實參是類型為 T 的任何其他表達式
- 表達式值類別是亡值,則 decltype 產生 T&&;
- 表達式 的值類別是左值,則 decltype 產生 T&
- 表達式 的值類別是純右值,則 decltype 產生 T
如何理解呢?
decltype 的設計目的是精確地反映表達式的類型信息,包括值類別(左值、右值等)。
- 對于左值表達式,decltype 推導出引用類型主要基于以下幾個原因:左值表示有名字、可以取地址的對象,decltype 推導出左值引用類型 T& 是為了保留左值的可修改性和可尋址性。
int a = 20;// a 是左值表達式decltype(a) b = 30; // b 的類型是 intdecltype((a)) c = a; // (a) 也是左值表達式,c 的類型是 int&c = 40; // 修改 c 會影響 a
- 對于純右值表達式,純右值通常表示臨時對象或者字面量,它們沒有持久的存儲位置,decltype 推導出非引用類型 T 是符合其語義的。
int add(int x, int y) {return x + y;
}int main() {// add(1, 2) 是純右值表達式decltype(add(1, 2)) result = add(1, 2); std::cout << "result: " << result << std::endl;return 0;
}
- 對于亡值表達式,decltype 產生 T&&,亡值通常表示資源可以被移動的對象,decltype 推導出右值引用類型 T&& 是為了支持移動語義。移動語義允許在對象所有權轉移時避免不必要的復制操作。
int main() {int x = 10;// 使用 std::move 將左值 x 轉換為亡值decltype(std::move(x)) y = std::move(x); std::cout << "x: " << x << std::endl; // x 的值可能被移動走std::cout << "y: " << y << std::endl;return 0;
}
4.13、占位類型說明符 auto
常用情境:
- 迭代器聲明:在使用容器(如 std::vector、std::map 等)進行迭代時,迭代器的類型往往比較冗長。使用 auto 可以避免手動書寫復雜的迭代器類型。
- 復雜類型推導:當變量的類型非常復雜時,例如模板實例化類型、lambda 表達式的類型等,使用 auto 可以讓代碼更加簡潔。
- 范圍 for 循環
- 函數返回值類型推導(C++14 及以后)
4.14、typedef、using以及define
typedef是為現有類型創建別名(類型重命名),語法是typedef 原類型 別名。
typedef 名是既存類型的別名,而不是對新類型的聲明。用 typedef 定義一個無名類或枚舉時,首個 typedef 名會被視為該類型的 “官方名稱”,用于鏈接時識別類型。
typedef unsigned long ulong;
typedef int int_t, *intp_t, (&fp)(int, ulong), arr_t[10];
typedef struct {int a; int b;} S, *pS;template<class T>
struct add_const
{typedef const T type;
};
using有兩個作用:
- 為現有類型創建別名(類型別名聲明),using 別名 = 原類型
- 引入命名空間或基類成員,參考上面4.3節
using func = void (*) (int, int);// 別名模板
template<class T>
using ptr = T*;
// 名字 'ptr<T>' 現在是指向 T 的指針的別名
ptr<int> x;
#define(預處理指令)用于創建宏定義(文本替換),#define 標識符 替換文本。
typedef和using是編譯時的類型別名,受作用域限制,只在當前作用域內有效,不會影響其他文件,被編譯器視為完整類型,參與類型檢查。#define是預處理階段的文本替換,沒有作用域概念,不參與類型檢查,只是簡單的文本替換。
4.15、屬性說明符序列
屬性(Attributes)是一種為程序實體(像函數、類、變量等)添加額外信息的機制,編譯器可以利用這些信息來做優化或者給出特定的行為。屬性的應用位置不同,其作用的對象也會不同。
語法:
- [[屬性列表]]
- [[ using 屬性命名空間 : 屬性列表 ]]
- 屬性列表是由逗號分隔的零或更多個屬性的序列
屬性既可以在整個聲明之前出現,也可以直接跟在被聲明實體的名字之后,大多數其他情形中,屬性應用于直接位于其之前的實體。
標準屬性:
- [[noreturn]]:指示函數不返回,通常用于那些會導致程序終止或者無限循環的函數
[[noreturn]] void fatalError(const char* message) {std::cerr << "Fatal error: " << message << std::endl;std::exit(1);
}
- [[deprecated]]:以此屬性聲明的名字或實體,允許使用但因某種原因而不鼓勵使用
// 使用 [[deprecated]] 標記的函數
[[deprecated("This function is deprecated, use newFunction instead.")]]
int oldFunction() {return 1;
}int newFunction() {return 2;
}int main() {// 使用被標記為 deprecated 的函數,編譯器可能會發出警告int result = oldFunction();std::cout << "Result from oldFunction: " << result << std::endl;// 使用新函數result = newFunction();std::cout << "Result from newFunction: " << result << std::endl;return 0;
}
- [[fallthrough]]:從前一 case 標號的直落是故意的,且會警告直落的編譯器不應當對此診斷
- [[maybe_unused]]:抑制對于未使用實體的編譯器警告
// 使用 [[maybe_unused]] 標記的函數參數
void someFunction([[maybe_unused]] int arg) {// 函數體中沒有使用 arg 參數
}int main() {[[maybe_unused]] int unusedVariable = 10;someFunction(20);return 0;
}
- [[nodiscard]]:鼓勵編譯器在返回值被丟棄時發出警告
// 使用 [[nodiscard]] 標記的函數
[[nodiscard]] int calculateValue() {return 42;
}int main() {// 丟棄返回值,編譯器可能會發出警告calculateValue();// 保存返回值,不會有警告int result = calculateValue();std::cout << "Result: " << result << std::endl;return 0;
}
4.16、alignas 說明符
alignas( 表達式 )
alignas( 類型標識 )
alignas( 包名 … )
alignas 說明符可用于:
- 類的聲明或定義;
- 非位域類數據成員的聲明;
- 變量聲明,但它不能應用于下列內容:
- 函數形參;
- catch 子句的異常形參。
struct alignas(float) struct_float
{// 定義在此
};// sse_t 類型的每個對象將對齊到 32 字節邊界
struct alignas(32) sse_t
{float sse_data[4];
};
4.17、static_assert 聲明
進行編譯時斷言檢查。
static_assert( 布爾常量表達式 , 不求值字符串 )
static_assert( 布爾常量表達式 )
static_assert( 布爾常量表達式 , 常量表達式 )