Weryfikacja poprawności materiałów do zajęć z C++

0

Witam,

jestem nauczycielem i przygotowałem dla moich uczniów materiały do nauki C i C++. Nikt z nas nie jest jednak alfą i omegą, dlatego zdaję sobie sprawę, że mogą być w nich jakieś błędy.

Dlatego proszę chętne osoby o przeglądnięcie moich materiałów i wskazanie ewentualnych błędów oraz uwag.

Materiały te dostępne są pod adresem:

http://linux.media-soft.pl/index.php/materialy-do-zajec-programowanie/psio-c-cpp

i dostępne są na licencji:

http://creativecommons.org/licenses/by-nc-sa/3.0/pl/

Chcę, żeby posłużyły również innym (nauczycielom, uczniom, studentom i nie tylko).

1

Już w 1 prezentacji widzę pewny problem. Mianowicie auto jest "typem" dopiero w C++11, który jeszcze jest różnie wspierany przez różne kompilatory. Do C++11 oznaczał on, że kompilator ma sam stwierdzić czy dana zmienna ma być rejestrem czy w pamięci (czyli de facto standardowe zachowanie).

1

cytując

"Konstruktor - to publiczna funkcja składowa (metoda) (...)"

tak? a co powiesz na wzorzec projektowy np Singleton?
http://en.wikipedia.org/wiki/Singleton_pattern

kliknąłem w pierwsze lepsze. Jak znajdę więcej czasu to przeglądnę wszystko.

edit. Jeszcze czytam i będę tutaj dopisywał

w c++ nie piszemy int main(void) jeżeli chcemy by main był pusty. Można, owszem to się skompiluję, ale nie powinno się tak robić (nie wiem czy przy najwyższym stopniu ostrzegania kompilatora nawet nie wyrzuci warningiem) po prostu int main()

Zestawienie typów fundamentalnych

jest to zależne od systemu operacyjnego jak i od hardware'u. Nie można po prostu powiedzieć "int ma 32 bity i już." Nawet jest o tym napisane w Gręboszu (na którego się powołujesz). char może zajmować 16 bitów, było gdzieś pisane o tym na forum.

CZAS ŻYCIA OBIEKTU

no zaraz zaraz, piszesz o obiektach a pokazujesz typy (int, double) w c++ to nie są obiekty. Mało tego, zapomniałeś opisać coś takiego jak stos i sterta. Bo jak zrobię coś takiego to wtedy int (nie wskaźnik) przestanie istnieć?
int *a = new int(3);
jest pewna dyskusja na ten temat w komentarzach, warto przeczytać

"Instrukcja ”break” służy do wyskoczenia z bieżącego bloku lub pętli do pierwszej instrukcji następującej po nim."

mógłbyś mi to wytłumaczyć? bo break można używać tylko w switchu lub pętlach i ona po prostu przerywa te instrukcje. Więc pisanie o blokach jest nieprawdą.

"Instrukcja ”continue” służy do przerwania bieżącej iteracji w pętlach i rozpoczęciu nowej iteracji (w pętli ”for” bezpośrednio po instrukcji ”continue” wykonywana jest instrukcja kroku, a w pozostałych – sprawdzany jest warunek)."

tak? hm, czyli uważasz, że ten kod się zapętli?

    for (int i = 0 ; i < 10 ; i++)
    {
                continue;
    }

w pętli for także jest sprawdzany warunek. Gdyby nie był to byłoby bez sensu.

 double obwod_kwadratu(const double &bok)
{
  return 4*bok;
}

przecież ta funkcja to głupota. Po co deklarować coś const i referencje do tego skoro to typ wbudowany? Jeżeli nie potrzebujemy zmieniać mu wartości (a nie możemy bo mamy consta) to nie ma potrzeby przy typach wbudowanych wysyłać referencji

1
cout << ”Wprowadź imię: ”;
cin >> imie; // wczytuje z klawiatury imię
cout << ”Wprowadź adres: ”;
getline(cin, adres);

Przecież adres zawsze będzie pusty.

NAZWY
To wyrazy w kodzie spełniające następujące warunki:

  • mogą zawierać tylko znaki: [A-Z],[a-z],[0-9] oraz '_';
  • nie mogą zaczynać się od cyfry;
  • zalecane jest, aby zaczynały się od litery.
    Skoro wspomniano nawet co jest zalecano to czy nie należy wspomnieć że nie mogą to być słowa kluczowe tym bardziej że wcześniej byli wymienione.

PRZESTRZEŃ NAZW ”std::”
To nazwa przestrzeni, w której znajdują się nazwy zdefiniowane w bibliotece
”iostream” - z tego powodu wszystkie nazwy z tej biblioteki muszą być poprzedzone
nazwą przestrzeni, np. std::endl
Z tego zdania można by wywnioskować że tylko <iostream>. Od kiedy pliki nagłówkowe są bibliotekami? Biblioteką jest cały STL jego elementy to części biblioteki.

LICZBY
Z opisu by wynikało że zapis 17S lub 20U lub ... są niepoprawnymi liczbami. Dalej to jest wyjaśnione w stałych dosłownych, więc może tu nie jest potrzebne.

Polecenie ”return” – kończy wykonanie funkcji ”main” i zwraca do
programu wywołującego podaną liczbę całkowitą, np. return 0;
(wartość 0 oznacza brak błędów).
Przy niezbyt wnikliwym przeczytaniu można by pomyśleć że zawsze, w każdej funkcji.

Obiekt ”cout” – (domyślnie oznacza ekran monitora) zajmuje się
odbieraniem danych wyjściowych przesyłanych strumieniem ”<<”;
Dalej obiekty typu string nazywane po prostu zmiennymi a cout i cin to obiekty może należy jakoś ujednolicić, może zmienna strumienia wyjściowego. Po "domyślnie oznacza ekran monitora" trudno będzie wytłumaczyć że to jednocześnie może oznaczać - z przekierowanego pliku. Nazwanie operatora przesunięcia bitowego strumieniem, hmmm, słyszałem nazwę operator strumieniowy ale też wg mnie niefortunna nazwa. Wg mnie lepiej od razu wspomnieć że to jest operator przesunięcia danych później jak dojdzie do przesunięcia bitowego to przyznać się do lekkiego nagięcia prawdy, i usprawiedliwić się tym że bity to też dane :D

1

To, co zauważyłem na szybko:

  • Budowa liczby ze znakiem jest z punktu widzenia języka obojętna, a chyba wszystkie implementacje używają uzupełnienia do 2 a nie znak-moduł. Rozmiary fundamentalnych typów są nieokreślone (jedynie char musi mieć 1 bajt), określone są jedynie relacje pomiędzy nimi.
  • Przecinek przy definicji zmiennych to nie jest operator przecinka. Jego działanie w ogóle nie jest wyjaśnione (nawiasy nie są bez znaczenia).
  • Operator trójargumentowy jest z C.
  • To, co napisane jest o const_cast to totalna bzdura.
  • Program z uniami i wyświetlaniem reprezentacji double wykorzystuje undefined behavior.
  • Słowo kluczowe inline bardzo niewiele znaczy dla współczesnych kompilatorów a standard pozwala na jego olewanie. Ponadto funkcje te mają (a raczej mogą mieć) adresy.
  • To w jaki sposób dołącza się nagłówek z biblioteki C to nie jest kwestia stylu. To zmienia kilka rzeczy zupełnie (głównie to, co jest w przestrzeni nazw std::, niektóre makra są ponadto zastępowane funkcjami).
  • C++ nie jest żadnym rozszerzeniem języka C. To inny język, który wywodzi się z C. Biblioteki standardowe różnią się od siebie jeszcze bardziej.
  • Iteratory kontererów nie pochodzą z <iterator>.
  • Konstruktor kopiujący zazwyczaj musi mieć const w argumencie. Jeżeli się to pominie dzieją się "dziwne rzeczy" (związane z tym, że r-value nie można przekazać przez niestałą referencję).
  • "Operatory predefiniowane" są jakieś podejrzane. Kompilator po prostu wie jak pobrać adres obiektu i do czego służy przecinek, nie trzeba mu robić tego w taki sposób, w jaki przeciąża się operatory.
0

Dzięki za odzew. Poprawiam błędy. Jak poprawię wszystkie zauważone to opublikuję poprawione wersje tematów.

0

"Z polimorfizmem mamy do czynienia
podczas wywoływania wirtualnych funkcji składowych,
ponieważ odpowiednia funkcja składowa jest wybierana
niejawnie już podczas działania programu w zależności
od typu obiektu, który ją wywołał."

ja to rozumiem tak:
mamy wskaźnik na klasę bazową:
Bazowa * wsk = &objPochodna;
wsk->wirtualna();
Tutaj wskaźnik na klasę bazową wywołał funkcję wirtualną i nie jest wywoływana funkcja od klasy bazowej tylko od pochodnej. W tym zdaniu co podałeś można wywnioskować ,że to właśnie jest wywoływana funkcja od klasy bazowej: "w zależności
od typu obiektu, który ją wywołał" a wywołał ją właśnie wskaźnik na klasę bazową

Dla mnie lepiej by to brzmiało tak:
"Z polimorfizmem mamy do czynienia
podczas wywoływania wirtualnych funkcji składowych,
ponieważ odpowiednia funkcja składowa jest wybierana
niejawnie już podczas działania programu w zależności
od typu obiektu, na który wskazywała referencja bądź wskaźnik na klasę bazową"

0

Opublikowałem poprawione wersje tematów. Uwzględniłem błędy zauważone przez: winerfresh, fasadin i _13th_Dragon. Ufff, na dzisiaj mam już dość.

1

INICJALIZACJA – nadanie obiektowi wartości w momencie jego
powstania. Służy do tego operator ”=”.

a poniższy kod nie jest inicjalizacją?

int a(10);

L-WARTOŚĆ (l-value) to wszystko, co może stać po lewej
stronie operatora przypisania ”=” (może przyjmować wartości).

Nie jest to do końca prawda. Obecnie definiuje się l-wartość jako wyrażenie odnoszące się do pewnego obiektu w pamięci, a to znaczy, że można pobrać jej adres. Idąc dalej tym tokiem można wskazać takie l-wartości, które odnoszą się do stałych - a wtedy nie mogą już stać po lewej stronie operatora przypisania. l-wartości pierwotnie oznaczać miały wartości, które mogą stać po lewej stronie przypisania, ale obecnie nie jest to prawdą, jednak termin "l-wartość" pozostał w użyciu.

0

Na marginesie: Nie klikam nikomu, bo prawie każdy mi pomógł, a można kliknąć i tylko jedną odpowiedź. Ale widzę jeszcze u Was reputację. Jak to działa?

0
matek3005 napisał(a):

L-WARTOŚĆ (l-value) to wszystko, co może stać po lewej
stronie operatora przypisania ”=” (może przyjmować wartości).

Nie jest to do końca prawda. Obecnie definiuje się l-wartość jako wyrażenie odnoszące się do pewnego obiektu w pamięci, a to znaczy, że można pobrać jej adres. Idąc dalej tym tokiem można wskazać takie l-wartości, które odnoszą się do stałych - a wtedy nie mogą już stać po lewej stronie operatora przypisania. l-wartości pierwotnie oznaczać miały wartości, które mogą stać po lewej stronie przypisania, ale obecnie nie jest to prawdą, jednak termin "l-wartość" pozostał w użyciu.

A co powiecie na takie stwierdzenie:
Tylko l-wartość (nie każda) może stać po lewej stronie operatora ”=”. L-wartości mogą stać także po prawej stronie operatora ”=”.

Chodzi mi o odniesienie do często występującego błędu "lvalue required as left operand of assignment", który powstaje np. po wpisaniu takiego kodu:

int k = 1;
2 = k;
0
Endrju napisał(a):

Budowa liczby ze znakiem jest z punktu widzenia języka obojętna, a chyba wszystkie implementacje używają uzupełnienia do 2 a nie znak-moduł. Rozmiary fundamentalnych typów są nieokreślone (jedynie char musi mieć 1 bajt), określone są jedynie relacje pomiędzy nimi.

Uściśliłem, że chodzi mi o "g++" w 32-bitowym systemie operacyjnym.
Co do uzupełnienia do 2 i znak-moduł: w obydwu systemach pierszy bit mówi, jaki jest znak liczby. Za to różne są metody zmiany znaku liczby (O to Ci chodziło? Jeśli nie to daj znać o który tekst Ci chodzi).

Endrju napisał(a):

Przecinek przy definicji zmiennych to nie jest operator przecinka. Jego działanie w ogóle nie jest wyjaśnione (nawiasy nie są bez znaczenia).

Zmieniłem ten tekst i umieściłem go razem z pętlą "for" w temacie 1.6 - slajd 7.

Endrju napisał(a):

Operator trójargumentowy jest z C.

Poprawiłem to. Poprawiłem także terminologię.

Endrju napisał(a):

To, co napisane jest o const_cast to totalna bzdura.

Przeredagowałem to i mam nadzieję, że teraz brzmi to lepiej.

Endrju napisał(a):

Słowo kluczowe inline bardzo niewiele znaczy dla współczesnych kompilatorów a standard pozwala na jego olewanie. Ponadto funkcje te mają (a raczej mogą mieć) adresy.

Przeredagowałem to.

Endrju napisał(a):
  • To w jaki sposób dołącza się nagłówek z biblioteki C to nie jest kwestia stylu. To zmienia kilka rzeczy zupełnie (głównie to, co jest w przestrzeni nazw std::, niektóre makra są ponadto zastępowane funkcjami).
  • C++ nie jest żadnym rozszerzeniem języka C. To inny język, który wywodzi się z C. Biblioteki standardowe różnią się od siebie jeszcze bardziej.

Żeby to poprawić, przeredagowałem wszystkie tematy w 3 dziale. A tak na marginesie: czy powyższy cytat na pewno prawidłowo się formatuje? Tylko w podglądzie źle się formatuje.

Endrju napisał(a):

Iteratory kontererów nie pochodzą z <iterator>.

Przeredagowałem to. Rozumiem, że iterator każdego kontenera pochodzi z tego samego nagłówka z którego pochodzi dany kontener.

Endrju napisał(a):

Konstruktor kopiujący zazwyczaj musi mieć const w argumencie. Jeżeli się to pominie dzieją się "dziwne rzeczy" (związane z tym, że r-value nie można przekazać przez niestałą referencję).

Pisałem o tym już wcześniej, ale teraz mocniej to podkreśliłem.

Endrju napisał(a):

Program z uniami i wyświetlaniem reprezentacji double wykorzystuje undefined behavior.

Gdzie jest ten "undefined behavior" i jak to poprawić? Czy chodzi o to, że w innej architekturze double może mieć inny rozmiar?

string double2bin(double value)
{
  // Zakładam, że typy: ”double” i ”long long” mają taki sam rozmiar
  union double_bin { double l_double; long long l_int; } temp_val;
  temp_val.l_double = value;   string text = "  ";
  for (int i=63; i>=0; --i)
  { if (i==62 || i==51) text += "  ";
    text += abs((temp_val.l_int >> i) % 2)? '1': '0';
  }
  return text;
}
 
void main()
{
  cout << "WYLICZENIE WARTOŚCI DOUBLE: (-1)^ZNAK * 2^(CECHA-1023) "
          "* ( 1.0 + MANTYSA/(2^52) )" << endl << endl;
  cout << "LICZBA DZIESIĘTNA   ZNAK    CECHA             "
          "               MANTYSA" << endl;
  for (double d=-2.0; d<=2.0; d += 0.125)
    cout << setprecision(3) << setw(11) << fixed << d
         << "         " << double2bin(d) << endl;
}

Trochę to trwało, bo jest tego dużo.

1

W kodzie uzupełnień 2 do najstarszy bit informuje o znaku, ale bierze udział w kodowaniu wartości liczby. To chyba dość istotna różnica.

Co do const_cast, to nie wiem, czy już poprawiłeś w tym pdf czy nie (niestety nie pamiętam co konkretnie było napisane), ale nadal są tam bzdury. Jeżeli jakaś zmienna ma kwalifikator const to nie można jej zmieniać w żaden sposób. W absolutnie żaden - const_cast do tego nie służy i użycie go do tego celu jest błędem oraz UB. Ten operator istnieje np. po to, żeby można było wykorzystywać nowy kod z jakimiś starymi funkcjami w C. Załóżmy, że mamy jakąś starą funkcję, która przetwarza łańcuch znaków. Wiemy, że funkcja ta nie zmienia zawartości tego łańcucha, ale ktoś napisał, że przyjmuje ona char *. Żeby wysłać tam łańcuch const char * należy użyć const_cast ale można to zrobić tylko dlatego, że mamy pewność, że łańcuch ten nie jest modyfikowany.

Co do bibliotek, żeby pokazać Ci o jaką różnicę mi chodzi zobacz funkcję std::sqrt z <cmath> oraz tę samą funkcję w bibliotece C <math.h>. W tej pierwsze funkcja sqrt jest wielokrotnie przeciążona i znajduje się w przestrzeni nazw std. W drugiej, przed C99 była tylko jedna funkcja sqrt, potem dodano kolejne, ale z innymi nazwami.

UB z uniami nie da się poprawić. Jeżeli do unii wpisałeś coś do pola X to potem możesz odczytać tylko pole X. Odczytanie pola Y jest UB, ale w 99.9% przypadków to zadziała - ciężko to nazwać błędem (chociaż standard tak mówi) bo unie są powszechnie wykorzystywane w ten sposób. Całkowicie bezpieczną metodą na podejrzenie zawartości takiego double jest skopiowanie tych bitów do innej zmiennej np. za pomocą memcpy. Warto to po prostu wiedzieć, bo pokazuje to, jaki język C++ jest czasami dziwny.

0
Endrju napisał(a):

W kodzie uzupełnień 2 do najstarszy bit informuje o znaku, ale bierze udział w kodowaniu wartości liczby. To chyba dość istotna różnica.

Zgadza się. Tylko, że odbiegliśmy nieco od tematu, bo właściwie nie wiem w kontekście jakiego z moich tekstów to pisałeś. Nie wiem, gdzie mam coś poprawić.

Endrju napisał(a):

Co do const_cast, to nie wiem, czy już poprawiłeś w tym pdf czy nie (niestety nie pamiętam co konkretnie było napisane), ale nadal są tam bzdury. Jeżeli jakaś zmienna ma kwalifikator const to nie można jej zmieniać w żaden sposób. W absolutnie żaden - const_cast do tego nie służy i użycie go do tego celu jest błędem oraz UB. Ten operator istnieje np. po to, żeby można było wykorzystywać nowy kod z jakimiś starymi funkcjami w C. Załóżmy, że mamy jakąś starą funkcję, która przetwarza łańcuch znaków. Wiemy, że funkcja ta nie zmienia zawartości tego łańcucha, ale ktoś napisał, że przyjmuje ona char *. Żeby wysłać tam łańcuch const char * należy użyć const_cast ale można to zrobić tylko dlatego, że mamy pewność, że łańcuch ten nie jest modyfikowany.

Rozumiem, że według standardów prawidłowe jest tylko rzutowanie ze zmiennej do const. Ale może w praktyce czasami trzeba obejść czyjś kiepski kod i wtedy programista może być zmuszony do użycia takich rozwiązań, jak rzutowanie z const do zmiennej mimo, że jest to UB. Z tego powodu zostawię ten kod, a uczniom będę podkreślał, że użycie tego typu rozwiązań jest ostatecznością (taki backdoor). W końcu "g++" podany przeze mnie przykład skompilował bez żadnych błędów, ostrzeżeń, czy nawet informacji i zadziałał zgodnie z intencją. A więc do czegoś jednak może się to w praktyce przydać (sam nigdy nie miałem potrzeby stosowania tego typu rzutowania, ale o takiej możliwości wspomina m. in. J. Grębosz).

Endrju napisał(a):

Co do bibliotek, żeby pokazać Ci o jaką różnicę mi chodzi zobacz funkcję std::sqrt z <cmath> oraz tę samą funkcję w bibliotece C <math.h>. W tej pierwsze funkcja sqrt jest wielokrotnie przeciążona i znajduje się w przestrzeni nazw std. W drugiej, przed C99 była tylko jedna funkcja sqrt, potem dodano kolejne, ale z innymi nazwami.

Dzięki za linki. Nie znałem tego adresu. Przyda się. A co do tych bibliotek, to fakt, różnice są. Niektóre z tych tekstów pisałem w nocy z ledwo otwartymi oczami i potem często dopiero na lekcji podczas prezentacji slajdów sam dziwiłem się, jak mogłem coś takiego napisać :-D

Endrju napisał(a):

UB z uniami nie da się poprawić. Jeżeli do unii wpisałeś coś do pola X to potem możesz odczytać tylko pole X. Odczytanie pola Y jest UB, ale w 99.9% przypadków to zadziała - ciężko to nazwać błędem (chociaż standard tak mówi) bo unie są powszechnie wykorzystywane w ten sposób. Całkowicie bezpieczną metodą na podejrzenie zawartości takiego double jest skopiowanie tych bitów do innej zmiennej np. za pomocą memcpy. Warto to po prostu wiedzieć, bo pokazuje to, jaki język C++ jest czasami dziwny.

Tego przykładu nie chcę już bardziej komplikować, bo dla wielu uczniów przytoczony przeze mnie kod będzie wystarczająco skomplikowany.

1

@VoyciecHo ty chyba zupełnie nie rozumiesz o co chodzilo koledze @Endrju z tymi constami. Chodzi o to że nawet jeśli zrobisz const_cast to NIE WOLNO zmieniać ci tego obiektu, mimo że zniknęło z niego const. Ale robi się tak kiedy wiemy że coś na pewno nie zmienia obiektu, ale jednocześnie wymaga obiektu nie-const a my chcemy tam podać obiekt const. Zapewniam cię ze to nie problem napisać kod który po modyfikacji zmiennej const z której usuniesz const za pomocą const_cast się wysypie...
Nie znałeś linka do cplusplus reference? o_O Aż boję się pytać w takim razie z czego korzystałes...

#include <iostream>
#include <cstring>
using namespace std;

int main(){
    //const char* napis_w_pamieci_niemodyfikowalnej = "Ala ma kota!";
    //char* napis_w_pamieci_niemodyfikowalnej2 = const_cast<char*>(napis_w_pamieci_niemodyfikowalnej);
    //strcpy(napis_w_pamieci_niemodyfikowalnej2,"Sierotka");
    
    const char napis_na_stosie[] = "Ala ma kota!";
    char* napis_na_stosie2 = const_cast<char*>(napis_na_stosie);
    strcpy(napis_na_stosie2,"Sierotka");
    return 0;
}

Odkomentuj sobie te 3 linijki i zobacz co się stanie.

0

@Shalom, poniższy kod jest przykładem prawidłowego użycia "const_cast"?

#include <iostream>
using namespace std;
void wyswietl_napis(char *napis) { cout << napis << endl; }
int main()
{
  const char *NAPIS = "Tekst";
  char *napis = const_cast<char *>(NAPIS);
  wyswietl_napis(napis);
}
1

@VoyciecHo fajnie że konsultujesz materiały na forum programistycznym.

Przeglądnąłem 2 lub 3 prezentacje i znalazłem pomyłkę(chyba że ktoś już o tym wspomniał ale nie rzuciło mi się w oczy):
IV Klasy, 4. Więcej o konstruktorach.

WŁASNOŚCI LISTY INICJALIZACYJNEJ – oto one:
(...)
kolejność na liście inicjalizacyjnej nie ma znaczenia – obowiązuje kolejność
występowania definicji w ciele klasy;
(...)

i późniejszy przykład z niezachowaniem kolejności względem deklaracji w definicji klasy. Otóż zmienne klasy powinny być inicjalizowane w kolejności jak zostały zdefiniowane. Kompilacja przykłądu ze slajdu w g++ z flagą -Wall daje ostrzeżenie o nie zachowaniu kolejności.

 
// konstruktor z poprawioną kolejnością
Abc(double ad, int ai, char ac): i(ai), d(ad)
{
    c = ac;
}

1 użytkowników online, w tym zalogowanych: 0, gości: 1