Fandom

C/C++

FAQ

53strony na
tej wiki
Dodaj nową stronę
Dyskusja1 Udostępnij

FAQ grupy pl.comp.lang.c Edytuj

"Najczęściej zadawane pytania i odpowiedzi jakie na nie padały". Założenia niniejszego zbiorku są następujące:

  • umieszczamy tu pytania, które faktycznie pojawiały się kilka razy, czasem w krótkich odstępach czasu,
  • bezpośrednio w odpowiedziach zawarte będą ogólne wnioski, po szczegóły odsyłamy do wskazanych dyskusji na grupie.

Zadawanie pytań na grupie Edytuj

Istnieje obszerny instruktaż o zadawaniu pytań, ale jest dość obszerny i ogólny. To FAQ ma pretensje do bycia streszczeniem, wobec tego linki do najważniejszych kwestii przy pisaniu na grupę:

  • precyzja - w przypadku grupy pl.comp.lang.c oznacza to co najmniej podanie:
    • czy chodzi o C, C++, czy może C++/CLI - to naprawdę wbrew pozorom różne języki, szczególnie, jeśli zacznie się używać bibliotek dla nich standardowych!
    • systemu operacyjnego
    • kompilatora
    • stosowanych bibliotek
  • przedstawienie tylko sedna problemu - najlepiej minimalnego kawałka kodu, w którym problem występuje, ale jednocześnie niech ten kod będzie kompletny. Nierzadko samo upraszczanie kodu i znalezienie minimalnej kombinacji przy której błąd występuje pomaga w samodzielnym rozwiązaniu problemu. Jeśli kod jest zbyt długi, to może warto umieścić w serwisie do dzielenia się kodem, np. pastebin.pl czy codepad (pozwala na uruchamianie krótkich programów w C++ i nie tylko).
  • pokazanie (błędnych) wyników działania, komunikatów o błędach, ostrzeżeń itp.
  • sprawdzenie i podanie błędów zwracanych przez poszczególne funkcje w ramach błędnego kodu. Kody błędów po coś wymyślono!
  • opisanie celu, do jakiego się zmierza. Czasem na pytanie "Jak przyspieszyć wyszukiwanie na liście?" pada, jak się potem okazuje sensowna, odpowiedź: "Użyj posortowanej tablicy i wyszukiwania binarnego, albo tablicy haszującej.", bo pytanie powinno brzmieć: "Jak szybko wyszukać?".

Jak i gdzie wysłać pytania Edytuj

TODO: do opracowania klient usenet, usenet.gazeta.pl, google groups

Szablon idealnego pytania Edytuj

Być może początkującym trudno będzie praktycznie zastosować się do powyższych wskazówek, dlatego poniżej prosty do wypełnienia "szablon idealnego pytania":

Piszę w [ C | C++ | C++/CLI (niepotrzebne usuń)],
używam kompilatora [ nazwa i wersja kompilatora ]
pod systemem operacyjnym [ nazwa systemu ].
 
Chodzi mi o zrobienie [ opis ogólnego celu ], próbuję robić to tak:
[ opis sposobu rozwiązania lub kawałek kodu (w miarę możliwości kompilujący się) ]
ale mam następujące problemy:
   [ nie kompiluje się (koniecznie komunikaty błędów),
     daje błędne wyniki (jakie daje, a jakie powinny być, również - dane wejściowe),
     wysypuje się (objawy, komunikaty systemowe itp.) 
     inny problem (byle dobrze opisany...)
   ]

Oczywiście nie zawsze pytanie da się zadać wg takiego schematu, np. że pytanie dotyczy jakiejś kwestii języka i jest (lub powinno być) niezależne od kompilatora i systemu operacyjnego.

Niemniej zawsze warto pamiętać, że to, co dla nas, jako pytających oczywiste ("Przecież wiadomo, że chodzi mi o Visual Studio 2005 EE pod Windows!"), nie zawsze jest oczywiste dla odpowiadających. Zapomnieć o istotnych informacjach też łatwo ("Ale piszesz w C, C++, czy C++/CLI?").

CzterowierszEdytuj

Wolisz czytać coś takiego:

Zamienia naturalną kolejność czytania. 
> Dlaczego odpowiadanie nad cytatem jest takie denerwujące?
> > Odpowiadanie nad cytatem.
> > > Co jest jedną z najbardziej denerwujących rzeczy na grupach?

Czy coś takiego:

> > > Co jest jedną z najbardziej denerwujących rzeczy na grupach?
> > Odpowiadanie nad cytatem.
> Dlaczego odpowiadanie nad cytatem jest takie denerwujące?
Zamienia naturalną kolejność czytania.

Pytanie retoryczne 8-) Na grupie przyjęła się druga forma. Odpowiadanie nad cytatem (tzw. top posting) jest nienaturalny dla dyskusji w językach, w których pisze się od góry do dołu — czyli w polskim też. Dlatego bezwzględnie odpowiadaj pod odpowiednio przyciętym cytatem. Jeśli masz odmienne przyzwyczajenia, pisząc na grupę — zmień je.

Tłumaczenie "Bo mi tak się kursor w programie ustawia." jest bzdurną wymówką. To Ty piszesz post, a nie program czytnika news. Zawsze możesz ręcznie przejść tam, gdzie trzeba lub zmienić czytnik.

Nie zadawaj pytań "Kto mi zrobi pracę domową?".Edytuj

Skoro została zadana, to znaczy, że zadający uważa, że powinieneś sam umieć ją zrobić. Pokaż, że zabrałeś się za problem, jeśli natrafisz na konkretne problemy w trakcie rozwiązywania – pewnie ktoś pomoże.

Jeśli chcesz zobaczyć, jak "live" odpowiadają na tego typu pytania i prośby grupowicze zobacz np. ten wątek. W nim bardzo łopatologiczna odpowiedź Sasq (z drobnymi zmianami):

My tutaj po prostu wierzymy, że pisząc coś za Ciebie wcale Ci nie pomożemy, tylko zaszkodzimy. Bo co z tego, że oddasz ten program i zdasz zaliczenie, może nawet uda Ci się jakoś dopłynąć do końca studiów [...], ale jeśli się tego sam nie nauczysz robić, to żaden papierek ukończenia studiów Ci nie pomoże w znalezieniu pracy, czy stworzeniu własnej.

Uważamy natomiast, że pomożemy Ci, jeśli nakierujemy Cię na właściwe tory i postanowisz zrobić to sam, następnie napiszesz nam na jakie problemy napotkałeś, a wtedy już na pewno ktoś tutaj Ci pomoże pójść dalej.

Jeśli nie chcesz nauczyć się programować, to oczywiście nie robiąc tego za Ciebie również Ci pomagamy: uświadomić sobie, że mijasz się z powołaniem i lepiej dla Ciebie będzie, gdy znajdziesz sobie inne zajęcie, w którym będziesz się czuł dobrze.

Teraz już rozumiesz, dlaczego tutaj nikt nie zrobi Twojego zadania domowego za Ciebie?

Sprawdź w wyszukiwarce!Edytuj

Jeśli odpowiedź wyskakuje jako pierwsza w wynikach np. Google'a, a sam nie sprawdziłeś tego - spodziewaj się złośliwych komentarzy... i ciesz się, jeśli będą tylko złośliwe 8-)

Sprawdź to samo w wynikach wyszukiwania dla archiwum grupy.

Jeśli szukałeś i nie mogłeś znaleźć - podaj jakimi słowami kluczowymi się posługiwałeś. Wtedy masz dużą szansę, że ktoś podpowie właściwe. Zwłaszcza, że będzie widzał, że się starałeś znaleźć coś samodzielnie - nie zwalasz całej roboty na innych.

Mozliwe jest tez uzycie portalu gazeta.pl C/C++

Statystyki grupy pl.comp.lang.c Edytuj

Statystyki grupy oraz jej uzytkownikow znajduja sie pod adresem http://www.newsgroups.pl/pl-comp-lang-c - aktualnie serwis statystyk nie działa, więc poszukiwana jest alternatywa.

Propozycje pytań do opracowania Edytuj

Każda pomoc w rozwinięciu niniejszego FAQ jest bardzo mile widziana. W tym podrozdziale umieszczane są propozycje ciekawych pytań i problemów do opracowania, które powtarzają się co jakiś czas na grupie.

  • Odśmiecacz pamięci, czyli garbage collector
  • Literały napisowe jako tablice, "stałość" literałów.

PytaniaEdytuj

Jak obliczyć rozmiar statycznej tablicy w czasie kompilacji?Edytuj

Jednym z najlepiej znanych sposobów jest użycie operatora sizeof:

char table[5] = { 0 };
std::size_t n = sizeof(table)/sizeof(table[0]);

Idiomatyczne jest użycie pierwszego elementu tablicy, a nie typu - w ten sposób uniezależniamy się od ewentualnej zmiany typu elementów tablicy.

Ponieważ zapis `sizeof(table)/sizeof(table[0])` jest długi, w C++ warto go zastąpić, ale nie makrami, a szablonami:

template<typename T, size_t N> 
inline size_t countof(T(&a)[N]) 
{ 
   return N; 
}

Taka funkcja jest wprawdzie optymalizowana do stałej, ale formalnie jest nadal wynikiem funkcji - nie może być użyta np. jako rozmiar innej tablicy. Można to obejść za pomocą konstrukcji:

template<typename T, size_t N> 
inline const char(&sizer(T (&)[N]))[N]; // tak - deklaracja wystarczy!

int table[5];
double dt[sizeof(sizer(table))];

Do kompletu przydaje się też szablon endof() - generuje wskaźnik będący odpowiednikiem za-końcowego iteratora, przydatnego do inicjowania kolekcji STL:

template<typename T, size_t N>
inline T* endof(T(&a)[N])
{
  return a + N;
}

int table[] = {1, 2, 3};
std::vector<int> vec(table, endof(table));

Dynamiczne tablice wielowymiarowe i operator[][]Edytuj

Jeden z "przebojów" grupy - ten zbiór linków do dyskusji mówi sam za siebie... Najważniejsze wnioski:

  • C++
    • najprościej - użyć konstrukcji ze standardowych kontenerów C++: vector<vector<TYP> > tablica; lub boost::multi_array
    • bardziej zaawansowane - użycie proxy, przykład szablonów dla statycznych rozmiarów tablicy. Ale czy warto się męczyć, gdy masz biblioteki? 8-)
  • C
    • można użyć tablicy tablicy jednowymiarowej, dostęp realizować konstrukcjami: tablica[y * SIZEX + x],
    • zaalokować odpowiednią "hierarchię". Dla przypadku dwuwymiarowego: najpierw tablicę wskaźników (o rozmiarze pierwszego wymiaru), potem do każdego elementu podczepić tablicę o rozmiarze drugiego wymiaru - przykład kodu.

"Jaką książkę polecacie?"Edytuj

Zobacz w dziale Literatura. Warto również zapoznać się z dyskusją o wyższości nowego wydania "Pasji" i "Symfonii" nad starymi.

Wszystkie książki z działu Literatura zostały tak czy inaczej przywoływane na pl.comp.lang.c, dlatego dla każdej można zebrać sporo opinii, szukając w archiwum grupy, np. po nazwiskach autorów: "Koenig Moo" (Andrew Koenig, Barbara E. Moo, "C++. Potęga języka. Od przykładu do przykładu", wydawnictwo Helion).

Porównanie (równość) liczb zmiennopozycyjnychEdytuj

Na początek prosty przykład:

#include <iostream>
int main()
{
  double a = 1;
  for(size_t i = 0; i < 30; ++i)
    a *= 10.0;

  // powinno wyjść zero po podzieleniu przez 10^30 i odjęciu jedynki...
  double zero = a / 1e30 - 1;

  // ...a wychodzi... ?
  std::cout << zero << std::endl;

  return 0;
}

Zamiast spodziewanego 0 mamy inną wartość (np. -1.11022e-016, czyli -1.11022*10-16). Próba sprawdzenia warunku:

if(zero == 0.0)
{
   // to się raczej nie wykona...
}

Liczby rzeczywiste są w komputerach reprezentowane ze skończoną dokładnością - nie wszystkie wartości mogą być dokładnie zapisane. Co więcej - zapisywane są zazwyczaj jako ułamki binarne, dlatego często "równe" liczby dziesiętne są reprezentowane z błędem. Choćby (0.1)10 (dziesiętnie) jest w zapisie binarnym ułamkiem okresowym (0.0(0011))2, czyli przy skończonej liczbie cyfr binarnych nie uda się zapisać dokładnie (0.1)10. Drobne błędy zapisu jednej liczby kumulują się przy wykonywaniu kolejnych operacji, aż w końcu dają takie wyniki, jak w przykładzie.

Efekt zapisu binarnego widać w kontrprzykładzie - gdy używamy liczb, które mają dokładną reprezentację w systemie dwójkowym:

#include <iostream>
int main()
{
  double a = 1;
  for(size_t i = 0; i < 30; ++i)
    a *= 2;

  // powinno wyjść zero po podzieleniu przez 2^30 i odjęciu jedynki...
  double zero = a / 1073741824.0 - 1.0;

  // ...a wychodzi... ?
  std::cout << zero << std::endl;

  return 0;
}

Jakby tego wszystkiego było mało, to możliwa jest również zmiana błędów zaokrągleń wynikająca z różnych ustawień kompilatora - obliczenia w koprocesorze mogą być prowadzone z większą precyzją niż double. Jeśli kompilator w ramach optymalizacji usunie przechowywanie wyników w pamięci (konwersję wyników pośrednich z wewnętrznej reprezentacji na double), to błędy zaokrągleń będą inne. Dlatego porównanie (test równości) liczb rzeczywistych zawsze trzeba wykonywać według takiego schematu:

if(fabs(a - b) < epsilon) ...

Z powodów takich błędów zaokrągleń (ściślej - niemożliwych do przewidzenia i opanowania błędów zaokrągleń) liczby rzeczywiste (o podstawie innej niż 10) nie powinny być stosowane w systemach, gdzie liczy się pieniądze. Jeden z wątków przybliżających problem, więcej znajdziesz szukając np. "błędy zaokrągleń". Warto zobaczyć również post Qrczaka bardzo dokładnie pokazujący na przykładzie, skąd biorą się "ogony" w przybliżeniach.

Duże liczbyEdytuj

Najczęściej ten problem pojawia się to w kontekście "oblicznia silni z dowolną dokładnością" lub wyrazów ciągu Fibonacciego. Najprościej użyć w tym wypadku gotowych bibliotek:

  • Biblioteka dla C i C++ - GNU Multiple Precision Arithmetic Library (GMP) - obliczenia całkowitoliczbowe, stało- i zmiennoprzecinkowe dla liczb o dowolnej wielkości (dynamicznie alokowana pamięć dla przechowywania liczby)
  • Biblioteka TTMath - obliczenia dla liczb o statycznie definiowanym rozmiarze.

Kalkulator i parser, czyli obliczanie wyrażeńEdytuj

RPN vs. drzewo wyrażenia

Zobacz Przykład prostego parsera oraz dyskusję na jego temat.

Napisy w CEdytuj

...czyli dlaczego:

char *a = "abc"; 
if(a == "abc") 
{
   /* nie działa! (zazwyczaj...) */ 
}

No cóż... jeśli tego nie wiesz, to dokładniej przeczytaj jakąś książkę, może być "ANSI C", rozdział 5. Chociaż grupowicze bywają łaskawi i jeszcze na dodatek tłumaczą różnice między tablicą a wskaźnikiem, to w Twoim przypadku mogą się zdenerwować 8-)

No dobrze - do rzeczy: operator równości porównuje wartości wskaźników, czyli adresy, pod jakimi napisy mieszczą się w pamięci. Każde dwa napisy, nawet identyczne co do zawartości, mogą (i zwykle będą) w innym miejscu pamięci. Zagadka rozwiązana 8-)

Zamiast porównania użyj strcmp, tylko pamiętaj - ta funkcja w przypadku identycznych zawartości zwróci zero! Dokładny opis funkcji języka C do pracy z łańcuchami znakowymi należy szukać w opisach modułu string.h (dla C) lub cstring (dla C++), np. "C/C++ Reference" a dokładniej w sekcji "Standard C String and Character".

Jeszcze pułapka. Takie porównanie czasem "działa":

char *napis1 = "abc";
char *napis2 = napis1;
if(napis1 == napis2)
{
  /* to oczywiście "zadziała", bo napis1 i napis2 wskazują na to samo miejsce */
}

Co więcej - czasem "działa" również:

char *napis1 = "abc";
char *napis2 = "abc";
if(napis1 == napis2)
{
  /* to czasem "działa", a czasem nie - być może w zależności od kompilatora 
     i ustawionych opcji */
}

Podobnie jak powyższy przykład, "działa" czasami pierwszy przykład z tej odpowiedzi. Dzieje się tak dlatego, że kompilator potrafi i może optymalizować użycie stałych napisowych. Identyczne napisy w kodzie mogą być "zaszyte" tylko raz - co za tym idzie - mieć ten sam adres w pamięci. Ale to nie jest reguła, a jedynie możliwość pozostawiona twórcom kompilatora.

A tak w ogóle - może zainteresujesz się C++ i użyjesz std::string? Wtedy porównanie i przypisanie działa "normalnie" i nie trzeba się martwić o przydzielanie pamięci.

Przeciążanie operatorów strumieniowychEdytuj

Poniżej znajduje się przykład jak w najprostszy sposób przeładować operatory >> i <<.

class My_type
 {
   int first;
   double second;
 };

 #include <iostream>

 inline std::ostream & operator<< ( std::ostream & os_, const My_type & mt_value )
 // UWAGA: operator<< może pracować na const My_type w przeciwieństwie do operatora>>
 // może też być tak:
 // std::ostream & std::ostream.operator<< ( const My_type & mt_value )
 {
   os_ << "( "<< mt_value.first << ", " << mt_value.second << " )";
   return os_; 
 }

 inline std::istream & operator>> ( std::istream & is_, My_type & mt_value )
 // ten operator z przyczyn oczywistych nie może pracować na const My_type
 {
   is_ >> mt_value.first;
   is_ >> mt_value.second;
 }

Dla zgłębienia szczegółów (związanych z formatowaniem, dostępem do składowych prywatnych i użyciem funkcji nieformatowanego wejścia dla operatorów wejściowych) polecam rozdział 13.12 w książce "C++. Biblioteka standardowa. Podręcznik programisty".

Odczytywanie klawisza bez ENTERaEdytuj

W tym pytaniu mieszczą się wszystkie aspekty "kolorowej" obsługi konsoli - wczytywanie znaków bez potwierdzania klawiszem Enter, czyszczenie ekranu, wypisywanie tekstu w zadanej pozycji, operowanie kolorami itp.

Tego typu właściwości nie są dostępna w bibliotece standardowej. Standardowa biblioteka wspiera tylko podstawowe własności terminala, a dokładnie dostęp do terminala, jak do pliku sekwencyjnego (bez możliwości cofnięcia wskaźników zapisu i odczytu). Tego typu sprawy można zrealizować tylko metodami specyficznymi dla danego systemu lub za pomocą odpowiedniej biblioteki. Niektóre (starsze) środowiska udostępniają bibliotekę <conio.h>, dla systemów POSIX-owych dostępne są też biblioteki Curses/NCurses, które operują wieloma aspektami terminala (w tym również ustawianie kursora w określonej pozycji ekranu).

Typ wskaźnika na funkcję/typ wskaźnika na metodę i użycieEdytuj

Ze wskaźnikiem do zwykłej funkcji kłopot, ale nie aż tak wielki: Mamy taką funkcję (deklaracja):

int sumuj(int a, int b);

i chcielibyśmy stworzyć wskaźnik, który mógłby pokazywać na tę funkcję. Robimy to tak:

int (*wskNaFunSumuj)(int, int) = sumuj;

a czytamy (jak zwykle od środka i od prawej do lewej):

(*wskNaFunSumuj) /* wskNaFunSumuj jest wskaźnikiem */
(*wskNaFunSumuj)(int, int) /* mogącym pokazywać na funkcje wywoływaną z dwoma parametrami typu int */
int (*wskNaFunSumuj)(int, int) /* a i zwracającą int */

Tak przygotowany wskaźnik wywołujemy jak zwykłą funkcję:

wskNaFunSumuj(5,1);

Większy problem pojawia się w przypadku wskaźnika do niestatycznej metody klasy. Metody niestatyczne, choć "wyglądają" podobnie do zwykłych funkcji, tak naprawdę mają o jeden parametr więcej - jest nim this. Metody niestatyczne muszą być wywoływane "na rzecz" jakiegoś obiektu, tzn. muszą otrzymywać pierwszy parametr i to określonego typu. Również zapis typu wskaźnika na niestatyczną metodę różni się wyraźnie od "zwykłego" wskaźnika na funkcję. Dla klasy:

class Klasa
{
public:
  int sumuj(int a, int b);
  int mnoz(int a, int b);
};

wskaźnik na metodę będzie wyglądał tak:

int (Klasa::*wskNaMetode)(int, int) = &Klasa::sumuj;

a jego wywołanie musi być związane z obiektem:

Klasa obiekt;
(obiekt.*wskNaMetode)(5, 1);

Częściej jednak wskaźnik na metodę jest wykorzystywany w ramach klasy (innej metody niestatycznej) i wywoływany na rzecz obiektu *this, czyli:

(*this).*wskNaMetode(5, 1);

lub (pamiętając, że x->y jest skrótem dla (*x).y):

(this->*wskNaMetode)(5, 1);

Oczywiście jak zwykle - więcej o wskaźniku na metodę, a w szczególności - dlaczego wskaźnik na metodę niestatyczną jest różny od "zwykłego" wskaźnika na funkcję (lub metodę statyczną).

Rzutowanie wskaźników na typ całkowitoliczbowy i z powrotemEdytuj

Wskaźnik to wskaźnik i z liczbą nie musi mieć wiele wspólnego, choć wydawałoby się, że wskaźnik to adres, a adres to liczba. Jeśli chcesz pisać zupełnie przenośny kod, to akcje w stylu:

void *p = malloc(10);
int a = (int)p;
void *q = (void*)a;
if(p == q)
{
  // nie zawsze działa, a jak działa, to poniekąd przypadkiem
}

nie mogą mieć miejsca. Tyle od strony "czystości" języka. Jednak w większości obecnych implementacji wskaźnik jest "przekładalny" na liczbę całkowitą i da się (odpowiednią kompilacją warunkową czy szablonami) wybrać typ całkowitoliczbowy, który "zmieści" zawartość wskaźnika. Na dodatek będzie można wtedy wykonywać takie operacje, jak wyrównanie wskaźnika - szczegóły w tej dyskusji. Ale chociaż w C99 istnieje intptr_t, który ma wejść do C++0x, to we wspomnianym wyżej wątku jest też taka uwaga o wykonywaniu operacji arytmetycznych na wartości wskaźnika zrzutowanej do intptr_t.

Zapisywanie struktur na dyskEdytuj

Zazwyczaj ten problem występuje pod nazwą "zapis struktur do pliku". "Prosta" metoda polega na zapisie binarnego obrazu takich struktur do pliku... i wszystko jest w porządku dopóki w tych strukurach nie ma wskaźników i nie przenosimy danych między systemami (a czasem wystarczy - między wersjami programu skompilowanym innym kompilatorem lub z innymi opcjami kompilacji). Tutaj jest opisana większość problemów z tym związanych. Do tego dochodzą problemy z danymi dowiązanymi do struktur za pomocą wskaźników, szczególnie w przypadku "zapisu listy do pliku", ale dotyczy również np. napisów pamiętanych jako char*.

Ogólna rada to serializacja, czyli funkcje (w C++ odpowiednie metody obiektów lub operatory strumieniowe >> i <<), które przygotowują dane w "uniwersalnym" formacie, niezależnym od położenia w pamięci i reprezentacji typów prostych, i potrafią wczytać takie dane. Formatem takim może być format tekstowy, w szczególności XML, ale może być również ściśle ustalony format binarny.

Często przy wyborze formatu binarnego zakłada się, że elementy struktury będą następowały po sobie bezpośrednio. Kompilatory zazwyczaj stosują wyrównanie - wypełniają w pamięci miejsce między polami typów "krótszych" niż długość słowa maszynowego. Do wyłączenia tego zachowania służy zazwyczaj #pragma pack (pozwala wyłączać selektywnie) i/lub odpowiednia opcja kompilacji.

Wykrywanie końca plikuEdytuj

Często przetwarza się cały plik w pętli wczytując fragmenty i wykonując na nich operacje. Pierwsze, naturalne podejście do problemu może wyglądać tak:

while(!feof(file)) {
    fread(..., file);
    // rób coś z wczytanym kawałkiem
}

To nie działa i zwykle trafia na grupę w postaci pytania "Dlaczego fread dwa razy ...?"

Żeby zrozumieć o co chodzi wystarczy znać kilka faktów. feof(file) nie sprawdza, czy pozycja wewnątrz pliku pokrywa się z jego końcem, tylko stwierdza, czy została ustawiona flaga EOF. Flagę tę ustawiają funkcje wczytujące, kiedy osiągną koniec pliku. Oznacza to, że nawet otwierając pusty plik, trzeba wykonać fread, żeby test feof zwrócił prawdę. Oznacza to również, że feof należy testować tuż po odczytaniu. Podsumowując powyższy fragment powinien wyglądać tak:

while(true) {
    fread(..., file);
    if(feof(file)) break;
    // rób coś z wczytanym kawałkiem
}

Wysyłanie/odbieranie przez siećEdytuj

Oprócz wszystkich problemów opisanych w odpowiedzi "Zapisywanie struktur na dysk", w przypadku przesyłaniu danych łączem TCP dochodzi problem podziału na pakiety. Ściślej - brak takiego pojęcia w ramach API (BSD sockets lub winsock) TCP.

W praktyce oznacza to, że nie ma relacji między wywołaniami send(), a recv(). Jedno wywołanie send nie musi oznaczać wysłania jednego pakietu - może to być więcej niż jeden, a być może dopiero kilka wywołań stworzy jeden pakiet TCP. Co więcej - send() nie musi przekazać do wysłania od razu wszystkich danych, może wysłać (tak naprawdę - przesłać do bufora stosu TCP/IP) tylko część danych - wartość zwracana informuje o liczbie przekazanych bajtów. Z kolei funkcja recv() odbiera dane nie według przychodzących pakietów, ale tyle, ile może odczytać z buforów stosu TCP/IP, które są wypełniane danymi z kolejnych pakietów.

Rozwiązanie bazujące na zasadzie "jedno send() to jedno recv" "działa" zazwyczaj dlatego, że testowane jest na tym samym komputerze, na interfejsie loopback. A w takiej sytuacji system zwykle tworzy jeden bufor dla jednego wywołania send(), który oddaje jako "odczytane" dane przy najbliższym recv().

Poprawną metodą wysyłania większej liczby danych jest pętla:

const char* p = (const char*)bufor; // bufor z danymi
int len = bufor_size; // długość danych

while(len)
{
  int sent = send(sock, p, len, 0);
  if(sent < 0)
  {
    // błąd!
  }
  len -= sent;
  p += sent;
}

Poprawną metodą odbierania większej liczby danych jest pętla:

const char* p = (const char*)bufor; // bufor na dane
int len = bufor_size; // długość bufora na dane

while(len) 
{ 
  int received = recv(sock, p, len, 0); 
  if(received < 0) 
  { 
    // błąd! 
  } 
  len -= received; 
  p += received; 
}

Więcej jak zwykle w archiwum grupy.

"Co jest szybsze?"Edytuj

"Kanoniczna" odpowiedź Marcina "Qrczaka" Kowalczyka brzmi "Zmierz". Współczesne procesory, systemy operacyjne i kompilatory robią dość zaawansowane "sztuczki" ze - zdawałoby się - prostym kodem. Dlatego rozstrzygnięcie, w przypadku względnie prostych konstrukcji, "czy metoda A jest szybsza od B", nie jest możliwe w ogólności. Różne kompilatory (a nawet ten sam przy różnych ustawieniach) mogą różnie przetłumaczyć/zoptymalizować, różne procesory będą różnie szeregować instrukcje wygenerowane przez kompilator. Nawet rozmiar pamięci podręcznej (cache) może mieć tu znaczenie - konstrukcja wykonująca się formalnie szybciej, ale potrzebująca nieco więcej pamięci niż ten rozmiar może być wolniejsza.

No i last but not least - skoro czytasz tę odpowiedź, to pewnie chciałeś coś zoptymalizować... więc optymalizację odłóż na koniec! 8-) Lepiej, żeby program działał wolniej, ale poprawnie, niż bardzo szybko robił błędy. Po za tym największy zysk zazwyczaj osiąga się na dobrej konstrukcji algorytmu i struktur danych, a optymalizować warto z tylko "wąskie gardła". A pewnie częściej, kompilator i tak będzie lepszy. Szczególnie nie próbuj być "mądrzejszy od kompilatora" używając takich sztuczek. Więcej również w wątku o "wolnym" std::vector.

Czy można użyć delete this;?Edytuj

Owszem, ale trzeba pamiętać, że:

  • od momentu użycia nie wolno odwołać się do żadnej składowej niestatycznej obiektu. Po prostu - obiektu już nie ma, a this przestał być ważny (wskazuje na zwolniony obszar)
  • tak obsługiwane obiekty nie mogą być tworzone jako zmienne automatyczne, najlepiej zagwarantować to przez prywatny lub chroniony konstruktor oraz użycie fabryki obiektów (najprościej - metody statycznej).

"delete this" w archiwum

Czy poprawne jest użycie void* p; delete p; ? Edytuj

Ciekawa dyskusja na ten temat rozwinęła się w wątku delete i typ.

Zgodnie z tym co stanowi standard języka C++, ISO/IEC 14882:2003, klauzula 5.3.5, poniższy fragment kodu powoduje tzw. niezdefiniowane zachowanie (w skrócie UB):

struct T {};
void* p = new T;
delete p;

Standard C++ wymaga zgodności typu statycznego (klasa T w przykładzie) kasowanego obiektu z typem dynamicznym (typ wskaźnika). Jedynym wyjątkiem są hierarchie dziedziczenia z wirtualnym destruktorem, wówczas można kasować obiekt przez wskaźnik do klasy bazowej. W każdym innym przypadku zachowanie nie jest zdefiniowane.

Sequence pointsEdytuj

... czyli dlaczego

int a = a++ + ++a;

daje różne wyniki. Odpowiedź brzmi: bo może. Takie zachowanie jest niezdefiniowane (undefined behavior), co oznacza, że może zdarzyć się cokolwiek: dowolny wynik, wyjątek itp. W tym wątku również więcej o undefined behavior, unspecified behavior i implementation defined, w tym wyjaśnienie dlaczego obliczanie wyrażenia typu a++ + ++a jest niezdefiniowanym zachowaniem.

Definicja składowej statycznej/zmiennej globalnejEdytuj

Pisząc obiektowy kod, w pewnym momencie można zapomnieć, że składowe statyczne nie są związane z obiektem, ale z klasą jako taką. Ponieważ klasa istnieje "od początku", tak samo powinny istnieć ich składowe statyczne. A żeby istniały muszą być gdzieś zdefiniowane. Napisanie w pliku nagłówkowym:

class Klasa
{
  static int a_;
};

nie wystarczy. To jedynie deklaracja - informacja dla kompilatora, że taki obiekt, jak int Klasa::a_ gdzieś jest. Próba odwołania się do zadeklarowanej, ale niezdefiniowanej składowej daje błąd linkera - kompilator wie (na podstawie deklaracji), że int Klasa::a_ istnieje, ale przyjmuje, że może istnieć w innym module. Brak definicji spowoduje, że jednak nie istnieje - moduły nie dają się połączyć. Rozwiązanie: dodanie w pliku .cpp definicji:

int Klasa::a_; 
// można, dla niektórych typów - trzeba, nadać składowej wartość początkową:
int Klasa::a_ = 1;

W przypadku zmiennych globalnych popełniany bywa odwrotny błąd: w pliku nagłówkowym umieszczana jest od razu definicja zmiennej:

int a;

co przy włączaniu takiego nagłówka do plików .cpp powoduje definiowanie wielu obiektów int a - jest to złamanie tzw. One Definition Rule - w jednej jednostce translacji tylko jedna definicja danego obiektu. Tu zaprotestuje już kompilator, informując o ponownej definicji. "Zamiany" definicji na deklarację dokonujemy za pomocą słówka extern:

extern int a;

i wracamy do problemu ze składową statyczną - gdzieś jednak musi być definicja zmiennej a.

Analogicznym błędem jest próba "ułatwienia" sobie życia przez dodanie definicji składowej statycznej klasy od razu w pliku nagłówkowym. W tym wypadku błąd zgłasza zazwyczaj linker na etapie konsolidacji - widzi dwa symbole o identycznych nazwach w różnych modułach.

Tutaj wypada wskazać na niekonsekwencję składni - static dla zmiennej globalnej oznacza "zmienna dostępna tylko w tej jednostce kompilacji". Oznacza to, że tak oznaczona definicja przestaje być "widziana" w innych modułach ("widziana" przez nazwę - można oczywiście przekazać np. wskaźnik do tej zmiennej). Znaczenie static dla składowej klasy jest zupełnie inne - bardziej przypomina extern - deklaracja zmiennej związanej z klasą.

Jeden z wątków na ten temat - tutaj. Do prześledzenia: czasem const powoduje, że brak extern "nie przeszkadza" kompilatorowi.

Konwersja liczba->napis i napis->liczbaEdytuj

W C można użyć standardowych (lub niestandardowych, ale często implementowanych w kompilatorach) funkcji.

Dla konwersji liczba->napis są to:

  • sprintf (lub bezpieczniej snprintf - niestandardowa, często występuje)
  • itoa (niestandardowe, tylko dla liczb całkowitych)

Dla konwersji napis->liczba:

  • sscanf
  • strtol (liczby całkowite ze znakiem), strtoul (liczby całkowite bez znaku), strtod (liczby rzeczywiste)
  • atoi, atol, atof (niestandardowe, trudno wykryć błąd konwersji)

W C++ najwygodniej wypisać napis do strumienia i odczytać z niego jako liczbę lub wypisać liczbę do strumienia i odczytać jako napis. Można to zrobić "ręcznie", tworząc std::stringstream lub zastosować gotowe rozwiązanie, takie jak boost::lexical_cast lub analogiczne opisane na grupie.

Do konwersji liczba->napis można też użyć jednej z bibliotek Boost'a - Boost Format.

Można także wykorzystać bibliotekę Fastreams i jej funkcje rzutujące Fastreams - Casts.

Typy specyficzne dla niektórych bibliotek/kompilatorów mogą dostarczać również własnych funkcji/metod formatujących napisy, jak np. CString::Format() w MFC.

class a struct w C++Edytuj

Różnica między class a struct w C++ sprowadza się do domyślnej dostępności składowych: w typie deklarowanym za pomocą class składowe są domyślnie prywatne (private), a w struct - publiczne (public). Drugą, podobną różnicą jest domyślna sekcja, do której są domyślnie przenoszone klasy dziedziczone.

Czyli równoważne są:

struct Klasa_s
{
  // wszystkie składowe publiczne

  // ewentualne dalsze sekcje z modyfikatorami dostępu
};

i

class Klasa_c
{
  public:
  // wszystkie składowe publiczne

  // ewentualne dalsze sekcje z modyfikatorami dostępu
};

oraz

class Klasa_c
{
  // wszystkie składowe prywatne

  // ewentualne dalsze sekcje z modyfikatorami dostępu
};

i

struct Klasa_s
{
  private:
  // wszystkie składowe prywatne

  // ewentualne dalsze sekcje z modyfikatorami dostępu
};

W tym drugim przypadku konstrukcja:

class Klasa_c: Struktura { /* ... */ };

jest równoważna

class Klasa_c: private Struktura { /* ... */ };

natomiast

struct Klasa_s: Struktura { /* ... */ };

jest równoważna

struct Klasa_s: public Struktura { /* ... */ };

Inną mało ważną sprawą jest to, że słowo class może wystąpić jako "typ" parametru wzorca, natomiast nie można w tej roli użyć słowa struct. Choć użycie class w tym kontekście jest przestarzałe i należy używać typename.

Poza tym nie ma między tymi słowami żadnych różnic. Nawet jeśli wprowadzimy deklarację typu niekompletnego:

struct Klasa;

a potem zdefiniujemy go jako:

class Klasa { /* ... */ };

będzie to również poprawne (choć nie wszystkie kompilatory o tym wiedzą 8-) - np. Visual C++ ostrzega o różnym słowie kluczowym w deklaracji zapwiadającej).

Do czego służy słowo kluczowe typename?Edytuj

Obszerne wyjaśnienie tego elementu języka C++ znajduje się w Comeau C++ Template FAQ.

Poniżej znajduje się szablon klasy z T użytym jako parametr szablonu oraz funkcją składową foo():

template <typename T> class xyz {
   void foo() { T::x * p; /* ... */ p = blah; }
 };

Czym T::x * p; to deklaracja wskaźnika p? Czy może jest to mnożenie p przez, zdefiniowane gdzieś, T::x?

Jest to jedna z sytuacji, w której kompilator nie potrafi rozstrzygnąć na podstawie z kontekstu czy ma doczynienia z deklaracją (typem) czy z wyrażeniem. Dzieje się tak ponieważ kompilator nie ma pojęcia czym jest T, więc również nie wie czym jest T::x, gdzie x jest zależne od T. Nie wie, aż do momentu konkretyzacji szablonu (ang. template instantiation).

W powyższym przykładzie kompilatorowi należy pomóc rozwiązać tę wątpliwość i za pomoca słowa kluczowego typename podpowiedzieć, że T::x to typ:

template <typename T> class xyz {
   void foo() { typename T::x * p; /* ... */ p = blah; }
 };

Dziwne zachowanie wyjątków pod MinGWEdytuj

MinGW oprócz flagi -mthreads wymaga dorzucenia do programu biblioteki mingwm10.dll, która podmienia funkcję zwracajacą wskaźnik na funkcję obsługi wyjątków (a raczej na flagę, że wyjątek został rzucony).

Funkcja ta w implementacji MinGW zwraca thread-specific storage. Domyślna implementacja w CRT windows zwraca ten sam wskaźnik dla każdego wątku, co powoduje "przerzucanie" wyjątków pomiedzy wątkami programu.

Lista plików i katalogówEdytuj

Niestety C ani C++ nie udostępnia żadnych ogólnych mechanizmów do zarządzania katalogami plików.

Można kombinować za pomocą findfirst/findnext ewentualnie zamienników (FindFirst/FindNext/FindClose w WinAPI, opendir()/readdir()/closedir() pod Uniksami) - przykłady w archiwum (konkretne - po użyciu nazw funkcji jak słów kluczowych).

W C++ można też użyć przenośnej Boost Filesystem Library.

Można również w tym celu wykorzystać rozwiązania z bibliotek STLsoft, jak findfile_sequence z WinSTL lub readdir_sequence z UNIXSTL. Całość dostępna na licencji Synesis Software Standard Source License, bliskiej licencji BSD.

Wczytywanie bez spacji Edytuj

Zarówno konstrukcja C:

char napis[100];
scanf("%s", napis);

jak i C++:

std::string napis;
std::cin >> napis;

wczytuje ciągi znaków do napotkania pierwszego białego znaku (spacji, tabulacji, końca linii).

Rozwiązaniem jest wczytanie od razu całej linii albo specjalizowanymi funkcjami:

/* C */
char napis[100];
fgets(napis, sizeof(napis), stdin);
// C++
std::string napis;
std::getline(std::cin, napis);

albo używając nieco ciekawszych konstrukcji: w C jest to użycie konstrukcji zbioru znaków jako specyfikatora:

/* C */
char napis[100];
/* wczytaj wszystkie znaki, które nie są '\n', ale nie więcej niż jest miejsca */
scanf("%*[^\n]", sizeof(napis)-1, napis);

W ten sposób można też czytać np. tylko cyfry:

scanf("%*[0-9]", sizeof(napis)-1, napis);

W C++ analogiczną konstrukcję trzeba zbudować samemu, czytając znak po znaku ze strumienia. Uwaga: strumień std::cin standardowo ma włączoną flagę ignorowania białych znaków, czyli konstrukcja przepisująca z std::cin na std::cout:

std::copy(
    std::istream_iterator<char>(std::cin),
    std::istream_iterator<char>(),
    std::ostream_iterator<char>(std::cout, ""));

pominie białe znaki. Rozwiązaniem jest wyłączenie stosownej flagi:

std::cin.setf(0, std::ios::skipws);

Niektóre wątki opisujące problem złapią się np na takie zapytanie.

Wzajemna zależność modułów/klas Edytuj

Problem ten zazwyczaj ma dwa elementy:

  • wzajemna zależność plików nagłówkowych
  • wzajemna zależność klas

Pierwszy z problemów polega na zapętlonym dołączaniu plików nagłówkowych:

// plik A.h
#include "B.h"
// plik B.h
#include "A.h"

Kompilator może w takiej sytuacji zaprotestować:

a.h:1:15: error: #include nested too deeply

Rozwiązaniem jest użycie tzw. include guards, czyli konstrukcji o postaci:

// plik A.h
#ifndef A_GUARD_H__
#define A_GUARD_H__
#include "B.h"
// ...
#endif
// plik B.h
#ifndef B_GUARD_H__
#define B_GUARD_H__
#include "A.h"
// ...
#endif

Niektóre kompilatory pozwalają na użycie #pragma once:

// plik A.h
#pragma once
#include "B.h"
// ...
// plik B.h
#pragma once
#include "A.h"
// ...

Drugi problem polega na wzajemnej zależności klas. Tu rozwiązaniem jest możliwość definiowania wskaźników i referencji do typów niekompletnych, czyli zadeklarowanych, ale nieznanej w miejscu użycia definicji. W przypadku zależności:

class A
{
  B* b;
};

class B
{
  A* a;
};

gdy kompilator mówi np:

error: ISO C++ forbids declaration of ‘B’ with no type
error: expected ‘;’ before ‘*’ token

wystarczy:

class B;

class A
{
  B* b;
};

class B
{
  A* a;
};

Jest to deklaracja wyprzedzająca (forward declaration), która w ogóle bywa pomocna w ograniczaniu zależności między modułami. Jeśli np. interfejs klasy A będzie wymagał jedynie wskaźników lub referencji na obiekty klasy B, to nie jest konieczne dołączanie pliku nagłówkowego klasy B do pliku nagłówkowego klasy A - wystarczy deklaracja wyprzedzająca. Ogranicza to zależność wszystkich użytkowników nagłówka klasy A od zmian w nagłówku klasy B.

// plik A.h
#ifndef A_GUARD_H__
#define A_GUARD_H__
#include "B.h"

class A
{
   public:
      void foo(const B& b);
};

#endif

wystarczy:

// plik A.h
#ifndef A_GUARD_H__
#define A_GUARD_H__
class B;

class A
{
   public:
      void foo(const B& b);
};

#endif

Do tego zapewne niezbędne będzie dołączenie nagłówka "B.h" w pliku "A.cc".

Różnica między tablicą a wskaźnikiem Edytuj

W wielu przypadkach tablica jest niejawnie konwertowana do wskaźnika do jej pierwszego elementu, a jednocześnie na każdym wskaźniku można użyć operatora indeksowania, który (dla wskaźników) jest jedynie "lukrem syntaktycznym": p[i] <=> *(p+i).

Jednak są przypadki, gdzie intuicja łatwo zawodzi, co jest tablicą, a co wskaźnikiem:

#include <cstddef>

template <typename T> void f(const T* v) 
{
  std::cout << "pointer, sizeof == " << sizeof(v) << std::endl;
}

template <typename T, std::size_t N> void f(const T (&v) [N]) 
{
  std::cout << "table[" << N << "], sizeof() == " << sizeof(v) << std::endl;
}

void foo(char *p)
{
  f(p);
}

void bar(char p[100])
{
  f(p);
}

int main() 
{
  char tab[1];
  char *p = tab;
  f(p);
  f(tab);
  foo(p);
  bar(p);
  foo(tab);
  bar(tab);
}

Wynik może wyglądać tak (dla sizeof(void*) == 4):

pointer, sizeof == 4
table[1], sizeof() == 1
pointer, sizeof == 4
pointer, sizeof == 4
pointer, sizeof == 4
pointer, sizeof == 4

Pierwsze trzy przypadki są oczywiste i zgodne z intuicją. Kolejny (czwarty) pokazuje, że argument funkcji zapisany jako tablica jest de facto wskaźnikiem - w funkcji bar() można nawet wykonać p++ i wszystko będzie OK! Następny jest intuicyjny: tablica jest konwertowana na wskaźnik. Ostatni pokazuje jeszcze raz, że argument "tablicowy" jest jednak wskaźnikiem.

Przy okazji widać, że szablony mogą "rozpoznawać" tablice i wskaźniki.

Komunikacja między procesami Edytuj

Nie jest to wprawdzie temat stricte związany z C++, ale ponieważ często się pojawia... Ogólnie problem wygląda tak: mamy dwa programy (procesy), które powinny przekazywać między sobą dane, zazwyczaj w większych ilościach. Możliwe rozwiązania (w różnych systemach dostępne są różne metody):

  • dostępne wszędzie
    • sockety
    • pamięć dzielona
    • kolejki komunikatów
    • plik
    • pipe'y (nazwane i anonimowe)
  • tylko pod Windows
    • mailsloty
    • COM
    • komunikaty WM_COPYDATA

Każda z metod ma swoje wady i zalety (patrz ipc).

Ad blocker interference detected!


Wikia is a free-to-use site that makes money from advertising. We have a modified experience for viewers using ad blockers

Wikia is not accessible if you’ve made further modifications. Remove the custom ad blocker rule(s) and the page will load as expected.

Więcej w Fandom

Losowa wiki