트랙
/
C++
C++
/
연습 문제
/
스피디왜건 재단
스피디왜건 재단

스피디왜건 재단

학습 연습 문제

소개

다른 많은 언어와 마찬가지로 C++에도 _포인터_가 있어요. _참조_와 _포인터_가 비슷하다는 것은 이미 알고 있겠지만, 이 둘은 컴퓨터 내부 동작에 한층 더 가까이 다가간 개념이라고 생각하면 돼요. _포인터_는 객체의 주소를 담는 변수예요. 포인터를 사용하면 객체와 직접 상호작용할 수 있어요. 덕분에 C++에서 동적 메모리 할당과 효율적인 데이터 조작이 가능해져요.

_포인터_가 처음이라면 조금 신비롭게 느껴질 수도 있지만, 한번 익숙해지고 나면 꽤 단순해요.

포인터는 C++에서 아주 중요한 부분이니, 시간을 들여 제대로 이해해 봐요. 이 개념에서 다루는 가장 기본적인 형태는 멍청한 포인터 또는 _생 포인터_라고도 불러요. 최신 C++에는 _스마트 포인터_도 있어요. 하지만 기본형은 전혀 똑똑하지 않아서 모든 작업을 직접 처리해야 해요.

세부 사항을 파고들기 전에, _포인터_의 쓰임새를 이해해 두면 좋아요. _포인터_는 객체의 주소를 프로그램의 다른 부분과 공유하는 방법이에요. 이는 크게 두 가지 이유에서 유용해요:

  1. _참조_와 마찬가지로, 포인터는 복사를 피하고 프로그램의 리소스 사용량을 줄이는 데 도움이 돼요.
  2. _참조_와 달리, 포인터는 다른 객체를 가리키도록 다시 할당할 수 있어요.
  3. 포인터는 널 값을 가리킬 수도 있어요. 이는 현재 어떤 객체도 가리키지 않고 있다는 것을 나타내요.

일반적인 문법

C++에서 포인터를 선언할 때는 포인터가 가리킬 데이터 타입을 먼저 쓰고, 그 뒤에 별표(*)와 포인터의 이름을 붙여요. 포인터는 선언할 때 자동으로 초기화되지 않아요. 명시적으로 값을 대입하지 않으면 포인터는 보통 불확정한 값을 담고 있는데, 이를 흔히 "가비지 주소"라고 불러요. 일부 컴파일러는 포인터를 nullptr로 초기화하기도 하지만, 모든 컴파일러에서 그런 동작이 보장되는 것은 아니니 의존해서는 안 돼요. 잠재적인 문제를 피하려면 생 포인터를 명시적으로 초기화하고, 사용하기 전에 널이 아닌지 확인하는 것이 좋아요.

int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid

변수의 주소를 포인터에 대입하려면 주소 연산자(&)를 사용해요. 포인터를 역참조할 때는 간접 참조 연산자(*)를 사용해요.

std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};

한 가지 주의할 점이 있어요. 역참조는 반드시 명시적으로 해야 하지만, _참조_는 별칭처럼 그냥 동작해요.

포인터 산술

_포인터 산술_을 사용하면 포인터에 산술 연산을 할 수 있어요. 특히 배열을 다룰 때 유용해요. 포인터에 정수를 더하면 다른 원소를 가리키게 돼요.

// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
Caution

C++에서 포인터 산술은 주의해서 다루지 않으면 __정의되지 않은 동작__으로 이어지기 쉬워요. 정의되지 않은 동작은 예상치 못한 프로그램 결과, 충돌, 심지어 보안 취약점으로 나타날 수 있어요. 정의되지 않은 동작이 초래한 결과 중 악명 높은 예로, 1996년 Ariane 5 로켓의 폭발이 있어요. 당시 64비트 부동소수점 수를 16비트 부호 있는 정수로 변환하면서 발생한 소프트웨어 예외가 치명적인 실패로 이어졌어요.

멤버 변수에 접근하기

C++에서 -> 연산자는 객체를 가리키는 포인터를 통해 그 객체의 멤버에 접근할 때 사용해요. 포인터가 가리키는 객체의 멤버에 접근하는 일을 간단하게 만들어 주는 축약형이에요. 예를 들어 ptr이 멤버 변수 x를 가진 객체를 가리키는 포인터라면, (*ptr).x 대신 ptr->x를 바로 쓸 수 있어요. 이 연산자는 객체를 가리키는 포인터를 다룰 때 코드의 가독성을 높이고 장황함을 줄여 줘요.

다음은 멤버 변수 superpower를 가진 구조체 Superhero에 대한 간단한 예제예요. main 함수는 Superhero 객체(원더 우먼을 나타내요)를 가리키는 포인터 dianaPrince를 만들어요. -> 연산자로 멤버 변수 superpower에 접근하는데, 여기에는 원더 우먼의 상징적인 "진실의 올가미"가 담겨 있어요.

struct Superhero {
    std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;

포인터와 참조

포인터와 참조는 둘 다 객체에 간접적으로 접근할 수 있게 해 주지만, 할 수 있는 일과 안전성 측면에서 차이가 있어요. 포인터는 가리키는 대상을 바꿀 수 있는 유연성을 제공하고, 널을 대입할 수도 있어요. 하지만 이런 유연성은 널 포인터를 역참조하거나 댕글링 포인터를 만드는 등의 위험을 불러와요. 반면 참조는 널이 될 수 없고, 만들어질 때 유효한 객체에 묶이기 때문에 이런 위험을 피할 수 있어요. 안전성이 더 높기 때문에, 포인터가 제공하는 추가 기능이 꼭 필요한 경우가 아니라면 참조를 포인터보다 선호하는 것이 좋아요.

지침

엔지니어, 환영해요! Speedywagon Foundation의 마지막 베테랑 중 한 명이에요. 이 비밀 조직은 수십 년 동안 Pillar Men 같은 고대의 위협과 싸워 왔어요. 그 과정에서 최첨단 기술과 오래된 라이브러리를 섞어 만든 재단의 기술 시스템을 수년간 유지보수해 왔어요.

그런데 최근에는 Pillar Men의 활동을 추적하는 센서들이 오작동하고 있어요. 재단의 시스템은 낡았고, 이 코드는 업데이트할 수 없는 레거시 C++ 라이브러리와 맞물려 돌아가요. 임무는 구식 포인터 기반 라이브러리를 사용해 Pillar Men 센서 활동을 감시하는 네 가지 핵심 함수를 구현하는 거예요.

현대적인 C++ 엔지니어라면 스마트 포인터를 쓰는 편이 낫겠지만, 아쉽게도 레거시 코드는 옛 방식을 존중하라고 요구해요. 인류의 운명이 이 포인터들에 달려 있을지도 모르니, 신중하게 진행해요. Hamon 에너지가 함께하기를 바라요.

Note

센서 판독값은 아주 클 수 있어서, 실제 라이브러리에서 사용하는 목업 _struct_를 제공해요. 코드는 이미 헤더 파일에 구현해 두었어요.

struct pillar_men_sensor {
    int activity{};
    std::string location{};
    std::vector<int> data{};
};

1. 센서 연결 확인 (connection_check)

첫 번째 임무는 Pillar Men 센서가 제대로 연결되어 있는지 확인하는 거예요. 연결이 끊긴 센서 때문에 잘못된 경보가 울리면 안 되니까요. connection_check 함수를 작성해요. 이 함수는 센서의 포인터가 유효한지 nullptr인지 확인해서 검사해요.

  • pillar_men_sensor _struct_의 포인터를 받는 함수를 정의해요.
  • 센서 포인터가 null이 아니면 true를, 그렇지 않으면 false를 반환해야 해요.
pillar_men_sensor* sensor{nullptr};
bool isConnected = connection_check(sensor);
// isConnected => false

2. 센서 활동 집계 (activity_counter)

Pillar Men이 그림자 속에 숨어 있으니, 센서가 어떤 활동이든 감지했는지 알아야 해요. activity_counter 함수를 작성해요. 이 함수는 센서 배열과, 배열 안 센서의 개수를 나타내는 capacity를 받아요.

  • 배열의 첫 번째 원소에 대한 포인터와 배열의 capacity를 받는 함수를 정의해요.
  • 포인터 연산을 사용해 센서 배열을 순회하면서 활동 판독값을 누적해요.
  • 누적된 활동 값을 반환해요.
pillar_men_sensor sensor_array[3] = {{0}, {101}, {22}};
int totalActivity = activity_counter(sensor_array, 3);
// totalActivity => 123

3. 경보 제어 (alarm_control)

진짜 위험이 있을 때가 아니면 아무 센서나 경보를 울려서는 안 돼요. alarm_control 함수는 센서의 활동 수준이 0보다 클 때만 경보를 울리도록 해요. 이 함수는 시스템 충돌을 막기 위해 null 센서인지도 확인해야 해요.

  • pillar_men_sensor에 대한 포인터를 받는 함수를 정의해요.
  • 함수는 먼저 센서가 nullptr인지 확인해요. 센서가 nullptr이면 false를 반환해요.
  • 센서가 유효하고 활동 값이 0보다 크면 true를, 그렇지 않으면 false를 반환해요.
pillar_men_sensor db{9008, "songokunoie", {7, 7, 7}};
bool alarm = alarm_control(&db);
// alarm => true

4. uv_alarm 함수로 데이터의 이상 여부 확인하기

이번 과제에서는 자외선 노출 수준과 센서 활동을 바탕으로 경보를 울려야 할지 판단하는 uv_alarm 함수를 구현해요. uv_alarm 함수는 제공된 uv_light_heuristic 함수를 사용해야 해요. 이 함수는 데이터 벡터를 대상으로 동작하고, 특정 임곗값을 기준으로 값을 반환해요. 이건 실제 운영 환경에서 실행될 복잡한 코드의 목업 버전이니, 인터페이스는 바꾸지 말아 주세요.

speedywagon 네임스페이스에 uv_alarm 함수를 정의해요. 함수는 다음을 따라야 해요:

  • pillar_men_sensor _struct_에 대한 포인터를 매개변수로 받아요.
  • 센서 포인터가 null이면 false를 반환해요.
  • 센서의 data 배열의 주소를 넘겨서 uv_light_heuristic 함수를 호출해요.
  • uv_light_heuristic가 반환한 값이 sensor->activity 수준보다 크면 true를, 그렇지 않으면 false를 반환해요.
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
C++ Exercism

스피디왜건 재단 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 C++ 트랙을 개념 19개연습 문제 100개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.