C에서 포인터가 NULL인지 언제 확인해야합니까?


18

요약 :

C의 함수가 항상 NULL포인터를 참조하지 않는지 확인해야 합니까? 그렇지 않은 경우 이러한 검사를 건너 뛰는 것이 적절합니까?

세부 사항 :

프로그래밍 인터뷰에 관한 책을 읽었으며 C의 함수 인수에 대한 적절한 입력 유효성 검사가 무엇인지 궁금합니다. 분명히 사용자로부터 입력을받는 모든 함수는 NULL포인터를 역 참조하기 전에 확인하는 등 검증을 수행해야 합니다. 그러나 동일한 파일 내에서 API를 통해 노출하지 않을 것으로 예상되는 함수의 경우는 어떻습니까?

예를 들어 git의 소스 코드에 다음이 나타납니다.

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

경우 *graph입니다 NULL다음, 널 포인터는 아마 프로그램을 충돌하지만, 아마도 다른 예기치 않은 동작의 결과로, 역 참조됩니다. 반면에 함수는 static아마도 프로그래머가 이미 입력을 확인했을 것입니다. 잘 모르겠습니다. C로 작성된 응용 프로그램의 짧은 예제이기 때문에 무작위로 임의로 선택했습니다. NULL을 확인하지 않고 포인터가 사용되는 다른 많은 곳을 보았습니다. 내 질문은 일반적 으로이 코드 세그먼트와 관련이 없습니다.

나는 예외 처리 의 맥락에서 비슷한 질문을 보았다 . 그러나 C 또는 C ++와 같은 안전하지 않은 언어의 경우 처리되지 않은 예외에 대한 자동 오류 전파가 없습니다.

반면, 오픈 소스 프로젝트 (예 : 위의 예)에서 포인터를 사용하기 전에 검사를 수행하지 않는 많은 코드를 보았습니다. 함수에 올바른 인수를 사용하여 호출했다고 가정 할 때 언제 함수에 검사를 넣을 지에 대한 지침에 대해 누군가 생각하고 있는지 궁금합니다.

프로덕션 코드를 작성하는 데 일반적 으로이 질문에 관심이 있습니다. 그러나 나는 또한 프로그래밍 인터뷰의 맥락에 관심이 있습니다. 예를 들어 많은 알고리즘 교과서 (예 : CLR)는 오류 검사없이 의사 코드로 알고리즘을 제시하는 경향이 있습니다. 그러나 이것이 알고리즘의 핵심을 이해하는 데는 좋지만 좋은 프로그래밍 방법은 아닙니다. 따라서 코드 예제를 단순화하기 위해 오류 확인을 건너 뛰고 있다고 인터뷰 담당자에게 말하고 싶지 않습니다 (교과서처럼). 그러나 과도한 오류 검사로 비효율적 인 코드를 생성하고 싶지는 않습니다. 예를 들어, null graph_get_current_column_color을 확인하도록 수정 *graph되었지만 null이 아닌 경우 수행 할 작업이 명확 *graph하지 않지만 역 참조해서는 안됩니다.


7
호출자가 내부를 이해하지 않아야하는 API 용 함수를 작성하는 경우 문서가 중요한 곳 중 하나입니다. 인수가 유효하고 NULL이 아닌 포인터 여야한다고 문서화하면이를 검사하는 것은 호출자의 책임이됩니다.
Blrfl


2017 년의 후시와 함께, 질문과 대부분의 답변은 2013 년에 작성되었음을 명심하고, 컴파일러 최적화로 인한 시간 여행 정의되지 않은 동작 문제를 해결하는 답변이 있습니까?
rwong

유효한 포인터 인수를 기대하는 API 호출의 경우 NULL 만 테스트하는 값이 무엇인지 궁금합니다. 역 참조 된 유효하지 않은 포인터는 NULL만큼이나 나쁘고 segfault는 모두 동일합니다.
PaulHK

답변:


15

프로그래머 오류 또는 런타임 오류로 인해 잘못된 널 포인터가 발생할 수 있습니다. 런타임 오류는 malloc메모리 부족으로 인한 장애 또는 네트워크가 패킷을 삭제하거나 사용자가 바보 같은 것을 입력 하는 등 프로그래머가 해결할 수없는 것입니다. 프로그래머가이 기능을 잘못 사용하여 프로그래머 오류가 발생합니다.

내가 본 경험의 일반적인 규칙은 런타임 오류는 항상 확인해야하지만 매번 프로그래머 오류를 확인하지 않아도된다는 것입니다. 어떤 바보 프로그래머가 직접 전화했다고 가정 해 봅시다 graph_get_current_column_color(0). 처음 호출 될 때 segfault가 발생하지만 일단 수정하면 수정 사항이 영구적으로 컴파일됩니다. 실행될 때마다 확인할 필요가 없습니다.

때때로, 특히 써드 파티 라이브러리에서는 명령문 assert대신 프로그래머 오류를 확인하기위한가 표시됩니다 if. 이를 통해 개발 중에 수표를 컴파일하고 프로덕션 코드에 남겨 둘 수 있습니다. 또한 잠재적 인 프로그래머 오류의 원인이 증상에서 멀리 제거되는 경우가 있습니다.

분명히, 당신은 항상 더 많은 사람을 찾을 수 있지만, 내가 아는 대부분의 C 프로그래머는 조금 더 안전한 코드보다 덜 복잡한 코드를 선호합니다. "safer"는 주관적인 용어입니다. 현장에서 미묘한 부패 오류보다 개발 중 뻔뻔한 segfault가 바람직합니다.


문제는 다소 주관적이지만 지금은 가장 좋은 대답처럼 보입니다. 이 질문에 대한 의견을 주신 모든 분들께 감사드립니다.
가브리엘 남부

1
iOS에서 malloc은 절대 NULL을 반환하지 않습니다. 메모리가 없으면 응용 프로그램에서 먼저 메모리를 해제하도록 요청 한 다음 운영 체제에 요청합니다 (다른 응용 프로그램에서 메모리를 해제하고 메모리를 해제하도록 요청). 메모리가 없으면 여전히 응용 프로그램을 종료합니다 . 점검이 필요 없습니다.
gnasher729

11

"Software Tools"의 Kernighan & Plauger는 모든 것을 점검 할 것이며, 실제로는 결코 일어날 수 없다고 생각되는 조건에서는 "Ca n't possible"오류 메시지와 함께 중단 될 것이라고 썼습니다.

그들은 터미널에서 "일어날 수 없음"이 나오는 횟수에 의해 급격히 겸손 해 졌다고보고합니다.

역 참조하기 전에 항상 포인터에 NULL이 있는지 확인해야합니다. 항상 . 발생하지 않는 NULL 및 프로세서 "폐기물"에 대해 중복 검사를 수행하는 코드의 양은 크래시 덤프 이상으로 디버깅 할 필요가없는 크래시 수보다 많은 비용을 지불합니다. 운이 좋으면

포인터가 루프 내부에서 변하지 않으면 루프 외부에서 포인터를 검사하면 충분하지만 루프에서 사용하기 위해 범위 제한 로컬 변수에 "복사"하여 적절한 const 장식을 추가해야합니다. 이 경우 루프 바디에서 호출 된 모든 함수에 프로토 타입의 필수 구성 요소가 모두 포함되어 있는지 확인해야합니다. 당신이하지 않는, 또는 할 수없는 경우에, 당신은 NULL을 위해 그것을 확인해야합니다 (예 : 공급 업체 패키지 또는 난치의 동료들 때문에) EVERY TIME IT가 수정 될 수 COL 머피는 불치의 낙관적이었다 확인으로하기 때문에, 누군가가 IS 것 당신이보고 있지 않을 때 그것을 zap합니다.

함수 안에 있고 포인터가 NULL이 아닌 것으로 가정하면 확인해야합니다.

함수에서 함수를 수신 중이고 NULL이 아닌 것으로 표시되면이를 확인해야합니다. malloc ()은 특히 이것으로 유명합니다. (Nortel Networks는 현재 단종 된이 문제에 대해 강력하고 빠른 서면 코딩 표준을 가지고있었습니다. 한 시점에서 크래시를 디버깅하여 malloc ()으로 돌아가서 NULL 포인터를 반환하고 바보 코더가 확인하는 것을 귀찮게하지 않았습니다. 그가 기억하기에 충분한 기억력을 가지고 있었기 때문에 글을 쓰기 전에 ... 나는 마침내 그것을 발견했을 때 매우 불쾌한 것을 말했습니다.)


8
NULL이 아닌 포인터가 필요한 함수에 있지만 어쨌든 확인하면 NULL입니다 ... 다음은 무엇입니까?
detly February

1
@detly하고있는 일을 멈추고 오류 코드를 반환하거나, assert를 트립하십시오
James

1
@ 제임스-생각하지 않았다 assert. NULL체크 를 포함하도록 기존 코드를 변경하는 것에 대해 이야기하고 있다면 오류 코드 아이디어가 마음에 들지 않습니다 .
detly February

10
@detly 오류 코드가 마음에 들지 않으면 C 개발자로 멀지 않을 것입니다.
James

5
@ JohnR.Strohm-이것은 C입니다, 그것은 주장이나 아무것도 아닙니다 : P
detly

5

포인터가 널이 될 수 없다는 것을 스스로 확신 할 수 있으면 검사를 건너 뛸 수 있습니다.

일반적으로 널 포인터 검사는 오브젝트에서 현재 사용할 수 없음을 나타내는 표시 자로 널이 나타날 것으로 예상되는 코드에서 구현됩니다. Null은 센티넬 값으로 사용됩니다 (예 : 링크 된 목록 또는 포인터 배열을 종료하는 경우). argv전달 된 문자열 의 벡터는 문자열 main이 널 문자로 끝나는 방식과 유사하게 포인터로 널로 끝나야합니다. argv[argc]널 포인터이며 명령 행을 구문 분석 할 때이를 사용할 수 있습니다.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

따라서 널 검사 상황은 예상 값인 상황입니다. 널 검사는 링크 된 목록의 검색 중지와 같은 널 포인터의 의미를 구현합니다. 코드가 포인터를 역 참조하는 것을 방지합니다.

설계 상 널 포인터 값이 예상되지 않는 상황에서는이를 점검 할 필요가 없습니다. 유효하지 않은 포인터 값이 발생하면 널 (null)이 아닌 것처럼 보일 수 있으며, 이식 가능한 방법으로 유효한 값과 구별 할 수 없습니다. 예를 들어, 초기화되지 않은 스토리지를 읽은 후 얻은 포인터 값은 포인터 유형, 일부 그늘진 변환을 통해 얻은 포인터 또는 범위를 벗어난 포인터로 해석됩니다.

다음과 같은 데이터 유형 정보 graph *: 이것은 null 값이 유효한 그래프, 즉 모서리가없고 노드가없는 것입니다. 이 경우, graph *포인터 를 취하는 모든 함수 는 그래프를 표현할 때 올바른 도메인 값이므로 해당 값을 처리해야합니다. 반면 graph *에 a는 컨테이너 형 객체에 대한 포인터 일 수 있으며 그래프를 보유하면 null이되지 않습니다. 그런 다음 널 포인터는 "그래프 객체가 존재하지 않거나 아직 할당하지 않았거나 해제했거나 현재 연결된 그래프가 없습니다"라고 알려줍니다. 포인터의 후자의 사용은 결합 부울 / 위성이다 : null이 아닌되는 포인터는 "이 자매 개체가"표시, 그리고 그것은 그 객체를 제공합니다.

객체를 해제하지 않더라도 단순히 한 객체를 다른 객체와 분리하기 위해 포인터를 null로 설정할 수 있습니다.

tty_driver->tty = NULL; /* detach low level driver from the tty device */

포인터가 특정 지점에서 null 일 수 없다는 가장 설득력있는 인수는 "if (ptr! = NULL) {"및 해당 "}"로 해당 지점을 래핑하는 것입니다. 그 외에도 공식 검증 영역에 있습니다.
John R. Strohm

4

푸 그에 하나의 목소리를 더 추가하겠습니다.

다른 답변들과 마찬가지로, 나는이 시점에서 확인하는 것을 귀찮게하지 않습니다. 그것은 발신자의 책임입니다. 그러나 나는 단순한 편의성 (및 C 프로그래밍 오만)보다는 구축 할 기초가 있습니다.

도널드 크 누스 (Donald Knuth)의 프로그램을 가능한 한 취약하게 만드는 원칙을 따르려고 노력합니다. 문제가 발생하면 충돌이 발생 하고 null 포인터를 참조하는 것이 일반적으로 좋은 방법입니다. 일반적인 아이디어는 충돌 또는 무한 루프가 잘못된 데이터를 만드는 것보다 훨씬 낫다는 것입니다. 그리고 프로그래머의 주목을받습니다!

그러나 널 포인터 (특히 큰 데이터 구조의 경우)를 참조한다고해서 항상 충돌이 발생하는 것은 아닙니다. 한숨. 사실입니다. 그리고 Asserts가 빠지는 곳입니다. 그것들은 간단하고 프로그램을 즉시 중단시킬 수 있습니다 ( "null이 발생하면 어떻게해야합니까?"라는 질문에 대답합니다). 다양한 상황에서 켜거나 끌 수 있습니다 고객이 데이터가 나쁜 것보다 충돌을 일으켜 암호 메시지를 보는 것이 더 낫기 때문에 끄지 마십시오.

그건 내 두 센트입니다.


1

나는 일반적으로 포인터가 할당 될 때만 확인합니다. 일반적으로 실제로 그것에 대해 무언가를하고 유효하지 않은 경우 복구 할 수있는 유일한 시간입니다.

예를 들어 창에 대한 핸들을 얻으면 null이 맞는지 확인한 다음 null 조건에 대해 무언가를 수행하지만 매번 null인지 확인하지는 않습니다. 포인터가 전달되는 모든 함수에서 포인터를 사용합니다. 그렇지 않으면 중복 오류 처리 코드가 산으로 표시됩니다.

같은 함수 graph_get_current_column_color는 잘못된 포인터가 발생하면 상황에 유용한 것을 완전히 수행 할 수 없으므로 호출자에게 NULL 검사를 남겨 둡니다.


1

나는 그것이 다음에 달려 있다고 말하고 싶다.

  1. CPU 사용률이 중요합니까? NULL을 확인할 때마다 시간이 걸립니다.
  2. 포인터가 NULL 일 가능성은 무엇입니까? 이전 함수에서 방금 사용 되었습니까? 포인터의 값이 변경되었을 수 있습니다.
  3. 시스템이 선점입니까? 작업 변경이 발생하고 값이 변경 될 수 있다는 의미입니까? ISR이 제공되어 가치를 변경할 수 있습니까?
  4. 코드는 얼마나 밀접하게 결합되어 있습니까?
  5. NULL 포인터를 자동으로 확인하는 일종의 자동 메커니즘이 있습니까?

CPU 사용률 / 홀수 포인터가 NULL입니다. NULL 을 확인할 때마다 시간이 걸립니다. 이러한 이유로 포인터의 값이 변경 될 수있는 위치로 검사를 제한하려고합니다.

선점 시스템 코드가 실행 중이고 다른 작업으로 인해 코드가 중단되어 잠재적으로 값을 변경할 수있는 경우 확인해야합니다.

단단히 결합 된 모듈 시스템이 단단히 결합 된 경우 더 많은 점검이 필요합니다. 이것이 의미하는 바는 여러 모듈간에 공유되는 데이터 구조가있는 경우 한 모듈이 다른 모듈에서 무언가를 변경할 수 있다는 것입니다. 이러한 상황에서는 더 자주 확인하는 것이 좋습니다.

자동 검사 / 하드웨어 지원 마지막으로 고려해야 할 사항은 실행중인 하드웨어에 NULL을 검사 할 수있는 메커니즘이있는 경우입니다. 특히 페이지 오류 감지를 말합니다. 시스템에 페이지 오류 감지 기능이 있으면 CPU 자체에서 NULL 액세스를 확인할 수 있습니다. 개인적으로 나는 이것이 항상 실행되고 프로그래머가 명시 적 검사를하는 데 의존하지 않기 때문에 이것이 가장 좋은 메커니즘이라고 생각합니다. 또한 오버 헤드가 거의 없다는 이점도 있습니다. 이것이 가능하다면 디버깅을 조금 어렵지만 지나치게하지는 않는 것이 좋습니다.

사용 가능한지 테스트하려면 포인터로 프로그램을 작성하십시오. 포인터를 0으로 설정 한 다음 읽고 쓰십시오.


자동 NULL 검사를 수행하는 것으로 segfault를 분류할지 여부를 모르겠습니다. CPU 메모리 보호 기능을 사용하면 한 프로세스가 나머지 시스템에 많은 피해를 입히지 않지만 자동 보호라고 부르지는 않습니다.
Gabriel Southern

1

내 의견으로는 입력 (예 : 전 / 후 조건)을 검증하는 것이 프로그래밍 오류를 감지하는 좋은 방법이지만 무시할 수없는 종류의 시끄럽고 불쾌하고 쇼 스톱 오류가 발생하는 경우에만 가능합니다. assert일반적으로 그 효과가 있습니다.

이것에 미치지 못하는 것은 매우 신중하게 조정 된 팀이 없으면 악몽으로 변할 수 있습니다. 물론 모든 팀은 엄격한 기준에 따라 매우 신중하게 조정되고 통합되지만, 내가 일한 대부분의 환경은 그보다 훨씬 떨어졌습니다.

예를 들어, 나는 종교적으로 널 포인터의 존재를 확인해야한다고 믿는 일부 동료들과 협력하여 다음과 같이 많은 코드를 뿌렸습니다.

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... 그리고 때로는 오류 코드를 반환하거나 설정하지 않고도 그렇게 좋아합니다. 그리고 이것은 수십 년 전의 많은 타사 플러그인으로 코드베이스에있었습니다. 또한 많은 버그로 괴로워하는 코드베이스였으며 종종 문제의 직접적인 원인에서 멀리 떨어진 사이트에서 충돌하는 경향이 있었기 때문에 근본 원인으로 추적하기가 매우 어려운 버그였습니다.

그리고이 관행은 그 이유 중 하나였습니다. move_vertexnull 꼭지점을 전달하는 위의 함수 의 사전 설정 조건을 위반하는 것이지만 그러한 함수는 자동으로 수락하고 아무런 응답도하지 않았습니다. 따라서 발생하는 경향은 플러그인에 프로그래머 실수가있을 수 있습니다. 이로 인해 함수에 null을 전달하고 감지하지 못하고 나중에 많은 일을 수행하기 때문에 결국 시스템이 벗겨 지거나 충돌하기 시작합니다.

그러나 실제 문제는이 문제를 쉽게 감지 할 수 없다는 것이 었습니다. 그래서 한 번 위의 아날로그 코드를로 바꾸면 어떻게 될지 보려고했습니다 assert.

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... 그리고 내 공포에, 나는 응용 프로그램을 시작할 때에도 주장이 왼쪽과 오른쪽으로 실패한다는 것을 알았습니다. 처음 몇 개의 콜 사이트를 수정 한 후 더 많은 작업을 수행 한 후 보트로드에 더 많은 어설 션 오류가 발생했습니다. 너무 많은 코드를 수정 할 때까지 계속 변경하여 너무 방해 적이었고 null 포인터 검사를 간신히 유지했기 때문에 변경 사항을 되돌릴 수 있었지만 함수가 null 꼭지점을 허용한다는 것을 문서화했습니다.

그러나 이는 최악의 시나리오 임에도 불구하고 사전 / 사후 조건 위반을 쉽게 감지 할 수 없게 만드는 위험입니다. 그런 다음 몇 년 동안 테스트 레이더에서 비행하는 동안 이러한 사전 / 사후 조건을 위반하는 코드를 자동으로 축적 할 수 있습니다. 내 견해로는, 뻔뻔스럽고 독창적 인 주장 실패 외부의 그러한 널 포인터 검사는 실제로 좋은 것보다 훨씬 더 많은 해를 끼칠 수 있습니다.

null 포인터를 확인해야 할 때의 근본적인 질문에 관해서는 프로그래머 오류를 감지하도록 설계되었으며 침묵하고 감지하기 어려워하지 않도록 자유롭게 주장합니다. 프로그래밍 오류가 아니며 메모리 부족 오류와 같이 프로그래머가 제어 할 수없는 것이라면 null을 확인하고 오류 처리를 사용하는 것이 좋습니다. 그 외에도 디자인 질문이며 함수가 유효한 사전 / 사후 조건으로 간주하는 것을 기반으로합니다.


0

한 가지 방법은 이미 널 확인을하지 않은 한 항상 널 확인을 수행하는 것입니다. 입력 B ()와 ()에 함수 A ()에서 전달되는 그렇다면 이미 포인터를 확인했다 하고 다른 곳에서는 호출되지 않습니다 특정 B ()이며, 그 다음 B는 (A 믿을 수) ()해야합니다 데이터를 삭제했습니다.


1
... 6 개월 시간에 누군가가 따라 와서 통화 B () (아마도 () B를 썼다 누구든지 가정 몇 가지 더 많은 코드 추가 될 때까지 반드시 제대로 NULL을 점검 참조). 그럼 당신은 망쳐지지 않습니까? 기본 규칙-함수에 대한 입력에 대해 유효하지 않은 조건이 존재하는 경우 입력이 함수의 제어 범위를 벗어나기 때문에이를 점검하십시오.
Maximus Minimus 2013

@ mh01 임의의 코드를 스매싱 (예 : 가정하고 문서를 읽지 않음)한다면 추가 NULL검사가 많은 것을 할 것이라고 생각하지 않습니다 . 생각해보십시오 : 이제 B()점검 NULL하고 ... 무엇을합니까? 돌아온다 -1? 발신자가에 대해 확인하지 않으면 어쨌든 반환 값 사례 NULL를 처리 할 것이라는 확신이 -1있습니까?
detly

1
그것이 바로 발신자 책임입니다. 귀하는 귀하가 제공 한 임의의 / 알 수없는 / 잠재적으로 검증되지 않은 입력에 대한 신뢰를 포함하지 않는 자신의 책임을 다합니다. 그렇지 않으면 당신은 경찰 도시에 있습니다. 발신자가 확인하지 않으면 발신자가 실수 한 것입니다. 당신은 확인, 당신의 자신의 엉덩이가 덮여, 당신은 발신자에게 적어도 당신이 일을 제대로했다고 말한 사람에게 말할 수 있습니다.
Maximus Minimus 2013
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.