Як ініціалізувати приватні статичні члени в C ++?


519

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

class foo
{
    private:
        static int i;
};

int foo::i = 0;

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


2
Привіт, Джейсон. Я не знайшов коментар до ініціалізації за замовчуванням статичних членів (особливо інтегральних). Насправді вам потрібно написати int foo :: i, щоб лінкер міг його знайти, але він буде ініціалізований автоматично з 0! Цього рядка було б достатньо: int foo :: i; (Це справедливо для всіх об'єктів, що зберігаються в статичній пам'яті; лінкер відповідає за ініціалізацію статичних об'єктів.)
Ніко

1
Наведені нижче відповіді не стосуються шаблонного класу. Вони кажуть: ініціалізація повинна перейти у вихідний файл. Для шаблонного класу це не можливо і не потрібно.
Йоахім Ш

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

Відповіді:


556

Декларація класу повинна бути у файлі заголовка (Або у вихідному файлі, якщо він не наданий).
Файл: foo.h

class foo
{
    private:
        static int i;
};

Але ініціалізація повинна бути у вихідному файлі.
Файл: foo.cpp

int foo::i = 0;

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

Примітка: Метт Кертіс: вказує, що C ++ дозволяє спростити вище , якщо статична змінна - член має сопзЬ типу Int (наприклад int, bool, char). Потім можна оголосити та ініціалізувати змінну члена безпосередньо всередині декларації класу у файлі заголовка:

class foo
{
    private:
        static int const i = 42;
};

4
Так. Але я припускаю, що питання було спрощено. Технічно декларація та визначення можуть бути в одному вихідному файлі. Але це обмежує використання класу іншими класами.
Мартін Йорк

11
насправді не лише POD, він також повинен бути типом int (int, short, bool, char ...)
Метт Кертіс

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

5
@Martin: крім виправлення s / POD / інтегральний тип /, якщо адреса коли-небудь взята, тоді також має бути визначення. Як би це не звучало, декларація з ініціалізатором у визначенні класу - це не визначення. Шаблонний Const ідіома забезпечує обхідний шлях для тих випадків , коли вам необхідно визначення в файлі заголовка. Інший і простіший спосіб вирішення - це функція, яка виробляє значення локальної статичної константи. Cheers & hth.,
Ура та хт. - Альф

3
Ви можете додати уточнення, що int foo :: i = 0; не повинно бути всередині функції (включаючи основну функцію). У мене це було на початку моєї основної функції, і це не подобається.
qwerty9967

89

Для змінної :

foo.h:

class foo
{
private:
    static int i;
};

foo.cpp:

int foo::i = 0;

Це тому, що може бути лише один екземпляр foo::i у вашій програмі . Це свого роду еквівалент extern int iу файлі заголовка та int iу вихідному файлі.

Для константи можна вказати значення прямо в декларації класу:

class foo
{
private:
    static int i;
    const static int a = 42;
};

2
Це дійсний пункт. Я додам це теж своє пояснення. Але слід зазначити, що це працює лише для типів POD.
Мартін Йорк

З того часу, коли C ++ дозволяє бути просто хорошим з декларуванням у класі та без визначення цілісних типів. Оскільки C ++ 98 сам або C ++ 03 або коли? Будь ласка, поділіться будь ласка автентичними посиланнями. Стандартне формулювання C ++ не синхронізується з компіляторами. Вони згадують, що член все одно буде визначений, якщо вони використовуються. Отже, мені не потрібно стандартного цитування C ++
smRaj

1
Цікаво, чому privateзмінні можна ініціалізувати за межами Класу тут, чи можна це зробити і для нестатичних змінних.
Крішна Оза

Ви знайшли пояснення? @Krishna_Oza
nn0p

@ nn0p ще немає, але ініціалізація нестатичних приватних змінних зовні Classне має сенсу в Cpp.
Кришна Оза

41

Оскільки C ++ 17, статичні члени можуть бути визначені в заголовку з вбудованим ключовим словом.

http://en.cppreference.com/w/cpp/language/static

"Статичний член даних може бути оголошений вбудованим. Статичний член даних вбудований може бути визначений у визначенні класу і може вказати ініціалізатор члена за замовчуванням. Для цього не потрібно визначення поза класу:"

struct X
{
    inline static int n = 1;
};

1
Це можливо, оскільки C ++ 17, який зараз перебуває у стані нового стандарту.
Гребу

31

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

Файли заголовків призначені для декларацій.

Файли заголовків збираються один раз для кожного .cppфайлу, який безпосередньо або опосередковано #includesїх, а код поза будь-якою функцією запускається при ініціалізації програми, перш ніжmain() .

Поклавши: foo::i = VALUE;у заголовок, foo:iбуде присвоєно значення VALUE(що б там не було) для кожного .cppфайлу, і ці призначення будуть відбуватися у невизначеному порядку (визначеному лінкером) перед main()запуском.

Що робити, якщо ми маємо #define VALUEінше число в одному з наших .cppфайлів? Це складеться чудово, і ми не зможемо знати, хто з них виграє, поки ми не запустимо програму.

Ніколи не виконуваний код в заголовок з тієї ж причини , що ви ніколи #includeв .cppфайл.

включіть охоронці (які, я погоджуюся, ви завжди повинні використовувати) захищають вас від чогось іншого: той самий заголовок непрямо #included кілька разів під час компіляції одного .cppфайлу


2
Ви маєте рацію щодо цього, звичайно, за винятком шаблону класу (про який не питають, але я маю справу з багатьма). Отже, якщо клас є повністю визначеним і не є шаблоном класу, тоді помістіть ці статичні члени в окремий файл CPP, але для шаблонів класів визначення має бути в одному блоці перекладу (наприклад, файл заголовка).
monkey0506

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

publib.boulder.ibm.com/infocenter/macxhelp/v6v81/… Це посилання зображує екземпляри статичних членів шаблону в головній функції, яка є чистішою, якщо це трохи тягаря.
Джошуа Клейтон

1
Ваш аргумент дійсно величезний. Спочатку ви не можете #define VALUE, оскільки ім'я макросів має бути дійсним ідентифікатором. І навіть якби ти міг - хто це зробив би? Файли заголовків призначені для декларації -? Давайте .. Єдиний випадок, коли вам слід уникати введення значень у заголовку, - це боротьба з odr-used. І введення значення в заголовок може призвести до зайвої перекомпіляції, коли вам потрібно змінити значення.
Олександр Фулар

20

Використовуючи компілятор Microsoft [1], статичні змінні, які не є intподібними, також можна визначити у файлі заголовка, але поза межами декларації класу, використовуючи специфіку Microsoft __declspec(selectany).

class A
{
    static B b;
}

__declspec(selectany) A::b;

Зауважте, що я не кажу, що це добре, я просто кажу, що це можна зробити.

[1] У ці дні підтримується більше компіляторів, ніж MSC __declspec(selectany)- принаймні gcc та clang. Можливо, навіть більше.


17
int foo::i = 0; 

Це правильний синтаксис для ініціалізації змінної, але він повинен міститись у вихідному файлі (.cpp), а не в заголовку.

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


12

У мене тут недостатньо респондентів, щоб додати це як коментар, але IMO - це гарний стиль писати заголовки разом із #include guards , що, як зазначив Paranaix кілька годин тому, запобігло б помилку множинного визначення. Якщо ви вже не використовуєте окремий файл CPP, не потрібно використовувати його лише для ініціалізації статичних неінтегральних членів.

#ifndef FOO_H
#define FOO_H
#include "bar.h"

class foo
{
private:
    static bar i;
};

bar foo::i = VALUE;
#endif

Мені не потрібно використовувати для цього окремий файл CPP. Звичайно, ви можете, але немає технічних причин, чому вам доведеться це робити.


21
#include охоронці просто запобігають кілька визначень на одиницю перекладу.
Пол Фульц II

3
щодо гарного стилю: ви можете додати коментар до завершального завершення:#endif // FOO_H
Рига,

9
Це працює лише в тому випадку, якщо у вас є лише один компілюючий блок, який включає foo.h. Якщо два або більше cpps включають foo.h, що є типовою ситуацією, кожен cpp оголошує одну і ту ж статичну змінну, щоб лінкер скаржився на багаторазове визначення `foo :: i ', якщо ви не використовуєте компіляцію пакета з файлами (компілювати лише один файл, що включає всі cpps). Але хоча компіляція пакетів є чудовим рішенням проблеми - оголосити (int foo :: i = 0;) у cpp!
Alejadro Xalabarder

1
Або просто використовувати#pragma once
тамбр

12

Якщо ви хочете ініціалізувати певний тип з'єднання (fe string), ви можете зробити щось подібне:

class SomeClass {
  static std::list<string> _list;

  public:
    static const std::list<string>& getList() {
      struct Initializer {
         Initializer() {
           // Here you may want to put mutex
           _list.push_back("FIRST");
           _list.push_back("SECOND");
           ....
         }
      }
      static Initializer ListInitializationGuard;
      return _list;
    }
};

Оскільки ListInitializationGuardстатична змінна всередині SomeClass::getList()методу, вона буде побудована лише один раз, а це означає, що конструктор викликається один раз. Це initialize _listзмінює значення, яке вам потрібно. Будь-який наступний виклик до getListпросто поверне вже ініціалізований _listоб'єкт.

Звичайно, ви завжди маєте доступ до _listоб'єкта за допомогою getList()методу виклику .


1
Ось версія цієї ідіоми , яка не вимагає створення одного методу для кожного об'єкта члена: stackoverflow.com/a/48337288/895245
Чіро Сантіллі郝海东冠状病六四事件法轮功

9

C ++ 11 статичний шаблон конструктора, який працює для декількох об'єктів

Одна ідіома була запропонована за адресою: https://stackoverflow.com/a/27088552/895245, але тут виходить більш чиста версія, яка не потребує створення нового методу на одного члена.

main.cpp

#include <cassert>
#include <vector>

// Normally on the .hpp file.
class MyClass {
public:
    static std::vector<int> v, v2;
    static struct StaticConstructor {
        StaticConstructor() {
            v.push_back(1);
            v.push_back(2);
            v2.push_back(3);
            v2.push_back(4);
        }
    } _staticConstructor;
};

// Normally on the .cpp file.
std::vector<int> MyClass::v;
std::vector<int> MyClass::v2;
// Must come after every static member.
MyClass::StaticConstructor MyClass::_staticConstructor;

int main() {
    assert(MyClass::v[0] == 1);
    assert(MyClass::v[1] == 2);
    assert(MyClass::v2[0] == 3);
    assert(MyClass::v2[1] == 4);
}

GitHub вище за течією .

Складіть і запустіть:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

Дивіться також: статичні конструктори в C ++? Мені потрібно ініціалізувати приватні статичні об’єкти

Тестовано на Ubuntu 19.04.

C ++ 17 вбудована змінна

Згадується за адресою: https://stackoverflow.com/a/45062055/895245, але ось приклад, який можна виконати з декількох файлів, щоб зробити його ще зрозумілішим: як працюють вбудовані змінні?


5

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

class Foo
   {
   public:
     int GetMyStatic() const
     {
       return *MyStatic();
     }

   private:
     static int* MyStatic()
     {
       static int mStatic = 0;
       return &mStatic;
     }
   }

Вищевказаний код має "бонус" не вимагати файлу CPP / джерела. Знову ж таки метод, який я використовую для моїх бібліотек C ++.


4

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

#include <stdio.h>

class Foo
{
   public:

     int   GetMyStaticValue () const {  return MyStatic();  }
     int & GetMyStaticVar ()         {  return MyStatic();  }
     static bool isMyStatic (int & num) {  return & num == & MyStatic(); }

   private:

      static int & MyStatic ()
      {
         static int mStatic = 7;
         return mStatic;
      }
};

int main (int, char **)
{
   Foo obj;

   printf ("mystatic value %d\n", obj.GetMyStaticValue());
   obj.GetMyStaticVar () = 3;
   printf ("mystatic value %d\n", obj.GetMyStaticValue());

   int valMyS = obj.GetMyStaticVar ();
   int & iPtr1 = obj.GetMyStaticVar ();
   int & iPtr2 = valMyS;

   printf ("is my static %d %d\n", Foo::isMyStatic(iPtr1), Foo::isMyStatic(iPtr2));
}

це виводи

mystatic value 7
mystatic value 3
is my static 1 0

3

Також працює у файлі privateStatic.cpp:

#include <iostream>

using namespace std;

class A
{
private:
  static int v;
};

int A::v = 10; // possible initializing

int main()
{
A a;
//cout << A::v << endl; // no access because of private scope
return 0;
}

// g++ privateStatic.cpp -o privateStatic && ./privateStatic

3

Що з set_default()методом?

class foo
{
    public:
        static void set_default(int);
    private:
        static int i;
};

void foo::set_default(int x) {
    i = x;
}

Нам залишилося б тільки скористатися set_default(int x)методом, і наша staticзмінна буде ініціалізована.

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


3

Проблема з лінкером, з якою ви стикалися, ймовірно, спричинена:

  • Надаючи визначення класу та статичного члена у файлі заголовка,
  • Включення цього заголовка у два або більше вихідних файлів.

Це поширена проблема для тих, хто починає з C ++. Член статичного класу повинен бути ініціалізований в одному блоці перекладу, тобто в одному вихідному файлі.

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

class Foo
{
    // int& getObjectInstance() const {
    static int& getObjectInstance() {
        static int object;
        return object;
    }

    void func() {
        int &object = getValueInstance();
        object += 5;
    }
};

1
Я досі є повноцінним n00b на C ++, але це виглядає геніально, мені дуже дякую! Я отримую ідеальне управління життєвим циклом об'єкта "одиночний" безкоштовно.
Рафаель Кітовер

2

Один із способів визначення констант «старої школи» - це замінити їх на enum:

class foo
{
    private:
        enum {i = 0}; // default type = int
        enum: int64_t {HUGE = 1000000000000}; // may specify another type
};

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


1

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

Мені потрібно було ініціалізувати приватний статичний член даних у шаблоновому класі.

в .h або .hpp, виглядає приблизно так, щоб ініціалізувати статичний член даних класу шаблонів:

template<typename T>
Type ClassName<T>::dataMemberName = initialValue;

0

Це відповідає вашим цілям?

//header file

struct MyStruct {
public:
    const std::unordered_map<std::string, uint32_t> str_to_int{
        { "a", 1 },
        { "b", 2 },
        ...
        { "z", 26 }
    };
    const std::unordered_map<int , std::string> int_to_str{
        { 1, "a" },
        { 2, "b" },
        ...
        { 26, "z" }
    };
    std::string some_string = "justanotherstring";  
    uint32_t some_int = 42;

    static MyStruct & Singleton() {
        static MyStruct instance;
        return instance;
    }
private:
    MyStruct() {};
};

//Usage in cpp file
int main(){
    std::cout<<MyStruct::Singleton().some_string<<std::endl;
    std::cout<<MyStruct::Singleton().some_int<<std::endl;
    return 0;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.