C++11:系統類型增強
- 強枚舉類型
- 作用域限定
- 隱式類型轉換
- 指定類型
- 前置聲明
- 類型別名 using
- 模板別名
- 復雜指針別名
- auto
- 限制性 auto
- 注意事項
- nullptr
- decltype
強枚舉類型
在C++98的枚舉設計中,存在很多缺陷,為此C++11推出了強枚舉
來代替舊版的枚舉,提供更加安全可靠的枚舉類型。
強枚舉的語法如下:
enum class e : type
{val1 = 1,val2 = 2
}
此處定義了一個名為e
的強枚舉,只需要在enum
后面加一個calss
關鍵字即可,此處的: type
用于指定底層類型,后面講解,可以省略。
作用域限定
舊版本的 enum
會直接把枚舉內部的值放到外層作用域,如果多個枚舉有一樣的值,或者在相同作用域定義了與枚舉同名的變量,那么就會導致命名沖突:
enum e1
{a = 1
};enum e2
{a = 2
};
以上代碼中,e1::a
和e2::a
發生了沖突,全局作用域存在兩個叫做a
的變量。
在強枚舉中,每個枚舉自成一個作用域,不會污染外部變量,相互之間也不會沖突。
enum class e1
{a = 1
};enum class e2
{a = 2
};
以上代碼就不會報錯了,因為e1
和e2
都是強類型枚舉,各自作用域獨立,枚舉也不會放到全局作用域。
由于枚舉值不在全局作用域了,那么也就不能直接訪問a
了,必須通過域限定符來訪問:e1::a
和 e2::a
。
隱式類型轉換
舊版本的 enum
可以隨意的隱式轉換成其他類型,這是從C語言繼承下來的極其不安全的特性,它可以轉化為任意整形家族,甚至char
,bool
,float
。
enum type_old
{a = 128
};int main()
{int x = a;signed char c = a;long long ll = a;float f = a;return 0;
}
以上代碼是合法的(在 vs2022
與 g++13.3
編譯均通過),這非常不安全,例如此處的 a = 128
,它其實是超出了 signed char
的范圍的,但是他不僅沒有報錯,而且還隱式的發生了截斷,此時如果再std::cout << (int)c
,你會得到-128
這個值。
強枚舉有非常嚴格的類型限定,他不能隱式轉化為強枚舉之外的類型。
enum class type_new
{a = 1
};int main()
{// err: 不允許隱式轉化int x = type_new::a;type_new e1 = 1;// success: 顯示類型轉換int y = static_cast<int>(type_new::a);type_new e2 = (type_new)1;// success: C風格顯示類型轉換int z = (int)type_new::a;type_new e3 = static_cast<type_new>(1);return 0;
}
使用強枚舉后,既不允許從其他類型隱式轉為強枚舉(哪怕這個值在枚舉中存在),也不允許強枚舉隱式轉為其他類型。如果需要轉換,那么必須用static_cast
或者C語言風格的顯式類型轉換。
指定類型
在舊版enum
中,其類型往往是不確定的,這可能隨著編譯器不同而變化,一般為int
。就算你傳入一個long long
類型,最后也會被轉回int
。
這是因為 在C++標準中規定:編譯器指定的枚舉底層類型,只要可以存儲所有的枚舉值即可。
例如你的枚舉值是1 2 3
,那么就有可能用short
這樣的來存儲,如果再大一點就可能是int
。不過就算C++標準這么規定,其實大部分主流編譯器都固定使用int
,不論你數據范圍是多少,例如MSVC
和gcc
。
enum type_old
{a = INT_MAX + 1ll // 此處 1ll 表示 long long 字面量,后綴ll不可省略
};int main()
{std::cout << a << std::endl;return 0;
}
以上代碼中,type_old::a
這個枚舉接受了一個大于int
最大值的long long
字面量,程序正常運行,最后輸出結果為:-2147483648
也就是int
的最小值,這是因為發生了從long long -> int
的隱式截斷,你無法在 C++98 的枚舉中存儲大于int
范圍的值。相應的,當你存儲的數據范圍比較小,比如枚舉值都是0 ~ 127
,你也不能用一個字節來存,必須用int
,(或者你可以祈禱某種編譯器檢測到了你的數據范圍比較小,給你改用小范圍的類型來存儲)。
在強枚舉中,可以指定枚舉值的底層類型:
enum class type_new : long long
{a = INT_MAX + 1ll
};int main()
{std::cout << static_cast<long long>(type_new::a) << std::endl;return 0;
}
代碼中: long long
指定了枚舉的底層使用long long
存儲,最后程序輸出:2147483648
,也就是INT_MAX + 1
。
要注意的是,如果你在C++11環境運行以下代碼,也是合法的:
enum type_new : long long // 此處把 class 刪掉了,是普通枚舉
{a = INT_MAX + 1ll
};
因為C++11在更新強枚舉的同時,對舊版枚舉也做了優化,允許普通枚舉也指定底層類型!但是普通枚舉作用域,隱式轉化等特性依然保留。
前置聲明
舊版枚舉是不允許前置聲明的,例如以下代碼會報錯:
enum type_old;void func(type_old e)
{
}enum type_old
{a = 128
};int main()
{func(a);return 0;
}
以上代碼在C++98環境運行會報錯,剛才說過,枚舉底層使用什么類型是不確定的,在不同編譯器可能不同,這就導致func
函數的第一個參數type_old
聲明后,無法確定其內存大小,從而編譯失敗。
在C++11中,其實強枚舉直接這么寫也會報錯,例如把上面的聲明改成:enum class type_new
這樣的強枚舉,還是會報錯。
根本原因是無法確定枚舉的底層變量,從而無法得知大小。因為C++標準明確說了編譯器可以自己來指定底層變量,在枚舉值還沒有定義之前,編譯器根本就無法推斷用什么類型來存,也就不知道這個枚舉的底層類型。
此時上一個特性就派上用場了,用戶可以自己顯式指定枚舉底層類型,那編譯器不就明確了枚舉的大小了么?
而剛才又說過,C++11對普通枚舉和強枚舉都支持指定底層類型,那么代碼就可以這樣改寫:
enum type_old : int;
enum type_new : int;void func(type_old e1, type_new e2)
{
}enum type_old : int
{a = 128
};enum type_new : int
{a = 128
};
現在不論強枚舉還是普通枚舉,都可以提前聲明了,因為通過: int
明確指定了底層使用int
存儲,那么func
的兩個參數就知道自己要給參數預留多少空間,此時前置聲明就有用了。
類型別名 using
在 C++11 之前,typedef
是定義類型別名的唯一方式,但它存在語法局限,尤其是在模板編程中不夠靈活。
在C++11之前,using
主要用于展開命名空間,或者聲明其它命名空間內部的變量。
C++11 給 using
添加了類型別名的功能,不僅替代了 typedef
,還提供了更強大的功能,特別是在模板別名和復雜類型表達式簡化方面。
語法如下:
using new_name = old_name;
模板別名
如果想給一系列模板類取別名,例如希望簡化list
的迭代器類型std::list<T>::iterator
變成list_it<T>
,使用typedef
是無法做到的,而using
就可以配合模板使用:
template<typename T>
using list_it = std::list<T>::iterator;int main()
{list_it<int> p; // successreturn 0;
}
這是typedef
無法做到的,也是using
最大的優勢。
復雜指針別名
在使用typedef
給函數指針或者數組指針取別名的時候,語法會很復雜,而且可讀性很差,例如:
// 把 void(*)(int, int) 類型的函數指針取別名為 func_ptr
typedef void(*func_ptr)(int, int);// 把 int(*)[] 類型的數組指針取別名為 arr_ptr
typedef int(*arr_ptr)[];
這是因為在typedef
中,要求新名稱必須寫在*
后面,這樣編譯器才知道這個新的名稱是一個指針,這就導致可讀性很差,例如一個帶有回調函數的函數指針取別名:
typedef void(*func_ptr)(void(*)(void), int);
此處func_ptr
的類型是void(*)(void(*)(void), int)
,如果C語言基礎差一些,這段代碼要琢磨一點時間。
在using
中,無需把新名稱寫到*
后面,就是固定寫在=
左邊,右邊就是原始類型,例如:
// 把 void(*)(int, int) 類型的函數指針取別名為 func_ptr
using func_ptr = void(*)(int, int);// 把 int(*)[] 類型的數組指針取別名為 arr_ptr
typedef arr_ptr = int(*)[];
這樣語義就明確很多了,程序員一下就看出來新名稱是什么,原始類型是什么。
auto
在C++中,auto
關鍵字可以用來自動推斷變量的類型,它在編譯時會根據初始化表達式的類型來確定變量的類型。
使用auto
的主要好處是可以簡化代碼并提高可讀性。它可以減少手動指定變量類型的工作,并且可以防止類型錯誤。相比于顯式指定變量類型,使用auto
可以讓代碼更加靈活和易于維護。
- 自動推斷基本類型變量的類型
auto age = 25; // 推斷age為int類型
auto salary = 5000.50; // 推斷salary為double類型
auto
也可以自動推斷指針的類型,比如這樣:
int x = 10;
auto y = &x;
此時y
的類型自動判別為int*
。
實際上不建議這么做,C++中最好還是明確每個變量的類型,對于這種簡單的類型還是不要用auto
的好。
- 自動推斷非常長的類型
std::vector<int> numbers = {1, 2, 3, 4, 5};for (auto it = numbers.begin(); it != numbers.end(); ++it)
{std::cout << *it << " ";
}
有的時候獲得變量的類型會需要很長的代碼,使用auto
可以縮短變量類型的長度,這是C++推薦的做法,當然用戶心里還是要清楚auto
最后接收到了什么類型,只是懶得寫出來而已。
- 接受不確定的類型
auto add = [](int a, int b){ return a + b; };
在lambda
表達式中,返回值類型是不確定的,必須用auto
接收。這是因為lambda
設計出來,就是只用一次就不再用的匿名函數,那么用戶就無需知道這個表達式的類型,因為拿到類型就可以再去定義相同的函數了,那就不要用lambda
,直接寫一個函數/仿函數就行了。因此C++中lambda
的類型是隨機生成的,必須用auto
才能接收。
限制性 auto
除去基本的類型推斷,auto
可以限制接收到的類型必須是指針或引用。
看到一段代碼:
int x = 10;auto* a1 = x;
auto* a2 = &x;
auto a3 = &x;
在auto* a1 = x;
中,x的類型是int,那么auto本應將其值判別為int,但是由于auto*
被*
限制了,此時auto
必須得到一個指針,所以編譯器會報錯;而auto* a2 = &x;
得到的就是指針,此時代碼不會報錯,可以正常識別為int*
。
在本質上auto* a2 = &x;
和auto a3 = &x;
的結果是沒有區別的,只是auto*
要求得到的必須是一個指針類型,而auto
不限制其類型。
同理auto&
也可以限定類型必須是一個引用,否則會報錯。
注意事項
auto
不能作為函數的參數auto
不能用于聲明數組
比如以下代碼:
int arr1[] = {1, 3, 5, 7, 9};
auto arr2[] = {1, 3, 5, 7, 9};
此時第二條代碼就會報錯,因為其用auto
類型定義了一個數組。
- 在同一行定義多個變量時,如果將
auto
作為其類型,必須一整行都是同一個類型的變量。
比如以下代碼:
int x = 1, y = 2;
auto a = 3, b = 4;
auto c = 5, d = 6.0;
以上代碼中,auto a = 3, b = 4;
是合法的,因為一行內都是int類型。
但是auto c = 5, d = 6.0;
是非法的,因為同一行內有不同類型,會報錯。
nullptr
在C++11后,推出了新的空指針nullptr
,明明已經有NULL
了,為啥還需要nullptr
?
NULL
在C語言中,表示的是((void*)0)
,也就是被強制轉為void*
類型的0。但是在C++中,NULL
就是整數0
比如可以用剛才學的typeid
驗證一下:
cout << typeid(NULL).name() << endl;
輸出結果為:int
,這下就石錘了NULL
在C++中就是int
。
這會導致不少問題,比如這樣:
void func(int x)
{cout << "參數為整型" << endl;
}void func(void* x)
{cout << "參數為指針" << endl;
}int main()
{func(NULL);return 0;
}
以上代碼中,func
函數有兩個重載,一個是參數為指針,一個是參數為整型。我現在就是想傳一個空指針去調用指針版本的func
。但是最后還是會調用int
類型的。
而nullptr
不一樣,nullptr
不僅不是整型,而且其也不是void*
。C++給了nullptr
一個專屬類型nullptr_t
。這個類型有一個非常非常大的優勢,該類型只能轉化為其它指針類型,不能轉化為指針以外的類型。
比如以下代碼:
int x1 = NULL;//正確
int x2 = nullptr;//錯誤
因為NULL
本質是0,其可以轉化為很多非指針類型,比如int
,double
,char
。但是nullptr
是nullptr_t
,它只能轉化為其他指針。上述代碼中,我們把nullptr
轉化為一個int
,此時編譯器會直接報錯,絕對禁止這個行為。
但是這樣是可以的:
void* p1 = nullptr;
int* p2 = nullptr;
char* p3 = nullptr;
double* p4 = nullptr;
可以看到,nullptr
保證了指針類型的穩定,空指針不會被傳遞到指針以外的類型。因此nullptr
在各方面都有足夠的優勢,以更加安全的形式給用戶提供空指針。
decltype
在C++11以前,有一個關鍵字typeid
,其可以識別一個類型,并且可以通過name
成員函數來輸出類型名。
比如這樣:
int i = 0;
int* pi = &i;cout << typeid(i).name() << endl;
cout << typeid(pi).name() << endl;
輸出結果為:
int
int * __ptr64
也就是說,我們可以通過typeid
來檢測甚至輸出變量類型。
而decltype
也是用于識別類型的,但是decltype
與typeid
應用方向不同。
decltype
可以檢測一個變量的類型,并且拿這個類型去聲明新的類型
比如這樣:
int i = 0;
decltype(i) x = 5;
decltype(i)
檢測出i
的類型為int
,于是decltype(i)
整體就變成int
,從而定義出一個新的變量x
。
auto
和 decltype
的區別在于,decltype
聲明變量可以無需初始化。
int a;
decltype(a) b; // success
auto c = a; // success
auto d; // error
decltype
還可以作為模板參數
例如把lambda
傳給std::priority_queue
作為比較條件:
auto comp = [](const std::string& a, const std::string& b) {return a.size() >= b.size();};std::priority_queue<std::string, std::deque<std::string>, decltype(comp)> q(comp);