Разыменование пустого указателя как исправить

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
#include <iostream>
#include <iomanip>
#include <cstring>
#include <fstream>
using namespace std;
 
struct Element {
    char name[33];
};
 
void Table(ifstream& File, const int N, Element** mass, int& index);
void Search(char choice[33], int& index, const int N, Element** mass);
int Hash(char str[33], const int N);
void Saving(char str[33], Element** mass, const int N, int& index);
 
int main()
{
    setlocale(LC_ALL, "Russian");
    int index;
    const int N = 100;
    Element* mass[N];
    for (int i = 0; i < N; i++) {
        mass[i] = NULL;
    }
    ifstream File("File.txt");
    if (!File.is_open()) {
        cout << "Error!" << endl;
        exit(1);
    }
    else {
        while (!File.eof()) {
            Table(File, N, mass, index);
        }
    }
    char choice[33];
    cout << "Введите идентификатор для поиска в хэш-таблице:" << endl;
    cin >> choice;
    Search(choice, index, N, mass);
    File.close();
    system("pause");
}
 
void Table(ifstream &File, const int N, Element** mass, int& index) {
    int num = 0, number = 0, nums = 0, strnum = 0;
    char text[33];
    bool check = false, check1 = false;
    char str[33] = {0};
    File >> text;
    if (strchr(text, '(') != NULL && strchr(text, ')') != NULL) {
        for (int i = 0; i < strlen(text); i++) {
            if (text[i] == '(')
                nums = i;
        }
        if (text[nums+1] == ')') {
            while (text[num] != '(') {
                str[num] = text[num];
                num++;
            }
            cout << str << setw(15) << 't' << "идентификатор" << endl;
            Saving(str, mass, N, index);
            cout << '(' << setw(15) << 't' << "открывающая скобка" << endl;
            cout << ')' << setw(15) << 't' << "закрывающая скобка" << endl;
        }
        else {
            while (text[num] != '(') {
                str[num] = text[num];
                num++;
            }
            cout << str << setw(15) << 't' << "идентификатор" << endl;
            Saving(str, mass, N, index);
            memset(str, 0, 33);
            cout << '(' << setw(15) << 't' << "открывающая скобка" << endl;
            while (text[num + 1] != ')') {
                str[strnum] = text[num + 1];
                num++;
                number++;
                strnum++;
            }
            for (int i = 0; i < number; i++) {
                if (str[i] == '.')
                    check = true;
            }
            if (isxdigit(str[0])) {
                if (number == 1)
                    cout << str << setw(15) << 't' << "шестнадцатеричное число" << endl;
                else if (check)
                    cout << str << setw(15) << 't' << "десятичное число с плавающей точкой" << endl;
                else if (isdigit(str[0]))
                    cout << str << setw(15) << 't' << "целое десятичное число без знака" << endl;
                else {
                    cout << str << setw(15) << 't' << "идентификатор" << endl;
                    Saving(str, mass, N, index);
                }
            }
            else {
                cout << str << setw(15) << 't' << "идентификатор" << endl;
                Saving(str, mass, N, index);
            }
            cout << ')' << setw(15) << 't' << "закрывающая скобка" << endl;
        }
    }
    else if (strchr(text, '(') != NULL) {
        while (text[num] != '(') {
            str[num] = text[num];
            num++;
        }
        cout << str << setw(15) << 't' << "идентификатор" << endl;
        Saving(str, mass, N, index);
        memset(str, 0, 33);
        cout << '(' << setw(15) << 't' << "открывающая скобка" << endl;
        while (text[num+1] != ',') {
            str[number] = text[num+1];
            num++;
            number++;
        }
        for (int i = 0; i < number; i++) {
            if (str[i] == '.')
                check = true;
        }
        if (isxdigit(str[0])) {
            if (number == 1)
                cout << str << setw(15) << 't' << "шестнадцатеричное число" << endl;
            else if (check)
                cout << str << setw(15) << 't' << "десятичное число с плавающей точкой" << endl;
            else if (isdigit(str[0]))
                cout << str << setw(15) << 't' << "целое десятичное число без знака" << endl;
            else {
                cout << str << setw(15) << 't' << "идентификатор" << endl;
                Saving(str, mass, N, index);
            }
        }
        else {
            cout << str << setw(15) << 't' << "идентификатор" << endl;
            Saving(str, mass, N, index);
        }
        cout << ',' << setw(15) << 't' << "запятая" << endl;
    }
    else if (strchr(text, ')') != NULL) {
        while (text[num] != ')') {
            str[num] = text[num];
            num++;
            number++;
        }
        for (int i = 0; i < number; i++) {
            if (str[i] == '.')
                check = true;
        }
        if (isxdigit(str[0])) {
            if (number == 1)
                cout << str << setw(15) << 't' << "шестнадцатеричное число" << endl;
            else if (check)
                cout << str << setw(15) << 't' << "десятичное число с плавающей точкой" << endl;
            else if (isdigit(str[0]))
                cout << str << setw(15) << 't' << "целое десятичное число без знака" << endl;
            else {
                cout << str << setw(15) << 't' << "идентификатор" << endl;
                Saving(str, mass, N, index);
            }
        }
        else {
            cout << str << setw(15) << 't' << "идентификатор" << endl;
            Saving(str, mass, N, index);
        }
        cout << ')' << setw(15) << 't' << "закрывающая скобка" << endl;
    }
    else if (text[0] == '/' && text[1] == '/' || text[0] == '/' && text[1] == '*' && text[strlen(text)-2] == '*' && text[strlen(text)-1] == '/') {
        cout << text << setw(15) << 't' << "комментарий" << endl;
    }
    else {
        for (int i = 0; i < strlen(text); i++) {
            if (text[i] == ',')
                check1 = true;
        }
        while (text[num] != ',') {
            str[num] = text[num];
            num++;
            number++;
        }
        for (int i = 0; i < number; i++) {
            if (str[i] == '.')
                check = true;
        }
        if (isxdigit(str[0])) {
            if (number == 1)
                cout << str << setw(15) << 't' << "шестнадцатеричное число" << endl;
            else if (check)
                cout << str << setw(15) << 't' << "десятичное число с плавающей точкой" << endl;
            else if (isdigit(str[0]))
                cout << str << setw(15) << 't' << "целое десятичное число без знака" << endl;
            else {
                cout << str << setw(15) << 't' << "идентификатор" << endl;
                Saving(str, mass, N, index);
            }
        }
        else {
            cout << str << setw(15) << 't' << "идентификатор" << endl;
            Saving(str, mass, N, index);
        }
        if (check1)
            cout << ',' << setw(15) << 't' << "запятая" << endl;
    }
}
 
void Search(char choice[33], int& index, const int N, Element** mass) {
    bool there_is = false;
    index = Hash(choice, N);
    if (mass[index]->name == choice)
        cout << choice << " найден под номером " << index + 1 << endl;
    else {
        for (int i = 0; i < N; i++) {
            if (mass[i]->name == choice) {
                cout << choice << " найден под номером " << index + 1 << endl;
                there_is = true;
                break;
            }
        }
        if (!there_is)
            cout << choice << " не найден" << endl;
    }
}
 
int Hash(char str[33], const int N) {
    int hash;
    hash = toupper(str[0]) - 'A';
    hash += strlen(str);
    return hash % N;
}
 
void Saving(char str[33], Element** mass, const int N, int& index) {
    bool there_is = false;
    index = Hash(str, N);
    if (mass[index] == NULL) {
        strcpy_s(mass[index]->name, str);
    }
    else {
        if (mass[index]->name != str) {
            for (int i = 0; i < N; i++) {
                if (mass[i]->name == str)
                    there_is = true;
                    break;
            }
            if (!there_is) {
                for (int i = 0; i < N; i++) {
                    if (mass[i] == NULL)
                        strcpy_s(mass[i]->name, str);
                }
            }
        }
    }
}

Разыменовывание нулевого указателя приводит к неопределённому поведению

Время на прочтение
6 мин

Количество просмотров 52K

Silent NULL (Разыменовывание нулевого указателя приводит к неопределённому поведению)
Ненароком я породил большую дискуссию, касающуюся того, допустимо ли использовать в Си/Си++ выражение &P->m_foo, если P является нулевым указателем. Программисты разделились на два лагеря. Одни уверенно доказывали, что так писать нельзя, другие столь же уверенно утверждали, что можно. Приводились различные аргументы и ссылки. И я понял, что нужно внести окончательную ясность в этот вопрос. Для этого я обратился к экспертам Microsoft MVP и разработчикам Visual C++, общающимся через закрытый список рассылки. Они помогли подготовить эту статью, и я представляю её всем желающим. Для нетерпеливых: этот код не корректен.

Напомню историю обсуждений

Все началось со статьи о проверке ядра Linux с помощью анализатора PVS-Studio. Но сама проверка ядра тут ни причём. Дело в том, что в статье я привёл следующий фрагмент из кода Linux:

static int podhd_try_init(struct usb_interface *interface,
        struct usb_line6_podhd *podhd)
{
  int err;
  struct usb_line6 *line6 = &podhd->line6;

  if ((interface == NULL) || (podhd == NULL))
    return -ENODEV;
  ....
}

Я назвал этот код опасным, так как посчитал, что здесь имеет место неопределённое поведение.

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

#define offsetof(st, m) ((size_t)(&((st *)0)->m))

Здесь имеет место разыменование нулевого указателя, но код успешно работает. Были и другие письма с рассуждениями того, что раз нет доступа по нулевому указателю, то нет и проблемы.

Хотя я и доверчивый, но стараюсь проверять информацию. Я начал разбираться с этой темой и в результате написал небольшую статью: “Размышления над разыменованием нулевого указателя”.

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

После статьи вновь последовали письма с возражениями, и я понял, что надо разобраться с данной темой окончательно. Я обратился с вопросом к экспертам, чтобы узнать их мнение. Эта статья является их обобщенным ответом.

О языке Си

Выражение ‘&podhd->line6’ является неопределенным поведением в языке C в том случае, если ‘podhd’ — нулевой указатель.

Вот что говорится про оператор взятия адреса ‘&’ в стандарте C99 (Раздел 6.5.3.2 «Операторы взятия адреса и разыменовывания»):

Операнд унарного оператора & должен быть либо указателем функции, либо результатом оператора [] или унарного оператора *, либо lvalue-выражением, указывающим на объект, который не является битовым полем и не содержит в объявлении спецификатора регистрового класса памяти.

Выражение ‘podhd->line6’ однозначно не является указателем функции, результатом оператора [] или *. Это как раз lvalue-выражение. Однако, когда указатель ‘podhd’ равен нулю, выражение не указывает на объект, поскольку в Разделе 6.3.2.3 «Указатели» сказано следующее:

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

Если «lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение» (Стандарт C99, Раздел 6.3.2.1 «Lvalue-выражения, массивы и указатели функций»):

lvalue — это выражение объектного типа или неполного типа, отличного от void; если lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение.

Ещё раз кратко:

Когда оператор -> был применен к указателю, его результатом стало lvalue-выражение, для которого не существует объекта, и в результате мы имеем дело с неопределенным поведением.

О языке Си++

В языке С++ всё обстоит точно также. Выражение ‘&podhd->line6’ является неопределенным поведением в языке C++ в том случае, если ‘podhd’ — нулевой указатель.

С толку немного сбивает дискуссия на WG21 (232. Is indirection through a null pointer undefined behavior?), на которую я ссылался в предыдущей статье. Там настаивают, будто бы такое выражение не является неопределенным поведением. Однако никто так и не нашел никаких правил в стандартах C++, которые разрешали бы использовать «poldh->line6», когда «polhd» — нулевой указатель.

Указатель «polhd» нарушает основное ограничение (Раздел 5.2.5/4, второй пункт в списке) о том, что он должен указывать на объект. Ни один объект в C++ не может иметь адреса nullptr.

Итого

struct usb_line6 *line6 = &podhd->line6;

Этот код является некорректным в языке Си и Си++, если указатель podhd равен 0. Если указатель равен 0, то возникает неопределённое поведение.

То, что программа может работать, является везением. Неопределённое поведение может проявить себя, как угодно. В том числе, программа может работать так, как хотел программист. Это один из частных случаев, но не более того.

Так писать нельзя. Указатель должен быть проверен до разыменования.

Разное в дополнение

  • При рассмотрении идиоматической реализации offsetof() следует учитывать, что компилятору разрешено использовать непереносимые приемы для реализации этой функциональности. Тот факт, что в реализации библиотеки в компиляторе используется константа нулевого указателя при реализации offsetof(), вовсе не означает, что в пользовательском коде можно без опаски применять ‘&podhd->line6′ в случае, когда’podhd’ является нулевым указателем.
  • GCC может (и делает это) проводить оптимизацию, основываясь на предположении, что никакого неопределенного поведения возникнуть не может, и убрать в данном случае проверки указателей на ноль — поэтому ядро компилируется с набором ключей, указывающих компилятору не делать этого. Например, эксперты в качестве примера ссылаются на статью “What Every C Programmer Should Know About Undefined Behavior #2/3”.
  • Возможно, вам также будет интересно узнать, что подобным образом нулевой указатель был задействован в эксплойте ядра с помощью TUN/TAP-драйва. Подробности можно посмотреть по ссылке “Fun with NULL pointers”. Некоторые могут решить, будто эти два примера имеют мало общего, поскольку во втором случае есть существенное отличие: в баге TUN/TAP-драйвера вместо простого взятия адреса поля структуры, к которому обращался нулевой указатель, это поле было явно взято в качестве значения для инициализации переменной. Однако с точки зрения стандарта C взятие адреса поля с помощью нулевого указателя также является неопределенным поведением.
  • А есть ли какая-та ситуация, когда при P == nullptr мы напишем &P->m_foo и всё будет хорошо? Да, например это может быть аргументом оператора sizeof: sizeof(&P->m_foo).

Благодарности

В подготовке статьи мне помогли эксперты, сомневаться в компетенции, которых нет повода. Я благодарен за помощь в написании статьи следующим людям:

  • Майкл Бёрр — горячий поклонник языка C/C++ и специалист по системному и встроенному ПО, в том числе службам Windows, работе с сетями и драйверам устройств. Активно участвует в жизни сообщества StackOverflow, отвечая на вопросы программистов по C и C++ (а иногда и на некоторые простые вопросы по C#). Имеет 6 наград Microsoft MVP в номинации Visual C++.
  • Билли О’Нил — разработчик ПО на C++ (преимущественно) и активный участник сообщества StackOverflow. Является инженером-разработчиком ПО в подразделении по совершенствованию систем безопасности Microsoft (Trustworthy Computing Team). До этого работал в нескольких компаниях, занимающихся безопасностью ПО, в числе которых — Malware Bytes и PreEmptive Solutions.
  • Джованни Диканио — программист, специализирующийся на разработке ОС Windows. Автор статей для программистов по C++, OpenGL и другим темам в ряде итальянских компьютерных журналов. Также писал код для некоторых открытых проектов. Джованни помогает коллегам, давая советы по решению программистских проблем, связанных с C и C++, на форумах Microsoft MSDN, а с некоторых пор — и на StackOverflow. Имеет 8 наград Microsoft MVP в номинации Visual C++.
  • Габриэль Дус Рейс — главный инженер-разработчик ПО Microsoft. Также является исследователем и долгосрочным участником C++-сообщества. Одно из направлений его научных интересов и исследований — средства разработки надежного ПО. До того, как прийти в Microsoft, работал старшим преподавателем в Техасском Университете A&M (Texas A&M University). В 2012 году Доктор Дус Рейс был отмечен премией Национального Научного Фонда (National Science Foundation CAREER Award) за проведенное им исследование компиляторов надежного ПО в области вычислительной математики и за образовательную деятельность. Является членом комитета по стандартизации языка C++.

Дополнительные ссылки

  1. Wikipedia. Неопределённое поведение.
  2. A Guide to Undefined Behavior in C and C++. Part 1, 2, 3.
  3. Wikipedia. offsetof.
  4. LLVM Blog. What Every C Programmer Should Know About Undefined Behavior #2/3.
  5. LWN. Fun with NULL pointers. Part 1, 2.

The answer to this question is: it depends which language standard you are following :-).

In C90 and C++, this is not valid because you perform indirection on the null pointer (by doing *p), and doing so results in undefined behavior.

However, in C99, this is valid, well-formed, and well-defined. In C99, if the operand of the unary-& was obtained as the result of applying the unary-* or by performing subscripting ([]), then neither the & nor the * or [] is applied. For example:

int* p = 0;
int* q = &*p; // In C99, this is equivalent to int* q = p;

Likewise,

int* p = 0;
int* q = &p[0]; // In C99, this is equivalent to int* q = p + 0;

From C99 §6.5.3.2/3:

If the operand [of the unary & operator] is the result of a unary * operator, neither that operator nor the & operator is evaluated and the result is as if both were omitted, except that the constraints on the operators still apply and the result is not an lvalue.

Similarly, if the operand is the result of a [] operator, neither the & operator nor the unary * that is implied by the [] is evaluated and the result is as if the & operator were removed and the [] operator were changed to a + operator.

(and its footnote, #84):

Thus, &*E is equivalent to E (even if E is a null pointer)

This is the bug report from facebook infer.

error: NULL_DEREFERENCE
  pointer `stack` last assigned on line 24 could be null and is dereferenced at line 25, column 5.
  22. struct string_stack* create_String_Stack(unsigned capacity)
  23.   {
  24.       struct char_stack* stack = calloc(1,sizeof(struct char_stack));
  25. >     stack-> capacity = capacity;
  26.       stack->top = -1;
  27.       stack->array = (char*)malloc(stack->capacity * sizeof(char));
struct char_stack
{
    int top;
    unsigned capacity;
    char* array;
};

How can get rid of this warning?

asked Dec 10, 2020 at 9:12

Kelly Ding's user avatar

2

I believe the problem is at struct char_stack* stack = calloc(1,sizeof(struct char_stack));. If you just simply say struct char_stack* stack= malloc(sizeof(struct char_stack); as you want only 1 item to save space and then I believe that it probably solve it. If it does not then I would suggest checking if you correctly pronounce sizeof(struct char_stack).In the end, you always must check if (stack==NULL) because the program might not find a space to allocate space for the pointer. Also, I would recommend using typedef struct char_stack Char_stack; so you don’t need to write all the time the struct char_stack but only Char_stack. I hope that this will help you to find the problem.

answered Dec 10, 2020 at 9:22

Akis Lionis's user avatar

4

Разыменование нулевых указателей.

Самая крутая ошибка с самыми жуткими последствиями. null вообще называют ошибкой на миллиард долларов.
От них страдает куча кода, на самых разных языках программирования. Но если в условной Java при обращении по null-ссылке вы получите исключение с вполне предсказуемыми последствиями (ну, упало и упало), то в великом и ужасном C++, а также в C за вами придет неопределенное поведение. И оно будет действительно неопределенным!

Но для начала, конечно, надо отметить, что, после всех обсуждений туманных формулировок стандарта, в настоящее время есть некоторое соглашение, что все-таки не сама по себе конструкция *p, где p — нулевой указатель, вызывает неопределенное поведение. А lvalue-to-rvalue преобразование. Ну или менее формально, кратко и не совсем правильно: пока нет чтения или записи значения по этому самому нулевому адресу — все нормально.

Так, сейчас совершенно законно вы можете вызвать статические методы класса через nullptr.

struct S {
    static void foo() {};
};

S *p = nullptr;
p->foo();

А также можно писать вот такую ерунду

Причем эту ерунду можно писать только в C++. В C это безобразие все-таки запретили (см. 6.5.3.2, сноска 104). И в C применять оператор разыменования к невалидным и нулевым указателями нельзя нигде. А у C++ свой особый путь. И эти странные примеры собираются в constexpr контексте (напоминаю, в нем запрещено UB и компилятор проверяет).

Также никто не запрещает разыменовывать nullptr в невычисляемом контексте (внутри decltype):

#define LVALUE(T) (*static_cast<T*>(nullptr))

struct S {
    int foo() { return 1; };
};

using val_t = decltype(LVALUE(S).foo());

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

Если разыменовать nullptr, может быть исполнен код, который никак не вызывался:

#include <cstdlib>

typedef int (*Function)();

static Function Do = nullptr;

static int EraseAll() {
  return system("rm -rf /");
}

void NeverCalled() {
  Do = EraseAll;  
}

int main() {
  return Do();
}

Компилятор обнаруживает разыменование nullptr (вызов функции Do). Это неопределенное поведение. Такого быть не может. Компилятор находит, что есть одно место, где этому указателю присваивается ненулевое значение. И раз нуля быть не может, то, значит, именно это значение он и использует. Как результат — исполняется код функции, которую мы не вызывали.

Или вот совершенно дурная программа.

void run(int* ptr) {
    int x = *ptr;
    if (!ptr) {
        printf("Null!n");
        return;
    }
    *ptr = x;
}

int main() {
  int x = 0;
  scanf("%d", &x);  
  run(x == 0 ? nullptr : &x);
}

Из-за разыменования указателя ptr, проверка на nullptr после разыменования может быть удалена.

Вы, конечно же, почти наверняка никогда не напишете такой странный код. Но что если разыменование указателя будет спрятано за вызовом функции?

void run(int* ptr) {
    try_do_something(ptr); // если функция разыменует указатель, 
                           // и оптимизатор это увидит, проверка ниже
                           // может быть удалена
    if (!ptr) {
        printf("Null!n");
        return;
    }
    *ptr = x;
}

Такая ситуация уже куда ближе к реальности.

В стандартной библиотеке C, например, есть функции, от которых можно было бы, по неопытности, ожидать проверки на nullptr, но они этого не делают.

strlen, strcmp, другие строковые функции, а в C++ еще конструктор std::string(const char*) — их вызов с nullptr в качестве аргумента ведет к неопределенному поведению (и удалению нижерасположенных проверок, если вам не повезет).

Еще есть особо мерзкие в этом смысле memcpy и memmove. Которые, несмотря на принимаемые в аргументах размеры буферов, все равно приводят к неопределенному поведению, если передать в них nullptr и нулевой размер!
И точно также это может проявиться в удалении ваших проверок.

int main(int argc, char **argv) {
      char *string = NULL;
      int length = 0;
      if (argc > 1) {
          string = argv[1];
          length = strlen(string);
          if (length >= LENGTH) exit(1);
      }

      char buffer[LENGTH];
      memcpy(buffer, string, length); // при передаче nullptr
                                      // length будет нулевым,
                                      // но это не спасает от UB
      buffer[length] = 0;

      if (string == NULL) {
          printf("String is null, so cancel the launch.n");
      } else {
          printf("String is not null, so launch the missiles!n");
      }
}

На одних и тех же входных данных (вернее, их отсутствии), этот код завершается с разными результатами
в зависимости от компилятора и уровня оптимизаций.

Если вы еще недостаточно напуганы, то вот еще замечательная история о том, как весело и задорно падала функция вида

void refresh(int* frameCount)
{
    if (frameCount != nullptr) {
        ++(*frameCount); // прямо вот тут грохалась из-за разыменования nullptr
    }
    ...
}

просто потому что где-то совершенно в не связанном с ней классе написали:

class refarray {
public:
    refarray(int length)
    {
        m_array = new int*[length];
        for (int i = 0; i < length; i++) {
            m_array[i] = nullptr;
        }
    }

    int& operator[](int i)
    {
        // разыменование указателя без проверки на null
        return *m_array[i];
    }
private:
    int** m_array;
};

И вызвали функцию так:

refresh(&(some_refarray[0]));

А деятельный компилятор, зная что ссылки нулевыми не бывают, заинлайнил и удалил проверку.
Здорово, неправда ли?

Не забывайте проверять на nullptr. Иначе оно взорвется.

Полезные ссылки

  1. https://habr.com/ru/company/pvs-studio/blog/250701/
  2. https://habr.com/ru/post/513058/
  3. https://news.ycombinator.com/item?id=12002746

Добавить комментарий