第 9 章 文件系統
該書采用 Creative Commons License 授權
9.1. 概述
庫 Boost.Filesystem 簡化了處理文件和目錄的工作。 它提供了一個名為 boost::filesystem::path
的類,可以對路徑進行處理。 另外,還有多個函數用於創建目錄或驗證某個給定文件的有效性。
9.2. 路徑
boost::filesystem::path
是 Boost.Filesystem 中的核心類,它表示路徑的信息,並提供了處理路徑的方法。
實際上,boost::filesystem::path
是 boost::filesystem::basic_path<std::string>
的一個 typedef
。 此外還有一個 boost::filesystem::wpath
是boost::filesystem::basic_path<std::wstring>
的 typedef
。
所有定義均位於 boost::filesystem 名字空間,定義於 boost/filesystem.hpp
中。
可以通過傳入一個字符串至 boost::filesystem::path
類來構建一個路徑。
#include <boost/filesystem.hpp> int main() { boost::filesystem::path p1("C:\\"); boost::filesystem::path p2("C:\\Windows"); boost::filesystem::path p3("C:\\Program Files"); }
沒有一個 boost::filesystem::path
的構造函數會實際驗證所提供路徑的有效性,或檢查給定的文件或目錄是否存在。 因此,boost::filesystem::path
甚至可以用無意義的路徑來初始化。
#include <boost/filesystem.hpp> int main() { boost::filesystem::path p1("..."); boost::filesystem::path p2("\\"); boost::filesystem::path p3("@:"); }
以上程序可以執行的原因是,路徑其實只是字符串而已。 boost::filesystem::path
只是處理字符串罷了;文件系統沒有被訪問到。
boost::filesystem::path
特別提供了一些方法來以字符串方式獲取一個路徑。 有趣的是,有三種不同的方法。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\Windows\\System"); std::cout << p.string() << std::endl; std::cout << p.file_string() << std::endl; std::cout << p.directory_string() << std::endl; }
string()
方法返回一個所謂的可移植路徑。 換句話說,就是 Boost.Filesystem 用它自己預定義的規則來正規化給定的字符串。 在以上例子中,string()
返回 C:/Windows/System
。 如你所見,Boost.Filesystem 內部使用斜杠符 /
作為文件名與目錄名的分隔符。
可移植路徑的目的是在不同的平台,如 Windows 或 Linux 之間,唯一地標識文件和目錄。 因此就不再需要使用預處理器宏來根據底層的操作系統進行路徑的編碼。 構建可移植路徑的規則大多符合POSIX標准,在 Boost.Filesystem 參考手冊 給出。
請注意,boost::filesystem::path
的構造函數同時支持可移植路徑和平台相關路徑。 在上面例子中所使用的路徑 "C:\\Windows\\System" 就不是可移植路徑,而是 Windows 專用的。 它可以被 Boost.Filesystem 正確識別,但僅當該程序是在 Windows 操作系統下運行的時候! 當程序運行於一個 POSIX 兼容的操作系統,如 Linux 時,string()
將返回 C:\Windows\System
。 因為在 Linux 中,反斜杠符 \
並不被用作分隔符,無論是可移植格式或原生格式,Boost.Filesystem 都不會認為它是文件和目錄的分隔符。
很多時候,都不能避免使用平台相關路徑作為字符串。 一個例子就是,使用操作系統函數時必須要用平台相關的編碼。 方法 file_string()
和 directory_string()
正是為此目的而提供的。
在上例中,這兩個方法都會返回 C:\Windows\System
- 與底層操作系統無關。 在 Windows 上這個字符串是有效路徑,而在一個 Linux 系統上則既不是可移植路徑也不是平台相關路徑,會象前面所說那樣被解析。
以下例子使用一個可移植路徑來初始化 boost::filesystem::path
。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("/"); std::cout << p.string() << std::endl; std::cout << p.file_string() << std::endl; std::cout << p.directory_string() << std::endl; }
由於 string()
返回的是一個可移植路徑,所以它與用於初始化 boost::filesystem::path
的字符串相同:/
。 但是 file_string()
和 directory_string()
方法則會因底層平台而返回不同的結果。 在 Windows 中,它們都返回 \
,而在 Linux 中則都返回 /
。
你可能會奇怪為什么會有兩個不同的方法用來返回平台相關路徑。 到目前為止,在所看到的例子中,file_string()
和 directory_string()
都是返回相同的值。 但是,有些操作系統可能會返回不同的結果。 因為 Boost.Filesystem 的目標是支持盡可能多的操作系統,所以它提供了兩個方法來適應這種情況。 即使你可能更為熟悉 Windows 或 POSIX 系統如 Linux,但還是建議使用file_string()
來取出文件的路徑信息,且使用 directory_string()
取出目錄的路徑信息。 這無疑會增加代碼的可移植性。
boost::filesystem::path
提供了幾個方法來訪問一個路徑中的特定組件。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\Windows\\System"); std::cout << p.root_name() << std::endl; std::cout << p.root_directory() << std::endl; std::cout << p.root_path() << std::endl; std::cout << p.relative_path() << std::endl; std::cout << p.parent_path() << std::endl; std::cout << p.filename() << std::endl; }
如果在是一個 Windows 操作系統上執行,則字符串 "C:\\Windows\\System" 被解釋為一個平台相關的路徑信息。 因此,root_name()
返回 C:
, root_directory()
返回 /
, root_path()
返回C:/
, relative_path()
返回 Windows/System
, parent_path()
返回 C:/Windows
, 而 filename()
返回 System
。
如你所見,沒有平台相關的路徑信息被返回。 沒有一個返回值包含反斜杠 \
,只有斜杠 /
。 如果需要平台相關信息,則要使用 file_string()
或 directory_string()
。 為了使用這些路徑中的單獨組件,必須創建一個類型為 boost::filesystem::path
的新對象並相應的進行初始化。
如果以上程序在 Linux 操作系統中執行,則返回值有所不同。 多數方法會返回一個空字符串,除了 relative_path()
和 filename()
會返回 C:\Windows\System
。 字符串 "C:\\Windows\\System" 在 Linux 中被解釋為一個文件名,這個字符串既不是某個路徑的可移植編碼,也不是一個被 Linux 支持的平台相關編碼。 因此,Boost.Filesystem 沒有其它選擇,只能將整個字符串解釋為一個文件名。
Boost.Filesystem 還提供了其它方法來檢查一個路徑中是否包含某個特定子串。 這些方法是:has_root_name()
, has_root_directory()
, has_root_path()
, has_relative_path()
,has_parent_path()
和 has_filename()
。 各個方法都是返回一個 bool
類型的值。
還有兩個方法用於將一個文件名拆分為各個組件。 它們應當僅在 has_filename()
返回 true
時使用。 否則只會返回一個空字符串,因為如果沒有文件名就沒什么可拆分了。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("photo.jpg"); std::cout << p.stem() << std::endl; std::cout << p.extension() << std::endl; }
這個程序分別返回 photo
給 stem()
,以及 .jpg
給 extension()
。
除了使用各個方法調用來訪問路徑的各個組件以外,你還可以對組件本身進行迭代。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\Windows\\System"); for (boost::filesystem::path::iterator it = p.begin(); it != p.end(); ++it) std::cout << *it << std::endl; }
如果是在 Windows 上執行,則該程序將相繼輸出 C:
, /
, Windows
和 System
。 在其它的操作系統如 Linux 上,輸出結果則是 C:\Windows\System
。
前面的例子示范了不同的方法來訪問路徑中的各個組件,以下例子則示范了修改路徑信息的方法。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\"); p /= "Windows\\System"; std::cout << p.string() << std::endl; }
通過使用重載的 operator/=()
操作符,這個例子將一個路徑添加到另一個之上。 在 Windows 中,該程序將輸出 C:\Windows\System
。 在 Linux 中,輸出將會是 C:\/Windows\System
,因為斜杠符 /
是文件與目錄的分隔符。 這也是重載 operator/=()
操作符的原因:畢竟,斜杠是這個方法名的一個部分。
除了 operator/=()
,Boost.Filesystem 只提供了 remove_filename()
和 replace_extension()
方法來修改路徑信息。
9.3. 文件與目錄
boost::filesystem::path
的各個方法內部其實只是對字符串進行處理。 它們可以用來訪問一個路徑的各個組件、相互添加路徑等等。
為了處理硬盤上的物理文件和目錄,提供了幾個獨立的函數。 這些函數需要一個或多個 boost::filesystem::path
類型的參數,並且在其內部會調用操作系統功能來處理這些文件或目錄。
在介紹各個函數之前,很重要的一點是要弄明白出現錯誤時會發生什么。 所有要在內部訪問操作系統功能的函數都有可能失敗。 在失敗的情況下,將拋出一個類型為boost::filesystem::filesystem_error
的異常。 這個類是派生自 boost::system::system_error
的,因此適用於 Boost.System 框架。
除了繼承自父類 boost::system::system_error
的 what()
和 code()
方法以外,還有另外兩個方法:path1()
和 path2()
。 它們均返回一個類型為 boost::filesystem::path
的對象,因此在發生錯誤時可以很容易地確定路徑信息 - 即使是對那些需要兩個 boost::filesystem::path
參數的函數。
多數函數存在兩個變體:在失敗時,一個會拋出類型為 boost::filesystem::filesystem_error
的異常,而另一個則返回類型為 boost::system::error_code
的對象。 對於后者,需要對返回值進行明確的檢查以確定是否出錯。
以下例子介紹了一個函數,它可以查詢一個文件或目錄的狀態。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\"); try { boost::filesystem::file_status s = boost::filesystem::status(p); std::cout << boost::filesystem::is_directory(s) << std::endl; } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
boost::filesystem::status()
返回一個 boost::filesystem::file_status
類型的對象,該對象可以被傳遞給其它輔助函數來評估。 例如,如果查詢的是一個目錄的狀態,則boost::filesystem::is_directory()
將返回 true
。 除了 boost::filesystem::is_directory()
,還有其它函數,如 boost::filesystem::is_regular_file()
, boost::filesystem::is_symlink()
和 boost::filesystem::exists()
,它們都會返回一個 bool
類型的值。
除了 boost::filesystem::status()
,另一個名為 boost::filesystem::symlink_status()
的函數可用於查詢一個符號鏈接的狀態。 在此情況下,實際上查詢的是符號鏈接所指向的文件的狀態。在 Windows 中,符號鏈接以 lnk
文件擴展名識別。
另有一組函數可用於查詢文件和目錄的屬性。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\Windows\\win.ini"); try { std::cout << boost::filesystem::file_size(p) << std::endl; } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
函數 boost::filesystem::file_size()
以字節數返回一個文件的大小。
#include <boost/filesystem.hpp> #include <iostream> #include <ctime> int main() { boost::filesystem::path p("C:\\Windows\\win.ini"); try { std::time_t t = boost::filesystem::last_write_time(p); std::cout << std::ctime(&t) << std::endl; } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
要獲得一個文件最后被修改的時間,可使用 boost::filesystem::last_write_time()
。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\"); try { boost::filesystem::space_info s = boost::filesystem::space(p); std::cout << s.capacity << std::endl; std::cout << s.free << std::endl; std::cout << s.available << std::endl; } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
boost::filesystem::space()
用於取回磁盤的總空間和剩余空間。 它返回一個 boost::filesystem::space_info
類型的對象,其中定義了三個公有屬性:capacity, free 和 available。 這三個屬性的類型均為 boost::uintmax_t
,該類型定義於 Boost.Integer 庫,通常是 unsigned long long
的 typedef。 磁盤空間是以字節數來計算的。
目前所看到的函數都不會觸及文件和目錄本身,不過有另外幾個函數可以用於創建、改名或刪除文件和目錄。
#include <boost/filesystem.hpp> #include <iostream> int main() { boost::filesystem::path p("C:\\Test"); try { if (boost::filesystem::create_directory(p)) { boost::filesystem::rename(p, "C:\\Test2"); boost::filesystem::remove("C:\\Test2"); } } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
以上例子應該是自解釋的。 仔細察看,可以看到傳遞給各個函數的不一定是 boost::filesystem::path
類型的對象,也可以是一個簡單的字符串。 這是可以的,因為 boost::filesystem::path
提供了一個非顯式的構造函數,可以從簡單的字符串轉換為 boost::filesystem::path
類型的對象。 這實際上簡化了 Boost.Filesystem 的使用,因為可以無須顯式創建一個對象。
還有其它的函數,如 create_symlink()
用於創建符號鏈接,以及 copy_file()
用於復制文件或目錄。
以下例子中介紹了一個函數,基於一個文件名或一小節路徑來創建一個絕對路徑。
#include <boost/filesystem.hpp> #include <iostream> int main() { try { std::cout << boost::filesystem::complete("photo.jpg") << std::endl; } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
輸出哪個路徑是由該程序運行時所處的路徑決定的。 例如,如果該例子從 C:\
運行,輸出將是 C:/photo.jpg
。
請再次留意斜杠符 /
! 如果想得到一個平台相關的路徑,則需要初始化一個 boost::filesystem::path
類型的對象,且必須調用 file_string()
。
要取出一個相對於其它目錄的絕對路徑,可將第二個參數傳遞給 boost::filesystem::complete()
。
#include <boost/filesystem.hpp> #include <iostream> int main() { try { std::cout << boost::filesystem::complete("photo.jpg", "D:\\") << std::endl; } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
現在,該程序顯示的是 D:/photo.jpg
。
最后,還有一個輔助函數用於取出當前工作目錄,如下例所示。
#include <windows.h> #include <boost/filesystem.hpp> #include <iostream> int main() { try { std::cout << boost::filesystem::current_path() << std::endl; SetCurrentDirectory("C:\\"); std::cout << boost::filesystem::current_path() << std::endl; } catch (boost::filesystem::filesystem_error &e) { std::cerr << e.what() << std::endl; } }
以上程序只能在 Windows 中執行,這是 SetCurrentDirectory()
函數的原因。 這個函數更換了當前工作目錄,因此對 boost::filesystem::current_path()
的兩次調用將返回不同的結果。
函數 boost::filesystem::initial_path()
用於返回應用程序開始執行時所處的目錄。 但是,這個函數取決於操作系統的支持,因此如果需要可移植性,建議不要使用。 在這種情況下,Boost.Filesystem 文檔中建議的方法是,可以在程序開始時保存 boost::filesystem::current_path()
的返回值,以備后用。
9.4. 文件流
C++ 標准在 fstream
頭文件中定義了幾個文件流。 這些流不能接受 boost::filesystem::path
類型的參數。 由於 Boost.Filesystem 庫很有可能被包含在 C++ 標准的 Technical Report 2 中,所以這些文件流將通過相應的構造函數來進行擴展。 為了當前可以讓文件流與類型為 boost::filesystem::path
的路徑信息一起工作,可以使用頭文件 boost/filesystem/fstream.hpp
。 它提供了對文件流所需的擴展,這些都是基於 Technical Report 2 即將加入 C++ 標准中的。
#include <boost/filesystem/fstream.hpp> #include <iostream> int main() { boost::filesystem::path p("test.txt"); boost::filesystem::ofstream ofs(p); ofs << "Hello, world!" << std::endl; }
不僅是構造函數,還有 open()
方法也需要重載,以接受類型為 boost::filesystem::path
的參數。
9.5. 練習
You can buy solutions to all exercises in this book as a ZIP file.
-
創建一個程序,該程序為位於應用程序當前工作目錄的上一層目錄中的一個名為
data.txt
的文件創建一個絕對路徑。 例如,如果該程序從C:\Program Files\Test
執行,則應顯示C:\Program Files\data.txt
。