====== 11 - Struktury, union, výčtové typy, volání C z Javy ======
/* * prezentace: [[.:prezentace]]
* řešení: [[.:reseni:start]]*/
* pro vyučující: [[private:tutorialinstruction:12:start|]]
===== Trocha teorie =====
==== Struktury a vytváření typů =====
Struktura je heterogenní datový typ, který slouží zejména ke spojení vzájemně souvisejících údajů (datových položek). Síla tohoto datového typu se obvykle projevuje v přehlednosti zdrojového kódu, ale lze ji i jednoduše využít pro návratové proměnné z funkcí.
=== Syntaxe struktury a příklad použití ===
struct jménoStruktury { struct student {
typ jménoPoložky; char jmeno[20];
typ jménoPoložky; char prijmeni[20];
... float prumer;
} proměnné; } Marketa, Pavel;
=== Vytvoření nového datového typu ===
Zavedení nového uživatelského datového typu je možné příkazem ''typedef''. Syntaxe příkazu je:
typedef definice_typu identifikator_typu;
Nový datový typ můžeme vytvořit i pro stuktury (viz níže), což vede k pohodlnějšímu používání struktur.
=== Přístup k položkám struktury ===
K položkám struktury se přistupuje operátorem ''.'' (tečka). V případě ukazatele na strukturu pak operátorem ''->'' (šipka) - nahrazuje dereferenci s tečkovým operátorem (viz příklad).\\
Všimněte si, že funkce //vytvorStudenta// vrací ukazatel na strukturu a funkce //vypisStudenta// přijímá ukazatel na strukturu, tj. není vytvářena lokální kopie pro dané funkce a program je efektivnější.
typedef struct {
char name[20]; /* jmeno studenta */
float gpa; /* studijni prumer - grade point average */
} Student;
Student* create_student() {
Student *student = (Student*) malloc(sizeof(*student)); /* v tomto jednoduchem pripade predpokladem uspesnou alokaci pameti */
printf("Zadejte jmeno: ");
scanf("%s", student->name); /* jmeno je typu pole znaku, ktere je reprezentovane adresou prvniho znaku */
printf("Zadejte studijni prumer: ");
scanf("%f", &student->gpa); /* scanf prijima v parametru adresu, proto referencni operator & */
return student;
}
void print_student(Student *student) {
printf("Vypis: %s, %.2f \n", student->name, student->gpa); /* stejne jako (*student).name a (*student).gpa */
}
int main(int argc, char* argv[]) {
/* inicializace struktury pri deklaraci */
Student student = {"Chuck Norris", 2};
student.gpa = 0.5; /* preci jenom se jedna o Chucka, proto upravuje jeho studijni prumer */
print_student (&student);
print_student(create_student());
return 1;
}
=== Typ union ===
Syntaxe typu union je stejná jako u struktury, místo struct používáme klíčkové slovo union. Hlavní rozdíl je v tom, že struktura alokuje paměť pro všechny položky, typ union zabírá v paměti pouze tolik místa, kolik je potřeba pro uložení největší položky. Z toho vyplývá, že lze použít v jednom okamžiku pouze jednu datovou položku unionu.
typedef union {
int temperature; /* teplota ohrivace */
char cooling; /* stav chlazeni */
} Heater;
typedef struct {
float temperature;
Heater state; /* stav je union ve struktuře */
} Wheater; /* pocasi */
Teplota ohřívače je počítána jen když je teplota nízká, naopak stav chlazení je uváděn jen když je teplota vysoká; pokud je uvedena teplota, položka stav bude mít jen jednu hodnotu.
=== Výčtový typ enum ===
Výčtový typ //enum//, jehož podstata je stejná jako v jazyce Java, je v jazyce C chápán jako náhrada za konstanty vytvářené pomocí makra preprocesoru //#define//. Výčtovým typem je definována množina všech přípustných hodnot proměnné; vnitřně jsou pak tyto konstanty reprezentovány celými čísly zpravidla typu ''int'', případně nejmenšího znaménkového typu, který je schopen rozsah množiny obsáhnout.
typedef enum {
RED, GREEN, BLUE
} Color;
==== Spojové struktury =====
Podobně jako v jsme si ukazovali v Javě, tak i v C můžeme vytvořit spojový seznam. Místo referenční proměnné využijeme však v C ukazatel na další prvek jako proměnnou typu ukazatel na strukturu.
typedef struct _student { /* musime deklarovat typ, abychom se mohli na nej odkaze v deklaraci polozky next */
char name[30];
float gpa;
struct _student *next; /* Ukazatel na sebe sama, Student jeste neni znam proto pouzivame _student */
} Student; /* Student je jmeno zavedeneho typu struktury */
Student *create_student(Student *next) {
Student *tmp = (Student*) malloc(sizeof (Student));
printf("Zadejte jmeno: \n");
scanf("%s", tmp->name);
printf("Zadejte studijni prumer: \n");
scanf("%f", &tmp->gpa);
tmp->next = next;
return tmp;
}
void print_student(Student *student) {
printf("%s, %.2f \n", student->name, student->gpa);
}
int main(int argc, char* argv[]) {
Student *start = NULL, *cur;
int i;
for (i = 0; i < 3; i++) /* vytvoreni tri studentu */
start = create_student(start);
cur = start;
while (cur) { /* vypis vytvorenych studentu */
print_student(cur);
cur = cur->next;
}
return (EXIT_SUCCESS);
}
===== Úlohy k procvičení =====
==== Struktury ====
- Vytvořte strukturu popisující entitu osoba s položkami jméno (name), příjmení (surname), typ osoby (type), datum narození (birthdate) a studijní průměr (gpa).
- Vytvořte výčtový typ enum definující typ osoby - student, učitel (teacher), apod.
- Napište funkci, která vytvoří jednu osobu na základě dat načtených od uživatele.
- Vytvořte program, který bude umět:
- seřadit studenty podle abecedy,
- seřadit studenty podle studijního průměru,
- rozdělit studenty na prospěchově podprůměrné a nadprůměrné,
- vypsat studenty.
Použijte vhodné dělení do funkcí. Pro řazení si můžete naprogramovat svoji funkci, nebo lépe použijte funkci **qsort** z knihovny stdlib.
void qsort( void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));
Funkce ''qsort'' řadí algoritmem quicksort souvislé pole libovolného typu, na které ukazuje //base// o //nmemb// položkách velikosti //size//. K třídění je použita funkce, na kterou ukazuje //compar//. Funkci //compar()// budou při volání předávány jako argumenty dva prvky z pole //base//. Funkce //compar()// musí vracet zápornou hodnotu, pokud je první argument menší než druhý, nulovou hodnotu pokud se rovnají a kladnou hodnotu pokud je první argument větší než druhý.
Pro porovnání textových řetězců si napište vlastní funkci nebo využijete funkci ''strcmp'' z knihovny ''string.h''.
int strcmp(const char *s1, const char *s2);
Pro jednoduchost předpokládejte, že všechny řetězce vždy končí znakem '\0' a jsou správně alokovány.
=== Pro pokročilé ===
- Rozšiřte program o možnost načíst, v programu uchovat a uložit seznam struktur ze souboru (použijte textové i binární soubory).
== Příklad uložení/načtení osoby/pole osob typu struktura do/z souboru ==
#define FNAME "osoby.txt"
int write_person(Person *person, int number) {
FILE *f;
/* otevreni binarni souboru pro zapis */
if ((f = fopen(FNAME, "wb")) == NULL) {
printf("Chyba zapisu do souboru %s.\n", FNAME);
return 1;
}
if (number != fwrite(person, sizeof(Person), number, f)) {
fclose(soubor);
return 1;
}
fclose(f);
return 0;
}
int read_person(Person *person) {
FILE *f;
Person tmp;
int index = 0;
/* otevreni binarniho souboru pro cteni */
if ((f = fopen(FNAME, "rb")) == NULL) {
printf("Chyba cteni souboru %s.\n", FNAME);
return 1;
}
while (fread(&tmp, sizeof(Person), 1, f) == 1) {
person[index++] = tmp; /* vyuzivame operatoru prirazeni nad strukturou */
}
fclose(f);
return 0;
}
Uvedený příklad neřeší alokaci struktury a bude správně fungovat pouze pro jedinou osobu nebo dle alokované paměti odkazované předávanou proměnnou ''person''.
==== Spojové struktury ====
=== Spojový seznam ===
== Jednoduché úlohy ==
Vytvořte spojový seznam uchovávající seznam obdélníků s vlastnostmi šířka, výška a textovým popisem obsahujícím barvu. Seznam si bude uchovávat ukazatel na začátek i ukazatel na konec.
Napište funkce pro
- vytvoření prázdného seznamu,
- zjištění, zda je daný seznam prázdný,
- vložení jednoho obdélníka na začátek,
- vložení jednoho obdélníka na konec,
- výpis seznamu,
- zjištění počtu prvků seznamu,
- zjištění, zda a popř. kolikrát se daný obdélník v seznamu vyskytuje,
- odebrání jednoho prvku a to:
- ze začátku,
- od konce,
- odebrání prvku, který bude vstupním parametrem funkce.
== Mírně těžší úlohy ==
Implementujte předchozí úlohy s použitím seřazeného zřetězeného seznamu, od nejmenšího (plošně) po největší obdélník. Tedy například funkce pro vkládání prvku x bude vkládat na příslušnou pozici.
== Středně pracné úlohy ==
Pro seznamy z předchozích kapitol implementujte funkci, která spojí dva seznamy, resp. spojí dva seřazené seznamy v jeden nový, seřazený.
=== Stromy ===
Binární vyhledávací strom (BVT) je datová struktura založená na binárním stromu, v němž jsou jednotlivé prvky uspořádány tak, aby v tomto stromu bylo možné rychle vyhledávat danou hodnotu. To zajišťují tyto vlastnosti:
* Jedná se o binární strom, každý uzel tedy má nanejvýš dva syny − levého a pravého.
* Každému uzlu je přiřazen určitý klíč, my budeme používat celá čísla. Podle hodnot těchto klíčů jsou uzly uspořádány.
* Levý podstrom uzlu obsahuje pouze klíče menší než je klíč tohoto uzlu.
* Pravý podstrom uzlu obsahuje pouze klíče větší než je klíč tohoto uzlu.
Příklad BVT ukazuje následující obrázek:
{{:courses:a0b36pr2:labs:bvt.png|}}
Napište a vyzkoušejte funkce pro práci s BVT:
- vytvoření prázdného BVT,
- výpis BVT vzestupně a sestupně (použijte rekurzi),
- vložení jednoho prvku,
- zjištění existence prvku s daným klíčem,
- odebrání jednoho prvku,
- sloučení dvou stromů,
- nalezení průniku a sjednocení dvou stromů,
- vyvážení stromu (BVT strom je vyvážený pokud nejvyšší list není výše než nejhlubší list + 1).
===== Volání C z Javy (Java Native Interface) =====
Součástí Java platformy je rozhraní JNI (Java Native Interface), které umožňuje propojit kód běžící na virtuálním stroji Javy s nativními programy a knihovnami napsanými v jiných programovacích jazycích (např. C). Hlavními výhodami jsou zvýšení výkonu v některých low-level úlohách (např. Math.sqrt(double a)) a umožnění komunikace s hardwarovými zařízeními (Bluetooth, apod.). Nevýhodou je především ztráta platformní nezávislosti, protože takový program využívá knihovny zkompilované pro daný operační systém.
{{:courses:a0b36pr2:labs:jni.png?600|}}
Implementace nativní knihovny a propojení s Javou je znázorněno na obrázku výše. Příklad ilustruje vytvoření C knihovny, která vypisuje text "Hello World!" a její volání z Java aplikace. Celý proces implementace je možné shrnout do několika kroků:
- Vytvoření třídy s deklaracemi nativních metod:
class HelloWorld {
static { /* statický inicializátor */
System.loadLibrary("HelloWorld"); /* načtení dynamické knihovny vytvořené v jazyku C */
}
private native void print(); /* deklarace nativní metody */
public static void main(String[] args) {
new HelloWorld().print();
}
}
* metoda **loadLibrary(String libname)** slouží k načtení externí systémové knihovny, parametr //libname// zastupuje cestu ke knihovně,
* příkazy uvnitř **statického inicializátoru** (static { }) se provádí po zavedení třídy do paměti, ještě před voláním konstruktoru => nejprve se načte externí knihovna a až poté se zpracovává kód třídy,
* **native** je klíčové slovo pro označení nativní metody, tj. metody vytvořené v jazyku C, kterou budeme volat.
- Kompilace Java programu:
* zkompilujte vytvořenou třídu: buď v NetBeans kliknout pravým tlačítkem na název projektu a vybrat "Clean and Build" nebo externím nástrojem javac,
* v podadresáři "build\classes" projektového adresáře se vytvoří bytecode (.class).
- Generování hlavičkového souboru:
* aplikujte nástroj javah (součástí JDK, typicky v C:\Program Files\Java\jdk\bin) na bytecode Java třídy ke generování C hlavičkového souboru (.h) s deklaracemi nativních funkcí. Název bytecode souboru zadejte bez přípony .class: javah -jni HelloWorld
* ve vytvořeném hlavičkovém souboru je nejdůležitější vygenerovaná deklarace C funkce, která je propojena s Java nativní metodou:
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);
- Implementace deklarovaných funkcí v C:
* V NetBeans zvolte //New Project// -> //C/C++ Dynamic Library//.
* Vytvořený projekt je třeba propojit s JNI knihovnami:
* v NetBeans klikněte pravým tlačítkem na název projektu a zvolte //Properties//,
* na záložce //C Compiler// vyplňte do //Include Directories// cesty k JNI (typicky C:/Program Files/Java/jdk/include a C:/Program Files/Java/jdk/include/win32),
* na stejné záložce ještě vyplňte //Additional Options// hodnou: -Wl,--add-stdcall-alias -mno-cygwin -shared -m32 -Wall -pedantic
* **-Wl,--add-stdcall-alias** nastavuje pro linker přepínač --add-stdcall-alias, který zaručí vytvoření knihovny pro Windows,
* **-mno-cygwin** zaručí vytvoření knihovny nezávislé na Cygwin,
* **-shared** indikuje vytvoření dynamické knihovny namísto spustitelného souboru (pokud nepoužíváte NetBeans nebo jste nevytvořili //C/C++ Dynamic Library//),
* **-m32** indikuje vytvoření 32b knihovny (má význam jen pokud používáte 64b GCC a 32b JDK),
* **-Wall -pedantic** zaručí výpis všech varovných hlášení a důslednou kontrolu zdrojového kódu podle striktní normy.
* Do vytvořeného projektového adresáře nakopírujte vygenerovaný hlavičkový soubor a propojte ho s projektem (klikněte pravým tlačítkem myši na //Source Files// a vyberte //Add Existing Item//).
* Klikněte pravým tlačítkem myši na //Source Files//, vyberte //New// a poté //C Source File//.
* Do nově vytvořeného souboru přidejte //#include// na JNI, na vytvořený hlavičkový soubor a implementujte deklarované funkce:
#include
#include
#include "HelloWorld.h"
JNIEXPORT void JNICALL
Java_HelloWorld_print(JNIEnv *env, jobject obj) {
printf("Hello World!\n");
return;
}
- Kompilace C programu a generování nativní knihovny:
* v NetBeans klikněte pravým tlačítkem myši na název projektu a vyberte //Clean and Build//,
* v podadresáři //dist// projektového adresáře se vytvoří nativní dynamická knihovna (ve Windows .dll, v Unix .so).
- Spuštění programu java interpretrem
* překopírujte vytvořenou knihovnu (.dll nebo .so) do Java projektového adresáře nebo do adresáře kde je umístěn spustitelný soubor Java aplikace,
* bude-li knihovna v jiném adresáři, musíte definovat cestu v metodě loadLibrary(),
* spusťte Java aplikaci a na obrazovce shlédněte výpis textu „Hello World!“ definovaný v C.
Více informací o JNI můžete nalézt např. v knize [[http://java.sun.com/docs/books/jni/|Java Native Interface: Programmer's Guide and Specification]].