
Syntax & Sementic Error
프로그래밍을 하며 오류가 발생하는건 피할 수 없는 일이며 이를 디버깅하여 고치는것이 정말 중요하다
따라서 다양한 디버깅 기술과 경험은 프로그래밍에 있어 굉장한 도움이 된다
우선 Syntax Error는 구문 오류로 C++ 문법에 맞지 않는 코드를 작성할 때 발생한다
(;누락, 괄호, 변수명(숫자 시작과 같은) 등)
int main()
{
int a //;누락
//괄호 누락
컴파일러는 이러한 구문 오류를 감지하고 에러를 뱉어주기 때문에 프로그래머는 쉽게 찾아 수정할 수 있다
Sementic Error는 의미 오류이다, 코드가 문법적으로는 완벽하지만 프로그래머의 의도대로 동작하지 않을 때 발생하는 에러이다
물론 특정 sementic error는 컴파일러에 의해 감지된다 (선언되지 않은 변수 사용, 타입 불일치 등)
int main()
{
a = 100; //선언되지 않은 변수
return "Hi"; //타입 불일치
}
이외의 다른 sementic error는 런타임에서 발생한다
아주 중요한 sementic error원인 중 하나는 0으로 나누는것이다
/ 연산에서 0으로 나누게 되면 무조건 크래시가 발생하게 된다 (매우 주의)
그리고 초기화 되지 않은 값 사용으로 인한 의미 오류, 잘못된 함수 이름과 시그니쳐 (함수 이름은 add지만 내부적으로 기능은 subtract인 경우)
return;이 다른 코드보다 위에 있어 밑의 코드가 호출되지 않는 경우 등 다양하게 존재한다
이러한 sementic error를 해결하기 위해 다양한 디버깅 기술과 경험이 필요하다
주석처리를 활용한 debugging
가장 기본적인 디버깅 프로세스는 주석처리이다
프로그램이 개발자의 의도와 맞지않는 동작을 할 때 의심되는 부분을 주석처리 하여 실행시켜보고 정상 동작하면 해당 코드를 수정하는 방식이다
console 출력을 활용한 debugging
다음은 console 출력을 통해 디버깅하는 방식이다
(원하는 literal text나 값을 출력하여 디버깅)
함수의 호출 순서로 인해 sementic error가 발생할 때 찾기 좋은 방법이다
디버깅 할 때 console 출력 명령은 std::cout대신 std::cerr을 사용하는것이 좋다, cout은 buffering이 존재하기 때문에 디버깅 타이밍에서 원하는 데이터를 출력하지 못할 가능성이 존재한다
std::cerr은 buffering되지 않기 때문에 원하는 출력 요청을 전부 출력한다
int GetValue()
{
std::cerr << "GetValue()" << std::endl;
}
해당 console 출력으로 의도한 순서대로 함수가 호출되는지 확인하여 디버깅이 가능하다
이러한 debugging용 코드는 들여쓰기 하지 않는것이 좋을 수 있다 (추후에 제거하기 쉽다)
하지만 기본적으로 이러한 console 출력 명령문으로 디버깅하는건 좋은 방법은 아니다, 코드를 복잡하게 만들며 이러한 디버깅 코드를 추가,제거하며 또 다른 이슈를 발생시킬 수 있기 때문이다 또한 이러한 디버깅 코드는 재사용이 어렵다
조건부 전처리 지시문을 이용한 debugging
디버깅 코드를 재사용성이 좋게 하기 위해 조건부 전처리 지시문을 이용하는 방법이 있다
#define ENABLE_DEBUG
int GetValue()
{
#ifdef ENABLE_DEBUG
std::cerr << "GetValue()" << std::endl;
#endif
return 5;
}
define을 했을때, 안 했을때에 따라 직접 디버그 모드로 전환이 가능하다
이러면 디버깅 코드의 재사용성이 향상된다, 하지만 아직 코드를 복잡하게 만드는건 마찬가지다
logger
또 다른 방법으로는 logger를 사용하는 방식이다
log란 발생한 event들의 순차적 기록이며 타임 스탬프가 존재한다, 이러한 log를 생성하는 프로세스를 logging이라고 한다
다른 library를 사용하여 log를 logfile에 기록할 수 있고 나중에 확인이 가능하다
log file에는 장점이 존재한다
log file의 정보는 프로그램의 출력과 분리되어 있어 일반적인 console 출력과 섞이지 않는다, 또한 log file은 다른 프로그래머에게 쉽게 보내 디버깅도 가능하다
(버그 발생 시 log file 전달로 빠른 해결 가능, plog나 spdlog와 같은 third party library 사용 필요)
일반적인 C++에서는 log 출력용 명령어인 std::clog << 가 존재한다 (cerr과 다르게 버퍼링이 존재, 단순 로그용)
Stepping Debugging
코드의 흐름을 단계별로 따라가며 디버깅할 수 있는 방식이다
F10으로 단계별 코드 흐름 진행이 가능하다

계속해서 F10으로 한줄 한줄 진행이 가능하다
이때 특정 값을 실시간으로 확인이 가능하다 (원하는 값에 커서 올리기, 값을 watch로 드래그하여 확인)

이렇게 watch에 띄워놓고 디버깅하면 실시간으로 값이 어떻게 변하는지 확인이 가능하다, 만약 범위를 벗어난 변수라면 더 이상 확인할 수 없다, 이때 다시 범위로 돌아온다면 watch에 값이 다시 표시된다
watch에서 간단한 수식도 적용이 가능하다

또한 &를 이용하여 해당 변수의 메모리 주소도 확인이 가능하다

함수 호출부에서 F11로 해당 함수 내부로 진입이 가능하다

F11로 함수 내부로 진입 후 함수 종료까지 진행하면 다시 함수 호출부분으로 돌아오게 된다 (함수 반환 주소값을 통한 복귀)
Ctrl + F10으로 원하는 코드 부분부터 디버깅 진행이 가능하다 (맨 위에서부터 진행되어 해당 코드에서 멈춘다)
breakpoint를 찍어 원하는 코드에서 프로그램 실행 중지(break)가 가능하다 (F9)

프로그램이 실행되어 돌다가 L17에서 멈추게 된다
이후에 마찬가지로 F5, F10, F11로 원하는대로 진행하면 된다
이러한 breakpoint는 해당 지점의 코드가 실행될 때 마다 break가 걸리게 된다
디버깅 상태에서 원하는 L에 Ctrl + Shift + F10을 통해 현재 break 시점부터 해당 L 이전 코드를 실행하지 않고 Jump가 가능하다

여기서 L17에 커서를 두고 Ctrl + Shift + F10을 하면 Test2 cout 명령이 실행되지 않기 때문에 console에는 Test1, Test3만 출력되게 된다
또한 BreakPoint에 Condition을 추가할 수 있다

추가한 Condition에 부합하면 break가 걸리고 그렇지 않다면 break가 걸리지 않는다
마지막으로 현재 디버깅중인 라인을 커서로 옮겨 실행 시점을 변경할 수 있다
Call Stack
Call Stack은 현재 디버깅 지점까지 호출된 모든 함수들의 목록이다, Call Stack에는 호출된 함수와 호출이 종료되었을 때 돌아갈 코드 line이 포함된다
void a()
{
std::cout << "a() called\n";
}
void b()
{
std::cout << "b() called\n";
a();
}
int main()
{
a();
b();
return 0;
}

a() 내부에 break point를 찍고 call stack을 확인해보면 main()에서 a()가 호출되었음을 알 수 있다
Register, Memory
특정 변수의 메모리 주소를 통해 Memory에 어떤 값이 들어있는지 확인이 가능하다 (Ctrl + D + Y)

그리고 Ctrl + Alt + D로 Reigster에서 Assembly단계에서의 코드 흐름 확인도 가능하다

어떻게 하면 오류가 적은 프로그래밍을 할 수 있을까?
첫번째, 새로운 기능들이 많이 추가되다 보면 함수나 코드의 길이가 굉장히 길어질 수 있다, 이때 함수가 길어지면 복잡하고 이해하기 힘들어질 수 있다
하나의 함수에 너무 많은 기능이 들어가게 되었을 때 이를 리팩토링하는 방식으로 오류를 방지할 수 있다
리팩토링이란 동작은 변경하지 않고 코드 구조만 변경하는 프로세스를 의미한다
(모듈성 향상)
두번째, 방어 프로그래밍을 하는 습관을 들이자
내가 예상한 방식보다 더 다양한 방식으로 유저들은 프로그램을 사용한다, 예를들어 정수만 입력하길 바라지만 유저들은 문자열, 문자, 기호등을 입력하려고 한다 이러한 행동 자체를 방지하는 방어적 프로그래밍을 하자
(습관적인 null check는 필수이다)
이외에도 sementic error를 감지하는 static analysis tools를 사용하는것도 방법이다
