FAQ grupy pl.comp.lang.c[]
"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[]
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[]
TODO: do opracowania klient usenet, usenet.gazeta.pl, google groups
Szablon idealnego pytania[]
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?").
Czterowiersz[]
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ą?".[]
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![]
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[]
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[]
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.
Pytania[]
Jak obliczyć rozmiar statycznej tablicy w czasie kompilacji?[]
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[][][]
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;
lubboost::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-)
- najprościej - użyć konstrukcji ze standardowych kontenerów C++:
- 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.
- można użyć tablicy tablicy jednowymiarowej, dostęp realizować konstrukcjami:
"Jaką książkę polecacie?"[]
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 zmiennopozycyjnych[]
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 liczby[]
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ń[]
RPN vs. drzewo wyrażenia
Zobacz Przykład prostego parsera oraz dyskusję na jego temat.
Napisy w C[]
...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 strumieniowych[]
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 ENTERa[]
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życie[]
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 powrotem[]
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 dysk[]
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 pliku[]
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ć[]
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?"[]
"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;
?[]
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).
Czy poprawne jest użycie void* p; delete p;
?[]
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 points[]
... 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 globalnej[]
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->liczba[]
W C można użyć standardowych (lub niestandardowych, ale często implementowanych w kompilatorach) funkcji.
Dla konwersji liczba->napis są to:
sprintf
(lub bezpieczniejsnprintf
- 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++[]
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
?[]
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 MinGW[]
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ów[]
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[]
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[]
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[]
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[]
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).