Raphael A. Finkel
Studenti pokročilých kurzů informatiky (CS) by měli mít základní znalosti o jazyku C, Unixu a nástrojích pro vývoj softwaru. Tato stránka podrobně popisuje některé z těchto základů a nabízí cvičení.
C
Globální proměnné, řetězce, buffery, dynamická alokace, celá čísla, struktury, ukazatele, výstup, parametry příkazového řádku, vlastnosti jazyka, více zdrojových souborů, propojení více objektových souborů, ladění.
Jazyk C umožňuje deklarovat proměnné mimo jakoukoli funkci. Tyto proměnné se nazývají globální proměnné.
- Globální proměnná se alokuje jednou při spuštění programu a zůstává v paměti, dokud program neskončí.
- Globální proměnná je viditelná pro všechny funkce ve stejném souboru.
- Můžete zpřístupnit globální proměnnou deklarovanou ve souboru
A.c
všem funkcím v jiných souborechB.c
,C.c
,D.c
atd. tím, že ji deklarujete s modifikátoremextern
ve zmíněných souborech, například:
extern int theVariable;
- Pokud sdílí proměnnou více souborů, deklarujte ji jako
extern
v záhlavním souborufoo.h
a připojte tento soubor pomocí#include foo.h
ve všech souborechB.c
,C.c
,D.c
atd. Proměnnou musíte deklarovat přesně v jednom souboru bez modifikátoruextern
, jinak nebude vůbec alokována.
Používat příliš mnoho globálních proměnných není dobrý nápad, protože není možné lokalizovat místa, kde jsou používány. Existují však situace, kdy globální proměnné umožňují vyhnout se předávání velkého množství parametrů funkcím.
Řetězce
Řetězce v jazyce C jsou ukazatele na pole znaků ukončené nulovým znakem (\0
).
- Deklarace řetězce:
char *variableName;
- Pokud je váš řetězec konstantní, můžete mu přiřadit literál:
char *myString = "Toto je ukázkový řetězec";
- Prázdný řetězec obsahuje pouze nulový znak:
myString = ""; // prázdný řetězec, délka 0, obsahuje null
- Ukazatel
NULL
není platnou hodnotou řetězce:
myString = NULL; // neplatný řetězec
Ukazatel NULL
můžete použít k označení konce pole řetězců:
argv[0] = "progName";
argv[1] = "firstParam";
argv[2] = "secondParam";
argv[3] = NULL; // terminátor
Pokud je řetězec vypočítán za běhu programu, musíte vyhradit dostatek místa pro jeho uložení, včetně prostoru pro nulový znak:
char *myString;
myString = (char *) malloc(strlen(someString) + 1); // alokace paměti
strcpy(myString, someString); // kopírování řetězce
Abyste zabránili únikům paměti, vždy uvolněte paměť alokovanou pomocí malloc
s funkcí free
. Parametrem funkce free
musí být počáteční adresa alokované paměti:
free((void *) myString);
Abyste udrželi svůj kód čistý a čitelný, zavolejte free()
ve stejné funkci, kde jste použili malloc()
. Mezi těmito body můžete volat jiné funkce pro práci s řetězcem.
Při kopírování řetězců buďte opatrní, aby nikdy nedošlo ke kopírování více bajtů, než kolik může cílová datová struktura pojmout. Přetečení bufferu je nejčastější příčinou bezpečnostních problémů v programech. Vhodnější je používat strncpy()
a strncat()
místo strcpy()
a strcat()
.
Buffery
Buffer je oblast paměti, která funguje jako kontejner pro data. I když data mohou mít určitý význam (například pole struktur s mnoha poli), programy, které čtou a zapisují buffery, je často zpracovávají jako pole bajtů.
- Pole bajtů není totéž co řetězec, i když jsou obě deklarována jako
char *
nebochar []
. - Mohou obsahovat jiné než ASCII znaky a nemusí být ukončeny nulovým znakem.
- Nelze použít
strlen()
k určení délky dat v bufferu (buffer může obsahovat nulové bajty). Místo toho zjistíte délku dat pomocí návratové hodnoty systémového volání (napříkladread
), které data generovalo. - Pro práci s bajtovými buffery používejte
memcpy()
nebobcopy()
místostrcpy()
astrcat()
.
Zápis bufferu do souboru
Ukázka zápisu bufferu o velikosti 123 bajtů do souboru:
char *fileName = "/tmp/foo";
#define BUFSIZE 4096
char buf[BUFSIZE]; // buffer o velikosti maximálně BUFSIZE bajtů
...
int outFile; // souborový deskriptor
int bytesToWrite; // počet bajtů k zápisu
char *outPtr = buf;
...
if ((outFile = creat(fileName, 0660)) < 0) { // chyba
perror(fileName); // výpis příčiny chyby
exit(1);
}
bytesToWrite = 123; // příklad inicializace
while ((bytesWritten = write(outFile, outPtr, bytesToWrite)) < bytesToWrite) {
if (bytesWritten < 0) { // chyba
perror("write");
exit(1);
}
outPtr += bytesWritten;
bytesToWrite -= bytesWritten;
}
Chcete-li, aby překladač alokoval paměť pro buffery, deklarujte buffer s velikostí, kterou lze vypočítat:
#define BUFSIZE 1024
char buf[BUFSIZE];
Pokud deklarujete buffer bez velikosti:
char buf[];
má neznámou velikost a jazyk C pro něj nealokuje žádnou paměť. Pokud velikost bufferu neznáte během kompilace, použijte dynamickou alokaci:
char *buf = (char *) malloc(bufferSize);
kde bufferSize
je výsledek výpočtu za běhu programu.
Dynamicky alokovatelná a uvolňovaná paměť
Paměť můžete dynamicky alokovat a uvolňovat.
Individuální instance jakéhokoliv typu:
typedef ... myType;
myType *myVariable = (myType *) malloc(sizeof(myType));
// Nyní můžete přistupovat k *myVariable
...
free((void *) myVariable);
Dobrou praxí je volat free()
ve stejné funkci, kde jste použili malloc()
.
Jednorozměrná pole jakéhokoliv typu:
myType *myArray = (myType *) malloc(arrayLength * sizeof(myType));
// Nyní jsou alokovány prvky myArray[0] .. myArray[arrayLength - 1]
...
free((void *) myArray);
Dvourozměrná pole:
Dvourozměrná pole se reprezentují jako pole ukazatelů, přičemž každý ukazatel směřuje na pole.
myType **myArray = (myType **) malloc(numRows * sizeof(myType *));
int rowIndex;
for (rowIndex = 0; rowIndex < numRows; rowIndex += 1) {
myArray[rowIndex] = (myType *) malloc(numColumns * sizeof(myType));
}
// Nyní jsou alokovány prvky myArray[0][0] .. myArray[numRows-1][numColumns-1]
// Můžete je inicializovat
...
for (rowIndex = 0; rowIndex < numRows; rowIndex += 1) {
free((void *) myArray[rowIndex]);
}
free((void *) myArray);
Pokud používáte C++, nemíchejte new/delete
s malloc/free
ve stejné datové struktuře. Výhoda new/delete
u instancí tříd spočívá v tom, že automaticky volají konstruktory a destruktory pro inicializaci a ukončení dat. Při použití malloc/free
musíte inicializaci a ukončení provádět ručně.
Celá čísla
V jazyce C jsou celá čísla obvykle reprezentována na 4 bajtech. Například číslo 254235
je uloženo jako binární číslo 00000000, 00000011, 11100001, 00011011
.
ASCII text však reprezentuje čísla jako běžné znaky, s jedním bajtem na číslici podle standardního kódování. V ASCII je číslo 254235
reprezentováno jako 00110010, 00110101, 00110110, 00110010, 00110011, 00110101
.
Efektivní práce s celými čísly:
Pokud potřebujete uložit soubor celých čísel, je obecně efektivnější jak z hlediska místa, tak času ukládat 4bajtové verze než je převádět na ASCII řetězce. Například zápis jednoho celého čísla do otevřeného souboru:
write(outFile, &myInteger, sizeof(myInteger));
Práce s jednotlivými bajty celého čísla
K jednotlivým bajtům celého čísla můžete přistupovat pomocí převodu na strukturu:
int IPAddress; // uložen jako celé číslo, chápaný jako 4 bajty
typedef struct {
char byte1, byte2, byte3, byte4;
} IPDetails_t;
IPDetails_t *details = (IPDetails_t *) (&IPAddress);
printf("Bajt 1: %o, Bajt 2: %o, Bajt 3: %o, Bajt 4: %o\n",
details->byte1, details->byte2, details->byte3, details->byte4);
Různé reprezentace vícebajtových čísel
Vícebajtová čísla mohou být na různých strojích reprezentována odlišně:
- Některé (např. Sun SparcStation) ukládají nejvýznamnější bajt jako první (big-endian).
- Jiné (např. Intel i80x86 a jeho následovníci) ukládají nejméně významný bajt jako první (little-endian).
Pokud zapisujete celá čísla, která mohou být čtena na jiných strojích, použijte převod na „síťový“ formát bajtů pomocí htons()
nebo htonl()
. Při čtení dat v „síťovém“ formátu je převeďte na místní formát pomocí ntohs()
nebo ntohl()
.
Rozložení paměti struktur
Paměťové uspořádání struktur a hodnotu vrácenou sizeof()
můžete předvídat. Například:
struct foo {
char a; // zabírá 1 bajt
// C přidá 3 bajty výplně, aby proměnná b začínala na hranici 4 bajtů
int b; // zabírá 4 bajty
unsigned short c; // zabírá 2 bajty
unsigned char d[2]; // zabírá 2 bajty
};
Proto sizeof(struct foo)
vrátí hodnotu 12. Tato předvídatelnost (pro danou architekturu) je jedním z důvodů, proč je C někdy nazýváno „přenosným assemblerem“. Je důležité tuto předvídatelnost znát při generování dat, která musí dodržovat specifický formát (např. záhlaví síťového paketu).
Ukazatele v jazyce C
V jazyce C můžete deklarovat ukazatele na jakýkoli typ a přiřadit jim hodnoty, které odkazují na objekty tohoto typu.
Ukazatel na celé číslo:
int someInteger;
int *intPtr = &someInteger; // deklarace ukazatele a přiřazení adresy
someCall(intPtr); // předání ukazatele jako parametru
someCall(&someInteger); // ekvivalentní výše uvedenému
Funkce v knihovně C, která přijímá ukazatel na hodnotu, obvykle tuto hodnotu modifikuje (stává se „výstupním“ nebo „vstupně-výstupním“ parametrem). Ve výše uvedeném příkladu pravděpodobně someCall
modifikuje hodnotu someInteger
.
Ukazatel na pole celých čísel:
#define ARRAY_LENGTH 100
int intArray[ARRAY_LENGTH];
int *intArrayPtr;
...
int sum = 0;
for (intArrayPtr = intArray; intArrayPtr < intArray + ARRAY_LENGTH; intArrayPtr++) {
sum += *intArrayPtr;
}
Ukazatel na pole struktur:
#define ARRAY_LENGTH 100
typedef struct {int foo, bar;} pair_t; // nový typ pair_t
pair_t structArray[ARRAY_LENGTH]; // pole typu pair_t
pair_t *structArrayPtr;
...
int sum = 0;
for (structArrayPtr = structArray; structArrayPtr < structArray + ARRAY_LENGTH; structArrayPtr++) {
sum += structArrayPtr->foo + structArrayPtr->bar;
}
Když přičtete celé číslo k ukazateli, posune se ukazatel o tolik prvků daného typu, kolik udává číslo, bez ohledu na velikost těchto prvků. Překladač zná velikost prvků a provede tento výpočet správně.
Výstup
Výstup můžete formátovat pomocí printf
nebo jeho varianty fprintf
.
Formátovací řetězec používá:
%d
pro celé číslo,%s
pro řetězec,%f
pro číslo s plovoucí řádovou čárkou.
Speciální znaky:
\t
(tabulátor),\n
(nový řádek).
Příklad:
printf("Myslím, že číslo %d je %s\n", 13, "šťastné");
Smíchání funkcí printf()
, fprintf()
a cout
může způsobit nečekané pořadí výstupu, protože používají nezávislé buffery, které se vyprazdňují až po jejich zaplnění.
Funkce main
Hlavní funkce může přijímat parametry reprezentující vstupy z příkazové řádky.
Příklad zápisu:
int main(int argc, char *argv[]);
argc
je počet parametrů,argv
je pole řetězců, tj. pole ukazatelů na pole znaků ukončených nulovým znakem.
První prvek argv
je podle konvence jméno programu:
int main(int argc, char *argv[]) {
printf("Mám %d parametrů; moje jméno je %s a můj první parametr je %s\n",
argc, argv[0], argv[1]);
}
Užitečné vlastnosti jazyka
- Inkrementace: Používejte operátor
++
k navýšení hodnoty proměnné nebo k posunu ukazatele na další objekt. Doporučuje se používat postinkrementaci (myInt++
). - Zkrácené zápisy:
myInt -= 3; // ekvivalentní myInt = myInt - 3
myInt *= 42; // ekvivalentní myInt = myInt * 42
myInt += 1; // někdy preferováno místo myInt++
- Reprezentace čísel:
- Desítkově:
123
, - Osmičkově:
0453
(s prefixem0
), - Šestnáctkově:
0xffaa
(s prefixem0x
). - Bitové operace:
myInt = myInt | 0444; // bitový OR
myInt &= 0444; // bitový AND
myInt = a ^ b; // bitový XOR
Podmíněné výrazy
Místo zápisu:
if (a < 7)
a = someValue;
else
a = someOtherValue;
Můžete napsat:
a = a < 7 ? someValue : someOtherValue;
Struktura programu
Programy by měly být rozděleny do více zdrojových souborů:
- Typy, funkce, globální proměnné a konstanty sdílené více soubory by měly být deklarovány v záhlavním souboru (končícím
.h
). - Zdrojové soubory odkazují na záhlavní soubory pomocí
#include
.
Kompilace:
K propojení všech objektových souborů použijte:
gcc *.o -o myProgram
Pokud potřebujete knihovny, specifikujte je po objektových souborech:
gcc *.o -lxml2 -o myProgram
Ladění programů v C
- Segmentation fault: Nejčastější příčiny:
- Index mimo rozsah,
- Neinicializovaný ukazatel,
- Hodnota
NULL
. - Přidávání výpisů: Použijte
printf()
, abyste našli zdroj chyby. - Použití
gdb
: Umožňuje sledovat chyby a paměťové úniky.
Při dlouhém běhu programy musí uvolňovat veškerou alokovanou paměť, aby nedošlo k jejímu vyčerpání.
Unix
Standardní soubory, příkazy, systémová volání, oprávnění k souborům
Podle konvence každý proces začíná se třemi standardními soubory: standardní vstup, standardní výstup a standardní chybový výstup, spojené s deskriptory souborů 0, 1 a 2.
- Standardní vstup (stdin): Obvykle připojený k vaší klávesnici. Cokoli napíšete, přejde do programu.
- Standardní výstup (stdout): Obvykle připojený k obrazovce. Cokoli program vypíše, se zobrazí.
- Standardní chyba (stderr): Také obvykle připojená k obrazovce.
Pomocí shellu můžete volat programy tak, aby byl standardní výstup jednoho programu přímo připojen („piped“) ke standardnímu vstupu jiného programu:
ls | wc
Standardní vstup/výstup můžete také přesměrovat do souboru:
ls > lsOutFile
wc < lsOutFile
sort -u < largeFile > sortedFile
Programy obecně nevědí a nezajímá je, zda shell změnil význam jejich standardních souborů.
Příkazy Unixu
Příkazy jsou pouze názvy spustitelných souborů. Proměnná prostředí PATH
určuje shellu, kde tyto soubory hledat. Tato proměnná má obvykle hodnotu jako /bin:/usr/bin:/usr/local/bin:...
.
Pro zjištění, kde shell najde konkrétní program, například vim
, použijte:
where vim
Systémová volání a knihovny
- Návratová hodnota volání obvykle označuje, zda volání bylo úspěšné (hodnota je obvykle 0 nebo kladná) nebo neúspěšné (hodnota je obvykle -1).
- Vždy kontrolujte návratovou hodnotu knihovních volání. Pokud systémové volání selže, funkce
perror()
může vytisknout chybu na standardní chybový výstup:
int fd;
char *filename = "myfile";
if ((fd = open(filename, O_RDONLY)) < 0) {
perror(filename); // může vypsat "myfile: No such file or directory"
}
Stránka manuálu pro systémové volání nebo knihovní funkci může obsahovat datový typ, který nedefinuje (např. size_t
, time_t
nebo O_RDONLY
). Tyto typy jsou obvykle definovány v hlavičkových souborech uvedených v manuálové stránce; všechny tyto hlavičkové soubory musíte zahrnout do svého programu.
Oprávnění k souborům
V Unixu se oprávnění obvykle vyjadřují osmičkovými čísly.
Například v případě volání creat()
je hodnota 0660
osmičkové číslo, které představuje binární číslo 110110000
. Toto oprávnění uděluje práva ke čtení a zápisu vlastníkovi souboru a jeho skupině, ale žádná práva ostatním uživatelům.
Při vytváření souboru nastavíte oprávnění parametrem ve volání creat()
.
Příkaz ls -l
zobrazí oprávnění souborů.
Oprávnění můžete změnit pomocí programu chmod
.
Všechny vaše procesy mají atribut nazvaný umask, obvykle reprezentovaný osmičkovým číslem. Když proces vytvoří soubor, bity v umask se odstraní z oprávnění zadaných ve volání creat()
. Například pokud je váš umask
nastaven na 066
, ostatní uživatelé nebudou moci číst ani zapisovat soubory, které vytvoříte. Umask můžete zkontrolovat a změnit pomocí programu umask
, který obvykle spouštíte v úvodním skriptu shellu (například ~/.login
nebo ~/.profile
).
Nástroje pro vývoj softwaru
Textové editory
Pro vytváření, úpravu a kontrolu svého programu používejte textový editor. Existuje několik rozumných možností:
- Vim: Nabízí pokročilé nástroje jako zvýrazňování syntaxe, párování závorek, doplňování slov, automatické odsazování, vyhledávání podle značek a integrované vyhledávání v manuálových stránkách. Je navržen pro ovládání klávesnicí a nevyžaduje použití myši.
- Emacs: Obsahuje ještě více funkcí než Vim, ale jeho naučení vyžaduje značné úsilí.
- Základní editory:
pico
,gedit
,joe
(pro Unix) anotepad
,word
(pro Microsoft).
Integrovaná vývojová prostředí (IDE), jako jsou Eclipse, Code Warrior nebo .NET, často obsahují textové editory, ladicí nástroje a kompilátory.
Ladicí nástroj gdb
- Povolte efektivní ladění přidáním příznaku
-g
při kompilaci. - Chcete-li ladit program, který selhal a zanechal soubor
core
, spusťte:
gdb myProgram core
- Pro spuštění programu od začátku pod kontrolou
gdb
zadejte:
gdb myProgram
- Užitečné příkazy:
where
: Zobrazí zásobník volání s čísly řádků.break
: Nastaví bod přerušení v konkrétním souboru a řádku.next
: Provede další příkaz.step
: Vstoupí do funkce.
Kompilace pomocí make
Použijte program make
pro organizaci procesu rekompilace a relinkování programů při změně zdrojového souboru.
Příklad Makefile:
SOURCES = driver.c input.c output.c
OBJECTS = driver.o input.o output.o
HEADERS = common.h
CFLAGS = -g -Wall
program: $(OBJECTS)
$(CC) $(CFLAGS) $(OBJECTS) -o program
$(OBJECTS): $(HEADERS)
testRun: program
program < testData
Pro automatické vytváření pravidel, jak soubory závisí na hlavičkových souborech, použijte program makedepend
.
Vyhledávání definic
Pomocí programu grep
můžete rychle hledat definice nebo proměnné, zejména v hlavičkových souborech:
grep "struct timeval {" /usr/include/*/*.h
Cvičení
Provádějte tato cvičení v jazyce C.
- Napište program nazvaný
atoi
Vytvořte program, který otevře datový soubor specifikovaný v příkazové řádce a přečte z něj jeden řádek obsahující celé číslo v ASCII formátu. Program by měl převést řetězec na celé číslo, vynásobit jej třemi a výsledek vytisknout na standardní výstup. Nesmíte použít funkciatoi()
. Použijte programmake
k řízení procesu sestavení. Váš Makefile by měl obsahovat tři pravidla:atoi
(kompiluje program),run
(spustí program s testovacími daty a přesměruje výstup do souboru) aclean
(odstraní dočasné soubory). Zajistěte, aby program správně zpracovával chyby, například neexistující nebo nepřístupné soubory, a skončil s jasnou chybovou zprávou. Program otestujte pomocígdb
, nastavte bod přerušení namain()
a krokově sledujte jeho provedení pomocí příkazustep
. - Implementujte vlastní verzi programu
cat
Napište zjednodušenou verzi programucat
. Váš program by měl přijímat více názvů souborů (nebo žádné) jako argumenty a vypisovat obsah zadaných souborů na standardní výstup, jeden po druhém. Pokud nejsou zadány žádné názvy souborů, měl by program číst ze standardního vstupu. Není potřeba podporovat další možnosti nebo přepínače. - Napište program
removeSuffix
Vytvořte program, který přijme jeden parametr: název souboru se sufixy. Každý řádek v souboru by měl obsahovat nesprázdný řetězec (sufix
), následující znak>
a poté řetězec náhrady (který může být prázdný). Program by měl uložit tyto páry sufix-náhrada do hashovací tabulky s použitím externího řetězení. Následně by měl program číst slova oddělená mezerami ze standardního vstupu, najít pro každé slovo nejdelší odpovídající sufix a modifikovat slovo nahrazením sufixu příslušnou náhradou. Výstupem by měl být jeden řádek na každé modifikované slovo ve formátuslovo>modifikované_slovo
. Slova, která nebyla modifikována, nebudou vypsána.