Оголошення змінних у файлах заголовків - статичні чи ні?


91

При рефакторингу деяких #definesя наткнувся на декларації, подібні до наведених нижче, у файлі заголовка C ++:

static const unsigned int VAL = 42;
const unsigned int ANOTHER_VAL = 37;

Питання в тому, яку різницю, якщо вона є, буде мати статичний ефект? Зверніть увагу, що багаторазове включення заголовків неможливе завдяки класичному #ifndef HEADER #define HEADER #endifтрюку (якщо це важливо).

Чи означає статична статична лише одна копія VAL, якщо заголовок включений кількома вихідними файлами?


Відповіді:


107

Це staticозначає, що VALдля кожного вихідного файлу, до якого він включений, буде створено по одній копії . Але це також означає, що багаторазове включення не призведе до кількох визначень, VALякі будуть стикатися під час зв’язку. У C без цього staticвам потрібно було б переконатися, що визначено лише один вихідний файл, VALтоді як інші вихідні файли оголосили його extern. Зазвичай це можна зробити, визначивши його (можливо, за допомогою ініціалізатора) у вихідному файлі та помістивши externдекларацію у файл заголовка.

static змінні на глобальному рівні видно лише у власному вихідному файлі, незалежно від того, потрапили вони туди за допомогою включення чи були у головному файлі.


Примітка редактора: У C ++ constоб'єкти, що не містять staticані externключових слів, ані ключових слів, не є неявними static.


Я фанат останнього речення, неймовірно корисний. Я не проголосував за відповідь, тому що 42 краще. редагувати: граматика
RealDeal_EE'18

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

4
@ Brent212 Компілятор не знає, чи надходила декларація / визначення з файлу заголовка чи основного файлу. Тож ви даремно сподіваєтесь. Буде дві копії VAL, якщо хтось був нерозумним і ввів статичне визначення у файл заголовка, і він потрапив у два джерела.
Justsalt

1
Значення const мають внутрішній зв'язок у C ++
adrianN

112

В staticі externвлучно на змінному файл в області видимості визначити , чи є вони доступні в інших одиницях трансляції (тобто інших .cабо .cppфайлів).

  • staticдає змінну внутрішню зв'язок, приховуючи її від інших модулів перекладу. Однак змінні з внутрішнім зв'язком можуть бути визначені в декількох одиницях перекладу.

  • externдає змінний зовнішній зв'язок, роблячи його видимим для інших підрозділів перекладу. Зазвичай це означає, що змінна повинна бути визначена лише в одній одиниці перекладу.

За замовчуванням (коли ви не вказуєте staticабо extern) є однією з тих областей, в яких C і C ++ відрізняються.

  • У C змінні з файловим діапазоном externза замовчуванням є (зовнішні зв'язки). Якщо ви використовуєте С, VALє staticі ANOTHER_VALє extern.

  • У C ++ файлові змінні staticза замовчуванням є (внутрішнє зв’язування), якщо вони є const, і externза замовчуванням, якщо ні. Якщо ви використовуєте С ++, обидва VALі ANOTHER_VALє static.

З проекту специфікації С :

6.2.2 Зв'язки ідентифікаторів ... -5- Якщо оголошення ідентифікатора для функції не має специфікатора класу сховища, його зв'язок визначається точно так, як якщо б він був оголошений із специфікатором класу сховища extern. Якщо оголошення ідентифікатора для об'єкта має обсяг файлу і не має специфікатора класу сховища, його зв'язок є зовнішнім.

З проекту специфікації C ++ :

7.1.1 - Специфікатори класу сховища [dcl.stc] ... -6- Ім'я, оголошене в області простору імен без специфікатора класу сховища, має зовнішнє зв’язування, якщо воно не має внутрішнього зв’язку через попередню декларацію та за умови, що воно не оголошена конст. Об'єкти, оголошені const і не явно оголошені extern, мають внутрішній зв'язок.


47

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

test.h:

static int TEST = 0;
void test();

test1.cpp:

#include <iostream>
#include "test.h"

int main(void) {
    std::cout << &TEST << std::endl;
    test();
}

test2.cpp:

#include <iostream>
#include "test.h"

void test() {
    std::cout << &TEST << std::endl;
}

Запуск цього дає вам такий результат:

0x446020
0x446040


5
Дякую за приклад!
Kyrol

Цікаво, якби вони TESTбули const, чи змогла б LTO оптимізувати її в єдине місце пам'яті. Але -O3 -fltoз GCC 8.1 ні.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Це було б незаконно, навіть якщо це постійно, статичні гарантії того, що кожен екземпляр є локальним для одиниці компіляції. Можливо, він міг би вбудувати саме значення константи, якщо його використовувати як константу, але оскільки ми беремо її адресу, він повинен повертати унікальний покажчик.
slicedlime

6

constзмінні в C ++ мають внутрішній зв'язок. Отже, використання staticне дає ефекту.

ах

const int i = 10;

one.cpp

#include "a.h"

func()
{
   cout << i;
}

two.cpp

#include "a.h"

func1()
{
   cout << i;
}

Якби це була програма C, ви отримали б помилку "множинного визначення" для i(через зовнішнє зв’язування).


2
Ну, використання staticмає наслідком, що воно акуратно сигналізує про наміри та усвідомлення того, що кодує, що ніколи не буває погано. Для мене це як у тому числі, virtualколи перевизначення: нам не потрібно, але все виглядає набагато інтуїтивніше - і узгоджується з іншими деклараціями - коли ми це робимо.
underscore_d

Ви могли б отримати помилку кілька визначень в C. Це невизначене поведінку, без діагностики потрібно
MM

5

Статичне оголошення на цьому рівні коду означає, що мітка змінної видно лише в поточному блоці компіляції. Це означає, що лише код у цьому модулі буде бачити цю змінну.

якщо у вас є файл заголовка, який оголошує змінну статичним, і цей заголовок включений до декількох файлів C / CPP, тоді ця змінна буде "локальною" для цих модулів. Буде N копій цієї змінної для N місць, що включають заголовок. Вони взагалі не пов’язані між собою. Будь-який код у будь-якому з цих вихідних файлів буде посилатися лише на змінну, яка оголошена в цьому модулі.

У цьому конкретному випадку ключове слово 'static', здається, не забезпечує жодної переваги. Можливо, мені чогось не вистачає, але, здається, це не має значення - я ще ніколи не бачив, щоб щось подібне робилося.

Щодо вбудовування, у цьому випадку змінна, швидше за все, вбудована, але це лише тому, що вона оголошена const. Компілятор може бути більш імовірно, вбудованими змінним модуль статичного, але це залежить від ситуації і код компілюється. Немає гарантії того, що компілятор вкладе "статику".


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

У цьому випадку, через const, staticмається на увазі і, отже, необов’язково. Висновок полягає в тому, що немає схильності до множинних помилок визначення, як стверджував Майк Ф.
underscore_d


2

Щоб відповісти на запитання, "чи означає статична статистика лише одну копію VAL, якщо заголовок включений кількома вихідними файлами?" ...

NO . VAL завжди визначатиметься окремо у кожному файлі, що містить заголовок.

Стандарти для C та C ++ у цьому випадку різняться.

У C змінні з файловим діапазоном за замовчуванням є зовнішніми. Якщо ви використовуєте C, VAL є статичним, а ANOTHER_VAL - зовнішнім.

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

У C ++ змінні файлового простору за замовчуванням є статичними, якщо вони є const, і зовнішніми за замовчуванням, якщо вони не є. Якщо ви використовуєте C ++, і VAL, і ANOTHER_VAL є статичними.

Також потрібно врахувати той факт, що обидві змінні позначені як const. В ідеалі компілятор завжди вибирає вбудовування цих змінних і не включає для них сховище. Існує ціла низка причин, чому можна виділити пам’ять. Я можу придумати ...

  • параметри налагодження
  • адреса, взята у файлі
  • компілятор завжди виділяє пам'ять (складні типи const неможливо легко вбудувати, тому стає особливим випадком для базових типів)

Примітка: В абстрактній машині є одна копія VAL у кожному окремому блоці перекладу, що включає заголовок. На практиці компоновщик може вирішити їх все одно поєднати, а компілятор може оптимізувати деякі або всі з них першими.
М.М.

1

Припускаючи, що ці декларації мають загальний обсяг (тобто не є змінними-членами), тоді:

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

Опускаючи статичну зв'язок є ехЬегпом за замовчуванням. Знову ж , ви були врятовані сопзЬ Несса - компілятор може оптимізувати / рядне використання. Якщо ви відкинете const, ви отримаєте багатозначно визначену помилку під час посилання.


Я вважаю, що компілятор повинен виділити простір для const int у всіх випадках, оскільки інший модуль завжди може сказати "extern const int whatever; something (& whatever);"

1

Ви не можете оголосити статичну змінну, не визначивши її також (це тому, що модифікатори класу сховища static і extern взаємовиключні). Статичну змінну можна визначити у файлі заголовка, але це призведе до того, що кожен вихідний файл, що включає файл заголовка, має власну приватну копію змінної, що, мабуть, не те, що було призначено.


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

1

Змінні const за замовчуванням статичні в C ++, але зовнішні C. Тому, якщо ви використовуєте C ++, це не має сенсу, яку конструкцію використовувати.

(7.11.6 C ++ 2003, і ​​Apexndix C має зразки)

Приклад у порівнянні джерел компіляції / посилань як програми C та C ++:

bruziuz:~/test$ cat a.c
const int b = 22;
int main(){return 0;}
bruziuz:~/test$ cat b.c
const int b=2;
bruziuz:~/test$ gcc -x c -std=c89 a.c b.c
/tmp/ccSKKIRZ.o:(.rodata+0x0): multiple definition of `b'
/tmp/ccDSd0V3.o:(.rodata+0x0): first defined here
collect2: error: ld returned 1 exit status
bruziuz:~/test$ gcc -x c++ -std=c++03 a.c b.c 
bruziuz:~/test$ 
bruziuz:~/test$ gcc --version | head -n1
gcc (Ubuntu 5.4.0-6ubuntu1~16.04.5) 5.4.0 20160609

Там є сенс ще в тому числі static. Це сигналізує про наміри / обізнаність щодо того, що робить програміст, і підтримує паритет з іншими типами оголошень (і, fwiw, C), які не мають неявного static. Це все одно, що включати virtualта останнім часом overrideу декларації про перевизначення функцій - не потрібно, але набагато більше самодокументування і, у випадку останнього, сприяє статичному аналізу.
underscore_d

Я абсолютно згоден. наприклад, що стосується мене в реальному житті, я завжди пишу це явно.
bruziuz

"Отже, якщо ви використовуєте C ++, немає сенсу, яку конструкцію використовувати ..." - Хм ... Я щойно скомпілював проект, який використовував constлише змінну в заголовку з g++ (GCC) 7.2.1 20170915 (Red Hat 7.2.1-2). Це призвело до приблизно 150 багатозначних символів (по одному для кожної одиниці перекладу був включений заголовок). Я думаю , що нам потрібні або static, inlineабо анонімні / неназвані імена , щоб уникнути зовнішніх зв'язків.
jww

Я спробував baby-example з gcc-5.4 з декларуванням const intвсередині простору імен та в глобальному просторі імен. І він скомпільований і дотримується правила "Об'єкти, оголошені const, а не явно оголошені extern, мають внутрішні зв'язки." ".... Можливо, у проекті з якихось причин цей заголовок включений до скомпільованих джерел C, де правила зовсім інші.
bruziuz

@jww Я завантажив приклад із проблемою зв’язку для C та без проблем для C ++
bruziuz

0

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

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


-2

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


припускаючи, що під "бібліотеками" ви маєте на увазі одиниці перекладу , тоді ні, включення-охоронці абсолютно нічого не роблять для запобігання множинним визначенням, оскільки вони захищають лише від повторних включень всередині однієї одиниці перекладу . отже, вони нічого не роблять, щоб зробити static"менш важливим". і навіть з обома, ви можете отримати кілька внутрішньо пов'язаних визначень, які, ймовірно, не призначені.
underscore_d
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.