Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Новости и прочее

О неправомерном доступе к памяти через указатели

Указатели в Си справедливо критикуют за «вседозволенность»: они могут принимать любые значения. А вот являются ли эти значения правильными — это мы нередко узнаём только во время исполнения программы, что приносит неприятные новости. Макс Шлее, «Qt 4.5 Профессиональное программирование на C++», стр. 83:

Итератор в стиле STL … можно представить как некий обобщённый указатель, ссылающийся на элементы контейнера. … вызов метода end() возвращает итератор, указывающий на конец контейнера. Обратите внимание: именно на конец контейнера, а не на последний элемент, т. е. на позицию, на которой мог бы быть помещён следующий элемент. Другими словами, этот итератор не указывает на элемент, а служит только для обозначения достижения конца контейнера.

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

Ах, если бы метод end() только сигнализировал об окончании цикла. Но он выдаёт указатель на участок памяти, который не является контейнером. А это — классический несанкционированный доступ. Если бы процессор, на котором исполнялся метод end(), имел бы аппаратную защиту, то она бы сработала. С помощью этого указателя в участок памяти, который прилегает к контейнеру, можно что-то записать, испортив «чужие» объекты памяти. Или прочитать их, что может, например, нарушить конфиденциальность. Это очень логично: если метод end() или любой другой выдаёт какой-то указатель, то значит этому указателю надо априори доверять. Выходит, что обращение по этому указателю вполне законно, что на самом деле не так.

Проектируя язык для надёжного программирования, мы обязаны исключить практику использования незаконных адресов. Назначение таких адресов — просигнализировать о наступлении какого-то события. Но сигналы можно подать другими, безопасными способами (см. ненадёжные функции в статье «Обработка ошибок»).

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

Вот образец этой провокации с той же страницы того же издания:

Qvector vec;
vec << "one" << "two" << "three";
Qvector::iterator it = vec.begin();
Qvector::iterator it = vec.end();
for (; it != vec.begin();) {
	--it;
	qDebug() << "Element: " << *it;
}

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

О ссылках и указателях

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

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

Тема ссылок и указателей весьма ёмка и об этом в дальнейшем будет отдельный разговор.

Опубликовано: 2022.03.29, последняя правка: 2023.01.31    23:07

ОценитеОценки посетителей
   ███████████ 3 (25%)
   ███████ 2 (16.6%)
   ███████ 2 (16.6%)
   ██████████████████ 5 (41.6%)

Отзывы

     2024/02/08 22:03, void          # 

Например, в языке Ада есть как указатели: Var'Address, так и типизированные ссылки: Var'Access. Где проверка типизации 'Access, в идеале, происходит во время компиляции. Для ссылок — запрещён alias'инг переменных как областей памяти по умолчанию. В указателях 'Address и указателях и переменных Си — по умолчанию разрешён.

Вообще, это пример неадекватности, непродуманности изначальной концепции, например, Си и Си++. В котором тоже получаются сначала *ptr, потом &ref, потом всякие прочие смартпоинтеры, D-ptr для бинарной совместимости, виртуальные методы или там указатели на член класса VMTABLE/VPTR, copy/move семантика, RAII ну и т.п. Или например, Rust с его Borrow Checker-ом, и контролем времени жизни. Или например, типобезопасный BitC из EROS. Или например, язык Pony с семантикой акторов и различными (5!) типами ссылок reference-capabilities iso/trn/ref/val/box/tag:

https://tutorial.ponylang.io/reference-capabilities/reference-capabilities.html В базовом Си эта, на мой взгляд, неадекватность указателей тянется ещё со времен его создания и K&R C. С самого момента появления: C это улучшенный B который улучшенный BCPL, CPL — в котором был всё тот же фундаментальный недостаток, указатели стирали тип и разные указатели на разные объекты разных типов были aliasing, то есть, совместимы по присваиванию. В K&R Си если не описали например, тип по умолчанию — то он подразумевался как int. То есть, неявно неописанная переменная приводилась к int. Который чисто случайно оказался сравним: sizeof(int) = sizeof(*int)=... В С++ вместо того, чтобы сделать нормально, как в Ада, ещё усугубили, добавив к указателям ещё и ссылки и прочие шаблоны с концептами, смартпоинтеры, virtual, move/copy, lvalue и т.п. В общем, вместо того чтобы сделать нормальный typeinfo как атрибуты в Ada или как RTTI информация из модуля в D для CTFE — ещё более усугубили изначально дырявую концепцию из базового Си и BCPL.

Цирк с понями и велосипеды c квадратными колёсами — это на мой взгляд, всё же несколько перебор. Более правильно тут было сделано в Ada, D, BitC — на мой взгляд. BitC и Cyclone:

https://ru.wikipedia.org/wiki/Cyclone_ (%D1%8F%D0%B7 %D1%8B%D0%BA_ %D0%BF %D1%80%D0%BE %D0%B3%D1%80 %D0%B0 %D0%BC%D0%BC %D0%B8 %D1%80%D0%BE %D0%B2 %D0%B0%D0%BD %D0%B8 %D1%8F)

"Программист может полностью контролировать процесс согласования типов."

     2024/02/12 13:48, veector          # 

Подскажите пожалуйста, а какая проблема поднята в статье? Ведь когда программист выбрал, язык на котором будет делать программу, он же отчетливо осознает, каким инструментом будет пользоваться. Можно же программы делать и в машинных кодах, и на ассемблере, и на Си, и на питоне, и на java. В приведенной цитате из "Профессиональное программирование на C++", просто дается предупреждение, как мера предосторожности, применительно к языку программирования. Это же не является ни недостатком, ни достоинством, а особенностью того, что С/С++ приближены к информационной модели адресного пространства, которым оперирует процессор при исполнении приложения.

Так-то и вилкой можно пораниться, но без вилок же никуда.

Адрес — это номер байта в основном адресном пространстве процессора. Чисто технически, в адресном пространстве располагается много чего, не только ОЗУ.

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

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

     2024/02/12 22:43, Автор сайта          # 

какая проблема поднята в статье?

Обсуждается проблема, которая вынесена в заголовок статьи. Если язык программирования допускает возможность совершения какой-то ошибки, то она рано и поздно будет совершена.

просто дается предупреждение, как мера предосторожности

А хотелось бы, чтобы не книга предупреждала, а компилятор выдавал сообщение об ошибке: «попытка неправомерного доступа».

Это же не является ... недостатком

Раз это провоцирует совершение ошибок, то значит это недостатком всё-таки является. У каждого языка они свои особенности. Да, Си разрешает доступ по произвольному адресу. Это острый нож, которым легко пораниться. Но при должной проработке можно сделать инструменты с возможностью решать те же задачи, только безопасно. При этом цена решения будет либо нулевая, либо минимальная.

Адрес — это номер байта в основном адресном пространстве процессора.
Указатель — это переменная в памяти, наделенная единственным смыслом, хранить номер байта в адресном пространстве.

Программа в машинных кодах имеет всего две сущности: данные и машинные коды, которые ими пользуются. Данные и коды могут быть как правильными, так и неправильными. Их правильность зависит (помимо программиста и технического задания) от языка и компилятора. Если язык не допускает использование неинициализированных переменных, значит данные в двоичном коде после компилятора тоже будут инициализированы. Если ссылка в C++ инициализируется единожды и является адресом существующего объекта, то и в двоичном коде будет порядок. Если в языке нет goto, то и нельзя выполнить переход по неправильному адресу. Если адреса участков памяти передаются от одного владельца другому, то и потерять этот адрес нельзя.

Целью является обеспечение корректности адресов при генерации машинного кода.

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

Если Вы посмотрите, какой ассемблерный код генерируется для ссылок и указателей при вызове функций, то будете удивлены: он одинаков в обоих случаях. Я даже делал эксперименты: за счёт приведения типов подменял адреса вызываемых функций
void f1 (int*) ...
void f2 (int&) ...
и вызывал одну вместо другой. Функция f2 запросто работает вместо f1, а f1 — вместо f2.

значения ссылок внутри С++ функций нельзя изменить чисто технически

Можно, просто Вы были недостаточно упорны и изобретательны: приведение типов никто не отменял. Объединения тоже. И не будут отменять, потому что поддерживается обратная совместимость. Да, компилятор обеспечивает контроль, но это «защита от честных людей». Это всё преодолевается.

     2024/02/13 09:32, veector          # 

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

     2024/02/13 12:11, veector          # 

Если Вы посмотрите, какой ассемблерный код генерируется для ссылок и указателей при вызове функций, то будете удивлены: он одинаков в обоих случаях.

Например, если брать gcc x86 (я использовал mingw в составе code::blocks), то отличия начинаются в более сложных функциях. Дело в том, что в gcc принято передавать все параметры через стек, поэтому, ваши простые функции имеют одинаковый "прототип" на ассемблере и машинных кодах, т.к. gcc нечего "сокращать" используя логику ссылок, по сравнению с логикой указателей. Но, когда появляются более сложные конструкции, тогда применение ссылок начинает влиять на код.

Пример, в котором бинарный/ассемблерный коды функций F1() и F2() отличаются, также отличаются и их "прототипы" вызова в машинных кодах.
#include <iostream>

using namespace std;

typedef int& IntRef_t;
typedef int* IntPtr_t;

void F1(IntPtr_t* IntPtrPtr)
{
int IntValue = ++(**IntPtrPtr);
printf("F1 = %i\n", IntValue);
}

void F2(IntRef_t& IntRefRef)
{
int IntValue = ++IntRefRef;
printf("F2 = %i\n", IntValue);
}

int main()
{
int D[2] = {0, 0};
IntPtr_t IntPtr;

IntPtr = &D[0];
F1(&IntPtr);
IntRef_t IntRef1 = D[1];
F2(IntRef1);

printf("D[0] = %i, D[1] = %i\n", D[0], D[1]);
return 0;
}
Еще хотел уточнить, что собирать пример стоит с максимально отключенной оптимизацией. Иногда, программисты воспринимают сокращение кода из-за применения ссылок как оптимизацию, хотя это не так. Ссылки позволяют компилятору делать оптимальный код ещё на уровне непосредственной интерпретации кода программы, до начала работы различных механизмов оптимизации.

     2024/02/13 23:02, Автор сайта          # 

Давайте разложим по полочкам.

1). Можно ли забыть проинициализировать переменную в программе на языке ассемблера (фактически в машинном коде)? Да, можно. А в Си/C++? Тоже можно. Совпадают ли в этом их модели? Совпадают. Хорошо ли это или плохо? Возможность допустить чтение неинициализированной переменной есть у обоих языков, поэтому повторение языком Си модели поведения процессора — не есть хорошо.

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

2). Ссылка в C++ тоже не делает машинный код длиннее. В этом у неё нет разницы с ассемблером. Она инициализируется единожды и адресом существующего объекта. По этой причине возможность ошибиться в программах на C++ меньше, чем в ассемблерных программах. В этом случае информационная модель работы с адресным пространством у C++ не совпадает с процессорной моделью:
процессорная модель позволяет иметь любой адрес, а C++ — адрес только существующего объекта. Тут тоже имеет место быть благотворное ограничение, которое не сказывается на скорости работы программ.

3) Информационная модель процессора позволяет сделать переход по любому адресу, хоть по адресу данных, хоть по адресу команды внутрь цикла. А Си накладывает ограничения: нельзя с помощью goto перейти на переменную или запрыгнуть внутрь цикла. Выпрыгнуть из цикла — пожалуйста, а внутрь — ни-ни. Тут видна разница между моделями Си и процессора. Страдает ли от этого производительность программ? Нет, абсолютно не страдает. Но в этом случае меньше ошибок и это благотворное ограничение.

Для своего времени и Си был хорош, и Паскаль. Но с момента их появления прошло более полувека. Человечество накопило опыт программирования, что привело к принятию полезных приёмов и отбраковке плохих.

там все четко и по делу

Да нет же, «ляпов» полно. Тоже висячий «else» есть что в Си, что в Паскале. Ну а неопределённое поведение в Си — это вообще отдельная песня. Откуда взяться чёткости? Кстати, и тут видна разница в модели поведения Си и процессора. Процессор выставляет флаг переполнения, а Си это игнорирует.

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

Просто мы не пересекались. У этих разработчики не было машины времени, чтобы подсмотреть накопленный на сегодня опыт. Но мы-то знаем, что изменилось за полвека, прогресс идёт во всех сферах жизни, и в программировании тоже. Но и у нас тоже нет машины времени, чтобы вернуться и исправить Си и Паскаль. Поэтому не трогаем эти языки, просто делаем новые.

     2024/02/15 13:22, Деньги на WWWетер          # 

разработчики языков C/C++ (а так же Pascal, который Borland) преследовали другие цели... Лично я разделяю их цели целиком и полностью и считаю, что никаких логических противоречий, по крайней мере в Си, не допущено

Попробую угадать: veector до сих пор под ДОСом работает. И цели разработчиков ДОС разделяет, потому что не допустили логических противоречий.

     2024/02/15 15:23, veector          # 

Гадать не нужно, можно просто спросить, а правильно заданный вопрос — это уже половина ответа ;)
И нет, ДОС-ом уже давным-давно давным-давно не пользуюсь.

     2024/02/15 16:21, veector          # 

Поэтому не трогаем эти языки, просто делаем новые.

Уважаемый Автор сайта, это замечательно, что вы занимаетесь разработкой языков и освещаете эту тему, т.к. универсальных языков нет.

На условном lor-е или habr-е подобные темы, про указатели, неинициализированные переменные и т.п. в Си, появляются с завидной периодичностью. Да и бог бы с ними, но вот на данном ресурсе, прям неожиданно, что и заставило меня вам ответить.

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

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

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

Давайте разложим по полочкам.

А, давайте.

1) Можно ли забыть проинициализировать ...

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

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

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

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

Влияет и очень существенно. Например, на скорость работы программы, что особенно заметно на большом объеме обрабатываемых данных (это не обязательно большие массивы, а может быть очень много маленьких). Еще влияет на логику работы с памятью в адресном пространстве, тут целых три, на мой взгляд, важных нюанса:
  • Шаренная память между приложениями для общих переменных у разных программ. Тут не то, что бы инициализировать надо правильно, а для начала надо знать правила работы с каждой переменной.
  • В адресном пространстве располагается не только ОЗУ, но и другие участники, например, звуковые карты, видео адаптеры и т.д. и т.п.. А ещё есть мультипроцессорные системы даже в рамках одной микросхемы.
  • Нет никакого смысла инициализировать данные кучи или программного стека, т.к. по технологии виртуализации для них операционная система ещё даже не подставила реальное ОЗУ. Ибо, например, реальное ОЗУ подставится в момент первого обращения к данному адресному пространству. Что, кстати, очень сильно зависит от операционки и её стратегии работы с памятью, включая настройки ядра. Например, в ОС на базе ядра Linux, по умолчанию вызов malloc() реально память не выделяет, а лишь обозначает намерение программы воспользоваться участком адресного пространства для целей работы его как ОЗУ.

2) Ссылка в C++ тоже не делает машинный код длиннее.

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

3) Информационная модель процессора позволяет сделать переход по любому адресу ... А Си накладывает ограничения: нельзя с помощью goto перейти на переменную или запрыгнуть внутрь цикла. ...

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

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

Да нет же, «ляпов» полно. Тоже висячий «else» есть что в Си, что в Паскале. Ну а неопределённое поведение в Си — это вообще отдельная песня.

Да мне тоже далеко не всё нравится в том же Си, но это как бы не очень касается темы статьи про адресное пространство и указатели, а держать ответ за весь язык в целом, я не готов.

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

     2024/02/16 00:00, alextretyak          # 

Поддерживаю автора сайта. Метод end(), который возвращает "фейковый" итератор (который запрещено разыменовывать) — это вообще какой-то бред. Проблема усугубляется тем, что по стандарту должно быть возможно через −−end() получить итератор на последний элемент контейнера. А если кто пробовал самостоятельно реализовать двусвязный список, тот знает, что в последнем узле списка в поле next_node удобно хранить просто нулевой указатель, который и является признаком окончания списка. Но если следовать стандарту C++, то end() не может возвращать просто нулевой итератор, т.к. к нему невозможно применить операцию ‘−−’. Из-за этого приходится извращаться с каким-нибудь sentinel node, который например в реализации от Microsoft выделяется динамически (https://devblogs.microsoft.com/oldnewthing/20230804-00/?p=108547) в конструкторе списка, что приводит к нехорошим последствиям: к примеру, массив из миллиона пустых std::list-ов потребует миллион вызовов оператора new для выделения памяти под sentinel nodes.

Хотя, насчёт невозможности применить операцию ‘−−’ к нулевому итератору я несколько преувеличил: в итераторе можно хранить не только указатель на узел, но ещё и указатель на сам объект-список. Но всё равно получается излишнее усложнение реализации для соответствия принятому стандарту.

И даже сама терминология итераторов в C++ абсолютно бестолковая. О чём красноречиво говорит такой факт, что итераторов в стиле C++ больше нет ни в одном языке программирования. Мне нравится, как итераторы реализованы в Python и почти также в Rust. У контейнеров есть метод iter(), который возвращает итератор по этому контейнеру. У итератора есть метод next(), который выполняет переход к следующему элементу итерируемого контейнера и как-то сигнализирует о том, что элементов в контейнере больше нет (т.е. текущий элемент был последний) — в Python это делается через raise StopIteration, в Rust через возврат None.

На этом слайде (https://youtu.be/d3qY4dZ2r4w?si=z0-QayzWZ4MbBEhY&t=2674) презентации есть сравнительная таблица итераторов в C++, D, C#, Rust, Python и Java. Как видно из этой таблицы, лишней сущности last (точнее after_last, он же end) нет ни в D, ни в C#, ни в Rust, ни в Java, ни в Python. И только C++ идёт своим особым путём, трактуя понятие "итератор" как навороченный указатель. Во всех нормальных остальных языках программирования под итератором понимается сущность, которая обеспечивает возможность обхода контейнера или какого-либо итерируемого объекта.

veector

Пример, в котором бинарный/ассемблерный коды функций F1() и F2() отличаются, также отличаются и их "прототипы" вызова в машинных кодах.

Ваш пример некорректен. Функция F2 у вас на самом деле принимает обычную одиночную ссылку на int. Т.к. в данном случае имеет место т.н. "reference collapsing", а именно срабатывает правило ‘‘T& & → T&’’ (https://leimao.github.io/blog/CPP-Reference-Collapsing/). В этом легко убедиться, если попытаться добавить перегруженную функцию void F2(IntRef_t IntRef) — в этом случае компилятор выдаст ошибку "C2084: function 'void F2(IntRef_t)' already has a body".

Дело в том, что в C++ нет такого понятия, как ссылка на ссылку, поэтому в C++11 для обозначения нового типа ссылок (rvalue references) ввели запись &&, т.к. в С++03 она не имела смысла, и ваш пример, кстати, не скомпилируется с опцией -std=c++03 — (https://godbolt.org/z/c9ddPsqz3), т.к. "reference collapsing" появился только в C++11.

А функция F1 принимает двойной указатель (т.е. указатель на указатель на int). Если это исправить (https://godbolt.org/z/j9GzMqbdo), то генерируемый ассемблерный/машинный код для F1 и F2 будет полностью совпадать (за исключением адреса строкового литерала "F2 = %i\n".

Автор сайта

А Си накладывает ограничения: нельзя с помощью goto перейти на переменную или запрыгнуть внутрь цикла. Выпрыгнуть из цикла — пожалуйста, а внутрь — ни-ни.

Вообще-то, в Си можно запрыгнуть внутрь цикла. :)(: Вот пример:

https://godbolt.org/z/4xqeM7Kxq.

И даже в C++ можно (https://godbolt.org/z/je7nz8c91). При условии, что goto не пропускает объявления переменных. Но, разумеется, делать так не следует — оба приведённых мной примера являются UB и компилятор выдаёт предупреждение об использовании неинициализированной переменной.

     2024/02/16 09:38, veector          # 

alextretyak, все так. Я выбрал тот стандарт и то свойство, которые были удобны для демонстрации.

     2024/02/16 09:49, veector          # 

alextretyak, я не знаком с Rust, но у меня есть к вам вопрос по теме итераторов.

У контейнеров есть метод iter(), который возвращает итератор по этому контейнеру. У итератора есть метод next(), который выполняет переход к следующему элементу итерируемого контейнера и как-то сигнализирует о том, что элементов в контейнере больше нет (т.е. текущий элемент был последний)

Зачастую, нужно несколько одновременно работающих итераторов с одной таблицей. Из-за этого, приходится делать отдельные объекты типа итератор и создавать их столько раз, сколько нужно. Вот этот метод iter() у контейнеров, он позволяет сделать несколько разных итераторов, работающих независимо, или это посто один итератор встроенный в класс контейнера?

     2024/02/16 22:08, Автор сайта          # 

veector

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

Первые два нюанса действительно особый случай. Настолько особый, что и дисциплина обращений к такой памяти особая. Чтение такой памяти привносит недетерминизм, то есть непредопределённость, по-русски говоря. А запись в такую память имеет побочные эффекты. Соответственно функции, читающие из такой памяти, обладают недетерминизмом, а записывающие обладают побочным эффектом.

А вот с третьим перечисленным Вами нюансом я не согласен. В чём смысл вывода типов? Фишка в том, что переменная не создаётся то тех пор, пока в неё что-то не записали. Ведь это правильно — читать только тогда, когда в неё что-то записали. А запись — это вычисление значение этой переменной — до того, как она понадобилась для чтения. Поэтому неважно, где такая переменная находится: в статической ли памяти, в стеке или куче. Важно, что чтению должна предшествовать запись, а если первым всё-таки идёт чтение, то имеем непредсказуемое значение.

А если переменная не записывалась и не читалась, то она просто не нужна. Компилятор языка, где нет вывода типов, должен потрудиться, чтобы выявить такие переменные и выдать предупреждение. А вывод типов просто на автомате не создаёт таких переменных. Компилятор упрощается: ему не надо искать неиспользуемые переменные.

Третий Ваш нюанс имеет совсем другой нюанс. Если ОС выделяет программе физическую память, как она есть, то чтение неинициализированной памяти помогает хакерам узнавать всякие пароли, ключи шифрования и прочие чувствительные вещи. Поэтому современные ОС выделяемую память прописывают чем-нибудь, чаще нулями. Это замедляет работу, но повышает безопасность.

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

Вообще-то, в Си можно запрыгнуть внутрь цикла.

Надо попробовать. А то прочитал когда-то, принял это для себя как аксиому. А может, аксиома касалась не Си, а другого языка? Запросто могло так быть.

Все жалуются на свою память, но никто не жалуется на свой ум © Франсуа де Ларошфуко

     2024/02/17 00:00, alextretyak          # 

veector

т.к. универсальных языков нет.

Я бы уточнил: «на данный момент/пока ещё нет». Но это не означает, что такой язык невозможно создать в принципе. И работы в этом направлении активно ведутся: Mojo, daslang.org. (Это примеры из совсем новых языков, хотя, разумеется, попытки создания универсальных языков были и раньше.)

Собственно, и я тоже занимаюсь разработкой такого языка. Вот моя последняя статья на эту тему: https://habr.com/ru/articles/784294/ Хотя она больше про компилятор, но в конце есть немного и про язык.

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

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

А разве бывают решаемые человеком задачи, в которых было бы допустимо использование неинициализированных переменных? Неинициализированными можно оставить только такие переменные, к которым вообще не происходит обращения, что случается довольно редко.

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

Нормальное решение, которого придерживаются практически все современные языки программирования. И во многих реализована оптимизация удаления кода "излишних" инициализаций (например, в int i = 0; i = 1; присваивание нулю будет отброшено).

К тому же, инициализация бывает разная. Например, в таком коде:
int i;
if (flag)
i = 1;
else
i = 2;
printf("%i", i);
переменную i можно считать корректно проинициализированной. Суть в том, чтобы компилятор делал проверку того, что на всех возможных путях выполнения кода переменная инициализируется перед тем, как она используется.

Заметьте, что хотя C++ допускает неинициализированные переменные на уровне языка, некоторые компиляторы C++ (Clang, MSVC) выдают предупреждение об использовании потенциально неинициализированных переменных. Моя позиция (и, насколько я понимаю, позиция автора сайта) заключается в том, чтобы это предупреждение сделать ошибкой компиляции.

Ибо, например, реальное ОЗУ подставится в момент первого обращения к данному адресному пространству.

И что же будет находиться по этому адресному пространству? Какой-то неизвестный мусор? Память, заполненная какими-то данными от других процессов (в которой могут оказаться пароли, ключи шифрования и прочее)? Ничего подобного. Практически все операционные системы обеспечивают защиту от возможности получения мусорных страниц памяти от других процессов путём их зануления. Да, фактически такое зануление не выполняется сразу же. Можно запросить 1 гигабайт памяти и не опасаться того, что операционная система будет забивать этот гигабайт нулями немедленно.

Но! Можно полагаться на тот факт, что при первом обращении к любой странице памяти из запрошенного гигабайта, содержимое этой страницы памяти будет гарантированно обнулено. Таким образом, хотя реально в выделенном блоке памяти находится (точнее будет находиться, т.к. физические страницы назначаются по мере обращения к виртуальным) какой-то мусор (возможно, с информацией из других процессов), но виртуально запрошенный блок памяти уже полностью проинициализирован нулями. И этим можно пользоваться. Вот пример такого использования:

https://fortran-lang.discourse.group/t/why-is-this-fortran-code-so-much-faster-than-its-c-counterpart/3746

Там человек спрашивает, почему одинаковый по смыслу код в Фортране работает намного быстрее, чем в C++. Обратите внимание на строку allocate(x(n), source = 0d0).

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

Но компилятор C++ не может повторить такой трюк, т.к. контейнеры STL не обращаются к malloc напрямую, а используют глобальный operator new, который может быть переопределён, а потому, полагаться на то, что полученная им память будет заполнена нулями, не получится.

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

С большими массивами, которые имеет смысл оставлять неинициализированными, программист имеет дело очень редко. В остальном же коде наличие неинициализированных переменных приводит (либо может привести) к ошибкам в работе программы, и от этих ошибок, в первую очередь, и должен защищать компилятор. А когда действительно нужен непроинициализированный массив, можно использовать специальные средства в языке программирования: в языке D это uninitializedArray (https://dlang.org/library/std/array/uninitialized_array.html), а также void initializers (https://dlang.org/spec/declaration.html#void_init). В языке C++20 это std::make_unique_for_overwrite, а также предлагаемый string::resize_and_overwrite.

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

Во-первых, я не очень понимаю, что вы имеете в виду под таблицей. Хеш-таблицу? Просто "таблицы" есть, насколько мне известно, только в языке Lua и являются гибридом хэш-таблицы и массива.

Во-вторых, давайте попробуем устранить путаницу в терминологии, которую внёс C++. То, что в C++ принято называть итератором, в других языках называют чаще курсором. И работают с ним несколько по-другому. А итератор лишь обеспечивает возможность обхода итерируемого объекта. Его можно получить напрямую у контейнера методом iter(). В этом случае будет производиться обход по всем элементам этого контейнера. Его итератор возвращают методы наподобие reversed() (позволяет обойти все элементы контейнера в обратном порядке), filter() (обходит только элементы, удовлетворяющие определённому условию) и т.п.

Вот этот метод iter() у контейнеров, он позволяет сделать несколько разных итераторов, работающих независимо, или это посто один итератор встроенный в класс контейнера?

Каждый вызов метода iter() создаёт новый независимый объект-итератор. В документации (https://doc.rust-lang.org/book/ch13-02-iterators.html#the-iterator-trait-and-the-next-method) приводится пример "ручной" работы с итератором. А вот пример получения двух независимых итераторов от одного контейнера и такой же "ручной" работы с ними:
fn main() {
let v1 = vec![1, 2, 3];

let mut v1_iter = v1.iter();
let mut v2_iter = v1.iter();

print!("{}", v1_iter.next().unwrap());
print!("{}", v1_iter.next().unwrap());
print!("{}", v2_iter.next().unwrap());
}
Но вообще-то, так итераторы обычно не используют. Методы iter() и next() нужны не для явного вызывания, а для неявного:
fn main() {
let v1 = vec![1, 2, 3];

for val in v1 { // методы iter() и next() вызываются компилятором неявно
println!("Got: {}", val);
}
}

     2024/02/18 11:00, Автор сайта          # 

Ваш код
int i;
if (flag)
i = 1;
else
i = 2;
printf("%i", i);
я бы написал по-другому. Вывод типов, а следовательно и объявление переменной, и её инициализация, может делаться и условным выражением, что лаконичнее и элегантнее (последнее зависит от вкусов).
а = (если б < в
0
иначе
1.0)
или
а = (если б < в; 0; иначе 1.0)
И не только условным выражением, но и переключателем. При этом выбирается:
  • такой тип, который исключает искажение значения,
  • достаточный тип, то есть наименьший из возможных, которые не искажают значение.
Если же в ветвях условного выражения встречаются несовместимые типы, то это ошибка.

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

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

     2024/02/18 15:04, Деньги на WWWетер          # 

А как бы этот код выглядел, если его записать в стиле Питона?
а = (если б < в
0
иначе
1.0)

     2024/02/19 14:25, veector          # 

alextretyak
Спасибо за пояснения про итераторы в Rust, все понятно, вопросов нет, сделано хорошо.

Каждый вызов метода iter() создаёт новый независимый объект-итератор.

По поводу "инициализации", вы же сами и написали, что "К тому же, инициализация бывает разная. Например, в таком коде:" — как вы это будете "занулять" на автомате? Я уже не говорю про переменную flag, значение которой, по вашей с Автором сайта логики автоматической инициализации, тоже ведь кто-то когда-то должен быть записать.

И что же будет находиться по этому адресному пространству? Какой-то неизвестный мусор?

Как я уже замечал ранее, современная тенденция в разработке аппаратуры — проецировать в адресное пространство процессоров не только ОЗУ, а вообще все на свете. Например, там может быть ПЗУ со значением серийного номера процессора или MAC-адресом для встроенной сетевой карты по умолчанию. Применений очень много, и если вы с ними не сталкиваетесь, то не значит, что этого нет.

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

     2024/02/19 14:33, veector          # 

По поводу "автозануления" ОЗУ операционными системами, скажу так, что когда на кону стоит скорость обработки, то "автозануление" не используется. Ну и зависит это от много, включая от настроек ядер операционных систем, вот тут как раз об этом же и написали в самом конце: https://fortran-lang.discourse.group/t/why-is-this-fortran-code-so-much-faster-than-its-c-counterpart/3746/12

In this particular case, the system calloc implementation did better than C++'s operator new + memset. But as others have shown, you cannot generalize this to all platforms (OS’s, architectures).

     2024/02/19 18:41, Автор сайта          # 

современная тенденция в разработке аппаратуры — проецировать в адресное пространство процессоров не только ОЗУ, а вообще все на свете. Например, там может быть ПЗУ со значением серийного номера процессора или MAC-адресом для встроенной сетевой карты по умолчанию. Применений очень много, и если вы с ними не сталкиваетесь, то не значит, что этого нет.

Именно поэтому я написал выше:

Чтение такой памяти привносит недетерминизм, то есть непредопределённость, по-русски говоря. А запись в такую память имеет побочные эффекты. Соответственно функции, читающие из такой памяти, обладают недетерминизмом, а записывающие обладают побочным эффектом.

Ничего в этом удивительного нет. Многие познакомились с этим вместе со знакомством с IBM PC (порты вводы/вывода, к примеру). Но и до них это было, например у Wang-700. При этом не имеет принципиального значения, кто привносит недетерминизм и побочные эффекты — оборудование, операционная система или параллельный поток вашей же программы. Просто у оборудования адреса таких участков памяти фиксированные.

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

Во-первых, будучи в здравом уме, человек может оказаться слабым и склонным к ошибкам. Совершенно непреднамеренно. Если Вы в этом сомневаетесь, то почитайте, какие ошибки отыскивают статические анализаторы кода. Весьма живописно. Во-вторых, это может происходить преднамеренно, со злым или исследовательским умыслом, чтобы прочитать старое содержимое памяти. Но об этом тоже уже писалось.

     2024/02/20 00:00, alextretyak          # 

Автор сайта

я бы написал по-другому. Вывод типов

Да, вывод типов, а также выражение «если-иначе» — это всё хорошо, но я хотел сделать акцент на другом. А именно, на случаях очень сложных инициализаций.
Допустим нам необходимо посчитать число n, которое в дальнейшем будет использоваться как количество итераций некоторого цикла. Если написать так:
auto n = 0;
if (flag1) {
if (flag2) {
n = 1;
}
else {
n = 2;
}
if (flag3)
n++;
}
else {
if (flag4) {
n = 3;
}
else {
... // здесь забыли проинициализировать `n`
}
}
// ... `n` используется где-то ниже
то в случае, когда выполнение кода пойдёт по пути, в котором n не инициализируется, в n окажется начальное значение 0, и при этом возможно возникновение очень трудно уловимой ошибки.

Если же вместо auto n = 0; написать int n; [я использую здесь синтаксис C++ просто чтоб было понятнее], то это будет предписывать компилятору, что n инициализируется где-то дальше, и компилятор должен проверить, что n действительно инициализируется перед использованием (ведь с записью auto n = 0; такую проверку компилятор делать не будет). Так программист может написать int n; в случае, когда ему необходимо проверить, что n инициализируется на всех путях выполнения. Язык с более продвинутым выводом типов может разрешить даже запись auto n; вместо int n;.

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

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

Увы, но ОС вынуждена всё-таки обнулять память.

Верно. Поэтому какой бы язык программирования не выбрал программист (да хоть даже ассемблер), он не сможет получить "мусорные" данные от других процессов. При всём желании. Т.к. смотрим, что написано в документации наиболее распространённых операционных систем: Windows (если говорить про desktop) и Linux (если говорить про серверный сегмент). В Windows любая аллокация памяти (malloc, HeapAlloc или самописный аллокатор памяти — неважно) в итоге приводит к системному вызову VirtualAlloc, в документации к которому (https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc) написано просто и понятно:

Memory allocated by this function is automatically initialized to zero.

В Linux аналогично: в документации к mmap (https://man7.org/linux/man-pages/man2/mmap.2.html) сказано:

MAP_ANONYMOUS
The mapping is not backed by any file; its contents are initialized to zero.

Да, есть ещё флаг MAP_UNINITIALIZED, но он требует включения специальной опции при сборке ядра и на практике используется только в мире embedded-устройств.

P.S. Да, разумеется, гарантированно зануляет память не каждая операционная система, но давайте быть честными: кто из нас писал программы под операционные системы, которые этого не делают? Я вот не писал, и, возможно, никогда и не буду. Так зачем мне об этом думать? Думать о том, что мой язык программирования будет неэффективно работать на системах, под которые практически никто не пишет. Пусть те, кто под них пишет, выбирают другой язык программирования (если для них это настолько критично), благо выбрать есть из чего.

Причём заметьте, я сказал «будет неэффективно работать», а не «вообще не будет работать» (на таких системах). Ибо, когда из исходного кода компилятор генерирует исполняемый двоичный файл, это всегда производится для конкретной архитектуры процессора и для конкретной операционной системы. Даже если взять платформы наподобие .NET Framework, где приложения распространяются на некоем промежуточном языке, то в конечном счёте для выполнения программы должен получиться машинный код под конкретную архитектуру и операционную систему.

Таким образом, компилятор в любом случае знает, будет ли полученная от системы память гарантированно занулена или не будет, и во втором случае просто добавит memset(mem, 0, size) и программа будет корректно работать, пусть и не так эффективно (особенно в случае выделения огромного блока памяти, который будет использоваться лишь частично).

     2024/02/20 09:03, veector          # 

Но об этом тоже уже писалось.

Уважаемый Автор сайта, писать-то писалось, но вот полезных выводов у вас, извините, но я не замечаю (может просто проглядел).

А упомянутые вами внешние анализаторы кода — вот этот как раз полезный инструмент, который избавляет вас, как разработчика языка, тащить всё-и-вся, включая всевозможные кейсы применения языка, в сам язык и компилятор. Думаю, на это, тему про "неправомерное использование указателей" можно в основном и закрыть.

     2024/02/20 09:04, veector          # 

... но давайте быть честными: кто из нас писал программы под операционные системы, которые этого не делают?

Например, я, причем это 99% моей непосредственной работы.

     2024/02/20 22:21, Автор сайта          # 

выражение «если-иначе» — это всё хорошо, но я хотел сделать акцент на другом. А именно, на случаях очень сложных инициализаций.

Думаю, при должной старательности сложные инициализации можно упростить. Или даже выделить инициализацию в отдельную функцию, ведь return не может возвратить неинициализированное значение. В примерах выше условный оператор был превращён в условное выражение, то есть конструкцию, возвращающую аргумент. Можно подумать над тем, чтобы и цикл что-то возвращал, тогда он будет не оператором цикла, а выражением цикла.

в n окажется начальное значение 0, и при этом возможно возникновение очень трудно уловимой ошибки.

А если нулём прописывается указатель, то вообще беда.

необходимо проверить, что n инициализируется на всех путях выполнения

Вот поэтому проще сделать единственное инициализирующее выражение, пусть и с сложными выражениями и циклами, чем что-то отслеживать.

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

Имел в виду не «занулять» (в том числе и бессмысленно, лишь бы чем-то проинициализировать), а присваивать осмысленное значение, вытекающее из логики алгоритма.

какой бы язык программирования не выбрал программист (да хоть даже ассемблер), он не сможет получить "мусорные" данные от других процессов. При всём желании.

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

флаг MAP_UNINITIALIZED, но он требует включения специальной опции при сборке ядра и на практике используется только в мире embedded-устройств.

Да, там, а ещё есть ОС РВ, поэтому полностью верю, что

veector: это 99% моей непосредственной работы

используется только в мире embedded-устройств.

Вот только этот мир (IoT) растёт быстрее мира компьютеров и смартфонов.

кто из нас писал программы под операционные системы, которые этого не делают?

Я писал! При этом я писал под Windows. По-моему (но могу ошибиться), Windows XP ещё не обнуляла память, а началось это с Vista. Иногда сталкивался с тем, что программа начинает вести себя по-другому просто от добавления ещё одной переменной. Тогда понимаешь, в чём дело, и ищешь: где это я забыл проинициализировать? Изменение поведения от того, что не может поменять поведение, как раз и было признаком невыполненной инициализации.

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

ОС РВ — вполне себе достойная уважения ниша. Имеют право не обнулять память потому, что перечень установленных программ там заранее зафиксирован.

я сказал «будет неэффективно работать»

Неэффективность будет связана с двукратной записью в одни и те же участки памяти: сперва нуля, а потом собственно значения. Но не трёх и более кратной записью. Так что максимальное торможение — максимум в 2 раза. В реальности меньше, потому что есть ещё статическая память, операции регистр-регистр, есть кэш процессора и т.д. Кстати, в ОС жёсткого реального времени динамического выделения памяти нет. Память выделяется статически. Там эта тема не актуальна.

veector: полезных выводов у вас, извините, но я не замечаю

Отвечу словами Окуджавы:

Каждый пишет, как он слышит, каждый слышит, как он дышит.

     2024/02/22 00:00, alextretyak          # 

veector

Например, я, причем это 99% моей непосредственной работы.

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

Автор сайта

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

Патчить ядро Windows для доступа к чужой памяти? Зачем ломать стену, когда рядом есть дверь? :)(:
Помнится, ещё в Windows 95 или 98 я использовал программу ArtMoney для получения бесконечных денег в играх. Когда узнал, как она работает, был весьма удивлён — штатными средствами WinAPI (а именно, функциями ReadProcessMemory/WriteProcessMemory) можно получить доступ к памяти любого другого процесса. Хотя, начиная с Vista эти функции требуют административных привелегий.

По-моему (но могу ошибиться), Windows XP ещё не обнуляла память, а началось это с Vista.

Обнуляла. Но речь только про системную функцию VirtualAlloc. Она обнуляла память всегда с момента своего появления в Windows NT, вот только в прикладных программах эта функция непосредственно, как правило, не используется, т.к. она выделяет только блоки размером кратным 64Кб. В программах на Си чаще всего используется библиотечная функция malloc(), а в C++ — оператор new, который стандартно реализован через вызов того же malloc(). А вот память, которую вернул malloc(), далеко не всегда занулена (собственно, поэтому есть отдельная функция — calloc(), которая возвращает занулённый блок памяти): во-первых, в Debug-сборках реализация malloc() перед тем как вернуть указатель заполняет каждый байт выделенного блока памяти значением 0xCD (как раз таки для обнаружения обращений к неинициализированным данным). И, во-вторых, malloc() может вернуть блок памяти, который был ранее освобождён функцией free(), и в этом блоке будут "мусорные" данные, вот только не от других процессов, а от этого же процесса, записанные им в свою память во время его предыдущей работы.

     2024/02/22 14:55, veector          # 

alextretyak, много всяких разных, наверное, всё, что придумается в embedded сфере, причем, всевозможных версий. Более подробно, увы, не скажу.

     2024/02/29 13:21, veector          # 

В копилку хейтеров нативной работы с указателями в С и С++.

Белый дом рекомендовал отказаться от C и C++ в пользу безопасных языков программирования

https://3dnews.ru/1100993/beliy-dom-rekomendoval-otkazatsya-ot-c-i-c-v-polzu-bezopasnih-yazikov-programmirovaniya

Офис национального директора по кибербезопасности (ONCD) Белого дома США призвал разработчиков отказаться от использования языков программирования C и C++ в разработке критически важного ПО. Этот совет основывается на опасениях, связанных с безопасностью управления памятью — аспекте, играющем критическую роль в предотвращении уязвимостей, таких как переполнение буфера и висячие указатели.

А на хабре уже начался замес: https://habr.com/ru/news/712040/

Бьёрн Страуструп ответил АНБ США по поводу рекомендации ведомства отказаться от использования языков C и C++

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

     2024/02/29 20:02, Max Vetrov          # 

А на хабре уже начался замес: https://habr.com/ru/news/712040/

Он уже закончился. Новость начала 2023 года.

     2024/02/29 22:47, Автор сайта          # 

О хейтерах нативных указателей. Давайте уж, если мы русские люди, говорить по-русски. Есть слово "ненавистники", которое ни в чём не уступает рунглишу-новоязу.

Описывать отношение к указателям, которые не подчиняются никаким правилам, словом "ненависть" неправильно. "Осторожность", "желание обезопасить себя", "дую на воду, обжегшись на молоке", но это далеко не "ненависть". Вот ввели по глупости термин "гомофобия", а ведь редко кто испытывает "фобию", то есть страх. В основном отношение варьируется от "отвращения" до "презрения". Не надо передёргивать понятия.

Есть статья посвежее на эту тему: https://safe.cnews.ru/news/top/2024-02-27_vlasti_ssha_amerika_v_opasnosti

Власти США призывают разработчиков ПО как можно скорее отказаться от использования небезопасных по их мнению, языков С и С++. Администрация действующего Президента США ... потребовала создавать программное обеспечение, «безопасное с самого начала», а не становящееся таким по мере выискивания и устранения уязвимостей в нем

Конечно, испытывать озабоченность только по поводу неправильного использования памяти — это сильно упрощать проблему. Одно только неопределённое поведение, забитое в стандарты, чего стоит. Статические анализаторы хоть и помогают, но решают только часть проблем, а не все. "Хорошие" подмножества языка — тоже полумера.

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

Очень грамотные и компетентные люди допускают порой такие ошибки, что просто диву даёшься. А всё потому что человек смертен, он не машина, ему свойственно ошибаться. Всё-таки почитайте отчёты об ошибках, найденных статическими анализаторами. На Хабре полно статей. Вот поэтому хорошо иметь инструменты, страхующие от ошибок. Лучше ошибки выявить во время компиляции, чем во время исполнения. В этом могут помочь языки, в которых работа с указателями подчинена дисциплине. При этом правильные (с точки зрения языка) указатели точно соответствуют тому, что есть в машине. То есть указатели самые что ни на есть нативные. Просто язык не даёт создать неправильные указатели, заведомо ошибочные.

     2024/03/01 18:47, Сисадмин со стажем          # 

На сегодня, 1 марта, был запланирован переход (во всей стране!) на новую систему учёта алкоголя в ЕГАИС. Но... он не состоялся. Потому что ПО кривое. То ли руки у программистов не оттуда растут, то ли языки и инструменты хреновые. Внедрение отложено до 1 июля. Не удивительно, потому что ПО глючит так, что аж базы данных портятся. Эти базы возят на восстановление к разработчику ПО. Не бесплатно, конечно, а за денюжку.

Кто-то из лиц, принимающих решения (https://dzen.ru/b/ZeHGS9J1nSS7O8vs):

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

Программное обеспечение, «безопасное с самого начала» можно только приветствовать. Но что-то слабо верится, что что-то изменится. Уже насмотрелся. Нет, не при нашей жизни.

     2024/03/03 14:07, Прохожий          # 

То ли руки у программистов не оттуда растут, то ли языки и инструменты хреновые.

То ли рынок такой и требования различные, то ли пользователи схемы различные ищут.

Коллеги, не надо срочно вскрывать все имеющиеся бутылки или искать какие-то схемы.

     2024/03/03 16:49, Автор сайта          # 

ПО глючит так, что аж базы данных портятся.

Он попытается тебя убить

Добавить свой отзыв

Написать автору можно на электронную почту
mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Содержание

Каким должен быть язык программирования?

Анализ и критика

●  Устарел ли текст как форма представления программы

●  Русский язык и программирование

●  Многоязыковое программирование

Синтаксис языков программирования

Синтаксический сахар

●  Некоторые «вкусности» Алгол-68

●  «Двухмерный» синтаксис Python

●  Почему языки с синтаксисом Си популярнее языков с синтаксисом Паскаля?

●  Должна ли программа быть удобочитаемой?

●  Стиль языка программирования

●  Тексто-графическое представление программы

●●  Разделители

●●  Строки программы

●●  Слева направо или справа налево?

●  Комментарии

●●  Длинные комментарии

●●  Короткие комментарии

●●  Комментарии автоматической генерации документации

●●  Нерабочий код

●●  Помеченные комментарии

●  Нужны ли беззнаковые целые?

●  Шестнадцатиричные и двоичные константы

●  Условные операторы

●  Переключатель

●  Циклы

●●  Продолжение цикла и выход из него

●  Некошерный «goto»

●  Изменение приоритетов операций

●  Операции присвоения и проверки на равенство. Возможно ли одинаковое обозначение?

●  Так ли нужны операции «&&», «||» и «^^»?

●  Постфиксные инкремент и декремент

●  Почему в PHP для конкатенации строк используется «.»?

●  Указатели и ссылки в C++

●●  О неправомерном доступе к памяти через указатели

●  Обработка ошибок

●  Функциональное программирование

●●  Нечистые действия в чистых функциях

●●  О чистоте и нечистоте функций и языков

●●  Макросы — это чистые функции, исполняемые во время компиляции

●●  Хаскелл, детище британских учёных

●●  Измеряем замедление при вызове функций высших порядков

●●  C vs Haskell: сравнение скорости на простом примере

●●  Уникальность имён функций: за и против

●●  Каррирование: для чего и как

●●  О тестах, доказывающих отсутствие ошибок

●  Надёжные программы из ненадёжных компонентов

●●  О многократном резервировании функций

●  Использование памяти

●  Почему динамическое распределение памяти — это плохо

●  Как обеспечить возврат функциями объектов переменной длины?

●●  Типы переменного размера (dynamically sized types, DST) в языке Rust

●●  Массивы переменной длины в C/C++

●●  Размещение объектов в стеке, традиционный подход

●●  Размещение объектов переменной длины с использованием множества стеков

●●  Размещение объектов переменной длины с использованием двух стеков

●●  Реализация двухстековой модели размещения данных

●●  Двухстековая модель: тесты на скорость

●●  Изменение длины объекта в стеке во время исполнения

●●  Размещение объектов переменной длины с использованием одного стека

●  Можно ли забыть о «куче», если объекты переменной длины хранить в стеке

●  Безопасность и размещение объектов переменной длины в стеке

●  Массивы, структуры, типы, классы переменной длины

●  О хранении данных в стеке, вместо заключения

●  Реализация параметрического полиморфизма

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

Компьютерный юмор

Новости и прочее




Последние отзывы

2024/06/21 00:20 ••• Gudleifr
О превращении кибернетики в шаманство

2024/06/19 00:16 ••• Gudleifr
Программирование исчезнет. Будет дрессировка нейронных сетей

2024/06/12 11:27 ••• Автор сайта
Все языки эквивалентны. Но некоторые из них эквивалентнее других

2024/05/31 12:31 ••• Прохожий
Идеальный транслятор

2024/05/28 15:16 ••• Прохожий
Русской операционной системой должна стать ReactOS

2024/05/25 17:18 ••• Прохожий
Избранные компьютерные анекдоты

2024/05/11 16:33 ••• Автор сайта
Энтузиасты-разработчики компиляторов и их проекты

2024/04/28 15:58 ••• Автор сайта
Обработка ошибок

2024/04/23 00:00 ••• alextretyak
Признаки устаревшего языка

2024/04/21 00:00 ••• alextretyak
Постфиксные инкремент и декремент

2024/04/20 21:28 ••• Бурановский дедушка
Русский язык и программирование

2024/04/01 23:39 ••• Бурановский дедушка
Новости и прочее

2024/03/22 20:41 ••• void
Раскрутка компилятора