Jazyk C++ není jen rozšířením C, přináší i některá omezení a drobné
nekompatibility. Nejdůležitější omezení si dnes projdeme a ukážeme si, že rozumně napsaný kód v C bude fungovat i jako C++.
23.1.2006 06:00 | Jan Němec | read 24789×
DISCUSSION
Drobná omezení C++
Restrikcí je v C++ oproti C celá řada, ale zpravidla se nejedná o podstatné
záležitosti a v některých případech je pro plné pochopení rozdílu jazyků
zapotřebí poměrně pokročilá znalost příslušných norem. Navíc téměř vždy
kód, který lze přeložit pouze jako C nebo má dokonce v obou jazycích jiný
význam, odporuje zásadám správného programování v C, i když je třeba v souladu
s normou jazyka.
Nová klíčová slova
C++ obsahuje celou řadu nových klíčových slov, které pochopitelně nemůžeme
použít jako identifikátory. Následující definice je sice korektní v C, ale
v C++ neprojde překladem, neboť class je zde klíčové slovo.
int class = 1;
Přísnější typová kontrola
V C++ neprojdou některé implicitní konverze, například mezi ukazateli různých
typů
int *pi;
pi = malloc(sizeof(int));
a je třeba explicitně přetypovávat. Přísnější pravidla se týkají především
ukazatelů, ale změny jsou i u přetypování intu na výčtový typ a podobně.
int *pi;
pi = (int *) malloc(sizeof(int));
Návratová hodnota funkcí
Funkce, která formálně podle hlavičky vrací hodnotu nějakého typu různého od
void, ji musí vracet i fakticky.
int funkce() {
}
Jedná se o užitečnou kontrolu běžné programátorské chyby - zapomenutého
returnu. V poměrně vzácných případech funkcí, které nikdy nekončí standardním
způsobem (například zavolají exit, exec, nekonečnou smyčku a podobně) nebo
nás jejich návratová hodnota nezajímá, ale přesto mají z nějakého důvodu
v hlavičce uvedený neprázdný návratový typ, tak musíme zavolat return.
Prototyp funkcí
Původní verze C se chovaly vůči parametrům funkcí trochu jako asembler.
Volající kód prostě uložil na zásobník nějaké parametry a jejich počet
a typ plně závisel na rozhodnutí programátora. Kód funkce pak definoval
parametry podobně jako lokální proměnné a bylo jen na programátorovi, zda
se předané a vybrané parametry typem, počtem a pořadím shodují. Díky tomu
překladač nepotřeboval znát v místě volání hlavičku funkce. Uvedený postup
například umožňuje dávat funkcím přebytečné parametry, které nevyužijí, a
jistě i některé mnohem nebezpečnější věci od nepřenositelného kódu až po
vyložené chyby. Na dalším vývoji jazyka C je vidět snaha tato pravidla zpřísnit
a neumožnit zde přílišnou a nebezpečnou volnost, ale teprve C++ řeší problém
definitivně. Překladač musí znát v místě volání prototyp funkce
a ta musí být zavolána přesně podle hlavičky. (Situaci v C++ trochu komplikují
implicitní konverze, implicitní parametry a přetěžování funkcí, nicméně na
úrovni přeloženého kódu to na celé věci nic nemění.)
Stará definice podle původní normy C K & R je v C++ zakázána.
Návratová hodnota
Funkce musí vždy explicitně uvádět návratový typ a tento typ nesmí být
deklarován přímo v hlavičce funkce. Možná vás to překvapí, ale C by mělo
umožnit (ale v praxi často neumožňuje) například zápis
struct s {int i;} f(void) {
/* ... */
}
Funkce main
Funkce main nesmí být rekurzivní a nelze získat její adresu.
Předběžná definice
C umožňuje opakovaně předběžně definovat globální (ale již nikoli lokální)
proměnnou i bez klíčového slova extern, pouze jednou však smí být v definici
inicializována. Jedná se pochopitelně o jedinou proměnnou, i když je definována
vícekrát. V C++ je to zakázané.
int i;
int i;
int i = 1;
int main(void) {
return 0;
}
I v C++ je samozřejmě možné nejprve deklarovat proměnnou (typicky v hlavičkovém
souboru) pomocí extern a to klidně i vícekrát, ale definována bez extern smí
být pouze jednou. Tento postup se dnes považuje za standardní v obou jazycích.
Skok přes lokální definici
V C++ je zakázáno přeskočit například pomocí goto nebo break ve switch
lokální definici proměnné. Standard C++ totiž umožňuje (a překladač C často
toleruje) definici lokální proměnné mimo začátek bloku tj. až po nějakém
příkazu. V C++ má každý typ formálně nějaký konstruktor (i když třeba prázdný)
a překladač jej proto nedovolí přeskočit, výjimkou je pouze přeskočení celého
bloku. Jedná se o poměrně otravné omezení, které je navíc u typů bez faktického
konstruktoru zcela zbytečné.
switch (i) {
case 0:
int j;
/* Proveď něco */
break;
case 1:
/* Proveď něco jiného */
break;
}
Nejjednodušší zpravidla bývá kus kódu s definicí proměnné zabalit do bloku.
switch (i) {
case 0: {
int j;
/* Proveď něco */
}
break;
case 1:
/* Proveď něco jiného*/
break;
}
Samozřejmě programátor by měl v podobných případech zvážit, zda není lepší
kód rozčlenit a například pro každou case větev definovat funkci.
String o jedničku menší
C umožňuje definovat řetězec o jedničku menší než je jeho inicializace, pokud
ji počítáme i s ukončovací nulou. V C++ to neprojde.
char str[3] = "C++";
puts(str);
Spoléhat se však na to, že překladač C za řetězec umístí znak '\0', by bylo
dosti riskantní, například moje gcc 4.0.1 to tak neudělá. Funkce puts v uvedeném
příkladu tak nejspíše vypíše za řetězcem ještě kus paměti až po nejbližší nulu,
případně program spadne.
Otevřené pole
C umožňuje použít ve struktuře pole bez uvedené velikosti, C++ to zakazuje.
struct T {
int i;
char s[];
};
V tomto případě však gcc nepotvrdilo moje teoretické znalosti a příklad jsem
přeložil i jako C++.
Kompatibilita struktur
V C jsou do určité míry (například pro přiřazení) zaměnitelné dvě na dvou
místech stejným způsobem definované struktury, v C++ nikoli, zde se jedná o 2
různé typy. Moje pokusy s gcc (bez ohledu na jazyk a přepínače určující normu)
ovšem skončily chybou při překladu.
Dalším podstatným rozdílem je prostor jmen struktur a typů. Z nějakých
(poměrně nepochopitelných) důvodů C zavádí místo jediného hned dva různé
pojmy: typ a struktura. Tím pádem lze v C pomocí nejrůznějších kombinací
klíčových slov struct a typedef a definice proměnné hned několika způsoby
zavést proměnnou strukturovaného typu. Jméno typu a jméno struktury jsou
přitom dvě různé věci, navíc existují pro obojí dva odlišné prostory jmen.
Lze tedy pojmenovat strukturu stejně jako nějaký (třeba zcela nesouvisející)
typ. V C++ lze jméno struktury použít, jako by to byl typ, a samostatný prostor
jmen struktur zde neexistuje. Díky tomu může dojít při překladu C kódu pomocí
překladače C++ ke konfliktu identifikátorů.
typedef int signed32;
struct signed32 {
int i;
};
Uvedené definice typů by asi žádný rozumný programátor nenapsal. Hodilo by se
však pojmenovat nějaký konkrétní typ a odpovídající strukturu stejně. To
naštěstí možné je i v C++.
/* Projde v C i v C++. */
typedef struct T {
int i;
int j;
} T;
C dokonce umožňuje v rámci struktury definovat proměnnou, která má jméno
stejné jako nějaký typ definovaný pomocí typedef. C++ to jednoznačně zakazuje.
typedef int signed32;
struct T {
int signed32;
};
Ukázali jsme si, že i v čistém C lze snadno psát tak, aby výsledný kód
odpovídal rovněž normě C++ a to bych také programátorům doporučil. Vede to k větší
přenositelnosti a tím i použitelnosti nejen kódu, ale i programátora samotného.
Konstrukce, které projdou pouze v C jsou ve většině případů nebezpečné a často
naznačují místa možných chyb. Trochu pedantské je snad jen explicitní
přetypovávání ukazatelů po malloc, ale v řadě jiných případů odhalí právě
silnější typová kontrola C++ nepříjemné chyby již v době překladu.
Domácí úkoly
Ověřte si rozdíly mezi C a C++ uvedené v článku na svých oblíbených
překladačích. Při použití nějaké novější verze gcc by mělo vše fungovat tak,
jak je v článku popsáno, ale s jinými překladači můžete narazit na odlišnosti.
Vyzkoušejte si na některých příkladech z předchozích dílů tohoto seriálu,
zda je možné překládat běžný C kód překladačem C++. Pokud narazíte na problém,
napište o něm do diskuse pod článkem.
Pokračování příště
I v příštím dílu nás budou zajímat odlišnosti obou jazyků.