LINUX.ORG.RU

Си, учебная задача, массив, указатели

 , ,


0

3

Не знаю какими словами спросить у поисковых систем. Код ниже выдает такой результат:

690165708 14754882 1571426279 748212300 546573552 710529569 1908956059 1365401208 1566297428 705403694

690165708 14754882 1571426279 748212300 925961456 909718834 875771960 359798784 1566297428 705403694

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

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

int* GenTwoDigitRand(int qTty);

int main(int argc, char **argv) {
	int const mySize = 9;
	int* myArray = GenTwoDigitRand(mySize);
	
	for (int i = 0; i <= mySize; ++i) {
		printf("%d ", myArray[i]);
	}
	
	return 0;
}

int* GenTwoDigitRand(int qTty) {	
	srand(time(NULL));
	int myArray[qTty];
	int myRand = 0;
	for (int n = 0; n <= qTty; ++n) {
		myRand = rand();
		myArray[n] = myRand;
	}
	int *p = myArray;
	
	for (int n = 0; n <= qTty; ++n) {
		printf("%d ", myArray[n]);
	}
	printf("\n");
	
	return p;
}

Подскажите что прочитать или с каким запросом лезть в поисковик. Спасибо.



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

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

Фишка которая тебя запутывает в том что твоя программа может даже работать правильно, но через раз. Так как время существования данных в myArray по указателю p это случайная величина с момента выхода из GenTwoDigitRand ибо ты ссылаешься на уже освобождённую память на стеке, которая активно переиспользуется, другими частями, твоей же программы.


Подскажите что прочитать или с каким запросом лезть в поисковик. Спасибо.

  • Области видимости переменных в языке Си
  • Стек и куча язык Си
LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от Anoxemian

Компилятор скорее будет ругаться на return локальной переменной (протрассировать что в p будет именно она элементарно, ведь присвоение там единственное. Но видимо не ругается.

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

firkax ★★★★★
()

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

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

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

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

Это запись в ячейку, на 9 int-ов большую чем начало массива. Если туда сначала что-то записать, а потом сразу прочитать - прочитается то же самое что записали. Если же не записывать а только читать - то мусор, да, но это и ячейки [0] касается, и вообще всех локальных переменных кроме static.

Вполне возможно, что компилятор в этой ячейке расположит переменную int myRand или int *p или (только в 64-бит) выравнивание до 8-байтной границы. В целом зависит от реализации и иногда даже от опций компиляции. Если там выравнивание, то писать туда вообще полностью безопасно, считай компилятор подарил ещё 1 элемент.

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

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

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

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

UB, как оно есть.

НЕ НАДО гадать, как оно будет, это абсолютно бесплодное (и для программиста – вредное) занятие. Просто не надо так делать.

В целом зависит от реализации

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

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

У меня под рукой.

$ gcc -Wall -Wextra -Wpedantic main.c
main.c: In function ‘main’:
main.c:7:14: warning: unused parameter ‘argc’ [-Wunused-parameter]
    7 | int main(int argc, char **argv) {
      |          ~~~~^~~~
main.c:7:27: warning: unused parameter ‘argv’ [-Wunused-parameter]
    7 | int main(int argc, char **argv) {
      |                    ~~~~~~~^~~~

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

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

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

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

программа должна собираться разными компиляторами

Вовсе не всегда. Есть обратный подход: конкретную версию конкретного компилятора объявляют зависимость проекта (такой же как какой-нить libjpeg) и компилируют только им.

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

То увеличение, про которое я писал, оно не из-за того что в [9] что-то записали, а из-за того, что размер int[9] - 36 байт, не кратно восьми, а на 64-битных системах в ряде случаев рекомендуется, а где-то даже требуется, выравнивать переменные по 64-битной границе. То есть реальный размер округлится до 40 байт. Но, повторю, всё зависит от разных обстоятельств.

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

Подскажите что прочитать

Области видимости (scope) переменных.

  int* GenTwoDigitRand(int qTty) {
  ...
  int myArray[qTty];
  int *p = myArray;
  return p;
}

как так получаеться, что массив полученный в функцию main, частично отличаеться от того

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

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

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

Си – низкоуровневый язык. Откуда он должен память брать? Если речь про ОС, то при запуске программы вся память заранее выделена. Её размер нужно увеличить, её надо у ОС запрашивать.

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

Jullyfish
()

Дома, с компилятором под рукой, сделал красивое:

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

void
GenTwoDigitRand(int qTty, int * myArray)
{
	srand((unsigned)time(NULL));

	for (int n = 0, myRand = 0; n < qTty; ++n)
	{
		myRand = rand();
		printf("%d ", myRand);
		myArray[n] = myRand;
	}

	puts("");
}

int
main (void)
{
	const int mySize = 9;
	int myArray[mySize];
	GenTwoDigitRand(mySize, myArray);

	for (int i = 0; i < mySize; ++i)
		printf("%d ", myArray[i]);
	puts("");

	return 0;
}

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

Можно было бы выделять память в calloc() внутри GenTwoDigitRand(), но если можно работать без динамической памяти - то почему бы и нет.

Пофикшены неправильные границы цикла.

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

сделал красивое

(Так, попридираюсь.)

Почему GenTwoDigitRand не static void?

Почему у main ( есть пробел перед скобочкой, а у GenTwoDigitRand( нет?

Я бы весь вывод сделал через printf(), чтобы не плодить puts(""), но это уже на вкус и цвет:

printf("%d%с", myArray[i], i != mySize-1 ? ' ' : '\n');

И чтобы не плодить лишний пробел при выводе текста.

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

НЕ НАДО гадать, как оно будет, это абсолютно бесплодное (и для программиста – вредное) занятие.

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

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

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

На самом деле неоднозначный вопрос. Если индексы отрицательные - это обычно какой-то баг, а если баг, то в общем-то какая разница в int он или unsigned int? Ну и иногда отрицательный индекс таки используется, если указатель не на начало структуры.

Мне кажется, в процессе начального изучения Си лучше не заморачиваться такими тонкостями

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

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

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

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

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

Кстати, про unsigned int, кое-что новичку вообще странным может показаться, например,

#include <stdio.h>
int main()
{
   unsigned int a= 0;
   unsigned int b = 0;
   printf("a - 1 = %d\n",a-1);
   b = a-1;
   printf("b = %d\n",b); 
   return 0;
}

Результат работы:

a - 1 = -1
b = -1

WTF? :))) А модификатор в printf тогда надо использовать %u, а не %d Все этого сугубое IMHO, конечно, но из-за таких фокусов, по-моему, лучше как раз не использовать unsigned типы без явной необходимости.

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

Нет, как раз надо.

Потом начинают сувать эти int-ы везде, из-за int-ов получаются знаковые переполнения в неожиданных местах (потому что отрицательные числа отжирают половину диапазона, даже на 32 битах на это можно напороться, хоть и сложно, а на 64 так вообще легко) + если неудачные опции компилятора то ещё и UB (знаковое переполнение) с общей порчей программы.

А ещё диапазон беззнакового индекса проверяется максимально наглядным index<size, а со знаковым приходится делать index>=0 && index<size которое хоть и не сильно длиннее, но создаёт на экране лишние тавтологические сущности отвлекающие внимание.

Дефолтный int для переменных это плохая привычка из тех же времён когда были дефолтные функции без прототипов.

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

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

int* GenTwoDigitRand(int qTty) {	
	srand(time(NULL));
	int myArray[qTty];
	int myRand = 0;
	for (int n = 0; n <= qTty; ++n) {
		myRand = rand();
		myArray[n] = myRand;
	}
	{
		int *p = myArray;
		
		for (int n = 0; n <= qTty; ++n) {
			printf("%d ", myArray[n]);
		}
		printf("\n");
		
		return p;
	}
}
firkax ★★★★★
()
Ответ на: комментарий от praseodim

Да, сувание везде дефолтного %d это такая же забагованная привычка которую сразу надо пресекать. И не только из-за знаков, тот же size_t должен быть %lu на 64 битах а не %u и тем более не %d (на самом деле у него есть %zu чтобы не думать про битность, но оно несовместимо с древними libc, и в любом случае префикс перед u/d нужен).

А если uint64 на 32 битах принять как %d то он не только выведет обрезанное число, но и запутается в стеке и будет выводить дальше чушь вплоть до сегфолтов если там дальше %s будет.

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

Дефолтный int для переменных это плохая привычка

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

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

С такими запросами в 2к26 ходят не в гугл, а в нейросети.

А проблема у тебя в том, что ты возвращаешь из функции адрес локальной переменной. После выхода из функции myArray[qTty] не существует и прочитаться оттуда может любая лажа.

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

Либо выделять массив в куче с помощью malloc/calloc в GenTwoDigitRand, но тогда нужно не забыть сделать в main free возвращённого указателя.

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

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

Ты хотел сказать даже на 64 битах напороться?

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

+ если неудачные опции компилятора то ещё и UB (знаковое переполнение) с общей порчей программы.

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

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

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

Я не настаиваю, что так всегда правильно

Я бы сказал на обычный int надо смотреть как на самый быстрый int на платформе. Уже одно это является достаточным основанием чтобы он был дефолтным выбором. Понятно что всегда найдутся исключения когда unsigned / short / long / long long / fixed-size / size_t / итд будут оправданы.

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

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

Разница в стандарте, и некоторых компиляторах, что манипулируют стандартами дабы получить +0.1% к производительности на не определённых в стандарте тонкостях.

Лично я считаю, что послабления в стандарте Си (так называемые UB) сделаны чтобы позволить разное поведение в железе. Что на одном железе знаковые целые будут переполняться по модулю, а на другом будут сатурироваться.

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

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

Понятно что всегда найдутся исключения когда unsigned / short / long / long long / fixed-size / size_t / итд будут оправданы.

int и unsigned - это одно и то же по скорости.

int = signed int
unsigned = unsigned int

Вместо int можно писать signed.

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

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

int и unsigned - это одно и то же по скорости.

А вот и неправда. В частности именно из-за того что согласно стандарту переполнение int (в отличие от unsigned) - это UB. Что открывает дорогу всяким интересным трансформациям. Мне на слово верить не надо - подизассемблируйте сами.

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

Ты хотел сказать даже на 64 битах напороться?

Нет, я написал именно то что хотел.

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

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

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

В норме её быть не должно, да. Но графоманы из сишного комитета придумали будто знаковое (и тоько знаковое) переполнение это UB, а авторы компиляторов этой чуши местами потворствуют. Поэтому если ставишь -O2 или -O1 (в O1 эта пакости относительно недавно просочилась) надо не забывать сопровождать его -fno-strict-overflow иначе адекватной работы знакового переполнения ты не дождёшься.

де знаковый точно не подходит.

Знаковые прекрасно подходят для всех побитовых операций. Единственное где не подходят - это правый сдвиг, он дублирует знаковый бит, не добавляет слева нули если их там нет.

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

Потому что в некоторых архитектурах понадобится команда расширения 32-бит значения до 64-бит перед использованием для адресации памяти.

ПыСы. На нормальных архитектурах movz или его аналог справляется с задачей на ура. И в нынешних реалиях это гораздо лучше чем вгружать пол-регистра (погуглите «partial register stall») не платя при этом увеличением memory bandwidth.

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

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

С unsigned проверка числа на вхождение в заданный диапазон вдвойне быстрее.

if (a >= 100 && a < 200) something(a);
if ((unsigned)(a - 100) < 100) something(a);
jpegqs
()
Ответ на: комментарий от jpegqs

Программа с UB - неправильная программа

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

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

ПыСы. На нормальных архитектурах movz или его аналог справляется с задачей на ура. И в нынешних реалиях это гораздо лучше чем вгружать пол-регистра (погуглите «partial register stall»).

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

Достаточно любой арифметической операции и придётся добавлять расширение после неё. А int используют часто, в том числе в циклах. Где размер числа никак не влияет, так как и в стек может не сохраняться.

int test_int(int a, int b, int *x) {
    return x[a + b];
}

int test_unsigned(unsigned a, unsigned b, int *x) {
    return x[a + b];
}

int test_long(long a, long b, int *x) {
    return x[a + b];
}
test_int:
        add     edi, esi
        movsx   rdi, edi
        mov     eax, DWORD PTR [rdx+rdi*4]
        ret

test_unsigned:
        lea     eax, [rdi+rsi]
        mov     eax, DWORD PTR [rdx+rax*4]
        ret

test_long:
        add     rdi, rsi
        mov     eax, DWORD PTR [rdx+rdi*4]
        ret
jpegqs
()
Последнее исправление: jpegqs (всего исправлений: 1)
Ответ на: комментарий от praseodim

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

Вот пример UB, при котором GCC соптимизирует проверку за счёт UB.

И позволит запустить программу с аргументами 0x7fffffff 0x7fffffff, при которых GCC позволит прочитать 0x22222222 за пределами массива.

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

int test(int a, int b, int *buf) {
	if (a >= 0 && b >= 0) {
		a += b;
		// let's check that we are not reading outside the buffer
		if (a >= 0 && a < 8) return buf[a];
	}
	return -1;
}

int main(int argc, char **argv) {
	struct { int secret[3], buf[8]; } data = {
		{ 0x11111111, 0x22222222, 0x33333333 },
		{ 0, 1, 2, 3, 4, 5, 6, 7 }
	};

	if (argc > 2) {
		int a = strtol(argv[1], NULL, 0), b = strtol(argv[2], NULL, 0);
		printf("%08x: %08x\n", a + b, test(a, b, data.buf));
	}
	return 0;
}
jpegqs
()
Ответ на: комментарий от jpegqs

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

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

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

А int используют часто, в том числе в циклах.

Да вы что! Ну надо же, кто бы мог подумать…

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

Давайте-ка вы покажете пример, где int будет быстрее unsigned. Я уже два привел, где unsigned быстрее. Пока что ваши заявления голословные.

P.S.: В моём примере с адресацией на arm64 будет везде 2 команды. У mips64 и riscv64 будет быстрее long и int. На e2k быстрее всего long. У каждой архитектуры есть свои нюансы.

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