#4 Pytania rekrutacyjne – kod w języku C
Lista artykułów o rekrutacji w embedded
- #1 Pytania rekrutacyjne – wiedza o systemach wbudowanych cz.1
- #2 Pytania rekrutacyjne – wiedza o systemach wbudowanych cz.2
- #3 Pytania rekrutacyjne – język C w teorii
- #4 Pytania rekrutacyjne – kod w języku C
W tym artykule przedstawiam kolejną kategorię pytań rekrutacyjnych na stanowisko programisty w języku C, które tym razem skupiają się na praktycznych aspektach kodowania. Chociaż pytania praktyczne stanowią zazwyczaj mniejszą część rozmowy kwalifikacyjnej, są ważnym elementem pozwalającym rekruterom ocenić faktyczne umiejętności kodowania kandydata. Zwykle pytania te dotyczą napisania krótkiego fragmentu kodu lub przeanalizowania go w kontekście określonego problemu. Oczekuje się od kandydata umiejętności pisania zwięzłych i efektywnych rozwiązań, co sprawdza jego biegłość w operowaniu na poziomie kodu źródłowego i rozumieniu niuansów składni. Część z tych pytań przyjmuje formę zagadek w języku C, gdzie kluczową rolę odgrywa znajomość szczegółów języka i jego bardziej skomplikowanych mechanizmów.
Takie pytania często dotyczą kodu, który może być trudny do interpretacji, a czasem ma nieoczywiste lub zaskakujące wyniki, wymagające znajomości zaawansowanych technik programistycznych. Pomimo że w codziennym programowaniu rzadko stosuje się podobne podejście, rekruterzy korzystają z tego rodzaju pytań, by sprawdzić zdolność kandydata do analizy i radzenia sobie z nietypowymi sytuacjami. Często są to konstrukcje kodu niezalecane w praktyce, ponieważ mogą prowadzić do niebezpiecznych lub trudnych do przewidzenia wyników programu. Mimo to umiejętność ich interpretacji pokazuje rekruterom poziom wiedzy kandydata oraz jego wyczucie w stosowaniu języka C. Warto przygotować się na takie wyzwania, nawet jeśli mogą wydawać się nieco oderwane od rzeczywistości, ponieważ są one sprawdzianem umiejętności logicznego myślenia oraz znajomości tajników języka C. Zrozumienie i przećwiczenie tych zadań nie tylko zwiększy pewność siebie na rozmowie, ale również pomoże poszerzyć kompetencje techniczne.
Poniżej przedstawiam Ci 15 pytań, w których zadaniem jest napisanie krótkiego fragmentu kodu. Do każdego pytania przedstawiam przykładowe rozwiązanie zadania, chociaż pamiętaj o tym, że sposobów jest zazwyczaj tak dużo, jak osób rozwiązujących dane zagadnienie. Starałem się wybierać w miarę proste, ale i ciekawe sposoby realizacji kodu. A może masz propozycję swoich rozwiązań? Napisz je w komentarzu!

1. Napisz „typedef” na wskaźnik do funkcji, która przyjmuje dwa argumenty typu int i zwraca int.
typedef int (*FuncPtr)(int, int);
Stosowanie typedef dla funkcji ma na celu uproszczenie i poprawienie czytelności kodu oraz ułatwienie zarządzania wskaźnikami do funkcji. Dzięki typedef można stworzyć alias dla określonego typu wskaźnika do funkcji, co pozwala unikać wielokrotnych, skomplikowanych deklaracji i sprawia, że kod jest bardziej zrozumiały.
2. Jak przekazać adres funkcji jako argument do innej funkcji w języku C? Napisz przykładowy kod.
void printMessage() { printf("Hello, World!\n"); }
void callFunction(void (*func)()) { func(); }
int main()
{
callFunction(printMessage);
return 0;
}
Funkcję można przekazać jako argument do innej funkcji, używając wskaźnika na funkcję. W powyższym przykładzie void callFunction(void (*func)()) przyjmuje wskaźnik do funkcji printMessage, który następnie jest wywoływany wewnątrz callFunction.
3. Napisz funkcję inicjalizującą tablicę, tak aby każdy element tablicy był równy jego indeksowi.
void initArray(uint32_t *arr, uint32_t size)
{
for (uint32_t </code>i = 0; i < size; i++)
{
*(arr+i) = i;
}
}
4. Napisz proste makro do obliczania kwadratu liczby.
#define SQUARE(x) ((x) * (x))
Powyższa instrukcja tworzy makro SQUARE, które przyjmuje jeden argument x i zwraca jego kwadrat. Użycie nawiasów zapobiega problemom z kolejnością wykonywania operacji.
5. Wyświetl rozmiar tablicy za pomocą operatora „sizeof”.
uint32_t arr[10];
printf("Rozmiar tablicy: %u\n", sizeof(arr) / sizeof(arr[0]));
Operator sizeof zwraca rozmiar w bajtach, więc dzieląc całkowity rozmiar przez rozmiar jednego elementu, otrzymujemy liczbę elementów.
6. Napisz funkcję, która zamieni łańcuch znaków na liczbę całkowitą. Obsłuż liczby dziesiętne, heksadecymalne i binarne.
#include <stdio.h>
int str_to_int(const char *str)
{
int result = 0;
int base = 10; // Domyślnie baza dziesiętna
int i = 0;
int sign = 1;
// Sprawdzenie znaku na początku łańcucha
if (str[0] == '-')
{
sign = -1;
i++;
}
else if (str[0] == '+')
{
i++;
}
// Sprawdzenie prefiksu dla liczb szesnastkowych (0x) lub binarnych (0b)
if (str[i] == '0')
{
if (str[i + 1] == 'x' || str[i + 1] == 'X')
{
base = 16;
i += 2;
}
else if (str[i + 1] == 'b' || str[i + 1] == 'B')
{
base = 2;
i += 2;
}
}
// Przetwarzanie liczby w zależności od podstawy
while (str[i] != '\0')
{
int digit = 0;
if (str[i] >= '0' && str[i] <= '9')
{
digit = str[i] - '0';
}
else if (base == 16 && str[i] >= 'A' && str[i] <= 'F')
{
digit = str[i] - 'A' + 10;
}
else if (base == 16 && str[i] >= 'a' && str[i] <= 'f')
{
digit = str[i] - 'a' + 10;
}
else
{
// Jeśli nie jest to cyfra w danej bazie, przerywamy przetwarzanie
break;
}
// Aktualizacja wyniku, sprawdzając zakres dozwolonych cyfr w podstawie
if (digit >= base)
{
break; // Przerywamy jeśli cyfra nie jest poprawna dla bazy
}
result = result * base + digit;
i++;
}
return result * sign;
}
- Jeśli pierwszy znak to -, zmienna sign przyjmuje wartość -1, a jeśli to +, funkcja przechodzi do następnego znaku.
- Prefiks 0x lub 0X oznacza system szesnastkowy (hex), a 0b lub 0B oznacza system binarny.
- Funkcja przetwarza cyfry, mnożąc wynik przez podstawę i dodając wartość każdej cyfry w odpowiedniej podstawie.
- Wynik mnożony przez sign (dla wartości ujemnych).
Poniżej przykład użycia funkcji:
int main()
{
const char *dec_str = "123";
const char *hex_str = "0x7B";
const char *bin_str = "0b1111011";
const char *neg_str = "-45";
printf("Dziesiętna: %s -> %d\n", dec_str, str_to_int(dec_str));
printf("Szesnastkowa: %s -> %d\n", hex_str, str_to_int(hex_str));
printf("Binarny: %s -> %d\n", bin_str, str_to_int(bin_str));
printf("Ujemna: %s -> %d\n", neg_str, str_to_int(neg_str));
return 0;
}
7. Jaki będzie wynik wyrażenia: i = i++; ?
Wynik może być nieokreślony, ponieważ i = i++; powoduje konflikt między przypisaniem a postinkrementacją. W praktyce większość kompilatorów zachowa starą wartość i, ale lepiej unikać tego typu konstrukcji. Jest to tzw. „zachowanie niezdefiniowane” (undefined behavior), czyli sytuacja w języku C (i innych językach niskopoziomowych), w której kod wykonuje operacje, które nie mają zdefiniowanego wyniku według standardu języka. Kompilator nie jest zobowiązany do określenia, co się stanie w przypadku takiego zachowania – może prowadzić do różnych skutków, takich jak niespodziewane wyniki, awarie programu, a nawet brak jakiejkolwiek widocznej reakcji. Kod wywołujący undefined behavior może działać inaczej na różnych platformach, wersjach kompilatora czy przy innych optymalizacjach.
8. Jaki będzie wynik wyrażenia?
i = (1+1, 2+2, 3+3, 4+4);
Zmienna i przyjmie wartość 8, ponieważ operator przecinka „,” wykonuje kolejne operacje i zwraca wartość ostatniego wyrażenia, czyli 4+4.
9. Jaki będzie wynik operacji?
float a = 2;
float b = 1 / 2 * a;
Wynik b wyniesie 0.0, ponieważ 1 / 2 zwraca 0 w arytmetyce liczb całkowitych. Wartość a jest mnożona przez 0, więc wynik końcowy to 0.0.
10. Napisz makro do zamiany wartości dwóch zmiennych typu int.
#define SWAP(a, b) { int temp = a; a = b; b = temp; }
11. Wyjaśnij, jak przekazać zmienną „int” do funkcji za pomocą wskaźnika i zmodyfikować ją wewnątrz funkcji.
void modifyValue(uint32_t *num)
{
*num = 10;
}
12. Wyjaśnij, co robi operator „&” w poniższym wyrażeniu.
int x = 5;
int *ptr = &x;
Operator & zwraca adres zmiennej x, który następnie jest przypisywany wskaźnikowi ptr.
13. Napisz funkcję, która odwraca kolejność bitów w liczbie całkowitej.
uint32_t reverseBits(uint32_t n)
{
uint32_t reversed = 0;
for (int i = 0; i < 32; i++)
{
reversed <<= 1;
reversed |= (n & 1);
n >>= 1;
}
return reversed;
}
14 Napisz funkcję, która sprawdzi, czy liczba jest potęgą dwójki.
bool isPowerOfTwo(uint32_t n)
{
return (n & (n - 1)) == 0;
}
Liczba będąca potęgą dwójki ma tylko jeden bit ustawiony na 1. Warunek (n & (n – 1)) == 0 sprawdza, czy dokładnie jeden bit jest ustawiony w podanej jako argument liczbie.
15. Napisz przykład kodu, jak ustawić bit 5 w rejestrze 32-bitowym mając podany adres rejestru.
#define REGISTER_ADDRESS 0x40021000
int main()
{
// Tworzymy wskaźnik do adresu rejestru
volatile uint32_t *reg = (uint32_t *)REGISTER_ADDRESS;
// Ustawiamy bit 5
*reg |= (1 << 5);
return 0;
}
Podsumowanie
Pytania rekrutacyjne dotyczące kodowania w języku C mogą obejmować szerokie spektrum zagadnień – od prostych operacji na bitach po bardziej złożone problemy z zarządzaniem pamięcią i wskaźnikami. Omówione przykłady miały na celu pokazać różne sposoby podejścia do typowych zadań i wyzwań, jakie można napotkać podczas rozmowy rekrutacyjnej. Dzięki tym pytaniom rekruterzy mogą ocenić nie tylko znajomość języka, ale również umiejętność rozwiązywania problemów oraz zrozumienie działania kompilatora, struktury pamięci i efektywnej pracy z rejestrami. Ćwiczenie praktycznych zadań, takich jak ustawianie bitów w rejestrze, tworzenie makr czy analiza kodu pod kątem błędów, pomoże w przygotowaniu się do rozmowy i lepszym zrozumieniu, jak funkcjonują różne mechanizmy języka C.
Przedstawione zadania, choć często są krótkie, pomagają w głębszym zrozumieniu kluczowych aspektów C i sprawdzają zdolność programisty do wykrywania subtelnych błędów oraz zarządzania niskopoziomowymi operacjami na pamięci. Warto przed rozmową przećwiczyć tego typu operacje, ponieważ na co dzień często korzystamy z napisanego już wcześniej kodu, kopiując go i „na gorąco” możemy mieć problem z napisaniem z pozoru łatwego kodu, pomimo że dobrze rozumiemy zagadnienie.
