Kirjautuminen

Haku

Tehtävät

Koodit: C++: Tiedoston koon selvittäminen

Kirjoittaja: Metabolix

Kirjoitettu: 21.08.2011 – 03.06.2018

Tagit: ohjelmointitavat, koodi näytille, vinkki

Tiedoston koon selvittäminen on ohjelmoinnissa arkipäivää. C++ sisältää tähän funktion std::filesystem::file_size, joka lisättiin standardiin vasta vuonna 2017. Funktio toimii hyvin yksinkertaisesti:

#include <iostream>
#include <filesystem>
int main(int argc, char** argv) {
	const char* ohjelma = argv[0];
	if (std::filesystem::exists(ohjelma)) {
		std::cout << "Tämän ohjelman koko on " << std::filesystem::file_size(ohjelma) << " tavua.\n";
	} else {
		std::cout << "Ohjelman koko ei nyt selviä.\n";
	}
}

Koodin käyttö GCC:llä vaatii (vielä vuonna 2018) lisäparametrin -lstdc++fs.

Aiemmin on pitänyt käyttää käyttöjärjestelmien omia funktioita (mm. Linuxissa stat ja Windowsissa GetFileSize) tai eri kirjastojen funktioita (mm. standardifunktion edeltäjä, Boost-kirjaston boost::filesystem::file_size).

Tämä vinkki on alunperin kirjoitettu ennen vuotta 2017 eli ennen kuin C++ sisälsi standarditapaa tiedoston koon selvittämiseen. Seuraavat koodit esittelevät vaihtoehtoisia keinoja tiedoston koon selvittämiseen.

Perinteinen tapa ja sen ongelmat

Perinteinen tapa tiedoston koon selvittämiseen on avata tiedosto, siirtyä tiedoston loppuun ja katsoa, missä kohti ollaan. Kohtuullisen kokoisilla tiedostoilla tämä toimii oikein hyvin, eikä koodiakaan tarvita paljon:

#include <fstream>
std::streamsize tiedoston_koko_v1(const char* nimi) {
	std::ifstream tiedosto(nimi, std::ios::binary);
	tiedosto.seekg(0, std::ios::end);
	return tiedosto.tellg();
}
// Tai vielä lyhyemmin:
std::streamsize tiedoston_koko_v1_min(const char* nimi) {
	return std::ifstream(nimi, std::ios::binary | std::ios::ate).tellg();
}

Koodissa on eräs vakava puute: C++ ei takaa, että streamsize-tyyppinen muuttuja kykenisi käsittelemään valtavia tiedostoja. 32-bittisessä Windowsissa MinGW-kääntäjällä käänettynä ohjelma tuottaakin kahdesta gigatavusta alkaen outoja tuloksia:

KokoTavuinaWin32
1 kt10241024
1 Mt10485761048576
1 Gt10737418241073741824
2 Gt2147483648-2147483648
3 Gt3221225472-1073741824
4 Gt42949672960
5 Gt53687091201073741824

Luku siis pyörähtää kahden gigatavun kohdalta miinukselle ja on neljän gigatavun kohdalla uudestaan nollassa. Linuxissa tai 64-bittisissä järjestelmissä ongelmaa ei näytä olevan, mutta se ei toki Windows-käyttäjää lohduta.

Toinen huono ominaisuus tässä koodissa on, että olemattoman tiedoston koko näyttää olevan -1. Jos ohjelma ei huomioi tätä, voi seurata ongelmia.

Tarkkaan ottaen C++ ei myöskään takaa, että seekg-funktiolla päästäisiin luotettavasti tiedoston loppuun. Käytännössä funktio onneksi toimii toivotulla tavalla, kun käsitellään tavallisia tiedostoja tavallisissa käyttöjärjestelmissä; muunlainen toiminta olisi hankalaa ja epäloogista.

Jos on selvää, että tiedoston koko on alle kahden gigatavun ja että tutkittavat tiedostot ovat aina olemassa, tälläkin funktiolla pärjää hienosti. Joskus täytyy kuitenkin pystyä parempaan.

Kehittyneempi versio

Kun tiedostovirta ei osaa ilmoittaa tilaansa järkevästi, täytyy turvautua kepulikonsteihin. Äsken todettiin, että liian suuri tiedosto aiheuttaa luvun pyörähtämisen ensin negatiiviseksi ja sitten takaisin nollaan. Virheellisestäkin vastauksesta voidaan hyödyntää osa: siitä saadaan selville, mihin kohti kierrosta tiedosto päättyy. Jos siis vastaukseksi tulee 123, tiedetään, että tiedostossa on 123 tavua sekä X kertaa 4 gigatavua (32-bittisessä Windowsissa). Tämän jälkeen voidaankin siirtyä tiedoston kohtaan 123 ja kelata siitä 4 gigatavun pätkissä eteenpäin, kunnes löydetään oikea loppukohta. Lopun tunnistaa helposti siitä, että tiedoston lukeminen epäonnistuu.

Seuraavassa koodissa käytetään long long -tyyppiä, joka on ainakin 64-bittinen eli selviää tiedostoista jopa miljardeihin gigatavuihin asti. Se kuuluu C++:n standardikalustoon vasta versiosta C++11 alkaen, mutta monet kääntäjät ovat tunteneet sen jo 1990-luvulla.

Koodissa on lisäksi parannettu hieman virhetilanteiden käsittelyä: jos tiedoston avaaminen ei onnistu tai vastaus ei mahdu muuttujaan, heitetään poikkeus.

#include <fstream>
#include <stdexcept>
#include <limits>

long long tiedoston_koko_ll(const char* nimi) {
	std::ifstream tiedosto(nimi, std::ios::binary);
	if (!tiedosto) {
		throw std::runtime_error("Tiedostoa ei saada auki!");
	}

	// Lasketaan streamsize-yksikön pienimmän ja suurimman arvon ero.
	// Jos streamsize pyörähtää ympäri esim. neljän gigan välein,
	// tästä tulee 4 gigaa miinus 1 tavu (4294967296 - 1 = 4294967295).
	long long N = 0;
	N += std::numeric_limits<std::streamsize>::max();
	N -= std::numeric_limits<std::streamsize>::min();

	// Haetaan perinteisellä tyylillä loppukohta ja kelataan alkuun.
	tiedosto.seekg(0, std::ios::end);
	const std::streampos loppu = tiedosto.tellg();
	tiedosto.seekg(0, std::ios::beg);

	// Otetaan alustava vastaus. Jos se on negatiivinen (kuten voi olla
	// esim. 2 gigan ja 4 gigan välillä), käännetään se positiiviseksi
	// lisäämällä N + 1 tavua. Samalla kelataan tiedostoa.
	long long vastaus = std::streamsize(loppu);
	if (vastaus < 0) {
		vastaus += N + 1;
		// Koska seekg kelpuuttaa vain streamsize-lukuja, isot siirrot
		// pitää tehdä pienemmissä pätkissä. Seuraava koodi siirtää
		// ensin kolme kertaa kolmanneksen matkasta ja sitten ne
		// pari tavua, jotka hukkuvat pyöristysvirheen takia.
		// Esim. v = 16 = 3 * (v / 3) + (v % 3) = 3 * 5 + 1.
		tiedosto.seekg(vastaus / 3, std::ios::cur);
		tiedosto.seekg(vastaus / 3, std::ios::cur);
		tiedosto.seekg(vastaus / 3, std::ios::cur);
		tiedosto.seekg(vastaus % 3, std::ios::cur);
	} else {
		// Jos tulos olikin positiivinen, siirrytään vain sen verran eteenpäin.
		tiedosto.seekg(vastaus, std::ios::cur);
	}

	// Nyt oletettavasti tiedostoa on jäljellä X * (N + 1) tavua.
	// Yritetään lukea yksi tavu. Jos onnistui, ei olla vielä lopussa
	// ja voidaan hypätä vielä N tavua eteenpäin.
	while (tiedosto.get(), !tiedosto.eof()) {
		tiedosto.seekg(N / 3, std::ios::cur);
		tiedosto.seekg(N / 3, std::ios::cur);
		tiedosto.seekg(N / 3, std::ios::cur);
		tiedosto.seekg(N % 3, std::ios::cur);

		vastaus += N + 1;
		// Varaudutaan yhä virheisiin.
		if (vastaus < N) {
			throw std::runtime_error("Liian suuri tiedosto!");
		}
	}

	return vastaus;
}

Jos long long -tyyppi tuntuu omassa koodissa kummajaiselta, avuksi voi tehdä vielä int-tyyppiä käyttävän funktion, joka tarkistaa vastauksen sopivuuden int-muuttujaan.

// Tehdään funktiosta vielä int-versio.
int tiedoston_koko_i(const char* nimi) {
	long long oikea_tulos = tiedoston_koko_ll(nimi);
	int tulos = oikea_tulos;
	if (tulos != oikea_tulos) {
		throw std::runtime_error("Liian suuri tiedosto!");
	}
	return tulos;
}

Pääohjelma testausta varten

Esitettyjä funktioita voi kokeilla vaikkapa tällaisella ohjelmalla:

#include <stdexcept>
#include <iostream>
#include <string>

int main(int argc, char** argv) {
	std::cout << "Anna tiedoston nimi: ";
	std::string nimi;
	std::getline(std::cin, nimi);
	try {
		long long koko = tiedoston_koko(nimi.c_str());
		std::cout << "Tiedosto " << nimi << " on kooltaan " << koko << " tavua." << std::endl;
	} catch (std::runtime_error& e) {
		std::cout << "Virhe tiedoston " << nimi << " tutkimisessa: " << e.what() << std::endl;
	}
}

Kommentit

Torgo [22.08.2011 17:14:27]

#

Näppärä kepulikonsti. Mainittakoon että tuo perinteinen triviaali ratkaisu toimii myös, jos se käännetään 64-bittiseen järjestelmään. Pitäisi siis toimia myös 64-bittisellä Windowsilla edellyttäen, että se on käännetty 64-bittiseksi.

Kuten mainitsit, niin seekg:n ei luvata pääsevän tiedoston loppuun, mutta käytännössä se ei liene ongelma. Standardikirjaston avulla voi kuitenkin toteuttaa vaihtoehtoisenkin ratkaisun, joka toimii myös suurille tiedostoille. Avataan tiedosto binäärimuotoon ja luetaan tavu kerralla niin kauan kuin pystytään. Tässäkin tavassa on tosin pari ongelmaa. 1) Hidas suurille tiedostoille. 2) Mikään ei takaa, että se miten monta tavua pystytään onnistuneesti lukemaan olisi sama kuin tiedoston koko.
Usein tämä tapa voidaan yhdistää ohjelman muuhun tomintaan, jolloin hitaus ei enää ole ongelma.

Muita vaihtoehtoja on käyttää joistain kääntäjistä löytyviä 64-bittisiä vastineita tiedostonkäsittelyfunktioille. Tai käyttöjärjestelmäriippuvaisia funktioita. Mutta koska tässä haettiin portattavaa ratkaisua, niin mainitsemasi boostin käyttö on suositeltavaa. Myös stat on useimmille riittävän portattava. Se toimii ainakin Windowsissa, Linuxissa, Macissa ja muissa Unix-pohjaisissa järjestelmissä. Statilla saattaa tosin olla kääntäjästä riippuen tuota 2GB ongelmaa.

vesikuusi [06.12.2011 01:25:39]

#

Loistava koodivinkki! Mietiskelinkin kyseistä asiaa kesällä vähän, ja tässähän ratkaisu onkin. On se hienoa, kun jotkut osaavat henkilöt jaksaa vääntää toisten iloksi ja hyödyksi näitä vinkkejä :) Pitää opiskella tätä myöhemmin lisää.. :P

Lumi-ukkeli [11.11.2012 11:49:42]

#

http://msdn.microsoft.com/en-us/library/windows/desktop/aa364955(v=vs.85).aspx
http://linux.die.net/man/2/stat

vesikuusi [11.11.2012 23:44:47]

#

Lumi-ukkeli kirjoitti:

http://msdn.microsoft.com/en-us/library/windows/desktop/aa364955(v=vs.85).aspx
http://linux.die.net/man/2/stat

Tämän vinkin pointti taisi olla esittää ANSI-C++ -tyylinen ratkaisu. Ihan hyviä funktiota tietää ovat nuokin, jos tekee softaa pelkästään yhdelle käyttikselle.

Kirjoita kommentti

Muista lukea kirjoitusohjeet.
Tietoa sivustosta