포인터 인덱싱


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에 매핑들 0xdeadbee7과 0xdeadbeef. 그렇게해야하는 프로그램은 거의 없습니다.

&연산자 를 사용 하거나을 호출 하는 등의 주소를 만들 때 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.