(Почему) использует неопределенное поведение неинициализированной переменной?


82

Если у меня есть:

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

Два вопроса:

  • Действительно ли поведение этого кода не определено?
    (Например, может произойти сбой кода [или того хуже] в совместимой системе?)

  • Если да, то почему C говорит, что поведение не определено, когда совершенно ясно, что здесь xдолжно быть ноль?

    т.е. какое преимущество дает отсутствие здесь определения поведения?

Ясно, что компилятор может просто использовать любое мусорное значение, которое он посчитает «удобным» внутри переменной, и он будет работать так, как задумано ... что не так в таком подходе?



3
Какое преимущество дает определение здесь особого случая поведения? Конечно, позволяет всем делать наши программы и библиотеки больше и медленнее, потому что @Mehrdad хочет избежать инициализации переменной в одном конкретном и редком случае.
Пол Томблин,

9
@ W'rkncacnter Я не согласен с тем, что это обман. Независимо от того, какое значение он принимает, OP ожидает, что после этого оно будет равно нулю x -= x. Вопрос в том, почему доступ к неинициализированным значениям вообще осуществляется через UB.
Mysticial

6
Интересно, что утверждение x = 0; обычно преобразуется в xor x, x в сборке. Это почти то же самое, что вы пытаетесь сделать здесь, но с xor вместо вычитания.
0xFE

1
т.е. в чем преимущество отсутствия определения поведения здесь? '- Я бы подумал, что преимущество стандарта, не перечисляющего бесконечность выражений со значениями, не зависящими от одной или нескольких переменных, очевидно. В то же время, @Paul, такое изменение стандарта не приведет к увеличению размеров программ и библиотек.
Джим Балтер

Ответы:


90

Да, это поведение не определено, но по другим причинам, о которых большинство людей не знает.

Во-первых, использование унифицированного значения само по себе не является неопределенным поведением, но значение просто неопределенно. Доступ к этому - UB, если значение является представлением ловушки для типа. Беззнаковые типы редко имеют представления прерывания, так что с этой стороны вы будете в относительной безопасности.

Неопределенным поведение делает дополнительное свойство вашей переменной, а именно то, что она «могла быть объявлена ​​с помощью register», то есть ее адрес никогда не используется. Такие переменные обрабатываются особым образом, поскольку существуют архитектуры с реальными регистрами ЦП, которые имеют своего рода дополнительное состояние, которое является «неинициализированным» и не соответствует значению в области типов.

Редактировать: соответствующая фраза стандарта 6.3.2.1p2:

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

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

  • Здесь берутся адреса aи b, поэтому их значение просто неопределенно.
  • Поскольку unsigned charникогда не было представлений прерывания, что неопределенное значение просто не указано, любое значение unsigned charмогло произойти.
  • В конце a должно быть сохранено значение 0.

Edit2: a и bимеют неопределенные значения:

3.19.3 неопределенное значение:
допустимое значение соответствующего типа, если настоящий международный стандарт не предъявляет требований к выбору значения в любом случае.


6
Возможно, я что-то упускаю, но мне кажется, что у unsigneds наверняка есть представления ловушек. Можете ли вы указать на часть стандарта, в которой это сказано? В §6.2.6.2 / 1 я вижу следующее: «Для беззнаковых целочисленных типов, отличных от unsigned char , биты представления объекта должны быть разделены на две группы: биты значения и биты заполнения (последних может не быть). ... это должно быть известно как представление значения. Значения любых битов заполнения не определены. ⁴⁴⁾ "с комментарием:" ⁴⁴⁾ Некоторые комбинации битов заполнения могут генерировать представления прерывания ".
Conio

6
Продолжая комментарий: «Некоторые комбинации битов заполнения могут генерировать представления прерывания, например, если один бит заполнения является битом четности. Несмотря на это, никакая арифметическая операция с действительными значениями не может генерировать представление прерывания, кроме как часть исключительного условия, такого как переполнение, и это не может произойти с типами без знака ". - Это здорово, если у нас есть допустимое значение для работы, но неопределенное значение может быть представлением ловушки до инициализации (например, бит четности установлен неправильно).
Conio

4
@conio Вы правы для всех типов, кроме unsigned char, но этот ответ использует unsigned char. Однако обратите внимание: строго соответствующая программа может вычислять sizeof(unsigned) * CHAR_BITи определять, основываясь на том UINT_MAX, что конкретные реализации не могут иметь представления ловушек unsigned. После того, как эта программа сделала это определение, она может продолжить делать именно то, что делает этот ответ unsigned char.

4
@JensGustedt: Разве это не memcpyотвлекает, то есть не стал бы ваш пример по-прежнему применяться, если бы его заменили на *&a = *&b;.
R .. GitHub НЕ ПОМОГАЕТ ICE

4
@R .. Я больше не уверен. В списке рассылки комитета C идет постоянное обсуждение, и кажется, что все это большой беспорядок, а именно большой разрыв между тем, что является (или было) предполагаемым поведением, и тем, что на самом деле написано. Что ясно, так это то, что доступ к памяти как unsigned charи, следовательно, memcpyпомогает, *&менее ясен. Я сообщу, когда все уляжется.
Йенс Густедт

24

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

Примечание: следующие примеры действительны только потому, xчто его адрес никогда не использовался, поэтому он «похож на регистр». Они также были бы допустимы, если бы тип xимел представления ловушек; это редко случается с беззнаковыми типами (это требует «траты» по крайней мере одного бита памяти и должно быть задокументировано) и невозможно для unsigned char. Если xбы был тип со знаком, то реализация могла бы определить битовый шаблон, который не является числом между - (2 n-1 -1) и 2 n-1 -1, в качестве представления ловушки. См . Ответ Йенса Густедта .

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

Когда строка 3 оценивается, xона еще не инициализирована, поэтому (по причинам компилятора) строка 3 должна быть какой-то случайностью, которая не может произойти из-за других условий, которые компилятор не был достаточно умен, чтобы выяснить. Поскольку zне используется после строки 4 и xне используется перед строкой 5, для обеих переменных можно использовать один и тот же регистр. Итак, эта небольшая программа скомпилирована для следующих операций с регистрами:

Конечное значение x- это конечное значение r0, а конечное значение y- это конечное значение r1. Это значения x = -3 и y = -4, а не 5 и 4, как если бы xони были правильно инициализированы.

Для более подробного примера рассмотрим следующий фрагмент кода:

Предположим, что компилятор обнаруживает, что у conditionнего нет побочного эффекта. Поскольку conditionне изменяет x, компилятор знает, что при первом прогоне цикла невозможно получить доступ, xпоскольку он еще не инициализирован. Следовательно, первое выполнение тела цикла эквивалентно x = some_value(), нет необходимости проверять условие. Компилятор может скомпилировать этот код, как если бы вы написали

Это можно смоделировать внутри компилятора, если учесть, что любое значение, зависящее от, xимеет любое удобное значение, если xоно не инициализировано. Поскольку поведение, когда неинициализированная переменная не определена, а не переменная, имеющая просто неопределенное значение, компилятору не нужно отслеживать какие-либо особые математические отношения между любыми удобными значениями. Таким образом, компилятор может проанализировать приведенный выше код следующим образом:

  • во время первой итерации цикла xне инициализируется к моменту -xоценки.
  • -x имеет неопределенное поведение, поэтому его значение не имеет значения.
  • Применяется правило оптимизации , поэтому этот код можно упростить до .condition ? value : valuecondition; value

Столкнувшись с кодом в вашем вопросе, этот же компилятор анализирует, что при x = - xоценке значение-x - это все, что удобно. Таким образом, задание можно оптимизировать.

Я не искал примера компилятора, который ведет себя так, как описано выше, но именно такой вид оптимизации пытаются делать хорошие компиляторы. Я не удивлюсь, если с ним столкнусь. Вот менее правдоподобный пример компилятора, с которым происходит сбой вашей программы. (Это может быть не так уж неправдоподобно, если вы компилируете свою программу в каком-либо расширенном режиме отладки.)

Этот гипотетический компилятор отображает каждую переменную на другой странице памяти и настраивает атрибуты страницы таким образом, чтобы чтение из неинициализированной переменной приводило к ловушке процессора, вызывающей отладчик. Любое присвоение переменной сначала гарантирует, что ее страница памяти отображается нормально. Этот компилятор не пытается выполнять какую-либо расширенную оптимизацию - он находится в режиме отладки, предназначенном для легкого обнаружения ошибок, таких как неинициализированные переменные. Когда x = - xвыполняется оценка, правая сторона вызывает прерывание и запускается отладчик.


+1 Хорошее объяснение, стандарт уделяет особое внимание этой ситуации. Продолжение этой истории см. В моем ответе ниже. (слишком долго, чтобы оставлять комментарии).
Йенс Густедт

@JensGustedt О, в вашем ответе содержится очень важный момент, который я (и другие) упустил: если тип не имеет значений прерывания, которые для беззнакового типа требуют «траты» хотя бы одного бита, xимеет неинициализированное значение, но поведение при доступе будет быть определенным, если x не имеет поведения, подобного регистру.
Жиль 'SO- перестань быть злым'

@Gilles: по крайней мере, clang делает оптимизацию, о которой вы упомянули: (1) , (2) , (3) .
Влад

1
Какая практическая польза от такого способа обработки вещей? Если нижестоящий код никогда не использует значение x, то все операции над ним могут быть опущены, независимо от того, было ли его значение определено или нет. Если следующий код, например if (volatile1) x=volatile2; ... x = (x+volatile3) & 255;, будет в равной степени удовлетворен любым значением 0-255, которое xможет содержаться в случае, когда volatile1он дал ноль, я бы подумал, что реализация, которая позволит программисту опустить ненужную запись, xдолжна рассматриваться как более качественная, чем та, которая будет вести себя ...
суперкот

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

16

Да, программа может вылететь. Например, могут быть представления прерываний (определенные битовые шаблоны, которые не могут быть обработаны), которые могут вызвать прерывание ЦП, а необработанное может привести к сбою программы.

(6.2.6.1 в последнем черновике C11 говорится) Некоторые представления объектов не обязательно должны представлять значение типа объекта. Если сохраненное значение объекта имеет такое представление и читается выражением lvalue, не имеющим символьного типа, поведение не определено. Если такое представление создается побочным эффектом, который изменяет весь или любую часть объекта выражением lvalue, не имеющим символьного типа, поведение не определено. 50) Такое представление называется представлением ловушки.

(Это объяснение применимо только к платформам, на которых unsigned intмогут быть представления ловушек, что редко в реальных системах; см. Комментарии для деталей и ссылок на альтернативные и, возможно, более общие причины, которые приводят к текущей формулировке стандарта.)


3
@VladLazarenko: Речь идет о C, а не о конкретных процессорах. Любой может банально спроектировать процессор, у которого есть битовые шаблоны для целых чисел, которые сводят его с ума. Рассмотрим ЦП, в регистрах которого есть «сумасшедший бит».
Дэвид Шварц

2
Итак, могу ли я сказать, что поведение хорошо определено в случае целых чисел и x86?

3
Что ж, теоретически у вас может быть компилятор, который решил использовать только 28-битные целые числа (на x86) и добавить конкретный код для обработки каждого сложения, умножения (и т. Д.) И гарантировать, что эти 4 бита не будут использоваться (или испустить SIGSEGV в противном случае ). Это могло быть вызвано неинитализированным значением.
eq

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

7
@Vlad Lazarenko: процессоры Itanium имеют флаг NaT (Not a Thing) для каждого целочисленного регистра. Флаг NaT используется для управления спекулятивным исполнением и может задерживаться в регистрах, которые не были должным образом инициализированы перед использованием. Чтение из такого регистра с установленным битом NaT приводит к исключению. См. Blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx
мэйнфрейм Nordic,

13

(Этот ответ относится к C 1999 г. Для C 2011 см. Ответ Йенса Густедта.)

Стандарт C не говорит, что использование значения объекта с автоматической продолжительностью хранения, которое не инициализировано, является неопределенным поведением. Стандарт C 1999 говорит в 6.7.8 10: «Если объект с автоматической продолжительностью хранения не инициализирован явно, его значение не определено». (В этом параграфе описывается, как инициализируются статические объекты, поэтому единственными неинициализированными объектами, которые нас беспокоят, являются автоматические объекты.)

3.17.2 определяет «неопределенное значение» как «либо неопределенное значение, либо представление прерывания». 3.17.3 определяет «неуказанное значение» как «действительное значение соответствующего типа, если настоящий международный стандарт не налагает требований к выбору значения в любом случае».

Таким образом, если неинициализированное значение unsigned int xимеет неопределенное значение, оно x -= xдолжно давать ноль. Остается вопрос, может ли это быть ловушкой. Доступ к значению прерывания вызывает неопределенное поведение согласно 6.2.6.1 5.

Некоторые типы объектов могут иметь представления прерывания, например, сигнальные NaN чисел с плавающей запятой. Но беззнаковые целые числа особенные. Согласно 6.2.6.2, каждый из N битов значений беззнакового int представляет степень 2, а каждая комбинация битов значений представляет одно из значений от 0 до 2 N -1. Таким образом, целые числа без знака могут иметь представление прерывания только из-за некоторых значений в их битах заполнения (например, бит четности).

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


Если xесть представление ловушки, то x -= xможет ловушка, верно? Тем не менее, +1 для указания беззнаковых целых чисел без дополнительных битов должен иметь определенное поведение - это явно противоположно другим ответам и (согласно цитате), похоже, это то, что подразумевает стандарт.
user541686

Да, если тип xимеет представление ловушки, то x -= xможет быть ловушка. Даже простое xиспользование в качестве значения может вызвать ловушку. (Это безопасно использовать xв качестве lvalue; на запись в объект не повлияет содержащееся в нем представление ловушки.)
Эрик Постпишил

беззнаковые типы редко имеют представление-ловушку
Йенс Густедт

Цитируя Раймонда Чена : «На ia64 каждый 64-битный регистр на самом деле состоит из 65 бит. Дополнительный бит называется« NaT », что означает« ничего ». Бит устанавливается, когда регистр не содержит допустимого значения. Думайте об этом как о целочисленной версии NaN с плавающей запятой. ... если у вас есть регистр, значение которого равно NaT, и вы даже дышите на него неправильно (например, пытаетесь сохранить его значение в памяти), процессор вызовет исключение STATUS_REG_NAT_CONSUMPTION ". Т.е. бит ловушки может полностью выходить за пределы значения.
Приветствия и hth. - Alf

-1 Выражение «Если на вашей целевой платформе int без знака не имеет битов заполнения, то неинициализированное int без знака не может иметь представление ловушки, и использование его значения не может вызвать неопределенное поведение». не учитывает схемы, подобные битам x64 NaT.
Приветствия и hth. - Alf

11

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

Ясно, что компилятор может просто использовать любое мусорное значение, которое он посчитает «удобным» внутри переменной, и он будет работать так, как задумано ... что не так в таком подходе?

Как вы думаете, почему этого не происходит? Это именно тот подход. Компилятор не обязан заставлять его работать, но и не заставляет его отказывать.


1
Однако компилятору не обязательно иметь для этого специальный код. Простое выделение пространства (как всегда) без инициализации переменной дает ей правильное поведение. Не думаю, что здесь нужна особая логика.
user541686

7
1) Конечно, могли. Но я не могу придумать никаких аргументов, которые улучшили бы это. 2) Платформа знает, что на значение неинициализированной памяти нельзя полагаться, поэтому ее можно изменить. Например, он может обнулить неинициализированную память в фоновом режиме, чтобы при необходимости иметь обнуленные страницы, готовые к использованию. (Подумайте, произойдет ли это: 1) Мы читаем значение для вычитания, скажем, получаем 3. 2) Страница обнуляется, потому что она не инициализирована, изменяя значение на 0. 3) Мы выполняем атомарное вычитание, выделяем страницу и делаем значение -3. Упс.)
Дэвид Шварц

2
-1 потому что вы вообще не оправдываете свои претензии. Бывают ситуации, когда можно ожидать, что компилятор просто принимает значение, записанное в ячейку памяти.
Йенс Густедт

1
@JensGustedt: Я не понимаю вашего комментария. Не могли бы вы уточнить?
Дэвид Шварц

3
Потому что вы просто утверждаете, что есть общее правило, не ссылаясь на него. Таким образом, это просто попытка «авторитетного доказательства», чего я не ожидаю от SO. И за то, что не удалось эффективно аргументировать, почему это не может быть неопределенным значением. Единственная причина, по которой это UB в общем случае, заключается в том, что xего можно объявить какregister , то есть его адрес никогда не используется. Я не знаю, знали ли вы об этом (если вы эффективно это скрывали), но правильный ответ должен упомянуть об этом.
Йенс Густедт

7

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

  • В случае, если переменная имеет автоматическую продолжительность хранения и не имеет адреса, код всегда вызывает неопределенное поведение [1].
  • В противном случае, если система поддерживает представления прерывания для данного типа переменной, код всегда вызывает неопределенное поведение [2].
  • В противном случае, если нет представлений прерываний, переменная принимает неопределенное значение. Нет гарантии, что это неопределенное значение будет постоянным при каждом чтении переменной. Однако гарантируется, что это не представление ловушки, и поэтому гарантируется, что он не вызовет неопределенное поведение [3].

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


[1]: C11 6.3.2.1:

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

[2]: C11 6.2.6.1:

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

[3] C11:

3.19.2
неопределенное значение
: неопределенное значение или представление прерывания.

3.19.3
неуказанное значение
допустимое значение соответствующего типа, если настоящий международный стандарт не предъявляет требований к выбору значения в любом случае.
ПРИМЕЧАНИЕ. Неопределенное значение не может быть представлением прерывания.

3.19.4
прерывание: представление
объекта, которое не обязательно должно представлять значение типа объекта.


3
@Vality В реальном мире 99,9999% всех компьютеров представляют собой ЦП с дополнением до двух без представления ловушек. Поэтому представление ловушек не является нормой, и обсуждение поведения на таких реальных компьютерах очень актуально. Нецелесообразно предполагать, что дико экзотические компьютеры являются нормой. Представления ловушек в реальном мире настолько редки, что наличие термина «представление ловушек» в стандарте в основном следует рассматривать как стандартный дефект, унаследованный с 1980-х годов. Так же как и поддержка компьютеров дополнений, знаков и величин.
Lundin

3
Между прочим, это отличная причина, по которой stdint.hвсегда следует использовать вместо собственных типов C. Потому что stdint.hпринудительное дополнение до 2 и отсутствие битов заполнения. Другими словами, stdint.hтипы не могут быть полны дерьма.
Lundin

2
И снова в ответе комитета на отчет о дефектах говорится: «Ответ на вопрос 2 состоит в том, что любая операция, выполняемая с неопределенными значениями, в результате будет иметь неопределенное значение». и «Ответ на вопрос 3 заключается в том, что библиотечные функции будут демонстрировать неопределенное поведение при использовании с неопределенными значениями».
Антти Хаапала

2
DRs 451 и 260
Антти Хаапала

1
@AnttiHaapala Да, я знаю об этом DR. Это не противоречит этому ответу. Вы можете получить неопределенное значение при чтении неинициализированной области памяти, и это не обязательно всегда одно и то же значение. Но это неопределенное поведение, а не неопределенное поведение.
Лундин

2

Хотя во многих ответах основное внимание уделяется процессорам, которые блокируют доступ к неинициализированному регистру, странное поведение может возникнуть даже на платформах, которые не имеют таких ловушек, при использовании компиляторов, которые не прилагают особых усилий для использования UB. Рассмотрим код:

компилятор для платформы, подобной ARM, где все инструкции, кроме загрузки и сохранения, работают с 32-битными регистрами, может разумно обрабатывать код способом, эквивалентным:

Если любое из непостоянных чтений дает ненулевое значение, r0 будет загружено со значением в диапазоне 0 ... 65535. В противном случае он выдаст то, что содержалось при вызове функции (то есть значение, переданное в x), которое может не быть значением в диапазоне 0..65535. В Стандарте отсутствует какая-либо терминология для описания поведения значения, тип которого - uint16_t, но значение которого выходит за пределы диапазона 0..65535, за исключением того, что сказано, что любое действие, которое может вызвать такое поведение, вызывает UB.


Интересно. Так вы говорите, что принятый ответ неверен? Или вы говорите, что это верно в теории, но на практике компиляторы могут делать более странные вещи?
user541686 08

@Mehrdad: Обычно реализации имеют поведение, выходящее за рамки того, что было бы возможно в отсутствие UB. Я думаю, что было бы полезно, если бы Стандарт признал концепцию частично неопределенного значения, чьи «выделенные» биты будут вести себя так, как в худшем случае, неопределенным, но с дополнительными старшими битами, которые ведут себя недетерминированно (например, если результат вышеупомянутой функции сохраняется в переменной типа uint16_t, эта переменная иногда может читаться как 123, а иногда как 6553623). Если результат будет проигнорирован ...
supercat 08

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

Мне кажется, что то, что вы описываете, - это именно то, что содержится в принятом ответе - что если переменная могла быть объявлена ​​с помощью register, то у нее могут быть дополнительные биты, которые делают поведение потенциально неопределенным. Это именно то, что вы говорите, верно?
user541686 08

@Mehrdad: принятый ответ фокусируется на архитектурах, регистры которых имеют дополнительное «неинициализированное» состояние, и перехватывают, если загружен неинициализированный регистр. Такие архитектуры существуют, но не являются обычным явлением. Я описываю сценарий, в котором обычное оборудование может демонстрировать поведение, выходящее за рамки того, что предусмотрено стандартом C, но будет полезно ограничено, если компилятор не добавит в смесь своего собственного дополнительного дурака. Например, если функция имеет параметр, который выбирает операцию для выполнения, и некоторые операции возвращают полезные данные, а другие - нет, ...
supercat
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.