포인터 인덱싱


11

현재 "C의 수치 레시피"라는 제목의 책을 ​​읽고 있습니다. 이 책에서 저자는 1로 시작하는 인덱스가있는 경우 특정 알고리즘이 본질적으로 더 잘 작동하는 방법에 대해 자세히 설명합니다 (전적으로 그의 주장을 따르지 않으며이 게시물의 요점이 아닙니다) .C는 항상 0으로 시작하는 배열을 인덱싱합니다 이 문제를 해결하기 위해 할당 후 포인터를 간단히 줄이는 것이 좋습니다.

float *a = malloc(size);
a--;

그는 이것이 1로 시작하는 색인을 가진 포인터를 효과적으로 제공 할 것이며, 그러면 다음과 같이 자유롭게 될 것이라고 말했다.

free(a + 1);

내가 아는 한, 이것은 C 표준에 의해 정의되지 않은 동작입니다. 이것은 분명히 HPC 커뮤니티 내에서 평판이 좋은 책이므로 그가 말하는 것을 단순히 무시하고 싶지는 않지만 할당 된 범위 밖의 포인터를 줄이는 것은 나에게 매우 스케치적인 것 같습니다. C에서 이것이 "허용 된"동작입니까? 나는 gcc와 icc를 모두 사용하여 테스트했으며, 그 결과는 아무것도 걱정하지 않는다는 것을 나타내는 것으로 보이지만 절대적으로 긍정적이기를 원합니다.


3
C 표준 은 무엇 입니까? 기억에 남을 때마다 "C의 수치 레시피"는 1990 년대에 K & R 시대에 그리고 아마도 ANSI C
gnat


3
"gcc와 icc를 모두 사용하여 테스트 한 결과, 두 가지 결과 모두 내가 아무 것도 걱정하지 않고 있지만 절대적으로 긍정적이기를 원한다." 컴파일러가 허용하기 때문에 C 언어가 허용한다고 가정하지 마십시오. 물론, 앞으로 코드가 깨지더라도 괜찮습니다.
Doval

5
"Numerical Recipies"는 일반적으로 소프트웨어 개발이나 수치 분석의 패러다임이 아니라 유용하고 빠르며 더러운 책으로 간주됩니다. 일부 비판에 대한 요약은 "Numerical Recipies"에 대한 Wikipedia 기사를 확인하십시오.
Charles E. Grant

1
: 제쳐두고, 여기에 왜 우리에게 지수는 0에서의 cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
러셀 Borogove

답변:


16

당신과 같은 코드가 맞습니다.

float a = malloc(size);
a--;

ANSI C 표준 섹션 3.3.6에 따라 정의되지 않은 동작이 생성됩니다.

포인터 피연산자와 결과가 동일한 배열 객체의 멤버를 가리 키거나 배열 객체의 마지막 멤버를 지나지 않는 한, 동작은 정의되지 않습니다

이와 같은 코드의 경우 책의 C 코드 품질 (1990 년대 후반에 사용했을 때의 품질)은 그다지 높지 않은 것으로 간주되었습니다.

정의되지 않은 동작의 문제점은 컴파일러가 어떤 결과를 생성하든 그 결과는 정의에 의한 것입니다 (높은 파괴적이고 예측할 수없는 경우에도).
운 좋게도, 그러한 경우에 실제로 예상치 못한 동작을 발생시키기 위해 노력하는 컴파일러는 거의 없으며 mallocHPC에 사용되는 컴퓨터 의 일반적인 구현에는 반환 주소 직전에 약간의 부기 데이터가 있으므로 감소하면 일반적으로 그 부기 데이터에 대한 포인터를 제공합니다. 거기에 쓰는 것은 좋지 않지만 포인터를 만드는 것만으로는 이러한 시스템에서 무해합니다.

런타임 환경이 변경되거나 코드가 다른 환경으로 이식되면 코드 손상 될 있습니다.


4
정확하게, 다중 뱅크 아키텍처에서 malloc은 뱅크에서 0 번째 주소를 제공 할 수 있으며 감소하면 CPU 트랩에 언더 플로우가 발생할 수 있습니다.
Vality

1
나는 그것이 "운이 좋다"는 것에 동의하지 않습니다. 정의되지 않은 동작을 호출 할 때마다 컴파일러가 즉시 충돌하는 코드를 생성하면 훨씬 더 좋을 것이라고 생각합니다.
David Conrad

4
@DavidConrad : 그렇다면 C는 당신을위한 언어가 아닙니다. C에서 정의되지 않은 많은 동작은 쉽게 감지 할 수 없거나 성능이 크게 저하 될 때만 감지 할 수 없습니다.
Bart van Ingen Schenau

"컴파일러 스위치"를 추가 할 생각이었습니다. 분명히 최적화 된 코드를 원하지 않을 것입니다. 하지만 당신 말이 맞습니다. 그래서 제가 10 년 전에 C 쓰기를 포기했습니다.
David Conrad

@BartvanIngenSchenau는 '심한 성능 히트'의 의미에 따라 C (예 : clang + klee)뿐만 아니라 sanatizer (asan, tsan, ubsan, valgrind 등)에 대한 기호 실행이 디버깅에 매우 유용한 경향이 있습니다.
Maciej Piechotka

10

공식적으로, 참조되지 않은 경우에도 배열 외부에 포인터 지점을 갖는 것은 정의되지 않은 동작입니다 (끝을 지나친 포인터 제외) .

실제로, 경우 프로세서가 플랫 메모리 모델이 있습니다 (같은 이상한 것들에 반대 x86-16 ), 및 경우 컴파일러는 당신에게 런타임 오류 또는 잘못된 최적화를 제공하지 않습니다 유효하지 않은 포인터를 작성하는 경우, 다음 코드 의지 작업 괜찮아


1
말이 되네요 불행히도, 그것은 내가 좋아하는 경우에 너무 두 개입니다.
wolfPack88

3
마지막 요점은 IMHO가 가장 문제가 많은 것입니다. 컴파일러는 UB의 경우 "자연스럽게"플랫폼이 무엇이든 할 수 있도록 해주지 않았지만, 옵티마이 저가 적극적으로 그것을 이용 하고 있기 때문에 그렇게 가볍게 사용하지는 않을 것입니다.
Matteo Italia

3

첫째, 그것은 정의되지 않은 행동입니다. 오늘날 일부 최적화 컴파일러 는 정의되지 않은 동작에 대해 매우 공격적입니다. 예를 들어,이 경우 정의되지 않은 동작이므로 컴파일러는 a와 감소하지 않고 명령과 프로세서주기를 저장하기로 결정할 수 있습니다. 공식적으로 정확하고 합법적입니다.

이를 무시하면 1 또는 2 또는 1980을 뺄 수 있습니다. 예를 들어 1980 년에서 2013 년 사이의 재무 데이터가있는 경우 1980 년을 뺄 수 있습니다. 이제 float * a = malloc (size); 확실히이 일부 K 널 포인터는 -는 것을 큰 상수 k를. 이 경우 실제로 무언가 잘못 될 것으로 예상합니다.

이제 메가 바이트 크기의 큰 구조를 취하십시오. 두 구조체를 가리키는 포인터 p를 할당하십시오. p-1은 널 포인터 일 수 있습니다. p-1이 줄 바꿈 될 수 있습니다 (구조가 메가 바이트이고 malloc 블록이 주소 공간 시작에서 900KB 인 경우). 따라서 p-1> p라는 컴파일러의 악의가 없을 수 있습니다. 상황이 재미있을 수 있습니다.


1

... 할당 된 범위 밖에서 포인터를 간단히 감소시키는 것은 저에게 매우 스케치적인 것 같습니다. C에서 이것이 "허용 된"동작입니까?

허용 되었습니까? 예. 좋은 생각? 보통은 아닙니다.

C는 어셈블리 언어의 약자이며 어셈블리 언어에는 포인터가 없으며 메모리 주소 만 있습니다. C의 포인터는 산술을 할 때 지시하는 크기만큼 증가 또는 감소하는 측면 동작을 갖는 메모리 주소입니다. 이것은 구문 관점에서 다음을 잘 만듭니다.

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

C에서는 배열이 실제로 중요하지 않습니다. 그것들은 배열처럼 동작하는 연속적인 메모리 범위에 대한 포인터 일뿐입니다. []연산자이므로 포인터 연산을하고 간접 참조에 대한 속기 a[x]실제로 수단 *(a + x).

등의 커플 일부 I / O 장치로 위의 작업을 수행하는 타당한 이유가 있습니다 double에 매핑들 0xdeadbee70xdeadbeef. 그렇게해야하는 프로그램은 거의 없습니다.

&연산자 를 사용 하거나을 호출 하는 등의 주소를 만들 때 malloc()원래 포인터를 그대로 유지하여 가리키는 포인터가 실제로 유효한 것을 알 수 있습니다. 포인터를 줄이면 잘못된 코드 비트가이를 역 참조하여 잘못된 결과를 얻거나 무언가를 방해하거나 환경에 따라 세그먼트 위반을 저지를 수 있습니다. 이것은 특히 가치가 있습니다. 왜냐하면 malloc()전화 free()를 건 사람 이 원래 값을 전달하도록 기억해야하며 변경 된 버전이 아니라 모든 지옥이 풀릴 수 있기 때문입니다.

C에 1 기반 배열이 필요한 경우 절대 사용하지 않는 하나의 추가 요소를 할당하는 대신 안전하게 사용할 수 있습니다.

double *array_create(size_t size) {
    // Wasting one element, so don't allow it to be full-sized
    assert(size < SIZE_MAX);
    return malloc((size+1) * sizeof(double));
}

inline double array_index(double *array, size_t index) {
    assert(array != NULL);
    assert(index >= 1);  // This is a 1-based array
    return array[index];
}

이것은 상한을 초과하는 것을 방지하기 위해 아무것도하지 않지만 처리하기 쉽습니다.


추가:

C99 초안의 일부 장과 구절 (죄송합니다. 링크 만하면됩니다) :

§6.5.2.1.1에 따르면 첨자 연산자와 함께 사용되는 두 번째 ( "other") 표현식은 정수 유형입니다. -1는 정수 p[-1]이므로 유효하므로 포인터도 &(p[-1])유효합니다. 이것은 해당 위치에서 메모리에 액세스하는 것이 정의 된 동작을 생성한다는 것을 의미하지는 않지만 포인터는 여전히 유효한 포인터입니다.

§6.5.2.2 포인터에 요소 수를 첨가하는 등가의 배열 첨자 운영자가 평가하여, 따라서 말한다 p[-1]동등하다 *(p + (-1)). 여전히 유효하지만 바람직한 동작을 생성하지 못할 수 있습니다.

§6.5.6.8에서는 다음과 같이 말합니다 (강조 광산).

정수 유형이있는 표현식이 포인터에 더하거나 뺄 때 결과에는 포인터 피연산자의 유형이 있습니다.

... 식 경우 P받는 점 i번째 배열 객체 표현의 요소 (P)+N(등가 N+(P)) 및 (P)-N (여기서 N값을 갖고 n, 행) 지점은 각각 i+n번째 및 i−n배열 객체의 번째 요소는 그들이 존재 구비 .

이는 포인터 산술 결과가 배열의 요소를 가리켜 야 함을 의미합니다. 산술은 한 번에 수행되어야한다고 말하지 않습니다. 따라서:

double a[20];

// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];

double *p = a - 1;  // This is just a pointer.  No dereferencing.

double e = p[0];   // Does not point at any element of a; behavior is undefined.
double f = p[1];   // Points at element 0 of a; behavior is defined.

이런 식으로 일하는 것이 좋습니다? 나는하지 않으며 내 대답은 이유를 설명합니다.


8
-1 C 표준이 정의되지 않은 결과를 생성한다고 선언 한 코드를 포함하는 '허용'의 정의는 유용한 것이 아닙니다.
피트 Kirkham

다른 사람들은 그것이 정의되지 않은 행동이라고 지적 했으므로 "허용된다"고 말해서는 안됩니다. 그러나 여분의 미사용 요소 0을 할당하는 것이 좋습니다.
200_success

이것은 실제로 옳지 않습니다. 적어도 C 표준에서는 금지되어 있습니다.
Vality

@PeteKirkham : 동의하지 않습니다. 내 답변에 대한 부록을 참조하십시오.
Blrfl

4
포인터에 정수를 추가하는 경우 ISO C11 표준의 @Blrfl 6.5.6 상태 : "포인터 피연산자와 결과가 동일한 배열 객체의 요소를 가리 키거나 배열 객체의 마지막 요소를 지나면 , 평가는 오버플로를 발생시키지 않아야하며, 그렇지 않으면 동작이 정의되지 않습니다. "
Vality
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.