Чому статичні члени даних повинні визначатися поза класом окремо в C ++ (на відміну від Java)?


41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

Я не бачу необхідності A::xвизначати окремо у .cpp-файлі (або в тому самому файлі для шаблонів). Чому не можна одночасно A::xоголошувати та визначати?

Чи заборонено це з історичних причин?

Моє головне питання, чи вплине це на будь-яку функціональність, якщо staticчлени даних були декларовані / визначені одночасно (так само, як і Java )?


Як найкраща практика, як правило, краще обернути статичну змінну статичним методом (можливо, як локальний статичний), щоб уникнути проблем з порядком ініціалізації.
Tamás Szelei

2
Це правило насправді трохи послаблене в C ++ 11. Статичних членів const зазвичай більше не потрібно визначати. Дивіться: en.wikipedia.org/wiki/…
mirk

4
@afishwhoswimsaround: Вказання на узагальнені правила для всіх ситуацій не є хорошою ідеєю (найкращі практики слід застосовувати з контекстом). Тут ви намагаєтеся вирішити проблему, яка не існує. Проблема порядку ініціалізації стосується лише об'єктів, у яких є конструктори та доступ до інших об'єктів статичної тривалості зберігання. Оскільки 'x' є int, перше не застосовується, оскільки 'x' є приватним, друге не застосовується. По-третє, це не має нічого спільного з питанням.
Мартін Йорк

1
Належить до переповнення стека?
Гонки легкості з Монікою

2
С ++ 17 дозволяє інлайн ініціалізації статичних елементів даних (навіть для нецілих типів): inline static int x[] = {1, 2, 3};. Дивіться en.cppreference.com/w/cpp/language/static#Static_data_members
Володимир Решетніков

Відповіді:


15

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

Модель компіляції C ++ походить від моделі C, в яку ви імпортуєте декларації у вихідний файл, включаючи (заголовок) файли. Таким чином компілятор рекурсивно бачить рівно один великий вихідний файл, що містить усі включені файли та всі файли, що входять у ці файли. Це має одну велику перевагу IMO, а саме те, що він робить компілятор простішим у виконанні. Звичайно, ви можете написати що-небудь у включені файли, тобто як декларації, так і визначення. Поставляти декларації у заголовкові файли та визначення у файли .c або .cpp лише корисною практикою.

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

Наприклад, у GNU Pascal ви можете написати одиницю aу такий файл a.pas:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

де глобальна змінна оголошується та ініціалізується в тому самому вихідному файлі.

Тоді ви можете мати різні одиниці, які імпортують a та використовують глобальну змінну MyStaticVariable, наприклад одиницю b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

і одиниця c ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Нарешті, ви можете використовувати одиниці b і c в основній програмі m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Ви можете скласти ці файли окремо:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

а потім створити виконуваний файл із:

$ gpc -o m m.o a.o b.o c.o

і запустіть його:

$ ./m
1
2
3

Примітка тут полягає в тому, що коли компілятор бачить директиву використання в програмному модулі (наприклад, використовує b.pas), він не включає відповідний файл .pas, а шукає .gpi-файл, тобто попередньо скомпільований файл інтерфейсу (див . документацію ). Ці .gpiфайли генеруються компілятором разом з .oфайлами при складанні кожного модуля. Отже глобальний символ MyStaticVariableвизначається лише один раз у об’єктному файлі a.o.

Java працює аналогічно: коли компілятор імпортує клас A до класу B, він переглядає файл класу для A і не потребує цього файлу A.java. Таким чином, усі визначення та ініціалізації для класу A можна помістити в один вихідний файл.

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


42

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

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

Так, це хоча б частково історична проблема, що датується з фронту; може бути записаний компілятор, який створить свого роду прихований "static_members_of_everything.cpp" і посилання на нього. Однак це порушить зворотну сумісність, і реальної користі від цього не буде.


2
Моє запитання - не причина теперішньої поведінки, а скоріше виправдання такої граматики мови. Іншими словами, припустимо, якщо staticзмінні оголошуються / визначаються там же (як Java), то що може піти не так?
iammilind

8
@iammilind Я думаю, ви не розумієте, що граматика необхідна через пояснення цієї відповіді. Тепер чому? Через модель компіляції C (і C ++): файли c і cpp - це файл реального коду, який компілюється окремо, як окремі програми, потім вони пов'язані між собою, щоб зробити повний виконуваний файл. Заголовки насправді не є кодом для компілятора, вони є лише текстом для копіювання та вставки всередині файлів c та cpp. Тепер, якщо щось визначено кілька разів, воно не може скомпілювати, так само, як і у вас, якщо у вас є кілька локальних змінних з тим самим іменем.
Клаїм

1
@Klaim, а що з staticчленами template? Вони дозволені у всіх файлах заголовків, оскільки вони повинні бути видимими. Я не заперечую цієї відповіді, але це також не відповідає моєму питанню.
iammilind

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

2
@iammilind: Шаблони зазвичай інстанціюються у кожному об'єктному файлі, включаючи їх статичні змінні. У Linux з об’єктними файлами ELF компілятор позначає інстанції як слабкі символи , це означає, що лінкер поєднує в собі кілька копій однієї і тієї ж інстанції. Ця ж технологія може бути використана для визначення статичних змінних у заголовкових файлах, тому причина цього не робиться, ймовірно, поєднання історичних причин та міркувань щодо компіляції. Будемо сподіватися, що вся модель компіляції буде виправлена, коли наступний стандарт C ++ включає модулі .
Хан

6

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

Декларація класу (яка називається декларацією з поважних причин) потрапляє в декілька одиниць перекладу. Якби декларація містила визначення для статичних змінних, то в кінцевому підсумку ви отримаєте кілька визначень у кількох одиницях перекладу (І пам’ятайте, ці назви мають зовнішню зв'язок.)

Така ситуація можлива, але вимагає, щоб лінкер обробляв декілька визначень без нарікань.

(І зауважте, що це суперечить Правилу одного визначення, якщо це не можна зробити відповідно до виду символу чи того, який розділ він розміщений.)


6

Існує велика різниця між C ++ та Java.

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

У C ++ немає "кінцевого власника знань": C ++, C, Fortran Pascal тощо - всі "перекладачі" з вихідного коду (файлу CPP) у проміжний формат (файл OBJ або файл ".o", залежно від ОС), де оператори переводяться в машинні інструкції, а імена стають непрямими адресами, опосередкованими таблицею символів.

Програму створює не компілятор, а інша програма ("лінкер"), яка об'єднує всі OBJ-ті разом (незалежно від мови, з якої вони походять), повторно вказуючи всі адреси, що спрямовані на символи, на їх ефективне визначення.

До речі, як працює лінкер, визначення (що створює фізичний простір для змінної) повинно бути унікальним.

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

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

Компілятор не знає (і ніколи не дізнається), чи всі ці OBJ коли-небудь будуть використовуватися разом для формування єдиної програми або окремо для формування різних незалежних програм.

Лінкер не знає, як і чому існують визначення та звідки вони беруться (він навіть не знає про C ++: кожен "статичний мова" може давати визначення та посилання, які повинні бути пов'язані). Він просто знає, що є посилання на певний "символ", який "визначений" за заданою адресою.

Якщо для даного символу існує декілька визначень (не плутайте визначення з посиланнями), лінкер не знає (як агностик мови), що робити з ними.

Це як об'єднання декількох міст, щоб утворити велике місто: якщо у вас є два " Часові площі " та кількість людей, які приходять ззовні, просять перейти до " Часової площі ", ви не можете визначитися з чисто технічною базою (без жодних знань про політику, яка присвоїла ці імена та буде відповідальна за управління ними), куди саме їх відправити.


3
Різниця між Java та C ++ щодо глобальних символів пов'язана не з тим, що Java має віртуальну машину, а скоріше з моделлю компіляції C ++. У цьому відношенні я не ставлю Pascal і C ++ до однієї категорії. Швидше я б згрупував C і C ++ разом як "мови, на яких імпортовані декларації включаються та компілюються разом з основним вихідним файлом", на відміну від Java та Pascal (а може бути, OCaml, Scala, Ada тощо) як "мови, на яких імпортовані декларації шукають компілятор у попередньо складених файлах, що містять інформацію про експортовані символи ".
Джорджіо

1
@Giorgio: посилання на Java може бути не вітається, але я вважаю, що відповідь Еміліо здебільшого правдивий, переходячи до суті питання, а саме до фази об'єктних файлів / посилань після роздільної компіляції.
ixache

5

Це потрібно, тому що інакше компілятор не знає, куди поставити змінну. Кожен файл cpp складається окремо і не знає іншого. Лінкер розв'язує змінні, функції тощо. Я особисто не бачу, в чому різниця між vtable та статичними членами (нам не потрібно вибирати, у якому файлі визначається vtable).

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


2

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

До C ++ 11 ініціалізація класу в C ++ не була дозволена. Тому не можна писати так:

struct X
{
  static int i = 4;
};

Отже, щоб ініціалізувати змінну, слід записати її поза класом у вигляді:

struct X
{
  static int i;
};
int X::i = 4;

Як обговорювалося і в інших відповідях, int X::iтепер глобальна ситуація, і декларація глобальною у багатьох файлах спричиняє помилки декількох символів.

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

static int X::i = 4;
^^^^^^

0

A :: x - це просто глобальна змінна, але простір імен на A та обмеження доступу.

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

Я б назвав це все поганим дизайном, але є кілька функцій, якими ви можете скористатися таким чином:

  1. порядок виклику конструктора ... Не важливо для int, але для більш складного члена, який, можливо, отримує доступ до інших статичних чи глобальних змінних, це може бути критично важливим.

  2. статичний ініціалізатор - ви можете дозволити клієнту вирішити, до чого слід ініціалізувати A :: x.

  3. в c ++ і c, оскільки ви маєте повний доступ до пам'яті через покажчики, фізичне розташування змінних є важливим. Є дуже неслухняні речі, якими ви можете скористатися залежно від того, де змінна розташована в об’єкті посилання.

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


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