지금까지 몇 가지 개념에서 C++ 함수를 사용해 보고 직접 작성해 봤어요. 이제는 좀 더 기술적인 부분을 살펴볼 차례예요. 아래 코드 조각은 가장 흔히 쓰이는 용어들을 쉽게 참고할 수 있도록 보여줘요. C++은 공백을 무시하기 때문에, 각 요소를 한 줄에 하나씩 배치하도록 서식을 바꿨어요.
// Function declaration:
bool // Return type
admin_detected(string user, string password) // Type signature
; // Don't forget the ';' for the declaration
// Function definition:
bool // Return type
admin_detected // Function name
(string user, string password) // Parameter list
{ return user == "admin" && password == "1234"; } // Function body
선언은 컴파일러에게 그런 이름과 반환 타입, 매개변수 목록을 가진 함수가 있다는 메모와 같은 역할을 해요. 정의가 빠져 있으면 코드는 동작하지 않아요. 선언은 필수가 아니지만, 정의보다 먼저 함수를 사용할 때 필요해요. 선언은 순환 참조 같은 문제를 해결할 수 있고, 인터페이스와 구현을 분리하는 데에도 사용할 수 있어요.
const 한정자값이 한 번 초기화된 뒤에는 바뀌지 않도록 보장하고 싶을 때가 있어요.
C++은 상수를 나타내는 한정자로 const 키워드를 사용해요.
const int number_of_dragon_balls{7};
number_of_dragon_balls--; // compilation error
상수를 _UPPER_SNAKE_CASE_로 작성하는 경우를 자주 보게 될 거예요. 다른 규칙이 없다면, 이 표기법은 매크로에 남겨 두는 것이 좋아요.
상수 변수를 설정한 뒤에 바꾸려고 하면 코드가 컴파일되지 않아요. 이것은 의도치 않은 변경을 막아 주고, 컴파일러에게는 최적화할 여지를 열어 줘요. 사람 입장에서도 어떤 부분이 영향을 받지 않는다는 걸 알면 코드를 더 쉽게 이해할 수 있어요.
함수 매개변수의 한정자로도 const를 붙일 수 있어요.
string guess_number(const int& secret, const int& guess) {
if (secret < guess) return "lower.";
if (secret > guess) return "higher.";
return "exact!";
}
함수에 const 참조를 전달하면 그 값이 바뀌지 않는다는 것을 확신할 수 있어요.
복사 비용이 큰 객체, 예를 들어 긴 문자열에는 const 참조를 자주 사용해요.
const 한정자의 세 번째 용도는 클래스의 인스턴스를 변경하지 않는 멤버 함수예요.
class Stubborn {
public:
Stubborn(string reply) {
response = reply;
}
string answer(const string& question) const {
if (question.length() == 0) { return ""; }
return response;
}
private:
string response{};
};
Stubborn의 멤버 함수 answer는 매개변수로 const string& 참조를 사용해요.
이렇게 하면 함수에 전달된 원본 객체를 복사하는 작업을 피할 수 있어요.
매개변수 목록만 다르다면 여러 함수가 같은 이름을 가질 수 있어요. 이것을 함수 오버로딩이라고 하고, 보통 이런 함수들이 아주 비슷한 작업을 할 때 사용해요.
반환 타입을 제외한 함수 헤더를 함수의 __타입 시그니처__라고 해요. 타입 시그니처가 달라지면 새로운 함수가 돼요.
play_sound 예제에는 여러 상황에 대응하기 위한 여섯 가지 오버로드가 있어요:
// different argument types:
void play_sound(char note); // C, D, E, ..., B
void play_sound(string solfege); // do, re, mi, ..., ti
void play_sound(int jianpu); // 1, 2, 3, ..., 7
// different number of arguments:
void play_sound(string solfege, double duration);
// different qualifiers:
void play_sound(vector<string>& solfege);
void play_sound(const vector<string>& solfege);
타입 시그니처는 함수의 이름, 매개변수의 개수, 각 매개변수의 타입과 한정자로 정의돼요(이름은 포함되지 않아요). 반환 타입은 타입 시그니처에 명시적으로 포함되지 않아요. 그래서 반환 타입만 다른 두 함수를 만들면 컴파일 오류가 발생해요. 컴파일러는 둘 중 어떤 것을 사용해야 하는지 분명하지 않다며 문제를 제기해요.
어떤 함수는 매개변수가 아주 많아질 수 있고, 호출할 때 대부분의 매개변수에 같은 값을 사용하는 경우가 많아요. 이런 호출에서 반복되는 부분은 기본 인자로 피할 수 있어요.
void record_new_horse_birth(string name, int weight, string color="brown-ish", string dam="Alruccaba", string sire="Poseidon");
record_new_horse_birth("Urban Sea", 130); // color will be brown, dam "Alruccabam", sire "Poseidon"
record_new_horse_birth("Highclere", 175, "off-white", "Fall Aspen"); // sire will be "Poseidon"
함수 선언은 정의보다 먼저 읽히는 경우가 많아서, 기본 인자를 설정하기에 더 좋은 위치예요. 어떤 매개변수에 기본값이 선언되어 있으면, 그 오른쪽에 있는 모든 매개변수에도 기본값이 선언되어야 해요. 때로는 복잡한 함수 오버로드를 기본 인자를 사용한 더 적은 수의 함수로 리팩터링하면 유지보수성이 좋아져요.
이 연습 문제에서는 좋아하는 요리책에 나오는 근사한 라자냐를 준비하고 요리하는 것과 관련된 코드를 좀 더 작성해 볼 거예요.
과제는 다섯 개예요. 첫 번째는 요리 자체에 관한 것이고, 나머지 네 개는 완벽한 준비에 관한 것이에요.
다음에 만들 라자냐를 위해, 요리를 즐길 수 있도록 충분한 시간을 확보해 두고 싶어요. 라자냐에 어떤 층을 넣을지는 이미 계획해 두었어요. 이제 그것을 바탕으로 준비에 얼마나 시간이 걸릴지 예측해 보려고 해요.
층 벡터를 string으로, 층당 평균 준비 시간을 분 단위 int로 받는 preparationTime 함수를 구현해 보세요.
이 함수는 층의 개수를 기준으로 전체 준비 시간의 예측값을 int로 반환해야 해요.
평균 준비 시간을 전달하지 않으면 기본값 2를 사용해요.
std::vector<std::string> layers{"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"};
preparationTime(layers, 3)
// => 18
preparationTime(layers)
// => 12
시간을 확보하는 것 외에도, 꿈에 그리던 라자냐를 만들 수 있도록 소스와 면이 충분한지 확인하고 싶어요. 라자냐의 면 층마다 50그램의 면이 필요해요. 라자냐의 소스 층마다 0.2리터의 소스가 필요해요.
층 벡터를 string 매개변수로 받는 quantities 함수를 정의해 보세요.
그러면 이 함수가 요리를 만드는 데 필요한 면과 소스의 양을 계산해요.
결과는 이미 정의되어 있는 amount _구조체_로 반환해야 해요.
amount에는 멤버 변수가 두 개 있는데, 하나는 noodles의 무게를 나타내는 int이고 다른 하나는 sauce의 부피를 나타내는 double이에요.
struct amount { int noodles; double sauce; };
amount needed{quantities({"sauce", "noodles", "sauce", "meat", "mozzarella", "noodles"})};
// needed.noodles => 100
// needed.sauce => 0.4
얼마 전에 친구를 방문해서 그 집에서 라자냐를 먹었어요. 정말 훌륭했고 뭔가 특별한 맛이 났어요. 친구가 재료 목록을 보내주면서, 목록의 마지막 항목이 그 요리를 그렇게 특별하게 만든 "비밀 재료"라고 알려줬어요. 이제 그 비밀 재료를 내 레시피에도 추가하고 싶어요.
두 개의 vector of strings를 매개변수로 받는 addSecretIngredient 함수를 작성해 보세요.
첫 번째 매개변수는 내 레시피의 재료들에 대한 _참조_이고, 두 번째 _문자열 벡터_에는 친구가 보내준 재료들이 담겨 있어요.
내 재료 목록의 마지막 요소는 항상 "?"라는 문자열이에요.
이 함수는 그것을 친구 목록의 마지막 항목으로 바꿔야 해요.
참고: addSecretIngredient는 아무것도 반환하지 않아요. 내 재료 목록을 직접 수정해야 해요.
친구의 재료 목록도 (성능을 높이기 위해) 참조로 받을 수 있지만, 수정되면 안 돼요.
const 참조를 사용해서 이 목록이 수정되지 않도록 확실히 해 두세요.
std::vector<std::string> friendsList{"noodles", "sauce", "mozzarella", "kampot pepper"};
std::vector<std::string> myList{"noodles", "meat", "sauce", "mozzarella", "?"};
addSecretIngredient(myList, friendsList);
// myList => {"noodles", "meat", "sauce", "mozzarella", "kampot pepper"}
요리책에 적힌 양은 라자냐 두 인분 분량밖에 나오지 않아요. 다음에는 더 많은 사람을 위해 요리하고 싶으니, 인분 수에 따라 필요한 양을 계산해 보고 싶어요.
두 개의 매개변수를 받는 scaleRecipe 함수를 구현해 보세요.
double 벡터.이 함수는 원하는 인분 수에 필요한 양을 담은 새로운 벡터를 반환해야 해요.
다만 원래 레시피는 그대로 유지하고 싶어요.
즉, 이 함수에서는 quantities 인자가 수정되어서는 안 돼요.
std::vector<double> quantities{1.2, 3.6, 10.5};
scaleRecipe(quantities, 4);
// => { 2.4, 7.2, 21 }
어린 시절의 이야기들이 떠올라요. 가문의 가장 깊은 요리 비법을 지키는 분, 증조고모님에 관한 이야기 말이에요. 전설에 따르면 그분은 상상 그 이상의 풍미를 열어줄 열쇠를 쥐고 있다고 해요. 하지만 그분의 방식은 독특해요. 그분은 목록이나 계량 따위는 다루지 않고, 속삭이듯 한 번에 재료를 하나씩만 알려줘요.
설렘과 두려움이 뒤섞인 마음으로, 증조고모님께 연락해 보기로 해요. 예상대로, 그분은 수수께끼 같은 방식으로 답하며 신비에 싸인 비밀 재료 하나를 건네줘요.
증조고모님의 선물은 복잡함을 한 층 더해 주는데, 그걸 레시피에 활용하고 싶어요.
두 개의 인자를 받는 addSecretIngredient 함수의 또 다른 버전을 구현해 보세요. 하나는 내 재료 목록을 나타내는 벡터에 대한 참조이고, 다른 하나는 증조고모님의 수수께끼 같은 선물을 나타내는 문자열 하나예요.
std::vector<std::string> myList{"noodles", "meat", "sauce", "mozzarella", "?"};
std::string secretIngredient{"spice melange"};
addSecretIngredient(myList, secretIngredient);
// myList => {"noodles", "meat", "sauce", "mozzarella", "spice melange"}
행운을 빌어요, 용감한 요리사!
Exercism에 가입하고 C++ 트랙을 개념 19개연습 문제 100개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.