Niekompatybilności z językiem C
Powoli i nie bez bólu zgodzono się na to, że nie powinno być niczym nie uzasadnionych niezgodności między językiem C++ a językiem ANSI C, ale też przyjęto, że istnieje coś takiego, jak uzasadniona niezgodność. [...] Później uznano taką zasadę: `język C++: tak bliski języka C, jak tylko możliwe, ale nie bliższy' [...]. Miarą sukcesu takiej polityki jest to, że każdy przykład w książce Kernighana i Ritchie'ego jest napisany w języku C będącym podzbiorem języka C++. Bjarne Stroustrup
Wstęp
Wiele z zamieszczonych tu informacji były już po części wspominane wcześniej. Może i nie będą specjalnie przydatne, ale warto się z nimi zapoznać przed zapoznaniem się z elementami biblioteki standardowej C. Nie wszystkie są specjalnie uciążliwe, postanowiłem tutaj jednak zebrać to wszystko “do kupy”.
Struktury, wskaźniki, konwersje
W C panowały następujące zasady, które nie przetrwały w C++:
- Typ strukturalny i wyliczeniowy był poprzedzony słowem kluczowym identyfikującym (struct/union/enum); w konsekwencji nie zajmowało to identyfikatora w bieżącym zasięgu (co prawda nadal można używać tego identyfikatora przed nazwą typu, ale zajmowanie nazwy typu ma swoje konsekwencje).
- Struktura zagnieżdżona w innej ma w C++ nazwę z podaniem operatora zasięgu, podczas gdy w C była identyczna, jak gdyby była zadeklarowana normalnie w zewnętrznym zasięgu.
- Wskaźnik void* jest typem uniwersalno-wskaźnikowym; jest on niejawnie konwertowany na każdy typ wskaźnikowy (z zastrzeżeniami co do wariancji); w szczególności zresztą w C można konwertować niejawnie dwa dowolne wskaźniki między sobą i z wartością całkowitą, void* wyróżniłem z uwagi na to, że jest to powszechnie w C stosowane, bo kompilator nie rzuca wtedy ostrzeżeń. W C++ niejawne konwersje pomiędzy wartościami wskaźnikowymi niepokrewnych typów są zabronione.
Kwestia 'const' i typy literałów
Język ANSI C, mimo wprowadzenia (zapożyczonego zresztą z C++) słowa const, niewiele mógł na jego rzecz w języku zrobić. Efektem tego wszystkiego są zupełnie odmienne sposoby traktowania owego const w C i C++.
Przede wszystkim, w języku C const nie służy w ogóle do tworzenia stałych (w języku C zresztą pojęcie "stałej" nie istnieje). To, co const w ANSI C ma wspólnego z const w C++ to tylko i wyłącznie stałość wskaźnika (tyle że przypisanie wskaźnika na obiekt stały do wskaźnika na obiekt zmienny nie jest w ANSI C błędem). Co do obiektów zaś, to obiekt zadeklarowany z const w ANSI C nie różni się od obieku zadeklarowanego bez const właściwie niczym, poza tym, że zabrania się taki obiekt zmieniać. W C++ np. stałe mają wiązanie statyczne i dlatego np. można je deklarować w plikach nagłówkowych (w przypadku języka C taki myk wymagałby "static const", co zresztą i tak nie wniesie niczego w kwestii optymalizacji). W C stałe, podobnie jak zmienne, mają wiązanie takie, jakie im zadeklarowano.
Stałe w C++ (a już zwłaszcza stałe całkowite, bo to tych stałych głównie najbardziej owa różnica dotyka) są elementami na poziomie podobnym do literałów. Aby osiągnąć to samo w języku C, należy użyć w tym celu makra preprocesora (ewentualnie enum). Za pomocą takiej stałej bowiem możemy deklarować rozmiar dla tablicy, możemy też podawać jako stałe w konstrukcji switch/case. W C nie jest to dozwolone; tam stała to jest tylko taka zmienna, tyle że stała :).
Takoż jedną z rzeczy, której wprowadzenie do ANSI C nie zmienił fakt dodania const, jest typ stałej napisowej. W C++ napis składający się z N liter ma zawsze typ
No i niestety, nie usuwa to wszystkich "przykrych konsekwencji". Ponieważ napis jest tablicą o stałych znakach, zatem kompilator ma prawo (i zaręczam, że z niego korzysta) umieścić te obiekty w pamięci nie przeznaczonej do zapisu (co oznacza, że próba dokonania zapisu w takim obiekcie kończy się wylotem). W praktyce to oznacza, że jeśli mamy program w ANSI C, gdzie do char* przypisuje się napis w cudzysłowiu, to nosi on w sobie pewien mały potencjał destrukcyjny. Co prawda dokonywanie zmian w napisie, który jest zahardkodowany w cudzysłowiu, wydaje się być absurdalne, to jednak trzeba pamiętać, że jeśli użyjemy const char*, to mamy żywy dowód, że nikt tam nic nie zmienia, znacznie bardziej wiarygodny niż niewiele warta w praktyce (niestety) wiara w to, że programiści nie robią głupot w programie (co prawda przy const char* należy jeszcze brać poprawkę na rzutowanie wymuszone, ale już nie popadajmy w paranoję:). Dodatkowo, kompilatory mają też zazwyczaj opcje, które pozwalają traktować napis w cudzysłowiu jako zmienną.
Dodatkowo jeszcze jedna różnica w typach literałów. Literał w apostrofach, np. 'x' jest w C++ wartością typu char, natomiast w C jest typu int. Widywałem takoż wykorzystanie tego w C, na przykład podając więcej, niż jeden znak w apostrofach ('FORM'). Jest to oczywiście tak samo poprawne w C i w C++, czyli efekt jest zależny od implementacji (najczęściej robi się z tego liczbę całkowitą, której kolejne bajty - ułożone w tej samej kolejności, czyli niezależnie od endianu! - mają wartości podanych znaków). Jest to zatem właściwość, której nie powinno się używać (i jeśli kompilator takie coś łyka i obsługuje, to znaczy że jest to rozszerzenie kompilatora!). W gcc tak właśnie się dzieje, choć oczywiście stosowane jest ostrzeżenie (w C++ taki 'FORM' ma wtedy typ int).
Opuszczanie `int'; styl `K&R' definiowania argumentów funkcji
Jest to składnia (K&R to oczywiście "Kernighan & Ritchie", twórcy języka C; pierwotnie w języku C obowiązywała wyłącznie taka właśnie składnia) o następującej postaci (podam już przykładowo, bez schematów):
int strcpy( dest, source ) char *source, *dest; { ... }
Zaletą tego stylu jest to, że można podawać listę zmiennych, tak jak przy deklaracjach zmiennych lokalnych. Nie obowiązuje też oczywiście w takich deklaracjach żadna kolejność.
Posiada również wady. Czytelności tej składni niestety zarzucić nie można. W ogóle nie widać tego, jakie typy argumentów są oczekiwane na konkretnych pozycjach (nawet jeśli programista zachowa kolejność w deklaracjach). Założenia standardu C nie nakazują zachowania zgodności z normalnymi zapowiedziami; niektóre archaiczne kompilatory C nie dopuszczają innych zapowiedzi, niż bez specyfikacji argumentów. Z kolei gdyby używać tych specyfikacji, to ich synchronizacja jest w ten sposób dodatkowo utrudniona.
Ta składnia oczywiście nie zaakceptowała się w C++. Przytaczam to tylko jako ciekawostkę, którą nadal można spotkać w programach, których autorzy na siłę starają się dostosować swoje dzieła do kompilatorów z epoki kamienia łupanego. Nawet sami twórcy C uważają, że jest to anachronizm, który prawdopodobnie zniknie w którymś z następnych standardów.
W C, jak też w pierwotnych wersjach C++, istniała z trudem stłumiona zasada, że w deklaracjach czegokolwiek oprócz zmiennych, można było pominąć nazwę "typu czołowego", jeśli był nim int. W sumie dziś pozostała ona wyłącznie (w C) w formie ostrzeżeń kompilatora (np. jeśli deklaruje się funkcję, lub zmienną ze słowem static, extern czy auto). Proszę sobie wyobrazić, co w efekcie oznaczała ona w połączeniu ze stylem K&R:
fn( a, b, c ) char* b;
{
...
}
co odpowiada "normalnemu" nagłówkowi:
int fn( int a, char* b, int c );
extern "C"
Wspominałem już o tym niejednokrotnie, chciałbym tutaj jedynie uściślić kwestię używania extern "C". Dyrektywa ta pozwala na zaimportowanie kodu dostowowanego do C dla C++. Okoliczności, w których konieczne jest jego użycie to przede wszystkim deklaracje funkcji C. Nazwa takiej funkcji nie jest manglowana, a zewnętrznie funkcja jest traktowana jak funkcja C, co oznacza, że nie może być nią operator (tzn. właściwie to może, sprawdziłem to na g++ faktycznie, operator + miał zewnętrzną niezmanglowaną nazwę - __pl!) i nie może być przeciążona (nie znaczy to oczywiście, że nie da się jej przeciążyć, a jedynie, że tylko jedna z przeciążonych funkcji może być extern "C").
Brak manglowania nazwy jest jedną z istotniejszych rzeczy w extern "C". Podczas wiązania poszukuje się funkcji o określonej nazwie, a nazwy funkcji z biblioteki standardowej C są przecie zapisane w "gołej" postaci.
Restrykcyjność
Poza tym, co już wymieniłem należy jeszcze pamiętać o następujących rzeczach:
- w C++ używanie prototypów (zapowiedzi) funkcji jest absolutnie obowiązkowe (g++ w wersji 2.7.2 przyjmował, że niezapowiedziana funkcja ma deklarację "uniwersalną", o czym ostrzegał; aktualne wersje już generują błąd)
- Pusta lista argumentów w C++ jest synonimem (void), natomiast odpowiednikiem () w C jest (...) w C++ (ma sens tylko z extern "C")
- Globalne dane w C++ można zadeklarować dokładnie RAZ. Istnieje konkretnie podział na obiekty o słabym i silnym wiazaniu, przy czym obiekty o słabym wiązaniu to tylko te generowane na potrzeby samego języka (czyli np. tablice metod wirtualnych, czy wersje out-line funkcji inline). Obiekty deklarowane jawnie podlegają zawsze silnemu wiazaniu, więc próba zlinkowania dwóch plików deklarujących symbol o tej samej nazwie zakończy się niepowodzeniem
- Globalne stałe w C++ (const) mają wiązanie statyczne, podczas gdy w C było zewnętrzne. Aby stała w C++ miała wiązanie zewnętrzne, należy dodać modyfikator extern. Stała może być również deklarowana tylko raz, a zaimportowanie różni się od deklaracji tylko tym, że deklarowana jest inicjalizowana
- Typ wyliczeniowy jest w C++ traktowany jako oddzielny typ, z tym tylko zastrzeżeniem, że konstruktor typu int może przyjmować dowolny enum, jednak niejawne konwersje enumów na int są niedozwolone.