Pull to refresh

Comments 170

Рандомный чувак после чтения статьи: -Ну вот я теперь и шарю в этом вашем С++.

Да, ты прав, многие языки содержат в своем стандартном наборе если не идентичные, то очень похожие конструкции, особенно это касаться С-подобных языков. Я делал акцент конкретно на С++ из-за его популярности в учебных заведениях, а так же из-за возможности напрямую работать с памятью (указатели и разыменовывание), ведь так проще наглядно объяснить как это работает.

Речь, скорее шла о том, что С++ специфичных вещей тут довольно мало, и на самом деле здесь максимум расписан Си с классами. Да, если честно, даже и он не расписан. С++ раз эдак в 50 более объемный язык. Как синтаксически, так и... ну скажем "исторически", в плане костылей.

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

Ради сдачи сессии и не стоит знать больше, большему в универе ничему и не учат в принципе)

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

Очень ошибочное понимание, потому что здесь очень много упущено даже из базового линковка, стек работа динамической памяти, утечка памяти, даже число int не занимает 4 байта в общем случае и тем более RAM в комп'ютерах имеет размер ячейки равный битность компьютера.

RAM в комп'ютерах имеет размер ячейки равный битность компьютера

А вот тут неточность. Размер ячейки RAM ровно 1 бит. Да и битность процессора к RAM прямого отношения не имеет.

memory word,- depends on specific architecture : byte, double-byte, 4-byte, block ...

I don't realy remember, does CPU (even!) operate with an one separate bit (except flags)...

Во-первых это совсем другой термин. Ячейка памяти это memory cell.

Во-вторых memory word это из терминологии word-adressable архитектур, которые не имеют никакого отношения к современным компьютерам, потому что они byte-adressable. Не путать с машинным словом (word), это другой термин, связанный с CPU и не связанный с RAM.

Ну и в-третьих не совсем понятно какое все это имеет отношение к размеру типа int. Подозреваю что имелось ввиду выравнивание, но тогда формулировка полностью неверная.

А при чем здесь вообще RAM? Новичок обычно работает с десктопной платформой: Linux, Windows и т.п. На таких платформах ваша программа будет работать с виртуальной памятью, а не с RAM. Доступа к RAM как таковой у вас нет. С RAM работает ОС и вас непосредственно к RAM она не подпустит.

Может быть пацан прогает под DOS в реальном режиме.

Да тут и string-то нету. Сказано только, что есть модуль для работы со строками. На этом и закончили)

 Второй способ называется сложение с присваиванием

Если быть педантичным, то += - это присваивание со сложением. Такие операторы называются операторами compund assignment и на них распространяются все правила операторов присваивания.

float a = 5 / 2;

Ура, дробное число получено, можешь отметить этот день в своем календаре)))

Что за ерунду вы пишете? Это же классика С и C++ FAQ. 5 / 2 всегда равно 2 и никогда не дает "дробное число" в том смысле, что переменная a получит значение 2.0, а не 2.5. Для целочисленных операндов / - это операция целочисленного деления.

Создадим переменную с новым типом данных, логическим. Он занимает всего 1 байт и может быть равен либо 0, либо 10 это ложь1 это правда.

В языке C++ тип данных bool принимает значения false или true, а не 0 или 1. Это несколько иное.

Ссылочная переменная, хранит в себе исключительно ссылку на первый байт переменной, на которую он ссылается 

К чему здесь это оговорка про какой-то "первый байт"? Ссылка ссылается на переменную - этим все сказано.

Каждая ячейка памяти имеет свой адрес, записанный в шестнадцатеричной системе.

Где это он записан в шестнадцатеричной системе? А если я запишу адрес в десятичной системе, то это будет уже не адрес?

Если мы напишем нечто вот такое: A + 1, то увидим еще одну ссылку, как не трудно догадаться, это ссылка на вторую ячейку массива.

Вы уже ввели в своей статье разделение на указатели и ссылки. Почему же вы продолжаете упорно называть указатели ссылками?

"Нетрудно догадаться"? Вы все время замечаете, что это "ссылка на первый байт элемента". Поэтому по здравой логике "догадаться" мы скорее всего должны, что A+1 - это ссылка на второй байт, так? А это все таки ссылка на второй элемент. До этого непросто догадаться из-за ваших странных (и ненужных) уточнений про "первый байт".

Надеюсь ты еще помнишь, что массив хранит в себе адреса, по которым мы можем перемещаться, 

Что? Массив int D[5][5]; не хранит в себе никакие адреса. Все адреса, которые вы используете для адресной арифметики по такому массиву, вычисляются "на лету". Они не хранятся нигде в массиве.

Функция должна быть объявлена до ее использования. Это значит что все функции будут располагаться вверху файла.

Не ясно, что значит "функции будут располагаться".

Да, верно. Уже поправил. Спасибо!

#include - пишется в начале файла. Подключает так называемые библиотеки,

#include не подключает никакие библиотеки. #include включает стандартные заголовки или внешние заголовочные файлы. Это совсем другое.

<string> - модуль со строковым типом данных, которого в "чистом" С++ нету.

Небольшой экскурс. Строки, это на самом деле массивы символов, и чтоб не возникало возни с этим, создали модуль, который упрощает работу с ними. Если интересно как это устроенно на программном уровне, погугли "С-строки".

Но при при чем здесь C-строки? Заголовок <string> не имеет прямого отношения к С-строкам.

P.S. И, конечно же, проверка правописания и tsya.ru. Трудно читать.

> #include — пишется в начале файла.

ха ха
class LotsOfFun{
#include "standard_funcs.h"
};


Впрочем никого не призываю так делать
Угу, как минимум все они оказались в private-части.

Или более жизненный, на мой взгляд, пример: когда студенты пишут шаблонный класс, то заставляю определение методов вынести отдельно, поэтому в конце файла появляется
#include "impl/some_class.h"
> Угу, как минимум все они оказались в private-части.
кстати не обязательно, ведь включенный файл может начинаться с
public:

На реальном проекте видел приёмчик когда с помощью переопределения макросов и инклудов одного и того же файла между этими переопределениями собирали такую себе "рефлексию".

Ещё, конечно, #include "templates_impl.inl" в конце файла можно вспомнить - техника позволяющая сделать работу с шаблонами ощутимо чище.

Очень надеюсь что с приходом C++20 и модулей эта вся жесть уйдёт в прошлое.

Неправда ваша

int a; это непосредственно выделение тех четырех байт памяти под целое число

Далеко не всегда - зависит от компилятора и от разрядности системы. То, что вы описали - u_int8_t

Степень 31, а не 32 потому что первый бит отвечает за знак числа, если 0 то +, если 1 то -.

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

Это абсолютно то же самое, только короче.

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

Строки со второй по четвертую на самом деле делают одно и тоже: просто к значению переменной c прибавляют 1.

Опять-таки нет.
Строка с=с+1 сначала помещает с в регистр, потом - делает SUM C, 1, потом - записывает куда-то результат, и только потом изменяет ячейку памяти. с+=1 - команда не требует возможности обращения к промежуточному результату и экономит 1 такт на перезаписи.

На подобии команды cout есть функция cin

Говорить о cin/cout, не рассказав ни слова о потоках - не совсем корректно, особенно если статья для чайников

Обрати внимание, что задать значение константе нужно сразу при объявлении, потом поменять его уже будет нельзя.

Если сильно надо - то можно. const_cast никто не отменял


Ну и к вопросу о указателях/ссылках - на дворе 2022ой, но в статье нет ни слова о умных указателях. И о том, что почти всегда вместо массива имеет смысл использовать stl::vector. Не всегда, но почти.

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

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

Если сильно надо - то можно. const_cast никто не отменял

Это как это??? Никакойconst_cast не позволит изменить значение константного объекта. В С++ вообще не существует возможности изменить константный объект, т.е. любые попытки это сделать приводят к неопределенному поведению (кроме, конечно же, mutable членов класса). И const_cast к этому не имеет никакого отношения.

Строка с=с+1 сначала помещает с в регистр, потом - делает SUM C, 1, потом - записывает куда-то результат

 с+=1 - команда не требует возможности обращения к промежуточному результату и экономит 1 такт на перезаписи.

К языку С++ все эти разглагольствования не имею никакого отношения. Если c - это просто переменная, то c += 1 по определению эквивалентно c = c + 1.

 Знаковый бит равен 1 - это отрицательное число и в прямом, и в обратном и в дополнительном коде

По тексту читается (поправьте меня, если я ошибаюсь), что только знаковый бит отвечает за знак чиста. Грубо говоря, если его инвертировать - число поменяет знак.
Это слегка не так

Никакойconst_cast не позволит изменить значение константного объекта

Документация говорит об обратном
Под спойлером пример - можете запустить и проверить

Программа
//g++  7.4.0

#include <iostream>

int main()
{
    const volatile int w = 10; 
    int &wr = const_cast <int &> (w); 
    wr = 20; 
    std::cout << w << std::endl;	//output: 20
}

К языку С++ все эти разглагольствования не имею никакого отношения

Тут надо смотреть, во что оно скомпилируется. Если компилировать под 8086 без оптимизаций, например, то я полагаю, с+=1 перейдет в INC с, а c=c+1 - сложит и присвоит через SUM и MOV.

UPD: нашел в документации

Hidden text

Ссылка на пруф
https://en.cppreference.com/w/cpp/language/operator_assignment#Builtin_compound_assignment

Под спойлером пример - можете запустить и проверить

Данная программа не является программой на C++, так как содержит в себе undefined behavior.

const_cast makes it possible to form a reference or pointer to non-const type that is actually referring to a const object or a reference or pointer to non-volatile type that is actually referring to a volatile object. Modifying a const object through a non-const access path and referring to a volatile object through a non-volatile glvalue results in undefined behavior.

https://en.cppreference.com/w/cpp/language/const_cast

Тут надо смотреть, во что оно скомпилируется. Если компилировать под 8086 без оптимизаций, например, то я полагаю, с+=1 перейдет в INC с, а c=c+1 - сложит и присвоит через SUM и MOV.

Тут не надо ничего смотреть кроме стандарта. Для интегральных типов это одно и то же. Как при этом ведет себя какой-либо компилятор - дело десятое. Тем более, что современный clang, что gcc сгенерируют один и тот же код с любым бэкэндом даже с -O0

Обновил коммент выше, приложил скрин документации.
Сокращенное сложение - атомарная операция, обычное сложение с последующим присваиванием - нет.

Нет, к атомарности это не имеет никакого отношения. Это работает только тогда, когда x+c вызывает какой-то сайд-эффект. Сложение интегральных типов никаких сайд-эффектов не порождает.

Грубейше неверно!

Вы уже второй раз "прикладываете документацию", но при этом выдумываете то, чего в этой документации нет даже отдаленно. Выделенное вами утверждение об "evaluated only once" не имеет никакого отношения ни к какой "атомарности".

Документация говорит об обратном

Еще она говорит что

Modifying a const object through a non-const access path and referring to a volatile object through a non-volatile glvalue results in undefined behavior.

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

Грубо говоря, если его инвертировать - число поменяет знак.

Я не увидел там такой далеко идущей категоричности.

Документация говорит об обратном

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

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

Более того, по вашей же ссылке ясно сказано: "Modifying a const object through a non-const access path [...] results in undefined behavior".

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

Программа

Ваша программа имеет неопределенное поведение. Она ничего не демонстрирует.

Если компилировать под 8086 без оптимизаций, например, то я полагаю, с+=1 перейдет в INC с, а c=c+1 - сложит и присвоит через SUM и MOV.

В С++ нет таких понятий как "8086", "без оптимизаций", "SUM и MOV".

нашел в документации

И? Что вы там увидели? Утверждение "evaluated only once" имеет какое-то значение только если c - некое выражение с нетривиальным поведением и/или побочными эффектами. То есть, например, это гарантирует, что в f() += 1 функция f будет вызвана только один раз. Если жеc - просто переменная, то в c += 1 выделенный вами текст не значит вообще ничего.

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

настолько глубоких подробностей не знаю ни я,

Может быть тогда не стоило писать статью, не ориентируясь в материале?

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

Говорить о cin/cout, не рассказав ни слова о потоках - не совсем корректно, особенно если статья для чайников

Отдельно стоит добавить о необходимости endl или flush при выводе в поток, иначе можно удивиться почему на экране ничего не появилось.

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


Кхе, кхе. :) Этот код ну очень любят военные. :)

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

Выглядит как доклад студента, сдавшего наконец сессию с 3 раза ;) Для шпаргалки букв слишком много, для справочного материала - слишком мало.

К сожалению мало кто пользовался ею,

Верю. Читать тяжело, и систематизировано всё достаточно плохо. Из С++ просматривается только cin,cout,new. Предложение гуглить из шпаргалки про С-строки это сильно ;)

Есть над чем работать ;)

Первая статья, опыта нет, но надо же с чего-то начинать)

А приведите примеры, какие пет проекты на С++ были бы в тему начинающим на этом языке ?

калькулятор, что-то для работы с базой данных, простая игрушка

UFO just landed and posted this here
Можно я чуть поприкалываюсь? Не обижайтесь, если где-то будет слишком грубо. Да и где-то я мог что-то подзабыть и ошибиться. Но в целом, вперёд и с юмором! :)

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


Далеко не любая. Можно написать программу просто работая с регистрами (их мы же ОЗУ не будем называть?). И ввод/вывод тоже взять с портов прямо в регистры.

а в 4 байтах именно 32 бита


Байты были очень разные. Те, которые по 8 бит не единственные.

Степень 31, а не 32 потому что первый бит отвечает за знак числа, если 0 то +, если 1 то -.


Он не первый, а 31. И для беззнакового числа он не используется. Для знакового это просто число в дополнительном коде, который сейчас почти везде. Единственное, где это верно, так это прямой код.

Ты можешь


Использовать такое обращение к читателю как-то не очень принято…

Строка int a; это непосредственно выделение тех четырех байт памяти под целое число.


Компилятор BC31 повесился. У него int только два байта. О, горе ему! :)

где-то в памяти 4 байта


Надесь, не в куче? :)

нечто вот такое: 00000000 00000000 00000000 00000001


А тут заплакали процессоры с big-endian нотацией. :) У них такого числа не получилось в памяти. Увы. :)
Кстати, внимательней посмотрел. Нет, тут плачут little-endian процессоры. Ну или вы неправильно прочли память, куда записали 1. Думаю, всё-таки последнее.

Это абсолютно то же самое, только короче.


Ну-ну. А поменяйте-ка int на какой-нибудь класс и посмотрите, будет ли вызываться операция присваивания в этом случае. :)

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


Не встречал. А, кстати, переменные static по стандарту инициализируются нулём. Всегда.

Все три способа на программном уровне работают одинаково


C++ и ++C точно работают одинаково? Временный объект не создаётся в первом случае?

a = 3.5;


Компилятор предупреждение, что float присваивается значение double не написал?

есть функция


А, это функция… Буду знать. Я-то думал, что это объекты.

В них имена переменных не могут повторяться.


Интересно, а как вы, имея глобальную переменную A, используете в функции локальную переменную A?

Нумерация в массивах, и вообще в программировании в целом, начинается с нуля,


Тут зарыдал паскаль. И по-моему, ещё и бейсик (но я его уже больше 20 лет не использовал, так что это не точно).

A[i] = i;


А попробуйте i[A]=i. :)

имеет свой адрес, записанный в шестнадцатеричной системе.


Стесняюсь спросить, чем не устроили другие системы счисления? :)

значения 4, 5 и 2 являются константами.


Есть один способ… Она ведь тоже имеет тот самый адрес «в шестнадцатеричной системе счисления».

но на практике больше чем две не делают


Вот скажите на милость, вы как это узнали-то? У вас столь богатая практика, что вы можете за все-все задачи это утверждать? :)

Надеюсь ты еще помнишь, что массив хранит в себе адреса,


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

Сразу скажу что разница будет ровно 20


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

про динамические двумерные массивы.


А удалять-то эти самые массивы когда будем?

#include — пишется в начале файла.


В любом месте, где вы хотите физически встроить кусок текста из #include.

Подключает так называемые библиотеки


Всего лишь подключает заголовочные файлы.

using namespace std — Подключает стандартное пространство имен. Что это такое пока не важно, это из ООП.


А, вот оно что… Я тут вспомнил: точно, Objective C — вот ООП. А C++ — это Си с классами, это не ООП. :)

то все придется писать с std:: в начале.


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

string — модуль со строковым типом данных, которого в «чистом» С++ нету.


Он смылся, когда отмывали Си++ до чистоты? :)

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


Я это «болото» за 22 года так и не выучил до конца. :) Поэтому, я вам завидую. Честно. :) Мне-то уже 39, а у вас всё впереди.
Удачи! :)

C++ и ++C точно работают одинаково? Временный объект не создаётся в первом случае?

В спецификациях префиксного и постфиксного ++ нет ни слова ни о каких "временных объектах". Зачем их сюда притягивать?

Ну-ну. А поменяйте-ка int на какой-нибудь класс и посмотрите, будет ли вызываться операция присваивания в этом случае. :)

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

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

Нет. Разность указателей T * в С и С++ зависит только от того, сколько элементов типа T содержится между этими указателями. Адресная арифметика в этих языках специфицирована на высоком уровне и от устройства адресации процессора не зависит. Стоит заметить, что вычитать друг из друга в С и С++ разрешается только указатели на элементы одного и того же массива.

В спецификациях префиксного и постфиксного ++ нет ни слова ни о каких «временных объектах». Зачем их сюда притягивать?


А это чтобы не было потом мучительно больно.

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


Что он хотел сказать я не знаю, а сказал он что никакой разницы нет.

Разность указателей T * в С и С++ зависит только от того, сколько элементов типа T содержится между этими указателями.


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

Вычитать же указатели из разных частей программы приведёт к неопределённому поведению.

WAT?

Результат вычитания может не поравиться, но откуда возьмется неопределенное поведение?

Результат вычитания может не поравиться, но откуда возьмется неопределенное поведение?


Возьмётся от возможного неожиданного/неопределённого результата. Вы просто получите странные вещи. На одной платформе всё будет работать. На другой не будет. Так как-то.

Тут бы пример ;) Я не вижу никаких препятствий в вычитании любого указателя из любого, хоть и NULL (но зачем это может понадобиться, продолжаю не понимать).

Просто вы думаете, что у вас линейная модель памяти, и адреса только увеличиваются. А они могут на самом деле быть в формате сегмент: смещение и тогда у вас в указателе хранится не линейный адрес, а вот эта парочка. И вычитание даст ерунду.
А где может понадобиться… Так сходу и не придумать. В embedded, возможно, где-нибудь.

Как на счёт вычитание указателей на память, выравненную по разным границам?

Условно:

auto diff = ((int*)0x4) - ((int*)0x3).

Я пишу на С++ уже 9 лет, но понятия не имею, какой будет результат

С просто создан для стреляния в ногу ;) Но думаю что будет 1. Вы же не собираетесь в этот указатель 0х3 что-то писать?

А я думаю будет 0 :) Потому по логике операция вычитания указателей должна дать количество элементов типа T, которые поместятся между двумя адресами.

Что касательно писать в 0х3 - нет ни одного правила в стандарте, запрещающего мне это сделать.

UFO just landed and posted this here
*((int*)0x3) = 0xDEADBEAF;

?
А объект там и не должен существовать. Это может система с физической адресацией, а адрес 0x3 - это захардкоженный адрес, который мапится на совершенно другое оборудование в системе (не RAM), которое способно понять, что такое 0xDEADBEEF.

UFO just landed and posted this here

Здесь - нигде. Но в С++ он мог быт начат где угодно) Писать по вот таким рандомным адресам - обычное дело в embedded.

UFO just landed and posted this here

То и значит, что "где угодно". Как пример: я могу написать в любом TU int anything, потом написать ld-скрипт, который переменную с именем anything расположит ровно по адресу 0x3. И будет её всегда туда располагать.

Соответственно на старте программы и проинициализируется этот адрес. Он будет валидный и туда можно будет писать.

UFO just landed and posted this here

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

Окей.

*((volatile int*)0x3) = 0xDEADBEEF

Теперь компилятор не имеет никаких прав и будет делать ровно то, что я прошу. А есть там какой объект проинициализированный или нет - это уже детали. Если есть - всё будет хорошо, это не UB и поведение нормальное. А вот если нет - то UB и результат непредсказуем. Но я подчеркну - в стандарте нет ни одного правила, запрещающего мне писать по любому адресу, какому захочу. Есть правила, говорящие, что если по этому адресу нет ничего валидного - беда-беда, но это не одно и то же.

UFO just landed and posted this here

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

Да, но не сказано ГДЕ нужно это сделать. Я могу это сделать в любом TU в программе. Это никак не меняет того факта, что если я разместил какой-то int где угодно в программе по адресу 0x3 и этот int всё еще там, пока я пишу в адрес 0x3 - это абсолютно валидный код, ничего не нарушающий, не создающий UB, и т.д. и т.п.

UFO just landed and posted this here

А я утверждаю, что UB - это прежде всего behaviour. Компилятор всегда на строчку `*((volatile int*)0x3) = 1` сгенерирует один и тот же код. Он не может не сгенерировать код записи в память, когда его об этом явно просят. С точки зрения компилятора поведение очень defined.

А вот во время исполнения мы можем получить как defined, так и undefined поведение. Если мы создали все условия, чтобы по адресу 0x3 всё было валидно, значит поведение всегда будет well-defined. В ином случае - да UB.

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

UFO just landed and posted this here

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

Мне лень ходить в стандарт, поэтому я схожу на cppreference.

volatile object - an object whose type is volatile-qualified... volatile accesses cannot be optimized out or reordered with another visible side effect that is sequenced-before or sequenced-after the volatile access

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

Что значит речь не о том? Я компилятору вполне внятно сказал "по этому адресу лежит объект с типом volatile int, я хочу в него записать значение, и меня вообще не волнует, что ты об этом думаешь". И компилятор обязан сгенерировать код. Без вариантов. Компилятор компилирует один TU, и может тупо не видеть этот int. Но будет он там или нет - дело даже не компилятора. Компилятор не располагает объекты в бинаре. Линковщик это делает.

UFO just landed and posted this here
UFO just landed and posted this here

Так стоп. После того, как я скастил 0x3 к указателю, с точки зрения синтаксиса программы последующее разыменование - это обращение к объекту.

У компилятора нет никакой возможности доказать, что там этого объекта нет. Этот объект может жить в другом TU. Следовательно компилятор ничего не остаётся, кроме как поверить мне на слово.

Единственный (известный мне) способ сообщить компилятору, что сущность объявлена в другом TU - это extern. Лайфтайм такого объекта будет начинаться со старта программы.

Пожалуй, если объявить все объекты такого типа (скажем, регистры устройства) как extern, а потом связать их линкером, то все будет корректно. Безусловно, это очень удобный метод. Особенно подходит для всяких динамических сущностей и перемещаемых (например, с помощью MMU) объектов :D

Ну ничего, завезут скоро std::start_lifetime_as и заживём!

P.S.

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

UFO just landed and posted this here

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

Если код порождает UB, он не имеет никакой языковой семантики, как вам уже не раз говорили. Никакие обещания с cppreference к нему не применимы.

Написанный вами огрызок - бессмысленный набор символов, в котором лишь вам лично мерещатся какие-то химеры вроде "volatile access", точно так же как маленькому ребенку ночью в висящем на стуле халате видится крадущийся Гойко Митич с томагавком в руке. С точки зрения языка С++ там нет ни "volatile", ни "access", ни чего-либо вообще.

Ещё раз - дайте мне кусок стандартам, который подтверждает, что писать в случайную память - это UB

Undefined behaviour с точки зрения компилятора целиком - это ситуация, когда нет никаких ограничений на действия генерируемого кода.

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

Стандарт не говорит о том, код с каким поведением должен быть сгенерирован при попытке сделать ваше *((*volatile int)0x3) = 1. Большинство современных компиляторов сгенерируют то, что вы ожидаете. От этого этот код не становится валидным и никаких гарантий, что так будет всегда нет.

А я утверждаю, что UB - это прежде всего behaviour. Компилятор всегда на строчку `*((volatile int*)0x3) = 1` сгенерирует один и тот же код.

Это чушь. Как только на сцене появляется UB, никакого сгенерированного кода уже быть не может.

И тем не менее он генерируется. Чудеса, не иначе

И тем не менее он генерируется. Чудеса, не иначе

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

Ага, и потом обычное дело - получить набор забавных спецэффектов при попытке запустить код, откомпилированный с -O3 вместо -O0, на котором все вроде как работало :D

Да гспди, загляните в HAL-библиотеку для STM32. Там всё в таком коде, пищущим по магическим адресам.

UFO just landed and posted this here

В таком случае никто в Embedded в принципе не программирует на С++)

UFO just landed and posted this here

Вы просто пессимист. Ввиду того, что UB включает в себя всё что угодно, включая валидную программу на С++, можно сказать, что любая программа с UB написана на С++ :)

UFO just landed and posted this here

А вот это — нет. В валидной программе на C++ UB нет.

Да. Но это ведь не означает, что внутри UB нет валидной программы на С++ :)

Скорее всего она написана на просто C :)

Но я же могу подключить её в С++? Вся эта магия - она в макросах, так что существует в заголовочном файле.

А, ну если в макросах в заголовочном, то это не "стандартный C++" :)

А, то есть вы нас обманывали. В самой программе этой "магии" не было, а вся она была спрятана в библиотечных средствах (напр., в заголовке), предоставляемым host environment? Тогда и говорить не о чем: внутренняя реализация этой библиотеки не имеет и не должена иметь никакого отношения к С++. Точно так же реализация обычной стандартной библиотеки языка С++ не имеет никакого отношения к С++.

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

STM HAL именно такой. Это не C++ библиотека, а библиотека, написанная на конкретном диалекте, предоставляемом определенным диапазоном версий g++-arm. Работает? Ну да. Потому что обычно собирается в стерильной среде строго определенным компилятором.

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

В С++ нет ничего синтаксически некорректного в желании записать что-то по любому адресу. Стандарт говорит: "Если по этому адресу лежит то, что ты туда пишешь, то всё нормально". А вот если "там лежит не то, что ты туда пишешь" или "там ничего не лежит", то тут UB.

О том и речь. Программа соответствует грамматике cpp, компилируется, даже код какой-то генерируется, но не является программой на cpp (содержит ub).

Не какой-то, а конкретный код генерируется. UB происходит не на стадии компиляции, а на стадии исполнения программы. Если по адресу записываемому адресу на момент исполнения программы всегда живёт объект нужного типа, абсолютно валидно туда что-то писать. До момента старта программы UB нет.

UFO just landed and posted this here

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

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

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

Следующий код:

int* p1 = (int*)malloc(sizeof(int));
int* p2 = (int*)realloc(p1, sizeof(int));
    
if (p1 == p2)
{
    *p1 = 1;
    *p2 = 2;
    std::printf("%d %d \n", *p1, *p2);
    std::printf("%p %p \n", (const void*)p1, (const void*)p2);
}

Запросто может выдать следующий результат (https://godbolt.org/z/sK7nv9Y6x):

1 2 
0x2366eb0 0x2366eb0

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

А при чем здесь именно синтаксическая корректность? Вот эта программа тоже является синтаксически корректной

int main()
{
  int a = 42, a = 53, a = 128;
}

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

Лично мне никогда не приходилось писать freestanding код на C++ (а необходимость что-то скастить таким образом может возникнуть только во freestanding или драйвере, на мой взгляд), но, насколько я помню стандарт, reinterpret_cast is implementation defined, но не является ub при касте инта к указателю на какой-нибудь простой тип.

И вроде даже при касте к сложному объекту, если с точки зрения с++ abstract machine этот объект валиден (это, скорее всего, потребует правок в компиляторе, но стандарт это явно не запрещает).

Вроде как проблему с implementation defined и убеждением абстрактной машины должен решать std::start_lifetime_as, но тут я повторюсь, что я не специалист.

Даже в C можно наесться при написании большого freestanding проекта, на C++ это, наверное, совсем весело.

UFO just landed and posted this here

Если задача писать int по определенному адресу, то placement new должно быть достаточно. start_lifetime_as хорошо, но в данном случае не обязательно.

UFO just landed and posted this here

Там же до этого не было никакого объекта с точки зрения C++. А работать можно например через что-то типа синглтона Майерса - статический указатель, инициализируем однократно через placement new и все время возвращаем. Дешево и сердито.

UFO just landed and posted this here

А компилятор может это доказать?

Но он не может доказать и обратного :)

Ну это тот же подсахарённый глобальный флаг.

Ну он по крайней мере не глобальный :) Просто со static storage duration.

UFO just landed and posted this here

Так отсутствие доказательств не является доказательством отсутствия, особенно в конструктивной математике :]

Ну в любом случае для trivially-destructible типов лайфтайм автоматически заканчивается в момент storage reuse. Так что тут все ОК.

Если деструктор объекта, "который там уже был", тривиален, то никаких проблем нет. Лайфтайм этого объекта просто тихо заканчивается. Так в С++ было всегда.

Ну а если деструктор нетривиален, то начинаются нюансы. Хотя тащить их сюда смысла нет, ибо все это предназначено в первую очередь для буферов из unsigned char.

Ну а если деструктор нетривиален, то начинаются нюансы. Хотя тащить их сюда смысла нет, ибо все это предназначено в первую очередь для буферов из unsigned char.

Ну, вот вопрос: если есть выделенный буфер в виде массива char, в который я получил бинарные данные, то правильно ли я понимаю, что для implicit-lifetime типа T можно просто сделать так?

T* p = reinterpret_cast<T*>(buffer);

Если нет, то можно ли сделать так?

T* p = new(buffer) T;

И хочется услышать мнение @0xd34df00d

UFO just landed and posted this here
UFO just landed and posted this here

Почему это? Я утверждаю, что модифицировал объект типа T, представив его массивом char, и загрузив в него данные из сети.

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

UFO just landed and posted this here

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

Нет, подождите. То, какой reinterpret_cast разрешается, перечислено по вашей ссылки выше. А в type aliasing написано можно ли потом к памяти обращаться. Так что каст я сделал абсолютно верно.

DynamicType - это тип, объект которого у вас изначально существовал где-то в памяти (в данном случае char-массив), а AliasedType - это тип, через который вы его трогаете (в данном случае T)

Именно. Вот я и утверждаю, что T уже существовал. Для этого я оговорил, что T - implicit-lifetime тип. И как я понимаю, в соответствии с object model время жизни объекта такого типа начнётся просто по факту выделения буфера (пункт 13).
Если же я где-то не прав (где?), то я предложил просто сделать placement new.

Возникает ещё один вопрос: а зачем нам start_lifetime_as? @antoshkka?

UFO just landed and posted this here
UFO just landed and posted this here

Если я правильно понимаю, то все равно нельзя (по крайней мере, до C++20, в нем я внимательно стандарт на эту тему не читал).

Почитал я стандарт, а также P0593R6 и P2590R2. Пока всё же склоняюсь к тому, что если T - implicit-lifetime тип, то в соответствии с 6.7.1/10-11 код

char* buffer = new char[size];
read(buffer, size);
T* p = reinterpret_cast<T*>(buffer);
p->x = 42;

вполне законен.

Однако, необходимо обратить внимание на

If multiple such sets of objects would give the program defined behavior, it is unspecified which such set of objects is created.

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

new (buffer) char[size];

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

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

void process(Stream* stream) {
  std::unique_ptr<char[]> buffer = stream->read();
  if (buffer[0] == FOO)
    processFoo(reinterpret_cast<Foo*>(buffer.get())); // undefined behaviour
  else
    processBar(reinterpret_cast<Bar*>(buffer.get())); // undefined behaviour
}

В моём понимании, это именно "multiple sets of objects", о которых идёт речь. И вот тут поможет start_lifetime_as.

А второй вариант не ведёт к UB, но может затереть ваши данные.

Ну, поскольку речь о implicit-lifetime типах, то у него будет trivial constructor, который в память ничего не запишет. Но есть другой момент. Так что получается, что как раз этот вариант приводит к неопределённому поведению

А что вы туда запишете и откуда вы знаете, что по этому адресу существует объект? Более того, как вы в этом убедите компилятор?

Достаточно того, чтобы компилятор не знал что объект там не лежит.

UFO just landed and posted this here

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

char x[100500];

char * a = x;

char * b = x+1;

auto diff = ((int*)a) - ((int*)b);

Но это не совсем С++ style.

Вы "пишете на С++ уже 9 лет", но до сих пор не имеете представления о том, что означает термин неопределенное поведение?

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

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

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

Результат вычитания может не поравиться, но откуда возьмется неопределенное поведение?

Из спецификации языков С и С++. Где ясно сказано, что вычитание указателей разрешено только для указателей на элементы одного и того же массива (или на воображаемый элемент после последнего).

на воображаемый элемент после последнего

Это какое то еретическое толкование священного текста ;) Если можно работать с N+1 элементом - индукция нам говорит, что так же можно работать с любым (тоже воображаемым, почему нет). Просто массив в С - непрерывная область памяти by design, и разница между указателями на разные ее части осмыслена. А так, считать разницу между любыми указателями никто не запрещает. Может, кому то хочется иследовать логику работы аллокатора памяти.

UFO just landed and posted this here

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

Превращайте указатели в intptr_t или uintptr_t и исследуйте на здоровье, только так.

Нет, конечно. Это прямое и буквальное толкование священного текста, который вы вообще пытаетесь игнорировать.

Никакой индукции тут нет и быть не может.

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

Главным источником фактического "непредсказуемого поведения" всегда являлась и является внутренняя логика компилятора, который использует UB для выполнения оптимизаций. Как известно, эквивалентным определением неопределенного поведения является следующее:

Компилятор имеет право транслировать код в предположении, что условия, приводящие к неопределенному поведению, не возникают никогда

Если компилятор сумеет обнаружить, что вы где-то вычитаете друг из друга два "левых" указателя, компилятор имеет право вообще вышвырнуть этот участок кода из программы. Или заменить результат вычитания на безусловный 0. И или на 42. И он будет прав, ибо он имеет право полагать, что этот код никогда не будет выполняться.

Или он может вообще отказаться транслировать ваш код.

Компиляторы уже давно это делают. Мы уже прошли и через strict overflow semantics, и через strict aliasing semantics, и через возвращение нулевых ссылок/указателей на локальные переменные в функции, и еще много других компиляторных решений, основанных на UB, но почему-то все равно определенным индивидуумам трудно понять, что UB - это в первую очередь последствия оптимизационных решений компилятора, а не следствие свойств их аппаратной архитектуры.

Если вы пытаетесь вычитать "левые" указатели, то язык С++ открытым тестом вам говорит - это не вычитание вообще. А ваши разглагольствования про "непрерывную память" - это пустопорожние разглагольствования, которые никакого отношения к вопросу не имеют.

Как правильно заметил KanuTaH выше, хотите произвольно обращаться с адресами - приводите указатели к std::uintptr_t и дальше делайте с результатами что угодно.

А это чтобы не было потом мучительно больно.

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

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

работающему со встроенными типами, все эти соображения неприменимы.


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

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


Стойте! Новичков?! :O Я так понимаю, написание статьи предполагает нифига не новичка. Он же чему-то научить хотел? Другое дело, что по неведомой причине, автор решил, что он уже изучил Си++. Я не знаю, откуда у него такая уверенность в себе, что он решил заняться обучением читателей. Оно, конечно, похвально, но рекомендовать эту статью именно новичкам я бы не стал.

Я сам предпочитаю использовать префиксный ++


А вот я люблю именно постфиксный. Каюсь. Но я его так использую только со встроенными типами. А так, мне просто приятнее глазу n++, а не ++n.

Был какой-то фокус на эту тему, забытый мной за давностью лет

char a[]="Hello, World!";
char *b="Hello, World!";

if(sizeof(a)==sizeof(b)) printf("Hello, World!\n");
Нет, другое.
Вспомнил. Взятие адреса от имени массива даёт указатель на первый элемент.

Нет, конечно. Никакого "взятия адреса" тут не нужно. Это правило называется "array type decay" или "array-to-pointer conversion".

Это правило говорит, что значение типа массив T [N] может быть неявно преобразовано к типу указатель T *. Получающийся в результате указатель указывает на нулевой элемент массива. Вот и все.

Я имею в виду вот это:
int a[10];
int *b=(int*)&a;
int *c=&a[0];
b и c -указывают на первый элемент.

Если же a заменить на динамический массив, то b станет указывать на указатель на этот массив, приведённый к int *.

Не понимаю, зачем вы взялись городить весь этот огород с (int *) и & (причем сразу с & у вас не получилось и вы вынуждены были добавить приведение типа).

То, о чем обычно ведут речь в таких случаях, выглядит так

int a[10];
int *b = a;
int *c = &a[0];  

assert(b == c);
// Здесь `b` и `c` указывают на нулевой элемент `a`

И не надо никакого взятия адреса или насильного приведения типа.

Если же a заменить на динамический массив

Что такое "динамический массив" и при чем он тут?

Не на "динамический массив", а на указатель. Вся суть в том, что вы заменяете a на указатель. А уж на что он там будет указывать - на некий "динамический массив" или на дырку от бублика - роли не играет.

причем сразу у вас не получилось и вы вынуждены были добавить приведение типа


В каких-то старых компиляторах оно было не нужно. Но тому лет 20.

И не надо никакого взятия адреса или насильного приведения типа.


Это вообще-то пример, показывающий, что имя массива не указатель на первый элемент. Как только мы меняем это имя на действительно указатель на первый элемент массива, b и c становятся разными.

Не на динамический массив, а на указатель.


Вот конкретно тут вы и так поняли, что я имел в виду. ;)

В каких-то старых компиляторах оно было не нужно. Но тому лет 20.

В стандартном С++, как впрочем и в стандартном С, код

int a[10];
int *b = &a;

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

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

С этим никто и не спорил. Имя массива - ни в коем случае не указатель на первый элемент. В rvalue-контекстах имя массива может лишь неявно конвертироваться к значению указателя на первый элемент. Именно это сказано в правиле "array type decay", которое я привел выше.

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


А когда компиляторы были не кривые? Я с gcc 2.95 когда переносил программу на последний, знаете, сколько именно error он надавал? :) А 2.95 пофиг вообще было.

С этим никто и не спорил.


Тогда вы потеряли контекст беседы. Потому что всё началось с того, что я указал про один из фокусов, показывающих, что имя массива не есть указатель на первый элемент. Дальше я вспомнил, что это был за фокус. А дальше вы зачем-то подключились и заново повторили ровно то, что я в исходном сообщении и написал. А дальше с чем вы спорили я уже не знаю. Я же просто расписал, что имелось в виду под фокусом. Вот и всё.

Имя массива - ни в коем случае не указатель на первый элемент.

В какой момент земной истории это случилось? Цитирую В.В. Подбельского (1996 год):

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

В стандартном С++, как впрочем и в стандартном С, код

int a[10];
int *b = &a;

является некорректным, т.е. некомпилируемым.

Почему? Во-первых, следовало бы ожидать

int a[10];
int *b = a;

Во-вторых, следовало бы написать

int **b = &a;

Так мне представляется более корректным.

Написание int **b = &a; предполагает, что у вас в есть ячейка памяти, обозначаемая переменной a, и в ней находится адрес массива. Но это не так. В переменной a находится сам массив!

Вот это-то мне и не понятно. Я-то всегда полагал, что переменная a — это и есть ячейка памяти, которая содержит адрес первого элемента массива.

Если мы введём следующий код:

#include <iostream>

using namespace std;

int main()
{

    int a[3]={ 1, 2, 3 };
    
    cout<<hex;
    
    cout<<"a[*]: "<<a<<endl;
    
    cout<<"a[1]: "<<&a[0]<<" { "<<a[0]<<" }"<<endl;
    cout<<"a[2]: "<<&a[1]<<" { "<<a[1]<<" }"<<endl;
    cout<<"a[3]: "<<&a[2]<<" { "<<a[2]<<" }"<<endl;

    return 0;
}

то мы получим примерно такой вывод:

a[*]: 0x7fff5371ebec
a[1]: 0x7fff5371ebec { 1 }
a[2]: 0x7fff5371ebf0 { 2 }
a[3]: 0x7fff5371ebf4 { 3 }

Но когда используется sizeof, то приходится вспоминать, что там есть массив.

Просто у ostream есть перегрузка, которая принимает указатель — но нет перегрузки, которая принимала бы массив (точнее, ссылку на него, по значению массив не передаётся).


Если же такую перегрузку организовать — всё становится интереснее. Добавьте вот этот код и попробуйте снова:


std::ostream& operator << (std::ostream& os, int (&a)[3]) {
  os << "{";
  for (int i=0; i<3; i++) {
    if (i != 0)
      os << ", ";
    os << a[i];
  }
  os << "}";
  return os;
}

Также попробуйте посмотреть typeid, это тоже может оказаться интересно. Например, видно что типы у a и &a[0] различаются, а &a является вовсе не двойным указателем как можно было бы подумать:


int a[] = { 1, 2, 3 };
std::cout << typeid(a).name() << std::endl; // A3_i
std::cout << typeid(&a).name() << std::endl; // PA3_i
std::cout << typeid(&a[0]).name() << std::endl; // Pi

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

Я-то всегда полагал, что переменная a — это и есть ячейка памяти, которая содержит адрес первого элемента массива.

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

Если мы введём следующий код:

И что именно этот код должен демонстрировать???

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

Я ранее уже подробно объяснил, что именно имеется в ввиду под утверждением "имя массива воспринимается как указатель". (Кстати, именно "воспринимается", а не "является".) Однако слово "константный" в его утверждении совершенно неуместно. То есть и Подбельский тут малость "несет чушь".

Почему?

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

int **b = &a;

Так мне представляется более корректным.

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

Был какой-то фокус на эту тему, забытый мной за давностью лет

Насколько помню, перед указателем на первый элемент хранится длина массива

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

Это у строк в Паскале была такая "фича".

Речь, очевидно, идет о соседней статье о структуре блока памяти, создаваемой в С++ выражением new []. В нем действительно перед первым элементом может тайно храниться длина массива.

Всего лишь подключает заголовочные файлы.

Или любые файлы? :)

Изучение ассемблера - корень познания остальных языков. С++ в реальности сложен (в универе всё может быть проще если преподаватель не "гик"); плюсы сложны, потому что далеко не всегда знаешь что под капотом, а помнить все правила языка невозможно, поэтому постоянный доступ к интернету - must have любого программиста. Язык С - как следующий уровень после ассемблера. На С, думаю, с опытом можно программировать почти без доступа к интернету - хватит практики и пары книг. Кажется, что С - золотая середина между машинными кодами и, например, python. Но с другой стороны прогресс не остановить и придется вникать и в range, и в corutine и т.п. C++ кажется сближается с Python, равно как Windows с Linux :)

Я как то не ожидал, что автор реально начнет учить). Блин.. Название статьи было каким то провокационным, а тут гайд..

Вообще, на самом деле, эти звездочки должны быть рядом с названием типа данных, вот так: int*int** ...

Звёздочки это модификатор типа переменной(или константы), и влияет она исключительно на имя переменной следующей за ней.
Например вот так:

   int v = 0xDEADBEEF, b = 0xFFFF;
   int *p = &v, &r = v, i = v;
   std::cout << std::hex << ((i >> 16) & b) << ", " << (*p & b) << ", " << ((r >> 4) & (b >> 4)) << std::endl;
---
dead, beef, bee

Следовало бы сказать так:

int *p;

*p —это число типа int;* это операция разыменовывания, значит, p — это указатель на число типа int. Тогда становится понятным, что

int **dp;

— это двойной указатель, а

int (*p)(void);

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

int * (*p)(void);

— это уже функция, которая возвращает указатель. Надеюсь, я ничего не перепутал. Поправьте, если что.

int * (*p)(void);

— это уже функция, которая возвращает указатель. Надеюсь, я ничего не перепутал. Поправьте, если что.

Это указатель на функцию, которая возвращает указатель.

*p —это число типа int;* это операция разыменовывания, значит,

Операция разыменования объявлении переменной? О_о

Да вы, батенька, эстет.

Я, конечно, оговорился.

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

Например я лично в учебнике Паскаль с нуля (https://ru.wikibooks.org/wiki/PascalABC.net_с_нуля) начинаю с констант. Не надо занимать подробными обьяснениями про "выделения памяти" и прочее. Зато сразу можно получить визуальные результат работы программы.

Фактически тот, кто правит конфигурационный файлы готовый программы уже занимается программированием на самом начальном уровне.

А переменные даются только в 4-м уроке(всего их 7).

Для сдачи сессии наверно хватит, и вообще похвально что собрал столько материала в кучу

Тут конечно ошибка на ошибке, но в будущем объяснить почему << "\n" это не то же самое что << endlбудет проще, если человек эти вещи в голове хотя бы смог подружить

Авторские комментарии типа "неймспейсы опустим, это из ООП", лучше бы убрать. Не рассмотрим так не рассмотрим. Потому что дальше обычно написано что-то очень нехорошее

Когда решил, что понимаешь квантовую механику плюсы, и начинаешь всем их учить
UFO just landed and posted this here

Зачем всё это, если есть куча достойных книг по C++ и на русском, и на английском?
Стивен Прата - Язык программирования C++. Лекции и упражнения (6-е изд., 2012)
Stanley Lippman - C++ Primer (5th edition, 2012)
Ivor Horton, Peter Van Weert - Beginning C++20. From Novice to Professional (6th edition, 2020)
Marc Gregoire - Professional C++ (5th edition, 2021)
Покупаешь книжку в электронном варианте и сочетанием клавиш Ctrl + f любая тема на счёт раз ищется в книжке. Кроме этого, есть оглавление в начале книги и предметный указатель в конце.
И я уж не говорю про огромное количество статей и блогов по современным плюсам в сети... Вот, например: https://hackingcpp.com/cpp/blogs.html

UFO just landed and posted this here
Sign up to leave a comment.

Articles