TI/Włączanie kodu w innych językach/Języki kompilowalne

Z Brain-wiki

Język interpretowany vs język kompilowany

Do tej pory pisaliście w pythonie. Jest to język interpretowany. To znaczy wyglądało to tak, że tworzyliście plik tekstowy, zgodnie z określonymi regułami, w którym zapisywaliście kod źródłowy programu. Gdy chcieliście wykonać program, wywoływaliście interpreter pythona podając mu wasz kod źródłowy w postaci tekstowej:

python moj_program.py

W językach kompilowanych (np. C, C++) po utworzeniu kodu źródłowego uruchamia się program zwany kompilatorem, który w przeciwieństwie do interpretera, nie wykonuje od razu programu, jedynie tworzy plik z kodem maszynowym. Np.:

gcc program.c -o program

utworzy z naszego kodu źródłowego program.c kod maszynowy program. Dopiero ten plik jest wykonywalnym programem, zależnym od architektury komputera na którym został utworzony — jest to kod maszynowy zrozumiały dla danej maszyny. Kompilację wystarczy wykonać raz, i nie potrzeba już źródeł do uruchamiania programu wykonywalnego. Jednakże po każdej zmianie kodu źródłowego program należy skompilować jeszcze raz. W przeciwnym razie zmiany te nie zostaną uwzględnione w działaniu programu.

Program podstawowy

W podstawowym programie musimy mieć funkcję główną main (oraz biblioteki), która zostaje automatycznie wywołana przy uruchomieniu programu. Składnia funkcji main:

int main(int argc, char *argv[], char *envp[])
{

.

   return 0;
}

My będziemy korzystać z takiej:

int main(int argc, char *argv[])

{

.

    return 0;

}

Funkcja main jest funkcją zwracającą wartość typu całkowitego (int przed main). Przy wywołaniu programu przekazywane są jej następujące argumenty: argc (typu całkowitego) — całkowita liczba przekazanych parametrów oraz tablica typu znakowego (char *argv[]), w której zapisane są agrumenty jako napisy (stringi).

Tablice w C++ oraz C, numerowane są od 0 do n-1. Pierwszy argument przekazywany funkcji main, czyli argv[0], to nazwa samego programu. argv[argc-1] jest ostatnim slowem, ktore wpiszesz, wywolujac program.

Program, jeżeli wykona się poprawnie powinien zwrócić wartość 0 (zaszłości historyczne).

Pierwszy program w C++

Standardowo pierwszym naszym programem będzie wypisanie "hello world!". Jak to zrobimy w C++?

#include <iostream>
using namespace std;
int main()
{
    cout<<"Hello world"<<endl;
    return 0;
}

Linijka po linijce:

  1. Włączamy nagłówek biblioteki standardowej, której m.in. znajdują sie polecenia wypisania na ekran.
  2. Piszemy, że będziemy korzystać z globalnych klas, funkcji i obiektów, zdefiniowanych w bibliotekach (włączonych).
  3. Rozpoczynamy funkcję główną — nasz program. Każdy program, żeby działać musi mieć funkcję główną. Można tez definiować inne funkcje — będziemy to robić na kolejnych zajęciach. W najprostszym wypadku nagłówek funkcji main wygląda tak: int main().
  4. Otwieramy ciało funkcji głównej {.
  5. Wypisujemy na standardowe wyjście napis Hello world i przechodzimy do następnej linijki poleceniem endl. Moglibyśmy też napisać cout << "Hello world\n"; "\n" jest tzw. znakiem specjalnym.
  6. Funkcja główna jest funkcją zwracająca liczbę całkowitą. Standardowo (jeżeli program się wykonał poprawnie) zwracamy 0.
  7. Zamykamy ciało funkcji głownej }.

Pierwszy program w C

Standardowo pierwszym naszym programem będzie wypisanie "hello world!". Jak to zrobimy w C++?

#include <stdio.h>
int main()
{
    print("Hello world!\n")
    return 0;
}

Linijka po linijce:

  1. Włączamy nagłówek biblioteki standardowej, której m.in. znajdują sie polecenia wypisania na ekran.
  2. Rozpoczynamy funkcję główną — nasz program. Każdy program, żeby działać musi mieć funkcję główną. Można tez definiować inne funkcje — będziemy to robić na kolejnych zajęciach. W najprostszym wypadku nagłówek funkcji main wygląda tak: int main().
  3. Otwieramy ciało funkcji głównej {.
  4. Wypisujemy na standardowe wyjście napis Hello world i przechodzimy do następnej linijki poleceniem \n.
  5. Funkcja główna jest funkcją zwracająca liczbę całkowitą. Standardowo (jeżeli program się wykonał poprawnie) zwracamy 0.
  6. Zamykamy ciało funkcji głownej }.

Komentarze w tekście programu

Zarówno w C jak i C++ komentarze jednolinijkowe zaczynają się od podwójnego ukośnika odwrotnego //. Komentarze można też zawrzeć w /* i */.

Instrukcja include

Pliki nagłówkowe, zarówno systemowe, jak i autorstwa użytkownika, włącza się za pomocą dyrektywy preprocesora #include:

  • #include <plik> używana jest w przypadku systemowych plików nagłówkowych. W tym przypadku plik plik poszukiwany jest w standardowych katalogach — zwykle w /usr/include.
  • #include "plik" używana jest w przypadku plików nagłówkowych autorstwa użytkownika. Poszukiwane są one na początku w katalogu zawierającym kompilowany program, następnie w katalogach zawartych w cudzysłowach, a następnie w standardowych katalogach, wktórych znajdują się systemowe pliki nagłówkowe.

Nazwa pliku czy w nawiasach trójkątnych, czy w cudzysłowach traktowana jest jako napis. Preprocesor nie rozpoznaje ani znaków wskazujących na rozpoczęcie komentarza (/*) ani ukośników odwrotnych, które są traktowane jako zwykłe znaki, a nie znaki specjalne.

Szybki wstęp do C dla znających pythona -- różnice i podobieństwa

Uruchamianie programu

Python jest językiem interpretowanym, a C kompilowanym. Dlatego też program pythonowy wykonujemy podając jako argument komendzie interpretera plik z kodem źródłowym, który jest następnie interpretowany przez interpreter i wykonywany. Programy pythonowe (a jednocześnie piki z kodem pythonowym) mają rozszerzenie .py.

python moj_program.py

W C plik z kodem źródłowym ma rozszerzenie .c. Aby wykonać program, musimy pier przerobić plik z kodem źródłowym na program -- ciąg bajtów oznaczających instrukcje zrozumiałe już bezpośrednio dla procesora (procesor nie jest w stanie czytać tekstu kodu źródłowego, umie wykonywać proste instrukcje na rejestrach).

gcc moj_program.c -o moj_program

A dopiero potem wykonać program. (Po fladze -o podajemy nazwę jaka ma zostać nadana programowi wykonywalnemu. Domyślnie kompilator utworzy program wykonywalny o nazwie a.out)

./moj_program


Struktura programu

W programie pythonowym na początku mieliśmy często instrukcje

 import modul

W pewnym sensie odpowiednikiem tego w C jest instrukcja:

#include <modul>

W pythonie do funkcji w modułu odwoływaliśmy się:

modul.funkcja_z_modulu()

W C możemy się bezpośrednio do nazwy funkcji odwoałać:

funkcja_z_modułu();


O ile w pythonie większość rzeczy to była konwencja, czyli importy mogliśmy równie dobrze w środku pliku z kodem wpisać, o tyle w C instrukcje include faktycznie muszą być na początku, inazej kompilator zgłosi błąd.


W programie pythonowym mogliśmy pisać dowolne instrukcje w dowolnym miejscu w pliku -- nie było wymogu definiowania żadnych funkcji itd. W programie w C konieczna jest jedna podstawowa funkcja main (opisana trochę wyżej). Tym co już wykona skompilowany program będize właśnie funkcja main. Na zewnątrz tej funkcji możemy sobie podefiniować inne funkcje, i wykonać jeszcze kilka rzeczy ze ściśle określonej puli, jak deklarowanie zmiennych/typów, ale nie możemy już z taką dowolnością jak w pythonie wpisać pętli for w dowolnym miejscu w pliku.

Wcięcia i inne podstawowe drobiazgi składniowe

W pythonie do oznaczenia bloku programu znajdującego się na tym samym poziomie używamy wcięć. W C możemy wcięcia robić dowolnie, natomiast zakres bloku oznaczamy klamerkami: {}


def jakas_funkcja(): # jest dwukropek, komentarz zaznaczamy poprzez #
    print "hello world"


#include <stdio.h>
int jakas_funkcja() // nie ma dwukropka, "//" to znak komentarza
{ // blok jest oznaczony nawiasami klamrowi, poniższe wcięcia są tylko dla ładności kodu
    printf("Hello world!\n"); // po każdej linijce konieczny jest średnik
    return 0;
}

Typy

W pythonie nie ma czegoś takiego jak deklaracja zmiennych. Kiedy potrzebujemy zachować sobie w pamięci wynik jakichś operacji, piszemy po prostu:

x = moja_funkcja()

nie uprzedzając nikogo wcześniej, że będziemy używać takiej zmiennej jak x.

W C już na początku pisania programu trzeba się zdecydować ile zmiennych będziemy używać i jakiego będą typy.

int x; //deklaracja zmiennej
x = moja_funkcja();

Podstawowe typy w C:

int integer, liczba całkowita zapisana na 32 bitach
short liczba całkowita zapisana na 16 bitach
long liczba całkowita zapisana na 64 bitach
float 32 bity
double 64 bity (python, którego my używamy jest zaimplementowany w C,
w pythonie do implementacji typu float jest użyty typ double z C)
char character, 1 bajt

Podstawowe operacje w C

Takie same jak w pythonie:

+ dodawanie
- odejmowanie
* mnożenie
/ dzielenie
= operator przypisania wartości
< mniejszy
<= mniejszy równy
== równy
!= różny
>= większy równy
> większy

Różnice (operatory logiczne):

C Python
&& and
¦¦ or
! not

Podstawowe konstrukcje: if, while, for

W C nie ma typy True i False -- podajemy 0 lub 1, wyrażenia logiczne ewaluują się do 0 lub 1.

 if (warunek)
 { 
 	printf("True \n");
 }
 else 
 	printf("False\n");
    int i = initial_i;

    while (i <= i_max)
    {
     	// jakieś instrukcje

	i = i + i_increment;
    }


    for (i = initial_i; i <= i_max; i = i + i_increment)
    {
	//jakieś instrukcje
    }

Funkcje

typ_zwracany nazwa_funkcji(lista_argumentow)
{
    //deklaracje zmiennych lokalnych

    //instrukcje

    return cos_typu_zwracanego;
}

na przykład:

void fun()
{
   printf("fun");
}


int mnozenie(int a, int b)
{
    return a * b;
}


Wskaźniki

Oto przykład obrazujący czym są wskaźniki:

	int zmienna_x;
        int* wskaznik_na_x;

        zmienna_x = 42;
	wskaznik_na_x = &zmienna_x;
        printf("Wartosc wskaznika: %d, wartosc pod wskaznikiem: %d \n", wskaznik_na_x, *wskaznik_na_x);
	*wskaznik_na_x = 0;
	printf("wartosc x : %d \n", zmienna_x);


Przykładowy program

Program pokazujący jak uważnie trzeba korzystać ze wskaźników:

#include <stdio.h>


int test_wskaznikow(int a)

{

	int zmienna_x;
        int* wskaznik_na_x;

        zmienna_x = a;
	wskaznik_na_x = &zmienna_x;
        printf("Wartosc wskaznika: %d, wartosc pod wskaznikiem: %d \n", wskaznik_na_x, *wskaznik_na_x);
	*wskaznik_na_x = 0;
	printf("wartosc x : %d \n", zmienna_x);
        return wskaznik_na_x;
}


int main(){

int wsk_adres = test_wskaznikow(42);
printf("funkcja uzyla miejsca w pamieci o adresie %d \n", wsk_adres);

return 0;

}

Ten program nie jest napisany do końca prawidłowo, tzn skompiluje się, ale podczas kompilacji dostaniemy tzw. "warningi", ostrzeżenia, że prawdopodobnie zrobiliśmy coś nie do końca jak należy, ale czysto teoretycznie program w takiej formie mógłby się wykonać:

user-46-113-227-209:~ magda$ gcc test.c
test.c: In function ‘test_wskaznikow’:
test.c:13: warning: format ‘%d’ expects type ‘int’, but argument 2 has type ‘int *’
test.c:16: warning: return makes integer from pointer without a cast


Chodzi o to, że odwołujemy się bezpośrednio do wskaźnika traktując do jako liczbę całkowitą. Należało by pierw zrzutować go na odpowiedni typ, czyli poprawić następujące linijki:

printf("Wartosc wskaznika: %ld , wartosc pod wskaznikiem: %d \n", (long)wskaznik_na_x, *wskaznik_na_x);
return (long)wskaznik_na_x;

Zauważmy teraz, że wykonanie programu wciąż wygląda podejrzanie:

user-46-113-227-209:~ magda$ ./a.out 
Wartosc wskaznika: 140734799804988 , wartosc pod wskaznikiem: 42 
wartosc x : 0 
funkcja uzyla miejsca w pamieci o adresie 1606416956 


Wewnątrz funkcji wypisywany jest dużo dłuższy adres z pamięciu, niż w funkcji main. Dlaczego tak się dzieje? Wskazówka: zwróć uwagę na to, jakiego typu jest wskaźnik wewnątrz funkcji, a jako jaki typ jest traktowany w funkcji main. Co należałoby poprawić?

Zadania

Silnia

Napisz program obliczający silnię zadanej liczby N. Zdefiniuj funkcję silnia, a następnie wywołaj ją w funkcji main. W pierwszym podejściu możesz wartość N wpisać "na sztywno" w kodzie. W drugim podejściu spróbuj pobierać ją jako parametr programu.

kompilacja (courtesy of wikipedia)

GCC (ang. GNU Compiler Collection) to zestaw kompilatorów do różnych języków programowania rozwijany w ramach projektu GNU i udostępniany na licencji GPL oraz LGPL. GCC jest podstawowym kompilatorem w systemach uniksopodobnych przy czym szczególnie ważną rolę odgrywa w procesie budowy jądra Linuksa. Początkowo skrótowiec GCC oznaczał GNU C Compiler, ponieważ był to kompilator wyłącznie do języka C.

Standardowo używamy kompilator gcc do kompilacji programów napisanych w C (rozszerzenie pliku c), a g++ do kompilacji programów napisanych w C++ (rozszerzenie pliku cc).

Program gcc (wywoływany podczas kompilacji np. z linii poleceń) odpowiada za przetworzenie argumentów, uruchomienie odpowiedniego kompilatora właściwego dla języka programowania w jakim zakodowano plik z kodem źródłowym, wykonanie programu asemblera dla tak otrzymanego wyniku oraz uruchomienie konsolidatora (linkera) w celu uzyskania pliku wykonywalnego. Przykładowo dla pliku napisanego w C zostaną wykonane następujące programy: preprocesor cpp, kompilator cc1, asembler as oraz konsolidator collect2 (dostępny zazwyczaj jako program ld).

Przydatne opcje:

  • -c, bez linkowania. Otrzymujemy pliki obiektowe *.o, używamy zwykle gdy nasz program składa się z kilku plików z funkcjami i nagłówkami tychże funkcji (własne biblioteki).
  • -o (output), podanie nazwy pliku wynikowego: g++ -o hello hello.cc
  • -Wall wypisz wszystkie ostrzeżenia (Warnings all)
  • -O optymalizacja (-O, -O0, -O1, -O2, -O3, -Os)(kolejne stopnie optymalizacji)
  • -g tworzy informację potrzebną do debuggowania w naturalnym dla danego systemu formacie. Debuggujemy zwykle za pomocą gdb. Opcji -g można używać z opcją -O. gdb można uruchomić w emacsie (dlatego go tak lubimy).

Zadanie

Napisz dwa programy — hello.c i hello.cc wypisujące "Hello world!" w terminalu. Skompiluj je i wywołaj. Dodaj do nich komentarze.