C++ 控制臺格式化打印技巧
下次當你為控制臺輸出的格式而苦惱時,請參考這篇文章及其速查表。
我寫文章主要是為了給自己寫文檔。我在編程時非常健忘,所以我經(jīng)常會寫下有用的代碼片段、特殊的特性,以及我使用的編程語言中的常見錯誤。這篇文章完全切合我最初的想法,因為它涵蓋了從 C++ 控制臺格式化打印時的常見用例。
像往常一樣,這篇文章帶有大量的例子。除非另有說明,代碼片段中顯示的所有類型和類都是 std
命名空間的一部分。所以當你閱讀這段代碼時,你必須在類型和類的前面加上using namespace std;
。當然,該示例代碼也可以在 GitHub 上找到。
面向?qū)ο蟮牧?/h3>
如果你曾經(jīng)用過 C++ 編程,你肯定使用過 cout。當你包含 <iostream>
時,ostream 類型的 cout
對象就進入了作用域。這篇文章的重點是 cout
,它可以讓你打印到控制臺,但這里描述的一般格式化對所有 ostream 類型的流對象都有效。ostream
對象是 basic_ostream
的一個實例,其模板參數(shù)為 char
類型。頭文件 <iosfwd>
是 <iostream>
的包含層次結(jié)構(gòu)的一部分,包含了常見類型的前向聲明。
類 basic_ostream
繼承于 basic_ios
,該類型又繼承于 ios_base
。在 cppreference.com 上你可以找到一個顯示不同類之間關(guān)系的類圖。
ios_base
類是所有 I/O 流類的基類。basic_ios
類是一個模板類,它對常見的字符類型進行了模板特化,稱為 ios
。因此,當你在標準 I/O 的上下文中讀到 ios
時,它是 basic_ios
的 char
類型的模板特化。
格式化流
一般來說,基于 ostream
的流有三種格式化的方法。
- 使用
ios_base
提供的格式標志。 - 在頭文件
<iomanip>
和<ios>
中定義的流修改函數(shù)。 - 通過調(diào)用
<<
操作符的 特定重載。
所有這些方法都有其優(yōu)點和缺點,通常取決于使用哪種方法的情況。下面顯示的例子混合使用所有這些方法。
右對齊
默認情況下,cout
占用的空間與要打印的數(shù)據(jù)所需的空間一樣大。為了使這種右對齊的輸出生效,你必須定義一個行允許占用的最大寬度。我使用格式標志來達到這個目的。
右對齊輸出的標志和寬度調(diào)整只適用于其后的行。
cout.setf(ios::right, ios::adjustfield);
cout.width(50);
cout << "This text is right justified" << endl;
cout << "This text is left justified again" << endl;
在上面的代碼中,我使用 setf
配置了右對齊的輸出。我建議你將位掩碼 ios::adjustfield
應(yīng)用于 setf
,這將使位掩碼指定的所有標志在實際的 ios::right
標志被設(shè)置之前被重置,以防止發(fā)生組合碰撞。
填充空白
當使用右對齊輸出時,默認情況下,空的地方會用空白字符填充。你可以通過使用 setfill
指定填充字符來改變它:
cout << right << setfill('.') << setw(30) << 500 << " pcs" << endl;
cout << right << setfill('.') << setw(30) << 3000 << " pcs" << endl;
cout << right << setfill('.') << setw(30) << 24500 << " pcs" << endl;
代碼輸出如下:
...........................500 pcs
..........................3000 pcs
.........................24500 pcs
組合
想象一下,你的 C++ 程序記錄了你的儲藏室?guī)齑妗2粫r地,你想打印一份當前庫存的清單。要做到這一點,你可以使用以下格式。
下面的代碼是左對齊和右對齊輸出的組合,使用點作為填充字符,可以得到一個漂亮的列表:
cout << left << setfill('.') << setw(20) << "Flour" << right << setfill('.') << setw(20) << 0.7 << " kg" << endl;
cout << left << setfill('.') << setw(20) << "Honey" << right << setfill('.') << setw(20) << 2 << " Glasses" << endl;
cout << left << setfill('.') << setw(20) << "Noodles" << right << setfill('.') << setw(20) << 800 << " g" << endl;
cout << left << setfill('.') << setw(20) << "Beer" << right << setfill('.') << setw(20) << 20 << " Bottles" << endl;
輸出:
Flour...............................0.70 kg
Honey..................................2 Glasses
Noodles..............................800 g
Beer..................................20 Bottles
打印數(shù)值
當然,基于流的輸出也能讓你輸出各種變量類型。
布爾型
boolalpha
開關(guān)可以讓你把布爾型的二進制解釋轉(zhuǎn)換為字符串:
cout << "Boolean output without using boolalpha: " << true << " / " << false << endl;
cout << "Boolean output using boolalpha: " << boolalpha << true << " / " << false << endl;
以上幾行產(chǎn)生的輸出結(jié)果如下:
Boolean output without using boolalpha: 1 / 0
Boolean output using boolalpha: true / false
地址
如果一個整數(shù)的值應(yīng)該被看作是一個地址,那么只需要把它投到 void*
就可以了,以便調(diào)用正確的重載。下面是一個例子:
unsigned long someAddress = 0x0000ABCD;
cout << "Treat as unsigned long: " << someAddress << endl;
cout << "Treat as address: " << (void*)someAddress << endl;
該代碼產(chǎn)生了以下輸出:
Treat as unsigned long: 43981
Treat as address: 0000ABCD
該代碼打印出了具有正確長度的地址。一個 32 位的可執(zhí)行文件產(chǎn)生了上述輸出。
整數(shù)
打印整數(shù)是很簡單的。為了演示,我使用 setf
和 setiosflags
來指定數(shù)字的基數(shù)。應(yīng)用流修改器 hex
/oct
也有同樣的效果。
int myInt = 123;
cout << "Decimal: " << myInt << endl;
cout.setf(ios::hex, ios::basefield);
cout << "Hexadecimal: " << myInt << endl;
cout << "Octal: " << resetiosflags(ios::basefield) << setiosflags(ios::oct) << myInt << endl;
注意: 默認情況下,沒有指示所使用的基數(shù),但你可以使用 showbase
添加一個。
Decimal: 123
Hexadecimal: 7b
Octal: 173
用零填充
0000003
0000035
0000357
0003579
你可以通過指定寬度和填充字符得到類似上述的輸出:
cout << setfill('0') << setw(7) << 3 << endl;
cout << setfill('0') << setw(7) << 35 << endl;
cout << setfill('0') << setw(7) << 357 << endl;
cout << setfill('0') << setw(7) << 3579 << endl;
浮點值
如果我想打印浮點數(shù)值,我可以選擇“固定”和“科學(xué)”格式。此外,我還可以指定精度:
double myFloat = 1234.123456789012345;
int defaultPrecision = cout.precision(); // == 2
cout << "Default precision: " << myFloat << endl;
cout.precision(4);
cout << "Modified precision: " << myFloat << endl;
cout.setf(ios::scientific, ios::floatfield);
cout << "Modified precision & scientific format: " << myFloat << endl;
/* back to default */
cout.precision(defaultPrecision);
cout.setf(ios::fixed, ios::floatfield);
cout << "Default precision & fixed format: " << myFloat << endl;
上面的代碼產(chǎn)生以下輸出:
Default precision: 1234.12
Modified precision: 1234.1235
Modified precision & scientific format: 1.2341e+03
Default precision & fixed format: 1234.12
時間和金錢
通過 put_money
,你可以用正確的、與當?shù)赜嘘P(guān)的格式來打印貨幣單位。這需要你的控制臺能夠輸出 UTF-8 字符集。請注意,變量 specialOffering
以美分為單位存儲貨幣價值。
long double specialOffering = 9995;
cout.imbue(locale("en_US.UTF-8"));
cout << showbase << put_money(specialOffering) << endl;
cout.imbue(locale("de_DE.UTF-8"));
cout << showbase << put_money(specialOffering) << endl;
cout.imbue(locale("ru_RU.UTF-8"));
cout << showbase << put_money(specialOffering) << endl;
ios
的 imbue
方法讓你指定一個地區(qū)。通過命令 locale -a
,你可以得到你系統(tǒng)中所有可用的地區(qū)標識符的列表。
$99.95
99,950€
99,950₽
(不知道出于什么原因,在我的系統(tǒng)上,它打印的歐元和盧布有三個小數(shù)位,對我來說看起來很奇怪,但這也許是官方的格式。)
同樣的原則也適用于時間輸出。函數(shù) put_time
可以讓你以相應(yīng)的地區(qū)格式打印時間。此外,你可以指定時間對象的哪些部分被打印出來。
time_t now = time(nullptr);
tm localtm = *localtime(&now);
cout.imbue(locale("en_US.UTF-8"));
cout << "en_US : " << put_time(&localtm, "%c") << endl;
cout.imbue(locale("de_DE.UTF-8"));
cout << "de_DE : " << put_time(&localtm, "%c") << endl;
cout.imbue(locale("ru_RU.UTF-8"));
cout << "ru_RU : " << put_time(&localtm, "%c") << endl;
格式指定符 %c
會打印一個標準的日期和時間字符串:
en_US : Tue 02 Nov 2021 07:36:36 AM CET
de_DE : Di 02 Nov 2021 07:36:36 CET
ru_RU : Вт 02 ноя 2021 07:36:36
創(chuàng)建自定義的流修改器
你也可以創(chuàng)建你自己的流。下面的代碼在應(yīng)用于 ostream
對象時插入了一個預(yù)定義的字符串:
ostream& myManipulator(ostream& os) {
string myStr = ">>>Here I am<<<";
os << myStr;
return os;
}
另一個例子: 如果你有重要的事情要說,就像互聯(lián)網(wǎng)上的大多數(shù)人一樣,你可以使用下面的代碼在你的信息后面根據(jù)重要程度插入感嘆號。重要程度被作為一個參數(shù)傳遞:
struct T_Importance {
int levelOfSignificance;
};
T_Importance importance(int lvl){
T_Importance x = {.levelOfSignificance = lvl };
return x;
}
ostream& operator<<(ostream& __os, T_Importance t){
for(int i = 0; i < t.levelOfSignificance; ++i){
__os.put('!');
}
return __os;
}
這兩個修飾符現(xiàn)在都可以簡單地傳遞給 cout
:
cout << "My custom manipulator: " << myManipulator << endl;
cout << "I have something important to say" << importance(5) << endl;
產(chǎn)生以下輸出:
My custom manipulator: >>>Here I am<<<
I have something important to say!!!!!
結(jié)語
下次你再糾結(jié)于控制臺輸出格式時,我希望你記得這篇文章及其 速查表。
在 C++ 應(yīng)用程序中,cout
是 printf 的新鄰居。雖然使用 printf
仍然有效,但我可能總是喜歡使用 cout
。特別是與定義在 <ios>
中的修改函數(shù)相結(jié)合,會產(chǎn)生漂亮的、可讀的代碼。