Указатели и доступ к памяти в ц. Быть осторожен

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

Приоритет, круглые скобки, указатели с итеративными функциями массива

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

Если у вас есть указатель

int *pt;

затем используйте его, не инициализируя его (то есть, я понимаю, что это означает без оператора типа *pt= &myVariable):

*pt = 606;

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

char *str = "Sometimes I feel like I'm going crazy.";

Там, где ссылка говорит: "Не беспокойтесь о том, где в памяти размещена строка; она обрабатывается автоматически компилятором". Поэтому не нужно говорить initialize *str = &str[0]; или *str = str; , Смысл, компилятор автоматически char str[n]; на заднем фоне?

Почему это обрабатывается по-другому? Или я полностью недопонимаю?

Ответ 1

В этом случае:

char *str = "Sometimes I feel like I'm going crazy.";

Вы инициализируете str чтобы он содержал адрес заданного строкового литерала. Вы на самом деле ничего не разыменовываете в этой точке.

Это тоже хорошо:

char *str;
str = "Sometimes I feel like I'm going crazy.";

Потому что вы присваиваете str а не разыменовываете его.

Это проблема:

int *pt;
*pt = 606;

Потому что pt не инициализируется, а затем разыменовывается.

Вы также не можете сделать это по той же причине (плюс типы не совпадают):

*pt= &myVariable;

Но вы можете сделать это:

pt= &myVariable;

После чего вы можете свободно использовать *pt.

Ответ 2

Когда вы пишете sometype *p = something; , это эквивалентно sometype *p; p = something; sometype *p; p = something; не некоторый sometype *p; *p = something; sometype *p; *p = something; , Это означает, что когда вы используете строковый литерал, подобный этому, компилятор выясняет, куда его поместить, а затем помещает туда свой адрес.

Заявление

char *str = "Sometimes I feel like I'm going crazy.";

эквивалентно

char *str;
str = "Sometimes I feel like I'm going crazy.";

Ответ 3

Упрощение строкового литерала может быть выражено как:

const char literal[] = "Sometimes I feel like I'm going crazy.";

так что выражение

char *str = "Sometimes I feel like I'm going crazy.";

логически эквивалентно:

const char literal[] = "Sometimes I feel like I'm going crazy.";
const char *str = literal;

Конечно, у литералов нет названий.

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

/* Wrong */
char *c;
*c = 'a';
/* Wrong  - you assign the pointer with the integer value */ 
char *d = 'a';

/* Correct  */
char *d = malloc(1);
*d = 'a';

/* Correct */
char x
char *e = &x;
*e = 'b';

Последний пример:

/* Wrong - you assign the pointer with the integer value */
int *p = 666;

/* Wrong you dereference the pointer which references to the not allocated space */
int *r;
*r = 666;

/* Correct */
int *s = malloc(sizeof(*s));
*s = 666;

/* Correct */
int t;
int *u = &t;
*u = 666;

И последний - что-то похожее на строковые литералы = составные литералы:

/* Correct */
int *z = (int[]){666,567,234};
z[2] = 0;
*z = 5;

/* Correct */
int *z = (const int[]){666,567,234}; 

Ответ 4

Хорошая работа с этим примером. Он хорошо показывает разницу между объявлением указателя (например, char *text;) и назначением указателя (например, text = "Hello, World!";).

Когда вы пишете:

char *text = "Hello!";

это по сути то же самое, что сказать:

char *text;        /* Note the '*' before text */
text = "Hello!";   /* Note that there no '*' on this line */

(Точно так же, как вы знаете, первая строка также может быть написана как char* text;)

Так почему же нет * на второй строке? Поскольку text имеет тип char* и "Hello!" также имеет тип char*. Здесь нет разногласий.

Кроме того, следующие три строки идентичны для компилятора:

char *text = "Hello!";
char* text = "Hello!";
char * text = "Hello!";

Размещение пробела до или после * не имеет значения. Вторая строка, возможно, легче читается, поскольку она указывает на то, что text - это char*. (Но будьте осторожны! Этот стиль может сжечь вас, если вы объявите более одной переменной в строке!)

Что касается:

int *pt;
*pt = 606;   /* Unsafe! */

Вы можете сказать, что *pt - это int, как и 606, но точнее сказать, что pt (без *) - это указатель на память, которая должна содержать int. Принимая во внимание, что *pt*) относится к int внутри памяти, на которую указывает pt (без *).

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

Теперь интересная часть о линиях:

int *pt;
*pt = 606;   /* Unsafe! */

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

Важно понимать, что *pt не является переменной (даже если она часто используется как единица). *pt относится только к значению в памяти, адрес которого содержится в pt. Следовательно, безопасность использования *pt зависит от того, содержит ли pt действительный адрес памяти. Если pt не установлен в допустимую память, использование *pt небезопасно.

Так что теперь вы можете задаться вопросом: какой смысл объявлять pt как int* вместо просто int?

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

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

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

(Однако это не относится ко всем программистам. Некоторые признают привлекательность и простоту замены указателя не указателем и с радостью изменят свой код.)

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

Ответ 5

В вашем примере есть 4 понятия, которые вы перепутали:

  1. объявление указателя int *p; или char *str; декларации указателей
  2. инициализация указателя при объявлении. char *str = "some string"; объявляет указатель и инициализирует его.
  3. присвоение значения указателю. str = "other string"; присваивает значение указателю Аналогично p = (int*)606; назначил бы значение 606 указателю. Однако в первом случае значение допустимо и указывает на расположение строки в статической памяти. Во втором случае вы назначаете произвольный адрес p. Это может быть или не быть юридическим адресом. Итак, p = &myint; или p = malloc(sizeof(int)); лучший выбор.
  4. присвоение значения тому, на что указывает указатель. *p = 606; присваивает значение 'pointee'. Теперь это зависит, является ли значение указателя 'p' допустимым или нет. Если вы не инициализировали указатель, это незаконно (если вам не повезло :-)).

Ответ 6

Много хороших объяснений здесь. ОП спросил

Почему это обрабатывается по-другому?

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

Короткий ответ

Это дизайнерское решение.

Длинный ответ

Когда вы используете литерал в asigment, у компилятора есть две опции: либо он помещает литерал в сгенерированную инструкцию по сборке (возможно, разрешает инструкции по сборке переменной длины приспосабливаться к разным длинам в байтах литерала), либо он размещает литерал там, где процессор может его достать. (память, регистры...). Для int s кажется хорошим выбором поместить их в инструкцию по сборке, но для строк... почти все строки, используемые в программах (?), Слишком длинны для размещения в инструкции по сборке. Учитывая, что произвольно длинные инструкции по сборке плохи для процессоров общего назначения, разработчики C решили оптимизировать этот вариант использования для строк и сэкономить программисту один шаг, выделив ему память. Таким образом, поведение одинаково для всех машин.

Контрпример Просто чтобы увидеть, что для других языков это не обязательно должно быть так, проверьте это. Там (это Python) константы int фактически помещаются в память и всегда получают идентификатор. Таким образом, если вы попытаетесь получить адрес двух разных переменных, которым был присвоен один и тот же литерал, он вернет один и тот же id (поскольку они ссылаются на один и тот же литерал, уже помещенный в память загрузчиком Python). Полезно подчеркнуть, что в Python id эквивалентен адресу в абстрактной машине Python.

Ответ 7

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

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

@100000 Sometimes I feel like I'm going crazy.\0

Компилятор сгенерировал код, такой, что при создании переменной str он автоматически инициализируется с адресом, где эта строка была сохранена. Таким образом, в этом примере str → 100000. Отсюда и указатель имени, str самом деле не содержит этих строковых данных, он содержит его адрес (то есть число), "указывая" на него, говоря "этот фрагмент данных по этому адресу".

Таким образом, если str обрабатывается как целое число, оно будет содержать значение 100000.

Когда вы разыменовываете указатель, такой как *str = '\0', он говорит: Память, на которую указывает str, поместите эту '\ 0' туда.

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

Например:

int *pt = blah;  // What does 'pt' point at?

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

Но случай:

int number = 605;
int *pt    = &number

*pt = 606;

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

Поэтому, когда мы используем адрес оператора & для переменной, он дает нам номер в памяти, где хранится содержимое переменной. Так что, если number переменной оказался сохраненным в byte 100040:

int number = 605;
printf( "Number is stored at %p\n", &number );

Мы получили бы вывод:

Number is stored at 100040

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

// words, words_ptr1, words_ptr2 all end up being the same address
char words[] = "Sometimes I feel like I'm going crazy."
char *words_ptr1 = &(words[0]);
char *words_ptr2 = words;

Ответ 8

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

Почему

int *pt;
*pt = 606;

не в порядке (нерабочий случай), и

char *str = "Sometimes I feel like I'm going crazy.";

в порядке (рабочий случай)?

Считают, что:

  1. char *str = "Sometimes I feel like I'm going crazy.";
    

    эквивалентно

    char *str;
    str = "Sometimes I feel like I'm going crazy.";
    
  2. Наиболее близким "аналогичным", рабочим случаем для int является (использование составного литерала вместо строкового литерала)

    int *pt = (int[]){ 686, 687 };
    

    или же

    int *pt;
    pt = (int[]){ 686, 687 };
    

Итак, различия с вашим нерабочим делом в три раза:

  1. Используйте pt =... вместо *pt =...

  2. Используйте составной литерал, а не значение (по той же причине str = 'a' не будет работать).

  3. Составные литералы не всегда гарантированно работают, так как время их хранения зависит от стандарта/реализации. Фактически, его использование, как указано выше, может привести к ошибке компиляции, получающей taking address of temporary array.

Ответ 9

Строковая переменная может быть объявлена либо как массив символов char txt[] либо с помощью символьного указателя char* txt. Следующее иллюстрирует объявление и инициализацию строки:

char* txt = "Hello";

C string literal

Фактически, как показано выше, txt является указателем на первый символ строкового литерала.

Способны ли мы изменять (читать/писать) строковую переменную или нет, зависит от того, как мы ее объявили.

6.4.5 Строковые литералы (ISO)
6. Не определено, различаются ли эти массивы при условии, что их элементы имеют соответствующие значения. Если программа пытается изменить такой массив, поведение не определено.

На самом деле, если мы объявим строку txt как мы это делали ранее, компилятор объявит строковый литерал в разделе данных только для чтения .rodata (зависит от платформы), даже если txt не объявлен как const char*. Поэтому мы не можем изменить его. На самом деле, мы не должны даже пытаться изменить его. В этом случае gcc может -Wwrite-strings предупреждения (-Wwrite-strings) или даже не работать из-за -Werror. В этом случае лучше объявить строковую переменную как указатель на константу:

const char* txt = "Hello";

С другой стороны, мы можем объявить строковую переменную как массив символов:

char txt[] = "Hello";

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

Примечание. Массив символов можно использовать так, как если бы он был указателем на его первый символ. Вот почему мы можем использовать синтаксис txt[0] или *txt для доступа к первому символу. И мы можем даже явно преобразовать массив символов в указатель:

char txt[] = "Hello";
char* ptxt = (char*) txt;