C Управління пам'яттю


90

Я завжди чув, що на мові С потрібно реально спостерігати за тим, як ти керуєш пам’яттю. І я все ще починаю вивчати C, але поки що мені взагалі не доводилось робити жодної пам'яті, що керує пов'язаними з ними діями .. Я завжди уявляв, що маю звільняти змінні і робити всілякі потворні речі. Але це, здається, не так.

Хтось може показати мені (із прикладами коду) приклад того, коли вам довелося б зробити «управління пам’яттю»?


Гарне місце для вивчення G4G
EsmaeelE

Відповіді:


231

Є два місця, де змінні можна помістити в пам’ять. Коли ви створюєте таку змінну:

int  a;
char c;
char d[16];

Змінні створюються в " стеку ". Змінні стека автоматично звільняються, коли вони виходять за межі області дії (тобто коли код більше не може до них дістатися). Ви можете почути їх як "автоматичні" змінні, але це вийшло з моди.

Багато прикладів для початківців використовуватимуть лише змінні стека.

Стек хороший, оскільки він автоматичний, але він також має два недоліки: (1) Компілятор повинен заздалегідь знати, наскільки великі змінні, і (b) простір стека дещо обмежений. Наприклад: у Windows у налаштуваннях за замовчуванням для компоновника Microsoft стек встановлений у 1 МБ, і не всі вони доступні для ваших змінних.

Якщо ви не знаєте під час компіляції, наскільки великий ваш масив, або якщо вам потрібен великий масив або структура, вам потрібен "план B".

План Б називається « купа ». Зазвичай ви можете створювати такі великі розміри, наскільки вам дозволяє операційна система, але ви повинні зробити це самостійно. Раніше публікації показували вам один із способів це зробити, хоча є й інші способи:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(Зверніть увагу, що змінні в купі не маніпулюють безпосередньо, а за допомогою покажчиків)

Після створення змінної купи, проблема полягає в тому, що компілятор не може визначити, коли ви закінчили з нею, тому ви втрачаєте автоматичний випуск. Ось тут і з’являється «ручний випуск», про який ви мали на увазі. Ваш код тепер відповідає за те, щоб вирішити, коли змінна більше не потрібна, і випустити її, щоб пам’ять можна було взяти для інших цілей. У наведеному вище випадку з:

free(p);

Що робить цей другий варіант «неприємним бізнесом», так це те, що не завжди легко зрозуміти, коли змінна вже не потрібна. Якщо ви забудете випустити змінну, коли вона вам не потрібна, ваша програма споживатиме більше пам'яті, ніж їй потрібно. Ця ситуація називається "витоком". «Витік» пам’яті не можна використовувати ні для чого, поки ваша програма не закінчиться і ОС не відновить усі свої ресурси. Навіть більш неприємні проблеми можливі, якщо помилково випустити змінну купи до того, як ви фактично закінчите з нею.

У C та C ++ ви несете відповідальність за очищення змінних кучі, як показано вище. Однак існують мови та середовища, такі як Java та .NET, такі як C #, які використовують інший підхід, коли купа очищається самостійно. Цей другий метод, який називається "збір сміття", набагато простіший для розробника, але ви сплачуєте штраф накладними витратами та продуктивністю. Це баланс.

(Я розглянув багато деталей, щоб дати простішу, але, сподіваюся, більш рівноцінну відповідь)


3
Якщо ви хочете помістити щось у стек, але не знаєте, наскільки воно велике під час компіляції, alloca () може збільшити рамку стека, щоб звільнити місце. Немає freea (), весь кадр стека вискакує, коли функція повертається. Використання alloca () для великих розподілів загрожує небезпекою.
DGentry

1
Можливо, ви могли б додати одне-два речення про розташування в пам'яті глобальних змінних
Michael Käfer

У C не наводите повернення malloc(), його причини UB, (char *)malloc(size);см stackoverflow.com/questions/605845 / ...
EsmaeelE

17

Ось приклад. Припустимо, у вас є функція strdup (), яка дублює рядок:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

І ви називаєте це так:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

Ви бачите, що програма працює, але ви виділили пам’ять (через malloc), не звільняючи її. Ви втратили вказівник на перший блок пам’яті, коли вдруге зателефонували strdup.

Це не є великою проблемою для цього невеликого обсягу пам'яті, але розглянемо випадок:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

Зараз ви використали 11 гігабайтів пам'яті (можливо, більше, залежно від диспетчера пам’яті), і якщо у вас не було збоїв, ваш процес, ймовірно, працює досить повільно.

Щоб виправити ситуацію, вам потрібно зателефонувати безкоштовно () для всього, що отримано з malloc () після того, як ви закінчите його використовувати:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

Сподіваюся, цей приклад допоможе!


Мені ця відповідь більше подобається. Але я отримав невелике побічне запитання. Я би очікував, що щось подібне буде вирішено за допомогою бібліотек, чи немає бібліотеки, яка б точно імітувала основні типи даних і додавала до них функції звільнення пам'яті, щоб, коли змінні звикають, вони також звільнялися автоматично?
Lorenzo

Жоден, що є частиною стандарту. Якщо ви перейдете до C ++, ви отримаєте рядки та контейнери, які виконують автоматичне управління пам'яттю.
Mark Harrison

Бачу, значить, є якісь сторонні бібліотеки? Не могли б ви назвати їх?
Lorenzo

9

Вам потрібно зробити «управління пам’яттю», коли ви хочете використовувати пам’ять у купі, а не в стеку. Якщо ви не знаєте, наскільки великим є масив до часу виконання, тоді вам доведеться скористатися купою. Наприклад, ви можете захотіти зберегти щось у рядку, але не знаєте, яким великим буде його вміст, поки програма не буде запущена. У такому випадку ви б написали щось подібне:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory

5

Я думаю, що найбільш стислий спосіб відповісти на питання, щоб розглянути роль вказівника в C. Вказівник - це легкий, але потужний механізм, який дає вам величезну свободу ціною величезної здатності стріляти собі в ногу.

У C відповідальність за те, щоб ваші вказівники вказували на пам'ять, якою ви володієте, є вашою і лише вашою. Це вимагає організованого та дисциплінованого підходу, якщо ви не відмовитесь від покажчиків, що ускладнює написання ефективного C.

Опубліковані відповіді на сьогодні зосереджені на автоматичному (стековому) і купі змінних розподілах. Використання розподілу стеків робить для автоматично керованої та зручної пам'яті, але в деяких випадках (великі буфери, рекурсивні алгоритми) це може призвести до жахливої ​​проблеми переповнення стека. Точне знання того, скільки пам’яті ви можете виділити на стек, дуже залежить від системи. У деяких вбудованих сценаріях кілька десятків байтів можуть бути вашим обмеженням, у деяких сценаріях робочого столу ви можете безпечно використовувати мегабайти.

Розподіл купи менше притаманний мові. В основному це набір викликів бібліотеки, який надає вам право власності на блок пам’яті заданого обсягу, поки ви не будете готові повернути його («безкоштовно»). Це звучить просто, але асоціюється з незліченним горем програміста. Проблеми прості (звільнення однієї і тієї ж пам’яті двічі, або зовсім не [витоки пам’яті], не виділення достатньої кількості пам’яті (переповнення буфера тощо), але їх важко уникнути та налагодити. Високодисциплінований підхід є абсолютно обов'язковим у практичній роботі, але, звичайно, мова насправді не вимагає цього.

Я хотів би згадати інший тип розподілу пам'яті, який ігнорувався іншими публікаціями. Можна статично розподіляти змінні, оголошуючи їх поза будь-якою функцією. Я думаю, що загалом цей тип розподілу отримує поганий результат, оскільки він використовується глобальними змінними. Однак нічого не сказано, що єдиний спосіб використовувати виділену таким чином пам’ять - це недисциплінована глобальна змінна в безладді коду спагетті. Статичний метод розподілу може бути використаний просто для того, щоб уникнути деяких підводних каменів купи та методів автоматичного розподілу. Деякі програмісти на C з подивом дізнаються, що великі та складні програми для вбудовування та ігор C були побудовані без використання розподілу купи взагалі.


4

Тут є кілька чудових відповідей про те, як розподілити та звільнити пам’ять, і, на мій погляд, найскладнішою стороною використання C є забезпечення того, що єдиною пам’яттю, яку ви використовуєте, є виділена вами пам’ять - якщо це зроблено неправильно, що ви закінчуєте з цим є двоюрідний брат цього сайту - переповнення буфера - і ви можете перезаписати пам'ять, яка використовується іншим додатком, з дуже непередбачуваними результатами.

Приклад:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

На даний момент ви виділили 5 байт для myString і заповнили його "abcd \ 0" (рядки закінчуються нулем - \ 0). Якщо ваш рядок був

myString = "abcde";

Ви повинні призначити "abcde" у 5 байтах, які ви виділили своїй програмі, а кінцевий нульовий символ буде розміщений в кінці цього - частина пам'яті, яка не була виділена для вашого використання і може бути безкоштовний, але в рівній мірі може використовуватися іншим додатком - це критична частина управління пам’яттю, де помилка матиме непередбачувані (а іноді і неповторні) наслідки.


Тут ви виділяєте 5 байт. Звільніть, призначивши вказівник. Будь-яка спроба звільнити цей покажчик призводить до невизначеної поведінки. Примітка. С-рядки не перевантажують оператор = немає копії.
Мартін Йорк,

Хоча це насправді залежить від malloc, який ви використовуєте. Багато операторів malloc вирівнюються до 8 байт. Отже, якщо цей malloc використовує систему верхнього / нижнього колонтитула, malloc зарезервує 5 + 4 * 2 (4 байти як для верхнього, так і для нижнього колонтитула). Це було б 13 байт, а malloc просто дав би вам додаткові 3 байти для вирівнювання. Я не кажу, що це гарна ідея використовувати це, тому що це буде працювати лише у систем, чий malloc працює так, але принаймні важливо знати, чому робити щось неправильно може працювати.
kodai

Локі: Я відредагував відповідь для використання strcpy()замість =; Я припускаю, що це був намір Кріса до н.е.
echristopherson

Я вірю, що апаратний захист пам'яті сучасних платформ не дозволяє процесам користувацького простору перезаписувати адресні простори інших процесів; замість цього ви отримаєте помилку сегментації. Але це не є частиною С як такої.
ехрістоферсон,

4

Що слід пам’ятати, це завжди ініціалізувати вказівники на NULL, оскільки неініціалізований вказівник може містити псевдовипадкову дійсну адресу пам’яті, що може змусити помилки вказівника йти безшумно. Застосовуючи покажчик для ініціалізації за допомогою NULL, ви завжди можете зрозуміти, чи використовуєте ви цей покажчик, не ініціалізуючи його. Причина полягає в тому, що операційні системи "підключають" віртуальну адресу 0x00000000 до загальних винятків захисту, щоб затримати використання нульового покажчика.


2

Також вам може знадобитися використовувати динамічне розподіл пам'яті, коли вам потрібно визначити величезний масив, скажімо int [10000]. Ви не можете просто покласти його в стек, тому що тоді, хм ... ви отримаєте переповнення стека.

Ще одним хорошим прикладом може бути реалізація структури даних, скажімо пов'язаного списку або двійкового дерева. У мене немає зразка коду, який можна вставити сюди, але ви можете легко його загуглити.


2

(Я пишу, тому що відчуваю, що відповіді поки що не зовсім відповідають.)

Причиною того, що вам доводиться згадувати управління пам’яттю, є наявність проблеми / рішення, що вимагає створення складних структур. (Якщо ваші програми виходять з ладу, якщо ви виділяєте багато місця в стеці одночасно, це помилка.) Як правило, перша структура даних, яку вам потрібно буде вивчити, це якийсь список . Ось один зв’язаний, на маківці:

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

Звичайно, ви хотіли б кілька інших функцій, але в основному для цього вам потрібно керувати пам’яттю. Слід зазначити, що існує низка прийомів, які можливі при ручному керуванні пам'яттю, наприклад,

  • Використовуючи той факт, що malloc гарантовано (за мовним стандартом) повертає покажчик, що ділиться на 4,
  • виділяючи зайвий простір для якоїсь власної зловісної мети,
  • створення пулу пам'яті ..

Отримайте хороший налагоджувач ... Удачі!


Вивчення структур даних - це наступний ключовий крок у розумінні управління пам’яттю. Вивчення алгоритмів для належного запуску цих структур покаже вам відповідні методи подолання цих труднощів. Ось чому ви знайдете структури даних та алгоритми, що викладаються на тих самих курсах.
aj.toulan

0

@ Євро Міцеллі

Одним мінусом, який слід додати, є те, що покажчики на стек перестають бути дійсними, коли функція повертається, тому ви не можете повернути покажчик на змінну стека з функції. Це поширена помилка і головна причина, чому ви не можете обійтись лише за допомогою змінних стеку. Якщо вашій функції потрібно повернути покажчик, то вам доведеться malloc і мати справу з управлінням пам'яттю.


0

@ Тед Персіваль :
... вам не потрібно закидати повернене значення malloc ().

Ви, звичайно, праві. Я вважаю, що це завжди було правдою, хоча у мене немає копії K&R для перевірки.

Мені не подобається багато неявних перетворень в C, тому я, як правило, використовую закиди, щоб зробити "магію" більш видимою. Іноді це допомагає читабельності, іноді ні, а іноді змушує компілятор ловити тиху помилку. Проте я так чи інакше не маю твердої думки з цього приводу.

Це особливо ймовірно, якщо ваш компілятор розуміє коментарі у стилі C ++.

Так ... ти мене там спіймав. Я проводжу набагато більше часу на C ++, ніж C. Дякую, що це помітили.


@echristopherson, дякую. Ви маєте рацію, але зауважте, що ці запитання були з серпня 2008 року до того, як переповнення стека було навіть у публічній бета-версії. Тоді ми ще придумували, як повинен працювати сайт. Формат цього запитання / відповіді не обов'язково слід розглядати як модель використання SO. Дякую!
Euro Micelli

Ах, дякую, що вказали на це - я тоді ще не розумів, що аспект сайту все ще змінювався.
echristopherson

0

У C ви насправді маєте два різні варіанти. По-перше, ви можете дозволити системі керувати пам’яттю за вас. Крім того, ви можете зробити це самостійно. Як правило, ви хотіли б дотримуватися першого якомога довше. Однак, автоматично керована пам'ять в C надзвичайно обмежена, і вам доведеться вручну керувати пам'яттю у багатьох випадках, таких як:

a. Ви хочете, щоб змінна пережила функції, і ви не хочете мати глобальну змінну. напр .:

структура пари {
   int val;
   структура struct * next;
}

структурна пара * new_pair (int val) {
   структура пари * np = malloc (sizeof (структура пари));
   np-> val = val;
   np-> next = NULL;
   повернути np;
}

b. ви хочете мати динамічно виділену пам’ять. Найпоширенішим прикладом є масив без фіксованої довжини:

int * my_special_array;
my_special_array = malloc (sizeof (int) * number_of_element);
для (i = 0; i

c. Ви хочете зробити щось дійсно брудне. Наприклад, я хотів би, щоб структура представляла багато різновидів даних, і мені не подобається об'єднання (об'єднання виглядає дуже охайно):

struct data { тип даних int; довгі data_in_mem; }; структура тварина {/ * щось * /}; struct person {/ * якась інша річ * /}; структура тварина * read_animal (); struct person * read_person (); / * В основному * / структура вибірки даних; sampe.data_type = input_type; перемикач (тип_входу) { справа DATA_PERSON: sample.data_in_mem = read_person (); перерву; справа DATA_ANIMAL: sample.data_in_mem = read_animal (); за замовчуванням: printf ("А-а-а! Я попереджаю вас, що ще раз, і я буду сегментувати вашу ОС"); }

Дивіться, довгого значення достатньо, щоб вмістити НІЧОГО. Просто не забудьте звільнити його, інакше ви ПОЖАЛУЄТЕ. Це один з моїх улюблених трюків, щоб розважитися в C: D.

Однак, як правило, ви хочете уникати своїх улюблених трюків (T___T). Ви рано чи пізно зламаєте свою ОС, якщо будете використовувати їх занадто часто. Поки ви не використовуєте * alloc і free, можна впевнено сказати, що ви все ще незаймані і що код все ще виглядає добре.


"Дивіться, довгого значення достатньо, щоб вмістити НІЧОГО" -: / про що ви говорите, у більшості систем довге значення дорівнює 4 байтам, точно так само, як і int. Єдина причина, по якій він підходить тут для покажчиків, полягає в тому, що розмір long виявляється однаковим із розміром покажчика. Однак ви дійсно повинні використовувати void *.
Score_Under

-2

Звичайно. Якщо ви створюєте об'єкт, який існує за межами області, в якій ви його використовуєте. Ось надуманий приклад (майте на увазі, мій синтаксис буде вимкнено; мій C іржавий, але цей приклад все одно ілюструє концепцію):

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

У цьому прикладі я використовую об’єкт типу SomeOtherClass протягом життя MyClass. Об'єкт SomeOtherClass використовується в декількох функціях, тому я динамічно розподіляю пам'ять: об'єкт SomeOtherClass створюється при створенні MyClass, використовується кілька разів протягом життя об'єкта, а потім звільняється після звільнення MyClass.

Очевидно, якби це був справжній код, не було б жодної причини (крім можливої ​​витрати пам'яті стека) створювати myObject таким чином, але такий тип створення / знищення об'єктів стає корисним, коли у вас багато об'єктів, і ви хочете точно контролювати коли вони створюються та знищуються (щоб ваш додаток не засмоктував 1 ГБ оперативної пам'яті протягом усього життя, наприклад), а у віконному середовищі це майже обов'язково, оскільки об'єкти, які ви створюєте (кнопки, скажімо) , повинні існувати за межами сфери дії будь-якої конкретної функції (або навіть класу).


1
Хе, так, це C ++, чи не так? Дивно, що пішло п’ять місяців, щоб хтось зателефонував мені.
TheSmurf
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.