이번 편의 목표: 컴파일러가 의미를 보장하지 않는 코드를 구분하고, 소유권이 드러나는 마지막 형태로 가격 합계를 계산합니다.
정의되지 않은 동작은 그 프로그램을 실행했을 때 어떤 일이 일어나도 표준이 책임을 지지 않는 지점입니다. 지금 컴퓨터에서 우연히 원하는 숫자가 나와도 올바른 프로그램이 아닙니다. 최적화기는 그 일이 일어나지 않는다고 가정하고 주변 코드까지 바꿉니다.
대표적인 경우는 다음입니다. 초기화하지 않은 지역 변수를 읽습니다. 배열 범위 밖을 읽거나 씁니다. free한 메모리와 수명이 끝난 객체를 사용합니다. 같은 주소를 두 번 free합니다. 부호 있는 정수의 연산 결과가 그 타입에 들어가지 않습니다. 가상 소멸자가 없는 부모 포인터로 자식을 삭제합니다. 지역 변수의 주소를 반환해 호출자가 그 주소를 사용합니다.
이 코드들은 예제로 실행하지 않습니다. 경고와 검사 도구로 걸리게 만드는 쪽이 연습입니다.
gcc -std=c17 -Wall -Wextra -fsanitize=address,undefined -o hello hello.c
C++도 같은 검사 옵션을 g++에 붙일 수 있습니다. 주소 검사와 정의되지 않은 동작 검사는 범위 밖 접근, 해제 뒤 사용, 부호 있는 넘침 같은 실수를 실행 중에 보고합니다. 검사가 통과했다는 사실은 그 입력에 한정됩니다. 없는 실수를 증명하지는 않습니다.
고수로 가는 습관은 따로 있습니다. 경고를 경고인 채로 남기지 않습니다. 소유권은 unique_ptr, vector, string처럼 타입에 드러냅니다. 바꾸지 않는 참조와 멤버 함수에는 const를 붙입니다. 한 함수는 한 가지 일을 합니다. 자원을 빌리면 그 자원을 닫는 주체가 코드에서 보여야 합니다.
아래 프로그램은 그 습관으로 되돌아온 형태입니다. 동적 메모리 함수를 직접 호출하지 않고, 책 두 권의 가격 합을 구합니다.
#include <iostream>
#include <string>
#include <vector>
struct Book {
std::string title;
int price;
};
int total_price(const std::vector<Book> &books) {
int sum = 0;
for (const Book &book : books) {
sum += book.price;
}
return sum;
}
constexpr int paper_fee(int days) {
return days * 100;
}
int main() {
std::vector<Book> books;
books.push_back(Book{"개미", 12000});
books.push_back(Book{"공책", 1800});
constexpr int fee = paper_fee(3);
std::cout << total_price(books) << '\n';
std::cout << fee << '\n';
return 0;
}가격 합은 13800입니다. paper_fee는 constexpr이라 인자가 컴파일 때 정해지면 결과도 컴파일 때 계산됩니다. 3일 요금은 300입니다.
기초의 int와 포인터는 사라지지 않았습니다. vector 원소도 메모리에 나란히 있고, 참조는 그 원소의 별명입니다. 달라진 점은 길이, 복사, 해제, 없을 수 있는 소유권을 타입이 대신 말한다는 것입니다. 그 문장을 코드에서 읽을 수 있으면 이 연재가 목표로 한 자리까지 온 것입니다.
직접 해 보기
책 한 권을 더 넣어 합계를 다시 계산하세요. paper_fee의 일수를 0과 5로 바꿔 상수 결과가 바뀌는지도 확인하세요.
해설
세 권의 가격을 더합니다. 일수 0의 요금은 0이고, 일수 5의 요금은 500입니다. constexpr 함수라도 실행 중에 정해지는 값을 넘기면 그 호출은 실행 때 계산됩니다.
COMMENTS
댓글 0
이 글에 대한 의견이나 질문을 남겨 주세요.
댓글은 구글이나 카카오로 시작한 뒤에 남길 수 있습니다.
등록된 댓글이 없습니다.