[C] Czytanie danych z pliku

Witam. Proszę o pomoc w rozwiązaniu problemu. Chciałbym napisać coś w rodzaju dzienniczka szkolnego. Program wczytywałby dane (imię, nazwisko, ocena - rozdzielone spacją, w jednym wierszu jeden uczeń) z kilku plików (liczba wierszy dowolna) do struktury uczeń (imię, nazwisko, ocena). Cała grupa ma być tablicą struktur. Jeśli nazwisko oraz imię się powtarzają, ocena zostaje dopisana do istniejącej już struktury. Niestety, nie mam zielonego pojęcia, jak wczytać dane do struktur. Będą potrzebne później do operacji typu sortowanie, średnia ucznia, średnia grupy, itp. Proszę o rady, podpowiedzi. Im więcej informacji szukam, tym mniej z tego wszystkiego rozumiem.

funkcje fopen,fclose.fscanf - odczyt,fprintf - zapis zmiennej,bądż zmiennych.
Struktura FILE *plik.
Obsługujesz podobnie jak printf i scanf.
Odczyt realizuje się w pętli while (plik!=EOF) - dopóki nie ma końca pliku.
Na końcu fclose(plik);
Dopiero zamknięcie powoduje zapis na dysk.

Dlaczego używasz prawie 50-letniego języka C? Są zdecydowanie wygodniejsze technologie do tego typu rzeczy. Najwygodniej pewnie w Windows Forms, z użyciem “bazy” serializowanej do JSON’a w pliku.

@kowgli A dlaczego Ty używasz 50-letniej architektury procesora? :wink:

Może @anon12432793 chce się nauczyć programować w C? To wbrew pozorom dużo uczy, np. w kontekście panowania nad pamięcią w programach.

@kowgli Po części jest tak, jak wspomina @hindus, ale powodem wyboru tego języka było to, że od niego zaczynam programowanie na studiach.

Na innym forum znalazłem taki program:
#include <stdio.h>
#include <stdlib.h>

struct Student{
    char name[20];
    char surname[20];
    float srednia;
}typedef Student;
 
void readParamFromFile(FILE *file, Student students){
    while(fscanf(file,"%s %s %f", students.name, students.surname, &students.grade)>0) {
        printf("%s %s %f\n", students.name, students.surname, students.grade);
    }
}
 
int main(){
 FILE *plik = fopen("abcd.txt","r");
 
 Student students[2];
 
 readParamFromFile(plik,students[0]);
 readParamFromFile(plik,students[1]);
}

Ale niestety to rozwiązanie nie działa prawidłowo. Równie dobrze mógłbym funkcję wywołać raz z parametrem np. students[15], a wynik będzie dokładnie ten sam (wszystkie linijki z pliku abcd.txt).

Uczy, ale może akurat jakieś archaiczne sposoby dostępu do plików nie są czymś na co warto tracić czas.
Szczególnie, że np. w C# wygląda to tak:

string tekst = "Ala ma kota";
File.WriteAllText(@"C:\temp\dane.txt", tekst);

Ale pod spodem dzieje się w sumie to samo co w C :wink:
https://referencesource.microsoft.com/#mscorlib/system/io/streamwriter.cs,715bd5de78456219

Oczywiście to duże uproszczenie, nie mniej jednak ja jestem zwolennikiem teorii, że tak jak dobry kierowca zna mechanizm działania swojego samochodu, tak dobry programista powinien wiedzieć jak działa programowanie - i że pod magicznym WriteAllText nie ma magicznej jednej komendy do procesora tylko starożytne pisanie do file handlera.

Im dłużej w tym siedę, tym bardziej jestem świadomy, że istota i trudność programowania leży zupełnie gdzieś indziej - na poziomie architektury, wzorców itd… Choć oczywiście są różne aspekty. W każdym razie wydaje mi się, że zgłębianie antycznej składni C nie jest specjalnie wartościowe. Zawsze jest jakiś poziom abstrakcji na którym ucinasz. Możesz równie dobrze zejść niżej do poziomu wywołań systemu plików, assemblera, bramek logicznych, elektroniki, fizyki kwantowej…

Jestem przekonany, że @krystian0120 w trakcie swojej nauki pozna jeszcze różne języki wyższego poziomu i doceni łatwość tworzenia aplikacji. Tymczasem musi skoncentrować się na staruszku C. Więc zamiast zastanawiać się jak powinien wyglądać program nauczania, lepiej wróćmy do sedna problemu - czyli rozwiązania problemu dla którego program nie działa jak powinien.

Może dlatego, że ta funkcja odczytuje studentów z pliku i wyświetla ich dane w konsoli, a nie zapisuje studentów do pliku albo zwraca listę studentów w postaci tablicy.

Prosiłbym więc o podpowiedź, w jaki sposób mogę wczytać dane do struktury (nazwisko, imię, ocena). Rzecz jasna - jedna struktura reprezentuje jednego ucznia. Nie udało mi się niestety odnaleźć rozwiązania problemu. Czytałem o różnych funkcjach, ale słabo ogarniam temat.

Dobra więc postaram się pomóc, ale nie licz na gotowca. Dodatkowo będę posługiwał się pseudokodem bo będę ewentualne fragmenty kogu pisał z palca.

Zastanawiam się trochę jak Ci pomóc bo na podstawie kodu jaki pokazałeś mam wątpliwości czy poradzisz sobie z dynamiczną alokacją pamięci, a to będzie tutaj kluczowe. Tym bardziej, że nie do końca poprawnie zdefiniowałeś strukturę.

Rozwiązanie problemu musisz zacząć od ustalenia struktury. To już prawie masz czyli:

struct Student {
    char name[];
    char surname[];
    float grades[];
};

Grades to są wszystkie oceny danego ucznia, długość tablicy nie może być z góry ustalona i musi być dynamicznie tworzona zależnie od tego ile dany uczeń będzie posiadał ocen oraz w razie potrzeby powiększana. Tutaj rozwiązanie zależy od podejścia do problemu, można oceny wywalić do osobnej struktury, a można ustalić, że uczeń nie może mieć więcej niż ustalone N ocen, wszystko zależy od przyjętych zasad i kryteriów.

Kolejna rzecz jaką musisz zrobić to określić format pliku (Twojego pliku) w którym zapisywać będziesz dane. Możesz do tego celu wykorzystać formaty tekstowe takie jak JSON, XML. Nada się również do tego plik binarny lub własny format tekstowy jaki wymyślisz.
Przykładowo możesz ustalić, że format pliku będzie następujący:

  • każda kolejna linia to są dane kolejnego studenta - lub dane z jednej instancji Twojej struktury
  • poszczególne pola struktury oddzielone są ; - jak zapisać oceny tutaj możesz poszczególną ocenę również oddzielić średnikiem lub niejako zapisać tablicę jako jedno pole i poszczególne elementy tablicy oddzielić np przecinkiem ,

Zgodnie z tymi założeniami plik z kilkoma wpisami może wyglądać następująco:

Marcin;Kowalski;3;6;3;6;3.5;4.5;5
Zbigniew;Rak;5;4.5;5;6;3.5;4.5;5

lub druga wersja:

Marcin;Kowalski;3,6,3,6,3.5,4.5,5
Zbigniew;Rak;5,4.5,5,6,3.5,4.5,5

Zwróć uwagę na różnice pomiędzy kropką a przecinkiem, przecinek w drugim przykładzie oddziela kolejne elementy tablicy, natomiast kropka oznacza część dziesiętną.

Teraz mając już ustalony format naszego pliku, możesz zabrać się do zapisywania według tych zasad danych. Mając tablicę Studetn students[23]; musisz w pętli zapisać dane linijka po linijce, mniej więcej w kodzie musisz mieć:

// otwarcie pliku do zapisu
char buffer[128]; // rozsadna dlugosc ktorej nie przekroczy jedna linijka

for(int i = 0; i < 23; i++)
{
	// w pierwszej kolejnosci musimy ogarnac oceny
	// tutaj musisz sam zdecydowac w jaki sposob i gdzie bedziesz trzymal informacj o tym ile ocen ma dany uczen
	for(int j = 0; j < ilosc ocen danego uczna; i++) {
		sprintf(buffer, "%s;%d", buffer, students[i].grades[j]);
	}
	
	sprintf(buffer, "%s;%s;%s", students[i].name, students[i].surname, buffer);
	
	// mamy juz jedna linijke wiec ja zapisujesz do pliku
	// to bedzie cos w rodzaju
	write_line(file, buffer);
}

Teraz pozostało już tylko odczytać dane, ale tego już nie rozpisuję bo to proces odwrotny do zapisu. Po prostu wczytujesz linijkę z pliku, dzielisz ją po ; tworzysz nową instancję struktury i zapisujesz do niej wczytane dane, uprzednio w razie konwertując (rzutowanie) je na odpowiedni typ.

Oczywiście musisz rozwiązać kilka problemów które będą dodatkowo przy wczytywaniu:

  • jak i skąd określić ilu uczniów jest w pliku by przygotować odpowiednio dużą tablicę
  • podobnie jak wyżej ale tyczy się to ilości ocen danego ucznia
  • czy może wystąpić sytuacja, że w pliku dwukrotnie widnieje ten sam student, ale z różnymi ocenami i co w takim przypadku zrobić: połączyć, pierwsze wystąpienie jest ważniejsze, ostatnie wystąpienie jest ważniejsze
  • sprawdzanie czy student którego dane wczytujesz przypadkiem nie jest już w “rejestrze” czyli trzeba dodatkowo przeszukać wczytanych studentów i sprawdzić czy nie jest to ta sama osoba, powiązane z pkt wyżej

Dodatkowo pseudo kod który napisałem wyżej może zawierać błąd logiczny i struktura pliku zostanie złamana po przez np podwójne wystąpienie obok siebie znaków średnika, trzeba to sprawdzić i skorygować błąd.

Jeśli masz pytania to pisz, gdy będziesz miał już jakiś kod to się też pochwal.