Использование неинициализированной памяти c как исправить

Использование неинициализированной памяти C++ Решение и ответ на вопрос 2512983
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include <iostream>
 
using namespace std;
 
bool used[1024];
 
int j = 0;
int r = 0;
int i = 0;
int k = 0;
 
void dfs(int **iArr, int n, int m, int t) {
 
    used[t] = true;
 
    int p;
 
    for (i = k; i < n; i++)
    {
        j = r;
        if ((iArr[i][j] != 0) && (!used[i]))
        {
            used[i] = true;
            p = i;
 
            cout << i << " ";
 
            for (j = 0; j < m; j++)
            {
                i = p;
                if (iArr[i][j] != 0)
                {
                    r = j;
 
                    for (k = 0; k < n; k++)
                    {
                        j = r;
 
                        if ((iArr[k][j] != 0) && (!used[k]))
                        {
                            dfs(iArr, n, m, i);
                        }
                    }
                }
            }
        }
    }
}
 
int _src[12][14] = 
{
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0},
    {1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0},
    {0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0},
    {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0},
    {0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0},
    {0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1},
    {0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1}
};
 
int main()
{
    int n = std::size(_src);
    int m = std::size(*_src);
    int **iArr= new int *[n];
    for (int i = 0; i < n; ++i)
    {
        iArr[i] = new int[m];
        std::copy(std::begin(_src[i]), std::end(_src[i]), iArr[i]);
    }
 
    for (int i = 0; i < n; i++)
    {
        used[i] = false;
        for (int j = 0; j < m; j++)
            cout << " " << iArr[i][j];
        cout << endl;
    }
 
 
    int from = -1;
    //cout << "From >> ";
    //cin >> from;
 
    cout << "Order: " << endl;
 
    dfs(iArr, n, m, from);
 
    cout << endl;
    for (int i = 0; i < n; ++i)
        delete[] iArr[i];
 
    delete[] iArr;
    return 0;
}

There are two main techniques people use to delay creation of an object. I will show how they apply for a single object, but you can extend these techniques to a static or dynamic array.

Aligned Storage

The first approach is using std::aligned_storage, which is a glorified char array with alignment taken into consideration:

template<typename T>
class Uninitialized1 {
    std::aligned_storage_t<sizeof(T)> _data;

public:
    template<typename... Args>
    void construct(Args... args) {
        new (&_data) T(args...);
        std::cout << "Data: " << *reinterpret_cast<T*>(&_data) << "n";
    }
};

Note that I have left out things like perfect forwarding to keep the main point.

One drawback of this is that there’s no way to have a constexpr constructor that takes a value to copy into the class. This makes it unsuitable for implementing std::optional.

Unions

A different approach uses plain old unions. With aligned storage, you have to be careful, but with unions, you have to be doubly careful. Don’t assume my code is bug-free as is.

template<typename T>
class Uninitialized2 {
    union U {
        char dummy;
        T data;

        U() {}
        U(T t) : data(t) {
            std::cout << "Constructor data: " << data << "n";
        }
    } u;

public:
    Uninitialized2() = default;
    Uninitialized2(T t) : u(t) {}

    template<typename... Args>
    void construct(Args... args) {
        new (&u.data) T(args...);
        std::cout << "Data: " << u.data << "n";
    }
};

A union stores a strongly-typed object, but we put a dummy with trivial construction before it. This means that the default constructor of the union (and of the whole class) can be made trivial. However, we also have the option of initializing the second union member directly, even in a constexpr-compatible way.


One very important thing I left out is that you need to manually destroy these objects. You will need to manually invoke destructors, and that should bother you, but it’s necessary because the compiler can’t guarantee the object is constructed in the first place. Please do yourself a favour and study up on these techniques in order to learn how to utilize them properly, as there are some pretty subtle details, and things like ensuring every object is properly destroyed can become tricky.


I (barely) tested these code snippets with a small class and driver:

struct C {
    int _i;

public:
    explicit C(int i) : _i(i) {
        std::cout << "Constructing C with " << i << "n";
    }

    operator int() const { return _i; }
};

int main() {
    Uninitialized1<C> u1;
    std::cout << "Made u1n";
    u1.construct(5);

    std::cout << "n";

    Uninitialized2<C> u2;
    std::cout << "Made u2n";
    u2.construct(6);

    std::cout << "n";

    Uninitialized2<C> u3(C(7));
    std::cout << "Made u3n";
}

The output with Clang was as follows:

Made u1
Constructing C with 5
Data: 5

Made u2
Constructing C with 6
Data: 6

Constructing C with 7
Constructor data: 7
Made u3

There are two main techniques people use to delay creation of an object. I will show how they apply for a single object, but you can extend these techniques to a static or dynamic array.

Aligned Storage

The first approach is using std::aligned_storage, which is a glorified char array with alignment taken into consideration:

template<typename T>
class Uninitialized1 {
    std::aligned_storage_t<sizeof(T)> _data;

public:
    template<typename... Args>
    void construct(Args... args) {
        new (&_data) T(args...);
        std::cout << "Data: " << *reinterpret_cast<T*>(&_data) << "n";
    }
};

Note that I have left out things like perfect forwarding to keep the main point.

One drawback of this is that there’s no way to have a constexpr constructor that takes a value to copy into the class. This makes it unsuitable for implementing std::optional.

Unions

A different approach uses plain old unions. With aligned storage, you have to be careful, but with unions, you have to be doubly careful. Don’t assume my code is bug-free as is.

template<typename T>
class Uninitialized2 {
    union U {
        char dummy;
        T data;

        U() {}
        U(T t) : data(t) {
            std::cout << "Constructor data: " << data << "n";
        }
    } u;

public:
    Uninitialized2() = default;
    Uninitialized2(T t) : u(t) {}

    template<typename... Args>
    void construct(Args... args) {
        new (&u.data) T(args...);
        std::cout << "Data: " << u.data << "n";
    }
};

A union stores a strongly-typed object, but we put a dummy with trivial construction before it. This means that the default constructor of the union (and of the whole class) can be made trivial. However, we also have the option of initializing the second union member directly, even in a constexpr-compatible way.


One very important thing I left out is that you need to manually destroy these objects. You will need to manually invoke destructors, and that should bother you, but it’s necessary because the compiler can’t guarantee the object is constructed in the first place. Please do yourself a favour and study up on these techniques in order to learn how to utilize them properly, as there are some pretty subtle details, and things like ensuring every object is properly destroyed can become tricky.


I (barely) tested these code snippets with a small class and driver:

struct C {
    int _i;

public:
    explicit C(int i) : _i(i) {
        std::cout << "Constructing C with " << i << "n";
    }

    operator int() const { return _i; }
};

int main() {
    Uninitialized1<C> u1;
    std::cout << "Made u1n";
    u1.construct(5);

    std::cout << "n";

    Uninitialized2<C> u2;
    std::cout << "Made u2n";
    u2.construct(6);

    std::cout << "n";

    Uninitialized2<C> u3(C(7));
    std::cout << "Made u3n";
}

The output with Clang was as follows:

Made u1
Constructing C with 5
Data: 5

Made u2
Constructing C with 6
Data: 6

Constructing C with 7
Constructor data: 7
Made u3

    msm.ru

    Нравится ресурс?

    Помоги проекту!

    >
    c6001: использование неинициализированной памяти. Почему?

    • Подписаться на тему
    • Сообщить другу
    • Скачать/распечатать тему



    Сообщ.
    #1

    ,
    21.01.23, 11:30

      Есть код:

      ExpandedWrap disabled

        int *x;

        x = new int[1000];

        if( … )

        {

        }else

        {

          int xMinMax[2];

          xMinMax[0] = xMinMax[1] = x[0]; // здесь подчёркивает и пишет ‘c6001: Использование неинициализированной памяти «*x»‘

          …

        }

      Пишет такое предупреждение (см. код). VisualStudio 2019. Отчего так?


      grgdvo



      Сообщ.
      #2

      ,
      21.01.23, 14:36

        Member

        **

        Рейтинг (т): 21

        Вокруг

        ExpandedWrap disabled

          x = new int[1000];

        никаких условий нет??
        Все линейно??


        Dushevny



        Сообщ.
        #3

        ,
        21.01.23, 14:40

          Member

          **

          Рейтинг (т): 16

          Потому что после new() для каждого элемента вызывается конструктор по-умолчанию. Для int он ничего не делает, в том числе и не обнуляет переменную. Поэтому каждый элемент этого массива остается непроинициализированным, т.е. содержит мусор, который был в куче на месте этого массива перед его созданием.

          Сообщение отредактировано: Dushevny — 21.01.23, 14:42


          Majestio



          Сообщ.
          #4

          ,
          21.01.23, 15:01

            Злое Солнце

            ***

            Рейтинг (т): 10

            Цитата Dushevny @ 21.01.23, 14:40

            для каждого элемента вызывается конструктор по-умолчанию

            Хе-хе … вопрос интересный! Что-то мне казалось, что для POD-типов нет ни конструкторов (не путать с аллокаторами), не их вызовов, ни по-умоланию и ваапще никак! И тут такой ответ, что я в тупике … :-?

            Вызываю Qraizer‘а!
            >:-[

            user posted image


            Славян



            Сообщ.
            #5

            ,
            21.01.23, 15:52

              Цитата grgdvo @ 21.01.23, 14:36

              никаких условий нет??
              Все линейно??

              Да, всё именно банально так.

              Добавлено 21.01.23, 15:56
              А, тьфу, всё понял! Это, как и написал Dushevny, оттого, что в x[0] то что-то кладётся по условия, а не всегда!

              ExpandedWrap disabled

                int *x;

                x = new int[1000];

                if( … ) x[0] = …;

                if( … )

                {

                }else

                {

                  int xMinMax[2];

                  xMinMax[0] = xMinMax[1] = x[0]; // здесь подчёркивает и пишет ‘c6001: Использование неинициализированной памяти «*x»‘

                  …

                }

              Добавлено 21.01.23, 15:59

              Цитата grgdvo @ 21.01.23, 14:36

              Вокруг

              ExpandedWrap disabled

                x = new int[1000];

              никаких условий нет??

              Получается, ошибся я, — есть условие ‘вокруг’=после. Виноват-с… :blush:


              Majestio



              Сообщ.
              #6

              ,
              21.01.23, 16:01

                Злое Солнце

                ***

                Рейтинг (т): 10

                Славян, на какую именно строку идет «ругня»? Ты выдаешь такие «крохи» инфы, что просто приходится ломать спинной моск в попытках «угадать» >:(
                Ну что может быть проще привести весь проблемный код, и вывести все сообщения об ошибках и предупреждениях в первозданном виде?
                Не, без комментов далее …

                Добавлено 21.01.23, 16:04
                ADD: И даже после решения вопроса я не отказываюсь от своих «фу» и «фи» >:( 8-)


                Dushevny



                Сообщ.
                #7

                ,
                21.01.23, 17:11

                  Member

                  **

                  Рейтинг (т): 16

                  Цитата Majestio @ 21.01.23, 15:01

                  Что-то мне казалось, что для POD-типов нет ни конструкторов (не путать с аллокаторами), не их вызовов, ни по-умолчанию и ваапще никак!

                  Что нет, что пустой и выкинутый оптимизатором по причине своей пустоты — для выходного кода результат одинаков. Может и есть исключение для POD-типов в стандарте — не помню, мне проще для себя для однообразия считать что конструктор есть, но пустой и потому соптимизированный.

                  Сообщение отредактировано: Dushevny — 21.01.23, 17:16


                  Славян



                  Сообщ.
                  #8

                  ,
                  21.01.23, 17:17

                    Цитата Majestio @ 21.01.23, 16:01

                    на какую именно строку идет «ругня»?

                    Строка приведена. Она вся подчёркивается волнистой.
                    Но суть то вся понята, ибо действительно x[0] то ли инициализировался, то ли нет — компилятор не знает, и вот на это использование и идёт ругань. Всё действительно просто.
                    Это как тривиальное:

                    ExpandedWrap disabled

                      int a;

                      int b = a;

                    Добавлено 21.01.23, 17:23
                    Ну, чуть полнее, так:

                    ExpandedWrap disabled

                      int *x, k=0;

                      x = new int[1000];

                      if( … ) x[k++] = …;

                      if( !k )

                      {

                        …

                      }else

                      {

                        int xMinMax[2];

                        xMinMax[0] = xMinMax[1] = x[0]; // здесь подчёркивает и пишет ‘c6001: Использование неинициализированной памяти «*x»‘

                        …

                      }

                    Потому мне то вот понятно, что использован будет инициализированный элемент x[0], а машине — неведомо сие…


                    Majestio



                    Сообщ.
                    #9

                    ,
                    21.01.23, 17:41

                      Злое Солнце

                      ***

                      Рейтинг (т): 10

                      Цитата Славян @ 21.01.23, 17:17

                      Она вся подчёркивается волнистой.

                      Ну я на форуме обычно на все смотрю корпускулярно — не видны эти ваши красные волны! 8-)

                      Добавлено 21.01.23, 17:43

                      Цитата Dushevny @ 21.01.23, 17:11

                      Что нет, что пустой и выкинутый оптимизатором по причине своей пустоты — для выходного кода результат одинаков.

                      Не не не — «точность — вежливость королей». Это нужно выяснить.

                      Guru

                      Qraizer



                      Сообщ.
                      #10

                      ,
                      21.01.23, 18:17

                        Moderator

                        *******

                        Рейтинг (т): 520

                        Ну конечно нет никаких конструкторов у POD. Но по факту это ничего не меняет.
                        В Стандарте определение объектов без инициализаторов называется default initialization и выполняется по-разному для классов и не-классов.

                        • Для классов вызывается конструктор по умолчанию. Если он не предоставлен программистом, он может быть сгенерирован компилятором. Если он не может быть сгенерирован, ошибка; если может, то он работает по тем же правилам инициализации для всех агрегированных объектов, т.е. все базовые объекты и поля инициализирует default initialization. Аналогично для массивов: default initialization для каждого элемента.
                        • Для не-классов ничего не выполняется. Это оставляет их в неизменном виде. Указатели являются не-классами.
                        • Ссылки и константные объекты не могут быть default initialization, это ошибка. Для агрегатов, содержащих в себе объекты этого типа, конструктор не может быть сгенерирован.

                        Но в Стандарте есть ещё такая штука, как zero initialization. Она не применяется к автоматическим и динамическим объектам, но если применяется, то до любой другой инициализации, в частности и default initialization. Т.к. для классов default initialization вполне себе инициализатор, хоть и зачастую неявный, то работу zero initialization можно увидеть только на статических и локальных в потоках объектах не-классах. И да, она забивает объект нулями. За исключением указателей, которые ставятся в nullptr. (Это на случай, если у вас nullptr не равен побитово целочисленному нулю ;). Ну а вдруг.) И да, включая pad-ы между полями агрегатов. А, ещё для ссылок есть исключение: zero initialization для них не выполняется, но т.к. они обязаны быть value initialized, то этого не видно.

                        Сообщение отредактировано: Qraizer — 21.01.23, 18:19


                        Majestio



                        Сообщ.
                        #11

                        ,
                        21.01.23, 18:20

                          Злое Солнце

                          ***

                          Рейтинг (т): 10

                          Цитата Qraizer @ 21.01.23, 18:17

                          Ну конечно нет никаких конструкторов у POD. Но по факту это ничего не меняет.
                          В Стандарте определение объектов без инициализаторов называется default initialization и выполняется по-разному для классов и не-классов.
                          Для классов вызывается конструктор по умолчанию. Если он не предоставлен программистом, он может быть сгенерирован компилятором. Если он не может быть сгенерирован, ошибка; если может, то он работает по тем же правилам инициализации для всех агрегированных объектов, т.е. все базовые объекты и поля инициализирует default initialization. Аналогично для массивов: default initialization для каждого элемента.
                          Для не-классов ничего не выполняется. Это оставляет их в неизменном виде. Указатели являются не-классами.
                          Ссылки и константные объекты не могут быть default initialization, это ошибка. Для агрегатов, содержащих в себе объекты этого типа, конструктор не может быть сгенерирован.
                          Но в Стандарте есть ещё такая штука, как zero initialization. Она не применяется к автоматическим и динамическим объектам, но если применяется, то до любой другой инициализации, в частности и default initialization. Т.к. для классов default initialization вполне себе инициализатор, хоть и зачастую неявный, то работу zero initialization можно увидеть только на статических и локальных в потоках объектах не-классах. И да, она забивает объект нулями. За исключением указателей, которые ставятся в nullptr. (Это на случай, если у вас nullptr не равен побитово целочисленному нулю . Ну а вдруг.) И да, включая pad-ы между полями агрегатов. А, ещё для ссылок есть исключение: zero initialization для них не выполняется, но т.к. они обязаны быть value initialized, то этого не видно.

                          ЧТД, RTFM :-)

                          0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)

                          0 пользователей:

                          • Предыдущая тема
                          • C/C++: Общие вопросы
                          • Следующая тема

                          Рейтинг@Mail.ru

                          [ Script execution time: 0,0629 ]   [ 16 queries used ]   [ Generated: 10.02.23, 10:10 GMT ]  

                          Добавлено 8 апреля 2021 в 17:40

                          Неинициализированные переменные

                          В отличие от некоторых языков программирования, C/C++ не инициализирует большинство переменных автоматически заданным значением (например, нулем). Таким образом, когда компилятор выделяет переменной место в памяти, значением по умолчанию для этой переменной является любое (мусорное) значение, которое уже находится в этой области памяти! Переменная, которой не было присвоено известное значение (обычно посредством инициализации или присваивания), называется неинициализированной переменной.

                          Примечание автора


                          Многие читатели ожидают, что термины «инициализированный» и «неинициализированный» будут строго противоположными, но это не совсем так! Инициализация означает, что объекту было предоставлено начальное значение в точке определения. Неинициализированный означает, что объекту не было присвоено известное значение (каким-либо образом, включая присваивание). Следовательно, объект, который не инициализирован, но которому затем было присвоено значение, больше не является неинициализированным (потому что ему было присвоено известное значение).

                          Резюмируем:

                          • инициализация = объекту присваивается известное значение в точке определения;
                          • присваивание = объекту присваивается известное значение в точке, выходящей за рамки определения;
                          • неинициализированный = объекту еще не присвоено известное значение.

                          В качестве отступления…


                          Отсутствие инициализации является оптимизацией производительности, унаследованной от C, когда компьютеры были медленными. Представьте себе случай, когда вы собираетесь прочитать 100 000 значений из файла. В таком случае вы можете создать 100 000 переменных, а затем заполнить их данными из файла.

                          Если бы C++ инициализировал все эти переменные при создании значениями по умолчанию, это привело бы к 100 000 инициализаций (что было бы медленно) и к небольшой выгоде (поскольку вы всё равно перезапишете эти значения).

                          На данный момент вы всегда должны инициализировать свои переменные, потому что затраты на это ничтожны по сравнению с выгодой. Как только вы освоите язык, тогда могут быть определенные случаи, когда вы можете пропустить инициализацию в целях оптимизации. Но делать это всегда нужно выборочно и намеренно.

                          Использование значений неинициализированных переменных может привести к неожиданным результатам. Рассмотрим следующую короткую программу:

                          #include <iostream>
                           
                          int main()
                          {
                              // определяем целочисленную переменную с именем x
                              int x; // эта переменная не инициализирована, потому что мы не присвоили ей значение
                              
                              // выводим значение x на экран
                              std::cout << x; // кто знает, что мы получим, потому что x не инициализирована
                           
                              return 0;
                          }

                          В этом случае компьютер выделит для x некоторую неиспользуемую память. Затем он отправит значение, находящееся в этой ячейке памяти, в std::cout, который напечатает значение (интерпретируемое как целое число). Но какое значение он напечатает? Ответ – «кто знает!», и ответ может (или не может) меняться каждый раз, когда вы запускаете программу. Когда автор запускал эту программу в Visual Studio, std::cout в первый раз вывел значение 7177728, а во второй раз – 5277592. Не стесняйтесь компилировать и запускать программу самостоятельно (ваш компьютер не взорвется).

                          В качестве отступления…


                          Некоторые компиляторы, такие как Visual Studio, при использовании конфигурации отладочной сборки будут инициализировать содержимое памяти некоторым предустановленным значением. Этого не произойдет при использовании конфигурации сборки выпуска. Поэтому, если вы хотите запустить указанную выше программу самостоятельно, убедитесь, что вы используете конфигурацию сборки выпуска (чтобы вспомнить, как это сделать, смотрите урок «0.9 – Настройка компилятора: конфигурации сборки»). Например, если вы запустите приведенную выше программу в конфигурации отладки в Visual Studio, она будет неизменно печатать -858993460, потому что с помощью этого значения (интерпретируемого как целое число) Visual Studio инициализирует память в конфигурациях отладки.

                          Большинство современных компиляторов пытаются определить, используется ли переменная без присваивания значения. Если они смогут это обнаружить, они обычно выдадут ошибку времени компиляции. Например, компиляция приведенной выше программы в Visual Studio выдала следующее предупреждение:

                          c:VCprojectstesttest.cpp(11) : warning C4700: uninitialized local variable 'x' used

                          Если ваш компилятор по этой причине не позволяет вам скомпилировать и запустить приведенную выше программу, вот возможное решение этой проблемы:

                          #include <iostream>
                          
                          // Пока не беспокойтесь о том, что такое &, мы просто используем его, 
                          // чтобы заставить компилятор думать, что переменная x используется 
                          void doNothing(int&) 
                          {
                          }
                           
                          int main()
                          {
                              // определяем целочисленную переменную с именем x
                              int x; // эта переменная не инициализирована
                           
                              // заставляем компилятор думать, что мы присваиваем значение этой переменной
                              doNothing(x);
                           
                              // выводим значение x на экран (кто знает, что мы получим, потому что x не инициализирован)
                              std::cout << x;
                           
                              return 0;
                          }

                          Использование неинициализированных переменных – одна из наиболее распространенных ошибок, которые совершают начинающие программисты, и, к сожалению, она также может быть одной из самых сложных для отладки (потому что программа всё равно может работать нормально, если неинициализированное значение было присвоено определенной области памяти, которая содержала приемлемое значение, например, 0).

                          Это основная причина использования оптимальной практики «всегда инициализировать переменные».

                          Неопределенное поведение

                          Использование значения из неинициализированной переменной – наш первый пример неопределенного поведения. Неопределенное поведение – это результат выполнения кода, поведение которого не определено языком C++. В этом случае в языке C++ нет правил, определяющих, что произойдет, если вы используете значение переменной, которой не было присвоено известное значение. Следовательно, если вы действительно сделаете это, результатом будет неопределенное поведение.

                          Код, реализующий неопределенное поведение, может проявлять любые из следующих симптомов:

                          • ваша программа при каждом запуске дает разные результаты;
                          • ваша программа постоянно дает один и тот же неверный результат;
                          • ваша программа ведет себя непоследовательно (иногда дает правильный результат, иногда нет);
                          • кажется, что ваша программа работает, но позже выдает неверные результаты;
                          • ваша программа вылетает сразу после запуска или позже;
                          • ваша программа работает с одними компиляторами, но не работает с другими;
                          • ваша программа работает до тех пор, пока вы не измените какой-нибудь другой, казалось бы, несвязанный код.

                          Или ваш код в любом случае может действительно вести себя правильно. Природа неопределенного поведения заключается в том, что вы никогда не знаете точно, что получите, будете ли вы получать это каждый раз, и изменится ли это поведение, когда вы внесете какие-либо изменения.

                          C++ содержит множество случаев, которые могут привести к неопределенному поведению, если вы не будете осторожны. Мы будем указывать на них в будущих уроках всякий раз, когда с ними столкнемся. Обратите внимание на эти случаи и убедитесь, что вы их избегаете.

                          Правило


                          Старайтесь избегать всех ситуаций, которые приводят к неопределенному поведению, например, использование неинициализированных переменных.

                          Примечание автора


                          Один из наиболее распространенных типов комментариев, которые мы получаем от читателей, гласит: «Вы сказали, что я не могу делать X, но я всё равно сделал это, и моя программа работает! Почему?».

                          Есть два общих ответа. Наиболее распространенный ответ заключается в том, что ваша программа на самом деле демонстрирует неопределенное поведение, но это неопределенное поведение в любом случае дает желаемый результат… пока. Завтра (или на другом компиляторе или машине) этого может и не быть.

                          В качестве альтернативы, иногда авторы компиляторов допускают вольность к требованиям языка, когда эти требования могут быть более строгими, чем необходимо. Например, в стандарте может быть сказано: «Вы должны сделать X перед Y», но автор компилятора может счесть это ненужным и заставить Y работать, даже если вы сначала не выполните X. Это не должно влиять на работу правильно написанных программ, но в любом случае может привести к тому, что неправильно написанные программы будут работать. Таким образом, альтернативный ответ на вышеупомянутый вопрос заключается в том, что ваш компилятор может просто не следовать стандарту! Такое случается. Вы можете избежать этого, если отключили расширения компилятора, как описано в уроке «0.10 – Настройка компилятора: расширения компилятора».

                          Небольшой тест


                          Вопрос 1

                          Что такое неинициализированная переменная? Почему вам следует избегать их использования?

                          Ответ

                          Неинициализированная переменная – это переменная, которой программа не присвоила значение (обычно посредством инициализации или присваивания). Использование значения, хранящегося в неинициализированной переменной, приведет к неопределенному поведению.


                          Вопрос 2

                          Что такое неопределенное поведение и что может произойти, если вы сделаете что-то, что демонстрирует неопределенное поведение?

                          Ответ

                          Неопределенное поведение – это результат выполнения кода, поведение которого не определяется языком. Результатом может быть что угодно, в том числе и то, что ведет себя правильно.

                          Теги

                          C++ / CppLearnCppДля начинающихОбучениеПрограммирование

                          Если вы пишете код на языке C или C++, поиск и устранение ошибок работы с памятью, таких, как утечки, выход за границы массива или обращение к неинициализированной памяти, могут доставить немало хлопот. Существует по крайней мере два инструмента для решения этих проблем — Valgrind (не путать с Vagrant!) и Clang’овский MemorySanitizer. Последний работает исключительно под Linux и показал себя несколько сырым и не слишком гибким инструментом, поэтому поговорим о Valgrind. Он довольно гибок и работает везде. Кроме того, в отличие от MemorySanitizer, Valgrind может находить неинициализированные данные с точностью до одного бита. Из недостатков Valgrind стоит отметить сравнительно низкую скорость работы.

                          Простой пример

                          Перейдем сразу к делу и проверим работу Valgrind на такой программе:

                          #include <stdio.h>
                          #include <stdlib.h>
                          #include <string.h>

                          void run_test(int i)
                          {
                            int delta = 123;
                            char* mem = malloc(1024);
                            strcpy(mem, «i = «);
                            printf(«%s %dn«, mem, i + delta);
                            /* free(mem); */
                          }

                          void main()
                          {
                            int i;
                            for(i = 0; i < 10; i++)
                              run_test(i);
                          }

                          Компилируем с отладочными символами и запускаем ее под Valgrind:

                          gcc -O0 -g vgcheck.c -o vgcheck
                          valgrind ./vgcheck

                          Результат:

                          ==1948== HEAP SUMMARY:
                          ==1948==     in use at exit: 10,240 bytes in 10 blocks
                          ==1948==   total heap usage: 11 allocs, 1 frees, 11,264 bytes allo…
                          ==1948==
                          ==1948== LEAK SUMMARY:
                          ==1948==    definitely lost: 10,240 bytes in 10 blocks
                          ==1948==    indirectly lost: 0 bytes in 0 blocks
                          ==1948==      possibly lost: 0 bytes in 0 blocks
                          ==1948==    still reachable: 0 bytes in 0 blocks
                          ==1948==         suppressed: 0 bytes in 0 blocks
                          ==1948== Rerun with —leak-check=full to see details of leaked memory

                          Видим, что память утекла. Запускаем с --leak-check=full:

                          ==2047== 10,240 bytes in 10 blocks are definitely lost in loss recor…
                          ==2047==    at 0x4C2AF1F: malloc (in /usr/lib/valgrind/vgpreload_mem…
                          ==2047==    by 0x400561: run_test (vgcheck.c:8)
                          ==2047==    by 0x4005AF: main (vgcheck.c:18)

                          Теперь раскомментируем вызов free и уберем инициализацию переменной delta. Посмотрим, увидит ли Valgrind обращение к неинициализированной памяти:

                          ==2102== Conditional jump or move depends on uninitialised value(s)
                          ==2102==    at 0x4E8003C: vfprintf (in /usr/lib/libc-2.25.so)
                          ==2102==    by 0x4E87EA5: printf (in /usr/lib/libc-2.25.so)
                          ==2102==    by 0x4005CA: run_test (vgcheck.c:10)
                          ==2102==    by 0x4005F4: main (vgcheck.c:18)

                          Видит. Запустим с --track-origins=yes чтобы найти, откуда именно пришла неинициализированная переменаая:

                          ==2205== Conditional jump or move depends on uninitialised value(s)
                          ==2205==    at 0x4E800EE: vfprintf (in /usr/lib/libc-2.25.so)
                          ==2205==    by 0x4E87EA5: printf (in /usr/lib/libc-2.25.so)
                          ==2205==    by 0x4005CA: run_test (vgcheck.c:10)
                          ==2205==    by 0x4005F4: main (vgcheck.c:18)
                          ==2205==  Uninitialised value was created by a stack allocation
                          ==2205==    at 0x400586: run_test (vgcheck.c:6)

                          Как видите, Valgrind нашел место объявления неинициализированной переменой с точностью до имени файла и номера строчки.

                          Теперь исправим все ошибки:

                          ==2239== HEAP SUMMARY:
                          ==2239==     in use at exit: 0 bytes in 0 blocks
                          ==2239==   total heap usage: 11 allocs, 11 frees, 11,264 bytes allo…
                          ==2239==
                          ==2239== All heap blocks were freed — no leaks are possible

                          Ну разве не красота?

                          Пример посложнее — запускаем PostgreSQL под Valgrind

                          Рассмотрим, как происходит запуск под Valgrind больших программ, например, PostgreSQL. Работа с памятью в этой РСУБД устроена особым образом. Например, в ней используются иерархические пулы памяти (memory contexts). Для понимания всего этого хозяйства Valgrind’у нужны подсказки. Чтобы такие подсказки появились, нужно раскомментировать строчку:

                          … в файле src/include/pg_config_manual.h, после чего полностью пересобрать PostgreSQL. Затем запуск под Valgrind осуществляется как-то так:

                          valgrind —leak-check=no —track-origins=yes —gen-suppressions=all
                            —read-var-info=yes
                            —log-file=$HOME/work/postgrespro/postgresql-valgrind/%p.log
                            —suppressions=src/tools/valgrind.supp —time-stamp=yes
                            —trace-children=yes postgres -D
                            $HOME/work/postgrespro/postgresql-install/data-master
                            2>&1 | tee $HOME/work/postgrespro/postgresql-valgrind/postmaster.log

                          Полный пример вы найдете в файле valgrind.sh из этого репозитория на GitHub.

                          Обратите внимание на флаг --leak-check=no. Даже с упомянутыми подсказками Valgrind все равно не подходит для поиска утечек памяти в PostgreSQL. Он попросту будет генерировать слишком много ложных сообщений об ошибках. Поэтому здесь Valgrind используется только для поиска обращений к неинициализированной памяти.

                          Флаг --trace-children=yes в приведенной выше команде, как несложно догадаться, говорит Valgrind’у цепляться к процессам-потомкам.

                          Еще стоит отметить флаг --suppressions, который задает файл с описанием ошибок, которые следует игнорировать, а также флаг --gen-suppressions=all, который в случае возникновения ошибок генерирует строки, которые можно добавить в этот самый файл для игнорирования ошибок. Кстати, в файле можно использовать wildcards, в стиле:

                          {
                             <libpango>
                             Memcheck:Leak
                             …
                             obj:/usr/*lib*/libpango*
                          }

                          В зависимости от используемых флагов, make installcheck под Valgrind’ом на моем ноутбуке выполняется от получаса до часа. Для сравнения, без Valgrind’а соответствующий прогон тестов занимает порядка 3.5 минут. Отсюда можно сделать вывод, что программа под Valgrind выполняется в 10-20 раз медленнее.

                          Использование Valgrind совместно с GDB

                          Посмотрев на приведенные выше отчеты Valgrind’а об ошибках, можно заметить, что в определенном смысле они недостаточно информативны. В частности, в них нет имен переменных и информации о том, какие конкретно данные в них лежали на момент возникновения ошибки. Решается эта проблема запуском Valgrind’а с флагами:

                          valgrind —vgdb=yes —vgdb-error=1 дальше_как_обычно

                          Эти флаги говорят Valgrind остановить процесс и запустить gdb-сервер после возникновения первой ошибки. Можно указать и --vgdb-error=0, чтобы подключиться к процессу отладчиком сразу после его запуска. Однако это может быть плохой идеей, если вы также указали --trace-children=yes и при этом программа создает множество дочерних процессов.

                          При возникновении ошибки Valgrind напишет:

                          ==00:00:00:06.603 16153== TO DEBUG THIS PROCESS USING GDB: start GDB…
                          ==00:00:00:06.603 16153==   /path/to/gdb postgres
                          ==00:00:00:06.603 16153== and then give GDB the following command
                          ==00:00:00:06.603 16153==   target remote | vgdb —pid=16153

                          После этого, чтобы подключиться к процессу при помощи GDB, говорим:

                          # где postgres — имя исполняемого файла
                          gdb postgres

                          … и уже в отладчике:

                          target remote | vgdb —pid=16153

                          Из интересных дополнительных команд доступны следующие. Посмотреть список утечек:

                          Узнать, кто ссылается на память:

                          monitor who_points_at (address) (len)

                          Проверка инициализированности памяти (0 — бит инициализирован, 1 — не инициализирован, _ — not addressable):

                          monitor get_vbits (address) (len)

                          Прочее:

                          Дальше отлаживаем, как обычно. Например, говорим continue. Как только произойдет следующая ошибка, программа снова остановится по брейкпоинту. Можно смотреть значения переменных, перемещаться между фреймами стека, ставить собственные брейкпоинты, и так далее.

                          Заключение

                          К сожалению, в рамках одного поста невозможно рассмотреть абсолютно все возможности Valgrind. Например, в него входят инструменты Callgrind и Massif, предназначенные для поиска узких мест в коде и профилирования памяти соответственно. Эти инструменты я не рассматриваю, так как для решения названных задач предпочитаю использовать perf и Heaptrack. Также существует инструмент Helgrind, предназначенный для поиска гонок. Его изучение я вынужден оставить вам в качестве упражнения.

                          Как видите, пользоваться Valgrind крайне просто. Он, конечно, не идеален. Как уже отмечалось, Valgrind существенно замедляет выполнение программы. Кроме того, в нем случаются ложноположительные срабатывания. Однако последняя проблема решается составлением специфичного для вашего проекта файла подавления конкретных отчетов об ошибках. Так или иначе, если вы пишете на C/C++ и не прогоняете код под Valgrind хотя бы в Jenkins или TeamCity незадолго до релиза, вы явно делаете что-то не так!

                          А как вы ищете утечки и обращения к неинициализированной памяти?

                          Метки: C/C++, Отладка.

                          Понравилась статья? Поделить с друзьями:

                          Читайте также:

                        • Использование мобильных данных на андроиде 2 гб как исправить
                        • Испортила джинсы белизной как исправить
                        • Использование минеральных удобрений приносит большую пользу растениям речевая ошибка
                        • Испортила волосы после осветления как исправить
                        • Использована неинициализированная локальная переменная c как исправить

                        • 0 0 голоса
                          Рейтинг статьи
                          Подписаться
                          Уведомить о
                          guest

                          0 комментариев
                          Старые
                          Новые Популярные
                          Межтекстовые Отзывы
                          Посмотреть все комментарии