fbpx

Chciałbym zrobić wstęp do podziału projektu na pliki w języku C. Jest to nam potrzebne też do tego, aby zrozumieć czym jest moduł lub jednostka kompilacji.

Dobre zrozumienie pojęcia “jednostka kompilacji” spowoduje to, że później lepiej będziesz się orientował w zakresach widoczności np. zmiennych. Dodatkowo lepiej zrozumiesz czym jest kompilacja, a czym linkowanie.

Dlaczego podział na pliki?

Pisząc swój program na 20000% zacznie on zajmować sporo linii. Poruszanie się po programie mającym 1 tys. linii zaczyna być uciążliwe:

  • Scrollowanie jest bardzo niewygodne
  • Ciężko jest odnajdować interesujące nas rzeczy jeśli nie trzymaliśmy się ściśle jakiegoś porządku
  • …z porządkiem zresztą też

Pomyśl, że chociażby Windows ma tych linii około miliona. Słyszałem to już kilka lat temu, więc w najnowsze pewnie mają już kilka milionów.

Jak żyć?!

Trzeba w końcu zacząć dzielić program na mniejsze moduły. Moduły te to będą zestawy plików, które później dołączamy do programu głównego, lub łączymy je między sobą.

Takie moduły możemy już znać z biblioteki standardowej! Wszystkie biblioteki zewnętrzne jak np. string.h są właśnie takimi osobnymi zestawami plików. Nazywamy je właśnie bibliotekami.

Pisząc własne programy na mikrokontrolery również będziesz pisał takie biblioteki do różnych celów. Obsługa czujnika, obsługa menu, jakaś grafika. To będą pojedyncze moduły.

Pliki w języku C

Jakie mamy pliki w C? Te najważniejsze i te, które nas interesują to pliki z rozszerzeniem *.c i *.h.

Pliki H

Jest to header, czyli nagłówek. W tym miejscu będą się znajdowały jedynie prototypy i deklaracje.

Plik taki nie ma kodu wykonywalnego. Albo inaczej… nie chcemy, aby miał 🙂

W nim będziemy pisali tzw. interfejs. Będzie to tylko to, czym możemy się posługiwać używając tego pliku lub ściślej mówiąc biblioteki.

Mówiąc po ludzku… Tutaj będą znajdowały się takie rzeczy jak deklaracje:

  • typów
  • enumów
  • struktur
  • funkcji, czyli same prototypy
  • zmiennych globalnych (o ile będziemy je chcieli ;))

Nie będzie tutaj kodu wykonywalnego, czyli ciał funkcji.

Zestaw takich deklaracji dołączymy do drugiego rodzaju pliku (z rozszerzeniem *.c). Na przykład do pliku main.c.

Potrzebujemy wykorzystać do tego celu preprocesor.

#include “plik.h”

#include <plik.h>

W ostrym nawiasie <plik.h> wskazujesz plik ze ścieżki domyślnej kompilatora.

W cudzysłowie podajesz plik ze ścieżki projektu.

Pliki C

Pliki z rozszerzeniem *.c to pliki źródłowe. W skrócie źródła.

To jest miejsce na właściwy kod. Tutaj piszemy ciała funkcji.

Do tego pliku również dołączamy inne nagłówki jak, chociażby nagłówek główny main.h lub stdio.h.

Plik ten musi znać definicje typów, na których pracuje, oraz inne nagłówki, które będą wymagane do skompilowania tego pliku.

Oprócz funkcji możemy umieścić tu zmienne globalne. Będą one mogły być widoczne globalnie, czyli w całym projekcie. Tylko… to będzie rzadkość i ostateczność tak naprawdę. W dobrej praktyce raczej unikamy zmiennych globalnych. Kiedyś o tym opowiem więcej.

Jeśli nie globalne, to jakie? Globalne o ograniczonym zasięgu do tego konkretnego pliku. To jest dużo lepsza i częstsza praktyka

Chyba będę musiał wcześniej opowiedzieć o tych globalach. Daj znać, czy zrobić to w następnych mailach.

Minimum dla kompilatora

Może to się wyda zaskoczenie, ale trzeba wiedzieć, że kompilator do skompilowania pliku potrzebuje tylko definicji.

Nie potrzebuje mieć konkretnej zmiennej zaalokowanej w pamięci.

Nie potrzebuje też ciała funkcji! Wystarczy mu sam prototyp.

Możesz spytać jak to?! Słowo klucz: na etapie kompilacji.

To jest całkiem śmieszne, ale kompilacja jako cały proces od A do Z dzieli się na kilka etapów. Najważniejsze z nich to:

  1. Preprocesor
  2. Kompilacja
  3. Linkowanie

Tak! Kompilacja jest częścią kompilacji! Ekstra, co nie? 🙂

I to właśnie do kroku nr 2 potrzebna jest wiedza tylko o prototypach. Dlaczego?

Bo ten etap kompiluje TYLKO jeden plik C jednocześnie. Z jednego pliku tworzy jeden tzw. plik obiektowy. Tworzą się takie moduły. Jest to pojedyncza jednostka kompilacji.

W nim (tym pliku obiektowym) znajdują się (mówiąc w ogromnym uproszczeniu) informacje o tym, że chcemy użyć zmiennej X w jakimś miejscu lub funkcji Y z argumentami Z i W w takim miejscu programu.

Kompilacja tworzy więc (znowu w ogromnym skrócie) mapę powiązań co, kiedy, w jaki sposób i z jakimi operacjami robić. Goły asembler bez powiązań z adresami w pamięci.

Nie musi znać funkcji, ale wie, że taką trzeba wywołać. Teraz kto spina te informacje? LINKER na etapie linkowania.

Linker na wejściu zbiera wszystkie pliki obiektowe po kompilacji i tworzy z nich całość. Rozwiązuje te powiązania zmiennych i funkcji między modułami i przypisuje im konkretne miejsca w pamięci np. mikrokontrolera.

Widzi na przykład, że Moduł1 chciał wywołać funkcję znajdująca się w Moduł2. Łączy więc wywołanie z funkcją znajdującą się w INNEJ jednostce kompilacji.

To będzie ważne, aby zrozumieć istotę działania. Bo taki prototyp co mówi kompilatorowi?

“Mam taką funkcję i chcę ją użyć. Nie mam ciała – to rozwiąże linker”

Kompilator ustawi instrukcje, argumenty na stosie, zwrotkę z funkcji i tyle. Teraz linker musi podłączyć te operacje z odpowiednią funkcją w innej jednostce kompilacji.

I tutaj dochodzimy to tego, że zarówno kompilator jak i linker będzie zgłaszał inne problemy.

Kompilator powie Ci najczęściej, że nie zna nazwy zmiennej lub funkcji przy ich użyciu. Oznacza to, że nigdy wcześniej nie widział prototypu funkcji lub definicji zmiennej. Trzeba go poinformować tylko i wyłącznie o istnieniu (nazwie i typach). Ciało funkcji lub rezerwacja pamięci dla zmiennej może być w innej jednostce kompilacji. To go już nie interesuje.

Co zgłasza najczęściej linker? Że nie może rozwiązać powiązania. Taka sytuacja będzie, jeśli w jednej jednostce kompilacji powiadomimy prototypem o istnieniu funkcji, której… ciała nie będzie nigdzie indziej (w innych jednostkach kompilacji). Linker nie rozwiązał powiązania i zwrócił błąd.

To są dwa zupełnie różne komunikaty i inaczej trzeba do nich podchodzić.

Podsumowanie

Trochę się rozpisałem, a nadal czuję, że nie wszystko powiedziałem… Daj mi znać, jeśli chciałbyś czegoś dłuższej i bardziej rozbudowanej formie.

Nie zrobiliśmy, chociażby treningu, czyli tego jak to ma faktycznie wyglądać np. w kompilatorze online. Tekstowo może być to ciężkie do zrealizowania. Może wolałbyś naukę w formie wideo? Mam w takim razie pewną propozycję 🙂

Chcesz się nauczyć języka C z myślą o mikrokontrolerach?

Stworzyłem kurs dedykowany mikrokontrolerom. Uczę w nim języka C od podstaw. Wszystko to, co omówiłem w tym wpisie (i wiele, wiele więcej) znajduje się w programie kursu.

Zebrałem swoje doświadczenie z kilku lat programowania embedded i chcę przekazać Ci jak najlepszą wiedzę. Uczestniczyłem w różnych projektach: samodzielnie, start-up, średnia firma i olbrzymia korporacja.

Oprócz podstaw i składni przekazuję masę dobrych praktyk. Wplatam to między tłumaczenie kolejnych aspektów języka C.

Dodatkowym atutem jest również to, że pokazuję jak można dobrze prowadzić projekt. Pokażę Ci jak radzić sobie z budowaniem warstw abstrakcji. Skorzystamy przy tym ze struktur, wskaźników i callbacków. No i oczywiście podział na pliki. To wiele pomaga.

Takie odseparowane warstwy dużo łatwiej dają się przenosić między projektami, a nawet między różnymi rodzinami mikrokontrolerów.

Dołącz do listy oczekujących na kurs i zacznij naukę razem z przygotowanymi przeze mnie materiałami. Po zapisaniu się będziesz otrzymywał co tydzień maile o języku C: https://cdlamikrokontrolerow.pl

5/5 - (2 votes)

Podobne artykuły

.

4 komentarze

es2 · 01/07/2022 o 23:58

Deklarowanie zmiennych itp w plikach .H to był, podkreślam BYŁ dobry (czasami) POMYSŁ. Od CubeIDE 1.9 jest problem, setki ostrzeżeń, dziesiątki błędów. Już to przechodziłem i to dwa razy.
Nie polecam takiej “rozryówki”. W jednym przypadku, używam wersji 1.8, które się “nie czepia”.

    Mateusz Salamon · 04/07/2022 o 09:10

    Korzystam z 1.9.0 w tej chwili i jest wszystko ok 😉 Zasady programowania w C się nie zmieniły 😀

Daniel · 27/05/2022 o 11:10

Wstaje sobie człowiek rano, kończy wcześniej calla, zaczyna przeglądać sobie aktualności… Ale w życiu by nie przypuszczał, że akurat przeczyta artykuł, który po latach okazjonalnego korzystania z C wyjaśni mu, jak naprawdę działa kompilator i to całe linkowanie. Wow, to będzie dobry dzień! 🙂

    Mateusz Salamon · 06/06/2022 o 19:29

    Cieszę się, że mogłem pomóc 🙂 Właśnie sporo osób nie pojmuje tego, a to jest ważne, aby wiedzieć gdzie jest błąd kompilacji 🙂

Dodaj komentarz

Avatar placeholder

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *