跳转至

變量

數據類型

C++ 的類型系統由如下幾部分組成:

  1. 基礎類型(括號內為代表關鍵詞/代表類型)
    1. 無類型/void 型 (void)
    2. (C++11 起)空指針類型 (std::nullptr_t)
    3. 算術類型
      1. 整數類型 (int)
      2. 布爾類型/bool 型 (bool)
      3. 字符類型 (char)
      4. 浮點類型 (float,double)
  2. 複合類型2

布爾類型

一個 bool 類型的變量取值只可能為兩種:truefalse

一般情況下,一個 bool 類型變量佔有 \(1\) 字節(一般情況下,\(1\) 字節 =\(8\) 位)的空間。

C 語言的布爾類型

C 語言最初是沒有布爾類型的,直到 C99 時才引入 _Bool 關鍵詞作為布爾類型,其被視作無符號整數類型。

Note

C 語言的 bool 類型從 C23 起不再使用整型的零與非零值定義,而是定義為足夠儲存 truefalse 兩個常量的類型。

為方便使用,stdbool.h 中提供了 bool,true,false 三個宏,定義如下:

1
2
3
#define bool _Bool
#define true 1
#define false 0

這些宏於 C23 中移除,並且 C23 起引入 true,falsebool 作為關鍵字,同時保留 _Bool 作為替代拼寫形式1

整數類型

用於存儲整數。最基礎的整數類型是 int.

注意

由於歷史原因,C++ 中布爾類型和字符類型會被視作特殊的整型。

在幾乎所有的情況下都 不應該 將除 signed charunsigned char 之外的字符類型作為整型使用。

整數類型一般按位寬有 5 個梯度:char,short,int,long,long long.

C++ 標準保證 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long) <= sizeof(long long)

由於歷史原因,整數類型的位寬有多種流行模型,為解決這一問題,C99/C++11 引入了 定寬整數類型

int 類型的大小

在 C++ 標準中,規定 int 的位數 至少\(16\) 位。

事實上在現在的絕大多數平台,int 的位數均為 \(32\) 位。

對於 int 關鍵字,可以使用如下修飾關鍵字進行修飾:

符號性:

  • signed:表示帶符號整數(默認);
  • unsigned:表示無符號整數。

大小:

  • short:表示 至少 \(16\) 位整數;
  • long:表示 至少 \(32\) 位整數;
  • (C++11 起)long long:表示 至少 \(64\) 位整數。

下表給出在 一般情況下,各整數類型的位寬和表示範圍大小(少數平台上一些類型的表示範圍可能與下表不同):

類型名 等價類型 位寬(C++ 標準) 位寬(常見) 位寬(較罕見)
signed char signed char \(8\) - -
unsigned char unsigned char \(8\) - -
short,short int,signed short,signed short int short int \(\geq 16\) \(16\) -
unsigned short,unsigned short int unsigned short int \(\geq 16\) \(16\) -
int,signed,signed int int \(\geq 16\) \(32\) \(16\)(常見於 Win16 API)
unsigned,unsigned int unsigned int \(\geq 16\) \(32\) \(16\)(常見於 Win16 API)
long,long int,signed long,signed long int long int \(\geq 32\) \(32\) \(64\)(常見於 64 位 Linux、macOS)
unsigned long,unsigned long int unsigned long int \(\geq 32\) \(32\) \(64\)(常見於 64 位 Linux、macOS)
long long,long long int,signed long long,signed long long int long long int \(\geq 64\) \(64\) -
unsigned long long,unsigned long long int unsigned long long int \(\geq 64\) \(64\) -

當位寬為 \(x\) 時,有符號類型的表示範圍為 \(-2^{x-1}\sim 2^{x-1}-1\), 無符號類型的表示範圍為 \(0 \sim 2^x-1\). 具體而言,有下表:

位寬 表示範圍
\(8\) 有符號:\(-2^{7}\sim 2^{7}-1\), 無符號:\(0 \sim 2^{8}-1\)
\(16\) 有符號:\(-2^{15}\sim 2^{15}-1\), 無符號:\(0 \sim 2^{16}-1\)
\(32\) 有符號:\(-2^{31}\sim 2^{31}-1\), 無符號:\(0 \sim 2^{32}-1\)
\(64\) 有符號:\(-2^{63}\sim 2^{63}-1\), 無符號:\(0 \sim 2^{64}-1\)
等價的類型表述

在不引發歧義的情況下,允許省略部分修飾關鍵字,或調整修飾關鍵字的順序。這意味着同一類型會存在多種等價表述。

例如 intsignedint signedsigned int 表示同一類型,而 unsigned longunsigned long int 表示同一類型。

另外,一些編譯器實現了擴展整數類型,如 GCC 實現了 128 位整數:有符號版的 __int128_t 和無符號版的 __uint128_t,如果您在比賽時想使用這些類型,請仔細閲讀比賽規則 以確定是否允許或支持使用擴展整數類型。

注意

STL 不一定對擴展整數類型有足夠的支持,故使用擴展整數類型時需格外小心。

示例代碼
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <cmath>
#include <iostream>

int f1(int n) {
  return abs(n);  // Good
}

int f2(int n) {
  return std::abs(n);  // Good
}

__int128_t f3(__int128_t n) {
  return abs(n);  // Bad
}

// Wrong
// __int128_t f4(__int128_t n) {
//   return std::abs(n);
// }

int main() {
  std::cout << "f1: " << f1(-42) << std::endl;
  std::cout << "f2: " << f2(-42) << std::endl;
  // std::cout << "f3: " << f3(-42) << std::endl; // Wrong
  // std::cout << "f4: " << f4(-42) << std::endl; // Wrong
  return 0;
}

以上示例代碼存在如下問題:

  1. __int128_t f3(__int128_t) 中使用的是 C 風格的絕對值函數,其簽名為 int abs(int),故 n 首先會強制轉換為 int,然後才會調用 abs 函數。
  2. __int128_t f4(__int128_t) 中使用的是 C++ 風格的絕對值函數,其並沒有簽名為 __int128_t std::abs(__int128_t) 的函數重載,所以無法通過編譯。
  3. C++ 的流式輸出不支持 __int128_t__uint128_t

以下是一種解決方案:

修正後的代碼
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <cmath>
#include <iostream>

__int128_t abs(__int128_t n) { return n < 0 ? -n : n; }

std::ostream &operator<<(std::ostream &os, __uint128_t n) {
  if (n > 9) os << n / 10;
  os << (int)(n % 10);
  return os;
}

std::ostream &operator<<(std::ostream &os, __int128_t n) {
  if (n < 0) {
    os << '-';
    n = -n;
  }
  return os << (__uint128_t)n;
}

int f1(int n) { return abs(n); }

int f2(int n) { return std::abs(n); }

__int128_t f3(__int128_t n) { return abs(n); }

int main() {
  std::cout << "f1: " << f1(-42) << std::endl;
  std::cout << "f2: " << f2(-42) << std::endl;
  std::cout << "f3: " << f3(-42) << std::endl;
}

字符類型

分為「窄字符類型」和「寬字符類型」,由於算法競賽幾乎不會用到寬字符類型,故此處僅介紹窄字符類型。

窄字符型位數一般為 \(8\) 位,實際上底層存儲方式仍然是整數,一般通過 ASCII 編碼 實現字符與整數的一一對應,有如下三種:

  • signed char:有符號字符表示的類型,表示範圍在 \(-128 \sim 127\) 之間。
  • unsigned char:無符號字符表示的類型,表示範圍在 \(0 \sim 255\) 之間。
  • char 擁有與 signed charunsigned char 之一相同的表示和對齊,但始終是獨立的類型。

    char 的符號性取決於編譯器和目標平台:ARM 和 PowerPC 的默認設置通常沒有符號,而 x86 與 x64 的默認設置通常有符號。

    GCC 可以在編譯參數中添加 -fsigned-char-funsigned-char 指定將 char 視作 signed charunsigned char,其他編譯器請參照文檔。需要注意指定與架構默認值不同的符號有可能會破壞 ABI,造成程序無法正常工作。

注意

與其他整型不同,charsigned charunsigned char三種不同的類型

一般來説 signed char,unsigned char 不應用來存儲字符,絕大多數情況下,這兩種類型均被視作整數類型。

浮點類型

用於存儲「實數」(注意並不是嚴格意義上的實數,而是實數在一定規則下的近似),包括以下三種:

  • float:單精度浮點類型。如果支持就會匹配 IEEE-754 binary32 格式。
  • double:雙精度浮點類型。如果支持就會匹配 IEEE-754 binary64 格式。
  • long double:擴展精度浮點類型。如果支持就會匹配 IEEE-754 binary128 格式,否則如果支持就會匹配 IEEE-754 binary64 擴展格式,否則匹配某種精度優於 binary64 而值域至少和 binary64 一樣好的非 IEEE-754 擴展浮點格式,否則匹配 IEEE-754 binary64 格式。
浮點格式 位寬 最大正數 精度位數
IEEE-754 binary32 格式 \(32\) \(3.4\times 10^{38}\) \(6\sim 9\)
IEEE-754 binary64 格式 \(64\) \(1.8\times 10^{308}\) \(15\sim 17\)
IEEE-754 binary64 擴展格式 \(\geq 80\) \(\geq 1.2\times 10^{4932}\) \(\geq 18\sim 21\)
IEEE-754 binary128 格式 \(128\) \(1.2\times 10^{4932}\) \(33\sim 36\)

IEEE-754 浮點格式的最小負數是最大正數的相反數。

因為 float 類型表示範圍較小,且精度不高,實際應用中常使用 double 類型表示浮點數。

另外,浮點類型可以支持一些特殊值:

  • 無窮(正或負):INFINITY.
  • 負零:-0.0,例如 1.0 / 0.0 == INFINITY,1.0 / -0.0 == -INFINITY.
  • 非數(NaN):std::nan,NAN,一般可以由 0.0 / 0.0 之類的運算產生。它與任何值(包括自身)比較都不相等,C++11 後可以 使用 std::isnan 判斷一個浮點數是不是 NaN.

無類型

void 類型為無類型,與上面幾種類型不同的是,不能將一個變量聲明為 void 類型。但是函數的返回值允許為 void 類型,表示該函數無返回值。

空指針類型

請參閲指針的 對應章節

定寬整數類型

C++11 起提供了定寬整數的支持,具體如下:

  • <cstdint>:提供了若干定寬整數的類型和各定寬整數類型最大值、最小值等的宏常量。
  • <cinttypes>:為定寬整數類型提供了用於 std::fprintf 系列函數和 std::fscanf 系列函數的格式宏常量。

定寬整數有如下幾種:

  • intN_t: 寬度 恰為 \(N\) 位的有符號整數類型,如 int32_t.
  • int_fastN_t: 寬度 至少\(N\) 位的 最快的 有符號整數類型,如 int_fast32_t.
  • int_leastN_t: 寬度 至少\(N\) 位的 最小的 有符號整數類型,如 int_least32_t.

無符號版本只需在有符號版本前加一個字母 u 即可,如 uint32_t,uint_least8_t.

標準規定必須實現如下 16 種類型:

int_fast8_t,int_fast16_t,int_fast32_t,int_fast64_t,

int_least8_t,int_least16_t,int_least32_t,int_least64_t,

uint_fast8_t,uint_fast16_t,uint_fast32_t,uint_fast64_t,

uint_least8_t,uint_least16_t,uint_least32_t,uint_least64_t.

絕大多數編譯器在此基礎上都實現瞭如下 8 種類型:

int8_t,int16_t,int32_t,int64_t,

uint8_t,uint16_t,uint32_t,uint64_t.

在實現了對應類型的情況下,C++ 標準規定必須實現表示對應類型的最大值、最小值、位寬的宏常量,格式為將類型名末尾的 _t 去掉後轉大寫並添加後綴:

  • _MAX 表示最大值,如 INT32_MAX 即為 int32_t 的最大值。
  • _MIN 表示最小值,如 INT32_MIN 即為 int32_t 的最小值。
注意

定寬整數類型本質上是普通整數類型的類型別名,所以混用定寬整數類型和普通整數類型可能會影響跨平台編譯,例如:

示例代碼
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#include <algorithm>
#include <cstdint>
#include <iostream>

int main() {
  long long a;
  int64_t b;
  std::cin >> a >> b;
  std::cout << std::max(a, b) << std::endl;
  return 0;
}

int64_t 在 64 位 Windows 下一般為 long long int, 而在 64 位 Linux 下一般為 long int, 所以這段代碼在使用 64 位 Linux 下的 GCC 時不能通過編譯,而使用 64 位 Windows 下的 MSVC 時可以通過編譯,因為 std::max 要求輸入的兩個參數類型必須相同。

此外,C++17 起在 <limits> 中提供了 std::numeric_limits 類模板,用於查詢各種算數類型的屬性,如最大值、最小值、是否是整形、是否有符號等。

1
2
3
4
5
6
7
8
9
#include <cstdint>
#include <limits>

std::numeric_limits<int32_t>::max();  // int32_t 的最大值, 2'147'483'647
std::numeric_limits<int32_t>::min();  // int32_t 的最小值, -2'147'483'648

std::numeric_limits<double>::min();  // double 的最小值, 約為 2.22507e-308
std::numeric_limits<double>::epsilon();  // 1.0 與 double 的下個可表示值的差,
                                         // 約為 2.22045e-16

類型轉換

在一些時候(比如某個函數接受 int 類型的參數,但傳入了 double 類型的變量),我們需要將某種類型,轉換成另外一種類型。

C++ 中類型的轉換機制較為複雜,這裏主要介紹對於基礎數據類型的兩種轉換:數值提升和數值轉換。

數值提升

數值提升過程中,值本身保持不變。

Note

C 風格的可變參數域在傳值過程中會進行默認參數提升。如:

示例代碼
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdarg.h>
#include <stdio.h>

void test(int tot, ...) {
  va_list valist;
  int i;

  // 初始化可變參數列表
  va_start(valist, tot);

  for (i = 0; i < tot; ++i) {
    // 獲取第 i 個變量的值
    double xx = va_arg(valist, double);  // Correct
    // float xx = va_arg(valist, float); // Wrong

    // 輸出第 i 個變量的底層存儲內容
    printf("i = %d, value = 0x%016llx\n", i, *(long long *)(&xx));
  }

  // 清理可變參數列表的內存
  va_end(valist);
}

int main() {
  float f;
  double fd, d;
  f = 123.;   // 0x42f60000
  fd = 123.;  // 0x405ec00000000000
  d = 456.;   // 0x407c800000000000
  test(3, f, fd, d);
}

在調用 test 時,f 提升為 double,從而底層存儲內容和 fd 相同,輸出為

1
2
3
i = 0, value = 0x405ec00000000000
i = 1, value = 0x405ec00000000000
i = 2, value = 0x407c800000000000

若將 double xx = va_arg(valist, double); 改為 float xx = va_arg(valist, float);,GCC 應該給出一條類似下文的警告:

1
2
3
4
5
6
7
In file included from test.c:2:
test.c: In function ‘test’:
test.c:14:35: warning: ‘float’ is promoted to ‘double’ when passed through ‘...’
  14 |         float xx = va_arg(valist, float);
     |                                   ^
test.c:14:35: note: (so you should pass ‘double’ not ‘float’ to ‘va_arg’)
test.c:14:35: note: if this code is reached, the program will abort

此時的程序將會在輸出前終止。

這一點也能解釋為什麼 printf%f 既能匹配 float 也能匹配 double

整數提升

小整數類型(如 char)的純右值可轉換成較大整數類型(如 int)的純右值。

具體而言,算術運算符不接受小於 int 的類型作為它的實參,而在左值到右值轉換後,如果適用就會自動實施整數提升。

具體地,有如下規則:

  • 源類型為 signed charsigned short / short 時,可提升為 int
  • 源類型為 unsigned charunsigned short 時,若 int 能保有源類型的值範圍,則可提升為 int,否則可提升為 unsigned int。(C++20char8_t 也適用本規則)
  • char 的提升規則取決於其底層類型是 signed char 還是 unsigned char
  • bool 類型可轉換到 intfalse 變為 0true 變為 1
  • 若目標類型的值範圍包含源類型,且源類型的值範圍不能被 intunsigned int 包含,則源類型可提升為目標類型。3
注意

char->short 不是數值提升,因為 char 要優先提升為 int / unsigned int,之後是 int / unsigned int->short,不滿足數值提升的條件。

如(以下假定 int 為 32 位,unsigned short 為 16 位,signed charunsigned char 為 8 位,bool 為 1 位)

  • (signed char)'\0' - (signed char)'\xff' 會先將 (signed char)'\0' 提升為 (int)0、將 (signed char)'\xff' 提升為 (int)-1, 再進行 int 間的運算,最終結果為 (int)1
  • (unsigned char)'\0' - (unsigned char)'\xff' 會先將 (unsigned char)'\0' 提升為 (int)0、將 (unsigned char)'\xff' 提升為 (int)255, 再進行 int 間的運算,最終結果為 (int)-255
  • false - (unsigned short)12 會先將 false 提升為 (int)0、將 (unsigned short)12 提升為 (int)12, 再進行 int 間的運算,最終結果為 (int)-12

浮點提升

位寬較小的浮點數可以提升為位寬較大的浮點數(例如 float 類型的變量和 double 類型的變量進行算術運算時,會將 float 類型變量提升為 double 類型變量),其值不變。

數值轉換

數值轉換過程中,值可能會發生改變。

注意

數值提升優先於數值轉換。如 bool->int 時是數值提升而非數值轉換。

整數轉換

  • 如果目標類型為位寬為 \(x\) 的無符號整數類型,則轉換結果是原值 \(\bmod 2^x\) 後的結果。

    • 若目標類型位寬大於源類型位寬:

      • 若源類型為有符號類型,一般情況下需先進行符號位擴展再轉換。

        • (short)-1(short)0b1111'1111'1111'1111)轉換為 unsigned int 類型時,先進行符號位擴展,得到 0b1111'1111'1111'1111'1111'1111'1111'1111,再進行整數轉換,結果為 (unsigned int)4'294'967'295(unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111)。
        • (short)32'767(short)0b0111'1111'1111'1111)轉換為 unsigned int 類型時,先進行符號位擴展,得到 0b0000'0000'0000'0000'0111'1111'1111'1111,再進行整數轉換,結果為 (unsigned int)32'767(unsigned int)0b0000'0000'0000'0000'0111'1111'1111'1111)。
      • 若源類型為無符號類型,則需先進行零擴展再轉換。

        如將 (unsigned short)65'535(unsigned short)0b1111'1111'1111'1111)轉換為 unsigned int 類型時,先進行零擴展,得到 0b0000'0000'0000'0000'1111'1111'1111'1111,再進行整數轉換,結果為 (unsigned int)65'535(unsigned int)0b0000'0000'0000'0000'1111'1111'1111'1111)。

    • 若目標類型位寬不大於源類型位寬,則需先截斷再轉換。

      如將 (unsigned int)4'294'967'295(unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111)轉換為 unsigned short 類型時,先進行截斷,得到 0b1111'1111'1111'1111,再進行整數轉換,結果為 (unsigned short)65'535(unsigned short)0b1111'1111'1111'1111)。

  • 如果目標類型為位寬為 \(x\) 的帶符號整數類型,則 一般情況下,轉換結果可以認為是原值 \(\bmod 2^x\) 後的結果。4

    例如將 (unsigned int)4'294'967'295(unsigned int)0b1111'1111'1111'1111'1111'1111'1111'1111)轉換為 short 類型時,結果為 (short)-1(short)0b1111'1111'1111'1111)。

  • 如果目標類型是 bool,則是 布爾轉換

  • 如果源類型是 bool,則 false 轉為對應類型的 0,true 轉為對應類型的 1。

浮點轉換

位寬較大的浮點數轉換為位寬較小的浮點數,會將該數舍入到目標類型下最接近的值。

浮點整數轉換

  • 浮點數轉換為整數時,會捨棄浮點數的全部小數部分。

    如果目標類型是 bool,則是 布爾轉換

  • 整數轉換為浮點數時,會舍入到目標類型下最接近的值。

    如果該值不能適應到目標類型中,那麼行為未定義。

    如果源類型是 bool,那麼 false 轉換為零,而 true 轉換為一。

布爾轉換

將其他類型轉換為 bool 類型時,零值轉換為 false,非零值轉換為 true

定義變量

簡單地説5,定義一個變量,需要包含類型説明符(指明變量的類型),以及要定義的變量名。

例如,下面這幾條語句都是變量定義語句。

1
2
3
int oi;
double wiki;
char org = 'c';

在目前我們所接觸到的程序段中,定義在花括號包裹的地方的變量是局部變量,而定義在沒有花括號包裹的地方的變量是全局變量。實際有例外,但是現在不必瞭解。

定義時沒有初始化值的全局變量會被初始化為 \(0\)。而局部變量沒有這種特性,需要手動賦初始值,否則可能引起難以發現的 bug。

變量作用域

作用域是變量可以發揮作用的代碼塊。

全局變量的作用域,自其定義之處開始6,至文件結束位置為止。

局部變量的作用域,自其定義之處開始,至代碼塊結束位置為止。

由一對大括號括起來的若干語句構成一個代碼塊。

1
2
3
4
5
6
7
int g = 20;  // 定義全局變量

int main() {
  int g = 10;         // 定義局部變量
  printf("%d\n", g);  // 輸出 g
  return 0;
}

如果一個代碼塊的內嵌塊中定義了相同變量名的變量,則內層塊中將無法訪問外層塊中相同變量名的變量。

例如上面的代碼中,輸出的 \(g\) 的值將是 \(10\)。因此為了防止出現意料之外的錯誤,請儘量避免局部變量與全局變量重名的情況。

常量

常量是固定值,在程序執行期間不會改變。

常量的值在定義後不能被修改。定義時加一個 const 關鍵字即可。

1
2
const int a = 2;
a = 3;

如果修改了常量的值,在編譯環節就會報錯:error: assignment of read-only variable‘a’

參考資料與註釋

  1. Working Draft, Standard for Programming Language C++
  2. 類型 - cppreference.com
  3. C 語言的 算術類型 - cppreference.com
  4. 基礎類型 - cppreference.com
  5. 定寬整數類型(C++11 起)- cppreference.com
  6. William Kahan (1 October 1997)."Lecture Notes on the Status of IEEE Standard 754 for Binary Floating-Point Arithmetic".
  7. 隱式轉換 - cppreference.com
  8. 聲明 - cppreference
  9. 作用域 - cppreference.com

  1. 參見 https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3054.pdf 

  2. 包括數組類型、引用類型、指針類型、類類型、函數類型等。由於本篇文章是面向初學者的,故不在本文做具體介紹。具體請參閲 類型 - cppreference.com 

  3. 不包含寬字符類型、位域和枚舉類型,詳見 整型轉換 - cppreference。 

  4. 自 C++20 起生效。C++20 前結果是實現定義的。詳見 整型轉換 - cppreference。 

  5. 定義一個變量時,除了類型説明符之外,還可以包含其他説明符。詳見 聲明 - cppreference。 

  6. 更準確的説法是 聲明點。