Будут ли строковые литералы смежными в памяти в C

Я столкнулся с этим, когда разыменование адреса памяти a-3 дает мне символ, присутствующий в b.

char *b = "welcome";
{
    char *a = "home";
    printf("%c", *(a-3));
}

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

Я попытался скомпилировать это с помощью онлайн-компилятора C. Он вернул символ, хранящийся в b.

Я хотел понять, как такое исполнение возможно.

Несвязано: *(a-3) написано более четко a[-3]

Ted Lyngmo 06.07.2024 08:47

Это undefined behavior, потому что вы просили a[-3], который случайно является каким-то персонажем в b

Iman Abdollahzadeh 06.07.2024 09:14

Экспериментируя, нельзя понять, что разрешено, а что нет в C, приходится читать стандарт (или хорошую книгу).

Paul Hankin 06.07.2024 10:29

halfblood_prince, Даже если бы в смежной памяти были строковые литералы "bbbbb" непосредственно перед "home" и "bbbbb" сразу после того же самого "home", printf("%c", *(a-3)); все равно не выдал бы "b", поскольку доступ через *(a-3) является неопределенным поведением.

chux - Reinstate Monica 07.07.2024 01:03
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
4
213
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Ответ принят как подходящий

не смог найти какого-либо относительного стандарта, поддерживающего это

Это потому, что его нет. Программа имеет неопределенное поведение, поскольку вы получаете доступ к "home" за пределами границ, а это означает, что программа может делать практически все, включая то, что она делала в вашем случае.

Никогда не полагайтесь на неопределенное поведение.

Программа имеет неопределенное поведение, поскольку вы пытаетесь получить доступ к области за пределами «дома», разыменовывая адреса памяти. Однако, чтобы ответить на ваш вопрос о том, как возможно выполнение, компилятор может сохранять в памяти строку символов home\0 сразу после строки символов welcome\0. Поскольку адрес, на который указывает указатель, является первым символом строки, a будет указывать на h в памяти. Если вы вычтете 3 из адреса, на который указывает a, вы вернетесь на 3 байта назад в памяти, передав нулевой символ в конце «приветствия», передав e и перейдя на m в приветствии в качестве символа для печати. Тем не менее, как уже упоминалось в другом ответе, это неопределенное поведение, и на него никогда не следует полагаться, поскольку вы получаете доступ к a за пределами строки.

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

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

Стандарт не дает особых гарантий относительно адресов строковых литералов в памяти: они должны быть разными для разного содержимого строки, но относительный порядок не определен, на самом деле относительный порядок может вообще не иметь смысла, поскольку указатели на разные массивы можно сравнивать только на равенство, а не на относительный порядок с <, <=, > или >=.

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

Строковые литералы могут даже перекрываться: "hello world" и "world" можно разместить в памяти так, что "hello world" + 6 == "world". Ранние компиляторы C использовали это преимущество для уменьшения объема памяти. Современные компиляторы и компоновщики все еще могут это сделать или нет, без гарантий.

Поведение вашей программы неопределенное, но есть портативный способ проверить эти условия, используя printf:

#include <stdio.h>

int main(void) {
    const char *a = "home";
    const char *b = "welcome";
    const char *c = "come";
    const char *d = "home";

    printf("%p: %s\n", (void *)a, a);
    printf("%p: %s\n", (void *)b, b);
    printf("%p: %s\n", (void *)c, c);
    printf("%p: %s\n", (void *)d, d);
    return 0;
}

Вывод покажет:

  • Для архитектур с линейным адресным пространством относительный порядок строковых констант в памяти
  • являются ли строки, на которые указывают a и d, одинаковыми в памяти или разными.
  • является ли строка "come" суффиксом строки "welcome" или отдельной строковой константой.

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

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

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

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

Выполнение вашего кода невозможно, поскольку он не компилируется. Но в любом случае выражение, которое вы используете (*(a-3)), на самом деле эквивалентно a[-3] или даже (-3)[a] --- поскольку последнее является устаревшей конструкцией, которая лишь утверждает конмутативность сложения, но датируется эпохой первых компиляторов, поэтому, пожалуйста, не используйте *(a-3), когда хотите сказать a[-3]), указывает на три места из массива строковых литералов. Если вы исправите код, чтобы его можно было компилировать, выполнение по-прежнему возможно, поскольку в C проверка границ массива не выполняется (по соображениям эффективности), и именно это вызывает неопределенное поведение. Неопределенное поведение означает, что исполняемый код может быть создан, но результаты выполнения совершенно непредсказуемы.

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

Я хотел понять, как такое исполнение возможно.

Есть как минимум три способа подумать об этом:

  1. Как объяснялось в нескольких других ответах, поведение здесь неопределенное, а это означает, что может случиться что угодно. Программа могла напечатать «добро пожаловать». Или, теоретически, он может напечатать «привет», или «до свидания», или «djwjnsdvjnvjsd», или ничего, или что угодно.
  2. Да, компиляторы склонны хранить строки в более или менее одной и той же части памяти. Поэтому неудивительно, если одна строка таким образом окажется «рядом» с другой.
  3. Очевидно, вы никогда не захотите зависеть от подобных вещей в «настоящей» программе. Может показаться, что сегодня это работает, но это не работает по правильным причинам, поэтому может перестать работать завтра.

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

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

halfblood_prince 07.07.2024 11:30

Другие вопросы по теме