C/C++ (32) - Omezení C++ oproti C

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 | přečteno 24524×

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ů.

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=1085