[c++] 두 개의 NULL 포인터를 빼는 동작이 정의되어 있습니까?

무효가 아닌 포인터 변수 두 개가 모두 NULL가치 가있는 경우 정의 된 (C99 및 / 또는 C ++ 98에 따라) 차이가 있습니까?

예를 들어 다음과 같은 버퍼 구조가 있다고 가정합니다.

struct buf {
  char *buf;
  char *pwrite;
  char *pread;
} ex;

말, ex.buf배열 또는 일부 malloc으로 할당 한 메모리를 가리키는. 내 코드는 항상 보장하는 경우 pwritepread그 배열 내의 점을하거나 과거 한 다음, 그 상당히 확신 ex.pwrite - ex.pread항상 정의됩니다. 그러나 어떤 경우 pwrite와 것은 pread모두 NULL입니다. 둘을 빼는 것이 정의되어 (ptrdiff_t)0있거나 엄격하게 준수하는 코드가 NULL에 대한 포인터를 테스트해야합니까? 내가 관심있는 유일한 경우는 포인터가 모두 NULL (초기화되지 않은 버퍼를 나타냄) 인 경우입니다. 그 이유는 위의 가정이 충족되면 완전히 호환되는 “사용 가능한”기능과 관련이 있습니다.

size_t buf_avail(const struct s_buf *b)
{
    return b->pwrite - b->pread;
}



답변

C99에서는 기술적으로 정의되지 않은 동작입니다. C99 §6.5.6에 따르면 :

7) 이러한 연산자의 목적을 위해, 배열의 요소가 아닌 개체에 대한 포인터는 개체 유형이 요소 유형 인 길이 1 인 배열의 첫 번째 요소에 대한 포인터와 동일하게 동작합니다.

[…]

9) 두 포인터를 뺄 때, 둘 다 동일한 배열 객체의 요소를 가리켜 야합니다. 또는 배열 객체의 마지막 요소를 지나는 하나를 가리켜 야합니다. 결과는 두 배열 요소의 첨자의 차이입니다. […]

그리고 §6.3.2.3 / 3은 다음과 같이 말합니다.

값이 0 인 정수 상수 표현식 또는 유형으로 캐스트 된 표현식을
void *널 포인터 상수라고합니다. 55) 널 포인터 상수가 포인터 유형으로 변환되면 널 포인터 라고하는 결과 포인터 는 객체 또는 함수에 대한 포인터와 같지 않은 비교를 보장합니다.

따라서 널 포인터는 객체와 같지 않기 때문에 6.5.6 / 9의 전제 조건을 위반하므로 정의되지 않은 동작입니다. 그러나 실제로는 거의 모든 컴파일러가 부작용없이 0의 결과를 반환 할 것이라고 확신합니다.

C89에서는 표준 문구가 약간 다르지만 정의되지 않은 동작입니다.

반면에 C ++ 03은이 인스턴스에서 동작을 정의했습니다. 표준은 두 개의 널 포인터를 뺄 때 특별한 예외를 만듭니다. C ++ 03 §5.7 / 7 내용 :

포인터 값에서 값 0을 더하거나 빼면 결과가 원래 포인터 값과 비교됩니다. 두 포인터가 동일한 객체를 가리 키거나 둘 다 동일한 배열의 끝을 지나서 하나를 가리 키거나 둘 다 null이고 두 포인터를 빼면 결과는 유형으로 변환 된 값 0과 동일합니다 ptrdiff_t.

C ++ 11 (C ++ 14의 최신 초안, n3690)은 C ++ 03과 동일한 표현을 사용 std::ptrdiff_t하며 ptrdiff_t.


답변

C ++ 표준 (5.7 [expr.add] / 7)에서 발견했습니다.

두 포인터 […]가 모두 널이고 두 포인터를 빼면 결과는 std :: ptrdiff_t 유형으로 변환 된 값 0과 동일합니다.

다른 사람들이 말했듯이 C99는 동일한 배열 객체의 두 포인터 사이에 더하기 / 빼기가 필요합니다. NULL은 유효한 객체를 가리 키지 않기 때문에 빼기에 사용할 수 없습니다.


답변

편집 :이 대답은 C에만 유효하며 대답했을 때 C ++ 태그를 보지 못했습니다.

아니요, 포인터 산술은 동일한 개체 내를 가리키는 포인터에만 허용됩니다. C 표준 널 포인터의 정의에 따라 어떤 객체도 가리 키지 않기 때문에 이것은 정의되지 않은 동작입니다.

(하지만 합리적인 컴파일러가 바로 반환 할 것이라고 생각 0하지만 누가 알겠습니까?)


답변

C 표준은이 경우 동작에 대한 요구 사항을 부과하지 않지만 많은 구현에서 표준에서 요구하는 최소값을 넘어서 많은 경우 포인터 산술 동작을 지정합니다.

어떤 부합 C 구현 및에 거의 모든 (모든 경우) C는 같은 방언, 다음과 같은 보장은 어떤 포인터 개최의 구현 p같은 그 중 하나 *p또는 *(p-1)식별하는 오브젝트 :

  • 임의의 정수 값을 z제로, 포인터 값을 동일 (p+z)(p-z)에 모든면에서 동등한 것 p둘 경우 그들은 단지 상수가 될 것이다 제외 p하고는 z일정하다.
  • q해당하는 항목 p에 대해 p-q및 표현식 q-p은 모두 0을 생성합니다.

null을 포함한 모든 포인터 값에 대해 이러한 보장을 유지하면 사용자 코드에서 일부 null 검사가 필요하지 않을 수 있습니다. 또한 대부분의 플랫폼에서 null인지 여부에 관계없이 모든 포인터 값에 대해 이러한 보장을 유지하는 코드를 생성하는 것은 null을 특별히 처리하는 것보다 더 간단하고 저렴합니다. 그러나 일부 플랫폼은 0을 더하거나 뺄 때에도 널 포인터로 포인터 산술을 수행하려는 시도를 트랩 할 수 있습니다. 이러한 플랫폼에서 보증을 유지하기 위해 포인터 작업에 추가해야하는 컴파일러 생성 널 검사의 수는 많은 경우 결과로 생략 될 수있는 사용자 생성 널 검사의 수를 크게 초과합니다.

보증을 유지하는 데 드는 비용이 크지 만 프로그램이 혜택을받을 수있는 프로그램이 거의 없다면 “null + zero”계산을 트랩하고 해당 사용자 코드를 다음과 같이 요구하는 것이 합리적 일 것입니다. 이러한 구현에는 보증이 불필요하게 만들 수있는 수동 null 검사가 포함됩니다. 그러한 수당은 보장을 유지하는 가치가 비용을 초과하는 다른 99.44 %의 구현에 영향을 미칠 것으로 예상되지 않았습니다. 그러한 구현은 그러한 보장을 유지해야하지만, 작성자는 표준 작성자가이를 말할 필요가 없어야합니다.

C ++의 작성자는 포인터 산술의 성능을 크게 저하시킬 수있는 플랫폼에서도 어떤 비용 으로든 준수 구현이 위의 보증을 유지해야한다고 결정했습니다. 그들은 유지 비용이 많이 드는 플랫폼에서도 보증의 가치가 비용을 초과 할 것이라고 판단했습니다. 이러한 태도는 C ++를 C보다 높은 수준의 언어로 취급하려는 욕구의 영향을 받았을 수 있습니다. AC 프로그래머는 특정 대상 플랫폼이 비정상적인 방식으로 (null + zero)와 같은 경우를 처리 할 때를 알 것으로 예상 할 수 있지만 C ++ 프로그래머는 그런 것들에 대해 스스로를 걱정할 것으로 예상되지 않았습니다. 따라서 일관된 행동 모델을 보장하는 것은 비용의 가치가있는 것으로 판단되었습니다.

물론 오늘날 “정의 된”항목에 대한 질문은 플랫폼이 지원할 수있는 동작과 거의 관련이 없습니다. 대신, 이제는 컴파일러가 “최적화”라는 이름으로, 이전에 플랫폼이 올바르게 처리했을 코너 케이스를 처리하기 위해 프로그래머가 수동으로 코드를 작성하도록 요구하는 것이 유행입니다. 예를 들어 n주소에서 시작하는 문자 를 출력해야하는 코드 p가 다음과 같이 작성되면

void out_characters(unsigned char *p, int n)
{
  unsigned char *end = p+n;
  while(p < end)
    out_byte(*p++);
}

이전 컴파일러는 p == NULL 및 n == 0 인 경우 특별한 경우 n == 0이 필요없이 부작용없이 안정적으로 아무것도 출력하지 않는 코드를 생성합니다. 그러나 최신 컴파일러에서는 추가 코드를 추가해야합니다.

void out_characters(unsigned char *p, int n)
{
  if (n)
  {
    unsigned char *end = p+n;
    while(p < end)
      out_byte(*p++);
  }
}

옵티마이 저가 제거 할 수도 있고 제거 할 수도 없습니다. 추가 코드를 포함하지 않으면 일부 컴파일러는 p “널이 될 수 없음”이므로 후속 널 포인터 검사가 생략되어 실제 “문제”와 관련이없는 지점에서 코드가 중단 될 수 있다고 생각할 수 있습니다.


답변