C, PHP, VB, .NET

Дневникът на Филип Петров


* Static в C/C++

Публикувано на 21 март 2025 в раздел С/С++.

Ключовата дума static в C/C++ дава възможност на променлива да се запише в статичната област на паметта. Това е област, в която се дава възможност да се дефинират променливи с глобален обхват (в частност именно там се дефинират глобалните променливи). Те се намират в най-високите адреси на паметта, т.е. след heap. Или казано по друг начин - паметта изглежда по следния начин:

ниски адреси            високи адреси
STACK-расте-> | <-расте-HEAP | STATIC

Когато в C/C++ дефинираме глобална променлива, тя се разполага в STATIC паметта и се знае, че оттам насетне тя ще „живее“ там до края на изпълнението на програмата, както и че ще бъде споделена между всички функции.

Дефинирането на локална променлива като static ни дава възможност да я разположим в тази област и също „да живее“ до края на изпълнението на програмата, т.е. да не се загубва след края на изпълнението на функцията. Ще илюстрирам това с пример. Нека имаме функция, която дефинира локална променлива, увеличава я с единица и я отпечатва на екрана:

#include <stdio.h>
void func(){
   int counter = 0;
   counter++;
   printf("%d\n", counter);
}
int main() {
   func();
   func();
   func();
   return 0;
}

Съвсем очаквано резултатът ще са три пъти числото 1 на нов ред, защото всеки път се създава нова локална променлива counter (и тя „живее“ в стека при  всяко поредно извикване на функцията). Ако обаче направим counter да бъде статична локална променлива:

#include <stdio.h>
void func(){
   static int counter = 0;
   counter++;
   printf("%d\n", counter);
}
int main() {
   func();
   func();
   func();
   return 0;
}

резултатът вече ще е съвсем различен - вече ще получаваме числата 1, 2 и 3. При първото извикване на func() променливата counter се е създала в статичната памет и се е инициализирала на 0. При второто извикване тази променлива не се инициализира повторно (пропуска се "=0"), защото логиката е направена така, че това да се пропуска ако такава променлива вече съществува. Тоест при второто извикване на функцията вместо да се дефинира нова променлива counter с начална стойност 0, ще бъде използвана съществуващата в статичната памет променлива и ще се вземе нейната стойност, която е 1. Ето така локалните статични променливи дават възможност да си пазят стойността и след излизането от функцията.

Употребата на локални статични променливи в езика C е по-скоро непрепоръчителна. Обикновено те намират място в по-специфични случаи - например те са много удобни когато правим рекурсивни функции (когато една функция извиква самата себе си). Но дори при тях употребата на static е спорна. Ето един пример. Нека вземем класическата функция за пресмятане на числата на Фибоначи:

#include <stdio.h>
unsigned long long fibonacci(int n);
int main() {
   int n = 30;
   printf("Fib(%d) = %llu", n, fibonacci(n));
   return 0;
}
unsigned long long fibonacci(int n) {
   if (n > 93) return 0; // извън типа
   if (n <= 1) return n;
   return fibonacci(n-1) + fibonacci(n-2);
}

При много големи числа това пресмятане ще е изключително бавно. Затова можем да включим техниката, наречена „мемоизация“ - да си записваме вече изчислени числа в масив и да не ги пресмятаме пак. Вместо да е глобален (безсмислено, защото нямаме нужда от този масив извън самата функция) или пък да си го предаваме като входен параметър с указател, най-удобно е масивът да е статична за функцията променлива:

#include <stdio.h>
unsigned long long fibonacci(int n);
int main() {
   int n = 30;
   printf("Fib(%d) = %llu", n, fibonacci(n));
   return 0;
}
unsigned long long fibonacci(int n) {
   if (n > 93) return 0; // извън типа
   static unsigned long long memo[93] = {0};
   if (n <= 1) return n;
   if (memo[n] != 0) return memo[n];
   memo[n] = fibonacci(n-1) + fibonacci(n-2);
   return memo[n];
}

Извън рекурсивните алгоритми кодът става по-труден за проследяване и започва да крие неприятни „изненади“. Нещата се променят веднага когато се премине в света на обектно-ориентираното програмиране в C++. Там дефинирането на член-променлива на даден клас като статична веднага се явява като логична и често търсена техника за програмиране. Това прави член-променливата като споделен ресурс между всички обекти от съответния клас. Или да я приемем като глобална променлива за обхвата на класа. Чрез такива променливи например може различни нишки (Threads) да споделят общи ресурси. И т.н.

В света на ООП също получаваме и възможността да дефинираме статични функции. Това са функции, които могат да достъпват само и единствено статични член-променливи - ограничение, за сметка на което получаваме друго нещо, което е много удобно - такива функции могат да се извикват директно чрез името на класа, т.е. без да се създава обект. Именно така правим библиотеки с готови функции, които да се извикват директно, без да сме създавали обект с оператор new.

Но да се върнем обратно в света на C. Rма още един начин да използваме статични променливи - да дефинираме статични глобални променливи. Нека сравним следните два кода:

// код 1
#include <stdio.h>
int counter = 0;
void func(){
   counter++;
   printf("%d\n", counter);
}
int main() {
   func();
   func();
   func();
   return 0;
}

// код 2
#include <stdio.h>
static int counter = 0;
void func(){
   counter++;
   printf("%d\n", counter);
}
int main() {
   func();
   func();
   func();
   return 0;
}

Тук вече обхватът на променливата е глобален (тя е достъпна не само във функцията func(), а навсякъде в програмата). И двата  примера отпечатват 1, 2 и 3, т.е. видимо действат по един и същи начин. Къде тогава е разликата между тях?

Разликата се появява тогава, когато имаме връзки между различни фалове, т.е. когато един файл добавя код от друг. Нека разгледаме следния пример - създали сме проект в CodeBlocks, в който имаме един основен файл (main.c), един допълнителен (additional.c), който (според добрите практики) е вмъкнат не директно, а чрез посредничеството на header файл additional.h.

Нека се опитаме да дефинираме глобална променлива с едно и също име както в main.c, така и в additional.c. Ще видите, че няма да се получи. Съвсем логично компилаторът ще се оплаче и ще каже, че една и съща променлива се опитва да бъде декларирана два пъти. Ето примерния код:

// Основен файл: main.c
#include "additional.h"
#include <stdio.h>
int x = 5;
int main() {
   printf("main.c: %d\n", x);
   func();
   return 0;
}
// header файл: additional.h
#ifndef ADDITIONAL_H_
#define ADDITIONAL_H_
void func();
#endif

// втори файл: additional.c
#include "additional.h"
int x = 10;
void func(){
   printf("additional.c: %d\n", x);
}

Грешката тук ще бъде main.c|3|multiple definition of `x' и кодът няма да може да се компилира.

Ако искаме променливата да е една и съща и за двата файла (main.c и additional.c), трябва да я декларираме в единия от тях, а в другия да я маркираме като extern (външна за файла). Например така:

// Основен файл: main.c
#include "additional.h"
#include <stdio.h>
extern int x;
int main() {
   printf("main.c: %d\n", x);
   func();
return 0;
}
// header файл: additional.h
#ifndef ADDITIONAL_H_
#define ADDITIONAL_H_
void func();
#endif

// втори файл: additional.c
#include "additional.h"
int x = 10;
void func(){
   printf("additional.c: %d\n", x);
}

Дефинирайки променливата x като extern в main.c ние казваме на компилатора, че такава променлива ще има, но тя не е дефинирана в този файл. Бихме могли да направим тази декларация и в header файла - по този начин автоматично ще се включи в main и няма да имаме нужда от деклариране на extern променливите при всяко добавяне на библиотеката, която пишем. Независимо кой от двата подхода вземем, вече я няма грешката „повторно дефиниране“ (когато дефинираме две глобални променливи с едно и също име) или грешката с „няма такава променлива“ (когато се опитваме да ползваме променлива, която не е декларирана в текущия файл).

Да, но по този начин променливата x стана споделена между двата файла - променяйки я в единия ще се променя и в другия. Как да направим така, че main.c да си има своя собствена глобална (за файла) променлива x, а additional.c да има друга своя собствена глобална (за файла) променлива x? Именно тук се намесва ключовата дума static. Дефинирайки глобална променлива като статична ние променяме нейния обхват и той вече не е за цялата програма, а е само за текущия файл. Следният пример:

// Основен файл: main.c
#include "additional.h"
#include <stdio.h>
static int x = 5;
int main() {
   printf("main.c: %d\n", x);
   func();
return 0;
}
// header файл: additional.h
#ifndef ADDITIONAL_H_
#define ADDITIONAL_H_
void func();
#endif

// втори файл: additional.c
#include "additional.h"
static int x = 10;
void func(){
   printf("additional.c: %d\n", x);
}

ще даде следния резултат:

main.c: 5
additional.c: 10

Виждаме, че вече различните файлове си имат свои собствени статични глобални променливи и не се бъркат един с друг.

 



Добави коментар

Адресът на електронната поща няма да се публикува


*