“항상 변수를 초기화”하지 않아도 중요한 버그가 숨겨지지 않습니까?


34

C ++ 핵심 지침에는 ES.20 : 항상 객체 초기화 규칙이 있습니다.

사용 된 설정 오류 및 관련 정의되지 않은 동작을 피하십시오. 복잡한 초기화 이해 문제를 피하십시오. 리팩토링을 단순화하십시오.

그러나이 규칙은 버그를 찾는 데 도움이되지 않으며 버그 만 숨 깁니다.
프로그램에 초기화되지 않은 변수를 사용하는 실행 경로가 있다고 가정합니다. 버그입니다. 정의되지 않은 동작은 제쳐두고, 문제가 있음을 의미하며 프로그램이 제품 요구 사항을 충족하지 못할 수도 있습니다. 프로덕션 환경에 배포 할 때 돈이 손실되거나 더 악화 될 수 있습니다.

우리는 어떻게 버그를 선별합니까? 우리는 시험을 씁니다. 그러나 테스트는 실행 경로의 100 %를 다루지 않으며 프로그램 입력의 100 %를 다루지 않습니다. 또한 테스트조차도 잘못된 실행 경로를 다루며 여전히 통과 할 수 있습니다. 초기화되지 않은 변수는 다소 유효한 값을 가질 수 있습니다.

그러나 테스트 외에도 0xCDCDCDCD와 같이 초기화되지 않은 변수에 쓸 수있는 컴파일러가 있습니다. 이것은 테스트의 탐지 속도를 약간 향상시킵니다.
더 나은 방법-Address Sanitizer와 같은 도구가 있으며 초기화되지 않은 메모리 바이트를 모두 읽습니다.

그리고 마지막으로 정적 분석기가 있는데, 프로그램을 살펴보고 해당 실행 경로에 미리 읽기 설정이 있음을 알 수 있습니다.

따라서 강력한 도구가 많이 있지만 변수 살균제를 초기화하면 아무것도 찾지 않습니다 .

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

또 다른 규칙이 있습니다. 프로그램 실행에 버그가 발생하면 가능한 빨리 프로그램을 종료해야합니다. 그것을 살릴 필요가 없으며, 단지 충돌하고, 충돌 덤프를 작성하고, 엔지니어에게 조사를 제공하십시오.
변수를 초기화하는 것은 불필요하게 반대를 수행합니다. 그렇지 않으면 이미 세그먼트 오류가 발생했을 때 프로그램이 계속 유지됩니다.


10
나는 이것이 좋은 질문이라고 생각하지만, 나는 당신의 모범을 이해하지 못합니다. 읽기 오류가 발생하고 bytes_read변경되지 않은 경우 (0으로 유지) 왜 버그입니까? 프로그램은 bytes_read!=0이후에 암시 적으로 기대하지 않는 한 여전히 제정신으로 계속 될 수 있습니다 . 그래서 그것은 좋은 살균제입니다 불평하지 않습니다. 반면에, 때 bytes_read사전에 초기화되지, 프로그램이 너무 초기화하지, 제정신이 방식으로 계속 할 수 없습니다 bytes_read실제로 소개 사전에 없었다 버그.
Doc Brown

2
@ Abyx : 타사 인 경우에도 버퍼를 처리하지 않으면 \0버그가 있습니다. 그것을 다루지 않는 것이 문서화된다면, 당신의 호출 코드는 버그가 있습니다. bytes_read==0사용하기 전에 확인하기 위해 호출 코드를 수정 하면 시작한 곳으로 돌아갑니다. 초기화하지 않으면 코드가 버그 bytes_read 있고 안전하다면 코드가 버그 입니다. ( 일반적으로 함수는 오류가 발생하더라도 매개 변수를 채우는 것으로 가정합니다 . 실제로는 아닙니다. 출력은 단독으로 또는 정의되지 않은 채로 남아 있습니다.)
Mat

1
이 코드가에 의해 err_t반환 된 것을 무시하는 이유 가 my_read()있습니까? 예제의 어느 곳에도 버그가 있다면 그게 전부입니다.
Blrfl

1
쉽습니다. 의미있는 경우에만 변수를 초기화하십시오. 그렇지 않다면 그렇지 않습니다. "더미"데이터를 사용하여 버그를 감추기 때문에 나쁘다는 데 동의 할 수 있습니다.
피터 B

1
"또 다른 규칙이있다-프로그램 실행에 버그가 발생하면 프로그램은 가능한 빨리 죽어야한다. 생존 할 필요가없고, 단지 충돌하고, 충돌 덤프를 작성하고, 조사를 위해 엔지니어들에게 제공해야한다." 제어 소프트웨어. 비행기 잔해에서 크래시 덤프를 복구하는 행운을 빕니다.
Giorgio

답변:


44

여러 계정에서 추론이 잘못되었습니다.

  1. 세그먼테이션 결함은 발생하기에 확실하지 않습니다. 초기화되지 않은 변수를 사용하면 정의되지 않은 동작이 발생 합니다. 세그먼테이션 결함은 이러한 동작이 나타날 수있는 한 가지 방법이지만 정상적으로 실행되는 것처럼 보입니다.
  2. 컴파일러는 초기화되지 않은 메모리를 정의 된 패턴 (예 : 0xCD)으로 채우지 않습니다. 초기화되지 않은 변수가 사용되는 곳을 찾는 데 도움을주기 위해 일부 디버거가 수행하는 작업입니다. 이러한 프로그램을 디버거 외부에서 실행하면 변수에 완전히 임의의 가비지가 포함됩니다. 같은 카운터 bytes_read가 값 10을 가질 때 값 을 가질 가능성이 높습니다 0xcdcdcdcd.
  3. 초기화되지 않은 메모리를 고정 패턴으로 설정하는 디버거에서 실행하더라도 시작시에만 수행됩니다. 이는이 메커니즘이 정적 (가능하면 힙 할당) 변수에 대해서만 안정적으로 작동 함을 의미합니다. 스택에 할당되거나 레지스터에만 존재하는 자동 변수의 경우 변수가 이전에 사용 된 위치에 저장 될 가능성이 높으므로 정보 기억 패턴이 이미 덮어 써졌습니다.

항상 변수를 초기화하는 지침 뒤에 아이디어는이 두 가지 상황을 가능하게하는 것입니다

  1. 변수는 존재의 시작부터 유용한 값을 포함합니다. 변수를 필요할 때만 선언하기위한 지침과이를 결합하면 변수가 존재하지만 초기화되지 않은 선언과 첫 번째 할당 사이에서 변수를 사용하기 시작하는 함정에 빠질 미래 유지 보수 프로그래머는 피할 수 있습니다.

  2. 변수에는 함수 my_read가 값을 업데이트 했는지 여부를 확인하기 위해 나중에 테스트 할 수있는 정의 된 값이 포함 됩니다. 초기화하지 않으면 bytes_read시작한 값을 알 수 없기 때문에 실제로 유효한 값이 있는지 알 수 없습니다.


8
1) 1 % 대 99 %와 같은 확률에 관한 것입니다. 2 및 3) VC ++는 로컬 변수에 대해서도 이러한 초기화 코드를 생성합니다. 3) 정적 (전역) 변수는 항상 0으로 초기화됩니다.
Abyx

5
@Abyx : 1) 내 경험에 따르면, 확률은 ~ 80 % "즉시 명백한 행동 차이가 없음", 10 % "잘못된 행동", 10 % "segfault"입니다. (2)와 (3)의 경우 : VC ++는 디버그 빌드에서만이를 수행합니다. 릴리스 빌드를 선택적으로 중단하고 많은 테스트에 표시되지 않기 때문에 그것에 의지하는 것은 매우 나쁜 생각입니다.
Christian Aichinger

8
"지침 뒤에 아이디어"가이 답변의 가장 중요한 부분이라고 생각합니다. 이 지침은으로 모든 변수 선언을 따르도록 지시하지 않습니다 = 0;. 조언의 목적은 유용한 값을 얻을 수있는 시점에서 변수를 선언하고 즉시이 값을 지정하는 것입니다. 이것은 바로 다음 규칙 ES21 및 ES22에서 명확하게 밝혀졌습니다. 이 세 가지는 모두 함께 일하는 것으로 이해되어야합니다. 개별적인 관련없는 규칙이 아닙니다.
GrandOpener

1
@GrandOpener 정확합니다. 변수가 선언 된 시점에 할당 할 의미있는 값이 없으면 변수 범위가 잘못되었을 수 있습니다.
Kevin Krumwiede

5
"컴파일러가 절대로 채워지지 않는다"고 항상 그렇지는 않습니까?
코드 InChaos

25

당신은 "이 규칙은 버그를 찾는 데 도움이되지 않고 단지 숨길뿐"이라고 썼습니다. 글쎄, 규칙의 목표는 버그를 찾는 것을 돕는 것이 아니라 버그를 피하는 것입니다. 그리고 버그를 피할 때 숨겨진 것은 없습니다.

예제와 관련하여 문제를 설명 할 수 있습니다. my_read함수는 bytes_read모든 상황에서 초기화하기 위해 서면 계약을 가지고 있지만 오류가 발생하지 않았기 때문에 적어도이 경우에는 결함 이 있다고 가정하십시오 . bytes_read매개 변수를 먼저 초기화하지 않음으로써 런타임 환경을 사용하여 해당 버그를 표시하는 것이 목적입니다 . 주소 살균제가 있는지 확실하게 알고 있다면 실제로 그러한 버그를 감지하는 가능한 방법입니다. 버그를 수정하려면 my_read내부적으로 기능 을 변경해야 합니다.

그러나 적어도 동등하게 유효합니다보기의 다른 점은,이 : 결함이있는 행동은 단지에서 나온다 조합 초기화하지 않는 bytes_read사전, 호출 my_read(기대가 함께 나중에 bytes_read그 후 초기화된다). 이러한 기능에 대한 서면 사양 my_read이 100 % 명확하지 않거나 오류 발생시 동작에 대해 잘못된 경우 실제 구성 요소에서 자주 발생하는 상황입니다 . 그러나 bytes_read호출하기 전에 0으로 초기화되는 한 프로그램은 초기화가 내부 my_read에서 수행 된 것과 같은 방식으로 작동하므로 올바르게 작동 하므로이 조합에서는 프로그램에 버그가 없습니다.

따라서 그에 따른 권장 사항은 다음과 같습니다.

  • 함수 또는 코드 블록이 특정 매개 변수를 초기화하는지 테스트 하려는 경우
  • 스테이크의 함수에 해당 매개 변수에 값을 지정하지 않는 것이 확실히 잘못된 계약이 있다고 100 % 확신합니다.
  • 당신은 환경이 이것을 잡을 수 있다고 100 % 확신합니다

특정 툴링 환경에 대해 일반적으로 테스트 코드 에서 정렬 할 수있는 조건 입니다.

그러나 프로덕션 코드에서는 항상 그러한 변수를 미리 초기화하는 것이 더 좋습니다. 계약이 불완전하거나 잘못된 경우 또는 주소 소독제 또는 이와 유사한 안전 조치가 활성화되지 않은 경우 버그를 방지하는보다 방어적인 접근 방식입니다. 프로그램 실행에 버그가 발생하면 올바르게 작성한대로 "충돌 초기"규칙이 적용됩니다. 그러나 변수를 미리 초기화하면 아무런 문제가 없다는 것을 의미하므로 추가 실행을 중지 할 필요가 없습니다.


4
이것이 제가 읽을 때 생각했던 것과 정확히 같습니다. 깔개 밑을 청소하는 것이 아니라 쓰레기통으로 청소하는 것입니다!
corsiKa

22

항상 변수를 초기화하십시오

고려중인 상황의 차이점은 초기화가없는 경우 정의되지 않은 동작 이 발생하고 초기화하는 데 시간이 걸린 경우 잘 정의 되고 결정적인 버그 가 발생한다는 것 입니다. 이 두 경우가 얼마나 다른지 강조 할 수 없습니다.

가상 시뮬레이션 프로그램에서 가상 직원에게 발생할 수있는 가상의 예를 고려하십시오. 이 가상의 팀은 가상적으로 판매하는 제품이 요구를 충족 시켰음을 입증하기 위해 결정 론적 시뮬레이션을 만들려고했습니다.

좋아, 나는 단어 주입으로 멈출 것이다. 나는 당신이 요점을 얻는 것 같아 ;-)

이 시뮬레이션에는 초기화되지 않은 수백 개의 변수가있었습니다. 한 개발자가 시뮬레이션에서 valgrind를 실행했으며 "초기화되지 않은 값에 대한 분기"오류가 있음을 발견했습니다. "음, 그것은 비결정론을 야기 할 수있는 것처럼 보이며, 가장 필요할 때 테스트 실행을 반복하기가 어렵습니다." 개발자는 관리 부서에 갔지만 관리 일정이 빡빡해서이 문제를 추적 할 리소스를 확보 할 수 없었습니다. "모든 변수를 사용하기 전에 모든 변수를 초기화합니다. 코딩 방법이 우수합니다."

시뮬레이션이 완전 이탈 모드에 있고 팀 전체가 예산을 책정 한 모든 프로젝트가 너무 작았 기 때문에 약속 된 모든 것을 마치기 위해 최종 배송이 시작되기 몇 달 전에 전체 팀이 달리고 있습니다. 어떤 이유로 인해 결정 론적 시뮬레이션이 결정적으로 디버그하기 위해 작동하지 않았기 때문에 필수 기능을 테스트 할 수 없다는 사실을 알게되었습니다.

전체 팀이 중단되고 2 개월 동안 전체 시뮬레이션 코드베이스를 결합하여 기능을 구현하고 테스트하는 대신 초기화되지 않은 값 오류를 수정하는 데 더 많은 시간을 소비했을 수 있습니다. 말할 것도없이, 직원은 "나는 당신에게 그렇게 말한 것"을 건너 뛰고 다른 개발자들이 초기화되지 않은 값이 무엇인지 이해하도록 도와주었습니다. 이상하게도,이 사고 직후 코딩 표준이 변경되어 개발자가 항상 변수를 초기화하도록 권장했습니다.

그리고 이것은 경고입니다. 이것은 코를 가로 지르는 총알입니다. 실제 문제는 생각보다 훨씬 더 교활합니다.

초기화되지 않은 값을 사용하는 것은 "정의되지 않은 동작"입니다 (와 같은 일부 경우 제외 char). 정의되지 않은 행동 (또는 짧게 말해서 UB)은 당신에게 미쳤고 완전히 나쁘기 때문에, 그것이 대안보다 낫다고 결코 믿어서는 안됩니다. 때로는 특정 컴파일러가 UB를 정의하고 사용하기에 안전하다는 것을 알 수 있지만 정의되지 않은 동작은 "컴파일러가 느끼는 모든 동작"입니다. 지정되지 않은 값을 갖는 것처럼 "sane"이라고 부르는 것을 수행 할 수 있습니다. 잘못된 opcode를 방출하여 잠재적으로 프로그램이 손상 될 수 있습니다. 그것은 컴파일 타임에 경고를 트리거하거나, 컴파일러는 수 크게하면 오류를 고려한다.

아니면 전혀 아무것도 할 수 없습니다

UB의 탄광에서 카나리아는 내가 읽은 SQL 엔진의 사례입니다. 연결하지 않은 것을 용서해주세요. 기사를 다시 찾지 못했습니다. 더 큰 버퍼 크기를 함수에 전달할 때 특정 버전의 데비안에서만 SQL 엔진에서 버퍼 오버런 문제가 발생했습니다 . 버그는 정식으로 기록되고 조사되었습니다. 재미있는 부분은 : 버퍼 오버런 이 확인되었습니다 . 버퍼 오버런을 처리하는 코드가 있습니다. 다음과 같이 보였습니다 :

// move the pointers properly to copy data into a ring buffer.
char* putIntoRingBuffer(char* begin, char* end, char* get, char*put, char* newData, unsigned int dataLength)
{
    // If dataLength is very large, we might overflow the pointer
    // arithmetic, and end up with some very small pointer number,
    // causing us to fail to realize we were trying to write past the
    // end.  Check this before we continue
    if (put + dataLength < put)
    {
        RaiseError("Buffer overflow risk detected");
        return 0;
    }
    ...
    // typical ring-buffer pointer manipulation followed...
}

내 표현에 더 많은 의견을 추가했지만 아이디어는 동일합니다. 경우 put + dataLength랩 주위, 그것은보다 작은 것 put포인터 (그들은 확실히 부호 INT는 호기심에 대한 포인터의 크기였다 있도록 컴파일시 검사를했다). 이런 일이 발생하면 표준 링 버퍼 알고리즘이이 오버플로로 인해 혼동 될 수 있다는 것을 알고 있으므로 0을 반환합니다 .

결과적으로 C ++에서는 포인터의 오버플로가 정의되지 않았습니다. 대부분의 컴파일러는 포인터를 정수로 취급하기 때문에 원하는 정수 오버플로 동작이 발생합니다. 그러나이 있다 정의되지 않은 동작 컴파일러에게 허용되는 의미, 아무것도 그것을 원하는합니다.

이 버그의 경우, 데비안은 무슨 일이 있었 다른 주요 리눅스 맛 것도 자신의 생산 릴리스에서 업데이트되지했다고 GCC의 새 버전을 사용하도록 선택할 수 있습니다. 이 새로운 버전의 gcc는보다 공격적인 데드 코드 최적화 기능을 가지고있었습니다. 컴파일러는 정의되지 않은 동작을 확인하고 if명령문 의 결과 가 "코드 최적화를 최고로 만드는 것"이라고 결론을 내 렸습니다. 이는 UB의 절대적으로 합법적 인 번역이었습니다. 따라서, UB 포인터 오버 플로우 없이는 ptr+dataLength절대로 아래에있을 수 없으므로 명령문이 절대 트리거되지 않고 버퍼 오버런 검사를 최적화한다고 가정했습니다.ptrif

"sane"UB를 사용하면 실제로 주요 SQL 제품에 버퍼 오버런 악용 발생 하여 코드를 작성하지 않아도 됩니다.

정의되지 않은 동작에 의존하지 마십시오. 이제까지.


정의되지 않은 동작에 대한 매우 재미있는 읽기를 위해 software.intel.com/en-us/blogs/2013/01/06/… 은 얼마나 나쁜지에 대한 놀랍도록 잘 작성된 게시물입니다. 그러나 그 특정 게시물은 원자 작업에 관한 것이므로 매우 혼란 스럽습니다. 따라서 UB의 입문서와 잘못 될 수있는 방법으로 추천하지 마십시오.
Cort Ammon

1
C가 본질적으로 lvalue 또는 이들의 배열을 초기화되지 않은, 트래핑되지 않은 불확정 값 또는 지정되지 않은 값으로 설정하거나 불쾌한 lvalue를 덜 불쾌한 값으로 설정하거나 (비 트래핑 불확정 또는 지정되지 않음) 정의 된 값을 그대로 둡니다. 컴파일러는 이러한 지시문을 사용하여 유용한 최적화를 지원할 수 있으며 프로그래머는이 지시문을 사용하여 희소 행렬 기술과 같은 것을 사용할 때 쓸모없는 코드를 작성하지 않고도 "최적화"를 차단할 수 있습니다.
supercat

@supercat 유효한 솔루션 인 플랫폼을 대상으로 가정 할 때 유용한 기능입니다. 알려진 문제의 예 중 하나는 메모리 유형에 유효하지 않을뿐만 아니라 일반적인 방법으로는 달성 할 수없는 메모리 패턴을 생성하는 기능입니다. bool는 명백한 문제가있는 훌륭한 예이지만 x86 또는 ARM 또는 MIPS와 같은 매우 유용한 플랫폼에서 작업하고 있다고 생각하지 않는 한 이러한 문제는 모두 opcode 시간에 해결됩니다.
콜트 암몬

정수 산술의 크기로 인해 옵티마이 저가 a에 사용 된 값 switch이 8 미만 임을 증명할 수 있으므로 "큰"값이 올 위험이 없다고 가정 한 빠른 명령을 사용할 수 있습니다. 지정되지 않은 값 (컴파일러의 규칙을 사용하여 구성 할 수 없음)이 나타나고 예기치 않은 작업이 수행되고 갑자기 점프 테이블의 끝에서 엄청난 점프가 발생합니다. 여기에 지정되지 않은 결과를 허용한다는 것은 프로그램의 모든 스위치 명령문에 "발생할 수없는"이러한 경우를 지원하기위한 추가 트랩이 있어야 함을 의미 합니다.
Cort Ammon

내장 함수가 표준화 된 경우, 컴파일러는 의미를 존중하기 위해 필요한 모든 작업을 수행해야합니다. 예를 들어 일부 코드 경로가 변수를 설정하고 일부는 설정하지 않고 내장 함수는 "초기화되지 않았거나 불확실한 경우 지정되지 않은 값으로 변환; 그렇지 않으면 그대로 두십시오"라고 말하면 "값이 아님"레지스터가있는 플랫폼의 컴파일러는 코드 경로 전에 초기화하거나 코드 경로에서 초기화하는 변수를 초기화하려면 코드를 삽입하십시오. 그렇지 않으면 초기화가 누락되지만 그렇게하는 데 필요한 의미 분석은 매우 간단합니다.
supercat

5

나는 주로 변수를 재 할당 할 수없는 함수형 프로그래밍 언어로 작업합니다. 이제까지. 이 클래스의 버그를 완전히 제거합니다. 이것은 처음에는 큰 제약처럼 보였지만 새로운 데이터를 배우는 순서와 일치하는 방식으로 코드를 구성해야하므로 코드를 단순화하고 유지 관리하기가 쉽습니다.

이러한 습관은 명령형 언어로도 이어질 수 있습니다. 더미 값으로 변수를 초기화하지 않도록 코드를 리팩터링하는 것이 거의 항상 가능합니다. 그것이 그 지침서에서 지시하는 것입니다. 자동화 된 도구를 행복하게 만드는 것이 아니라 의미있는 것을 넣기를 원합니다.

C 스타일 API를 사용한 예제는 조금 더 까다 롭습니다. 이 경우 함수를 사용할 때 컴파일러가 불평하지 않도록 0으로 초기화하지만 my_read단위 테스트 에서 한 번은 오류 조건이 제대로 작동하는지 확인하기 위해 다른 것으로 초기화합니다. 모든 사용시 가능한 모든 오류 조건을 테스트 할 필요는 없습니다.


5

아니요, 버그를 숨기지 않습니다. 대신 사용자가 오류가 발생하면 개발자가이를 재현 할 수있는 방식으로 동작을 결정 론적으로 만듭니다.


1
-1로 초기화하면 실제로 의미가 있습니다. "int bytes_read = 0"은 실제로 0 바이트를 읽을 수 있기 때문에 -1로 초기화하면 바이트 읽기 시도가 성공하지 못했음을 확인할 수 있으므로 테스트 할 수 있습니다.
피터 B

4

TL; DR :이 프로그램을 올바르게 만드는 두 가지 방법이 있습니다. 변수를 초기화하고기도하십시오. 단 하나의 결과 만 일관되게 제공합니다.


귀하의 질문에 대답하기 전에 먼저 정의되지 않은 행동의 의미를 설명해야 합니다. 실제로 컴파일러 작성자가 많은 작업을 수행하도록하겠습니다.

해당 기사를 읽지 않으려는 경우 TL; DR은 다음과 같습니다.

정의되지 않은 동작 은 개발자와 컴파일러 간의 사회적 계약입니다. 컴파일러는 사용자가 정의되지 않은 동작에 의존하지 않을 것이라고 맹목적으로 생각합니다.

"코에서 날아 다니는 악마"의 원형은 불행히도이 사실의 의미를 완전히 전달하지 못했습니다. 모든 일이 일어날 수 있음을 증명하기는했지만 믿기 어려워서 대부분 으 sh했습니다.

그러나 진실은 정의되지 않은 동작 이 프로그램을 사용하려고 시도하기도 전에 (디버거 내에서, 그렇지 않은지) 컴파일 자체에 영향을 미치고 그 동작을 완전히 변경할 수 있다는 것입니다.

나는 위의 2 부에서 놀라운 예를 찾습니다.

void contains_null_check(int *P) {
  int dead = *P;
  if (P == 0)
    return;
  *P = 4;
}

로 변환됩니다 :

void contains_null_check(int *P) {
  *P = 4;
}

확인하기 전에 역 참조되기 때문에 P불가능한 것이 분명하기 때문 0입니다.


이것이 당신의 모범에 어떻게 적용됩니까?

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

정의되지 않은 동작으로 인해 런타임 오류가 발생 한다고 가정하는 일반적인 실수를 저지른 것입니다. 그렇지 않을 수 있습니다.

정의 my_read가 다음 과 같다고 상상해 보자 .

err_t my_read(buffer_t buffer, int* bytes_read) {
    err_t result = {};
    int blocks_read = 0;
    if (!(result = low_level_read(buffer, &blocks_read))) { return result; }
    *bytes_read = blocks_read * BLOCK_SIZE;
    return result;
}

인라인이있는 우수한 컴파일러가 예상대로 진행하십시오.

int bytes_read; // UNINITIALIZED

// start inlining my_read

err_t result = {};
int blocks_read = 0;
if (!(result = low_level_read(buffer, &blocks_read))) {
    // nothing
} else {
    bytes_read = blocks_reads * BLOCK_SIZE;
}

// end of inlining my_read

buffer.shrink(bytes_read);

그런 다음 좋은 컴파일러가 예상되는대로 쓸모없는 분기를 최적화합니다.

  1. 초기화되지 않은 변수는 사용하지 않아야합니다
  2. bytes_read그렇지 않은 경우 초기화되지 않은 상태 result로 사용됩니다0
  3. 개발자는 result결코 없을 것이라고 약속 합니다 0!

그래서는 result결코 0:

int bytes_read; // UNINITIALIZED
err_t result = {};
int blocks_read = 0;
result = low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

아, result절대 사용되지 않습니다 :

int bytes_read; // UNINITIALIZED
int blocks_read = 0;
low_level_read(buffer, &blocks_read);

bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

아, 우리는 선언을 연기 할 수 있습니다 bytes_read:

int blocks_read = 0;
low_level_read(buffer, &blocks_read);

int bytes_read = blocks_reads * BLOCK_SIZE;
buffer.shrink(bytes_read);

그리고 우리는 원래의 변형을 엄격히 확인하고 있으며 디버거는 초기화되지 않은 변수가 없기 때문에 트랩하지 않습니다.

나는 그 길로 내려 왔고, 예상되는 행동과 조립이 맞지 않을 때의 문제를 이해하는 것은 실제로 재미가 없습니다.


때로는 컴파일러가 UB 경로를 실행할 때 소스 파일을 삭제하도록 프로그램을 가져와야한다고 생각합니다. 그러면 프로그래머는 최종 사용자에게 UB의 의미를 배우게됩니다 ....
mattnz

1

예제 코드를 자세히 살펴 보겠습니다.

int bytes_read = 0;
my_read(buffer, &bytes_read); // err_t my_read(buffer_t, int*);
// bytes_read is not changed on read error.
// It's a bug of "my_read", but detection is suppressed by initialization.
buffer.shrink(bytes_read); // Uninitialized bytes_read could be detected here.

// Another bug: use empty buffer after read error.
use(buffer);

이것은 좋은 예입니다. 이와 같은 오류가 예상되면 라인을 삽입하고 assert(bytes_read > 0);런타임에이 버그를 잡을 수 있습니다 . 초기화되지 않은 변수로는 불가능합니다.

그러나 그렇지 않다고 가정하고 함수 내부에 오류가 있다고 가정합니다 use(buffer). 디버거에 프로그램을로드하고 역 추적을 확인한 후이 코드에서 호출 된 것을 확인합니다. 따라서이 스 니펫 상단에 중단 점을두고 다시 실행 한 후 버그를 재현합니다. 우리는 그것을 잡으려고 노력하는 단일 단계입니다.

초기화하지 않은 경우 bytes_read가비지가 포함됩니다. 매번 같은 쓰레기를 반드시 포함 할 필요는 없습니다. 우리는 선을 넘어 섰다 my_read(buffer, &bytes_read);. 이제 이전과 다른 값이면 버그를 전혀 재현하지 못할 수도 있습니다! 다음 번에 동일한 입력에서 완전한 사고로 작동 할 수 있습니다. 지속적으로 0이면 일관된 동작을 얻습니다.

우리는 아마도 같은 실행에서 백 트레이스에서도 값을 확인합니다. 이 제로라면, 우리는 할 수 있습니다 보고 뭔가 잘못이다; bytes_read성공하면 0이되어서는 안됩니다. 또는 가능하면 -1로 초기화 할 수도 있습니다. 여기서 버그를 발견 할 수 있습니다. 그래도 bytes_read그럴듯한 가치가 잘못 되었다면, 한눈에 알아 볼까요?

이것은 특히 포인터에 해당합니다. NULL 포인터는 디버거에서 항상 분명하며 매우 쉽게 테스트 할 수 있으며 역 참조하려는 경우 최신 하드웨어에서 segfault를 사용해야합니다. 가비지 포인터는 나중에 재현 할 수없는 메모리 손상 버그를 유발할 수 있으며 디버깅이 거의 불가능합니다.


1

OP는 정의되지 않은 동작에 의존하지 않거나 최소한 정확하지 않습니다. 실제로, 정의되지 않은 행동에 의존하는 것은 나쁘다. 동시에, 예기치 않은 경우 프로그램의 동작 정의되지 않지만 다른 종류의 정의되지 않습니다. 변수를 0으로 설정했지만 초기 0을 사용하는 실행 경로를 사용하지 않으려는 경우 버그가 있고 경로 가 있으면 프로그램이 제대로 작동 하지 않습니까? 당신은 이제 잡초에 있습니다. 그 값을 사용할 계획은 없지만 어쨌든 사용하고 있습니다. 손상되지 않거나 프로그램이 중단되거나 프로그램이 자동으로 데이터를 손상시킬 수 있습니다. 당신은 모른다.

OP가 말하는 것은이 버그를 찾는 데 도움이되는 도구가 있다는 것입니다. 값을 초기화하지 않고 어쨌든 사용하는 경우 버그가 있음을 알려주는 정적 및 동적 분석기가 있습니다. 정적 분석기는 프로그램 테스트를 시작하기 전에 알려줍니다. 반면에 맹목적으로 값을 초기화하면 분석기에서 해당 초기 값을 사용하지 않을 것이라고 알 수 없으므로 버그가 감지되지 않습니다. 운이 좋으면 무해하거나 단순히 프로그램을 중단시킵니다. 운이 좋지 않으면 데이터가 자동으로 손상됩니다.

내가 OP에 동의하지 않는 유일한 장소는 맨 마지막에 있으며 "여기서는 이미 세그먼트 오류가 발생했을 때"라고 말합니다. 실제로 초기화되지 않은 변수는 안정적으로 세그먼테이션 오류를 생성하지 않습니다. 대신, 나는 당신이 프로그램을 실행하려는 시도조차 할 수없는 정적 분석 도구를 사용해야한다고 말하고 싶습니다.


0

질문에 대한 답변은 프로그램 내에 나타나는 다른 유형의 변수로 분류되어야합니다.


지역 변수

일반적으로 선언은 변수가 처음 값을 얻는 지점에 있어야합니다. 이전 스타일 C와 같은 변수를 미리 선언하지 마십시오.

//Bad: predeclared variables
int foo = 0;
double bar = 0.0;
long* baz = NULL;

bar = getBar();
foo = (int)bar;
baz = malloc(foo);


//Correct: declaration and initialization at the same place
double bar = getBar();
int foo = (int)bar;
long* baz = malloc(foo);

이는 초기화 필요성의 99 %를 제거하고 변수는 최종 값을 즉시 해제합니다. 몇 가지 예외는 초기화가 어떤 조건에 따라 달라지는 경우입니다.

Base* ptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}

다음과 같은 경우를 작성하는 것이 좋습니다.

Base* ptr = nullptr;
if(foo()) {
    ptr = new Derived1();
} else {
    ptr = new Derived2();
}
assert(ptr);

나. 변수의 현명한 초기화가 수행되도록 명시 적으로 주장하십시오.


멤버 변수

나는 다른 응답자가 말한 것에 동의합니다. 이들은 항상 생성자 / 초기화 기 목록에 의해 초기화되어야합니다. 그렇지 않으면 회원들 사이의 일관성을 유지하기가 어렵습니다. 또한 모든 경우에 초기화가 필요하지 않은 멤버 집합이있는 경우 클래스를 리팩터링하여 해당 멤버를 항상 필요한 곳에 파생 클래스에 추가하십시오.


버퍼

이것은 내가 다른 답변에 동의하지 않는 곳입니다. 사람들이 변수 초기화에 대해 종교적으로 행동 할 때 종종 다음과 같이 버퍼를 초기화합니다.

char buffer[30];
memset(buffer, 0, sizeof(buffer));

char* buffer2 = calloc(30);

나는 이것이 거의 항상 유해하다고 믿습니다.이 초기화의 유일한 효과는 도구를 valgrind무력하게 렌더링한다는 것 입니다. 초기화 된 버퍼에서 더 많은 코드를 읽는 코드는 버그 일 가능성이 높습니다. 그러나 초기화하면 해당 버그가에 노출 될 수 없습니다 valgrind. 따라서 0으로 채워지는 메모리에 실제로 의존하지 않는 한 사용하지 마십시오 (이 경우 0이 필요한 것을 주석으로 처리하십시오).

또한 valgrind초기화 전 사용 버그와 메모리 누수를 노출시키기 위해 전체 테스트 스위트를 실행하는 툴 또는 유사한 툴을 빌드 시스템에 추가하는 것이 좋습니다 . 이것은 변수의 모든 사전 초기화보다 더 중요합니다. 이 valgrind대상은 코드가 공개되기 전에 정기적으로 실행되어야합니다.


글로벌 변수

초기화되지 않은 전역 변수는 가질 수 없으므로 (적어도 C / C ++ 등에서)이 초기화가 원하는지 확인하십시오.


삼항 연산자를 사용하여 조건부 초기화를 작성할 수 있습니다. 예 : Base& b = foo() ? new Derived1 : new Derived2;
Davislor

@Lorehead 간단한 경우에는 효과가 있지만 더 복잡한 경우에는 효과가 없습니다. 세 개 이상의 사례가있는 경우이 작업을 수행하지 않으려는 경우 생성자가 가독성을 위해 세 개 이상의 인수를 사용합니다. 원인. 그리고 루프에서 하나의 초기화 분기에 대한 인수를 검색하는 것과 같이 수행해야 할 계산도 고려하지 않습니다.
cmaster December

더 복잡한 경우에는 초기화 코드를 초기화 함수로 래핑 할 수 Base &b = base_factory(which);있습니다. 이것은 코드를 두 번 이상 호출해야하거나 결과를 일정하게 만들 수있는 경우에 가장 유용합니다.
Davislor

@Lorehead 그것은 사실이며, 필요한 논리가 간단하지 않으면 갈 길입니다. 그럼에도 불구하고, 초기화 비아 ?:가 PITA 인 곳 사이에 작은 회색 영역이 있으며 팩토리 기능은 여전히 ​​과도 하다고 생각합니다 . 이러한 경우는 거의 없으며 그 사이에 존재합니다.
cmaster

-2

올바른 컴파일러 옵션이 설정된 괜찮은 C, C ++ 또는 Objective-C 컴파일러는 변수가 값을 설정하기 전에 변수가 사용되는지 컴파일 타임에 알려줍니다. 이러한 언어에서 초기화되지 않은 변수의 값을 사용하는 것은 정의되지 않은 동작이므로 "사용하기 전에 값을 설정"하는 것은 힌트 나 지침 또는 모범 사례가 아니며 100 % 요구 사항입니다. 그렇지 않으면 프로그램이 완전히 중단됩니다. Java 및 Swift와 같은 다른 언어에서는 컴파일러가 변수를 초기화하기 전에 변수를 사용할 수 없습니다.

"초기화"와 "값 설정"사이에는 논리적 차이가 있습니다. 달러와 유로 사이의 전환율을 찾고 "double rate = 0.0;"이라고 쓰려면 변수에 값이 설정되어 있지만 초기화되지 않았습니다. 여기에 저장된 0.0은 올바른 결과와 관련이 없습니다. 이 상황에서 버그로 인해 정확한 전환율을 저장하지 않으면 컴파일러에서 알려줄 기회가 없습니다. 방금 "이중 요금"이라고 쓴 경우 컴파일러는 의미있는 전환율을 저장하지 않았습니다.

컴파일러가 변수를 초기화하지 않고 사용한다고 알려주기 때문에 변수를 초기화하지 마십시오. 그것은 버그를 숨기고 있습니다. 실제 문제는 사용해서는 안되는 변수를 사용하거나 하나의 코드 경로에서 값을 설정하지 않은 변수를 사용한다는 것입니다. 문제를 해결하고 숨기지 마십시오.

컴파일러가 초기화되지 않고 사용되었다고 말할 수 있기 때문에 변수를 초기화하지 마십시오. 다시, 당신은 문제를 숨기고 있습니다.

사용하기에 가까운 변수를 선언하십시오. 이렇게하면 선언 시점에서 의미있는 값으로 초기화 할 수있는 가능성이 높아집니다.

변수를 재사용하지 마십시오. 변수를 재사용하면 변수를 두 번째 목적으로 사용할 때 쓸모없는 값으로 초기화 될 가능성이 높습니다.

일부 컴파일러에는 잘못된 부정이 있으며 초기화 확인은 중지 문제와 동일하다고 언급되었습니다. 둘 다 실제로는 관련이 없습니다. 인용 된 바와 같이, 컴파일러가 버그가보고 된 후 10 년 후에 초기화되지 않은 변수의 사용을 찾을 수 없다면, 대체 컴파일러를 찾아야 할 때입니다. Java는 이것을 두 번 구현합니다. 컴파일러에서 한 번, 검증기에서 한 번, 문제없이. 정지 문제를 해결하는 쉬운 방법은 변수를 사용하기 전에 초기화하는 것이 아니라 간단하고 빠른 알고리즘으로 확인할 수있는 방식으로 사용하기 전에 초기화하는 것입니다.


이것은 피상적으로는 좋지만 초기화되지 않은 값 경고의 정확성에 너무 의존합니다. 이것들을 완벽하게 수정하는 것은 Halting Problem과 동일하며 프로덕션 컴파일러는 잘못된 부정을 겪을 수 있습니다 (즉 , 초기화되지 않은 변수를 진단 하지 않음 ). 예를 들어 , 10 년 이상 수정되지 않은 GCC 버그 18501을 참조하십시오 .
zwol

당신이 gcc에 대해 말하는 것은 방금 말한 것입니다. 나머지는 관련이 없습니다.
gnasher729

gcc에 대해서는 슬프지만 나머지가 왜 관련이 있는지 이해하지 못하면 스스로 교육해야합니다.
zwol
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.