Предыстория

Около трех лет назад, когда я пошел в девятый класс, остро встал вопрос о том, что мне делать дальше. Было несколько вариантов, но в итоге я выбрал Киевский Колледж Связи, который, по словам знакомого, лучший в Киеве из области программирования. Ожидая потока знаний в мою голову, я стал сам потихоньку углубляться в IT и ко второму курсу, когда в учебной программе наконец начали появляться специализированные предметы, уже имел некоторый, относительно неплохой багаж знаний. Окрыленный надеждой получить если не все знания этого мира, то хотя бы половину, я с разбегу ударился в стену разумных доводов о том, что не стоит ожидать от этого места чего-то заоблачного. И вот я сижу на очередной паре и вставляю спички в глаза, ибо все это я успел выучить за первые две недели целенаправленного погружения в это "болото". К концу года, когда вот-вот должна начаться сессия и практика, я окончательно осознал, что на всю группу найдется максимум 3 человека, которые все поняли и могут спокойно оперировать полученными знаниями. (Двое из этих людей и так варились в этом программистском котле, а третий очень заинтересовался где-то к концу первой четверти и достиг очень неплохого уровня за минимальный промежуток времени). Тогда у меня и родилась идея написать максимально подробную "шпаргалку" для тех, кто заинтересован в успешной сдаче сессии но не понял что происходило весь прошлый год. К сожалению мало кто пользовался ею, по этому я решил предоставить ее более широкой общественности, ибо жалко добру пропадать.

Теперь по сути

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

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

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

int a;
a = 1;

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

Для того чтобы смотреть что выдает код который ми пишем есть такая команда:cout << a; где a, это любая перменная или фраза записанная в кавычках. Например: cout << "переменная а = " << a << endl; endl означает конец строки. Вместо него также можно писать "\n", результат будет одинаковый. Ты можешь вписывать это в любом месте где тебе хочеться посмотреть чему равна переменная, я же этого делать не буду, чтоб не засорять примеры.

Строка int a; это непосредственно выделение тех четырех байт памяти под целое число. Мы взяли где-то в памяти 4 байта, присвоили им имя а и теперь можем обращаться к этой памяти как к чему-то единому, называя это целочисленной переменной.

Строка a = 1; задает нашей переменной значение равное 1, фактически это значит, что в те 4 байта памяти запишется нечто вот такое: 00000000 00000000 00000000 00000001. Тут 31 ноль и 1 единица в конце, это двоичное представление числа, что в нашем случае не сильно отличается от десятичного. Это действие, присвоение переменной начального значения, называется инициализацией.

Теперь немного упростим эту запись:

int a = 1;

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

Теперь попробуем немного похимичить:

int a = 1;
int b = 2;
int c = a + b;

Не сложно догадаться чему будет равно c. Это, на самом деле, и есть вся суть программирования, на основе одних данных получать другие. Все что тут произошло, можно переписать в виде 3 = 1 + 2.

А сейчас покажу как довести учителя математики до истерики.

int c = 1;
c = c + 1;
c += 1;
c++;

Строки со второй по четвертую на самом деле делают одно и тоже: просто к значению переменной c прибавляют 1. Первый способ это прировнять c к этой же c только + 1. Старая двойка, которая там была, перезапишется новым значением, в данном случае тройкой. То же самое, что мы делали в первом блоке, но теперь не просто число, а математическое выражение. Второй способ называется сложение с присваиванием. Это действие прибавляет к изменяемой переменной то число, которое написано в правой части, и потом записывает, получившийся результат, в эту же переменную - тройка перезапишется четверкой. Есть так же -=*=/=, думаю достаточно очевидно, что они сделают. Ну и третий способ: два плюса под ряд возле переменной - четверка перезапишется пятеркой.

++/-- меняет значение ТОЛЬКО на единицу (-- отнимает единицу).

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

Попробуем записать в перменную результат математического выражения 5 / 2:

int a = 5 / 2;

На выходе мы ожидаем получить 2.5, поскольку 5 / 2 = 2.5 Но компилятор уже спешит нас обламать и выводит в консоль ровно 2. Вернемся к битам и байтам. Наше число 5 (если обрезать левые 24 нолика) выглядит вот так: 00000101 а число 2 выглядит так: 00000010

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

В следующем примере попробуем получить дробное число. Для этого воспользуемся типом float при создании переменной а:

float a = 5. / 2;

После 5 стоит точка для того чтобы указать компилятору что нам требуеться дробный ответ. Если же оставить там 5 / 2 то ответ все равно будет целым числом.

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

Попробуем разные способы установки значений для дробных чисел:

float a = 3;
a = 3.5;
a = 1.;

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


И так, мы уже знаем как создавать и работать с числовыми переменными. Но на этом много не напрограммируешь, необходимо как-то получать и обрабатывать данные из вне. Наподобие команды cout есть функция cin, которая принимает данные и записывает их в переменную.

int a;
cout << "Введи число, а после нажми Enter:" << endl;
cin >> a;
cout << "a = " << a << endl;

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

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

bool a = true;
a = 1 == 1;
a = 1 == 2;
a = a == false;
a = !a;

Строка h = 1 == 1;. Что здесь происходит? Мы сравниваем 1 и 1, равна ли единица единице. Разумеется что ответ правда, по этому на выходе мы получим значение true.

Вот сейчас очень важно, = это приравнивание, с помощью этого оператора мы задаем значение, а == это сравнивание, мы сравниваем два значения.

Есть так же:

  • ! - отрицание

  • !=- не равно

  • < - меньше

  • > - больше

  • <= - меньше или равно

  • >= - больше или равно

И так, переходим на третью строку: a = 1 == 2; 1 не равно 2 (как неожиданно), по этому результат сравнения будет false. Четвертая строка демонстрирует возможность сравнивать переменную с каким то значением или другой переменной. Поскольку на прошлой строке у нас получилась ложь, то сравнение false и false вернет, ожидаемо, правду. Ну и последняя строка, на которой мы берем значение обратное значению переменной a. Простыми словами это выглядит как не правда, что по другому является ложью.


Что ж, мы теперь полностью готовы к познанию того, как обрабатывать данные.

if(true){
	cout << "Это условие правдиво" << endl;
}

Это, так называемое, условие if(условие){ действие }. Дословно это можно перевести так: "Если утверждение в круглых скобках правдиво, то выполнить действие в фигурных скобках". В нашем случае утверждение в скобках всегда правдиво, поскольку мы туда передаем заведомо правдивое значение.

Теперь немного усложним предыдущее условие.

bool a = false;
if(a){
	cout << "Это условие правдиво" << endl;
}
else{
	cout << "Это условие ложно" << endl;
}

Вторая часть это else{ действие }, которая дословно обозначает "Иначе, если условие не выполнилось, то...". В данном случае, поскольку наша переменная a равна false, то выполниться блок else

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

int a;
cout << "Введи число, а после нажми Enter:" << endl;
cin >> a;
if(a < 5){
	cout << "a < 5" << endl;
}
else if(i > 5){
	cout << "a > 5" << endl;
}
else{
	cout << "a = 5" << endl;
}

На этот раз у нас добавился блок else if(условие){ действие }, который будет выполнен в случае если первое условие не сработало. Дословно это значит "Иначе если первое условие не сработало, то проверить другое условие". Стоит заметить, что таких блоков с дополнительными условиями может быть сколько угодно, главное соблюдать порядок: if -> else if -> else.

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

Разберем фундаментальный вид циклов. Выглядит он так: while(условие){ действие }, и дословно значит: "Повторять действие в фигурных скобках, до тех пор, пока правдиво условие в круглых".

int a = 0;
while(a != 5){
	cout << "Введи число, а после нажми Enter:" << endl;
	cin >> a;
}

Правда вот есть один нюанс, если мы поместим на место условия true, то цикл будет повторяться вечно. В нашем случае цикл будет повторяться до тех пор, пока не будет введено число 5.

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

int k = 0;
while(k < 5){
	cout << "k = " << k << endl;
	k++;
}

Мы создали переменную i, поставили в цикле условие i < 5, а в самом цикле на каждом этапе прибавляем к ее значению 1.

Теперь посмотрим на следующий тип циклов.

for (int i = 0; i < 5; i++) {
	cout << "l = " << l << endl;
}

Делает этот цикл ровно то же самое что и предыдущий, но в написании он компактнее. Разберем все по порядку. В самом начале мы объявляем переменную-счетчик (так же туда можно поместить уже объявленную переменную ранее переменную, тогда бы это выглядело так: for (i = 0; i < 5; i++) . Так же эту часть можно полностью опустить, написав вместо нее ;). Дальше мы ставим условие, в нашем случае i < 5; (эту часть также можно опустить написав просто ;, но тогда цикл будет повторяться бесконечно). Ну и последняя часть, действие которое будет выполнено при переходе на следующий круг цикла. Вместо i++ вполне написать i *= 2. В зависимости от того, что написано в последней части объявления цикла будет меняться и его поведение. К слову, это тоже можно пропустить: просто не писать ничего на том месте, тогда нам придется или где-то в другом месте как-то менять переменную i, или же цикл будет бесконечным.

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

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

int i = 0;
do {
	cout << "i = " << i << endl;
  i++;
} while (i < 5);

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

int i = 5;
do {
	cout << "i = " << i << endl;
  i++;
} while (i < 5);

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

int A[10];
cout << "A = " << A << endl;
cout << A[0] << " " << A[1] << " " << A[2] << " " << A[3] << " " <<A[4] << " " <<A[5] << " " << A[6] << " " << A[7] << " " << A[8] << " " << A[9] << endl;

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

Нумерация в массивах, и вообще в программировании в целом, начинается с нуля, по этому первый элемент массива это 0, а последний - его длинна минус один.

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

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

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

for (int i = 0; i < 10; i++) {
	A[i] = i;
}
for (int i = 0; i < 10; i++) {
	cout << A[i] << " ";
}

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

Теперь вернемся к адресам. Но для начала расскажу что такое ссылки и как их использовать. Ссылочная переменная, хранит в себе исключительно ссылку на первый байт переменной, на которую он ссылается (Каждая ячейка памяти имеет свой адрес, записанный в шестнадцатеричной системе. Для доступа к значению переменной по ссылке необходимо лишь знать ее тип данных и ссылку на первый байт). Объявляется путем прибавления к имени ссылочной переменной знака &.

int a = 2;
int &ref = a;
cout << "a = " << a << endl;
cout << "ref = " << ref << endl;
a++;
cout << "a = " << a << endl;
cout << "ref = " << ref << endl;

Теперь мы можем получить значение переменной a, обратившись к переменной ref.

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

int a = 2;
int *ptr = &a;
cout << "ptr = " << ptr << endl;
cout << "*ptr = " << *ptr << endl;

Несложно заметить, что при выводе значения переменной ptr, в консоли мы видим как раз тот адрес, о котором я говорил выше. Теперь посмотрим на вывод *ptr. Знак * берет не адрес, а значение переменной которая находиться по этому адресу - производит разыменовывание.

Ну и напоследок посмотрим на вывод &a и *&a.

int a = 2;
cout << "&a = " << &a << endl;
cout << "*&a = " << *&a << endl;

&a Достает адрес переменной a (который, к слову, и записался в переменную ptr), а *&a разыменовывает только что взятую ссылку, что, по сути, отменяет действие & (как степень и корень =) )

Отмечу так же, что ни разыменовывание, ни взятие адреса не меняет переменную a.

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

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

int A[10];
A[0] = 3;
A[1] = 1;
cout << "A = " << A << endl;
cout << "*A = " << *A << endl;
cout << "A + 1 = " << A + 1 << endl;
cout << "*(A + 1) = " << *(A + 1) << endl;

Мы получили то значение, которое записывали туда раее. Если мы напишем нечто вот такое: A + 1, то увидим еще одну ссылку, как не трудно догадаться, это ссылка на вторую ячейку массива. Внимательно посмотрев на ее адрес, мы увидим что разница между адресом первой ячейки и второй составляет ровно 4, что доказывает нам: во-первых, то что ссылка содержит адрес именно первого байта выделенного под переменную, и, во-вторых, что цело число занимает ровно 4 байта памяти. Возвращаясь к нашему массиву, если мы разыменуем ссылку A + 1, то получим значение второй ячейки, в нашем случае это 1.

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

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

Самое время поговорить о константах. Мы их раньше уже использовали, просто не придавали этому значения. Если коротко, то константа, это значение, которое не меняется на протяжении всего выполнения программы. Например, в строках int a = 4;b = 5; и c += 2; значения 45 и 2 являются константами. Вообще любые числа, которые мы явно записываем в код программы, являются константами.

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

int const а = 6;
cout << "а = " << а << endl;
float const pi = 3.14;
cout << "а + pi = " << а + pi << endl;

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

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

int const а = 6;
float const pi = 3.14;
int В[a];
B[0] = 2;
cout << B[0] << " ";
for (int l = 1; l < m; l++) {
	B[l] = B[l - 1] * 2;
	cout << B[l] << " ";
}

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

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

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

int a;
cout << "Введи число, а после нажми Enter:" << endl;
cin >> a;
int *C = new int[a];
for (int i = 0; i < a; i++) {
	cout << "Введите " << l + 1 << "-ое число" << endl;
	cin >> C[i];
}
for (int i = 0; i < a; i++) {
	cout << C[i] << " ";
}

Мы создаем переменную-указатель, и помещаем в нее новый массив целых чисел (по структуре объявления думаю понятно что происходит). В квадратные скобки мы помещаем переменную a, которую мы вводили с клавиатуры. Ну и теперь можем вручную задать значения для ячеек.

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

int D[5][5];
cout << "D = " << D << endl;
cout << "D + 1 = " << D + 1 << endl;
cout << "*D = " << *D << endl;
cout << "*D + 1 = " << *D + 1 << endl;
cout << "**D = " << **D << endl;

Надеюсь ты еще помнишь, что массив хранит в себе адреса, по которым мы можем перемещаться, а для получения значения нам нужно провести разыменовывание этих адресов. Давай посмотрим что будет, если мы выведем первый адрес на который ссылается массив. Что ж, похоже это не сильно отличается от того, что мы видели при работе с одномерным массивом. Теперь посмотрим что выведет строка D + 1. Опять адрес. Попробуй отнять от второго адреса первый. Сразу скажу что разница будет ровно 20 (нужно отнять в шестнадцатеричной системе, а потом перевести в десятичную), что значит, что одна ячейка массива D занимает 20 байт. Вспоминаем что мы создавали целочисленный массив, одно целое число занимает 4 байта, делим 20 на 4, получаем 5, значит в первой ячейке массива D содержится еще один массив из 5 ячеек. Попробуем до них добраться.

Ране мы говорили о том, как доставать значения из массива с помощью разыменовывания ссылок. Теперь попробуем сделать то же самое с массивом D и выведем это в консоль. Мы опять получили ссылку, причем ту же самую что при выводе не разыменованного D. Теперь прибавим 1 к тому что у нас получилось. Что-то новенькое. Теперь если мы от получившейся ссылки *D + 1 отнимем ссылку *D, то у нас получиться 4. Отлично, теперь разыменуем ссылку *D. Ура, мы докопались до числа, которое там лежит!

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

int D[5][5];
for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 5; j++) {
			D[i][j] = (i + 1)*(j + 1);
	}
}
for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 5; j++) {
			cout << D[i][j] << " ";
	}
	cout << endl;
}

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

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

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

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

int a;
cout << "Введи число, а после нажми Enter:" << endl;
cin >> a;
int **E = new int *[i];
for (int i = 0; i < a; i++){
	E[i] = new int [a];
}

for (int i = 0; i < a; i++) {
	for (int j = 0; j < a; j++) {
		cout << E[i][j] << " ";
	}
	cout << endl;
}

Мы создаем переменную E и указываем на двойную вложенность указателей.

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

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

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

К элементам динамических массивов так же можно обращаться с помощью ссылок и разыменовывания.


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

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

Страшно и сложно. Посмотрим на пример

int function(int a){
    int b = a * a;
    return b;
}

Теперь посмотрим как это использовать

int function(int a){
    int b = a * a;
    return b;
}

int main(){
    int a = function(5);
    cout << "5^2 = " << a << endl;
}

Что тут происходит? Мы создаем переменную a и после выполняем функцию function с аргументом 5. Это значит что внутри функции создалась переменная a, которая, к слову, никак не связанна с той переменной a которая находится в функции main. И теперь, уже в функции function, мы создаем переменную b, которую сразу инициализируем значением a * a (a в нашем случае равно 5). И в самом конце мы возвращаем переменную b, которая уже равна 25.

Теперь уже в главной функции мы записываем в переменную a значение которое вернула function, то есть 25.

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

Дополнительно

#include - пишется в начале файла. Подключает так называемые библиотеки, которые содержать функции, написанные другими программистами. Для работы с консолью нужна библиотека iostream, пишется все вместе вот так: #include <iostream>

<math.h> - библиотека с математическими функциями, такими как степень, модуль, синус, косинус, корень, и другие. Полный список можно найти в интернете.

using namespace std - Подключает стандартное пространство имен. Что это такое пока не важно, это из ООП. Важно то, что если не написать эту строку, то все придется писать с std:: в начале. Вот как будет выглядеть создание переменной и вывод в консоль:

int a = 0;
std::cout << a << std::endl;

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

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

#include <Windows.h> - Модуль, который позволяет взаимодействовать с WinAPI, проще говоря позволяет управлять системой виндовс. В нашем случае его можно использовать для этих двух строк.

SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);

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

Файл с кодом, приведенным в этой статье, ты можеш найти по этой ссылке на гитхаб

Заключение

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

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

Удачи ????

Комментарии (162)


  1. myloggerman
    30.07.2022 18:15
    -5

    Бимба, одним словом


  1. Breathe_the_pressure
    30.07.2022 18:18
    +24

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


    1. TheCalligrapher
      30.07.2022 19:53
      +7

      Вы просто похитили и перелицевали известный анекдот: ... - "Хэнде хох!" - "Но это же немецкий..." - "А, ну значит еще немецкий знаю".


  1. Shreedeer
    30.07.2022 18:23
    +16

    Ну от c++ тут только работа с выводом и string…


    1. kuschanow Автор
      30.07.2022 18:35

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


      1. ReadOnlySadUser
        30.07.2022 18:46
        +12

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

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


    1. twerqu1
      30.07.2022 18:38
      +2

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


      1. kuschanow Автор
        30.07.2022 18:42
        +1

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


        1. valergood
          30.07.2022 20:04
          +3

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


          1. playermet
            30.07.2022 21:34

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

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


            1. SIMPLicity
              30.07.2022 22:17
              +1

              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)...


              1. playermet
                01.08.2022 11:32
                +1

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

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

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


          1. TheCalligrapher
            30.07.2022 22:00
            +1

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


            1. hard2018
              30.07.2022 22:42
              +1

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


    1. lamerok
      30.07.2022 19:55

      Ссылки ещё.


    1. HemulGM
      30.07.2022 21:49

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


  1. TheCalligrapher
    30.07.2022 18:54
    +18

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

    Если быть педантичным, то += - это присваивание со сложением. Такие операторы называются операторами 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]; не хранит в себе никакие адреса. Все адреса, которые вы используете для адресной арифметики по такому массиву, вычисляются "на лету". Они не хранятся нигде в массиве.

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

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


    1. kuschanow Автор
      30.07.2022 19:01
      +1

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


    1. TheCalligrapher
      30.07.2022 19:26
      +8

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

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

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

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

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

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


      1. bogolt
        30.07.2022 23:08
        +4

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

        ха ха

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


        Впрочем никого не призываю так делать


        1. DSarovsky
          31.07.2022 10:50
          +2

          Угу, как минимум все они оказались в private-части.

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

          #include "impl/some_class.h"


        1. semenyakinVS
          31.07.2022 10:55
          +2

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

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

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


  1. p07a1330
    30.07.2022 19:23
    +1

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

    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. TheCalligrapher
      30.07.2022 19:34
      +1

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

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

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

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

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

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

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


      1. p07a1330
        30.07.2022 19:52

         Знаковый бит равен 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


        1. DmitriyN
          30.07.2022 20:13
          +4

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

          Данная программа не является программой на 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


          1. p07a1330
            30.07.2022 20:18

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


            1. DmitriyN
              30.07.2022 20:25
              +1

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


            1. TheCalligrapher
              30.07.2022 20:30

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

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


        1. KanuTaH
          30.07.2022 20:13
          +2

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

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

          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.

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


        1. TheCalligrapher
          30.07.2022 20:16
          +3

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

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

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

          Ничего подобного там не сказано. Документация по ссылке говорит именно то, что она должна говорить: 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 выделенный вами текст не значит вообще ничего.


    1. kuschanow Автор
      30.07.2022 19:38

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


      1. p07a1330
        30.07.2022 22:25
        +2

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

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

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


    1. faultedChip
      30.07.2022 20:06
      +2

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

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


    1. da-nie
      30.07.2022 20:54

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


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


  1. DVSamsonov
    30.07.2022 19:28

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


  1. iig
    30.07.2022 19:31
    +4

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

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

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

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


    1. kuschanow Автор
      30.07.2022 19:42
      +2

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


  1. GothicJS
    30.07.2022 19:44
    +1

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


    1. kompilainenn2
      30.07.2022 21:25
      +2

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


    1. iig
      30.07.2022 22:24
      +2

      leetcode


      1. AnthonyMikh
        01.08.2022 00:44
        +1

        Пожалуйста, нет. Leetcode ужасен


  1. F0iL
    30.07.2022 20:08

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

    Во-вторых, писать using namespace std; даже в начале .cpp -файлов во многих проектах считается плохой практикой, потому что может привести к очень неочевидных ошибкам.

    В-третьих, вместо #include <math.h> лучше писать #include <cmath>, вы всё-таки на C++ пишете же, в не на Си.

    В четвёртых,

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

    <string> является частью стандарта C++, поэтому не надо вводить читателя в заблуждение.


  1. da-nie
    30.07.2022 21:34
    +4

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

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


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

    а в 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, а у вас всё впереди.
    Удачи! :)


    1. TheCalligrapher
      30.07.2022 21:52

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

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

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

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

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

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


      1. da-nie
        30.07.2022 21:59

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


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

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


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

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


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


        1. iig
          30.07.2022 22:30

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

          WAT?

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


          1. da-nie
            30.07.2022 22:36

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


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


            1. iig
              30.07.2022 22:43

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


              1. da-nie
                30.07.2022 22:47

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


              1. ReadOnlySadUser
                30.07.2022 23:37

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

                Условно:

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

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


                1. randomsimplenumber
                  30.07.2022 23:57

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


                  1. ReadOnlySadUser
                    31.07.2022 00:00

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

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


                    1. 0xd34df00d
                      31.07.2022 00:02

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


                      1. ReadOnlySadUser
                        31.07.2022 00:19

                        *((int*)0x3) = 0xDEADBEAF;
                        

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


                      1. 0xd34df00d
                        31.07.2022 00:32
                        +3

                        Запись инта по такому-то адресу требует, чтобы там существовал объект соответствующего типа, и чтобы его лайфтайм был начат. Где он у вас здесь начат?


                      1. ReadOnlySadUser
                        31.07.2022 01:09

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


                      1. 0xd34df00d
                        31.07.2022 01:14

                        Что значит «где угодно»? Достаточно умный компилятор, умеющий в кросс-TU-шный анализ, увидит, что этот объект начал свой лайфтайм где-то ещё, или увидит, что по этому адресу никогда и ничего не начиналось?


                      1. ReadOnlySadUser
                        31.07.2022 01:19
                        -1

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

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


                      1. 0xd34df00d
                        31.07.2022 01:22
                        +3

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


                        И это я ещё не затрагивал тему pointer provenance, с которой мне неочевидно, что даже знающий про ld-скрипты компилятор не будет иметь права считать это UB.


                      1. ReadOnlySadUser
                        31.07.2022 01:28
                        -1

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

                        Окей.

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

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


                      1. 0xd34df00d
                        31.07.2022 01:36
                        +2

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

                        Нет, конечно, volatile тут вас не спасёт.


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

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


                      1. ReadOnlySadUser
                        31.07.2022 01:40

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

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


                      1. 0xd34df00d
                        31.07.2022 01:46

                        Я могу это сделать в любом TU в программе.

                        Да. Но если вы этого не сделали, то у вас UB.


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

                        Вопрос в том, что такое «разместить по адресу 0x3». Я утверждаю, что эти ваши ld-скрипты — это не размещение по адресу 0x3, а


                        int *ptr = nullptr;
                        while (static_cast<uintptr_t>(ptr) != 0x3)
                        {
                          delete ptr;
                          ptr = new int;
                        }

                        — это более-менее единственный вариант разместить инт по адресу 0x3.


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


                      1. ReadOnlySadUser
                        31.07.2022 01:52
                        -1

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

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

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


                      1. 0xd34df00d
                        31.07.2022 01:58
                        +3

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

                        Чего вы так привязались к этому volatile? Он не отменяет UB.


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


                      1. ReadOnlySadUser
                        31.07.2022 02:04

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

                        Мне лень ходить в стандарт, поэтому я схожу на 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


                      1. DmitriyN
                        31.07.2022 02:07
                        +1

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


                      1. ReadOnlySadUser
                        31.07.2022 02:11
                        -1

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


                      1. 0xd34df00d
                        31.07.2022 02:17
                        +1

                        Я компилятору вполне внятно сказал "по этому адресу лежит объект с типом volatile int […]"

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


                      1. 0xd34df00d
                        31.07.2022 02:15
                        +2

                        Там написано, что доступ к этому объекту не может быть соптимизирован, а не что доступ к любому не-объекту интерпретируется как доступ к объекту. volatile — это не «а я в домике, тут нет UB».


                      1. ReadOnlySadUser
                        31.07.2022 02:22

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

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


                      1. DmitriyN
                        31.07.2022 02:31
                        +2

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

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


                      1. ReadOnlySadUser
                        31.07.2022 02:37
                        +2

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

                        P.S.

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


                      1. 0xd34df00d
                        31.07.2022 02:35
                        +1

                        Почему? Будет сборка с каким-нибудь -flto — увидит он всё, что в других TU.


                        Рассчитывать на «а вдруг это в других TU» я бы не стал.


                      1. ReadOnlySadUser
                        31.07.2022 02:28
                        +1

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


                      1. TheCalligrapher
                        31.07.2022 03:00
                        +4

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

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


                      1. ReadOnlySadUser
                        31.07.2022 11:13

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


                      1. DmitriyN
                        31.07.2022 02:04
                        +2

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

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

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


                      1. TheCalligrapher
                        31.07.2022 02:51
                        +2

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

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


                      1. ReadOnlySadUser
                        31.07.2022 11:11

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


                      1. TheCalligrapher
                        31.07.2022 19:29
                        +3

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

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


                      1. DmitriyN
                        31.07.2022 01:16
                        +1

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


                      1. ReadOnlySadUser
                        31.07.2022 01:20

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


                      1. 0xd34df00d
                        31.07.2022 01:22
                        +2

                        Ну это всего лишь значит, что HAL-библиотека для STM32 написана не на C++.


                      1. ReadOnlySadUser
                        31.07.2022 01:29

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


                      1. 0xd34df00d
                        31.07.2022 01:37
                        +3

                        Так как я не видел ещё ни одной кодовой базы без UB, то, похоже, никто не программирует на C++ не только в embedded.


                      1. ReadOnlySadUser
                        31.07.2022 01:42

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


                      1. 0xd34df00d
                        31.07.2022 01:46
                        +3

                        Вы просто пессимист.

                        Это да. С плюсами иначе никак.


                        Ввиду того, что UB включает в себя всё что угодно, включая валидную программу на С++

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


                      1. ReadOnlySadUser
                        31.07.2022 01:53

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

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


                      1. KanuTaH
                        31.07.2022 01:30

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


                      1. ReadOnlySadUser
                        31.07.2022 01:35

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


                      1. KanuTaH
                        31.07.2022 01:36

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


                      1. TheCalligrapher
                        31.07.2022 19:39
                        +1

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


                      1. DmitriyN
                        31.07.2022 01:34
                        +2

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

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

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


                      1. ReadOnlySadUser
                        31.07.2022 01:37

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


                      1. DmitriyN
                        31.07.2022 01:41

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


                      1. ReadOnlySadUser
                        31.07.2022 01:46
                        -2

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


                      1. F0iL
                        31.07.2022 11:59

                        Не какой-то, а конкретный код генерируется. UB происходит не на стадии компиляции, а на стадии исполнения программы.

                        UB может сработать в том числе и на этапе компиляции, если компилятор так захочет и будет иметь на это право (вот хорошие примеры: https://habr.com/ru/company/infopulse/blog/338812/, https://markshroyer.com/2012/06/c-both-true-and-false/, https://blog.regehr.org/archives/232, https://kqueue.org/blog/2012/06/25/more-randomness-or-less/). Поэтому оно и undefined: даже если конкретный компилятор или семейство компиляторов генерируют для кода с неким UB ожидаемый программистом машинный код и все работает как надо, нет абсолютно никаких гарантий что компиляция этого кода другим компилятором/под другой таргет/с иными опциями компилятора выдаст вам такой же корректный результат в бинарнике.


                      1. q271
                        01.08.2022 09:56
                        +3

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

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

                        Например, функция 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
                        

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


                      1. TheCalligrapher
                        31.07.2022 19:45

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

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

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


                      1. DmitriyN
                        31.07.2022 01:14

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

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

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

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


                      1. 0xd34df00d
                        31.07.2022 01:17
                        +4

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

                        Ура! Возможно, в 2023-м году (а пока оно докатится до эмбеддеда/продакшена — в 2030-м) на стандартных плюсах наконец-то можно будет писать эмбеддед-код. Это — действительно близкий к железу и предназначенный для низкоуровневого программирования язык, не то что эти ваши хрусты.


                      1. KanuTaH
                        31.07.2022 01:28
                        +1

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


                      1. 0xd34df00d
                        31.07.2022 01:39
                        +1

                        Я, кстати, забыл — а что там происходит с лайфтаймом того объекта, который там уже был? Как мне работать с этим адресом из нескольких разных мест, кроме как имея глобальный флаг, который бы показывал, проинициализирован уже инт или нет?


                      1. KanuTaH
                        31.07.2022 01:43

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


                      1. 0xd34df00d
                        31.07.2022 01:48

                        Там же до этого не было никакого объекта с точки зрения C++.

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


                        А работать можно например через что-то типа синглтона Майерса — статический указатель, инициализируем однократно через placement new и все время возвращаем.

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


                      1. KanuTaH
                        31.07.2022 01:50

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

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

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

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


                      1. 0xd34df00d
                        31.07.2022 01:51

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

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


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

                        И с локом для многопоточности. Уж вот действительно не платишь за то, что не используешь.


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


                      1. KanuTaH
                        31.07.2022 01:59

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

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


                      1. TheCalligrapher
                        31.07.2022 04:51

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

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


                      1. vamireh
                        31.07.2022 23:44

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

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

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

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

                        T* p = new(buffer) T;

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


                      1. F0iL
                        01.08.2022 00:28

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

                        Тут у вас при попытке дальнейшего использования *p правила алиасинга нарушаются, не?


                      1. vamireh
                        01.08.2022 00:32
                        -1

                        Какие именно?

                        AliasedType is std::byte, (since C++17) char, or unsigned char: this permits examination of the object representation of any object as an array of bytes


                      1. F0iL
                        01.08.2022 07:53
                        +1

                        Так AliasedType в данном случае это T, а не buffer.


                      1. vamireh
                        01.08.2022 08:23
                        -1

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

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


                      1. F0iL
                        01.08.2022 08:40

                        Формулировка там такая:

                        the stored value of an object of type DynamicType through a glvalue of type AliasedType

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

                        Это подтверждается следующей фразой:

                        this permits examination of the object representation of any object as an array of bytes.

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

                        Если поискать по stack overflow, то там находится то же самое:

                        To understand the statement there, you have to realize that DynamicType refers to the type of the object, and AliasedType to the type of the expression used to access that value.

                        то есть AliasedType - это тот тип, через который вы обращаетесь к объекту (тот тип что в треугольных скобках в reinterpret_cast<>). Собственно, на том же cppreference все примеры кастуют из некотого типа в массив байт, но нет ни одного примера, где делали бы наоборот.

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

                        По сути дела, то что вы пытаетесь сделать - это каламбур типизации, и есть отличное выступление на тему того, как в C++ это делать можно, а как нельзя: https://youtube.com/watch?v=5A9NZADhTwc


                      1. vamireh
                        01.08.2022 09:30

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

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

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

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

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


                      1. F0iL
                        01.08.2022 09:56

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

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


                      1. 0xd34df00d
                        02.08.2022 07:27
                        +1

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


                        Правильно — создать объект вашего типа (на стеке или через new) и сделать в него memcpy из буфера.


                      1. ReadOnlySadUser
                        31.07.2022 00:19

                        .


                      1. mayorovp
                        31.07.2022 12:11

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

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


                      1. 0xd34df00d
                        31.07.2022 18:57

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


                    1. randomsimplenumber
                      31.07.2022 08:46

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

                      char x[100500];

                      char * a = x;

                      char * b = x+1;

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

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


                1. TheCalligrapher
                  31.07.2022 00:13
                  +2

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

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


                  1. ReadOnlySadUser
                    31.07.2022 00:22
                    +2

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

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


                    1. TheCalligrapher
                      31.07.2022 00:29
                      +2

                      Добро пожаловать в мир С++


          1. TheCalligrapher
            30.07.2022 22:41
            +1

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

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


            1. iig
              30.07.2022 23:03

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

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


              1. 0xd34df00d
                30.07.2022 23:12
                +3

                Стандарт таки запрещает. А у вас шаг индукции необоснован.


              1. KanuTaH
                30.07.2022 23:15
                +2

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

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


              1. TheCalligrapher
                31.07.2022 00:05
                +1

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

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

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

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

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

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

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

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

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

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


        1. TheCalligrapher
          30.07.2022 22:47

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

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

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


          1. da-nie
            30.07.2022 22:52

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


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

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


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

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


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


    1. KoCMoHaBT61
      30.07.2022 21:53

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

      char a[]="Hello, World!";
      char *b="Hello, World!";
      
      if(sizeof(a)==sizeof(b)) printf("Hello, World!\n");
      


      1. da-nie
        30.07.2022 21:59
        +1

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


        1. TheCalligrapher
          30.07.2022 22:08

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

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


          1. da-nie
            30.07.2022 22:09

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

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


            1. TheCalligrapher
              30.07.2022 22:20

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

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

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

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

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

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

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


              1. da-nie
                30.07.2022 22:23

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


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

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


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

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


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


                1. TheCalligrapher
                  30.07.2022 22:32

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

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

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

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

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

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


                  1. da-nie
                    30.07.2022 22:42

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


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

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


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


                    1. TheCalligrapher
                      30.07.2022 22:53

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


                  1. OlegZH
                    31.07.2022 22:35

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

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

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

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

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

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

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

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

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

                    int **b = &a;

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


                    1. mayorovp
                      31.07.2022 23:02

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


                    1. TheCalligrapher
                      31.07.2022 23:14

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

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

                      Почему?

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

                      int **b = &a;

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

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


    1. p07a1330
      30.07.2022 22:19

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

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

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


      1. OlegZH
        31.07.2022 22:24
        +1

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


        1. TheCalligrapher
          31.07.2022 22:40

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


    1. QtRoS
      31.07.2022 04:32

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

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


  1. sci_nov
    30.07.2022 21:52
    +1

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


  1. Rafriell
    30.07.2022 21:57
    +1

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


  1. Kotofay
    30.07.2022 23:10

    Вообще, на самом деле, эти звездочки должны быть рядом с названием типа данных, вот так: 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


    1. OlegZH
      31.07.2022 22:22

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

      int *p;

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

      int **dp;

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

      int (*p)(void);

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

      int * (*p)(void);

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


      1. TheCalligrapher
        31.07.2022 22:35
        +3

        int * (*p)(void);

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

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


        1. Kotofay
          01.08.2022 11:57

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

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

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


  1. nmrulin
    30.07.2022 23:58

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

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

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

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


  1. Sild
    31.07.2022 01:49
    +1

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

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

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


  1. vvzvlad
    01.08.2022 04:04
    +1

    Когда решил, что понимаешь квантовую механику плюсы, и начинаешь всем их учить


    1. TheCalligrapher
      01.08.2022 04:15
      +1

      "всем их учить "? Может "всех им учить"?


  1. AllKnowerHou
    01.08.2022 20:17

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


  1. sourcerer
    02.08.2022 10:21

    Зачем всё это, если есть куча достойных книг по 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