Чому покажчики за замовчуванням не ініціалізуються з NULL?


118

Чи може хтось пояснити, чому покажчики не ініціалізуються NULL?
Приклад:

  void test(){
     char *buf;
     if (!buf)
        // whatever
  }

Програма не входитиме всередину if, тому що bufце не нуль.

Мені хотілося б знати, чому в будь-якому випадку нам потрібна змінна із кошиком, спеціально вказівники, що адресують кошик на пам'яті?


13
Ну, тому що фундаментальні типи залишаються неініціалізованими. Тож я припускаю ваше "справжнє" питання: чому не ініціалізуються основні типи?
GManNickG

11
"програма не буде входити всередину if, тому що buf не є нульовим". Це не правильно. Так як ви не знаєте , що ЬеЕ це , ви не можете знати , що це НЕ .
Дрю Дорман

На відміну від чогось на зразок Java, C ++ дає тонни більше відповідальності перед розробником.
Ріші

цілі числа, покажчики, за замовчуванням 0, якщо ви використовуєте () конструктор.
Ерік Аронесті

Через припущення, що хтось, що використовує C ++, знає, що вони роблять, більше того, хто використовує необроблені покажчики над розумним покажчиком, знає (тим більше) що вони роблять!
Піднесений Лев

Відповіді:


161

Усі ми розуміємо, що вказівник (та інші типи POD) слід ініціалізувати.
Тоді стає питання "хто повинен їх ініціалізувати".

Ну є в основному два методи:

  • Компілятор ініціалізує їх.
  • Розробник ініціалізує їх.

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

Отже, у нас виникла ситуація, що компілятор додав до коду додаткову інструкцію, яка ініціалізує змінну до NULL, потім пізніше додається код розробника, щоб зробити правильну ініціалізацію. Або за інших умов змінна потенційно ніколи не використовується. Багато розробників на C ++ прикриваються при обох умовах ціною додаткової інструкції.

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

АЛЕ : Ви можете імітувати ефект примусової ініціалізації. Більшість компіляторів попередить вас про неініціалізовані змінні. Тому я завжди перетворюю свій попереджувальний рівень на найвищий можливий рівень. Потім скажіть компілятору, щоб всі попередження розглядалися як помилки. За цих умов більшість компіляторів буде генерувати помилку для змінних, які не ініціалізуються, але використовуються, і, таким чином, не дозволять створювати код.


5
Боб Табор сказав: "Занадто багато людей не придумали ініціалізації!" Його "дружньо" автоматично ініціалізувати всі змінні, але це потребує часу, а повільні програми "непривітні". Лист або редактори, які показали випадковий знайдений смітник, були б неприйнятними. C, гострий інструмент для навчених користувачів (небезпечний при неправильному використанні) не повинен займати час ініціалізації автоматичних змінних. Макрос навчального колеса для init змінних може бути, але багато хто думає, що краще встати, запам’ятати і трохи кровоточити. Ви в дрібниці працюєте так, як практикуєте. Тож практикуйтеся так, як вам би це було.
Білл IV

2
Ви були б здивовані тим, скільки помилок уникне лише той, хто виправить усю ініціалізацію. Це було б виснажливою роботою, якби не попередження компілятора.
Джонатан Хенсон

4
@ Локі, мені важко слідувати твоїй думці. Я просто намагався оцінити вашу відповідь як корисну, сподіваюся, ви це зібрали. Якщо ні, то мені шкода.
Джонатан Хенсон

3
Якщо вказівник спочатку встановлено на NULL, а потім встановить будь-яке значення, компілятор повинен мати можливість виявити це та оптимізувати першу ініціалізацію NULL, правда?
Корчкіду

1
@Korchkidu: Іноді. Однією з найважливіших проблем є те, що немає способу попередити вас про те, що ви забули зробити ініціалізацію, оскільки він не може знати, що за замовчуванням не ідеально підходить для вашого використання.
Дедуплікатор

41

Цитуючи Bjarne Stroustrup в TC ++ PL (Special Edition p.22):

Реалізація функції не повинна накладати значні накладні витрати на програми, які цього не потребують.


і не дайте можливість. Здається
Джонатан

8
@ Jonathan ніщо не заважає вам ініціалізувати покажчик на null - або на 0, як це стандартно в C ++.
stefanB

8
Так, але Stroustrup міг зробити так, щоб синтаксис сприяв правильності програми, а не продуктивності, ініціалізуючи нуль вказівника, і змусити програміста явно вимагати неініціалізації вказівника. Зрештою, більшість людей віддають перевагу правильному, але повільному над швидким, але неправильним, на тій підставі, що оптимізувати невелику кількість коду, як правило, простіше, ніж виправляти помилки у всій програмі. Особливо, коли багато чого може зробити гідний компілятор.
Роберт Так

1
Це не порушує сумісність. Ідея розглядалася спільно з "int * x = __uninitialized" - безпека за замовчуванням, швидкість за наміром.
MSalters

4
Мені подобається, що Dробить. Якщо ви не хочете ініціалізації, використовуйте цей синтаксис float f = void;або int* ptr = void;. Тепер він ініціалізується за замовчуванням, але якщо вам це справді потрібно, ви можете зупинити компілятор від цього.
deft_code

23

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

int * p = & some_int;

або:

int * p = 0;

або:

class A {
   public:
     A() : p( 0 ) {}  // initialise via constructor
   private:
     int * p;
};

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

1
Ви ініціалізуєте членів класу в конструкторі класу - так працює C ++.

3
@Jonathan: але null також є сміттям. З нульовим вказівником ви не можете зробити нічого корисного. Перенаправлення - це стільки ж помилка. Створюйте покажчики з належними значеннями, а не нулями.
DrPizza

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

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

20

Тому що одним із девізів C ++ є:


Ви не платите за те, що не використовуєте


Саме з цієї причини operator[] в vectorкласі не перевіряє , якщо індекс виходить за межі, наприклад.


12

З історичних причин, головним чином, тому, що це робиться в C. Чому це робиться так у C, це інше питання, але я думаю, що принцип нульових накладних витрат якимось чином був задіяний у цьому проектному рішенні.


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

8

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

Ви компілюєте з попередженнями, правда?


І це можливо, як підтвердження, що трасування компіляторів може бути несправним.
Дедуплікатор

6

Є надзвичайно мало ситуацій, коли коли-небудь має сенс змінна бути неініціалізованою, а ініціалізація за замовчуванням має невелику вартість, то чому це робити?

C ++ не є C89. Чорт, навіть C не C89. Ви можете змішувати декларації та код, тому вам слід відкладати декларацію до тих пір, поки у вас є відповідне значення для ініціалізації.


2
Тоді просто кожне значення потрібно буде записати двічі - один раз за допомогою програми настройки компілятора і знову за допомогою програми користувача. Зазвичай це не велика проблема, але вона накопичується (наприклад, якщо ви створюєте масив з 1 мільйона елементів). Якщо ви хочете автоматичної ініціалізації, ви завжди можете створити власні типи, які роблять це; але таким чином ви не змушені приймати зайві накладні витрати, якщо цього не хочете.
Джеремі Фріснер

3

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

char* pBuf;
if (condition)
{
    pBuf = new char[50];
}
else
{
    pBuf = m_myMember->buf();
}

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


1
з іншого боку, ви можете зробити char * pBuf = умова? новий char [50]: m_myMember-> buf (); Це більше схоже на синтаксис, ніж на ефективність, але я все-таки згоден з вами.
the_drow

1
@the_drow: Ну, можна зробити це складнішим, тому таке переписування неможливо.
Дедуплікатор

2

Якщо ви хочете, щоб вказівник, який завжди ініціалізується на NULL, ви можете використовувати шаблон C ++, щоб імітувати цю функціональність:

template<typename T> class InitializedPointer
{
public:
    typedef T       TObj;
    typedef TObj    *PObj;
protected:
    PObj        m_pPointer;

public:
    // Constructors / Destructor
    inline InitializedPointer() { m_pPointer=0; }
    inline InitializedPointer(PObj InPointer) { m_pPointer = InPointer; }
    inline InitializedPointer(const InitializedPointer& oCopy)
    { m_pPointer = oCopy.m_pPointer; }
    inline ~InitializedPointer() { m_pPointer=0; }

    inline PObj GetPointer() const  { return (m_pPointer); }
    inline void SetPointer(PObj InPtr)  { m_pPointer = InPtr; }

    // Operator Overloads
    inline InitializedPointer& operator = (PObj InPtr)
    { SetPointer(InPtr); return(*this); }
    inline InitializedPointer& operator = (const InitializedPointer& InPtr)
    { SetPointer(InPtr.m_pPointer); return(*this); }
    inline PObj operator ->() const { return (m_pPointer); }
    inline TObj &operator *() const { return (*m_pPointer); }

    inline bool operator!=(PObj pOther) const
    { return(m_pPointer!=pOther); }
    inline bool operator==(PObj pOther) const
    { return(m_pPointer==pOther); }
    inline bool operator!=(const InitializedPointer& InPtr) const
    { return(m_pPointer!=InPtr.m_pPointer); }
    inline bool operator==(const InitializedPointer& InPtr) const
    { return(m_pPointer==InPtr.m_pPointer); }

    inline bool operator<=(PObj pOther) const
    { return(m_pPointer<=pOther); }
    inline bool operator>=(PObj pOther) const
    { return(m_pPointer>=pOther); }
    inline bool operator<=(const InitializedPointer& InPtr) const
    { return(m_pPointer<=InPtr.m_pPointer); }
    inline bool operator>=(const InitializedPointer& InPtr) const
    { return(m_pPointer>=InPtr.m_pPointer); }

    inline bool operator<(PObj pOther) const
    { return(m_pPointer<pOther); }
    inline bool operator>(PObj pOther) const
    { return(m_pPointer>pOther); }
    inline bool operator<(const InitializedPointer& InPtr) const
    { return(m_pPointer<InPtr.m_pPointer); }
    inline bool operator>(const InitializedPointer& InPtr) const
    { return(m_pPointer>InPtr.m_pPointer); }
};

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

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

ОК додано всіх операторів порівняння. Заміна за замовчуванням може бути зайвою, але вони тут явно, оскільки це приклад.
Адісак

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

1
@Jonathan: Це, в основному, "розумний вказівник", який не робить нічого, крім встановлення вказівника на нуль. IE замість Foo *a, ви використовуєте InitializedPointer<Foo> a- чисто академічні вправи, як Foo *a=0менше набирати текст. Однак наведений вище код дуже корисний з точки зору освіти. З невеликою модифікацією (до "placeholding" ctor / dtor та присвоєння ops), це може бути легко розширено до різних типів інтелектуальних покажчиків, включаючи скоп-покажчики (які вільні на деструкторі) та посилання-підрахунки-покажчики, додаючи inc / dec операцій, коли m_pPointer встановлений або очищений.
Адісак

2

Зауважте, що статичні дані ініціалізуються до 0 (якщо не сказано інше).

І так, ви завжди повинні оголошувати свої змінні якомога пізніше та з початковим значенням. Код, як

int j;
char *foo;

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


це ГАРАНТИРОВАНО, чи просто звичайна практика, яку використовують компілятори сьогодні?
gha.st

1
статичні змінні ініціалізуються на 0, що також робить правильну річ для покажчиків (тобто встановлює їх на NULL, а не на всі біти 0). Така поведінка гарантується стандартом.
Алок Сінгал

1
ініціалізація статичних даних до нуля гарантується стандартом C і C ++, це не просто звичайна практика
groovingandi

1
можливо тому, що деякі люди хочуть переконатися, що їх стек добре вирівняний, вони попередньо оголошують усі змінні у верхній частині функції? Можливо, вони пишуть на діалекті змінного струму, що ПОТРІБНО це?
KitsuneYMG

1

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

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

Заради розумності завжди ініціалізуйте покажчики на NULL таким чином, якщо будь-яка спроба скинути її з ладу без mallocабо new призведе до втягнення програміста в причину неправильної поведінки програми.

Сподіваюся, це допомагає та має сенс,


0

Ну, якби C ++ ініціалізував покажчики, тоді люди, які скаржаться на те, що "C ++ повільніше, ніж C", було б щось справжнє;)


Це не моя причина. Моя причина полягає в тому, що якщо на апаратному забезпеченні є 512 байти ПЗУ та 128 байт оперативної пам’яті та додаткова інструкція до нуля вказівника, це навіть один байт, що є досить великим відсотком від усієї програми. Мені потрібен цей байт!
Джеррі Єремія

0

C ++ походить із середовища C - і з цієї причини повертається кілька причин:

C, навіть більше, ніж C ++ - це заміна мови складання. Це не робить нічого, чого ти не скажеш. З цього приводу: Якщо ви хочете НУЛЮВАТИ це - зробіть це!

Крім того, якщо ви зведете нанівець речі на голому металі, як-от C, автоматично виникають запитання про несумісність: Якщо ви щось зраджуєте - чи автоматично це буде занулено? Як щодо структури, створеної на стеці? чи всі байти повинні бути нульовими? Що з глобальними змінними? як щодо твердження типу "(* 0x18);" це не означає, що позиція пам'яті 0x18 повинна бути нульовою?


Насправді в С, якщо ви хочете виділити пам'ять, що повністю нульова, ви можете використовувати calloc().
Девід Торнлі

1
тільки мій погляд - якщо ви хочете це зробити, можете, але це не робиться для вас автоматично
gha.st

0

Про які ці вказівки ви говорите?

Для забезпечення безпеки винятків, завжди використовуйте auto_ptr, shared_ptr, weak_ptrта інші їх варіанти.
Ознакою хорошого коду є той, який не включає в себе жодного дзвінка delete.


3
Оскільки C ++ 11, уникайте auto_ptrта замінюйте unique_ptr.
Дедуплікатор

-2

О, малюк. Справжня відповідь полягає в тому, що пам’ять легко звести нанівець, що є базовою ініціалізацією для скажімо вказівника. Що також не має нічого спільного з ініціалізацією самого об’єкта.

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


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