C++でファイル操作を行う際、ファイル名から拡張子を抽出する処理は非常に頻繁に発生します。
例えば、画像編集ソフトであれば「.png」や「.jpg」といった形式に応じて処理を分けたり、コンパイラであればソースファイルの拡張子を確認して言語を判別したりする必要があります。
かつてのC++では文字列操作を駆使して自前でロジックを組む必要がありましたが、現代のC++(C++17以降)では、標準ライブラリに強力なfilesystemが導入され、より安全かつ簡潔に記述できるようになりました。
本記事では、最新の標準的な手法から、互換性を重視した従来の手法まで、拡張子取得に関するテクニックを網羅的に詳しく解説します。
C++で拡張子を取得する主な手法
C++においてファイルパスから拡張子を切り出すには、大きく分けて2つのアプローチがあります。
一つはC++17以降で推奨されている「std::filesystem」ライブラリを利用する方法、もう一つはそれ以前の環境や特殊な制約下で用いられる「std::string」の文字列操作を利用する方法です。

現代の開発において、特別な理由がない限りはstd::filesystemの利用を強く推奨します。
これは単にコードが短くなるだけでなく、Windowsのバックスラッシュ(\)とUNIX系のスラッシュ(/)の違いといった、プラットフォーム固有のパスの扱いをライブラリ側で適切に吸収してくれるからです。
std::filesystemライブラリを使用する方法 (推奨)
C++17から標準導入された<filesystem>ヘッダーを使用すると、ファイルシステムのパスをオブジェクトとして扱うことができます。
このライブラリの中に含まれるstd::filesystem::pathクラスは、パスの構成要素を抽出するための便利なメンバ関数を多数備えています。
path::extension()の基本的な使い方
拡張子を取得するための最も直接的なメソッドはextension()です。
この関数は、パスの中から最後に現れるドット(.)以降の文字列を返します。

以下のサンプルプログラムで、実際の動作を確認してみましょう。
#include <iostream>
#include <filesystem> // C++17以上が必要
#include <string>
namespace fs = std::filesystem;
int main() {
// ファイルパスを定義
fs::path filePath = "/usr/local/bin/script.sh";
// 拡張子を取得
// extension()は .sh のようにドットを含んだ形式で返る
fs::path ext = filePath.extension();
// 結果の出力 (pathオブジェクトはそのまま出力可能)
std::cout << "ファイルパス: " << filePath << std::endl;
std::cout << "拡張子: " << ext << std::endl;
// stringとして扱いたい場合
std::string extStr = ext.string();
if (extStr == ".sh") {
std::cout << "これはシェルスクリプトです。" << std::endl;
}
return 0;
}
ファイルパス: "/usr/local/bin/script.sh"
拡張子: ".sh"
これはシェルスクリプトです。
extension()関数の重要な特徴は、取得される文字列にドットが含まれるという点です。
比較を行う際には、.jpgのようにドットを含めた文字列と比較する必要があることに注意してください。
また、ドットが含まれないファイル名(例:README)の場合は、空の文字列が返されます。
拡張子の比較と判定
実務では、取得した拡張子が特定の形式であるかを判定する場面が多いでしょう。
この際、大文字と小文字の区別に注意が必要です。
Windows環境では.JPGと.jpgが同一視されることが多いですが、C++の文字列比較はデフォルトで厳密に区別されます。
#include <iostream>
#include <filesystem>
#include <string>
#include <algorithm> // transform用
namespace fs = std::filesystem;
int main() {
fs::path p = "PHOTO.JPG";
std::string ext = p.extension().string();
// 小文字に変換して比較するテクニック
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == ".jpg" || ext == ".jpeg") {
std::cout << "画像ファイル(JPEG)を検出しました。" << std::endl;
}
return 0;
}
画像ファイル(JPEG)を検出しました。
このように、std::transformを使用して取得した拡張子を小文字に統一することで、表記の揺れを吸収した安全な判定が可能になります。
拡張子の書き換えと削除
単に取得するだけでなく、拡張子を変更して別のファイル名を作りたい場合もあります。
その際はreplace_extension()が便利です。
#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;
int main() {
fs::path p = "backup.txt";
// 拡張子を .bak に変更
p.replace_extension(".bak");
std::cout << "変更後: " << p << std::endl;
// 拡張子を削除する (空の文字列を指定)
p.replace_extension("");
std::cout << "削除後: " << p << std::endl;
return 0;
}
変更後: "backup.bak"
削除後: "backup"
このメソッドを使うと、元のパスオブジェクト自体が書き換わります。
新しい拡張子を指定する際、ドットを付けても付けなくても、ライブラリ側で適切に処理してくれるため、ヒューマンエラーが発生しにくい設計となっています。
文字列操作(std::string)を使用する方法 (C++14以前)
古いプロジェクトや、組み込み環境などでC++17以降が使用できない場合には、std::stringの機能を組み合わせて拡張子を抽出します。
この方法はファイルシステムに依存せず、単純な文字列処理として動作します。
find_last_ofとsubstrを組み合わせる方法
基本的なロジックは、「文字列の末尾から検索して、最初に見つかったドットの位置を特定し、そこから後ろを切り出す」というものです。

#include <iostream>
#include <string>
std::string getExtension(const std::string& filename) {
// 最後のドットの位置を探す
size_t pos = filename.find_last_of('.');
// ドットが見つからない場合、またはドットが文字列の最後にある場合
if (pos == std::string::npos) {
return "";
}
// ドットを含めてそれ以降を返す
return filename.substr(pos);
}
int main() {
std::string file1 = "document.pdf";
std::string file2 = "archive.tar.gz";
std::string file3 = "README";
std::cout << "file1: " << getExtension(file1) << std::endl;
std::cout << "file2: " << getExtension(file2) << std::endl; // .gz が返る
std::cout << "file3: " << getExtension(file3) << std::endl;
return 0;
}
file1: .pdf
file2: .gz
file3:
このコードでは、archive.tar.gz のような多重拡張子の場合、一番最後の拡張子のみが抽出されます。
これはstd::filesystem::path::extension()と同じ挙動です。
エッジケースへの対応 (フォルダ名にドットがある場合)
文字列操作で行う場合の落とし穴として、ディレクトリ名にドットが含まれているケースがあります。
例えば、/home/user.name/file というパスに対して単純に「最後のドット」を探すと、user.name のドットを拡張子の開始点だと誤認してしまう恐れがあります。
これを防ぐには、パス区切り文字(/ や \)の位置も考慮する必要があります。
#include <iostream>
#include <string>
std::string getExtensionSafe(const std::string& path) {
// 最後のドットと、最後のパス区切り文字を探す
size_t lastDot = path.find_last_of('.');
size_t lastSep = path.find_last_of("/\\");
// ドットが存在し、かつそれが最後のセパレータよりも後ろにある場合のみ拡張子とみなす
if (lastDot != std::string::npos && (lastSep == std::string::npos || lastDot > lastSep)) {
return path.substr(lastDot);
}
return "";
}
int main() {
std::string path = "/v1.0.release/install_log";
std::cout << "拡張子: '" << getExtensionSafe(path) << "'" << std::endl;
return 0;
}
拡張子: ''
このように、文字列操作で行う場合は多くの例外パターンを考慮しなければなりません。
これが、可能な限りstd::filesystemを推奨する大きな理由の一つです。
実践的な活用シーン
拡張子の取得ができるようになると、プログラムの柔軟性が大幅に向上します。
ここでは、実務でよく使われる2つのパターンを紹介します。
特定の拡張子のファイルをリストアップする
ディレクトリ内のファイルを走査し、特定の拡張子を持つものだけを表示する例です。
#include <iostream>
#include <filesystem>
#include <vector>
namespace fs = std::filesystem;
int main() {
std::string targetDir = "./images"; // 走査対象ディレクトリ
std::vector<fs::path> imageFiles;
// ディレクトリが存在するか確認
if (!fs::exists(targetDir)) return -1;
for (const auto& entry : fs::directory_iterator(targetDir)) {
if (entry.is_regular_file()) {
// 拡張子が .png または .jpg のものだけをリストに追加
auto ext = entry.path().extension();
if (ext == ".png" || ext == ".jpg") {
imageFiles.push_back(entry.path());
}
}
}
for (const auto& f : imageFiles) {
std::cout << "見つかった画像: " << f.filename() << std::endl;
}
return 0;
}
ファイルの種類に応じた処理の分岐
拡張子をキーとして、適切なパーサー(解析器)を選択するような設計も一般的です。
| 拡張子 | 処理内容 | 担当クラスの例 |
|---|---|---|
| .json | JSONデータのパース | JsonParser |
| .xml | XML設定ファイルの読み込み | XmlParser |
| .csv | 表形式データの解析 | CsvParser |
| .txt | プレーンテキストとして表示 | TextHandler |
どちらの方法を選ぶべきか?
最後に、紹介した2つの手法の使い分けを表にまとめました。
| 特徴 | std::filesystem (C++17) | std::string 操作 (従来) |
|---|---|---|
| 簡潔さ | 非常に簡潔。専用関数がある。 | 複数の関数を組み合わせる必要がある。 |
| 安全性 | パス区切り文字などを自動考慮。安全。 | 自前でロジックを組むためバグが出やすい。 |
| パフォーマンス | オブジェクト生成のコストが僅かにある。 | 単純な検索のみなので非常に高速。 |
| 移植性 | モダンな環境(C++17以降)が必要。 | ほぼすべてのC++環境で動作する。 |
結論として、新規の開発であれば「std::filesystem」一択と言えます。
実行速度が極限まで求められるループ内で、かつパスの形式が100%保証されているような特殊なケースを除いて、標準ライブラリの堅牢さを享受すべきです。
まとめ
C++で拡張子を取得する方法は、言語の進化とともに劇的に進化しました。
以前はfind_last_ofやsubstrを駆使して「文字列として」扱っていた処理が、現在ではstd::filesystem::pathを用いることで「パスという構造体として」論理的に扱えるようになっています。
この記事では、extension()による取得方法から、replace_extension()による変更、さらには文字列操作を用いた旧来の手法とその注意点について解説しました。
拡張子の比較を行う際はドットの有無や大文字・小文字に注意し、環境が許す限りはC++17の標準機能を積極的に活用して、バグの少ないクリーンなコードを目指しましょう。
これらの知識を身につけることで、ファイル操作を含むアプリケーション開発がよりスムーズに進むはずです。
