LINUX.ORG.RU

Указатель на указатель в си

 


0

2

Почему для изменения значения переменной через доступ к указатель указателя необходимо использовать две звезды * * в примере ниже, а write хватает * звезды чтобы добраться до значения?

    char test = 'a';
    char *ptr_to_char = &test;
    char **ptr_to_ptr = &ptr_to_char;
    
    *ptr_to_ptr = 'd'; 
/* Incompatible integer to pointer conversion assigning to 'char *' from 'int'*/

    **ptr_to_ptr = 'b';
/* works */
    
    write(1, *ptr_to_ptr, 1);
/* also works */


Последнее исправление: dffrwpv (всего исправлений: 4)

Почему для изменения значения переменной через доступ к указатель указателя необходимо использовать две звезды * * в примере ниже, а write хватает * звезды чтобы добраться до значения?

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

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

PS. И, кстати, это очень хороший вопрос, поставил палец вверх за него.

Vic
()
Последнее исправление: Vic (всего исправлений: 4)

Потому что *ptr_to_ptr вернет адрес – число, скорее всего 64 бита (если у тебя 64 битная система). Так что ты пытаешься сделать что то типа 0x7f179f01869c = 'd', т.е. использовать адрес переменной как число. Тебе нужно делать *0x7f179f01869c = 'd'. Т.e, сказать компилятору ‘положи байт в ячейку по адресу 0x7f179f01869c’.

dup
()
Ответ на: комментарий от Vic

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

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

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

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

write(1, *ptr_to_ptr, 1);

собирается без проблем потому что ее прототип такой

ssize_t write(int fd, const void *buf, size_t count);

т.е. buf это указатель, *ptr_to_ptr вернет адрес test (&test) – работа идет с указателями – все нормально (почти).

В случае с *ptr_to_ptr – будет возвращен тип значения char * к которому будет присваиваться переменная типа int на что ,собственно, и указывает компилятор.

dup
()
Ответ на: комментарий от dup

dup, не смотри на сообщение об ошибке которое у автора в вопросе.

Суть вопроса автора вот в чем.

char test = 'a';
char *ptr_to_char = &test;
Автор вопроса спрашивает, почему отличается синтаксис (1) и (2) и почему в варианте (2) нет звездочки. Понятное дело, что автор немного в звездочках запутался.
*ptr_to_char = 'b'; // (1)
write(1, ptr_to_char, 1); // (2)

Си, когда генерирует машинный код, «видит» это так:

<адрес переменной> = <источник значение переменной/константы>; // (1)
write(<источник значения переменной/константы>, <источник значения переменной/константы>, <источник значения переменной/константы>); // (2)

// Что бы добраться до значений в "<значение переменной/константы>", компилятор вставляет код чтения значения из переменной/константы, которую ты указал как источник значения в строке с write().
В то же самое время, в исходнике, ты должен во write() помещать сами переменные, а значения из них будет доставать сгенерированный компилятором код, в зависимости от того, где компилятор ранее разместил исходные переменные (в памяти, на стеке текущей функции (где стоит вызов write() или в регистре процессора).

PS.

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

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

Если я правильно помню, то в С++ эту оплошность исправили и в функцию стало можно передавать саму переменную (ее адрес для генерации когда функции), прикрутив это костылями к понятию «ссылка».

PPS.

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

Vic
()
Последнее исправление: Vic (всего исправлений: 6)
Ответ на: комментарий от Vic

Автор вопроса спрашивает, почему отличается синтаксис (1) и (2) и почему в варианте (2) нет звездочки. Понятное дело, что автор немного в звездочках запутался. варианте (2) нет звездочки.

У автора в примерe звездочка таки есть.

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

__attribute__ ((noinline)) void wow(int *a) {
  *a = 0x777;
}

int main (void) {
  int a = 0x666;
  wow(&a);
  printf("%x\n", a);
  return 0;
}

Какое значение по итогу будет распечатано?

Если я правильно помню, то в С++ эту оплошность исправили и в функцию стало можно передавать саму переменную (ее адрес для генерации когда функции), прикрутили это к понятию «ссылка».

Ссылка по сути есть указатель.

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

Согласен, я так и делаю в конечном счете.

dup
()
Ответ на: комментарий от dup

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

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

Ээээ, хм, что то я не понял уже … А как же пишут проги где есть обращние к не выделенной памяти ? Разве компилятор может пропустить конструкцию типа адрес + число ?

mx__ ★★★★★
()
Ответ на: комментарий от dup

У автора в примерe звездочка таки есть.

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

Какое значение по итогу будет распечатано?

Очевидно, что «777».

Согласен, я так и делаю в конечном счете.

Отлично! Вот вам кусочек листинга ассемблера вашего примера. Самое интересное то, как аргументы для printf() будут размещены на стеке (это как раз касается исходного вопроса от автора):

LC0:
	.ascii "%x\12\0"

		# printf("%x\n", a);
		mov	eax, DWORD PTR [esp+28] # Чтение значения переменной "a" в регистр eax
		mov	DWORD PTR [esp], OFFSET FLAT:LC0 # Первый аргумент, адрес первого символа строки "%x\n".
		mov	DWORD PTR [esp+4], eax # Второй аргумент, значение регистра eax (значение переменной "a") в стек, следом за адресом строки.
		call	_printf # Вызов printf().
Как я и описывал выше, компилятор вставил код, читающий значение переменной «a», что бы передать ее в функцию printf(). Согласно правилам си, для параметра «%x» функция printf() ожидает значение в виде uint32_t (т.е. 32-битный DWORD, т.к. я использую 32-битный компилятор MinGW32 в составе code::blocks), который функция printf() будет рассматривать как число, которое надо вывести в шестнадцатеричном виде.

А вот Си-строчки, которые на самом деле массивы, согласно правилам си (как и все массивы), передаются не как значение, а как адрес первого символа, что учитывается кодом реализации самой функции printf().

Ссылка по сути есть указатель.

Для нативных языков (чьи модели размещения в памяти приближены к модели процессора):

  • «Адрес» - номер байта в памяти (в адресном пространстве) процессора.
  • «Переменная» - область памяти процессора, размером столько байтах, сколько надо для хранения переменной заданного типа.
  • «Указатель» - целочисленная переменная без знака, хранящая адрес. Т.е. это обычная переменная, но наделенная особым смыслом, что бы ее отличать от других переменных.

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

Vic
()
Ответ на: комментарий от mx__

В молодости я писал на ассемблере для 68000, но там прямая адресация и когда перешел на 286 пытаясь понять его конструкцию и относительную адресацию понял что на ассемблере тут писать себе дороже

Ну, по сути, там не то что бы адресация была не линейная, просто вычисление адреса было муторным для программиста, из-за особенностей железа того времени. Слава богу, теперь это в прошлом и когда доходит дело до программ, адресация линейна. Мучаются только разработчики биосов, т.к. для совместимости, даже 64-разрядные процессоры стартуют в вот в том самом «реальном» x86 режиме с муторным вычислением адреса. Это вот то самое легаси, от которого все хотят отказаться очень давно, но не могут, т.к. пока есть популярные операционки, код которых рассчитан на старт в «реальном» режиме. Как исчезнут, так и то легаси само собой отвалится - UEFI, как я понимаю, это как раз первый шаг, там уже загрузчики операционных систем сделаны в виде обычных файлов, что позволит в дальнейшем инсталяторам операционок записывать нужный загрузчик в зависимости от того. в каком режиме стартанет процессор.

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

Сейчас я вас еще больше ошарашу - у x86 процессоров два адресных пространства:

  • ОЗУ - память, откуда процессор берет команды и данные для выполнения. Для этой области есть команды чтения и записи по номеру байта. Эту область мы для краткости и называем адресным пространством процессоров.
  • Ввод-вывод - память, откуда процессор может только читать или записывать. Для этой области есть команды чтения и записи по номеру «порта». Эта часть появилась из-за аппаратуры того далекого времени, которое подключалось на отдельные шины ввода вывода.

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

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

А как же пишут проги где есть обращение к не выделенной памяти ? Разве компилятор может пропустить конструкцию типа адрес + число ?

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

Контроль осуществляют операционные системы и различные библиотеки. Только им ведомо, что конкретно находится в данной области адресного пространства. Это они назначают программа области и отслеживают, что если программа попытается за них обратиться, то ее обычно «снимают». Конечно, вся эта кухня c отслеживанием требует поддержки со стороны процессора (есть кучи таблиц, по которым процессор проверяет каждую команду на корректность обращения к диапазону адресов, эти таблицы настраивает операционка).

Vic
()
Последнее исправление: Vic (всего исправлений: 5)
Ответ на: комментарий от Vic

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

Шина адреса у 8086/88-х процессоров была 20-ти битная, а регистры 16-ти битные. В результате 1 Мб адресного пространства ну никак не получалось линейно адресовать.

Кроме того, была попытка сделать 86-е обратно совместимыми с 8080. Идея была, что код с 8080 укладывается в пределах сегмента 8086. Я на 8080 не кодировал на ассемблере и их опкоды не очень знаю, но кажется частично они таки совпадают, если используют AX как аккумулятор (регистр А для 8080) и ближние переходы внутри сегмента. На практике то ли там не все совместимо, то ли в любом случае аппаратура разная и работать так смог бы почти только чисто вычислительный код.

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

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

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

praseodim ★★★★★
()
Ответ на: комментарий от shTigrits

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

praseodim ★★★★★
()
Ответ на: комментарий от praseodim

Не совсем правильно спросил. В машинном коде отличаются указатель на Int и указатель на Long int? Я, как и Lovesan, всегда думал, что это всегда число равное разрядности шины адреса, в независимости от размерности указываемых данных. А о размерности «адресной арифметики» уже голова болит у компилятора

shTigrits ★★★
()
Последнее исправление: shTigrits (всего исправлений: 2)

Я придумал гениальное объяснение, берите бесплатно. Указатель - это как адрес прописки жильца. Адрес прописки нельзя поменять, просто подкорректировав его например на конверте, так меняется только конкретная копия адреса, и все, кто ничего не корректировал, и не видит скорректированный адрес, будут по-прежнему использовать оригинальный адрес прописки. Чтобы изменить адрес для всех, надо обратиться в соотв. госорган регистрации, по соотв. адресу. Т.е. это адрес (контора), по которому находится объект (БД), который хранит другой адрес (жильца).

seiken ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Не обижайся, просто кому то дано только формы через Visual Studio 2008 накидывать.

А что ты имеешь против UI/UX инженеров? Вот в Линуксе с ГНОМОМ с этим большие проблемы.

seiken ★★★★★
()
Ответ на: комментарий от shTigrits

В машинном коде отличаются указатель на Int и указатель на Long int?

И да и нет.

  • Адрес - никак не отличается, т.к. любой адрес - это просто номер байта.
  • А вот команда, с помощью которой будет производиться, например, чтение памяти по указанному адресу - будут разные, как и второй операнд, в который будет помещено значение. Т.к. для Int надо скопировать меньше байт, а для Long int больше.

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

PS. Да простит меня praseodim, за то что я быстрее ответил.

Vic
()
Последнее исправление: Vic (всего исправлений: 2)
Ответ на: комментарий от seiken

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

Vic
()
Ответ на: комментарий от mx__

Почему компилятор пропускает такое ?

Потому что с/c++ компилятор возлагает данную задачу именно на программиста.

но я что то не понимаю как можно читать и т.д. выходя за границы памяти

#include <stdio.h>


struct SomeStruct {
  int a;
  int b;
  int c;
  int r;
};

int main (void) {


  struct SomeStruct s = {};

  unsigned long idx = sizeof(s);

  unsigned long ccc = *(unsigned long *)((unsigned long)(&s) + idx);
  printf("%lx\n", ccc);
  return 0;
}

dup
()
Ответ на: комментарий от mx__

Почему компилятор пропускает такое ?

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

Ведь понятие «выход за границу памяти» — это чисто человеческое, субъективное понятие, придуманное для контроля работы алгоритмов конкретной программы, что бы программа не заставляла процессор читать/писать в области адресов, которые ей не положены по логике ее работы.

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

С точки зрения процессора — «ошибки выхода за границы» не существует, т.к. у процессора есть адресное пространство, процессор может читать и писать в любое место своего адресного пространства. Программа лишь управляет процессором, что куда и откуда читать/писать.

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

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

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

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

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

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

  • или загрузить только то, что поместиться в массив
  • или отказаться от загрузки
  • или оповестить пользователя
  • или еще что-то, что зависит только от логики работы программы

В некоторых языках стараются придумывать всякие встроенные защитные механизмы, но они замедляют работу (т.к. добавляется множество проверок) и все-равно принципиально не защищают от всех ошибок. Поэтому, единого решения про ошибки — нет, споры идут до сих пор. Например, вот так пытаются делать в RUST, заставляя программиста обрабатывать потенциальные ошибки: https://habr.com/ru/companies/otus/articles/579538/

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

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

Vic
()
Последнее исправление: Vic (всего исправлений: 6)
Ответ на: комментарий от dup

Компилятор gcc на моей машине тебя перехитрил и не стал переменную idx помещать следом за s.

    ...
    printf("%lx (%p+%lx != %p)\n", ccc, &s, idx, &idx);
    ...

    gcc version 8.1.0 (i686-posix-dwarf-rev0, Built by MinGW-W64 project)

    4c (0028FED0+10 != 0028FECC)

Судя по ассемблерному листингу, он поместил idx перед s (имеет полное право так делать).

	...
	# struct SomeStruct s = {};
	mov	DWORD PTR [esp+48], 0	 # s
	mov	DWORD PTR [esp+52], 0	 # s
	mov	DWORD PTR [esp+56], 0	 # s
	mov	DWORD PTR [esp+60], 0	 # s
	# unsigned long idx = sizeof(s);
	mov	DWORD PTR [esp+44], 16	 # idx
	...
Классический пример, когда вышли за логичные пределы памяти внутри своей программы. В данном случае, прочитали память из свободных ячеек памяти стека, что с точки зрения логики программы имеет смысл «прочитали какой-то мусор».

Vic
()
Последнее исправление: Vic (всего исправлений: 3)
Ответ на: комментарий от dup

Я понимаю что Вы меня закидаете помидорами и прочим, но я сейчас попытаюсь сформулировать более правильно (если получится)

unsigned long ccc = *(unsigned long *)((unsigned long)(&s) + idx);

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

Может показаться и странно но я задумался а зачем позволяются вообще какие-то математические операции с указателями ?

Внутри структуры мы работаем с ее переменными, а снаружи … а туда и не нужно лазить. Зачем ?

mx__ ★★★★★
()
Ответ на: комментарий от mx__

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

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
    char *String = NULL; // Указатель на строку в памяти: в переменной String всегда 
        // будет адрес первого символа строки в памяти.
    int StringLen = 0; // Длина строки, без учета символа "конец строки".
    char Symbol = (char)0; // Сюда будем получать символ с клавиатуры.

    String = malloc(StringLen+1); // Программе нужен 1 символ под "конец строки".
        // Функции malloc()/realloc() по сути просят выделить памяти программе в 
        // адресном пространстве процессора, а все контрольные механизмы получают
        // и запоминают выделенную область.
        // Т.е. программа прямо в процессе своей работы просит выделить ей память.
    *(String+StringLen) = (char)0; // Поместить "конец строки" в строку. Теперь строка 
        // по адресу из указателя String, является существующей и пустой строкой.
    printf("Enter string and press Enter: ");
    while (Symbol == (char)0)
    {
        Symbol = getchar(); // Получить символ с клавиатуры.
        if (Symbol != '\n')
        {
            StringLen++; // Увеличить размер строки на 1 символ.
            String = realloc(String, StringLen+1); // "Расширить" память на 1 символ.
            *(String+StringLen-1) = Symbol; // Поместить символ в строку.
            Symbol = (char)0;
            *(String+StringLen) = Symbol; // Поместить символ "конец строки" в строку.
        };
    };
    printf("\nString = \"%s\"\n", String);
    free(String); // Сообщить всем, что эта память больше не нужна.
    String = NULL; // Программа помечает себе, что памяти под строчку больше нет, чтобы
        // случайно не начать ей пользоваться. NULL - это защитный адрес в памяти,
        // т.е. если программа попробует что-то читать или писать по адресу NULL, то
        // всем защитным механизмам станет ясно, что программа ошиблась.
    return 0;
}

Vic
()
Последнее исправление: Vic (всего исправлений: 10)
Ответ на: комментарий от Vic

Спасибо.

И что ? Вы выделяете и потом используйте. Не вижу противоречий.

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

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

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

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

P.S. Странно, все понимают разницу между статическими и динамически переменными. Почему нельзя юзать переменую до ее обьявления. Но почему такой клинч с памятью ? Почему ее можно юзать до ее выделения ?

mx__ ★★★★★
()
Последнее исправление: mx__ (всего исправлений: 1)
Ответ на: комментарий от mx__

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

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

Повторюсь, у процессора нет понятия «выделил память» или «не выделил память» - это все человеческие понятия, рамки для алгоритмов программ, что бы они работали так, как надо человеку, т.е. вот что-то типа такого: «Эй, гражданина! Ты туда не ходи, ты сюда ходи. А то снег в башка попадет.», только для алгоритмов.

Т.е. любой программе на си и с++, как процессору, доступно все адресное пространство, а вот это вот все «выделил»/«не выделил», «туда»/«не туда» записал - это дополнительные внешние рамки для внешнего контроля правильности работы алгоритмов.

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

И что ? Вы выделяете и потом используйте. Не вижу противоречий.

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

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

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

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

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

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

К сожалению, далеко не все.

Статическая и динамическая переменная - это понятия которые человек сделал для себя на основе собственного критерия, который выглядит так:

По способу вычисления адреса переменной в процессе выполнения программы, переменные делятся на «статические», «автоматические» и «динамические».

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

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

«Объявление переменной» - это тоже чисто человеческое понятие, которое заключается в том, что человек объявляет (резервирует/назначает/выделяет/задает) некую область памяти в адресном пространстве как «переменную» и все, кто будет работать с этой областью памяти, должен начать соблюдать правила работы согласно «типу» «переменной», иначе логичной работы не получится.

Правило «объявление переменной» наводит порядок в исходном тексте программы и это самое первое правило, до которого додумались люди при написании программ. Оно реализовано даже в ассемблере и его использование всячески поощряется. Правда оно не запрещает и не отменяет понятия адресного пространства и работу с ним без «объявления переменной», т.к. без этого все-равно никуда и надо уметь писать/читать области адресного пространства без объявления переменной, например, при межпрограммном обмене, где программы пишут в память друг другу или в общую, межпрограммную память, которая не относится ни к какой программе.

PS.

На мой взгляд, что бы хорошо понять все это, лучше всего попробовать поставить себя на место процессора. Для этого надо взять листочек в клеточку, которое будет имитировать адресное пространство, а каждая клеточка это 1 байт. Придумать пару простых операций (mov, add, goto ...) и мысленно пробовать выполнять команды, закодированные на этом листочке в клеточку. Там вы как раз начнете задумываться, а как закодировать адрес, значения и т.п.

Vic
()
Ответ на: комментарий от Vic

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

Вы не внимательно прочитали. Я же вроде указал что это пример не правильный. Чтобы проще было понять напишу так : арифметические операции с указателями в случае ВЫДЕЛЕНИЯ памяти - допустимы. Все остальное НЕТ.

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

P.S. Когда 35 лет назад писал проги на ассемблере, то все в начале писалось на бумагу, с принтерами было туго, и только потом переносилось в комп.

mx__ ★★★★★
()
Последнее исправление: mx__ (всего исправлений: 2)
Ответ на: комментарий от mx__

Вы не внимательно прочитали. Я же вроде указал что это пример не правильный. Чтобы проще было понять напишу так : арифметические операции с указателями в случае ВЫДЕЛЕНИЯ памяти - допустимы. Все остальное НЕТ.

Парсить любую строчку, тоже будете не используя арифметические действия с указателем? При парсинге будете писать разные варианты для строчки, выделенной в динамической памяти и для строчки в автоматической переменной или константе? (вопросы риторические)

PS.

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

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

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

Vic
()
Ответ на: комментарий от Vic

(вопросы риторические)

А что компилятор не парсит строки ? А много проходов ? (ответы риторические)

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

P.S. Как сейчас работает компилятор си/c++ я более менее представляю, я просто про то что люди его писавшие не продумали все, пусть будет а нужно это или нет это уже решать самим программерам. Ну это нормально, нельзя сразу что написать, постоянно будут вылазить косяки, главное чтобы до них потом дошло что вот ЭТО тут не нужно.

mx__ ★★★★★
()
Ответ на: комментарий от mx__

Я изначально прекрасно понял про что вы.

Вы предъявляете претензии про арифметические операции с указателями не тем компиляторам. Компиляторы Си/С++ работают с указателями так, как изначально и задумано, а нее потому что кто-то в чем-то ошибся при их разработке. Потому что эти компиляторы созданы под архитектуру процессоров, под их информационную модель работы с адресным пространством.

PS.

А что компилятор не парсит строки ? А много проходов ?

А что, компиляторы Си/Си++ написаны на каком-то другом компиляторе? И/или им не надо память выделять у процессора под переменные, строки и для всего остального, о чем я вам писал во всех остальных ответах?

Vic
()
Последнее исправление: Vic (всего исправлений: 1)
Ответ на: комментарий от Vic

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

А это разве не одно и тоже ?

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

mx__ ★★★★★
()
Ответ на: комментарий от mx__

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

то есть сишное выражение array[i] = 10 транслируется в одну команду проца

поддержки «массивов» в процах нет, там есть просто поддержка индексации относительно некоей базы. то есть встроенная «адресная арифметика».

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

Вот я к этому и веду, си не асм, и тут вообще можно обойтись без этого.

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

mx__ ★★★★★
()
Ответ на: комментарий от Vic

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

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

И если ты не соображаешь что делаешь то не делай этого, так как ЛЮБУЮ программу на Си можно написать и не используя эту фичу.

mx__ ★★★★★
()
Ответ на: комментарий от mx__

Отсюда вопрос, что это за проги такие что выходят за границы памяти ?

Например, проги, реализующие менеджер кучи (памяти).

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

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

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

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

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

praseodim ★★★★★
()
Ответ на: комментарий от praseodim

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

Берешь старую прогу запускаешь на современной винде и как часто падает в корку, начинаешь глядеть и там 9 из 10 паскаль…

Может из того что в свое время все эти программульки только на Дельфи/БордандС и крапали, то ли у них какой то рантайм левый … хз.

mx__ ★★★★★
()
Последнее исправление: mx__ (всего исправлений: 1)
Ответ на: комментарий от mx__

Берешь старую прогу запускаешь на современной винде и как часто падает в корку, начинаешь глядеть и там 9 из 10 паскаль…

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

praseodim ★★★★★
()
Последнее исправление: praseodim (всего исправлений: 1)
Ответ на: комментарий от lovesan

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

Иди гугли provenance, клоун.

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

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 2)
Ответ на: комментарий от lovesan

Хорошо, зайдем от противного - что тогда такое указатель? Это объект? Что там кроме числа? Тайптег? Дескриптор типа? Размерность?

Указатель в C – это такой такой тип, значение которого указывает на другой объект либо равно NULL. Его можно кастовать к числу (intptr_t) и обратно, но сам он числом не является.

hateyoufeel ★★★★★
()
Ответ на: комментарий от hateyoufeel

Указатель в Си - это переменная, которая хранит значение (си-типа) «адрес».

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

Просто адрес (не си-тип, а исходное понятие из мира процессоров) - это номер байта в адресном пространстве процессора (в его основном адресном пространстве, которое ассоциировано с ОЗУ).

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

PS. Вообще, у процессоров может быть не одно адресное пространство. Например у x86 два адресных пространства для которых применяются разные команды: «Memory» и «I/O» ( https://stackoverflow.com/questions/3215878/what-are-in-out-instructions-in-x86-used-for )

Vic
()
Последнее исправление: Vic (всего исправлений: 1)
Ответ на: комментарий от Vic

Так что под капотом, адрес в Си - это число.

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

Конечно, тут хардкорные сишники, давно пробухавшие свой мозг, могут заявить, что строки – это тоже числа. Но у них всё числа. Даже небо, даже Аллах, даже жоп^W^W!

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 3)
Ответ на: комментарий от hateyoufeel

Не, это особенность реализации.

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

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

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

Vic
()
Ответ на: комментарий от Vic

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

Какой такой смысл? В стандарте нет такого термина и стандарт не требует, чтобы в понятие «указатель» был вложен какой-либо смысл. Указатель должен идентифицировать объект в памяти. Больше он ничего не должен.

Даже если ты реализуешь «адреса» внутри как строки, с этими «строками» ты будешь делать все те же операции, как и с числами.

Нет, не будешь. Ты давно делил или умножал указатели? Вот именно!

Только вот со «строками» делать «+1» а к «адресу» будет ой как не просто.

Почему? У тебя просто крайне бедная фантазия.

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

Отнесите меня, пожалуйста, к голым бабам. Мне их компания весьма приятна.

hateyoufeel ★★★★★
()
Последнее исправление: hateyoufeel (всего исправлений: 1)