<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>dj_trace25.log</title>
        <link>https://velog.io/</link>
        <description>안녕하세요.</description>
        <lastBuildDate>Fri, 27 Mar 2026 10:45:17 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>dj_trace25.log</title>
            <url>https://velog.velcdn.com/images/dj_trace25/profile/49170904-4161-4049-a52c-8332534af558/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. dj_trace25.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/dj_trace25" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[2026 개정 1급 - 1편 : 소방관계법령]]></title>
            <link>https://velog.io/@dj_trace25/2026-%EA%B0%9C%EC%A0%95-1%EA%B8%89-1%ED%8E%B8-%EC%86%8C%EB%B0%A9%EA%B4%80%EA%B3%84%EB%B2%95%EB%A0%B9</link>
            <guid>https://velog.io/@dj_trace25/2026-%EA%B0%9C%EC%A0%95-1%EA%B8%89-1%ED%8E%B8-%EC%86%8C%EB%B0%A9%EA%B4%80%EA%B3%84%EB%B2%95%EB%A0%B9</guid>
            <pubDate>Fri, 27 Mar 2026 10:45:17 GMT</pubDate>
            <description><![CDATA[<p>소방 관계 법령 안에 총 7가지의 법령이 있음.</p>
<ol>
<li>소방기본법</li>
<li>화재 예방 및 안전관리에 관한 법률 = 화재 예방법</li>
<li>소방 시설 설치 및 관리에 관한 법률 = 소방 시설법</li>
</ol>
<hr>
<ol start="4">
<li>다중이용업소법</li>
<li>초고층재난관리법</li>
<li>재난안전법</li>
<li>위험물안전관리법</li>
</ol>
<hr>
<p>이 중 1,2,3번이 가장 중요하고 출제 비중이 높음
사유는 이 3가지가 소방안전관리자 과정에서 가장 유구한 역사를 가지고 있기 때문</p>
<hr>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/053ae8a3-7e0d-4749-9992-c844d04f6cb0/image.png" alt=""></p>
<p>소방기본법 &gt; 예경진구조구급,공공의 안녕 및 질서 유지</p>
<p>화재 예방법과 소방시설법 &gt; 원래 하나였다가 둘로 쪼개진법이기때문에 주목적이 같다. 공공의 안전과 복리증진 이바지</p>
<hr>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/d1174394-5856-4218-9179-650e4ac4dcb5/image.png" alt=""></p>
<p>특정 소방 대상물은 용도를 파악해서 아예 건물안에 소방 시설을 설치해야하는 소방 대상물로 보는게 특정 소방 대상물</p>
<p>그리고 소방시설은 이 특정소방대상물에 집어넣는 소방 설비</p>
<p>소방시설등은 소방시설에 포함 안되는 기타 등등은 소방시설등이라 뭉뚱그려 말함</p>
<hr>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/9e57a3f0-29f4-441e-9fdd-2677f25b4a46/image.png" alt=""></p>
<p>그리고 원칙상 소방안전관리자는 관계인엔 포함되지 않는다.</p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/31527883-f4b4-4e2a-9d3c-78ff97242032/image.png" alt=""></p>
<p>관계 지역은 가볍게 읽고</p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/8a7b6ecc-f911-4555-a4f5-9fa0cbad8c49/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/7e59cd8d-14be-47eb-94cf-3a373993b8d3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/9ec66fb1-fe8a-4927-9622-5fe7ddf054cc/image.png" alt="">
이 경우 둘다 피난층</p>
<hr>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/99f68e09-bcb8-41f7-9c9f-f5f246ec9cee/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/62a0f918-afb8-4501-834c-d07f25db7d7b/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/d47fd0d7-465a-4f9b-9078-a9e4f5fc6d21/image.png" alt=""></p>
<p>소방안전관리대상물은 소방 대상물 중에서도 특정 소방 대상물(소화 시설 보관해야하는 건축물)이 있고, 그 특정 소방 대상물중에서도 소방안전관리자를 선임해야하는 건축물이 있는데 그걸 소방안전관리대상물이라함. </p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/5be1abe1-ee09-410e-9b2e-89f7866ec92e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/2f3c7e12-7c4f-4c1d-8842-8df1eb68b312/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/c4ece3a2-e95a-4577-8504-575248a95a48/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/25d11d32-a192-42d0-8c46-ac362c4293fd/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 21]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-21</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-21</guid>
            <pubDate>Sat, 07 Mar 2026 09:27:46 GMT</pubDate>
            <description><![CDATA[<h2 id="211--연산자-오버로딩-소개">21.1 — 연산자 오버로딩 소개</h2>
<p>11.1 레슨에서 &#39;함수 오버로딩&#39;에 대해 배웠던 것 기억하시나요? 함수 오버로딩은 이름은 같지만 전달받는 재료(매개변수)가 다른 여러 개의 함수를 만들 수 있게 해주는 편리한 기능입니다. 덕분에 다루는 데이터의 종류가 달라도 매번 새로운 함수 이름을 지어낼 필요 없이 같은 이름의 함수를 사용할 수 있었죠.</p>
<p>C++에서는 <code>+</code>나 <code>-</code> 같은 &#39;연산자(operator)&#39;들도 사실은 내부적으로 함수처럼 작동합니다. 따라서 이 연산자 함수들에 함수 오버로딩 기능을 적용하면, 우리가 직접 만든 클래스 같은 새로운 데이터 타입에 맞춰 연산자의 작동 방식을 새롭게 정의할 수 있습니다. 이렇게 기존 연산자에 나만의 새로운 능력을 부여하는 것을 바로 <strong>&#39;연산자 오버로딩&#39;</strong>이라고 부릅니다.</p>
<p>이번 장에서는 이 연산자 오버로딩과 관련된 다양한 주제들을 하나씩 살펴보겠습니다.</p>
<hr>
<h3 id="함수로서의-연산자">함수로서의 연산자</h3>
<p>다음 예시를 한번 볼까요?</p>
<pre><code class="language-cpp">int x { 2 };
int y { 3 };
std::cout &lt;&lt; x + y &lt;&lt; &#39;\n&#39;;
</code></pre>
<p>컴파일러(코드를 컴퓨터 언어로 번역해 주는 프로그램)는 숫자(정수)들을 더할 수 있는 더하기 연산자(<code>+</code>)를 기본적으로 내장하고 있습니다. 이 연산자 함수는 <code>x</code>와 <code>y</code>라는 정수를 받아서 두 값을 더한 뒤, 그 결과인 정수를 돌려줍니다. 코드를 읽으실 때 <code>x + y</code>라는 수식을 보시면, 머릿속으로는 이것이 <code>operator+(x, y)</code>라는 함수를 호출하는 것과 똑같다고 생각하시면 이해하기 쉽습니다. (여기서 함수 이름이 <code>operator+</code>인 것이죠!)</p>
<p>자, 이제 아주 비슷한 다음 코드를 볼까요?</p>
<pre><code class="language-cpp">double z { 2.0 };
double w { 3.0 };
std::cout &lt;&lt; w + z &lt;&lt; &#39;\n&#39;;
</code></pre>
<p>컴파일러는 소수점 숫자(<code>double</code>)를 계산할 수 있는 더하기 연산자(<code>+</code>) 기능도 이미 가지고 있습니다. <code>w + z</code>라는 코드는 <code>operator+(w, z)</code>라는 함수 호출로 바뀝니다. 이때 컴파일러는 똑똑하게도 &#39;함수 오버로딩&#39; 기능을 사용해서, 정수용 더하기 함수가 아니라 소수점용 더하기 함수를 불러와야 한다는 것을 알아냅니다.</p>
<p>그렇다면 우리가 직접 만든 클래스(객체) 두 개를 더하려고 하면 어떤 일이 일어날까요?</p>
<pre><code class="language-cpp">Mystring string1 { &quot;Hello, &quot; };
Mystring string2 { &quot;World!&quot; };
std::cout &lt;&lt; string1 + string2 &lt;&lt; &#39;\n&#39;;
</code></pre>
<p>이 경우 어떤 결과가 나올까요? 직관적으로는 &quot;Hello, World!&quot;라는 문장이 화면에 예쁘게 출력될 것이라고 기대하게 됩니다. 하지만 <code>Mystring</code>은 우리가 새롭게 만들어낸 타입이기 때문에, 컴파일러는 <code>Mystring</code>끼리 더할 때 쓸 수 있는 <code>+</code> 연산자가 무엇인지 전혀 알지 못합니다. 그래서 이 코드를 실행하면 에러가 발생하게 되죠. 우리가 원하는 대로 코드가 작동하게 만들려면, 컴파일러에게 <code>Mystring</code> 두 개를 더할 때 <code>+</code> 연산자가 어떻게 행동해야 하는지 알려주는 &#39;오버로딩된 함수&#39;를 우리가 직접 만들어 주어야 합니다. 다음 레슨에서 그 방법을 자세히 알아보겠습니다.</p>
<hr>
<h3 id="오버로딩된-연산자-해결하기-어떤-연산자를-쓸지-결정하기">오버로딩된 연산자 해결하기 (어떤 연산자를 쓸지 결정하기)</h3>
<p>연산자가 포함된 코드를 해석할 때, 컴파일러는 다음과 같은 규칙을 따릅니다.</p>
<ul>
<li>만약 연산하는 값들이 모두 <code>int</code>나 <code>double</code> 같은 <strong>C++ 기본 데이터 타입</strong>이라면, 컴파일러는 원래 가지고 있는 내장 기능을 사용합니다. 만약 내장된 기능이 없다면 에러를 발생시킵니다.</li>
<li>만약 연산하는 값 중 하나라도 우리가 <strong>직접 만든 타입</strong>(예: 직접 만든 클래스나 <code>enum</code> 타입)이라면, 컴파일러는 &#39;함수 오버로딩 해결 알고리즘&#39;을 사용해서 딱 맞는 오버로딩된 연산자가 있는지 찾아봅니다. 이 과정에서 연산자의 모양에 맞추기 위해 값의 타입을 컴파일러가 알아서(암시적으로) 변환할 수도 있습니다. 만약 내장된 연산자에 맞추기 위해 우리가 만든 타입을 기본 타입으로 자동 변환하는 과정이 일어날 수도 있습니다(이 부분은 나중에 다루겠습니다). 만약 딱 맞는 것을 찾지 못하거나, 어떤 것을 써야 할지 너무 애매하다면 컴파일러는 에러를 뿜어냅니다.</li>
</ul>
<hr>
<h3 id="연산자-오버로딩의-한계점-할-수-없는-것들">연산자 오버로딩의 한계점 (할 수 없는 것들)</h3>
<p>첫째, C++에 있는 거의 모든 연산자를 마음대로 오버로딩할 수 있습니다. 하지만 <strong>예외</strong>가 몇 가지 있습니다. 조건부 연산자(<code>?:</code>), 크기 확인(<code>sizeof</code>), 범위 지정(<code>::</code>), 멤버 선택(<code>.</code>), 포인터 멤버 선택(<code>.*</code>), 타입 확인(<code>typeid</code>), 그리고 캐스팅(형변환) 연산자들은 오버로딩을 할 수 없습니다.</p>
<p>둘째, <strong>이미 존재하는 연산자만</strong> 오버로딩할 수 있습니다. 새로운 기호를 만들어내거나 기존 기호의 이름을 바꿀 수는 없습니다. 예를 들어, 거듭제곱 계산을 하겠다고 <code>**</code>라는 완전히 새로운 연산자를 창조해 낼 수는 없습니다.</p>
<p>셋째, 오버로딩하려는 연산자의 양쪽 값 중 <strong>최소한 하나는 사용자가 직접 만든 타입</strong>이어야 합니다. 즉, <code>operator+(int, Mystring)</code>처럼 정수와 우리가 만든 문자열 클래스를 더하는 기능은 만들 수 있지만, <code>operator+(int, double)</code>처럼 C++ 기본 타입들끼리의 계산법을 마음대로 뜯어고칠 수는 없습니다.</p>
<p>참고로 C++ 표준 라이브러리(예: <code>std::string</code>)도 사용자가 만든 타입으로 간주됩니다. 그래서 이론적으로는 <code>operator+(double, std::string)</code> 같은 오버로딩도 가능은 합니다. 하지만 훗날 C++ 언어가 업데이트되면서 이런 연산자를 공식적으로 지원하게 되면 우리 프로그램이 망가질 수 있기 때문에, 이렇게 하는 것은 좋은 생각이 아닙니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
오버로딩된 연산자는 최소한 하나 이상의 프로그램 정의 타입(우리가 직접 만든 타입)과 함께 사용되어야 합니다. 그래야 미래에 C++ 표준이 업데이트되더라도 프로그램에 문제가 생기지 않습니다.</p>
</blockquote>
<p>넷째, 연산자가 원래 가지고 있는 <strong>피연산자(계산에 필요한 값)의 개수를 바꿀 수 없습니다.</strong>
(예를 들어, 값 두 개가 필요한 <code>+</code> 연산자를 값 하나만 받도록 바꿀 수 없습니다.)</p>
<p>마지막으로 아주 중요한 점입니다. 모든 연산자는 원래 가지고 있던 <strong>계산 우선순위와 결합 방향을 그대로 유지</strong>하며, 이는 절대 바꿀 수 없습니다.</p>
<p>초보 프로그래머들은 종종 비트 기호인 <code>^</code>를 &#39;거듭제곱&#39; 연산자로 오버로딩하려고 시도합니다. 하지만 C++에서 <code>^</code> 연산자는 <code>+</code>나 <code>-</code> 같은 기본 사칙연산보다 계산 우선순위가 훨씬 낮기 때문에 문제가 발생합니다.</p>
<p>일반적인 수학에서는 거듭제곱을 사칙연산보다 먼저 계산하므로, <code>4 + 3 ^ 2</code>는 <code>4 + (3의 2제곱)</code>이 되어 <code>4 + 9 =&gt; 13</code>이 됩니다.</p>
<p>하지만 C++에서는 <code>+</code> 연산자가 <code>^</code> 연산자보다 먼저 계산됩니다! 따라서 C++에서 <code>4 + 3 ^ 2</code>라고 쓰면 <code>(4 + 3)</code>이 먼저 계산되어 <code>7 ^ 2</code>가 되고, 결과는 어이없게도 <code>49</code>가 되어버립니다.</p>
<p>이걸 제대로 작동하게 하려면 매번 <code>4 + (3 ^ 2)</code>처럼 명시적으로 괄호를 쳐야 하는데, 이는 직관적이지도 않고 실수하기도 딱 좋습니다.</p>
<p>이러한 우선순위 문제 때문에, 연산자는 원래 의도된 쓰임새와 가장 비슷한 방식으로만 오버로딩하는 것이 좋습니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
연산자를 오버로딩할 때는, 해당 연산자의 원래 의도나 기능과 최대한 가깝게 만드는 것이 제일 좋습니다.</p>
</blockquote>
<p>게다가 연산자들은 이름이 기호로만 되어 있어서 무슨 역할을 하는지 한눈에 알기 어려울 때가 많습니다. 예를 들어, 문자열 클래스에서 <code>+</code> 연산자를 &#39;두 문자열 하나로 합치기&#39;로 쓰는 것은 아주 자연스럽습니다. 하지만 <code>-</code> 연산자는 어떨까요? 문자열에서 <code>-</code> 연산이 무슨 의미일지 딱 떠오르지 않습니다. 이럴 때는 코드를 읽는 사람이 헷갈리기 쉽습니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
오버로딩하려는 연산자의 의미가 직관적이고 명확하지 않다면, 무리해서 기호를 쓰지 말고 그냥 이해하기 쉬운 단어로 된 일반 함수를 새로 만드세요.</p>
</blockquote>
<p>마지막으로, 오버로딩된 연산자는 <strong>원래 연산자와 일관된 방식으로 값을 돌려주어야(반환해야)</strong> 합니다. <code>+</code>나 <code>-</code>처럼 원래의 값을 바꾸지 않는 연산자들은 계산된 &#39;새로운 결과값&#39; 자체를 반환해야 합니다. 반면에 원래의 값을 변경하는 연산자(예: <code>++</code> 증가 연산자나 대입 연산자)는 변경된 대상(왼쪽 값)을 &#39;참조(reference)&#39; 형태로 반환하는 것이 일반적입니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
원본 값을 변경하지 않는 연산자(예: 사칙연산)는 일반적으로 결과를 <strong>값(value)</strong>으로 반환해야 합니다.
왼쪽 원본 값을 변경하는 연산자(예: 전위 증감 연산자, 대입 연산자)는 일반적으로 왼쪽 원본 값을 <strong>참조(reference)</strong>로 반환해야 합니다.</p>
</blockquote>
<p>이런 몇 가지 규칙들만 잘 지킨다면, 여러분이 만든 클래스에 정말 다양하고 유용한 기능들을 마음껏 추가할 수 있습니다! 문자열 클래스에 <code>+</code>를 써서 글자들을 덧붙이거나, 분수 클래스 두 개를 더할 수도 있죠. <code>&lt;&lt;</code> 연산자를 오버로딩하면 우리가 만든 객체의 정보를 화면이나 파일에 아주 쉽게 출력할 수도 있습니다. <code>==</code> 연산자로 두 객체가 똑같은지 비교할 수도 있고요. 이처럼 연산자 오버로딩은 우리가 만든 클래스를 마치 C++의 기본 기능처럼 자연스럽고 편리하게 쓸 수 있게 해주기 때문에, C++에서 가장 강력하고 유용한 기능 중 하나로 꼽힙니다.</p>
<p>이어지는 다음 레슨들에서는 다양한 종류의 연산자들을 실제로 어떻게 오버로딩하는지 조금 더 깊이 파헤쳐 보겠습니다.</p>
<hr>
<h2 id="212--프렌드friend-함수를-사용한-산술-연산자-오버로딩">21.2 — 프렌드(friend) 함수를 사용한 산술 연산자 오버로딩</h2>
<p>C++에서 가장 자주 쓰이는 연산자 중 하나가 바로 <strong>산술 연산자</strong>입니다. 더하기(<code>+</code>), 빼기(<code>-</code>), 곱하기(<code>*</code>), 나누기(<code>/</code>)가 여기에 속하죠. 이 산술 연산자들은 모두 <strong>이항 연산자(binary operator)</strong>라는 공통점이 있습니다. 이항 연산자란 연산자를 기준으로 양쪽에 하나씩, 총 두 개의 값(피연산자)이 필요하다는 뜻이에요. 이 네 가지 연산자는 모두 완전히 똑같은 방식으로 오버로딩(재정의)할 수 있답니다.</p>
<p>연산자를 오버로딩하는 방법에는 크게 세 가지가 있습니다. &#39;멤버 함수&#39;를 쓰는 방법, &#39;프렌드(friend) 함수&#39;를 쓰는 방법, 그리고 &#39;일반 함수&#39;를 쓰는 방법이죠. </p>
<p>이번 레슨에서는 <strong>프렌드 함수를 사용하는 방법</strong>을 먼저 배워볼게요. 
(이항 연산자에서는 이 방법이 제일 직관적이고 이해하기 쉽거든요!) </p>
<p>다음 레슨에서는 일반 함수를 쓰는 법을 알아보고, 이번 챕터 후반부에서는 멤버 함수를 쓰는 법을 다룰 예정입니다. 물론 각각의 방법을 언제 써야 하는지도 나중에 자세히 정리해 드릴게요.</p>
<hr>
<h3 id="프렌드friend-함수로-연산자-오버로딩하기">프렌드(friend) 함수로 연산자 오버로딩하기</h3>
<p>먼저 아래의 클래스를 한번 살펴볼까요?</p>
<pre><code class="language-cpp">class Cents
{
private:
    int m_cents {};

public:
    Cents(int cents) : m_cents{ cents } { }
    int getCents() const { return m_cents; }
};
</code></pre>
<p>다음 예제는 두 개의 <code>Cents</code> 객체를 서로 더하기 위해 더하기(<code>+</code>) 연산자를 어떻게 오버로딩하는지 보여줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Cents
{
private:
    int m_cents {};

public:
    Cents(int cents) : m_cents{ cents } { }

    // 프렌드 함수를 사용하여 Cents + Cents 더하기
    friend Cents operator+(const Cents&amp; c1, const Cents&amp; c2);

    int getCents() const { return m_cents; }
};

// 참고: 이 함수는 멤버 함수가 아닙니다!
Cents operator+(const Cents&amp; c1, const Cents&amp; c2)
{
    // Cents 생성자와 기본 연산자 +(int, int)를 사용합니다
    // 프렌드 함수이기 때문에 m_cents에 직접 접근할 수 있습니다
    return c1.m_cents + c2.m_cents;
}

int main()
{
    Cents cents1{ 6 };
    Cents cents2{ 8 };
    Cents centsSum{ cents1 + cents2 };
    std::cout &lt;&lt; &quot;I have &quot; &lt;&lt; centsSum.getCents() &lt;&lt; &quot; cents.\n&quot;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같은 결과가 나옵니다.</p>
<pre><code>I have 14 cents.
</code></pre><p>더하기(<code>+</code>) 연산자를 오버로딩하는 건 생각보다 아주 간단해요! 
<code>operator+</code>라는 이름의 함수를 만들고, 우리가 더하고 싶은 두 개의 값을 매개변수로 넣어준 뒤, 적절한 반환 타입(결과값의 형태)을 정해서 함수 내용을 작성하기만 하면 끝이랍니다.</p>
<p>우리가 만든 <code>Cents</code> 객체의 경우, <code>operator+()</code> 함수를 만드는 과정은 정말 쉽습니다.
첫째, <strong>매개변수 타입</strong>입니다. 이번에 만들 <code>operator+</code>는 두 개의 <code>Cents</code> 객체를 더할 것이기 때문에, 함수도 두 개의 <code>Cents</code> 타입 객체를 받아야 합니다.
둘째, <strong>반환 타입</strong>입니다. <code>operator+</code>의 계산 결과 역시 새로운 <code>Cents</code> 객체가 되어야 하므로, 반환 타입도 <code>Cents</code>로 정해줍니다.</p>
<p>마지막으로 <strong>함수 구현</strong>입니다. 두 개의 <code>Cents</code> 객체를 더한다는 건, 결국 각 객체 안에 들어있는 <code>m_cents</code>라는 숫자(멤버 변수)끼리 더한다는 뜻이에요. 우리가 만든 오버로딩 함수는 클래스의 &#39;프렌드(friend)&#39;로 등록되어 있기 때문에, 객체 안의 숨겨진(private) <code>m_cents</code> 변수에 직접 접근할 수 있습니다. 그리고 <code>m_cents</code>는 단순한 정수형(int)이기 때문에, C++이 기본적으로 제공하는 덧셈 기능을 써서 그냥 <code>+</code> 기호로 더해주면 됩니다.</p>
<p>빼기(<code>-</code>) 연산자를 오버로딩하는 것도 똑같이 쉽습니다!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Cents
{
private:
    int m_cents {};

public:
    explicit Cents(int cents) : m_cents{ cents } { }

    // 프렌드 함수를 사용하여 Cents + Cents 더하기
    friend Cents operator+(const Cents&amp; c1, const Cents&amp; c2);

    // 프렌드 함수를 사용하여 Cents - Cents 빼기
    friend Cents operator-(const Cents&amp; c1, const Cents&amp; c2);

    int getCents() const { return m_cents; }
};

// 참고: 이 함수는 멤버 함수가 아닙니다!
Cents operator+(const Cents&amp; c1, const Cents&amp; c2)
{
    // Cents 생성자와 기본 연산자 +(int, int)를 사용합니다
    // 프렌드 함수이기 때문에 m_cents에 직접 접근할 수 있습니다
    return Cents { c1.m_cents + c2.m_cents };
}

// 참고: 이 함수는 멤버 함수가 아닙니다!
Cents operator-(const Cents&amp; c1, const Cents&amp; c2)
{
    // Cents 생성자와 기본 연산자 -(int, int)를 사용합니다
    // 프렌드 함수이기 때문에 m_cents에 직접 접근할 수 있습니다
    return Cents { c1.m_cents - c2.m_cents };
}

int main()
{
    Cents cents1{ 6 };
    Cents cents2{ 2 };
    Cents centsSum{ cents1 - cents2 };
    std::cout &lt;&lt; &quot;I have &quot; &lt;&lt; centsSum.getCents() &lt;&lt; &quot; cents.\n&quot;;

    return 0;
}
</code></pre>
<p>곱하기(<code>*</code>)나 나누기(<code>/</code>) 연산자 역시, 각각 <code>operator*</code>와 <code>operator/</code>라는 이름으로 함수를 정의해주기만 하면 똑같이 쉽게 오버로딩할 수 있습니다.</p>
<hr>
<h3 id="프렌드-함수를-클래스-내부에-정의하기">프렌드 함수를 클래스 내부에 정의하기</h3>
<p>프렌드 함수는 클래스의 멤버 함수가 아니지만, 원한다면 클래스 코드 블록 안쪽에 직접 정의(작성)할 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Cents
{
private:
    int m_cents {};

public:
    explicit Cents(int cents) : m_cents{ cents } { }

    // 프렌드 함수를 사용하여 Cents + Cents 더하기
    // 정의가 클래스 내부에 있더라도, 이 함수는 클래스의 멤버로 간주되지 않습니다.
    friend Cents operator+(const Cents&amp; c1, const Cents&amp; c2)
    {
        // Cents 생성자와 기본 연산자 +(int, int)를 사용합니다
        // 프렌드 함수이기 때문에 m_cents에 직접 접근할 수 있습니다
        return Cents { c1.m_cents + c2.m_cents };
    }

    int getCents() const { return m_cents; }
};

int main()
{
    Cents cents1{ 6 };
    Cents cents2{ 8 };
    Cents centsSum{ cents1 + cents2 };
    std::cout &lt;&lt; &quot;I have &quot; &lt;&lt; centsSum.getCents() &lt;&lt; &quot; cents.\n&quot;;

    return 0;
}
</code></pre>
<p>구현 내용이 아주 짧고 단순한 오버로딩 연산자라면 이렇게 클래스 안에 바로 작성하는 것도 괜찮은 방법입니다.</p>
<hr>
<h3 id="서로-다른-타입의-피연산자를-위한-연산자-오버로딩">서로 다른 타입의 피연산자를 위한 연산자 오버로딩</h3>
<p>연산자를 만들다 보면, 서로 <strong>다른 타입</strong>의 값을 더하거나 빼고 싶을 때가 자주 생깁니다. 
예를 들어, <code>Cents(4)</code>라는 객체에 숫자 <code>6</code>(정수형)을 더해서 <code>Cents(10)</code>이라는 결과를 만들고 싶은 경우처럼 말이죠.</p>
<p>C++이 <code>x + y</code>라는 식을 계산할 때, 앞의 <code>x</code>는 첫 번째 매개변수가 되고 뒤의 <code>y</code>는 두 번째 매개변수가 됩니다. 만약 <code>x</code>와 <code>y</code>가 같은 타입이라면 <code>x + y</code>를 하든 <code>y + x</code>를 하든 상관이 없습니다. 둘 다 똑같은 버전의 <code>operator+</code> 함수를 불러오니까요. 하지만 <strong>서로 다른 타입</strong>이라면 이야기가 달라집니다. <code>x + y</code>를 할 때와 <code>y + x</code>를 할 때 호출되는 함수가 서로 다릅니다.</p>
<p>예를 들어, <code>Cents(4) + 6</code>은 <code>operator+(Cents, int)</code> 함수를 호출하지만, 반대로 <code>6 + Cents(4)</code>는 <code>operator+(int, Cents)</code> 함수를 호출합니다. 결과적으로 서로 다른 타입의 값을 더하는 이항 연산자를 만들고 싶다면, <strong>순서가 바뀌는 두 가지 경우를 위해 함수를 2개 작성</strong>해야 합니다. 아래 예제를 확인해 보세요!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Cents
{
private:
    int m_cents {};

public:
    explicit Cents(int cents) : m_cents{ cents } { }

    // 프렌드 함수를 사용하여 Cents + int 더하기
    friend Cents operator+(const Cents&amp; c1, int value);

    // 프렌드 함수를 사용하여 int + Cents 더하기
    friend Cents operator+(int value, const Cents&amp; c1);


    int getCents() const { return m_cents; }
};

// 참고: 이 함수는 멤버 함수가 아닙니다!
Cents operator+(const Cents&amp; c1, int value)
{
    // Cents 생성자와 기본 연산자 +(int, int)를 사용합니다
    // 프렌드 함수이기 때문에 m_cents에 직접 접근할 수 있습니다
    return Cents { c1.m_cents + value };
}

// 참고: 이 함수는 멤버 함수가 아닙니다!
Cents operator+(int value, const Cents&amp; c1)
{
    // Cents 생성자와 기본 연산자 +(int, int)를 사용합니다
    // 프렌드 함수이기 때문에 m_cents에 직접 접근할 수 있습니다
    return Cents { c1.m_cents + value };
}

int main()
{
    Cents c1{ Cents{ 4 } + 6 };
    Cents c2{ 6 + Cents{ 4 } };

    std::cout &lt;&lt; &quot;I have &quot; &lt;&lt; c1.getCents() &lt;&lt; &quot; cents.\n&quot;;
    std::cout &lt;&lt; &quot;I have &quot; &lt;&lt; c2.getCents() &lt;&lt; &quot; cents.\n&quot;;

    return 0;
}
</code></pre>
<p>두 오버로딩 함수가 똑같은 내용으로 작성된 것을 확인하셨나요? 
사실 똑같은 덧셈을 하는 건데, 단지 받는 매개변수의 <strong>순서</strong>만 다를 뿐이기 때문입니다.</p>
<hr>
<h3 id="또-다른-예제">또 다른 예제</h3>
<p>다른 예제를 하나 더 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class MinMax
{
private:
    int m_min {}; // 지금까지 확인된 최솟값
    int m_max {}; // 지금까지 확인된 최댓값

public:
    MinMax(int min, int max)
        : m_min { min }, m_max { max }
    { }

    int getMin() const { return m_min; }
    int getMax() const { return m_max; }

    friend MinMax operator+(const MinMax&amp; m1, const MinMax&amp; m2);
    friend MinMax operator+(const MinMax&amp; m, int value);
    friend MinMax operator+(int value, const MinMax&amp; m);
};

MinMax operator+(const MinMax&amp; m1, const MinMax&amp; m2)
{
    // m1과 m2 중 더 작은 값 가져오기
    int min{ m1.m_min &lt; m2.m_min ? m1.m_min : m2.m_min };

    // m1과 m2 중 더 큰 값 가져오기
    int max{ m1.m_max &gt; m2.m_max ? m1.m_max : m2.m_max };

    return MinMax { min, max };
}

MinMax operator+(const MinMax&amp; m, int value)
{
    // m과 value 중 더 작은 값 가져오기
    int min{ m.m_min &lt; value ? m.m_min : value };

    // m과 value 중 더 큰 값 가져오기
    int max{ m.m_max &gt; value ? m.m_max : value };

    return MinMax { min, max };
}

MinMax operator+(int value, const MinMax&amp; m)
{
    // operator+(MinMax, int)를 호출합니다
    return m + value;
}

int main()
{
    MinMax m1{ 10, 15 };
    MinMax m2{ 8, 11 };
    MinMax m3{ 3, 12 };

    MinMax mFinal{ m1 + m2 + 5 + 8 + m3 + 16 };

    std::cout &lt;&lt; &quot;Result: (&quot; &lt;&lt; mFinal.getMin() &lt;&lt; &quot;, &quot; &lt;&lt;
        mFinal.getMax() &lt;&lt; &quot;)\n&quot;;

    return 0;
}
</code></pre>
<p><code>MinMax</code> 클래스는 지금까지 입력된 값들 중에서 최솟값과 최댓값을 기억하는 역할을 합니다. 여기서는 <code>+</code> 연산자를 무려 3번이나 오버로딩했네요! 이렇게 하면 두 개의 <code>MinMax</code> 객체끼리 더할 수도 있고, <code>MinMax</code> 객체에 일반 정수(int)를 더할 수도 있게 됩니다.</p>
<p>이 예제를 실행하면 다음과 같은 결과가 나옵니다.</p>
<pre><code>Result: (3, 16)
</code></pre><p>이 결과는 우리가 <code>mFinal</code>을 만들기 위해 더했던 값들 중에서 가장 작은 값(3)과 가장 큰 값(16)이라는 것을 알 수 있죠!</p>
<p>자, 그럼 <code>MinMax mFinal { m1 + m2 + 5 + 8 + m3 + 16 }</code>라는 코드가 어떻게 계산되는지 조금만 더 자세히 알아볼까요? <code>+</code> 연산자는 <strong>왼쪽에서 오른쪽으로</strong> 차례대로 계산된다는 사실을 기억해 주세요.</p>
<ol>
<li>먼저 <code>m1 + m2</code>가 계산됩니다. 이는 <code>operator+(m1, m2)</code>를 호출하게 되어, <code>MinMax(8, 15)</code>라는 결과값을 만듭니다.</li>
<li>다음으로 <code>MinMax(8, 15) + 5</code>가 계산됩니다. 이는 <code>operator+(MinMax(8, 15), 5)</code>를 호출하여 <code>MinMax(5, 15)</code>가 됩니다.</li>
<li>이어서 <code>MinMax(5, 15) + 8</code>을 하면, 똑같은 방식으로 <code>MinMax(5, 15)</code>가 반환됩니다.</li>
<li>거기에 <code>MinMax(5, 15) + m3</code>을 더하면 <code>MinMax(3, 15)</code>가 됩니다.</li>
<li>마지막으로 <code>MinMax(3, 15) + 16</code>을 계산해서 최종적으로 <code>MinMax(3, 16)</code>이 완성됩니다.</li>
</ol>
<p>이 최종 결과값이 바로 <code>mFinal</code> 객체를 처음 만들(초기화) 때 사용되는 거예요.</p>
<p>다시 말해, 이 코드는 <code>MinMax mFinal = (((((m1 + m2) + 5) + 8) + m3) + 16)</code> 와 같이 하나씩 순차적으로 계산됩니다. 매번 덧셈을 할 때마다 <code>MinMax</code> 객체가 반환되고, 이 객체가 다음 연산자의 왼쪽 피연산자 역할을 하면서 사슬처럼 이어지는 방식입니다.</p>
<hr>
<h3 id="다른-연산자를-재활용해서-연산자-구현하기">다른 연산자를 재활용해서 연산자 구현하기</h3>
<p>위 예제 코드 중에서 <code>operator+(int, MinMax)</code> 함수를 어떻게 만들었는지 다시 한번 주목해 보세요. 안에서 직접 계산하지 않고, 이미 만들어둔 <code>operator+(MinMax, int)</code>를 불러와서 사용했습니다. (어차피 순서만 다르고 결과는 똑같으니까요!)</p>
<p>이렇게 하면 <code>operator+(int, MinMax)</code>의 내용을 단 한 줄로 줄일 수 있습니다. 중복되는 똑같은 코드를 여러 번 작성할 필요가 없으니 코드 관리가 훨씬 쉬워지고, 함수를 읽고 이해하기도 편해집니다.</p>
<p>이처럼 이미 만들어진 다른 오버로딩 연산자를 호출해서 새로운 연산자를 정의하는 경우가 많습니다. 코드를 더 깔끔하고 단순하게 만들 수 있다면 이 방법을 적극적으로 사용하는 것이 좋습니다. (물론 계산 내용이 단 한 줄짜리처럼 너무 간단하다면, 굳이 다른 함수를 부르지 않고 직접 작성해도 큰 상관은 없습니다.)</p>
<hr>
<p><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br></p>
<p><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br><br></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 20]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-20</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-20</guid>
            <pubDate>Sat, 07 Mar 2026 07:03:06 GMT</pubDate>
            <description><![CDATA[<h2 id="201--함수-포인터-function-pointers">20.1 — 함수 포인터 (Function Pointers)</h2>
<p>이전 레슨에서 포인터는 변수의 주소를 저장하는 변수라고 배웠습니다. 
함수 포인터도 이와 아주 비슷해요! 변수 대신 <strong>함수</strong>를 가리킨다는 점만 빼고요.</p>
<p>다음 함수를 한번 살펴볼까요?</p>
<pre><code class="language-cpp">int foo()
{
    return 5;
}
</code></pre>
<p><code>foo()</code>라는 이름이 함수의 이름이라는 건 알 수 있습니다. 
그런데 이 함수의 &#39;타입&#39;은 무엇일까요? 함수도 자기만의 고유한 타입을 가지고 있습니다. 
이 경우, &#39;매개변수(입력값)가 없고 정수(int)를 반환하는 함수 타입&#39;이 됩니다.</p>
<p>변수처럼 함수도 컴퓨터 메모리의 지정된 주소에 살고 있습니다.</p>
<p>함수를 호출하면(실행하면), 컴퓨터는 그 함수가 살고 있는 메모리 주소로 훌쩍 점프해서 코드를 실행합니다.</p>
<pre><code class="language-cpp">int foo() // foo를 위한 코드는 메모리 주소 0x002717f0에서 시작합니다
{
    return 5;
}

int main()
{
    foo(); // 주소 0x002717f0으로 점프합니다

    return 0;
}
</code></pre>
<p>프로그래밍을 하다 보면 언젠가 이런 아주 흔한 실수를 하게 될 겁니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int foo() // 코드는 메모리 주소 0x002717f0에서 시작합니다
{
    return 5;
}

int main()
{
    std::cout &lt;&lt; foo &lt;&lt; &#39;\n&#39;; // foo()를 호출하려고 했는데, 실수로 foo 그 자체를 출력하고 있습니다!

    return 0;
}
</code></pre>
<p><code>foo()</code>를 실행해서 그 결과값을 화면에 띄우는 대신, <code>foo</code>라는 함수 자체를 출력하라고 화면에 던져버린 셈입니다. 이럴 땐 무슨 일이 일어날까요?</p>
<p>괄호 없이 함수 이름만 덩그러니 쓰면, C++은 이 함수를 &#39;함수 포인터(함수의 주소를 들고 있는 녀석)&#39;로 바꿔버립니다. 그러고 나서 출력하려고 시도하는데, C++의 기본 출력 기능은 함수 포인터를 어떻게 화면에 보여줘야 할지 모릅니다. 그래서 규칙에 따라 이 포인터를 참/거짓을 나타내는 <code>bool</code> 타입으로 바꿔버리죠. 함수는 빈 곳을 가리키는 게 아니니까 언제나 &#39;참(true)&#39;으로 평가됩니다. 따라서 결과적으로 화면에는 숫자 <code>1</code>이 출력됩니다.</p>
<blockquote>
<p><strong>팁</strong>
일부 컴파일러(예: Visual Studio)는 확장 기능을 통해 숫자 1 대신 함수의 진짜 주소를 출력해주기도 합니다: <code>0x002717f0</code></p>
</blockquote>
<p>만약 여러분의 컴퓨터에서 주소가 안 나오는데 꼭 확인해보고 싶다면, 함수를 &#39;void 포인터&#39;라는 것으로 강제로 변환해서 출력해볼 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt;
int foo() // code starts at memory address 0x002717f0
{
    return 5;
}
&gt;
int main()
{
    std::cout &lt;&lt; reinterpret_cast&lt;void*&gt;(foo) &lt;&lt; &#39;\n&#39;; // Tell C++ to interpret function foo as a void pointer (implementation-defined behavior)
&gt;
    return 0;
}</code></pre>
<blockquote>
<p>다만 이 방법은 시스템 환경마다 다르게 동작할 수 있으니 참고만 하세요.</p>
</blockquote>
<p>일반 변수를 가리키는 포인터를 만들 수 있듯이, 함수를 가리키는 포인터도 당연히 만들 수 있습니다. 지금부터 이 &#39;함수 포인터&#39;를 어떻게 만들고 쓰는지 알아볼 텐데요. 사실 이건 조금 어려운 주제라서, C++의 기초만 빠르게 배우고 싶은 초보자라면 나머지 내용은 그냥 가볍게 훑어보거나 건너뛰어도 괜찮습니다!</p>
<hr>
<h3 id="함수를-가리키는-포인터-만들기">함수를 가리키는 포인터 만들기</h3>
<p>함수 포인터를 만드는 문법은 C++에서 가장 못생긴 문법 중 하나로 꼽힙니다.</p>
<pre><code class="language-cpp">// fcnPtr은 매개변수가 없고 정수를 반환하는 함수를 가리키는 포인터입니다
int (*fcnPtr)();
</code></pre>
<p>위 코드에서 <code>fcnPtr</code>은 &#39;매개변수가 없고 정수를 반환하는 함수&#39;를 가리킬 수 있는 포인터입니다. 이런 똑같은 모양의 조건을 가진 함수라면 무엇이든 가리킬 수 있어요.</p>
<p><code>*fcnPtr</code> 주변의 괄호는 아주 중요합니다. 만약 괄호를 빼고 <code>int* fcnPtr()</code>이라고 쓰면, 컴퓨터는 이걸 &quot;매개변수가 없고 정수 포인터(int*)를 반환하는 함수&quot;로 완전히 잘못 오해하게 됩니다.</p>
<p>함수 포인터를 상수처럼 고정하고 싶다면(한 번 가리킨 함수를 못 바꾸게 하려면) <code>const</code>를 별표(<code>*</code>) 뒤에 붙여주면 됩니다.</p>
<pre><code class="language-cpp">int (*const fcnPtr)();
</code></pre>
<p>만약 <code>const</code>를 맨 앞의 <code>int</code> 앞에 쓰면, &quot;상수 정수(const int)를 반환하는 함수&quot;를 뜻하게 되니 위치를 조심해야 합니다.</p>
<blockquote>
<p><strong>팁</strong>
함수 포인터 문법은 진짜 눈에 안 들어옵니다. 이런 복잡한 선언을 어떻게 읽어내는지 알려주는 글들이 있으니 참고해 보세요:</p>
</blockquote>
<ul>
<li><a href="https://c-faq.com/decl/spiral.anderson.html">https://c-faq.com/decl/spiral.anderson.html</a></li>
<li><a href="https://web.archive.org/web/20110818081319/http://ieng9.ucsd.edu/~cs30x/rt_lt.rule.html">https://web.archive.org/web/20110818081319/http://ieng9.ucsd.edu/~cs30x/rt_lt.rule.html</a></li>
</ul>
<hr>
<h3 id="함수-포인터에-함수-지정하기">함수 포인터에 함수 지정하기</h3>
<p>함수 포인터를 처음 만들 때 함수를 바로 연결해줄 수 있고, 나중에 다른 함수로 바꿀 수도 있습니다. 일반 변수처럼 <code>&amp;</code> 기호를 써서 함수의 주소를 가져오면 됩니다.</p>
<pre><code class="language-cpp">int foo()
{
    return 5;
}

int goo()
{
    return 6;
}

int main()
{
    int (*fcnPtr)(){ &amp;foo }; // fcnPtr이 foo 함수를 가리키도록 설정합니다
    fcnPtr = &amp;goo; // 이제 fcnPtr은 goo 함수를 가리킵니다

    return 0;
}
</code></pre>
<p>초보자들이 정말 많이 하는 실수가 바로 이것입니다:</p>
<pre><code class="language-cpp">fcnPtr = goo();
</code></pre>
<p>이렇게 괄호를 써버리면, <code>goo()</code> 함수를 <strong>실행</strong>한 결과값(숫자 6)을 포인터에 넣으려고 하게 됩니다. 하지만 함수 포인터는 숫자가 아니라 &#39;주소&#39;를 담는 상자이므로 에러가 납니다. 괄호를 꼭 빼고 주소를 넣어주세요.</p>
<p>또한, 포인터의 타입(매개변수와 반환값의 종류)이 가리키려는 함수의 타입과 완벽히 똑같아야 합니다.</p>
<pre><code class="language-cpp">// 함수 원형 (미리 모양만 선언해 둔 것)
int foo();
double goo();
int hoo(int x);

// 함수 포인터에 함수 연결해보기
int (*fcnPtr1)(){ &amp;foo };    // 성공
int (*fcnPtr2)(){ &amp;goo };    // 실패 -- 반환하는 타입(int vs double)이 다릅니다!
double (*fcnPtr4)(){ &amp;goo }; // 성공

fcnPtr1 = &amp;hoo;              // 실패 -- fcnPtr1은 매개변수가 없는데, hoo()는 매개변수 x가 필요합니다
int (*fcnPtr3)(int){ &amp;hoo }; // 성공
</code></pre>
<p>C++은 알아서 눈치껏 함수를 함수 포인터로 변환해 주기 때문에 사실 <code>&amp;</code> 기호를 생략해도 잘 작동합니다. 하지만 함수 포인터를 &#39;void 포인터&#39; 같이 아예 다른 종류로는 바꿀 수 없어요.</p>
<pre><code class="language-cpp">// 함수 원형
int foo();

// 함수 포인터 초기화
int (*fcnPtr5)() { foo }; // 성공, foo가 알아서 함수 포인터로 변환됩니다
void* vPtr { foo };       // 실패, (일부 프로그램에선 억지로 될 수도 있지만 원칙적으로 안 됩니다)
</code></pre>
<p>아무것도 가리키지 않는다는 뜻의 빈 포인터, <code>nullptr</code>을 넣을 수도 있습니다:</p>
<pre><code class="language-cpp">int (*fcnptr)() { nullptr }; // 성공
</code></pre>
<hr>
<h3 id="함수-포인터로-함수-실행하기">함수 포인터로 함수 실행하기</h3>
<p>함수 포인터의 진짜 쓸모는 바로 그 포인터를 이용해 함수를 짠! 하고 실행하는 겁니다. 여기엔 두 가지 방법이 있어요. 첫 번째는 정석대로 <code>*</code> 기호를 쓰는 방법입니다.</p>
<pre><code class="language-cpp">int foo(int x)
{
    return x;
}

int main()
{
    int (*fcnPtr)(int){ &amp;foo }; // fcnPtr을 foo 함수로 초기화합니다
    (*fcnPtr)(5); // fcnPtr을 통해 숫자 5를 넣고 foo 함수를 실행합니다!

    return 0;
}
</code></pre>
<p>두 번째는 굳이 <code>*</code>를 쓰지 않고 자연스럽게 부르는 방법입니다.</p>
<pre><code class="language-cpp">int foo(int x)
{
    return x;
}

int main()
{
    int (*fcnPtr)(int){ &amp;foo }; // fcnPtr을 foo 함수로 초기화합니다
    fcnPtr(5); // fcnPtr을 통해 숫자 5를 넣고 foo 함수를 실행합니다!

    return 0;
}
</code></pre>
<p>보시다시피 두 번째 방법이 우리가 평소에 함수를 부르던 모습이랑 완전 똑같아서 훨씬 편합니다. (실은 우리가 쓰는 일반 함수 이름도 다 함수 포인터처럼 작동하거든요!) 최신 프로그램들은 다 이 편한 방법을 지원합니다.</p>
<p>한 가지 주의할 점! 포인터가 빈 껍데기(<code>nullptr</code>)일 수도 있으니, 실행하기 전에 포인터가 비어있지 않은지 꼭 확인하는 습관을 들이세요. 빈 포인터를 실행하면 프로그램이 펑하고 터질 수 있습니다.</p>
<pre><code class="language-cpp">int foo(int x)
{
    return x;
}

int main()
{
    int (*fcnPtr)(int){ &amp;foo }; // fcnPtr을 foo 함수로 초기화합니다

    if (fcnPtr) // fcnPtr이 빈 포인터(null)가 아닌지 확인합니다
        fcnPtr(5); // 안전하다면 실행합니다 (확인 안 하고 실행하면 오류가 날 수 있어요)

    return 0;
}
</code></pre>
<hr>
<h3 id="기본-인자는-함수-포인터를-통해-호출할-때-작동하지-않습니다-고급">기본 인자는 함수 포인터를 통해 호출할 때 작동하지 않습니다 (고급)</h3>
<p>보통 함수에 &quot;값을 안 넣으면 이 값으로 쳐줘!&quot;라는 기본값(기본 인자)이 있으면, 컴퓨터가 코드를 번역할 때(컴파일 타임) 알아서 그 빈칸을 채워줍니다.</p>
<p>하지만 함수 포인터를 통해 함수를 부를 때는 이야기가 달라집니다. 이건 프로그램이 실제로 돌아가는 중간(런타임)에 어떤 함수가 선택될지 결정되기 때문에, 컴퓨터가 미리 빈칸을 기본값으로 채워줄 수가 없어요.</p>
<blockquote>
<p><strong>핵심 통찰</strong>
어떤 함수를 부를지가 프로그램 실행 중(런타임)에 결정되기 때문에, 함수 포인터를 써서 실행할 때는 설정해둔 &#39;기본 인자(기본값)&#39;가 적용되지 않습니다.</p>
</blockquote>
<p>이 특징을 잘 이용하면, 이름은 똑같고 모양만 달라서 컴퓨터가 헷갈려하는 함수들 중에 딱 하나만 콕 집어서 실행하도록 만들 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int x)
{
    std::cout &lt;&lt; &quot;print(int)\n&quot;;
}

void print(int x, int y = 10)
{
    std::cout &lt;&lt; &quot;print(int, int)\n&quot;;
}

int main()
{
//    print(1); // 어떤 print 함수를 부르는 건지 애매해서 컴퓨터가 에러를 냅니다

    // 단계별로 풀어서 해결하는 방법
    using vnptr = void(*)(int); // void(int) 형태의 함수 포인터에 대한 별명을 만듭니다
    vnptr pi { print }; // 우리의 함수 포인터를 print 함수와 연결합니다
    pi(1); // 함수 포인터를 통해 매개변수가 하나인 print(int) 함수만 정확히 부릅니다

    // 짧고 굵게 해결하는 방법
    static_cast&lt;void(*)(int)&gt;(print)(1); // print를 void(int) 형태로 변환한 뒤 숫자 1을 넣고 부릅니다

    return 0;
}
</code></pre>
<hr>
<h3 id="함수를-다른-함수의-인자매개변수로-전달하기">함수를 다른 함수의 인자(매개변수)로 전달하기</h3>
<p>함수 포인터를 배우는 가장 큰 이유가 바로 여기에 있습니다. <strong>함수 안에 다른 함수를 재료처럼 집어넣을 수 있다는 것!</strong> 이렇게 다른 함수의 매개변수로 쏙 들어가는 함수를 흔히 &#39;콜백 함수(callback functions)&#39;라고 부릅니다.</p>
<p>예를 들어 숫자들이 들어있는 배열을 정렬하는 프로그램을 만든다고 쳐볼게요. 정렬은 알아서 잘하되, 숫자를 오름차순(작은 것부터)으로 할지, 내림차순(큰 것부터)으로 할지는 사용하는 사람이 입맛대로 고르게 해주고 싶습니다.</p>
<p>정렬 프로그램들은 보통 &#39;숫자 두 개를 비교해서 자리를 바꿀까 말까?&#39;를 결정하는 방식으로 작동합니다. 즉, 이 <strong>비교하는 부분</strong>만 바꿔 끼울 수 있게 만들면 정렬 코드를 통째로 다시 짤 필요가 없다는 거죠!</p>
<p>우리가 예전에 만들었던 선택 정렬 코드를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::swap을 사용하기 위해 필요합니다

void SelectionSort(int* array, int size)
{
    if (!array)
        return;

    // 배열의 모든 요소를 하나씩 밟고 지나갑니다
    for (int startIndex{ 0 }; startIndex &lt; (size - 1); ++startIndex)
    {
        // smallestIndex는 지금까지 발견한 가장 작은 숫자의 위치입니다.
        int smallestIndex{ startIndex };

        // 배열의 나머지 부분에서 가장 작은 숫자를 찾습니다
        for (int currentIndex{ startIndex + 1 }; currentIndex &lt; size; ++currentIndex)
        {
            // 만약 현재 숫자가 우리가 찾았던 가장 작은 숫자보다 작다면
            if (array[smallestIndex] &gt; array[currentIndex]) // 여기서 비교가 일어납니다!
            {
                // 이 숫자가 이번 턴의 새로운 제일 작은 숫자가 됩니다
                smallestIndex = currentIndex;
            }
        }

        // 맨 앞의 숫자와 우리가 찾은 가장 작은 숫자의 자리를 바꿉니다
        std::swap(array[startIndex], array[smallestIndex]);
    }
}
</code></pre>
<p>저기 <code>&gt;</code> 기호로 고정되어 있는 비교 부분을 함수로 따로 빼내 보겠습니다. 두 숫자를 비교해서 자리를 바꿀지 말지(true/false) 알려주는 함수예요.</p>
<pre><code class="language-cpp">bool ascending(int x, int y)
{
    return x &gt; y; // 첫 번째 숫자가 두 번째 숫자보다 크면 자리를 바꾸라고 알려줍니다 (오름차순)
}
</code></pre>
<p>이걸 원래 코드에 끼워 넣으면 이렇게 됩니다:</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::swap을 사용하기 위해 필요합니다

void SelectionSort(int* array, int size)
{
    if (!array)
        return;

    // 배열의 모든 요소를 하나씩 밟고 지나갑니다
    for (int startIndex{ 0 }; startIndex &lt; (size - 1); ++startIndex)
    {
        // smallestIndex는 지금까지 발견한 가장 작은 숫자의 위치입니다.
        int smallestIndex{ startIndex };

        // 배열의 나머지 부분에서 가장 작은 숫자를 찾습니다
        for (int currentIndex{ startIndex + 1 }; currentIndex &lt; size; ++currentIndex)
        {
            // 만약 현재 숫자가 우리가 찾았던 가장 작은 숫자보다 작다면
            if (ascending(array[smallestIndex], array[currentIndex])) // 비교를 함수에게 맡겼습니다!
            {
                // 이 숫자가 이번 턴의 새로운 제일 작은 숫자가 됩니다
                smallestIndex = currentIndex;
            }
        }

        // 맨 앞의 숫자와 우리가 찾은 가장 작은 숫자의 자리를 바꿉니다
        std::swap(array[startIndex], array[smallestIndex]);
    }
}
</code></pre>
<p>자, 이제 하이라이트입니다! 우리가 직접 비교 함수를 못 박아두는 대신, <strong>프로그램을 사용하는 사람이 직접 자기가 원하는 비교 함수를 넣을 수 있도록</strong> 빈칸(함수 포인터)을 뚫어줄 겁니다.</p>
<p>사용자가 넣을 함수는 두 개의 숫자를 받아서 참/거짓을 알려주는 형태니까, 함수 포인터의 모양은 이렇게 될 겁니다.</p>
<pre><code class="language-cpp">bool (*comparisonFcn)(int, int);
</code></pre>
<p>이제 정렬 함수의 세 번째 재료로 이 포인터를 받아서 쓰기만 하면 됩니다. 사용자가 오름차순 함수를 넣으면 오름차순으로, 내림차순 함수를 넣으면 내림차순으로 작동하는 마법 같은 정렬 함수가 완성됩니다!</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::swap을 사용하기 위해 필요합니다
#include &lt;iostream&gt;

// 세 번째 매개변수가 바로 사용자가 직접 만들어 넣을 비교 함수입니다!
void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int))
{
    if (!array || !comparisonFcn)
        return;

    // 배열의 모든 요소를 하나씩 밟고 지나갑니다
    for (int startIndex{ 0 }; startIndex &lt; (size - 1); ++startIndex)
    {
        // bestIndex는 지금까지 발견한 가장 조건에 맞는 숫자의 위치입니다.
        int bestIndex{ startIndex };

        // 배열의 나머지 부분에서 가장 조건에 맞는 숫자를 찾습니다
        for (int currentIndex{ startIndex + 1 }; currentIndex &lt; size; ++currentIndex)
        {
            // 사용자가 넘겨준 함수를 이용해 두 숫자를 비교합니다!
            if (comparisonFcn(array[bestIndex], array[currentIndex])) // 여기서 비교가 일어납니다
            {
                // 이 숫자가 이번 턴의 새로운 &#39;최고의&#39; 숫자가 됩니다
                bestIndex = currentIndex;
            }
        }

        // 맨 앞의 숫자와 우리가 찾은 최고의 숫자의 자리를 바꿉니다
        std::swap(array[startIndex], array[bestIndex]);
    }
}

// 오름차순(점점 커지게)으로 정렬해주는 비교 함수입니다
bool ascending(int x, int y)
{
    return x &gt; y; // 첫 번째 숫자가 더 크면 자리를 바꿉니다
}

// 내림차순(점점 작아지게)으로 정렬해주는 비교 함수입니다
bool descending(int x, int y)
{
    return x &lt; y; // 두 번째 숫자가 더 크면 자리를 바꿉니다
}

// 화면에 배열의 숫자들을 예쁘게 출력해주는 함수입니다
void printArray(int* array, int size)
{
    if (!array)
        return;

    for (int index{ 0 }; index &lt; size; ++index)
    {
        std::cout &lt;&lt; array[index] &lt;&lt; &#39; &#39;;
    }
    std::cout &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };

    // descending() 함수를 넣어서 배열을 내림차순으로 정렬합니다
    selectionSort(array, 9, descending);
    printArray(array, 9);

    // ascending() 함수를 넣어서 배열을 오름차순으로 정렬합니다
    selectionSort(array, 9, ascending);
    printArray(array, 9);

    return 0;
}
</code></pre>
<p>결과가 어떻게 나올까요?</p>
<pre><code class="language-text">9 8 7 6 5 4 3 2 1
1 2 3 4 5 6 7 8 9
</code></pre>
<p>정말 멋지죠! 우리는 뼈대 역할을 하는 정렬 함수 하나만 만들었을 뿐인데, 사용자가 원하는 대로 입맛에 맞게 정렬 방식을 조종할 수 있게 되었습니다.</p>
<p>심지어 &quot;짝수를 먼저 세워!&quot; 같은 아주 특이한 비교 함수를 만들어서 넣어도 찰떡같이 알아듣습니다.</p>
<pre><code class="language-cpp">bool evensFirst(int x, int y)
{
    // x가 짝수이고 y가 홀수면, 짝수인 x가 먼저입니다 (자리 안 바꿈)
    if ((x % 2 == 0) &amp;&amp; !(y % 2 == 0))
        return false;

    // x가 홀수이고 y가 짝수면, 짝수인 y가 먼저 가야 합니다 (자리 바꿈)
    if (!(x % 2 == 0) &amp;&amp; (y % 2 == 0))
        return true;

    // 둘 다 짝수거나 둘 다 홀수면, 그냥 오름차순으로 정리합니다
    return ascending(x, y);
}

int main()
{
    int array[9]{ 3, 7, 9, 5, 6, 1, 8, 2, 4 };

    selectionSort(array, 9, evensFirst);
    printArray(array, 9);

    return 0;
}
</code></pre>
<p>이 코드의 결과는 이렇습니다.</p>
<pre><code class="language-text">2 4 6 8 1 3 5 7 9
</code></pre>
<p>이렇게 함수 포인터를 사용하면 내가 미리 만들어 둔 튼튼한 코드 뼈대 사이에 다른 사람이 자기만의 코드를 쏙 끼워 넣게(hook) 만들어 줄 수 있습니다. 덕분에 코드를 여러 번 다시 쓸 수 있어서 프로그래머의 퇴근 시간을 아주 앞당겨주죠!</p>
<p>참고로, 매개변수 자리에 그냥 함수의 원형 모양을 써도 C++이 알아서 함수 포인터로 찰떡같이 이해하고 바꿔줍니다. 즉 아래 두 코드는 완전히 같은 의미입니다.</p>
<pre><code class="language-cpp">void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int))
</code></pre>
<pre><code class="language-cpp">void selectionSort(int* array, int size, bool comparisonFcn(int, int))
</code></pre>
<p>다만 이 편리함은 &#39;매개변수&#39; 자리에서만 통합니다. 일반적인 곳에서 두 번째 방식처럼 쓰면 그냥 함수를 선언하는 걸로 알아들으니 주의하세요.</p>
<hr>
<h3 id="기본-함수-제공하기">기본 함수 제공하기</h3>
<p>사용자한테 &quot;매번 비교 함수 만들어서 넣어!&quot;라고 하면 귀찮아하겠죠? 이럴 땐 우리가 미리 사람들이 자주 쓸 법한 <code>ascending()</code>이나 <code>descending()</code> 같은 편의점용 함수를 준비해두면 아주 좋습니다.</p>
<p>심지어 &quot;아무 함수도 안 넣으면 그냥 오름차순으로 해줄게&quot;라고 기본값을 설정할 수도 있습니다.</p>
<pre><code class="language-cpp">// 함수를 안 넣으면 기본으로 ascending(오름차순) 함수를 쓰도록 설정합니다
void selectionSort(int* array, int size, bool (*comparisonFcn)(int, int) = ascending);
</code></pre>
<p>이렇게 해두면 사용자가 함수 포인터 없이 그냥 정렬 함수만 부를 경우 자동으로 오름차순 정렬이 됩니다. (물론 컴퓨터가 알아먹을 수 있게 이 코드 이전에 <code>ascending</code> 함수가 먼저 만들어져 있어야 합니다.)</p>
<hr>
<h3 id="타입-별명으로-함수-포인터를-예쁘게-만들기">타입 별명으로 함수 포인터를 예쁘게 만들기</h3>
<p>솔직히 인정합시다. 함수 포인터 문법은 별표에 괄호에 진짜 너무 못생겼고 복잡합니다. 다행히 <code>using</code>이라는 키워드로 예쁜 &#39;별명&#39;을 지어주면 일반 변수처럼 아주 깔끔하게 쓸 수 있습니다.</p>
<pre><code class="language-cpp">using ValidateFunction = bool(*)(int, int);
</code></pre>
<p>이 한 줄은 &quot;이제부터 <code>bool(*)(int, int)</code>라는 복잡한 함수 포인터 모양을 그냥 <code>ValidateFunction</code>이라고 부를게!&quot;라는 뜻입니다.</p>
<p>이제 아래의 복잡했던 코드가:</p>
<pre><code class="language-cpp">bool validate(int x, int y, bool (*fcnPtr)(int, int)); // 눈 아프고 복잡합니다
</code></pre>
<p>이렇게 변신합니다:</p>
<pre><code class="language-cpp">bool validate(int x, int y, ValidateFunction pfcn) // 아주 깔끔하죠!
</code></pre>
<hr>
<h3 id="stdfunction-사용하기-using-stdfunction">std::function 사용하기 (Using std::function)</h3>
<p>C++은 이런 지저분한 문법을 아예 싹 덮어버릴 수 있도록 표준 라이브러리 <code>&lt;functional&gt;</code>에 <code>std::function</code>이라는 아주 편리한 도구를 만들어 두었습니다. 최신 C++에서는 이게 함수 포인터의 끝판왕입니다.</p>
<pre><code class="language-cpp">#include &lt;functional&gt;

bool validate(int x, int y, std::function&lt;bool(int, int)&gt; fcn); // bool을 반환하고 두 개의 int를 받는 std::function
</code></pre>
<p>보시다시피 꺾쇠 <code>&lt; &gt;</code> 안에 <code>반환타입(매개변수타입)</code> 형태로 직관적으로 적어주면 끝입니다. 매개변수가 없으면 그냥 빈 괄호 <code>()</code>를 쓰면 됩니다.</p>
<p>앞서 본 예제를 <code>std::function</code>으로 바꿔볼까요?</p>
<pre><code class="language-cpp">#include &lt;functional&gt;
#include &lt;iostream&gt;

int foo()
{
    return 5;
}

int goo()
{
    return 6;
}

int main()
{
    std::function&lt;int()&gt; fcnPtr{ &amp;foo }; // 매개변수가 없고 int를 반환하는 함수를 담는 상자 만들기!
    fcnPtr = &amp;goo; // 이제 fcnPtr은 goo 함수를 가리킵니다
    std::cout &lt;&lt; fcnPtr() &lt;&lt; &#39;\n&#39;; // 일반 함수 부르듯 편하게 씁니다

    std::function fcnPtr2{ &amp;foo }; // C++17부터는 모양을 자동으로 유추하게 놔둘 수도 있습니다(CTAD)

    return 0;
}
</code></pre>
<p>이것도 이름이 너무 길다 싶으면 아까 배운 별명을 지어주는 방식을 섞어 쓰면 완벽합니다.</p>
<pre><code class="language-cpp">using ValidateFunctionRaw = bool(*)(int, int); // 옛날 방식의 날것 그대로의 함수 포인터 별명
using ValidateFunction = std::function&lt;bool(int, int)&gt;; // 깔끔한 std::function 방식의 별명
</code></pre>
<p>참고로 <code>std::function</code>은 <code>fcnPtr()</code>처럼 이름만 부르는 자연스러운 방식만 허용하고, <code>(*fcnPtr)()</code>처럼 앞에 별표를 굳이 붙이는 옛날 방식은 허용하지 않습니다.</p>
<hr>
<h3 id="함수-포인터의-타입-추론">함수 포인터의 타입 추론</h3>
<p>귀찮음을 덜어주는 마법의 단어 <code>auto</code> 기억하시죠? 일반 변수를 만들 때처럼, 함수 포인터를 만들 때도 <code>auto</code>를 써서 컴퓨터가 알아서 모양을 맞춰주게 할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int foo(int x)
{
    return x;
}

int main()
{
    auto fcnPtr{ &amp;foo }; // 알아서 foo를 가리키는 알맞은 함수 포인터를 만들어줍니다!
    std::cout &lt;&lt; fcnPtr(5) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>코드가 아주 짧고 깔끔해지죠? 물론 이 방법은 코드가 너무 짧아진 나머지, 이 함수가 매개변수를 몇 개나 받고 뭘 반환하는지가 눈에 안 보여서 코드를 읽다가 헷갈릴 수 있다는 단점이 있긴 합니다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>함수 포인터는 수많은 함수들을 배열 같은 곳에 깔끔하게 모아두고 싶을 때나, 다른 함수의 재료(콜백 함수)로 함수 자체를 넘겨주고 싶을 때 아주 유용하게 쓰입니다.</p>
<p>다만, 옛날 방식의 <code>(*fcnPtr)</code> 문법은 너무 못생겼고 실수하기 딱 좋기 때문에, <strong>최신 기능인 <code>std::function</code>을 사용하는 것을 강력히 추천</strong>합니다. 딱 한 번만 쓰고 말 거라면 매개변수 자리에 <code>std::function</code>을 바로 쓰고, 여러 번 반복해서 쓸 거라면 예쁜 별명(<code>using</code>)을 지어주는 것이 가장 좋은 습관입니다!</p>
<hr>
<h2 id="202--스택stack과-힙heap">20.2 — 스택(Stack)과 힙(Heap)</h2>
<p>컴퓨터에서 프로그램이 실행될 때, 프로그램이 사용하는 메모리(기억 공간)는 마치 집의 방들을 용도에 따라 나누듯 몇 가지 구역으로 나뉩니다. 초보자분들도 이해하기 쉽게 정리해 볼게요!</p>
<ul>
<li><p><strong>코드 영역 (텍스트 영역)</strong> 
우리가 짠 프로그램 코드가 들어가는 곳입니다. (보통 읽기만 가능해요.)</p>
</li>
<li><p><strong>bss 영역</strong> 
아직 값이 정해지지 않은(0으로 초기화된) 전역 변수나 정적 변수가 사는 곳입니다.</p>
</li>
<li><p><strong>데이터 영역</strong> 
이미 값이 정해진 전역 변수나 정적 변수가 사는 곳입니다.</p>
</li>
<li><p><strong>힙(Heap)</strong> 
프로그램이 실행되는 도중에 &quot;나 메모리 좀 빌려줘!&quot; 할 때 동적으로 공간을 내어주는 커다란 창고입니다.</p>
</li>
<li><p><strong>콜 스택(Call stack)</strong> 
함수를 부를 때 필요한 재료들(매개변수)이나, 함수 안에서 잠깐 쓰는 변수들(지역 변수)이 잠시 머무는 곳입니다.</p>
</li>
</ul>
<p>이번 강의에서는 가장 중요하고 재미있는 일들이 일어나는 <strong>힙(Heap)</strong>과 <strong>스택(Stack)</strong>, 이 두 가지에만 집중해 보겠습니다.</p>
<hr>
<h3 id="힙heap-영역">힙(Heap) 영역</h3>
<p>힙(Heap) 영역은 <strong>&#39;필요할 때 언제든 메모리를 빌려 쓸 수 있는 거대한 창고&#39;</strong>라고 생각하시면 쉽습니다. C++에서 <code>new</code>라는 마법의 단어(연산자)를 사용하면, 이 힙 영역에서 필요한 만큼 메모리를 빌려오게 됩니다.</p>
<p>만약 <code>int</code>형 변수 하나가 4바이트의 공간을 차지한다고 가정해 볼까요?</p>
<pre><code class="language-cpp">int* ptr { new int }; // new int는 힙 창고에서 4바이트의 공간을 빌려옵니다.
int* array { new int[10] }; // new int[10]은 힙 창고에서 40바이트의 공간을 빌려옵니다.
</code></pre>
<p>이렇게 빌려온 공간의 &#39;주소&#39;를 우리가 만든 포인터에 저장해 두고 쓰는 방식입니다. 
운영체제가 빈 공간을 어떻게 찾아서 빌려주는지 그 복잡한 원리까지는 몰라도 괜찮아요! 
다만 한 가지 알아둘 점은, <strong>메모리를 연달아 빌린다고 해서 실제로 창고 안에서 나란히 붙어있는 공간을 주지는 않을 수도 있다</strong>는 것입니다.</p>
<pre><code class="language-cpp">int* ptr1 { new int };
int* ptr2 { new int };
// ptr1과 ptr2가 가리키는 주소가 창고 안에서 나란히 붙어있지 않을 수도 있습니다!
</code></pre>
<p>다 쓴 메모리를 <code>delete</code>로 삭제하면, 메모리에 있던 변수가 아예 세상에서 사라지는 게 아니라 <strong>&quot;다 썼으니 창고에 반납할게요&quot;</strong> 하고 운영체제에 돌려주는 것을 의미합니다. 그럼 다음에 다른 누군가가 그 공간을 다시 빌려 쓸 수 있겠죠.</p>
<p>힙 영역은 다음과 같은 장단점이 있습니다:</p>
<ul>
<li><strong>단점:</strong> 스택에 비해 메모리를 빌려오는 속도가 조금 느립니다.</li>
<li><strong>주의점:</strong> 직접 반납(<code>delete</code>)하거나 프로그램이 완전히 끝날 때까지 메모리 공간을 계속 차지하고 있습니다. (다 쓰고 반납 안 하면 &#39;메모리 누수&#39;라는 골치 아픈 문제가 생겨요!)</li>
<li><strong>단점:</strong> 포인터를 통해서만 접근해야 해서, 일반 변수를 바로 쓰는 것보다 아주 살짝 느립니다.</li>
<li><strong>장점:</strong> 창고가 아주 넓기 때문에 엄청나게 큰 배열이나 복잡한 데이터를 저장하기에 아주 좋습니다.</li>
</ul>
<hr>
<h3 id="콜-스택call-stack">콜 스택(Call stack)</h3>
<p>콜 스택(보통 줄여서 그냥 <strong>&#39;스택&#39;</strong>이라고 부릅니다)은 훨씬 더 흥미로운 역할을 합니다. 
스택은 프로그램이 시작된 후 지금까지 <strong>어떤 함수들이 실행 중인지 기억</strong>하고, 그 함수들이 사용하는 지역 변수들의 자리를 마련해 주는 똑똑한 비서입니다.</p>
<p>스택이 어떻게 일하는지 이해하려면, 먼저 &#39;스택 자료 구조&#39;가 무엇인지 알아야 해요.</p>
<hr>
<h3 id="스택stack-자료-구조">스택(Stack) 자료 구조</h3>
<p>&#39;자료 구조&#39;란 데이터를 효율적으로 쓰기 위해 정리하는 방법입니다. 여러분이 식당에 갔을 때 뷔페 한쪽에 <strong>차곡차곡 쌓여 있는 접시 더미</strong>를 떠올려 보세요. 접시가 무겁게 쌓여 있기 때문에 우리가 할 수 있는 행동은 딱 세 가지뿐입니다.</p>
<ol>
<li>맨 위에 있는 접시가 뭔지 본다.</li>
<li>맨 위에 있는 접시를 꺼낸다.</li>
<li>새 접시를 맨 위에 올려놓는다.</li>
</ol>
<p>컴퓨터 프로그래밍의 스택도 똑같습니다! 스택은 데이터를 보관하는 상자인데, 아무 곳에나 마음대로 넣고 뺄 수 있는 배열과 달리 규칙이 엄격합니다.</p>
<ul>
<li>스택 맨 위(꼭대기)에 있는 항목 확인하기 (보통 <code>top()</code>이나 <code>peek()</code>이라는 함수를 씁니다)</li>
<li>맨 위에서 항목 꺼내기 (<strong>팝(Pop)</strong>이라고 부릅니다)</li>
<li>맨 위에 새 항목 올려놓기 (<strong>푸시(Push)</strong>라고 부릅니다)</li>
</ul>
<p>스택은 <strong>가장 나중에 들어온 녀석이 가장 먼저 나가는 (LIFO: Last-In, First-Out)</strong> 구조입니다. 접시를 방금 맨 위에 올려두었다면, 다음 사람이 접시를 가져갈 때 바로 그 접시를 제일 먼저 가져가게 되겠죠. 넣을 때(Push)는 위로 쌓이고, 뺄 때(Pop)는 위에서부터 줄어듭니다.</p>
<p>간단한 예시를 볼까요?</p>
<blockquote>
<p>스택: 텅 빔
푸시 1 -&gt; 스택: 1
푸시 2 -&gt; 스택: 1 2
푸시 3 -&gt; 스택: 1 2 3
팝 -&gt; 스택: 1 2 (3이 빠짐)
팝 -&gt; 스택: 1 (2가 빠짐)</p>
</blockquote>
<p>접시 비유도 좋지만, <strong>아래에서부터 위로 차곡차곡 고정되어 쌓여 있는 우편함</strong>을 상상하면 더 정확합니다.
가장 아래쪽 빈 우편함에 포스트잇(마커)을 붙여둡니다. 새 물건(Push)을 넣으면 포스트잇이 붙은 우편함에 넣고 포스트잇을 한 칸 위로 옮깁니다. 물건을 뺄 때(Pop)는 포스트잇을 한 칸 아래로 내린 뒤 그 우편함에서 물건을 꺼냅니다. 포스트잇 아래에 있는 것만 &quot;스택에 들어있다&quot;고 치고, 그 위는 신경 쓰지 않는 식입니다.</p>
<hr>
<h3 id="콜-스택call-stack-영역">콜 스택(Call stack) 영역</h3>
<p>콜 스택 영역은 이 스택 방식을 똑같이 사용합니다. 
프로그램이 켜지면 제일 먼저 <code>main()</code> 함수가 스택에 쏙 들어갑니다(Push).</p>
<p>프로그램을 실행하다가 <strong>새로운 함수를 부르면, 그 함수도 스택 맨 위에 층층이 쌓입니다(Push)</strong>. 그리고 그 <strong>함수가 일을 다 마치면 스택에서 빠져나옵니다(Pop)</strong>. 따라서 현재 스택에 쌓여있는 함수들을 밑에서부터 쭉 훑어보면, 지금 실행 중인 곳까지 오기 위해 어떤 함수들을 거쳐왔는지 한눈에 알 수 있습니다.</p>
<p>우편함 비유를 적용해 볼게요. 여기서 물건을 넣고 빼는 단위는 함수 하나의 정보가 담긴 <strong>&#39;스택 프레임(Stack frame)&#39;</strong>이라는 보따리입니다. 그리고 포스트잇(마커) 역할을 하는 것은 CPU 안에 있는 <strong>&#39;스택 포인터(SP)&#39;</strong>라는 녀석이죠. 스택 포인터는 항상 스택의 맨 꼭대기를 가리키며 &quot;지금 여기까지 찼어!&quot;라고 알려줍니다.</p>
<p>재미있는 점은, 스택에서 함수를 뺄 때(Pop) 굳이 우편함 안의 내용물을 지우개로 빡빡 지우며 청소할 필요가 없다는 것입니다. 그냥 스택 포인터(포스트잇)만 한 칸 아래로 내리면 끝납니다! 어차피 나중에 새 함수가 들어오면 그 자리에 새 정보를 덮어씌울 테니까요. 이렇게 하면 속도가 훨씬 빨라집니다.</p>
<hr>
<h3 id="콜-스택의-작동-방식">콜 스택의 작동 방식</h3>
<p>함수를 부를 때 내부적으로 어떤 일들이 일어나는지 순서대로 쉽게 살펴볼게요.</p>
<ol>
<li>프로그램이 &quot;어, 함수 호출이네!&quot; 하고 알아챕니다.</li>
<li>그 함수를 위한 &#39;스택 프레임(보따리)&#39;을 만들어 스택 맨 위에 올립니다(Push). 
이 보따리 안에는 다음 내용이 들어갑니다:</li>
</ol>
<ul>
<li><strong>돌아갈 주소:</strong> 함수가 끝나고 원래 하던 일로 다시 돌아올 위치를 적어둔 메모지.</li>
<li>함수에 전달할 매개변수들.</li>
<li>함수 안에서만 쓸 지역 변수들을 위한 빈 공간.</li>
</ul>
<ol start="3">
<li>이제 프로그램은 방금 부른 함수의 시작점으로 점프해서 들어갑니다.</li>
<li>함수 안의 코드들을 열심히 실행합니다.</li>
<li>함수가 끝에 도달하면 다음 일들이 벌어집니다:</li>
</ol>
<ul>
<li>스택 프레임을 스택에서 빼냅니다(Pop). 이러면 함수 안에서 쓰던 지역 변수들도 자연스럽게 공간을 반납하게 됩니다.</li>
<li>함수가 남긴 결과값(Return value)을 챙깁니다.</li>
<li>아까 보따리에 적어둔 &#39;돌아갈 주소&#39;를 보고 원래 하던 곳으로 무사히 점프해서 돌아갑니다.</li>
</ul>
<blockquote>
<p><strong>참고로...</strong>
컴퓨터 종류에 따라 스택이 메모리 주소 0번지부터 점점 큰 숫자로 쌓이는 기계도 있고, 반대로 큰 숫자에서 0번지 쪽으로 거꾸로 쌓이는 기계도 있습니다. 아주 깊은 기술적인 내용이니 &quot;그렇구나~&quot; 하고 넘어가셔도 괜찮습니다.</p>
</blockquote>
<hr>
<h3 id="간단한-콜-스택-예제">간단한 콜 스택 예제</h3>
<p>아주 간단한 코드로 콜 스택이 어떻게 변하는지 볼까요?</p>
<pre><code class="language-cpp">int foo(int x)
{
    // b
    return x;
} // foo 함수가 여기서 콜 스택에서 빠집니다 (pop)

int main()
{
    // a
    foo(5); // foo 함수가 여기서 콜 스택에 쌓입니다 (push)
    // c

    return 0;
}
</code></pre>
<p>코드 안의 a, b, c 위치에서 콜 스택의 모습은 다음과 같습니다:</p>
<p><strong>a 위치:</strong></p>
<blockquote>
<p>main()</p>
</blockquote>
<p><strong>b 위치:</strong></p>
<blockquote>
<p>foo() (매개변수 x 포함)
main()</p>
</blockquote>
<p><strong>c 위치:</strong></p>
<blockquote>
<p>main()</p>
</blockquote>
<hr>
<h3 id="스택-오버플로우stack-overflow">스택 오버플로우(Stack overflow)</h3>
<p>스택은 무한정 넓은 곳이 아니라 크기가 정해져 있습니다. 윈도우(Visual Studio)에서는 기본적으로 겨우 <strong>1MB</strong>밖에 안 되고, 유닉스 환경에서도 <strong>8MB</strong> 정도입니다. 만약 이 좁은 스택에 너무 많은 정보를 구겨 넣으려고 하면 <strong>스택 오버플로우(Stack overflow, 스택이 넘침)</strong>라는 끔찍한 에러가 발생합니다!</p>
<p>주로 스택에 너무 큰 배열을 만들려고 하거나, 함수가 함수를 부르고 또 부르는 과정(꼬리에 꼬리를 무는 호출)이 너무 깊어질 때 발생합니다. 스택이 넘쳐버리면 운영체제는 화를 내며 프로그램을 강제로 종료시켜 버립니다(뻗어버리는 거죠).</p>
<p>아래 코드는 스택을 꽉 채워 터지게 만드는 예제입니다. 직접 실행해 보면 프로그램이 어떻게 죽는지 볼 수 있어요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int stack[10000000];
    std::cout &lt;&lt; &quot;hi&quot; &lt;&lt; stack[0]; // 컴파일러가 배열을 맘대로 없애버리지 못하게 stack[0]을 억지로 써줍니다.

    return 0;
}
</code></pre>
<p>이 프로그램은 스택이라는 좁은 방에 대략 40MB짜리 엄청나게 큰 배열을 우겨 넣으려고 합니다. 스택이 이걸 감당하지 못해서, 결국 허락되지 않은 다른 메모리 구역까지 침범하다가 프로그램이 강제 종료됩니다. 윈도우에서는 &quot;접근 위반(access violation)&quot;이라는 에러 코드를 뱉고, &quot;hi&quot;라는 글자는 출력조차 되지 않습니다.</p>
<p>스택이 터지는 또 다른 흔한 이유도 볼까요?</p>
<pre><code class="language-cpp">// h/t 카운터 추가 아이디어를 주신 독자 yellowEmu님께 감사드립니다.
#include &lt;iostream&gt;

int g_counter{ 0 };

void eatStack()
{
    std::cout &lt;&lt; ++g_counter &lt;&lt; &#39; &#39;;

    // 컴파일러가 무한 반복이라고 경고하는 걸 막기 위해 조건문을 하나 달아줍니다.
    if (g_counter &gt; 0)
        eatStack(); // 주목: eatStack() 함수가 자기 자신을 또 부릅니다!

    // 컴파일러가 멋대로 코드를 최적화하는 것을 막기 위한 용도입니다.
    std::cout &lt;&lt; &quot;hi&quot;;
}

int main()
{
    eatStack();

    return 0;
}
</code></pre>
<p>이 프로그램은 <code>eatStack()</code> 함수가 실행될 때마다 스택에 새로운 보따리(스택 프레임)를 올립니다. 그런데 이 함수가 자기 자신을 부르고, 불려온 애가 또 자기를 부르고... 끝없이 자기를 부르기 때문에 결국 스택 메모리가 꽉 차서 넘쳐(오버플로우) 버립니다.</p>
<blockquote>
<p><strong>참고 노트</strong>
원작자가 윈도우 10 컴퓨터에서 테스트해 보았을 때, 디버그 모드에서는 <code>eatStack()</code>이 4,848번 불린 후 프로그램이 뻗었고, 릴리스 모드에서는 128,679번 불린 후에 뻗었다고 하네요!</p>
</blockquote>
<blockquote>
<p><strong>관련 컨텐츠</strong>
자기 자신을 계속해서 부르는 함수에 대해서는 다음 강의인 <strong>20.3 - 재귀(Recursion)</strong>에서 더 자세히 다룰 예정입니다!</p>
</blockquote>
<p>정리하자면, 스택은 다음과 같은 장단점이 있습니다:</p>
<ul>
<li><strong>장점:</strong> 메모리를 빌리고 반납하는 속도가 굉장히 빠릅니다.</li>
<li><strong>장점:</strong> 스택에 들어간 변수는 알아서 관리됩니다. 함수가 끝나서 스택에서 빠질 때 자동으로 싹 청소됩니다.</li>
<li><strong>장점:</strong> 컴파일할 때 크기가 다 정해져서, 변수 이름으로 빠르고 직접적으로 접근할 수 있습니다.</li>
<li><strong>주의점:</strong> 스택은 크기가 매우 작습니다! 그래서 엄청나게 큰 배열이나 무거운 데이터를 스택에 넣는 것은 절대 권장하지 않습니다.</li>
</ul>
<blockquote>
<p><strong>참고 노트</strong>
이 코멘트를 보시면 스택에 있는 변수들이 어떻게 배치되고, 실제로 어떻게 주소를 받는지에 대한 추가적이고 조금 더 쉬운 설명이 있습니다.</p>
</blockquote>
<hr>
<h2 id="203--재귀-recursion">20.3 — 재귀 (Recursion)</h2>
<p>C++에서 <strong>재귀 함수(recursive function)</strong>란 아주 간단히 말해 &#39;자기 자신을 다시 부르는(호출하는) 함수&#39;를 뜻해요. 마치 거울 속에 거울이 있는 것처럼요! 먼저, 조금 잘못 만들어진 재귀 함수 예제를 하나 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void countDown(int count)
{
    std::cout &lt;&lt; &quot;push &quot; &lt;&lt; count &lt;&lt; &#39;\n&#39;;
    countDown(count-1); // countDown() 함수가 자기 자신을 다시 부릅니다 (재귀 호출)
}

int main()
{
    countDown(5);

    return 0;
}
</code></pre>
<p>이 프로그램에서 <code>countDown(5)</code>가 실행되면 화면에 &quot;push 5&quot;가 출력되고, 함수 안에서 <code>countDown(4)</code>를 또 부릅니다. 그러면 <code>countDown(4)</code>는 &quot;push 4&quot;를 출력하고 <code>countDown(3)</code>을 부르겠죠. 이런 식으로 <code>countDown(n)</code>이 <code>countDown(n-1)</code>을 부르는 과정이 끝없이 반복됩니다. 사실상 무한 루프(영원히 끝나지 않는 반복문)와 똑같은 상태가 되는 거예요.</p>
<p>함수가 호출될 때마다 컴퓨터는 &#39;콜 스택(call stack)&#39;이라는 메모리 공간에 정보를 쌓아둡니다. 하지만 위 예제에서는 <code>countDown()</code> 함수가 끝을 맺지 못하고 계속 자기 자신을 부르기만 하니까, 쌓인 정보가 스택에서 빠져나갈(pop) 기회가 전혀 없게 됩니다!</p>
<p>결국 어느 순간 컴퓨터는 스택 메모리를 모두 다 써버리게 되고, 이를 <strong>스택 오버플로우(stack overflow)</strong>라고 부릅니다. 이 상태가 되면 프로그램은 에러를 내며 튕기거나 강제로 종료돼요. 원본 글의 작성자 컴퓨터에서는 프로그램이 종료되기 전까지 무려 -11732까지 카운트다운을 했다고 하네요!</p>
<blockquote>
<p><strong>참고 노트</strong>
테일 콜(Tail call, 꼬리 호출)이란 함수의 맨 끝(꼬리 부분)에서 일어나는 함수 호출을 말합니다. 컴파일러(코드를 컴퓨터 언어로 번역해 주는 프로그램)는 이런 재귀적인 테일 콜을 반복문(재귀가 아닌 형태)으로 쉽게 최적화할 수 있어요. 만약 최적화가 적용되었다면 위 예제에서도 시스템 메모리가 부족해지는 일이 발생하지 않았을 겁니다. 위 코드를 직접 실행해 봤는데 에러 없이 영원히 실행된다면, 컴파일러가 똑똑하게 코드를 최적화했기 때문일 가능성이 높습니다.</p>
</blockquote>
<hr>
<h3 id="재귀-종료-조건">재귀 종료 조건</h3>
<p>재귀 함수는 일반적인 함수와 거의 똑같이 작동해요. 하지만 방금 본 예제에서 알 수 있듯, 재귀 함수를 쓸 때 가장 중요하게 기억해야 할 차이점이 있습니다. 바로 <strong>재귀 종료 조건</strong>을 반드시 만들어 주어야 한다는 점이에요. 그렇지 않으면 메모리가 바닥날 때까지 영원히 실행되니까요.</p>
<p>종료 조건이란 쉽게 말해 <strong>&quot;특정 상황이 되면 자기 자신을 그만 부르고 멈춰라!&quot;</strong>라고 알려주는 규칙입니다.</p>
<p>종료 조건은 보통 <code>if</code> 문을 사용해서 만듭니다. 아까 보았던 함수에 종료 조건을 추가해서 새롭게 고쳐볼게요 (출력 내용도 조금 추가했습니다).</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void countDown(int count)
{
    std::cout &lt;&lt; &quot;push &quot; &lt;&lt; count &lt;&lt; &#39;\n&#39;;

    if (count &gt; 1) // 종료 조건: count가 1보다 클 때만 자기 자신을 부릅니다.
        countDown(count-1);

    std::cout &lt;&lt; &quot;pop &quot; &lt;&lt; count &lt;&lt; &#39;\n&#39;;
}

int main()
{
    countDown(5);
    return 0;
}
</code></pre>
<p>이제 이 프로그램을 실행하면, 제일 먼저 다음과 같이 출력됩니다.</p>
<pre><code>push 5
push 4
push 3
push 2
push 1
</code></pre><p>이 순간, 컴퓨터의 콜 스택(메모리가 쌓여 있는 모습)을 들여다본다면 다음과 같이 함수들이 차곡차곡 쌓여 있을 거예요.</p>
<pre><code>countDown(1)
countDown(2)
countDown(3)
countDown(4)
countDown(5)
main()
</code></pre><p>이번에는 종료 조건(<code>if (count &gt; 1)</code>)이 있기 때문에, <code>countDown(1)</code>은 <code>countDown(0)</code>을 부르지 않아요! 조건에 맞지 않아 <code>if</code> 문 안의 코드가 실행되지 않고, 바로 &quot;pop 1&quot;을 출력한 뒤 함수가 성공적으로 끝납니다.</p>
<p>함수가 끝났으니 <code>countDown(1)</code>은 스택에서 빠져나가고(pop), 프로그램의 흐름은 자신을 불렀던 <code>countDown(2)</code>로 돌아갑니다. <code>countDown(2)</code>는 멈췄던 지점부터 다시 실행되어 &quot;pop 2&quot;를 출력하고 또 끝이 납니다. 이렇게 차례대로 스택에서 빠져나가면서 모든 <code>countDown</code> 함수가 메모리에서 지워질 때까지 이 과정이 반복돼요.</p>
<p>결과적으로 이 프로그램은 다음과 같은 전체 결과를 출력합니다.</p>
<pre><code>push 5
push 4
push 3
push 2
push 1
pop 1
pop 2
pop 3
pop 4
pop 5
</code></pre><p>여기서 &quot;push&quot;는 자기 자신을 부르기 <em>전</em>에 실행되니까 숫자가 정방향(5부터 1)으로 나오고, &quot;pop&quot;은 함수가 끝나고 스택에서 빠져나올 때(들어간 순서의 역순으로 빠져나옴) 실행되니까 숫자가 역방향(1부터 5)으로 나온다는 점을 눈여겨보세요!</p>
<hr>
<h3 id="조금-더-쓸만한-예제">조금 더 쓸만한 예제</h3>
<p>재귀 호출이 어떻게 작동하는지 기본 원리를 알아봤으니, 이번에는 조금 더 흔하게 쓰이는 실용적인 재귀 함수를 살펴볼까요?</p>
<pre><code class="language-cpp">// 1부터 sumto까지의 모든 정수를 더한 값을 반환합니다 (1과 sumto 포함)
// 음수를 넣으면 0을 반환합니다.
int sumTo(int sumto)
{
    if (sumto &lt;= 0)
        return 0; // 기본 케이스 (종료 조건): 사용자가 0이나 음수 같은 예상치 못한 값을 넣었을 때
    if (sumto == 1)
        return 1; // 일반적인 기본 케이스 (종료 조건)

    return sumTo(sumto - 1) + sumto; // 재귀 함수 호출
}
</code></pre>
<p>재귀 프로그램은 코드만 눈으로 딱 보고 어떻게 돌아가는지 이해하기가 꽤 어려워요. 이럴 때는 특정 숫자를 직접 넣었을 때 어떤 일이 일어나는지 차근차근 따라가 보는 것이 가장 좋습니다. <code>sumto</code>에 5를 넣었다고 상상해 볼까요?</p>
<ol>
<li><code>sumTo(5)</code> 호출: 5 &lt;= 1은 거짓이므로, <code>sumTo(4) + 5</code>를 반환하려고 합니다.</li>
<li><code>sumTo(4)</code> 호출: 4 &lt;= 1은 거짓이므로, <code>sumTo(3) + 4</code>를 반환하려고 합니다.</li>
<li><code>sumTo(3)</code> 호출: 3 &lt;= 1은 거짓이므로, <code>sumTo(2) + 3</code>을 반환하려고 합니다.</li>
<li><code>sumTo(2)</code> 호출: 2 &lt;= 1은 거짓이므로, <code>sumTo(1) + 2</code>를 반환하려고 합니다.</li>
<li><code>sumTo(1)</code> 호출: 1 &lt;= 1은 참이므로, <strong>1</strong>을 반환합니다. 드디어 종료 조건을 만났네요!</li>
</ol>
<p>이제 함수가 차례대로 결과를 돌려주며 콜 스택에서 하나씩 빠져나옵니다(unwind).</p>
<ul>
<li><code>sumTo(1)</code>은 1을 반환합니다.</li>
<li><code>sumTo(2)</code>는 <code>sumTo(1) + 2</code>를 계산하니까, 1 + 2 = 3을 반환합니다.</li>
<li><code>sumTo(3)</code>은 <code>sumTo(2) + 3</code>을 계산하니까, 3 + 3 = 6을 반환합니다.</li>
<li><code>sumTo(4)</code>는 <code>sumTo(3) + 4</code>를 계산하니까, 6 + 4 = 10을 반환합니다.</li>
<li><code>sumTo(5)</code>는 <code>sumTo(4) + 5</code>를 계산하니까, 10 + 5 = <strong>15</strong>를 반환합니다.</li>
</ul>
<p>이렇게 풀어놓고 보니 1부터 넘겨받은 숫자 5까지 모든 수를 더한다는 게 훨씬 이해하기 쉽죠?
재귀 함수는 직관적으로 이해하기 어렵기 때문에, <strong>좋은 주석(설명글)을 달아두는 것이 아주 중요합니다.</strong></p>
<p>참고로 위 코드에서는 <code>--sumto</code> 대신 <code>sumto - 1</code>이라는 값을 사용해 재귀 호출을 했어요. 왜냐하면 <code>--</code> 기호(값을 1 줄이는 연산자)는 변수의 실제 값 자체를 바꿔버리는 &#39;부작용(side effect)&#39;이 있기 때문이에요. 하나의 수식 안에서 값이 변하는 변수를 여러 번 쓰면 프로그램이 예측 불가능한 이상한 행동(정의되지 않은 동작)을 할 수 있습니다. <code>sumto - 1</code>을 쓰면 변수의 원래 값은 그대로 둔 채 1을 뺀 새로운 값만 전달할 수 있어서 안전하답니다.</p>
<hr>
<h3 id="재귀-알고리즘">재귀 알고리즘</h3>
<p>재귀 함수는 보통 큰 문제를 먼저 작게 쪼개서 푼 다음(재귀적으로), 그 작은 해답들을 조합해서 전체의 정답을 찾아내는 방식으로 작동합니다. 위에서 본 알고리즘에서도 <code>sumTo(value)</code>를 구하기 위해, 먼저 더 작은 문제인 <code>sumTo(value-1)</code>을 풀고 거기에 <code>value</code>를 더해서 정답을 알아냈죠.</p>
<p>이런 재귀 알고리즘들 중에는 너무 쉬워서 계산할 필요도 없는 입력값들이 있습니다. 예를 들어 <code>sumTo(1)</code>은 그냥 1이죠 (머릿속으로 1초 만에 알 수 있잖아요!). 이렇게 더 이상 재귀를 할 필요 없이 바로 정답이 나오는 단순한 입력값을 <strong>기본 케이스(base case)</strong>라고 부릅니다. 이 기본 케이스들이 바로 재귀를 멈춰주는 &#39;종료 조건&#39; 역할을 한답니다. 기본 케이스는 보통 입력값이 0, 1, 빈 문자열(&quot;&quot;), 혹은 데이터가 없는 상태(null)일 때 나타나는 경우가 많아요.</p>
<hr>
<h3 id="피보나치-수열">피보나치 수열</h3>
<p>수학에서 가장 유명한 재귀 알고리즘 중 하나가 바로 &#39;피보나치 수열&#39;입니다. 피보나치 수열은 나뭇가지가 뻗어나가는 모양, 조개껍데기의 나선형, 파인애플 껍질의 무늬, 솔방울의 구조 등 자연 속 아주 많은 곳에서 발견할 수 있어요.</p>
<p>피보나치 나선형의 그림은 다음과 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/439274ff-bba1-4652-9ea2-8ecb0dff624c/image.webp" alt=""></p>
<p>나선형 안에 그려진 사각형들의 한 변의 길이를 보면 피보나치 수열의 숫자들과 정확히 일치합니다.</p>
<p>피보나치 수는 수학적으로 이렇게 정의돼요.</p>
<p>$$F(n) =
\begin{cases}
0 &amp; \text{if } n = 0 \
1 &amp; \text{if } n = 1 \
F(n-1) + F(n-2) &amp; \text{if } n &gt; 1
\end{cases}$$</p>
<p>규칙이 수학적으로 정해져 있다 보니, n번째 피보나치 수를 계산하는 재귀 함수를 코드(효율적이지는 않지만)로 짜는 것은 무척 쉽습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int fibonacci(int count)
{
    if (count == 0)
        return 0; // 기본 케이스 (종료 조건)
    if (count == 1)
        return 1; // 기본 케이스 (종료 조건)

    return fibonacci(count-1) + fibonacci(count-2);
}

// 처음 13개의 피보나치 수를 보여주는 메인 프로그램
int main()
{
    for (int count { 0 }; count &lt; 13; ++count)
        std::cout &lt;&lt; fibonacci(count) &lt;&lt; &#39; &#39;;

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 다음과 같은 결과가 나옵니다.</p>
<pre><code>0 1 1 2 3 5 8 13 21 34 55 89 144
</code></pre><p>방금 그림에서 본 피보나치 나선형 다이어그램의 숫자들과 완전히 똑같죠?</p>
<hr>
<h3 id="메모이제이션-알고리즘">메모이제이션 알고리즘</h3>
<p>방금 본 피보나치 재귀 알고리즘은 사실 효율이 썩 좋지 않아요. 기본 케이스가 아닐 때마다 함수 안에서 피보나치 함수를 &#39;두 번씩&#39;이나 더 부르기 때문이죠. 이렇게 되면 함수 호출 횟수가 기하급수적으로 늘어납니다 (실제로 위 코드는 <code>fibonacci()</code> 함수를 무려 1205번이나 불렀답니다!).</p>
<p>이런 불필요한 호출을 줄이는 멋진 기술이 있는데, 그중 하나가 바로 
<strong>메모이제이션(memoization)</strong>이에요. 메모이제이션은 복잡하고 오래 걸리는 계산 결과를 &#39;캐시(메모리장)&#39;에 슬쩍 저장해 두었다가, 똑같은 입력값이 다시 들어오면 귀찮게 또 계산하지 않고 저장해 둔 정답을 바로 꺼내주는 기법입니다.</p>
<p>캐시를 적용한(메모이즈된) 피보나치 코드를 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

// 이 코드의 변형을 제공해준 potterman28wxcv에게 감사를 표합니다 (h/t)
// std::vector를 좀 더 쉽게 다루기 위해 count를 std::size_t 타입으로 바꿨습니다.
int fibonacci(std::size_t count)
{
    // 계산된 결과를 저장(캐시)해 둘 static std::vector를 만듭니다.
    static std::vector results{ 0, 1 };

    // 만약 이미 계산해 본 적 있는 count라면, 저장해둔 결과를 그대로 씁니다!
    if (count &lt; std::size(results))
        return results[count];

    // 계산해 본 적이 없다면 새로 결과를 계산해서 목록에 추가합니다.
    results.push_back(fibonacci(count - 1) + fibonacci(count - 2));
    return results[count];
}

// 처음 13개의 피보나치 수를 보여주는 메인 프로그램
int main()
{
    for (int count { 0 }; count &lt; 13; ++count)
        std::cout &lt;&lt; fibonacci(static_cast&lt;std::size_t&gt;(count)) &lt;&lt; &#39; &#39;;

    return 0;
}
</code></pre>
<p>이렇게 메모이제이션을 적용하면 함수를 딱 35번만 부르고도 똑같은 결과를 낼 수 있어요. 
원래 1205번 불렀던 것에 비하면 엄청난 발전이죠!</p>
<hr>
<h3 id="재귀-vs-반복">재귀 vs 반복</h3>
<p>초보자분들이 재귀 함수를 배울 때 가장 많이 하는 질문이 있어요. <strong>&quot;for 문이나 while 문 같은 반복문(iterative)을 쓰면 다 똑같이 할 수 있는데, 왜 굳이 어렵게 재귀 함수를 쓰나요?&quot;</strong></p>
<p>맞아요, 재귀로 풀 수 있는 문제는 항상 반복문으로도 풀 수 있습니다. 하지만 복잡하고 까다로운 문제일수록 재귀를 쓰는 쪽이 코드를 작성하기도, 읽기도 훨씬 간결하고 쉬운 경우가 많습니다. 예를 들어, 피보나치수열을 반복문으로 만들 수는 있지만 재귀로 만드는 것보다는 조금 더 골치가 아픕니다! (직접 한 번 시도해 보세요!)</p>
<p>속도 면에서는 반복문(<code>for</code> 루프나 <code>while</code> 루프)이 거의 항상 재귀 함수보다 빠르고 효율적입니다. 함수를 새로 부를 때마다 메모리(스택)를 쌓았다 지웠다 하는 숨은 작업(오버헤드)이 필요한데, 반복문은 그런 과정을 겪지 않기 때문이에요.</p>
<p>그렇다고 반복문이 언제나 정답이라는 뜻은 아닙니다. 코드를 읽기 쉽고 관리하기 편하게(유지 보수) 만드는 게 더 중요할 때가 있거든요. 재귀를 쓰면 코드가 훨씬 깔끔해져서, 약간의 성능을 희생하더라도 이득을 볼 때가 많습니다. (물론 정답을 찾기 위해 함수가 수만 번씩 불려야 하는 극단적인 상황이 아니라면요!)</p>
<p>보통 다음 조건들 중 대부분이 맞을 때 재귀를 사용하는 것이 좋습니다.</p>
<ul>
<li>재귀로 코드를 짜는 게 (반복문보다) 훨씬 간단할 때.</li>
<li>재귀가 너무 깊이 들어가지 않을 거라는 보장이 있을 때 (예: 함수가 10만 번씩 연달아 자기 자신을 부를 일이 없을 때).</li>
<li>반복문으로 만들 경우 데이터를 저장할 복잡한 스택 구조를 직접 만들어 관리해야 할 때.</li>
<li>성능이 0.001초 단위로 중요한 핵심 코드가 아닐 때.</li>
</ul>
<p>하지만 재귀로 짜는 게 더 쉽다면, 일단 재귀로 먼저 코드를 완성한 다음 나중에 성능을 위해 반복문으로 최적화하는 것도 좋은 방법입니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
재귀가 정말로 딱 들어맞는 상황이 아니라면, 일반적으로는 재귀보다는 반복문을 우선적으로 사용하는 것이 좋습니다.</p>
</blockquote>
<hr>
<h2 id="204--명령줄-인수">20.4 — 명령줄 인수</h2>
<h3 id="명령줄-인수가-필요한-이유">명령줄 인수가 필요한 이유</h3>
<p>이전 레슨(0.5 컴파일러, 링커, 라이브러리 소개)에서 배웠듯이, 코드를 컴파일하고 링크하면 실제로 실행할 수 있는 파일이 하나 만들어집니다. 이 프로그램 파일이 실행되면 언제나 <code>main()</code>이라는 이름의 함수부터 코드가 시작되죠. 우리는 지금까지 <code>main</code> 함수를 다음과 같은 모양으로 써왔습니다.</p>
<p><code>int main()</code></p>
<p>여기서 괄호 <code>()</code> 안이 텅 비어 있다는 점에 주목해 보세요. 이 <code>main()</code> 함수는 시작할 때 아무런 입력값(매개변수)도 받지 않겠다는 뜻입니다. 하지만 많은 프로그램은 일을 시작하기 위해 <strong>어떤 재료(입력값)</strong>를 필요로 합니다.</p>
<p>예를 들어, 큰 이미지 파일을 읽어서 작고 귀여운 썸네일(미리보기 이미지)로 만들어 주는 <code>Thumbnail</code>이라는 프로그램을 만든다고 가정해 볼게요. 이 프로그램은 도대체 &#39;어떤 이미지 파일&#39;을 열어봐야 할지 어떻게 알 수 있을까요? 당연히 사용자가 프로그램에게 어떤 파일을 열지 알려줄 수 있는 방법이 있어야 합니다. 이를 위해 보통은 다음과 같은 방식으로 코드를 짭니다.</p>
<pre><code class="language-cpp">// Program: Thumbnail
#include &lt;iostream&gt;
#include &lt;string&gt;

int main()
{
    std::cout &lt;&lt; &quot;Please enter an image filename to create a thumbnail for: &quot;; // 썸네일을 만들 이미지 파일 이름을 입력하세요:
    std::string filename{};
    std::cin &gt;&gt; filename;

    // open image file (이미지 파일 열기)
    // create thumbnail (썸네일 생성하기)
    // output thumbnail (썸네일 출력하기)
}
</code></pre>
<p>하지만 이 방식에는 큰 단점이 숨어 있습니다. 프로그램을 실행할 때마다 사용자가 직접 키보드로 파일 이름을 타이핑할 때까지 <strong>프로그램이 멍하니 멈춰서 기다린다는 것</strong>입니다. 
물론 여러분이 까만 명령창(터미널)에서 손으로 딱 한 번만 실행한다면 별문제가 아닐 겁니다. 하지만 여러 개의 파일을 한꺼번에 처리하고 싶거나, 내가 만든 프로그램이 아닌 다른 프로그램이 이 프로그램을 자동으로 실행하게 만들고 싶을 때는 아주 불편해집니다.</p>
<p>이 상황들을 조금 더 자세히 파헤쳐 볼까요?</p>
<p>어떤 폴더 안에 있는 모든 이미지 파일의 썸네일을 한꺼번에 다 만들고 싶다고 생각해 봅시다. 방금 짠 코드대로라면 폴더 안의 이미지 개수만큼 프로그램을 여러 번 실행하고, 매번 파일 이름을 일일이 타이핑해야 합니다. 이미지가 수백 개라면 하루 종일 키보드만 두드려야 할지도 몰라요! 이럴 때는 폴더 안의 모든 파일 이름을 확인해서, 각 파일마다 <code>Thumbnail</code> 프로그램을 한 번씩 <strong>자동으로 실행해 주는 프로그램</strong>을 만드는 것이 훨씬 현명한 해결책입니다.</p>
<p>이번에는 웹사이트를 운영하는 경우를 생각해 봅시다. 사용자가 웹사이트에 이미지를 올릴 때마다 웹사이트가 자동으로 썸네일을 만들게 하고 싶습니다. 그런데 우리가 만든 프로그램은 인터넷에서 입력을 받도록 만들어지지 않았죠. 그렇다면 사용자가 업로드할 때 프로그램에 파일 이름을 어떻게 전달할 수 있을까요? 이럴 때는 웹 서버가 이미지 업로드가 끝나면 자동으로 <code>Thumbnail</code> 프로그램을 켜주는 것이 좋습니다.</p>
<p>결국 위 두 가지 경우 모두, 프로그램이 일단 켜진 후에 사용자가 직접 파일 이름을 쳐주기를 기다리는 방식은 좋지 않습니다. 그보다는 외부 프로그램이 <code>Thumbnail</code>을 <strong>처음 켜는 바로 그 순간에 파일 이름이라는 재료(입력 데이터)를 한 번에 쏙 넣어주는 방법</strong>이 필요합니다.</p>
<p><strong>명령줄 인수(Command line arguments)</strong>란, 프로그램이 처음 시작될 때 운영체제(윈도우, 맥 등)가 프로그램 안으로 쏙 던져주는 추가적인 &#39;문자열(글자) 정보&#39;를 말합니다. 프로그램은 이 정보를 입력값으로 받아서 쓸 수도 있고, 필요 없으면 무시할 수도 있습니다. 마치 함수가 다른 함수에게 매개변수를 넘겨주듯이, 명령줄 인수는 <strong>사람이나 다른 프로그램이 어떤 프로그램에게 시작부터 입력값을 쥐여주는 방법</strong>이라고 생각하면 이해하기 쉽습니다.</p>
<hr>
<h3 id="명령줄-인수-전달하기">명령줄 인수 전달하기</h3>
<p>우리가 만든 실행 프로그램은 까만 명령창(터미널)에서 파일 이름을 타이핑해서 직접 실행할 수 있습니다. 예를 들어, 윈도우 컴퓨터의 현재 폴더에 있는 <code>WordCount</code>라는 실행 파일을 켜려면 이렇게 입력합니다.</p>
<p><code>WordCount</code></p>
<p>리눅스나 맥(Unix 기반 OS)에서는 보통 이렇게 입력하죠.</p>
<p><code>./WordCount</code></p>
<p>이 <code>WordCount</code> 프로그램에 명령줄 인수를 전달하는 방법은 아주 간단합니다. 
그냥 <strong>프로그램 이름 뒤에 띄어쓰기를 하고 원하는 인수를 쭉 적어주면 됩니다.</strong></p>
<p><code>WordCount Myfile.txt</code></p>
<p>이제 <code>WordCount</code> 프로그램이 실행될 때, <code>Myfile.txt</code>라는 글자가 명령줄 인수로서 프로그램에 쏙 들어갑니다. 스페이스바(띄어쓰기)를 기준으로 여러 개의 인수를 전달하는 것도 가능합니다.</p>
<p><code>WordCount Myfile.txt Myotherfile.txt</code></p>
<p>만약 명령창이 아니라 코드를 짜는 프로그램(IDE)에서 실행 버튼을 눌러 테스트하고 있다면, IDE 안에도 명령줄 인수를 입력할 수 있는 설정 창이 따로 있습니다.</p>
<ul>
<li><strong>Microsoft Visual Studio의 경우:</strong> 솔루션 탐색기에서 프로젝트 이름을 마우스 오른쪽 버튼으로 클릭한 뒤, <code>속성(Properties)</code>을 선택하세요. <code>구성 속성(Configuration Properties)</code> 아래의 <code>디버깅(Debugging)</code> 메뉴를 엽니다. 오른쪽 화면을 보면 <code>명령 인수(Command Arguments)</code>라는 칸이 있습니다. 거기에 테스트할 인수를 적어두면, 실행 버튼을 누를 때마다 그 인수가 자동으로 프로그램에 전달됩니다.</li>
<li><strong>Code::Blocks의 경우:</strong> 위쪽 메뉴에서 <code>Project(프로젝트) -&gt; Set program’s arguments(프로그램 인수 설정)</code>를 선택하면 됩니다.</li>
</ul>
<hr>
<h3 id="명령줄-인수-사용하기">명령줄 인수 사용하기</h3>
<p>자, 밖에서 프로그램 안으로 인수를 어떻게 던져주는지는 알게 되었습니다. 이제 해야 할 일은 C++ 코드 안에서 그 인수를 잘 받아서 사용하는 것이겠죠? 그러기 위해서 우리는 지금까지 썼던 텅 빈 <code>main()</code>과는 조금 다른 모양의 <code>main()</code> 함수를 써야 합니다. 이 새로운 <code>main()</code> 함수는 두 개의 매개변수를 받는데, C++ 세상의 관습에 따라 보통 <strong><code>argc</code></strong>와 <strong><code>argv</code></strong>라는 이름을 씁니다.</p>
<p><code>int main(int argc, char* argv[])</code></p>
<p>가끔 코드를 보다 보면 다음과 같이 쓰인 것도 볼 수 있습니다.</p>
<p><code>int main(int argc, char** argv)</code></p>
<p>컴퓨터 입장에서는 이 두 가지 코드가 완전히 똑같이 작동합니다. 하지만 첫 번째 방식이 눈으로 볼 때 배열(목록)이라는 느낌이 확 들어서 초보자가 직관적으로 이해하기 훨씬 쉽습니다. 
그래서 보통 첫 번째 방식을 더 많이 사용합니다.</p>
<ul>
<li><strong><code>argc</code></strong>: 이 프로그램에 전달된 <strong>인수의 총 개수</strong>를 담고 있는 정수입니다. (쉽게 외우는 꿀팁: <strong>arg</strong>ument <strong>c</strong>ount의 줄임말입니다.) 사용자가 아무런 인수를 안 줬더라도 <code>argc</code>는 무조건 최소 1입니다. 왜냐하면 &#39;실행되는 프로그램의 이름 자기 자신&#39;이 항상 첫 번째 인수로 들어가기 때문입니다. 사용자가 뒤에 인수를 하나씩 추가해서 적어줄 때마다 이 <code>argc</code> 숫자도 1씩 늘어납니다.</li>
<li><strong><code>argv</code></strong>: 실제로 전달된 <strong>인수 값(글자들)이 들어 있는 곳</strong>입니다. (꿀팁: <strong>arg</strong>ument <strong>v</strong>alues의 줄임말이라고 생각하세요. 정확한 명칭은 &#39;argument vectors&#39;입니다.) <code>char* argv[]</code>라는 선언 부분이 초보자에게는 살짝 무시무시하게 생겼지만, 전혀 겁먹을 필요 없습니다. <code>argv</code>는 그냥 <strong>여러 개의 글자 뭉치(문자열)를 줄지어 담아놓은 배열(목록)</strong>일 뿐입니다. 
이 목록에 몇 개의 글자 뭉치가 들어있는지가 바로 <code>argc</code>인 것이죠.</li>
</ul>
<p>전달받은 모든 명령줄 인수를 화면에 출력해 주는 <code>MyArgs</code>라는 짧은 프로그램을 직접 만들어 봅시다.</p>
<pre><code class="language-cpp">// Program: MyArgs
#include &lt;iostream&gt;

int main(int argc, char* argv[])
{
    std::cout &lt;&lt; &quot;There are &quot; &lt;&lt; argc &lt;&lt; &quot; arguments:\n&quot;; // 인수가 총 argc개 있습니다:

    // Loop through each argument and print its number and value
    // (각 인수를 하나씩 차례대로 확인하면서 번호와 실제 값을 출력합니다)
    for (int count{ 0 }; count &lt; argc; ++count)
    {
        std::cout &lt;&lt; count &lt;&lt; &#39; &#39; &lt;&lt; argv[count] &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}
</code></pre>
<p>이제 명령창에서 <code>Myfile.txt</code>와 <code>100</code>이라는 인수를 달아서 프로그램(<code>MyArgs</code>)을 실행해 보면, 화면에 다음과 같이 출력됩니다.</p>
<pre><code class="language-text">There are 3 arguments:
0 C:\MyArgs
1 Myfile.txt
2 100
</code></pre>
<p>보시다시피 0번 인수는 현재 실행 중인 프로그램의 전체 경로와 이름입니다. 1번과 2번 인수가 바로 우리가 직접 뒤에 적어서 넘겨준 두 개의 명령줄 인수입니다.</p>
<blockquote>
<p><strong>참고로...</strong>
여기서 <code>argv</code> 배열을 하나씩 꺼내 볼 때 C++의 편리한 기능인 &#39;범위 기반 for문(range-based for-loop)&#39;을 사용할 수 없습니다. <code>argv</code>는 크기 정보가 깎여나간 옛날 C언어 스타일의 배열이기 때문에 작동하지 않거든요. 따라서 위 코드처럼 숫자를 세며 도는 고전적인 <code>for</code>문을 사용해야 합니다.</p>
</blockquote>
<hr>
<h3 id="숫자로-된-인수-다루기">숫자로 된 인수 다루기</h3>
<p>아주 중요한 사실이 하나 있습니다. 명령줄 인수는 우리가 <code>100</code>처럼 숫자 형태를 적어서 넘겨주더라도, <strong>프로그램 안으로는 무조건 &#39;문자열(글자)&#39; 형태로 들어옵니다.</strong> 즉 계산기처럼 숫자 <code>100</code>으로 인식하는 게 아니라 글자 <code>&quot;100&quot;</code>으로 인식한다는 뜻입니다.</p>
<p>그래서 명령줄 인수를 진짜 숫자처럼 계산에 쓰고 싶다면, 이 글자를 숫자로 바꿔주는 변환 과정이 반드시 필요합니다. 안타깝게도 C++에서는 이 과정이 초보자가 느끼기에 조금 복잡하게 느껴질 수 있습니다.</p>
<p>C++에서 글자를 숫자로 변환하는 대표적인 방법은 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;sstream&gt; // for std::stringstream (std::stringstream 기능을 사용하기 위해 필요합니다)
#include &lt;string&gt;

int main(int argc, char* argv[])
{
    if (argc &lt;= 1)
    {
        // On some operating systems, argv[0] can end up as an empty string instead of the program&#39;s name.
        // We&#39;ll conditionalize our response on whether argv[0] is empty or not.
        // (일부 운영체제에서는 프로그램 이름이 들어와야 할 argv[0]이 빈칸으로 들어올 수도 있습니다. 
        // 따라서 argv[0]이 비어있는지 아닌지에 따라 화면에 보여줄 안내문을 다르게 만듭니다.)
        if (argv[0])
            std::cout &lt;&lt; &quot;Usage: &quot; &lt;&lt; argv[0] &lt;&lt; &quot; &lt;number&gt;&quot; &lt;&lt; &#39;\n&#39;; // 올바른 사용법: (프로그램 이름) &lt;숫자&gt;
        else
            std::cout &lt;&lt; &quot;Usage: &lt;program name&gt; &lt;number&gt;&quot; &lt;&lt; &#39;\n&#39;; // 올바른 사용법: &lt;프로그램 이름&gt; &lt;숫자&gt;

        return 1;
    }

    std::stringstream convert{ argv[1] }; // set up a stringstream variable named convert, initialized with the input from argv[1]
    // (첫 번째 인수로 들어온 글자 argv[1]을 집어넣어, &#39;convert&#39;라는 이름의 stringstream 변수를 만듭니다.)

    int myint{};
    if (!(convert &gt;&gt; myint)) // do the conversion (변환을 시도해 봅니다)
        myint = 0; // if conversion fails, set myint to a default value (만약 숫자가 아닌 글자라서 변환에 실패하면, 기본값인 0으로 설정합니다)

    std::cout &lt;&lt; &quot;Got integer: &quot; &lt;&lt; myint &lt;&lt; &#39;\n&#39;; // 정수 획득 성공: (숫자)

    return 0;
}
</code></pre>
<p>이 프로그램에 <code>567</code>이라는 입력을 같이 주어 실행하면 다음과 같이 나옵니다.</p>
<pre><code class="language-text">Got integer: 567
</code></pre>
<p>여기서 사용된 <code>std::stringstream</code>이라는 마법의 도구는 우리가 키보드로 입력을 받을 때 쓰는 <code>std::cin</code>과 작동 방식이 거의 똑같습니다. <code>std::cin</code>이 키보드 입력을 쭉 빨아들여서 숫자로 바꿔주듯이, 여기서는 <code>argv[1]</code>에 들어있는 글자(&quot;567&quot;)를 가져다 놓고 <code>&gt;&gt;</code> 기호를 사용해 쏙 뽑아내어 정수형 변수인 <code>myint</code>에 진짜 숫자로 저장하는 원리입니다.</p>
<p><code>std::stringstream</code>에 대해서는 뒤에 나오는 챕터에서 훨씬 더 자세히 다룰 예정이니, 지금은 &#39;아, 글자를 숫자로 바꿀 때 쓰는 도구구나&#39; 정도로만 이해하셔도 충분합니다.</p>
<hr>
<h3 id="운영체제가-명령줄-인수를-먼저-분석합니다">운영체제가 명령줄 인수를 먼저 분석합니다</h3>
<p>명령창에 무언가를 타이핑해서 엔터를 치거나 IDE에서 프로그램을 실행할 때, 여러분의 명령을 제일 먼저 받아서 처리하는 것은 바로 운영체제(윈도우, 맥, 리눅스 등)입니다. 운영체제는 단순히 실행 파일을 더블클릭해서 켜주는 역할만 하는 게 아닙니다. 여러분이 띄어쓰기로 적어놓은 인수들을 어떻게 잘라서 프로그램에 넘겨줄지 먼저 <strong>분석(파싱)</strong>하는 아주 중요한 역할을 합니다.</p>
<p>일반적으로 운영체제들은 큰따옴표(<code>&quot;</code>)나 역슬래시(<code>\</code>) 같은 특수 문자를 만나면 자기만의 특별한 규칙을 적용합니다.</p>
<p>예를 들어 다음과 같이 띄어쓰기를 해서 입력하면:</p>
<p><code>MyArgs Hello world!</code></p>
<p>운영체제는 띄어쓰기를 기준으로 싹둑싹둑 자르기 때문에 아래처럼 두 개의 다른 인수로 들어갑니다.</p>
<pre><code class="language-text">There are 3 arguments:
0 C:\MyArgs
1 Hello
2 world!
</code></pre>
<p>하지만, 띄어쓰기가 있더라도 큰따옴표(<code>&quot;</code>)로 묶어주면 운영체제는 &#39;아, 이건 떨어져 있어도 하나의 덩어리구나!&#39;라고 똑똑하게 인식합니다.</p>
<p><code>MyArgs &quot;Hello world!&quot;</code></p>
<p>결과는 다음과 같이 띄어쓰기가 포함된 하나의 덩어리로 잘 나옵니다.</p>
<pre><code class="language-text">There are 2 arguments:
0 C:\MyArgs
1 Hello world!
</code></pre>
<p>그렇다면 큰따옴표 기호(<code>&quot;</code>) 그 자체를 글자로 넘겨주고 싶을 때는 어떻게 할까요? 대부분의 운영체제에서는 큰따옴표 앞에 <strong>역슬래시(<code>\</code>)</strong>를 붙여서 &#39;이건 특수 기능이 아니라 그냥 일반 글자 큰따옴표야!&#39;라고 알려줄 수 있습니다.</p>
<p><code>MyArgs \&quot;Hello world!\&quot;</code></p>
<p>이렇게 치면 앞뒤에 큰따옴표가 고스란히 달린 채로 글자가 잘 전달됩니다.</p>
<pre><code class="language-text">There are 3 arguments:
0 C:\MyArgs
1 &quot;Hello
2 world!&quot;
</code></pre>
<p>이 밖에도 여러분이 어떤 운영체제를 쓰느냐에 따라 특수문자를 일반 글자처럼 전달하기 위해(이스케이프 처리) 역슬래시를 붙여야 하는 문자가 더 있을 수 있습니다.</p>
<hr>
<h3 id="결론-1">결론</h3>
<p>명령줄 인수는 프로그램이 딱 켜지는 바로 그 순간에 사용자나 다른 프로그램이 아주 간편하게 입력 데이터를 던져줄 수 있는 훌륭한 방법입니다.</p>
<p>여러분이 앞으로 프로그램을 만들 때 시작하자마자 꼭 필요한 재료(데이터)가 있다면, 그것을 명령줄 인수로 받도록 설계해 보세요. 그리고 만약 깜빡하고 명령줄 인수를 안 넣고 실행했다면? 그땐 프로그램 안에서 스스로 알아채고 &quot;파일 이름을 입력해주세요~&quot;라고 물어보게끔 코드를 짜면 됩니다. 이렇게 두 가지 방식을 잘 섞어 쓰면, 수백 개 파일을 자동으로 돌릴 때도 편하고 직접 하나씩 실행할 때도 친절한 일석이조의 프로그램을 만들 수 있습니다.</p>
<hr>
<h2 id="205--생략-부호ellipsis와-이를-피해야-하는-이유">20.5 — 생략 부호(Ellipsis)와 이를 피해야 하는 이유</h2>
<p>지금까지 우리가 배운 모든 함수들은, 함수가 받을 &#39;재료(매개변수)&#39;의 개수를 미리 정확히 알고 있어야 했습니다. (기본값을 설정해 두었더라도 말이죠.) 하지만, 상황에 따라 <strong>함수에 원하는 개수만큼 마음대로 재료를 넘겨주고 싶을 때</strong>가 있습니다. C++에서는 이를 가능하게 해주는 특별한 기능인 <strong>생략 부호(ellipsis, <code>...</code>)</strong>를 제공합니다.</p>
<p>생략 부호는 실제로 잘 사용되지 않으며, 잠재적으로 위험할 수 있어서 가급적 사용하지 않는 것을 권장합니다. 따라서 이 섹션의 내용은 가볍게 읽고 넘어가셔도 좋습니다.</p>
<p>생략 부호를 사용하는 함수는 다음과 같이 생겼습니다.</p>
<pre><code class="language-cpp">반환_타입 함수_이름(매개변수_목록, ...)
</code></pre>
<p>여기서 <code>매개변수_목록</code>은 우리가 평소에 쓰는 일반적인 매개변수들입니다. 중요한 점은, <strong>생략 부호를 쓰려면 반드시 한 개 이상의 일반 매개변수가 앞에 있어야 한다</strong>는 것입니다. 함수에 전달된 값들은 먼저 일반 매개변수들의 자리를 채우게 됩니다.</p>
<p>점 세 개(<code>...</code>)로 표현되는 생략 부호는 <strong>반드시 함수의 맨 마지막 매개변수</strong>로 적어야 합니다. 이 생략 부호는 앞의 일반 매개변수들이 다 채워지고 남은 &#39;추가 재료&#39;들을 모두 쓸어 담는 역할을 합니다. 정확한 표현은 아니지만, 초보자분들은 이 생략 부호를 <strong>남은 매개변수들을 몽땅 담아두는 하나의 바구니(배열)</strong>라고 생각하시면 이해하기 쉽습니다.</p>
<hr>
<h3 id="생략-부호ellipsis-예제">생략 부호(Ellipsis) 예제</h3>
<p>생략 부호를 배우는 가장 좋은 방법은 직접 코드를 보는 것입니다! 여러 개의 정수를 받아서 평균을 구해주는 간단한 프로그램을 만들어 보겠습니다. 코드는 다음과 같습니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cstdarg&gt; // 생략 부호를 사용하기 위해 꼭 필요합니다!

// 생략 부호(...)는 반드시 마지막 매개변수여야 합니다.
// count는 우리가 뒤에 추가로 몇 개의 숫자를 더 넘겨줄 것인지 알려줍니다.
double findAverage(int count, ...)
{
    int sum{ 0 };

    // 생략 부호에 담긴 값들을 꺼내 보려면 va_list라는 특별한 도구가 필요합니다.
    std::va_list list;

    // va_start를 사용해 va_list를 준비시킵니다.
    // 첫 번째 인자는 방금 만든 list이고,
    // 두 번째 인자는 생략 부호 바로 앞에 있던 마지막 일반 매개변수(여기선 count)입니다.
    va_start(list, count);

    // 생략 부호에 담긴 값의 개수만큼 반복문을 돌립니다.
    for (int arg{ 0 }; arg &lt; count; ++arg)
    {
         // va_arg를 사용해 생략 부호 바구니에서 값을 하나씩 꺼냅니다.
         // 첫 번째 인자는 우리가 사용하는 list이고,
         // 두 번째 인자는 우리가 꺼낼 값의 &#39;타입(자료형)&#39;입니다.
         sum += va_arg(list, int);
    }

    // 작업이 다 끝났으면 va_list를 정리(청소)해 줍니다.
    va_end(list);

    return static_cast&lt;double&gt;(sum) / count;
}

int main()
{
    // 5개의 숫자를 추가로 넘깁니다: 1, 2, 3, 4, 5
    std::cout &lt;&lt; findAverage(5, 1, 2, 3, 4, 5) &lt;&lt; &#39;\n&#39;;

    // 6개의 숫자를 추가로 넘깁니다: 1, 2, 3, 4, 5, 6
    std::cout &lt;&lt; findAverage(6, 1, 2, 3, 4, 5, 6) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같이 출력됩니다:</p>
<pre><code class="language-text">3
3.5
</code></pre>
<p>보시다시피, 이 함수는 매번 다른 개수의 숫자들을 받아들일 수 있습니다! 자, 이제 이 마법 같은 일이 어떻게 일어나는지 하나씩 뜯어볼까요?</p>
<ol>
<li>가장 먼저 <code>&lt;cstdarg&gt;</code> 헤더 파일을 포함해야 합니다. 이 파일 안에는 생략 부호에 숨겨진 값들을 꺼내 쓰는 데 필요한 핵심 도구들(<code>va_list</code>, <code>va_arg</code>, <code>va_start</code>, <code>va_end</code>)이 들어있습니다.</li>
<li>그런 다음 함수를 선언합니다. 일반 매개변수가 최소 하나는 있어야 하죠. 여기서는 <code>count</code>라는 정수를 받아서, 뒤에 몇 개의 숫자가 더 따라올지 미리 알려줍니다. 그리고 맨 마지막에 <code>...</code>을 적습니다.</li>
<li>주의할 점은 생략 부호 매개변수에는 이름이 없다는 것입니다! 대신, 우리는 <code>va_list</code>라는 특별한 타입을 사용해 그 안의 값들에 접근합니다. 초보자분들은 <code>va_list</code>를 <strong>생략 부호 바구니 안의 숫자들을 차례대로 가리키는 요술 손가락(포인터)</strong>이라고 생각하시면 좋습니다. 우리는 이 손가락의 이름을 간단히 <code>list</code>라고 지었습니다.</li>
<li>이제 <code>va_start()</code>를 불러서 이 요술 손가락이 첫 번째 추가 숫자를 가리키게 준비시킵니다.</li>
<li>현재 손가락이 가리키는 값을 실제로 꺼내오려면 <code>va_arg()</code>를 씁니다. 이때 어떤 타입(예: <code>int</code>)으로 꺼낼지 알려줘야 합니다. 재미있는 점은, <strong><code>va_arg()</code>로 값을 하나 꺼내면, 요술 손가락이 알아서 다음 숫자를 가리키도록 옆으로 이동</strong>한다는 것입니다!</li>
<li>모든 작업이 끝나면 <code>va_end()</code>를 불러서 사용했던 도구를 깔끔하게 정리해 줍니다.</li>
</ol>
<p>참고로, 처음부터 다시 값들을 읽고 싶다면 언제든지 <code>va_start()</code>를 다시 불러서 손가락을 맨 처음 위치로 되돌릴 수 있습니다.</p>
<hr>
<h3 id="생략-부호가-위험한-이유-첫-번째-타입-검사type-checking가-무시됩니다">생략 부호가 위험한 이유 첫 번째: 타입 검사(Type checking)가 무시됩니다</h3>
<p>생략 부호는 매개변수 개수를 마음대로 조절할 수 있는 엄청난 자유를 줍니다. 하지만, 큰 자유에는 큰 책임이 따르는 법이죠.</p>
<p>일반적으로 C++ 컴파일러는 아주 꼼꼼한 선생님 같습니다. 함수가 정수(<code>int</code>)를 원하는데 여러분이 문자열(<code>string</code>)을 넘겨주면, &quot;타입이 안 맞잖아!&quot;라며 경고를 주고 실행을 막아줍니다. 이를 &#39;타입 검사&#39;라고 합니다.</p>
<p>하지만 <strong>생략 부호 안으로 들어가는 값들에 대해서는 컴파일러가 이 검사를 아예 포기해 버립니다.</strong> 즉, 아무 타입이나 막 집어넣을 수 있다는 뜻입니다. 문제는, 여러분이 실수로 말도 안 되는 값을 넣어도 컴파일러가 아무런 경고를 해주지 않는다는 것입니다. 오직 함수를 호출하는 사람(여러분)이 알아서 올바른 값을 넣었기를 기도해야 합니다. 당연히 실수가 발생하기 아주 쉽겠죠?</p>
<p>아주 미묘하지만 치명적인 실수 예시를 보겠습니다:</p>
<pre><code class="language-cpp">// 두 번째 숫자(첫 번째 추가 숫자)로 정수 1이 아니라 소수점 1.0(double)을 넣었습니다!
std::cout &lt;&lt; findAverage(6, 1.0, 2, 3, 4, 5, 6) &lt;&lt; &#39;\n&#39;;
</code></pre>
<p>언뜻 보면 별문제가 없어 보입니다. 컴파일도 완벽하게 됩니다. 하지만 결과는 아주 충격적입니다:</p>
<pre><code class="language-text">1.78782e+008
</code></pre>
<p>무려 1억 7천만이 넘는 엄청나게 큰 쓰레기 값이 나왔습니다. 도대체 무슨 일이 일어난 걸까요?</p>
<p>컴퓨터는 모든 데이터를 0과 1의 조합(비트)으로 저장합니다. 변수의 &#39;타입&#39;은 컴퓨터에게 이 0과 1들을 어떻게 해석할지 알려주는 설명서와 같습니다. 그런데 앞서 말했듯, 생략 부호는 이 &#39;타입 설명서&#39;를 갖다 버립니다! 그래서 값을 제대로 꺼내려면 <code>va_arg()</code>에게 &quot;이번에 꺼낼 값은 <code>int</code>야!&quot;라고 수동으로 알려줘야 합니다.</p>
<p>위 코드에서 우리는 함수 안에서 <code>va_arg(list, int)</code>를 쓰며 모든 값이 <code>int</code>(정수)일 것이라고 찰떡같이 믿고 있었습니다. 그런데 우리가 넣은 <code>1.0</code>은 <code>double</code>(실수) 타입이고 크기가 8바이트입니다. 반면 <code>int</code>는 4바이트죠.
결과적으로 <code>va_arg</code>는 8바이트짜리 실수 데이터의 절반(4바이트)만 뚝 잘라서 정수로 잘못 해석해 버리고, 다음번엔 나머지 절반을 또 정수로 해석합니다. 데이터가 완전히 꼬여버려서 &#39;쓰레기 값&#39;이 나오게 된 것입니다.</p>
<p>심지어 아래처럼 말도 안 되는 코드를 짜도 컴파일러는 불평 한마디 안 합니다:</p>
<pre><code class="language-cpp">int value{ 7 };
// 소수점, 문자열, 문자, 변수의 주소, 함수의 주소 등 온갖 잡동사니를 다 넣었습니다.
std::cout &lt;&lt; findAverage(6, 1.0, 2, &quot;Hello, world!&quot;, &#39;G&#39;, &amp;value, &amp;findAverage) &lt;&lt; &#39;\n&#39;;
</code></pre>
<p>믿기 어렵겠지만, 이 코드도 정상적으로 컴파일되며 작성자의 컴퓨터에서는 <code>1.79766e+008</code>이라는 쓰레기 값을 내뱉었습니다.</p>
<p>컴퓨터 공학에는 <strong>&quot;쓰레기가 들어가면 쓰레기가 나온다 (Garbage in, garbage out)&quot;</strong>라는 유명한 말이 있습니다. 컴퓨터는 바보 같아서, 사람이 아무리 터무니없는 데이터를 줘도 의심 없이 처리하고 터무니없는 결과를 돌려준다는 뜻이죠. 생략 부호가 딱 이렇습니다.</p>
<hr>
<h3 id="생략-부호가-위험한-이유-두-번째-매개변수가-몇-개나-전달되었는지-모릅니다">생략 부호가 위험한 이유 두 번째: 매개변수가 몇 개나 전달되었는지 모릅니다</h3>
<p>생략 부호는 값의 &#39;타입&#39;만 버리는 게 아니라, <strong>값이 &#39;몇 개&#39;나 들어왔는지도 잊어버립니다.</strong> 그래서 우리가 직접 몇 개의 값이 들어왔는지 추적하는 방법을 만들어내야 합니다. 보통 다음 3가지 방법 중 하나를 사용합니다.</p>
<hr>
<h3 id="방법-1-길이개수를-매개변수로-전달하기">방법 1: 길이(개수)를 매개변수로 전달하기</h3>
<p>첫 번째 방법은, 우리가 앞서 <code>findAverage()</code> 예제에서 했던 것처럼 앞쪽 일반 매개변수로 &quot;내가 추가로 O개의 값을 더 줄게!&quot;라고 미리 개수를 알려주는 것입니다.</p>
<p>하지만 이 방법도 완벽하진 않습니다. 아래 코드를 볼까요?</p>
<pre><code class="language-cpp">// &quot;6개 줄게!&quot; 라고 해놓고 실제로는 5개(1, 2, 3, 4, 5)만 줬습니다.
std::cout &lt;&lt; findAverage(6, 1, 2, 3, 4, 5) &lt;&lt; &#39;\n&#39;;
</code></pre>
<p>이 코드를 실행하면 <code>699773</code> 같은 이상한 값이 나옵니다. 왜 그럴까요?
우리는 함수에게 6개를 달라고 했지만 5개밖에 주지 않았습니다. 함수는 5개까지는 잘 꺼내지만, 6번째 값을 찾기 위해 컴퓨터 메모리의 엉뚱한 공간(스택)을 뒤져서 아무 쓰레기 값이나 가져와 버렸기 때문입니다.</p>
<p>더 잡기 힘든 실수도 있습니다:</p>
<pre><code class="language-cpp">// &quot;6개 줄게!&quot; 라고 해놓고 실수로 7개(1, 2, 3, 4, 5, 6, 7)를 줬습니다.
std::cout &lt;&lt; findAverage(6, 1, 2, 3, 4, 5, 6, 7) &lt;&lt; &#39;\n&#39;;
</code></pre>
<p>이 결과는 <code>3.5</code>가 나옵니다. 얼핏 맞게 나온 것 같지만, 사실 함수는 6번째 숫자인 6까지만 계산하고 맨 마지막에 있는 <strong>7은 완전히 무시</strong>해 버렸습니다. 이런 종류의 실수는 버그를 찾아내기가 정말 힘듭니다.</p>
<hr>
<h3 id="방법-2-보초값sentinel-value-사용하기">방법 2: 보초값(Sentinel value) 사용하기</h3>
<p>두 번째 방법은 끝을 알리는 특별한 표지판, 즉 &#39;보초값&#39;을 사용하는 것입니다. 문자열의 끝을 알리는 널 종료 문자(<code>\0</code>)를 생각하시면 됩니다. 생략 부호에서는 보통 맨 마지막 값으로 이 보초값을 줍니다. 끝을 알리는 표지판으로 <code>-1</code>을 사용하는 예제를 살펴봅시다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cstdarg&gt; 

// 생략 부호는 마지막에 와야 합니다.
double findAverage(int first, ...)
{
    // 첫 번째 숫자는 따로 처리해 줍니다.
    int sum{ first };

    std::va_list list;

    // 첫 번째 매개변수(first)를 기준으로 va_list를 초기화합니다.
    va_start(list, first);

    int count{ 1 };
    // 무한 반복을 돕니다.
    while (true)
    {
        // 값을 하나 꺼냅니다.
        int arg{ va_arg(list, int) };

        // 만약 꺼낸 값이 우리가 정한 보초값(-1)이라면 반복문을 탈출합니다!
        if (arg == -1)
            break;

        sum += arg;
        ++count;
    }

    va_end(list);

    return static_cast&lt;double&gt;(sum) / count;
}

int main()
{
    // 맨 마지막에 끝을 알리는 -1을 꼭 넣어줍니다.
    std::cout &lt;&lt; findAverage(1, 2, 3, 4, 5, -1) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; findAverage(1, 2, 3, 4, 5, 6, -1) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이제 첫 번째 매개변수로 &quot;몇 개를 줄 건지&quot; 미리 말하지 않아도 됩니다. 대신 끝에 보초값만 잘 꽂아주면 되죠.</p>
<p>하지만 여기에도 단점이 있습니다.
첫째, C++ 규칙상 생략 부호 앞에는 일반 매개변수가 꼭 하나 있어야 합니다. 그래서 평균을 낼 첫 번째 숫자를 일반 매개변수(<code>first</code>)로 억지로 빼내어 따로 처리해야 하는 번거로움이 생겼습니다.
둘째, 사용자가 <strong>맨 끝에 보초값 넣는 것을 깜빡하면</strong> 함수는 -1이 우연히 나올 때까지 메모리를 끝없이 헤집고 다니며 무한 루프를 돌거나 프로그램이 팅겨버립니다.
셋째, 만약 우리가 음수(-1)까지 포함해서 평균을 내고 싶다면 어떻게 될까요? <code>-1</code>을 더 이상 끝을 알리는 신호로 쓸 수 없게 됩니다. 보초값은 &#39;내가 계산하려는 실제 데이터 중에는 절대 나올 수 없는 값&#39;으로 정해야만 안전합니다.</p>
<hr>
<h3 id="방법-3-해독-문자열decoder-string-사용하기">방법 3: 해독 문자열(Decoder string) 사용하기</h3>
<p>세 번째 방법은, 마치 암호문 같은 &quot;해독 문자열&quot;을 넘겨주어 프로그램에게 매개변수의 개수와 타입을 친절하게 알려주는 방식입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;
#include &lt;cstdarg&gt;

// 첫 번째 매개변수로 해독 문자열(decoder)을 받습니다.
double findAverage(std::string_view decoder, ...)
{
    double sum{ 0 };

    std::va_list list;
    va_start(list, decoder);

    // 해독 문자열의 글자를 하나씩 확인합니다.
    for (auto codetype: decoder)
    {
        switch (codetype)
        {
        case &#39;i&#39;: // 글자가 &#39;i&#39;이면 정수(int)로 꺼냅니다.
            sum += va_arg(list, int);
            break;

        case &#39;d&#39;: // 글자가 &#39;d&#39;이면 실수(double)로 꺼냅니다.
            sum += va_arg(list, double);
            break;
        }
    }

    va_end(list);

    // 해독 문자열의 길이(std::size)가 곧 매개변수의 개수입니다.
    return sum / std::size(decoder);
}

int main()
{
    // &quot;iiiii&quot; -&gt; 5개의 정수(i)가 뒤따라온다는 뜻입니다.
    std::cout &lt;&lt; findAverage(&quot;iiiii&quot;, 1, 2, 3, 4, 5) &lt;&lt; &#39;\n&#39;;

    // &quot;iiiiii&quot; -&gt; 6개의 정수(i)가 뒤따라옵니다.
    std::cout &lt;&lt; findAverage(&quot;iiiiii&quot;, 1, 2, 3, 4, 5, 6) &lt;&lt; &#39;\n&#39;;

    // &quot;iiddi&quot; -&gt; 정수, 정수, 실수, 실수, 정수 순서로 온다는 뜻입니다!
    std::cout &lt;&lt; findAverage(&quot;iiddi&quot;, 1, 2, 3.5, 4.5, 5) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 방식의 가장 큰 장점은 여러 가지 다양한 타입(정수, 실수 등)을 섞어서 받을 수 있다는 것입니다. 하지만 암호문 문자열을 작성하는 게 조금 복잡해 보일 수 있고, 만약 여러분이 실수로 암호문과 실제 데이터를 다르게 적는다면 여전히 끔찍한 에러가 발생합니다.</p>
<p>(C 언어를 배워보신 분이라면 눈치채셨겠지만, 여러분이 흔히 쓰던 <code>printf</code> 함수가 정확히 이 방식(<code>%d</code>, <code>%f</code> 등)을 사용합니다!)</p>
<hr>
<h3 id="생략-부호를-더-안전하게-사용하는-권장-사항">생략 부호를 더 안전하게 사용하는 권장 사항</h3>
<ol>
<li><strong>가장 좋은 방법은 생략 부호를 아예 쓰지 않는 것입니다!</strong> 조금 번거롭더라도 더 안전한 다른 대안들이 많습니다. 예를 들어, 위의 평균 구하기 예제라면 개수를 마음대로 늘렸다 줄일 수 있는 동적 배열(<code>std::vector</code> 같은 것)을 넘겨주는 것이 훨씬 좋습니다. 그러면 컴파일러가 강력한 타입 검사를 해줘서 이상한 값이 들어오는 걸 막아주면서도, 원하는 개수만큼 데이터를 넘길 수 있습니다.</li>
<li>부득이하게 생략 부호를 꼭 써야 한다면, <strong>모든 추가 매개변수의 타입을 똑같이 통일</strong>하는 것이 좋습니다. (전부 다 <code>int</code>로 하거나 전부 다 <code>double</code>로 하는 식으로요.) 정수와 실수를 막 섞어 쓰면 타입이 꼬여서 쓰레기 값이 나올 확률이 폭발적으로 늘어납니다.</li>
<li>개수를 직접 알려주거나 해독 문자열을 쓰는 방식(방법 1, 3)이 보초값을 쓰는 방식(방법 2)보다 그나마 안전합니다. 적어도 사용자가 횟수를 명확히 정해주기 때문에, 잘못된 값이 들어가더라도 무한 루프에 빠지는 대참사는 막을 수 있기 때문입니다.</li>
</ol>
<blockquote>
<p><strong>고급 학습자를 위한 내용</strong>
기존 생략 부호의 단점을 개선하기 위해, C++11에서는 <strong>매개변수 팩(parameter packs)</strong>과 <strong>가변 인자 템플릿(variadic templates)</strong>이라는 기능을 도입했습니다. 이는 생략 부호처럼 개수가 자유로우면서도 강력한 타입 검사를 지원합니다. 하지만 사용법이 너무 어려워 널리 쓰이지는 못했습니다.
이후 C++17에서 <strong>폴드 표현식(fold expressions)</strong>이 추가되면서 매개변수 팩의 사용성이 크게 개선되었고, 현재는 생략 부호를 대체할 아주 실용적인 선택지가 되었습니다.
이 사이트의 향후 업데이트에서 이 멋진 주제들에 대한 강의를 추가할 예정입니다.</p>
</blockquote>
<hr>
<h2 id="206--람다익명-함수-소개">20.6 — 람다(익명 함수) 소개</h2>
<p>18.3 레슨(표준 라이브러리 알고리즘 소개)에서 다루었던 다음 코드를 한번 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

// 요소가 일치하면 이 함수는 true를 반환합니다.
bool containsNut(std::string_view str)
{
    // std::string_view::find는 부분 문자열을 찾지 못하면 std::string_view::npos를 반환합니다.
    // 찾았다면 str 안에서 해당 부분 문자열이 시작하는 인덱스(위치)를 반환합니다.
    return str.find(&quot;nut&quot;) != std::string_view::npos;
}

int main()
{
    constexpr std::array&lt;std::string_view, 4&gt; arr{ &quot;apple&quot;, &quot;banana&quot;, &quot;walnut&quot;, &quot;lemon&quot; };

    // 배열을 훑어보면서 &quot;nut&quot;이라는 글자가 포함된 요소가 있는지 확인합니다.
    auto found{ std::find_if(arr.begin(), arr.end(), containsNut) };

    if (found == arr.end())
    {
        std::cout &lt;&lt; &quot;No nuts\n&quot;;
    }
    else
    {
        std::cout &lt;&lt; &quot;Found &quot; &lt;&lt; *found &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}
</code></pre>
<p>이 코드는 문자열 배열을 검색해서 &quot;nut&quot;이라는 글자가 들어간 첫 번째 단어를 찾습니다. 실행하면 다음과 같은 결과가 나옵니다.</p>
<pre><code class="language-text">Found walnut
</code></pre>
<p>코드가 잘 작동하긴 하지만, 좀 더 깔끔하게 고칠 수 있습니다.
가장 큰 문제는 <code>std::find_if</code> 함수를 사용할 때 <strong>함수 포인터</strong>를 넘겨주어야 한다는 점입니다. 겨우 딱 한 번 쓰고 말 함수인데도 굳이 이름을 지어줘야 하고, 함수 안에 함수를 만들 수 없으니 코드 맨 바깥쪽(전역 범위)에 덩그러니 만들어야 하죠. 게다가 함수 내용이 너무 짧아서, 함수 이름이나 주석을 읽는 것보다 그냥 코드 한 줄을 보는 게 이해하기 더 빠를 정도입니다.</p>
<hr>
<h3 id="람다는-이름이-없는-익명-함수입니다">람다는 이름이 없는 익명 함수입니다</h3>
<p><strong>람다 표현식</strong>(간단히 <strong>람다</strong> 또는 <strong>클로저</strong>라고도 부릅니다)을 사용하면 함수 안에서 &#39;이름이 없는 함수&#39;를 뚝딱 만들 수 있습니다. 이렇게 함수 안에 함수를 중첩해서 만들면 이름이 겹쳐서 코드가 지저분해지는 것(이름 공간 오염)을 막을 수 있고, 함수를 실제로 사용하는 곳 바로 옆에 정의할 수 있어서 코드를 읽기 훨씬 편해집니다.</p>
<p>C++에서 람다 문법은 처음 보면 굉장히 특이하게 생겨서 적응할 시간이 조금 필요합니다. 람다는 다음과 같은 모양을 가집니다.</p>
<pre><code class="language-cpp">[ 캡처_블록 ] ( 매개변수 ) -&gt; 반환_타입
{
    실행할_코드;
}
</code></pre>
<ul>
<li><strong>캡처 블록 (Capture clause):</strong> 바깥쪽 변수를 가져다 쓸 필요가 없다면 비워두어도 됩니다 <code>[]</code>.</li>
<li><strong>매개변수 (Parameters):</strong> 함수에 전달할 값이 없다면 비워둘 수 있습니다 <code>()</code>. 반환 타입을 따로 적지 않는다면 괄호 자체를 아예 생략할 수도 있습니다.</li>
<li><strong>반환 타입 (Return type):</strong> 생략 가능합니다. 생략하면 컴파일러가 <code>auto</code>처럼 알아서 결과값을 보고 타입을 유추해 줍니다. 일반 함수에서는 반환 타입을 컴파일러에게 맡기는 것을 피하라고 했지만, 람다는 보통 코드가 아주 짧고 단순하기 때문에 컴파일러에게 맡겨도 괜찮습니다.</li>
</ul>
<p>그리고 람다는 이름이 없는(익명) 함수이므로 이름을 지어줄 필요가 전혀 없습니다.</p>
<blockquote>
<p><strong>참고로...</strong>
가장 단순한 형태의 람다는 다음과 같이 생겼습니다.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt;
int main()
{
  [] {}; // 반환 타입 생략, 캡처 없음, 매개변수 생략된 가장 단순한 람다입니다.
&gt;
  return 0;
}</code></pre>
<p>이제 처음 보았던 예제를 람다를 사용해서 다시 써보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

int main()
{
  constexpr std::array&lt;std::string_view, 4&gt; arr{ &quot;apple&quot;, &quot;banana&quot;, &quot;walnut&quot;, &quot;lemon&quot; };

  // 함수가 필요한 바로 그 자리에 함수를 직접 정의합니다.
  auto found{ std::find_if(arr.begin(), arr.end(),
                           [](std::string_view str) // 여기가 람다입니다. 캡처 블록은 비어있습니다.
                           {
                             return str.find(&quot;nut&quot;) != std::string_view::npos;
                           }) };

  if (found == arr.end())
  {
    std::cout &lt;&lt; &quot;No nuts\n&quot;;
  }
  else
  {
    std::cout &lt;&lt; &quot;Found &quot; &lt;&lt; *found &lt;&lt; &#39;\n&#39;;
  }

  return 0;
}
</code></pre>
<p>함수 포인터를 사용했을 때와 완벽하게 똑같이 작동하며, 결과도 동일합니다.
새로 만든 람다를 원래 있던 <code>containsNut</code> 함수와 비교해 보세요. 매개변수와 안의 내용이 완전히 똑같죠? 외부 변수를 쓸 일이 없으니 캡처 블록(<code>[]</code>)은 비워두었고(캡처 블록에 대해서는 다음 레슨에서 자세히 배웁니다), 코드를 짧게 쓰기 위해 반환 타입(<code>-&gt; bool</code>)도 생략했습니다. <code>!=</code> 연산자가 어차피 <code>bool</code>(참/거짓)을 반환하기 때문에 람다도 알아서 <code>bool</code>을 반환하게 됩니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
코드는 항상 가장 좁은 범위에서, 그리고 처음 사용하는 곳과 가장 가까운 곳에 정의하는 것이 좋습니다. 다른 함수에 인자로 넘겨주기 위해 딱 한 번만 사용할 아주 단순한 함수가 필요하다면, 일반 함수를 만드는 것보다 람다를 사용하는 것이 훨씬 좋습니다.</p>
</blockquote>
<hr>
<h3 id="람다의-타입">람다의 타입</h3>
<p>위 예제에서는 람다가 필요한 자리에 곧바로 람다를 적어 넣었습니다. 이렇게 사용하는 것을 <strong>함수 리터럴(function literal)</strong>이라고 부르기도 합니다.</p>
<p>하지만 함수를 호출하는 줄에 람다를 통째로 길게 적으면 코드가 읽기 힘들어질 때가 있습니다. 숫자나 문자를 변수에 미리 저장해두고 나중에 꺼내 쓰는 것처럼, 람다도 변수에 저장해두었다가 나중에 사용할 수 있습니다. 람다에 뜻을 알기 쉬운 이름을 붙여서 변수에 저장해두면 코드를 읽기가 훨씬 편해집니다.</p>
<p>예를 들어, 아래 코드는 배열의 모든 숫자가 짝수인지 확인하는 코드입니다.</p>
<pre><code class="language-cpp">// 나쁨: 이 코드가 무슨 일을 하는지 이해하려면 람다 내부 코드를 직접 읽어봐야 합니다.
return std::all_of(array.begin(), array.end(), [](int i){ return ((i % 2) == 0); });
</code></pre>
<p>이 코드를 변수를 활용해 읽기 쉽게 바꾸면 다음과 같습니다.</p>
<pre><code class="language-cpp">// 좋음: 대신 람다를 이름 있는 변수에 저장하고 그 변수를 함수에 전달합니다.
auto isEven{
  [](int i)
  {
    return (i % 2) == 0;
  }
};

return std::all_of(array.begin(), array.end(), isEven);
</code></pre>
<p>마지막 줄을 소리 내어 읽어보세요. &quot;배열(array)의 모든(all_of) 요소가 짝수인지(isEven) 확인해서 반환하라&quot;처럼 아주 자연스럽게 읽힙니다!</p>
<blockquote>
<p><strong>핵심 통찰</strong>
람다를 변수에 저장하면 람다에 유용한 이름을 붙여줄 수 있어서 코드가 훨씬 읽기 편해집니다. 또한, 람다를 변수에 저장해두면 똑같은 람다를 여러 번 재사용할 수도 있습니다.</p>
</blockquote>
<p>그런데 여기서 질문! 변수 <code>isEven</code>의 데이터 타입(자료형)은 대체 무엇일까요?</p>
<p>놀랍게도 람다에는 우리가 직접 코드에 적을 수 있는 명확한 타입 이름이 없습니다. 우리가 람다를 만들면 컴파일러가 알아서 세상에 단 하나뿐인 고유한 비밀 타입을 만들어냅니다.</p>
<blockquote>
<p><strong>고급 학습자를 위한 내용</strong>
사실 람다는 진짜 함수가 아닙니다 (이것이 C++에서 함수 안에 함수를 못 만드는 규칙을 피해 갈 수 있는 비결이기도 합니다). 람다는 함수처럼 호출해서 쓸 수 있도록 <code>operator()</code>가 오버로딩된 <strong>함수 객체(functor)</strong>라는 특별한 형태의 객체입니다.</p>
</blockquote>
<p>우리가 람다의 진짜 타입 이름은 알 수 없지만, 만든 람다를 변수에 저장하는 방법은 여러 가지가 있습니다. 람다의 캡처 블록이 비어있다면(<code>[]</code> 안에 아무것도 없다면) 일반적인 함수 포인터에 저장할 수 있습니다. 또는 캡처 블록이 비어있지 않더라도 <code>std::function</code>이나 <code>auto</code> 키워드를 사용해 저장할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;functional&gt;

int main()
{
  // 1. 일반 함수 포인터. 캡처 블록이 비어있을 때(빈 [])만 사용할 수 있습니다.
  double (*addNumbers1)(double, double){
    [](double a, double b) {
      return a + b;
    }
  };

  addNumbers1(1, 2);

  // 2. std::function 사용. 캡처 블록이 비어있지 않아도 사용할 수 있습니다 (다음 레슨에서 다룹니다).
  std::function addNumbers2{ // 참고: C++17 이전 버전에서는 std::function&lt;double(double, double)&gt; 라고 써야 합니다.
    [](double a, double b) {
      return a + b;
    }
  };

  addNumbers2(3, 4);

  // 3. auto 사용. 람다의 진짜(숨겨진) 타입 그대로 변수에 저장합니다.
  auto addNumbers3{
    [](double a, double b) {
      return a + b;
    }
  };

  addNumbers3(5, 6);

  return 0;
}
</code></pre>
<p>람다의 &#39;진짜 타입&#39;을 그대로 살려서 저장하는 유일한 방법은 <code>auto</code>를 사용하는 것뿐입니다. 게다가 <code>auto</code>를 사용하면 <code>std::function</code>을 사용할 때 발생하는 미세한 성능 저하(오버헤드)도 피할 수 있습니다.</p>
<p>그렇다면 다른 함수에 람다를 인자로 넘겨주고 싶을 때는 어떻게 해야 할까요? 
여기에는 4가지 방법이 있습니다.</p>
<pre><code class="language-cpp">#include &lt;functional&gt;
#include &lt;iostream&gt;

// 방법 1: `std::function` 매개변수 사용
void repeat1(int repetitions, const std::function&lt;void(int)&gt;&amp; fn)
{
    for (int i{ 0 }; i &lt; repetitions; ++i)
        fn(i);
}

// 방법 2: 타입 템플릿 매개변수를 가진 함수 템플릿 사용
template &lt;typename T&gt;
void repeat2(int repetitions, const T&amp; fn)
{
    for (int i{ 0 }; i &lt; repetitions; ++i)
        fn(i);
}

// 방법 3: 축약된 함수 템플릿 문법 사용 (C++20부터)
void repeat3(int repetitions, const auto&amp; fn)
{
    for (int i{ 0 }; i &lt; repetitions; ++i)
        fn(i);
}

// 방법 4: 함수 포인터 사용 (캡처가 없는 람다에만 가능)
void repeat4(int repetitions, void (*fn)(int))
{
    for (int i{ 0 }; i &lt; repetitions; ++i)
        fn(i);
}

int main()
{
    auto lambda = [](int i)
    {
        std::cout &lt;&lt; i &lt;&lt; &#39;\n&#39;;
    };

    repeat1(3, lambda);
    repeat2(3, lambda);
    repeat3(3, lambda);
    repeat4(3, lambda);

    return 0;
}
</code></pre>
<ul>
<li><strong>방법 1</strong>은 <code>std::function</code>을 사용합니다. 매개변수로 무엇을 받고 무엇을 반환하는지 명확하게 보여서 좋습니다. 하지만 함수를 호출할 때마다 람다를 은근슬쩍 변환해야 해서 미세한 성능 저하가 생깁니다. 코드를 헤더 파일과 <code>.cpp</code> 파일로 나누기 쉽다는 장점도 있습니다.</li>
<li><strong>방법 2</strong>는 템플릿(<code>T</code>)을 사용합니다. 코드가 실행될 때 람다의 진짜 타입에 맞춰 가장 빠르고 효율적으로 작동하지만, 매개변수나 반환 타입을 한눈에 파악하기 어렵다는 단점이 있습니다.</li>
<li><strong>방법 3</strong>은 C++20의 <code>auto</code>를 사용한 방법으로, 내부적으로는 방법 2와 완전히 똑같은 템플릿을 만들어냅니다.</li>
<li><strong>방법 4</strong>는 함수 포인터를 사용합니다. 캡처가 없는 단순한 람다는 함수 포인터로 자동 변환되기 때문에 가능한 방법입니다.</li>
</ul>
<blockquote>
<p><strong>권장 사항</strong>
람다를 변수에 저장할 때는 무조건 <code>auto</code>를 사용하세요.
람다를 다른 함수로 넘겨줄 때는:</p>
<ul>
<li>C++20 이상을 사용 중이라면 매개변수 타입으로 <code>auto</code>를 사용하세요.</li>
<li>그 이전 버전을 사용 중이라면 함수 템플릿이나 <code>std::function</code>을 사용하세요 (캡처가 없는 단순 람다라면 함수 포인터도 괜찮습니다).</li>
</ul>
</blockquote>
<hr>
<h3 id="제네릭-람다">제네릭 람다</h3>
<p>기본적으로 람다의 매개변수는 일반 함수의 매개변수와 똑같이 작동합니다.
하지만 아주 중요한 차이점이 하나 있는데, C++14부터 람다의 매개변수 타입으로 <code>auto</code>를 사용할 수 있다는 점입니다 (참고로 C++20부터는 일반 함수도 매개변수에 <code>auto</code>를 쓸 수 있게 되었습니다).</p>
<p>매개변수에 <code>auto</code>를 적어두면, 람다를 호출할 때 넘겨주는 값을 보고 컴파일러가 알아서 알맞은 데이터 타입을 맞춰줍니다. 이렇게 <code>auto</code> 매개변수를 가져서 어떤 데이터 타입이든 유연하게 받아낼 수 있는 람다를 <strong>제네릭 람다(generic lambdas)</strong>라고 부릅니다.</p>
<blockquote>
<p><strong>고급 학습자를 위한 내용</strong>
람다에서 <code>auto</code>를 사용하는 것은 사실 템플릿 매개변수를 짧게 줄여 쓴 것과 같습니다.</p>
</blockquote>
<p>제네릭 람다를 사용하는 예제를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

int main()
{
  constexpr std::array months{ // C++17 이전에는 std::array&lt;const char*, 12&gt; 라고 써야 합니다.
    &quot;January&quot;,
    &quot;February&quot;,
    &quot;March&quot;,
    &quot;April&quot;,
    &quot;May&quot;,
    &quot;June&quot;,
    &quot;July&quot;,
    &quot;August&quot;,
    &quot;September&quot;,
    &quot;October&quot;,
    &quot;November&quot;,
    &quot;December&quot;
  };

  // 같은 알파벳으로 시작하는 연속된 두 달을 찾습니다.
  const auto sameLetter{ std::adjacent_find(months.begin(), months.end(),
                                      [](const auto&amp; a, const auto&amp; b) {
                                        return a[0] == b[0];
                                      }) };

  // 두 달을 무사히 찾았는지 확인합니다.
  if (sameLetter != months.end())
  {
    // std::next는 sameLetter 바로 다음 요소를 가리킵니다.
    std::cout &lt;&lt; *sameLetter &lt;&lt; &quot; and &quot; &lt;&lt; *std::next(sameLetter)
              &lt;&lt; &quot; start with the same letter\n&quot;;
  }

  return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">June and July start with the same letter
</code></pre>
<p>이 예제에서는 <code>auto</code> 매개변수를 사용해 문자열을 전달받았습니다. 어떤 종류의 문자열이든 대괄호(<code>[]</code>)를 사용해 첫 번째 글자를 가져올 수 있기 때문에, 사용자가 넘겨준 값이 <code>std::string</code>이든 그냥 옛날 방식의 문자열이든 신경 쓸 필요가 없습니다. 덕분에 나중에 <code>months</code> 배열의 데이터 타입을 바꾸더라도 람다 코드는 하나도 고칠 필요가 없습니다.</p>
<p>하지만 <code>auto</code>가 항상 최고의 선택인 것은 아닙니다. 다음 예제를 보세요.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

int main()
{
  constexpr std::array months{ // C++17 이전에는 std::array&lt;const char*, 12&gt; 라고 써야 합니다.
    &quot;January&quot;,
    &quot;February&quot;,
    &quot;March&quot;,
    &quot;April&quot;,
    &quot;May&quot;,
    &quot;June&quot;,
    &quot;July&quot;,
    &quot;August&quot;,
    &quot;September&quot;,
    &quot;October&quot;,
    &quot;November&quot;,
    &quot;December&quot;
  };

  // 알파벳 5글자로 된 달이 몇 개인지 셉니다.
  const auto fiveLetterMonths{ std::count_if(months.begin(), months.end(),
                                       [](std::string_view str) {
                                         return str.length() == 5;
                                       }) };

  std::cout &lt;&lt; &quot;There are &quot; &lt;&lt; fiveLetterMonths &lt;&lt; &quot; months with 5 letters\n&quot;;

  return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">There are 2 months with 5 letters
</code></pre>
<p>이 예제에서 만약 <code>auto</code>를 썼다면 컴파일러는 데이터 타입을 다루기 까다로운 <code>const char*</code>(옛날 C언어 방식 문자열)로 추론했을 것입니다. 길이를 재려면 복잡해지죠. 그래서 여기서는 명시적으로 <code>std::string_view</code> 타입이라고 딱 정해주는 것이 좋습니다. 그러면 사용자가 C언어 방식 문자열을 넘겨주더라도 길이를 쉽게 구할 수(<code>str.length()</code>) 있기 때문입니다.</p>
<hr>
<h3 id="constexpr-람다">Constexpr 람다</h3>
<p>C++17부터 람다가 만들어내는 결과가 상수 표현식(항상 변하지 않는 고정된 값)의 조건을 만족하면 암묵적으로 컴파일 시간에 계산되는 <code>constexpr</code>로 취급됩니다. 
보통 다음 두 가지 조건을 만족해야 합니다.</p>
<ol>
<li>람다에 캡처가 아예 없거나, 캡처하는 모든 것이 <code>constexpr</code>이어야 합니다.</li>
<li>람다 안에서 호출하는 함수들도 모두 <code>constexpr</code>이어야 합니다. 참고로 표준 라이브러리의 많은 알고리즘과 수학 함수들은 C++20이나 C++23이 되어서야 <code>constexpr</code>로 만들어졌습니다.</li>
</ol>
<p>위의 &#39;5글자 달 세기&#39; 예제는 C++17에서는 <code>constexpr</code>이 될 수 없지만, <code>std::count_if</code> 함수가 <code>constexpr</code>로 개선된 C++20부터는 가능해집니다. 즉, C++20부터는 다음과 같이 결과를 컴파일 시간에 미리 계산해둘 수 있습니다.</p>
<pre><code class="language-cpp">constexpr auto fiveLetterMonths{ std::count_if(months.begin(), months.end(),
                                     [](std::string_view str) {
                                       return str.length() == 5;
                                     }) };
</code></pre>
<hr>
<h3 id="제네릭-람다와-정적static-변수">제네릭 람다와 정적(static) 변수</h3>
<p>11.7 레슨에서 함수 템플릿 안에 정적(static) 변수가 있으면, 템플릿에서 만들어지는 각각의 함수마다 자기만의 고유한 정적 변수를 따로 가지게 된다고 배웠습니다.
제네릭 람다 안의 <code>auto</code> 매개변수도 원리가 똑같기 때문에 같은 일이 벌어집니다. 넘겨받는 데이터 타입이 달라질 때마다 완전히 새로운 람다가 하나씩 복제되어 만들어집니다.</p>
<p>아래 예제를 보면 하나의 제네릭 람다가 어떻게 두 개의 독립적인 람다로 나뉘는지 확인할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

int main()
{
  // 값을 출력하고, 이 람다가 지금까지 몇 번 호출되었는지 횟수를 셉니다.
  auto print{
    [](auto value) {
      static int callCount{ 0 };
      std::cout &lt;&lt; callCount++ &lt;&lt; &quot;: &quot; &lt;&lt; value &lt;&lt; &#39;\n&#39;;
    }
  };

  print(&quot;hello&quot;); // 0: hello
  print(&quot;world&quot;); // 1: world

  print(1); // 0: 1
  print(2); // 1: 2

  print(&quot;ding dong&quot;); // 2: ding dong

  return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">0: hello
1: world
0: 1
1: 2
2: ding dong
</code></pre>
<p>문자열(&quot;hello&quot;, &quot;world&quot; 등)을 넘겨줬을 때와 숫자(1, 2)를 넘겨줬을 때 각각 다른 버전의 람다가 만들어집니다.
대부분의 경우에는 별문제가 없지만, 람다 안에 <code>static</code> 변수가 있다면 이야기가 달라집니다. 문자열 전용 람다와 숫자 전용 람다가 각자의 <code>callCount</code> 변수를 따로 관리하게 되는 것이죠! 출력 결과를 보면 문자열을 넣었을 때와 숫자를 넣었을 때 숫자가 따로 올라가는 것을 볼 수 있습니다.</p>
<p>모든 람다가 하나의 변수(<code>callCount</code>)를 같이 공유하게 만들려면 람다 바깥에 전역 변수나 정적 지역 변수를 만들어야 합니다. 하지만 이전 레슨에서 배웠듯이 이런 변수들은 버그를 만들고 코드를 복잡하게 할 수 있습니다. 이 문제는 다음 레슨에서 &#39;람다 캡처&#39;를 배우면 깔끔하게 해결할 수 있습니다.</p>
<hr>
<h3 id="반환-타입-추론과-후행-반환-타입">반환 타입 추론과 후행 반환 타입</h3>
<p>람다에서 반환 타입을 생략해서 컴파일러에게 추론을 맡길 경우, 람다 안에 있는 <code>return</code> 문장들을 보고 타입을 결정합니다. 이때 <strong>람다 안의 모든 <code>return</code> 문장은 반드시 똑같은 데이터 타입을 반환해야 합니다</strong> (그렇지 않으면 컴파일러가 도대체 어느 타입에 맞춰야 할지 몰라 혼란에 빠집니다).</p>
<p>예를 들어볼까요:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
  auto divide{ [](int x, int y, bool intDivision) { // 참고: 반환 타입이 명시되지 않음
    if (intDivision)
      return x / y; // 반환 타입은 int (정수)
    else
      return static_cast&lt;double&gt;(x) / y; // 에러: 이전 반환 타입(int)과 일치하지 않음 (double)
  } };

  std::cout &lt;&lt; divide(3, 2, true) &lt;&lt; &#39;\n&#39;;
  std::cout &lt;&lt; divide(3, 2, false) &lt;&lt; &#39;\n&#39;;

  return 0;
}
</code></pre>
<p>이 코드는 에러가 납니다. 첫 번째 <code>return</code>은 정수(<code>int</code>)를 돌려주려고 하는데, 두 번째 <code>return</code>은 소수(<code>double</code>)를 돌려주려고 하기 때문입니다.</p>
<p>이렇게 상황에 따라 다른 타입을 반환하고 싶다면 두 가지 해결책이 있습니다.</p>
<ol>
<li>모든 <code>return</code> 값이 똑같은 타입이 되도록 직접 형태를 변환(캐스팅)해주거나,</li>
<li>람다의 반환 타입을 명시적으로 딱 정해줘서 컴파일러가 알아서 알맞게 변환하도록 맡기는 것입니다.</li>
</ol>
<p>보통은 두 번째 방법이 훨씬 깔끔하고 좋습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
  // 참고: -&gt; double을 써서 반환 값이 double 타입임을 명확히 적어줍니다.
  auto divide{ [](int x, int y, bool intDivision) -&gt; double {
    if (intDivision)
      return x / y; // 결과값(int)을 알아서 double로 변환해 줍니다.
    else
      return static_cast&lt;double&gt;(x) / y;
  } };

  std::cout &lt;&lt; divide(3, 2, true) &lt;&lt; &#39;\n&#39;;
  std::cout &lt;&lt; divide(3, 2, false) &lt;&lt; &#39;\n&#39;;

  return 0;
}
</code></pre>
<p>이렇게 명확하게 <code>-&gt; double</code>이라고 적어두면, 나중에 반환 타입을 고치고 싶을 때 람다 내부의 복잡한 코드는 건드릴 필요 없이 저 <code>-&gt; double</code> 부분만 수정하면 되어서 무척 편리합니다.</p>
<hr>
<h3 id="표준-라이브러리-함수-객체">표준 라이브러리 함수 객체</h3>
<p>덧셈, 음수 만들기, 대소 비교 같이 아주 뻔하고 자주 쓰는 기능들을 위해 매번 람다를 직접 만들 필요는 없습니다. C++ 표준 라이브러리인 <code>&lt;functional&gt;</code> 헤더에는 이미 만들어져 있는 유용한 함수 객체들이 아주 많습니다.</p>
<p>아래 예제를 보세요.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;

bool greater(int a, int b)
{
  // a가 b보다 크면 a를 b보다 앞에 배치합니다.
  return a &gt; b;
}

int main()
{
  std::array arr{ 13, 90, 99, 5, 40, 80 };

  // 정렬 함수인 std::sort에 greater 함수를 넘겨줍니다.
  std::sort(arr.begin(), arr.end(), greater);

  for (int i : arr)
  {
    std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
  }

  std::cout &lt;&lt; &#39;\n&#39;;

  return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">99 90 80 40 13 5
</code></pre>
<p>이 코드를 위해 <code>greater</code>라는 함수를 람다로 일일이 바꾸는 것은 귀찮은 일입니다. 대신 이미 C++에 만들어져 있는 <code>std::greater</code>를 가져다 쓰면 아주 간단합니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;functional&gt; // std::greater를 사용하기 위해 포함합니다.

int main()
{
  std::array arr{ 13, 90, 99, 5, 40, 80 };

  // std::greater를 std::sort에 넘겨줍니다.
  std::sort(arr.begin(), arr.end(), std::greater{}); // 참고: 객체를 생성하기 위해 중괄호 {} 가 필요합니다.

  for (int i : arr)
  {
    std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
  }

  std::cout &lt;&lt; &#39;\n&#39;;

  return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">99 90 80 40 13 5
</code></pre>
<hr>
<h3 id="결론-2">결론</h3>
<p>람다와 알고리즘 라이브러리를 함께 쓰는 것이 처음에는 <code>for</code>나 <code>while</code> 같은 일반적인 반복문(loop)을 쓰는 것보다 불필요하게 복잡해 보일 수 있습니다. 하지만 이 조합을 잘 활용하면 단 몇 줄의 코드만으로도 굉장히 강력한 기능을 만들 수 있고, 나중에는 오히려 반복문보다 읽기 훨씬 편해집니다.</p>
<p>무엇보다도, 알고리즘 라이브러리는 일반 반복문으로는 쉽게 하기 힘든 강력하고 편리한 병렬 처리(동시에 여러 작업 처리) 기능을 제공합니다. 일반 반복문으로 짠 코드를 업그레이드하는 것보다 라이브러리를 사용한 코드를 업그레이드하는 것이 훨씬 쉽고 안전합니다.</p>
<p>람다는 정말 훌륭한 기능이지만, 세상 모든 일반 함수를 람다로 교체해야 한다는 뜻은 아닙니다. 코드가 길고 복잡하거나 여러 곳에서 반복해서 재사용해야 하는 기능이라면 기존처럼 일반 함수를 만드는 것이 훨씬 좋습니다.</p>
<hr>
<h2 id="207--람다-캡처">20.7 — 람다 캡처</h2>
<h3 id="캡처-절과-값으로-캡처하기">캡처 절과 값으로 캡처하기</h3>
<p>이전 레슨(20.6 -- 람다(익명 함수) 소개)에서 아래와 같은 예제를 다루었습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

int main()
{
  std::array&lt;std::string_view, 4&gt; arr{ &quot;apple&quot;, &quot;banana&quot;, &quot;walnut&quot;, &quot;lemon&quot; };

  auto found{ std::find_if(arr.begin(), arr.end(),
                           [](std::string_view str)
                           {
                             return str.find(&quot;nut&quot;) != std::string_view::npos;
                           }) };

  if (found == arr.end())
  {
    std::cout &lt;&lt; &quot;No nuts\n&quot;; // 견과류 없음
  }
  else
  {
    std::cout &lt;&lt; &quot;Found &quot; &lt;&lt; *found &lt;&lt; &#39;\n&#39;; // 찾음
  }

  return 0;
}
</code></pre>
<p>이제 이 &quot;nut(견과류)&quot; 찾기 예제를 조금 바꿔서, 사용자가 직접 검색할 단어를 입력하도록 만들어 보겠습니다. 생각보다 직관적으로 바로 동작하지는 않을 겁니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;
#include &lt;string&gt;

int main()
{
  std::array&lt;std::string_view, 4&gt; arr{ &quot;apple&quot;, &quot;banana&quot;, &quot;walnut&quot;, &quot;lemon&quot; };

  // 사용자에게 무엇을 검색할지 물어봅니다.
  std::cout &lt;&lt; &quot;search for: &quot;;

  std::string search{};
  std::cin &gt;&gt; search;

  auto found{ std::find_if(arr.begin(), arr.end(), [](std::string_view str) {
    // &quot;nut&quot; 대신 @search 변수를 검색합니다.
    return str.find(search) != std::string_view::npos; // 에러: 이 범위에서는 search에 접근할 수 없습니다.
  }) };

  if (found == arr.end())
  {
    std::cout &lt;&lt; &quot;Not found\n&quot;; // 찾지 못함
  }
  else
  {
    std::cout &lt;&lt; &quot;Found &quot; &lt;&lt; *found &lt;&lt; &#39;\n&#39;; // 찾음
  }

  return 0;
}
</code></pre>
<p>이 코드는 컴파일(실행)되지 않습니다. 일반적인 중첩 블록 <code>{ }</code> 안에서는 바깥에 있는 변수를 자유롭게 쓸 수 있지만, <strong>람다는 다릅니다.</strong> 람다는 기본적으로 바깥에 있는 변수를 볼 수 없습니다. 예외적으로 아래와 같은 특별한 변수들만 볼 수 있죠.</p>
<ul>
<li>프로그램이 끝날 때까지 살아있는 변수 (전역 변수나 <code>static</code> 변수 등)</li>
<li>컴파일할 때 값이 정해지는 상수 (<code>constexpr</code> 객체)</li>
</ul>
<p>우리가 만든 <code>search</code> 변수는 위 조건에 해당하지 않기 때문에, 람다 안에서는 <code>search</code>가 무엇인지 전혀 알지 못합니다. 투명인간 취급을 하는 거죠.</p>
<blockquote>
<p><strong>팁</strong>
람다는 람다 외부에 정의된 특정 종류의 객체(예: 전역 변수, 정적 지역 변수 같은 정적 저장소 기간을 가진 객체나 constexpr 객체)에만 접근할 수 있습니다.</p>
</blockquote>
<p>람다 안에서 <code>search</code> 변수를 사용하려면 <strong>캡처 절(capture clause)</strong> 이라는 것을 사용해야 합니다.</p>
<hr>
<h3 id="캡처-절">캡처 절</h3>
<p>&#39;캡처 절&#39;은 람다가 원래는 볼 수 없는 바깥쪽 변수를 사용할 수 있게 (간접적으로) 허락해 주는 문법입니다. 방법은 아주 간단합니다. 람다의 대괄호 <code>[]</code> 안에 람다 안에서 쓰고 싶은 변수 이름을 적어주기만 하면 됩니다.</p>
<p>여기서는 <code>search</code> 변수의 값을 람다 안에서 쓰고 싶으니, 캡처 절에 <code>search</code>를 추가해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;
#include &lt;string&gt;

int main()
{
  std::array&lt;std::string_view, 4&gt; arr{ &quot;apple&quot;, &quot;banana&quot;, &quot;walnut&quot;, &quot;lemon&quot; };

  std::cout &lt;&lt; &quot;search for: &quot;;

  std::string search{};
  std::cin &gt;&gt; search;

  // @search 변수를 캡처합니다                    vvvvvv
  auto found{ std::find_if(arr.begin(), arr.end(), [search](std::string_view str) {
    return str.find(search) != std::string_view::npos;
  }) };

  if (found == arr.end())
  {
    std::cout &lt;&lt; &quot;Not found\n&quot;; // 찾지 못함
  }
  else
  {
    std::cout &lt;&lt; &quot;Found &quot; &lt;&lt; *found &lt;&lt; &#39;\n&#39;; // 찾음
  }

  return 0;
}
</code></pre>
<p>이제 사용자가 입력한 단어로 배열 안의 요소를 성공적으로 검색할 수 있습니다!</p>
<p><strong>출력 결과</strong></p>
<pre><code class="language-text">search for: nana
Found banana
</code></pre>
<hr>
<h3 id="캡처는-실제로-어떻게-작동할까요">캡처는 실제로 어떻게 작동할까요?</h3>
<p>위의 코드를 보면 람다가 <code>main</code> 함수 안에 있는 <code>search</code> 변수를 직접 가져다 쓰는 것처럼 보이지만, <strong>사실은 그렇지 않습니다.</strong> 람다는 겉보기엔 그냥 괄호 블록처럼 생겼지만, 내부적으로는 조금 다르게 작동합니다(이 차이를 아는 것이 매우 중요합니다).</p>
<p>람다가 만들어질 때, 캡처 절에 적어둔 변수들은 람다 내부로 똑같은 이름으로 <strong>복제</strong> 됩니다. 마법처럼 원본 변수의 값을 그대로 복사해서 람다 전용 변수를 새로 하나 만드는 것입니다.</p>
<p>즉, 위 예제에서 람다 객체가 생성될 때, 람다는 자신만의 <code>search</code>라는 변수를 따로 갖게 됩니다. 이 복제된 <code>search</code>는 원본 <code>search</code>와 값만 똑같을 뿐, 완전히 다른 독립적인 변수입니다. 마치 원본을 만지는 것 같지만 실제로는 복사본을 만지고 있는 것이죠.</p>
<p>이렇게 이름은 같아도, 복제된 변수의 타입이 원본과 항상 똑같은 것은 아닙니다. 
이에 대해서는 이어지는 내용에서 더 살펴보겠습니다.</p>
<blockquote>
<p><strong>핵심 통찰</strong>
람다가 캡처한 변수는 바깥쪽 변수의 진짜 &#39;원본&#39;이 아니라, 값만 똑같이 베껴온 <strong>&#39;복사본&#39;</strong> 입니다.</p>
</blockquote>
<blockquote>
<p><strong>고급 학습자를 위한 내용</strong>
람다는 겉보기엔 함수 같지만, 실제로는 함수처럼 호출할 수 있는 <strong>객체(object)</strong> 입니다. (이를 함수 객체, 즉 &#39;펑터(functor)&#39;라고 부르며, 나중에 직접 만드는 법을 배울 것입니다.)
컴파일러가 람다를 발견하면, 람다를 위한 맞춤형 객체를 하나 몰래 만들어냅니다. 이때 캡처된 변수들은 이 객체의 &#39;멤버 변수(데이터 멤버)&#39;가 됩니다.
프로그램이 실행되다가 람다를 정의한 코드를 만나면 이 람다 객체가 생성(인스턴스화)되고, 그때 캡처된 변수들도 초기화됩니다.</p>
</blockquote>
<hr>
<h3 id="캡처는-기본적으로-const상수로-취급됩니다">캡처는 기본적으로 const(상수)로 취급됩니다</h3>
<p>람다를 실행할 때 내부적으로 <code>operator()</code>라는 것이 호출됩니다. 그런데 기본적으로 람다는 자신이 캡처해 온 <strong>복사본들을 수정하지 못하도록 꽉 잠가버립니다(const 취급).</strong> 아래 예제에서 <code>ammo(총알)</code> 변수를 캡처한 뒤, 람다 안에서 총알 개수를 줄여보려고 시도해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
  int ammo{ 10 }; // 총알 10발

  // 람다를 정의하고 &quot;shoot(쏘다)&quot;이라는 변수에 저장합니다.
  auto shoot{
    [ammo]() {
      // 에러 발생! ammo는 수정할 수 없습니다.
      --ammo;

      std::cout &lt;&lt; &quot;Pew! &quot; &lt;&lt; ammo &lt;&lt; &quot; shot(s) left.\n&quot;; // 빵! 총알 ~발 남음.
    }
  };

  // 람다를 호출합니다.
  shoot();

  std::cout &lt;&lt; ammo &lt;&lt; &quot; shot(s) left\n&quot;; // 총알 ~발 남음.

  return 0;
}
</code></pre>
<p>이 코드는 컴파일되지 않습니다. 람다 안에서 <code>ammo</code>는 값을 바꿀 수 없는 <code>const</code>로 취급되기 때문입니다.</p>
<hr>
<h3 id="변경-가능한-캡처">변경 가능한 캡처</h3>
<p>만약 람다가 캡처한 복사본의 값을 꼭 수정해야 한다면, 람다에 <code>mutable(변경 가능한)</code>이라는 마법의 키워드를 붙여주면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
  int ammo{ 10 }; // 총알 10발

  auto shoot{
    [ammo]() mutable { // 이제 변경이 가능합니다 (mutable 추가)
      // 이제 ammo를 수정할 수 있습니다.
      --ammo;

      std::cout &lt;&lt; &quot;Pew! &quot; &lt;&lt; ammo &lt;&lt; &quot; shot(s) left.\n&quot;;
    }
  };

  shoot(); // 빵!
  shoot(); // 빵!

  std::cout &lt;&lt; ammo &lt;&lt; &quot; shot(s) left\n&quot;; // 원본 총알 확인

  return 0;
}
</code></pre>
<p><strong>출력 결과:</strong></p>
<pre><code class="language-text">Pew! 9 shot(s) left.
Pew! 8 shot(s) left.
10 shot(s) left
</code></pre>
<p>이제 에러 없이 실행은 되지만, 논리적으로 이상한 점이 있습니다. 람다 안에서는 총알이 9발, 8발로 줄어들었는데, 마지막에 원본 총알을 확인해 보니 그대로 10발입니다!
왜 그럴까요? 앞서 설명했듯이 람다는 <code>ammo</code>의 <strong>복사본</strong>을 가져갔기 때문입니다. 람다는 원본이 아니라 자기 주머니에 있는 &#39;복사본 총알&#39;만 소비한 것입니다.</p>
<p>그리고 한 가지 더 신기한 점은, 람다를 여러 번 호출해도 람다 안의 <code>ammo</code> 복사본 값이 계속 유지된다는 점입니다 (9 -&gt; 8로 줄어듦).</p>
<blockquote>
<p><strong>주의 사항</strong>
캡처된 변수들은 람다 객체 내부의 멤버 변수로 저장되기 때문에, 람다를 여러 번 호출해도 그 값이 사라지지 않고 계속 유지됩니다!</p>
</blockquote>
<hr>
<h3 id="참조로-캡처하기">참조로 캡처하기</h3>
<p>일반 함수에서 원본 데이터를 바꾸기 위해 &#39;참조(reference)&#39;를 사용하는 것처럼, 람다에서도 변수를 <strong>참조로 캡처</strong>하면 원본 변수를 직접 수정할 수 있습니다.</p>
<p>변수를 참조로 캡처하려면 캡처 절의 변수 이름 앞에 <code>&amp;</code> 기호를 붙이면 됩니다. 복사본(값)으로 캡처할 때와 달리, 참조로 캡처한 변수는 원본 자체가 <code>const</code>가 아닌 이상 람다 안에서 자유롭게 수정할 수 있습니다.
무거운 데이터(문자열, 복잡한 객체 등)를 함수에 넘길 때 복사를 피하기 위해 참조를 쓰는 것처럼, 람다에서도 특별한 이유가 없다면 <strong>값으로 캡처하는 것보다 참조로 캡처하는 것을 권장</strong>합니다.</p>
<p>아래는 <code>ammo</code>를 참조로 캡처하도록 수정한 코드입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
  int ammo{ 10 };

  auto shoot{
    // 원본을 직접 만지므로 이제 mutable 키워드가 필요 없습니다.
    [&amp;ammo]() { // &amp;ammo는 ammo를 참조로 캡처한다는 뜻입니다.
      // 람다 안의 ammo를 바꾸면 main 함수의 원본 ammo도 바뀝니다.
      --ammo;

      std::cout &lt;&lt; &quot;Pew! &quot; &lt;&lt; ammo &lt;&lt; &quot; shot(s) left.\n&quot;;
    }
  };

  shoot();

  std::cout &lt;&lt; ammo &lt;&lt; &quot; shot(s) left\n&quot;; // 원본 총알 확인

  return 0;
}
</code></pre>
<p>이제 우리가 기대했던 대로 작동합니다!</p>
<pre><code class="language-text">Pew! 9 shot(s) left.
9 shot(s) left
</code></pre>
<p>이번에는 배열을 정렬할 때 <code>std::sort</code>가 비교를 몇 번이나 수행하는지 세어보는 예제에 참조 캡처를 활용해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

struct Car
{
  std::string_view make{};  // 제조사
  std::string_view model{}; // 모델명
};

int main()
{
  std::array&lt;Car, 3&gt; cars{ { { &quot;Volkswagen&quot;, &quot;Golf&quot; },
                             { &quot;Toyota&quot;, &quot;Corolla&quot; },
                             { &quot;Honda&quot;, &quot;Civic&quot; } } };

  int comparisons{ 0 }; // 비교 횟수

  std::sort(cars.begin(), cars.end(),
    // @comparisons 변수를 참조로 캡처합니다.
    [&amp;comparisons](const auto&amp; a, const auto&amp; b) {
      // 참조로 캡처했기 때문에 &quot;mutable&quot; 없이도 값을 수정할 수 있습니다.
      ++comparisons;

      // 자동차 제조사(make)를 기준으로 정렬합니다.
      return a.make &lt; b.make;
  });

  std::cout &lt;&lt; &quot;Comparisons: &quot; &lt;&lt; comparisons &lt;&lt; &#39;\n&#39;;

  for (const auto&amp; car : cars)
  {
    std::cout &lt;&lt; car.make &lt;&lt; &#39; &#39; &lt;&lt; car.model &lt;&lt; &#39;\n&#39;;
  }

  return 0;
}
</code></pre>
<p><strong>예상 출력 결과</strong></p>
<pre><code class="language-text">Comparisons: 2
Honda Civic
Toyota Corolla
Volkswagen Golf
</code></pre>
<hr>
<h3 id="여러-변수-캡처하기">여러 변수 캡처하기</h3>
<p>쉼표(<code>,</code>)를 사용하면 여러 개의 변수를 한 번에 캡처할 수 있습니다. 값으로 캡처하는 변수와 참조로 캡처하는 변수를 섞어서 쓰는 것도 얼마든지 가능합니다!</p>
<pre><code class="language-cpp">int health{ 33 };
int armor{ 100 };
std::vector&lt;CEnemy&gt; enemies{};

// health와 armor는 값으로(복사해서) 캡처하고, enemies는 참조로(원본을) 캡처합니다.
[health, armor, &amp;enemies](){};
</code></pre>
<hr>
<h3 id="기본-캡처">기본 캡처</h3>
<p>람다 안에서 쓰는 변수가 많아지면, 캡처 절 <code>[]</code> 안에 일일이 변수 이름을 적어주는 것이 귀찮고 실수하기도 쉽습니다. 다행히 컴파일러에게 &quot;내가 람다 안에서 쓰는 변수들은 알아서 다 캡처해 줘!&quot;라고 부탁할 수 있는 기능이 있습니다.</p>
<p>이를 <strong>기본 캡처</strong>라고 부르며, 람다 안에서 언급된 변수들만 자동으로 캡처해 줍니다. (언급되지 않은 변수는 캡처하지 않습니다.)</p>
<ul>
<li>사용된 모든 변수를 <strong>값(복사본)</strong> 으로 자동 캡처하려면 <code>=</code>를 씁니다. (<code>[=]</code>)</li>
<li>사용된 모든 변수를 <strong>참조(원본)</strong> 로 자동 캡처하려면 <code>&amp;</code>를 씁니다. (<code>[&amp;]</code>)</li>
</ul>
<p>다음은 <code>=</code>를 사용해 기본 캡처를 하는 예제입니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
  std::array areas{ 100, 25, 121, 40, 56 };

  int width{};
  int height{};

  std::cout &lt;&lt; &quot;Enter width and height: &quot;;
  std::cin &gt;&gt; width &gt;&gt; height;

  auto found{ std::find_if(areas.begin(), areas.end(),
                           [=](int knownArea) { // 람다 안에서 쓰인 width와 height를 자동으로 값으로 캡처합니다.
                             return width * height == knownArea; // 여기서 두 변수가 사용되었기 때문입니다.
                           }) };

  if (found == areas.end())
  {
    std::cout &lt;&lt; &quot;I don&#39;t know this area :(\n&quot;; // 이 면적은 없네요 :(
  }
  else
  {
    std::cout &lt;&lt; &quot;Area found :)\n&quot;; // 면적을 찾았습니다 :)
  }

  return 0;
}
</code></pre>
<p>기본 캡처 기능은 일반 캡처와 섞어서 쓸 수도 있습니다. 
다만, 똑같은 변수를 두 번 캡처하려고 하면 에러가 납니다.</p>
<pre><code class="language-cpp">int health{ 33 };
int armor{ 100 };
std::vector&lt;CEnemy&gt; enemies{};

// health와 armor는 값으로, enemies는 참조로 캡처합니다.
[health, armor, &amp;enemies](){};

// 기본적으로 모두 값(=)으로 캡처하되, enemies만 예외적으로 참조(&amp;)로 캡처합니다.
[=, &amp;enemies](){};

// 기본적으로 모두 참조(&amp;)로 캡처하되, armor만 예외적으로 값으로 캡처합니다.
[&amp;, armor](){};

// 에러: 이미 모든 것을 참조(&amp;)로 캡처한다고 했는데, 또 참조(&amp;armor)로 하겠다고 중복 선언했습니다.
[&amp;, &amp;armor](){};

// 에러: 이미 모든 것을 값(=)으로 캡처한다고 했는데, 또 값(armor)으로 하겠다고 중복 선언했습니다.
[=, armor](){};

// 에러: armor를 두 번 적었습니다.
[armor, &amp;health, &amp;armor](){};

// 에러: 기본 캡처 기호(&amp; 나 =)는 무조건 맨 앞에 와야 합니다.
[armor, &amp;](){};
</code></pre>
<hr>
<h3 id="람다-캡처-안에서-새로운-변수-정의하기">람다 캡처 안에서 새로운 변수 정의하기</h3>
<p>때로는 기존 변수를 살짝 가공해서 캡처하거나, 아예 람다 안에서만 쓸 새로운 변수를 만들고 싶을 때가 있습니다. 이럴 때는 캡처 절 안에서 데이터 타입 없이 변수를 직접 정의할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;algorithm&gt;

int main()
{
  std::array areas{ 100, 25, 121, 40, 56 };

  int width{};
  int height{};

  std::cout &lt;&lt; &quot;Enter width and height: &quot;;
  std::cin &gt;&gt; width &gt;&gt; height;

  // 배열에는 면적(area)이 저장되어 있지만, 사용자는 너비(width)와 높이(height)를 입력했습니다.
  // 따라서 검색을 하려면 먼저 너비와 높이를 곱해 면적을 계산해 두어야 합니다.
  auto found{ std::find_if(areas.begin(), areas.end(),
                           // 람다 안에서만 볼 수 있는 새로운 변수를 선언합니다.
                           // userArea의 타입은 자동으로 int로 결정(추론)됩니다.
                           [userArea{ width * height }](int knownArea) {
                             return userArea == knownArea;
                           }) };

  if (found == areas.end())
  {
    std::cout &lt;&lt; &quot;I don&#39;t know this area :(\n&quot;;
  }
  else
  {
    std::cout &lt;&lt; &quot;Area found :)\n&quot;;
  }

  return 0;
}
</code></pre>
<p>여기서 <code>userArea</code>라는 변수는 람다가 만들어질 때 단 한 번만 계산됩니다. 계산된 결과는 람다 객체 안에 잘 보관되어, 람다를 여러 번 호출해도 다시 계산할 필요 없이 똑같이 사용됩니다. 만약 람다가 <code>mutable</code>이고 캡처 절에서 만든 이 변수를 수정한다면, 수정된 값이 계속 덮어씌워져 유지됩니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
캡처 절 안에서 변수를 초기화하는 것은 값이 짧고(단순하고) 타입이 명확할 때만 사용하세요. 코드가 길어지고 복잡해진다면, 람다 바깥에서 먼저 변수를 계산해 둔 다음 그걸 캡처하는 것이 더 좋습니다.</p>
</blockquote>
<hr>
<h3 id="dangling-캡처-변수">Dangling 캡처 변수</h3>
<p>변수들은 람다가 만들어지는 바로 그 시점에 캡처됩니다. 만약 참조로 캡처한 변수가 람다보다 먼저 수명을 다해 파괴되어 버리면 어떻게 될까요? 람다는 허공을 가리키는, 
이른바 <strong>&#39;매달린 참조(Dangling reference)&#39;</strong> 를 쥐고 있게 됩니다.</p>
<p>예를 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

// 람다를 반환하는 함수
auto makeWalrus(const std::string&amp; name)
{
  // name을 참조로 캡처한 람다를 만들어서 반환합니다.
  return [&amp;]() {
    std::cout &lt;&lt; &quot;I am a walrus, my name is &quot; &lt;&lt; name &lt;&lt; &#39;\n&#39;; // 정의되지 않은 동작(위험!)
  };
}

int main()
{
  // 이름이 Roofus인 새로운 바다코끼리를 만듭니다.
  // sayName은 makeWalrus 함수가 만들어준 람다를 담고 있습니다.
  auto sayName{ makeWalrus(&quot;Roofus&quot;) };

  // makeWalrus가 반환한 람다 함수를 호출해 봅니다.
  sayName();

  return 0;
}
</code></pre>
<p>이 코드에서 <code>makeWalrus(&quot;Roofus&quot;)</code>를 호출하면, 문자열 &quot;Roofus&quot;를 담은 <strong>임시 변수</strong>가 만들어집니다. 람다는 이 임시 변수를 &#39;참조(원본 그대로)&#39;로 캡처했습니다.
그런데 문제는 <code>makeWalrus</code> 함수의 실행이 끝나면 이 임시 변수는 메모리에서 완전히 파괴되어 사라진다는 겁니다! 하지만 우리의 <code>sayName</code> 람다는 여전히 그 파괴된 변수가 있던 자리를 기억하고 있습니다.
나중에 <code>sayName()</code>을 실행해서 그 자리에 접근하려고 하면, 이미 데이터가 날아간 쓰레기 메모리를 건드리게 되어 프로그램이 뻗거나 이상한 행동(정의되지 않은 동작)을 하게 됩니다.</p>
<p>만약 <code>makeWalrus</code> 함수가 <code>name</code>을 참조가 아니라 값(복사본)으로 받았더라도 똑같은 문제가 생깁니다. 함수가 끝나면 매개변수 <code>name</code>은 사라지기 때문입니다.</p>
<blockquote>
<p><strong>주의 사항</strong>
변수를 참조로 캡처할 때는 정말 조심해야 합니다! 특히 <code>[&amp;]</code> 처럼 기본 참조 캡처를 무심코 쓸 때 위험합니다. 캡처 당하는 변수는 <strong>반드시 람다보다 오래 살아남아야</strong> 합니다.</p>
</blockquote>
<p>만약 람다를 나중에 실행할 때도 <code>name</code> 변수를 안전하게 쓰고 싶다면, 참조가 아니라 <strong>값(복사본)</strong> 으로 캡처해야 합니다 (직접 <code>[name]</code>이라고 적거나 <code>[=]</code>를 사용). 그러면 람다가 자신만의 튼튼한 복사본을 가지게 되어 원본이 파괴되든 말든 신경 쓰지 않아도 됩니다.</p>
<hr>
<h3 id="변경-가능한-람다의-의도치-않은-복사">변경 가능한 람다의 의도치 않은 복사</h3>
<p>앞서 람다는 내부적으로 &#39;객체(object)&#39;라고 말씀드렸습니다. 객체라는 말은 곧 <strong>다른 곳으로 복사될 수 있다</strong>는 뜻입니다. 그리고 이 점이 가끔 골치 아픈 문제를 만듭니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
  int i{ 0 };

  // count라는 이름의 람다를 만듭니다 (변경 가능)
  auto count{ [i]() mutable {
    std::cout &lt;&lt; ++i &lt;&lt; &#39;\n&#39;;
  } };

  count(); // count 실행 (1 출력)

  auto otherCount{ count }; // count를 그대로 복사해서 otherCount를 만듭니다

  // 원본과 복사본을 각각 실행해 봅니다.
  count();
  otherCount();

  return 0;
}
</code></pre>
<p><strong>출력 결과</strong></p>
<pre><code class="language-text">1
2
2
</code></pre>
<p>1, 2, 3이 출력될 줄 알았는데 2가 두 번 출력되었습니다!
이유는 간단합니다. <code>otherCount</code>를 만들 때, 그 시점에서의 <code>count</code> 상태를 통째로 복사(도장 찍기)했기 때문입니다. 처음 <code>count()</code>를 한 번 실행해서 람다 안의 <code>i</code>가 1이 되었죠? 그래서 복사본인 <code>otherCount</code>도 똑같이 <code>i=1</code>인 상태를 물려받고 시작한 겁니다. 둘은 복사된 이후 각자의 길을 걷기 때문에 독립적인 <code>i</code>를 가지고 따로따로 1씩 증가시킨 것입니다.</p>
<p>이제 조금 더 알아차리기 힘든 예제를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;functional&gt;

void myInvoke(const std::function&lt;void()&gt;&amp; fn)
{
    fn();
}

int main()
{
    int i{ 0 };

    // 자신이 가진 복사본 @i를 증가시키고 출력하는 람다
    auto count{ [i]() mutable {
      std::cout &lt;&lt; ++i &lt;&lt; &#39;\n&#39;;
    } };

    myInvoke(count);
    myInvoke(count);
    myInvoke(count);

    return 0;
}
</code></pre>
<p><strong>출력 결과:</strong></p>
<pre><code class="language-text">1
1
1
</code></pre>
<p>방금 전 예제와 똑같은 문제가 아주 은밀하게 숨어 있습니다.
<code>myInvoke(count)</code>를 호출할 때, 컴파일러는 우리가 만든 &#39;람다 타입&#39;과 함수가 요구하는 <code>std::function</code> 타입이 다르다는 걸 눈치챕니다. 그래서 컴파일러가 알아서 람다를 <code>std::function</code>이라는 임시 박스에 포장(변환)해서 넘겨줍니다. 이 포장 과정에서 <strong>람다의 복사본</strong>이 만들어져 버립니다!
결국 1, 2, 3 누적 카운트는 <code>count</code> 원본에서 이루어지는 게 아니라, 호출할 때마다 새로 만들어지는 일회용 복사본에서만 이루어지기 때문에 계속 1만 출력되는 것입니다.</p>
<p>이런 끔찍한 복사 문제를 막고 싶다면 두 가지 방법이 있습니다.
첫째, 람다 안에서 복사본을 수정하는(<code>mutable</code>) 방식을 버리고, 차라리 클래스를 직접 만들거나 다른 구조를 사용하는 것입니다. (하지만 코드가 복잡해질 수 있습니다.)
둘째, 아예 처음부터 람다를 <code>std::function</code> 박스 안에 명시적으로 담아두는 것입니다. 그러면 함수로 넘길 때 추가적인 복사 포장 작업이 일어나지 않습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;functional&gt;

void myInvoke(const std::function&lt;void()&gt;&amp; fn)
{
    fn();
}

int main()
{
    int i{ 0 };

    // 람다 객체를 생성하자마자 바로 std::function에 담아둡니다.
    std::function count{ [i]() mutable { 
      std::cout &lt;&lt; ++i &lt;&lt; &#39;\n&#39;;
    } };

    myInvoke(count); // 이제 포장된 채로 넘어가므로 복사본이 생기지 않습니다!
    myInvoke(count); // 복사 생기지 않음
    myInvoke(count); // 복사 생기지 않음

    return 0;
}
</code></pre>
<p><strong>이제 원하던 출력 결과가 나옵니다:</strong></p>
<pre><code class="language-text">1
2
3
</code></pre>
<p>또 다른 멋진 해결책으로는 <strong>참조 래퍼(reference wrapper)</strong> 를 사용하는 방법이 있습니다. C++의 <code>&lt;functional&gt;</code> 헤더에는 일반 객체를 마치 참조(reference)인 것처럼 속여서 전달하게 해주는 <code>std::reference_wrapper</code>라는 유용한 도구가 있습니다.
<code>std::ref()</code> 함수를 쓰면 람다를 아주 쉽게 참조 형태로 감쌀 수 있죠. 이렇게 람다를 한 겹 감싸주면, 누군가 람다를 복사하려고 할 때 <strong>람다 원본이 복사되는 게 아니라 껍데기(참조)만 복사</strong>되므로, 안전하게 원본 하나만 유지할 수 있습니다.</p>
<p><code>std::ref</code>를 활용해 수정한 코드를 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;functional&gt; // std::reference_wrapper와 std::ref를 사용하기 위한 헤더

void myInvoke(const std::function&lt;void()&gt;&amp; fn)
{
    fn();
}

int main()
{
    int i{ 0 };

    // 자신이 가진 복사본 @i를 증가시키고 출력하는 람다
    auto count{ [i]() mutable {
      std::cout &lt;&lt; ++i &lt;&lt; &#39;\n&#39;;
    } };

    // std::ref(count)는 count가 참조처럼 취급되도록 보장해 줍니다.
    // 누군가 count를 복사하려고 하면 실제로는 &#39;참조&#39;만 복사되기 때문에, 
    // 결과적으로 실제 count 객체는 세상에 단 하나만 존재하게 됩니다.
    myInvoke(std::ref(count));
    myInvoke(std::ref(count));
    myInvoke(std::ref(count));

    return 0;
}
</code></pre>
<p><strong>이제 원하던 출력 결과가 나옵니다:</strong></p>
<pre><code class="language-text">1
2
3
</code></pre>
<p>이 방식이 정말 강력한 이유는, <code>myInvoke</code> 함수가 매개변수를 참조(&amp;)가 아니라 값으로(복사해서) 받도록 설계되어 있어도 정상적으로 1, 2, 3을 카운트한다는 점입니다!</p>
<blockquote>
<p><strong>규칙</strong>
C++ 표준 라이브러리 함수들은 종종 함수 객체(람다 포함)를 내부적으로 복사합니다. 만약 여러분이 캡처된 변수를 수정하는 <code>mutable</code> 람다를 라이브러리 함수에 넘겨줘야 한다면, 복사 방지를 위해 <code>std::ref</code>를 사용해서 넘겨주는 것이 좋습니다.</p>
</blockquote>
<blockquote>
<p><strong>권장 사항</strong>
가장 좋은 방법은 애초에 <strong><code>mutable</code> 람다를 최대한 쓰지 않는 것</strong>입니다. 데이터를 내부에서 수정하지 않는 람다가 읽기도 훨씬 쉽고, 위와 같은 골치 아픈 복사 버그나 나중에 병렬 처리(멀티 스레드)를 할 때 생기는 무서운 에러들을 피할 수 있습니다.</p>
</blockquote>
<hr>
<h2 id="20x--20장-요약-및-퀴즈">20.x — 20장 요약 및 퀴즈</h2>
<h3 id="챕터-복습">챕터 복습</h3>
<p>또 하나의 챕터를 무사히 마쳤네요! </p>
<p>함수에 데이터(인수)를 넘겨줄 때는 <strong>값(value)</strong>, <strong>참조(reference)</strong>, <strong>주소(address)</strong> 이렇게 세 가지 방법을 쓸 수 있습니다. 상황에 맞게 골라 쓰는 것이 아주 중요해요!</p>
<ul>
<li><strong>값으로 전달 (Pass by value):</strong> 기본 데이터 타입(<code>int</code>, <code>double</code> 등)이나 열거형을 넘길 때 씁니다. 원본은 놔두고 &#39;복사본&#39;만 함수에 던져주는 안전하고 단순한 방법입니다.</li>
<li><strong>참조로 전달 (Pass by reference):</strong> 구조체나 클래스처럼 덩치가 큰 데이터이거나, 함수 안에서 원본 데이터를 직접 수정해야 할 때 씁니다. 원본을 직접 연결해 주므로 복사하는 수고를 덜어줍니다.</li>
<li><strong>주소로 전달 (Pass by address):</strong> 포인터나 C 스타일의 기본 배열을 넘길 때 씁니다.</li>
<li><em>팁:</em> 참조나 주소로 데이터를 넘길 때, 함수 안에서 원본이 멋대로 바뀌면 안 된다면 가급적 <code>const</code>를 붙여서 안전하게 보호해 주세요.</li>
</ul>
<p>함수가 결과값을 돌려줄 때(반환)도 마찬가지로 <strong>값, 참조, 주소</strong> 세 가지 방식을 쓸 수 있습니다.</p>
<ul>
<li>대부분의 경우에는 그냥 <strong>&#39;값으로 반환&#39;</strong>하는 것으로 충분합니다.</li>
<li>하지만 동적으로 할당된 데이터(필요할 때 메모리를 빌려 쓴 데이터), 구조체, 클래스처럼 무거운 데이터를 다룰 때는 <strong>&#39;참조나 주소로 반환&#39;</strong>하는 것이 아주 효율적이고 유용합니다.</li>
<li><em>주의할 점:</em> 참조나 주소로 결과값을 돌려줄 때는, 함수가 끝나면 메모리에서 사라져 버릴 데이터(지역 변수 등)를 가리키고 있지는 않은지 반드시 확인해야 합니다. 사라진 데이터를 가리키면 프로그램이 뻗을 수 있거든요!</li>
</ul>
<p><strong>함수 포인터</strong>를 사용하면 함수 자체를 다른 함수에 데이터처럼 넘겨줄 수 있습니다. 이 기능은 리스트를 정렬하는 방식처럼, 함수를 호출하는 쪽에서 함수의 세부적인 동작을 입맛대로 바꾸고 싶을 때(커스터마이징) 아주 유용합니다.</p>
<p>프로그램이 실행되는 도중에 필요한 만큼 메모리를 빌려 쓰는 <strong>동적 메모리</strong>는 <strong>&#39;힙(Heap)&#39;</strong>이라는 특별한 메모리 공간에 만들어집니다.</p>
<p><strong>콜 스택(Call stack)</strong>은 프로그램이 시작된 후부터 지금까지 실행 중인 모든 함수들(호출되었지만 아직 끝나지 않은 함수들)의 목록을 차곡차곡 기록해 두는 곳입니다.
우리가 함수 안에서 만드는 일반적인 변수(지역 변수)들은 이 <strong>&#39;스택(Stack)&#39;</strong>이라는 공간에 만들어져요. 단, 스택은 크기가 제한되어 있어서 너무 많이 쓰면 넘쳐버릴 수 있습니다. (스택처럼 동작하는 구조를 직접 만들고 싶다면 <code>std::vector</code>를 활용하면 좋습니다!)</p>
<p><strong>재귀 함수(Recursive function)</strong>는 자기 자신을 다시 부르는 신기한 함수입니다. 끝없이 자기 자신을 부르다 프로그램이 멈추지 않도록, 모든 재귀 함수에는 반드시 &quot;여기까지만 하고 끝내!&quot;라는 <strong>종료 조건(termination condition)</strong>이 있어야 합니다.</p>
<p><strong>명령줄 인수(Command line arguments)</strong>를 사용하면, 프로그램을 처음 켤 때 사용자나 다른 프로그램이 우리 프로그램에 초기 데이터를 쏙 넣어줄 수 있습니다. 단, 이렇게 들어오는 데이터는 무조건 텍스트(C 스타일 문자열) 형태이기 때문에, 만약 숫자로 쓰고 싶다면 우리가 직접 숫자로 바꿔주어야(변환해야) 합니다.</p>
<p><strong>생략 부호(Ellipsis, <code>...</code>)</strong>를 사용하면 함수에 넘겨주는 데이터(인수)의 개수를 그때그때 다르게(가변적으로) 할 수 있습니다. 하지만 이 방법을 쓰면 컴파일러가 &quot;이 데이터 타입이 맞나?&quot; 하고 검사하는 기능을 꺼버리고, 데이터가 몇 개나 들어왔는지도 알 수 없게 됩니다. 그래서 이 세부 사항들을 관리하는 건 전적으로 프로그래머의 몫이 되니 주의해서 써야 합니다.</p>
<p><strong>람다 함수(Lambda functions)</strong>는 다른 함수 안에 쏙 집어넣을 수 있는 함수입니다. 이름이 없어도(익명) 쓸 수 있고, C++의 알고리즘 라이브러리(데이터를 쉽게 다루는 도구들)와 함께 쓸 때 정말 무궁무진하게 활용할 수 있답니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 19]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-19</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-19</guid>
            <pubDate>Fri, 06 Mar 2026 14:51:47 GMT</pubDate>
            <description><![CDATA[<h2 id="191--new와-delete를-사용한-동적-메모리-할당">19.1 — new와 delete를 사용한 동적 메모리 할당</h2>
<h3 id="동적-메모리-할당이-필요한-이유">동적 메모리 할당이 필요한 이유</h3>
<p>C++에는 메모리(데이터를 저장하는 공간)를 빌리는 세 가지 기본적인 방법이 있습니다. 여러분은 이미 그중 두 가지를 경험해 보셨을 거예요.</p>
<ol>
<li><strong>정적(Static) 메모리 할당:</strong> 프로그램이 켜질 때 딱 한 번 메모리를 빌리고, 프로그램이 끝날 때까지 계속 그 자리를 차지하는 방식입니다. (전역 변수 등이 여기에 속해요.)</li>
<li><strong>자동(Automatic) 메모리 할당:</strong> 함수 안으로 들어갈 때 메모리를 빌리고, 함수가 끝날 때 자동으로 메모리를 반납하는 방식입니다. 필요할 때마다 자동으로 이루어지죠. 
(우리가 평소에 쓰는 일반적인 지역 변수들이 속합니다.)</li>
<li><strong>동적(Dynamic) 메모리 할당:</strong> 바로 이 글에서 우리가 배울 주인공입니다!</li>
</ol>
<p>정적 할당과 자동 할당에는 <strong>두 가지 큰 공통점(이자 단점)</strong>이 있습니다.</p>
<ul>
<li>변수나 배열의 &#39;크기&#39;를 코드를 짤 때(컴파일할 때) 미리 정확히 알고 있어야 합니다.</li>
<li>메모리를 빌리고 반납하는 과정이 자동으로 알아서 처리됩니다.</li>
</ul>
<p>보통은 이 두 가지 특징이 전혀 문제 되지 않습니다. 하지만 외부 요인
(사용자의 입력이나 파일 읽기 등)을 다룰 때는, 이 제약 때문에 곤란한 상황이 발생하곤 해요.</p>
<p>예를 들어볼까요? 누군가의 이름을 저장하고 싶은데, 사용자가 이름을 입력하기 전까지는 그 이름이 몇 글자인지 알 수가 없죠. 또는 컴퓨터 디스크에서 기록을 불러오고 싶은데, 기록이 몇 개나 있는지 미리 알 수 없는 경우도 있습니다. 게임을 만들 때도 몬스터가 죽고 새로 태어나면서 몬스터의 수가 계속 변하는데, 몇 마리인지 딱 정해놓을 수가 없잖아요?</p>
<p>만약 코드를 짤 때 모든 크기를 미리 정해둬야만 한다면, 우리가 할 수 있는 최선은
그저 <strong>&quot;이 정도면 충분하겠지?&quot; 하고 최댓값을 어림짐작해서 찍는 것</strong>뿐입니다.</p>
<pre><code class="language-cpp">char name[25]; // 이름이 25자 미만이길 바래야죠!
Record record[500]; // 기록이 500개 미만이길!
Monster monster[40]; // 몬스터는 최대 40마리
Polygon rendering[30000]; // 이 3D 렌더링 폴리곤이 3만 개를 넘지 않기를!
</code></pre>
<p>하지만 이런 식의 &#39;찍기&#39;는 적어도 4가지 이유에서 아주 안 좋은 해결책입니다.</p>
<ol>
<li><strong>메모리 낭비:</strong> 할당해 둔 메모리를 다 쓰지 않으면 아까운 공간을 낭비하게 됩니다. 모든 이름을 25글자로 맞춰 놨는데 실제 평균 이름이 12글자라면, 절반 이상의 메모리를 그냥 버리는 셈이죠.</li>
<li><strong>사용 중인 공간 확인의 어려움:</strong> 어디까지가 진짜 데이터고 어디부터가 빈 공간인지 알기 어렵습니다. 문자열은 끝에 <code>\0</code>이 있어서 구분하기 쉽지만, <code>monster[24]</code> 같은 경우는 어떨까요? 이 몬스터가 살아있는지, 죽었는지, 아예 처음부터 존재하긴 했는지 알려면 상태를 체크하는 복잡한 코드가 추가로 필요해집니다.</li>
<li><strong>비좁은 공간 (스택 오버플로우):</strong> 일반적인 변수나 고정된 배열은 <strong>스택(Stack)</strong>이라는 메모리 공간에 저장됩니다. 문제는 이 스택이라는 공간이 보통 아주 작다는 거예요. (Visual Studio의 경우 기본적으로 딱 1MB만 줍니다.) 이 용량을 넘치게 쓰면 &#39;스택 오버플로우(Stack overflow)&#39;라는 에러가 터지면서 프로그램이 강제로 꺼져버립니다.</li>
</ol>
<p>Visual Studio에서 아래 코드를 실행해 보면 바로 프로그램이 뻗는 걸 볼 수 있습니다.</p>
<pre><code class="language-cpp">int main() {
    int array[1000000]; // 정수 100만 개 할당 (약 4MB 메모리 차지)
}
</code></pre>
<ol start="4">
<li><strong>가장 큰 문제, 억지스러운 제한과 에러:</strong> 공간을 미리 정해두면 프로그램이 유연하지 못하게 됩니다. 디스크에서 600개의 기록을 불러와야 하는데 우리가 500개짜리 공간만 만들어 뒀다면 어떻게 될까요? 사용자에게 에러를 띄우거나, 500개만 간신히 읽거나, 최악의 경우엔 공간이 터져버리면서(배열 오버플로우) 끔찍한 오류를 보게 될 것입니다.</li>
</ol>
<p>다행히도, 이 모든 골칫거리는 <strong>동적 메모리 할당</strong>을 통해 아주 쉽게 해결할 수 있습니다!
동적 메모리 할당이란 프로그램이 실행되는 도중에 &quot;운영 체제야, 나 지금 메모리가 좀 필요한데 빌려줄래?&quot; 하고 요청하는 방법입니다. 이 메모리는 비좁은 스택(Stack)이 아니라, 운영 체제가 넉넉하게 관리하는 <strong>힙(Heap)</strong>이라는 아주 거대한 메모리 창고에서 가져오게 됩니다. 요즘 컴퓨터에서 힙의 크기는 무려 기가바이트(GB) 단위랍니다!</p>
<hr>
<h3 id="단일-변수-동적으로-할당하기">단일 변수 동적으로 할당하기</h3>
<p>변수 한 개를 동적으로 할당하려면(빌리려면), 배열이 아닌 일반 형태의 <code>new</code> 연산자를 사용하면 됩니다.</p>
<pre><code class="language-cpp">new int; // 정수용 메모리를 동적으로 할당 (그리고 결과는 버림)
</code></pre>
<p>위 코드는 운영 체제에게 정수(int) 하나를 담을 만큼의 메모리를 달라고 요청하는 것입니다. <code>new</code> 연산자는 그 메모리를 사용해서 공간을 만든 다음, <strong>그 공간이 어디에 있는지 알려주는 &#39;주소(포인터)&#39;를 반환</strong>합니다.</p>
<p>보통은 이 반환된 주소를 잃어버리지 않게 포인터 변수에 잘 저장해 둡니다. 그래야 나중에 그 공간을 찾아갈 수 있거든요.</p>
<pre><code class="language-cpp">int* ptr{ new int }; // 정수 메모리를 동적으로 할당하고, 나중에 접근할 수 있게 그 주소를 ptr에 저장
</code></pre>
<p>이제 포인터가 가리키는 곳으로 찾아가서(역참조) 메모리를 사용할 수 있습니다.</p>
<pre><code class="language-cpp">*ptr = 7; // 할당받은 메모리에 7이라는 값을 저장
</code></pre>
<p>만약 지금까지 포인터를 왜 쓰는지 헷갈리셨다면, 이제 확실히 아시겠죠? 방금 빌린 넉넉한 메모리의 &#39;주소&#39;를 기억해둘 포인터가 없다면, 우리는 그 메모리를 두 번 다시 찾아가서 사용할 방법이 없기 때문입니다!</p>
<p>다만 주의할 점이 있어요. 힙(Heap)에 있는 데이터는 스택(Stack)에 있는 데이터보다 접근하는 속도가 살짝 느립니다. 스택에 있는 데이터는 컴파일러가 위치를 정확히 알고 있어서 바로 찾아가지만, 힙에 있는 데이터는 포인터라는 지도(주소)를 한 번 읽고, 그 주소로 다시 찾아가야 하는 두 번의 단계를 거쳐야 하기 때문이죠.</p>
<hr>
<h3 id="동적-메모리-할당은-어떻게-작동할까요">동적 메모리 할당은 어떻게 작동할까요?</h3>
<p>여러분의 컴퓨터에는 프로그램들이 쓸 수 있는 넉넉한 메모리가 있습니다. 프로그램을 실행하면 운영 체제가 이 메모리의 일부에 프로그램을 올려놓죠. 프로그램이 쓰는 메모리는 구역별로 역할이 나뉘어 있습니다. 어떤 곳은 코드가 들어가고, 어떤 곳은 함수나 지역 변수(스택)를 다루는 데 쓰입니다.</p>
<p>하지만 남아도는 엄청나게 많은 메모리는 누군가 &quot;나 좀 쓸게!&quot;라고 요청할 때까지 가만히 대기하고 있습니다.
여러분이 동적으로 메모리를 할당(<code>new</code>)한다는 것은, 운영 체제에게 &quot;이 남는 메모리 중 일부를 내 프로그램 전용으로 예약해 줘&quot;라고 부탁하는 것과 같습니다. 운영 체제가 수락하면, 그 메모리의 주소를 넘겨주죠. 이때부터 그 공간은 여러분 마음대로 쓸 수 있습니다.
그리고 다 쓰고 나면, 다른 프로그램이 쓸 수 있게 다시 운영 체제에 꼭 돌려줘야 합니다.</p>
<p>자동으로 정리되는 일반 메모리(스택)와 달리, 동적으로 빌린 메모리(힙)는 <strong>프로그램이 스스로 직접 요청하고 직접 반납해야 할 책임</strong>이 있습니다.</p>
<blockquote>
<p><strong>핵심 통찰</strong>
스택(Stack) 객체를 만들고 없애는 건 전부 자동으로 처리됩니다. 우리가 메모리 주소 같은 걸 신경 쓸 필요가 전혀 없죠.
하지만 힙(Heap) 객체는 자동으로 관리되지 않습니다. 우리가 직접 개입해야 해요! 다 썼을 때 정확히 어떤 객체를 부숴야 하는지 알기 위해, 우리는 그 객체를 가리키는 고유한 &#39;메모리 주소&#39;가 필요합니다.
<code>new</code> 연산자를 사용하면 새로 만든 객체의 주소가 담긴 포인터가 튀어나옵니다. 우리는 이 주소를 잘 저장해 뒀다가, 나중에 데이터를 읽거나 다 쓰고 버려달라고(파괴해 달라고) 요청할 때 써먹어야 합니다.</p>
</blockquote>
<hr>
<h3 id="동적으로-할당된-변수-초기화하기">동적으로 할당된 변수 초기화하기</h3>
<p>동적으로 변수를 만들 때, 값을 텅 비워두지 않고 곧바로 원하는 값을 넣어서(초기화해서) 만들 수도 있습니다.</p>
<pre><code class="language-cpp">int* ptr1{ new int (5) }; // 직접 초기화(direct initialization) 사용
int* ptr2{ new int { 6 } }; // 유니폼 초기화(uniform initialization) 사용
</code></pre>
<hr>
<h3 id="단일-변수-삭제반환하기">단일 변수 삭제(반환)하기</h3>
<p>동적으로 빌린 메모리를 다 썼다면, C++에게 &quot;이 메모리 이제 다시 가져가도 돼!&quot;라고 명확하게 말해줘야 합니다. 변수 하나를 반납할 때는 <code>delete</code> 연산자를 사용합니다.</p>
<pre><code class="language-cpp">// ptr이 이전에 new 연산자로 할당되었다고 가정합니다
delete ptr; // ptr이 가리키는 메모리를 운영 체제에 반환합니다
ptr = nullptr; // ptr을 빈 포인터(null pointer)로 설정합니다
</code></pre>
<hr>
<h3 id="메모리를-삭제한다는-것은-무슨-의미일까요">메모리를 삭제한다는 것은 무슨 의미일까요?</h3>
<p><code>delete</code>라는 단어 때문에 무언가를 완전히 &#39;삭제&#39;해버린다고 오해하기 쉽지만, 사실 이 연산자는 아무것도 지우지 않습니다! 그저 우리가 빌려 썼던 방을 <strong>운영 체제에게 다시 &quot;반납&quot;</strong>할 뿐입니다. 그러면 운영 체제는 빈 방이 된 그 공간을 다른 프로그램이나 나중에 다시 쓸 수 있게 준비해 둡니다.</p>
<p>문법만 보면 마치 <code>ptr</code>이라는 <strong>변수 자체를 지우는 것처럼 보이지만, 절대 그렇지 않아요!</strong> 포인터 변수 <code>ptr</code> 자체는 여전히 살아서 존재하며, 다른 변수들처럼 새로운 값(예: <code>nullptr</code>)을 다시 넣어줄 수도 있습니다.</p>
<p>동적으로 할당받지 않은 엉뚱한 포인터에 대고 <code>delete</code>를 하면 프로그램에 아주 심각한 문제가 생길 수 있으니 꼭 주의하세요.</p>
<hr>
<h3 id="댕글링-포인터-허공을-맴도는-포인터">댕글링 포인터 (허공을 맴도는 포인터)</h3>
<p>메모리를 반납(<code>delete</code>)하고 나면, C++는 반납된 메모리 안에 있던 데이터나 포인터의 값이 어떻게 될지 아무것도 보장해주지 않습니다. 대부분의 경우 운영 체제에 반납된 공간에는 원래 있던 숫자들이 그대로 남아있게 되고, 우리의 포인터 변수 역시 방금 반납해서 <strong>더 이상 우리 것이 아닌 그 메모리 주소를 여전히 가리킨 채로 남아있게 됩니다.</strong></p>
<p>이렇게 더 이상 내 것이 아닌, 이미 반납된 메모리를 가리키고 있는 포인터를 <strong>댕글링 포인터(Dangling pointer)</strong>라고 부릅니다. (마치 끊어진 밧줄에 대롱대롱 매달려 있다는 뜻이죠!) 이 댕글링 포인터의 값을 읽으려 하거나 또 <code>delete</code> 하려고 하면, 프로그램이 어떻게 미쳐 날뛸지 모르는 &#39;알 수 없는 행동(undefined behavior)&#39;이 발생합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main() {
    int* ptr{ new int }; // 정수를 동적으로 할당
    *ptr = 7; // 해당 메모리 위치에 값을 넣음

    delete ptr; // 메모리를 운영 체제에 반환. 이제 ptr은 허공을 가리키는 &#39;댕글링 포인터&#39;가 됨.

    std::cout &lt;&lt; *ptr; // 댕글링 포인터에 접근하면 알 수 없는 행동(undefined behavior) 발생
    delete ptr; // 이미 반환한 메모리를 또 반환하려고 해도 알 수 없는 행동 발생.

    return 0;
}
</code></pre>
<p>위 프로그램에서, 우리가 아까 넣었던 7이라는 숫자는 운 좋게 그대로 남아있을 수도 있지만, 다른 숫자로 바뀌어 버렸을 수도 있습니다. 더 무서운 점은, 그 메모리 공간이 이미 다른 프로그램의 차지가 되어버렸을 수도 있다는 거예요. 그런 남의 땅을 함부로 건드리려고 하면 운영 체제가 화를 내며 우리 프로그램을 강제로 종료시켜 버릴 겁니다.</p>
<p>메모리를 반납할 때 댕글링 포인터가 여러 개 생겨버릴 수도 있습니다. 다음 코드를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main() {
    int* ptr{ new int{} }; // 정수를 동적으로 할당
    int* otherPtr{ ptr }; // otherPtr도 이제 같은 메모리 위치를 가리킴

    delete ptr; // 메모리를 운영 체제에 반환. ptr과 otherPtr 둘 다 댕글링 포인터가 됨.
    ptr = nullptr; // ptr은 이제 안전한 빈 포인터(nullptr)가 됨

    // 하지만, otherPtr은 여전히 위험한 댕글링 포인터로 남아있음!

    return 0;
}
</code></pre>
<p>이런 골치 아픈 일을 막기 위한 꿀팁들이 있습니다.
첫째, 여러 개의 포인터가 똑같은 동적 메모리 하나를 가리키게 만드는 일은 웬만하면 피하세요. 어쩔 수 없다면, 정확히 어떤 포인터가 이 메모리의 &#39;진짜 주인(삭제할 책임이 있는 녀석)&#39;인지, 그리고 어떤 포인터가 그냥 놀러 온 &#39;손님(접근만 하는 녀석)&#39;인지 명확히 구분해야 합니다.
둘째, <code>delete</code>로 포인터를 지웠다면, 그 포인터가 곧바로 사라질 상황이 아니라면 <strong>무조건 <code>nullptr</code>로 설정해서 비워두세요.</strong> </p>
<blockquote>
<p><strong>권장 사항</strong>
포인터를 <code>delete</code> 한 직후에 포인터 자체가 수명을 다해 사라지는 게 아니라면, 무조건 <code>nullptr</code> (빈 포인터)로 설정하세요.</p>
</blockquote>
<hr>
<h3 id="new-연산자도-실패할-수-있어요">new 연산자도 실패할 수 있어요</h3>
<p>드문 일이긴 하지만, 운영 체제한테 메모리를 달라고 부탁했을 때 운영 체제가 &quot;미안, 남은 공간이 없어!&quot; 하고 거절할 수도 있습니다.</p>
<p>기본적으로 <code>new</code>가 실패하면 <code>bad_alloc</code>이라는 &#39;예외(Exception)&#39;가 터집니다. 아직 우리는 예외 처리 방법을 배우지 않았기 때문에, 이런 일이 생기면 프로그램은 그냥 픽 쓰러져서(크래시) 꺼져버릴 겁니다.</p>
<p>프로그램이 이렇게 픽픽 꺼지는 건 보통 원하지 않으실 테니, <code>new</code>가 실패했을 때 프로그램을 끄지 말고 조용히 &#39;빈 포인터(null pointer)&#39;를 반환하게 만드는 방법이 있습니다. <code>new</code>와 타입 사이에 <code>(std::nothrow)</code>라는 마법의 주문을 넣으면 됩니다.</p>
<pre><code class="language-cpp">int* value { new (std::nothrow) int }; // 정수 할당에 실패하면 value는 빈 포인터(null pointer)가 됨
</code></pre>
<p>만약 공간을 빌리는 데 실패하면, 주소 대신 <code>null</code>(아무것도 없음)이 들어옵니다.
주의할 점은, 실패해서 텅 비어버린 포인터를 억지로 읽으려고 하면 프로그램이 뻗어버린다는 것입니다. 그래서 가장 좋은 습관은 메모리를 달라고 요청한 뒤에 <strong>&quot;진짜 제대로 빌렸나?&quot; 하고 먼저 확인해 보는 것</strong>입니다.</p>
<pre><code class="language-cpp">int* value { new (std::nothrow) int{} }; // 정수 크기의 메모리를 요청
if (!value) // new가 null을 반환한 경우 (실패한 경우) 처리
{
    // 여기서 에러 처리를 합니다
    std::cerr &lt;&lt; &quot;메모리를 할당할 수 없습니다\n&quot;;
}
</code></pre>
<p>물론 최신 컴퓨터에선 메모리가 꽉 차서 실패하는 일이 거의 없기 때문에 (특히 연습할 때는 더더욱요), 이 확인 과정을 깜빡 잊기 아주 쉽답니다!</p>
<hr>
<h3 id="빈-포인터null-pointer와-동적-메모리-할당">빈 포인터(Null pointer)와 동적 메모리 할당</h3>
<p>빈 포인터(아무것도 가리키지 않는 <code>nullptr</code>)는 동적 메모리를 다룰 때 엄청나게 유용합니다. 동적 할당의 세계에서 빈 포인터는 사실상 <strong>&quot;나 아직 메모리 공간 못 받았어&quot;</strong>라는 뜻과 같습니다. 덕분에 이런 식의 스마트한 코드를 짤 수 있어요.</p>
<pre><code class="language-cpp">// ptr이 아직 할당되지 않았다면, 새로 할당합니다
if (!ptr)
    ptr = new int;
</code></pre>
<p>참고로 빈 포인터에 대고 <code>delete</code>를 하면 그냥 아무 일도 일어나지 않고 무사히 넘어갑니다. 따라서 굳이 아래처럼 복잡하게 쓸 필요가 없습니다.</p>
<pre><code class="language-cpp">if (ptr) // ptr이 빈 포인터가 아니라면
    delete ptr; // 삭제해라
// 그렇지 않으면 아무것도 하지 마라
</code></pre>
<p>그냥 쿨하게 한 줄만 쓰면 됩니다.</p>
<pre><code class="language-cpp">delete ptr;
</code></pre>
<p><code>ptr</code>에 진짜 메모리가 들어있다면 깔끔하게 반납될 것이고, 비어있는(<code>null</code>) 상태라면 아무 일도 일어나지 않을 테니까요.</p>
<blockquote>
<p><strong>권장 사항</strong>
빈 포인터(null pointer)를 <code>delete</code> 하는 건 완벽하게 안전하며 아무 일도 일으키지 않습니다. 굳이 <code>if</code> 문을 써서 검사할 필요가 없습니다.</p>
</blockquote>
<hr>
<h3 id="메모리-누수">메모리 누수</h3>
<p>동적으로 빌린 메모리는 우리가 명시적으로 &quot;반납할게!&quot;(<code>delete</code>)라고 하거나, 프로그램이 아예 끝날 때까지 끈질기게 자기 자리를 지킵니다. (물론 똑똑한 운영 체제가 끝나면 알아서 치워주긴 하지만요.) 하지만 이 메모리의 주소를 들고 있는 &#39;포인터 변수&#39; 자체는 일반 변수라서 함수가 끝나면 수명이 다해 사라져 버립니다. 여기서 아주 치명적인 엇박자가 발생합니다.</p>
<p>다음 함수를 한 번 보세요.</p>
<pre><code class="language-cpp">void doSomething() {
    int* ptr{ new int{} };
}
</code></pre>
<p>이 함수는 안에서 정수 하나를 동적으로 빌리지만, <code>delete</code>를 통해 반납하지는 않고 있습니다. 포인터 변수 <code>ptr</code>은 그저 평범한 지역 변수일 뿐이라서, 함수가 끝나는 순간 뿅 하고 사라집니다. 문제는 <code>ptr</code>만이 그 빌린 메모리의 주소를 알고 있는 유일한 변수였다는 점이죠. <code>ptr</code>이 사라지면서, 프로그램은 동적으로 빌린 메모리가 <strong>어디 있는지 알 수 있는 지도를 영영 잃어버리게 됩니다.</strong> 지도가 없으니 영원히 찾아가서 반납할(<code>delete</code>) 수가 없는 상태가 돼버리는 거죠.</p>
<p>이런 끔찍한 상황을 <strong>메모리 누수(Memory leak)</strong>라고 부릅니다. 운영 체제에 돌려주지도 않은 채로 메모리 주소를 까먹어 버린 상황이죠. 우리 프로그램은 지도를 잃어버려서 지울 수 없고, 운영 체제 입장에서는 &quot;아직 쟤네 프로그램이 저 공간 쓰고 있네&quot;라고 생각해서 다른 곳에 빌려주지도 못합니다. 그야말로 우주 미아가 되어버린 공간이죠.</p>
<p>이런 메모리 누수들이 자꾸 쌓이면 프로그램이 빈 메모리를 야금야금 다 갉아먹게 됩니다. 우리 프로그램뿐만 아니라 컴퓨터에 켜져 있는 다른 프로그램들까지 쓸 메모리가 부족해지죠. 누수가 심한 프로그램은 메모리를 싹 다 먹어 치워서 컴퓨터 전체를 렉 걸리게 만들거나 심지어 블루스크린을 띄울 수도 있습니다. 프로그램이 완전히 종료되고 나서야 운영 체제가 빗자루를 들고 와서 잃어버렸던 메모리들을 싹 청소하고 되찾아갑니다.</p>
<p>포인터 변수가 사라질 때만 메모리 누수가 생기는 건 아닙니다. 메모리 주소를 들고 있던 포인터에 덮어쓰기로 다른 값을 넣어버려도 똑같이 지도를 잃어버리게 됩니다.</p>
<pre><code class="language-cpp">int value = 5;
int* ptr{ new int{} }; // 메모리 할당
ptr = &amp;value; // 기존에 할당받은 주소를 잃어버림. 메모리 누수 발생!
</code></pre>
<p>이럴 때는 새 주소를 넣기 전에 원래 갖고 있던 메모리를 확실하게 먼저 반납해주면 됩니다.</p>
<pre><code class="language-cpp">int value{ 5 };
int* ptr{ new int{} }; // 메모리 할당
delete ptr; // 메모리를 운영 체제에 다시 돌려줌
ptr = &amp;value; // 포인터에 value의 주소를 새로 지정
</code></pre>
<p>비슷한 이유로, 같은 포인터에 메모리 할당을 두 번 연속으로 해도 메모리 누수가 발생합니다.</p>
<pre><code class="language-cpp">int* ptr{ new int{} };
ptr = new int{}; // 예전 주소를 잃어버림. 메모리 누수 발생!
</code></pre>
<p>두 번째로 빌려온 메모리의 주소가 첫 번째 주소를 덮어써 버리기 때문에, 처음 빌렸던 메모리의 주소는 영영 잃어버리게 됩니다! 이 역시 덮어쓰기 전에 꼭 먼저 <code>delete</code>를 해줘서 예방해야 합니다.</p>
<hr>
<h3 id="결론">결론</h3>
<ul>
<li><code>new</code>와 <code>delete</code> 연산자를 사용하면 프로그램에 필요한 단일 변수를 동적으로 빌리고 반납할 수 있습니다.</li>
<li>동적으로 빌린 메모리는 여러분이 직접 반납하거나 프로그램이 끝날 때까지 계속 살아남습니다.</li>
<li>이미 반납해서 내 것이 아닌 곳을 가리키는 포인터(댕글링 포인터)나 텅 빈 포인터를 억지로 읽으려고 하면 큰일이 나니 항상 조심하세요.</li>
</ul>
<p>다음 강의에서는 <code>new</code>와 <code>delete</code>를 사용해서 배열을 동적으로 만들고 지우는 방법에 대해 알아보겠습니다!</p>
<hr>
<h2 id="192--배열-동적-할당하기">19.2 — 배열 동적 할당하기</h2>
<p>변수 하나를 필요할 때 그때그때(동적으로) 만들 수 있는 것처럼, 여러 변수가 이어져 있는 &#39;배열&#39;도 필요할 때마다 동적으로 만들 수 있습니다.</p>
<p>일반적인 &#39;고정 배열&#39;은 프로그램을 만들기 전(컴파일 타임)에 미리 그 크기를 딱 정해두어야 하죠. 하지만 <strong>배열을 동적으로 할당</strong>하면, 프로그램이 실행 중인 상태(런타임)에서 우리가 원하는 만큼 배열의 길이를 마음대로 정할 수 있답니다! (즉, 배열의 길이가 항상 고정된 상수, 즉 <code>constexpr</code>일 필요가 없다는 뜻이에요.)</p>
<p>일반적인 &#39;고정 배열&#39;은 프로그램을 만들기 전(컴파일 타임)에 미리 그 크기를 딱 정해두어야 하죠. 하지만 <strong>배열을 동적으로 할당</strong>하면, 프로그램이 실행 중인 상태(런타임)에서 우리가 원하는 만큼 배열의 길이를 마음대로 정할 수 있답니다! (즉, 배열의 길이가 항상 고정된 상수, 즉 <code>constexpr</code>일 필요가 없다는 뜻이에요.)</p>
<blockquote>
<p><strong>저자의 노트</strong>
이번 강의에서는 가장 흔하게 쓰이는 방식인 &#39;C언어 스타일의 배열&#39;을 동적으로 할당해 볼 거예요.
참고로 최신 C++ 기능인 <code>std::array</code>를 동적으로 할당할 수도 있지만, 보통 이런 상황에서는 굳이 동적 할당을 직접 하기보다는 알아서 크기가 조절되는 <code>std::vector</code>를 사용하는 것이 훨씬 편하고 좋습니다.</p>
</blockquote>
<p>배열을 동적으로 만들려면, 일반적인 방법 대신 <code>new</code>와 <code>delete</code>의 <strong>배열 전용 버전</strong>
(보통 <code>new[]</code> 와 <code>delete[]</code>라고 부름)을 사용해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;cstddef&gt;
#include &lt;iostream&gt;

int main()
{
    std::cout &lt;&lt; &quot;Enter a positive integer: &quot;;
    std::size_t length{};
    std::cin &gt;&gt; length;

    int* array{ new int[length]{} }; // 배열 전용 new를 사용합니다. 길이가 상수일 필요가 없다는 점에 주목하세요!

    std::cout &lt;&lt; &quot;I just allocated an array of integers of length &quot; &lt;&lt; length &lt;&lt; &#39;\n&#39;;

    array[0] = 5; // 0번째 요소를 값 5로 설정합니다.

    delete[] array; // 배열 전용 delete를 사용하여 배열 메모리를 해제합니다.

    // 어차피 이 코드 직후에 범위를 벗어나기 때문에(스코프 종료), 여기서 array를 nullptr이나 0으로 설정할 필요는 없습니다.

    return 0;
}
</code></pre>
<p>우리가 &quot;배열을 만들어줘!&quot;라고 대괄호(<code>[]</code>)를 사용해 명령했기 때문에, C++은 똑똑하게 일반 <code>new</code> 대신 배열용 <code>new</code>를 써야 한다는 걸 알아챕니다. 코드에서 <code>new</code> 글자 바로 옆에 <code>[]</code>가 붙어있지는 않지만, 내부적으로는 <code>new[]</code> 기능이 작동하는 것이죠.</p>
<p>동적으로 만든 배열의 길이는 <code>std::size_t</code>라는 타입을 가집니다. 만약 일반 <code>int</code> 타입(상수가 아닌 것)을 길이로 사용하려고 하면, 컴퓨터가 &quot;데이터가 손실될 수도 있어!&quot;라며 경고를 보낼 수 있습니다. 이럴 때는 안전하게 <code>static_cast</code>를 써서 <code>std::size_t</code>로 타입을 바꿔주는 것이 좋습니다.</p>
<p>여기서 아주 중요한 사실이 있어요! 동적 배열은 일반 고정 배열과는 <strong>완전히 다른 넉넉한 공간</strong>에서 메모리를 빌려옵니다. 그래서 크기를 엄청나게 크게 만들 수 있죠. 위 프로그램을 직접 실행해서 배열 길이를 1,000,000(백만)이나 심지어 100,000,000(일억)으로 입력해도 문제없이 돌아갑니다. 한번 직접 해보세요!</p>
<p>이런 엄청난 장점 때문에, C++에서 메모리를 아주 많이 써야 하는 프로그램들은 대부분 이렇게 &#39;동적 할당&#39; 방식을 사용한답니다.</p>
<hr>
<h3 id="동적으로-할당된-배열-삭제하기">동적으로 할당된 배열 삭제하기</h3>
<p>동적으로 빌려온 배열을 다 쓰고 나서 돌려줄(삭제할) 때는, 반드시 <strong>배열 전용 삭제 기호인 <code>delete[]</code></strong>를 사용해야 합니다.</p>
<p>이것은 컴퓨터(CPU)에게 &quot;변수 하나만 덜렁 지우는 게 아니라, 여러 개가 묶인 배열 전체를 싹 다 청소해야 해!&quot;라고 알려주는 역할을 합니다. 초보 프로그래머들이 정말 자주 하는 치명적인 실수 중 하나가, 배열을 지울 때 <code>delete[]</code> 대신 그냥 <code>delete</code>를 써버리는 것입니다. 배열에 일반 <code>delete</code>를 쓰면 데이터가 꼬이거나, 메모리가 줄줄 새거나, 프로그램이 갑자기 튕기는 등 아주 끔찍한 일(정의되지 않은 동작)이 발생할 수 있습니다.</p>
<p>여기서 많은 분들이 궁금해하는 점이 있습니다. *&quot;컴퓨터는 배열을 지울 때 도대체 얼마나 큰 메모리를 지워야 하는지 어떻게 아는 걸까요?&quot;* 정답은, 처음에 <code>new[]</code>로 배열을 만들 때 컴퓨터가 몰래 <strong>&#39;이 변수에 얼마나 큰 메모리를 빌려줬는지&#39; 기록</strong>해두기 때문입니다. 그래서 나중에 <code>delete[]</code>를 부르면 알아서 딱 그만큼만 정확히 지워줍니다. 하지만 아쉽게도 프로그래머가 이 &#39;숨겨진 길이 정보&#39;를 코드에서 직접 꺼내 볼 수는 없습니다.</p>
<hr>
<h3 id="동적-배열은-고정-배열과-거의-똑같습니다">동적 배열은 고정 배열과 거의 똑같습니다</h3>
<p>예전 &#39;17.8 강의&#39;에서 고정 배열은 &#39;첫 번째 데이터가 있는 메모리 주소&#39;를 들고 있다는 걸 배웠습니다. 또한, 배열이 <strong>첫 번째 데이터를 가리키는 &#39;포인터&#39;로 자연스럽게 변신(Decay)</strong>할 수 있다는 것도 배웠죠. 이렇게 포인터로 변해버리면 배열의 전체 길이나 크기(<code>sizeof()</code>)를 알 수 없게 되지만, 그 외에는 작동 방식에 큰 차이가 없습니다.</p>
<p><strong>동적 배열도 처음 태어날 때부터 아예 &#39;첫 번째 데이터를 가리키는 포인터&#39;로 시작합니다.</strong> 그래서 고정 배열이 포인터로 변했을 때처럼, 스스로 자기 길이가 얼마인지 모른다는 똑같은 단점을 가지고 있어요.</p>
<p>한마디로, 동적 배열은 포인터로 변신한 고정 배열과 기능적으로 완전히 똑같습니다. 단 하나, <strong>프로그래머가 다 쓴 뒤에 직접 <code>delete[]</code>를 써서 청소해 줘야 한다는 책임감</strong>만 추가된 셈이죠!</p>
<hr>
<h3 id="동적으로-할당된-배열-초기화하기">동적으로 할당된 배열 초기화하기</h3>
<p>동적으로 만든 배열의 모든 칸을 깔끔하게 <code>0</code>으로 채우고(초기화하고) 싶다면, 작성법은 아주 간단합니다. 끝에 빈 중괄호 <code>{}</code>만 붙여주면 돼요!</p>
<pre><code class="language-cpp">int* array{ new int[length]{} };
</code></pre>
<p>옛날 버전인 C++11 이전에는, 동적 배열에 <code>0</code>이 아닌 다른 숫자들을 한 번에 넣기가 무척 어려웠습니다. (여러 숫자를 한 번에 넣는 초기화 리스트는 고정 배열에서만 쓸 수 있었거든요.) 그래서 아래처럼 일일이 배열 칸을 하나씩 돌면서 값을 지정해 줘야만 했습니다.</p>
<pre><code class="language-cpp">int* array = new int[5];
array[0] = 9;
array[1] = 7;
array[2] = 5;
array[3] = 3;
array[4] = 1;
</code></pre>
<p>정말 귀찮고 번거로운 일이었죠!
하지만 C++11 버전부터는 드디어 동적 배열에도 <strong>중괄호 <code>{}</code>(초기화 리스트)</strong>를 써서 원하는 값들을 한방에 쏙쏙 넣을 수 있게 되었습니다!</p>
<pre><code class="language-cpp">int fixedArray[5] = { 9, 7, 5, 3, 1 }; // C++11 이전: 고정 배열 초기화 방식
int* array{ new int[5]{ 9, 7, 5, 3, 1 } }; // C++11 이후: 동적 배열 초기화 방식

// 똑같은 타입 이름(int)을 두 번씩 쓰기 귀찮다면 auto를 쓸 수도 있습니다. 이름이 긴 타입일 때 아주 유용해요.
auto* array{ new int[5]{ 9, 7, 5, 3, 1 } };
</code></pre>
<p>여기서 주의할 점은, 배열 길이(<code>[5]</code>)와 중괄호(<code>{...}</code>) 사이에는 등호(<code>=</code>)가 들어가지 않는다는 것입니다.</p>
<p>코드를 헷갈리지 않고 일관성 있게 쓰기 위해, 일반 고정 배열을 만들 때도 위와 비슷한 중괄호 방식(균일 초기화)을 쓸 수 있습니다.</p>
<pre><code class="language-cpp">int fixedArray[]{ 9, 7, 5, 3, 1 }; // C++11에서 고정 배열 초기화하기
char fixedArray[]{ &quot;Hello, world!&quot; }; // C++11에서 고정 배열(문자열) 초기화하기
</code></pre>
<p>이때 대괄호 <code>[]</code> 안에 배열의 크기를 굳이 적어주지 않아도 컴퓨터가 알아서 세어주니 생략해도 괜찮습니다.</p>
<hr>
<h3 id="배열-크기-조절하기">배열 크기 조절하기</h3>
<p>동적 할당을 사용하면 처음에 배열을 만들 때 우리가 원하는 만큼 길이를 정할 수 있죠. 하지만 아쉽게도 <strong>C++에는 &#39;이미 만들어진 배열의 크기&#39;를 나중에 고무줄처럼 쭈욱 늘리거나 줄이는 기능이 기본적으로 없습니다.</strong> 만약 크기를 꼭 바꿔야 한다면 어떻게 해야 할까요?</p>
<ol>
<li>더 큰 새 배열을 새로 할당하고,</li>
<li>옛날 배열에 있던 데이터를 하나씩 복사해서 옮긴 다음,</li>
<li>옛날 배열을 지워버리는</li>
</ol>
<p>이런 복잡한 꼼수를 써야 합니다. 하지만 이 과정은 실수하기 딱 좋고, 데이터가 복잡한 객체(클래스)일 경우에는 만들어지는 규칙이 까다로워서 예상치 못한 오류가 뻥뻥 터지기 쉽습니다.</p>
<p>결론적으로, 이렇게 복잡하게 배열 크기를 직접 조절하려고 애쓰지 마세요! 크기가 유연하게 변하는 배열이 필요하다면 <strong>C++에서 제공하는 아주 똑똑한 도구인 <code>std::vector</code>를 대신 사용하는 것을 강력히 추천</strong>합니다. 복잡한 과정은 얘가 알아서 다 해주거든요!</p>
<hr>
<h2 id="193--소멸자-destructors">19.3 — 소멸자 (Destructors)</h2>
<p>소멸자(destructor)는 클래스로 만든 객체가 파괴될(사라질) 때 실행되는 또 다른 특별한 멤버 함수입니다. 쉽게 비유하자면, <strong>생성자가 &#39;방에 들어올 때 불을 켜는(초기화) 역할&#39;이라면, 소멸자는 &#39;방에서 나갈 때 불을 끄고 청소하는(정리) 역할&#39;</strong>이라고 생각하시면 됩니다!</p>
<p>객체가 정해진 수명을 다해서 자연스럽게 사라지거나, 우리가 <code>delete</code> 키워드를 써서 직접 지웠을 때, 메모리에서 완전히 지워지기 직전에 필요한 뒷정리를 하도록 소멸자가 <strong>자동으로</strong> 불립니다. 단순한 값을 저장하는 기본 클래스들은 C++이 알아서 깔끔하게 치워주기 때문에 굳이 소멸자를 만들 필요가 없습니다.</p>
<p>하지만 만약 여러분의 클래스 객체가 특별한 자원(예: 동적 메모리 공간, 파일, 데이터베이스 등)을 쥐고 있거나 객체가 사라지기 전에 꼭 마무리해야 할 작업이 있다면 소멸자가 아주 유용하게 쓰입니다. 소멸자는 객체가 세상에서 사라지기 직전에 마지막으로 실행되는 곳이니까요!</p>
<hr>
<h3 id="소멸자-이름-짓기">소멸자 이름 짓기</h3>
<p>생성자처럼 소멸자도 이름을 짓는 특별한 규칙이 있습니다:</p>
<ul>
<li>클래스 이름과 완전히 똑같아야 하며, 맨 앞에 <strong>물결표(<code>~</code>)</strong>를 붙여야 합니다.</li>
<li>매개변수(괄호 안에 넣는 값)를 받을 수 없습니다.</li>
<li>반환 타입(return type)이 없습니다.</li>
<li>하나의 클래스에는 <strong>단 한 개의 소멸자</strong>만 만들 수 있습니다.</li>
</ul>
<p>일반적으로는 코드에서 소멸자를 직접 부를(호출할) 필요가 없습니다. 객체가 죽을 때 알아서 불리기 때문이죠. 굳이 한 번 청소한 곳을 두 번 청소할 이유는 없으니까요! 하지만 소멸자가 실행되는 동안 객체가 아직 완전히 파괴된 것은 아니기 때문에, 소멸자 안에서 다른 멤버 함수를 부르는 것은 안전합니다.</p>
<hr>
<h3 id="소멸자-예제">소멸자 예제</h3>
<p>소멸자를 사용하는 간단한 클래스를 코드로 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cassert&gt;
#include &lt;cstddef&gt;

class IntArray
{
private:
    int* m_array{};
    int m_length{};

public:
    IntArray(int length) // 생성자: 객체가 태어날 때 실행됨
    {
        assert(length &gt; 0);

        m_array = new int[static_cast&lt;std::size_t&gt;(length)]{};
        m_length = length;
    }

    ~IntArray() // 소멸자: 객체가 사라질 때 실행됨
    {
        // 앞에서 동적으로 빌려왔던 배열 메모리를 깨끗하게 삭제(반납)합니다.
        delete[] m_array;
    }

    void setValue(int index, int value) { m_array[index] = value; }
    int getValue(int index) { return m_array[index]; }

    int getLength() { return m_length; }
};

int main()
{
    IntArray ar ( 10 ); // 10개의 정수 공간을 가진 배열을 할당합니다.
    for (int count{ 0 }; count &lt; ar.getLength(); ++count)
        ar.setValue(count, count+1);

    std::cout &lt;&lt; &quot;5번째 요소의 값은: &quot; &lt;&lt; ar.getValue(5) &lt;&lt; &#39;\n&#39;;

    return 0;
} // 여기서 ar 객체의 수명이 끝나고 파괴되므로, ~IntArray() 소멸자 함수가 자동으로 불립니다!
</code></pre>
<blockquote>
<p><strong>팁</strong>
위 예제를 실행(컴파일)할 때 만약 다음과 같은 에러가 뜬다면:
<code>error: &#39;class IntArray&#39; has pointer data members [-Werror=effc++]|</code>
<code>error:   but does not override &#39;IntArray(const IntArray&amp;)&#39; [-Werror=effc++]|</code>
<code>error:   or &#39;operator=(const IntArray&amp;)&#39; [-Werror=effc++]|</code>
이 예제를 실행할 때는 컴파일 설정에서 <code>-Weffc++</code> 플래그를 빼주시거나, 클래스 안에 다음 두 줄의 코드를 추가해 주시면 됩니다.
<code>IntArray(const IntArray&amp;) = delete;</code>
<code>IntArray&amp; operator=(const IntArray&amp;) = delete;</code>
멤버에 <code>=delete</code>를 사용하는 방법은 나중에 &#39;14.14 - 복사 생성자 소개&#39; 강의에서 자세히 다룰 예정이니 지금은 걱정하지 마세요!</p>
</blockquote>
<p>이 프로그램을 실행하면 다음과 같은 결과가 나옵니다:
<code>5번째 요소의 값은: 6</code></p>
<p>천천히 살펴볼게요. <code>main()</code> 함수의 첫 번째 줄에서 우리는 <code>ar</code>이라는 이름의 <code>IntArray</code> 객체를 만들고, 크기로 10을 넘겨주었습니다. 이때 <strong>생성자</strong>가 불리면서 배열을 위한 메모리 공간을 컴퓨터로부터 빌려옵니다(이것을 동적 할당이라고 합니다). 배열의 크기를 코드를 짤 때 미리 알 수 없고 사용하는 사람이 정하기 때문에, 이렇게 직접 메모리를 빌려와야만 합니다.</p>
<p>그리고 <code>main()</code> 함수가 끝날 때, <code>ar</code> 객체의 수명이 다해서 죽게 됩니다. 그러면 똑똑한 C++은 자동으로 <code>~IntArray()</code> <strong>소멸자</strong>를 부르고, 생성자에서 빌려왔던 메모리를 잊지 않고 깨끗하게 반납(<code>delete</code>)해 줍니다!</p>
<blockquote>
<p><strong>알림</strong>
지난 &#39;16.2 - std::vector와 list 생성자 소개&#39;에서, 배열이나 리스트 같은 컨테이너 클래스의 &#39;길이&#39;를 정해서 초기화할 때는 괄호 <code>()</code>를 사용하는 것이 좋다고 배웠습니다. (요소들의 목록을 직접 넣을 때와 구분하기 위해서요!) 그래서 이 예제에서도 <code>IntArray ar ( 10 );</code> 처럼 괄호를 사용해 초기화했습니다.</p>
</blockquote>
<hr>
<h3 id="생성자와-소멸자의-타이밍">생성자와 소멸자의 타이밍</h3>
<p>앞에서 말했듯이, 생성자는 객체가 태어날 때 불리고 소멸자는 객체가 사라질 때 불립니다. 진짜로 그런지 <code>cout</code> (출력) 기능을 사용해서 언제 불리는지 직접 눈으로 확인해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Simple
{
private:
    int m_nID{};

public:
    Simple(int nID)
        : m_nID{ nID }
    {
        std::cout &lt;&lt; nID &lt;&lt; &quot;번 Simple 생성 중 (Constructing)\n&quot;;
    }

    ~Simple()
    {
        std::cout &lt;&lt; m_nID &lt;&lt; &quot;번 Simple 소멸 중 (Destructing)\n&quot;;
    }

    int getID() { return m_nID; }
};

int main()
{
    // 1번: 일반적인 방식(스택 메모리)으로 Simple 객체를 만듭니다.
    Simple simple{ 1 };
    std::cout &lt;&lt; simple.getID() &lt;&lt; &#39;\n&#39;;

    // 2번: 동적 할당(new 키워드)으로 Simple 객체를 만듭니다.
    Simple* pSimple{ new Simple{ 2 } };

    std::cout &lt;&lt; pSimple-&gt;getID() &lt;&lt; &#39;\n&#39;;

    // pSimple은 동적으로 빌려왔기 때문에, 우리가 꼭 delete로 직접 지워줘야 합니다.
    delete pSimple;

    return 0;
} // 1번 객체(simple)는 함수가 끝나는 여기서 수명이 다해 파괴됩니다.
</code></pre>
<p>이 프로그램을 실행하면 다음과 같은 결과가 나옵니다:
<code>1번 Simple 생성 중 (Constructing)</code>
<code>1</code>
<code>2번 Simple 생성 중 (Constructing)</code>
<code>2</code>
<code>2번 Simple 소멸 중 (Destructing)</code>
<code>1번 Simple 소멸 중 (Destructing)</code></p>
<p>결과를 잘 보세요! &#39;1번&#39;이 먼저 만들어졌지만, 소멸(파괴)될 때는 &#39;2번&#39;이 먼저 죽고 &#39;1번&#39;이 나중에 죽었습니다. 그 이유는 우리가 <code>delete pSimple;</code> 코드를 통해 함수가 끝나기도 전에 &#39;2번&#39;을 직접 지워버렸기 때문입니다. 반면에 &#39;1번(<code>simple</code>)&#39;은 <code>main()</code> 함수가 완전히 끝날 때까지 살아있다가 마지막에 알아서 사라진 것이죠.</p>
<p>참고로, 전체 프로그램 어디서든 쓸 수 있는 전역 변수(Global variables)는 <code>main()</code> 함수가 시작되기도 전에 먼저 만들어지고, <code>main()</code> 함수가 끝난 후 제일 마지막에 파괴됩니다.</p>
<hr>
<h3 id="raii-resource-acquisition-is-initialization">RAII (Resource Acquisition Is Initialization)</h3>
<p>이름이 조금 어렵고 길죠? RAII(자원 획득은 초기화다)는 C++에서 아주 중요하게 쓰이는 프로그래밍 규칙입니다. 가장 핵심적인 아이디어는 <strong>&quot;자원(메모리, 파일 등)을 사용하는 기간을 객체가 살아있는 기간과 똑같이 맞추자!&quot;</strong>라는 것입니다.</p>
<p>C++에서는 이 훌륭한 아이디어를 생성자와 소멸자로 실천합니다. 객체가 태어날 때(생성자) 메모리나 파일 같은 자원을 빌려옵니다. 그리고 살아있는 동안 그 자원을 유용하게 씁니다. 마지막으로 객체가 죽을 때(소멸자) 쥐고 있던 자원을 자동으로 반납합니다.</p>
<p>이 RAII 방식의 가장 큰 장점은 <strong>컴퓨터의 자원이 새어나가는 것(메모리 누수)을 완벽하게 막아준다는 것</strong>입니다. 우리가 깜빡하고 자원 반납을 잊어버려도 객체가 죽을 때 알아서 청소해주기 때문이죠!</p>
<p>이 수업 맨 처음 살펴본 <code>IntArray</code> 클래스가 바로 이 RAII를 보여주는 완벽한 예시입니다. (생성자에서 메모리를 할당받고, 소멸자에서 반환했죠). 여러분이 즐겨 쓰시는 <code>std::string</code>이나 <code>std::vector</code> 같은 표준 기능들도 모두 이 똑똑한 RAII 방식을 따르고 있어서 알아서 메모리를 관리해 준답니다.</p>
<hr>
<h3 id="stdexit-함수-사용-시-주의사항">std::exit() 함수 사용 시 주의사항</h3>
<p>만약 코드 중간에 프로그램 작동을 멈추기 위해 <code>std::exit()</code> 함수를 사용하면, 프로그램이 바로 강제 종료되면서 <strong>어떤 소멸자도 불리지 않습니다.</strong> 소멸자가 로그를 남기거나 중요한 데이터를 저장하는 등 꼭 필요한 마무리 작업을 하도록 만들어 두었다면, 이 함수를 쓸 때 매우 주의해야 합니다! 작업이 마무리되지 않은 채로 프로그램이 꺼져버리니까요.</p>
<hr>
<h3 id="요약">요약</h3>
<p>정리하자면, 생성자와 소멸자를 짝꿍처럼 잘 활용하면 클래스가 스스로 초기화도 하고 마무리 청소까지 알아서 척척 해냅니다. 개발자가 메모리를 지우는 걸 깜빡하는 등의 실수를 할 확률을 확 줄여주고, 클래스를 사용하는 사람 입장에서도 훨씬 편안하게 코드를 짤 수 있게 해주는 아주 고마운 기능입니다!</p>
<hr>
<h2 id="194--포인터의-포인터이중-포인터와-동적-다차원-배열">19.4 — 포인터의 포인터(이중 포인터)와 동적 다차원 배열</h2>
<blockquote>
<p><strong>고급 학습자를 위한 내용</strong>
이 레슨은 선택 사항이며, C++에 대해 더 깊이 알고 싶은 분들을 위한 내용입니다. 앞으로의 레슨에서 이 내용을 몰라도 문제없으니 편하게 읽어보세요!</p>
</blockquote>
<p>포인터의 포인터(이중 포인터)는 이름 그대로예요. 
바로 &#39;다른 포인터의 주소를 저장하는 포인터&#39;랍니다.</p>
<hr>
<h3 id="포인터의-포인터-pointers-to-pointers">포인터의 포인터 (Pointers to pointers)</h3>
<p>일반적으로 정수(<code>int</code>)를 가리키는 포인터는 별표(<code>*</code>) 하나를 써서 만듭니다.</p>
<pre><code class="language-cpp">int* ptr; // int를 가리키는 포인터, 별표 한 개
</code></pre>
<p><code>int</code>를 가리키는 포인터의 주소를 담는 &#39;포인터의 포인터&#39;는 별표를 두 개 씁니다.</p>
<pre><code class="language-cpp">int** ptrptr; // int 포인터를 가리키는 포인터, 별표 두 개
</code></pre>
<p>포인터의 포인터도 일반 포인터와 똑같이 작동해요. &#39;역참조&#39;(dereference, <code>*</code> 기호를 써서 가리키는 곳의 값을 가져오는 것)를 하면 원래 포인터가 가지고 있던 값을 꺼내올 수 있죠. 그리고 그 꺼내온 값 역시 포인터이기 때문에, 한 번 더 역참조를 하면 마침내 맨 처음 저장했던 진짜 숫자 값을 얻을 수 있습니다. 이렇게 역참조를 연달아 할 수 있어요.</p>
<pre><code class="language-cpp">int value { 5 };
int* ptr { &amp;value };
std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // int 포인터를 역참조해서 int 값을 가져옴

int** ptrptr { &amp;ptr };
std::cout &lt;&lt; **ptrptr &lt;&lt; &#39;\n&#39;; // 한 번 역참조해서 int 포인터를 가져오고, 한 번 더 역참조해서 최종 int 값을 가져옴
</code></pre>
<p>위 프로그램을 실행하면 다음과 같이 나옵니다.
5
5</p>
<p>주의할 점은, 포인터의 포인터에 일반 변수의 주소를 다이렉트로 바로 넣을 수는 없다는 거예요.</p>
<pre><code class="language-cpp">int value { 5 };
int** ptrptr { &amp;&amp;value }; // 이렇게 하면 안 됩니다! (오류 발생)
</code></pre>
<p>왜냐하면 주소를 가져오는 <code>&amp;</code> 연산자는 메모리에 자리가 확실히 있는 온전한 변수(lvalue)에만 쓸 수 있는데, <code>&amp;value</code> 자체는 잠깐 생겼다 사라지는 임시 값(rvalue)이기 때문이에요.</p>
<p>하지만, 포인터의 포인터를 텅 비워둘 수는 있습니다 (null로 설정).</p>
<pre><code class="language-cpp">int** ptrptr { nullptr };
</code></pre>
<hr>
<h3 id="포인터-배열-arrays-of-pointers">포인터 배열 (Arrays of pointers)</h3>
<p>포인터의 포인터는 몇 가지 쓰임새가 있는데요. 가장 흔하게 쓰이는 곳은 바로 &#39;포인터들을 담아두는 배열&#39;을 동적으로 만들 때입니다.</p>
<pre><code class="language-cpp">int** array { new int*[10] }; // 10개의 int 포인터를 담을 수 있는 배열 할당
</code></pre>
<p>이건 우리가 아는 동적 배열과 똑같이 작동해요. 다만 배열 안에 들어가는 내용물이 단순한 숫자가 아니라, &quot;정수를 가리키는 포인터&quot;라는 점만 다릅니다.</p>
<hr>
<h3 id="동적으로-할당된-2차원-배열">동적으로 할당된 2차원 배열</h3>
<p>포인터의 포인터를 쓰는 또 다른 흔한 이유는 다차원 배열을 동적으로 만들기 위해서예요 (다차원 배열이 기억나지 않는다면 이전 레슨인 &#39;17.12 -- C스타일 다차원 배열&#39;을 훑어보세요!).</p>
<p>크기가 정해져 있는 2차원 배열은 평소처럼 이렇게 쉽게 만들 수 있죠.</p>
<pre><code class="language-cpp">int array[10][5];
</code></pre>
<p>하지만 2차원 배열을 &#39;동적&#39;으로 만드는 건 조금 더 까다롭습니다. 아마 이렇게 직관적으로 해보고 싶으실 거예요.</p>
<pre><code class="language-cpp">int** array { new int[10][5] }; // 작동하지 않아요!
</code></pre>
<p>안타깝게도 이 코드는 에러가 납니다.</p>
<p>여기엔 두 가지 해결책이 있어요. 만약 배열의 맨 오른쪽 크기(여기선 5)가 변하지 않는 고정된 값(<code>constexpr</code>)이라면 이렇게 할 수 있습니다.</p>
<pre><code class="language-cpp">int x { 7 }; // 상수가 아님 (변할 수 있는 값)
int (*array)[5] { new int[x][5] }; // 맨 오른쪽 크기(5)는 반드시 고정된 상수여야 함
</code></pre>
<p>여기서 괄호 <code>()</code>는 꼭 필요해요! 그래야 컴퓨터가 &quot;아, 이 <code>array</code>는 5개의 <code>int</code>가 들어있는 배열을 가리키는 포인터구나&quot;라고 알아듣거든요. 괄호가 없으면 컴퓨터는 <code>int* array[5]</code>로 읽어서, &#39;int 포인터 5개를 담은 배열&#39;로 잘못 해석해 버립니다.</p>
<p>이럴 때는 컴퓨터가 알아서 타입을 맞춰주는 <code>auto</code>를 쓰면 정말 편합니다.</p>
<pre><code class="language-cpp">int x { 7 }; // 상수가 아님
auto array { new int[x][5] }; // 훨씬 간단하죠!
</code></pre>
<p>하지만 안타깝게도, 배열의 맨 오른쪽 크기가 컴파일할 때 정해진 상수가 아니라면 이 간단한 방법은 쓸 수 없어요. 그럴 때는 조금 복잡해집니다.
먼저 앞서 배운 대로 &#39;포인터들을 담는 배열&#39;을 만듭니다. 그런 다음, 반복문을 돌면서 각 포인터마다 또 다른 동적 배열을 달아주는 거예요. 즉, 우리의 동적 2차원 배열은 <strong>&#39;동적 1차원 배열들을 엮어 놓은 동적 1차원 배열&#39;</strong>이 되는 셈이죠!</p>
<pre><code class="language-cpp">int** array { new int*[10] }; // 10개의 int 포인터 배열을 할당합니다 — 이것들이 &#39;행(가로줄)&#39;이 됩니다.
for (int count { 0 }; count &lt; 10; ++count)
    array[count] = new int[5]; // 각각의 행마다 5개짜리 배열을 달아줍니다 — 이것들이 &#39;열(세로칸)&#39;이 됩니다.
</code></pre>
<p>이렇게 만들어 두면 평소처럼 배열을 쓸 수 있어요.</p>
<pre><code class="language-cpp">array[9][4] = 3; // 이건 (array[9])[4] = 3; 과 똑같은 뜻이에요.
</code></pre>
<p>이 방법을 쓰면 각각의 가로줄(행)을 따로따로 만들기 때문에, 굳이 네모 반듯한 직사각형 배열이 아니어도 괜찮아요. 예를 들어, 계단처럼 생긴 삼각형 배열도 만들 수 있습니다.</p>
<pre><code class="language-cpp">int** array { new int*[10] }; // 10개의 int 포인터 배열 할당 — 이것들이 &#39;행&#39;
for (int count { 0 }; count &lt; 10; ++count)
    array[count] = new int[count+1]; // 행 번호가 커질수록 배열 길이도 길어집니다 — 이것들이 &#39;열&#39;
</code></pre>
<p>위 코드에서 <code>array[0]</code>은 길이가 1인 배열, <code>array[1]</code>은 길이가 2인 배열... 이런 식으로 점점 길어지는 걸 볼 수 있죠.</p>
<p>다 쓰고 나서 이 방법으로 만든 2차원 배열을 지울(메모리 해제) 때도 똑같이 반복문이 필요해요.</p>
<pre><code class="language-cpp">for (int count { 0 }; count &lt; 10; ++count)
    delete[] array[count];
delete[] array; // 이 부분은 반드시 맨 마지막에 해야 합니다!
</code></pre>
<p>주의하세요! 배열을 지울 때는 우리가 만든 순서의 <strong>반대</strong>로 지워야 합니다 (안에 있는 알맹이 배열부터 지우고, 마지막에 껍데기 배열을 지워요). 껍데기(<code>array</code>)를 먼저 지워버리면, 그 안에 있던 알맹이 배열들의 위치를 찾을 길이 없어져서 컴퓨터가 엉뚱한 행동(정의되지 않은 동작)을 하게 됩니다.</p>
<p>이렇게 2차원 배열을 동적으로 만들고 지우는 건 복잡하고 실수하기 딱 좋아요. 그래서 가로 길이가 x, 세로 길이가 y인 2차원 배열이 필요하다면, 그냥 <code>x * y</code> 크기의 <strong>길쭉한 1차원 배열 하나로 쫙 펴서(flatten)</strong> 쓰는 게 훨씬 편할 때가 많습니다.</p>
<pre><code class="language-cpp">// 이렇게 복잡하게 하는 대신:
int** array { new int*[10] }; // 10개의 int 포인터 배열 할당 — 행
for (int count { 0 }; count &lt; 10; ++count)
    array[count] = new int[5]; // 열

// 이렇게 해보세요!
int *array { new int[50] }; // 10x5 2차원 배열을 50개짜리 1차원 배열 하나로 쫙 폅니다.
</code></pre>
<p>이렇게 1차원 배열로 펴놓고, 아주 간단한 수학 계산만 쓰면 2차원 느낌으로 몇 번째 줄, 몇 번째 칸인지 쉽게 찾아낼 수 있답니다.</p>
<pre><code class="language-cpp">int getSingleIndex(int row, int col, int numberOfColumnsInArray)
{
     return (row * numberOfColumnsInArray) + col;
}

// 쫙 펴놓은 1차원 배열에서 [9][4] 위치에 3을 넣기
array[getSingleIndex(9, 4, 5)] = 3;
</code></pre>
<hr>
<h3 id="주소로-포인터-전달하기">주소로 포인터 전달하기</h3>
<p>우리가 함수에 변수의 주소를 넘겨줘서 변수 값을 바꿀 수 있었던 것처럼, 포인터의 포인터를 함수에 넘겨주면 그 포인터가 가리키는 &#39;포인터 자체&#39;의 값을 바꿀 수도 있습니다 (벌써 머리가 아파오나요?).</p>
<p>하지만, 함수에서 포인터가 가리키는 대상을 바꾸게 하고 싶다면, 이중 포인터를 쓰는 것보다 <strong>포인터에 대한 참조(reference to a pointer)</strong>를 쓰는 게 훨씬 깔끔하고 좋습니다. 이 내용은 &#39;12.11 -- 주소로 전달하기 (2부)&#39; 레슨에서 다루고 있어요.</p>
<hr>
<h3 id="포인터의-포인터의-포인터">포인터의 포인터의 포인터...</h3>
<p>별표를 세 개 써서 포인터의 포인터의 포인터를 만드는 것도 가능하긴 해요.</p>
<pre><code class="language-cpp">int*** ptrx3;
</code></pre>
<p>이걸 쓰면 동적인 3차원 배열을 만들 수 있겠죠. 하지만 그러려면 반복문 안에 반복문을 또 넣어야 하고, 에러 없이 제대로 돌아가게 만들기가 엄청나게 복잡해집니다.</p>
<p>심지어 별표 4개짜리도 만들 수 있어요.</p>
<pre><code class="language-cpp">int**** ptrx4;
</code></pre>
<p>원한다면 그 이상도 얼마든지 덧붙일 수 있죠.
하지만 현실에서는 이렇게까지 겹겹이 포인터를 타고 들어갈 일이 거의 없기 때문에 잘 보지 못하실 겁니다.</p>
<hr>
<h3 id="결론-1">결론</h3>
<p>어쩔 수 없는 상황이 아니라면, <strong>포인터의 포인터는 되도록 쓰지 않는 것을 강력히 추천합니다.</strong> 쓰기 너무 복잡하고 자칫하면 위험해질 수 있거든요. 일반 포인터만 써도 텅 비거나 엉뚱한 곳을 가리키는 오류(null or dangling pointer)를 내기 십상인데, 이중 포인터는 값을 가져오려면 두 번이나 역참조를 해야 하니 그 위험성이 두 배로 커집니다!</p>
<hr>
<h2 id="195--void-pointers-void-포인터">19.5 — Void pointers (void 포인터)</h2>
<p>void 포인터(제네릭 포인터, 또는 &#39;만능 포인터&#39;라고도 불러요)는 어떤 종류의 데이터 타입이든 가리지 않고 모두 가리킬 수 있는 아주 특별한 포인터입니다! 일반 포인터를 만들 때와 똑같이 선언하지만, 타입 자리에 <code>void</code>라는 키워드를 사용합니다.</p>
<pre><code class="language-cpp">void* ptr {}; // ptr은 void 포인터입니다
</code></pre>
<p>이 만능 포인터는 숫자(int), 소수(float), 심지어 직접 만든 복잡한 데이터(struct)까지 어떤 것이든 가리킬 수 있어요.</p>
<pre><code class="language-cpp">int nValue {};
float fValue {};
struct Something{
    int n;
    float f;
};

Something sValue {};
void* ptr {};
ptr = &amp;nValue; // 가능 (valid)
ptr = &amp;fValue; // 가능 (valid)
ptr = &amp;sValue; // 가능 (valid)
</code></pre>
<p>하지만 치명적인 단점이 하나 있습니다! void 포인터는 자신이 <strong>&#39;정확히 어떤 종류의 데이터&#39;를 가리키고 있는지 스스로 알지 못해요</strong>. 그래서 별표(<code>*</code>)를 붙여서 그 안의 실제 값을 꺼내오는 행동(이것을 <strong>역참조</strong>라고 합니다)을 하면 에러가 납니다. 값을 꼭 보고 싶다면, 값을 꺼내기 전에 반드시 다른 포인터 타입으로 <strong>&#39;변신(캐스팅, cast)&#39;</strong>시켜주어야 합니다.</p>
<pre><code class="language-cpp">int value{ 5 };
void* voidPtr{ &amp;value };

// std::cout &lt;&lt; *voidPtr &lt;&lt; &#39;\n&#39;; // 에러: void 포인터는 역참조(값을 꺼내오는 것)를 할 수 없어요!

int* intPtr{ static_cast&lt;int*&gt;(voidPtr) }; // 하지만, void 포인터를 int 포인터로 변환(캐스팅)해준다면...

std::cout &lt;&lt; *intPtr &lt;&lt; &#39;\n&#39;; // 변환된 결과값을 통해 정상적으로 실제 값을 꺼내올 수 있습니다
</code></pre>
<p>이 코드는 다음과 같이 출력됩니다:
5</p>
<p>그럼 당연히 이런 궁금증이 생기겠죠? *&quot;void 포인터가 자기가 뭘 가리키는지 모른다면, 도대체 무슨 타입으로 변환해 줘야 할지 우리가 어떻게 아나요?&quot;*
정답은... <strong>프로그래머인 여러분이 직접 잘 기억하고 추적해야 한다</strong>는 것입니다!</p>
<p>실제로 void 포인터가 어떻게 쓰이는지 예시를 통해 볼까요?</p>
<pre><code class="language-cpp">#include &lt;cassert&gt;
#include &lt;iostream&gt;

enum class Type{
    tInt, // 참고: &quot;int&quot;는 이미 예약된 키워드라 사용할 수 없어요. 대신 &quot;tInt&quot;를 사용합니다
    tFloat,
    tCString
};

void printValue(void* ptr, Type type){
    switch (type)
    {
    case Type::tInt:
        std::cout &lt;&lt; *static_cast&lt;int*&gt;(ptr) &lt;&lt; &#39;\n&#39;; // int 포인터로 변환(캐스팅)한 후, 값을 꺼내옵니다(역참조)
        break;
    case Type::tFloat:
        std::cout &lt;&lt; *static_cast&lt;float*&gt;(ptr) &lt;&lt; &#39;\n&#39;; // float 포인터로 변환한 후, 값을 꺼내옵니다
        break;
    case Type::tCString:
        std::cout &lt;&lt; static_cast&lt;char*&gt;(ptr) &lt;&lt; &#39;\n&#39;; // char 포인터로 변환합니다 (값을 따로 꺼내올 필요 없음)
        // std::cout은 char*를 C스타일 문자열로 취급해서 전체를 출력해 줍니다
        // 만약 여기서 별표(*)를 붙여 값을 꺼내왔다면, 문자열 전체가 아니라 맨 앞의 글자 하나만 출력되었을 거예요
        break;
    default:
        std::cerr &lt;&lt; &quot;printValue(): invalid type provided\n&quot;;
        assert(false &amp;&amp; &quot;type not found&quot;);
        break;
    }
}

int main(){
    int nValue{ 5 };
    float fValue{ 7.5f };
    char szValue[]{ &quot;Mollie&quot; };

    printValue(&amp;nValue, Type::tInt);
    printValue(&amp;fValue, Type::tFloat);
    printValue(szValue, Type::tCString);

    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과입니다:
5
7.5
Mollie</p>
<hr>
<h3 id="void-포인터-관련-기타-지식">void 포인터 관련 기타 지식</h3>
<p>void 포인터도 아무것도 가리키지 않는 텅 빈 상태(null)로 만들 수 있습니다.</p>
<pre><code class="language-cpp">void* ptr{ nullptr }; // ptr은 현재 null 포인터인 void 포인터입니다
</code></pre>
<p>앞서 void 포인터는 자신이 가리키는 데이터의 정체(크기)를 모른다고 했죠? 그래서 void 포인터에 대고 바로 <code>delete</code>를 사용해 메모리를 지워버리면, 컴퓨터가 얼만큼을 지워야 할지 몰라 <strong>예측할 수 없는 심각한 오류(미정의 동작)</strong>가 발생합니다. 만약 메모리를 해제해야 한다면, 반드시 먼저 알맞은 원래의 데이터 타입으로 <code>static_cast</code>를 통해 변신시켜준 다음에 지워야 해요.</p>
<p>마찬가지로 void 포인터로는 <strong>포인터 연산(포인터에 +1, -1을 해서 다음 칸으로 이동하는 것)</strong>도 할 수 없어요. 다음 칸으로 넘어가려면 지금 데이터가 메모리에서 얼만큼의 크기를 차지하는지 알아야 하는데, void 포인터는 그 크기를 모르기 때문입니다.</p>
<blockquote>
<p><strong>참고로...</strong>
C++에 &#39;void 레퍼런스(참조)&#39;라는 것은 존재하지 않아요. 만약 존재한다면 <code>void&amp;</code> 같은 형태일 텐데, 자신이 참조하는 값이 어떤 타입인지 알 수 없는 빈 껍데기가 되어버리기 때문입니다.</p>
</blockquote>
<hr>
<h3 id="결론-및-요약">결론 및 요약</h3>
<p>결론적으로 말씀드리면, 정말 꼭 필요한 상황이 아니라면 <strong>void 포인터는 최대한 피하는 것이 좋습니다</strong>.</p>
<p>왜냐하면 컴파일러가 &#39;이 데이터 타입이 맞는지&#39; 검사해 주는 안전장치를 아예 꺼버리는 것과 같거든요. 실수로 전혀 말이 안 되는 코드를 짜더라도 컴파일러가 경고조차 해주지 않습니다. 예를 들어 아래 코드는 에러 없이 무사히 실행됩니다.</p>
<pre><code class="language-cpp">int nValue{ 5 };
printValue(&amp;nValue, Type::tCString);
</code></pre>
<p>하지만 저 코드가 실제로 어떤 엉뚱한 결과를 뱉어낼지는 아무도 모르죠!</p>
<p>앞서 본 예시 함수(<code>printValue</code>)가 여러 데이터 타입을 한 번에 처리하는 멋지고 깔끔한 방법처럼 보일 수 있어요. 하지만 C++에는 타입 검사도 안전하게 다 해주면서 똑같은 기능을 구현할 수 있는 훨씬 훌륭한 방법인 <strong>함수 오버로딩(function overloading)</strong>이 있습니다. 오용을 막아주는 든든한 기능이죠.
또한, 예전에는 여러 타입을 다루기 위해 void 포인터를 썼던 많은 곳들이, 요즘은 강력한 타입 검사 기능을 제공하는 <strong>템플릿(templates)</strong>으로 훨씬 안전하게 대체되었습니다.</p>
<p>아주 가끔 프로그래밍을 하다 보면 void 포인터가 꼭 필요한 합리적인 상황을 만날 수도 있습니다. 하지만 그럴 때는 항상 *&quot;C++의 다른 안전한 언어 기능(오버로딩, 템플릿 등)으로 똑같이 해결할 방법은 없을까?&quot;*를 먼저 꼼꼼히 고민해 보시길 바랍니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 18]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-18</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-18</guid>
            <pubDate>Fri, 06 Mar 2026 12:19:58 GMT</pubDate>
            <description><![CDATA[<h2 id="181--선택-정렬selection-sort을-이용한-배열-정렬">18.1 — 선택 정렬(Selection sort)을 이용한 배열 정렬</h2>
<h3 id="정렬이-필요한-이유">정렬이 필요한 이유</h3>
<p>배열 정렬이란 배열 안에 있는 데이터들을 특정한 순서대로 보기 좋게 나열하는 과정을 말합니다. 정렬은 우리가 컴퓨터를 사용하는 일상생활 곳곳에서 아주 유용하게 쓰이고 있어요. 예를 들어, 이메일 프로그램은 보통 가장 최근에 받은 메일을 먼저 보여주기 위해 시간순으로 이메일을 정렬합니다. 연락처 목록을 볼 때는 이름이 가나다순이나 알파벳순으로 정렬되어 있죠? 그래야 원하는 사람의 이름을 찾기 훨씬 쉬우니까요. 이렇게 화면에 정보를 띄워주기 전에 데이터를 미리 찾기 쉽게 다듬는 것이 바로 정렬의 역할입니다.</p>
<p>정렬은 사람뿐만 아니라 컴퓨터가 데이터를 검색할 때도 훨씬 빠르고 효율적으로 일할 수 있게 도와줍니다. 이름 목록에서 특정 이름이 있는지 확인해야 하는 상황을 상상해 볼까요? 만약 이름이 뒤죽박죽 섞여 있다면, 컴퓨터는 그 이름이 있는지 확인하기 위해 배열의 처음부터 끝까지 하나하나 전부 다 검사해야만 합니다. 데이터가 엄청나게 많다면 이 작업은 시간이 아주 오래 걸리고 힘든(비용이 많이 드는) 작업이 될 거예요.</p>
<p>하지만 이제 이름들이 알파벳순으로 예쁘게 정렬되어 있다고 생각해 보세요. 이 경우에는 우리가 찾는 이름보다 알파벳 순서가 더 뒤에 있는 이름을 마주치는 순간, 검색을 바로 멈추면 됩니다! 그 뒤에 남은 이름들은 전부 우리가 찾는 이름보다 알파벳 순서가 늦을 것이 확실하기 때문에, 굳이 끝까지 찾아볼 필요조차 없는 것이죠.</p>
<p>사실 이렇게 이미 정렬된 배열 안에서 데이터를 찾는 데는 훨씬 더 훌륭하고 똑똑한 방법들이 존재합니다. 아주 간단한 검색 방법만 사용하더라도 100만 개나 되는 데이터 속에서 단 20번만 값을 비교해보고 원하는 것을 찾아낼 수 있을 정도니까요! 물론 단점도 있습니다. 애초에 흩어진 배열을 순서대로 &#39;정렬&#39;하는 작업 자체가 컴퓨터에게는 꽤 번거로운 일입니다. 그래서 데이터를 여러 번 반복해서 검색해야 하는 상황이 아니라면, 검색 한 번 빨리 하겠다고 굳이 정렬을 먼저 하는 것은 오히려 손해일 수도 있습니다.</p>
<p>어떤 경우에는 정렬을 해두는 것만으로도 아예 &#39;검색&#39; 자체가 필요 없어지기도 합니다. 학생들의 시험 점수 중에서 가장 높은 1등 점수를 찾는다고 해볼까요? 점수가 막 섞여 있다면 모든 점수를 다 훑어봐야 1등 점수를 알 수 있습니다. 하지만 이미 점수가 순서대로 정렬되어 있다면, 그냥 맨 앞자리나 맨 뒷자리(오름차순이냐 내림차순이냐에 따라 다름)만 쏙 확인하면 끝납니다. 찾고 자시고 할 것도 없죠!</p>
<hr>
<h3 id="정렬은-어떻게-이루어질까">정렬은 어떻게 이루어질까?</h3>
<p>정렬은 보통 두 개의 데이터를 짝지어 서로 비교해 본 다음, 우리가 정해둔 기준에 맞지 않으면 두 데이터의 위치를 서로 바꾸는(swap) 과정을 계속 반복하면서 이루어집니다. 어떤 정렬 방식을 선택하느냐에 따라 이 데이터들을 비교하는 순서가 달라지고, 어떤 순서로 정렬할지
(예: 작은 것부터 큰 것으로 가는 오름차순, 큰 것부터 작은 것으로 가는 내림차순)에 따라 자리를 바꾸는 기준이 달라집니다.</p>
<p>C++에서는 두 데이터의 자리를 바꿀 때, C++ 표준 라이브러리의 <code>&lt;utility&gt;</code> 헤더에 미리 만들어져 있는 <code>std::swap()</code>이라는 함수를 아주 편리하게 사용할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;utility&gt;

int main()
{
    int x{ 2 };
    int y{ 4 };
    std::cout &lt;&lt; &quot;Before swap: x = &quot; &lt;&lt; x &lt;&lt; &quot;, y = &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;
    std::swap(x, y); // x와 y의 값을 서로 바꿉니다.
    std::cout &lt;&lt; &quot;After swap:  x = &quot; &lt;&lt; x &lt;&lt; &quot;, y = &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 화면에 다음과 같이 출력됩니다:</p>
<pre><code class="language-text">Before swap: x = 2, y = 4
After swap:  x = 4, y = 2
</code></pre>
<p><code>std::swap()</code>을 거치고 난 후, <code>x</code>와 <code>y</code>가 가진 값이 서로 완벽하게 맞바뀐 것을 볼 수 있습니다!</p>
<hr>
<h3 id="선택-정렬-selection-sort">선택 정렬 (Selection sort)</h3>
<p>배열을 정렬하는 방법은 아주 여러 가지가 존재합니다. 그중에서도 <strong>선택 정렬(Selection sort)</strong>은 아마도 정렬 방법 중에서 가장 이해하기 쉬운 녀석일 것입니다. 비록 속도는 다른 정렬 방법들에 비해 조금 느린 편이지만, 작동 원리가 매우 직관적이라서 초보자들에게 정렬의 개념을 가르칠 때 아주 좋은 교재가 됩니다.</p>
<p>선택 정렬을 이용해서 숫자를 가장 작은 것부터 가장 큰 것 순서(오름차순)로 줄 세우는 과정은 다음과 같습니다:</p>
<ol>
<li>배열의 맨 처음(인덱스 0)부터 시작해서 끝까지 쭉 훑어보며 <strong>가장 작은 값</strong>을 찾습니다.</li>
<li>찾은 가장 작은 값을 배열의 <strong>맨 첫 번째 자리(인덱스 0)</strong>에 있는 값과 서로 자리를 바꿉니다.</li>
<li>이제 첫 번째 자리는 완성되었으니 신경 쓰지 말고, 그다음 자리(인덱스 1)부터 시작해서 위 1번과 2번의 과정을 남은 배열 끝까지 계속 반복합니다.</li>
</ol>
<p>쉽게 말해, 전체에서 제일 작은 녀석을 골라내서 맨 앞에 갖다 놓고, 남은 애들 중에서 제일 작은 녀석을 골라내서 두 번째 자리에 갖다 놓는 작업을 계속하는 거예요. 데이터가 바닥날 때까지 말이죠!</p>
<p>5개의 숫자가 들어있는 배열을 통해 이 과정이 어떻게 진행되는지 직접 눈으로 따라가 봅시다. 처음 배열의 상태입니다.
<code>{ 30, 50, 20, 10, 40 }</code></p>
<p>먼저, 첫 번째 자리(인덱스 0)부터 시작해서 제일 작은 숫자를 찾습니다.
<code>{ 30, 50, 20, 10, 40 }</code> (여기서 제일 작은 숫자는 10이네요!)</p>
<p>이 제일 작은 숫자 10을 첫 번째 자리(인덱스 0)에 있던 30과 서로 바꿉니다:
<code>{ 10, 50, 20, 30, 40 }</code></p>
<p>이제 맨 앞자리(10)는 제대로 된 주인을 찾았으니 무시해도 좋습니다. 남은 숫자들 중 두 번째 자리(인덱스 1)부터 시작해서 제일 작은 숫자를 찾습니다:
<code>{ 10, 50, 20, 30, 40 }</code> (남은 애들 중 제일 작은 숫자는 20이네요!)</p>
<p>이 20을 두 번째 자리(인덱스 1)에 있던 50과 자리를 바꿉니다:
<code>{ 10, 20, 50, 30, 40 }</code></p>
<p>이제 앞의 두 자리 정렬이 끝났습니다. 세 번째 자리(인덱스 2)부터 시작해서 제일 작은 숫자를 또 찾습니다:
<code>{ 10, 20, 50, 30, 40 }</code> (이번엔 30이 제일 작네요!)</p>
<p>30을 세 번째 자리(인덱스 2)에 있던 50과 바꿉니다:
<code>{ 10, 20, 30, 50, 40 }</code></p>
<p>네 번째 자리(인덱스 3)부터 남은 숫자 중 제일 작은 것을 찾습니다:
<code>{ 10, 20, 30, 50, 40 }</code> (40이 제일 작습니다!)</p>
<p>40을 네 번째 자리(인덱스 3)에 있던 50과 바꿉니다:
<code>{ 10, 20, 30, 40, 50 }</code></p>
<p>마지막으로 다섯 번째 자리(인덱스 4)에서 가장 작은 숫자를 찾습니다:
<code>{ 10, 20, 30, 40, 50 }</code> (혼자 남은 50이네요)</p>
<p>50을 다섯 번째 자리에 있는 값(자기 자신)과 바꿉니다 (사실상 아무 변화가 일어나지 않습니다):
<code>{ 10, 20, 30, 40, 50 }</code></p>
<p>짜잔! 정렬이 모두 끝났습니다!
<code>{ 10, 20, 30, 40, 50 }</code></p>
<p>여기서 한 가지 눈치채셨나요? 맨 마지막에 남는 1개는 항상 자기 자신과 자리를 바꾸게 되므로(쓸데없는 행동이죠), 굳이 마지막까지 검사할 필요 없이 배열의 맨 끝부분 바로 앞까지만 반복 작업을 해주면 됩니다.</p>
<hr>
<h3 id="c로-구현한-선택-정렬">C++로 구현한 선택 정렬</h3>
<p>지금까지 배운 선택 정렬 알고리즘을 C++ 코드로 똑같이 옮겨 적어보겠습니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;iterator&gt;
#include &lt;utility&gt;

int main()
{
    int array[]{ 30, 50, 20, 10, 40 };
    constexpr int length{ static_cast&lt;int&gt;(std::size(array)) };

    // 배열의 요소를 하나씩 차례대로 훑고 지나갑니다
    // (단, 맨 마지막 요소는 그 앞까지 정렬하는 동안 알아서 자기 자리를 찾을 테니, 마지막 요소 바로 앞까지만 반복합니다)
    for (int startIndex{ 0 }; startIndex &lt; length - 1; ++startIndex)
    {
        // smallestIndex는 이번 한 바퀴를 돌면서 찾아낸 &#39;가장 작은 요소&#39;의 위치(인덱스)를 기억할 변수입니다
        // 처음 시작할 때는 현재 살펴보고 있는 첫 번째 요소가 가장 작다고 일단 가정합니다
        int smallestIndex{ startIndex };

        // 그런 다음, 배열의 나머지 뒷부분을 쭉 살펴보면서 더 작은 요소가 숨어있는지 찾아냅니다
        for (int currentIndex{ startIndex + 1 }; currentIndex &lt; length; ++currentIndex)
        {
            // 만약 우리가 지금까지 제일 작다고 생각했던 것보다 더 작은 요소를 발견하게 된다면
            if (array[currentIndex] &lt; array[smallestIndex])
                // 진짜 제일 작은 요소의 위치를 새로운 위치로 업데이트해서 기억해둡니다
                smallestIndex = currentIndex;
        }

        // 위 과정을 다 거치고 나면, smallestIndex는 남은 배열 부분에서 &#39;진짜로 가장 작은 요소&#39;의 위치를 가리키게 됩니다
        // 이제 우리가 처음에 가정했던 시작 위치의 요소와 진짜 가장 작은 요소를 서로 맞바꿔 줍니다 (이로써 제자리를 찾아갑니다)
        std::swap(array[startIndex], array[smallestIndex]);
    }

    // 이제 전체 배열이 예쁘게 정렬되었을 테니, 잘 작동하는지 화면에 쭉 출력해서 확인해 봅니다
    for (int index{ 0 }; index &lt; length; ++index)
        std::cout &lt;&lt; array[index] &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>코드를 처음 보시는 분들이 여기서 가장 헷갈려 하는 부분은 바로 반복문(<code>for</code> 문) 안에 또 다른 반복문이 들어있는 구조일 것입니다. 이를 &#39;중첩 반복문&#39;이라고 부릅니다.
바깥쪽에 있는 반복문(<code>startIndex</code>)은 우리가 정렬해서 꽂아 넣을 자리를 맨 앞에서부터 한 칸씩 뒤로 이동시키는 역할을 합니다. 바깥쪽 반복문이 한 칸 이동할 때마다, 안쪽에 있는 반복문(<code>currentIndex</code>)이 맹활약하면서 아직 정렬되지 않은 나머지 뒷부분을 전부 훑어보고 그중 가장 작은 녀석의 위치를 찾아냅니다.
안쪽 반복문이 가장 작은 녀석의 위치를 <code>smallestIndex</code>에 잘 기억해 두었다가 작업이 끝나면, 그 녀석을 바깥쪽 반복문이 가리키고 있던 현재 자리(<code>startIndex</code>)의 값과 교체합니다. 그리고 바깥쪽 반복문은 다음 칸으로 이동하여 이 전체 과정을 반복하는 원리입니다.</p>
<blockquote>
<p><strong>팁:</strong> 만약 위에 있는 코드가 머릿속에 잘 안 그려진다면, 이면지를 한 장 꺼내서 샘플 배열을 하나 적어두고 직접 손으로 따라가 보는 것이 최고입니다. 종이 맨 위에 정렬되지 않은 처음 배열의 숫자들을 적으세요. 그리고 <code>startIndex</code>, <code>currentIndex</code>, <code>smallestIndex</code>라는 이름표를 달아둔 화살표를 그려서 각각 어떤 숫자를 가리키고 있는지 표시해 보세요. 코드가 한 줄 한 줄 실행될 때마다 화살표를 직접 옮겨보고, 바깥쪽 반복문이 한 바퀴 끝날 때마다 그다음 줄에 새롭게 변화된 배열의 모습을 그려 나가다 보면 원리가 아주 쉽게 이해될 거예요.</p>
</blockquote>
<p>사람들의 이름을 정렬할 때도 작동하는 방식은 완전히 똑같습니다. 단지 배열의 종류를 숫자를 담는 <code>int</code>에서 문자를 담는 <code>std::string</code>으로 슬쩍 바꿔주고, 안에 숫자가 아닌 이름들을 채워 넣어주기만 하면 된답니다.</p>
<hr>
<h3 id="stdsort-함수-사용하기">std::sort 함수 사용하기</h3>
<p>배열을 정렬하는 일은 프로그래밍을 하다 보면 밥 먹듯이 아주 자주 일어나는 일입니다. 그래서 C++에서는 우리가 일일이 복잡한 정렬 코드를 짜지 않아도 되도록, C++ 표준 라이브러리 안에 <code>std::sort</code>라는 이름의 강력하고 편리한 정렬 함수를 기본적으로 제공해 줍니다. 이 함수는 <code>&lt;algorithm&gt;</code> 헤더를 추가해서 사용할 수 있고, 아래처럼 아주 간단하게 부려먹을 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt; // std::sort를 사용하려면 이 헤더가 필요합니다
#include &lt;iostream&gt;
#include &lt;iterator&gt; // std::size를 사용하려면 이 헤더가 필요합니다

int main()
{
    int array[]{ 30, 50, 20, 10, 40 };

    // 마법의 주문! 배열의 처음부터 끝까지 알아서 정렬해 줍니다.
    std::sort(std::begin(array), std::end(array));

    for (int i{ 0 }; i &lt; static_cast&lt;int&gt;(std::size(array)); ++i)
        std::cout &lt;&lt; array[i] &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 <code>std::sort</code> 함수를 별다른 조건 없이 그대로 실행시키면, 기본적으로 작은 것부터 큰 것으로 가는 오름차순으로 배열을 척척 정리해 줍니다. 보이지 않는 내부에서는 우리가 앞에서 배웠던 선택 정렬처럼 두 데이터를 <code>작다(&lt;)</code> 기호를 사용해 짝지어 비교하고, 순서가 엇갈려 있으면 서로 자리를 휙휙 바꿔가며 영리하게 정렬을 수행한답니다.</p>
<p>이 편리한 <code>std::sort</code>에 대한 더 깊고 재미있는 이야기는 나중에 다른 장에서 다룰 예정이니 기대해 주세요!</p>
<hr>
<h2 id="182--이터레이터반복자-소개">18.2 — 이터레이터(반복자) 소개</h2>
<p>배열이나 여러 데이터가 모여 있는 구조에서 데이터를 &#39;처음부터 끝까지 하나씩 살펴보는 일(반복, Iterating)&#39;은 프로그래밍에서 정말 흔하게 하는 작업입니다. 우리는 지금까지 이 작업을 하기 위해 여러 가지 방법을 배웠습니다. 인덱스와 반복문을 사용하는 방법(<code>for</code> 문, <code>while</code> 문), 포인터와 포인터 연산을 사용하는 방법, 그리고 아주 간편한 &#39;범위 기반(range-based) <code>for</code> 문&#39;을 사용하는 방법 말이죠.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;cstddef&gt;
#include &lt;iostream&gt;

int main()
{
    // C++17에서는 arr 변수의 타입이 std::array&lt;int, 7&gt;로 자동 추론됩니다.
    // 만약 이 예제를 컴파일할 때 오류가 발생한다면, 아래 주의 사항을 확인하세요.
    std::array arr{ 0, 1, 2, 3, 4, 5, 6 };
    std::size_t length{ std::size(arr) };

    // 명시적인 인덱스를 사용하는 while 문
    std::size_t index{ 0 };
    while (index &lt; length)
    {
        std::cout &lt;&lt; arr[index] &lt;&lt; &#39; &#39;;
        ++index;
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    // 명시적인 인덱스를 사용하는 for 문
    for (index = 0; index &lt; length; ++index)
    {
        std::cout &lt;&lt; arr[index] &lt;&lt; &#39; &#39;;
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    // 포인터를 사용하는 for 문 (참고: ptr을 증가시켜야 하므로 const가 될 수 없습니다)
    for (auto ptr{ &amp;arr[0] }; ptr != (&amp;arr[0] + length); ++ptr)
    {
        std::cout &lt;&lt; *ptr &lt;&lt; &#39; &#39;;
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    // 범위 기반 for 문
    for (int i : arr)
    {
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<blockquote>
<p><strong>주의 사항</strong>
이 레슨의 예제들은 &#39;클래스 템플릿 인수 추론&#39;이라는 C++17의 편리한 기능을 사용합니다. 쉽게 말해, 초기화하는 값을 보고 컴파일러가 알아서 타입을 유추해 주는 기능입니다. 위 예제에서 컴파일러가 <code>std::array arr{ 0, 1, 2, 3, 4, 5, 6 };</code>을 보면, 우리가 <code>std::array&lt;int, 7&gt; arr { 0, 1, 2, 3, 4, 5, 6 };</code>을 만들고 싶어 한다는 것을 찰떡같이 알아챕니다.
만약 여러분의 컴파일러가 C++17을 지원하지 않는 설정이라면 &quot;arr 앞에 템플릿 인수가 누락되었습니다&quot; 같은 오류가 뜰 수 있습니다. 이럴 때는 레슨 0.12를 참고해 C++17을 활성화하는 것이 가장 좋습니다. 만약 버전을 올릴 수 없다면, 코드를 명시적인 타입이 적힌 형태로 바꿔주세요. (예: <code>std::array arr{ 0, 1, 2, 3, 4, 5, 6 };</code> 부분을 <code>std::array&lt;int, 7&gt; arr { 0, 1, 2, 3, 4, 5, 6 };</code>로 수정)</p>
</blockquote>
<p>데이터에 접근하기 위해서만 인덱스를 사용하는 경우라면, 인덱스 방식은 불필요하게 코드를 많이 쳐야 합니다. 게다가 이 방식은 배열처럼 요소에 &#39;직접 접근(번호표를 보고 바로 찾아가는 것)&#39;이 가능한 구조에서만 쓸 수 있습니다. 리스트(list) 같은 다른 데이터 구조에서는 인덱스로 바로 찾아가는 것이 불가능하거든요.</p>
<p>포인터와 포인터 연산(포인터에 숫자를 더해서 다음 칸으로 이동하는 것)을 사용해 반복하는 방법은 코드가 복잡해 보이고, 이 규칙을 잘 모르는 사람에게는 매우 헷갈릴 수 있습니다. 또한 이 방법은 배열처럼 메모리상에 데이터가 &#39;다닥다닥 연속으로&#39; 붙어있을 때만 제대로 작동합니다. 리스트, 트리, 맵 같은 구조에서는 데이터가 연속해 있지 않기 때문에 쓸 수 없습니다.</p>
<blockquote>
<p><strong>고급 학습자를 위한 내용</strong>
메모리에 연속으로 붙어있지 않은 데이터 구조라도, 포인터 연산(덧셈/뺄셈) 없이 그냥 포인터 자체만으로 반복할 수 있는 경우도 있습니다. 예를 들어 연결 리스트(Linked list)에서는 각 데이터가 다음 데이터를 가리키는 포인터를 가지고 있어서, 이 꼬리물기 포인터를 따라가며 하나씩 살펴볼 수 있습니다.</p>
</blockquote>
<p>범위 기반 <code>for</code> 문은 아주 흥미롭습니다. 데이터를 하나씩 꺼내오는 복잡한 과정이 우리 눈에는 안 보이게 숨겨져 있거든요. 게다가 배열, 리스트, 트리, 맵 등 종류를 가리지 않고 모든 구조에서 다 잘 작동합니다. 도대체 어떻게 이게 가능한 걸까요? 바로 <strong>&#39;이터레이터(Iterator)&#39;</strong>를 사용하기 때문입니다.</p>
<hr>
<h3 id="이터레이터-반복자">이터레이터 (반복자)</h3>
<p><strong>이터레이터(Iterator)</strong>는 컨테이너(배열의 값들이나 문자열의 글자들처럼 데이터를 담아두는 바구니) 안을 처음부터 끝까지 돌아다니며, 지나가는 길에 있는 각 데이터에 접근할 수 있게끔 특별히 만들어진 도구입니다. 비유하자면 데이터 창고를 안내해 주는 &#39;가이드&#39;와 같습니다.</p>
<p>하나의 컨테이너가 여러 종류의 이터레이터를 제공할 수도 있습니다. 예를 들어 어떤 배열은 앞에서부터 뒤로 걸어가는 &#39;정방향 이터레이터&#39;를 제공하기도 하고, 뒤에서부터 앞으로 거꾸로 걸어가는 &#39;역방향 이터레이터&#39;를 제공하기도 합니다.</p>
<p>알맞은 이터레이터를 하나 만들기만 하면, 프로그래머는 데이터가 메모리에 어떻게 저장되어 있는지, 앞으로 어떻게 걸어갈지 고민할 필요가 없습니다. 그냥 이터레이터가 제공하는 간단한 조작법만 쓰면 됩니다.
특히 C++의 이터레이터들은 보통 사용법이 다 똑같습니다. <code>operator++</code> (플러스플러스 기호)를 쓰면 다음 데이터로 이동하고, <code>operator*</code> (별표 기호)를 쓰면 지금 가리키고 있는 데이터를 꺼내옵니다. 이 통일된 사용법 덕분에 우리는 아주 다양한 종류의 데이터 컨테이너를 똑같은 방법으로 다룰 수 있게 됩니다.</p>
<hr>
<h3 id="이터레이터-역할을-하는-포인터">이터레이터 역할을 하는 포인터</h3>
<p>가장 단순한 형태의 이터레이터는 바로 &#39;포인터&#39;입니다. 메모리에 데이터가 일렬로 쭉 저장되어 있을 때 포인터를 사용하면 아주 잘 작동하죠. 포인터를 사용해 간단한 배열을 훑어보는 코드를 다시 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    std::array arr{ 0, 1, 2, 3, 4, 5, 6 };

    auto begin{ &amp;arr[0] };
    // 주의: 이것은 마지막 원소의 바로 &#39;다음&#39; 빈 공간을 가리킵니다.
    auto end{ begin + std::size(arr) };

    // 포인터를 사용하는 for 문
    for (auto ptr{ begin }; ptr != end; ++ptr) // ++를 사용해 다음 요소로 이동
    {
        std::cout &lt;&lt; *ptr &lt;&lt; &#39; &#39;; // 역참조(*)를 통해 현재 요소의 실제 값을 가져옴
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>출력 결과:
<code>0 1 2 3 4 5 6</code></p>
<p>위 코드에서 우리는 두 개의 변수를 만들었습니다. 하나는 데이터의 출발점을 가리키는 <code>begin</code>이고, 다른 하나는 끝나는 지점을 표시하는 <code>end</code>입니다. 배열에서 이 <strong>끝 지점(end marker)</strong>은 보통 &#39;마지막 데이터가 있는 자리&#39;가 아니라, <strong>&#39;마지막 데이터 바로 다음 빈칸&#39;</strong>을 의미합니다. 만약 데이터가 하나 더 있었다면 들어갈 바로 그 자리 말이죠.</p>
<p>포인터는 이 <code>begin</code>부터 출발해 <code>end</code>에 닿기 전까지 이동하며, 포인터에 역참조 기호(<code>*</code>)를 붙여서 현재 위치의 데이터를 읽어옵니다.</p>
<blockquote>
<p><strong>주의 사항</strong>
끝 지점을 계산할 때 아래처럼 배열 문법을 사용해 주소를 가져오고 싶으실 수도 있습니다.
<code>int* end{ &amp;arr[std::size(arr)] };</code>
하지만 이렇게 하면 &#39;정의되지 않은 동작(Undefined behavior, 프로그램이 뻗거나 예측 불가능하게 행동함)&#39;이 발생합니다. 왜냐하면 저 코드는 배열의 진짜 범위를 벗어난 곳의 값을 은연중에 읽어오려고 시도하기 때문입니다.
대신 아래처럼 안전한 방식을 사용하세요:
<code>int* end{ arr.data() + std::size(arr) }; // data()는 첫 번째 요소를 가리키는 포인터를 줍니다.</code></p>
</blockquote>
<hr>
<h3 id="표준-라이브러리-이터레이터">표준 라이브러리 이터레이터</h3>
<p>데이터를 순서대로 살펴보는 건 프로그래밍에서 숨 쉬는 것만큼 자연스럽고 흔한 일입니다. 그래서 C++의 모든 표준 라이브러리 컨테이너(데이터 저장소들)는 이터레이터 기능을 기본적으로 내장하고 있습니다. 우리가 힘들게 출발점과 끝점을 계산할 필요 없이, 컨테이너에게 직접 <code>begin()</code>과 <code>end()</code>라는 함수를 불러서 &quot;시작점과 끝점을 알려줘!&quot;라고 물어보기만 하면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    std::array array{ 1, 2, 3 };

    // 배열에게 시작점과 끝점을 물어봅니다 (begin 및 end 멤버 함수 사용).
    auto begin{ array.begin() };
    auto end{ array.end() };

    for (auto p{ begin }; p != end; ++p) // ++를 사용해 다음 요소로 이동.
    {
        std::cout &lt;&lt; *p &lt;&lt; &#39; &#39;; // 역참조를 통해 현재 요소의 값을 가져옴.
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>출력 결과:
<code>1 2 3</code></p>
<p><code>&lt;iterator&gt;</code>라는 헤더 파일 안에는 범용적으로 쓸 수 있는 <code>std::begin</code>과 <code>std::end</code>라는 함수도 들어 있습니다.</p>
<blockquote>
<p><strong>팁</strong>
구형 C스타일 배열을 위한 <code>std::begin</code>과 <code>std::end</code>는 <code>&lt;iterator&gt;</code> 헤더에 정의되어 있습니다.
이터레이터를 지원하는 최신 컨테이너들을 위한 <code>std::begin</code>과 <code>std::end</code>는 해당 컨테이너의 헤더 파일(예: <code>&lt;array&gt;</code>, <code>&lt;vector&gt;</code>)에 같이 들어 있습니다.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;array&gt;    // &lt;iterator&gt;를 포함합니다
#include &lt;iostream&gt;

int main()
{
    std::array array{ 1, 2, 3 };

    // std::begin과 std::end를 사용해 시작점과 끝점을 가져옵니다.
    auto begin{ std::begin(array) };
    auto end{ std::end(array) };

    for (auto p{ begin }; p != end; ++p) // ++를 사용해 다음 요소로 이동
    {
        std::cout &lt;&lt; *p &lt;&lt; &#39; &#39;; // 역참조를 통해 현재 요소의 값을 가져옴
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이것도 똑같이 출력됩니다:
<code>1 2 3</code></p>
<p>지금 당장 이터레이터의 &#39;타입(자료형)&#39;이 정확히 뭔지 몰라도 괜찮습니다. 나중에 다른 챕터에서 자세히 다룰 거거든요. 여기서 가장 중요한 핵심은 <strong>&#39;컨테이너 안을 돌아다니는 복잡한 과정은 이터레이터가 다 알아서 해준다&#39;</strong>는 점입니다. 우리가 필요한 건 딱 네 가지뿐입니다. 시작점(<code>begin</code>), 끝점(<code>end</code>), 다음으로 넘어가게 해주는 버튼(<code>operator++</code>), 그리고 값을 꺼내오는 버튼(<code>operator*</code>)입니다.</p>
<hr>
<h3 id="이터레이터에서의-operator-와-operator-비교">이터레이터에서의 operator&lt; 와 operator!= 비교</h3>
<p>이전 레슨 8.10(For 문)에서, 우리는 숫자를 비교해 반복문을 돌릴 때는 <code>!=</code>보다 <code>&lt;</code> 기호를 쓰는 것이 더 좋다고 배웠습니다.
<code>for (index = 0; index &lt; length; ++index)</code></p>
<p>하지만 <strong>이터레이터를 쓸 때는 <code>!=</code> (같지 않다) 기호를 사용해 이터레이터가 끝 지점(end)에 도달했는지 확인하는 것이 표준 관행</strong>입니다.
<code>for (auto p{ begin }; p != end; ++p)</code></p>
<p>왜 그럴까요? 어떤 이터레이터들은 구조상 크기를 비교하는 것(<code>&lt;</code>, <code>&gt;</code>)이 아예 불가능하기 때문입니다. 하지만 <code>!=</code> 기호는 어떤 종류의 이터레이터든 상관없이 항상 무사히 작동합니다.</p>
<hr>
<h3 id="다시-보는-범위-기반-for-문-range-based-for-loops">다시 보는 범위 기반 for 문 (Range-based for loops)</h3>
<p><code>begin()</code>과 <code>end()</code> 함수를 가지고 있거나, <code>std::begin()</code> 및 <code>std::end()</code> 함수와 함께 쓸 수 있는 모든 데이터 타입은 아주 편리한 &#39;범위 기반 for 문&#39;에 쓸 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    std::array array{ 1, 2, 3 };

    // 이 코드는 우리가 앞에서 썼던 반복문과 완전히 똑같이 동작합니다.
    for (int i : array)
    {
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>보이지 않는 곳에서, 범위 기반 for 문은 훑어볼 대상의 <code>begin()</code>과 <code>end()</code>를 몰래 호출해서 사용하고 있습니다. <code>std::array</code>는 자체적으로 이 함수들을 가지고 있기 때문에 범위 기반 반복문에 넣고 돌릴 수 있는 거죠. 구형 C스타일의 고정 크기 배열도 <code>std::begin</code>과 <code>std::end</code> 함수와 호환되기 때문에 역시 사용할 수 있습니다.
하지만 구형 C스타일의 &#39;동적 배열(크기가 변하는 배열)&#39;이나 포인터로 깎여버린(decayed) 배열은 쓸 수 없습니다. 이런 배열들은 길이 정보를 잃어버려서 <code>std::end</code> 함수를 찾을 수 없기 때문입니다.</p>
<p>여러분이 직접 만든 타입에도 이 함수들을 추가해서 범위 기반 for 문을 쓸 수 있게 만드는 방법은 나중에 배우게 될 겁니다.</p>
<p>그리고 이터레이터는 범위 기반 for 문에서만 쓰이는 게 아닙니다. 표준 라이브러리의 <code>std::sort</code>(데이터 정렬하기) 같은 수많은 유용한 알고리즘에서도 널리 쓰입니다. 이제 이터레이터가 뭔지 아셨으니, 앞으로 표준 라이브러리를 쓰면서 이 녀석이 얼마나 자주 등장하는지 보게 되실 겁니다.</p>
<hr>
<h3 id="이터레이터-무효화-허공을-맴도는-이터레이터">이터레이터 무효화 (허공을 맴도는 이터레이터)</h3>
<p>포인터나 참조자(Reference)와 마찬가지로, 이터레이터도 가리키고 있던 데이터의 메모리 주소가 바뀌거나 데이터가 파괴되어 버리면 길을 잃고 &quot;허공을 가리키게(dangling)&quot; 될 수 있습니다. 이런 상황을 이터레이터가 <strong>무효화(Invalidated)되었다</strong>고 부릅니다. 무효화된 이터레이터에 억지로 접근하려고 하면 아까 말했던 끔찍한 &#39;정의되지 않은 동작(Undefined behavior)&#39;이 발생합니다.</p>
<p>데이터 저장소를 수정하는 몇몇 작업들(예를 들어 <code>std::vector</code>라는 바구니에 새로운 데이터를 쑤셔 넣는 일)은, 그 안에 들어있던 기존 데이터들의 메모리 주소를 통째로 이사시켜 버리는 부작용을 낳기도 합니다. 데이터들이 이사를 가버리면, 원래 위치를 가리키고 있던 옛날 이터레이터들은 전부 쓸모가 없어집니다(무효화됩니다).
좋은 C++ 공식 문서들은 어떤 행동을 했을 때 이터레이터가 무효화될 수 있는지 항상 친절하게 경고해 둡니다. (cppreference 사이트의 <code>std::vector</code> 문서 중 &quot;Iterator invalidation(이터레이터 무효화)&quot; 항목을 참고해 보세요.)</p>
<p>우리가 자주 쓰는 범위 기반 for 문 역시 뒤에서는 이터레이터를 쓰고 있습니다. 그러니 반복문이 쌩쌩 돌아가고 있는 중간에 이터레이터를 무효화시키는 짓을 하지 않도록 각별히 조심해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

int main()
{
    std::vector v { 0, 1, 2, 3 };

    for (auto num : v) // 뒤에서 몰래 v의 이터레이터를 돌리며 요소를 봅니다
    {
        if (num % 2 == 0)
            v.push_back(num + 1); // 데이터를 밀어 넣다가 v의 이터레이터가 무효화되면, 프로그램이 어떻게 뻗을지 모릅니다 (정의되지 않은 동작)
    }

    return 0;
}
</code></pre>
<p>이터레이터가 무효화되는 또 다른 예시를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector v{ 1, 2, 3, 4, 5, 6, 7 };

    auto it{ v.begin() };

    ++it; // 두 번째 요소로 이동합니다
    std::cout &lt;&lt; *it &lt;&lt; &#39;\n&#39;; // 정상 작동: 2가 출력됩니다

    v.erase(it); // 지금 이터레이터가 가리키고 있는 요소를 지워버립니다!

    // erase() 함수는 지워진 요소(및 그 뒤에 있는 요소들)를 가리키던 이터레이터들을 모두 무효화시킵니다.
    // 그러므로 이제 이터레이터 &quot;it&quot;은 쓸모없는 쓰레기가 되었습니다.

    ++it; // 정의되지 않은 동작 (에러 위험!)
    std::cout &lt;&lt; *it &lt;&lt; &#39;\n&#39;; // 정의되지 않은 동작 (에러 위험!)

    return 0;
}
</code></pre>
<p>이렇게 무효화되어 길을 잃은 이터레이터는, 올바른 위치를 가리키는 새 이터레이터를 덮어씌워 주면(예: <code>begin()</code>, <code>end()</code>, 혹은 이터레이터를 반환하는 다른 함수의 결괏값을 다시 넣어줌) 다시 정상적으로 쓸 수 있습니다.</p>
<p>참고로 <code>erase()</code> 함수는 요소를 하나 지우고 나면, 친절하게도 <strong>&#39;지워진 녀석의 바로 다음 요소&#39;를 가리키는 새로운 이터레이터를 반환</strong>해 줍니다. (만약 마지막 요소를 지웠다면 <code>end()</code>를 줍니다).
따라서 위에서 에러가 나던 코드는 아래처럼 아주 쉽게 고칠 수 있습니다!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector v{ 1, 2, 3, 4, 5, 6, 7 };

    auto it{ v.begin() };

    ++it; // 두 번째 요소로 이동
    std::cout &lt;&lt; *it &lt;&lt; &#39;\n&#39;;

    it = v.erase(it); // 현재 요소를 지운 다음, `it`이 다음 요소를 가리키도록 새 이터레이터 값을 받아옵니다.

    std::cout &lt;&lt; *it &lt;&lt; &#39;\n&#39;; // 이제 안전합니다! 3이 출력됩니다.

    return 0;
}
</code></pre>
<hr>
<h2 id="183--표준-라이브러리-알고리즘-소개">18.3 — 표준 라이브러리 알고리즘 소개</h2>
<p>프로그래밍을 갓 배우기 시작한 분들은 정렬하기, 개수 세기, 배열 검색하기 같은 꽤 간단한 작업을 하기 위해 직접 반복문(loop)을 짜는 데 많은 시간을 보냅니다. 그런데 이 반복문이라는 게 은근히 골칫거리입니다. 오타나 실수를 내기도 쉽고, 나중에 코드를 다시 읽을 때 &quot;이게 뭐 하는 코드였지?&quot; 하고 한눈에 파악하기가 어려워서 유지보수하기도 까다롭거든요.</p>
<p>검색, 개수 세기, 정렬은 프로그래밍에서 정말 숨 쉬듯이 자주 하는 작업입니다. 그래서 C++ 표준 라이브러리(Standard Library)는 이런 귀찮은 작업들을 단 몇 줄의 코드만으로 끝낼 수 있도록 <strong>미리 만들어진 유용한 함수들의 묶음</strong>을 제공합니다. 게다가 이 함수들은 전문가들이 이미 꼼꼼하게 테스트해 두어서 안전하고, 속도도 엄청 빠르며, 여러 종류의 데이터 상자(컨테이너)에서 모두 사용할 수 있습니다. 심지어 일꾼(CPU 스레드) 여러 명을 동시에 투입해서 작업을 더 빨리 끝내는 &#39;병렬 처리&#39;를 지원하는 함수도 많답니다!</p>
<p>이런 &#39;알고리즘 라이브러리&#39;가 제공하는 기능은 크게 세 가지로 나눌 수 있어요.</p>
<ul>
<li><strong>검사기 (Inspectors):</strong> 데이터 상자 안을 들여다보기만 할 때 씁니다 (데이터를 수정하진 않아요). 검색이나 개수 세기가 여기 속합니다.</li>
<li><strong>변경기 (Mutators):</strong> 데이터 상자 안의 데이터를 지지고 볶고 수정할 때 씁니다. 정렬하거나 순서를 마구 섞는 작업이 있죠.</li>
<li><strong>도우미 (Facilitators):</strong> 데이터 값들을 바탕으로 새로운 결과를 만들어 낼 때 씁니다. 값들을 곱해주는 기능이나, 요소들을 어떤 순서로 정렬할지 심판을 봐주는 기능 등이 있어요.</li>
</ul>
<p>이런 꿀 기능들은 <code>&lt;algorithm&gt;</code>이라는 라이브러리 안에 살고 있습니다. 이번 레슨에서는 가장 자주 쓰이는 친구들 몇 명을 만나볼 거예요. 하지만 이 외에도 정말 무궁무진한 기능이 있으니, 나중에 공식 문서를 한번 쭉 읽어보시면서 어떤 마법 같은 기능들이 더 있는지 구경해 보시길 추천합니다!</p>
<p><em>(참고: 이 함수들은 모두 &#39;반복자(iterator)&#39;라는 것을 사용해서 작동합니다. 반복자가 아직 어색하시다면, 이전 레슨인 &#39;18.2 -- 반복자 소개&#39;를 먼저 가볍게 복습하고 오시면 훨씬 이해가 쉬울 거예요!)</em></p>
<hr>
<h3 id="stdfind를-사용해서-값으로-요소-찾기">std::find를 사용해서 값으로 요소 찾기</h3>
<p><code>std::find</code>는 상자(컨테이너) 안에서 우리가 찾는 값이 처음으로 나타나는 위치를 콕 집어 찾아줍니다. 이 함수를 부르려면 세 가지 준비물이 필요해요. </p>
<p>1) 검색을 시작할 위치, 2) 검색을 끝낼 위치, 그리고 3) 내가 찾고 싶은 값입니다.</p>
<p>만약 값을 찾으면 그 값이 있는 위치를 가리키는 &#39;반복자&#39;를 돌려주고, 끝까지 다 뒤져도 못 찾으면 상자의 맨 끝(end)을 가리키는 반복자를 돌려줍니다.</p>
<p>예를 들어볼까요?</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    std::array arr{ 13, 90, 99, 5, 40, 80 };

    std::cout &lt;&lt; &quot;Enter a value to search for and replace with: &quot;;
    int search{};
    int replace{};
    std::cin &gt;&gt; search &gt;&gt; replace;

    // 입력값 검증은 생략했습니다

    // std::find는 찾은 요소(또는 컨테이너의 끝)를 가리키는 반복자를 반환합니다.
    // (이 반복자의 정확한 타입은 굳이 신경 쓰지 않아도 되므로) 
    // auto를 사용해서 변수에 편하게 저장할게요.
    auto found{ std::find(arr.begin(), arr.end(), search) };

    // 알고리즘이 찾으려는 값을 찾지 못하면 &#39;끝(end) 반복자&#39;를 반환합니다.
    // end() 멤버 함수를 호출해서 이것과 똑같은지 비교해 볼 수 있어요.
    if (found == arr.end())
    {
        std::cout &lt;&lt; &quot;Could not find &quot; &lt;&lt; search &lt;&lt; &#39;\n&#39;;
    }
    else
    {
        // 찾은 요소의 값을 새 값으로 덮어씁니다.
        *found = replace;
    }

    for (int i : arr)
    {
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><strong>값을 무사히 찾았을 때의 실행 결과:</strong>
Enter a value to search for and replace with: 5 234
13 90 99 234 40 80</p>
<p><strong>값을 찾지 못했을 때의 실행 결과:</strong>
Enter a value to search for and replace with: 0 234
Could not find 0
13 90 99 5 40 80</p>
<hr>
<h3 id="stdfind_if를-사용해서-조건에-맞는-요소-찾기">std::find_if를 사용해서 조건에 맞는 요소 찾기</h3>
<p>가끔은 정확히 똑같은 값이 아니라, &quot;이 글자가 포함된 단어가 있나?&quot;처럼 <strong>특정 조건</strong>을 만족하는 값이 있는지 찾고 싶을 때가 있습니다. 이럴 때는 <code>std::find_if</code>가 아주 딱 맞습니다.</p>
<p><code>std::find_if</code>는 <code>std::find</code>와 일하는 방식은 비슷한데, 찾고 싶은 &#39;값&#39;을 넘겨주는 대신 <strong>&#39;함수&#39;</strong>를 넘겨준다는 게 다릅니다. 배열의 요소들을 하나하나 훑어보면서 이 함수에 값을 던져주는데요, 함수가 &quot;어? 이거 조건에 맞네!&quot; 하고 <code>true</code>(참)를 외치면 찾은 것으로 간주하고, 아니면 <code>false</code>(거짓)를 외치고 다음 요소로 넘어가는 식입니다. (나중에 배울 &#39;람다&#39;라는 걸 쓸 수도 있어요.)</p>
<p>아래 예제는 문자열 안에 &quot;nut&quot;이라는 글자가 들어있는 요소가 있는지 <code>std::find_if</code>로 찾아보는 코드입니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

// 요소가 조건에 맞으면 true를 반환하는 함수입니다.
bool containsNut(std::string_view str)
{
    // std::string_view::find는 부분 문자열을 찾지 못하면 std::string_view::npos를 반환합니다.
    // 찾았다면 str 안에서 해당 부분 문자열이 시작하는 위치(인덱스)를 반환하죠.
    return str.find(&quot;nut&quot;) != std::string_view::npos;
}

int main()
{
    std::array&lt;std::string_view, 4&gt; arr{ &quot;apple&quot;, &quot;banana&quot;, &quot;walnut&quot;, &quot;lemon&quot; };

    // 배열을 쭉 훑어보면서 &quot;nut&quot;이라는 부분 문자열이 포함된 요소가 있는지 확인합니다.
    auto found{ std::find_if(arr.begin(), arr.end(), containsNut) };

    if (found == arr.end())
    {
        std::cout &lt;&lt; &quot;No nuts\n&quot;;
    }
    else
    {
        std::cout &lt;&lt; &quot;Found &quot; &lt;&lt; *found &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}
</code></pre>
<p><strong>출력 결과:</strong>
Found walnut</p>
<p>만약 이 작업을 라이브러리 없이 직접 짜야 했다면, 배열을 훑는 반복문 하나에, 문자열을 비교하는 반복문 두 개까지... 최소 3개의 반복문을 겹겹이 써야 했을 겁니다. 표준 라이브러리를 쓰면 이렇게 단 몇 줄로 깔끔하게 해결됩니다!</p>
<hr>
<h3 id="stdcount와-stdcount_if를-사용해서-개수-세기">std::count와 std::count_if를 사용해서 개수 세기</h3>
<p>이름만 봐도 딱 아시겠죠? <code>std::count</code>와 <code>std::count_if</code>는 특정 값이나 특정 조건에 맞는 요소가 <strong>총 몇 개 있는지</strong>를 세어주는 착한 함수들입니다.</p>
<p>다음 예제에서는 &quot;nut&quot;이라는 글자가 포함된 요소가 모두 몇 개인지 세어볼게요.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

bool containsNut(std::string_view str)
{
    return str.find(&quot;nut&quot;) != std::string_view::npos;
}

int main()
{
    std::array&lt;std::string_view, 5&gt; arr{ &quot;apple&quot;, &quot;banana&quot;, &quot;walnut&quot;, &quot;lemon&quot;, &quot;peanut&quot; };

    auto nuts{ std::count_if(arr.begin(), arr.end(), containsNut) };

    std::cout &lt;&lt; &quot;Counted &quot; &lt;&lt; nuts &lt;&lt; &quot; nut(s)\n&quot;;

    return 0;
}
</code></pre>
<p><strong>출력 결과:</strong>
Counted 2 nut(s)</p>
<hr>
<h3 id="stdsort를-사용해서-내-맘대로-정렬하기">std::sort를 사용해서 내 맘대로 정렬하기</h3>
<p>예전 레슨에서 배열을 작은 수부터 큰 수로(오름차순) 예쁘게 줄 세울 때 <code>std::sort</code>를 써보셨을 거예요. 그런데 <code>std::sort</code>는 그것보다 훨씬 똑똑합니다. <code>std::sort</code>의 세 번째 준비물로 우리가 직접 만든 &#39;함수&#39;를 끼워 넣어 주면, 우리가 원하는 어떤 방식으로든 맘대로 줄을 세울 수 있거든요.</p>
<p>이 함수는 두 개의 값을 비교해서, 첫 번째 값이 두 번째 값보다 더 앞에 서야 한다면 <code>true</code>를 반환하도록 규칙을 정해주면 됩니다. (기본적으로 <code>std::sort</code>는 아무것도 안 알려주면 알아서 오름차순으로 줄을 세웁니다.)</p>
<p>그럼 이번에는 반대로, 큰 수부터 작은 수로(내림차순) 정렬해 볼까요? <code>greater</code>라는 우리만의 비교 규칙 함수를 만들어서 써보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;

bool greater(int a, int b)
{
    // a가 b보다 크면 a를 b보다 앞에 배치합니다 (내림차순 정렬).
    return (a &gt; b);
}

int main()
{
    std::array arr{ 13, 90, 99, 5, 40, 80 };

    // 우리가 만든 greater 함수를 std::sort에 넘겨줍니다.
    std::sort(arr.begin(), arr.end(), greater);

    for (int i : arr)
    {
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><strong>출력 결과:</strong>
99 90 80 40 13 5</p>
<p>이것도 마찬가지로 머리 아프게 정렬 반복문을 직접 만들 필요 없이, 단 몇 줄만으로 내 맘대로 정렬을 끝냈습니다!</p>
<p>그런데 여기서 조금 헷갈리실 수 있는 부분! 우리가 만든 <code>greater</code> 함수는 2개의 재료(인수)가 필요한데, 코드에서는 괄호 <code>()</code>도 없이 이름만 달랑 적어줬죠? 값은 대체 어디서 가져온 걸까요?
함수 이름 뒤에 괄호를 빼고 적으면, 이건 함수를 당장 실행하라는 게 아니라 &quot;이 함수가 여깄어~&quot; 하고 위치만 가리키는 <strong>함수 포인터</strong>가 됩니다. 예전에 함수를 괄호 없이 출력하려고 했을 때 화면에 이상하게 &quot;1&quot;이라고 찍혔던 거 기억하시나요? <code>std::sort</code>는 이 포인터를 가지고 있다가, 자기가 알아서 배열 안의 두 요소를 쏙쏙 뽑아 <code>greater</code> 함수에 넣고 실행해 보는 겁니다. <code>std::sort</code>가 속에서 어떤 정렬 방식을 쓰는지 우리는 모르기 때문에, 어떤 두 요소가 비교될지는 그때그때 다릅니다. (이 &#39;함수 포인터&#39;에 대해서는 나중 챕터에서 더 자세히 다룰 거니까 너무 걱정하지 마세요!)</p>
<hr>
<h3 id="stdfor_each를-사용해서-모든-요소에-작업-수행하기">std::for_each를 사용해서 모든 요소에 작업 수행하기</h3>
<p><code>std::for_each</code>는 리스트를 넘겨받아서, 그 안에 있는 <strong>모든 요소 하나하나에 똑같은 함수를 실행</strong>시켜 줍니다. 리스트에 있는 모든 숫자를 두 배로 뻥튀기하고 싶거나 할 때 아주 유용하죠.</p>
<p>배열의 모든 숫자를 두 배로 만드는 예제를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;array&gt;
#include &lt;iostream&gt;

void doubleNumber(int&amp; i)
{
    i *= 2;
}

int main()
{
    std::array arr{ 1, 2, 3, 4 };

    std::for_each(arr.begin(), arr.end(), doubleNumber);

    for (int i : arr)
    {
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><strong>출력 결과:</strong>
2 4 6 8</p>
<p>처음 프로그래밍을 배우시는 분들은 여기서 &quot;어? 그냥 범위 기반 for문(range-based for-loop) 쓰면 코드도 더 짧고 편한데 굳이 이 복잡해 보이는 걸 왜 쓰지?&quot; 하고 의아해하실 수 있습니다. 하지만 <code>std::for_each</code>만의 숨겨진 강력한 장점들이 있습니다. 두 방식을 비교해 볼게요.</p>
<pre><code class="language-cpp">std::ranges::for_each(arr, doubleNumber); // C++20부터는 begin()과 end()를 쓰지 않아도 됩니다.
// std::for_each(arr.begin(), arr.end(), doubleNumber); // C++20 이전 방식

for (auto&amp; i : arr)
{
    doubleNumber(i);
}
</code></pre>
<p><code>std::for_each</code>를 쓰면, 코드를 읽는 사람에게 &quot;나는 <code>arr</code>의 모든 요소에 <code>doubleNumber</code> 작업을 할 거야!&quot;라는 내 의도를 아주 또렷하게 보여줄 수 있습니다. 반면 for문은 <code>i</code>라는 변수를 하나 더 끼워 넣어야 하죠. 이게 별거 아닌 것 같아 보여도, 사람이 피곤하거나 깜빡 정신을 놓으면 자잘한 실수들을 만들기 딱 좋습니다.
예를 들어 <code>auto</code>를 안 써서 나도 모르게 타입이 변환되어 버리거나, 참조 기호(<code>&amp;</code>)를 깜빡해서 정작 배열 원본 값은 안 변하고 헛수고를 하거나, <code>doubleNumber</code>에 <code>i</code>가 아닌 다른 엉뚱한 변수를 넣는 실수를 할 수 있죠. <code>std::for_each</code>를 쓰면 구조상 이런 억울한 실수가 아예 일어날 수 없습니다!</p>
<p>게다가 <code>std::for_each</code>는 처음이나 끝부분을 마음대로 건너뛰고 실행할 수도 있습니다. 예를 들어 첫 번째 요소는 빼고 두 배로 만들고 싶다면, <code>std::next</code>를 써서 시작 위치를 한 칸 밀어버리면 됩니다.</p>
<pre><code class="language-cpp">std::for_each(std::next(arr.begin()), arr.end(), doubleNumber);
// 이제 배열은 [1, 4, 6, 8]이 됩니다. 첫 번째 요소인 1은 두 배가 되지 않았어요.
</code></pre>
<p>보통의 범위 기반 for문으로는 이렇게 부분만 쏙 골라서 실행하기가 꽤 번거롭습니다.</p>
<p>가장 중요한 차이점은 속도입니다. <code>std::for_each</code>는 다른 알고리즘들처럼 여러 작업을 동시에 해치우는 <strong>병렬 처리</strong>가 가능하기 때문에, 다뤄야 할 데이터가 엄청나게 많거나 대규모 프로젝트를 할 때는 단순 for문보다 훨씬 처리 속도가 빠릅니다.</p>
<hr>
<h3 id="성능과-실행-순서">성능과 실행 순서</h3>
<p>알고리즘 라이브러리에 있는 기능들은 대체로 &quot;나는 최소한 이 정도 속도는 낼 거야!&quot; 라거나 &quot;나는 꼭 이 순서대로 실행할 거야!&quot; 하는 약속(보장)을 가지고 있습니다. 예를 들어 <code>std::for_each</code>는 각 요소에 딱 한 번씩만 들르고, 무조건 맨 앞에서부터 순서대로 차근차근 실행하겠다고 굳게 약속합니다.</p>
<p>대부분의 알고리즘이 속도 보장은 해주지만, <strong>실행 순서까지 확고하게 보장해 주는 알고리즘은 생각보다 많지 않습니다</strong>. 그래서 &#39;당연히 차례대로 실행되겠지?&#39; 하고 우리 마음대로 넘겨짚고 코드를 짜면 위험합니다.
예를 들어 첫 번째 값에는 1을 곱하고, 두 번째에는 2를 곱하고, 세 번째는 3을 곱하는 식으로 순서가 절대적으로 중요한 작업을 해야 한다면, 반드시 &#39;순차적 실행&#39;을 보장하는 알고리즘만 골라서 써야 합니다!</p>
<p>순서대로 얌전히 실행될 것을 보장하는 알고리즘 삼총사는 <code>std::for_each</code>, <code>std::copy</code>, <code>std::copy_backward</code>, <code>std::move</code>, <code>std::move_backward</code>가 있습니다. (물론 &#39;순방향 반복자&#39;라는 걸 쓰는 다른 많은 알고리즘들도 구조상 자연스럽게 순서대로 실행되긴 합니다.)</p>
<blockquote>
<p><strong>권장 사항</strong>
특정 알고리즘을 사용하기 전에, 내가 지금 하려는 작업이 속도나 실행 순서가 중요한지, 그리고 이 알고리즘이 그걸 만족시켜 주는지 꼭 한번 체크하는 습관을 들이세요!</p>
</blockquote>
<hr>
<h3 id="c20의-범위-ranges">C++20의 범위 (Ranges)</h3>
<p>알고리즘을 쓸 때마다 <code>arr.begin()</code>이랑 <code>arr.end()</code>를 꼬박꼬박 타자 치기, 솔직히 너무 귀찮고 손가락 아프지 않으셨나요? 걱정하지 마세요! C++20부터는 <strong>범위(ranges)</strong>라는 멋진 기능이 추가되어서, 그냥 <code>arr</code> 하나만 툭 던져줘도 똑같이 작동합니다. 덕분에 코드가 훨씬 짧아지고 읽기도 편해졌답니다.</p>
<hr>
<h3 id="결론">결론</h3>
<p>알고리즘 라이브러리는 우리 코드를 훨씬 단순하고, 실수 없이 튼튼하게 만들어주는 마법 같은 도구 상자입니다. 이번 레슨에서는 그중 아주 일부분만 살짝 맛보았지만, 이 함수들은 일하는 방식이 다들 비슷비슷하기 때문에 원리 몇 가지만 익혀두시면 다른 기능들도 금방 가져다 쓰실 수 있을 거예요.</p>
<blockquote>
<p><strong>참고로...</strong>
<a href="https://www.youtube.com/watch?v=2olsGf6JIkU">이 비디오</a>에서 라이브러리에 있는 다양한 알고리즘들을 아주 깔끔하게 정리해서 설명해 주니, 시간이 나실 때 꼭 한번 시청해 보세요.</p>
</blockquote>
<blockquote>
<p><strong>권장 사항</strong>
직접 반복문을 짜면서 고생하기보다는, 내가 하려는 작업과 똑같은 기능을 하는 함수가 알고리즘 라이브러리에 있는지 먼저 찾아보고 적극적으로 활용해 보세요!</p>
</blockquote>
<hr>
<h2 id="184--코드-실행-시간-측정하기">18.4 — 코드 실행 시간 측정하기</h2>
<p>코드를 짜다 보면, &quot;이 방법이랑 저 방법 중에 어느 쪽이 더 빠를까?&quot; 하고 궁금해질 때가 있죠. 그걸 어떻게 알 수 있을까요?</p>
<p>가장 쉬운 방법은 <strong>코드가 실행되는 데 걸리는 시간을 직접 재보는 것</strong>입니다. 
C++11 버전부터는 <code>chrono</code>라는 라이브러리 안에 시간을 잴 수 있는 도구들이 들어있어요. 하지만 이 <code>chrono</code> 라이브러리를 있는 그대로 쓰기엔 조금 복잡하고 어렵게 느껴질 수 있습니다.</p>
<p>다행히도 좋은 소식이 하나 있어요! 우리가 필요한 시간 측정 기능들만 쏙쏙 뽑아서, 앞으로 우리 프로그램에서 쉽게 꺼내 쓸 수 있도록 하나의 &#39;클래스(class)&#39;로 깔끔하게 포장해 둘 수 있답니다.</p>
<p>바로 이 클래스입니다.</p>
<pre><code class="language-cpp">#include &lt;chrono&gt; // std::chrono 함수들을 사용하기 위해 포함

class Timer
{
private:
    // 복잡한 타입 이름을 쉽게 쓰기 위해 별칭(alias) 만들기
    using Clock = std::chrono::steady_clock;
    using Second = std::chrono::duration&lt;double, std::ratio&lt;1&gt; &gt;;

    std::chrono::time_point&lt;Clock&gt; m_beg { Clock::now() };

public:
    void reset()
    {
        m_beg = Clock::now();
    }

    double elapsed() const
    {
        return std::chrono::duration_cast&lt;Second&gt;(Clock::now() - m_beg).count();
    }
};
</code></pre>
<p>이게 전부입니다! 사용 방법도 정말 간단해요. <code>main</code> 함수 맨 위(혹은 시간 측정을 시작하고 싶은 곳)에 <code>Timer</code> 객체를 하나 만들어 주기만 하면 됩니다. 그러고 나서 프로그램이 거기까지 실행되는 데 얼마나 걸렸는지 알고 싶을 때마다 <code>elapsed()</code>라는 함수를 불러주면 된답니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    Timer t;

    // 시간을 측정할 코드를 여기에 넣으세요

    std::cout &lt;&lt; &quot;걸린 시간: &quot; &lt;&lt; t.elapsed() &lt;&lt; &quot; 초\n&quot;;

    return 0;
}
</code></pre>
<p>자, 이제 숫자 10,000개가 들어있는 배열을 정렬하는 실제 예제에 이 타이머를 사용해 볼까요? 먼저, 우리가 이전 장에서 직접 만들었던 &#39;선택 정렬(selection sort)&#39; 방식을 써보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;chrono&gt; // std::chrono 함수들을 사용하기 위해 포함
#include &lt;cstddef&gt; // std::size_t를 사용하기 위해 포함
#include &lt;iostream&gt;
#include &lt;numeric&gt; // std::iota를 사용하기 위해 포함

const int g_arrayElements { 10000 };

class Timer
{
private:
    // 복잡한 타입 이름을 쉽게 쓰기 위해 별칭(alias) 만들기
    using Clock = std::chrono::steady_clock;
    using Second = std::chrono::duration&lt;double, std::ratio&lt;1&gt; &gt;;

    std::chrono::time_point&lt;Clock&gt; m_beg{ Clock::now() };

public:
    void reset()
    {
        m_beg = Clock::now();
    }

    double elapsed() const
    {
        return std::chrono::duration_cast&lt;Second&gt;(Clock::now() - m_beg).count();
    }
};

void sortArray(std::array&lt;int, g_arrayElements&gt;&amp; array)
{
    // 배열의 각 요소를 하나씩 확인합니다 
    // (마지막 요소는 이미 정렬되어 있을 테니 제외합니다)
    for (std::size_t startIndex{ 0 }; startIndex &lt; (g_arrayElements - 1); ++startIndex)
    {
        // smallestIndex는 이번 반복에서 찾은 가장 작은 값의 위치(인덱스)입니다.
        // 일단 현재 시작 위치의 값이 가장 작다고 가정하고 시작합니다.
        std::size_t smallestIndex{ startIndex };

        // 그런 다음 배열의 나머지 부분에서 더 작은 값이 있는지 찾아봅니다.
        for (std::size_t currentIndex{ startIndex + 1 }; currentIndex &lt; g_arrayElements; ++currentIndex)
        {
            // 만약 지금까지 찾은 가장 작은 값보다 더 작은 값을 발견했다면
            if (array[currentIndex] &lt; array[smallestIndex])
            {
                // 그 위치를 기억해 둡니다.
                smallestIndex = currentIndex;
            }
        }

        // 이제 smallestIndex는 남은 배열 중 가장 작은 값을 가리킵니다.
        // 시작 위치의 값과 가장 작은 값을 서로 바꿉니다 (이렇게 해서 알맞은 자리에 정렬합니다).
        std::swap(array[startIndex], array[smallestIndex]);
    }
}

int main()
{
    std::array&lt;int, g_arrayElements&gt; array;
    std::iota(array.rbegin(), array.rend(), 1); // 배열을 10000부터 1까지의 역순 숫자로 채웁니다

    Timer t;

    sortArray(array);

    std::cout &lt;&lt; &quot;걸린 시간: &quot; &lt;&lt; t.elapsed() &lt;&lt; &quot; 초\n&quot;;

    return 0;
}
</code></pre>
<p>원작자의 컴퓨터에서 3번 실행해 본 결과, 0.0507초, 0.0506초, 0.0498초가 나왔다고 합니다. 대략 <strong>0.05초</strong> 정도 걸렸다고 볼 수 있겠네요.</p>
<p>그럼 이번에는 C++ 기본 라이브러리에서 제공하는 강력한 도구인 <code>std::sort</code>를 사용해서 똑같이 테스트해 볼까요?</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt; // std::sort를 사용하기 위해 포함
#include &lt;array&gt;
#include &lt;chrono&gt; // std::chrono 함수들을 사용하기 위해 포함
#include &lt;cstddef&gt; // std::size_t를 사용하기 위해 포함
#include &lt;iostream&gt;
#include &lt;numeric&gt; // std::iota를 사용하기 위해 포함

const int g_arrayElements { 10000 };

class Timer
{
private:
    // 복잡한 타입 이름을 쉽게 쓰기 위해 별칭(alias) 만들기
    using Clock = std::chrono::steady_clock;
    using Second = std::chrono::duration&lt;double, std::ratio&lt;1&gt; &gt;;

    std::chrono::time_point&lt;Clock&gt; m_beg{ Clock::now() };

public:
    void reset()
    {
        m_beg = Clock::now();
    }

    double elapsed() const
    {
        return std::chrono::duration_cast&lt;Second&gt;(Clock::now() - m_beg).count();
    }
};

int main()
{
    std::array&lt;int, g_arrayElements&gt; array;
    std::iota(array.rbegin(), array.rend(), 1); // 배열을 10000부터 1까지의 역순 숫자로 채웁니다

    Timer t;

    std::ranges::sort(array); // C++20 버전 이상부터 사용 가능
    // 만약 컴파일러가 C++20을 지원하지 않는다면 아래 코드를 사용하세요:
    // std::sort(array.begin(), array.end());

    std::cout &lt;&lt; &quot;걸린 시간: &quot; &lt;&lt; t.elapsed() &lt;&lt; &quot; 초\n&quot;;

    return 0;
}
</code></pre>
<p>원작자의 컴퓨터에서는 이 코드가 0.000693초, 0.000692초, 0.000699초를 기록했습니다. 평균적으로 대략 <strong>0.0007초</strong>가 걸렸네요.
다시 말해 이 경우, <strong>표준 라이브러리의 <code>std::sort</code>가 우리가 직접 만든 선택 정렬보다 무려 100배나 더 빠르다는 뜻입니다!</strong></p>
<hr>
<h3 id="프로그램-성능속도에-영향을-미치는-요소들">프로그램 성능(속도)에 영향을 미치는 요소들</h3>
<p>프로그램 실행 시간을 재는 것 자체는 아주 쉽지만, 그 결과는 꽤 여러 가지 이유로 휙휙 바뀔 수 있어요. 그래서 제대로 측정하는 법과 무엇이 시간에 영향을 주는지 알아두는 게 무척 중요합니다.</p>
<ol>
<li><strong>디버그(Debug) 모드가 아닌 릴리즈(Release) 모드로 측정하세요.</strong> 디버그 모드는 오류를 찾기 쉽게 하려고 코드를 빠르게 만들어주는 &#39;최적화&#39; 기능을 꺼둡니다. 이 때문에 속도 차이가 엄청나게 나요. 원작자 컴퓨터에서 디버그 모드로 <code>std::sort</code>를 실행했더니 무려 33배나 느린 0.0235초가 걸렸다고 합니다!</li>
<li><strong>컴퓨터가 뒤에서 몰래 하고 있는 다른 작업들을 조심하세요.</strong> 측정할 때 컴퓨터가 CPU나 메모리, 하드디스크를 무리하게 쓰고 있으면 안 됩니다 (예: 게임 중이거나, 바이러스 검사 중이거나, 몰래 윈도우 업데이트가 깔리는 중일 때). 심지어 그냥 켜둔 인터넷 브라우저가 새 광고를 띄우느라 잠깐 CPU를 100% 써버리는 일도 생겨요. 측정하기 전에 켜져 있는 프로그램을 최대한 다 끄면, 결과가 매번 들쭉날쭉하는 걸 막을 수 있습니다.</li>
<li><strong>랜덤 숫자(난수)를 쓴다면 결과가 달라질 수 있어요.</strong> 매번 랜덤 숫자가 다르게 나오면 코드가 일하는 양도 달라집니다. 여러 번 잴 때 항상 비슷한 결과를 얻고 싶다면, 랜덤 숫자를 만드는 공식에 &#39;고정된 시작 값(시드)&#39;을 넣어주세요. 그러면 매번 똑같은 순서로 난수가 나오니까 측정이 편해져요. (다만, 프로그램의 성능 자체가 이 랜덤 숫자에 너무 의존적이라면 이 방법이 오히려 가짜 결과를 보여줄 수도 있으니 주의해야 합니다.)</li>
<li><strong>사용자 입력을 기다리는 시간은 빼고 재야 합니다.</strong> 사용자 이름이나 비밀번호를 입력하라고 기다리는 시간은 프로그램 성능과 아무 상관이 없겠죠? 입력이 필요하다면 측정 중에는 사람이 타이핑하지 않게, 코드가 자동으로 입력값을 가져오도록 살짝 바꿔서 재는 것이 좋습니다.</li>
</ol>
<hr>
<h3 id="성능-측정하는-방법">성능 측정하는 방법</h3>
<p>내 프로그램의 진짜 성능을 알아보려면 <strong>최소한 3번 이상</strong> 측정해 보세요. 3번 다 결과가 비슷하게 나온다면, 그게 현재 컴퓨터에서 내 프로그램이 내는 진짜 속도라고 볼 수 있습니다. 만약 결과가 중구난방이라면, 비슷한 숫자들이 모일 때까지 여러 번 더 재보셔야 해요 (그리고 유독 튀는 숫자가 나왔다면 왜 그런지 이유를 파악해야 합니다). 보통 컴퓨터가 뒤에서 다른 짓을 하느라 한두 번씩 엄청 늦게 끝나는 &#39;튀는 값(outlier)&#39;이 나오는 건 흔한 일입니다.</p>
<p>계속 재봤는데도 결과가 들쭉날쭉하다면, 컴퓨터의 다른 프로그램들이 크게 방해하고 있거나 내 프로그램 안의 무작위적인 요소(랜덤) 때문일 확률이 큽니다.</p>
<p>프로그램 속도는 컴퓨터 사양, 운영체제, 켜져 있는 다른 앱 등 정말 많은 것들에 영향을 받아요. 그래서 &quot;내 프로그램은 10초 만에 끝난다!&quot; 같은 <strong>&#39;절대적인 시간&#39;은 남의 컴퓨터에서는 별 의미가 없습니다.</strong> 남의 컴퓨터에선 1초가 될 수도, 1분이 될 수도 있거든요. 직접 테스트해 보기 전엔 모르는 일이죠.</p>
<p>하지만 내 컴퓨터 하나에서 하는 <strong>&#39;상대적인 비교&#39;는 아주 쓸모가 많습니다.</strong> 어떤 코드를 A 방법으로 짰을 때는 10초 걸리고, B 방법으로 짰을 때는 8초 걸렸다면, 다른 컴퓨터에서도 시간 수치 자체는 다르겠지만 결국 <strong>B 방법이 더 빠를 거라는 사실은 변함이 없으니까요.</strong></p>
<p>가장 확실하게 비교하는 꿀팁을 하나 드릴게요. B 방법을 다 재보고 난 뒤에, 다시 처음의 A 방법을 한번 더 재보는 겁니다. 만약 다시 잰 A 방법이 맨 처음 잰 10초와 똑같이 나온다면, 컴퓨터 상태가 일정했다는 뜻이니 안심하고 &#39;B가 더 빠르다&#39;라고 결론 내릴 수 있습니다.
하지만 만약 A 방법의 시간이 갑자기 달라졌다면? 방금 내 컴퓨터에 무슨 일이 생겨서 성능에 영향을 줬다는 뜻이에요. 이럴 때는 그동안 측정한 결과를 다 버리고 처음부터 다시 꼼꼼하게 측정하는 것이 좋습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 17]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-17</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-17</guid>
            <pubDate>Fri, 06 Mar 2026 09:32:27 GMT</pubDate>
            <description><![CDATA[<h2 id="171--stdarray-입문">17.1 — <code>std::array</code> 입문</h2>
<p>이전 16.1 레슨에서 컨테이너와 배열에 대해 처음 배웠었죠. 
핵심만 아주 간단하게 다시 짚어볼게요!</p>
<ul>
<li><strong>컨테이너(Container)</strong> 는 이름이 없는 여러 개의 데이터(원소라고 부름)를 한꺼번에 담아두는 &#39;보관함&#39; 같은 역할을 해요.</li>
<li><strong>배열(Array)</strong> 은 이 데이터들을 메모리 공간에 빈틈없이 일렬로 나란히 저장해요. 그리고 각 자리에 번호(인덱스)가 있어서 원하는 데이터를 아주 빠르게 바로바로 꺼낼 수 있습니다.</li>
<li>C++에서 가장 자주 쓰이는 배열 종류는 크게 세 가지예요: <code>std::vector</code>, <code>std::array</code>, 그리고 아주 옛날 방식인 C스타일 배열이죠.</li>
</ul>
<p>그리고 16.10 레슨에서, 배열은 크게 두 가지 종류로 나뉜다고 했어요.</p>
<ul>
<li><strong>고정 크기 배열 (Fixed-size arrays):</strong> 말 그대로 크기가 고정된 배열이에요. 배열을 처음 만들 때 크기를 몇 개로 할지 반드시 정해둬야 하고, 나중에 크기를 늘리거나 줄일 수 없어요. C스타일 배열과 오늘 배울 <code>std::array</code> 가 여기에 속해요.</li>
<li><strong>동적 배열 (Dynamic arrays):</strong> 프로그램이 실행되는 도중에 고무줄처럼 크기를 자유롭게 늘리거나 줄일 수 있는 배열이에요. <code>std::vector</code> 가 바로 이 동적 배열이랍니다.</li>
</ul>
<p>이전 장에서는 주로 <code>std::vector</code> 에 대해 배웠어요. 빠르고, 쓰기 편하고, 기능도 다양해서 우리가 배열이 필요할 때 가장 먼저 찾는 아주 훌륭한 도구죠.</p>
<p><strong>&quot;그럼 무조건 융통성 있는 동적 배열만 쓰면 안 되나요?&quot;</strong></p>
<p>동적 배열은 확실히 강력하고 편리하지만, 세상 모든 일이 그렇듯 장점이 있으면 살짝 양보해야 하는 부분(Tradeoff)도 있기 마련이에요.</p>
<ol>
<li><code>std::vector</code> 는 크기가 딱 고정된 배열보다 속도가 아주 살짝 느립니다. (물론 코드를 비효율적으로 짜서 배열 크기를 계속 늘렸다 줄였다 하는 게 아니라면, 아마 체감하기 힘들 정도로 미미한 차이이긴 해요.)</li>
<li><code>std::vector</code> 는 <code>constexpr</code> (컴파일할 때 값이 영원히 고정되는 완벽한 상수) 기능을 아주 제한적인 상황에서만 쓸 수 있어요.</li>
</ol>
<p>최신 C++(모던 C++)에서는 바로 이 두 번째 이유가 <strong>핵심</strong>이에요! 
<code>constexpr</code> 배열을 사용하면 코드가 훨씬 더 단단해지고 오류가 줄어들며, 컴파일러가 코드를 알아서 엄청나게 최적화해 줄 수 있거든요.
따라서 <code>constexpr</code> 배열을 쓸 수 있는 상황이라면 무조건 써야 하고, 그럴 때 바로 우리가 써야 할 컨테이너가 <code>std::array</code> 인 것입니다!</p>
<blockquote>
<p><strong>권장 사항</strong>
값이 변하지 않는 <code>constexpr</code> 배열을 만들 때는 <code>std::array</code> 를 사용하고, 그 외의 일반적인 경우에는 <code>std::vector</code> 를 사용하세요.</p>
</blockquote>
<hr>
<h3 id="stdarray-만들기"><code>std::array</code> 만들기</h3>
<p><code>std::array</code> 를 쓰려면 <code>&lt;array&gt;</code> 헤더 파일을 불러와야 해요. 애초에 <code>std::vector</code> 와 비슷하게 작동하도록 만들어졌기 때문에, 써보시면 둘이 서로 다른 점보다는 비슷한 점이 훨씬 많다는 걸 알게 되실 거예요.</p>
<p>하지만 처음 만들 때의 모습은 살짝 다릅니다:</p>
<pre><code class="language-cpp">#include &lt;array&gt;  // std::array를 쓰기 위해 필요해요
#include &lt;vector&gt; // std::vector를 쓰기 위해 필요해요

int main()
{
    std::array&lt;int, 5&gt; a {};  // 5개의 int 데이터를 담는 std::array

    std::vector&lt;int&gt; b(5);    // 5개의 int 데이터를 담는 std::vector (비교용이에요)

    return 0;
}
</code></pre>
<p>우리가 만든 <code>std::array</code> 의 선언을 보면 꺾쇠 <code>&lt; &gt;</code> 안에 두 가지 정보(템플릿 인자)가 들어갑니다.
첫 번째인 <code>int</code> 는 이 배열 안에 <strong>어떤 종류의 데이터</strong> 를 넣을지 정해주는 것이고, 두 번째인 <code>5</code> 는 <strong>배열의 길이(크기)</strong> 를 딱 정해주는 숫자입니다.</p>
<hr>
<h3 id="stdarray-의-길이는-반드시-상수-표현식-이어야-해요"><code>std::array</code> 의 길이는 반드시 <strong>상수 표현식</strong> 이어야 해요</h3>
<p>프로그램 실행 중에 마음대로 크기를 바꿀 수 있는 <code>std::vector</code> 와는 다르게, <code>std::array</code> 의 길이는 프로그램이 실행되기 전(컴파일 단계)에 이미 확실히 정해져 있는 <strong>상수 표현식</strong> 이어야 해요.
주로 그냥 숫자(정수 리터럴)를 바로 쓰거나, <code>constexpr</code> 변수, 또는 열거형(enum) 값을 사용해서 길이를 정해줍니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;

int main()
{
    std::array&lt;int, 7&gt; a {}; // 평범한 숫자(리터럴)를 사용했어요

    constexpr int len { 8 };
    std::array&lt;int, len&gt; b {}; // constexpr 변수를 사용했어요

    enum Colors
    {
         red,
         green,
         blue,
         max_colors
    };

    std::array&lt;int, max_colors&gt; c {}; // 열거형(enum) 값을 사용했어요

#define DAYS_PER_WEEK 7
    std::array&lt;int, DAYS_PER_WEEK&gt; d {}; // 매크로를 사용했어요 (이렇게 쓰지 말고 constexpr 변수를 쓰세요!)

    return 0;
}
</code></pre>
<p>주의할 점은, 실행 중에 값이 바뀔 수 있는 일반 변수나 매개변수 등은 길이로 쓸 수 없다는 거예요!</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

void foo(const int length) // length는 프로그램 실행 중에 값이 결정되는 상수예요
{
    std::array&lt;int, length&gt; e {}; // 에러: length는 컴파일러가 미리 알 수 있는 상수 표현식이 아니에요!
}

int main()
{
    // const가 아닌 일반 변수를 사용하는 경우
    int numStudents{};
    std::cin &gt;&gt; numStudents; // 사용자에게 값을 입력받으므로 값이 바뀔 수 있죠

    std::array&lt;int, numStudents&gt; {}; // 에러: numStudents는 상수 표현식이 아니에요!

    foo(7);

    return 0;
}
</code></pre>
<blockquote>
<p><strong>주의하세요!</strong>
의아하게 들릴 수 있지만, 길이가 0인 <code>std::array</code> 도 만들 수 있어요.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    std::array&lt;int, 0&gt; arr {}; // 길이가 0인 std::array를 만들어요
    std::cout &lt;&lt; arr.empty();  // arr의 길이가 0이면 true가 나옵니다

    return 0;
}
</code></pre>
<p>길이가 0인 <code>std::array</code> 는 데이터가 아예 없는 아주 특별한 케이스예요. 만약 빈 배열인데 실수로 안의 데이터에 접근하려고 시도하면(예: <code>arr[0]</code>) 프로그램이 꼬여버리는 치명적인 오류(미정의 동작)가 발생합니다. 배열이 텅 비었는지 확인하고 싶을 때는 <code>empty()</code> 라는 함수를 쓰면 안전하게 확인할 수 있어요.</p>
<hr>
<h3 id="stdarray-값-채워넣기-초기화"><code>std::array</code> 값 채워넣기 (초기화)</h3>
<p>조금 특이하게도 <code>std::array</code> 에는 &#39;생성자&#39;라는 복잡한 기능이 없어요. 대신 데이터를 담는 순수한 바구니(집합체, Aggregate) 취급을 받기 때문에 <strong>집합체 초기화</strong> 라는 단순한 방식을 씁니다.
어렵게 생각할 것 없이, 중괄호 <code>{}</code> 안에 쉼표로 값을 쭉 나열해서 한 번에 초기화하는 방식이에요.</p>
<pre><code class="language-cpp">#include &lt;array&gt;

int main()
{
    std::array&lt;int, 6&gt; fibonnaci = { 0, 1, 1, 2, 3, 5 }; // 중괄호를 사용한 복사 리스트 초기화 방식
    std::array&lt;int, 5&gt; prime { 2, 3, 5, 7, 11 };         // 중괄호를 사용한 직접 리스트 초기화 방식 (이 방식을 추천해요!)

    return 0;
}
</code></pre>
<p>이렇게 하면 배열의 0번째 칸부터 순서대로 우리가 적은 값들이 쏙쏙 들어갑니다.</p>
<p>만약 아무 값도 안 넣어주면 어떻게 될까요? 기본값으로 알아서 비워지는데, 문제는 컴퓨터 메모리 특성상 이전에 쓰던 &#39;쓰레기값&#39;이 남아있을 수 있다는 거예요.
그래서 안전하게 처음부터 모든 칸을 깔끔하게 &#39;0&#39;으로 비우고 싶다면, 텅 빈 중괄호 <code>{}</code> 를 써서 &#39;값 초기화&#39;를 해주는 것이 가장 좋습니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;vector&gt;

int main()
{
    std::array&lt;int, 5&gt; a;   // 기본 초기화 (int 자리들에 쓰레기값이 남아있을 수 있어요)
    std::array&lt;int, 5&gt; b{}; // 값 초기화 (모든 int 자리가 깔끔하게 0으로 채워져요) (이 방식을 추천해요!)

    std::vector&lt;int&gt; v(5);  // 값 초기화 (모든 int 자리가 깔끔하게 0으로 채워져요) (비교용이에요)

    return 0;
}
</code></pre>
<p>배열 크기보다 숫자를 너무 많이 적어 넣으면 컴파일러가 에러를 냅니다. 반대로 크기보다 숫자를 적게 넣으면, 우리가 적어준 것만 채우고 남은 빈자리는 알아서 모두 0으로 깔끔하게 채워준답니다!</p>
<pre><code class="language-cpp">#include &lt;array&gt;

int main()
{
    std::array&lt;int, 4&gt; a { 1, 2, 3, 4, 5 }; // 컴파일 에러: 값을 너무 많이 넣었어요!
    std::array&lt;int, 4&gt; b { 1, 2 };          // 남은 자리인 b[2]와 b[3]은 알아서 0으로 채워져요

    return 0;
}
</code></pre>
<hr>
<h3 id="const-와-constexpr-stdarray">Const 와 Constexpr <code>std::array</code></h3>
<p><code>std::array</code> 는 내용을 바꿀 수 없는 <code>const</code> 로 만들 수 있어요:</p>
<pre><code class="language-cpp">#include &lt;array&gt;

int main()
{
    const std::array&lt;int, 5&gt; prime { 2, 3, 5, 7, 11 };

    return 0;
}
</code></pre>
<p>안에 들어있는 데이터 하나하나에 일일이 <code>const</code> 를 붙여주지 않아도, 배열 자체가 통째로 <code>const</code> 이기 때문에 안의 내용물도 자동으로 절대 바꿀 수 없는 상태가 됩니다.</p>
<p>그리고 <code>std::array</code> 는 완벽한 상수인 <code>constexpr</code> 기능도 100% 완벽하게 지원합니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;

int main()
{
    constexpr std::array&lt;int, 5&gt; prime { 2, 3, 5, 7, 11 };

    return 0;
}
</code></pre>
<p><strong>이 완벽한 <code>constexpr</code> 지원이 바로 우리가 <code>std::array</code> 를 사용해야 하는 가장 핵심적인 이유입니다.</strong></p>
<blockquote>
<p><strong>권장 사항</strong>
가능하다면 여러분의 <code>std::array</code> 는 항상 <code>constexpr</code> 로 만드세요. 만약 <code>constexpr</code> 로 만들 수 없는 상황이라면, 차라리 <code>std::vector</code> 를 쓰는 게 나을지 고민해 보는 것이 좋습니다.</p>
</blockquote>
<hr>
<h3 id="똑똑한-컴파일러의-타입-추론-기능-ctad---c17-이상">똑똑한 컴파일러의 타입 추론 기능 (CTAD) - C++17 이상</h3>
<p>C++17 버전부터는 &#39;클래스 템플릿 인자 추론(CTAD)&#39; 이라는 똑똑한 기능이 생겼어요. 우리가 괄호 안에 적어준 값들을 보고, 컴파일러가 눈치껏
<strong>&quot;아~ 데이터 타입은 이거고, 길이는 이만큼이구나!&quot;</strong> 하고 알아서 맞춰주는 기능이에요.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    constexpr std::array a1 { 9, 7, 5, 3, 1 }; // 값들을 보고 컴파일러가 알아서 std::array&lt;int, 5&gt; 라고 추론해요
    constexpr std::array a2 { 9.7, 7.31 };     // 값들을 보고 컴파일러가 알아서 std::array&lt;double, 2&gt; 라고 추론해요

    return 0;
}
</code></pre>
<p>이 방식이 코드를 훨씬 깔끔하게 만들어주기 때문에 사용할 수 있다면 아주 적극적으로 추천합니다. (단, 오래된 컴파일러를 쓴다면 데이터 타입과 길이를 예전처럼 직접 다 적어주셔야 해요.)</p>
<blockquote>
<p><strong>권장 사항</strong>
컴파일러가 알아서 타입과 길이를 눈치채도록 CTAD 기능을 적극 활용하세요.</p>
</blockquote>
<p>하지만 주의할 점! C++23 기준으로, 타입과 길이 중 하나만 쏙 빼고 하나만 적어주는 건 불가능해요. 둘 다 생략해서 컴파일러에게 전적으로 맡기거나, 아니면 둘 다 꼼꼼히 적어줘야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    constexpr std::array&lt;int&gt; a2 { 9, 7, 5, 3, 1 };     // 에러: 정보가 부족해요 (길이를 안 적음)
    constexpr std::array&lt;5&gt; a2 { 9, 7, 5, 3, 1 };       // 에러: 정보가 부족해요 (타입을 안 적음)

    return 0;
}
</code></pre>
<hr>
<h3 id="stdto_array-를-써서-길이만-생략하기---c20-이상"><code>std::to_array</code> 를 써서 길이만 생략하기 - C++20 이상</h3>
<p>&quot;타입은 내가 정하고 싶은데, 길이는 일일이 세기 귀찮아!&quot; 할 때가 있죠? C++20부터는 <code>std::to_array</code> 라는 도우미 함수를 쓰면 길이만 생략하는 꼼수를 쓸 수 있어요.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    constexpr auto myArray1 { std::to_array&lt;int, 5&gt;({ 9, 7, 5, 3, 1 }) }; // 타입과 크기를 모두 지정해요
    constexpr auto myArray2 { std::to_array&lt;int&gt;({ 9, 7, 5, 3, 1 }) };    // 타입만 지정하고, 크기는 알아서 추론하게 해요
    constexpr auto myArray3 { std::to_array({ 9, 7, 5, 3, 1 }) };         // 타입과 크기를 모두 알아서 추론하게 해요

    return 0;
}
</code></pre>
<p>하지만 이 기능은 너무 남발하면 안 돼요. <code>std::to_array</code> 는 임시 배열을 하나 더 만들었다가 옮겨 담는 과정을 거치기 때문에, 그냥 <code>std::array</code> 를 만드는 것보다 컴퓨터가 일을 조금 더 무겁게 해야 하거든요.
반복문 안에서 계속 만들어내야 하거나 속도가 중요한 곳에서는 피하는 게 좋습니다. 
꼭 필요한 상황에서만 가끔 쓰는 걸 추천해요.</p>
<p>예를 들면, C++에서는 숫자 뒤에 꼬리표를 붙여서 <code>short</code> 타입이라고 명시하는 방법이 없어요. 이럴 때 일일이 길이를 세서 적지 않고 <code>short</code> 배열을 만들고 싶다면 아주 유용하겠죠.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    constexpr auto shortArray { std::to_array&lt;short&gt;({ 9, 7, 5, 3, 1 }) };
    std::cout &lt;&lt; sizeof(shortArray[0]) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<hr>
<h3 id="operator-로-배열-안의-내용-꺼내-보기"><code>operator[]</code> 로 배열 안의 내용 꺼내 보기</h3>
<p><code>std::vector</code> 와 마찬가지로, <code>std::array</code> 에서도 대괄호 <code>[]</code> 기호를 사용하는 게 안의 데이터를 꺼내보는 가장 흔하고 친숙한 방법이에요:</p>
<pre><code class="language-cpp">#include &lt;array&gt; // std::array를 쓰기 위해 필요해요
#include &lt;iostream&gt;

int main()
{
    constexpr std::array&lt;int, 5&gt; prime{ 2, 3, 5, 7, 11 };

    std::cout &lt;&lt; prime[3]; // 인덱스 번호 3번 자리에 있는 값(7)을 화면에 출력해요
    std::cout &lt;&lt; prime[9]; // 잘못된 인덱스 번호예요! (미정의 동작 발생)

    return 0;
}
</code></pre>
<p>여기서 꼭 기억해야 할 아주 중요한 점이 있어요! <strong><code>[]</code> 기호는 우리가 배열 범위를 벗어났는지 절대 확인해주지 않는다는 거예요.</strong> 위 코드처럼 5칸짜리 배열인데 9번 자리를 달라고 요구하면, 프로그램이 죽거나 이상한 쓰레기값을 가져오는 끔찍한 일(미정의 동작)이 벌어집니다. 항상 내가 가진 배열 크기 안에서만 번호를 부르도록 조심해야 합니다!</p>
<hr>
<h2 id="172--stdarray-길이와-인덱싱">17.2 — std::array 길이와 인덱싱</h2>
<p>우리는 앞서 <code>std::vector</code>를 배우며 표준 라이브러리 컨테이너들이 길이나 위치를 나타낼 때 &#39;부호 없는 정수(unsigned)&#39;를 사용한다는 사실을 알게 되었습니다. 안타깝게도 <strong><code>std::array</code> 역시 같은 표준 컨테이너 가족이기 때문에, 부호 있는 정수(int)와 함께 사용할 때 발생하는 데이터 타입 불일치 문제를 그대로 안고 있습니다.</strong></p>
<p>이번 레슨에서는 <code>std::array</code>의 길이를 확인하고 특정 위치의 값을 찾는(인덱싱) 방법들을 정리해 보려 합니다. 전체적인 사용법은 <code>std::vector</code>와 매우 비슷해서 익숙한 느낌이 드실 텐데, 여기서 주목해야 할 <strong><code>std::array</code>만의 독보적인 장점은 바로 <code>constexpr</code>을 완벽하게 지원한다는 점</strong>입니다.</p>
<p><code>std::array</code>는 프로그램이 실행되기 전인 컴파일 단계에서 이미 모든 크기와 값이 고정되는 &#39;컴파일 타임 상수&#39;로 다룰 수 있습니다. 여기서 우리가 기억해야 할 중요한 규칙이 하나 등장합니다. 원래 부호가 있는 값을 부호 없는 값으로 변환할 때는 데이터가 잘려 나갈 위험 때문에 컴파일러가 엄격하게 굴지만, <strong>그 값이 <code>constexpr</code>일 때는 예외적으로 너그러워집니다.</strong></p>
<p>즉, 컴파일러가 미리 계산해 보고 &quot;이 값은 데이터 손실 위험이 없는 안전한 양수다&quot;라고 확신할 수 있다면, 우리가 복잡한 형변환을 거치지 않아도 훨씬 유연하게 코드를 작성할 수 있게 해줍니다. 이 특성을 잘 활용하면 <code>std::array</code>를 다룰 때 발생하는 번거로운 인덱스 문제들을 훨씬 스마트하게 해결할 수 있습니다.</p>
<hr>
<h3 id="stdarray의-길이는-stdsize_t-타입이에요">std::array의 길이는 std::size_t 타입이에요</h3>
<p><code>std::array</code> 는 템플릿 구조체라는 형태로 만들어져 있고, 대략 이렇게 생겼습니다:</p>
<pre><code class="language-cpp">template&lt;typename T, std::size_t N&gt; // N은 타입이 아닌(non-type) 템플릿 매개변수입니다
struct array;
</code></pre>
<p>보시다시피, 배열의 길이(<code>N</code>)를 나타내는 매개변수는 <code>std::size_t</code> 타입을 사용해요. 
이제는 잘 아시겠지만, <code>std::size_t</code> 는 아주 커다란 &#39;부호 없는 정수(unsigned)&#39; 타입이랍니다.</p>
<p>따라서 우리가 <code>std::array</code> 를 만들 때, 길이를 나타내는 값은 반드시 <code>std::size_t</code> 타입이거나 <code>std::size_t</code> 로 변신할 수 있는 값이어야 해요. 다행히 이 길이는 무조건 <strong>constexpr</strong> (컴파일 시점에 미리 확정되는 상수)이어야만 합니다. 그래서 우리가 평범한 정수(음수/양수 모두 되는 signed)를 적어 넣더라도, 컴파일러가 알아서 아무런 에러 없이 컴파일 시점에 <code>std::size_t</code> 로 안전하게 둔갑시켜 줍니다. 데이터 손실 경고 같은 건 걱정 안 하셔도 돼요!</p>
<blockquote>
<p><strong>참고로 알아두면 좋은 팁...</strong>
C++23 이전에는 C++에 <code>std::size_t</code> 를 콕 집어 표현하는 숫자 꼬리표(리터럴 접미사)가 아예 없었어요. 보통은 일반 정수(<code>int</code>)를 써도 컴파일러가 알아서 <code>std::size_t</code> 로 잘 바꿔주기 때문에 굳이 필요가 없었거든요.
이 꼬리표는 주로 타입을 자동으로 추론할 때 쓰라고 추가된 거예요. 예를 들어 <code>constexpr auto x { 0 }</code> 이라고 쓰면 컴퓨터는 이걸 <code>std::size_t</code> 가 아니라 평범한 <code>int</code> 로 생각해 버려요. 이럴 때 굳이 길고 복잡하게 <code>static_cast</code> 를 쓰지 않고도, 그냥 <code>0</code> (int) 과 <code>0UZ</code> (std::size_t) 를 예쁘게 구분해서 쓸 수 있게 해주는 아주 유용한 기능이랍니다.</p>
</blockquote>
<hr>
<h3 id="stdarray의-길이와-인덱스는-size_type-인데-이건-언제나-stdsize_t-예요">std::array의 길이와 인덱스는 size_type 인데, 이건 언제나 std::size_t 예요</h3>
<p><code>std::vector</code> 와 마찬가지로, <code>std::array</code> 안에는 <strong>size_type</strong> 이라는 별명이 숨어 있어요. 이 별명은 배열의 길이나 위치(인덱스)를 나타낼 때 쓰이는 타입을 부르는 말이에요. <code>std::array</code> 의 경우, 이 <strong>size_type</strong> 은 <strong>언제나</strong> <code>std::size_t</code> 를 가리키는 별명입니다.</p>
<p>눈여겨볼 점은, <code>std::array</code> 의 길이를 정의하는 템플릿 부분에는 <strong>size_type</strong> 이 아니라 <code>std::size_t</code> 라고 대놓고 적혀 있다는 거예요. 왜냐하면 <strong>size_type</strong> 은 <code>std::array</code> 가 만들어지고 나서야 그 안에 생기는 별명인데, 템플릿을 정의하는 저 시점에서는 아직 그 별명이 존재하지 않거든요. 딱 저곳에서만 <code>std::size_t</code> 를 직접 쓰고, 나머지 모든 곳에서는 <strong>size_type</strong> 을 사용한답니다.</p>
<hr>
<h3 id="stdarray의-길이-구하기">std::array의 길이 구하기</h3>
<p><code>std::array</code> 의 길이를 알아내는 데는 보통 3가지 흔한 방법이 쓰여요.</p>
<p>첫 번째, <code>std::array</code> 객체에게 직접 <code>size()</code> 함수를 써서 길이를 물어보는 방법이에요. 
(부호 없는 <strong>size_type</strong> 으로 길이를 알려줍니다.)</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    constexpr std::array arr { 9.0, 7.2, 5.4, 3.6, 1.8 };
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; arr.size() &lt;&lt; &#39;\n&#39;; // 길이를 size_type 타입 (std::size_t의 별명) 으로 반환합니다

    return 0;
}
</code></pre>
<p>출력 결과는 다음과 같아요:
<code>length: 5</code></p>
<p>참고로, 글자 길이를 잴 때 <code>length()</code> 와 <code>size()</code> 라는 똑같은 기능의 함수를 두 개나 가지고 있는 문자열(<code>std::string</code>) 친구들과는 달리, <code>std::array</code> 
(그리고 대부분의 다른 C++ 컨테이너들)는 오직 <code>size()</code> 하나만 가지고 있답니다.</p>
<p>두 번째, C++17부터는 컨테이너 밖에서 쓰는 <code>std::size()</code> 라는 일반 함수를 쓸 수 있어요. (이것도 결국 내부적으로는 첫 번째 방법을 호출해서 부호 없는 <strong>size_type</strong> 으로 돌려줍니다.)</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    constexpr std::array arr{ 9, 7, 5, 3, 1 };
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; std::size(arr); // C++17 지원, 길이를 size_type 타입 (std::size_t의 별명) 으로 반환합니다

    return 0;
}
</code></pre>
<p>마지막으로 세 번째! C++20부터는 <code>std::ssize()</code> 라는 일반 함수를 쓸 수 있게 되었어요. 이 녀석은 길이를 &#39;부호가 있는(signed) 아주 큰 정수 타입&#39;(보통 <code>std::ptrdiff_t</code>)으로 알려주는 기특한 함수입니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    constexpr std::array arr { 9, 7, 5, 3, 1 };
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; std::ssize(arr); // C++20 지원, 길이를 부호가 있는(signed) 큰 정수 타입으로 반환합니다

    return 0;
}
</code></pre>
<p>세 가지 방법 중에서 길이를 &#39;부호가 있는 정수(음수 표현 가능)&#39;로 돌려주는 함수는 이 녀석이 유일해요!</p>
<hr>
<h3 id="constexpr-값으로-stdarray-길이-구하기">constexpr 값으로 std::array 길이 구하기</h3>
<p><code>std::array</code> 의 길이는 프로그램이 실행되기 전에 이미 고정되는 <strong>constexpr</strong> 이기 때문에, 방금 배운 세 가지 함수를 쓰면 언제나 길이를 <strong>constexpr</strong> 값으로 얻어낼 수 있어요! 
(심지어 배열 자체를 <strong>constexpr</strong> 로 만들지 않았어도 가능하답니다!)
이 말은 즉, 이 함수들을 고정된 값이 필요한 곳(상수 표현식)에 마음껏 써도 된다는 뜻이고, 여기서 얻은 길이를 평범한 <code>int</code> 에 집어넣어도 데이터 손실 경고가 뜨지 않는다는 놀라운 장점이 있어요.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    std::array arr { 9, 7, 5, 3, 1 }; // 참고: 이 예제에서는 constexpr이 아닙니다
    constexpr int length{ std::size(arr) }; // 성공: 반환값이 constexpr std::size_t 이며, int로 변환되어도 데이터 손실(narrowing conversion)이 아닙니다

    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; length &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<blockquote>
<p><strong>Visual Studio 사용자 분들을 위한 안내</strong>
Visual Studio에서는 위 코드를 작성하면 C4365라는 경고를 잘못 띄우는 버그가 있어요. 이 문제는 이미 마이크로소프트에 보고되어 있으니 무시하셔도 괜찮습니다.</p>
</blockquote>
<blockquote>
<p><strong>주의하세요! (경고)</strong>
C++ 언어 자체의 작은 결함 때문에, <code>std::array</code> 를 함수의 매개변수로 넘길 때 참조(const reference) 방식을 사용하면 안타깝게도 위 함수들이 <strong>constexpr</strong> 값을 돌려주지 못하고 에러를 냅니다.</p>
<pre><code class="language-cpp">
void printLength(const std::array&lt;int, 5&gt; &amp;arr)
{
constexpr int length{ std::size(arr) }; // 컴파일 에러!
std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; length &lt;&lt; &#39;\n&#39;;
}

int main()
{
std::array arr { 9, 7, 5, 3, 1 };
constexpr int length{ std::size(arr) }; // 아주 잘 작동합니다
std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; length &lt;&lt; &#39;\n&#39;;

printLength(arr);

return 0;
}</code></pre>
<p>이 성가신 결함은 C++23에서 고쳐졌지만, 아직 이 최신 기능을 완벽하게 지원하는 컴파일러가 많지 않아요.</p>
<p>지금 당장 쓸 수 있는 해결책은 함수 자체를 &#39;템플릿&#39;으로 만들어서, 배열의 길이를 매개변수로 직접 넘겨받는 거랍니다. (이 방법은 다음 17.3 레슨에서 자세히 다룰 테니 가벼운 마음으로 넘어가세요!)</p>
<pre><code class="language-cpp">template `&lt;auto lenth&gt;`
void printLength(const std::array&lt;int, Length&gt; &amp;arr)
{
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; Length &lt;&lt; &#39;\n&#39;;
}
</code></pre>
</blockquote>
<hr>
<h3 id="operator-나-at-으로-stdarray-안의-값-꺼내기">operator[] 나 at() 으로 std::array 안의 값 꺼내기</h3>
<p>이전 17.1 레슨에서, <code>std::array</code> 안의 값을 꺼낼 때 가장 많이 쓰는 방법은 대괄호 <code>[]</code> (첨자 연산자)를 쓰는 거라고 배웠죠. 이 방법은 엄청나게 빠르지만, 우리가 배열 크기를 벗어난 엉뚱한 번호를 넣었을 때 컴퓨터가 막아주지 않아요(범위 검사를 안 함). 만약 없는 번호를 부르면 프로그램이 미쳐 날뛰게 될 겁니다(Undefined behavior).</p>
<p>그래서 <code>std::vector</code> 처럼 <code>std::array</code> 에도 <code>at()</code> 이라는 안전장치 멤버 함수가 있어요. 
이 함수는 프로그램이 실행되는 도중에 우리가 제대로 된 번호를 불렀는지 안전하게 검사해 줍니다. 하지만 전문가들은 이 함수 사용을 별로 추천하지 않아요. 보통은 값을 꺼내기 &#39;전에&#39; 미리 번호가 맞는지 우리가 직접 확인하거나, 아예 프로그램이 실행되기도 전인 컴파일 시점에 검사하는 것을 훨씬 선호하거든요.</p>
<p>참고로 <code>[]</code> 나 <code>at()</code> 모두 우리가 부르는 번호(인덱스)가 부호 없는 <strong>size_type</strong> (<code>std::size_t</code>) 타입일 거라고 기대하고 있어요.
만약 우리가 넣는 번호가 <strong>constexpr</strong> 값이라면, 컴파일러가 알아서 에러 없이 예쁘게 <code>std::size_t</code> 로 변환해 줍니다. 데이터 손실 같은 건 없으니 부호 문제로 머리 아플 일은 없어요.</p>
<p>하지만, 만약 우리가 넣는 번호가 <strong>constexpr</strong> 이 아닌 일반적인 정수라면 이야기가 달라집니다. 이때는 <code>std::size_t</code> 로 변환될 때 데이터 손실 위험이 있다고 판단해서 컴파일러가 짜증 섞인 경고를 뱉어낼 수도 있어요. (이 상황에 대해서는 16.3 레슨에서 <code>std::vector</code> 를 예로 들어 아주 자세히 다뤘었죠!)</p>
<hr>
<h3 id="stdget-은-constexpr-번호를-부를-때-컴파일-시점에-안전-검사를-해줘요">std::get() 은 constexpr 번호를 부를 때 컴파일 시점에 안전 검사를 해줘요!</h3>
<p><code>std::array</code> 의 길이는 프로그램 실행 전(컴파일 시점)에 이미 정해진 <strong>constexpr</strong> 이라고 거듭 강조했죠? 그렇다면, 우리가 꺼내려는 번호(인덱스)도 마침 <strong>constexpr</strong> 이라면, 컴파일러가 코드를 번역할 때 &quot;어라? 이 번호가 배열 크기 안에 잘 들어맞나?&quot; 하고 미리 검사해 줄 수 있을 거예요! (만약 범위를 벗어난 번호를 불렀다면, 아예 컴파일을 멈춰서 에러를 내주면 가장 완벽하겠죠.)</p>
<p>하지만 앞서 말했듯 <code>[]</code> 는 애초에 그런 검사를 안 하고, <code>at()</code> 은 프로그램이 <strong>실행될 때</strong> 야 비로소 검사를 합니다. 함수의 매개변수들은 애초에 <strong>constexpr</strong> 로 넘길 수도 없고요. 그럼 도대체 컴파일 시점에 미리 번호 검사를 받으려면 어떻게 해야 할까요?</p>
<p>정답은 바로 <code>std::get()</code> 이라는 특별한 함수 템플릿을 쓰는 겁니다! 
이 녀석은 꺼내려는 번호를 꺾쇠괄호(<code>&lt; &gt;</code>) 안에 템플릿 인자로 넣어서 사용해요:</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    constexpr std::array prime{ 2, 3, 5, 7, 11 };

    std::cout &lt;&lt; std::get&lt;3&gt;(prime); // 인덱스 3에 있는 요소의 값을 출력합니다
    std::cout &lt;&lt; std::get&lt;9&gt;(prime); // 유효하지 않은 인덱스 (컴파일 에러)

    return 0;
}
</code></pre>
<p><code>std::get()</code> 의 코드를 살짝 들여다보면, 내부적으로 <code>static_assert</code> 라는 아주 엄격한 검사관이 들어있어요. 이 검사관은 우리가 꺾쇠괄호 안에 적은 번호가 배열의 전체 길이보다 작은지 확인하고, 만약 벗어났다면 가차 없이 컴파일을 중단시키고 에러를 뿜어냅니다.</p>
<p>꺾쇠괄호 안에 들어가는 템플릿 인자는 무조건 <strong>constexpr</strong> 이어야만 하기 때문에, 이 <code>std::get()</code> 은 우리가 부르려는 번호가 상수(constexpr)일 때만 사용할 수 있다는 점을 꼭 기억해 두세요!</p>
<hr>
<h2 id="173--stdarray-전달하고-반환하기">17.3 — <code>std::array</code> 전달하고 반환하기</h2>
<p><code>std::array</code> 타입의 객체도 일반적인 다른 데이터들처럼 함수에 쏙 집어넣을 수 있습니다. 
만약 데이터를 통째로 복사해서 넘겨주는 &#39;값에 의한 전달(pass by value)&#39; 방식을 쓰면, 컴퓨터가 배열 전체를 복사하느라 힘을 빼게 됩니다(성능이 떨어집니다). 그래서 복사하는 과정을 피하기 위해 보통 <strong>상수 참조(const reference)</strong> 방식을 사용해서 원본을 가리키기만 하는 형태로 함수에 전달합니다.</p>
<p><code>std::array</code> 를 사용할 때는 <strong>배열의 길이</strong> 와 <strong>안에 들어가는 데이터의 타입</strong> 이 모두 배열의 &#39;신분증&#39; 같은 역할을 합니다. 따라서 함수에서 이 배열을 넘겨받을 때는, 데이터 타입과 길이를 꼭 명확하게 적어주어야 합니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

void passByRef(const std::array&lt;int, 5&gt;&amp; arr) // 여기에 &lt;int, 5&gt;를 명확하게 적어주어야 합니다
{
    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // CTAD(클래스 템플릿 인수 추론)가 알아서 std::array&lt;int, 5&gt; 타입을 알아냅니다
    passByRef(arr);

    return 0;
}
</code></pre>
<p>안타깝게도 CTAD(컴파일러가 코드를 보고 알아서 타입을 눈치껏 맞춰주는 편리한 기능)는 함수의 매개변수에서는 아직 작동하지 않습니다. 그래서 함수 괄호 안에 그냥 <code>std::array</code> 라고만 적어두고 컴파일러에게 알아서 맞춰달라고 할 수는 없습니다.</p>
<hr>
<h3 id="함수-템플릿을-써서-다양한-타입과-길이의-stdarray-전달하기">함수 템플릿을 써서 다양한 타입과 길이의 <code>std::array</code> 전달하기</h3>
<p>만약 들어가는 데이터가 <code>int</code> 든 <code>double</code> 이든 상관없이, 그리고 길이가 5든 100이든 상관없이 모두 받아줄 수 있는 &#39;만능 함수&#39;를 만들고 싶다면 어떻게 해야 할까요? 
바로 <strong>함수 템플릿</strong> 을 만들면 됩니다. 데이터 타입과 길이를 들어갈 수 있게 빈칸(매개변수)으로 뚫어놓으면, C++ 컴파일러가 알아서 실제 쓰일 때 그 빈칸을 채워 진짜 함수들을 만들어냅니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
함수 템플릿에 대한 자세한 내용은 &#39;11.6 -- 함수 템플릿&#39; 강의에서 다룹니다.</p>
</blockquote>
<p><code>std::array</code> 는 원래 이렇게 생겼습니다:</p>
<pre><code class="language-cpp">template&lt;typename T, std::size_t N&gt; // N은 타입이 아닌 템플릿 매개변수(숫자 값 등)입니다
struct array;
</code></pre>
<p>우리는 이 모양을 그대로 흉내 내서 함수 템플릿을 만들 수 있습니다:</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;typename T, std::size_t N&gt; // 참고: 이 템플릿 매개변수 선언은 std::array의 선언과 모양이 똑같습니다
void passByRef(const std::array&lt;T, N&gt;&amp; arr)
{
    static_assert(N != 0); // 길이가 0인 std::array가 들어오면 실패(오류) 처리합니다

    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // CTAD를 이용해 std::array&lt;int, 5&gt;로 유추합니다
    passByRef(arr);  // 성공: 컴파일러가 passByRef(const std::array&lt;int, 5&gt;&amp; arr) 함수를 만들어냅니다

    std::array arr2{ 1, 2, 3, 4, 5, 6 }; // CTAD를 이용해 std::array&lt;int, 6&gt;으로 유추합니다
    passByRef(arr2); // 성공: 컴파일러가 passByRef(const std::array&lt;int, 6&gt;&amp; arr) 함수를 만들어냅니다

    std::array arr3{ 1.2, 3.4, 5.6, 7.8, 9.9 }; // CTAD를 이용해 std::array&lt;double, 5&gt;로 유추합니다
    passByRef(arr3); // 성공: 컴파일러가 passByRef(const std::array&lt;double, 5&gt;&amp; arr) 함수를 만들어냅니다

    return 0;
}
</code></pre>
<p>위 예제에서 우리는 <code>passByRef()</code> 라는 함수 템플릿을 딱 하나만 만들었습니다. 
여기서 <code>T</code> 와 <code>N</code> 은 첫째 줄의 <code>template &lt;typename T, std::size_t N&gt;</code> 에서 정의되었습니다. <code>T</code> 는 데이터의 타입을 결정할 수 있게 해주고, <code>N</code> 은 배열의 길이를 결정할 수 있게 해주는 특별한 매개변수입니다.</p>
<blockquote>
<p><strong>주의!</strong>
<code>std::array</code> 의 길이를 나타내는 템플릿 매개변수(<code>N</code>)의 타입은 <code>int</code> 가 아니라 반드시 <strong><code>std::size_t</code></strong> 여야 합니다! 템플릿은 자동으로 타입을 변환해주지 않기 때문에 <code>int</code> 를 쓰면 짝이 맞지 않아 컴파일러가 에러를 뿜어냅니다.</p>
</blockquote>
<p>그래서 우리가 <code>main()</code> 에서 <code>passByRef(arr)</code> 를 부르면, 컴파일러가 알아서 <code>void passByRef(const std::array&lt;int, 5&gt;&amp; arr)</code> 라는 맞춤형 함수를 찍어내서 실행합니다. <code>arr2</code> 와 <code>arr3</code> 에 대해서도 똑같이 알아서 만들어줍니다.
결론적으로, 단 하나의 만능 함수 템플릿으로 어떤 타입, 어떤 길이의 <code>std::array</code> 도 모두 처리할 수 있게 된 것입니다!</p>
<p>원한다면 둘 중 하나만 빈칸(템플릿)으로 뚫어놓을 수도 있습니다. 아래 예제에서는 배열의 길이는 아무거나 들어올 수 있게 템플릿으로 만들었지만, 데이터 타입은 무조건 <code>int</code> 만 받도록 고정했습니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;std::size_t N&gt; // 참고: 여기서는 길이만 템플릿으로 만들었습니다
void passByRef(const std::array&lt;int, N&gt;&amp; arr) // 데이터 타입을 int로 고정해두었습니다
{
    static_assert(N != 0); // 길이가 0인 std::array가 들어오면 실패(오류) 처리합니다

    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // CTAD를 이용해 std::array&lt;int, 5&gt;로 유추합니다
    passByRef(arr);  // 성공: 컴파일러가 passByRef(const std::array&lt;int, 5&gt;&amp; arr) 함수를 만들어냅니다

    std::array arr2{ 1, 2, 3, 4, 5, 6 }; // CTAD를 이용해 std::array&lt;int, 6&gt;으로 유추합니다
    passByRef(arr2); // 성공: 컴파일러가 passByRef(const std::array&lt;int, 6&gt;&amp; arr) 함수를 만들어냅니다

    std::array arr3{ 1.2, 3.4, 5.6, 7.8, 9.9 }; // CTAD를 이용해 std::array&lt;double, 5&gt;로 유추합니다
    passByRef(arr3); // 오류: 컴파일러가 일치하는 함수를 찾을 수 없습니다 (double 타입이라서 튕겨냅니다)

    return 0;
}
</code></pre>
<hr>
<h3 id="c20-기능-auto-를-이용한-비타입-템플릿-매개변수">C++20 기능: <code>auto</code> 를 이용한 비타입 템플릿 매개변수</h3>
<p>템플릿을 만들 때마다 <code>std::size_t</code> 처럼 길이를 나타내는 정확한 타입을 외워서 적는 건 꽤나 귀찮은 일입니다.
C++20부터는 <strong><code>auto</code></strong> 키워드를 쓰면, 컴파일러가 알아서 눈치껏 타입을 맞춰주기 때문에 훨씬 편해졌습니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;typename T, auto N&gt; // 이제 auto를 사용해 N의 타입을 알아서 유추합니다
void passByRef(const std::array&lt;T, N&gt;&amp; arr)
{
    static_assert(N != 0); // 길이가 0인 std::array가 들어오면 실패(오류) 처리합니다

    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 }; // CTAD를 이용해 std::array&lt;int, 5&gt;로 유추합니다
    passByRef(arr);  // 성공: 컴파일러가 passByRef(const std::array&lt;int, 5&gt;&amp; arr) 함수를 만들어냅니다

    std::array arr2{ 1, 2, 3, 4, 5, 6 }; // CTAD를 이용해 std::array&lt;int, 6&gt;으로 유추합니다
    passByRef(arr2); // 성공: 컴파일러가 passByRef(const std::array&lt;int, 6&gt;&amp; arr) 함수를 만들어냅니다

    std::array arr3{ 1.2, 3.4, 5.6, 7.8, 9.9 }; // CTAD를 이용해 std::array&lt;double, 5&gt;로 유추합니다
    passByRef(arr3); // 성공: 컴파일러가 passByRef(const std::array&lt;double, 5&gt;&amp; arr) 함수를 만들어냅니다

    return 0;
}
</code></pre>
<p>여러분의 컴파일러가 C++20을 지원한다면, 아주 유용하게 쓸 수 있는 방법입니다.</p>
<hr>
<h3 id="배열-길이에-대한-컴파일-타임-검사-static-assert">배열 길이에 대한 컴파일 타임 검사 (Static assert)</h3>
<p>위에서 본 것과 비슷한 아래의 함수를 한번 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;typename T, std::size_t N&gt;
void printElement3(const std::array&lt;T, N&gt;&amp; arr)
{
    std::cout &lt;&lt; arr[3] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 };
    printElement3(arr);

    return 0;
}
</code></pre>
<p>이 코드는 지금 당장은 아무 문제 없이 잘 돌아가지만, 초보 프로그래머가 실수하기 딱 좋은 엄청난 버그 폭탄을 숨기고 있습니다. 찾으셨나요?</p>
<p>위 프로그램은 배열의 3번 인덱스(네 번째 값)를 화면에 보여주는 역할을 합니다. 배열 안에 값이 4개 이상 들어있다면 문제가 없겠죠. 하지만 배열 길이가 2밖에 안 되는데 3번 인덱스를 찾으려고 하면, 컴파일러는 아무 경고 없이 그냥 넘어가 버립니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;typename T, std::size_t N&gt;
void printElement3(const std::array&lt;T, N&gt;&amp; arr)
{
    std::cout &lt;&lt; arr[3] &lt;&lt; &#39;\n&#39;; // 유효하지 않은 인덱스입니다 (에러 발생 위치)
}

int main()
{
    std::array arr{ 9, 7 }; // 2개의 요소만 있는 배열입니다 (유효한 인덱스는 0과 1뿐입니다)
    printElement3(arr);

    return 0;
}
</code></pre>
<p>이러면 프로그램이 알 수 없는 이상한 행동(정의되지 않은 동작)을 하게 됩니다. 이런 실수를 했을 때 컴파일러가 &quot;너 잘못 짰어!&quot; 하고 미리 알려주면 얼마나 좋을까요?</p>
<p>일반 함수의 매개변수와 달리, 템플릿 매개변수는 <strong>컴파일 타임 상수</strong> (프로그램을 실행하기도 전에 이미 그 값이 확정됨)라는 엄청난 장점이 있습니다. 이 장점을 활용하면 문제를 미리 예방할 수 있습니다.</p>
<p>첫 번째 해결책은 <code>operator[]</code> (대괄호) 대신 <strong><code>std::get()</code></strong> 을 쓰는 것입니다. 대괄호는 길이를 넘었는지 검사하지 않지만, <code>std::get()</code> 은 프로그램을 실행하기도 전(컴파일할 때)에 길이를 검사해 줍니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;typename T, std::size_t N&gt;
void printElement3(const std::array&lt;T, N&gt;&amp; arr)
{
    std::cout &lt;&lt; std::get&lt;3&gt;(arr) &lt;&lt; &#39;\n&#39;; // 컴파일 시점에 인덱스 3이 유효한지 안전하게 검사합니다
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 };
    printElement3(arr); // 성공

    std::array arr2{ 9, 7 };
    printElement3(arr2); // 컴파일 오류가 발생하여 실수를 미리 막아줍니다

    return 0;
}
</code></pre>
<p>컴파일러가 <code>printElement3(arr2)</code> 코드를 확인하면 길이가 2인 배열 전용 함수를 만드는데, 그 안에서 <code>std::get&lt;3&gt;(arr)</code> 을 발견하고는 &quot;어? 길이 2짜리 배열에서 3번 인덱스를 달라고? 안 돼!&quot;라며 에러를 띄워줍니다.</p>
<p>두 번째 해결책은 우리가 직접 <strong><code>static_assert</code></strong> 라는 기능을 이용해 &quot;배열 길이는 무조건 3보다 커야 해!&quot;라고 단단히 못을 박아두는 것입니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;typename T, std::size_t N&gt;
void printElement3(const std::array&lt;T, N&gt;&amp; arr)
{
    // 전제 조건: 3번 요소가 존재하려면 배열 길이가 3보다 커야 합니다
    static_assert (N &gt; 3);

    // 이 지점부터는 배열 길이가 무조건 3보다 크다고 확신하고 안심할 수 있습니다

    std::cout &lt;&lt; arr[3] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::array arr{ 9, 7, 5, 3, 1 };
    printElement3(arr); // 성공

    std::array arr2{ 9, 7 };
    printElement3(arr2); // 컴파일 오류가 발생합니다

    return 0;
}
</code></pre>
<p>마찬가지로 배열 길이가 2인 <code>arr2</code> 가 들어오면, 컴퓨터가 <code>static_assert (2 &gt; 3)</code> 라는 수학적으로 말이 안 되는 수식을 보고 에러를 뿜어내어 우리의 실수를 막아줍니다.</p>
<hr>
<h3 id="stdarray-반환하기-함수에서-밖으로-내보내기"><code>std::array</code> 반환하기 (함수에서 밖으로 내보내기)</h3>
<p><code>std::array</code> 를 함수에 집어넣는 건 참조 방식을 쓰면 되니까 아주 쉬웠습니다. 
그런데 함수에서 결과물로 <code>std::array</code> 를 밖으로 뱉어낼(반환할) 때는 조금 복잡해집니다. <code>std::vector</code> 와 달리 <code>std::array</code> 는 가볍게 &#39;이동(move)&#39;시키는 게 불가능해서, 배열 전체를 하나하나 복사해서 반환해야 하거든요.</p>
<p>상황에 따라 주로 두 가지 방법을 씁니다.</p>
<p><strong>1. 값으로 반환하기 (Return by value)</strong>
다음 조건이 모두 맞다면 통째로 복사해서 반환해도 괜찮습니다.</p>
<ul>
<li>배열 크기가 작다.</li>
<li>안에 든 데이터가 가벼워서 복사해도 부담이 없다.</li>
<li>0.001초가 중요한 엄청난 고성능 프로그램이 아니다.</li>
</ul>
<p>복사를 하느라 컴퓨터가 살짝 힘을 쓰긴 하겠지만, 코드가 가장 자연스럽고 깔끔해집니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;limits&gt;

// 값으로 반환하는 함수 템플릿
template &lt;typename T, std::size_t N&gt;
std::array&lt;T, N&gt; inputArray() // 값으로 반환하기
{
    std::array&lt;T, N&gt; arr{};
    std::size_t index { 0 };
    while (index &lt; N)
    {
        std::cout &lt;&lt; &quot;Enter value #&quot; &lt;&lt; index &lt;&lt; &quot;: &quot;;
        std::cin &gt;&gt; arr[index];

        if (!std::cin) // 잘못된 입력 처리하기
        {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits&lt;std::streamsize&gt;::max(), &#39;\n&#39;);
            continue;
        }
        ++index;
    }

    return arr;
}

int main()
{
    std::array&lt;int, 5&gt; arr { inputArray&lt;int, 5&gt;() };

    std::cout &lt;&lt; &quot;The value of element 2 is &quot; &lt;&lt; arr[2] &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<ul>
<li><strong>장점:</strong> 초보자가 보기에도 코드가 가장 자연스럽습니다. 함수가 어떤 값을 뱉어낸다는 게 눈에 확 띄고, 함수를 실행함과 동시에 빈 배열에 결과값을 쏙 넣을 수 있습니다.</li>
<li><strong>단점:</strong> 배열 전체를 복사해야 해서 무겁습니다. 그리고 템플릿이 자동으로 유추해줄 단서가 없기 때문에 <code>&lt;int, 5&gt;</code> 라고 직접 다 적어줘야 합니다.</li>
</ul>
<p><strong>2. 출력 매개변수(Out parameter)로 반환하기</strong>
복사하는 게 너무 무겁고 아깝다면 이 방법을 씁니다. 함수를 부르는 쪽에서 빈 배열을 미리 준비해서 함수에 던져주면(참조 방식), 함수가 그 빈 배열의 속을 꽉꽉 채워주는 방식입니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;limits&gt;
#include &lt;iostream&gt;

template &lt;typename T, std::size_t N&gt;
void inputArray(std::array&lt;T, N&gt;&amp; arr) // 상수가 아닌 참조로 전달하기 (원본을 수정할 수 있도록 넘김)
{
    std::size_t index { 0 };
    while (index &lt; N)
    {
        std::cout &lt;&lt; &quot;Enter value #&quot; &lt;&lt; index &lt;&lt; &quot;: &quot;;
        std::cin &gt;&gt; arr[index];

        if (!std::cin) // 잘못된 입력 처리하기
        {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits&lt;std::streamsize&gt;::max(), &#39;\n&#39;);
            continue;
        }
        ++index;
    }
}

int main()
{
    std::array&lt;int, 5&gt; arr {};
    inputArray(arr); // 함수가 arr의 속을 채워줍니다

    std::cout &lt;&lt; &quot;The value of element 2 is &quot; &lt;&lt; arr[2] &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<ul>
<li><strong>장점:</strong> 복사가 전혀 일어나지 않아서 아주 빠르고 효율적입니다.</li>
<li><strong>단점:</strong> 함수가 값을 반환하는 대신 매개변수를 슬쩍 수정하는 거라, 코드를 처음 보는 사람은 이해하기 어려울 수 있습니다. 또, 배열을 만들면서 바로 채울 수 없고 무조건 빈 배열을 먼저 선언해 둬야 합니다.</li>
</ul>
<hr>
<h3 id="그냥-stdvector-쓰면-안-되나요">그냥 <code>std::vector</code> 쓰면 안 되나요?</h3>
<p>사실 가장 좋은 꿀팁입니다. <strong><code>std::vector</code></strong> 는 &#39;이동(move)&#39;이라는 기술이 가능해서 통째로 반환해도 복사본을 만들지 않고 쌩하고 날아갑니다. 만약 <code>std::array</code> 를 통째로 반환해야 하는 상황이 생긴다면, 굳이 무거운 복사를 참아가며 <code>std::array</code> 를 고집하기보다는 유연하고 빠른 <strong><code>std::vector</code></strong> 를 대신 사용하는 것을 강력히 추천합니다!</p>
<hr>
<h2 id="174--클래스-타입의-stdarray와-중괄호-생략-쉽게-이해하기">17.4 — 클래스 타입의 std::array와 중괄호 생략 쉽게 이해하기</h2>
<p><code>std::array</code> 에는 숫자(<code>int</code>)나 문자(<code>char</code>) 같은 단순하고 기본적인 타입만 넣을 수 있는 게 아닙니다. 구조체(struct)나 클래스(class)처럼 우리가 직접 만든 복잡한 덩어리들도 얼마든지 담을 수 있어요! 즉, 포인터들을 모아둔 <code>std::array</code> 나, 구조체들을 모아둔 <code>std::array</code> 를 만들 수 있다는 뜻입니다.</p>
<p>하지만 초보자분들이 구조체나 클래스를 담은 <code>std::array</code> 를 처음 만들 때(초기화할 때) 에러가 나면서 많이들 당황하시곤 합니다. 그래서 이번 레슨에서는 이 부분을 아주 속 시원하고 알기 쉽게 풀어보겠습니다.</p>
<blockquote>
<p><strong>작성자의 참고 노트</strong> 
이번 레슨에서는 이해를 돕기 위해 &#39;구조체(struct)&#39;를 예시로 사용할 거예요. 하지만 여기서 배우는 모든 원리는 &#39;클래스(class)&#39;에도 똑같이 적용되니 걱정하지 마세요!</p>
</blockquote>
<hr>
<h3 id="구조체를-담은-stdarray-만들고-값-넣기">구조체를 담은 std::array 만들고 값 넣기</h3>
<p>먼저 아주 간단한 <code>House</code> (집) 구조체를 만들어 볼까요?</p>
<pre><code class="language-cpp">struct House{
    int number{};
    int stories{};
    int roomsPerStory{};
};
</code></pre>
<p>이 <code>House</code> 구조체들을 담는 <code>std::array</code> 를 만들고, 나중에 값을 하나씩 집어넣는 건 우리가 흔히 아는 방식 그대로 잘 작동합니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

struct House{
    int number{};
    int stories{};
    int roomsPerStory{};
};

int main(){
    std::array&lt;House, 3&gt; houses{};

    houses[0] = { 13, 1, 7 };
    houses[1] = { 14, 2, 5 };
    houses[2] = { 15, 2, 4 };

    for (const auto&amp; house : houses)
    {
        std::cout &lt;&lt; &quot;House number &quot; &lt;&lt; house.number
                  &lt;&lt; &quot; has &quot; &lt;&lt; (house.stories * house.roomsPerStory)
                  &lt;&lt; &quot; rooms.\n&quot;;
    }

    return 0;
}
</code></pre>
<p>위 코드를 실행하면 예상대로 다음처럼 잘 출력됩니다.</p>
<blockquote>
<p>House number 13 has 7 rooms.
House number 14 has 10 rooms.
House number 15 has 8 rooms.</p>
</blockquote>
<hr>
<h3 id="구조체를-담은-stdarray-초기화하기-만들면서-동시에-값-넣기">구조체를 담은 std::array 초기화하기 (만들면서 동시에 값 넣기)</h3>
<p>배열을 만들 때 값을 한 번에 쏙쏙 집어넣는 것도 잘 작동합니다. 
단, <strong>어떤 타입</strong> 인지 명확하게 적어준다면요!</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

struct House{
    int number{};
    int stories{};
    int roomsPerStory{};
};

int main(){
    constexpr std::array houses { // CTAD(템플릿 인자 추론)를 사용해 &lt;House, 3&gt; 이라는 걸 컴파일러가 눈치채게 합니다
            House{ 13, 1, 7 },
            House{ 14, 2, 5 },
            House{ 15, 2, 4 }
        };

    for (const auto&amp; house : houses)
    {
        std::cout &lt;&lt; &quot;House number &quot; &lt;&lt; house.number
            &lt;&lt; &quot; has &quot; &lt;&lt; (house.stories * house.roomsPerStory)
            &lt;&lt; &quot; rooms.\n&quot;;
    }

    return 0;
}
</code></pre>
<p>위 코드에서는 CTAD(컴파일러가 타입을 스스로 유추하는 똑똑한 기능)를 써서 이 배열이 <code>std::array&lt;House, 3&gt;</code> 이라는 걸 알아내게 했습니다. 그리고 <code>House{ ... }</code> 처럼 각 줄마다 <strong>House</strong> 라고 친절하게 적어줬기 때문에 아무 문제 없이 작동합니다.</p>
<hr>
<h3 id="매번-house라고-적기-귀찮다면-여기서부터-문제가-생깁니다">매번 &#39;House&#39;라고 적기 귀찮다면? (여기서부터 문제가 생깁니다!)</h3>
<p>위 코드에서 값을 넣을 때마다 <code>House</code> 라고 일일이 적어주는 게 조금 번거롭게 느껴지지 않나요?</p>
<pre><code class="language-cpp">    constexpr std::array houses {
        House{ 13, 1, 7 }, // 여기에 House라고 썼고
        House{ 14, 2, 5 }, // 여기도 썼고
        House{ 15, 2, 4 }  // 여기도 썼네요
    };
</code></pre>
<p>생각해 보면 아까 배열을 만들고 나중에 값을 대입할 때는 굳이 <code>House</code> 라고 적지 않아도 알아서 잘 들어갔거든요.</p>
<pre><code class="language-cpp">// 컴파일러는 houses 배열의 각 칸이 House라는 걸 이미 알고 있습니다.
// 그래서 오른쪽의 숫자를 알아서 House 형태로 바꿔서 넣어줍니다.
houses[0] = { 13, 1, 7 };
houses[1] = { 14, 2, 5 };
houses[2] = { 15, 2, 4 };
</code></pre>
<p>그래서 아마 여러분은 &quot;처음 배열을 만들 때도 <code>House</code> 라는 글자를 빼고 숫자만 적어도 알아서 들어가지 않을까?&quot; 하고 이렇게 시도해 볼 수 있습니다.</p>
<pre><code class="language-cpp">// 작동하지 않습니다!
constexpr std::array&lt;House, 3&gt; houses { // 컴파일러에게 각 칸이 House라고 말해줬지만...
        { 13, 1, 7 }, // 정작 값 앞에는 House를 빼버렸습니다.
        { 14, 2, 5 },
        { 15, 2, 4 }
    };
</code></pre>
<p><strong>놀랍게도 이 코드는 에러가 납니다.</strong> 왜 안 되는지 그 속사정을 아주 쉽게 파헤쳐 볼까요?</p>
<hr>
<h3 id="왜-에러가-날까요-stdarray의-비밀">왜 에러가 날까요? (std::array의 비밀)</h3>
<p>사실 <code>std::array</code> 는 진짜 순수한 배열이 아닙니다. <strong>진짜 C스타일 배열 하나를 몰래 품고 있는 &#39;포장지(구조체)&#39;</strong> 에 불과합니다. 속살은 이렇게 생겼어요.</p>
<pre><code class="language-cpp">template&lt;typename T, std::size_t N&gt;
struct array{
    T implementation_defined_name[N]; // 타입 T를 N개 담을 수 있는 &#39;숨겨진 C스타일 배열&#39;입니다
};
</code></pre>
<p>그래서 우리가 아까처럼 코드를 쓰면, 바보 같은 컴파일러는 우리의 의도와 다르게 완전히 오해를 해버립니다.</p>
<pre><code class="language-cpp">// 작동하지 않습니다!
constexpr std::array&lt;House, 3&gt; houses { // 1. 자, houses 포장지(구조체) 초기화 시작!
    { 13, 1, 7 }, // 2. 어? 안에 있는 &#39;숨겨진 C스타일 배열&#39;의 첫 번째 칸(0번 칸)에 이걸 넣으라는 거구나!
    { 14, 2, 5 }, // 3. 엥? 자리가 없는데? 이건 뭐야? (?)
    { 15, 2, 4 }  // 4. 이것도 뭐야? 에러!! (?)
};
</code></pre>
<p>즉, 컴파일러는 첫 번째 줄 <code>{ 13, 1, 7 }</code> 전체를 <strong>&quot;숨겨진 내부 배열 3칸 중 첫 번째 칸에 몽땅 넣어라&quot;</strong> 는 뜻으로 착각해 버립니다. 그리고 나머지 두 줄을 보고는 &quot;더 이상 넣을 데가 없는데 값이 너무 많아!&quot; 라며 에러를 뱉어내는 거죠.</p>
<hr>
<h3 id="정답은-이중-중괄호-치기">정답은 &#39;이중 중괄호&#39; 치기!</h3>
<p>이 오해를 풀고 코드를 정상적으로 작동하게 만드는 올바른 방법은, <strong>중괄호를 한 겹 더 씌워주는 것</strong> 입니다.</p>
<pre><code class="language-cpp">// 예상대로 아주 잘 작동합니다!
constexpr std::array&lt;House, 3&gt; houses { // 1. houses 포장지(구조체) 초기화 시작!
    { // 2. 여기서부터가 &#39;숨겨진 C스타일 배열&#39;에 들어갈 진짜 값들이야! 라고 알려주는 추가 중괄호
        { 13, 4, 30 }, // 배열의 0번 칸에 들어갈 값
        { 14, 3, 10 }, // 배열의 1번 칸에 들어갈 값
        { 15, 3, 40 }, // 배열의 2번 칸에 들어갈 값
     }
};
</code></pre>
<p>바깥쪽 중괄호 <code>{}</code> 는 <code>std::array</code> 라는 포장지를 위한 것이고, 안쪽 중괄호 <code>{}</code> 가 바로 그 안에 숨어있는 진짜 배열을 위한 것입니다. 이렇게 해주면 각각의 방에 값들이 예쁘게 쏙쏙 들어갑니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong> 
구조체, 클래스, 배열 등을 담은 <code>std::array</code> 를 만들 때, 값 앞에 일일이 <code>House{...}</code> 처럼 이름을 적어주기 귀찮다면 <strong>반드시 바깥에 중괄호를 한 번 더 <code>{{ }}</code> 씌워주세요!</strong> 그래야 컴파일러가 헷갈리지 않습니다.</p>
</blockquote>
<p>완성된 코드는 이렇습니다:</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

struct House{
    int number{};
    int stories{};
    int roomsPerStory{};
};

int main(){
    constexpr std::array&lt;House, 3&gt; houses {{ // 이중 중괄호 {{ }} 에 주목하세요!
        { 13, 1, 7 },
        { 14, 2, 5 },
        { 15, 2, 4 }
    }};

    for (const auto&amp; house : houses)
    {
        std::cout &lt;&lt; &quot;House number &quot; &lt;&lt; house.number
                  &lt;&lt; &quot; has &quot; &lt;&lt; (house.stories * house.roomsPerStory)
                  &lt;&lt; &quot; rooms.\n&quot;;
    }

    return 0;
}
</code></pre>
<hr>
<h3 id="중괄호-생략-이란">중괄호 생략 이란?</h3>
<p>&quot;어라? 숫자만 넣을 때는 중괄호 한 번만 써도 잘 됐는데요?&quot; 라고 생각하셨다면 아주 예리하십니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main(){
    constexpr std::array&lt;int, 5&gt; arr { 1, 2, 3, 4, 5 }; // 단일 중괄호 (어? 왜 이건 되죠?)

    for (const auto n : arr)
        std::cout &lt;&lt; n &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>물론 여기도 원래는 이중 중괄호 <code>{{ 1, 2, 3, 4, 5 }}</code> 를 쓰는 게 원칙상 맞습니다.</p>
<p>하지만 C++은 우리를 편하게 해주려고 <strong>중괄호 생략 (Brace elision)</strong> 이라는 마법을 부립니다. 배열 안에 들어가는 게 단순한 숫자 하나씩일 때, 또는 아까처럼 <code>House{13,1,7}</code> 처럼 이름을 명확히 적어줬을 때는 컴파일러가 눈치껏 &quot;아, 굳이 중괄호 두 번 안 쳐도 내가 찰떡같이 알아먹지!&quot; 하고 한 겹을 생략할 수 있게 허락해 주는 것입니다.</p>
<p><strong>초보자를 위한 꿀팁:</strong> 언제 생략해도 되는지 헷갈린다면? 고민하지 말고 <strong>무조건 이중 중괄호 <code>{{ }}</code> 를 쓰시면 마음이 편안해집니다.</strong> 아니면 일단 중괄호를 한 번만 써보고, 컴파일러가 빨간 줄(에러)을 띄우면 그때 쓱 한 겹 더 씌워주면 됩니다!</p>
<hr>
<h3 id="학생-예제로-복습하기">학생 예제로 복습하기</h3>
<p>마지막으로 <code>Student</code> 라는 구조체를 담은 배열을 어떻게 만들었는지 살펴볼까요? 
여기서는 각 값 앞에 <code>Student</code> 라고 친절하게 이름을 적어주었기 때문에 중괄호를 한 번만(생략해서) 썼습니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

// 각 학생은 id(번호)와 name(이름)을 가집니다
struct Student{
    int id{};
    std::string_view name{};
};

// 3명의 학생을 담은 배열입니다. (각 값 앞에 Student라고 명시했기 때문에 중괄호를 한 번만 썼습니다)
constexpr std::array students{ Student{0, &quot;Alex&quot;}, Student{ 1, &quot;Joe&quot; }, Student{ 2, &quot;Bob&quot; } };

const Student* findStudentById(int id){
    // 모든 학생을 하나씩 확인합니다
    for (auto&amp; s : students)
    {
        // 입력한 번호(id)와 일치하는 학생을 찾으면 그 학생의 주소를 반환합니다
        if (s.id == id) return &amp;s;
    }

    // 일치하는 번호를 못 찾았다면 빈 값(nullptr)을 반환합니다
    return nullptr;
}

int main(){
    constexpr std::string_view nobody { &quot;nobody&quot; };

    const Student* s1 { findStudentById(1) };
    std::cout &lt;&lt; &quot;You found: &quot; &lt;&lt; (s1 ? s1-&gt;name : nobody) &lt;&lt; &#39;\n&#39;;

    const Student* s2 { findStudentById(3) };
    std::cout &lt;&lt; &quot;You found: &quot; &lt;&lt; (s2 ? s2-&gt;name : nobody) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>출력 결과:</p>
<blockquote>
<p>You found: Joe
You found: nobody</p>
</blockquote>
<p>참고로, 위 코드에서 <code>std::array students</code> 가 수정 불가능한 상수(<code>constexpr</code>)로 만들어졌기 때문에, 이 학생 정보를 찾아주는 함수 <code>findStudentById()</code> 도 반드시 수정 불가능한 <code>const</code> 포인터를 반환해야 합니다. 따라서 <code>main()</code> 함수에서 결과값을 받을 때도 <code>const Student*</code> 로 받아야 한다는 점 잊지 마세요!</p>
<hr>
<h2 id="176--stdarray와-열거형">17.6 — std::array와 열거형</h2>
<p>이전 레슨(16.9)에서는 배열과 열거형(enum)에 대해 이야기해 보았죠. 이제 우리가 코드 작성 시 아주 유용한 도구인 <code>constexpr std::array</code> 를 가지게 되었으니, 이 이야기를 조금 더 발전시켜서 몇 가지 재미있는 꿀팁을 알려드릴게요.</p>
<hr>
<h3 id="배열의-초기화-개수가-맞는지-확인하기-static_assert-사용"><strong>배열의 초기화 개수가 맞는지 확인하기</strong> (static_assert 사용)</h3>
<p>C++에서 <code>constexpr std::array</code> 를 만들 때 안의 값들(초기값)만 쏙쏙 넣어주면, 똑똑한 컴파일러가 &quot;아, 이 배열은 이만큼의 길이가 필요하겠구나!&quot; 하고 스스로 크기를 알아맞히는 기능이 있어요.</p>
<p>하지만 만약 우리가 실수로 값을 <strong>원래 있어야 할 개수보다 적게</strong> 넣으면 어떻게 될까요? 배열의 길이가 생각했던 것보다 짧아지게 되고, 나중에 빈 공간에 접근하려고 하면 프로그램이 엉뚱하게 작동하는 문제(정의되지 않은 동작)가 발생할 수 있습니다.</p>
<p>예를 들어볼게요:</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    constexpr std::array testScores { 78, 94, 66, 77 }; // 앗, 값이 4개밖에 없네요

    std::cout &lt;&lt; &quot;Cartman got a score of &quot; &lt;&lt; testScores[StudentNames::cartman] &lt;&lt; &#39;\n&#39;; // 잘못된 인덱스 접근으로 인해 정의되지 않은 동작 발생

    return 0;
}
</code></pre>
<p>이렇게 배열 안에 들어가는 값의 개수가 정확한지 확인해야 할 때는 <strong>static_assert</strong> 라는 기능을 사용해서 아주 안전하게 검사할 수 있어요. <code>static_assert</code> 는 프로그램이 만들어질 때(컴파일될 때) &quot;이 조건이 맞는지 꼭 확인해!&quot; 라고 명령을 내리는 든든한 문지기 역할을 합니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    constexpr std::array testScores { 78, 94, 66, 77 };

    // 시험 점수의 개수가 학생 수와 똑같은지 확인합니다
    static_assert(std::size(testScores) == max_students); // 컴파일 에러: static_assert 조건 실패

    std::cout &lt;&lt; &quot;Cartman got a score of &quot; &lt;&lt; testScores[StudentNames::cartman] &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이렇게 코드를 짜두면 참 편리해요. 나중에 새로운 학생(열거형 값)을 추가해 놓고 시험 점수를 깜빡 잊고 안 적었더라도, 프로그램이 아예 실행조차 되지 않고 에러를 내뿜기 때문에 실수를 바로잡을 수 있거든요. 이 방법은 두 개의 서로 다른 <code>constexpr std::array</code> 가 똑같은 길이를 가졌는지 확인할 때도 사용할 수 있습니다.</p>
<hr>
<h3 id="열거형enum의-입력과-출력-을-더-쉽게-만드는-constexpr-배열"><strong>열거형(enum)의 입력과 출력</strong> 을 더 쉽게 만드는 constexpr 배열</h3>
<p>이전 레슨(13.5)에서 열거형(enum)의 이름을 화면에 출력하거나 입력받는 방법을 배웠어요. 그때는 열거형을 문자열로 바꾸거나, 문자열을 다시 열거형으로 바꿔주는 &#39;도우미 함수&#39;를 직접 길게 만들었죠. 그런데 이 함수들은 똑같은 단어들을 중복해서 가지고 있어야 했고, 일일이 조건문을 확인해야 해서 무척 번거로웠어요.</p>
<pre><code class="language-cpp">constexpr std::string_view getPetName(Pet pet)
{
    switch (pet)
    {
    case cat:   return &quot;cat&quot;;
    case dog:   return &quot;dog&quot;;
    case pig:   return &quot;pig&quot;;
    case whale: return &quot;whale&quot;;
    default:    return &quot;???&quot;;
    }
}

constexpr std::optional&lt;Pet&gt; getPetFromString(std::string_view sv)
{
    if (sv == &quot;cat&quot;)   return cat;
    if (sv == &quot;dog&quot;)   return dog;
    if (sv == &quot;pig&quot;)   return pig;
    if (sv == &quot;whale&quot;) return whale;

    return {};
}
</code></pre>
<p>이 방식의 귀찮은 점은, 나중에 새로운 동물을 추가할 때마다 이 함수들도 잊지 않고 일일이 수정해 줘야 한다는 거예요.</p>
<p>이제 이 함수들을 훨씬 더 똑똑하게 고쳐볼까요? 대부분의 열거형은 값이 0부터 시작해서 차례대로 1씩 커집니다. 이 특징을 이용하면, 배열 하나에 열거형의 이름들을 순서대로 담아두고 사용할 수 있어요!</p>
<p>이렇게 하면 두 가지 마법 같은 일이 가능해집니다:</p>
<ol>
<li>열거형의 값을 인덱스(순서 번호)로 사용해서, 배열에서 곧바로 해당하는 이름을 꺼내올 수 있어요.</li>
<li>반복문을 사용해 모든 이름을 하나씩 훑어볼 수 있고, 몇 번째 인덱스인지를 확인해서 이름과 열거형 값을 다시 짝지어줄 수도 있습니다.</li>
</ol>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // sv 접미사를 사용하여 std::array가 타입을 std::string_view로 추론하게 합니다
    using namespace std::string_view_literals; // sv 접미사를 사용하기 위함
    constexpr std::array colorName { &quot;black&quot;sv, &quot;red&quot;sv, &quot;blue&quot;sv };

    // 모든 색상에 대해 문자열이 잘 정의되었는지 확인합니다
    static_assert(std::size(colorName) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    // 열거형 값을 인덱스로 사용해서 열거형의 이름을 바로 가져올 수 있습니다
    return Color::colorName[static_cast&lt;std::size_t&gt;(color)];
}

// operator&lt;&lt; 에게 Color를 어떻게 출력하는지 알려줍니다
// std::ostream은 std::cout의 타입입니다
// 반환 타입과 매개변수 타입은 참조(reference)입니다 (불필요한 복사를 막기 위해서요)!
std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, Color::Type color)
{
    return out &lt;&lt; getColorName(color);
}

// operator&gt;&gt; 에게 이름을 통해 Color를 어떻게 입력받는지 알려줍니다
// 함수가 값을 수정할 수 있도록 const가 아닌 참조로 color를 전달합니다
std::istream&amp; operator&gt;&gt; (std::istream&amp; in, Color::Type&amp; color)
{
    std::string input {};
    std::getline(in &gt;&gt; std::ws, input);

    // 이름 목록을 쭉 확인하면서 일치하는 이름이 있는지 찾습니다
    for (std::size_t index=0; index &lt; Color::colorName.size(); ++index)
    {
        if (input == Color::colorName[index])
        {
            // 일치하는 이름을 찾았다면, 그 인덱스를 사용해 열거형 값을 얻을 수 있습니다
            color = static_cast&lt;Color::Type&gt;(index);
            return in;
        }
    }

    // 일치하는 것을 못 찾았다면 잘못된 입력이라는 뜻입니다
    // 따라서 입력 스트림을 실패(fail) 상태로 설정합니다
    in.setstate(std::ios_base::failbit);

    // 추출에 실패하면, operator&gt;&gt; 는 기본 타입들을 0으로 초기화합니다
    // 이 연산자도 똑같이 동작하게 만들려면 아래 줄의 주석을 해제하세요
    // color = {};
    return in;
}

int main()
{
    auto shirt{ Color::blue };
    std::cout &lt;&lt; &quot;Your shirt is &quot; &lt;&lt; shirt &lt;&lt; &#39;\n&#39;;

    std::cout &lt;&lt; &quot;Enter a new color: &quot;;
    std::cin &gt;&gt; shirt;
    if (!std::cin)
        std::cout &lt;&lt; &quot;Invalid\n&quot;;
    else
        std::cout &lt;&lt; &quot;Your shirt is now &quot; &lt;&lt; shirt &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드의 실행 결과는 다음과 같습니다:</p>
<pre><code class="language-text">Your shirt is blue
Enter a new color: red
Your shirt is now red
</code></pre>
<hr>
<h4 id="범위-기반-for문-과-열거형"><strong>범위 기반 for문</strong> 과 열거형</h4>
<p>가끔 열거형 안에 있는 값들을 처음부터 끝까지 한 바퀴 쫙 돌면서(반복하면서) 작업하고 싶을 때가 있어요. 숫자 인덱스를 쓰는 일반적인 <code>for</code> 문을 사용해서 할 수도 있지만, 이렇게 하면 숫자를 열거형 타입으로 억지로 변환해 주는 귀찮은 작업(static casting)을 코드에 많이 적어야 합니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // sv 접미사를 사용하여 std::array가 타입을 std::string_view로 추론하게 합니다
    using namespace std::string_view_literals; // sv 접미사를 사용하기 위함
    constexpr std::array colorName { &quot;black&quot;sv, &quot;red&quot;sv, &quot;blue&quot;sv };

    // 모든 색상에 대해 문자열이 잘 정의되었는지 확인합니다
    static_assert(std::size(colorName) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    return Color::colorName[color];
}

// operator&lt;&lt; 에게 Color를 어떻게 출력하는지 알려줍니다
// std::ostream은 std::cout의 타입입니다
// 반환 타입과 매개변수 타입은 참조(reference)입니다 (불필요한 복사를 막기 위해서요)!
std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, Color::Type color)
{
    return out &lt;&lt; getColorName(color);
}

int main()
{
    // 일반 for문을 사용해 모든 색상을 순회합니다
    for (int i=0; i &lt; Color::max_colors; ++i )
        std::cout &lt;&lt; static_cast&lt;Color::Type&gt;(i) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>그렇다면 코드가 훨씬 깔끔해지는 <strong>범위 기반 for문</strong> 을 쓰면 어떨까요? 안타깝게도 C++에서는 이 편리한 방법을 열거형에 직접 사용할 수는 없게 되어 있어요.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

namespace Color
{
    enum Type
    {
        black,
        red,
        blue,
        max_colors
    };

    // sv 접미사를 사용하여 std::array가 타입을 std::string_view로 추론하게 합니다
    using namespace std::string_view_literals; // sv 접미사를 사용하기 위함
    constexpr std::array colorName { &quot;black&quot;sv, &quot;red&quot;sv, &quot;blue&quot;sv };

    // 모든 색상에 대해 문자열이 잘 정의되었는지 확인합니다
    static_assert(std::size(colorName) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    return Color::colorName[color];
}

// operator&lt;&lt; 에게 Color를 어떻게 출력하는지 알려줍니다
// std::ostream은 std::cout의 타입입니다
// 반환 타입과 매개변수 타입은 참조(reference)입니다 (불필요한 복사를 막기 위해서요)!
std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, Color::Type color)
{
    return out &lt;&lt; getColorName(color);
}

int main()
{
    for (auto c: Color::Type) // 컴파일 에러: 열거형은 순회(traverse)할 수 없습니다
        std::cout &lt;&lt; c &lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 문제를 해결하기 위해 많은 프로그래머들이 창의적인 방법들을 만들어냈지만, 가장 직관적이고 쉬운 해결책은 바로 우리가 방금 배운 <code>constexpr std::array</code> 를 활용하는 겁니다!</p>
<p>배열에는 <strong>범위 기반 for문</strong> 을 마음껏 쓸 수 있거든요. 열거형의 값들을 차례대로 담은 배열을 하나 만들어두고, 그 배열을 반복하도록 만들면 끝입니다. (단, 이 방법은 열거형 값들이 서로 중복되지 않고 고유할 때만 제대로 작동해요!)</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;string_view&gt;

namespace Color
{
    enum Type
    {
        black,     // 0
        red,       // 1
        blue,      // 2
        max_colors // 3
    };

    using namespace std::string_view_literals; // sv 접미사를 사용하기 위함
    constexpr std::array colorName { &quot;black&quot;sv, &quot;red&quot;sv, &quot;blue&quot;sv };
    static_assert(std::size(colorName) == max_colors);

    constexpr std::array types { black, red, blue }; // 모든 열거형 값들을 담고 있는 std::array 입니다
    static_assert(std::size(types) == max_colors);
};

constexpr std::string_view getColorName(Color::Type color)
{
    return Color::colorName[color];
}

// operator&lt;&lt; 에게 Color를 어떻게 출력하는지 알려줍니다
// std::ostream은 std::cout의 타입입니다
// 반환 타입과 매개변수 타입은 참조(reference)입니다 (불필요한 복사를 막기 위해서요)!
std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, Color::Type color)
{
    return out &lt;&lt; getColorName(color);
}

int main()
{
    for (auto c: Color::types) // 성공: std::array에는 범위 기반 for문을 사용할 수 있습니다
        std::cout &lt;&lt; c &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위의 예제 코드를 보면 <code>Color::types</code> 배열에 들어있는 요소들의 타입이 바로 <code>Color::Type</code> 입니다. 따라서 반복문을 돌 때 변수 <code>c</code> 의 타입도 자연스럽게 우리가 원하는 <code>Color::Type</code> 으로 알아서 맞춰집니다. 아주 깔끔하죠!</p>
<p>출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">black
red
blue
</code></pre>
<hr>
<h2 id="177--c-스타일-배열-소개">17.7 — C 스타일 배열 소개</h2>
<p>이제 <code>std::vector</code> 와 <code>std::array</code> 를 배웠으니, 배열의 마지막 종류인 <strong>C 스타일 배열</strong>도 마저 알아보겠습니다.</p>
<p>16.1 수업 <em>컨테이너와 배열 소개</em> 에서 말했듯이, C 스타일 배열은 C 언어에서 물려받은 기능입니다. 그리고 다른 배열 종류와 달리, 이것은 C++ 표준 라이브러리 클래스가 아니라 
<strong>C++ 언어 자체에 기본으로 들어 있는 기능</strong> 입니다.
그래서 C 스타일 배열을 쓰기 위해서는 <code>#include</code> 로 헤더 파일을 추가할 필요가 없습니다.</p>
<blockquote>
<p><strong>참고로</strong>
C++ 언어가 원래 직접 지원하는 배열은 C 스타일 배열뿐입니다. 그래서 표준 라이브러리의 배열 컨테이너인 <code>std::array</code> 나 <code>std::vector</code> 도 내부적으로는 보통 C 스타일 배열을 바탕으로 만들어집니다.</p>
</blockquote>
<hr>
<h3 id="c-스타일-배열-선언하기">C 스타일 배열 선언하기</h3>
<p>C 스타일 배열은 언어 자체 기능이기 때문에, 전용 선언 문법이 따로 있습니다.
C 스타일 배열을 선언할 때는 대괄호 <code>[]</code> 를 써서, 이 변수가 C 스타일 배열이라고 컴파일러에게 알려줍니다.</p>
<p>대괄호 안에는 배열의 길이(length)를 넣을 수 있습니다. 이 길이는 <code>std::size_t</code> 형의 정수값이며, 배열 안에 원소가 몇 개 들어 있는지를 컴파일러에게 알려줍니다.</p>
<p>다음 정의는 <code>testScore</code> 라는 이름의 C 스타일 배열 변수를 만들고, 그 안에 <code>int</code> 형 원소 30개를 넣습니다.</p>
<pre><code class="language-cpp">int main()
{
    int testScore[30] {};      // testScore라는 이름의 C 스타일 배열을 정의한다. int 원소 30개가 값 초기화된다(헤더 포함 불필요)

//  std::array&lt;int, 30&gt; arr{}; // 비교용: 값 초기화된 int 원소 30개를 가진 std::array 예시(&lt;array&gt;를 #include 해야 함)

    return 0;
}</code></pre>
<p>C 스타일 배열의 길이는 최소 1 이상이어야 합니다.
배열 길이가 0이거나, 음수이거나, 정수가 아니면 컴파일러가 오류를 냅니다.</p>
<blockquote>
<p><strong>심화 내용</strong>
힙(heap)에 동적으로 할당한 C 스타일 배열은 길이가 0인 것도 허용됩니다.</p>
</blockquote>
<hr>
<h2 id="c-스타일-배열의-길이는-상수-표현식이어야-한다">C 스타일 배열의 길이는 상수 표현식이어야 한다</h2>
<p><code>std::array</code> 와 마찬가지로, C 스타일 배열을 선언할 때 배열 길이는 <strong>상수 표현식</strong> 이어야 합니다. 즉, 컴파일하기 전에 값이 이미 확정되어 있어야 합니다. 형식은 <code>std::size_t</code> 여야 하지만, 대부분의 경우 이 점은 크게 신경 쓰지 않아도 됩니다.</p>
<blockquote>
<p><strong>팁</strong>
일부 컴파일러는 C99의 가변 길이 배열(VLA, variable-length array) 기능과 호환되도록, <code>constexpr</code> 가 아닌 길이도 허용할 수 있습니다.</p>
</blockquote>
<p>하지만 가변 길이 배열은 <strong>정식 C++ 문법이 아닙니다</strong> .
그러므로 C++ 프로그램에서는 사용하지 않는 것이 좋습니다.</p>
<blockquote>
</blockquote>
<p>만약 여러분의 컴파일러가 이런 배열을 허용한다면, 아마도 컴파일러 확장 기능을 끄지 않은 것일 가능성이 큽니다.
(0.10 수업 <em>컴파일러 설정: 컴파일러 확장 기능</em> 참고)</p>
<hr>
<h2 id="c-스타일-배열에-인덱스로-접근하기">C 스타일 배열에 인덱스로 접근하기</h2>
<p><code>std::array</code> 처럼, C 스타일 배열도 첨자 연산자 <code>operator[]</code> 로 원소에 접근할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int arr[5]; // int 값 5개를 담는 배열 정의

    arr[1] = 7; // 첨자 연산자를 사용해 배열의 1번 원소에 접근
    std::cout &lt;&lt; arr[1]; // 7 출력

    return 0;
}</code></pre>
<p>표준 라이브러리 컨테이너 클래스들은 보통 인덱스로 <code>std::size_t</code> 같은 <strong>부호 없는 정수형</strong> 만 사용합니다. 하지만 C 스타일 배열의 인덱스는 <strong>부호 있는 정수든, 부호 없는 정수든, 어떤 정수형이든</strong> 사용할 수 있고, 범위 없는 열거형(unscoped enum)도 사용할 수 있습니다.</p>
<p>즉, 표준 라이브러리 컨테이너에서 자주 생기는 부호 변환 관련 인덱스 문제를 C 스타일 배열은 덜 겪습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int arr[] { 9, 8, 7, 6, 5 };

    int s { 2 };
    std::cout &lt;&lt; arr[s] &lt;&lt; &#39;\n&#39;; // 부호 있는 인덱스를 사용해도 괜찮음

    unsigned int u { 2 };
    std::cout &lt;&lt; arr[u] &lt;&lt; &#39;\n&#39;; // 부호 없는 인덱스를 사용해도 괜찮음

    return 0;
}</code></pre>
<blockquote>
<p><strong>팁</strong>
C 스타일 배열은 부호 있는 인덱스, 부호 없는 인덱스, 범위 없는 열거형 인덱스를 모두 받을 수 있습니다.</p>
</blockquote>
<p>하지만 <code>operator[]</code> 는 <strong>범위 검사</strong>를 하지 않습니다.
그래서 배열 범위를 벗어난 인덱스를 넣으면 <strong>정의되지 않은 동작</strong>이 발생합니다.</p>
<blockquote>
<p><strong>참고로</strong>
배열을 선언할 때 <code>int arr[5]</code> 처럼 쓰는 <code>[]</code> 는 첨자 연산자 <code>operator[]</code> 를 호출하는 것이 아닙니다.
그것은 그냥 <strong>배열 선언 문법의 일부</strong> 입니다.</p>
</blockquote>
<hr>
<h3 id="c-스타일-배열의-집합체-초기화">C 스타일 배열의 집합체 초기화</h3>
<p><code>std::array</code> 와 마찬가지로, C 스타일 배열도 <strong>집합체</strong>입니다.
그래서 <strong>집합체 초기화</strong>를 사용할 수 있습니다.</p>
<p>간단히 다시 말하면, 집합체 초기화는 집합체의 멤버를 <strong>중괄호 목록으로 한 번에 직접 초기화하는 방법</strong> 입니다.
즉, 중괄호 <code>{}</code> 안에 쉼표로 구분된 값들을 넣어서 초기화합니다.</p>
<pre><code class="language-cpp">int main()
{
    int fibonnaci[6] = { 0, 1, 1, 2, 3, 5 }; // 중괄호 목록을 사용한 복사 리스트 초기화
    int prime[5] { 2, 3, 5, 7, 11 };         // 중괄호 목록을 사용한 리스트 초기화(권장)

    return 0;
}</code></pre>
<p>이 두 방식 모두 배열 원소를 <strong>0번부터 차례대로</strong> 초기화합니다.</p>
<p>만약 C 스타일 배열에 초기값을 주지 않으면, 원소들은 <strong>기본 초기화</strong> 됩니다.
대부분의 경우 이것은 원소들이 <strong>초기화되지 않은 상태로 남는다</strong> 는 뜻입니다.</p>
<p>보통은 원소가 제대로 초기화되길 원하므로, 초기값 없이 정의할 때는 빈 중괄호 <code>{}</code> 를 사용해서 <strong>값 초기화</strong> 하는 것이 좋습니다.</p>
<pre><code class="language-cpp">int main()
{
    int arr1[5];    // 멤버가 기본 초기화됨(int 원소들은 초기화되지 않은 채로 남음)
    int arr2[5] {}; // 멤버가 값 초기화됨(int 원소들은 0으로 초기화됨)(권장)

    return 0;
}</code></pre>
<p>초기화 목록에 들어 있는 값의 개수가 배열 길이보다 많으면 컴파일러가 오류를 냅니다.
반대로 값의 개수가 배열 길이보다 적으면, 남은 원소들은 값 초기화됩니다.</p>
<pre><code class="language-cpp">int main()
{
    int a[4] { 1, 2, 3, 4, 5 }; // 컴파일 오류: 초기값이 너무 많음
    int b[4] { 1, 2 };          // arr[2]와 arr[3]는 값 초기화됨

    return 0;
}</code></pre>
<p>C 스타일 배열의 단점 중 하나는, 원소 타입을 반드시 직접 써야 한다는 점입니다.
C 스타일 배열은 클래스 템플릿이 아니기 때문에 CTAD(클래스 템플릿 인수 추론)가 동작하지 않습니다.</p>
<p>또한 <code>auto</code> 를 사용해서 초기화 목록으로부터 배열 원소 타입을 추론하게 하는 것도 불가능합니다.</p>
<pre><code class="language-cpp">int main()
{
    auto squares[5] { 1, 4, 9, 16, 25 }; // 컴파일 오류: C 스타일 배열에는 타입 추론을 사용할 수 없음

    return 0;
}</code></pre>
<hr>
<h3 id="길이-생략하기">길이 생략하기</h3>
<p>다음 배열 정의에는 살짝 중복되는 정보가 있습니다. 보이시나요?</p>
<pre><code class="language-cpp">int main()
{
    const int prime[5] { 2, 3, 5, 7, 11 }; // prime의 길이는 5

    return 0;
}</code></pre>
<p>우리는 컴파일러에게 배열 길이가 5라고 직접 말했고, 동시에 초기값도 5개 넣었습니다.
즉, 같은 정보를 두 번 말한 셈입니다.</p>
<p>C 스타일 배열을 초기화 목록으로 초기화할 때는, 배열 정의에서 길이를 생략할 수 있습니다. 그러면 컴파일러가 초기값 개수를 보고 배열 길이를 알아서 계산해 줍니다.</p>
<p>다음 두 배열 정의는 똑같이 동작합니다.</p>
<pre><code class="language-cpp">int main()
{
    const int prime1[5] { 2, 3, 5, 7, 11 }; // prime1은 길이 5라고 직접 지정
    const int prime2[] { 2, 3, 5, 7, 11 };  // prime2는 컴파일러가 길이 5라고 추론

    return 0;
}</code></pre>
<p>이 방법은 <strong>모든 배열 원소에 대해 초기값을 명시했을 때만</strong> 동작합니다.</p>
<pre><code class="language-cpp">int main()
{
    int bad[] {}; // 오류: 컴파일러는 이것을 길이 0 배열로 추론하는데, 길이 0 배열은 허용되지 않음

    return 0;
}</code></pre>
<p>초기화 목록으로 C 스타일 배열의 모든 원소를 초기화할 때는, <strong>배열 길이를 생략하고 컴파일러가 계산하게 하는 편이 더 좋습니다</strong> .</p>
<p>그렇게 하면 나중에 초기값을 추가하거나 제거해도 배열 길이가 자동으로 맞춰집니다.
또한, 직접 적어 둔 배열 길이와 실제 초기값 개수가 서로 안 맞는 실수도 피할 수 있습니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
모든 배열 원소를 명시적으로 초기화할 때는, C 스타일 배열의 길이를 생략하는 것을 권장합니다.</p>
</blockquote>
<hr>
<h3 id="const-와-constexpr-c-스타일-배열">const 와 constexpr C 스타일 배열</h3>
<p><code>std::array</code> 와 마찬가지로, C 스타일 배열도 <code>const</code> 또는 <code>constexpr</code> 로 만들 수 있습니다.
다른 <code>const</code> 변수와 마찬가지로, <code>const</code> 배열은 반드시 초기화해야 하고, 한 번 정한 뒤에는 원소 값을 바꿀 수 없습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

namespace ProgramData
{
    constexpr int squares[5] { 1, 4, 9, 16, 25 }; // constexpr int로 이루어진 배열
}

int main()
{
    const int prime[5] { 2, 3, 5, 7, 11 }; // const int로 이루어진 배열
    prime[0] = 17; // 컴파일 오류: const int는 바꿀 수 없음

    return 0;
}</code></pre>
<hr>
<h3 id="c-스타일-배열에-대한-sizeof">C 스타일 배열에 대한 <code>sizeof</code></h3>
<p>이전 수업에서 <code>sizeof()</code> 연산자는 객체나 타입의 크기를 <strong>바이트 단위</strong> 로 구한다고 배웠습니다.
C 스타일 배열에 <code>sizeof()</code> 를 적용하면, <strong>배열 전체가 차지하는 바이트 수</strong> 를 반환합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int prime[] { 2, 3, 5, 7, 11 }; // 컴파일러가 prime의 길이를 5로 추론함

    std::cout &lt;&lt; sizeof(prime); // 20 출력(단, int가 4바이트라고 가정)

    return 0;
}</code></pre>
<p><code>int</code> 가 4바이트라고 가정하면, 위 프로그램은 20을 출력합니다.
<code>prime</code> 배열은 <code>int</code> 원소 5개를 가지고 있고, 각 원소가 4바이트이므로 <code>5 * 4 = 20</code> 바이트입니다.</p>
<p>여기에는 별도의 추가 정보(overhead)가 없습니다.
C 스타일 배열 객체는 <strong>원소들만 가지고 있고, 그 외의 추가 데이터는 없습니다</strong> .</p>
<hr>
<h3 id="c-스타일-배열의-길이-구하기">C 스타일 배열의 길이 구하기</h3>
<p>C++17에서는 <code>&lt;iterator&gt;</code> 헤더에 있는 <code>std::size()</code> 를 사용할 수 있습니다.
이 함수는 배열 길이를 <strong>부호 없는 정수값(<code>std::size_t</code>)</strong> 으로 돌려줍니다.</p>
<p>C++20에서는 <code>std::ssize()</code> 도 사용할 수 있는데, 이것은 배열 길이를 <strong>부호 있는 정수값</strong> 으로 돌려줍니다. (보통 <code>std::ptrdiff_t</code> 같은 큰 부호 있는 정수형입니다.)</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;iterator&gt; // std::size와 std::ssize용

int main()
{
    const int prime[] { 2, 3, 5, 7, 11 };   // 컴파일러가 prime의 길이를 5로 추론함

    std::cout &lt;&lt; std::size(prime) &lt;&lt; &#39;\n&#39;;  // C++17, 부호 없는 정수값 5를 반환
    std::cout &lt;&lt; std::ssize(prime) &lt;&lt; &#39;\n&#39;; // C++20, 부호 있는 정수값 5를 반환

    return 0;
}</code></pre>
<blockquote>
<p><strong>팁</strong>
<code>std::size()</code> 와 <code>std::ssize()</code> 의 정식 헤더는 <code>&lt;iterator&gt;</code> 입니다.
하지만 이 함수들이 워낙 자주 쓰이다 보니, <code>&lt;array&gt;</code> 나 <code>&lt;vector&gt;</code> 같은 다른 헤더에서도 함께 사용할 수 있는 경우가 많습니다.</p>
</blockquote>
<p>다만 C 스타일 배열에 <code>std::size()</code> 또는 <code>std::ssize()</code> 를 쓸 때는, 다른 헤더를 이미 포함하지 않았을 수도 있습니다.
그럴 때는 보통 <code>&lt;iterator&gt;</code> 를 포함하는 것이 관례입니다.</p>
<blockquote>
</blockquote>
<p>이 함수들을 제공하는 전체 헤더 목록은 cppreference의 size 함수 문서에서 볼 수 있습니다.</p>
<hr>
<h3 id="c-스타일-배열의-길이-구하기c14-이하">C 스타일 배열의 길이 구하기(C++14 이하)</h3>
<p>C++17 이전에는 C 스타일 배열의 길이를 구하는 표준 라이브러리 함수가 없었습니다.</p>
<p>만약 C++11 또는 C++14를 쓰고 있다면, 대신 다음 함수를 사용할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;cstddef&gt; // std::size_t용
#include &lt;iostream&gt;

template &lt;typename T, std::size_t N&gt;
constexpr std::size_t length(const T(&amp;)[N]) noexcept
{
    return N;
}

int main() {

    int array[]{ 1, 1, 2, 3, 5, 8, 13, 21 };
    std::cout &lt;&lt; &quot;The array has: &quot; &lt;&lt; length(array) &lt;&lt; &quot; elements\n&quot;;

    return 0;
}</code></pre>
<p>이 코드는 함수 템플릿을 사용합니다.
이 함수는 C 스타일 배열을 <strong>참조(reference)</strong> 로 받고, 그 배열 길이를 나타내는 비타입 템플릿 매개변수 <code>N</code> 을 그대로 반환합니다.</p>
<p>아주 오래된 코드에서는, 배열 전체 크기를 원소 하나의 크기로 나누어서 길이를 구하는 방법도 자주 보입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int array[8] {};
    std::cout &lt;&lt; &quot;The array has: &quot; &lt;&lt; sizeof(array) / sizeof(array[0]) &lt;&lt; &quot; elements\n&quot;;

    return 0;
}</code></pre>
<p>이 코드는 다음을 출력합니다.</p>
<pre><code class="language-text">The array has: 8 elements</code></pre>
<p>이게 왜 될까요?
먼저, 배열 전체 크기는 다음과 같습니다.</p>
<p>배열 전체 크기 = 배열 길이 × 원소 하나의 크기</p>
<p>짧게 쓰면:</p>
<p>배열 크기 = 길이 × 원소 크기</p>
<p>이 식을 길이에 대해 정리하면:</p>
<p>길이 = 배열 크기 / 원소 크기</p>
<p>보통 원소 크기는 <code>sizeof(array[0])</code> 로 구합니다.
그래서 다음 식이 나옵니다.</p>
<p><code>length = sizeof(array) / sizeof(array[0])</code></p>
<p>가끔은 이것을 다음처럼 쓰는 경우도 있습니다.</p>
<p><code>sizeof(array) / sizeof(*array)</code></p>
<p>이것도 같은 뜻입니다.</p>
<p>하지만 다음 수업에서 보겠지만, 이 공식은 <strong>배열이 decay된 경우</strong> 쉽게 깨질 수 있습니다.
그러면 프로그램이 예상치 못하게 망가질 수 있습니다.</p>
<p>반면 C++17의 <code>std::size()</code> 와 위에서 보여 준 <code>length()</code> 함수 템플릿은 이런 경우 <strong>컴파일 오류를 내기 때문에 더 안전합니다</strong> .</p>
<blockquote>
<p><strong>관련 내용</strong>
배열 decay는 다음 수업 17.8 <em>C 스타일 배열 decay</em> 에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="c-스타일-배열은-대입을-지원하지-않는다">C 스타일 배열은 대입을 지원하지 않는다</h3>
<p>조금 의외일 수 있지만, C++ 배열은 배열 전체에 대한 대입을 지원하지 않습니다.</p>
<pre><code class="language-cpp">int main()
{
    int arr[] { 1, 2, 3 }; // 괜찮음: 초기화는 가능
    arr[0] = 4;            // 개별 원소에 대한 대입은 가능
    arr = { 5, 6, 7 };     // 컴파일 오류: 배열 전체 대입은 불가능

    return 0;
}</code></pre>
<p>기술적으로 말하면, 대입 연산의 왼쪽 피연산자는 <strong>수정 가능한 lvalue</strong> 여야 합니다.
그런데 C 스타일 배열은 그런 대상으로 취급되지 않기 때문에 이런 대입이 불가능합니다.</p>
<p>만약 C 스타일 배열에 새로운 값 목록을 통째로 넣고 싶다면, 가장 좋은 방법은 <code>std::vector</code> 를 쓰는 것입니다.
또는 C 스타일 배열의 각 원소를 하나씩 바꾸거나, <code>std::copy</code> 를 사용해서 다른 C 스타일 배열의 내용을 복사할 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt; // std::copy용

int main()
{
    int arr[] { 1, 2, 3 };
    int src[] { 5, 6, 7 };

    // src를 arr로 복사
    std::copy(std::begin(src), std::end(src), std::begin(arr));

    return 0;
}</code></pre>
<hr>
<h3 id="정말-쉽게-이해하는-한-줄-요약">정말 쉽게 이해하는 한 줄 요약</h3>
<p>C 스타일 배열은 <strong>“같은 타입의 값을 여러 개 붙여서 저장하는, C++ 기본 내장 배열”</strong> 입니다.
다만 편리한 기능이 적고 실수하기 쉬워서, 현대 C++ 에서는 보통 <code>std::array</code> 나 <code>std::vector</code> 를 더 많이 씁니다.</p>
<hr>
<h2 id="178--c-스타일-배열-decay">17.8 — C 스타일 배열 decay</h2>
<h3 id="c-스타일-배열을-함수에-넘길-때-생기는-문제">C 스타일 배열을 함수에 넘길 때 생기는 문제</h3>
<p>C 언어를 만든 사람들은 한 가지 문제를 해결해야 했습니다. 
먼저 아래처럼 아주 단순한 프로그램을 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int val)
{
    std::cout &lt;&lt; val;
}

int main()
{
    int x { 5 };
    print(x);

    return 0;
}</code></pre>
<p><code>print(x)</code>를 호출하면, 인수 <code>x</code>의 값 <code>5</code>가 매개변수 <code>val</code>로 <strong>복사</strong>됩니다.
함수 안에서는 <code>val</code>의 값 <code>5</code>를 화면에 출력합니다.
<code>int</code> 하나 복사하는 건 부담이 거의 없으니, 여기서는 아무 문제가 없습니다.</p>
<p>이제 비슷한 예제를 보겠습니다. 
이번에는 <code>int</code> 하나가 아니라, 원소가 1000개인 C 스타일 <code>int</code> 배열을 사용합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printElementZero(int arr[1000])
{
    std::cout &lt;&lt; arr[0]; // 첫 번째 원소의 값을 출력
}

int main()
{
    int x[1000] { 5 };   // 원소가 1000개인 배열 정의, x[0]은 5로 초기화
    printElementZero(x);

    return 0;
}</code></pre>
<p>이 프로그램도 잘 컴파일되고, 예상대로 <code>5</code>를 출력합니다.</p>
<p>겉으로 보기에는 앞의 예제와 비슷하지만, 실제로는 예상과 조금 다르게 동작합니다.
왜냐하면 C 언어 설계자들이 <strong>두 가지 큰 문제</strong>를 동시에 해결하려고 특별한 방법을 만들었기 때문입니다.</p>
<p>첫 번째 문제는 이것입니다.
함수를 호출할 때마다 원소 1000개짜리 배열을 통째로 복사하면 비용이 큽니다.
원소 타입 자체도 복사 비용이 큰 타입이라면 더 부담스럽습니다.
그래서 복사를 피하고 싶었습니다.
그런데 C에는 <strong>참조(reference)</strong>가 없어서, 참조 전달로 복사를 피할 수 없었습니다.</p>
<p>두 번째 문제도 있습니다.
길이가 다른 배열들도 <strong>하나의 함수</strong>로 받고 싶었습니다.
예를 들어 위의 <code>printElementZero()</code>는 배열 길이가 몇이든, <code>arr[0]</code>만 있으면 되니 아무 길이의 배열이든 받는 게 이상적입니다.
배열 길이마다 함수를 하나씩 만드는 건 너무 불편합니다.</p>
<p>그런데 C에는 “길이가 아무거나인 배열”을 직접 표현하는 문법도 없고, 템플릿도 없고, 길이가 다른 배열끼리 자동 변환도 안 됩니다.</p>
<p>그래서 C 언어 설계자들은 아주 영리한 해결책을 만들었습니다.
이 방식은 호환성 때문에 C++에도 그대로 들어왔습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printElementZero(int arr[1000]) // 복사본을 만들지 않음
{
    std::cout &lt;&lt; arr[0]; // 첫 번째 원소의 값을 출력
}

int main()
{
    int x[7] { 5 };      // 원소가 7개인 배열 정의
    printElementZero(x); // somehow works!

    return 0;
}</code></pre>
<p>이상하게도 위 코드는 <strong>원소 7개짜리 배열</strong>을, <strong>원소 1000개짜리 배열을 받는 것처럼 보이는 함수</strong>에 넘기고도 잘 동작합니다.
게다가 배열 복사도 일어나지 않습니다.</p>
<p>이번 단원에서는 이것이 어떻게 가능한지 살펴보겠습니다.</p>
<p>또한 C 설계자들이 고른 이 해결책이 왜 위험할 수 있는지, 그리고 왜 현대 C++에서는 그다지 좋은 방식이 아닌지도 함께 보겠습니다.</p>
<p>하지만 그 전에, 먼저 두 가지 하위 주제를 알아야 합니다.</p>
<hr>
<h3 id="배열이-포인터로-변환되는-것-array-decay">배열이 포인터로 변환되는 것 (array decay)</h3>
<p>대부분의 경우, C 스타일 배열이 식(expression) 안에서 사용되면, 배열은 자동으로 <strong>원소 타입을 가리키는 포인터</strong>로 바뀝니다.
이 포인터는 배열의 <strong>첫 번째 원소(인덱스 0)의 주소</strong>로 초기화됩니다.</p>
<p>이 현상을 보통 <strong>array decay</strong>(줄여서 decay)라고 부릅니다.</p>
<p>다음 프로그램을 보면 확인할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iomanip&gt; // std::boolalpha용
#include &lt;iostream&gt;

int main()
{
    int arr[5]{ 9, 7, 5, 3, 1 }; // 우리 배열의 원소 타입은 int

    // 먼저, arr가 int* 포인터로 decay된다는 것을 확인해 보자

    auto ptr{ arr }; // 값을 평가하면 arr가 decay됨, 타입 추론은 int*를 추론해야 함
    std::cout &lt;&lt; std::boolalpha &lt;&lt; (typeid(ptr) == typeid(int*)) &lt;&lt; &#39;\n&#39;; // ptr의 타입이 int*이면 true 출력

    // 이제 이 포인터가 배열의 첫 번째 원소 주소를 가지고 있다는 것도 확인해 보자

    std::cout &lt;&lt; std::boolalpha &lt;&lt; (&amp;arr[0] == ptr) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p>작성자의 컴퓨터에서는 다음이 출력되었습니다.</p>
<pre><code class="language-text">true
true</code></pre>
<p>배열이 decay되어 만들어진 포인터는 특별한 포인터가 아닙니다.
그냥 <strong>첫 번째 원소의 주소를 담고 있는 일반 포인터</strong>입니다.</p>
<p>마찬가지로, <code>const</code> 배열(예: <code>const int arr[5]</code>)은 <code>pointer-to-const</code>, 즉 <code>const int*</code>로 decay됩니다.</p>
<blockquote>
<p><strong>팁</strong></p>
<p>C++에서 C 스타일 배열이 decay되지 않는 흔한 경우는 몇 가지 있습니다.</p>
<ul>
<li><code>sizeof()</code>나 <code>typeid()</code>의 인수로 사용될 때</li>
<li><code>operator&amp;</code>로 배열 자체의 주소를 구할 때</li>
<li>클래스 타입의 멤버로 전달될 때</li>
<li>참조로 전달될 때</li>
</ul>
</blockquote>
<p>C 스타일 배열은 대부분의 경우 포인터로 decay되기 때문에, “배열은 곧 포인터다”라고 착각하기 쉽습니다.
하지만 이건 사실이 아닙니다.</p>
<p>배열 객체는 <strong>원소들이 순서대로 들어 있는 덩어리</strong>이고,
포인터 객체는 <strong>주소 하나를 저장하는 변수</strong>일 뿐입니다.</p>
<p>배열 타입과 decay된 배열의 타입 정보도 다릅니다.
위 예제에서 <code>arr</code>의 타입은 <code>int[5]</code>이고, decay된 후의 타입은 <code>int*</code>입니다.</p>
<p>중요한 차이는 이것입니다.</p>
<ul>
<li><code>int[5]</code>에는 <strong>길이 정보</strong>가 들어 있음</li>
<li><code>int*</code>에는 <strong>길이 정보가 없음</strong></li>
</ul>
<blockquote>
<p><strong>핵심 통찰</strong></p>
<p>decay된 배열 포인터는, 자신이 가리키는 배열의 길이를 알지 못합니다.
“decay”라는 말은 바로 이 <strong>길이 정보가 사라진다</strong>는 뜻도 함께 담고 있습니다.</p>
</blockquote>
<hr>
<h3 id="c-스타일-배열에-첨자를-쓰면-사실-decay된-포인터에-operator를-적용하는-것이다">C 스타일 배열에 첨자(<code>[]</code>)를 쓰면, 사실 decay된 포인터에 <code>operator[]</code>를 적용하는 것이다</h3>
<p>C 스타일 배열은 값으로 평가될 때 포인터로 decay되므로, 배열에 첨자 <code>[]</code>를 붙이는 것도 사실은 <strong>decay된 포인터에 대해 첨자를 쓰는 것</strong>입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int arr[] { 9, 7, 5, 3, 1 };
    std::cout &lt;&lt; arr[2]; // decay된 배열에 첨자를 써서 2번 원소를 얻음, 5 출력

    return 0;
}</code></pre>
<p>포인터에도 직접 <code>operator[]</code>를 사용할 수 있습니다.
그 포인터가 첫 번째 원소의 주소를 가지고 있다면, 결과는 똑같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int arr[] { 9, 7, 5, 3, 1 };

    const int* ptr{ arr };  // arr가 포인터로 decay됨
    std::cout &lt;&lt; ptr[2];    // ptr에 첨자를 써서 2번 원소를 얻음, 5 출력

    return 0;
}</code></pre>
<p>이게 왜 편리한지는 조금 뒤에 보게 됩니다.
그리고 이것이 정확히 어떻게 동작하는지, 또 포인터가 첫 번째 원소가 아닌 다른 곳을 가리키면 어떻게 되는지는 다음 단원 <strong>17.9 -- Pointer arithmetic and subscripting</strong>에서 더 자세히 다룹니다.</p>
<hr>
<h3 id="array-decay는-c-스타일-배열-전달-문제를-해결해-준다">array decay는 C 스타일 배열 전달 문제를 해결해 준다</h3>
<p>array decay는 이 단원 처음에 나온 두 가지 문제를 한 번에 해결합니다.</p>
<p>C 스타일 배열을 함수 인수로 넘기면, 배열은 포인터로 decay되고,
그 배열의 첫 번째 원소 주소를 담은 포인터가 함수로 전달됩니다.</p>
<p>즉, 겉으로는 배열을 <strong>값으로 전달하는 것처럼 보여도</strong>, 실제로는 <strong>주소를 전달하는 것</strong>입니다.
그래서 배열 전체 복사본을 만들지 않아도 됩니다.</p>
<blockquote>
<p><strong>핵심 통찰</strong></p>
<p>C 스타일 배열은, 겉보기와 달리 <strong>값 전달이 아니라 주소 전달</strong>됩니다.</p>
</blockquote>
<p>이제 같은 원소 타입이지만 길이가 다른 두 배열을 생각해 봅시다.
예를 들어 <code>int[5]</code>와 <code>int[7]</code>은 서로 다른 타입이라서 원래는 호환되지 않습니다.</p>
<p>하지만 둘 다 decay되면 똑같이 <code>int*</code>가 됩니다.
즉, <strong>길이 정보가 사라지기 때문에</strong> 길이가 다른 배열도 같은 방식으로 전달할 수 있게 됩니다.</p>
<blockquote>
<p><strong>핵심 통찰</strong></p>
<p>원소 타입이 같고 길이만 다른 두 C 스타일 배열은, decay되면 같은 포인터 타입으로 바뀝니다.</p>
</blockquote>
<p>다음 예제에서는 두 가지를 보여 줍니다.</p>
<ol>
<li>길이가 다른 배열들을 하나의 함수에 넘길 수 있다는 것</li>
<li>배열을 받는 함수 매개변수는 배열 원소 타입의 <code>(const) 포인터</code>로 선언할 수 있다는 것</li>
</ol>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printElementZero(const int* arr) // const 주소 전달
{
    std::cout &lt;&lt; arr[0];
}

int main()
{
    const int prime[] { 2, 3, 5, 7, 11 };
    const int squares[] { 1, 4, 9, 25, 36, 49, 64, 81 };

    printElementZero(prime);   // prime은 const int* 포인터로 decay됨
    printElementZero(squares); // squares는 const int* 포인터로 decay됨

    return 0;
}</code></pre>
<p>이 예제는 잘 동작하고, 다음을 출력합니다.</p>
<pre><code class="language-text">2
1</code></pre>
<p><code>main()</code> 안에서 <code>printElementZero(prime)</code>를 호출하면, <code>prime</code>은 <code>const int[5]</code> 배열에서 <code>const int*</code> 포인터로 decay됩니다.
이 포인터는 <code>prime</code>의 첫 번째 원소 주소를 담고 있습니다.</p>
<p>마찬가지로 <code>printElementZero(squares)</code>를 호출하면, <code>squares</code>는 <code>const int[8]</code>에서 <code>const int*</code>로 decay됩니다.
그리고 역시 첫 번째 원소의 주소를 담습니다.</p>
<p>함수에 실제로 전달되는 것은 바로 이런 <code>const int*</code> 포인터들입니다.</p>
<p>그래서 <code>printElementZero()</code> 함수의 매개변수도 같은 포인터 타입인 <code>const int*</code>여야 합니다.</p>
<p>이 함수 안에서는 그 포인터에 첨자 <code>[]</code>를 써서 원하는 배열 원소에 접근합니다.</p>
<p>C 스타일 배열은 주소로 전달되기 때문에, 함수는 <strong>복사본이 아니라 원본 배열</strong>에 직접 접근합니다.
따라서 함수가 배열 원소를 바꿀 수도 있습니다.</p>
<p>그래서 함수가 배열을 수정할 생각이 없다면, 매개변수에 <code>const</code>를 붙이는 것이 좋습니다.</p>
<hr>
<h3 id="c-스타일-배열을-받는-함수-매개변수-문법">C 스타일 배열을 받는 함수 매개변수 문법</h3>
<p>매개변수를 <code>int* arr</code>처럼 선언하면, <code>arr</code>가 “정수 하나를 가리키는 포인터”인지, “배열의 첫 번째 원소를 가리키는 포인터”인지 한눈에 잘 드러나지 않습니다.</p>
<p>그래서 C 스타일 배열을 받을 때는, 보통 대체 문법인 <code>int arr[]</code>를 사용하는 편이 더 읽기 쉽습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printElementZero(const int arr[]) // const int*와 똑같이 처리됨
{
    std::cout &lt;&lt; arr[0];
}

int main()
{
    const int prime[] { 2, 3, 5, 7, 11 };
    const int squares[] { 1, 4, 9, 25, 36, 49, 64, 81 };

    printElementZero(prime);   // prime은 포인터로 decay됨
    printElementZero(squares); // squares는 포인터로 decay됨

    return 0;
}</code></pre>
<p>이 프로그램은 바로 앞 예제와 완전히 똑같이 동작합니다.
컴파일러는 함수 매개변수 <code>const int arr[]</code>를 <code>const int*</code>와 같은 의미로 해석하기 때문입니다.</p>
<p>하지만 <code>arr[]</code> 문법에는 장점이 있습니다.
이 매개변수가 “값 하나를 가리키는 포인터”가 아니라, <strong>decay된 C 스타일 배열을 받기 위한 것</strong>이라는 뜻을 더 잘 전달해 줍니다.</p>
<p>대괄호 안에는 길이 정보를 쓸 필요가 없습니다.
어차피 사용되지 않기 때문입니다.
길이를 써도 무시됩니다.</p>
<blockquote>
<p><strong>권장 사항</strong></p>
<p>C 스타일 배열을 기대하는 함수 매개변수는 포인터 문법(<code>int* arr</code>)보다 배열 문법(<code>int arr[]</code>)을 사용하는 것이 좋습니다.</p>
</blockquote>
<p>다만 이 문법의 단점도 있습니다.
겉으로는 배열처럼 보여서, 사실은 이미 decay되어 포인터가 되었다는 점이 덜 눈에 띕니다.
그래서 decay된 배열에서는 기대대로 동작하지 않는 일을 실수로 하기가 더 쉽습니다.
이제 그런 문제들을 보겠습니다.</p>
<hr>
<h3 id="array-decay의-문제점">array decay의 문제점</h3>
<p>array decay는 길이가 다른 C 스타일 배열을 복사 없이 함수에 넘길 수 있게 해 준 영리한 해결책이었습니다. 하지만 배열 길이 정보가 사라지기 때문에, 여러 종류의 실수가 생기기 쉽습니다.</p>
<p>첫 번째 문제는 <code>sizeof()</code>입니다.
배열과 decay된 배열(즉 포인터)에 대해 <code>sizeof()</code>는 서로 다른 값을 돌려줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printArraySize(int arr[])
{
    std::cout &lt;&lt; sizeof(arr) &lt;&lt; &#39;\n&#39;; // 4 출력 (주소가 32비트라고 가정)
}

int main()
{
    int arr[]{ 3, 2, 1 };

    std::cout &lt;&lt; sizeof(arr) &lt;&lt; &#39;\n&#39;; // 12 출력 (int가 4바이트라고 가정)

    printArraySize(arr);

    return 0;
}</code></pre>
<p>즉, C 스타일 배열에 <code>sizeof()</code>를 쓰는 것은 꽤 위험할 수 있습니다.
지금 내가 다루는 것이 <strong>진짜 배열 객체인지</strong>, 아니면 <strong>이미 decay된 포인터인지</strong>를 확실히 알아야 하기 때문입니다.</p>
<p>이전 단원 <strong>17.7 -- Introduction to C-style arrays</strong>에서는, 예전부터 <code>sizeof(arr) / sizeof(*arr)</code>를 C 스타일 배열 길이를 구하는 꼼수처럼 써 왔다고 말했습니다.</p>
<p>하지만 이 방식은 위험합니다.
<code>arr</code>가 이미 decay되었다면, <code>sizeof(arr)</code>는 배열 크기가 아니라 <strong>포인터 크기</strong>를 반환합니다.
그러면 배열 길이를 잘못 계산하게 되고, 프로그램이 오작동할 가능성이 큽니다.</p>
<p>다행히 C++17의 <code>std::size()</code>(그리고 C++20의 <code>std::ssize()</code>)는 포인터를 넘기면 컴파일 자체를 막아 줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int printArrayLength(int arr[])
{
    std::cout &lt;&lt; std::size(arr) &lt;&lt; &#39;\n&#39;; // 컴파일 오류: std::size()는 포인터에서 동작하지 않음
}

int main()
{
    int arr[]{ 3, 2, 1 };

    std::cout &lt;&lt; std::size(arr) &lt;&lt; &#39;\n&#39;; // 3 출력

    printArrayLength(arr);

    return 0;
}</code></pre>
<p>두 번째 문제는, 어쩌면 더 중요할 수 있는데, <strong>리팩터링</strong>을 어렵게 만든다는 점입니다.
즉, 긴 함수를 더 짧고 깔끔한 함수들로 나누는 과정에서 문제가 생기기 쉽습니다.</p>
<p>원래는 일반 배열로 잘 동작하던 코드가, 함수로 분리되면서 decay된 배열을 다루게 되면:</p>
<ul>
<li>컴파일이 안 되거나</li>
<li>더 나쁘게는, 조용히 잘못 동작할 수도 있습니다</li>
</ul>
<p>세 번째 문제는 길이 정보가 없어서 생기는 여러 실전 문제들입니다.
배열 길이를 모르면, 그 배열이 충분히 긴지 확인하기가 어렵습니다.</p>
<p>사용자는 함수가 기대하는 길이보다 짧은 배열을 넘길 수도 있고, 심지어 배열이 아니라 값 하나의 주소를 넘길 수도 있습니다.
그런데 함수가 아무 검증 없이 <code>arr[2]</code> 같은 접근을 해 버리면, <strong>정의되지 않은 동작(undefined behavior)</strong>이 발생할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printElement2(int arr[])
{
    // arr에 원소가 최소 3개 있는지 어떻게 보장할까?
    std::cout &lt;&lt; arr[2] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int a[]{ 3, 2, 1 };
    printElement2(a);  // ok

    int b[]{ 7, 6 };
    printElement2(b);  // 컴파일은 되지만 정의되지 않은 동작 발생

    int c{ 9 };
    printElement2(&amp;c); // 컴파일은 되지만 정의되지 않은 동작 발생

    return 0;
}</code></pre>
<p>배열 길이를 모르면, 배열을 처음부터 끝까지 순회하는 것도 어렵습니다.
어디까지 가야 끝인지 알 수 없기 때문입니다.</p>
<p>물론 이런 문제를 피하는 방법은 있습니다.
하지만 그런 방법들은 프로그램을 더 복잡하게 만들고, 더 약하게(깨지기 쉽게) 만들기도 합니다.</p>
<hr>
<h3 id="배열-길이-문제를-우회하는-방법">배열 길이 문제를 우회하는 방법</h3>
<p>전통적으로 프로그래머들은 배열 길이 정보가 없다는 문제를 두 가지 방식 중 하나로 해결해 왔습니다.</p>
<p>첫 번째 방법은 <strong>배열과 배열 길이를 따로따로 넘기는 것</strong>입니다.</p>
<pre><code class="language-cpp">#include &lt;cassert&gt;
#include &lt;iostream&gt;

void printElement2(const int arr[], int length)
{
    assert(length &gt; 2 &amp;&amp; &quot;printElement2: Array too short&quot;); // length에는 static_assert를 쓸 수 없음

    std::cout &lt;&lt; arr[2] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    constexpr int a[]{ 3, 2, 1 };
    printElement2(a, static_cast&lt;int&gt;(std::size(a)));  // ok

    constexpr int b[]{ 7, 6 };
    printElement2(b, static_cast&lt;int&gt;(std::size(b)));  // assert 발생

    return 0;
}</code></pre>
<p>하지만 이 방법도 여전히 여러 문제가 있습니다.</p>
<ul>
<li>호출하는 쪽에서 배열과 길이를 <strong>맞는 쌍으로</strong> 넘겨야 합니다.
길이를 잘못 넘기면 함수는 여전히 오작동할 수 있습니다.</li>
<li><code>std::size()</code>나 <code>std::size_t</code>를 반환하는 길이 함수와 함께 쓰면, 부호(sign) 변환 문제가 생길 수 있습니다.</li>
<li>런타임 <code>assert</code>는 실제 실행 중 그 코드가 지나갈 때만 잡힙니다.
테스트에서 놓친 호출 경로가 있으면, 사용자가 실제로 실행했을 때 터질 수도 있습니다.</li>
<li>현대 C++이라면 <code>constexpr</code> 배열 길이를 컴파일 타임에 검사하려고 <code>static_assert</code>를 쓰고 싶지만, 함수 매개변수는 <code>constexpr</code>가 될 수 없어서(심지어 <code>constexpr</code> 또는 <code>consteval</code> 함수 안에서도) 쉽지 않습니다.</li>
<li>이 방법은 <strong>명시적인 함수 호출</strong>일 때만 쓸 수 있습니다.
만약 함수 호출이 암묵적이라면(예: 배열을 피연산자로 쓰는 연산자 호출), 길이를 따로 넘길 기회가 없습니다.</li>
</ul>
<p>두 번째 방법은, <strong>의미상 유효하지 않은 특별한 값</strong>을 배열 끝 표시로 쓰는 것입니다.
예를 들어 시험 점수에 <code>-1</code>은 말이 안 된다고 하면, <code>-1</code>을 “여기가 끝”이라고 표시하는 식입니다.</p>
<p>이렇게 하면 배열 길이는 시작부터 그 끝 표시 원소까지 세어서 구할 수 있습니다.
배열 순회도 끝 표시가 나올 때까지 하면 됩니다.</p>
<p>이 방법의 좋은 점은, 암묵적인 함수 호출에서도 동작한다는 것입니다.</p>
<blockquote>
<p><strong>핵심 통찰</strong></p>
<p>C 스타일 문자열도 C 스타일 배열이며, 문자열 끝을 표시하기 위해 <strong>널 종료 문자</strong>를 사용합니다.
그래서 문자열이 decay되더라도 끝을 찾으면서 순회할 수 있습니다.</p>
</blockquote>
<p>하지만 이 방법도 문제점이 많습니다.</p>
<ul>
<li>끝 표시 원소가 없으면, 순회가 배열 끝을 넘어가 버려서 정의되지 않은 동작이 발생합니다.</li>
<li>배열을 순회하는 함수는 끝 표시 원소를 특별 취급해야 합니다.
예를 들어 C 스타일 문자열 출력 함수는 종료 문자를 출력하면 안 됩니다.</li>
<li>실제 배열 길이와, 의미 있는 원소 개수가 서로 다릅니다.
길이를 잘못 다루면, 의미 없는 끝 표시 원소를 일반 데이터처럼 처리할 수도 있습니다.</li>
<li>이 방식은 “의미상 절대 쓸 수 없는 값”이 있을 때만 가능합니다.
그런데 실제로는 그런 값이 없는 경우도 많습니다.</li>
</ul>
<hr>
<h3 id="대부분의-경우-c-스타일-배열은-피하는-것이-좋다">대부분의 경우 C 스타일 배열은 피하는 것이 좋다</h3>
<p>C 스타일 배열은 전달 방식도 일반적이지 않습니다.
겉보기에는 값 전달 같지만 실제로는 주소 전달입니다.
그리고 decay가 일어나면 길이 정보도 사라집니다.</p>
<p>이런 이유들 때문에 C 스타일 배열은 점점 덜 쓰이게 되었습니다.
가능하면 피하는 것을 권장합니다.</p>
<blockquote>
<p><strong>권장 사항</strong></p>
<p>가능하다면 C 스타일 배열은 피하세요.</p>
<ul>
<li>읽기 전용 문자열(문자열 리터럴 심볼 상수, 문자열 매개변수)에는 <code>std::string_view</code>를 우선 사용하세요.</li>
<li>수정 가능한 문자열에는 <code>std::string</code>을 우선 사용하세요.</li>
<li>전역이 아닌 <code>constexpr</code> 배열에는 <code>std::array</code>를 우선 사용하세요.</li>
<li><code>constexpr</code>가 아닌 배열에는 <code>std::vector</code>를 우선 사용하세요.</li>
</ul>
<p>전역 <code>constexpr</code> 배열에는 C 스타일 배열을 사용해도 괜찮습니다. 이것은 바로 아래에서 설명합니다.</p>
</blockquote>
<blockquote>
<p><strong>참고로...</strong></p>
<p>C++에서는 배열을 참조로 전달할 수 있습니다.
이 경우 함수에 넘길 때 배열이 decay되지 않습니다.
(다만 배열에 대한 참조도, 값을 평가하면 결국 decay될 수 있습니다.)</p>
<p>하지만 이 방식을 항상 빠짐없이 적용해야 하는데, 한 번이라도 참조를 빼먹으면 decay가 발생합니다.
게다가 배열 참조 매개변수는 길이가 고정되어 있어야 하므로, 함수가 <strong>특정 길이의 배열 하나만</strong> 처리할 수 있습니다.</p>
<p>길이가 다른 배열도 처리하려면 함수 템플릿까지 써야 합니다.
그런데 그렇게까지 해서 C 스타일 배열을 “고칠” 바에는, 그냥 <code>std::array</code>를 쓰는 편이 낫습니다.</p>
</blockquote>
<hr>
<h3 id="그렇다면-현대-c에서는-언제-c-스타일-배열을-사용할까">그렇다면 현대 C++에서는 언제 C 스타일 배열을 사용할까?</h3>
<p>현대 C++에서 C 스타일 배열은 보통 두 경우에 사용됩니다.</p>
<p>첫 번째는 <strong><code>constexpr</code> 전역 데이터</strong>(또는 <code>constexpr static local</code> 데이터)를 저장할 때입니다.
이런 배열은 프로그램 어디서든 직접 접근할 수 있어서, 함수로 전달할 일이 적고, 따라서 decay 문제도 피하기 쉽습니다.</p>
<p>또한 C 스타일 배열 문법이 <code>std::array</code>보다 약간 덜 번거롭게 느껴질 수도 있습니다.
더 중요한 점은, 이런 배열에 인덱싱할 때 표준 라이브러리 컨테이너들처럼 부호 변환 문제를 덜 겪는다는 것입니다.</p>
<p>두 번째는, 함수나 클래스가 <strong><code>constexpr</code>이 아닌 C 스타일 문자열 인수</strong>를 직접 처리하고 싶을 때입니다.
즉, 굳이 <code>std::string_view</code>로 바꾸라고 요구하지 않는 경우입니다.</p>
<p>이유는 두 가지가 있을 수 있습니다.</p>
<p>첫째, <code>constexpr</code>이 아닌 C 스타일 문자열을 <code>std::string_view</code>로 바꾸려면, 문자열 길이를 알아내기 위해 문자열을 한 번 끝까지 훑어야 합니다.
만약 함수가 성능이 매우 중요한 코드 구간에 있고, 길이 정보가 딱히 필요 없다면(예: 어차피 함수가 직접 문자열을 끝까지 순회할 예정이라면), 이 변환을 생략하는 것이 도움이 될 수 있습니다.</p>
<p>둘째, 그 함수나 클래스가 내부적으로 또 다른 <strong>C 스타일 문자열을 기대하는 함수들</strong>을 호출한다면,
굳이 <code>std::string_view</code>로 바꿨다가 다시 C 스타일 문자열 방식으로 맞추는 과정이 비효율적일 수 있습니다.
(물론 <code>std::string_view</code>를 꼭 써야 할 다른 이유가 있다면 이야기는 달라집니다.)</p>
<hr>
<h2 id="179--포인터-연산과-배열-인덱싱첨자">17.9 — 포인터 연산과 배열 인덱싱(첨자)</h2>
<p>이전 레슨 &#39;16.1 -- 컨테이너와 배열 소개&#39;에서 배열은 메모리 상에 순서대로 나란히 저장된다고 배웠습니다. 이번 레슨에서는 배열의 인덱스(몇 번째 요소인지 나타내는 번호)가 수학적으로 어떻게 작동하는지 조금 더 깊이 파헤쳐 보겠습니다.</p>
<p>앞으로의 레슨에서 이 인덱싱 수학을 직접 쓸 일은 많지 않겠지만, 이번 내용을 알아두면 <strong>범위 기반 for 문(range-based for loops)</strong>이 실제로 어떻게 작동하는지 이해하는 데 큰 도움이 됩니다. 또한 나중에 &#39;반복자(iterators)&#39;라는 개념을 배울 때 아주 유용하게 쓰일 거예요!</p>
<hr>
<h3 id="포인터-연산이란-무엇인가요">포인터 연산이란 무엇인가요?</h3>
<p><strong>포인터 연산</strong>이란 포인터(메모리 주소를 담는 변수)에 덧셈, 뺄셈, 1 증가(++), 1 감소(--) 같은 간단한 수학 계산을 해서 <strong>새로운 메모리 주소를 만들어내는 기능</strong>입니다.</p>
<p>어떤 포인터 <code>ptr</code>이 있다고 해볼게요. <code>ptr + 1</code>을 하면 단순히 주소 숫자에 1이 더해지는 게 아니라, 메모리 상에 있는 <strong>바로 다음 데이터의 주소</strong>를 알려줍니다. 
(포인터가 가리키는 데이터의 종류에 따라 몇 바이트를 건너뛸지 결정됩니다)</p>
<p>예를 들어, <code>ptr</code>이 정수형 포인터(<code>int*</code>)이고 정수(<code>int</code>)가 4바이트 크기라면, <code>ptr + 1</code>은 <code>ptr</code>의 주소에서 딱 4바이트 뒤에 있는 메모리 주소를 반환합니다. 마찬가지로 <code>ptr + 2</code>는 8바이트 뒤의 메모리 주소를 알려주겠죠.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x {};
    const int* ptr{ &amp;x }; // int가 4바이트라고 가정해 봅시다

    std::cout &lt;&lt; ptr &lt;&lt; &#39; &#39; &lt;&lt; (ptr + 1) &lt;&lt; &#39; &#39; &lt;&lt; (ptr + 2) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>원문 작성자의 컴퓨터에서는 이 코드가 다음과 같이 출력되었습니다.</p>
<p><code>00AFFD80 00AFFD84 00AFFD88</code></p>
<p>출력된 주소들을 잘 보면, 이전 주소보다 정확히 4바이트씩 커졌다는 것을 알 수 있습니다.</p>
<p>자주 쓰이진 않지만, 포인터 연산은 뺄셈도 가능합니다. 포인터 <code>ptr</code>이 있을 때, <code>ptr - 1</code>은 메모리 상에 있는 <strong>바로 앞 데이터의 주소</strong>를 알려줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x {};
    const int* ptr{ &amp;x }; // int가 4바이트라고 가정해 봅시다

    std::cout &lt;&lt; ptr &lt;&lt; &#39; &#39; &lt;&lt; (ptr - 1) &lt;&lt; &#39; &#39; &lt;&lt; (ptr - 2) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>원문 작성자의 컴퓨터에서는 다음과 같이 출력되었습니다.</p>
<p><code>00AFFD80 00AFFD7C 00AFFD78</code></p>
<p>이 경우에는 주소가 4바이트씩 작아진 것을 볼 수 있습니다. 
(참고로 C는 16진수에서 12를, 8은 8을 의미하므로 차이가 4입니다!)</p>
<blockquote>
<p><strong>핵심 통찰</strong>
포인터 연산은 단순히 주소 숫자에서 1을 더하거나 빼는 것이 아닙니다. 포인터가 가리키는 자료형의 크기를 기준으로, <strong>다음/이전 데이터 객체의 주소</strong>를 반환하는 것입니다.</p>
</blockquote>
<p>포인터에 증가 연산자(<code>++</code>)나 감소 연산자(<code>--</code>)를 붙이면 앞서 본 덧셈/뺄셈과 똑같이 작동합니다. 다만, 차이점은 포인터 변수 자체가 가지고 있는 <strong>주소 값을 실제로 변경</strong>한다는 것입니다.</p>
<p>정수 <code>x</code>가 있을 때 <code>++x</code>는 <code>x = x + 1</code>을 짧게 쓴 것이죠? 마찬가지로 포인터 <code>ptr</code>이 있을 때 <code>++ptr</code>은 <code>ptr = ptr + 1</code>을 짧게 쓴 것입니다. 포인터 연산을 한 다음 그 결과를 다시 <code>ptr</code>에 집어넣는 거죠.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x {};
    const int* ptr{ &amp;x }; // int가 4바이트라고 가정해 봅시다

    std::cout &lt;&lt; ptr &lt;&lt; &#39;\n&#39;;

    ++ptr; // ptr = ptr + 1
    std::cout &lt;&lt; ptr &lt;&lt; &#39;\n&#39;;

    --ptr; // ptr = ptr - 1
    std::cout &lt;&lt; ptr &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>원문 작성자의 컴퓨터에서는 이렇게 출력되었습니다.</p>
<p><code>00AFFD80 00AFFD84 00AFFD80</code></p>
<blockquote>
<p><strong>주의 사항</strong>
엄밀히 말해서, 위 코드는 &#39;정의되지 않은 동작&#39;입니다. C++ 표준에 따르면 포인터 연산은 포인터와 그 결과값이 <strong>같은 배열 안에 속해 있을 때(혹은 배열의 마지막 요소 바로 다음을 가리킬 때)만</strong> 안전하게 작동한다고 정의되어 있습니다. 하지만 요즘 나오는 대부분의 C++ 컴파일러들은 이 규칙을 강제로 막지 않아서, 배열 바깥에서 포인터 연산을 해도 에러를 띄우지는 않는 경우가 많습니다. (그래도 조심하는 게 좋습니다!)</p>
</blockquote>
<hr>
<h3 id="배열의-인덱스는-사실-포인터-연산입니다">배열의 인덱스([])는 사실 포인터 연산입니다</h3>
<p>이전 레슨 (17.8 -- C 스타일 배열 붕괴)에서 우리는 배열의 인덱스 기호(<code>operator[]</code>)를 포인터에도 사용할 수 있다고 배웠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int arr[] { 9, 7, 5, 3, 1 };

    const int* ptr{ arr }; // 0번째 요소의 주소를 담고 있는 일반적인 포인터
    std::cout &lt;&lt; ptr[2];   // ptr의 인덱스 2에 접근하여 요소를 가져옵니다. 5가 출력됩니다.

    return 0;
}
</code></pre>
<p>여기서 무슨 일이 일어나고 있는지 조금 더 자세히 들여다볼게요.</p>
<p>알고 보면 <code>ptr[n]</code> 이라는 인덱스 방식은, <code>*((ptr) + (n))</code> 이라는 복잡한 코드를 아주 짧고 예쁘게 줄여 쓴 것에 불과합니다. 모양만 다를 뿐 사실 <strong>포인터 연산</strong>이라는 뜻이죠! </p>
<p>괄호는 연산이 올바른 순서로 되도록 막아준 것이고, 앞에 붙은 별표(<code>*</code>)는 그 주소에 있는 실제 값을 꺼내오는(역참조) 역할을 합니다.</p>
<p>과정을 쉽게 설명해 드릴게요:</p>
<ol>
<li>먼저 <code>ptr</code>에 <code>arr</code>을 넣어 초기화합니다. 배열 <code>arr</code>이 초기화에 사용되면, 배열은 &#39;0번째 요소의 주소를 담은 포인터&#39;로 슬쩍 변신(붕괴)합니다. 그래서 이제 <code>ptr</code>은 0번째 요소의 주소를 가지게 됩니다.</li>
<li>그다음 <code>ptr[2]</code>를 출력합니다. <code>ptr[2]</code>는 <code>*((ptr) + (2))</code>와 같고, 이는 <code>*(ptr + 2)</code>와 같습니다. <code>ptr + 2</code>는 <code>ptr</code>에서 2칸 떨어진 데이터의 주소, 즉 &#39;인덱스 2번&#39; 요소의 주소를 찾아줍니다. 그리고 앞에 붙은 <code>*</code> 덕분에 그 주소 안에 있는 진짜 값(여기서는 5)이 뿅 하고 나타나는 것입니다.</li>
</ol>
<p>다른 예시를 한 번 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int arr[] { 3, 2, 1 };

    // 먼저, 인덱스([])를 사용해 배열 요소의 주소와 값을 가져와 봅시다
    std::cout &lt;&lt; &amp;arr[0] &lt;&lt; &#39; &#39; &lt;&lt; &amp;arr[1] &lt;&lt; &#39; &#39; &lt;&lt; &amp;arr[2] &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; arr[0] &lt;&lt; &#39; &#39; &lt;&lt; arr[1] &lt;&lt; &#39; &#39; &lt;&lt; arr[2] &lt;&lt; &#39;\n&#39;;

    // 이제 포인터 연산을 사용해서 똑같은 작업을 해봅시다
    std::cout &lt;&lt; arr&lt;&lt; &#39; &#39; &lt;&lt; (arr+ 1) &lt;&lt; &#39; &#39; &lt;&lt; (arr+ 2) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; *arr&lt;&lt; &#39; &#39; &lt;&lt; *(arr+ 1) &lt;&lt; &#39; &#39; &lt;&lt; *(arr+ 2) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>작성자의 컴퓨터에서는 이렇게 출력되었습니다.</p>
<p><code>00AFFD80 00AFFD84 00AFFD88</code>
<code>3 2 1</code>
<code>00AFFD80 00AFFD84 00AFFD88</code>
<code>3 2 1</code></p>
<p><code>arr</code>이 주소 <code>00AFFD80</code>을 가지고 있다면, <code>(arr + 1)</code>은 4바이트 뒤의 주소를, <code>(arr + 2)</code>는 8바이트 뒤의 주소를 반환한다는 것을 알 수 있습니다. 이 주소들 앞에 <code>*</code>를 붙이면(역참조) 그 위치에 있는 값(3, 2, 1)을 얻을 수 있습니다.</p>
<p>배열 요소들은 메모리상에 항상 나란히 붙어있기 때문에, <code>arr</code>이 배열의 0번째 요소를 가리키는 포인터라면 <code>*(arr + n)</code>은 자연스럽게 배열의 n번째 요소가 됩니다.</p>
<p>이것이 바로 <strong>배열의 순서가 1이 아니라 0부터 시작하는 진짜 이유</strong>입니다! 0부터 시작해야 컴퓨터가 인덱스를 계산할 때 매번 1을 빼지 않아도 되어서 수학적으로 훨씬 효율적이거든요.</p>
<blockquote>
<p><strong>참고로...</strong>
재미있는 사실 하나 알려드릴게요! 컴파일러가 <code>ptr[n]</code>을 <code>*((ptr) + (n))</code>으로 바꿔서 계산한다고 했죠? 그렇기 때문에 덧셈의 순서를 바꿔서 <code>n[ptr]</code> 이라고 적어도 똑같이 작동합니다! 컴파일러가 이를 <code>*((n) + (ptr))</code>로 바꾸기 때문이죠. 하지만 코드를 읽는 사람을 엄청나게 헷갈리게 만드니까 <strong>절대 이렇게 쓰지는 마세요!</strong></p>
</blockquote>
<hr>
<h3 id="음수-인덱스도-가능하다고요">음수 인덱스도 가능하다고요?</h3>
<p>지난 레슨에서 C 스타일 배열의 인덱스에는 양수(부호 없는 정수)뿐만 아니라 음수(부호 있는 정수)도 쓸 수 있다고 말씀드렸습니다. 단순한 편의를 위해서가 아닙니다. C 스타일 배열에서는 실제로 <strong>음수 인덱스를 쓰는 것이 가능</strong>하기 때문입니다. 이상하게 들리겠지만, 원리를 알면 말이 됩니다.</p>
<p>방금 <code>*(ptr+1)</code>이 메모리에서 <strong>다음 데이터</strong>를 가져오고, <code>ptr[1]</code>이 그것과 똑같은 기능이라고 배웠죠?</p>
<p>그렇다면 레슨 맨 처음에 배운 <code>*(ptr-1)</code>은 메모리에서 <strong>이전 데이터</strong>를 가져온다는 사실도 기억하시나요? 이걸 인덱스로 짧게 쓰면 어떻게 될까요? 맞습니다. 바로 <strong><code>ptr[-1]</code></strong>이 됩니다!</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    const int arr[] { 9, 8, 7, 6, 5 };

    // ptr이 3번째 요소를 가리키도록 설정합니다
    const int* ptr { &amp;arr[3] };

    // ptr이 3번째 요소를 가리키고 있다는 것을 증명해 봅시다
    std::cout &lt;&lt; *ptr &lt;&lt; ptr[0] &lt;&lt; &#39;\n&#39;; // 66 출력
    // 놀랍게도 ptr[-1]은 2번째 요소입니다!
    std::cout &lt;&lt; *(ptr-1) &lt;&lt; ptr[-1] &lt;&lt; &#39;\n&#39;; // 77 출력

    return 0;
}
</code></pre>
<hr>
<h3 id="포인터-연산으로-배열-훑어보기-순회하기">포인터 연산으로 배열 훑어보기 (순회하기)</h3>
<p>포인터 연산이 가장 많이 쓰이는 곳 중 하나는, 인덱스 번호를 쓰지 않고 C 스타일 배열의 처음부터 끝까지 쭉 훑어보는(순회하는) 작업입니다. 다음 코드가 어떻게 작동하는지 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    constexpr int arr[]{ 9, 7, 5, 3, 1 };

    const int* begin{ arr };                // begin은 시작 요소를 가리킵니다
    const int* end{ arr + std::size(arr) }; // end는 배열의 &#39;마지막 요소 바로 다음 칸&#39;을 가리킵니다

    for (; begin != end; ++begin)           // begin부터 end에 도달하기 전까지 반복합니다
    {
        std::cout &lt;&lt; *begin &lt;&lt; &#39; &#39;;     // 현재 요소를 가져오기 위해 포인터를 역참조합니다
    }

    return 0;
}
</code></pre>
<p>위의 예제에서, 우리는 <code>begin</code> 포인터가 가리키는 요소(배열의 0번째 요소)부터 탐색을 시작합니다. <code>begin</code>과 <code>end</code>의 주소가 아직 다르므로(<code>begin != end</code>), 루프 안의 코드가 실행됩니다. 루프 안에서는 <code>*begin</code>을 통해 현재 위치의 값을 꺼내옵니다. 한 바퀴가 끝나면 <code>++begin</code>을 실행해서 포인터가 다음 요소를 가리키도록 한 칸 전진시킵니다. 이 작업은 <code>begin</code>과 <code>end</code>가 완전히 같아질 때까지 계속 반복됩니다.</p>
<p>따라서 위 코드는 다음과 같이 출력됩니다.
<code>9 7 5 3 1</code></p>
<p>여기서 <code>end</code> 포인터가 배열의 <strong>&#39;마지막 요소 바로 다음 칸(one-past-the-end)&#39;</strong>을 가리키도록 설정된 것에 주목하세요. 그 위치에 실제 데이터가 없더라도, 그 주소를 가지고만 있는 것은 문제가 되지 않습니다 (단, 그 주소에서 값을 꺼내오려고(<code>*end</code>) 하면 안 됩니다!). 이렇게 설정하면 계산이나 조건 비교가 아주 깔끔해져서 어디서 +1이나 -1을 억지로 해줄 필요가 없습니다.</p>
<blockquote>
<p><strong>팁</strong>
C 스타일 배열을 가리키는 포인터에서 포인터 연산을 할 때, 계산된 주소값이 <strong>유효한 배열 요소의 주소</strong>이거나 <strong>마지막 요소 바로 다음 칸</strong>이라면 안전합니다. 하지만 이 범위를 벗어난 주소를 계산하게 되면, (그 주소의 값을 읽으려 시도하지 않더라도) 정의되지 않은 동작(undefined behavior)이 발생하므로 주의해야 합니다.</p>
</blockquote>
<p>이전 레슨 &#39;17.8 -- C 스타일 배열 붕괴&#39;에서, 배열이 포인터로 변해버리면(붕괴하면) 배열의 전체 크기를 구하는 <code>std::size</code> 같은 기능이 작동하지 않아서 코드를 수정하기 어렵다고 했습니다. 하지만 이렇게 시작(<code>begin</code>)과 끝(<code>end</code>) 포인터로 배열을 순회하는 방식을 쓰면, 반복문 부분을 깔끔하게 별도의 함수로 분리할 수 있습니다!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printArray(const int* begin, const int* end)
{
    for (; begin != end; ++begin)   // begin부터 end에 도달하기 전까지 반복합니다
    {
        std::cout &lt;&lt; *begin &lt;&lt; &#39; &#39;; // 현재 요소를 가져오기 위해 포인터를 역참조합니다
    }

    std::cout &lt;&lt; &#39;\n&#39;;
}

int main()
{
    constexpr int arr[]{ 9, 7, 5, 3, 1 };

    const int* begin{ arr };                // begin은 시작 요소를 가리킵니다
    const int* end{ arr + std::size(arr) }; // end는 배열의 &#39;마지막 요소 바로 다음 칸&#39;을 가리킵니다

    printArray(begin, end);

    return 0;
}
</code></pre>
<p>이 프로그램은 <code>arr</code> 배열 자체를 함수에 넘겨주지 않았는데도 아주 잘 작동합니다! 배열을 직접 전달하지 않았으니 배열이 붕괴되어 크기를 잃어버릴 걱정도 없죠. 대신 <code>begin</code>과 <code>end</code>라는 두 개의 포인터만으로 배열을 순회하는 데 필요한 모든 정보를 함수에 전달한 것입니다.</p>
<p>나중에 반복자(iterators)와 알고리즘(algorithms)을 배울 때 다시 보겠지만, C++ 표준 라이브러리에는 이렇게 <code>begin</code>과 <code>end</code> 쌍을 이용해서 데이터를 다루는 함수들이 정말 많답니다.</p>
<hr>
<h3 id="범위-기반-for-문도-사실-포인터-연산으로-만들어졌습니다">범위 기반 for 문도 사실 포인터 연산으로 만들어졌습니다</h3>
<p>다음과 같은 아주 편리한 &#39;범위 기반 for 문&#39;을 생각해 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    constexpr int arr[]{ 9, 7, 5, 3, 1 };

    for (auto e : arr)         // `begin`부터 `end`에 도달하기 전까지 반복합니다
    {
        std::cout &lt;&lt; e &lt;&lt; &#39; &#39;; // 반복 변수를 통해 현재 요소를 가져옵니다
    }

    return 0;
}
</code></pre>
<p>만약 범위 기반 for 문의 공식 문서를 찾아본다면, 컴퓨터 내부에서는 이 반복문을 대략 이런 구조로 바꿔서 실행한다는 것을 알 수 있습니다.</p>
<pre><code class="language-cpp">{
    auto __begin = begin-expr; // 시작점
    auto __end = end-expr;     // 끝점

    for ( ; __begin != __end; ++__begin)
    {
        range-declaration = *__begin; // 현재 값을 변수에 담기
        loop-statement;               // 루프 안의 실제 코드 실행
    }
}
</code></pre>
<p>이 원리를 바탕으로, 앞서 보았던 범위 기반 for 문을 내부 구현 방식대로 직접 풀어서 써보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    constexpr int arr[]{ 9, 7, 5, 3, 1 };

    auto __begin = arr;                // arr이 시작점(begin-expr)이 됩니다
    auto __end = arr + std::size(arr); // arr + std::size(arr)이 끝점(end-expr)이 됩니다

    for ( ; __begin != __end; ++__begin)
    {
        auto e = *__begin;         // e라는 변수에 현재 포인터가 가리키는 값을 복사해 넣습니다
        std::cout &lt;&lt; e &lt;&lt; &#39; &#39;;     // 여기가 루프 안에서 우리가 작성했던 코드입니다
    }

    return 0;
}
</code></pre>
<p>방금 전 &#39;포인터 연산으로 배열 훑어보기&#39; 섹션에서 우리가 직접 짰던 코드랑 완전히 똑같이 생겼죠?! 유일한 차이점이라면, 포인터에서 값을 꺼내올 때 <code>*__begin</code>을 그대로 쓰지 않고, 한 번 더 편하게 쓰기 위해 <code>e</code>라는 변수에 담아서 썼다는 것뿐입니다!</p>
<hr>
<h2 id="1710--c-스타일-문자열">17.10 — C 스타일 문자열</h2>
<p>이전 17.7 레슨(C 스타일 배열 소개)에서 우리는 여러 개의 데이터를 순서대로 모아두는 
&#39;C 스타일 배열&#39;에 대해 배웠습니다.
<code>int testScore[30] {}; // 30개의 정수를 담을 수 있는 배열 (인덱스 번호는 0부터 29까지)</code></p>
<p>그리고 5.2 레슨(리터럴)에서는 &quot;Hello, world!&quot;처럼 문자들이 순서대로 모여 있는 것을 &#39;문자열&#39;이라고 불렀고, &#39;C 스타일 문자열 리터럴&#39;이라는 개념도 소개했었죠. </p>
<p>여기서 &quot;Hello, world!&quot;라는 문자열은 <code>const char[14]</code>라는 타입을 가집니다. 눈에 보이는 13개의 글자에, <strong>문자열의 끝을 알리는 숨겨진 &#39;널 종료 문자(null-terminator)&#39; 1개가 더해진 거예요.</strong></p>
<p>혹시 눈치채셨나요? C 스타일 문자열은 사실 요소의 타입이 <code>char</code>나 <code>const char</code>로 이루어진 <strong>C 스타일 배열일 뿐입니다!</strong></p>
<p>우리가 코드 안에 &quot;안녕&quot;처럼 따옴표로 묶인 C 스타일 문자열 리터럴을 쓰는 건 괜찮습니다. 하지만 C 스타일 문자열 &#39;객체(변수)&#39;를 직접 다루는 것은 요즘 현대 C++에서는 잘 쓰이지 않습니다. 다루기 까다롭고 위험하기 때문이죠. (그래서 요즘은 더 안전하고 편한 <code>std::string</code>이나 <code>std::string_view</code>를 주로 사용합니다). 그럼에도 불구하고 예전에 쓰인 코드들에서는 여전히 이 C 스타일 문자열을 자주 마주치게 될 테니, 꼭 알고 넘어가야 합니다.</p>
<p>그래서 이번 레슨에서는 현대 C++에서 C 스타일 문자열 객체를 다룰 때 꼭 알아야 할 가장 중요한 핵심들을 살펴보겠습니다.</p>
<hr>
<h3 id="c-스타일-문자열-정의하기">C 스타일 문자열 정의하기</h3>
<p>C 스타일 문자열 변수를 만드는 방법은 간단합니다. 그냥 <code>char</code> (또는 <code>const char</code>, <code>constexpr char</code>) 타입의 C 스타일 배열을 선언하면 됩니다.</p>
<pre><code class="language-cpp">char str1[8]{};                    // 8개의 문자를 담는 배열, 인덱스는 0부터 7까지
const char str2[]{ &quot;string&quot; };     // 7개의 문자를 담는 배열, 인덱스는 0부터 6까지
constexpr char str3[] { &quot;hello&quot; }; // 6개의 문자를 담는 배열, 인덱스는 0부터 5까지
</code></pre>
<p>항상 기억하세요! 눈에 보이지 않는 끝맺음 문자인 <strong>&#39;널 종료 문자&#39;를 담기 위해 늘 빈칸 1개가 더 필요합니다.</strong></p>
<p>초기값을 넣어서 C 스타일 문자열을 만들 때는, 배열의 크기(길이)를 숫자로 직접 쓰지 말고 컴파일러가 알아서 계산하게 두는 것을 강력히 추천합니다. 이렇게 하면 나중에 문자열의 내용이 길어지거나 짧아져도 숫자를 일일이 수정할 필요가 없고, 널 종료 문자를 위한 공간을 깜빡 잊어버릴 위험도 없으니까요.</p>
<hr>
<h3 id="c-스타일-문자열은-붕괴decay합니다">C 스타일 문자열은 붕괴(Decay)합니다</h3>
<p>17.8 레슨에서 우리는 C 스타일 배열이 대부분의 상황에서 &#39;포인터&#39;로 변해버리는 현상(이를 <strong>붕괴</strong>라고 부릅니다)을 배웠습니다. C 스타일 문자열도 결국 배열이기 때문에 똑같이 포인터로 퇴화해 버립니다.</p>
<p>C 스타일 문자열 리터럴은 <code>const char*</code>로 퇴화하고, C 스타일 문자열 배열은 배열이 값을 바꿀 수 있는지(<code>const</code> 여부)에 따라 <code>const char*</code>나 <code>char*</code>로 퇴화합니다. 이렇게 문자열이 포인터로 모습이 바뀌어버리면, 문자열이 원래 얼마나 길었는지에 대한 <strong>길이 정보가 싹 사라지게 됩니다.</strong></p>
<p>이처럼 길이 정보를 잃어버리기 때문에, C 스타일 문자열의 끝에는 항상 &#39;널 종료 문자&#39;가 달려있는 것입니다. 비록 길이가 몇인지 잊어버렸더라도, 처음부터 시작해서 이 널 종료 문자가 나올 때까지 글자 수를 하나하나 세면 (비효율적이긴 하지만) 길이를 다시 알아낼 수 있으니까요.</p>
<hr>
<h3 id="c-스타일-문자열-출력하기">C 스타일 문자열 출력하기</h3>
<p>C 스타일 문자열을 화면에 띄울 때, <code>std::cout</code>은 이 &#39;널 종료 문자&#39;를 만날 때까지 글자들을 계속 출력합니다. 널 종료 문자가 &quot;여기가 문자열의 끝이야!&quot;라고 표시해주기 때문에, 길이를 잃어버리고 퇴화한 문자열이라도 문제없이 끝까지 출력할 수 있는 겁니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(char ptr[]){
    std::cout &lt;&lt; ptr &lt;&lt; &#39;\n&#39;; // 문자열을 출력합니다
}

int main(){
    char str[]{ &quot;string&quot; };
    std::cout &lt;&lt; str &lt;&lt; &#39;\n&#39;; // 문자열을 출력합니다

    print(str);

    return 0;
}
</code></pre>
<p>만약 널 종료 문자가 없는 문자열을 출력하려고 하면 어떻게 될까요? (예를 들어 실수로 널 종료 문자를 다른 글자로 덮어써 버렸다면요) 결과는 &#39;정의되지 않은 동작(예측할 수 없는 치명적인 오류)&#39;으로 이어집니다. 가장 흔하게 일어나는 현상은, 원래 문자열을 다 출력하고 나서도 멈추지 않고 메모리 옆 칸을 계속 뒤지며 아무 글자나 마구잡이로 화면에 찍어내는 것입니다. 그러다 우연히 &#39;0(널 종료 문자)&#39;이 들어있는 메모리를 마주쳐야 겨우 멈추게 되죠!</p>
<hr>
<h3 id="c-스타일-문자열-입력받기">C 스타일 문자열 입력받기</h3>
<p>사용자에게 주사위를 원하는 만큼 굴린 다음, 나온 숫자들을 띄어쓰기 없이 쭉 적어보라고 한다고 상상해 봅시다 (예: 524412616). 사용자가 숫자를 몇 개나 적을까요? 우리는 전혀 알 수가 없습니다.</p>
<p>C 스타일 문자열은 한 번 크기를 정하면 늘어나거나 줄어들지 않는 &#39;고정 크기 배열&#39;이기 때문에, 이럴 땐 <strong>우리가 필요할 것 같은 최대 크기보다 훨씬 더 큰 배열을 넉넉하게 만들어 두는 것</strong>이 유일한 해결책입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    char rolls[255] {}; // 254개의 글자 + 널 종료 문자까지 담을 수 있을 만큼 충분히 큰 배열을 선언합니다
    std::cout &lt;&lt; &quot;Enter your rolls: &quot;;
    std::cin &gt;&gt; rolls;
    std::cout &lt;&lt; &quot;You entered: &quot; &lt;&lt; rolls &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>C++20 버전 이전에는 <code>std::cin &gt;&gt; rolls</code>가 띄어쓰기를 만나기 전까지 최대한 많은 글자를 <code>rolls</code> 배열에 쑤셔 넣으려고 했습니다. 문제는 사용자가 실수로든 고의로든 우리가 준비한 254글자보다 훨씬 많은 글자를 입력하는 걸 막을 방법이 없었다는 점입니다. 만약 그런 일이 발생하면, 사용자의 입력값이 <code>rolls</code> 배열의 크기를 넘쳐흘러 버리고 치명적인 오류(정의되지 않은 동작)가 발생합니다.</p>
<blockquote>
<p><strong>핵심 통찰</strong>
배열 오버플로우(Array overflow) 또는 버퍼 오버플로우(Buffer overflow)는 담을 수 있는 공간보다 더 많은 데이터가 복사될 때 발생하는 무서운 컴퓨터 보안 문제입니다. 이런 일이 생기면 허락된 공간 너머의 메모리 영역까지 덮어써 버려서 프로그램이 엉뚱하게 작동합니다. 악의적인 해커들은 이런 약점을 이용해 메모리 내용을 조작하고 자기들 입맛대로 프로그램을 오작동하게 만들기도 합니다.</p>
</blockquote>
<p>이러한 문제 때문에 C++20부터는 <code>&gt;&gt;</code> 연산자가 &#39;퇴화하지 않은(길이 정보를 그대로 가진)&#39; C 스타일 문자열을 입력받을 때만 작동하도록 규칙이 바뀌었습니다. 덕분에 딱 배열의 길이만큼만 글자를 가져올 수 있어서 넘쳐흐르는 것을 막을 수 있죠. 하지만 동시에, 이미 포인터로 퇴화해버린 C 스타일 문자열에는 더 이상 <code>&gt;&gt;</code> 연산자로 값을 입력받을 수 없다는 뜻이기도 합니다.</p>
<p>그래서 <code>std::cin</code>을 사용해 C 스타일 문자열을 읽어 들일 때 가장 추천하는 안전한 방법은 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;iterator&gt; // std::size를 사용하기 위해 포함합니다

int main(){
    char rolls[255] {}; // 254개의 글자 + 널 종료 문자까지 담을 수 있을 만큼 충분히 큰 배열을 선언합니다
    std::cout &lt;&lt; &quot;Enter your rolls: &quot;;
    std::cin.getline(rolls, std::size(rolls));
    std::cout &lt;&lt; &quot;You entered: &quot; &lt;&lt; rolls &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이렇게 <code>cin.getline()</code>을 사용하면 빈칸(띄어쓰기)을 포함해서 최대 254글자까지만 <code>rolls</code> 배열에 안전하게 읽어 들입니다. 공간을 넘어서는 나머지 글자들은 그냥 버려지죠. <code>getline()</code>은 글자를 얼마나 읽을지 길이를 정해줄 수 있기 때문에 최대 한도액을 알려줄 수 있습니다. 배열이 퇴화하지 않은 상태라면 <code>std::size()</code>를 써서 전체 크기를 쉽게 구해 넣을 수 있습니다. 하지만 퇴화된 배열이라면 다른 꼼수로 길이를 알아내야 하고, 실수로 엉뚱한 길이를 입력해버리면 프로그램이 망가지거나 보안에 구멍이 뚫릴 수 있습니다.</p>
<p>요즘의 현대 C++에서는 사용자에게 글자를 입력받아 저장할 때 <strong><code>std::string</code></strong>을 사용하는 것이 훨씬 더 안전합니다. <code>std::string</code>은 글자가 들어오는 양에 맞춰 알아서 척척 자기 크기를 늘려주기 때문입니다.</p>
<hr>
<h3 id="c-스타일-문자열-수정하기">C 스타일 문자열 수정하기</h3>
<p>여기서 한 가지 꼭 알아두어야 할 점은, C 스타일 문자열도 C 스타일 배열과 똑같은 규칙을 따른다는 것입니다. 즉, 처음 만들 때 값을 넣어서 초기화하는 것은 가능하지만, 일단 한 번 만들어진 다음에는 <code>=</code> 연산자로 새로운 통짜 문자열을 집어넣을 수 없다는 뜻입니다!</p>
<pre><code class="language-cpp">char str[]{ &quot;string&quot; }; // 정상: 처음 만들 때 초기화하는 것은 괜찮습니다
str = &quot;rope&quot;;           // 오류: 만들어진 이후에 = 으로 새로운 값을 대입하는 것은 안 됩니다!
</code></pre>
<p>이런 제약 때문에 C 스타일 문자열을 다루는 게 조금 성가시게 느껴집니다.</p>
<p>하지만 C 스타일 문자열도 본질은 결국 &#39;배열&#39;이기 때문에, <code>[]</code> 기호(인덱스)를 사용해서 문자열 안에 있는 특정 글자 딱 하나만 콕 집어서 바꾸는 건 얼마든지 가능합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    char str[]{ &quot;string&quot; };
    std::cout &lt;&lt; str &lt;&lt; &#39;\n&#39;;
    str[1] = &#39;p&#39;;
    std::cout &lt;&lt; str &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 다음과 같이 출력됩니다.
string
spring</p>
<hr>
<h3 id="c-스타일-문자열의-길이-알아내기">C 스타일 문자열의 길이 알아내기</h3>
<p>거듭 강조하지만 C 스타일 문자열은 C 스타일 배열입니다. 그래서 <code>std::size()</code> (C++20부터는 <code>std::ssize()</code>)를 사용해서 배열의 전체 길이를 알아낼 수 있습니다. 하지만 여기서 <strong>주의해야 할 두 가지 함정</strong>이 있습니다.</p>
<ol>
<li>퇴화된(포인터로 쪼그라든) 문자열에서는 이 방법이 먹히지 않습니다.</li>
<li>이 방법은 진짜 글자 수가 아니라, 숨겨진 널 종료 문자를 포함해서 &#39;이 C 스타일 배열이 메모리에서 차지하는 전체 칸 수&#39;를 알려줍니다.</li>
</ol>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    char str[255]{ &quot;string&quot; }; // 6개의 실제 글자 + 널 종료 문자
    std::cout &lt;&lt; &quot;length = &quot; &lt;&lt; std::size(str) &lt;&lt; &#39;\n&#39;; // 출력 결과: length = 255

    char *ptr { str };
    std::cout &lt;&lt; &quot;length = &quot; &lt;&lt; std::size(ptr) &lt;&lt; &#39;\n&#39;; // 컴파일 오류가 발생합니다!

    return 0;
}
</code></pre>
<p>이를 해결하는 다른 방법은 <code>&lt;cstring&gt;</code> 헤더 파일 안에 있는 <code>strlen()</code> 함수를 사용하는 것입니다. <code>strlen()</code>은 퇴화된 배열에서도 아주 잘 작동하며, 숨겨진 널 종료 문자를 뺀 &#39;순수한 글자 수&#39;만 깔끔하게 세어서 반환해 줍니다.</p>
<pre><code class="language-cpp">#include &lt;cstring&gt; // std::strlen을 사용하기 위해 포함합니다
#include &lt;iostream&gt;

int main(){
    char str[255]{ &quot;string&quot; }; // 6개의 실제 글자 + 널 종료 문자
    std::cout &lt;&lt; &quot;length = &quot; &lt;&lt; std::strlen(str) &lt;&lt; &#39;\n&#39;; // 출력 결과: length = 6

    char *ptr { str };
    std::cout &lt;&lt; &quot;length = &quot; &lt;&lt; std::strlen(ptr) &lt;&lt; &#39;\n&#39;;   // 출력 결과: length = 6

    return 0;
}
</code></pre>
<p>하지만 안타깝게도 <code>std::strlen()</code> 함수는 좀 느린 편입니다. 길이를 한 번에 바로 아는 게 아니라, 배열의 맨 처음부터 시작해서 널 종료 문자를 만날 때까지 글자를 일일이 하나씩 세어가며 발품을 팔아야 하기 때문입니다.</p>
<hr>
<h3 id="기타-c-스타일-문자열-조작-함수들">기타 C 스타일 문자열 조작 함수들</h3>
<p>C 스타일 문자열은 이름 그대로 과거 C 언어 시절의 주력 문자열 형태였습니다. 
그래서 C 언어에는 이를 쪼개고 붙이고 다루기 위한 다양한 도구(함수)들이 많이 준비되어 있죠. C++은 이 함수들을 고스란히 물려받아서 <code>&lt;cstring&gt;</code> 헤더에 잘 모아두었습니다.</p>
<p>오래된 코드들을 보다 보면 마주치게 될 유용한 옛날 함수 몇 가지를 소개합니다.</p>
<ul>
<li><code>strlen()</code> -- C 스타일 문자열의 길이(글자 수)를 알려줍니다.</li>
<li><code>strcpy()</code>, <code>strncpy()</code>, <code>strcpy_s()</code> -- 한 문자열을 다른 문자열에 덮어씁니다(복사).</li>
<li><code>strcat()</code>, <code>strncat()</code> -- 한 문자열의 맨 끝부분에 다른 문자열을 꼬리표처럼 이어 붙입니다.</li>
<li><code>strcmp()</code>, <code>strncmp()</code> -- 두 문자열이 서로 똑같은지 비교합니다. (완전히 똑같으면 <code>0</code>을 반환합니다).</li>
</ul>
<p>하지만 <code>strlen()</code>을 제외하고는, 특별한 이유가 없다면 <strong>이 함수들을 사용하지 않는 것을 강력히 권장합니다.</strong></p>
<hr>
<h3 id="const가-아닌-c-스타일-문자열-객체는-피하세요">const가 아닌 C 스타일 문자열 객체는 피하세요</h3>
<p><code>const</code>가 붙지 않아서 내용을 언제든 바꿀 수 있는(non-const) C 스타일 문자열은 꼭 써야 할 명확하고 어쩔 수 없는 이유가 없다면 피하는 것이 상책입니다. 다루기도 매우 불편할뿐더러, 아까 말했던 메모리를 넘쳐버리는 오류(오버런)가 생기기 딱 좋기 때문입니다. 이런 오류는 프로그램에 원인을 알 수 없는 고장을 일으키거나 보안 문제를 일으킬 수 있습니다.</p>
<p>메모리가 극단적으로 부족한 기기용 프로그램을 짜는 등 아주 드물게 고정된 크기의 버퍼나 C 스타일 문자열을 꼭 써야만 하는 상황이라면, 차라리 검증이 끝난 훌륭한 외부 라이브러리(3rd party fixed-length string library)를 가져다 쓰시는 것을 추천합니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
값을 수정할 수 있는(non-const) C 스타일 문자열 변수는 피하시고, 대신 훨씬 똑똑하고 안전한 <code>std::string</code>을 사용하세요.</p>
</blockquote>
<hr>
<h2 id="1711--c스타일-문자열-기호-상수">17.11 — C스타일 문자열 기호 상수</h2>
<p>이전 레슨(17.10 - C스타일 문자열)에서는 C스타일 문자열을 만들고 그 안에 값을 넣는(초기화) 방법에 대해 알아보았습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    char name[]{ &quot;Alex&quot; }; // C스타일 문자열
    std::cout &lt;&lt; name &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>C++에서는 값이 변하지 않는(상수) C스타일 문자열을 만드는 두 가지 방법을 제공합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const char name[] { &quot;Alex&quot; };        // 방법 1: C스타일 문자열 글자 그대로(리터럴) 초기화된 const C스타일 문자열
    const char* const color{ &quot;Orange&quot; }; // 방법 2: C스타일 문자열 리터럴을 가리키는 const 포인터

    std::cout &lt;&lt; name &lt;&lt; &#39; &#39; &lt;&lt; color &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같이 출력됩니다.
<code>Alex Orange</code></p>
<p>위의 두 가지 방법은 똑같은 결과를 만들어내지만, C++이 컴퓨터의 메모리(저장 공간)를 사용하는 방식에는 약간 차이가 있습니다.</p>
<p><strong>방법 1</strong>의 경우, 먼저 &quot;Alex&quot;라는 글자가 메모리 어딘가(아마도 읽기만 가능한 구역)에 저장됩니다. 그러고 나서 프로그램은 길이가 5인 배열(&#39;A&#39;, &#39;l&#39;, &#39;e&#39;, &#39;x&#39; 4글자 + 끝을 알리는 빈칸 문자 하나)을 담을 빈 공간을 새로 만들고, 그곳에 &quot;Alex&quot;를 복사해서 집어넣습니다.
결과적으로 &quot;Alex&quot;가 두 개(어딘가에 숨겨진 원본 하나, <code>name</code> 변수가 가진 복사본 하나) 생기는 셈이죠. 어차피 <code>name</code>은 <code>const</code>(상수)라서 앞으로 내용이 바뀔 일도 없는데, 굳이 복사본을 만드는 건 메모리를 낭비하는 비효율적인 방식입니다.</p>
<p><strong>방법 2</strong>의 경우, 컴파일러(코드를 번역해 주는 프로그램)마다 처리 방식이 다를 수 있습니다. 하지만 보통은 &quot;Orange&quot;라는 글자를 읽기 전용 메모리에 한 번만 떡하니 저장해 두고, 그 위치(주소)를 포인터가 가리키게만 만듭니다. (불필요한 복사본을 만들지 않아요!)</p>
<p>프로그램을 더 빠르고 효율적으로 만들기 위해(최적화), 똑같은 글자들은 하나의 값으로 묶일 수도 있습니다. 예를 들어볼까요?</p>
<pre><code class="language-cpp">const char* name1{ &quot;Alex&quot; };
const char* name2{ &quot;Alex&quot; };
</code></pre>
<p>&quot;Alex&quot;라는 글자가 두 번 쓰였죠? 이 글자들은 어차피 변하지 않는 상수이기 때문에, 컴파일러는 메모리를 아끼기 위해 &quot;Alex&quot;를 딱 한 번만 저장해 둡니다. 그리고 <code>name1</code>과 <code>name2</code>가 모두 그 똑같은 저장 위치(주소)를 가리키게 공유하도록 만듭니다.</p>
<hr>
<h3 id="const-c스타일-문자열의-타입-알아서-맞추기-타입-추론">const C스타일 문자열의 타입 알아서 맞추기 (타입 추론)</h3>
<p>C스타일 문자열에서 <code>auto</code>를 사용해 알아서 타입을 맞추게(타입 추론) 하는 것은 꽤 간단하고 직관적입니다.</p>
<pre><code class="language-cpp">auto s1{ &quot;Alex&quot; };  // const char* 타입으로 추론됨
auto* s2{ &quot;Alex&quot; }; // const char* 타입으로 추론됨
auto&amp; s3{ &quot;Alex&quot; }; // const char(&amp;)[5] 타입으로 추론됨 (길이가 5인 배열의 참조)
</code></pre>
<hr>
<h3 id="포인터와-c스타일-문자열-출력하기">포인터와 C스타일 문자열 출력하기</h3>
<p>여러분은 <code>std::cout</code>이 포인터의 종류에 따라 화면에 출력하는 방식이 조금 다르다는 걸 눈치채셨을 수도 있습니다.
다음 예시를 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int narr[]{ 9, 7, 5, 3, 1 };
    char carr[]{ &quot;Hello!&quot; };
    const char* ptr{ &quot;Alex&quot; };

    std::cout &lt;&lt; narr &lt;&lt; &#39;\n&#39;; // narr은 int* 타입으로 변환(decay)됩니다.
    std::cout &lt;&lt; carr &lt;&lt; &#39;\n&#39;; // carr은 char* 타입으로 변환(decay)됩니다.
    std::cout &lt;&lt; ptr &lt;&lt; &#39;\n&#39;;  // ptr은 이미 char* 타입입니다. (원문 주석의 name은 ptr의 오타입니다)

    return 0;
}
</code></pre>
<p>글쓴이의 컴퓨터에서는 이 코드가 다음과 같이 출력되었습니다.</p>
<pre><code class="language-text">003AF738
Hello!
Alex
</code></pre>
<p>왜 숫자(int) 배열은 메모리 주소(003AF738 같은 이상한 값)를 출력하고, 문자(char) 배열은 글자를 그대로 출력했을까요?</p>
<p>정답은 <code>std::cout</code> 같은 출력 기능이 여러분의 &#39;의도&#39;를 짐작하기 때문입니다. 문자가 아닌 다른 종류의 포인터를 넘겨주면, 포인터가 가지고 있는 원래 값(즉, 메모리 주소)을 그대로 화면에 보여줍니다.
하지만 <code>char*</code>나 <code>const char*</code> 타입(문자 포인터)을 넘겨주면, <code>std::cout</code>은 속으로 &quot;아! 이 사람은 글자를 화면에 보여주고 싶구나!&quot;라고 생각합니다. 그래서 메모리 주소를 보여주는 대신, 그 주소가 가리키고 있는 진짜 글자들을 출력해 버리는 것입니다!</p>
<p>대부분의 경우에는 이렇게 알아서 해주는 게 무척 편리하지만, 가끔은 생각지도 못한 엉뚱한 결과를 낳기도 합니다. 아래 코드를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    char c{ &#39;Q&#39; };
    std::cout &lt;&lt; &amp;c;

    return 0;
}
</code></pre>
<p>이 코드를 작성한 사람은 문자 <code>c</code>가 저장된 진짜 메모리 주소(<code>&amp;c</code>)를 확인하고 싶었을 겁니다. 하지만 <code>&amp;c</code>는 타입이 <code>char*</code>(문자 포인터)가 되어버리죠. 그래서 <code>std::cout</code>은 눈치 없이 이것을 글자로 착각하고 출력하려고 시도합니다! 문제는 문자 <code>c</code> 뒤에는 문자열이 끝났다는 표시(널 종단자, null terminator)가 없기 때문에, 프로그램이 고장 난 것처럼 엉뚱하게 작동하게 됩니다(정의되지 않은 동작).</p>
<p>글쓴이의 컴퓨터에서는 이런 식으로 외계어가 출력되었습니다.
<code>Q╠╠╠╠╜╡4;¿■A</code></p>
<p>왜 이런 짓을 한 걸까요? 먼저 <code>std::cout</code>은 <code>&amp;c</code>(문자 포인터)를 C스타일 문자열로 착각했습니다. 그래서 &#39;Q&#39;를 출력하고 멈추지 않고 계속 다음 메모리 공간으로 전진했습니다. 그 뒤의 메모리에는 알 수 없는 쓰레기 값들이 잔뜩 들어있었던 거죠. 그렇게 쓰레기 값들을 막 출력하다가, 우연히 <code>0</code>이라는 값이 들어있는 메모리를 만나고 나서야 &quot;아! 여기서 문자열이 끝났구나!&quot;라고 생각하고 출력을 멈춘 겁니다. 여러분의 컴퓨터에서는 변수 <code>c</code> 뒤에 어떤 메모리가 있느냐에 따라 전혀 다른 외계어가 보일 수도 있습니다.</p>
<p>사실 실제 코딩을 하면서 문자 변수의 메모리 주소를 출력하고 싶어 하는 경우는 거의 없기 때문에 흔하게 겪을 일은 아닙니다. 하지만 이 예시는 컴퓨터 내부에서 데이터가 어떻게 처리되는지, 그리고 프로그램이 어떻게 의도치 않은 방향으로 폭주할 수 있는지를 아주 잘 보여줍니다.</p>
<p>만약 정말로 문자 포인터(<code>char*</code>)의 메모리 주소를 꼭 화면에 출력하고 싶다면, <code>static_cast</code>라는 기능을 써서 타입을 <code>const void*</code>로 강제로 바꿔주면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const char* ptr{ &quot;Alex&quot; };

    std::cout &lt;&lt; ptr &lt;&lt; &#39;\n&#39;;                           // ptr을 C스타일 문자열(글자)로 출력합니다.
    std::cout &lt;&lt; static_cast&lt;const void*&gt;(ptr) &lt;&lt; &#39;\n&#39;; // ptr이 가지고 있는 메모리 주소를 출력합니다.

    return 0;
}
</code></pre>
<blockquote>
<p><strong>관련 컨텐츠</strong>
<code>void*</code>에 대해서는 19.5 레슨(Void 포인터)에서 다룰 예정입니다. 지금 당장 이게 어떻게 작동하는지 몰라도 여기서 쓰는 데는 전혀 문제없습니다.</p>
</blockquote>
<hr>
<h3 id="c스타일-문자열-상수-대신-stdstring_view를-사용하세요">C스타일 문자열 상수 대신 std::string_view를 사용하세요</h3>
<p>요즘 사용하는 최신 C++에서는 굳이 C스타일 문자열 기호 상수를 쓸 이유가 거의 없습니다. 대신에 <code>constexpr std::string_view</code> 객체를 사용하는 것이 훨씬 좋습니다. 속도도 똑같이 빠르거나 오히려 더 빠르고, 헷갈리지 않고 항상 일관되게 작동하기 때문입니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
C스타일 문자열 기호 상수는 피하고, 대신 <code>constexpr std::string_view</code>를 사용하는 것을 강력히 추천합니다!</p>
</blockquote>
<hr>
<h2 id="1712--다차원-c스타일-배열">17.12 — 다차원 C스타일 배열</h2>
<p>틱택토(Tic-tac-toe) 게임을 떠올려보세요. 빙고랑 비슷한 게임이죠!  이 게임의 기본 판은 3x3 격자 모양이고, 두 명의 플레이어가 번갈아 가며 &#39;X&#39;와 &#39;O&#39; 기호를 그립니다. 기호 3개를 한 줄로 먼저 잇는 사람이 이기는 게임이죠.</p>
<p>게임판의 데이터를 9개의 개별 변수에 따로따로 저장할 수도 있겠지만, 우리는 이미 똑같은 종류의 데이터를 여러 개 다룰 때는 &#39;배열(array)&#39;이라는 묶음을 사용하는 게 훨씬 편하다는 것을 알고 있습니다.</p>
<pre><code class="language-cpp">int ttt[9]; // 정수형 C스타일 배열 (값 0 = 빈칸, 1 = 플레이어 1, 2 = 플레이어 2)
</code></pre>
<p>이 코드는 메모리에 9개의 칸이 일렬로 나란히 늘어선 C스타일 배열을 만듭니다. 우리는 이 데이터들이 아래처럼 한 줄로 쭉 늘어서 있다고 상상할 수 있어요.</p>
<pre><code class="language-cpp">// ttt[0] ttt[1] ttt[2] ttt[3] ttt[4] ttt[5] ttt[6] ttt[7] ttt[8]
</code></pre>
<p>배열의 <strong>차원(dimension)</strong>이라는 건, 배열 안의 특정 값을 콕 집어내기 위해 &#39;방 번호(인덱스)&#39;가 몇 개나 필요한지를 말합니다. 차원이 딱 하나뿐인 배열을 <strong>1차원 배열(single-dimensional array 또는 one-dimensional array)</strong>이라고 부릅니다. (줄여서 1d 배열이라고도 해요). 위에서 만든 <code>ttt</code>가 바로 1차원 배열의 예시입니다. <code>ttt[2]</code>처럼 방 번호 하나만 쓰면 원하는 값을 꺼낼 수 있으니까요.</p>
<p>하지만 가만히 생각해 보면, 한 줄로 길게 늘어선 이 1차원 배열은 평면(2차원) 공간에서 진행되는 실제 틱택토 게임판의 모습과는 거리가 좀 멉니다. 우리는 이것보다 더 멋진 방법을 쓸 수 있어요.</p>
<hr>
<h3 id="2차원-배열-two-dimensional-arrays">2차원 배열 (Two-dimensional arrays)</h3>
<p>이전 수업에서 우리는 배열 안에 어떤 형태의 데이터든 다 넣을 수 있다고 배웠습니다. 이 말은 즉, <strong>배열의 알맹이로 &#39;또 다른 배열&#39;을 넣을 수도 있다</strong>는 뜻입니다! 그런 배열을 만드는 방법은 아주 간단해요.</p>
<pre><code class="language-cpp">int a[3][5]; // 5개의 정수를 가진 배열이 3개 들어있는 배열
</code></pre>
<p>이렇게 배열 안에 배열이 들어있는 것을 <strong>2차원 배열(two-dimensional array, 줄여서 2d 배열)</strong>이라고 부릅니다. 방 번호를 적는 대괄호 <code>[]</code>가 두 개 붙어있기 때문이죠.</p>
<p>2차원 배열을 다룰 때는 첫 번째(왼쪽) 괄호가 <strong>행(가로줄)</strong>을 고르고, 두 번째(오른쪽) 괄호가 <strong>열(세로칸)</strong>을 고른다고 생각하면 이해하기 아주 쉽습니다. 머릿속으로 이 2차원 배열이 아래와 같은 표 모양으로 펼쳐져 있다고 상상해 보세요.</p>
<pre><code class="language-cpp">//      열 0     열 1     열 2     열 3     열 4
// a[0][0] a[0][1] a[0][2] a[0][3] a[0][4]   행 0
// a[1][0] a[1][1] a[1][2] a[1][3] a[1][4]   행 1
// a[2][0] a[2][1] a[2][2] a[2][3] a[2][4]   행 2
</code></pre>
<p>2차원 배열의 값을 꺼내거나 바꿀 때는 그냥 괄호 두 개를 써주면 됩니다.</p>
<pre><code class="language-cpp">a[2][3] = 7; // a[행][열], 즉 행이 2이고 열이 3인 위치의 값
</code></pre>
<p>이제 이 방법을 쓰면 틱택토 게임판도 2차원 배열로 완벽하게 만들 수 있습니다!</p>
<pre><code class="language-cpp">int ttt[3][3];
</code></pre>
<p>짠! 이제 행과 열 번호를 이용해 아주 쉽게 다룰 수 있는 3x3 모양의 게임판이 완성되었습니다.</p>
<hr>
<h3 id="다차원-배열">다차원 배열</h3>
<p>차원이 2개 이상인 배열을 통틀어 <strong>다차원 배열</strong>이라고 부릅니다.</p>
<p>C++는 2차원을 넘어 3차원, 4차원 그 이상의 다차원 배열도 얼마든지 만들 수 있게 해줍니다.</p>
<pre><code class="language-cpp">int threedee[4][4][4]; // 4x4x4 배열 (정수 4개가 든 배열 4개가 다시 4개 모인 배열)
</code></pre>
<p>예를 들어, 유명한 게임 &#39;마인크래프트&#39;의 지형은 16x16x16 크기의 블록들(청크 섹션이라고 부름)로 나뉘어져 있는데, 이런 3차원 공간을 다룰 때 아주 유용하겠죠.</p>
<p>물론 3차원보다 더 복잡한 배열도 지원하지만, 실제로 쓰이는 일은 거의 없습니다.</p>
<hr>
<h3 id="2차원-배열은-메모리에-어떻게-저장될까">2차원 배열은 메모리에 어떻게 저장될까?</h3>
<p>컴퓨터의 메모리(램)는 일직선으로 된 1차원 공간입니다. 그래서 다차원 배열이라 할지라도 실제로는 그냥 한 줄로 길게 쭉 늘어서서 저장됩니다.</p>
<p>아래와 같은 2차원 배열이 있을 때, 메모리에 저장하는 방식에는 크게 두 가지가 있습니다.</p>
<pre><code class="language-cpp">//      열 0     열 1     열 2     열 3     열 4
// [0][0] [0][1] [0][2] [0][3] [0][4]   행 0
// [1][0] [1][1] [1][2] [1][3] [1][4]   행 1
// [2][0] [2][1] [2][2] [2][3] [2][4]   행 2
</code></pre>
<p><strong>C++는 &#39;행 우선 순서(row-major order)&#39;라는 방식을 사용합니다.</strong> 이 방식은 위에서부터 한 가로줄(행)씩 차례대로, 왼쪽에서 오른쪽 방향으로 쭉 메모리에 채워 넣는 방식입니다.</p>
<p><code>[0][0] [0][1] [0][2] [0][3] [0][4] [1][0] [1][1] [1][2] [1][3] [1][4] [2][0] [2][1] [2][2] [2][3] [2][4]</code></p>
<p>포트란(Fortran) 같은 일부 다른 프로그래밍 언어들은 반대로 <strong>&#39;열 우선 순서(column-major order)&#39;</strong>를 사용합니다. 이건 세로칸(열)을 먼저 위에서 아래로 다 채우고, 그다음 오른쪽 세로칸으로 넘어가는 방식이죠.</p>
<p><code>[0][0] [1][0] [2][0] [0][1] [1][1] [2][1] [0][2] [1][2] [2][2] [0][3] [1][3] [2][3] [0][4] [1][4] [2][4]</code></p>
<p>C++에서 배열을 처음 만들고 값을 채워 넣을 때(초기화할 때), 값들은 바로 이 &#39;행 우선 순서&#39;대로 들어갑니다. <strong>따라서 우리가 코드로 배열의 값들을 하나씩 훑어볼 때도, 컴퓨터 메모리에 저장된 순서(가로줄 먼저)대로 읽어들이는 것이 가장 빠르고 효율적입니다.</strong></p>
<hr>
<h3 id="2차원-배열-초기화하기-initializing-two-dimensional-arrays">2차원 배열 초기화하기 (Initializing two-dimensional arrays)</h3>
<p>2차원 배열에 처음 값을 넣어줄 때는 중괄호 <code>{}</code> 안에 또 중괄호를 넣어서 묶어주는 것이 제일 알아보기 쉽습니다. 안쪽에 있는 중괄호 한 덩어리가 가로줄(행) 하나를 뜻하게 됩니다.</p>
<pre><code class="language-cpp">int array[3][5]{
  { 1, 2, 3, 4, 5 },     // 행 0
  { 6, 7, 8, 9, 10 },    // 행 1
  { 11, 12, 13, 14, 15 } // 행 2
};
</code></pre>
<p>일부 똑똑한 컴파일러들은 안쪽 중괄호를 생략해도 알아서 처리해주지만, <strong>코드를 읽기 쉽게 만들기 위해 꼭 안쪽 중괄호까지 모두 적어주는 것을 강력히 권장합니다.</strong></p>
<p>안쪽 중괄호를 썼는데 배열의 전체 칸 수보다 값을 적게 넣었다면, 나머지 빈칸들은 알아서 0으로 채워집니다(값 초기화).</p>
<pre><code class="language-cpp">int array[3][5]{
  { 1, 2 },          // 행 0 = 1, 2, 0, 0, 0
  { 6, 7, 8 },       // 행 1 = 6, 7, 8, 0, 0
  { 11, 12, 13, 14 } // 행 2 = 11, 12, 13, 14, 0
};
</code></pre>
<p>값을 채워 넣으면서 배열을 만들 때는, <strong>오직 가장 왼쪽(첫 번째) 대괄호 안의 숫자만</strong> 비워둘 수 있습니다.</p>
<pre><code class="language-cpp">int array[][5]{
  { 1, 2, 3, 4, 5 },
  { 6, 7, 8, 9, 10 },
  { 11, 12, 13, 14, 15 }
};
</code></pre>
<p>이렇게 하면, 컴퓨터(컴파일러)가 안쪽 중괄호 덩어리 개수를 직접 세어보고 &quot;아, 행이 3개구나!&quot; 하고 알아서 계산해 주기 때문입니다.</p>
<p>하지만 가장 왼쪽이 아닌 다른 대괄호들을 비워두는 것은 허용되지 않습니다. (에러가 나요!)</p>
<pre><code class="language-cpp">int array[][]{
  { 1, 2, 3, 4 },
  { 5, 6, 7, 8 }
};
</code></pre>
<p>당연히 일반 배열처럼 다차원 배열의 모든 칸을 한 번에 0으로 꽉 채울 수도 있습니다. 아주 간단하죠.</p>
<pre><code class="language-cpp">int array[3][5] {};
</code></pre>
<hr>
<h3 id="2차원-배열과-반복문">2차원 배열과 반복문</h3>
<p>1차원 배열에 들어있는 모든 값을 한 번씩 다 확인하고 싶을 때는 반복문 하나면 충분했습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int arr[] { 1, 2, 3, 4, 5 };

    // 인덱스를 사용하는 for 반복문
    for (std::size_t i{0}; i &lt; std::size(arr); ++i)
        std::cout &lt;&lt; arr[i] &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    // 범위 기반 for 반복문
    for (auto e: arr)
        std::cout &lt;&lt; e &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>하지만 2차원 배열은 가로와 세로가 있으니 반복문이 2개 필요합니다. 하나는 &#39;행&#39;을 고르고, 다른 하나는 &#39;열&#39;을 골라야 하니까요.</p>
<p>반복문 2개를 겹쳐 쓸 때는, 어떤 것을 &#39;바깥쪽 반복문&#39;으로 하고 어떤 것을 &#39;안쪽 반복문&#39;으로 할지 결정해야 합니다. 방금 전에 컴퓨터가 일하기 편하게 하려면 메모리에 저장된 순서대로 접근하는 것이 가장 효율적이라고 했죠? C++는 가로줄(행)을 먼저 저장하는 &#39;행 우선 순서&#39;를 사용하니까, <strong>가로줄(행)을 고르는 것을 바깥쪽 반복문으로 두고, 세로칸(열)을 고르는 것을 안쪽 반복문으로 두는 것이 가장 좋습니다.</strong></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int arr[3][4] {
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }
    };

    // 인덱스를 사용하는 이중 for 반복문
    for (std::size_t row{0}; row &lt; std::size(arr); ++row) // std::size(arr)는 행의 개수를 반환합니다
    {
        for (std::size_t col{0}; col &lt; std::size(arr[0]); ++col) // std::size(arr[0])은 열의 개수를 반환합니다
            std::cout &lt;&lt; arr[row][col] &lt;&lt; &#39; &#39;;

        std::cout &lt;&lt; &#39;\n&#39;;
    }

    // 범위 기반 이중 for 반복문
    for (const auto&amp; arow: arr) // 각 행 배열을 가져옵니다
    {
        for (const auto&amp; e: arow) // 그 행 안의 각 원소를 가져옵니다
            std::cout &lt;&lt; e &lt;&lt; &#39; &#39;;

        std::cout &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}
</code></pre>
<hr>
<h3 id="2차원-배열-실전-예제-a-two-dimensional-array-example">2차원 배열 실전 예제 (A two-dimensional array example)</h3>
<p>그럼 이제 2차원 배열을 실제로 어떻게 써먹는지 구구단(곱셈표)을 만드는 예제를 통해 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    constexpr int numRows{ 10 };
    constexpr int numCols{ 10 };

    // 10x10 배열 선언하기
    int product[numRows][numCols]{};

    // 구구단 계산하기
    // 어떤 수든 0을 곱하면 0이 되니까, 0행과 0열은 굳이 계산할 필요가 없어요.
    for (std::size_t row{ 1 }; row &lt; numRows; ++row)
    {
        for (std::size_t col{ 1 }; col &lt; numCols; ++col)
        {
            product[row][col] = static_cast&lt;int&gt;(row * col);
        }
    }

    for (std::size_t row{ 1 }; row &lt; numRows; ++row)
    {
        for (std::size_t col{ 1 }; col &lt; numCols; ++col)
        {
            std::cout &lt;&lt; product[row][col] &lt;&lt; &#39;\t&#39;;
        }

        std::cout &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}
</code></pre>
<p>이 프로그램은 1부터 9까지의 모든 구구단 값을 계산하고 화면에 예쁘게 표 형태로 보여줍니다.
표를 출력할 때 잘 보면, <code>for</code> 반복문이 0이 아니라 1부터 시작한다는 점을 알 수 있어요. 0행과 0열을 그대로 출력하면 화면에 숫자 0만 가득 찰 테니까, 그걸 빼고 출력하려고 일부러 1부터 시작한 거랍니다! 결과는 아래와 같습니다.</p>
<pre><code class="language-text">1    2    3    4    5    6    7    8    9    
2    4    6    8    10    12    14    16    18    
3    6    9    12    15    18    21    24    27    
4    8    12    16    20    24    28    32    36    
5    10    15    20    25    30    35    40    45    
6    12    18    24    30    36    42    48    54    
7    14    21    28    35    42    49    56    63    
8    16    24    32    40    48    56    64    72    
9    18    27    36    45    54    63    72    81    
</code></pre>
<hr>
<h3 id="데카르트-좌표계-vs-배열-인덱스">데카르트 좌표계 vs 배열 인덱스</h3>
<p>수학이나 기하학에서는 물건의 위치를 설명할 때 &#39;데카르트 좌표계&#39;라는 것을 자주 사용합니다. 말이 좀 어렵지만, 중학교 수학 시간에 배운 가로축(x)과 세로축(y)이 있는 그래프를 떠올리면 됩니다! 가로축은 &quot;x&quot;, 세로축은 &quot;y&quot;라고 부르는 게 일반적이죠.</p>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/a2174968-f51b-4b8d-b73c-537f4021abe5/image.png" alt=""></p>
<p>2차원 평면에서 어떤 물건의 위치는 <code>{ x, y }</code> 쌍으로 표현할 수 있습니다. 여기서 x좌표는 중심에서 오른쪽으로 얼마나 떨어져 있는지, y좌표는 위로 얼마나 올라가 있는지를 나타냅니다. (참고로 컴퓨터 그래픽에서는 종종 y축이 반대로 뒤집혀서, y좌표가 아래쪽으로 얼마나 내려갔는지를 뜻하기도 합니다).</p>
<p>자, 그럼 우리가 배운 C++의 2차원 배열 모양을 다시 살펴볼까요?</p>
<pre><code class="language-cpp">//      열 0     열 1     열 2     열 3     열 4
// [0][0]  [0][1]  [0][2]  [0][3]  [0][4]  행 0
// [1][0]  [1][1]  [1][2]  [1][3]  [1][4]  행 1
// [2][0]  [2][1]  [2][2]  [2][3]  [2][4]  행 2
</code></pre>
<p>사실 이것도 일종의 2차원 좌표계입니다. 어떤 값의 위치를 <code>[행][열]</code> 로 나타내는 것뿐이죠. (수학 그래프와 달리 세로축 방향이 아래를 향하고 있다는 점이 다릅니다).</p>
<p>수학의 좌표계나 배열의 좌표계, 각자 따로 놓고 보면 이해하기 꽤 쉬운데요. 문제는 수학의 좌표인 <code>{ x, y }</code>를 배열의 방 번호인 <code>[행][열]</code>로 바꾸려고 할 때 직관적이지 않아서 헷갈리기 쉽다는 점입니다.</p>
<blockquote>
<p><strong>핵심 통찰</strong>
여기서 아주 중요한 사실을 하나 알려드릴게요! 수학 좌표계의 &#39;x좌표(가로)&#39;는 배열 시스템에서 &#39;몇 번째 <strong>열(column)</strong>인지&#39;를 뜻합니다. 반대로 &#39;y좌표(세로)&#39;는 &#39;몇 번째 <strong>행(row)</strong>인지&#39;를 뜻하죠.
따라서 수학의 <code>{ x, y }</code> 좌표를 배열에 쓰려면 <strong><code>[y][x]</code></strong> 순서로 적어줘야 합니다. 우리가 평소에 생각하는 x, y 알파벳 순서와 완전히 반대인 셈이죠!</p>
</blockquote>
<p>이런 이유 때문에 코드로 2차원 반복문을 작성하다 보면 아래와 같은 모양이 만들어집니다.</p>
<pre><code class="language-cpp">for (std::size_t y{0}; y &lt; std::size(arr); ++y) // 바깥쪽 반복문은 행, 즉 y를 나타냅니다
{
    for (std::size_t x{0}; x &lt; std::size(arr[0]); ++x) // 안쪽 반복문은 열, 즉 x를 나타냅니다
        std::cout &lt;&lt; arr[y][x] &lt;&lt; &#39; &#39;; // y(행)를 먼저 쓰고, 그 다음 x(열)를 써서 값을 꺼냅니다
</code></pre>
<p>여기서 배열의 값을 <code>arr[y][x]</code> 순서로 꺼내고 있다는 점을 꼭 기억해 두세요. 알파벳 순서가 거꾸로라서 처음엔 많이 어색하겠지만, 금방 익숙해지실 겁니다!</p>
<hr>
<h2 id="1713--다차원-stdarray">17.13 — 다차원 std::array</h2>
<p>이전 레슨에서는 C 스타일의 다차원 배열(마치 엑셀 표처럼 가로세로로 늘어선 배열)에 대해 이야기했어요.</p>
<pre><code class="language-cpp">// C 스타일의 2차원 배열
int arr[3][4] {
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
};
</code></pre>
<p>하지만 아시다시피, 프로그램 전체에서 쓰는 전역 데이터를 저장할 때를 제외하고는 C 스타일 배열은 가급적 피하는 것이 좋습니다.</p>
<p>이번 레슨에서는 안전하고 현대적인 <code>std::array</code>를 사용해 다차원 배열을 어떻게 다루는지 알아볼 거예요. 초보자분들도 이해하기 쉽도록 아주 쉽게 풀어서 설명해 드릴게요!</p>
<hr>
<h3 id="표준-라이브러리에는-다차원-배열을-위한-클래스가-따로-없습니다">표준 라이브러리에는 다차원 배열을 위한 클래스가 따로 없습니다</h3>
<p><code>std::array</code>는 기본적으로 1차원(한 줄짜리) 배열로 만들어져 있어요. 그럼 당연히 &quot;다차원 배열을 위한 전용 표준 클래스는 없나요?&quot;라는 궁금증이 생기겠죠.
정답은... 안타깝게도 없습니다.</p>
<hr>
<h3 id="2차원-stdarray-만들기">2차원 std::array 만들기</h3>
<p>2차원 <code>std::array</code>를 만드는 가장 정석적인 방법은 <code>std::array</code> 안에 또 다른 <code>std::array</code>를 넣는 거예요. 마치 상자 안에 상자를 넣는 마트료시카 인형처럼요! 코드로 보면 다음과 같습니다.</p>
<pre><code class="language-cpp">std::array&lt;std::array&lt;int, 4&gt;, 3&gt; arr {{  // 이중 중괄호 {{ }} 에 주의하세요
    { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 10, 11, 12 }
}};
</code></pre>
<p>여기서 눈여겨볼 만한 점들이 몇 가지 있어요:</p>
<ul>
<li>다차원 <code>std::array</code>에 초기값을 넣어줄 때는 바깥쪽에 이중 중괄호 <code>{{ }}</code>를 사용해야 해요.</li>
<li>코드를 작성하는 방식이 꽤 길고 읽기도 어렵습니다.</li>
<li>템플릿이 안으로 중첩되는 방식 때문에 배열의 크기 순서가 뒤집혀 보입니다. 우리는 보통 3행 4열(3줄, 각 줄에 4개) 배열을 만들고 싶을 때 <code>arr[3][4]</code>라고 생각하는 게 자연스럽죠. 하지만 <code>std::array&lt;std::array&lt;int, 4&gt;, 3&gt;</code>에서는 숫자의 순서가 반대로 되어 있습니다. (안쪽 배열이 4칸짜리이고, 그 묶음이 총 3줄 있다는 뜻이에요.)</li>
</ul>
<p>요소를 꺼내 쓰는(인덱싱) 방법은 C 스타일 2차원 배열과 똑같습니다:</p>
<pre><code class="language-cpp">std::cout &lt;&lt; arr[1][2]; // 1번 행(두 번째 줄), 2번 열(세 번째 칸)의 요소를 출력합니다
</code></pre>
<p>또한 1차원 배열처럼 함수에 통째로 넘겨줄 수도 있어요:</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

template &lt;typename T, std::size_t Row, std::size_t Col&gt;
void printArray(const std::array&lt;std::array&lt;T, Col&gt;, Row&gt; &amp;arr)
{
    for (const auto&amp; arow: arr)   // 배열의 각 행(가로줄)을 가져옵니다
    {
        for (const auto&amp; e: arow) // 해당 행의 각 요소를 가져옵니다
            std::cout &lt;&lt; e &lt;&lt; &#39; &#39;;

        std::cout &lt;&lt; &#39;\n&#39;;
    }
}

int main()
{
    std::array&lt;std::array&lt;int, 4&gt;, 3&gt;  arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }
    }};

    printArray(arr);

    return 0;
}
</code></pre>
<p>으악, 괄호가 너무 많아서 눈이 아프죠? 이건 겨우 2차원일 뿐이에요. 3차원, 4차원으로 넘어가면 코드는 훨씬 더 길고 끔찍해집니다!</p>
<hr>
<h3 id="별칭-템플릿으로-2차원-stdarray-쉽게-만들기">별칭 템플릿으로 2차원 std::array 쉽게 만들기</h3>
<p>이전 레슨에서 &#39;타입 별칭(Type alias)&#39;을 배우면서, 복잡한 타입에 짧고 쉬운 별명을 붙여줄 수 있다고 했었죠. 하지만 일반적인 타입 별칭을 쓰면 아래처럼 템플릿에 들어갈 모든 정보를 하나하나 고정해서 적어줘야 합니다.</p>
<pre><code class="language-cpp">using Array2dint34 = std::array&lt;std::array&lt;int, 4&gt;, 3&gt;;
</code></pre>
<p>이렇게 하면 3x4 크기의 2차원 정수 배열이 필요할 때마다 <code>Array2dint34</code>라는 이름을 쓸 수 있어요. 하지만 크기나 데이터 종류가 바뀔 때마다 이런 별칭을 매번 새로 만들어야 한다면 너무 귀찮겠죠!</p>
<p>이럴 때 <strong>별칭 템플릿(alias template)</strong>을 쓰면 아주 완벽합니다! 별칭을 만들 때 저장할 데이터의 종류(타입), 행(가로줄)의 개수, 열(세로줄)의 개수를 우리가 템플릿의 재료로 직접 지정해줄 수 있거든요.</p>
<pre><code class="language-cpp">// 2차원 std::array를 위한 별칭 템플릿
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
using Array2d = std::array&lt;std::array&lt;T, Col&gt;, Row&gt;;
</code></pre>
<p>이제 3x4 크기의 2차원 정수 배열이 필요하면 언제 어디서든 그냥 <code>Array2d&lt;int, 3, 4&gt;</code>라고 쓰면 됩니다. 훨씬 깔끔해졌죠!</p>
<p>전체 예제 코드를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

// 2차원 std::array를 위한 별칭 템플릿
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
using Array2d = std::array&lt;std::array&lt;T, Col&gt;, Row&gt;;

// Array2d를 함수 매개변수로 사용할 때는 템플릿 매개변수를 다시 지정해 주어야 합니다
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
void printArray(const Array2d&lt;T, Row, Col&gt; &amp;arr)
{
    for (const auto&amp; arow: arr)   // 배열의 각 행을 가져옵니다
    {
        for (const auto&amp; e: arow) // 해당 행의 각 요소를 가져옵니다
            std::cout &lt;&lt; e &lt;&lt; &#39; &#39;;

        std::cout &lt;&lt; &#39;\n&#39;;
    }
}

int main()
{
    // 3행 4열의 2차원 정수 배열을 정의합니다
    Array2d&lt;int, 3, 4&gt; arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }
    }};

    printArray(arr);

    return 0;
}
</code></pre>
<p>코드가 얼마나 짧고 쓰기 편해졌는지 확인해 보세요!</p>
<p>이 별칭 템플릿의 또 다른 멋진 점은 우리가 원하는 순서대로 규칙을 정할 수 있다는 거예요. 원래 <code>std::array</code>는 &#39;타입&#39;을 먼저 쓰고 &#39;크기&#39;를 쓰기 때문에 그 규칙은 따랐습니다. 하지만 행(Row)과 열(Col) 중 무엇을 먼저 쓸지는 우리 마음이에요. C 스타일 배열이 행을 먼저 쓰는 방식이므로, 우리도 덜 헷갈리게 행(Row)을 열(Col)보다 먼저 쓰도록 만들었습니다.</p>
<p>이 방식은 3차원 이상의 복잡한 배열로 확장할 때도 아주 유용합니다.</p>
<pre><code class="language-cpp">// 3차원 std::array를 위한 별칭 템플릿
template &lt;typename T, std::size_t Row, std::size_t Col, std::size_t Depth&gt;
using Array3d = std::array&lt;std::array&lt;std::array&lt;T, Depth&gt;, Col&gt;, Row&gt;;
</code></pre>
<hr>
<h3 id="2차원-배열의-각-차원-길이-구하기">2차원 배열의 각 차원 길이 구하기</h3>
<p>1차원 배열에서는 <code>.size()</code> 함수를 써서 배열의 길이를 쉽게 알 수 있었죠. 그런데 2차원 배열에서는 어떨까요? 이 경우 그냥 <code>.size()</code>를 호출하면 첫 번째 차원(보통 행의 개수인 3)의 길이만 달랑 알려줍니다.</p>
<p>이럴 때 원하는 차원의 요소를 하나 콕 집어낸 다음, 거기에 대고 <code>.size()</code>를 부르면 어떨까 하고 솔깃한 생각이 들 수 있어요. (하지만 꽤 위험한 방법이랍니다!)</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

// 2차원 std::array를 위한 별칭 템플릿
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
using Array2d = std::array&lt;std::array&lt;T, Col&gt;, Row&gt;;

int main()
{
    // 3행 4열의 2차원 정수 배열을 정의합니다
    Array2d&lt;int, 3, 4&gt; arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }
    }};

    std::cout &lt;&lt; &quot;Rows: &quot; &lt;&lt; arr.size() &lt;&lt; &#39;\n&#39;;    // 첫 번째 차원(행)의 길이를 구합니다
    std::cout &lt;&lt; &quot;Cols: &quot; &lt;&lt; arr[0].size() &lt;&lt; &#39;\n&#39;; // 두 번째 차원(열)의 길이를 구합니다. 만약 첫 번째 차원의 길이가 0이라면 정의되지 않은 동작(오류)이 발생합니다!

    return 0;
}
</code></pre>
<p>첫 번째 차원(행)의 길이를 알기 위해 배열 전체에 <code>.size()</code>를 불렀습니다. 두 번째 차원(열)의 길이를 알려면, 먼저 <code>arr[0]</code>으로 첫 번째 줄을 꺼내온 다음 거기에 <code>.size()</code>를 부른 거죠. 3차원 배열이라면 <code>arr[0][0].size()</code>라고 했을 거예요.</p>
<p>하지만 위 코드는 큰 문제가 하나 있습니다. 만약 마지막 차원을 제외한 다른 차원의 길이가 0일 경우, 없는 걸 꺼내오려다 프로그램이 꼬여버리는 &#39;미정의 동작(undefined behavior)&#39;이 발생하게 됩니다!</p>
<p>더 안전하고 좋은 방법은, 우리가 템플릿을 만들 때 넘겨준 숫자에서 직접 길이를 가져오는 전용 함수를 따로 만드는 거예요.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

// 2차원 std::array를 위한 별칭 템플릿
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
using Array2d = std::array&lt;std::array&lt;T, Col&gt;, Row&gt;;

// Row 매개변수에서 행의 개수를 가져옵니다
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
constexpr int rowLength(const Array2d&lt;T, Row, Col&gt;&amp;) // 원한다면 std::size_t를 반환해도 됩니다
{
    return Row;
}

// Col 매개변수에서 열의 개수를 가져옵니다
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
constexpr int colLength(const Array2d&lt;T, Row, Col&gt;&amp;) // 원한다면 std::size_t를 반환해도 됩니다
{
    return Col;
}

int main()
{
    // 3행 4열의 2차원 정수 배열을 정의합니다
    Array2d&lt;int, 3, 4&gt; arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }
    }};

    std::cout &lt;&lt; &quot;Rows: &quot; &lt;&lt; rowLength(arr) &lt;&lt; &#39;\n&#39;; // 첫 번째 차원(행)의 길이를 구합니다
    std::cout &lt;&lt; &quot;Cols: &quot; &lt;&lt; colLength(arr) &lt;&lt; &#39;\n&#39;; // 두 번째 차원(열)의 길이를 구합니다

    return 0;
}
</code></pre>
<p>이렇게 하면 배열의 실제 데이터가 아니라 배열의 &#39;타입 정보&#39;만 보고 길이를 알아내기 때문에, 길이가 0이어도 오류가 나지 않아요. 또한 굳이 강제로 형변환할 필요 없이 배열의 길이를 우리에게 익숙한 <code>int</code> 자료형으로 쉽게 받아올 수 있다는 장점도 있습니다.</p>
<hr>
<h3 id="2차원-배열-납작하게-펴기">2차원 배열 납작하게 펴기</h3>
<p>차원이 2개 이상인 다차원 배열은 초보자뿐만 아니라 숙련자에게도 다루기 꽤 까다롭습니다:</p>
<ul>
<li>만들거나 사용할 때 코드가 길고 복잡해집니다.</li>
<li>두 번째 이상의 차원 길이를 알아내기가 번거롭습니다.</li>
<li>반복문으로 모든 데이터를 살펴보기가 점점 더 어려워집니다 (차원이 늘어날수록 <code>for</code>문도 그 안에 겹겹이 계속 추가해야 하거든요).</li>
</ul>
<p>다차원 배열을 좀 더 쉽게 다루는 꿀팁 중 하나는 바로 배열을 <strong>납작하게 펴는 것(flatten)</strong>입니다. 여러 방향으로 표처럼 늘어선 배열을 그냥 하나의 긴 1차원 배열(한 줄)로 줄여버리는 과정을 말해요.</p>
<p>예를 들어, 가로줄(Row)과 세로줄(Col)을 가진 2차원 배열을 억지로 만드는 대신, 단순히 <code>Row * Col</code>개의 요소를 가진 기다란 1차원 배열 하나를 만드는 거예요. 공간은 똑같이 차지하면서 형태만 단순해지는 거죠.</p>
<p>하지만 1차원 배열은 한 줄짜리라서 그 자체만으로는 2차원처럼 행렬 좌표를 써서 다룰 수는 없습니다. 이를 해결하기 위해, 겉보기엔 2차원 배열처럼 쓸 수 있게 해주는 &#39;도구(인터페이스)&#39;를 만들어 덮어씌울 수 있어요. 사용자가 가로, 세로 좌표를 입력하면, 이 도구가 알아서 1차원 배열의 알맞은 위치를 계산해 연결해 주는 원리입니다.</p>
<p>C++11 이상의 버전에서 이 방법을 어떻게 쓰는지 예제로 확인해 볼까요?</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;functional&gt;

// 두 개의 차원을 사용해 1차원 std::array를 정의할 수 있게 해주는 별칭 템플릿
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
using ArrayFlat2d = std::array&lt;T, Row * Col&gt;;

// ArrayFlat2d를 2차원처럼 다룰 수 있게 해주는 수정 가능한 뷰(view)
// 이것은 뷰(보기 창)이므로, 대상이 되는 ArrayFlat2d가 메모리에 살아있어야 합니다
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
class ArrayView2d
{
private:
    // m_arr을 ArrayFlat2d에 대한 참조자(&amp;)로 만들고 싶을 수 있지만,
    // 참조자는 한 번 연결되면 대상을 바꿀 수 없어서 복사 할당이 불가능해집니다.
    // std::reference_wrapper를 사용하면 참조처럼 작동하면서도 복사 할당이 가능해집니다.
    std::reference_wrapper&lt;ArrayFlat2d&lt;T, Row, Col&gt;&gt; m_arr {};

public:
    ArrayView2d(ArrayFlat2d&lt;T, Row, Col&gt; &amp;arr)
        : m_arr { arr }
    {}

    // 단일 인덱스로 요소 가져오기 (operator[] 사용)
    T&amp; operator[](int i) { return m_arr.get()[static_cast&lt;std::size_t&gt;(i)]; }
    const T&amp; operator[](int i) const { return m_arr.get()[static_cast&lt;std::size_t&gt;(i)]; }

    // 2차원 인덱스로 요소 가져오기 (C++23 이전에는 operator[]가 다중 차원을 지원하지 않아 operator()를 사용함)
    T&amp; operator()(int row, int col) { return m_arr.get()[static_cast&lt;std::size_t&gt;(row * cols() + col)]; }
    const T&amp; operator()(int row, int col) const { return m_arr.get()[static_cast&lt;std::size_t&gt;(row * cols() + col)]; }

    // C++23부터는 다차원 operator[]가 지원되므로 아래 주석을 해제하고 사용할 수 있습니다
//    T&amp; operator[](int row, int col) { return m_arr.get()[static_cast&lt;std::size_t&gt;(row * cols() + col)]; }
//    const T&amp; operator[](int row, int col) const { return m_arr.get()[static_cast&lt;std::size_t&gt;(row * cols() + col)]; }

    int rows() const { return static_cast&lt;int&gt;(Row); }
    int cols() const { return static_cast&lt;int&gt;(Col); }
    int length() const { return static_cast&lt;int&gt;(Row * Col); }
};

int main()
{
    // 3행 4열 크기를 가지는 1차원 std::array 정의
    ArrayFlat2d&lt;int, 3, 4&gt; arr {
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12 };

    // 1차원 배열을 2차원처럼 보여주는 뷰 정의
    ArrayView2d&lt;int, 3, 4&gt; arrView { arr };

    // 배열의 크기 출력
    std::cout &lt;&lt; &quot;Rows: &quot; &lt;&lt; arrView.rows() &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Cols: &quot; &lt;&lt; arrView.cols() &lt;&lt; &#39;\n&#39;;

    // 1차원 방식으로 배열 출력
    for (int i=0; i &lt; arrView.length(); ++i)
        std::cout &lt;&lt; arrView[i] &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    // 2차원 방식으로 배열 출력
    for (int row=0; row &lt; arrView.rows(); ++row)
    {
        for (int col=0; col &lt; arrView.cols(); ++col)
            std::cout &lt;&lt; arrView(row, col) &lt;&lt; &#39; &#39;;
        std::cout &lt;&lt; &#39;\n&#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같이 나옵니다:</p>
<pre><code>Rows: 3
Cols: 4
1 2 3 4 5 6 7 8 9 10 11 12 
1 2 3 4 
5 6 7 8 
9 10 11 12 
</code></pre><p>C++23 이전 버전에서는 대괄호 <code>[]</code> 안에 숫자를 하나만 넣을 수 있기 때문에, 2차원 느낌을 내기 위해 보통 두 가지 우회 방법을 씁니다:</p>
<ul>
<li>대괄호 대신 소괄호 <code>()</code>를 사용합니다. 소괄호 안에는 숫자를 여러 개 받을 수 있거든요. 이렇게 하면 1차원으로 접근할 땐 <code>[]</code>로, 다차원 좌표로 접근할 땐 <code>()</code>로 구분해서 쓸 수 있습니다. 위 예제에서 우리가 선택한 방법이기도 해요.</li>
<li><code>[]</code>를 썼을 때 하위 뷰(임시 객체)를 반환하게 해서 <code>[][]</code>처럼 대괄호를 연달아 이어 쓰게 만드는 방법입니다. 하지만 이건 구조가 너무 복잡해지고 차원이 높아지면 골치 아파져요.</li>
</ul>
<p>다행히 C++23부터는 대괄호 <code>[]</code> 안에 쉼표로 인덱스를 여러 개 넣을 수 있게 규칙이 업그레이드되었습니다. 이제 굳이 소괄호 <code>()</code>를 쓰지 않아도 <code>[]</code> 하나로 깔끔하게 처리할 수 있게 된 거죠.</p>
<blockquote>
<p><strong>관련 컨텐츠</strong>
<code>std::reference_wrapper</code>에 대한 자세한 내용은 레슨 17.5 -- std::reference_wrapper를 통한 참조 배열에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="stdmdspan-c23">std::mdspan (C++23)</h3>
<p>C++23에서 새롭게 도입된 <code>std::mdspan</code>은 메모리에 일렬로 나란히 붙어있는 데이터들을 마치 다차원 배열처럼 다룰 수 있게 해주는 멋진 &#39;수정 가능한 뷰&#39;입니다. 여기서 &#39;수정 가능한 뷰&#39;라는 말은 단순히 데이터 내용만 훔쳐보는 읽기 전용 뷰(<code>std::string_view</code> 같은 것)가 아니라, 원본 데이터가 변경 가능하다면 이 뷰를 통해서 데이터의 내용도 바꿀 수 있다는 뜻이에요.</p>
<p>아래 예제는 바로 위에서 살펴본 예제와 똑같은 결과를 내지만, 우리가 복잡하게 직접 만든 뷰 클래스 대신 C++ 표준 라이브러리의 <code>std::mdspan</code>을 사용했어요.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;
#include &lt;mdspan&gt;

// 두 개의 차원을 사용해 1차원 std::array를 정의할 수 있게 해주는 별칭 템플릿
template &lt;typename T, std::size_t Row, std::size_t Col&gt;
using ArrayFlat2d = std::array&lt;T, Row * Col&gt;;

int main()
{
    // 3행 4열 크기를 가지는 1차원 std::array 정의
    ArrayFlat2d&lt;int, 3, 4&gt; arr {
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12 };

    // 1차원 배열을 연결하는 2차원 스팬(span) 정의
    // std::mdspan에는 데이터가 있는 메모리 주소(포인터)를 넘겨주어야 합니다.
    // std::array나 std::vector의 data() 함수를 쓰면 이 주소를 얻을 수 있습니다.
    std::mdspan mdView { arr.data(), 3, 4 };

    // 배열의 크기 출력
    // std::mdspan에서는 이것을 extents(범위)라고 부릅니다.
    std::size_t rows { mdView.extents().extent(0) };
    std::size_t cols { mdView.extents().extent(1) };
    std::cout &lt;&lt; &quot;Rows: &quot; &lt;&lt; rows &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Cols: &quot; &lt;&lt; cols &lt;&lt; &#39;\n&#39;;

    // 1차원 방식으로 출력
    // data_handle() 함수를 쓰면 데이터 배열의 시작 주소를 얻을 수 있고,
    // 이를 이용해 요소에 접근할 수 있습니다.
    for (std::size_t i=0; i &lt; mdView.size(); ++i)
        std::cout &lt;&lt; mdView.data_handle()[i] &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    // 2차원 방식으로 출력
    // 다차원 [] 연산자를 사용해 요소에 접근합니다.
    for (std::size_t row=0; row &lt; rows; ++row)
    {
        for (std::size_t col=0; col &lt; cols; ++col)
            std::cout &lt;&lt; mdView[row, col] &lt;&lt; &#39; &#39;;
        std::cout &lt;&lt; &#39;\n&#39;;
    }
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>코드가 직관적이라 이해하기 쉬우실 거예요! 그래도 몇 가지 핵심만 짚고 넘어가 볼까요:</p>
<ul>
<li><code>std::mdspan</code>을 사용하면 우리가 원하는 만큼 차원의 개수를 쭉 늘려서 뷰를 만들 수 있어요.</li>
<li><code>std::mdspan</code>을 만들 때 첫 번째로 넣어주는 값은 배열 데이터가 모여있는 메모리의 시작 주소(포인터)여야 합니다. 구형 C 스타일 배열이라면 그냥 배열 이름을 쓰면 되고, <code>std::array</code>나 <code>std::vector</code>라면 <code>.data()</code> 함수를 호출해서 이 주소를 쉽게 가져올 수 있습니다.</li>
<li><code>std::mdspan</code>을 긴 1차원처럼 다루고 싶다면, <code>.data_handle()</code> 함수를 써서 포인터를 가져온 다음 거기에 <code>[]</code>를 붙여 사용해야 해요.</li>
<li>C++23부터는 대괄호 안에 여러 개의 숫자를 쉼표로 넣을 수 있으므로, 촌스럽게 <code>[row][col]</code>처럼 두 번 쓰지 않고 <code>[row, col]</code> 형식으로 세련되게 접근합니다.</li>
</ul>
<p>참고로, 다가오는 C++26 버전에서는 <code>std::array</code>와 <code>std::mdspan</code>의 장점만을 합쳐서 아예 메모리 관리까지 알아서 해주는 완벽한 <code>std::mdarray</code>라는 기능이 추가될 예정이랍니다!</p>
<hr>
<h2 id="17x--17장-요약-및-퀴즈">17.x — 17장 요약 및 퀴즈</h2>
<ul>
<li><strong>고정 크기 배열(Fixed-size arrays)</strong> 또는 고정 길이 배열(fixed-length arrays)은 배열이 생성되는 시점에 길이를 알아야 하며, 이후에는 길이를 변경할 수 없습니다. C 스타일 배열과 <code>std::array</code>는 모두 고정 크기 배열입니다. 런타임에 크기를 조절할 수 있는 배열은 동적 배열(dynamic arrays)이라고 하며, <code>std::vector</code>가 이에 해당합니다.</li>
<li><code>std::array</code>의 길이는 반드시 <strong>상수 표현식(constant expression)</strong>이어야 합니다. 주로 정수 리터럴, <code>constexpr</code> 변수, 또는 범위 없는 열거자(unscoped enumerator)가 길이 값으로 제공됩니다.</li>
<li><code>std::array</code>는 <strong>집계형(aggregate)</strong>입니다. 즉, 생성자가 없으며 대신 &#39;집계 초기화(aggregate initialization)&#39;를 사용하여 초기화됩니다.</li>
<li>가능하다면 <code>std::array</code>를 <code>constexpr</code>로 정의하세요. <code>constexpr</code>로 만들 수 없는 상황이라면 대신 <code>std::vector</code>의 사용을 고려해 보는 것이 좋습니다.</li>
<li>클래스 템플릿 인수 추론(CTAD)을 사용하면 초기화 값을 통해 컴파일러가 <code>std::array</code>의 타입과 길이를 알아서 유추하게 할 수 있습니다.</li>
</ul>
<p><code>std::array</code>는 다음과 같이 선언된 템플릿 구조체(struct)로 구현되어 있습니다:</p>
<pre><code class="language-cpp">template&lt;typename T, std::size_t N&gt; // N은 비타입(non-type) 템플릿 매개변수입니다
struct array;
</code></pre>
<ul>
<li>배열의 길이를 나타내는 비타입 템플릿 매개변수(<code>N</code>)의 타입은 <code>std::size_t</code>입니다.</li>
<li><strong><code>std::array</code>의 길이를 구하는 방법:</strong></li>
<li><code>size()</code> 멤버 함수를 사용하여 길이를 구할 수 있습니다 (부호 없는 <code>size_type</code>으로 반환됨).</li>
<li>C++17에서는 <code>std::size()</code> 비멤버(non-member) 함수를 사용할 수 있습니다 (내부적으로 <code>size()</code> 멤버 함수를 호출하여 동일하게 반환함).</li>
<li>C++20에서는 <code>std::ssize()</code> 비멤버 함수를 사용할 수 있으며, 이 함수는 부호 있는(signed) 큰 정수형(주로 <code>std::ptrdiff_t</code>)으로 길이를 반환합니다.</li>
<li>이 세 가지 함수는 배열이 참조(reference)로 전달된 경우를 제외하고 모두 <code>constexpr</code> 값을 반환합니다. (이 예외적인 결함은 C++23의 P2280에서 수정되었습니다.)</li>
</ul>
<ul>
<li><strong><code>std::array</code> 인덱싱(요소 접근) 방법:</strong></li>
<li>첨자 연산자 (<code>operator[]</code>) 사용: 경계 검사(bounds checking)를 하지 않으므로, 유효하지 않은 인덱스를 넣으면 미정의 동작(undefined behavior)이 발생합니다.</li>
<li><code>at()</code> 멤버 함수 사용: 런타임에 경계 검사를 수행합니다. 하지만 보통 인덱싱 전에 미리 경계 검사를 하거나 컴파일 타임에 검사하는 것을 원하므로, 이 함수는 가급적 피하는 것을 권장합니다.</li>
<li><code>std::get()</code> 함수 템플릿 사용: 인덱스를 비타입 템플릿 인수로 받으며, 컴파일 타임에 경계 검사를 수행합니다.</li>
</ul>
<ul>
<li><code>template &lt;typename T, std::size_t N&gt;</code> (또는 C++20의 <code>template &lt;typename T, auto N&gt;</code>) 형태의 매개변수를 가진 함수 템플릿을 사용하면, 요소 타입과 길이가 제각각인 다양한 <code>std::array</code>를 함수에 전달할 수 있습니다.</li>
<li><code>std::array</code>를 &#39;값으로 반환(return by value)&#39;하면 배열과 모든 요소가 복사됩니다. 배열의 크기가 작고 요소 복사 비용이 저렴하다면 괜찮지만, 상황에 따라서는 출력 매개변수(out parameter)를 사용하는 것이 더 나은 선택일 수 있습니다.</li>
<li>구조체, 클래스, 또는 배열을 이용해 <code>std::array</code>를 초기화할 때, 각 초기화 값에 요소 타입을 명시하지 않으면 컴파일러가 올바르게 해석할 수 있도록 한 쌍의 중괄호를 추가로 씌워주어야 합니다 (<code>{{ }}</code> 형태). 이는 집계 초기화의 특성 때문이며, 리스트 생성자를 사용하는 다른 표준 라이브러리 컨테이너들은 이런 추가 중괄호가 필요하지 않습니다.</li>
<li>C++의 집계형은 <strong>중괄호 생략(brace elision)</strong>이라는 개념을 지원하여 특정 상황에서 중괄호를 생략할 수 있게 해줍니다. 일반적으로 스칼라(단일) 값으로 <code>std::array</code>를 초기화하거나, 각 요소의 타입이 명시적으로 지정된 클래스/배열로 초기화할 때는 중괄호를 생략할 수 있습니다.</li>
<li>참조(reference)로 이루어진 배열은 만들 수 없습니다. 하지만 수정 가능한 lvalue 참조처럼 동작하는 <strong><code>std::reference_wrapper</code></strong>의 배열은 만들 수 있습니다.</li>
<li><code>std::reference_wrapper</code>에 대해 알아둘 점:</li>
<li>할당 연산자(<code>operator=</code>)를 사용하면 참조하는 대상을 다른 객체로 변경(reseat)할 수 있습니다.</li>
<li><code>std::reference_wrapper&lt;T&gt;</code>는 <code>T&amp;</code>로 암시적 변환됩니다.</li>
<li><code>get()</code> 멤버 함수를 사용해 <code>T&amp;</code>를 가져올 수 있으며, 참조 중인 객체의 값을 업데이트할 때 유용합니다.</li>
<li><code>std::ref()</code>와 <code>std::cref()</code> 함수를 사용하면 <code>std::reference_wrapper</code> 객체를 더 쉽게 생성할 수 있습니다.</li>
</ul>
<ul>
<li>CTAD를 사용하는 <code>constexpr std::array</code>가 올바른 개수의 초기화 값을 가졌는지 확인하려면 항상 <code>static_assert</code>를 활용하세요.</li>
<li><strong>C 스타일 배열</strong>은 C 언어에서 물려받았으며 C++ 핵심 언어에 내장되어 있습니다. 핵심 언어의 일부이기 때문에 고유한 선언 문법을 가집니다. 선언 시 대괄호(<code>[]</code>)를 사용하여 컴파일러에게 해당 객체가 C 스타일 배열임을 알립니다. 대괄호 안에는 배열의 길이를 나타내는 <code>std::size_t</code> 타입의 정수 값을 선택적으로 적을 수 있으며, 이 길이는 반드시 상수 표현식이어야 합니다.</li>
<li>C 스타일 배열도 집계형(aggregate)이므로 집계 초기화를 사용할 수 있습니다. 초기화 리스트를 사용해 배열의 모든 요소를 초기화할 때는 길이를 명시하지 않고 컴파일러가 알아서 계산하도록 두는 것이 좋습니다.</li>
<li>C 스타일 배열은 <code>operator[]</code>로 인덱싱할 수 있습니다. 이때 인덱스로 부호 있는 정수, 부호 없는 정수, 또는 범위 없는 열거형을 모두 사용할 수 있습니다. 덕분에 C 스타일 배열은 표준 라이브러리 컨테이너들이 겪는 부호 변환 관련 인덱싱 문제를 겪지 않습니다!</li>
<li>C 스타일 배열은 <code>const</code>나 <code>constexpr</code>로 선언할 수 있습니다.</li>
<li><strong>C 스타일 배열의 길이를 구하는 방법:</strong></li>
<li>C++17: 부호 없는 <code>std::size_t</code>로 길이를 반환하는 <code>std::size()</code> 비멤버 함수를 사용합니다.</li>
<li>C++20: 부호 있는 큰 정수형으로 길이를 반환하는 <code>std::ssize()</code> 비멤버 함수를 사용합니다.</li>
</ul>
<ul>
<li>대부분의 경우, C 스타일 배열이 수식에서 사용되면 배열의 첫 번째 요소(인덱스 0)의 주소를 가진 요소 타입의 포인터로 암시적 변환됩니다. 이를 흔히 <strong>배열 붕괴(array decay)</strong>라고 부릅니다.</li>
<li><strong>포인터 연산(Pointer arithmetic)</strong>은 포인터에 덧셈, 뺄셈, 증감 연산자 등 특정 정수 산술 연산을 적용하여 새로운 메모리 주소를 만들어내는 기능입니다. 포인터 <code>ptr</code>이 있을 때, <code>ptr + 1</code>은 (가리키는 타입의 크기에 맞춰) 메모리상에 있는 그다음 객체의 주소를 반환합니다.</li>
<li>배열의 시작(0번 요소)부터 인덱싱할 때는 첨자 연산자(<code>[]</code>)를 사용하고, 특정 요소부터 상대적인 위치를 찾을 때는 포인터 연산을 사용하는 것이 좋습니다.</li>
<li><strong>C 스타일 문자열</strong>은 요소 타입이 <code>char</code> 또는 <code>const char</code>인 C 스타일 배열일 뿐입니다. 따라서 C 스타일 문자열도 배열 붕괴가 발생합니다.</li>
<li>배열의 <strong>차원(dimension)</strong>은 요소 하나를 선택하는 데 필요한 인덱스의 개수입니다.</li>
<li>차원이 하나뿐인 배열은 1차원 배열(single-dimensional array 또는 1d array)이라고 부릅니다. 배열 안의 배열은 두 개의 첨자가 필요하므로 2차원 배열(2d array)이라고 합니다. 차원이 1보다 큰 배열은 모두 <strong>다차원 배열(multidimensional arrays)</strong>입니다.</li>
<li><strong>배열 평탄화(Flattening)</strong>는 다차원 배열의 차원을 줄여 (주로 1차원 배열로) 만드는 과정을 뜻합니다.</li>
<li>C++23에 도입된 <code>std::mdspan</code>은 메모리에 연속적으로 나열된 요소들을 다차원 배열처럼 다룰 수 있게 해주는 뷰(view)입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 16]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-16</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-16</guid>
            <pubDate>Tue, 03 Mar 2026 11:49:13 GMT</pubDate>
            <description><![CDATA[<h2 id="161--컨테이너와-배열-소개">16.1 — 컨테이너와 배열 소개</h2>
<h3 id="변수가-너무-많아질-때-생기는-문제-확장성-문제">변수가 너무 많아질 때 생기는 문제 (확장성 문제)</h3>
<p>학생 30명의 시험 점수를 기록하고 반 평균을 구하고 싶다고 상상해 볼까요? 
이 작업을 하려면 변수 30개가 필요할 겁니다. 아마 아래처럼 만들어야겠죠.</p>
<pre><code class="language-cpp">// 30개의 정수형 변수를 할당합니다 (각각 이름이 다 달라야 해요!)
int testScore1 {};
int testScore2 {};
int testScore3 {};
// ...
int testScore30 {};
</code></pre>
<p>만들어야 할 변수가 정말 많죠! 
게다가 반 평균을 구하려면 아래처럼 엄청나게 긴 코드를 짜야 할 거예요.</p>
<pre><code class="language-cpp">int average { (testScore1 + testScore2 + testScore3 + testScore4 + testScore5
     + testScore6 + testScore7 + testScore8 + testScore9 + testScore10
     + testScore11 + testScore12 + testScore13 + testScore14 + testScore15
     + testScore16 + testScore17 + testScore18 + testScore19 + testScore20
     + testScore21 + testScore22 + testScore23 + testScore24 + testScore25
     + testScore26 + testScore27 + testScore28 + testScore29 + testScore30)
     / 30; };
</code></pre>
<p>이건 타이핑하기도 힘들 뿐만 아니라, 똑같은 작업을 계속 반복해야 합니다. 
(게다가 숫자 하나를 실수로 잘못 쳐도 눈치채기 어렵겠죠) </p>
<p>그리고 이 값들을 가지고 뭔가 다른 작업(예: 화면에 점수 출력하기)을 하려면, 
이 수많은 변수 이름을 처음부터 끝까지 다 다시 적어야 합니다.</p>
<p>자, 이제 반에 새로운 학생 한 명이 전학을 와서 프로그램을 수정해야 한다고 쳐볼까요? 우리는 전체 코드를 샅샅이 뒤져서 관련된 모든 곳에 <code>testScore31</code> 을 수동으로 직접 추가해야 합니다. </p>
<p>이미 잘 돌아가고 있는 코드를 고치는 건 언제나 새로운 버그를 만들 위험이 따릅니다. 
예를 들어, 평균을 구할 때 나누는 수를 <code>30</code> 에서 <code>31</code> 로 바꾸는 걸 깜빡하기 정말 쉽겠죠!</p>
<p>겨우 30개의 변수만 해도 이 정도인데, 수백 개나 수천 개의 데이터를 다뤄야 한다면 어떨지 상상해 보세요. 똑같은 종류의 데이터가 여러 개 필요할 때, 이렇게 변수를 일일이 하나씩 만드는 방식은 확실히 한계가 있습니다.</p>
<p>데이터를 구조체(struct) 안에 묶어볼 수도 있을 거예요.</p>
<pre><code class="language-cpp">struct testScores
{
    // 30개의 정수형 변수를 할당합니다 (각각 이름이 다 달라야 해요!)
    int score1 {};
    int score2 {};
    int score3 {};
    // ...
    int score30 {};
}
</code></pre>
<p>이렇게 하면 점수들을 조금 더 깔끔하게 정리할 수 있고 함수에 데이터를 통째로 넘겨주기도 쉬워지지만, 가장 핵심적인 문제는 해결되지 않습니다. 여전히 각각의 점수 데이터를 하나하나 만들고, 일일이 다른 이름을 불러서 써야 한다는 점은 똑같거든요.</p>
<p>눈치채셨겠지만, C++에는 이런 골치 아픈 문제를 해결할 수 있는 방법들이 있습니다. 이번 장에서는 그중 한 가지 해결책을 소개해 드릴 거고요, 다음 장들에서는 또 다른 유용한 변형 방법들을 알아볼 예정입니다.</p>
<hr>
<h3 id="컨테이너">컨테이너</h3>
<p>마트에 달걀 12개를 사러 갔다고 생각해 보세요. 달걀을 하나하나 낱개로 골라서 장바구니에 담지는 않으시죠? (안 그러시죠..?) 대신, 12개가 미리 담겨 있는 달걀 판 하나를 고를 겁니다.</p>
<p>여기서 달걀 판이 바로 일종의 <strong>컨테이너(용기)</strong> 로, 미리 정해진 개수(보통 6개, 12개, 24개)의 달걀을 담아두는 역할을 합니다.</p>
<p>시리얼은 어떨까요? 그 수많은 작은 시리얼 조각들을 찬장에 낱개로 흩뿌려 보관하고 싶진 않으실 거예요! 시리얼은 보통 종이상자에 담겨 나오는데, 이 상자 역시 컨테이너입니다. 우리는 일상생활에서 여러 개의 물건을 하나로 묶어 쉽게 관리하기 위해 이런 컨테이너들을 아주 흔하게 사용합니다.</p>
<p>프로그래밍에도 컨테이너가 있습니다. 아주 많을 수도 있는 여러 개의 데이터(객체)를 하나로 묶어서 쉽게 만들고 관리하기 위해서죠. </p>
<p>프로그래밍에서 <strong>컨테이너</strong>란 이름이 없는 여러 개의 데이터(이를 <strong>요소(element)</strong> 라고 부릅니다)를 저장할 수 있는 공간을 제공하는 데이터 타입을 말합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
우리는 주로 서로 관련 있는 여러 개의 값들을 한 묶음으로 다뤄야 할 때 컨테이너를 사용합니다.</p>
</blockquote>
<p>사실 여러분은 이미 컨테이너를 하나 사용해 보셨습니다. 바로 문자열(string)이에요! 문자열 컨테이너는 여러 개의 &#39;문자(character)&#39;들을 모아서 저장해 주고, 나중에 텍스트로 출력할 수 있게 해줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

int main()
{
    std::string name{ &quot;Alex&quot; }; // 문자열(string)은 문자들을 담는 컨테이너입니다.
    std::cout &lt;&lt; name; // 문자열을 일련의 문자들의 연속으로 출력합니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="컨테이너-안의-요소들에는-이름이-없습니다">컨테이너 안의 요소들에는 이름이 없습니다</h3>
<p>컨테이너 자체에는 보통 이름이 있습니다. (이름이 없으면 어떻게 코딩할 때 꺼내 쓰겠어요?) 하지만 컨테이너 &#39;안&#39;에 들어있는 개별 요소들에는 이름이 없습니다. 만약 요소마다 이름이 있어야 한다면, 변수가 100개일 때 각기 다른 이름 100개를 지어줘야 할 테니까요! 이렇게 요소들에게 고유한 이름을 지어주지 않아도 원하는 만큼 데이터를 마구 넣을 수 있다는 점이 정말 중요하며, 일반적인 구조체와 컨테이너를 구분 짓는 가장 큰 차이점입니다. 앞서 살펴본 <code>testScores</code> 처럼 평범한 구조체들은 안에 든 데이터마다 각각의 이름이 꼭 필요하기 때문에 보통 컨테이너라고 부르지 않습니다.</p>
<p>위의 문자열 예시를 보면, 컨테이너 자체는 <code>name</code> 이라는 이름을 가졌지만, 그 안에 들어있는 개별 문자들(&#39;A&#39;, &#39;l&#39;, &#39;e&#39;, &#39;x&#39;)에는 각각의 이름이 따로 없습니다.</p>
<p>그렇다면 요소들의 이름이 없는데, 대체 어떻게 꺼내 쓸 수 있을까요? 모든 컨테이너는 안에 있는 데이터를 꺼내 쓸 수 있는 각자만의 &#39;방법&#39;들을 제공합니다. 정확히 어떤 방법을 쓰는지는 컨테이너의 종류마다 다릅니다. 이 부분은 다음 레슨에서 첫 번째 예시와 함께 자세히 알아볼 거예요.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
컨테이너 안의 요소들은 각자의 이름이 없습니다. 덕분에 프로그래머가 일일이 이름을 지어주는 수고를 하지 않아도, 컨테이너 안에 원하는 만큼 많은 데이터를 넣을 수 있습니다.</p>
<p>각 컨테이너는 이 이름 없는 데이터들에 접근할 수 있는 고유한 방법을 제공하지만, 그 방법은 컨테이너의 종류에 따라 다릅니다.</p>
</blockquote>
<hr>
<h3 id="컨테이너의-길이">컨테이너의 길이</h3>
<p>프로그래밍에서는 컨테이너 안에 들어있는 데이터(요소)의 개수를 주로 <strong>길이(length)</strong> 또는 <strong>카운트(count)</strong> 라고 부릅니다.</p>
<p>이전 레슨인 &#39;5.7 — std::string 소개&#39;에서 <code>std::string</code> 의 <code>length</code> 라는 기능을 사용해 문자열 컨테이너 안에 문자가 몇 개 들어있는지 알아내는 방법을 보여드렸었죠.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

int main()
{
    std::string name{ &quot;Alex&quot; };
    std::cout &lt;&lt; name &lt;&lt; &quot; has &quot; &lt;&lt; name.length() &lt;&lt; &quot; characters\n&quot;;

    return 0;
}
</code></pre>
<p>위 코드는 이렇게 출력됩니다.
<code>Alex has 4 characters</code></p>
<p>C++에서는 컨테이너 안의 요소 개수를 말할 때 <strong>크기(size)</strong> 라는 단어도 정말 자주 씁니다. 하지만 이건 좀 아쉬운 작명이에요. 왜냐하면 &quot;크기(size)&quot;라는 말은 데이터가 메모리 공간을 몇 바이트나 차지하는지(<code>sizeof</code> 연산자가 알려주는 값)를 뜻하기도 하거든요.</p>
<p>그래서 우리는 헷갈리지 않게, 컨테이너 안에 들어있는 &#39;데이터의 개수&#39;를 말할 때는 가급적 &quot;길이(length)&quot;라는 용어를 쓰고, 데이터가 메모리에서 차지하는 &#39;저장 공간의 용량&#39;을 말할 때는 &quot;크기(size)&quot;라는 용어를 쓰겠습니다.</p>
<hr>
<h3 id="컨테이너로-할-수-있는-작업들-연산">컨테이너로 할 수 있는 작업들 (연산)</h3>
<p>다시 달걀 판 이야기로 돌아가 볼까요? 달걀 판으로 뭘 할 수 있을까요? 일단 달걀 판을 구해야겠죠. 그리고 뚜껑을 열어서 달걀을 하나 고를 수도 있고, 그 달걀로 마음대로 요리를 할 수도 있습니다. 판에서 달걀을 빼낼 수도 있고, 빈자리에 새 달걀을 채워 넣을 수도 있습니다. 달걀이 몇 개 남았는지 세어볼 수도 있죠.</p>
<p>이와 비슷하게, 프로그래밍의 컨테이너들도 보통 다음과 같은 기능들을 상당수 제공합니다.</p>
<ul>
<li><strong>컨테이너 만들기</strong> (예: 텅 빈 컨테이너 만들기, 미리 정해둔 개수만큼 공간 확보하기, 초기값들을 넣어두고 시작하기 등)</li>
<li><strong>요소에 접근하기</strong> (예: 첫 번째 데이터 가져오기, 마지막 데이터 가져오기, 아무 데이터나 가져오기)</li>
<li><strong>요소 넣고 빼기</strong> (삽입과 삭제)</li>
<li><strong>컨테이너에 들어있는 요소 개수 확인하기</strong></li>
</ul>
<p>물론 컨테이너를 관리하기 편하게 해주는 다른 추가 기능들을 제공하기도 합니다.</p>
<p>요즘 나오는 프로그래밍 언어들은 아주 다양한 종류의 컨테이너를 제공합니다. 이 컨테이너들은 구체적으로 어떤 기능들을 지원하는지, 그리고 그 기능이 얼마나 &#39;빠른지(성능)&#39;에 따라 차이가 납니다. 어떤 컨테이너는 아무 데이터나 엄청나게 빨리 찾을 수 있지만, 데이터를 중간에 넣거나 빼는 건 못할 수도 있습니다. 반대로 데이터를 넣고 빼는 건 빛의 속도인데, 데이터를 찾을 때는 처음부터 순서대로 하나씩 찾아야만 하는 컨테이너도 있죠.</p>
<p>모든 컨테이너는 저마다의 장점과 한계가 있습니다. 지금 내가 풀고 있는 문제에 딱 맞는 컨테이너를 고르는 것은 코드를 나중에 고치기 쉽게 만들고 전체적인 프로그램 속도를 높이는데 엄청나게 중요합니다. 이 주제는 나중에 다른 레슨에서 더 깊이 다루겠습니다.</p>
<hr>
<h3 id="요소의-자료형">요소의 자료형</h3>
<p>대부분의 프로그래밍 언어(C++ 포함)에서 컨테이너는 <strong>동종(homogenous)</strong> 입니다. 말이 좀 어렵지만, 쉽게 말해 <strong>&quot;컨테이너 안에는 모두 똑같은 자료형의 데이터만 넣어야 한다&quot;</strong> 는 뜻입니다. (정수 컨테이너에는 정수만, 문자 컨테이너에는 문자만!)</p>
<p>어떤 컨테이너는 아예 들어갈 데이터 타입이 미리 정해져 있기도 합니다. (예: 문자열 컨테이너는 주로 문자(char)만 담습니다.) 하지만 대부분은 프로그래머가 직접 컨테이너에 담을 데이터 타입을 마음대로 정할 수 있습니다. C++에서는 보통 컨테이너를 &#39;클래스 템플릿(class template)&#39;이라는 것으로 구현해 둬서, 프로그래머가 원하는 타입을 템플릿 인자로 넘겨주면 그 타입에 맞게 알아서 변신하게끔 되어 있습니다. 이 내용은 다음 레슨에서 예시로 보여드릴게요.</p>
<p>덕분에 새로운 타입의 데이터를 담을 때마다 새로운 컨테이너를 굳이 또 만들 필요가 없어서 아주 유연하고 편리합니다. 그냥 기존 컨테이너에 &quot;나 이번엔 정수 담을래!&quot; 하고 지정만 해주면 바로 쓸 수 있거든요.</p>
<blockquote>
<p><strong>참고로 알아두세요...</strong>
동종 컨테이너의 반대말은 <strong>이종(heterogenous)</strong> 컨테이너입니다. 서로 다른 타입의 데이터들을 한 바구니에 마구 섞어 담을 수 있는 컨테이너죠. 파이썬(Python) 같은 스크립트 언어들에서 이런 이종 컨테이너를 주로 지원합니다.</p>
</blockquote>
<hr>
<h3 id="c에서의-컨테이너">C++에서의 컨테이너</h3>
<p>C++ 표준 라이브러리(Standard Library)에는 흔히 쓰이는 컨테이너들을 만들어 모아둔 <strong>컨테이너 라이브러리(Containers library)</strong> 라는 부분이 있습니다. 이런 컨테이너 역할을 하는 클래스를 <strong>컨테이너 클래스(container class)</strong> 라고 부릅니다. </p>
<p>C++에서 &quot;컨테이너&quot;라는 단어는 일반적인 프로그래밍에서 쓰이는 의미보다 좀 더 좁은 의미로 쓰입니다. C++에서는 오직 이 &#39;컨테이너 라이브러리&#39; 안에 들어있는 클래스들만 공식적인 컨테이너로 인정해 줍니다. 그래서 우리는 일반적인 개념을 말할 때는 그냥 &quot;컨테이너&quot;라고 부르고, C++ 라이브러리 안에 있는 특정 클래스를 콕 집어 말할 때는 &quot;컨테이너 클래스&quot;라고 부르겠습니다.</p>
<blockquote>
<p><strong>심화 학습자를 위한 참고</strong>
다음의 타입들은 일반적인 프로그래밍 관점에서는 컨테이너가 맞지만, C++ 표준에서는 공식 컨테이너로 인정하지 않습니다.</p>
<ul>
<li>C 스타일 배열 (C-style arrays)</li>
<li><code>std::string</code></li>
<li><code>std::vector&lt;bool&gt;</code></li>
</ul>
<p>C++에서 공식 컨테이너로 인정받으려면 여기서 명시된 모든 까다로운 조건들을 지켜야 합니다. (이 조건들에는 특정 함수들을 반드시 구현해야 한다는 내용도 있어서, C++의 공식 컨테이너는 무조건 클래스 형태여야만 합니다!) 위에 적힌 타입들은 이 모든 조건을 완벽히 지키지는 않거든요.
하지만 <code>std::string</code> 과 <code>std::vector&lt;bool&gt;</code> 은 대부분의 핵심 조건을 지키고 있어서 실제 상황에서는 컨테이너처럼 똑같이 동작합니다. 그래서 이들을 종종 &quot;유사 컨테이너(pseudo-containers)&quot;라고 부르기도 합니다.</p>
</blockquote>
<p>제공되는 여러 컨테이너 클래스 중에서도 <code>std::vector</code> 와 <code>std::array</code> 가 압도적으로 가장 많이 쓰입니다. 그래서 앞으로 우리도 이 두 가지에 거의 모든 집중을 쏟을 겁니다. 
다른 컨테이너 클래스들은 아주 특별한 상황에서만 가끔 쓰이는 편입니다.</p>
<hr>
<h3 id="배열array-소개">배열(Array) 소개</h3>
<p><strong>배열(Array)</strong> 은 데이터들을 <strong>연속적으로</strong> 저장하는 컨테이너 자료형입니다.<br>&quot;연속적&quot;이라는 말은 데이터들이 메모리상에 중간에 빈 공간 없이 다닥다닥 붙어서 차례대로 저장된다는 뜻입니다. 배열은 이런 특징 덕분에 어떤 위치에 있는 데이터든 아주 빠르고 직접적으로 꺼내 볼 수 있습니다. 개념 자체도 아주 단순하고 쓰기 쉬워서, 서로 관련된 여러 데이터를 다뤄야 할 때 우리가 가장 먼저 선택하는 도구입니다.</p>
<p>C++에는 크게 세 가지 종류의 기본 배열이 있습니다: (C 스타일) 배열, <code>std::vector</code> 컨테이너 클래스, 그리고 <code>std::array</code> 컨테이너 클래스입니다.</p>
<ul>
<li><strong>(C 스타일) 배열</strong> 은 옛날 C 언어 시절부터 물려받은 유산입니다. 예전 코드들과의 호환성을 위해 기본 C++ 언어 자체에 내장되어 있습니다. C++ 표준에서는 이걸 그냥 &quot;배열&quot;이라고 부르지만, 요즘 C++ 프로그래머들은 이름이 비슷한 <code>std::array</code> 와 헷갈리지 않게 보통 <strong>C 배열</strong> 이나 <strong>C 스타일 배열</strong> 이라고 부릅니다. (&quot;벌거벗은 배열&quot;, &quot;크기가 고정된 배열&quot;, &quot;고정 배열&quot;, &quot;내장 배열&quot;이라고도 불러요.) 우리는 &quot;C 스타일 배열&quot;이라는 용어를 가장 선호하며, 세 가지 종류를 통틀어 말할 때만 &quot;배열&quot;이라고 부르겠습니다. 최신 프로그래밍 기준에서 볼 때, C 스타일 배열은 가끔 이상하게 동작할 때가 있고 심지어 <strong>위험하기까지 합니다.</strong> 왜 위험한지는 나중에 알아볼게요.</li>
<li>C++에서 배열을 좀 더 안전하고 쓰기 쉽게 만들기 위해 2003년(C++03)에 도입된 것이 바로 <strong><code>std::vector</code></strong> 컨테이너 클래스입니다. <code>std::vector</code> 는 세 가지 배열 중에 가장 유연하고, 다른 배열들에는 없는 아주 다양하고 유용한 능력들을 많이 가지고 있습니다.</li>
<li>마지막으로, 위험한 C 스타일 배열을 직접적으로 대체하기 위해 2011년(C++11)에 <strong><code>std::array</code></strong> 컨테이너 클래스가 등장했습니다. <code>std::vector</code> 보다는 기능이 제한적이지만, 특히 크기가 작은 배열을 다룰 때 성능이 더 효율적일 수 있습니다.</li>
</ul>
<p>현대의 C++ 프로그래밍에서도 이 세 가지 배열은 각자의 상황과 역할에 맞게 모두 쓰이고 있기 때문에, 우리는 세 가지 모두를 적절한 깊이로 공부할 예정입니다.</p>
<hr>
<h3 id="앞으로-나아가기">앞으로 나아가기</h3>
<p>다음 레슨에서는 우리의 첫 번째 컨테이너 클래스인 <code>std::vector</code> 를 소개하고, 이 글의 맨 처음에 얘기했던 &quot;변수가 너무 많아지는 확장성 문제&quot;를 어떻게 효율적으로 해결하는지 그 여정을 시작해 보겠습니다. <code>std::vector</code> 와 관련된 새로운 개념들을 아주 많이 배워야 하고, 중간중간 생기는 또 다른 추가적인 문제들도 해결해야 해서 여기서 시간을 꽤 많이 보낼 거예요.</p>
<p>좋은 점이 하나 있다면, 모든 컨테이너 클래스들은 사용하는 방법(인터페이스)이 꽤 비슷하다는 점입니다. 그래서 하나(예: <code>std::vector</code>)를 확실히 다룰 줄 알게 되면, 다른 것(예: <code>std::array</code>)을 배우는 건 훨씬 쉽습니다. 나중에 다른 컨테이너를 배울 때는 눈에 띄는 차이점들 위주로 다루고 가장 중요한 핵심만 다시 짚어보도록 할게요.</p>
<h3 id="저자의-말-용어-정리">저자의 말 (용어 정리)</h3>
<p>용어에 대해 짧게 정리하고 갈게요!</p>
<ul>
<li>표준 라이브러리에 있는 대부분의 혹은 모든 컨테이너 클래스에 두루두루 적용되는 이야기를 할 때는 <strong>컨테이너 클래스(container classes)</strong> 라는 용어를 쓰겠습니다.</li>
<li>다른 프로그래밍 언어의 배열들까지 포함해서 일반적인 모든 종류의 배열에 공통으로 적용되는 이야기를 할 때는 <strong>배열(array)</strong> 이라는 용어를 쓰겠습니다.</li>
</ul>
<p><code>std::vector</code> 는 이 두 가지 카테고리에 모두 속하기 때문에, 제가 어떤 용어를 쓰든 간에 <code>std::vector</code> 에도 당연히 적용되는 말이라고 생각하시면 됩니다.</p>
<hr>
<h2 id="162--stdvector-소개와-리스트-생성자">16.2 — <code>std::vector</code> 소개와 리스트 생성자</h2>
<p>이전 강의에서는 컨테이너와 배열을 함께 소개했습니다. 이번 강의에서는 이 장에서 계속 사용할 배열 타입인 <code>std::vector</code> 를 소개합니다. 그리고 지난 강의에서 이야기한 확장성 문제의 한 부분도 해결해 보겠습니다.</p>
<hr>
<h3 id="stdvector-소개"><code>std::vector</code> 소개</h3>
<p><code>std::vector</code> 는 C++ 표준 컨테이너 라이브러리에 있는 컨테이너 클래스 중 하나로, 배열을 구현합니다. <code>std::vector</code> 는 <code>&lt;vector&gt;</code> 헤더에 클래스 템플릿으로 정의되어 있고, 템플릿 타입 인수가 원소의 타입을 결정합니다. 그래서 <code>std::vector&lt;int&gt;</code> 는 <code>int</code> 타입 원소를 담는 <code>std::vector</code> 를 뜻합니다.</p>
<p><code>std::vector</code> 객체를 만드는 방법은 간단합니다.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

int main()
{
    // 값 초기화 (기본 생성자 사용)
    std::vector&lt;int&gt; empty{}; // int 원소 0개를 담는 vector

    return 0;
}</code></pre>
<p>변수 <code>empty</code> 는 원소 타입이 <code>int</code> 인 <code>std::vector</code> 입니다. 여기서는 값 초기화를 사용했기 때문에, 이 벡터는 비어 있는 상태(즉, 원소가 없는 상태)로 시작합니다.</p>
<p>원소가 하나도 없는 벡터는 지금은 별로 쓸모없어 보일 수 있습니다. 
하지만 이후 강의에서 다시 보게 됩니다.</p>
<hr>
<h3 id="값-목록으로-stdvector-초기화하기">값 목록으로 <code>std::vector</code> 초기화하기</h3>
<p>컨테이너의 목적은 관련된 값들을 관리하는 것이므로, 대부분의 경우에는 처음부터 값을 넣어서 만들고 싶습니다. 이때 원하는 초기값들을 넣은 리스트 초기화를 사용하면 됩니다. 예를 들면 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

int main()
{
    // 리스트 생성 (리스트 생성자 사용)
    std::vector&lt;int&gt; primes{ 2, 3, 5, 7 };          // 값이 2, 3, 5, 7 인 int 원소 4개를 담는 vector
    std::vector vowels { &#39;a&#39;, &#39;e&#39;, &#39;i&#39;, &#39;o&#39;, &#39;u&#39; }; // 값이 &#39;a&#39;, &#39;e&#39;, &#39;i&#39;, &#39;o&#39;, &#39;u&#39; 인 char 원소 5개를 담는 vector. CTAD (C++17)로 원소 타입 char 를 자동 추론함 (권장)

    return 0;
}</code></pre>
<p><code>primes</code> 에서는 원소 타입이 <code>int</code> 인 <code>std::vector</code> 를 만들겠다고 직접 지정했습니다. 
초기값 4개를 넣었으므로, <code>primes</code> 는 값이 <code>2</code>, <code>3</code>, <code>5</code>, <code>7</code> 인 원소 4개를 가집니다.</p>
<p><code>vowels</code> 에서는 원소 타입을 직접 쓰지 않았습니다. 대신 C++17의 CTAD를 사용해서, 컴파일러가 초기값을 보고 원소 타입을 자동으로 추론하게 했습니다. 초기값 5개를 넣었으므로, <code>vowels</code> 는 값이 <code>&#39;a&#39;</code>, <code>&#39;e&#39;</code>, <code>&#39;i&#39;</code>, <code>&#39;o&#39;</code>, <code>&#39;u&#39;</code> 인 원소 5개를 가집니다.</p>
<hr>
<h3 id="리스트-생성자와-초기화-리스트">리스트 생성자와 초기화 리스트</h3>
<p>이제 위 코드가 어떻게 동작하는지 조금 더 자세히 보겠습니다.</p>
<p>강의 <code>13.8</code> 에서 초기화 리스트는 중괄호 안에 쉼표로 구분된 값 목록이라고 배웠습니다.
(예: <code>{ 1, 2, 3 }</code>)</p>
<p>컨테이너에는 보통 <strong>리스트 생성자</strong> 라는 특별한 생성자가 있습니다. 
이 생성자는 초기화 리스트를 사용해서 컨테이너 객체를 만들 수 있게 해 줍니다. 
리스트 생성자는 보통 다음 세 가지를 합니다.</p>
<ol>
<li>모든 초기값을 담을 수 있도록 충분한 저장 공간을 마련합니다(필요하면).</li>
<li>컨테이너의 길이를 초기화 리스트 원소 수에 맞게 설정합니다(필요하면).</li>
<li>원소들을 초기화 리스트의 값으로 순서대로 초기화합니다.</li>
</ol>
<p>즉, 컨테이너에 값들의 초기화 리스트를 넘기면 리스트 생성자가 호출되고, 그 값들로 컨테이너가 만들어집니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
원소 값들을 넣어 컨테이너를 만들 때는, 
그 값들의 초기화 리스트와 함께 리스트 초기화를 사용하세요.</p>
</blockquote>
<blockquote>
<p><strong>관련 내용</strong>
직접 만든 클래스에 리스트 생성자를 추가하는 방법은 
<code>23.7 -- std::initializer_list</code> 에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="첨자-연산자-operator-로-배열-원소에-접근하기">첨자 연산자 <code>operator[]</code> 로 배열 원소에 접근하기</h3>
<p>이제 원소들의 배열을 만들었으니, 각 원소에는 어떻게 접근할까요?</p>
<p>잠깐 비유를 들어 봅시다. 똑같이 생긴 우편함 여러 개가 나란히 있다고 생각해 보세요. 구분하기 쉽게 각 우편함 앞에는 번호가 적혀 있습니다. </p>
<p>첫 번째 우편함은 <code>0</code>, 두 번째는 <code>1</code> 식입니다. 
그래서 “번호 <code>0</code> 우편함에 넣으세요”라고 하면 첫 번째 우편함을 뜻한다는 걸 알 수 있습니다.</p>
<p>C++에서 배열 원소에 접근하는 가장 흔한 방법은 배열 이름과 첨자 연산자 <code>operator[]</code> 를 함께 쓰는 것입니다. 특정 원소를 고르려면 대괄호 안에 원하는 원소를 가리키는 정수 값을 넣습니다. 이 정수 값을 <strong>첨자(subscript)</strong> 또는 보통 <strong>인덱스(index)</strong> 라고 부릅니다. 
우편함 예시처럼 첫 번째 원소는 인덱스 <code>0</code>, 두 번째 원소는 인덱스 <code>1</code> 로 접근합니다.</p>
<p>예를 들어 <code>primes[0]</code> 은 <code>primes</code> 배열에서 인덱스 <code>0</code> 인 원소(첫 번째 원소)를 반환합니다. 첨자 연산자는 원소의 <strong>복사본</strong>이 아니라 <strong>실제 원소에 대한 참조</strong>를 돌려줍니다. 그래서 배열 원소에 접근한 뒤에는 일반 변수처럼 사용할 수 있습니다(예: 값을 대입하거나, 출력하거나).</p>
<p>인덱스가 <code>1</code> 이 아니라 <code>0</code> 부터 시작하므로, C++의 배열은 <strong>0부터 시작하는</strong> 배열이라고 합니다. 보통 우리는 <code>1</code> 부터 세는 데 익숙하므로, 처음에는 헷갈릴 수 있습니다.</p>
<hr>
<h3 id="핵심-아이디어">핵심 아이디어</h3>
<p>인덱스는 실제로 배열의 첫 번째 원소로부터의 <strong>거리(offset)</strong> 입니다.</p>
<p>배열의 첫 번째 원소에서 시작해서 <code>0</code> 칸 이동하면, 여전히 첫 번째 원소에 있습니다. 그래서 인덱스 <code>0</code> 이 첫 번째 원소입니다.</p>
<p>배열의 첫 번째 원소에서 시작해서 <code>1</code> 칸 이동하면, 두 번째 원소에 도착합니다. 
그래서 인덱스 <code>1</code> 이 두 번째 원소입니다.</p>
<p>인덱스가 절대 위치가 아니라 상대 거리라는 점은 
<code>17.9 -- Pointer arithmetic and subscripting</code> 에서 더 자세히 다룹니다.</p>
<p>이 때문에 말로 설명할 때는 조금 헷갈릴 수 있습니다. 
예를 들어 “배열 원소 1” 이라고 하면, 인덱스 <code>0</code> 인 첫 번째 원소를 뜻하는지, 인덱스 <code>1</code> 인 두 번째 원소를 뜻하는지 모호할 수 있습니다. 그래서 보통은 인덱스보다 <strong>순서</strong>로 말합니다(예: “첫 번째 원소”는 인덱스 <code>0</code> 인 원소).</p>
<p>예를 들어 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector primes { 2, 3, 5, 7, 11 }; // 처음 5개의 소수를 저장함 (int)

    std::cout &lt;&lt; &quot;The first prime number is: &quot; &lt;&lt; primes[0] &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;The second prime number is: &quot; &lt;&lt; primes[1] &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;The sum of the first 5 primes is: &quot; &lt;&lt; primes[0] + primes[1] + primes[2] + primes[3] + primes[4] &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p>출력 결과:</p>
<pre><code class="language-text">The first prime number is: 2
The second prime number is: 3
The sum of the first 5 primes is: 28</code></pre>
<p>배열을 사용하면 이제 소수 5개를 저장하기 위해 이름이 다른 변수 5개를 따로 만들 필요가 없습니다. 대신 원소가 5개인 배열 <code>primes</code> 하나만 만들고, 인덱스 값만 바꿔서 다른 원소에 접근하면 됩니다.</p>
<p><code>operator[]</code> 와 배열 원소에 접근하는 다른 방법들은 다음 강의 <code>16.3 -- std::vector and the unsigned length and subscript problem</code> 에서 더 이야기합니다.</p>
<hr>
<h3 id="범위를-벗어난-첨자">범위를 벗어난 첨자</h3>
<p>배열에 인덱스로 접근할 때는, 넣은 인덱스가 반드시 배열 안에 있는 유효한 원소를 가리켜야 합니다. 즉, 길이가 <code>N</code> 인 배열이라면 첨자는 <code>0</code> 부터 <code>N-1</code> 까지의 값이어야 합니다(양 끝 포함).</p>
<p><code>operator[]</code> 는 <strong>범위 검사</strong>를 하지 않습니다. 즉, 인덱스가 <code>0</code> 부터 <code>N-1</code> 사이인지 확인하지 않습니다. 잘못된 인덱스를 <code>operator[]</code> 에 넘기면 <strong>정의되지 않은 동작</strong>이 발생합니다.</p>
<p>음수 인덱스를 쓰면 안 된다는 것은 비교적 기억하기 쉽습니다. 하지만 인덱스가 <code>N</code> 인 원소는 없다는 점은 놓치기 쉽습니다. 배열의 마지막 원소 인덱스는 <code>N-1</code> 이므로, 인덱스 <code>N</code> 을 쓰면 배열 끝 바로 다음 위치의 원소에 접근하려고 하게 됩니다.</p>
<blockquote>
<p><strong>팁</strong>
원소가 <code>N</code> 개인 배열에서 첫 번째 원소의 인덱스는 <code>0</code>, 두 번째는 <code>1</code>, 마지막은 <code>N-1</code> 입니다. 인덱스가 <code>N</code> 인 원소는 없습니다.</p>
</blockquote>
<p>첨자로 <code>N</code> 을 사용하면 정의되지 않은 동작이 발생합니다.
(실제로는 배열에 없는 <code>N+1</code> 번째 원소에 접근하려는 것이기 때문입니다)</p>
<blockquote>
<p><strong>팁</strong>
일부 컴파일러(예: Visual Studio)는 인덱스가 유효한지 런타임 <code>assert</code> 로 검사해 줍니다. 이런 경우 디버그 모드에서 잘못된 인덱스를 사용하면 프로그램이 <code>assert</code> 에 걸려 중단됩니다. 릴리스 모드에서는 이 <code>assert</code> 가 제거되므로 성능 손해는 없습니다.</p>
</blockquote>
<hr>
<h3 id="배열은-메모리에-연속으로-배치된다">배열은 메모리에 연속으로 배치된다</h3>
<p>배열의 중요한 특징 중 하나는, 원소들이 메모리 안에 <strong>항상 연속으로</strong> 배치된다는 점입니다. 
즉, 원소들이 메모리에서 서로 바로 옆에 붙어 있고, 중간에 빈 공간이 없습니다.</p>
<p>예를 들어 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector primes { 2, 3, 5, 7, 11 }; // 처음 5개의 소수를 저장함 (int)

    std::cout &lt;&lt; &quot;An int is &quot; &lt;&lt; sizeof(int) &lt;&lt; &quot; bytes\n&quot;;
    std::cout &lt;&lt; &amp;(primes[0]) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &amp;(primes[1]) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &amp;(primes[2]) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p>작성자의 컴퓨터에서 한 번 실행했을 때 결과는 다음과 같았습니다.</p>
<pre><code class="language-text">An int is 4 bytes
00DBF720
00DBF724
00DBF728</code></pre>
<p>이 <code>int</code> 원소들의 메모리 주소는 서로 <code>4</code> 바이트 차이가 납니다. 
이것은 작성자의 컴퓨터에서 <code>int</code> 의 크기와 같습니다.</p>
<p>이 말은 배열이 <strong>원소마다 추가로 드는 숨은 비용</strong> 이 없다는 뜻입니다. 
또, 컴파일러가 배열 안의 어떤 원소 주소든 빠르게 계산할 수 있게 해 줍니다.</p>
<blockquote>
<p><strong>관련 내용</strong></p>
</blockquote>
<p>첨자 접근이 어떤 계산으로 이루어지는지는 
<code>17.9 -- Pointer arithmetic and subscripting</code> 에서 설명합니다.</p>
<blockquote>
</blockquote>
<p>배열은 몇 안 되는 <strong>임의 접근(random access)</strong> 이 가능한 컨테이너입니다. 
즉, 컨테이너 안의 어떤 원소든 바로 접근할 수 있습니다.
(반대로 순차 접근(sequential access)은 정해진 순서대로만 접근해야 합니다)</p>
<blockquote>
</blockquote>
<p>배열 원소에 대한 임의 접근은 보통 빠르고, 배열을 아주 쓰기 쉽게 만들어 줍니다. 이것이 배열이 다른 컨테이너보다 자주 선호되는 중요한 이유 중 하나입니다.</p>
<hr>
<h3 id="특정-길이의-stdvector-만들기">특정 길이의 <code>std::vector</code> 만들기</h3>
<p>사용자에게 값 10개를 입력받아 <code>std::vector</code> 에 저장한다고 생각해 봅시다. 
이 경우, 아직 값을 넣기 전이라도 길이가 <code>10</code> 인 <code>std::vector</code> 가 먼저 필요합니다. 
그럼 어떻게 해야 할까요?</p>
<p>이렇게 초기화 리스트에 자리 채움용 값 10개를 넣어 만들 수도 있습니다.</p>
<pre><code class="language-cpp">std::vector&lt;int&gt; data { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; // int 값 10개를 담는 vector</code></pre>
<p>하지만 이 방법은 여러 이유로 좋지 않습니다. 
타이핑이 너무 많고, 초기값이 몇 개인지 한눈에 보기 어렵습니다. 
나중에 개수를 바꾸고 싶어도 수정하기 불편합니다.</p>
<p>다행히 <code>std::vector</code> 에는 <code>std::size_t</code> 값 하나를 받아서, 그 길이의 <code>std::vector</code> 를 만드는 <code>explicit</code> 생성자(<code>explicit std::vector&lt;T&gt;(std::size_t)</code>)가 있습니다.</p>
<pre><code class="language-cpp">std::vector&lt;int&gt; data( 10 ); // int 원소 10개를 담는 vector, 값 초기화되어 0 이 됨</code></pre>
<p>이렇게 만들어진 각 원소는 값 초기화됩니다. 
<code>int</code> 의 경우에는 <code>0</code> 으로 초기화되고, 클래스 타입이면 기본 생성자가 호출됩니다.</p>
<p>하지만 이 생성자를 사용할 때 주의할 점이 하나 있습니다. 
이 생성자는 반드시 <strong>직접 초기화</strong>로 호출해야 합니다.</p>
<hr>
<h3 id="비어-있지-않은-초기화-리스트는-리스트-생성자를-우선한다">비어 있지 않은 초기화 리스트는 리스트 생성자를 우선한다</h3>
<p>왜 이전 생성자를 직접 초기화로 호출해야 하는지 이해하려면, 다음 정의를 보세요.</p>
<pre><code class="language-cpp">std::vector&lt;int&gt; data{ 10 }; // 이것은 무엇을 할까?</code></pre>
<p>이 초기화는 서로 다른 두 생성자와 맞을 수 있습니다.</p>
<ol>
<li><p><code>{ 10 }</code> 을 초기화 리스트로 해석하면, 
리스트 생성자와 매칭되어 <strong>길이가 1이고 값이 <code>10</code> 인 벡터</strong> 가 됩니다.</p>
</li>
<li><p><code>{ 10 }</code> 을 중괄호로 감싼 단일 초기화 값으로 해석하면, <code>std::vector&lt;T&gt;(std::size_t)</code> 생성자와 매칭되어 <strong>길이가 <code>10</code> 이고 각 원소가 <code>0</code> 으로 값 초기화된 벡터</strong> 가 됩니다.</p>
</li>
</ol>
<p>보통 클래스 타입 초기화가 둘 이상의 생성자와 맞으면 모호하다고 판단되어 컴파일 오류가 납니다. 하지만 C++에는 이 경우를 위한 특별 규칙이 있습니다. </p>
<p><strong>초기화 리스트가 비어 있지 않으면, 맞는 리스트 생성자가 다른 생성자보다 우선 선택됩니다.</strong> 
이 규칙이 없으면, 리스트 생성자는 인수를 하나만 받는 다른 생성자들과 자주 충돌하게 됩니다.</p>
<p><code>{ 10 }</code> 은 초기화 리스트로 해석될 수 있고, <code>std::vector</code> 에는 리스트 생성자가 있으므로 이 경우에는 리스트 생성자가 우선됩니다.</p>
<hr>
<h3 id="핵심-아이디어-1">핵심 아이디어</h3>
<p>초기화 리스트를 사용해 클래스 타입 객체를 만들 때는 다음 규칙이 적용됩니다.</p>
<ul>
<li>초기화 리스트가 비어 있으면, 리스트 생성자보다 기본 생성자가 우선됩니다.</li>
<li>초기화 리스트가 비어 있지 않으면, 맞는 리스트 생성자가 다른 맞는 생성자보다 우선됩니다.</li>
</ul>
<p>이 차이를 더 분명히 보기 위해, 복사 초기화, 직접 초기화, 리스트 초기화를 비슷한 예제로 비교해 보겠습니다.</p>
<pre><code class="language-cpp">// 복사 초기화
std::vector&lt;int&gt; v1 = 10;     // 10 은 초기화 리스트가 아님. 복사 초기화는 explicit 생성자와 매칭되지 않음: 컴파일 오류

// 직접 초기화
std::vector&lt;int&gt; v2(10);      // 10 은 초기화 리스트가 아님. explicit 단일 인수 생성자와 매칭됨

// 리스트 초기화
std::vector&lt;int&gt; v3{ 10 };    // { 10 } 은 초기화 리스트로 해석됨. 리스트 생성자와 매칭됨

// 복사 리스트 초기화
std::vector&lt;int&gt; v4 = { 10 }; // { 10 } 은 초기화 리스트로 해석됨. 리스트 생성자와 매칭됨
std::vector&lt;int&gt; v5({ 10 });  // { 10 } 은 초기화 리스트로 해석됨. 리스트 생성자와 매칭됨

// 기본 초기화
std::vector&lt;int&gt; v6 {};       // {} 는 빈 초기화 리스트. 기본 생성자와 매칭됨
std::vector&lt;int&gt; v7 = {};     // {} 는 빈 초기화 리스트. 기본 생성자와 매칭됨</code></pre>
<p><code>v1</code> 의 경우, 초기값 <code>10</code> 은 초기화 리스트가 아니므로 리스트 생성자와 맞지 않습니다.
단일 인수 생성자 <code>explicit std::vector&lt;T&gt;(std::size_t)</code> 도 복사 초기화에서는 
<code>explicit</code> 생성자와 매칭되지 않으므로 사용할 수 없습니다. 
결국 맞는 생성자가 없어서 컴파일 오류가 납니다.</p>
<p><code>v2</code> 의 경우, 초기값 <code>10</code> 은 초기화 리스트가 아니므로 리스트 생성자와는 맞지 않습니다. 
대신 단일 인수 생성자 <code>explicit std::vector&lt;T&gt;(std::size_t)</code> 와는 맞기 때문에 그 생성자가 선택됩니다.</p>
<p><code>v3</code> 에서는 <code>{ 10 }</code> 이 리스트 생성자와도 맞고 <code>explicit std::vector&lt;T&gt;(std::size_t)</code> 와도 맞을 수 있습니다. 하지만 리스트 생성자가 다른 맞는 생성자보다 우선하므로 리스트 생성자가 선택됩니다.</p>
<p><code>v4</code> 에서는 <code>{ 10 }</code> 이 리스트 생성자와 맞습니다. 리스트 생성자는 <code>non-explicit</code> 생성자이므로 복사 초기화에도 사용할 수 있습니다. 그래서 리스트 생성자가 선택됩니다.</p>
<p>놀랍게도 <code>v5</code> 는 직접 초기화처럼 보이지만, 사실은 <strong>복사 리스트 초기화의 다른 문법</strong> 입니다. 즉, <code>v4</code> 와 같은 의미입니다.</p>
<p>이것은 C++ 초기화 문법의 조금 불편한 점 중 하나입니다. 
<code>{ 10 }</code> 은 리스트 생성자가 있으면 그쪽과 매칭되고, 리스트 생성자가 없으면 단일 인수 생성자와 매칭됩니다. 즉, <strong>리스트 생성자가 존재하느냐에 따라 동작이 달라집니다.</strong> 
일반적으로 컨테이너에는 리스트 생성자가 있다고 생각하면 됩니다.</p>
<blockquote>
<p><strong>경고</strong>
어떤 클래스에 리스트 생성자가 없을 때 비어 있지 않은 초기화 리스트로 객체를 만들고 있었다면, 나중에 그 클래스에 리스트 생성자가 추가될 경우 어떤 생성자가 호출되는지가 바뀔 수 있습니다.</p>
</blockquote>
<p><code>v6</code> 와 <code>v7</code> 은 둘 다 빈 초기화 리스트로 초기화됩니다. 
이 경우에는 기본 생성자가 우선됩니다.</p>
<blockquote>
</blockquote>
<p>정리하면, 리스트 초기화는 보통 <strong>원소 값 목록으로 컨테이너를 초기화하기 위해</strong> 만들어진 문법이며, 실제로도 그 목적에 사용하는 것이 맞습니다. </p>
<blockquote>
</blockquote>
<p>대부분의 경우 우리가 원하는 것도 바로 그것입니다. 
따라서 <code>10</code> 이 <strong>원소 값</strong> 이라면 <code>{ 10 }</code> 이 맞습니다. 
하지만 <code>10</code> 이 컨테이너의 <strong>비리스트 생성자에 넘길 인수</strong> 라면 직접 초기화를 사용해야 합니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
컨테이너(또는 리스트 생성자가 있는 타입)를 만들 때, 
초기값이 원소 값이 아니라면 직접 초기화를 사용하세요.</p>
</blockquote>
<blockquote>
<p><strong>팁</strong>
<code>std::vector</code> 가 클래스 타입의 멤버일 때는, <code>std::vector</code> 의 길이를 어떤 초기값으로 설정하는 기본 초기화를 어떻게 써야 할지 바로 떠올리기 어렵습니다.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;vector&gt;
&gt;
struct Foo
{
    std::vector&lt;int&gt; v1(8); // 컴파일 오류: 멤버 기본 초기화에서는 직접 초기화를 사용할 수 없음
};</code></pre>
<blockquote>
</blockquote>
<p>이 코드는 동작하지 않습니다. 
멤버 기본 초기화에서는 직접 초기화(괄호 초기화)를 사용할 수 없기 때문입니다.</p>
<blockquote>
</blockquote>
<p>클래스 타입 멤버에 기본 초기값을 줄 때는 다음 규칙이 있습니다.</p>
<blockquote>
</blockquote>
<ul>
<li>복사 초기화 또는 리스트 초기화(직접 리스트 초기화 / 복사 리스트 초기화)만 사용할 수 있습니다.</li>
<li>CTAD는 사용할 수 없습니다(즉, 원소 타입을 직접 써야 합니다).<blockquote>
</blockquote>
해결 방법은 다음과 같습니다.<blockquote>
</blockquote>
<pre><code class="language-cpp">struct Foo
{
  std::vector&lt;int&gt; v{ std::vector&lt;int&gt;(8) }; // 가능
};</code></pre>
<blockquote>
</blockquote>
이 코드는 원소 8개짜리 <code>std::vector</code> 를 만든 다음, 
그것을 <code>v</code> 의 초기값으로 사용합니다.</li>
</ul>
<hr>
<h3 id="const-와-constexpr-stdvector"><code>const</code> 와 <code>constexpr std::vector</code></h3>
<p><code>std::vector</code> 타입 객체는 <code>const</code> 로 만들 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

int main()
{
    const std::vector&lt;int&gt; prime { 2, 3, 5, 7, 11 }; // prime 과 그 원소들은 수정할 수 없음

    return 0;
}</code></pre>
<p><code>const std::vector</code> 는 반드시 초기화되어야 하고, 한 번 만들어진 뒤에는 수정할 수 없습니다. 이런 벡터의 원소들도 <code>const</code> 인 것처럼 취급됩니다.</p>
<p>하지만 <code>std::vector</code> 의 원소 타입 자체를 <code>const</code> 로 정의하면 안 됩니다.
(예: <code>std::vector&lt;const int&gt;</code> 는 허용되지 않음)</p>
<blockquote>
<p><strong>핵심 아이디어</strong>
표준 라이브러리 컨테이너는 <strong>원소 자체를 <code>const</code> 로 가지도록 설계되지 않았습니다.</strong></p>
</blockquote>
<p>컨테이너의 상수성은 원소를 <code>const</code> 로 만드는 데서 오는 것이 아니라, 
<strong>컨테이너 자체를 <code>const</code> 로 만드는 것</strong> 으로 결정됩니다.</p>
<blockquote>
</blockquote>
<p><code>std::vector</code> 의 큰 단점 중 하나는 <code>constexpr</code> 로 만들 수 없다는 점입니다. <code>constexpr</code> 배열이 필요하다면 <code>std::array</code> 를 사용하세요.</p>
<hr>
<h3 id="왜-이름이-vector-일까">왜 이름이 <code>vector</code> 일까?</h3>
<p>사람들이 일상적으로 “vector” 라는 말을 쓰면, 보통 <strong>크기와 방향을 가진 기하학적 벡터</strong> 를 떠올립니다. 그런데 <code>std::vector</code> 는 그런 벡터가 아닌데 왜 이런 이름이 붙었을까요?</p>
<p>책 <em>From Mathematics to Generic Programming</em> 에서 Alexander Stepanov는 대략 이렇게 말했습니다. </p>
<p>“STL의 vector 라는 이름은 이전 프로그래밍 언어인 Scheme과 Common Lisp에서 가져왔다. 하지만 이것은 수학에서 훨씬 오래전부터 쓰이던 vector의 의미와 맞지 않았다. 이 자료구조는 사실 array 라고 불렸어야 했다. 안타깝게도 이런 실수를 한 번 하면, 그 결과는 아주 오랫동안 남을 수 있다.”</p>
<p>즉, 쉽게 말해 <code>std::vector</code> 라는 이름은 사실 조금 잘못 붙은 이름이지만, 
이제는 바꾸기엔 너무 늦었다는 뜻입니다.</p>
<hr>
<h2 id="163--stdvector와-부호-없는unsigned-길이-및-첨자subscript-문제">16.3 — std::vector와 부호 없는(unsigned) 길이 및 첨자(subscript) 문제</h2>
<p>이전 레슨인 &#39;16.2 -- std::vector 및 리스트 생성자 소개&#39;에서는 배열의 인덱스를 지정하여 요소에 접근할 수 있게 해주는 <code>operator[]</code>에 대해 알아보았습니다.</p>
<p>이번 레슨에서는 배열 요소에 접근하는 다른 방법들과 함께, 컨테이너 클래스의 길이
(현재 컨테이너 클래스에 포함된 요소의 개수)를 구하는 몇 가지 다양한 방법들을 살펴볼 것입니다.</p>
<p>하지만 그 전에, C++ 설계자들이 저지른 한 가지 큰 실수에 대해, 그리고 그 실수가 C++ 표준 라이브러리의 모든 컨테이너 클래스에 어떤 영향을 미치는지 먼저 이야기해 보아야 합니다.</p>
<hr>
<h3 id="컨테이너-길이의-부호-문제">컨테이너 길이의 부호 문제</h3>
<p>하나의 전제로 시작해 보겠습니다. 배열의 인덱스를 지정하는 데 사용되는 데이터 타입은 배열의 길이를 저장하는 데 사용되는 데이터 타입과 일치해야 합니다. 이는 가능한 가장 긴 배열의 모든 요소를 인덱싱할 수 있게 하고, 그 범위를 벗어나지 않도록 하기 위함입니다.</p>
<p>비야네 스트로우스트룹(Bjarne Stroustrup)이 회고하듯, C++ 표준 라이브러리의 컨테이너 클래스들이 설계될 당시(1997년경), 설계자들은 길이(와 배열 인덱스)를 부호 있는(signed) 타입으로 할지, 아니면 부호 없는(unsigned) 타입으로 할지 결정해야 했습니다. 그들은 부호 없는 타입을 선택했습니다.</p>
<p>그 이유는 다음과 같았습니다.</p>
<ul>
<li>표준 라이브러리 배열 타입의 인덱스는 음수가 될 수 없습니다.</li>
<li>부호 없는 타입을 사용하면 부호에 쓰일 여분의 비트를 활용해 더 큰 길이의 배열을 만들 수 있습니다 (이는 16비트 시절에는 매우 중요한 문제였습니다).</li>
<li>인덱스 범위 검사 시 조건 확인을 두 번 할 필요 없이 한 번만 하면 됩니다 (인덱스가 0보다 작은지 확인할 필요가 없기 때문입니다).</li>
</ul>
<p>돌이켜 보면, 이는 일반적으로 잘못된 선택이었다고 평가받습니다. 오늘날 우리는 다음과 같은 사실을 알고 있습니다:</p>
<ul>
<li>암시적 변환 규칙으로 인해 부호 없는 값을 사용하여 음수를 방지하려는 의도는 통하지 않습니다 (음수인 부호 있는 정수는 그저 매우 큰 부호 없는 정수로 암시적 변환되어 쓰레기 결과를 초래합니다).</li>
<li>32비트나 64비트 시스템에서는 범위를 넓히기 위한 여분의 비트가 대개 필요하지 않습니다 (20억 개가 넘는 요소를 가진 배열을 만들 일은 거의 없으니까요).</li>
<li>게다가 흔히 사용되는 <code>operator[]</code>는 어차피 범위 검사를 수행하지도 않습니다.</li>
</ul>
<p>이전 레슨인 &#39;4.5 -- 부호 없는 정수, 그리고 이를 피해야 하는 이유&#39;에서 우리는 수량을 저장할 때 부호 있는 값을 선호하는 이유에 대해 논의했습니다. 또한 부호 있는 값과 부호 없는 값을 섞어 쓰는 것은 예기치 않은 동작을 유발하는 지름길이라는 점도 언급했습니다. 따라서 표준 라이브러리 컨테이너 클래스들이 길이(그리고 인덱스)에 부호 없는 값을 사용한다는 것은 큰 문제입니다. 이러한 타입들을 사용할 때 부호 없는 값을 사용하는 것을 피할 수 없게 만들기 때문입니다.</p>
<p>당분간 우리는 과거의 이 결정과 그로 인해 발생하는 불필요한 복잡성을 안고 갈 수밖에 없습니다.</p>
<hr>
<h3 id="부호-변환은-축소-변환입니다-단-constexpr인-경우는-예외입니다">부호 변환은 축소 변환입니다. 단, <code>constexpr</code>인 경우는 예외입니다.</h3>
<p>더 진행하기 전에, 부호 변환(부호 있는 타입에서 부호 없는 타입으로, 또는 그 반대로의 정수 변환)과 관련하여 &#39;10.4 — 축소 변환, 리스트 초기화, 그리고 constexpr 초기화&#39; 레슨에서 다루었던 내용을 빠르게 복습해 보겠습니다. 이번 장에서 이 주제를 아주 많이 다루게 될 것이기 때문입니다.</p>
<p>부호 변환은 <strong>축소 변환</strong>으로 간주됩니다. 왜냐하면 부호 있는 타입이나 부호 없는 타입은 서로 반대되는 타입의 범위에 포함된 모든 값을 오롯이 담을 수 없기 때문입니다. 이러한 변환이 런타임에 수행될 때, 컴파일러는 축소 변환이 허용되지 않는 문맥(예: 리스트 초기화)에서는 오류를 발생시킵니다. 반면, 이러한 변환이 수행되는 다른 문맥에서는 경고를 띄울 수도 있고 그렇지 않을 수도 있습니다.</p>
<p>예를 들어 보겠습니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void foo(unsigned int){}

int main(){
    int s { 5 };

    [[maybe_unused]] unsigned int u { s }; // 컴파일 오류: 리스트 초기화는 축소 변환을 허용하지 않음
    foo(s);                                // 경고 발생 가능: 복사 초기화는 축소 변환을 허용함

    return 0;
}
</code></pre>
<p>위의 예제에서 변수 <code>u</code>의 초기화는 컴파일 오류를 발생시킵니다. 리스트 초기화를 수행할 때는 축소 변환이 엄격히 금지되기 때문입니다. 반면 <code>foo()</code> 호출은 복사 초기화를 수행하는데, 이는 축소 변환을 허용합니다. 이 경우 컴파일러가 부호 변환 경고를 얼마나 적극적으로 잡아내느냐에 따라 경고가 나올 수도 있고 나오지 않을 수도 있습니다. 예를 들어, GCC와 Clang 컴파일러 모두 <code>-Wsign-conversion</code> 플래그를 사용하면 이 상황에서 경고를 띄웁니다.</p>
<p>하지만, 부호를 변환하려는 값이 <strong><code>constexpr</code></strong>(상수 표현식)이고 반대 타입의 동일한 값으로 문제없이 변환될 수 있다면, 이 부호 변환은 축소 변환으로 간주되지 않습니다. 이는 컴파일러가 컴파일 타임에 이 변환이 안전하다는 것을 확실히 보장할 수 있고, 만약 안전하지 않다면 컴파일 과정을 알아서 중단시킬 수 있기 때문입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void foo(unsigned int){}

int main(){
    constexpr int s { 5 };                 // 이제 constexpr로 선언되었습니다.
    [[maybe_unused]] unsigned int u { s }; // 성공: s는 constexpr이며 안전하게 변환될 수 있으므로 축소 변환이 아닙니다.
    foo(s);                                // 성공: s는 constexpr이며 안전하게 변환될 수 있으므로 축소 변환이 아닙니다.

    return 0;
}
</code></pre>
<p>이 경우 <code>s</code>가 <code>constexpr</code>이고 변환할 값(<code>5</code>)이 부호 없는 값으로도 온전히 표현될 수 있으므로, 이 변환은 축소 변환으로 간주되지 않으며 아무런 문제 없이 암시적으로 수행될 수 있습니다.</p>
<p>이러한 <strong>축소 변환이 아닌 <code>constexpr</code> 변환</strong>(<code>constexpr int</code>에서 <code>constexpr std::size_t</code>로의 변환 등)은 우리가 앞으로 아주 유용하게, 자주 활용하게 될 핵심 개념입니다.</p>
<hr>
<h3 id="stdvector의-길이와-인덱스의-타입은-size_type입니다"><code>std::vector</code>의 길이와 인덱스의 타입은 <code>size_type</code>입니다</h3>
<p>레슨 &#39;10.7 -- Typedef와 타입 별칭(type aliases)&#39;에서, 우리는 타입의 이름이 달라질 수 있는 경우(예: 컴파일러 구현에 따라 정의되는 경우)에 typedef와 타입 별칭을 자주 사용한다고 언급했습니다. 예를 들어 <code>std::size_t</code>는 보통 <code>unsigned long</code>이나 <code>unsigned long long</code>과 같이 크기가 큰 부호 없는(unsigned) 정수 타입을 위한 typedef입니다.</p>
<p>각각의 C++ 표준 라이브러리 컨테이너 클래스들은 <code>size_type</code>(때로는 <code>T::size_type</code>으로 표기됨)이라는 이름의 중첩된(nested) typedef 멤버를 정의합니다. 이는 해당 컨테이너의 길이(그리고 지원되는 경우 인덱스)를 나타내는 데 사용되는 타입에 대한 별칭입니다.</p>
<p>여러분은 보통 공식 문서나 컴파일러의 경고/에러 메시지에서 <code>size_type</code>을 자주 보게 될 것입니다. 예를 들어, <code>std::vector</code>의 <code>size()</code> 멤버 함수에 대한 공식 문서를 보면 <code>size()</code>가 <code>size_type</code> 타입의 값을 반환한다고 명시되어 있습니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
중첩된 typedef에 대해서는 레슨 &#39;15.3 -- 중첩 타입 (멤버 타입)&#39;에서 다룹니다.</p>
</blockquote>
<p><code>size_type</code>은 거의 항상 <code>std::size_t</code>의 별칭이지만, (드문 경우지만) 다른 타입을 사용하도록 재정의(override)될 수도 있습니다.</p>
<blockquote>
<p>** 핵심 포인트**</p>
</blockquote>
<ul>
<li><code>size_type</code>은 표준 라이브러리 컨테이너 클래스 내부에 정의된 중첩 typedef로, 컨테이너 클래스의 길이 및 인덱스를 나타내는 타입으로 사용됩니다.</li>
<li><code>size_type</code>의 기본값은 <code>std::size_t</code>이며, 이 설정이 변경되는 일은 거의 없기 때문에 우리는 <code>size_type</code>을 사실상 <code>std::size_t</code>의 별칭이라고 가정해도 무방합니다.</li>
</ul>
<blockquote>
<p><strong>심화 학습</strong></p>
</blockquote>
<ul>
<li><code>std::array</code>를 제외한 모든 표준 라이브러리 컨테이너는 메모리 할당을 위해 <code>std::allocator</code>를 사용합니다. 이러한 컨테이너들의 경우, <code>T::size_type</code>은 사용된 할당자(allocator)의 <code>size_type</code>에서 파생됩니다. <code>std::allocator</code>는 최대 <code>std::size_t</code> 바이트의 메모리를 할당할 수 있으므로, <code>std::allocator&lt;T&gt;::size_type</code>은 <code>std::size_t</code>로 정의됩니다. 따라서 <code>T::size_type</code>의 기본값은 자연스럽게 <code>std::size_t</code>가 됩니다.</li>
<li>사용자 정의 할당자(custom allocator)를 만들고 그 안에서 <code>T::size_type</code>을 <code>std::size_t</code>가 아닌 다른 타입으로 정의한 경우에만 컨테이너의 <code>T::size_type</code>이 달라집니다. 이는 매우 드문 일이며 특정 애플리케이션의 특수한 목적을 위해 개별적으로 이루어지는 작업입니다. 따라서 여러분의 코드가 그러한 커스텀 할당자를 사용하지 않는 한(만약 사용한다면 여러분이 직접 그 사실을 알고 있을 것입니다), <code>T::size_type</code>이 <code>std::size_t</code>일 것이라고 가정하는 것이 일반적으로 안전합니다.</li>
<li>컨테이너 클래스의 <code>size_type</code> 멤버에 직접 접근할 때는 반드시 템플릿화된 컨테이너 클래스의 전체 이름으로 스코프(scope)를 명시해야 합니다. 
(예: <code>std::vector&lt;int&gt;::size_type</code>)</li>
</ul>
<hr>
<h3 id="size-멤버-함수-또는-stdsize를-사용하여-stdvector의-길이-구하기"><code>size()</code> 멤버 함수 또는 <code>std::size()</code>를 사용하여 <code>std::vector</code>의 길이 구하기</h3>
<p><code>size()</code> 멤버 함수를 사용하여 컨테이너 클래스 객체에 현재 길이를 요청할 수 있습니다.
(이 함수는 길이를 부호 없는(unsigned) 타입인 <code>size_type</code>으로 반환합니다)</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime { 2, 3, 5, 7, 11 };
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; prime.size() &lt;&lt; &#39;\n&#39;; // 길이를 `size_type` (std::size_t의 별칭) 타입으로 반환합니다.
    return 0;
}
</code></pre>
<p>이 코드의 출력 결과는 다음과 같습니다.</p>
<p><code>length: 5</code></p>
<p><code>length()</code>와 <code>size()</code> 멤버 함수를 모두 가지고 있는(두 함수는 동일한 작업을 수행합니다) <code>std::string</code> 및 <code>std::string_view</code>와 달리, <code>std::vector</code>를 비롯한 C++의 대부분의 다른 컨테이너 타입은 <code>size()</code> 함수만 가지고 있습니다. 이제 왜 컨테이너의 &#39;길이(length)&#39;를 종종 &#39;크기(size)&#39;라고 모호하게 부르는지 이해하셨을 것입니다.</p>
<p>C++17부터는 <strong><code>std::size()</code> 비멤버 함수</strong>도 사용할 수 있습니다 
(컨테이너 클래스의 경우, 이 함수는 내부적으로 단순히 <code>size()</code> 멤버 함수를 호출합니다).</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime { 2, 3, 5, 7, 11 };
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; std::size(prime); // C++17, 길이를 `size_type` (std::size_t의 별칭) 타입으로 반환합니다.

    return 0;
}
</code></pre>
<blockquote>
<p><strong>심화 학습</strong></p>
</blockquote>
<p><code>std::size()</code>는 붕괴되지 않은 C 스타일 배열에도 사용할 수 있기 때문에, 때로는 <code>size()</code> 멤버 함수를 직접 호출하는 것보다 이 방법이 더 선호되기도 합니다. </p>
<blockquote>
</blockquote>
<p>특히 컨테이너 클래스나 붕괴되지 않은 C 스타일 배열을 모두 인수로 받을 수 있는 함수 템플릿을 작성할 때 아주 유용합니다.</p>
<blockquote>
<p><strong>참고:</strong> C 스타일 배열 붕괴에 대해서는 레슨 &#39;17.8 -- C 스타일 배열 붕괴&#39;에서 자세히 다룹니다.</p>
</blockquote>
<p>위의 두 가지 방법 중 하나를 사용하여 배열의 길이를 구한 뒤, 이를 <strong>부호 있는 타입의 변수에 저장</strong>하려고 하면 부호 변환 경고나 오류가 발생할 가능성이 높습니다. </p>
<p>이 상황에서 가장 간단하게 해결할 수 있는 방법은 반환값을 원하는 타입으로 명시적 형변환(<code>static_cast</code>) 하는 것입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime { 2, 3, 5, 7, 11 };
    int length { static_cast&lt;int&gt;(prime.size()) }; // 반환값을 int 타입으로 static_cast 합니다.
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; length ;

    return 0;
}
</code></pre>
<hr>
<h3 id="c20-stdssize를-사용하여-stdvector의-길이-구하기">C++20: <code>std::ssize()</code>를 사용하여 <code>std::vector</code>의 길이 구하기</h3>
<p>C++20에서는 <code>std::ssize()</code> 비멤버(non-member) 함수를 새롭게 도입했습니다.
이 함수는 길이를 크기가 큰 <strong>부호 있는(signed)</strong> 정수 타입(일반적으로 <code>std::size_t</code>의 부호 있는 짝꿍으로 자주 사용되는 <code>std::ptrdiff_t</code>)으로 반환합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; std::ssize(prime); // C++20, 길이를 크기가 큰 부호 있는 정수 타입으로 반환합니다.

    return 0;
}
</code></pre>
<p>이것은 길이를 구하는 세 가지 함수(<code>size()</code>, <code>std::size()</code>, <code>std::ssize()</code>) 중 유일하게 길이를 부호 있는(signed) 타입으로 반환하는 함수입니다.</p>
<p>이 방법을 사용하여 길이를 부호 있는 타입의 변수에 저장하고 싶다면, 
두 가지 방법 중 하나를 선택할 수 있습니다.</p>
<p>첫째, 일반적인 <code>int</code> 타입은 <code>std::ssize()</code>가 반환하는 부호 있는 타입보다 크기가 작을 수 있습니다. 따라서 길이를 <code>int</code> 변수에 할당하려는 경우, 이 변환을 명확하게 하기 위해 결과값을 <code>int</code>로 명시적 형변환(<code>static_cast</code>) 해야 합니다. (그렇지 않으면 축소 변환(narrowing conversion) 경고나 컴파일 오류가 발생할 수 있습니다)</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };
    int length { static_cast&lt;int&gt;(std::ssize(prime)) }; // 반환값을 int로 명시적 형변환(static_cast) 합니다.
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; length;

    return 0;
}
</code></pre>
<p>둘째, 대안으로 <code>auto</code> 키워드를 사용하여 컴파일러가 해당 변수에 딱 맞는 올바른 부호 있는 타입을 스스로 추론하도록 맡길 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };
    auto length { std::ssize(prime) }; // std::ssize()가 반환하는 부호 있는 타입을 자동으로 추론하기 위해 auto를 사용합니다.
    std::cout &lt;&lt; &quot;length: &quot; &lt;&lt; length;

    return 0;
}
</code></pre>
<hr>
<h3 id="operator를-사용한-배열-요소-접근은-경계-검사를-수행하지-않습니다"><code>operator[]</code>를 사용한 배열 요소 접근은 경계 검사를 수행하지 않습니다</h3>
<p>이전 레슨에서 첨자 연산자 <code>operator[]</code> 에 대해 소개했습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };

    std::cout &lt;&lt; prime[3];  // 인덱스가 3인 요소의 값(7)을 출력합니다.
    std::cout &lt;&lt; prime[9];  // 유효하지 않은 인덱스입니다 (미정의 동작 발생).

    return 0;
}
</code></pre>
<p><code>operator[]</code>는 경계 검사를 수행하지 않습니다. 
<code>operator[]</code>에 들어가는 인덱스는 비상수일 수도 있습니다. 
이 부분에 대해서는 이후 섹션에서 더 자세히 다루도록 하겠습니다.</p>
<hr>
<h3 id="at-멤버-함수를-사용한-배열-요소-접근은-런타임-경계-검사를-수행합니다"><code>at()</code> 멤버 함수를 사용한 배열 요소 접근은 런타임 경계 검사를 수행합니다</h3>
<p>배열 컨테이너 클래스들은 배열 요소에 접근하는 또 다른 방법을 지원합니다. 
<code>at()</code> 멤버 함수를 사용하면 런타임 경계 검사를 거쳐 배열 요소에 접근할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };

    std::cout &lt;&lt; prime.at(3); // 인덱스가 3인 요소의 값을 출력합니다.
    std::cout &lt;&lt; prime.at(9); // 유효하지 않은 인덱스입니다 (예외 발생).

    return 0;
}
</code></pre>
<p>위의 예제에서 <code>prime.at(3)</code> 호출은 인덱스 3이 유효한지 먼저 확인합니다. 유효한 인덱스이므로 배열의 3번 요소에 대한 참조를 반환하고, 우리는 그 값을 출력할 수 있습니다.</p>
<p>하지만 <code>prime.at(9)</code> 호출은 9가 이 배열의 유효한 인덱스가 아니기 때문에 (런타임에) 실패합니다. <code>at()</code> 함수는 참조를 반환하는 대신, 프로그램을 종료시키는 오류를 발생시킵니다.</p>
<blockquote>
<p>** 심화 학습**
<code>at()</code> 멤버 함수가 범위를 벗어난 인덱스를 만나면, 실제로는 <code>std::out_of_range</code> 타입의 예외를 던집니다. 이 예외가 프로그램 내에서 적절히 처리되지 않으면 프로그램이 강제 종료됩니다. 예외와 그 처리 방법에 대해서는 27장에서 자세히 다루게 됩니다.</p>
</blockquote>
<p><code>operator[]</code>와 마찬가지로, <code>at()</code>에 전달되는 인덱스는 비상수일 수 있습니다.</p>
<p>매 호출마다 런타임에 경계 검사를 수행해야 하기 때문에, <code>at()</code>은 <code>operator[]</code>보다 느립니다 (물론 더 안전하긴 합니다). 더 안전함에도 불구하고 실제 현업에서는 <code>at()</code>보다 <code>operator[]</code>가 일반적으로 훨씬 더 많이 사용됩니다. 그 주된 이유는, 애초에 유효하지 않은 인덱스를 사용하려는 시도 자체를 하지 않도록 <strong>인덱싱을 하기 전에 논리적으로 미리 경계 검사를 확실히 하는 것</strong>이 더 좋은 프로그래밍 습관이기 때문입니다.</p>
<hr>
<h3 id="constexpr-부호-있는-정수로-stdvector-인덱싱하기"><code>constexpr</code> 부호 있는 정수로 <code>std::vector</code> 인덱싱하기</h3>
<p><code>constexpr int</code>를 사용하여 <code>std::vector</code>를 인덱싱할 때, 우리는 이것이 축소 변환으로 취급되지 않으면서도 컴파일러가 자연스럽게 <code>std::size_t</code>로 암시적 변환을 수행하도록 내버려 둘 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };

    std::cout &lt;&lt; prime[3] &lt;&lt; &#39;\n&#39;;     // 성공: 3이 int에서 std::size_t로 변환되며, 축소 변환이 아닙니다.

    constexpr int index { 3 };         // constexpr 선언
    std::cout &lt;&lt; prime[index] &lt;&lt; &#39;\n&#39;; // 성공: constexpr 인덱스가 std::size_t로 암시적 변환되며, 축소 변환이 아닙니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="비상수-값으로-stdvector-인덱싱하기">비상수 값으로 <code>std::vector</code> 인덱싱하기</h3>
<p>배열을 인덱싱하는 데 사용되는 첨자는 비상수일 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };

    std::size_t index { 3 };           // 비상수(non-constexpr)
    std::cout &lt;&lt; prime[index] &lt;&lt; &#39;\n&#39;; // operator[]는 std::size_t 타입의 인덱스를 예상하므로 변환이 필요하지 않습니다.

    return 0;
}
</code></pre>
<p>하지만 우리의 모범 사례(&#39;4.5 -- 부호 없는 정수, 그리고 이를 피해야 하는 이유&#39;)에 따라, 우리는 일반적으로 수량을 저장할 때 부호 없는 타입의 사용을 피하고자 합니다.</p>
<p>첨자가 비상수인 부호 있는 값일 때, 우리는 다음과 같은 문제에 직면하게 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };

    int index { 3 };                   // 비상수(non-constexpr)
    std::cout &lt;&lt; prime[index] &lt;&lt; &#39;\n&#39;; // 경고 발생 가능: 인덱스가 암시적으로 std::size_t로 변환됨, 축소 변환 발생

    return 0;
}
</code></pre>
<p>이 예제에서 <code>index</code>는 비상수인 부호 있는 <code>int</code>입니다. <code>std::vector</code>의 일부로 정의된 <code>operator[]</code>의 첨자는 <code>size_type</code>(<code>std::size_t</code>의 별칭) 타입을 갖습니다. 따라서 <code>prime[index]</code>를 호출할 때, 우리가 전달한 부호 있는 <code>int</code>는 <code>std::size_t</code>로 변환되어야 합니다.</p>
<p>이러한 변환은 위험하지 않아야 합니다 (<code>std::vector</code>의 인덱스는 음수가 아닐 것으로 예상되며, 음수가 아닌 부호 있는 값은 부호 없는 값으로 안전하게 변환되기 때문입니다). 하지만 이 변환이 런타임에 수행될 때 이는 <strong>축소 변환</strong>으로 간주되며, 컴파일러는 이것이 안전하지 않은 변환이라는 경고를 띄워야 합니다 (만약 경고를 발생시키지 않는다면, 경고가 발생하도록 컴파일러 설정을 수정하는 것을 고려해야 합니다).</p>
<p>배열 인덱싱은 매우 흔하게 사용되는 작업이고 이러한 변환이 일어날 때마다 경고가 생성되기 때문에, 컴파일 로그가 불필요한 경고들로 금세 도배될 수 있습니다. 또는 &quot;경고를 오류로 처리(treat warning as errors)&quot; 설정이 활성화되어 있다면 아예 컴파일이 중단될 것입니다.</p>
<p>이 문제를 피할 수 있는 방법은 여러 가지가 있지만 (예를 들어, 배열을 인덱싱할 때마다 <code>int</code>를 <code>std::size_t</code>로 <code>static_cast</code> 하는 등), 이 모든 방법은 필연적으로 코드를 지저분하게 하거나 복잡하게 만듭니다. 이 경우 가장 간단한 방법은 <strong><code>std::size_t</code> 타입의 변수를 인덱스로 사용하고, 이 변수를 인덱싱 이외의 다른 용도로는 사용하지 않는 것</strong>입니다. 그렇게 하면 애초에 비상수 변환이 일어나는 것을 피할 수 있습니다.</p>
<blockquote>
<p><strong>팁</strong>
또 다른 좋은 대안은 <code>std::vector</code> 자체를 인덱싱하는 대신, 
<code>data()</code> 멤버 함수의 결과를 인덱싱하는 것입니다:</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector prime{ 2, 3, 5, 7, 11 };

    int index { 3 };                          // 비상수 부호 있는 값
    std::cout &lt;&lt; prime.data()[index] &lt;&lt; &#39;\n&#39;; // 성공: 부호 변환 경고 없음

    return 0;
}
</code></pre>
<p>내부적으로 <code>std::vector</code>는 그 요소들을 C 스타일 배열에 보관합니다. <code>data()</code> 멤버 함수는 이 기반이 되는 C 스타일 배열에 대한 포인터를 반환하며, 우리는 이것을 인덱싱할 수 있습니다. C 스타일 배열은 부호 있는 타입과 부호 없는 타입 모두로 인덱싱하는 것을 허용하기 때문에, 부호 변환 문제에 부딪히지 않습니다. C 스타일 배열에 대해서는 레슨 &#39;17.7 -- C 스타일 배열 소개&#39;와 &#39;17.8 -- C 스타일 배열 붕괴&#39;에서 더 자세히 논의합니다.</p>
<blockquote>
<p><strong>저자의 노트 (Author’s note)</strong>
이와 같은 인덱싱 문제를 해결하기 위한 추가적인 옵션들은 레슨 &#39;16.7 -- 배열, 루프, 그리고 부호 문제 해결책&#39;에서 논의할 것입니다.</p>
</blockquote>
<hr>
<h2 id="164--stdvector-전달하기">16.4 — <code>std::vector</code> 전달하기</h2>
<p><code>std::vector</code> 타입의 객체도 다른 평범한 데이터들처럼 함수에 쏙 집어넣을 수 있어요. 
하지만 주의할 점이 있습니다! <code>std::vector</code>를 &#39;값으로 전달(pass by value)&#39;하게 되면, 벡터 안에 들어있는 모든 데이터를 통째로 복사하게 됩니다. 데이터가 많다면 컴퓨터가 아주 버거워하겠죠(비용이 큽니다).</p>
<p>그래서 이런 불필요하고 무거운 복사를 막기 위해, 우리는 보통 &#39;참조로 전달(pass by reference)&#39; 방식을 사용합니다. 원본의 위치만 알려주는 방식이죠! (보통은 데이터가 수정되지 않도록 <code>const</code> 참조를 씁니다.)</p>
<p><code>std::vector</code>를 사용할 때는, 그 안에 &#39;어떤 종류의 데이터(요소)&#39;가 들어가는지도 벡터의 정체성(타입 정보)에 포함됩니다. 따라서 함수에서 <code>std::vector</code>를 받을 때는 그 안에 어떤 타입이 들어있는지 명확하게 적어주어야 해요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void passByRef(const std::vector&lt;int&gt;&amp; arr) // 여기에 &lt;int&gt;를 명확하게 적어주어야 합니다
{
    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector primes{ 2, 3, 5, 7, 11 };
    passByRef(primes);

    return 0;
}
</code></pre>
<hr>
<h3 id="다른-타입의-요소를-가진-stdvector-전달하기">다른 타입의 요소를 가진 <code>std::vector</code> 전달하기</h3>
<p>방금 만든 <code>passByRef()</code> 함수는 정수가 들어있는 <code>std::vector&lt;int&gt;</code> 만 기다리고 있기 때문에, 실수(double) 같은 다른 종류의 데이터가 들어있는 벡터는 전달할 수 없어요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void passByRef(const std::vector&lt;int&gt;&amp; arr)
{
    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector primes{ 2, 3, 5, 7, 11 };
    passByRef(primes);  // 성공: 이것은 std::vector&lt;int&gt; 입니다

    std::vector dbl{ 1.1, 2.2, 3.3 };
    passByRef(dbl); // 컴파일 에러: std::vector&lt;double&gt;을 std::vector&lt;int&gt;로 변환할 수 없습니다

    return 0;
}
</code></pre>
<p>C++17 이상의 최신 버전에서는 <strong>CTAD</strong>를 써서 이 문제를 해결해보려고 할 수도 있을 거예요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void passByRef(const std::vector&amp; arr) // 컴파일 에러: CTAD는 함수 매개변수를 추론하는 데 사용할 수 없습니다
{
    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector primes{ 2, 3, 5, 7, 11 }; // 성공: CTAD를 사용하여 std::vector&lt;int&gt;로 추론합니다
    passByRef(primes);

    return 0;
}
</code></pre>
<p>하지만 안타깝게도, CTAD는 처음 벡터를 만들 때는 안에 든 값을 보고 타입을 척척 알아맞히지만, 아직 &#39;함수의 매개변수&#39; 자리에서는 작동하지 않는답니다.</p>
<p>예전에도 함수가 받는 데이터의 &#39;타입&#39;만 다르고 하는 일은 똑같을 때 비슷한 문제를 겪은 적이 있죠? 이럴 때 쓰기 딱 좋은 게 바로 <strong>함수 템플릿</strong> 이라는 기능이에요! 데이터 타입을 템플릿(틀)으로 만들어두면, C++이 알아서 우리가 넘겨주는 데이터에 맞춰 진짜 함수를 뚝딱 찍어내 줍니다.</p>
<blockquote>
<p><strong>관련 내용</strong> 
함수 템플릿에 대해서는 &#39;11.6 - 함수 템플릿&#39; 강의에서 다루고 있어요.</p>
</blockquote>
<p>이제 똑같은 템플릿 매개변수를 사용해서 함수 템플릿을 만들어 볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename T&gt;
void passByRef(const std::vector&lt;T&gt;&amp; arr)
{
    std::cout &lt;&lt; arr &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector primes{ 2, 3, 5, 7, 11 };
    passByRef(primes); // 성공: 컴파일러가 passByRef(const std::vector&lt;int&gt;&amp;) 함수를 만들어냅니다

    std::vector dbl{ 1.1, 2.2, 3.3 };
    passByRef(dbl);    // 성공: 컴파일러가 passByRef(const std::vector&lt;double&gt;&amp;) 함수를 만들어냅니다

    return 0;
}
</code></pre>
<p>위 예제에서는 <code>const std::vector&lt;T&gt;&amp;</code> 타입의 데이터를 받는 <code>passByRef()</code> 라는 단 하나의 함수 템플릿을 만들었어요. 여기서 <strong>T</strong> 는 바로 윗줄의 <code>template &lt;typename T&gt;</code> 에서 정의된 것인데요. 이 <strong>T</strong> 는 함수를 호출하는 사람이 어떤 타입이든 자유롭게 정할 수 있게 해주는 &#39;빈칸&#39; 역할을 합니다.</p>
<p>따라서 <code>main()</code> 함수에서 정수형 벡터인 <code>primes</code> 를 넣어 호출하면, 컴퓨터가 알아서 정수형 전용 함수인 <code>void passByRef(const std::vector&lt;int&gt;&amp; arr)</code> 를 만들어내고 실행해요.</p>
<p>마찬가지로 실수형 벡터인 <code>dbl</code> 을 넣어 호출하면, 알아서 실수형 전용 함수인 <code>void passByRef(const std::vector&lt;double&gt;&amp; arr)</code> 를 찍어내서 실행한답니다.</p>
<p>결과적으로, 단 하나의 템플릿만으로도 어떤 타입, 어떤 길이의 <code>std::vector</code> 든 다 받아낼 수 있는 만능 함수를 만든 셈이죠!</p>
<hr>
<h3 id="제네릭-템플릿이나-약식-함수-템플릿을-이용해-stdvector-전달하기">제네릭 템플릿이나 약식 함수 템플릿을 이용해 <code>std::vector</code> 전달하기</h3>
<p>아예 벡터뿐만 아니라 &#39;모든 종류의 객체&#39;를 다 받아주는 더 강력한 템플릿을 만들 수도 있어요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename T&gt;
void passByRef(const T&amp; arr) // [] 연산자가 오버로딩된 모든 타입의 객체를 받아줍니다
{
    std::cout &lt;&lt; arr[0] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector primes{ 2, 3, 5, 7, 11 };
    passByRef(primes); // 성공: 컴파일러가 passByRef(const std::vector&lt;int&gt;&amp;) 함수를 만들어냅니다

    std::vector dbl{ 1.1, 2.2, 3.3 };
    passByRef(dbl);    // 성공: 컴파일러가 passByRef(const std::vector&lt;double&gt;&amp;) 함수를 만들어냅니다

    return 0;
}
</code></pre>
<p>C++20 버전부터는 <code>auto</code> 매개변수를 이용한 <strong>약식 함수 템플릿</strong> 을 써서 똑같은 작업을 더 간단하게 할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void passByRef(const auto&amp; arr) // 약식 함수 템플릿
{
    std::cout &lt;&lt; arr &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector primes{ 2, 3, 5, 7, 11 };
    passByRef(primes); // 성공: 컴파일러가 passByRef(const std::vector&lt;int&gt;&amp;) 함수를 만들어냅니다

    std::vector dbl{ 1.1, 2.2, 3.3 };
    passByRef(dbl);    // 성공: 컴파일러가 passByRef(const std::vector&lt;double&gt;&amp;) 함수를 만들어냅니다

    return 0;
}
</code></pre>
<p>이 두 가지 방법은 코드가 에러 없이 돌아가기만 한다면 정말 &#39;아무 타입&#39;이나 다 받아줍니다. <code>std::vector</code> 말고도 다른 데이터 타입에서 써먹고 싶은 함수를 만들 때 아주 유용하죠. </p>
<p>예를 들어, 위 함수들은 <code>std::array</code> 나 문자열인 <code>std::string</code>, 심지어 우리가 생각지도 못한 전혀 다른 타입에서도 잘 작동할 거예요.</p>
<p>하지만 단점도 있습니다. 아무거나 다 받다 보니, 문법적으로는 통과가 되더라도 실제 의미상으로는 전혀 말이 안 되는 데이터가 들어가서 나중에 골치 아픈 버그를 일으킬 위험이 있거든요.</p>
<hr>
<h3 id="배열의-길이-확인하기">배열의 길이 확인하기</h3>
<p>방금 봤던 것과 비슷한 다음 템플릿 함수를 한번 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename T&gt;
void printElement3(const std::vector&lt;T&gt;&amp; arr)
{
    std::cout &lt;&lt; arr[3] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector arr{ 9, 7, 5, 3, 1 };
    printElement3(arr);

    return 0;
}
</code></pre>
<p>이 코드에서 <code>printElement3(arr)</code> 은 문제없이 잘 작동합니다. 
하지만 방심한 초보 프로그래머를 노리는 무서운 함정이 하나 숨어있어요. 눈치채셨나요?</p>
<p>위 프로그램은 인덱스 번호가 3번인 (즉, 4번째에 있는) 값을 출력합니다. 
배열 안에 4개 이상의 데이터가 있다면 아무 문제가 없죠. 
하지만, 데이터가 4개도 안 되는 짧은 배열을 집어넣어도 컴퓨터(컴파일러)는 아무런 불만 없이 코드를 통과시켜 버립니다. 예를 들어볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename T&gt;
void printElement3(const std::vector&lt;T&gt;&amp; arr)
{
    std::cout &lt;&lt; arr[3] &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector arr{ 9, 7 }; // 2개의 요소만 있는 배열 (사용 가능한 인덱스는 0과 1뿐입니다)
    printElement3(arr);

    return 0;
}
</code></pre>
<p>이렇게 되면 배열의 범위를 벗어난 곳에 접근하게 되어 프로그램이 엉뚱하게 작동하거나 뻗어버리는 <strong>정의되지 않은 동작</strong>이 발생하게 됩니다.</p>
<p>이럴 때 쓸 수 있는 한 가지 방법은 <code>arr.size()</code> 로 길이를 확인하는 검사기(assert)를 다는 거예요. 그러면 버그를 잡는 디버그 모드로 실행할 때 이런 에러를 미리 잡아낼 수 있죠. 단, <code>std::vector</code> 의 크기를 확인하는 기능은 프로그램이 &#39;실행 중일 때&#39;만 작동하기 때문에, 코드를 작성하는 시점에서는 미리 검사할 수 없다는 한계가 있습니다.</p>
<blockquote>
<p><strong>팁</strong> 
사실 가장 좋은 방법은, 배열의 길이를 깐깐하게 확인해야 하는 상황이라면 아예 <code>std::vector</code> 를 쓰지 않는 것입니다. 대신 프로그램 실행 전에도 크기를 알 수 있는 <code>std::array</code> 를 쓰는 것이 훨씬 낫습니다. 그러면 코드를 짤 때 길이를 미리 검사(<code>static_assert</code>)할 수 있거든요. 이 내용은 나중에 &#39;17.3 - std::array 전달 및 반환하기&#39;에서 배울 거예요.</p>
</blockquote>
<p>결론적으로 <strong>가장 좋은 해결책</strong> 은 애초에 &quot;무조건 몇 개 이상의 데이터가 들어있는 벡터만 넣어주세요!&quot; 라고 강요하는 함수 자체를 만들지 않는 것이랍니다.</p>
<hr>
<h2 id="165--stdvector-반환하기-그리고-이동-의미론move-semantics-소개">16.5 — std::vector 반환하기, 그리고 이동 의미론(Move semantics) 소개</h2>
<p>함수에 <code>std::vector</code> 를 넘겨줄 때, 우리는 보통 배열 데이터 전체를 복사하는 엄청난 비용을 피하려고 &#39;(const) 참조&#39; 방식을 사용합니다.</p>
<p>그렇기 때문에, <code>std::vector</code> 를 반환할 때는 그냥 &#39;값으로 반환(return by value)&#39;해도 괜찮다는 사실을 알게 되면 아마 깜짝 놀라실 거예요.</p>
<hr>
<h3 id="복사-의미론-copy-semantics">복사 의미론 (Copy semantics)</h3>
<p>다음 프로그램을 한 번 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector arr1 { 1, 2, 3, 4, 5 }; // { 1, 2, 3, 4, 5 }를 arr1에 복사합니다.
    std::vector arr2 { arr1 };          // arr1을 arr2에 복사합니다.

    arr1[0] = 6; // 우리는 arr1을 계속 사용할 수 있습니다.
    arr2[0] = 7; // 그리고 arr2도 계속 사용할 수 있습니다.

    std::cout &lt;&lt; arr1[0] &lt;&lt; arr2[0] &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><code>arr2</code> 가 <code>arr1</code> 을 이용해 초기화될 때, <code>std::vector</code> 의 복사 생성자가 호출되면서 <code>arr1</code> 의 내용이 <code>arr2</code> 로 고스란히 복사됩니다.</p>
<p>여기서는 <code>arr1</code> 과 <code>arr2</code> 가 각자 독립적으로 살아남아야 하므로, 복사본을 만드는 것이 유일하고 합리적인 방법이에요. 결과적으로 이 예제에서는 초기화할 때마다 하나씩, 총 두 번의 복사가 일어납니다.</p>
<p><strong>복사 의미론(Copy semantics)</strong> 이라는 거창한 말은 사실 별게 아닙니다. 
그냥 &quot;객체의 복사본을 어떻게 만들 것인가?&quot;를 정해둔 규칙일 뿐이에요. 
어떤 타입이 복사 의미론을 지원한다고 하면, 복사하는 규칙이 잘 정해져 있어서 그 타입의 객체를 안전하게 복사할 수 있다는 뜻입니다. 그리고 &quot;복사 의미론이 발동되었다&quot;는 말은 우리가 객체를 복사하게 만드는 어떤 행동을 했다는 뜻이죠.</p>
<p>클래스 타입의 경우, 보통 <strong>복사 생성자</strong> 나 복사 대입 연산자를 통해 이 규칙을 구현합니다. 대개는 클래스 안의 모든 데이터를 하나하나 다 복사하게 만들어 둬요. 방금 본 예제에서 <code>std::vector arr2 { arr1 };</code> 라는 코드가 바로 복사 규칙을 작동시킨 겁니다. </p>
<p>그러면 <code>std::vector</code> 의 복사 생성자가 불려가서 <code>arr1</code> 의 모든 데이터를 <code>arr2</code> 로 복사하죠. 결국 <code>arr1</code> 은 <code>arr2</code> 와 똑같은 내용을 가지면서도, 서로 완전히 독립적인 남남이 됩니다.</p>
<hr>
<h3 id="복사하는-게-최선이-아닐-때">복사하는 게 &#39;최선&#39;이 아닐 때</h3>
<p>자, 이제 조금 다른 예제를 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

std::vector&lt;int&gt; generate() // 값으로 반환(return by value)합니다.
{
    // 의무적인 복사 생략(mandatory copy elision)이 적용되지 않도록 일부러 이름이 있는 객체를 사용합니다.
    std::vector arr1 { 1, 2, 3, 4, 5 }; // { 1, 2, 3, 4, 5 }를 arr1에 복사합니다.
    return arr1;
}

int main()
{
    std::vector arr2 { generate() }; // generate()의 반환값은 이 표현식이 끝날 때 사라집니다(죽습니다).

    // 여기서부터는 generate()의 반환값을 사용할 방법이 전혀 없습니다.
    arr2[0] = 7; // 우리는 오직 arr2에만 접근할 수 있습니다.

    std::cout &lt;&lt; arr2[0] &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이번에 <code>arr2</code> 를 초기화할 때는, <code>generate()</code> 함수가 반환하는 <strong>임시 객체</strong>를 사용하고 있습니다. 이전 예제에서는 나중에도 계속 쓸 수 있는 이름 있는 변수(lvalue)를 썼지만, 이번에는 다릅니다. 이 임시 객체는 한 번 쓰이고 나면 이 줄이 끝날 때 바로 파괴되어 버리는 1회용(rvalue)이거든요. 어차피 이 임시 객체와 그 안의 데이터가 곧 사라질 운명이니, 우리는 어떻게든 그 데이터를 살려내서 <code>arr2</code> 안으로 가져와야만 합니다.</p>
<p>보통 이럴 때 우리가 하던 방식은 첫 번째 예제와 같습니다. 
그냥 복사 규칙을 써서 무겁고 비싼 복사를 한 번 더 하는 거죠. 그렇게 하면 임시 객체가 파괴되더라도 <code>arr2</code> 가 자기만의 데이터를 안전하게 가질 수 있으니까요.</p>
<p>하지만 이전과 결정적으로 다른 점이 있습니다. <strong>어차피 저 임시 객체는 죽을 운명이라는 거예요!</strong> 초기화가 끝나면 임시 객체는 더 이상 자기 데이터를 필요로 하지 않아요. 굳이 똑같은 데이터 두 세트가 동시에 존재할 필요가 없죠. 이런 상황에서 굳이 무겁게 복사를 한 다음에 원본 데이터를 버리는 것은, 너무 비효율적이고 안타까운 일입니다.</p>
<hr>
<h3 id="이동-의미론-move-semantics-맛보기">이동 의미론 (Move Semantics) 맛보기</h3>
<p>그렇다면 복사하는 대신에, <code>arr2</code> 가 임시 객체의 데이터를 살짝 <strong>&quot;훔쳐오는(steal)&quot;</strong> 방법이 있다면 어떨까요?</p>
<p>그러면 <code>arr2</code> 가 그 데이터의 새로운 주인이 되고, 번거롭게 데이터를 복사할 필요도 없어집니다. 이렇게 데이터의 소유권이 한 객체에서 다른 객체로 넘어가는 것을 가리켜 데이터가 <strong>이동(moved)되었다</strong> 고 표현합니다. 이렇게 이동하는 비용은 정말 깃털처럼 가볍습니다 (보통 메모리 주소 포인터 몇 개만 쓱 바꿔치기하면 되니까, 거대한 배열을 통째로 복사하는 것보다 엄청나게 빠르죠!).</p>
<p>덤으로 얻는 장점도 있어요! 코드가 끝나고 임시 객체가 파괴될 때, 이미 데이터를 다 뺏기고 텅텅 비어있기 때문에 파괴할 데이터조차 남아있지 않습니다. 데이터를 없애는 데 드는 수고마저 덜게 되는 거죠.</p>
<p>이것이 바로 <strong>이동 의미론(Move semantics)</strong> 의 핵심입니다! 한 객체에서 다른 객체로 데이터를 어떻게 넘겨줄지 정해주는 규칙이죠. 이 마법 같은 규칙이 발동되면, 이동할 수 있는 데이터는 모조리 이동시키고, 이동이 불가능한 데이터만 복사합니다. 비싸고 느린 복사를 싸고 빠른 이동으로 대체할 수 있기 때문에 훨씬 더 효율적입니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
<strong>이동 의미론</strong>은 특정 상황에서 데이터를 비싸게 복사하는 대신, 데이터의 소유권을 다른 객체로 아주 저렴하게 넘겨주는 훌륭한 최적화 기술입니다. 
(이동할 수 없는 데이터라면 어쩔 수 없이 복사하게 됩니다.)</p>
</blockquote>
<hr>
<h3 id="이동-의미론은-언제-발동할까요">이동 의미론은 언제 발동할까요?</h3>
<p>보통은 같은 타입의 객체로 초기화하거나 값을 넣을 때, 복사 규칙이 사용됩니다.
(복사 생략이 일어나지 않았다고 가정했을 때요)</p>
<blockquote>
<p><strong>참고</strong> 
복사 생략(copy elision)에 대한 내용은 &#39;14.15 강의 - 클래스 초기화와 복사 생략&#39;에서 다루었습니다.</p>
</blockquote>
<p>하지만, <strong>다음 세 가지 조건이 모두 만족될 때</strong> 는 복사 대신 &#39;이동&#39;이 발동됩니다!</p>
<ul>
<li>해당 객체의 타입이 이동 규칙을 지원할 때.</li>
<li>객체가 같은 타입의 <strong>1회용 임시 객체(rvalue)</strong> 로 초기화되거나 대입될 때.</li>
<li>이동 자체가 통째로 생략되지 않을 때.</li>
</ul>
<p>여기서 슬픈 소식이 하나 있습니다. 아직 이 기능을 지원하는 타입이 그리 많지는 않다는 거예요. 하지만 다행히도 우리가 자주 쓰는 <code>std::vector</code> 와 <code>std::string</code> 은 둘 다 완벽하게 지원한답니다!</p>
<p>이동 규칙이 정확히 어떻게 굴러가는지에 대한 깊은 내용은 22장에서 파헤쳐 볼 예정입니다. 지금은 그저 &quot;이동 의미론이 대체 뭔지&quot;, 그리고 &quot;어떤 녀석들이 이동을 할 수 있는지&quot; 정도만 알아두셔도 충분해요.</p>
<hr>
<h3 id="stdvector처럼-이동할-수-있는-타입은-값으로-반환해도-끄떡없어요">std::vector처럼 이동할 수 있는 타입은 &#39;값으로 반환&#39;해도 끄떡없어요</h3>
<p>함수에서 &#39;값으로 반환&#39;하게 되면 그 결과물은 1회용 임시 객체(rvalue)가 됩니다. 
따라서 반환되는 타입이 이동 의미론을 지원하기만 한다면, 목적지 객체에 데이터를 복사하는 대신 휙! 하고 <strong>이동</strong> 시킬 수 있어요. 덕분에 이런 타입들을 값으로 반환하는 건 비용이 거의 안 드는 아주 가벼운 작업이 됩니다!</p>
<blockquote>
<p><strong>핵심 포인트</strong>
우리는 <code>std::vector</code> 나 <code>std::string</code> 처럼 <strong>이동 가능한 타입</strong> 을 부담 없이 &#39;값으로 반환&#39;할 수 있습니다. 무겁게 복사하지 않고 가볍게 값을 휙 던져주니까요!
<em>(단, 이런 타입들을 함수에 인자로 넘겨줄 때는 여전히 &#39;const 참조&#39; 방식을 사용해야 합니다.)</em></p>
</blockquote>
<p>&quot;잠깐, 잠깐만요! 복사하기 무거운 애들은 &#39;값으로 넘기기&#39; 하면 안 된다면서요? 
근데 이동이 가능하면 &#39;값으로 반환&#39;하는 건 괜찮다고요?&quot;
네, 완벽하게 정답입니다!</p>
<hr>
<h3 id="깊이-알아보기-선택-사항">깊이 알아보기 (선택 사항)</h3>
<p>아래부터 이어지는 내용은 안 보셔도 무방하지만, 왜 이런 마법이 가능한지 이해하는 데 큰 도움이 될 거예요.</p>
<p>C++ 프로그래밍을 하면서 우리가 가장 많이 하는 행동 중 하나는, 함수에 어떤 값을 던져주고 다른 값을 받아오는 겁니다. 만약 주고받는 값이 클래스 타입이라면, 이 과정은 크게 <strong>4단계</strong> 로 나뉩니다.</p>
<ol>
<li>함수에 던져줄 값을 만듭니다.</li>
<li>실제로 그 값을 함수 안으로 넘겨줍니다.</li>
<li>함수 안에서 반환할 결과값을 만듭니다.</li>
<li>실제로 그 결과값을 함수를 부른 쪽으로 돌려줍니다.</li>
</ol>
<p><code>std::vector</code> 를 이용한 예제로 이 과정을 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

std::vector&lt;int&gt; doSomething(std::vector&lt;int&gt; v2)
{
    std::vector v3 { v2[0] + v2[0] }; // 3단계: 호출한 곳으로 돌려줄 결과값을 만듭니다.
    return v3; // 4단계: 실제로 값을 돌려줍니다(반환).
}

int main()
{
    std::vector v1 { 5 }; // 1단계: 함수에 던져줄 값을 만듭니다.
    std::cout &lt;&lt; doSomething(v1)[0] &lt;&lt; &#39;\n&#39;; // 2단계: 실제로 값을 함수 안으로 넘겨줍니다.

    std::cout &lt;&lt; v1[0] &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>먼저 <code>std::vector</code> 가 &#39;이동 능력&#39;이 전혀 없다고 가정해 봅시다. 
이 경우 위 프로그램은 무려 <strong>네 번의 복사</strong> 를 하게 됩니다.</p>
<ol>
<li>던져줄 값을 만들 때 초기화 내용을 <code>v1</code> 에 복사.</li>
<li>함수에 값을 넘겨줄 때 <code>v1</code> 을 매개변수 <code>v2</code> 에 복사.</li>
<li>결과값을 만들 때 초기화 내용을 <code>v3</code> 에 복사.</li>
<li>결과값을 돌려줄 때 <code>v3</code> 를 바깥으로 복사.</li>
</ol>
<p>자, 이제 이걸 어떻게 최적화할 수 있을지 이야기해 볼까요? 우리에겐 &#39;참조&#39;로 넘기기, &#39;복사 생략(elision)&#39;, &#39;이동 의미론&#39;, &#39;출력 매개변수&#39; 같은 다양한 무기들이 있습니다.</p>
<p><strong>1번과 3번 복사는 최적화할 방법이 아예 없습니다.</strong> 함수에 넘겨줄 벡터가 필요하고, 반환할 벡터도 필요하기 때문에 무조건 뼈대가 되는 객체를 생성해야만 하거든요.
우리가 건드릴 수 있는 부분은 바로 <strong>2번과 4번 복사</strong> 입니다.</p>
<blockquote>
<p><strong>2번 복사 최적화하기</strong></p>
</blockquote>
<p><strong>2번 복사</strong> 는 우리가 함수를 부를 때 &#39;값으로 넘기기&#39;를 했기 때문에 발생했습니다. 
대안이 있을까요?</p>
<blockquote>
</blockquote>
<ul>
<li><strong>참조나 주소로 넘길 수 있나요?</strong> 
네! 넘겨준 객체(<code>v1</code>)가 갑자기 사라지지 않는다는 게 보장되니까요.<blockquote>
</blockquote>
</li>
<li><strong>이 복사를 생략할 수 있나요?</strong> 
아니요. 쓸데없는 중복 복사가 아니기 때문에 불가능합니다.<blockquote>
</blockquote>
</li>
<li><strong>출력 매개변수를 쓸 수 있나요?</strong> 
아니요. 우린 지금 값을 넣는 중이지 받아오는 게 아니에요.<blockquote>
</blockquote>
</li>
<li><strong>이동 의미론을 쓸 수 있나요?</strong> 
아니요! 인자로 넘긴 <code>v1</code> 은 나중에도 써야 하는 멀쩡한 변수입니다. 만약 데이터를 <code>v2</code> 로 이동시켜 뺏어버리면, <code>v1</code> 은 텅 비어버리고 나중에 <code>v1[0]</code> 을 출력하려다 프로그램이 터질 수 있습니다.<blockquote>
</blockquote>
결론적으로 여기서는 <strong>&#39;const 참조&#39;로 넘기는 게 최고의 선택</strong> 입니다. 무거운 복사를 피할 수 있고 제일 안전하니까요!</li>
</ul>
<blockquote>
<p><strong>4번 복사 최적화하기</strong></p>
</blockquote>
<p><strong>4번 복사</strong> 는 함수 안에서 다 만든 값을 바깥으로 반환할 때 발생합니다.</p>
<blockquote>
</blockquote>
<ul>
<li><strong>참조나 주소로 반환할 수 있나요?</strong> 
절대 안 됩니다! <code>v3</code> 는 함수 안에서 만들어져서 함수가 끝나면 파괴될 녀석입니다. 파괴될 녀석의 주소를 돌려주면 큰일 납니다.<blockquote>
</blockquote>
</li>
<li><strong>이 복사를 생략할 수 있나요?</strong> 
네, 가능성이 높습니다! 컴파일러가 똑똑하다면 코드를 요령껏 재배치해서 애초에 복사를 안 만들 수 있습니다. 하지만 컴파일러 마음이라 100% 보장할 수는 없어요.<blockquote>
</blockquote>
</li>
<li><strong>출력 매개변수를 쓸 수 있나요?</strong> 
네. 하지만 코드가 복잡하고 못생겨지며 제약이 너무 많아서 좋은 방법은 아닙니다.<blockquote>
</blockquote>
</li>
<li><strong>이동 의미론을 쓸 수 있나요?</strong> 
네! 어차피 <code>v3</code> 는 함수가 끝나면 파괴될 운명입니다. 무겁게 밖으로 복사할 필요 없이, <strong>이동 의미론을 써서 v3의 데이터를 밖으로 휙 던져주고(이동) 빠지면</strong> 완벽합니다!</li>
</ul>
<p>가장 좋은 시나리오는 컴파일러가 알아서 복사를 생략해 주는 거지만, 이건 우리가 통제할 수 없습니다. 그 다음으로 훌륭한 최고의 대안이 바로 <strong>이동 의미론</strong>입니다! </p>
<p>컴파일러가 복사를 안 없애줄 때 구세주가 되어주거든요. 게다가 이동할 수 있는 타입들은 우리가 &#39;값으로 반환&#39;하기만 하면 알아서 자동으로 발동합니다!</p>
<blockquote>
<p><strong>정리하자면...</strong>
<code>std::vector</code> 처럼 이동이 가능한 타입들을 다룰 때는 
<strong>&#39;함수 인자로 넣을 땐 const 참조로, 밖으로 뺄 땐 값으로 반환&#39;</strong> 
하는 것이 최고의 비법입니다.</p>
</blockquote>
<hr>
<h2 id="166--배열과-반복문-arrays-and-loops">16.6 — 배열과 반복문 (Arrays and loops)</h2>
<p>이 챕터의 첫 번째 강의(16.1 - 컨테이너와 배열 소개)에서는 비슷한 역할을 하는 변수들이 엄청나게 많아질 때 생기는 골치 아픈 문제들을 이야기했었죠. 이번 강의에서는 그 문제를 다시 한번 짚어보고, <strong>배열</strong> 이 어떻게 이 문제들을 마법처럼 깔끔하게 해결해 주는지 알아볼 거예요!</p>
<hr>
<h3 id="변수가-너무-많아지는-문제-다시-살펴보기">변수가 너무 많아지는 문제, 다시 살펴보기</h3>
<p>학생 5명으로 이루어진 반의 시험 점수 평균을 구한다고 해볼까요? 
(설명을 간단하게 하기 위해 학생 수를 5명으로 줄였어요.)</p>
<p>각각의 변수를 따로따로 만들어서 코드를 짜면 이렇게 될 겁니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    // 5개의 정수 변수를 만듭니다 (각각 이름이 다 달라요!)
    int testScore1{ 84 };
    int testScore2{ 92 };
    int testScore3{ 76 };
    int testScore4{ 81 };
    int testScore5{ 56 };

    int average { (testScore1 + testScore2 + testScore3 + testScore4 + testScore5) / 5 };

    std::cout &lt;&lt; &quot;The class average is: &quot; &lt;&lt; average &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>변수도 너무 많고, 직접 타이핑해야 할 것도 참 많죠? </p>
<p>만약 학생이 30명, 아니 600명이라면 어떨지 상상해 보세요. 
게다가 새로운 시험 점수를 하나 추가하려면 변수를 새로 만들고, 값을 넣어주고, 평균 계산식에도 일일이 더해줘야 해요. 평균을 구할 때 나누는 숫자(위 코드에서는 5)를 바꾸는 것도 잊지 않으셨겠죠? 깜빡했다면 계산 결과가 틀려지는 오류가 발생할 거예요. 기존 코드를 손봐야 할 때마다 이렇게 실수할 위험이 커진답니다.</p>
<p>이쯤 되면 관련된 변수가 무더기로 있을 때는 <strong>배열</strong> 을 써야 한다는 걸 아실 거예요. 자, 개별 변수들을 <code>std::vector</code> 로 싹 바꿔볼게요!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector testScore { 84, 92, 76, 81, 56 };
    std::size_t length { testScore.size() };

    int average { (testScore[0] + testScore[1] + testScore[2] + testScore[3] + testScore[4])
        / static_cast&lt;int&gt;(length) };

    std::cout &lt;&lt; &quot;The class average is: &quot; &lt;&lt; average &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>오, 훨씬 낫네요! 만들어야 하는 변수 개수가 확 줄었고, 평균을 구할 때 나누는 값도 직접 숫자를 적는 대신 배열의 길이(<code>length</code>)에서 바로 가져오고 있어요.</p>
<p>하지만 평균을 구하는 계산식은 아직도 좀 찜찜합니다. 배열 안에 있는 요소(데이터)들을 <code>[0]</code>, <code>[1]</code> 처럼 일일이 손으로 다 적어줘야 하니까요. 이렇게 직접 적어주면, 우리가 적은 개수와 똑같은 길이의 배열에서만 이 계산식이 작동해요. 만약 배열 길이가 달라지면, 그때마다 그 길이에 맞는 새로운 계산식을 또 만들어야 하죠.</p>
<p>우리에게 진짜 필요한 건, 요소들을 하나하나 적는 노가다 없이 
<strong>배열의 모든 요소에 쉽게 접근할 수 있는 방법</strong> 이에요.</p>
<hr>
<h3 id="배열과-반복문의-만남">배열과 반복문의 만남</h3>
<p>이전 강의에서 배열의 인덱스(순서를 나타내는 번호)는 꼭 고정된 숫자일 필요가 없다고 배웠어요. 즉, 프로그램이 실행 중일 때 값이 변하는 &#39;변수&#39;를 인덱스로 쓸 수 있다는 뜻이에요!</p>
<p>위의 평균 계산식에서 쓴 인덱스들을 보면 <code>0, 1, 2, 3, 4</code> 처럼 차례대로 1씩 커지고 있죠. 그렇다면 어떤 변수의 값을 0부터 4까지 차례대로 변하게 만들 수 있다면, 일일이 숫자를 쓰는 대신 그 변수를 인덱스 자리에 쏙 넣으면 되지 않을까요? 우리는 이미 그 방법을 알고 있어요. 바로 <strong>for 반복문</strong> 을 쓰는 거예요!</p>
<p>이제 for 반복문을 써서 위의 예제를 다시 써볼게요. 
반복문에서 1씩 커지는 변수를 배열의 인덱스로 쓸 겁니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector testScore { 84, 92, 76, 81, 56 };
    std::size_t length { testScore.size() };

    int average { 0 };
    for (std::size_t index{ 0 }; index &lt; length; ++index) // index를 0부터 &#39;길이-1&#39;까지 반복시킵니다
        average += testScore[index];                      // 해당 `index` 번호에 있는 값을 average에 더합니다
    average /= static_cast&lt;int&gt;(length);                  // 다 더한 값을 길이로 나누어 평균을 계산합니다

    std::cout &lt;&lt; &quot;The class average is: &quot; &lt;&lt; average &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>작동 원리는 아주 단순해요. <code>index</code> 변수가 0으로 시작해서 <code>testScore[0]</code> 의 값이 <code>average</code> 에 더해집니다. 그러고 나면 <code>index</code> 가 1로 늘어나죠. 그다음 <code>testScore[1]</code> 이 더해지고, 다시 <code>index</code> 가 2로 늘어납니다.  이렇게 계속되다가 마침내 <code>index</code> 가 5가 되면, <code>index &lt; length</code> (5 &lt; 5)라는 조건이 거짓(false)이 되면서 반복문이 알아서 종료됩니다.</p>
<p>이 시점이 되면 반복문 덕분에 <code>testScore[0]</code> 부터 <code>testScore[4]</code> 까지의 값이 모두 <code>average</code> 에 알차게 더해져 있을 거예요.
마지막으로, 이렇게 싹 다 더한 값을 배열의 길이로 나눠서 진짜 평균을 구하는 거죠.</p>
<p>이 방법은 코드를 관리하기에 정말 완벽해요! 반복문이 도는 횟수는 배열의 길이에 따라 자동으로 정해지고, 반복문 변수가 배열의 인덱스 역할을 알아서 척척 해줍니다. 이제 배열 요소를 일일이 손으로 적을 필요가 없어요.
만약 시험 점수를 추가하거나 빼고 싶다면? 처음에 배열을 만들 때 숫자만 추가/삭제하면 끝이에요. 나머지 코드는 단 한 줄도 고칠 필요 없이 완벽하게 작동한답니다!</p>
<p>이렇게 컨테이너(배열 같은 자료 구조)의 각 요소를 하나씩 순서대로 확인하는 것을 프로그래밍 용어로 <strong>순회(traversal)</strong> 라고 해요. 다른 말로는 <strong>반복(iteration)</strong> 한다거나 <strong>이터레이팅(iterating)</strong> 한다고도 부릅니다. 꼭 기억해 두세요!</p>
<blockquote>
<p><strong>작가의 노트</strong>
컨테이너 클래스들은 길이나 인덱스를 다룰 때 보통 <code>size_t</code> 라는 타입을 사용해요. 그래서 이번 강의에서도 똑같이 <code>size_t</code> 를 썼습니다. 음수까지 표현할 수 있는(signed) 타입들을 사용하는 방법은 다가오는 16.7 강의에서 다룰게요.</p>
</blockquote>
<hr>
<h3 id="템플릿-배열-그리고-반복문-무한한-확장성-열기">템플릿, 배열, 그리고 반복문: 무한한 확장성 열기!</h3>
<ul>
<li><strong>배열</strong>은 데이터마다 일일이 이름을 붙이지 않고도 여러 개를 한 번에 저장할 수 있게 해줘요.</li>
<li><strong>반복문</strong>은 요소들을 손으로 다 적지 않고도 배열을 쭉 훑어볼 수(순회) 있게 해줘요.</li>
<li><strong>템플릿</strong>은 저장할 데이터의 &#39;타입(종류)&#39;을 내 마음대로 바꿀 수 있게 해줘요.</li>
</ul>
<p>이 세 가지가 합쳐지면 정말 놀라운 일이 벌어집니다! 안에 들어있는 데이터가 정수든 소수든, 개수가 5개든 100개든 상관없이 어떤 컨테이너라도 완벽하게 다룰 수 있는 코드를 짤 수 있거든요.</p>
<p>이해를 돕기 위해, 방금 만든 평균 계산 코드를 &#39;함수 템플릿&#39;이라는 형태로 업그레이드해 볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

// std::vector 안에 있는 값들의 평균을 계산해 주는 함수 템플릿입니다
template &lt;typename T&gt;
T calculateAverage(const std::vector&lt;T&gt;&amp; arr){
    std::size_t length { arr.size() };

    T average { 0 };                                      // 배열 안에 든 요소가 T 타입이면, 평균값을 저장할 변수도 T 타입이어야겠죠?
    for (std::size_t index{ 0 }; index &lt; length; ++index) // 모든 요소를 처음부터 끝까지 순회합니다
        average += arr[index];                            // 모든 요소를 싹 다 더합니다
    average /= static_cast&lt;int&gt;(length);                  // 전체 개수로 나눕니다 (개수는 사람 수나 사물 개수처럼 자연스러운 정수니까요)

    return average;
}

int main(){
    std::vector class1 { 84, 92, 76, 81, 56 };
    std::cout &lt;&lt; &quot;The class 1 average is: &quot; &lt;&lt; calculateAverage(class1) &lt;&lt; &#39;\n&#39;; // 정수형(int) 5개의 평균을 계산합니다!

    std::vector class2 { 93.2, 88.6, 64.2, 81.0 };
    std::cout &lt;&lt; &quot;The class 2 average is: &quot; &lt;&lt; calculateAverage(class2) &lt;&lt; &#39;\n&#39;; // 소수형(double) 4개의 평균을 계산합니다!

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 이렇게 출력됩니다.</p>
<pre><code class="language-text">The class 1 average is: 77
The class 2 average is: 81.75
</code></pre>
<p>위의 예제에서 우리는 <code>calculateAverage()</code> 라는 똑똑한 함수 템플릿을 만들었어요. 이 함수는 안에 든 데이터 종류나 길이에 상관없이 어떤 <code>std::vector</code> 를 던져줘도 평균을 구해냅니다. <code>main()</code> 함수를 보면 정수(<code>int</code>) 5개가 든 배열이든, 소수점(<code>double</code>) 4개가 든 배열이든 똑같이 잘 작동하는 걸 확인할 수 있어요!</p>
<p><code>calculateAverage()</code> 함수는 안에서 사용된 더하기(<code>+=</code>)나 나누기(<code>/=</code>) 같은 연산이 가능한 타입(<code>T</code>)이라면 뭐든지 다 처리할 수 있습니다. 만약 더하기나 나누기를 할 수 없는 이상한 타입을 넣으면? 컴파일러가 알아채고 당장 오류를 뿜어낼 거예요.</p>
<p>참, 코드를 보면서 왜 길이를 나타내는 <code>length</code> 를 굳이 <code>int</code> 로 바꿨는지(<code>static_cast&lt;int&gt;</code>) 궁금하실 수도 있어요. 평균을 구할 때는 다 더한 총합을 항목의 &#39;개수&#39;로 나누죠. 개수라는 건 소수점이 없는 정수(integral value)잖아요? 그래서 코드를 읽을 때 &quot;아, 의미상 정수로 나누는 게 맞지&quot; 하고 바로 이해할 수 있도록 <code>int</code> 로 명확하게 바꿔준 거랍니다.</p>
<hr>
<h3 id="배열과-반복문으로-할-수-있는-일들">배열과 반복문으로 할 수 있는 일들</h3>
<p>이제 반복문으로 컨테이너를 훑어보는(순회하는) 방법을 완벽히 알았으니, 실전에서 주로 이걸로 뭘 하는지 살펴볼게요. 우리는 보통 다음 4가지 중 하나를 하려고 반복문을 씁니다.</p>
<ol>
<li><strong>새로운 값 계산하기:</strong> 기존 데이터들을 써서 새로운 결과 만들기 (예: 평균 구하기, 싹 다 더하기)</li>
<li><strong>원하는 데이터 찾기:</strong> 특정 요소 찾기 (예: 나랑 똑같은 값 있나?, 100점 맞은 사람 몇 명인지 세기, 제일 큰 값 찾기)</li>
<li><strong>각 요소마다 특정 작업하기:</strong> (예: 화면에 하나씩 예쁘게 출력하기, 모든 점수에 보너스 2점씩 더하기)</li>
<li><strong>순서 바꾸기:</strong> 요소들 다시 줄 세우기 (예: 작은 숫자부터 큰 숫자 순서로 정렬하기)</li>
</ol>
<p>첫 번째부터 세 번째까지는 꽤 쉬워요. 반복문을 한 바퀴만 돌리면서 데이터를 슬쩍 보거나 입맛대로 바꾸면 되니까요.</p>
<p>하지만 요소들의 순서를 바꾸는(정렬) 작업은 꽤 까다롭습니다. 보통 반복문 안에 반복문을 또 넣어야 하거든요. 우리가 직접 머리를 싸매고 코드를 짤 수도 있지만, 사실 C++ 표준 라이브러리에 아주 훌륭한 알고리즘들이 이미 다 만들어져 있어요! 그냥 그걸 가져다 쓰는 게 훨씬 똑똑한 방법이죠. 이 부분은 나중에 알고리즘 챕터에서 더 자세히 알려드릴게요.</p>
<hr>
<h3 id="배열을-쓸-때-흔히-하는-실수-1-차이-오류-off-by-one-errors">배열을 쓸 때 흔히 하는 실수: 1 차이 오류 (Off-by-one errors)</h3>
<p>인덱스를 써서 배열을 훑어볼 때는 <strong>반복문이 정확한 횟수만큼 도는지</strong> 눈에 불을 켜고 확인해야 해요. 초보자뿐만 아니라 전문가도 가장 많이 하는 실수가 바로 &#39;1 차이 오류&#39;랍니다. 반복문이 실수로 한 번 더 돌거나 한 번 덜 도는 거죠.</p>
<p>보통은 <code>index</code> 를 <code>0</code> 부터 시작해서 <code>index &lt; length</code> 일 때까지만 반복하도록 코드를 짭니다.</p>
<p>하지만 프로그래밍을 처음 하시는 분들은 실수로 조건에 <strong>작거나 같다(<code>&lt;=</code>)</strong> 를 써서 <code>index &lt;= length</code> 처럼 만드는 경우가 종종 있어요. 이렇게 되면 <code>index</code> 와 <code>length</code> 가 같아질 때도 반복문이 돌아가 버려요. 결국 배열의 크기를 넘어서 엉뚱한 메모리를 건드리게 되고, 프로그램이 알 수 없는 이상한 행동을 하게 만드는 무서운 결과를 낳습니다. 조심, 또 조심하세요!</p>
<hr>
<h3 id="167--배열-루프-그리고-부호sign-문제-해결-방법">16.7 — 배열, 루프, 그리고 부호(Sign) 문제 해결 방법</h3>
<p>이전 레슨인 &#39;4.5 - 부호 없는 정수와 이를 피해야 하는 이유&#39;에서, 우리는 수량을 저장할 때 일반적으로 <strong>부호 있는 값</strong>을 선호한다고 배웠습니다. 부호 없는 값은 가끔 우리의 예상과 전혀 다르게 동작할 수 있기 때문이죠. 하지만 &#39;16.3 - <strong>std::vector</strong> 와 부호 없는 길이 및 인덱스 문제&#39; 레슨에서 다루었듯이, <strong>std::vector</strong> 와 같은 컨테이너 클래스들은 길이와 인덱스를 표현할 때 부호 없는 정수 타입인 <strong>std::size_t</strong> 를 사용합니다.</p>
<p>이로 인해 다음과 같은 골치 아픈 문제가 발생할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename T&gt;
void printReverse(const std::vector&lt;T&gt;&amp; arr)
{
    for (std::size_t index{ arr.size() - 1 }; index &gt;= 0; --index) // index는 부호 없는(unsigned) 정수입니다
    {
        std::cout &lt;&lt; arr[index] &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector arr{ 4, 6, 7, 3, 8, 2, 1, 9 };

    printReverse(arr);

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 처음에는 배열을 거꾸로 잘 출력하는 것처럼 보입니다.</p>
<p><code>9 1 2 8 3 7 6 4</code></p>
<p>하지만 그 직후, <strong>정의되지 않은 동작</strong>을 일으킵니다. 
쓰레기 값을 출력하거나 프로그램이 아예 튕겨버릴 수 있죠.</p>
<p>여기에는 두 가지 큰 문제가 숨어 있습니다.</p>
<p>첫째, 우리의 루프는 <code>index &gt;= 0</code> (즉, 인덱스가 양수인 동안) 계속 실행되도록 작성되었습니다. 그런데 <code>index</code>가 부호 없는(unsigned) 타입이라면, 이 조건은 <strong>항상 참(true)</strong> 이 됩니다. 마이너스 값이 아예 존재할 수 없으니까요! 결과적으로 이 루프는 영원히 끝나지 않습니다.</p>
<p>둘째, <code>index</code>가 0일 때 1을 빼면(감소시키면), 마이너스 값이 되는 대신 <strong>엄청나게 큰 양수</strong> 로 한 바퀴 돌아가 버립니다(이를 랩어라운드, Wrap-around라고 합니다).</p>
<p>그리고 다음 루프에서 이 거대한 숫자를 배열의 인덱스로 사용하게 되죠. 이는 배열의 진짜 크기를 완전히 벗어난 접근(Out-of-bounds)이며, 프로그램을 망가뜨리는 원인이 됩니다. 만약 배열(벡터)이 텅 비어있을 때도 똑같은 문제가 발생합니다.</p>
<p>이런 문제를 피하는 여러 가지 꼼수가 있긴 하지만, 애초에 이런 방식 자체가 버그를 끌어당기는 자석과도 같습니다.</p>
<p>루프 변수로 <strong>부호 있는(signed)</strong> 타입을 사용하면 이런 문제를 훨씬 쉽게 피할 수 있지만, 대신 또 다른 귀찮은 점이 생깁니다. 다음은 부호 있는 인덱스를 사용해서 위 문제를 고쳐본 코드입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename T&gt;
void printReverse(const std::vector&lt;T&gt;&amp; arr)
{
    for (int index{ static_cast&lt;int&gt;(arr.size()) - 1}; index &gt;= 0; --index) // index는 부호 있는(signed) 정수입니다
    {
        std::cout &lt;&lt; arr[static_cast&lt;std::size_t&gt;(index)] &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::vector arr{ 4, 6, 7, 3, 8, 2, 1, 9 };

    printReverse(arr);

    return 0;
}
</code></pre>
<p>이 코드는 우리가 원하는 대로 완벽하게 작동합니다. 하지만 코드가 굉장히 지저분해졌습니다. <code>static_cast</code> (타입 변환)가 두 번이나 들어갔기 때문이죠. 
특히 <code>arr[static_cast&lt;std::size_t&gt;(index)]</code> 부분은 눈으로 읽기 너무 불편합니다. 
안전성을 얻은 대신, 코드를 읽기 쉽게 만드는 <strong>가독성</strong>을 크게 희생한 셈입니다.</p>
<p>부호 있는 인덱스를 사용한 또 다른 예시를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

// std::vector의 평균값을 계산하는 함수 템플릿
template &lt;typename T&gt;
T calculateAverage(const std::vector&lt;T&gt;&amp; arr)
{
    int length{ static_cast&lt;int&gt;(arr.size()) };

    T average{ 0 };
    for (int index{ 0 }; index &lt; length; ++index)
        average += arr[static_cast&lt;std::size_t&gt;(index)];
    average /= length;

    return average;
}

int main()
{
    std::vector testScore1 { 84, 92, 76, 81, 56 };
    std::cout &lt;&lt; &quot;The class 1 average is: &quot; &lt;&lt; calculateAverage(testScore1) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>여기서도 <code>static_cast</code> 때문에 코드가 지저분해지는 건 여전합니다. 정말 끔찍하죠.</p>
<p>그럼 도대체 어떻게 해야 할까요? 안타깝게도 이 문제에 대해 <strong>완벽한 정답은 없습니다.</strong>
대신 우리가 쓸 수 있는 여러 가지 선택지들이 있습니다. 지금부터 저희가 생각하기에 <strong>가장 최악인 방법부터 가장 추천하는 방법 순서</strong> 로 소개해 드리겠습니다. 실무에서 다른 사람들이 작성한 코드를 보면 이 방법들을 모두 마주치게 될 것입니다.</p>
<blockquote>
<p><strong>저자의 참고 사항</strong>
여기서는 <strong>std::vector</strong> 를 기준으로 설명하지만, <strong>std::array</strong> 와 같은 모든 표준 라이브러리 컨테이너들도 똑같이 작동하며 같은 문제를 가지고 있습니다. 따라서 아래의 설명은 어떤 컨테이너를 쓰든 똑같이 적용됩니다.</p>
</blockquote>
<hr>
<h3 id="부호-변환-경고-끄기-비추천">부호 변환 경고 끄기 (비추천)</h3>
<p>혹시 컴파일러에서 부호 변환 경고가 왜 기본적으로 꺼져 있는 경우가 많은지 궁금하셨다면, 바로 이 문제 때문입니다. 표준 라이브러리 컨테이너에 부호 있는 인덱스를 사용할 때마다 경고가 발생하거든요. 이 경고들이 컴파일 로그를 꽉 채워버리면, 정작 우리가 고쳐야 할 진짜 중요한 경고들을 놓치게 됩니다.</p>
<p>그래서 아예 이 경고들을 무시하도록 설정하는 것이 하나의 방법입니다.
가장 간단한 해결책이지만, <strong>절대 추천하지 않습니다.</strong> 자칫하면 진짜 버그를 일으킬 수 있는 위험한 부호 변환 문제까지 모두 숨겨버리기 때문입니다.</p>
<hr>
<h3 id="부호-없는-루프-변수-사용하기">부호 없는 루프 변수 사용하기</h3>
<p>많은 개발자들이 &quot;표준 라이브러리가 원래 부호 없는 인덱스를 쓰도록 만들어졌으니, 우리도 부호 없는 인덱스를 쓰면 되지!&quot; 라고 생각합니다. 아주 타당한 의견입니다. 단, 부호가 있는 값과 없는 값이 섞여서 발생하는 문제(mismatch)가 생기지 않도록 각별히 조심해야 합니다. 가능하면 인덱스 루프 변수는 순수하게 &#39;위치를 찾는 용도&#39;로만 사용하는 것이 좋습니다.</p>
<p>그렇다면 어떤 부호 없는 타입을 사용해야 할까요?</p>
<p>이전 레슨에서 표준 라이브러리 컨테이너들은 <code>size_type</code> 이라는 내부 타입을 가지고 있다고 배웠습니다. <code>.size()</code> 함수도 이 타입을 반환하고, <code>[]</code> 기호도 이 타입을 인덱스로 사용하죠. 따라서 기술적으로는 <code>size_type</code> 을 사용하는 것이 가장 안전하고 일관된 방법입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector arr { 1, 2, 3, 4, 5 };

    for (std::vector&lt;int&gt;::size_type index { 0 }; index &lt; arr.size(); ++index)
        std::cout &lt;&lt; arr[index] &lt;&lt; &#39; &#39;;

    return 0;
}
</code></pre>
<p>하지만 이 방법엔 아주 큰 단점이 있습니다. 이름이 너무 길다는 것이죠! <code>std::size_type</code> 이라고 간단히 쓸 수 없고, <code>std::vector&lt;int&gt;::size_type</code> 처럼 컨테이너의 전체 이름을 다 적어줘야 합니다. 타이핑하기도 힘들고 읽기도 불편합니다.</p>
<p>심지어 템플릿 함수 안에서 사용할 때는 앞에 <code>typename</code> 이라는 키워드까지 붙여줘야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

template &lt;typename T&gt;
void printArray(const std::vector&lt;T&gt;&amp; arr)
{
    // 의존적 이름(dependent type)에는 typename 키워드 접두사가 필요합니다
    for (typename std::vector&lt;T&gt;::size_type index { 0 }; index &lt; arr.size(); ++index)
        std::cout &lt;&lt; arr[index] &lt;&lt; &#39; &#39;;
}

int main()
{
    std::vector arr { 9, 7, 5, 3, 1 };

    printArray(arr);

    return 0;
}
</code></pre>
<p>이 코드를 더 짧게 줄이기 위해 <code>decltype</code> 이나 별칭(<code>using</code>)을 쓸 수도 있지만, 여전히 기억하기 어렵고 복잡합니다.</p>
<p>그래서 많은 프로그래머들은 길고 복잡한 <code>size_type</code> 대신, 기억하기도 쉽고 치기도 편한 <strong>std::size_t</strong> 를 직접 사용합니다. (사실 <code>size_type</code> 도 결국 대부분 <code>size_t</code> 와 똑같기 때문입니다.)</p>
<pre><code class="language-cpp">for (std::size_t index { 0 }; index &lt; arr.size(); ++index)
</code></pre>
<p>여러분이 직접 메모리 할당기를 만드는 고수급 개발자가 아니라면, 이 정도면 꽤 합리적인 방법입니다.</p>
<hr>
<h3 id="부호-있는-루프-변수-사용하기">부호 있는 루프 변수 사용하기</h3>
<p>표준 라이브러리를 다루기엔 조금 번거로워지지만, 루프 변수로 부호 있는 값을 사용하는 것은 우리가 평소에 쓰는 코드 스타일(수량엔 부호 있는 값을 쓴다)과 일치합니다. 
좋은 습관을 일관되게 지킬수록 전체적인 에러는 줄어들기 마련이죠.</p>
<p>부호 있는 변수를 쓰려면 세 가지 문제를 해결해야 합니다.</p>
<ol>
<li>어떤 타입을 쓸 것인가?</li>
<li>배열의 길이를 어떻게 부호 있는 값으로 가져올 것인가?</li>
<li>부호 있는 루프 변수를 배열 인덱스(부호 없음)로 어떻게 변환할 것인가?</li>
</ol>
<p><strong>1. 어떤 부호 있는 타입을 사용해야 할까요?</strong>
엄청나게 큰 배열을 다루는 게 아니라면, 그냥 우리가 평소에 쓰는 기본 숫자인 <strong>int</strong> 를 쓰시면 됩니다.</p>
<p>만약 배열이 아주 크거나 좀 더 안전하게 코딩하고 싶다면, 이름은 좀 이상하지만 <strong>std::ptrdiff_t</strong> 라는 타입을 사용할 수 있습니다. 이는 <code>std::size_t</code> 의 부호 있는 버전이라고 생각하시면 됩니다.</p>
<p>이름이 너무 어려우니, 아래처럼 직접 별명을 만들어(alias) 사용하는 것도 좋은 팁입니다.</p>
<pre><code class="language-cpp">using Index = std::ptrdiff_t;
// index를 사용한 샘플 루프
for (Index index{ 0 }; index &lt; static_cast&lt;Index&gt;(arr.size()); ++index)
</code></pre>
<p><strong>2. 배열의 길이를 부호 있는 값으로 가져오기</strong>
C++20 이전 버전에서는 배열의 크기를 반환하는 <code>.size()</code> 의 결과값에 <code>static_cast</code> 를 씌워 부호 있는 타입으로 바꿔주는 것이 최선이었습니다.</p>
<p>하지만 <strong>C++20</strong> 부터는 구세주가 등장했습니다! 바로 <strong>std::ssize()</strong> 입니다. 이 함수는 알아서 배열의 크기를 &#39;부호 있는 타입&#39;으로 반환해 줍니다. C++ 언어를 만드는 사람들도 이제는 &quot;부호 있는 인덱스를 쓰는 게 맞다&quot;고 생각한다는 강력한 증거이기도 하죠.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector arr{ 9, 7, 5, 3, 1 };

    for (auto index{ std::ssize(arr)-1 }; index &gt;= 0; --index) // std::ssize는 C++20에 도입되었습니다
        std::cout &lt;&lt; arr[static_cast&lt;std::size_t&gt;(index)] &lt;&lt; &#39; &#39;;

    return 0;
}
</code></pre>
<p><strong>3. 부호 있는 루프 변수를 부호 없는 인덱스로 변환하기</strong>
부호 있는 변수를 만들었지만, 막상 배열 안에 <code>arr[index]</code> 처럼 넣으려고 하면 또 경고가 뜹니다. 그래서 매번 <code>static_cast</code> 를 써야 하는데 코드가 지저분해지죠.</p>
<p>이를 해결하기 위해 짧은 이름의 도우미 함수를 직접 만들 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;type_traits&gt; // std::is_integral과 std::is_enum을 위해 필요
#include &lt;vector&gt;

using Index = std::ptrdiff_t;

// value를 std::size_t 타입의 객체로 변환하는 도우미 함수
// UZ는 std::size_t 타입 리터럴의 접미사입니다.
template &lt;typename T&gt;
constexpr std::size_t toUZ(T value)
{
    // T가 정수형 타입인지 확인합니다
    static_assert(std::is_integral&lt;T&gt;() || std::is_enum&lt;T&gt;());

    return static_cast&lt;std::size_t&gt;(value);
}

int main()
{
    std::vector arr{ 9, 7, 5, 3, 1 };

    auto length { static_cast&lt;Index&gt;(arr.size()) };  // C++20에서는 std::ssize() 사용을 권장합니다
    for (auto index{ length-1 }; index &gt;= 0; --index)
        std::cout &lt;&lt; arr[toUZ(index)] &lt;&lt; &#39; &#39;; // 부호 변환 경고를 피하기 위해 toUZ()를 사용합니다

    return 0;
}
</code></pre>
<p><code>arr[toUZ(index)]</code> 라고 쓰니 아까보다 훨씬 읽기 편해졌죠?</p>
<hr>
<h3 id="원시-c-스타일-배열로-접근하기-가장-추천하는-인덱싱-방법">원시 C 스타일 배열로 접근하기 (가장 추천하는 인덱싱 방법)</h3>
<p>이전 레슨에서, 컨테이너를 직접 인덱싱하는 대신 <strong>.data()</strong> 함수를 써서 그 안에 숨겨진 진짜 C 스타일 배열을 꺼내 쓸 수 있다고 배웠습니다. C 스타일 배열은 부호가 있든 없든 가리지 않고 인덱스를 받아주기 때문에, 복잡한 변환 경고 문제를 완벽하게 피해갈 수 있습니다!</p>
<pre><code class="language-cpp">int main()
{
    std::vector arr{ 9, 7, 5, 3, 1 };

    auto length { static_cast&lt;Index&gt;(arr.size()) };  // C++20에서는 std::ssize() 사용을 권장합니다
    for (auto index{ length - 1 }; index &gt;= 0; --index)
        std::cout &lt;&lt; arr.data()[index] &lt;&lt; &#39; &#39;;       // 부호 변환 경고를 피하기 위해 data()를 사용합니다

    return 0;
}
</code></pre>
<p>저희는 인덱스를 꼭 써야 한다면 이 방법이 가장 좋다고 생각합니다:</p>
<ul>
<li>부호 있는 변수와 인덱스를 마음껏 쓸 수 있습니다.</li>
<li>복잡한 별명이나 도우미 함수를 만들 필요가 없습니다.</li>
<li><code>.data()</code> 를 붙인다고 해서 코드가 크게 지저분해지지 않습니다.</li>
<li>최적화된 환경에서는 속도 저하도 전혀 없습니다.</li>
</ul>
<hr>
<h3 id="아예-인덱싱을-쓰지-마세요">아예 인덱싱을 쓰지 마세요</h3>
<p>지금까지 여러 가지 꼼수와 방법들을 소개해 드렸지만, 사실 모두 어느 정도의 단점을 가지고 있습니다. 하지만 이 모든 스트레스에서 벗어날 수 있는 훨씬 깔끔하고 현명한 방법이 있습니다. <strong>바로 인덱스 숫자 자체를 쓰지 않는 것입니다.</strong></p>
<p>C++ 에는 인덱스 숫자 없이도 배열 안의 데이터들을 하나씩 꺼내볼 수 있는 훌륭한 기능들이 있습니다. 숫자를 쓰지 않으니 부호 문제로 골치 아플 일도 없죠!
가장 대표적인 두 가지 방법이 바로 <strong>범위 기반 for 루프(range-based for loops)</strong> 와 <strong>반복자(iterators)</strong> 입니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
범위 기반 for 루프는 다음 레슨 (16.8)에서 배웁니다.
반복자(Iterators)는 다가오는 레슨 (18.2)에서 배웁니다.</p>
</blockquote>
<p>만약 여러분이 인덱스 변수를 단지 &#39;배열의 처음부터 끝까지 돌아보기 위한 용도&#39;로만 쓰고 계셨다면, 무조건 인덱스를 쓰지 않는 방법을 우선으로 선택하세요.</p>
<blockquote>
<p><strong>모범 사례</strong>
가능한 한 정수 숫자를 이용한 배열 인덱싱은 피하세요.</p>
</blockquote>
<hr>
<h2 id="168--범위-기반-for-문-for-each-문">16.8 — 범위 기반 for 문 (for-each 문)</h2>
<p>이전 16.6 레슨(배열과 반복문)에서는 인덱스(순서 번호)를 사용해서 배열의 각 항목을 하나씩 확인하는 기본 <code>for</code> 문을 배웠습니다. 예시를 다시 한번 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    std::size_t length { fibonacci.size() };
    for (std::size_t index { 0 }; index &lt; length; ++index)
       std::cout &lt;&lt; fibonacci[index] &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>기본 <code>for</code> 문은 배열을 순서대로 살펴볼 때 아주 편리한 방법입니다. 하지만 배열의 길이를 잘못 계산하거나, 처음과 끝을 잘못 지정하는 등(off-by-one 에러) 실수하기가 아주 쉽다는 단점이 있습니다. 게다가 인덱스의 부호 문제로 골치를 앓을 수도 있죠.</p>
<p>배열을 처음부터 끝까지 쭉 훑어보는 작업은 프로그래밍에서 정말 자주 쓰입니다. 그래서 C++은 인덱스를 헷갈리게 계산하지 않아도 되는 아주 편리한 기능을 제공합니다. 바로 <strong>범위 기반 for 문</strong> (또는 <strong>for-each 문</strong> 이라고도 부름)입니다! 이 반복문은 사용하기 훨씬 간단하고 안전하며, <code>std::vector</code>, <code>std::array</code> 등 C++에서 쓰는 거의 모든 배열 타입에 사용할 수 있습니다.</p>
<hr>
<h3 id="범위-기반-for-문">범위 기반 for 문</h3>
<p><strong>범위 기반 for 문</strong> 은 다음과 같은 형태로 작성합니다:</p>
<pre><code class="language-cpp">for (element_declaration : array_object)
   statement;
</code></pre>
<p>컴퓨터가 이 반복문을 만나면, <code>array_object</code>(배열) 안에 있는 항목들을 처음부터 끝까지 하나씩 차례대로 꺼내옵니다. 반복될 때마다 현재 항목의 값이 <code>element_declaration</code>
(새로 만든 변수)에 쏙 들어가고, 그 밑에 있는 <code>statement</code>(실행할 코드)가 작동하는 방식입니다.</p>
<p>여기서 팁! 새로 선언하는 변수의 타입은 배열 안에 들어있는 항목의 타입과 똑같아야 합니다. 다르면 컴퓨터가 억지로 타입을 바꾸려다가 문제가 생길 수 있어요.</p>
<p>피보나치(fibonacci) 배열의 모든 항목을 출력하는 아주 간단한 예시를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    for (int num : fibonacci) // fibonacci 배열을 순회하며 각 값을 `num`에 복사합니다.
       std::cout &lt;&lt; num &lt;&lt; &#39; &#39;; // `num`의 현재 값을 출력합니다.

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같이 나옵니다:
<code>0 1 1 2 3 5 8 13 21 34 55 89</code></p>
<p>정말 놀랍지 않나요? 배열의 전체 길이가 얼마인지 직접 적을 필요도 없고, 헷갈리는 인덱스(<code>[i]</code>)를 쓸 필요도 없습니다!</p>
<p>작동 원리를 조금 더 자세히 살펴볼까요? 
이 반복문은 <code>fibonacci</code> 배열의 모든 항목을 끝까지 훑어봅니다.</p>
<ol>
<li>첫 번째 반복: 변수 <code>num</code> 에 첫 번째 값인 <code>0</code>이 들어갑니다. 그리고 화면에 <code>0</code>을 출력하죠.</li>
<li>두 번째 반복: <code>num</code> 에 두 번째 값인 <code>1</code>이 들어갑니다. 그리고 <code>1</code>을 출력합니다.</li>
</ol>
<p>이런 식으로 배열에 남은 항목이 없을 때까지 알아서 착착 진행됩니다. 모든 항목을 다 보면 반복문은 스스로 조용히 종료되고, 프로그램은 다음 단계로 넘어갑니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong> 
여기서 선언한 변수(<code>num</code>)는 &quot;몇 번째인가요?&quot;를 알려주는 인덱스가 아닙니다. 
배열 안에 들어있는 <strong>실제 값</strong> 그 자체입니다!
변수에 값이 쏙 들어간다는 것은, 원본 배열에 있는 값이 변수로 <strong>복사</strong> 된다는 뜻이에요. (데이터 덩어리가 크다면 복사하는 데 시간이 꽤 걸리겠죠?)</p>
</blockquote>
<blockquote>
<p><strong>적극 권장 사항</strong> 
컨테이너(배열 등) 안의 모든 항목을 훑어볼 때는 복잡한 일반 <code>for</code> 문보다 
<strong>범위 기반 for 문</strong> 을 사용하는 것이 훨씬 좋습니다.</p>
</blockquote>
<hr>
<h3 id="텅-빈-배열에서는-어떻게-될까요">텅 빈 배열에서는 어떻게 될까요?</h3>
<p>만약 컨테이너 안에 항목이 하나도 없다면, 범위 기반 for 문 안에 있는 코드는 아예 실행되지 않고 조용히 넘어갑니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector empty { };

    for (int num : empty)
       std::cout &lt;&lt; &quot;Hi mom!\n&quot;;

    return 0;
}
</code></pre>
<p>안에 아무것도 없으니 위의 코드는 화면에 아무것도 출력하지 않습니다. 엄마 미안해요!</p>
<hr>
<h3 id="auto-키워드로-더-똑똑하게-쓰기"><code>auto</code> 키워드로 더 똑똑하게 쓰기</h3>
<p>아까 변수 타입과 배열 항목의 타입을 똑같이 맞춰야 한다고 했죠? 
이럴 때 <strong>auto</strong> 키워드를 쓰면 정말 찰떡궁합입니다. 컴퓨터(컴파일러)가 알아서 배열을 보고
&quot;아, 이 배열에는 정수가 들어있으니 변수도 정수형이겠구나!&quot; 하고 타입을 자동으로 맞춰주거든요. 우리가 일일이 적을 필요도 없고, 오타를 낼 걱정도 사라집니다.</p>
<p>위에서 본 예시에 <strong>auto</strong> 를 적용해 볼게요:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

    for (auto num : fibonacci) // 컴파일러가 num의 타입을 `int`로 자동 유추합니다.
       std::cout &lt;&lt; num &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><code>std::vector fibonacci</code> 에는 <code>int</code>(정수) 타입들이 들어있기 때문에, 컴퓨터는 <code>num</code> 이 <code>int</code> 타입이라는 것을 척 보고 알아냅니다.</p>
<blockquote>
<p><strong>적극 권장 사항</strong> 
범위 기반 for 문을 쓸 때는 <strong>auto</strong> 키워드를 사용해서 컴퓨터가 스스로 타입을 알아내게 하세요.</p>
</blockquote>
<p><strong>auto</strong> 를 쓰면 좋은 점이 또 하나 있습니다. 나중에 배열의 데이터 타입이 <code>int</code>에서 더 큰 숫자인 <code>long</code>으로 바뀌더라도, 코드를 수정할 필요가 없습니다. 컴퓨터가 알아서 바뀐 타입에 맞춰주기 때문에 에러가 날 확률이 크게 줄어들죠!</p>
<hr>
<h3 id="참조reference를-사용해서-무거운-복사-피하기">참조(Reference)를 사용해서 무거운 복사 피하기</h3>
<p>이번에는 글자들(<code>std::string</code>)이 들어있는 배열을 훑어보는 코드를 생각해 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;vector&gt;

int main()
{
    std::vector&lt;std::string&gt; words{ &quot;peter&quot;, &quot;likes&quot;, &quot;frozen&quot;, &quot;yogurt&quot; };

    for (auto word : words)
        std::cout &lt;&lt; word &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드가 반복될 때마다 배열에 있는 글자들이 변수 <code>word</code> 로 하나씩 &#39;복사&#39;됩니다. 글자 데이터(<code>std::string</code>)를 복사하는 건 컴퓨터에게 꽤 무겁고 힘든 작업이에요. 단순히 화면에 출력만 하고 버릴 건데, 굳이 무겁게 복사본을 만들 필요는 없겠죠?</p>
<p>다행히 변수를 <strong>참조</strong> (reference) 로 만들면 이 문제를 해결할 수 있습니다! 그냥 배열에 있는 원본을 손가락으로 가리키기만 하는 방식이에요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;vector&gt;

int main()
{
    std::vector&lt;std::string&gt; words{ &quot;peter&quot;, &quot;likes&quot;, &quot;frozen&quot;, &quot;yogurt&quot; };

    for (const auto&amp; word : words) // 이제 word는 읽기 전용 참조(const reference)입니다.
        std::cout &lt;&lt; word &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위 예시에서 <code>word</code> 에 <code>const auto&amp;</code> 를 붙였습니다. 이렇게 하면 매번 반복될 때마다 굳이 무겁게 복사본을 만들지 않고, 원본 데이터에 바로 연결됩니다! 화면에 출력만 할 거라서 <code>const</code>(읽기 전용, 수정 불가)를 붙여서 원본이 다치지 않게 안전장치도 걸어두었죠.</p>
<p>만약 원본 데이터를 직접 수정하고 싶다면 <code>const</code>를 빼고 그냥 <code>auto&amp;</code> 라고 쓰면 됩니다.</p>
<hr>
<h3 id="언제-auto-auto-const-auto-를-써야-할까요">언제 <code>auto</code>, <code>auto&amp;</code>, <code>const auto&amp;</code> 를 써야 할까요?</h3>
<p>가벼운 데이터는 <strong>auto</strong> , 원본을 수정하고 싶을 때는 <strong>auto&amp;</strong> , 무거운 데이터는 <strong>const auto&amp;</strong> 를 쓰는 것이 기본 규칙입니다. 하지만 범위 기반 for 문을 쓸 때는 <strong>미래를 대비해서 무조건 const auto&amp; 를 쓰는 것을 추천</strong> 하는 전문가들이 많습니다. 왜 그럴까요?</p>
<p>다음 코드를 한번 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;
#include &lt;vector&gt;

int main()
{
    std::vector&lt;std::string_view&gt; words{ &quot;peter&quot;, &quot;likes&quot;, &quot;frozen&quot;, &quot;yogurt&quot; }; // 요소들이 std::string_view 타입입니다.

    for (auto word : words) // string_view는 보통 값으로 전달하므로, 여기서는 auto를 사용합니다.
        std::cout &lt;&lt; word &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><code>std::string_view</code>는 아주 가벼운 녀석이라서 복사해도 전혀 부담이 없습니다.
그래서 <strong>auto</strong> 를 썼죠.
그런데 만약 나중에 프로그램이 수정되어서, 이 배열이 무거운 <code>std::string</code> 배열로 바뀐다면 어떨까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;vector&gt;

int main()
{
    std::vector&lt;std::string&gt; words{ &quot;peter&quot;, &quot;likes&quot;, &quot;frozen&quot;, &quot;yogurt&quot; }; // 이 부분을 수정해야 하는 건 명백합니다.

    for (auto word : words) // 하지만 이 부분도 수정해야 한다는 건 눈치채기 어렵습니다.
        std::cout &lt;&lt; word &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드는 아무 에러 없이 잘 실행됩니다. 하지만 <code>auto</code>라고 적어둔 코드 때문에, 갑자기 매번 무거운 문자열 복사 작업이 몰래 일어나게 됩니다. 우리도 모르는 사이에 프로그램이 엄청나게 느려져 버리는 엄청난 손해를 보게 되는 거죠!</p>
<p>이런 무서운 상황을 피하려면 어떻게 해야 할까요?
단순히 복사본을 피하고 싶다면, 처음부터 <strong>const auto&amp;</strong> 를 기본으로 쓰는 것이 좋습니다. 원본을 살짝 가리키는 방식(참조)은 성능에 나쁜 영향을 거의 주지 않으면서도, 나중에 데이터 타입이 무거운 것으로 바뀌었을 때 갑자기 프로그램이 느려지는 대참사를 완벽하게 막아주기 때문입니다.</p>
<blockquote>
<p><strong>권장 사항</strong>
범위 기반 for 문에서 요소의 타입을 정할 때는 다음 기준을 따르세요.</p>
<ul>
<li><strong>auto</strong> : 데이터를 복사해서 그 복사본을 수정하고 싶을 때.</li>
<li><strong>auto&amp;</strong> : 원본 데이터를 직접 수정하고 싶을 때.</li>
<li><strong>const auto&amp;</strong> : 그 외의 모든 경우 
(그냥 원본 데이터를 살펴보기만 할 때. 이것이 가장 추천하는 방법입니다!)</li>
</ul>
</blockquote>
<hr>
<h3 id="다른-종류의-표준-배열컨테이너들과-함께-쓰기">다른 종류의 표준 배열(컨테이너)들과 함께 쓰기</h3>
<p>범위 기반 for 문은 우리가 배운 기본 배열뿐만 아니라, <code>std::array</code>, <code>std::vector</code>는 물론이고 리스트(Linked list), 트리(Trees), 맵(Maps) 같은 C++의 아주 다양한 데이터 묶음과 모두 찰떡처럼 잘 작동합니다. 아직 이런 것들을 안 배웠어도 전혀 걱정 마세요! 
&quot;범위 기반 for 문은 거의 모든 컨테이너에서 쓸 수 있는 만능 도구구나!&quot; 라고만 기억해 두시면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;array&gt;
#include &lt;iostream&gt;

int main()
{
    std::array fibonacci{ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 }; // 여기서는 std::array를 사용했다는 점에 주목하세요.

    for (auto number : fibonacci)
    {
        std::cout &lt;&lt; number &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<blockquote>
<p><strong>심화 학습자를 위한 참고</strong>
배열의 길이를 잃어버린 &#39;퇴화된(decayed) C-스타일 배열&#39;에서는 이 기능을 쓸 수 없습니다. 범위 기반 for 문은 배열이 언제 끝나는지 전체 길이를 꼭 알아야 하거든요. 또, 열거형(enum)에서도 바로 쓸 수는 없는데, 이를 해결하는 방법은 나중에 17.6 레슨에서 배우게 됩니다.</p>
</blockquote>
<hr>
<h3 id="현재-항목의-인덱스순서-번호-알아내기">현재 항목의 인덱스(순서 번호) 알아내기</h3>
<p>범위 기반 for 문은 아주 편리하지만, &quot;지금 이게 몇 번째 항목이지?&quot; 하고 알려주는 기능은 쏙 빠져있습니다. C++이 제공하는 여러 컨테이너 중에는 아예 순서 번호(인덱스)라는 개념 자체가 없는 애들도 있기 때문이죠.</p>
<p>이 반복문은 무조건 앞으로만 직진하고 중간에 건너뛰지 않기 때문에, 순서를 세는 변수(카운터)를 따로 하나 만들어서 숫자를 올려가는 방식을 쓸 수는 있습니다. 하지만 굳이 그렇게까지 순서 번호가 필요하다면, 범위 기반 for 문 대신 그냥 옛날 방식의 일반 <code>for</code> 문을 쓰는 게 더 나은 선택일 수 있습니다.</p>
<hr>
<h3 id="거꾸로-훑어보기-c20-버전-이상">거꾸로 훑어보기 (C++20 버전 이상)</h3>
<p>방금 말씀드렸듯, 이 반복문은 무조건 처음부터 끝까지 앞으로만 쭉 직진합니다. 하지만 가끔은 뒤에서부터 거꾸로 읽어오고 싶을 때가 있죠? 예전 C++ 버전에서는 이를 쉽게 할 수 없어서 일반 <code>for</code> 문을 써야만 했습니다.</p>
<p>하지만 최신 C++20 버전부터는 마법 같은 방법이 생겼습니다! <code>Ranges</code> 라이브러리의 <code>std::views::reverse</code> 라는 도구를 쓰면, 거꾸로 훑어보는 뷰(view)를 만들어낼 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;ranges&gt; // C++20
#include &lt;string_view&gt;
#include &lt;vector&gt;

int main()
{
    std::vector&lt;std::string_view&gt; words{ &quot;Alex&quot;, &quot;Bobby&quot;, &quot;Chad&quot;, &quot;Dave&quot; }; // 알파벳 순서대로 정렬됨

    for (const auto&amp; word : std::views::reverse(words)) // 역순 뷰(reverse view)를 생성합니다.
        std::cout &lt;&lt; word &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 이름이 거꾸로 출력됩니다:</p>
<pre><code>Dave
Chad
Bobby
Alex
</code></pre><p>아직 <code>Ranges</code> 라이브러리에 대해 자세히 배우지는 않았으니, 지금은 &quot;최신 버전에는 이런 멋진 마법도 있구나!&quot; 하고 가볍게 알아두시면 충분합니다.</p>
<hr>
<h2 id="169--열거형을-활용한-배열-인덱싱과-길이-구하기">16.9 — 열거형을 활용한 배열 인덱싱과 길이 구하기</h2>
<p>배열을 사용할 때 가장 큰 문제점 중 하나는, <code>0</code>, <code>1</code>, <code>2</code> 같은 숫자 인덱스만으로는 그 칸에 들어있는 데이터가 <strong>어떤 의미인지 알 수 없다는 점</strong> 입니다.</p>
<p>5명의 시험 점수를 담아둔 배열을 예로 들어볼게요.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };

    testScores[2] = 76; // 이 점수는 누구의 것일까요?
}
</code></pre>
<p><code>testScores[2]</code>가 도대체 어느 학생의 점수를 뜻하는 걸까요? 코드만 봐서는 전혀 알 수가 없습니다.</p>
<hr>
<h3 id="일반-열거형unscoped-enumerator을-인덱스로-쓰기">일반 열거형(Unscoped enumerator)을 인덱스로 쓰기</h3>
<p>이전 레슨에서 배열(또는 <code>std::vector</code>)의 칸 번호(인덱스)를 지정할 때는 보통 <code>std::size_t</code>라는 양수형 숫자 타입을 써야 한다고 배웠습니다.</p>
<p>그런데 일반적인 <code>enum</code>(범위를 지정하지 않은 열거형)은 컴퓨터가 알아서 <code>std::size_t</code> 타입의 숫자로 척척 변환해 줍니다! 이 원리를 이용하면, 숫자가 아니라 <strong>이름표(열거형)</strong> 를 배열의 인덱스로 써서 코드의 의미를 아주 명확하게 만들 수 있어요.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

namespace Students
{
    enum Names
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        max_students // 5
    };
}

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };

    testScores[Students::stan] = 76; // 이제 stan(스탠)의 시험 점수를 수정합니다.

    return 0;
}
</code></pre>
<p>이렇게 하면 배열의 각 칸이 누구의 점수인지 훨씬 쉽게 알 수 있죠! 
게다가 열거자에 적힌 이름들은 기본적으로 &#39;상수(constexpr)&#39; 취급을 받기 때문에, 부호가 없는 숫자로 변환될 때 생기는 에러나 경고도 피할 수 있답니다.</p>
<hr>
<h3 id="상수가-아닌-열거형-변수-사용하기">상수가 아닌 열거형 변수 사용하기</h3>
<p>일반 <code>enum</code>의 바탕이 되는 숫자 타입은 컴퓨터(컴파일러) 마음대로 결정됩니다. 
양수만 될 수도 있고, 음수/양수 모두 되는 타입(signed)이 될 수도 있어요. 
열거형 안의 이름표들을 직접 쓸 때는 알아서 처리되니 문제가 없지만, 열거형 &#39;변수&#39;를 따로 만들어서 쓰려고 하면 문제가 생길 수도 있습니다.</p>
<p>만약 기본 타입이 음수/양수 모두 되는 타입으로 잡혀 있다면, 이걸 배열 인덱스로 쓸 때 부호 변환 경고가 뜰 수 있어요.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

namespace Students
{
    enum Names
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        max_students // 5
    };
}

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };
    Students::Names name { Students::stan }; // 상수가 아님

    testScores[name] = 76; // Student::Names의 기본 타입이 부호 있는(signed) 타입이라면 부호 변환 경고가 뜰 수 있습니다.

    return 0;
}
</code></pre>
<p>이럴 때는 <code>name</code>을 상수로 만들거나, 아니면 아예 <code>enum</code>을 만들 때부터 
<strong>&quot;이 열거형은 무조건 부호 없는 양수(unsigned int)로 쓸 거야!&quot;</strong> 라고 콕 짚어주면 해결됩니다.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

namespace Students
{
    enum Names : unsigned int // 바탕 타입을 unsigned int로 명시적으로 지정합니다.
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        max_students // 5
    };
}

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };
    Students::Names name { Students::stan }; // 상수가 아님

    testScores[name] = 76; // name이 unsigned(부호 없음)이므로 부호 변환 경고가 발생하지 않습니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="개수-세기용-열거자-사용하기">개수 세기용 열거자 사용하기</h3>
<p>눈치채셨나요? 아까부터 열거형 목록 맨 끝에 <code>max_students</code>라는 항목이 슬쩍 들어가 있었죠. 열거형은 따로 숫자를 안 적어주면 0부터 시작해서 1씩 커집니다. 그래서 앞에 5명의 학생이 있으면, 마지막에 오는 <code>max_students</code>는 자동으로 <strong>5</strong> 가 됩니다!</p>
<p>이걸 <strong>&#39;개수 세기용 열거자&#39;</strong> 라고 부를게요. 이 값은 전체 학생 수가 몇 명인지 알고 싶을 때 요긴하게 쓰입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

namespace Students
{
    enum Names
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        // 앞으로 추가될 학생들은 여기에 넣으세요
        max_students // 5
    };
}

int main()
{
    std::vector&lt;int&gt; testScores(Students::max_students); // 5개의 요소를 가진 벡터(배열)를 만듭니다

    testScores[Students::stan] = 76; // 이제 stan(스탠)의 시험 점수를 수정합니다.

    std::cout &lt;&lt; &quot;The class has &quot; &lt;&lt; Students::max_students &lt;&lt; &quot; students\n&quot;;

    return 0;
}
</code></pre>
<p>이 방법은 정말 마법 같아요! 나중에 학생 한 명을 더 추가하고 싶으면, <code>max_students</code> 바로 윗줄에 이름만 적어주면 됩니다. 그러면 <code>max_students</code> 값도 자동으로 1 늘어나고, 배열 크기도 알아서 커지기 때문에 다른 코드를 손댈 필요가 전혀 없습니다.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;
#include &lt;iostream&gt;

namespace Students
{
    enum Names
    {
        kenny, // 0
        kyle, // 1
        stan, // 2
        butters, // 3
        cartman, // 4
        wendy, // 5 (추가됨)
        // 앞으로 추가될 학생들은 여기에 넣으세요
        max_students // 이제 6이 됩니다
    };
}

int main()
{
    std::vector&lt;int&gt; testScores(Students::max_students); // 이제 6개의 요소를 할당합니다

    testScores[Students::stan] = 76; // 여전히 잘 작동합니다

    std::cout &lt;&lt; &quot;The class has &quot; &lt;&lt; Students::max_students &lt;&lt; &quot; students\n&quot;;

    return 0;
}
</code></pre>
<hr>
<h3 id="개수-세기용-열거자로-배열-길이-꼼꼼하게-확인하기-asserting">개수 세기용 열거자로 배열 길이 꼼꼼하게 확인하기 (Asserting)</h3>
<p>보통은 처음부터 초기값을 쫙 나열해서 배열을 만드는 경우가 많아요. 이때 우리가 만든 &#39;학생 수(개수 세기용 열거자)&#39;와 &#39;실제 배열에 들어간 점수의 개수&#39;가 똑같은지 확인해 주는 것이 좋습니다. 만약 학생 이름은 새로 적어놓고 점수 적는 걸 깜빡했다면 여기서 딱 걸리게 되거든요!</p>
<pre><code class="language-cpp">#include &lt;cassert&gt;
#include &lt;iostream&gt;
#include &lt;vector&gt;

enum StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    std::vector testScores { 78, 94, 66, 77, 14 };

    // 시험 점수의 개수가 학생 수와 동일한지 확인합니다
    assert(std::size(testScores) == max_students);

    return 0;
}
</code></pre>
<blockquote>
<p><strong>팁</strong>
만약 일반 배열이 아니라 상수(constexpr) 배열을 쓰고 있다면 <code>static_assert</code>를 써야 합니다. <code>std::vector</code>는 상수를 지원하지 않지만, <code>std::array</code>나 C 스타일 배열은 지원하거든요.</p>
</blockquote>
<blockquote>
<p><strong>모범 사례</strong>
상수로 만든(constexpr) 배열의 길이를 확인할 때는 <strong>static_assert</strong> 를 사용하세요. 
일반 배열의 길이를 확인할 때는 <strong>assert</strong> 를 사용하세요.</p>
</blockquote>
<hr>
<h3 id="배열과-enum-class-스코프가-있는-안전한-열거형">배열과 enum class (스코프가 있는 안전한 열거형)</h3>
<p>일반 <code>enum</code>은 이름들이 코드 이곳저곳에 섞여서 충돌할 위험이 있습니다. 
그래서 C++에서는 좀 더 안전한 <code>enum class</code>를 쓰는 걸 추천하죠. </p>
<p>하지만 깐깐한 <code>enum class</code>는 자동으로 숫자로 변환되지 않기 때문에, 배열 인덱스로 바로 쓰려고 하면 에러를 뿜어냅니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

enum class StudentNames // 이제 enum class입니다
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    // 컴파일 에러: StudentNames에서 std::size_t로 변환할 수 없습니다.
    std::vector&lt;int&gt; testScores(StudentNames::max_students);

    // 컴파일 에러: StudentNames에서 std::size_t로 변환할 수 없습니다.
    testScores[StudentNames::stan] = 76;

    // 컴파일 에러: StudentNames에서 operator&lt;&lt;가 출력할 수 있는 타입으로 변환할 수 없습니다.
    std::cout &lt;&lt; &quot;The class has &quot; &lt;&lt; StudentNames::max_students &lt;&lt; &quot; students\n&quot;;

    return 0;
}
</code></pre>
<p>이걸 해결하는 가장 단순한 방법은 <code>static_cast</code>를 써서 강제로 숫자로 바꿔주는 건데... 코드가 너무 길어지고 못생겨집니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

enum class StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

int main()
{
    std::vector&lt;int&gt; testScores(static_cast&lt;int&gt;(StudentNames::max_students));

    testScores[static_cast&lt;int&gt;(StudentNames::stan)] = 76;

    std::cout &lt;&lt; &quot;The class has &quot; &lt;&lt; static_cast&lt;int&gt;(StudentNames::max_students) &lt;&lt; &quot; students\n&quot;;

    return 0;
}
</code></pre>
<p>타자 치기 너무 귀찮죠? 더 좋은 방법은 단항 <code>+</code> 기호에 &quot;이 기호를 붙이면 열거형을 숫자로 바꿔줘!&quot;라는 특별한 기능을 심어두는(오버로딩) 겁니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;type_traits&gt; // std::underlying_type_t를 사용하기 위해 필요
#include &lt;vector&gt;

enum class StudentNames
{
    kenny, // 0
    kyle, // 1
    stan, // 2
    butters, // 3
    cartman, // 4
    max_students // 5
};

// 단항 + 연산자를 오버로딩하여 StudentNames를 기본 바탕 타입으로 변환합니다
constexpr auto operator+(StudentNames a) noexcept
{
    return static_cast&lt;std::underlying_type_t&lt;StudentNames&gt;&gt;(a);
}

int main()
{
    std::vector&lt;int&gt; testScores(+StudentNames::max_students);

    testScores[+StudentNames::stan] = 76;

    std::cout &lt;&lt; &quot;The class has &quot; &lt;&lt; +StudentNames::max_students &lt;&lt; &quot; students\n&quot;;

    return 0;
}
</code></pre>
<p>이렇게 하면 <code>enum class</code> 이름 앞에 <code>+</code>만 톡 붙여주면 되니까 훨씬 깔끔하죠!
하지만, 배열에 접근할 일이 너~무 많다면 매번 <code>+</code>를 붙이는 것도 일이 될 수 있으니, 그냥 안전한 공간(<code>namespace</code>나 <code>class</code>) 안에 일반 <code>enum</code>을 넣어서 쓰는 것도 좋은 선택이랍니다.</p>
<hr>
<h2 id="1610--stdvector-크기-조절과-용량">16.10 — std::vector 크기 조절과 용량</h2>
<p>이번 장의 이전 레슨들에서는 컨테이너와 배열, 그리고 <code>std::vector</code> 에 대해 배웠어요. 
배열 안의 데이터에 어떻게 접근하는지, 배열의 길이는 어떻게 아는지, 그리고 배열을 어떻게 쭉 훑어보는지(순회하는지)도 이야기했었죠. 예시로는 <code>std::vector</code> 를 사용했지만, 우리가 배운 개념들은 보통 모든 종류의 배열에 다 똑같이 써먹을 수 있는 것들이었어요.</p>
<p>이제 남은 레슨들에서는 <code>std::vector</code> 가 다른 대부분의 배열들과 확연히 다르게 만들어주는 <strong>&#39;단 하나의 엄청난 특징&#39;</strong> 에 집중해 볼 거예요. 바로 <strong>&#39;처음 만들어진 이후에도 스스로 크기를 늘리거나 줄일 수 있는 능력&#39;</strong> 이랍니다!</p>
<hr>
<h3 id="크기가-고정된-배열-vs-크기를-바꿀-수-있는-동적-배열">크기가 고정된 배열 vs 크기를 바꿀 수 있는 동적 배열</h3>
<p>대부분의 배열은 아주 큰 단점이 하나 있어요. 처음 만들 때 배열의 길이를 미리 딱 정해야 하고, 한 번 정해지면 절대 바꿀 수 없다는 거죠. 이런 배열들을 <strong>&#39;고정 크기 배열&#39;</strong> 이라고 부릅니다. <code>std::array</code> 와 C언어 스타일의 배열이 바로 이 고정 크기 배열에 속해요. 이건 다음 장에서 더 자세히 다룰게요.</p>
<p>반면에 <code>std::vector</code> 는 <strong>&#39;동적 배열(Dynamic array)&#39;</strong> 이에요. 필요할 때마다 크기를 자유자재로 바꿀 수 있는 배열이죠. 이 유연함이 바로 <code>std::vector</code> 를 아주 특별하게 만들어줍니다.</p>
<hr>
<h3 id="프로그램-실행-중에-stdvector-크기-바꾸기">프로그램 실행 중에 <code>std::vector</code> 크기 바꾸기</h3>
<p><code>std::vector</code> 는 만들어진 후에도 <code>resize()</code> 라는 기능(멤버 함수)을 써서 원하는 새로운 길이로 크기를 바꿀 수 있어요. 코드로 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main(){
    std::vector v{ 0, 1, 2 }; // 3개의 요소를 가진 벡터 만들기
    std::cout &lt;&lt; &quot;The length is: &quot; &lt;&lt; v.size() &lt;&lt; &#39;\n&#39;;

    v.resize(5);              // 5개의 요소로 크기 늘리기
    std::cout &lt;&lt; &quot;The length is: &quot; &lt;&lt; v.size() &lt;&lt; &#39;\n&#39;;

    for (auto i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 이렇게 출력돼요:</p>
<blockquote>
<p>The length is: 3
The length is: 5
0 1 2 0 0</p>
</blockquote>
<p>여기서 주목해야 할 아주 중요한 사실 두 가지가 있어요!
첫째, 크기를 늘렸을 때 <strong>원래 들어있던 데이터(0, 1, 2)는 지워지지 않고 그대로 유지</strong> 되었습니다!
둘째, 새로 생겨난 빈칸들은 기본값으로 알아서 채워집니다. 숫자의 경우 <code>0</code> 으로 채워지기 때문에, 새로 생긴 두 칸이 모두 <code>0</code> 이 된 것을 볼 수 있어요.</p>
<p>물론, 반대로 크기를 더 작게 줄일 수도 있습니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void printLength(const std::vector&lt;int&gt;&amp; v){
    std::cout &lt;&lt; &quot;The length is: &quot;    &lt;&lt; v.size() &lt;&lt; &#39;\n&#39;;
}

int main(){
    std::vector v{ 0, 1, 2, 3, 4 }; // 처음 길이는 5
    printLength(v);

    v.resize(3);                    // 3개의 요소로 크기 줄이기
    printLength(v);

    for (int i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>출력 결과:</p>
<blockquote>
<p>The length is: 5
The length is: 3
0 1 2</p>
</blockquote>
<hr>
<h3 id="stdvector-의-길이length-vs-용량capacity"><code>std::vector</code> 의 길이(Length) vs 용량(Capacity)</h3>
<p>12채의 집이 일렬로 늘어서 있다고 상상해 보세요. 우리는 집의 개수(길이)가 12채라고 말할 겁니다. 그런데 그 집들에 실제로 사람들이 살고 있는지 알고 싶다면... 직접 초인종을 누르며 돌아다녀 봐야 알 수 있겠죠. 즉, <strong>&#39;길이&#39;</strong> 만 알 때는 그저 &#39;몇 개가 존재하는지&#39;만 알 수 있습니다.</p>
<p>이번엔 계란판을 생각해 볼까요? 지금 계란 5개가 들어있습니다. 여기서 계란의 개수(길이)는 5개입니다. 하지만 여기서 우리가 신경 써야 할 개념이 하나 더 있어요. &quot;이 계란판이 꽉 찬다면 최대 몇 개까지 담을 수 있을까?&quot; 하는 거죠. 이 계란판의 <strong>&#39;용량(Capacity)&#39;</strong> 은 12입니다. 총 12개를 담을 수 있는 공간이 있는데 현재 5개만 쓰고 있는 거죠. 따라서 우리는 7개의 계란을 더 사 와도 넘치지 않게 담을 수 있습니다. 길이와 용량 두 가지를 모두 알면, &#39;현재 몇 개가 있는지&#39;와 &#39;앞으로 몇 개를 더 넣을 공간이 있는지&#39;를 구분할 수 있게 됩니다.</p>
<p>지금까지 우리는 <code>std::vector</code> 의 <strong>&#39;길이&#39;</strong> 에 대해서만 이야기했어요. 하지만 <code>std::vector</code> 에는 <strong>&#39;용량&#39;</strong> 이라는 것도 있습니다. 쉽게 말해, <strong>용량</strong> 은 &#39;벡터가 데이터를 담기 위해 미리 준비해 둔 메모리 빈 공간의 크기&#39;이고, <strong>길이</strong> 는 &#39;그 공간 안에서 실제로 사용 중인 데이터의 개수&#39;입니다.</p>
<p>용량이 5인 <code>std::vector</code> 는 요소 5개를 담을 공간을 미리 확보해 둔 상태예요. 만약 여기에 2개의 데이터만 들어있다면, 이 벡터의 길이(사이즈)는 2입니다. 나머지 3칸은 미리 자리를 잡아두긴 했지만 아직 쓰고 있지 않은 &#39;예비 공간&#39;인 셈이죠. 나중에 데이터가 더 들어오면 넘치지 않게 바로 쓸 수 있습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong> </p>
<ul>
<li><strong>벡터의 길이(Length)</strong> : 현재 &quot;사용 중인&quot; 데이터가 몇 개인가?</li>
<li><strong>벡터의 용량(Capacity)</strong> : 현재 메모리에 &quot;확보해 둔 빈 공간&quot;이 총 몇 개인가?</li>
</ul>
</blockquote>
<hr>
<h3 id="stdvector-의-용량-확인하기"><code>std::vector</code> 의 용량 확인하기</h3>
<p><code>capacity()</code> 라는 함수를 쓰면 현재 벡터의 용량이 얼마인지 물어볼 수 있어요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void printCapLen(const std::vector&lt;int&gt;&amp; v){
    std::cout &lt;&lt; &quot;Capacity: &quot; &lt;&lt; v.capacity() &lt;&lt; &quot; Length:&quot;    &lt;&lt; v.size() &lt;&lt; &#39;\n&#39;;
}

int main(){
    std::vector v{ 0, 1, 2 }; // 처음 길이는 3

    printCapLen(v);

    for (auto i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    v.resize(5); // 5개의 요소로 크기 늘리기

    printCapLen(v);

    for (auto i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 컴퓨터마다 조금 다를 수 있지만 대략 이렇게 나옵니다:</p>
<blockquote>
<p>Capacity: 3  Length: 3
0 1 2
Capacity: 5  Length: 5
0 1 2 0 0</p>
</blockquote>
<p>처음에 3개의 데이터로 벡터를 만들었죠? 이때 벡터는 딱 3개가 들어갈 공간만 준비(용량 3)하고 3개를 모두 사용(길이 3)합니다.
그다음 <code>resize(5)</code> 를 불러서 길이를 5로 늘려달라고 주문했어요. 벡터는 현재 빈 공간이 3칸밖에 없는데 5칸이 필요해졌으니, 추가 데이터를 담기 위해 더 큰 새로운 공간을 구해야만 했습니다.
결과적으로 벡터는 5개를 담을 공간(용량 5)을 마련했고, 5개 모두 사용 중(길이 5)인 상태가 되었습니다.</p>
<p>평소에 코딩할 때는 <code>capacity()</code> 함수를 직접 쓸 일이 많지는 않겠지만, 벡터의 내부 공간이 어떻게 변하는지 눈으로 확인하기 위해 이번 레슨에서는 자주 써볼 거예요.</p>
<hr>
<h3 id="메모리-재할당-그게-왜-부담스러울까요">메모리 재할당, 그게 왜 부담스러울까요?</h3>
<p><code>std::vector</code> 가 관리하는 메모리 공간의 크기를 바꾸는 과정을 <strong>&#39;재할당&#39;</strong> 이라고 부릅니다. 이 과정은 마치 &#39;새 집으로 이사하는 것&#39;과 비슷해요.</p>
<ol>
<li>벡터가 원하는 만큼의 데이터가 들어갈 수 있는 더 큰 새 집(새 메모리 공간)을 구합니다.</li>
<li>원래 집에 있던 짐(데이터)들을 전부 하나하나 새 집으로 옮깁니다(복사 또는 이동). 그리고 원래 살던 헌 집은 시스템에 반납합니다.</li>
<li>이제 벡터의 용량과 길이를 새 집에 맞춰 업데이트합니다.</li>
</ol>
<p>겉보기에는 그냥 벡터가 쭉 늘어난 것처럼 보이지만, 컴퓨터 내부에서는 <strong>완전히 새로운 공간으로 싹 다 이사</strong> 를 한 거랍니다!
이사를 할 때 모든 짐을 다 싸서 옮겨야 하니 아주 힘들고 시간이 오래 걸리겠죠? 컴퓨터도 마찬가지라서, 메모리를 재할당하는 건 컴퓨터의 힘을 많이 쓰는(비용이 큰) 작업이에요. 그래서 가급적 이사를 너무 자주 하지 않도록 피하는 것이 좋습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
재할당은 컴퓨터에게 꽤 부담스러운 작업입니다. 
불필요한 재할당은 피하는 것이 좋습니다.</p>
</blockquote>
<hr>
<h3 id="길이와-용량을-굳이-나누는-이유가-뭔가요">길이와 용량을 굳이 나누는 이유가 뭔가요?</h3>
<p><code>std::vector</code> 는 공간이 부족하면 알아서 이사(재할당)를 하긴 하지만, 웬만하면 이사하는 걸 무척 귀찮아합니다. (마치 &quot;안 하는 게 낫겠습니다&quot;라고 말하던 소설 주인공 바틀비처럼 말이죠!) 이사하는 데 에너지가 너무 많이 드니까요.</p>
<p>만약 벡터가 &#39;용량&#39;이라는 개념 없이 &#39;길이&#39;만 알고 있다면 어떻게 될까요? 크기를 1칸 늘리든 줄이든, 무언가 바뀔 때마다 매번 새집을 구해서 이사(재할당)를 해야 할 겁니다. 정말 끔찍하죠! 하지만 <strong>길이와 용량을 따로 관리하면, 벡터가 언제 진짜 이사가 필요한지 똑똑하게 판단할 수 있습니다.</strong></p>
<p>아래 예시를 볼까요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void printCapLen(const std::vector&lt;int&gt;&amp; v){
    std::cout &lt;&lt; &quot;Capacity: &quot; &lt;&lt; v.capacity() &lt;&lt; &quot; Length:&quot;    &lt;&lt; v.size() &lt;&lt; &#39;\n&#39;;
}

int main(){
    // 길이가 5인 벡터 만들기
    std::vector v{ 0, 1, 2, 3, 4 };
    v = { 0, 1, 2, 3, 4 }; // 좋아요, 배열의 길이는 5
    printCapLen(v);

    for (auto i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    // 벡터의 크기를 3개의 요소로 줄이기
    v.resize(3); // 여기에 3개의 요소로 이루어진 리스트를 할당할 수도 있습니다
    printCapLen(v);

    for (auto i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    // 다시 5개의 요소로 크기 늘리기
    v.resize(5);
    printCapLen(v);

    for (auto i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>결과는 이렇습니다:</p>
<blockquote>
<p>Capacity: 5  Length: 5
0 1 2 3 4
Capacity: 5  Length: 3
0 1 2
Capacity: 5  Length: 5
0 1 2 0 0</p>
</blockquote>
<p>처음 5개의 데이터를 넣었을 때 용량과 길이가 모두 5였습니다.
그다음 <code>v.resize(3)</code> 을 불러서 배열을 작게 줄여달라고 했죠. 길이는 3으로 줄었지만, 주목하세요! <strong>용량은 여전히 5</strong> 입니다. 벡터가 집(메모리)을 버리고 이사(재할당)를 하지 않았다는 뜻이에요. 단순히 뒤쪽 빈방의 불만 꺼둔 셈이죠.
마지막으로 다시 <code>v.resize(5)</code> 를 불렀습니다. 벡터는 이미 용량이 5, 즉 빈방이 준비되어 있었기 때문에 <strong>이번에도 귀찮은 이사를 할 필요가 없었습니다.</strong> 그냥 꺼뒀던 불을 켜고 길이만 5로 되돌린 다음, 뒤쪽 두 칸을 기본값(0)으로 채워주기만 하면 끝이었죠.</p>
<p>길이와 용량을 따로 나눈 덕분에 우리는 불필요한 이사(재할당)를 2번이나 아낄 수 있었습니다. 나중에 데이터를 하나씩 하나씩 계속 추가해야 할 일이 생길 텐데, 그때마다 매번 이사하지 않아도 된다는 건 정말 엄청난 장점이에요.</p>
<blockquote>
<p><strong>핵심 포인트</strong> 
용량을 길이와 분리해서 기억해 두면, 크기가 변할 때마다 매번 무리해서 재할당하는 것을 막을 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="벡터에서-몇-번째-데이터를-꺼낼지인덱싱는-용량이-아니라-길이-기준입니다">벡터에서 몇 번째 데이터를 꺼낼지(인덱싱)는 용량이 아니라 &#39;길이&#39; 기준입니다</h3>
<p>여기서 초보자들이 흔히 하는 실수가 있어요! <code>[]</code> 기호나 <code>at()</code> 함수를 써서 벡터 안의 n번째 방에 들어갈 때, 쓸 수 있는 방 번호는 용량이 아니라 <strong>&#39;길이&#39;</strong> 까지만 허용됩니다.</p>
<p>위의 예시처럼 용량이 5이고 길이가 3인 상황을 생각해 보세요. 0번, 1번, 2번 방까지만 정상적으로 들어갈 수 있습니다. 용량이 5라고 해서 아직 사용하지도 않는 3번, 4번 방에 들어가려고 하면, 범위를 벗어났다는 에러(Out of bounds)가 나게 됩니다.</p>
<blockquote>
<p><strong>경고</strong> 
데이터에 접근할 때 쓰는 방 번호(인덱스)는 반드시 0부터 벡터의 <strong>&#39;길이&#39;</strong> (용량 아님!) 사이여야 합니다!</p>
</blockquote>
<hr>
<h3 id="stdvector-의-불필요한-용량-줄이기"><code>std::vector</code> 의 불필요한 용량 줄이기</h3>
<p>벡터의 크기를 늘리면 필요에 따라 용량도 같이 늘어난다고 했죠. 하지만 반대로 길이를 줄인다고 해서 확보해 둔 용량이 알아서 줄어들지는 않습니다.</p>
<p>데이터 몇 개 안 쓸 거 같아서 빈방을 굳이 줄이겠다고 힘들게 재할당(이사)을 하는 건 사실 별로 좋은 선택이 아니에요. 하지만 만약 요소가 100,000개 들어있던 초대형 벡터에서 99,990개를 지워버리고 10개만 남겼다면? 안 쓰는 빈방이 99,990개나 되니 메모리 낭비가 너무 심하겠죠!</p>
<p>이럴 때 메모리 낭비를 해결하기 위해 <code>std::vector</code> 에는 <code>shrink_to_fit()</code> (내 몸에 딱 맞게 줄여줘!) 이라는 기능이 있습니다. &quot;현재 사용 중인 길이에 맞춰서 안 쓰는 빈 용량은 좀 버려줘&quot;라고 부탁하는 기능이에요. 다만 이건 말 그대로 &#39;부탁&#39;일 뿐 강제성은 없어서, 컴퓨터(컴파일러)가 상황을 보고 굳이 안 해도 되겠다 싶으면 무시할 수도 있습니다. 하지만 보통은 부탁을 잘 들어줍니다.</p>
<p>예시를 볼까요:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void printCapLen(const std::vector&lt;int&gt;&amp; v){
    std::cout &lt;&lt; &quot;Capacity: &quot; &lt;&lt; v.capacity() &lt;&lt; &quot; Length:&quot;    &lt;&lt; v.size() &lt;&lt; &#39;\n&#39;;
}

int main(){

    std::vector&lt;int&gt; v(1000); // 1000개의 요소가 들어갈 공간 할당하기
    printCapLen(v);

    v.resize(0); // 0개의 요소로 크기 줄이기
    printCapLen(v);

    v.shrink_to_fit();
    printCapLen(v);

    return 0;
}
</code></pre>
<p>결과는 이렇게 나옵니다:</p>
<blockquote>
<p>Capacity: 1000  Length: 1000
Capacity: 1000  Length: 0
Capacity: 0  Length: 0</p>
</blockquote>
<p>보시다시피, 길이를 0으로 줄였을 때는 용량이 그대로 1000이었지만, <code>shrink_to_fit()</code> 을 부르자 벡터가 불필요한 용량을 0으로 싹 줄여서 1000개짜리 빈 공간을 시스템에 예쁘게 돌려준 것을 확인할 수 있습니다!</p>
<hr>
<h2 id="1611--stdvector-와-스택stack의-동작-방식">16.11 — std::vector 와 스택(stack)의 동작 방식</h2>
<p>여러분이 사용자가 여러 개의 값(예: 시험 점수들)을 입력하는 프로그램을 만든다고 상상해 보세요. 이때 사용자가 몇 개의 점수를 입력할지는 프로그램을 실행하기 전(컴파일 타임)에는 알 수 없고, 실행할 때마다 달라질 수 있습니다. 우리는 이 값들을 화면에 보여주거나 처리하기 위해 <strong>std::vector</strong> 에 저장할 거예요.</p>
<p>지금까지 배운 내용을 바탕으로 몇 가지 방법을 생각해 볼 수 있어요.</p>
<ol>
<li>첫 번째, 사용자에게 &quot;몇 개의 점수를 입력할 건가요?&quot;라고 물어보고, 그 개수만큼 벡터를 만든 다음 점수를 입력받는 거예요. 나쁜 방법은 아니지만, 사용자가 미리 정확한 개수를 알아야 하고 실수로 개수를 잘못 세면 안 된다는 단점이 있어요. 10개나 20개가 넘는 항목을 일일이 세는 건 정말 귀찮은 일이죠. 게다가 컴퓨터가 대신 개수를 세어줄 수 있는데, 굳이 사용자에게 세라고 할 필요가 있을까요?</li>
<li>두 번째, 사용자가 아무리 많아도 특정 개수(예: 30개) 이상은 입력하지 않을 거라고 가정하고, 그만큼 큰 벡터를 미리 만드는 거예요. 그리고 다 입력할 때까지(혹은 30개에 도달할 때까지) 계속 입력받는 거죠. 다 입력하고 나면, 실제로 입력한 개수만큼 벡터의 크기를 줄이면 됩니다.</li>
</ol>
<p>하지만 이 두 번째 방법의 단점은 사용자가 딱 30개까지만 입력할 수 있다는 거예요. 만약 30개보다 더 많이 입력하고 싶다면? 안타깝게도 방법이 없죠.</p>
<p>이 문제를 해결하기 위해 30개가 꽉 차면 벡터의 크기를 더 크게 늘리는 코드를 추가할 수도 있어요. 하지만 이렇게 되면 프로그램의 원래 목적(점수 처리)과 배열 크기 관리 코드가 섞이게 되어서, 프로그램이 훨씬 복잡해지고 버그가 생기기 쉬워집니다.</p>
<p>진짜 문제는 우리가 &#39;사용자가 몇 개를 입력할지&#39; 미리 짐작하려고 한다는 데 있어요. 입력할 개수를 미리 알 수 없는 상황에서는 훨씬 더 좋은 방법이 있답니다!</p>
<p>하지만 그 방법을 알아보기 전에, 잠깐 다른 이야기를 먼저 해볼게요.</p>
<hr>
<h3 id="스택stack이란-무엇일까요">스택(Stack)이란 무엇일까요?</h3>
<p>비유를 하나 들어볼게요! 식당에 쌓여 있는 접시 무더기를 생각해 보세요. 왠지 모르겠지만 이 접시들은 엄청 무거워서 한 번에 딱 하나씩만 들 수 있다고 가정해 봅시다.  접시가 무겁고 쌓여 있기 때문에, 이 접시 더미를 다루는 방법은 딱 두 가지뿐이에요.</p>
<ol>
<li>맨 위에 새 접시를 올려놓는다 (기존 접시는 아래에 깔리게 됨)</li>
<li>맨 위에 있는 접시를 빼낸다 (아래에 있던 접시가 드러남)</li>
</ol>
<p>중간이나 맨 밑에서 접시를 빼거나 넣는 건 절대 안 돼요. 그러려면 한 번에 여러 개의 접시를 들어야 하니까요.</p>
<p>이렇게 스택(접시 더미)에 물건을 넣고 빼는 순서를 <strong>후입선출 (LIFO: Last-In, First-Out)</strong> 이라고 불러요. 즉, 가장 마지막에 들어간 접시가 가장 먼저 나오게 되는 구조랍니다.</p>
<hr>
<h3 id="프로그래밍에서의-스택">프로그래밍에서의 스택</h3>
<p>프로그래밍에서 <strong>스택(Stack)</strong> 은 이 LIFO(후입선출) 방식으로 데이터를 넣고 빼는 보관함(컨테이너)을 말해요.  주로 <strong>push(푸시)</strong> 와 <strong>pop(팝)</strong> 이라는 두 가지 핵심 동작으로 작동합니다.</p>
<table>
<thead>
<tr>
<th>동작 이름</th>
<th>하는 일</th>
<th>필수 여부</th>
<th>참고 사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Push</strong> (푸시)</td>
<td>스택 맨 위에 새 데이터를 넣습니다</td>
<td>예</td>
<td></td>
</tr>
<tr>
<td><strong>Pop</strong> (팝)</td>
<td>스택 맨 위에서 데이터를 빼냅니다</td>
<td>예</td>
<td>빼낸 데이터를 반환하거나 아무것도 반환하지 않을 수 있음(void)</td>
</tr>
</tbody></table>
<p>그 외에도 스택에서 유용하게 쓰이는 동작들이 더 있어요.</p>
<table>
<thead>
<tr>
<th>동작 이름</th>
<th>하는 일</th>
<th>필수 여부</th>
<th>참고 사항</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Top</strong> 또는 <strong>Peek</strong></td>
<td>스택 맨 위에 있는 데이터를 확인만 합니다</td>
<td>선택 사항</td>
<td>데이터를 빼내지(지우지)는 않음</td>
</tr>
<tr>
<td><strong>Empty</strong></td>
<td>스택이 비어있는지 확인합니다</td>
<td>선택 사항</td>
<td></td>
</tr>
<tr>
<td><strong>Size</strong></td>
<td>스택에 데이터가 몇 개 있는지 셉니다</td>
<td>선택 사항</td>
<td></td>
</tr>
</tbody></table>
<p>스택은 프로그래밍에서 정말 흔하게 쓰여요. 이전에 &#39;호출 스택(call stack)&#39;에 대해 배운 적이 있죠? 어떤 함수들이 호출되었는지 기록해 두는 곳인데, 이름 그대로 이것도 스택이랍니다! (네, 비밀을 밝힌 것치곤 좀 싱겁죠 ㅎㅎ). 함수가 실행되면 그 함수에 대한 정보가 스택의 맨 위에 추가(Push)되고, 함수가 끝나면 맨 위에서 제거(Pop)돼요. 그래서 호출 스택의 맨 위는 항상 &#39;지금 당장 실행 중인 함수&#39;를 나타냅니다.</p>
<p>Push와 Pop이 어떻게 작동하는지 순서대로 볼까요?</p>
<blockquote>
<p>(스택: 비어있음)
Push 1 (스택: 1)
Push 2 (스택: 1 2)
Push 3 (스택: 1 2 3)
Pop (스택: 1 2)
Push 4 (스택: 1 2 4)
Pop (스택: 1 2)
Pop (스택: 1)
Pop (스택: 비어있음)</p>
</blockquote>
<hr>
<h3 id="c-에서의-스택">C++ 에서의 스택</h3>
<p>어떤 프로그래밍 언어들은 스택을 완전히 별개의 전용 보관함으로 만들어 둬요. 하지만 이러면 좀 불편할 때가 있어요. 예를 들어, 스택을 건드리지 않고 안에 있는 값들만 쭉 화면에 출력하고 싶을 때, 순수하게 스택 기능만 있으면 그렇게 하기가 어렵거든요.</p>
<p>그래서 C++ 에서는 스택처럼 쓸 수 있는 기능(멤버 함수)들을 이미 있는 보관함들( <strong>std::vector</strong> , <strong>std::deque</strong> , <strong>std::list</strong> 등)에 추가해 두었어요. 이 보관함들은 원래 끝부분에서 데이터를 빠르게 넣고 빼는 데 특화되어 있거든요. 덕분에 원래의 편리한 기능들을 그대로 쓰면서 스택처럼 활용할 수도 있답니다.</p>
<hr>
<h3 id="잠깐-참고로">잠깐 참고로...</h3>
<p>아까 들었던 접시 더미 비유도 좋지만, 배열(array)을 이용해 스택을 어떻게 만드는지 이해하기 위해 더 찰떡인 비유를 하나 들어볼게요.</p>
<p>위로 쭉 쌓아 올린 우편함들을 상상해 보세요.  각 우편함에는 편지를 딱 하나씩만 넣을 수 있고, 처음엔 다 비어있어요. 이 우편함들은 서로 단단히 못 박혀 있고, 맨 위쪽 우편함에는 독이 묻은 가시가 돋아 있어서 새로운 우편함을 중간이나 맨 위에 더 끼워 넣을 수는 없다고 쳐볼게요. (우편함의 전체 개수를 늘릴 수 없다는 뜻이에요!)</p>
<p>우편함 개수를 늘릴 수 없는데 어떻게 스택처럼 쓸 수 있을까요?</p>
<ol>
<li>먼저, 포스트잇 같은 마커(표시)를 사용해서 &#39;여기가 스택의 맨 위야!&#39;라고 표시해 둡니다. 이 표시는 항상 &#39;비어있는 우편함 중 가장 밑에 있는 곳&#39;을 가리켜요. 처음엔 다 비어있으니까 맨 아래 우편함에 표시를 붙이겠죠.</li>
<li>이제 데이터를 넣을 때(Push)는, 표시가 붙어있는 우편함에 데이터를 넣고 표시는 한 칸 위로 올려 붙여요.</li>
<li>반대로 데이터를 뺄 때(Pop)는, 표시를 한 칸 아래로 내린 다음, 그 우편함에 있던 데이터를 꺼내서 우편함을 다시 비워줍니다.</li>
</ol>
<p>즉, 표시보다 아래에 있는 것들은 &#39;스택에 들어있는 데이터&#39;이고, 표시와 그 위에 있는 우편함들은 &#39;비어있는 공간&#39;인 셈이죠.</p>
<p>자, 이제 여기서 &#39;표시&#39;를 <strong>length</strong> (데이터 개수, 길이)라고 부르고, 전체 &#39;우편함의 개수&#39;를 <strong>capacity</strong> (용량)라고 불러봅시다!</p>
<p>이번 레슨의 나머지 부분에서는 <strong>std::vector</strong> 의 스택 기능이 어떻게 작동하는지 알아보고, 마지막으로 처음에 말했던 &#39;점수 입력받기&#39; 문제를 이 방법으로 어떻게 멋지게 해결하는지 보여드릴게요.</p>
<hr>
<h3 id="stdvector-의-스택-동작">std::vector 의 스택 동작</h3>
<p><strong>std::vector</strong> 에서 스택 기능은 다음 함수들을 통해 사용할 수 있어요.</p>
<table>
<thead>
<tr>
<th>함수 이름</th>
<th>스택 동작</th>
<th>하는 일</th>
<th>참고 사항</th>
</tr>
</thead>
<tbody><tr>
<td><code>push_back()</code></td>
<td>Push</td>
<td>스택 맨 위에 새 데이터를 넣습니다</td>
<td>벡터의 맨 끝에 요소를 추가합니다</td>
</tr>
<tr>
<td><code>pop_back()</code></td>
<td>Pop</td>
<td>스택 맨 위에서 데이터를 빼냅니다</td>
<td>반환값은 없고(void), 벡터 맨 끝 요소를 지웁니다</td>
</tr>
<tr>
<td><code>back()</code></td>
<td>Top / Peek</td>
<td>스택 맨 위에 있는 데이터를 확인합니다</td>
<td>데이터를 지우지는 않습니다</td>
</tr>
<tr>
<td><code>emplace_back()</code></td>
<td>Push</td>
<td><code>push_back()</code>의 다른 형태인데 더 효율적일 때가 있습니다 (아래 참고)</td>
<td>벡터의 맨 끝에 요소를 추가합니다</td>
</tr>
</tbody></table>
<p>그럼 이 함수들을 사용하는 예제 코드를 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void printStack(const std::vector&lt;int&gt;&amp; stack)
{
    if (stack.empty()) // stack.size == 0 이라면
        std::cout &lt;&lt; &quot;비어있음&quot;;

    for (auto element : stack)
        std::cout &lt;&lt; element &lt;&lt; &#39; &#39;;

    // \t 는 텍스트 줄을 맞추기 위한 탭(tab) 문자입니다
    std::cout &lt;&lt; &quot;\t용량(Capacity): &quot; &lt;&lt; stack.capacity() &lt;&lt; &quot;  길이(Length) &quot; &lt;&lt; stack.size() &lt;&lt; &quot;\n&quot;;
}

int main()
{
    std::vector&lt;int&gt; stack{}; // 빈 스택 만들기

    printStack(stack);

    stack.push_back(1); // push_back() 은 스택에 요소를 밀어 넣습니다(Push)
    printStack(stack);

    stack.push_back(2);
    printStack(stack);

    stack.push_back(3);
    printStack(stack);

    std::cout &lt;&lt; &quot;맨 위(Top): &quot; &lt;&lt; stack.back() &lt;&lt; &#39;\n&#39;; // back() 은 마지막 요소를 반환합니다

    stack.pop_back(); // pop_back() 은 스택에서 요소를 빼냅니다(Pop)
    printStack(stack);

    stack.pop_back();
    printStack(stack);

    stack.pop_back();
    printStack(stack);

    return 0;
}
</code></pre>
<p>(GCC나 Clang 컴파일러에서는 이렇게 출력돼요)</p>
<blockquote>
<p>비어있음   용량(Capacity): 0  길이(Length): 0
1       용량(Capacity): 1  길이(Length): 1
1 2     용량(Capacity): 2  길이(Length): 2
1 2 3   용량(Capacity): 4  길이(Length): 3
맨 위(Top):3
1 2     용량(Capacity): 4  길이(Length): 2
1       용량(Capacity): 4  길이(Length): 1
비어있음   용량(Capacity): 4  길이(Length): 0</p>
</blockquote>
<p>기억나시나요? 여기서 길이(length)는 벡터 안에 있는 데이터의 개수, 즉 우리 스택에 쌓인 데이터의 개수를 의미해요.</p>
<p>대괄호 <code>operator[]</code> 나 <code>at()</code> 함수를 쓸 때와 다르게, <code>push_back()</code> 이나 <code>emplace_back()</code> 을 사용하면 벡터의 길이(length)가 자동으로 늘어나요. 만약 데이터를 넣을 공간(capacity, 용량)이 부족하면, 공간을 더 넓히는 재할당(reallocation) 작업이 자동으로 일어납니다.</p>
<p>위 예제에서도 공간이 3번이나 재할당되었죠 (용량이 0에서 1로, 1에서 2로, 2에서 4로 늘어났어요).</p>
<blockquote>
<p><strong>핵심 포인트</strong>
<code>push_back()</code> 과 <code>emplace_back()</code> 은 <strong>std::vector</strong> 의 길이를 늘려줍니다. 그리고 남은 용량이 부족하면 알아서 더 큰 공간으로 이사(재할당)를 갑니다.</p>
</blockquote>
<hr>
<h3 id="push할-때-넉넉하게-생기는-추가-용량">Push할 때 넉넉하게 생기는 추가 용량</h3>
<p>위의 출력 결과를 보면, 마지막으로 공간이 늘어날 때 요소를 1개만 더 넣었는데도 용량이 2에서 4로 껑충 뛰었죠? <code>push_back</code>으로 인해 공간이 늘어나야 할 때, <strong>std::vector</strong> 는 보통 앞으로 데이터가 더 들어올 것을 대비해서 공간을 좀 더 여유 있게 잡아둡니다. 그래야 다음에 데이터가 들어올 때 또 이사(재할당)를 가는 번거로움을 줄일 수 있거든요.</p>
<p>얼마나 여유 있게 잡을지는 여러분이 쓰는 컴파일러마다 조금씩 달라요.</p>
<ul>
<li><strong>GCC와 Clang</strong> 은 현재 용량을 2배로 늘려요. (2에서 4로 늘어났죠!)</li>
<li><strong>Visual Studio 2022</strong> 는 현재 용량의 1.5배로 늘려요. (2에서 3으로 늘어납니다.)</li>
</ul>
<p>그래서 어떤 컴파일러를 쓰느냐에 따라 출력 결과의 용량 부분이 조금 다르게 보일 수 있습니다.</p>
<hr>
<h3 id="스택처럼-쓸-때는-크기-조정resize을-조심하세요">스택처럼 쓸 때는 크기 조정(Resize)을 조심하세요</h3>
<p>벡터 공간을 다시 할당(재할당)하는 건 컴퓨터 입장에서 꽤 힘든 일(계산 비용이 높음)이에요. 그래서 가능하면 재할당이 안 일어나게 하는 게 좋겠죠? 방금 본 예제에서 처음부터 용량을 딱 3으로 맞춰놓고 시작했다면 3번이나 이사를 가는 일을 피할 수 있었을 거예요.</p>
<p>그럼 첫 번째 예제의 시작 부분을 이렇게 고쳐보면 어떨까요?</p>
<pre><code class="language-cpp">std::vector&lt;int&gt; stack(3); // 괄호를 써서 초기화하면 벡터의 용량을 3으로 설정합니다
</code></pre>
<p>이렇게 고치고 다시 실행하면 이런 결과가 나옵니다.</p>
<blockquote>
<p>0 0 0     용량(Capacity): 3  길이(Length): 3
0 0 0 1     용량(Capacity): 6  길이(Length): 4
0 0 0 1 2     용량(Capacity): 6  길이(Length): 5
0 0 0 1 2 3     용량(Capacity): 6  길이(Length): 6
맨 위(Top): 3
0 0 0 1 2     용량(Capacity): 6  길이(Length): 5
0 0 0 1     용량(Capacity): 6  길이(Length): 4
0 0 0     용량(Capacity): 6  길이(Length): 3</p>
</blockquote>
<p>앗, 뭔가 이상하죠? 스택 맨 밑에 <code>0</code>이 세 개나 깔려버렸어요! 이유는 이렇습니다. 괄호로 초기화해서 벡터 크기를 정해주거나 <code>resize()</code> 함수를 사용하면, 용량(Capacity)뿐만 아니라 길이(Length)까지 함께 3으로 설정되어 버려요. 즉, 우리가 원했던 대로 빈 상자 3개가 준비된 게 아니라, 이미 <code>0</code>이라는 값이 들어있는 꽉 찬 상자 3개가 생겨버린 거죠. 그래서 우리가 <code>push_back</code>으로 넣은 값들은 그 <code>0</code>들 위에 차곡차곡 쌓이게 된 거예요.</p>
<p><code>resize()</code> 함수를 써서 길이를 미리 늘려두는 건, 대괄호(<code>[ ]</code>)를 써서 특정 위치의 값을 콕 집어서 바꿀 때는 아주 좋아요. 하지만 스택처럼 맨 위에 계속 쌓아 올리려고 할 때는 이렇게 골칫거리가 됩니다.</p>
<p>우리가 진짜 원하는 건, 길이(Length)는 0으로 놔둔 채로 용량(Capacity, 여유 공간)만 늘려두는 기능이에요. 그래야 쓸데없이 0이 추가되지 않으면서 나중에 재할당되는 것도 막을 수 있으니까요.</p>
<hr>
<h3 id="reserve-함수로-용량만-늘리기">reserve() 함수로 용량만 늘리기</h3>
<p>이럴 때 쓰는 게 바로 <code>reserve()</code> 라는 함수예요! 이 함수는 현재 길이(데이터 개수)는 건드리지 않고, <strong>std::vector</strong> 의 전체 용량만 넉넉하게 잡아줍니다.</p>
<p>아까와 똑같은 코드에 <code>reserve()</code> 만 추가해서 용량을 설정해 볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

void printStack(const std::vector&lt;int&gt;&amp; stack)
{
    if (stack.empty()) // stack.size == 0 이라면
        std::cout &lt;&lt; &quot;비어있음&quot;;

    for (auto element : stack)
        std::cout &lt;&lt; element &lt;&lt; &#39; &#39;;

    // \t 는 텍스트 줄을 맞추기 위한 탭(tab) 문자입니다
    std::cout &lt;&lt; &quot;\t용량(Capacity): &quot; &lt;&lt; stack.capacity() &lt;&lt; &quot;  길이(Length) &quot; &lt;&lt; stack.size() &lt;&lt; &quot;\n&quot;;
}

int main()
{
    std::vector&lt;int&gt; stack{};

    printStack(stack);

    stack.reserve(6); // 6개의 요소가 들어갈 공간을 미리 확보합니다 (하지만 길이는 바꾸지 않아요)
    printStack(stack);

    stack.push_back(1);
    printStack(stack);

    stack.push_back(2);
    printStack(stack);

    stack.push_back(3);
    printStack(stack);

    std::cout &lt;&lt; &quot;맨 위(Top): &quot; &lt;&lt; stack.back() &lt;&lt; &#39;\n&#39;;

    stack.pop_back();
    printStack(stack);

    stack.pop_back();
    printStack(stack);

    stack.pop_back();
    printStack(stack);

    return 0;
}
</code></pre>
<p>실행 결과는 다음과 같습니다.</p>
<blockquote>
<p>비어있음   용량(Capacity): 0  길이(Length): 0
비어있음   용량(Capacity): 6  길이(Length): 0
1       용량(Capacity): 6  길이(Length): 1
1 2     용량(Capacity): 6  길이(Length): 2
1 2 3   용량(Capacity): 6  길이(Length): 3
맨 위(Top): 3
1 2     용량(Capacity): 6  길이(Length): 2
1       용량(Capacity): 6  길이(Length): 1
비어있음   용량(Capacity): 6  길이(Length): 0</p>
</blockquote>
<p>보이시나요? <code>reserve(6)</code> 을 불렀더니 용량은 6으로 늘어났지만 길이는 0으로 그대로 유지되었어요. 이미 여유 공간이 6이나 있으니까, 데이터를 넣을 때 더 이상 번거로운 재할당(이사)이 일어나지 않게 되었죠!</p>
<p><strong>핵심 포인트</strong></p>
<ul>
<li><code>resize()</code> 함수는 벡터의 <strong>길이</strong>를 바꿉니다 (필요하면 용량도 늘림).</li>
<li><code>reserve()</code> 함수는 벡터의 <strong>용량</strong>만 바꿉니다 (길이는 안 건드림).</li>
</ul>
<p><strong>꿀팁</strong>
<strong>std::vector</strong> 의 요소를 늘리고 싶을 때:</p>
<ul>
<li><code>[ ]</code> 를 써서 특정 위치(인덱스)에 접근할 계획이라면 <code>resize()</code> 를 쓰세요. 길이를 늘려줘야 오류가 안 나요.</li>
<li>스택처럼 <code>push_back()</code> 으로 맨 끝에 밀어 넣을 계획이라면 <code>reserve()</code> 를 쓰세요. 길이 변경 없이 여유 공간만 확보해 줍니다.</li>
</ul>
<hr>
<h3 id="push_back-vs-emplace_back">push_back() vs emplace_back()</h3>
<p><code>push_back()</code> 과 <code>emplace_back()</code> 둘 다 스택에 데이터를 넣는 역할을 해요. 이미 만들어져 있는 변수(객체)를 넣을 때는 둘이 똑같이 작동하니까, 좀 더 익숙한 <code>push_back()</code> 을 쓰는 게 좋습니다.</p>
<p>하지만, 스택에 넣기 위해서 일회용으로 쓰일 데이터(임시 객체)를 그 자리에서 바로 만들어서 넣을 때는 <code>emplace_back()</code> 이 훨씬 더 빠르고 효율적이에요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;
#include &lt;vector&gt;

class Foo
{
private:
    std::string m_a{};
    int m_b{};
public:
    Foo(std::string_view a, int b)
        : m_a { a }, m_b { b }
        {}

    explicit Foo(int b)
        : m_a {}, m_b { b }
        {};
};

int main()
{
    std::vector&lt;Foo&gt; stack{};

    // 이미 만들어진 객체가 있을 때는, push_back 과 emplace_back 의 효율이 비슷합니다
    Foo f{ &quot;a&quot;, 2 };
    stack.push_back(f);    // 이 방법을 더 권장합니다
    stack.emplace_back(f);

    // 밀어 넣기 위해 임시 객체를 새로 만들어야 할 때는, emplace_back 이 더 효율적입니다
    stack.push_back({ &quot;a&quot;, 2 }); // 임시 객체를 먼저 만든 다음, 벡터 안으로 복사해 넣습니다
    stack.emplace_back(&quot;a&quot;, 2);  // 값만 전달해서 벡터 안에서 객체를 직접 만들어 버립니다 (복사할 필요 없음)

    // push_back 은 &#39;명시적(explicit) 생성자&#39;를 무시할 수 없지만, emplace_back 은 사용할 수 있습니다
    stack.push_back({ 2 }); // 컴파일 에러: Foo(int) 생성자는 explicit 으로 설정되어 있습니다
    stack.emplace_back(2);  // 이건 가능합니다 (ok)

    return 0;
}
</code></pre>
<p>위 코드에서 <code>push_back({ &quot;a&quot;, 2 })</code> 를 쓰면, 컴퓨터는 몰래 임시 <code>Foo</code> 객체를 하나 만들고, 그걸 다시 벡터 안으로 &#39;복사&#39;하는 과정을 거쳐요. 문자가 길어지거나 복잡한 데이터라면 이 &#39;복사&#39; 과정에서 시간이 꽤 걸릴 수 있어요.</p>
<p>반면에 <code>emplace_back(&quot;a&quot;, 2)</code> 를 쓰면, 굳이 임시 객체를 밖에서 만들 필요 없이 재료(&quot;a&quot;와 2)만 쓱 넘겨줍니다. 그러면 벡터가 자기 집 안에서 그 재료를 가지고 직접 요리(객체 생성)를 해버리죠. 복사할 필요가 없으니 당연히 더 빠르겠죠! (이걸 완벽한 전달, perfect forwarding 이라고 불러요).</p>
<p>다만 주의할 점은, <code>push_back()</code> 은 좀 엄격해서 <code>explicit</code> 이라고 표시된 특별한 생성자는 알아서 쓰지 않지만, <code>emplace_back()</code> 은 그걸 가져다 써버릴 수 있어요. 그래서 자칫 의도치 않은 방식으로 값이 바뀌어 들어가는 실수를 할 수 있어서 약간 더 위험할 수 있습니다.
(참고로 C++20 이전 버전에서는 <code>emplace_back()</code> 이 일부 초기화 방식을 지원하지 않았습니다.)</p>
<p><strong>모범 사례 (Best Practice)</strong></p>
<ul>
<li>보관함에 넣기 위해 임시 객체를 새로 만들어야 하거나, <code>explicit</code> 생성자를 꼭 써야 할 때는 <code>emplace_back()</code> 을 사용하세요.</li>
<li>그 외 일반적인 경우에는 모두 <code>push_back()</code> 을 사용하는 것을 추천합니다.</li>
</ul>
<hr>
<h3 id="스택-기능으로-처음-문제-해결하기">스택 기능으로 처음 문제 해결하기</h3>
<p>자, 이제 맨 처음에 이야기했던 &#39;점수 입력받기&#39; 문제를 어떻게 해결해야 할지 감이 오시죠?
<strong>std::vector</strong> 에 데이터가 몇 개나 들어올지 미리 알 수 없다면, 스택 기능(<code>push_back</code>)을 써서 데이터를 차곡차곡 쌓아 넣는 게 정답입니다!</p>
<p>예제 코드를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;limits&gt;
#include &lt;vector&gt;

int main()
{
    std::vector&lt;int&gt; scoreList{};

    while (true)
    {
        std::cout &lt;&lt; &quot;점수를 입력하세요 (끝내려면 -1 입력): &quot;;
        int x{};
        std::cin &gt;&gt; x;

        if (!std::cin) // 잘못된 입력 처리 (예: 숫자가 아닌 문자 입력)
        {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits&lt;std::streamsize&gt;::max(), &#39;\n&#39;);
            continue; // 다시 입력받도록 루프 처음으로 돌아감
        }

        // 사용자가 다 입력했다면(-1), 반복문을 빠져나갑니다
        if (x == -1)
            break;

        // 사용자가 정상적인 점수를 입력했다면, 벡터에 밀어 넣습니다(push)
        scoreList.push_back(x);
    }

    std::cout &lt;&lt; &quot;입력하신 점수 목록: \n&quot;;

    for (const auto&amp; score : scoreList)
        std::cout &lt;&lt; score &lt;&lt; &#39; &#39;;

    return 0;
}
</code></pre>
<p>이 프로그램은 사용자에게 시험 점수를 계속 입력받아서 벡터에 차곡차곡 추가해요. 사용자가 점수 입력을 다 마치면(-1 입력), 벡터에 들어있는 모든 값을 쭉 보여줍니다.</p>
<p>놀랍지 않나요? 우리는 코드를 짜면서 점수가 몇 개인지 숫자를 세지도 않았고, 배열의 크기(길이)를 계산하거나 위치(인덱스)를 다룰 필요도 전혀 없었어요! 우리는 그저 &#39;점수를 받아서 저장한다&#39;는 핵심 논리에만 집중했고, 골치 아픈 저장 공간 관리는 전부 벡터가 알아서 처리해 주었답니다!</p>
<hr>
<h2 id="1612--stdvectorbool">16.12 — <code>std::vector&lt;bool&gt;</code></h2>
<p>이전 O.1 레슨(비트 플래그와 <code>std::bitset</code>을 통한 비트 조작)에서, 우리는 <code>std::bitset</code> 이 8개의 불리언(Boolean, 참/거짓) 값을 단 하나의 바이트(1 byte) 안에 꽉꽉 압축해 넣는 방법에 대해 배웠습니다. 그리고 이 비트들은 <code>std::bitset</code> 의 기능을 통해 쉽게 수정할 수 있었죠.</p>
<p>그런데 <code>std::vector</code> 에도 아주 흥미로운 비밀 무기가 하나 숨겨져 있습니다. 바로 <strong><code>std::vector&lt;bool&gt;</code></strong> 이라는 특별한 버전인데요! 이것도 마찬가지로 8개의 참/거짓 값을 1바이트에 압축해서 메모리 공간을 훨씬 효율적으로 사용할 수 있게 해줍니다.</p>
<blockquote>
<p><strong>고급 학습자를 위한 참고</strong>
템플릿 클래스가 특정 타입(여기서는 <code>bool</code> )에 대해 아예 다른 방식으로 동작하도록 만들어진 것을 <strong>클래스 템플릿 특수화(Class template specialization)</strong> 라고 부릅니다. 이 내용은 나중에 26.4 레슨에서 더 자세히 다룰 예정입니다.</p>
</blockquote>
<p>하지만 비트 조작을 위해 만들어진 전문 도구인 <code>std::bitset</code> 과는 다르게, <strong><code>std::vector&lt;bool&gt;</code></strong> 에는 비트를 직접 만지는 섬세한 기능(멤버 함수)들이 빠져있습니다.</p>
<hr>
<h3 id="stdvectorbool-사용해-보기"><code>std::vector&lt;bool&gt;</code> 사용해 보기</h3>
<p>대부분의 상황에서 <strong><code>std::vector&lt;bool&gt;</code></strong> 은(는) 평범한 벡터와 똑같이 작동합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;vector&gt;

int main()
{
    std::vector&lt;bool&gt; v { true, false, false, true, true };

    for (int i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    // 인덱스 4의 불리언 값을 false로 변경합니다.
    v[4] = false;

    for (int i : v)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>작성자의 64비트 컴퓨터에서 이 코드를 실행하면 다음과 같이 출력됩니다.</p>
<pre><code class="language-text">1 0 0 1 1
1 0 0 1 0
</code></pre>
<hr>
<h3 id="stdvectorbool-의-치명적인-단점들-주의사항"><code>std::vector&lt;bool&gt;</code> 의 치명적인 단점들 (주의사항)</h3>
<p>공간을 아껴준다는 장점이 있지만, <strong><code>std::vector&lt;bool&gt;</code></strong> 을(를) 사용할 때는 반드시 알아둬야 할 중요한 단점들이 있습니다.</p>
<ol>
<li><strong>기본 덩치가 큽니다 (High Overhead):</strong> 기본적으로 차지하는 메모리 크기가 꽤 큽니다 (작성자의 컴퓨터에서는 기본 크기가 무려 40바이트나 됩니다). 따라서 저장하려는 참/거짓 값의 개수가 아주 많지 않다면, 오히려 빈 공간(오버헤드) 때문에 메모리를 더 낭비하게 될 수 있습니다.</li>
<li><strong>들쭉날쭉한 성능:</strong> 성능이 어떤 환경(컴파일러 등)을 쓰느냐에 따라 크게 달라집니다. 어떤 환경에서는 엄청나게 빠르지만, 최적화가 엉망인 환경에서는 오히려 아주 느려질 수 있습니다.</li>
<li><strong>진짜 &#39;벡터&#39;가 아닙니다 (가장 중요!):</strong> 이름만 벡터일 뿐, C++에서 말하는 정식 &#39;컨테이너&#39;의 기준을 통과하지 못합니다! 메모리에 일렬로 예쁘게 늘어서 있지도 않고, 우리가 아는 진짜 <code>bool</code> 값을 저장하는 것도 아닙니다 (단순한 비트 조각들을 모아둔 것에 불과합니다).</li>
</ol>
<p>이름은 벡터라서 평소에는 벡터처럼 행동하지만, C++의 다른 표준 기능들과 완벽하게 어울리지 못합니다. 즉, 다른 타입에서는 멀쩡하게 작동하던 코드가 <strong><code>std::vector&lt;bool&gt;</code></strong> 에서는 에러를 뿜어낼 수 있다는 뜻입니다.</p>
<p>예를 들어, 아래 코드는 <code>T</code> 가 <code>bool</code> 이 아닌 다른 모든 타입일 때는 잘 작동합니다.</p>
<pre><code class="language-cpp">template&lt;typename T&gt;
void foo( std::vector&lt;T&gt;&amp; v )
{
    T&amp; first = v[0]; // 첫 번째 요소에 대한 참조를 가져옵니다.
    // first를 사용하여 무언가를 수행합니다.
}
</code></pre>
<hr>
<h3 id="결론-stdvectorbool-은-피하세요">결론: <code>std::vector&lt;bool&gt;</code> 은 피하세요!</h3>
<p>요즘 C++ 개발자들 사이의 공통된 의견은 <strong>&quot;웬만하면 std::vector<bool> 은 쓰지 말자!&quot;</strong> 입니다. 메모리를 조금 아껴서 얻는 이득보다, 이게 &#39;진짜 컨테이너&#39;가 아니기 때문에 생기는 골칫거리와 호환성 문제가 훨씬 크기 때문입니다.</p>
<p>안타깝게도 이 압축 버전의 <strong><code>std::vector&lt;bool&gt;</code></strong> 은(는) 기본적으로 켜져 있고, 이걸 끄고 진짜 <code>bool</code> 을 담는 평범한 벡터로 되돌릴 방법이 없습니다. 그래서 아예 C++ 표준에서 이 기능을 없애버리자는(deprecate) 목소리도 나오고 있으며, 나중에는 <code>std::dynamic_bitset</code> 같은 새로운 형태의 기능으로 교체하려는 작업이 진행 중입니다.</p>
<p><strong>초보자를 위한 상황별 추천 가이드</strong></p>
<p>그렇다면 대신 무엇을 써야 할까요? 다음과 같이 사용하는 것을 추천합니다.</p>
<ul>
<li><strong><code>std::bitset</code> 사용하기:</strong> 필요한 비트의 개수를 코드를 짤 때 이미 알고 있고, 저장할 데이터가 엄청나게 많지 않을 때 (예: 64k 이하) 아주 좋습니다.</li>
<li><strong><code>std::vector&lt;char&gt;</code> 사용하기:</strong> 크기를 자유롭게 늘렸다 줄였다 해야 하는 참/거짓 목록이 필요하고, 메모리 공간을 한계까지 쥐어짜듯 아낄 필요가 없다면 이것을 강력히 추천합니다! 완벽하게 일반 벡터처럼 작동해서 에러 걱정이 없습니다.</li>
<li><strong>외부 라이브러리 사용하기:</strong> 진짜 비트 조작이 가능한, 크기가 늘어나는 비트셋이 필요하다면 <code>boost::dynamic_bitset</code> 같은 믿을 만한 외부 도구를 쓰세요. 이런 도구들은 가짜 표준 컨테이너인 척하지 않아서 안전합니다.</li>
</ul>
<blockquote>
<p><strong>가장 좋은 습관 (Best practice)</strong>
<strong><code>std::vector&lt;bool&gt;</code></strong> 보다는 <strong><code>constexpr std::bitset</code></strong> 이나 <strong><code>std::vector&lt;char&gt;</code></strong> , 또는 외부 라이브러리의 동적 비트셋을 우선적으로 사용하세요!</p>
</blockquote>
<hr>
<h2 id="16x--16장-요약">16.x — 16장 요약</h2>
<p><strong>격려의 한마디</strong>
이번 장은 결코 쉽지 않았을 거예요! 우리는 정말 많은 내용을 다루었고, C++의 까다로운 부분들도 파헤쳐 보았습니다. 포기하지 않고 끝까지 오신 것을 축하드려요!
<strong>배열</strong> 은 여러분의 C++ 프로그램이 엄청난 능력을 발휘할 수 있게 해주는 핵심 열쇠랍니다.</p>
<hr>
<h3 id="컨테이너와-배열의-기초">컨테이너와 배열의 기초</h3>
<ul>
<li><strong>컨테이너 (Container)</strong> 는 여러 개의 데이터(이를 <strong>요소</strong> 라고 불러요)를 담아두는 &#39;보관함&#39; 같은 데이터 타입입니다. 서로 관련 있는 값들을 한꺼번에 다루고 싶을 때 주로 사용하죠.</li>
<li>이 보관함 안에 들어있는 데이터의 개수를 <strong>길이 (Length)</strong> 또는 <strong>크기 (Size)</strong> 라고 부릅니다. C++를 포함한 대부분의 언어에서 컨테이너는 <strong>동종 (Homogenous)</strong> 인데, 이는 보관함 하나에는 한 가지 종류의 데이터(예: 정수면 정수만, 문자면 문자만)만 담을 수 있다는 뜻이에요.</li>
<li>C++에는 여러 가지 유용한 보관함들을 미리 만들어둔 <strong>컨테이너 라이브러리</strong> 가 기본적으로 제공됩니다.</li>
<li><strong>배열 (Array)</strong> 은 데이터들을 메모리상에 중간에 빈틈없이 <strong>연속적으로 (Contiguously)</strong> 나란히 붙여서 저장하는 보관함입니다. 데이터가 나란히 있기 때문에 어떤 위치에 있든 아주 빠르고 직접적으로 찾아갈 수 있어요.</li>
<li>C++에는 세 가지 주요 배열이 있습니다: 기본 C 스타일 배열, <code>std::vector</code>, 그리고 <code>std::array</code> 입니다.</li>
<li>이 중 <strong>std::vector</strong> 는 C++에서 가장 많이 쓰이는 배열 보관함입니다. <code>&lt;vector&gt;</code> 라는 헤더 파일을 불러와서 사용하며, 꺾쇠 <code>&lt; &gt;</code> 안에 어떤 타입의 데이터를 담을지 적어줍니다. 예를 들어 <code>std::vector&lt;int&gt;</code> 는 &#39;정수를 담는 벡터 보관함&#39;을 만들겠다는 뜻이에요.</li>
<li>보관함을 처음 만들 때 중괄호 <code>{ }</code>를 사용해서 안에 들어갈 값들을 바로 채워 넣을 수 있는데, 이를 <strong>리스트 생성자</strong> 를 사용한 초기화라고 합니다.</li>
</ul>
<hr>
<h3 id="데이터-꺼내보기-인덱스와-접근">데이터 꺼내보기 (인덱스와 접근)</h3>
<ul>
<li>배열 안의 특정 값을 꺼낼 때는 배열 이름 옆에 대괄호 <code>[]</code>를 쓰고, 그 안에 몇 번째 값을 꺼낼지 번호를 적습니다. 이 번호를 <strong>인덱스 (Index)</strong> 라고 해요.</li>
<li>주의할 점은 C++에서는 첫 번째 값이 1번이 아니라 0번부터 시작한다는 거예요! (이를 <strong>0 기반 (Zero-based)</strong> 시스템이라고 합니다). 즉, 첫 번째 값은 0번, 두 번째 값은 1번이 됩니다.</li>
<li>대괄호 <code>[]</code>는 우리가 실수로 배열의 크기를 벗어난 엉뚱한 번호를 입력해도 경고해주지 않습니다. (이를 <strong>경계 검사 (Bounds checking)</strong> 를 하지 않는다고 해요). 범위를 벗어난 번호를 찾으려 하면 프로그램이 알 수 없는 오류를 일으킬 수 있으니 조심해야 합니다!</li>
<li>배열은 보관함 안에 몇 개의 데이터가 있든 상관없이 모든 데이터에 똑같이 빠른 속도로 접근할 수 있는데, 이를 <strong>임의 접근 (Random access)</strong> 이라고 부릅니다.</li>
</ul>
<p><strong>리스트 초기화 주의사항</strong></p>
<pre><code class="language-cpp">std::vector v1 { 5 }; // 값 &#39;5&#39;가 들어있는 1개의 요소를 가진 벡터를 정의합니다.
std::vector v2 ( 5 ); // 기본값으로 초기화된 5개의 요소를 가진 벡터를 정의합니다.
</code></pre>
<ul>
<li><code>std::vector</code> 는 내용을 바꿀 수 없는 상수(<code>const</code>)로 만들 수는 있지만, 컴파일 타임 상수(<code>constexpr</code>)로는 만들 수 없습니다.</li>
</ul>
<hr>
<h3 id="크기-측정과-안전한-사용">크기 측정과 안전한 사용</h3>
<ul>
<li>컨테이너의 크기를 나타낼 때는 일반적인 정수형 대신 <strong>size_type</strong> 이라는 특수한 양수 전용 타입을 사용합니다. (주로 <code>std::size_t</code>라는 타입을 의미해요). 사용할 때는 <code>std::vector&lt;int&gt;::size_type</code> 처럼 전체 이름을 길게 적어줘야 합니다.</li>
<li>보관함에 데이터가 몇 개 들어있는지 알고 싶다면 <code>size()</code> 함수를 쓰면 됩니다. C++20부터는 <code>std::ssize()</code> 라는 함수도 추가되었는데, 이 함수는 크기를 일반적인 정수형(음수도 표현 가능한 타입)으로 알려줘서 수학 계산을 할 때 훨씬 편합니다.</li>
<li>대괄호 <code>[]</code> 대신 <code>at()</code> 이라는 함수를 사용해서 데이터를 꺼낼 수도 있습니다. 이 함수는 프로그램이 실행될 때 번호가 범위를 벗어나지 않았는지 안전하게 검사해 줍니다. 범위를 벗어나면 에러(예외)를 발생시키고 프로그램이 안전하게 멈추도록 도와주죠.</li>
</ul>
<hr>
<h3 id="똑똑하게-데이터-전달하기-복사와-이동">똑똑하게 데이터 전달하기 (복사와 이동)</h3>
<ul>
<li><code>std::vector</code> 도 함수에 전달할 수 있습니다. 하지만 그냥 전달하면 보관함 전체를 통째로 복사하게 되어 컴퓨터가 무척 힘들어합니다. 그래서 불필요한 복사를 막기 위해 보통 <strong>참조 (Reference)</strong> 라는 방식을 사용해서 원본의 &#39;위치&#39;만 알려주는 식으로 전달합니다.</li>
<li><strong>복사 의미론 (Copy semantics)</strong> 은 데이터를 똑같이 하나 더 만들어내는 규칙을 말합니다.</li>
<li>반면, 데이터를 복사하는 대신 원래 있던 데이터를 새로운 곳으로 쓱 넘겨주는 것을 데이터가 <strong>이동 (Moved)</strong> 되었다고 합니다.</li>
<li><strong>이동 의미론 (Move semantics)</strong> 은 이런 이동 규칙을 말해요. 무겁고 큰 데이터를 다룰 때, 일일이 복사하는 대신 소유권만 옮겨주면 프로그램이 훨씬 빠르고 효율적으로 동작합니다! <code>std::vector</code> 나 <code>std::string</code> 같은 타입은 함수에서 반환될 때 똑똑하게도 이 &#39;이동&#39; 방식을 사용합니다.</li>
</ul>
<hr>
<h3 id="배열-안의-데이터-훑어보기-순회">배열 안의 데이터 훑어보기 (순회)</h3>
<ul>
<li>보관함 안의 모든 데이터를 처음부터 끝까지 차례대로 살펴보는 것을 <strong>순회 (Traversal)</strong> 하거나 <strong>반복 (Iterating)</strong> 한다고 합니다.</li>
<li>보통 반복문(for 루프 등)을 써서 인덱스 번호를 하나씩 늘려가며 확인하는데, 이때 횟수를 한 번 더 돌거나 덜 도는 실수를 조심해야 합니다.</li>
<li>그래서 인덱스 번호 없이도 아주 쉽게 모든 데이터를 꺼내볼 수 있는 <strong>범위 기반 for 루프 (Range-based for loop)</strong> 를 사용하는 것이 훨씬 편하고 좋습니다! (초보자분들께 강력히 추천해요).</li>
<li>이 루프를 쓸 때는 타입 추론(<code>auto</code>) 기능을 활용하세요. 값을 바꿔야 하는 게 아니라면 항상 <code>const auto&amp;</code> 를 사용하는 습관을 들이는 것이 좋습니다. 이렇게 하면 불필요한 데이터 복사를 막아줍니다.</li>
</ul>
<hr>
<h3 id="동적-배열과-마법의-공간-조절">동적 배열과 마법의 공간 조절</h3>
<ul>
<li>처음에 크기를 정하면 바꿀 수 없는 배열을 <strong>고정 크기 배열 (Fixed-size array)</strong> 이라고 합니다. 반면, 프로그램 실행 중에도 마음대로 크기를 늘렸다 줄였다 할 수 있는 배열을 <strong>동적 배열 (Dynamic array)</strong> 이라고 해요. <code>std::vector</code> 가 바로 이 동적 배열이라서 엄청난 인기를 누리는 것이죠! <code>resize()</code> 함수를 부르면 언제든 원하는 길이로 바꿀 수 있습니다.</li>
<li>여기서 아주 중요한 개념 두 가지가 있습니다:</li>
<li><strong>용량 (Capacity)</strong> : 보관함이 꽉 차기 전까지 총 몇 개의 데이터를 담을 수 있는지 (미리 확보해 둔 빈 공간 포함).</li>
<li><strong>길이 (Length)</strong> : 현재 보관함에 &#39;실제로&#39; 들어있는 데이터의 개수.</li>
</ul>
<ul>
<li>만약 빈 공간이 없는데 데이터를 더 넣으려고 하면? <code>std::vector</code> 는 스스로 더 큰 새 보관함을 구해와서 기존 데이터들을 모두 새 보관함으로 이사시킵니다. 이를 <strong>재할당 (Reallocation)</strong> 이라고 해요. 이사는 힘든 작업이니 가급적 적게 일어나는 게 좋겠죠?</li>
<li><code>shrink_to_fit()</code> 함수를 쓰면, 쓰지 않고 남은 빈 공간을 없애서 보관함 크기를 실제 내용물에 딱 맞게 줄여달라고 요청할 수 있습니다.</li>
</ul>
<hr>
<h3 id="스택stack처럼-사용하기">스택(Stack)처럼 사용하기</h3>
<ul>
<li>식당에 쌓여있는 접시 더미를 상상해 보세요. 가장 마지막에 올려놓은 접시를 가장 먼저 꺼내 쓰게 되죠? 이런 방식을 <strong>후입선출 (LIFO, Last-in, First-out)</strong> 이라고 합니다. 프로그래밍에서 이렇게 작동하는 보관함을 <strong>스택 (Stack)</strong> 이라고 부릅니다. 스택에 데이터를 넣는 것을 <strong>push</strong> , 빼는 것을 <strong>pop</strong> 이라고 해요.</li>
<li><code>std::vector</code> 는 이 스택처럼 쓸 수 있습니다! 맨 끝에 데이터를 추가할 때 <code>push_back()</code> 이나 <code>emplace_back()</code> 함수를 쓰면 됩니다.</li>
<li>새로운 임시 객체를 만들어서 바로 넣을 때는 <code>emplace_back()</code> 이 조금 더 빠르고 똑똑하게 동작합니다. 그 외의 평범한 상황에서는 <code>push_back()</code> 을 쓰시면 됩니다.</li>
</ul>
<p><strong>공간 늘리기 팁:</strong></p>
<ul>
<li><code>resize()</code> 함수: 배열의 &#39;실제 길이&#39;를 늘릴 때 사용합니다. (데이터가 실제로 추가됨)</li>
<li><code>reserve()</code> 함수: 데이터는 놔두고 &#39;빈 공간(용량)&#39;만 미리 넉넉하게 확보해 둘 때 사용합니다. (이사를 자주 안 가도록 미리 큰 집을 구하는 것과 같아요).</li>
</ul>
<hr>
<h3 id="주의사항-이상한-녀석-stdvectorbool">주의사항: 이상한 녀석, std::vector<code>&lt;bool&gt;</code></h3>
<ul>
<li>참/거짓(<code>bool</code>) 값을 담기 위한 <code>std::vector&lt;bool&gt;</code> 이라는 특별한 녀석이 있습니다. 이 녀석은 메모리 공간을 아주 쪼잔하게 아끼려고 8개의 값을 1바이트 안에 억지로 구겨 넣는 꼼수를 부립니다.</li>
<li>문제는 이런 꼼수 때문에, 이 녀석은 진짜 배열도 아니고 C++의 컨테이너 표준 규칙도 따르지 않게 되어버렸다는 점입니다. 다른 정상적인 코드들과 섞여서 문제를 일으키기 딱 좋으니, <strong><code>std::vector&lt;bool&gt;</code></strong> 은 가급적 피하고 사용하지 않는 것이 속 편합니다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[The Cherno C++ - How C++ Works]]></title>
            <link>https://velog.io/@dj_trace25/The-Cherno-C-How-C-Works</link>
            <guid>https://velog.io/@dj_trace25/The-Cherno-C-How-C-Works</guid>
            <pubDate>Sun, 01 Mar 2026 23:06:46 GMT</pubDate>
            <description><![CDATA[<h1 id="c-동작-원리-학습-문서">C++ 동작 원리 학습 문서</h1>
<h2 id="개요">개요</h2>
<p>이 문서는 C++ 프로그램이 텍스트 형태의 소스 코드에서 실행 가능한 바이너리(Executable Binary) 파일로 변환되는 전체 과정을 설명합니다. C++의 빌드 과정은 크게 전처리, 컴파일, 링크의 단계로 나뉘며, 각 단계의 핵심 개념을 초보자도 이해할 수 있도록 명확하게 정리했습니다.</p>
<hr>
<h2 id="1-프로그램의-시작점-main-함수-0139">1. 프로그램의 시작점: main 함수 [<a href="http://www.youtube.com/watch?v=SfGuIVzE_Os&amp;t=99">01:39</a>]</h2>
<p>모든 C++ 프로그램은 <code>main</code> 함수를 가집니다. 이는 프로그램의 진입점(Entry Point)으로, 운영체제가 애플리케이션을 실행할 때 가장 먼저 코드를 읽고 실행하기 시작하는 부분입니다.</p>
<ul>
<li>컴퓨터는 <code>main</code> 함수 내부의 코드를 위에서부터 아래로 한 줄씩 순차적으로 실행합니다.</li>
<li><code>main</code> 함수의 반환 타입은 정수형(<code>int</code>)이지만, 프로그래머가 명시적으로 값을 반환(return)하지 않아도 컴파일러가 자동으로 <code>0</code>을 반환하는 것으로 처리합니다. 이는 프로그램의 정상 종료를 의미하는 특별한 예외 규칙입니다.</li>
</ul>
<h2 id="2-전처리기-pre-processor-0055">2. 전처리기 (Pre-processor) [<a href="http://www.youtube.com/watch?v=SfGuIVzE_Os&amp;t=55">00:55</a>]</h2>
<p>컴파일러가 소스 코드를 기계어로 번역하기 직전에 수행되는 단계입니다.</p>
<ul>
<li><code>#</code> 기호로 시작하는 구문(예: <code>#include</code>)을 전처리기 지시문이라고 부릅니다.</li>
<li>예를 들어 <code>#include &lt;iostream&gt;</code> 구문이 있다면, 전처리기는 <code>iostream</code>이라는 파일의 모든 내용을 찾아 복사한 뒤, 현재 파일의 해당 위치에 그대로 붙여넣습니다.</li>
</ul>
<h2 id="3-연산자operator와-함수-0254">3. 연산자(Operator)와 함수 [<a href="http://www.youtube.com/watch?v=SfGuIVzE_Os&amp;t=174">02:54</a>]</h2>
<p>C++ 문법을 처음 접할 때 혼란스러울 수 있는 부분 중 하나는 꺾쇠 기호(<code>&lt;&lt;</code>)의 사용입니다.</p>
<ul>
<li><code>std::cout &lt;&lt; &quot;Hello World&quot;</code>에서 사용되는 <code>&lt;&lt;</code> 기호는 단순한 기호가 아니라 오버로딩된 연산자(Overloaded Operator)입니다.</li>
<li>C++에서 연산자는 근본적으로 함수와 동일하게 동작합니다. 즉, 위의 코드는 문자열 데이터를 콘솔 화면으로 밀어넣어 출력하도록 정의된 하나의 함수를 호출하는 것과 같습니다.</li>
</ul>
<h2 id="4-컴파일-compilation-과정-0820">4. 컴파일 (Compilation) 과정 [<a href="http://www.youtube.com/watch?v=SfGuIVzE_Os&amp;t=500">08:20</a>]</h2>
<p>전처리기가 작업을 마친 후, 사람이 읽을 수 있는 C++ 코드를 컴퓨터가 이해할 수 있는 기계어(Machine Code)로 변환하는 과정입니다.</p>
<ul>
<li><strong>개별 컴파일</strong>: 프로젝트 내의 모든 <code>.cpp</code> 파일은 각각 독립적으로 컴파일됩니다. 이때 헤더 파일은 직접 컴파일되지 않으며, 전처리기를 통해 <code>.cpp</code> 파일에 포함된 상태로만 처리됩니다.</li>
<li><strong>오브젝트 파일 생성</strong>: 각 <code>.cpp</code> 파일은 컴파일을 거쳐 각각 독립된 오브젝트 파일(<code>.obj</code>)로 변환됩니다.</li>
</ul>
<h2 id="5-선언declaration과-정의definition-1516">5. 선언(Declaration)과 정의(Definition) [<a href="http://www.youtube.com/watch?v=SfGuIVzE_Os&amp;t=916">15:16</a>]</h2>
<p>코드를 여러 파일로 나누어 관리할 때 반드시 이해해야 하는 핵심 개념입니다.</p>
<ul>
<li><strong>선언 (Declaration)</strong>: 컴파일러에게 특정 이름의 함수나 변수가 존재함을 알려주는 역할만 합니다. 내부 로직(본문)은 작성하지 않으며, 이를 통해 다른 파일에 있는 함수를 호출할 때 컴파일 오류가 발생하지 않도록 컴파일러를 납득시킵니다.</li>
<li><strong>정의 (Definition)</strong>: 해당 함수가 실제로 어떤 동작을 수행하는지 그 내부 로직(본문)을 작성하는 구역입니다.</li>
</ul>
<h2 id="6-링커-linker의-역할-0853">6. 링커 (Linker)의 역할 [<a href="http://www.youtube.com/watch?v=SfGuIVzE_Os&amp;t=533">08:53</a>]</h2>
<p>컴파일러가 생성한 여러 개의 개별 오브젝트 파일(<code>.obj</code>)을 하나로 연결하여 최종적인 하나의 실행 파일(<code>.exe</code>)을 만드는 과정입니다.</p>
<ul>
<li>링커의 주된 목적은 분리된 파일들을 접착제처럼 연결(Wiring)하는 것입니다.</li>
<li>만약 소스 코드에서 특정 함수를 &#39;선언&#39;하여 사용했으나, 프로젝트 전체를 뒤져봐도 그 함수의 실제 &#39;정의&#39;를 찾을 수 없다면 링커는 연결에 실패합니다. 이때 발생하는 오류가 바로 &#39;확인되지 않은 외부 기호(Unresolved External Symbol)&#39; 오류입니다.</li>
</ul>
<h2 id="7-빌드-설정-build-configuration-0506">7. 빌드 설정 (Build Configuration) [<a href="http://www.youtube.com/watch?v=SfGuIVzE_Os&amp;t=306">05:06</a>]</h2>
<p>개발 환경(예: Visual Studio)에서는 코드를 컴파일하고 빌드하는 규칙을 설정할 수 있습니다.</p>
<ul>
<li><strong>Debug 모드</strong>: 프로그램의 논리적 오류를 쉽게 추적할 수 있도록 최적화(Optimization) 기능이 비활성화됩니다. 실행 속도는 상대적으로 느리지만 개발 과정에서 반드시 필요합니다.</li>
<li><strong>Release 모드</strong>: 최종 사용자에게 배포하기 위한 설정으로, 실행 속도와 성능을 극대화하기 위해 코드를 최적화합니다.</li>
</ul>
<hr>
<h2 id="핵심-요약">핵심 요약</h2>
<p>위 본문에서 다룬 C++ 동작 원리의 중요한 핵심 개념은 다음과 같습니다.</p>
<ol>
<li><strong>main 함수</strong>: C++ 프로그램이 실행을 시작하는 최초의 진입점.</li>
<li><strong>전처리기</strong>: 실제 컴파일 이전에 <code>#</code> 지시문을 먼저 평가하여 필요한 파일의 내용을 복사 및 병합하는 작업 수행.</li>
<li><strong>컴파일러</strong>: 각각의 <code>.cpp</code> 파일을 독립적으로 기계어로 번역하여 개별적인 <code>.obj</code>(오브젝트) 파일을 생성하는 도구.</li>
<li><strong>선언과 정의</strong>: 선언은 대상의 존재 여부만 컴파일러에 알리는 것이고, 정의는 실제 동작 코드를 구현하는 것.</li>
<li><strong>링커</strong>: 컴파일된 여러 개의 <code>.obj</code> 파일을 하나로 결합하여 단일 <code>.exe</code>(실행) 파일을 생성하고, 선언과 정의가 일치하도록 연결하는 도구.</li>
</ol>
<p>[<a href="https://youtu.be/SfGuIVzE_Os?si=mo0ZYpj_gP-4trat">https://youtu.be/SfGuIVzE_Os?si=mo0ZYpj_gP-4trat</a>]</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[The Cherno C++ - Welcome to C++]]></title>
            <link>https://velog.io/@dj_trace25/The-Cherno-C-Welcome-to-C</link>
            <guid>https://velog.io/@dj_trace25/The-Cherno-C-Welcome-to-C</guid>
            <pubDate>Sun, 01 Mar 2026 09:24:58 GMT</pubDate>
            <description><![CDATA[<h1 id="c-입문-학습-자료-c의-개요와-특징">C++ 입문 학습 자료: C++의 개요와 특징</h1>
<p>본 문서는 C++ 프로그래밍 언어의 기본적인 목적과 동작 원리를 이해하기 위해 작성된 자가 학습용 기술 문서입니다. 아래 내용은 제공된 영상에 기반하여 작성되었습니다.</p>
<h2 id="1-c를-학습하는-이유-0050">1. C++를 학습하는 이유 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=50">00:50</a>]</h2>
<p>C++는 하드웨어에 대한 직접적인 제어권과 뛰어난 성능을 제공하는 프로그래밍 언어입니다. 이러한 특성 때문에 다음과 같은 분야에서 필수적으로 사용됩니다.</p>
<ul>
<li><strong>고성능 애플리케이션:</strong> 빠른 실행 속도와 최적화가 요구되는 소프트웨어 개발에 적합합니다.</li>
<li><strong>게임 개발:</strong> Unity, Unreal, Frostbite 등 업계 표준 게임 엔진은 대부분 C++로 작성되어 있습니다 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=65">01:05</a>].</li>
<li><strong>네이티브 실행 환경:</strong> 특정 하드웨어 아키텍처나 플랫폼에서 직접 구동되어야 하는 프로그램 개발에 쓰입니다.</li>
</ul>
<h2 id="2-c의-동작-원리-0128">2. C++의 동작 원리 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=88">01:28</a>]</h2>
<p>C++는 네이티브 언어로 분류되며, 다음과 같은 과정을 거쳐 실행됩니다.</p>
<ol>
<li><strong>소스 코드 작성:</strong> 개발자가 C++ 문법에 맞춰 코드를 작성합니다.</li>
<li><strong>컴파일:</strong> 컴파일러가 소스 코드를 타겟 플랫폼의 CPU가 직접 이해하고 실행할 수 있는 기계어로 변환합니다.</li>
<li><strong>실행:</strong> 변환된 기계어가 타겟 플랫폼의 CPU에서 즉각적으로 명령을 수행합니다 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=97">01:37</a>].</li>
</ol>
<p>이러한 특성 덕분에 특정 플랫폼용 컴파일러만 존재한다면 Windows, Mac, Linux 기반의 데스크톱은 물론 모바일(iOS, Android)과 콘솔 기기(PlayStation, Xbox, Nintendo 등) 등 광범위한 플랫폼을 지원할 수 있습니다 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=118">01:58</a>].</p>
<h2 id="3-c와-가상-머신vm-기반-언어의-차이-0242">3. C++와 가상 머신(VM) 기반 언어의 차이 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=162">02:42</a>]</h2>
<p>C++의 장점을 깊이 이해하기 위해서는 Java나 C#과 같은 가상 머신 기반 언어와의 차이점을 알아야 합니다.</p>
<ul>
<li><strong>가상 머신 기반 언어:</strong> 코드를 중간 언어로 우선 컴파일한 뒤, 프로그램 실행 시점에 가상 머신이 이를 기계어로 실시간 번역하여 실행합니다. 이는 외국어 책을 읽을 때 통역사가 옆에서 실시간으로 번역해 주는 것과 같습니다 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=179">02:59</a>].</li>
<li><strong>C++ (네이티브 언어):</strong> 타겟 플랫폼의 기계어로 완전히 번역된 상태의 결과물을 생성합니다. 이는 책 자체를 이미 해당 언어로 번역하여 출판한 것과 같아, 별도의 번역 과정 없이 즉시 실행될 수 있습니다 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=226">03:46</a>].</li>
</ul>
<h2 id="4-성능에-관한-주의-사항-0411">4. 성능에 관한 주의 사항 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=251">04:11</a>]</h2>
<p>C++가 기계어로 직접 컴파일된다고 해서 무조건 빠른 속도를 보장하는 것은 아닙니다. 비효율적으로 작성된 C++ 코드는 오히려 런타임 최적화를 지원하는 Java나 C# 코드보다 느리게 동작할 수 있습니다. 따라서 C++의 진정한 성능을 끌어내기 위해서는 메모리 관리, 포인터, 자료구조 등을 올바르게 이해하고 최적화된 코드를 작성하는 방법을 학습해야 합니다 [<a href="http://www.youtube.com/watch?v=18c3MTX0PK0&amp;t=269">04:29</a>].</p>
<hr>
<h2 id="핵심-요약">핵심 요약</h2>
<ul>
<li><strong>C++의 강점:</strong> 하드웨어에 대한 직접적인 제어와 뛰어난 성능을 제공하며, 고성능 소프트웨어 및 게임 엔진 개발의 핵심 언어입니다.</li>
<li><strong>네이티브 컴파일:</strong> C++ 코드는 타겟 기기의 CPU가 직접 이해하는 기계어로 즉시 변환되므로 런타임 번역 과정이 불필요합니다.</li>
<li><strong>플랫폼 독립성 확보:</strong> 적합한 컴파일러를 통해 데스크톱, 모바일, 콘솔을 아우르는 광범위한 환경에서 네이티브 코드를 실행할 수 있습니다.</li>
<li><strong>최적화의 중요성:</strong> C++의 이점을 극대화하려면 메모리 구조에 대한 이해를 바탕으로 올바르고 효율적인 코드를 작성하는 능력이 필요합니다.</li>
</ul>
<p><strong>참고 영상 URL:</strong> <a href="https://www.youtube.com/watch?v=18c3MTX0PK0">https://www.youtube.com/watch?v=18c3MTX0PK0</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 15]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-15</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-15</guid>
            <pubDate>Sun, 01 Mar 2026 08:11:01 GMT</pubDate>
            <description><![CDATA[<h2 id="151--숨겨진-this-포인터와-멤버-함수-체이닝">15.1 — 숨겨진 “this” 포인터와 멤버 함수 체이닝</h2>
<p>새내기 프로그래머들이 클래스에 대해 자주 묻는 질문 중 하나는 바로 이것입니다. </p>
<p>*&quot;멤버 함수를 실행할 때, C++은 대체 어떤 객체에서 그 함수를 실행해야 하는지 어떻게 기억하고 있는 걸까요?&quot;*</p>
<p>먼저 이 상황을 확인해 보기 위해 아주 간단한 클래스를 하나 만들어 볼게요. 
이 클래스는 숫자(정수) 하나를 품고 있고, 그 숫자를 가져오거나 바꾸는 간단한 함수들을 가지고 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Simple
{
private:
    int m_id{};

public:
    Simple(int id)
        : m_id{ id }
    {
    }

    int getID() const { return m_id; }
    void setID(int id) { m_id = id; }

    void print() const { std::cout &lt;&lt; m_id; }
};

int main()
{
    Simple simple{1};
    simple.setID(2);

    simple.print();

    return 0;
}
</code></pre>
<p>여러분이 예상하신 대로, 이 프로그램은 다음 결과를 보여줍니다.</p>
<pre><code class="language-text">2</code></pre>
<p>여기서 신기한 점이 있어요. 우리가 <code>simple.setID(2);</code> 를 불렀을 때, C++은 <code>setID()</code> 라는 함수가 <code>simple</code> 이라는 특정 객체를 위해 일해야 한다는 것을 알고 있습니다. 
게다가 코드 안에 있는 <code>m_id</code> 가 사실은 <code>simple.m_id</code> 를 가리킨다는 것도 귀신같이 알아채죠.</p>
<p>그 비밀은 바로, C++이 뒤에서 몰래 <strong>this</strong> 라는 이름의 &#39;숨겨진 포인터&#39;를 사용하고 있기 때문입니다! 이번 레슨에서는 이 <strong>this</strong> 에 대해 더 자세히 파헤쳐 볼게요.</p>
<hr>
<h3 id="숨겨진-this-포인터">숨겨진 <strong>this</strong> 포인터</h3>
<p>모든 멤버 함수 안에는 <strong>this</strong> 라는 키워드가 숨어 있습니다. 이건 <strong>&#39;현재 작업 중인 바로 그 객체&#39;</strong>의 주소가 적힌 지워지지 않는 명찰(const pointer)이라고 생각하시면 돼요.</p>
<p>보통 우리는 <strong>this</strong> 를 굳이 눈에 보이게 쓰지 않지만, 원한다면 직접 꺼내서 쓸 수도 있다는 걸 보여드릴게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Simple
{
private:
    int m_id{};

public:
    Simple(int id)
        : m_id{ id }
    {
    }

    int getID() const { return m_id; }
    void setID(int id) { m_id = id; }

    void print() const { std::cout &lt;&lt; this-&gt;m_id; } // `this` 포인터를 사용해 현재 객체를 가리키고, -&gt; 기호로 m_id 변수를 콕 집어냅니다
};

int main()
{
    Simple simple{ 1 };
    simple.setID(2);

    simple.print();

    return 0;
}
</code></pre>
<p>이 코드도 방금 전 예제와 완전히 똑같이 작동하고, <code>2</code>를 출력합니다.
여기서 이전 코드와 이번 코드의 <code>print()</code> 함수가 사실상 100% 똑같은 일을 한다는 걸 주목해 주세요.</p>
<pre><code class="language-cpp">void print() const { std::cout &lt;&lt; m_id; }       // 내가 안 써도 C++이 알아서 this를 써줌 (암시적)
void print() const { std::cout &lt;&lt; this-&gt;m_id; } // 내가 직접 this를 써줌 (명시적)
</code></pre>
<p>알고 보니 첫 번째 코드는 두 번째 코드를 쓰기 편하게 줄여 놓은 거였어요! 우리가 짠 코드를 컴퓨터가 알아들을 수 있게 번역(컴파일)할 때, 컴파일러는 객체의 멤버를 사용할 때마다 몰래 앞에 <code>this-&gt;</code> 를 붙여줍니다. 덕분에 우리가 매번 귀찮게 <code>this-&gt;</code> 를 반복해서 쓰지 않아도 되니까 코드가 훨씬 깔끔해지는 거죠.</p>
<blockquote>
<p><strong>잠깐 복습!</strong>
포인터(주소표)를 통해 객체 안의 내용물을 꺼낼 때는 <code>-&gt;</code> 라는 화살표 기호를 씁니다. <code>this-&gt;m_id</code> 는 <code>(*this).m_id</code> 와 완전히 똑같은 뜻이에요.</p>
</blockquote>
<hr>
<h3 id="this-는-어떻게-몰래-전달될까요"><strong>this</strong> 는 어떻게 몰래 전달될까요?</h3>
<p>아래의 함수 호출 코드를 돋보기로 들여다볼까요?
<code>simple.setID(2);</code></p>
<p><code>setID(2)</code> 함수를 부를 때 괄호 안에 재료(인자)가 &#39;2&#39; 하나만 있는 것 같죠? 하지만 사실은 두 개랍니다! 컴파일러가 코드를 번역할 때, 이 줄을 이렇게 슬쩍 바꿔버립니다.</p>
<p><code>Simple::setID(&amp;simple, 2); // 주목: 주인공이었던 simple 객체가 함수 괄호 안의 첫 번째 재료로 들어갔어요!</code></p>
<p>이제 이건 아주 평범한 함수 모양이 되었습니다. 그리고 함수 이름 앞에 있던 <code>simple</code> 객체는, 자신의 주소(<code>&amp;simple</code>) 형태로 함수의 첫 번째 재료가 되어 전달되죠.</p>
<p>하지만 이건 절반의 설명일 뿐이에요. 함수를 부르는 쪽에서 재료를 하나 더 던져주기로 했으니, 함수를 정의하는 쪽에서도 그 재료를 받을 바구니(매개변수)를 하나 더 만들어야겠죠? 원래 우리의 <code>setID()</code> 함수는 이랬습니다.</p>
<p><code>void setID(int id) { m_id = id; }</code></p>
<p>이 코드가 최종적으로 어떻게 변하는지는 컴퓨터 환경마다 조금 다를 수 있지만, 대략 이런 모습으로 바뀝니다.</p>
<p><code>static void setID(Simple* const this, int id) { this-&gt;m_id = id; }</code></p>
<p><code>setId</code> 함수 괄호 안 맨 왼쪽에 <strong>this</strong> 라는 새로운 바구니(매개변수)가 생겼네요! 이건 다른 곳을 가리키도록 바꿀 수는 없는 고정된 포인터(const pointer)입니다. 그리고 안쪽에 있던 <code>m_id</code> 도 방금 받은 <strong>this</strong> 를 사용해서 <code>this-&gt;m_id</code> 로 바뀌었죠.</p>
<p><em>(고급 독자를 위한 참고: 여기서 <code>static</code> 이란 뜻은, 이 함수가 특정 객체에 찰싹 달라붙어 있는 게 아니라, 클래스라는 동네 안에 있는 평범한 함수처럼 취급된다는 뜻입니다.)</em></p>
<h4 id="총정리-해볼까요">총정리 해볼까요?</h4>
<ol>
<li>우리가 <code>simple.setID(2)</code> 를 실행하면, 컴퓨터는 몰래 <code>Simple::setID(&amp;simple, 2)</code> 를 실행해서 <code>simple</code> 의 주소표를 함수에 넘겨줍니다.</li>
<li>이 함수 안에는 <strong>this</strong> 라는 숨겨진 바구니가 있어서, 방금 날아온 <code>simple</code> 의 주소표를 쏙 받아냅니다.</li>
<li>함수 안에서 쓰인 멤버 변수 앞에는 컴파일러가 알아서 <code>this-&gt;</code> 를 붙여줍니다. <strong>this</strong> 가 <code>simple</code> 을 가리키고 있으니, <code>this-&gt;m_id</code> 는 결국 <code>simple.m_id</code> 를 뜻하게 되는 거죠.</li>
</ol>
<p>다행인 점은 <strong>이 모든 복잡한 과정이 100% 자동으로 일어난다는 것</strong>입니다. 작동 원리를 다 못 외우셔도 프로그래밍하는 데 아무 지장 없어요! 딱 한 가지만 기억하세요.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
모든 일반 멤버 함수에는 현재 작업 중인 객체를 가리키는 <strong>this</strong> 라는 숨겨진 안내판(포인터)이 있습니다. <strong>this</strong> 는 항상 &#39;지금 일하고 있는 바로 그 녀석&#39;을 가리킵니다.</p>
</blockquote>
<hr>
<h3 id="this-가-직접-나설-때-명시적으로-쓰기"><strong>this</strong> 가 직접 나설 때 (명시적으로 쓰기)</h3>
<p>대부분의 경우 여러분이 직접 <strong>this</strong> 를 타자 칠 일은 없습니다. 
하지만 가끔 직접 써주면 아주 유용할 때가 있어요.</p>
<p>첫 번째로, 클래스의 변수 이름과 함수의 매개변수(들어오는 재료) 이름이 완전히 똑같을 때, <strong>this</strong> 를 써서 &quot;누가 누군지&quot; 확실히 구분해 줄 수 있습니다.</p>
<pre><code class="language-cpp">struct Something
{
    int data{}; // 이건 구조체(struct)라서 이름 앞에 m_ 을 안 붙였어요

    void setData(int data)
    {
        this-&gt;data = data; // this-&gt;data는 내 몸통 안에 있는 멤버 변수이고, 그냥 data는 밖에서 괄호 타고 들어온 재료입니다
    }
};
</code></pre>
<p>이 <code>Something</code> 안에는 <code>data</code> 라는 변수가 있어요. 그런데 <code>setData()</code> 함수가 받는 재료의 이름도 <code>data</code> 네요. 함수 안에서 그냥 <code>data</code> 라고 부르면, 컴퓨터는 내 몸통 안의 변수가 아니라 밖에서 들어온 재료 <code>data</code> 를 뜻하는 걸로 알아듣습니다.</p>
<p>그래서 &quot;아니, 내 몸통 안에 있는 원래 내 <code>data</code> 말이야!&quot; 라고 콕 집어 말해주기 위해 <code>this-&gt;data</code> 라고 쓰는 거죠. (물론 제일 좋은 방법은 처음부터 헷갈리지 않게 멤버 변수 이름 앞에 <code>m_</code> 을 붙이는 거랍니다!)</p>
<hr>
<h3 id="this-반환하기"><code>*this</code> 반환하기</h3>
<p>두 번째로, 멤버 함수가 자기 자신(현재 객체)을 통째로 뱉어내게(반환하게) 만들면 아주 멋진 일이 벌어집니다. 이렇게 하면 여러 개의 함수를 한 줄에 기차처럼 칙칙폭폭 이어서 쓸 수 있거든요! 이걸 바로 <strong>함수 체이닝(Function chaining)</strong> 또는 <strong>메서드 체이닝(Method chaining)</strong> 이라고 부릅니다.</p>
<p>우리가 화면에 글자를 찍을 때 쓰는 <code>std::cout</code> 을 볼까요?
<code>std::cout &lt;&lt; &quot;Hello, &quot; &lt;&lt; userName;</code></p>
<p>컴퓨터는 이걸 이렇게 괄호 쳐서 먼저 계산합니다.
<code>(std::cout &lt;&lt; &quot;Hello, &quot;) &lt;&lt; userName;</code></p>
<p>먼저 앞부분이 실행돼서 화면에 &quot;Hello, &quot;를 찍어요. 그런데 이 줄이 여기서 끝이 아니라 뒤에 계속 이어져야 하잖아요? 만약 앞부분을 실행하고 남는 결과가 아무것도 없다면(<code>void</code>), 엉뚱하게 이런 모양이 돼버릴 겁니다.</p>
<p><code>void{} &lt;&lt; userName; // 아무것도 없는 빈 공간에 userName을 밀어 넣으라니? (에러 발생!)</code></p>
<p>그래서 <code>operator&lt;&lt;</code> (화면에 글자 찍는 기능)는 자기 할 일을 다 하고 나면, 자기가 썼던 <code>std::cout</code> 객체를 다시 바깥으로 퉤! 하고 뱉어냅니다(반환합니다). 그럼 코드가 이렇게 바뀌죠.</p>
<p><code>(std::cout) &lt;&lt; userName;</code></p>
<p>덕분에 우리는 <code>std::cout</code> 을 맨 처음에 딱 한 번만 쓰고도 <code>&lt;&lt;</code> 기호를 이용해 원하는 만큼 글자를 계속 이어 붙일 수 있는 거예요.</p>
<p>이 마법 같은 기차놀이를 우리가 만든 클래스에도 똑같이 적용할 수 있습니다! 아래의 계산기 코드를 보세요.</p>
<pre><code class="language-cpp">class Calc
{
private:
    int m_value{};

public:
    void add(int value) { m_value += value; }
    void sub(int value) { m_value -= value; }
    void mult(int value) { m_value *= value; }

    int getValue() const { return m_value; }
};
</code></pre>
<p>여기서 5를 더하고, 3을 빼고, 4를 곱하고 싶다면 보통은 이렇게 답답하게 세 줄로 나눠서 적어야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    Calc calc{};
    calc.add(5); // 끝나고 아무것도 안 뱉음 (void)
    calc.sub(3); // 끝나고 아무것도 안 뱉음 (void)
    calc.mult(4); // 끝나고 아무것도 안 뱉음 (void)

    std::cout &lt;&lt; calc.getValue() &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>하지만 각 함수가 끝날 때 자기를 부른 객체(<code>*this</code>)를 되돌려주도록(반환하도록) 살짝만 고쳐볼까요?</p>
<pre><code class="language-cpp">class Calc
{
private:
    int m_value{};

public:
    Calc&amp; add(int value) { m_value += value; return *this; }
    Calc&amp; sub(int value) { m_value -= value; return *this; }
    Calc&amp; mult(int value) { m_value *= value; return *this; }

    int getValue() const { return m_value; }
};
</code></pre>
<p>각 함수 끝에 <code>return *this;</code> 가 추가된 게 보이시나요? 이제 우리는 이렇게 멋진 한 줄 코드를 쓸 수 있습니다!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    Calc calc{};
    calc.add(5).sub(3).mult(4); // 마법의 메서드 체이닝 (기차놀이!)

    std::cout &lt;&lt; calc.getValue() &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>세 줄이나 되던 코드가 한 줄로 예쁘게 줄었죠! 어떻게 이렇게 되는지 단계별로 볼까요?</p>
<ol>
<li>처음에 <code>calc.add(5)</code> 가 실행돼서 5를 더합니다. 그리고 자기를 부른 자기 자신(<code>calc</code>)을 그대로 뱉어냅니다.</li>
<li>그럼 바로 뒤에 붙어있던 <code>.sub(3)</code> 은 방금 뱉어진 <code>calc</code> 를 받아서 거기서 3을 뺍니다. 그리고 또 자기를 뱉어냅니다.</li>
<li>마지막으로 <code>.mult(4)</code> 가 받아서 4를 곱합니다.</li>
</ol>
<p>함수가 끝날 때마다 자기 자신을 다음 타자에게 넘겨주니까, 끊기지 않고 계속 이어서 명령을 내릴 수 있는 거예요. 결국 값은 <code>(((0 + 5) - 3) * 4)</code> 가 되어서 <code>8</code> 이 저장됩니다.</p>
<p><em>(<strong>this</strong> 는 항상 지금 일하고 있는 진짜 객체를 가리키고 있기 때문에, &quot;혹시 비어있으면(null) 어떡하지?&quot; 하는 걱정은 안 하셔도 된답니다!)</em></p>
<hr>
<h3 id="왜-this-는-참조가-아니라-포인터일까요">왜 <strong>this</strong> 는 참조(&amp;)가 아니라 포인터(*)일까요?</h3>
<p>설명을 듣다 보니, <strong>this</strong> 는 언제나 어떤 객체를 찰떡같이 가리키고 있잖아요? 그럼 굳이 복잡하게 포인터를 써서 화살표(<code>-&gt;</code>) 기호를 쓸 게 아니라, 더 깔끔한 참조(reference)를 쓰면 되지 않았나 싶으실 거예요.</p>
<p>정답은 아주 재미있습니다. 처음에 C++ 언어를 만들면서 <strong>this</strong> 라는 개념을 집어넣었을 그 옛날 당시에는... C++에 아직 &#39;참조(reference)&#39;라는 문법 자체가 존재하지 않았기 때문이에요!</p>
<p>만약 오늘날 C++이 새로 만들어졌다면, 틀림없이 <strong>this</strong> 를 포인터 대신 참조로 만들었을 겁니다. (실제로 Java나 C# 같은 최신 언어들은 <strong>this</strong> 를 참조 방식으로 사용하고 있어요.)</p>
<hr>
<h2 id="152--클래스와-헤더-파일">15.2 — 클래스와 헤더 파일</h2>
<p>지금까지 우리가 작성한 모든 클래스는 아주 간단해서, 클래스 안에서 직접 함수를 만들(구현할) 수 있었습니다. 예를 들어, 아래는 모든 함수가 클래스 안에 들어있는 간단한 <code>Date</code> 클래스입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Date
{
private:
    int m_year{};
    int m_month{};
    int m_day{};

public:
    Date(int year, int month, int day)
        : m_year { year }
        , m_month { month }
        , m_day { day}
    {
    }

    void print() const { std::cout &lt;&lt; &quot;Date(&quot; &lt;&lt; m_year &lt;&lt; &quot;, &quot; &lt;&lt; m_month &lt;&lt; &quot;, &quot; &lt;&lt; m_day &lt;&lt; &quot;)\n&quot;; }

    int getYear() const { return m_year; }
    int getMonth() const { return m_month; }
    int getDay() const { return m_day; }
};

int main()
{
    Date d { 2015, 10, 14 };
    d.print();

    return 0;
}
</code></pre>
<p>하지만 클래스가 점점 길어지고 복잡해지면, 모든 함수 내용을 클래스 안에 두는 것이 오히려 코드를 관리하고 작업하기 어렵게 만듭니다. 이미 만들어진 클래스를 사용할 때는 <strong>공개된 기능</strong> 이 무엇인지만 알면 되지, 그 내부가 어떻게 돌아가는지까지 전부 알 필요는 없거든요. 함수가 어떻게 작동하는지 적힌 복잡한 내용들이 섞여 있으면, 정작 이 클래스를 어떻게 써야 하는지 한눈에 파악하기 힘들어집니다.</p>
<p>이 문제를 해결하기 위해, C++에서는 클래스의 &quot;선언(껍데기)&quot; 부분과 &quot;구현(알맹이)&quot; 부분을 나눌 수 있게 해줍니다. 즉, 함수 내용을 클래스 밖으로 빼서 정의할 수 있는 것이죠.</p>
<p>아래는 위와 똑같은 <code>Date</code> 클래스이지만, 생성자와 <code>print()</code> 함수의 내용을 클래스 밖으로 빼낸 모습입니다. 클래스 안에는 &quot;이런 함수가 있을 거야&quot;라고 알려주는 선언만 남겨두고, 실제 작동하는 코드는 밖으로 이동시켰습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Date
{
private:
    int m_year{};
    int m_month{};
    int m_day{};

public:
    Date(int year, int month, int day); // 생성자 선언

    void print() const; // 출력 함수 선언

    int getYear() const { return m_year; }
    int getMonth() const { return m_month; }
    int getDay() const  { return m_day; }
};

Date::Date(int year, int month, int day) // 생성자 정의 (실제 구현)
    : m_year{ year }
    , m_month{ month }
    , m_day{ day }
{
}

void Date::print() const // 출력 함수 정의 (실제 구현)
{
    std::cout &lt;&lt; &quot;Date(&quot; &lt;&lt; m_year &lt;&lt; &quot;, &quot; &lt;&lt; m_month &lt;&lt; &quot;, &quot; &lt;&lt; m_day &lt;&lt; &quot;)\n&quot;;
};

int main()
{
    const Date d{ 2015, 10, 14 };
    d.print();

    return 0;
}
</code></pre>
<p>일반 함수를 만들 때처럼, 클래스에 속한 함수도 클래스 밖에서 만들 수 있습니다. 유일한 차이점은 함수 이름 앞에 이 함수가 어떤 클래스 소속인지 알려주기 위해 클래스 이름
(여기서는 <code>Date::</code>)을 붙여야 한다는 것입니다. 이렇게 해야 컴퓨터(컴파일러)가 &quot;아, 이건 그냥 함수가 아니라 Date 클래스의 함수구나!&quot;라고 이해할 수 있습니다.</p>
<p>참고로, 값을 읽어오기만 하는 짧은 함수(<code>getYear</code> 같은 접근자 함수)는 여전히 클래스 안에 남겨두었습니다. 이런 함수들은 보통 한 줄짜리라서 클래스 안에 둬도 복잡해 보이지 않거든요. 오히려 밖으로 빼면 쓸데없이 코드 줄 수만 길어집니다. 그래서 이렇게 단순한 한 줄짜리 함수들은 보통 클래스 안에 그냥 둡니다.</p>
<hr>
<h3 id="클래스-정의를-헤더-파일에-넣기">클래스 정의를 헤더 파일에 넣기</h3>
<p>소스 파일(<code>.cpp</code>) 안에 클래스를 만들면, 그 클래스는 오직 그 파일 안에서만 쓸 수 있습니다. 하지만 프로그램이 커지면 우리가 만든 클래스를 여러 파일에서 가져다 쓰고 싶어지겠죠?</p>
<p>이전 강의에서 함수 선언을 <strong>헤더 파일(.h)</strong> 에 넣을 수 있다고 배웠습니다. 그렇게 하면 여러 코드 파일에서 <code>#include</code>를 사용해 그 함수들을 불러올 수 있었죠. 클래스도 똑같습니다! 클래스의 껍데기(선언)를 헤더 파일에 넣고, 그 클래스를 쓰고 싶은 다른 파일들에서 <code>#include</code>로 불러오면 됩니다.</p>
<p>다만 함수와는 조금 다릅니다. 함수는 이름만 미리 알려줘도 쓸 수 있지만, 클래스는 컴퓨터가 그 크기가 얼마나 되는지, 안에 어떤 변수들이 있는지 정확히 알아야 메모리를 만들 수 있습니다. 그래서 헤더 파일에는 단순히 이름만 적는 게 아니라, 클래스의 전체적인 형태(정의)를 모두 적어주어야 합니다.</p>
<hr>
<h3 id="클래스-헤더와-코드-파일-이름-짓기">클래스 헤더와 코드 파일 이름 짓기</h3>
<p>가장 일반적인 방법은 클래스와 <strong>동일한 이름의 헤더 파일</strong> 을 만들고, 클래스 밖으로 빼낸 함수들은 <strong>동일한 이름의 .cpp 파일</strong> 에 넣는 것입니다.</p>
<p>우리 <code>Date</code> 클래스를 <code>.h</code> 파일과 <code>.cpp</code> 파일로 쪼개볼까요?</p>
<p><strong>Date.h:</strong></p>
<pre><code class="language-cpp">#ifndef DATE_H
#define DATE_H

class Date
{
private:
    int m_year{};
    int m_month{};
    int m_day{};

public:
    Date(int year, int month, int day);

    void print() const;

    int getYear() const { return m_year; }
    int getMonth() const { return m_month; }
    int getDay() const { return m_day; }
};

#endif
</code></pre>
<p><strong>Date.cpp:</strong></p>
<pre><code class="language-cpp">#include &quot;Date.h&quot;

Date::Date(int year, int month, int day) // 생성자 정의
    : m_year{ year }
    , m_month{ month }
    , m_day{ day }
{
}

void Date::print() const // 출력 함수 정의
{
    std::cout &lt;&lt; &quot;Date(&quot; &lt;&lt; m_year &lt;&lt; &quot;, &quot; &lt;&lt; m_month &lt;&lt; &quot;, &quot; &lt;&lt; m_day &lt;&lt; &quot;)\n&quot;;
};
</code></pre>
<p>이제 <code>Date</code> 클래스를 사용하고 싶은 다른 파일이 있다면, 그냥 <code>#include &quot;Date.h&quot;</code>만 적어주면 됩니다. 단, 프로그램이 제대로 합쳐지려면(링크 단계) <code>Date.cpp</code> 파일도 프로젝트에 포함되어 같이 번역(컴파일)되어야 한다는 점을 잊지 마세요!</p>
<blockquote>
<p><strong>모범 사례</strong></p>
<ul>
<li>클래스의 전체적인 모양(정의)은 클래스 이름과 같은 헤더 파일에 넣으세요. 
아주 단순한 함수들은 클래스 안에 그대로 둬도 됩니다.</li>
<li>복잡하고 긴 함수들은 클래스 이름과 같은 소스 파일(<code>.cpp</code>)에 따로 빼서 작성하세요.</li>
</ul>
</blockquote>
<hr>
<h3 id="헤더-파일에-클래스를-넣으면-규칙odr-위반-아닌가요">헤더 파일에 클래스를 넣으면 규칙(ODR) 위반 아닌가요?</h3>
<p>C++에는 &quot;모든 것은 프로그램 전체에서 딱 한 번만 정의되어야 한다&quot;는 <strong>단일 정의 규칙(ODR)</strong> 이 있습니다. 헤더 파일을 여러 곳에서 <code>#include</code> 하면 여러 번 정의되는 셈인데 괜찮을까요?</p>
<p>다행히도 클래스 같은 &#39;타입(Types)&#39;은 이 규칙에서 예외입니다! 그래서 여러 파일에서 클래스 정의를 불러와도 문제가 없습니다. 만약 이게 안 됐다면 클래스는 별로 쓸모가 없었을 거예요.
하지만 <em>같은 파일</em> 안에서 똑같은 클래스를 두 번 불러오는 것은 여전히 규칙 위반입니다. 이를 막기 위해 우리는 헤더 가드(<code>#ifndef</code> 같은 것)나 <code>#pragma once</code>를 사용합니다.</p>
<hr>
<h3 id="인라인-멤버-함수">인라인 멤버 함수</h3>
<p>클래스 자체는 예외지만, &#39;함수&#39;는 단일 정의 규칙(ODR)의 예외가 아닙니다. 그럼 헤더 파일에 함수가 들어있을 때 여러 번 불러오면 어떻게 오류를 피할 수 있을까요?</p>
<ul>
<li>클래스 <strong>안에</strong> 작성된 함수들은 자동으로 <code>inline</code> 처리가 됩니다. 인라인 함수는 단일 정의 규칙의 예외라서 여러 번 불러와도 괜찮습니다.</li>
<li>클래스 <strong>밖에</strong> 작성된 함수들은 자동으로 <code>inline</code> 처리가 되지 않습니다. 그래서 이런 함수들을 헤더 파일에 두면 여러 번 정의되었다고 에러가 납니다. 이것이 바로 긴 함수들을 <code>.cpp</code> 파일에 따로 모아두는 이유입니다. (<code>.cpp</code> 파일은 프로그램에서 한 번만 만들어지니까요.)</li>
</ul>
<p>만약 굳이 클래스 밖에 작성한 함수를 헤더 파일에 남겨두고 싶다면, 함수 앞에 명시적으로 <code>inline</code> 이라는 단어를 붙여주면 됩니다.</p>
<hr>
<h3 id="왜-모든-걸-헤더-파일에-넣지-않을까요">왜 모든 걸 헤더 파일에 넣지 않을까요?</h3>
<p>함수 내용까지 전부 헤더 파일에 넣으면 편할 것 같지만, 두 가지 큰 단점이 있습니다.</p>
<ol>
<li>앞서 말했듯, 클래스 모양이 너무 복잡해져서 읽기 힘들어집니다.</li>
<li>헤더 파일의 코드를 단 한 줄이라도 수정하면, 그 헤더를 가져다 쓴 <strong>모든 파일</strong> 을 다시 번역(컴파일)해야 합니다. 작은 프로젝트면 금방 끝나지만, 엄청나게 큰 상업용 프로그램이라면 코드를 한 줄 고치고 몇 시간씩 기다려야 할 수도 있습니다!</li>
</ol>
<p>반면에 <code>.cpp</code> 파일의 코드를 수정하면, 그 <code>.cpp</code> 파일 딱 하나만 다시 번역하면 됩니다. 따라서 복잡한 코드는 최대한 <code>.cpp</code> 파일에 넣는 것이 훨씬 좋습니다.</p>
<p>물론 예외도 있습니다.</p>
<ul>
<li>딱 한 파일에서만 쓸 아주 작은 클래스라면 그냥 <code>.cpp</code> 안에 다 몰아넣어도 됩니다.</li>
<li>복잡한 함수가 한두 개뿐이고 앞으로 수정할 일이 거의 없다면, 헤더 파일에 <code>inline</code>으로 넣는 게 편할 수 있습니다.</li>
<li>요즘 C++에서는 공유하기 쉽게 아예 모든 걸 헤더 파일에 때려 넣은 
&quot;헤더 전용(Header-only)&quot; 라이브러리를 만들기도 합니다.</li>
<li>나중에 배울 템플릿(Template) 클래스의 경우, 규칙상 거의 무조건 헤더 파일에 모든 걸 작성해야 합니다.</li>
</ul>
<hr>
<h3 id="멤버-함수의-기본-인수">멤버 함수의 기본 인수</h3>
<p>일반 함수의 경우 기본 인수(입력하지 않으면 자동으로 들어가는 값)는 주로 선언(헤더 파일) 쪽에 적으라고 배웠습니다. 클래스 함수의 경우는 훨씬 간단합니다. 
<strong>기본 인수는 무조건 클래스 안(선언부)에 적어주세요.</strong></p>
<blockquote>
<p><strong>모범 사례</strong>
클래스 함수의 기본 인수는 항상 클래스 정의 안에 넣으세요.</p>
</blockquote>
<hr>
<h3 id="라이브러리">라이브러리</h3>
<p>여러분은 이미 <code>std::string</code> 처럼 C++ 표준 라이브러리에 있는 클래스들을 써왔습니다. 이때 <code>#include &lt;string&gt;</code> 이라고 헤더 파일만 불러왔지, <code>string.cpp</code> 같은 코드 파일을 프로젝트에 추가한 적은 없으실 겁니다.</p>
<p>헤더 파일은 &quot;문법이 맞는지&quot; 컴퓨터가 확인하게 해주는 역할만 합니다. 진짜 복잡한 내부 코드는 어디 있을까요? 이미 번역이 다 끝난 파일 형태로 숨겨져 있다가, 프로그램이 완성될 때(링크 단계) 자동으로 찰싹 달라붙습니다. 그래서 여러분은 그 코드를 볼 수 없는 것이죠.</p>
<p>기업들이 라이브러리를 팔거나 배포할 때도 보통 <code>.h</code> 헤더 파일과 &#39;미리 컴파일된 파일&#39;만 줍니다. 매번 다시 번역하기엔 너무 오래 걸리고, 용량도 아낄 수 있으며, 무엇보다 자기들의 소중한 코드를 남이 훔쳐보지 못하게 하기 위해서입니다.</p>
<p>지금 당장 여러분이 이런 라이브러리를 만들 일은 없겠지만, 이렇게 헤더 파일(<code>.h</code>)과 소스 파일(<code>.cpp</code>)을 나누어 작성하는 버릇을 들이면 나중에 훌륭한 프로그래머로 성장하는 데 큰 도움이 될 것입니다.</p>
<hr>
<h2 id="153--중첩-타입-멤버-타입"><strong>15.3 — 중첩 타입 (멤버 타입)</strong></h2>
<p>다음의 짧은 프로그램을 한 번 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

enum class FruitType
{
    apple,
    banana,
    cherry
};

class Fruit
{
private:
    FruitType m_type { };
    int m_percentageEaten { 0 };

public:
    Fruit(FruitType type) :
        m_type { type }
    {
    }

    FruitType getType() { return m_type; }
    int getPercentageEaten() { return m_percentageEaten; }

    bool isCherry() { return m_type == FruitType::cherry; }
};

int main()
{
    Fruit apple { FruitType::apple };

    if (apple.getType() == FruitType::apple)
        std::cout &lt;&lt; &quot;I am an apple&quot;;
    else
        std::cout &lt;&lt; &quot;I am not an apple&quot;;

    return 0;
}
</code></pre>
<p>이 프로그램 자체에는 아무런 문제가 없습니다. 
하지만 <code>enum class FruitType</code> 은 원래 <code>Fruit</code> 클래스와 세트로 함께 쓰려고 만든 건데, 클래스 밖에 따로 덩그러니 떨어져 있다 보니 두 개가 어떻게 연결되어 있는지 우리가 직접 눈치껏 알아내야 하는 불편함이 있습니다.</p>
<hr>
<h3 id="중첩-타입-멤버-타입"><strong>중첩 타입 (멤버 타입)</strong></h3>
<p>지금까지 우리는 &#39;데이터 멤버(변수)&#39;와 &#39;멤버 함수&#39;, 이렇게 두 가지 종류를 가진 클래스를 보았습니다. 위의 <code>Fruit</code> 클래스도 이 두 가지를 모두 가지고 있죠.</p>
<p>그런데 클래스에는 또 다른 종류의 멤버가 들어갈 수 있습니다. 
바로 <strong>중첩 타입</strong> (또는 멤버 타입) 입니다! 중첩 타입을 만드는 방법은 아주 간단해요. 
그냥 클래스 안에서 원하는 접근 지정자(<code>public</code>, <code>private</code> 등) 아래에 타입을 새롭게 정의하면 됩니다.</p>
<p>위의 프로그램을 <code>Fruit</code> 클래스 안에 &#39;중첩 타입&#39;을 정의하는 방식으로 다시 써보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Fruit
{
public:
    // FruitType을 클래스 안으로 옮기고 public 접근 지정자 아래에 두었습니다.
    // 이름도 Type으로 짧게 바꾸고, enum class 대신 일반 enum으로 변경했습니다.
    enum Type
    {
        apple,
        banana,
        cherry
    };

private:
    Type m_type {};
    int m_percentageEaten { 0 };

public:
    Fruit(Type type) :
        m_type { type }
    {
    }

    Type getType() { return m_type;  }
    int getPercentageEaten() { return m_percentageEaten;  }

    // Fruit 클래스 내부에서는 더 이상 앞에 FruitType:: 을 붙일 필요가 없습니다.
    bool isCherry() { return m_type == cherry; } 
};

int main()
{
    // 참고: 클래스 밖에서는 이제 Fruit:: 이라는 소속을 붙여서 접근합니다.
    Fruit apple { Fruit::apple };

    if (apple.getType() == Fruit::apple)
        std::cout &lt;&lt; &quot;I am an apple&quot;;
    else
        std::cout &lt;&lt; &quot;I am not an apple&quot;;

    return 0;
}
</code></pre>
<p>여기서 짚고 넘어갈 만한 중요한 포인트가 몇 가지 있습니다.</p>
<p>첫째, <code>FruitType</code> 이 이제 클래스 안으로 쏙 들어왔고, 이름이 <code>Type</code> 으로 바뀌었습니다. (이름을 왜 바꿨는지는 곧 설명해 드릴게요!)</p>
<p>둘째, 중첩 타입인 <code>Type</code> 이 클래스의 맨 꼭대기에 정의되었습니다. 중첩 타입은 사용하기 전에 먼저 그 정체가 완전히 정의되어 있어야 하거든요. 그래서 보통 맨 처음에 적어줍니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
중첩 타입은 항상 클래스의 맨 위쪽에 정의하세요.</p>
</blockquote>
<p>셋째, 중첩 타입도 일반적인 접근 권한 규칙을 따릅니다. <code>Type</code> 은 <code>public</code> 아래에 정의되었기 때문에, 누구나 클래스 밖에서 이 타입과 그 안의 값들에 직접 접근할 수 있습니다.</p>
<p>넷째, 이름 공간(namespace)처럼 클래스도 그 안에 있는 이름들을 품어주는 &#39;범위(Scope)&#39; 역할을 합니다.</p>
<p>따라서 <code>Type</code> 의 진짜 풀네임은 <code>Fruit::Type</code> 이 되고, <code>apple</code> 의 풀네임은 <code>Fruit::apple</code> 이 됩니다.</p>
<p>클래스 &#39;내부&#39; 멤버들끼리는 굳이 저렇게 긴 풀네임을 쓰지 않아도 됩니다. 예를 들어 멤버 함수인 <code>isCherry()</code> 안에서는 <code>Fruit::</code> 이라는 소속을 붙이지 않고 그냥 <code>cherry</code> 라고만 써도 찰떡같이 알아듣습니다.</p>
<p>하지만 클래스 &#39;외부&#39;에서는 반드시 풀네임(예: <code>Fruit::apple</code>)을 써야 합니다. 우리가 이름을 <code>FruitType</code> 에서 <code>Type</code> 으로 바꾼 이유가 바로 이겁니다. 밖에서 부를 때 <code>Fruit::FruitType</code> 이라고 쓰면 이름이 쓸데없이 겹치니까, 깔끔하게 <code>Fruit::Type</code> 이라고 쓸 수 있게 만든 거죠!</p>
<p>마지막으로, 기존의 범위가 있는 열거형(<code>enum class</code>)을 일반 열거형(<code>enum</code>)으로 바꾸었습니다. 이제 <code>Fruit</code> 클래스 자체가 울타리(범위) 역할을 든든하게 해주기 때문에, 굳이 <code>enum class</code> 를 써서 이중으로 울타리를 칠 필요가 없어진 거죠. 일반 <code>enum</code> 으로 바꾸었기 때문에 밖에서 <code>Fruit::Type::apple</code> 처럼 길게 안 쓰고 <code>Fruit::apple</code> 처럼 짧고 편하게 쓸 수 있습니다.</p>
<hr>
<h3 id="중첩-typedef-및-타입-별칭"><strong>중첩 typedef 및 타입 별칭</strong></h3>
<p>클래스 안에는 <code>typedef</code> 나 <code>using</code> 을 이용한 &#39;타입 별칭(별명)&#39;도 넣을 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
public:
    using IDType = int;

private:
    std::string m_name{};
    IDType m_id{};
    double m_wage{};

public:
    Employee(std::string_view name, IDType id, double wage)
        : m_name { name }
        , m_id { id }
        , m_wage { wage }
    {
    }

    const std::string&amp; getName() { return m_name; }
    IDType getId() { return m_id; } // 클래스 안에서는 짧은 이름만 써도 됩니다.
};

int main()
{
    Employee john { &quot;John&quot;, 1, 45000 };
    Employee::IDType id { john.getId() }; // 클래스 밖에서는 반드시 풀네임을 써야 합니다.

    std::cout &lt;&lt; john.getName() &lt;&lt; &quot; has id: &quot; &lt;&lt; id &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같이 출력됩니다.</p>
<blockquote>
<p>John has id: 1</p>
</blockquote>
<p>보시다시피 클래스 &#39;안&#39;에서는 그냥 <code>IDType</code> 이라고 편하게 쓰면 되지만, 클래스 &#39;밖&#39;에서는 <code>Employee::IDType</code> 처럼 정확한 풀네임을 써야 한다는 점을 기억해 주세요.</p>
<p>(타입 별칭의 장점은 10.7 레슨에서 다루었는데, 여기서도 같은 역할을 합니다. C++ 표준 라이브러리에서도 이런 중첩 <code>typedef</code> 를 아주 흔하게 사용합니다. 글을 쓰는 현재 기준으로 <code>std::string</code> 은 무려 10개나 되는 중첩 <code>typedef</code> 를 가지고 있답니다!)</p>
<hr>
<h3 id="중첩-클래스와-바깥-클래스-멤버-접근"><strong>중첩 클래스와 바깥 클래스 멤버 접근</strong></h3>
<p>클래스 안에 &#39;또 다른 클래스&#39;를 중첩해서 넣는 일은 꽤 드물지만, 가능하긴 합니다. C++에서 중첩 클래스(안쪽 클래스)는 자기를 감싸고 있는 바깥쪽 클래스의 <code>this</code> 포인터에 접근할 수 없습니다. 즉, 안쪽 클래스가 바깥 클래스의 멤버 변수나 함수를 &#39;직접&#39; 마음대로 꺼내 쓸 수는 없다는 뜻입니다. 왜냐하면 안쪽 클래스는 바깥 클래스 객체가 없어도 독립적으로 만들어질 수 있기 때문이에요. (바깥 클래스 객체가 아예 존재하지 않는 상황일 수도 있으니까요!)</p>
<p>하지만 안쪽 클래스도 어쨌든 바깥 클래스 식구(멤버) 중 하나이기 때문에, 바깥 클래스의 <code>private</code> 멤버에 접근할 수 있는 특별한 권한 자체는 가지고 있습니다. 단지 어떤 객체의 데이터인지 명확히 알려주기만 하면 됩니다.</p>
<p>예제를 통해 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
public:
    using IDType = int;

    class Printer
    {
    public:
        void print(const Employee&amp; e) const
        {
            // Printer는 Employee의 &#39;this&#39; 포인터에 접근할 수 없기 때문에,
            // m_name과 m_id를 직접 출력할 수는 없습니다.
            // 대신, 사용할 Employee 객체를 통째로 전달받아야 합니다.
            // Printer는 Employee의 한 식구(멤버)이므로, 
            // 전달받은 객체(e)를 통하면 private 멤버인 e.m_name과 e.m_id에 직접 접근할 수 있습니다.
            std::cout &lt;&lt; e.m_name &lt;&lt; &quot; has id: &quot; &lt;&lt; e.m_id &lt;&lt; &#39;\n&#39;;
        }
    };

private:
    std::string m_name{};
    IDType m_id{};
    double m_wage{};

public:
    Employee(std::string_view name, IDType id, double wage)
        : m_name{ name }
        , m_id{ id }
        , m_wage{ wage }
    {
    }

    // 이 예제에서는 사용되지 않으므로 접근 함수(getter)들을 제거했습니다.
};

int main()
{
    const Employee john{ &quot;John&quot;, 1, 45000 };
    const Employee::Printer p{}; // 안쪽(중첩) 클래스의 객체를 생성합니다.
    p.print(john);

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같이 출력됩니다.</p>
<blockquote>
<p>John has id: 1</p>
</blockquote>
<p>중첩 클래스가 자주 쓰이는 아주 대표적인 예외 상황이 하나 있습니다. 바로 표준 라이브러리인데요, 데이터 모음(컨테이너)을 순회할 때 쓰는 대부분의 &#39;반복자(iterator)&#39; 클래스들이 자신이 탐색할 컨테이너의 중첩 클래스로 만들어져 있습니다. 예를 들어 <code>std::string::iterator</code> 는 <code>std::string</code> 의 중첩 클래스랍니다. (반복자는 나중에 다른 장에서 자세히 다룰게요.)</p>
<hr>
<h3 id="중첩-타입과-전방-선언"><strong>중첩 타입과 전방 선언</strong></h3>
<p><strong>전방 선언</strong>이란, 컴파일러에게 &quot;이런 타입이 앞으로 나올 테니까 놀라지 마&quot;라고 이름만 미리 알려주는 것을 말합니다. 중첩 타입은 자기를 감싸고 있는 바깥 클래스 안에서 미리 전방 선언을 할 수 있습니다. 그러고 나서 나중에 바깥 클래스 안이나 밖에서 그 내용을 진짜로 정의하면 됩니다.</p>
<p>예를 들면 이렇게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class outer
{
public:
    class inner1;   // 정상: 감싸는 클래스 안에서 이름만 미리 전방 선언 가능
    class inner1{}; // 정상: 전방 선언했던 타입을 감싸는 클래스 안에서 정의
    class inner2;   // 정상: 감싸는 클래스 안에서 전방 선언 가능
};

class outer::inner2 // 정상: 전방 선언했던 타입을 감싸는 클래스 &#39;밖&#39;에서 정의
{};

int main()
{
    return 0;
}
</code></pre>
<p><strong>하지만, 바깥 클래스 자체가 어떻게 생겼는지 정의되기도 전에 그 안에 들어갈 중첩 타입만 쏙 빼서 먼저 전방 선언할 수는 없습니다.</strong></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class outer;         // 정상: 중첩되지 않은 일반 클래스는 전방 선언 가능
class outer::inner1; // 오류: 바깥 클래스(outer)가 정의되기도 전에 중첩 타입(inner1)을 전방 선언할 수 없음

class outer
{
public:
    class inner1{}; // 참고: 여기서 중첩 타입이 선언됨
};

class outer::inner1; // 정상 (하지만 무의미함): 이미 바깥 클래스를 정의할 때 중첩 타입도 같이 선언되었기 때문

int main()
{
    return 0;
}
</code></pre>
<p>바깥 클래스를 정의한 &#39;후&#39;에 중첩 타입을 한 번 더 전방 선언하는 것은 기술적으로 오류는 나지 않지만, 이미 바깥 클래스 안에 중첩 타입에 대한 선언이 들어있을 테니 두 번 쓰는 셈이 되어 아무런 의미가 없습니다 (중복).</p>
<hr>
<h2 id="154--소멸자-소개">15.4 — 소멸자 소개</h2>
<p><strong>정리 문제</strong>
네트워크를 통해 데이터를 보내는 프로그램을 만들고 있다고 상상해 봅시다. 서버에 연결을 맺는 과정은 컴퓨터 입장에서 꽤 힘든(비용이 많이 드는) 작업입니다. 그래서 데이터를 매번 찔끔찔끔 보내기보다는, 한 번에 모아두었다가 왕창 보내고 싶을 겁니다. 이런 역할을 하는 클래스는 아마 아래 코드처럼 생겼을 거예요.</p>
<pre><code class="language-cpp">// 이 예제는 (의도적으로) 불완전하기 때문에 컴파일되지 않습니다.
class NetworkData
{
private:
    std::string m_serverName{};
    DataStore m_dataQueue{};

public:
    NetworkData(std::string_view serverName)
        : m_serverName { serverName }
    {
    }

    void addData(std::string_view data)
    {
        m_dataQueue.add(data);
    }

    void sendData()
    {
        // 서버에 연결
        // 모든 데이터 전송
        // 데이터 비우기
    }
};

int main()
{
    NetworkData n(&quot;someipAddress&quot;);

    n.addData(&quot;somedata1&quot;);
    n.addData(&quot;somedata2&quot;);

    n.sendData();

    return 0;
}
</code></pre>
<p>하지만 이 <strong>NetworkData</strong> 클래스에는 잠재적인 폭탄이 하나 숨어 있습니다. 바로 프로그램이 종료되기 전에 무조건 <code>sendData()</code> 함수를 &#39;직접&#39; 호출해 주어야 한다는 점입니다. 만약 이 클래스를 사용하는 사람이 깜빡 잊고 함수 호출을 안 하면, 데이터는 서버로 가지 못하고 프로그램이 꺼질 때 그대로 허공으로 날아가 버립니다.</p>
<p>&quot;에이, 저렇게 뻔히 보이는데 함수 부르는 걸 잊어버리겠어?&quot; 라고 생각하실 수도 있습니다. 네, 이 짧은 예제에서는 맞습니다. 하지만 아래 함수처럼 상황이 조금만 더 복잡해진다면 어떨까요?</p>
<pre><code class="language-cpp">bool someFunction()
{
    NetworkData n(&quot;someipAddress&quot;);

    n.addData(&quot;somedata1&quot;);
    n.addData(&quot;somedata2&quot;);

    if (someCondition)
        return false;

    n.sendData();
    return true;
}
</code></pre>
<p>이 코드를 볼까요? 만약 <code>someCondition</code> 이라는 조건이 참(<code>true</code>)이라면, 중간에 있는 <code>return false;</code>를 만나 함수가 일찍 끝나버립니다! 즉, 맨 밑에 있는 <code>n.sendData();</code> 는 영영 실행되지 못하는 것이죠. 코드는 멀쩡히 잘 적혀 있지만, 특정 상황에서는 데이터 전송이 누락되어 버립니다. 프로그래밍을 하다 보면 이런 실수는 정말 흔하게 일어납니다.</p>
<p>이 문제를 좀 더 크게 바라봅시다. 메모리, 파일, 데이터베이스, 네트워크 연결 같은 <strong>자원(Resource)</strong> 을 사용하는 클래스들은, 객체(클래스로 만든 결과물)가 다 쓰이고 파괴되기 전에 반드시 문을 닫거나 전송을 마치는 등의 &#39;정리&#39; 작업이 필요합니다. 때로는 객체가 사라지기 전에 로그 파일에 기록을 남기거나, 서버에 정보를 보내야 할 수도 있죠.</p>
<p>이처럼 객체가 사라지기 전에 프로그램이 정상적으로 돌아가게 하려고 꼭 거쳐야 하는 모든 뒷수습 작업을 묶어서 <strong>정리(Clean up)</strong> 라고 부릅니다. 만약 이 중요한 뒷정리를 사용하는 사람(프로그래머)의 기억력에만 의존해서 직접 함수를 부르게 만든다면, 언젠가는 반드시 버그가 터지게 되어 있습니다.</p>
<p>그런데 애초에 왜 이걸 사람이 일일이 신경 써야 할까요? 객체가 수명을 다해 파괴될 때가 되었다면, &quot;아, 이제 끝났으니 내가 알아서 뒷정리해야지!&quot; 하고 자동으로 실행되게 할 수는 없을까요?</p>
<p><strong>소멸자가 해결해 드립니다!</strong>
이전 강의인 &#39;14.9 — 생성자 소개&#39;에서 <strong>생성자(Constructor)</strong> 에 대해 배웠습니다. 생성자는 객체가 처음 태어날 때(만들어질 때) 자동으로 불려 와서 기초 설정을 해주는 특별한 함수죠.</p>
<p>이와 아주 비슷하게, 클래스에는 객체가 &#39;파괴되어 사라질 때&#39; 알아서 자동으로 호출되는 또 다른 특별한 함수가 있습니다. 이것을 바로 <strong>소멸자(Destructor)</strong> 라고 부릅니다. <strong>소멸자</strong> 는 객체가 완전히 사라지기 직전에 필요한 모든 청소와 뒷정리를 스스로 하도록 설계되었습니다. (생성자가 &#39;탄생&#39;이라면 소멸자는 &#39;죽음&#39;을 담당한다고 보시면 됩니다!)</p>
<p><strong>소멸자 이름 짓는 법</strong>
생성자처럼, 소멸자도 이름을 짓는 엄격한 규칙이 있습니다:</p>
<ul>
<li>소멸자의 이름은 클래스 이름과 완전히 똑같아야 하며, 맨 앞에 물결표(<code>~</code>)를 붙여야 합니다.</li>
<li>소멸자는 어떤 값(인수)도 전달받을 수 없습니다. (괄호 안이 비어있어야 합니다.)</li>
<li>소멸자는 반환 타입(return type)이 아예 없습니다.</li>
<li>하나의 클래스에는 단 하나의 소멸자만 만들 수 있습니다.</li>
</ul>
<p>또한, 소멸자를 코드에서 여러분이 &#39;직접&#39; 부르시면 안 됩니다. 객체가 파괴될 때 어차피 컴퓨터가 알아서 자동으로 불러주기 때문입니다. 굳이 똑같은 뒷정리를 두 번 할 필요는 없으니까요.</p>
<p>참고로, 소멸자 안에서 클래스의 다른 함수를 부르는 것은 아주 안전합니다. 
소멸자 코드가 끝날 때까지는 객체가 아직 완전히 파괴된 것이 아니기 때문입니다.</p>
<p><strong>소멸자 예제</strong>
코드로 직접 확인해 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Simple
{
private:
    int m_id {};

public:
    Simple(int id)
        : m_id { id }
    {
        std::cout &lt;&lt; &quot;Constructing Simple &quot; &lt;&lt; m_id &lt;&lt; &#39;\n&#39;;
    }

    ~Simple() // 여기가 우리의 소멸자입니다
    {
        std::cout &lt;&lt; &quot;Destructing Simple &quot; &lt;&lt; m_id &lt;&lt; &#39;\n&#39;;
    }

    int getID() const { return m_id; }
};

int main()
{
    // Simple 객체 할당
    Simple simple1{ 1 };
    {
        Simple simple2{ 2 };
    } // simple2는 여기서 사라집니다

    return 0;
} // simple1은 여기서 사라집니다
</code></pre>
<p>이 프로그램을 실행하면 다음과 같은 결과가 나옵니다:</p>
<pre><code class="language-text">Constructing Simple 1
Constructing Simple 2
Destructing Simple 2
Destructing Simple 1
</code></pre>
<p>결과를 잘 보세요. 각 <code>Simple</code> 객체가 파괴될 때 자동으로 소멸자가 불려 와서 메시지를 찍습니다. 눈여겨볼 점은 &quot;Destructing Simple 2&quot;가 1번보다 먼저 출력된다는 점입니다!
왜 그럴까요? <code>simple2</code>는 자기를 감싸고 있는 안쪽 중괄호 <code>{ }</code> 가 끝나는 순간 수명을 다해 먼저 파괴되기 때문입니다. 반면에 <code>simple1</code>은 <code>main()</code> 함수가 완전히 끝날 때까지 살아있다가 제일 마지막에 파괴됩니다.</p>
<p>(기억해 두세요: 전역 변수나 정적 지역 변수 같은 정적 변수들은 프로그램이 켜질 때 만들어지고, 프로그램이 완전히 꺼질 때 파괴됩니다.)</p>
<p><strong>NetworkData 프로그램 개선하기</strong>
자, 이제 강의 맨 처음에 봤던 골칫거리 코드로 돌아가 봅시다. 사용자가 까먹고 <code>sendData()</code> 를 안 부를까 봐 조마조마했던 부분을 소멸자로 시원하게 고쳐보겠습니다!</p>
<pre><code class="language-cpp">class NetworkData
{
private:
    std::string m_serverName{};
    DataStore m_dataQueue{};

public:
    NetworkData(std::string_view serverName)
        : m_serverName { serverName }
    {
    }

    ~NetworkData()
    {
        sendData(); // 객체가 파괴되기 전에 모든 데이터가 전송되도록 보장합니다
    }

    void addData(std::string_view data)
    {
        m_dataQueue.add(data);
    }

    void sendData()
    {
        // 서버에 연결
        // 모든 데이터 전송
        // 데이터 비우기
    }
};

int main()
{
    NetworkData n(&quot;someipAddress&quot;);

    n.addData(&quot;somedata1&quot;);
    n.addData(&quot;somedata2&quot;);

    return 0;
}
</code></pre>
<p>이제 <strong>NetworkData</strong> 안에 소멸자가 생겼습니다! 덕분에 객체가 파괴되기 직전에, 자기가 가지고 있던 모든 데이터를 &#39;무조건&#39; 서버로 알아서 보내고 깔끔하게 생을 마감할 것입니다. 마무리 정리가 100% 자동으로 이루어지니, 여러분은 신경 쓸 일도 줄어들고 오류가 날 확률도 획기적으로 낮아졌습니다.</p>
<p><strong>암시적 소멸자 (자동 생성 소멸자)</strong>
만약 프로그래머가 클래스를 만들면서 소멸자를 깜빡하고(혹은 안 필요해서) 안 만들면 어떻게 될까요? C++ 컴파일러가 알아서 아무 내용도 없는 텅 빈 소멸자를 몰래 하나 만들어 줍니다. 이것을 <strong>암시적 소멸자(Implicit destructor)</strong> 라고 부르며, 그냥 자리만 차지하는 가짜 소멸자라고 생각하시면 됩니다.</p>
<p>만약 여러분이 만든 클래스가 파괴될 때 딱히 특별하게 청소할 거리가 없다면, 굳이 소멸자를 안 만드셔도 전혀 문제없습니다. 컴파일러가 알아서 빈 소멸자를 만들도록 내버려 두면 되니까요.</p>
<p><strong><code>std::exit()</code> 함수에 대한 경고</strong>
이전 강의인 &#39;8.12 — 프로그램 일찍 종료하기&#39;에서 <code>std::exit()</code> 라는 함수를 배웠습니다. 이 함수는 프로그램을 그 즉시 강제로 확 꺼버리는 기능이 있습니다.
문제는 프로그램이 이렇게 비상 종료될 때는, 사용 중이던 지역 변수들이 정상적인 파괴 과정을 거치지 못한다는 점입니다. 즉, <strong>소멸자</strong> 가 호출되지 않고 그냥 날아가 버립니다! 만약 소멸자가 파일 저장 등 아주 중요한 뒷정리를 하도록 믿고 맡겨두셨다면, 이런 상황에서는 뒤통수를 맞을 수 있으니 꼭 주의해야 합니다.</p>
<p><strong>고급 독자를 위한 정보</strong>
처리되지 않은 예외(Unhandled exceptions)로 인해 프로그램이 터져서 죽어버릴 때도 마찬가지입니다. 이때도 스택 풀기(Stack unwinding, 함수 호출 기록을 거꾸로 되짚어 가며 정리하는 과정)가 일어나지 않는다면, 프로그램 종료 전에 <strong>소멸자</strong>가 호출되지 않을 수 있습니다.</p>
<hr>
<h2 id="156--정적-멤버-변수">15.6 — 정적 멤버 변수</h2>
<p>이전 레슨인 7.4 (전역 변수 소개)와 7.11 (정적 지역 변수)에서 우리는 전역 변수와 정적 지역 변수에 대해 배웠습니다. 이 두 종류의 변수는 모두 <strong>정적 지속 시간</strong>을 가집니다. 즉, 프로그램이 시작될 때 딱 만들어져서, 프로그램이 완전히 끝날 때 파괴된다는 뜻이에요. 이런 변수들은 자신이 속한 영역(스코프)을 벗어나더라도 자신의 값을 그대로 기억하고 유지한답니다.</p>
<p>예를 들어볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int generateID()
{
    static int s_id{ 0 }; // 정적 지역 변수 (static local variable)
    return ++s_id;
}

int main()
{
    std::cout &lt;&lt; generateID() &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; generateID() &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; generateID() &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 화면에 이렇게 나옵니다:</p>
<pre><code class="language-text">1
2
3
</code></pre>
<p>여기서 정적 지역 변수인 <code>s_id</code>가 함수가 여러 번 호출되는 동안에도 초기화되지 않고 자신의 값을 계속 유지하고 있다는 점을 주목해 주세요.</p>
<p>클래스(Class)를 사용하면 이 <code>static</code> (정적) 키워드를 쓸 수 있는 곳이 두 군데 더 생깁니다. 바로 <strong>정적 멤버 변수</strong> 와 <strong>정적 멤버 함수</strong> 입니다. 다행히도 이 개념들은 꽤 직관적이고 이해하기 쉬워요. 이번 레슨에서는 정적 멤버 변수에 대해 알아보고, 다음 레슨에서 정적 멤버 함수를 다루겠습니다.</p>
<hr>
<h3 id="정적-멤버-변수">정적 멤버 변수</h3>
<p><code>static</code> 키워드를 멤버 변수에 어떻게 쓰는지 알아보기 전에, 먼저 아주 평범한 아래의 클래스를 살펴볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Something
{
    int value{ 1 };
};

int main()
{
    Something first{};
    Something second{};

    first.value = 2;

    std::cout &lt;&lt; first.value &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; second.value &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>우리가 클래스로 객체(Object)를 만들 때, 각각의 객체는 자신만의 &#39;일반 멤버 변수&#39; 복사본을 가집니다. 마치 각자 개인 수첩을 하나씩 나눠 갖는 것과 같아요. 위 코드에서는 <code>Something</code> 클래스 객체를 두 개(<code>first</code>와 <code>second</code>) 만들었기 때문에, <code>value</code>라는 변수도 <code>first.value</code>와 <code>second.value</code> 두 개가 생깁니다. 이 둘은 완전히 서로 다른 변수예요.</p>
<p>그래서 위 프로그램의 결과는 다음과 같습니다:</p>
<pre><code class="language-text">2
1
</code></pre>
<p>하지만 <code>static</code> 키워드를 사용하면 멤버 변수를 &#39;정적&#39;으로 만들 수 있습니다. 개인 수첩을 갖는 일반 변수와 달리, <strong>정적 멤버 변수</strong> 는 클래스로 만든 모든 객체들이 다 함께 사용하는 &#39;공유 칠판&#39; 같은 역할을 합니다.</p>
<p>위의 코드와 비슷하지만 <code>static</code>이 들어간 아래 프로그램을 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Something
{
    static int s_value; // s_value를 static으로 선언합니다 (초기화는 아래로 이동됨)
};

int Something::s_value{ 1 }; // s_value를 정의하고 1로 초기화합니다 (이 부분은 아래에서 다룰게요)

int main()
{
    Something first{};
    Something second{};

    first.s_value = 2;

    std::cout &lt;&lt; first.s_value &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; second.s_value &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 프로그램의 결과는 다음과 같습니다:</p>
<pre><code class="language-text">2
2
</code></pre>
<p><code>s_value</code>가 정적 멤버 변수이기 때문에, 클래스의 모든 객체가 이 변수를 공유합니다. 즉, <code>first.s_value</code>와 <code>second.s_value</code>는 사실 똑같은 하나의 변수를 가리키고 있는 거예요! 위 코드를 보면 <code>first</code>를 이용해 값을 2로 바꿨는데, <code>second</code>를 이용해서도 그 바뀐 값(2)을 똑같이 확인할 수 있습니다.</p>
<hr>
<h3 id="정적-멤버는-특정-객체에-묶여있지-않습니다">정적 멤버는 특정 객체에 묶여있지 않습니다</h3>
<p>위의 예제처럼 객체(<code>first</code>나 <code>second</code>)를 통해서 정적 멤버에 접근할 수도 있지만, 사실 <strong>정적 멤버</strong> 는 객체를 단 하나도 만들지 않은 상태에서도 이미 존재합니다!
가만히 생각해 보면 당연한 일이에요. 정적 변수들은 프로그램이 시작될 때 만들어지고 끝날 때 파괴되기 때문에, 일반 멤버 변수들처럼 특정 객체가 만들어지고 사라지는 타이밍에 얽매이지 않거든요.</p>
<p>쉽게 말해, 정적 멤버는 클래스라는 울타리 안에 살고 있는 <strong>전역 변수</strong> 라고 생각하시면 됩니다. 클래스 안에 있는 정적 멤버나, 네임스페이스(namespace) 안에 있는 일반 변수나 사실상 거의 차이가 없어요.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
정적 멤버는 클래스의 범위(Scope) 안에 존재하는 전역 변수입니다.</p>
</blockquote>
<p><code>s_value</code>라는 정적 멤버 변수는 특정 객체와 상관없이 독립적으로 존재하기 때문에, 클래스 이름과 범위 지정 연산자(<code>::</code>)를 사용해서 직접 접근할 수 있습니다. (예: <code>Something::s_value</code>)</p>
<pre><code class="language-cpp">class Something
{
public:
    static int s_value; // s_value를 static으로 선언합니다
};

int Something::s_value{ 1 }; // s_value를 정의하고 1로 초기화합니다 (이 부분은 아래에서 다룰게요)

int main()
{
    // 주의: 우리는 Something 타입의 객체를 전혀 만들지 않았습니다!

    Something::s_value = 2;
    std::cout &lt;&lt; Something::s_value &lt;&lt; &#39;\n&#39;;
    return 0;
}
</code></pre>
<p>위 코드에서는 객체를 통하지 않고 클래스 이름인 <code>Something</code>을 이용해 <code>s_value</code>를 사용했습니다. 우리가 <code>Something</code> 객체를 단 하나도 만들지 않았는데도 <code>Something::s_value</code>를 사용하고 값을 바꿀 수 있죠? 정적 멤버를 사용할 때는 이렇게 클래스 이름을 통해 접근하는 것이 가장 좋은 방법입니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
정적 멤버에 접근할 때는 항상 클래스 이름과 범위 지정 연산자(<code>::</code>)를 사용하세요. 
(예: <code>Something::s_value</code>)</p>
</blockquote>
<hr>
<h3 id="정적-멤버-변수-정의하고-초기화하기">정적 멤버 변수 정의하고 초기화하기</h3>
<p>클래스 안에서 정적 멤버 변수를 선언(<code>static int s_value;</code>)하는 것은 컴퓨터에게 &quot;이런 정적 변수가 존재할 거야~&quot;라고 알려주는 역할만 할 뿐, 실제로 변수를 만들어내는(정의하는) 것은 아닙니다.
정적 멤버 변수는 사실상 전역 변수와 같기 때문에, 반드시 클래스 밖(전역 범위)에서 명시적으로 정의를 해주고 값을 초기화해 주어야 합니다.</p>
<p>위 예제에서는 이 코드가 그 역할을 했죠:</p>
<pre><code class="language-cpp">int Something::s_value{ 1 }; // s_value를 정의하고 1로 초기화합니다
</code></pre>
<p>이 한 줄의 코드는 두 가지 역할을 합니다. 전역 변수처럼 정적 멤버 변수를 실제로 만들어내고, 거기에 <code>1</code>이라는 초기값을 넣어주는 거죠. 만약 초기값을 따로 주지 않으면 기본적으로 <code>0</code>으로 채워집니다.</p>
<p>참고로 이렇게 밖에서 정의할 때는 접근 제어자(private, protected 등)의 영향을 받지 않습니다. 클래스 안에서 <code>private</code>으로 숨겨놨더라도, 밖에서 정의하고 초기화하는 것은 문제가 안 돼요. (정의하는 행위 자체는 접근으로 치지 않거든요.)</p>
<hr>
<h3 id="클래스-안에서-정적-멤버-변수-초기화하기-지름길">클래스 안에서 정적 멤버 변수 초기화하기 (지름길)</h3>
<p>항상 클래스 밖에서 초기화해야 한다면 귀찮겠죠? 몇 가지 예외적인 지름길이 있습니다.
첫 번째로, 정적 멤버가 <strong>상수 정수형</strong> (여기엔 <code>char</code>나 <code>bool</code>도 포함돼요)이거나 <code>const enum</code>일 때는 클래스 안에서 바로 초기화할 수 있습니다.</p>
<pre><code class="language-cpp">class Whatever
{
public:
    static const int s_value{ 4 }; // static const int는 클래스 안에서 바로 정의하고 초기화할 수 있습니다
};
</code></pre>
<p>또한 C++17부터는 <code>inline</code>이라는 마법의 키워드를 사용할 수 있습니다. <code>inline</code> 변수를 사용하면 상수인지 아닌지에 상관없이 클래스 안에서 곧바로 초기화할 수 있어서 아주 편리합니다. 최근에는 이 방법이 가장 권장됩니다.</p>
<pre><code class="language-cpp">class Whatever
{
public:
    static inline int s_value{ 4 }; // static inline 변수는 바로 정의하고 초기화할 수 있습니다
};
</code></pre>
<p><code>constexpr</code> 멤버는 C++17부터 자동으로 <code>inline</code> 취급을 받기 때문에, 굳이 <code>inline</code>이라고 쓰지 않아도 클래스 내부에서 초기화가 가능합니다.</p>
<pre><code class="language-cpp">#include &lt;string_view&gt;

class Whatever
{
public:
    static constexpr double s_value{ 2.2 }; // 문제 없음!
    static constexpr std::string_view s_view{ &quot;Hello&quot; }; // constexpr 초기화를 지원하는 클래스에서도 잘 작동합니다
};
</code></pre>
<blockquote>
<p><strong>모범 사례</strong>
정적 멤버 변수는 <code>inline</code>이나 <code>constexpr</code>로 만들어서 클래스 내부에서 깔끔하게 초기화하는 것을 추천합니다.</p>
</blockquote>
<hr>
<h3 id="정적-멤버-변수는-언제-쓸까요-예제">정적 멤버 변수는 언제 쓸까요? (예제)</h3>
<p>그렇다면 클래스 안에 정적 변수를 왜 쓸까요? 가장 대표적인 쓰임새는 클래스로 찍어내는 모든 객체들에게 <strong>고유한 ID 번호</strong> 를 부여할 때입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
private:
    static inline int s_idGenerator { 1 };
    int m_id {};

public:
    // id 생성기에서 다음 값을 가져옵니다
    Something() : m_id { s_idGenerator++ }
    {
    }

    int getID() const { return m_id; }
};

int main()
{
    Something first{};
    Something second{};
    Something third{};

    std::cout &lt;&lt; first.getID() &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; second.getID() &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; third.getID() &lt;&lt; &#39;\n&#39;;
    return 0;
}
</code></pre>
<p>결과는 다음과 같습니다:</p>
<pre><code class="language-text">1
2
3
</code></pre>
<p><code>s_idGenerator</code>는 모든 <code>Something</code> 객체가 공유하는 정적 변수입니다. 새로운 객체가 만들어질 때마다 생성자가 이 값을 객체의 개인 ID(<code>m_id</code>)로 저장하고, 다음 객체를 위해 공유 값인 <code>s_idGenerator</code>를 1 증가시킵니다. 이렇게 하면 새로 생겨나는 모든 객체들이 절대 겹치지 않는 자신만의 고유한 ID를 가질 수 있게 되죠!</p>
<p>정적 멤버 변수는 <strong>조회 테이블</strong> 을 만들 때도 매우 유용합니다. 복잡한 계산 값을 미리 배열에 저장해 두고 필요할 때마다 꺼내 쓴다고 가정해 보세요. 이 배열을 일반 멤버로 만들면 객체를 100개 만들 때 배열도 100개가 생겨서 메모리 낭비가 심해집니다. 하지만 정적(static) 멤버로 만들면, 객체가 몇 개든 상관없이 배열은 딱 1개만 존재해서 모든 객체가 공유하므로 메모리를 크게 아낄 수 있습니다.</p>
<hr>
<h3 id="오직-정적-멤버만-타입-추론auto-와-ctad을-쓸-수-있습니다">오직 정적 멤버만 타입 추론(auto 와 CTAD)을 쓸 수 있습니다</h3>
<p>C++에는 타입을 알아서 맞춰주는 편리한 기능인 <code>auto</code>나, 템플릿 타입을 유추해 주는 CTAD라는 기능이 있습니다.
초보자분들은 &quot;아, 편한 기능이구나&quot; 정도로만 아셔도 충분해요! 중요한 건, 이런 편리한 타입 추론 기능은 <strong>오직 정적 멤버에만</strong> 쓸 수 있고 일반(비정적) 멤버에는 쓸 수 없다는 점입니다.</p>
<p>왜 그런지 깊게 들어가면 많이 복잡해지지만, 간단히 말해 일반 멤버에 이 기능을 허용하면 컴퓨터가 너무 헷갈려 하는 상황이 발생하기 때문입니다. 반면 정적 멤버는 규칙이 단순해서 헷갈릴 일이 없거든요.</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::pair&lt;T, U&gt;를 사용하기 위해 필요합니다

class Foo
{
private:
    auto m_x { 5 };           // 에러: 일반(비정적) 멤버는 auto를 쓸 수 없습니다
    std::pair m_v { 1, 2.3 }; // 에러: 일반 멤버는 CTAD를 쓸 수 없습니다

    static inline auto s_x { 5 };           // 성공: 정적 멤버는 auto를 쓸 수 있습니다
    static inline std::pair s_v { 1, 2.3 }; // 성공: 정적 멤버는 CTAD를 쓸 수 있습니다

public:
    Foo() {};
};

int main()
{
    Foo foo{};

    return 0;
}
</code></pre>
<hr>
<h2 id="157--정적-멤버-함수">15.7 — 정적 멤버 함수</h2>
<p>이전 학습인 &#39;15.6 - 정적(Static) 멤버 변수&#39;에서, 정적 멤버 변수는 특정 &#39;객체(붕어빵)&#39;가 아니라 &#39;클래스(붕어빵 틀)&#39; 자체에 속한다는 걸 배웠어요. 만약 이 정적 변수가 공개(public)되어 있다면, 클래스 이름과 범위 지정 연산자(<code>::</code>)를 써서 바로 꺼내 쓸 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
public:
    static inline int s_value { 1 };
};

int main()
{
    std::cout &lt;&lt; Something::s_value; // s_value는 public이므로 직접 접근할 수 있어요!
}
</code></pre>
<p>하지만 정적 멤버 변수가 비공개(private)라면 어떨까요? 다음 예시를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
private: // 이제 private입니다!
    static inline int s_value { 1 };
};

int main()
{
    std::cout &lt;&lt; Something::s_value; // 오류: s_value는 private이라서 클래스 밖에서 직접 접근할 수 없어요.
}
</code></pre>
<p>이런 경우에는 <code>s_value</code>가 private이기 때문에 <code>main()</code> 함수에서 직접 가져다 쓸 수 없어요. 보통 우리는 private 변수를 꺼내 올 때 public 함수를 사용하죠. 그래서 <code>s_value</code>를 꺼내기 위한 평범한 public 함수를 만들 수는 있겠지만, 그렇게 되면 그 함수를 쓰기 위해 굳이 <strong>객체(붕어빵)</strong> 를 하나 새로 만들어야 하는 번거로움이 생깁니다!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
private:
    static inline int s_value { 1 };

public:
    int getValue() { return s_value; }
};

int main()
{
    Something s{};
    std::cout &lt;&lt; s.getValue(); // 작동은 하지만, getValue()를 호출하려면 굳이 객체를 만들어야 해요.
}
</code></pre>
<p>우리는 더 똑똑한 방법을 쓸 수 있습니다.</p>
<hr>
<h3 id="정적-멤버-함수">정적 멤버 함수</h3>
<p>변수에만 <code>static</code>을 붙일 수 있는 게 아니에요. 함수에도 똑같이 붙일 수 있답니다. 방금 본 예시에 정적 멤버 함수를 적용해 볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
private:
    static inline int s_value { 1 };

public:
    static int getValue() { return s_value; } // 정적 멤버 함수
};

int main()
{
    std::cout &lt;&lt; Something::getValue() &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p><strong>정적 멤버 함수</strong>는 특정 객체(붕어빵)에 묶여 있지 않아요. 그래서 굳이 객체를 만들지 않고도 클래스 이름과 <code>::</code> 기호만 써서 바로 호출할 수 있답니다 (예: <code>Something::getValue()</code>). 정적 변수와 마찬가지로 객체를 통해서 부를 수도 있지만, 헷갈릴 수 있어서 추천하지 않아요.</p>
<hr>
<h3 id="정적-멤버-함수에는-this-포인터가-없어요">정적 멤버 함수에는 <code>this</code> 포인터가 없어요!</h3>
<p>정적 멤버 함수에는 꼭 기억해야 할 두 가지 흥미로운 특징이 있어요.</p>
<p>첫째, 정적 멤버 함수는 특정 객체에 소속된 게 아니기 때문에 <strong><code>this</code> 포인터</strong> 가 없습니다! 생각해보면 당연해요. <code>this</code>는 보통 &#39;지금 이 작업을 하고 있는 그 객체&#39;를 가리키는데, 정적 함수는 애초에 객체 위에서 돌아가는 게 아니니까 <code>this</code>가 필요 없죠.</p>
<p>둘째, 정적 멤버 함수는 다른 정적 멤버(변수나 함수)에는 자유롭게 접근할 수 있지만, <strong>일반(non-static) 멤버</strong> 에는 접근할 수 없어요. 일반 멤버는 반드시 어떤 객체 안에 존재해야 하는데, 정적 함수는 작업할 객체 자체가 없기 때문이에요!</p>
<hr>
<h3 id="클래스-밖에서-정적-멤버-정의하기">클래스 밖에서 정적 멤버 정의하기</h3>
<p>정적 멤버 함수도 일반 멤버 함수처럼 클래스 바깥에서 그 내용을 정의할 수 있어요. 방법은 똑같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class IDGenerator
{
private:
    static inline int s_nextID { 1 };

public:
     static int getNextID(); // 정적 함수의 선언부입니다.
};

// 여기는 클래스 밖에서 정적 함수를 정의하는 부분입니다. 여기서 static 키워드는 쓰지 않아요!
int IDGenerator::getNextID() { return s_nextID++; }

int main()
{
    for (int count{ 0 }; count &lt; 5; ++count)
        std::cout &lt;&lt; &quot;The next ID is: &quot; &lt;&lt; IDGenerator::getNextID() &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">The next ID is: 1
The next ID is: 2
The next ID is: 3
The next ID is: 4
The next ID is: 5
</code></pre>
<p>이 클래스 안의 모든 데이터와 함수가 <code>static</code>이기 때문에, 이 기능을 쓰기 위해 굳이 객체를 만들 필요가 없다는 점을 꼭 기억하세요! 이 클래스는 정적 멤버 변수로 다음 ID 값을 기억하고, 정적 멤버 함수로 그 값을 반환하면서 숫자를 1씩 올려줍니다.</p>
<p>15.2 학습에서 다루었듯이, 클래스 안에서 만든 함수는 자동으로 <strong>인라인(inline)</strong> 처리가 되지만, 클래스 밖에서 정의한 함수는 그렇지 않아요. 그래서 헤더 파일 안에 클래스 밖에서 정의한 정적 멤버 함수를 넣을 때는, 여러 파일에서 불러오더라도 에러(ODR 위반)가 나지 않게 꼭 <code>inline</code> 키워드를 붙여주는 게 좋습니다.</p>
<hr>
<h3 id="모든-멤버가-정적인-클래스를-만들-때-주의할-점">모든 멤버가 정적인 클래스를 만들 때 주의할 점</h3>
<p>모든 멤버가 <code>static</code>인 클래스(일명 &#39;순수 정적 클래스&#39;)를 만들 때는 조심해야 해요. 유용할 때도 있지만, 단점도 있거든요.</p>
<p>첫째, 모든 정적 멤버는 딱 한 번만 만들어지기 때문에, 여러 개의 독립적인 버전을 만들 수 없어요. 예를 들어, 서로 완전히 다르게 작동하는 2개의 <code>IDGenerator</code> 가 필요하더라도, 순수 정적 클래스로는 불가능하죠.</p>
<p>둘째, 전역(Global) 변수 학습에서 배웠듯이, 전역 변수는 누군가 어디서든 값을 바꿀 수 있어서 예상치 못한 에러를 낼 수 있기 때문에 위험합니다. 순수 정적 클래스도 똑같아요. 사실상 전역 변수나 전역 함수를 모아둔 것과 비슷해서 이런 부작용을 똑같이 겪을 수 있습니다.</p>
<p>그래서 무작정 모든 걸 <code>static</code>으로 만들기보다는, 평범한 일반 클래스를 하나 만들어서 그걸 전역(Global) 객체로 하나 생성하는 방식이 더 나을 수 있어요. 그러면 필요할 때 전역 객체를 쓰면서도, 언제든 독립적인 개별 객체를 여러 개 만들어서 쓸 수 있으니까요.</p>
<hr>
<h3 id="순수-정적-클래스-vs-네임스페이스namespace">순수 정적 클래스 vs 네임스페이스(Namespace)</h3>
<p>순수 정적 클래스는 네임스페이스와 아주 비슷해요. 둘 다 그 안에서 전역적으로 쓰이는 변수나 함수를 정의할 수 있죠. 하지만 가장 큰 차이점은 <strong>클래스는 접근 제어(private, public)</strong> 가 가능하고, 네임스페이스는 그게 안 된다는 거예요.</p>
<p>결론적으로, 외부에서 못 보게 숨겨야 할 데이터가 있거나 접근 제어가 필요하다면 <strong>정적 클래스</strong> 를 쓰고, 그게 아니라면 <strong>네임스페이스</strong> 를 쓰는 게 좋습니다.</p>
<hr>
<h3 id="c에는-정적static-생성자가-없어요">C++에는 정적(Static) 생성자가 없어요</h3>
<p>일반 변수들을 생성자로 초기화하는 것처럼, 정적 변수도 &#39;정적 생성자&#39;로 초기화하면 좋겠다고 생각할 수 있어요. 다른 언어들에는 이런 기능이 있지만, 안타깝게도 C++에는 없습니다.</p>
<p>대신 정적 변수는 정의하는 순간 바로 값을 넣어(초기화) 줄 수 있어요 (심지어 private이라도 말이죠!). 위의 <code>IDGenerator</code> 예제에서도 그렇게 했고, 아래 다른 예시도 확인해 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Chars
{
    char first{};
    char second{};
    char third{};
    char fourth{};
    char fifth{};
};

struct MyClass
{
    static inline Chars s_mychars { &#39;a&#39;, &#39;e&#39;, &#39;i&#39;, &#39;o&#39;, &#39;u&#39; }; // 정의하는 시점에 정적 변수를 초기화합니다.
};

int main()
{
    std::cout &lt;&lt; MyClass::s_mychars.third; // i 출력

    return 0;
}
</code></pre>
<p>만약 정적 멤버 변수를 초기화하는 데 코드를 실행해야 한다면 (예: 반복문 사용 등), 조금 복잡하지만 방법이 있습니다. 가장 무난한 방법은 값을 채워주는 함수를 따로 만들어서 객체를 반환하게 한 다음, 그 값을 복사해서 넣는 거예요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Chars
{
    char first{};
    char second{};
    char third{};
    char fourth{};
    char fifth{};
};

class MyClass
{
private:
    static Chars generate()
    {
        Chars c{}; // 객체를 하나 만듭니다.
        c.first = &#39;a&#39;; // 원하는 대로 값을 채워 넣습니다.
        c.second = &#39;e&#39;;
        c.third = &#39;i&#39;;
        c.fourth = &#39;o&#39;;
        c.fifth = &#39;u&#39;;

        return c; // 객체를 반환합니다.
    }

public:
    static inline Chars s_mychars { generate() }; // 반환된 객체의 데이터를 s_mychars에 복사합니다.
};

int main()
{
    std::cout &lt;&lt; MyClass::s_mychars.third; // i 출력

    return 0;
}
</code></pre>
<hr>
<h2 id="158--프렌드friend-비멤버-함수">15.8 — 프렌드(friend) 비멤버 함수</h2>
<p>우리는 이전 장부터 계속해서 클래스의 데이터를 보호하는 &#39;접근 제어(public, private)&#39;의 장점에 대해 이야기해 왔습니다. 이 기능은 누가 클래스의 데이터에 접근할 수 있는지를 관리해 주죠. <code>private</code> 데이터는 클래스 내부에서만 건드릴 수 있고, <code>public</code> 데이터는 누구나 접근할 수 있습니다. [14.6 수업 - 접근 함수]에서는 데이터를 <code>private</code>으로 안전하게 숨겨두고, 외부 사람들은 <code>public</code> 함수를 통해서만 접근하게 만드는 방식이 왜 좋은지 배웠습니다.</p>
<p>하지만, 가끔은 이런 룰만으로는 상황을 해결하기 어렵거나 비효율적일 때가 있습니다.</p>
<p>예를 들어, 어떤 데이터를 전문적으로 관리하는 &#39;저장소(storage)&#39; 클래스가 있다고 해볼게요. 그런데 이 데이터를 화면에 멋지게 보여주고 싶어졌습니다. 화면 출력 기능은 옵션도 많고 꽤 복잡한 작업입니다.</p>
<p>물론 데이터 저장 기능과 화면 출력 기능을 하나의 클래스에 다 쑤셔 넣을 수도 있겠지만, 그러면 코드가 너무 복잡하고 지저분해지겠죠. 그래서 저장소 클래스는 &#39;저장&#39;만 하고, 화면 출력 클래스는 &#39;출력&#39;만 하도록 두 개를 분리하는 것이 좋습니다. 역할 분담이 확실해지니까요!
하지만 여기서 문제가 생깁니다. 출력 클래스가 저장소 클래스의 <code>private</code> 데이터에 접근할 수가 없어서, 화면에 데이터를 띄우는 제 역할을 할 수 없게 되어버립니다.</p>
<p>또 다른 경우로, 나중에 &#39;연산자 오버로딩&#39;이라는 것을 배울 때 자주 겪게 될 텐데, 문법적인 이유로 클래스 안에 속한 &#39;멤버 함수&#39;보다 밖에 있는 &#39;비멤버(non-member) 함수&#39;를 쓰는 것이 더 편할 때가 있습니다. 하지만 비멤버 함수 역시 클래스의 <code>private</code> 데이터에는 접근할 수 없다는 똑같은 문제에 부딪히게 됩니다.</p>
<p>만약 우리가 쓰려는 기능에 딱 맞는 <code>public</code> 함수가 이미 만들어져 있다면 정말 다행입니다. 그냥 그걸 쓰면 되니까요. 하지만 그런 함수가 아예 없다면 대체 어떻게 해야 할까요?</p>
<p>그렇다고 다른 클래스나 비멤버 함수가 일을 할 수 있도록 클래스에 새로운 <code>public</code> 함수를 무작정 추가하는 것도 좋은 생각은 아닙니다. 밖에서 함부로 건드리면 안 되는 민감한 정보이거나, 잘못 사용될 위험이 있는 데이터일 수 있으니까요.</p>
<p>우리에게 정말 필요한 것은, <strong>&quot;딱 내가 원하는 상대에게만 예외적으로 접근 권한을 주는 방법&quot;</strong> 입니다.</p>
<hr>
<h3 id="friendship은-마법입니다">Friendship은 마법입니다</h3>
<p>이 문제의 완벽한 해결책이 바로 &#39;프렌드(friend)&#39;입니다.</p>
<p>클래스 안에서 <code>friend</code> 키워드를 사용해 선언을 해주면, 컴파일러에게 
&quot;이 함수(또는 클래스)는 내 친구야!&quot;라고 알려줄 수 있습니다. C++에서 <strong>프렌드(friend)</strong> 란 다른 클래스의 <code>private</code>이나 <code>protected</code> 데이터에 자유롭게 접근할 수 있는 특별한 권한을 받은 클래스나 함수를 말합니다. 이 방법을 쓰면, 클래스는 안전함을 유지하면서도 내가 콕 집은 상대에게만 모든 권한을 열어줄 수 있습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
우정(접근 권한)은 항상 &#39;데이터를 가진 클래스&#39;가 직접 허락해 주는 것입니다. 접근하고 싶은 쪽에서 마음대로 친구가 될 수는 없습니다. 덕분에 클래스는 여전히 &quot;누가 내 데이터에 접근할 수 있는지&quot;를 완벽하게 통제할 수 있죠.</p>
</blockquote>
<p>예를 들어, 아까 말한 저장소 클래스가 출력 클래스를 &#39;친구&#39;로 받아준다면, 출력 클래스는 저장소 클래스의 모든 데이터에 마음껏 접근할 수 있게 됩니다. 코드는 깔끔하게 분리되어 있으면서도, 화면 출력이라는 본래 역할을 무사히 해낼 수 있는 것이죠.</p>
<p>참고로 <code>friend</code> 선언은 <code>public</code>이나 <code>private</code> 같은 접근 제어 구역의 영향을 받지 않습니다. 따라서 클래스 내부의 어디에 적어두든 상관없이 똑같이 작동합니다.</p>
<p>프렌드가 무엇인지 알았으니, 이제 비멤버 함수, 멤버 함수, 그리고 다른 클래스에 프렌드 권한을 주는 실제 예시들을 살펴보겠습니다. 이번 수업에서는 <strong>프렌드 비멤버 함수</strong> 에 대해 알아보고, 다음 [15.9 수업]에서 프렌드 클래스와 프렌드 멤버 함수를 다루겠습니다.</p>
<hr>
<h3 id="프렌드-비멤버-함수">프렌드 비멤버 함수</h3>
<p><strong>프렌드 함수</strong> 란 마치 그 클래스의 진짜 멤버인 것처럼 <code>private</code> 및 <code>protected</code> 데이터에 접근할 수 있는 함수를 말합니다. 그 외의 나머지 특징은 일반 함수와 완전히 똑같습니다.</p>
<p>간단한 클래스가 비멤버 함수를 친구로 만드는 예제를 살펴보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Accumulator
{
private:
    int m_value { 0 };

public:
    void add(int value) { m_value += value; }

    // 비멤버 함수인 void print(const Accumulator&amp; accumulator)를 
    // Accumulator의 친구로 만들어주는 프렌드 선언입니다.
    friend void print(const Accumulator&amp; accumulator);
};

void print(const Accumulator&amp; accumulator)
{
    // print() 함수는 Accumulator의 친구이기 때문에
    // Accumulator의 private 멤버에 자유롭게 접근할 수 있습니다.
    std::cout &lt;&lt; accumulator.m_value;
}

int main()
{
    Accumulator acc{};
    acc.add(5); // accumulator에 5를 더합니다.

    print(acc); // print() 비멤버 함수를 호출합니다.

    return 0;
}
</code></pre>
<p>이 예제에서 우리는 <code>Accumulator</code> 객체를 받아 작동하는 <code>print()</code> 라는 비멤버 함수를 만들었습니다. <code>print()</code>는 <code>Accumulator</code> 클래스에 속한 함수가 아니기 때문에 원래대로라면 <code>private</code> 변수인 <code>m_value</code>에 접근할 수 없습니다. 하지만 <code>Accumulator</code> 클래스 안에서 <code>print(const Accumulator&amp; accumulator)</code> 를 친구로 허락해 주었기 때문에, 이제 당당하게 접근할 수 있습니다.</p>
<p>주의할 점은 <code>print()</code> 가 비멤버 함수라는 것입니다. 클래스에 속해 있지 않기 때문에, 어떤 객체의 값을 출력할지 알 수 있도록 반드시 <code>Accumulator</code> 객체를 괄호 안에 직접 넣어주어야 합니다.</p>
<hr>
<h3 id="클래스-안에서-프렌드-비멤버-함수-정의하기">클래스 안에서 프렌드 비멤버 함수 정의하기</h3>
<p>멤버 함수를 클래스 안에서 작성할 수 있는 것처럼, 프렌드 비멤버 함수도 클래스 내부에서 곧바로 작성(정의)할 수 있습니다. 다음은 <code>Accumulator</code> 클래스 안에서 프렌드 비멤버 함수 <code>print()</code> 를 작성한 예제입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Accumulator
{
private:
    int m_value { 0 };

public:
    void add(int value) { m_value += value; }

    // 클래스 내부에서 정의된 프렌드 함수는 비멤버 함수로 취급됩니다.
    friend void print(const Accumulator&amp; accumulator)
    {
        // print()는 Accumulator의 친구이기 때문에
        // Accumulator의 private 멤버에 접근할 수 있습니다.
        std::cout &lt;&lt; accumulator.m_value;
    }
};

int main()
{
    Accumulator acc{};
    acc.add(5); // accumulator에 5를 더합니다.

    print(acc); // print() 비멤버 함수를 호출합니다.

    return 0;
}
</code></pre>
<p>함수가 <code>Accumulator</code> 클래스 안에 쏙 들어가 있으니까 마치 멤버 함수 같아 보이시죠? 하지만 속으시면 안 됩니다! <code>friend</code> 로 정의되었기 때문에, 이 함수는 여전히 <strong>비멤버 함수</strong> 로 취급됩니다 (마치 클래스 밖에 적어둔 것과 똑같이 동작합니다).</p>
<hr>
<h3 id="문법적으로-프렌드-비멤버-함수가-더-편한-경우">문법적으로 프렌드 비멤버 함수가 더 편한 경우</h3>
<p>수업 처음에 &quot;멤버 함수보다 비멤버 함수를 쓰는 게 더 편할 때가 있다&quot;고 말씀드렸었죠. 그게 어떤 경우인지 예제로 확인해 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Value
{
private:
    int m_value{};

public:
    explicit Value(int v): m_value { v }  { }

    bool isEqualToMember(const Value&amp; v) const;
    friend bool isEqualToNonmember(const Value&amp; v1, const Value&amp; v2);
};

bool Value::isEqualToMember(const Value&amp; v) const
{
    return m_value == v.m_value;
}

bool isEqualToNonmember(const Value&amp; v1, const Value&amp; v2)
{
    return v1.m_value == v2.m_value;
}

int main()
{
    Value v1 { 5 };
    Value v2 { 6 };

    std::cout &lt;&lt; v1.isEqualToMember(v2) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; isEqualToNonmember(v1, v2) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 예제에서는 두 <code>Value</code> 객체의 값이 똑같은지 확인하는 두 가지 함수를 만들었습니다. <code>isEqualToMember()</code>는 멤버 함수이고, <code>isEqualToNonmember()</code>는 비멤버 함수입니다. 코드가 어떻게 다른지 집중해서 봐주세요.</p>
<p>멤버 함수인 <code>isEqualToMember()</code>를 쓸 때는 기준이 되는 객체(<code>v1</code>)는 숨겨져 있고, 비교할 객체(<code>v2</code>)만 괄호 안에 넣습니다. 그래서 코드를 읽을 때 &quot;앞의 <code>m_value</code>는 숨겨진 내 거고, 뒤의 <code>v.m_value</code>는 괄호로 들어온 쟤 거구나&quot; 하고 머릿속으로 한 번 정리를 해야 합니다.</p>
<p>반면 비멤버 함수인 <code>isEqualToNonmember()</code>는 두 객체를 모두 괄호 안에 나란히 넣습니다. 함수 안에서도 <code>v1.m_value</code> 와 <code>v2.m_value</code> 처럼 누구의 값인지 명확하게 이름표가 붙어 있죠. 모양이 좌우 대칭이라 훨씬 읽기 편하고 직관적입니다.</p>
<p>물론 <code>v1.isEqualToMember(v2)</code> 라고 쓰는 게 더 좋다고 생각하실 수도 있습니다. 하지만 나중에 연산자 오버로딩을 배울 때, 왜 비멤버 방식이 중요한지 이 주제가 다시 등장할 것입니다.</p>
<hr>
<h3 id="여러-클래스의-친구가-되기">여러 클래스의 친구가 되기</h3>
<p>하나의 함수가 동시에 여러 클래스의 친구가 될 수도 있습니다. 다음 예제를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Humidity; // Humidity 클래스에 대한 전방 선언(forward declaration)

class Temperature
{
private:
    int m_temp { 0 };

public:
    explicit Temperature(int temp) : m_temp { temp } { }

    // 이 줄이 에러 없이 작동하려면 위에서 전방 선언을 해두어야 합니다.
    friend void printWeather(const Temperature&amp; temperature, const Humidity&amp; humidity); 
};

class Humidity
{
private:
    int m_humidity { 0 };

public:
    explicit Humidity(int humidity) : m_humidity { humidity } {  }

    friend void printWeather(const Temperature&amp; temperature, const Humidity&amp; humidity);
};

void printWeather(const Temperature&amp; temperature, const Humidity&amp; humidity)
{
    std::cout &lt;&lt; &quot;온도는 &quot; &lt;&lt; temperature.m_temp &lt;&lt;
       &quot; 이고 습도는 &quot; &lt;&lt; humidity.m_humidity &lt;&lt; &quot; 입니다.\n&quot;;
}

int main()
{
    Humidity hum { 10 };
    Temperature temp { 12 };

    printWeather(temp, hum);

    return 0;
}
</code></pre>
<p>이 예제에서 주목해야 할 점은 세 가지입니다.</p>
<ol>
<li><code>printWeather()</code> 함수는 온도와 습도를 똑같이 중요하게 다룹니다. 따라서 어느 한 클래스에 억지로 집어넣는 것보다, 바깥에 꺼내두는 비멤버 함수 방식이 훨씬 자연스럽습니다.</li>
<li>이 함수는 온도 클래스와 습도 클래스 모두와 친구를 맺었기 때문에, 양쪽의 <code>private</code> 데이터에 자유롭게 접근할 수 있습니다.</li>
<li>코드 맨 위에 있는 <code>class Humidity;</code> 라는 줄을 눈여겨보세요. 이것을 <strong>전방 선언(Forward declaration)</strong> 이라고 부릅니다. 컴파일러에게 &quot;나중에 Humidity라는 클래스가 나올 거야&quot;라고 미리 귀띔해 주는 역할을 합니다. 이 줄이 없다면, 컴파일러는 <code>Temperature</code> 클래스 안에서 <code>Humidity</code>를 만났을 때 &quot;이게 대체 뭐야?&quot; 하고 에러를 뱉어낼 것입니다.</li>
</ol>
<hr>
<h3 id="프렌드는-데이터-숨기기은닉-원칙을-위반하는-거-아닌가요">프렌드는 데이터 숨기기(은닉) 원칙을 위반하는 거 아닌가요?</h3>
<p>아닙니다. 앞서 말씀드렸듯 프렌드 권한은 데이터를 숨긴 클래스 측에서 &quot;내 친구니까 접근해도 좋아&quot;라고 스스로 허락한 것입니다. 프렌드를 클래스의 능력을 함께 쓰는 &#39;확장팩&#39; 정도로 생각해 보세요. 클래스가 예상하고 허락한 접근이므로 원칙 위반이 아닙니다.</p>
<p>프렌드를 적절히 활용하면, 억지로 코드를 합치지 않아도 되기 때문에 프로그램 설계가 깔끔해지고 유지보수가 훨씬 쉬워집니다. 멤버 함수보다 비멤버 함수를 쓰는 게 구조상 더 알맞을 때도 큰 도움이 되고요.</p>
<p>하지만 주의할 점도 있습니다. 프렌드는 클래스의 속사정(내부 구현)을 훤히 알고 직접 접근하기 때문에, 클래스 내부 코드가 바뀌면 프렌드 함수도 덩달아 수정해야 할 확률이 높습니다. 친구가 너무 많으면 하나를 고칠 때 줄줄이 고쳐야 하는 &#39;도미노 현상&#39;이 일어날 수 있죠.</p>
<blockquote>
<p><strong>권장 사항</strong>
프렌드 함수를 구현할 때도, 꼭 필요한 경우가 아니라면 직접 데이터에 손대기보다는 클래스에 이미 마련된 <code>public</code> 함수(인터페이스)를 사용하는 것이 좋습니다. 이렇게 하면 나중에 클래스 코드가 바뀌더라도 프렌드 함수까지 뜯어고치는 불상사를 막을 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="프렌드-함수보다는-일반비프렌드-함수를-선호하세요">프렌드 함수보다는 일반(비프렌드) 함수를 선호하세요</h3>
<p>[14.8 수업]에서 우리는 멤버 함수보다는 비멤버 함수를 쓰는 게 좋다고 배웠습니다. 똑같은 이유로, 프렌드 함수보다는 일반(비프렌드) 함수를 쓰는 것이 더 안전하고 좋습니다.</p>
<p>예를 들어, 아래 코드처럼 친구가 데이터에 &#39;직접&#39; 접근하게 놔두면 어떻게 될까요? 나중에 변수 이름(<code>m_value</code>)을 살짝 바꾸기만 해도 <code>print()</code> 함수까지 찾아가서 고쳐야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Accumulator
{
private:
    int m_value { 0 }; // 만약 이 이름을 바꾼다면...

public:
    void add(int value) { m_value += value; } // 여기도 수정해야 하고...

    friend void print(const Accumulator&amp; accumulator);
};

void print(const Accumulator&amp; accumulator)
{
    std::cout &lt;&lt; accumulator.m_value; // 여기도 수정해야 합니다!
}

int main()
{
    Accumulator acc{};
    acc.add(5); // accumulator에 5를 더합니다.

    print(acc); // print() 비멤버 함수를 호출합니다.

    return 0;
}
</code></pre>
<p>이럴 때는 아래처럼 코드를 짜는 것이 훨씬 똑똑한 방법입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Accumulator
{
private:
    int m_value { 0 };

public:
    void add(int value) { m_value += value; }
    int value() const { return m_value; } // 안전하게 값을 읽어오는 접근 함수를 추가했습니다!
};

void print(const Accumulator&amp; accumulator) // 이제 Accumulator의 친구가 아닙니다.
{
    std::cout &lt;&lt; accumulator.value(); // 직접 접근하는 대신 접근 함수를 사용합니다.
}

int main()
{
    Accumulator acc{};
    acc.add(5); // accumulator에 5를 더합니다.

    print(acc); // print() 비멤버 함수를 호출합니다.

    return 0;
}
</code></pre>
<p>두 번째 예제에서는 <code>print()</code> 함수가 변수를 직접 건드리지 않고, <code>value()</code> 라는 접근 함수를 통해 안전하게 값을 가져옵니다. 이제 <code>Accumulator</code> 내부 구조가 어떻게 바뀌든 <code>print()</code> 함수는 신경 쓸 필요가 전혀 없습니다!</p>
<blockquote>
<p>** 권장 사항 **
가능하고 합리적인 상황이라면, 함수를 프렌드로 만들기보다는 일반 함수로 구현하는 것을 우선적으로 고려하세요.</p>
</blockquote>
<p>다만, 뻔한 기능이라고 해서 기존 클래스에 <code>public</code> 함수를 무작정 자꾸 추가하는 것은 조심해야 합니다. 코드가 지저분해지고 복잡해질 수 있거든요. 위의 <code>Accumulator</code> 예제처럼 누적된 값을 가져오는 단순한 접근 함수를 하나 만드는 것은 아주 훌륭한 선택입니다. 하지만 상황이 너무 복잡해서 새로운 접근 함수를 끝도 없이 만들어야 한다면, 차라리 깔끔하게 프렌드를 쓰는 편이 낫습니다.</p>
<hr>
<h2 id="159--프렌드-클래스와-프렌드-멤버-함수">15.9 — 프렌드 클래스와 프렌드 멤버 함수</h2>
<h3 id="프렌드-클래스">프렌드 클래스</h3>
<p><strong>프렌드 클래스(Friend class)</strong> 란 다른 클래스의 숨겨진 정보(<code>private</code>이나 <code>protected</code> 멤버)에 자유롭게 접근할 수 있는 특별한 클래스입니다. 진짜 친한 친구끼리 비밀을 공유하는 것과 같다고 생각하면 이해하기 쉬울 거예요!</p>
<p>예제를 한번 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Storage
{
private:
    int m_nValue {};
    double m_dValue {};

public:
    Storage(int nValue, double dValue)
       : m_nValue { nValue }, m_dValue { dValue }
    { }

    // Display 클래스를 Storage의 프렌드(친구)로 만듭니다.
    friend class Display;
};

class Display
{
private:
    bool m_displayIntFirst {};

public:
    Display(bool displayIntFirst)
         : m_displayIntFirst { displayIntFirst }
    {
    }

    // Display가 Storage의 프렌드이기 때문에, Display는 Storage의 숨겨진(private) 멤버에 접근할 수 있습니다.
    void displayStorage(const Storage&amp; storage)
    {
        if (m_displayIntFirst)
            std::cout &lt;&lt; storage.m_nValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_dValue &lt;&lt; &#39;\n&#39;;
        else // double 값을 먼저 출력합니다.
            std::cout &lt;&lt; storage.m_dValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_nValue &lt;&lt; &#39;\n&#39;;
    }

    void setDisplayIntFirst(bool b)
    {
         m_displayIntFirst = b;
    }
};

int main()
{
    Storage storage { 5, 6.7 };
    Display display { false };

    display.displayStorage(storage);

    display.setDisplayIntFirst(true);
    display.displayStorage(storage);

    return 0;
}
</code></pre>
<p><code>Display</code> 클래스가 <code>Storage</code>의 친구(프렌드)이기 때문에, <code>Display</code>는 자기가 가진 <code>Storage</code> 객체의 숨겨진 정보에 얼마든지 접근할 수 있습니다.
이 프로그램을 실행하면 다음과 같은 결과가 나옵니다.</p>
<blockquote>
<p>6.7 5
5 6.7</p>
</blockquote>
<p>프렌드 클래스에 대해 몇 가지 더 알아두면 좋은 점이 있어요.</p>
<p>첫째, <code>Display</code>가 <code>Storage</code>의 친구라고 해도, <code>Storage</code> 객체의 <code>*this</code> 포인터(자기 자신을 가리키는 포인터)에는 접근할 수 없어요. (<code>*this</code>는 사실 함수 매개변수처럼 숨겨져 전달되는 것이기 때문이죠.)</p>
<p>둘째, 프로그래밍 세계에서 우정은 <strong>일방통행</strong> 일 수 있습니다! <code>Display</code>가 <code>Storage</code>의 친구라고 해서, 반대로 <code>Storage</code>도 자동으로 <code>Display</code>의 친구가 되는 건 아니에요. 서로 친구가 되려면 양쪽 모두 서로를 프렌드로 선언해야 합니다. <em>(글쓴이의 말: 현실의 짝사랑 같아서 조금 슬프게 들렸다면 미안해요!)</em></p>
<p>셋째, 우정은 <strong>건너뛰지 않습니다</strong>. A가 B의 친구이고, B가 C의 친구라고 해서 A와 C가 자동으로 친구가 되는 건 아니에요.</p>
<p><strong>심화 내용:</strong>
우정은 <strong>상속되지도 않습니다</strong>. A가 B를 친구로 만들었다고 해서, B의 자식 클래스들까지 자동으로 A의 친구가 되지는 않아요.</p>
<p>프렌드 클래스를 선언하는 것은 컴파일러에게 &quot;이런 클래스가 나중에 나올 거야&quot;라고 미리 알려주는 <strong>전방 선언(forward declaration)</strong> 역할도 같이 합니다. 그래서 따로 미리 선언해둘 필요가 없어요. 위 예제에서 <code>friend class Display;</code>라고 쓴 것은 &quot;Display를 내 친구로 할게&quot;라는 뜻인 동시에 &quot;Display라는 클래스가 존재해&quot;라고 미리 알려주는 역할을 완벽하게 해냅니다.</p>
<hr>
<h3 id="프렌드-멤버-함수">프렌드 멤버 함수</h3>
<p>클래스 전체를 통째로 친구로 만드는 대신, <strong>특정 함수 딱 하나만</strong> 친구로 만들 수도 있습니다. 일반 함수를 친구로 만들 때와 방법은 비슷하지만, 대신 그 멤버 함수의 이름을 정확히 적어주면 됩니다.</p>
<p>하지만 막상 해보면 생각보다 조금 까다로울 수 있어요. 이전 예제를 조금 바꿔서 <code>Display::displayStorage</code> 함수 딱 하나만 프렌드 멤버 함수로 만들어 볼게요. 아마 처음에는 아래처럼 시도해 볼 겁니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Display; // Display 클래스에 대한 전방 선언 (미리 알려줌)

class Storage
{
private:
    int m_nValue {};
    double m_dValue {};

public:
    Storage(int nValue, double dValue)
        : m_nValue { nValue }, m_dValue { dValue }
    {
    }

    // Display::displayStorage 멤버 함수를 Storage 클래스의 프렌드로 만듭니다.
    friend void Display::displayStorage(const Storage&amp; storage); // 에러: Storage는 아직 Display 클래스의 전체 모습을 보지 못했습니다.
};

class Display
{
private:
    bool m_displayIntFirst {};

public:
    Display(bool displayIntFirst)
        : m_displayIntFirst { displayIntFirst }
    {
    }

    void displayStorage(const Storage&amp; storage)
    {
        if (m_displayIntFirst)
            std::cout &lt;&lt; storage.m_nValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_dValue &lt;&lt; &#39;\n&#39;;
        else // double 값을 먼저 출력합니다.
            std::cout &lt;&lt; storage.m_dValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_nValue &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Storage storage { 5, 6.7 };
    Display display { false };
    display.displayStorage(storage);

    return 0;
}
</code></pre>
<p>하지만 안타깝게도 이 코드는 작동하지 않습니다. 특정 멤버 함수 하나만 친구로 만들려면, 컴파일러는 단순히 &quot;이름만 미리 아는 것(전방 선언)&quot;으로는 부족하고, <strong>그 함수가 속한 클래스의 전체 모습(전체 정의)</strong> 을 꼼꼼히 확인해야 하거든요. 위 코드에서는 <code>Storage</code>가 <code>Display</code>의 전체 모습을 아직 모르는 상태라서 에러가 발생합니다.</p>
<p>다행히 해결책은 쉽습니다! <code>Display</code> 클래스의 코드를 <code>Storage</code> 클래스보다 <strong>위로 올려주면</strong> 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Display
{
private:
    bool m_displayIntFirst {};

public:
    Display(bool displayIntFirst)
        : m_displayIntFirst { displayIntFirst }
    {
    }

    void displayStorage(const Storage&amp; storage) // 컴파일 에러: 컴파일러는 Storage가 무엇인지 모릅니다.
    {
        if (m_displayIntFirst)
            std::cout &lt;&lt; storage.m_nValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_dValue &lt;&lt; &#39;\n&#39;;
        else // double 값을 먼저 출력합니다.
            std::cout &lt;&lt; storage.m_dValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_nValue &lt;&lt; &#39;\n&#39;;
    }
};

class Storage
{
private:
    int m_nValue {};
    double m_dValue {};

public:
    Storage(int nValue, double dValue)
        : m_nValue { nValue }, m_dValue { dValue }
    {
    }

    // Display::displayStorage 멤버 함수를 Storage 클래스의 프렌드로 만듭니다.
    friend void Display::displayStorage(const Storage&amp; storage); // 이제 문제없습니다.
};

int main()
{
    Storage storage { 5, 6.7 };
    Display display { false };
    display.displayStorage(storage);

    return 0;
}
</code></pre>
<p>앗, 그런데 이번엔 다른 문제가 생겼네요. <code>Display</code>를 위로 올렸더니, 그 안에 있는 <code>displayStorage()</code> 함수가 사용하는 매개변수 <code>Storage</code>를 컴파일러가 못 알아보게 된 거예요. 순서를 다시 원상복구하면 아까 문제가 다시 생기니 그럴 수도 없고 난감하죠.</p>
<p>하지만 걱정 마세요! 이 문제도 간단한 두 단계를 거쳐 해결할 수 있습니다.</p>
<ol>
<li>먼저 <code>class Storage;</code>라고 <strong>전방 선언</strong> 을 해줍니다. 그러면 컴파일러가 &quot;아, Storage라는 게 나중에 나오는구나&quot; 하고 안심합니다.</li>
<li>그 다음, <code>Display::displayStorage()</code> 함수의 진짜 알맹이(정의 부분)를 클래스 밖으로 빼서, <code>Storage</code> 클래스의 코드가 다 끝난 <strong>맨 밑으로</strong> 옮겨줍니다.</li>
</ol>
<p>코드로 보면 다음과 같습니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Storage; // Storage 클래스에 대한 전방 선언

class Display
{
private:
    bool m_displayIntFirst {};

public:
    Display(bool displayIntFirst)
        : m_displayIntFirst { displayIntFirst }
    {
    }

    void displayStorage(const Storage&amp; storage); // 여기서 참조(reference)를 사용하기 위해 Storage에 대한 전방 선언이 필요합니다.
};

class Storage // Storage 클래스의 전체 정의
{
private:
    int m_nValue {};
    double m_dValue {};

public:
    Storage(int nValue, double dValue)
        : m_nValue { nValue }, m_dValue { dValue }
    {
    }

    // Display::displayStorage 멤버 함수를 Storage 클래스의 프렌드로 만듭니다.
    // (displayStorage가 멤버이므로) Display 클래스의 전체 정의를 보아야 합니다.
    friend void Display::displayStorage(const Storage&amp; storage);
};

// 이제 Display::displayStorage를 정의할 수 있습니다.
// (우리가 Storage의 멤버에 접근하므로) Storage 클래스의 전체 정의를 보아야 합니다.
void Display::displayStorage(const Storage&amp; storage)
{
    if (m_displayIntFirst)
        std::cout &lt;&lt; storage.m_nValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_dValue &lt;&lt; &#39;\n&#39;;
    else // double 값을 먼저 출력합니다.
        std::cout &lt;&lt; storage.m_dValue &lt;&lt; &#39; &#39; &lt;&lt; storage.m_nValue &lt;&lt; &#39;\n&#39;;
}

int main()
{
    Storage storage { 5, 6.7 };
    Display display { false };
    display.displayStorage(storage);

    return 0;
}
</code></pre>
<p>드디어 모든 것이 완벽하게 작동합니다!</p>
<ul>
<li>맨 위의 <code>Storage</code> 전방 선언 덕분에 <code>Display</code> 안에서 함수를 문제없이 선언할 수 있었습니다.</li>
<li><code>Display</code>의 전체 코드가 먼저 나왔기 때문에, <code>Storage</code> 안에서 해당 함수를 친구로 지정할 수 있었습니다.</li>
<li>마지막으로 <code>Storage</code> 전체 코드가 나온 뒤에 함수의 알맹이를 작성했기 때문에, 함수 안에서 <code>Storage</code>의 숨겨진 정보들에 제대로 접근할 수 있게 된 거죠.</li>
</ul>
<p>조금 복잡하게 느껴진다면 위 코드의 주석들을 천천히 다시 읽어보세요. 핵심은 <strong>단순히 이름만 언급할 때는 &#39;전방 선언&#39;만으로 충분하지만, 그 클래스 안의 구체적인 내용(멤버)에 접근하려면 반드시 &#39;클래스 전체 정의&#39;를 컴파일러가 미리 봐야 한다</strong> 는 것입니다.</p>
<p>&quot;이거 순서 맞추기 너무 골치 아픈데요?&quot; 라고 생각하셨나요? 맞아요, 골치 아픈 게 정상입니다! 다행히 이런 복잡한 춤을 추는 이유는 우리가 모든 코드를 <strong>하나의 파일</strong> 안에 다 넣으려고 했기 때문이에요.</p>
<p>가장 좋은 해결책은 각 클래스의 틀을 <strong>헤더 파일(.h)</strong> 에 따로따로 나누어 담고, 실제 함수의 내용들은 각각의 <strong>.cpp 파일</strong> 에 따로 모아두는 것입니다. 그렇게 파일들을 깔끔하게 나누면 이런 복잡한 순서 고민을 전혀 할 필요가 없답니다!</p>
<hr>
<h2 id="1510--참조-한정자">15.10 — 참조 한정자</h2>
<blockquote>
<p><strong>작성자의 메모</strong> 
이번 레슨이 &#39;선택 사항&#39;이라는 점을 알려드립니다. 가볍게 쭉 읽어보면서 이런 개념이 있구나 하고 익숙해지는 것을 추천하지만, 앞으로 나올 다른 레슨들을 배우기 위해 여기서 완벽하게 다 이해해야 할 필요는 없습니다.</p>
</blockquote>
<p>이전 레슨인 &#39;14.7 - 데이터 멤버에 대한 참조를 반환하는 멤버 함수&#39;에서, 클래스 안의 데이터를 &#39;참조(reference)&#39;로 반환하는 함수를 쓸 때 주의할 점을 배웠습니다. 함수를 호출하는 본체(암시적 객체)가 잠깐 생겼다 사라지는 임시 객체인 <strong>우측값(rvalue)</strong> 이라면 이 방식이 아주 위험해질 수 있다고 했었죠. 간단히 다시 복습해 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name{};

public:
    Employee(std::string_view name)
        : m_name { name }
    {}

    const std::string&amp; getName() const { return m_name; } // getter는 const 참조로 반환합니다.
};

// createEmployee() 함수는 Employee 객체를 &#39;값&#39;으로 반환합니다 (즉, 반환된 값은 우측값/임시 객체입니다)
Employee createEmployee(std::string_view name)
{
    Employee e { name };
    return e;
}

int main()
{
    // 케이스 1: 괜찮음: 우측값 클래스 객체의 멤버에 대한 반환된 참조를 같은 줄(표현식) 안에서 바로 사용합니다.
    std::cout &lt;&lt; createEmployee(&quot;Frank&quot;).getName() &lt;&lt; &#39;\n&#39;;

    // 케이스 2: 나쁨: 우측값 클래스 객체의 멤버에 대한 반환된 참조를 나중에 쓰려고 변수에 저장합니다.
    const std::string&amp; ref { createEmployee(&quot;Garbo&quot;).getName() }; // createEmployee()의 반환값이 파괴될 때 참조(ref)는 허공을 가리키는 댕글링(dangling) 참조가 됩니다.
    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 정의되지 않은 동작 (미정의 동작)

    return 0;
}
</code></pre>
<p>케이스 2를 보면, <code>createEmployee(&quot;Garbo&quot;)</code> 가 만들어낸 우측값(임시 객체)은 <code>ref</code> 변수를 초기화한 직후에 바로 메모리에서 파괴되어 버립니다. 그래서 <code>ref</code> 는 방금 파괴되어서 사라진 데이터를 멍하니 가리키고 있게 되죠. 그 이후에 <code>ref</code> 를 사용하려고 하면 프로그램이 꼬여버리는 <strong>미정의 동작(undefined behavior)</strong> 이 발생합니다.</p>
<p>여기서 약간의 딜레마가 생깁니다.</p>
<p>만약 <code>getName()</code> 함수를 <strong>값으로 반환(return by value)</strong> 하게 만들면, 객체가 잠깐 나타났다 사라지는 <strong>우측값(rvalue)</strong> 일 때는 안전합니다. 하지만 가장 흔한 상황인 일반 변수, 즉 <strong>좌측값(lvalue)</strong> 일 때는 굳이 필요 없는 무거운 복사 작업이 일어나게 됩니다.</p>
<p>반대로 <code>getName()</code> 함수를 <strong>const 참조로 반환(return by const reference)</strong> 하게 만들면, 문자열을 복사할 필요가 없어서 아주 효율적입니다. 하지만 객체가 임시로 쓰이는 <strong>우측값(rvalue)</strong> 일 때 잘못 사용하면 위에서 본 것처럼 치명적인 미정의 동작을 일으킬 수 있죠.</p>
<p>보통 이런 멤버 함수들은 일반적인 변수 형태인 좌측값 객체에서 훨씬 더 자주 호출됩니다. 그래서 C++에서는 일반적으로 &#39;const 참조로 반환&#39;하는 방식을 선택하고, 대신 우측값(임시 객체)일 때는 참조를 변수에 저장해서 잘못 쓰는 일이 없도록 프로그래머가 스스로 조심하는 방법을 관행적으로 사용합니다.</p>
<hr>
<h3 id="참조-한정자">참조 한정자</h3>
<p>위에서 살펴본 문제의 근본적인 원인은, 하나의 함수(<code>getName</code>)가 두 가지 다른 상황(좌측값일 때와 우측값일 때)을 모두 완벽하게 처리해야 한다는 점입니다. 한 상황에 가장 좋은 방법이 다른 상황에서는 별로 좋지 않으니까요.</p>
<p>이 문제를 해결하기 위해 C++11에서는 <strong>참조 한정자(ref-qualifier)</strong> 라는 잘 알려지지 않은 멋진 기능을 도입했습니다. 이 기능을 사용하면 함수를 호출하는 객체가 좌측값인지 우측값인지에 따라 함수를 오버로딩(따로 정의)할 수 있습니다. 즉, 좌측값을 위한 <code>getName()</code> 과 우측값을 위한 <code>getName()</code> 두 가지 버전을 따로 만들 수 있는 것이죠!</p>
<p>먼저, 참조 한정자가 없는 원래의 <code>getName()</code> 함수를 보겠습니다.</p>
<pre><code class="language-cpp">const std::string&amp; getName() const { return m_name; } // 좌측값과 우측값 암시적 객체 모두에서 호출 가능합니다.
</code></pre>
<p>이 함수에 참조 한정자를 붙이려면, 좌측값 전용 함수에는 뒤에 <code>&amp;</code> 기호를 붙이고, 우측값 전용 함수에는 뒤에 <code>&amp;&amp;</code> 기호를 붙여주면 됩니다.</p>
<pre><code class="language-cpp">const std::string&amp; getName() const &amp;  { return m_name; } // &amp; 한정자는 좌측값 객체일 때만 이 함수를 실행하게 하며, 참조로 반환합니다.
std::string        getName() const &amp;&amp; { return m_name; } // &amp;&amp; 한정자는 우측값 객체일 때만 이 함수를 실행하게 하며, 값으로 복사해서 반환합니다.
</code></pre>
<p>이 두 함수는 엄연히 다른 오버로딩 함수이기 때문에, <strong>반환 타입(return type)</strong> 도 다르게 설정할 수 있습니다! 좌측값용 함수는 효율성을 위해 &#39;const 참조&#39;로 반환하고, 우측값용 함수는 안전성을 위해 &#39;값&#39;으로 반환하도록 만든 것입니다.</p>
<p>아래는 전체 적용 예시입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name{};

public:
    Employee(std::string_view name)
        : m_name { name }
    {}

    const std::string&amp; getName() const &amp;  { return m_name; } // &amp; 한정자: 함수를 좌측값 객체에서만 실행하도록 제한 (참조 반환)
    std::string        getName() const &amp;&amp; { return m_name; } // &amp;&amp; 한정자: 함수를 우측값 객체에서만 실행하도록 제한 (값 반환)
};

// createEmployee()는 Employee를 값으로 반환합니다 (즉, 반환된 값은 우측값입니다)
Employee createEmployee(std::string_view name)
{
    Employee e { name };
    return e;
}

int main()
{
    Employee joe { &quot;Joe&quot; };
    std::cout &lt;&lt; joe.getName() &lt;&lt; &#39;\n&#39;; // Joe는 좌측값이므로, std::string&amp; getName() &amp; 버전이 호출됩니다 (참조 반환)

    std::cout &lt;&lt; createEmployee(&quot;Frank&quot;).getName() &lt;&lt; &#39;\n&#39;; // Frank는 우측값(임시 객체)이므로, std::string getName() &amp;&amp; 버전이 호출됩니다 (복사본 반환)

    return 0;
}
</code></pre>
<p>이렇게 하면 좌측값일 때는 성능(속도)을 챙길 수 있고, 우측값일 때는 안전성을 챙길 수 있습니다.</p>
<hr>
<h3 id="심화-학습">심화 학습</h3>
<p>위에서 만든 우측값용 <code>getName()</code> 함수는 복사가 일어나기 때문에 성능 면에서 약간 아쉬울 수 있습니다. 어차피 임시 객체(const가 아닌 우측값)는 이 코드가 끝나면 사라질 텐데, 굳이 무겁게 복사할 필요가 있을까요? 그래서 단순히 복사하는 대신, 멤버를 <strong>이동(move)</strong> 시키도록 <code>std::move</code> 를 사용할 수도 있습니다.</p>
<p>이를 위해 const가 아닌 우측값을 위한 getter 함수를 아래처럼 하나 더 추가할 수 있습니다.</p>
<pre><code class="language-cpp">// 객체가 const가 아닌 우측값이라면, std::move를 사용해서 m_name을 이동(move)시킵니다.
std::string getName() &amp;&amp; { return std::move(m_name); }
</code></pre>
<p>이 함수는 기존의 const 우측값 getter와 함께 쓸 수도 있고, 그냥 이 함수만 단독으로 쓸 수도 있습니다 (const 우측값은 현실에서 거의 쓰이지 않기 때문입니다). <code>std::move</code> 에 대해서는 나중에 레슨 22.4에서 자세히 다룰 예정입니다.</p>
<hr>
<h3 id="참조-한정-멤버-함수에-대한-몇-가지-참고-사항">참조 한정 멤버 함수에 대한 몇 가지 참고 사항</h3>
<p>첫째, 하나의 함수 이름에 대해 <strong>참조 한정자가 없는 함수</strong> 와 <strong>참조 한정자가 있는 함수</strong> 를 섞어서 같이 쓸 수는 없습니다. 둘 중 하나의 방식만 선택해야 합니다.</p>
<p>둘째, C++의 일반적인 문법에서 const 좌측값 참조가 우측값을 받아줄 수 있는 것처럼, 만약 <code>const &amp;</code> (const 좌측값 한정) 함수 하나만 만들어두면, 좌측값이든 우측값이든 가리지 않고 다 이 함수를 호출할 수 있습니다.</p>
<p>셋째, <code>= delete</code> 키워드를 사용해서 특정 한정자 함수를 강제로 삭제(사용 금지)할 수도 있습니다. 예를 들어, <code>&amp;&amp;</code> (우측값 한정) 버전을 지워버리면, 우측값(임시 객체)으로는 아예 이 함수를 호출하지 못하도록 원천 차단할 수 있습니다.</p>
<h3 id="그럼-왜-참조-한정자-사용을-추천하지-않을까요">그럼 왜 참조 한정자 사용을 추천하지 않을까요?</h3>
<p>참조 한정자는 꽤 깔끔한 기능 같아 보이지만, 이런 식으로 사용하는 데에는 몇 가지 뚜렷한 단점이 있습니다.</p>
<ul>
<li>참조를 반환하는 모든 getter 함수마다 일일이 우측값용 오버로딩을 추가하면 클래스 코드가 너무 지저분해집니다. 별로 자주 일어나지도 않고 좋은 코딩 습관으로 충분히 막을 수 있는 문제를 예방하겠다고 말이죠.</li>
<li>우측값 함수를 값 반환으로 만들면, 굳이 복사나 이동을 하지 않아도 안전했던 상황(예: 맨 위 코드의 &#39;케이스 1&#39;처럼 같은 줄에서 한 번 출력하고 마는 경우)에서도 쓸데없이 성능 비용(복사 비용)을 지불해야 합니다.</li>
</ul>
<p>게다가 이런 이유도 있습니다.</p>
<ul>
<li>대부분의 C++ 개발자들이 이 기능의 존재 자체를 잘 모릅니다 (그래서 오해를 사거나 비효율적으로 사용될 수 있습니다).</li>
<li>C++ 표준 라이브러리(Standard Library)도 이 기능을 거의 사용하지 않습니다.</li>
</ul>
<p>이러한 모든 이유로 인해, 저희는 참조 한정자를 <strong>모범 사례(best practice)</strong> 로 추천하지 않습니다. 대신, 데이터를 얻어오는 접근 함수(getter)가 반환한 결과는 그 자리에서 바로바로 사용하고, 반환된 참조를 변수에 저장해두고 나중에 또 쓰려는 <strong>행동은 피하는 것</strong> 이 가장 좋습니다.</p>
<hr>
<p>이번 15장 요약본도 초보자분들이 이해하기 쉽도록, 복잡한 전문 용어를 최대한 부드럽게 풀어서 번역해 드리겠습니다. 어려운 C++ 문법을 배우시느라 정말 고생 많으셨어요! 이전과 동일하게 굵은 글씨 띄어쓰기 규칙도 완벽하게 적용했습니다.</p>
<hr>
<h2 id="15x--15장-요약-및-퀴즈">15.x — 15장 요약 및 퀴즈</h2>
<ul>
<li>모든 (정적(static)이 아닌) 멤버 함수 안에는 <code>this</code> 라는 특별한 키워드가 숨어 있습니다. 이것은 현재 작업 중인 객체의 메모리 주소를 기억하는 변하지 않는 포인터(const pointer)입니다. 함수가 <code>*this</code> 를 참조(reference)로 반환하게 만들면, 한 줄의 코드에서 여러 개의 멤버 함수를 기차처럼 이어서 호출하는 <strong>메서드 체이닝(method chaining)</strong> 이 가능해집니다.</li>
<li>클래스를 정의할 때는 클래스 이름과 똑같은 이름의 헤더 파일(<code>.h</code>) 안에 넣는 것을 추천합니다. 접근 함수(getter/setter)나 내용이 텅 빈 생성자처럼 아주 단순한 멤버 함수들은 클래스 정의 안에 직접 적어 넣어도 괜찮습니다.</li>
<li>반면에 복잡하고 내용이 긴 멤버 함수들은 클래스와 이름이 같은 소스 파일(<code>.cpp</code>)에 따로 빼서 작성하는 것이 좋습니다.</li>
<li>클래스 안에서 또 다른 &#39;타입(type)&#39;을 만드는 것을 <strong>중첩 타입(nested type)</strong> 또는 <strong>멤버 타입(member type)</strong> 이라고 부릅니다. 타입의 별명을 지어주는 구문(타입 별칭)도 클래스 안에 중첩해서 쓸 수 있습니다.</li>
<li>클래스 템플릿(class template) 안에서 만들어진 멤버 함수는 그 클래스의 템플릿 매개변수를 그대로 쓸 수 있습니다. 하지만 클래스 템플릿 바깥에서 멤버 함수를 따로 정의하려면, 템플릿 매개변수를 다시 한 번 명시해 주어야 합니다. 그리고 이 함수들은 클래스 템플릿 정의 바로 아래에 (같은 파일 안에서) 작성해야 합니다.</li>
<li><strong>정적 멤버 변수(Static member variables)</strong> 는 프로그램이 끝날 때까지 살아있으며, 해당 클래스로 만든 모든 객체들이 똑같이 &#39;공유&#39;하는 변수입니다. 이 변수들은 객체를 단 하나도 만들지 않았을 때조차도 이미 존재하고 있습니다. 이 변수들을 사용할 때는 <code>클래스이름::멤버이름</code> 처럼 범위 지정 연산자(<code>::</code>)를 사용해서 접근하는 것이 좋습니다.</li>
<li>정적 멤버를 <code>inline</code> 으로 만들면, 굳이 밖으로 빼지 않고 클래스를 정의하는 중괄호 안에서 곧바로 초기화(기본값 설정)를 할 수 있습니다.</li>
<li><strong>정적 멤버 함수(Static member functions)</strong> 는 객체를 굳이 만들지 않아도 바로 호출할 수 있는 함수입니다. 이 함수들 안에는 특정 객체를 가리키는 <code>this</code> 포인터가 없기 때문에, 객체마다 다르게 가지는 일반(비정적) 데이터 멤버는 사용할 수 없습니다. 오직 정적 데이터만 사용할 수 있습니다.</li>
<li>클래스 안에서 <code>friend</code> 키워드를 사용하면, 다른 특정 클래스나 함수를 &quot;내 친구야!&quot;라고 컴파일러에게 알려줄 수 있습니다. <strong>프렌드(friend)</strong> 가 되면, 원래는 꽁꽁 숨겨져 있어서 밖에서 볼 수 없는 남의 클래스의 은닉된 정보(<code>private</code>, <code>protected</code> 멤버)에 마음껏 접근할 수 있는 특별한 권한을 얻습니다.</li>
<li><strong>프렌드 함수(friend function)</strong> 는 마치 자신이 그 클래스의 식구인 것처럼 은닉된 정보에 접근할 수 있는 함수입니다 (일반 함수이든 다른 클래스의 멤버 함수이든 상관없습니다).</li>
<li><strong>프렌드 클래스(friend class)</strong> 는 다른 클래스의 은닉된 정보에 자유롭게 접근할 수 있는 통째로 친구 맺은 클래스를 말합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 14 | 이전 챕터 리메이크 필요]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-14</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-14</guid>
            <pubDate>Thu, 26 Feb 2026 00:59:57 GMT</pubDate>
            <description><![CDATA[<h2 id="141--객체-지향-프로그래밍oop-소개">14.1 — 객체 지향 프로그래밍(OOP) 소개</h2>
<h3 id="절차적-프로그래밍">절차적 프로그래밍</h3>
<p>이전 레슨 1.3(객체와 변수 소개)에서 우리는 C++의 객체를 &quot;값을 저장하는 데 사용할 수 있는 메모리의 한 조각&quot;이라고 정의했습니다. 그리고 <strong>이름이 있는 객체를 우리는 변수</strong>라고 부릅니다.</p>
<p>지금까지 작성한 C++ 프로그램은 컴퓨터에 내리는 순차적인 명령들의 나열이었습니다.
이 명령들은 객체를 통해 데이터를 정의하고, 그 데이터에 대한 작업은 명령문과 표현식이 포함된 함수를 통해 수행합니다.</p>
<p>지금까지 우리는 <strong>절차적 프로그래밍</strong> 이라는 방식의 프로그래밍을 해왔습니다. 
절차적 프로그래밍에서는 프로그램의 논리를 구현하는 &quot;절차&quot;(C++에서는 함수라고 부름)를 만드는 데 중점을 둡니다. 우리는 데이터 객체를 이런 함수에 전달하고, 함수는 데이터에 작업을 수행한 뒤, 그 결과를 함수를 호출한 쪽으로 반환합니다.</p>
<p>절차적 프로그래밍에서는 함수와 그 함수가 다루는 데이터가 서로 완전히 분리되어 있습니다. 프로그래머는 원하는 결과를 만들어내기 위해 함수와 데이터를 직접 엮어주어야 합니다.
그 결과 코드는 다음과 같은 형태가 됩니다.</p>
<pre><code class="language-cpp">eat(you, apple); // 먹는다(당신이, 사과를)
</code></pre>
<p>이제 주변을 한번 둘러보세요. 책, 건물, 음식, 심지어 여러분 자신까지 모든 것이 다 객체입니다. 이러한 객체들은 두 가지 큰 특징을 가지고 있습니다. </p>
<ol>
<li>연관된 몇 가지 <strong>속성(Properties)</strong> (예: 무게, 색깔, 크기, 단단함, 모양 등)</li>
<li>보여줄 수 있는 몇 가지 <strong>동작(Behaviors)</strong> (예: 열리기, 다른 것을 뜨겁게 만들기 등)입니다. </li>
</ol>
<p>이런 속성과 동작은 서로 떼어낼 수 없습니다. 프로그래밍에서 속성은 객체로 표현되고, 동작은 함수로 표현됩니다. 하지만 절차적 프로그래밍은 속성(객체)과 동작(함수)을 따로 분리해 놓기 때문에, 현실 세계를 제대로 반영하지 못한다는 단점이 있습니다.</p>
<hr>
<h3 id="객체-지향-프로그래밍이란">객체 지향 프로그래밍이란?</h3>
<p><strong>객체 지향 프로그래밍(Object-oriented programming, 흔히 OOP로 줄여 부름)</strong> 에서는 속성과 잘 정의된 동작들을 모두 포함하는 사용자 정의 데이터 타입을 만드는 데 중점을 둡니다. </p>
<p>OOP에서 &quot;객체&quot;라는 말은 이러한 타입들로부터 우리가 찍어낼(인스턴스화할) 수 있는 결과물을 뜻합니다. 이 방식을 사용하면 코드가 다음과 같이 바뀝니다.</p>
<pre><code class="language-cpp">you.eat(apple); // 당신이.먹는다(사과를)
</code></pre>
<p>이렇게 하면 주체(you)가 누구인지, 어떤 동작이 실행되는지(eat()), 그리고 그 동작에 어떤 객체(apple)가 이용되는지 훨씬 명확해집니다.</p>
<p>속성과 동작이 더 이상 분리되어 있지 않기 때문에 객체를 모듈화하기가 훨씬 쉽습니다.
덕분에 프로그램을 작성하고 이해하기 쉬워지며, 코드를 재사용하기도 아주 좋아집니다.</p>
<p>또한 이런 객체들은 우리가 데이터와 어떻게 상호작용할지, 그리고 객체들이 서로 어떻게 상호작용할지 정의할 수 있게 해주어 데이터를 다루는 훨씬 직관적인 방법을 제공합니다.</p>
<p>이러한 객체를 만드는 방법은 다음 레슨에서 자세히 알아보겠습니다.</p>
<hr>
<h3 id="절차적-방식-vs-oop-방식-예제">절차적 방식 vs OOP 방식 예제</h3>
<p>다음은 동물의 이름과 다리 개수를 출력하는 짧은 프로그램입니다.
절차적 프로그래밍 스타일로 작성되었습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

enum AnimalType
{
    cat,
    dog,
    chicken,
};

constexpr std::string_view animalName(AnimalType type)
{
    switch (type)
    {
    case cat: return &quot;cat&quot;;
    case dog: return &quot;dog&quot;;
    case chicken: return &quot;chicken&quot;;
    default:  return &quot;&quot;;
    }
}

constexpr int numLegs(AnimalType type)
{
    switch (type)
    {
    case cat: return 4;
    case dog: return 4;
    case chicken: return 2;
    default:  return 0;
    }
}

int main()
{
    constexpr AnimalType animal{ cat };
    std::cout &lt;&lt; &quot;A &quot; &lt;&lt; animalName(animal) &lt;&lt; &quot; has &quot; &lt;&lt; numLegs(animal) &lt;&lt; &quot; legs\n&quot;;

    return 0;
}
</code></pre>
<p>이 프로그램에서 우리는 동물의 다리 개수를 구하거나 이름을 가져오는 등의 역할을 하는 함수들을 만들었습니다.</p>
<p>지금 당장은 잘 작동하지만, 만약 동물을 뱀으로 바꾸고 싶다면 어떻게 해야 할지 생각해 보세요. 코드에 뱀을 추가하려면 <code>AnimalType</code>, <code>numLegs()</code>, <code>animalName()</code>을 전부 고쳐야 합니다. 만약 코드가 훨씬 방대했다면 <code>AnimalType</code>을 사용하는 다른 모든 함수도 업데이트해야 할 것입니다. 수정해야 할 코드가 엄청나게 많아지고, 멀쩡하던 코드가 고장 날 위험도 커집니다.</p>
<p>이제 똑같은 출력 결과를 내는 동일한 프로그램을, <strong>OOP적인 사고방식을 조금 더 적용하여</strong> 작성해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

struct Cat
{
    std::string_view name{ &quot;cat&quot; };
    int numLegs{ 4 };
};

struct Dog
{
    std::string_view name{ &quot;dog&quot; };
    int numLegs{ 4 };
};

struct Chicken
{
    std::string_view name{ &quot;chicken&quot; };
    int numLegs{ 2 };
};

int main()
{
    constexpr Cat animal;
    std::cout &lt;&lt; &quot;a &quot; &lt;&lt; animal.name &lt;&lt; &quot; has &quot; &lt;&lt; animal.numLegs &lt;&lt; &quot; legs\n&quot;;

    return 0;
}
</code></pre>
<p>이 예제에서는 각 동물이 프로그램 내에서 고유한 타입으로 정의되어 있습니다. 그리고 그 타입이 해당 동물과 관련된 모든 것(이 경우엔 이름과 다리 개수 저장)을 스스로 관리합니다.</p>
<p>이제 아까처럼 동물을 뱀으로 업데이트하고 싶은 경우를 생각해 볼까요?</p>
<p>우리가 해야 할 일은 그저 <code>Snake</code> 타입을 하나 만들어서 <code>Cat</code> 대신 사용하는 것뿐입니다. 기존 코드는 거의 건드릴 필요가 없으므로, 이미 잘 작동하고 있는 코드를 망가뜨릴 위험이 크게 줄어듭니다.</p>
<p>물론 위에 제시된 고양이, 개, 닭 예제는 중복되는 부분이 많습니다.
각 동물이 완전히 똑같은 종류의 속성을 가지고 있으니까요.</p>
<p>이런 경우에는 공통적인 <code>Animal</code> 구조체를 하나 만들고 각 동물마다 인스턴스를 생성하는 것이 더 나을 수 있습니다. 하지만 다른 동물에게는 필요 없지만 닭에게만 필요한 새로운 속성
(예: 하루에 먹는 벌레의 수)을 추가하고 싶다면 어떨까요? </p>
<p>공통된 <code>Animal</code> 구조체를 쓴다면 모든 동물이 그 속성을 가져야만 합니다. 
반면 OOP 모델을 사용하면, 그 속성을 닭 객체에만 쏙 넣을 수 있습니다.</p>
<hr>
<h3 id="oop가-가져다주는-또-다른-이점들">OOP가 가져다주는 또 다른 이점들</h3>
<p>학교에서 프로그래밍 과제를 제출할 때는 한 번 제출하면 사실상 끝입니다.  교수님이나 조교가 코드를 실행해 보고 정답이 나오는지 확인한 뒤 점수를 매기고 나면, 여러분의 코드는 아마 버려질 것입니다.</p>
<p>하지만 다른 개발자들과 함께 쓰는 저장소나, 실제 사용자들이 쓰는 애플리케이션에 코드를 제출할 때는 상황이 전혀 다릅니다. 새로운 운영체제나 소프트웨어 업데이트가 여러분의 코드를 고장 낼 수도 있습니다. 사용자들이 여러분의 논리적 오류를 찾아낼 수도 있고, 비즈니스 파트너가 새로운 기능을 추가해 달라고 요구할 수도 있습니다. 다른 개발자들은 여러분의 코드를 망가뜨리지 않으면서 기능을 확장해야 합니다. 즉, 코드는 계속 진화해야 하며, 그 과정에서 시간과 골칫거리, 그리고 오류 발생을 최소화해야 합니다.</p>
<p>이런 문제를 해결하는 가장 좋은 방법은 코드를 최대한 모듈화하고 중복을 없애는 것입니다. 이를 위해 OOP는 상속, 캡슐화, 추상화, 다형성이라는 아주 유용한 무기들도 제공합니다.</p>
<p>우리는 앞으로 이 모든 개념이 무엇인지, 그리고 어떻게 코드를 덜 중복되게 만들고 수정 및 확장을 쉽게 해주는지 차근차근 배울 것입니다. 일단 OOP에 익숙해져서 감을 잡고 나면, 아마 다시는 순수 절차적 프로그래밍으로 돌아가고 싶지 않을 것입니다.</p>
<p>그렇다고 해서 OOP가 절차적 프로그래밍을 완전히 대체하는 것은 아닙니다. 코드가 복잡해질 때 그것을 잘 관리할 수 있도록 여러분의 도구 상자에 훌륭한 도구를 하나 더 추가하는 것이라고 보면 됩니다.</p>
<hr>
<h3 id="객체object라는-용어에-대하여">&quot;객체(Object)&quot;라는 용어에 대하여</h3>
<p>참고로 <strong>객체</strong> 라는 용어는 너무 여러 의미로 쓰이고 있어서 약간의 혼란을 줍니다. 전통적인 프로그래밍에서 객체는 단순히 &#39;값을 저장하는 메모리 공간&#39;을 뜻하며, 그게 다입니다.</p>
<p>하지만 객체 지향 프로그래밍에서 &quot;객체&quot;는 데이터를 저장하는 공간이면서 동시에 속성과 동작을 결합한 것을 의미합니다. 이 튜토리얼 시리즈에서는 전통적인 의미의 객체라는 뜻을 기본으로 사용하며, OOP의 객체를 콕 집어 말할 때는 <strong>클래스 객체(Class object)</strong> 라는 용어를 사용할 것입니다.</p>
<hr>
<h2 id="142--클래스-소개">14.2 — 클래스 소개</h2>
<p>이전 장에서는 구조체 <code>13.7 — 구조체, 멤버, 멤버 선택 소개</code> 에 대해 다루었으며, 구조체가 어떻게 여러 멤버 변수를 하나의 객체로 묶어 초기화하고 통째로 전달하는 데 유용한지 이야기했습니다. </p>
<p>즉, 구조체는 관련 데이터 값들을 저장하고 이동시키기 편리한 &#39;꾸러미&#39; 역할을 합니다.
다음 구조체를 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    int day{};
    int month{};
    int year{};
};

void printDate(const Date&amp; date)
{
    std::cout &lt;&lt; date.day &lt;&lt; &#39;/&#39; &lt;&lt; date.month &lt;&lt; &#39;/&#39; &lt;&lt; date.year; // 일/월/년 형식을 가정합니다
}

int main()
{
    Date date{ 4, 10, 21 }; // 집합체 초기화(aggregate initialization)를 사용하여 초기화합니다
    printDate(date);        // 구조체 전체를 함수에 전달할 수 있습니다

    return 0;
}
</code></pre>
<p>위 예제에서는 <code>Date</code> 객체를 생성한 후 날짜를 출력하는 함수에 전달합니다.
이 프로그램의 출력 결과는 다음과 같습니다.</p>
<pre><code class="language-text">4/10/21</code></pre>
<blockquote>
<p><strong>참고 삼아 말씀드립니다</strong>
이 튜토리얼에서 다루는 모든 구조체는 <strong>집합체</strong> 입니다.
집합체에 대해서는 <code>13.8 — 구조체 집합체 초기화</code> 강의에서 설명했습니다.</p>
</blockquote>
<p>구조체는 무척 유용하지만, 크고 복잡한 프로그램(특히 여러 개발자가 함께 작업하는 프로그램)을 만들 때는 몇 가지 단점 때문에 어려움을 겪을 수 있습니다.</p>
<hr>
<h3 id="클래스-불변성-문제">클래스 불변성 문제</h3>
<p>구조체의 가장 큰 문제점은 아마도 <strong>클래스 불변성</strong>을 명확하게 기록하고 강제할 효과적인 방법이 없다는 점일 것입니다. </p>
<p><code>9.6 — Assert와 static_assert</code> 강의에서 우리는 불변성을 &quot;어떤 구성 요소가 실행되는 동안 반드시 참이어야 하는 조건&quot;이라고 정의했습니다.</p>
<p>클래스 타입과 관련해서, <strong>클래스 불변성</strong>이란 객체가 평생 동안 유효한 상태를 유지하기 위해 항상 참이어야 하는 조건을 말합니다. 이 불변성이 깨진 객체는 <strong>유효하지 않은 상태</strong>에 있다고 말하며, 이런 객체를 계속 사용하면 예상치 못한 동작이나 미정의 동작이 발생할 수 있습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
클래스 불변성이 깨진 객체를 사용하면 예상치 못한 동작이나 미정의 동작이 발생할 수 있습니다.</p>
</blockquote>
<p>먼저 다음 구조체를 살펴봅시다.</p>
<pre><code class="language-cpp">struct Pair
{
    int first {};
    int second {};
};
</code></pre>
<p><code>first</code> 와 <code>second</code> 멤버는 각각 독립적으로 어떤 값이든 가질 수 있으므로, 
<code>Pair</code> 구조체에는 불변성이 없습니다. </p>
<p>이제 위와 거의 똑같아 보이는 다음 구조체를 살펴봅시다.</p>
<pre><code class="language-cpp">struct Fraction
{
    int numerator { 0 };
    int denominator { 1 };
};
</code></pre>
<p>수학에서 분모가 0인 분수는 정의되지 않는다는 것을 알고 있습니다. 분수의 값은 분자를 분모로 나눈 값인데, 0으로 나누는 것은 수학적으로 정의되지 않기 때문입니다.</p>
<p>따라서 우리는 <code>Fraction</code> 객체의 <code>denominator</code>(분모) 멤버가 절대 <code>0</code>이 되지 않도록 보장하고 싶습니다. 만약 0이 된다면 해당 <code>Fraction</code> 객체는 유효하지 않은 상태가 되며, 이 객체를 계속 사용하면 미정의 동작이 발생할 수 있습니다.</p>
<p>예를 들어보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Fraction
{
    int numerator { 0 };
    int denominator { 1 }; // 클래스 불변성: 절대 0이 되어서는 안 됩니다
};

void printFractionValue(const Fraction&amp; f)
{
     std::cout &lt;&lt; f.numerator / f.denominator &lt;&lt; &#39;\n&#39;;
}

int main()
{
    Fraction f { 5, 0 };   // 분모가 0인 Fraction 객체를 생성합니다
    printFractionValue(f); // 0으로 나누기 에러를 유발합니다

    return 0;
}
</code></pre>
<p>위 예제에서는 <code>Fraction</code> 의 불변성을 기록하기 위해 주석을 사용했습니다. 
또한 사용자가 초기값을 제공하지 않을 경우 분모가 1로 설정되도록 <strong>기본 멤버 초기화</strong>를 제공했습니다. 덕분에 사용자가 <code>Fraction</code> 객체를 값 초기화 하기로 선택한다면 객체가 유효한 상태를 유지할 수 있습니다. 여기까지는 꽤 괜찮은 시작입니다.</p>
<p>하지만 우리가 이 클래스 불변성을 명시적으로 어기는 것을 막을 방법은 없습니다.
<code>Fraction f</code> 를 생성할 때, 집합체 초기화를 사용해 분모를 0으로 <strong>명시적으로 초기화</strong>해 버렸습니다. 당장 문제가 발생하지는 않지만, 이제 우리 객체는 유효하지 않은 상태가 되었고, 이 객체를 계속 사용하면 예상치 못한 문제나 미정의 동작이 발생할 수 있습니다.</p>
<p>그리고 나중에 <code>printFractionValue(f)</code> 를 호출했을 때 정확히 그런 문제가 나타납니다. 프로그램은 &#39;0으로 나누기 에러&#39; 때문에 종료되어 버립니다.</p>
<blockquote>
<p><strong>참고로...</strong>
약간의 개선 방법으로는 <code>printFractionValue</code> 함수 몸체 맨 위에 <code>assert(f.denominator != 0);</code> 을 추가하는 것입니다. 이렇게 하면 코드의 의도가 더 명확해지고, 어떤 전제 조건이 위반되었는지 쉽게 알 수 있습니다. 하지만 실제 동작 면에서는 변하는 게 없습니다. </p>
</blockquote>
<p>우리는 이런 문제가 발생한 하류(잘못된 값이 사용될 때)가 아니라, <strong>문제의 근원</strong>(멤버가 처음 초기화되거나 잘못된 값이 할당될 때)에서 문제를 잡아내기를 원합니다.</p>
<p><code>Fraction</code> 예제는 비교적 단순하기 때문에, 유효하지 않은 <code>Fraction</code> 객체를 만들지 않도록 조심하는 게 그리 어렵지 않을 수 있습니다. 하지만 많은 구조체를 사용하거나, 멤버가 많은 구조체, 또는 멤버들 간에 복잡한 관계가 있는 구조체를 다루는 더 복잡한 코드베이스에서는 어떤 값들의 조합이 클래스 불변성을 깨뜨리는지 한눈에 파악하기 어려울 수 있습니다.</p>
<hr>
<h3 id="좀-더-복잡한-클래스-불변성">좀 더 복잡한 클래스 불변성</h3>
<p><code>Fraction</code> 의 클래스 불변성은 단순합니다. <code>denominator</code> 멤버가 0이 될 수 없다는 것이죠. 개념적으로 이해하기 쉽고 피하기도 크게 어렵지 않습니다.</p>
<p>클래스 불변성은 구조체의 멤버들이 <strong>서로 연관된 값을 가져야 할 때</strong> 더욱 큰 골칫거리가 됩니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;

struct Employee
{
    std::string name { };
    char firstInitial { }; // 항상 `name`의 첫 번째 문자(또는 `0`)를 가져야 합니다
};
</code></pre>
<p>위의 구조체에서 <code>firstInitial</code> 멤버에 저장된 문자 값은 항상 <code>name</code> 의 첫 번째 문자와 일치해야 합니다.</p>
<p><code>Employee</code> 객체가 초기화될 때 클래스 불변성을 유지하는 책임은 전적으로 사용자에게 있습니다. 만약 <code>name</code> 에 새로운 값이 할당된다면, 사용자는 <code>firstInitial</code> 도 함께 업데이트해야 합니다. <code>Employee</code> 객체를 사용하는 개발자는 이러한 연관성을 눈치채지 못할 수도 있고, 안다 하더라도 업데이트를 깜빡할 수 있습니다.</p>
<p><code>Employee</code> 객체를 생성하고 업데이트하는 데 도움을 주는 함수(항상 <code>name</code> 의 첫 번째 문자에서 <code>firstInitial</code> 을 설정하도록 보장하는 함수)를 작성하더라도, 우리는 여전히 사용자가 이 함수들의 존재를 알고 사용해 주기를 기대해야만 합니다.</p>
<p>간단히 말해서, 클래스 불변성을 유지하는 것을 객체 사용자에게 맡기는 것은 문제투성이 코드를 낳을 가능성이 높습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
클래스 불변성 유지를 객체 사용자에게 의존하면 문제가 발생할 가능성이 큽니다.</p>
</blockquote>
<p>이상적으로는 클래스 타입이 아예 유효하지 않은 상태가 될 수 없도록 만들거나, 만약 그런 상태가 된다면 (나중에 무작위로 미정의 동작이 발생하도록 내버려 두는 대신) 즉시 신호를 보내도록 코드를 튼튼하게 만들고 싶을 것입니다.</p>
<p>(집합체로서의) 구조체는 이 문제를 깔끔하게 해결하는 데 필요한 메커니즘을 갖추고 있지 않습니다.</p>
<hr>
<h3 id="클래스-소개">클래스 소개</h3>
<p>비야네 스트로스트룹은 C++를 개발할 때, 개발자들이 직관적으로 사용할 수 있는 
&#39;프로그램 정의 타입&#39; 기능을 도입하고 싶어 했습니다. </p>
<p>그는 또한 크고 복잡한 프로그램을 괴롭히는 흔한 함정과 유지보수의 어려움(앞서 언급한 클래스 불변성 문제 등)에 대한 깔끔한 해결책을 찾는 데 관심이 많았습니다.</p>
<p>다른 프로그래밍 언어(특히 최초의 객체 지향 프로그래밍 언어인 Simula)를 다뤄본 경험을 바탕으로, 비야네는 거의 모든 것에 사용할 수 있을 만큼 일반적이고 강력한 프로그램 정의 타입을 개발할 수 있다고 확신했습니다.</p>
<p>그는 Simula의 영향을 받아 이 타입을 <strong>클래스(class)</strong> 라고 불렀습니다.</p>
<p>구조체와 마찬가지로, <strong>클래스</strong>는 여러 가지 타입의 멤버 변수들을 많이 가질 수 있는 <strong>프로그램 정의 복합 타입</strong>입니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
기술적인 관점에서 보면 구조체와 클래스는 거의 똑같습니다. 따라서 구조체로 구현된 예제는 클래스로도 구현할 수 있고 그 반대도 가능합니다. 하지만 실용적인 관점에서 우리는 구조체와 클래스를 다르게 사용합니다.</p>
<p>구조체와 클래스의 기술적, 실용적인 차이점은 
<code>14.5 — public, private 멤버와 접근 지정자(access specifiers)</code> 강의에서 다룹니다.</p>
</blockquote>
<blockquote>
<p><strong>관련 내용</strong>
클래스가 불변성 문제를 어떻게 해결하는지는 
<code>14.8 — 데이터 은닉(캡슐화)의 이점</code> 강의에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="클래스-정의하기">클래스 정의하기</h3>
<p>클래스는 프로그램이 정의하는 데이터 타입이므로 <strong>사용하기 전에 반드시 정의</strong>되어야 합니다.
클래스를 정의하는 방법은 구조체와 비슷하지만, <code>struct</code> 대신 <code>class</code> 키워드를 사용합니다.</p>
<p>예를 들어, 다음은 간단한 직원 클래스를 정의한 것입니다.</p>
<pre><code class="language-cpp">class Employee
{
    int m_id {};
    int m_age {};
    double m_wage {};
};
</code></pre>
<blockquote>
<p><strong>관련 내용</strong>
클래스의 멤버 변수 이름 앞에 &quot;m_&quot;을 붙이는 이유는 
<code>14.5 — public, private 멤버와 접근 지정자</code> 강의에서 설명합니다.</p>
</blockquote>
<p>클래스와 구조체가 얼마나 비슷한지 보여주기 위해, 강의 첫 부분에서 보았던 것과 똑같은 동작을 하는 다음 프로그램을 살펴봅시다. </p>
<p>다만 이번에는 <code>Date</code> 가 구조체가 아니라 클래스입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Date       // struct를 class로 변경했습니다
{
public:          // 그리고 이 줄을 추가했습니다. 이를 &#39;접근 지정자(access specifier)&#39;라고 합니다
    int m_day{}; // 각 멤버 이름 앞에 &quot;m_&quot; 접두사를 추가했습니다
    int m_month{};
    int m_year{};
};

void printDate(const Date&amp; date)
{
    std::cout &lt;&lt; date.m_day &lt;&lt; &#39;/&#39; &lt;&lt; date.m_month &lt;&lt; &#39;/&#39; &lt;&lt; date.m_year;
}

int main()
{
    Date date{ 4, 10, 21 };
    printDate(date);

    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과는 다음과 같습니다.</p>
<pre><code class="language-text">4/10/21</code></pre>
<blockquote>
<p><strong>관련 내용</strong>
접근 지정자가 무엇인지는 다가오는 
<code>14.5 — public, private 멤버와 접근 지정자</code> 강의에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="c-표준-라이브러리의-대부분은-클래스입니다">C++ 표준 라이브러리의 대부분은 클래스입니다</h3>
<p>여러분은 이미 자신도 모르는 사이에 클래스 객체를 사용하고 있었을지도 모릅니다. <code>std::string</code> 과 <code>std::string_view</code> 모두 클래스로 정의되어 있습니다.</p>
<p>사실, 표준 라이브러리에서 별칭이 아닌 타입 대부분은 클래스로 정의되어 있습니다!</p>
<p>클래스는 진정한 C++의 심장이자 영혼입니다. 너무 기초적이고 중요해서 C++의 원래 이름이 &quot;클래스가 있는 C&quot;였을 정도입니다! 클래스에 익숙해지고 나면, C++ 프로그래밍 시간의 대부분을 클래스를 작성하고, 테스트하고, 사용하는 데 보내게 될 것입니다.</p>
<hr>
<h2 id="143--멤버-함수">14.3 — 멤버 함수</h2>
<p>레슨 <code>13.7(구조체, 멤버, 멤버 선택 소개)</code> 에서는 멤버 변수를 가질 수 있는 사용자 정의 타입인 구조체(struct)를 소개했습니다. 다음은 날짜를 저장하는 데 사용되는 구조체의 예시입니다.</p>
<pre><code class="language-cpp">struct Date
{
    int year {};
    int month {};
    int day {};
};
</code></pre>
<p>이제 이 날짜를 화면에 출력하고 싶다면(실제로 자주 하게 될 작업이죠), 이를 수행하는 함수를 따로 작성하는 것이 합리적입니다. 전체 프로그램 코드는 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    // 여기에 멤버 변수들이 있습니다
    int year {};
    int month {};
    int day {};
};

void print(const Date&amp; date)
{
    // 멤버 선택 연산자(.)를 사용하여 멤버 변수에 접근합니다
    std::cout &lt;&lt; date.year &lt;&lt; &#39;/&#39; &lt;&lt; date.month &lt;&lt; &#39;/&#39; &lt;&lt; date.day;
}

int main()
{
    Date today { 2020, 10, 14 }; // 구조체를 집합체(aggregate) 초기화합니다

    today.day = 16; // 멤버 선택 연산자(.)를 사용하여 멤버 변수에 접근합니다
    print(today);   // 일반적인 호출 방식을 사용하여 비멤버 함수를 호출합니다

    return 0;
}
</code></pre>
<p>이 프로그램은 다음과 같이 출력합니다.</p>
<pre><code class="language-text">2020/10/16</code></pre>
<hr>
<h3 id="속성과-동작의-분리">속성과 동작의 분리</h3>
<p>주변을 한번 둘러보세요. 책, 건물, 음식, 심지어 여러분 자신까지 모든 것이 객체입니다. 
현실 세계의 객체는 크게 두 가지 핵심 요소로 이루어져 있습니다.</p>
<ol>
<li>눈으로 볼 수 있는 <strong>속성</strong> (예: 무게, 색상, 크기, 단단함, 모양 등)</li>
<li>그 속성들을 바탕으로 할 수 있는 <strong>동작</strong> (예: 열리기, 다른 물건 부수기 등)</li>
</ol>
<p>현실에서 이러한 속성과 동작은 서로 뗄 수 없는 관계입니다.</p>
<p>프로그래밍에서는 이러한 <strong>속성을 변수로</strong>, <strong>동작을 함수로</strong> 표현합니다.</p>
<p>위의 <code>Date</code> 예제를 보면 속성 <code>Date</code> 의 멤버 변수와 그 속성을 사용하는 동작 <code>print()</code> 함수를 따로따로 정의했습니다. 우리는 오직 <code>print()</code> 함수의 매개변수인 <code>const Date&amp;</code> 만을 보고 <code>Date</code> 와 <code>print()</code> 가 서로 관련이 있다고 짐작할 뿐입니다.</p>
<p>물론 <code>Date</code> 와 <code>print()</code> 를 같은 네임스페이스 안에 묶어두어 &quot;이 둘은 한 세트입니다&quot;라고 명확히 할 수도 있지만, 그렇게 하면 프로그램에 이름과 네임스페이스 접두사가 더 많아져 코드가 복잡해집니다.</p>
<p>속성과 동작을 하나의 패키지처럼 묶어서 함께 정의할 수 있는 방법이 있다면 정말 좋을 것입니다.</p>
<hr>
<h3 id="멤버-함수">멤버 함수</h3>
<p>클래스 타입(구조체, 클래스, 공용체 포함)은 멤버 변수뿐만 아니라 자기 자신만의 함수도 가질 수 있습니다. 이렇게 클래스 타입에 속해 있는 함수를 <strong>멤버 함수</strong> 라고 부릅니다.</p>
<blockquote>
<p><strong>참고로...</strong>
Java나 C# 같은 다른 객체 지향 언어에서는 이를 <strong>메서드(method)</strong> 라고 부릅니다. C++에서는 &quot;메서드&quot;라는 용어를 잘 쓰지 않지만, 다른 언어를 먼저 배운 프로그래머들은 여전히 그 용어를 사용할 수도 있습니다.</p>
</blockquote>
<p>멤버 함수가 아닌 일반 함수들은 멤버 함수와 구분하기 위해 <strong>비멤버 함수</strong> 라고 부릅니다. 
위에서 작성했던 <code>print()</code> 함수는 비멤버 함수입니다.</p>
<blockquote>
<p><strong>저자의 노트</strong>
이번 레슨에서는 멤버 함수의 예를 보여주기 위해 구조체를 사용하지만, 여기서 설명하는 모든 내용은 클래스에도 똑같이 적용됩니다. 왜 구조체 대신 클래스를 사용하는지에 대해서는 다음 레슨에서 클래스와 멤버 함수 예제를 다룰 때 명확해질 것입니다.</p>
</blockquote>
<p>멤버 함수는 클래스 타입 <strong>정의 안에서 선언</strong>되어야 하며, 정의는 클래스 내부나 외부 어디서든 할 수 있습니다. 기억하시겠지만, 함수를 정의하는 것은 곧 선언하는 것이기도 하므로 클래스 안에서 멤버 함수를 정의하면 그것도 선언으로 인정됩니다.</p>
<p>지금은 내용을 단순하게 유지하기 위해, 멤버 함수를 클래스 정의 안에서 바로 작성해 보겠습니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
멤버 함수를 클래스 정의 외부에서 선언하고 정의하는 방법은 레슨 15.2(클래스와 헤더 파일)에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="멤버-함수-예제">멤버 함수 예제</h3>
<p>레슨 맨 처음에 있던 <code>Date</code> 예제를 다시 가져와서, <code>print()</code> 를 비멤버 함수에서 멤버 함수로 바꿔보겠습니다.</p>
<pre><code class="language-cpp">// 멤버 함수 버전
#include &lt;iostream&gt;

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() // print라는 이름의 멤버 함수를 정의합니다
    {
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // 구조체를 집합체(aggregate) 초기화합니다

    today.day = 16; // 멤버 선택 연산자(.)를 사용하여 멤버 변수에 접근합니다
    today.print();  // 멤버 선택 연산자(.)를 사용하여 멤버 함수에도 접근합니다

    return 0;
}
</code></pre>
<p>이 프로그램은 정상적으로 컴파일되며 이전과 똑같은 결과를 출력합니다.</p>
<pre><code class="language-text">2020/10/16</code></pre>
<p>비멤버 함수 예제와 멤버 함수 예제 사이에는 <strong>세 가지 중요한 차이점</strong>이 있습니다.</p>
<ol>
<li><code>print()</code> 함수를 선언(및 정의)하는 위치</li>
<li><code>print()</code> 함수를 호출하는 방법</li>
<li><code>print()</code> 함수 내부에서 멤버 변수에 접근하는 방법</li>
</ol>
<p>이제 각각을 차례대로 살펴보겠습니다.</p>
<blockquote>
<p><strong>멤버 함수는 클래스 타입 정의 안에 선언됩니다</strong>
비멤버 예제에서 <code>print()</code> 비멤버 함수는 <code>Date</code> 구조체 외부의 전역 공간에 정의되었습니다. 기본적으로 외부에서 접근할 수 있으므로, 다른 소스 파일에서도 호출할 수 있습니다.</p>
</blockquote>
<p>반면 멤버 예제에서 <code>print()</code> 멤버 함수는 <code>Date</code> 구조체 정의 안에서 선언 및 정의되었습니다. <code>print()</code> 가 <code>Date</code> 의 일부로 선언되었기 때문에, 컴파일러는 이 <code>print()</code> 가 <code>Date</code> 의 멤버 함수라는 것을 알게 됩니다.</p>
<blockquote>
</blockquote>
<p>클래스 정의 내부에 작성된 멤버 함수는 자동으로 <strong>인라인</strong> 처리되므로, 여러 파일에서 클래스를 포함하더라도 단일 정의 규칙(ODR)을 위반하는 에러가 발생하지 않습니다.</p>
<blockquote>
<p><strong>멤버 함수 호출하기 (그리고 암시적 객체)</strong>
비멤버 예제에서는 <code>print(today)</code> 처럼 함수 안에 <code>today</code> 를 직접(명시적으로) 집어넣어 호출했습니다.</p>
</blockquote>
<p>반면 멤버 예제에서는 <code>today.print()</code> 라고 호출합니다. 멤버 선택 연산자(<code>.</code>)를 사용하여 호출할 멤버 함수를 고르는 이 방식은 멤버 변수를 사용할 때
(<code>today.day = 16;</code>) 와 똑같아서 아주 자연스럽습니다.</p>
<blockquote>
</blockquote>
<p>모든 (정적이지 않은) 멤버 함수는 반드시 그 클래스 타입의 객체와 연결해서 호출해야 합니다. 여기서는 <code>today</code> 가 <code>print()</code> 를 호출하는 주체(객체)가 됩니다.</p>
<blockquote>
</blockquote>
<p>여기서 주목할 점은, 멤버 함수의 경우 <code>today</code> 를 괄호 안에 매개변수로 넘겨줄 필요가 없다는 것입니다. 멤버 함수를 호출한 객체는 멤버 함수 내부로 <strong>암시적으로</strong> 전달됩니다. 이런 이유로, 멤버 함수를 부른 주체 객체를 종종 <strong>암시적 객체</strong> 라고 부릅니다.</p>
<blockquote>
</blockquote>
<p>다시 말해, 우리가 <code>today.print()</code> 를 호출하면 <code>today</code> 가 암시적 객체가 되어 <code>print()</code> 멤버 함수 안으로 조용히 넘어가는 것입니다.</p>
<blockquote>
<p><strong>멤버 함수 내부에서 멤버에 접근할 때는 암시적 객체를 사용합니다</strong>
다시 비멤버 버전의 <code>print()</code> 를 살펴봅시다.</p>
</blockquote>
<pre><code class="language-cpp">// 비멤버 버전의 print
void print(const Date&amp; date)
{
    // 멤버 선택 연산자(.)를 사용하여 멤버 변수에 접근합니다
    std::cout &lt;&lt; date.year &lt;&lt; &#39;/&#39; &lt;&lt; date.month &lt;&lt; &#39;/&#39; &lt;&lt; date.day;
}</code></pre>
<blockquote>
</blockquote>
<p>이 버전은 <code>const Date&amp; date</code> 라는 참조 매개변수를 받습니다. 
함수 안에서는 이 <code>date</code> 를 통해서 <code>date.year</code>, <code>date.month</code> 처럼 멤버에 접근합니다. <code>print(today)</code> 라고 호출하면 <code>date</code> 가 <code>today</code> 와 연결되어 각각 <code>today.year</code> 등으로 쓰이게 됩니다.</p>
<blockquote>
</blockquote>
<p>이제 <code>print()</code> 멤버 함수의 모습을 다시 보겠습니다.</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">void print() // print라는 이름의 멤버 함수를 정의합니다
{
    std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
}</code></pre>
<blockquote>
</blockquote>
<p>멤버 예제에서는 앞에 아무것도 붙이지 않고 그냥 <code>year</code>, <code>month</code>, <code>day</code> 라고만 적었습니다.</p>
<blockquote>
</blockquote>
<p>멤버 함수 안에서 앞에 점(<code>.</code>)이 붙지 않은 멤버 변수 이름은 <strong>자동으로 암시적 객체의 변수</strong> 로 연결됩니다.</p>
<blockquote>
</blockquote>
<p>즉, <code>today.print()</code> 가 호출되면 <code>today</code> 가 암시적 객체가 되므로, 
함수 안의 <code>year</code>, <code>month</code>, <code>day</code> 는 알아서 <code>today.year</code>, <code>today.month</code>, <code>today.day</code> 의 값을 가리키게 되는 것입니다.</p>
<blockquote>
<p><strong>핵심 요약</strong>
비멤버 함수를 사용할 때는, 작업할 객체를 함수에 직접 던져주어야 하고 그 객체를 통해 명시적으로 변수를 꺼내 써야 합니다.
반면 멤버 함수를 사용할 때는, 객체가 알아서 함수로 전달되며 함수 안에서도 내 것처럼 자연스럽게 변수들을 꺼내 쓸 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="또-다른-멤버-함수-예제">또 다른 멤버 함수 예제</h3>
<p>조금 더 재미있는 멤버 함수 예제를 살펴보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

struct Person
{
    std::string name{};
    int age{};

    void kisses(const Person&amp; person)
    {
        std::cout &lt;&lt; name &lt;&lt; &quot; kisses &quot; &lt;&lt; person.name &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Person joe{ &quot;Joe&quot;, 29 };
    Person kate{ &quot;Kate&quot;, 27 };

    joe.kisses(kate);

    return 0;
}
</code></pre>
<p>이 코드는 다음과 같이 출력합니다.
<code>Joe kisses Kate</code></p>
<p>어떻게 작동하는지 살펴봅시다. 먼저 <code>joe</code> 와 <code>kate</code> 라는 두 사람을 만들었습니다.
그리고 <code>joe.kisses(kate)</code> 를 호출합니다.
여기서 <code>joe</code> 가 암시적 객체가 되고, <code>kate</code> 는 괄호 안의 명시적 인수로 전달됩니다.</p>
<p><code>kisses()</code> 함수 안으로 들어가면, 점(<code>.</code>)이 없는 <code>name</code> 은 암시적 객체인 <code>joe</code> 를 가리켜서 <code>joe.name</code> 이 됩니다. 반면 <code>person.name</code> 은 점(<code>.</code>)이 붙어 있고 매개변수 <code>person</code> (즉, <code>kate</code>)을 가리키므로 <code>kate.name</code> 이 됩니다.</p>
<blockquote>
<p><strong>핵심 요약</strong>
멤버 함수가 없었다면 <code>kisses(joe, kate)</code> 라고 썼을 것입니다. 하지만 멤버 함수를 사용하면 <code>joe.kisses(kate)</code> 라고 쓸 수 있습니다. 코드가 영어 문장처럼 훨씬 자연스럽게 읽히며, 누가 행동을 주도하고 누가 대상이 되는지 아주 명확해집니다.</p>
</blockquote>
<hr>
<h3 id="멤버-변수와-함수는-순서에-상관없이-정의할-수-있습니다">멤버 변수와 함수는 순서에 상관없이 정의할 수 있습니다</h3>
<p>C++ 컴파일러는 보통 코드를 위에서 아래로 읽으며 번역합니다. 그래서 일반적인 함수(비멤버)는 사용하기 전에 반드시 먼저 선언되어 있어야 컴파일러가 에러를 내지 않습니다.</p>
<pre><code class="language-cpp">int x()
{
    return y(); // 에러: y가 아직 선언되지 않아서 컴파일러가 뭔지 모릅니다
}

int y()
{
    return 5;
}
</code></pre>
<p>하지만 <strong>클래스 정의 안에서는 이 규칙이 적용되지 않습니다.</strong> 멤버 변수와 멤버 함수를 미리 선언하지 않아도 서로 부르고 사용할 수 있습니다. 즉, 여러분이 편한 순서대로 정의해도 괜찮다는 뜻입니다.</p>
<pre><code class="language-cpp">struct Foo
{
    int z() { return m_data; } // 데이터 멤버가 아래에 정의되어 있어도 접근할 수 있습니다
    int x() { return y(); }    // 멤버 함수가 아래에 정의되어 있어도 접근할 수 있습니다

    int m_data { y() };        // 심지어 기본 멤버 초기화에서도 작동합니다 (아래 경고 참조)
    int y() { return 5; }
};
</code></pre>
<blockquote>
<p><strong>경고</strong>
데이터 멤버(변수)들은 코드가 <strong>작성된 순서대로</strong> 초기화됩니다. 만약 어떤 변수를 초기화할 때 그보다 아래에 적힌 다른 변수를 끌어다 쓰면, 아직 초기화되지 않은 쓰레기값을 가져오게 되어 <strong>미정의 동작</strong> 이 발생할 수 있습니다.
따라서 기본 초기화 값을 설정할 때는 다른 멤버 변수를 가져다 쓰지 않는 것이 안전합니다.</p>
</blockquote>
<hr>
<h3 id="멤버-함수는-오버로딩할-수-있습니다">멤버 함수는 오버로딩할 수 있습니다</h3>
<p>일반 함수들처럼, 멤버 함수들도 서로 매개변수가 달라 구분만 가능하다면 같은 이름으로 여러 개를 만들 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

struct Date
{
    int year {};
    int month {};
    int day {};

    void print()
    {
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }

    void print(std::string_view prefix)
    {
        std::cout &lt;&lt; prefix &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

int main()
{
    Date today { 2020, 10, 14 };

    today.print(); // Date::print()를 호출합니다
    std::cout &lt;&lt; &#39;\n&#39;;

    today.print(&quot;The date is: &quot;); // Date::print(std::string_view)를 호출합니다
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 프로그램의 출력은 다음과 같습니다.</p>
<pre><code>2020/10/14
The date is: 2020/10/14
</code></pre><hr>
<h3 id="구조체와-멤버-함수">구조체와 멤버 함수</h3>
<p>과거 C 언어 시절의 구조체는 데이터만 담을 수 있었고 함수는 담을 수 없었습니다. 
하지만 C++를 만든 비야네 스트라우스트룹은 고민 끝에 C++의 구조체도 멤버 함수를 가질 수 있도록 규칙을 단순하고 일관성 있게 통일했습니다.</p>
<p>따라서 현대의 C++에서는 구조체에 멤버 함수를 넣는 것이 전혀 문제 되지 않습니다.
단, 나중에 배울 특별한 함수인 &#39;생성자(constructor)&#39;는 예외입니다. </p>
<p>구조체에 생성자를 넣으면 구조체의 장점인 <strong>집합체</strong> 성질을 잃어버리게 되기 때문입니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
멤버 함수는 구조체와 클래스 모두에서 자유롭게 사용할 수 있습니다.
단, 구조체는 본연의 성질(집합체)을 잃지 않도록 생성자 멤버 함수를 만들지 않는 것이 좋습니다.</p>
</blockquote>
<hr>
<h3 id="데이터-멤버가-없는-클래스-타입">데이터 멤버가 없는 클래스 타입</h3>
<p>변수는 하나도 없고 멤버 함수만 덩그러니 있는 클래스나 구조체를 만드는 것도 가능하긴 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Foo
{
    void printHi() { std::cout &lt;&lt; &quot;Hi!\n&quot;; }
};

int main()
{
    Foo f{};
    f.printHi(); // 호출하려면 객체가 필요합니다

    return 0;
}
</code></pre>
<p>하지만 데이터가 전혀 없다면 굳이 클래스 타입을 사용하는 것은 <strong>과도한 설계</strong>일 수 있습니다. 이런 경우에는 비멤버 함수를 묶어주는 <strong>네임스페이스</strong> 를 사용하는 것이 훨씬 좋습니다. 객체를 일부러 만들 필요도 없고, 데이터가 없다는 사실이 읽는 사람에게 더 명확해지기 때문입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

namespace Foo
{
    void printHi() { std::cout &lt;&lt; &quot;Hi!\n&quot;; }
}

int main()
{
    Foo::printHi(); // 객체가 필요하지 않습니다

    return 0;
}
</code></pre>
<hr>
<h2 id="144--const-클래스-객체와-const-멤버-함수">14.4 — Const 클래스 객체와 const 멤버 함수</h2>
<p>이전 5.1 레슨에서 <code>int</code>, <code>double</code>, <code>char</code> 등 기본 데이터 타입의 객체들을 <code>const</code> 키워드를 사용해 상수로 만드는 방법을 배웠습니다.</p>
<p><strong>모든 const 변수는 생성하는 시점에 반드시 초기화</strong>해야 합니다.</p>
<pre><code class="language-cpp">const int x;      // 컴파일 에러: 초기화되지 않음
const int y{};    // 성공: 값으로 초기화됨
const int z{ 5 }; // 성공: 리스트로 초기화됨
</code></pre>
<p>마찬가지로 클래스 타입의 객체 <code>struct</code>, <code>class</code>, <code>union</code> 역시 <code>const</code> 키워드를 사용해 상수로 만들 수 있습니다. 이러한 객체들도 반드시 생성 시점에 초기화되어야 합니다.</p>
<pre><code class="language-cpp">struct Date
{
    int year {};
    int month {};
    int day {};
};

int main()
{
    const Date today { 2020, 10, 14 }; // const 클래스 타입 객체

    return 0;
}
</code></pre>
<p>일반 변수와 마찬가지로, 클래스 타입 객체도 생성된 이후에 값이 변경되지 않도록 확실히 보장해야 할 때는 보통 <code>const</code> 또는 <code>constexpr</code> 로 선언하는 것이 좋습니다.</p>
<hr>
<h3 id="const-객체의-데이터-멤버-수정은-금지됩니다">const 객체의 데이터 멤버 수정은 금지됩니다</h3>
<p><strong>const 클래스 타입 객체</strong> 가 한 번 초기화되고 나면, 해당 객체의 데이터 멤버를 수정하려는 모든 시도는 허용되지 않습니다. 객체의 <strong>상수성</strong> 을 위반하기 때문입니다. </p>
<p>여기에는 (멤버가 public인 경우) 멤버 변수를 직접 바꾸는 것은 물론이고, 멤버 변수의 값을 변경하는 멤버 함수를 호출하는 것도 포함됩니다.</p>
<pre><code class="language-cpp">struct Date
{
    int year {};
    int month {};
    int day {};

    void incrementDay()
    {
        ++day;
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const 객체

    today.day += 1;        // 컴파일 에러: const 객체의 멤버를 수정할 수 없으므로
    today.incrementDay();  // 컴파일 에러: const 객체의 멤버를 수정하는 멤버 함수를 호출할 수 없으므로

    return 0;
}
</code></pre>
<hr>
<h3 id="const-객체는-비-const-멤버-함수를-호출할-수-없습니다">const 객체는 비-const 멤버 함수를 호출할 수 없습니다</h3>
<p>다음 코드가 컴파일 에러를 일으킨다는 사실에 조금 놀라실 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    int year {};
    int month {};
    int day {};

    void print()
    {
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const 객체

    today.print();  // 컴파일 에러: const가 아닌 멤버 함수는 호출할 수 없음

    return 0;
}
</code></pre>
<p><code>print()</code> 함수가 멤버 변수를 수정하려고 시도하지 않음에도 불구하고, <code>today.print()</code>를 호출하면 const 위반이 발생합니다. 그 이유는 <code>print()</code> 멤버 함수 자체가 const로 선언되지 않았기 때문입니다. </p>
<p>컴파일러는 <strong>const 객체에서 &#39;const가 아닌 멤버 함수&#39;를 호출하는 것을 허용하지 않습니다.</strong></p>
<hr>
<h3 id="const-멤버-함수">Const 멤버 함수</h3>
<p>위의 문제를 해결하려면 <code>print()</code>를 <strong>const 멤버 함수</strong> 로 만들어야 합니다. </p>
<p>const 멤버 함수란, 객체 자신을 수정하지 않으며 객체를 수정할 위험이 있는 
다른 비-const 멤버 함수도 호출하지 않겠다고 보장하는 함수입니다.</p>
<p><code>print()</code>를 <strong>const 멤버 함수</strong> 로 만드는 방법은 아주 간단합니다. 매개변수 목록 뒤, 그리고 함수 본문이 시작되기 전 사이에 <code>const</code> 키워드를 붙여주기만 하면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() const // 이제 const 멤버 함수가 됨
    {
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const 객체

    today.print();  // 성공: const 객체는 const 멤버 함수를 호출할 수 있음

    return 0;
}
</code></pre>
<p>위 예제에서 <code>print()</code>는 const 멤버 함수가 되었으므로, 
이제 <code>today</code>와 같은 const 객체에서도 문제없이 호출할 수 있습니다.</p>
<hr>
<h3 id="심화-학습자를-위해">심화 학습자를 위해</h3>
<p>클래스 정의 외부에 정의된 멤버 함수의 경우, 클래스 내부의 함수 <strong>선언부</strong>와 클래스 외부의 함수 <strong>정의부 양쪽 모두</strong>에 <code>const</code> 키워드를 붙여야 합니다. 
(이 부분에 대한 예제는 <code>15.2 레슨 -- 클래스와 헤더 파일</code> 에서 다룹니다.)</p>
<p>생성자는 객체의 멤버들을 초기화해야 하고 이 과정에서 멤버를 수정하게 되므로 const로 만들 수 없습니다. (생성자는 <code>14.9 레슨 -- 생성자 소개</code> 에서 다룹니다.)</p>
<p>const 멤버 함수가 데이터 멤버를 변경하려고 시도하거나 비-const 멤버 함수를 호출하려고 하면 컴파일 에러가 발생합니다.</p>
<pre><code class="language-cpp">struct Date
{
    int year {};
    int month {};
    int day {};

    void incrementDay() const // const로 만듦
    {
        ++day; // 컴파일 에러: const 함수는 멤버를 수정할 수 없음
    }
};

int main()
{
    const Date today { 2020, 10, 14 }; // const 객체

    today.incrementDay();

    return 0;
}
</code></pre>
<p>이 예제에서 <code>incrementDay()</code>는 const 멤버 함수로 선언되었지만 <code>day</code>의 값을 변경하려고 시도하므로 컴파일 에러를 일으킵니다.</p>
<p>단, const 멤버 함수라도 클래스 멤버가 아닌 변수(지역 변수나 함수 매개변수 등)를 수정하거나 멤버가 아닌 일반 함수를 호출하는 것은 평소처럼 가능합니다. </p>
<p><code>const</code>는 오직 &#39;클래스 멤버&#39;에만 적용됩니다.</p>
<blockquote>
<p><strong>핵심 요약</strong></p>
<ul>
<li>const 멤버 함수가 <strong>할 수 없는 일</strong> 
암시적 객체(자신) 수정하기, 비-const 멤버 함수 호출하기.</li>
</ul>
<ul>
<li>const 멤버 함수가 <strong>할 수 있는 일</strong> 
자기 자신이 아닌 다른 객체 수정하기, 다른 const 멤버 함수 호출하기, 클래스 소속이 아닌 일반 함수 호출하기.</li>
</ul>
</blockquote>
<hr>
<h3 id="const-멤버-함수는-일반비-const-객체에서도-호출될-수-있습니다">const 멤버 함수는 일반(비-const) 객체에서도 호출될 수 있습니다</h3>
<p>const 멤버 함수는 일반적인 비-const 객체에서도 호출이 가능합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() const // const 함수
    {
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // 비-const (일반) 객체

    today.print();  // 성공: 비-const 객체에서도 const 멤버 함수 호출 가능

    return 0;
}
</code></pre>
<p>const 멤버 함수는 const 객체와 비-const 객체 모두에서 호출될 수 있습니다. 따라서 멤버 함수가 객체의 상태를 <strong>변경하지 않는다면, 그 함수는 무조건 const로 선언</strong>하는 것이 좋습니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
객체의 상태를 변경하지 않는 (그리고 앞으로도 절대 변경하지 않을) 멤버 함수는 const 객체와 비-const 객체 모두에서 사용할 수 있도록 const로 만들어야 합니다.
단, 어떤 멤버 함수에 <code>const</code>를 적용할지는 신중하게 결정하세요. 함수를 한 번 const로 만들면 그 함수는 여러 const 객체들에서 자유롭게 쓰일 텐데, 나중에 해당 함수에서 <code>const</code>를 제거해버리면 그 함수를 호출하고 있던 기존의 const 객체 관련 코드들이 모두 망가지게 됩니다.</p>
</blockquote>
<hr>
<h3 id="const-참조를-통한-const-객체-생성">const 참조를 통한 const 객체 생성</h3>
<p>지역 변수를 const로 선언해서 const 객체를 만드는 방법도 있지만, 더 흔하게 const 객체를 얻는 방법은 함수에 객체를 <strong>const 참조</strong> 로 전달하는 것입니다.</p>
<p><code>12.5 레슨 -- lvalue reference로 전달하기</code> 에서 클래스 타입의 인수를 값 대신 const 참조로 전달할 때의 장점을 다루었습니다. </p>
<p>짧게 복습하자면, 클래스 타입의 인수를 값으로 전달하면 속도가 느린 복사본이 생성됩니다. 대부분의 경우 복사본은 필요하지 않으며, 원본 인수에 대한 참조만으로도 복사 과정을 피하면서 코드를 잘 작동시킬 수 있습니다. 또한 함수가 const lvalue와 rvalue 모두 받아들일 수 있도록 하기 위해 우리는 주로 <strong>const 참조</strong> 를 사용합니다.</p>
<p>다음 코드에서 무엇이 잘못되었는지 알아내실 수 있나요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() // 비-const
    {
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

void doSomething(const Date&amp; date)
{
    date.print();
}

int main()
{
    Date today { 2020, 10, 14 }; // 비-const (일반) 객체
    today.print();

    doSomething(today);

    return 0;
}
</code></pre>
<p>정답은, <code>doSomething()</code> 함수 내부에서 <code>date</code>가 const 참조로 전달되었기 때문에 <strong>const 객체</strong> 로 취급된다는 점입니다. 그런데 우리는 그 const 상태인 <code>date</code>를 사용해서 비-const 멤버 함수인 <code>print()</code>를 호출하려고 하고 있습니다. const 객체에서는 비-const 멤버 함수를 호출할 수 없으므로, 이는 컴파일 에러를 발생시킵니다.</p>
<p>해결책은 간단합니다. <code>print()</code>를 const 함수로 만들어 주면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    int year {};
    int month {};
    int day {};

    void print() const // 이제 const 함수가 됨
    {
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

void doSomething(const Date&amp; date)
{
    date.print();
}

int main()
{
    Date today { 2020, 10, 14 }; // 비-const (일반) 객체
    today.print();

    doSomething(today);

    return 0;
}
</code></pre>
<p>이제 <code>doSomething()</code> 함수 내에서 <code>const date</code>가 const 멤버 함수인 <code>print()</code>를 성공적으로 호출할 수 있게 되었습니다.</p>
<hr>
<h3 id="멤버-함수의-const-및-비-const-오버로딩">멤버 함수의 const 및 비-const 오버로딩</h3>
<p>마지막으로, 자주 쓰이는 기법은 아니지만 동일한 이름의 멤버 함수를 
const 버전과 비-const 버전 두 가지로 오버로딩하는 것이 가능합니다.</p>
<p>이것이 가능한 이유는 const 한정자도 함수 시그니처의 일부로 간주되기 때문입니다. 
즉, const 여부만 다르고 나머지는 똑같은 두 함수는 서로 다른 함수로 인식됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Something
{
    void print()
    {
        std::cout &lt;&lt; &quot;non-const\n&quot;;
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;const\n&quot;;
    }
};

int main()
{
    Something s1{};
    s1.print(); // print() 호출

    const Something s2{};
    s2.print(); // print() const 호출

    return 0;
}
</code></pre>
<p>이 코드의 출력 결과는 다음과 같습니다.</p>
<pre><code class="language-text">non-const
const
</code></pre>
<p>이처럼 동일한 함수를 const 버전과 비-const 버전으로 오버로딩하는 것은 주로 반환 값의 상수성을 다르게 설정해야 할 때 쓰입니다. 하지만 꽤 드물게 사용되는 방법입니다.</p>
<hr>
<h2 id="145--퍼블릭public-프라이빗private-멤버와-접근-지정자">14.5 — 퍼블릭(Public), 프라이빗(Private) 멤버와 접근 지정자</h2>
<p>쌀쌀한 가을날, 부리토를 먹으며 길을 걷고 있다고 상상해 보세요. 앉을 곳을 찾아 주변을 둘러봅니다. 왼쪽에는 잘 깎인 잔디와 그늘진 나무, 조금 불편해 보이는 벤치, 그리고 놀이터에서 뛰노는 아이들이 있는 공원이 있습니다. 오른쪽에는 낯선 사람의 가정집이 보입니다. 창문 너머로 푹신한 안락의자와 따뜻하게 타오르는 벽난로가 눈에 띕니다.
여러분은 깊은 한숨을 쉬며 공원을 선택합니다.</p>
<p>여러분이 공원을 선택한 결정적인 이유는 공원이 <strong>공공(public)</strong> 장소인 반면, 가정집은 <strong>개인적(private)</strong> 인 공간이기 때문입니다. 여러분을 포함한 누구나 공공장소에는 자유롭게 들어갈 수 있습니다. 하지만 개인의 집에는 그 집의 식구들(또는 명시적으로 허락받은 사람)만 들어갈 수 있죠.</p>
<hr>
<h3 id="멤버-접근-권한">멤버 접근 권한</h3>
<p>클래스 타입의 멤버들에도 이와 비슷한 개념이 적용됩니다. 클래스 타입의 각 멤버는 누가 그 멤버에 접근할 수 있는지를 결정하는 <strong>접근 수준</strong>이라는 속성을 가집니다.</p>
<p>C++에는 <strong>퍼블릭(public)</strong> , <strong>프라이빗(private)</strong> , <strong>프로텍티드(protected)</strong> 라는 세 가지 접근 수준이 있습니다. 이번 레슨에서는 가장 많이 쓰이는 퍼블릭과 프라이빗에 대해 알아보겠습니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
프로텍티드(protected) 접근 수준에 대해서는 상속을 다루는 장
<code>레슨 24.5 -- 상속과 접근 지정자</code> 에서 설명합니다.</p>
</blockquote>
<p>어떤 멤버에 접근하려고 할 때마다, 컴파일러는 해당 멤버의 접근 수준이 접근을 허락하는지 검사합니다. 만약 허락되지 않은 접근이라면 컴파일러는 에러를 발생시킵니다. 이러한 접근 수준 시스템을 흔히 <strong>접근 제어</strong>라고 부릅니다.</p>
<hr>
<h3 id="구조체의-멤버는-기본적으로-퍼블릭입니다">구조체의 멤버는 기본적으로 퍼블릭입니다</h3>
<p><strong>퍼블릭</strong> 접근 수준을 가진 멤버를 <strong>퍼블릭 멤버</strong>라고 부릅니다. </p>
<p><strong>퍼블릭 멤버</strong>는 접근 방식에 아무런 제한이 없는 클래스 타입의 멤버를 말합니다. 앞서 들었던 공원 비유처럼, 퍼블릭 멤버는 해당 범위 내에만 있다면 누구나 접근할 수 있습니다.</p>
<p>퍼블릭 멤버는 같은 클래스 내의 다른 멤버들이 접근할 수 있습니다. 중요한 점은, 특정 클래스 타입의 외부에 존재하는 코드를 뜻하는 <strong>외부</strong>에서도 퍼블릭 멤버에 접근할 수 있다는 것입니다. 여기서 <strong>외부</strong> 란 비멤버 함수나 다른 클래스의 멤버들을 의미합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
구조체의 멤버는 기본적으로 퍼블릭입니다. 퍼블릭 멤버는 같은 클래스 타입의 다른 멤버들뿐만 아니라 외부에서도 접근할 수 있습니다.
&quot;외부&quot;라는 용어는 특정 클래스의 멤버가 아닌 바깥쪽 코드를 가리킬 때 사용합니다. 여기에는 비멤버 함수와 다른 클래스의 멤버들이 포함됩니다.</p>
</blockquote>
<p>기본적으로 구조체의 모든 멤버는 퍼블릭 멤버입니다. 다음 구조체를 살펴보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Date
{
    // 구조체 멤버는 기본적으로 퍼블릭(public)이며, 누구나 접근할 수 있습니다
    int year {};       // 기본적으로 퍼블릭
    int month {};      // 기본적으로 퍼블릭
    int day {};        // 기본적으로 퍼블릭

    void print() const // 기본적으로 퍼블릭
    {
        // 퍼블릭 멤버는 해당 클래스 타입의 멤버 함수 내에서 접근할 수 있습니다
        std::cout &lt;&lt; year &lt;&lt; &#39;/&#39; &lt;&lt; month &lt;&lt; &#39;/&#39; &lt;&lt; day;
    }
};

// 비멤버 함수인 main은 &quot;외부(the public)&quot;에 해당합니다
int main()
{
    Date today { 2020, 10, 14 }; // 구조체를 집계 초기화(aggregate initialize)합니다

    // 퍼블릭 멤버는 외부에서 접근할 수 있습니다
    today.day = 16; // 정상 작동: day 멤버는 퍼블릭입니다
    today.print();  // 정상 작동: print() 멤버 함수는 퍼블릭입니다

    return 0;
}
</code></pre>
<p>이 예제에서는 세 곳에서 멤버에 접근하고 있습니다.</p>
<ol>
<li>멤버 함수인 <code>print()</code> 내부에서 암시적 객체의 <code>year</code>, <code>month</code>, <code>day</code> 멤버에 접근합니다.</li>
<li><code>main()</code> 함수에서 <code>today.day</code>에 직접 접근하여 값을 설정합니다.</li>
<li><code>main()</code> 함수에서 멤버 함수인 <code>today.print()</code>를 호출합니다.</li>
</ol>
<p>퍼블릭 멤버는 어디서든 접근이 가능하므로, 이 세 가지 접근은 모두 허용됩니다.</p>
<p><code>main()</code> 함수는 <code>Date</code> 구조체의 멤버가 아니기 때문에 <strong>외부</strong> 로 간주됩니다.
하지만 외부 코드는 퍼블릭 멤버에 접근할 수 있는 권한이 있으므로, 
<code>main()</code> 함수에서 <code>Date</code>의 멤버에 직접 접근할 수 있는 것입니다.</p>
<hr>
<h3 id="클래스의-멤버는-기본적으로-프라이빗입니다">클래스의 멤버는 기본적으로 프라이빗입니다</h3>
<p><strong>프라이빗</strong> 접근 수준을 가진 멤버를 <strong>프라이빗 멤버</strong> 라고 부릅니다. 
<strong>프라이빗 멤버</strong> 는 오직 같은 클래스에 속한 다른 멤버들만 접근할 수 있는 멤버를 말합니다.</p>
<p>위의 예제와 거의 똑같지만, 구조체 대신 클래스를 사용한 다음 예제를 살펴보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Date // 이제 구조체가 아닌 클래스입니다
{
    // 클래스 멤버는 기본적으로 프라이빗(private)이며, 다른 멤버들만 접근할 수 있습니다
    int m_year {};     // 기본적으로 프라이빗
    int m_month {};    // 기본적으로 프라이빗
    int m_day {};      // 기본적으로 프라이빗

    void print() const // 기본적으로 프라이빗
    {
        // 프라이빗 멤버는 멤버 함수 내에서 접근할 수 있습니다
        std::cout &lt;&lt; m_year &lt;&lt; &#39;/&#39; &lt;&lt; m_month &lt;&lt; &#39;/&#39; &lt;&lt; m_day;
    }
};

int main()
{
    Date today { 2020, 10, 14 }; // 컴파일 에러: 더 이상 집계 초기화를 사용할 수 없습니다

    // 프라이빗 멤버는 외부에서 접근할 수 없습니다
    today.m_day = 16; // 컴파일 에러: m_day 멤버는 프라이빗입니다
    today.print();    // 컴파일 에러: print() 멤버 함수는 프라이빗입니다

    return 0;
}
</code></pre>
<p>이 예제에서도 앞서 살펴본 것과 동일한 세 곳에서 멤버에 접근합니다. 
하지만 이 프로그램을 컴파일해 보면 3개의 컴파일 에러가 발생하는 것을 알 수 있습니다.</p>
<ul>
<li><code>main()</code> 내부의 <code>today.m_day = 16</code>과 <code>today.print()</code> 코드는 이제 컴파일 에러를 발생시킵니다. <code>main()</code>은 외부에 속하는 코드인데, 외부에선 프라이빗 멤버에 직접 접근하는 것이 허용되지 않기 때문입니다.</li>
<li>반면 <code>print()</code> 내부에서 <code>m_year</code>, <code>m_month</code>, <code>m_day</code>에 접근하는 것은 허용됩니다. <code>print()</code>는 클래스의 멤버 함수이고, 클래스의 멤버는 다른 프라이빗 멤버에 자유롭게 접근할 수 있기 때문입니다.</li>
</ul>
<p>그렇다면 세 번째 컴파일 에러는 어디서 발생한 걸까요? </p>
<p>놀랍게도 <code>today</code> 객체를 초기화하는 부분에서 에러가 발생합니다. 
이전 레슨(13.8)에서 배운 것처럼, 집계 타입은 &quot;프라이빗 또는 프로텍티드인 비정적 데이터 멤버를 가질 수 없습니다&quot;. 우리의 <code>Date</code> 클래스는 프라이빗 데이터 멤버를 가지고 있기 때문에(클래스는 기본적으로 멤버가 프라이빗이므로), 더 이상 집계 타입의 조건을 만족하지 못합니다. 따라서 집계 초기화 방식을 사용할 수 없는 것입니다.</p>
<p>클래스(일반적으로 집계 타입이 아닌 경우)를 올바르게 초기화하는 방법은 다가오는 레슨 14.9 -- 생성자 소개)에서 자세히 다루겠습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
클래스의 멤버는 기본적으로 프라이빗입니다. 프라이빗 멤버는 클래스 내의 다른 멤버들만 접근할 수 있으며, 외부에서는 접근할 수 없습니다.</p>
<p>프라이빗 멤버를 가진 클래스는 더 이상 집계 타입이 아니므로, 
집계 초기화를 사용할 수 없습니다.</p>
</blockquote>
<hr>
<h3 id="프라이빗-멤버-변수의-이름-짓기">프라이빗 멤버 변수의 이름 짓기</h3>
<p>C++에서는 프라이빗 데이터 멤버의 이름 앞에 &quot;m_&quot; 접두사를 붙이는 것이 일반적인 관례입니다. 이렇게 하는 데는 몇 가지 중요한 이유가 있습니다.
어떤 클래스의 멤버 함수를 예로 들어보겠습니다.</p>
<pre><code class="language-cpp">// 프라이빗 멤버 m_name을 매개변수 name의 값으로 설정하는 멤버 함수
void setName(std::string_view name)
{
    m_name = name;
}
</code></pre>
<p>첫째, <code>m_</code> 접두사는 이 변수가 멤버 함수 내의 매개변수나 지역 변수가 아니라 데이터 멤버라는 것을 쉽게 구분할 수 있게 해줍니다. </p>
<p><code>m_name</code> 이 멤버이고 &quot;name&quot;은 멤버가 아니라는 것을 한눈에 알 수 있죠. 이는 이 함수가 클래스의 &#39;상태&#39;를 변경하고 있다는 것을 명확히 해줍니다. 데이터 멤버의 값을 변경하면 그 변경 사항은 멤버 함수가 끝난 후에도 계속 유지되지만, 함수의 매개변수나 지역 변수는 그렇지 않기 때문에 이 구분은 매우 중요합니다.
(지역 정적 변수에 <code>s_</code>를, 전역 변수에 <code>g_</code>를 붙이는 것을 권장하는 것과 같은 맥락입니다.)</p>
<p>둘째, &quot;m_&quot; 접두사는 프라이빗 멤버 변수와 지역 변수, 함수 매개변수, 멤버 함수의 이름이 서로 충돌하는 것을 막아줍니다.</p>
<p>만약 프라이빗 멤버 이름을 <code>m_name</code> 대신 <code>name</code>으로 지었다면 다음과 같은 문제가 생깁니다.</p>
<ul>
<li>함수의 매개변수인 <code>name</code>이 데이터 멤버인 <code>name</code>을 가려버리게(shadow) 됩니다.</li>
<li>만약 멤버 함수의 이름마저 <code>name</code>이라면, <code>name</code>이라는 식별자가 중복 정의되었다며 컴파일 에러가 발생할 것입니다.</li>
</ul>
<blockquote>
<p><strong>모범 사례</strong>
프라이빗 데이터 멤버의 이름을 지을 때는 &quot;m_&quot; 접두사로 시작하도록 하여, 지역 변수나 함수 매개변수, 멤버 함수와 쉽게 구분되도록 하는 것을 권장합니다.
원한다면 클래스의 퍼블릭 멤버에도 이 관례를 따를 수 있습니다. 하지만 구조체의 경우 멤버 함수를 가지는 일이 드물기 때문에 퍼블릭 멤버에 이 접두사를 잘 쓰지 않습니다.</p>
</blockquote>
<hr>
<h3 id="접근-지정자로-접근-수준-설정하기">접근 지정자로 접근 수준 설정하기</h3>
<p>기본적으로 구조체의 멤버는 퍼블릭이고, 클래스의 멤버는 프라이빗입니다.
하지만 우리는 <strong>접근 지정자</strong>를 사용하여 멤버의 접근 수준을 직접 명시적으로 설정할 수 있습니다. 접근 지정자는 그 지정자 아래에 나오는 <strong>모든 멤버</strong>의 접근 수준을 설정합니다. 
C++는 <code>public:</code>, <code>private:</code>, <code>protected:</code> 라는 세 가지 접근 지정자를 제공합니다.</p>
<p>다음 예제에서는 <code>public:</code> 지정자를 사용하여 외부에서 <code>print()</code> 멤버 함수를 사용할 수 있게 만들고, <code>private:</code> 지정자를 사용하여 데이터 멤버들을 프라이빗으로 만듭니다.</p>
<pre><code class="language-cpp">class Date
{
// 여기에 정의된 멤버는 기본적으로 프라이빗이 됩니다

public: // 여기에 퍼블릭 접근 지정자가 있습니다

    void print() const // 위의 public: 지정자 덕분에 퍼블릭이 됩니다
    {
        // 멤버들은 다른 프라이빗 멤버에 접근할 수 있습니다
        std::cout &lt;&lt; m_year &lt;&lt; &#39;/&#39; &lt;&lt; m_month &lt;&lt; &#39;/&#39; &lt;&lt; m_day;
    }

private: // 여기에 프라이빗 접근 지정자가 있습니다

    int m_year { 2020 };  // 위의 private: 지정자 덕분에 프라이빗이 됩니다
    int m_month { 14 };   // 위의 private: 지정자 덕분에 프라이빗이 됩니다
    int m_day { 10 };     // 위의 private: 지정자 덕분에 프라이빗이 됩니다
};

int main()
{
    Date d{};
    d.print();  // 정상 작동: main()은 퍼블릭 멤버에 접근이 허용됩니다

    return 0;
}
</code></pre>
<p>이 코드는 정상적으로 컴파일됩니다.</p>
<p><code>print()</code>는 <code>public:</code> 지정자 덕분에 퍼블릭 멤버가 되었으므로, 
외부에 해당하는 <code>main()</code> 함수에서 접근할 수 있습니다. </p>
<p>반면 프라이빗 멤버가 존재하기 때문에 <code>d</code>를 집계 초기화할 수는 없습니다. 
이 예제에서는 임시방편으로 기본 멤버 초기화를 사용했습니다.</p>
<p>클래스는 기본적으로 프라이빗 접근을 사용하기 때문에, 맨 처음 시작할 때 <code>private:</code> 지정자는 생략해도 됩니다.</p>
<pre><code class="language-cpp">class Foo
{
    // 클래스는 기본적으로 프라이빗 멤버를 가지므로 여기에 private 지정자가 필요 없습니다
    int m_something {};  // 기본적으로 프라이빗
};
</code></pre>
<p>하지만 구조체와 클래스의 기본 접근 수준이 다르기 때문에, 
헷갈리는 것을 방지하기 위해 많은 개발자들이 명시적으로 적어주는 것을 선호합니다.</p>
<pre><code class="language-cpp">class Foo
{
private: // 기술적으로는 불필요하지만, 이후 멤버들이 프라이빗이라는 것을 명확히 해줍니다
    int m_something {};  // 기본적으로 프라이빗
};
</code></pre>
<p>기술적으로는 중복이더라도 이렇게 <code>private:</code> 지정자를 명시해주면, 
<code>Foo</code>가 클래스로 정의되었는지 구조체로 정의되었는지 따져가며 기본 접근 수준을 유추할 필요 없이 코드의 의도를 명확하게 파악할 수 있습니다.</p>
<hr>
<h3 id="접근-수준-요약">접근 수준 요약</h3>
<p>각 접근 수준에 대한 간단한 요약표입니다.</p>
<table>
<thead>
<tr>
<th>접근 수준</th>
<th>접근 지정자</th>
<th>멤버의 접근</th>
<th>파생 클래스의 접근</th>
<th>외부(Public)의 접근</th>
</tr>
</thead>
<tbody><tr>
<td>Public</td>
<td><code>public:</code></td>
<td>예</td>
<td>예</td>
<td>예</td>
</tr>
<tr>
<td>Protected</td>
<td><code>protected:</code></td>
<td>예</td>
<td>예</td>
<td>아니요</td>
</tr>
<tr>
<td>Private</td>
<td><code>private:</code></td>
<td>예</td>
<td>아니요</td>
<td>아니요</td>
</tr>
</tbody></table>
<p>클래스 타입 안에서는 원하는 순서대로 얼마든지 여러 개의 접근 지정자를 번갈아 사용할 수 있습니다. (예: 퍼블릭 멤버들을 먼저 쓰고, 그 다음 프라이빗 멤버들을 쓰고, 다시 퍼블릭 멤버들을 쓰는 등).</p>
<p>실제로 대부분의 클래스는 용도에 맞게 프라이빗과 퍼블릭 접근 지정자를 모두 활용합니다.</p>
<hr>
<h3 id="구조체와-클래스의-접근-수준-모범-사례">구조체와 클래스의 접근 수준 모범 사례</h3>
<p>이제 접근 수준이 무엇인지 알았으니, 이를 어떻게 활용해야 하는지 이야기해 봅시다.</p>
<ul>
<li><p><strong>구조체(Structs)</strong> 에서는 접근 지정자를 아예 쓰지 않는 것이 좋습니다. 
즉, 모든 멤버가 기본값인 퍼블릭 상태로 남도록 두는 것입니다. 
구조체는 집계 타입으로 쓰는 것이 좋은데, 접근 지정자를 사용해 
<code>private:</code>이나 <code>protected:</code>를 넣으면 더 이상 집계 타입이 아니게 됩니다.</p>
</li>
<li><p><strong>클래스(Classes)</strong> 는 반대로 데이터 멤버를 프라이빗(또는 프로텍티드)으로 숨기는 것이 일반적입니다. 왜 그래야 하는지에 대해서는 다음 레슨 <code>14.6 -- 접근 함수</code> 에서 이유를 설명하겠습니다.</p>
</li>
<li><p>클래스의 멤버 함수는 객체가 생성된 후 외부에서 사용할 수 있도록 대개 퍼블릭으로 만듭니다. 하지만 외부에서 직접 쓰라고 만든 기능이 아닐 경우에는 멤버 함수도 프라이빗(또는 프로텍티드)으로 만들 수 있습니다.</p>
</li>
</ul>
<blockquote>
<p><strong>모범 사례</strong>
클래스에서는 일반적으로 멤버 변수를 프라이빗으로 숨기고, 멤버 함수를 퍼블릭으로 공개합니다.
구조체에서는 접근 지정자 사용을 피하는 것이 좋습니다 (모든 멤버가 기본값인 퍼블릭이 됩니다).</p>
</blockquote>
<hr>
<h3 id="접근-수준은-객체가-아닌-클래스-단위로-작동합니다">접근 수준은 객체가 아닌 &#39;클래스&#39; 단위로 작동합니다</h3>
<p>C++ 접근 수준에 대해 많은 사람들이 놓치거나 오해하는 미묘한 점이 하나 있습니다. 
바로 접근 권한이 &#39;객체&#39; 단위가 아니라 &#39;<strong>클래스</strong>&#39; 단위로 적용된다는 것입니다.</p>
<p>자신의 프라이빗 멤버에 멤버 함수가 접근할 수 있다는 사실은 이미 배우셨을 겁니다.
그런데 접근 권한이 클래스 단위로 적용되기 때문에, 어떤 멤버 함수는 같은 범위 내에 있는 <strong>같은 클래스 타입의 다른 객체</strong> 의 프라이빗 멤버에도 직접 접근할 수 있습니다.</p>
<p>예제로 확인해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Person
{
private:
    std::string m_name{};

public:
    void kisses(const Person&amp; p) const
    {
        std::cout &lt;&lt; m_name &lt;&lt; &quot; kisses &quot; &lt;&lt; p.m_name &lt;&lt; &#39;\n&#39;;
    }

    void setName(std::string_view name)
    {
        m_name = name;
    }
};

int main()
{
    Person joe;
    joe.setName(&quot;Joe&quot;);

    Person kate;
    kate.setName(&quot;Kate&quot;);

    joe.kisses(kate);

    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">Joe kisses Kate</code></pre>
<p>여기서 주목해야 할 몇 가지 포인트가 있습니다.</p>
<ul>
<li><p>첫째, <code>m_name</code>이 프라이빗으로 선언되었기 때문에 <code>Person</code> 클래스의 멤버들만 접근할 수 있고 외부에서는 접근할 수 없습니다.</p>
</li>
<li><p>둘째, 프라이빗 멤버가 있어서 더 이상 집계 타입이 아니므로 집계 초기화를 할 수 없습니다. 임시방편으로 <code>setName()</code>이라는 퍼블릭 멤버 함수를 만들어 이름을 지정해주었습니다.</p>
</li>
<li><p>셋째, <code>kisses()</code>는 멤버 함수이므로 프라이빗 멤버인 <code>m_name</code>에 직접 접근할 수 있습니다. 그런데 흥미로운 점은 <code>p.m_name</code>에도 직접 접근하고 있다는 것입니다. 
<code>p</code> 역시 <code>Person</code> 객체이고, <code>kisses()</code>는 같은 <code>Person</code> 클래스의 멤버 함수이기 때문에 다른 <code>Person</code> 객체의 프라이빗 멤버에도 마음대로 접근할 수 있는 것입니다.</p>
</li>
</ul>
<p>이 원리는 연산자 오버로딩 챕터에서 아주 유용하게 쓰일 예정입니다.</p>
<hr>
<h3 id="구조체와-클래스의-기술적-실용적-차이점">구조체와 클래스의 기술적, 실용적 차이점</h3>
<p>이제 구조체와 클래스의 &#39;기술적인&#39; 차이점에 대해 결론을 내려보겠습니다. 준비되셨나요?</p>
<ul>
<li>클래스는 멤버가 기본적으로 프라이빗이고, 구조체는 멤버가 기본적으로 퍼블릭입니다.</li>
<li>…네, 이게 전부입니다.</li>
</ul>
<blockquote>
<p><strong>저자의 메모</strong>
아주 엄밀하게 따지자면 사소한 차이가 하나 더 있습니다. 구조체는 다른 클래스를 상속받을 때 기본적으로 &#39;퍼블릭 상속&#39;을 하고, 클래스는 &#39;프라이빗 상속&#39;을 합니다. 
이것이 무슨 뜻인지는 상속 챕터에서 배우겠지만, 어차피 상속을 할 때는 명시적으로 방식을 지정해야 하므로 실질적으로는 거의 의미가 없는 차이입니다.</p>
</blockquote>
<p><strong>실제 실무에서</strong> 우리는 구조체와 클래스를 다음과 같이 구분해서 사용합니다.
다음 조건들을 모두 만족한다면 경험적으로 <strong>구조체</strong>를 사용하는 것이 좋습니다.</p>
<ul>
<li>접근을 제한할 필요가 없는 단순한 데이터 모음일 때</li>
<li>집계 초기화만으로 충분할 때</li>
<li>클래스의 불변성을 유지하거나 복잡한 초기화, 정리 작업이 필요 없을 때</li>
</ul>
<p>구조체를 사용하기 좋은 예시: constexpr 전역 프로그램 데이터, 좌표를 나타내는 Point 구조체 (숨길 필요 없는 단순한 int 변수 모음), 함수에서 여러 개의 데이터를 한 번에 반환하기 위해 사용하는 구조체 등.</p>
<p>이 외의 경우에는 <strong>클래스</strong>를 사용하세요.</p>
<p>우리는 구조체를 &#39;집계 타입&#39;으로 유지하고 싶어 합니다. 따라서 구조체를 집계 타입이 아니게 만드는 어떤 기능(프라이빗 멤버 등)이라도 추가해야 한다면, 그 구조체는 클래스로 바꾸는 것이 낫습니다.</p>
<hr>
<h2 id="146--접근-함수">14.6 — 접근 함수</h2>
<p>이전 레슨에서 퍼블릭과 프라이빗 접근 수준에 대해 이야기했습니다. 
다시 한번 기억해 보자면, 클래스는 보통 데이터 멤버를 프라이빗으로 만들며, 외부에서는 이 프라이빗 멤버에 직접 접근할 수 없습니다.</p>
<p>다음 <code>Date</code> 클래스를 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Date
{
private:
    int m_year{ 2020 };
    int m_month{ 10 };
    int m_day{ 14 };

public:
    void print() const
    {
        std::cout &lt;&lt; m_year &lt;&lt; &#39;/&#39; &lt;&lt; m_month &lt;&lt; &#39;/&#39; &lt;&lt; m_day &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Date d{};  // Date 객체를 만듭니다
    d.print(); // 날짜를 출력합니다

    return 0;
}
</code></pre>
<p>이 클래스는 전체 날짜를 출력해 주는 <code>print()</code> 멤버 함수를 제공하지만, 사용자가 원하는 작업을 하기에는 충분하지 않을 수 있습니다. </p>
<p>예를 들어, <code>Date</code> 객체를 사용하는 사람이 연도만 따로 가져오고 싶다면 어떻게 해야 할까요? 아니면 연도를 다른 값으로 바꾸고 싶다면요? </p>
<p><code>m_year</code> 변수가 프라이빗이기 때문에 그렇게 할 수 없습니다.</p>
<p>어떤 클래스에서는 프라이빗 멤버 변수의 값을 가져오거나 설정할 수 있게 해주는 것이 (클래스의 역할에 따라) 적절할 수 있습니다.</p>
<hr>
<h3 id="접근-함수">접근 함수</h3>
<p><strong>접근 함수</strong>는 프라이빗 멤버 변수의 값을 가져오거나 바꾸는 역할을 하는 간단한 퍼블릭 멤버 함수입니다. 접근 함수는 크게 두 가지 종류로 나뉩니다. </p>
<p><strong>게터(Getter)</strong> (접근자라고도 부름) 는 프라이빗 멤버 변수의 값을 반환하는 퍼블릭 멤버 함수입니다. </p>
<p><strong>세터(Setter)</strong> (변경자라고도 부름) 는 프라이빗 멤버 변수의 값을 새롭게 설정하는 퍼블릭 멤버 함수입니다.</p>
<hr>
<h3 id="용어-정리">용어 정리</h3>
<p>&quot;뮤테이터(Mutator)&quot;라는 용어는 종종 &quot;세터(setter)&quot;와 같은 의미로 사용됩니다. 하지만 더 넓은 의미에서 <strong>뮤테이터(Mutator)</strong> 란 객체의 상태를 수정(변경)하는 모든 멤버 함수를 말합니다. </p>
<p>이 정의에 따르면 세터는 뮤테이터의 한 종류입니다. 
세터가 아니더라도 객체의 상태를 바꾸는 함수라면 모두 뮤테이터라고 할 수 있습니다.</p>
<p>게터는 보통 <code>const</code>로 만들어져서 <code>const</code> 객체와 상수가 아닌 객체 모두에서 호출할 수 있게 합니다. 반면 세터는 데이터 멤버를 수정해야 하므로 <code>const</code>가 아니어야 합니다.</p>
<p>이해를 돕기 위해, 모든 게터와 세터가 포함되도록 <code>Date</code> 클래스를 업데이트해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Date
{
private:
    int m_year { 2020 };
    int m_month { 10 };
    int m_day { 14 };

public:
    void print()
    {
        std::cout &lt;&lt; m_year &lt;&lt; &#39;/&#39; &lt;&lt; m_month &lt;&lt; &#39;/&#39; &lt;&lt; m_day &lt;&lt; &#39;\n&#39;;
    }

    int getYear() const { return m_year; }        // 연도 게터
    void setYear(int year) { m_year = year; }     // 연도 세터

    int getMonth() const  { return m_month; }     // 월 게터
    void setMonth(int month) { m_month = month; } // 월 세터

    int getDay() const { return m_day; }          // 일 게터
    void setDay(int day) { m_day = day; }         // 일 세터
};

int main()
{
    Date d{};
    d.setYear(2021);
    std::cout &lt;&lt; &quot;The year is: &quot; &lt;&lt; d.getYear() &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드는 다음을 출력합니다.</p>
<pre><code class="language-cpp">The year is: 2021</code></pre>
<hr>
<h3 id="접근-함수의-이름-짓기">접근 함수의 이름 짓기</h3>
<p>접근 함수의 이름을 짓는 데 있어 모두가 따르는 완벽한 표준은 없습니다. 
하지만 다른 방식들보다 더 자주 쓰이는 몇 가지 이름 짓기 방식이 있습니다.</p>
<p><strong>&quot;get&quot;과 &quot;set&quot;을 앞에 붙이는 방식:</strong></p>
<pre><code class="language-cpp">int getDay() const { return m_day; }  // 게터
void setDay(int day) { m_day = day; } // 세터
</code></pre>
<p>&quot;get&quot;과 &quot;set&quot; 접두사를 사용하는 것의 장점은 이것들이 접근 함수라는 것을 
(그리고 호출하는 데 컴퓨터 자원이 적게 든다는 것을) 아주 명확하게 보여준다는 점입니다.</p>
<p><strong>접두사 없이 사용하는 방식:</strong></p>
<pre><code class="language-cpp">int day() const { return m_day; }  // 게터
void day(int day) { m_day = day; } // 세터
</code></pre>
<p>이 스타일은 더 간결하며, 게터와 세터 모두에 같은 이름을 사용합니다 (두 개를 구분하기 위해 함수 오버로딩 기능을 활용합니다). C++ 표준 라이브러리가 이 방식을 사용합니다.</p>
<p>접두사가 없는 방식의 단점은 코드를 읽을 때 멤버 변수를 설정하는 작업인지 눈에 확 띄지 않는다는 점입니다.</p>
<pre><code class="language-cpp">d.day(5); // 이것이 day 멤버를 5로 설정하는 것처럼 보이나요?
</code></pre>
<blockquote>
<p><strong>핵심 포인트</strong></p>
<p>프라이빗 데이터 멤버 앞에 &quot;m_&quot;을 붙이는 가장 큰 이유 중 하나는 데이터 멤버와 게터의 이름이 똑같아지는 것을 막기 위해서입니다. 
(자바 같은 다른 언어는 이름이 같아도 되지만, C++은 지원하지 않습니다)</p>
</blockquote>
<p><strong>&quot;set&quot; 접두사만 사용하는 방식:</strong></p>
<pre><code class="language-cpp">int day() const { return m_day; }     // 게터
void setDay(int day) { m_day = day; } // 세터
</code></pre>
<p>위의 방식 중 어떤 것을 선택할지는 개인의 취향에 달려 있습니다. 
하지만 세터에는 &quot;set&quot; 접두사를 사용하는 것을 강력히 추천합니다. 
게터는 &quot;get&quot; 접두사를 써도 되고 아무것도 쓰지 않아도 괜찮습니다.</p>
<blockquote>
<p><strong>팁</strong></p>
<p>세터에 &quot;set&quot; 접두사를 사용하여 객체의 상태를 바꾸고 있다는 사실을 더 확실하게 나타내세요.</p>
</blockquote>
<hr>
<h3 id="게터는-값-또는-const-lvalue-참조로-반환해야-합니다">게터는 값 또는 const lvalue 참조로 반환해야 합니다</h3>
<p>게터는 데이터에 대한 &quot;읽기 전용&quot; 접근을 제공해야 합니다. 
따라서 좋은 프로그래밍 습관은 (데이터를 복사하는 비용이 저렴하다면) 
값으로 반환하거나, (복사하는 비용이 크다면) <code>const</code> 참조로 반환하는 것입니다.</p>
<p>데이터 멤버를 참조로 반환하는 것은 꽤 까다로운 주제이므로, 다음 레슨인 
<code>14.7 -- 데이터 멤버에 대한 참조를 반환하는 멤버 함수</code> 에서 더 자세히 다루겠습니다.</p>
<hr>
<h3 id="접근-함수-사용-시-주의할-점">접근 함수 사용 시 주의할 점</h3>
<p>언제 접근 함수를 사용해야 하고 피해야 하는지에 대해서는 꽤 많은 논쟁이 있습니다. 
많은 개발자들은 접근 함수를 남용하는 것이 좋은 클래스 설계 원칙에 어긋난다고 말합니다.
(이 주제만으로도 책 한 권을 쓸 수 있을 정도입니다)</p>
<p>우선은 조금 더 실용적인 접근법을 추천해 드립니다. 
클래스를 만들 때 다음 사항들을 고려해 보세요.</p>
<ul>
<li>클래스에 유지해야 할 특별한 규칙(불변성)이 없고 여러 개의 접근 함수가 필요하다면, (데이터가 외부로 공개되는) <code>struct</code>를 사용하여 멤버에 직접 접근하도록 만드는 것을 고려해 보세요.</li>
<li>접근 함수를 단순하게 만들기보다는 &#39;행동&#39;이나 &#39;동작&#39;을 구현하는 것을 우선하세요. 
예를 들어, <code>setAlive(bool)</code> 세터를 만드는 대신 <code>kill()</code>(죽이기)과 <code>revive()</code>(부활시키기) 함수를 구현하세요.</li>
<li>외부에서 개별 멤버의 값을 가져오거나 설정할 필요가 정말로 합리적인 경우에만 접근 함수를 만드세요.</li>
</ul>
<hr>
<h3 id="퍼블릭-접근-함수를-제공할-거라면-왜-데이터를-프라이빗으로-만드나요">퍼블릭 접근 함수를 제공할 거라면 왜 데이터를 프라이빗으로 만드나요?</h3>
<p>아주 좋은 질문입니다. 이 질문에 대한 답은 다가오는 레슨인 
<code>14.8 -- 데이터 은닉(캡슐화)의 이점</code> 에서 자세히 알려드리겠습니다.</p>
<hr>
<h2 id="147--멤버-변수의-참조를-반환하는-멤버-함수">14.7 — 멤버 변수의 참조를 반환하는 멤버 함수</h2>
<p>이전 레슨에서 우리는 &#39;참조로 반환하기&#39;에 대해 배웠습니다. 그때 가장 중요하게 강조했던 규칙은 <strong>&quot;함수가 끝난 후에도 참조하는 대상이 반드시 살아있어야 한다&quot;</strong> 는 것이었죠.</p>
<p>이 말은, 함수 안에서 잠깐 쓰려고 만든 &#39;지역 변수&#39;는 참조로 반환하면 안 된다는 뜻입니다. 함수가 끝나면 지역 변수는 파괴되어 사라지는데, 반환된 참조는 텅 빈 허공을 가리키게 되기 때문입니다.</p>
<p>하지만 매개변수로 받아온 참조나, 전역 변수, 정적(static) 변수처럼 <strong>함수가 끝나도 계속 살아있는 변수들</strong> 은 참조로 반환해도 괜찮습니다.</p>
<p>예를 들어보겠습니다.</p>
<pre><code class="language-cpp">// 두 개의 std::string 객체를 받아서 알파벳 순서상 먼저 오는 것을 참조로 반환합니다.
const std::string&amp; firstAlphabetical(const std::string&amp; a, const std::string&amp; b)
{
    return (a &lt; b) ? a : b; // std::string에 operator&lt; 를 사용해서 어느 것이 알파벳순으로 먼저인지 알아낼 수 있습니다.
}

int main()
{
    std::string hello { &quot;Hello&quot; };
    std::string world { &quot;World&quot; };

    std::cout &lt;&lt; firstAlphabetical(hello, world); // hello나 world 중 하나가 참조로 반환될 것입니다.

    return 0;
}
</code></pre>
<p>클래스 안에 있는 &#39;멤버 함수&#39;들도 이와 똑같은 규칙을 따릅니다. 하지만 멤버 함수에는 우리가 꼭 짚고 넘어가야 할 특별한 경우가 하나 있습니다. 바로 <strong>자신의 멤버 변수를 참조로 반환하는 경우</strong> 입니다.</p>
<p>주로 데이터를 읽어오는 역할을 하는 <strong>&#39;게터(Getter)&#39;</strong> 함수에서 이런 모습을 자주 볼 수 있습니다. 이 글에서는 게터 함수를 예로 들어 설명하겠지만, 이 원리는 멤버 변수의 참조를 반환하는 모든 멤버 함수에 똑같이 적용됩니다.</p>
<hr>
<h3 id="멤버-변수를-값으로-반환하면-프로그램이-느려질-수-있습니다">멤버 변수를 &#39;값&#39;으로 반환하면 프로그램이 느려질 수 있습니다</h3>
<p>다음 예시를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

class Employee
{
    std::string m_name{};

public:
    void setName(std::string_view name) { m_name = name; }
    std::string getName() const { return m_name; } // 게터(getter)가 &#39;값&#39;으로 반환합니다.
};

int main()
{
    Employee joe{};
    joe.setName(&quot;Joe&quot;);
    std::cout &lt;&lt; joe.getName();

    return 0;
}
</code></pre>
<p>위 코드에서 <code>getName()</code> 함수는 <code>m_name</code> 변수를 <strong>값(Value)</strong> 으로 반환합니다.</p>
<p>이 방식이 가장 안전하긴 하지만, <code>getName()</code> 을 부를 때마다 <code>m_name</code> 문자열을 통째로 새롭게 <strong>복사(Copy)</strong> 해야 한다는 큰 단점이 있습니다. 데이터를 단순히 읽어오기만 하는 게터 함수는 프로그램 안에서 엄청나게 자주 호출되기 때문에, 이렇게 매번 데이터를 복사하는 것은 성능상 좋은 선택이 아닙니다.</p>
<hr>
<h3 id="멤버-변수를-lvalue-참조로-반환하기">멤버 변수를 &#39;lvalue 참조&#39;로 반환하기</h3>
<p>다행히 멤버 함수는 멤버 변수를 <strong>참조</strong>로 반환할 수 있습니다. 쉽게 말해 원본 데이터를 복사하는 대신, 원본과 바로 연결되는 &#39;직통 통로&#39;만 넘겨주는 것입니다.</p>
<p>멤버 변수는 자신이 속해 있는 &#39;객체(Object)&#39;와 운명을 함께합니다. 
즉, 객체가 살아있는 동안에는 멤버 변수도 살아있습니다. </p>
<p>멤버 함수는 항상 어떤 객체를 통해서 호출되는데, 함수를 부른 쪽(호출자)에서 그 객체가 살아있다면 멤버 함수가 변수의 통로(참조)를 반환해도 아주 안전합니다. 
함수 호출이 끝나도 원본 데이터가 여전히 살아있기 때문이죠.</p>
<p>아까 본 예시에서 <code>getName()</code> 이 복사 대신 <strong>참조(const reference)</strong> 를 반환하도록 수정해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

class Employee
{
    std::string m_name{};

public:
    void setName(std::string_view name) { m_name = name; }
    const std::string&amp; getName() const { return m_name; } // 게터가 상수를 참조(const reference)하는 방식으로 반환합니다.
};

int main()
{
    Employee joe{}; // joe는 main 함수가 끝날 때까지 살아있습니다.
    joe.setName(&quot;Joe&quot;);

    std::cout &lt;&lt; joe.getName(); // joe.m_name을 통째로 복사하지 않고, 참조(직통 연결)로 반환합니다.

    return 0;
}
</code></pre>
<p>이제 <code>joe.getName()</code> 을 부르면, 무거운 복사 과정을 거치지 않고 <code>joe.m_name</code> 에 접근할 수 있는 참조만 쏙 반환합니다. 그러면 <code>main()</code> 함수에서는 이 참조를 사용해 화면에 <code>joe</code> 의 이름을 출력합니다.</p>
<p><code>joe</code> 라는 객체는 <code>main()</code> 함수가 끝날 때까지 살아있기 때문에, <code>joe.m_name</code> 을 가리키는 참조 역시 그 시간 동안 아무 문제 없이 안전하게 쓸 수 있습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
멤버 변수를 참조로 반환하는 것은 괜찮습니다. 함수 호출이 끝나더라도 그 변수를 품고 있는 원본 객체가 여전히 살아있기 때문에, 반환받은 참조를 안심하고 사용할 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="반환-타입은-멤버-변수의-타입과-똑같이-맞추세요">반환 타입은 멤버 변수의 타입과 똑같이 맞추세요</h3>
<p>참조를 반환할 때 함수의 &#39;반환 타입&#39;은 원래 멤버 변수의 타입과 똑같아야 합니다.
위의 예시에서 <code>m_name</code> 은 <code>std::string</code> 타입이므로, <code>getName()</code> 의 반환 타입도 <code>const std::string&amp;</code> 가 되어야 합니다.</p>
<p>만약 반환 타입을 <code>std::string_view</code> 같이 다른 타입으로 적는다면, 함수가 호출될 때마다 억지로 타입을 바꾸느라 불필요한 임시 객체가 만들어지게 됩니다. 이는 매우 비효율적입니다. 만약 밖에서 <code>std::string_view</code> 형태가 필요하다면, 참조를 받아간 쪽에서 알아서 변환해 쓰게 두는 것이 맞습니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
쓸데없는 데이터 변환을 막으려면, 참조를 반환하는 함수의 타입은 원래 데이터 변수의 타입과 100% 똑같이 맞추세요.</p>
</blockquote>
<p>게터를 만들 때 <code>auto</code> 키워드를 쓰면 컴파일러가 알아서 타입을 맞춰주기 때문에 실수를 줄일 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

class Employee
{
    std::string m_name{};

public:
    void setName(std::string_view name) { m_name = name; }
    const auto&amp; getName() const { return m_name; } // m_name으로부터 반환 타입을 알아내기 위해 auto를 사용합니다.
};

int main()
{
    Employee joe{}; // joe는 main 함수가 끝날 때까지 살아있습니다.
    joe.setName(&quot;Joe&quot;);

    std::cout &lt;&lt; joe.getName(); // joe.m_name을 참조로 반환합니다.

    return 0;
}
</code></pre>
<p>하지만 <code>auto</code> 를 쓰면 코드를 읽는 사람 입장에서는 이 함수가 정확히 어떤 타입을 반환하는지 한눈에 알기 어렵다는 단점이 있습니다. 그래서 보통은 명확하게 반환 타입을 직접 적어주는 방식을 더 선호합니다.</p>
<hr>
<h3 id="잠깐-생겼다-사라지는-임시-객체rvalue와-참조-반환의-위험성">잠깐 생겼다 사라지는 임시 객체(Rvalue)와 참조 반환의 위험성</h3>
<p>여기서 조금 주의해야 할 상황이 하나 있습니다.</p>
<p>앞서 본 예시에서 <code>joe</code> 는 함수가 끝날 때까지 튼튼하게 살아남는 객체(lvalue)였습니다. 
그래서 <code>joe.getName()</code> 이 돌려준 참조도 끝까지 안전했죠.</p>
<p>하지만 만약 <strong>방금 만들어졌다가 금방 사라져버리는 임시 객체(rvalue)</strong> 를 다룬다면 어떨까요? (예를 들어, 어떤 함수가 객체를 &#39;값&#39;으로 새로 만들어서 툭 던져주는 경우입니다.)</p>
<p>이런 임시 객체(rvalue)들은 자신이 속한 <strong>한 줄의 코드</strong>가 실행되고 나면 곧바로 파괴되어 사라집니다. 객체가 파괴되면 그 안에 들어있던 멤버 변수들도 같이 날아가 버리죠. 이때 이 사라진 멤버 변수를 가리키고 있던 참조가 있다면, 텅 빈 공간을 가리키는 댕글링 참조가 되어버립니다. 이걸 계속 쓰려고 하면 프로그램에 미정의 동작이 발생합니다.</p>
<blockquote>
<p><strong>경고</strong>
임시 객체(rvalue)는 그 코드가 적힌 한 줄(전체 표현식)이 끝날 때 파괴됩니다. 따라서 임시 객체의 멤버에 연결된 참조는 <strong>딱 그 한 줄 안에서만</strong> 안전하게 쓸 수 있습니다.</p>
</blockquote>
<p>코드를 보며 무슨 뜻인지 이해해 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
    std::string m_name{};

public:
    void setName(std::string_view name) { m_name = name; }
    const std::string&amp; getName() const { return m_name; } // 게터가 상수를 참조하는 방식으로 반환합니다.
};

// createEmployee()는 Employee를 &#39;값&#39;으로 반환합니다 (즉, 반환된 값은 수명이 짧은 임시 객체인 rvalue가 됩니다).
Employee createEmployee(std::string_view name)
{
    Employee e;
    e.setName(name);
    return e;
}

int main()
{
    // Case 1: 정상 - rvalue 객체의 멤버에 대한 참조를 같은 줄(표현식) 안에서 즉시 사용했습니다.
    std::cout &lt;&lt; createEmployee(&quot;Frank&quot;).getName();

    // Case 2: 위험 - rvalue 객체의 멤버에 대한 참조를 나중에 쓰려고 저장해 두었습니다.
    const std::string&amp; ref { createEmployee(&quot;Garbo&quot;).getName() }; // createEmployee()의 반환값이 파괴되면서 참조가 끊어집니다(댕글링).
    std::cout &lt;&lt; ref; // 미정의 동작 (프로그램이 터지거나 이상한 값이 나올 수 있습니다)

    // Case 3: 정상 - 참조하던 값을 나중에 쓰기 위해 아예 새로운 지역 변수에 &#39;복사&#39;해 두었습니다.
    std::string val { createEmployee(&quot;Hans&quot;).getName() }; // 참조하던 멤버를 복사해 옵니다.
    std::cout &lt;&lt; val; // 정상: val은 참조하던 멤버와 이제 완전히 별개인 독립적인 변수입니다.

    return 0;
}
</code></pre>
<p><code>createEmployee()</code> 를 부르면 새로운 <code>Employee</code> 임시 객체(rvalue)가 만들어져서 돌아옵니다. 이 녀석은 그 줄의 코드 실행이 끝나면 흔적도 없이 파괴될 운명입니다.</p>
<ul>
<li><p><strong>Case 1:</strong> <code>createEmployee(&quot;Frank&quot;)</code> 가 만든 임시 객체에서 바로 <code>.getName()</code> 으로 이름의 참조를 가져온 뒤, 그 자리에서 바로 화면에 출력(<code>std::cout</code>)했습니다. 그 한 줄 안에서 모든 할 일을 무사히 마쳤으므로 아주 안전합니다.</p>
</li>
<li><p><strong>Case 2:</strong> 이번에는 <code>.getName()</code> 으로 가져온 임시 객체의 이름 참조를 <code>ref</code> 라는 변수에 <strong>묶어(저장해) 두었습니다</strong>. 안타깝게도 이 줄이 넘어가는 순간 원본 임시 객체가 파괴되어 사라집니다. 다음 줄에서 <code>ref</code> 를 출력하려고 해 봤자, 원본이 이미 사라졌기 때문에 프로그램이 오작동하게 됩니다.</p>
</li>
<li><p><strong>Case 3:</strong> 원본이 곧 파괴될 것을 알고, 참조를 저장하는 대신 아예 <code>val</code> 이라는 내 개인 변수에 데이터를 쏙 <strong>복사</strong>해버렸습니다. 이제 원본 임시 객체가 파괴되더라도, 내 손에는 복사본이 안전하게 남아있기 때문에 나중에 언제든지 자유롭게 쓸 수 있습니다.</p>
</li>
</ul>
<hr>
<h3 id="참조를-반환하는-함수-어떻게-안전하게-쓸-수-있을까">참조를 반환하는 함수, 어떻게 안전하게 쓸 수 있을까?</h3>
<p>임시 객체(rvalue)를 다룰 때의 위험성에도 불구하고, 게터 함수가 값을 무겁게 복사하지 않고 가벼운 참조로 반환하는 것은 C++의 아주 훌륭한 관행입니다.</p>
<p>그렇다면 어떻게 해야 오류 없이 안전하게 쓸 수 있을까요? 
위 3가지 Case를 요약한 다음의 핵심 규칙만 기억하시면 됩니다.</p>
<ol>
<li><p>참조를 돌려주는 멤버 함수를 썼다면, <strong>반환받은 값을 그 자리에서 즉시 사용하세요</strong> (Case 1 방식). 이렇게 습관을 들이면 원본이 튼튼한 녀석(lvalue)이든 임시 객체(rvalue)든 꼬일 일이 없습니다.</p>
</li>
<li><p>원본 객체가 오래 살아남는다는 확실한 보장이 없다면, 반환된 참조를 변수에 <strong>묶어서 저장해두지 마세요</strong> (Case 2처럼 하면 안 됩니다).</p>
</li>
<li><p>만약 그 값을 나중에 또 써야 하는데 원본 객체가 곧 파괴될 것 같다면, 참조로 묶어두는 대신 그냥 일반 변수에 담아서 <strong>안전한 복사본을 만드세요</strong> (Case 3 방식).</p>
</li>
</ol>
<blockquote>
<p><strong>모범 사례</strong>
원본 객체가 임시 객체(rvalue)일 때 댕글링 참조(고장 난 참조)가 생기는 것을 막으려면, 참조를 반환하는 함수의 결과값은 받은 즉시 바로 그 자리에서 사용하는 것이 가장 좋습니다.</p>
</blockquote>
<hr>
<h3 id="private-멤버-변수를-변경할-수-있는-참조로-반환하지-마세요">private 멤버 변수를 변경할 수 있는 참조로 반환하지 마세요</h3>
<p>참조는 그 변수 본체와 완벽히 똑같이 행동합니다. 
만약 외부에서 함부로 건드리면 안 되는 <code>private</code> 변수를 그냥 변경 가능한 참조로 밖으로 빼내버리면, 외부에서 그 참조를 이용해 내부 값을 마음대로 바꿀 수 있게 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_value{ 4 }; // private 멤버 (외부에서 함부로 접근 불가)

public:
    int&amp; value() { return m_value; } // 변경 가능한(non-const) 참조를 반환합니다 (절대 이렇게 하지 마세요!)
};

int main()
{
    Foo f{};                // f.m_value가 기본값인 4로 초기화됩니다.
    f.value() = 5;          // m_value = 5 와 완벽히 같은 결과를 만듭니다. (private 변수가 밖에서 뚫렸습니다!)
    std::cout &lt;&lt; f.value(); // 5가 출력됩니다.

    return 0;
}
</code></pre>
<p>보안을 위해 꽁꽁 숨겨둔 <code>private</code> 변수의 마스터키를 복사해서 밖으로 던져준 것이나 다름없습니다. 이렇게 하면 객체를 안전하게 보호하려는 목적이 완전히 무너집니다.</p>
<hr>
<h3 id="데이터-변경을-막는-const-멤버-함수는-값을-바꿀-수-있는-참조를-반환할-수-없습니다">데이터 변경을 막는 const 멤버 함수는, 값을 바꿀 수 있는 참조를 반환할 수 없습니다</h3>
<p><code>const</code> 키워드가 붙은 멤버 함수는 &quot;나는 객체의 상태를 절대 변경하지 않겠다&quot;고 선언한 함수입니다. 따라서 이런 함수는 다른 사람이 멤버 변수를 바꿀 수 있도록 허락하는 &#39;non-const 참조&#39;를 밖으로 반환할 수 없도록 문법적으로 막혀 있습니다.</p>
<p>만약 이게 허용된다면, 함수 자체는 데이터를 안 바꾼다고 약속해 놓고 정작 호출한 사람에게는 &quot;직접 값을 바꿀 수 있는 권한(참조)&quot;을 넘겨주게 되니, <code>const</code> 함수의 진짜 목적을 속이는 셈이 되기 때문입니다.</p>
<hr>
<h2 id="148--데이터-은닉캡슐화의-장점">14.8 — 데이터 은닉(캡슐화)의 장점</h2>
<p>이전 레슨 <code>14.5 -- Public과 private 멤버 및 접근 제어자</code> 에서, 클래스의 멤버 변수는 보통 &#39;private(비공개)&#39;으로 만든다고 말씀드렸습니다. 클래스를 처음 배우는 초보자분들은 종종 왜 이렇게 해야 하는지 이해하기 어려워하십니다. 변수를 private으로 만들면 외부에서 접근할 수 없게 되니까요. 좋게 봐줘도 코드를 작성할 때 일거리만 더 늘어나는 것 같고, 나쁘게 보면 
(특히 private 데이터에 접근할 수 있는 public 함수를 따로 만들어줄 거라면) 완전히 무의미한 짓거리처럼 보일 수도 있습니다.</p>
<p>이 질문에 대한 답은 프로그래밍에서 너무나도 중요하고 기초적인 내용이라, 
이번 레슨 전체를 할애해서 설명해 보려고 합니다!</p>
<p>먼저 비유를 하나 들어볼게요.</p>
<p>현대 사회에서 우리는 수많은 기계나 전자 기기를 사용합니다. 
리모컨으로 TV를 켜고 끄거나, 자동차의 가속 페달을 밟아 앞으로 나아가게 하거나, 스위치를 눌러 불을 켭니다. 이 기기들에는 공통점이 있습니다. 바로 사용자가 핵심적인 동작을 수행할 수 있도록 <strong>간단한 사용자 인터페이스(버튼, 페달, 스위치 등)</strong> 를 제공한다는 점입니다.</p>
<p>하지만 이 기기들이 실제로 <strong>어떻게</strong> 작동하는지는 여러분의 눈에 보이지 않게 &#39;숨겨져&#39; 있습니다. 리모컨 버튼을 누를 때 리모컨이 TV와 어떻게 통신하는지 몰라도 됩니다. 자동차 페달을 밟을 때 엔진이 어떻게 바퀴를 굴리는지 몰라도 됩니다. 사진을 찍을 때 센서가 어떻게 빛을 모아서 픽셀 이미지로 바꾸는지 몰라도 되죠.</p>
<p>이렇게 <strong>인터페이스(사용 방법)</strong> 와 <strong>구현(내부 동작 원리)</strong> 을 분리하는 것은 엄청나게 유용합니다. 기기가 어떻게 작동하는지 낱낱이 이해하지 않아도, 그냥 기기를 &#39;어떻게 다루는지&#39;만 알면 사용할 수 있기 때문이죠. 덕분에 우리가 이런 물건들을 사용하는 과정이 훨씬 단순해지고, 우리가 다룰 수 있는 기기의 종류도 훨씬 많아집니다.</p>
<hr>
<h3 id="클래스에서의-구현과-인터페이스">클래스에서의 구현과 인터페이스</h3>
<p>프로그래밍에서도 비슷한 이유로 인터페이스와 구현을 분리하는 것이 유용합니다. 
그 전에, 클래스에서 말하는 &#39;인터페이스&#39;와 &#39;구현&#39;이 정확히 무슨 뜻인지 정의해 봅시다.</p>
<p>클래스의 <strong>인터페이스</strong>란 사용자가 그 클래스로 만든 객체와 어떻게 상호작용할지를 정의하는 부분입니다. 클래스 외부에서는 &#39;public(공개)&#39; 멤버에만 접근할 수 있기 때문에, 클래스의 public 멤버들이 곧 인터페이스가 됩니다. 이런 이유로 public 멤버들로 구성된 인터페이스를 <strong>퍼블릭 인터페이스</strong> 라고 부르기도 합니다.</p>
<p>인터페이스는 클래스를 만든 사람과 클래스를 사용하는 사람 사이의 무언의 &#39;계약&#39;과도 같습니다. 만약 기존에 있던 인터페이스가 바뀌게 되면, 그걸 사용하던 기존 코드들이 고장 날 수 있습니다. 따라서 클래스의 인터페이스는 처음부터 설계를 잘해서 나중에 자주 바뀌지 않게 안정적으로 만드는 것이 중요합니다.</p>
<p>클래스의 <strong>구현</strong> 이란 클래스가 의도한 대로 동작하게 만드는 &#39;실제 코드&#39;를 말합니다. 데이터를 저장하는 멤버 변수들, 그리고 프로그램의 논리를 담고 데이터를 조작하는 멤버 함수의 알맹이(코드 내용)가 모두 여기에 포함됩니다.</p>
<hr>
<h3 id="데이터-은닉">데이터 은닉</h3>
<p>프로그래밍에서 <strong>데이터 은닉</strong>이란 (정보 은닉 또는 데이터 추상화라고도 부름), 사용자가 데이터 타입의 내부 구현에 접근하지 못하게 숨김으로써 인터페이스와 구현을 강제로 분리하는 기법입니다.</p>
<p>C++ 클래스에서 데이터 은닉을 구현하는 방법은 아주 간단합니다.</p>
<ol>
<li><p>클래스의 데이터 멤버(변수)들을 private으로 만듭니다 (사용자가 직접 건드리지 못하게요). 멤버 함수의 알맹이 코드는 원래부터 사용자가 직접 접근할 수 없으니 따로 건드릴 필요가 없습니다.</p>
</li>
<li><p>사용자가 호출할 수 있도록 멤버 함수들을 public으로 만듭니다.</p>
</li>
</ol>
<p>이 규칙을 따르면, 사용자는 오직 &#39;퍼블릭 인터페이스(public 함수들)&#39;를 통해서만 객체를 다루게 되며, 내부의 복잡한 구현 세부 사항에는 직접 접근할 수 없게 됩니다.</p>
<p>C++에서 정의하는 클래스들은 데이터 은닉을 사용해야 합니다. 
실제로 C++ 표준 라이브러리에서 제공하는 모든 클래스들은 다 이렇게 만들어져 있습니다. 반면에 <strong>구조체</strong>는 데이터 은닉을 사용하면 안 됩니다. 구조체에 public이 아닌 멤버가 있으면 집계형 타입으로 취급받지 못하기 때문입니다.</p>
<p>클래스를 이런 식으로 만들려면 코드를 작성하는 사람은 손이 좀 더 갑니다. 클래스를 사용하는 사람 입장에서도 변수에 직접 접근하는 것보다 퍼블릭 인터페이스를 거쳐야 하니 좀 번거롭게 느껴질 수 있습니다. 하지만 이렇게 하면 클래스의 재사용성과 유지보수성을 높여주는 엄청난 장점들이 생깁니다. 레슨의 나머지 부분에서는 이 장점들에 대해 이야기해 보겠습니다.</p>
<hr>
<h3 id="용어-정리-1">용어 정리</h3>
<p>프로그래밍에서 <strong>캡슐화</strong>라는 용어는 보통 다음 두 가지 중 하나를 뜻합니다.</p>
<ol>
<li>하나 이상의 항목을 어떤 상자(컨테이너) 안에 가둬두는 것.</li>
<li>데이터와 그 데이터를 다루는 함수들을 하나로 묶는 것.</li>
</ol>
<p>C++에서는 데이터를 가지고 있고, 그 데이터를 다루기 위한 퍼블릭 인터페이스를 가진 클래스를 가리켜 &#39;캡슐화되었다&#39;고 말합니다. 캡슐화는 데이터 은닉을 하기 위한 필수 조건이고, 데이터 은닉이 워낙 중요한 기법이다 보니, 보통 &#39;캡슐화&#39;라고 하면 &#39;데이터 은닉&#39;의 의미까지 포함해서 부르는 경우가 많습니다.</p>
<p>이 튜토리얼 시리즈에서는 캡슐화된 모든 클래스가 데이터 은닉을 적용하고 있다고 가정하겠습니다.</p>
<hr>
<h3 id="장점-1-데이터-은닉은-클래스-사용을-쉽게-만들고-복잡성을-줄여줍니다">장점 1: 데이터 은닉은 클래스 사용을 쉽게 만들고 복잡성을 줄여줍니다.</h3>
<p>캡슐화된 클래스를 사용할 때는 그 클래스가 내부적으로 어떻게 만들어졌는지 알 필요가 없습니다. 오직 인터페이스만 알면 됩니다. </p>
<p>즉, &#39;어떤 public 함수를 쓸 수 있는지&#39;, &#39;거기에 무슨 값을 넘겨줘야 하는지&#39;, &#39;결과로 뭘 돌려받는지&#39;만 이해하면 끝입니다.</p>
<p>예를 들어볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

int main()
{
    std::string_view sv{ &quot;Hello, world!&quot; };
    std::cout &lt;&lt; sv.length();

    return 0;
}
</code></pre>
<p>이 짧은 프로그램에서, <code>std::string_view</code>가 내부적으로 어떻게 만들어졌는지는 우리에게 전혀 노출되지 않습니다. 데이터 멤버가 몇 개인지, 이름이 뭔지, 타입이 뭔지 우리는 볼 수 없죠. <code>length()</code> 함수가 문자열의 길이를 정확히 어떻게 계산해서 돌려주는지도 모릅니다.</p>
<p>가장 멋진 점은, <strong>우리가 그걸 알 필요가 없다는 겁니다!</strong> 프로그램은 그냥 잘 돌아갑니다. 
우리는 그저 <code>std::string_view</code> 객체를 처음 만들 때 어떻게 하는지, 그리고 <code>length()</code> 함수가 뭘 반환하는지만 알면 됩니다.</p>
<p>이런 세부 사항을 신경 쓰지 않아도 된다는 건 프로그램의 복잡성을 엄청나게 줄여주고, 결과적으로 실수도 줄여줍니다. 다른 어떤 이유보다도, 이것이 바로 캡슐화의 가장 <strong>핵심적인 장점</strong>입니다.</p>
<p>만약 <code>std::string</code>이나 <code>std::vector</code>, <code>std::cout</code>을 사용하기 위해 그것들이 내부적으로 어떻게 구현되었는지 몽땅 이해해야 한다면, C++가 얼마나 끔찍하게 복잡했을지 상상해 보세요!</p>
<hr>
<h3 id="장점-2-데이터-은닉은-불변성을-유지하게-해줍니다">장점 2: 데이터 은닉은 &#39;불변성&#39;을 유지하게 해줍니다.</h3>
<p>예전 클래스 입문 레슨 <code>14.2 -- Introduction to classes</code> 에서 <strong>클래스 불변성</strong> 이라는 개념을 소개한 적이 있습니다. 이는 객체가 살아있는 동안 &#39;정상적인 상태&#39;를 유지하기 위해 항상 참이어야 하는 조건들을 말합니다.</p>
<p>다음 프로그램을 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

struct Employee // 기본적으로 멤버들은 public(공개) 상태입니다
{
    std::string name{ &quot;John&quot; };
    char firstInitial{ &#39;J&#39; }; // 이름의 첫 글자와 일치해야 합니다

    void print() const
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; name &lt;&lt; &quot; has first initial &quot; &lt;&lt; firstInitial &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Employee e{}; // 기본값인 &quot;John&quot;과 &#39;J&#39;로 설정됩니다
    e.print();

    e.name = &quot;Mark&quot;; // 직원의 이름을 &quot;Mark&quot;로 바꿉니다
    e.print(); // 잘못된 첫 글자가 출력됩니다

    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과는 다음과 같습니다.</p>
<pre><code class="language-text">Employee John has first initial J
Employee Mark has first initial J
</code></pre>
<p>우리 <code>Employee</code> 구조체에는 <code>firstInitial</code>(첫 글자)이 항상 <code>name</code>(이름)의 첫 번째 문자와 같아야 한다는 &#39;불변성(규칙)&#39;이 있습니다. 만약 이 규칙이 깨지면 <code>print()</code> 함수는 엉뚱한 결과를 내놓게 됩니다.</p>
<p><code>name</code> 멤버가 public이기 때문에, <code>main()</code> 함수에서 <code>e.name</code>을 <code>&quot;Mark&quot;</code>로 직접 바꿀 수 있었습니다. 하지만 이때 <code>firstInitial</code> 멤버는 업데이트되지 않았죠. 우리의 규칙(불변성)이 깨졌고, 두 번째 <code>print()</code> 호출은 예상대로 동작하지 않았습니다.</p>
<p>이렇게 사용자에게 클래스의 내부 구현에 직접 접근할 권한을 주면, 모든 불변성(규칙)을 유지해야 하는 책임이 고스란히 사용자에게 넘어갑니다. 하지만 사용자는 그걸 제대로 안 할 수도 있죠. 사용자에게 이런 짐을 지우는 것은 코드를 매우 복잡하게 만듭니다.</p>
<p>이제 변수들을 private으로 숨기고, 직원의 이름을 설정하는 멤버 함수(public)를 제공하도록 프로그램을 다시 써보겠습니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee // 기본적으로 멤버들은 private(비공개) 상태입니다
{
    std::string m_name{};
    char m_firstInitial{};

public:
    void setName(std::string_view name)
    {
        m_name = name;
        m_firstInitial = name.front(); // std::string::front()를 사용해 `name`의 첫 글자를 가져옵니다
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; has first initial &quot; &lt;&lt; m_firstInitial &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Employee e{};
    e.setName(&quot;John&quot;);
    e.print();

    e.setName(&quot;Mark&quot;);
    e.print();

    return 0;
}
</code></pre>
<p>이제 프로그램이 예상대로 잘 동작합니다:</p>
<pre><code class="language-text">Employee John has first initial J
Employee Mark has first initial M
</code></pre>
<p>사용자 입장에서 바뀐 건 딱 하나입니다. <code>name</code> 변수에 직접 값을 넣는 대신, <code>setName()</code>이라는 함수를 호출하는 거죠. 그러면 이 함수가 알아서 <code>m_name</code>과 <code>m_firstInitial</code>을 둘 다 올바르게 설정해 줍니다. 사용자는 이제 이 규칙(불변성)을 신경 써야 하는 부담에서 완전히 벗어났습니다!</p>
<hr>
<h3 id="장점-3-데이터-은닉은-오류를-더-잘-잡아내고-처리하게-해줍니다">장점 3: 데이터 은닉은 오류를 더 잘 잡아내고 처리하게 해줍니다.</h3>
<p>위 프로그램에서 <code>m_firstInitial</code>이 <code>m_name</code>의 첫 글자와 같아야 한다는 규칙이 생긴 이유는, <code>m_firstInitial</code>이라는 변수가 <code>m_name</code>과 별개로 존재하기 때문입니다. </p>
<p>아예 <code>m_firstInitial</code> 변수를 없애버리고, 대신 첫 글자를 반환하는 멤버 함수를 만들면 이 규칙 자체를 없앨 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

class Employee
{
    std::string m_name{ &quot;John&quot; };

public:
    void setName(std::string_view name)
    {
        m_name = name;
    }

    // std::string::front()를 사용해 `m_name`의 첫 글자를 가져옵니다
    char firstInitial() const { return m_name.front(); }

    void print() const
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; has first initial &quot; &lt;&lt; firstInitial() &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Employee e{}; // 기본값인 &quot;John&quot;으로 설정됩니다
    e.setName(&quot;Mark&quot;);
    e.print();

    return 0;
}
</code></pre>
<p>하지만, 이 프로그램에도 여전히 지켜야 할 &#39;클래스 불변성&#39;이 하나 숨어 있습니다. 
잠깐 멈춰서 그게 뭔지 한번 알아맞혀 보세요.</p>
<p>정답은 바로 <strong><code>m_name</code>이 빈 문자열(&quot;&quot;)이면 안 된다</strong> 는 것입니다.
(모든 직원은 이름이 있어야 하니까요) 만약 <code>m_name</code>이 빈 문자열로 설정되면 당장은 아무 일도 안 일어나겠지만, 나중에 <code>firstInitial()</code>이 호출될 때 <code>std::string</code>의 <code>front()</code> 함수가 텅 빈 문자열의 첫 글자를 가져오려다가 &#39;정의되지 않은 동작&#39;을 일으키게 됩니다.</p>
<p>이상적으로는 <code>m_name</code>이 애초에 텅 빌 수 없게 막고 싶을 겁니다.</p>
<p>만약 사용자가 <code>m_name</code>에 직접 접근할 수 있다면, 
그냥 <code>m_name = &quot;&quot;</code> 이라고 써버리면 그만이고 우리가 막을 방법이 없습니다.</p>
<p>하지만 우리는 사용자가 반드시 <code>setName()</code>이라는 public 함수를 통해서만 이름을 설정하도록 강제했기 때문에, <code>setName()</code> 함수 안에서 사용자가 준 이름이 올바른지 <strong>검사(validate)</strong> 할 수 있습니다. 이름이 비어있지 않다면 <code>m_name</code>에 저장하고, 만약 빈 문자열이라면 다음과 같은 여러 가지 대처를 할 수 있습니다:</p>
<ul>
<li>빈 문자열로 설정하려는 요청을 무시하고 그냥 돌아가기.</li>
<li>Assert(단언문)로 프로그램 중단시키기.</li>
<li>예외(Exception) 던지기.</li>
<li>호키포키 춤추기. 아, 잠깐, 이건 아니네요.</li>
</ul>
<p>여기서 핵심은 <strong>사용자의 잘못된 사용을 미리 감지하고, 우리가 보기에 가장 적절한 방식으로 알아서 처리할 수 있다</strong> 는 점입니다. 이런 상황을 어떻게 처리하는 게 가장 좋은지는 나중에 다른 레슨에서 다루겠습니다.</p>
<hr>
<h3 id="장점-4-데이터-은닉을-사용하면-기존-프로그램을-망가뜨리지-않고-내부-코드를-바꿀-수-있습니다">장점 4: 데이터 은닉을 사용하면 기존 프로그램을 망가뜨리지 않고 내부 코드를 바꿀 수 있습니다.</h3>
<p>이 간단한 예제를 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Something
{
    int value1 {};
    int value2 {};
    int value3 {};
};

int main()
{
    Something something;
    something.value1 = 5;
    std::cout &lt;&lt; something.value1 &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p>이 프로그램은 잘 돌아가지만, 만약 우리가 클래스 내부를 다음과 같이 바꾼다면 어떻게 될까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Something
{
    int value[3] {}; // 3개의 값을 가진 배열을 사용합니다
};

int main()
{
    Something something;
    something.value1 = 5;
    std::cout &lt;&lt; something.value1 &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p>아직 배열을 배우진 않았지만 신경 쓰지 마세요. 중요한 건, <code>value1</code>이라는 이름의 변수가 더 이상 존재하지 않기 때문에 이 프로그램은 아예 <strong>컴파일조차 되지 않는다</strong> 는 점입니다. 
<code>main()</code> 함수에서는 여전히 사라진 <code>value1</code>이라는 이름을 쓰고 있으니까요.</p>
<p>데이터 은닉을 하면, 클래스를 사용하는 기존 프로그램들을 망가뜨리지 않고도 클래스의 내부 작동 방식을 마음대로 바꿀 수 있습니다.</p>
<p>위 예제를 캡슐화해서 <code>m_value1</code>에 접근하는 함수를 만든 버전입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
private:
    int m_value1 {};
    int m_value2 {};
    int m_value3 {};

public:
    void setValue1(int value) { m_value1 = value; }
    int getValue1() const { return m_value1; }
};

int main()
{
    Something something;
    something.setValue1(5);
    std::cout &lt;&lt; something.getValue1() &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p>이제 이 클래스의 내부를 다시 배열로 바꿔보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
private:
    int m_value[3]; // 참고: 이 클래스의 내부 구현을 변경했습니다!

public:
    // 새로운 내부 구현에 맞춰 멤버 함수들도 업데이트해야 합니다
    void setValue1(int value) { m_value[0] = value; }
    int getValue1() const { return m_value[0]; }
};

int main()
{
    // 하지만 이 클래스를 사용하는 바깥쪽 프로그램 코드는 수정할 필요가 없습니다!
    Something something;
    something.setValue1(5);
    std::cout &lt;&lt; something.getValue1() &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p>우리가 클래스의 &#39;퍼블릭 인터페이스(함수 이름이나 사용법)&#39;를 바꾸지 않았기 때문에, 이 인터페이스를 사용하는 바깥쪽 프로그램(<code>main()</code>)은 단 한 줄도 고칠 필요가 없었고 예전과 똑같이 잘 돌아갑니다.</p>
<p>비유하자면, 밤에 요정들이 몰래 집에 들어와서 TV 리모컨의 내부 부품을 다른 (하지만 호환되는) 기술로 싹 바꿔놓아도, 여러분은 겉보기엔 똑같이 작동하니까 아마 눈치채지도 못할 겁니다!</p>
<hr>
<h3 id="장점-5-인터페이스가-있는-클래스는-버그-찾기디버깅가-더-쉽습니다">장점 5: 인터페이스가 있는 클래스는 버그 찾기(디버깅)가 더 쉽습니다.</h3>
<p>마지막으로, 캡슐화는 프로그램에 문제가 생겼을 때 원인을 찾는 데 큰 도움을 줍니다. 프로그램이 오작동하는 경우는 보통 멤버 변수 중 하나에 잘못된 값이 들어갔기 때문입니다. 만약 누구나 변수에 직접 접근해서 값을 바꿀 수 있다면, 수많은 코드 중에서 도대체 
&#39;어느 부분&#39;이 이 변수를 망쳐놨는지 추적하기가 굉장히 힘듭니다. 변수를 수정하는 모든 곳에 중단점을 걸고 확인해야 하는데, 그런 곳이 수십, 수백 군데일 수도 있거든요.</p>
<p>하지만 오직 &#39;단 하나의 멤버 함수&#39;를 통해서만 변수를 바꿀 수 있게 해 두면, 그냥 그 함수 딱 하나에만 중단점을 걸어두고 누가 어떤 값을 넘겨주는지 지켜보면 됩니다. 범인을 잡아내기가 훨씬 쉬워지는 거죠.</p>
<hr>
<h3 id="멤버-함수보다는-비멤버-함수를-선호하세요">멤버 함수보다는 &#39;비멤버 함수&#39;를 선호하세요</h3>
<p>C++에서는 어떤 함수를 클래스 바깥의 일반 함수(비멤버 함수)로 만들 수 있다면, 
클래스 안의 멤버 함수로 만드는 것보다 <strong>비멤버 함수로 만드는 것을 권장</strong> 합니다.</p>
<p>여기에는 수많은 장점이 있습니다.</p>
<ol>
<li><p>비멤버 함수는 클래스 인터페이스의 일부가 아닙니다. 따라서 클래스의 인터페이스가 훨씬 작고 단순해져서, 클래스를 이해하기가 더 쉬워집니다.</p>
</li>
<li><p>비멤버 함수는 클래스의 내부 구현에 직접 접근할 수 없고 반드시 퍼블릭 인터페이스를 거쳐야 하므로, 캡슐화 원칙을 강제로 지키게 만듭니다. 
&#39;편하다&#39;는 이유로 내부 구현을 몰래 건드리고 싶은 유혹을 차단합니다.</p>
</li>
<li><p>클래스의 내부 구현을 바꿀 때, 비멤버 함수는 신경 쓰지 않아도 됩니다 
(인터페이스 사용법 자체가 바뀌지 않는 한).</p>
</li>
<li><p>비멤버 함수가 보통 디버깅하기 더 쉽습니다.</p>
</li>
<li><p>특정 프로그램에만 필요한 데이터나 논리를 담은 비멤버 함수를, 여기저기 재사용하기 좋은 클래스 본체로부터 분리해 낼 수 있습니다.</p>
</li>
</ol>
<p>만약 Java나 C# 같은 최신 객체 지향 언어(OOP)를 써보신 적이 있다면, 이 조언이 꽤 놀랍게 들리실 겁니다. 그 언어들은 &#39;클래스가 우주의 중심&#39;이고 모든 것이 클래스를 중심으로 돌아간다는 개념을 사용합니다. 그래서 멤버 함수를 가장 중요하게 생각하죠 
(사실 Java나 C#은 비멤버 함수 자체를 아예 지원하지 않습니다).</p>
<blockquote>
<p><strong>모범 사례</strong>
가능하면 함수는 비멤버(클래스 밖의 일반 함수)로 구현하는 것이 좋습니다. 
(특히 특정 프로그램에만 딱 맞춰진 논리나 데이터가 들어간 함수일수록 더 그렇습니다).</p>
</blockquote>
<p><strong>팁 (Tip)</strong>
함수를 멤버로 만들지 비멤버로 만들지 결정하는 간단한 가이드라인입니다.</p>
<ol>
<li><p><strong>어쩔 수 없을 때는 멤버 함수를 쓰세요.</strong> 
C++에서는 생성자, 소멸자, 가상 함수, 특정 연산자 등 몇몇 함수들은 반드시 멤버 함수로만 만들어야 합니다. (다음 레슨에서 생성자에 대해 배울 때 보게 될 거예요).</p>
</li>
<li><p>외부로 노출하면 안 되는 private(또는 protected) 데이터에 접근해야만 하는 함수라면 </p>
</li>
</ol>
<p><strong>멤버 함수를 우선 고려</strong>하세요.
3. 그 외의 경우(특히 객체의 상태를 바꾸지 않는 함수라면)에는 <strong>비멤버 함수를 우선 고려</strong>하세요.</p>
<p>마지막 두 가지 규칙에는 예외가 좀 있는데, 이건 나중에 관련 주제를 다룰 때 설명해 드릴게요.</p>
<p>한 가지 고민되는 상황은, 비멤버 함수를 쓰려고 했더니 그러기 위해 클래스에 새로운 접근 함수(Getter 등)를 억지로 추가해야 할 때입니다. 이럴 때는 장단점을 따져봐야 합니다.
접근 함수를 추가한다는 건 인터페이스의 크기와 복잡성이 커진다는 뜻입니다. 이 새로운 접근 함수를 다른 곳에서도 여러 번 쓸 게 아니라면, 굳이 추가할 가치가 없을 수도 있습니다.
(내부 상태라서) 밖으로 노출되면 안 되거나, 사용자가 클래스의 불변성을 깨뜨릴 위험이 있는 데이터에는 <strong>절대 접근 함수(Getter/Setter)를 추가하지 마세요.</strong></p>
<p>세 가지 비슷한 예제를 통해 &#39;가장 안 좋은 방식&#39;부터 &#39;가장 좋은 방식&#39;까지 순서대로 비교해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

class Yogurt
{
    std::string m_flavor{ &quot;vanilla&quot; };

public:
    void setFlavor(std::string_view flavor)
    {
        m_flavor = flavor;
    }

    const std::string&amp; getFlavor() const { return m_flavor; }

    // 최악: getter 함수가 있는데도 print() 멤버 함수가 m_flavor에 직접 접근합니다
    void print() const
    {
        std::cout &lt;&lt; &quot;The yogurt has flavor &quot; &lt;&lt; m_flavor &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Yogurt y{};
    y.setFlavor(&quot;cherry&quot;);
    y.print();

    return 0;
}
</code></pre>
<p>위 코드는 <strong>최악의 버전</strong> 입니다. 
맛을 알려주는 getter(<code>getFlavor()</code>)가 있는데도 <code>print()</code> 멤버 함수가 <code>m_flavor</code>에 직접 접근하고 있습니다. 나중에 클래스 내부 구현이 바뀌면 <code>print()</code>도 같이 뜯어고쳐야 할 확률이 높습니다. 게다가 <code>print()</code>가 출력하는 문구는 이 프로그램에만 맞춰진 내용이라, 다른 프로그램에서 이 클래스를 쓰려면 문구를 바꾸기 위해 클래스를 복사하거나 수정해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

class Yogurt
{
    std::string m_flavor{ &quot;vanilla&quot; };

public:
    void setFlavor(std::string_view flavor)
    {
        m_flavor = flavor;
    }

    const std::string&amp; getFlavor() const { return m_flavor; }

    // 더 나음: print() 멤버 함수가 데이터 멤버에 직접 접근하지 않습니다
    void print(std::string_view prefix) const
    {
        std::cout &lt;&lt; prefix &lt;&lt; &#39; &#39; &lt;&lt; getFlavor() &lt;&lt; &#39;\n&#39;;
    }
};

int main()
{
    Yogurt y{};
    y.setFlavor(&quot;cherry&quot;);
    y.print(&quot;The yogurt has flavor&quot;);

    return 0;
}
</code></pre>
<p>위 버전은 <strong>더 낫지만</strong>, 아직 완벽하진 않습니다. 
<code>print()</code>가 여전히 멤버 함수이긴 하지만, 최소한 변수에 직접 접근하진 않습니다. 
내부 구현이 바뀌더라도 <code>print()</code>를 고칠 필요가 없습니다. 출력할 앞부분 문구(<code>prefix</code>)를 매개변수로 받게 만들어서, 문구를 결정하는 역할을 클래스 밖의 <code>main()</code>으로 넘겼습니다. 하지만 이 함수는 여전히 출력 방식(앞문구 + 공백 + 맛 + 줄바꿈)을 강제합니다. 만약 다른 프로그램에서 출력 형식을 다르게 하고 싶다면 또 다른 함수를 추가해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

class Yogurt
{
    std::string m_flavor{ &quot;vanilla&quot; };

public:
    void setFlavor(std::string_view flavor)
    {
        m_flavor = flavor;
    }

    const std::string&amp; getFlavor() const { return m_flavor; }
};

// 최고: 멤버 함수가 아닌 일반 함수 print()는 클래스 인터페이스의 일부가 아닙니다
void print(const Yogurt&amp; y)
{
    std::cout &lt;&lt; &quot;The yogurt has flavor &quot; &lt;&lt; y.getFlavor() &lt;&lt; &#39;\n&#39;;
}

int main()
{
    Yogurt y{};
    y.setFlavor(&quot;cherry&quot;);
    print(y);

    return 0;
}
</code></pre>
<p>위 버전이 <strong>가장 좋은 버전</strong> 입니다. <code>print()</code>는 이제 클래스 밖으로 완전히 빠져나온 비멤버 함수입니다. 멤버 변수에도 직접 접근하지 않습니다. 클래스 내부가 바뀌어도 <code>print()</code>는 전혀 신경 쓸 필요가 없습니다. 게다가, 이 클래스를 가져다 쓰는 각 프로그램들은 <strong>자기들 입맛에 맞게 출력하는 자신만의 <code>print()</code> 함수를 따로 만들어서 쓰면 됩니다.</strong></p>
<hr>
<h3 id="클래스-멤버를-적는-순서">클래스 멤버를 적는 순서</h3>
<p>일반적인 코드를 작성할 때는 변수나 함수를 사용하기 &#39;전에&#39; 무조건 먼저 선언해야 합니다. 하지만 클래스 안에서는 이런 제약이 없습니다. 이전 레슨(14.3 -- Member functions)에서 말씀드렸듯, 멤버들을 <strong>원하는 순서대로</strong> 마음껏 배치할 수 있습니다.</p>
<p>그렇다면 어떤 순서로 적는 게 제일 좋을까요?</p>
<p>크게 두 가지 파벌(?)이 있습니다.</p>
<ol>
<li><strong>private 멤버를 먼저 쓰고, 그 아래에 public 멤버 함수를 쓰는 방식:</strong> 전통적인 &#39;선언 후 사용&#39; 스타일에 가깝습니다. 코드를 읽는 사람은 멤버 함수가 쓰기 전에 데이터 변수들이 어떻게 생겼는지 먼저 볼 수 있어서, 세부적인 내부 구현을 이해하는 데 도움이 됩니다.</li>
<li><strong>public 멤버를 맨 위에 쓰고, private 멤버를 맨 아래로 빼는 방식:</strong> 이 클래스를 사용하는 다른 사람들은 &#39;퍼블릭 인터페이스(사용법)&#39;에 가장 관심이 많습니다. 그래서 가장 필요한 정보를 맨 위에 두고, (가장 안 중요한) 내부 구현을 맨 밑으로 밀어버리는 겁니다.</li>
</ol>
<p>현대 C++에서는, 특히 다른 개발자들과 코드를 공유할 때는 <strong>두 번째 방법(public 먼저, private 나중에)</strong> 을 훨씬 더 많이 추천합니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
public 멤버를 가장 먼저 선언하고, 그다음에 protected 멤버, 마지막으로 private 멤버를 선언하세요. 이렇게 하면 외부에 공개되는 인터페이스를 돋보이게 하고, 내부 세부 구현은 덜 눈에 띄게 숨겨줍니다.</p>
</blockquote>
<blockquote>
<p><strong>저자의 말</strong>
이 사이트에 있는 대부분의 예제 코드는 추천 방식과 정반대의 순서(private 먼저)를 사용하고 있습니다. 역사적인 이유도 있지만, 언어의 원리를 막 배우는 단계에서는 내부가 어떻게 돌아가는지 해부하고 분석하는 데 집중하기 때문에 변수가 먼저 보이는 게 더 직관적이라고 생각하기 때문입니다.</p>
</blockquote>
<blockquote>
<p><strong>고급 독자를 위해</strong>
Google C++ 스타일 가이드에서는 다음 순서를 권장합니다.</p>
</blockquote>
<ol>
<li>타입 및 타입 별칭 (typedef, using, enum, 중첩 구조체/클래스, friend 타입)</li>
<li>Static(정적) 상수</li>
<li>팩토리 함수</li>
<li>생성자 및 대입 연산자</li>
<li>소멸자</li>
<li>그 외 모든 함수 (static 및 비-static 멤버 함수, friend 함수)</li>
<li>데이터 멤버 변수 (static 및 비-static)</li>
</ol>
<hr>
<h2 id="149--생성자constructors-소개">14.9 — 생성자(Constructors) 소개</h2>
<p>만약 클래스가 <strong>집계</strong> 형태일 때는, 우리는 &#39;집계 초기화&#39;라는 방법을 써서 클래스를 아주 직접적이고 간단하게 초기화할 수 있습니다.</p>
<pre><code class="language-cpp">struct Foo // Foo는 집계(aggregate) 형태입니다
{
    int x {};
    int y {};
};

int main()
{
    Foo foo { 6, 7 }; // 집계 초기화를 사용합니다

    return 0;
}
</code></pre>
<p>집계 초기화는 멤버 변수들이 정의된 순서대로 차례차례 값을 넣어줍니다. 그래서 위 예제에서 <code>foo</code> 객체가 짠 하고 만들어질 때, <code>foo.x</code> 에는 6이 들어가고 <code>foo.y</code> 에는 7이 들어갑니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
<strong>집계</strong>가 정확히 무엇인지, 그리고 집계 초기화에 대한 자세한 내용은 
<code>13.8 - 구조체 집계 초기화</code> 강의에서 다루었습니다.</p>
</blockquote>
<p>하지만 데이터를 안전하게 숨기기 위해 멤버 변수 중 단 하나라도 <code>private</code> (비공개)으로 만드는 순간, 이 클래스는 더 이상 집계 형태가 아닙니다. (집계 형태는 <code>private</code> 멤버를 가질 수 없다는 규칙이 있거든요.) 즉, 더 이상 그 편리했던 집계 초기화를 쓸 수 없다는 뜻입니다.</p>
<pre><code class="language-cpp">class Foo // Foo는 집계 형태가 아닙니다 (private 멤버를 가지고 있기 때문이죠)
{
    int m_x {};
    int m_y {};
};

int main()
{
    Foo foo { 6, 7 }; // 컴파일 에러: 집계 초기화를 사용할 수 없습니다

    return 0;
}
</code></pre>
<p><code>private</code> 멤버가 있는 클래스에서 집계 초기화를 막아둔 것은 사실 아주 합리적인 이유가 있습니다.</p>
<ol>
<li><p>집계 초기화를 하려면 클래스 내부가 어떻게 생겼는지(멤버가 무엇이고 어떤 순서로 만들어졌는지) 다 알아야 합니다. 하지만 우리가 데이터를 <code>private</code> 으로 숨기는 이유는 바로 그런 내부 사정을 외부에서 모르게 감추려고 하는 것이니까요.</p>
</li>
<li><p>만약 클래스가 반드시 지켜야 하는 규칙(불변성)을 가지고 있다면, 
사용자가 마음대로 아무 값이나 집어넣게 놔두면 안 되기 때문입니다.</p>
</li>
</ol>
<p>그렇다면 <code>private</code> 멤버 변수를 가진 클래스는 도대체 어떻게 초기화해야 할까요? 
아까 예제에서 컴파일러가 뿜어낸 에러 메시지에 힌트가 있습니다.</p>
<p>*&quot;error: no matching constructor for initialization of ‘Foo&#39;&quot; 
(에러: &#39;Foo&#39;를 초기화할 수 있는 일치하는 생성자가 없습니다)*</p>
<p>아하! 우리에게는 딱 맞는 생성자가 필요한 거였네요. 그런데 생성자가 대체 뭘까요?</p>
<hr>
<h3 id="생성자">생성자</h3>
<p><strong>생성자(Constructor)</strong> 란 집계 형태가 아닌 클래스 객체가 만들어진 직후에 알아서 자동으로 실행되는 아주 특별한 함수입니다.</p>
<p>우리가 집계 형태가 아닌 객체를 만들려고 할 때, 컴파일러는 우리가 넘겨준 초기화 값
(예: 6과 7)을 받아줄 수 있는 &#39;접근 가능한 생성자&#39;가 있는지 열심히 찾아봅니다.</p>
<ul>
<li>만약 딱 맞는 생성자를 찾으면, 객체가 들어갈 메모리 공간을 먼저 확보한 다음, 
그 생성자 함수를 실행합니다.</li>
<li>만약 딱 맞는 생성자를 찾지 못하면, 아까 보신 것처럼 컴파일 에러가 발생합니다.</li>
</ul>
<blockquote>
<p><strong>핵심 포인트</strong>
초보 프로그래머분들이 &quot;생성자가 객체를 만들어내는(create) 건가요?&quot; 하고 많이들 헷갈려합니다. <strong>정답은 &#39;아닙니다&#39;</strong> 에요. 컴파일러가 생성자를 부르기 전에 이미 객체가 들어갈 빈 공간(메모리)을 미리 다 준비해 둡니다. 생성자는 그저 &#39;아직 텅 비어있는 객체&#39;에 불려가서 내부를 꾸며주는(초기화하는) 역할만 할 뿐입니다.
하지만 주의할 점은, 우리가 넘겨준 값과 짝이 맞는 생성자를 찾지 못하면 컴파일러가 에러를 내기 때문에, 결과적으로 생성자가 없으면 객체 자체를 만들 수 없는 것은 맞습니다.</p>
</blockquote>
<p>생성자는 객체를 어떻게 만들지 결정하는 것 외에도 보통 다음 두 가지 중요한 일을 합니다.</p>
<ol>
<li>멤버 변수들을 초기화합니다 (보통 &#39;멤버 초기화 리스트&#39;라는 것을 사용합니다).</li>
<li>다른 필요한 준비 작업을 합니다. 예를 들어, 들어온 값이 올바른지 검사하거나, 파일이나 데이터베이스를 여는 등의 작업을 생성자 안에서 할 수 있습니다.</li>
</ol>
<p>생성자가 무사히 자기 할 일을 마치면, 우리는 &quot;객체가 잘 <strong>생성(constructed)</strong> 되었다&quot; 고 말합니다. 이제 이 객체는 안심하고 사용할 수 있는 완벽한 상태가 된 것입니다.</p>
<p>참고로, 집계 형태는 생성자를 가질 수 없게 되어 있습니다. 만약 여러분이 집계 형태였던 구조체나 클래스에 생성자를 직접 추가해버리면, 그건 더 이상 집계 형태가 아니게 됩니다.</p>
<hr>
<h3 id="생성자-이름-짓기">생성자 이름 짓기</h3>
<p>일반적인 멤버 함수들과 다르게, 생성자는 이름을 지을 때 꼭 지켜야 할 엄격한 규칙이 있습니다.</p>
<ul>
<li>생성자의 이름은 <strong>반드시 클래스 이름과 똑같아야 합니다</strong> (대소문자까지 완벽히 같아야 합니다). 만약 템플릿 클래스라면, 템플릿 매개변수 부분은 뺀 기본 이름만 사용합니다.</li>
<li>생성자는 반환값(return type)이 아예 없습니다 (<code>void</code> 조차도 쓰지 않습니다).</li>
<li>생성자는 주로 사람들이 외부에서 객체를 만들 때 사용하므로 보통 <code>public</code> (공개)으로 설정합니다.</li>
</ul>
<hr>
<h3 id="기본적인-생성자-예제">기본적인 생성자 예제</h3>
<p>아까 에러가 났던 코드에 기본적인 생성자를 추가해서 고쳐볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo(int x, int y) // 여기가 바로 두 개의 초기화 값을 받아주는 생성자 함수입니다
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; x &lt;&lt; &quot;, &quot; &lt;&lt; y &lt;&lt; &quot;) constructed\n&quot;;
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Foo foo{ 6, 7 }; // Foo(int, int) 형태의 생성자를 자동으로 부릅니다
    foo.print();

    return 0;
}
</code></pre>
<p>이제 이 프로그램은 정상적으로 실행되고 다음과 같은 결과를 보여줍니다:</p>
<pre><code class="language-text">Foo(6, 7) constructed
Foo(0, 0)
</code></pre>
<p>컴파일러가 <code>Foo foo{ 6, 7 }</code> 이라는 코드를 보면, 정수(<code>int</code>) 두 개를 받을 수 있는 <code>Foo</code> 생성자가 있는지 찾아봅니다. 방금 우리가 만든 <code>Foo(int, int)</code> 가 딱 맞기 때문에 컴파일러가 통과시켜 준 것입니다!</p>
<p>프로그램이 실행될 때 <code>foo</code> 객체가 만들어지면, 메모리 자리를 먼저 맡아둔 다음 
<code>Foo(int, int)</code> 생성자가 실행됩니다. 이때 매개변수 <code>x</code> 에는 6이, <code>y</code> 에는 7이 들어갑니다. 그리고 생성자 안의 내용이 실행되면서 화면에 <code>Foo(6, 7) constructed</code> 라고 출력되죠.</p>
<p>그런데 화면에 출력된 두 번째 줄을 보면 쪼금 이상합니다. 
<code>print()</code> 함수를 불렀을 때 멤버 변수인 <code>m_x</code> 와 <code>m_y</code> 의 값이 0으로 나오죠?
그 이유는 우리가 <code>Foo(int, int)</code> 생성자를 부르긴 했지만, 생성자 안에서 실제로 멤버 변수들에 그 값을 넣어주는 코드를 작성하지 않았기 때문입니다. 진짜로 멤버 변수에 값을 넣어주는 방법은 다음 강의에서 자세히 배울 예정입니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
생성자로 객체를 초기화할 때 쓰이는 복사(copy), 직접(direct), 리스트(list) 초기화 방식의 차이점은 <code>14.15 - 클래스 초기화와 복사 생략</code> 강의에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="생성자의-암시적-형-변환-알아서-타입-맞춰주기">생성자의 암시적 형 변환 (알아서 타입 맞춰주기)</h3>
<p>이전 <code>10.1 - 암시적 형 변환</code> 강의에서, 함수에 넘겨준 값의 타입이 함수가 원래 기대했던 타입과 다를 경우, 컴파일러가 알아서(암시적으로) 타입을 변환해 준다는 것을 배웠습니다.</p>
<pre><code class="language-cpp">void foo(int, int)
{
}

int main()
{
    foo(&#39;a&#39;, true); // &#39;a&#39;와 true가 알아서 int로 바뀌어 foo(int, int)와 짝지어집니다

    return 0;
}
</code></pre>
<p>생성자도 이와 전혀 다르지 않습니다!.
<code>Foo(int, int)</code> 생성자는 <code>int</code> 로 몰래 바꿀 수 있는 값이라면 어떤 값이 들어와도 찰떡같이 받아줍니다.</p>
<pre><code class="language-cpp">class Foo
{
public:
    Foo(int x, int y)
    {
    }
};

int main()
{
    Foo foo{ &#39;a&#39;, true }; // &#39;a&#39;와 true가 int로 변환되어 Foo(int, int) 생성자를 부릅니다

    return 0;
}
</code></pre>
<hr>
<h3 id="생성자는-const상수이면-안-됩니다">생성자는 const(상수)이면 안 됩니다</h3>
<p>생성자가 존재하는 이유는 바로 새로 만들어지는 객체에 처음 값을 세팅해 주는 것입니다. 
값을 세팅(변경)해야 하는데, 절대 변경할 수 없음을 뜻하는 <code>const</code> 를 생성자에 붙이면 안 되겠죠? 따라서 생성자는 절대로 <code>const</code> 일 수 없습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
private:
    int m_x{};

public:
    Something() // 생성자는 반드시 non-const(const가 아닌 상태)여야 합니다
    {
        m_x = 5; // non-const 생성자 안에서 멤버 값을 바꾸는 건 전혀 문제 없습니다
    }

    int getX() const { return m_x; } // 이 함수는 const 함수입니다
};

int main()
{
    const Something s{}; // const 객체를 만들지만, 그 과정에서 (non-const인) 생성자를 알아서 부릅니다

    std::cout &lt;&lt; s.getX(); // 5가 출력됩니다

    return 0;
}
</code></pre>
<p>원래 <code>const</code> 로 만들어진 객체에는 값을 바꿀 여지가 있는 함수(non-const 함수)를 쓸 수 없습니다. </p>
<p>하지만 C++ 표준 규칙에 따르면, <strong>&quot;객체가 처음 조립(생성)되고 있는 과정 중에는 <code>const</code> 규칙이 적용되지 않으며, 생성자가 완전히 끝난 후부터 비로소 <code>const</code> 규칙이 켜진다&quot;</strong> 고 명시되어 있습니다. (그래서 안심하고 초기화할 수 있는 것입니다!)</p>
<hr>
<h3 id="생성자constructors-vs-세터setters">생성자(Constructors) vs 세터(Setters)</h3>
<ul>
<li><strong>생성자</strong>는 객체가 처음 세상에 태어나는 순간에 객체 전체를 한 번에 초기화하기 위해 만들어졌습니다.</li>
<li><strong>세터(Setter)</strong> 함수는 이미 만들어져서 살아가고 있는 객체의 특정 멤버 변수 하나의 값만 콕 집어서 바꿀 때 사용하기 위해 만들어졌습니다.</li>
</ul>
<hr>
<h2 id="1410--생성자-멤버-초기화-리스트">14.10 — 생성자 멤버 초기화 리스트</h2>
<h3 id="멤버-초기화-리스트로-멤버-변수-초기화하기"><strong>멤버 초기화 리스트</strong>로 멤버 변수 초기화하기</h3>
<p>생성자가 클래스 안의 멤버 변수들에 처음 값을 초기화하도록 만들려면,
<strong>멤버 초기화 리스트</strong>라는 것을 사용해요. (배열 같은 걸 초기화할 때 쓰는 그냥 &#39;초기화 리스트&#39;와 이름이 비슷해서 헷갈릴 수 있으니 주의하세요!)</p>
<p>백문이 불여일견이죠. 예제를 보는 게 가장 이해하기 쉽습니다. 
아래 예제에서 <code>Foo(int, int)</code> 생성자는 <code>m_x</code> 와 <code>m_y</code> 에 값을 넣기 위해 멤버 초기화 리스트를 사용하도록 업데이트되었습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo(int x, int y)
        : m_x { x }, m_y { y } // 여기가 바로 우리의 멤버 초기화 리스트입니다!
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; x &lt;&lt; &quot;, &quot; &lt;&lt; y &lt;&lt; &quot;) 가 생성되었습니다\n&quot;;
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Foo foo{ 6, 7 };
    foo.print();

    return 0;
}
</code></pre>
<p>멤버 초기화 리스트는 생성자가 받는 매개변수 괄호 바로 뒤에 적어줍니다.
시작할 때 콜론(<code>:</code>)을 찍고, 초기화할 멤버 변수와 그 값을 쉼표(<code>,</code>)로 구분해서 나열하면 돼요.</p>
<p>여기서는 반드시 중괄호 <code>{}</code> 나 괄호 <code>()</code> 를 사용하는 <strong>직접 초기화</strong> 방식을 써야 합니다. 
(중괄호를 쓰는 것을 더 권장해요) </p>
<p>등호(<code>=</code>)를 사용하는 복사 초기화 방식은 여기선 작동하지 않거든요. 
그리고 리스트 맨 끝에는 세미콜론(<code>;</code>)을 붙이지 않는다는 점도 꼭 기억해 주세요!</p>
<p>이 프로그램을 실행하면 다음과 같은 결과가 나옵니다.</p>
<blockquote>
<p>Foo(6, 7) 가 생성되었습니다
Foo(6, 7)</p>
</blockquote>
<p><code>foo</code> 라는 객체가 짜잔~ 하고 만들어질 때, 리스트에 적혀 있던 멤버 변수들이 지정된 값으로 초기화됩니다. 위 코드에서는 <code>m_x</code> 가 <code>x</code> 의 값인 6으로, <code>m_y</code> 가 <code>y</code> 의 값인 7로 초기화되는 거죠. 그 작업이 끝나면 비로소 생성자의 몸체(중괄호 안의 코드)가 실행됩니다.</p>
<p><code>print()</code> 함수를 불러보면, <code>m_x</code> 와 <code>m_y</code> 가 성공적으로 6과 7의 값을 가지고 있는 걸 볼 수 있습니다.</p>
<hr>
<h3 id="멤버-초기화-리스트-작성-스타일-줄바꿈-팁">멤버 초기화 리스트 작성 스타일 (줄바꿈 팁)</h3>
<p>C++ 에서는 빈칸이나 줄바꿈을 자유롭게 할 수 있어서, 본인이 보기 편한 스타일로 코드를 꾸밀 수 있어요. 아래의 세 가지 스타일 모두 문법적으로 완벽하게 정상입니다 
(실제 실무에서도 세 가지 모두 흔히 볼 수 있어요).</p>
<pre><code class="language-cpp">Foo(int x, int y) : m_x { x }, m_y { y }
{
}
</code></pre>
<pre><code class="language-cpp">Foo(int x, int y) :
    m_x { x },
    m_y { y }
{
}
</code></pre>
<pre><code class="language-cpp">Foo(int x, int y)
    : m_x { x }
    , m_y { y }
{
}
</code></pre>
<p><strong>가장 추천하는 방식은 세 번째 스타일입니다:</strong></p>
<ul>
<li>생성자 이름 바로 아랫줄에 콜론(<code>:</code>)을 두면, 
함수 부분과 초기화 리스트 부분이 눈에 띄게 깔끔하게 분리됩니다.</li>
<li>초기화 리스트 부분을 들여쓰기하면 함수 이름이 훨씬 더 잘 보여요.</li>
</ul>
<p>만약 초기화할 멤버가 아주 적고 간단하다면, 
첫 번째 스타일처럼 한 줄에 다 적어도 괜찮습니다.</p>
<pre><code class="language-cpp">Foo(int x, int y)
    : m_x { x }, m_y { y }
{
}
</code></pre>
<p>하지만 내용이 길어진다면, 줄을 맞추기 위해 쉼표(<code>,</code>)를 앞에 두고 각 변수를 새로운 줄에 적는 세 번째 방식을 많이 씁니다.</p>
<pre><code class="language-cpp">Foo(int x, int y)
    : m_x { x }
    , m_y { y }
{
}
</code></pre>
<hr>
<h3 id="멤버-초기화-순서">멤버 초기화 순서</h3>
<p>C++ 의 규칙에 따르면, 변수들이 초기화되는 순서는 <strong>초기화 리스트에 적어둔 순서가 아닙니다.</strong> 반드시 <strong>클래스 내부에서 변수들이 선언된 순서대로</strong> 초기화됩니다.</p>
<p>위의 예제에서 클래스 안을 보면 <code>m_x</code> 가 <code>m_y</code> 보다 먼저 선언되어 있죠? 그렇기 때문에 설령 초기화 리스트 맨 끝에 <code>m_x</code> 를 적어두더라도, 무조건 <code>m_x</code> 가 먼저 초기화됩니다.</p>
<p>우리는 보통 글을 읽듯 &#39;왼쪽에서 오른쪽으로 초기화되겠지?&#39; 라고 직관적으로 생각하기 때문에, 이 규칙을 모르면 아주 찾기 힘든 버그를 만들 수 있어요. 다음 예제를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt; // std::max 를 사용하기 위해
#include &lt;iostream&gt;

class Foo
{
private:
    int m_x{};
    int m_y{};

public:
    Foo(int x, int y)
        : m_y { std::max(x, y) }, m_x { m_y } // 바로 이 줄에 문제가 있습니다!
    {
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Foo foo { 6, 7 };
    foo.print();

    return 0;
}
</code></pre>
<p>이 코드를 짠 사람의 의도는 이랬을 거예요.
&quot;들어온 <code>x</code>, <code>y</code> 값 중 더 큰 값을 찾아서 <code>m_y</code> 에 먼저 넣고, 그 <code>m_y</code> 값을 이용해서 <code>m_x</code> 에도 똑같이 넣어줘야지!&quot;</p>
<p>하지만 작성자의 컴퓨터에서 이 코드를 실행해 보면 다음과 같이 이상한 결과가 나옵니다.</p>
<blockquote>
<p>Foo(-858993460, 7)</p>
</blockquote>
<p><strong>무슨 일이 일어난 걸까요?</strong>
초기화 리스트에는 <code>m_y</code> 가 먼저 적혀 있지만, 클래스 안에서 선언된 순서를 보면 <code>m_x</code> 가 먼저입니다. 따라서 <code>m_x</code> 가 먼저 초기화를 시작합니다.
그런데 <code>m_x</code> 에 넣으려는 값은 <code>m_y</code> 네요? <code>m_y</code> 는 아직 초기화도 안 된 상태인데 말이죠! 그래서 <code>m_x</code> 에는 알 수 없는 쓰레기값이 들어가 버립니다. 
그후에서야 <code>m_y</code> 가 정상적으로 큰 값을 받아 초기화됩니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
이런 실수를 막기 위해, 멤버 초기화 리스트의 순서는 <strong>반드시 클래스 안에 선언된 순서와 똑같이</strong> 맞춰서 적어주세요. 똑똑한 컴파일러들은 순서가 꼬여있으면 경고를 띄워주기도 합니다.</p>
<p>또한, 어떤 멤버 변수를 초기화할 때 &#39;다른 멤버 변수&#39;의 값을 가져다 쓰는 것은 최대한 피하는 것이 좋습니다. 그렇게 하면 실수로 순서가 틀리더라도 값들이 서로 얽혀있지 않아서 문제가 생기지 않거든요.</p>
</blockquote>
<hr>
<h3 id="멤버-초기화-리스트-vs-기본-멤버-초기화">멤버 초기화 리스트 vs 기본 멤버 초기화</h3>
<p>멤버 변수에 처음 값을 넣어주는 방법은 여러 가지가 있고, 우선순위가 정해져 있습니다.</p>
<ol>
<li><strong>1순위</strong> 멤버 변수가 &#39;멤버 초기화 리스트&#39;에 적혀 있다면, 그 값이 가장 먼저 선택됩니다.</li>
<li><strong>2순위</strong> 리스트에는 없지만, 변수를 선언할 때 아예 기본값을 지정해 뒀다면(기본 멤버 초기화), 그 값이 선택됩니다.</li>
<li><strong>3순위</strong> 둘 다 없다면, C++ 의 기본 방식대로 처리됩니다 (숫자 같은 경우엔 보통 의미 없는 쓰레기값이 남아있게 됩니다).</li>
</ol>
<p>즉, 클래스에 기본값도 적혀있고 초기화 리스트에도 값이 있다면, 
<strong>초기화 리스트의 값이 우선</strong> 한다는 뜻이에요! 
세가지 경우가 모두 들어있는 예제를 볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x {};    // 기본 멤버 초기화 (초기화 리스트가 이기므로 무시됩니다)
    int m_y { 2 }; // 기본 멤버 초기화 (초기화 리스트에 없으므로 이 값이 사용됩니다)
    int m_z;       // 초기화 값 없음

public:
    Foo(int x)
        : m_x { x } // 멤버 초기화 리스트
    {
        std::cout &lt;&lt; &quot;Foo 가 생성되었습니다\n&quot;;
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;, &quot; &lt;&lt; m_z &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Foo foo { 6 };
    foo.print();

    return 0;
}
</code></pre>
<p>출력 결과는 다음과 같습니다.</p>
<blockquote>
<p>Foo 가 생성되었습니다
Foo(6, 2, -858993460)</p>
</blockquote>
<p><code>foo</code> 가 만들어질 때 상황을 살펴볼게요.</p>
<ul>
<li><code>m_x</code> 는 초기화 리스트에 있으므로 전달받은 6이 됩니다. 
(선언부의 빈 중괄호 <code>{}</code> 는 무시돼요)</li>
<li><code>m_y</code> 는 리스트에 없지만 기본값 2가 설정되어 있어서 2가 됩니다.</li>
<li><code>m_z</code> 는 리스트에도 없고 기본값도 없기 때문에 초기화되지 않고 방치됩니다. 
그래서 출력해보면 쓰레기값(-858993460 같은)이 나오는 예상치 못한 결과가 발생합니다.</li>
</ul>
<hr>
<h3 id="생성자-함수-몸체-중괄호-내부">생성자 함수 몸체 (중괄호 내부)</h3>
<p>생성자의 몸체(중괄호 <code>{}</code> 안)는 보통 텅 비워두는 경우가 많습니다. 
왜냐하면 우리가 생성자를 쓰는 주된 목적인 &#39;초기화&#39;는 방금 배운 멤버 초기화 리스트에서 이미 다 끝났기 때문이에요. 더 할 일이 없으면 비워두면 됩니다.</p>
<p>하지만 초기화 리스트가 끝난 다음 추가로 뭔가 더 설정해야 할 일이 있다면, 이 몸체 안에 코드를 적어주면 됩니다. 파일이나 데이터베이스를 열거나, 메모리를 추가로 배정하거나 하는 일들 말이죠. 위 예제들에서는 생성자가 잘 실행되었다고 화면에 문구를 출력하는 용도로 썼네요.</p>
<p>초보 프로그래머분들이 자주 하는 실수 중 하나는, 생성자 몸체 안에서 멤버 변수에 값을 넣으려고 하는 거예요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x { 0 };
    int m_y { 1 };

public:
    Foo(int x, int y)
    {
        m_x = x; // 잘못된 방법: 이것은 &#39;초기화&#39;가 아니라 이미 만들어진 변수에 값을 덮어씌우는 &#39;대입(할당)&#39; 입니다.
        m_y = y; // 잘못된 방법: 이것도 &#39;대입(할당)&#39; 입니다.
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Foo foo { 6, 7 };
    foo.print();

    return 0;
}
</code></pre>
<p>이런 간단한 코드에서는 겉보기에 결과가 똑같이 나오니까 문제가 없어 보일 수 있습니다. 하지만, C++ 에는 한 번 값이 정해지면 바꿀 수 없는 변수(<code>const</code>)나 참조자(<code>reference</code>) 같은 특별한 변수들이 있어요. 이 친구들은 오직 &#39;초기화&#39; 단계에서만 값을 가질 수 있기 때문에, 위 코드처럼 몸체 안에서 값을 &#39;대입(할당)&#39;하려고 하면 에러가 뻥! 하고 터집니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
멤버 초기화 리스트의 실행이 끝나면, 이 객체는 비로소 <strong>&#39;초기화&#39;</strong> 가 끝난 것으로 간주됩니다. 그리고 생성자의 함수 몸체까지 싹 다 실행이 완료되어야, 이 객체가 완전히 <strong>&#39;생성&#39;</strong> 된 것으로 간주됩니다.</p>
<p><strong>결론:</strong> 생성자 몸체 안에서 값을 대입하기보다는, 반드시 <strong>멤버 초기화 리스트</strong> 를 사용하여 변수들을 초기화하는 습관을 기르세요!</p>
</blockquote>
<hr>
<h3 id="잘못된-값이-들어왔을-때-대처하기-생성자가-실패할-때">잘못된 값이 들어왔을 때 대처하기 (생성자가 실패할 때)</h3>
<p>분수를 나타내는 클래스를 만든다고 생각해 볼까요?</p>
<pre><code class="language-cpp">class Fraction
{
private:
    int m_numerator {};   // 분자
    int m_denominator {}; // 분모

public:
    Fraction(int numerator, int denominator):
        m_numerator { numerator }, m_denominator { denominator }
    {
    }
};
</code></pre>
<p>분수에서는 분모가 0이 되면 수학적으로 불가능하죠? (0으로 나눌 수는 없으니까요). 
즉, 이 클래스에는 분모 <code>m_denominator</code> 는 절대 0이 될 수 없다&quot;는 불변성이 있습니다.</p>
<p>그런데 만약 사용자가 <code>Fraction f { 1, 0 };</code> 처럼 분모에 0을 넣어서 객체를 만들려고 시도하면 어떻게 해야 할까요?</p>
<p>안타깝게도 멤버 초기화 리스트 안에서는 우리가 오류를 막아낼 수 있는 도구가 거의 없습니다. 조건부 연산자(<code>?:</code>)를 써서 임시방편으로 막아볼 순 있겠지만...</p>
<pre><code class="language-cpp">class Fraction
{
private:
    int m_numerator {};
    int m_denominator {};

public:
    Fraction(int numerator, int denominator):
        m_numerator { numerator }, m_denominator { denominator != 0.0 ? denominator : ??? } // 여기서 어떻게 해야 할까요? 임의의 숫자를 넣어야 할까요?
    {
    }
};
</code></pre>
<p>분모가 0일 때 강제로 1 같은 숫자로 바꿔버리면, 사용자는 자기가 요청하지도 않은 이상한 분수 객체를 받게 됩니다. 게다가 우리는 &quot;네가 입력한 값이 잘못되어서 내가 마음대로 바꿨어!&quot; 라고 알려줄 방법도 없죠.
그래서 보통은 멤버 초기화 리스트에서 억지로 오류를 고치려 하지 않고, 일단 들어온 값으로 초기화를 한 다음 다른 방식으로 이 곤란한 상황을 처리하려고 시도합니다.</p>
<p>이렇게 생성자가 정상적이고 유효한 객체를 만들어내지 못한 상황을 가리켜 <strong>&quot;생성자가 실패했다&quot;</strong> 고 부릅니다.</p>
<hr>
<h3 id="생성자가-실패했을-때-간단한-예고편">생성자가 실패했을 때 (간단한 예고편)</h3>
<p>일반적인 함수들은 실행하다가 문제가 생기면, 
함수를 호출한 쪽으로 에러 코드를 반환해서 알려주면 됩니다.</p>
<p>하지만 <strong>생성자는 반환값이라는 게 아예 존재하지 않습니다.</strong> 그래서 이 방식은 쓸 수가 없어요.</p>
<ul>
<li><code>isValid()</code> 같이 현재 객체가 정상인지 아닌지 확인해주는 함수를 추가로 만들 수도 있어요. 하지만 이건 사용자가 객체를 만들 때마다 그 함수를 호출해서 체크해야 한다는 단점이 있고, 사람이 하는 일이라 까먹기 십상입니다. 그러면 결국 버그로 이어지죠.</li>
<li>프로그램을 아예 강제로 종료시켜버리는 것도, 대부분의 프로그램에서는 너무 극단적인 방법이라 쓰기 어렵습니다.</li>
</ul>
<p>이럴 때 남은 가장 좋은 방법은 바로 <strong>예외를 던지는 것</strong> 입니다!
예외를 던지면 객체를 만드는 과정 자체가 아예 취소되어 버립니다. 사용자는 비정상적인 객체를 만질 기회조차 얻지 못하게 되죠. 그래서 생성자가 실패할 것 같을 때는 보통 예외 처리를 사용하는 것이 가장 깔끔한 해결책입니다. (이 부분은 나중에 더 깊게 배울 테니 지금은 이런 게 있구나~ 하고 넘어가셔도 좋습니다.)</p>
<blockquote>
<p><strong>조금 더 심화된 내용 (아직은 몰라도 괜찮아요!)</strong>
만약 예외 처리를 사용할 수 없거나 사용하고 싶지 않다면, 사용자가 직접 객체를 만들게 놔두지 말고 특별한 함수를 거치게 하는 방법도 있습니다.
아래 예제처럼 <code>createFraction</code> 이라는 함수를 만들어두면, 분모가 0일 때는 텅 빈 값을 돌려주고, 정상일 때만 제대로 된 객체를 포장해서 돌려주는 식으로 안전하게 처리할 수 있어요 (<code>std::optional</code> 활용).</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;optional&gt;
&gt;
class Fraction
{
private:
    int m_numerator { 0 };
    int m_denominator { 1 };
&gt;
    // private 생성자는 외부(public)에서 마음대로 호출할 수 없습니다
    Fraction(int numerator, int denominator):
        m_numerator { numerator }, m_denominator { denominator }
    {
    }
&gt;
public:
    // 이 함수가 private 멤버(생성자 포함)에 접근할 수 있도록 권한을 줍니다
    friend std::optional&lt;Fraction&gt; createFraction(int numerator, int denominator);
};
&gt;
std::optional&lt;Fraction&gt; createFraction(int numerator, int denominator)
{
    if (denominator == 0) // 분모가 0이면
        return {};        // 아무것도 없는 빈 값을 반환합니다
&gt;
    return Fraction{numerator, denominator}; // 정상일 때만 객체를 만들어 반환합니다
}
&gt;
int main()
{
    auto f1 { createFraction(0, 1) };
    if (f1) // 정상적으로 만들어졌는지 확인
    {
        std::cout &lt;&lt; &quot;분수 객체가 성공적으로 생성되었습니다\n&quot;;
    }
&gt;
    auto f2 { createFraction(0, 0) }; // 분모가 0인 잘못된 요청
    if (!f2) // 실패했는지 확인
    {
        std::cout &lt;&lt; &quot;잘못된 분수입니다\n&quot;;
    }
}</code></pre>
<hr>
<h2 id="1411--기본-생성자와-기본-인수">14.11 — 기본 생성자와 기본 인수</h2>
<p><strong>기본 생성자</strong> 란 아무런 인수도 넘겨받지 않는 생성자를 말해요. 
쉽게 말해 괄호 안에 아무런 조건이 없는 텅 빈 생성자라고 생각하시면 됩니다.</p>
<p>기본 생성자를 가지고 있는 클래스의 예시를 한번 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
public:
    Foo() // 기본 생성자
    {
        std::cout &lt;&lt; &quot;Foo 기본 생성자가 호출되었습니다\n&quot;;
    }
};

int main()
{
    Foo foo{}; // 초기화 값이 없으므로, Foo의 기본 생성자를 부릅니다.

    return 0;
}
</code></pre>
<p>위 프로그램을 실행하면 <code>Foo</code> 타입의 객체가 하나 만들어져요. 우리가 괄호 안에 아무런 초기값을 주지 않았기 때문에, 자동으로 기본 생성자인 <code>Foo()</code> 가 불리고, 화면에는 이렇게 출력된답니다.</p>
<pre><code class="language-cpp">Foo 기본 생성자가 호출되었습니다</code></pre>
<hr>
<h3 id="클래스-타입의-값-초기화-vs-기본-초기화">클래스 타입의 값 초기화 vs 기본 초기화</h3>
<p>어떤 클래스에 기본 생성자가 있다면, 값 초기화 방식이나 기본 초기화 방식 모두 기본 생성자를 똑같이 부르게 됩니다. 그래서 위 예제의 <code>Foo</code> 클래스 같은 경우, 아래 두 코드는 사실상 완전히 똑같이 작동해요.</p>
<pre><code class="language-cpp">Foo foo{}; // 값 초기화, Foo() 기본 생성자를 부릅니다.
Foo foo2;  // 기본 초기화, Foo() 기본 생성자를 부릅니다.
</code></pre>
<p>하지만 이전 레슨에서 배웠듯이, <strong>값 초기화</strong>가 더 안전한 방법이에요. 
컴퓨터 입장에서는 어떤 클래스가 단순한 데이터 모음인지 아닌지 구별하기 어렵기 때문에, 마음 편하게 모든 곳에 중괄호 <code>{}</code> 를 쓰는 &#39;값 초기화&#39;를 사용하는 것이 신경 쓸 일도 없고 제일 안전하답니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
모든 클래스 타입을 만들 때는 기본 초기화보다 값 초기화(중괄호 <code>{}</code> 사용)를 우선해서 사용하세요.</p>
</blockquote>
<hr>
<h3 id="기본-인수를-가진-생성자">기본 인수를 가진 생성자</h3>
<p>다른 일반 함수들처럼, 성자도 오른쪽 끝에 있는 매개변수들에 기본값을 미리 정해둘 수 있어요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x { };
    int m_y { };

public:
    Foo(int x=0, int y=0) // 기본 인수를 가지고 있습니다.
        : m_x { x }
        , m_y { y }
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;) 가 생성되었습니다\n&quot;;
    }
};

int main()
{
    Foo foo1{};     // 기본 인수를 사용해서 Foo(int, int) 생성자를 부릅니다.
    Foo foo2{6, 7}; // Foo(int, int) 생성자를 부릅니다.

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 이렇게 나옵니다.
<code>Foo(0, 0) 가 생성되었습니다</code>
<code>Foo(6, 7) 가 생성되었습니다</code></p>
<p>만약 어떤 생성자의 <strong>모든</strong> 매개변수에 기본값이 다 채워져 있다면, 그 생성자는 &#39;기본 생성자&#39; 역할도 할 수 있어요. 아무런 값을 넘겨주지 않고도 부를 수 있으니까요.</p>
<hr>
<h3 id="생성자-오버로딩">생성자 오버로딩</h3>
<p>생성자도 결국엔 함수이기 때문에 &#39;오버로딩&#39;이 가능해요. 
오버로딩이란 이름은 같지만 매개변수가 다른 함수를 여러 개 만드는 것을 말해요. 
덕분에 우리는 객체를 입맛에 맞게 여러 가지 방법으로 만들 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() // 기본 생성자
    {
        std::cout &lt;&lt; &quot;Foo가 생성되었습니다\n&quot;;
    }

    Foo(int x, int y) // 기본 생성자가 아님 (인수를 받음)
        : m_x { x }, m_y { y }
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;) 가 생성되었습니다\n&quot;;
    }
};

int main()
{
    Foo foo1{};     // Foo() 생성자를 부릅니다.
    Foo foo2{6, 7}; // Foo(int, int) 생성자를 부릅니다.

    return 0;
}
</code></pre>
<p>여기서 꼭 기억해야 할 사실이 있어요. <strong>클래스는 단 하나의 기본 생성자만 가져야 합니다.</strong> 
만약 아무 값도 받지 않는 기본 생성자가 두 개 이상 있다면, 컴파일러는 도대체 둘 중 어떤 걸 써야 할지 몰라서 헷갈리게 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() // 기본 생성자
    {
        std::cout &lt;&lt; &quot;Foo가 생성되었습니다\n&quot;;
    }

    Foo(int x=1, int y=2) // 이것도 기본 생성자 역할을 함
        : m_x { x }, m_y { y }
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;) 가 생성되었습니다\n&quot;;
    }
};

int main()
{
    Foo foo{}; // 컴파일 에러: 어떤 생성자 함수를 불러야 할지 모호합니다.

    return 0;
}
</code></pre>
<p>위 예제에서 우리가 아무 값 없이 <code>foo</code>를 만들려고 했죠? 그럼 컴퓨터는 기본 생성자를 찾게 되는데, 앗! 두 개나 발견해 버렸습니다. 컴퓨터는 스스로 결정을 내리지 못하기 때문에 결국 컴파일 에러를 뱉어내고 멈춰버립니다.</p>
<hr>
<h3 id="암시적-기본-생성자">암시적 기본 생성자</h3>
<p>만약 우리가 클래스에 생성자를 <strong>단 한 개도 만들지 않았다면</strong>, 컴퓨터가 우리를 위해 텅 빈 기본 생성자를 몰래 하나 만들어 줍니다. 이것을 <strong>암시적 기본 생성자</strong>라고 불러요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x{};
    int m_y{};

    // 참고: 선언된 생성자가 하나도 없습니다.
};

int main()
{
    Foo foo{};

    return 0;
}
</code></pre>
<p>우리가 생성자를 만들지 않았기 때문에 컴퓨터가 알아서 암시적 기본 생성자를 만들어 주고, <code>foo{}</code>를 만들 때 그 자동 생성자를 사용하게 됩니다.</p>
<p>컴퓨터가 만들어주는 암시적 기본 생성자는 사실 아무런 매개변수도 없고, 내용물도 텅 빈 생성자와 똑같아요. 즉, 위 코드에서 컴퓨터는 몰래 아래와 같은 코드를 만들어 넣은 거랍니다.</p>
<pre><code class="language-cpp">public:
    Foo() // 암시적으로 자동 생성된 기본 생성자
    {
    }
</code></pre>
<p>이 자동 생성자는 클래스 안에 저장할 데이터가 아예 없을 때나 쓸만해요. 
데이터가 있다면 우리가 직접 원하는 값을 넣어줘야 할 텐데, 
이 텅 빈 자동 생성자로는 아무것도 할 수 없으니까요.</p>
<hr>
<h3 id="-default-를-사용해서-명시적으로-기본-생성자-만들기"><code>= default</code> 를 사용해서 명시적으로 기본 생성자 만들기</h3>
<p>가끔은 우리가 직접 생성자를 만들었음에도 불구하고, 컴퓨터가 알아서 만들어주는 
&#39;텅 빈 기본 생성자&#39;가 필요할 때가 있어요. </p>
<p>이럴 때는 <code>= default</code> 라는 문법을 써서 &quot;컴퓨터야, 네가 알아서 기본 생성자 하나 만들어줘!&quot; 라고 당당하게 요구할 수 있습니다. 이걸 <strong>명시적으로 요구한 기본 생성자</strong>라고 해요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x {};
    int m_y {};

public:
    Foo() = default; // 명시적으로 요구한 기본 생성자를 만듭니다.

    Foo(int x, int y)
        : m_x { x }, m_y { y }
    {
        std::cout &lt;&lt; &quot;Foo(&quot; &lt;&lt; m_x &lt;&lt; &quot;, &quot; &lt;&lt; m_y &lt;&lt; &quot;) 가 생성되었습니다\n&quot;;
    }
};

int main()
{
    Foo foo{}; // Foo() 기본 생성자를 부릅니다.

    return 0;
}
</code></pre>
<p>원래 규칙대로라면, 우리가 이미 <code>Foo(int, int)</code>라는 생성자를 만들었기 때문에 컴퓨터는 기본 생성자를 자동으로 만들어주지 않아요. 하지만 우리가 <code>= default</code>를 써서 특별히 부탁했기 때문에 만들어준 거랍니다. 덕분에 <code>foo{}</code> 처럼 빈 괄호로도 객체를 무사히 만들 수 있게 되죠.</p>
<blockquote>
<p><strong>모범 사례</strong>
텅 빈 내용물 <code>{}</code> 을 가진 기본 생성자를 직접 쓰는 것보다, <code>= default</code> 를 사용하는 것이 훨씬 좋습니다.</p>
</blockquote>
<hr>
<h3 id="명시적으로-요구한-기본-생성자-default-vs-내용이-빈-사용자-정의-생성자">명시적으로 요구한 기본 생성자(<code>= default</code>) vs 내용이 빈 사용자 정의 생성자(<code>{}</code>)</h3>
<p>이 두 가지는 적어도 두 가지 상황에서 다르게 작동해요.
가장 중요한 차이는 바로 값을 채워 넣는 방식(초기화)에 있어요. </p>
<p>객체를 만들 때, 만약 사용자가 직접 빈 생성자 <code>{}</code> 를 만들었다면 객체는 그냥 기본 초기화만 됩니다. 하지만, 컴퓨터가 알아서 만들거나 <code>= default</code> 로 만든 생성자를 쓰면, 객체 내부의 값들이 먼저 <strong>0으로 깨끗하게 초기화</strong> 된 다음에 기본 초기화가 진행된답니다!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class User
{
private:
    int m_a; // 참고: 기본 초기화 값이 없습니다.
    int m_b {};

public:
    User() {} // 사용자가 직접 정의한 빈 생성자

    int a() const { return m_a; }
    int b() const { return m_b; }
};

class Default
{
private:
    int m_a; // 참고: 기본 초기화 값이 없습니다.
    int m_b {};

public:
    Default() = default; // 명시적으로 요구한 기본 생성자

    int a() const { return m_a; }
    int b() const { return m_b; }
};

class Implicit
{
private:
    int m_a; // 참고: 기본 초기화 값이 없습니다.
    int m_b {};

public:
    // 암시적 기본 생성자 (컴퓨터가 알아서 만듦)

    int a() const { return m_a; }
    int b() const { return m_b; }
};

int main()
{
    User user{}; // 기본 초기화됨
    std::cout &lt;&lt; user.a() &lt;&lt; &#39; &#39; &lt;&lt; user.b() &lt;&lt; &#39;\n&#39;;

    Default def{}; // 0으로 먼저 초기화된 후, 기본 초기화됨
    std::cout &lt;&lt; def.a() &lt;&lt; &#39; &#39; &lt;&lt; def.b() &lt;&lt; &#39;\n&#39;;

    Implicit imp{}; // 0으로 먼저 초기화된 후, 기본 초기화됨
    std::cout &lt;&lt; imp.a() &lt;&lt; &#39; &#39; &lt;&lt; imp.b() &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행해 보면 이런 결과가 나옵니다. (첫 번째 숫자는 쓰레기 값이에요)</p>
<pre><code class="language-text">782510864 0
0 0
0 0</code></pre>
<p><code>user.a</code> 는 0으로 깨끗하게 청소되지 않아서, 알 수 없는 이상한 쓰레기 값이 그대로 남아있는 걸 볼 수 있죠?</p>
<p>하지만 실무에서는 크게 걱정할 필요 없어요. 우리가 클래스를 만들 때 변수들에 미리미리 기본값(예: <code>int m_a{};</code>)을 잘 적어주기만 하면 예방할 수 있는 문제랍니다!</p>
<hr>
<h3 id="말이-될-때만-기본-생성자를-만드세요">말이 될 때만 기본 생성자를 만드세요</h3>
<p>기본 생성자는 우리가 아무런 재료를 주지 않아도 객체를 뚝딱 만들 수 있게 해 줘요.
그러니까, <strong>아무런 값 없이 만들어져도 말이 되는 객체일 때만</strong> 기본 생성자를 제공해야 해요.</p>
<p>예를 들어 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Fraction // 분수를 나타내는 클래스
{
private:
    int m_numerator{ 0 };   // 분자
    int m_denominator{ 1 }; // 분모

public:
    Fraction() = default;
    Fraction(int numerator, int denominator)
        : m_numerator{ numerator }
        , m_denominator{ denominator }
    {
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Fraction(&quot; &lt;&lt; m_numerator &lt;&lt; &quot;, &quot; &lt;&lt; m_denominator &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Fraction f1 {3, 5};
    f1.print();

    Fraction f2 {}; // 0/1 분수를 얻게 됩니다.
    f2.print();

    return 0;
}
</code></pre>
<p>분수를 나타내는 클래스라면, 사용자가 아무 값을 안 주었을 때 그냥 &#39;0/1&#39;이라고 치면 되니까 기본 생성자가 있어도 아주 자연스럽죠.</p>
<p>하지만 다음 클래스는 어떨까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee // 직원을 나타내는 클래스
{
private:
    std::string m_name{ }; // 이름
    int m_id{ };           // 사번

public:
    Employee(std::string_view name, int id)
        : m_name{ name }
        , m_id{ id }
    {
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Employee(&quot; &lt;&lt; m_name &lt;&lt; &quot;, &quot; &lt;&lt; m_id &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Employee e1 { &quot;Joe&quot;, 1 };
    e1.print();

    Employee e2 {}; // 컴파일 에러: 일치하는 생성자가 없습니다.
    e2.print();

    return 0;
}
</code></pre>
<p>직원을 나타내는 클래스인데 직원의 &#39;이름&#39;이 없다는 건 상식적으로 말이 안 되잖아요? 
이런 경우에는 아예 기본 생성자를 안 만드는 게 맞아요. 그래야 누군가 실수로 이름 없는 유령 직원을 만들려고 할 때 컴퓨터가 딱! 에러를 내서 막아줄 수 있으니까요.</p>
<hr>
<h2 id="1412--위임-생성자">14.12 — 위임 생성자</h2>
<p>코딩을 할 때는 똑같은 코드를 여러 번 쓰는 것을 최대한 피하는 것이 좋습니다.
다음 함수들을 한 번 살펴볼까요?</p>
<pre><code class="language-cpp">void A()
{
    // 작업 A를 수행하는 코드들
}

void B()
{
    // 작업 A를 수행하는 코드들
    // 작업 B를 수행하는 코드들
}
</code></pre>
<p>두 함수 모두 완전히 똑같은 일(작업 A)을 하는 코드를 가지고 있네요. 
이럴 때는 아래처럼 코드를 깔끔하게 고칠 수 있습니다(리팩토링).</p>
<pre><code class="language-cpp">void A()
{
    // 작업 A를 수행하는 코드들
}

void B()
{
    A();
    // 작업 B를 수행하는 코드들
}
</code></pre>
<p>이렇게 하면 <code>A()</code> 와 <code>B()</code> 함수에 중복으로 들어있던 코드를 없앨 수 있어요. 
나중에 수정할 일이 생겨도 한 군데만 고치면 되니까 코드를 관리하기가 훨씬 쉬워집니다.</p>
<p>클래스 안에 <strong>생성자</strong>가 여러 개 있을 때도 마찬가지예요. 
각 생성자 안에 들어있는 코드가 완전히 똑같거나 아주 비슷한 경우가 정말 많거든요. 
함수에서 했던 것처럼, 생성자에서도 중복되는 코드를 없애고 싶을 겁니다.</p>
<p>다음 예시를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name { &quot;???&quot; };
    int m_id { 0 };
    bool m_isManager { false };

public:
    Employee(std::string_view name, int id) // 직원은 반드시 이름과 ID를 가져야 합니다
        : m_name{ name }, m_id { id }
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; created\n&quot;;
    }

    Employee(std::string_view name, int id, bool isManager) // 선택적으로 관리자(manager)가 될 수도 있습니다
        : m_name{ name }, m_id{ id }, m_isManager { isManager }
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; created\n&quot;;
    }
};

int main()
{
    Employee e1{ &quot;James&quot;, 7 };
    Employee e2{ &quot;Dave&quot;, 42, true };
}
</code></pre>
<p>위 코드를 보면 두 생성자의 몸체(중괄호 <code>{ }</code> 안)에 완전히 똑같은 출력문(<code>std::cout</code>)이 들어있죠?</p>
<blockquote>
<p><strong>참고 사항</strong>
생성자가 무언가를 화면에 출력하게 만드는 건 사실 좋은 습관은 아니에요.
(디버깅 목적 제외) 아무것도 출력하고 싶지 않을 때 그 생성자를 쓸 수 없게 되니까요. 여기서는 단지 어떤 일이 일어나고 있는지 쉽게 보여드리기 위해 출력문을 넣은 것뿐입니다.</p>
</blockquote>
<p>생성자도 다른 함수(같은 클래스 안의 다른 멤버 함수 포함)를 부를 수 있어요. 
그래서 아까 일반 함수에서 했던 것처럼 이렇게 고쳐볼 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name { &quot;???&quot; };
    int m_id{ 0 };
    bool m_isManager { false };

    void printCreated() const // 우리가 새로 만든 도우미 함수
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; created\n&quot;;
    }

public:
    Employee(std::string_view name, int id)
        : m_name{ name }, m_id { id }
    {
        printCreated(); // 여기서 호출합니다
    }

    Employee(std::string_view name, int id, bool isManager)
        : m_name{ name }, m_id{ id }, m_isManager { isManager }
    {
        printCreated(); // 그리고 여기서도요
    }
};

int main()
{
    Employee e1{ &quot;James&quot;, 7 };
    Employee e2{ &quot;Dave&quot;, 42, true };
}
</code></pre>
<p>이전 버전보다는 나아졌어요. (중복되던 코드가 도우미 함수 호출 하나로 바뀌었으니까요.) 하지만 굳이 새로운 함수를 만들어야 한다는 단점이 있습니다. </p>
<p>게다가 두 생성자 모두 <code>m_name</code> 과 <code>m_id</code> 를 똑같이 초기화하고 있잖아요? 
이상적으로는 이 초기화 중복도 없애버리고 싶을 겁니다.</p>
<p>더 좋은 방법이 없을까요? 당연히 있습니다! 
하지만 바로 이 부분에서 초보 프로그래머들이 실수를 많이 하곤 해요.</p>
<hr>
<h3 id="함수-몸체-안에서-생성자를-부르면-임시-객체가-만들어집니다">함수 몸체 안에서 생성자를 부르면 &#39;임시 객체&#39;가 만들어집니다</h3>
<p>아까 <code>B()</code> 함수 안에서 <code>A()</code> 함수를 불렀던 것처럼, 그냥 생성자 안에서 다른 생성자를 부르면 되지 않을까? 라고 생각하기 쉽습니다. </p>
<p>예를 들어, <code>Employee(이름, ID, 관리자여부)</code> 생성자 안에서 <code>Employee(이름, ID)</code> 생성자를 불러서 초기화도 하고 출력문도 실행하는 거죠. 코드로 보면 이렇습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name { &quot;???&quot; };
    int m_id { 0 };
    bool m_isManager { false };

public:
    Employee(std::string_view name, int id)
        : m_name{ name }, m_id { id } // 이 생성자는 이름과 ID를 초기화합니다
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; created\n&quot;; // 출력문이 다시 여기로 돌아왔습니다
    }

    Employee(std::string_view name, int id, bool isManager)
        : m_isManager { isManager } // 이 생성자는 관리자 여부(m_isManager)만 초기화합니다
    {
        // m_name과 m_id를 초기화하기 위해 Employee(std::string_view, int) 생성자를 호출합니다
        Employee(name, id); // 이 코드는 예상대로 작동하지 않습니다!
    }

    const std::string&amp; getName() const { return m_name; }
};

int main()
{
    Employee e2{ &quot;Dave&quot;, 42, true };
    std::cout &lt;&lt; &quot;e2 has name: &quot; &lt;&lt; e2.getName() &lt;&lt; &quot;\n&quot;; // e2.m_name을 출력합니다
}
</code></pre>
<p>하지만 이 코드는 제대로 작동하지 않아요. 프로그램을 실행해 보면 다음과 같이 출력됩니다.</p>
<pre><code>Employee Dave created
e2 has name: ???
</code></pre><p>&quot;Employee Dave created&quot;라는 문구가 출력되긴 했지만, <code>e2</code> 객체 생성이 끝난 후 <code>e2.m_name</code> 을 확인해 보니 여전히 처음 기본값인 <code>&quot;???&quot;</code> 가 들어있네요! 도대체 어떻게 된 일일까요?</p>
<p>우리는 <code>Employee(name, id)</code> 가 현재 우리가 만들고 있는 <code>e2</code> 객체를 이어서 초기화해 줄 거라고 기대했어요. 하지만 객체의 초기화는 <strong>멤버 초기화 리스트</strong>가 끝나는 순간 이미 완료된 것으로 간주됩니다. 즉, 생성자의 몸체(중괄호 안)가 실행될 때는 이미 뭔가 더 초기화하기엔 너무 늦어버린 거예요.</p>
<p>함수 몸체 안에서 생성자를 부르는 건, 현재 객체를 초기화하는 게 아니라 <strong>임시로 쓸 새로운 객체를 만들어서 초기화해 버리는 것</strong>과 같습니다. 위 예시에서 <code>Employee(name, id);</code> 라는 코드는 이름 없는 &#39;임시 Employee 객체&#39;를 하나 뚝딱 만듭니다. </p>
<p>그리고 이 <strong>임시 객체</strong> 의 이름이 &quot;Dave&quot;로 설정되면서 &quot;Employee Dave created&quot;가 출력되는 거예요. 그러고 나서 이 임시 객체는 파괴되어 버립니다. 결국 진짜 우리가 원했던 <code>e2</code> 객체는 건드리지도 못한 채 기본값 그대로 남아있게 되는 거죠.</p>
<blockquote>
<p><strong>권장 사항</strong>
생성자를 다른 함수의 몸체(중괄호 안)에서 직접 호출하지 마세요. 
에러가 나거나 엉뚱한 임시 객체만 만들어질 뿐입니다.
만약 정말로 임시 객체가 필요하다면 중괄호를 쓰는 리스트 초기화 방식을 사용하세요. (예: <code>Employee{&quot;Dave&quot;, 42}</code>) 이렇게 하면 &quot;나는 지금 객체를 새로 만들고 있다&quot;는 의도가 명확해집니다.</p>
</blockquote>
<p>그렇다면 생성자 안에서 다른 생성자를 부를 수 없다면, 이 중복 문제는 어떻게 해결해야 할까요?</p>
<hr>
<h3 id="위임-생성자">위임 생성자</h3>
<p>다행히 C++에서는 한 생성자가 같은 클래스에 있는 <strong>다른 생성자에게 초기화 작업을 떠넘기는(위임하는)</strong> 기능을 제공합니다. 이 과정을 <strong>생성자 체이닝</strong> 이라고도 부르고, 이렇게 책임을 떠넘기는 생성자를 <strong>위임 생성자</strong> 라고 부릅니다.</p>
<p>한 생성자가 다른 생성자에게 초기화를 위임하려면, 그냥 <strong>멤버 초기화 리스트</strong> 안에서 다른 생성자를 호출하기만 하면 됩니다. 이렇게요!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name { &quot;???&quot; };
    int m_id { 0 };

public:
    Employee(std::string_view name)
        : Employee{ name, 0 } // Employee(std::string_view, int) 생성자에게 초기화를 위임합니다
    {
    }

    Employee(std::string_view name, int id)
        : m_name{ name }, m_id { id } // 실제로 멤버 변수들을 초기화하는 곳입니다
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; created\n&quot;;
    }
};

int main()
{
    Employee e1{ &quot;James&quot; };
    Employee e2{ &quot;Dave&quot;, 42 };
}
</code></pre>
<p><code>e1 { &quot;James&quot; }</code> 가 만들어질 때 어떤 일이 일어나는지 순서대로 볼까요?</p>
<ol>
<li>먼저 <code>name</code> 에 &quot;James&quot;를 받는 <code>Employee(std::string_view)</code> 생성자가 호출됩니다.</li>
<li>이 생성자의 초기화 리스트를 보니 다른 생성자에게 일을 떠넘기네요! 
그래서 <code>Employee(std::string_view, int)</code> 생성자가 이어서 호출됩니다.</li>
<li>이때 첫 번째 인자로 &quot;James&quot;가, 두 번째 인자로 숫자 <code>0</code>이 넘어갑니다.</li>
<li>일을 넘겨받은 생성자가 실제로 멤버 변수들을 초기화하고, 자신의 중괄호 안 코드를 실행합니다. (&quot;Employee James created&quot; 출력)</li>
<li>그 작업이 다 끝나면 다시 처음 호출됐던 생성자로 돌아오고, 텅 빈 몸체를 훑고 지나간 뒤 모든 과정이 끝납니다.</li>
</ol>
<p>이 방법의 단점은 가끔 초기화할 &#39;값&#39;을 중복해서 적어야 한다는 거예요. 
위임할 때 정수형 매개변수 값으로 <code>0</code>을 직접 적어주었죠(하드코딩). 
변수에 설정해 둔 기본값을 알아서 끌어다 쓸 방법이 없기 때문입니다.</p>
<p>위임 생성자에 대해 몇 가지 더 알아둘 점이 있습니다. 
<strong>첫째, 다른 생성자에게 위임하는 생성자는 스스로 멤버 초기화를 할 수 없습니다.</strong> 
즉, 내 할 일을 남에게 &#39;위임&#39;하거나, 내가 직접 &#39;초기화&#39;하거나 둘 중 하나만 할 수 있어요.</p>
<blockquote>
<p><strong>참고로...</strong>
방금 예시에서는 매개변수가 적은 생성자가 매개변수가 많은 생성자에게 위임을 했죠? 이게 아주 흔하게 쓰이는 일반적인 방식입니다.
만약 반대로 매개변수가 많은 쪽에서 적은 쪽으로 위임을 했다면, <code>id</code> 값을 이용해 <code>m_id</code> 를 초기화할 방법이 없었을 거예요. (위임과 초기화를 동시에 할 수는 없으니까요!)</p>
</blockquote>
<p><strong>둘째, 생성자끼리 핑퐁을 하듯이 서로 무한정 위임하는 것도 문법적으로 가능은 합니다.</strong> 하지만 이렇게 무한 루프에 빠지면 프로그램의 메모리(스택)가 꽉 차서 프로그램이 튕겨버리게(crash) 됩니다. 반드시 모든 위임의 끝에는 책임을 떠안고 직접 초기화해 주는 &#39;최종 보스&#39; 생성자가 하나 있어야 해요.</p>
<blockquote>
<p><strong>권장 사항</strong>
클래스에 생성자가 여러 개 있다면, <strong>위임 생성자</strong>를 써서 중복되는 코드를 줄일 수 없는지 고민해 보세요.</p>
</blockquote>
<hr>
<h3 id="기본-인자를-이용해-생성자-개수-줄이기">기본 인자를 이용해 생성자 개수 줄이기</h3>
<p>때로는 &#39;기본값&#39;을 잘 활용하면 굳이 생성자를 여러 개 만들 필요 없이 하나로 줄일 수도 있습니다. 예를 들어, <code>id</code> 매개변수에 기본값을 주면, 이름만 필수로 받고 <code>id</code> 는 선택적으로 받는 만능 생성자 하나를 만들 수 있어요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name{};
    int m_id{ 0 }; // 기본 멤버 초기화 값

public:

    Employee(std::string_view name, int id = 0) // id에 대한 기본 인자(default argument)
        : m_name{ name }, m_id{ id }
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; created\n&quot;;
    }
};

int main()
{
    Employee e1{ &quot;James&quot; };
    Employee e2{ &quot;Dave&quot;, 42 };
}
</code></pre>
<p>함수를 호출할 때 기본값은 항상 오른쪽 매개변수부터 채워져야 하죠? 
그래서 클래스를 만들 때 좋은 습관은 이렇습니다.</p>
<ul>
<li>사용자가 <strong>반드시</strong> 입력해야 하는 값(이름 같은 것)을 먼저 선언하고, 생성자의 가장 왼쪽 매개변수로 둡니다.</li>
<li>사용자가 굳이 입력하지 않아도 되는 값(기본값으로 충분한 것들)은 나중에 선언하고, 생성자의 가장 오른쪽 매개변수로 둡니다.</li>
</ul>
<blockquote>
<p><strong>권장 사항</strong>
초기화할 때 사용자가 필수적으로 값을 넣어야 하는 멤버를 가장 먼저 선언하세요. (생성자의 제일 왼쪽 매개변수로 설정)
사용자가 값을 안 넣어도 되는(기본값이 있는) 멤버는 그 다음에 선언하세요. 
(생성자의 제일 오른쪽 매개변수로 설정)</p>
</blockquote>
<p>이 방법 역시 <code>m_id</code> 의 기본값인 <code>0</code>을 두 번(멤버 변수 옆에 한 번, 생성자 매개변수에 한 번) 써야 한다는 단점이 있습니다.</p>
<hr>
<h3 id="딜레마-중복되는-생성자-vs-중복되는-기본값">딜레마: 중복되는 생성자 vs 중복되는 기본값</h3>
<p>지금까지 생성자의 중복 코드를 줄이기 위해 &#39;위임 생성자&#39;와 &#39;기본 인자&#39;를 써보았습니다. 하지만 두 방법 모두 초기화할 &#39;값&#39;을 여러 번 똑같이 적어줘야 한다는 아쉬움이 있었죠. 
현재 C++에는 &quot;생성자의 기본값은 그냥 멤버 변수에 적힌 기본값을 끌어다 써라!&quot;라고 명령할 방법이 없습니다.</p>
<p>그렇다면 값이 중복되더라도 생성자 개수를 확 줄이는 게 나을까요, 아니면 생성자가 많아지더라도 값을 한 번만 쓰는 게 나을까요?
사람마다 의견이 다르지만, <strong>보통은 초기화 값이 조금 중복되더라도 생성자 개수를 적게 유지하는 것이 코드를 이해하기 더 쉽습니다.</strong> </p>
<hr>
<h3 id="심화-학습">심화 학습</h3>
<p>초기화 값(예: <code>0</code>)이 여러 군데에서 중복으로 쓰일 때는, 그 값에 이름을 붙여서 <strong>상수</strong>로 만들어두고 필요한 곳마다 그 이름을 가져다 쓰는 방법이 있습니다. 이렇게 하면 값을 딱 한 곳에서만 편하게 관리할 수 있어요.</p>
<p>전역 변수를 쓸 수도 있겠지만, 클래스 안에 <code>static constexpr</code> 멤버로 만드는 것이 더 좋습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    static constexpr int default_id { 0 }; // 원하는 초기화 값을 가진 이름 있는 상수를 정의합니다

    std::string m_name {};
    int m_id { default_id }; // 여기서 사용할 수 있습니다

public:

    Employee(std::string_view name, int id = default_id) // 그리고 여기서도 사용할 수 있죠
        : m_name { name }, m_id { id }
    {
        std::cout &lt;&lt; &quot;Employee &quot; &lt;&lt; m_name &lt;&lt; &quot; created\n&quot;;
    }
};

int main()
{
    Employee e1 { &quot;James&quot; };
    Employee e2 { &quot;Dave&quot;, 42 };
}
</code></pre>
<p>여기서 <code>static</code> 이라는 단어를 쓰면, 모든 <code>Employee</code> 객체들이 단 하나의 <code>default_id</code> 값을 함께 공유하게 됩니다. <code>static</code> 이 없다면 객체를 만들 때마다 <code>default_id</code> 가 각자 하나씩 쓸데없이 복사되어 생길 테니 메모리 낭비가 되겠죠.</p>
<p>이 방법의 단점은 <code>default_id</code> 같은 새로운 이름이 추가되면서 클래스가 아주 조금 더 복잡해 보일 수 있다는 것입니다. 상수가 얼마나 많이 필요한지, 얼마나 여러 곳에서 쓰이는지에 따라 이 방법을 쓸지 말지 결정하면 됩니다.</p>
<p>(정적 멤버 변수(<code>static</code>)에 대한 더 자세한 내용은 15.6절에서 다룹니다!)</p>
<hr>
<h2 id="1413--임시-클래스-객체">14.13 — 임시 클래스 객체</h2>
<p>다음 예제를 한 번 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int add(int x, int y)
{
    int sum{ x + y }; // x + y의 결과를 변수에 저장합니다
    return sum;       // 그 변수의 값을 반환합니다
}

int main()
{
    std::cout &lt;&lt; add(5, 3) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위의 <code>add()</code> 함수를 보면, <code>sum</code> 이라는 변수를 만들어서 <code>x + y</code> 의 결과를 저장하고 있습니다. 그리고 <code>return</code> 문에서 이 변수를 사용해 결과값을 반환하죠.</p>
<p>물론 이렇게 쓰면 버그를 잡을 때(디버깅) <code>sum</code> 에 무슨 값이 들어갔는지 슬쩍 확인해 보기 좋을 수는 있습니다. 하지만 <strong>객체</strong>를 하나 만들어 놓고 딱 한 번만 쓰고 버리기 때문에, 함수가 굳이 필요 이상으로 복잡해지는 단점이 있습니다.</p>
<p>보통 이렇게 변수를 딱 한 번만 쓸 거라면, 애초에 변수 자체를 만들 필요가 없습니다. 
변수를 만들어서 담아두는 대신, 계산식 자체를 변수 자리에 바로 쓱 밀어 넣으면 되거든요. 이렇게 고친 <code>add()</code> 함수를 확인해 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int add(int x, int y)
{
    return x + y; // x + y를 직접 반환합니다
}

int main()
{
    std::cout &lt;&lt; add(5, 3) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 방법은 값을 반환할 때뿐만 아니라, 
함수에 값을 전달할 때(인자)도 똑같이 써먹을 수 있습니다. </p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printValue(int value)
{
    std::cout &lt;&lt; value;
}

int main()
{
    int sum{ 5 + 3 };
    printValue(sum);

    return 0;
}
</code></pre>
<p>이렇게 쓸 수 있다는 거죠:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printValue(int value)
{
    std::cout &lt;&lt; value;
}

int main()
{
    printValue(5 + 3);

    return 0;
}
</code></pre>
<p>어때요, 코드가 훨씬 깔끔해졌죠? 굳이 이름을 지어서 변수를 만들 필요도 없고, 나중에 코드를 읽을 때 &quot;어, 이 변수가 혹시 다른 곳에서도 쓰이나?&quot; 하고 함수 전체를 뒤적일 필요도 없습니다. <code>5 + 3</code> 은 그냥 계산식일 뿐이니까, 딱 저 한 줄에서만 쓰이고 끝난다는 걸 한눈에 알 수 있죠.</p>
<p>단, 이 방법은 <strong>rvalue</strong>가 허용되는 곳에서만 쓸 수 있습니다. 
<strong>lvalue</strong>가 꼭 필요한 자리라면 어쩔 수 없이 변수(객체)를 만들어야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void addOne(int&amp; value) // non-const 참조로 전달하려면 lvalue(메모리 주소가 있는 값)가 필요합니다
{
    ++value;
}

int main()
{
    int sum { 5 + 3 };
    addOne(sum);   // 성공: sum은 lvalue입니다

    addOne(5 + 3); // 컴파일 에러: lvalue가 아닙니다

    return 0;
}
</code></pre>
<hr>
<h3 id="임시-클래스-객체">임시 클래스 객체</h3>
<p>이 문제는 <code>int</code> 같은 기본 타입뿐만 아니라 <strong>클래스</strong> 타입에서도 똑같이 적용됩니다.</p>
<blockquote>
<p><strong>참고로...</strong>
여기서는 클래스를 예로 들지만, 이 레슨에서 배우는 &#39;리스트 초기화&#39; 개념은 구조체에도 완벽하게 똑같이 적용됩니다.</p>
</blockquote>
<p>아래 예제는 위에서 본 것과 비슷하지만, 
<code>int</code> 대신 우리가 직접 만든 <code>IntPair</code> 라는 클래스를 사용합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const { return m_y; }
};

void print(IntPair p)
{
    std::cout &lt;&lt; &quot;(&quot; &lt;&lt; p.x() &lt;&lt; &quot;, &quot; &lt;&lt; p.y() &lt;&lt; &quot;)\n&quot;;
}

int main()
{
    // 케이스 1: 변수를 전달하기
    IntPair p { 3, 4 };
    print(p); // (3, 4) 출력

    return 0;
}
</code></pre>
<p>케이스 1을 보면, <code>IntPair p</code> 라는 변수를 먼저 만들고 나서 <code>print()</code> 함수에 <code>p</code> 를 전달하고 있습니다.</p>
<p>하지만 여기서도 <code>p</code> 는 딱 한 번만 쓰였죠? 게다가 <code>print()</code> 함수는 굳이 이름 있는 변수(lvalue)를 고집하지 않기 때문에, 굳이 변수를 만들 이유가 없습니다. 자, 그럼 <code>p</code> 를 없애봅시다.</p>
<p>이름표가 붙은 변수 대신 <strong>임시 객체</strong>를 전달하면 됩니다. <strong>임시 객체</strong>란 이름이 없고 딱 한 번 계산될 때만 잠깐 살았다가 사라지는 객체를 말합니다. (마치 한 번 물을 마시고 버리는 종이컵 같은 녀석이죠!) 이름이 없어서 <strong>익명 객체</strong>나 <strong>이름 없는 객체</strong>라고도 부릅니다.</p>
<p>이런 임시 클래스 객체를 만드는 가장 흔한 두 가지 방법은 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const{ return m_y; }
};

void print(IntPair p)
{
    std::cout &lt;&lt; &quot;(&quot; &lt;&lt; p.x() &lt;&lt; &quot;, &quot; &lt;&lt; p.y() &lt;&lt; &quot;)\n&quot;;
}

int main()
{
    // 케이스 1: 변수를 전달하기
    IntPair p { 3, 4 };
    print(p);

    // 케이스 2: 임시 IntPair 객체를 만들어서 함수에 전달하기
    print(IntPair { 5, 6 } );

    // 케이스 3: { 7, 8 }을 임시 IntPair 객체로 암시적 변환하여 전달하기
    print( { 7, 8 } );

    return 0;
}
</code></pre>
<p>케이스 2에서는, 컴파일러에게 <code>IntPair</code> 객체를 만들되 <code>{ 5, 6 }</code> 으로 채워 달라고 명령합니다. 이 객체는 이름표가 없기 때문에 &#39;임시 객체&#39;가 됩니다.
이 녀석은 <code>print()</code> 함수의 매개변수 <code>p</code> 로 쏙 들어가고, 
함수가 끝나면(물 마시고 나면 종이컵 버리듯이) 깔끔하게 파괴됩니다.</p>
<p>케이스 3에서도 <code>print()</code> 함수에 전달할 임시 <code>IntPair</code> 객체를 만들고 있습니다. 다만 이번엔 우리가 &quot;이거 IntPair 타입으로 만들어줘!&quot;라고 딱 집어서 말하지 않았죠. 그래도 컴파일러가 아주 똑똑하게 함수가 원하는 타입을 눈치채고, 알아서 <code>{ 7, 8 }</code> 을 <code>IntPair</code> 객체로 변환해 줍니다.</p>
<p>요약하자면 이렇습니다.</p>
<pre><code class="language-cpp">IntPair p { 1, 2 }; // { 1, 2 }로 초기화된 이름이 있는 객체 p 만들기
IntPair { 1, 2 };   // { 1, 2 }로 초기화된 임시 객체 만들기
{ 1, 2 };           // 컴파일러가 { 1, 2 }를 예상되는 타입(보통 매개변수나 반환 타입)에 맞는 임시 객체로 변환하려고 시도함
</code></pre>
<p>마지막 케이스(알아서 변환해 주는 것)에 대해서는 나중에 
<code>14.16 — 변환 생성자와 explicit 키워드</code> 레슨에서 더 자세히 다룰 거예요.</p>
<p>몇 가지 예시를 더 볼까요?</p>
<pre><code class="language-cpp">std::string { &quot;Hello&quot; }; // &quot;Hello&quot;로 초기화된 임시 std::string 만들기
std::string {};          // 값 초기화 / 기본 생성자를 사용하여 임시 std::string 만들기
</code></pre>
<hr>
<h3 id="직접-초기화를-통한-임시-객체-생성-선택-사항">직접 초기화를 통한 임시 객체 생성 (선택 사항)</h3>
<p>지금까지 중괄호 <code>{}</code> 를 써서 임시 객체를 만들 수 있다는 걸 배웠는데요, 
&quot;그럼 다른 초기화 방법으로도 임시 객체를 만들 수 있나?&quot; 하고 궁금해하실 수도 있습니다. 복사 초기화 방식으로는 임시 객체를 만드는 문법이 따로 없습니다.</p>
<p>하지만 소괄호 <code>()</code> 를 사용하는 <strong>직접 초기화</strong>로는 만들 수 있습니다.</p>
<pre><code class="language-cpp">Foo (1, 2); // (1, 2)로 직접 초기화된 임시 Foo 객체 (Foo { 1, 2 }와 비슷함)
</code></pre>
<p>언뜻 보면 함수를 호출하는 것 같이 생겼지만, 사실 <code>Foo { 1, 2 }</code> 와 똑같은 결과를 만들어냅니다 (단지 데이터가 깎여나가는 걸 방지해주는 기능만 없을 뿐이죠). 평범해 보이죠?</p>
<p>하지만 지금부터 <strong>왜 이 방법을 쓰면 안 되는지</strong> 그 이유를 설명해 드릴게요.</p>
<blockquote>
<p><strong>저자의 참고 사항</strong>
이 부분은 그냥 &quot;아, 이런 것도 있구나~&quot; 하고 가볍게 읽어 넘기시면 됩니다. 굳이 외우거나 남에게 설명할 수 있을 정도로 파고들 필요는 없어요. 읽다가 머리가 아프더라도, &quot;아, 이래서 요즘 C++에서는 중괄호 <code>{}</code> 를 쓰라고 하는 거구나!&quot; 하고 깨닫게 되실 거예요.</p>
</blockquote>
<p>아무런 값을 넣지 않을 때(인자가 없을 때)를 한 번 볼까요?</p>
<pre><code class="language-cpp">Foo();     // 값으로 초기화된 임시 Foo 객체 (Foo {}와 동일함)
</code></pre>
<p>아마 <code>Foo()</code> 도 <code>Foo {}</code> 처럼 임시 객체를 만들어 줄 거라고 기대하셨을 겁니다. 네, 맞아요. 그런데 문제는... 이 문법이 <strong>이름 있는 변수</strong> 랑 같이 쓰일 때는 뜻이 완전히 엉뚱하게 바뀌어버린다는 겁니다!</p>
<pre><code class="language-cpp">Foo bar{}; // 변수 bar의 정의, 값으로 초기화됨
Foo bar(); // 매개변수가 없고 Foo를 반환하는 함수 bar의 선언 (Foo bar{} 및 Foo()와 일관성 없음)
</code></pre>
<p>진짜 이상해질 준비 되셨나요?!?</p>
<pre><code class="language-cpp">Foo(1);    // 리터럴 1의 함수 스타일 캐스트, 임시 Foo 객체 반환 (Foo { 1 }과 비슷함)
Foo(bar);  // Foo 타입의 변수 bar 정의 (Foo { bar } 및 Foo(1)과 일관성 없음)
</code></pre>
<p>잠깐, 뭐라고요?</p>
<p>소괄호 안에 숫자 <code>1</code> 을 넣은 건 우리가 예상했던 대로 임시 객체를 만들어냅니다.
그런데 소괄호 안에 글자 <code>bar</code> 를 넣으면 갑자기 <code>bar</code> 라는 이름의 변수를 떡하니 만들어버립니다! (마치 <code>Foo bar;</code> 라고 쓴 것처럼요). 만약 <code>bar</code> 가 이미 있는 이름이라면 &quot;이름이 겹쳤어!&quot; 라며 컴파일 에러를 뱉어냅니다.</p>
<p>컴파일러는 숫자 1 같은 &#39;리터럴(그냥 값)&#39;은 변수 이름이 될 수 없다는 걸 알기 때문에, 그 경우에는 알아서 임시 객체로 처리해 주는 거죠.</p>
<blockquote>
<p><strong>여담이지만...</strong>
왜 <code>Foo(bar);</code> 가 <code>Foo bar;</code> 와 똑같이 동작하는지 궁금하시다면...
소괄호 <code>()</code> 의 가장 흔한 역할은 무언가를 &#39;묶는(그룹화)&#39; 겁니다. 수학에서 <code>(1 + 2) * 3</code> 처럼요. <code>(1 + 2) * 3</code> 이 된다면, <code>(3) * 3</code> 도 당연히 돼야겠죠?
같은 이유로, C++ 문법은 소괄호로 무언가를 묶는 걸 허용하고, 그 묶음 안에 딱 한 개만 들어있는 것도 허용합니다. 그래서 <code>Foo(bar)</code> 는 <code>Foo</code> 타입에 괄호로 예쁘게 포장된 <code>bar</code> 라는 이름표가 붙은 변수로 해석되는 겁니다. 우리 눈엔 좀 웃기게 보이지만, 언어 규칙을 꼬지 않으려면 이걸 허용할 수밖에 없었거든요.</p>
</blockquote>
<blockquote>
<p><strong>심화 학습자를 위해</strong>
조금 더 복잡한 경우를 볼까요? <code>Foo * bar();</code> 라는 문장을 생각해 봅시다. 괄호를 어떻게 쓰냐에 따라 뜻이 완전히 뒤집힙니다.</p>
<ul>
<li><code>Foo * bar();</code> (괄호 없음) 기본적으로 <code>*</code> 가 <code>Foo</code> 에 붙습니다. 매개변수가 없고 <code>Foo*</code> (포인터)를 반환하는 함수 <code>bar</code> 의 선언입니다.</li>
<li><code>Foo (*bar)();</code> <code>*</code> 를 <code>bar</code> 와 명시적으로 묶습니다. 이건 매개변수가 없고 <code>Foo</code> 를 반환하는 함수의 주소를 담는 <strong>함수 포인터</strong> <code>bar</code> 를 정의하는 겁니다.</li>
<li><code>Foo (* bar());</code> 이건 <code>Foo * bar();</code> 와 같습니다. 여기서 괄호는 그냥 쓸데없이 붙은 거예요.</li>
<li>마지막으로 <code>(Foo *) bar();</code> 이건 <code>Foo* bar()</code> 와 같을 것 같죠? 아닙니다! 이건 <code>bar()</code> 함수를 실행한 다음, 그 결과값을 <code>Foo*</code> 타입으로 C 스타일 강제 형변환(캐스트)을 하고, <strong>그냥 버려버리는</strong> 문장입니다!</li>
</ul>
<p>참... C++는 가끔 진짜 이상하죠.</p>
</blockquote>
<blockquote>
<p><strong>핵심 포인트</strong>
소괄호 <code>()</code> 는 역할이 너무 많아서 복잡합니다. 함수 호출, 직접 초기화, 임시 객체 생성, C 스타일 캐스팅, 기호 묶기, 변수 정의 등등... 그래서 소괄호를 보면 <strong>&quot;이게 도대체 무슨 역할을 하는 거지?&quot;</strong> 하고 헷갈리기 쉽습니다.
반면에 중괄호 <code>{}</code> 를 보면 무조건 &quot;아, 이건 객체를 다루는 거구나!&quot; 하고 명확하게 알 수 있죠.</p>
</blockquote>
<p>자, 머리 아픈 이야기는 여기까지. 다시 지루하지만 중요한 이야기로 돌아갑시다.</p>
<hr>
<h3 id="임시-객체와-값-반환">임시 객체와 값 반환</h3>
<p>함수가 &#39;값&#39;으로 무언가를 반환할 때, 그 반환되는 녀석의 정체는 바로 <strong>임시 객체</strong>입니다. 
(return 문에 있는 값을 이용해 만들어집니다.)</p>
<p>예제를 볼까요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const { return m_y; }
};

void print(IntPair p)
{
    std::cout &lt;&lt; &quot;(&quot; &lt;&lt; p.x() &lt;&lt; &quot;, &quot; &lt;&lt; p.y() &lt;&lt; &quot;)\n&quot;;
}

// 케이스 1: 이름이 있는 변수를 만들고 반환하기
IntPair ret1()
{
    IntPair p { 3, 4 };
    return p; // 임시 객체 반환 (p를 사용해 초기화됨)
}

// 케이스 2: 임시 IntPair 객체를 만들고 반환하기
IntPair ret2()
{
    return IntPair { 5, 6 }; // 임시 객체 반환 (다른 임시 객체를 사용해 초기화됨)
}

// 케이스 3: { 7, 8 }을 IntPair로 암시적 변환하여 반환하기
IntPair ret3()
{
    return { 7, 8 }; // 임시 객체 반환 (다른 임시 객체를 사용해 초기화됨)
}

int main()
{
    print(ret1());
    print(ret2());
    print(ret3());

    return 0;
}
</code></pre>
<p>케이스 1을 보면, <code>return p</code> 를 할 때 <code>p</code> 의 값을 베껴서 &#39;임시 객체&#39;를 하나 만든 다음, 그걸 반환합니다. 앞서 봤던 함수 인자로 넘겨줄 때와 똑같은 원리입니다.</p>
<blockquote>
<p><strong>몇 가지 알아둘 점</strong>
첫째, 기본 <code>int</code> 타입과 마찬가지로 임시 클래스 객체가 계산식에 쓰일 때는 <strong>rvalue</strong> 취급을 받습니다. 즉, rvalue가 들어갈 수 있는 자리에만 쓸 수 있습니다.
둘째, 임시 객체는 만들어진 그 줄(정확히는 전체 표현식)이 끝날 때 미련 없이 파괴됩니다.</p>
</blockquote>
<hr>
<h3 id="static_cast-vs-임시-객체의-명시적-생성"><code>static_cast</code> vs 임시 객체의 명시적 생성</h3>
<p>안전한 변환(데이터 손실이 없는 변환)을 할 때는 보통 두 가지 선택지가 있습니다. <code>static_cast</code> 를 쓰거나, 아니면 대놓고 임시 객체를 새로 만들어버리는 거죠.</p>
<p>예를 들어,</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    char c { &#39;a&#39; };

    std::cout &lt;&lt; static_cast&lt;int&gt;( c ) &lt;&lt; &#39;\n&#39;; // static_cast는 c의 값으로 직접 초기화된 임시 int를 반환합니다
    std::cout &lt;&lt; int { c } &lt;&lt; &#39;\n&#39;;             // c의 값으로 리스트 초기화된 임시 int를 명시적으로 만듭니다

    return 0;
}
</code></pre>
<p><code>static_cast&lt;int&gt;(c)</code> 는 <code>c</code> 의 값으로 세팅된 임시 <code>int</code> 를 반환하고, 
<code>int { c }</code> 역시 <code>c</code> 의 값으로 세팅된 임시 <code>int</code> 를 새로 만듭니다. 
어찌 됐든 우리가 원하는 &#39;c의 값을 가진 임시 int&#39;를 얻는 건 똑같습니다.</p>
<p>그럼 조금 더 복잡한 예시를 볼까요?</p>
<p><code>printString.h</code> 파일:</p>
<pre><code class="language-cpp">#include &lt;string&gt;

void printString(const std::string &amp;s)
{
    std::cout &lt;&lt; s &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p><code>main.cpp</code> 파일:</p>
<pre><code class="language-cpp">#include &quot;printString.h&quot;
#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

int main()
{
    std::string_view sv { &quot;Hello&quot; };

    // printString() 함수를 사용해서 sv를 출력하고 싶습니다
//    printString(sv); // 컴파일 에러: std::string_view는 std::string으로 암시적 변환되지 않습니다

    printString( static_cast&lt;std::string&gt;(sv) ); // 케이스 1: static_cast는 sv로 직접 초기화된 임시 std::string을 반환합니다
    printString( std::string { sv } );           // 케이스 2: sv로 리스트 초기화된 임시 std::string을 명시적으로 만듭니다
    printString( std::string ( sv ) );           // 케이스 3: C 스타일 캐스트는 sv로 직접 초기화된 임시 std::string을 반환합니다 (이 방법은 피하세요!)

    return 0;
}
</code></pre>
<p>만약 저 <code>printString.h</code> 헤더 파일이 우리가 함부로 고칠 수 없는 남이 만든 라이브러리 코드라고 쳐봅시다. 그런데 우리는 <code>string_view</code> 타입인 <code>sv</code> 를 저 함수에 넣고 싶어요. 하지만 성능 문제 때문에 <code>std::string_view</code> 는 <code>std::string</code> 으로 스스로(암시적) 변환되지 않기 때문에 냅다 집어넣으면 에러가 납니다.</p>
<p>이럴 때는 우리가 &quot;이걸로 바꿔!&quot;라고 콕 집어서(명시적으로) 말해줘야 합니다.</p>
<ul>
<li><strong>케이스 1:</strong> <code>static_cast</code> 를 써서 <code>sv</code> 를 <code>std::string</code> 으로 바꿉니다.</li>
<li><strong>케이스 2:</strong> 중괄호 <code>{}</code> 를 써서 아예 새로운 임시 <code>std::string</code> 을 만듭니다. 우리가 대놓고 만들어달라고 했으니 에러 없이 잘 넘어갑니다.</li>
<li><strong>케이스 3:</strong> 옛날 C 스타일 캐스팅 방식입니다. 결과는 나오지만, 이 방식은 위험할 수 있어서 최신 C++에서는 피하는 게 좋습니다!</li>
</ul>
<blockquote>
<p><strong>베스트 프랙티스 (권장 사항)</strong>
간단한 꿀팁을 드릴게요: <strong>int 나 char 같은 기본 타입</strong> 으로 바꿀 때는 <code>static_cast</code> 를 쓰고, <strong>클래스(Class) 타입</strong> 으로 바꿀 때는 중괄호 <code>{}</code> 를 써서 임시 객체를 만드세요.
<strong><code>static_cast</code> 를 써야 하는 경우:</strong></p>
<ul>
<li>데이터가 잘려 나갈 위험이 있는 변환(Narrowing conversion)을 해야 할 때</li>
<li>타입을 바꿔서 뭔가 다르게 행동하길 원한다는 걸 남들에게 팍팍 티 내고 싶을 때 (예: char를 int로)</li>
<li>무슨 이유가 있어서 꼭 &#39;직접 초기화(괄호 사용)&#39; 방식이 필요할 때</li>
</ul>
<p><strong>중괄호 <code>{}</code> 를 써서 새로운 임시 객체를 만들어야 하는 경우:</strong></p>
<ul>
<li>리스트 초기화 방식(중괄호 사용)의 혜택을 받고 싶을 때 (데이터가 잘려 나가는 걸 컴파일러가 막아줌)</li>
<li>변환할 때 생성자에 여러 가지 옵션(인자)을 더 넘겨줘야 할 때</li>
</ul>
</blockquote>
<p><strong>관련 콘텐츠</strong>
리스트 생성자에 대한 내용은 <code>16.2 — std::vector 및 리스트 생성자 소개</code> 레슨에서 더 자세히 다룹니다.</p>
<hr>
<h2 id="1414--복사-생성자-소개">14.14 — 복사 생성자 소개</h2>
<p>다음 프로그램을 한 번 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // 기본 생성자
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Fraction(&quot; &lt;&lt; m_numerator &lt;&lt; &quot;, &quot; &lt;&lt; m_denominator &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Fraction f { 5, 3 };  // Fraction(int, int) 생성자를 호출합니다
    Fraction fCopy { f }; // 여기서는 어떤 생성자가 사용될까요?

    f.print();
    fCopy.print();

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 에러 없이 아주 잘 작동하고, 다음과 같은 결과를 보여줍니다.</p>
<pre><code class="language-text">Fraction(5, 3)
Fraction(5, 3)
</code></pre>
<p>어떻게 작동하는 건지 자세히 들여다보겠습니다.
처음에 <strong>f</strong> 라는 변수를 만들 때는 우리가 정의한 <code>Fraction(int, int)</code> 생성자가 사용됩니다. 5와 3이라는 숫자를 넣어줬으니까요.</p>
<p>그렇다면 그다음 줄은 어떨까요? <strong>fCopy</strong> 를 만드는 것도 분명히 무언가를 생성(초기화)하는 과정입니다. 그런데 숫자가 아니라 아까 만든 <strong>f</strong> 자체를 집어넣고 있죠? 이때 도대체 어떤 생성자가 호출된 걸까요?</p>
<p>정답은 바로 <strong>복사 생성자</strong> 입니다.</p>
<hr>
<h3 id="복사-생성자란-무엇인가요">복사 생성자란 무엇인가요?</h3>
<p><strong>복사 생성자</strong>란, 쉽게 말해서 &#39;똑같은 복제본을 만들어내는 기계&#39;입니다. 이미 존재하는 객체(여기서는 <strong>f</strong>)를 재료로 삼아서, 그것과 똑같은 종류의 새로운 객체(<strong>fCopy</strong>)를 만들 때 사용됩니다. 복사 생성자가 일을 마치고 나면, 새로 만들어진 객체는 원본 객체의 완벽한 복사본이 됩니다.</p>
<hr>
<h3 id="알아서-챙겨주는-암시적-복사-생성자">알아서 챙겨주는 &#39;암시적 복사 생성자&#39;</h3>
<p>우리가 클래스 안에 복사 생성자를 직접 만들지 않아도 괜찮습니다. 
C++은 아주 친절해서, 우리가 안 만들면 알아서 기본 복사기를 하나 제공해 줍니다. 
이것을 <strong>암시적 복사 생성자</strong> 라고 부릅니다.</p>
<p>방금 본 예제에서 <code>Fraction fCopy { f };</code> 코드는 C++이 알아서 만들어준 이 기본 복사기를 사용한 것입니다.</p>
<p>이 기본 복사기는 어떻게 작동할까요? 
아주 단순합니다. 객체 안에 들어있는 부품(멤버 변수)들을 하나씩 하나씩 그대로 베껴 옵니다.
예를 들어, <strong>fCopy</strong> 의 분자(<code>m_numerator</code>)는 <strong>f</strong> 의 분자(값 5)를 그대로 가져와서 설정하고, <strong>fCopy</strong> 의 분모(<code>m_denominator</code>)는 <strong>f</strong> 의 분모(값 3)를 그대로 가져와서 설정합니다.</p>
<p>결과적으로 두 객체는 완전히 똑같은 값을 가지게 되므로, 어느 쪽에서 <code>print()</code> 함수를 부르든 똑같은 결과가 나오는 것이죠.</p>
<hr>
<h3 id="나만의-복사-생성자-직접-만들기">나만의 복사 생성자 직접 만들기</h3>
<p>기본으로 제공되는 것 말고, 우리가 직접 복사 생성자를 만들 수도 있습니다. 
이번에는 복사가 일어날 때마다 화면에 메시지를 띄우는 나만의 복사 생성자를 만들어 볼게요. 복사 기능이 정말로 실행되는지 눈으로 확인해 보기 위해서죠.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // 기본 생성자
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // 복사 생성자
    Fraction(const Fraction&amp; fraction)
        // 매개변수의 해당 멤버를 사용하여 멤버들을 초기화합니다
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout &lt;&lt; &quot;Copy constructor called\n&quot;; // 작동한다는 것을 증명하기 위해 출력합니다
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Fraction(&quot; &lt;&lt; m_numerator &lt;&lt; &quot;, &quot; &lt;&lt; m_denominator &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Fraction f { 5, 3 };  // Fraction(int, int) 생성자를 호출합니다
    Fraction fCopy { f }; // Fraction(const Fraction&amp;) 복사 생성자를 호출합니다

    f.print();
    fCopy.print();

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 다음과 같이 출력됩니다.</p>
<pre><code class="language-text">Copy constructor called
Fraction(5, 3)
Fraction(5, 3)
</code></pre>
<p>우리가 방금 만든 복사 생성자는 C++이 기본으로 만들어주는 것과 똑같은 일을 합니다. 
단지 화면에 글씨를 한 줄 더 찍어줄 뿐이죠. 이 메시지를 통해 <strong>fCopy</strong> 가 만들어질 때 정말로 복사 생성자가 쓰였다는 걸 알 수 있습니다.</p>
<blockquote>
<p><strong>기억해 두세요</strong>
C++에서 &#39;접근 제어(private, public)&#39;는 객체 하나하나가 아니라 &#39;클래스 종류&#39;를 기준으로 작동합니다. 즉, 같은 <code>Fraction</code> 가족이라면 다른 객체의 숨겨진 데이터(<code>private</code> 멤버)라도 마음대로 열어볼 수 있습니다. 그래서 우리가 만든 복사 생성자 안에서 원본 <code>fraction</code> 객체의 <code>private</code> 멤버에 직접 접근할 수 있었던 것입니다.</p>
</blockquote>
<p>복사 생성자 안에서는 &#39;복사&#39;하는 일만 해야 합니다. 복사하면서 다른 엉뚱한 행동을 하게 만들면 안 됩니다. 왜냐하면 C++ 컴파일러(코드를 번역해 주는 프로그램)가 가끔 프로그램을 더 빠르게 만들려고 이 복사 과정을 통째로 건너뛰는 경우가 있기 때문입니다. 만약 복사 생성자에 중요한 다른 기능을 넣어두었다면, 그 기능이 실행되지 않을 수도 있습니다.</p>
<hr>
<h3 id="기본이-최고-암시적-복사-생성자를-선호하세요">기본이 최고! 암시적 복사 생성자를 선호하세요</h3>
<p>C++이 기본으로 제공하는 복사 기능은 대부분의 경우 우리가 딱 원하는 방식
(부품 하나씩 똑같이 복사하기)으로 완벽하게 작동합니다.</p>
<ul>
<li><strong>권장 사항:</strong> 굳이 내가 직접 복사 생성자를 만들어야 할 특별한 이유가 없다면, C++이 알아서 만들어주는 <strong>암시적 복사 생성자</strong> 를 그냥 사용하는 것이 가장 좋습니다.</li>
</ul>
<p>나중에 &#39;동적 메모리 할당&#39;이라는 조금 더 복잡한 개념을 배우게 되면, 그때는 복사 생성자를 직접 만들어야 하는 경우가 생깁니다 (이는 21.13강에서 배우게 됩니다).</p>
<hr>
<h3 id="복사-생성자의-매개변수는-반드시-참조여야-합니다">복사 생성자의 매개변수는 반드시 &#39;참조&#39;여야 합니다</h3>
<p>복사 생성자를 직접 만들 때, 받아오는 원본 재료(매개변수)는 반드시 원본 그 자체를 가리키는 <strong>참조</strong> 형태로 받아와야 합니다. 복사하는 과정에서 원본이 망가지면 안 되기 때문에, 원본을 변경할 수 없게 막아주는 <code>const</code> 참조(<code>const Fraction&amp;</code>)를 사용하는 것이 좋습니다.</p>
<ul>
<li><strong>권장 사항:</strong> 복사 생성자를 직접 작성한다면, 매개변수는 반드시 <code>const lvalue 참조</code> 여야 합니다.</li>
</ul>
<hr>
<h3 id="함수에-값을-넘겨줄-때와-복사-생성자">함수에 값을 넘겨줄 때와 복사 생성자</h3>
<p>우리가 어떤 객체를 함수에 통째로 넘겨줄 때, 원본이 아니라 원본의 &#39;복사본&#39;이 함수 안으로 들어갑니다. 이때 바로 암시적으로 <strong>복사 생성자</strong>가 사용됩니다!</p>
<p>다음 예제를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // 기본 생성자
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    // 복사 생성자
    Fraction(const Fraction&amp; fraction)
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout &lt;&lt; &quot;Copy constructor called\n&quot;;
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Fraction(&quot; &lt;&lt; m_numerator &lt;&lt; &quot;, &quot; &lt;&lt; m_denominator &lt;&lt; &quot;)\n&quot;;
    }
};

void printFraction(Fraction f) // f는 값에 의한 전달(pass by value)입니다
{
    f.print();
}

int main()
{
    Fraction f{ 5, 3 };

    printFraction(f); // 복사 생성자를 사용하여 f가 함수 매개변수로 복사됩니다

    return 0;
}
</code></pre>
<p>이 예제를 실행하면 화면에 이렇게 뜹니다.</p>
<pre><code class="language-text">Copy constructor called
Fraction(5, 3)
</code></pre>
<p><code>printFraction(f);</code> 를 부를 때, <code>main</code> 함수에 있던 원래 <strong>f</strong> 가 복사 생성자를 거쳐 함수의 매개변수인 새로운 <strong>f</strong> 로 복사되어 들어가는 것을 볼 수 있습니다.</p>
<hr>
<h3 id="함수에서-값을-반환할-때와-복사-생성자">함수에서 값을 반환할 때와 복사 생성자</h3>
<p>함수에서 계산이 끝나고 그 결과(객체)를 돌려줄 때도 마찬가지입니다. 결과를 밖으로 내보내기 위해 임시로 복사본을 하나 만드는데, 이때도 <strong>복사 생성자</strong> 가 쓰입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // 기본 생성자
    Fraction(int numerator = 0, int denominator = 1)
        : m_numerator{ numerator }, m_denominator{ denominator }
    {
    }

    // 복사 생성자
    Fraction(const Fraction&amp; fraction)
        : m_numerator{ fraction.m_numerator }
        , m_denominator{ fraction.m_denominator }
    {
        std::cout &lt;&lt; &quot;Copy constructor called\n&quot;;
    }

    void print() const
    {
        std::cout &lt;&lt; &quot;Fraction(&quot; &lt;&lt; m_numerator &lt;&lt; &quot;, &quot; &lt;&lt; m_denominator &lt;&lt; &quot;)\n&quot;;
    }
};

void printFraction(Fraction f) // f는 값에 의한 전달(pass by value)입니다
{
    f.print();
}

Fraction generateFraction(int n, int d)
{
    Fraction f{ n, d };
    return f;
}

int main()
{
    Fraction f2 { generateFraction(1, 2) }; // 복사 생성자를 사용하여 Fraction이 반환됩니다

    printFraction(f2); // 복사 생성자를 사용하여 f2가 함수 매개변수로 복사됩니다

    return 0;
}
</code></pre>
<p>이 과정은 원래대로라면 이렇게 진행됩니다.</p>
<ol>
<li><code>generateFraction</code> 함수가 끝날 때 밖으로 내보낼 &#39;임시 복사본&#39;을 만들며 복사 생성자 1번 호출.</li>
<li>이 &#39;임시 복사본&#39;을 이용해서 <code>main</code> 함수의 <strong>f2</strong> 를 만들며 복사 생성자 2번 호출.</li>
<li><strong>f2</strong> 를 <code>printFraction</code> 함수에 넘겨주면서 복사 생성자 3번 호출.</li>
</ol>
<p>그래서 화면에 &quot;Copy constructor called&quot;가 세 번 뜰 거라고 예상할 수 있습니다.
하지만 여러분의 컴퓨터에서 직접 실행해 보면 이 메시지가 두 번, 혹은 한 번만 뜰 수도 있습니다!</p>
<p>이는 컴퓨터가 코드를 똑똑하게 최적화해서 불필요한 복사 과정을 스스로 생략해 버렸기 때문입니다. 이것을 <strong>복사 생략</strong>이라고 부르는데, 이에 대해서는 나중에 14.15강에서 더 자세히 배울 예정이니 지금은 &quot;아, 똑똑하게 줄여주는 기능이 있구나&quot; 정도로만 넘어가셔도 좋습니다.</p>
<hr>
<h3 id="-default-로-기본-복사-생성자-명시하기"><code>= default</code> 로 기본 복사 생성자 명시하기</h3>
<p>복사 생성자를 직접 만들지 않으면 C++이 알아서 만들어준다고 했죠?
만약 &quot;C++아, 네가 만들어주는 그 기본 복사기 좀 써줄래?&quot;라고 코드에 확실하게 명시하고 싶다면 <code>= default;</code> 를 사용하면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Fraction
{
private:
    int m_numerator{ 0 };
    int m_denominator{ 1 };

public:
    // 기본 생성자
    Fraction(int numerator=0, int denominator=1)
        : m_numerator{numerator}, m_denominator{denominator}
    {
    }

    // 명시적으로 기본 복사 생성자를 요청합니다
    Fraction(const Fraction&amp; fraction) = default;

    void print() const
    {
        std::cout &lt;&lt; &quot;Fraction(&quot; &lt;&lt; m_numerator &lt;&lt; &quot;, &quot; &lt;&lt; m_denominator &lt;&lt; &quot;)\n&quot;;
    }
};

int main()
{
    Fraction f { 5, 3 };
    Fraction fCopy { f };

    f.print();
    fCopy.print();

    return 0;
}
</code></pre>
<hr>
<h3 id="참고-사항-조금-더-깊은-이야기">참고 사항 (조금 더 깊은 이야기)</h3>
<p>C++에는 <strong>3의 법칙(Rule of three)</strong> 이라는 아주 유명한 규칙이 있습니다. 
만약 여러분이 복사 생성자, 소멸자, 복사 대입 연산자 중 하나라도 직접 만들어야 하는 상황이 생겼다면, 높은 확률로 나머지 두 개도 세트로 같이 만들어야 한다는 뜻입니다. </p>
<p>나중에는 이게 <strong>5의 법칙</strong> 으로 확장되기도 하죠. 지금 당장 외울 필요는 없지만, 
나중에 &#39;동적 메모리&#39; 파트를 배울 때 이 법칙이 아주 중요하게 등장할 것입니다.</p>
<hr>
<h2 id="1415--클래스-초기화와-복사-생략">14.15 — 클래스 초기화와 복사 생략</h2>
<p>아주 예전 <code>1.4 레슨 - 변수 대입과 초기화</code> 에서, 우리는 기본 타입의 변수에 값을 처음 넣는 6가지 기본 방법에 대해 배웠습니다. 기억나시나요?</p>
<pre><code class="language-cpp">int a;         // 초기값 없음 (기본 초기화)
int b = 5;     // 등호 기호 뒤에 초기값 넣기 (복사 초기화)
int c( 6 );    // 괄호 안에 초기값 넣기 (직접 초기화)

// 리스트 초기화 방법들 (C++11 버전부터 등장)
int d { 7 };   // 중괄호 안에 초기값 넣기 (직접 리스트 초기화)
int e = { 8 }; // 등호 뒤 중괄호 안에 초기값 넣기 (복사 리스트 초기화)
int f {};      // 빈 중괄호로 초기화 (값 초기화)
</code></pre>
<p>이 모든 초기화 방법들은 &#39;클래스&#39;로 만든 객체에도 똑같이 사용할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
public:

    // 기본 생성자 (아무것도 안 넘겨줄 때 불림)
    Foo()
    {
        std::cout &lt;&lt; &quot;Foo()\n&quot;;
    }

    // 일반 생성자 (숫자를 넘겨줄 때 불림)
    Foo(int x)
    {
        std::cout &lt;&lt; &quot;Foo(int) &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;
    }

    // 복사 생성자 (다른 Foo를 똑같이 베낄 때 불림)
    Foo(const Foo&amp;)
    {
        std::cout &lt;&lt; &quot;Foo(const Foo&amp;)\n&quot;;
    }
};

int main()
{
    // Foo() 기본 생성자를 부릅니다
    Foo f1;           // 기본 초기화
    Foo f2{};         // 값 초기화 (이 방식을 추천해요)

    // foo(int) 일반 생성자를 부릅니다
    Foo f3 = 3;       // 복사 초기화 (explicit(명시적) 키워드가 없는 생성자만 가능)
    Foo f4(4);        // 직접 초기화
    Foo f5{ 5 };      // 직접 리스트 초기화 (이 방식을 추천해요)
    Foo f6 = { 6 };   // 복사 리스트 초기화 (explicit(명시적) 키워드가 없는 생성자만 가능)

    // foo(const Foo&amp;) 복사 생성자를 부릅니다
    Foo f7 = f3;      // 복사 초기화
    Foo f8(f3);       // 직접 초기화
    Foo f9{ f3 };     // 직접 리스트 초기화 (이 방식을 추천해요)
    Foo f10 = { f3 }; // 복사 리스트 초기화

    return 0;
}
</code></pre>
<p>최신 C++에서는 복사 초기화, 직접 초기화, 리스트 초기화가 기본적으로 다 똑같은 역할을 합니다. 바로 <strong>객체를 초기화</strong> 하는 것이죠.</p>
<p>어떤 방식으로 초기화하든 다음 과정을 거칩니다.</p>
<ul>
<li>클래스를 초기화할 때, 컴파일러는 그 클래스 안에 있는 여러 생성자
(객체를 조립하는 설명서)들을 쫙 훑어보고 가장 상황에 딱 맞는 생성자를 고릅니다. 
이 과정에서 넘겨준 값의 형태가 살짝 바뀔 수도 있습니다(암시적 형변환).</li>
<li>클래스가 아닌 일반 타입(int 등)을 초기화할 때는, 암시적 형변환이 가능한지 규칙을 확인합니다.</li>
</ul>
<blockquote>
<p><strong>핵심 포인트</strong>
초기화 방법들 사이에는 3가지 중요한 차이점이 있습니다. 
(당장 다 외우지 않으셔도 괜찮습니다!)</p>
<ol>
<li><strong>리스트 초기화(중괄호 <code>{}</code> 사용)</strong> 는 데이터가 손실될 위험이 있는 변환(예: 실수를 정수로 넣는 것)을 허락하지 않습니다.</li>
<li><strong>복사 초기화(<code>=</code> 사용)</strong> 는 명시적(explicit)이지 않은 일반적인 생성자나 변환 함수만 사용할 수 있습니다. 이 내용은 나중에 &#39;14.16 레슨&#39;에서 다룰 거예요.</li>
<li><strong>리스트 초기화</strong> 는 일반 생성자보다 &#39;리스트 생성자&#39;라는 것을 최우선으로 찾아서 연결하려고 합니다. 이 내용은 &#39;16.2 레슨&#39;에서 다루겠습니다.</li>
</ol>
<p>참고로, 특정 상황에서는 특정 초기화 방법이 아예 금지되어 있기도 합니다. 
(예: 생성자의 멤버 초기화 리스트 안에서는 <code>=</code>를 쓰는 복사 초기화는 안 되고, 직접 초기화 방식만 가능합니다.)</p>
</blockquote>
<hr>
<h3 id="불필요한-복사">불필요한 복사</h3>
<p>다음 간단한 프로그램을 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
    int m_x{};

public:
    Something(int x)
        : m_x{ x }
    {
        std::cout &lt;&lt; &quot;일반 생성자\n&quot;;
    }

    Something(const Something&amp; s)
        : m_x { s.m_x }
    {
        std::cout &lt;&lt; &quot;복사 생성자\n&quot;;
    }

    void print() const { std::cout &lt;&lt; &quot;Something(&quot; &lt;&lt; m_x &lt;&lt; &quot;)\n&quot;; }
};

int main()
{
    Something s { Something { 5 } }; // 이 줄을 집중해서 보세요!
    s.print();

    return 0;
}
</code></pre>
<p>위 코드에서 변수 <code>s</code>를 만드는 과정을 살펴봅시다.</p>
<ol>
<li>먼저 숫자 <code>5</code>를 가지고 &#39;임시&#39; <code>Something</code> 객체를 하나 뚝딱 만듭니다. 
(이때 <code>Something(int)</code> 일반 생성자가 불립니다.)</li>
<li>그리고 이 &#39;임시 객체&#39;를 바탕으로 진짜 <code>s</code>를 초기화합니다. 
임시 객체도 <code>Something</code>이고 <code>s</code>도 <code>Something</code>이니까, 임시 객체의 값을 <code>s</code>로 똑같이 베끼기 위해 <code>Something(const Something&amp;)</code> 복사 생성자가 불립니다.</li>
<li>결과적으로 <code>s</code>는 <code>5</code>라는 값을 가지게 되죠.</li>
</ol>
<p>만약 컴퓨터가 아무런 똑똑한 최적화를 하지 않는다면 화면에는 이렇게 출력될 겁니다.</p>
<blockquote>
<p>일반 생성자
복사 생성자
Something(5)</p>
</blockquote>
<p>그런데 가만 보면, 이 과정은 쓸데없이 비효율적입니다! 굳이 생성자를 두 번이나 부를 필요가 있었을까요? 사실 방금 쓴 코드는 아래 코드와 결과가 완전히 똑같습니다.</p>
<pre><code class="language-cpp">Something s { 5 }; // 일반 생성자 Something(int) 한 번만 부르고, 복사 생성자는 쓰지 않음
</code></pre>
<p>이 코드가 결과는 같으면서 복사하는 과정이 없으니 훨씬 효율적이죠.</p>
<hr>
<h3 id="복사-생략">복사 생략</h3>
<p>컴파일러는 프로그램을 더 빠르고 좋게 만들기 위해 코드를 알아서 수정할 권한이 있습니다. 그렇다면 여기서 궁금증이 하나 생깁니다.</p>
<p>*&quot;컴파일러가 알아서 쓸데없는 복사 과정을 없애고, <code>Something s { Something{5} };</code> 라고 쓴 코드를 처음부터 <code>Something s { 5 };</code> 라고 쓴 것처럼 찰떡같이 알아듣고 처리해 줄 수 있을까요?&quot;*</p>
<p>정답은 <strong>&quot;네, 그렇습니다.&quot;</strong> 그리고 이렇게 똑똑하게 처리하는 과정을 <strong>복사 생략</strong>이라고 부릅니다.</p>
<p><strong>복사 생략</strong> 은 컴파일러가 쓸데없는 객체 복사 과정을 통째로 없애버리는 최적화 기술입니다. 원래대로라면 복사 생성자를 불렀을 텐데, 컴파일러가 융통성을 발휘해 복사 자체를 안 하도록 코드를 바꿔버리는 거죠. 이렇게 컴파일러가 복사 생성자를 건너뛰는 것을 &#39;생략되었다&#39;고 표현합니다.</p>
<p>여기서 아주 중요한 특징이 하나 있습니다. 보통 다른 최적화 기술들은 프로그램의 겉보기 결과(화면에 글자가 나오는 것 등)를 바꾸면 안 된다는 &quot;as-if 규칙&quot;을 따라야 합니다.
하지만 <strong>복사 생략</strong>은 이 규칙의 <strong>예외</strong> 입니다!</p>
<p>즉, 복사 생성자 안에 화면에 글자를 출력하는 코드가 있더라도, 컴파일러는 그냥 복사를 생략해버릴 수 있습니다. 이 때문에 복사 생성자 안에는 &#39;복사&#39; 이외의 다른 행동(예: 화면 출력)을 넣으면 안 됩니다. 컴파일러가 복사를 생략해버리면 화면에 출력도 안 돼서 여러분이 기대한 것과 프로그램이 다르게 움직일 수 있거든요!</p>
<p>(이전에 배운 &#39;as-if 규칙&#39;이 기억 안 나신다면, 5.4 레슨을 참고해 주세요.)</p>
<p>위의 예제 코드를 C++17 컴파일러에서 실행해 보면 다음과 같은 결과가 나옵니다:</p>
<blockquote>
<p>일반 생성자
Something(5)</p>
</blockquote>
<p>보이시나요? 컴파일러가 쓸데없는 복사를 피하려고 복사 생성자를 아예 무시해버렸기 때문에 &quot;복사 생성자&quot;라는 글자가 화면에 나오지 않습니다! 복사 생략 덕분에 프로그램의 겉보기 결과가 바뀐 것이죠.</p>
<hr>
<h3 id="값으로-전달과-값으로-반환에서의-복사-생략">값으로 전달과 값으로 반환에서의 복사 생략</h3>
<p>함수에 값을 넘겨주거나 함수에서 값을 돌려받을 때도 보통 복사 생성자가 불립니다. 
하지만 이때도 컴파일러가 복사를 생략해버리는 경우가 있습니다. 아래 코드로 확인해 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Something
{
public:
    Something() = default;
    Something(const Something&amp;)
    {
        std::cout &lt;&lt; &quot;복사 생성자가 불렸습니다\n&quot;;
    }
};

Something rvo()
{
    return Something{}; // Something() 기본 생성자를 부르고, 반환할 때 복사 생성자를 부릅니다
}

Something nrvo()
{
    Something s{}; // Something() 기본 생성자를 부릅니다
    return s;      // 반환할 때 복사 생성자를 부릅니다
}

int main()
{
    std::cout &lt;&lt; &quot;s1 초기화 중\n&quot;;
    Something s1 { rvo() }; // 복사 생성자를 부릅니다

    std::cout &lt;&lt; &quot;s2 초기화 중\n&quot;;
    Something s2 { nrvo() }; // 복사 생성자를 부릅니다

        return 0;
}
</code></pre>
<p>만약 C++14 이하의 아주 옛날 버전에서 최적화를 끈 상태라면, 이 프로그램은 복사 생성자를 무려 4번이나 부릅니다.</p>
<ol>
<li><code>rvo</code> 함수가 <code>main</code>으로 값을 반환할 때 한 번.</li>
<li><code>rvo()</code>에서 받은 반환값으로 <code>s1</code>을 만들 때 한 번.</li>
<li><code>nrvo</code> 함수가 <code>s</code>를 <code>main</code>으로 반환할 때 한 번.</li>
<li><code>nrvo()</code>에서 받은 반환값으로 <code>s2</code>를 만들 때 한 번.</li>
</ol>
<p>하지만 <strong>복사 생략</strong>덕분에, 요즘 여러분이 쓰는 컴파일러는 이 4번의 복사 과정을 대부분(또는 전부) 지워버릴 겁니다. (예를 들어 Visual Studio 2022는 3번을 지우고, GCC는 4번 모두 지워버립니다.)</p>
<p>초보자분들은 언제 컴파일러가 복사 생략을 하고 안 하는지 달달 외울 필요가 전혀 없습니다! 그냥 <strong>&quot;컴파일러가 할 수만 있다면 알아서 불필요한 복사를 줄여 최적화를 해준다&quot;</strong> 라고만 편하게 생각하세요. 복사 생성자가 불릴 줄 알았는데 안 불렸다면, 십중팔구 이 복사 생략 때문입니다.</p>
<hr>
<h3 id="c17부터-의무가-된-복사-생략">C++17부터 의무가 된 복사 생략</h3>
<p>C++17 버전 이전에는 복사 생략이 컴파일러 마음대로 &quot;해도 되고 안 해도 되는&quot; 선택사항이었습니다. 하지만 C++17부터는 특정 상황에서는 <strong>무조건 복사를 생략해야 하는 의무</strong> 가 되었습니다. (여러분이 컴파일러에게 최적화하지 말라고 명령해도 알아서 억지로 생략해버립니다.)</p>
<p>위와 똑같은 코드를 C++17 이상 버전에서 실행하면, <code>rvo()</code> 함수가 값을 반환할 때와 그 값으로 <code>s1</code>을 만들 때는 &#39;무조건&#39; 복사가 생략되어야 합니다. 반면 <code>nvro()</code>를 통해 <code>s2</code>를 만드는 과정은 무조건 해야 하는 의무 상황은 아니기 때문에, 여러분이 쓰는 컴파일러의 종류나 설정에 따라 복사를 할 수도 있고 안 할 수도 있습니다.</p>
<ul>
<li><strong>선택적인 복사 생략 상황:</strong> 컴파일러가 복사를 생략해 주더라도, 어쨌든 복사 생성자 자체는 정상적으로 쓸 수 있게 존재해야 합니다. (지워져 있으면 에러가 납니다.)</li>
<li><strong>의무적인 복사 생략 상황:</strong> 어차피 100% 무조건 복사를 안 할 것이기 때문에, 복사 생성자가 지워져 있거나 막혀 있어도 아무 상관 없이 잘 넘어갑니다.</li>
</ul>
<blockquote>
<p><em>(고급 독자를 위한 참고)</em> 선택적 복사 생략이 일어나지 않는 상황이더라도, 복사보다 더 효율적인 &#39;이동(move)&#39;이라는 기술이 대신 쓰일 수도 있습니다. 이 부분은 16.5 레슨에서 배울 테니 지금은 넘어가셔도 좋습니다!</p>
</blockquote>
<hr>
<h2 id="1416--변환-생성자와-explicit-키워드">14.16 — 변환 생성자와 explicit 키워드</h2>
<p>10.1 레슨에서 우리는 <strong>암시적 형 변환</strong> 이라는 개념을 배웠어요. 이건 쉽게 말해서, 우리가 넘겨준 데이터의 종류(타입)가 원래 필요한 종류와 다를 때, <strong>컴파일러가 눈치껏 알아서 올바른 타입으로 쓱 바꿔주는 마법</strong>을 말해요.</p>
<p>이 마법 덕분에 우리는 아래처럼 코드를 짤 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printDouble(double d) // double(실수) 매개변수를 가집니다.
{
    std::cout &lt;&lt; d;
}

int main()
{
    printDouble(5); // int(정수) 타입의 인자를 넘겨주고 있습니다.

    return 0;
}
</code></pre>
<p>위 코드를 보면 <code>printDouble</code> 함수는 <code>double</code>(실수)을 원하는데, 우리는 <code>int</code>(정수)인 <code>5</code>를 주고 있어요. 서로 짝이 안 맞죠? 하지만 컴파일러는 &quot;아, 정수 5를 실수 5.0으로 바꿔서 주면 되겠구나!&quot; 하고 판단해서 알아서 바꿔줍니다.</p>
<hr>
<h3 id="사용자-정의-변환">사용자 정의 변환</h3>
<p>이번엔 비슷한 다른 예시를 살펴볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
private:
    int m_x{};
public:
    Foo(int x)
        : m_x{ x }
    {
    }

    int getX() const { return m_x; }
};

void printFoo(Foo f) // Foo 매개변수를 가집니다.
{
    std::cout &lt;&lt; f.getX();
}

int main()
{
    printFoo(5); // int 타입의 인자를 넘겨주고 있습니다.

    return 0;
}
</code></pre>
<p>이번에 <code>printFoo</code> 함수는 <code>Foo</code>라는 우리가 직접 만든 클래스(객체)를 원하는데, 여전히 <code>int</code> 정수인 <code>5</code>를 주고 있어요. 이번에도 컴파일러는 어떻게든 함수를 실행시키기 위해 정수 <code>5</code>를 <code>Foo</code> 객체로 몰래 변환하려고 시도합니다.</p>
<p>첫 번째 예시(정수 -&gt; 실수)는 C++ 자체에 이미 규칙이 있어서 쉬웠어요. 
하지만 이번엔 우리가 마음대로 만든 <code>Foo</code>라는 타입이잖아요? C++는 정수 5를 어떻게 <code>Foo</code>로 바꿔야 할지 기본 규칙을 가지고 있지 않습니다.</p>
<p>대신, 컴파일러는 우리가 클래스 안에 &quot;정수를 받아서 Foo로 만들어주는 함수&quot;를 만들어 두었는지 쓱 찾아봅니다. 이렇게 우리가 직접 만든 변환 방법을 <strong>사용자 정의 변환</strong>이라고 불러요.</p>
<hr>
<h3 id="변환-생성자">변환 생성자</h3>
<p>방금 예시에서 컴파일러는 정수 <code>5</code>를 <code>Foo</code> 객체로 바꿔줄 수 있는 함수를 찰떡같이 찾아냅니다. 바로 <code>Foo(int)</code> 생성자죠!</p>
<p>지금까지 우리는 생성자를 객체를 대놓고(명시적으로) 만들 때만 썼을 거예요.</p>
<ul>
<li><code>Foo x { 5 }; // int 값 5를 명시적으로 Foo로 변환 (직접 만들기)</code></li>
</ul>
<p>사실 함수를 부를 때 일어나는 일도 똑같습니다.</p>
<ul>
<li><code>printFoo(5); // int 값 5를 암시적으로 Foo로 변환 (알아서 만들기)</code></li>
</ul>
<p>결국 <code>5</code>라는 값을 주고 <code>Foo</code> 객체를 얻고 싶은 건데, <code>Foo(int)</code> 생성자가 딱 그 역할을 하는 거예요! 그래서 <code>printFoo(5)</code>를 부르면, 파라미터 <code>f</code>는 <code>5</code>를 재료로 삼아 <code>Foo(int)</code> 생성자를 통해 만들어지게 됩니다.</p>
<p><em>(참고로, C++17 버전부터는 이 과정이 중간 단계 없이 아주 빠르고 효율적으로 진행되도록 규칙이 개선되었답니다.)</em></p>
<p>이렇게 알아서 변환(암시적 변환)을 할 때 쓰일 수 있는 생성자를 <strong>변환 생성자</strong> 라고 부릅니다. 기본적으로 C++의 모든 생성자는 이 변환 생성자 역할을 할 수 있어요.</p>
<hr>
<h3 id="사용자-정의-변환은-딱-한-번만-가능해요">사용자 정의 변환은 딱 한 번만 가능해요!</h3>
<p>자, 이제 아래 코드를 볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

class Employee
{
private:
    std::string m_name{};
public:
    Employee(std::string_view name)
        : m_name{ name }
    {
    }

    const std::string&amp; getName() const { return m_name; }
};

void printEmployee(Employee e) // Employee 매개변수를 가집니다.
{
    std::cout &lt;&lt; e.getName();
}

int main()
{
    printEmployee(&quot;Joe&quot;); // 문자열 리터럴 인자를 넘겨주고 있습니다.

    return 0;
}
</code></pre>
<p>이번엔 <code>Foo</code> 대신 <code>Employee</code> 클래스를 썼어요. 함수는 <code>Employee</code>를 원하는데, 
우리는 <code>&quot;Joe&quot;</code>라는 글자를 넣었죠. 다행히 우리는 <code>Employee(std::string_view)</code>라는 생성자도 준비해 뒀어요.</p>
<p>그런데 놀랍게도 이 코드는 실행이 안 되고 <strong>에러</strong>가 납니다! 이유는 간단해요. 
<strong>컴파일러는 알아서 변환을 해줄 때, 사용자 정의 변환을 딱 한 번만 허용</strong> 하기 때문이에요.</p>
<p>위 코드에서는 변환이 두 번이나 필요합니다.</p>
<ol>
<li><code>&quot;Joe&quot;</code> (일반 글자) -&gt; <code>std::string_view</code> 로 변환</li>
<li><code>std::string_view</code> -&gt; <code>Employee</code> 로 변환</li>
</ol>
<p>이걸 고치는 두 가지 쉬운 방법이 있어요.</p>
<p><strong>방법 1: 처음부터 <code>std::string_view</code>로 주기</strong></p>
<pre><code class="language-cpp">int main()
{
    using namespace std::literals;
    printEmployee( &quot;Joe&quot;sv); // 이제 std::string_view 리터럴입니다.

    return 0;
}
</code></pre>
<p>이렇게 하면 <code>std::string_view</code>에서 <code>Employee</code>로 딱 한 번만 변환하면 되니까 잘 작동합니다!</p>
<p><strong>방법 2: 알아서 하라고 맡기지 말고, 대놓고(명시적으로) 만들어 주기</strong></p>
<pre><code class="language-cpp">int main()
{
    printEmployee(Employee{ &quot;Joe&quot; });

    return 0;
}
</code></pre>
<p>우리가 이미 <code>Employee</code>를 직접 뚝딱 만들어서 넘겨줬기 때문에, 함수에 전달할 때는 추가적인 변환이 전혀 필요 없어서 잘 작동합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
컴파일러가 몰래 하는 알아서 변환(암시적 변환)은, 우리가 중괄호 <code>{}</code>를 써서 대놓고 객체를 만드는 <strong>직접 초기화</strong> 방식으로 아주 쉽게 바꿀 수 있어요.</p>
</blockquote>
<hr>
<h3 id="변환-생성자가-대형-사고를-칠-때">변환 생성자가 대형 사고를 칠 때</h3>
<p>아래 코드를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Dollars
{
private:
    int m_dollars{};
public:
    Dollars(int d)
        : m_dollars{ d }
    {
    }

    int getDollars() const { return m_dollars; }
};

void print(Dollars d)
{
    std::cout &lt;&lt; &quot;$&quot; &lt;&lt; d.getDollars();
}

int main()
{
    print(5);

    return 0;
}
</code></pre>
<p><code>print(5)</code>를 부르면 컴파일러가 눈치껏 <code>Dollars(int)</code> 생성자를 써서 <code>5</code>를 달러 객체로 바꾸고 <code>$5</code>를 출력합니다.</p>
<p>그런데... 이게 정말 코드를 짠 사람이 원했던 걸까요? 
어쩌면 이 사람은 그냥 화면에 숫자 <code>5</code>가 나오길 바랐는데, 컴파일러가 혼자 착각해서 달러로 바꿔버린 걸지도 몰라요. 프로그램이 엄청 커지면, 이렇게 컴파일러가 마음대로 변환해버리는 탓에 언제 어디서 엉뚱한 결과가 터질지 모릅니다.</p>
<p>안전하게 가려면 <code>print(Dollars)</code> 함수에는 오직 진짜 <code>Dollars</code> 객체만 넣을 수 있게 막아두는 게 좋겠죠?</p>
<hr>
<h3 id="explicit-키워드">explicit 키워드</h3>
<p>이런 사고를 막기 위해 <code>explicit</code>  이라는 키워드를 사용합니다. 컴파일러에게
<strong>&quot;이 생성자는 네 맘대로 알아서 변환할 때 절대 쓰지 마!&quot;</strong> 라고 단호하게 선을 긋는 거예요.</p>
<p>생성자 앞에 <code>explicit</code> 을 붙이면 두 가지가 확실하게 금지됩니다.</p>
<ol>
<li>알아서 복사해서 만들어주는 것 금지.</li>
<li>알아서 타입을 쓱 바꿔주는 것 금지.</li>
</ol>
<p>이제 방금 전 코드의 생성자에 <code>explicit</code> 을 붙여볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Dollars
{
private:
    int m_dollars{};
public:
    explicit Dollars(int d) // 이제 명시적(explicit)입니다.
        : m_dollars{ d }
    {
    }

    int getDollars() const { return m_dollars; }
};

void print(Dollars d)
{
    std::cout &lt;&lt; &quot;$&quot; &lt;&lt; d.getDollars();
}

int main()
{
    print(5); // Dollars(int)가 explicit이므로 컴파일 에러가 발생합니다.

    return 0;
}
</code></pre>
<p>이제 컴파일러는 숫자 <code>5</code>를 몰래 <code>Dollars</code>로 바꿀 수 없어서, 
뭘 어떻게 해야 할지 모르고 쿨하게 <strong>에러</strong>를 뿜어냅니다!</p>
<hr>
<h3 id="그럼-explicit-생성자는-어떻게-쓰나요">그럼 explicit 생성자는 어떻게 쓰나요?</h3>
<p>알아서(암시적으로) 변환하는 것만 막았을 뿐, 우리가 대놓고(직접) 객체를 만들 때는 여전히 편하게 쓸 수 있어요!</p>
<pre><code class="language-cpp">// Dollars(int)가 explicit이라고 가정합니다.
int main()
{
    Dollars d1(5); // 정상 작동 (직접 초기화)
    Dollars d2{5}; // 정상 작동 (직접 리스트 초기화)
}
</code></pre>
<p>만약 <code>print</code> 함수에 꼭 <code>5</code>라는 숫자를 써서 부르고 싶다면 어떻게 할까요? 
에러가 나던 <code>print(5);</code> 대신, 우리가 직접 달러 객체를 만들어서 넣어주면 됩니다!</p>
<ul>
<li><code>print(Dollars{5}); // 정상 작동: 대놓고 Dollars 객체를 만들어서 넘김</code></li>
</ul>
<p>이렇게 하면 코드도 실행되고, 누가 봐도 &quot;아! 여기선 달러 객체를 만들어서 넘기려고 한 거구나!&quot; 하고 의도를 100% 명확하게 알 수 있습니다.</p>
<hr>
<h3 id="함수에서-값을-반환할-때와-explicit">함수에서 값을 반환할 때와 explicit</h3>
<p>함수에서 어떤 값을 반환할 때도, 원래 반환해야 하는 타입과 다르면 컴파일러가 알아서 변환을 시도합니다. 이때도 당연히 <code>explicit</code> 생성자는 얌체같이 끼어들 수 없어요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Foo
{
public:
    explicit Foo() // 참고: 예시를 위해 explicit으로 설정
    {
    }

    explicit Foo(int x) // 참고: explicit 설정됨
    {
    }
};

Foo getFoo()
{
    // explicit Foo() 케이스
    return Foo{ };   // 정상 작동 (명시적으로 직접 만듦)
    return { };      // 에러: 빈 괄호를 알아서 Foo로 변환할 수 없음

    // explicit Foo(int) 케이스
    return 5;        // 에러: int를 알아서 Foo로 변환할 수 없음
    return Foo{ 5 }; // 정상 작동 (명시적으로 직접 만듦)
    return { 5 };    // 에러: 괄호 안의 5를 알아서 Foo로 변환할 수 없음
}

int main()
{
    return 0;
}
</code></pre>
<hr>
<h3 id="explicit-사용-모범-가이드">explicit 사용 모범 가이드</h3>
<p>요즘 C++ 전문가들은 <strong>인자를 딱 한 개만 받는 생성자에는 기본적으로 <code>explicit</code> 을 무조건 붙이는 걸 추천</strong> 합니다. 이렇게 하면 컴파일러가 멋대로 변환하다가 버그를 만드는 걸 애초에 차단할 수 있거든요. 정말 변환이 필요하면, 우리가 중괄호 <code>{}</code>를 써서 명확하게 알려주면 되니까요.</p>
<p><strong><code>explicit</code> 을 절대 붙이면 안 되는 경우</strong>
복사(Copy) 및 이동(Move) 생성자 (얘네는 애초에 타입 변환을 하는 목적이 아니에요).</p>
<p><strong>보통은 <code>explicit</code> 을 안 붙이는 경우</strong>
인자가 아예 없는 기본 생성자 (특별히 막을 이유가 거의 없습니다).
인자를 무조건 여러 개 받아야 하는 생성자 (애초에 변환 후보로 잘 안 쓰입니다).</p>
<p><strong>꼭 <code>explicit</code> 을 붙여야 하는 경우</strong>
<strong>인자를 딱 한 개만 받는 생성자! (가장 중요)</strong></p>
<p><strong>예외 상황 (안 붙여도 괜찮은 경우)</strong>
변환하기 전과 후가 완전히 똑같은 의미를 가지고 있고, 컴퓨터 자원도 적게 먹을 때. (예를 들어 C스타일 글자를 <code>std::string_view</code>로 바꿀 때는 아주 가벼워서 괜찮지만, 무거운 <code>std::string</code>을 만들 때는 <code>explicit</code> 으로 막아둡니다.)</p>
<p><strong>최종 요약</strong>
인자를 1개만 받는 생성자에는 일단 <code>explicit</code> 을 붙이는 습관을 들이세요.
컴파일러가 내 코드를 멋대로 해석하는 걸 막아주는 든든한 방패가 되어줄 겁니다.</p>
<hr>
<h2 id="1417--constexpr-애그리게이트집합체와-클래스">14.17 — Constexpr 애그리게이트(집합체)와 클래스</h2>
<p>지난 F.1 레슨에서 우리는 <code>constexpr</code> 함수에 대해 배웠어요. 이 함수들은 코드를 컴파일할 때(번역할 때) 계산될 수도 있고, 프로그램이 실제로 실행 중일 때(런타임) 계산될 수도 있는 아주 똑똑한 녀석들이죠. 예제를 하나 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

constexpr int greater(int x, int y)
{
    return (x &gt; y ? x : y);
}

int main()
{
    std::cout &lt;&lt; greater(5, 6) &lt;&lt; &#39;\n&#39;; // greater(5, 6)은 컴파일할 때 계산될 수도 있고, 실행 중(런타임)에 계산될 수도 있어요

    constexpr int g { greater(5, 6) };  // greater(5, 6)은 무조건 컴파일할 때 계산되어야 해요
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;             // 6을 출력해요

    return 0;
}
</code></pre>
<p>이 예제에서 <code>greater()</code> 는 <code>constexpr</code> 함수이고, <code>greater(5, 6)</code> 은 상수 표현식이에요. 
즉, 컴파일할 때나 실행할 때 아무 때나 계산될 수 있죠.</p>
<p><code>std::cout &lt;&lt; greater(5, 6)</code> 부분에서는 굳이 컴파일 단계에서 계산을 끝낼 필요가 없기 때문에, 컴파일러(코드를 번역해 주는 프로그램)가 언제 계산할지 자유롭게 결정해요. 
하지만 <code>g</code> 라는 <code>constexpr</code> 변수에 값을 넣으려고 할 때는 이야기가 다릅니다! </p>
<p><code>g</code> 는 무조건 컴파일할 때 값이 정해져야 하는 변수이기 때문에, 이때 <code>greater(5, 6)</code> 은 <strong>반드시 컴파일 단계에서 계산</strong> 되어야만 한답니다.</p>
<p>이제 조금 다르게 생긴 비슷한 예제를 살펴볼게요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Pair
{
    int m_x {};
    int m_y {};

    int greater() const
    {
        return (m_x &gt; m_y  ? m_x : m_y);
    }
};

int main()
{
    Pair p { 5, 6 };                  // 입력값들이 모두 constexpr(상수) 값이에요
    std::cout &lt;&lt; p.greater() &lt;&lt; &#39;\n&#39;; // p.greater()는 실행 중(런타임)에 계산돼요

    constexpr int g { p.greater() };  // 컴파일 에러: greater()가 constexpr 함수가 아니에요
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이번 버전에서는 <code>Pair</code> 라는 구조체를 만들었고, <code>greater()</code> 가 그 안의 멤버 함수가 되었네요. 그런데 여기서 문제가 생겨요! 멤버 함수인 <code>greater()</code> 앞에 <code>constexpr</code> 이 안 붙어있죠? 그래서 <code>p.greater()</code> 는 상수 표현식이 될 수 없어요.</p>
<p>따라서 <code>std::cout &lt;&lt; p.greater()</code> 를 부를 때는 그냥 프로그램 실행 중에 평범하게 계산됩니다. 하지만 <code>constexpr</code> 변수인 <code>g</code> 에 값을 넣으려고 하면, 컴파일러가 
&quot;어? 이거 컴파일할 때 계산 못 하는 함수인데?&quot; 라며 불평하고 <strong>컴파일 에러</strong>를 뿜어내게 됩니다.</p>
<p><code>p</code> 에 들어가는 입력값이 5와 6으로 이미 딱 정해져 있는데, 왜 컴파일할 때 계산하지 못하는 걸까요? 어떻게 하면 고칠 수 있는지 알아봅시다!</p>
<hr>
<h3 id="constexpr-멤버-함수">Constexpr 멤버 함수</h3>
<p>일반 함수들처럼, 구조체나 클래스 안에 속한 멤버 함수도 앞에 <code>constexpr</code> 키워드만 붙여주면 <code>constexpr</code> 함수로 만들 수 있어요. 이렇게 하면 컴파일할 때나 실행할 때 언제든 계산할 수 있게 된답니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Pair
{
    int m_x {};
    int m_y {};

    constexpr int greater() const // 컴파일할 때나 실행할 때 모두 계산할 수 있어요
    {
        return (m_x &gt; m_y  ? m_x : m_y);
    }
};

int main()
{
    Pair p { 5, 6 };
    std::cout &lt;&lt; p.greater() &lt;&lt; &#39;\n&#39;; // 성공: p.greater()는 실행 중에 계산돼요

    constexpr int g { p.greater() };  // 컴파일 에러: p가 constexpr 변수가 아니에요
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이번에는 <code>greater()</code> 함수에 <code>constexpr</code> 을 붙여주었어요. 그래서 이제 컴파일러가 알아서 언제 계산할지 고를 수 있게 되었죠.
첫 번째 출력문에서는 실행 중에 아주 잘 계산됩니다.</p>
<p><strong>하지만!</strong> <code>g</code> 변수를 만들려고 시도하면 여전히 에러가 납니다. 
왜냐고요? <code>greater()</code> 자체는 <code>constexpr</code> 로 업그레이드되었지만, 그 함수를 부르는 주인공인 <code>p</code> 가 아직 <code>constexpr</code> 변수가 아니기 때문이에요. 
주인공이 준비가 안 돼서 전체가 상수가 될 수 없는 상황인 거죠.</p>
<hr>
<h3 id="constexpr-집합체">Constexpr 집합체</h3>
<p>좋아요, 문제가 <code>p</code> 라면 해결책은 간단합니다. <code>p</code> 도 <code>constexpr</code> 로 만들어주면 되죠!</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Pair // Pair는 애그리게이트(단순 집합체)예요
{
    int m_x {};
    int m_y {};

    constexpr int greater() const
    {
        return (m_x &gt; m_y  ? m_x : m_y);
    }
};

int main()
{
    constexpr Pair p { 5, 6 };        // 이제 constexpr이 되었어요
    std::cout &lt;&lt; p.greater() &lt;&lt; &#39;\n&#39;; // p.greater()는 실행 중이거나 컴파일할 때 계산돼요

    constexpr int g { p.greater() };  // p.greater()는 무조건 컴파일할 때 계산되어야 해요
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>짠! 완벽하게 작동합니다. <code>Pair</code> 구조체는 데이터만 모아둔 &#39;애그리게이트&#39;이고, 애그리게이트는 기본적으로 <code>constexpr</code> 기능을 지원하거든요. 이제 변수 <code>p</code> 도 <code>constexpr</code> 타입이고, <code>greater()</code> 도 <code>constexpr</code> 멤버 함수니까, 찰떡궁합으로 <code>p.greater()</code> 는 완벽한 상수 표현식이 되었습니다!</p>
<hr>
<h3 id="constexpr-클래스-객체와-constexpr-생성자">Constexpr 클래스 객체와 Constexpr 생성자</h3>
<p>자, 이번에는 우리들의 <code>Pair</code> 를 집합체가 아닌 진짜 &#39;클래스&#39; 형태로 만들어 볼게요. 
(데이터를 숨기고 생성자를 추가하면 더 이상 집합체가 아니게 됩니다.)</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Pair // Pair는 이제 애그리게이트가 아니에요
{
private:
    int m_x {};
    int m_y {};

public:
    Pair(int x, int y): m_x { x }, m_y { y } {}

    constexpr int greater() const
    {
        return (m_x &gt; m_y  ? m_x : m_y);
    }
};

int main()
{
    constexpr Pair p { 5, 6 };       // 컴파일 에러: p는 리터럴 타입(Literal type)이 아니에요
    std::cout &lt;&lt; p.greater() &lt;&lt; &#39;\n&#39;;

    constexpr int g { p.greater() };
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>방금 전 예제와 거의 똑같지만, <code>Pair</code> 가 더 이상 집합체가 아니라는 점만 다릅니다. 
그런데 이 코드를 컴파일하면 &quot;<code>Pair</code> 가 <strong>리터럴 타입</strong> 이 아니야!&quot; 라며 에러가 발생해요. 
대체 무슨 소리일까요?</p>
<p>C++에서 <strong>리터럴 타입</strong>이란, &#39;컴파일하는 동안(상수 표현식 안에서) 짠! 하고 만들어질 수 있는 자격증을 갖춘 데이터 타입&#39;을 말해요. </p>
<p>다시 말해, 이 자격증이 없는 타입은 <code>constexpr</code> 객체로 만들 수가 없어요. 
안타깝게도 방금 우리가 만든 <code>Pair</code> 클래스는 이 자격증이 없네요.</p>
<blockquote>
<p><strong>[용어 정리]</strong> &gt; <strong>리터럴</strong> 과 <strong>리터럴 타입</strong>은 조금 다릅니다.</p>
<ul>
<li><strong>리터럴</strong>은 소스 코드에 직접 적어 넣은 상수 값(예: 숫자 <code>5</code>)을 말해요.</li>
<li><strong>리터럴 타입</strong>은 이런 <code>constexpr</code> 값이 될 수 있는 &#39;데이터의 종류(타입)&#39;를 말합니다.</li>
</ul>
<p>리터럴 타입의 조건은 복잡하지만, 다음 4가지만 기억해 두시면 좋아요!</p>
<ol>
<li>일반적인 숫자나 포인터</li>
<li>참조(Reference) 타입</li>
<li>대부분의 애그리게이트</li>
<li><strong><code>constexpr</code> 생성자를 가진 클래스</strong></li>
</ol>
</blockquote>
<p>아하! 이제 이유를 알겠네요. 클래스로 물건(객체)을 만들려면 컴파일러가 &#39;생성자&#39;라는 함수를 불러서 초기 설정을 해줘야 해요. 그런데 우리 <code>Pair</code> 클래스의 생성자 앞에는 <code>constexpr</code> 이 없어서 컴파일할 때 부를 수가 없었던 거예요.</p>
<p>고치는 방법은 너무나 쉽습니다. 생성자 앞에도 <code>constexpr</code> 을 딱! 붙여주기만 하면 끝이에요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Pair
{
private:
    int m_x {};
    int m_y {};

public:
    constexpr Pair(int x, int y): m_x { x }, m_y { y } {} // 이제 constexpr 생성자예요

    constexpr int greater() const
    {
        return (m_x &gt; m_y  ? m_x : m_y);
    }
};

int main()
{
    constexpr Pair p { 5, 6 };
    std::cout &lt;&lt; p.greater() &lt;&lt; &#39;\n&#39;;

    constexpr int g { p.greater() };
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>구조체를 썼을 때처럼 이제 아주 잘 작동합니다!</p>
<blockquote>
<p><strong>[초보자를 위한 꿀팁]</strong> 
여러분이 만든 클래스가 컴파일할 때 계산되기를 원한다면, <strong>멤버 함수와 생성자 모두에게 <code>constexpr</code> 을 붙여주세요.</strong></p>
<p>참고로 <code>constexpr</code> 은 클래스 설계의 아주 중요한 부분이라서, 나중에 함부로 빼버리면 이 클래스를 믿고 쓰던 다른 코드들이 펑! 하고 망가질 수 있으니 조심해야 해요.</p>
</blockquote>
<hr>
<h3 id="constexpr-멤버-함수는-일반-객체에도-필요할-수-있어요">Constexpr 멤버 함수는 일반 객체에도 필요할 수 있어요</h3>
<p>위의 예제에서는 상수 변수인 <code>g</code> 를 만들기 위해 관련된 <code>p</code>, 생성자, <code>greater()</code> 모두가 <code>constexpr</code> 이어야 한다는 걸 보았어요. 하지만 함수로 한번 감싸면 조금 헷갈릴 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Pair
{
private:
    int m_x {};
    int m_y {};

public:
    constexpr Pair(int x, int y): m_x { x }, m_y { y } {}

    constexpr int greater() const
    {
        return (m_x &gt; m_y  ? m_x : m_y);
    }
};

constexpr int init()
{
    Pair p { 5, 6 };    // 컴파일할 때 계산되려면 생성자가 constexpr이어야 해요
    return p.greater(); // 컴파일할 때 계산되려면 greater() 함수도 constexpr이어야 해요
}

int main()
{
    constexpr int g { init() }; // init() 함수가 컴파일 단계에서 계산돼요
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><code>constexpr</code> 함수는 언제든 실행될 수 있다고 했죠? 만약 컴파일할 때 실행된다면, <strong>그 안에서 부르는 다른 함수들도 무조건 컴파일할 때 실행할 수 있는 <code>constexpr</code> 함수여야만 해요.</strong></p>
<p>위 코드에서 <code>g</code> 는 상수이므로 <code>init()</code> 은 컴파일할 때 계산되어야 합니다. 흥미로운 건 <code>init()</code> 안에서 <code>p</code> 를 굳이 <code>constexpr</code> 이나 <code>const</code> 로 만들지 않았다는 거예요. 그래도 괜찮습니다. 하지만 결국 이 모든 과정이 컴파일 중에 이루어져야 하기 때문에, <code>p</code> 를 만드는 생성자와 <code>greater()</code> 함수는 반드시 <code>constexpr</code> 이어야 해요. 둘 중 하나라도 아니면 에러가 발생한답니다.</p>
<blockquote>
<p><strong>핵심 요약</strong> 
<code>constexpr</code> 함수가 컴파일 환경에서 계산되고 있다면, 그 안에서는 오직 <code>constexpr</code> 함수들만 호출할 수 있습니다!</p>
</blockquote>
<hr>
<h3 id="constexpr-멤버-함수는-const일-수도-아닐-수도-있어요-c14-버전-이상">Constexpr 멤버 함수는 const일 수도, 아닐 수도 있어요 (C++14 버전 이상)</h3>
<p>아주 옛날 C++11 버전에서는 <code>constexpr</code> 멤버 함수를 만들면 무조건 값이 절대 변하지 않는 <code>const</code> 함수로 취급했어요. 하지만 C++14 버전부터는 규칙이 바뀌었답니다! 이제 <code>constexpr</code> 함수라고 해서 무조건 <code>const</code> 인 건 아니에요. 만약 값을 바꾸지 않는 안전한 함수로 만들고 싶다면 끝에 명시적으로 <code>const</code> 를 적어줘야 합니다.</p>
<hr>
<h3 id="constexpr이면서-const가-아닌-멤버-함수는-데이터를-바꿀-수-있어요-선택-심화-학습">Constexpr이면서 const가 아닌 멤버 함수는 데이터를 바꿀 수 있어요 (선택 심화 학습)</h3>
<p>조금 헷갈릴 수 있지만 재미있는 사실! <code>constexpr</code> 이면서 <code>const</code> 가 아닌 멤버 함수는 컴파일하는 도중에도 클래스 안의 데이터를 쇽쇽 바꿀 수 있어요. (단, 그 객체 자체가 <code>const</code> 로 묶여있지 않을 때만요.)</p>
<p>억지로 만든 예제이긴 하지만 한번 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

class Pair
{
private:
    int m_x {};
    int m_y {};

public:
    constexpr Pair(int x, int y): m_x { x }, m_y { y } {}

    constexpr int greater() const // constexpr이면서 const 함수예요
    {
        return (m_x &gt; m_y  ? m_x : m_y);
    }

    constexpr void reset() // constexpr이지만 const는 아니에요
    {
        m_x = m_y = 0; // const가 아닌 멤버 함수는 내부 데이터를 바꿀 수 있어요
    }

    constexpr const int&amp; getX() const { return m_x; }
};

// 이 함수는 constexpr이에요
constexpr Pair zero()
{
    Pair p { 1, 2 }; // p는 const가 아니에요 (변할 수 있어요)
    p.reset();       // const가 아닌 객체에서 const가 아닌 멤버 함수를 부르는 건 문제 없어요
    return p;
}

int main()
{
    Pair p1 { 3, 4 };
    p1.reset();                     // const가 아닌 객체에서 const가 아닌 멤버 함수를 부르는 건 문제 없어요
    std::cout &lt;&lt; p1.getX() &lt;&lt; &#39;\n&#39;; // 0을 출력해요

    Pair p2 { zero() };             // zero()는 실행 중(런타임)에 계산돼요
    p2.reset();                     // const가 아닌 객체에서 const가 아닌 멤버 함수를 부르는 건 문제 없어요
    std::cout &lt;&lt; p2.getX() &lt;&lt; &#39;\n&#39;; // 0을 출력해요

    constexpr Pair p3 { zero() };   // zero()는 컴파일할 때 계산돼요
//  p3.reset();                     // 컴파일 에러: const 객체에서는 const가 아닌 멤버 함수를 부를 수 없어요
    std::cout &lt;&lt; p3.getX() &lt;&lt; &#39;\n&#39;; // 0을 출력해요

    return 0;
}
</code></pre>
<p>복잡해 보이지만 이 두 가지만 기억하세요.</p>
<ol>
<li><code>const</code> 가 아닌 함수는 <code>const</code> 가 아닌 객체의 데이터를 자유롭게 바꿀 수 있다.</li>
<li><code>constexpr</code> 함수는 실행할 때든 컴파일할 때든 다 쓰일 수 있다.</li>
</ol>
<p><code>p1</code> 과 <code>p2</code> 는 프로그램이 실행될 때 계산되므로 평범하게 데이터를 바꿀 수 있습니다.
정말 신기한 건 <code>p3</code> 예제예요! <code>p3</code> 는 <code>constexpr</code> 이니까 무조건 컴파일할 때 <code>zero()</code> 함수가 실행되어야 해요. 컴파일하는 중인데도 <code>zero()</code> 안에서 값을 바꾸는 <code>reset()</code> 함수가 아주 잘 작동하고 결과를 돌려줍니다.</p>
<blockquote>
<p><strong>[저자의 한마디]</strong> &gt; &quot;아니, 컴파일해서 상수를 만드는 중인데, 그 과정에서 상수가 아닌 변수(값이 막 변하는 변수)를 쓸 수 있다고요?&quot; 라며 머리가 띵~ 하실 수 있어요. 지극히 정상입니다!
<code>constexpr</code> 변수라고 해서 꼭 <code>const</code> 값들로만 초기화해야 하는 건 아니에요. 핵심은 <strong>&quot;컴파일러가 컴파일하는 타이밍에 그 값을 미리 계산해 낼 수 있는가?&quot;</strong> 입니다. 계산만 무사히 끝낼 수 있다면, 그 중간 과정에서 값이 변하는 일반 변수를 쓰든 데이터를 지지고 볶든 컴파일러는 상관하지 않는답니다!</p>
</blockquote>
<h3 id="const-참조reference나-포인터를-반환하는-constexpr-함수-선택-심화-학습">Const 참조(Reference)나 포인터를 반환하는 Constexpr 함수 (선택 심화 학습)</h3>
<p>보통은 <code>constexpr</code> 과 <code>const</code> 를 바로 옆에 나란히 붙여서 쓸 일이 거의 없어요. 하지만 함수가 돌려주는(반환하는) 값이 &#39;절대 변하면 안 되는 원본 데이터(const 참조)&#39;일 때는 두 개가 같이 쓰이기도 합니다.</p>
<p>위 코드의 <code>getX()</code> 함수가 바로 그런 녀석이죠.</p>
<p><code>constexpr const int&amp; getX() const { return m_x; }</code></p>
<p>세상에, <code>const</code> 가 정말 많죠? 너무 겁먹지 마시고 하나씩 뜯어볼게요!</p>
<ul>
<li>맨 앞의 <code>constexpr</code>: &quot;나는 컴파일할 때도 실행될 수 있는 함수야!&quot;</li>
<li>중간의 <code>const int&amp;</code>: &quot;내가 돌려줄 값은 절대 수정하면 안 되는 원본 데이터야!&quot;</li>
<li>맨 끝의 <code>const</code>: &quot;나(함수)는 클래스 안의 데이터를 절대 바꾸지 않는 착하고 안전한 녀석이야!&quot;</li>
</ul>
<blockquote>
<p><strong>[여담으로...]</strong> 
만약 참조 대신 포인터로 돌려준다면 코드가 이렇게 생겼을 거예요.
<code>constexpr const int* const getXPtr() const { return &amp;m_x; }</code>
<code>const</code> 가 4개나 줄줄이 달려있네요! 참 아름답지 않나요? ...아니라고요? 네, 알겠습니다. </p>
</blockquote>
<hr>
<p>C++를 처음 배울 때 클래스 개념은 정말 외계어처럼 느껴지는 게 당연해요! 절대 바보가 아니십니다. 누구나 처음엔 다 헷갈리고 어려워하니까 걱정하지 마세요. 요청하신 대로 원래의 뜻은 그대로 살리면서, 최대한 이해하기 쉽고 친절하게 풀어서 번역해 드렸습니다.</p>
<p>(참고로 제공해주신 원문에는 코드 블록이 포함되어 있지 않아서, 본문 텍스트 번역과 띄어쓰기 규칙에 최대한 집중했습니다!)</p>
<hr>
<h2 id="장-복습">장 복습</h2>
<ul>
<li><strong>절차적 프로그래밍(Procedural programming)</strong> 은 프로그램의 논리를 실행하는 &quot;절차&quot;(C++에서는 함수라고 부릅니다)를 만드는 데 집중하는 방식입니다. 우리는 이 함수들에게 데이터를 넘겨주고, 함수들은 그 데이터로 작업을 처리한 뒤, 필요에 따라 결과를 돌려줍니다.</li>
<li><strong>객체 지향 프로그래밍(Object-oriented programming, 흔히 OOP)</strong> 은 특성(데이터)과 잘 짜인 행동(함수)을 모두 한데 모아 놓은 &#39;나만의 데이터 타입&#39;을 만드는 데 집중하는 방식입니다.</li>
<li><strong>클래스 불변성(Class invariant)</strong> 이란, 객체가 살아있는 동안 정상적인 상태를 유지하기 위해 항상 &#39;참(True)&#39;이어야만 하는 규칙이나 조건입니다. 이 규칙이 깨져버린 객체는 <strong>유효하지 않은 상태(invalid state)</strong> 가 되었다고 말하며, 이 객체를 계속 쓰면 프로그램이 예상치 못하게 튕기거나 이상하게 동작할 수 있습니다.</li>
<li><strong>클래스(Class)</strong> 란 데이터와 그 데이터를 가지고 노는 함수들을 하나의 보따리 안에 묶어 놓은 복합 데이터 타입입니다.</li>
<li>클래스 안에 들어 있는 함수들을 <strong>멤버 함수(Member functions)</strong> 라고 부릅니다. 멤버 함수를 실행할 때 기준이 되는 객체를 보통 <strong>암시적 객체(Implicit object)</strong> 라고 합니다. 반대로 클래스에 속하지 않은 일반 함수들은 구분하기 쉽게 <strong>비멤버 함수(Non-member functions)</strong> 라고 부릅니다. 만약 여러분의 클래스 안에 데이터가 하나도 없다면, 클래스 대신 네임스페이스(namespace)를 쓰는 것이 더 좋습니다.</li>
<li><strong>const 멤버 함수</strong> 는 &quot;나는 객체의 데이터를 절대 바꾸지 않을 것이며, 데이터를 바꿀 위험이 있는 다른 함수도 부르지 않겠다&quot;라고 약속하는 함수입니다. 객체의 상태를 건드릴 일이 전혀 없는 함수라면 무조건 <code>const</code>를 붙여주세요. 그래야 수정 가능한 객체는 물론, 수정 불가능한 <code>const</code> 객체에서도 이 함수를 안심하고 쓸 수 있습니다.</li>
<li>클래스의 각 멤버는 누가 나에게 접근할 수 있는지를 정하는 <strong>접근 수준(Access level)</strong> 이라는 속성을 가집니다. 이를 흔히 <strong>접근 제어(Access controls)</strong> 라고도 부릅니다. 이 규칙은 객체 하나하나마다 다르게 적용되는 게 아니라, 클래스 전체를 기준으로 정해집니다.</li>
<li><strong>Public(공개) 멤버</strong> 는 접근에 아무런 제한이 없는 멤버입니다. (코드의 범위 안에만 있다면) 누구나 자유롭게 가져다 쓸 수 있습니다. 같은 클래스의 다른 멤버는 물론이고, 클래스 바깥에 있는 외부 코드(이를 &#39;대중(public)&#39;이라고 부릅니다)에서도 접근할 수 있습니다. 구조체(<code>struct</code>)는 기본적으로 모든 멤버가 공개(public) 상태입니다.</li>
<li><strong>Private(비공개) 멤버</strong> 는 오직 같은 클래스 안에 있는 식구들(다른 멤버들)끼리만 접근할 수 있는 꽁꽁 숨겨진 멤버입니다. 클래스(<code>class</code>)는 기본적으로 모든 멤버가 비공개(private) 상태입니다. private 멤버가 하나라도 생기면 그 클래스는 더 이상 단순한 데이터 모음(aggregate)이 아니게 되므로, 중괄호 <code>{}</code>를 이용한 단순 초기화를 쓸 수 없습니다. private 멤버 변수의 이름은 앞에 <code>m_</code>을 붙여서 지어보세요. 그러면 일반 지역 변수나 함수 매개변수와 쉽게 구분할 수 있어서 아주 편합니다.</li>
<li>우리는 <strong>접근 지정자(Access specifier)</strong> 를 사용해서 멤버를 공개할지 비공개할지 직접 정할 수 있습니다. 하지만 구조체(<code>struct</code>)를 쓸 때는 모든 멤버가 public이 되도록 접근 지정자를 아예 쓰지 않는 것이 좋습니다.</li>
<li><strong>접근 함수(Access function)</strong> 는 꽁꽁 숨겨진 private 멤버 변수의 값을 슬쩍 보거나 바꾸기 위해 만들어 놓은 아주 단순한 public 멤버 함수입니다. 여기에는 두 가지 종류가 있습니다. <strong>Getter(게터)</strong> 는 숨겨진 값을 읽어서 바깥으로 전달해 주는 함수이고, <strong>Setter(세터)</strong> 는 바깥에서 값을 받아와 숨겨진 변수의 값을 새롭게 바꿔주는 함수입니다.</li>
<li>클래스의 <strong>인터페이스(Interface)</strong> 는 사용자가 그 클래스의 객체와 어떻게 소통할 수 있는지를 알려주는 &#39;사용 설명서&#39;와 같습니다. 클래스 밖에서는 오직 public 멤버에만 접근할 수 있기 때문에, 이 public 멤버들이 모여서 하나의 인터페이스를 이룹니다. 그래서 이를 <strong>퍼블릭 인터페이스(Public interface)</strong> 라고도 부릅니다.</li>
<li>클래스의 <strong>구현부(Implementation)</strong> 는 클래스가 실제로 작동하게 만드는 알맹이 코드들입니다. 데이터를 담아두는 멤버 변수들과, 실제 연산을 수행하는 멤버 함수의 몸체 부분이 모두 여기에 속합니다.</li>
<li>프로그래밍에서 <strong>데이터 은닉(Data hiding)</strong> 이란, 복잡한 내부 동작 원리(구현부)를 사용자에게서 숨겨버림으로써 &#39;사용법(인터페이스)&#39;과 &#39;내부 원리(구현부)&#39;를 분리하는 기술입니다.</li>
<li><strong>캡슐화(Encapsulation)</strong> 라는 용어도 데이터 은닉과 비슷한 뜻으로 자주 쓰입니다. 하지만 캡슐화는 데이터를 숨기든 안 숨기든 단순히 &#39;데이터와 함수를 하나로 묶는다&#39;는 뜻으로도 쓰이기 때문에, 문맥에 따라 의미가 살짝 헷갈릴 수 있습니다.</li>
<li>클래스를 설계할 때는 사용자가 알아야 할 public 멤버를 먼저 위에 적고, 몰라도 되는 private 멤버를 맨 아래에 적는 것이 좋습니다. 이렇게 하면 사용 설명서(퍼블릭 인터페이스)가 돋보이고 복잡한 내부 코드는 숨겨지는 효과가 있습니다.</li>
<li><strong>생성자(Constructor)</strong> 는 클래스 객체가 처음 태어날 때(만들어질 때) 초기 설정을 해주는 특별한 멤버 함수입니다. 단순 데이터 묶음이 아닌 일반 클래스 객체를 제대로 만들려면 반드시 상황에 맞는 생성자가 있어야 합니다.</li>
<li><strong>멤버 초기화 목록(Member initializer list)</strong> 을 사용하면 생성자 안에서 멤버 변수들을 깔끔하게 초기화할 수 있습니다. 이때 목록의 순서는 클래스 안에서 변수를 선언했던 순서와 똑같이 맞추는 것이 좋습니다. 생성자 몸체 안에서 <code>=</code> 기호로 값을 넣는 것보다, 이 초기화 목록을 사용하는 것이 훨씬 더 좋고 빠른 방법입니다.</li>
<li>아무런 매개변수(입력값)를 받지 않거나, 모든 매개변수에 기본값이 정해져 있는 생성자를 <strong>기본 생성자(Default constructor)</strong> 라고 부릅니다. 사용자가 특별한 초기값을 주지 않으면 이 기본 생성자가 알아서 출동합니다. 만약 여러분이 생성자를 하나도 만들지 않았다면, 친절한 C++ 컴파일러가 알아서 텅 빈 기본 생성자를 하나 만들어 주는데 이를 <strong>암시적 기본 생성자(Implicit default constructor)</strong> 라고 합니다.</li>
<li>생성자는 자기 혼자 다 일하지 않고, 같은 클래스 안에 있는 다른 생성자에게 초기화 작업을 떠넘길(위임할) 수 있습니다. 이렇게 생성자끼리 연결되는 것을 <strong>생성자 체이닝(Constructor chaining)</strong> 이라 하고, 일을 떠넘기는 생성자를 <strong>위임 생성자(Delegating constructors)</strong> 라고 부릅니다. 참고로 생성자는 일을 떠넘기거나 직접 초기화하거나 둘 중 하나만 할 수 있습니다.</li>
<li><strong>임시 객체(Temporary object)</strong> 란 이름도 없이 태어났다가 코드 한 줄(단일 표현식)이 끝나면 곧바로 사라지는 아주 짧은 수명의 객체입니다.</li>
<li><strong>복사 생성자(Copy constructor)</strong> 는 이미 존재하는 똑같은 타입의 객체를 마치 복사기처럼 그대로 베껴서 새로운 객체를 만들 때 쓰는 생성자입니다. 여러분이 직접 만들지 않으면, C++이 알아서 변수들을 하나씩 똑같이 복사해 주는 암시적 복사 생성자를 만들어 줍니다.</li>
<li><strong>as-if 규칙(As-if rule)</strong> 이란, 프로그램의 겉보기 동작만 변하지 않는다면 컴파일러가 코드를 더 빠르게 실행되도록 자기 마음대로 뜯어고칠 수 있다는 규칙입니다. 이 규칙의 예외 중 하나가 <strong>복사 생략(Copy elision)</strong> 이라는 최적화 기술입니다. 이는 불필요하게 객체를 복사하는 낭비를 컴파일러가 알아서 없애버리는 마법 같은 기능입니다. 이 기능 때문에 복사 생성자가 호출될 일 자체가 사라지는 것을 두고, 생성자가 <strong>생략되었다(elided)</strong> 고 표현합니다.</li>
<li>어떤 값을 우리가 만든 클래스 타입으로 바꾸거나, 그 반대로 바꾸기 위해 직접 만든 함수를 <strong>사용자 정의 변환(User-defined conversion)</strong> 이라고 합니다. 그중에서도 자동으로 타입을 슬쩍 바꿔주는(암시적 변환) 역할을 할 수 있는 생성자를 <strong>변환 생성자(Converting constructor)</strong> 라고 부릅니다. 원칙적으로 모든 생성자는 이 변환 생성자 역할을 할 수 있습니다.</li>
<li>생성자가 자기 마음대로 타입을 변환하는 데 쓰이는 게 싫다면 <code>explicit</code> (명시적인) 이라는 키워드를 붙여주면 됩니다. 이 키워드가 붙은 생성자는 알아서 값을 복사해서 넣어주거나, 암시적으로 형태를 바꾸는 일에 쓰일 수 없습니다.</li>
<li>입력값(인수)을 딱 하나만 받는 생성자 앞에는 무조건 <code>explicit</code>을 붙이는 것을 습관으로 들이세요. 다만, 타입이 달라도 의미상 완전히 똑같고 성능도 빠르다면(<code>std::string</code>을 <code>std::string_view</code>로 바꾸는 등) 예외적으로 안 붙여도 괜찮습니다. 단, 복사 생성자나 이동 생성자 앞에는 절대 <code>explicit</code>을 붙이면 안 됩니다. 애초에 걔네들은 형태를 변환하려고 있는 애들이 아니기 때문입니다.</li>
<li>생성자를 포함한 멤버 함수 앞에는 <code>constexpr</code> (상수 표현식)을 붙일 수 있습니다. C++14 버전부터는 <code>constexpr</code>을 붙였다고 해서 그 함수가 자동으로 <code>const</code> 멤버 함수가 되지는 않습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 13]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-13</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-13</guid>
            <pubDate>Thu, 26 Feb 2026 00:44:06 GMT</pubDate>
            <description><![CDATA[<h2 id="131--프로그램-정의-사용자-정의-타입-소개">13.1 — 프로그램 정의 (사용자 정의) 타입 소개</h2>
<p>기본 타입(Fundamental types)은 C++ 언어 자체에 내장되어 있기 때문에 즉시 사용할 수 있습니다. 
예를 들어, <code>int</code>나 <code>double</code> 타입의 변수를 만들고 싶다면 다음과 같이 작성하면 됩니다.</p>
<pre><code class="language-cpp">int x; // 기본 타입 &#39;int&#39;의 변수 정의
double d; // 기본 타입 &#39;double&#39;의 변수 정의
</code></pre>
<p>이는 기본 타입을 간단하게 확장한 복합 타입 (함수, 포인터, 참조, 배열 등)에도 똑같이 적용됩니다.</p>
<pre><code class="language-cpp">void fcn(int) {}; // void(int) 타입의 함수 정의
int* ptr; // &#39;int에 대한 포인터&#39;라는 복합 타입 변수 정의
int&amp; ref { x }; // &#39;int에 대한 참조&#39;라는 복합 타입 변수 정의 (x로 초기화됨)
int arr[5]; // int[5] 타입인 5개의 정수를 가진 배열 정의 (배열은 나중에 다룰 예정입니다)
</code></pre>
<p>C++ 언어는 이미 이러한 타입들의 이름과 기호가 무엇을 의미하는지 알고 있기 때문에 코드가 문제없이 잘 작동합니다. 
즉, 우리가 따로 정의를 제공하거나 불러올 필요가 없습니다.</p>
<p>하지만 타입 별칭(Type alias, 10.7 레슨에서 소개함)을 생각해 봅시다. 타입 별칭은 기존 타입에 새로운 이름을 지어주는 기능입니다. 
프로그램에 새로운 이름을 추가하는 것이기 때문에, 사용하기 전에 반드시 먼저 정의해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

using Length = int; // &#39;Length&#39;라는 이름으로 타입 별칭 정의

int main(){
    Length x { 5 }; // 위에서 정의했기 때문에 여기서 &#39;Length&#39;를 사용할 수 있습니다
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>만약 <code>Length</code> 의 정의를 빼먹는다면, 컴파일러는 <code>Length</code> 가 무엇인지 알 수 없어서 해당 타입으로 변수를 만들려 할 때 에러를 발생시킵니다. <code>Length</code> 의 정의는 실제 객체를 만드는 것이 아닙니다. 단지 컴파일러에게 <code>Length</code> 가 무엇인지 알려주어 나중에 사용할 수 있게 해줄 뿐입니다.</p>
<hr>
<h3 id="사용자-정의--프로그램-정의-타입이란-무엇인가요">사용자 정의 / 프로그램 정의 타입이란 무엇인가요?</h3>
<p>이전 장(12.1 - 복합 데이터 타입 소개)에서 우리는 분수(Fraction)를 저장할 때 겪는 문제에 대해 이야기했습니다. 분수는 개념적으로 하나로 연결된 분자와 분모를 가지고 있죠. 해당 레슨에서는 분자와 분모를 두 개의 독립적인 정수로 따로 떼어 저장할 때 생기는 어려움을 다루었습니다.</p>
<p>만약 C++에 분수를 위한 타입이 기본적으로 내장되어 있었다면 완벽했겠지만, 아쉽게도 없습니다. C++에는 사람들이 필요로 할 만한 모든 타입을 미리 예상하고 포함해 두는 것이 불가능하기 때문에 수백 가지의 유용한 타입들이 기본적으로 제공되지 않습니다.</p>
<p>대신 C++는 다른 방식으로 이 문제를 해결합니다. 바로 우리가 <strong>프로그램에서 사용할 수 있는 완전히 새로운 맞춤형 타입을 직접 만들 수 있게</strong> 해주는 것입니다! 이러한 타입을 <strong>사용자 정의 타입(User-defined types)</strong> 이라고 부릅니다. 하지만 이 레슨의 뒷부분에서 설명하겠지만, 우리가 직접 작성하는 프로그램에서 사용하기 위해 만든 타입은 <strong>프로그램 정의 타입(Program-defined types)</strong> 이라는 용어를 사용하는 것이 더 좋습니다.</p>
<p>C++에는 프로그램 정의 타입을 만들 때 사용할 수 있는 두 가지 종류의 복합 타입이 있습니다.</p>
<ul>
<li>열거형 타입 (범위가 없는 열거형과 범위가 있는 열거형 포함)</li>
<li>클래스 타입 (구조체, 클래스, 공용체 포함)</li>
</ul>
<hr>
<h3 id="프로그램-정의-타입-정의하기">프로그램 정의 타입 정의하기</h3>
<p>타입 별칭과 마찬가지로, 프로그램 정의 타입도 사용하기 전에 반드시 정의하고 이름을 지어주어야 합니다. 
프로그램 정의 타입의 정의를 <strong>타입 정의(Type definition)</strong> 라고 부릅니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
프로그램 정의 타입은 사용하기 전에 반드시 이름과 정의가 있어야 합니다. 다른 복합 타입들은 이 두 가지가 필요하지 않습니다.
함수는 사용하기 전에 이름과 정의가 필요하지만 사용자 정의 타입으로 간주되지는 않습니다. 왜냐하면 이름과 정의가 주어지는 것은 함수 자체이지, 함수의 &#39;타입&#39;이 아니기 때문입니다. 우리가 직접 정의하는 함수는 &#39;사용자 정의 함수&#39;라고 부릅니다.</p>
</blockquote>
<p>아직 구조체(struct)가 무엇인지 제대로 배우지 않았지만, 다음은 사용자 맞춤형 <code>Fraction</code>(분수) 타입을 정의하고 그 타입을 사용해 객체를 생성(인스턴스화)하는 예제입니다.</p>
<pre><code class="language-cpp">// 컴파일러가 Fraction이 무엇인지 이해할 수 있도록 Fraction이라는 프로그램 정의 타입을 정의합니다
// (구조체가 무엇이고 어떻게 사용하는지는 이번 장에서 나중에 설명하겠습니다)
// 이것은 단지 Fraction 타입이 어떻게 생겼는지 정의할 뿐, 실제 객체를 생성하지는 않습니다
struct Fraction{
    int numerator {};
    int denominator {};
};

// 이제 Fraction 타입을 사용할 수 있습니다
int main(){
    Fraction f { 3, 4 }; // 이것은 실제로 f라는 이름의 Fraction 객체를 생성(인스턴스화)합니다

    return 0;
}
</code></pre>
<p>이 예제에서 우리는 <code>struct</code> 키워드를 사용하여 <code>Fraction</code> 이라는 새로운 프로그램 정의 타입을 전역 범위에 정의했습니다 (파일의 어느 곳에서든 사용할 수 있도록 말이죠). 이 과정은 메모리를 할당하지 않습니다. 단지 컴파일러에게 <code>Fraction</code> 이 어떻게 생겼는지 알려주어 나중에 <code>Fraction</code> 타입의 객체를 할당할 수 있게 해주는 것입니다.  그 후 <code>main()</code> 함수 안에서 우리는 <code>f</code> 라는 이름의 <code>Fraction</code> 타입 변수를 생성하고 초기화합니다.</p>
<p>프로그램 정의 타입의 정의는 반드시 세미콜론(<code>;</code>)으로 끝나야 합니다. 타입 정의 끝에 세미콜론을 빼먹는 것은 프로그래머들이 아주 흔하게 하는 실수입니다. 컴파일러가 타입 정의 줄이 아닌 그 <strong>다음</strong> 줄에서 에러를 띄울 수 있기 때문에 버그를 찾기 매우 어려울 수 있습니다.</p>
<blockquote>
<p><strong>경고</strong>
타입 정의 끝에 세미콜론을 붙이는 것을 절대 잊지 마세요.</p>
</blockquote>
<p>다음 레슨(13.2 - 범위가 없는 열거형)에서 프로그램 정의 타입을 정의하고 사용하는 더 많은 예제를 보여드릴 예정이며, 구조체에 대해서는 13.7 레슨부터 본격적으로 다룹니다.</p>
<hr>
<h3 id="프로그램-정의-타입-이름-짓기">프로그램 정의 타입 이름 짓기</h3>
<p>관례적으로 프로그램 정의 타입의 이름은 대문자로 시작하며, 뒤에 따로 접미사를 붙이지 않습니다 (예: <code>fraction</code>, <code>fraction_t</code>, <code>Fraction_t</code> 가 아니라 <code>Fraction</code> 으로 작성).</p>
<blockquote>
<p><strong>모범 사례</strong>
프로그램 정의 타입의 이름은 대문자로 시작하게 짓고, 접미사를 사용하지 마세요.</p>
</blockquote>
<p>초보 프로그래머들은 타입 이름과 변수 이름이 비슷해서 다음과 같은 변수 정의를 헷갈려하곤 합니다.</p>
<pre><code class="language-cpp">Fraction fraction {}; // Fraction 타입의 fraction이라는 이름의 변수를 생성합니다
</code></pre>
<p>이는 다른 변수를 정의하는 것과 전혀 다르지 않습니다. 타입(<code>Fraction</code>)이 먼저 오고 (대문자로 시작하기 때문에 프로그램 정의 타입이라는 것을 알 수 있죠), 그다음 변수 이름(<code>fraction</code>)이 오며, 마지막으로 선택적인 초기화 코드가 옵니다. C++는 대소문자를 구분하기 때문에 이름이 충돌할 일은 없습니다!</p>
<hr>
<h3 id="여러-파일로-이루어진-프로그램에서-프로그램-정의-타입-사용하기">여러 파일로 이루어진 프로그램에서 프로그램 정의 타입 사용하기</h3>
<p>프로그램 정의 타입을 사용하는 모든 코드 파일은 해당 타입을 사용하기 전에 전체 타입 정의를 반드시 확인해야 합니다.
전방 선언만으로는 충분하지 않습니다. 이는 컴파일러가 해당 타입의 객체를 위해 메모리를 얼마나 할당해야 하는지 알아야 하기 때문입니다.</p>
<p>타입 정의를 필요로 하는 여러 코드 파일에 전달하기 위해, 프로그램 정의 타입은 보통 헤더 파일에 정의됩니다.
그리고 그 타입 정의가 필요한 모든 코드 파일에서 <code>#include</code> 를 통해 불러옵니다. 이러한 헤더 파일의 이름은 보통 프로그램 정의 타입의 이름과 똑같이 짓습니다 (예를 들어, <code>Fraction</code> 이라는 프로그램 정의 타입은 <code>Fraction.h</code> 에 정의합니다).</p>
<blockquote>
<p><strong>모범 사례</strong>
단 하나의 코드 파일에서만 사용하는 프로그램 정의 타입은 해당 코드 파일 안에서 처음 사용하는 곳과 최대한 가까운 위치에 정의하세요.
여러 코드 파일에서 사용하는 프로그램 정의 타입은 타입 이름과 동일한 이름의 헤더 파일에 정의한 후, 필요한 코드 파일마다 <code>#include</code> 로 불러와야 합니다.</p>
</blockquote>
<p><code>Fraction</code> 타입을 헤더 파일(<code>Fraction.h</code>)로 옮겨서 여러 코드 파일에 포함시킬 수 있게 만들면 다음과 같은 모습이 됩니다.</p>
<p><strong>Fraction.h:</strong></p>
<pre><code class="language-cpp">#ifndef FRACTION_H
#define FRACTION_H

// Fraction이라는 새로운 타입을 정의합니다
// 이것은 단지 Fraction이 어떻게 생겼는지 정의할 뿐, 실제 객체를 생성하지는 않습니다
// 주의: 이것은 전방 선언이 아니라 완전한 정의입니다
struct Fraction{
    int numerator {};
    int denominator {};
};

#endif
</code></pre>
<p><strong>Fraction.cpp:</strong></p>
<pre><code class="language-cpp">#include &quot;Fraction.h&quot; // 이 코드 파일에 우리의 Fraction 정의를 포함시킵니다

// 이제 Fraction 타입을 사용할 수 있습니다
int main(){
    Fraction f{ 3, 4 }; // 이것은 실제로 f라는 이름의 Fraction 객체를 생성합니다

    return 0;
}
</code></pre>
<hr>
<h3 id="타입-정의는-단일-정의-규칙odr에서-부분적으로-예외입니다">타입 정의는 단일 정의 규칙(ODR)에서 부분적으로 예외입니다</h3>
<p>2.7 레슨(전방 선언과 정의)에서 우리는 단일 정의 규칙(ODR)에 따라 각 함수와 전역 변수가 프로그램 내에서 단 하나의 정의만 가져야 한다고 배웠습니다. 정의가 포함되지 않은 파일에서 해당 함수나 전역 변수를 사용하려면 전방 선언이 필요합니다 (우리는 보통 이를 헤더 파일을 통해 전달합니다). 함수나 <code>constexpr</code> 이 아닌 변수의 경우, 선언만으로도 컴파일러를 만족시킬 수 있고 나중에 링커(Linker)가 모든 것을 연결해 주기 때문에 이 방식이 잘 작동합니다.</p>
<p>하지만 타입에 대해 이와 비슷한 방식으로 전방 선언을 사용하는 것은 작동하지 않습니다. 컴파일러가 특정 타입을 사용하려면 보통 전체 정의를 직접 봐야 하기 때문입니다. 따라서 우리는 타입이 필요한 모든 코드 파일에 전체 타입 정의를 전달할 수 있어야 합니다.</p>
<p>이를 가능하게 하기 위해, 타입은 단일 정의 규칙(ODR)에서 부분적으로 예외 처리를 받습니다. 즉, 특정 타입은 여러 코드 파일에 걸쳐 정의될 수 있습니다.</p>
<p>여러분은 아마 눈치채지 못했겠지만 이미 이 기능을 사용해 보셨을 겁니다. 만약 여러분의 프로그램에 <code>#include &lt;iostream&gt;</code> 을 포함하는 두 개의 코드 파일이 있다면, 두 파일 모두에 입출력 관련 타입 정의들을 모두 가져오고 있는 셈이니까요.</p>
<p>여기서 알아두어야 할 두 가지 주의사항이 있습니다. 첫째, 하나의 코드 파일 안에는 여전히 하나의 타입 정의만 존재해야 합니다 (보통 헤더 가드(Header guards)가 이를 막아주기 때문에 큰 문제는 되지 않습니다). 둘째, 특정 타입에 대한 모든 타입 정의는 완전히 똑같아야 합니다. 그렇지 않으면 정의되지 않은 동작(Undefined behavior)이 발생합니다.</p>
<hr>
<h3 id="용어-정리-사용자-정의-타입-vs-프로그램-정의-타입">용어 정리: 사용자 정의 타입 vs 프로그램 정의 타입</h3>
<p><strong>사용자 정의 타입(User-defined type)</strong> 이라는 용어는 일상적인 대화에서 종종 등장하며, C++ 언어 표준에서도 언급됩니다 (하지만 명확히 정의되어 있지는 않습니다). 일상적인 대화에서 이 용어는 보통 &#39;우리 자신의 프로그램 내에서 직접 정의한 타입&#39;을 의미합니다 (위의 <code>Fraction</code> 타입 예제처럼요).</p>
<p>C++ 언어 표준은 <strong>사용자 정의 타입</strong> 이라는 용어를 조금 다르게 사용합니다. 언어 표준에서 &#39;사용자 정의 타입&#39;이란 여러분, 표준 라이브러리, 또는 구현체(예: 컴파일러가 언어 확장을 지원하기 위해 정의한 타입)가 정의한 모든 클래스 타입이나 열거형 타입을 뜻합니다. 직관적이지 않을 수 있지만, 이 기준에 따르면 <code>std::string</code> (표준 라이브러리에 정의된 클래스 타입)도 사용자 정의 타입으로 간주됩니다!</p>
<p>더 확실한 구분을 위해, C++20 언어 표준에서는 <strong>프로그램 정의 타입(Program-defined type)</strong> 이라는 용어를 새롭게 정의했습니다. 이는 표준 라이브러리, 구현체, 또는 핵심 언어의 일부로 정의되지 않은 클래스 타입과 열거형 타입을 의미합니다. 다시 말해, &#39;프로그램 정의 타입&#39;은 오직 우리 (또는 서드파티 라이브러리)가 직접 정의한 클래스 타입과 열거형 타입만을 포함합니다.</p>
<p>결과적으로, 우리가 직접 작성하는 프로그램에서 사용하기 위해 정의하는 클래스 타입과 열거형 타입만을 이야기할 때는 더 정확한 의미를 가진 <strong>프로그램 정의(Program-defined)</strong> 라는 용어를 사용하는 것이 좋습니다.</p>
<table>
<thead>
<tr>
<th>타입 (Type)</th>
<th>의미 (Meaning)</th>
<th>예시 (Examples)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>기본 (Fundamental)</strong></td>
<td>C++ 핵심 언어에 내장된 기본 타입</td>
<td><code>int</code>, <code>std::nullptr_t</code></td>
</tr>
<tr>
<td><strong>복합 (Compound)</strong></td>
<td>다른 타입들을 기반으로 정의된 타입</td>
<td><code>int&amp;</code>, <code>double*</code>, <code>std::string</code>, <code>Fraction</code></td>
</tr>
<tr>
<td><strong>사용자 정의 (User-defined)</strong></td>
<td>클래스 타입 또는 열거형 타입</td>
<td></td>
</tr>
</tbody></table>
<hr>
<h2 id="132--범위-없는-열거형unscoped-enumerations">13.2 — 범위 없는 열거형(Unscoped enumerations)</h2>
<p>C++에는 유용한 기본 자료형과 복합 자료형이 많이 있습니다. 
하지만 우리가 프로그래밍을 하다 보면 이런 기본적인 자료형만으로는 충분하지 않을 때가 있습니다.</p>
<p>예를 들어, 사과가 빨간색인지, 노란색인지, 초록색인지 기억해야 하거나, 정해진 목록에서 셔츠의 색상을 저장하는 프로그램을 만든다고 가정해 볼까요? 기본 자료형만 사용할 수 있다면 어떻게 코드를 짜야 할까요?</p>
<p>아마도 &#39;0은 빨간색, 1은 초록색, 2는 파란색&#39;처럼 암묵적인 규칙을 만들어 정수값으로 색상을 저장하게 될 것입니다.</p>
<pre><code class="language-cpp">int main(){
    int appleColor{ 0 }; // 내 사과는 빨간색입니다
    int shirtColor{ 1 }; // 내 셔츠는 초록색입니다

    return 0;}
</code></pre>
<p>하지만 이 방식은 전혀 직관적이지 않습니다. 
우리는 이미 코드에 덩그러니 쓰인 <strong>매직 넘버</strong> 가 왜 나쁜지 배운 적이 있죠. 
기호 상수를 사용하면 이 매직 넘버를 없앨 수 있습니다.</p>
<pre><code class="language-cpp">constexpr int red{ 0 };
constexpr int green{ 1 };
constexpr int blue{ 2 };

int main(){
    int appleColor{ red };
    int shirtColor{ green };

    return 0;}
</code></pre>
<p>코드를 읽기는 조금 편해졌지만, 프로그래머는 여전히 <code>int</code> 타입인 <code>appleColor</code>와 <code>shirtColor</code>가 따로 정의된 색상 상수값 중 하나를 가져야 한다는 사실을 직접 유추해 내야만 합니다.</p>
<p>타입 별칭을 사용하면 코드를 조금 더 명확하게 만들 수 있습니다.</p>
<pre><code class="language-cpp">using Color = int; // Color라는 이름의 타입 별칭을 정의합니다

// 다음 색상 값들은 Color 타입을 위해 사용되어야 합니다
constexpr Color red{ 0 };
constexpr Color green{ 1 };
constexpr Color blue{ 2 };

int main(){
    Color appleColor{ red };
    Color shirtColor{ green };

    return 0;}
</code></pre>
<p>점점 나아지고 있네요! 코드를 읽는 사람은 이 색상 상수들이 <code>Color</code> 타입의 변수와 짝을 이뤄 사용된다는 것을 알 수 있습니다.</p>
<p>하지만 <code>Color</code>는 결국 <code>int</code>의 별칭일 뿐이므로, 여전히 이 상수들이 올바르게 쓰이도록 강제할 방법이 없다는 문제가 남습니다. 
여전히 다음과 같은 엉뚱한 코드를 작성할 수 있죠.</p>
<pre><code class="language-cpp">Color eyeColor{ 8 }; // 문법적으로는 유효하지만, 의미상으로는 아무 뜻도 없습니다
</code></pre>
<p>게다가 디버거로 이 변수들을 확인해 보면, 기호의 의미인 &#39;red&#39;가 아니라 정수값인 &#39;0&#39;만 보이기 때문에 우리 프로그램이 올바르게 돌아가고 있는지 확인하기가 어렵습니다. 다행히도 C++에는 훨씬 더 좋은 방법이 있습니다.</p>
<p><code>bool</code> 타입을 한 번 떠올려 보세요. 
<code>bool</code>이 특별한 이유는 오직 <code>true</code>와 <code>false</code>라는 두 가지 값만 가질 수 있다는 점입니다. 
컴파일러는 <code>bool</code>을 다른 타입과 명확히 구분할 수 있습니다.</p>
<p>만약 우리가 <strong>우리만의 맞춤형 타입</strong> 을 만들고, 그 타입이 가질 수 있는 이름표(값)들을 직접 정의할 수 있다면, 앞서 말한 문제를 우아하게 해결할 완벽한 도구가 될 것입니다.</p>
<hr>
<h3 id="열거형-enumerations">열거형 (Enumerations)</h3>
<p><strong>열거형(Enumeration, 또는 Enum)</strong> 은 가질 수 있는 값이 우리가 이름 붙인 기호 상수(이를 <strong>열거자(Enumerator)</strong> 라고 부릅니다)들로만 제한되는 복합 자료형입니다.</p>
<p>C++는 두 가지 종류의 열거형을 지원합니다. 
바로 <strong>범위 없는 열거형(Unscoped enumerations)</strong> 과 <strong>범위 있는 열거형(Scoped enumerations)</strong>입니다. 
이번 장에서는 범위 없는 열거형에 대해 알아보겠습니다.</p>
<p>열거형은 프로그래머가 직접 정의하는 타입이므로, 사용하기 전에 전체 정의가 완료되어야 합니다. (전방 선언만으로는 부족합니다.)</p>
<hr>
<h3 id="범위-없는-열거형-unscoped-enumerations">범위 없는 열거형 (Unscoped enumerations)</h3>
<p>범위 없는 열거형은 <code>enum</code> 키워드를 사용해 정의합니다. 
백문이 불여일견이니, 색상 값을 담을 수 있는 열거형을 만들어보며 이해해 봅시다.</p>
<pre><code class="language-cpp">// Color라는 이름의 새로운 범위 없는 열거형을 정의합니다
enum Color{
    // 여기에 열거자(enumerators)들이 있습니다
    // 이 기호 상수들은 이 타입이 가질 수 있는 모든 가능한 값들을 정의합니다
    // 각 열거자는 세미콜론이 아니라 쉼표로 구분합니다
    red,
    green,
    blue, // 마지막 쉼표는 선택 사항이지만 권장됩니다
}; // 열거형 정의는 반드시 세미콜론으로 끝나야 합니다

int main(){
    // Color 열거형 타입의 변수 몇 개를 정의합니다
    Color apple { red };   // 내 사과는 빨간색입니다
    Color shirt { green }; // 내 셔츠는 초록색입니다
    Color cup { blue };    // 내 컵은 파란색입니다

    Color socks { white }; // 에러: white는 Color의 열거자가 아닙니다
    Color hat { 2 };       // 에러: 2는 Color의 열거자가 아닙니다

    return 0;}
</code></pre>
<p><code>enum</code> 키워드를 사용해 컴파일러에게 <code>Color</code>라는 범위 없는 열거형을 정의하겠다고 알려주는 것으로 시작합니다.</p>
<p>중괄호 안에는 <code>Color</code> 타입의 <strong>열거자(Enumerator)</strong> 인 <code>red</code>, <code>green</code>, <code>blue</code>를 정의합니다. 
이 열거자들은 <code>Color</code> 타입이 가질 수 있는 구체적인 값들입니다. 
<strong>주의할 점</strong> 은 각 열거자를 세미콜론(;)이 아닌 쉼표(,)로 구분해야 한다는 것입니다.</p>
<p><code>main()</code> 함수 안에서 <code>Color</code> 타입의 변수 세 개를 만들었습니다. 
열거형 변수를 초기화할 때는 반드시 해당 열거형에 정의된 열거자 중 하나만 사용해야 합니다. 
변수 <code>socks</code>와 <code>hat</code>은 초기값인 <code>white</code>와 <code>2</code>가 <code>Color</code>의 열거자가 아니기 때문에 컴파일 에러가 발생합니다.</p>
<p>참고로 열거자들은 암묵적으로 <code>constexpr</code>(상수 표현식)로 취급됩니다.</p>
<hr>
<h3 id="용어-다시-보기">용어 다시 보기</h3>
<ul>
<li><strong>열거형(Enumeration, Enumerated type)</strong> 은 프로그래머가 만든 타입 그 자체를 말합니다. (예: <code>Color</code>)</li>
<li><strong>열거자(Enumerator)</strong> 는 그 열거형에 속한 특정한 이름의 값을 말합니다. (예: <code>red</code>)</li>
</ul>
<hr>
<h3 id="이름-짓기-규칙">이름 짓기 규칙</h3>
<p>관례적으로 프로그래머가 정의한 모든 타입이 그렇듯, 열거형의 이름은 대문자로 시작합니다.
열거자의 이름에는 공통된 명명 규칙이 딱히 없습니다. 소문자로 시작하거나, 대문자로 시작하거나, 전체를 대문자로 쓰거나(RED), 접두사를 붙이기도 합니다.</p>
<p>하지만 <strong>모던 C++ 가이드라인</strong> 에서는 매크로와 충돌할 수 있는 전체 대문자(ALL CAPS) 표기법은 피하라고 권장합니다.</p>
<blockquote>
<p><strong>권장 사항</strong> 
열거형의 이름은 대문자로 시작하고, 열거자의 이름은 소문자로 시작하도록 지어주세요.</p>
</blockquote>
<hr>
<h3 id="열거형은-서로-다른-독립적인-타입입니다">열거형은 서로 다른 독립적인 타입입니다</h3>
<p>여러분이 만든 각 열거형은 컴파일러가 다른 타입과 명확히 구분할 수 있는 <strong>독립적인 타입(Distinct type)</strong> 으로 취급됩니다. 
따라서 한 열거형의 열거자를 다른 열거형 객체에 섞어서 쓸 수 없습니다.</p>
<pre><code class="language-cpp">enum Pet{
    cat,
    dog,
    pig,
    whale,
};

enum Color{
    black,
    red,
    blue,
};

int main(){
    Pet myPet { black }; // 컴파일 에러: black은 Pet의 열거자가 아닙니다
    Color shirt { pig }; // 컴파일 에러: pig는 Color의 열거자가 아닙니다

    return 0;}
</code></pre>
<hr>
<h3 id="열거형의-활용">열거형의 활용</h3>
<p>열거자는 의미를 설명해 주기 때문에 코드의 가독성을 높여줍니다. 
개수가 적고 서로 연관된 상수들의 집합이 있을 때 열거형을 쓰면 좋습니다.
흔히 요일, 방위, 카드 무늬 등을 정의할 때 사용됩니다.</p>
<pre><code class="language-cpp">enum DaysOfWeek{
    sunday,
    monday,
    tuesday, // ... 등등
};

enum CardinalDirections{
    north,
    east,
    south,
    west,
};
</code></pre>
<p>가끔 함수가 제대로 실행되었는지 에러가 났는지 알려주는 <strong>상태 코드(Status code)</strong> 를 반환해야 할 때가 있죠. 
예전에는 의미를 알 수 없는 음수를 썼지만,</p>
<pre><code class="language-cpp">int readFileContents(){
    if (!openFile())
        return -1;
    if (!readFile())
        return -2;
    // ...
    return 0; // 성공
}
</code></pre>
<p>이제는 열거형을 써서 훨씬 명확하게 만들 수 있습니다.</p>
<pre><code class="language-cpp">enum FileReadResult{
    readResultSuccess,
    readResultErrorFileOpen,
    readResultErrorFileRead,
    readResultErrorFileParse,
};

FileReadResult readFileContents(){
    if (!openFile())
        return readResultErrorFileOpen;
    // ...
    return readResultSuccess;}
</code></pre>
<p>이렇게 하면 함수를 호출하는 쪽에서도 결과값을 이해하기 쉬운 열거자와 비교할 수 있습니다.</p>
<pre><code class="language-cpp">if (readFileContents() == readResultSuccess){
    // 무언가를 수행합니다
}else{
    // 에러 메시지를 출력합니다
}
</code></pre>
<p>게임에서 아이템이나 몬스터 종류를 구분할 때, 또는 사용자가 여러 옵션 중 하나를 선택해야 할 때도 아주 유용하게 쓰입니다.</p>
<pre><code class="language-cpp">enum SortOrder{
    alphabetical,
    alphabeticalReverse,
    numerical,
};

void sortData(SortOrder order){
    switch (order)
    {
        case alphabetical:
            // 알파벳 정방향 순서로 데이터를 정렬합니다
            break;
        case alphabeticalReverse:
            // 알파벳 역방향 순서로 데이터를 정렬합니다
            break;
        case numerical:
            // 숫자 순서로 데이터를 정렬합니다
            break;
    }
}
</code></pre>
<hr>
<h3 id="범위-없는-열거형의-스코프scope-문제">범위 없는 열거형의 스코프(Scope) 문제</h3>
<p>이 열거형들을 &#39;범위 없는(Unscoped)&#39;이라고 부르는 데는 이유가 있습니다. 
열거자의 이름들이 열거형 자체가 정의된 공간(전역 스코프 등)과 똑같은 곳에 흩뿌려지기 때문입니다.</p>
<pre><code class="language-cpp">enum Color // 이 열거형은 전역 네임스페이스에 정의되었습니다
{
    red, // 따라서 red도 전역 네임스페이스에 들어갑니다
    green,
    blue,
};
</code></pre>
<p>이러면 전역 스코프가 오염되고 <strong>이름 충돌(Naming collisions)</strong> 이 발생할 확률이 크게 높아집니다. 
그래서 같은 스코프 안에서는 여러 열거형에서 똑같은 이름의 열거자를 쓸 수가 없습니다.</p>
<pre><code class="language-cpp">enum Color{
    red,
    green,
    blue, // blue는 전역 네임스페이스에 들어갑니다
};

enum Feeling{
    happy,
    tired,
    blue, // 에러: 위에서 정의한 blue와 이름 충돌이 발생합니다
};
</code></pre>
<hr>
<h3 id="열거자-이름-충돌-방지하기">열거자 이름 충돌 방지하기</h3>
<p>이름 충돌을 막는 좋은 방법 중 하나는 열거형을 <strong>네임스페이스(Namespace)</strong> 안에 넣는 것입니다.</p>
<pre><code class="language-cpp">namespace Color{
    // Color, red, blue, green이라는 이름들은 네임스페이스 Color 안에 정의됩니다
    enum Color
    {
        red,
        green,
        blue,
    };
}

namespace Feeling{
    enum Feeling
    {
        happy,
        tired,
        blue, // Feeling::blue는 Color::blue와 충돌하지 않습니다
    };
}

int main(){
    Color::Color paint{ Color::blue };
    Feeling::Feeling me{ Feeling::blue };

    return 0;}
</code></pre>
<blockquote>
<p><strong>권장 사항</strong> 
열거형이 전역 네임스페이스를 어지럽히지 않도록, 네임스페이스나 클래스 같은 명명된 스코프 안에 넣는 것을 항상 우선순위로 두세요.</p>
</blockquote>
<hr>
<h3 id="열거자-비교하기">열거자 비교하기</h3>
<p>동등 연산자(<code>==</code> 및 <code>!=</code>)를 사용하여 열거형 변수가 어떤 값을 가지고 있는지 쉽게 테스트할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

enum Color{
    red,
    green,
    blue,
};

int main(){
    Color shirt{ blue };

    if (shirt == blue) // 셔츠가 파란색이라면
        std::cout &lt;&lt; &quot;당신의 셔츠는 파란색입니다!&quot;;
    else
        std::cout &lt;&lt; &quot;당신의 셔츠는 파란색이 아닙니다!&quot;;

    return 0;}
</code></pre>
<hr>
<h2 id="133--범위-없는-열거형의-정수-변환">13.3 — 범위 없는 열거형의 정수 변환</h2>
<p>이전 레슨(13.2 -- 범위 없는 열거형)에서는 열거자(enumerator)가 기호 상수(symbolic constant)라고 배웠습니다. 
그때 말씀드리지 않은 사실이 하나 있는데, 바로 이 열거자들이 내부적으로는 <strong>정수형(integral type)</strong> 값을 가진다는 것입니다.</p>
<p>이는 <code>char</code> 자료형(4.11 -- 문자형)과 비슷합니다. 다음 코드를 볼까요?</p>
<pre><code class="language-cpp">char ch { &#39;A&#39; };
</code></pre>
<p><code>char</code> 는 실제로는 1바이트 크기의 정수 값입니다. 문자 <code>&#39;A&#39;</code> 는 내부적으로 정수 값(이 경우 <code>65</code>)으로 변환되어 저장되죠.</p>
<p>열거형을 정의할 때, 각각의 열거자는 목록에 있는 위치에 따라 자동으로 정수 값과 연결됩니다.
기본적으로 첫 번째 열거자는 정수 값 <code>0</code> 을 가지고, 그 다음 열거자들은 이전 값보다 1씩 큰 값을 가지게 됩니다.</p>
<pre><code class="language-cpp">enum Color
{
    black,   // 0
    red,     // 1
    blue,    // 2
    green,   // 3
    white,   // 4
    cyan,    // 5
    yellow,  // 6
    magenta, // 7
};

int main()
{
    Color shirt{ blue }; // shirt는 실제로 정수 값 2를 저장합니다

    return 0;
}
</code></pre>
<p>열거자의 값을 우리가 직접 지정해 줄 수도 있습니다. 
이 정수 값은 양수나 음수 모두 가능하며, 다른 열거자와 같은 값을 가질 수도 있습니다. 
값을 직접 지정하지 않은 열거자는 바로 이전 열거자의 값보다 1 큰 값을 가집니다.</p>
<pre><code class="language-cpp">enum Animal
{
    cat = -3,    // 음수 값도 가능합니다
    dog,         // -2
    pig,         // -1
    horse = 5,
    giraffe = 5, // horse와 같은 값을 가집니다
    chicken,     // 6
};
</code></pre>
<p>위 코드에서 <code>horse</code> 와 <code>giraffe</code> 는 같은 값을 가집니다. 이렇게 되면 두 열거자는 구분이 안 되고 사실상 똑같이 취급됩니다. 
C++ 에서는 이를 허용하지만, 같은 열거형 안에서 두 열거자에 같은 값을 할당하는 것은 일반적으로 피하는 것이 좋습니다.</p>
<p>대부분의 경우 열거자의 기본값만으로도 충분하므로, 특별한 이유가 없다면 값을 직접 지정하지 마세요.</p>
<blockquote>
<p><strong>모범 사례</strong> 
특별한 이유가 없다면 열거자에 명시적인 값을 할당하지 마세요.</p>
</blockquote>
<hr>
<h3 id="열거형의-값-초기화-value-initializing-an-enumeration">열거형의 값 초기화 (Value-initializing an enumeration)</h3>
<p>값 초기화(value-initialization)를 사용하여 열거형을 0으로 초기화하면, 해당 값(<code>0</code>)을 가진 열거자가 목록에 없더라도 열거형은 <code>0</code> 값을 가지게 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

enum Animal
{
    cat = -3,    // -3
    dog,         // -2
    pig,         // -1
    // 참고: 이 목록에는 값이 0인 열거자가 없습니다
    horse = 5,   // 5
    giraffe = 5, // 5
    chicken,     // 6
};

int main()
{
    Animal a {}; // 값 초기화를 통해 a를 0으로 초기화합니다
    std::cout &lt;&lt; a; // 0을 출력합니다

    return 0;
}
</code></pre>
<p>여기에는 두 가지 의미론적 결과가 따릅니다.</p>
<ol>
<li><strong>값이 0인 열거자가 존재한다면</strong>, 값 초기화를 할 때 그 열거자의 의미가 기본값이 됩니다. 
예를 들어 이전의 <code>Color</code> 열거형에서 값 초기화된 색상은 기본적으로 <code>black</code> 이 됩니다. 
이러한 이유로, 값이 0인 열거자를 해당 열거형의 <strong>가장 적절한 기본 상태</strong> 를 나타내도록 설정하는 것이 좋습니다.
따라서 다음과 같은 코드는 문제를 일으킬 가능성이 높습니다.<pre><code class="language-cpp">enum UniverseResult
{
 destroyUniverse, // 기본값 (0)
 saveUniverse
};
</code></pre>
</li>
</ol>
<pre><code>
2. **값이 0인 열거자가 없다면**, 값 초기화를 했을 때 의미상 유효하지 않은 상태가 만들어지기 쉽습니다. 
이런 경우에는 값이 0인 &quot;유효하지 않음(invalid)&quot; 또는 &quot;알 수 없음(unknown)&quot;을 뜻하는 열거자를 추가하는 것을 추천합니다. 
이렇게 하면 해당 상태의 의미를 문서화할 수 있고, 코드에서도 명시적으로 처리할 수 있습니다.
```cpp
enum Winner
{
    winnerUnknown, // 기본값 (0)
    player1,
    player2,
};

// 코드의 다른 부분에서
if (w == winnerUnknown) // 상황에 맞게 처리합니다
</code></pre><blockquote>
<p><strong>모범 사례</strong> 
<code>0</code> 값을 가지는 열거자를 해당 열거형의 가장 적절한 기본 상태로 만드세요. 마땅한 기본 상태가 없다면, <code>0</code> 값을 가지는 &quot;유효하지 않음(invalid)&quot;이나 &quot;알 수 없음(unknown)&quot; 열거자를 추가하는 것을 고려해 보세요. 문서화도 되고 적절한 예외 처리도 가능해집니다.</p>
</blockquote>
<hr>
<h3 id="범위-없는-열거형은-정수-값으로-암시적-변환됩니다">범위 없는 열거형은 정수 값으로 암시적 변환됩니다</h3>
<p>열거형은 정수 값을 저장하지만, 정수 자료형으로 취급되지는 않습니다(복합 자료형입니다). 
하지만 범위 없는 열거형은 정수 값으로 <strong>암시적 변환(implicit conversion)</strong> 이 일어납니다. 
열거자는 컴파일 타임 상수이기 때문에 이는 <code>constexpr</code> 변환에 해당합니다(이에 대해서는 10.4 레슨에서 다룹니다).</p>
<p>다음 프로그램을 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

enum Color
{
    black, // 0 할당됨
    red, // 1 할당됨
    blue, // 2 할당됨
    green, // 3 할당됨
    white, // 4 할당됨
    cyan, // 5 할당됨
    yellow, // 6 할당됨
    magenta, // 7 할당됨
};

int main()
{
    Color shirt{ blue };

    std::cout &lt;&lt; &quot;Your shirt is &quot; &lt;&lt; shirt &lt;&lt; &#39;\n&#39;; // 이 코드는 무엇을 할까요?

    return 0;
}
</code></pre>
<p>열거형은 정수 값을 가지고 있기 때문에, 여러분이 예상하셨듯이 다음과 같이 출력됩니다.
<code>Your shirt is 2</code></p>
<p>함수 호출이나 연산자에 열거형이 사용될 때, 컴파일러는 먼저 해당 열거형과 일치하는 함수나 연산자를 찾으려 시도합니다. 
예를 들어 컴파일러가 <code>std::cout &lt;&lt; shirt</code> 를 컴파일할 때, 먼저 <code>operator&lt;&lt;</code> 가 <code>Color</code> 타입의 객체를 출력할 줄 아는지 확인합니다. 
하지만 해당 연산자는 그 방법을 모릅니다.</p>
<p>컴파일러는 일치하는 항목을 찾지 못했기 때문에, 이번에는 <code>operator&lt;&lt;</code> 가 이 범위 없는 열거형이 변환될 수 있는 <strong>정수 타입</strong> 의 객체를 출력할 줄 아는지 확인합니다. 이것은 가능하기 때문에 <code>shirt</code> 안의 값은 정수 값으로 변환되어 <code>2</code> 라는 정수로 출력됩니다.</p>
<ul>
<li><strong>관련 내용(Related content)</strong> * 13.4 레슨에서는 열거형을 문자열로 변환하는 방법을 다룹니다.</li>
<li>13.5 레슨에서는 <code>std::cout</code> 에게 열거자를 직접 출력하는 방법을 가르쳐주는 오버로딩에 대해 배웁니다.</li>
</ul>
<hr>
<h3 id="열거형의-크기와-기본-자료형-underlying-type">열거형의 크기와 기본 자료형 (Underlying type)</h3>
<p>열거자들은 정수형 값을 가집니다. 그렇다면 구체적으로 어떤 정수형일까요? 
열거자의 값을 표현하는 데 사용되는 특정 정수형을 열거형의 <strong>기본 자료형(underlying type 또는 base)</strong> 이라고 부릅니다.</p>
<p>범위 없는 열거형에 대해 C++ 표준은 구체적으로 어떤 정수형을 기본 자료형으로 써야 하는지 명시하지 않았으므로, 이는 컴파일러 구현에 따라 달라집니다. 대부분의 컴파일러는 <code>int</code> 를 기본 자료형으로 사용합니다(즉, 범위 없는 열거형의 크기는 <code>int</code> 와 동일합니다). </p>
<p>단, 열거자 값을 저장하는 데 더 큰 자료형이 필요한 경우는 예외입니다. 
하지만 모든 컴파일러나 플랫폼에서 항상 이럴 것이라고 가정해서는 안 됩니다.</p>
<p>열거형의 기본 자료형을 명시적으로 지정할 수도 있습니다. 단, 기본 자료형은 반드시 정수형이어야 합니다. 
예를 들어, 네트워크로 데이터를 전송하는 등 데이터 크기에 민감한 상황에서 작업한다면 열거형에 더 작은 크기의 자료형을 지정하고 싶을 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;cstdint&gt;  // std::int8_t 사용을 위해
#include &lt;iostream&gt;

// 8비트 정수를 열거형의 기본 자료형으로 사용합니다
enum Color : std::int8_t
{
    black,
    red,
    blue,
};

int main()
{
    Color c{ black };
    std::cout &lt;&lt; sizeof(c) &lt;&lt; &#39;\n&#39;; // 1(바이트)을 출력합니다

    return 0;
}
</code></pre>
<blockquote>
<p><strong>모범 사례</strong> 
꼭 필요할 때만 열거형의 기본 자료형을 명시적으로 지정하세요.</p>
</blockquote>
<blockquote>
<p><strong>주의</strong> 
<code>std::int8_t</code> 와 <code>std::uint8_t</code> 는 일반적으로 문자형(<code>char</code>)의 타입 별칭이기 때문에, 이들을 열거형의 기본 자료형으로 사용하면 열거자가 정수 값이 아닌 문자 값으로 출력될 가능성이 높습니다.</p>
</blockquote>
<hr>
<h3 id="정수를-범위-없는-열거자로-변환하기">정수를 범위 없는 열거자로 변환하기</h3>
<p>컴파일러는 범위 없는 열거형을 정수로 암시적 변환해 주지만, <strong>정수를 범위 없는 열거형으로 암시적 변환해 주지는 않습니다.</strong> 
따라서 다음 코드는 컴파일 에러를 발생시킵니다.</p>
<pre><code class="language-cpp">enum Pet // 기본 자료형 지정 안 됨
{
    cat, // 0 할당됨
    dog, // 1 할당됨
    pig, // 2 할당됨
    whale, // 3 할당됨
};

int main()
{
    Pet pet { 2 }; // 컴파일 에러: 정수 값 2는 Pet으로 암시적 변환되지 않습니다
    pet = 3;       // 컴파일 에러: 정수 값 3은 Pet으로 암시적 변환되지 않습니다

    return 0;
}
</code></pre>
<p>이 문제를 해결하는 방법은 두 가지가 있습니다.</p>
<p>첫 번째 방법은 <code>static_cast</code> 를 사용하여 정수를 범위 없는 열거자로 <strong>명시적 변환(explicit conversion)</strong> 하는 것입니다.</p>
<pre><code class="language-cpp">enum Pet // 기본 자료형 지정 안 됨
{
    cat, // 0 할당됨
    dog, // 1 할당됨
    pig, // 2 할당됨
    whale, // 3 할당됨
};

int main()
{
    Pet pet { static_cast&lt;Pet&gt;(2) }; // 정수 2를 Pet으로 변환합니다
    pet = static_cast&lt;Pet&gt;(3);       // 우리의 돼지가 고래로 진화했습니다!

    return 0;
}
</code></pre>
<p>대상 열거형의 열거자가 실제로 가지고 있는 정수 값을 <code>static_cast</code> 로 변환하는 것은 안전합니다. 
우리의 <code>Pet</code> 열거형은 <code>0</code>, <code>1</code>, <code>2</code>, <code>3</code> 값을 가진 열거자들을 가지고 있으므로, 정수 값 <code>0</code>, <code>1</code>, <code>2</code>, <code>3</code> 을 <code>Pet</code> 으로 변환하는 것은 유효합니다.</p>
<p>또한, 비록 그 값에 해당하는 열거자가 목록에 없더라도 대상 열거형의 <strong>기본 자료형이 표현할 수 있는 범위 내의 정수 값</strong> 이라면 <code>static_cast</code> 하는 것은 안전합니다. 기본 자료형의 범위를 벗어나는 값을 변환하면 정의되지 않은 동작(Undefined behavior)이 발생합니다.</p>
<blockquote>
<p><strong>심화 학습</strong> </p>
<ul>
<li>열거형에 명시적으로 정의된 기본 자료형이 있다면, 열거형의 범위는 그 기본 자료형의 범위와 동일합니다.</li>
<li>명시적인 기본 자료형이 없다면 상황이 조금 더 복잡해집니다. 이 경우 컴파일러가 기본 자료형을 선택하게 되는데, 모든 열거자의 값이 들어갈 수만 있다면 부호 있는(signed) 타입이나 부호 없는(unsigned) 타입 중 아무거나 선택할 수 있습니다. 따라서, <strong>모든 열거자의 값을 담을 수 있는 가장 작은 비트 수의 범위 안에 들어가는 정수 값</strong> 만 <code>static_cast</code> 하는 것이 안전합니다.</li>
</ul>
<p>이해를 돕기 위해 두 가지 예를 들어보겠습니다.</p>
<ul>
<li>열거자들의 값이 2, 9, 12라면, 이 값들은 범위가 0부터 15인 부호 없는 4비트 정수형에 딱 맞게 들어갈 수 있습니다. 
따라서 이 열거형으로는 0부터 15까지의 정수 값만 <code>static_cast</code> 하는 것이 안전합니다.</li>
<li>열거자들의 값이 -28, 2, 6이라면, 이 값들은 범위가 -32부터 31인 부호 있는 6비트 정수형에 들어갈 수 있습니다. 
따라서 이 열거형으로는 -32부터 31까지의 정수 값만 <code>static_cast</code> 하는 것이 안전합니다.</li>
</ul>
</blockquote>
<p>두 번째 방법은 C++17부터 적용되는 것으로, 범위 없는 열거형에 명시적으로 기본 자료형이 지정된 경우, 컴파일러는 정수 값을 이용한 <strong>리스트 초기화(list initialization)</strong> 를 허용합니다.</p>
<pre><code class="language-cpp">enum Pet: int // 기본 자료형을 지정했습니다
{
    cat, // 0 할당됨
    dog, // 1 할당됨
    pig, // 2 할당됨
    whale, // 3 할당됨
};

int main()
{
    Pet pet1 { 2 }; // 정상: 기본 자료형이 지정된 범위 없는 열거형은 정수로 중괄호 초기화가 가능합니다 (C++17)
    Pet pet2 (2);   // 컴파일 에러: 정수로 직접 초기화할 수 없습니다
    Pet pet3 = 2;   // 컴파일 에러: 정수로 복사 초기화할 수 없습니다

    pet1 = 3;       // 컴파일 에러: 정수를 할당(대입)할 수 없습니다

    return 0;
}
</code></pre>
<hr>
<h2 id="134--열거형을-문자열로-문자열을-열거형으로-변환하기">13.4 — 열거형을 문자열로, 문자열을 열거형으로 변환하기</h2>
<p>이전 레슨(13.3 -- 범위 없는 열거자의 정수 변환)에서 다음과 같은 예제를 살펴보았습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

enum Color
{
    black, // 0
    red,   // 1
    blue,  // 2
};

int main()
{
    Color shirt{ blue };

    std::cout &lt;&lt; &quot;Your shirt is &quot; &lt;&lt; shirt &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 실행하면 다음과 같이 출력됩니다:</p>
<blockquote>
<p>Your shirt is 2</p>
</blockquote>
<p><code>operator&lt;&lt;</code> 는 <code>Color</code> 를 어떻게 출력해야 할지 모르기 때문에, 컴파일러는 <code>Color</code> 를 <strong>정수 값</strong> 으로 암시적 변환하여 대신 출력합니다.</p>
<p>대부분의 경우 열거형을 2와 같은 정수 값으로 출력하는 것은 우리가 원하는 결과가 아닙니다. 그보다는 열거자가 나타내는 진짜 이름(예: blue)을 출력하고 싶어 하죠. C++는 이를 위한 기본 기능을 제공하지 않기 때문에 우리가 직접 해결책을 찾아야 합니다. 다행히도 이 작업은 그리 어렵지 않습니다.</p>
<hr>
<h3 id="열거자의-이름-가져오기">열거자의 이름 가져오기</h3>
<p>열거자의 이름을 가져오는 가장 일반적인 방법은 열거자를 전달받아 그 이름을 문자열로 반환하는 함수를 만드는 것입니다. 
하지만 이를 위해서는 특정 열거자가 들어왔을 때 어떤 문자열을 반환할지 결정하는 방법이 필요합니다.</p>
<p>여기에는 두 가지 일반적인 방법이 있습니다.</p>
<p>레슨 8.5(<strong>switch 문 기초</strong>) 에서, <strong>switch 문</strong> 은 정수 값이나 열거형 값 모두에 사용할 수 있다고 배웠습니다. 
다음 예제에서는 <strong>switch 문</strong> 을 사용하여 열거자를 확인하고, 해당 열거자에 맞는 색상 문자열 리터럴을 반환해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

enum Color
{
    black,
    red,
    blue,
};

constexpr std::string_view getColorName(Color color)
{
    switch (color)
    {
    case black: return &quot;black&quot;;
    case red:   return &quot;red&quot;;
    case blue:  return &quot;blue&quot;;
    default:    return &quot;???&quot;;
    }
}

int main()
{
    constexpr Color shirt{ blue };

    std::cout &lt;&lt; &quot;Your shirt is &quot; &lt;&lt; getColorName(shirt) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드의 출력 결과는 다음과 같습니다.</p>
<blockquote>
<p>Your shirt is blue</p>
</blockquote>
<p>위 예제에서는 우리가 전달한 열거자를 담고 있는 <code>color</code> 변수에 대해 <strong>switch 문</strong> 을 사용합니다. 
switch 문 내부에는 <code>Color</code> 의 각 열거자에 대한 case 라벨이 있습니다. 
각 case는 알맞은 색상의 이름을 <strong>C 스타일 문자열 리터럴</strong> 로 반환합니다. 
이 문자열 리터럴은 <code>std::string_view</code> 로 암시적 변환되어 함수를 호출한 곳으로 반환됩니다. 
또한, 사용자가 예상치 못한 값을 전달할 경우를 대비하여 <code>&quot;???&quot;</code> 를 반환하는 default case도 마련해 두었습니다.</p>
<p><strong>기억해 두세요</strong>
<strong>C 스타일 문자열 리터럴</strong> 은 프로그램이 실행되는 내내 존재합니다. 따라서 이를 바라보고 있는(viewing) <code>std::string_view</code> 를 반환해도 안전합니다. <code>std::string_view</code> 가 복사되어 반환되더라도, 참조하고 있는 원본 문자열은 여전히 존재하기 때문입니다.</p>
<p>이 함수는 <strong>상수 표현식(constant expression)</strong> 에서 색상 이름을 사용할 수 있도록 <code>constexpr</code> 로 선언되었습니다.</p>
<p><strong>관련 내용</strong>
<strong>constexpr 함수</strong> 에 대한 자세한 내용은 레슨 F.1 -- Constexpr 함수에서 다룹니다.</p>
<p>이 방법을 사용하면 열거자의 이름을 문자열로 가져올 수 있습니다. 하지만 콘솔에 출력할 때 <code>std::cout &lt;&lt; getColorName(shirt)</code> 라고 작성하는 것은 <code>std::cout &lt;&lt; shirt</code> 만큼 깔끔하지는 않죠. 다음 레슨인 13.5 -- 입출력 연산자 오버로딩 소개에서 <code>std::cout</code> 이 열거형을 직접 출력할 수 있도록 만드는 방법을 배울 것입니다.</p>
<p>열거자를 문자열로 연결(매핑)하는 두 번째 방법은 <strong>배열(array)</strong> 을 사용하는 것입니다. 이 내용은 레슨 17.6 -- <code>std::array</code> 와 열거형에서 다룹니다.</p>
<hr>
<h3 id="범위-없는-열거자-입력받기">범위 없는 열거자 입력받기</h3>
<p>이제 입력을 받는 경우를 살펴봅시다. 
다음 예제에서는 <code>Pet</code> 이라는 열거형을 정의합니다. 
<code>Pet</code> 은 프로그램에서 우리가 직접 만든 타입이기 때문에, C++는 <code>std::cin</code> 을 사용해 <code>Pet</code> 을 어떻게 입력받아야 할지 모릅니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

enum Pet
{
    cat,   // 0
    dog,   // 1
    pig,   // 2
    whale, // 3
};

int main()
{
    Pet pet { pig };
    std::cin &gt;&gt; pet; // 컴파일 에러: std::cin은 Pet을 입력받는 방법을 모릅니다

    return 0;
}
</code></pre>
<p>이 문제를 우회하는 간단한 방법은 먼저 정수를 입력받은 다음, <code>static_cast</code> 를 사용하여 그 정수를 알맞은 열거형 타입으로 변환하는 것입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

enum Pet
{
    cat,   // 0
    dog,   // 1
    pig,   // 2
    whale, // 3
};

constexpr std::string_view getPetName(Pet pet)
{
    switch (pet)
    {
    case cat:   return &quot;cat&quot;;
    case dog:   return &quot;dog&quot;;
    case pig:   return &quot;pig&quot;;
    case whale: return &quot;whale&quot;;
    default:    return &quot;???&quot;;
    }
}

int main()
{
    std::cout &lt;&lt; &quot;Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): &quot;;

    int input{};
    std::cin &gt;&gt; input; // 정수를 입력받습니다

    if (input &lt; 0 || input &gt; 3)
        std::cout &lt;&lt; &quot;You entered an invalid pet\n&quot;;
    else
    {
        Pet pet{ static_cast&lt;Pet&gt;(input) }; // 정수를 Pet으로 static_cast 변환합니다
        std::cout &lt;&lt; &quot;You entered: &quot; &lt;&lt; getPetName(pet) &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}
</code></pre>
<p>이 방법이 잘 작동하긴 하지만 조금 어색합니다. 또한, <code>input</code> 값이 열거자의 유효한 범위 안에 있다는 것을 확실히 확인한 후에만 <code>static_cast&lt;Pet&gt;(input)</code> 을 사용해야 한다는 점을 잊지 마세요.</p>
<hr>
<h3 id="문자열에서-열거형-가져오기">문자열에서 열거형 가져오기</h3>
<p>숫자를 입력하는 대신, 사용자가 열거자를 나타내는 문자열(예: &quot;pig&quot;)을 직접 입력하고 코드가 이를 알맞은 <code>Pet</code> 열거자로 변환해 준다면 훨씬 편할 것입니다. 하지만 이를 구현하려면 두 가지 문제를 해결해야 합니다.</p>
<p>첫째, 문자열에는 <strong>switch 문</strong> 을 사용할 수 없으므로 사용자가 입력한 문자열을 비교할 다른 방법이 필요합니다.
여기서 가장 간단한 접근법은 여러 개의 <strong>if 문</strong> 을 사용하는 것입니다.</p>
<p>둘째, 사용자가 잘못된 문자열을 입력했을 때 어떤 <code>Pet</code> 열거자를 반환해야 할까요? &quot;없음/잘못됨&quot;을 뜻하는 열거자를 하나 추가해서 반환하는 것도 방법이겠지만, 여기서는 <code>std::optional</code> 을 사용하는 것이 더 좋은 선택입니다.</p>
<p><strong>관련 내용</strong>
<code>std::optional</code> 에 대해서는 레슨 12.15 -- <code>std::optional</code> 에서 다룹니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;optional&gt; // std::optional을 사용하기 위함
#include &lt;string&gt;
#include &lt;string_view&gt;

enum Pet
{
    cat,   // 0
    dog,   // 1
    pig,   // 2
    whale, // 3
};

constexpr std::string_view getPetName(Pet pet)
{
    switch (pet)
    {
    case cat:   return &quot;cat&quot;;
    case dog:   return &quot;dog&quot;;
    case pig:   return &quot;pig&quot;;
    case whale: return &quot;whale&quot;;
    default:    return &quot;???&quot;;
    }
}

constexpr std::optional&lt;Pet&gt; getPetFromString(std::string_view sv)
{
    // 문자열이 아닌 정수 값(또는 열거형)에만 switch 문을 사용할 수 있으므로
    // 여기서는 if 문을 사용해야 합니다
    if (sv == &quot;cat&quot;)   return cat;
    if (sv == &quot;dog&quot;)   return dog;
    if (sv == &quot;pig&quot;)   return pig;
    if (sv == &quot;whale&quot;) return whale;

    return {};
}

int main()
{
    std::cout &lt;&lt; &quot;Enter a pet: cat, dog, pig, or whale: &quot;;
    std::string s{};
    std::cin &gt;&gt; s;

    std::optional&lt;Pet&gt; pet { getPetFromString(s) };

    if (!pet)
        std::cout &lt;&lt; &quot;You entered an invalid pet\n&quot;;
    else
        std::cout &lt;&lt; &quot;You entered: &quot; &lt;&lt; getPetName(*pet) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위의 해결책에서는 문자열을 비교하기 위해 여러 개의 if-else 문을 사용했습니다. 
사용자가 입력한 문자열이 열거자의 이름과 일치하면 해당하는 열거자를 반환합니다. 
일치하는 문자열이 없으면 &quot;값이 없음&quot;을 뜻하는 <code>{}</code> 를 반환합니다.</p>
<p><strong>심화 학습</strong>
참고로 위의 코드는 소문자만 인식합니다. 대소문자 구분 없이 입력받고 싶다면, 다음 함수를 사용하여 사용자의 입력을 모두 소문자로 변환할 수 있습니다:</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt; // std::transform을 사용하기 위함
#include &lt;cctype&gt;    // std::tolower를 사용하기 위함
#include &lt;iterator&gt;  // std::back_inserter를 사용하기 위함
#include &lt;string&gt;
#include &lt;string_view&gt;

// 이 함수는 전달받은 std::string_view의 소문자 버전인 std::string을 반환합니다.
// 이 함수는 1:1 문자 매핑만 수행할 수 있습니다.
std::string toASCIILowerCase(std::string_view sv)
{
    std::string lower{};
    std::transform(sv.begin(), sv.end(), std::back_inserter(lower),
        [](char c)
        {
            return static_cast&lt;char&gt;(std::tolower(static_cast&lt;unsigned char&gt;(c)));
        });
    return lower;
}
</code></pre>
<p>이 함수는 <code>std::string_view sv</code> 의 문자를 하나씩 확인하면서, 람다(lambda) 함수와 <code>std::tolower()</code> 를 사용하여 소문자로 변환한 다음, 그 소문자를 <code>lower</code> 문자열에 덧붙입니다.</p>
<p><strong>람다(lambda)</strong> 에 대한 내용은 레슨 20.6 -- 람다(익명 함수) 소개에서 다룹니다.</p>
<p>출력과 마찬가지로, <code>std::cin &gt;&gt; pet</code> 처럼 바로 입력받을 수 있다면 가장 좋을 것입니다. 이 내용은 다음 레슨인 13.5 -- 입출력 연산자 오버로딩 소개에서 배울 예정입니다.</p>
<hr>
<h2 id="135--입출력io-연산자-오버로딩-소개">13.5 — 입출력(I/O) 연산자 오버로딩 소개</h2>
<p>이전 레슨(13.4 - 열거형과 문자열 간의 변환)에서는 열거형을 문자열로 변환하는 함수를 사용하는 예제를 살펴보았습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

enum Color
{
    black,
    red,
    blue,
};

constexpr std::string_view getColorName(Color color)
{
    switch (color)
    {
    case black: return &quot;black&quot;;
    case red:   return &quot;red&quot;;
    case blue:  return &quot;blue&quot;;
    default:    return &quot;???&quot;;
    }
}

int main()
{
    constexpr Color shirt{ blue };

    std::cout &lt;&lt; &quot;Your shirt is &quot; &lt;&lt; getColorName(shirt) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위 예제는 잘 작동하지만, 두 가지 단점이 있습니다.</p>
<ul>
<li>열거자의 이름을 가져오기 위해 직접 만든 함수 이름을 일일이 기억해야 합니다.</li>
<li>출력할 때마다 함수를 호출해야 하므로 코드가 지저분해집니다.</li>
</ul>
<p>가장 이상적인 방법은 <code>operator&lt;&lt;</code> 에게 열거형을 출력하는 방법을 알려주어, <code>std::cout &lt;&lt; shirt</code> 처럼 코드를 작성했을 때 우리가 기대하는 대로 바로 출력되게 만드는 것입니다.</p>
<hr>
<h3 id="연산자-오버로딩operator-overloading-소개"><strong>연산자 오버로딩(Operator overloading)</strong> 소개</h3>
<p>레슨 11.1에서 함수 오버로딩에 대해 배웠습니다. 
함수 오버로딩을 사용하면 매개변수가 다를 경우 같은 이름의 함수를 여러 개 만들 수 있습니다. 
덕분에 데이터 타입마다 새로운 함수 이름을 고민할 필요가 없었죠.</p>
<p>마찬가지로 C++은 <strong>연산자 오버로딩</strong> 을 지원합니다. 
이를 통해 기존 연산자(+, -, &lt;&lt; 등)의 기능을 확장하여, 우리가 직접 만든 데이터 타입(클래스나 열거형 등)에서도 작동하도록 만들 수 있습니다.</p>
<p>기본적인 연산자 오버로딩 방법은 꽤 간단합니다.</p>
<ol>
<li>오버로딩할 연산자의 이름을 함수 이름으로 사용하여 함수를 정의합니다 (예: <code>operator+</code>).</li>
<li>각 피연산자(왼쪽에서 오른쪽 순서)에 맞는 타입의 매개변수를 추가합니다. 이때 매개변수 중 최소 하나는 반드시 사용자 정의 타입(클래스나 열거형)이어야 합니다. 그렇지 않으면 컴파일 에러가 발생합니다.</li>
<li>논리적으로 알맞은 반환 타입(return type)을 설정합니다.</li>
<li><code>return</code> 문을 사용해 연산 결과를 반환합니다.</li>
</ol>
<p>컴파일러는 수식에서 연산자를 만났을 때, 피연산자 중 하나라도 사용자 정의 타입이 있다면 이를 처리할 수 있는 오버로딩된 연산자 함수가 있는지 확인합니다. 예를 들어 <code>x + y</code> 라는 코드가 있다면, 컴파일러는 이 연산을 수행할 수 있는 <code>operator+(x, y)</code> 함수가 있는지 찾습니다. 모호하지 않고 명확한 <code>operator+</code> 함수를 찾으면 이를 호출하고 그 결과를 반환합니다.</p>
<blockquote>
<p><strong>관련 내용:</strong> 연산자 오버로딩에 대한 더 자세한 내용은 21장에서 다룹니다.</p>
</blockquote>
<blockquote>
<p><strong>심화 학습:</strong> 연산자는 가장 왼쪽 피연산자의 멤버 함수로도 오버로딩할 수 있습니다. 이는 레슨 21.5에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="열거자를-출력하기-위한-operator-오버로딩">열거자를 출력하기 위한 <code>operator&lt;&lt;</code> 오버로딩</h3>
<p>본격적으로 시작하기 전에, 출력할 때 <code>operator&lt;&lt;</code> 가 어떻게 작동하는지 짧게 복습해 보겠습니다.</p>
<p><code>std::cout &lt;&lt; 5</code> 같은 간단한 코드를 생각해 봅시다. 
<code>std::cout</code> 은 <code>std::ostream</code> 타입(표준 라이브러리에서 제공하는 사용자 정의 타입)이고, <code>5</code> 는 <code>int</code> 타입의 리터럴(숫자 값)입니다.</p>
<p>이 코드가 실행되면, 컴파일러는 <code>std::ostream</code> 과 <code>int</code> 타입을 인자로 받을 수 있는 오버로딩된 <code>operator&lt;&lt;</code> 함수를 찾아서 호출합니다 (이 함수도 표준 입출력 라이브러리에 정의되어 있습니다). 이 함수 내부에서는 콘솔 화면에 값(<code>5</code>)을 출력하는 작업이 이루어집니다. 마지막으로, <code>operator&lt;&lt;</code> 함수는 왼쪽 피연산자(여기서는 <code>std::cout</code>)를 다시 반환합니다. 덕분에 여러 번 연속해서 <code>operator&lt;&lt;</code> 를 사용할 수 있는 것(체이닝)입니다.</p>
<p>이 원리를 바탕으로 <code>Color</code> 열거형을 출력하는 <code>operator&lt;&lt;</code> 를 직접 구현해 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

enum Color
{
    black,
    red,
    blue,
};

constexpr std::string_view getColorName(Color color)
{
    switch (color)
    {
    case black: return &quot;black&quot;;
    case red:   return &quot;red&quot;;
    case blue:  return &quot;blue&quot;;
    default:    return &quot;???&quot;;
    }
}

// operator&lt;&lt; 에게 Color 열거형을 출력하는 방법을 알려줍니다.
// std::ostream은 std::cout, std::cerr 등의 타입입니다.
// 복사본이 생성되는 것을 막기 위해 반환 타입과 매개변수 타입으로 참조(&amp;)를 사용합니다.
std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, Color color)
{
    out &lt;&lt; getColorName(color); // 전달받은 출력 스트림(out)에 색상 이름을 출력합니다.
    return out;                 // operator&lt;&lt; 는 관례적으로 왼쪽 피연산자를 반환합니다.

    // 위 코드는 다음 한 줄로 줄일 수 있습니다:
    // return out &lt;&lt; getColorName(color);
}

int main()
{
    Color shirt{ blue };
    std::cout &lt;&lt; &quot;Your shirt is &quot; &lt;&lt; shirt &lt;&lt; &#39;\n&#39;; // 잘 작동합니다!

    return 0;
}
</code></pre>
<p>출력 결과는 다음과 같습니다:
<code>Your shirt is blue</code></p>
<p>방금 만든 오버로딩 함수를 조금 더 자세히 살펴보겠습니다. 
먼저, 우리가 오버로딩하려는 연산자가 <code>&lt;&lt;</code> 이므로 함수 이름은 <code>operator&lt;&lt;</code> 가 됩니다. 
이 함수는 두 개의 매개변수를 가집니다.</p>
<p>왼쪽 매개변수(왼쪽 피연산자와 연결됨)는 출력 스트림이며, 타입은 <code>std::ostream</code> 입니다. 
함수를 호출할 때 <code>std::ostream</code> 객체의 복사본이 만들어지는 것을 원치 않지만, 출력을 위해 객체의 상태가 변경되어야 하므로 상수(<code>const</code>)가 아닌 일반 <strong>참조(reference)</strong> 로 전달합니다. 오른쪽 매개변수(오른쪽 피연산자와 연결됨)는 우리의 <code>Color</code> 객체입니다. <code>operator&lt;&lt;</code> 는 관례적으로 왼쪽 피연산자를 반환하므로, 반환 타입 역시 왼쪽 피연산자의 타입인 <code>std::ostream&amp;</code> 가 됩니다.</p>
<p>이제 함수 내부를 봅시다. <code>std::ostream</code> 객체는 이미 <code>std::string_view</code> 를 출력하는 방법을 알고 있습니다(표준 라이브러리 덕분입니다). 따라서 <code>out &lt;&lt; getColorName(color)</code> 코드는 색상 이름을 <code>std::string_view</code> 로 가져와서 그대로 출력 스트림에 밀어 넣기만 하면 됩니다.</p>
<p>여기서 <code>std::cout</code> 대신 <code>out</code> 매개변수를 사용했다는 점을 주목하세요. 이렇게 해야 함수를 호출하는 쪽에서 원하는 출력 스트림을 유연하게 결정할 수 있습니다(예를 들어 <code>std::cerr &lt;&lt; color</code> 라고 작성하면 <code>std::cout</code> 이 아니라 <code>std::cerr</code> 로 출력됩니다).</p>
<p>왼쪽 피연산자를 반환하는 것도 쉽습니다. 매개변수 <code>out</code> 이 왼쪽 피연산자이므로, 단순히 <code>out</code> 을 반환하면 됩니다.</p>
<p>종합해 볼까요? 우리가 <code>std::cout &lt;&lt; shirt</code> 를 호출하면, 컴파일러는 <code>Color</code> 타입에 맞게 오버로딩된 <code>operator&lt;&lt;</code> 함수를 찾습니다. 그런 다음 <code>out</code> 매개변수에는 <code>std::cout</code> 이, <code>color</code> 매개변수에는 <code>shirt</code> 변수(값은 <code>blue</code>)가 전달되어 함수가 실행됩니다. <code>out</code> 은 <code>std::cout</code> 의 참조이고 <code>color</code> 는 <code>blue</code> 의 복사본이므로, <code>out &lt;&lt; getColorName(color)</code> 는 화면에 <code>&quot;blue&quot;</code> 를 출력합니다. 마지막으로, 추가적인 출력이 이어질 수 있도록 <code>out</code> 이 호출자에게 다시 반환됩니다.</p>
<hr>
<h3 id="열거자를-입력받기-위한-operator-오버로딩">열거자를 입력받기 위한 <code>operator&gt;&gt;</code> 오버로딩</h3>
<p>위에서 <code>operator&lt;&lt;</code> 에게 열거형 출력 방법을 알려준 것과 비슷하게, <code>operator&gt;&gt;</code> 에게 열거형 입력 방법을 알려줄 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;limits&gt;
#include &lt;optional&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

enum Pet
{
    cat,   // 0
    dog,   // 1
    pig,   // 2
    whale, // 3
};

constexpr std::string_view getPetName(Pet pet)
{
    switch (pet)
    {
    case cat:   return &quot;cat&quot;;
    case dog:   return &quot;dog&quot;;
    case pig:   return &quot;pig&quot;;
    case whale: return &quot;whale&quot;;
    default:    return &quot;???&quot;;
    }
}

constexpr std::optional&lt;Pet&gt; getPetFromString(std::string_view sv)
{
    if (sv == &quot;cat&quot;)   return cat;
    if (sv == &quot;dog&quot;)   return dog;
    if (sv == &quot;pig&quot;)   return pig;
    if (sv == &quot;whale&quot;) return whale;

    return {};
}

// pet은 입력 및 출력(in/out)을 모두 담당하는 매개변수입니다.
std::istream&amp; operator&gt;&gt;(std::istream&amp; in, Pet&amp; pet)
{
    std::string s{};
    in &gt;&gt; s; // 사용자로부터 문자열을 입력받습니다.

    std::optional&lt;Pet&gt; match { getPetFromString(s) };
    if (match) // 일치하는 항목을 찾았다면
    {
        pet = *match; // std::optional을 역참조하여 일치하는 열거자를 가져옵니다.
        return in;
    }

    // 일치하는 항목을 찾지 못했다면 입력이 잘못된 것입니다.
    // 따라서 입력 스트림을 실패(fail) 상태로 설정합니다.
    in.setstate(std::ios_base::failbit);

    // 데이터 추출에 실패하면 operator&gt;&gt; 는 기본 타입들을 0으로 초기화합니다.
    // 이 연산자도 똑같이 동작하게 하려면 아래 줄의 주석을 해제하세요.
    // pet = {};

    return in;
}

int main()
{
    std::cout &lt;&lt; &quot;Enter a pet: cat, dog, pig, or whale: &quot;;
    Pet pet{};
    std::cin &gt;&gt; pet;

    if (std::cin) // 일치하는 항목을 찾았다면
        std::cout &lt;&lt; &quot;You chose: &quot; &lt;&lt; getPetName(pet) &lt;&lt; &#39;\n&#39;;
    else
    {
        std::cin.clear(); // 입력 스트림을 정상(good) 상태로 초기화합니다.
        std::cin.ignore(std::numeric_limits&lt;std::streamsize&gt;::max(), &#39;\n&#39;);
        std::cout &lt;&lt; &quot;Your pet was not valid\n&quot;;
    }

    return 0;
}
</code></pre>
<p>출력할 때와 비교해서 눈여겨볼 만한 몇 가지 차이점이 있습니다.
첫째, <code>std::cin</code> 은 <code>std::istream</code> 타입이므로 왼쪽 매개변수와 반환 타입을 <code>std::ostream&amp;</code> 대신 <code>std::istream&amp;</code> 로 사용합니다.
둘째, <code>pet</code> 매개변수는 상수(<code>const</code>)가 아닌 일반 참조입니다. 이를 통해 추출(입력)에 성공했을 때 <code>operator&gt;&gt;</code> 함수가 전달받은 오른쪽 피연산자의 값을 직접 수정할 수 있게 됩니다.</p>
<blockquote>
<p><strong>핵심 포인트:</strong> 오른쪽 피연산자인 <code>pet</code> 은 출력(out) 매개변수입니다. 출력 매개변수에 대해서는 레슨 12.13에서 다룹니다.</p>
</blockquote>
<p>만약 <code>pet</code> 이 참조 매개변수가 아니라 값 매개변수(value parameter)였다면, <code>operator&gt;&gt;</code> 함수는 실제 오른쪽 피연산자가 아니라 넘겨받은 복사본에 새로운 값을 할당해버리고 말았을 것입니다. 우리는 실제 피연산자의 값이 업데이트되기를 원하므로 참조를 사용해야 합니다.</p>
<p>함수 내부에서는 이미 문자열 입력 방법을 알고 있는 <code>operator&gt;&gt;</code> 를 이용해 <code>std::string</code> 으로 우선 입력을 받습니다. 사용자가 입력한 값이 동물의 이름 중 하나와 일치하면, <code>pet</code> 에 해당 열거자를 할당하고 왼쪽 피연산자(<code>in</code>)를 반환합니다.</p>
<p>만약 사용자가 유효하지 않은 동물 이름을 입력했다면, <code>std::cin</code> 을 &quot;실패 모드(failure mode)&quot;로 설정하여 예외 상황을 처리합니다. 이는 입력 추출에 실패했을 때 <code>std::cin</code> 이 보통 취하는 상태입니다. 호출하는 쪽에서는 <code>std::cin</code> 의 상태를 확인하여 입력이 성공했는지 실패했는지 알아낼 수 있습니다.</p>
<blockquote>
<p><strong>관련 내용:</strong> 레슨 17.6(std::array와 열거형)에서는 <code>std::array</code> 를 사용하여 입출력 연산자의 중복을 줄이고, 새로운 열거자가 추가될 때마다 연산자를 수정해야 하는 번거로움을 피하는 방법을 알아봅니다.</p>
</blockquote>
<hr>
<h2 id="136--영역-지정-열거형-enum-class">13.6 — 영역 지정 열거형 (enum class)</h2>
<p>기존의 일반 열거형은 C++에서 고유한 자료형(타입)으로 취급되지만, 타입 검사가 엄격하지 않아 가끔 상식적으로 말이 안 되는 동작을 허용하기도 합니다. 다음 예시를 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    enum Color
    {
        red,
        blue,
    };

    enum Fruit
    {
        banana,
        apple,
    };

    Color color { red };
    Fruit fruit { banana };

    if (color == fruit) // 컴파일러는 color와 fruit를 정수로 변환하여 비교합니다
        std::cout &lt;&lt; &quot;color and fruit are equal\n&quot;; // 그리고 둘이 같다고 판단합니다!
    else
        std::cout &lt;&lt; &quot;color and fruit are not equal\n&quot;;

    return 0;
}
</code></pre>
<p>이 코드는 다음과 같이 출력됩니다:
<code>color and fruit are equal</code></p>
<p>컴파일러가 <code>color</code> 와 <code>fruit</code> 를 비교할 때, 서로 다른 타입인 <code>Color</code> 와 <code>Fruit</code> 를 어떻게 비교해야 할지 모릅니다. 
그래서 두 값을 모두 정수로 변환해 봅니다. 둘 다 정수로 바꾸면 숫자끼리의 비교가 가능해지기 때문입니다. 이 예시에서 <code>color</code> 와 <code>fruit</code> 는 모두 숫자 <code>0</code> 으로 변환되는 열거자(enumerator)를 가지고 있기 때문에, 컴파일러는 두 값이 같다고 판단해 버립니다.</p>
<p>하지만 <code>color</code> 와 <code>fruit</code> 는 서로 다른 열거형에 속해 있고 애초에 비교할 목적으로 만든 것이 아니기 때문에, 이는 의미상 맞지 않는 행동입니다. 안타깝게도 일반 열거형에서는 이런 문제를 쉽게 막을 방법이 없습니다.</p>
<p>이러한 문제와 더불어 네임스페이스 오염(일반 열거형을 전역에 정의하면 그 안의 값들이 전역 네임스페이스를 꽉 채워버리는 문제) 때문에, C++ 설계자들은 열거형을 더 깔끔하게 사용할 수 있는 새로운 해결책이 필요하다고 판단했습니다.</p>
<hr>
<h3 id="영역-지정-열거형-scoped-enumerations">영역 지정 열거형 (Scoped enumerations)</h3>
<p>그 해결책이 바로 <strong>영역 지정 열거형(Scoped enumeration)</strong> 입니다. 
(왜 이런 이름이 붙었는지는 곧 알게 되시겠지만, C++에서는 보통 <strong>enum class</strong> 라고 부릅니다.)</p>
<p>영역 지정 열거형은 일반 열거형과 비슷하게 작동하지만 두 가지 큰 차이점이 있습니다. 첫째, <strong>정수로 자동(암시적) 변환되지 않습니다</strong>. 둘째, 열거형 값들이 열거형이 정의된 주변 영역이 아니라 <strong>해당 열거형 내부 영역(scope)에만</strong> 속하게 됩니다.</p>
<p>영역 지정 열거형을 만들려면 <code>enum class</code> 라는 키워드를 사용합니다. 나머지 작성법은 일반 열거형과 똑같습니다. 다음 예시를 확인해 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    enum class Color // &quot;enum class&quot;는 이것을 일반 열거형이 아닌 영역 지정 열거형으로 정의합니다
    {
        red, // red는 Color의 영역 안에 속하는 것으로 간주됩니다
        blue,
    };

    enum class Fruit
    {
        banana, // banana는 Fruit의 영역 안에 속하는 것으로 간주됩니다
        apple,
    };

    Color color { Color::red }; // 참고: red에 직접 접근할 수 없으며, Color::red를 사용해야 합니다
    Fruit fruit { Fruit::banana }; // 참고: banana에 직접 접근할 수 없으며, Fruit::banana를 사용해야 합니다

    if (color == fruit) // 컴파일 에러: 컴파일러는 서로 다른 타입인 Color와 Fruit를 비교하는 방법을 모릅니다
        std::cout &lt;&lt; &quot;color and fruit are equal\n&quot;;
    else
        std::cout &lt;&lt; &quot;color and fruit are not equal\n&quot;;

    return 0;
}
</code></pre>
<p>영역 지정 열거형은 다른 타입과 비교할 수 있도록 자동으로 형태가 변환되지 않기 때문에, 위 프로그램은 19번째 줄에서 컴파일 에러를 발생시킵니다.</p>
<blockquote>
<p><strong>참고 사항</strong>
C++에서 <code>class</code> 키워드(그리고 <code>static</code> 키워드)는 문맥에 따라 여러 가지 의미를 가질 수 있는, 가장 많이 재사용되는 키워드 중 하나입니다. 영역 지정 열거형이 <code>class</code> 키워드를 사용하긴 하지만, 구조체나 클래스처럼 진짜 &quot;클래스 타입&quot;으로 취급되지는 않습니다.
이 문맥에서는 <code>enum struct</code> 를 사용해도 <code>enum class</code> 와 완전히 똑같이 작동합니다. 하지만 <code>enum struct</code> 는 관례적으로 잘 쓰이지 않는 표현이므로 사용을 피하는 것이 좋습니다.</p>
</blockquote>
<hr>
<h3 id="영역-지정-열거형은-자신만의-독립적인-영역을-가집니다">영역 지정 열거형은 자신만의 독립적인 영역을 가집니다</h3>
<p>일반 열거형은 자신의 값들을 열거형이 정의된 곳과 같은 영역에 둡니다. 
반면 영역 지정 열거형은 자신의 값들을 <strong>해당 열거형 내부 영역에만</strong> 둡니다. 
즉, 영역 지정 열거형은 자신의 값들을 담아두는 &#39;네임스페이스(namespace)&#39; 역할을 합니다. 
이렇게 내장된 네임스페이스 기능 덕분에 이름이 겹쳐서 충돌하는 일을 막아주고, 전역 공간이 지저분해지는 것을 방지할 수 있습니다.</p>
<p>영역 지정 열거형의 값에 접근할 때는, 마치 열거형 이름과 똑같은 네임스페이스 안에 있는 값에 접근하는 것처럼 작성하면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    enum class Color // &quot;enum class&quot;는 이것을 일반 열거형이 아닌 영역 지정 열거형으로 정의합니다
    {
        red, // red는 Color의 영역 안에 속하는 것으로 간주됩니다
        blue,
    };

    std::cout &lt;&lt; red &lt;&lt; &#39;\n&#39;;        // 컴파일 에러: 이 영역에는 red가 정의되어 있지 않습니다
    std::cout &lt;&lt; Color::red &lt;&lt; &#39;\n&#39;; // 컴파일 에러: std::cout은 이것을 출력하는 방법을 모릅니다 (정수로 자동 변환되지 않음)

    Color color { Color::blue }; // 정상 작동

    return 0;
}
</code></pre>
<p>영역 지정 열거형 자체가 이미 이름 공간을 분리해 주는 역할을 하므로, 특별한 이유가 없다면 굳이 또 다른 네임스페이스 안에 넣을 필요가 없습니다. 불필요한 중복일 뿐입니다.</p>
<hr>
<h3 id="영역-지정-열거형은-정수로-자동암시적-변환되지-않습니다">영역 지정 열거형은 정수로 자동(암시적) 변환되지 않습니다</h3>
<p>일반 열거형과 달리, 영역 지정 열거형의 값들은 정수로 몰래 자동으로 바뀌지 않습니다. 열거형을 숫자로 바꿀 일이 별로 없고, 다른 열거형끼리 실수로 비교하거나 <code>red + 5</code> 같은 이상한 연산을 원천 차단해 주기 때문에 대부분의 경우 이는 아주 좋은 기능입니다.</p>
<p>물론, 아래처럼 같은 영역 지정 열거형 안에 있는 값들끼리는 (타입이 같으므로) 정상적으로 비교할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    enum class Color
    {
        red,
        blue,
    };

    Color shirt { Color::red };

    if (shirt == Color::red) // 이처럼 같은 Color 타입끼리의 비교는 괜찮습니다
        std::cout &lt;&lt; &quot;The shirt is red!\n&quot;;
    else if (shirt == Color::blue)
        std::cout &lt;&lt; &quot;The shirt is blue!\n&quot;;

    return 0;
}
</code></pre>
<p>가끔은 영역 지정 열거형의 값을 정수처럼 사용해야 할 때도 있습니다. 이럴 때는 <code>static_cast</code> 를 사용하여 정수로 명시적 변환(강제 변환)을 할 수 있습니다. C++23부터는 <code>&lt;utility&gt;</code> 헤더에 있는 <code>std::to_underlying()</code> 함수를 사용하는 것이 더 좋습니다. 이 함수는 열거형 값을 그 열거형의 바탕이 되는 실제 타입(보통 정수형)으로 안전하게 변환해 줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;utility&gt; // std::to_underlying() 사용을 위해 추가 (C++23)

int main()
{
    enum class Color
    {
        red,
        blue,
    };

    Color color { Color::blue };

    std::cout &lt;&lt; color &lt;&lt; &#39;\n&#39;; // 정수로 자동 변환되지 않기 때문에 작동하지 않습니다
    std::cout &lt;&lt; static_cast&lt;int&gt;(color) &lt;&lt; &#39;\n&#39;;   // int로 명시적 변환, 1을 출력합니다
    std::cout &lt;&lt; std::to_underlying(color) &lt;&lt; &#39;\n&#39;; // 바탕 타입(underlying type)으로 변환, 1을 출력합니다 (C++23)

    return 0;
}
</code></pre>
<p>반대로 정수를 영역 지정 열거형으로 <code>static_cast</code> 할 수도 있습니다. 이는 사용자로부터 입력을 받을 때 무척 유용합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    enum class Pet
    {
        cat, // 0이 할당됨
        dog, // 1이 할당됨
        pig, // 2가 할당됨
        whale, // 3이 할당됨
    };

    std::cout &lt;&lt; &quot;Enter a pet (0=cat, 1=dog, 2=pig, 3=whale): &quot;;

    int input{};
    std::cin &gt;&gt; input; // 정수를 입력받음

    Pet pet{ static_cast&lt;Pet&gt;(input) }; // 정수를 Pet 타입으로 강제 변환(static_cast)

    return 0;
}
</code></pre>
<p>C++17부터는 <code>static_cast</code> 가 없어도 정수값을 사용하여 영역 지정 열거형을 중괄호 초기화(list initialize) 할 수 있습니다. (일반 열거형과 달리 바탕 타입을 명시하지 않아도 됩니다.)</p>
<pre><code class="language-cpp">// 이전 예제의 enum class Pet을 사용
Pet pet { 1 }; // 정상 작동
</code></pre>
<blockquote>
<p><strong>모범 사례</strong>
어쩔 수 없는 특별한 이유가 없다면 일반 열거형보다 <strong>영역 지정 열거형(enum class)</strong> 을 사용하는 것이 좋습니다.
영역 지정 열거형이 주는 많은 장점에도 불구하고, 실무에서는 일반 열거형 역시 여전히 많이 쓰입니다. 정수로 자동 변환되는 것이 꼭 필요하거나(매번 <code>static_cast</code> 를 쓰기 번거로울 때), 굳이 네임스페이스 영역을 분리할 필요가 없는 상황들이 있기 때문입니다.</p>
</blockquote>
<hr>
<h3 id="영역-지정-열거형을-정수로-더-쉽게-변환하기-고급">영역 지정 열거형을 정수로 더 쉽게 변환하기 (고급)</h3>
<p>영역 지정 열거형은 훌륭한 기능이지만, 정수로 자동 변환되지 않는다는 점이 가끔 귀찮게 느껴질 수 있습니다. 예를 들어 열거형 값을 배열의 인덱스로 쓰려고 할 때처럼 정수 변환이 자주 필요한 경우, 매번 <code>static_cast</code> 를 적어주는 것은 코드를 꽤 지저분하게 만듭니다.</p>
<p>만약 변환 과정을 더 편하게 만들고 싶다면, 단항 연산자 <code>+</code> (unary operator+)를 오버로딩하여 이 변환을 수행하게 만드는 유용한 꼼수(hack)가 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;type_traits&gt; // std::underlying_type_t 사용을 위해 추가

enum class Animals
{
    chicken, // 0
    dog, // 1
    cat, // 2
    elephant, // 3
    duck, // 4
    snake, // 5

    maxAnimals,
};

// 단항 + 연산자를 오버로딩하여 열거형을 바탕 타입으로 변환
// https://stackoverflow.com/a/42198760 에서 발췌, 아이디어를 제공한 Pixelchemist에게 감사
// C++23에서는 &lt;utility&gt;를 인클루드하고 대신 std::to_underlying(a)를 반환할 수 있습니다
template &lt;typename T&gt;
constexpr auto operator+(T a) noexcept
{
    return static_cast&lt;std::underlying_type_t&lt;T&gt;&gt;(a);
}

int main()
{
    std::cout &lt;&lt; +Animals::elephant &lt;&lt; &#39;\n&#39;; // 단항 연산자 + 를 사용하여 Animals::elephant를 정수로 변환

    return 0;
}
</code></pre>
<p>이 코드는 다음을 출력합니다:
<code>3</code></p>
<p>이 방법은 실수로 정수로 자동 변환되는 것은 든든하게 막아주면서도, 필요할 때만 <code>+</code> 기호를 써서 명시적이고 편리하게 변환할 수 있도록 해줍니다.</p>
<hr>
<h3 id="using-enum-문-c20"><code>using enum</code> 문 (C++20)</h3>
<p>C++20에 도입된 <code>using enum</code> 문은 열거형 안에 있는 모든 값들을 현재 영역(scope)으로 바로 불러옵니다. 
<code>enum class</code> 와 함께 사용하면, 매번 앞에 열거형 이름을 접두사처럼 붙이지 않고도 값에 곧바로 접근할 수 있게 해 줍니다.</p>
<p>이 기능은 <code>switch</code> 문 안에서처럼 똑같은 접두사를 반복해서 계속 써야 하는 경우에 매우 유용합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

enum class Color
{
    black,
    red,
    blue,
};

constexpr std::string_view getColor(Color color)
{
    using enum Color; // 모든 Color 열거형 값을 현재 영역으로 가져옵니다 (C++20)
    // 이제 Color:: 접두사 없이 Color의 열거형 값에 접근할 수 있습니다

    switch (color)
    {
    case black: return &quot;black&quot;; // 참고: Color::black 대신 black만 사용
    case red:   return &quot;red&quot;;
    case blue:  return &quot;blue&quot;;
    default:    return &quot;???&quot;;
    }
}

int main()
{
    Color shirt{ Color::blue };

    std::cout &lt;&lt; &quot;Your shirt is &quot; &lt;&lt; getColor(shirt) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위 예제에서 <code>Color</code> 는 <code>enum class</code> 이기 때문에, 보통은 <code>Color::blue</code> 처럼 전체 이름을 모두 적어주어야 합니다. 하지만 <code>getColor()</code> 함수 안에서 <code>using enum Color;</code> 라는 문장을 적어주었기 때문에, 이제 <code>Color::</code> 라는 접두사 없이 값들에 접근할 수 있게 되었습니다.</p>
<p>덕분에 <code>switch</code> 문 안에서 길고 뻔한 접두사를 여러 번 반복해서 써야 하는 수고를 크게 덜 수 있습니다.</p>
<hr>
<h2 id="137--구조체-멤버-그리고-멤버-선택-소개">13.7 — 구조체, 멤버, 그리고 멤버 선택 소개</h2>
<p>프로그래밍을 하다 보면 어떤 대상을 표현하기 위해 여러 개의 변수가 필요한 경우가 많습니다. 
이전 장(12.1 -- 복합 데이터 타입 소개)에서 이야기했듯이, 분수는 분자와 분모가 하나로 연결되어 하나의 수학적 객체를 이룹니다.</p>
<p>또 다른 예로, 회사 직원의 정보를 저장하는 프로그램을 만든다고 가정해 봅시다. 
직원의 이름, 직함, 나이, 사원 번호, 매니저 번호, 급여, 생일, 입사일 같은 다양한 정보들을 관리해야 할 것입니다.</p>
<p>만약 이 모든 정보를 독립적인 개별 변수로 관리한다면, 다음과 같은 형태가 될 것입니다.</p>
<pre><code class="language-cpp">std::string name;
std::string title;
int age;
int id;
int managerId;
double wage;
int birthdayYear;
int birthdayMonth;
int birthdayDay;
int hireYear;
int hireMonth;
int hireDay;
</code></pre>
<p>하지만 이렇게 개별 변수를 사용하는 방식에는 몇 가지 문제가 있습니다. 
첫째, 이 변수들이 서로 관련되어 있는지 한눈에 알기 어렵습니다(주석을 읽거나 코드 문맥을 파악해야만 알 수 있죠). 
둘째, 당장 관리해야 할 변수가 12개나 됩니다. 만약 이 직원 정보를 함수에 전달하려면 12개의 인수를 순서에 맞게 넘겨줘야 하는데, 이렇게 되면 함수 원형과 호출 코드가 아주 지저분해집니다. 게다가 함수는 오직 하나의 값만 반환할 수 있는데, 어떻게 직원 정보 전체를 반환할 수 있을까요?</p>
<p>만약 직원이 한 명이 아니라면 어떨까요? 직원이 한 명 추가될 때마다 12개의 변수를 새로 만들고, 각각 고유한 이름을 붙여줘야 합니다! 이는 전혀 효율적이지 않습니다. 우리에게 정말 필요한 것은 이렇게 서로 관련된 데이터들을 모아서 한 번에 쉽게 관리할 수 있는 방법입니다.</p>
<p>다행히도 C++에는 이런 문제를 해결하기 위해 고안된 두 가지 복합 데이터 타입이 있습니다. 바로 지금부터 알아볼 <strong>구조체(struct)</strong> 와 곧 배우게 될 <strong>클래스(class)</strong> 입니다. <strong>구조체(structure의 줄임말)</strong> 는 프로그래머가 직접 정의하는 데이터 타입(13.1 -- 프로그램 정의(사용자 정의) 타입 소개)으로, 여러 개의 변수를 하나의 타입으로 묶을 수 있게 해줍니다. 곧 보시겠지만, 이를 통해 관련된 변수 묶음을 훨씬 쉽게 관리할 수 있습니다!</p>
<blockquote>
<p><strong>참고 사항</strong>
구조체는 클래스 타입의 일종입니다(클래스와 공용체도 마찬가지입니다). 
따라서 클래스 타입에 적용되는 모든 규칙은 구조체에도 동일하게 적용됩니다.</p>
</blockquote>
<hr>
<h3 id="구조체-정의하기">구조체 정의하기</h3>
<p>구조체는 프로그래머가 정의하는 타입이므로, 사용하기 전에 먼저 컴파일러에게 이 구조체가 어떻게 생겼는지 알려주어야 합니다. 
다음은 간단한 직원 정보를 담는 구조체를 정의한 예시입니다.</p>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage {};
};
</code></pre>
<p><code>struct</code> 키워드는 컴파일러에게 우리가 구조체를 정의하고 있다는 것을 알려줍니다. 
여기서는 구조체의 이름을 <code>Employee</code>라고 지었습니다(프로그램에서 정의하는 타입은 보통 대문자로 시작하는 이름을 사용합니다).</p>
<p>그런 다음 중괄호 <code>{}</code> 안에 각 <code>Employee</code> 객체가 가질 변수들을 정의합니다. 이 예시에서 우리가 만들 각 <code>Employee</code>는 <code>int id</code>, <code>int age</code>, <code>double wage</code>라는 3개의 변수를 가집니다. 이렇게 구조체의 일부로 포함된 변수들을 <strong>데이터 멤버(data members)</strong> 또는 <strong>멤버 변수(member variables)</strong> 라고 부릅니다.</p>
<blockquote>
<p><strong>팁</strong> 
일상생활에서 멤버(회원)는 어떤 그룹에 속한 개인을 뜻합니다. 
예를 들어, 여러분은 농구팀의 멤버일 수 있고, 동생은 합창단의 멤버일 수 있죠.</p>
<p>C++에서 <strong>멤버(member)</strong> 란 구조체(또는 클래스)에 속하는 변수, 함수, 또는 타입을 의미합니다. 
모든 멤버는 반드시 구조체(또는 클래스) 정의 안에 선언되어야 합니다.
앞으로의 강의에서 <strong>멤버</strong> 라는 단어를 아주 많이 사용할 테니, 그 의미를 꼭 기억해 두세요.</p>
</blockquote>
<p>일반 변수를 값 초기화할 때 빈 중괄호를 사용하는 것처럼, 각 멤버 변수 뒤에 있는 빈 중괄호는 <code>Employee</code> 객체가 생성될 때 내부의 멤버 변수들이 기본값으로 초기화되도록 보장해 줍니다. 이에 대해서는 몇 단원 뒤에 나오는 기본 멤버 초기화(13.9 -- 기본 멤버 초기화)에서 더 자세히 다루겠습니다.</p>
<p>마지막으로, 타입 정의의 끝은 항상 세미콜론(<code>;</code>)으로 맺어야 합니다.
다시 한 번 말씀드리지만, <code>Employee</code>는 단지 타입을 정의한 것일 뿐, 아직 실제로 어떤 객체가 생성된 것은 아닙니다.</p>
<hr>
<h3 id="구조체-객체-정의하기">구조체 객체 정의하기</h3>
<p><code>Employee</code> 타입을 사용하려면, 간단히 <code>Employee</code> 타입의 변수를 정의하면 됩니다.</p>
<pre><code class="language-cpp">Employee joe {}; // Employee는 타입이고, joe는 변수 이름입니다.
</code></pre>
<p>위 코드는 이름이 <code>joe</code>인 <code>Employee</code> 타입의 변수를 정의합니다. 코드가 실행되면 3개의 데이터 멤버를 포함하는 <code>Employee</code> 객체가 생성(인스턴스화)됩니다. 빈 중괄호는 객체가 값으로 초기화되도록 보장합니다.</p>
<p>다른 타입들과 마찬가지로, 같은 구조체 타입의 변수를 여러 개 정의하는 것도 가능합니다.</p>
<pre><code class="language-cpp">Employee joe {}; // Joe를 위한 Employee 구조체 생성
Employee frank {}; // Frank를 위한 Employee 구조체 생성
</code></pre>
<hr>
<h3 id="멤버-접근하기">멤버 접근하기</h3>
<p>다음 예시를 살펴보겠습니다.</p>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe {};

    return 0;
}
</code></pre>
<p>위 예시에서 <code>joe</code>라는 이름은 전체 구조체 객체(멤버 변수들을 포함하는)를 가리킵니다. 특정 멤버 변수에 접근하려면, 구조체 변수 이름과 멤버 이름 사이에 <strong>멤버 선택 연산자(.)</strong> 를 사용합니다. 예를 들어 Joe의 나이 멤버에 접근하려면 <code>joe.age</code>라고 작성하면 됩니다.</p>
<p>구조체의 멤버 변수는 일반 변수와 똑같이 작동합니다. 따라서 대입, 산술 연산, 비교 연산 등 일반적인 작업들을 모두 수행할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe {};

    joe.age = 32;  // 멤버 선택 연산자(.)를 사용하여 변수 joe의 age 멤버를 선택합니다.

    std::cout &lt;&lt; joe.age &lt;&lt; &#39;\n&#39;; // joe의 나이를 출력합니다.

    return 0;
}
</code></pre>
<p>위 코드는 다음을 출력합니다:
<code>32</code></p>
<p>구조체의 가장 큰 장점 중 하나는 구조체 변수당 오직 하나의 새로운 이름만 만들면 된다는 것입니다(멤버 변수의 이름들은 구조체 타입 정의의 일부로 이미 고정되어 있습니다). 다음 예시에서는 <code>joe</code>와 <code>frank</code>라는 두 개의 <code>Employee</code> 객체를 생성해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe {};
    joe.id = 14;
    joe.age = 32;
    joe.wage = 60000.0;

    Employee frank {};
    frank.id = 15;
    frank.age = 28;
    frank.wage = 45000.0;

    int totalAge { joe.age + frank.age };
    std::cout &lt;&lt; &quot;Joe and Frank have lived &quot; &lt;&lt; totalAge &lt;&lt; &quot; total years\n&quot;;

    if (joe.wage &gt; frank.wage)
        std::cout &lt;&lt; &quot;Joe makes more than Frank\n&quot;;
    else if (joe.wage &lt; frank.wage)
        std::cout &lt;&lt; &quot;Joe makes less than Frank\n&quot;;
    else
        std::cout &lt;&lt; &quot;Joe and Frank make the same amount\n&quot;;

    // Frank가 승진을 했습니다.
    frank.wage += 5000.0;

    // 오늘은 Joe의 생일입니다.
    ++joe.age; // 전위 증감 연산자를 사용하여 Joe의 나이를 1 증가시킵니다.

    return 0;
}
</code></pre>
<p>위 예시를 보면 어떤 멤버 변수가 Joe의 것이고 어떤 것이 Frank의 것인지 아주 쉽게 구분할 수 있습니다. 이는 개별 변수들을 사용할 때보다 훨씬 더 높은 수준의 체계성을 제공합니다. 게다가 Joe와 Frank의 멤버들이 같은 이름을 공유하기 때문에, 동일한 구조체 타입의 변수가 여러 개 있을 때 일관성을 유지할 수 있습니다.</p>
<p>다음 단원에서는 구조체를 어떻게 초기화하는지 등 구조체에 대해 계속해서 더 깊이 알아보겠습니다.</p>
<hr>
<h2 id="138--구조체-집합체-초기화-struct-aggregate-initialization">13.8 — 구조체 집합체 초기화 (Struct aggregate initialization)</h2>
<p>이전 레슨(13.7 -- 구조체, 멤버, 멤버 선택 소개)에서는 구조체를 정의하고, 구조체 객체를 생성하며, 멤버에 접근하는 방법에 대해 이야기했습니다. 이번 레슨에서는 구조체를 어떻게 초기화하는지 알아보겠습니다.</p>
<hr>
<h3 id="데이터-멤버는-기본적으로-초기화되지-않습니다">데이터 멤버는 기본적으로 초기화되지 않습니다</h3>
<p>일반 변수와 마찬가지로, 데이터 멤버는 기본적으로 초기화되지 않습니다. 다음 구조체를 살펴보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id; // 참고: 여기에 초기화 구문이 없습니다.
    int age;
    double wage;
};

int main()
{
    Employee joe; // 참고: 여기에도 초기화 구문이 없습니다.
    std::cout &lt;&lt; joe.id &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>초기값을 전혀 제공하지 않았기 때문에, <code>joe</code> 가 생성될 때 <code>joe.id</code>, <code>joe.age</code>, <code>joe.wage</code> 는 모두 초기화되지 않은 상태로 남습니다. 따라서 <code>joe.id</code> 의 값을 출력하려고 시도하면 예측할 수 없는 결과(정의되지 않은 동작)가 발생합니다.</p>
<p>하지만 구조체를 초기화하는 방법을 본격적으로 알아보기 전에, 잠시 다른 이야기를 먼저 해보겠습니다.</p>
<hr>
<h3 id="집합체aggregate란-무엇일까요">집합체(Aggregate)란 무엇일까요?</h3>
<p>일반적인 프로그래밍에서 <strong>집합체 데이터 타입(Aggregate data type)</strong> (또는 간단히 <strong>집합체(Aggregate)</strong> ) 이란 여러 개의 데이터 멤버를 포함할 수 있는 모든 타입을 뜻합니다. 어떤 집합체는 멤버들이 서로 다른 타입(예: 구조체)을 가질 수 있고, 어떤 집합체는 모든 멤버가 반드시 동일한 타입(예: 배열)이어야 합니다.</p>
<p>C++에서 집합체의 정의는 이보다 좀 더 좁고 상당히 복잡한 편입니다.</p>
<blockquote>
<p><strong>저자의 참고 사항</strong>
이 튜토리얼 시리즈에서 &quot;집합체&quot; (또는 &quot;비집합체&quot;) 라는 용어를 사용할 때는 C++에서의 집합체 정의를 의미합니다.</p>
</blockquote>
<blockquote>
<p><strong>고급 독자를 위한 참고 사항</strong>
간단히 말해서, C++의 집합체는 C스타일 배열(17.7 -- C스타일 배열 소개)이거나, 다음 조건을 만족하는 클래스 타입(구조체, 클래스, 공용체)입니다.</p>
<ul>
<li>사용자가 선언한 생성자가 없음 (14.9 -- 생성자 소개)</li>
<li>private 또는 protected인 비정적(non-static) 데이터 멤버가 없음 (14.5 -- Public 및 private 멤버와 접근 지정자)</li>
<li>가상(virtual) 함수가 없음 (25.2 -- 가상 함수와 다형성)</li>
</ul>
<p>널리 쓰이는 <code>std::array</code> (17.1 -- std::array 소개) 타입 역시 집합체입니다. C++ 집합체의 정확한 정의는 여기서 확인하실 수 있습니다.</p>
</blockquote>
<p>지금 단계에서 이해해야 할 핵심은 <strong>데이터 멤버만 있는 구조체는 집합체</strong> 라는 사실입니다.</p>
<hr>
<h3 id="구조체의-집합체-초기화">구조체의 집합체 초기화</h3>
<p>일반 변수는 단일 값만 가질 수 있으므로 초기값도 하나만 제공하면 됩니다.</p>
<pre><code class="language-cpp">int x { 5 };
</code></pre>
<p>하지만 구조체는 여러 멤버를 가질 수 있습니다.</p>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage {};
};
</code></pre>
<p>구조체 타입으로 객체를 정의할 때는, 초기화 시점에 여러 멤버를 동시에 초기화할 방법이 필요합니다.</p>
<pre><code class="language-cpp">Employee joe; // joe.id, joe.age, joe.wage를 어떻게 초기화할까요?
</code></pre>
<p>집합체는 <strong>집합체 초기화(Aggregate initialization)</strong> 라는 방식을 사용하여 멤버들을 직접 초기화할 수 있습니다. 
이를 위해 중괄호 안에 쉼표로 값을 나열한 <strong>초기화 리스트(Initializer list)</strong> 를 제공하면 됩니다.</p>
<p>집합체 초기화에는 주로 두 가지 형태가 있습니다.</p>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee frank = { 1, 32, 60000.0 }; // 중괄호 리스트를 사용한 복사 리스트 초기화
    Employee joe { 2, 28, 45000.0 };     // 중괄호 리스트를 사용한 리스트 초기화 (권장 방식)

    return 0;
}
</code></pre>
<p>이러한 초기화 방식은 각각 <strong>멤버별 초기화(Memberwise initialization)</strong> 를 수행합니다. 
즉, 구조체에 선언된 순서대로 각 멤버가 초기화된다는 뜻입니다. 따라서 <code>Employee joe { 2, 28, 45000.0 };</code> 코드는 먼저 <code>joe.id</code> 를 <code>2</code>로, 그 다음 <code>joe.age</code> 를 <code>28</code>로, 마지막으로 <code>joe.wage</code> 를 <code>45000.0</code>으로 초기화합니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
집합체를 초기화할 때는 (복사가 아닌) 중괄호 리스트 형태를 사용하는 것을 권장합니다.</p>
</blockquote>
<p>C++20부터는 괄호로 묶인 값 목록을 사용하여 (일부) 집합체를 초기화할 수도 있습니다.</p>
<pre><code class="language-cpp">Employee robert ( 3, 45, 62500.0 );  // 괄호 리스트를 사용한 직접 초기화 (C++20)
</code></pre>
<p>이 마지막 방식은 가급적 피하는 것이 좋습니다. 
중괄호 생략(brace elision)을 활용하는 집합체(특히 <code>std::array</code>)에서는 현재 이 문법이 제대로 작동하지 않기 때문입니다.</p>
<hr>
<h3 id="초기화-리스트에서-초기값이-누락된-경우">초기화 리스트에서 초기값이 누락된 경우</h3>
<p>집합체를 초기화할 때 입력한 값의 개수가 멤버 수보다 적다면, 명시적인 초기값이 없는 멤버들은 다음과 같이 초기화됩니다.</p>
<ul>
<li>멤버에 기본 초기값이 정의되어 있다면, 그 값을 사용합니다.</li>
<li>그렇지 않다면, 해당 멤버는 빈 초기화 리스트로부터 복사 초기화됩니다. 
대부분의 경우, 이 과정에서 해당 멤버들은 <strong>값 초기화</strong> 가 됩니다 
(클래스 타입의 경우, 리스트 생성자가 있더라도 기본 생성자가 호출됩니다).</li>
</ul>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage { 76000.0 };
    double whatever;
};

int main()
{
    Employee joe { 2, 28 }; // joe.whatever는 0.0으로 값 초기화가 됩니다.

    return 0;
}
</code></pre>
<p>위 예제에서 <code>joe.id</code> 는 <code>2</code>로, <code>joe.age</code> 는 <code>28</code>로 초기화됩니다. <code>joe.wage</code> 는 명시적인 초기값이 주어지지 않았지만 구조체 내부에 기본 초기값이 지정되어 있으므로 <code>76000.0</code>으로 초기화됩니다. 마지막으로 <code>joe.whatever</code> 역시 명시적인 초기값이 주어지지 않았으므로 <code>0.0</code>으로 값 초기화가 됩니다.</p>
<blockquote>
<p><strong>팁 (Tip)</strong>
이는 곧 빈 초기화 리스트를 사용하면 구조체의 모든 멤버를 한 번에 값 초기화할 수 있다는 뜻입니다.
<code>Employee joe {}; // 모든 멤버를 값 초기화합니다.</code></p>
</blockquote>
<hr>
<h3 id="구조체를-출력하기-위한-operator-오버로딩">구조체를 출력하기 위한 operator&lt;&lt; 오버로딩</h3>
<p>이전 13.5 레슨(I/O 연산자 오버로딩 소개)에서는 열거형(enum)을 출력하기 위해 <code>operator&lt;&lt;</code> 를 오버로딩하는 방법을 보여드렸습니다. 
구조체를 위해 <code>operator&lt;&lt;</code> 를 오버로딩하는 것 역시 매우 유용합니다.</p>
<p>이전 섹션과 동일한 예제에 오버로딩된 <code>operator&lt;&lt;</code> 를 추가해 보았습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, const Employee&amp; e)
{
    out &lt;&lt; e.id &lt;&lt; &#39; &#39; &lt;&lt; e.age &lt;&lt; &#39; &#39; &lt;&lt; e.wage;
    return out;
}

int main()
{
    Employee joe { 2, 28 }; // joe.wage는 0.0으로 값 초기화가 됩니다.
    std::cout &lt;&lt; joe &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>출력 결과는 다음과 같습니다.</p>
<pre><code class="language-text">2 28 0
</code></pre>
<p><code>joe.wage</code> 가 실제로 <code>0.0</code>으로 값 초기화된 것을 확인할 수 있습니다 (출력은 <code>0</code>으로 나옵니다).</p>
<p>열거형과 달리 구조체는 여러 값을 담을 수 있습니다. 출력 형식(예: 값을 구분하는 방법)은 전적으로 여러분의 자유입니다.</p>
<p>위에서 우리가 작성한 <code>operator&lt;&lt;</code> 가 출력하는 세 개의 값은 직관적이지 않습니다. 이 값들이 무엇을 의미하는지 전혀 알 수 없기 때문입니다. 동일한 예제에서 출력 함수가 좀 더 친절하게 설명해 주도록 업데이트해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, const Employee&amp; e)
{
    out &lt;&lt; &quot;id: &quot; &lt;&lt; e.id &lt;&lt; &quot; age: &quot; &lt;&lt; e.age &lt;&lt; &quot; wage: &quot; &lt;&lt; e.wage;
    return out;
}

int main()
{
    Employee joe { 2, 28 }; // joe.wage는 0.0으로 값 초기화가 됩니다.
    std::cout &lt;&lt; joe &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이제 출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">id: 2 age: 28 wage: 0
</code></pre>
<p>훨씬 이해하기 쉬워졌습니다.</p>
<hr>
<h3 id="const-구조체">Const 구조체</h3>
<p>구조체 타입의 변수도 <code>const</code> (또는 <code>constexpr</code>)가 될 수 있으며, 다른 모든 <code>const</code> 변수와 마찬가지로 반드시 초기화되어야 합니다.</p>
<pre><code class="language-cpp">struct Rectangle
{
    double length {};
    double width {};
};

int main()
{
    const Rectangle unit { 1.0, 1.0 };
    const Rectangle zero { }; // 모든 멤버를 값 초기화합니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="지정-초기자-designated-initializers-c20">지정 초기자 (Designated initializers) (C++20)</h3>
<p>값 리스트를 통해 구조체를 초기화할 때, 초기값들은 선언된 순서대로 멤버에 차례차례 적용됩니다.</p>
<pre><code class="language-cpp">struct Foo
{
    int a {};
    int c {};
};

int main()
{
    Foo f { 1, 3 }; // f.a = 1, f.c = 3

    return 0;
}
</code></pre>
<p>만약 이 구조체 정의를 업데이트해서 마지막이 아닌 중간 위치에 새로운 멤버를 추가한다면 어떤 일이 벌어질지 생각해 보세요.</p>
<pre><code class="language-cpp">struct Foo
{
    int a {};
    int b {}; // 방금 추가됨
    int c {};
};

int main()
{
    Foo f { 1, 3 }; // 이제 f.a = 1, f.b = 3, f.c = 0이 됩니다.

    return 0;
}
</code></pre>
<p>이제 모든 초기값이 뒤로 밀려버렸습니다. 
더 큰 문제는 문법 자체는 여전히 유효하기 때문에 컴파일러가 이를 에러로 감지하지 못할 수도 있다는 점입니다.</p>
<p>이런 문제를 피하기 위해 C++20에서는 구조체 멤버를 초기화하는 새로운 방법인 <strong>지정 초기자(Designated initializers)</strong> 를 추가했습니다. 
지정 초기자를 사용하면 어떤 초기값이 어떤 멤버와 연결되는지 명확하게 지정할 수 있습니다. 
각 멤버는 리스트 또는 복사 초기화를 사용하여 초기화할 수 있으며, 구조체에 선언된 순서와 동일한 순서로 초기화해야 합니다. 
그렇지 않으면 경고나 에러가 발생합니다. 명시적으로 지정되지 않은 멤버는 값 초기화가 됩니다.</p>
<pre><code class="language-cpp">struct Foo
{
    int a{ };
    int b{ };
    int c{ };
};

int main()
{
    Foo f1{ .a{ 1 }, .c{ 3 } }; // 정상: f1.a = 1, f1.b = 0 (값 초기화됨), f1.c = 3
    Foo f2{ .a = 1, .c = 3 };   // 정상: f2.a = 1, f2.b = 0 (값 초기화됨), f2.c = 3
    Foo f3{ .b{ 2 }, .a{ 1 } }; // 에러: 초기화 순서가 구조체에 선언된 순서와 일치하지 않습니다.

    return 0;
}
</code></pre>
<blockquote>
<p><strong>Clang 사용자를 위한 참고 사항</strong>
중괄호를 사용하여 단일 값의 지정 초기자를 작성할 때, Clang 컴파일러가 &quot;스칼라 초기화 주변에 중괄호가 있습니다(braces around scalar initializer)&quot;라는 경고를 부적절하게 표시하는 경우가 있습니다. 곧 수정되기를 바랍니다.</p>
</blockquote>
<p>지정 초기자는 코드만 보고도 어떤 값이 들어가는지 쉽게 알 수 있게 해주고(자체 문서화), 초기값의 순서를 실수로 섞는 것을 방지해주기 때문에 아주 유용합니다. 하지만 초기화 리스트를 눈에 띄게 길고 복잡하게 만들기도 하므로, 현 시점에서는 이를 1순위 모범 사례로 권장하지는 않습니다.</p>
<p>또한, 집합체를 초기화할 때마다 지정 초기자를 항상 사용하도록 강제하는 규칙이 없기 때문에, 초기값이 밀리는 위험을 막으려면 기존 집합체 정의의 중간에 새로운 멤버를 추가하는 것은 피하는 것이 좋습니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
집합체에 새 멤버를 추가할 때는 다른 멤버의 초기값이 밀리지 않도록 정의 목록의 맨 아래에 추가하는 것이 가장 안전합니다.</p>
</blockquote>
<hr>
<h3 id="초기화-리스트를-사용한-할당-assignment">초기화 리스트를 사용한 할당 (Assignment)</h3>
<p>이전 레슨에서 보여드린 것처럼, 구조체의 각 멤버에 개별적으로 값을 할당할 수 있습니다.</p>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe { 1, 32, 60000.0 };

    joe.age  = 33;      // Joe의 생일이 지났습니다.
    joe.wage = 66000.0; // 그리고 급여가 올랐습니다.

    return 0;
}
</code></pre>
<p>하나의 멤버만 변경할 때는 괜찮지만, 여러 멤버를 동시에 업데이트하고 싶을 때는 좋은 방법이 아닙니다. 초기화 리스트로 구조체를 초기화하는 것과 비슷하게, 초기화 리스트를 사용하여 구조체에 한 번에 값을 할당할 수도 있습니다 (이 역시 멤버별 할당을 수행합니다).</p>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe { 1, 32, 60000.0 };
    joe = { joe.id, 33, 66000.0 }; // Joe의 생일이 지났고 급여가 올랐습니다.

    return 0;
}
</code></pre>
<p>여기서 <code>joe.id</code> 의 값은 변경하고 싶지 않았기 때문에, 멤버별 할당 과정에서 자기 자신의 값을 그대로 유지할 수 있도록 리스트에 기존 <code>joe.id</code> 의 값을 자리 표시자(placeholder)로 다시 적어주어야 했습니다. 모양새가 조금 번거롭고 예쁘지 않죠.</p>
<hr>
<h3 id="지정-초기자를-사용한-할당-c20">지정 초기자를 사용한 할당 (C++20)</h3>
<p>리스트 할당에서도 지정 초기자를 사용할 수 있습니다.</p>
<pre><code class="language-cpp">struct Employee
{
    int id {};
    int age {};
    double wage {};
};

int main()
{
    Employee joe { 1, 32, 60000.0 };
    joe = { .id = joe.id, .age = 33, .wage = 66000.0 }; // Joe의 생일이 지났고 급여가 올랐습니다.

    return 0;
}
</code></pre>
<p>이러한 할당 과정에서 명시적으로 지정되지 않은 멤버들은 값 초기화가 될 때 사용되는 기본값으로 덮어쓰기 됩니다. 
만약 우리가 <code>joe.id</code> 에 지정 초기자를 써주지 않았다면, <code>joe.id</code> 에는 <code>0</code>이 할당되었을 것입니다.</p>
<hr>
<h3 id="같은-타입의-다른-구조체로-구조체-초기화하기">같은 타입의 다른 구조체로 구조체 초기화하기</h3>
<p>구조체는 동일한 타입의 다른 구조체를 사용하여 초기화될 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Foo
{
    int a{};
    int b{};
    int c{};
};

std::ostream&amp; operator&lt;&lt;(std::ostream&amp; out, const Foo&amp; f)
{
    out &lt;&lt; f.a &lt;&lt; &#39; &#39; &lt;&lt; f.b &lt;&lt; &#39; &#39; &lt;&lt; f.c;
    return out;
}

int main()
{
    Foo foo { 1, 2, 3 };

    Foo x = foo; // 복사 초기화
    Foo y(foo);  // 직접 초기화
    Foo z {foo}; // 직접 리스트 초기화

    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; z &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위 코드의 출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">1 2 3
1 2 3
1 2 3
</code></pre>
<p>이 방식은 앞서 배웠던 집합체 초기화가 아니라, 우리가 이미 익숙한 표준 초기화 형태(복사, 직접 또는 직접 리스트 초기화)를 사용한다는 점을 기억하세요.</p>
<p>이러한 패턴은 동일한 타입의 구조체를 반환하는 함수의 결과값으로 구조체를 초기화할 때 가장 자주 볼 수 있습니다. 이 부분은 13.10 레슨(구조체 전달 및 반환)에서 더 자세히 다루도록 하겠습니다.</p>
<hr>
<h2 id="139--기본-멤버-초기화-default-member-initialization">13.9 — 기본 멤버 초기화 (Default member initialization)</h2>
<p>구조체(또는 클래스) 타입을 정의할 때, 타입 정의의 일부로 각 멤버에 대한 기본 초기화 값을 지정할 수 있습니다. 
<code>static</code>으로 표시되지 않은 멤버의 경우, 이 과정을 <strong>비정적 멤버 초기화</strong> 라고 부르기도 합니다.
이때 사용하는 초기화 값을 <strong>기본 멤버 초기화 값</strong> 이라고 합니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
정적 멤버와 정적 멤버 초기화는 &#39;15.6 -- 정적 멤버 변수&#39; 단원에서 다룹니다.</p>
</blockquote>
<p>다음은 그 예시입니다.</p>
<pre><code class="language-cpp">struct Something{
    int x;       // 초기화 값 없음 (나쁨)
    int y {};    // 기본적으로 값 초기화됨
    int z { 2 }; // 명시적인 기본값
};

int main(){
    Something s1; // s1.x는 초기화되지 않음, s1.y는 0, s1.z는 2임

    return 0;
}
</code></pre>
<p>위 <code>Something</code> 구조체 정의에서 <code>x</code>는 기본값이 없고, <code>y</code>는 기본적으로 값 초기화되며, <code>z</code>는 <code>2</code>라는 기본값을 가집니다. 
사용자가 <code>Something</code> 타입의 객체를 만들 때 직접 초기화 값을 주지 않으면, 이 기본 멤버 초기화 값들이 대신 사용됩니다.</p>
<p>우리가 만든 <code>s1</code> 객체는 별도의 초기화 값을 가지지 않으므로, <code>s1</code>의 멤버들은 기본값으로 초기화됩니다. 
<code>s1.x</code>는 기본 초기화 값이 없어서 초기화되지 않은(쓰레기 값이 들어있는) 상태로 남습니다. 
<code>s1.y</code>는 기본적으로 값 초기화가 진행되어 <code>0</code>이 됩니다. 그리고 <code>s1.z</code>는 <code>2</code>로 초기화됩니다.</p>
<p><code>s1.z</code>에 명시적인 초기화 값을 직접 주지 않았음에도 불구하고, 
미리 제공된 기본 멤버 초기화 값 덕분에 0이 아닌 값으로 초기화된다는 점을 기억해 두세요.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
기본 멤버 초기화(또는 나중에 다룰 다른 방법들)를 사용하면, 명시적인 초기화 값을 주지 않아도 구조체와 클래스가 스스로 초기화될 수 있습니다!</p>
</blockquote>
<blockquote>
<p><strong>고급 독자를 위한 참고</strong>
CTAD(&#39;13.14 -- 클래스 템플릿 인수 추론(CTAD)과 추론 가이드&#39;에서 다룸)는 비정적 멤버 초기화에 사용할 수 없습니다.</p>
</blockquote>
<hr>
<h3 id="명시적-초기화-값이-기본값보다-우선합니다">명시적 초기화 값이 기본값보다 우선합니다</h3>
<p>초기화 리스트에서 사용자가 명시적으로 지정한 값은 항상 기본 멤버 초기화 값보다 우선해서 적용됩니다.</p>
<pre><code class="language-cpp">struct Something{
    int x;       // 기본 초기화 값 없음 (나쁨)
    int y {};    // 기본적으로 값 초기화됨
    int z { 2 }; // 명시적인 기본값
};

int main(){
    Something s2 { 5, 6, 7 }; // s2.x, s2.y, s2.z에 명시적 초기화 값 사용 (기본값은 사용되지 않음)

    return 0;
}
</code></pre>
<p>위의 경우 <code>s2</code>는 모든 멤버에 대해 직접 명시적인 초기화 값을 입력했으므로, 기본 멤버 초기화 값은 전혀 사용되지 않습니다. 
즉, <code>s2.x</code>, <code>s2.y</code>, <code>s2.z</code>는 각각 <code>5</code>, <code>6</code>, <code>7</code>로 초기화됩니다.</p>
<hr>
<h3 id="기본값이-있을-때-초기화-리스트의-값이-생략된-경우">기본값이 있을 때 초기화 리스트의 값이 생략된 경우</h3>
<p>이전 단원(&#39;13.8 -- 구조체 집합체 초기화&#39;)에서, 집합체를 초기화할 때 제공된 값이 멤버의 수보다 적으면 나머지 멤버들은 모두 값 초기화(0으로 초기화)된다고 배웠습니다. 하지만 특정 멤버에 기본 멤버 초기화 값이 이미 설정되어 있다면, 값 초기화 대신 그 기본값이 사용됩니다.</p>
<pre><code class="language-cpp">struct Something{
    int x;       // 기본 초기화 값 없음 (나쁨)
    int y {};    // 기본적으로 값 초기화됨
    int z { 2 }; // 명시적인 기본값
};

int main(){
    Something s3 {}; // s3.x는 값 초기화, s3.y와 s3.z는 기본값 사용

    return 0;
}
</code></pre>
<p>위의 경우 <code>s3</code>는 빈 리스트 <code>{}</code> 로 초기화되었기 때문에 초기화 값이 모두 생략되었습니다. 이는 기본 멤버 초기화 값이 존재하면 그것을 사용하고, 없다면 값 초기화를 진행한다는 것을 의미합니다. 따라서 (기본 초기화 값이 없는) <code>s3.x</code>는 <code>0</code>으로 값 초기화되고, <code>s3.y</code>는 기본 설정에 따라 <code>0</code>으로 값 초기화되며, <code>s3.z</code>는 설정된 기본값인 <code>2</code>로 초기화됩니다.</p>
<hr>
<h3 id="초기화-경우의-수-요약">초기화 경우의 수 요약</h3>
<p>집합체가 <strong>초기화 리스트</strong> 와 함께 정의된 경우:</p>
<ol>
<li>명시적인 초기화 값이 있으면, 그 명시적 값을 사용합니다.</li>
<li>초기화 값이 생략되었고 기본 멤버 초기화 값이 있다면, 기본값을 사용합니다.</li>
<li>초기화 값이 생략되었고 기본 멤버 초기화 값도 없다면, 값 초기화가 일어납니다.</li>
</ol>
<p>집합체가 초기화 리스트 없이 정의된 경우:</p>
<ol>
<li>기본 멤버 초기화 값이 있으면, 기본값을 사용합니다.</li>
<li>기본 멤버 초기화 값이 없으면, 멤버는 초기화되지 않은 상태로 남습니다.</li>
</ol>
<p>멤버는 항상 선언된 순서대로 초기화됩니다. 다음 예제는 모든 경우의 수를 보여줍니다:</p>
<pre><code class="language-cpp">struct Something{
    int x;       // 기본 초기화 값 없음 (나쁨)
    int y {};    // 기본적으로 값 초기화됨
    int z { 2 }; // 명시적인 기본값
};

int main(){
    Something s1;             // 초기화 리스트 없음: s1.x는 초기화되지 않음, s1.y와 s1.z는 기본값 사용
    Something s2 { 5, 6, 7 }; // 명시적 초기화: s2.x, s2.y, s2.z는 명시적 값 사용 (기본값은 사용되지 않음)
    Something s3 {};          // 초기화 값 생략: s3.x는 값 초기화됨, s3.y와 s3.z는 기본값 사용

    return 0;
}
</code></pre>
<p>우리가 가장 주의해야 할 부분은 <code>s1.x</code>입니다. <code>s1</code>은 초기화 리스트가 없고 <code>x</code>는 기본 멤버 초기화 값도 없기 때문에, <code>s1.x</code>는 초기화되지 않은 상태로 남습니다 (변수는 항상 초기화해야 하므로 이는 좋지 않은 코드입니다).</p>
<hr>
<h3 id="멤버에-항상-기본값을-제공하세요">멤버에 항상 기본값을 제공하세요</h3>
<p>멤버가 초기화되지 않은 상태로 남는 것을 방지하려면, 단순히 각 멤버가 기본값(명시적인 숫자 형태든 빈 중괄호 <code>{}</code> 든)을 가지도록 만들어주면 됩니다. 이렇게 하면 초기화 리스트를 제공하든 안 하든 상관없이 모든 멤버가 어떤 값으로든 안전하게 초기화됩니다.</p>
<p>모든 멤버가 기본값을 가지고 있는 다음 구조체를 살펴보세요:</p>
<pre><code class="language-cpp">struct Fraction{
    int numerator { }; // 여기에 { 0 }을 사용해야 하지만, 예제를 위해 대신 값 초기화를 사용하겠습니다.
    int denominator { 1 };
};

int main(){
    Fraction f1;          // f1.numerator는 0으로 값 초기화, f1.denominator는 1을 기본값으로 사용
    Fraction f2 {};       // f2.numerator는 0으로 값 초기화, f2.denominator는 1을 기본값으로 사용
    Fraction f3 { 6 };    // f3.numerator는 6으로 초기화, f3.denominator는 1을 기본값으로 사용
    Fraction f4 { 5, 8 }; // f4.numerator는 5로 초기화, f4.denominator는 8로 초기화

    return 0;
}
</code></pre>
<p>어떤 방식으로 객체를 생성하든 우리의 멤버들은 안전하게 값으로 초기화됩니다.</p>
<blockquote>
<p><strong>권장 사항 (Best practice)</strong> 
모든 멤버에 기본값을 제공하세요. 
변수를 정의할 때 초기화 리스트가 빠져 있더라도 멤버가 초기화되도록 보장해 줍니다.</p>
</blockquote>
<hr>
<h3 id="집합체에서-기본-초기화default-initialization-vs-값-초기화value-initialization">집합체에서 기본 초기화(Default initialization) vs 값 초기화(Value initialization)</h3>
<p>위 예제의 두 줄을 다시 살펴보겠습니다:</p>
<pre><code class="language-cpp">Fraction f1;          // f1.numerator는 0으로 값 초기화, f1.denominator는 1을 기본값으로 사용
Fraction f2 {};       // f2.numerator는 0으로 값 초기화, f2.denominator는 1을 기본값으로 사용
</code></pre>
<p><code>f1</code>은 (아무것도 없는) 기본 초기화가 되었고, <code>f2</code>는 (빈 중괄호 <code>{}</code> 를 사용한) 값 초기화가 되었지만, 결과는 같다는 것(<code>numerator</code>는 <code>0</code>, <code>denominator</code>는 <code>1</code>로 초기화됨)을 눈치채셨을 것입니다. 그렇다면 어느 것을 선택해야 할까요?</p>
<p>값 초기화 방식(<code>f2</code>)이 더 안전합니다. 왜냐하면 기본값이 지정되지 않은 멤버가 있더라도 확실하게 값 초기화(0으로 초기화) 되도록 보장해주기 때문입니다. (물론 항상 멤버에 기본값을 제공하는 것이 원칙이지만, 혹시라도 하나를 빼먹었을 경우를 대비한 훌륭한 안전망이 됩니다.)</p>
<p>값 초기화를 선호해야 하는 또 다른 장점이 있습니다. 바로 다른 타입의 객체를 초기화하는 방식과 일관성이 생긴다는 점입니다. 일관된 코딩 스타일은 오류를 예방하는 데 큰 도움이 됩니다.</p>
<blockquote>
<p><strong>권장 사항 (Best practice)</strong> &gt; 집합체의 경우, (중괄호가 없는) 기본 초기화보다 (빈 중괄호를 사용하는) 값 초기화를 더 선호하세요.</p>
</blockquote>
<p>그렇긴 하지만, 프로그래머들이 클래스 타입에 대해 값 초기화 대신 기본 초기화를 사용하는 모습도 흔하게 볼 수 있습니다. 이는 역사적인 이유(값 초기화는 C++11부터 도입됨) 때문이기도 하고, 비집합체(non-aggregates)의 특정 상황에서는 기본 초기화가 값 초기화보다 더 효율적으로 동작할 수 있기 때문이기도 합니다 (이 내용은 &#39;14.11 -- 기본 생성자와 기본 인수&#39; 단원에서 다룹니다).</p>
<p>따라서 이 튜토리얼에서 구조체와 클래스에 대한 값 초기화 사용을 강박적으로 강요하지는 않겠지만, 강력하게 권장하는 바입니다.</p>
<hr>
<h2 id="1310--구조체-전달-및-반환하기-passing-and-returning-structs">13.10 — 구조체 전달 및 반환하기 (Passing and returning structs)</h2>
<p>직원의 정보가 3개의 개별 변수로 나뉘어 있다고 상상해 보세요.</p>
<pre><code class="language-cpp">int main()
{
    int id { 1 };
    int age { 24 };
    double wage { 52400.0 };

    return 0;
}
</code></pre>
<p>이 직원 정보를 어떤 함수에 전달하려면, 세 개의 변수를 모두 넘겨주어야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printEmployee(int id, int age, double wage)
{
    std::cout &lt;&lt; &quot;ID:   &quot; &lt;&lt; id &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Age:  &quot; &lt;&lt; age &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Wage: &quot; &lt;&lt; wage &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int id { 1 };
    int age { 24 };
    double wage { 52400.0 };

    printEmployee(id, age, wage);

    return 0;
}
</code></pre>
<p>변수 3개를 따로 전달하는 것은 그럭저럭 괜찮아 보일 수 있습니다. 하지만 직원 변수가 10개나 12개라면 어떨까요? 각각의 변수를 따로 전달하는 것은 시간도 오래 걸리고 실수하기도 쉽습니다. 게다가 직원의 속성(예: 이름)을 새로 추가하게 되면, 그 새로운 매개변수와 데이터를 받기 위해 모든 함수의 선언부, 정의부, 호출부를 일일이 수정해야 합니다!</p>
<hr>
<h3 id="구조체-전달하기-참조-방식">구조체 전달하기 (참조 방식)</h3>
<p>개별 변수 대신 <strong>구조체(Struct)</strong> 를 사용할 때 얻는 가장 큰 장점은, 데이터가 필요한 함수에 구조체 전체를 한 번에 전달할 수 있다는 것입니다. 구조체는 일반적으로 복사본이 생성되어 성능이 떨어지는 것을 막기 위해 <strong>참조(Reference)</strong> (보통 <code>const</code> 참조) 방식으로 전달됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

void printEmployee(const Employee&amp; employee) // 참고: 여기서 참조(reference) 방식으로 전달합니다
{
    std::cout &lt;&lt; &quot;ID:   &quot; &lt;&lt; employee.id &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Age:  &quot; &lt;&lt; employee.age &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Wage: &quot; &lt;&lt; employee.wage &lt;&lt; &#39;\n&#39;;
}

int main()
{
    Employee joe { 14, 32, 24.15 };
    Employee frank { 15, 28, 18.27 };

    // Joe의 정보 출력
    printEmployee(joe);

    std::cout &lt;&lt; &#39;\n&#39;;

    // Frank의 정보 출력
    printEmployee(frank);

    return 0;
}
</code></pre>
<p>위 예제에서 우리는 <code>printEmployee()</code> 함수에 <code>Employee</code> 구조체 전체를 통째로 전달했습니다 (joe를 위해 한 번, frank를 위해 한 번).</p>
<p>위 프로그램의 출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">ID:   14
Age:  32
Wage: 24.15

ID:   15
Age:  28
Wage: 18.27
</code></pre>
<p>개별 멤버가 아닌 구조체 객체 전체를 전달하기 때문에, 구조체 안에 멤버 변수가 아무리 많아도 매개변수는 단 한 개만 필요합니다. 그리고 나중에 <code>Employee</code> 구조체에 새로운 멤버를 추가하기로 결정하더라도, 함수의 선언이나 호출 부분을 바꿀 필요가 없습니다! 새로운 멤버도 자동으로 함께 전달되기 때문입니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
구조체를 언제 값(Value)으로 전달하고, 언제 참조(Reference)로 전달해야 하는지에 대해서는 <strong>레슨 12.6 - const 좌측값 참조로 전달하기</strong> 에서 자세히 다룹니다.</p>
</blockquote>
<hr>
<h3 id="임시-구조체-전달하기">임시 구조체 전달하기</h3>
<p>이전 예제에서는 <code>printEmployee()</code> 함수에 전달하기 전에 <code>joe</code> 라는 이름의 <code>Employee</code> 변수를 먼저 만들었습니다. 이렇게 하면 변수에 이름을 붙일 수 있어 코드를 설명하는 데 유용할 수 있습니다. 하지만 두 줄의 코드(하나는 <code>joe</code>를 만드는 코드, 다른 하나는 <code>joe</code>를 사용하는 코드)가 필요하게 됩니다.</p>
<p>변수를 딱 한 번만 사용할 때는, 굳이 변수에 이름을 붙이고 생성과 사용 과정을 분리하는 것이 코드를 더 복잡하게 만들 수 있습니다. 이런 경우에는 <strong>임시 객체(Temporary object)</strong> 를 사용하는 것이 더 좋을 수 있습니다. 임시 객체는 일반적인 변수가 아니기 때문에 이름(식별자)을 가지지 않습니다.</p>
<p>위와 똑같은 예제이지만, <code>joe</code> 와 <code>frank</code> 변수를 임시 객체로 바꾼 코드입니다:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

void printEmployee(const Employee&amp; employee) // 참고: 여기서 참조(reference) 방식으로 전달합니다
{
    std::cout &lt;&lt; &quot;ID:   &quot; &lt;&lt; employee.id &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Age:  &quot; &lt;&lt; employee.age &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Wage: &quot; &lt;&lt; employee.wage &lt;&lt; &#39;\n&#39;;
}

int main()
{
    // Joe의 정보 출력
    printEmployee(Employee { 14, 32, 24.15 }); // 함수에 전달할 임시 Employee 객체 생성 (타입을 명시적으로 지정함) (권장)

    std::cout &lt;&lt; &#39;\n&#39;;

    // Frank의 정보 출력
    printEmployee({ 15, 28, 18.27 }); // 함수에 전달할 임시 Employee 객체 생성 (매개변수에서 타입을 추론함)

    return 0;
}
</code></pre>
<p>우리는 임시 <code>Employee</code> 객체를 두 가지 방법으로 만들 수 있습니다.</p>
<p>첫 번째 호출에서는 <code>Employee { 14, 32, 24.15 }</code> 문법을 사용했습니다. 이는 컴파일러에게 <code>Employee</code> 객체를 만들고, 우리가 제공한 값들로 초기화하라고 지시하는 것입니다. 이 방식은 우리가 어떤 종류의 임시 객체를 만들고 있는지 명확하게 보여주고, 컴파일러가 우리의 의도를 오해할 일이 없기 때문에 <strong>더 권장되는 문법</strong> 입니다.</p>
<p>두 번째 호출에서는 <code>{ 15, 28, 18.27 }</code> 문법을 사용했습니다. 컴파일러는 똑똑하게도, 함수 호출이 성공하려면 이 값들이 <code>Employee</code> 타입으로 변환되어야 한다는 것을 알아냅니다. 참고로 이 형태는 &#39;암시적 변환(implicit conversion)&#39;으로 간주되므로, 오직 &#39;명시적 변환&#39;만 허용되는 까다로운 상황에서는 작동하지 않습니다.</p>
<blockquote>
<p><strong>관련 내용</strong>
클래스 타입의 임시 객체와 변환에 대해서는 <strong>레슨 14.13 - 임시 클래스 객체</strong> 에서 더 자세히 다룹니다.</p>
</blockquote>
<p>임시 객체에 대해 몇 가지 더 알아둘 점이 있습니다. 임시 객체는 정의되는 순간에 생성 및 초기화되며, 자신이 만들어진 전체 표현식(코드 한 줄)의 실행이 끝날 때 파괴됩니다. 그리고 임시 객체는 &#39;우측값(rvalue)&#39;으로 취급되므로, 우측값을 허용하는 곳에서만 사용할 수 있습니다. 임시 객체가 함수 인수로 사용될 때는, 우측값을 받아들일 수 있는 매개변수와만 연결됩니다. 여기에는 &#39;값에 의한 전달(pass by value)&#39;과 &#39;const 참조에 의한 전달&#39;이 포함되며, &#39;non-const 참조&#39;나 &#39;주소에 의한 전달&#39;은 허용되지 않습니다.</p>
<hr>
<h3 id="구조체-반환하기">구조체 반환하기</h3>
<p>3차원 데카르트 좌표계의 한 점을 반환해야 하는 함수가 있다고 생각해 봅시다. 이 점은 x 좌표, y 좌표, z 좌표라는 3개의 속성을 가집니다. 
하지만 함수는 오직 하나의 값만 반환할 수 있습니다. 그렇다면 3개의 좌표를 모두 사용자에게 반환하려면 어떻게 해야 할까요?</p>
<p>가장 흔한 방법 중 하나는 구조체를 반환하는 것입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Point3d
{
    double x { 0.0 };
    double y { 0.0 };
    double z { 0.0 };
};

Point3d getZeroPoint()
{
    // 변수를 만들어서 반환할 수 있습니다 (아래에서 이 코드를 더 개선해 볼 것입니다)
    Point3d temp { 0.0, 0.0, 0.0 };
    return temp;
}

int main()
{
    Point3d zero{ getZeroPoint() };

    if (zero.x == 0.0 &amp;&amp; zero.y == 0.0 &amp;&amp; zero.z == 0.0)
        std::cout &lt;&lt; &quot;The point is zero\n&quot;;
    else
        std::cout &lt;&lt; &quot;The point is not zero\n&quot;;

    return 0;
}
</code></pre>
<p>이 코드는 다음과 같이 출력합니다:</p>
<pre><code class="language-text">The point is zero
</code></pre>
<p>함수 내부에서 정의된 구조체는 소멸된 참조를 반환하는 문제를 피하기 위해 보통 값(Value)으로 반환됩니다.</p>
<p>위의 <code>getZeroPoint()</code> 함수에서는 오직 값을 반환하기 위한 목적만으로 <code>temp</code> 라는 이름의 객체를 새로 만들었습니다:</p>
<pre><code class="language-cpp">Point3d getZeroPoint()
{
    // 변수를 만들어서 반환할 수 있습니다 (아래에서 이 코드를 더 개선해 볼 것입니다)
    Point3d temp { 0.0, 0.0, 0.0 };
    return temp;
}
</code></pre>
<p>하지만 여기서 객체의 이름(<code>temp</code>)은 코드를 읽는 데 아무런 추가적인 정보나 도움을 주지 않습니다.</p>
<p>대신 이름이 없는 임시 객체(익명 객체)를 반환하도록 수정하면 코드를 조금 더 깔끔하게 만들 수 있습니다:</p>
<pre><code class="language-cpp">Point3d getZeroPoint()
{
    return Point3d { 0.0, 0.0, 0.0 }; // 이름 없는 Point3d 객체 반환
}
</code></pre>
<p>이 경우, 임시 <code>Point3d</code> 객체가 생성되어 함수를 호출한 쪽으로 복사된 후, 표현식이 끝날 때 파괴됩니다. 코드가 훨씬 더 깔끔해졌다는 점에 주목해 보세요. (두 줄이 한 줄로 줄었고, <code>temp</code> 변수가 다른 곳에서 또 쓰이는 건 아닌지 고민할 필요도 없어졌습니다.)</p>
<blockquote>
<p><strong>관련 내용</strong>
익명 객체에 대해서는 <strong>레슨 14.13 - 임시 클래스 객체</strong> 에서 더 자세히 다룹니다.</p>
</blockquote>
<hr>
<h3 id="반환-타입-추론하기">반환 타입 추론하기</h3>
<p>함수에 명시적인 반환 타입(예: <code>Point3d</code>)이 이미 적혀 있는 경우, <code>return</code> 문에서 타입을 아예 생략할 수도 있습니다:</p>
<pre><code class="language-cpp">Point3d getZeroPoint()
{
    // 함수 선언부에서 이미 타입을 지정했으므로
    // 여기서 다시 타입을 적어줄 필요가 없습니다
    return { 0.0, 0.0, 0.0 }; // 이름 없는 Point3d 객체 반환
}
</code></pre>
<p>이는 암시적 변환으로 간주됩니다.</p>
<p>또한, 이 경우에는 모든 값을 0으로 반환하려고 하므로, 빈 중괄호를 사용하여 값 초기화(value-initialized)된 <code>Point3d</code> 객체를 반환할 수도 있습니다:</p>
<pre><code class="language-cpp">Point3d getZeroPoint()
{
    // 빈 중괄호를 사용하여 모든 멤버를 값 초기화(value-initialize) 할 수 있습니다
    return {};
}
</code></pre>
<hr>
<h3 id="중요한-구성-요소인-구조체">중요한 구성 요소인 구조체</h3>
<p>구조체 그 자체로도 매우 유용하지만, C++와 객체 지향 프로그래밍의 핵심이라고 할 수 있는 <strong>클래스(Class)</strong> 는 우리가 여기서 소개한 개념들 바로 위에 세워집니다. 구조체(특히 데이터 멤버, 멤버 선택, 기본 멤버 초기화 등)를 잘 이해해 두면, 나중에 클래스를 배울 때 훨씬 더 쉽고 수월하게 넘어갈 수 있을 것입니다.</p>
<hr>
<h2 id="1311--구조체-기타-주제들-struct-miscellany">13.11 — 구조체 기타 주제들 (Struct miscellany)</h2>
<h3 id="프로그램-정의-멤버를-가지는-구조체">프로그램 정의 멤버를 가지는 구조체</h3>
<p>C++에서 구조체(그리고 클래스)는 프로그래머가 직접 정의한 다른 타입을 멤버로 가질 수 있습니다. 여기에는 두 가지 방법이 있습니다.</p>
<p>첫 번째로, (전역 범위에) 프로그래머 정의 타입을 하나 만든 다음, 이를 다른 프로그래머 정의 타입의 멤버로 사용하는 방법입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee
{
    int id {};
    int age {};
    double wage {};
};

struct Company
{
    int numberOfEmployees {};
    Employee CEO {}; // Employee는 Company 구조체 내부에 있는 구조체입니다
};

int main()
{
    Company myCompany{ 7, { 1, 32, 55000.0 } }; // Employee를 초기화하기 위한 중첩된 초기화 리스트
    std::cout &lt;&lt; myCompany.CEO.wage &lt;&lt; &#39;\n&#39;; // CEO의 급여를 출력

    return 0;
}
</code></pre>
<p>위 예제에서는 <code>Employee</code> 구조체를 먼저 정의한 뒤, 이를 <code>Company</code> 구조체의 멤버로 사용했습니다. <code>Company</code> 를 초기화할 때, 중첩된 초기화 리스트를 사용하면 내부에 있는 <code>Employee</code> 도 한 번에 초기화할 수 있습니다. 만약 CEO의 급여를 확인하고 싶다면, 멤버 선택 연산자(<code>.</code>)를 두 번 연속해서 사용하면 됩니다: <code>myCompany.CEO.wage;</code></p>
<p>두 번째로, 어떤 타입을 다른 타입 안에 완전히 중첩시킬 수도 있습니다. 
만약 <code>Employee</code> 라는 타입이 <code>Company</code> 의 일부로만 존재한다면, <code>Employee</code> 타입을 아예 <code>Company</code> 구조체 내부로 쏙 집어넣을 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Company
{
    struct Employee // Company::Employee를 통해 접근합니다
    {
        int id{};
        int age{};
        double wage{};
    };

    int numberOfEmployees{};
    Employee CEO{}; // Employee는 Company 구조체 내부에 있는 구조체입니다
};

int main()
{
    Company myCompany{ 7, { 1, 32, 55000.0 } }; // Employee를 초기화하기 위한 중첩된 초기화 리스트
    std::cout &lt;&lt; myCompany.CEO.wage &lt;&lt; &#39;\n&#39;; // CEO의 급여를 출력

    return 0;
}
</code></pre>
<p>이런 중첩 방식은 주로 클래스에서 더 자주 쓰입니다. 
따라서 이 부분은 나중에 다룰 &#39;15.3 — 중첩 타입(멤버 타입)&#39; 레슨에서 조금 더 자세히 이야기해 보겠습니다.</p>
<hr>
<h3 id="소유자인-구조체는-소유자인-데이터-멤버를-가져야-합니다">소유자인 구조체는 소유자인 데이터 멤버를 가져야 합니다</h3>
<p>지난 &#39;5.9 레슨 — std::string_view (2부)&#39;에서, 우리는 <strong>소유자(Owner)</strong> 와 <strong>관찰자(Viewer)</strong> 라는 두 가지 중요한 개념을 배웠습니다. 
소유자는 자신의 데이터를 직접 관리하고, 그 데이터가 언제 사라질지 통제합니다. 반면에 관찰자는 그저 남의 데이터를 쳐다보기만 할 뿐, 그 데이터가 변경되거나 사라지는 것을 통제할 수 없습니다.</p>
<p>대부분의 경우, 우리는 구조체(나 클래스)가 자신이 품고 있는 데이터의 진정한 소유자가 되기를 원합니다. 
이렇게 하면 몇 가지 아주 좋은 장점이 있습니다.</p>
<ul>
<li>구조체가 존재하는 한, 그 안에 있는 데이터 멤버들도 안전하게 유지됩니다.</li>
<li>데이터 멤버의 값이 예상치 못한 순간에 마음대로 변하지 않습니다.</li>
</ul>
<p>구조체를 소유자로 만드는 가장 쉬운 방법은, 각각의 데이터 멤버들에게 &#39;소유권을 가진 타입&#39;을 주는 것입니다 (예를 들어 관찰자, 포인터, 참조자 같은 타입은 피하는 것이죠). 구조체 안의 모든 데이터 멤버가 소유자라면, 그 구조체 자체도 자동으로 소유자가 됩니다.</p>
<p>만약 구조체가 &#39;관찰자&#39;인 데이터 멤버를 하나라도 가지고 있다면, 그 멤버가 쳐다보고 있는 원본 데이터가 구조체보다 먼저 사라져버릴 위험이 있습니다. 이런 일이 발생하면 구조체 안에는 이미 사라진 데이터를 가리키는 <strong>허상(dangling) 멤버</strong> 가 남게 되며, 이 멤버를 사용하려고 하면 프로그램이 어떻게 동작할지 알 수 없는 심각한 오류(미정의 동작)가 발생합니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong> 
대부분의 경우 구조체(그리고 클래스)를 소유자로 만드는 것이 안전합니다. 
이를 위한 가장 쉬운 방법은 각 데이터 멤버가 소유권이 있는 타입(관찰자, 포인터, 참조자가 아닌 타입)인지 꼼꼼히 확인하는 것입니다.</p>
</blockquote>
<blockquote>
<p><strong>저자의 노트 (Author’s note)</strong> 
항상 안전한 구조체를 만드는 습관을 들이세요. 여러분의 멤버 변수가 길 잃은 허상(dangling) 상태가 되도록 방치하지 마세요!</p>
</blockquote>
<p>문자열 데이터 멤버를 만들 때 (관찰자인) <code>std::string_view</code> 대신 거의 항상 (소유자인) <code>std::string</code> 을 사용하는 이유도 바로 이 때문입니다. 아래 예제를 보시면 왜 이것이 중요한지 바로 이해되실 거예요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

struct Owner
{
    std::string name{}; // std::string은 소유자입니다
};

struct Viewer
{
    std::string_view name {}; // std::string_view는 관찰자입니다
};

// getName()은 사용자가 입력한 문자열을 임시 std::string으로 반환합니다.
// 이 임시 std::string은 함수 호출이 포함된 전체 표현식이 끝날 때 파괴됩니다.
std::string getName()
{
    std::cout &lt;&lt; &quot;Enter a name: &quot;;
    std::string name{};
    std::cin &gt;&gt; name;
    return name;
}

int main()
{
    Owner o { getName() };  // getName()의 반환 값은 초기화 직후에 파괴됩니다
    std::cout &lt;&lt; &quot;The owners name is &quot; &lt;&lt; o.name &lt;&lt; &#39;\n&#39;;  // 정상 작동

    Viewer v { getName() }; // getName()의 반환 값은 초기화 직후에 파괴됩니다
    std::cout &lt;&lt; &quot;The viewers name is &quot; &lt;&lt; v.name &lt;&lt; &#39;\n&#39;; // 미정의 동작 (알 수 없는 결과)

    return 0;
}
</code></pre>
<p>위 코드에서 <code>getName()</code> 함수는 사용자가 입력한 이름을 &#39;임시 <code>std::string</code>&#39;으로 돌려줍니다. 
이 임시 데이터는 함수 호출이 끝나는 문장의 마지막에서 수명을 다하고 사라집니다.</p>
<p><code>o</code> 의 경우를 살펴볼까요? 이 임시 <code>std::string</code> 은 <code>o.name</code> 을 초기화하는 데 사용됩니다. <code>o.name</code> 은 <code>std::string</code> 이기 때문에, 전달받은 임시 데이터를 안전하게 자기 공간으로 <strong>복사</strong>합니다. 복사가 끝난 뒤 원본 임시 데이터는 사라지지만, <code>o.name</code> 은 복사본을 안전하게 가지고 있으므로 아무런 문제가 없습니다. 그 다음 줄에서 <code>o.name</code> 을 출력하면 우리가 예상한 대로 이름이 잘 나옵니다.</p>
<p>하지만 <code>v</code> 의 경우는 다릅니다. 여기서도 임시 <code>std::string</code> 이 <code>v.name</code> 을 초기화하는 데 쓰입니다. 그런데 <code>v.name</code> 은 <code>std::string_view</code> (관찰자)이기 때문에 복사본을 만들지 않고 임시 데이터를 그저 &#39;바라보기만&#39; 합니다. 그 직후 임시 데이터가 수명을 다해 파괴되면, <code>v.name</code> 은 허공을 바라보는 <strong>허상(dangling) 상태</strong> 가 되어버립니다. 다음 줄에서 <code>v.name</code> 을 출력하려고 하면, 이미 사라진 데이터에 접근하는 꼴이 되므로 오류(미정의 동작)가 발생합니다.</p>
<hr>
<h3 id="구조체-크기와-데이터-구조-정렬-struct-size-and-data-structure-alignment">구조체 크기와 데이터 구조 정렬 (Struct size and data structure alignment)</h3>
<p>보통 구조체의 크기는 그 안에 들어있는 모든 멤버 변수들의 크기를 합친 것과 같다고 생각하기 쉽습니다. 하지만 항상 그런 것은 아닙니다!</p>
<p>다음 프로그램을 한 번 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Foo
{
    short a {};
    int b {};
    double c {};
};

int main()
{
    std::cout &lt;&lt; &quot;The size of short is &quot; &lt;&lt; sizeof(short) &lt;&lt; &quot; bytes\n&quot;;
    std::cout &lt;&lt; &quot;The size of int is &quot; &lt;&lt; sizeof(int) &lt;&lt; &quot; bytes\n&quot;;
    std::cout &lt;&lt; &quot;The size of double is &quot; &lt;&lt; sizeof(double) &lt;&lt; &quot; bytes\n&quot;;

    std::cout &lt;&lt; &quot;The size of Foo is &quot; &lt;&lt; sizeof(Foo) &lt;&lt; &quot; bytes\n&quot;;

    return 0;
}
</code></pre>
<p>저자의 컴퓨터에서 이 코드를 실행하면 다음과 같이 출력됩니다.</p>
<pre><code class="language-text">The size of short is 2 bytes
The size of int is 4 bytes
The size of double is 8 bytes
The size of Foo is 16 bytes
</code></pre>
<p>분명 <code>short</code>(2) + <code>int</code>(4) + <code>double</code>(8) 을 더하면 14바이트인데, <code>Foo</code> 구조체의 전체 크기는 16바이트가 나왔습니다! 왜 그럴까요?</p>
<p>사실, 구조체의 크기는 내부 변수들의 크기를 모두 합친 것보다 <strong>최소한 같거나 크다</strong> 고만 확신할 수 있습니다. 즉, 실제로는 더 커질 수도 있다는 뜻이죠. 그 이유는 C++ 컴파일러가 컴퓨터의 처리 속도(성능)를 높이기 위해 구조체 변수들 사이에 몰래 빈 공간을 끼워 넣기 때문입니다. 이렇게 빈 공간을 추가하는 것을 <strong>패딩(padding)</strong> 이라고 부릅니다.</p>
<p>위의 <code>Foo</code> 구조체에서 컴파일러는 멤버 <code>a</code> 뒤에 2바이트의 패딩(빈 공간)을 보이지 않게 추가했습니다. 그 결과 14바이트가 아니라 16바이트가 된 것입니다.</p>
<blockquote>
<p><strong>심화 학습자를 위해 (For advanced readers)</strong> &gt;
컴파일러가 대체 왜 이런 빈 공간을 끼워 넣는지(패딩) 그 구체적인 원리는 이 튜토리얼의 범위를 벗어납니다. 하지만 궁금하시다면 위키백과에서 <strong>데이터 구조 정렬(data structure alignment)</strong> 에 대해 더 찾아보실 수 있습니다. 물론 이것은 구조체나 C++의 기본을 이해하기 위해 꼭 알아야 하는 필수 내용은 아니니 가볍게 넘어가셔도 괜찮습니다!</p>
</blockquote>
<p>이 패딩이라는 녀석은 구조체의 크기에 생각보다 꽤 큰 영향을 미칩니다. 다음 예제를 확인해 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Foo1
{
    short a{}; // a 뒤에 2바이트의 패딩이 생깁니다
    int b{};
    short c{}; // c 뒤에 2바이트의 패딩이 생깁니다
};

struct Foo2
{
    int b{};
    short a{};
    short c{};
};

int main()
{
    std::cout &lt;&lt; sizeof(Foo1) &lt;&lt; &#39;\n&#39;; // 12 출력
    std::cout &lt;&lt; sizeof(Foo2) &lt;&lt; &#39;\n&#39;; // 8 출력

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 다음과 같이 출력됩니다.</p>
<pre><code class="language-text">12
8
</code></pre>
<p>놀랍지 않나요? <code>Foo1</code> 과 <code>Foo2</code> 는 들어있는 멤버가 완전히 똑같습니다. 
단지 멤버를 선언한 &#39;순서&#39;만 다를 뿐이죠. 그런데도 중간중간 끼어든 패딩 때문에 <code>Foo1</code> 의 크기가 무려 50%나 더 큽니다.</p>
<blockquote>
<p><strong>꿀팁 (Tip)</strong> 
크기가 가장 큰 멤버부터 가장 작은 멤버 순서(내림차순)로 변수를 정의하면, 불필요한 패딩을 최소한으로 줄일 수 있습니다.
C++ 컴파일러는 프로그래머가 적어 놓은 변수 순서를 마음대로 바꿀 수 없도록 규칙이 정해져 있습니다. 
따라서 이런 최적화 작업은 우리가 직접 코드를 짤 때 신경 써 주어야 합니다.</p>
</blockquote>
<hr>
<h2 id="1312--포인터와-참조를-사용한-멤버-선택">13.12 — 포인터와 참조를 사용한 멤버 선택</h2>
<h3 id="구조체와-구조체-참조를-위한-멤버-선택">구조체와 구조체 참조를 위한 멤버 선택</h3>
<p><code>13.7 - 구조체, 멤버, 멤버 선택 소개</code> 레슨에서, 우리는 멤버 선택 연산자 (<code>.</code>)를 사용하여 구조체 객체에서 멤버를 선택할 수 있다는 것을 보여주었습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee{
    int id {};
    int age {};
    double wage {};
};

int main(){
    Employee joe { 1, 34, 65000.0 };

    // 구조체 객체에서 멤버를 선택하기 위해 멤버 선택 연산자 (.)를 사용합니다.
    ++joe.age; // Joe의 생일이 지났습니다.
    joe.wage = 68000.0; // Joe가 승진했습니다.

    return 0;
}
</code></pre>
<p>객체에 대한 참조는 객체 자체와 똑같이 작동하므로, 참조를 통해 구조체의 멤버를 선택할 때도 멤버 선택 연산자 (<code>.</code>)를 사용할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee{
    int id{};
    int age{};
    double wage{};
};

void printEmployee(const Employee&amp; e){
    // 구조체 참조에서 멤버를 선택하기 위해 멤버 선택 연산자 (.)를 사용합니다.
    std::cout &lt;&lt; &quot;Id: &quot; &lt;&lt; e.id &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Age: &quot; &lt;&lt; e.age &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;Wage: &quot; &lt;&lt; e.wage &lt;&lt; &#39;\n&#39;;
}

int main(){
    Employee joe{ 1, 34, 65000.0 };

    ++joe.age;
    joe.wage = 68000.0;

    printEmployee(joe);

    return 0;
}
</code></pre>
<hr>
<h3 id="구조체-포인터를-위한-멤버-선택">구조체 포인터를 위한 멤버 선택</h3>
<p>하지만, 멤버 선택 연산자 (<code>.</code>)는 구조체를 가리키는 포인터에는 직접 사용할 수 없습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee{
    int id{};
    int age{};
    double wage{};
};

int main(){
    Employee joe{ 1, 34, 65000.0 };

    ++joe.age;
    joe.wage = 68000.0;

    Employee* ptr{ &amp;joe };
    std::cout &lt;&lt; ptr.id &lt;&lt; &#39;\n&#39;; // 컴파일 에러: 포인터에는 . 연산자를 사용할 수 없습니다.

    return 0;
}
</code></pre>
<p>일반적인 변수나 참조를 사용하면 객체에 직접 접근할 수 있습니다. 
하지만 포인터는 &#39;주소&#39;를 담고 있기 때문에, 포인터를 가지고 무언가를 하기 전에는 먼저 포인터를 <strong>역참조 (dereference)</strong> 하여 객체를 가져와야 합니다. 따라서 포인터를 통해 구조체 멤버에 접근하는 한 가지 방법은 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee{
    int id{};
    int age{};
    double wage{};
};

int main(){
    Employee joe{ 1, 34, 65000.0 };

    ++joe.age;
    joe.wage = 68000.0;

    Employee* ptr{ &amp;joe };
    std::cout &lt;&lt; (*ptr).id &lt;&lt; &#39;\n&#39;; // 썩 좋진 않지만 작동합니다: 먼저 ptr을 역참조한 다음, 멤버 선택 연산자를 사용합니다.

    return 0;
}
</code></pre>
<p>하지만 이 방법은 조금 보기 안 좋습니다. 특히 역참조 연산(<code>*</code>)이 멤버 선택 연산(<code>.</code>)보다 먼저 실행되도록 꼭 괄호로 묶어줘야 하기 때문입니다.</p>
<p>더 깔끔한 코드를 작성하기 위해, C++은 포인터에서 객체의 멤버를 쉽게 선택할 수 있는 <strong>포인터를 통한 멤버 선택 연산자 (-&gt;)</strong> (때로는 <strong>화살표 연산자</strong> 라고도 부름)를 제공합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Employee{
    int id{};
    int age{};
    double wage{};
};

int main(){
    Employee joe{ 1, 34, 65000.0 };

    ++joe.age;
    joe.wage = 68000.0;

    Employee* ptr{ &amp;joe };
    std::cout &lt;&lt; ptr-&gt;id &lt;&lt; &#39;\n&#39;; // 더 좋은 방법: -&gt; 연산자를 사용하여 객체를 가리키는 포인터에서 멤버를 선택합니다.

    return 0;
}
</code></pre>
<p>이 포인터를 통한 멤버 선택 연산자 (<code>-&gt;</code>)는 일반 멤버 선택 연산자 (<code>.</code>)와 똑같이 작동하지만, 멤버를 선택하기 전에 포인터 객체를 자동으로 역참조해 줍니다. 따라서 <code>ptr-&gt;id</code> 는 <code>(*ptr).id</code> 와 완전히 똑같은 의미입니다.</p>
<p>이 화살표 연산자는 키보드로 치기 쉬울 뿐만 아니라, 간접 참조를 알아서 처리해 주므로 연산자 우선순위를 걱정할 필요가 없어 실수를 훨씬 줄여줍니다. 결론적으로, 포인터를 통해 멤버에 접근할 때는 항상 <code>.</code> 연산자 대신 <code>-&gt;</code> 연산자를 사용하는 것이 좋습니다.</p>
<blockquote>
<p><strong>모범 사례</strong> 
포인터를 사용하여 멤버에 접근할 때는 멤버 선택 연산자 (<code>.</code>) 대신 포인터를 통한 멤버 선택 연산자 (<code>-&gt;</code>)를 사용하세요.</p>
</blockquote>
<hr>
<h3 id="operator--연속해서-사용하기-체이닝"><code>operator-&gt;</code> 연속해서 사용하기 (체이닝)</h3>
<p><code>operator-&gt;</code>를 통해 접근한 멤버가 클래스나 구조체를 가리키는 포인터라면, 
같은 줄에서 <code>operator-&gt;</code>를 한 번 더 사용하여 그 내부의 멤버에 다시 접근할 수 있습니다.</p>
<p>다음 예제는 이 과정을 잘 보여줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Point{
    double x {};
    double y {};
};

struct Triangle{
    Point* a {};
    Point* b {};
    Point* c {};
};

int main(){
    Point a {1,2};
    Point b {3,7};
    Point c {10,2};

    Triangle tr { &amp;a, &amp;b, &amp;c };
    Triangle* ptr {&amp;tr};

    // ptr은 Triangle을 가리키는 포인터이고, 이 구조체 안에는 Point를 가리키는 포인터들이 들어있습니다.
    // ptr이 가리키는 Triangle 안의 멤버 c(Point 포인터)를 통해 멤버 y에 접근하려면, 다음 두 가지 방법은 동일한 역할을 합니다:

    // . 연산자를 사용한 접근
    std::cout &lt;&lt; (*(*ptr).c).y &lt;&lt; &#39;\n&#39;; // 보기 안 좋습니다!

    // -&gt; 연산자를 사용한 접근
    std::cout &lt;&lt; ptr -&gt; c -&gt; y &lt;&lt; &#39;\n&#39;; // 훨씬 낫습니다!
}
</code></pre>
<p>두 개 이상의 <code>operator-&gt;</code>를 연속해서 사용할 때 (예: <code>ptr-&gt;c-&gt;y</code>), 코드가 한눈에 안 들어올 수 있습니다. 
이때 멤버와 <code>operator-&gt;</code> 사이에 띄어쓰기를 추가하면 (예: <code>ptr -&gt; c -&gt; y</code>) 어떤 멤버에 접근하고 있는지 조금 더 쉽게 알아볼 수 있습니다.</p>
<hr>
<h3 id="포인터-멤버와-일반-멤버-섞어-쓰기">포인터 멤버와 일반 멤버 섞어 쓰기</h3>
<p>멤버 선택 연산자는 항상 &#39;현재 선택된 변수&#39;에 맞게 사용해야 합니다. 
만약 포인터 멤버와 일반 멤버 변수가 섞여 있다면, <code>.</code> 과 <code>-&gt;</code> 가 이어서 함께 쓰이는 모습을 볼 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

struct Paw{
    int claws{};
};

struct Animal{
    std::string name{};
    Paw paw{};
};

int main(){
    Animal puma{ &quot;Puma&quot;, { 5 } };

    Animal* ptr{ &amp;puma };

    // ptr은 포인터이므로 -&gt;를 사용합니다.
    // paw는 포인터가 아니므로 .을 사용합니다.

    std::cout &lt;&lt; (ptr-&gt;paw).claws &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>참고로 <code>(ptr-&gt;paw).claws</code> 에서 <code>operator-&gt;</code> 와 <code>operator.</code> 모두 왼쪽에서 오른쪽 순서로 계산되기 때문에 괄호가 반드시 필요한 것은 아닙니다. 하지만 괄호를 쳐주면 코드를 읽기가 조금 더 편해집니다.</p>
<hr>
<h2 id="1313--클래스-템플릿-class-templates">13.13 — 클래스 템플릿 (Class templates)</h2>
<p>이전 <strong>11.6 레슨 -- 함수 템플릿</strong> 에서는 우리가 다루고 싶은 다양한 데이터 타입마다 똑같은 역할을 하는 함수를 여러 개(오버로딩) 만들어야 하는 불편함에 대해 이야기했습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 두 int 값 중 더 큰 값을 계산하는 함수
int max(int x, int y)
{
    return (x &lt; y) ? y : x;
}

// 두 double 값 중 더 큰 값을 계산하는 거의 동일한 함수
// 유일한 차이점은 타입 정보뿐입니다
double max(double x, double y)
{
    return (x &lt; y) ? y : x;
}

int main()
{
    std::cout &lt;&lt; max(5, 6);     // max(int, int) 호출
    std::cout &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; max(1.2, 3.4); // max(double, double) 호출

    return 0;
}
</code></pre>
<p>이 문제에 대한 해결책은 <strong>함수 템플릿</strong> 을 만드는 것이었습니다. 
템플릿 하나만 만들어 두면, 컴파일러가 필요한 타입에 맞춰 일반 함수를 자동으로 찍어내(인스턴스화) 사용할 수 있기 때문입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// max를 위한 단일 함수 템플릿
template &lt;typename T&gt;
T max(T x, T y)
{
    return (x &lt; y) ? y : x;
}

int main()
{
    std::cout &lt;&lt; max(5, 6);     // max&lt;int&gt;(int, int)를 인스턴스화하고 호출
    std::cout &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; max(1.2, 3.4); // max&lt;double&gt;(double, double)를 인스턴스화하고 호출

    return 0;
}
</code></pre>
<blockquote>
<p><strong>관련 내용</strong>
함수 템플릿이 어떻게 함수를 찍어내는지(인스턴스화)에 대한 자세한 내용은 <strong>11.7 레슨 -- 함수 템플릿 인스턴스화</strong> 에서 다룹니다.</p>
</blockquote>
<hr>
<h3 id="집계-타입aggregate-types도-비슷한-문제를-겪습니다">집계 타입(Aggregate types)도 비슷한 문제를 겪습니다</h3>
<p>구조체, 클래스, 공용체, 배열 같은 <strong>집계 타입</strong> 도 비슷한 문제에 부딪힙니다.</p>
<p>예를 들어, 두 개의 <code>int</code> 값 쌍(pair)을 다루면서 둘 중 어느 숫자가 더 큰지 알아내야 하는 프로그램을 작성한다고 가정해 보겠습니다. 
아마 다음과 같이 코드를 짤 수 있을 것입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Pair
{
    int first{};
    int second{};
};

constexpr int max(Pair p) // Pair의 크기가 작으므로 값으로 전달(pass by value)
{
    return (p.first &lt; p.second ? p.second : p.first);
}

int main()
{
    Pair p1{ 5, 6 };
    std::cout &lt;&lt; max(p1) &lt;&lt; &quot; is larger\n&quot;;

    return 0;
}
</code></pre>
<p>나중에 개발을 하다 보니 <code>double</code> 값의 쌍(pair)도 필요해졌습니다. 그래서 프로그램을 다음과 같이 업데이트했습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

struct Pair
{
    int first{};
    int second{};
};

struct Pair // 컴파일 에러: 잘못된 Pair 재정의
{
    double first{};
    double second{};
};

constexpr int max(Pair p)
{
    return (p.first &lt; p.second ? p.second : p.first);
}

constexpr double max(Pair p) // 컴파일 에러: 오버로딩된 함수가 반환 타입만 다름
{
    return (p.first &lt; p.second ? p.second : p.first);
}

int main()
{
    Pair p1{ 5, 6 };
    std::cout &lt;&lt; max(p1) &lt;&lt; &quot; is larger\n&quot;;

    Pair p2{ 1.2, 3.4 };
    std::cout &lt;&lt; max(p2) &lt;&lt; &quot; is larger\n&quot;;

    return 0;
}
</code></pre>
<p>안타깝게도 이 프로그램은 컴파일되지 않으며, 해결해야 할 몇 가지 문제가 있습니다.</p>
<ol>
<li><strong>타입은 오버로딩 불가</strong> 함수와 달리 타입 정의는 오버로딩할 수 없습니다. 
컴파일러는 두 번째 <code>double</code> 버전의 <code>Pair</code>를 처음 정의된 <code>Pair</code>를 잘못 재정의한 것으로 간주합니다.</li>
<li><strong>반환 타입만으론 구분 불가</strong> 함수는 오버로딩할 수 있지만, 위 코드의 <code>max(Pair)</code> 함수들은 반환 타입만 다를 뿐 매개변수가 똑같습니다. 오버로딩된 함수는 반환 타입만으로 구분할 수 없습니다.</li>
<li><strong>코드의 심각한 중복</strong> <code>Pair</code> 구조체들은 데이터 타입만 빼면 완전히 똑같고, <code>max(Pair)</code> 함수들도 반환 타입만 빼면 완전히 똑같습니다.</li>
</ol>
<p><code>Pair</code> 구조체의 이름을 서로 다르게 지어주면(예: <code>PairInt</code>, <code>PairDouble</code>) 첫 번째와 두 번째 문제는 해결할 수 있습니다. 
하지만 이 방식은 만들어둔 이름들을 전부 외워야 하고, 새로운 타입이 필요할 때마다 똑같은 코드를 계속 복사해서 붙여넣어야 하므로 중복 문제를 전혀 해결하지 못합니다.</p>
<p>다행히 훨씬 더 좋은 방법이 있습니다.</p>
<blockquote>
<p><strong>작성자의 노트</strong>
다음 내용으로 넘어가기 전에, 함수 템플릿, 템플릿 타입, 혹은 템플릿이 함수를 어떻게 생성하는지 기억나지 않는다면 <strong>11.6 레슨</strong> 과 <strong>11.7 레슨</strong> 을 다시 한번 복습해 주세요.</p>
</blockquote>
<hr>
<h3 id="클래스-템플릿-class-templates">클래스 템플릿 (Class templates)</h3>
<p>함수 템플릿이 함수를 붕어빵처럼 찍어내는 틀이라면, <strong>클래스 템플릿</strong> 은 클래스나 구조체를 찍어내는 템플릿 틀입니다.</p>
<blockquote>
<p><strong>참고</strong>
&#39;클래스 타입&#39;은 구조체(struct), 클래스(class), 공용체(union)를 모두 아우르는 말입니다. 
설명을 쉽게 하기 위해 구조체를 예시로 &#39;클래스 템플릿&#39;을 설명하겠지만, 여기서 배우는 모든 내용은 클래스에도 똑같이 적용됩니다.</p>
</blockquote>
<p>우리가 처음에 만들었던 <code>int</code> 버전의 <code>Pair</code> 구조체를 다시 한번 보겠습니다.</p>
<pre><code class="language-cpp">struct Pair
{
    int first{};
    int second{};
};
</code></pre>
<p>이제 이 구조체를 <strong>클래스 템플릿</strong> 으로 다시 작성해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
struct Pair
{
    T first{};
    T second{};
};

int main()
{
    Pair&lt;int&gt; p1{ 5, 6 };        // Pair&lt;int&gt;를 인스턴스화하고 객체 p1 생성
    std::cout &lt;&lt; p1.first &lt;&lt; &#39; &#39; &lt;&lt; p1.second &lt;&lt; &#39;\n&#39;;

    Pair&lt;double&gt; p2{ 1.2, 3.4 }; // Pair&lt;double&gt;를 인스턴스화하고 객체 p2 생성
    std::cout &lt;&lt; p2.first &lt;&lt; &#39; &#39; &lt;&lt; p2.second &lt;&lt; &#39;\n&#39;;

    Pair&lt;double&gt; p3{ 7.8, 9.0 }; // 이전에 정의된 Pair&lt;double&gt;을 사용하여 객체 p3 생성
    std::cout &lt;&lt; p3.first &lt;&lt; &#39; &#39; &lt;&lt; p3.second &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>함수 템플릿과 마찬가지로, 클래스 템플릿도 <strong>템플릿 매개변수 선언</strong> 으로 시작합니다. <code>template</code> 키워드를 먼저 쓰고, 그다음 꺾쇠괄호(<code>&lt; &gt;</code>) 안에 클래스 템플릿에서 사용할 모든 템플릿 타입을 지정합니다. 필요한 타입마다 <code>typename</code> (권장) 또는 <code>class</code> (비권장) 키워드를 쓰고 그 뒤에 템플릿 타입의 이름(예: <code>T</code>)을 적어주면 됩니다. 이 예제에서는 구조체의 두 멤버 변수가 같은 타입을 사용할 것이므로 템플릿 타입이 하나만 있으면 됩니다.</p>
<p>그다음은 평소처럼 구조체를 정의하면 되는데, 나중에 실제 타입(int, double 등)으로 바뀔 자리에 아까 정해둔 템플릿 타입(<code>T</code>)을 넣어주기만 하면 됩니다. 정말 쉽죠! 이것으로 클래스 템플릿 정의가 끝났습니다.</p>
<p><code>main</code> 함수 안에서는 우리가 원하는 어떤 타입으로든 <code>Pair</code> 객체를 만들어낼 수 있습니다.</p>
<ul>
<li>먼저 <code>Pair&lt;int&gt;</code> 타입의 객체를 만듭니다. 컴파일러는 <code>Pair&lt;int&gt;</code>에 대한 정의가 아직 없다는 것을 확인하고, 템플릿 틀을 사용해 모든 <code>T</code>가 <code>int</code>로 바뀐 <code>Pair&lt;int&gt;</code> 구조체를 만들어냅니다.</li>
<li>다음으로 <code>Pair&lt;double&gt;</code> 객체를 만듭니다. 마찬가지로 <code>T</code>가 <code>double</code>로 바뀐 <code>Pair&lt;double&gt;</code> 구조체를 찍어냅니다.</li>
<li>마지막 <code>p3</code>의 경우, <code>Pair&lt;double&gt;</code>이 이미 방금 전에 만들어졌기 때문에 컴파일러는 새로 만들지 않고 기존에 만들어둔 것을 재사용합니다.</li>
</ul>
<p>다음은 템플릿 인스턴스화가 모두 끝난 후, 컴파일러가 실제로 변환해서 컴파일하게 되는 내부 코드의 모습입니다. 
위 예제와 완전히 동일하게 작동합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// Pair 클래스 템플릿 선언
// (더 이상 사용되지 않으므로 정의가 필요 없습니다)
template &lt;typename T&gt;
struct Pair;

// Pair&lt;int&gt;의 모습을 명시적으로 정의
template &lt;&gt; // 컴파일러에게 이것이 템플릿 매개변수가 없는 템플릿 타입임을 알려줌
struct Pair&lt;int&gt;
{
    int first{};
    int second{};
};

// Pair&lt;double&gt;의 모습을 명시적으로 정의
template &lt;&gt; // 컴파일러에게 이것이 템플릿 매개변수가 없는 템플릿 타입임을 알려줌
struct Pair&lt;double&gt;
{
    double first{};
    double second{};
};

int main()
{
    Pair&lt;int&gt; p1{ 5, 6 };        // Pair&lt;int&gt;를 인스턴스화하고 객체 p1 생성
    std::cout &lt;&lt; p1.first &lt;&lt; &#39; &#39; &lt;&lt; p1.second &lt;&lt; &#39;\n&#39;;

    Pair&lt;double&gt; p2{ 1.2, 3.4 }; // Pair&lt;double&gt;를 인스턴스화하고 객체 p2 생성
    std::cout &lt;&lt; p2.first &lt;&lt; &#39; &#39; &lt;&lt; p2.second &lt;&lt; &#39;\n&#39;;

    Pair&lt;double&gt; p3{ 7.8, 9.0 }; // 이전에 정의된 Pair&lt;double&gt;을 사용하여 객체 p3 생성
    std::cout &lt;&lt; p3.first &lt;&lt; &#39; &#39; &lt;&lt; p3.second &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드를 직접 컴파일해 보시면 예상대로 잘 작동하는 것을 확인할 수 있습니다!</p>
<blockquote>
<p><strong>심화 학습자를 위해</strong>
위 예제는 &#39;클래스 템플릿 특수화(Class template specialization)&#39;라는 기능을 사용하고 있습니다(향후 <strong>26.4 레슨</strong> 에서 다룹니다). 
지금 당장 이 기능이 어떻게 작동하는지 알 필요는 없습니다.</p>
</blockquote>
<hr>
<h3 id="함수에서-클래스-템플릿-사용하기">함수에서 클래스 템플릿 사용하기</h3>
<p>이제 다시 <code>max()</code> 함수가 여러 타입에서 작동하게 만들었던 과제로 돌아가 봅시다. 
컴파일러는 <code>Pair&lt;int&gt;</code>와 <code>Pair&lt;double&gt;</code>을 완전히 별개의 타입으로 취급하기 때문에, 매개변수 타입이 다른 오버로딩 함수를 사용할 수 있습니다.</p>
<pre><code class="language-cpp">constexpr int max(Pair&lt;int&gt; p)
{
    return (p.first &lt; p.second ? p.second : p.first);
}

constexpr double max(Pair&lt;double&gt; p) // 정상 작동: 매개변수 타입으로 구분되는 오버로딩된 함수
{
    return (p.first &lt; p.second ? p.second : p.first);
}
</code></pre>
<p>이렇게 하면 컴파일은 잘 되지만, 중복 문제는 여전히 해결되지 않습니다. 
우리가 진짜로 원하는 건 <strong>어떤 타입의 Pair가 들어오든 다 처리해 주는 단 하나의 함수</strong> 입니다. 
즉, 템플릿 타입 <code>T</code>를 사용하는 <code>Pair&lt;T&gt;</code>를 매개변수로 받는 함수가 필요합니다. 이를 위해서는 <strong>함수 템플릿</strong> 이 제격이죠!</p>
<p><code>max()</code>를 함수 템플릿으로 구현한 전체 예제는 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
struct Pair
{
    T first{};
    T second{};
};

template &lt;typename T&gt;
constexpr T max(Pair&lt;T&gt; p)
{
    return (p.first &lt; p.second ? p.second : p.first);
}

int main()
{
    Pair&lt;int&gt; p1{ 5, 6 };
    std::cout &lt;&lt; max&lt;int&gt;(p1) &lt;&lt; &quot; is larger\n&quot;; // 명시적으로 max&lt;int&gt; 호출

    Pair&lt;double&gt; p2{ 1.2, 3.4 };
    std::cout &lt;&lt; max(p2) &lt;&lt; &quot; is larger\n&quot;; // 템플릿 인수 연역(추론)을 사용하여 max&lt;double&gt; 호출 (권장)

    return 0;
}
</code></pre>
<p><code>max()</code> 함수 템플릿은 아주 직관적입니다. <code>Pair&lt;T&gt;</code>를 전달받고 싶기 때문에, 컴파일러에게 <code>T</code>가 무엇인지 알려주어야 합니다. 따라서 템플릿 타입 <code>T</code>를 정의하는 템플릿 매개변수 선언으로 함수를 시작합니다. 그러면 이 <code>T</code>를 함수의 반환 타입으로도 쓰고, 매개변수인 <code>Pair&lt;T&gt;</code>의 템플릿 타입으로도 쓸 수 있습니다.</p>
<p><code>max()</code> 함수에 <code>Pair&lt;int&gt;</code> 인수를 넣어 호출하면, 컴파일러는 이 함수 템플릿에서 <code>T</code>를 <code>int</code>로 바꿔서 <code>int max&lt;int&gt;(Pair&lt;int&gt;)</code>라는 실제 함수를 만들어냅니다. 아래는 이 상황에서 컴파일러가 실제로 만들어내는 코드입니다.</p>
<pre><code class="language-cpp">template &lt;&gt;
constexpr int max(Pair&lt;int&gt; p)
{
    return (p.first &lt; p.second ? p.second : p.first);
}
</code></pre>
<p>모든 함수 템플릿 호출과 마찬가지로, <code>max&lt;int&gt;(p1)</code>처럼 템플릿 타입을 명시적으로 지정해 주거나, <code>max(p2)</code>처럼 템플릿 인수 연역(Type deduction) 기능을 통해 컴파일러가 알아서 추론하게 내버려 둘 수도 있습니다. (후자를 권장합니다!)</p>
<hr>
<h3 id="템플릿-타입-멤버와-일반-멤버가-함께-있는-클래스-템플릿">템플릿 타입 멤버와 일반 멤버가 함께 있는 클래스 템플릿</h3>
<p>클래스 템플릿 내부에서 일부 멤버는 템플릿 타입(<code>T</code>)을 사용하고, 다른 멤버는 일반 타입(예: <code>int</code>)을 사용할 수도 있습니다.</p>
<pre><code class="language-cpp">template &lt;typename T&gt;
struct Foo
{
    T first{};    // first는 T가 대체되는 어떤 타입이든 가질 수 있습니다
    int second{}; // second는 T의 타입과 상관없이 항상 int 타입을 가집니다
};
</code></pre>
<p>직관적으로 이해되는 그대로 작동합니다. <code>first</code>의 자료형은 템플릿 타입 <code>T</code>에 따라 달라지며, <code>second</code>는 무조건 <code>int</code> 자료형이 됩니다.</p>
<hr>
<h3 id="다중-템플릿-타입을-가진-클래스-템플릿">다중 템플릿 타입을 가진 클래스 템플릿</h3>
<p>클래스 템플릿은 여러 개의 템플릿 타입을 가질 수도 있습니다. 예를 들어, <code>Pair</code> 클래스의 두 멤버 변수가 서로 다른 자료형을 가질 수 있게 만들고 싶다면, 두 개의 템플릿 타입을 지정하면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T, typename U&gt;
struct Pair
{
    T first{};
    U second{};
};

template &lt;typename T, typename U&gt;
void print(Pair&lt;T, U&gt; p)
{
    std::cout &lt;&lt; &#39;[&#39; &lt;&lt; p.first &lt;&lt; &quot;, &quot; &lt;&lt; p.second &lt;&lt; &#39;]&#39;;
}

int main()
{
    Pair&lt;int, double&gt; p1{ 1, 2.3 }; // int와 double을 담고 있는 쌍(pair)
    Pair&lt;double, int&gt; p2{ 4.5, 6 }; // double과 int를 담고 있는 쌍(pair)
    Pair&lt;int, int&gt; p3{ 7, 8 };      // 두 개의 int를 담고 있는 쌍(pair)

    print(p2);

    return 0;
}
</code></pre>
<p>여러 템플릿 타입을 정의하려면 꺾쇠괄호 안에 원하는 타입들을 쉼표(<code>,</code>)로 구분해서 적어줍니다. 위 예제에서는 <code>T</code>와 <code>U</code>라는 두 개의 템플릿 타입을 정의했습니다. 위 코드의 <code>p1</code>, <code>p2</code>처럼 <code>T</code>와 <code>U</code>에 들어갈 실제 타입을 서로 다르게 지정할 수도 있고, <code>p3</code>처럼 동일한 타입으로 지정할 수도 있습니다.</p>
<hr>
<h3 id="둘-이상의-클래스-타입과-호환되는-함수-템플릿-만들기">둘 이상의 클래스 타입과 호환되는 함수 템플릿 만들기</h3>
<p>위 예제의 <code>print()</code> 함수 템플릿을 다시 살펴봅시다.</p>
<pre><code class="language-cpp">template &lt;typename T, typename U&gt;
void print(Pair&lt;T, U&gt; p)
{
    std::cout &lt;&lt; &#39;[&#39; &lt;&lt; p.first &lt;&lt; &quot;, &quot; &lt;&lt; p.second &lt;&lt; &#39;]&#39;;
}
</code></pre>
<p>함수 매개변수 타입을 아예 <code>Pair&lt;T, U&gt;</code>로 명시해 놓았기 때문에, 이 함수는 <code>Pair&lt;T, U&gt;</code> 타입이거나 그 타입으로 변환될 수 있는 인수만 받을 수 있습니다. 함수를 <code>Pair&lt;T, U&gt;</code> 전용으로 쓰고 싶을 때는 이 방식이 아주 이상적입니다.</p>
<p>하지만 때로는 &#39;조건만 맞으면 어떤 타입의 클래스라도 다 받아주는&#39; 범용 함수 템플릿을 만들고 싶을 때가 있습니다. 
이럴 때는 매개변수 자리에 구체적인 클래스 대신 타입 템플릿 매개변수를 사용하면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T, typename U&gt;
struct Pair
{
    T first{};
    U second{};
};

struct Point
{
    int first{};
    int second{};
};

template &lt;typename T&gt;
void print(T p) // 타입 템플릿 매개변수는 어떤 타입과도 일치합니다
{
    std::cout &lt;&lt; &#39;[&#39; &lt;&lt; p.first &lt;&lt; &quot;, &quot; &lt;&lt; p.second &lt;&lt; &#39;]&#39;; // 타입에 first와 second 멤버가 있어야만 컴파일됩니다
}

int main()
{
    Pair&lt;double, int&gt; p1{ 4.5, 6 };
    print(p1); // print(Pair&lt;double, int&gt;)와 일치

    std::cout &lt;&lt; &#39;\n&#39;;

    Point p2 { 7, 8 };
    print(p2); // print(Point)와 일치

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>위 예제의 <code>print()</code> 함수는 이제 단일 타입 템플릿 매개변수(<code>T</code>)만을 가집니다. 즉 어떤 타입이 들어오든 다 매칭해 줍니다. 대신 함수 내부에서 <code>.first</code>와 <code>.second</code>에 접근하므로, 인수로 넘어온 타입 내부에 <code>first</code>와 <code>second</code>라는 멤버 변수만 존재하면 어떤 클래스/구조체든 문제없이 컴파일됩니다. <code>Pair</code> 객체와 <code>Point</code> 객체 모두를 성공적으로 출력하는 것을 볼 수 있습니다.</p>
<p><strong>주의할 점이 하나 있습니다.</strong> 다음 코드를 봐주세요.</p>
<pre><code class="language-cpp">template &lt;typename T, typename U&gt;
struct Pair // Pair라는 이름의 클래스 타입을 정의
{
    T first{};
    U second{};
};

template &lt;typename Pair&gt; // Pair라는 이름의 타입 템플릿 매개변수를 정의 (Pair 클래스 타입을 가림)
void print(Pair p)       // 이것은 클래스 타입 Pair가 아니라 템플릿 매개변수 Pair를 참조합니다
{
    std::cout &lt;&lt; &#39;[&#39; &lt;&lt; p.first &lt;&lt; &quot;, &quot; &lt;&lt; p.second &lt;&lt; &#39;]&#39;;
}
</code></pre>
<p>얼핏 보면 이 <code>print()</code> 함수는 우리가 만들어둔 <code>Pair</code> 클래스 전용 함수처럼 보일 수 있습니다. 하지만 이 함수는 직전 예제(매개변수 이름을 <code>T</code>로 지었던 예제)와 완전히 똑같이 작동하며, <strong>모든 타입</strong> 과 일치합니다.
문제는 템플릿 매개변수 이름을 <code>Pair</code>라고 지어버리면서 발생합니다. 이 이름이 전역 스코프에 있던 구조체 이름인 <code>Pair</code>를 가려버립니다(Shadowing). 따라서 함수 템플릿 안에서 <code>Pair</code>는 더 이상 구조체 이름이 아니라 임의의 템플릿 타입 변수로 취급됩니다. 템플릿 타입은 어떤 타입과도 매칭되므로, 이것은 우리가 만든 <code>Pair</code> 구조체뿐만 아니라 온갖 타입과 다 매칭되어 버립니다!</p>
<p>이것이 템플릿 매개변수 이름을 <code>T</code>, <code>U</code>, <code>N</code>처럼 짧고 단순하게 지어야 하는 이유입니다. 기존 클래스 이름과 겹쳐서 가려버릴 확률을 줄일 수 있으니까요.</p>
<hr>
<h3 id="stdpair">std::pair</h3>
<p>이렇게 두 개의 데이터를 쌍으로 묶어서 다루는 일은 실무에서 아주 흔합니다. 그래서 C++ 표준 라이브러리는 <code>&lt;utility&gt;</code> 헤더 안에 <strong>std::pair</strong> 라는 클래스 템플릿을 이미 제공하고 있습니다. 이 기능은 방금 우리가 만들었던 다중 템플릿 방식의 <code>Pair</code> 클래스와 완전히 동일합니다!</p>
<p>사실 우리가 짠 코드를 <code>std::pair</code>로 깔끔하게 바꿀 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;utility&gt;

template &lt;typename T, typename U&gt;
void print(std::pair&lt;T, U&gt; p)
{
    // std::pair의 멤버는 `first`와 `second`라는 미리 정의된 이름을 가집니다
    std::cout &lt;&lt; &#39;[&#39; &lt;&lt; p.first &lt;&lt; &quot;, &quot; &lt;&lt; p.second &lt;&lt; &#39;]&#39;;
}

int main()
{
    std::pair&lt;int, double&gt; p1{ 1, 2.3 }; // int와 double을 담고 있는 쌍(pair)
    std::pair&lt;double, int&gt; p2{ 4.5, 6 }; // double과 int를 담고 있는 쌍(pair)
    std::pair&lt;int, int&gt; p3{ 7, 8 };      // 두 개의 int를 담고 있는 쌍(pair)

    print(p2);

    return 0;
}
</code></pre>
<p>이 레슨에서는 내부 원리를 이해하기 위해 <code>Pair</code> 클래스를 직접 만들어 보았지만, 실제 프로그래밍을 할 때는 직접 만드는 것보다 표준 라이브러리인 <code>std::pair</code>를 사용하는 것을 강력히 권장합니다.</p>
<hr>
<h3 id="여러-파일에서-클래스-템플릿-사용하기">여러 파일에서 클래스 템플릿 사용하기</h3>
<p>함수 템플릿과 마찬가지로, 클래스 템플릿도 보통 <strong>헤더 파일(.h)</strong> 에 정의합니다. 그래야 코드가 필요한 여러 파일에서 <code>#include</code> 하여 사용할 수 있기 때문입니다. 템플릿 정의와 타입 정의는 C++의 &#39;단일 정의 원칙(One-definition rule)&#39;의 예외가 적용되므로 여러 파일에서 포함되어도 충돌을 일으키지 않습니다.</p>
<p><strong>pair.h:</strong></p>
<pre><code class="language-cpp">#ifndef PAIR_H
#define PAIR_H

template &lt;typename T&gt;
struct Pair
{
    T first{};
    T second{};
};

template &lt;typename T&gt;
constexpr T max(Pair&lt;T&gt; p)
{
    return (p.first &lt; p.second ? p.second : p.first);
}

#endif
</code></pre>
<p><strong>foo.cpp:</strong></p>
<pre><code class="language-cpp">#include &quot;pair.h&quot;
#include &lt;iostream&gt;

void foo()
{
    Pair&lt;int&gt; p1{ 1, 2 };
    std::cout &lt;&lt; max(p1) &lt;&lt; &quot; is larger\n&quot;;
}
</code></pre>
<p><strong>main.cpp:</strong></p>
<pre><code class="language-cpp">#include &quot;pair.h&quot;
#include &lt;iostream&gt;

void foo(); // 함수 foo()에 대한 전방 선언(forward declaration)

int main()
{
    Pair&lt;double&gt; p2 { 3.4, 5.6 };
    std::cout &lt;&lt; max(p2) &lt;&lt; &quot; is larger\n&quot;;

    foo();

    return 0;
}
</code></pre>
<hr>
<h2 id="1314--클래스-템플릿-인수-추론-ctad-및-추론-가이드">13.14 — 클래스 템플릿 인수 추론 (CTAD) 및 추론 가이드</h2>
<h3 id="클래스-템플릿-인수-추론-ctad-c17">클래스 템플릿 인수 추론 (CTAD) (C++17)</h3>
<p>C++17부터는 클래스 템플릿으로 객체를 생성할 때, 컴파일러가 객체의 초기값을 보고 템플릿 타입을 자동으로 알아낼(추론할) 수 있습니다. 이를 <strong>클래스 템플릿 인수 추론</strong> (Class template argument deduction), 줄여서 <strong>CTAD</strong> 라고 부릅니다. 예를 들어 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::pair를 사용하기 위해

int main()
{
    std::pair&lt;int, int&gt; p1{ 1, 2 }; // 클래스 템플릿 std::pair&lt;int, int&gt;를 명시적으로 지정 (C++11 이후)
    std::pair p2{ 1, 2 };           // 초기값으로부터 std::pair&lt;int, int&gt;를 추론하기 위해 CTAD 사용 (C++17)

    return 0;
}
</code></pre>
<p><strong>CTAD</strong> 는 템플릿 인수 목록이 아예 없을 때만 동작합니다. 따라서 다음 두 가지 경우는 모두 에러가 발생합니다.</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::pair를 사용하기 위해

int main()
{
    std::pair&lt;&gt; p1 { 1, 2 };    // 에러: 템플릿 인수가 너무 적음, 두 인수 모두 추론되지 않음
    std::pair&lt;int&gt; p2 { 3, 4 }; // 에러: 템플릿 인수가 너무 적음, 두 번째 인수가 추론되지 않음

    return 0;
}
</code></pre>
<blockquote>
<p><strong>저자의 노트</strong>
앞으로 이 사이트의 많은 강의에서 <strong>CTAD</strong> 를 사용할 예정입니다. 만약 C++14(또는 그 이전) 표준으로 예제 코드를 컴파일한다면 템플릿 인수가 빠졌다는 에러가 발생할 것입니다. 컴파일이 성공하게 하려면 예제 코드에 템플릿 인수를 직접 추가해 주어야 합니다.</p>
</blockquote>
<p><strong>CTAD</strong> 도 일종의 타입 추론이기 때문에, 리터럴 접미사(literal suffixes)를 사용해서 추론되는 타입을 바꿀 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::pair를 사용하기 위해

int main()
{
    std::pair p1 { 3.4f, 5.6f }; // pair&lt;float, float&gt;로 추론됨
    std::pair p2 { 1u, 2u };     // pair&lt;unsigned int, unsigned int&gt;로 추론됨

    return 0;
}
</code></pre>
<hr>
<h3 id="템플릿-인수-추론-가이드-c17">템플릿 인수 추론 가이드 (C++17)</h3>
<p>대부분의 경우 <strong>CTAD</strong> 는 별다른 설정 없이도 잘 작동합니다. 
하지만 어떤 경우에는 컴파일러가 템플릿 인수를 올바르게 추론할 수 있도록 약간의 힌트를 주어야 할 때가 있습니다.</p>
<p>위에서 본 <code>std::pair</code> 예제와 거의 똑같이 생긴 다음 프로그램이 오직 C++17에서만 컴파일되지 않는다는 사실에 놀라실 수도 있습니다.</p>
<pre><code class="language-cpp">// 우리만의 Pair 타입 정의
template &lt;typename T, typename U&gt;
struct Pair
{
    T first{};
    U second{};
};

int main()
{
    Pair&lt;int, int&gt; p1{ 1, 2 }; // 성공: 템플릿 인수를 명시적으로 지정함
    Pair p2{ 1, 2 };           // C++17에서 컴파일 에러 발생 (C++20에서는 성공)

    return 0;
}
</code></pre>
<p>이 코드를 C++17에서 컴파일하면 &quot;클래스 템플릿 인수 추론 실패&quot;나 &quot;템플릿 인수를 추론할 수 없음&quot; 또는 &quot;적합한 생성자나 추론 가이드가 없음&quot;과 같은 에러가 발생할 것입니다.</p>
<p>이런 에러가 나는 이유는, C++17에서는 <strong>CTAD</strong> 가 집합체(aggregate) 클래스 템플릿에 대해 템플릿 인수를 어떻게 추론해야 하는지 모르기 때문입니다. 이 문제를 해결하기 위해 컴파일러에게 <strong>추론 가이드</strong> (deduction guide) 를 제공하여, 특정 클래스 템플릿의 인수를 어떻게 추론해야 하는지 알려줄 수 있습니다.</p>
<p>다음은 추론 가이드를 추가한 동일한 프로그램입니다.</p>
<pre><code class="language-cpp">template &lt;typename T, typename U&gt;
struct Pair
{
    T first{};
    U second{};
};

// 다음은 우리가 만든 Pair를 위한 추론 가이드입니다 (C++17에서만 필요함)
// T와 U 타입의 인수로 초기화된 Pair 객체는 Pair&lt;T, U&gt;로 추론되어야 합니다
template &lt;typename T, typename U&gt;
Pair(T, U) -&gt; Pair&lt;T, U&gt;;

int main()
{
    Pair&lt;int, int&gt; p1{ 1, 2 }; // 클래스 템플릿 Pair&lt;int, int&gt;를 명시적으로 지정 (C++11 이후)
    Pair p2{ 1, 2 };           // 초기값으로부터 Pair&lt;int, int&gt;를 추론하기 위해 CTAD 사용 (C++17)

    return 0;
}
</code></pre>
<p>이 예제는 C++17에서 정상적으로 컴파일됩니다. 우리가 만든 <code>Pair</code> 클래스의 추론 가이드는 꽤 단순하지만, 어떻게 작동하는지 조금 더 자세히 살펴보겠습니다.</p>
<pre><code class="language-cpp">// 다음은 우리가 만든 Pair를 위한 추론 가이드입니다 (C++17에서만 필요함)
// T와 U 타입의 인수로 초기화된 Pair 객체는 Pair&lt;T, U&gt;로 추론되어야 합니다
template &lt;typename T, typename U&gt;
Pair(T, U) -&gt; Pair&lt;T, U&gt;;
</code></pre>
<p>첫째, <code>Pair</code> 클래스에서 사용한 것과 똑같은 템플릿 타입 정의를 사용합니다. 우리의 추론 가이드가 컴파일러에게 <code>Pair&lt;T, U&gt;</code>의 타입을 추론하는 방법을 알려주려면 <code>T</code>와 <code>U</code>가 무엇인지(템플릿 타입이라는 것을) 정의해야 하므로 이는 매우 자연스러운 일입니다.
둘째, 화살표 오른쪽에는 우리가 컴파일러의 추론을 도와줄 목적지 타입을 적습니다. 이 경우 컴파일러가 <code>Pair&lt;T, U&gt;</code> 타입 객체의 템플릿 인수를 추론할 수 있게 하고 싶으므로, 그 형태를 그대로 적어줍니다.
마지막으로, 화살표 왼쪽에는 컴파일러가 어떤 형태의 선언을 찾아야 하는지 알려줍니다. 여기서는 <code>Pair</code>라는 이름의 객체가 두 개의 인수(<code>T</code> 타입 하나, <code>U</code> 타입 하나)를 가진 선언을 찾으라고 알려주는 것입니다. 이를 <code>Pair(T t, U u)</code>처럼 적을 수도 있지만, 매개변수 이름인 <code>t</code>와 <code>u</code>는 실제로 사용하지 않기 때문에 이름을 생략해도 괜찮습니다.</p>
<p>요약하자면, 컴파일러가 두 개의 인수(각각 <code>T</code>와 <code>U</code> 타입)를 가지는 <code>Pair</code> 객체의 선언을 보게 되면 그 타입을 <code>Pair&lt;T, U&gt;</code>로 추론하라고 알려주는 것입니다.</p>
<p>따라서 프로그램 안에서 컴파일러가 <code>Pair p2{ 1, 2 };</code>라는 정의를 보게 되면, &quot;아, 이건 <code>Pair</code>의 선언이고 <code>int</code> 타입 인수가 두 개 있네. 그러니까 추론 가이드를 사용해서 이것을 <code>Pair&lt;int, int&gt;</code>로 추론해야겠다.&quot;라고 생각하게 됩니다.</p>
<p>다음은 단일 템플릿 타입을 사용하는 <code>Pair</code>의 비슷한 예시입니다.</p>
<pre><code class="language-cpp">template &lt;typename T&gt;
struct Pair
{
    T first{};
    T second{};
};

// 다음은 우리가 만든 Pair를 위한 추론 가이드입니다 (C++17에서만 필요함)
// T와 T 타입의 인수로 초기화된 Pair 객체는 Pair&lt;T&gt;로 추론되어야 합니다
template &lt;typename T&gt;
Pair(T, T) -&gt; Pair&lt;T&gt;;

int main()
{
    Pair&lt;int&gt; p1{ 1, 2 }; // 클래스 템플릿 Pair&lt;int&gt;를 명시적으로 지정 (C++11 이후)
    Pair p2{ 1, 2 };      // 초기값으로부터 Pair&lt;int&gt;를 추론하기 위해 CTAD 사용 (C++17)

    return 0;
}
</code></pre>
<p>이 경우, 우리의 추론 가이드는 <code>Pair(T, T)</code>(<code>T</code> 타입 인수가 두 개인 <code>Pair</code>)를 <code>Pair&lt;T&gt;</code>와 연결해 줍니다.</p>
<blockquote>
<p><strong>팁</strong>
C++20부터는 컴파일러가 집합체(aggregates)를 위한 추론 가이드를 자동으로 만들어주는 기능이 추가되었습니다. 따라서 추론 가이드는 오직 C++17과의 호환성을 위해서만 필요합니다.
그렇기 때문에 추론 가이드가 없는 버전의 <code>Pair</code>도 C++20에서는 잘 컴파일됩니다.
<code>std::pair</code>(그리고 다른 표준 라이브러리 템플릿 타입들)는 미리 정의된 추론 가이드를 포함하고 있습니다. 그래서 우리가 직접 추론 가이드를 제공하지 않아도 <code>std::pair</code>를 사용한 처음 예제가 C++17에서 정상적으로 컴파일되었던 것입니다.</p>
</blockquote>
<blockquote>
<p><strong>고급 독자를 위해</strong>
집합체가 아닌(Non-aggregates) 경우에는 생성자가 그 역할을 대신해 주기 때문에 C++17에서도 추론 가이드가 필요하지 않습니다.</p>
</blockquote>
<hr>
<h3 id="기본값이-있는-타입-템플릿-매개변수">기본값이 있는 타입 템플릿 매개변수</h3>
<p>함수 매개변수가 기본 인수를 가질 수 있는 것처럼, 템플릿 매개변수에도 기본값을 줄 수 있습니다. 이 기본값들은 템플릿 매개변수가 직접 명시되지 않았고 추론할 수도 없을 때 사용됩니다.</p>
<p>다음은 위에서 작성한 <code>Pair&lt;T, U&gt;</code> 클래스 템플릿 프로그램을 수정하여 타입 템플릿 매개변수 <code>T</code>와 <code>U</code>의 기본값을 <code>int</code> 타입으로 설정한 예시입니다.</p>
<pre><code class="language-cpp">template &lt;typename T=int, typename U=int&gt; // T와 U의 기본값을 int 타입으로 설정
struct Pair
{
    T first{};
    U second{};
};

template &lt;typename T, typename U&gt;
Pair(T, U) -&gt; Pair&lt;T, U&gt;;

int main()
{
    Pair&lt;int, int&gt; p1{ 1, 2 }; // 클래스 템플릿 Pair&lt;int, int&gt;를 명시적으로 지정 (C++11 이후)
    Pair p2{ 1, 2 };           // 초기값으로부터 Pair&lt;int, int&gt;를 추론하기 위해 CTAD 사용 (C++17)

    Pair p3;                   // 기본값인 Pair&lt;int, int&gt;를 사용

    return 0;
}
</code></pre>
<p><code>p3</code>를 정의할 때 타입 템플릿 매개변수에 대한 타입을 직접 명시하지 않았고, 타입을 추론할 수 있는 초기값도 없습니다. 따라서 컴파일러는 지정된 기본값을 사용하게 되며, 결과적으로 <code>p3</code>는 <code>Pair&lt;int, int&gt;</code> 타입이 됩니다.</p>
<hr>
<h3 id="비정적-멤버-초기화에서는-ctad가-동작하지-않습니다">비정적 멤버 초기화에서는 CTAD가 동작하지 않습니다</h3>
<p>비정적 멤버 초기화(non-static member initialization)를 사용해서 클래스 타입의 멤버를 초기화할 때는 이 상황에서 <strong>CTAD</strong> 가 동작하지 않습니다. 반드시 모든 템플릿 인수를 직접 명시해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;utility&gt; // std::pair를 사용하기 위해

struct Foo
{
    std::pair&lt;int, int&gt; p1{ 1, 2 }; // 성공, 템플릿 인수가 명시적으로 지정됨
    std::pair p2{ 1, 2 };           // 컴파일 에러, 이 문맥에서는 CTAD를 사용할 수 없음
};

int main()
{
    std::pair p3{ 1, 2 };           // 성공, 여기서는 CTAD를 사용할 수 있음
    return 0;
}
</code></pre>
<hr>
<h3 id="함수-매개변수에서는-ctad가-동작하지-않습니다">함수 매개변수에서는 CTAD가 동작하지 않습니다</h3>
<p>CTAD는 클래스 템플릿 인수(argument) 추론의 약자이며, 클래스 템플릿 매개변수(parameter) 추론이 아닙니다. 따라서 템플릿 매개변수의 타입이 아닌, 템플릿 인수의 타입만 추론합니다.</p>
<p>그러므로 함수 매개변수에는 <strong>CTAD</strong> 를 사용할 수 없습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;utility&gt;

void print(std::pair p) // 컴파일 에러, 여기서는 CTAD를 사용할 수 없음
{
    std::cout &lt;&lt; p.first &lt;&lt; &#39; &#39; &lt;&lt; p.second &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::pair p { 1, 2 }; // p는 std::pair&lt;int, int&gt;로 추론됨
    print(p);

    return 0;
}
</code></pre>
<p>이런 경우에는 대신 템플릿을 사용해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;utility&gt;

template &lt;typename T, typename U&gt;
void print(std::pair&lt;T, U&gt; p)
{
    std::cout &lt;&lt; p.first &lt;&lt; &#39; &#39; &lt;&lt; p.second &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::pair p { 1, 2 }; // p는 std::pair&lt;int, int&gt;로 추론됨
    print(p);

    return 0;
}
</code></pre>
<hr>
<h2 id="1315--템플릿-별칭-alias-templates">13.15 — 템플릿 별칭 (Alias templates)</h2>
<p>10.7 레슨 -- typedef와 타입 별칭(Type aliases)에서는 기존 타입에 대해 별칭을 지정하는 방법에 대해 알아보았습니다.</p>
<p>모든 템플릿 인수가 명확하게 지정된 클래스 템플릿의 별칭을 만드는 것은 일반적인 타입 별칭을 만드는 것과 똑같이 작동합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
struct Pair
{
    T first{};
    T second{};
};

template &lt;typename T&gt;
void print(const Pair&lt;T&gt;&amp; p)
{
    std::cout &lt;&lt; p.first &lt;&lt; &#39; &#39; &lt;&lt; p.second &lt;&lt; &#39;\n&#39;;
}

int main()
{
    using Point = Pair&lt;int&gt;; // 일반적인 타입 별칭 만들기
    Point p { 1, 2 };        // 컴파일러는 이것을 Pair&lt;int&gt;로 바꿉니다.

    print(p);

    return 0;
}
</code></pre>
<p>이러한 별칭은 (함수 내부처럼) 지역적으로 정의할 수도 있고, 전역적으로 정의할 수도 있습니다.</p>
<hr>
<h3 id="템플릿-별칭-alias-templates">템플릿 별칭 (Alias templates)</h3>
<p>때로는 템플릿 클래스의 별칭을 만들 때 템플릿 인수를 미리 다 정해두지 않고, 나중에 별칭을 사용하는 사람이 직접 인수를 넣도록 비워두고 싶을 때가 있습니다. 이럴 때 <strong>템플릿 별칭(Alias template)</strong> 을 정의할 수 있습니다. 템플릿 별칭은 말 그대로 타입 별칭을 찍어내는 템플릿이라고 생각하면 쉽습니다. 일반 타입 별칭이 아예 새로운 타입을 만드는 것이 아니듯, 템플릿 별칭도 완전히 새로운 독립적인 타입을 정의하는 것은 아닙니다.</p>
<p>작동 방식을 보여주는 예제는 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
struct Pair
{
    T first{};
    T second{};
};

// 여기에 템플릿 별칭이 있습니다.
// 템플릿 별칭은 반드시 전역 스코프(global scope)에 정의해야 합니다.
template &lt;typename T&gt;
using Coord = Pair&lt;T&gt;; // Coord는 Pair&lt;T&gt;의 별칭입니다.

// print 함수 템플릿은 Coord의 템플릿 매개변수 T가 &#39;타입 템플릿 매개변수&#39;라는 것을 알아야 합니다.
template &lt;typename T&gt;
void print(const Coord&lt;T&gt;&amp; c)
{
    std::cout &lt;&lt; c.first &lt;&lt; &#39; &#39; &lt;&lt; c.second &lt;&lt; &#39;\n&#39;;
}

int main()
{
    Coord&lt;int&gt; p1 { 1, 2 }; // C++20 이전: 모든 타입 템플릿 인수를 명시적으로 지정해야 합니다.
    Coord p2 { 1, 2 };      // C++20 이후: CTAD가 작동하는 상황에서는 &#39;템플릿 별칭 추론(alias template deduction)&#39;을 사용하여 인수를 알아낼 수 있습니다.

    std::cout &lt;&lt; p1.first &lt;&lt; &#39; &#39; &lt;&lt; p1.second &lt;&lt; &#39;\n&#39;;
    print(p2);

    return 0;
}
</code></pre>
<p>이 예제에서는 <code>Pair&lt;T&gt;</code>의 별칭으로 <code>Coord</code>라는 템플릿 별칭을 정의했습니다. 여기서 타입 템플릿 매개변수 <code>T</code>는 <code>Coord</code> 별칭을 사용하는 사용자가 나중에 정하게 됩니다. <code>Coord</code>는 템플릿 별칭이고, <code>Coord&lt;T&gt;</code>는 <code>Pair&lt;T&gt;</code>를 위해 생성된(인스턴스화된) 타입 별칭입니다. 한 번 정의하고 나면, <code>Pair</code>를 쓸 자리에 <code>Coord</code>를 쓸 수 있고, <code>Pair&lt;T&gt;</code>를 쓸 자리에 <code>Coord&lt;T&gt;</code>를 쓸 수 있습니다.</p>
<p>이 예제에서 기억해 둘 만한 몇 가지 중요한 점이 있습니다.</p>
<ul>
<li>첫째, 블록(중괄호) 안에서 정의할 수 있는 일반적인 타입 별칭과는 다르게, 템플릿 별칭은 (다른 모든 템플릿과 마찬가지로) 반드시 전역 스코프(global scope)에서 정의해야 합니다.</li>
<li>둘째, C++20 이전에는 템플릿 별칭으로 객체를 생성할 때 템플릿 인수를 직접 명확하게 적어주어야 했습니다. 하지만 C++20부터는 <strong>템플릿 별칭 추론(alias template deduction)</strong> 을 사용할 수 있습니다. 이를 통해 별칭이 지정된 원래 타입이 CTAD(클래스 템플릿 인수 추론)를 지원하는 경우라면, 초기화 값만 보고도 템플릿 인수의 타입을 자동으로 알아낼 수 있습니다.</li>
<li>셋째, CTAD는 함수 매개변수에서는 작동하지 않습니다. 따라서 템플릿 별칭을 함수 매개변수로 사용할 때는, 템플릿 별칭이 사용할 템플릿 인수를 반드시 명확하게 정의해주어야 합니다. 즉, 다음과 같이 작성해야 합니다.</li>
</ul>
<pre><code class="language-cpp">template &lt;typename T&gt;
void print(const Coord&lt;T&gt;&amp; c)
{
    std::cout &lt;&lt; c.first &lt;&lt; &#39; &#39; &lt;&lt; c.second &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p>다음과 같이 쓰면 안 됩니다.</p>
<pre><code class="language-cpp">void print(const Coord&amp; c) // 작동하지 않습니다. 템플릿 인수가 빠져 있습니다.
{
    std::cout &lt;&lt; c.first &lt;&lt; &#39; &#39; &lt;&lt; c.second &lt;&lt; &#39;\n&#39;;
}
</code></pre>
<p>이 규칙은 <code>Coord</code>나 <code>Coord&lt;T&gt;</code> 대신에 원래 타입인 <code>Pair</code>나 <code>Pair&lt;T&gt;</code>를 사용할 때의 규칙과 전혀 다르지 않습니다.</p>
<hr>
<h2 id="13y--언어-레퍼런스reference-사용하기">13.y — 언어 레퍼런스(Reference) 사용하기</h2>
<p>프로그래밍 언어(특히 C++) 학습 여정의 어느 단계에 있든, LearnCpp.com이 C++를 배우거나 무언가를 찾아보기 위해 사용하는 유일한 리소스일 수 있습니다. LearnCpp.com은 초보자에게 친숙한 방식으로 개념을 설명하도록 설계되었지만, 언어의 모든 측면을 다룰 수는 없습니다. 이 튜토리얼에서 다루는 주제를 벗어나 탐색을 시작하면, 필연적으로 이 튜토리얼에서 답하지 않는 질문에 부딪히게 될 것입니다. 이 경우 외부 리소스를 활용해야 합니다.</p>
<p>그런 리소스 중 하나가 질문을 올릴 수 있는(또는 더 좋게는, 다른 사람이 먼저 한 동일한 질문의 답변을 읽을 수 있는) <a href="https://stackoverflow.com">Stack Overflow</a>입니다. 하지만 때로는 레퍼런스 가이드(Reference guide, 참조 설명서)를 먼저 살펴보는 것이 더 나을 수 있습니다. 가장 중요한 주제에 집중하고 학습을 더 쉽게 하기 위해 비공식적이고 일반적인 언어를 사용하는 튜토리얼과 달리, 레퍼런스 가이드는 공식적인 용어를 사용하여 C++를 정확하게 설명합니다. 이 때문에 레퍼런스 자료는 포괄적이고 정확하며... 이해하기 어렵습니다.</p>
<p>이 레슨에서는 3가지 예제를 조사하면서 레슨 전반에 걸쳐 언급하는 인기 있는 표준 레퍼런스인 <a href="https://cppreference.com">cppreference</a>를 사용하는 방법을 보여드리겠습니다.</p>
<hr>
<h3 id="개요-overview">개요 (Overview)</h3>
<p>Cppreference는 핵심 언어와 라이브러리에 대한 <a href="https://en.cppreference.com/w/cpp">개요(overview)</a>로 여러분을 맞이합니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/overview-min.png" alt=""></p>
<p>여기서부터 cppreference가 제공하는 모든 것에 접근할 수 있지만, 검색 기능이나 검색 엔진을 사용하는 것이 더 쉽습니다. 이 개요는 LearnCpp.com의 튜토리얼을 마친 후 라이브러리를 더 깊이 파고들고, 아직 알지 못했던 언어의 다른 기능들을 확인하기 위해 방문하기에 아주 좋은 곳입니다.</p>
<p>표의 상단 절반은 현재 언어에 포함된 기능을 보여주는 반면, 하단 절반은 기술 사양(technical specifications)을 보여주는데, 이는 향후 C++ 버전에 추가될 수도 있고 추가되지 않을 수도 있는 기능이거나, 이미 언어에 부분적으로 수용된 기능입니다. 이는 곧 어떤 새로운 기능이 출시될지 확인하고 싶을 때 유용할 수 있습니다.</p>
<p>C++11부터 cppreference는 모든 기능에 해당 기능이 추가된 언어 표준 버전을 표시합니다. 표준 버전은 위 이미지의 일부 링크 옆에 보이는 작은 녹색 숫자입니다. 버전 번호가 없는 기능은 C++98/03부터 사용 가능했던 기능입니다. 버전 번호는 개요뿐만 아니라 cppreference의 모든 곳에 있으며, 특정 C++ 버전에서 무엇을 사용할 수 있고 없는지 정확히 알려줍니다.</p>
<blockquote>
<p><strong>경고 (Warning)</strong>
검색 엔진을 사용하는데 기술 사양이 막 표준으로 채택된 경우, 공식 레퍼런스 대신 기술 사양 문서로 연결될 수 있으며, 두 문서의 내용이 다를 수 있습니다.</p>
</blockquote>
<blockquote>
<p><strong>팁 (Tip)</strong>
Cppreference는 C++와 C 모두를 위한 레퍼런스입니다. C++는 C와 일부 함수 이름을 공유하기 때문에 무언가를 검색한 후 C 레퍼런스 페이지에 접속하게 될 수도 있습니다. cppreference 상단의 URL과 탐색 모음(navigation bar)은 현재 C 또는 C++ 레퍼런스를 탐색하고 있는지 항상 보여줍니다.</p>
</blockquote>
<hr>
<h3 id="stdstringlength">std::string::length</h3>
<p>이전 레슨에서 배운 함수인 문자열의 길이를 반환하는 <code>std::string::length</code>를 조사하는 것부터 시작하겠습니다.</p>
<p>cppreference 우측 상단에서 &quot;string&quot;을 검색해 보세요. 그러면 타입과 함수들의 긴 목록이 나타나며, 지금은 가장 위에 있는 것만 관련이 있습니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/string-search-ddg-min.png" alt=""></p>
<p>곧바로 &quot;string length&quot;를 검색할 수도 있었지만, 이번 레슨에서 가능한 많은 것을 보여주기 위해 긴 경로를 택하고 있습니다. &quot;Strings library&quot;를 클릭하면 C++가 지원하는 다양한 종류의 문자열에 대해 이야기하는 페이지로 이동합니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/strings-lib-min.png" alt=""></p>
<p>&quot;std::basic_string&quot; 섹션 아래를 보면 <code>typedef</code> 목록을 볼 수 있고, 그 목록 안에 <code>std::string</code>이 있습니다.</p>
<p>&quot;std::string&quot;을 클릭하면 <a href="https://en.cppreference.com/w/cpp/string/basic_string"><code>std::basic_string</code></a> 페이지로 연결됩니다. <code>std::string</code>은 <code>std::basic_string&lt;char&gt;</code>의 <code>typedef</code>이기 때문에 <code>std::string</code>에 대한 별도의 페이지는 없으며, 이는 <code>typedef</code> 목록에서 다시 확인할 수 있습니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/typedef-min.png" alt=""></p>
<p><code>&lt;char&gt;</code>는 문자열의 각 문자가 <code>char</code> 타입임을 의미합니다. C++는 다른 문자 타입을 사용하는 여러 문자열을 제공한다는 점에 유의하세요. 이들은 ASCII 대신 유니코드(Unicode)를 사용할 때 유용할 수 있습니다.</p>
<p>같은 페이지를 조금 더 내려가면 <a href="https://en.cppreference.com/w/cpp/string/basic_string#Member_functions">멤버 함수 목록</a>(타입이 갖는 동작)이 있습니다. 어떤 타입으로 무엇을 할 수 있는지 알고 싶다면 이 목록이 매우 편리합니다. 이 목록에서 <code>length</code>(그리고 <code>size</code>) 행을 찾을 수 있습니다.</p>
<p>링크를 따라가면 둘 다 동일한 작업을 수행하는 <a href="https://en.cppreference.com/w/cpp/string/basic_string/size"><code>length</code>와 <code>size</code></a>의 상세 함수 설명으로 이동합니다.</p>
<p>각 페이지의 상단은 해당 기능과 구문, 오버로드(overloads) 또는 선언에 대한 짧은 요약으로 시작합니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/string-length-overloads-min.png" alt=""></p>
<p>페이지 제목은 모든 템플릿 매개변수와 함께 클래스와 함수의 이름을 보여줍니다. 이 부분은 무시해도 됩니다. 제목 아래에는 다양한 함수 오버로드(같은 이름을 공유하는 여러 버전의 함수)와 그것들이 어떤 언어 표준에 적용되는지 볼 수 있습니다.</p>
<p>그 아래에서, 함수가 받는 매개변수들과 반환값이 의미하는 바를 볼 수 있습니다.</p>
<p><code>std::string::length</code>는 간단한 함수이기 때문에 이 페이지에는 내용이 많지 않습니다. 많은 페이지에서 해당 기능의 사용 예제를 보여주며, 이 페이지도 예외는 아닙니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/string-length-example-min.png" alt=""></p>
<p>C++를 여전히 배우고 있는 동안에는 예제에 아직 본 적 없는 기능들이 있을 것입니다. 예제가 충분히 있다면, 아마도 함수가 어떻게 사용되고 무엇을 하는지 아이디어를 얻을 수 있을 만큼은 이해할 수 있을 것입니다. 예제가 너무 복잡하다면 다른 곳에서 예제를 검색하거나, 이해하지 못하는 부분의 레퍼런스를 읽을 수 있습니다(예제에서 함수와 타입을 클릭하여 무엇을 하는지 볼 수 있습니다).</p>
<p>이제 우리는 <code>std::string::length</code>가 무엇을 하는지 알게 되었습니다. 비록 전부터 알고 있었지만 말이죠. 새로운 것을 한번 살펴봅시다!</p>
<hr>
<h3 id="stdcinignore">std::cin.ignore</h3>
<p>레슨 <a href="https://www.learncpp.com/cpp-tutorial/stdcin-and-handling-invalid-input/">9.5 -- std::cin과 잘못된 입력 처리</a>에서, 줄바꿈까지 모든 것을 무시하는 데 사용하는 <code>std::cin.ignore</code>에 대해 이야기했습니다. 이 함수의 매개변수 중 하나는 길고 장황한 값입니다. 그게 다시 뭐였죠? 그냥 큰 숫자를 사용하면 안 되나요? 어쨌든 이 인수는 무슨 역할을 할까요? 알아봅시다!</p>
<p>cppreference 검색창에 &quot;std::cin.ignore&quot;를 입력하면 다음과 같은 결과가 나옵니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/ignore-ddg-min.png" alt=""></p>
<ul>
<li><code>std::cin, std::wcin</code> - 우리는 단순한 <code>std::cin</code>이 아니라 <code>.ignore</code>를 원합니다.</li>
<li><code>std::basic_istream&lt;CharT,Traits&gt;::ignore</code> - 으, 이게 뭐죠? 일단 건너뜁시다.</li>
<li><code>std::ignore</code> - 아니요, 그게 아닙니다.</li>
<li><code>std::basic_istream</code> - 이것도 아닙니다.</li>
</ul>
<p>검색 결과에 원하는게 없네요, 이제 어쩌죠? <a href="https://en.cppreference.com/w/cpp/io/cin"><code>std::cin</code></a>으로 이동하여 거기서부터 찾아봅시다. 해당 페이지에 즉시 눈에 띄는 것은 없습니다. 상단에서 <code>std::cin</code> 및 <code>std::wcin</code>의 선언을 볼 수 있고, <code>std::cin</code>을 사용하기 위해 어떤 헤더를 포함해야 하는지 알려줍니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/cintop-min.png" alt=""></p>
<p><code>std::cin</code>이 <code>std::istream</code> 타입의 객체라는 것을 알 수 있습니다. <a href="https://en.cppreference.com/w/cpp/io/basic_istream"><code>std::istream</code></a> 링크를 따라가 봅시다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/basic_istream-min.png" alt=""></p>
<p>잠깐! 검색 엔진에서 &quot;std::cin.ignore&quot;를 검색했을 때 <code>std::basic_istream</code>을 본 적이 있습니다. 알고 보니 <code>istream</code>은 <code>basic_istream</code>의 <code>typedef</code> 이름이었으므로, 어쩌면 우리의 검색이 아주 틀린 것은 아니었을지도 모릅니다.</p>
<p>페이지를 아래로 스크롤하면 친숙한 함수들이 우리를 맞이합니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/members-min.png" alt=""></p>
<p>우리는 <code>operator&gt;&gt;</code>, <code>get</code>, <code>getline</code>, <code>ignore</code> 등 이 함수들 중 다수를 이미 사용해 보았습니다. 해당 페이지를 스크롤하며 <code>std::cin</code>에 또 어떤 것들이 있는지 살펴보세요. 그런 다음, 우리가 관심을 갖고 있는 <a href="https://en.cppreference.com/w/cpp/io/basic_istream/ignore"><code>ignore</code></a>를 클릭합니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/ignore-top-min.png" alt=""></p>
<p>페이지 상단에는 함수 시그니처와 함수 및 그 두 매개변수가 하는 역할에 대한 설명이 있습니다. 매개변수 뒤의 <code>=</code> 기호는 <strong>기본 인수(default argument)</strong>를 나타냅니다(이에 대해서는 레슨 11.5 -- 기본 인수에서 다룹니다). 기본값이 있는 매개변수에 대한 인수를 제공하지 않으면 기본값이 사용됩니다.</p>
<p>첫 번째 항목이 우리의 모든 질문에 답해줍니다. <code>std::numeric_limits&lt;std::streamsize&gt;::max()</code>가 문자 수 확인을 비활성화한다는 점에서 <code>std::cin.ignore</code>에 특별한 의미가 있다는 것을 알 수 있습니다. 이는 <code>std::cin.ignore</code>가 구분 기호를 찾거나 검사할 문자가 부족해질 때까지 계속해서 문자를 무시한다는 것을 의미합니다.</p>
<p>이미 알고 있는 함수의 매개변수나 반환값이 의미하는 바를 잊어버린 경우에는 전체 설명을 다 읽을 필요가 없는 경우가 많습니다. 그러한 상황에서는 매개변수나 반환값에 대한 설명만 읽어도 충분합니다.</p>
<p><img src="https://www.learncpp.com/blog/wp-content/uploads/images/CppTutorial/cppreference/parameters-return-min.png" alt=""></p>
<p>매개변수 설명은 간결합니다. <code>std::numeric_limits&lt;std::streamsize&gt;::max()</code>에 대한 특별한 처리나 다른 중지 조건을 포함하고 있지는 않지만, 좋은 리마인더(reminder) 역할을 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 12]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-12</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-12</guid>
            <pubDate>Sat, 21 Feb 2026 12:18:59 GMT</pubDate>
            <description><![CDATA[<h2 id="121--복합-데이터-타입compound-data-types-소개">12.1 — 복합 데이터 타입(Compound data types) 소개</h2>
<p>&#39;4.1 레슨 — 기본 데이터 타입 소개&#39;에서는 C++ 언어 자체에서 코어 언어의 일부로 제공하는 <strong>기본 데이터 타입(fundamental data types)</strong> 에 대해 알아보았습니다.</p>
<p>지금까지 우리는 프로그램에서 이런 기본 타입들, 특히 <code>int</code> 타입을 아주 많이 사용해 왔습니다. 기본 타입들은 간단한 작업에는 엄청나게 유용하지만, 조금 더 복잡한 작업을 시작하게 되면 우리가 필요로 하는 모든 것을 해결해 주지는 못합니다.</p>
<p>예를 들어, 두 분수를 곱하는 수학 프로그램을 만든다고 상상해 보세요. 프로그램에서 분수는 어떻게 표현할 수 있을까요? 아마도 아래처럼 두 개의 정수(하나는 분자용, 하나는 분모용)를 짝지어 사용할 수 있을 것입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    // 첫 번째 분수
    int num1 {};
    int den1 {};

    // 두 번째 분수
    int num2 {};
    int den2 {};

    // 분자와 분모 사이의 슬래시(/)를 무시(제거)하기 위해 사용
    char ignore {};

    std::cout &lt;&lt; &quot;Enter a fraction: &quot;;
    std::cin &gt;&gt; num1 &gt;&gt; ignore &gt;&gt; den1;

    std::cout &lt;&lt; &quot;Enter a fraction: &quot;;
    std::cin &gt;&gt; num2 &gt;&gt; ignore &gt;&gt; den2;

    std::cout &lt;&lt; &quot;The two fractions multiplied: &quot;
        &lt;&lt; num1 * num2 &lt;&lt; &#39;/&#39; &lt;&lt; den1 * den2 &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>프로그램의 실행 결과는 다음과 같습니다:</p>
<pre><code class="language-text">Enter a fraction: 1/2
Enter a fraction: 3/4
The two fractions multiplied: 3/8
</code></pre>
<p>이 프로그램은 잘 작동하지만, 앞으로 개선해야 할 몇 가지 문제점이 있습니다.
첫째, 각 정수 쌍이 아주 느슨하게만 연결되어 있다는 점입니다. 주석이나 코드가 사용된 문맥을 제외하면, 분자와 분모 쌍이 서로 관련되어 있다는 것을 알기 어렵습니다.
둘째, &#39;똑같은 코드를 반복하지 말라&#39;는 DRY(Don&#39;t Repeat Yourself) 원칙에 따라, 우리는 사용자가 분수를 입력하는 과정을 (오류 처리와 함께) 담당할 함수를 만들어야 합니다. 하지만 함수는 단 하나의 값만 반환할 수 있는데, 어떻게 분자와 분모 두 개의 값을 함수를 호출한 곳으로 돌려줄 수 있을까요?</p>
<p>자, 이번에는 직원들의 ID 목록을 저장해야 하는 프로그램을 작성한다고 상상해 봅시다. 어떻게 해야 할까요? 아마 다음과 같이 시도할 수 있을 겁니다.</p>
<pre><code class="language-cpp">int main(){
    int id1 { 42 };
    int id2 { 57 };
    int id3 { 162 };
    // 기타 등등 계속...
}
</code></pre>
<p>하지만 직원이 100명이라면 어떨까요? 우선 100개의 변수 이름을 일일이 타이핑해야 할 겁니다. 게다가 그 변수들을 전부 화면에 출력해야 하거나 함수로 전달해야 한다면요? 아마 코드를 치느라 엄청난 고생을 하게 될 것입니다. 이런 방식은 데이터가 많아질 때 감당하기 힘듭니다(확장성이 없습니다).</p>
<p>분명히 기본 데이터 타입만으로는 한계가 있습니다.</p>
<h3 id="복합-데이터-타입-compound-data-types">복합 데이터 타입 (Compound data types)</h3>
<p>다행히도 C++은 두 번째 데이터 타입 그룹인 <strong>복합 데이터 타입</strong>(때로는 합성 데이터 타입이라고도 함)을 지원합니다. 
복합 데이터 타입은 <strong>이미 존재하는 다른 데이터 타입들을 바탕으로 정의되는 타입</strong>입니다. 
이 타입들은 특정 종류의 문제를 해결하는 데 유용한 추가적인 특성과 동작 방식을 가지고 있습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
모든 데이터 타입은 &#39;기본 타입&#39; 아니면 &#39;복합 타입&#39;입니다. C++ 언어 표준은 각 타입이 어느 범주에 속하는지 명확하게 정의하고 있습니다.</p>
</blockquote>
<p>이번 장과 앞으로의 장들에서 배우겠지만, 우리는 복합 데이터 타입을 사용하여 위에서 언급한 모든 문제들을 아주 깔끔하게 해결할 수 있습니다.</p>
<p>C++은 다음과 같은 복합 타입들을 지원합니다.</p>
<table>
<thead>
<tr>
<th>큰 분류 (Category)</th>
<th>세부 분류 (Subcategory)</th>
</tr>
</thead>
<tbody><tr>
<td>함수 (Functions)</td>
<td>함수 (Functions)</td>
</tr>
<tr>
<td>C 스타일 배열 (C-style Arrays)</td>
<td>C 스타일 배열 (C-style Arrays)</td>
</tr>
<tr>
<td>포인터 타입 (Pointer types)</td>
<td>객체 포인터 (Pointer to object)</td>
</tr>
<tr>
<td>포인터 타입 (Pointer types)</td>
<td>함수 포인터 (Pointer to function)</td>
</tr>
<tr>
<td>멤버 포인터 타입 (Pointer to member types)</td>
<td>데이터 멤버 포인터 (Pointer to data member)</td>
</tr>
<tr>
<td>멤버 포인터 타입 (Pointer to member types)</td>
<td>멤버 함수 포인터 (Pointer to member function)</td>
</tr>
<tr>
<td>참조 타입 (Reference types)</td>
<td>L-value 참조 (L-value references)</td>
</tr>
<tr>
<td>참조 타입 (Reference types)</td>
<td>R-value 참조 (R-value references)</td>
</tr>
<tr>
<td>열거형 타입 (Enumerated types)</td>
<td>범위 없는 열거형 (Unscoped enumerations)</td>
</tr>
<tr>
<td>열거형 타입 (Enumerated types)</td>
<td>범위 있는 열거형 (Scoped enumerations)</td>
</tr>
<tr>
<td>클래스 타입 (Class types)</td>
<td>구조체 (Structs)</td>
</tr>
<tr>
<td>클래스 타입 (Class types)</td>
<td>클래스 (Classes)</td>
</tr>
<tr>
<td>클래스 타입 (Class types)</td>
<td>공용체 (Unions)</td>
</tr>
</tbody></table>
<p>사실 여러분은 이미 하나의 복합 타입을 규칙적으로 사용해 오고 있습니다. 바로 &#39;함수&#39;입니다. 예를 들어 다음과 같은 함수를 생각해 보세요.</p>
<pre><code class="language-cpp">void doSomething(int x, double y){}
</code></pre>
<p>이 함수의 타입은 <code>void(int, double)</code>입니다. 이 타입이 여러 기본 타입들로 구성되어 있다는 점에 주목하세요. 그래서 복합 타입인 것입니다. 물론 함수는 자신만의 특별한 동작(예: 호출 가능성)도 가지고 있습니다.</p>
<p>다뤄야 할 내용이 아주 많기 때문에, 우리는 여러 장에 걸쳐서 학습할 예정입니다. 
이번 장에서는 L-value 참조와 포인터를 포함하여 비교적 간단한 복합 타입들을 다루겠습니다. 
다음 장에서는 범위 없는 열거형, 범위 있는 열거형, 그리고 첫 번째 클래스 타입인 &#39;구조체(structs)&#39;를 배울 것입니다. 
그 이후의 장들에서는 &#39;클래스(classes)&#39;를 소개하고 더 유용한 &#39;배열(array)&#39; 타입들에 대해 깊이 파고들 것입니다. 
여기에는 <code>std::string</code>(5.7 레슨에서 소개됨)도 포함되는데, 이것도 사실 클래스 타입 중 하나랍니다!</p>
<blockquote>
<p><strong>용어 사전 (Nomenclature)</strong>
&#39;클래스 타입(class type)&#39;이란 구조체(struct), 클래스(class), 또는 공용체(union)를 의미하는 타입입니다. 앞으로의 레슨에서 이 용어를 아주 많이 사용하게 될 것입니다.</p>
</blockquote>
<hr>
<h2 id="122--값-카테고리-lvalue와-rvalue">12.2 — 값 카테고리 (lvalue와 rvalue)</h2>
<p>우리가 배울 첫 번째 복합 타입인 &#39;lvalue 참조(reference)&#39;에 대해 이야기하기 전에, 잠시 길을 벗어나 <strong>lvalue</strong>가 대체 무엇인지부터 알아보겠습니다.</p>
<p>이전 레슨(1.10 - 표현식 소개)에서 우리는 표현식(expression)을 &quot;하나의 값을 만들어내기 위해 실행할 수 있는 리터럴, 변수, 연산자, 함수 호출의 조합&quot;으로 정의했습니다.</p>
<p>예를 들어보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    std::cout &lt;&lt; 2 + 3 &lt;&lt; &#39;\n&#39;; // 2 + 3 표현식은 5라는 값을 만들어냅니다.

    return 0;
}
</code></pre>
<p>위 프로그램에서 <code>2 + 3</code>이라는 표현식은 평가(계산)되어 <code>5</code>라는 값을 만들고, 그 값이 콘솔 화면에 출력됩니다.</p>
<p>또한 레슨 6.4(증감 연산자와 부수 효과)에서는 표현식이 끝난 후에도 그 결과가 계속 남아있는 &#39;부수 효과(side effect)&#39;를 만들 수 있다는 점도 배웠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 };
    ++x; // 이 표현식 구문은 x의 값을 1 증가시키는 부수 효과를 가집니다.
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // 6을 출력합니다.

    return 0;
}
</code></pre>
<p>위 프로그램에서 <code>++x</code> 표현식은 <code>x</code>의 값을 증가시키며, 이 변경된 값은 표현식의 실행이 완전히 끝난 뒤에도 그대로 유지됩니다.</p>
<p>표현식은 값을 만들거나 부수 효과를 일으키는 것 외에도 한 가지 일을 더 할 수 있습니다. 바로 <strong>어떤 객체나 함수 그 자체로 해석(평가)되는 것</strong>입니다. 이에 대해서는 곧 자세히 살펴보겠습니다.</p>
<hr>
<h3 id="표현식의-두-가지-속성">표현식의 두 가지 속성</h3>
<p>컴파일러가 표현식을 어떻게 계산해야 하는지, 그리고 코드의 어느 위치에 쓸 수 있는지 판단하기 위해 C++의 모든 표현식은 두 가지 속성을 가집니다. 바로 <strong>타입(type)</strong>과 <strong>값 카테고리(value category)</strong>입니다.</p>
<hr>
<h3 id="표현식의-타입-type">표현식의 타입 (Type)</h3>
<p>표현식의 타입은 그 표현식을 <strong>계산했을 때</strong> 나오는 값, 객체, 또는 함수의 데이터 타입과 같습니다.</p>
<pre><code class="language-cpp">int main()
{
    auto v1 { 12 / 4 };   // 정수 / 정수 =&gt; 정수(int)
    auto v2 { 12.0 / 4 }; // 실수 / 정수 =&gt; 실수(double)

    return 0;
}
</code></pre>
<ul>
<li><p><code>v1</code>의 경우, 컴파일러는 두 개의 <code>int</code>(정수)를 나누면 <code>int</code> 결과가 나온다는 것을 컴파일할 때 미리 파악합니다. 
따라서 이 표현식의 타입은 <code>int</code>이며, 타입 추론(<code>auto</code>)을 통해 <code>v1</code>의 타입도 <code>int</code>로 정해집니다.</p>
</li>
<li><p><code>v2</code>의 경우, <code>double</code>(실수)과 <code>int</code>를 나누면 <code>double</code> 결과가 나옵니다. 산술 연산자는 양쪽의 타입이 같아야 하므로 <code>int</code>가 <code>double</code>로 변환되어 실수 나눗셈이 진행됩니다. 따라서 이 표현식의 타입은 <code>double</code>이 됩니다.</p>
</li>
</ul>
<p>컴파일러는 이 &#39;표현식의 타입&#39;을 보고, 현재 작성된 코드가 문법에 맞는지 확인합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int x)
{
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

int main()
{
    print(&quot;foo&quot;); // 오류: print()는 int형 인자를 기대하지만, 문자열 리터럴을 전달하려고 했습니다.

    return 0;
}
</code></pre>
<p>위 코드에서 <code>print(int)</code> 함수는 정수형 매개변수를 기대합니다. 하지만 우리가 전달한 표현식(문자열 <code>&quot;foo&quot;</code>)의 타입은 정수가 아니며 정수로 변환할 방법도 없기 때문에 컴파일 에러가 발생합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong> 
표현식의 <strong>타입</strong>은 반드시 컴파일 시간에 결정되어야 합니다(그래야 타입 검사와 추론이 정상 작동합니다). 하지만 표현식의 <strong>값</strong> 자체는 상황에 따라 컴파일 시간(<code>constexpr</code>인 경우)에 결정될 수도 있고, 프로그램이 실행되는 런타임에 결정될 수도 있습니다.</p>
</blockquote>
<hr>
<h3 id="표현식의-값-카테고리-value-category">표현식의 값 카테고리 (Value Category)</h3>
<p>이제 다음 프로그램을 살펴봅시다.</p>
<pre><code class="language-cpp">int main()
{
    int x{};

    x = 5; // 유효함: x에 5를 대입할 수 있습니다.
    5 = x; // 오류: 리터럴 값 5에 x의 값을 대입할 수 없습니다.

    return 0;
}
</code></pre>
<p>이 두 개의 대입문 중 하나는 정상이고 하나는 에러가 납니다(숫자 5에 변수 x를 넣는다는 건 말이 안 되니까요). 그렇다면 컴파일러는 대입 연산자(<code>=</code>)의 양쪽 끝에 어떤 표현식이 올 수 있는지 어떻게 알 수 있을까요?</p>
<p>그 해답이 바로 표현식의 두 번째 속성인 <strong>값 카테고리</strong>에 있습니다. 표현식의 값 카테고리는 해당 표현식이 <strong>값으로 해석되는지, 함수로 해석되는지, 아니면 특정한 형태의 객체로 해석되는지</strong>를 나타냅니다.</p>
<p>C++11 이전에는 딱 두 가지 카테고리만 존재했습니다. 바로 <strong>lvalue</strong>와 <strong>rvalue</strong>입니다.</p>
<blockquote>
<p><strong>작성자의 노트</strong>
C++11부터는 &#39;이동 의미론(move semantics)&#39;이라는 기능을 지원하기 위해 세 가지 추가 카테고리(glvalue, prvalue, xvalue)가 생겼습니다. 하지만 이 레슨에서는 초보자가 훨씬 쉽게 이해할 수 있도록 과거 기준인 lvalue와 rvalue로만 설명하겠습니다. 당장은 이 두 가지만 알아도 충분하며, 나머지 복잡한 카테고리는 나중에 다루겠습니다.</p>
</blockquote>
<hr>
<h3 id="lvalue와-rvalue-표현식">lvalue와 rvalue 표현식</h3>
<p><strong>lvalue</strong>는 <strong>식별 가능한 객체나 함수</strong>로 해석되는 표현식입니다.</p>
<p>C++ 표준에서 말하는 &quot;식별성(identity)&quot;이란, 메모리 주소 등을 통해 다른 비슷한 녀석들과 딱 꼬집어 구별할 수 있다는 뜻입니다. 
이렇게 식별 가능한 객체는 이름(식별자), 참조, 포인터 등을 통해 접근할 수 있고, 일반적으로 코드가 한 줄 실행되고 끝나는 것보다 더 오랫동안 메모리에 살아남습니다.</p>
<pre><code class="language-cpp">int main()
{
    int x { 5 };
    int y { x }; // x는 lvalue 표현식입니다.

    return 0;
}
</code></pre>
<p>위 코드에서 표현식 <code>x</code>는 이름이 있는 변수 <code>x</code> 그 자체를 가리키므로 <strong>lvalue 표현식</strong>입니다.</p>
<p>언어에 상수가 도입되면서 lvalue는 두 가지 하위 종류로 나뉘게 되었습니다. 
값을 수정할 수 있는 <strong>수정 가능한 lvalue</strong>와, <code>const</code>나 <code>constexpr</code>이 붙어있어 값을 바꿀 수 없는 <strong>수정 불가능한 lvalue</strong>입니다.</p>
<pre><code class="language-cpp">int main()
{
    int x{};
    const double d{};

    int y { x }; // x는 수정 가능한 lvalue 표현식입니다.
    const double e { d }; // d는 수정 불가능한 lvalue 표현식입니다.

    return 0;
}
</code></pre>
<p><strong>rvalue</strong>는 <strong>lvalue가 아닌 모든 표현식</strong>을 뜻합니다. rvalue 표현식은 하나의 일회성 <strong>&#39;값&#39;</strong>으로 해석됩니다.</p>
<p>흔히 볼 수 있는 rvalue로는 우리가 직접 적는 숫자나 문자 같은 리터럴(단, lvalue로 취급되는 C 스타일 문자열 리터럴은 제외)과 값을 반환하는 함수의 결과값 등이 있습니다. rvalue는 따로 이름표가 없어서 식별이 불가능하므로 생성되자마자 즉시 사용해야 하며, 해당 표현식이 실행되는 그 짧은 순간에만 존재합니다.</p>
<pre><code class="language-cpp">int return5()
{
    return 5;
}

int main()
{
    int x{ 5 }; // 5는 rvalue 표현식입니다.
    const double d{ 1.2 }; // 1.2는 rvalue 표현식입니다.

    int y { x }; // x는 수정 가능한 lvalue 표현식입니다.
    const double e { d }; // d는 수정 불가능한 lvalue 표현식입니다.
    int z { return5() }; // return5()는 rvalue 표현식입니다. (결과가 값으로 반환되기 때문)

    int w { x + 1 }; // x + 1은 rvalue 표현식입니다.
    int q { static_cast&lt;int&gt;(d) }; // d를 int로 정적 형변환(static cast)한 결과는 rvalue 표현식입니다.

    return 0;
}
</code></pre>
<p>왜 <code>return5()</code>, <code>x + 1</code>, <code>static_cast&lt;int&gt;(d)</code>가 rvalue인지 궁금하실 텐데요. 그 이유는 이 표현식들이 식별 가능한 실체(객체)를 나타내는 것이 아니라, 임시로 쓰이고 버려질 <strong>값</strong>을 만들어내기 때문입니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong></p>
<ul>
<li><strong>lvalue 표현식</strong>: 식별 가능한 &#39;객체&#39;를 나타냅니다.</li>
<li><strong>rvalue 표현식</strong>: 임시적인 &#39;값&#39;을 나타냅니다.</li>
</ul>
</blockquote>
<hr>
<h3 id="값-카테고리와-연산자">값 카테고리와 연산자</h3>
<p>특별한 언급이 없는 한, C++의 연산자들은 기본적으로 피연산자(계산의 대상)가 rvalue일 것을 기대합니다. 
예를 들어 더하기 연산자(<code>+</code>)는 양쪽 모두 rvalue가 오길 기다립니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    std::cout &lt;&lt; 1 + 2; // 1과 2는 rvalue이며, + 연산자는 계산 결과로 rvalue를 반환합니다.

    return 0;
}
</code></pre>
<p>리터럴 <code>1</code>과 <code>2</code>는 모두 rvalue 표현식입니다. <code>+</code> 연산자는 이 값들을 가져와 <code>3</code>이라는 새로운 rvalue 표현식을 기꺼이 반환해 줍니다.</p>
<p>이제 아까 보았던 <code>x = 5</code>는 되고 <code>5 = x</code>는 안 되는 이유를 명확히 대답할 수 있습니다. 대입 연산자(<code>=</code>)는 <strong>왼쪽에 반드시 &#39;수정 가능한 lvalue 표현식&#39;이 와야 합니다.</strong> <code>5 = x</code>가 실패하는 이유는 왼쪽의 <code>5</code>가 수정 가능한 lvalue가 아니라 단순한 rvalue 값이기 때문입니다.</p>
<pre><code class="language-cpp">int main()
{
    int x{};

    // 대입 연산은 왼쪽 피연산자로 수정 가능한 lvalue를, 오른쪽 피연산자로 rvalue를 요구합니다.
    x = 5; // 유효함: x는 수정 가능한 lvalue이고, 5는 rvalue입니다.
    5 = x; // 오류: 5는 rvalue이고, x는 수정 가능한 lvalue입니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="lvalue에서-rvalue로의-변환">lvalue에서 rvalue로의 변환</h3>
<p>대입 연산자는 오른쪽에 rvalue(값)가 오기를 기대한다고 했습니다. 그렇다면 다음 코드는 왜 문제없이 실행되는 걸까요?</p>
<pre><code class="language-cpp">int main()
{
    int x{ 1 };
    int y{ 2 };

    x = y; // y는 rvalue가 아니지만, 이 코드는 유효합니다.

    return 0;
}
</code></pre>
<p><strong>rvalue가 필요한 자리에 lvalue가 주어지면</strong>, C++은 그 상황에 맞춰 lvalue를 쓰기 위해 <strong>lvalue-to-rvalue 변환</strong>을 조용히 수행합니다. 
쉽게 말해 메모리에 저장된 lvalue 객체를 읽어들여서 알맹이 &#39;값(rvalue)&#39;만 쏙 뽑아낸다는 뜻입니다.</p>
<p>위 예제에서 lvalue인 <code>y</code>는 변환을 거쳐 <code>2</code>라는 rvalue 값을 만들어내고, 그 값이 최종적으로 <code>x</code>에 대입됩니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong></p>
<ul>
<li>lvalue는 rvalue로 자연스럽게(암시적으로) 변환될 수 있습니다. 즉, <strong>rvalue가 필요한 자리에는 어디든 lvalue를 쓸 수 있습니다.</strong></li>
<li>반면, <strong>rvalue는 lvalue로 변환되지 않습니다.</strong></li>
</ul>
</blockquote>
<p>이제 다음 예제를 보겠습니다.</p>
<pre><code class="language-cpp">int main()
{
    int x { 2 };

    x = x + 1;

    return 0;
}
</code></pre>
<p>이 한 줄의 구문에서 변수 <code>x</code>는 서로 다른 두 가지 역할로 쓰이고 있습니다. 대입 연산자(<code>=</code>)의 왼쪽에서는 변수 공간 그 자체를 가리키는 <strong>lvalue</strong>로 쓰입니다. 반대로 오른쪽에서는 덧셈을 하기 위해 <code>x</code> 안에 들어있던 값(2)을 뽑아내는 변환을 거쳐 <strong>rvalue</strong>로 쓰입니다. <code>+</code> 연산자는 3이라는 rvalue를 반환하고, 최종적으로 이 값이 다시 왼쪽의 lvalue <code>x</code>에 저장됩니다.</p>
<hr>
<h3 id="lvalue와-rvalue-구분하는-방법">lvalue와 rvalue 구분하는 방법</h3>
<p>여전히 어떤 것이 lvalue이고 rvalue인지 헷갈리실 수 있습니다. 
예를 들어, <code>++</code> 기호를 붙인 결과는 lvalue일까요, 아니면 rvalue일까요? 이를 구분하는 몇 가지 방법을 소개합니다.</p>
<blockquote>
<p><strong>팁 (간단한 판별법)</strong></p>
<ul>
<li><strong>lvalue 표현식</strong>: 함수나 식별 가능한 객체(변수 등)를 나타내며, 이 코드가 끝난 다음 줄에서도 메모리에 계속 살아남습니다.</li>
<li><strong>rvalue 표현식</strong>: 리터럴이나 임시 객체처럼 하나의 &#39;값&#39;으로 쓰이고, 해당 줄의 코드가 끝나면 흔적도 없이 사라집니다.</li>
</ul>
</blockquote>
<p>가장 확실하게 아는 방법은 컴파일러에게 직접 물어보는 코드를 짜는 것입니다. 
다음은 어떤 표현식이 lvalue인지 rvalue인지 판별해 주는 흥미로운 코드입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

// T&amp;는 lvalue 참조이므로, lvalue에 대해서는 이 오버로딩 함수가 우선적으로 선택됩니다.
template &lt;typename T&gt;
constexpr bool is_lvalue(T&amp;)
{
    return true;
}

// T&amp;&amp;는 rvalue 참조이므로, rvalue에 대해서는 이 오버로딩 함수가 우선적으로 선택됩니다.
template &lt;typename T&gt;
constexpr bool is_lvalue(T&amp;&amp;)
{
    return false;
}

// 도우미 매크로 (#expr은 expr에 전달된 내용을 텍스트 그대로 출력해 줍니다)
#define PRINTVCAT(expr) { std::cout &lt;&lt; #expr &lt;&lt; &quot; is an &quot; &lt;&lt; (is_lvalue(expr) ? &quot;lvalue\n&quot; : &quot;rvalue\n&quot;); }

int getint() { return 5; }

int main()
{
    PRINTVCAT(5);        // rvalue
    PRINTVCAT(getint()); // rvalue

    int x { 5 };
    PRINTVCAT(x);        // lvalue
    PRINTVCAT(std::string {&quot;Hello&quot;}); // rvalue
    PRINTVCAT(&quot;Hello&quot;);  // lvalue

    PRINTVCAT(++x);      // lvalue
    PRINTVCAT(x++);      // rvalue
}
</code></pre>
<p>출력 결과는 다음과 같습니다.</p>
<pre><code class="language-text">5 is an rvalue
getint() is an rvalue
x is an lvalue
std::string {&quot;Hello&quot;} is an rvalue
&quot;Hello&quot; is an lvalue
++x is an lvalue
x++ is an rvalue
</code></pre>
<p>이 판별법은 C++의 오버로딩(중복 정의) 기능을 활용한 것입니다. 
인자로 lvalue가 들어오면 lvalue 전용 함수가, rvalue가 들어오면 rvalue 전용 함수가 선택되는 원리입니다.</p>
<p>출력 결과를 보면 재미있는 사실을 알 수 있습니다. 
증감 연산자는 <strong>앞에 붙이면(전위 연산자, ++x) lvalue</strong>를 반환하지만, <strong>뒤에 붙이면(후위 연산자, x++) rvalue</strong>를 반환한다는 것입니다!</p>
<blockquote>
<p><strong>심화 학습 독자를 위해</strong>
숫자를 비롯한 일반 리터럴들은 모두 rvalue입니다. 하지만 <code>&quot;Hello&quot;</code> 같은 <strong>C 스타일 문자열 리터럴은 특이하게도 lvalue</strong>입니다. C 스타일 문자열(문자 배열)은 내부적으로 포인터로 변환되는 특징을 가집니다. 메모리 주소가 있어야만 포인터로 변환이 가능하기 때문에 이 배열은 lvalue여야만 합니다. C++은 오래된 C 언어 코드와의 호환성을 유지하기 위해 이 특징을 그대로 물려받았습니다. (배열 붕괴에 대해서는 나중에 17.8 레슨에서 다루겠습니다.)</p>
</blockquote>
<p>자, 이제 lvalue가 무엇인지 이해했으니 우리의 첫 번째 복합 타입인 <strong>lvalue 참조(lvalue reference)</strong>를 배울 준비가 끝났습니다!</p>
<hr>
<h2 id="123--lvalue-참조-lvalue-references">12.3 — Lvalue 참조 (Lvalue references)</h2>
<p>C++에서 <strong>참조(reference)</strong>는 이미 존재하는 객체(변수 등)에 붙이는 &#39;별명(alias)&#39;입니다. 한 번 참조를 정의하고 나면, 그 참조를 통해 하는 모든 작업은 실제 가리키고 있는 원본 객체에 똑같이 적용됩니다. 즉, 참조를 사용해 원본 객체의 값을 읽거나 수정할 수 있습니다.</p>
<p>처음에는 참조가 굳이 왜 필요한지, 쓸모없거나 불필요해 보일 수 있습니다. 하지만 참조는 C++ 전반에 걸쳐 아주 많이 사용됩니다. (앞으로 다가올 레슨들에서 그 예시를 보게 될 것입니다.)</p>
<blockquote>
<p><strong>핵심 포인트</strong>
참조는 본질적으로 가리키는 대상(원본 객체)과 완전히 동일하다고 생각하시면 됩니다. 함수에 대한 참조도 만들 수 있지만, 자주 쓰이진 않습니다. 모던 C++에는 두 가지 종류의 참조가 있습니다. 바로 &#39;lvalue 참조&#39;와 &#39;rvalue 참조&#39;입니다. 이번 장에서는 lvalue 참조에 대해 알아봅니다.</p>
</blockquote>
<hr>
<h3 id="lvalue-참조-타입">Lvalue 참조 타입</h3>
<p><strong>lvalue 참조</strong>(C++11 이전에는 참조가 하나뿐이었기 때문에 보통 그냥 &quot;참조&quot;라고 부릅니다)는 기존의 lvalue(보통 변수)에 대한 별명 역할을 합니다.</p>
<p>객체의 타입이 어떤 종류의 값을 담을 수 있는지 결정하듯, 참조의 타입은 어떤 타입의 객체를 가리킬 수 있는지를 결정합니다. 
Lvalue 참조 타입은 타입 이름 뒤에 앰퍼샌드 기호(<code>&amp;</code>)를 하나 붙여서 표시합니다.</p>
<pre><code class="language-cpp">// 일반적인 타입들
int        // 일반 int 타입 (참조 아님)
int&amp;       // int 객체에 대한 lvalue 참조
double&amp;    // double 객체에 대한 lvalue 참조
const int&amp; // const int 객체에 대한 lvalue 참조</code></pre>
<p>예를 들어, <code>int&amp;</code>는 <code>int</code> 타입의 객체를 가리키는 lvalue 참조 타입이고, <code>const int&amp;</code>는 <code>const int</code> 타입의 객체를 가리키는 lvalue 참조 타입입니다.</p>
<p>이때 <code>int&amp;</code>처럼 참조를 명시하는 타입을 <strong>참조 타입(reference type)</strong>이라고 부르고, <code>int</code>처럼 참조가 가리키는 원본 대상을 <strong>참조되는 타입(referenced type)</strong>이라고 부릅니다.</p>
<blockquote>
<p><strong>용어 정리</strong>
Lvalue 참조에는 두 가지 종류가 있습니다:</p>
<ol>
<li><code>const</code>가 아닌 lvalue 참조는 보통 그냥 &quot;lvalue 참조&quot;라고 부릅니다. 명확히 구분하기 위해 <strong>non-const lvalue 참조</strong>라고 부르기도 합니다.</li>
<li><code>const</code>인 lvalue 참조는 보통 <strong>const lvalue 참조</strong>라고 부릅니다.</li>
</ol>
<p>이번 레슨에서는 non-const lvalue 참조에 집중하고, const lvalue 참조는 다음 레슨(12.4)에서 다루겠습니다.</p>
</blockquote>
<hr>
<h3 id="lvalue-참조-변수">Lvalue 참조 변수</h3>
<p>lvalue 참조 타입을 사용해 할 수 있는 일 중 하나는 <strong>lvalue 참조 변수</strong>를 만드는 것입니다. 
이는 lvalue(보통 다른 변수)에 대한 참조 역할을 하는 변수입니다.</p>
<p>간단히 lvalue 참조 타입으로 변수를 정의하면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 };    // x는 일반 정수 변수입니다.
    int&amp; ref { x }; // ref는 이제 변수 x의 별명으로 사용할 수 있는 lvalue 참조 변수입니다.

    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;  // x의 값을 출력 (5)
    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // ref를 통해 x의 값을 출력 (5)

    return 0;
}
</code></pre>
<p>위 예제에서 <code>int&amp;</code> 타입은 <code>ref</code>를 <code>int</code>형에 대한 참조로 정의하고, 이를 변수 <code>x</code>에 연결(초기화)합니다. 
그 후에는 <code>ref</code>와 <code>x</code>를 똑같은 변수처럼 사용할 수 있습니다. 따라서 이 프로그램의 출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">5
5
</code></pre>
<p>컴파일러 입장에서는 앰퍼샌드(<code>&amp;</code>)가 타입 이름에 붙든(<code>int&amp; ref</code>), 변수 이름에 붙든(<code>int &amp;ref</code>) 상관하지 않습니다. 어떤 방식을 쓸지는 온전히 코딩 스타일의 문제입니다. 다만, 모던 C++ 프로그래머들은 참조가 변수 이름의 일부가 아니라 &#39;타입 정보의 일부&#39;라는 것을 명확히 하기 위해 <strong>타입 이름 쪽에 앰퍼샌드를 붙이는 것</strong>을 선호합니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
참조를 정의할 때 앰퍼샌드(<code>&amp;</code>)는 변수 이름이 아니라 타입 이름 옆에 붙이세요.</p>
</blockquote>
<blockquote>
<p><strong>고급 독자를 위한 팁</strong>
포인터에 이미 익숙한 분들에게 덧붙이자면, 이 문맥에서 앰퍼샌드(<code>&amp;</code>)는 &quot;메모리 주소&quot;를 의미하는 것이 아닙니다. &quot;lvalue 참조&quot;를 의미합니다.</p>
</blockquote>
<hr>
<h3 id="non-const-lvalue-참조를-통해-값-수정하기">non-const lvalue 참조를 통해 값 수정하기</h3>
<p>위 예제에서 참조를 사용해 원본 객체의 값을 읽어오는 방법을 보았습니다. 
마찬가지로, non-const 참조를 이용해 가리키고 있는 대상의 값을 수정할 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 }; // 일반 정수 변수
    int&amp; ref { x }; // ref는 이제 변수 x의 별명입니다.

    std::cout &lt;&lt; x &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 55 출력

    x = 6; // x의 값은 이제 6입니다.

    std::cout &lt;&lt; x &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 66 출력

    ref = 7; // 참조하는 객체(x)의 값이 이제 7로 바뀝니다.

    std::cout &lt;&lt; x &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 77 출력

    return 0;
}
</code></pre>
<p>이 코드의 출력 결과는 다음과 같습니다:</p>
<pre><code class="language-text">55
66
77
</code></pre>
<p><code>ref</code>는 <code>x</code>의 완벽한 별명이므로, <code>x</code>를 직접 수정하든 <code>ref</code>를 통해 수정하든 결국 <strong>똑같이</strong> <code>x</code>의 값이 바뀝니다.</p>
<hr>
<h3 id="참조-초기화">참조 초기화</h3>
<p>상수(constant)와 마찬가지로, <strong>모든 참조는 반드시 초기화되어야 합니다.</strong> 
참조는 &#39;참조 초기화(reference initialization)&#39;라는 방식을 통해 초기화됩니다.</p>
<pre><code class="language-cpp">int main()
{
    int&amp; invalidRef;   // 오류: 참조는 반드시 초기화되어야 합니다.

    int x { 5 };
    int&amp; ref { x }; // 정상: int 참조가 int 변수에 연결되었습니다.

    return 0;
}
</code></pre>
<p>참조가 어떤 객체(또는 함수)에 연결될 때, 우리는 참조가 그 객체에 <strong>바인딩(bound)</strong>되었다고 표현합니다. 
이 연결 과정을 <strong>참조 바인딩(reference binding)</strong>이라고 부르며, 대상이 되는 원본 객체를 <strong>참조 대상(referent)</strong>이라고 부릅니다.</p>
<p>Non-const lvalue 참조는 <strong>수정 가능한</strong> lvalue에만 연결될 수 있습니다.</p>
<pre><code class="language-cpp">int main()
{
    int x { 5 };
    int&amp; ref { x };         // 정상: 수정 가능한 lvalue에 non-const lvalue 참조가 바인딩됨

    const int y { 5 };
    int&amp; invalidRef { y };  // 오류: 수정 불가능한 lvalue에는 non-const lvalue 참조를 바인딩할 수 없음
    int&amp; invalidRef2 { 0 }; // 오류: rvalue에는 non-const lvalue 참조를 바인딩할 수 없음

    return 0;
}
</code></pre>
<blockquote>
<p><strong>핵심 포인트</strong>
만약 수정 불가능한 상수(const)나 rvalue에 참조를 연결할 수 있다면, 참조를 통해 그 값을 마음대로 바꿔버릴 수 있게 됩니다. 
이는 상수의 의미를 망가뜨리는 것이므로 C++에서는 이를 막고 있습니다.
(<code>void</code> 타입에 대한 참조 역시 허용되지 않습니다. 애초에 의미가 없으니까요.)</p>
</blockquote>
<p>참조의 타입(예: <code>int&amp;</code>)과 대상 객체의 타입(예: <code>int</code>)이 문자 그대로 똑같지 않더라도, 여기서는 형변환(conversion)이 일어나지 않습니다. 
타입의 차이는 참조 초기화 과정에서 내부적으로 처리됩니다.</p>
<hr>
<h3 id="참조는-대부분-자신의-타입과-일치하는-객체에만-연결됩니다">참조는 (대부분) 자신의 타입과 일치하는 객체에만 연결됩니다</h3>
<p>대부분의 경우, 참조는 자신의 &#39;참조되는 타입&#39;과 일치하는 타입의 객체에만 연결될 수 있습니다. 
(나중에 상속을 배울 때 일부 예외를 다룰 것입니다.)</p>
<p>만약 타입이 일치하지 않는 객체에 참조를 연결하려고 시도하면, 
컴파일러는 객체를 해당 타입으로 몰래 변환(암시적 변환)한 뒤에 연결하려고 시도합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
하지만 변환되어 나온 결과물은 rvalue가 됩니다. 앞서 말했듯 non-const lvalue 참조는 rvalue에 연결될 수 없으므로, 결국 타입이 맞지 않는 객체에 연결하려 하면 컴파일 오류가 발생합니다.</p>
</blockquote>
<pre><code class="language-cpp">int main()
{
    int x { 5 };
    int&amp; ref { x };            // 정상: 참조하는 타입(int)이 초기화 값의 타입과 일치함

    double d { 6.0 };
    int&amp; invalidRef { d };     // 오류: double을 int로 변환하는 것은 데이터 손실 우려가 있어 중괄호 초기화에서 금지됨
    double&amp; invalidRef2 { x }; // 오류: x를 double로 변환한 결과는 rvalue이므로 non-const lvalue 참조에 바인딩할 수 없음

    return 0;
}
</code></pre>
<hr>
<h3 id="참조는-한-번-연결되면-다른-객체로-대상을-바꿀-수-없습니다-cant-be-reseated">참조는 한 번 연결되면 다른 객체로 대상을 바꿀 수 없습니다 (Can&#39;t be reseated)</h3>
<p>한 번 초기화되고 나면, C++의 참조는 다시 연결(<strong>reseated</strong>)될 수 없습니다. 즉, 다른 객체를 가리키도록 중간에 변경할 수 없습니다.</p>
<p>초보자분들은 종종 할당 연산자(<code>=</code>)를 사용해 참조가 다른 변수를 가리키게 만들려고 시도합니다. 코드는 문제없이 컴파일되고 실행되지만, 예상과는 전혀 다르게 동작합니다. 아래 프로그램을 확인해 보세요:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 };
    int y { 6 };

    int&amp; ref { x }; // ref는 이제 x의 별명입니다.

    ref = y; // y의 값(6)을 x(ref가 참조하고 있는 객체)에 할당합니다.
    // 위 코드는 ref가 y를 가리키도록 대상을 변경하는 것이 아닙니다!

    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // 사용자는 이 코드가 5를 출력할 것이라고 예상할 수 있습니다.

    return 0;
}
</code></pre>
<p>이 코드는 다음을 출력합니다.</p>
<pre><code class="language-text">6
</code></pre>
<p>코드에서 참조가 사용되면, 이는 가리키고 있는 대상 객체로 치환됩니다. 따라서 <code>ref = y</code>는 <code>ref</code>의 방향을 <code>y</code>로 바꾸는 것이 아닙니다. 
<code>ref</code>는 <code>x</code>의 별명이므로 <code>x = y</code>라고 쓴 것과 완전히 똑같이 작동합니다. <code>y</code>의 값이 6이기 때문에 결국 <code>x</code>에 6이 덮어씌워진 것입니다.</p>
<hr>
<h3 id="참조의-범위scope와-수명duration">참조의 범위(Scope)와 수명(Duration)</h3>
<p>참조 변수 역시 일반 변수와 똑같은 범위와 수명 규칙을 따릅니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 }; // 일반 정수
    int&amp; ref { x }; // 변수 값에 대한 참조

     return 0;
} // x와 ref는 여기서 소멸됩니다.
</code></pre>
<hr>
<h3 id="참조와-원본-객체는-서로-수명이-독립적입니다">참조와 원본 객체는 서로 수명이 독립적입니다</h3>
<p>(다음 레슨에서 다룰 한 가지 예외를 제외하면) 참조의 수명과 원본 객체의 수명은 서로 아무런 관련이 없습니다. 
즉, 다음 두 가지가 모두 가능합니다:</p>
<ol>
<li>원본 객체보다 참조가 먼저 소멸될 수 있습니다.</li>
<li>참조보다 원본 객체가 먼저 소멸될 수 있습니다.</li>
</ol>
<p>참조가 원본 객체보다 먼저 소멸되더라도 원본 객체에는 아무런 영향을 주지 않습니다. 아래 예시를 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 };

    {
        int&amp; ref { x };   // ref는 x에 대한 참조입니다.
        std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // ref의 값을 출력 (5)
    } // ref는 여기서 소멸됩니다. x는 이 사실을 알지 못합니다.

    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // x의 값을 출력 (5)

    return 0;
} // x는 여기서 소멸됩니다.
</code></pre>
<p>위 코드는 다음을 출력합니다:</p>
<pre><code class="language-text">5
5
</code></pre>
<p><code>ref</code>가 블록을 벗어나 소멸되더라도, 변수 <code>x</code>는 누군가 자신을 가리키던 참조가 사라졌다는 사실을 전혀 모른 채 평소처럼 잘 살아있습니다.</p>
<hr>
<h3 id="허상-참조-dangling-references">허상 참조 (Dangling references)</h3>
<p>만약 원본 객체가 참조보다 먼저 파괴되어 사라져 버리면 어떻게 될까요? 
참조는 이미 존재하지도 않는 객체를 허공에 가리키고 있게 됩니다. 이런 상태의 참조를 <strong>허상 참조(dangling reference)</strong>라고 부릅니다.</p>
<p>이 허상 참조에 접근해 값을 읽거나 쓰려고 시도하면 예측할 수 없는 심각한 오류(정의되지 않은 동작)가 발생합니다. 허상 참조를 만드는 것은 피하기 쉬운 편이지만, 실제 프로그래밍에서 어떻게 이런 일이 일어나는지는 <strong>12.12 - 참조로 반환하기</strong> 레슨에서 보여드리겠습니다.</p>
<hr>
<h3 id="참조는-객체가-아닙니다">참조는 &#39;객체&#39;가 아닙니다</h3>
<p>다소 의외일 수 있지만, C++에서 참조는 엄밀히 말해 &#39;객체(object)&#39;가 아닙니다.</p>
<p>참조는 물리적인 메모리 공간을 차지할 필요가 없습니다. 가능하다면 컴파일러는 참조가 쓰인 모든 부분을 실제 원본 객체로 대체해 버리고 참조 자체를 흔적도 없이 최적화해 지워버립니다. (물론 항상 가능한 것은 아니며, 상황에 따라 공간을 차지할 수도 있습니다.)</p>
<p>이러한 사실 때문에 &quot;참조 변수&quot;라는 말은 약간 어색한 표현이기도 합니다. 
변수란 &#39;이름을 가진 객체&#39;인데, 참조는 애초에 객체가 아니기 때문입니다.</p>
<p>참조는 객체가 아니므로, 객체가 꼭 들어가야 하는 자리에는 참조를 사용할 수 없습니다. 예를 들어, *<em>&#39;참조에 대한 참조&#39;는 만들 수 없습니다.
*</em> Lvalue 참조는 오직 식별 가능한 &#39;객체&#39;만 가리킬 수 있기 때문입니다. 만약 진짜 객체처럼 다룰 수 있는 참조나, 중간에 대상을 바꿀 수 있는(reseat) 참조가 꼭 필요하다면 <code>std::reference_wrapper</code>(23.3장에서 다룸)를 사용하면 됩니다.</p>
<blockquote>
<p><strong>참고로...</strong>
다음 변수들을 한번 살펴보세요:</p>
<pre><code class="language-cpp">int var{};
int&amp; ref1{ var };  // var에 바인딩된 lvalue 참조
int&amp; ref2{ ref1 }; // var에 바인딩된 lvalue 참조
</code></pre>
<p><code>ref2</code>가 참조인 <code>ref1</code>로 초기화되었기 때문에, <code>ref2</code>가 &#39;참조에 대한 참조&#39;라고 착각하기 쉽습니다. 하지만 아닙니다. <code>ref1</code>은 <code>var</code>의 별명이므로, 수식에서 사용될 때 <code>ref1</code>은 결국 <code>var</code>로 취급됩니다. 따라서 <code>ref2</code> 역시 그저 <code>var</code>에 직접 연결된 평범한 참조일 뿐입니다.
진짜로 참조에 대한 참조를 만들려면 <code>int&amp;&amp;</code> 같은 문법을 써야겠지만, C++은 이를 지원하지 않습니다. 대신 이 문법은 C++11부터 <strong>rvalue 참조</strong>(22.2장에서 다룸)를 나타내는 용도로 완전히 새롭게 정의되었습니다.</p>
</blockquote>
<blockquote>
<p><strong>작성자의 노트</strong>
지금 단계에서는 참조가 조금 쓸모없어 보일지도 모르겠습니다. 하지만 전혀 걱정하지 마세요. 참조는 앞으로 엄청나게 많이 쓰일 것이며, 왜 그렇게 중요한지는 곧 다가올 <strong>12.5 - lvalue 참조로 전달하기</strong>와 <strong>12.6 - const lvalue 참조로 전달하기</strong>에서 확실히 알게 되실 겁니다.</p>
</blockquote>
<hr>
<h2 id="124--const에-대한-lvalue-참조-lvalue-references-to-const">12.4 — const에 대한 Lvalue 참조 (Lvalue references to const)</h2>
<p>이전 레슨(12.3 - Lvalue 참조)에서는 Lvalue 참조가 오직 &#39;수정 가능한 Lvalue&#39;에만 연결(바인딩, bind)될 수 있다는 점을 배웠습니다. 
즉, 아래와 같이 코드를 작성하면 오류가 발생합니다.</p>
<pre><code class="language-cpp">int main()
{
    const int x { 5 }; // x는 수정 불가능한 (const) Lvalue입니다.
    int&amp; ref { x }; // 오류: ref는 수정 불가능한 Lvalue에 바인딩될 수 없습니다.

    return 0;
}
</code></pre>
<p>이 코드가 허용되지 않는 이유는 간단합니다. 
만약 이게 가능하다면, 일반 참조(<code>ref</code>)를 사용해 상수로 선언된 변수(<code>x</code>)의 값을 몰래 바꿀 수 있게 되기 때문입니다.</p>
<p>그렇다면 상수(const) 변수를 참조하고 싶을 때는 어떻게 해야 할까요? 일반적인 Lvalue 참조로는 불가능합니다.</p>
<hr>
<h3 id="const에-대한-lvalue-참조">const에 대한 Lvalue 참조</h3>
<p>Lvalue 참조를 선언할 때 <code>const</code> 키워드를 함께 사용하면, 이 참조가 가리키는 대상을 &#39;수정할 수 없는 상태(const)&#39;로 취급하라고 컴파일러에게 알려주게 됩니다. 이를 <strong>const 값에 대한 Lvalue 참조</strong>(간단히 &#39;const 참조&#39;라고도 부릅니다)라고 합니다.</p>
<p>이러한 const Lvalue 참조는 수정 불가능한 Lvalue에 정상적으로 연결될 수 있습니다.</p>
<pre><code class="language-cpp">int main()
{
    const int x { 5 }; // x는 수정 불가능한 Lvalue입니다.
    const int&amp; ref { x }; // 정상: ref는 const 값에 대한 Lvalue 참조입니다.

    return 0;
}
</code></pre>
<p>const Lvalue 참조는 가리키는 대상을 상수로 취급하기 때문에, 값을 읽을(접근할) 수는 있지만 값을 수정할 수는 없습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int x { 5 }; // x는 수정 불가능한 Lvalue입니다.
    const int&amp; ref { x }; // 정상: ref는 const 값에 대한 Lvalue 참조입니다.

    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 정상: const 객체에 접근(읽기)할 수 있습니다.
    ref = 6; // 오류: const 참조를 통해 객체의 값을 수정할 수는 없습니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="수정-가능한-lvalue로-const-lvalue-참조-초기화하기">수정 가능한 Lvalue로 const Lvalue 참조 초기화하기</h3>
<p>const Lvalue 참조는 &#39;수정 가능한 Lvalue&#39;에도 연결될 수 있습니다.
이 경우, 원본 객체는 수정이 가능하더라도 <strong>이 참조(<code>ref</code>)를 통해 접근할 때만큼은 상수로 취급</strong>됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 }; // x는 수정 가능한 Lvalue입니다.
    const int&amp; ref { x }; // 정상: const 참조를 수정 가능한 Lvalue에 바인딩할 수 있습니다.

    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 정상: const 참조를 통해 객체에 접근할 수 있습니다.
    ref = 7; // 오류: const 참조를 통해서는 객체를 수정할 수 없습니다.

    x = 6; // 정상: 원본 x는 수정 가능한 Lvalue이므로, 원래 이름을 사용하면 여전히 값을 바꿀 수 있습니다.

    return 0;
}
</code></pre>
<p>위 프로그램에서 우리는 const 참조인 <code>ref</code>를 수정 가능한 Lvalue인 <code>x</code>에 연결했습니다. 
<code>ref</code>를 통해 <code>x</code>의 값을 읽을 수는 있지만, <code>ref</code> 자체가 const이기 때문에 <code>ref</code>를 사용해 <code>x</code>의 값을 바꿀 수는 없습니다.
하지만 원본 변수인 <code>x</code>를 직접 사용하면 여전히 값을 바꿀 수 있습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
참조하는 대상을 직접 수정해야 하는 상황이 아니라면, 일반 Lvalue 참조보다는 <strong>const Lvalue 참조</strong>를 사용하는 습관을 들이는 것이 좋습니다.</p>
</blockquote>
<hr>
<h3 id="rvalue로-const-lvalue-참조-초기화하기">rvalue로 const Lvalue 참조 초기화하기</h3>
<p>놀랍게도, const Lvalue 참조는 rvalue(숫자 5와 같은 값 자체)에도 연결될 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int&amp; ref { 5 }; // 정상: 5는 rvalue입니다.

    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 5가 출력됩니다.

    return 0;
}
</code></pre>
<p>이런 코드가 실행되면, 내부적으로 rvalue 값을 담은 <strong>임시 객체(temporary object)</strong>가 하나 만들어집니다. 
그리고 우리의 const 참조는 바로 그 임시 객체에 연결되는 것입니다.</p>
<hr>
<h3 id="다른-타입의-값으로-const-lvalue-참조-초기화하기">다른 타입의 값으로 const Lvalue 참조 초기화하기</h3>
<p>const Lvalue 참조는 심지어 타입이 다른 값에도 연결될 수 있습니다. 
단, 그 값이 참조의 타입으로 알아서 변환(암시적 변환)될 수 있어야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    // 상황 1
    const double&amp; r1 { 5 }; // 값 5로 초기화된 임시 double 객체가 만들어지고, r1은 그 임시 객체에 바인딩됩니다.

    std::cout &lt;&lt; r1 &lt;&lt; &#39;\n&#39;; // 5가 출력됩니다.

    // 상황 2
    char c { &#39;a&#39; };
    const int&amp; r2 { c }; // 문자 &#39;a&#39;로 초기화된 임시 int 객체가 만들어지고, r2는 그 임시 객체에 바인딩됩니다.

    std::cout &lt;&lt; r2 &lt;&lt; &#39;\n&#39;; // 97이 출력됩니다 (r2는 int형 참조이기 때문입니다).

    return 0;
}
</code></pre>
<ul>
<li><strong>상황 1:</strong> <code>double</code> 타입의 임시 객체가 생성되고 정수 5로 초기화됩니다. 그런 다음 <code>r1</code>이 이 임시 <code>double</code> 객체에 연결됩니다.</li>
<li><strong>상황 2:</strong> <code>int</code> 타입의 임시 객체가 생성되고 문자 &#39;a&#39;로 초기화됩니다. 그런 다음 <code>r2</code>가 이 임시 <code>int</code> 객체에 연결됩니다.</li>
</ul>
<p>두 경우 모두 참조의 타입과 새롭게 만들어진 임시 객체의 타입이 완벽히 일치하게 됩니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
만약 여러분이 const Lvalue 참조를 다른 타입의 값에 연결하려고 하면, 컴파일러는 참조와 똑같은 타입의 <strong>임시 객체</strong>를 새로 만듭니다. 
그 후 값을 넣어 초기화하고 참조를 그 임시 객체에 연결해버립니다.</p>
</blockquote>
<p>상황 2에서 <code>r2</code>를 출력할 때 문자가 아닌 숫자가 나오는 이유도 이 때문입니다. 
<code>r2</code>는 원본 문자 <code>c</code>를 참조하는 것이 아니라, 새롭게 만들어진 임시 <code>int</code> 객체를 참조하고 있기 때문입니다.</p>
<blockquote>
<p><strong>⚠️ 경고 (Warning)</strong>
우리는 보통 &quot;참조 = 원본 객체&quot;라고 생각합니다. 하지만 참조가 원본이 아닌 &#39;임시 복사본&#39;에 연결되어 버리면 이 공식이 깨집니다. 
이 상태에서는 원본을 수정해도 참조된 값은 바뀌지 않으며, 그 반대도 마찬가지입니다. (서로 완전히 다른 객체이기 때문입니다.)</p>
</blockquote>
<p>이게 왜 문제가 되는지 보여주는 엉뚱한 예시를 하나 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt;
int main()
{
    short bombs { 1 }; // 나 폭탄 가질 수 있어! (참고: 타입은 short입니다)
&gt;
    const int&amp; you { bombs }; // 너도 폭탄 가질 수 있어 (참고: 타입은 int&amp; 입니다)
    --bombs; // 폭탄 다 썼어
&gt;
    if (you) // 너 아직 가지고 있어?
    {
        std::cout &lt;&lt; &quot;폭탄 투하! 안녕, 잔혹한 세상아.\n&quot;; // 폭탄 춤을 추자
    }
&gt;
    return 0;
}
&gt;</code></pre>
<blockquote>
</blockquote>
<p>위 예제에서 <code>bombs</code>는 <code>short</code> 타입이고, <code>you</code>는 <code>const int&amp;</code> 타입입니다. <code>you</code>는 <code>int</code>에만 연결될 수 있기 때문에, 컴파일러는 <code>bombs</code>를 <code>int</code>로 변환하여 값 1을 가진** 새로운 임시 <code>int</code> 객체를 만들어버립니다.** 결국 <code>you</code>는 원본 <code>bombs</code>가 아니라 이 임시 객체에 연결됩니다.</p>
<blockquote>
</blockquote>
<p>나중에 <code>bombs</code>의 개수를 줄여도( <code>--bombs;</code> ), <code>you</code>는 전혀 영향을 받지 않습니다. 가리키는 대상이 다르기 때문입니다. 그
래서 우리는 <code>if (you)</code>가 거짓(false)이 될 거라 예상하지만, 실제로는 참(true)이 되어버립니다.</p>
<hr>
<h3 id="임시-객체에-바인딩된-const-참조는-임시-객체의-수명lifetime을-연장합니다">임시 객체에 바인딩된 const 참조는 임시 객체의 수명(Lifetime)을 연장합니다.</h3>
<p>일반적으로 임시 객체는 자신이 만들어진 코드 줄(표현식)이 끝날 때 메모리에서 사라집니다(파괴됩니다).</p>
<p>만약 <code>const int&amp; ref { 5 };</code> 라는 코드에서 숫자 5를 담고 있는 임시 객체가 이 줄이 끝날 때 바로 사라진다면 어떻게 될까요?
참조 변수인 <code>ref</code>는 이미 파괴된 대상을 가리키는 위험한 상태(이를 <strong>댕글링 참조, dangling reference</strong>라고 합니다)가 되고, 나중에 <code>ref</code>를 사용하려고 하면 프로그램이 비정상적으로 작동할 것입니다.</p>
<p>이를 막기 위해 C++에는 특별한 규칙이 있습니다. <strong>const Lvalue 참조가 임시 객체에 직접 바인딩되면, 그 임시 객체의 수명이 참조 변수의 수명과 똑같아지도록 연장됩니다.</strong></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    const int&amp; ref { 5 }; // 값 5를 가진 임시 객체는 ref와 똑같이 살아남도록 수명이 연장됩니다.

    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 따라서 여기서 안전하게 사용할 수 있습니다.

    return 0;
} // ref와 임시 객체 모두 여기서 수명을 다하고 파괴됩니다.
</code></pre>
<p>이 규칙 덕분에 우리는 임시 객체가 중간에 사라질 걱정 없이 다음 줄에서도 안전하게 값을 출력하고 사용할 수 있습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
일반 Lvalue 참조는 오직 &#39;수정 가능한 Lvalue&#39;에만 연결할 수 있습니다.
하지만 <strong>const Lvalue 참조는 수정 가능한 Lvalue, 수정 불가능한 Lvalue, 그리고 rvalue까지 모두 연결할 수 있습니다.</strong> 
이 때문에 훨씬 더 유연하게 활용할 수 있는 참조 방식입니다.</p>
</blockquote>
<hr>
<h3 id="constexpr-lvalue-참조">Constexpr Lvalue 참조</h3>
<p>참조에 <code>constexpr</code> 키워드를 적용하면, 해당 참조를 상수 표현식(constant expression) 안에서 사용할 수 있게 됩니다.
하지만 이 <code>constexpr</code> 참조에는 큰 제약이 하나 있습니다. 바로 전역 변수(globals)나 정적 지역 변수(static locals)처럼 프로그램 내내 메모리 위치가 고정되어 있는(static duration) 객체에만 연결할 수 있다는 점입니다.</p>
<p>일반 지역 변수는 함수가 호출되기 전까지는 메모리 주소를 알 수 없기 때문에 <code>constexpr</code> 참조를 연결할 수 없습니다.</p>
<pre><code class="language-cpp">int g_x { 5 };

int main()
{
    [[maybe_unused]] constexpr int&amp; ref1 { g_x }; // 정상: 전역 변수에 바인딩할 수 있습니다.

    static int s_x { 6 };
    [[maybe_unused]] constexpr int&amp; ref2 { s_x }; // 정상: 정적(static) 지역 변수에 바인딩할 수 있습니다.

    int x { 6 };
    [[maybe_unused]] constexpr int&amp; ref3 { x }; // 컴파일 오류: 정적이 아닌 일반 객체에는 바인딩할 수 없습니다.

    return 0;
}
</code></pre>
<p>const 변수에 대해 constexpr 참조를 정의할 때는, <code>constexpr</code>(참조 자체에 적용)과 <code>const</code>(참조하는 타입에 적용)를 모두 적어주어야 합니다.</p>
<pre><code class="language-cpp">int main()
{
    static const int s_x { 6 }; // const int 변수
    [[maybe_unused]] constexpr const int&amp; ref2 { s_x }; // constexpr과 const가 모두 필요합니다.

    return 0;
}
</code></pre>
<p>이러한 까다로운 제한 사항들 때문에 실무에서 constexpr 참조가 쓰이는 일은 거의 없습니다.</p>
<hr>
<h2 id="125--lvalue-참조로-전달하기-pass-by-lvalue-reference">12.5 — lvalue 참조로 전달하기 (Pass by lvalue reference)</h2>
<p>이전 강의들에서는 lvalue 참조와 const에 대한 lvalue 참조를 배웠습니다. 사실 이것들만 보면 크게 유용해 보이지 않았을 수 있어요. 
&quot;그냥 변수를 쓰면 되지 왜 굳이 변수의 별명을 만들까?&quot; 하고 생각하셨을 텐데요.</p>
<p>이번 강의에서는 드디어 참조(reference)가 왜 유용한지 명쾌하게 알려드릴게요! 
이 챕터 후반부부터는 참조가 아주 자주 쓰이는 걸 보시게 될 겁니다</p>
<p>먼저 배경지식을 복습해 볼까요? 2.4 강의에서 우리는 <strong>값으로 전달하기(pass by value)</strong>를 배웠습니다. 
함수에 인자를 넘길 때, 그 값이 함수의 매개변수로 &#39;복사&#39;되는 방식이죠.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printValue(int y)
{
    std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;;
} // y는 여기서 소멸됩니다.

int main()
{
    int x { 2 };

    printValue(x); // x는 매개변수 y로 &#39;값으로 전달(복사)&#39;됩니다. (비용이 적게 듦)

    return 0;
}
</code></pre>
<p>위 프로그램에서 <code>printValue(x)</code>를 호출하면, <code>x</code>의 값(2)이 매개변수 <code>y</code>로 <strong>복사</strong>됩니다. 그리고 함수가 끝날 때 객체 <code>y</code>는 파괴(소멸)되죠.</p>
<p>즉, 함수를 부를 때 우리가 넣은 값의 복사본을 만들어 두고서, 잠깐 쓰고 바로 버린다는 뜻입니다! 
다행히 기본 자료형(int 등)은 복사하는 데 비용(시간과 메모리)이 거의 안 들어서 이건 별문제가 안 됩니다.</p>
<hr>
<h3 id="어떤-객체들은-복사-비용이-아주-비쌉니다">어떤 객체들은 복사 비용이 아주 비쌉니다</h3>
<p><code>std::string</code>처럼 표준 라이브러리에서 제공하는 대부분의 타입은 &#39;클래스(class) 타입&#39;입니다. 
클래스 타입은 보통 복사하는 데 비용이 많이 듭니다. </p>
<p>그래서 우리는 가능하면 복사 비용이 비싼 객체를 굳이 복사하는 일은 피하는 게 좋습니다. 
복사본을 만들자마자 거의 바로 파괴할 거라면 더더욱요.</p>
<p>이 점을 잘 보여주는 다음 프로그램을 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

void printValue(std::string y)
{
    std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;;
} // y는 여기서 소멸됩니다.

int main()
{
    std::string x { &quot;Hello, world!&quot; }; // x는 std::string 타입입니다.

    printValue(x); // x는 매개변수 y로 &#39;값으로 전달(복사)&#39;됩니다. (비용이 많이 듦)

    return 0;
}
</code></pre>
<p>이 코드는 <code>Hello, world!</code>를 출력합니다.</p>
<p>이 프로그램은 우리가 원하는 대로 잘 동작하지만, 아주 비효율적입니다. 
이전 예제와 똑같이 <code>printValue()</code>를 부를 때 인자 <code>x</code>가 매개변수 <code>y</code>로 복사됩니다. 
하지만 이번에는 인자가 <code>int</code>가 아니라 <code>std::string</code>이고, <code>std::string</code>은 복사 비용이 비싼 클래스 타입이죠. 
게다가 <code>printValue()</code>를 호출할 때마다 이 비싼 복사 작업이 매번 일어납니다!</p>
<p>우린 이것보다 더 똑똑하게 코드를 짤 수 있어요.</p>
<hr>
<h3 id="참조로-전달하기-pass-by-reference">참조로 전달하기 (Pass by reference)</h3>
<p>함수를 부를 때 이런 비싼 복사 과정을 피하는 한 가지 방법은 &#39;값으로 전달&#39; 대신 <strong>&#39;참조로 전달(pass by reference)&#39;</strong>을 사용하는 것입니다.</p>
<p>참조로 전달할 때는 <strong>함수의 매개변수를 일반 타입이 아니라 참조 타입(또는 const 참조 타입)으로 선언</strong>합니다. 
그러면 함수가 호출될 때 각 참조 매개변수가 알맞은 인자에 &#39;연결(bind)&#39;됩니다. 
참조는 원래 인자의 별명 역할을 하므로, <strong>복사가 전혀 일어나지 않습니다</strong>.</p>
<p>위의 예제를 &#39;값으로 전달&#39; 대신 &#39;참조로 전달&#39; 방식으로 바꿔보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

void printValue(std::string&amp; y) // 타입이 std::string&amp; 로 변경되었습니다.
{
    std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;;
} // y는 여기서 소멸됩니다.

int main()
{
    std::string x { &quot;Hello, world!&quot; };

    printValue(x); // x는 이제 참조 매개변수 y로 &#39;참조로 전달&#39;됩니다. (비용이 적게 듦)

    return 0;
}
</code></pre>
<p>이 프로그램은 앞선 예제와 완벽히 똑같지만, 매개변수 <code>y</code>의 타입이 <code>std::string</code>에서 <code>std::string&amp;</code>(lvalue 참조)로 바뀌었다는 점만 다릅니다.</p>
<p>이제 <code>printValue(x)</code>가 호출되면, lvalue 참조 매개변수 <code>y</code>는 인자 <code>x</code>에 바로 연결됩니다. 참조를 연결하는 건 항상 비용이 아주 저렴하고, <code>x</code>를 복사할 필요도 없죠. 참조는 연결된 객체의 별명 역할을 하므로, <code>printValue()</code> 안에서 참조 <code>y</code>를 쓰면 사실상 복사본이 아닌 진짜 인자 <code>x</code>에 바로 접근하는 셈입니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
참조로 전달하기를 사용하면, 함수를 호출할 때마다 인자를 복사하지 않고도 함수에 데이터를 넘겨줄 수 있습니다.</p>
</blockquote>
<p>아래 프로그램은 &#39;값 매개변수&#39;가 원래 인자와 아예 다른 별개의 객체인 반면, &#39;참조 매개변수&#39;는 원래 인자와 완전히 똑같은 객체로 취급된다는 걸 명확히 보여줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printAddresses(int val, int&amp; ref)
{
    std::cout &lt;&lt; &quot;값 매개변수의 주소는: &quot; &lt;&lt; &amp;val &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;참조 매개변수의 주소는: &quot; &lt;&lt; &amp;ref &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int x { 5 };
    std::cout &lt;&lt; &quot;x의 주소는: &quot; &lt;&lt; &amp;x &lt;&lt; &#39;\n&#39;;
    printAddresses(x, x);

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 대략 다음과 같은 결과가 나옵니다.</p>
<ul>
<li>x의 주소는: <code>0x7ffd16574de0</code></li>
<li>값 매개변수의 주소는: <code>0x7ffd16574de4</code></li>
<li>참조 매개변수의 주소는: <code>0x7ffd16574de0</code></li>
</ul>
<p>보시다시피, 원래 인자(<code>x</code>)와 값 매개변수(<code>val</code>)의 메모리 주소가 다릅니다. 즉, 둘은 서로 다른 별개의 객체라는 뜻이죠. 
두 메모리 공간이 따로 떨어져 있기 때문에, 값 매개변수가 원래 인자와 같은 값을 가지려면 어쩔 수 없이 인자의 값을 복사해 와야만 합니다.</p>
<p>반면에 참조 매개변수(<code>ref</code>)의 주소를 확인해 보면 원래 인자와 주소가 완벽히 똑같습니다. 
이는 참조 매개변수가 사실상 원래 인자와 동일한 객체로 취급되고 있다는 뜻입니다.</p>
<hr>
<h3 id="참조로-전달하면-원본-인자의-값을-바꿀-수-있습니다">참조로 전달하면 원본 인자의 값을 바꿀 수 있습니다</h3>
<p>객체를 &#39;값으로 전달&#39;하면, 함수 매개변수는 인자의 &#39;복사본&#39;을 받게 됩니다. 
즉, 함수 안에서 매개변수의 값을 아무리 지지고 볶아도 원본은 멀쩡하고 복사본만 바뀐다는 뜻입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void addOne(int y) // y는 x의 복사본입니다.
{
    ++y; // 이것은 실제 객체 x가 아니라 복사본을 수정합니다.
}

int main()
{
    int x { 5 };

    std::cout &lt;&lt; &quot;value = &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;

    addOne(x);

    std::cout &lt;&lt; &quot;value = &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;; // x는 수정되지 않았습니다.

    return 0;
}
</code></pre>
<p>위 프로그램에서 값 매개변수 <code>y</code>는 <code>x</code>의 복사본이기 때문에, <code>y</code>에 1을 더해봤자 <code>y</code>만 바뀔 뿐입니다. 
그래서 두 번 모두 <code>value = 5</code>가 출력됩니다.</p>
<p>하지만 참조는 참조하는 대상 객체와 완전히 똑같이 작동하므로, 참조로 전달했을 때는 매개변수에 변화를 주면 <strong>원본 인자도 똑같이 변하게 됩니다</strong>.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void addOne(int&amp; y) // y는 실제 객체 x에 연결됩니다.
{
    ++y; // 이것은 실제 객체 x를 수정합니다.
}

int main()
{
    int x { 5 };

    std::cout &lt;&lt; &quot;value = &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;

    addOne(x);

    std::cout &lt;&lt; &quot;value = &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;; // x가 수정되었습니다.

    return 0;
}
</code></pre>
<p>이 프로그램을 실행하면 결과가 다릅니다.</p>
<ul>
<li>value = 5</li>
<li>value = 6</li>
</ul>
<p>처음에 <code>x</code>의 값은 5입니다. <code>addOne(x)</code>가 호출되면서 참조 매개변수 <code>y</code>가 인자 <code>x</code>에 바로 연결되죠. 
함수 안에서 참조 <code>y</code>를 1 증가시키면, 사실상 복사본이 아니라 진짜 <code>x</code>를 5에서 6으로 증가시키는 셈입니다. 
이렇게 바뀐 값은 <code>addOne()</code> 함수가 끝난 후에도 그대로 유지됩니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
값을 const가 아닌(non-const) 참조로 전달하면, 함수 안에서 넘어온 원본 인자의 값을 직접 수정할 수 있습니다.</p>
</blockquote>
<p>함수가 넘겨받은 원본의 값을 직접 바꿀 수 있다는 건 아주 유용합니다. 예를 들어, 몬스터가 플레이어를 성공적으로 공격했는지 판정하는 함수를 만들었다고 해볼까요? 공격이 성공했다면 플레이어의 체력을 깎아야겠죠. 이때 플레이어 객체를 &#39;참조로 전달&#39;하면, 함수가 진짜 플레이어 객체의 체력을 직접 깎을 수 있습니다. 만약 &#39;값으로 전달&#39;했다면 엉뚱하게 복사본의 체력만 깎일 테니 아무 소용이 없었을 겁니다.</p>
<hr>
<h3 id="참조로-전달하기는-수정-가능한-lvalue-인자만-받을-수-있습니다">참조로 전달하기는 수정 가능한 lvalue 인자만 받을 수 있습니다</h3>
<p>const가 아닌 일반 값에 대한 참조는 오직 수정 가능한 lvalue(쉽게 말해 const가 아닌 일반 변수)에만 연결될 수 있습니다.
그래서 &#39;참조로 전달하기&#39; 역시 수정 가능한 lvalue 인자하고만 쓸 수 있죠.</p>
<p>이 규칙 때문에 const가 아닌 참조 전달은 실무에서 쓰임새가 꽤 제한됩니다. 
const 변수나 5 같은 숫자(리터럴)는 인자로 넘길 수가 없거든요. 예를 들면 이렇습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printValue(int&amp; y) // y는 수정 가능한 lvalue만 받습니다.
{
    std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int x { 5 };
    printValue(x); // 성공: x는 수정 가능한 lvalue입니다.

    const int z { 5 };
    printValue(z); // 오류: z는 수정 불가능한 lvalue입니다.

    printValue(5); // 오류: 5는 rvalue입니다.

    return 0;
}
</code></pre>
<p>다행히 이 문제를 해결할 아주 쉬운 방법이 있는데요, 이건 다음 강의에서 다루겠습니다. 
그때 &#39;값으로 전달하기&#39;와 &#39;참조로 전달하기&#39; 중 어떤 걸 언제 써야 할지도 함께 알아볼게요!</p>
<hr>
<h2 id="126--const-lvalue-참조로-전달하기-pass-by-const-lvalue-reference">12.6 — const lvalue 참조로 전달하기 (Pass by const lvalue reference)</h2>
<p>const가 아닌 일반 참조는 수정 가능한 lvalue에만 연결될 수 있습니다. 반면, <strong>const 참조는 수정 가능한 lvalue, 수정 불가능한 lvalue, 그리고 rvalue(우측값) 모두에 연결될 수 있습니다.</strong> 따라서 참조 매개변수를 const로 만들면, 어떤 종류의 인수(argument)를 넘겨주더라도 모두 받을 수 있게 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printRef(const int&amp; y) // y는 const 참조입니다.
{
    std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int x { 5 };
    printRef(x);   // 성공: x는 수정 가능한 lvalue이며, y는 x에 연결됩니다.

    const int z { 5 };
    printRef(z);   // 성공: z는 수정 불가능한 lvalue이며, y는 z에 연결됩니다.

    printRef(5);   // 성공: 5는 rvalue 리터럴이며, y는 임시 int 객체에 연결됩니다.

    return 0;
}
</code></pre>
<p>const 참조로 값을 전달하면 일반 참조 전달의 가장 큰 장점인 <strong>‘인수의 복사를 방지하는 것’</strong>은 그대로 가져가면서, 동시에 함수 내부에서 <strong>‘참조된 값을 절대 변경할 수 없도록 보장’</strong>해 줍니다.</p>
<p>예를 들어, 아래 코드에서 <code>ref</code>는 const이므로 값을 증가시키는 코드는 허용되지 않습니다.</p>
<pre><code class="language-cpp">void addOne(const int&amp; ref)
{
    ++ref; // 허용되지 않음: ref는 const입니다.
}
</code></pre>
<p>대부분의 경우, 우리는 함수가 원본 인수의 값을 마음대로 바꾸는 것을 원치 않습니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
함수가 인수의 값을 반드시 변경해야 하는 특별한 이유가 없다면, 일반 참조보다는 <strong>const 참조</strong>를 사용하는 것을 항상 우선시하세요.</p>
</blockquote>
<p>이제 왜 C++에서 const lvalue 참조가 rvalue와 연결되도록 허용했는지 그 진짜 이유를 알 수 있습니다. 
만약 이 기능이 없었다면, 참조를 사용하는 함수에 리터럴 숫자(예: <code>5</code>)나 다른 rvalue들을 직접 전달할 방법이 전혀 없었을 테니까요!</p>
<hr>
<h3 id="const-lvalue-참조-매개변수에-다른-타입의-인수-전달하기">const lvalue 참조 매개변수에 다른 타입의 인수 전달하기</h3>
<p>이전 레슨에서 배웠듯, const lvalue 참조는 참조 타입으로 변환할 수만 있다면 아예 다른 타입의 값에도 연결될 수 있습니다. 
이 과정에서 변환된 &#39;임시 객체&#39;가 하나 만들어지고, 참조 매개변수는 그 임시 객체에 연결됩니다.</p>
<p>이 기능을 허용한 주된 이유는, <strong>값으로 전달하든 const 참조로 전달하든 똑같이 편하게 인수를 넘길 수 있게 만들기 위해서</strong>입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printVal(double d)
{
    std::cout &lt;&lt; d &lt;&lt; &#39;\n&#39;;
}

void printRef(const double&amp; d)
{
    std::cout &lt;&lt; d &lt;&lt; &#39;\n&#39;;
}

int main()
{
    printVal(5); // 5가 임시 double로 변환된 후 매개변수 d에 복사됨
    printRef(5); // 5가 임시 double로 변환된 후 매개변수 d에 연결됨

    return 0;
}
</code></pre>
<p>값으로 전달(<code>printVal</code>)할 때는 어차피 복사가 일어날 거라고 예상하기 때문에, 중간에 타입 변환이 한 번 일어나서 추가 복사가 생겨도 큰 문제가 되지 않습니다 (게다가 컴파일러가 알아서 불필요한 복사는 최적화로 없애주곤 합니다).</p>
<p>하지만 참조로 전달(<code>printRef</code>)할 때는 보통 <strong>복사가 일어나는 것을 피하려고</strong> 사용하는 경우가 많습니다. 그런데 타입이 달라서 변환이 발생하면, 결과적으로 비용이 큰 복사 작업이 숨어서 일어날 수 있으므로 성능에 좋지 않을 수 있습니다.</p>
<blockquote>
<p><strong>경고 (Warning)</strong>
참조로 전달할 때는 넘겨주는 인수의 타입과 참조의 타입이 정확히 일치하는지 확인하세요. 
그렇지 않으면 예상치 못한 비싼 변환 작업이 일어날 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="값-전달과-참조-전달-섞어-쓰기">값 전달과 참조 전달 섞어 쓰기</h3>
<p>매개변수가 여러 개인 함수는 각각의 매개변수를 값으로 받을지, 참조로 받을지 따로따로 정할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;

void foo(int a, int&amp; b, const std::string&amp; c)
{
}

int main()
{
    int x { 5 };
    const std::string s { &quot;Hello, world!&quot; };

    foo(5, x, s);

    return 0;
}
</code></pre>
<p>위 예제에서 첫 번째 인수(<code>5</code>)는 값으로, 두 번째 인수(<code>x</code>)는 참조로, 세 번째 인수(<code>s</code>)는 const 참조로 전달되고 있습니다.</p>
<hr>
<h3 id="값-전달과-참조-전달-언제-사용해야-할까">값 전달과 참조 전달, 언제 사용해야 할까?</h3>
<p>대부분의 C++ 초보자에게는 어떤 걸 값으로 넘기고 어떤 걸 참조로 넘길지 결정하는 것이 꽤 헷갈립니다. 
다행히 대부분의 상황에서 통하는 아주 간단한 기준이 있습니다.</p>
<ul>
<li><code>int</code>, <code>double</code> 같은 <strong>기본 타입(Fundamental types)</strong>이나 <strong>열거형(Enum)</strong>은 복사하는 데 드는 비용이 매우 저렴합니다. 따라서 보통 <strong>값으로 전달</strong>합니다.</li>
<li><code>std::string</code> 같은 <strong>클래스 타입(Class types)</strong>은 복사하는 데 비용이 많이 들 수 있습니다. 따라서 보통 <strong>const 참조로 전달</strong>합니다.</li>
</ul>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
기본 규칙으로 <strong>기본 타입은 값으로</strong>, <strong>클래스 타입은 const 참조로</strong> 전달하세요.
무엇을 써야 할지 잘 모르겠다면 일단 const 참조로 전달하는 것이 예상치 못한 문제를 피하는 가장 안전한 길입니다.</p>
</blockquote>
<blockquote>
<p><strong>팁 (Tip)</strong>
알아두면 좋은 몇 가지 예외적인 경우들입니다.
다음은 효율성을 위해 보통 <strong>값으로 전달</strong>합니다.</p>
<ul>
<li>열거형 타입 (enum)</li>
<li>뷰(Views)와 스팬(Spans) (예: <code>std::string_view</code>, <code>std::span</code>)</li>
<li>참조나 포인터 역할을 대신하는 타입 (예: 반복자(iterators), <code>std::reference_wrapper</code>)</li>
<li>복사 비용이 아주 저렴한 단순한 클래스 타입 (예: 기본 타입을 담은 <code>std::pair</code>, <code>std::optional</code>, <code>std::expected</code>)</li>
</ul>
<p>다음은 <strong>참조로 전달</strong>해야 합니다.</p>
<ul>
<li>함수 내부에서 값이 변경되어야 하는 인수</li>
<li>구조상 복사할 수 없는 타입 (예: <code>std::ostream</code>)</li>
<li>복사하면 소유권 문제가 생기는 타입 (예: <code>std::unique_ptr</code>, <code>std::shared_ptr</code>)</li>
<li>가상 함수를 가지거나 상속을 목적으로 하는 타입 (객체 슬라이싱이라는 문제를 피하기 위해)</li>
</ul>
</blockquote>
<hr>
<h3 id="값-전달-vs-참조-전달의-실제-비용-심화-학습">값 전달 vs 참조 전달의 실제 비용 (심화 학습)</h3>
<p>앞서 <code>std::string_view</code>처럼 클래스 타입인데도 값으로 전달하는 예외를 보셨을 겁니다. 
그렇다면 그냥 모든 것을 무조건 참조로 전달하면 안 되는 걸까요? 이 섹션에서는 두 방식의 진짜 비용을 비교해 봅니다.</p>
<p>첫째, 함수 매개변수를 <strong>초기화하는 비용</strong>을 생각해야 합니다. 
값 전달에서 초기화란 곧 복사를 뜻합니다. 복사 비용은 다음 두 가지에 따라 커집니다.</p>
<ol>
<li><strong>객체의 크기:</strong> 메모리를 많이 쓸수록 복사하는 데 오래 걸립니다.</li>
<li><strong>추가 설정 비용:</strong> 어떤 클래스는 만들어질 때 파일을 열거나 동적 메모리를 할당하는 등 추가 작업을 합니다. 
복사할 때마다 이 비용을 지불해야 합니다.</li>
</ol>
<p>반면, 참조를 객체에 연결하는 작업은 항상 기본 타입을 복사하는 것만큼 <strong>매우 빠릅니다.</strong></p>
<p>둘째, 함수 매개변수를 <strong>사용하는 비용</strong>을 생각해야 합니다. 작은 값을 값으로 전달하면, 컴파일러는 이를 느린 RAM 대신 접근 속도가 빠른 CPU 레지스터에 보관하는 최적화를 할 수 있습니다.</p>
<p>값 매개변수를 사용할 때는 복사된 데이터가 있는 곳(레지스터나 RAM)에 한 번만 직접 접근하면 됩니다. 하지만 참조 매개변수를 쓸 때는 보통 단계가 하나 더 추가됩니다. 프로그램이 참조 변수가 있는 곳을 먼저 찾아간 다음, 그 참조가 가리키고 있는 진짜 객체가 있는 RAM 공간을 다시 찾아가야 합니다. 즉, 값 매개변수 사용이 1번의 접근이라면, 참조 매개변수 사용은 2번의 접근이 필요합니다.</p>
<p>셋째, 최적화 측면입니다. 컴파일러는 여러 개의 포인터나 참조가 같은 데이터를 가리킬 수 있는 상황(에일리어싱, Aliasing)에서는 혹시 모를 버그를 막기 위해 아주 보수적으로 코드를 최적화합니다. 하지만 값 전달은 완전히 독립된 복사본을 만들기 때문에 이런 걱정 없이 아주 적극적인 최적화가 가능합니다.</p>
<p>이제 왜 모든 걸 참조로 넘기지 않는지 답이 나왔습니다.</p>
<ul>
<li><strong>복사 비용이 싼 객체</strong>는 차라리 복사를 해서 넘겨주는 것이 메모리 접근 속도도 빠르고 컴파일러 최적화에도 유리합니다.</li>
<li><strong>복사 비용이 비싼 객체</strong>는 복사할 때 드는 엄청난 비용이 다른 모든 성능 이점을 깎아먹기 때문에 무조건 참조로 전달하는 것이 좋습니다.</li>
</ul>
<p>그렇다면 &quot;복사 비용이 싸다&quot;는 기준은 뭘까요? 
대략적으로 <strong>객체의 크기가 메모리 주소 크기의 2배 이하이며, 특별한 추가 설정 비용이 없는 객체</strong>라면 복사 비용이 싸다고 봅니다.</p>
<p>아래 코드는 어떤 타입이 복사 비용이 싼지 판별하는 간단한 매크로 예제입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 타입(또는 객체)의 크기가 메모리 주소 2개 크기 이하이면 true를 반환하는 함수형 매크로
#define isSmall(T) (sizeof(T) &lt;= 2 * sizeof(void*))

struct S
{
    double a;
    double b;
    double c;
};

int main()
{
    std::cout &lt;&lt; std::boolalpha; // 1이나 0 대신 true나 false로 출력
    std::cout &lt;&lt; isSmall(int) &lt;&lt; &#39;\n&#39;; // 참 (true)

    double d {};
    std::cout &lt;&lt; isSmall(d) &lt;&lt; &#39;\n&#39;; // 참 (true)
    std::cout &lt;&lt; isSmall(S) &lt;&lt; &#39;\n&#39;; // 거짓 (false)

    return 0;
}
</code></pre>
<blockquote>
<p><strong>참고 (As an aside)</strong>
일반적인 C++ 함수는 타입 자체를 인수로 받을 수 없기 때문에, 객체나 타입 이름 모두 넣을 수 있도록 전처리기 매크로를 사용했습니다. 다만 어떤 클래스에 &#39;추가 설정 비용&#39;이 있는지는 눈으로 봐서 알기 어렵습니다. 잘 모르겠다면 C++ 표준 라이브러리 클래스들은 일단 추가 설정 비용이 있다고 안전하게 가정하는 것이 좋습니다.</p>
</blockquote>
<hr>
<h3 id="함수-매개변수로는-const-stdstring-보다-stdstring_view를-선호하세요">함수 매개변수로는 const std::string&amp; 보다 std::string_view를 선호하세요</h3>
<p>현대 C++를 배우다 보면 항상 마주치는 질문이 있습니다. 
*&quot;문자열을 받을 때 <code>const std::string&amp;</code>를 써야 하나요, 아니면 <code>std::string_view</code>를 써야 하나요?&quot;*</p>
<p>대부분의 경우 <strong><code>std::string_view</code>가 더 나은 선택</strong>입니다. 훨씬 다양한 형태의 문자열 인수를 효율적으로 처리할 수 있기 때문입니다. 
또한, 긴 문자열 중 일부(부분 문자열)만 잘라서 넘길 때도 새로운 문자열을 복사할 필요가 없어 성능에 아주 좋습니다.</p>
<pre><code class="language-cpp">void doSomething(const std::string&amp;);
void doSomething(std::string_view);   // 대부분의 경우 이 방식을 권장합니다.
</code></pre>
<p>단, 예외적으로 <code>const std::string&amp;</code>를 써야 하는 상황도 있습니다:</p>
<ol>
<li>C++14 이전 버전을 사용 중이라 <code>std::string_view</code>가 없는 경우.</li>
<li>내 함수 안에서 C스타일 문자열이나 <code>std::string</code>을 넘겨받아야 하는 다른 외부 함수를 또 호출해야 하는 경우. (<code>std::string_view</code>는 C스타일 함수가 기대하는 문자열의 끝(null-terminated)을 보장하지 않으며, 다시 <code>std::string</code>으로 변환하려면 꽤 비효율적입니다.)</li>
</ol>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
특별히 다른 구형 함수와 호환을 맞춰야 하는 상황이 아니라면, 문자열은 <code>const std::string&amp;</code> 대신 <strong><code>std::string_view</code>를 값으로 전달</strong>하세요.</p>
</blockquote>
<hr>
<h3 id="왜-stdstring_view가-const-stdstring-보다-빠를까-심화-학습">왜 std::string_view가 const std::string&amp; 보다 빠를까? (심화 학습)</h3>
<p>C++에서 문자열 인수는 보통 다음 세 가지 형태로 넘어옵니다.</p>
<ol>
<li><code>std::string</code></li>
<li><code>std::string_view</code></li>
<li>&quot;Hello&quot; 같은 C스타일 문자열 리터럴</li>
</ol>
<p>컴파일러는 함수 매개변수 타입과 내가 넣은 인수의 타입이 다르면 알아서 <strong>임시 객체</strong>를 만들어 변환(Implicit conversion)을 시도합니다. 이때 <code>std::string_view</code>를 만드는 것은 문자열 원본을 복사하지 않기 때문에 아주 저렴하지만, <code>std::string</code>을 만드는 것은 문자열 전체를 통째로 복사하기 때문에 아주 비싼 작업이 됩니다.</p>
<table>
<thead>
<tr>
<th>인수 타입 (Argument)</th>
<th>std::string_view 매개변수</th>
<th>const std::string&amp; 매개변수</th>
</tr>
</thead>
<tbody><tr>
<td><strong>std::string</strong></td>
<td>비용이 저렴한 변환</td>
<td>비용이 저렴한 참조 연결</td>
</tr>
<tr>
<td><strong>std::string_view</strong></td>
<td>비용이 저렴한 복사</td>
<td><strong>std::string으로의 비싼 명시적 변환</strong></td>
</tr>
<tr>
<td><strong>C스타일 문자열/리터럴</strong></td>
<td>비용이 저렴한 변환</td>
<td><strong>비싼 변환</strong></td>
</tr>
</tbody></table>
<p>위 표에서 보시듯, <strong><code>std::string_view</code> 매개변수는 어떤 타입의 문자열을 넣어도 저렴하게 처리</strong>합니다. 반면 <code>const std::string&amp;</code> 매개변수는 똑같은 <code>std::string</code>을 넘길 때만 빠르고, 다른 형태의 문자열이 들어오면 내부적으로 비싼 문자열 복사가 일어납니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
#include &lt;string_view&gt;

void printSV(std::string_view sv)
{
    std::cout &lt;&lt; sv &lt;&lt; &#39;\n&#39;;
}

void printS(const std::string&amp; s)
{
    std::cout &lt;&lt; s &lt;&lt; &#39;\n&#39;;
}

int main()
{
    std::string s{ &quot;Hello, world&quot; };
    std::string_view sv { s };

    // std::string_view 매개변수에 전달
    printSV(s);              // 성공: std::string에서 std::string_view로의 저렴한 변환
    printSV(sv);             // 성공: std::string_view의 저렴한 복사
    printSV(&quot;Hello, world&quot;); // 성공: C스타일 문자열 리터럴에서 std::string_view로의 저렴한 변환

    // const std::string&amp; 매개변수에 전달
    printS(s);              // 성공: std::string 인수에 저렴하게 연결됨
    printS(sv);             // 컴파일 에러: std::string_view를 std::string으로 암시적 변환할 수 없음
    printS(static_cast&lt;std::string&gt;(sv)); // 나쁨: std::string 임시 객체를 생성하는 비싼 작업
    printS(&quot;Hello, world&quot;); // 나쁨: std::string 임시 객체를 생성하는 비싼 작업

    return 0;
}
</code></pre>
<p>또한 함수 내부에서 사용할 때도 차이가 납니다. <code>std::string_view</code>는 일반 객체라 문자열에 곧바로 접근할 수 있지만, <code>std::string&amp;</code>는 참조형이라 실제 데이터에 접근하기 위해 메모리를 한 다리 더 건너가야 합니다.</p>
<p>마지막으로 원본 문자열의 일부(부분 문자열)만 떼어서 넘기고 싶을 때, <code>std::string_view</code>를 쓰면 복사 없이 아주 빠르고 저렴하게 처리할 수 있어 성능 최적화에 유리합니다.</p>
<hr>
<h2 id="127--포인터-소개-introduction-to-pointers">12.7 — 포인터 소개 (Introduction to pointers)</h2>
<p>포인터(Pointers)는 C++를 배울 때 많은 초보자들이 두려워하고 막히는 &#39;전통적인 골칫거리&#39; 중 하나입니다. 
하지만 곧 알게 되겠지만, 포인터는 전혀 무서워할 필요가 없습니다.</p>
<p>사실 포인터는 <strong>lvalue 레퍼런스(참조)</strong>와 매우 비슷하게 동작합니다. 
그 부분을 자세히 설명하기 전에, 먼저 기초 개념부터 다져보겠습니다.</p>
<p>다음과 같은 아주 평범한 변수를 생각해 봅시다.</p>
<pre><code class="language-cpp">char x {}; // char 타입은 1바이트의 메모리를 사용합니다.
</code></pre>
<p>조금 단순하게 설명하자면, 이 코드가 실행될 때 컴퓨터는 RAM이라는 메모리 공간의 일부를 이 변수(객체)에 할당합니다. 이해를 돕기 위해 변수 <code>x</code>에 <strong>140번지 메모리 주소</strong>가 할당되었다고 가정해 보겠습니다. 앞으로 프로그램에서 <code>x</code>라는 변수를 사용할 때마다, 프로그램은 알아서 140번지 메모리로 찾아가 그곳에 저장된 값을 읽어옵니다.</p>
<p>변수가 참 편리한 이유는, 우리가 특정 메모리 주소가 몇 번지인지, 객체의 값을 저장하는 데 몇 바이트가 필요한지 신경 쓸 필요가 없다는 것입니다. 그저 우리가 지어준 이름(식별자)으로 변수를 부르기만 하면, 컴파일러가 그 이름을 적절한 메모리 주소로 번역해 줍니다. 
<strong>컴파일러가 복잡한 주소 처리를 다 알아서 해주는 셈이죠.</strong></p>
<p>이러한 특징은 레퍼런스에도 똑같이 적용됩니다.</p>
<pre><code class="language-cpp">int main(){
    char x {}; // 이 변수에 140번지 메모리 주소가 할당되었다고 가정합시다.
    char&amp; ref { x }; // ref는 x에 대한 lvalue 레퍼런스입니다. (타입과 함께 쓰일 때 &amp;는 lvalue 레퍼런스를 의미합니다)

    return 0;
}
</code></pre>
<p><code>ref</code>는 <code>x</code>의 별명처럼 작동하기 때문에, 우리가 <code>ref</code>를 사용할 때마다 프로그램은 동일하게 140번지 메모리로 가서 값을 읽어옵니다. 
여기서도 역시 주소를 찾아가는 일은 컴파일러가 알아서 처리합니다.</p>
<hr>
<h3 id="주소-연산자-the-address-of-operator-">주소 연산자 (The address-of operator, <code>&amp;</code>)</h3>
<p>기본적으로 변수가 사용하는 메모리 주소는 우리 눈에 보이지 않게 숨겨져 있지만, 원한다면 이 정보에 접근할 수 있습니다. 
<strong>주소 연산자(<code>&amp;</code>)</strong>는 해당 변수의 실제 메모리 주소를 반환합니다. 사용법은 아주 직관적입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    int x{ 5 };
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // 변수 x의 값을 출력합니다.
    std::cout &lt;&lt; &amp;x &lt;&lt; &#39;\n&#39;; // 변수 x의 메모리 주소를 출력합니다.

    return 0;
}
</code></pre>
<p>저자의 컴퓨터에서 위 프로그램을 실행했을 때 다음과 같이 출력되었습니다.</p>
<pre><code class="language-text">5
0027FEA0
</code></pre>
<p>위 예제에서 우리는 주소 연산자(<code>&amp;</code>)를 사용해 변수 <code>x</code>에 할당된 주소를 가져오고, 그 주소를 화면에 출력했습니다. 
메모리 주소는 보통 (0x 접두사 없이) 16진수 값으로 출력됩니다.</p>
<p>만약 메모리를 1바이트 이상 사용하는 객체라면, 주소 연산자는 그 객체가 사용하는 <strong>첫 번째 바이트의 메모리 주소</strong>를 반환합니다.</p>
<blockquote>
<p><strong>팁</strong>
<code>&amp;</code> 기호는 상황에 따라 의미가 달라지기 때문에 헷갈리기 쉽습니다.</p>
<ul>
<li>타입 이름 바로 뒤에 올 때 (<code>int&amp; ref</code>): <strong>lvalue 레퍼런스</strong></li>
<li>식에서 변수 앞에 단독으로 쓰일 때 (<code>&amp;x</code>): <strong>주소 연산자</strong> (주소를 가져옴)</li>
<li>두 값 사이에 쓰일 때 (<code>x &amp; y</code>): <strong>비트 AND 연산자</strong></li>
</ul>
</blockquote>
<hr>
<h3 id="역참조-연산자-the-dereference-operator-">역참조 연산자 (The dereference operator, <code>*</code>)</h3>
<p>단순히 변수의 주소만 알아내는 것은 그 자체로 큰 쓸모가 없습니다.
주소를 가지고 할 수 있는 가장 유용한 일은, <strong>그 주소에 실제로 들어있는 값에 접근</strong>하는 것입니다. 
<strong>역참조 연산자(<code>*</code>)</strong>(또는 간접 참조 연산자라고도 부름)는 주어진 메모리 주소에 있는 값을 lvalue로 반환해 줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    int x{ 5 };
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // 변수 x의 값을 출력합니다.
    std::cout &lt;&lt; &amp;x &lt;&lt; &#39;\n&#39;; // 변수 x의 메모리 주소를 출력합니다.

    std::cout &lt;&lt; *(&amp;x) &lt;&lt; &#39;\n&#39;; // 변수 x의 메모리 주소에 있는 값을 출력합니다. (괄호는 필수가 아니지만 코드를 읽기 쉽게 해줍니다)

    return 0;
}
</code></pre>
<p>저자의 컴퓨터에서는 다음과 같이 출력되었습니다.</p>
<pre><code class="language-text">5
0027FEA0
5
</code></pre>
<p>프로그램의 흐름은 아주 간단합니다. 먼저 변수 <code>x</code>를 선언하고 값을 출력합니다. 그다음 <code>x</code>의 메모리 주소를 출력합니다.
마지막으로, 역참조 연산자를 사용해 <code>x</code>의 메모리 주소로 찾아가 그 안에 있는 값(결국 <code>x</code>의 값인 5)을 가져와 화면에 출력합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
메모리 주소가 있다면, 역참조 연산자(<code>*</code>)를 사용해 그 주소에 있는 값을 가져올 수 있습니다.
주소 연산자(<code>&amp;</code>)와 역참조 연산자(<code>*</code>)는 완전히 <strong>정반대</strong>로 작동합니다. <code>&amp;</code>는 객체에서 주소를 얻어내고, <code>*</code>는 주소에서 객체를 얻어냅니다.</p>
</blockquote>
<blockquote>
<p><strong>팁</strong>
역참조 연산자(<code>*</code>)는 곱하기 연산자와 똑같이 생겼지만, 구분하는 법은 쉽습니다. 역참조 연산자는 변수 하나에만 붙고(단항), 곱하기 연산자는 두 개의 값 사이에 들어갑니다(이항).</p>
</blockquote>
<p>사실 변수의 메모리 주소를 가져온 다음, 곧바로 다시 역참조해서 값을 가져오는 행위 자체는 크게 유용하지 않습니다. 그냥 변수 이름을 쓰면 되니까요! 하지만 이제 주소 연산자(<code>&amp;</code>)와 역참조 연산자(<code>*</code>)라는 강력한 도구를 얻었으니, 본격적으로 <strong>포인터</strong>에 대해 이야기할 준비가 되었습니다.</p>
<hr>
<h3 id="포인터-pointers">포인터 (Pointers)</h3>
<p><strong>포인터</strong>는 일반적인 값 대신 <strong>메모리 주소</strong>(보통 다른 변수의 주소)를 값으로 저장하는 객체(변수)입니다. 
포인터를 사용하면 다른 객체의 주소를 저장해 두었다가 나중에 꺼내 쓸 수 있습니다.</p>
<blockquote>
<p><strong>참고</strong>
현대의 C++에서는 최근 도입된 &#39;스마트 포인터(smart pointers)&#39;와 구분하기 위해, 지금 우리가 배우는 이 기본적인 포인터를 &quot;원시 포인터(raw pointers)&quot; 또는 &quot;멍청한 포인터(dumb pointers)&quot;라고 부르기도 합니다.</p>
</blockquote>
<p>포인터를 지정하는 타입(예: <code>int*</code>)을 <strong>포인터 타입</strong>이라고 합니다. 
레퍼런스 타입을 앰퍼샌드(<code>&amp;</code>)로 선언하듯, 포인터 타입은 별표(<code>*</code>)를 사용해 선언합니다.</p>
<pre><code class="language-cpp">int; // 평범한 int 타입
int&amp;; // int 값에 대한 lvalue 레퍼런스
int*; // int 값에 대한 포인터 (정수 값의 주소를 보관함)
</code></pre>
<p>포인터 변수를 만드는 방법은 아주 간단합니다. 포인터 타입으로 변수를 정의하기만 하면 됩니다.</p>
<pre><code class="language-cpp">int main(){
    int x { 5 }; // 일반적인 변수
    int&amp; ref { x }; // 정수에 대한 레퍼런스 (x에 연결됨)

    int* ptr; // 정수에 대한 포인터

    return 0;
}
</code></pre>
<p>여기서 쓰인 별표(<code>*</code>)는 포인터를 선언하기 위한 문법의 일부일 뿐, 앞서 배운 역참조 연산자가 아니라는 점에 주의하세요.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
포인터 타입을 선언할 때는 별표(<code>*</code>)를 타입 이름 바로 옆에 붙이는 것이 좋습니다. (<code>int *ptr</code> 보다는 <code>int* ptr</code>)</p>
</blockquote>
<blockquote>
<p><strong>경고</strong>
한 줄에 여러 개의 변수를 선언하는 것은 권장하지 않지만, 만약 그렇게 한다면 각 변수마다 별표를 붙여줘야 합니다.</p>
<pre><code class="language-cpp">int* ptr1, ptr2; // 잘못됨: ptr1은 int에 대한 포인터지만, ptr2는 그냥 평범한 int 변수가 됩니다!
int* ptr3, * ptr4; // 올바름: ptr3과 ptr4 둘 다 int에 대한 포인터입니다.
</code></pre>
</blockquote>
<hr>
<h3 id="포인터-초기화-pointer-initialization">포인터 초기화 (Pointer initialization)</h3>
<p>일반 변수와 마찬가지로, 포인터도 <strong>기본적으로는 초기화되지 않습니다.</strong> 초기화되지 않은 포인터는 <strong>와일드 포인터(wild pointer)</strong>라고 부릅니다. 와일드 포인터는 아무 의미 없는 쓰레기 주소값을 가지고 있기 때문에, 이를 역참조하려고 하면 프로그램이 오작동하는 등 &#39;정의되지 않은 동작(undefined behavior)&#39;이 발생합니다. 따라서 포인터는 <strong>항상 안전한 값으로 초기화</strong>해야 합니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
포인터는 항상 초기화하세요.</p>
</blockquote>
<pre><code class="language-cpp">int main(){
    int x{ 5 };
&gt;
    int* ptr; // 초기화되지 않은 포인터 (쓰레기 주소를 가짐)
    int* ptr2{}; // 널 포인터 (다음 레슨에서 다룰 예정입니다)
    int* ptr3{ &amp;x }; // 변수 x의 주소로 초기화된 포인터
&gt;
    return 0;
}
&gt;</code></pre>
<p>포인터는 주소를 보관하기 때문에, 포인터를 초기화하거나 값을 넣을 때는 반드시 <strong>주소값</strong>을 넣어주어야 합니다. 
보통 주소 연산자(<code>&amp;</code>)를 사용해 다른 변수의 주소를 가져와서 포인터에 저장합니다.</p>
<p>어떤 객체의 주소를 포인터에 무사히 저장했다면, 이제 역참조 연산자(<code>*</code>)를 사용해 그 주소에 있는 값에 자유롭게 접근할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    int x{ 5 };
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // 변수 x의 값을 출력합니다.

    int* ptr{ &amp;x }; // ptr은 x의 주소를 보관합니다.
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 역참조 연산자를 사용하여 ptr이 보관하고 있는 주소(즉, x의 주소)에 있는 값을 출력합니다.

    return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">5
5
</code></pre>
<p>이 상황을 개념적으로 이해하자면, <code>ptr</code>이 <code>x</code>의 주소를 가지고 있으므로 우리는 &quot;<strong>ptr이 x를 가리킨다(pointing to)</strong>&quot;라고 표현합니다. 
&#39;포인터&#39;라는 이름도 바로 여기서 유래한 것입니다!</p>
<blockquote>
<p><strong>저자의 메모</strong>
포인터를 부르는 용어에 대해 짚고 넘어갈게요. &quot;X 포인터&quot;(X는 특정 타입)라는 말은 &quot;X에 대한 포인터&quot;를 짧게 부르는 것입니다. 즉 &quot;정수 포인터&quot;라고 하면 실제로는 &quot;정수(int)에 대한 포인터&quot;를 의미합니다.</p>
</blockquote>
<p>레퍼런스의 타입이 연결할 대상의 타입과 일치해야 하듯, <strong>포인터의 타입도 가리킬 대상의 타입과 반드시 일치해야 합니다.</strong></p>
<pre><code class="language-cpp">int main(){
    int i{ 5 };
    double d{ 7.0 };

    int* iPtr{ &amp;i }; // 정상: int에 대한 포인터는 int 객체를 가리킬 수 있습니다.
    int* iPtr2 { &amp;d }; // 오류: int에 대한 포인터는 double 객체를 가리킬 수 없습니다.
    double* dPtr{ &amp;d }; // 정상: double에 대한 포인터는 double 객체를 가리킬 수 있습니다.
    double* dPtr2{ &amp;i }; // 오류: double에 대한 포인터는 int 객체를 가리킬 수 없습니다.

    return 0;
}
</code></pre>
<p>또한, 포인터에 숫자(리터럴 값)를 직접 넣어 초기화하는 것은 허용되지 않습니다. (다음 레슨에서 배울 한 가지 예외는 제외하고요)</p>
<pre><code class="language-cpp">int* ptr{ 5 }; // 오류
int* ptr{ 0x0012FF7C }; // 오류: 0x0012FF7C는 그냥 정수 숫자로 취급됩니다.
</code></pre>
<hr>
<h3 id="포인터와-대입-pointers-and-assignment">포인터와 대입 (Pointers and assignment)</h3>
<p>포인터에서 대입 연산자(<code>=</code>)를 사용하는 방법은 두 가지가 있습니다.</p>
<ol>
<li><strong>포인터가 가리키는 대상을 바꾸기</strong> (포인터 자체에 새로운 주소를 넣기)</li>
<li><strong>포인터가 가리키는 대상의 &#39;값&#39;을 바꾸기</strong> (포인터를 역참조한 뒤 새로운 값을 넣기)</li>
</ol>
<p>먼저, 포인터가 다른 객체를 가리키도록 바꾸는 경우를 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    int x{ 5 };
    int* ptr{ &amp;x }; // ptr이 x를 가리키도록 초기화합니다.

    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 가리키고 있는 주소(x의 주소)에 있는 값을 출력합니다.

    int y{ 6 };
    ptr = &amp;y; // ptr이 y를 가리키도록 변경합니다.

    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 가리키고 있는 주소(y의 주소)에 있는 값을 출력합니다.

    return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">5
6
</code></pre>
<p>위 예제에서는 먼저 <code>ptr</code>을 <code>x</code>의 주소로 초기화한 후 역참조하여 5를 출력했습니다. 
그런 다음 <code>ptr = &amp;y;</code>를 통해 <code>ptr</code>이 보관하는 주소를 <code>y</code>의 주소로 바꿨습니다. 
다시 역참조해보면 이제 <code>y</code>의 값인 6이 출력됩니다.</p>
<p>이번에는 포인터를 이용해 <strong>가리키는 대상의 값을 직접 바꾸는 방법</strong>을 확인해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    int x{ 5 };
    int* ptr{ &amp;x }; // ptr을 변수 x의 주소로 초기화합니다.

    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // x의 값을 출력합니다.
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // ptr이 보관하고 있는 주소(x의 주소)에 있는 값을 출력합니다.

    *ptr = 6; // ptr이 보관하고 있는 주소에 있는 객체(x)에 값 6을 대입합니다. (여기서 ptr이 역참조되었음에 주목하세요)

    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // ptr이 보관하고 있는 주소(x의 주소)에 있는 값을 출력합니다.

    return 0;
}
</code></pre>
<p>결과는 다음과 같습니다.</p>
<pre><code class="language-text">5
5
6
6
</code></pre>
<p><code>*ptr</code>은 lvalue를 반환하기 때문에, 대입 연산자 왼쪽(<code>=</code>)에 두고 새로운 값을 넣을 수 있습니다. 
위 코드에서 <code>*ptr = 6;</code>을 실행하면, 실제로 <code>ptr</code>이 가리키고 있던 변수 <code>x</code>의 값이 6으로 업데이트됩니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong></p>
<ul>
<li>역참조 없이 <code>ptr</code>만 쓰면, 포인터가 가지고 있는 <strong>메모리 주소</strong> 자체에 접근합니다. 
(<code>ptr = &amp;y;</code>처럼 쓰면 포인터가 가리키는 &#39;방향&#39;을 바꿉니다)</li>
<li>역참조 기호를 붙여 <code>*ptr</code>로 쓰면, 포인터가 가리키고 있는 <strong>진짜 객체</strong>에 접근합니다. 
(<code>*ptr = 6;</code>처럼 쓰면 가리키고 있는 변수의 &#39;내용물&#39;을 바꿉니다)</li>
</ul>
</blockquote>
<hr>
<h3 id="포인터는-lvalue-레퍼런스와-매우-비슷하게-동작합니다">포인터는 lvalue 레퍼런스와 매우 비슷하게 동작합니다</h3>
<p>포인터와 lvalue 레퍼런스의 역할은 꽤 비슷합니다. 다음 프로그램을 확인해 보세요.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    int x{ 5 };
    int&amp; ref { x }; // x에 대한 레퍼런스를 가져옵니다.
    int* ptr { &amp;x }; // x에 대한 포인터를 가져옵니다.

    std::cout &lt;&lt; x;
    std::cout &lt;&lt; ref; // 레퍼런스를 사용하여 x의 값을 출력합니다 (5)
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 포인터를 사용하여 x의 값을 출력합니다 (5)

    ref = 6; // 레퍼런스를 사용하여 x의 값을 변경합니다.
    std::cout &lt;&lt; x;
    std::cout &lt;&lt; ref; // 레퍼런스를 사용하여 x의 값을 출력합니다 (6)
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 포인터를 사용하여 x의 값을 출력합니다 (6)

    *ptr = 7; // 포인터를 사용하여 x의 값을 변경합니다.
    std::cout &lt;&lt; x;
    std::cout &lt;&lt; ref; // 레퍼런스를 사용하여 x의 값을 출력합니다 (7)
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 포인터를 사용하여 x의 값을 출력합니다 (7)

    return 0;
}
</code></pre>
<p>이 프로그램의 결과는 다음과 같습니다.</p>
<pre><code class="language-text">555
666
777
</code></pre>
<p>결국 포인터와 레퍼런스 모두 <strong>다른 객체에 간접적으로 접근할 수 있는 방법</strong>을 제공합니다.
가장 큰 차이점은 <strong>포인터는 우리가 직접 <code>&amp;</code>로 주소를 가져오고 <code>*</code>로 역참조를 해줘야 하지만, 레퍼런스는 이런 과정이 뒷단에서 자동으로 처리된다는 것</strong>입니다.</p>
<p>포인터와 레퍼런스의 다른 차이점들은 다음과 같습니다.</p>
<ul>
<li>레퍼런스는 반드시 초기화해야 하지만, 포인터는 초기화가 필수는 아닙니다(하지만 꼭 해야 안전합니다).</li>
<li>레퍼런스는 객체가 아니지만, 포인터는 엄연한 독립적인 객체입니다.</li>
<li>레퍼런스는 한 번 대상을 정하면 다른 대상으로 바꿀 수 없지만, 포인터는 언제든 다른 대상을 가리키게 바꿀 수 있습니다.</li>
<li>레퍼런스는 항상 무언가 유효한 객체와 연결되어 있어야 하지만, 포인터는 아무것도 가리키지 않는 빈 상태(null)가 될 수 있습니다.</li>
<li>레퍼런스는 대체로 &#39;안전&#39;하지만, 포인터는 본질적으로 &#39;위험성&#39;을 내포하고 있습니다.</li>
</ul>
<hr>
<h3 id="주소-연산자는-포인터를-반환합니다">주소 연산자는 포인터를 반환합니다</h3>
<p>주소 연산자(<code>&amp;</code>)는 메모리 주소를 단순한 숫자로 반환하지 않고, 피연산자를 가리키는 <strong>포인터 타입을 반환</strong>한다는 점을 기억해 두면 좋습니다. (C++는 주소를 직접적인 리터럴 값으로 지원하지 않기 때문입니다.)
즉 <code>int x</code>라는 변수가 있을 때, <code>&amp;x</code>는 <code>x</code>의 주소를 담고 있는 <code>int*</code> (정수형 포인터)를 반환합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;typeinfo&gt;

int main(){
    int x{ 4 };
    std::cout &lt;&lt; typeid(x).name() &lt;&lt; &#39;\n&#39;; // x의 타입을 출력합니다.
    std::cout &lt;&lt; typeid(&amp;x).name() &lt;&lt; &#39;\n&#39;; // &amp;x의 타입을 출력합니다.

    return 0;
}
</code></pre>
<p>Visual Studio에서는 다음과 같이 출력됩니다.</p>
<pre><code class="language-text">int
int *
</code></pre>
<p>(컴파일러마다 표기가 조금 다를 수 있지만 의미는 같습니다.)</p>
<hr>
<h3 id="포인터의-크기-the-size-of-pointers">포인터의 크기 (The size of pointers)</h3>
<p>포인터가 차지하는 메모리 크기는 프로그램을 실행하는 컴퓨터의 시스템 환경(아키텍처)에 따라 달라집니다.
32비트 환경에서는 메모리 주소도 32비트이므로, 포인터의 크기 역시 <strong>32비트(4바이트)</strong>가 됩니다. 64비트 환경에서는 <strong>64비트(8바이트)</strong>가 됩니다.</p>
<p>중요한 점은, <strong>포인터가 가리키는 대상이 얼마나 큰지와 상관없이 포인터 자체의 크기는 항상 일정하다는 것</strong>입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main() // 32비트 애플리케이션이라고 가정합시다.
{
    char* chPtr{}; // char는 1바이트입니다.
    int* iPtr{}; // int는 보통 4바이트입니다.
    long double* ldPtr{}; // long double은 보통 8바이트 또는 12바이트입니다.

    std::cout &lt;&lt; sizeof(chPtr) &lt;&lt; &#39;\n&#39;; // 4를 출력합니다.
    std::cout &lt;&lt; sizeof(iPtr) &lt;&lt; &#39;\n&#39;; // 4를 출력합니다.
    std::cout &lt;&lt; sizeof(ldPtr) &lt;&lt; &#39;\n&#39;; // 4를 출력합니다.

    return 0;
}
</code></pre>
<p>포인터는 결국 &#39;메모리 주소&#39;일 뿐이고, 주소를 저장하는 데 필요한 공간의 크기는 어떤 타입이든 동일하기 때문입니다.</p>
<hr>
<h3 id="댕글링-포인터-dangling-pointers">댕글링 포인터 (Dangling pointers)</h3>
<p>&#39;댕글링(Dangling, 공중에 붕 뜬)&#39; 레퍼런스와 마찬가지로, <strong>댕글링 포인터</strong>는 <strong>더 이상 유효하지 않은(예: 이미 파괴되어 사라진) 객체의 주소를 들고 있는 포인터</strong>를 뜻합니다.</p>
<p>유효하지 않은 객체에 접근하려는 것이기 때문에, 댕글링 포인터를 역참조하면 프로그램이 멈추거나 오작동하는 등 정의되지 않은 동작이 발생합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
유효하지 않은 포인터를 역참조하면 정의되지 않은 심각한 동작이 발생합니다. 
그 주소를 다른 데 복사하는 등 단순한 사용조차도 컴파일러에 따라 결과가 다르게 나타날 수 있으니 매우 주의해야 합니다.</p>
</blockquote>
<p>댕글링 포인터가 만들어지는 대표적인 예시는 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    int x{ 5 };
    int* ptr{ &amp;x };

    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 정상 작동합니다.

    {
        int y{ 6 };
        ptr = &amp;y;

        std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 정상 작동합니다.
    } // 블록이 끝나면서 y가 범위를 벗어나 파괴됩니다. 이제 ptr은 댕글링 포인터가 됩니다.

    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 댕글링 포인터를 역참조하여 정의되지 않은 동작이 발생합니다.

    return 0;
}
</code></pre>
<p>위 프로그램은 아마도 이렇게 출력될 것입니다.</p>
<pre><code class="language-text">5
6
6
</code></pre>
<p>하지만 마지막 6이 제대로 출력되지 않을 수도 있습니다. 왜냐하면 안쪽 중괄호 블록이 끝날 때 변수 <code>y</code>가 메모리에서 파괴되어 사라졌고, <code>ptr</code>은 사라진 <code>y</code>의 자리를 여전히 가리키고 있는 &#39;댕글링 포인터&#39;가 되어버렸기 때문입니다.</p>
<h3 id="결론">결론</h3>
<p>포인터는 메모리 주소를 저장하는 변수입니다. 역참조 연산자(<code>*</code>)를 사용하면 포인터가 가리키고 있는 주소로 찾아가 값을 가져오거나 바꿀 수 있습니다. 초기화되지 않았거나(와일드), 대상이 사라졌거나(댕글링), 비어있는(널) 포인터를 역참조하면 심각한 오류가 발생해 프로그램이 강제 종료될 가능성이 높습니다.</p>
<p>포인터는 레퍼런스보다 훨씬 유연하게 활용할 수 있지만, 그만큼 위험성도 큽니다. 앞으로의 레슨에서 이를 더 깊이 파헤쳐 보겠습니다.</p>
<hr>
<h2 id="128--널-포인터-null-pointers">12.8 — 널 포인터 (Null pointers)</h2>
<p>지난 레슨(12.7 -- 포인터 소개)에서는 다른 객체의 주소를 저장하는 &#39;포인터(pointer)&#39;의 기본을 다뤘습니다. 
포인터가 가지고 있는 주소는 역참조 연산자(<code>*</code>)를 사용해 그 주소에 있는 실제 객체를 가져오는 데 쓸 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x{ 5 };
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;; // 변수 x의 값을 출력합니다

    int* ptr{ &amp;x }; // ptr은 x의 주소를 저장합니다
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 역참조 연산자를 사용하여 ptr이 저장하고 있는 주소(즉, x의 주소)에 있는 객체의 값을 출력합니다

    return 0;
}
</code></pre>
<p>위 예제를 실행하면 다음과 같이 출력됩니다.</p>
<pre><code>5
5
</code></pre><p>이전 레슨에서 포인터가 반드시 무언가를 가리켜야만 하는 것은 아니라고 언급한 적이 있습니다. 
이번 레슨에서는 이렇게 &#39;아무것도 가리키지 않는 포인터&#39;와 그로 인해 발생하는 여러 가지 상황들을 자세히 알아보겠습니다.</p>
<hr>
<h3 id="널-포인터-null-pointers">널 포인터 (Null pointers)</h3>
<p>포인터는 메모리 주소 외에도 <strong>&#39;널(null) 값&#39;</strong>이라는 것을 가질 수 있습니다. 
널 값(보통 줄여서 &#39;널&#39;이라고 부름)은 &#39;아무런 값도 없음&#39;을 뜻하는 특별한 값입니다. 
포인터가 널 값을 가지고 있다는 것은, <strong>그 포인터가 현재 아무것도 가리키고 있지 않다는 뜻</strong>입니다. 
이런 포인터를 <strong>널 포인터(null pointer)</strong>라고 부릅니다.</p>
<p>널 포인터를 만드는 가장 쉬운 방법은 값 초기화(value initialization)를 사용하는 것입니다.</p>
<pre><code class="language-cpp">int main()
{
    int* ptr {}; // ptr은 이제 널 포인터이며, 어떤 주소도 가지고 있지 않습니다

    return 0;
}
</code></pre>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
유효한 객체의 주소로 포인터를 초기화하지 않을 거라면, 우선 값 초기화를 통해 널 포인터로 만들어 두는 것이 좋습니다.</p>
</blockquote>
<p>포인터는 나중에 대입(assignment)을 통해 가리키는 대상을 언제든 바꿀 수 있습니다. 
따라서 처음에는 널(null)로 설정된 포인터라도 나중에 진짜 유효한 객체를 가리키도록 변경할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int* ptr {}; // ptr은 널 포인터이며, 아직 어떤 주소도 가지고 있지 않습니다

    int x { 5 };
    ptr = &amp;x; // ptr은 이제 객체 x를 가리킵니다 (더 이상 널 포인터가 아닙니다)

    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 역참조된 ptr을 통해 x의 값을 출력합니다

    return 0;
}
</code></pre>
<hr>
<h3 id="nullptr-키워드">nullptr 키워드</h3>
<p><code>true</code>나 <code>false</code>라는 키워드가 참/거짓을 나타내는 것처럼, <code>nullptr</code>이라는 키워드는 <strong>널 포인터 리터럴(값 자체)</strong>을 나타냅니다. <code>nullptr</code>을 사용하면 포인터를 명시적으로 널 포인터로 초기화하거나 널 값을 대입할 수 있습니다.</p>
<pre><code class="language-cpp">int main()
{
    int* ptr { nullptr }; // nullptr을 사용하여 포인터를 널 포인터로 초기화할 수 있습니다

    int value { 5 };
    int* ptr2 { &amp;value }; // ptr2는 유효한 포인터입니다
    ptr2 = nullptr; // nullptr을 대입하여 포인터를 다시 널 포인터로 만들 수 있습니다

    someFunction(nullptr); // 포인터를 매개변수로 받는 함수에 곧바로 nullptr을 전달할 수도 있습니다

    return 0;
}
</code></pre>
<p>위의 예제처럼 대입 연산자를 통해 <code>ptr2</code>의 값을 <code>nullptr</code>로 설정하면 <code>ptr2</code>는 널 포인터가 됩니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
포인터를 초기화하거나, 대입하거나, 함수에 널 포인터를 전달할 때 항상 <code>nullptr</code> 키워드를 사용하세요.</p>
</blockquote>
<hr>
<h3 id="널-포인터를-역참조하면-정의되지-않은-동작이-발생합니다">널 포인터를 역참조하면 정의되지 않은 동작이 발생합니다</h3>
<p>허공을 가리키는 댕글링 포인터(dangling pointer)를 역참조하면 알 수 없는 문제가 생기는 것처럼, 널 포인터를 역참조해도 <strong>정의되지 않은 동작(undefined behavior)</strong>이 발생합니다. 대부분의 경우 프로그램이 그 즉시 비정상적으로 멈추거나 꺼져버립니다.</p>
<p>다음 프로그램은 이를 잘 보여줍니다. 
코드를 실행해 보면 아마 프로그램이 비정상 종료될 것입니다. (직접 실행해 보셔도 컴퓨터가 고장 나지는 않으니 안심하세요!)</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int* ptr {}; // 널 포인터를 생성합니다
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 널 포인터를 역참조합니다 (위험!)

    return 0;
}
</code></pre>
<p>원리를 생각해 보면 당연한 일입니다. 포인터를 역참조한다는 것은 &quot;이 포인터가 가리키는 주소로 찾아가서 그곳에 있는 값을 가져와라&quot;라는 뜻입니다. 그런데 널 포인터는 &#39;아무것도 가리키지 않음&#39;을 뜻하는데, 도대체 어디로 가서 무슨 값을 가져올 수 있을까요?</p>
<p>실수로 널 포인터나 댕글링 포인터를 역참조하는 것은 C++ 프로그래머들이 가장 자주 저지르는 실수이자, 실제 현업에서 프로그램이 뻗어버리는(crash) 가장 흔한 원인이기도 합니다.</p>
<blockquote>
<p><strong>경고 (Warning)</strong>
포인터를 다룰 때는 코드가 널 포인터나 댕글링 포인터를 역참조하지 않도록 각별히 주의해야 합니다. 
그렇지 않으면 프로그램이 멈추는 등 정의되지 않은 동작이 발생합니다.</p>
</blockquote>
<hr>
<h3 id="널-포인터-확인하기">널 포인터 확인하기</h3>
<p><code>if</code> 같은 조건문을 사용해 값이 <code>true</code>인지 <code>false</code>인지 확인하는 것처럼, 조건문을 사용해 포인터가 <code>nullptr</code>인지 아닌지 확인할 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 };
    int* ptr { &amp;x };

    if (ptr == nullptr) // nullptr과 같은지 명시적으로 검사
        std::cout &lt;&lt; &quot;ptr is null\n&quot;;
    else
        std::cout &lt;&lt; &quot;ptr is non-null\n&quot;;

    int* nullPtr {};
    std::cout &lt;&lt; &quot;nullPtr is &quot; &lt;&lt; (nullPtr==nullptr ? &quot;null\n&quot; : &quot;non-null\n&quot;); // 명시적으로 검사

    return 0;
}
</code></pre>
<p>위 프로그램의 출력 결과는 다음과 같습니다.</p>
<pre><code>ptr is non-null
nullPtr is null
</code></pre><p>이전 레슨(4.9 불리언 값)에서 숫자 <code>0</code>은 불리언 값 <code>false</code>로 변환되고, 다른 숫자는 <code>true</code>로 변환된다고 배웠습니다.</p>
<p>포인터 역시 불리언 값으로 자연스럽게(암시적으로) 변환됩니다. 
<strong>널 포인터는 <code>false</code>로, 널이 아닌 일반 포인터는 <code>true</code>로 변환됩니다.</strong> 
덕분에 굳이 <code>== nullptr</code>이라고 길게 쓰지 않아도, 다음과 같이 간단하게 널 포인터 여부를 확인할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 5 };
    int* ptr { &amp;x };

    // 포인터는 널일 경우 false로, 널이 아닐 경우 true로 자동 변환됩니다
    if (ptr) // 불리언으로의 암시적 변환
        std::cout &lt;&lt; &quot;ptr is non-null\n&quot;;
    else
        std::cout &lt;&lt; &quot;ptr is null\n&quot;;

    int* nullPtr {};
    std::cout &lt;&lt; &quot;nullPtr is &quot; &lt;&lt; (nullPtr ? &quot;non-null\n&quot; : &quot;null\n&quot;); // 불리언으로의 암시적 변환

    return 0;
}
</code></pre>
<blockquote>
<p><strong>경고 (Warning)</strong>
이러한 조건문 검사는 포인터가 널인지, 널이 아닌지만 구분해 줍니다. 널이 아닌 포인터가 &#39;안전하고 유효한 객체&#39;를 가리키고 있는지, 아니면 &#39;이미 사라진 허공(댕글링 상태)&#39;을 가리키고 있는지까지 알아낼 수 있는 마법 같은 방법은 없습니다.</p>
</blockquote>
<hr>
<h3 id="댕글링-포인터를-피하기-위해-nullptr-사용하기">댕글링 포인터를 피하기 위해 nullptr 사용하기</h3>
<p>앞서 널 포인터나 댕글링 포인터를 역참조하면 심각한 문제가 생긴다고 말씀드렸습니다. 
따라서 우리 코드가 절대 이런 짓을 하지 않도록 막아야 합니다.</p>
<p>널 포인터를 역참조하는 문제는 조건문을 써서 포인터가 널이 아닐 때만 접근하게 하면 쉽게 막을 수 있습니다.</p>
<pre><code class="language-cpp">// ptr이 널 포인터일 수도 있고 아닐 수도 있는 어떤 포인터라고 가정해 봅시다.
if (ptr) // ptr이 널 포인터가 아니라면
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 역참조해도 안전합니다
else
    // ptr을 역참조하지 않는 다른 안전한 작업을 수행합니다 (에러 메시지를 띄우거나, 아무것도 하지 않음 등)
</code></pre>
<p>그렇다면 댕글링 포인터는 어떻게 해야 할까요? 앞서 말했듯 포인터가 댕글링 상태인지 알아낼 방법은 없으므로, <strong>애초에 프로그램 안에 댕글링 포인터가 돌아다니지 않도록 막는 것</strong>이 최선입니다. 이를 위해 유효한 객체를 가리키고 있지 않은 포인터는 반드시 <code>nullptr</code>로 덮어씌워 주어야 합니다.</p>
<p>그렇게 규칙을 지키면, 포인터를 역참조하기 전에 그저 &#39;널인지 아닌지&#39;만 확인하면 됩니다. 널이 아니라면 댕글링 상태가 아닐 것이라고 안심하고 코드를 짤 수 있기 때문입니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
모든 포인터는 유효한 객체의 주소를 가지고 있거나, 아니면 <code>nullptr</code> 상태여야만 합니다. 
그렇게 관리하면 포인터가 널인지 여부만 체크해도 코드가 안전하게 작동합니다.</p>
</blockquote>
<p>하지만 안타깝게도 댕글링 포인터를 피하는 것이 항상 말처럼 쉽지만은 않습니다. 객체가 메모리에서 삭제되어(파괴되어) 사라지면, 그 객체를 가리키던 모든 포인터는 허공을 맴도는 댕글링 상태가 됩니다. <strong>이런 포인터들은 자동으로 <code>nullptr</code>로 바뀌지 않습니다!</strong> 방금 사라진 객체를 가리키고 있던 포인터들을 찾아내어 모두 <code>nullptr</code>로 초기화해 주는 것은 온전히 프로그래머의 몫입니다.</p>
<blockquote>
<p><strong>경고 (Warning)</strong>
객체가 파괴되면 그 객체를 가리키던 모든 포인터는 댕글링 상태가 됩니다 (자동으로 <code>nullptr</code>이 되지 않습니다). 이런 상황을 잘 파악하고 직접 포인터를 <code>nullptr</code>로 변경해 주어야 합니다.</p>
</blockquote>
<hr>
<h3 id="과거의-널-포인터-리터럴-0과-null">과거의 널 포인터 리터럴: 0과 NULL</h3>
<p>오래전에 작성된 코드를 보면 <code>nullptr</code> 대신 다른 두 가지 값이 쓰인 것을 볼 수 있습니다.</p>
<p>첫 번째는 숫자 <code>0</code>입니다. 포인터 문맥에서 숫자 <code>0</code>은 특별히 널(null) 값을 의미하도록 약속되어 있습니다. 
C++에서 포인터에 그냥 일반 숫자를 대입할 수 있는 유일한 예외 상황이 바로 이 <code>0</code>을 넣을 때뿐입니다.</p>
<pre><code class="language-cpp">int main()
{
    float* ptr { 0 };  // ptr은 이제 널 포인터입니다 (예시일 뿐이니, 요즘 코드엔 이렇게 쓰지 마세요)

    float* ptr2; // ptr2는 초기화되지 않았습니다
    ptr2 = 0; // ptr2는 이제 널 포인터가 됩니다 (마찬가지로 권장하지 않습니다)

    return 0;
}
</code></pre>
<blockquote>
<p><strong>참고로...</strong>
현대의 컴퓨터 구조(아키텍처)에서는 널 포인터를 나타내기 위해 주로 메모리 주소 <code>0</code>번지를 사용합니다. 
하지만 C++ 표준이 이를 강제하는 것은 아니라서, 일부 독특한 컴퓨터 환경에서는 다른 주소 값을 널로 쓰기도 합니다. 
만약 포인터 자리에 숫자 <code>0</code>을 넣으면, 컴퓨터가 알아서 &#39;자신의 환경에 맞는 널 포인터 주소&#39;로 똑똑하게 번역해 줍니다.</p>
</blockquote>
<p>두 번째로 자주 보이는 것은 <code>&lt;cstddef&gt;</code> 헤더에 정의되어 있는 <code>NULL</code>이라는 매크로입니다. 이것은 C 언어 시절부터 사용되던 유산으로, 과거에는 널 포인터를 표현할 때 가장 흔하게 쓰였습니다.</p>
<p>하지만 현대 C++ 프로그래밍에서는 <strong><code>0</code>과 <code>NULL</code> 둘 다 사용하지 않는 것이 좋습니다 (대신 항상 <code>nullptr</code>을 사용하세요).</strong> 
그 이유에 대해서는 나중에 레슨 12.11 (주소로 전달하기 파트 2)에서 더 자세히 다루겠습니다.</p>
<hr>
<h3 id="가능한-한-포인터보다-참조reference를-선호하세요">가능한 한 포인터보다 참조(reference)를 선호하세요</h3>
<p>포인터와 참조(reference) 모두 &#39;다른 객체에 간접적으로 접근&#39;할 수 있게 해주는 기능입니다.</p>
<p>포인터는 가리키는 대상을 마음대로 바꿀 수 있고, 아무것도 가리키지 않는 널(null) 상태가 될 수도 있다는 추가적인 능력이 있습니다. 
하지만 그 강력한 능력 때문에 훨씬 더 위험합니다. 널 포인터를 실수로 사용할 위험이 항상 존재하며, 대상을 자유롭게 바꿀 수 있다 보니 실수로 댕글링 포인터를 만들기도 쉽습니다.</p>
<pre><code class="language-cpp">int main()
{
    int* ptr { };

    {
        int x{ 5 };
        ptr = &amp;x; // 포인터에 곧 사라질 운명인 객체를 연결합니다 (참조로는 이런 실수를 하기가 힘듭니다)
    } // 중괄호가 끝나면서 x는 파괴되었고, ptr은 이제 허공을 가리키는 댕글링 상태가 되었습니다

    if (ptr) // ptr이 nullptr은 아니기 때문에 이 조건문은 true로 통과해버립니다
        std::cout &lt;&lt; *ptr; // 이미 사라진 대상을 역참조하므로 정의되지 않은 동작이 발생합니다!

    return 0;
}
</code></pre>
<p>반면, 참조(reference)는 애초에 널(null) 상태가 되는 것 자체가 불가능하므로 널 참조에 대한 걱정을 할 필요가 없습니다. 
또한 참조는 만들어질 때 무조건 유효한 객체와 연결되어야 하고, 한 번 연결되면 다른 객체로 중간에 바꿀 수 없기 때문에 댕글링 참조를 만드는 실수도 훨씬 덜 일어납니다.</p>
<p>결론적으로 참조가 포인터보다 훨씬 안전하므로, 꼭 포인터의 특별한 능력이 필요한 상황이 아니라면 <strong>포인터보다는 참조를 우선적으로 사용하는 것이 좋습니다.</strong></p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
포인터가 제공하는 추가적인 기능이 반드시 필요한 경우가 아니라면, 포인터 대신 참조(reference)를 우선적으로 사용하세요.</p>
</blockquote>
<hr>
<h2 id="129--포인터와-const-pointers-and-const">12.9 — 포인터와 const (Pointers and const)</h2>
<p>다음 코드를 살펴봅시다.</p>
<pre><code class="language-cpp">int main(){
    int x { 5 };
    int* ptr { &amp;x }; // ptr은 일반적인(const가 아닌) 포인터입니다.

    int y { 6 };
    ptr = &amp;y; // 다른 값의 주소를 가리키도록 변경할 수 있습니다.

    *ptr = 7; // 포인터가 가리키고 있는 주소의 값을 변경할 수 있습니다.

    return 0;
}
</code></pre>
<p>일반적인(const가 아닌) 포인터를 사용하면, 포인터가 가리키는 대상(새로운 주소 할당)과 가리키고 있는 주소의 값(포인터를 통해 새 값 할당)을 모두 변경할 수 있습니다.</p>
<p>하지만, 만약 우리가 가리키려는 값이 <code>const</code>(상수)라면 어떻게 될까요?</p>
<pre><code class="language-cpp">int main(){
    const int x { 5 }; // 이제 x는 const(상수)입니다.
    int* ptr { &amp;x };   // 컴파일 에러: const int* 에서 int* 로 변환할 수 없습니다.

    return 0;
}
</code></pre>
<p>위 코드는 에러가 발생하여 실행되지 않습니다. 일반 포인터가 <code>const</code> 변수를 가리키게 할 수는 없기 때문입니다. 
이는 아주 당연한 이치입니다. <code>const</code> 변수는 값을 변경할 수 없도록 잠가둔 변수입니다. 만약 일반 포인터가 <code>const</code> 값을 가리키도록 허락한다면, 프로그래머가 포인터를 이용해 그 값을 마음대로 바꿔버릴 수 있게 되고, 이는 변수를 <code>const</code>로 만든 의미를 훼손하게 됩니다.</p>
<hr>
<h3 id="상수-값을-가리키는-포인터-pointer-to-const-value">상수 값을 가리키는 포인터 (Pointer to const value)</h3>
<p><strong>상수 값을 가리키는 포인터</strong>(간단히 &#39;상수 포인터&#39;라고도 함)는 말 그대로 값이 변하지 않는 상수(<code>const</code>)를 가리키는 일반 포인터입니다.</p>
<p>이 포인터를 선언하려면, 포인터의 데이터 타입 앞에 <code>const</code> 키워드를 붙여주면 됩니다.</p>
<pre><code class="language-cpp">int main(){
    const int x{ 5 };
    const int* ptr { &amp;x }; // 정상: ptr은 &quot;const int&quot;를 가리키고 있습니다.

    *ptr = 6; // 허용되지 않음: const 값은 변경할 수 없습니다.

    return 0;
}
</code></pre>
<p>위 예제에서 <code>ptr</code>은 <code>const int</code>를 가리킵니다. 가리키는 대상이 상숫값이므로, 포인터를 통해 그 값을 변경할 수는 없습니다.</p>
<p>하지만 포인터 자체는 <code>const</code>가 아니기 때문에(단지 <code>const</code>인 값을 가리킬 뿐입니다), 포인터에 새로운 주소를 할당하여 가리키는 대상을 다른 곳으로 바꿀 수는 있습니다.</p>
<pre><code class="language-cpp">int main(){
    const int x{ 5 };
    const int* ptr { &amp;x }; // ptr은 const int x를 가리킵니다.

    const int y{ 6 };
    ptr = &amp;y; // 정상: 이제 ptr은 const int y를 가리킵니다.

    return 0;
}
</code></pre>
<p>참조(reference)와 마찬가지로, 이 포인터는 <code>const</code>가 아닌 일반 변수도 가리킬 수 있습니다. 
단지 이 포인터를 통해 접근할 때는, 원래 변수가 <code>const</code>인지 아닌지와 상관없이 무조건 상수로 취급하여 값을 바꿀 수 없게 만듭니다.</p>
<pre><code class="language-cpp">int main(){
    int x{ 5 }; // const가 아님
    const int* ptr { &amp;x }; // ptr은 &quot;const int&quot;를 가리킵니다.

    *ptr = 6;  // 허용되지 않음: ptr은 &quot;const int&quot;를 가리키므로 ptr을 통해서는 값을 바꿀 수 없습니다.
    x = 6; // 허용됨: 일반 변수 이름인 x를 통해 접근할 때는 여전히 값을 바꿀 수 있습니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="const-포인터-const-pointers">const 포인터 (Const pointers)</h3>
<p>포인터 자체를 상수로 만들 수도 있습니다. <strong>const 포인터</strong>는 한 번 초기화되면 자기가 가리키는 주소값을 영원히 바꿀 수 없는 포인터를 말합니다.</p>
<p>const 포인터를 선언하려면 포인터를 뜻하는 별표(<code>*</code>) 기호 <strong>뒤에</strong> <code>const</code> 키워드를 적어줍니다:</p>
<pre><code class="language-cpp">int main(){
    int x{ 5 };
    int* const ptr { &amp;x }; // 별표 뒤의 const는 이 포인터가 &#39;const 포인터&#39;임을 의미합니다.

    return 0;
}
</code></pre>
<p>위 코드에서 <code>ptr</code>은 일반 <code>int</code> 값을 가리키는 const 포인터입니다.
일반적인 <code>const</code> 변수처럼, const 포인터는 만들어짐과 동시에 반드시 주소값을 넣어 초기화해야 하며, 나중에 다른 주소로 바꿀 수 없습니다:</p>
<pre><code class="language-cpp">int main(){
    int x{ 5 };
    int y{ 6 };

    int* const ptr { &amp;x }; // 정상: const 포인터가 x의 주소로 초기화되었습니다.
    ptr = &amp;y; // 에러: 한 번 초기화되면 const 포인터의 주소는 바꿀 수 없습니다.

    return 0;
}
</code></pre>
<p>하지만, 포인터가 가리키고 있는 값 자체는 일반 변수(non-const)이므로, 포인터를 이용해서 그 값을 바꾸는 것은 가능합니다.</p>
<pre><code class="language-cpp">int main(){
    int x{ 5 };
    int* const ptr { &amp;x }; // ptr은 이제 평생 x만 가리킵니다.

    *ptr = 6; // 정상: 가리키고 있는 값은 const가 아니므로 바꿀 수 있습니다.

    return 0;
}
</code></pre>
<hr>
<h3 id="상수-값을-가리키는-const-포인터-const-pointer-to-a-const-value">상수 값을 가리키는 const 포인터 (Const pointer to a const value)</h3>
<p>마지막으로, 데이터 타입 앞과 별표(<code>*</code>) 뒤에 모두 <code>const</code> 키워드를 사용하여 <strong>상수 값을 가리키는 const 포인터</strong>를 선언할 수도 있습니다:</p>
<pre><code class="language-cpp">int main(){
    int value { 5 };
    const int* const ptr { &amp;value }; // 상수 값을 가리키는 const 포인터

    return 0;
}
</code></pre>
<p>이 포인터는 자신이 가진 주소값도 바꿀 수 없고, 포인터를 통해 가리키는 값도 바꿀 수 없습니다. 오로지 가리키는 값을 읽어오는 것만 가능합니다.</p>
<hr>
<h3 id="포인터와-const-핵심-요약">포인터와 const 핵심 요약</h3>
<p>복잡해 보이지만, 다음 4가지 규칙만 기억하시면 됩니다. 아주 논리적입니다.</p>
<ol>
<li><strong>일반 포인터 (<code>int* ptr</code>)</strong>: 다른 주소를 할당하여 가리키는 대상을 변경할 수 있습니다.</li>
<li><strong>const 포인터 (<code>int* const ptr</code>)</strong>: 항상 같은 주소만 가리키며, 가리키는 대상을 변경할 수 없습니다.</li>
<li><strong>일반 값을 가리키는 포인터 (<code>int* ptr</code>)</strong>: 포인터를 통해 가리키는 값을 변경할 수 있습니다. 단, <code>const</code> 값은 가리킬 수 없습니다.</li>
<li><strong>상수 값을 가리키는 포인터 (<code>const int* ptr</code>)</strong>: 포인터를 통해 접근할 때 그 값을 상수로 취급하므로, 값을 변경할 수 없습니다. 이 포인터는 <code>const</code> 변수나 일반 변수 모두 가리킬 수 있습니다. (단, 주소가 없는 임시 값은 가리킬 수 없습니다).</li>
</ol>
<p>작성법을 외우는 것이 조금 헷갈릴 수 있는데, 이렇게 기억해 보세요:</p>
<ul>
<li><strong>별표(<code>*</code>) 좌측의 const</strong> (예: <code>const int* ptr</code>): 가리키는 데이터의 &#39;타입&#39;과 관련이 있습니다. 즉, 값이 상수이므로 포인터를 통해 값을 수정할 수 없습니다.</li>
<li><strong>별표(<code>*</code>) 우측의 const</strong> (예: <code>int* const ptr</code>): 포인터 &#39;자체&#39;와 관련이 있습니다. 즉, 포인터가 다른 곳을 가리키도록 주소를 수정할 수 없습니다.</li>
</ul>
<pre><code class="language-cpp">int main(){
    int v{ 5 };

    int* ptr0 { &amp;v };             // &quot;int&quot;를 가리키며 자신은 const가 아닙니다. 값과 주소 모두 수정할 수 있습니다.
    const int* ptr1 { &amp;v };       // &quot;const int&quot;를 가리키며 자신은 const가 아닙니다. 주소만 수정할 수 있습니다.
    int* const ptr2 { &amp;v };       // &quot;int&quot;를 가리키며 자신은 const입니다. 값만 수정할 수 있습니다.
    const int* const ptr3 { &amp;v }; // &quot;const int&quot;를 가리키며 자신은 const입니다. 값도 주소도 수정할 수 없습니다.

    // const가 * 의 왼쪽에 있으면, const는 &#39;값&#39;에 적용됩니다.
    // const가 * 의 오른쪽에 있으면, const는 &#39;포인터&#39;에 적용됩니다.

    return 0;
}
</code></pre>
<hr>
<h2 id="1210--주소로-전달하기-pass-by-address">12.10 — 주소로 전달하기 (Pass by address)</h2>
<p>이전 강의에서 우리는 함수에 인수를 전달하는 두 가지 방법, 
즉 <strong>값으로 전달하기</strong>(Pass by value)와 <strong>참조로 전달하기</strong>(Pass by reference)에 대해 배웠습니다.</p>
<p>다음은 <code>std::string</code> 객체를 값과 참조로 각각 전달하는 방법을 보여주는 간단한 예제 프로그램입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

void printByValue(std::string val) // 함수 매개변수는 str의 복사본입니다
{
    std::cout &lt;&lt; val &lt;&lt; &#39;\n&#39;; // 복사본을 통해 값을 출력합니다
}

void printByReference(const std::string&amp; ref) // 함수 매개변수는 str에 연결(바인딩)된 참조입니다
{
    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 참조를 통해 값을 출력합니다
}

int main()
{
    std::string str{ &quot;Hello, world!&quot; };

    printByValue(str); // 값으로 str을 전달하며, str의 복사본을 만듭니다
    printByReference(str); // 참조로 str을 전달하며, 복사본을 만들지 않습니다

    return 0;
}
</code></pre>
<p>인수 <code>str</code>을 <strong>값으로 전달</strong>하면, 함수 매개변수인 <code>val</code>은 원래 인수의 &#39;복사본&#39;을 받게 됩니다.
매개변수가 복사본이기 때문에, 함수 안에서 <code>val</code>을 변경하더라도 원본 <code>str</code>에는 아무런 영향을 주지 않습니다.</p>
<p>반면, <code>str</code>을 <strong>참조로 전달</strong>하면 참조 매개변수인 <code>ref</code>가 실제 인수에 직접 연결(바인딩)됩니다. 따라서 복사본을 만들지 않습니다. 
위 예제에서는 참조 매개변수가 <code>const</code>(상수)이므로 <code>ref</code>를 통해 값을 변경할 수는 없습니다. 
하지만 만약 <code>ref</code>가 <code>const</code>가 아니었다면, 함수 안에서 <code>ref</code>를 변경할 때 원본 <code>str</code>도 함께 변경되었을 것입니다.</p>
<p>두 경우 모두, 호출하는 쪽에서 함수에 인수로 전달할 실제 객체(<code>str</code>)를 제공한다는 점은 같습니다.</p>
<hr>
<h3 id="주소로-전달하기-pass-by-address">주소로 전달하기 (Pass by address)</h3>
<p>C++에는 함수에 값을 전달하는 세 번째 방법이 있는데, 이를 <strong>주소로 전달하기</strong>라고 부릅니다. 
이 방법에서는 객체 자체를 전달하는 대신, 호출하는 쪽에서 (포인터를 통해) <strong>객체의 주소</strong>를 전달합니다.</p>
<p>객체의 주소를 담고 있는 이 포인터는 호출된 함수의 포인터 매개변수로 복사됩니다. 
그런 다음 함수는 해당 포인터를 역참조(dereference, 포인터가 가리키는 실제 값에 접근)하여 전달된 객체를 사용할 수 있습니다.</p>
<p>위 프로그램에 &#39;주소로 전달하기&#39; 방식을 추가한 버전은 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

void printByValue(std::string val) // 함수 매개변수는 str의 복사본입니다
{
    std::cout &lt;&lt; val &lt;&lt; &#39;\n&#39;; // 복사본을 통해 값을 출력합니다
}

void printByReference(const std::string&amp; ref) // 함수 매개변수는 str에 연결(바인딩)된 참조입니다
{
    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 참조를 통해 값을 출력합니다
}

void printByAddress(const std::string* ptr) // 함수 매개변수는 str의 주소를 담고 있는 포인터입니다
{
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 역참조된 포인터를 통해 값을 출력합니다
}

int main()
{
    std::string str{ &quot;Hello, world!&quot; };

    printByValue(str); // 값으로 str을 전달하며, str의 복사본을 만듭니다
    printByReference(str); // 참조로 str을 전달하며, 복사본을 만들지 않습니다
    printByAddress(&amp;str); // 주소로 str을 전달하며, 복사본을 만들지 않습니다

    return 0;
}
</code></pre>
<p>이 세 가지 버전이 얼마나 비슷한지 눈치채셨나요? 이제 &#39;주소로 전달하기&#39; 버전을 좀 더 자세히 살펴보겠습니다.</p>
<p>첫째, <code>printByAddress()</code> 함수가 주소로 값을 받게 하기 위해, 함수 매개변수를 <code>ptr</code>이라는 이름의 포인터로 만들었습니다. 
이 함수는 값을 읽기만 할 것이므로 <code>ptr</code>은 상수 값을 가리키는 포인터(<code>const</code> 포인터)로 선언되었습니다.</p>
<p>함수 내부에서는 <code>ptr</code> 매개변수를 역참조(<code>*ptr</code>)하여 포인터가 가리키는 객체의 값에 접근합니다.</p>
<p>둘째, 함수를 호출할 때 그냥 <code>str</code> 객체를 넘겨줄 수는 없습니다. 
<code>str</code>의 <strong>주소</strong>를 넘겨주어야 합니다. 가장 쉬운 방법은 주소 연산자(<code>&amp;</code>)를 사용하여 <code>str</code>의 주소를 담은 포인터를 얻는 것입니다.</p>
<pre><code class="language-cpp">printByAddress(&amp;str); // 주소 연산자(&amp;)를 사용하여 str의 주소를 담은 포인터를 얻습니다
</code></pre>
<p>이 코드가 실행되면 <code>&amp;str</code>은 <code>str</code>의 주소를 담은 포인터를 생성합니다. 
그런 다음 이 주소는 함수 호출 과정에서 매개변수 <code>ptr</code>에 복사됩니다. 
이제 <code>ptr</code>이 <code>str</code>의 주소를 가지고 있으므로, 함수가 <code>ptr</code>을 역참조하면 <code>str</code>의 값을 얻게 되고 그 값을 화면에 출력하게 됩니다.</p>
<p>이게 전부입니다!</p>
<p>위 예제에서는 주소 연산자(<code>&amp;</code>)를 사용해 <code>str</code>의 주소를 직접 구했지만, 만약 이미 <code>str</code>의 주소를 담고 있는 포인터 변수가 있다면 그것을 대신 전달해도 됩니다.</p>
<pre><code class="language-cpp">int main()
{
    std::string str{ &quot;Hello, world!&quot; };

    printByValue(str); // 값으로 str을 전달하며, str의 복사본을 만듭니다
    printByReference(str); // 참조로 str을 전달하며, 복사본을 만들지 않습니다
    printByAddress(&amp;str); // 주소로 str을 전달하며, 복사본을 만들지 않습니다

    std::string* ptr { &amp;str }; // str의 주소를 담는 포인터 변수를 정의합니다
    printByAddress(ptr); // 주소로 str을 전달하며, 복사본을 만들지 않습니다

    return 0;
}
</code></pre>
<hr>
<h3 id="용어-정리">용어 정리</h3>
<ul>
<li><code>&amp;</code> 연산자를 사용하여 변수의 주소를 인수로 전달할 때, 우리는 &quot;변수가 <strong>주소로 전달되었다</strong>&quot;고 말합니다.</li>
<li>객체의 주소를 담고 있는 포인터 변수를 같은 타입의 매개변수로 전달할 때, 우리는 &quot;객체는 <strong>주소로 전달</strong>되었고, 포인터 자체는 <strong>값으로 전달</strong>되었다&quot;고 말합니다.</li>
</ul>
<hr>
<h3 id="주소로-전달하면-객체의-복사본을-만들지-않습니다">주소로 전달하면 객체의 복사본을 만들지 않습니다</h3>
<p>다음 코드를 살펴봅시다.</p>
<pre><code class="language-cpp">std::string str{ &quot;Hello, world!&quot; };
printByAddress(&amp;str); // 주소 연산자(&amp;)를 사용하여 str의 주소를 담은 포인터를 얻습니다
</code></pre>
<p>이전 강의에서 언급했듯, <code>std::string</code>을 복사하는 것은 비용이 꽤 많이 드는 작업이라 피하는 것이 좋습니다. <code>std::string</code>을 주소로 전달하면, 실제 <code>std::string</code> 객체를 복사하는 것이 아닙니다. 단지 객체의 주소가 담긴 <strong>포인터만 호출자에서 함수로 복사</strong>할 뿐입니다. 주소는 보통 4바이트나 8바이트의 작은 크기이므로 포인터를 복사하는 작업은 항상 매우 빠릅니다.</p>
<p>따라서 참조로 전달하는 것과 마찬가지로, <strong>주소로 전달하는 방식도 빠르며 인수로 전달된 객체의 복사본을 만들지 않습니다.</strong></p>
<hr>
<h3 id="주소로-전달하면-함수가-원본-인수의-값을-수정할-수-있습니다">주소로 전달하면 함수가 원본 인수의 값을 수정할 수 있습니다</h3>
<p>객체를 주소로 전달하면 함수는 전달된 객체의 주소를 받게 되고, 포인터를 역참조하여 객체에 접근할 수 있습니다. 
이것은 객체의 복사본이 아닌 <strong>실제 원본 인수의 주소</strong>이기 때문에, 함수 매개변수가 <code>const</code>가 아닌 포인터라면 함수 안에서 포인터를 통해 원본 값을 수정할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void changeValue(int* ptr) // 참고: 이 예제에서 ptr은 const가 아닌 값을 가리키는 포인터입니다
{
    *ptr = 6; // 값을 6으로 변경합니다
}

int main()
{
    int x{ 5 };

    std::cout &lt;&lt; &quot;x = &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;

    changeValue(&amp;x); // 함수에 x의 주소를 전달하고 있습니다

    std::cout &lt;&lt; &quot;x = &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드는 다음과 같이 출력합니다.</p>
<blockquote>
<p>x = 5
x = 6</p>
</blockquote>
<p>보시다시피 인수의 값이 수정되었고, 이 변경 사항은 <code>changeValue()</code> 함수가 끝난 후에도 계속 유지됩니다.</p>
<p>만약 함수가 전달받은 객체를 수정하지 않아야 한다면, 함수 매개변수를 <code>const</code> 값을 가리키는 포인터(pointer-to-const)로 만들어야 합니다.</p>
<pre><code class="language-cpp">void changeValue(const int* ptr) // 참고: 이제 ptr은 const 값을 가리키는 포인터입니다
{
    *ptr = 6; // 오류: 상수(const) 값을 변경할 수 없습니다
}
</code></pre>
<p>일반 매개변수에 <code>const</code>를 잘 붙이지 않는 이유와 마찬가지로, 포인터 매개변수 자체를 <code>const</code>로 만드는 일은 드뭅니다.
포인터 매개변수를 선언할 때, 매개변수 자체를 상수로 만들기 위해 <code>const</code>를 사용하는 것은 큰 의미가 없습니다.
(호출자에게 아무 영향을 주지 않으며, 단지 함수 내에서 포인터가 다른 곳을 가리키지 않겠다는 문서화 역할 정도만 합니다) 
반면, 함수가 전달된 객체를 수정할 수 있는지 여부를 나타내는 <strong>상수 값을 가리키는 포인터(pointer-to-const)</strong> 와 <strong>일반 포인터(pointer-to-non-const)</strong> 를 구분하는 것은 매우 중요합니다(호출자가 함수에 의해 원본 값이 바뀔 수 있는지를 알아야 하기 때문입니다).</p>
<pre><code class="language-cpp">void foo(const char* source, char* dest, int count);             // 일반 포인터를 사용하면, 모든 const가 의미를 가집니다.
void foo(const char* const source, char* const dest, int count); // 매개변수 자체를 상수 포인터로 사용하면, 너무 많은 const 사이에 묻혀서 `dest`가 원본을 수정할 수 있는 포인터라는 사실을 눈치채기 어렵습니다.
</code></pre>
<p>첫 번째 경우, <code>source</code>는 변경 불가능한 값을 가리키고 <code>dest</code>는 변경 가능한 값을 가리킨다는 것을 한눈에 알 수 있습니다.
두 번째 경우에는 함수가 <code>dest</code>를 통해 원본 객체를 수정할 수 있다는 사실을 파악하기가 훨씬 더 어렵습니다!</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
함수가 전달된 객체를 수정해야 하는 경우가 아니라면, 일반 포인터보다는 <strong>상수 값을 가리키는 포인터(pointer-to-const)</strong> 매개변수를 사용하는 것을 권장합니다. 또한, 특별한 이유가 없다면 함수 매개변수 포인터 자체를 <code>const</code> 포인터로 만들지 마세요.</p>
</blockquote>
<hr>
<h3 id="널null-체크">널(Null) 체크</h3>
<p>이제 아무 문제 없어 보이는 다음 프로그램을 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int* ptr)
{
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int x{ 5 };
    print(&amp;x);

    int* myPtr {};
    print(myPtr);

    return 0;
}
</code></pre>
<p>이 프로그램이 실행되면 처음엔 <code>5</code>를 출력하겠지만, 그다음에는 십중팔구 비정상 종료(crash)될 것입니다.</p>
<p><code>print(myPtr)</code>을 호출할 때 <code>myPtr</code>은 널(null) 포인터이므로, 함수 매개변수 <code>ptr</code> 역시 널 포인터가 됩니다.
함수 내부에서 이 널 포인터를 역참조하려고 시도하면 &#39;정의되지 않은 동작(undefined behavior)&#39;이 발생하여 프로그램이 뻗어버립니다.</p>
<p>주소로 매개변수를 전달할 때는 값을 역참조하기 전에 <strong>포인터가 널 포인터가 아닌지 확인하는 주의가 필요합니다.</strong> 
가장 간단한 방법은 <code>if</code> 조건문을 사용하는 것입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int* ptr)
{
    if (ptr) // ptr이 널 포인터가 아니라면
    {
        std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;;
    }
}

int main()
{
    int x{ 5 };

    print(&amp;x);
    print(nullptr);

    return 0;
}
</code></pre>
<p>위 프로그램에서는 <code>ptr</code>을 역참조하기 전에 널인지 아닌지 테스트합니다. 이렇게 간단한 함수에서는 괜찮지만, 복잡한 함수에서는 여러 번 테스트를 해야 해서 코드가 지저분해지거나, 핵심 로직이 깊게 중첩(nesting)될 수 있습니다.</p>
<p>대부분의 경우에는 반대로 하는 것이 더 효과적입니다. 
매개변수가 널인지 먼저 확인하고, 널이라면 즉시 함수를 빠져나오게(Early return) 하는 것입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int* ptr)
{
    if (!ptr) // ptr이 널 포인터라면, 즉시 호출자에게 반환합니다
        return;

    // 이 지점에 도달했다면 ptr이 유효하다고 가정할 수 있으므로,
    // 더 이상의 테스트나 중첩이 필요하지 않습니다.

    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int x{ 5 };

    print(&amp;x);
    print(nullptr);

    return 0;
}
</code></pre>
<p>만약 해당 함수에 절대 널 포인터가 전달되어서는 안 된다면, (이전 강의에서 배운) <code>assert</code>를 사용할 수도 있습니다. 
(<code>assert</code>는 &quot;절대 일어나선 안 될 일&quot;을 명시하기 위한 도구입니다.)</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cassert&gt;

void print(const int* ptr) // 이제 상수 int를 가리키는 포인터입니다
{
    assert(ptr); // 널 포인터가 전달되면 디버그 모드에서 프로그램을 종료시킵니다 (이런 일은 절대 일어나선 안 되기 때문입니다)

    // (선택 사항) 프로덕션 환경에서는 실제로 이런 일이 발생해도 프로그램이 뻗지 않도록 에러로 처리합니다
    if (!ptr)
        return;

    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;;
}

int main()
{
    int x{ 5 };

    print(&amp;x);
    print(nullptr);

    return 0;
}
</code></pre>
<hr>
<h3 id="상수-참조로-전달하기를-권장합니다">(상수) 참조로 전달하기를 권장합니다</h3>
<p>위 예제의 <code>print()</code> 함수는 널 값을 제대로 &#39;처리&#39;한다기보다는 그냥 함수를 중단해버립니다. 그렇다면 애초에 사용자가 널 값을 전달하게 허용할 이유가 있을까요? <strong>참조로 전달하기(Pass by reference)</strong> 는 &#39;주소로 전달하기&#39;의 모든 장점을 가지면서도, 실수로 널 포인터를 역참조할 위험이 전혀 없습니다.</p>
<p>이 밖에도 &#39;상수 참조로 전달하기(Pass by const reference)&#39;는 주소로 전달하기보다 몇 가지 더 유리한 점이 있습니다.</p>
<p>첫째, 주소로 전달하려면 객체가 반드시 &#39;주소&#39;를 가지고 있어야 합니다. 즉, 좌측값(lvalue)만 주소로 전달할 수 있고, 숫자 <code>5</code> 같은 우측값(rvalue)은 전달할 수 없습니다. 반면 상수 참조는 좌측값과 우측값을 모두 받을 수 있어 훨씬 유연합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printByValue(int val) // 함수 매개변수는 인수의 복사본입니다
{
    std::cout &lt;&lt; val &lt;&lt; &#39;\n&#39;; // 복사본을 통해 값을 출력합니다
}

void printByReference(const int&amp; ref) // 함수 매개변수는 인수에 바인딩된 참조입니다
{
    std::cout &lt;&lt; ref &lt;&lt; &#39;\n&#39;; // 참조를 통해 값을 출력합니다
}

void printByAddress(const int* ptr) // 함수 매개변수는 인수의 주소를 담고 있는 포인터입니다
{
    std::cout &lt;&lt; *ptr &lt;&lt; &#39;\n&#39;; // 역참조된 포인터를 통해 값을 출력합니다
}

int main()
{
    printByValue(5);     // 유효함 (단, 복사본을 만듦)
    printByReference(5); // 유효함 (매개변수가 상수 참조이기 때문)
    printByAddress(&amp;5);  // 오류: 우측값(r-value)의 주소를 가져올 수 없음

    return 0;
}
</code></pre>
<p>둘째, 참조로 전달하는 구문이 훨씬 자연스럽습니다. 그냥 리터럴 값이나 객체를 넣기만 하면 됩니다. 
반면 주소로 전달하려면 코드 여기저기에 앰퍼샌드(<code>&amp;</code>)와 별표(<code>*</code>)가 지저분하게 흩어지게 됩니다.</p>
<p>현대 C++에서는 &#39;주소로 전달하기&#39;로 할 수 있는 대부분의 일을 다른 방법으로 더 깔끔하게 처리할 수 있습니다. 
다음의 흔한 격언을 기억하세요. <strong>&quot;가능하다면 참조로 전달하고, 반드시 필요할 때만 주소로 전달하라.&quot;</strong></p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
주소로 전달해야만 하는 특별한 이유가 없다면, 항상 <strong>참조로 전달하기</strong>를 우선적으로 사용하세요.</p>
</blockquote>
<hr>
<h2 id="1211--주소로-전달하기-pass-by-address-2부">12.11 — 주소로 전달하기 (Pass by address) (2부)</h2>
<h3 id="선택적optional-인자를-위한-주소-전달">&quot;선택적(optional)&quot; 인자를 위한 주소 전달</h3>
<p>주소로 전달하는 방식이 가장 흔하게 쓰이는 경우 중 하나는 함수가 &quot;선택적인&quot; 인자를 받을 수 있게 만들 때입니다. 
말로 설명하는 것보다 예제를 보는 것이 훨씬 이해하기 쉽습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printIDNumber(const int *id=nullptr)
{
    if (id)
        std::cout &lt;&lt; &quot;Your ID number is &quot; &lt;&lt; *id &lt;&lt; &quot;.\n&quot;;
    else
        std::cout &lt;&lt; &quot;Your ID number is not known.\n&quot;;
}

int main()
{
    printIDNumber(); // 아직 사용자의 ID를 모릅니다

    int userid { 34 };
    printIDNumber(&amp;userid); // 이제 사용자의 ID를 압니다

    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과는 다음과 같습니다:</p>
<blockquote>
<p>Your ID number is not known.
Your ID number is 34.</p>
</blockquote>
<p>이 프로그램에서 <code>printIDNumber()</code> 함수는 주소로 전달되는 하나의 매개변수(<code>id</code>)를 가지며, 기본값은 <code>nullptr</code>로 설정되어 있습니다. <code>main()</code> 함수 안에서 이 함수를 두 번 호출합니다.
첫 번째 호출에서는 사용자의 ID를 모르기 때문에 아무런 인자 없이 호출합니다. 그러면 <code>id</code> 매개변수는 기본값인 <code>nullptr</code>을 가지게 되고, 함수는 &quot;Your ID number is not known.&quot;을 출력합니다.
두 번째 호출에서는 유효한 ID가 생겼기 때문에 <code>printIDNumber(&amp;userid)</code>처럼 변수의 주소를 넘겨줍니다. 
<code>id</code> 매개변수가 <code>userid</code>의 주소를 받으므로, 함수는 &quot;Your ID number is 34.&quot;를 출력하게 됩니다.</p>
<p>하지만 많은 경우, 위와 같은 결과는 <strong>함수 오버로딩(Function overloading)</strong>을 사용하는 것이 더 좋은 방법입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printIDNumber()
{
    std::cout &lt;&lt; &quot;Your ID is not known\n&quot;;
}

void printIDNumber(int id)
{
    std::cout &lt;&lt; &quot;Your ID is &quot; &lt;&lt; id &lt;&lt; &quot;\n&quot;;
}

int main()
{
    printIDNumber(); // 아직 사용자의 ID를 모릅니다

    int userid { 34 };
    printIDNumber(userid); // 사용자의 ID가 34라는 것을 압니다

    printIDNumber(62); // 이제 rvalue(우측값, 단순 숫자 등) 인자도 작동합니다

    return 0;
}
</code></pre>
<p>오버로딩을 사용하면 여러 가지 장점이 있습니다. 포인터가 비어 있는지(null) 확인하지 않아도 되기 때문에 역참조 오류를 걱정할 필요가 없고, 단순한 숫자(리터럴)나 다른 우측값(rvalue)도 인자로 바로 전달할 수 있습니다.</p>
<hr>
<h3 id="포인터-매개변수가-가리키는-대상-변경하기">포인터 매개변수가 가리키는 대상 변경하기</h3>
<p>주소를 함수에 전달할 때, 그 주소 값은 원본 인자에서 함수의 포인터 매개변수로 <strong>복사</strong>됩니다.
(주소를 복사하는 작업은 매우 빠르기 때문에 성능상 아무 문제가 없습니다) 
다음 프로그램을 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// [[maybe_unused]]는 ptr2가 설정되었지만 사용되지 않았다는 컴파일러 경고를 없애줍니다
void nullify([[maybe_unused]] int* ptr2)
{
    ptr2 = nullptr; // 함수 매개변수를 널 포인터로 만듭니다
}

int main()
{
    int x{ 5 };
    int* ptr{ &amp;x }; // ptr은 x를 가리킵니다

    std::cout &lt;&lt; &quot;ptr is &quot; &lt;&lt; (ptr ? &quot;non-null\n&quot; : &quot;null\n&quot;);

    nullify(ptr);

    std::cout &lt;&lt; &quot;ptr is &quot; &lt;&lt; (ptr ? &quot;non-null\n&quot; : &quot;null\n&quot;);
    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과는 다음과 같습니다:</p>
<blockquote>
<p>ptr is non-null
ptr is non-null</p>
</blockquote>
<p>보시다시피, 함수 안에서 포인터 매개변수(<code>ptr2</code>)가 가진 주소를 바꿔도 원본 인자(<code>ptr</code>)가 가진 주소에는 아무런 영향을 주지 않습니다.
(<code>ptr</code>은 여전히 <code>x</code>를 가리킵니다) <code>nullify()</code> 함수가 호출될 때, <code>ptr2</code>는 원본 <code>ptr</code>이 가지고 있던 주소의 &#39;복사본&#39;을 받게 됩니다. 따라서 함수 안에서 <code>ptr2</code>가 가리키는 대상을 <code>nullptr</code>로 변경하더라도, 이는 복사본인 <code>ptr2</code>에만 영향을 미칠 뿐입니다.</p>
<p>그렇다면 함수 안에서 원본 포인터가 가리키는 대상 자체를 바꾸고 싶다면 어떻게 해야 할까요?</p>
<hr>
<h3 id="주소로-전달하기를-참조로-전달하기">주소로 전달하기를… 참조로 전달하기?</h3>
<p>네, 가능합니다! 우리가 일반 변수를 참조(reference)로 전달할 수 있는 것처럼, 포인터도 참조로 전달할 수 있습니다. 
위와 똑같은 프로그램이지만, <code>ptr2</code>를 <strong>주소에 대한 참조(reference to an address)</strong>로 바꾼 코드를 살펴보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void nullify(int*&amp; refptr) // refptr은 이제 포인터에 대한 참조입니다
{
    refptr = nullptr; // 함수 매개변수를 널 포인터로 만듭니다
}

int main()
{
    int x{ 5 };
    int* ptr{ &amp;x }; // ptr은 x를 가리킵니다

    std::cout &lt;&lt; &quot;ptr is &quot; &lt;&lt; (ptr ? &quot;non-null\n&quot; : &quot;null\n&quot;);

    nullify(ptr);

    std::cout &lt;&lt; &quot;ptr is &quot; &lt;&lt; (ptr ? &quot;non-null\n&quot; : &quot;null\n&quot;);
    return 0;
}
</code></pre>
<p>이 프로그램의 출력 결과는 다음과 같습니다:</p>
<blockquote>
<p>ptr is non-null
ptr is null</p>
</blockquote>
<p>이제 <code>refptr</code>은 포인터에 대한 &#39;참조&#39;가 되었습니다. 따라서 원본 <code>ptr</code>이 인자로 전달되면, <code>refptr</code>은 곧 원본 <code>ptr</code> 그 자체와 하나로 묶이게(bound) 됩니다. 즉, 함수 안에서 <code>refptr</code>을 바꾸면 원본 <code>ptr</code>도 똑같이 바뀌게 됩니다.</p>
<blockquote>
<p><strong>참고로...</strong><br>포인터에 대한 참조를 사용하는 일은 꽤 드물기 때문에, 문법을 헷갈리기 쉽습니다 (<code>int*&amp;</code>인지 <code>int&amp;*</code>인지 헷갈리죠?). 
다행인 것은 만약 거꾸로 쓴다면 컴파일러가 알아서 에러를 내준다는 점입니다. 
(참조는 메모리에 존재하는 실물 객체가 아니기 때문에, &#39;참조를 가리키는 포인터&#39;는 만들 수 없기 때문입니다.) 
에러가 나면 그때 순서를 바꿔 쓰시면 됩니다.</p>
</blockquote>
<hr>
<h3 id="왜-0이나-null을-더-이상-사용하지-않는가-선택-사항">왜 0이나 NULL을 더 이상 사용하지 않는가 (선택 사항)</h3>
<p>이 섹션에서는 왜 포인터를 비울 때 <code>0</code>이나 <code>NULL</code>을 사용하는 것이 좋지 않은지 설명합니다.</p>
<ol>
<li>숫자 <code>0</code>은 정수로 해석될 수도 있고, 널 포인터(비어있는 포인터)로 해석될 수도 있습니다. 특정 상황에서는 우리가 둘 중 어떤 의도로 썼는지 모호해지며, 컴파일러가 우리의 의도와 다르게 해석해서 프로그램에 예상치 못한 오류를 일으킬 수 있습니다.</li>
<li>매크로 <code>NULL</code>은 C++ 언어 표준에 그 형태가 딱 하나로 정해져 있지 않습니다. 컴파일러에 따라 <code>0</code>, <code>0L</code>, <code>((void*)0)</code> 등 완전히 다른 형태로 정의될 수 있습니다.</li>
</ol>
<p>이전에 함수 오버로딩에 대해 배웠던 것을 떠올려보세요. 함수에 전달되는 인자의 타입에 따라 컴파일러는 여러 오버로딩된 함수 중 알맞은 것을 찾아냅니다. 하지만 <code>0</code>이나 <code>NULL</code>을 쓰면 여기서 문제가 발생할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cstddef&gt; // NULL을 사용하기 위해 포함

void print(int x) // 이 함수는 정수를 받습니다
{
    std::cout &lt;&lt; &quot;print(int): &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

void print(int* ptr) // 이 함수는 정수형 포인터를 받습니다
{
    std::cout &lt;&lt; &quot;print(int*): &quot; &lt;&lt; (ptr ? &quot;non-null\n&quot; : &quot;null\n&quot;);
}

int main()
{
    int x{ 5 };
    int* ptr{ &amp;x };

    print(ptr);  // ptr의 타입이 int*이므로 항상 print(int*)를 호출합니다 (좋음)
    print(0);    // 0은 정수 리터럴이므로 항상 print(int)를 호출합니다 (우리가 의도한 바이기를 바랍니다)

    print(NULL); // 이 구문은 컴파일러에 따라 다음 중 하나를 수행할 수 있습니다:
    // print(int) 호출 (Visual Studio의 경우)
    // print(int*) 호출
    // 모호한 함수 호출로 인한 컴파일 에러 발생 (GCC와 Clang의 경우)

    print(nullptr); // 항상 print(int*)를 호출합니다

    return 0;
}
</code></pre>
<p>저자의 컴퓨터(Visual Studio 사용)에서는 다음과 같이 출력됩니다:</p>
<blockquote>
<p>print(int<em>): non-null
print(int): 0
print(int): 0
print(int</em>): null</p>
</blockquote>
<p>숫자 <code>0</code>을 넘겨주면, 컴파일러는 <code>print(int*)</code>보다 <code>print(int)</code>를 우선적으로 선택합니다. 
만약 우리가 널 포인터를 의도하고 <code>print(int*)</code>가 호출되기를 바랐다면 전혀 엉뚱한 결과가 나오게 됩니다.</p>
<p><code>NULL</code>의 경우에도 그것이 <code>0</code>으로 정의되어 있다면 <code>print(int)</code>가 호출됩니다. 
다른 형태로 정의되어 있다면 에러가 나거나 의도치 않은 동작을 할 수 있습니다.</p>
<p>하지만 <strong><code>nullptr</code></strong>을 사용하면 이러한 모호함이 완전히 사라집니다. 
<code>nullptr</code>은 오직 포인터 타입하고만 짝이 맞기 때문에 항상 안전하게 <code>print(int*)</code>를 호출합니다.</p>
<hr>
<h3 id="stdnullptr_t-선택-사항">std::nullptr_t (선택 사항)</h3>
<p><code>nullptr</code>이 오버로딩에서 일반 정수와 구별될 수 있다는 것은, 자신만의 고유한 데이터 타입을 가지고 있다는 뜻입니다. </p>
<p>그렇다면 <code>nullptr</code>의 타입은 무엇일까요?</p>
<p>정답은 <code>&lt;cstddef&gt;</code> 헤더에 정의된 <strong><code>std::nullptr_t</code></strong> 입니다. <code>std::nullptr_t</code>는 오직 단 하나의 값, 바로 <code>nullptr</code>만 가질 수 있습니다! 
조금 우스꽝스러워 보일 수 있지만, 오직 <code>nullptr</code>만 인자로 받아들이는 함수를 만들고 싶을 때 유용하게 쓰일 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cstddef&gt; // std::nullptr_t를 사용하기 위해 포함

void print(std::nullptr_t)
{
    std::cout &lt;&lt; &quot;in print(std::nullptr_t)\n&quot;;
}

void print(int*)
{
    std::cout &lt;&lt; &quot;in print(int*)\n&quot;;
}

int main()
{
    print(nullptr); // print(std::nullptr_t) 호출

    int x { 5 };
    int* ptr { &amp;x };

    print(ptr); // print(int*) 호출

    ptr = nullptr;
    print(ptr); // print(int*) 호출 (ptr의 타입이 int* 이므로)

    return 0;
}
</code></pre>
<p>위 예제에서 <code>print(nullptr)</code>은 변환 과정이 필요 없기 때문에 <code>print(int*)</code> 대신 <code>print(std::nullptr_t)</code>를 호출합니다.</p>
<p>조금 헷갈릴 수 있는 부분은 <code>ptr</code>이 <code>nullptr</code> 값을 가지고 있을 때 <code>print(ptr)</code>을 호출하는 경우입니다. 
<strong>함수 오버로딩은 &#39;값&#39;이 아니라 &#39;타입&#39;을 기준으로 매칭된다</strong>는 것을 기억하세요. <code>ptr</code>의 타입은 여전히 <code>int*</code>이기 때문에, 비록 비어있더라도 <code>print(int*)</code>가 호출됩니다. 포인터 타입은 <code>std::nullptr_t</code>로 암시적 변환이 되지 않으므로 <code>print(std::nullptr_t)</code>는 아예 고려 대상조차 되지 않습니다.</p>
<p>이 내용을 직접 쓸 일은 거의 없겠지만, 만약을 위해 알아두면 좋습니다.</p>
<hr>
<h3 id="결국-값에-의한-전달만-존재합니다">결국 &#39;값에 의한 전달&#39;만 존재합니다</h3>
<p>이제 참조로 전달하기, 주소로 전달하기, 값으로 전달하기의 기본적인 차이를 이해하셨을 겁니다. 
그럼 잠시 모든 것을 아주 단순하게(환원주의적으로) 생각해보겠습니다. :)</p>
<p>컴파일러가 참조(reference)를 완전히 최적화해서 없애버리는 경우도 많지만, 참조가 반드시 실재해야 하는 경우도 있습니다. 
보통 컴파일러 내부에서 &#39;참조&#39;는 &#39;포인터&#39;를 사용하여 구현됩니다. 
즉, 보이지 않는 곳에서 <strong>참조로 전달하기는 사실상 주소로 전달하기와 똑같이 동작</strong>합니다.</p>
<p>그리고 이전 레슨에서, 주소로 전달하기는 단순히 주소를 호출자에서 함수로 &#39;복사&#39;하는 것이라고 배웠습니다. 
이것은 곧 <strong>주소를 값으로 전달하는 것</strong>에 불과합니다.</p>
<p>결론적으로, <strong>C++은 사실상 모든 것을 값으로 전달(Pass by value)한다</strong>고 볼 수 있습니다! 
주소나 참조로 전달했을 때 원본이 바뀌는 특성은, 단지 우리가 넘겨받은 그 &#39;주소 값&#39;을 역참조(dereference)해서 원본에 접근할 수 있기 때문에 생겨나는 결과일 뿐입니다. 일반적인 값을 전달받았을 때는 불가능한 일이죠!</p>
<hr>
<h2 id="1212--참조로-반환하기와-주소로-반환하기-return-by-reference-and-return-by-address">12.12 — 참조로 반환하기와 주소로 반환하기 (Return by reference and return by address)</h2>
<p>이전 강의에서 우리는 함수에 데이터를 넘겨줄 때 &#39;값으로 전달(pass by value)&#39;을 하면 데이터의 복사본이 만들어진다고 배웠습니다. 숫자나 문자 같은 기본 타입은 복사하는 데 걸리는 시간과 메모리가 적어서 괜찮습니다. 하지만 <code>std::string</code>과 같은 클래스 타입은 복사 비용이 꽤 비쌉니다. 그래서 우리는 비싼 복사 과정을 피하기 위해 대신 &#39;(const) 참조로 전달&#39;하거나 &#39;주소로 전달&#39;하는 방법을 사용합니다.</p>
<p>이와 똑같은 상황이 함수에서 값을 반환(return)할 때도 발생합니다. &#39;값으로 반환(return by value)&#39;을 하면 반환값의 복사본이 함수를 호출한 쪽으로 전달됩니다. 만약 반환하는 타입이 클래스라면, 이 복사 과정 역시 비용이 큽니다.</p>
<pre><code class="language-cpp">std::string returnByValue(); // std::string의 복사본을 반환합니다 (비용이 큼)
</code></pre>
<hr>
<h3 id="참조로-반환하기-return-by-reference">참조로 반환하기 (Return by reference)</h3>
<p>클래스 타입을 호출자에게 돌려줄 때, 우리는 복사본을 만드는 대신 &#39;참조로 반환&#39;할 수 있습니다. 
<strong>참조로 반환하기</strong>는 반환할 원본 객체에 연결된 &#39;참조(reference)&#39;를 반환하므로, 쓸데없는 복사본을 만들지 않습니다. 
방법은 아주 간단합니다. 함수의 반환 타입을 참조형(<code>&amp;</code>)으로 정의하기만 하면 됩니다.</p>
<pre><code class="language-cpp">std::string&amp;       returnByReference(); // 기존 std::string에 대한 참조를 반환합니다 (비용이 적음)
const std::string&amp; returnByReferenceToConst(); // 기존 std::string에 대한 const 참조를 반환합니다 (비용이 적음)
</code></pre>
<p>다음은 참조 반환이 어떻게 작동하는지 보여주는 간단한 예제 프로그램입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

const std::string&amp; getProgramName() // const 참조를 반환합니다
{
    static const std::string s_programName { &quot;Calculator&quot; }; // 정적(static) 지속 기간을 가짐, 프로그램 종료 시 소멸됨

    return s_programName;
}

int main()
{
    std::cout &lt;&lt; &quot;This program is named &quot; &lt;&lt; getProgramName();

    return 0;
}
</code></pre>
<p>이 프로그램은 다음과 같이 출력합니다:</p>
<blockquote>
<p>This program is named Calculator</p>
</blockquote>
<p><code>getProgramName()</code> 함수가 반환 타입을 <code>const 참조(&amp;)</code>로 지정했기 때문에, <code>return s_programName</code>이 실행될 때 복사본을 만들지 않고 원본 <code>s_programName</code>에 대한 참조를 그대로 반환합니다. 호출한 <code>main</code> 함수에서는 이 참조를 사용해 원래 값에 접근하고 화면에 출력할 수 있습니다.</p>
<blockquote>
<p><strong>주의</strong>
참조로 반환되는 객체는 함수가 끝난 후에도 살아있어야 합니다.</p>
</blockquote>
<p>참조로 반환할 때 반드시 명심해야 할 가장 중요한 규칙이 있습니다. 
<strong>참조가 가리키는 대상(객체)이 함수가 끝난 후에도 파괴되지 않고 살아있어야 한다는 것</strong>입니다. 
그렇지 않으면 반환된 참조는 이미 파괴되어 사라진 메모리를 가리키게 되며(이를 댕글링 참조, Dangling reference라고 부릅니다), 이 참조를 사용하려고 하면 프로그램이 엉뚱하게 작동하거나 튕겨버립니다(미정의 동작).</p>
<blockquote>
</blockquote>
<p>앞선 예제에서는 <code>s_programName</code> 변수 앞에 <code>static(정적)</code> 키워드가 붙어 있었기 때문에, 프로그램이 완전히 끝날 때까지 변수가 파괴되지 않아 안전했습니다. 이제, 댕글링 참조를 반환하면 어떤 큰일이 벌어지는지 아래 코드로 확인해 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
&gt;
const std::string&amp; getProgramName()
{
    const std::string programName { &quot;Calculator&quot; }; // 이제 정적(static)이 아닌 지역 변수입니다. 함수가 끝나면 소멸됩니다.
&gt;
    return programName;
}
&gt;
int main()
{
    std::cout &lt;&lt; &quot;This program is named &quot; &lt;&lt; getProgramName(); // 미정의 동작 (undefined behavior) 발생
&gt;
    return 0;
}
&gt;</code></pre>
<blockquote>
</blockquote>
<p>이 프로그램의 결과는 알 수 없습니다(미정의 동작). 
<code>getProgramName()</code> 함수가 끝날 때, 그 안에 있던 지역 변수 <code>programName</code>은 메모리에서 자동으로 파괴됩니다. 
즉, 함수는 &#39;이미 파괴된 변수&#39;를 가리키는 끊어진 참조를 반환하게 되고, <code>main()</code> 함수에서 이를 출력하려고 하면 문제가 터지는 것입니다.</p>
<blockquote>
</blockquote>
<p>요즘 컴파일러들은 이런 실수를 하면 경고나 에러를 띄워주지만, 코드가 복잡해지면 컴파일러도 알아채지 못할 수 있습니다.</p>
<blockquote>
<blockquote>
<p><strong>핵심 경고</strong>
참조로 반환할 객체는 반드시 함수보다 수명이 길어야 합니다. <strong>절대 일반 지역 변수나 임시 객체를 참조로 반환하지 마세요.</strong></p>
</blockquote>
</blockquote>
<hr>
<h3 id="임시-객체의-수명-연장-규칙은-함수-밖으로-이어지지-않습니다">임시 객체의 수명 연장 규칙은 함수 밖으로 이어지지 않습니다</h3>
<p>임시 값(잠깐 만들어지고 사라지는 값)을 참조로 반환하는 예제를 살펴보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

const int&amp; returnByConstReference()
{
    return 5; // 임시 객체에 대한 const 참조를 반환합니다
}

int main()
{
    const int&amp; ref { returnByConstReference() };

    std::cout &lt;&lt; ref; // 미정의 동작 발생

    return 0;
}
</code></pre>
<p>위 코드에서 함수는 숫자 <code>5</code>를 반환하는데, 반환 타입이 <code>const int&amp;</code>입니다. 
이렇게 되면 숫자 5를 담은 &#39;임시 객체&#39;가 만들어지고 그 참조가 반환됩니다. 
하지만 함수가 끝나는 즉시 이 임시 객체는 파괴됩니다. 
<code>main()</code> 함수에서 <code>ref</code>에 이 참조를 연결하려고 할 때는 이미 객체가 죽어버린 후라 늦었습니다. 
결국 <code>ref</code>는 댕글링 참조가 됩니다.</p>
<p>아래는 조금 덜 명백하지만 똑같이 작동하지 않는 예제입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

const int&amp; returnByConstReference(const int&amp; ref)
{
    return ref;
}

int main()
{
    // 경우 1: 직접 바인딩
    const int&amp; ref1 { 5 }; // 수명 연장됨
    std::cout &lt;&lt; ref1 &lt;&lt; &#39;\n&#39;; // 정상 작동

    // 경우 2: 간접 바인딩
    const int&amp; ref2 { returnByConstReference(5) }; // 댕글링 참조에 바인딩됨
    std::cout &lt;&lt; ref2 &lt;&lt; &#39;\n&#39;; // 미정의 동작 발생

    return 0;
}
</code></pre>
<p>&#39;경우 2&#39;에서는 값 5가 임시 객체로 만들어져 함수로 전달됩니다. 함수는 그 참조를 그대로 반환하지만, 참조가 함수라는 &#39;경계&#39;를 한 번 거쳤기 때문에 C++의 수명 연장 규칙이 적용되지 않습니다. 결과적으로 임시 객체는 파괴되고 <code>ref2</code>는 위험한 상태가 됩니다.</p>
<blockquote>
<p><strong>핵심 경고</strong>
참조의 수명 연장 규칙은 함수 경계를 넘어서서 작동하지 않습니다.</p>
</blockquote>
<hr>
<h3 id="const가-아닌-정적-지역-변수를-참조로-반환하지-마세요">const가 아닌 정적 지역 변수를 참조로 반환하지 마세요</h3>
<p>처음 예제에서는 설명을 쉽게 하기 위해 <code>const static</code> 지역 변수를 참조로 반환했습니다. 
하지만 const가 아닌 일반 <code>static</code> 지역 변수를 참조로 반환하는 것은 일반적으로 피해야 합니다. 아래 코드가 그 이유를 보여줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

const int&amp; getNextId()
{
    static int s_x{ 0 }; // 주의: const 변수가 아닙니다
    ++s_x; // 다음 ID 생성
    return s_x; // 그리고 그것에 대한 참조를 반환합니다
}

int main()
{
    const int&amp; id1 { getNextId() }; // id1은 참조입니다
    const int&amp; id2 { getNextId() }; // id2는 참조입니다

    std::cout &lt;&lt; id1 &lt;&lt; id2 &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 프로그램은 <code>12</code>가 아니라 <code>22</code>를 출력합니다!
그 이유는 <code>id1</code>과 <code>id2</code>가 결국 완전히 똑같은 원본 데이터(<code>s_x</code>)를 바라보고 있기 때문입니다. 
두 번째 호출에서 <code>s_x</code>의 값이 2로 바뀌어 버리니, <code>id1</code>을 출력할 때도 바뀐 값인 2가 나오는 것입니다.</p>
<p>이 문제를 해결하려면 <code>id1</code>과 <code>id2</code>를 참조(<code>&amp;</code>)가 아닌 일반 변수로 만들어서, 참조 대신 <strong>값의 복사본</strong>을 저장하게 해야 합니다.</p>
<blockquote>
<p><strong>고급 독자를 위한 내용</strong>
문자열 정렬이나 상태 초기화의 어려움 등으로 인해 const가 아닌 정적 변수 참조 반환은 예기치 못한 버그를 만들기 쉽습니다. 
특별한 이유가 없다면 무조건 피하는 것이 좋습니다.</p>
</blockquote>
<hr>
<h3 id="반환된-참조를-일반-변수에-저장하면-복사가-일어납니다">반환된 참조를 일반 변수에 저장하면 복사가 일어납니다</h3>
<p>함수가 참조를 반환하더라도, 그것을 &#39;참조가 아닌 일반 변수&#39;에 대입하거나 초기화하면 값이 복사됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

const int&amp; getNextId()
{
    static int s_x{ 0 };
    ++s_x;
    return s_x;
}

int main()
{
    const int id1 { getNextId() }; // 이제 id1은 일반 변수이며 getNextId()에서 참조로 반환된 값의 복사본을 받습니다
    const int id2 { getNextId() }; // 이제 id2는 일반 변수이며 getNextId()에서 참조로 반환된 값의 복사본을 받습니다

    std::cout &lt;&lt; id1 &lt;&lt; id2 &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>여기서는 <code>id1</code>과 <code>id2</code>가 일반 변수이기 때문에 반환된 값을 자기만의 공간에 복사합니다. 따라서 출력은 예상대로 <code>12</code>가 됩니다.</p>
<p>물론, 함수가 처음부터 댕글링 참조(끊어진 참조)를 반환했다면, 복사를 시도하기도 전에 에러(미정의 동작)가 나버리니 주의해야 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

const std::string&amp; getProgramName() // const 참조를 반환할 것입니다
{
    const std::string programName{ &quot;Calculator&quot; };

    return programName;
}

int main()
{
    std::string name { getProgramName() }; // 댕글링 참조의 복사본을 만듭니다
    std::cout &lt;&lt; &quot;This program is named &quot; &lt;&lt; name &lt;&lt; &#39;\n&#39;; // 미정의 동작 발생

    return 0;
}
</code></pre>
<hr>
<h3 id="외부에서-전달받은-참조를-그대로-다시-반환하는-것은-안전합니다">외부에서 전달받은 참조를 그대로 다시 반환하는 것은 안전합니다</h3>
<p>참조로 반환하는 것이 아주 유용하고 안전한 경우가 있습니다. 바로 <strong>함수의 인자로 들어온 참조를 그대로 다시 반환할 때</strong>입니다.
이게 안전한 이유는 논리적으로 당연합니다. 함수에 참조로 데이터를 넘겨줬다는 것은, 이미 함수를 호출한 쪽(바깥쪽)에 그 원본 데이터가 안전하게 존재한다는 뜻이기 때문입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

// 두 개의 std::string 객체를 받아 알파벳 순으로 먼저 오는 것을 반환합니다
const std::string&amp; firstAlphabetical(const std::string&amp; a, const std::string&amp; b)
{
    return (a &lt; b) ? a : b; // 어떤 것이 알파벳 순으로 먼저인지 확인하기 위해 std::string에 operator&lt;를 사용할 수 있습니다
}

int main()
{
    std::string hello { &quot;Hello&quot; };
    std::string world { &quot;World&quot; };

    std::cout &lt;&lt; firstAlphabetical(hello, world) &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 코드는 복사본을 하나도 만들지 않습니다! 
값으로 주고받았다면 문자열 복사가 최대 3번 일어났겠지만, 참조로 주고받음으로써 속도와 메모리 모두 이득을 보았습니다.</p>
<hr>
<h3 id="참조를-통해-원본-값을-수정할-수도-있습니다">참조를 통해 원본 값을 수정할 수도 있습니다</h3>
<p>함수에 참조(<code>const</code>가 아닌 일반 참조)로 데이터를 넘기면 함수 안에서 원본을 수정할 수 있듯이, 
함수가 참조를 반환하면 <strong>호출한 쪽에서 반환된 원본을 수정할 수도 있습니다.</strong></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 두 정수를 non-const 참조로 받아 더 큰 값을 참조로 반환합니다
int&amp; max(int&amp; x, int&amp; y)
{
    return (x &gt; y) ? x : y;
}

int main()
{
    int a{ 5 };
    int b{ 6 };

    max(a, b) = 7; // a와 b 중 더 큰 값을 7로 설정합니다

    std::cout &lt;&lt; a &lt;&lt; b &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p><code>max(a, b)</code>는 더 큰 값인 <code>b</code>의 참조를 반환합니다. 거기에 <code>= 7</code>을 했으니 사실상 <code>b = 7</code>을 한 것과 똑같습니다. 그래서 결과는 <code>57</code>이 출력됩니다.</p>
<hr>
<h3 id="주소로-반환하기-return-by-address">주소로 반환하기 (Return by address)</h3>
<p>&#39;주소로 반환하기&#39;는 참조 대신 포인터(메모리 주소)를 반환한다는 점만 빼면 참조로 반환하는 것과 거의 똑같습니다. 
함수가 끝난 후에도 객체가 살아있어야 한다는 주의사항도 동일합니다(그렇지 않으면 댕글링 포인터가 됩니다).</p>
<ul>
<li><strong>장점</strong>: 반환할 만한 유효한 객체가 없을 때 <code>nullptr</code>(빈 포인터)을 반환하여 &quot;결과 없음&quot;을 확실하게 표현할 수 있습니다. (예: 학생 목록에서 특정 학생을 찾는데, 못 찾으면 <code>nullptr</code> 반환)</li>
<li><strong>단점</strong>: 이 함수를 사용하는 사람은 반환값이 <code>nullptr</code>인지 항상 확인해야 합니다. 깜빡하고 빈 포인터를 그냥 사용해버리면 프로그램이 즉시 튕겨버립니다.</li>
</ul>
<blockquote>
<p><strong>모범 사례</strong>
&quot;결과가 없음(<code>nullptr</code>)&quot;을 표현해야 하는 특별한 상황이 아니라면, 안전성을 위해 항상 포인터보다는 &#39;참조로 반환&#39;하는 것을 선호하세요. (참고로 &quot;결과 없음&quot;을 더 안전하게 표현하려면 나중에 배울 <code>std::optional</code>을 사용하는 것이 가장 좋습니다.)</p>
</blockquote>
<hr>
<h2 id="1213--입력in-및-출력out-매개변수">12.13 — 입력(In) 및 출력(Out) 매개변수</h2>
<p>함수와 그 함수를 호출하는 쪽(호출자)은 &#39;매개변수&#39;와 &#39;반환값&#39;을 통해 서로 데이터를 주고받습니다. 
함수가 호출될 때 호출자는 인수를 넘겨주고, 함수는 매개변수를 통해 이를 받습니다. 
이러한 인수는 값(value), 참조(reference), 또는 주소(address)의 형태로 전달될 수 있습니다.</p>
<p>보통은 값을 통째로 넘기거나 원본을 보호하는 <code>const</code> 참조 방식을 많이 사용하지만, 때로는 다른 방식이 필요할 때가 있습니다</p>
<hr>
<h3 id="입력in-매개변수">입력(In) 매개변수</h3>
<p>대부분의 경우, 함수 매개변수는 호출자로부터 값을 입력받기 위한 용도로만 쓰입니다. 
이렇게 입력만 받는 매개변수를 <strong>입력 매개변수(in parameter)</strong>라고 부릅니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int x) // x는 입력 매개변수입니다
{
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

void print(const std::string&amp; s) // s는 입력 매개변수입니다
{
    std::cout &lt;&lt; s &lt;&lt; &#39;\n&#39;;
}

int main()
{
    print(5);
    std::string s { &quot;Hello, world!&quot; };
    print(s);

    return 0;
}
</code></pre>
<p>입력 매개변수는 보통 &#39;값(pass by value)&#39;이나 &#39;<code>const</code> 참조(pass by const reference)&#39; 형태로 전달됩니다.</p>
<hr>
<h3 id="출력out-매개변수">출력(Out) 매개변수</h3>
<p>상수가 아닌 참조(<code>non-const reference</code>)나 포인터를 사용해 인수를 전달하면, 함수 안에서 넘겨받은 원본 객체의 값을 직접 수정할 수 있습니다. 이는 단순히 반환값(<code>return</code>)을 사용하는 것만으로는 정보 전달이 충분하지 않을 때, 함수가 호출자에게 데이터를 돌려주는 훌륭한 방법이 됩니다.</p>
<p>이렇게 오로지 호출자에게 정보를 돌려주기 위한 목적으로 쓰이는 매개변수를 <strong>출력 매개변수(out parameter)</strong>라고 부릅니다.</p>
<p>예를 들어보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;cmath&gt;    // std::sin() 및 std::cos() 함수 사용을 위해
#include &lt;iostream&gt;

// sinOut과 cosOut은 출력 매개변수입니다
void getSinCos(double degrees, double&amp; sinOut, double&amp; cosOut)
{
    // sin()과 cos()은 각도(degree)가 아닌 라디안(radian)을 사용하므로 변환이 필요합니다
    constexpr double pi { 3.14159265358979323846 }; // 파이(pi) 값
    double radians = degrees * pi / 180.0;
    sinOut = std::sin(radians);
    cosOut = std::cos(radians);
}

int main()
{
    double sin { 0.0 };
    double cos { 0.0 };

    double degrees{};
    std::cout &lt;&lt; &quot;Enter the number of degrees: &quot;;
    std::cin &gt;&gt; degrees;

    // getSinCos 함수는 변수 sin과 cos에 결과값을 담아 반환합니다
    getSinCos(degrees, sin, cos);

    std::cout &lt;&lt; &quot;The sin is &quot; &lt;&lt; sin &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;The cos is &quot; &lt;&lt; cos &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이 함수는 하나의 입력 매개변수 <code>degrees</code>(값으로 전달됨)를 받고, 두 개의 출력 매개변수를 (참조를 통해) &quot;반환&quot;합니다.</p>
<p>여기서는 출력용이라는 것을 명확히 표시하기 위해 매개변수 이름 끝에 &quot;Out&quot;이라는 단어를 붙였습니다. 
이렇게 하면 처음에 변수에 어떤 값이 들어있는지는 전혀 중요하지 않고, 함수 안에서 새로운 값으로 덮어씌워질 것임을 쉽게 알 수 있습니다. 관례상 출력 매개변수는 주로 매개변수 목록의 가장 오른쪽에 둡니다.</p>
<p>어떻게 작동하는지 더 자세히 알아볼까요?</p>
<p>먼저 <code>main</code> 함수에서 <code>sin</code>과 <code>cos</code>라는 지역 변수를 만듭니다. 
이 변수들은 값의 복사본이 아닌 &#39;<strong>참조&#39; 방식으로</strong> <code>getSinCos()</code> 함수에 전달됩니다. 
즉, <code>getSinCos()</code> 함수는 복사본이 아니라 <code>main()</code>에 있는 실제 <code>sin</code>과 <code>cos</code> 변수에 직접 접근할 수 있다는 뜻입니다.</p>
<p>따라서 <code>getSinCos()</code>가 (참조 매개변수인 <code>sinOut</code>과 <code>cosOut</code>을 통해) 새로운 값을 할당하면, 원래의 <code>sin</code>과 <code>cos</code> 변수 값이 새로운 값으로 덮어씌워집니다. 그런 다음 <code>main()</code> 함수에서 이 업데이트된 값들을 출력하게 되는 것이죠.</p>
<p>만약 <code>sin</code>과 <code>cos</code>를 참조가 아니라 &#39;값&#39;으로 넘겼다면, <code>getSinCos()</code>는 원본이 아닌 복사본만 만지작거리게 되어 함수가 끝나는 순간 변경된 내용이 모두 사라졌을 것입니다. 하지만 참조로 넘겼기 때문에 변경 사항이 함수 밖에서도 그대로 유지되는 것입니다. 우리는 이 원리를 이용해 여러 개의 값을 호출자에게 돌려줄 수 있습니다.</p>
<hr>
<h3 id="출력-매개변수의-부자연스러운-사용법">출력 매개변수의 부자연스러운 사용법</h3>
<p>출력 매개변수는 유용하지만 몇 가지 단점이 있습니다.</p>
<ol>
<li>호출자는 당장 사용하지 않을 객체라도 미리 만들고(초기화하고) 인수로 넘겨주어야 합니다. 
이 객체들은 나중에 함수에서 값을 덮어써야 하므로 <code>const</code>로 만들 수도 없습니다.</li>
<li>반드시 객체의 형태로 넘겨주어야 하므로 임시로 값을 넘기거나 하나의 간단한 수식 안에서 쉽게 사용할 수 없습니다.</li>
</ol>
<p>다음 예제는 이 두 가지 단점을 잘 보여줍니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int getByValue()
{
    return 5;
}

void getByReference(int&amp; x)
{
    x = 5;
}

int main()
{
    // 값으로 반환
    [[maybe_unused]] int x{ getByValue() }; // 객체를 초기화하는 데 사용할 수 있음
    std::cout &lt;&lt; getByValue() &lt;&lt; &#39;\n&#39;;      // 수식 내에서 임시 반환값을 직접 사용할 수 있음

    // 출력 매개변수로 반환
    int y{};                // 값을 할당받을 객체를 먼저 만들어야 함
    getByReference(y);      // 그런 다음 함수에 전달하여 원하는 값을 할당받음
    std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;; // 그 후에야 해당 값을 사용할 수 있음

    return 0;
}
</code></pre>
<p>보시다시피, 출력 매개변수를 사용하는 문법은 조금 번거롭고 부자연스럽습니다.</p>
<hr>
<h3 id="값이-변경된다는-것을-한눈에-파악하기-어렵습니다">값이 변경된다는 것을 한눈에 파악하기 어렵습니다</h3>
<p>함수의 반환값을 변수에 대입할 때는 해당 변수의 값이 바뀔 거라는 게 매우 분명합니다.
<code>x = getByValue(); // x가 수정된다는 것이 매우 명확함</code></p>
<p>이는 코드를 읽는 사람에게 <code>x</code>의 값이 변할 것이라는 점을 직관적으로 알려주므로 좋은 방식입니다.
하지만 아까 보았던 <code>getSinCos()</code> 함수의 호출 형태를 다시 살펴볼까요?</p>
<p><code>getSinCos(degrees, sin, cos);</code></p>
<p>이 줄만 봐서는 <code>degrees</code>가 입력용이고 <code>sin</code>과 <code>cos</code>가 출력용이라는 것을 알아채기가 어렵습니다. 
프로그래머가 <code>sin</code>과 <code>cos</code> 값이 바뀐다는 사실을 인지하지 못하면 심각한 논리적 오류가 발생할 수 있습니다.</p>
<p>이럴 때 참조 대신 &#39;주소(포인터)&#39;로 값을 전달하면, 인수를 넘길 때 변수 앞에 주소 연산자(<code>&amp;</code>)를 붙여야 하므로 값이 바뀔 수 있다는 점을 조금 더 명확하게 알릴 수 있습니다.</p>
<p>다음 예제를 확인해 보세요.</p>
<pre><code class="language-cpp">void foo1(int x);  // 값으로 전달
void foo2(int&amp; x); // 참조로 전달
void foo3(int* x); // 주소로 전달

int main()
{
    int i{};

    foo1(i);  // i를 수정할 수 없음
    foo2(i);  // i를 수정할 수 있음 (명확하지 않음)
    foo3(&amp;i); // i를 수정할 수 있음 (기호 &amp; 덕분에 조금 더 명확함)

    int *ptr { &amp;i };
    foo3(ptr); // i를 수정할 수 있음 (포인터를 전달하므로 명확하지 않을 수 있음)

    return 0;
}
</code></pre>
<p><code>foo3(&amp;i)</code>를 호출할 때 <code>i</code> 대신 <code>&amp;i</code>를 넘기는 것을 주목하세요. 이 기호 덕분에 <code>i</code> 값이 수정될 수 있겠구나 하고 예상하기가 더 쉬워집니다.</p>
<p>하지만 이 방식도 완벽하지는 않습니다. 
<code>foo3(ptr)</code>처럼 이미 만들어진 포인터를 넘길 때는 <code>&amp;</code>를 붙일 필요가 없어서, 여전히 값이 변한다는 것을 눈치채기 어려울 수 있습니다.
게다가 포인터를 사용하면 빈 포인터(<code>nullptr</code>)가 전달될 가능성도 생깁니다. 따라서 함수 내부에서 이 값이 비어있는지 확인하고 처리하는 코드를 추가해야 하므로 로직이 더 복잡해집니다. 포인터 검사 로직을 추가하느니 차라리 그냥 참조(reference) 방식을 유지하는 것이 문제가 적을 때가 많습니다.</p>
<p>이러한 여러 가지 이유 때문에, <strong>다른 좋은 대안이 없는 경우를 제외하고는 출력 매개변수 사용은 피하는 것이 좋습니다.</strong></p>
<blockquote>
<p><strong>권장 사항 (Best practice)</strong></p>
<ul>
<li>다른 대안이 없는 드문 경우를 제외하고는 출력 매개변수 사용을 피하세요.</li>
<li>꼭 출력 매개변수를 써야 한다면 포인터 방식보다는 참조(reference) 방식을 선호하는 것이 좋습니다.</li>
</ul>
</blockquote>
<hr>
<h3 id="입출력inout-매개변수">입출력(In/out) 매개변수</h3>
<p>아주 드물게, 함수가 출력 매개변수의 기존 값을 먼저 읽고 나서 새 값으로 덮어쓰는 경우가 있습니다. 
이렇게 입력과 출력 역할을 동시에 하는 매개변수를 <strong>입출력 매개변수(in-out parameter)</strong>라고 합니다. 
입출력 매개변수 역시 출력 매개변수와 동일하게 작동하며, 앞서 설명한 단점들도 모두 똑같이 가지고 있습니다.</p>
<hr>
<h3 id="상수가-아닌-참조non-const-reference로-전달해야-할-때">상수가 아닌 참조(<code>non-const reference</code>)로 전달해야 할 때</h3>
<p>데이터를 복사하는 비용을 아끼기 위해 참조로 넘길 때는, (내용이 변경되지 않도록) 거의 항상 <code>const</code> 참조를 사용해야 합니다.</p>
<blockquote>
<p><strong>저자의 메모</strong>
이어지는 예제들에서는 우리가 관심 있는 특정 데이터 타입을 <code>Foo</code>라고 부르겠습니다. 
지금은 <code>Foo</code>를 여러분이 잘 아는 타입(예를 들어 <code>std::string</code>)이라고 상상하시면 됩니다.</p>
</blockquote>
<p>하지만 상수가 아닌 일반 참조를 사용하는 것이 더 나은 두 가지 대표적인 상황이 있습니다.</p>
<p><strong>첫째, 매개변수가 입출력(in-out) 매개변수인 경우입니다.</strong>
우리가 무언가를 수정해서 다시 돌려받기 위해 원본 객체를 함수에 넘기는 것이라면, 그냥 그 원본 객체를 직접 수정하는 것이 코드가 단순해지고 성능상으로도 유리합니다.</p>
<pre><code class="language-cpp">void someFcn(Foo&amp; inout)
{
    // inout 객체를 수정함
}

int main()
{
    Foo foo{};
    someFcn(foo); // 함수 호출 후 foo가 수정됨, 한눈에 파악하기 어려울 수 있음

    return 0;
}
</code></pre>
<p>함수 이름을 직관적으로 잘 지어주면 이런 혼란을 줄일 수 있습니다.</p>
<pre><code class="language-cpp">void modifyFoo(Foo&amp; inout)
{
    // inout 객체를 수정함
}

int main()
{
    Foo foo{};
    modifyFoo(foo); // 함수 호출 후 foo가 수정됨, 조금 더 명확해짐

    return 0;
}
</code></pre>
<p>이 방식 대신 평소처럼 객체를 <code>const</code> 참조로 넘기고, 함수 내부에서 새로운 객체를 만들어 값으로 반환한 뒤 이를 원본 변수에 덮어씌우는 방법도 있습니다.</p>
<pre><code class="language-cpp">Foo someFcn(const Foo&amp; in)
{
    Foo foo { in }; // 여기서 복사가 발생함
    // foo를 수정함
    return foo;
}

int main()
{
    Foo foo{};
    foo = someFcn(foo); // foo가 수정된다는 것이 명확하지만, 여기서 복사가 한 번 더 발생함

    return 0;
}
</code></pre>
<p>이 방식은 코드를 읽기가 편하고 전통적인 반환 문법을 따른다는 장점이 있지만, 눈에 보이지 않는 복사가 두 번이나 더 일어난다는 단점이 있습니다 (물론 똑똑한 컴파일러라면 이 중 하나 정도는 최적화로 없애주기도 합니다).</p>
<p><strong>둘째, 함수가 객체를 값으로 반환해야 하지만, 그 객체를 복사하는 비용이 &#39;매우&#39; 큰 경우입니다.</strong>
특히 프로그램 성능이 아주 중요한 부분에서 해당 함수가 엄청나게 많이 호출된다면, 복사 비용을 줄이기 위해 이 방법을 쓸 수 있습니다.</p>
<pre><code class="language-cpp">void generateExpensiveFoo(Foo&amp; out)
{
    // out 객체를 수정함
}

int main()
{
    Foo foo{};
    generateExpensiveFoo(foo); // 함수 호출 후 foo가 수정됨

    return 0;
}
</code></pre>
<blockquote>
<p><strong>심화 학습</strong>
위 상황에 해당하는 가장 흔한 예시는, 함수가 크기가 아주 거대한 배열(C스타일 배열이나 <code>std::array</code>)에 데이터를 채워 넣어야 하는데 배열 요소 복사 비용이 심하게 비싼 경우입니다. 배열에 대해서는 나중 챕터에서 다루겠습니다.
하지만 굳이 비정상적인 반환 방법까지 동원해야 할 정도로 객체의 복사 비용이 무거운 상황은 현실에서 매우 드물다는 점을 알아두세요.</p>
</blockquote>
<hr>
<h2 id="1214--포인터-참조reference-const를-활용한-타입-추론">12.14 — 포인터, 참조(reference), const를 활용한 타입 추론</h2>
<p>이전에 &#39;10.8강 - auto 키워드를 사용한 객체의 타입 추론&#39;에서, <code>auto</code> 키워드를 쓰면 컴파일러가 초기값을 보고 변수의 타입을 스스로 알아내게(추론하게) 할 수 있다고 배웠습니다.</p>
<pre><code class="language-cpp">int main(){
    int a { 5 };
    auto b { a }; // b의 타입은 int로 추론됨

    return 0;
}
</code></pre>
<p>또한, 기본적으로 타입 추론 과정에서 <code>const</code>는 <strong>제거된다</strong>는 점도 살펴보았습니다.</p>
<pre><code class="language-cpp">int main(){
    const double a { 7.8 }; // a의 타입은 const double
    auto b { a };           // b의 타입은 double (const가 제거됨)

    constexpr double c { 7.8 }; // c의 타입은 const double (constexpr은 암시적으로 const를 적용함)
    auto d { c };               // d의 타입은 double (const가 제거됨)

    return 0;
}
</code></pre>
<p>만약 추론된 타입에 <code>const</code>나 <code>constexpr</code>을 다시 적용하고 싶다면, 변수를 선언할 때 직접 적어주면 됩니다.</p>
<pre><code class="language-cpp">int main(){
    double a { 7.8 };    // a의 타입은 double
    const auto b { a };  // b의 타입은 const double (const가 적용됨)

    constexpr double c { 7.8 }; // c의 타입은 const double (constexpr은 암시적으로 const를 적용함)
    const auto d { c };         // d의 타입은 const double (const가 제거되었다가, 다시 적용됨)
    constexpr auto e { c };     // e의 타입은 constexpr double (const가 제거되었다가, constexpr이 다시 적용됨)

    return 0;
}
</code></pre>
<hr>
<h3 id="타입-추론은-참조reference도-제거합니다">타입 추론은 참조(Reference)도 제거합니다</h3>
<p>타입 추론은 <code>const</code>를 제거할 뿐만 아니라, 참조(<code>&amp;</code>) 기호도 함께 제거합니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;

std::string&amp; getRef(); // 참조를 반환하는 임의의 함수

int main(){
    auto ref { getRef() }; // 타입은 std::string으로 추론됨 (std::string&amp; 가 아님)

    return 0;
}
</code></pre>
<p>위 예제에서 <code>ref</code> 변수는 타입 추론을 사용하고 있습니다. 
<code>getRef()</code> 함수가 <code>std::string&amp;</code>(참조형)을 반환함에도 불구하고, 참조 속성이 제거되어 <code>ref</code>의 타입은 단순한 <code>std::string</code>이 됩니다.
<code>const</code>의 경우와 마찬가지로, 추론된 타입이 참조형이 되기를 원한다면 변수를 선언할 때 참조(<code>&amp;</code>) 기호를 다시 붙여주면 됩니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;

std::string&amp; getRef(); // 참조를 반환하는 임의의 함수

int main(){
    auto ref1 { getRef() };  // std::string (참조가 제거됨)
    auto&amp; ref2 { getRef() }; // std::string&amp; (참조가 제거되었다가, 다시 적용됨)

    return 0;
}
</code></pre>
<hr>
<h3 id="최상위-const-top-level-const-vs-하위-const-low-level-const">최상위 const (Top-level const) vs 하위 const (Low-level const)</h3>
<p>C++에서 <code>const</code>는 적용되는 위치에 따라 두 가지로 나뉩니다.</p>
<table>
<thead>
<tr>
<th>개념</th>
<th>설명</th>
<th>예시</th>
</tr>
</thead>
<tbody><tr>
<td><strong>최상위 const</strong></td>
<td>변수(객체) <strong>자체</strong>를 상수로 만듭니다.</td>
<td><code>const int x;</code> (x 자체가 상수)</td>
</tr>
<tr>
<td><strong>하위 const</strong></td>
<td>변수가 참조하거나 가리키는 <strong>대상</strong>을 상수로 만듭니다.</td>
<td><code>const int&amp; ref;</code> (참조하는 대상이 상수)</td>
</tr>
</tbody></table>
<p>참고로, 참조형(<code>&amp;</code>)은 구조상 이미 그 자체로 변경할 수 없으므로, 최상위 const 문법이 따로 존재하지 않습니다.
(항상 암시적으로 최상위 const 취급을 받습니다) 반면, 포인터는 최상위, 하위, 또는 둘 다 가질 수 있습니다.</p>
<pre><code class="language-cpp">const int* const ptr; // 왼쪽의 const는 하위 const, 오른쪽의 const는 최상위 const입니다.
</code></pre>
<p>타입 추론이 <code>const</code>를 제거한다고 할 때, <strong>오직 &#39;최상위 const&#39;만 제거됩니다.</strong> 대상을 보호하는 &#39;하위 const&#39;는 유지됩니다.</p>
<hr>
<h3 id="타입-추론과-const-참조">타입 추론과 const 참조</h3>
<p>초기값이 const 참조형일 경우, 먼저 참조(<code>&amp;</code>)가 제거됩니다. 
그 결과로 남은 <code>const</code>는 이제 객체 자체를 수식하는 <strong>최상위 const</strong>가 되므로 함께 제거됩니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;

const std::string&amp; getConstRef(); // const 참조를 반환하는 임의의 함수

int main(){
    auto ref1{ getConstRef() }; // std::string (참조가 제거되고, 결과에서 최상위 const도 제거됨)

    return 0;
}
</code></pre>
<p>위 예제에서 함수는 <code>const std::string&amp;</code>를 반환합니다. 여기서 참조(<code>&amp;</code>)가 먼저 떨어져 나가면 <code>const std::string</code>만 남게 됩니다. 이때의 <code>const</code>는 변수 자체를 뜻하는 최상위 const가 되므로, 결국 이마저도 제거되어 최종 타입은 <code>std::string</code>이 됩니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong> 
참조(<code>&amp;</code>)를 제거하면 하위 const가 최상위 const로 바뀔 수 있습니다.</p>
</blockquote>
<p>원한다면 참조와 const를 다시 명시적으로 붙여줄 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;

const std::string&amp; getConstRef(); // const 참조를 반환하는 임의의 함수

int main(){
    auto ref1{ getConstRef() };        // std::string (참조와 최상위 const가 모두 제거됨)
    const auto ref2{ getConstRef() };  // const std::string (참조와 const가 제거된 후, const가 다시 적용됨)

    auto&amp; ref3{ getConstRef() };       // const std::string&amp; (참조가 제거되었다가 다시 적용됨, 하위 const는 유지됨)
    const auto&amp; ref4{ getConstRef() }; // const std::string&amp; (참조가 제거되었다가 다시 적용됨, 하위 const 유지됨)

    return 0;
}
</code></pre>
<p><code>ref3</code>의 경우가 흥미롭습니다. 원래라면 참조가 제거되어야 하지만, 우리가 <code>&amp;</code>를 다시 붙여주었으므로 참조형이 유지됩니다. 
따라서 타입은 <code>const std::string&amp;</code>로 남게 되고, 이때의 <code>const</code>는 하위 const이므로 제거되지 않습니다.</p>
<blockquote>
<p><strong>모범 사례 (Best Practice)</strong> 
const 참조가 필요하다면, 문법상 굳이 필요 없는 상황이더라도 명시적으로 <code>const</code>를 적어주는 것이 좋습니다. 
코드의 의도가 명확해지고 실수를 줄일 수 있습니다.</p>
</blockquote>
<hr>
<h3 id="constexpr-참조는-어떻게-되나요">constexpr 참조는 어떻게 되나요?</h3>
<p><code>constexpr</code>은 표현식의 타입 자체에 포함되는 개념이 아닙니다. 따라서 <code>auto</code>는 <code>constexpr</code>을 추론해 내지 못합니다.</p>
<pre><code class="language-cpp">#include &lt;string_view&gt;
#include &lt;iostream&gt;

constexpr std::string_view hello { &quot;Hello&quot; };   // 암시적으로 const가 적용됨

constexpr const std::string_view&amp; getConstRef() // 함수는 constexpr이며, const std::string_view&amp; 를 반환함
{
    return hello;
}

int main(){
    auto ref1{ getConstRef() };                  // std::string_view (참조와 최상위 const가 모두 제거됨)
    constexpr auto ref2{ getConstRef() };        // constexpr const std::string_view (참조와 최상위 const가 제거된 후, constexpr이 적용되어 암시적으로 const가 됨)

    auto&amp; ref3{ getConstRef() };                 // const std::string_view&amp; (참조가 다시 적용되었고, 하위 const는 유지됨)
    constexpr const auto&amp; ref4{ getConstRef() }; // constexpr const std::string_view&amp; (참조가 다시 적용되었고, 하위 const 유지, constexpr 적용됨)

    return 0;
}
</code></pre>
<hr>
<h3 id="타입-추론과-포인터">타입 추론과 포인터</h3>
<p>참조와는 달리, <strong>타입 추론은 포인터(<code>*</code>)를 제거하지 않습니다.</strong></p>
<pre><code class="language-cpp">#include &lt;string&gt;

std::string* getPtr(); // 포인터를 반환하는 임의의 함수

int main(){
    auto ptr1{ getPtr() }; // std::string*

    return 0;
}
</code></pre>
<p>타입이 포인터라는 것을 코드를 읽는 사람에게 명확히 알려주기 위해, <code>auto</code> 대신 <code>auto*</code>를 사용할 수도 있습니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;

std::string* getPtr(); // 포인터를 반환하는 임의의 함수

int main(){
    auto ptr1{ getPtr() };  // std::string*
    auto* ptr2{ getPtr() }; // std::string*

    return 0;
}
</code></pre>
<blockquote>
<p><strong>왜 참조는 제거되는데 포인터는 유지되나요?</strong>
참조를 사용할 때는 사실상 그 &#39;대상 객체&#39;를 다루는 것입니다. 따라서 참조형 자체보다는 대상의 타입을 추론하는 것이 논리적입니다. 
반면 포인터는 객체의 &#39;메모리 주소&#39;를 담고 있는 별개의 변수입니다. 
포인터를 평가할 때는 포인터 그 자체를 다루는 것이므로, 포인터 타입을 그대로 유지하는 것이 맞습니다.</p>
</blockquote>
<hr>
<h3 id="요약-정리">요약 정리</h3>
<p>복잡한 내용들 때문에 머리가 아프셨다면 위로의 말씀을 드립니다! 가장 중요한 핵심만 빠르게 다시 짚어보겠습니다.</p>
<ul>
<li><strong>최상위 vs 하위 const:</strong> 최상위 const는 변수 자체를 상수로 만들고, 하위 const는 변수가 가리키는 대상을 상수로 만듭니다.</li>
<li><strong>타입 추론 규칙:</strong></li>
<li>기본적으로 참조(<code>&amp;</code>) 기호를 제거합니다. 이 과정에서 하위 const가 최상위 const로 변할 수 있습니다.</li>
<li>그다음, 모든 최상위 const를 제거합니다.</li>
<li><code>constexpr</code>은 애초에 타입 추론의 대상이 아니므로, 원한다면 직접 적어주어야 합니다.</li>
<li>포인터(<code>*</code>)는 제거하지 않고 그대로 유지합니다.</li>
</ul>
<ul>
<li><strong>명확하게 코딩하기:</strong> 컴파일러가 알아서 추론해 주더라도, <code>const</code>, <code>&amp;</code>, <code>constexpr</code> 같은 키워드는 프로그래머의 의도를 명확히 하기 위해 직접 명시적으로 적어주는 습관을 들이는 것이 좋습니다.</li>
<li><strong>포인터를 다룰 때는 <code>auto*</code> 고려하기:</strong> 포인터 타입을 추론할 때는 <code>auto*</code>를 사용하는 것을 권장합니다. 초기값이 포인터가 아닐 경우 컴파일러가 에러를 내주어 실수를 방지할 수 있습니다.</li>
</ul>
<hr>
<h2 id="1215--stdoptional">12.15 — std::optional</h2>
<p>&#39;9.4 레슨 - 오류 감지 및 처리&#39;에서는 함수가 스스로 적절히 해결할 수 없는 오류를 만나는 경우에 대해 이야기했습니다. 
예를 들어, 값을 계산해서 반환하는 다음 함수를 살펴봅시다.</p>
<pre><code class="language-cpp">int doIntDivision(int x, int y)
{
    return x / y;
}
</code></pre>
<p>만약 이 함수를 호출하는 쪽에서 의미상 유효하지 않은 값(예: <code>y = 0</code>)을 전달하면, 이 함수는 반환할 값을 계산할 수 없습니다(수학적으로 0으로 나누는 것은 정의되어 있지 않기 때문입니다). 이럴 땐 어떻게 해야 할까요? 계산을 수행하는 함수는 다른 부작용(side effects)을 일으키면 안 되므로, 함수 스스로 이 오류를 해결하는 것은 적절하지 않습니다. 이런 경우에는 일반적으로 함수가 오류를 감지하게 한 다음, 함수를 호출한 쪽으로 오류를 돌려보내서 프로그램 흐름에 맞게 직접 처리하도록 합니다.</p>
<p>앞서 언급한 레슨에서는 함수가 호출자에게 오류를 반환하는 두 가지 방법을 배웠습니다.</p>
<ul>
<li>반환값이 없는(<code>void</code>) 함수가 성공 또는 실패를 나타내는 <code>bool</code> 값을 반환하도록 하기.</li>
<li>값을 반환하는 함수가 오류를 나타내기 위해 &#39;센티널 값(sentinel value, 함수가 정상적일 때 반환할 수 있는 값의 범위에 속하지 않는 특별한 값)&#39;을 반환하도록 하기.</li>
</ul>
<p>두 번째 방법의 예로, 사용자가 <code>x</code>에 유효하지 않은 인수를 전달하면 절대 나올 수 없는 값인 <code>0.0</code>을 반환하는 <code>reciprocal()</code>(역수 구하기) 함수를 살펴보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// x의 역수는 1/x 이며, x=0이면 0.0을 반환합니다.
double reciprocal(double x)
{
    if (x == 0.0) // x가 의미상 유효하지 않은 값이라면
       return 0.0; // 오류가 발생했음을 알리기 위해 센티널 값인 0.0을 반환합니다.

    return 1.0 / x;
}

void testReciprocal(double d)
{
     double result { reciprocal(d) };
     std::cout &lt;&lt; &quot;The reciprocal of &quot; &lt;&lt; d &lt;&lt; &quot; is &quot;;
     if (result != 0.0)
         std::cout &lt;&lt; result &lt;&lt; &#39;\n&#39;;
     else
         std::cout &lt;&lt; &quot;undefined\n&quot;;
}

int main()
{
    testReciprocal(5.0);
    testReciprocal(-4.0);
    testReciprocal(0.0);

    return 0;
}
</code></pre>
<p>이 방법도 꽤 괜찮아 보이지만, 몇 가지 단점이 있습니다.</p>
<ul>
<li>프로그래머는 이 함수가 오류를 나타내기 위해 도대체 어떤 &#39;센티널 값&#39;을 사용하고 있는지 미리 알아야 합니다 (그리고 이 값은 함수마다 다를 수 있습니다).</li>
<li>같은 함수라도 버전에 따라 다른 센티널 값을 사용할 수도 있습니다.</li>
<li>이 방법은 <strong>모든 값이 정상적인 반환값으로 쓰일 수 있는 함수</strong>에는 적용할 수 없습니다.</li>
</ul>
<p>위에서 본 <code>doIntDivision()</code> 함수를 생각해 봅시다. 사용자가 <code>y</code>에 <code>0</code>을 전달하면 어떤 값을 오류로 반환해야 할까요? <code>0</code>을 사용할 수는 없습니다. 왜냐하면 어떤 수를 0으로 나눈 결과가 아니라, <code>0</code>을 다른 수로 나누면 <code>0</code>이라는 정상적인 결과가 나올 수 있기 때문입니다. 사실 이 함수에서는 자연스럽게 나오지 않는 &#39;특별한&#39; 반환값이 아예 존재하지 않습니다.</p>
<p>그럼 우리는 어떻게 해야 할까요?</p>
<p><strong>첫째,</strong> 아주 드물게 나올 법한 반환값을 센티널 값으로 골라 오류를 나타내는 데 사용할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;limits&gt; // std::numeric_limits를 사용하기 위해 포함

// 실패 시 std::numeric_limits&lt;int&gt;::lowest()를 반환합니다.
int doIntDivision(int x, int y)
{
    if (y == 0)
        return std::numeric_limits&lt;int&gt;::lowest();
    return x / y;
}
</code></pre>
<p><code>std::numeric_limits&lt;T&gt;::lowest()</code>는 <code>T</code> 타입이 가질 수 있는 가장 작은(가장 큰 음수) 값을 반환하는 함수입니다. 이것은 &#39;9.5 레슨 - std::cin과 잘못된 입력 처리&#39;에서 소개했던 <code>std::numeric_limits&lt;T&gt;::max()</code> (가장 큰 양수 값을 반환)와 반대되는 개념입니다.
위의 예제에서 <code>doIntDivision()</code>이 계속 진행될 수 없다면, <code>std::numeric_limits&lt;int&gt;::lowest()</code>를 반환하여 호출자에게 함수가 실패했음을 알립니다.</p>
<p>이 방법은 대체로 잘 작동하지만 두 가지 단점이 있습니다.</p>
<ul>
<li>이 함수를 호출할 때마다, 반환값이 <code>std::numeric_limits&lt;int&gt;::lowest()</code>와 같은지 비교해서 실패 여부를 검사해야 합니다. 코드가 길어지고 보기 흉해집니다.</li>
<li>이것은 &#39;반(semi)서술어 문제&#39;의 한 예입니다. 만약 사용자가 <code>doIntDivision(std::numeric_limits&lt;int&gt;::lowest(), 1)</code>을 호출하면, 반환되는 결과값은 <code>std::numeric_limits&lt;int&gt;::lowest()</code>가 됩니다. 이때 이 반환값이 함수가 성공해서 나온 정상 값인지, 실패해서 나온 오류 값인지 헷갈리게 됩니다. 함수를 어떻게 사용하느냐에 따라 문제가 될 수도 있고 아닐 수도 있지만, 우리가 신경 써야 할 또 다른 골칫거리이자 프로그램에 오류가 발생할 수 있는 잠재적 위험입니다.</li>
</ul>
<p><strong>둘째,</strong> 반환값으로 오류를 알리는 것을 포기하고, 예외(exceptions)와 같은 다른 메커니즘을 사용할 수 있습니다. 하지만 예외 처리는 그 자체로 복잡하고 성능 비용이 발생하며, 상황에 따라 적절하지 않거나 원치 않을 수 있습니다. 이런 간단한 연산에 예외를 사용하는 것은 약간 과한 느낌(overkill)이 있습니다.</p>
<p><strong>셋째,</strong> 하나의 값만 반환하는 것을 포기하고 <strong>두 개의 값</strong>을 반환할 수 있습니다. 하나는 함수가 성공했는지를 나타내는 <code>bool</code> 타입 값이고, 다른 하나는 실제 반환값(성공 시)이거나 의미 없는 값(실패 시)을 담는 원래 원했던 타입의 값입니다. 이 방법이 여러 선택지 중 가장 좋은 방법일 것입니다.</p>
<p>C++17 이전에는 이 세 번째 방법을 선택하려면 프로그래머가 직접 코드를 짜서 구현해야 했습니다. C++에서 이를 구현할 수 있는 여러 가지 방법이 있긴 하지만, 직접 만들다 보면 필연적으로 일관성이 떨어지고 오류가 생기기 쉽습니다.</p>
<hr>
<h3 id="stdoptional-반환하기">std::optional 반환하기</h3>
<p>C++17에서는 &#39;선택적인(optional) 값&#39;을 구현하는 클래스 템플릿 타입인 <code>std::optional</code>을 도입했습니다. 
즉, <code>std::optional&lt;T&gt;</code>는 <code>T</code> 타입의 값을 &#39;가질 수도 있고&#39;, &#39;가지지 않을 수도&#39; 있습니다.</p>
<p>이를 사용하여 앞서 말한 세 번째 옵션을 깔끔하게 구현할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;optional&gt; // std::optional을 사용하기 위해 포함 (C++17)

// 이제 우리 함수는 선택적으로 int 값을 반환합니다.
std::optional&lt;int&gt; doIntDivision(int x, int y)
{
    if (y == 0)
        return {}; // 또는 std::nullopt를 반환합니다.
    return x / y;
}

int main()
{
    std::optional&lt;int&gt; result1 { doIntDivision(20, 5) };
    if (result1) // 함수가 값을 반환했다면
        std::cout &lt;&lt; &quot;Result 1: &quot; &lt;&lt; *result1 &lt;&lt; &#39;\n&#39;; // 값을 가져옵니다.
    else
        std::cout &lt;&lt; &quot;Result 1: failed\n&quot;;

    std::optional&lt;int&gt; result2 { doIntDivision(5, 0) };

    if (result2)
        std::cout &lt;&lt; &quot;Result 2: &quot; &lt;&lt; *result2 &lt;&lt; &#39;\n&#39;;
    else
        std::cout &lt;&lt; &quot;Result 2: failed\n&quot;;

    return 0;
}
</code></pre>
<p>이 코드의 출력 결과는 다음과 같습니다.</p>
<blockquote>
<p>Result 1: 4
Result 2: failed</p>
</blockquote>
<p><code>std::optional</code>을 사용하는 방법은 아주 쉽습니다. 값을 넣어서 생성할 수도 있고, 값 없이 생성할 수도 있습니다.</p>
<pre><code class="language-cpp">std::optional&lt;int&gt; o1 { 5 };            // 값으로 초기화
std::optional&lt;int&gt; o2 {};               // 값 없이 초기화
std::optional&lt;int&gt; o3 { std::nullopt }; // 값 없이 초기화
</code></pre>
<p><code>std::optional</code> 안에 값이 있는지 확인하려면 다음 방법 중 하나를 선택하면 됩니다.</p>
<pre><code class="language-cpp">if (o1.has_value()) // has_value()를 호출하여 o1에 값이 있는지 확인
if (o2)             // bool로의 암시적 변환을 사용하여 o2에 값이 있는지 확인
</code></pre>
<p><code>std::optional</code>에서 값을 가져오려면 다음 방법 중 하나를 선택하면 됩니다.</p>
<pre><code class="language-cpp">std::cout &lt;&lt; *o1;             // 역참조하여 o1에 저장된 값을 가져옵니다. (값이 없는데 시도하면 미정의 동작 발생)
std::cout &lt;&lt; o2.value();      // value()를 호출하여 o2에 저장된 값을 가져옵니다. (값이 없으면 std::bad_optional_access 예외 발생)
std::cout &lt;&lt; o3.value_or(42); // value_or()를 호출하여 o3에 저장된 값을 가져옵니다. (값이 없으면 기본값인 `42` 반환)
</code></pre>
<p><code>std::optional</code>의 사용 문법이 기본적으로 포인터와 거의 똑같다는 점에 주목하세요.</p>
<table>
<thead>
<tr>
<th>동작</th>
<th>포인터 (Pointer)</th>
<th>std::optional</th>
</tr>
</thead>
<tbody><tr>
<td><strong>값을 가지지 않음</strong></td>
<td><code>{}</code> 또는 <code>std::nullptr</code> 로 초기화/할당</td>
<td><code>{}</code> 또는 <code>std::nullopt</code> 로 초기화/할당</td>
</tr>
<tr>
<td><strong>값을 가짐</strong></td>
<td>주소로 초기화/할당</td>
<td>값으로 초기화/할당</td>
</tr>
<tr>
<td><strong>값이 있는지 확인</strong></td>
<td>bool로의 암시적 변환</td>
<td>bool로의 암시적 변환 또는 <code>has_value()</code></td>
</tr>
<tr>
<td><strong>값 가져오기</strong></td>
<td>역참조 (<code>*</code>)</td>
<td>역참조 (<code>*</code>) 또는 <code>value()</code></td>
</tr>
</tbody></table>
<p>하지만 의미상(semantically)으로 포인터와 <code>std::optional</code>은 완전히 다릅니다.</p>
<ul>
<li>포인터는 <strong>참조 의미론(reference semantics)</strong>을 갖습니다. 즉, 포인터는 다른 객체를 가리키며(참조하며), 할당을 하면 객체 자체가 아니라 &#39;포인터(주소)&#39;가 복사됩니다. 만약 우리가 주소를 통해 포인터를 반환하면, 가리키는 객체가 아니라 포인터가 호출자에게 복사됩니다. 이것은 지역(local) 객체를 주소로 반환하면 안 된다는 뜻입니다. 객체의 주소를 호출자에게 반환하고 나면, 함수가 끝나면서 그 지역 객체는 파괴되기 때문에 반환된 포인터는 허공을 가리키는 댕글링(dangling) 포인터가 됩니다.</li>
<li><code>std::optional</code>은 <strong>값 의미론(value semantics)</strong>을 갖습니다. 즉, 실제로 내부에 자신의 값을 가지고 있으며, 할당을 하면 &#39;값&#39;이 복사됩니다. 우리가 <code>std::optional</code>을 값으로 반환하면, <code>std::optional</code> 객체 자체(그 안에 들어있는 값 포함)가 호출자에게 안전하게 복사됩니다. 즉, <code>std::optional</code>을 사용하면 함수 안에서 만들어진 값을 호출자에게 문제없이 돌려줄 수 있습니다.</li>
</ul>
<p>이 점을 염두에 두고 앞의 예제가 어떻게 작동하는지 살펴봅시다. 이제 <code>doIntDivision()</code>은 <code>int</code> 대신 <code>std::optional&lt;int&gt;</code>를 반환합니다. 함수 내부에서 오류를 감지하면 <code>{}</code>를 반환하는데, 이는 아무 값도 담고 있지 않은 비어있는 <code>std::optional</code>을 암시적으로 반환하는 것입니다. 올바른 값이 있다면 그 값을 반환하며, 이는 해당 값을 쏙 담고 있는 <code>std::optional</code>을 암시적으로 반환합니다.</p>
<p><code>main()</code> 함수 안에서는 반환된 <code>std::optional</code>에 값이 있는지 확인하기 위해 <code>bool</code>로의 암시적 변환을 사용합니다. 값이 있다면 <code>std::optional</code> 객체를 역참조(<code>*</code>)하여 값을 꺼내 사용합니다. 값이 없다면 오류 처리 조건문을 실행합니다. 정말 간단하죠!</p>
<hr>
<h3 id="stdoptional-반환의-장단점">std::optional 반환의 장단점</h3>
<p><code>std::optional</code>을 반환하는 것은 여러모로 훌륭합니다.</p>
<ul>
<li><code>std::optional</code>을 사용하면, 이 함수가 값을 반환할 수도 있고 안 할 수도 있다는 사실을 코드 자체로 명확히 보여줍니다(문서화 효과).</li>
<li>오류를 나타내기 위해 어떤 센티널 값을 사용했는지 외울 필요가 없습니다.</li>
<li><code>std::optional</code>을 사용하는 문법이 편리하고 직관적입니다.</li>
</ul>
<p>하지만 몇 가지 단점도 존재합니다.</p>
<ul>
<li>값을 가져오기 전에 <code>std::optional</code> 안에 진짜로 값이 들어있는지 반드시 확인해야 합니다. 만약 값이 없는 <code>std::optional</code>을 역참조하면 알 수 없는 오류(미정의 동작)가 발생합니다.</li>
<li><code>std::optional</code>은 함수가 &#39;왜&#39; 실패했는지에 대한 상세한 이유를 전달할 방법은 제공하지 않습니다.</li>
</ul>
<p>여러분의 함수가 실패한 이유를 자세히 알려줘야 하는 상황(실패 원인을 더 잘 파악하거나 여러 종류의 실패를 구분하기 위해)이 아니라면, 무언가를 반환하거나 실패할 가능성이 있는 함수에는 <code>std::optional</code>이 최고의 선택입니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
실패할 가능성이 있는 함수를 만들 때는 센티널 값 대신 <code>std::optional</code>을 반환하세요. 단, 함수가 실패한 &#39;이유&#39;에 대한 추가적인 정보가 필요한 경우는 제외입니다.</p>
</blockquote>
<blockquote>
<p><strong>관련 내용</strong>
C++23에 도입된 <code>std::expected</code>는 함수가 정상적으로 기대하는 값이나 예상치 못한 오류 코드를 반환할 수 있는 상황을 처리하기 위해 만들어졌습니다. 자세한 정보는 <code>std::expected</code> 레퍼런스를 참고하세요.</p>
</blockquote>
<hr>
<h3 id="stdoptional을-선택적-함수-매개변수로-사용하기">std::optional을 선택적 함수 매개변수로 사용하기</h3>
<p>&#39;12.11 레슨 - 주소로 전달하기 파트 2&#39;에서는 함수가 &quot;선택적인(optional)&quot; 인수를 받도록 하기 위해 어떻게 &#39;주소로 전달(pass by address)&#39; 방식을 사용하는지 논의했습니다. (즉, 호출자가 인수를 생략하고 싶을 때 <code>nullptr</code>을 넘기거나, 아니면 실제 객체의 주소를 넘기는 방식입니다.) 하지만 이 방식의 단점 중 하나는, <code>nullptr</code>이 아닌 진짜 인수를 전달할 때는 그 인수가 반드시 메모리 상의 주소를 가진 값(lvalue)이어야 한다는 것입니다.</p>
<p>이름에서 이미 눈치채셨겠지만, <code>std::optional</code>은 함수가 선택적 인수를 받을 수 있는 또 다른 대안입니다 (단, 함수 안으로 값을 집어넣기만 하는 in-parameter 용도로 사용됩니다). 기존의 이런 방식 대신:</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printIDNumber(const int *id=nullptr)
{
    if (id)
        std::cout &lt;&lt; &quot;Your ID number is &quot; &lt;&lt; *id &lt;&lt; &quot;.\n&quot;;
    else
        std::cout &lt;&lt; &quot;Your ID number is not known.\n&quot;;
}

int main()
{
    printIDNumber(); // 아직 사용자의 ID를 모릅니다.

    int userid { 34 };
    printIDNumber(&amp;userid); // 이제 사용자의 ID를 알게 되었습니다.

    return 0;
}
</code></pre>
<p>아래와 같이 사용할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;optional&gt;

void printIDNumber(std::optional&lt;const int&gt; id = std::nullopt)
{
    if (id)
        std::cout &lt;&lt; &quot;Your ID number is &quot; &lt;&lt; *id &lt;&lt; &quot;.\n&quot;;
    else
        std::cout &lt;&lt; &quot;Your ID number is not known.\n&quot;;
}

int main()
{
    printIDNumber(); // 아직 사용자의 ID를 모릅니다.

    int userid { 34 };
    printIDNumber(userid); // 이제 사용자의 ID를 알게 되었습니다.

    printIDNumber(62); // 62와 같은 임시값(rvalue)도 바로 전달할 수 있습니다!

    return 0;
}
</code></pre>
<p>이 접근 방식에는 두 가지 장점이 있습니다.</p>
<ul>
<li>이 매개변수가 선택적으로 사용된다는 것을 코드 상에서 명확히 보여줍니다.</li>
<li>(<code>std::optional</code> 내부에서 복사본을 만들기 때문에) 변수에 담지 않은 임시값(rvalue)도 바로 전달할 수 있습니다.</li>
</ul>
<p>하지만, <code>std::optional</code>은 인수의 복사본을 만들기 때문에 타입 <code>T</code>가 <code>std::string</code>처럼 복사하는 데 비용이 많이 드는 무거운 타입일 경우 문제가 됩니다. 일반적인 함수 매개변수에서는 이를 피하기 위해 매개변수를 <code>const lvalue 참조(const T&amp;)</code>로 만들어서 복사가 일어나지 않게 했습니다. 아쉽게도 C++23 기준으로 <code>std::optional</code>은 내부적으로 참조(reference)를 지원하지 않습니다.</p>
<p>그러므로, <code>T</code>가 평소에 <strong>값으로 전달(pass by value)</strong> 해도 무리가 없는 가벼운 타입일 때만 <code>std::optional&lt;T&gt;</code>를 선택적 매개변수로 사용하는 것을 권장합니다. 복사 비용이 비싼 타입이라면 기존처럼 <code>const T*</code>(포인터)를 사용하세요.</p>
<hr>
<h3 id="고급-독자를-위한-내용">고급 독자를 위한 내용</h3>
<p><code>std::optional</code>이 참조를 직접 지원하지는 않지만, <code>std::reference_wrapper</code> (&#39;17.5 레슨&#39;에서 다룰 예정)를 사용하면 참조처럼 흉내 낼 수 있습니다. <code>std::string</code> 속성을 가진 <code>Employee</code> 구조체와 <code>std::reference_wrapper</code>를 사용했을 때 위의 프로그램이 어떻게 달라지는지 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;functional&gt;  // std::reference_wrapper를 사용하기 위해 포함
#include &lt;iostream&gt;
#include &lt;optional&gt;
#include &lt;string&gt;

struct Employee
{
    std::string name{}; // 복사 비용이 비쌉니다.
    int id;
};

void printEmployeeID(std::optional&lt;std::reference_wrapper&lt;Employee&gt;&gt; e=std::nullopt)
{
    if (e)
        std::cout &lt;&lt; &quot;Your ID number is &quot; &lt;&lt; e-&gt;get().id &lt;&lt; &quot;.\n&quot;;
    else
        std::cout &lt;&lt; &quot;Your ID number is not known.\n&quot;;
}

int main()
{
    printEmployeeID(); // 아직 Employee 정보를 모릅니다.

    Employee e { &quot;James&quot;, 34 };
    printEmployeeID(e); // 이제 Employee의 ID를 알게 되었습니다.

    return 0;
}
</code></pre>
<p>비교를 위해, 동일한 작업을 포인터로 구현한 버전을 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

struct Employee
{
    std::string name{}; // 복사 비용이 비쌉니다.
    int id;
};

void printEmployeeID(const Employee* e=nullptr)
{
    if (e)
        std::cout &lt;&lt; &quot;Your ID number is &quot; &lt;&lt; e-&gt;id &lt;&lt; &quot;.\n&quot;;
    else
        std::cout &lt;&lt; &quot;Your ID number is not known.\n&quot;;
}

int main()
{
    printEmployeeID(); // 아직 Employee 정보를 모릅니다.

    Employee e { &quot;James&quot;, 34 };
    printEmployeeID(&amp;e); // 이제 Employee의 ID를 알게 되었습니다.

    return 0;
}
</code></pre>
<p>이 두 프로그램은 사실상 거의 똑같이 작동합니다. 저희는 첫 번째 방식이 두 번째 방식보다 더 읽기 쉽거나 유지보수하기 좋다고 생각하지 않으며, 굳이 두 가지 새로운 복잡한 타입을 프로그램에 끌어들일 가치가 없다고 봅니다.</p>
<p>많은 경우에, <strong>함수 오버로딩(function overloading)</strong>이 훨씬 더 나은 해결책을 제공합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

struct Employee
{
    std::string name{}; // 복사 비용이 비쌉니다.
    int id;
};

void printEmployeeID()
{
    std::cout &lt;&lt; &quot;Your ID number is not known.\n&quot;;
}

void printEmployeeID(const Employee&amp; e)
{
    std::cout &lt;&lt; &quot;Your ID number is &quot; &lt;&lt; e.id &lt;&lt; &quot;.\n&quot;;
}

int main()
{
    printEmployeeID(); // 아직 Employee 정보를 모릅니다.

    Employee e { &quot;James&quot;, 34 };
    printEmployeeID(e); // 이제 Employee의 ID를 알게 되었습니다.

    printEmployeeID( { &quot;Dave&quot;, 62 } ); // 임시값(rvalues)도 전달할 수 있습니다!

    return 0;
}
</code></pre>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
선택적인 값을 반환해야 할 때는 <code>std::optional</code>을 우선적으로 사용하세요.
선택적인 함수 매개변수가 필요할 때는 (가능하다면) <strong>함수 오버로딩</strong>을 우선적으로 사용하세요. 오버로딩이 여의치 않고 <code>T</code>가 일반적으로 값으로 전달하기 가벼운 타입이라면 <code>std::optional&lt;T&gt;</code>를 사용하세요. <code>T</code>를 복사하는 비용이 비싸다면 <code>const T*</code>를 사용하는 것이 좋습니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - F]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-F</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-F</guid>
            <pubDate>Sat, 21 Feb 2026 09:12:18 GMT</pubDate>
            <description><![CDATA[<h2 id="f1--constexpr-함수-1부">F.1 — <code>Constexpr</code> 함수 (1부)</h2>
<p>상수 표현식에는 한 가지 불편한 점이 있습니다. 바로 <strong>일반 함수는 상수 표현식 안에서 호출할 수 없다는 것입니다.</strong></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main() {
    constexpr double radius { 3.0 };
    constexpr double pi { 3.1459265359 };
    constexpr double circumference { 2.0 * radius * pi };

    std::cout &lt;&lt; &quot;Our circle has circumference &quot; &lt;&lt; circumference &lt;&lt; &#39;\n&#39;;
}

이 코드를 실행하면 다음과 같은 결과가 나옵니다.
Our circle has circumference 18.8496</code></pre>
<p>코드를 깔끔하게 만들기 위해 이 계산 과정을 함수로 분리해 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

double calcCircumference(double radius)
{
    constexpr double pi { 3.14159265359 };
    return 2.0 * pi * radius;
}

int main()
{
    constexpr double circumference { calcCircumference(3.0) }; // 컴파일 에러 발생
    std::cout &lt;&lt; &quot;Our circle has circumference &quot; &lt;&lt; circumference &lt;&lt; &quot;\n&quot;;
    return 0;
}</code></pre>
<p>안타깝게도 <strong>이 코드는 컴파일되지 않습니다.</strong>
<code>constexpr</code> 변수인 <code>circumference</code>는 초기값으로 <strong>반드시 상수 표현식</strong>을 가져야 합니다.
하지만 우리가 만든 <code>calcCircumference()</code>는 일반 함수이기 때문에 상수 표현식으로 인정받지 못합니다.</p>
<p>C++에서는 <strong>오직 상수 표현식만 허용되는 상황</strong>들이 있습니다. 
이런 상황에서 함수를 사용하고 싶은데 일반 함수는 쓸 수 없다면 어떻게 해야 할까요?</p>
<hr>
<h3 id="constexpr-함수는-상수-표현식에서-사용할-수-있습니다"><code>Constexpr</code> 함수는 상수 표현식에서 사용할 수 있습니다</h3>
<p><code>constexpr</code> <strong>함수</strong>는 상수 표현식 안에서 호출할 수 있도록 특별히 허용된 함수입니다.
함수를 <code>constexpr</code> 함수로 만드는 방법은 아주 간단합니다. 
함수의 반환 타입 맨 앞에 <code>constexpr</code> 키워드만 적어주면 됩니다.</p>
<p>위에서 에러가 났던 예제를 <code>constexpr</code> 함수를 이용해 고쳐보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

constexpr double calcCircumference(double radius) // 이제 constexpr 함수입니다
{
    constexpr double pi { 3.14159265359 };
    return 2.0 * pi * radius;
}

int main()
{
    constexpr double circumference { calcCircumference(3.0) }; // 이제 컴파일됩니다

    std::cout &lt;&lt; &quot;Our circle has circumference &quot; &lt;&lt; circumference &lt;&lt; &quot;\n&quot;;

    return 0;
}</code></pre>
<p>이제 <code>calcCircumference()</code>가 <code>constexpr</code> 함수가 되었기 때문에, 
<code>circumference</code> 변수를 초기화하는 것과 같은 <strong>상수 표현식 안에서 당당하게 사용할 수 있습니다.</strong></p>
<hr>
<h3 id="constexpr-함수는-컴파일-타임에-계산될-수-있습니다"><code>Constexpr</code> 함수는 컴파일 타임에 계산될 수 있습니다</h3>
<p>5.5강에서 배운 내용을 떠올려 봅시다. <code>constexpr</code> 변수를 초기화할 때처럼 반드시 상수 표현식이 필요한 곳에서는, <strong>그 값이 컴파일 타임에 미리 계산되어야 합니다.</strong> 만약 그 안에 <code>constexpr</code> 함수가 있다면, 그 함수 역시 컴파일 타임에 계산되어야만 합니다.</p>
<p>우리가 작성한 예제에서 <code>circumference</code> 변수는 <code>constexpr</code>이므로 컴파일 타임에 값이 필요합니다. 
따라서 컴파일러는 <code>calcCircumference(3.0)</code>을 프로그램 실행 전에(컴파일 타임에) 미리 계산합니다.</p>
<p>함수 호출 결과를 알아낸 컴파일러는 <strong>코드의 함수 호출 부분을 그 결과값으로 완전히 바꿔치기</strong> 합니다. 
즉, <code>calcCircumference(3.0)</code>이라는 코드가 <code>18.8496</code>이라는 결과값으로 바뀌게 됩니다. 
사실상 컴파일러는 아래와 같은 코드를 컴파일하게 되는 셈입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

constexpr double calcCircumference(double radius)
{
    constexpr double pi { 3.14159265359 };
    return 2.0 * pi * radius;
}

int main()
{
    constexpr double circumference { 18.8496 };

    std::cout &lt;&lt; &quot;Our circle has circumference &quot; &lt;&lt; circumference &lt;&lt; &quot;\n&quot;;

    return 0;
}
</code></pre>
<p>함수가 컴파일 타임에 계산되려면 <strong>다음 두 가지 조건</strong>도 반드시 충족해야 합니다.</p>
<ul>
<li><strong>함수에 넘겨주는 인자 값을 컴파일 타임에 미리 알 수 있어야 합니다.</strong> (예: 인자가 상수 표현식이어야 함)</li>
<li><strong><code>constexpr</code> 함수 내부의 모든 코드와 수식들이 컴파일 타임에 계산 가능한 것이어야 합니다.</strong>
(만약 <code>constexpr</code> 함수 안에서 다른 함수를 부른다면, 그 함수 역시 컴파일 타임에 계산할 수 있는 함수여야만 합니다.)</li>
</ul>
<hr>
<h3 id="constexpr-함수는-런타임프로그램-실행-중에도-실행될-수-있습니다">Constexpr 함수는 런타임(프로그램 실행 중)에도 실행될 수 있습니다</h3>
<p><code>constexpr</code> 함수라고 해서 무조건 컴파일 타임에만 써야 하는 것은 아닙니다. 
런타임에도 얼마든지 사용할 수 있으며, 이때는 <strong><code>constexpr</code>이 아닌 일반 결과값을 반환</strong>합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

constexpr int greater(int x, int y)
{
    return (x &gt; y ? x : y);
}

int main()
{
    int x{ 5 }; // constexpr 아님
    int y{ 6 }; // constexpr 아님

    std::cout &lt;&lt; greater(x, y) &lt;&lt; &quot; is greater!\n&quot;; // 런타임에 평가됩니다

    return 0;
}
</code></pre>
<p>이 예제에서 변수 <code>x</code>와 <code>y</code>는 상수가 아니기 때문에, 함수에 전달되는 값을 컴파일 타임에는 알 수 없습니다. 
따라서 이 함수는 컴파일 타임에 계산되지 못합니다. <strong>하지만 문제없습니다.</strong> 
프로그램이 실행될 때(런타임) 정상적으로 함수가 호출되어, 우리가 기대하는 일반적인 <code>int</code> 값을 무사히 반환합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
<code>constexpr</code> 함수가 런타임에 실행될 때는 <strong>일반 함수와 완전히 똑같이 동작합니다.</strong> 
즉, 런타임에는 <code>constexpr</code>이라는 키워드가 아무런 영향도 주지 않습니다.</p>
</blockquote>
<blockquote>
<p>** 핵심 포인트**
C++에서 <code>constexpr</code> 함수가 컴파일 타임과 런타임 양쪽에서 모두 실행될 수 있도록 허용한 이유는, 
<strong>단 하나의 함수로 두 가지 상황을 모두 처리하기 위해서</strong>입니다.
만약 이게 불가능했다면, 똑같은 역할을 하는 함수를 두 개(컴파일 타임용 함수 하나, 런타임용 일반 함수 하나)나 만들어야 했을 겁니다. 이는 코드를 중복되게 만들 뿐만 아니라, 두 함수의 이름마저 서로 다르게 지어야 하는 번거로움을 초래했을 것입니다!</p>
</blockquote>
<hr>
<h2 id="f2--constexpr-함수-2부">F.2 — <code>Constexpr</code> 함수 (2부)</h2>
<h3 id="상수가-필수가-아닌-문맥에서의-constexpr-함수-호출">상수가 필수가 아닌 문맥에서의 Constexpr 함수 호출</h3>
<p><code>constexpr</code> 함수는 언제나 컴파일 타임에 실행될 거라고 생각하기 쉽지만, 아쉽게도 항상 그런 것은 아닙니다.
레슨 5.5 상수 표현식에서 다루었듯이, <strong>상수 표현식이 반드시 필요하지 않은 상황</strong>에서는 컴파일러가 해당 코드를 컴파일 타임에 처리할지, 
아니면 프로그램이 실행되는 <strong>런타임에 처리할지</strong> 자유롭게 선택할 수 있습니다. </p>
<p>따라서 이런 곳에 쓰인 <code>constexpr</code> 함수는 *<em>컴파일 타임과 런타임 중 언제든 평가될 수 있습니다.
*</em></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

constexpr int getValue(int x)
{
    return x;
}

int main()
{
    int x { getValue(5) }; // 런타임 또는 컴파일 타임에 평가될 수 있음

    return 0;
}
</code></pre>
<p>위 예제에서 <code>getValue()</code>는 <code>constexpr</code> 함수이므로 <code>getValue(5)</code>라는 호출 자체는 상수 표현식입니다. 하지만 변수 <code>x</code>는 <code>constexpr</code>로 선언되지 않았기 때문에 굳이 상수로 초기화할 필요가 없습니다. 즉, 우리가 상수 표현식을 제공했음에도 불구하고 컴파일러는 <code>getValue(5)</code>를 컴파일 타임에 미리 계산할지, 아니면 프로그램 실행 중에 계산할지 알아서 결정합니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
<code>constexpr</code> 함수가 무조건 컴파일 타임에 평가된다고 보장할 수 있는 유일한 경우는 <strong>상수 표현식이 &#39;반드시 필요한&#39; 상황</strong>에 쓰였을 때뿐입니다.</p>
</blockquote>
<hr>
<h3 id="필수-상수-표현식에서의-constexpr-함수-진단-오류-확인">필수 상수 표현식에서의 Constexpr 함수 진단 (오류 확인)</h3>
<p>컴파일러는 어떤 <code>constexpr</code> 함수가 실제로 컴파일 타임에 평가되기 전까지는, 이 함수가 컴파일 타임에 제대로 작동하는지 미리 검사할 의무가 없습니다. 즉, 런타임 용도로는 아무 문제 없이 컴파일되지만 막상 컴파일 타임에 쓰려고 하면 오류를 뿜어내는 <code>constexpr</code> 함수를 만들기란 아주 쉽습니다. 다소 억지스럽지만 이해를 돕기 위한 예제를 볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int getValue(int x)
{
    return x;
}

// 이 함수는 런타임에 평가될 수 있습니다.
// 컴파일 타임에 평가될 경우 컴파일 오류가 발생합니다.
// 왜냐하면 getValue(x) 호출을 컴파일 타임에 해결할 수 없기 때문입니다.
constexpr int foo(int x)
{
    if (x &lt; 0) return 0; // C++23의 P2448R1 채택 이전에 필요했던 코드 (아래 참고 확인)
    return getValue(x);  // 여기서 constexpr이 아닌 함수를 호출함
}

int main()
{
    int x { foo(5) };           // 정상: 런타임에 평가됨
    constexpr int y { foo(5) }; // 컴파일 오류: foo(5)는 컴파일 타임에 평가될 수 없음

    return 0;
}
</code></pre>
<p>위 코드에서 <code>foo(5)</code>가 일반 변수 <code>x</code>를 초기화하는 데 쓰였을 때는 런타임에 실행되며 정상적으로 <code>5</code>를 반환합니다.</p>
<p>하지만 <code>foo(5)</code>가 <code>constexpr</code> 변수인 <code>y</code>를 초기화하는 데 쓰이면, 컴파일러는 이를 무조건 컴파일 타임에 계산해야 합니다. 
이때 컴파일러는 함수 내부의 <strong><code>getValue()</code>가 <code>constexpr</code> 함수가 아님을 발견</strong>하고, 결국 <code>foo(5)</code>를 컴파일 타임에 계산할 수 없다고 판단해 오류를 발생시킵니다.</p>
<p>그러므로 <code>constexpr</code> 함수를 만들 때는 <strong>반드시 상수가 필요한 상황(예: constexpr 변수 초기화)에서 호출해 보아, 컴파일 타임에도 정상적으로 작동하는지 명시적으로 테스트</strong>해야 합니다.</p>
<blockquote>
<p><strong>모범 사례</strong>
모든 <code>constexpr</code> 함수는 컴파일 타임에 실행될 수 있도록 작성해야 합니다. 언젠가는 상수 표현식이 필요한 곳에 쓰이게 될 테니까요.
런타임에는 잘 작동하던 코드가 컴파일 타임에는 실패할 수 있으므로, 항상 상수가 필수인 문맥에서 함수를 테스트하는 습관을 들이세요.</p>
</blockquote>
<blockquote>
<p><strong>고급 독자를 위한 참고</strong>
C++23 이전에는 <code>constexpr</code> 함수를 컴파일 타임에 실행 가능하게 만드는 인자 조건이 단 하나도 없다면 프로그램 자체가 &#39;잘못된 형식&#39;으로 간주되었습니다. 위 예제에서 <code>if (x &lt; 0) return 0;</code> 코드가 없다면 컴파일 타임 실행이 아예 불가능하므로 문제가 됩니다. 하지만 오류 메시지 출력이 필수가 아니었기에 컴파일러가 조용히 넘어갈 수도 있었습니다. 이 까다로운 규칙은 C++23(P2448R1)에서 폐지되었습니다.</p>
</blockquote>
<hr>
<h3 id="constexpr--consteval-함수의-매개변수는-constexpr이-아닙니다">Constexpr / Consteval 함수의 매개변수는 Constexpr이 아닙니다</h3>
<p><code>constexpr</code> 함수가 받는 매개변수는 자동으로 <code>constexpr</code>이 되지 않으며, 코드에 <code>constexpr</code>이라고 직접 적을 수도 없습니다.</p>
<blockquote>
<p><strong>핵심 포인트</strong>
만약 함수의 매개변수가 <code>constexpr</code>이라면, 그 함수는 오직 상수 인자로만 호출될 수 있다는 뜻이 될 것입니다. 
하지만 <strong><code>constexpr</code> 함수는 런타임에 실행될 때 일반적인 변수를 인자로 받아 작동할 수도 있어야 하므로</strong> 매개변수가 상수로 고정되지 않습니다.</p>
</blockquote>
<p>매개변수가 <code>constexpr</code>이 아니기 때문에, 함수 내부에서 상수가 꼭 필요한 곳에 이 매개변수를 써서는 안 됩니다.</p>
<pre><code class="language-cpp">consteval int goo(int c)    // c는 constexpr이 아니며, 상수 표현식에 사용할 수 없음
{
    return c;
}

constexpr int foo(int b)    // b는 constexpr이 아니며, 상수 표현식에 사용할 수 없음
{
    constexpr int b2 { b }; // 컴파일 오류: constexpr 변수는 상수 표현식 초기화 값이 필요함

    return goo(b);          // 컴파일 오류: consteval 함수 호출에는 상수 표현식 인자가 필요함
}

int main()
{
    constexpr int a { 5 };

    std::cout &lt;&lt; foo(a); // 정상: 상수 표현식 a는 constexpr 함수 foo()의 인자로 사용할 수 있음

    return 0;
}
</code></pre>
<p>위에서 인자 <code>a</code>는 상수지만, 정작 함수로 전달된 매개변수 <code>b</code>는 <code>constexpr</code>이 아닙니다. 
따라서 <code>b</code>를 상수가 필요한 곳(예: 변수 <code>b2</code> 초기화나 <code>goo(b)</code> 호출)에 쓰면 컴파일 오류가 발생합니다.</p>
<p>(참고: <code>constexpr</code> 함수의 매개변수를 <code>const</code>로 선언할 수는 있으며, 이 경우에는 &#39;런타임 상수&#39;로 취급됩니다.)</p>
<p><em>관련 내용: 상수로만 이루어진 매개변수가 꼭 필요하다면 &#39;11.9 - 타입이 아닌 템플릿 매개변수(Non-type template parameters)&#39; 문서를 참고하세요.</em></p>
<hr>
<h3 id="constexpr-함수는-암시적으로-인라인inline-처리됩니다">Constexpr 함수는 암시적으로 인라인(Inline) 처리됩니다</h3>
<p>컴파일러가 <code>constexpr</code> 함수를 컴파일 타임에 계산하려면, 함수를 호출하기 전에 <strong>해당 함수의 전체 내용(정의)을 모두 알고 있어야</strong> 합니다. 
단순히 &quot;이런 함수가 뒤에 나올 거야&quot;라고 알려주는 전방 선언만으로는 턱없이 부족합니다.</p>
<p>이 말은 즉, 여러 파일에서 호출되는 <code>constexpr</code> 함수는 사용하는 모든 번역 단위(.cpp 파일)마다 그 내용이 전부 복사되어 들어가야 한다는 뜻입니다. 원래 C++에서는 똑같은 함수가 여러 번 정의되면 &#39;단일 정의 원칙(ODR)&#39; 위반으로 오류가 나지만, 이 문제를 피하기 위해 
<strong><code>constexpr</code> 함수는 자동으로 &#39;인라인(inline)&#39; 처리되어 ODR 규칙의 예외를 적용받습니다.</strong></p>
<p>결과적으로 <code>constexpr</code> 함수는 어디서든 쉽게 불러올 수 있도록 <strong>보통 헤더 파일(.h)에 정의</strong>됩니다.</p>
<blockquote>
<p><strong>규칙</strong>
<strong>컴파일러</strong>는 <code>constexpr</code> (또는 <code>consteval</code>) 함수의 단순한 전방 선언이 아닌 전체 코드 내용을 볼 수 있어야 합니다.</p>
</blockquote>
<blockquote>
<p><strong>모범 사례</strong></p>
<ul>
<li>단일 소스 파일(.cpp)에서만 쓰는 <code>constexpr/consteval</code> 함수는 해당 파일 내에서 사용되기 전에 미리 작성하세요.</li>
<li>여러 소스 파일에서 공통으로 쓴다면, 어디서든 <code>#include</code> 할 수 있도록 헤더 파일에 작성하세요.</li>
</ul>
</blockquote>
<p>단, 오직 런타임에만 실행될 <code>constexpr</code> 함수라면 전방 선언만으로도 충분합니다. 
즉 컴파일 타임에 계산할 필요가 없는 상황이라면, 다른 파일에 정의된 <code>constexpr</code> 함수를 전방 선언만으로 불러와 쓸 수 있습니다.</p>
<blockquote>
<p><strong>고급 독자를 위한 참고</strong>
C++ 표준(CWG2166)에 따르면, 컴파일 타임에 실행되는 <code>constexpr</code> 함수의 전방 선언에 대한 정확한 규칙은 
&quot;해당 함수가 실제로 쓰이기 전 가장 바깥쪽 호출 단계 이전에만 정의되어 있으면 된다&quot;는 것입니다. 
따라서 서로가 서로를 호출하는 상호 재귀 함수를 만들기 위해 다음과 같은 코드 작성이 허용됩니다.</p>
<pre><code class="language-cpp">constexpr int foo(int);
</code></pre>
</blockquote>
<p>constexpr int goo(int c)
{
    return foo(c);   // note that foo is not defined yet
}</p>
<blockquote>
</blockquote>
<p>constexpr int foo(int b) // okay because foo is still defined before any calls to goo
{
    return b;
}</p>
<blockquote>
</blockquote>
<p>int main()
{
     constexpr int a{ goo(5) }; // this is the outermost invocation</p>
<blockquote>
</blockquote>
<pre><code>return 0;</code></pre><p>}</p>
<hr>
<h3 id="요약">요약</h3>
<ul>
<li>함수를 <code>constexpr</code>로 표시하는 것은 <strong>&quot;이 함수를 상수 표현식에 쓸 수 있다&quot;</strong>는 뜻이지, <strong>&quot;무조건 컴파일 타임에 계산된다&quot;</strong>는 뜻이 아닙니다.</li>
<li>상수 표현식은 상수가 <strong>반드시 필요한 문맥</strong>에서만 컴파일 타임 계산이 강제됩니다.</li>
<li>상수가 굳이 필요 없는 곳이라면, 컴파일러가 알아서 컴파일 타임에 할지 런타임에 할지 효율적인 쪽으로 선택합니다.</li>
<li>비상수(일반) 표현식은 언제나 런타임에 계산됩니다.</li>
</ul>
<hr>
<h3 id="추가-예제-살펴보기">추가 예제 살펴보기</h3>
<p>상황에 따라 <code>constexpr</code> 함수가 언제 평가될 확률이 높은지 다음 예제를 통해 감을 잡아봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

constexpr int greater(int x, int y)
{
    return (x &gt; y ? x : y);
}

int main()
{
    constexpr int g { greater(5, 6) };              // 사례 1: 항상 컴파일 타임에 평가됨
    std::cout &lt;&lt; g &lt;&lt; &quot; is greater!\n&quot;;

    std::cout &lt;&lt; greater(5, 6) &lt;&lt; &quot; is greater!\n&quot;; // 사례 2: 런타임 또는 컴파일 타임에 평가될 수 있음

    int x{ 5 }; // constexpr은 아니지만 컴파일 타임에 값을 알 수 있음
    std::cout &lt;&lt; greater(x, 6) &lt;&lt; &quot; is greater!\n&quot;; // 사례 3: 높은 확률로 런타임에 평가됨

    std::cin &gt;&gt; x;
    std::cout &lt;&lt; greater(x, 6) &lt;&lt; &quot; is greater!\n&quot;; // 사례 4: 항상 런타임에 평가됨

    return 0;
}
</code></pre>
<ul>
<li><strong>사례 1:</strong> 상수가 무조건 필요한 곳(constexpr 변수 초기화)에 쓰였으므로, <code>greater()</code>는 <strong>무조건 컴파일 타임</strong>에 계산됩니다.</li>
<li><strong>사례 2:</strong> 화면 출력 구문은 런타임에 실행되므로 상수가 굳이 필요하지 않은 곳입니다. 
하지만 전달한 인자(5, 6)가 모두 상수이므로 컴파일 타임에 계산될 &#39;자격&#39;은 갖췄습니다. 컴파일러가 자유롭게 시점을 선택합니다.</li>
<li><strong>사례 3:</strong> 일반 변수 <code>x</code>가 인자로 들어갔으므로 대개 <strong>런타임</strong>에 실행됩니다. 하지만 <code>x</code>의 값이 5라는 걸 컴파일러가 뻔히 알기 때문에, 이른바 <strong>&#39;as-if 규칙(마치 ~인 것처럼 규칙)&#39;</strong>에 따라 컴파일러 재량으로 몰래 컴파일 타임에 계산해버릴 수도 있습니다. (이 규칙 하에서는 일반 비상수 함수도 컴파일 타임에 계산될 수 있습니다!)</li>
<li><strong>사례 4:</strong> 사용자가 키보드로 어떤 값을 입력할지 컴파일 타임에는 절대 알 수 없으므로, 이 호출은 <strong>무조건 런타임</strong>에 실행됩니다.</li>
</ul>
<blockquote>
<p><strong>핵심 포인트 (평가 시점 정리)</strong>
함수가 실제로 컴파일 타임에 평가될 가능성을 다음과 같이 분류해 볼 수 있습니다.</p>
<blockquote>
<p><strong>1. 항상 (표준에 의해 강제됨):</strong></p>
</blockquote>
<ul>
<li>상수 표현식이 필수인 곳에서 <code>constexpr</code> 함수를 호출할 때</li>
<li>컴파일 타임에 평가 중인 다른 함수 안에서 <code>constexpr</code> 함수를 호출할 때</li>
</ul>
<blockquote>
<p><strong>2. 아마도 (굳이 안 할 이유가 없음):</strong></p>
</blockquote>
<ul>
<li>상수가 필수는 아니지만, 매개변수로 전달된 값이 전부 상수일 때</li>
</ul>
<blockquote>
<p><strong>3. 어쩌면 (최적화가 잘 된다면):</strong></p>
</blockquote>
<ul>
<li>상수가 필수는 아니고 인자도 변수지만, 그 변수의 값을 컴파일러가 미리 알 수 있을 때</li>
<li>모든 인자가 상수이며, 일반 함수지만 내용이 단순해 컴파일 타임에 평가 가능할 때</li>
</ul>
<blockquote>
<p><strong>4. 절대 아님 (불가능함):</strong></p>
</blockquote>
<ul>
<li>상수가 필수가 아니고, 전달된 인자의 값을 컴파일 시점에 전혀 알 수 없을 때 (예: 사용자 입력값)</li>
</ul>
</blockquote>
<p><strong>참고:</strong> 여러분이 사용하는 컴파일러의 &#39;최적화 설정&#39;에 따라 이 결과가 달라질 수 있습니다. 디버그(Debug) 모드는 보통 최적화를 끄기 때문에 릴리즈(Release) 모드와 다른 결과를 보일 수 있습니다. (예를 들어, GCC나 Clang 컴파일러는 <code>-O2</code> 같은 최적화 옵션을 주지 않으면 굳이 상수가 필요 없는 곳에서는 컴파일 타임 계산을 시도하지 않습니다.)</p>
<blockquote>
<p><strong>고급 독자를 위한 참고</strong>
컴파일러는 함수 호출 자체를 &#39;인라인(inline)&#39;으로 코드에 덮어씌우거나 최적화 과정에서 함수 호출을 아예 지워버릴 수도 있습니다. 이런 모든 최적화 작업이 함수의 평가 시점이나 평가 여부에 영향을 미치게 됩니다.</p>
</blockquote>
<hr>
<h2 id="f3--constexpr-함수-3부-및-consteval">F.3 — Constexpr 함수 (3부) 및 consteval</h2>
<h3 id="constexpr-함수가-컴파일-타임에-평가되도록-강제하기">constexpr 함수가 컴파일 타임에 평가되도록 강제하기</h3>
<p><code>constexpr</code> 함수가 가능할 때마다 컴파일 타임에 평가되도록 컴파일러에게 강제할 수 있는 직접적인 방법은 없습니다. 
(예: <code>constexpr</code> 함수의 반환값이 상수 표현식이 아닌 곳에 사용될 때)</p>
<p>하지만 컴파일 타임 평가가 가능한 <code>constexpr</code> 함수를, 상수 표현식이 필요한 곳에 반환값을 사용하도록 설정하여 
<strong>강제로 컴파일 타임에 평가되게</strong> 만들 수는 있습니다. 단, 이 작업은 함수를 호출할 때마다 매번 해줘야 합니다.</p>
<p>가장 흔한 방법은 함수의 반환값으로 <code>constexpr</code> 변수를 초기화하는 것입니다 (이전 예제들에서 변수 &#39;g&#39;를 사용했던 이유가 바로 이것입니다). 안타깝게도 이 방식은 단지 컴파일 타임 평가를 보장하기 위해 프로그램에 불필요한 새 변수를 도입해야 하므로, 코드가 지저분해지고 가독성이 떨어집니다.</p>
<hr>
<h3 id="c20의-consteval">C++20의 Consteval</h3>
<p>C++20은 <code>consteval</code>이라는 새로운 키워드를 도입했습니다. 이 키워드는 <strong>함수가</strong> <strong>반드시</strong> 컴파일 타임에 평가되어야 함을 나타내며, 그렇지 않으면 컴파일 에러가 발생합니다. 이러한 함수를 <strong>즉시 함수(immediate functions)</strong>라고 부릅니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

consteval int greater(int x, int y) // 이제 이 함수는 consteval입니다
{
    return (x &gt; y ? x : y);
}

int main()
{
    constexpr int g { greater(5, 6) };              // 정상: 컴파일 타임에 평가됩니다
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    std::cout &lt;&lt; greater(5, 6) &lt;&lt; &quot; is greater!\n&quot;; // 정상: 컴파일 타임에 평가됩니다

    int x{ 5 }; // constexpr이 아닙니다
    std::cout &lt;&lt; greater(x, 6) &lt;&lt; &quot; is greater!\n&quot;; // 에러: consteval 함수는 반드시 컴파일 타임에 평가되어야 합니다

    return 0;
}
</code></pre>
<p>위 예제에서 <code>greater()</code>를 처음 두 번 호출한 부분은 컴파일 타임에 정상적으로 평가됩니다. 하지만 <code>greater(x, 6)</code> 호출은 컴파일 타임에 평가될 수 없으므로 컴파일 에러가 발생합니다.</p>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong> 
어떤 이유로든 반드시 컴파일 타임에 평가되어야 하는 함수가 있다면 <code>consteval</code>을 사용하세요.
(예: 컴파일 타임에만 가능한 특정 작업을 수행하는 경우)</p>
</blockquote>
<p>놀랍게도 <code>consteval</code> 함수의 매개변수 자체는 <code>constexpr</code>이 아닙니다. (<code>consteval</code> 함수가 오직 컴파일 타임에만 평가될 수 있음에도 불구하고요) 
이는 언어 설계상의 일관성을 유지하기 위해 내려진 결정입니다.</p>
<hr>
<h3 id="constexpr-함수-호출이-컴파일-타임인지-런타임인지-확인하기">constexpr 함수 호출이 컴파일 타임인지 런타임인지 확인하기</h3>
<p>현재 C++에서는 이것을 완벽하고 신뢰할 수 있게 알아낼 방법이 없습니다.</p>
<blockquote>
<h4 id="stdis_constant_evaluated나-if-consteval을-사용하면-되지-않나요-고급"><code>std::is_constant_evaluated</code>나 <code>if consteval</code>을 사용하면 되지 않나요? (고급)</h4>
</blockquote>
<p>이 두 가지 기능 모두 함수 호출이 컴파일 타임에 평가되는지 런타임에 평가되는지를 정확히 알려주지는 않습니다.</p>
<blockquote>
</blockquote>
<p><code>&lt;type_traits&gt;</code> 헤더에 정의된 <code>std::is_constant_evaluated()</code>는 현재 함수가 <strong>상수 평가 컨텍스트(constant-evaluated context)</strong>에서 실행 중인지를 알려주는 <code>bool</code> 값을 반환합니다. &#39;상수 평가 컨텍스트&#39;(또는 상수 컨텍스트)란 <code>constexpr</code> 변수의 초기화처럼 상수 표현식이 반드시 요구되는 상황을 뜻합니다. 따라서 컴파일러가 상수 표현식을 컴파일 타임에 평가해야만 하는 상황에서는 <code>std::is_constant_evaluated()</code>가 예상대로 <code>true</code>를 반환합니다.</p>
<blockquote>
</blockquote>
<p>이 기능은 다음과 같은 코드를 작성할 수 있도록 의도되었습니다.</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">#include &lt;type_traits&gt; // std::is_constant_evaluated() 사용을 위해
&gt;
constexpr int someFunction()
{
    if (std::is_constant_evaluated()) // 상수 컨텍스트에서 평가되는 경우
        doSomething();
    else
        doSomethingElse();
}
&gt;</code></pre>
<blockquote>
</blockquote>
<p><strong>하지만 주의할 점이 있습니다.</strong> 컴파일러는 상수 표현식이 필요하지 않은 상황에서도 자체적인 판단으로 <code>constexpr</code> 함수를 컴파일 타임에 평가할 수 있습니다. 이런 경우에는 함수가 실제로 컴파일 타임에 평가되었음에도 불구하고 <code>std::is_constant_evaluated()</code>는 <code>false</code>를 반환합니다.</p>
<blockquote>
</blockquote>
<p>결론적으로 <code>std::is_constant_evaluated()</code>의 진짜 의미는 &quot;이 함수가 현재 컴파일 타임에 평가되고 있다&quot;가 아니라, <strong>&quot;컴파일러가 이 함수를 컴파일 타임에 평가하도록 강제받고 있다&quot;</strong>입니다.</p>
<blockquote>
</blockquote>
<h4 id="핵심-인사이트">핵심 인사이트</h4>
<blockquote>
</blockquote>
<p>이 동작이 조금 이상해 보일 수 있지만, 합당한 이유가 있습니다.</p>
<blockquote>
</blockquote>
<ul>
<li>이 기능을 처음 제안한 문서에 따르면, C++ 표준은 실제로 &quot;컴파일 타임&quot;과 &quot;런타임&quot;을 엄격하게 구분 짓지 않습니다. 이 구분을 명확히 정의하려면 언어 표준에 너무 큰 변화가 필요했을 것입니다.</li>
<li>최적화는 프로그램의 겉보기 동작(observable behavior)을 바꿔서는 안 됩니다. 만약 함수가 컴파일 타임에 평가될 때마다 <code>std::is_constant_evaluated()</code>가 무조건 <code>true</code>를 반환한다면, 최적화 프로그램이 런타임 대신 컴파일 타임 평가를 선택했을 때 함수의 동작 결과가 달라질 수 있습니다. 즉, 어떤 최적화 레벨로 컴파일했느냐에 따라 프로그램이 완전히 다르게 작동하는 심각한 문제가 생길 수 있습니다!</li>
<li>C++23에서 도입된 <code>if consteval</code>은 <code>if (std::is_constant_evaluated())</code>를 대체하는 기능으로, 문법이 더 깔끔하고 몇 가지 자잘한 문제를 해결해 줍니다. 하지만 기본적인 동작 원리와 평가 방식은 같습니다.</li>
</ul>
<hr>
<h3 id="c20-consteval을-사용해-constexpr-컴파일-타임-실행-강제하기">C++20: consteval을 사용해 constexpr 컴파일 타임 실행 강제하기</h3>
<p><code>consteval</code> 함수의 유일한 단점은 런타임에 평가될 수 없다는 것입니다. 
상황에 따라 양쪽 모두에서 작동할 수 있는 <code>constexpr</code> 함수에 비해 유연성이 떨어집니다.</p>
<p>그렇다면 가장 이상적인 것은, <strong>평소에는 유연한 <code>constexpr</code> 함수를 쓰되, 원할 때만 강제로 컴파일 타임에 평가되도록 만드는 편리한 방법</strong>일 것입니다 (반환값을 상수가 아닌 곳에 쓸 때도 포함해서요).</p>
<p>다음은 이 아이디어를 어떻게 구현할 수 있는지 보여주는 예제입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

#define CONSTEVAL(...) [] consteval { return __VA_ARGS__; }()               // Jan Scultke가 제안한 C++20 버전 (https://stackoverflow.com/a/77107431/460250)
#define CONSTEVAL11(...) [] { constexpr auto _ = __VA_ARGS__; return _; }() // Justin이 제안한 C++11 버전 (https://stackoverflow.com/a/63637573/460250)

// 이 함수는 상수 컨텍스트에서 실행되는 경우 두 숫자 중 더 큰 값을 반환하고
// 그렇지 않은 경우 더 작은 값을 반환합니다
constexpr int compare(int x, int y) // 함수는 constexpr입니다
{
    if (std::is_constant_evaluated())
        return (x &gt; y ? x : y);
    else
        return (x &lt; y ? x : y);
}

int main()
{
    int x { 5 };
    std::cout &lt;&lt; compare(x, 6) &lt;&lt; &#39;\n&#39;;                  // 런타임에 실행되며 5를 반환합니다

    std::cout &lt;&lt; compare(5, 6) &lt;&lt; &#39;\n&#39;;                  // 컴파일 타임에 실행될 수도 있고 아닐 수도 있지만, 항상 5를 반환합니다
    std::cout &lt;&lt; CONSTEVAL(compare(5, 6)) &lt;&lt; &#39;\n&#39;;       // 항상 컴파일 타임에 실행되며 6을 반환합니다

    return 0;
}
</code></pre>
<blockquote>
<p><strong>고급 독자를 위한 참고</strong>
위 예제는 가변 인자 매크로(variadic preprocessor macro: <code>#define</code>, <code>...</code>, <code>__VA_ARGS__</code>)를 사용하여 즉시 실행되는 <code>consteval</code> 람다 함수를 정의하는 방식을 썼습니다.</p>
</blockquote>
<p>매크로를 사용하지 않는 더 깔끔한 방법도 있습니다. 아래 코드를 참고하세요.</p>
<blockquote>
<p><strong>GCC 사용자를 위한 주의사항</strong>
GCC 14 이후 버전에 버그가 하나 있어서, 최적화 옵션이 켜져 있을 경우 아래 코드가 잘못된 결과를 낼 수 있습니다.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 이 함수가 모든 유형의 값과 작동하도록 단축된 함수 템플릿(C++20) 및 auto 반환 형식을 사용합니다
// 자세한 내용은 아래 &#39;관련 콘텐츠&#39; 상자를 참조하세요 (이 함수를 사용하기 위해 작동 방식을 알 필요는 없습니다)
// 이전 예제와의 일관성을 위해 여기서는 대문자 이름을 사용하기로 했지만, 호출을 더 쉽게 볼 수 있게 해주는 효과도 있습니다
consteval auto CONSTEVAL(auto value)
{
    return value;
}

// 이 함수는 상수 컨텍스트에서 실행되는 경우 두 숫자 중 더 큰 값을 반환하고
// 그렇지 않은 경우 더 작은 값을 반환합니다
constexpr int compare(int x, int y) // 함수는 constexpr입니다
{
    if (std::is_constant_evaluated())
        return (x &gt; y ? x : y);
    else
        return (x &lt; y ? x : y);
}

int main()
{
    std::cout &lt;&lt; CONSTEVAL(compare(5, 6)) &lt;&lt; &#39;\n&#39;;       // 컴파일 타임에 실행됩니다

    return 0;
}
</code></pre>
<p><code>consteval</code> 함수의 매개변수는 무조건 상수 평가를 거쳐야 하므로, <code>consteval</code> 함수의 인자로 <code>constexpr</code> 함수를 집어넣으면 그 <code>constexpr</code> 함수 역시 <strong>강제로 컴파일 타임에 평가</strong>되어야만 합니다. 그 후 <code>consteval</code> 함수는 계산된 결과를 그대로 반환해 줍니다.</p>
<p>여기서 <code>consteval</code> 함수가 값을 복사해서 반환(return by value)한다는 점을 눈여겨보세요. <code>std::string</code>처럼 복사 비용이 큰 타입을 런타임에 이렇게 복사한다면 비효율적이겠지만, <strong>컴파일 타임 컨텍스트에서는 전혀 문제가 되지 않습니다.</strong> 함수 호출 코드가 통째로 계산된 최종 결괏값으로 쏙 대체되기 때문입니다.</p>
<blockquote>
<p><strong>고급 독자를 위한 참고</strong></p>
<ul>
<li><code>auto</code> 반환 타입은 [10.9 레슨 -- 함수의 타입 추론]에서 다룹니다.</li>
<li>축약된 함수 템플릿(<code>auto</code> 매개변수)은 [11.8 레슨 -- 여러 템플릿 타입이 있는 함수 템플릿]에서 다룹니다.</li>
</ul>
</blockquote>
<hr>
<h2 id="f4--constexpr-함수-4부">F.4 — Constexpr 함수 (4부)</h2>
<h3 id="constexprconsteval-함수는-const가-아닌-지역-변수를-사용할-수-있습니다">Constexpr/consteval 함수는 const가 아닌 지역 변수를 사용할 수 있습니다</h3>
<p><code>constexpr</code>이나 <code>consteval</code> 함수 안에서도 <code>constexpr</code>이 아닌 일반 지역 변수를 사용할 수 있으며, 이 변수들의 값을 자유롭게 변경할 수도 있습니다.</p>
<p>간단한(다소 억지스러운) 예를 살펴봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

consteval int doSomething(int x, int y) // 이 함수는 consteval 함수입니다
{
    x = x + 2;       // const가 아닌 함수 매개변수의 값을 수정할 수 있습니다

    int z { x + y }; // const가 아닌 지역 변수를 생성할 수 있습니다
    if (x &gt; y)
        z = z - 1;   // 그리고 그 값을 수정할 수 있습니다

    return z;
}

int main()
{
    constexpr int g { doSomething(5, 6) };
    std::cout &lt;&lt; g &lt;&lt; &#39;\n&#39;;

    return 0;
}
</code></pre>
<p>이러한 함수가 컴파일 타임(프로그램을 컴파일하는 동안)에 평가될 때, 컴파일러는 기본적으로 이 함수를 직접 &quot;실행&quot;하여 계산된 값을 반환해 줍니다.</p>
<hr>
<h3 id="constexprconsteval-함수는-함수-매개변수나-지역-변수를-다른-constexpr-함수-호출의-인자로-사용할-수-있습니다">Constexpr/consteval 함수는 함수 매개변수나 지역 변수를 다른 constexpr 함수 호출의 인자로 사용할 수 있습니다</h3>
<p>이전에 우리는 다음과 같이 배웠습니다. 
&quot;constexpr (또는 consteval) 함수가 컴파일 타임에 평가될 때, 그 안에서 호출되는 다른 모든 함수들도 컴파일 타임에 평가되어야 한다.&quot;</p>
<p>놀랍게도, <code>constexpr</code>이나 <code>consteval</code> 함수는 자신의 (constexpr이 아닌) 매개변수나 (const가 아닐 수도 있는) 일반 지역 변수를 <strong>다른 constexpr 함수를 부를 때 인자로 사용</strong>할 수 있습니다. <code>constexpr</code>이나 <code>consteval</code> 함수가 컴파일 타임에 실행될 때, 컴파일러는 이미 모든 매개변수와 지역 변수의 값을 알고 있어야 합니다 (그렇지 않으면 컴파일 타임에 계산할 수 없으니까요). 따라서 이런 특별한 상황에서는 C++가 이 변수들의 값을 다른 <code>constexpr</code> 함수 호출의 인자로 쓰는 것을 허용하며, 그 함수 호출 역시 컴파일 타임에 계산될 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

constexpr int goo(int c) // 이제 goo()는 constexpr 함수입니다
{
    return c;
}

constexpr int foo(int b) // foo() 내부에서 b는 상수 표현식이 아닙니다
{
    return goo(b);       // foo()가 컴파일 타임에 처리된다면, `goo(b)` 역시 컴파일 타임에 처리될 수 있습니다
}

int main()
{
    std::cout &lt;&lt; foo(5);

    return 0;
}
</code></pre>
<p>위 예제에서 <code>foo(5)</code>는 컴파일 타임에 계산될 수도 있고 아닐 수도 있습니다. 만약 컴파일 타임에 계산된다면, 컴파일러는 <code>b</code>가 5라는 것을 알게 됩니다. 따라서 <code>b</code>가 <code>constexpr</code> 변수가 아니더라도, 컴파일러는 <code>goo(b)</code>를 부르는 것을 <code>goo(5)</code>를 부르는 것처럼 취급하여 이 함수를 컴파일 타임에 계산할 수 있습니다. 반대로 <code>foo(5)</code>가 프로그램이 실행 중일 때(런타임) 처리된다면, <code>goo(b)</code> 역시 런타임에 처리됩니다.</p>
<hr>
<h3 id="constexpr-함수가-constexpr이-아닌-일반-함수를-호출할-수-있나요">constexpr 함수가 constexpr이 아닌 일반 함수를 호출할 수 있나요?</h3>
<p>네, 가능합니다. 하지만 <strong>constexpr 함수가 상수 컨텍스트(반드시 상수가 필요한 상황)가 아닌 곳에서 사용될 때만</strong> 가능합니다. 
만약 constexpr 함수가 상수 컨텍스트에서 실행 중일 때는 일반 함수를 호출할 수 없으며 (그러면 컴파일 타임에 상수 값을 만들어낼 수 없기 때문입니다), 시도할 경우 컴파일 에러가 발생합니다.</p>
<p>일반 함수 호출을 허용하는 이유는 constexpr 함수가 다음과 같은 작업을 할 수 있게 하기 위해서입니다.</p>
<pre><code class="language-cpp">#include &lt;type_traits&gt; // std::is_constant_evaluated 사용을 위해 포함

constexpr int someFunction()
{
    if (std::is_constant_evaluated()) // 상수 컨텍스트에서 평가 중이라면
        return someConstexprFcn();
    else
        return someNonConstexprFcn();
}
</code></pre>
<p>이제 이 변형된 코드를 살펴보세요.</p>
<pre><code class="language-cpp">constexpr int someFunction(bool b)
{
    if (b)
        return someConstexprFcn();
    else
        return someNonConstexprFcn();
}
</code></pre>
<p>이 코드는 <code>someFunction(false)</code>가 상수 표현식(constant expression) 안에서 절대 호출되지 않는 한 아무런 문제가 없습니다.</p>
<blockquote>
<p><strong>참고로...</strong>
C++23 이전 표준에서는, constexpr 함수가 최소한 한 가지 인자 조합에 대해서는 constexpr 값을 반환해야 하며, 그렇지 않으면 문법적으로 잘못된(ill-formed) 코드로 간주했습니다. constexpr 함수 안에서 무조건적으로 일반 함수를 호출하게 만들면 그 코드는 규칙에 어긋나게 됩니다. 하지만 컴파일러가 이런 경우 무조건 에러나 경고를 내야 하는 것은 아니었습니다. 그래서 직접 상수 컨텍스트에서 호출하려고 시도하지 않는 한, 컴파일러는 별다른 불평을 하지 않았을 것입니다. C++23부터는 이 요구 사항이 폐지되었습니다.</p>
</blockquote>
<p>가장 좋은 결과를 위해 다음과 같은 방법을 권장합니다:</p>
<ul>
<li>가능하면 constexpr 함수 안에서는 일반(non-constexpr) 함수 호출을 피하세요.</li>
<li>상수 컨텍스트일 때와 아닐 때 함수가 다르게 동작해야 한다면, C++20에서는 <code>if (std::is_constant_evaluated())</code>를, C++23 이후에서는 <code>if consteval</code>을 사용하여 조건에 따라 다르게 작동하도록 만드세요.</li>
<li>constexpr 함수를 만들면 항상 상수 컨텍스트에서 잘 작동하는지 테스트해 보세요. 일반 컨텍스트에서는 잘 작동하더라도 상수 컨텍스트에서는 에러가 날 수 있기 때문입니다.</li>
</ul>
<hr>
<h3 id="언제-함수를-constexpr로-만들어야-할까요">언제 함수를 constexpr로 만들어야 할까요?</h3>
<p>일반적인 규칙은 다음과 같습니다. <strong>함수가 상수 표현식의 일부로 계산될 가능성이 있다면, 그 함수는 <code>constexpr</code>로 만들어야 합니다.</strong></p>
<p><strong>순수 함수(pure function)</strong>는 다음 조건을 만족하는 함수를 말합니다.</p>
<ol>
<li>같은 인자를 주면 언제나 똑같은 결과를 반환합니다.</li>
<li>부작용(side effect)이 없습니다. (예를 들어, 함수 외부에 있는 정적 지역 변수나 전역 변수의 값을 바꾸지 않고, 입출력 작업도 하지 않습니다.)</li>
</ol>
<p>순수 함수는 일반적으로 <code>constexpr</code>로 만드는 것이 좋습니다.</p>
<blockquote>
<p><strong>참고로...</strong>
constexpr 함수가 언제나 순수 함수여야만 하는 것은 아닙니다. C++23에서는 constexpr 함수가 정적(static) 지역 변수를 사용하고 수정할 수 있게 되었습니다. 정적 지역 변수의 값은 함수가 호출될 때마다 계속 유지되기 때문에, 이 값을 수정하는 것은 부작용(side-effect)으로 간주됩니다.
그렇긴 하지만, 여러분이 아주 간단하거나 일회성으로 쓸 프로그램을 만들면서 함수에 <code>constexpr</code>을 붙이지 않는다고 해서 세상이 무너지지는 않을 겁니다. (아마도요!)</p>
</blockquote>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
안 할 특별한 이유가 없다면, 상수 표현식의 일부로 계산될 수 있는 함수는 모두 <code>constexpr</code>로 만드세요. (지금 당장 그렇게 쓰이지 않더라도 말입니다.) 반대로 상수 표현식으로 쓰일 수 없는 함수에는 <code>constexpr</code>을 붙이면 안 됩니다.</p>
</blockquote>
<hr>
<h3 id="왜-모든-함수를-constexpr로-만들지-않나요">왜 모든 함수를 constexpr로 만들지 않나요?</h3>
<p>함수에 <code>constexpr</code>을 붙이고 싶지 않은 몇 가지 이유가 있습니다:</p>
<ol>
<li><code>constexpr</code>은 &quot;이 함수는 상수 표현식에서 쓸 수 있다&quot;는 신호입니다. 만약 함수가 상수 표현식으로 계산될 수 없다면 <code>constexpr</code>로 표시해선 안 됩니다.</li>
<li><code>constexpr</code>은 함수의 겉모습(인터페이스)의 일부입니다. 한 번 함수를 <code>constexpr</code>로 만들면, 다른 constexpr 함수들이 이를 호출하거나 상수 표현식이 필요한 곳에서 이 함수를 사용하게 됩니다. 나중에 마음이 바뀌어 <code>constexpr</code>을 지워버리면, 이 함수를 사용하던 다른 코드들에서 에러가 발생하게 됩니다.</li>
<li>constexpr 함수는 디버깅하기 더 어려울 수 있습니다. 디버거에서 실행을 잠시 멈추거나(중단점) 코드를 한 줄씩 확인(step through)할 수 없기 때문입니다.</li>
</ol>
<hr>
<h3 id="컴파일-타임에-실제로-계산되지-않는데도-왜-constexpr-함수로-만들어야-하나요">컴파일 타임에 실제로 계산되지 않는데도 왜 constexpr 함수로 만들어야 하나요?</h3>
<p>초보 프로그래머들은 종종 이렇게 묻습니다. *&quot;함수를 부를 때 일반 변수를 넣는 등, 내 프로그램에서는 어차피 실행할 때(런타임에)만 계산되는데 왜 굳이 <code>constexpr</code>을 붙여야 하죠?&quot;*</p>
<p>여기에는 몇 가지 이유가 있습니다:</p>
<ul>
<li><code>constexpr</code>을 사용해서 생기는 단점은 거의 없고, 오히려 컴파일러가 프로그램을 더 작고 빠르게 최적화하는 데 도움을 줄 수 있습니다.</li>
<li>지금 당장 컴파일 타임에 함수를 부르지 않는다고 해서, 나중에 프로그램을 수정하거나 확장할 때도 안 부른다는 보장은 없습니다. 미리 <code>constexpr</code>을 붙여두지 않았다면, 나중에 상수 컨텍스트에서 호출할 때 깜빡하고 <code>constexpr</code>을 안 붙여서 성능 이점을 놓칠 수도 있습니다. 혹은 어딘가에서 반드시 상수 표현식이 필요한 곳에 반환값을 써야 할 때 어쩔 수 없이 나중에 부랴부랴 <code>constexpr</code>을 붙여야 할 수도 있죠.</li>
<li>반복은 좋은 코딩 습관을 몸에 배게 해줍니다.</li>
</ul>
<p>간단하지 않은 제대로 된 프로젝트에서는, 자신이 만든 함수가 미래에 재사용되거나 확장될 수 있다는 마음가짐으로 코드를 짜는 것이 좋습니다. 기존 함수를 수정할 때는 항상 코드를 망가뜨릴 위험이 따르며, 이는 곧 다시 테스트를 해야 한다는 뜻이므로 시간과 에너지가 소모됩니다. 나중에 다시 고치고 테스트하는 수고를 덜기 위해, <strong>&quot;처음부터 올바르게 작성하는 데&quot;</strong> 1~2분을 더 투자하는 것은 그만한 가치가 충분히 있습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 11]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-11</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-11</guid>
            <pubDate>Fri, 20 Feb 2026 07:26:32 GMT</pubDate>
            <description><![CDATA[<h2 id="111--함수-오버로딩-소개-introduction-to-function-overloading">11.1 — 함수 오버로딩 소개 (Introduction to function overloading)</h2>
<p>다음과 같은 함수가 있다고 생각해 봅시다.</p>
<pre><code class="language-cpp">int add(int x, int y)
{
    return x + y;
}</code></pre>
<p>이 간단한 함수는 <code>int</code> 정수 두 개를 더해서 정수 결과를 반환합니다. 
하지만 만약 소수점이 있는 실수 <code>float</code> 두 개를 더하는 기능이 필요하다면 어떻게 해야 할까요? </p>
<p>위에서 만든 <code>add()</code> 함수는 실수를 다루기에 적합하지 않습니다. 
실수를 매개변수로 넣더라도 정수로 강제 변환되면서 소수점 아래 값들이 사라져 버리기 때문입니다.</p>
<p>이 문제를 해결하는 한 가지 방법은, 아래처럼 함수 이름을 조금씩 다르게 지어서 여러 개를 만드는 것입니다.</p>
<pre><code class="language-cpp">int addInteger(int x, int y)
{
    return x + y;
}

double addDouble(double x, double y)
{
    return x + y;
}</code></pre>
<p>하지만 이 방식은 불편한 점이 있습니다.</p>
<ul>
<li>비슷한 함수들의 이름을 <strong>일관된 규칙으로 정해야</strong> 하고</li>
<li>그 이름들을 <strong>다 외워야</strong> 하고</li>
<li>호출할 때도 <strong>상황에 맞는 함수를 정확히 골라서</strong> 써야 합니다.</li>
</ul>
<p>게다가 “정수 2개가 아니라 <strong>정수 3개를</strong> 더하는 함수도 필요해!” 같은 일이 생기면,
각각에 대해 또 다른 이름을 만들어야 해서 <strong>함수 이름 관리가 금방 부담스러워집니다.</strong></p>
<hr>
<h3 id="함수-오버로딩-소개">함수 오버로딩 소개</h3>
<p>다행히 C++에는 이런 상황을 깔끔하게 해결하는 기능이 있습니다. 바로 <strong>함수 오버로딩</strong>입니다.
함수 오버로딩이란 <strong>이름은 같지만 매개변수의 타입이 서로 다르기만 하다면</strong> 똑같은 이름을 가진 함수를 여러개 만들수 있게 해주는 기능입니다.</p>
<hr>
<h3 id="add를-오버로딩해-보기"><code>add()</code>를 오버로딩해 보기</h3>
<p>정수용 <code>add()</code>가 이미 있으니, 실수용 <code>add()</code>를 하나 더 만들면 됩니다.</p>
<pre><code class="language-cpp">double add(double x, double y){
    return x + y;
}</code></pre>
<p>이제 같은 범위에 <code>add()</code>가 두 개 생겼습니다.</p>
<pre><code class="language-cpp">int add(int x, int y) // 정수 버전
{
    return x + y;
}

double add(double x, double y) // 실수(부동소수점) 버전
{
    return x + y;
}

int main(){
    return 0;
}</code></pre>
<p>이 프로그램은 문제없이 컴파일됩니다. 왜냐하면** 두 함수는 매개변수 타입이 다르기 때문에<strong>, 컴파일러가 서로 다른 함수로 구분할 수 있습니다. 
즉, **이름은 같지만 별개의 함수로 취급</strong>됩니다.</p>
<hr>
<h3 id="오버로드-결정-소개">오버로드 결정 소개</h3>
<p>함수가 오버로딩되어 있을 때, <code>add(...)</code> 처럼 호출하면 컴파일러는 이렇게 행동합니다.</p>
<ul>
<li>전달한 <strong>인수</strong>를 보고</li>
<li>그 호출에 가장 알맞은 <code>add()</code>를 찾아서 선택합니다.</li>
</ul>
<p>이 과정을 <strong>오버로드 결정</strong>이라고 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int add(int x, int y){
    return x + y;
}

double add(double x, double y){
    return x + y;
}

int main(){
    std::cout &lt;&lt; add(1, 2); // add(int, int)를 호출
    std::cout &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; add(1.2, 3.4); // add(double, double)를 호출

    return 0;
}</code></pre>
<ul>
<li><code>add(1, 2)</code> 처럼 정수를 넣으면 컴파일러는 <code>add(int, int)</code>를 선택합니다.</li>
<li><code>add(1.2, 3.4)</code> 처럼 실수를 넣으면 컴파일러는 <code>add(double, double)</code> 를 선택합니다.</li>
</ul>
<hr>
<h4 id="결론">결론</h4>
<p>함수 오버로딩은 프로그램을 더 단순하게 만들어 줍니다.
즉, <strong>외워야 할 함수 이름의 개수를 줄여</strong>서 코드 관리가 쉬워집니다.
그래서 오버로딩은 <strong>적극적으로 사용해도 좋고, 실제로 자주 사용됩니다.</strong></p>
<hr>
<h2 id="112--함수-오버로딩-구별하기-function-overload-differentiation">11.2 — 함수 오버로딩 구별하기 (Function overload differentiation)</h2>
<p>11.2 챕터에서는 컴파일러가 이름이 같은 이 함수들을 구체적으로 <strong>어떻게 구별하는지</strong> 자세히 알아보겠습니다. 
만약 오버로딩된 함수들을 제대로 구별되게 만들지 않으면, 컴파일러는 에러를 일으킵니다.</p>
<h3 id="오버로드된-함수는-어떻게-구분될까">오버로드된 함수는 어떻게 구분될까?</h3>
<p>아래 항목들이 함수 오버로드 구분에 사용됩니다.</p>
<table>
<thead>
<tr>
<th>함수 속성</th>
<th align="right">구분에 사용됨?</th>
<th>참고</th>
</tr>
</thead>
<tbody><tr>
<td>매개변수 개수</td>
<td align="right">예</td>
<td></td>
</tr>
<tr>
<td>매개변수 타입</td>
<td align="right">예</td>
<td><code>typedef</code>, 타입 별칭(type alias), 값 전달 매개변수의 <code>const</code>는 구분에 포함되지 않음. <code>...</code>(ellipsis)는 포함됨</td>
</tr>
<tr>
<td>반환 타입</td>
<td align="right">아니오</td>
<td></td>
</tr>
</tbody></table>
<p><strong>함수의 반환 타입(return type)은 오버로드 구분에 사용되지 않습니다.</strong>
이 부분은 뒤에서 다시 설명하겠습니다.</p>
<hr>
<h3 id="매개변수-개수로-오버로딩하기">매개변수 개수로 오버로딩하기</h3>
<p>오버로드된 함수들은 <strong>각 함수의 매개변수 개수가 다르면</strong> 서로 구분됩니다. 예를 들어</p>
<pre><code class="language-cpp">int add(int x, int y)
{
    return x + y;
}

int add(int x, int y, int z)
{
    return x + y + z;
}
</code></pre>
<p>컴파일러는 다음을 쉽게 구분할 수 있습니다.</p>
<ul>
<li>정수 인자 2개로 호출하면 <code>add(int, int)</code></li>
<li>정수 인자 3개로 호출하면 <code>add(int, int, int)</code></li>
</ul>
<hr>
<h3 id="매개변수-타입으로-오버로딩하기">매개변수 타입으로 오버로딩하기</h3>
<p>각 오버로드 함수의 <strong>매개변수 타입 목록이 서로 다르면</strong> 함수도 구분할 수 있습니다. 
예를 들어, 아래 오버로드들은 모두 서로 구분됩니다.</p>
<pre><code class="language-cpp">int add(int x, int y);         // 정수 버전
double add(double x, double y); // 실수 버전
double add(int x, double y);    // 혼합 버전
double add(double x, int y);    // 혼합 버전</code></pre>
<h4 id="주의-사항">주의 사항</h4>
<p><strong>타입 별칭(Type aliases)</strong>이나 typedef는 새로운 타입을 만드는 것이 아니라 기존 타입에 다른 이름만 붙여주는 것입니다. 
따라서 이를 사용한 오버로딩은 구별되지 않습니다</p>
<pre><code class="language-cpp">typedef int Height; // typedef
using Age = int; // 타입 별칭 (type alias)

void print(int value);
void print(Age value); // print(int)와 구별되지 않음 (에러)
void print(Height value); // print(int)와 구별되지 않음 (에러)</code></pre>
<p>또한, <strong>값으로 전달(pass by value)</strong>받는 매개변수의 경우, <code>const</code>가 붙어 있어도 구별되지 않습니다.</p>
<pre><code class="language-cpp">void print(int);
void print(const int); // print(int)와 구별되지 않음 (에러)</code></pre>
<hr>
<h3 id="반환-타입은-오버로드-구분에-사용되지-않는다">반환 타입은 오버로드 구분에 사용되지 않는다</h3>
<p>함수를 오버로드할 때, <strong>반환 타입은 구분 기준이 아닙니다.</strong></p>
<p>예를 들어, 랜덤 값을 반환하는 함수를 만들고 싶은데
하나는 <code>int</code>를 반환하고, 다른 하나는 <code>double</code>을 반환하게 만들고 싶다고 해봅시다.</p>
<pre><code class="language-cpp">int getRandomValue();
double getRandomValue();</code></pre>
<p>하지만 <code>Visual Studio 2019</code> 에서는 다음과 같은 컴파일 오류가 발생합니다
<code>error C2556: &#39;double getRandomValue(void)&#39;: overloaded function differs only by return type from &#39;int getRandomValue(void)&#39;</code></p>
<p>이건 자연스러운 결과입니다.
컴파일러 입장에서 아래 코드를 보면</p>
<pre><code class="language-cpp">getRandomValue();</code></pre>
<p>두 함수 중 <strong>어느 것을 호출해야 하는지 알 수 없기 때문</strong>입니다.</p>
<hr>
<h3 id="타입-시그니처-type-signature">타입 시그니처 (Type signature)</h3>
<p>함수의 타입 시그니처(보통 그냥 &#39;시그니처&#39;라고 부름)는 <strong>함수들을 서로 구별하는 데 사용되는 정보</strong>들을 말합니다. 
C++에서 시그니처는 다음 요소들을 포함합니다.</p>
<ul>
<li>함수 이름</li>
<li>매개변수의 개수</li>
<li>매개변수의 타입</li>
<li>함수 수준 한정자 ( <code>const</code> 등)</li>
</ul>
<p>다시 강조하지만, <strong>반환 타입은 시그니처에 포함되지 않습니다.</strong></p>
<hr>
<h3 id="네임-맹글링-name-mangling">네임 맹글링 (Name mangling)</h3>
<p><strong>컴파일러가 코드를 컴파일할 때</strong> <strong>네임 맹글링</strong>이라는 과정을 거칩니다. 
쉽게 말해, 링커가 함수들을 헷갈리지 않도록 <strong>컴파일러가 함수 이름을 내부적으로 복잡하게 바꿔버리는 것</strong>입니다.
이때 매개변수의 개수나 타입을 이름에 섞어 넣습니다.</p>
<p>예를 들어, 소스 코드에는 <code>int fcn()</code>이라는 함수가 있지만, 컴파일된 코드에서는<code>__fcn_v</code> 같은 이름으로 바뀔 수 있습니다.
반면 <code>int fcn(int)</code>는 <code>__fcn_i</code>처럼 바뀔 수 있죠.</p>
<p>덕분에 소스 코드에서는 둘 다 <code>fcn()</code>이라는 같은 이름을 쓰지만, 컴파일러 내부에서는 <code>__fcn_v</code>와 <code>__fcn_i</code>라는 서로 다른 이름으로 관리되어 충돌이 나지 않는 것입니다.</p>
<hr>
<h2 id="113--함수-오버로드-해결과-모호한ambiguous-일치">11.3 — 함수 오버로드 해결과 모호한(ambiguous) 일치</h2>
<p>잘 구분된 오버로딩 함수들을 만들어 두었다고 해서 끝이 아닙니다. 
함수가 호출될 때, <strong>컴파일러는 그 호출에 딱 맞는 함수 선언을 무사히 찾아낼 수 있어야 합니다.</strong></p>
<p>오버로딩 되지 않은 일반 함수(이름이 고유한 함수)의 경우, 호출 시 매칭될 가능성이 있는 함수는 단 하나뿐입니다. 
조건이 맞거나(혹은 형 변환을 거쳐 맞추거나), 아니면 아예 맞지 않아서 컴파일 에러가 나거나 둘 중 하나입니다.</p>
<p>반면 오버로딩된 함수들의 경우, <strong>하나의 함수 호출에 매칭될 가능성이 있는 함수가 여러 개 존재할 수 있습니다.</strong> 
함수 호출은 단 하나의 함수로만 연결되어야 하므로, 컴파일러는 <strong>여러 함수 중 어떤 것이 가장 &#39;최적의 매칭&#39;인지 결정해야 합니다.</strong> 
이렇게 특정 오버로딩 함수와 함수 호출을 짝지어주는 과정을 <strong>오버로딩 해석(Overload resolution)</strong>이라고 부릅니다.</p>
<p>함수 호출 시 <strong>전달한 인자의 타입과 함수의 매개변수 타입이 정확히 일치하는 간단한 경우에는 이 과정이 아주 직관적</strong>입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int x)
{
     std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

void print(double d)
{
     std::cout &lt;&lt; d &lt;&lt; &#39;\n&#39;;
}

int main()
{
     print(5); // 5는 int형이므로, print(int)와 매칭됩니다.
     print(6.7); // 6.7은 double형이므로, print(double)과 매칭됩니다.

     return 0;
}</code></pre>
<p>그렇다면, 함수 호출 시 전달한 <strong>인자의 타입</strong>이 오버로딩된 함수들의 <strong>매개변수 타입</strong> 중 <strong>그 어떤 것과도 정확히 일치하지 않는다면</strong> 어떻게 될까요? 예를 들면 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int x)
{
     std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

void print(double d)
{
     std::cout &lt;&lt; d &lt;&lt; &#39;\n&#39;;
}

int main()
{
     print(&#39;a&#39;); // char형은 int나 double과 정확히 일치하지 않는데, 어떻게 될까요?
     print(5L); // long형은 int나 double과 정확히 일치하지 않는데, 어떻게 될까요?

     return 0;
}</code></pre>
<p>정확히 일치하는 함수가 없다고 해서 반드시 오류가 발생하는 것은 아닙니다.</p>
<p>왜냐하면</p>
<ul>
<li><code>char</code> → <code>int</code> 또는 <code>double</code> 로 변환 가능</li>
<li><code>long</code> → <code>int</code> 또는 <code>double</code> 로 변환 가능</li>
</ul>
<p>하지만 중요한 질문은 이것입니다. <strong>어떤 변환이 가장 적절한 변환일까?</strong>
이 강의에서는 컴파일러가 주어진 함수 호출을 특정 오버로딩 함수와 어떻게 짝지어주는지 알아보겠습니다.</p>
<hr>
<h3 id="오버로딩된-함수-호출-해석하기">오버로딩된 함수 호출 해석하기</h3>
<p>오버로딩된 함수가 호출되면, 컴파일러는 어떤 함수가 가장 최적의 매칭인지 알아내기 위해 일련의 규칙을 순서대로 밟아 나갑니다.</p>
<p>각 단계마다 컴파일러는 함수 호출에 사용된 인자에 다양한 형 변환을 적용해 봅니다. 
변환을 적용할 때마다 일치하는 오버로딩 함수가 있는지 확인합니다. 
확인을 마치면 그 단계가 끝나며, 결과는 다음 세 가지 중 하나가 됩니다.</p>
<ol>
<li><strong>일치하는 함수를 찾지 못함</strong> 컴파일러는 다음 매칭 단계로 넘어갑니다.</li>
<li><strong>단 하나의 일치하는 함수를 찾음</strong> 이 함수가 최적의 매칭으로 간주됩니다. 매칭 과정이 완료되며, 이후의 단계는 실행되지 않습니다.</li>
<li><strong>두 개 이상의 일치하는 함수를 찾음</strong> 컴파일러는 <strong>모호한 매칭 컴파일 에러를 발생</strong>시킵니다. 이에 대해서는 잠시 후에 자세히 다루겠습니다.</li>
</ol>
<p>만약 컴파일러가 <strong>모든 단계를 끝까지 거쳤는데도</strong> 일치하는 함수를 찾지 못하면, 
<strong>일치하는 오버로딩 함수를 찾을 수 없다는 컴파일 에러를 발생</strong>시킵니다.</p>
<hr>
<h3 id="인자-매칭-순서-the-argument-matching-sequence">인자 매칭 순서 (The argument matching sequence)</h3>
<blockquote>
<blockquote>
<p><strong>Step 1) 정확한 일치 찾기</strong>
먼저 컴파일러는 정확히 일치하는 함수가 있는지 찾습니다.</p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">void foo(int){}
void foo(double){}
&gt;&gt;
int main()
{
    foo(0);   // foo(int)와 정확히 일치
    foo(3.4); // foo(double)와 정확히 일치
&gt;&gt;
    return 0;
}</code></pre>
<p><code>0</code>은 <code>int</code> 타입이므로 <code>foo(int)</code>가 선택됩니다.</p>
<blockquote>
<br>
>**Step 1-2) 사소한 변환 적용**
컴파일러는 값은 그대로 유지하면서 타입만 약간 변경하는 변환도 시도합니다.
</blockquote>
<ul>
<li><code>int</code> → <code>const int</code></li>
<li><code>double</code> → <code>const double&amp;</code></li>
<li><code>non-reference</code> → <code>reference</code><pre><code class="language-cpp">void foo(const int){}
void foo(const double&amp;) // double에 대한 참조
{}
&gt;&gt;
int main()
{
  int x { 1 };
  foo(x); // int → const int 로 변환됨
&gt;&gt;
  double d { 2.3 };
  foo(d); // double → const double&amp; 로 변환됨
&gt;&gt;
  return 0;
}</code></pre>
이러한 변환도 <strong>정확한 일치로 취급</strong>됩니다.<blockquote>
<blockquote>
<p><strong>Step 2) 숫자 승격</strong>
정확한 일치가 없다면, 컴파일러는 <strong>숫자 승격을 시도</strong>합니다.</p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">void foo(int){}
void foo(double){}
&gt;&gt;
int main()
{
  foo(&#39;a&#39;);  // char → int 승격
  foo(true); // bool → int 승격
  foo(4.5f); // float → double 승격
&gt;&gt;
  return 0;
}</code></pre>
즉,<blockquote>
<blockquote>
</blockquote>
</blockquote>
</li>
<li><code>char</code> → <code>int</code></li>
<li><code>bool</code> → <code>int</code></li>
<li><code>float</code> → <code>double</code><blockquote>
<blockquote>
</blockquote>
<p>이러한 <strong>승격은 자동으로 발생</strong>합니다.</p>
<blockquote>
<p><strong>Step 3) 숫자 변환</strong>
승격으로 해결되지 않으면 <strong>숫자 변환을 시도</strong>합니다.</p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">#include &lt;string&gt;
&gt;&gt;
void foo(double){}
void foo(std::string){}
&gt;&gt;
int main()
{
  foo(&#39;a&#39;); // char → double 변환됨
&gt;&gt;
  return 0;
}</code></pre>
<code>char</code> → <code>int</code> → <code>double</code> 이런 방식으로 변환됩니다.<blockquote>
<blockquote>
<p><strong>중요한 핵심</strong>
숫자 승격은 숫자 변환보다 우선순위가 높습니다.</p>
</blockquote>
<blockquote>
<p><strong>Step 4) 사용자 정의 변환</strong></p>
</blockquote>
<p>클래스는 사용자 정의 변환을 만들 수 있습니다.</p>
</blockquote>
<pre><code class="language-cpp">// 아직 클래스를 배우지 않았으므로 이해되지 않아도 걱정하지 마세요
class X // X라는 새로운 타입을 정의합니다
{
public:
  operator int() { return 0; } // X 타입에서 int 타입으로의 사용자 정의 변환입니다
};
&gt;&gt;
void foo(int)
{
}
&gt;&gt;
void foo(double)
{
}
&gt;&gt;
int main()
{
  X x; // 여기서 X 타입의 객체(이름은 x)를 생성합니다
  foo(x); // x는 X에서 int로의 사용자 정의 변환을 사용해 int 타입으로 변환됩니다
&gt;&gt;
  return 0;
}</code></pre>
<blockquote>
<blockquote>
<p><strong>Step 5) 사용자 정의 변환으로도 찾지 못하면, 줄임표(ellipsis, ...)를 사용하는 매칭 함수를 찾습니다.</strong></p>
</blockquote>
<blockquote>
<p><strong>Step 6) 이 시점까지도 일치하는 함수를 찾지 못했다면, 컴파일러는 포기하고 일치하는 함수를 찾을 수 없다는 컴파일 에러를 발생시킵니다.</strong></p>
</blockquote>
</blockquote>
</li>
</ul>
<hr>
<h3 id="모호한-매칭-ambiguous-matches">모호한 매칭 (Ambiguous matches)</h3>
<p>같은 확인 단계(step) 내에서 매칭될 수 있는 함수를 컴파일러가 두 개 이상 발견했을 때 발생합니다. 
이때 컴파일러는 에러를 띄우고 실행을 멈춥니다.</p>
<pre><code class="language-cpp">void foo(int)
{
}

void foo(double)
{
}

int main()
{
    foo(5L); // 5L은 long 타입입니다

    return 0;
}</code></pre>
<p>리터럴 <code>5L</code>은 <code>long</code> 타입입니다. <code>1단계 정확한 일치</code> 와 <code>2단계 숫자 승격</code> 에서는 매칭을 찾지 못합니다. 
그다음 <code>3단계 숫자 변환</code> 를 시도할 때, <code>long</code> 인자는 <code>int</code>로 변환될 수도 있고 <code>double</code>로 변환될 수도 있습니다. 
<strong>두 가지 가능한 매칭이 같은 단계에서 발견되었으므로, 이 함수 호출은 모호해집니다.</strong></p>
<p>여러 개의 매칭 항목이 발견되면 모호한 함수 호출 에러가 발생합니다. 
즉, 같은 단계 안에서는 <strong>어떤 매칭이 다른 매칭보다 더 낫다고 우열을 가리지 않습니다.</strong></p>
<p>다음도 모호한 매칭의 예입니다.</p>
<pre><code class="language-cpp">void foo(unsigned int)
{
}

void foo(float)
{
}

int main()
{
    foo(0);       // int는 unsigned int나 float로 숫자 변환될 수 있습니다
    foo(3.14159); // double은 unsigned int나 float로 숫자 변환될 수 있습니다

    return 0;
}</code></pre>
<p><code>0</code>은 <code>foo(unsigned int)</code>로, <code>3.14159</code>는 <code>foo(float)</code>로 매칭될 것이라 예상할 수 있지만, <strong>둘 다 모호한 매칭 에러</strong>를 냅니다. 
<code>int</code>와 <code>double</code> 모두 <code>unsigned int</code>나 <code>float</code> 어느 쪽으로든 변환이 가능하며(숫자 변환), 둘의 우선순위가 동등하기 때문입니다.</p>
<hr>
<h3 id="모호한-매칭-해결하기">모호한 매칭 해결하기</h3>
<p><strong>1.  정확한 함수 추가</strong></p>
<pre><code class="language-cpp">void foo(int){}</code></pre>
<p><strong>2. 명시적 캐스팅</strong></p>
<pre><code class="language-cpp">int x{ 0 };

foo(static_cast&lt;unsigned int&gt;(x)); // unsigned int 버전 호출</code></pre>
<p><strong>3. 리터럴 접미사 사용</strong></p>
<pre><code class="language-cpp">foo(0u); // unsigned int literal</code></pre>
<hr>
<h3 id="여러-인자가-있는-경우">여러 인자가 있는 경우</h3>
<p>컴파일러는 각 인자를 모두 비교합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(char, int)
{
    std::cout &lt;&lt; &#39;a&#39; &lt;&lt; &#39;\n&#39;;
}

void print(char, double)
{
    std::cout &lt;&lt; &#39;b&#39; &lt;&lt; &#39;\n&#39;;
}

void print(char, float)
{
    std::cout &lt;&lt; &#39;c&#39; &lt;&lt; &#39;\n&#39;;
}

int main()
{
    print(&#39;x&#39;, &#39;a&#39;);

    return 0;
}</code></pre>
<ul>
<li>첫 번째 인자 <code>char</code> → <code>Step 1) 정확히 일치</code></li>
<li>두 번째 인자
<code>Step 2) 승격</code> <code>char</code> → <code>int</code> 
<code>Step 3) 변환</code> <code>char</code> → <code>double</code> 
<code>Step 3) 변환</code> <code>char</code> → <code>float</code> 
중복 되는 규칙이 있지만 <code>숫자 승격</code>이 <code>숫자 변환</code>보다 우선되므로 <code>print(char, int)</code>가 선택됩니다.</li>
</ul>
<hr>
<h2 id="114--함수-삭제하기-deleting-functions">11.4 — 함수 삭제하기 (Deleting functions)</h2>
<p>때로는 특정 타입의 값을 넣어 함수를 호출했을 때, 우리가 원하지 않는 방식으로 작동하는 경우가 있습니다. 
다음 예제를 함께 살펴볼까요?</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printInt(int x)
{
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

int main()
{
    printInt(5);    // 정상: 5를 출력합니다.
    printInt(&#39;a&#39;);  // 97을 출력합니다. -- 우리가 의도한 결과일까요?
    printInt(true); // 1을 출력합니다. -- 우리가 의도한 결과일까요?

    return 0;
}

출력:
5
97
1</code></pre>
<p><code>printInt(5)</code> 는 숫자를 전달했으니 확실히 문제가 없지만, 나머지 두 번의 호출은 조금 의아합니다.
<code>printInt(&#39;a&#39;)</code>의 경우, 컴파일러는 함수 규칙에 맞추기 위해 문자 <code>&#39;a&#39;</code>를 정수 <code>97</code>로 몰래 변환(승격)해 버립니다. 
<code>true</code> 역시 정수 <code>1</code>로 변환하죠. 게다가 컴파일러는 이런 행동을 하면서 아무런 경고나 에러도 띄우지 않습니다.</p>
<p>만약 우리가 <code>char</code> 나 <code>bool</code> 타입의 값으로 <code>printInt()</code> 를 호출하는 것을 <strong>아예 막고 싶다면</strong> 어떻게 해야 할까요?</p>
<hr>
<h3 id="-delete-지정자를-사용해-함수-삭제하기"><code>= delete</code> 지정자를 사용해 함수 삭제하기</h3>
<p>특정 함수가 아예 호출되지 못하도록 강력하게 막고 싶을 때, <code>= delete</code> 라는 문법을 사용하여 해당 함수를 <strong>삭제</strong> 처리할 수 있습니다.
만약 컴파일러가 코드를 읽다가 이 &#39;삭제된 함수&#39;를 사용하려는 시도를 발견하면, <strong>컴파일 단계에서 오류가 발생하고 컴파일이 중단</strong>됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printInt(int x)
{
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

void printInt(char) = delete; // 이 함수를 호출하면 컴파일이 중단(에러 발생)됩니다.
void printInt(bool) = delete; // 이 함수를 호출하면 컴파일이 중단(에러 발생)됩니다.

int main()
{
    printInt(97);   // 정상

    printInt(&#39;a&#39;);  // 컴파일 에러: 삭제된 함수입니다.
    printInt(true); // 컴파일 에러: 삭제된 함수입니다.

    printInt(5.0);  // 컴파일 에러: 모호한 매칭(ambiguous match)입니다.

    return 0;
}</code></pre>
<p>위 코드의 결과를 하나씩 살펴봅시다.</p>
<ul>
<li>먼저 <code>printInt(&#39;a&#39;)</code> 는 방금 우리가 삭제한 <code>printInt(char)</code> 와 정확히 일치합니다. 따라서 <strong>컴파일러는 에러를 뿜어냅니다.</strong></li>
<li><code>printInt(true)</code> 역시 <strong>삭제된</strong> <code>printInt(bool)</code>과 정확히 일치하므로 <strong>컴파일 에러가 발생</strong>합니다.</li>
</ul>
<p>그런데 <code>printInt(5.0)</code>은 꽤 흥미로우며, 예상치 못한 결과를 보여줍니다.</p>
<ol>
<li>먼저 정확히 일치하는 함수 printInt(double) 가 있는지 확인 → 없음</li>
<li>다음으로 가장 적절한 함수를 찾음</li>
</ol>
<p>이때 중요한 점은 <strong>삭제된 함수도 함수 선택 과정에 포함</strong>된다는 것입니다.
즉, 컴파일러는 다음 후보들을 모두 고려합니다.</p>
<ul>
<li><code>printInt(int)</code></li>
<li><code>printInt(char)</code> (삭제됨)</li>
<li><code>printInt(bool)</code> (삭제됨)</li>
</ul>
<p><code>5.0</code> 을 <code>int</code> <code>char</code> <code>bool</code> 중 어느 것으로 변환하는 것이 가장 완벽한지 명확하지 않기 때문에,
컴파일러는 헷갈린다는 의미로 <strong>&quot;모호한 매칭(ambiguous match)&quot;이라며 컴파일 에러를 발생</strong>시킵니다.</p>
<blockquote>
<p><strong>핵심 개념</strong>
<code>= delete</code> 의 의미는
<strong>&quot;이 함수는 존재하지 않는다&quot;</strong> 가 아니라
<strong>&quot;이 함수는 호출하는 것을 금지한다&quot;</strong> 입니다.
즉,</p>
</blockquote>
<ul>
<li>삭제된 함수도 여전히 존재합니다.</li>
<li>그리고 <strong>인자 매칭 순서</strong>에 참여합니다.</li>
<li>하지만 선택되면 컴파일 오류가 발생합니다.</li>
</ul>
<hr>
<h2 id="115--기본-인수-default-arguments">11.5 — 기본 인수 (Default arguments)</h2>
<p><strong>기본 인수</strong>란 함수 매개변수에 미리 정해둔 기본값을 말합니다. 다음 예시를 볼까요?</p>
<pre><code class="language-cpp">void print(int x, int y=10) // 10이 기본 인수입니다.
{
    std::cout &lt;&lt; &quot;x: &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;y: &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;
}</code></pre>
<p>함수를 호출할 때, <strong>기본 인수가 지정된 매개변수라면 우리가 값을 넘겨줄지 말지 선택할 수 있습니다. **
우리가 값을 직접 넘겨주면 그 값이 사용되고, **값을 넘겨주지 않고 생략하면 미리 정해둔 &#39;기본 인수&#39; 값이 대신 사용</strong>됩니다.</p>
<hr>
<h3 id="언제-기본-인수를-사용해야-할까요">언제 기본 인수를 사용해야 할까요?</h3>
<p>함수에 적절한 기본값이 필요하면서도, 원한다면 언제든지 사용자가 다른 값으로 바꿀 수 있게 해주고 싶을 때 아주 좋은 선택입니다.
예를 들어, 다음은 기본 인수가 흔히 쓰일 만한 함수들입니다. (주사위를 굴리거나 로그 파일을 여는 함수)</p>
<pre><code class="language-cpp">int rollDie(int sides=6);
void openLogFile(std::string filename=&quot;default.log&quot;);</code></pre>
<p>또한, 이미 만들어진 함수에 <strong>새로운 매개변수를 추가해야 할 때</strong>도 유용합니다.
기본 인수 없이 매개변수만 달랑 추가하면, 예전에 이 함수를 쓰던 모든 코드들이(새 매개변수를 안 넘겨주니까) 에러가 나면서 고장 나 버립니다. 결국 기존 코드를 일일이 고쳐야 하죠. 하지만 <strong>새 매개변수에 기본 인수를 달아주면, 예전 코드들은 알아서 기본값을 쓰기 때문에 아무 문제 없이 작동</strong>하고, 새로운 코드에서는 원하는 값을 명시해서 넣을 수 있으니 무척 편리합니다.</p>
<hr>
<h3 id="기본-인수는-재선언할-수-없으며-사용하기-전에-선언해야-합니다">기본 인수는 재선언할 수 없으며, 사용하기 전에 선언해야 합니다</h3>
<p>한 번 선언한 기본 인수는 <strong>같은 번역 단위</strong>안에서 다시 선언할 수 없습니다.
즉, <strong>전방 선언</strong>과 <strong>함수 정의</strong>가 둘 다 있을 때,</p>
<ul>
<li>기본 인수는 <strong>전방 선언</strong>에만 넣거나</li>
<li><strong>함수 정의</strong>에만 넣어야 합니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int x, int y=4); // 전방 선언

void print(int x, int y=4) // 컴파일 에러: 기본 인수 재정의
{
    std::cout &lt;&lt; &quot;x: &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;y: &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;
}</code></pre>
<p>또한 기본 인수는 <strong>사용하기 전에 먼저 선언되어 있어야</strong> 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(int x, int y); // 전방 선언(기본 인수 없음)

int main()
{
    print(3); // 컴파일 에러: y의 기본 인수가 아직 정의되지 않음

    return 0;
}

void print(int x, int y=4)
{
    std::cout &lt;&lt; &quot;x: &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;y: &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;
}</code></pre>
<h4 id="권장-방식best-practice">권장 방식(Best practice)</h4>
<ul>
<li>함수에 전방 선언이 있다면(특히 헤더 파일에 있다면), <strong>기본 인수는 전방 선언 쪽에 넣으세요.</strong></li>
<li>전방 선언이 없다면, <strong>함수 정의에 넣으면 됩니다.</strong></li>
</ul>
<pre><code class="language-cpp">// foo.h

#ifndef FOO_H
#define FOO_H

void print(int x, int y=4);

#endif</code></pre>
<pre><code class="language-cpp">// main.cpp

#include &quot;foo.h&quot;
#include &lt;iostream&gt;

void print(int x, int y)
{
    std::cout &lt;&lt; &quot;x: &quot; &lt;&lt; x &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;y: &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;
}

int main()
{
    print(5);

    return 0;
}
</code></pre>
<hr>
<h3 id="기본-인수와-함수-오버로딩-function-overloading">기본 인수와 함수 오버로딩 (Function overloading)</h3>
<p>기본 인수가 있는 함수도 오버로딩(이름이 같은 함수를 매개변수만 다르게 여러 개 만드는 것)할 수 있습니다. 
예를 들어 다음 코드는 정상적으로 작동합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string_view&gt;

void print(std::string_view s)
{
    std::cout &lt;&lt; s &lt;&lt; &#39;\n&#39;;
}

void print(char c = &#39; &#39;)
{
    std::cout &lt;&lt; c &lt;&lt; &#39;\n&#39;;
}

int main()
{
    print(&quot;Hello, world&quot;); // print(std::string_view)를 호출함
    print(&#39;a&#39;);            // print(char)를 호출함
    print();               // print(char)를 호출함

    return 0;
}</code></pre>
<p>세 번째 <code>print()</code> 호출은 사실 <code>print(char)</code>를 부른 것입니다. 
우리가 명시적으로 빈칸인 <code>print(&#39; &#39;)</code>를 넣어서 호출한 것과 똑같이 작동하죠.</p>
<p>이제 다음 경우를 살펴봅시다.</p>
<pre><code class="language-cpp">void print(int x);                  // 시그니처: print(int)
void print(int x, int y = 10);      // 시그니처: print(int, int)
void print(int x, double y = 20.5); // 시그니처: print(int, double)</code></pre>
<p>기본값은 함수의 *<em>&#39;시그니처(함수를 구분 짓는 특징)&#39;에 포함되지 않습니다. *</em>
따라서 위 함수들은 서로 다른 매개변수 타입을 가지고 있으므로, 오버로딩된 별개의 함수들로 잘 구분됩니다.</p>
<hr>
<h3 id="기본-인수는-모호한-호출ambiguous-call을-만들-수-있습니다">기본 인수는 모호한 호출(ambiguous call)을 만들 수 있습니다</h3>
<p>기본 인수 때문에 함수 호출이 쉽게 애매해질 수 있습니다.</p>
<pre><code class="language-cpp">void foo(int x = 0) {}
void foo(double d = 0.0) {}

int main()
{
    foo(); // 모호함(어느 foo를 호출해야 할지 결정 불가)

    return 0;
}</code></pre>
<p>이 경우 컴파일러는 <code>foo()</code>가 <code>foo(0)</code>인지 <code>foo(0.0)</code>인지 판단할 수 없습니다.</p>
<hr>
<h2 id="116--함수-템플릿-function-templates">11.6 — 함수 템플릿 (Function templates)</h2>
<p>두 숫자의 최댓값을 구하는 함수를 작성한다고 가정해 봅시다. 아마 이렇게 작성할 수 있을 거예요.</p>
<pre><code class="language-cpp">int max(int x, int y) {
    return (x &lt; y) ? y : x;
    // 참고: std::max 함수가 &lt; 연산자를 사용하기 때문에 여기서도 &gt; 대신 &lt; 를 사용합니다.
}</code></pre>
<p>이 함수를 호출할 때 다른 값을 넘겨줄 수는 있지만, <strong>매개변수의 타입은 고정되어 있습니다.</strong>
즉, 이 함수에는 <code>int</code> 값만 넣을 수 있죠. 결국 이 함수는 <strong>정수(또는 정수로 자동 변환될 수 있는 타입)에만 제대로 작동</strong>합니다.</p>
<p>그렇다면 나중에 <code>double</code> 타입의 실수 두 개를 비교해서 최댓값을 찾고 싶어지면 어떻게 해야 할까요?
C++에서는 함수의 모든 매개변수 타입을 명시해야 하므로, 당장 할 수 있는 해결책은 <code>double</code> 타입을 사용하는 새로운 <code>max</code> 함수를 <strong>오버로딩하여 만드는 것뿐</strong>입니다.</p>
<pre><code class="language-cpp">double max(double x, double y) {
    return (x &lt; y) ? y : x;
}</code></pre>
<p>우리가 지원하고 싶은 매개변수 타입이 늘어날 때마다 내용이 똑같은 오버로딩 함수를 매번 만드는 것은 유지보수를 힘들게 하고, 실수를 유발하며, 프로그래밍의 핵심 원칙인 DRY 원칙을 명백히 위반하는 일입니다.</p>
<p>결국 지금 우리에게 절실히 필요한 것은, <strong>어떤 타입의 인수를 넣든 모두 처리할 수 있는 단 하나의 *<em><code>max</code> *</em>버전을 작성하는 방법</strong>입니다.</p>
<hr>
<h3 id="c-템플릿-소개">C++ 템플릿 소개</h3>
<p>C++에서 템플릿 시스템은 다양한 데이터 타입과 함께 작동할 수 있는 함수나 클래스를 만드는 과정을 훨씬 단순하게 만들고자 설계되었습니다.</p>
<p>내용이 거의 똑같은 여러 개의 함수나 클래스를 일일이 수동으로 만드는 대신, 우리는 <strong>단 하나의 템플릿만</strong> 만들면 됩니다.
일반적인 함수 정의처럼, 템플릿 정의도 함수나 클래스가 어떻게 생겼는지 형태를 설명해 줍니다.</p>
<p>하지만 모든 타입을 정확히 명시해야 하는 일반 정의와 달리, 템플릿에서는 하나 이상의 <strong>자리 표시자 타입</strong>을 사용할 수 있습니다.
<strong>자리 표시자 타입이란</strong> 템플릿을 정의할 당시에는 정확히 어떤 타입인지 알 수 없지만, 나중에(템플릿을 실제로 사용할 때) 제공될 임의의 타입을 대신하는 역할을 합니다.</p>
<p>템플릿이 한 번 정의되고 나면, 컴파일러는 <strong>이 템플릿을 사용해 필요한 만큼 오버로딩된 함수(또는 클래스)를 알아서 척척 만들어 냅니다.</strong> 
이때 각각의 함수는 서로 다른 실제 타입을 사용하게 되죠!</p>
<p><strong>결과적으로는 우리가 직접 만들었을 때와 똑같이 타입별로 거의 똑같이 생긴 여러 개의 함수나 클래스가 생겨납니다.</strong> 
하지만 <strong>우리는 단 하나의 템플릿만 만들고 관리하면 되며, 나머지를 만드는 번거로운 작업은 컴파일러가 대신</strong> 해줍니다.</p>
<hr>
<h3 id="함수-템플릿-function-templates">함수 템플릿 (Function templates)</h3>
<p><strong>함수 템플릿</strong>은 각기 다른 실제 타입을 사용하는** 하나 이상의 오버로딩된 함수를 생성하는 데 사용되는 함수 모양의 정의<strong>입니다.
다른 함수들을 만들어내는 데 사용되는 **초기 함수 템플릿을 <code>기본 템플릿</code></strong> 이라 부르고, 
이 <strong>기본 템플릿으로부터 만들어진 함수들을 <code>인스턴스화된 함수</code></strong>라고 부릅니다.</p>
<p>기본 함수 템플릿을 만들 때, 우리는 나중에 템플릿 사용자가 지정하게 될 매개변수 타입, 반환(return) 타입, 또는 함수 내부에서 쓰이는 타입 자리에 <strong><code>자리 표시자 타입</code>을 사용</strong>합니다. (기술적으로는 <strong><code>타입 템플릿 매개변수(type template parameter)</code></strong> 라 부르고, 비공식적으로는 템플릿 타입이라고 부릅니다.)</p>
<p>함수 템플릿은 예제로 배우는 것이 최고입니다. 위에서 보았던 일반적인 <code>max(int, int)</code> 함수를 함수 템플릿으로 변환해 보겠습니다. 놀라울 정도로 쉬우니 그 과정을 차근차근 설명해 드릴게요.</p>
<hr>
<h3 id="max-함수-템플릿-만들기">max() 함수 템플릿 만들기</h3>
<pre><code class="language-cpp">int max(int x, int y) {
    return (x &lt; y) ? y : x;
}</code></pre>
<p>이 함수에서는 <code>int</code> 타입을 <strong>세 번</strong> 사용했습니다. 매개변수 <code>x</code>에 한 번, 매개변수 <code>y</code>에 한 번, 그리고 함수의 반환 타입에 한 번입니다.</p>
<p><code>max()</code> 함수 템플릿을 만들기 위해 우리는 두 가지를 할 것입니다.
첫 번째로, 나중에 지정되기를 원하는 실제 타입들을 <strong>타입 템플릿 매개변수</strong>로 바꿀 것입니다. 
이 경우 우리가 바꿔야 할 타입은 <code>int</code> 하나뿐이므로, 타입 템플릿 매개변수도 하나만 있으면 됩니다. 
이 매개변수의 이름은 <code>T</code>라고 하겠습니다.</p>
<p>아래는 모든 <code>int</code> 타입을 타입 템플릿 매개변수 <code>T</code>로 바꾼, 단일 템플릿 타입을 사용하는 새로운 함수입니다.</p>
<pre><code class="language-cpp">T max(T x, T y) // T가 무엇인지 정의하지 않았기 때문에 컴파일되지 않습니다.
{
    return (x &lt; y) ? y : x;
}</code></pre>
<p>아주 좋은 출발입니다. 
하지만 컴파일러는 <code>T</code>가 무엇인지 모르기 때문에 이 코드는 컴파일되지 않습니다!
게다가 이건 여전히 일반 함수일 뿐,** 아직 함수 템플릿이 아닙니다.**</p>
<p>두 번째로, 우리는 컴파일러에게 <strong>이것은 템플릿이고, **<code>T</code></strong> 는 어떤 타입이든 대신할 수 있는 자리 표시자 역할을 하는 타입 템플릿 매개변수라는 것**을 알려줄 것입니다.</p>
<p>이 두 가지는 나중에 사용될 템플릿 매개변수들을 정의하는 <strong>템플릿 매개변수 선언(template parameter declaration)</strong>을 통해 이루어집니다. 
템플릿 매개변수 선언의 범위는 <strong>바로 뒤따라오는 함수 템플릿(또는 클래스 템플릿)에만 엄격하게 제한</strong>됩니다. 
따라서 각각의 함수 템플릿이나 클래스 템플릿은 자신만의 템플릿 매개변수 선언을 가져야 합니다.</p>
<pre><code class="language-cpp">template &lt;typename T&gt; // 이것이 T를 타입 템플릿 매개변수로 정의하는 템플릿 매개변수 선언입니다.
T max(T x, T y) // 이것은 max&lt;T&gt;를 위한 함수 템플릿 정의입니다.
{
    return (x &lt; y) ? y : x;
}</code></pre>
<p>템플릿 매개변수 선언에서는 가장 먼저 <code>template</code> 키워드를 사용하여 우리가 템플릿을 만들고 있다는 것을 컴파일러에게 알립니다.
다음으로 꺾쇠괄호 <code>&lt; &gt;</code> 안에 우리 템플릿이 사용할 모든 템플릿 매개변수를 지정합니다.
각 타입 템플릿 매개변수에는 <code>typename</code>(권장) 또는 <code>class</code> 키워드를 쓰고, 그 뒤에 매개변수의 이름(예: T)을 적어줍니다.</p>
<p>믿기지 않으시겠지만, 벌써 다 끝났습니다! 
우리는 다양한 타입의 인수를 받아들일 수 있는 템플릿 버전의 <code>max()</code> 함수를 성공적으로 만들었습니다.</p>
<hr>
<h3 id="함수-템플릿-사용하기">함수 템플릿 사용하기</h3>
<p>함수 템플릿은 사실 진짜 함수가 아닙니다. 템플릿 코드 자체는 직접 컴파일되거나 실행되지 않죠. 
대신 함수 템플릿은 한 가지 중요한 역할을 합니다. 바로 <strong>컴파일되고 실행될 &#39;진짜 함수&#39;를 붕어빵 찍어내듯 만들어내는 것</strong>입니다.</p>
<p>우리가 만든 <code>max&lt;T&gt;</code> 함수 템플릿을 사용하려면, 다음과 같은 문법으로 함수를 호출하면 됩니다.</p>
<pre><code class="language-cpp">max&lt;actual_type&gt;(arg1, arg2); // actual_type은 int나 double 같은 실제 타입을 의미합니다.</code></pre>
<p>일반적인 함수 호출과 아주 비슷하게 생겼죠? 가장 큰 차이점은 꺾쇠괄호 <code>&lt; &gt;</code> 안에 타입을 추가한다는 것입니다.
이를 <strong>템플릿 인수</strong>라고 부르며, 템플릿 타입 <code>T</code> <strong>대신 사용될 실제 타입을 지정하는 역할</strong>을 합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
T max(T x, T y)
{
    return (x &lt; y) ? y : x;
}

int main()
{
    std::cout &lt;&lt; max&lt;int&gt;(1, 2) &lt;&lt; &#39;\n&#39;; // max&lt;int&gt;(int, int) 함수를 인스턴스화(생성)하고 호출합니다.

    return 0;
}</code></pre>
<p>컴파일러가 <code>max&lt;int&gt;(1, 2)</code>라는 함수 호출을 보게 되면, 현재 <code>max&lt;int&gt;(int, int)</code>로 정의된 함수가 없다는 것을 파악합니다.
따라서 컴파일러는 우리가 만들어둔 <code>max&lt;T&gt;</code> 함수 템플릿을 사용하여 <strong>알아서 이 함수를 만들어냅니다.</strong></p>
<p>이렇게 함수 템플릿을 기반으로 특정 타입을 가진 실제 함수를 만들어내는 과정을 <strong>함수 템플릿 인스턴스화, 줄여서 인스턴스화</strong>라고 부릅니다.
함수 호출로 인해 이렇게 자동으로 인스턴스화가 일어나는 것을 <strong>암시적 인스턴스화</strong>라고 합니다.</p>
<p>기술적으로 템플릿에서 만들어진 함수를 <strong>특수화</strong>라고 부르지만, 흔히 <strong>함수 인스턴스</strong>라고도 많이 부릅니다. 
그리고 바탕이 되는 원본 템플릿은 <strong>기본 템플릿</strong>이라고 합니다.
이렇게 만들어진 함수 인스턴스는 <strong>모든 면에서 일반 함수와 똑같이 작동</strong>합니다.</p>
<p>함수가 만들어지는 과정은 단순합니다.
컴파일러가 기본 템플릿을 복사한 다음, 템플릿 타입(<code>T</code>)을 우리가 지정한 실제 타입(<code>int</code>)으로 싹 바꿔주는 것입니다.</p>
<p>그래서 우리가 <code>max&lt;int&gt;(1, 2)</code>를 호출할 때 생성되는 함수는 대략 아래와 같은 모습이 됩니다.</p>
<pre><code class="language-cpp">template&lt;&gt; // 이 부분은 일단 무시하세요
int max&lt;int&gt;(int x, int y) // 생성된 max&lt;int&gt;(int, int) 함수
{
    return (x &lt; y) ? y : x;
}</code></pre>
<p>위의 예제 코드가 모든 인스턴스화 과정을 거친 후, 컴파일러가 실제로 컴파일하게 되는 최종 모습은 다음과 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 우리 함수 템플릿에 대한 선언입니다 (이제 정의 부분은 필요 없습니다)
template &lt;typename T&gt;
T max(T x, T y);

template&lt;&gt;
int max&lt;int&gt;(int x, int y) // 템플릿을 바탕으로 생성된 max&lt;int&gt;(int, int) 함수
{
    return (x &lt; y) ? y : x;
}

int main()
{
    std::cout &lt;&lt; max&lt;int&gt;(1, 2) &lt;&lt; &#39;\n&#39;; // max&lt;int&gt;(int, int) 함수를 인스턴스화(생성)하고 호출합니다.

    return 0;
}</code></pre>
<p>중요한 점은, 함수 템플릿은 각 파일(번역 단위)에서 <strong>해당 타입으로 처음 호출될 때 단 한 번만 생성(인스턴스화)</strong>된다는 것입니다. 
그 이후에 같은 타입으로 다시 호출하면, 새로 만들지 않고 이미 만들어둔 함수를 재사용합니다. 
반대로, 함수 템플릿을 코드에 써놓기만 하고 한 번도 호출하지 않으면 함수는 아예 만들어지지 않습니다.</p>
<hr>
<h3 id="템플릿-인수-연역-template-argument-deduction">템플릿 인수 연역 (Template argument deduction)</h3>
<pre><code class="language-cpp">std::cout &lt;&lt; max&lt;int&gt;(1, 2) &lt;&lt; &#39;\n&#39;; // max&lt;int&gt;를 호출하겠다고 명시적으로 지정</code></pre>
<p>위 호출에서는 <code>T</code>를 <code>int</code>로 바꾸겠다고 꺾쇠괄호로 명시했지만, 동시에 괄호 안에도 진짜<code>int</code> 값<code>1, 2</code>을 전달하고 있습니다.
이처럼 <strong>전달하는 인수의 타입과 만들고자 하는 함수의 타입이 같을 때</strong>는 굳이 <code>&lt;int&gt;</code>처럼 타입을 명시할 필요가 없습니다. 
대신 <strong>템플릿 인수 연역</strong>이라는 기능을 통해, <strong>함수 호출에 사용된 인수를 보고 컴파일러가 스스로 적절한 타입을 추론</strong>하게 할 수 있습니다.</p>
<p>즉, 위와 같이 쓰는 대신 아래 두 가지 방법 중 하나를 사용할 수 있습니다.</p>
<pre><code class="language-cpp">std::cout &lt;&lt; max&lt;&gt;(1, 2) &lt;&lt; &#39;\n&#39;;
std::cout &lt;&lt; max(1, 2) &lt;&lt; &#39;\n&#39;;</code></pre>
<p>어느 쪽을 사용하든, 컴파일러는 우리가 실제 타입을 명시하지 않은 것을 확인하고, 전달된 인수(1, 2)를 바탕으로 타입을 추론합니다. 
이 예제에서는 인수가 모두 <code>int</code>이므로, 컴파일러는 <code>T</code>를 <code>int</code>로 바꿔서<code>max&lt;int&gt;(int, int)</code> 함수를 생성하면 매개변수 타입과 인수 타입이 딱 맞아떨어진다고 똑똑하게 판단합니다.</p>
<p>그렇다면 <code>&lt;&gt;</code>를 빈칸으로 남겨두는 것과 아예 생략하는 것의 차이는 뭘까요?
이는 컴파일러가 이름이 같은 <strong>여러 함수(오버로딩된 함수) 중 어떤 것을 호출할지 결정하는 방식</strong>과 관련이 있습니다.</p>
<ul>
<li>위쪽 방법 (빈 꺾쇠괄호 <code>&lt;&gt;</code> 사용): 컴파일러는 오직 max <strong>템플릿 함수</strong>들 중에서만 어떤 것을 호출할지 고려합니다.</li>
<li>아래쪽 방법 (꺾쇠괄호 아예 생략): 컴파일러는 max 템플릿 함수뿐만 아니라, 일반적인 <strong>일반 함수(비 템플릿 함수)</strong>인 max까지 모두 찾아봅니다. 만약 템플릿으로 만든 함수와 일반 함수가 둘 다 사용 가능할 정도로 완벽히 일치한다면, <strong>컴파일러는 일반 함수를 우선적으로 선택</strong>합니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
T max(T x, T y)
{
    std::cout &lt;&lt; &quot;called max&lt;int&gt;(int, int)\n&quot;; // 템플릿 함수가 호출됨
    return (x &lt; y) ? y : x;
}

int max(int x, int y)
{
    std::cout &lt;&lt; &quot;called max(int, int)\n&quot;; // 일반 함수가 호출됨
    return (x &lt; y) ? y : x;
}

int main()
{
    std::cout &lt;&lt; max&lt;int&gt;(1, 2) &lt;&lt; &#39;\n&#39;; // max&lt;int&gt;(int, int)를 호출합니다
    std::cout &lt;&lt; max&lt;&gt;(1, 2) &lt;&lt; &#39;\n&#39;;    // max&lt;int&gt;(int, int)를 추론합니다 (일반 함수는 고려 대상 제외)
    std::cout &lt;&lt; max(1, 2) &lt;&lt; &#39;\n&#39;;      // 일반 함수인 max(int, int)를 호출합니다

    return 0;
}</code></pre>
<p>맨 아래의 <code>max(1, 2)</code> 코드는 <strong>일반 함수를 호출할 때의 모습과 완전히 똑같죠?</strong>
실제로 대부분의 상황에서는 템플릿 함수를 호출할 때도 이처럼 가장 흔하고 익숙한 일반 함수 호출 문법을 사용합니다.</p>
<p>함수 템플릿은 여러 타입을 다루기 위해 만들어졌기 때문에 그 내용이 <strong>&#39;범용적(generic)&#39;</strong>이어야 합니다.
반면 일반 함수는 특정 타입들의 조합만 처리하면 됩니다. 
따라서 템플릿 버전보다 특정 타입에 맞게 더 최적화되거나 특별한 처리를 할 수 있죠.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 이 함수 템플릿은 여러 타입을 다룰 수 있으므로 범용적으로 구현되어 있습니다.
template &lt;typename T&gt;
void print(T x)
{
    std::cout &lt;&lt; x; // T가 평소에 출력되는 방식대로 출력합니다.
}

// 이 함수는 오직 bool 타입만 어떻게 출력할지 고려하면 되므로, bool에 특화된 처리를 할 수 있습니다.
void print(bool x)
{
    std::cout &lt;&lt; std::boolalpha &lt;&lt; x; // bool 값을 1이나 0이 아닌 true나 false로 출력합니다.
}

int main()
{
    print&lt;bool&gt;(true); // print&lt;bool&gt;(bool) 템플릿 호출 -- 1 출력
    std::cout &lt;&lt; &#39;\n&#39;;

    print&lt;&gt;(true);     // print&lt;bool&gt;(bool) 템플릿 추론 (일반 함수 제외) -- 1 출력
    std::cout &lt;&lt; &#39;\n&#39;;

    print(true);       // 일반 함수 print(bool) 호출 -- true 출력
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
특별히 일반 함수를 무시하고 무조건 템플릿 버전을 호출해야 하는 상황이 아니라면, 함수 템플릿을 호출할 때도 <strong>일반 함수 호출 문법(꺾쇠괄호 생략)을 사용하는 것을 권장</strong>합니다.</p>
</blockquote>
<hr>
<h3 id="일반-매개변수와-혼합된-함수-템플릿">일반 매개변수와 혼합된 함수 템플릿</h3>
<p>함수 템플릿을 만들 때 템플릿 매개변수 <code>T</code> 와 일반 매개변수 <code>int</code> <code>double</code> 등을 섞어서 사용할 수도 있습니다.
<strong>템플릿 매개변수는 어떤 타입이든 맞춰질 수 있고, 일반 매개변수는 일반 함수처럼 고정된 타입으로 작동</strong>합니다.</p>
<pre><code class="language-cpp">// T는 템플릿 타입 매개변수입니다.
// double은 일반 매개변수입니다.
// 이 매개변수들은 안에서 쓰이지 않으므로 굳이 이름을 짓지 않아도 됩니다.
template &lt;typename T&gt;
int someFcn(T, double)
{
    return 5;
}

int main()
{
    someFcn(1, 3.4);   // someFcn(int, double)과 일치
    someFcn(1, 3.4f);  // someFcn(int, double)과 일치 -- float(3.4f)이 double로 승급(자동 변환)됨
    someFcn(1.2, 3.4); // someFcn(double, double)과 일치
    someFcn(1.2f, 3.4); // someFcn(float, double)과 일치
    someFcn(1.2f, 3.4f); // someFcn(float, double)과 일치 -- 두 번째 float이 double로 승급됨

    return 0;
}</code></pre>
<hr>
<h3 id="인스턴스화된-함수가-항상-컴파일되는-것은-아닙니다">인스턴스화된 함수가 항상 컴파일되는 것은 아닙니다</h3>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

template &lt;typename T&gt;
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::string hello { &quot;Hello, world!&quot; };
    std::cout &lt;&lt; addOne(hello) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p>컴파일러가 <code>addOne(hello)</code>를 처리하려고 할 때, 일치하는 일반 함수는 찾지 못합니다.
하지만 <code>addOne(T)</code> 함수 템플릿이 있으니, 이걸 이용해 <code>addOne(std::string)</code> 함수를 만들어낼 수 있다고 판단하죠.
그래서 컴파일러는 다음 코드를 생성해서 컴파일하려고 시도합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;

template &lt;typename T&gt;
T addOne(T x);

template&lt;&gt;
std::string addOne&lt;std::string&gt;(std::string x)
{
    return x + 1; // 여기서 문제 발생!
}

int main()
{
    std::string hello{ &quot;Hello, world!&quot; };
    std::cout &lt;&lt; addOne(hello) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p>하지만 이 코드는 <strong>컴파일 에러를 발생</strong>시킵니다. 
왜냐하면 <code>x</code>가 문자열 <code>std::string</code> 일 때 <code>x + 1</code>을 한다는 것은 <strong>문법적으로 말이 되지 않기 때문</strong>입니다.
이 문제를 해결하는 가장 명백한 방법은 아예 <code>addOne()</code> 함수에 <code>std::string</code> 타입의 인수를 <strong>넣지 않는 것</strong>입니다.</p>
<hr>
<h3 id="인스턴스화된-함수가-의미론적으로-항상-말이-되는-것은-아닙니다">인스턴스화된 함수가 의미론적으로 항상 말이 되는 것은 아닙니다</h3>
<p>컴파일러는 문법만 맞으면 템플릿을 통해 함수를 성공적으로 만들어냅니다.
하지만 그 함수가 실제로 <strong>의미가 있는 행동(의미론적으로 올바른 행동)</strong>을 하는지까지는 검사할 능력이 없습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
T addOne(T x)
{
    return x + 1;
}

int main()
{
    std::cout &lt;&lt; addOne(&quot;Hello, world!&quot;) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p>이 예제에서는 전통적인** C언어 스타일의 문자열 리터럴에** <code>addOne()</code>을 호출하고 있습니다. 
문자열 문장에 <code>+1</code>을 더한다니, 이게 도대체 무슨 의미일까요? 아무도 모릅니다!</p>
<p>그런데 놀랍게도, <strong>C++ 문법 규칙상 문자열 리터럴에 정수 값을 더하는 행위 자체(포인터 연산)는 허용되기 때문</strong>에 위 코드는 정상적으로 컴파일이 되어버립니다. 그리고 이런 엉뚱한 결과를 출력합니다.</p>
<pre><code class="language-cpp">ello, world!</code></pre>
<blockquote>
<p><strong>고급 독자를 위한 추가 내용</strong>
때로는 컴파일러에게 <strong>&quot;특정 타입으로는 절대로 이 템플릿을 인스턴스화하지 마!&quot;라고 강제</strong>할 수 있습니다. 
함수 템플릿 특수화 기능과 <code>= delete</code> 문법을 조합하면 되는데요, 특정 타입으로 함수를 호출하면 강제로 컴파일 에러를 뿜어내게 만드는 식입니다.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;string&gt;
&gt;
template &lt;typename T&gt;
T addOne(T x)
{
    return x + 1;
}
&gt;
// 함수 템플릿 특수화를 사용하여 컴파일러에게 addOne(const char*) 호출 시 컴파일 에러를 발생시키라고 지시합니다.
// const char*는 문자열 리터럴과 일치합니다.
template &lt;&gt;
const char* addOne(const char* x) = delete;
&gt;
int main()
{
    std::cout &lt;&lt; addOne(&quot;Hello, world!&quot;) &lt;&lt; &#39;\n&#39;; // 여기서 컴파일 에러 발생!
&gt;
    return 0;
}</code></pre>
<hr>
<h3 id="함수-템플릿과-기본-인수default-arguments">함수 템플릿과 기본 인수(Default arguments)</h3>
<p>일반 함수처럼 함수 템플릿도 일반 매개변수에 대해 <strong>기본값</strong>을 가질 수 있습니다.
템플릿을 통해 생성된 모든 함수들은 똑같은 기본값을 공유해서 사용하게 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
void print(T val, int times=1) // times를 넘기지 않으면 기본값 1이 사용됨
{
    while (times--)
    {
        std::cout &lt;&lt; val;
    }
}

int main()
{
    print(5);      // 5를 1번 출력
    print(&#39;a&#39;, 3); // &#39;a&#39;를 3번 출력

    return 0;
}

출력 결과:

5aaa</code></pre>
<hr>
<h3 id="수정-가능한-정적-지역-변수가-있는-함수-템플릿-주의하기">수정 가능한 정적 지역 변수가 있는 함수 템플릿 주의하기</h3>
<p>이전 7.11 강의에서 다뤘던 &#39;정적 지역 변수&#39;를 기억하시나요? 프로그램이 실행되는 내내 값이 유지되는 변수였죠.</p>
<p>만약 함수 템플릿 안에서 이 정적 지역 변수를 사용하면 어떻게 될까요?
흥미롭게도, 템플릿으로 인해 인스턴스화된 각각의 함수들은 <strong>자신만의 독립적인 정적 지역 변수 복사본을 가지게 됩니다.</strong>
만약 이 변수가 <code>const</code>라면 큰 문제가 안 되지만, 값을 자꾸 수정하는 용도로 쓴다면 예상과 전혀 다른 결과가 나올 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 값이 변경되는 정적 지역 변수를 포함하는 함수 템플릿입니다.
template &lt;typename T&gt;
void printIDAndValue(T value)
{
    static int id{ 0 };
    std::cout &lt;&lt; ++id &lt;&lt; &quot;) &quot; &lt;&lt; value &lt;&lt; &#39;\n&#39;;
}

int main()
{
    printIDAndValue(12);
    printIDAndValue(13);

    printIDAndValue(14.5);

    return 0;
}

결과는 다음과 같습니다.

1) 12
2) 13
1) 14.5</code></pre>
<p>아마 마지막 줄이 <code>3) 14.5</code>가 될 거라고 예상하셨을 수 있습니다. 
하지만 컴파일러가 실제로 컴파일하고 실행하는 코드는 아래와 같습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
void printIDAndValue(T value);

template &lt;&gt;
void printIDAndValue&lt;int&gt;(int value)
{
    static int id{ 0 }; // &lt;int&gt; 함수만의 id
    std::cout &lt;&lt; ++id &lt;&lt; &quot;) &quot; &lt;&lt; value &lt;&lt; &#39;\n&#39;;
}

template &lt;&gt;
void printIDAndValue&lt;double&gt;(double value)
{
    static int id{ 0 }; // &lt;double&gt; 함수만의 독립적인 id
    std::cout &lt;&lt; ++id &lt;&lt; &quot;) &quot; &lt;&lt; value &lt;&lt; &#39;\n&#39;;
}

int main()
{
    printIDAndValue(12);   // printIDAndValue&lt;int&gt;() 호출 (id = 1)
    printIDAndValue(13);   // printIDAndValue&lt;int&gt;() 호출 (id = 2)

    printIDAndValue(14.5); // printIDAndValue&lt;double&gt;() 호출 (id = 1로 새로 시작!)

    return 0;
}</code></pre>
<p>보시다시피 <code>printIDAndValue&lt;int&gt;</code> 함수와 <code>printIDAndValue&lt;double&gt;</code> 함수는 이름이 <code>id</code>로 같을 뿐, 
서로 공유하지 않는 <strong>각자의 독립적인 변수를 가지고 있기 때문에 카운트가 이어지지 않는 것</strong>입니다.</p>
<hr>
<h3 id="제네릭-프로그래밍-generic-programming">제네릭 프로그래밍 (Generic programming)</h3>
<p>템플릿의 <code>T</code>와 같은 타입은 <strong>나중에 어떤 실제 자료형으로든 교체될 수 있기 때문</strong>에, 
종종 <strong>제네릭 타입(generic types), 즉 범용 타입</strong>이라고 부릅니다.</p>
<p>그리고 이렇게 특정 데이터 타입에 얽매이지 않고 여러 상황에 두루 쓰일 수 있는 템플릿 코드를 작성하는 것을 <strong>제네릭 프로그래밍(generic programming)</strong>이라고 합니다.</p>
<p>C++는 원래 자료형(타입)을 아주 깐깐하게 따지는 언어지만, 제네릭 프로그래밍을 활용하면 타입의 제약에서 벗어나 &quot;이 함수가 어떤 흐름으로 작동해야 하는지(알고리즘)&quot;나 &quot;데이터를 어떻게 다룰 것인지(자료구조)&quot;에 훨씬 더 집중할 수 있게 해줍니다.</p>
<p>추천하는 방식은 이렇습니다. </p>
<ul>
<li>처음에는 그냥 일반 함수로 먼저 만들어서 테스트해 봅니다. </li>
<li>그러다가 &quot;아, 이 함수를 다른 타입으로도 써야겠네?&quot; 하고 여러 타입의 <strong>오버로딩(중복 정의)이 필요해지는 순간이 오면</strong>,</li>
<li>그때 함수 템플릿으로 변환하시면 됩니다.</li>
</ul>
<hr>
<h2 id="118--다중-템플릿-타입을-가진-함수-템플릿">11.8 — 다중 템플릿 타입을 가진 함수 템플릿</h2>
<p>11.6 챕터에서 <strong>두 값 중 큰 값(max)</strong> 을 구하는 함수 템플릿을 만들었습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
T max(T x, T y)
{
    return (x &lt; y) ? y : x;
}

int main()
{
    std::cout &lt;&lt; max(1, 2) &lt;&lt; &#39;\n&#39;;       // max(int, int)를 인스턴스화(생성)함
    std::cout &lt;&lt; max(1.5, 2.5) &lt;&lt; &#39;\n&#39;;   // max(double, double)를 인스턴스화(생성)함

    return 0;
}</code></pre>
<p>이제 아래처럼 비슷한 코드를 보겠습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;typename T&gt;
T max(T x, T y)
{
    return (x &lt; y) ? y : x;
}

int main()
{
    std::cout &lt;&lt; max(2, 3.5) &lt;&lt; &#39;\n&#39;;  // 컴파일 오류

    return 0;
}</code></pre>
<p>놀랍게도 이 프로그램은** 컴파일되지 않습니다.** 왜 그럴까요?
호출 <code>max(2, 3.5)</code>에서는 서로 다른 타입 <code>int</code> <code>double</code>을 인자로 넘기고 있습니다.
그리고 우리는 <code>max&lt;double&gt;(...)</code> 처럼 꺾쇠로 타입을 직접 지정하지 않았죠. 그래서 컴파일러는 순서대로 이렇게 시도합니다.</p>
<ol>
<li>템플릿이 아닌 일반 함수 중에서 <code>max(int, double)</code>에 딱 맞는 것이 있는지 찾음
→ 그런 함수가 없으니 실패</li>
<li>그다음 함수 템플릿이 맞는지 <strong>템플릿 인자 추론(연역)</strong>으로 확인
→ 이것도 실패. 이유는 간단합니다. <code>T</code>는 오직 <strong>한 가지 타입</strong>만 의미할 수 있기 때문입니다.</li>
</ol>
<p>즉, <code>max&lt;T&gt;(T, T)</code>에서 두 매개변수 타입이 둘 다 <code>T</code>라면, 실제 호출에서도 <strong>두 인자는 같은 타입으로 결정되어야 합니다.</strong>
하지만 <code>int</code>와 <code>double</code>을 동시에 만족시키는 “하나의 <code>T</code>”는 존재할 수 없죠.</p>
<blockquote>
<p><strong>왜 컴파일러가 자동으로 double로 맞춰주지 않을까?</strong></p>
</blockquote>
<p>“그럼 <code>max&lt;double&gt;(double, double)</code>를 만들고, <code>2(int)</code>를 <code>double</code>로 바꿔서 쓰면 되지 않나?”라고 생각할 수 있습니다.
하지만 답은 이렇습니다.</p>
<blockquote>
</blockquote>
<ul>
<li>타입 변환(예: <code>int</code> → <code>double</code>) 은 “함수 오버로드를 고르는 단계”에서만 일어나고,</li>
<li>** 템플릿 인자 추론 단계에서는 타입 변환을 하지 않습니다.**<blockquote>
</blockquote>
이렇게 설계된 이유는 최소 두 가지입니다.</li>
</ul>
<ol>
<li>규칙을 단순하게 만들기 위해서: 정확히 맞으면 되고, 아니면 안 된다</li>
<li>“두 매개변수 타입이 반드시 같아야 한다” 같은 템플릿을 만들 수 있게 하기 위해서(위 예시처럼)<blockquote>
</blockquote>
그래서 다른 해결책이 필요합니다. 이 문제는 (최소) 3가지 방법으로 해결할 수 있습니다.</li>
</ol>
<blockquote>
<p><strong>해결책 1: <code>static_cast</code> 로 호출자가 타입을 맞추기</strong>
첫 번째 방법은 <strong>호출하는 쪽</strong>에서 타입을 같게 만들어주는 것입니다.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt; 
template &lt;typename T&gt;
T max(T x, T y)
{
    return (x &lt; y) ? y : x;
}
&gt; 
int main()
{
    std::cout &lt;&lt; max(static_cast&lt;double&gt;(2), 3.5) &lt;&lt; &#39;\n&#39;; // int를 double로 바꿔서 max(double, double)을 호출
&gt; 
    return 0;
}</code></pre>
<p>이제 두 인자가 모두 <code>double</code>이므로, 컴파일러는 <code>max(double, double)</code>을 문제없이 만들어서 사용할 수 있습니다.</p>
<blockquote>
<p><strong>해결책 2: 템플릿 타입을 “명시적으로” 지정하기</strong></p>
</blockquote>
<p>만약 <strong>템플릿이 아니라 일반 함수</strong> <code>max(double, double)</code>가 있었다면, 아래 호출은 자동으로 <code>int</code> → <code>double</code> 변환이 일어나서 잘 동작합니다.</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt; 
double max(double x, double y)
{
    return (x &lt; y) ? y : x;
}
&gt; 
int main()
{
    std::cout &lt;&lt; max(2, 3.5) &lt;&lt; &#39;\n&#39;; // int 인자는 double로 변환됨
&gt; 
    return 0;
}</code></pre>
<p>하지만 템플릿 인자 추론 중에는 변환을 안 한다고 했죠.
대신, <strong>추론을 쓰지 않고</strong> 우리가 템플릿 타입을 직접 지정하면 됩니다.</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt; 
template &lt;typename T&gt;
T max(T x, T y)
{
    return (x &lt; y) ? y : x;
}
&gt; 
int main()
{
    // 타입을 double로 명시했으므로, 컴파일러는 템플릿 인자 추론을 하지 않음
    std::cout &lt;&lt; max&lt;double&gt;(2, 3.5) &lt;&lt; &#39;\n&#39;;
&gt; 
    return 0;
}</code></pre>
<blockquote>
</blockquote>
<p>여기서는 <code>T</code>를<code>double</code>로 “확정”했기 때문에, 컴파일러는<code>max&lt;double&gt;(double, double)</code>를 만들고, 맞지 않는 인자는 <strong>그때</strong> 자동 변환합니다. 그래서 <code>2</code>가 <code>double</code>로 바뀌어 들어갑니다.</p>
<blockquote>
</blockquote>
<p><code>static_cast</code>보단 읽기 좋지만, 여전히 “호출할 때 타입을 생각해야 하는” 불편함이 남습니다.</p>
<blockquote>
<p><strong>해결책 3: 템플릿 타입을 2개 이상 사용하기</strong></p>
</blockquote>
<p>문제의 뿌리는 이것입니다.</p>
<blockquote>
</blockquote>
<ul>
<li>템플릿 타입 매개변수를 <code>T</code> 하나만 만들었고,</li>
<li>두 매개변수 모두 <code>T</code>로 선언해서 <strong>둘의 타입이 같아야만</strong> 했다는 점<blockquote>
</blockquote>
그래서 가장 좋은 해결은, <strong>매개변수들이 서로 다른 타입이 될 수 있도록 템플릿 타입 매개변수를 2개로 늘리는 것</strong>입니다.<blockquote>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt; 
template &lt;typename T, typename U&gt; // T와 U, 두 개의 템플릿 타입 매개변수를 사용
T max(T x, U y) // x는 T 타입, y는 U 타입으로 각각 따로 결정될 수 있음
{
  return (x &lt; y) ? y : x; // 어라? 여기서 축소 변환(narrowing conversion) 문제가 생김
}
&gt; 
int main()
{
  std::cout &lt;&lt; max(2, 3.5) &lt;&lt; &#39;\n&#39;; // max&lt;int, double&gt;로 결정됨
&gt; 
  return 0;
}</code></pre>
<blockquote>
</blockquote>
이제 <code>x는 T</code>, <code>y는 U</code>이므로 서로 독립적으로 타입이 결정됩니다.
<code>max(2, 3.5)</code>를 호출하면 <code>T=int</code>, <code>U=double</code>이 가능하니 컴파일러는 <code>max&lt;int, double&gt;(int, double)</code>를 만들어줍니다.<blockquote>
<blockquote>
<p><strong>핵심 포인트(Key insight)</strong>
<code>T</code>와 <code>U</code>는 서로 독립적인 템플릿 매개변수이므로, <strong>서로 다른 타입으로도</strong> 결정될 수 있고, 같은 타입으로도 결정될 수 있습니다.</p>
</blockquote>
<blockquote>
<p><strong>그런데… 실행 결과가 이상하다?</strong></p>
</blockquote>
<p>위 코드를 컴파일/실행하면 결과가 이렇게 나올 수 있습니다.</p>
<blockquote>
</blockquote>
</blockquote>
<pre><code class="language-cpp">3</code></pre>
<blockquote>
<blockquote>
</blockquote>
<p><code>2</code>와 <code>3.5</code>의 최댓값이 <code>3</code>이라니, 왜죠?</p>
<blockquote>
</blockquote>
<p>조건 연산자 <code>?:</code>는 조건 부분을 제외한 두 값(여기서는 <code>y</code>와 <code>x</code>)이 <strong>같은 공통 타입</strong>으로 맞춰져야 합니다. 
이 공통 타입은 “보통의 산술 변환 규칙”(10.5 챕터)으로 결정됩니다.</p>
<blockquote>
</blockquote>
</blockquote>
</li>
<li><code>int</code>와 <code>double</code>의 공통 타입은 <code>double</code></li>
<li>그래서 <code>(x &lt; y) ? y : x</code>의 결과 자체는 <code>double</code> 값 <code>3.5</code>가 됩니다(여기까진 정상)<blockquote>
<blockquote>
</blockquote>
<p>문제는 <strong>함수의 반환 타입을 <code>T</code>로 선언</strong>했다는 점입니다.
<code>T=int</code>, <code>U=double</code>인 경우 함수 반환 타입은 <code>int</code>가 되고, <code>3.5가 int</code>로 바뀌면서 <code>3</code>으로 잘리는 축소 변환이 발생합니다.</p>
<blockquote>
</blockquote>
<p>그럼 반환 타입을 <code>U</code>로 바꾸면 될까요? 그것도 완벽한 해결은 아닙니다. 예를 들어 <code>max(3.5, 2)</code>라면 <code>U</code>가 <code>int</code>가 되어 비슷한 문제가 다시 생길 수 있습니다.</p>
<blockquote>
<p><strong>반환 타입을 <code>auto</code> 로 두고 컴파일러가 추론하게 하기</strong>
이럴 때는 반환 타입 추론 <code>auto</code> 이 유용합니다. <code>return</code> 문을 보고 컴파일러가 반환 타입을 정하도록 맡기는 거죠.</p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt;&gt;
template &lt;typename T, typename U&gt;
auto max(T x, U y) // 반환 타입을 컴파일러가 알아서 결정하게 함
{
  return (x &lt; y) ? y : x;
}
&gt;&gt;
int main()
{
  std::cout &lt;&lt; max(2, 3.5) &lt;&lt; &#39;\n&#39;;
&gt;&gt;
  return 0;
}</code></pre>
이제 서로 다른 타입을 넣어도 잘 동작합니다.<blockquote>
<blockquote>
</blockquote>
<p>단, <code>auto</code> 반환 타입 함수는 <strong>함수 구현(정의)이 먼저 필요</strong>합니다.
반환 타입을 알려면 컴파일러가 함수 내용을 봐야 하므로, 선언만 앞에 두는 전방 선언만으로는 부족합니다.</p>
</blockquote>
</li>
</ul>
<hr>
<h3 id="축약형-함수-템플릿-c20-표준">축약형 함수 템플릿 (C++20 표준)</h3>
<p>C++20부터는 <code>auto</code>를 매개변수 타입으로 쓰면, <strong>컴파일러가 자동으로 함수 템플릿으로 바꿔줍니다.</strong>
이 방식을 <strong>축약 함수 템플릿</strong>이라고 합니다.</p>
<pre><code class="language-cpp">auto max(auto x, auto y)
{
    return (x &lt; y) ? y : x;
}</code></pre>
<p>이 코드는 C++20에서 아래와 같은 의미입니다.</p>
<pre><code class="language-cpp">template &lt;typename T, typename U&gt;
auto max(T x, U y)
{
    return (x &lt; y) ? y : x;
}</code></pre>
<p>즉, 우리가 위에서 만든 “서로 다른 타입을 받을 수 있는 max”와 같습니다.
매개변수마다 독립적인 타입이 필요할 때는 이런 형태가 <strong>더 짧고 읽기 쉬워서 선호</strong>됩니다.</p>
<p>하지만, 만약 두 개 이상의 매개변수가 <strong>반드시 똑같은 타입이어야 한다면</strong> 이 축약형 문법으로는 표현하기가 무척 까다롭습니다.
다시 말해, 아래와 같은 코드를 간결하게 뚝딱 만들어내는 축약형 템플릿 문법은 존재하지 않습니다.</p>
<pre><code class="language-cpp">template &lt;typename T&gt;
T max(T x, T y) // 두 매개변수가 반드시 동일한 타입이어야 함
{
    return (x &lt; y) ? y : x;
}</code></pre>
<blockquote>
<p><strong>모범 사례 (Best practice)</strong>
매개변수가 하나뿐이거나, 여러 매개변수가 각자 독립적인 타입이어도 상관없다면 축약형 함수 템플릿(<code>auto</code> 매개변수)을 적극 활용하세요. (단, 컴파일러 언어 표준 설정이 C++20 이상이어야 합니다).</p>
</blockquote>
<hr>
<h3 id="함수-템플릿도-오버로딩할-수-있다">함수 템플릿도 오버로딩할 수 있다</h3>
<p>일반 함수처럼 함수 템플릿도 오버로딩할 수 있습니다. 
오버로드들은 템플릿 타입 개수나 매개변수 개수/타입이 달라도 됩니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 같은 타입 두 값을 더하기
template &lt;typename T&gt;
auto add(T x, T y)
{
    return x + y;
}

// 다른 타입 두 값을 더하기
// C++20부터는 auto add(auto x, auto y)로도 가능
template &lt;typename T, typename U&gt;
auto add(T x, U y)
{
    return x + y;
}

// 어떤 타입이든 3개 값을 더하기
// C++20부터는 auto add(auto x, auto y, auto z)로도 가능
template &lt;typename T, typename U, typename V&gt;
auto add(T x, U y, V z)
{
    return x + y + z;
}

int main()
{
    std::cout &lt;&lt; add(1.2, 3.4) &lt;&lt; &#39;\n&#39;; // add&lt;double&gt;()를 인스턴스화하고 호출
    std::cout &lt;&lt; add(5.6, 7) &lt;&lt; &#39;\n&#39;;   // add&lt;double, int&gt;()를 인스턴스화하고 호출
    std::cout &lt;&lt; add(8, 9, 10) &lt;&lt; &#39;\n&#39;; // add&lt;int, int, int&gt;()를 인스턴스화하고 호출

    return 0;
}</code></pre>
<p>여기서 흥미로운 점 하나</p>
<ul>
<li><code>add(1.2, 3.4)</code> 호출은 <code>add&lt;T&gt;(T, T)</code>도 맞고 <code>add&lt;T, U&gt;(T, U)</code>도 맞을 수 있습니다.</li>
<li>그런데 컴파일러는 보통 <strong>더 “제약이 강한(더 구체적인)” 템플릿</strong>을 우선합니다.<ul>
<li><code>add&lt;T&gt;(T, T)</code>는 “두 매개변수 타입이 같아야 한다”는 제약이 있어서 더 구체적이므로 이쪽이 선택됩니다.</li>
</ul>
</li>
</ul>
<p>여러 템플릿 후보 중 어떤 것을 더 우선할지 결정하는 규칙을 <strong>함수 템플릿의 부분 순서(partial ordering of function templates)</strong> 라고 부릅니다.</p>
<p>만약 여러 템플릿이 다 맞아 보이는데, <strong>컴파일러가 어느 쪽이 더 구체적인지 판단할 수 없으면 모호한 호출로 컴파일 오류가 납니다.</strong></p>
<hr>
<h2 id="119--비타입-템플릿-매개변수-non-type-template-parameters">11.9 — 비타입 템플릿 매개변수 (Non-type template parameters)</h2>
<p>이전 강의에서는 <strong>타입 템플릿 매개변수</strong>를 사용하는 함수 템플릿을 만드는 방법을 배웠습니다.
타입 템플릿 매개변수는 “나중에 실제 타입이 들어올 자리”를 미리 만들어 두는 타입용 빈칸이라고 생각하면 됩니다.</p>
<p>그런데 템플릿 매개변수는 타입만 받을 수 있는 게 아닙니다. 
알아두면 좋은 또 다른 종류가 있는데, 바로 <strong>비타입 템플릿 매개변수</strong>입니다.</p>
<h3 id="비타입-템플릿-매개변수란">비타입 템플릿 매개변수란?</h3>
<p>비타입 템플릿 매개변수는 “타입”이 아니라, <strong>특정 타입을 가진 <code>constexpr</code> 값</strong>이 들어올 자리를 만드는 템플릿 매개변수입니다.</p>
<p>즉,</p>
<ul>
<li>타입 템플릿 매개변수: “타입”이 들어올 자리</li>
<li>비타입 템플릿 매개변수: “컴파일 타임에 결정되는 값(<code>constexpr</code>)”이 들어올 자리</li>
</ul>
<p>비타입 템플릿 매개변수는 다음 같은 타입을 사용할 수 있습니다.</p>
<ul>
<li>정수형(integral type)</li>
<li>열거형(enumeration type)</li>
<li>std::nullptr_t</li>
<li>부동소수점 타입(C++20부터)</li>
<li>어떤 객체에 대한 포인터 또는 참조</li>
<li>어떤 함수에 대한 포인터 또는 참조</li>
<li>멤버 함수에 대한 포인터 또는 참조</li>
<li>리터럴 클래스 타입(C++20부터)</li>
</ul>
<hr>
<h3 id="stdbitset에서-이미-봤던-예">std::bitset에서 이미 봤던 예</h3>
<p><code>std::bitset</code>을 배울 때 비타입 템플릿 매개변수를 이미 한 번 봤습니다.</p>
<pre><code class="language-cpp">#include &lt;bitset&gt;

int main()
{
    std::bitset&lt;8&gt; bits{ 0b0000&#39;0101 }; // &lt;8&gt;은 비타입 템플릿 매개변수입니다

    return 0;
}</code></pre>
<p><code>std::bitset</code>에서는 <code>&lt;8&gt;</code>이 “비트를 몇 개 저장할지”를 알려줍니다.
즉, 비트 개수는 컴파일 타임에 정해져야 하므로 <code>constexpr</code> 값이 필요하고, 그래서 <strong>비타입 템플릿 매개변수</strong>가 쓰입니다.</p>
<hr>
<h3 id="우리가-직접-비타입-템플릿-매개변수-만들기">우리가 직접 비타입 템플릿 매개변수 만들기</h3>
<p>아래는 <code>int</code> 비타입 템플릿 매개변수를 쓰는 아주 간단한 함수 예시입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

template &lt;int N&gt; // int 타입의 비타입 템플릿 매개변수 N 선언
void print()
{
    std::cout &lt;&lt; N &lt;&lt; &#39;\n&#39;; // 여기서 N 값을 사용
}

int main()
{
    print&lt;5&gt;(); // 5가 비타입 템플릿 인자입니다

    return 0;
}

출력:
5</code></pre>
<p>설명:</p>
<ul>
<li><code>template &lt;int N&gt;</code>에서 <code>N</code>은 <code>int</code> 타입 값을 담는 자리입니다.</li>
<li><code>print()</code> 함수 안에서는 <code>N</code>을 그냥 값처럼 사용할 수 있습니다.</li>
<li><code>print&lt;5&gt;()</code>를 호출하면, 컴파일러는 사실상 이런 함수를 “인스턴스화해서” 사용합니다:</li>
</ul>
<pre><code class="language-cpp">template &lt;&gt;
void print&lt;5&gt;()
{
    std::cout &lt;&lt; 5 &lt;&lt; &#39;\n&#39;;
}</code></pre>
<p>실행 중에 <code>main()</code>에서 이 함수가 호출되면 <code>5</code>가 출력되고, 프로그램이 끝납니다. 간단하죠?</p>
<p>참고로, 타입 템플릿 매개변수에서 첫 번째 타입 이름으로 <code>T</code>를 자주 쓰듯이, 
<strong><code>int</code> 비타입 템플릿 매개변수 이름으로는 관례적으로 <code>N</code></strong>을 많이 씁니다.</p>
<hr>
<h3 id="비타입-템플릿-매개변수는-언제-유용할까">비타입 템플릿 매개변수는 언제 유용할까?</h3>
<p>C++20 기준으로, 함수의 매개변수는 <code>constexpr</code>로 만들 수 없습니다.
일반 함수나 <code>constexpr</code> 함수 모두 마찬가지이며(실행 시간에도 실행될 수 있어야 하므로 당연합니다), 
놀랍게도 무조건 컴파일 타임에 실행되어야 하는 <code>consteval</code> 함수조차 그렇습니다.</p>
<pre><code class="language-cpp">#include &lt;cassert&gt;
#include &lt;cmath&gt; // std::sqrt 사용을 위해
#include &lt;iostream&gt;

double getSqrt(double d)
{
    assert(d &gt;= 0.0 &amp;&amp; &quot;getSqrt(): d는 음수가 아니어야 합니다&quot;);

    // 위 assert 구문은 디버그 빌드가 아닐 경우(release 모드 등) 아마 컴파일에서 제외될 것입니다.
    if (d &gt;= 0)
        return std::sqrt(d);이 프로그램을 실행하면 getSqrt(-5.0) 호출에서 런타임 assert가 터집니다.

    return 0.0;
}

int main()
{
    std::cout &lt;&lt; getSqrt(5.0) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; getSqrt(-5.0) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p>이 프로그램을 실행하면 <code>getSqrt(-5.0)</code> 호출에서 런타임 <code>assert</code>가 터집니다.
이것도 아무것도 없는 것보다는 낫지만, <code>-5.0</code>은 리터럴이라 사실상 컴파일 타임에 알 수 있는 값이니, 
가능하다면 <code>static_assert</code>로 컴파일 타임에 잡아내는 편이 더 좋겠죠.</p>
<p>하지만 <code>static_assert</code>는 상수 표현식이 필요하고, 
함수 매개변수 <code>d</code>는 <code>constexpr</code>가 될 수 없어서 <code>static_assert(d &gt;= 0.0)</code> 같은 걸 할 수 없습니다.</p>
<p>그런데 <strong>함수 매개변수를 비타입 템플릿 매개변수로</strong> 바꾸면, 우리가 원하는 걸 할 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;cmath&gt; // for std::sqrt
#include &lt;iostream&gt;

template &lt;double D&gt; // 부동소수점 비타입 매개변수는 C++20 필요
double getSqrt()
{
    static_assert(D &gt;= 0.0, &quot;getSqrt(): D must be non-negative&quot;);

    if constexpr (D &gt;= 0) // 이번 예제에서는 constexpr는 그냥 무시해도 됩니다
        return std::sqrt(D); // 이상하게도 std::sqrt는 (C++26 전까지) constexpr 함수가 아닙니다

    return 0.0;
}

int main()
{
    std::cout &lt;&lt; getSqrt&lt;5.0&gt;() &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; getSqrt&lt;-5.0&gt;() &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 10]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-10</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-10</guid>
            <pubDate>Tue, 17 Feb 2026 17:21:37 GMT</pubDate>
            <description><![CDATA[<h2 id="101---암시적-형-변환-implicit-type-conversion">10.1 —  암시적 형 변환 (Implicit type conversion)</h2>
<p>우리는 챕터 4에서 <strong>형 변환</strong>에 대해 소개한 바 있습니다. 해당 레슨의 가장 중요한 점을 요약하면 다음과 같습니다.</p>
<ul>
<li><strong>하나의 타입에서 다른 타입으로 데이터를 변환하는 과정</strong>을 <strong>형 변환</strong>이라고 합니다.</li>
<li><strong>암시적 형 변환</strong>은 특정 데이터 타입이 필요한 곳에 다른 데이터 타입이 제공되었을 때 <strong>컴파일러에 의해 자동으로 수행</strong>됩니다.</li>
<li><strong>명시적 형 변환</strong>은 <code>static_cast</code>와 같은 <code>캐스트(cast) 연산자</code>를 사용하여 <strong>개발자가 직접 요청하는 변환</strong>입니다.</li>
<li>형 변환은 <strong>원래 데이터</strong>를 직접 바꾸는 것이 아니라, <strong>그 데이터를 이용해 새로운 형의 값을 만들어내는 것</strong>입니다.</li>
<li>값을 다른 타입의 값으로 변환할 때, 변환 과정은 그 결과를 담을 대상 타입의 <strong>임시 객체를 생성</strong>합니다.</li>
</ul>
<hr>
<h3 id="왜-변환이-필요한가">왜 변환이 필요한가</h3>
<p>객체의 값은 메모리에 비트들의 연속으로 저장됩니다. 
그리고 <strong>데이터 타입은 그 비트들을 어떤 값으로 해석해야 하는지 컴파일러에게 알려주는 역할</strong>을 합니다.</p>
<p>데이터 타입이 다르면 같은 숫자라도 메모리에 저장되는 비트 형태는 달라질 수 있습니다.</p>
<p>예를 들어, 정수 <code>3</code>은
<code>0000 0000 0000 0000 0000 0000 0000 0011</code>
과 같은 비트 형태로 저장될 수 있습니다.</p>
<p>반면, 실수 <code>3.0</code>은
<code>0100 0000 0100 0000 0000 0000 0000 0000</code>
처럼 전혀 다른 비트 형태로 저장됩니다.</p>
<p>즉, 값이 비슷해 보여도 <strong>데이터 타입이 다르면 메모리에 저장되는 방식이 달라집니다.</strong>
그렇다면 다음과 같은 코드를 작성하면 어떻게 될까요?</p>
<pre><code class="language-cpp">float f{ 3 }; // float 변수를 int 3으로 초기화</code></pre>
<p>컴파일러는 <code>int</code> 타입의 값 <code>3</code>이 사용하는 비트들을 그대로 <code>float</code> 변수의 메모리에 복사할 수 없습니다.
만약 그렇게 한다면, 그 비트들은 <strong>원래 정수를 나타내는 형태</strong>인데, 나중에 <code>float</code> 변수로 읽을 때는 <strong>부동 소수점 방식으로 해석</strong>됩니다.
즉, <strong>정수 기준으로 저장된 비트를 실수 기준으로 해석하게 되므로</strong>, 결과는 <code>3.0</code>이 아니라 전혀 다른 이상한 값이 됩니다.</p>
<hr>
<h3 id="암시적-형-변환이-발생하는-경우">암시적 형 변환이 발생하는 경우</h3>
<p>암시적 형 변환은 다음과 같이 <strong>어떤 타입이 요구되는 상황에서 다른 타입이 제공될 때 컴파일러가 자동으로 수행</strong>합니다.
<strong>C++에서 일어나는 형 변환의 대다수는 이 암시적 형 변환</strong>입니다. 구체적으로 다음과 같은 상황에서 발생합니다.</p>
<blockquote>
<p><strong>다른 데이터 타입의 값으로 변수를 초기화하거나 값을 할당할 때</strong></p>
</blockquote>
<pre><code class="language-cpp">double d{ 3 }; // int 값 3이 double 타입으로 암시적 변환됨
d = 6; // int 값 6이 double 타입으로 암시적 변환됨</code></pre>
<blockquote>
<p><strong>함수의 반환 타입과 반환 값 타입이 다를 때</strong></p>
</blockquote>
<pre><code class="language-cpp">float doSomething(){
    return 3.0; // double 값 3.0이 float 타입으로 암시적 변환됨
}</code></pre>
<blockquote>
<p><strong>서로 다른 타입의 피연산자를 사용하는 경우</strong></p>
</blockquote>
<pre><code class="language-cpp">double division{ 4.0 / 3 }; // int 값 3이 double 타입으로 암시적 변환됨</code></pre>
<blockquote>
<p><strong>if 문에서 불리언(Boolean)이 아닌 값을 사용할 때</strong></p>
</blockquote>
<pre><code class="language-cpp">if (5) // int 값 5가 bool 타입으로 암시적 변환됨
{}</code></pre>
<blockquote>
<p><strong>함수에 전달된 인자가 함수 매개변수와 다른 타입일 때</strong></p>
</blockquote>
<pre><code class="language-cpp">void doSomething(long l){}
doSomething(3); // int 값 3이 long 타입으로 암시적 변환됨</code></pre>
<hr>
<h3 id="컴파일러는-어떻게-변환-방법을-알까">컴파일러는 어떻게 변환 방법을 알까?</h3>
<p>C++ 표준에는 <strong>표준 변환</strong>이라는 규칙 모음이 정의되어 있습니다.
이 규칙은 다양한 <strong>기본 타입들</strong>이 어떻게 다른 타입으로 변환될 수 있는지를 명시합니다.
<code>배열</code> <code>참조</code> <code>포인터</code> <code>열거형</code> 을 포함한 <strong>특정 복합 타입들의 변환도 포함이며 이는 나중에 배웁니다.</strong>
컴파일러는 이 규칙을 사용하여 <strong>자동으로 변환을 수행</strong>합니다.</p>
<p>C++23 기준으로** 총 14가지의 서로 다른 표준 변환이 존재<strong>하며, 이를 **크게 5가지 범주로</strong> 나눌 수 있습니다.</p>
<table>
<thead>
<tr>
<th>범주 (Category)</th>
<th>의미 (Meaning)</th>
<th>링크 (Link)</th>
</tr>
</thead>
<tbody><tr>
<td><strong>숫자 승격 (Numeric promotions)</strong></td>
<td>작은 정수 타입을 <code>int</code>나 <code>unsigned int</code>로, <code>float</code>를 <code>double</code>로 변환</td>
<td>10.2 -- 부동 소수점 및 정수 승격</td>
</tr>
<tr>
<td><strong>숫자 변환 (Numeric conversions)</strong></td>
<td>승격(promotion)이 아닌 다른 정수 및 부동 소수점 변환</td>
<td>10.3 -- 숫자 변환</td>
</tr>
<tr>
<td><strong>한정자 변환 (Qualification conversions)</strong></td>
<td><code>const</code> 또는 <code>volatile</code>을 추가하거나 제거하는 변환</td>
<td></td>
</tr>
<tr>
<td><strong>값 변환 (Value transformations)</strong></td>
<td>표현식의 값 카테고리(value category)를 변경하는 변환</td>
<td>12.2 -- 값 카테고리 (lvalues 및 rvalues)</td>
</tr>
<tr>
<td><strong>포인터 변환 (Pointer conversions)</strong></td>
<td><code>std::nullptr</code>에서 포인터 타입으로, 또는 포인터 타입에서 다른 포인터 타입으로의 변환</td>
<td></td>
</tr>
</tbody></table>
<blockquote>
<p><strong>숫자 승격 (Numeric promotions)</strong>
<code>작은 타입</code>을 <code>더 큰 타입</code>으로 변환합니다.</p>
</blockquote>
<pre><code class="language-cpp">char → int
float → double</code></pre>
<blockquote>
<p><strong>숫자 변환 (Numeric conversions)</strong>
<code>숫자 승격</code>이 아닌 <code>다른 타입</code>으로 변환합니다.</p>
</blockquote>
<pre><code class="language-cpp">int → float
double → int</code></pre>
<blockquote>
<p><code>한정자 변환</code> <code>값 변환</code> <code>포인터 변환</code> 은 해당 개념을 배운 후 후술합니다.</p>
</blockquote>
<p>예를 들어, <code>int</code> 값을 <code>float</code> 값으로 변환하는 것은 <strong>숫자 변환</strong> 범주에 속하므로, 
<strong>컴파일러는 단순히</strong> <code>int</code>에서 <code>float</code>으로의 <strong>숫자 변환 규칙을 적용하여 작업을 수행</strong>합니다.
이 중에서 <strong><code>숫자 변환</code></strong>과 <strong><code>숫자 승격</code></strong>이 <strong>가장 중요하며</strong>, 이후 강의에서 자세히 다룹니다.</p>
<hr>
<h3 id="심화-학습자를-위한-전체-표준-변환-목록">심화 학습자를 위한 전체 표준 변환 목록</h3>
<p>전체 표준 변환 규칙의 목록은 다음과 같습니다. 가볍게 참고만 하셔도 좋습니다.</p>
<table>
<thead>
<tr>
<th>범주</th>
<th>표준 변환</th>
<th>설명</th>
<th>참고</th>
</tr>
</thead>
<tbody><tr>
<td>값 변환</td>
<td>Lvalue-to-rvalue</td>
<td>좌측값(lvalue) 표현식을 우측값(rvalue) 표현식으로 변환</td>
<td>12.2 -- 값 카테고리</td>
</tr>
<tr>
<td>값 변환</td>
<td>Array-to-pointer</td>
<td>C 스타일 배열을 첫 번째 배열 요소에 대한 포인터로 변환 (배열 붕괴, array decay)</td>
<td>17.8 -- C 스타일 배열 붕괴</td>
</tr>
<tr>
<td>값 변환</td>
<td>Function-to-pointer</td>
<td>함수를 함수 포인터로 변환</td>
<td>20.1 -- 함수 포인터</td>
</tr>
<tr>
<td>값 변환</td>
<td>Temporary materialization</td>
<td>값을 임시 객체로 변환</td>
<td></td>
</tr>
<tr>
<td>한정자 변환</td>
<td>Qualification conversion</td>
<td>타입에서 <code>const</code>나 <code>volatile</code>을 추가하거나 제거</td>
<td></td>
</tr>
<tr>
<td>숫자 승격</td>
<td>Integral promotions</td>
<td>더 작은 정수 타입을 <code>int</code>나 <code>unsigned int</code>로 변환</td>
<td>10.2 -- 부동 소수점 및 정수 승격</td>
</tr>
<tr>
<td>숫자 승격</td>
<td>Floating point promotions</td>
<td><code>float</code>를 <code>double</code>로 변환</td>
<td>10.2 -- 부동 소수점 및 정수 승격</td>
</tr>
<tr>
<td>숫자 변환</td>
<td>Integral conversions</td>
<td>정수 승격이 아닌 정수 변환</td>
<td>10.3 -- 숫자 변환</td>
</tr>
<tr>
<td>숫자 변환</td>
<td>Floating point conversions</td>
<td>부동 소수점 승격이 아닌 부동 소수점 변환</td>
<td>10.3 -- 숫자 변환</td>
</tr>
<tr>
<td>숫자 변환</td>
<td>Integral-floating conversions</td>
<td>정수와 부동 소수점 타입 간의 변환</td>
<td>10.3 -- 숫자 변환</td>
</tr>
<tr>
<td>숫자 변환</td>
<td>Boolean conversions</td>
<td>정수, 범위 없는 열거형, 포인터, 또는 멤버 포인터를 bool로 변환</td>
<td>4.10 -- if 문 소개</td>
</tr>
<tr>
<td>포인터 변환</td>
<td>Pointer conversions</td>
<td><code>std::nullptr</code>을 포인터로, 또는 포인터를 void 포인터나 기본(base) 클래스 포인터로 변환</td>
<td></td>
</tr>
<tr>
<td>포인터 변환</td>
<td>Pointer-to-member conversions</td>
<td><code>std::nullptr</code>을 멤버 포인터로 변환하거나, 기본 클래스의 멤버 포인터를 파생 클래스의 멤버 포인터로 변환</td>
<td></td>
</tr>
<tr>
<td>포인터 변환</td>
<td>Function pointer conversions</td>
<td>noexcept-함수 포인터를 일반 함수 포인터로 변환</td>
<td></td>
</tr>
</tbody></table>
<hr>
<h3 id="형-변환은-실패할-수도-있습니다">형 변환은 실패할 수도 있습니다</h3>
<p>암시적이든 명시적이든 <strong>형 변환</strong>이 호출되면, <strong>컴파일러는 변환이 가능한지 먼저 확인</strong>합니다.
유효한 변환 규칙을 찾으면 변환된 새로운 값을 생성합니다. 유효한 변환 규칙을 찾지 못하면 컴파일 오류가 발생합니다.</p>
<p>예를 들어</p>
<pre><code class="language-cpp">int main(){
    int x { &quot;14&quot; };
    return 0;
}</code></pre>
<p>문자열 <code>&quot;14&quot;</code>을 <code>int</code>로 변환하는 <strong>표준 규칙이 없기 때문에 컴파일 오류가 발생</strong>합니다.
<br>
어떤 경우에는 <strong>특정 문법이 일부 형 변환을 아예 차단</strong>하기도 합니다.</p>
<pre><code class="language-cpp">int x { 3.5 }; // 중괄호 초기화는 데이터 손실이 발생하는 변환을 허용하지 않습니다</code></pre>
<p>컴파일러는 <code>double</code> 값을 <code>int</code> 값으로 변환하는 방법 자체는 알고 있습니다. 
하지만 <strong>중괄호 초기화</strong>를 사용할 때는 데이터가 잘려나가는 <strong>축소 변환</strong>이 언어 규칙상 엄격히 금지되어 있어 컴파일이 거부됩니다.</p>
<p>또한, <strong>가능한 형 변환 후보가 여러 개 있어서</strong> 컴파일러가 도대체 어떤 것을 선택해야 할지 <strong>모호해하는 경우</strong>도 있습니다. 
이 부분에 대해서는 11챕터에서 자세히 다룰 것입니다.</p>
<hr>
<h2 id="102---부동소수점-승격과-정수-승격">10.2 —  부동소수점 승격과 정수 승격</h2>
<p>이전 강의 <code>4챕터 — 객체 크기와 sizeof 연산자</code> 에서, C++의 기본 자료형들이 <strong>최소한 어느 정도의 크기를 가져야 하는지 보장</strong>한다고 배웠습니다.
하지만 실제 자료형의 크기는 <strong>컴파일러와 컴퓨터 구조에 따라</strong> 달라질 수 있습니다.</p>
<p>이렇게 크기가 달라질 수 있도록 허용한 이유는, 
<code>int</code>와 <code>double</code> 같은 자료형을 각 컴퓨터에서 <strong>가장 빠르게 처리할 수 있는 크기로 설정할 수 있게 하기 위해</strong>서입니다.</p>
<p>예를 들어, <code>32</code>비트 컴퓨터는 보통 <strong>한 번에 32비트 데이터를 처리</strong>할 수 있습니다.
이 경우 <code>int</code>는 보통 <code>32</code>비트 크기로 설정됩니다. 왜냐하면 이것이 <strong>CPU가 자연스럽게 처리하는 크기이고, 성능도 가장 좋기 때문</strong>입니다.</p>
<h3 id="작은-크기-데이터를-처리할-때-무슨-일이-일어날까">작은 크기 데이터를 처리할 때 무슨 일이 일어날까?</h3>
<p>그렇다면 <code>32</code>비트 CPU에서 8비트 값 <code>char</code> 혹은 16비트 값 <code>short</code>을 처리해야 한다면 어떻게 될까요?</p>
<p><strong>CPU마다 다릅니다.</strong></p>
<ul>
<li>어떤 CPU(예: x86)는 <code>8비트</code>나 <code>16비트</code> 값을 <strong>직접 처리할 수 있음</strong>
→ 하지만 32비트 처리보다 느릴 수 있음</li>
<li>어떤 CPU(예: PowerPC)는 <strong>오직 32비트 값만 처리 가능</strong>
→ 작은 값은 복잡한 추가 작업이 필요함</li>
</ul>
<p>즉, <strong>작은 자료형을 그대로 처리하는 것이 항상 효율적인 것은 아닙니다.</strong></p>
<hr>
<h3 id="숫자-승격-numeric-promotion">숫자 승격 (Numeric promotion)</h3>
<p>C++는 다양한 컴퓨터에서 잘 작동하고 빠르게 실행되도록 설계되었습니다. 그래서 언어를 만든 사람들은 특정 CPU가 자신에게 가장 잘 맞는 데이터 크기보다 <strong>더 작은 값들을 항상 알아서 효율적으로 다룰 수 있을 거라고 가정하고 싶지 않았습니다.</strong></p>
<p>이 문제를 해결하기 위해, C++은 <strong>숫자 승격</strong>이라는 변환 규칙을 정의했습니다.</p>
<blockquote>
<p><strong>숫자 승격이란?</strong></p>
<blockquote>
<p><em>작은 숫자 자료형 <code>char</code> 등을</em>
<em>더 큰 자료형 <code>int</code> 또는 <code>double</code> 등으로 자동 변환하는 것</em></p>
</blockquote>
<p><strong>이렇게 하면 CPU가 더 효율적으로 처리할 수 있습니다.</strong></p>
</blockquote>
<hr>
<h3 id="숫자-승격의-중요한-특징">숫자 승격의 중요한 특징</h3>
<p>숫자 승격은 <strong>항상 값을 그대로 유지합니다.</strong> 이를 <strong>값 보존 변환</strong> 또는 <strong>안전한 변환</strong> 이라고 합니다.</p>
<p>즉, <strong>변환 전</strong>과 <strong>변환 후</strong> 값이 정확히 같습니다.</p>
<pre><code class="language-cpp">char x = 65;
int y = x;</code></pre>
<p>→ <code>y</code>의 값은 정확히 <code>65</code> 입니다. <strong>값 손실이 없습니다.</strong> 
이 변환은 안전하기 때문에 경고도 발생하지 않으며, <strong>컴파일러는 자동으로 사용</strong>합니다.</p>
<hr>
<h3 id="숫자-승격이-코드-중복을-줄여주는-이유">숫자 승격이 코드 중복을 줄여주는 이유</h3>
<p>숫자 승격은 또 다른 골칫거리도 해결해 줍니다. <code>int</code> 타입의 값을 출력하는 함수를 만든다고 가정해 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printInt(int x)
{
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}</code></pre>
<p>이 함수는 <code>int</code> 출력에는 문제가 없습니다. 하지만 <code>short</code> <code>char</code> 타입의 값도 출력하고 싶다면 어떻게 될까요?
만약 <strong>형 변환</strong>이라는 기능이 없다면, <code>short용 출력 함수</code> <code>char용 출력 함수</code> 를 따로따로 다 만들어야 할 겁니다.</p>
<pre><code class="language-cpp">void printShort(short x);
void printChar(char x);
void printUnsignedChar(unsigned char x);</code></pre>
<p>이처럼 많은 함수를 만들어야 합니다. 
하지만 <strong>숫자 승격</strong> 덕분에 <code>char</code> <code>short</code> 등이 자동으로 <code>int</code> 로 변환됩니다. 
그래서 하나의 함수만 있어도 됩니다.</p>
<hr>
<h3 id="숫자-승격의-종류">숫자 승격의 종류</h3>
<p>숫자 승격은 <strong>크게 두 종류</strong>로 나뉩니다.</p>
<ul>
<li><strong>정수 승격 (integral promotion)</strong></li>
<li><strong>부동소수점 승격 (floating point promotion)</strong></li>
</ul>
<hr>
<h3 id="부동-소수점-승격-floating-point-promotions">부동 소수점 승격 (Floating point promotions)</h3>
<p>더 쉬운 것부터 시작해 봅시다. 이 규칙은 매우 간단합니다.
부동 소수점 승격 규칙에 따라, <code>float</code> 타입의 값은 <code>double</code> 타입으로 <strong>변환될 수 있습니다.</strong></p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printDouble(double d)
{
    std::cout &lt;&lt; d &lt;&lt; &#39;\n&#39;;
}

int main()
{
    printDouble(5.0); // 변환이 필요하지 않음
    printDouble(4.0f); // float에서 double로 숫자 승격이 일어남

    return 0;
}</code></pre>
<p><code>printDouble()</code>을 두 번째로 호출할 때, <code>4.0f</code>라는 <code>float</code> 값은 함수가 요구하는 타입에 맞춰 <code>double</code>로 <strong>알아서 승격</strong>됩니다.</p>
<hr>
<h3 id="정수-승격-integral-promotions">정수 승격 (Integral promotions)</h3>
<p>정수 승격 규칙은 조금 더 복잡합니다. 규칙에 따르면 다음과 같은 변환들이 일어납니다.</p>
<blockquote>
<blockquote>
<p><code>char</code> <code>short</code>
→ <code>int</code>로 변환될 수 있습니다.</p>
</blockquote>
<blockquote>
<p><code>unsigned char</code> <code>unsigned short</code> <code>char8_t</code>
→ 그 값의 전체 범위를 <code>int</code>가 담을 수 있다면 <code>int</code>로 변환되고, <strong>담을 수 없다면</strong> <code>unsigned int</code>로 변환됩니다.</p>
</blockquote>
<blockquote>
<p><code>bool</code>
→ <code>bool</code> 타입은 <code>int</code>로 변환될 수 있으며, <code>false</code>는 <code>0</code>으로, <code>true</code>는 <code>1</code>이 됩니다.</p>
</blockquote>
<p>만약 <code>char</code>가 기본적으로 부호가 있는 타입이라면 위의 <code>signed char</code> 규칙을, 부호가 없는 타입이라면 <code>unsigned char</code> 규칙을 따릅니다.</p>
</blockquote>
<h4 id="일반적인-환경에서의-결과">일반적인 환경에서의 결과</h4>
<p>요즘 우리가 흔히 쓰는 컴퓨터 환경을 기준으로 생각하면, 복잡하게 외울 것 없이
<code>char</code> <code>signed char</code> <code>unsigned char</code> <code>signed short</code> <code>unsigned short</code> <code>bool</code>
<strong>모두 기본적으로 **<code>int</code></strong>로 승격된다고** 이해하시면 아주 편합니다.
<em>(흔히 환경 = 1바이트가 <code>8</code>비트이고, int가 <code>4</code>바이트 이상인 환경)</em></p>
<h4 id="예제">예제</h4>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printInt(int x)
{
    std::cout &lt;&lt; x &lt;&lt; &#39;\n&#39;;
}

int main()
{
    printInt(2);

    short s{ 3 }; // short 타입은 뒤에 붙이는 리터럴 접미사가 없으므로, 변수를 사용해 테스트합니다.
    printInt(s); // short에서 int로 수치 승격이 일어남

    printInt(&#39;a&#39;); // char에서 int로 수치 승격이 일어남
    printInt(true); // bool에서 int로 수치 승격이 일어남

    return 0;
}</code></pre>
<h4 id="주의사항-1가지">주의사항 1가지</h4>
<p><code>int</code>는 <code>signed</code>입니다. 즉, 값은 유지되지만 <code>signed/unsigned</code> 속성은 바뀔 수 있습니다.</p>
<pre><code class="language-cpp">unsigned char → int</code></pre>
<hr>
<h3 id="모든-확장-변환이-숫자-승격은-아닙니다">모든 확장 변환이 숫자 승격은 아닙니다.</h3>
<p><code>char</code>를 <code>short</code>로 바꾸거나, <code>int</code>를 <code>long</code>으로 바꾸는 것처럼 단순히 크기가 더 커진다고 해서 모두 C++의 <strong>숫자 승격이라고 부르지는 않습니다.</strong></p>
<p>이런 것들은 <strong>숫자 변환</strong>입니다. 왜 구분할까요? 
이런 변환들은 앞서 말한 <strong>&quot;CPU가 가장 효율적으로 처리할 수 있는 넉넉한 크기&quot;로 곧바로 변환해 주는 목적과는 다르기 때문</strong>입니다.</p>
<p><strong>승격</strong>이냐 <strong>변환</strong>이냐를 구분하는 것이 너무 학술적으로 들릴 수도 있습니다. 하지만 <strong>특정 상황에서 컴파일러는 숫자 변환보다 숫자 승격을 더 선호</strong>합니다. 이 차이가 결과를 어떻게 바꾸는지에 대해서는 나중에 함수 오버로딩 해결을 배우는 챕터 11에서 명확한 예제와 함께 다시 살펴볼 것입니다.</p>
<hr>
<h2 id="103---숫자-변환-numeric-conversions">10.3 —  숫자 변환 (Numeric conversions)</h2>
<p>C++에는 숫자 승격 외에도 또 다른 숫자 변환 종류가 있는데, 이를 <strong>숫자 변환</strong>이라고 합니다. 
이는 <strong>기본 데이터 타입들 사이에서 일어나는 더 다양하고 포괄적인 변환</strong>들을 의미합니다.</p>
<p>숫자 형 변환에는 <strong>크게 5가지 기본 유형</strong>이 있습니다.</p>
<blockquote>
<blockquote>
<p><strong>정수 타입을 다른 정수 타입으로 변환 (정수 승격 제외)</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">short s = 3; // int를 short로 변환
long l = 3; // int를 long으로 변환
char ch = s; // short를 char로 변환
unsigned int u = 3; // int를 unsigned int로 변환</code></pre>
<blockquote>
<blockquote>
<p><strong>부동 소수점 타입을 다른 부동 소수점 타입으로 변환 (부동 소수점 승격 제외)</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">float f = 3.0; // double을 float로 변환
long double ld = 3.0; // double을 long double로 변환</code></pre>
<blockquote>
<blockquote>
<p><strong>부동 소수점 타입을 정수 타입으로 변환</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">int i = 3.5; // double을 int로 변환</code></pre>
<blockquote>
<blockquote>
<p><strong>정수 타입을 부동 소수점 타입으로 변환</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">double d = 3; // int를 double로 변환</code></pre>
<blockquote>
<blockquote>
<p><strong>정수나 부동 소수점 타입을 <code>bool</code> 타입으로 변환</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">bool b1 = 3; // int를 bool로 변환
bool b2 = 3.0; // double을 bool로 변환</code></pre>
<hr>
<h3 id="안전한-숫자-변환과-불안전한-숫자-변환">안전한 숫자 변환과 불안전한 숫자 변환</h3>
<p><strong>숫자 승격</strong>은 항상 원래의 값을 그대로 유지하기 때문에 <strong>안전</strong>하지만, <strong>상당수의 숫자 변환</strong>은 <strong>불안전</strong>합니다.
불안전한 숫자 변환은 <strong>원래 값과 정확히 같은 값을 새로운 타입에서 표현할 수 없는 경우</strong>입니다.
숫자 변환은 안전성에 따라 <strong>3가지</strong>로 나눌 수 있습니다.</p>
<blockquote>
<h4 id="1-값-보존-변환-value-preserving-conversion--안전함">1. 값 보존 변환 (Value-preserving conversion) — 안전함</h4>
<p>이 변환은 <strong>새로운 타입이 원래 타입의 모든 값을 정확히 표현할 수 있는 경우</strong>입니다.</p>
</blockquote>
<ul>
<li>int → long</li>
<li>short → double<blockquote>
</blockquote>
컴파일러는 이런 안전한 변환에 대해 보통 <strong>경고를 하지 않습니다.</strong><pre><code class="language-cpp">int main(){
  int n { 5 };
  long l = n; // 문제없음, long 타입의 5 생성
&gt;
  short s { 5 };
  double d = s; // 문제없음, double 타입의 5.0 생성
&gt;
  return 0;
}</code></pre>
또한 다시 <strong>원래 타입으로 변환해도 값이 유지</strong>됩니다.<pre><code class="language-cpp">#include &lt;iostream&gt;
int main(){
  int n = static_cast&lt;int&gt;(static_cast&lt;long&gt;(3)); // int 3을 long으로 변환했다가 다시 int로 변환
  std::cout &lt;&lt; n &lt;&lt; &#39;\n&#39;;                         // 3 출력
&gt;
  char c = static_cast&lt;char&gt;(static_cast&lt;double&gt;(&#39;c&#39;)); // &#39;c&#39;를 double로 변환했다가 다시 char로 변환
  std::cout &lt;&lt; c &lt;&lt; &#39;\n&#39;;                               // &#39;c&#39; 출력
&gt;
  return 0;
}</code></pre>
</li>
</ul>
<blockquote>
<h4 id="2-재해석-변환-reinterpretive-conversion--위험하지만-데이터는-유지됨">2. 재해석 변환 (Reinterpretive conversion) — 위험하지만 데이터는 유지됨</h4>
<p>이 변환은 불안전합니다. <strong>변환된 값이 원래 값과 달라질 수 있기 때문</strong>입니다. 
하지만 데이터 자체가 날아가는 것은 아닙니다.&nbsp;<code>Signed</code>과 <code>Unsigned</code> 사이의 변환이 여기에 속합니다.</p>
</blockquote>
<pre><code class="language-cpp">int main(){
    int n1 { 5 };
    unsigned int u1 { n1 }; // 문제없음: unsigned int 5로 변환됨 (값 보존됨)
&gt;
    int n2 { -5 };
    unsigned int u2 { n2 }; // 나쁨: signed int 범위를 벗어난 아주 큰 정수가 됨
&gt;
    return 0;
}</code></pre>
<p><code>u1</code>의 경우 <code>5</code>라는 양수가 그대로 양수 <code>5</code>로 넘어가기 때문에 값이 보존됩니다.
하지만 <code>u2</code>의 경우, <code>-5</code>라는 음수를 <strong>음수 표현이 불가능한</strong> <code>unsigned int</code>에 억지로 넣게 됩니다. 
그 결과 <strong>래핑 현상이 발생</strong>해 아주 엉뚱하고 큰 숫자가 되어버립니다. 값이 보존되지 않는 것이죠.</p>
<blockquote>
</blockquote>
<p>이런 값의 변화는 대개 원치 않는 결과이며 프로그램 오류의 원인이 됩니다.
단, 재해석 변환 역시 데이터 자체가 삭제된 것은 아니기 때문에, 
엉뚱하게 변한 값이라도** 다시 원래 타입으로 캐스팅하면 원래의 값으로 돌아오긴 합니다.**</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
int main(){
    int u = static_cast&lt;int&gt;(static_cast&lt;unsigned int&gt;(-5)); // &#39;-5&#39;를 unsigned로 변환했다가 다시 되돌림
    std::cout &lt;&lt; u &lt;&lt; &#39;\n&#39;; // -5 출력
&gt;
    return 0;
}</code></pre>
<blockquote>
<h4 id="3-데이터-손실-변환-lossy-conversion--위험함">3. 데이터 손실 변환 (Lossy conversion) — 위험함</h4>
<p>이 변환은 데이터 일부가 사라집니다.
예를 들어, 소수점이 있는 <code>double</code>을 정수형인 <code>int</code>로 변환하면 데이터가 날아갑니다.</p>
</blockquote>
<pre><code class="language-cpp">int i = 3.0; // 문제 없음: int 값 3
int j = 3.5; // 데이터 손실: 소수점 0.5가 사라짐 → int 값 3</code></pre>
<blockquote>
</blockquote>
<p>더 큰 소수점 타입인 <code>double</code>을 더 작은 <code>float</code>로 변환할 때도 정밀도 손실이 발생합니다.</p>
<blockquote>
</blockquote>
<pre><code class="language-cpp">float f = 1.2;        // 문제없음: float 값 1.2로 변환됨 (값 보존됨)
float g = 1.23456789; // 데이터 손실: float 1.23457로 변환됨 (정밀도 손실)</code></pre>
<blockquote>
</blockquote>
<p><strong>손실 변환은 데이터가 이미 지워진 것</strong>이기 때문에, 다시 원래 타입으로 되돌려도 처음 값으로 돌아오지 않습니다. 
<code>3.5</code>를 <code>int</code>로 바꿔서 <code>3</code>이 되었다면, 이걸 다시 <code>double</code>로 바꿔봤자 <code>3.5</code>가 아닌 <code>3.0</code>이 될 뿐입니다.
컴파일러는 보통 이런 변환을 시도할 때 <strong>경고나 에러를 띄웁니다.</strong></p>
<hr>
<h3 id="숫자-변환-시-꼭-기억해야-할-4가지-원칙">숫자 변환 시 꼭 기억해야 할 4가지 원칙</h3>
<p>규칙이 너무 많아서 복잡해 보인다면, 초보자 분들은 아래 내용만 확실히 기억해 두세요.</p>
<blockquote>
<blockquote>
<h4 id="1-타입-범위를-벗어나면-이상한-결과가-발생합니다">1. 타입 범위를 벗어나면 이상한 결과가 발생합니다</h4>
</blockquote>
</blockquote>
<pre><code class="language-cpp">int main(){
    int i{ 30000 };
    char c = i; // char는 -128부터 127까지만 담을 수 있습니다.
&gt;&gt;
    std::cout &lt;&lt; static_cast&lt;int&gt;(c) &lt;&lt; &#39;\n&#39;;
&gt;&gt;
    return 0;
}
&gt;&gt;
결과: 48 (넘쳐서 엉뚱한 값이 나옴 - 오버플로우)</code></pre>
<blockquote>
<blockquote>
<p><strong>2. 큰 타입 → 작은 타입 변환은 값이 범위 내에 있으면 안전합니다</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">int i{ 2 };
short s = i; // int에서 short로 변환 (2는 short 안에 들어감)
std::cout &lt;&lt; s &lt;&lt; &#39;\n&#39;; // 2 출력
&gt;&gt;
double d{ 0.1234 };
float f = d; 
std::cout &lt;&lt; f &lt;&lt; &#39;\n&#39;; // 0.1234 출력</code></pre>
<blockquote>
<blockquote>
<p><strong>3. <code>int</code> → <code>float</code> 변환은 보통 안전합니다</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">int i{ 10 };
float f = i;
std::cout &lt;&lt; f &lt;&lt; &#39;\n&#39;; // 10 출력</code></pre>
<blockquote>
<blockquote>
<p><strong>4. <code>float</code> → <code>int</code> 변환은 소수점이 사라집니다</strong></p>
</blockquote>
</blockquote>
<pre><code class="language-cpp">int i = 3.5;
std::cout &lt;&lt; i &lt;&lt; &#39;\n&#39;; // 0.5가 버려지고 3 출력</code></pre>
<hr>
<h2 id="104--축소-변환-리스트-초기화-그리고-constexpr-초기화">10.4 — 축소 변환, 리스트 초기화, 그리고 constexpr 초기화</h2>
<h3 id="축소-변환-narrowing-conversions">축소 변환 (Narrowing conversions)</h3>
<p>C++에서 <strong>축소 변환</strong>이란 잠재적으로 <strong>위험할 수 있는 숫자 변환을 뜻</strong>합니다. 
변환하려는 원래 데이터가** 변환될 자료형의 그릇보다 커서, 데이터의 일부를 잃어버릴** 수 있기 때문입니다.
즉, <strong>목적지 타입이 원본 타입의 모든 값을 저장할 수 없는 경우</strong>를 말합니다.</p>
<p>다음과 같은 변환들이 축소 변환으로 정의됩니다.</p>
<blockquote>
<p><strong>1. 실수형 → 정수형 변환</strong> &nbsp;<code>double</code> → <code>int</code>
실수는 소수점을 포함할 수 있지만,** 정수는 소수점을 저장할 수 없기 때문에 값이 손실**될 수 있습니다.</p>
</blockquote>
<blockquote>
<p><strong>2. 실수형 → 더 작은 실수형 변환</strong> <code>double</code> → <code>float</code>
실수형에서 <strong>더 작거나 단계가 낮은 실수형으로 변환</strong>할 때 값이 손실될 수 있습니다.</p>
</blockquote>
<ul>
<li>단, 변환하려는 값이 <code>constexpr</code> 이고, 목적지 타입 범위 안에 있으면 <strong>축소 변환으로 간주되지 않습니다.</strong>
(정밀도가 줄어들더라도 범위 안에 있으면 예외가 적용됩니다.)</li>
</ul>
<blockquote>
<p><strong>3. 정수형 → 실수형 변환</strong></p>
</blockquote>
<ul>
<li>단, 값이 <code>constexpr</code>이고 목적지 타입에서 정확하게 표현 가능하면 축소 변환이 아닙니다.</li>
</ul>
<blockquote>
<p><strong>4. 정수형 → 더 작은 정수형 변환</strong> <code>long → int</code> <code>int → short</code> <code>int → unsigned int</code> <code>unsigned int → int</code></p>
</blockquote>
<ul>
<li>단, 값이 <code>constexpr</code>이고 목적지 타입에서 정확하게 표현 가능하면 축소 변환이 아닙니다.</li>
</ul>
<p>대부분의 경우, <strong>암시적 축소 변환은 컴파일러 경고를 발생시킵니다.</strong>
단, <code>signed ↔ unsigned</code> 변환은 컴파일러 설정에 따라 경고가 나오지 않을 수도 있습니다.</p>
<hr>
<h3 id="의도적인-축소-변환은-명시적으로-작성하기">의도적인 축소 변환은 명시적으로 작성하기</h3>
<p>축소 변환은 <strong>항상 피할 수 있는 것은 아닙니다. **
특히 함수 호출 시, **함수 매개변수 타입과 전달되는 값 타입이 다르면 축소 변환이 필요할 수 있습니다.</strong></p>
<p>이런 경우에는 <code>static_cast</code>를 사용하여 <strong>명시적으로 변환</strong>하는 것이 좋습니다.
이렇게 하면 변환이 의도된 것임을 명확히 보여줄 수 있고 컴파일러 경고도 제거할 수 있습니다</p>
<pre><code class="language-cpp">void someFcn(int i){}

int main(){
    double d{ 5.0 };

    someFcn(d); // 나쁨: 암시적 축소 변환이 일어나 컴파일러 경고를 발생시킵니다.

    // 좋음: 컴파일러에게 이 축소 변환이 의도적임을 명확하게 알려줍니다.
    someFcn(static_cast&lt;int&gt;(d)); // 경고가 발생하지 않습니다.

    return 0;
}</code></pre>
<hr>
<h3 id="중괄호-초기화는-축소-변환을-허용하지-않습니다">중괄호 초기화는 축소 변환을 허용하지 않습니다</h3>
<p>중괄호 <code>{}</code>를 사용해 변수를 초기화하는 <strong>리스트 초기화</strong> 방식은 축소 변환을 아예 허용하지 않습니다.
이것이 우리가 이 초기화 방식을 가장 선호하는 이유이기도 합니다.
<strong>축소 변환</strong>을 <strong>중괄호 초기화에서 사용하려면</strong> <code>static_cast</code> 를 사용해야 합니다.</p>
<pre><code class="language-cpp">int main(){
    double d { 3.5 };

    // static_cast&lt;int&gt;는 double을 int로 변환하고, 그 int 결과값으로 i를 초기화합니다.
    int i { static_cast&lt;int&gt;(d) };

    return 0;
}</code></pre>
<hr>
<h3 id="일부-constexpr-변환은-축소-변환으로-간주되지-않습니다">일부 constexpr 변환은 축소 변환으로 간주되지 않습니다</h3>
<p>값이 <strong>런타임</strong>에 결정되는 경우, 변환 결과도 런타임에만 알 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print(unsigned int u) // 참고: unsigned (부호 없음)
{
    std::cout &lt;&lt; u &lt;&lt; &#39;\n&#39;;
}

int main(){
    std::cout &lt;&lt; &quot;정수 값을 입력하세요: &quot;;
    int n{};
    std::cin &gt;&gt; n; // 5 또는 -5를 입력해 보세요.
    print(n);      // unsigned로 변환할 때 값이 유지될 수도 있고 아닐 수도 있습니다.

    return 0;
}</code></pre>
<p>컴파일러는 <code>n</code>에 어떤 값이 들어올지 모르기 때문에, 이 변환이 <strong>안전한지 판단할 수 없습니다.</strong>
그래서 <code>signed/unsigned</code> <strong>경고가 발생</strong>할 수 있습니다.</p>
<p>하지만 <code>constexpr</code> 값은 다릅니다.
<code>constexpr</code> <strong>값은 컴파일 시점에 이미 값이 결정</strong>되어 있습니다.</p>
<p>따라서 컴파일러는</p>
<ol>
<li>직접 변환을 수행하고</li>
<li>값이 유지되는지 확인할 수 있습니다</li>
</ol>
<p>값이 유지되면 → 축소 변환이 아님
값이 바뀌면 → 컴파일 오류 발생</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main(){
    constexpr int n1{ 5 };   // 참고: constexpr
    unsigned int u1 { n1 };  // 문제없음: 예외 조항 덕분에 축소 변환으로 간주되지 않습니다.

    constexpr int n2 { -5 }; // 참고: constexpr
    unsigned int u2 { n2 };  // 컴파일 오류: 값이 변하기 때문에 축소 변환으로 간주됩니다.

    return 0;
}</code></pre>
<hr>
<h2 id="105--산술-변환-arithmetic-conversions">10.5 — 산술 변환 (Arithmetic conversions)</h2>
<p>다음 표현식을 살펴봅시다.</p>
<pre><code class="language-cpp">int x { 2 + 3 };</code></pre>
<p>이 코드에서 <strong>이항 연산자</strong> <code>+</code> 는 두 개의 피연산자 <code>2</code> <code>3</code>를 받습니다. 그리고 두 피연산자는 모두 <code>int</code> 타입입니다.</p>
<p>피연산자의 타입이 동일하기 때문에</p>
<ul>
<li><strong>계산도</strong> <code>int</code> 타입으로 수행되고</li>
<li><strong>결과도</strong> <code>int</code> 타입으로 반환됩니다.</li>
</ul>
<p>따라서 <code>2 + 3</code> 의 결과는 <code>int</code> 타입 값 <code>5</code>가 됩니다.
그렇다면 <strong>피연산자의 타입이 서로 다르면</strong> 어떻게 될까요?</p>
<pre><code class="language-cpp">??? y { 2 + 3.5 };</code></pre>
<p>이 경우 <code>2</code> 는 <code>int</code> , <code>3.5</code> 는 <code>double</code> 입니다. 이때 결과는 어떤 타입이 될까요? <code>int</code>? <code>double</code>? 아니면 다른 타입?</p>
<hr>
<h3 id="같은-타입이-필요한-연산자">같은 타입이 필요한 연산자</h3>
<p>C++에서는 일부 연산자가 <strong>두 피연산자의 타입이 같아야만</strong> 합니다.
만약 피연산자의 타입이 서로 다르면, C++는 자동으로 피연산자의 타입을 변환합니다. 이를 <strong>일반적인 산술 변환</strong> 이라고 합니다.
이 규칙을 통해 두 피연산자는 동일한 타입으로 변환됩니다. 그리고 이렇게 변환된 타입을 <strong>공통 타입</strong>이라고 합니다.</p>
<hr>
<h3 id="같은-타입이-필요한-연산자-목록">같은 타입이 필요한 연산자 목록</h3>
<p>다음 연산자들은 <strong>두 피연산자의 타입이 반드시 같아야</strong> 합니다.</p>
<p><strong>1. 산술 연산자 <code>+</code> <code>-</code> <code>*</code> <code>/</code> <code>%</code></strong>
<strong>2. 비교 연산자 <code>&lt;</code> <code>&gt;</code> <code>&lt;=</code> <code>&gt;=</code> <code>==</code> <code>!=</code></strong>
<strong>3. 비트 연산자 <code>&amp;</code> <code>^</code> <code>|</code>
4. 조건 연산자 <code>?:</code></strong> (단, <code>bool</code> 타입이어야 하는 조건식 부분은 제외)</p>
<hr>
<h3 id="일반적인-산술-변환-규칙">일반적인 산술 변환 규칙</h3>
<p>이 규칙은 실제로 꽤 복잡하지만, 이해하기 쉽게 단순화해서 설명하겠습니다.
컴파일러는 <strong>타입마다 우선순위 목록</strong>을 가지고 있습니다.</p>
<pre><code class="language-cpp">long double   (가장 높음)
double
float
long long
long
int           (가장 낮음)</code></pre>
<p>한쪽은 정수 타입이고 다른 한쪽은 실수 타입이라면, <strong>정수 값이 실수 타입으로 변환</strong>됩니다.
단순히 <strong>순위가 낮은 타입이 순위가 높은 타입으로 변환</strong>됩니다.</p>
<blockquote>
<h4 id="예제-1-int-와-double-더하기">예제 1: int 와 double 더하기</h4>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;typeinfo&gt; // typeid()를 사용하기 위해
&gt;
int main()
{
    int i{ 2 };
    std::cout &lt;&lt; typeid(i).name() &lt;&lt; &#39;\n&#39;; // i의 타입 이름을 보여줌
&gt;
    double d{ 3.5 };
    std::cout &lt;&lt; typeid(d).name() &lt;&lt; &#39;\n&#39;; // d의 타입 이름을 보여줌
&gt;
    std::cout &lt;&lt; typeid(i + d).name() &lt;&lt; &#39; &#39; &lt;&lt; i + d &lt;&lt; &#39;\n&#39;; // i + d의 타입 이름을 보여줌
&gt;
    return 0;
}
&gt;
출력:
int
double
double 5.5</code></pre>
<p>이 경우 <code>double</code> 이 더 높은 우선순위를 가집니다.
따라서 <code>int</code> 값 <code>2</code> 는 <code>double</code> 값 <code>2.0</code> 으로 변환됩니다.
그 후 <code>2.0 + 3.5 = 5.5</code> 결과 타입은 <code>double</code> 입니다.</p>
<blockquote>
<p><strong>예제 2: short + short</strong></p>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;typeinfo&gt; // typeid()를 사용하기 위해
&gt;
int main()
{
    short a{ 4 };
    short b{ 5 };
    std::cout &lt;&lt; typeid(a + b).name() &lt;&lt; &#39; &#39; &lt;&lt; a + b &lt;&lt; &#39;\n&#39;; // a + b의 타입을 보여줌
&gt;
    return 0;
}</code></pre>
<p><code>short</code> 는 우선순위 목록에 없기 때문에, 두 값 모두 먼저 <code>int</code> 로 승격됩니다.
따라서 결과는 <code>int 9</code>가 됩니다.</p>
<hr>
<h3 id="signed-와-unsigned-문제">signed 와 unsigned 문제</h3>
<p><code>signed</code> 와 <code>unsigned</code> 를 섞어서 사용하면 예상하지 못한 결과가 나올 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;typeinfo&gt; // typeid()를 사용하기 위해

int main()
{
    std::cout &lt;&lt; typeid(5u-10).name() &lt;&lt; &#39; &#39; &lt;&lt; 5u - 10 &lt;&lt; &#39;\n&#39;; // 5u는 5를 부호 없는 정수(unsigned)로 취급하라는 뜻입니다.

    return 0;
}</code></pre>
<p>보통 우리는 <code>5 - 10 = -5</code> 처럼 생각합니다.
하지만 실제 결과는 <code>unsigned int 4294967291</code> 입니다.
왜냐하면 변환 규칙에 의해 <code>10 (int) → unsigned int</code> 로 변환되기 때문입니다.
<code>unsigned</code> 는 음수를 표현할 수 없기 때문에, 이상한 결과가 나옵니다.</p>
<hr>
<h3 id="stdcommon_type-와-stdcommon_type_t">std::common_type 와 std::common_type_t</h3>
<p>두 타입의 <strong>공통 타입을 알고 싶을 때 사용할 수 있는 도구</strong>입니다.
이 기능은 나중 강의에서 더 자세히 다룹니다.</p>
<ul>
<li>헤더 <code>#include &lt;type_traits&gt;</code></li>
<li><code>std::common_type_t&lt;int, double&gt; → double</code></li>
<li><code>std::common_type_t&lt;unsigned int, long&gt; → 공통 타입 반환</code></li>
</ul>
<hr>
<h2 id="106--명시적-타입-변환casting과-static_cast">10.6 — 명시적 타입 변환(casting)과 static_cast</h2>
<h3 id="정수-나눗셈-때문에-발생하는-흔한-실수">정수 나눗셈 때문에 발생하는 흔한 실수</h3>
<p>많은 초보 C++ 프로그래머들은 종종 아래와 같은 실수를 하곤 합니다.</p>
<pre><code class="language-cpp">double d = 10 / 4; // 정수 나눗셈을 수행하여, d를 2.0으로 초기화합니다.</code></pre>
<p>대부분의 경우, 우리는 <code>2.5</code> 를 기대했을 것입니다. 
하지만 <strong>정수 나눗셈</strong>을 실행하여 결과값으로 <code>2.0</code> 을 만들어냅니다. 
왜 이런 일이 발생할까요?</p>
<ul>
<li><code>10</code>과 <code>4</code>는 둘 다 <code>int</code> 타입입니다. 따라서 <strong>정수 나눗셈이 수행</strong>됩니다.</li>
<li>정수 나눗셈의 결과는 <code>2.5</code>가 아니라, <code>2</code>입니다.</li>
<li>그 다음에 <code>2</code>가 <code>double</code>로 변환되어 <code>2.0</code>이 됩니다.</li>
</ul>
<p>숫자(리터럴)를 직접 사용할 때는, <strong>둘 중 하나를</strong> <strong>실수로 적어주면 간단히 실수 나눗셈을 할 수 있습니다.</strong></p>
<pre><code class="language-cpp">double d = 10.0 / 4; // 실수 나눗셈을 수행하여, d를 2.5로 초기화합니다.</code></pre>
<hr>
<h3 id="변수-사용-시-문제">변수 사용 시 문제</h3>
<p>하지만 변수를 사용하면 어떻게 할까요? 여기서도 같은 문제가 발생합니다.</p>
<pre><code class="language-cpp">int x { 10 };
int y { 4 };
double d = x / y; // 정수 나눗셈을 수행하여, d를 2.0으로 초기화합니다.</code></pre>
<p>변수에는 리터럴처럼 <code>.0</code>을 붙일 수 없습니다.
따라서 변수의 타입을 <strong>직접 실수 타입으로 변환</strong>해야 합니다.
이를 위해 C++는 <strong>캐스트라는 기능을 제공</strong>합니다.</p>
<hr>
<h3 id="타입-캐스팅type-casting">타입 캐스팅(Type casting)</h3>
<p>캐스트란 <strong>프로그래머가 직접 컴파일러에게 타입 변환을 요청하는 것</strong> 입니다. 이것을 <strong>명시적 타입 변환</strong>이라고 합니다.
반대로, <strong>컴파일러가 자동으로 수행</strong>하는 것은 <strong>암시적 타입 변환</strong>이라고 합니다.</p>
<hr>
<h3 id="c에서-지원하는-캐스트-종류">C++에서 지원하는 캐스트 종류</h3>
<p>C++는 5가지 종류의 캐스트를 지원합니다.
<code>static_cast</code> <code>dynamic_cast</code> <code>const_cast</code> <code>reinterpret_cast</code> <code>C스타일 캐스트</code> 입니다.
앞의 네 가지는 <strong>이름 있는 캐스트(named casts)</strong> 라고도 부릅니다.</p>
<table>
<thead>
<tr>
<th>캐스트</th>
<th>설명</th>
<th>안전성</th>
</tr>
</thead>
<tbody><tr>
<td>static_cast</td>
<td>관련된 타입 간 변환</td>
<td>안전</td>
</tr>
<tr>
<td>dynamic_cast</td>
<td>상속 구조에서 런타임 변환</td>
<td>안전</td>
</tr>
<tr>
<td>const_cast</td>
<td>const 추가 또는 제거</td>
<td>const 추가만 안전</td>
</tr>
<tr>
<td>reinterpret_cast</td>
<td>비트 단위 재해석</td>
<td>위험</td>
</tr>
<tr>
<td>C-style cast</td>
<td>여러 캐스트 조합</td>
<td>위험</td>
</tr>
</tbody></table>
<p>모든 캐스트의 기본 작동 방식은 동일합니다. <code>변환할 값</code> 과 <code>목표 타입</code> 을 입력으로 받고, 변환이 완료된 결과를 출력해 줍니다.
이 강의에서는 가장 흔하게 쓰이는 <code>C스타일 캐스트</code> 와 <code>static_cast</code>에 집중하겠습니다.</p>
<h4 id="관련-내용-및-주의사항">관련 내용 및 주의사항</h4>
<ul>
<li><code>dynamic_cast</code>는 필요한 기본 지식을 배운 후 나중에 다룹니다.</li>
<li><code>const_cast</code>와 <code>reinterpret_cast</code>는 아주 드문 상황에서만 유용하고 잘못 쓰면 위험하므로 <strong>아주 타당한 이유가 없다면 사용을 피하세요.</strong></li>
</ul>
<hr>
<h3 id="c-스타일-캐스트">C 스타일 캐스트</h3>
<p>과거 C 언어 프로그래밍에서는 소괄호 <code>()</code> 연산자를 이용해 형 변환을 했습니다. </p>
<pre><code class="language-cpp">(type)value</code></pre>
<p>괄호 안에 바꿀 타입을 적고, 그 바로 오른쪽에 변환할 값을 적는 방식이죠. 
C++에서는 이를 <code>C스타일 캐스트</code> 라고 부릅니다. C 언어에서 넘어온 코드에서 종종 볼 수 있습니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x { 10 };
    int y { 4 };

    std::cout &lt;&lt; (double)x / y &lt;&lt; &#39;\n&#39;; // x를 double 타입으로 C스타일 캐스트

    return 0;
}</code></pre>
<ul>
<li><code>x</code>가 <code>double</code>로 변환됨</li>
<li>실수 나눗셈 수행</li>
<li>결과 <code>2.5</code></li>
</ul>
<hr>
<h3 id="함수-스타일-캐스트">함수 스타일 캐스트</h3>
<p>C++에서는 다음과 같은 형태도 가능합니다. 이 방식은 함수 호출처럼 보여서 조금 더 읽기 쉽습니다.</p>
<pre><code class="language-cpp">std::cout &lt;&lt; double(x) / y &lt;&lt; &#39;\n&#39;; // x를 double로 변환하는 함수 스타일 캐스트</code></pre>
<p>하지만** 현대 C++에서는 C스타일 캐스트를 일반적으로 피해야 합니다.** 그 이유는 크게 두 가지입니다.</p>
<blockquote>
<h4 id="이유-1-어떤-변환이-일어나는지-명확하지-않음"><strong>이유 1: 어떤 변환이 일어나는지 명확하지 않음</strong></h4>
<p>C스타일 캐스트는 겉보기엔 단순해 보여도, 사용되는 상황에 따라 <code>static_cast</code> <code>const_cast</code> <code>reinterpret_cast</code> 중 하나를 제멋대로 수행해 버립니다. 어떤 캐스트가 실행될지 코드를 읽고 명확히 알기 어렵고, 단순한 변환을 의도했는데 위험한 변환이 일어날 수도 있습니다. 이런 문제는 보통 프로그램을 실행할 때(런타임)가 되어서야 오류로 나타납니다.</p>
</blockquote>
<blockquote>
<h4 id="이유-2-찾기-어렵고-읽기-어렵다"><strong>이유 2: 찾기 어렵고 읽기 어렵다</strong></h4>
</blockquote>
<pre><code class="language-cpp">(double)x</code></pre>
<p>이 코드는 눈에 잘 띄지 않으며 검색하기도 어렵습니다. 반면 <code>static_cast&lt;double&gt;(x)</code>는 명확합니다.</p>
<pre><code class="language-cpp">static_cast&lt;double&gt;(x)</code></pre>
<hr>
<h3 id="대부분의-값-변환에는-static_cast를-사용하세요">대부분의 값 변환에는 <code>static_cast</code>를 사용하세요</h3>
<p>C++에서 가장 많이 사용하는 캐스트는 <code>static_cast</code> 입니다.</p>
<h4 id="기본-사용법">기본 사용법</h4>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    char c { &#39;a&#39; };

    std::cout &lt;&lt; static_cast&lt;int&gt;(c) &lt;&lt; &#39;\n&#39;; // a 대신 97 출력

    return 0;
}
</code></pre>
<h4 id="문법">문법</h4>
<pre><code class="language-cpp">static_cast&lt;변환할타입&gt;(값)</code></pre>
<hr>
<h3 id="static_cast의-특징"><code>static_cast</code>의 특징</h3>
<blockquote>
<p><strong>특징 1: 컴파일 타임 검사</strong>
잘못된 변환은 컴파일 오류를 발생 시킵니다.</p>
</blockquote>
<pre><code class="language-cpp">int x { static_cast&lt;int&gt;(&quot;Hello&quot;) }; // 잘못된 변환 → 컴파일 오류 발생</code></pre>
<blockquote>
<p><strong>특징 2: 위험한 변환을 제한</strong>
<code>C 스타일 캐스트</code> 보다 안전합니다.</p>
</blockquote>
<hr>
<h3 id="축소-변환을-명시적으로-만들-때-static_cast-활용하기">축소 변환을 명시적으로 만들 때 <code>static_cast</code> 활용하기</h3>
<p>데이터의 크기가 작아지거나 손실될 수 있는 변환(이를 <strong>축소 변환</strong>이라 합니다)이 <strong>암시적으로 일어날 때, 컴파일러는 종종 경고</strong>를 냅니다.</p>
<pre><code class="language-cpp">int i { 48 };
char ch = i; // 암시적 축소 변환 (데이터 손실 우려로 경고 발생 가능)</code></pre>
<p><code>2</code>바이트나 <code>4</code>바이트 크기인 <code>int</code>를 <code>1</code>바이트인 <code>char</code>에 욱여넣는 것은 잠재적으로 안전하지 않습니다. 
숫자가 <code>char</code>의 <strong>허용 범위를 넘쳐버릴(오버플로우) 수 있기 때문</strong>이죠. 그래서 컴파일러가 경고를 주는 것입니다.</p>
<p>이때 <code>static_cast</code>를 사용하면 컴파일러에게 <strong>&quot;내가 이 변환의 위험성을 알고 있고 의도한 거야!&quot;라고 명확히 알려줄 수 있습니다.</strong></p>
<pre><code class="language-cpp">int i { 48 };
// int에서 char로 명시적 변환을 수행하여, 변수 ch에 char 타입으로 대입합니다.
char ch { static_cast&lt;char&gt;(i) };</code></pre>
<p>이렇게 하면 결과값이 확실한 <code>char</code> 타입이 되기 때문에, 대입할 때 타입이 어긋나지 않아 컴파일러가 더 이상 경고를 보내지 않습니다. 
<strong>물론 범위를 벗어나 넘쳐흐르는 결과에 대한 책임은 프로그래머의 몫이 됩니다.</strong></p>
<p>또 다른 예로, <code>double</code>을 <code>int</code>로 변환할 때 데이터 손실(소수점 아래 잘림) 경고가 뜨는 것을 막고 싶다면 아래처럼 명확히 의도를 밝히면 됩니다.</p>
<pre><code class="language-cpp">int i { 100 };
i = static_cast&lt;int&gt;(i / 2.5); // &quot;소수점이 잘려도 괜찮아, 내가 의도한 거야&quot;</code></pre>
<hr>
<h2 id="107--typedef와-타입-별칭-typedefs-and-type-aliases">10.7 — Typedef와 타입 별칭 (Typedefs and type aliases)</h2>
<h3 id="타입-별칭-type-aliases">타입 별칭 (Type aliases)</h3>
<p>C++에서 <code>using</code> 은 기존 데이터 타입에 <strong>새로운 이름(별칭)</strong>을 붙일 수 있게 해주는 키워드입니다.</p>
<pre><code class="language-cpp">using Distance = double; // Distance를 double 타입의 별칭으로 정의</code></pre>
<p>이제 <code>Distance</code> 는 <code>double</code> 과 같은 의미로 사용할 수 있습니다.</p>
<pre><code class="language-cpp">Distance milesToDestination{ 3.4 }; // double 타입 변수를 정의</code></pre>
<p>컴파일러는 <code>Distance</code> 를 보면 자동으로 <code>double</code> 로 바꿔서 처리합니다.</p>
<p>다음 예제를 봅시다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    using Distance = double; // Distance를 double 타입의 별칭으로 정의합니다.

    Distance milesToDestination{ 3.4 }; // double 타입의 변수를 정의합니다.

    std::cout &lt;&lt; milesToDestination &lt;&lt; &#39;\n&#39;; // double 값을 출력합니다.

    return 0;
}


출력 결과:
3.4</code></pre>
<hr>
<h3 id="타입-별칭은-새로운-타입이-아닙니다">타입 별칭은 새로운 타입이 아닙니다</h3>
<p>타입 별칭은 새로운 타입을 만드는 것이 아닙니다. 단지 <strong>기존 타입에 다른 이름을 붙이는 것뿐</strong>입니다.</p>
<hr>
<h3 id="타입-별칭의-범위scope">타입 별칭의 범위(scope)</h3>
<p>타입 별칭의 이름도 <strong>일반 변수처럼 &#39;사용 가능한 범위(스코프)&#39; 규칙을 따릅니다. **
즉, 중괄호 <code>{}</code> 블록 안에서 별칭을 만들면 **그 블록 안에서만 쓸 수 있고</strong>, 
파일의 맨 위(전역 네임스페이스)에 만들면 <strong>그 파일 끝까지 어디서든</strong> 쓸 수 있습니다.</p>
<p>여러 파일에서 똑같은 타입 별칭을 쓰고 싶다면, <strong>헤더 파일에 정의</strong>해 두고 필요한 코드 파일에서 <code>#include</code>로 불러와 사용하면 됩니다.</p>
<pre><code class="language-cpp">// mytypes.h:
#ifndef MYTYPES_H
#define MYTYPES_H

    using Miles = long;
    using Speed = long;

#endif</code></pre>
<hr>
<h3 id="typedef-옛-방식">typedef (옛 방식)</h3>
<p><code>typedef</code>는 <strong>타입 별칭을 만드는 옛 방식</strong>입니다.</p>
<pre><code class="language-cpp">typedef long Miles;
using Miles = long;</code></pre>
<p><strong>둘은 같은 의미</strong>입니다.
하지만 modern C++에서는 <code>using</code> 이 더 권장됩니다.</p>
<blockquote>
<p><strong>용어 정리</strong> 
C++ 공식 표준 문서나 일반적인 프로그래밍 대화에서는 <code>typedef</code>나 <code>using</code>이나 결국 하는 일이 같기 때문에, 이 둘을 구분 없이 뭉뚱그려 <code>typedef</code>라고 부르기도 합니다.</p>
</blockquote>
<hr>
<h3 id="타입-별칭은-언제-사용해야-할까">타입 별칭은 언제 사용해야 할까?</h3>
<blockquote>
<p><strong>1. 플랫폼 독립 코드 작성</strong>
<code>int</code>는 시스템마다 크기가 다를 수 있습니다. 어떤 시스템에선 <code>2 bytes</code> 어떤 시스템에선 <code>4 bytes</code> 일수도 있습니다.
이 문제를 해결하기 위해 <code>int8_t</code> <code>int16_t</code> <code>int32_t</code> 같은 <strong>타입 별칭</strong>을 사용합니다.
이렇게 하면 <strong>플랫폼과 관계없이 올바른 크기를 보장</strong>할 수 있습니다.</p>
</blockquote>
<pre><code class="language-cpp">#ifdef INT_2_BYTES
&gt;
using int8_t = char;
using int16_t = int;
using int32_t = long;
&gt;
#else
&gt;
using int8_t = char;
using int16_t = short;
using int32_t = int;
&gt;
#endif</code></pre>
<blockquote>
<p><strong>2. 복잡한 타입을 간단하게 만들기</strong>
고급 C++로 넘어가면, 타입 이름이 타이핑하기 끔찍할 정도로 길어지는 경우가 생깁니다.
<code>std::vector&lt;std::pair&lt;std::string, int&gt;&gt;</code> 같이 일일이 치려면 손도 아프고 오타도 나기 쉽습니다. 
이때 타입 별칭을 쓰면 마법처럼 편해집니다.</p>
</blockquote>
<pre><code class="language-cpp">#include &lt;string&gt; 
#include &lt;vector&gt; 
#include &lt;utility&gt; 
&gt;
using VectPairSI = std::vector&lt;std::pair&lt;std::string, int&gt;&gt;; // 이 복잡하고 긴 타입에 VectPairSI라는 별칭을 줍니다.
&gt;
bool hasDuplicates(VectPairSI pairlist) // 함수 매개변수에 훨씬 짧아진 별칭을 사용합니다.
{
    // 코드가 들어갈 자리
    return false;
}
&gt;
int main()
{
     VectPairSI pairlist; // 별칭으로 간단하게 변수를 생성합니다.
&gt;
     return 0;
}</code></pre>
<blockquote>
</blockquote>
<p>훨씬 읽기 쉽습니다. 이것이 <strong>타입 별칭의 가장 좋은 사용 방법</strong>입니다.</p>
<blockquote>
<p><strong>3. 값의 &#39;의미&#39;를 명확하게 설명하고 싶을 때</strong></p>
</blockquote>
<pre><code class="language-cpp">int gradeTest();</code></pre>
<p>이 함수가 반환하는 <code>int</code> 의 의미가 불명확합니다.</p>
<pre><code class="language-cpp">using TestScore = int;
&gt;
TestScore gradeTest();</code></pre>
<p>타입 별칭을 사용하면 이제 시험 점수라는 의미가 명확해집니다.</p>
<blockquote>
<p><strong>4. 유지보수 쉽게 하기</strong></p>
</blockquote>
<pre><code class="language-cpp">using StudentId = short;</code></pre>
<p>나중에 쉽게 변경 가능합니다. <strong>코드 전체를 수정할 필요 없습니다.</strong></p>
<pre><code class="language-cpp">using StudentId = long;</code></pre>
<hr>
<h2 id="108---auto-키워드를-사용한-객체의-타입-추론type-deduction">10.8 —  auto 키워드를 사용한 객체의 타입 추론(Type Deduction)</h2>
<p>다음과 같이 변수를 정의할 때, 사실 우리는 타입 정보를 약간 불필요하게 중복해서 적고 있습니다.</p>
<pre><code class="language-cpp">double d{ 5.0 };</code></pre>
<p>C++에서는 모든 객체를 선언할 때 <strong>반드시 타입을 명시</strong>해야 합니다. 그래서 우리는 변수 <code>d</code>가 <code>double</code> 타입이라고 직접 써 주었습니다.
하지만 <code>d</code>에 넣기 위해 적어둔 값 <code>5.0</code> 역시 (숫자 형태를 보면 알 수 있듯이) <strong>이미 내부적으로 **<code>double</code></strong> 타입<strong>입니다.
즉, 우리는 **같은 타입 정보를 두 번 써 준 셈</strong>입니다.</p>
<hr>
<h3 id="초기값을-활용한-타입-추론">초기값을 활용한 타입 추론</h3>
<p>타입 추론이란, <strong>컴파일러가 초기값을 보고 변수의 타입을 자동으로 결정</strong>하는 기능입니다.
변수를 정의할 때 <code>auto</code> 키워드를 사용하면 <strong>타입 추론이 적용</strong>됩니다.</p>
<pre><code class="language-cpp">int main()
{
    auto d { 5.0 }; // 5.0은 double 리터럴이므로, d는 double 타입으로 추론됩니다.
    auto i { 1 + 2 }; // 1 + 2의 계산 결과가 int이므로, i는 int 타입으로 추론됩니다.
    auto x { i }; // i가 int이므로, x 역시 int 타입으로 추론됩니다.

    return 0;
}</code></pre>
<p><strong>함수 호출</strong>도 표현식이므로 타입 추론이 가능합니다.
<code>add()</code> 함수가 <code>int</code>를 반환하므로 <code>sum</code> 은 자동으로 <code>int</code> 가 됩니다.</p>
<pre><code class="language-cpp">int add(int x, int y)
{
    return x + y;
}

int main()
{
    auto sum { add(5, 6) }; // add() 함수가 int를 반환하므로, sum의 타입은 int로 추론됩니다.

    return 0;
}</code></pre>
<p><strong>리터럴 뒤에 붙는 접미사로 원하는 타입을 지정</strong>할수도 있습니다.</p>
<pre><code class="language-cpp">int main()
{
    auto a { 1.23f }; // &#39;f&#39; 접미사가 있으므로 a는 float으로 추론됩니다.
    auto b { 5u };    // &#39;u&#39; 접미사가 있으므로 b는 unsigned int로 추론됩니다.

    return 0;
}</code></pre>
<p>물론 <code>const</code> 나 <code>constexpr</code> 같은 <strong>한정자도</strong> <code>auto</code> 와 함께 사용할 수 있습니다.</p>
<pre><code class="language-cpp">int main()
{
    int a { 5 };            // a는 평범한 int입니다.

    const auto b { 5 };     // b는 const int입니다.
    constexpr auto c { 5 }; // c는 constexpr int입니다.

    return 0;
}</code></pre>
<hr>
<h3 id="추론할-단서가-꼭-필요합니다">추론할 &#39;단서&#39;가 꼭 필요합니다</h3>
<p><code>auto</code> 는 <strong>초기값을 보고 타입을 유추</strong>하는 것이므로, <strong>초기값이 아예 없거나 비어 있으면 작동하지 않습니다.</strong> 
또한, 반환값이 없는 <code>void 함수</code>를 <strong>초기값으로 넣어도 에러가 발생</strong>합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void foo()
{
}

int main()
{
    auto a;           // 에러: 컴파일러가 a의 타입을 추론할 단서가 없습니다.
    auto b { };       // 에러: 컴파일러가 b의 타입을 추론할 단서가 없습니다.
    auto c { foo() }; // 에러: void 타입(불완전한 타입)으로는 추론할 수 없습니다.

    return 0;
}</code></pre>
<hr>
<h3 id="auto의-장점">auto의 장점</h3>
<blockquote>
<p><strong>1. 코드 정렬이 깔끔해진다</strong>
변수 이름이 정렬되어 가독성이 좋아집니다.</p>
</blockquote>
<pre><code class="language-cpp">// 읽기 어려움
int a { 5 };
double b { 6.7 };
&gt;
// 읽기 쉬움
auto c { 5 };
auto d { 6.7 };</code></pre>
<blockquote>
<p><strong>2. 초기화 안 된 변수를 방지할 수 있다</strong>
<code>auto</code> 는 <strong>반드시 초기값이 있어야 하므로 실수로 초기화하지 않는 상황을 줄여</strong>줍니다.</p>
</blockquote>
<pre><code class="language-cpp">int x;   // 초기화 안 됨 (실수)
&gt;
auto y;  // 오류 발생 → 타입 추론 불가</code></pre>
<blockquote>
<p><strong>3. 불필요한 형 변환을 방지</strong>
<code>auto</code> 를 사용하면 반환 타입 그대로 사용하므로 불필요한 변환이 발생하지 않습니다.</p>
</blockquote>
<pre><code class="language-cpp">std::string_view getString(); // std::string_view를 반환하는 함수
std::string s1 { getString() }; // 나쁨: string_view → string 변환 (비용 발생)
auto s2 { getString() };        // 좋음: 변환 없음</code></pre>
<h3 id="auto의-단점">auto의 단점</h3>
<blockquote>
<p><strong>1. 타입이 코드에 직접 보이지 않는다</strong>
코드를 눈으로 읽을 때 객체의 정확한 타입을 알기 어렵습니다.</p>
</blockquote>
<pre><code class="language-cpp">auto y { 5 }; // 앗, double 타입이 필요했는데 실수로 정수를 넣어버려서 int로 추론되었습니다.</code></pre>
<blockquote>
</blockquote>
<p>다음은 또다른 예시 입니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt;
int main(){
    auto x { 3 };
    auto y { 2 };
&gt;    
    std::cout &lt;&lt; x / y &lt;&lt; &#39;\n&#39;; //소수점 나눗셈을 원했지만, 둘 다 int이므로 정수 나눗셈이 되어버립니다.
 &gt;    
    return 0;
}</code></pre>
<blockquote>
<p><strong>2. 초기값 타입이 바뀌면 변수 타입도 바뀐다</strong>
만약 나중에 누군가가 <code>add</code> 함수의 반환값이나 <code>gravity</code> 변수의 타입을 <code>int</code>에서 <code>double</code>로 수정한다면, 위 코드의 <code>sum</code> 역시 예기치 않게 <code>double</code>로 변해버립니다.</p>
</blockquote>
<pre><code class="language-cpp">auto sum { add(5, 6) + gravity };</code></pre>
<hr>
<h3 id="문자열-리터럴의-타입-추론">문자열 리터럴의 타입 추론</h3>
<p>다음 코드는 예상과 다르게 동작합니다. C++에서 문자열 리터럴은 역사적인 이유로 <code>const char*</code> 타입입니다.</p>
<pre><code class="language-cpp">auto s { &quot;Hello, world&quot; }; // s는 std::string이 아니라 const char*</code></pre>
<p>만약 문자열을 진짜 C++ 방식인 <code>std::string</code>이나 <code>std::string_view</code> 로 추론하게 만들고 싶다면, <code>s</code>나 <code>sv</code> 접미사를 꼭 붙여주어야 합니다.</p>
<pre><code class="language-cpp">#include &lt;string&gt;
#include &lt;string_view&gt;

int main()
{
    using namespace std::literals; // s와 sv 접미사를 쓰기 위한 가장 쉬운 방법입니다.

    auto s1 { &quot;goo&quot;s };  // &quot;goo&quot;s는 std::string 리터럴이므로, s1은 std::string으로 추론됩니다.
    auto s2 { &quot;moo&quot;sv }; // &quot;moo&quot;sv는 std::string_view 리터럴이므로, s2는 std::string_view로 추론됩니다.

    return 0;
}</code></pre>
<hr>
<h3 id="타입-추론은-const를-제거한다">타입 추론은 const를 제거한다</h3>
<p><code>auto</code> 는 기본적으로 <code>const</code> 를 제거합니다.</p>
<pre><code class="language-cpp">int main(){
    const int a { 5 }; // a는 const int
    auto b { a };      // b는 int (const 제거됨)
    return 0;
}</code></pre>
<p><code>const</code> 를 유지하려면 직접 써줘야 합니다.</p>
<pre><code class="language-cpp">const auto b { a };</code></pre>
<h3 id="constexpr과-타입-추론">constexpr과 타입 추론</h3>
<p><code>constexpr</code>은 암묵적으로 <code>const</code>의 성질을 가지는데, 이 역시 <code>auto</code> 를 쓰면 떨어져 나갑니다. 
유지하고 싶다면 직접 다시 적어주어야 합니다.</p>
<pre><code class="language-cpp">int main()
{
    constexpr double a { 3.4 };  // a는 &#39;const double&#39; 성질을 가집니다. (constexpr은 타입이 아니며, const가 암묵적으로 적용됨)

    auto b { a };                // b의 타입은 &#39;double&#39;입니다. (const가 떨어져 나감)
    const auto c { a };          // c의 타입은 &#39;const double&#39;입니다. (떨어진 const를 직접 다시 붙여줌)
    constexpr auto d { a };      // d의 타입은 &#39;const double&#39;입니다. (constexpr 키워드 때문에 const가 다시 적용됨)

    return 0;
}</code></pre>
<hr>
<h2 id="109--함수의-타입-추론-type-deduction-for-functions">10.9 — 함수의 타입 추론 (Type deduction for functions)</h2>
<h3 id="auto로-반환-타입-자동-추론하기">auto로 반환 타입 자동 추론하기</h3>
<p>컴파일러는 어차피 <code>return</code> 문을 보고 “이 값이 반환 타입으로 변환 가능한가?”를 검사해야 합니다.
그래서 C++14부터는** 함수의 반환 타입 자리에도** <code>auto</code>를 쓸 수 있게 확장되었습니다. 
즉, 반환 타입을 <strong>직접 쓰는 대신</strong> <code>auto</code>라고 적으면 <strong>컴파일러가 알아서 반환 타입을 추론</strong>합니다.</p>
<pre><code class="language-cpp">auto add(int x, int y){
    return x + y;
}</code></pre>
<p>여기서는 <code>return x + y;</code>가 <code>int</code> 값을 반환하므로, 컴파일러가 이 함수의 반환 타입을 <code>int</code>로 추론합니다.</p>
<hr>
<h3 id="auto-반환-타입을-쓰면-모든-return의-타입이-같아야-함">auto 반환 타입을 쓰면 모든 return의 타입이 같아야 함</h3>
<p><code>auto</code> 반환 타입을 사용할 때 주의할 점은, <strong>함수 안의 모든 **<code>return</code></strong> 문이 반드시 같은 타입의 값을 반환해야 한다는 것<strong>입니다. 
그렇지 않으면 **에러가 발생</strong>합니다.</p>
<pre><code class="language-cpp">auto someFcn(bool b){
    if (b)
        return 5;   // 반환 타입: int
    else
        return 6.7; // 반환 타입: double
}</code></pre>
<p>위 함수는 <code>return</code>이 <code>int</code>와 <code>double</code>로 <strong>서로 다르기 때문에 컴파일러가 오류</strong>를 냅니다.
만약 <strong>“상황에 따라 다른 타입을 반환”</strong> 같은 동작을 원한다면, 보통은 다음 중 하나로 해결합니다.</p>
<blockquote>
<p><strong>반환 타입을 명시적으로 적기</strong>
그러면 컴파일러는 반환 타입에 맞게(가능하면) 암시적 변환을 시도합니다. <br>
<strong>모든 <code>return</code> 값을 같은 타입으로 맞추기</strong>
예를 들어 위 코드에서는 <code>5</code>를 <code>5.0</code>으로 바꾸면 둘 다 <code>double</code>이 됩니다. 
리터럴이 아닌 타입이라면 <code>static_cast</code> 같은 명시적 변환을 사용할 수도 있습니다.</p>
</blockquote>
<hr>
<h3 id="반환-타입-자동-추론의-장점">반환 타입 자동 추론의 장점</h3>
<p>가장 큰 장점은 <strong>반환 타입 불일치로 인한 실수를 줄여준다는 점</strong>입니다.
즉, <strong>“내가 반환 타입을 잘못 적어서 원하지 않는 변환이 일어나는 상황”</strong>을 막는 데 도움이 됩니다.</p>
<p>또 다른 경우로, 함수의 반환 타입이 <strong>너무 길고 복잡하거나 한눈에 파악하기 힘들 때</strong> <code>auto</code>를 사용해 코드를 단순하게 만들 수 있습니다.</p>
<pre><code class="language-cpp">// 컴파일러가 unsigned short와 char를 더한 결과의 타입을 스스로 결정하도록 합니다.
auto add(unsigned short x, char y){
    return x + y;
}</code></pre>
<hr>
<h3 id="반환-타입-자동-추론의-단점">반환 타입 자동 추론의 단점</h3>
<p>편리해 보이지만 두 가지 큰 단점도 존재합니다.</p>
<blockquote>
<ol>
<li><code>auto</code> 반환 타입을 사용하는 함수는 <strong>사용되기 전</strong>에 <strong>&#39;완전히&#39; 정의되어 있어야 합니다.</strong> 
함수의 껍데기만 보여주는 전방 선언만으로는 부족합니다.</li>
</ol>
</blockquote>
<pre><code class="language-cpp">#include &lt;iostream&gt;
&gt;
auto foo();
&gt;
int main(){
    std::cout &lt;&lt; foo() &lt;&lt; &#39;\n&#39;; // 이 시점에서 컴파일러는 함수의 전방 선언만 본 상태입니다.
&gt;
    return 0;
}
&gt;
auto foo(){
    return 5;
}</code></pre>
<blockquote>
<ol start="2">
<li>객체에서 타입 추론을 할 때는** 보통 같은 문장 안에 초기값이 같이 있어서**, “어떤 타입이 추론될지” 비교적 알기 쉽습니다.</li>
</ol>
</blockquote>
<p>하지만 함수는 다릅니다. 함수의 <strong>선언(프로토타입)</strong>만 보면 실제로 어떤 타입을 반환하는지 알 수 없습니다. 좋은 IDE는 “추론된 반환 타입”을 보여주겠지만, 그런 도움 없이 코드를 읽는 사람은 함수 본문을 직접 열어봐야 반환 타입을 알 수 있습니다. 그만큼 실수할 가능성도 올라갑니다.</p>
<blockquote>
</blockquote>
<p>일반적으로 우리는 <strong>인터페이스(함수 선언은 인터페이스입니다)</strong>에 포함되는 타입은 명시적으로 적는 쪽을 더 선호합니다.</p>
<blockquote>
</blockquote>
<p>그래서 <strong>함수 반환 타입 추론은</strong> 객체 타입 추론만큼 “이게 정답”이라는 합의가 강하지 않습니다. 이
강의에서는 대체로 함수 반환 타입 추론을 피하는 것을 권장합니다.</p>
<hr>
<h3 id="후행-반환-타입-trailing-return-type-문법">후행 반환 타입 (Trailing return type) 문법</h3>
<p><code>auto</code> 키워드는 반환 타입을 알아서 추론해 달라는 뜻 외에도, <strong>반환 타입을 함수 선언의 제일 끝에 적는 &#39;후행 반환 문법&#39;</strong>을 사용할 때도 쓰입니다.</p>
<pre><code class="language-cpp">int add(int x, int y){
  return (x + y);
}</code></pre>
<p><strong>후행 반환 문법</strong>을 사용하면 위 코드를 아래처럼 똑같이 작성할 수 있습니다.</p>
<pre><code class="language-cpp">auto add(int x, int y) -&gt; int{
  return (x + y);
}</code></pre>
<p>여기서 <code>auto</code>는 타입 추론을 하는 게 아닙니다.
그냥 “뒤에 반환 타입을 쓰는 문법”을 만들기 위한 <strong>구성 요소</strong>일 뿐입니다.</p>
<h4 id="왜-이런-문법을-쓸까">왜 이런 문법을 쓸까?</h4>
<p><strong>1) 반환 타입이 복잡할 때 읽기 쉬움</strong></p>
<pre><code class="language-cpp">#include &lt;type_traits&gt; // std::common_type를 사용하기 위해

std::common_type_t&lt;int, double&gt; compare(int, double);         // 읽기 어려움(함수 이름이 어디에 있는지 한눈에 안 들어옴)
auto compare(int, double) -&gt; std::common_type_t&lt;int, double&gt;; // 읽기 쉬움(반환 타입이 필요할 때만 보면 됨)</code></pre>
<p>*<em>2) 여러 함수 선언을 깔끔하게 정렬할 수 있음
*</em></p>
<pre><code class="language-cpp">auto add(int x, int y) -&gt; int;
auto divide(double x, double y) -&gt; double;
auto printSomething() -&gt; void;
auto generateSubstring(const std::string &amp;s, int start, int len) -&gt; std::string;</code></pre>
<hr>
<h3 id="함수-매개변수-타입에는-타입-추론을-쓸-수-없음기본적으로">함수 매개변수 타입에는 타입 추론을 쓸 수 없음(기본적으로)</h3>
<p>타입 추론을 배우면, 초보자들이 종종 이런 코드를 시도합니다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void addAndPrint(auto x, auto y){
    std::cout &lt;&lt; x + y &lt;&lt; &#39;\n&#39;;
}

int main(){
    addAndPrint(2, 3);     // 경우 1: int 인자로 호출
    addAndPrint(4.5, 6.7); // 경우 2: double 인자로 호출

    return 0;
}</code></pre>
<p>안타깝게도 C++20 이전에는, 함수 매개변수에 <code>auto</code>를 쓰는 <strong>“그런 의미의 타입 추론”</strong>이 동작하지 않아서 위 코드는 컴파일되지 않습니다. 
(함수 매개변수는 <code>auto</code> 타입을 가질 수 없다는 오류가 납니다)</p>
<p>C++20에서는 위 프로그램이 컴파일되고 제대로 동작하긴 합니다. 하지만 이때의 <code>auto</code>는 <strong>“그냥 타입 추론”</strong>이 아니라, 이런 상황을 처리하기 위해 설계된** 함수 템플릿(function templates)** 기능을 사용하게 만드는 역할을 합니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 9]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-9</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-9</guid>
            <pubDate>Tue, 17 Feb 2026 10:47:38 GMT</pubDate>
            <description><![CDATA[<h2 id="코드-커버리지-code-coverage">코드 커버리지 (Code coverage)</h2>
<ul>
<li><strong>코드 커버리지</strong>라는 용어는 테스트를 진행하는 동안 <strong>프로그램의 소스 코드가 얼마나 실행되었는지를 나타낼 때 사용</strong>합니다. </li>
<li>코드 커버리지를 측정하는 <strong>지표(Metric)는 매우 다양</strong>해요.</li>
</ul>
<hr>
<h2 id="구문-커버리지-statement-coverage">구문 커버리지 (Statement coverage)</h2>
<ul>
<li><strong>구문 커버리지</strong>는 작성하신 테스트 루틴에 의해 실행된 <strong>코드 내 구문의 비율</strong>을 의미합니다.</li>
<li>다음 함수를 살펴볼까요?</li>
</ul>
<pre><code class="language-cpp">int foo(int x, int y)
{
    int z{ y };
    if (x &gt; y)
    {
        z = x;
    }
    return z;
}</code></pre>
<ul>
<li>이 함수를 <code>foo(1, 0)</code>으로 호출하면 <strong>함수 내의 모든 구문이 빠짐없이 실행</strong>됩니다.</li>
<li>함수에 대해 <strong>완벽한 구문 커버리지(100%)</strong>를 얻을 수 있습니다.</li>
<li>다음으로 우리의 <code>isLowerVowel()</code> 함수를 살펴볼게요.</li>
</ul>
<pre><code class="language-cpp">bool isLowerVowel(char c)
{
    switch (c) // 구문 1
    {
    case &#39;a&#39;:
    case &#39;e&#39;:
    case &#39;i&#39;:
    case &#39;o&#39;:
    case &#39;u&#39;:
        return true; // 구문 2
    default:
        return false; // 구문 3
    }
}</code></pre>
<ul>
<li>한 번의 함수 호출만으로는 구문 2와 구문 3에 동시에 도달할 수 없어요.</li>
<li>따라서 모든 구문을 테스트하려면 이 함수를 <strong>두 번 호출</strong>해야 합니다.</li>
</ul>
<hr>
<h2 id="분기-커버리지-branch-coverage">분기 커버리지 (Branch coverage)</h2>
<ul>
<li><strong>분기 커버리지</strong>는 <strong>실행된 분기의 비율을 의미</strong>하며, 발생 가능한 각 분기를 <strong>개별적으로 계산</strong>합니다.</li>
<li><code>if 문</code>에는 <strong>두 개의 분기</strong>가 있어요. 조건이 참일 때 실행되는 분기와, 거짓일 때 실행되는 분기입니다.</li>
<li><code>switch 문</code>은 더 많은 분기를 가질 수 있습니다.</li>
</ul>
<hr>
<h2 id="루프-커버리지-loop-coverage">루프 커버리지 (Loop coverage)</h2>
<ul>
<li><strong>루프 커버리지</strong>는 비공식적으로 <code>0</code> <code>1</code> <code>2</code> 테스트라고도 불리는데요.</li>
<li>코드에 루프(반복문)가 있다면 <code>0번</code> <code>1번</code> <code>2번</code> 반복될 때 제대로 작동하는지 확인해야 한다는 규칙입니다.</li>
<li>만약 <code>2번</code> 반복되는 경우에 잘 작동한다면, <code>2</code><strong>보다 큰 모든 반복 횟수에 대해서도 올바르게 작동해야 하니까</strong>요.</li>
<li>루프는 <strong>음수 번 실행될 수 없으므로</strong>, 이 세 가지 테스트면 모든 가능성을 충분히 다루게 됩니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void spam(int timesToPrint)
{
    for (int count{ 0 }; count &lt; timesToPrint; ++count)
         std::cout &lt;&lt; &quot;Spam! &quot;;
}</code></pre>
<ul>
<li>이 함수 내의 루프를 제대로 테스트하려면 세 번 호출해 보아야 해요.</li>
<li><code>0</code>번 반복을 테스트하는 <code>spam(0)</code></li>
<li><code>1</code>번 반복을 테스트하는 <code>spam(1)</code> </li>
<li><code>2</code>번 반복을 테스트하는 <code>spam(2)</code></li>
<li><code>spam(2)</code>가 제대로 작동한다면, <code>n</code>이 <code>2</code>보다 큰 <code>spam(n)</code>도 <strong>당연히 잘 작동</strong>할 거예요.</li>
</ul>
<hr>
<h2 id="다양한-카테고리의-입력-테스트하기-testing-different-categories-of-input">다양한 카테고리의 입력 테스트하기 (Testing different categories of input)</h2>
<ul>
<li><strong>매개변수를 받는 함수</strong>를 작성하거나 <strong>사용자의 입력을 받을 때</strong>는, 다양한 카테고리의 <strong>입력이 주어졌을 때 어떤 일이 일어날지 꼭 고려</strong>해야 합니다. 여기서 &#39;<strong>카테고리</strong>&#39;란 비슷한 특성을 가진 입력들의 집합을 의미해요.</li>
</ul>
<ul>
<li>예를 들어, 제가 정수의 제곱근을 구하는 함수를 작성했다고 가정해 볼게요. 어떤 값으로 테스트하는 것이 타당할까요?</li>
<li>아마 <strong>4와 같은 평범한 값으로 먼저 시작</strong>해 보겠죠. 하지만 <strong>여기서 그치지 않고 <code>0</code>이나 <code>음수</code>로도 테스트</strong>해 보는 것이 아주 좋은 생각입니다.</li>
<li>카테고리 테스트를 위한 몇 가지 기본 가이드라인은 다음과 같아요.</li>
</ul>
<blockquote>
<p><strong>정수(Integer)</strong></p>
</blockquote>
<ul>
<li>정수의 경우, 함수가 <code>음수</code> <code>0</code> <code>양수</code>를 어떻게 처리하는지 반드시 확인하세요. </li>
<li>또한 상황에 따라 오버플로우 문제도 꼭 체크해야 합니다.
ㅤ</li>
<li><em>부동 소수점 숫자(Floating point number)*</em></li>
<li>부동 소수점 숫자의 경우, 정밀도 문제를 가진 값들을 함수가 어떻게 처리하는지 고려해 보세요.</li>
<li>테스트하기에 좋은 <code>double</code> 자료형 값으로는 예상보다 약간 큰 숫자를 테스트하기 위한 <code>0.1</code>과 <code>-0.1</code> </li>
<li>그리고 예상보다 약간 작은 숫자를 테스트하기 위한 <code>0.7</code>과 <code>-0.7</code>이 있습니다.
ㅤ</li>
<li><em>문자열(String)*</em></li>
<li>문자열의 경우, 함수가 <code>빈 문자열</code> <code>영숫자 문자열</code> <code>공백이 포함된 문자열(앞, 뒤, 중간 포함)</code> </li>
<li>그리고 <code>오직 공백</code>으로만 이루어진 문자열을 어떻게 처리하는지 확인하세요.
ㅤ</li>
<li><em>포인터(Pointer)*</em></li>
<li>만약 함수가 포인터를 취한다면, <code>nullptr</code>도 잊지 말고 꼭 테스트해 보세요.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/7bd21b21-09c8-4559-bfd9-98ddc44a292a/image.png" alt=""></p>
<h2 id="c에서-흔히-발생하는-의미론적-오류-common-semantic-errors-in-c">C++에서 흔히 발생하는 의미론적 오류 (Common semantic errors in C++)</h2>
<ul>
<li>C++ 언어의 문법에 맞지 않는 코드를 작성했을 때 발생하는 <strong>구문 오류(Syntax errors)</strong>에 대해 다루었어요. </li>
<li>컴파일러가 이러한 오류를 친절하게 알려주기 때문에 발견하기 쉽고 고치기도 아주 간단하답니다.</li>
</ul>
<ul>
<li>우리가 의도한 대로 코드가 작동하지 않을 때 발생하는 <strong>의미론적 오류(Semantic errors)</strong>에 대해서도 배웠죠.</li>
<li>똑똑한 컴파일러가 경고를 보내는 일부 경우를 제외하면, 보통 이런 의미론적 오류를 스스로 잡아내지 못해요.</li>
</ul>
<ul>
<li>프로그램을 작성하다 보면 의미론적 오류를 저지르는 것은 거의 피할 수 없는 일입니다.</li>
<li>하지만 여기에 도움이 되는 또 한 가지 방법이 있어요. </li>
<li>바로 <strong>어떤 종류의 의미론적 오류가 가장 흔하게 발생하는지 미리 알아두는 것</strong>입니다.</li>
</ul>
<hr>
<h2 id="조건부-논리-오류-conditional-logic-errors">조건부 논리 오류 (Conditional logic errors)</h2>
<ul>
<li>가장 흔한 의미론적 오류 중 하나는 바로 <strong>조건부 논리 오류</strong>입니다.</li>
<li>조건부 논리 오류는 프로그래머가 <strong>조건문</strong>이나 <strong>루프 조건</strong>의 논리를 잘못 코딩했을 때 발생해요.</li>
<li>간단한 예를 들어볼게요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    std::cout &lt;&lt; &quot;Enter an integer: &quot;;
    int x{};
    std::cin &gt;&gt; x;

    if (x &gt;= 5) // 앗, operator&gt; 대신 operator&gt;=를 사용했네요
        std::cout &lt;&lt; x &lt;&lt; &quot; is greater than 5\n&quot;;

    return 0;
}</code></pre>
<ul>
<li>조건부 논리 오류가 나타나는 프로그램의 실행 결과는 다음과 같습니다.</li>
<li>사용자가 <code>5</code>를 입력하면 조건식 <code>x &gt;= 5</code>가 <strong>참</strong>으로 평가되므로, 연결된 명령문이 실행되어 버립니다. <code>5</code>는 <code>5</code>보다 크지 않은데 말이죠.</li>
</ul>
<pre><code class="language-cpp">Enter an integer: 5
5 is greater than 5</code></pre>
<ul>
<li><code>for</code> 루프를 사용한 또 다른 예를 살펴볼까요?</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    std::cout &lt;&lt; &quot;Enter an integer: &quot;;
    int x{};
    std::cin &gt;&gt; x;

    // 앗, operator&lt; 대신 operator&gt;를 사용했네요
    for (int count{ 1 }; count &gt; x; ++count)
    {
        std::cout &lt;&lt; count &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li>이 프로그램은 원래 <code>1</code>부터 사용자가 입력한 숫자 사이의 모든 숫자를 출력해야 해요. </li>
<li>하지만 실제로는 이렇게 작동합니다.</li>
</ul>
<pre><code class="language-cpp">Enter an integer: 5</code></pre>
<ul>
<li>이런 일이 발생하는 이유는 <code>for</code> 루프에 진입할 때 <code>count &gt; x</code>가 <strong>거짓</strong>이 되기 때문에 루프가 <strong>단 한 번도 반복되지 않기 때문</strong>이랍니다.</li>
</ul>
<hr>
<h2 id="하나-차이-오류-off-by-one-errors">하나 차이 오류 (Off-by-one errors)</h2>
<ul>
<li><strong>하나 차이 오류</strong>는 루프가 의도한 것보다 <strong>딱 한 번 더 실행</strong>되거나 <strong>한 번 덜 실행</strong>될 때 발생하는 오류예요.</li>
<li>프로그래머는 아래 코드가 <code>1 2 3 4 5</code>를 출력하기를 원했어요. </li>
<li>하지만 <strong>잘못된 관계 연산자를 사용했기 때문에</strong>(<code>&lt;=</code> 대신 <code>&lt;</code> 사용), 루프가 의도한 것보다 한 번 덜 실행되어 <code>1 2 3 4</code>만 출력하게 됩니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    for (int count{ 1 }; count &lt; 5; ++count)
    {
        std::cout &lt;&lt; count &lt;&lt; &#39; &#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<hr>
<h2 id="잘못된-연산자-우선순위-incorrect-operator-precedence">잘못된 연산자 우선순위 (Incorrect operator precedence)</h2>
<ul>
<li>6챕터 &#39;<strong>논리 연산자</strong>&#39;에서 가져온 다음 프로그램은 연산자 <strong>우선순위와 관련된 실수</strong>를 보여줍니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int x{ 5 };
    int y{ 7 };

    if (!x &gt; y) // 앗: 연산자 우선순위 문제가 발생했어요
        std::cout &lt;&lt; x &lt;&lt; &quot; is not greater than &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;
    else
        std::cout &lt;&lt; x &lt;&lt; &quot; is greater than &quot; &lt;&lt; y &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li><strong>논리 부정 연산자(Logical NOT <code>!</code>)</strong>가 <strong>비교 연산자(<code>&gt;</code>)</strong>보다 <strong>우선순위가 더 높기 때문</strong>에, 
이 조건문은 <strong>프로그래머의 의도와 다르게</strong> <code>(!x) &gt; y</code>처럼 평가된답니다.</li>
<li>결과적으로 이 프로그램은 다음과 같이 출력합니다.</li>
</ul>
<pre><code class="language-cpp">5 is greater than 7</code></pre>
<ul>
<li>이런 문제는 동일한 표현식 안에서 <code>논리 OR</code>와 <code>논리 AND</code>를 섞어 쓸 때도 발생할 수 있어요.
(<code>논리 AND</code>가 <code>논리 OR</code>보다 <strong>우선순위가 높습니다.</strong>) </li>
<li>이런 종류의 오류를 피하려면 <strong>명시적으로 괄호를 사용</strong>하는 습관을 들이는 것이 아주 좋습니다.</li>
</ul>
<hr>
<h2 id="부동-소수점-자료형의-정밀도-문제-precision-issues-with-floating-point-types">부동 소수점 자료형의 정밀도 문제 (Precision issues with floating point types)</h2>
<ul>
<li>다음 부동 소수점 변수는 전체 숫자를 저장할 만큼 충분한 <strong>정밀도</strong>를 가지고 있지 않아요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    float f{ 0.123456789f };
    std::cout &lt;&lt; f &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li>이러한 정밀도 부족으로 인해 숫자가 다음과 같이 약간 <strong>반올림</strong>되어 버립니다.</li>
</ul>
<pre><code class="language-cpp">0.123457</code></pre>
<ul>
<li>6챕터에서, <strong>부동 소수점 숫자에</strong> <code>==</code> 연산자와 <code>!=</code> 연산자를 사용하는 것이 <strong>미세한 반올림 오차</strong>로 인해 얼마나 문제의 소지가 있는지(그리고 어떻게 대처해야 하는지) 이야기했었죠. 다음이 그 예입니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    double d{ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 }; // 1.0이 되어야 정상이죠

    if (d == 1.0)
        std::cout &lt;&lt; &quot;equal\n&quot;;
    else
        std::cout &lt;&lt; &quot;not equal\n&quot;;

    return 0;
}</code></pre>
<ul>
<li>이 프로그램은 다음과 같이 출력합니다.</li>
</ul>
<pre><code class="language-cpp">not equal</code></pre>
<ul>
<li>부동 소수점 숫자로 산술 연산을 많이 하면 할수록, 이런 작은 <strong>반올림 오차</strong>들이 <strong>점점 더 눈덩이처럼 누적</strong>되게 됩니다.
<img src="https://velog.velcdn.com/images/dj_trace25/post/9e5f73c8-5c9a-4484-a429-aa1d91d73016/image.png" alt=""></li>
</ul>
<h2 id="오류-탐지와-처리-detecting-and-handling-errors">오류 탐지와 처리 (Detecting and handling errors)</h2>
<ul>
<li>위에서 초보 C++ 프로그래머들이 언어를 다루면서 흔히 겪는 여러 유형의 <strong>의미론적 오류</strong>들을 살펴보았습니다.</li>
<li>만약 오류가 언어의 기능을 잘못 사용했거나 논리적인 실수로 인해 발생한 것이라면, 
그 오류는 <strong>단순히 코드를 수정함으로써 바로잡을 수 있습니다.</strong></li>
</ul>
<ul>
<li>하지만 프로그램에서 발생하는 대부분의 오류는 언어의 기능을 무심코 잘못 사용해서 일어나는 것이 아닙니다. </li>
<li>오히려 대부분의 오류는 <strong>프로그래머의 잘못된 가정</strong>이나 <strong>적절한 오류 탐지 및 처리의 부재 때문에 발생</strong>한답니다.</li>
<li>이렇게 잘못된 가정이 흔히 발생하는 <strong>세 가지 주요 지점</strong>이 있습니다.</li>
</ul>
<blockquote>
</blockquote>
<ul>
<li><p><strong>함수가 반환될 때</strong> 호출된 함수가 실제로는 실패했는데도, 프로그래머는 성공했다고 가정할 수 있습니다.</p>
</li>
<li><p><strong>프로그램이 입력(사용자 또는 파일로부터)을 받을 때</strong> 입력값이 실제로는 잘못되었음에도, 
프로그래머는 입력 형식이 올바르고 의미상으로 유효하다고 가정할 수 있습니다.</p>
</li>
<li><p><strong>함수가 호출되었을 때</strong> 전달된 인수(Arguments)가 실제로는 유효하지 않은데도, 프로그래머는 의미상 유효할 것이라고 가정할 수 있습니다.</p>
</li>
<li><p>많은 초보 프로그래머들은 코드를 작성한 후, 오류가 전혀 없는 정상적인 경로, 즉 <strong>해피 패스(Happy path)</strong>만 테스트하곤 합니다. </p>
</li>
<li><p>하지만 여러분은 상황이 꼬이고 잘못될 수 있는 <strong>새드 패스(Sad paths)</strong>에 대해서도 반드시 계획하고 테스트해야 합니다.</p>
</li>
</ul>
<hr>
<h2 id="함수에서-오류-처리하기-handling-errors-in-functions">함수에서 오류 처리하기 (Handling errors in functions)</h2>
<ul>
<li>함수는 정말 다양한 이유로 실패할 수 있습니다.</li>
<li>호출자(Caller)가 유효하지 않은 값을 인수로 전달했을 수도 있고, 함수 본문 내부에서 무언가 실패했을 수도 있죠. </li>
<li>예를 들어, 읽기 모드로 파일을 여는 함수는 해당 파일을 찾을 수 없을 때 실패하게 됩니다.</li>
</ul>
<ul>
<li>이런 일이 발생했을 때, 여러분이 마음대로 사용할 수 있는 꽤 많은 선택지가 있습니다. </li>
<li>무조건 &#39;이게 최고의 방법이다!&#39; 하는 정답은 없어요.</li>
<li>문제의 본질이 무엇인지, 그리고 그 문제를 고칠 수 있는지 없는지에 따라 크게 좌우되거든요.</li>
<li>일반적으로 사용할 수 있는 <strong>4가지 전략</strong>은 다음과 같습니다.</li>
</ul>
<blockquote>
<ol>
<li>함수 내부에서 오류 처리하기</li>
<li>호출자(Caller)에게 오류를 넘겨서 처리하게 하기</li>
<li>프로그램 중단하기</li>
<li>예외(Exception) 던지기</li>
</ol>
</blockquote>
<hr>
<h2 id="함수-내부에서-오류-처리하기-handling-the-error-within-the-function">함수 내부에서 오류 처리하기 (Handling the error within the function)</h2>
<ul>
<li>가능하다면 오류가 발생한 바로 <strong>그 함수 안에서 오류를 복구</strong>하는 것이 가장 좋은 전략입니다. </li>
<li>그래야 함수 외부의 다른 코드에 영향을 주지 않고 오류를 격리하여 바로잡을 수 있기 때문이죠. </li>
<li>여기에는 <strong>&#39;성공할 때까지 재시도하기&#39;</strong>와 <strong>&#39;실행 중인 작업 취소하기&#39;</strong>라는 두 가지 선택지가 있습니다.</li>
</ul>
<ul>
<li>만약 프로그램이 통제할 수 없는 외부 요인 때문에 오류가 발생했다면, 프로그램은 성공할 때까지 재시도를 할 수 있습니다. </li>
<li>예를 들어, 프로그램에 인터넷 연결이 필요한데 사용자의 연결이 끊어졌다면, 프로그램은 경고 메시지를 띄운 뒤 반복문을 사용해 주 기적으로 인터넷 연결 상태를 다시 확인할 수 있습니다.</li>
</ul>
<ul>
<li>또 다른 예로, 사용자가 잘못된 값을 입력했다면, 프로그램은 사용자에게 다시 시도해 달라고 요청하고 유효한 값을 입력할 때까지 - - 반복할 수 있습니다. 잘못된 입력을 처리하고 반복문을 사용해 재시도하는 예제는 후술에서 더 자세히 보여드리도록 하겠습니다.</li>
<li>또 다른 전략은 <strong>오류를 그냥 무시하거나 작업을 취소하는 것</strong>입니다. 다음 코드를 볼까요?</li>
</ul>
<pre><code class="language-cpp">// y가 0일 경우, 아무런 경고 없이 조용히 실패(종료)합니다.
void printIntDivision(int x, int y)
{
    if (y != 0)
        std::cout &lt;&lt; x / y;
}</code></pre>
<ul>
<li>위의 예제에서 사용자가 <code>y</code>에 유효하지 않은 값 <code>0</code>을 전달하면, 우리는 나눗셈 결과를 출력해 달라는 요청을 그냥 무시해 버립니다. </li>
<li>하지만 이 방식의 가장 큰 문제점은 <strong>호출자나 사용자가 무언가 잘못되었다는 사실을 알아챌 방법이 없다</strong>는 거예요. </li>
<li>이런 경우에는 오류 메시지를 출력해 주는 것이 큰 도움이 될 수 있습니다.</li>
</ul>
<pre><code class="language-cpp">void printIntDivision(int x, int y)
{
    if (y != 0)
        std::cout &lt;&lt; x / y;
    else
        std::cout &lt;&lt; &quot;Error: Could not divide by zero\n&quot;; // 오류: 0으로 나눌 수 없습니다.
}</code></pre>
<ul>
<li>하지만, 호출하는 함수 측에서 호출된 함수가 <strong>어떤 반환값을 주거나 유용한 부수 효과를 일으킬 것으로 기대하고 있다면</strong>, 단순히 오류를 무시하는 것은 올바른 선택지가 될 수 없습니다.</li>
</ul>
<hr>
<h2 id="호출자에게-오류-넘기기-passing-errors-back-to-the-caller">호출자에게 오류 넘기기 (Passing errors back to the caller)</h2>
<ul>
<li>오류를 발견한 함수 안에서 오류를 적절히 처리할 수 없는 경우도 아주 많습니다. </li>
<li>예를 들어, 다음 함수를 한 번 살펴볼까요?</li>
</ul>
<pre><code class="language-cpp">int doIntDivision(int x, int y)
{
    return x / y;
}</code></pre>
<ul>
<li>만약 <code>y</code>가 <code>0</code>이라면 우리는 어떻게 해야 할까요? 이 함수는 반드시 어떤 값이든 반환해야 하므로, 프로그램 로직을 그냥 건너뛸 수는 없습니다. 그렇다고 사용자에게 <code>y</code>의 새 값을 입력하라고 요청해서도 안 됩니다. 이 함수는 순수한 &#39;계산용 함수&#39;이기 때문에, 여기에 입력 루틴을 집어넣는 것은 이 함수를 호출하는 프로그램의 성격에 맞을 수도 있고 안 맞을 수도 있기 때문이죠.</li>
<li>이런 상황에서 가장 좋은 선택은, <strong>호출자가 이 상황을 알아서 처리해 주기를 바라며 오류를 호출자에게 다시 넘겨주는 것(Pass back)</strong>입니다.</li>
<li>어떻게 할 수 있을까요? 함수의 반환 타입이 <code>void</code>라면, 이를 성공이나 실패를 나타내는 <code>bool</code> 타입으로 바꿀 수 있습니다. </li>
<li>예를 들어, 아래와 같이 작성하는 대신 이렇게 바꿀 수 있습니다.</li>
</ul>
<pre><code class="language-cpp">bool printIntDivision(int x, int y)
{
    if (y == 0)
    {
        std::cout &lt;&lt; &quot;Error: could not divide by zero\n&quot;;
        return false; // 실패했음을 알립니다.
    }

    std::cout &lt;&lt; x / y;

    return true; // 성공했음을 알립니다.
}</code></pre>
<ul>
<li>이렇게 하면 호출자는 <strong>반환값을 확인해서 함수가 어떤 이유로든 실패했는지를 알아낼 수 있습니다.</strong></li>
</ul>
<hr>
<h2 id="치명적-오류-fatal-errors">치명적 오류 (Fatal errors)</h2>
<ul>
<li>오류가 너무 심각해서 프로그램이 정상적으로 작동을 계속할 수 없다면, 이를 복구 불가능한 오류 또는 <strong>치명적 오류</strong>라고 부릅니다. </li>
<li>이럴 때는 <strong>프로그램을 종료하는 것이 최선</strong>입니다.</li>
</ul>
<ul>
<li>여러분의 코드가 <code>main()</code> 함수 안에 있거나 <code>main()</code>에서 직접 호출된 함수 안에 있다면, </li>
<li><em><code>main()</code>이 <code>0</code>이 아닌 상태 코드를 반환하게 하는 것이 가장 좋습니다.*</em></li>
<li>하지만, 깊게 중첩된 하위 함수 내부라면 오류를 <code>main()</code>까지 끝까지 전달하는 것이 불편하거나 불가능할 수도 있습니다. </li>
<li>이런 경우에는 <code>std::exit()</code>와 같은 <strong>중단 문</strong>을 사용할 수 있습니다.</li>
</ul>
<pre><code class="language-cpp">double doIntDivision(int x, int y)
{
    if (y == 0)
    {
        std::cout &lt;&lt; &quot;Error: Could not divide by zero\n&quot;;
        std::exit(1); // 프로그램을 즉시 종료하고 상태 코드 1을 반환합니다.
    }
    return x / y;
}</code></pre>
<hr>
<h2 id="예외-exceptions">예외 (Exceptions)</h2>
<ul>
<li>함수에서 발생한 오류를 호출자에게 다시 넘겨주는 과정이 복잡하기 때문에 C++은 오류를 호출자에게 넘겨주는 완전히 독립된 방법을 제공합니다. 바로 <strong>예외(Exceptions)</strong>입니다.</li>
</ul>
<ul>
<li>기본적인 아이디어는 이렇습니다. 오류가 발생하면 예외가 &quot;던져집니다(Thrown)&quot;. 현재 함수가 그 오류를 &quot;잡지(Catch)&quot; 않으면, 이 함수를 호출한 호출자가 오류를 잡을 기회를 얻게 됩니다. </li>
<li>만약 그 호출자도 오류를 잡지 않으면, 호출자의 호출자가 다시 기회를 얻습니다. 이렇게 오류는 누군가에게 잡혀서 처리될 때까지(처리되면 프로그램은 다시 정상적으로 실행됩니다), 또는 main() 함수마저 오류를 처리하지 못해 예외 오류와 함께 프로그램이 강제 종료될 때까지 콜 스택(Call stack)을 타고 위로 점진적으로 이동합니다.</li>
<li>예외 처리에 대한 자세한 내용은 27 챕터에서 다룰 예정입니다.
<img src="https://velog.velcdn.com/images/dj_trace25/post/b34c9a12-cb41-493b-b7e5-67f185020d0b/image.png" alt=""></li>
</ul>
<h2 id="stdcin과-잘못된-입력-처리하기">std::cin과 잘못된 입력 처리하기</h2>
<ul>
<li>여러분이 지금까지 작성해 온 프로그램에서는 <code>std::cin</code>을 사용하여 사용자에게 텍스트 입력을 요청해 왔습니다.</li>
<li>텍스트 입력은 형태가 매우 자유롭기 때문에 프로그램이 예상하지 못한 잘못된 입력을 받기가 아주 쉽답니다.</li>
<li>프로그램을 작성할 때는 사용자가 여러분의 프로그램을 어떻게 사용할지 항상 고민해야 해요.</li>
<li>잘 만들어진 프로그램은 사용자의 오용을 미리 예상하고 그런 상황을 부드럽게 처리하거나 가능하다면 아예 발생하지 않도록 막아냅니다.</li>
<li>이렇게 <strong>오류 상황을 잘 처리하는 프로그램을 가리켜 견고한(Robust) 프로그램</strong>이라고 불러요.</li>
</ul>
<ul>
<li>이번 강의에서는 사용자가 <code>std::cin</code>을 통해 유효하지 않은 텍스트를 입력하는 구체적인 사례들을 살펴보고, 
이런 상황들을 처리하는 여러 가지 방법을 알려드릴 거예요.</li>
<li>다음은 입력 과정에서 <code>operator&gt;&gt;</code>가 작동하는 방식을 간단히 정리한 것입니다.</li>
</ul>
<blockquote>
<ol>
<li>가장 먼저, <strong>입력 버퍼</strong> 의 맨 앞에 있는 선행 공백(스페이스, 탭, 줄바꿈 문자 등)이 제거됩니다. 
이 과정을 통해 이전 입력에서 버퍼에 남아있던 처리되지 않은 줄바꿈 문자들이 지워지게 돼요.
ㅤ</li>
<li>만약 입력 버퍼가 비어 있다면 <code>operator&gt;&gt;</code>는 사용자가 새로운 데이터를 입력할 때까지 기다립니다. 
사용자가 입력을 마치면 다시 선행 공백을 제거해요.
ㅤ</li>
<li>그런 다음 <code>operator&gt;&gt;</code>는 줄바꿈 문자를 만나거나, 
혹은 저장하려는 변수 타입에 맞지 않는 유효하지 않은 문자를 만날 때까지 연속된 문자들을 최대한 많이 <strong>추출(Extract)</strong>합니다.
ㅤ</li>
<li>추출 결과는 다음과 같이 나뉩니다.</li>
</ol>
</blockquote>
<ul>
<li>위 3번 단계에서 문자가 하나라도 추출되었다면, <strong>추출 성공</strong>입니다. 추출된 문자는 적절한 값으로 변환되어 변수에 할당(대입)됩니다.</li>
<li>만약 3번 단계에서** 아무 문자도 추출하지 못했다면, 추출 실패**입니다. (C++11 기준) 입력을 받으려던 객체에는 0이라는 값이 할당되며, <code>std::cin</code>의 상태가 초기화될 때까지 이후의 모든 추출 시도는 즉시 실패하게 됩니다.</li>
</ul>
<hr>
<h2 id="입력-유효성-검사-validating-input">입력 유효성 검사 (Validating input)</h2>
<ul>
<li>사용자의 입력이 프로그램이 기대하는 형태와 일치하는지 확인하는 과정을 <strong>입력 유효성 검사(Input validation)</strong>라고 해요.</li>
<li>입력 유효성을 검사하는 기본 방식에는 크게 세 가지가 있습니다.</li>
</ul>
<blockquote>
<p><strong>입력 중 검사 (Inline: 사용자가 타이핑할 때)</strong></p>
</blockquote>
<ul>
<li>애초에 사용자가 잘못된 입력을 타이핑하지 못하도록 막는 방식이에요.</li>
</ul>
<blockquote>
<p><strong>입력 후 검사 (Post-entry: 사용자가 타이핑을 마친 후)</strong></p>
</blockquote>
<ul>
<li>사용자가 문자열(String) 형태로 원하는 것을 마음껏 입력하게 한 다음, 그 문자열이 올바른지 검증해요. </li>
<li>만약 올바르다면 문자열을 최종 변수 형식으로 변환합니다.</li>
<li>사용자가 무엇이든 입력하게 두고, <code>std::cin</code>과 <code>operator&gt;&gt;</code>가 입력을 추출하도록 시도한 뒤에 오류 상황을 처리합니다.</li>
</ul>
<ul>
<li>문자열(String)의 경우 어떤 문자를 입력하든 제한이 없기 때문에 추출은 항상 성공해요.</li>
<li>다만 <code>std::cin</code>은 첫 번째 공백 문자를 만나면 추출을 멈춘다는 점을 기억해 주세요.</li>
<li>일단 문자열이 입력되면 프로그램은 이 문자열을 분석(Parse)하여 유효한지 확인할 수 있습니다. </li>
<li>하지만 문자열을 분석하고 다른 타입(예: 숫자)으로 변환하는 작업은 꽤 까다로울 수 있어서 아주 드물게만 사용된답니다.</li>
<li>그래서 우리는 가장 자주, <code>std::cin</code><strong>과 추출 연산자에게 이 어려운 작업을 맡기는 방식</strong>을 씁니다.</li>
<li>사용자가 마음대로 입력하게 둔 뒤에 <code>std::cin</code>과 <code>operator&gt;&gt;</code>가 처리를 시도하고, 
만약 <strong>실패하면 그 후유증을 수습</strong>하는 거죠. 이 방법이 가장 쉬우며, 아래에서 더 자세히 이야기할 내용입니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/89510d34-dced-4dc0-bbeb-4dfd26861c99/image.png" alt=""></p>
<h2 id="단언문assertions">단언문(Assertions)</h2>
<ul>
<li><strong>단언문(Assertion)</strong>은 프로그램에 버그가 없는 한 <strong>항상 참이 될 표현식</strong>입니다.</li>
<li>표현식이 <strong>참(true)</strong>으로 평가되면 단언문은 아무 일도 하지 않고 넘어갑니다.</li>
<li>만약 표현식이 <strong>거짓(false)</strong>으로 평가되면, 오류 메시지가 표시되고 프로그램이 종료됩니다( <code>std::abort</code>를 통해).</li>
</ul>
<ul>
<li>이 오류 메시지에는 일반적으로 <strong>실패한 표현식의 텍스트, 코드 파일의 이름, 그리고 단언문이 있는 줄 번호가 포함</strong>돼요. </li>
<li>덕분에 무엇이 문제인지뿐만 아니라, 코드의 어디에서 문제가 발생했는지 아주 쉽게 알 수 있죠. </li>
<li>이는 디버깅 작업에 엄청난 도움이 됩니다.</li>
</ul>
<ul>
<li>C++에서 런타임 단언문은 <code>&lt;cassert&gt;</code> 헤더에 있는 <code>assert 전처리기 매크로</code>를 통해 구현됩니다.<pre><code class="language-cpp">#include &lt;cassert&gt; // assert()를 사용하기 위해 포함
#include &lt;cmath&gt; // std::sqrt를 사용하기 위해 포함
#include &lt;iostream&gt;
</code></pre>
</li>
</ul>
<p>double calculateTimeUntilObjectHitsGround(double initialHeight, double gravity)
{
  assert(gravity &gt; 0.0); // 양의 중력이 없으면 물체는 땅에 닿지 않습니다.</p>
<p>  if (initialHeight &lt;= 0.0)
  {
    // 물체는 이미 땅에 있거나 묻혀 있습니다.
    return 0.0;
  }</p>
<p>  return std::sqrt((2.0 * initialHeight) / gravity);
}</p>
<p>int main()
{
  std::cout &lt;&lt; &quot;Took &quot; &lt;&lt; calculateTimeUntilObjectHitsGround(100.0, -9.8) &lt;&lt; &quot; second(s)\n&quot;;</p>
<p>  return 0;
}</p>
<pre><code>---
## NDEBUG 매크로
- `assert` 매크로는 조건을 검사할 때마다 아주 약간의 성능 비용이 발생해요. 
- 게다가, 상용 서비스용으로 배포되는 실제 프로덕션 코드에서는 오류로 멈추는 일이 결코 발생해서는 안 됩니다.


- 따라서 대부분의 개발자는 **디버그 빌드에서만 단언문이 활성화되는 것을 선호**합니다. 
- C++에는 프로덕션 코드에서 단언문을 끌 수 있는 내장된 방법이 있어요. 
- 바로 `NDEBUG`라는 전처리기 매크로가 정의되어 있으면, `assert 매크로`가 **비활성화**되는 것입니다.


- 대부분의 IDE는 릴리스 구성에 대한 프로젝트 설정의 일부로 `NDEBUG`를 기본적으로 설정해 둡니다.
- 테스트 목적으로 특정 번역 단위 내에서 단언문을 활성화하거나 비활성화할 수 있습니다. 
- 그렇게 하려면 모든 `#include` 문구 이전에 별도의 줄에 `#define NDEBUG` 또는 `#undef NDEBUG` 중 하나를 배치하세요.

```cpp
#define NDEBUG // 단언문을 비활성화합니다 (반드시 모든 #include 이전에 위치해야 함)
#include &lt;cassert&gt;
#include &lt;iostream&gt;

int main()
{
assert(false); // 이 번역 단위에서는 단언문이 비활성화되었으므로 작동하지 않습니다.
std::cout &lt;&lt; &quot;Hello, world!\n&quot;;

return 0;
}</code></pre><hr>
<h2 id="static_assert-정적-단언문">static_assert (정적 단언문)</h2>
<ul>
<li>C++에는 <strong><code>static_assert</code></strong>라고 불리는 또 다른 유형의 단언문도 존재합니다.</li>
<li>일반 <code>assert</code>가 실행 시간에 검사하는 것과 달리, <code>static_assert</code><strong>는 컴파일 타임에 검사되는 단언문</strong>이에요. </li>
<li>그래서 <code>static_assert</code>가 실패하면 <strong>컴파일 오류</strong>가 발생하여 아예 프로그램이 만들어지지 않게 됩니다.</li>
</ul>
<ul>
<li>또한, <code>&lt;cassert&gt;</code> 헤더에 선언되어 있는 <code>assert</code> 매크로와는 다르게, <strong><code>static_assert</code>는 C++의 키워드</strong>입니다. </li>
<li>따라서 이를 사용하기 위해 별도의 헤더 파일을 포함할 필요가 없답니다.</li>
</ul>
<ul>
<li><p><code>static_assert</code>는 다음과 같은 형태를 가집니다.</p>
<pre><code class="language-cpp">static_assert(조건, 진단_메시지)</code></pre>
</li>
<li><p>조건이 참이 아니면 진단 메시지가 출력됩니다. </p>
</li>
<li><p>타입들이 특정한 크기를 가지는지 확인하기 위해 <code>static_assert</code>를 사용하는 예제를 살펴볼까요?</p>
</li>
</ul>
<pre><code class="language-cpp">static_assert(sizeof(long) == 8, &quot;long must be 8 bytes&quot;);
static_assert(sizeof(int) &gt;= 4, &quot;int must be at least 4 bytes&quot;);

int main()
{
    return 0;
}</code></pre>
<hr>
<h2 id="단언문asserts과-오류-처리error-handling의-차이점">단언문(Asserts)과 오류 처리(Error handling)의 차이점</h2>
<ul>
<li>언문과 오류 처리는 목적이 꽤 비슷해 보여서 헷갈리기 쉽습니다. 차이점을 명확히 정리해 드릴게요.</li>
</ul>
<blockquote>
<p><strong>단언문(Assertions)</strong>은 절대 일어나서는 안 되는 일에 대한 가정을 문서화하여, 개발 과정 중에 발생하는 프로그래밍 오류를 감지하는 데 사용됩니다. 만약 단언문에서 설정한 조건이 깨졌다면, 그건 100% 프로그래머의 잘못입니다. 또한 단언문은 오류로부터의 복구를 허용하지 않아요. (애초에 일어나선 안 될 일이 일어났으니 복구할 필요도 없는 셈이죠.)
단언문은 주로 릴리스 빌드에서 컴파일 시 제거되므로 성능 걱정 없이 아주 넉넉하게 많이 넣으셔도 좋습니다.</p>
</blockquote>
<blockquote>
<p><strong>오류 처리(Error handling)</strong>는 릴리스 빌드에서 (아무리 드물더라도) 실제로 발생할 수 있는 상황들을 우아하게 처리해야 할 때 사용됩니다. 이러한 문제들은 복구가 가능한 문제일 수도 있고(프로그램이 계속 실행될 수 있음), 복구가 불가능한 문제일 수도 있습니다(프로그램을 종료해야 하지만, 적어도 친절한 오류 메시지를 보여주고 자원들이 제대로 정리되도록 보장할 수 있음). 오류 감지와 처리는 런타임 성능 비용과 개발 시간 비용이 모두 발생합니다.</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 8]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-8</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-8</guid>
            <pubDate>Mon, 16 Feb 2026 15:40:03 GMT</pubDate>
            <description><![CDATA[<h2 id="제어-흐름control-flow-소개">제어 흐름(Control flow) 소개</h2>
<ul>
<li>CPU가 실행하는 구체적인 구문들의 순서를 프로그램의 <strong>실행 경로</strong> 또는 짧게 <strong>경로</strong>라고 부릅니다.</li>
<li>C++은 정상적인 실행 경로를 프로그래머가 마음대로 바꿀 수 있도록 도와주는 다양한 <strong>제어 흐름 구문</strong>을 제공하고 있어요.</li>
<li>이처럼 제어 흐름 구문이 실행 지점을 순차적이지 않은 다른 구문으로 이동시키는 것을 <strong>분기</strong>라고 부른답니다.</li>
</ul>
<hr>
<h2 id="흐름-제어-구문의-분류-categories-of-flow-control-statements">흐름 제어 구문의 분류 (Categories of flow control statements)</h2>
<table>
<thead>
<tr>
<th>분류 (Category)</th>
<th>의미 (Meaning)</th>
<th>C++ 구현 형태 (Implemented in C++ by)</th>
</tr>
</thead>
<tbody><tr>
<td>조건문 (Conditional statements)</td>
<td>특정 조건(Condition)이 충족될 때만 일련의 코드를 실행하도록 합니다.</td>
<td><code>if</code>, <code>else</code>, <code>switch</code></td>
</tr>
<tr>
<td>점프 (Jumps)</td>
<td>CPU에게 다른 위치에 있는 구문부터 실행을 시작하도록 지시합니다.</td>
<td><code>goto</code>, <code>break</code>, <code>continue</code></td>
</tr>
<tr>
<td>함수 호출 (Function calls)</td>
<td>다른 위치로 점프하여 실행한 뒤, 다시 원래 위치로 돌아옵니다.</td>
<td>함수 호출, <code>return</code></td>
</tr>
<tr>
<td>반복문 (Loops)</td>
<td>특정 조건이 충족될 때까지, 일련의 코드를 0번 이상 반복해서 실행합니다.</td>
<td><code>while</code>, <code>do-while</code>, <code>for</code>, ranged-<code>for</code></td>
</tr>
<tr>
<td>중단 (Halts)</td>
<td>프로그램을 완전히 종료시킵니다.</td>
<td><code>std::exit()</code>, <code>std::abort()</code></td>
</tr>
<tr>
<td>예외 (Exceptions)</td>
<td>오류 처리(Error handling)를 위해 특별히 설계된 흐름 제어 구조입니다.</td>
<td><code>try</code>, <code>throw</code>, <code>catch</code></td>
</tr>
</tbody></table>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/9ef6549e-4bdf-4024-bf8b-63cdc0160df2/image.png" alt=""></p>
<h2 id="constexpr-if문-c17-도입">Constexpr if문 (C++17 도입)</h2>
<ul>
<li><code>constexpr if</code>문의 가장 큰 특징은 조건식이 프로그램 실행 중이 아니라 <strong>컴파일 타임</strong>에 평가된다는 점이에요.</li>
<li>만약 <code>constexpr</code> 조건식이 true로 평가되면, <code>if-else</code>문 <strong>전체 구조가</strong> &#39;참일 때 실행되는 문장&#39;으로 완전히 대체됩니다.</li>
<li>반대로 false로 평가되면, <code>if-else</code>문 전체가 &#39;거짓일 때 실행되는 문장&#39;으로 대체되거나,
<code>else</code>문이 아예 없는 경우에는 아무것도 없는 상태로 깔끔하게 사라지게 됩니다.</li>
<li><code>constexpr if</code>문을 사용하는 방법은 아주 간단해요. <code>if</code> 키워드 바로 뒤에 <code>constexpr</code> 키워드를 붙여주기만 하면 됩니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    constexpr double gravity{ 9.8 };

    if constexpr (gravity == 9.8) // 이제 constexpr if를 사용합니다.
        std::cout &lt;&lt; &quot;Gravity is normal.\n&quot;;
    else
        std::cout &lt;&lt; &quot;We are not on Earth.\n&quot;;

    return 0;
}</code></pre>
<ul>
<li>위 코드가 컴파일될 때, 컴파일러는 컴파일 타임에 조건식을 미리 평가합니다. </li>
<li>그리고 그 조건이 항상 true라는 것을 확인한 뒤, 불필요한 부분은 지우고 <code>std::cout &lt;&lt; &quot;Gravity is normal.\n&quot;;</code>라는 단 하나의 문장만 남겨두게 됩니다.</li>
<li>조건식이 상수 표현식인 경우에는 일반 <code>if</code>문보다 <code>constexpr if</code>문을 사용하는 것을 강력히 권장합니다.</li>
</ul>
<pre><code class="language-cpp">int main()
{
    constexpr double gravity{ 9.8 };

    std::cout &lt;&lt; &quot;Gravity is normal.\n&quot;;

    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/9e9fe059-a9b9-4ce5-b5de-1ecdcc0653b0/image.png" alt=""></p>
<h2 id="switch-문switch-statement-기초">switch 문(Switch statement) 기초</h2>
<ul>
<li>어떤 변수나 표현식이 여러 다른 값들과 같은지 검사하는 일은 프로그래밍에서 아주 흔하게 발생합니다. </li>
<li>그래서 C++은 이런 목적에 딱 맞게 특화된 <code>switch 문</code>이라는 대안적인 조건문을 제공한답니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printDigitName(int x)
{
    switch (x)
    {
    case 1:
        std::cout &lt;&lt; &quot;One&quot;;
        return;
    case 2:
        std::cout &lt;&lt; &quot;Two&quot;;
        return;
    case 3:
        std::cout &lt;&lt; &quot;Three&quot;;
        return;
    default:
        std::cout &lt;&lt; &quot;Unknown&quot;;
        return;
    }
}

int main()
{
    printDigitName(2);
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li><code>switch 문</code>의 기본 원리는 아주 간단해요. </li>
<li>먼저 하나의 표현식(때로는 &#39;조건&#39;이라고도 부릅니다)을 평가해서 결과 값을 만들어 냅니다.</li>
<li>그런 다음 아래의 세 가지 경우 중 하나가 발생합니다.<ul>
<li><ol>
<li>표현식의 값이 어떤 case 라벨뒤에 있는 값 중 하나와 같다면, 일치하는 case 라벨 이후의 문장들이 실행됩니다.</li>
</ol>
</li>
<li><ol start="2">
<li>일치하는 값을 찾지 못했는데 <strong>default 라벨</strong>이 존재한다면, default 라벨 이후의 문장들이 실행됩니다</li>
</ol>
</li>
<li><ol start="3">
<li>일치하는 값도 찾지 못했고 default 라벨도 없다면, switch 문은 <strong>통째로 건너뛰게</strong> 됩니다.</li>
</ol>
</li>
</ul>
</li>
</ul>
<hr>
<h2 id="switch-문-시작하기">switch 문 시작하기</h2>
<ul>
<li><code>switch 문</code>을 시작할 때는 <code>switch</code> 키워드를 사용하고, 그 뒤에 평가하고 싶은 조건 표현식을 <strong>괄호 안에</strong> 넣어줍니다.</li>
<li>보통 이 표현식은 단일 변수인 경우가 많지만, 유효한 표현식이라면 무엇이든 들어갈 수 있어요.<ul>
<li>switch 문의 조건은 반드시 <strong>정수형</strong>으로 평가되거나 열거형이어야 하며, 혹은 이러한 타입으로 변환될 수 있어야 합니다.</li>
</ul>
</li>
</ul>
<hr>
<h2 id="case-라벨-case-labels">case 라벨 (Case labels)</h2>
<ul>
<li>첫 번째 라벨의 종류는 바로 <strong>case 라벨</strong>입니다. <code>case</code> 키워드를 사용해서 선언하고, 그 뒤에 상수 표현식이 따라옵니다. </li>
<li>이 상수 표현식은 조건의 타입과 반드시 일치하거나 그 타입으로 변환될 수 있어야 해요.</li>
<li>조건 표현식의 값이 어떤 case 라벨 뒤의 표현식과 같다면, 해당** case 라벨 바로 다음 문장부터 실행이 시작**되어 순차적으로 진행됩니다.</li>
<li>사용할 수 있는 case 라벨의 개수에는 실질적인 제한이 없습니다. </li>
<li>하지만 switch 문 내의 모든 case 라벨은 고유해야 합니다. 즉, 중복해서 작성하면 안 됩니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printDigitName(int x)
{
    switch (x) // x를 평가하여 값 2를 얻어냅니다
    {
    case 1:
        std::cout &lt;&lt; &quot;One&quot;;
        return;
    case 2: // 이곳의 case 문과 일치하네요!
        std::cout &lt;&lt; &quot;Two&quot;; // 그래서 여기서부터 실행이 시작됩니다
        return; // 그리고 함수를 호출했던 곳으로 돌아갑니다(return)
    case 3:
        std::cout &lt;&lt; &quot;Three&quot;;
        return;
    default:
        std::cout &lt;&lt; &quot;Unknown&quot;;
        return;
    }
}

int main()
{
    printDigitName(2);
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<hr>
<h2 id="default-라벨-the-default-label">default 라벨 (The default label)</h2>
<ul>
<li>두 번째 라벨의 종류는 <strong>default 라벨</strong>이에요. </li>
<li><code>default</code> 키워드를 사용해서 선언합니다. </li>
<li>조건 표현식이 어떤 case 라벨과도 일치하지 않을 때, 만약 <strong>default 라벨이 존재한다면 default 라벨 바로 다음 문장부터 실행이 시작</strong>된답니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printDigitName(int x)
{
    switch (x) // x를 평가하여 값 5를 얻어냅니다
    {
    case 1:
        std::cout &lt;&lt; &quot;One&quot;;
        return;
    case 2:
        std::cout &lt;&lt; &quot;Two&quot;;
        return;
    case 3:
        std::cout &lt;&lt; &quot;Three&quot;;
        return;
    default: // 어떤 case 라벨과도 일치하지 않으므로
        std::cout &lt;&lt; &quot;Unknown&quot;; // 여기서부터 실행이 시작됩니다
        return; // 그리고 함수를 호출했던 곳으로 돌아갑니다(return)
    }
}

int main()
{
    printDigitName(5);
    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<hr>
<h2 id="일치하는-case-라벨도-없고-default-case도-없는-경우">일치하는 case 라벨도 없고 default case도 없는 경우</h2>
<ul>
<li>만약 조건 표현식의 값이 어떤 case 라벨과도 일치하지 않고, 제공된 default case 마저 없다면, *<em>switch 문 내부의 어떤 케이스도 실행되지 않습니다. *</em></li>
<li>대신 실행 흐름은 switch 블록이 끝난 바로 다음 문장부터 계속 이어지게 됩니다.</li>
</ul>
<hr>
<h2 id="break-문break-statement">break 문(Break statement)</h2>
<ul>
<li>위의 예제들에서는 라벨 아래 문장들의 실행을 멈추기 위해 <code>return 문</code>을 사용했어요. 하지만 <code>return 문</code>은 함수 전체를 빠져나가게 만든다는 특징이 있죠.</li>
<li><code>break 문</code>은 컴파일러에게 &quot;switch 문 내부의 실행이 끝났으니, switch 블록 다음 문장부터 실행을 계속해라&quot;라고 알려주는 역할을 합니다. </li>
<li>즉,** 함수 전체를 종료하지 않고도** <code>switch 문</code>만 깔끔하게 빠져나올 수 있게 해주는 것이죠!</li>
</ul>
<hr>
<h2 id="폴스루fallthrough">폴스루(Fallthrough)</h2>
<ul>
<li><code>switch문</code>이 <code>case 라벨</code>이나 선택 사항인 <code>default 라벨</code>과 일치하면, 일치하는 라벨 바로 다음 명령문부터 실행이 시작됩니다. </li>
<li>그리고 <code>break</code> <code>return</code>과 같은 종료 조건 중 하나가 발생할 때까지 실행은 순차적으로 계속 진행돼요.</li>
<li>여기서 꼭 기억해야 할 점은,** 다른 <code>case 레이블</code>이 나타나는 것은 종료 조건이 아니라는 것**입니다.</li>
<li>따라서 <code>break</code>나 <code>return</code>이 없으면 실행 흐름은 <strong>그다음 case들로 계속 넘쳐흘러 가게(overflow) 됩니다.</strong></li>
<li>특정 레이블 아래의 명령문에서 실행 흐름이 그다음 레이블 아래의 명령문으로 계속 흘러가는 현상을 <strong>폴스루(fallthrough)</strong> 라고 부릅니다.</li>
</ul>
<hr>
<h2 id="fallthrough-속성attribute">[[fallthrough]] 속성(Attribute)</h2>
<ul>
<li>폴스루가 의도한 것이거나 유용하게 쓰이는 경우는 드물기 때문에, 많은 컴파일러와 코드 분석 도구들은 폴스루가 발생하면 경고를 띄워 알려줍니다.</li>
<li>이를 해결하기 위해 C++17에서는 <code>[[fallthrough]]</code>라는 새로운 속성이 추가되었습니다.</li>
<li><code>[[fallthrough]]</code> 속성은 <strong>널 명령문</strong>을 수식하여 폴스루가 의도적임을 나타냅니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    switch (2)
    {
    case 1:
        std::cout &lt;&lt; 1 &lt;&lt; &#39;\n&#39;;
        break;
    case 2:
        std::cout &lt;&lt; 2 &lt;&lt; &#39;\n&#39;; // 여기서부터 실행이 시작됩니다.
        [[fallthrough]]; // 의도된 폴스루 -- 널 명령문을 나타내는 세미콜론(;)에 주목하세요.
    case 3:
        std::cout &lt;&lt; 3 &lt;&lt; &#39;\n&#39;; // 이 줄도 실행됩니다.
        break;
    }

    return 0;
}</code></pre>
<hr>
<h2 id="case-문-내부에서의-변수-선언과-초기화">case 문 내부에서의 변수 선언과 초기화</h2>
<ul>
<li>스위치문의 경우, 레이블 뒤에 오는 명령문들은 모두 <strong>전체 스위치 블록에 스코프가 종속</strong>됩니다. </li>
<li>각각의 <code>case</code>마다 암시적인 블록이 따로 만들어지지 않아요.</li>
<li>아래 예제에서 <code>case 1</code>과 <code>default</code> 레이블 사이에 있는 두 개의 명령문은 case 1에 속한 개별 블록이 아니라, 
 스위치 블록** 전체 범위의 일부로** 취급됩니다.</li>
</ul>
<pre><code class="language-cpp">switch (1)
{
case 1: // 암시적 블록을 생성하지 않습니다.
    foo(); // 이 부분은 case 1의 암시적 블록이 아니라, 스위치 전체 스코프의 일부입니다.
    break; // 이 부분은 case 1의 암시적 블록이 아니라, 스위치 전체 스코프의 일부입니다.
default:
    std::cout &lt;&lt; &quot;default case\n&quot;;
    break;
}</code></pre>
<ul>
<li>이러한 스코프 규칙 때문에, 변수를 선언할 때 주의해야 할 점이 있습니다. </li>
<li>스위치문 내부에서는 <code>case</code> 레이블 이전이든 이후이든 변수를 선언하거나 정의할 수 있습니다. </li>
<li>하지만 초기화는 조심해야 합니다.</li>
</ul>
<pre><code class="language-cpp">switch (1)
{
    int a; // 허용됨: case 레이블 이전의 정의는 허용됩니다.
    int b{ 5 }; // 오류(Illegal): case 레이블 이전의 초기화는 허용되지 않습니다.

case 1:
    int y; // 허용되지만 나쁜 관행: case 내부의 정의는 허용됩니다.
    y = 4; // 허용됨: 값을 할당(Assignment)하는 것은 허용됩니다.
    break;

case 2:
    int z{ 4 }; // 오류(Illegal): 뒤에 다른 case가 존재한다면 초기화는 허용되지 않습니다.
    y = 5; // 허용됨: y는 위(case 1)에서 선언되었으므로 여기서도 사용할 수 있습니다.
    break;

case 3:
    break;
}</code></pre>
<ul>
<li>변수 <code>y</code>는 <code>case 1</code>에서 정의되었지만, <code>case 2</code>에서도 문제없이 사용되었습니다. </li>
<li><code>switch</code>의 모든 <code>case</code>는 같은 스코프라서, 한 <code>case</code>에서 선언한 변수는 다른 <code>case</code>에서도 보입니다.</li>
<li>하지만 <strong>초기화는 실행이 필요</strong>하므로, 다른 <code>case</code>로 점프하면서 초기화를 건너뛸 수 있는 위치에서는 금지됩니다.</li>
<li>특정 case에서만 변수 선언+초기화를 하려면 <code>{}</code> 블록을 만들어 그 안에서 해야 안전합니다.</li>
</ul>
<pre><code class="language-cpp">switch (1)
{
case 1:
{ // 여기에 명시적인 블록을 추가한 것을 확인하세요.
    int x{ 4 }; // 허용됨: case 내부의 명시적 블록 안에서는 변수를 초기화할 수 있습니다.
    std::cout &lt;&lt; x;
    break;
}
default:
    std::cout &lt;&lt; &quot;default case\n&quot;;
    break;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/72d031f4-8628-4419-a78e-00e5b9a1ee7c/image.png" alt=""></p>
<h2 id="goto-문-goto-statements">Goto 문 (Goto statements)</h2>
<ul>
<li>이번에 다룰 제어 흐름 구문은 바로 <strong>무조건 점프</strong>입니다. </li>
<li>표현식의 결과에 따라 조건부로 점프가 일어나는 <code>if</code> 문이나 <code>switch</code> 문과는 다르게 무조건 점프는 프로그램의 실행 흐름을 코드의 <strong>다른 위치로 곧바로 건너뛰게</strong> 만들어 줍니다.</li>
</ul>
<ul>
<li>C++에서 이러한 무조건 점프는 <code>goto 문</code>을 통해 구현됩니다. </li>
<li>그리고 어디로 점프할지 그 도착 지점은 <strong>구문 레이블(Statement label)</strong>을 사용해서 지정해 줍니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cmath&gt; // sqrt() 함수를 사용하기 위해 포함합니다.

int main()
{
    double x{};
tryAgain: // 이것이 바로 구문 레이블(statement label)입니다.
    std::cout &lt;&lt; &quot;Enter a non-negative number: &quot;;
    std::cin &gt;&gt; x;

    if (x &lt; 0.0)
        goto tryAgain; // 이것이 바로 goto 문입니다.

    std::cout &lt;&lt; &quot;The square root of &quot; &lt;&lt; x &lt;&lt; &quot; is &quot; &lt;&lt; std::sqrt(x) &lt;&lt; &#39;\n&#39;;
    return 0;
}</code></pre>
<hr>
<h2 id="구문-레이블은-함수-범위function-scope를-가집니다">구문 레이블은 함수 범위(Function scope)를 가집니다</h2>
<ul>
<li>객체의 범위를 다루었던 7 챕터에서 우리는 <strong>지역 범위</strong>와 <strong>파일 범위</strong>라는 두 가지 범위를 배웠어요.</li>
<li>구문 레이블은 세 번째 종류의 범위인 <strong>함수 범위(Function scope)</strong>를 사용합니다. </li>
<li>이는 해당 레이블이 선언되기 전이라도 함수 전체에서 그 레이블을 볼 수 있다는 뜻입니다. </li>
<li>단, <code>goto 문</code>과 그에 연결된 구문 레이블은 <strong>반드시 같은 함수 안</strong>에 있어야 합니다.</li>
<li>앞으로 점프할 때, 점프 도착 지점에서도 여전히 범위 내에 있는 변수의 초기화 과정을 건너뛰어 점프할 수는 없습니다.</li>
</ul>
<pre><code class="language-cpp">int main()
{
    goto skip;   // 오류: 이 점프는 허용되지 않습니다. 왜냐하면...
    int x { 5 }; // 이 초기화된 변수가 &#39;skip&#39; 구문 레이블 위치에서도 여전히 범위 내에 있기 때문입니다.
skip:
    x += 3;      // 만약 x가 초기화되지 않았다면 이 코드는 대체 어떻게 평가될까요?
    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/8407eaee-00b3-412b-b439-e46f5c77ef39/image.png" alt=""></p>
<h2 id="while-문-while-statements">while 문 (While statements)</h2>
<ul>
<li><code>while 문</code>은 C++이 제공하는 <strong>세 가지 종류의 반복문 중 가장 단순한 형태</strong>이며, <code>if 문</code>과 아주 비슷한 구조를 가지고 있습니다.</li>
</ul>
<pre><code class="language-cpp">while (조건식)
    명령문;</code></pre>
<ul>
<li>while 문은 <code>while</code>이라는 키워드를 사용해서 선언해요. </li>
<li>while 문이 실행되면 가장 먼저 조건식을 <strong>평가</strong>합니다. </li>
<li>만약 조건식이 <strong>참</strong>으로 평가되면, 그 아래에 연결된 명령문이 실행됩니다.</li>
</ul>
<hr>
<h2 id="정수형-루프-변수는-부호-있는signed-타입이어야-합니다">정수형 루프 변수는 부호 있는(signed) 타입이어야 합니다.</h2>
<ul>
<li>정수형 루프 변수는 거의 항상 <strong>부호 있는(signed) 타입을 사용</strong>해야 합니다. </li>
<li>부호 없는(unsigned) 정수를 사용하면 전혀 예상치 못한 문제가 발생할 수 있거든요. </li>
<li>다음 코드를 함께 볼까요?</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    unsigned int count{ 10 }; // 주의: unsigned (부호 없는 정수)로 선언했습니다.

    // 10부터 0까지 카운트다운 합니다.
    while (count &gt;= 0)
    {
        if (count == 0)
        {
            std::cout &lt;&lt; &quot;blastoff!&quot;;
        }
        else
        {
            std::cout &lt;&lt; count &lt;&lt; &#39; &#39;;
        }
        --count;
    }

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li>처음에는 우리가 원했던 대로 <code>10 9 8 7 6 5 4 3 2 1 blastoff!</code>라고 잘 출력합니다. </li>
<li>하지만 그다음 순간 루프 변수 <code>count</code>에 <strong>오버플로우</strong>가 발생하면서, <code>4294967295</code>부터 다시 카운트다운을 시작해 버립니다.
<img src="https://velog.velcdn.com/images/dj_trace25/post/00981a8c-f535-496e-98a6-8430aca88921/image.png" alt=""></li>
</ul>
<h2 id="for-문-for-statements">for 문 (For statements)</h2>
<ul>
<li>C++에서 단연코 가장 많이 사용되는 <strong>반복문</strong>은 바로 <strong>for 문</strong>입니다. </li>
<li>반복을 제어하는 명확한 <strong>반복 변수(Loop variable)</strong>가 있을 때 for 문을 사용하는 것이 가장 좋습니다. </li>
<li>반복 변수를 정의하고, 초기화하고, 검사하고, 변경하는 모든 과정을 아주 쉽고 간결하게 한곳에 모아둘 수 있기 때문이죠.</li>
<li>C++11부터는 두 가지 종류의 for 문이 존재해요. 이번 강의에서는** 전통적인 클래식 for 문**을 다룹니다.</li>
</ul>
<hr>
<h2 id="for-문의-평가-과정-evaluation-of-for-statements">for 문의 평가 과정 (Evaluation of for-statements)</h2>
<pre><code class="language-cpp">for (init-statement; condition; end-expression)
   statement;</code></pre>
<ul>
<li><p>for 문은 크게 3단계로 나뉘어 실행됩니다.</p>
</li>
<li><ol>
<li>초기화 명령문(init-statement)이 실행됩니다. 
이 과정은 반복문이 처음 시작될 때 단 한 번만 일어납니다. 
주로 변수를 정의하고 초기화하는 데 사용돼요. 
여기서 만들어진 변수들은 <strong>&#39;반복문 스코프(Loop scope)&#39;</strong>를 가지게 되는데, 
쉽게 말해 이 변수들은 변수가 정의된 시점부터 반복문이 끝날 때까지만 존재한다는 뜻입니다. </li>
</ol>
</li>
</ul>
<ul>
<li><ol start="2">
<li>매 반복마다 조건식을 평가합니다. 
만약 조건식이 <strong>참(true)</strong>이라면 내부의 명령문(statement)이 실행됩니다. 
반대로 <strong>거짓(false)</strong>이라면 반복문은 즉시 종료되고, 코드의 실행 흐름은 반복문 바로 다음 줄로 넘어갑니다.</li>
</ol>
</li>
<li><ol start="3">
<li>명령문이 실행된 후 끝 표현식(end-expression)이 평가됩니다. 
보통 이 자리에는 초기화 단계에서 만든 반복 변수를 1씩 증가시키거나 <code>++</code> 감소시키는 <code>--</code> 코드가 들어갑니다. 
끝 표현식의 실행이 끝나면, 다시 두 번째 단계로 돌아가서 조건식을 재평가합니다.</li>
</ol>
</li>
</ul>
<blockquote>
<p>for 문의 각 부분이 실행되는 순서를 정확히 기억해 두는 것이 매우 중요합니다.</p>
</blockquote>
<ol>
<li>초기화 명령문 (Init-statement)</li>
<li>조건식 (Condition) (만약 여기서 거짓이 나오면 반복문은 바로 끝납니다.)</li>
<li>반복문 본문 (Loop body)</li>
<li>끝 표현식 (End-expression) (실행 후 다시 2번 조건식으로 돌아갑니다.)</li>
</ol>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    for (int i{ 1 }; i &lt;= 10; ++i)
        std::cout &lt;&lt; i &lt;&lt; &#39; &#39;;

    std::cout &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/26a606ac-7ccd-42a7-957c-cedaaf2e3ce6/image.png" alt=""></p>
<h2 id="break-강제-종료하기">Break (강제 종료하기)</h2>
<ul>
<li><strong>break 문</strong>은 <code>while 루프</code> <code>do-while 루프</code> <code>for 루프(반복문)</code> 혹은 <code>switch 문</code>을 즉시 종료시키는 역할을 합니다. </li>
<li>break로 인해 해당 블록을 빠져나오면, 바로 그다음 줄의 코드부터 실행이 계속됩니다.</li>
</ul>
<hr>
<h2 id="switch-문에서-빠져나오기-breaking-a-switch">switch 문에서 빠져나오기 (Breaking a switch)</h2>
<ul>
<li><code>switch 문</code>의 문맥에서 <code>break</code>는 보통 각 <code>case</code>의 끝에 사용되어 해당 <code>case</code>가 끝났음을 알립니다. </li>
<li>이를 통해 다음 <code>case</code>로 의도치 않게 넘어가 버리는 <strong>폴스루 현상을 방지</strong>할 수 있어요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void printMath(int x, int y, char ch)
{
    switch (ch)
    {
    case &#39;+&#39;:
        std::cout &lt;&lt; x &lt;&lt; &quot; + &quot; &lt;&lt; y &lt;&lt; &quot; = &quot; &lt;&lt; x + y &lt;&lt; &#39;\n&#39;;
        break; // 다음 case로 넘어가지 않도록(fall-through) 방지해요
    case &#39;-&#39;:
        std::cout &lt;&lt; x &lt;&lt; &quot; - &quot; &lt;&lt; y &lt;&lt; &quot; = &quot; &lt;&lt; x - y &lt;&lt; &#39;\n&#39;;
        break; // 다음 case로 넘어가지 않도록 방지해요
    case &#39;*&#39;:
        std::cout &lt;&lt; x &lt;&lt; &quot; * &quot; &lt;&lt; y &lt;&lt; &quot; = &quot; &lt;&lt; x * y &lt;&lt; &#39;\n&#39;;
        break; // 다음 case로 넘어가지 않도록 방지해요
    case &#39;/&#39;:
        std::cout &lt;&lt; x &lt;&lt; &quot; / &quot; &lt;&lt; y &lt;&lt; &quot; = &quot; &lt;&lt; x / y &lt;&lt; &#39;\n&#39;;
        break;
    }
}

int main()
{
    printMath(2, 3, &#39;+&#39;);

    return 0;
}</code></pre>
<hr>
<h2 id="루프에서-빠져나오기-breaking-a-loop">루프에서 빠져나오기 (Breaking a loop)</h2>
<ul>
<li><strong>반복문 내에서</strong> <code>break 문</code>을 사용하면 루프를 일찍 종료할 수 있습니다. </li>
<li>루프가 종료된 후에는 루프 바로 다음 문장부터 실행이 이어집니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    int sum{ 0 };

    // 사용자가 최대 10개의 숫자를 입력할 수 있도록 허용해요
    for (int count{ 0 }; count &lt; 10; ++count)
    {
        std::cout &lt;&lt; &quot;Enter a number to add, or 0 to exit: &quot;;
        int num{};
        std::cin &gt;&gt; num;

        // 사용자가 0을 입력하면 루프를 종료해요
        if (num == 0)
            break; // 지금 바로 루프를 빠져나갑니다

        // 그렇지 않으면 입력한 숫자를 합계에 더해요
        sum += num;
    }

    // break로 루프를 빠져나오면 여기서부터 실행이 계속됩니다
    std::cout &lt;&lt; &quot;The sum of all the numbers you entered is: &quot; &lt;&lt; sum &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<hr>
<h2 id="break-vs-return-어떤-차이가-있을까요">Break vs Return (어떤 차이가 있을까요?)</h2>
<ul>
<li><code>break 문</code>은 <strong>현재 실행 중인</strong> <code>switch</code>나 <code>반복문</code>만 종료시키고, 그 바로 바깥쪽의 다음 코드를 계속 실행합니다.</li>
<li>반면 <code>return</code> 문은 해당 루프가 들어있는 <strong>함수 전체를 완전히 종료</strong>시키고, 그 함수를 호출했던 위치로 되돌아갑니다.</li>
</ul>
<hr>
<h2 id="continue-이번-순서는-건너뛰기">Continue (이번 순서는 건너뛰기)</h2>
<ul>
<li><strong><code>continue 문</code></strong>은 전체 루프를 끝내지 않으면서도, 현재 진행 중인 <strong>반복(iteration, 루프의 한 사이클)</strong>만 깔끔하게 종료하고 다음 반복으로 넘어가게 해주는 편리한 방법이에요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    for (int count{ 0 }; count &lt; 10; ++count)
    {
        // 만약 숫자가 4로 나누어 떨어지면 이번 반복은 건너뜁니다
        if ((count % 4) == 0)
            continue; // 다음 반복(iteration)으로 이동해요

        // 숫자가 4로 나누어 떨어지지 않으면 계속 진행합니다
        std::cout &lt;&lt; count &lt;&lt; &#39;\n&#39;;

        // continue 문이 실행되면 바로 이 위치(루프의 끝)로 점프하게 됩니다
    }

    return 0;
}

이 프로그램은 0부터 9까지의 숫자 중 4로 나누어 떨어지지 않는 숫자만 출력합니다:

1
2
3
5
6
7
9</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/e90dbdc1-9715-4b8c-b691-8cc5b84f172d/image.png" alt=""></p>
<h2 id="중단halts-프로그램-일찍-종료하기">중단(Halts) (프로그램 일찍 종료하기)</h2>
<ul>
<li>이번 장에서 다룰 마지막 제어 흐름문은 바로 <strong>중단(Halt)</strong>입니다. </li>
<li>중단이란 <strong>프로그램을 완전히 종료시키는 제어 흐름문</strong>을 말해요. </li>
<li>C++에서 중단은 키워드가 아닌 함수 형태로 구현되어 있기 때문에, 중단문은 곧** 함수 호출(Function call) 형태**를 띠게 된답니다.</li>
</ul>
<ul>
<li>프로그램이 정상적으로 종료될 때 어떤 일이 일어나는지 잠깐 복습해 볼까요?</li>
<li>첫째, 함수를 빠져나가기 때문에 평소처럼 <strong>모든 지역 변수와 함수 매개변수가 소멸</strong>됩니다.</li>
<li>그다음, <code>main()</code> 함수의 반환값 상태 코드(Status code)를 인자로 삼아 <code>std::exit()</code>라는 특수한 함수가 호출된답니다. </li>
<li>그렇다면 이 <code>std::exit()</code>는 과연 무엇일까요?</li>
</ul>
<hr>
<h2 id="stdexit-함수">std::exit() 함수</h2>
<ul>
<li><code>std::exit()</code>는 프로그램을 <strong>정상 종료</strong>시키는 함수예요. </li>
<li>여기서 &#39;정상 종료&#39;란 프로그램이 우리가 예상한 방식대로 종료되었다는 뜻입니다.</li>
</ul>
<ul>
<li>참고로 정상 종료라는 말이 프로그램이 완벽하게 &#39;성공적으로&#39; 수행되었음을 의미하는 것은 아니에요.</li>
<li>예를 들어, 사용자가 입력한 파일명으로 작업을 처리하는 프로그램을 만들었다고 해볼게요. </li>
<li>만약 사용자가 잘못된 파일명을 입력했다면, 프로그램은 실패 상태를 알리기 위해 <code>0</code>이 아닌 상태 코드를 반환하겠지만, 
이 역시 프로그램 입장에서는 예상된 흐름이므로 여전히 &#39;정상 종료&#39;에 해당합니다.</li>
</ul>
<ul>
<li><code>std::exit()</code>는 여러 가지 정리(Cleanup) 작업을 수행해요.</li>
<li><ol>
<li>먼저 <strong>정적 저장 주기(Static storage duration)</strong>를 가진 객체들을 소멸시킵니다.</li>
</ol>
</li>
<li><ol start="2">
<li>사용 중인 파일이 있다면 기타 파일 정리 작업을 수행하죠.</li>
</ol>
</li>
<li><ol start="3">
<li>마지막으로, <code>std::exit()</code>에 <strong>전달된 인자</strong>를 상태 코드로 사용하여 운영체제(OS)에 제어권을 돌려준답니다.</li>
</ol>
</li>
</ul>
<hr>
<h2 id="명시적으로-stdexit-호출하기">명시적으로 std::exit() 호출하기</h2>
<ul>
<li><code>main()</code> 함수가 끝난 뒤에 <code>std::exit()</code>가 암시적으로 자동 호출되긴 하지만, 프로그램이 원래 끝나야 할 시점보다 일찍 프로그램을 멈추기 위해 <strong>명시적으로 직접 호출</strong>할 수도 있어요. 이렇게 사용할 때는 반드시 <code>&lt;cstdlib&gt;</code> 헤더를 포함(Include)해야 합니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;cstdlib&gt; // std::exit()를 사용하기 위해 포함
#include &lt;iostream&gt;

void cleanup()
{
    // 필요한 모든 종류의 정리 작업을 수행하는 코드를 여기에 작성합니다
    std::cout &lt;&lt; &quot;cleanup!\n&quot;;
}

int main()
{
    std::cout &lt;&lt; 1 &lt;&lt; &#39;\n&#39;;
    cleanup();

    std::exit(0); // 프로그램을 종료하고 운영체제에 상태 코드 0을 반환합니다.

    // 아래의 명령문들은 프로그램이 이미 종료되었으므로 절대 실행되지 않습니다.
    std::cout &lt;&lt; 2 &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li>위 예제에서는 <code>main()</code> 함수 안에서 <code>std::exit()</code>를 호출했지만, 사실 <code>std::exit()</code>는 <strong>어떤 함수에서든</strong> 호출할 수 있고, 호출된 <strong>그 시점에서 즉시 프로그램을 종료</strong>시킨답니다.</li>
</ul>
<hr>
<h2 id="stdexit는-지역-변수를-정리하지-않습니다">std::exit()는 지역 변수를 정리하지 않습니다</h2>
<ul>
<li><code>std::exit()</code>를 직접 호출할 때 아주 중요한 주의 사항이 하나 있어요. </li>
<li>바로 <code>std::exit()</code>는 <strong>현재 함수나 호출 스택 위쪽의 어떤 지역 변수도 정리하지 않는다</strong>는 점이에요.  </li>
<li>즉, 프로그램이 지역 변수들이 스스로 잘 정리될 것이라 믿고 작동하게끔 설계되었다면, <code>std::exit()</code>를 호출하는 것은 꽤 위험할 수 있습니다.</li>
</ul>
<hr>
<h2 id="stdatexit">std::atexit</h2>
<ul>
<li><code>std::exit()</code>는 프로그램을 즉시 종료시키기 때문에, 종료되기 전에 수동으로 어떤 정리 작업을 하고 싶을 수 있어요. </li>
<li>여기서 &#39;정리&#39;란 데이터베이스나 네트워크 연결을 닫는 것, 할당받은 메모리를 해제하는 것, 로그 파일에 정보를 기록하는 것 등을 말한답니다.</li>
</ul>
<ul>
<li>앞선 예제에서는 이 정리 작업을 처리하기 위해 우리가 직접 <code>cleanup()</code>이라는 함수를 호출했어요. </li>
<li>하지만 <code>exit()</code>를 호출할 때마다 매번 수동으로 정리 함수 부르는 것을 기억해야 한다면, 프로그래머에게 큰 부담이 되고 오류가 생기기 딱 좋은 환경이 됩니다.</li>
</ul>
<ul>
<li>이를 돕기 위해 C++은 <strong><code>std::atexit()</code></strong>라는 함수를 제공합니다. </li>
<li>이 함수를 사용하면, <code>std::exit()</code>를 통해 프로그램이 종료될 때 <strong>자동으로 호출될 함수를 미리 지정</strong>해 둘 수 있어요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;cstdlib&gt; // std::exit()를 사용하기 위해 포함
#include &lt;iostream&gt;

void cleanup()
{
    // 필요한 모든 종류의 정리 작업을 수행하는 코드를 여기에 작성합니다
    std::cout &lt;&lt; &quot;cleanup!\n&quot;;
}

int main()
{
    // std::exit()가 호출될 때 자동으로 호출되도록 cleanup()을 등록합니다.
    std::atexit(cleanup); // 참고: 지금 당장 cleanup() 함수를 실행하는 것이 아니기 때문에 괄호를 빼고 &#39;cleanup&#39;이라고만 적습니다.

    std::cout &lt;&lt; 1 &lt;&lt; &#39;\n&#39;;

    std::exit(0); // 프로그램을 종료하고 운영체제에 상태 코드 0을 반환합니다.

    // 아래의 명령문들은 절대 실행되지 않습니다.
    std::cout &lt;&lt; 2 &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li>여기서 주목할 점은, <code>cleanup()</code> 함수를 인자로 넘겨줄 때 괄호를 붙인 <code>cleanup()</code>(이것은 함수를 즉시 호출해 버립니다)이 아니라, 함수의 이름인 <code>cleanup</code>만 사용했다는 점입니다.</li>
</ul>
<blockquote>
<p><strong>std::atexit()와 이 정리 함수에 대해 몇 가지 알아둘 점이 있습니다.</strong></p>
</blockquote>
<ul>
<li><ol>
<li><code>main()</code> 함수가 끝날 때 <strong>암시적으로</strong> <code>std::exit()</code>가 호출되므로, <code>return</code>으로 프로그램이 끝나는 경우에도 <strong><code>std::atexit()</code>로 등록한 함수들이 똑같이 실행</strong>됩니다.</li>
</ol>
</li>
<li><ol start="2">
<li><strong>등록되는 함수는 매개변수가 없어야 하고, 반환값(Return value)도 없어야(void) 합니다.</strong></li>
</ol>
</li>
<li><ol start="3">
<li>원한다면 <code>std::atexit()</code>를 여러 번 써서 여러 개의 정리 함수를 등록할 수도 있어요. </li>
</ol>
</li>
<li>이때 함수들은 등록된 <strong>역순(Reverse order)으로 호출</strong>됩니다. (즉, 가장 마지막에 등록된 것이 제일 먼저 실행돼요.)</li>
</ul>
<hr>
<h2 id="고급-독자를-위한-내용-for-advanced-readers">고급 독자를 위한 내용 (For advanced readers)</h2>
<ul>
<li>멀티스레드 프로그램에서 <code>std::exit()</code>를 호출하면 프로그램이 <strong>강제 종료</strong>될 수 있습니다.</li>
<li><code>std::exit()</code>를 호출한 스레드가 다른 스레드에서 <strong>여전히 사용 중일지 모르는 정적 객체들을 파괴해 버리기 때문</strong>이에요.</li>
</ul>
<ul>
<li>이러한 이유로, C++은 <code>std::exit()</code> 및 <code>std::atexit()</code>와 비슷하게 작동하는 <strong><code>std::quick_exit()</code></strong>와 <strong><code>std::at_quick_exit()</code></strong>라는 또 다른 함수 쌍을 도입했습니다.</li>
<li><code>std::quick_exit()</code>는 프로그램을 정상적으로 종료하긴 하지만, 정적 객체들을 정리하지 않으며, 다른 종류의 정리 작업도 수행할지 안 할지 보장하지 않습니다. </li>
<li><code>std::at_quick_exit()</code>는 <code>std::quick_exit()</code>로 종료되는 프로그램에서 <code>std::atexit()</code>와 같은 역할을 수행합니다.</li>
</ul>
<hr>
<h2 id="stdabort와-stdterminate">std::abort와 std::terminate</h2>
<ul>
<li><code>std::abort()</code> 함수는 프로그램을 <strong>비정상 종료(Abnormal termination)</strong>시킵니다. </li>
<li>&#39;비정상 종료&#39;란 프로그램에 어떤 예외적인 런타임 오류가 발생하여 더 이상 실행을 계속할 수 없는 상태를 뜻해요. </li>
<li>예를 들어 숫자를 <code>0</code>으로 나누려고 시도하면 비정상 종료가 일어납니다. </li>
<li>아주 중요한 점은, <code>std::abort()</code>는 <strong>어떠한 정리 작업도 하지 않는다는 것</strong>입니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;cstdlib&gt; // std::abort()를 사용하기 위해 포함
#include &lt;iostream&gt;

int main()
{
    std::cout &lt;&lt; 1 &lt;&lt; &#39;\n&#39;;
    std::abort();

    // 아래의 명령문들은 절대 실행되지 않습니다.
    std::cout &lt;&lt; 2 &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li><code>std::terminate()</code> 함수는 일반적으로 <strong>예외(Exceptions)</strong>와 함께 사용됩니다. (예외에 대해서는 이후 챕터에서 자세히 다룰게요.) </li>
<li><code>std::terminate</code>를 명시적으로 호출할 수도 있지만, 대개는 발생한 예외가 제대로 처리(Handled)되지 않았을 때(그리고 기타 몇 가지 예외 관련 상황에서) 암시적으로 호출됩니다. </li>
<li>기본적으로 <code>std::terminate()</code>는 <code>std::abort()</code>를 호출한답니다.
<img src="https://velog.velcdn.com/images/dj_trace25/post/a2220616-b781-4cf9-9b46-fc6dd6830379/image.png" alt=""></li>
</ul>
<h2 id="알고리즘algorithms과-상태state">알고리즘(Algorithms)과 상태(State)</h2>
<ul>
<li><strong>알고리즘(Algorithm)</strong>은 어떤 문제를 해결하거나 유용한 결과를 만들어내기 위해 따라야 하는 &#39;유한한 일련의 지시 사항&#39;을 뜻해요.</li>
<li>어떤 알고리즘이 <strong>함수 호출 간에 정보를 유지</strong>한다면, 그 알고리즘은 <strong>상태를 유지(Stateful)</strong>한다고 말합니다.</li>
<li>반대로 <strong>무상태(Stateless) 알고리즘</strong>은 어떠한 정보도 저장하지 않으며 호출될 때마다 작업에 필요한 모든 정보를 전달받아야 해요.</li>
</ul>
<hr>
<h2 id="의사-난수-생성기-pseudo-random-number-generators-prngs">의사 난수 생성기 (Pseudo-random number generators, PRNGs)</h2>
<ul>
<li>무작위성을 모방하기 위해 프로그램은 일반적으로 <strong>&#39;의사 난수 생성기&#39;</strong>를 사용합니다. </li>
<li><strong>의사 난수 생성기(PRNG)</strong>란 난수 시퀀스처럼 보이는 특성을 가진 숫자들의 시퀀스를 생성하는 알고리즘이에요.</li>
<li>기본적인 PRNG 알고리즘을 작성하는 것은 꽤 쉽습니다. </li>
<li>다음은 16비트 의사 난수 100개를 생성하는 짧은 PRNG 예제입니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

// 설명의 목적으로만 작성된 코드입니다. 실제로는 사용하지 마세요.
unsigned int LCG16() // 우리의 PRNG 함수
{
    static unsigned int s_state{ 0 }; // 이 함수가 처음 호출될 때 한 번만 초기화됩니다.

    // 다음 숫자를 생성합니다.

    // 누군가가 시퀀스의 다음 숫자가 무엇일지 쉽게 예측하지 못하도록
    // 큰 상수와 의도적인 오버플로우(overflow)를 사용하여 상태를 수정합니다.

    s_state = 8253729 * s_state + 2396403; // 먼저 상태를 수정합니다.
    return s_state % 32768; // 그런 다음 새로운 상태를 사용하여 시퀀스의 다음 숫자를 반환합니다.
}

int main()
{
    // 100개의 난수를 출력합니다.
    for (int count{ 1 }; count &lt;= 100; ++count)
    {
        std::cout &lt;&lt; LCG16() &lt;&lt; &#39;\t&#39;;

        // 숫자를 10개 출력했다면, 새로운 줄로 넘어갑니다.
        if (count % 10 == 0)
            std::cout &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}

이 프로그램의 결과는 다음과 같습니다.

8397    18528    17747    9126    28505    13420    32479    23218    21477    30328    
20075    26558    20081    3716    13303    19146    24317    31888    12163    982    
1417    16540    16655    4834    16917    23208    26779    30702    5281    19124    
9767    13050    32045    4288    31155    17414    31673    11468    25407    11026    
4165    7896    25291    26654    15057    26340    30807    31530    31581    1264    
9187    25654    20969    30972    25967    9026    15989    17160    15611    14414    
16641    25364    10887    9050    22925    22816    11795    25702    2073    9516</code></pre>
<ul>
<li>사실, 이 특정 알고리즘은 난수 생성기로서 성능이 아주 좋은 편은 아닙니다. </li>
<li>결과가 짝수와 홀수를 번갈아 가며 나온다는 걸 눈치채셨나요? 이건 별로 무작위적이지 않죠. </li>
<li>하지만 대부분의 <code>PRNG</code>는 이 <code>LCG16()</code>과 비슷하게 작동해요. </li>
<li>단지 더 좋은 품질의 결과를 얻기 위해 더 많은 상태 변수와 더 복잡한 수학 연산을 사용할 뿐이랍니다.</li>
</ul>
<hr>
<h2 id="의사-난수-생성기에-시드-설정하기-seeding-a-prng">의사 난수 생성기에 시드 설정하기 (Seeding a PRNG)</h2>
<ul>
<li><code>PRNG</code>에 의해 생성된 &quot;난수&quot; 시퀀스는 사실 전혀 무작위가 아닙니다.</li>
<li><code>LCG16()</code> 역시 <strong>결정론적(Deterministic)</strong>이거든요. </li>
<li>특정한 초기 상태 값(예: <code>0</code>)이 주어지면, PRNG는 <strong>매번 똑같은 숫자 시퀀스를 생성</strong>해 냅니다. </li>
<li>위 프로그램을 세 번 실행해 보면 매번 똑같은 값의 시퀀스가 나오는 것을 확인할 수 있을 거예요.</li>
</ul>
<ul>
<li>다른 출력 시퀀스를 생성하려면 PRNG의 초기 상태를 다르게 해주어야 합니다. </li>
<li><code>PRNG</code>의 초기 상태를 설정하는 데 사용되는 값을 <strong>랜덤 시드(Random seed)</strong> 또는 줄여서 <strong>시드(Seed)</strong>라고 부릅니다. </li>
<li>시드를 사용하여 <code>PRNG</code>의 초기 상태를 설정했을 때, 우리는 이것을 <strong>&quot;시드가 설정되었다(seeded)&quot;</strong>라고 표현해요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

unsigned int g_state{ 0 };

void seedPRNG(unsigned int seed)
{
    g_state = seed;
}

// 설명의 목적으로만 작성된 코드입니다. 실제로는 사용하지 마세요.
unsigned int LCG16() // 우리의 PRNG 함수
{
    // 누군가가 시퀀스의 다음 숫자가 무엇일지 쉽게 예측하지 못하도록
    // 큰 상수와 의도적인 오버플로우를 사용하여 상태를 수정합니다.

    g_state = 8253729 * g_state + 2396403; // 먼저 상태를 수정합니다.
    return g_state % 32768; // 그런 다음 새로운 상태를 사용하여 시퀀스의 다음 숫자를 반환합니다.
}

void print10()
{
    // 10개의 난수를 출력합니다.
    for (int count{ 1 }; count &lt;= 10; ++count)
    {
        std::cout &lt;&lt; LCG16() &lt;&lt; &#39;\t&#39;;
    }

    std::cout &lt;&lt; &#39;\n&#39;;
}

int main()
{
    unsigned int x {};
    std::cout &lt;&lt; &quot;Enter a seed value(시드 값을 입력하세요): &quot;;
    std::cin &gt;&gt; x;

    seedPRNG(x); // 우리의 PRNG에 시드를 설정합니다.
    print10();   // 10개의 난수 값을 생성합니다.

    return 0;
}

다음은 이 프로그램을 3번 실행해 본 결과입니다.

Enter a seed value: 7
10458    3853    16032    17299    10726    32153    19116    7455    242    549    

Enter a seed value: 7
10458    3853    16032    17299    10726    32153    19116    7455    242    549    

Enter a seed value: 9876
24071    18138    27917    23712    8595    18406    23449    26796    31519    7922</code></pre>
<ul>
<li>보시다시피 동일한 시드 값을 제공하면 똑같은 출력 시퀀스를 얻게 됩니다. </li>
<li>다른 시드 값을 제공해야만 다른 출력 시퀀스를 얻을 수 있죠.</li>
</ul>
<hr>
<h2 id="시드-품질과-시드-부족-seed-quality-and-underseeding">시드 품질과 시드 부족 (Seed quality and underseeding)</h2>
<ul>
<li>프로그램을 실행할 때마다 다른 무작위 숫자를 생성하고 싶다면, <strong>실행할 때마다 시드를 다르게 줄 방법</strong>이 필요합니다.</li>
<li>안타깝게도 무작위 시드를 만들기 위해 <code>PRNG</code>를 사용할 수는 없어요. </li>
<li>난수를 생성하려면 무작위 시드가 필요하니까 모순이 되죠.</li>
<li>대신, 우리는 보통 시드 값을 생성하도록 설계된 <strong>특수한 시드 생성 알고리즘</strong>을 사용합니다.</li>
</ul>
<ul>
<li><code>PRNG</code>가 생성할 수 있는 고유한 시퀀스의 이론적 최대 개수는 PRNG가 가진 <strong>상태의 비트 수에 의해 결정</strong>됩니다.</li>
<li>예를 들어, <code>128</code>비트 상태를 가진 <code>PRNG</code>는 이론적으로 최대 <code>2^128</code>개의 고유한 출력 시퀀스를 생성할 수 있습니다.</li>
<li>하지만, 실제로 어떤 출력 시퀀스가 생성되는지는 초기 상태에 달려 있고, 이 초기 상태는 결국 <strong>시드</strong>에 의해 결정됩니다. </li>
<li>따라서 현실적으로 <code>PRNG</code>가 실제로 생성할 수 있는 고유한 출력 시퀀스의 수는 <strong>프로그램이 제공할 수 있는 고유한 시드 값의 개수에 의해 제한</strong>됩니다. </li>
</ul>
<ul>
<li><code>PRNG</code>에 충분한 비트의 양질의 시드 데이터가 제공되지 않는 경우, 우리는 이를 <strong>&quot;시드가 부족하다(Underseeded)&quot;</strong>라고 말합니다. </li>
<li>시드가 부족한 <code>PRNG</code>는 품질이 어딘가 손상된 무작위 결과를 생성하기 시작할 수 있으며, 시드 부족 현상이 심할수록 결과의 품질은 더욱 나빠지게 됩니다.</li>
</ul>
<ul>
<li>예를 들어, 시드가 부족한 <code>PRNG</code>는 다음과 같은 문제를 보일 수 있어요.</li>
<li><ol>
<li>연속적으로 실행하여 생성된 난수 시퀀스들이 <strong>서로 높은 상관관계</strong>를 가질 수 있습니다.</li>
</ol>
</li>
<li><ol start="2">
<li><code>N</code>번째 난수를 생성할 때, 특정 값은 아예 생성되지 않을 수 있습니다. 
예를 들어, 특정한 방식으로 시드가 부족한 <strong>메르센 트위스터</strong>는 첫 번째 출력으로 숫자 <code>7</code>이나 <code>13</code>을 절대 생성하지 못합니다.</li>
</ol>
</li>
<li><ol start="3">
<li>누군가가 처음 생성된 난수 값(또는 처음 몇 개의 난수 값)을 보고 시드를 추측해 낼 수도 있습니다. 
그렇게 되면 앞으로 생성될 모든 난수를 알아낼 수 있게 되고, 시스템의 허점을 악용하거나 속임수를 쓸 수 있게 됩니다.</li>
</ol>
</li>
</ul>
<hr>
<h2 id="c에서의-무작위화-randomization-in-c">C++에서의 무작위화 (Randomization in C++)</h2>
<ul>
<li>C++에서 무작위화 기능은 표준 라이브러리의 <code>&lt;random&gt;</code> 헤더를 통해 접근할 수 있습니다. </li>
<li>무작위 라이브러리 내에는 (C++20 기준으로) 사용할 수 있는 <strong>6가지 PRNG 제품군</strong>이 있습니다.</li>
</ul>
<table>
<thead>
<tr>
<th>타입 이름 (Type name)</th>
<th>제품군 (Family)</th>
<th>주기 (Period)</th>
<th>상태 크기 (State size)</th>
<th>성능 (Performance)</th>
<th>품질 (Quality)</th>
<th>사용해야 할까요?</th>
</tr>
</thead>
<tbody><tr>
<td>minstd_rand</td>
<td>선형 합동 생성기 (Linear congruential generator)</td>
<td>2³¹</td>
<td>4 bytes</td>
<td>나쁨 (Bad)</td>
<td>끔찍함 (Awful)</td>
<td>아니요 (No)</td>
</tr>
<tr>
<td>minstd_rand0</td>
<td>선형 합동 생성기 (Linear congruential generator)</td>
<td>2³¹</td>
<td>4 bytes</td>
<td>나쁨 (Bad)</td>
<td>끔찍함 (Awful)</td>
<td>아니요 (No)</td>
</tr>
<tr>
<td>mt19937</td>
<td>메르센 트위스터 (Mersenne twister)</td>
<td>2¹⁹⁹³⁷</td>
<td>2500 bytes</td>
<td>무난함 (Decent)</td>
<td>무난함 (Decent)</td>
<td>아마도요 (다음 섹션 참고)</td>
</tr>
<tr>
<td>mt19937_64</td>
<td>메르센 트위스터 (Mersenne twister)</td>
<td>2¹⁹⁹³⁷</td>
<td>2500 bytes</td>
<td>무난함 (Decent)</td>
<td>무난함 (Decent)</td>
<td>아마도요 (다음 섹션 참고)</td>
</tr>
<tr>
<td>ranlux24</td>
<td>빼고 자리올림 (Subtract and carry)</td>
<td>10¹⁷¹</td>
<td>96 bytes</td>
<td>끔찍함 (Awful)</td>
<td>좋음 (Good)</td>
<td>아니요 (No)</td>
</tr>
<tr>
<td>ranlux48</td>
<td>빼고 자리올림 (Subtract and carry)</td>
<td>10¹⁷¹</td>
<td>96 bytes</td>
<td>끔찍함 (Awful)</td>
<td>좋음 (Good)</td>
<td>아니요 (No)</td>
</tr>
<tr>
<td>knuth_b</td>
<td>셔플된 선형 합동 생성기 (Shuffled linear congruential generator)</td>
<td>2³¹</td>
<td>1028 bytes</td>
<td>끔찍함 (Awful)</td>
<td>나쁨 (Bad)</td>
<td>아니요 (No)</td>
</tr>
<tr>
<td>default_random_engine</td>
<td>위 중 하나 (구현에 따라 다름)</td>
<td>다양함</td>
<td>다양함</td>
<td>??</td>
<td>??</td>
<td>아니요 (No)</td>
</tr>
<tr>
<td>rand()</td>
<td>선형 합동 생성기 (Linear congruential generator)</td>
<td>2³¹</td>
<td>4 bytes</td>
<td>나쁨 (Bad)</td>
<td>끔찍함 (Awful)</td>
<td>아니요 (No)</td>
</tr>
</tbody></table>
<ul>
<li><code>knuth_b</code> <code>default_random_engine</code> 또는 <code>rand()</code> (C 언어와의 호환성을 위해 제공되는 난수 생성기)를 사용할 이유는 전혀 없습니다.</li>
<li>C++20을 기준으로 할 때, <strong>메르센 트위스터(Mersenne Twister)</strong> 알고리즘은 C++에서 기본으로 제공하는 PRNG 중 성능과 품질 모두 무난한 <strong>유일한</strong> 생성기입니다.</li>
</ul>
<hr>
<h2 id="그럼-메르센-트위스터mersenne-twister를-사용해야겠죠">그럼 메르센 트위스터(Mersenne Twister)를 사용해야겠죠?</h2>
<ul>
<li>아마도 그럴 겁니다. 대부분의 애플리케이션에서 메르센 트위스터는 성능과 품질 측면에서 충분히 훌륭합니다.</li>
<li>하지만 현대의 <code>PRNG</code> 기준으로 볼 때, 메르센 트위스터는 약간 구식이라는 점을 기억해 두시면 좋습니다. </li>
<li>메르센 트위스터의 가장 큰 문제점은 <code>624</code>개의 생성된 숫자를 보고 나면 <strong>다음 결과를 예측할 수 있다</strong>는 점입니다. </li>
<li>따라서 <strong>예측 불가능성이 요구되는 애플리케이션</strong>에는 <strong>절대 적합하지 않습니다.</strong></li>
</ul>
<ul>
<li>최고 품질의 난수 결과가 필요한 애플리케이션(예: 통계 시뮬레이션), 가장 빠른 속도가 필요한 경우, 또는 예측 불가능성이 중요한 애플리케이션(예: 암호화)을 개발 중이라면 <strong>서드파티(3rd party) 외부 라이브러리를 사용해야 합니다.</strong></li>
</ul>
<ul>
<li>현재 시점에서 인기 있는 선택지들은 다음과 같습니다:</li>
<li><ol>
<li>암호화 목적이 아닌(예측 가능한) 일반 PRNG의 경우: <code>Xoshiro</code> 제품군과 <code>Wyrand</code></li>
</ol>
</li>
<li><ol start="2">
<li>암호화 목적의(예측 불가능한) PRNG의 경우: <code>Chacha</code> 제품군</li>
</ol>
</li>
</ul>
<hr>
<h2 id="메르센-트위스터를-사용하여-c에서-난수-생성하기">메르센 트위스터를 사용하여 C++에서 난수 생성하기</h2>
<ul>
<li><strong>메르센 트위스터(Mersenne Twister)</strong> <code>PRNG</code>는 이름이 멋질 뿐만 아니라, 아마도 모든 프로그래밍 언어를 통틀어 가장 인기 있는 PRNG일 것입니다. 오늘날의 기준으로는 조금 오래된 방식이긴 하지만, 일반적으로 훌륭한 결과물과 준수한 성능을 보여줍니다. </li>
<li>C++의 random 라이브러리는 <strong>두 가지 메르센 트위스터 타입을 지원</strong>합니다.</li>
</ul>
<ul>
<li><code>mt19937</code> <strong>32비트 부호 없는 정수</strong>를 생성하는 메르센 트위스터입니다.</li>
<li><code>mt19937_64</code> <strong>64비트 부호 없는 정수</strong>를 생성하는 메르센 트위스터입니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;random&gt; // std::mt19937을 사용하기 위해 포함합니다

int main()
{
    std::mt19937 mt{}; // 32비트 메르센 트위스터 객체를 생성합니다

    // 난수를 여러 개 출력해 봅니다
    for (int count{ 1 }; count &lt;= 40; ++count)
    {
        std::cout &lt;&lt; mt() &lt;&lt; &#39;\t&#39;; // 난수를 하나 생성합니다

        // 5개의 숫자를 출력했다면, 새로운 줄로 넘어갑니다
        if (count % 5 == 0)
            std::cout &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}</code></pre>
<ul>
<li>가장 먼저 모든 난수 기능을 담고 있는 <code>&lt;random&gt;</code> 헤더를 포함했습니다. </li>
<li>그다음 <code>std::mt19937 mt</code>라는 구문을 통해 <strong>32비트 메르센 트위스터 엔진을 생성</strong>했죠. </li>
<li>그런 뒤 무작위 <strong>32비트 부호 없는 정수를 생성하고 싶을 때마다 **<code>mt()</code></strong>를 호출**했습니다.</li>
</ul>
<hr>
<h2 id="메르센-트위스터로-주사위-굴리기">메르센 트위스터로 주사위 굴리기</h2>
<ul>
<li><code>32</code>비트 <code>PRNG</code>는 <code>0</code>부터 <code>4,294,967,295</code>사이의 난수를 생성하지만, 우리가 항상 이처럼 큰 범위의 숫자를 원하는 것은 아닙니다.</li>
<li>만약 보드게임이나 주사위 게임을 시뮬레이션하는 프로그램이라면, <code>1</code>부터 <code>6</code> 사이의 난수를 생성하여 <code>6</code>면체 주사위 굴리기를 흉내 내고 싶을 것입니다. </li>
<li>만약 던전 탐험 게임이고 플레이어가 몬스터에게 <code>7</code>에서 <code>11</code> 사이의 피해를 주는 검을 가지고 있다면, 플레이어가 몬스터를 때릴 때마다 <code>7</code>에서 <code>11</code> 사이의 난수를 생성해야겠죠.</li>
</ul>
<ul>
<li>안타깝게도 <code>PRNG</code> 자체는 이런 기능을 할 수 없습니다. 오직 전체 범위의 숫자만 생성할 수 있죠. </li>
<li>우리가 필요한 것은 <code>PRNG</code>에서 출력된 숫자를 우리가 원하는 <strong>더 작은 범위의 값으로 변환해 주는 방법</strong>입니다.</li>
<li>물론 각 값이 나올 확률은 동일해야 합니다. 우리가 직접 이 작업을 수행하는 함수를 작성할 수도 있지만, <strong>편향되지 않은 결과를 만들어내는 것은 생각보다 까다로운 작업</strong>입니다.</li>
</ul>
<ul>
<li>다행히도 <code>random</code> 라이브러리에는 이를 도와주는 <strong>난수 분포(Random number distributions) 기능</strong>이 있습니다. </li>
<li>난수 분포는 <code>PRNG</code>의 출력을 다른 숫자 분포로 변환해 줍니다.</li>
</ul>
<ul>
<li><code>random</code> 라이브러리에는 여러 가지 난수 분포가 있지만, 통계 분석을 하지 않는 이상 대부분은 사용할 일이 없을 겁니다. </li>
<li>하지만 정말 유용하게 쓰이는 분포가 딱 하나 있습니다. 바로 <strong>균등 분포(Uniform distribution)</strong>입니다. </li>
<li>이는 두 숫자 <code>X</code>와 <code>Y</code> 사이에서 동일한 확률로 결과를 생성하는 난수 분포입니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;random&gt; // std::mt19937 및 std::uniform_int_distribution을 위해 포함합니다

int main()
{
    std::mt19937 mt{};

    // 1부터 6 사이의 숫자를 균등하게 생성하는 재사용 가능한 난수 생성기를 만듭니다
    std::uniform_int_distribution die6{ 1, 6 }; // C++14의 경우, std::uniform_int_distribution&lt;&gt; die6{ 1, 6 }; 를 사용하세요

    // 난수를 여러 개 출력해 봅니다
    for (int count{ 1 }; count &lt;= 40; ++count)
    {
        std::cout &lt;&lt; die6(mt) &lt;&lt; &#39;\t&#39;; // 여기서 주사위를 굴립니다

        // 10개의 숫자를 출력했다면, 새로운 줄로 넘어갑니다
        if (count % 10 == 0)
            std::cout &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}

이 코드는 다음과 같은 결과를 생성합니다:

3       1       3       6       5       2       6       6       1       2
2       6       1       1       6       1       4       5       2       5
6       2       6       2       1       3       5       4       5       6
1       4       2       3       1       2       2       6       2       1</code></pre>
<ul>
<li>이전 예제와 비교했을 때 눈에 띄는 차이점은 딱 두 가지입니다. </li>
<li>첫째, <code>1</code>과 <code>6</code> 사이의 숫자를 생성하기 위해 균등 분포 변수 <code>die6</code> 를 만들었습니다. </li>
<li>둘째, 32비트 부호 없는 정수를 생성하기 위해 <code>mt()</code>를 호출하는 대신, 이제는 <code>1</code>과 <code>6</code> 사이의 값을 생성하기 위해 <strong><code>die6(mt)</code></strong>를 호출하고 있습니다.</li>
</ul>
<hr>
<h2 id="위-프로그램은-생각만큼-무작위가-아닙니다">위 프로그램은 생각만큼 무작위가 아닙니다!</h2>
<ul>
<li>프로그램을 여러 번 실행해 보면 매번 완전히 똑같은 숫자들이 출력된다는 것을 알 수 있습니다.</li>
<li>시퀀스(Sequence) 내의 각 숫자는 이전 숫자에 대해 무작위일지 몰라도, 전체 시퀀스 자체는 전혀 무작위가 아니었던 거죠.</li>
<li>프로그램을 실행할 때마다 정확히 동일한 결과가 나옵니다.</li>
</ul>
<ul>
<li>우리의 코드에서는 메르센 트위스터를 <strong>단순히 값 초기화</strong>하고 있기 때문에, 프로그램이 실행될 때마다 동일한 기본 시드 값으로 초기화되고 있었던 것입니다. 시드가 같으니, 생성되는 난수도 같을 수밖에 없죠.</li>
<li>프로그램을 실행할 때마다 전체 시퀀스가 다르게 무작위화되려면, 고정된 숫자가 아닌 시드를 골라야 합니다. </li>
<li>가장 먼저 떠오르는 생각은 &quot;시드로 쓸 난수가 필요하겠네!&quot;일 텐데요. 좋은 생각이지만, 난수를 생성하기 위해 난수가 필요하다면 모순에 빠지게 됩니다.</li>
</ul>
<ul>
<li>사실 시드가 <strong>굳이 난수일 필요는 없습니다.</strong> </li>
<li>** 프로그램이 실행될 때마다 변하는 어떤 값만 선택하면 됩니다.** </li>
<li>그러면 <code>PRNG</code>를 사용하여 그 시드로부터 고유한 유사 난수 시퀀스를 생성할 수 있습니다.</li>
<li>이를 위해 일반적으로 두 가지 방법이 사용됩니다.</li>
<li><ol>
<li><strong>시스템 클록(System clock)</strong> 사용하기</li>
</ol>
</li>
<li><ol start="2">
<li>시스템의 <strong>랜덤 디바이스(Random device)</strong> 사용하기</li>
</ol>
</li>
</ul>
<hr>
<h2 id="시스템-클록으로-시드-설정하기">시스템 클록으로 시드 설정하기</h2>
<ul>
<li>프로그램을 실행할 때마다 항상 달라지는 것이 무엇일까요? </li>
<li>프로그램을 아주 정확히 똑같은 시간에 두 번 실행하지 않는 이상, 정답은 바로 <strong>&#39;현재 시간&#39;</strong>입니다. </li>
<li>따라서 현재 시간을 시드 값으로 사용하면 프로그램이 실행될 때마다 다른 난수 세트를 생성하게 됩니다.</li>
</ul>
<ul>
<li>C와 C++에서는 오랫동안 현재 시간 <code>std::time()</code> 함수를 사용하여 <code>PRNG</code>의 시드를 설정해 왔으므로, 기존 코드에서 이런 방식을 많이 보실 수 있을 겁니다.</li>
<li>다행히도 C++에는 시드 값을 생성하는 데 사용할 수 있는 다양한 시계를 포함한 <code>&lt;chrono&gt;</code> 라이브러리가 있습니다.</li>
<li>프로그램을 연속으로 빠르게 실행했을 때 두 시간 값이 동일해질 가능성을 최소화하려면, 최대한 빠르게 변하는 시간 단위를 사용해야 합니다.</li>
<li>이를 위해, 시계가 측정할 수 있는 가장 이른 시간부터 지금까지 시간이 얼마나 지났는지 시계에게 물어볼 것입니다. 
이 시간은 <strong>&quot;틱(Ticks)&quot;</strong>이라는 매우 작은 단위로 측정됩니다 (보통 나노초 단위지만, 밀리초일 수도 있습니다).</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;random&gt; // std::mt19937을 위해 포함
#include &lt;chrono&gt; // std::chrono를 위해 포함

int main()
{
    // steady_clock을 사용하여 메르센 트위스터의 시드를 설정합니다
    std::mt19937 mt{ static_cast&lt;std::mt19937::result_type&gt;(
        std::chrono::steady_clock::now().time_since_epoch().count()
        ) };

    // 1부터 6 사이의 숫자를 균등하게 생성하는 재사용 가능한 난수 생성기
    std::uniform_int_distribution die6{ 1, 6 };

    // 난수 여러 개 출력
    for (int count{ 1 }; count &lt;= 40; ++count)
    {
        std::cout &lt;&lt; die6(mt) &lt;&lt; &#39;\t&#39;; // 여기서 주사위를 굴립니다

        // 10개를 출력하면 줄바꿈
        if (count % 10 == 0)
            std::cout &lt;&lt; &#39;\n&#39;;
    }

    return 0;
}</code></pre>
<ul>
<li>이전 프로그램에서 바뀐 부분은 두 가지뿐입니다. </li>
<li>첫째, 시계에 접근할 수 있도록 <code>&lt;chrono&gt;</code>를 포함했습니다. </li>
<li>둘째, 메르센 트위스터의 시드 값으로 시계의 현재 시간을 사용했습니다. </li>
<li>이제 프로그램을 여러 번 실행해 보면 매번 결과가 달라지는 것을 직접 확인하실 수 있을 거예요.</li>
</ul>
<ul>
<li>이 방식의 단점은 프로그램을 짧은 시간 안에 여러 번 연속으로 실행하면, 각 실행을 위해 생성된 시드가 크게 다르지 않을 수 있다는 점입니다. </li>
<li>이는 통계적 관점에서 난수 결과의 품질에 영향을 미칠 수 있습니다. </li>
<li>일반적인 프로그램에서는 문제가 되지 않지만, 독립적이고 높은 품질의 결과가 필요한 프로그램에서는 이 시드 설정 방식이 불충분할 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - 7]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-7</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-7</guid>
            <pubDate>Sun, 15 Feb 2026 11:42:24 GMT</pubDate>
            <description><![CDATA[<h2 id="복합문compound-statement">복합문(Compound statement)</h2>
<ul>
<li><strong>복합문(Compound statement)</strong>, 또는 <strong>블록(Block)</strong>이라고 부르는 것은 <code>0</code>개 이상의 문장들을 <strong>하나로 묶은 그룹</strong>을 말해요.</li>
<li>아주 중요한 점은, 컴파일러가 이 묶음을 마치 하나의 문장인 것처럼 취급한다는 거예요.</li>
</ul>
<ul>
<li>우리가 함수를 만들 때 이미 블록을 사용해 왔답니다.</li>
<li>함수의 <strong>몸체</strong>(Body)가 바로 블록이니까요.</li>
</ul>
<ul>
<li>함수 안에 또 다른 함수를 정의할 수는 없지만, 블록 안에 또 다른 블록을 넣는 것은 가능해요. 이걸 &#39;중첩된다&#39;라고 표현하죠.</li>
<li>블록이 중첩되었을 때, 바깥쪽을 감싸고 있는 블록을 <strong>외부 블록(Outer block)</strong>이라 하고,</li>
<li>그 안에 들어있는 블록을** 내부 블록(Inner block)** 혹은 <strong>중첩 블록(Nested block)</strong>이라고 부릅니다.</li>
</ul>
<hr>
<h2 id="조건에-따라-여러-문장-실행하기">조건에 따라 여러 문장 실행하기</h2>
<ul>
<li>기본적으로 if 문은 조건이 &#39;참(True)&#39;일 때 딱 하나의 문장만 실행하게 되어 있어요.</li>
<li>하지만 조건이 맞았을 때 여러 가지 일을 한꺼번에 처리하고 싶다면 어떻게 해야 할까요?</li>
<li>이때 그 &#39;하나의 문장&#39; 자리에 문장들의 묶음인 &#39;블록&#39;을 넣어주면 된답니다.<pre><code class="language-cpp">if (1)
  std::cout &lt;&lt; &quot;하나의 문장&quot;;
</code></pre>
</li>
</ul>
<p>if (1) { //내부 블록
    std::cout &lt;&lt; &quot;하나의 문장&quot;;
    std::cout &lt;&lt; &quot;하나 이상의 문장&quot;;
 }</p>
<pre><code>
---
## 블록 중첩 레벨 (Nesting levels)
- 블록 안에 블록을 넣고, 그 안에 또 블록을 넣는 것도 가능할까요? 네, 가능합니다!
- 여기서** 중첩 레벨(Nesting level)** 혹은 **중첩 깊이(Nesting depth)**라는 용어가 나와요.
- 이건 함수 내의 어떤 지점에서 &#39;지금 내가 최대 몇 개의 블록 안에 감싸여 있는가&#39;를 나타내는 숫자예요.


- C++ 표준(Standard)에 따르면 컴파일러는 무려 `256`단계의 중첩까지 지원해야 한다고 해요.
- 하지만 모든 컴파일러가(예: 작성 시점 기준 Visual Studio 등) 이를 완벽히 지원하는 건 아닐 수 있어요.


- 기술적으로 가능하다고 해서 깊게 만드는 게 좋을까요?
- 아닙니다. 중첩 레벨은 **3단계 이하로 유지**하는 것이 좋습니다.


- 함수의 길이가 너무 길어지면 여러 개의 작은 함수로 나누는 **&#39;리팩토링(Refactoring)&#39;**을 하듯이,
- 블록의 중첩이 너무 깊어지면 코드를 읽기가 아주 힘들어져요.
- 이럴 때도 가장 깊은 곳에 있는 블록들을 떼어내어 별도의 함수로 만드는 리팩토링을 하는 것이 좋습니다.
![](https://velog.velcdn.com/images/dj_trace25/post/abeaea94-e197-40f8-871d-b200e8a22ca2/image.png)

## 나만의 네임스페이스 정의하기
- C++에서는 `namespace`라는 키워드를 사용해 우리만의 네임스페이스를 만들 수 있어요.
- 이렇게 여러분이 직접 만든 네임스페이스를 흔히 **사용자 정의 네임스페이스(User-defined namespaces)**라고 불러요.
- 더 정확히는 **&#39;프로그램 정의 네임스페이스&#39;**라고 하는 게 맞겠지만요.


- 문법은 아주 간단해요.
- `namespace` 키워드를 쓰고, 그 뒤에 네임스페이스의 식별자를 적은 다음, 중괄호 { } 안에 내용을 넣으면 끝이에요.

```cpp
namespace NamespaceIdentifier
{
    // 네임스페이스의 내용이 여기에 들어갑니다
}</code></pre><hr>
<h2 id="범위-지정-연산자-로-네임스페이스-접근하기">범위 지정 연산자 ::로 네임스페이스 접근하기</h2>
<ul>
<li>특정 네임스페이스 안에 있는 식별자를 찾으라고 컴파일러에게 지시하는 가장 좋은 방법은 범위 지정 연산자<code>::</code>를 사용하는 거예요.</li>
</ul>
<pre><code class="language-cpp">std::cout &lt;&lt; Goo::doSomething(4, 3) &lt;&lt; &#39;\n&#39;; // Goo 네임스페이스에 있는 doSomething()을 사용</code></pre>
<hr>
<h2 id="이름-없이-범위-지정-연산자-사용하기">이름 없이 범위 지정 연산자 사용하기</h2>
<ul>
<li>범위 지정 연산자 앞에 <code>::doSomething</code> 처럼 아무런 네임스페이스 이름도 적지 않을 수도 있어요.</li>
<li>이렇게 하면 컴파일러는 <strong>전역 네임스페이스</strong>에서 그 이름을 찾으라는 뜻으로 이해합니다.</li>
</ul>
<hr>
<h2 id="네임스페이스-내부에서의-식별자-찾기">네임스페이스 내부에서의 식별자 찾기</h2>
<ul>
<li>네임스페이스 안에서 어떤 식별자를 사용했는데, 앞에 범위 지정(Foo:: 같은 것)을 안 해줬다면 어떻게 될까요?</li>
<li>컴파일러는 일단 지금 있는 그 네임스페이스 안에서 정의를 찾으려고 노력해요.</li>
<li>만약 못 찾으면? 그 네임스페이스를 감싸고 있는 상위 네임스페이스를 차례대로 뒤져보고, 마지막으로 전역 네임스페이스까지 확인합니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void print() // 전역 네임스페이스의 print()
{
    std::cout &lt;&lt; &quot; there\n&quot;;
}

namespace Foo
{
    void print() // Foo 네임스페이스의 print()
    {
        std::cout &lt;&lt; &quot;Hello&quot;;
    }

    void printHelloThere()
    {
        print();   // Foo 네임스페이스 안의 print()를 먼저 호출함
        ::print(); // 전역 네임스페이스의 print()를 호출함
    }
}

int main()
{
    Foo::printHelloThere();
    return 0;
}</code></pre>
<hr>
<h2 id="네임스페이스-내용의-전방-선언forward-declaration">네임스페이스 내용의 전방 선언(Forward declaration)</h2>
<ul>
<li><strong>헤더 파일</strong>을 사용해서 전방 선언을 할 때도 주의할 점이 있어요.</li>
<li>네임스페이스 안에 있는 식별자를 전방 선언하려면, 선언도 <strong>똑같은 네임스페이스 안</strong>에서 해줘야 해요.</li>
</ul>
<pre><code class="language-cpp">// add.h

#ifndef ADD_H
#define ADD_H

namespace BasicMath
{
    // add() 함수는 BasicMath 네임스페이스의 일부입니다
    int add(int x, int y);
}

#endif</code></pre>
<pre><code class="language-cpp">// add.cpp

#include &quot;add.h&quot;

namespace BasicMath
{
    // add() 함수를 BasicMath 네임스페이스 안에서 정의합니다
    int add(int x, int y)
    {
        return x + y;
    }
}</code></pre>
<pre><code class="language-cpp">// main.cpp

#include &quot;add.h&quot;  // BasicMath::add()를 위해 포함
#include &lt;iostream&gt;

int main()
{
    std::cout &lt;&lt; BasicMath::add(4, 3) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<hr>
<h2 id="여러-개의-네임스페이스-블록">여러 개의 네임스페이스 블록</h2>
<ul>
<li>같은 이름의 네임스페이스 블록을 여러 곳(여러 파일 또는 같은 파일 내 여러 곳)에 나눠서 작성해도 괜찮아요.</li>
<li>모두 같은 네임스페이스의 가족으로 취급됩니다.</li>
</ul>
<hr>
<h2 id="중첩된-네임스페이스-nested-namespaces">중첩된 네임스페이스 (Nested namespaces)</h2>
<ul>
<li>네임스페이스 안에 또 다른 네임스페이스를 만들 수도 있어요.</li>
<li>C++17부터는 이렇게 중첩된 네임스페이스를 훨씬 간단하게 선언할 수 있어요.</li>
<li><code>Goo</code>가 <code>Foo</code>안에 있으니까 <code>Foo::Goo::add</code>라고 주소를 길게 써서 접근해야 해요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

namespace Foo::Goo // Foo 안에 Goo가 있다는 것을 한 번에 표현 (C++17 스타일)
{
    int add(int x, int y)
    {
        return x + y;
    }
}

int main()
{
    std::cout &lt;&lt; Foo::Goo::add(1, 2) &lt;&lt; &#39;\n&#39;;
    return 0;
}</code></pre>
<hr>
<h2 id="네임스페이스-별칭-namespace-aliases">네임스페이스 별칭 (Namespace aliases)</h2>
<ul>
<li>중첩된 네임스페이스 이름이 너무 길어서 타이핑하기 힘들다면, 네임스페이스 별칭을 써서 짧은 별명을 붙여줄 수 있어요.</li>
<li>별칭의 좋은 점은 코드를 유지 보수할 때도 드러납니다.</li>
</ul>
<ul>
<li>만약 Foo::Goo의 기능을 다른 곳(V2)으로 옮기고 싶다면, 별칭이 가리키는 곳만 <code>V2</code>로 바꿔주면 돼요.</li>
<li>코드 전체를 뒤져서 <code>Foo::Goo</code>를 일일이 바꿀 필요가 없죠.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

namespace Foo::Goo
{
    int add(int x, int y)
    {
        return x + y;
    }
}

int main()
{
    namespace Active = Foo::Goo; // 이제 Active는 Foo::Goo를 가리킵니다

    std::cout &lt;&lt; Active::add(1, 2) &lt;&lt; &#39;\n&#39;; // 실제로는 Foo::Goo::add()가 호출됨

    return 0;
} // Active 별칭은 여기서 끝납니다</code></pre>
<hr>
<h2 id="네임스페이스-사용-팁">네임스페이스 사용 팁</h2>
<ul>
<li>C++의 네임스페이스는 원래 정보를 계층적으로 정리하라고 만든 게 아니라, 주로 <strong>이름 충돌을 막기 위해 설계</strong>되었어요.</li>
<li>개인적인 작은 프로그램을 만들 때는 굳이 네임스페이스를 안 써도 괜찮아요.</li>
<li>하지만 외부 라이브러리를 많이 갖다 쓰는 큰 프로젝트라면 이름 충돌을 피하기 위해 코드를 네임스페이스로 감싸주는 게 좋습니다.</li>
</ul>
<ul>
<li>다른 사람에게 배포할 코드는 반드시 네임스페이스를 사용해서, 통합될 때 충돌이 나지 않게 해야 합니다.</li>
<li>보통 Foologger처럼 최상위 네임스페이스 하나면 충분해요.</li>
<li>이렇게 하면 사용자가 Foologger라고 쳤을 때 자동 완성 기능으로 라이브러리의 모든 기능을 쉽게 볼 수 있다는 장점도 있죠.
<img src="https://velog.velcdn.com/images/dj_trace25/post/a2423737-1431-4608-98dd-42273b4d1a72/image.png" alt=""></li>
</ul>
<h2 id="지역-변수-local-variables">지역 변수 (Local variables)</h2>
<ul>
<li>지난 2챕터 지역 범위 소개에서 우리는 지역 변수에 대해 배웠습니다.</li>
<li>함수 내부(함수의 매개변수 포함)에 정의된 변수들을 말하죠.</li>
</ul>
<ul>
<li>범위라는 개념도 배웠어요.</li>
<li>어떤 식별자(변수 이름 등)의 범위란 소스 코드 내에서 그 식별자에 접근할 수 있는 영역을 뜻해요.</li>
</ul>
<ul>
<li>접근할 수 있다면 &quot;범위 내에 있다(In scope)&quot;라고 하고,</li>
<li>접근할 수 없다면 &quot;범위를 벗어났다(Out of scope)&quot;라고 합니다.</li>
<li>범위는 컴파일 타임에 결정되는 속성이라서, 범위를 벗어난 식별자를 사용하려고 하면 컴파일 에러가 발생해요.</li>
</ul>
<hr>
<h2 id="지역-변수는-블록-범위block-scope를-가집니다">지역 변수는 블록 범위(Block scope)를 가집니다.</h2>
<ul>
<li>즉, 변수가 정의된 지점부터 그 변수가 포함된 블록이 끝나는 지점까지만 유효하다는 뜻이에요.</li>
</ul>
<hr>
<h2 id="지역-변수는-자동-저장-기간을-가집니다">지역 변수는 자동 저장 기간을 가집니다.</h2>
<ul>
<li>변수의 저장 기간은 변수가 언제 생성되고(메모리에 할당되고) 언제 소멸될지를 결정하는 규칙이에요.</li>
<li>대부분의 경우, 이 저장 기간이 변수의 수명을 결정합니다.</li>
</ul>
<ul>
<li>지역 변수는 자동 저장 기간(Automatic storage duration)을 가집니다.</li>
<li>쉽게 말해, 변수가 정의되는 시점에 자동으로 생성되고, 정의된 블록이 끝날 때 자동으로 소멸된다는 뜻이죠.</li>
<li>이런 이유로 지역 변수를 <strong>자동 변수(Automatic variable)</strong>라고 부르기도 해요.</li>
</ul>
<hr>
<h2 id="지역-변수는-연결linkage이-없습니다">지역 변수는 연결(Linkage)이 없습니다.</h2>
<ul>
<li>식별자(변수 이름 등)에는 <strong>연결</strong>이라는 또 다른 속성이 있어요.</li>
<li>지역 변수는 연결이 없습니다. </li>
<li>즉, 연결이 없는 식별자는 이름이 같더라도 각각의 선언이 서로 다른 고유한 객체나 함수를 의미합니다.</li>
<li>프로그래밍에서 &#39;연결&#39;이란, A 구역과 B 구역에 이름이 똑같은 변수(또는 함수)가 있을 때, 이 둘이 실제로 한 몸처럼 똑같은 녀석인지, 아니면 우연히 이름만 같을 뿐 각자 따로 노는 녀석인지를 판가름하는 성질을 말해요.</li>
</ul>
<hr>
<h2 id="변수는-가장-제한적인-범위에서-정의해야-합니다">변수는 가장 제한적인 범위에서 정의해야 합니다.</h2>
<ul>
<li>변수가 <strong>특정 중첩 블록 안에서만 사용된다면</strong>, 그 변수는 반드시 그 중첩 블록 안에서 정의해야 합니다.</li>
<li>변수의 범위를 제한하면 활성화된 변수의 수가 줄어들어 프로그램의 복잡도가 낮아집니다. </li>
<li>블록 안에 정의된 변수는 그 블록(과 그 내부 블록) 안에서만 영향을 미치므로 프로그램을 이해하기가 더 수월해집니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
    // 여기에 y를 정의하지 마세요.
    {
        // y는 오직 이 블록 안에서만 사용되므로, 여기서 정의합니다.
        int y { 5 };
        std::cout &lt;&lt; y &lt;&lt; &#39;\n&#39;;
    }
    // 그렇지 않으면 y가 필요 없는 이 곳에서도 y를 사용할 수 있게 됩니다.
    return 0;
}</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/8b40b365-bd08-4b45-91c5-da30f5f8260a/image.png" alt=""></p>
<h2 id="전역-변수-소개-introduction-to-global-variables">전역 변수 소개 (Introduction to global variables)</h2>
<ul>
<li>C++에서는 변수를 <strong>함수 외부에서</strong>도 선언할 수 있습니다. 이러한 변수들을 <strong>전역 변수(Global variables)</strong>라고 부릅니다.</li>
</ul>
<hr>
<h2 id="전역-변수-선언하기-declaring-global-variables">전역 변수 선언하기 (Declaring global variables)</h2>
<ul>
<li>관례적으로 전역 변수는 파일의 맨 위, <code>#include</code> 구문들 바로 아래의 전역 네임스페이스에 선언해요.</li>
</ul>
<hr>
<h2 id="전역-변수의-범위-the-scope-of-global-variables">전역 변수의 범위 (The scope of global variables)</h2>
<ul>
<li>전역 네임스페이스에 선언된 식별자는 전역 네임스페이스 범위를 갖습니다. </li>
<li>이를 보통 <strong>전역 범위(Global scope)</strong>라고 부르고, 때로는 비공식적으로 <strong>파일 범위(File scope)</strong>라고도 해요. </li>
<li>이는 변수가 선언된 시점부터 해당 변수가 선언된 <strong>파일</strong>이 끝날 때까지 어디서든 접근할 수 있다는 뜻입니다.</li>
<li>전역 변수는 사용자가 직접 정의한 네임스페이스 안에서도 정의할 수 있습니다.</li>
<li>전역 변수는 전역 네임스페이스에 바로 정의하기보다는, 가급적 네임스페이스안에 정의하는 것을 권장합니다.</li>
</ul>
<hr>
<h2 id="전역-변수는-정적-지속-시간을-갖습니다">전역 변수는 정적 지속 시간을 갖습니다.</h2>
<ul>
<li>전역 변수는** 프로그램이 시작될 때**(main() 함수가 실행되기도 전에) 생성되고, 프로그램이 종료될 때 파괴됩니다. </li>
<li>이것을 <strong>정적 지속 시간(Static duration)</strong>이라고 부릅니다. </li>
<li>이러한 정적 지속 시간을 가진 변수들을 종종 <strong>정적 변수(Static variables)</strong>라고도 부른답니다.</li>
</ul>
<hr>
<h2 id="전역-변수-초기화-global-variable-initialization">전역 변수 초기화 (Global variable initialization)</h2>
<ul>
<li>기본적으로 값이 초기화되지 않는 지역 변수와는 달리, 정적 지속 시간을 가진 변수들은 <strong>기본적으로 0으로 초기화</strong>가 됩니다.</li>
</ul>
<pre><code class="language-cpp">int g_x;       // 명시적 초기화 없음 (기본적으로 0으로 초기화됨)
int g_y {};    // 값 초기화 (결과적으로 0으로 초기화됨)
int g_z { 1 }; // 특정 값으로 리스트 초기화됨</code></pre>
<hr>
<h2 id="상수-전역-변수-constant-global-variables">상수 전역 변수 (Constant global variables)</h2>
<ul>
<li>지역 변수와 마찬가지로, 전역 변수도 상수가 될 수 있습니다. </li>
<li>그리고 모든 상수가 그렇듯, <strong>상수 전역 변수는 반드시 초기화</strong>되어야만 합니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

const int g_x;     // 오류: 상수 변수는 반드시 초기화되어야 합니다
constexpr int g_w; // 오류: constexpr 변수는 반드시 초기화되어야 합니다

const int g_y { 1 };     // const 전역 변수 g_y, 특정 값으로 초기화됨
constexpr int g_z { 2 }; // constexpr 전역 변수 g_z, 특정 값으로 초기화됨

void doSomething()
{
    // 전역 변수는 파일 내 어디서든 보고 사용할 수 있습니다
    std::cout &lt;&lt; g_y &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; g_z &lt;&lt; &#39;\n&#39;;
}

int main()
{
    doSomething();

    // 전역 변수는 파일 내 어디서든 보고 사용할 수 있습니다
    std::cout &lt;&lt; g_y &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; g_z &lt;&lt; &#39;\n&#39;;

    return 0;
}
// 여기서 g_y와 g_z의 범위가 끝납니다</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/9d4eb257-d02f-4825-b6f0-18d0bbb24146/image.png" alt=""></p>
<h2 id="변수-섀도잉">변수 섀도잉</h2>
<ul>
<li>C++에서 각각의 코드 블록 <code>{}</code> 은 자신만의 <strong>유효 범위(Scope)</strong>를 가집니다. </li>
<li>그렇다면 만약 바깥쪽 블록에 있는 변수와 똑같은 이름을 가진 변수를 안쪽(중첩된) 블록에 새로 만들면 어떤 일이 일어날까요?</li>
<li>이런 상황이 발생하면, 두 변수가 모두 유효한 영역에서는 <strong>안쪽에 있는 변수가 바깥쪽에 있는 변수를 &quot;가려버리게&quot;</strong> 됩니다. </li>
<li>우리는 이 현상을 <strong>이름 가리기(Name hiding)</strong> 또는 <strong>섀도잉(Shadowing)</strong>이라고 부릅니다.</li>
</ul>
<hr>
<h2 id="지역-변수-섀도잉-shadowing-of-local-variables">지역 변수 섀도잉 (Shadowing of local variables)</h2>
<ul>
<li>아래 코드를 보며 함께 이해해 볼까요?</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{ // 바깥쪽 블록 시작
    int apples { 5 }; // 바깥쪽 블록의 apples 변수입니다.

    { // 안쪽(중첩된) 블록 시작
        // 여기서는 아직 안쪽 apples가 정의되지 않았으므로, 바깥쪽 apples를 가리킵니다.
        std::cout &lt;&lt; apples &lt;&lt; &#39;\n&#39;; // 바깥쪽 apples의 값(5)을 출력합니다.

        int apples{ 0 }; // 안쪽 블록의 유효 범위에 새로운 apples를 정의합니다.

        // 이제부터 apples는 안쪽 블록의 apples를 가리킵니다.
        // 바깥쪽 블록의 apples는 일시적으로 가려집니다(hidden).

        apples = 10; // 바깥쪽이 아닌, 안쪽 블록의 apples에 10을 할당합니다.

        std::cout &lt;&lt; apples &lt;&lt; &#39;\n&#39;; // 안쪽 블록의 apples 값을 출력합니다.
    } // 안쪽 블록이 끝나며 안쪽 apples 변수는 소멸(Destroy)됩니다.

    std::cout &lt;&lt; apples &lt;&lt; &#39;\n&#39;; // 다시 바깥쪽 블록의 apples 값을 출력합니다.

    return 0;
} // 바깥쪽 블록이 끝나며 바깥쪽 apples 변수도 소멸됩니다.</code></pre>
<ul>
<li>위 프로그램에서 우리는 먼저 바깥쪽 블록에 <code>apples</code>라는 이름의 변수를 선언했습니다. </li>
<li>이 변수는 안쪽 블록에서도 여전히 보이므로, 처음에는 그 값인 <code>5</code>가 정상적으로 출력됩니다.</li>
</ul>
<ul>
<li>하지만 안쪽 블록에서 이름이 똑같은 또 다른 <code>apples</code> 변수를 선언하면 상황이 달라집니다. </li>
<li>이 선언 시점부터 안쪽 블록이 끝날 때까지 <code>apples</code>라는 이름은 바깥쪽 변수가 아니라 새로 만든 <strong>&#39;안쪽 변수&#39;</strong>를 가리키게 됩니다.</li>
<li>안쪽 블록 안에서 <strong>똑같은 이름으로 변수를 가려버린 상태</strong>라면, 가려진 바깥쪽 지역 변수에 직접 접근할 수 있는 방법은 없습니다.</li>
</ul>
<hr>
<h2 id="전역-변수-섀도잉-shadowing-of-global-variables">전역 변수 섀도잉 (Shadowing of global variables)</h2>
<ul>
<li>안쪽 블록의 변수가 바깥쪽 블록의 변수를 가리는 것과 똑같은 원리로, 전역 변수와 똑같은 이름을 가진 지역 변수를 만들면, 그 지역 변수가 유효한 범위 안에서는 <strong>전역 변수가 가려집니다.</strong></li>
<li>지역 변수 섀도잉과는 다르게, 전역 변수는 <strong>전역 네임스페이스</strong>에 속해 있기 때문에 가려진 상태에서도 접근할 방법이 하나 있습니다. </li>
<li>바로 접두사 없이 <strong>범위 지정 연산자(Scope operator, ::)</strong>를 사용하는 것입니다. </li>
<li>이렇게 하면 컴파일러에게 &quot;내가 말하는 건 지역 변수가 아니라 전역 변수야!&quot;라고 명확히 알려줄 수 있어요.
<img src="https://velog.velcdn.com/images/dj_trace25/post/817e2ac7-9a26-4aca-9aa9-bcbf76e6f7a7/image.png" alt=""></li>
</ul>
<h2 id="내부-링크-internal-linkage">내부 링크 (Internal linkage)</h2>
<ul>
<li>전역 변수와 함수 식별자는 <strong>내부 링크</strong>나 <strong>외부 링크</strong> 중 하나를 가질 수 있습니다.</li>
<li>내부 링크를 가진 식별자는 오직 하나의 단일 번역 단위(보통 하나의 .cpp 소스 파일) 안에서만 볼 수 있고 사용할 수 있으며, 다른 번역 단위에서는 접근할 수 없어요.</li>
<li>즉, 두 개의 서로 다른 소스 파일에 내부 링크를 가진 똑같은 이름의 식별자가 있더라도, 이 둘은 <strong>완전히 독립적인 것으로 취급</strong>된다는 뜻이랍니다.</li>
</ul>
<hr>
<h2 id="내부-링크를-가진-전역-변수-global-variables-with-internal-linkage">내부 링크를 가진 전역 변수 (Global variables with internal linkage)</h2>
<ul>
<li>내부 링크를 가진 전역 변수는 종종 <strong>내부 변수(Internal variables)</strong>라고도 불립니다.</li>
<li>상수가 아닌 전역 변수를 <strong>내부 변수</strong>로 만들려면, <code>static</code> 키워드를 사용하면 됩니다.</li>
</ul>
<ul>
<li><code>const</code>와 <code>constexpr</code> 전역 변수는 기본적으로 내부 링크를 가집니다. </li>
<li>따라서 굳이 <code>static</code> 키워드를 붙일 필요가 없어요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

static int g_x{}; // 상수가 아닌 전역 변수는 기본적으로 외부 링크를 가지지만, static 키워드를 통해 내부 링크를 부여할 수 있습니다.
const int g_y{ 1 }; // const 전역 변수는 기본적으로 내부 링크를 가집니다.
constexpr int g_z{ 2 }; // constexpr 전역 변수는 기본적으로 내부 링크를 가집니다.

int main()
{
    std::cout &lt;&lt; g_x &lt;&lt; &#39; &#39; &lt;&lt; g_y &lt;&lt; &#39; &#39; &lt;&lt; g_z &lt;&lt; &#39;\n&#39;;
    return 0;
}</code></pre>
<hr>
<h2 id="내부-링크를-가진-함수-functions-with-internal-linkage">내부 링크를 가진 함수 (Functions with internal linkage)</h2>
<ul>
<li>앞서 말씀드렸듯이, 함수 식별자 역시 링크를 가집니다. </li>
<li>함수는 기본적으로 외부 링크를 가지지만, <code>static</code> 키워드를 사용하면 내부 링크를 가지도록 설정할 수 있습니다.</li>
<li>다른 파일에서 함수 전방 선언을 통해 접근하려고 시도하면 실패하게 됩니다.</li>
</ul>
<hr>
<h2 id="왜-굳이-식별자에-내부-링크를-부여할까요">왜 굳이 식별자에 내부 링크를 부여할까요?</h2>
<ul>
<li><p>일반적으로 식별자에게 내부 링크를 부여하는 이유는 크게 두 가지가 있습니다.</p>
<ol>
<li><strong>다른 파일에서 접근하지 못하도록 확실히 차단하고 싶은 식별자가 있을 때.</strong> 함부로 값이 변경되면 안 되는 전역 변수나, 외부에서 몰래 호출되면 안 되는 헬퍼 함수(Helper function)가 좋은 예입니다.</li>
</ol>
</li>
</ul>
<ol start="2">
<li><strong>이름 충돌(Naming collisions)을 철저하게 방지하기 위해서.</strong> 내부 링크를 가진 식별자는 링커(Linker)에 노출되지 않기 때문에, 프로그램 전체가 아니라 오직 같은 번역 단위 안에서만 이름이 충돌할 가능성이 존재합니다.</li>
</ol>
<ul>
<li>많은 최신 개발 가이드에서는 &quot;다른 파일에서 사용할 목적이 아닌 <strong>모든 변수와 함수에는 내부 링크를 부여하라</strong>&quot;고 권장하고 있습니다. </li>
<li>만약 이를 꾸준히 지킬 수 있다면 아주 훌륭한 프로그래밍 습관이 될 거예요.</li>
<li>하지만 당장은 초보자분들을 위해 최소한의 가벼운 접근법을 추천해 드릴게요. </li>
<li>다른 파일에서의 접근을 차단해야 할 명확한 이유가 있는 식별자에게만 내부 링크를 부여해 보세요.
<img src="https://velog.velcdn.com/images/dj_trace25/post/23a3ce37-e3c6-4b81-bcb9-ed39e62ac0fa/image.png" alt=""></li>
</ul>
<h2 id="외부-링크external-linkage">외부 링크(External linkage)</h2>
<ul>
<li>외부 링크를 가진 식별자는 정의된 파일뿐만 아니라, 전방 선언을 통해 다른 코드 파일에서도 보고 사용할 수 있습니다.</li>
</ul>
<hr>
<h2 id="외부-링크를-가진-전역-변수">외부 링크를 가진 전역 변수</h2>
<ul>
<li>외부 링크을 가진 전역 변수는 때때로 <strong>외부 변수(External variables)</strong>라고도 불립니다. </li>
<li>전역 변수를 외부 변수로 만들어서 다른 파일에서도 접근할 수 있게 하려면, <code>extern</code> 키워드를 사용할 수 있어요.</li>
<li>상수가 아닌 전역 변수는 기본적으로 외부 연결을 가지기 때문에, 굳이 <code>extern</code>으로 표시할 필요가 없습니다.<pre><code class="language-cpp">int g_x { 2 }; // 상수가 아닌 전역 변수는 기본적으로 외부 연결을 가집니다 (extern 키워드 불필요)
extern const int g_y { 3 }; // const 전역 변수는 extern으로 정의하여 외부 연결을 갖게 만들 수 있습니다
extern constexpr int g_z { 3 }; // constexpr 전역 변수도 extern으로 정의할 수 있습니다 (하지만 별로 유용하지 않아요. 다음 섹션의 경고를 참고해 주세요)
</code></pre>
</li>
</ul>
<p>int main()
{
    return 0;
}</p>
<pre><code>---
## extern 키워드를 통한 변수 전방 선언
- 다른 파일에 정의된 외부 전역 변수를 실제로 사용하려면, 해당 변수를 사용하려는 모든 파일에 **변수의 전방 선언**을 작성해야 합니다. 
- 변수의 경우, 전방 선언을 만들 때도 `extern` 키워드를 사용해요 (이때 초기화 값은 넣지 않습니다).

```cpp
#include &lt;iostream&gt;

extern int g_x;       // 이 extern은 어딘가 다른 곳에 정의된 g_x라는 변수의 전방 선언입니다.
extern const int g_y; // 이 extern은 어딘가 다른 곳에 정의된 g_y라는 const 변수의 전방 선언입니다.

int main()
{
    std::cout &lt;&lt; g_x &lt;&lt; &#39; &#39; &lt;&lt; g_y &lt;&lt; &#39;\n&#39;; // 2 3을 출력합니다.

    return 0;
}</code></pre><ul>
<li>그리고 이 변수들의 정의는 다음과 같습니다.</li>
</ul>
<pre><code class="language-cpp">// 전역 변수 정의
int g_x { 2 };              // 상수가 아닌 전역 변수는 기본적으로 외부 연결을 가집니다.
extern const int g_y { 3 }; // 이 extern 키워드는 g_y에게 외부 연결을 부여합니다.</code></pre>
<blockquote>
<p>참고로 <strong>함수 전방 선언</strong>에는 <code>extern</code> 키워드가 필요하지 않습니다. 컴파일러는 <strong>함수 본문(Body)이 제공되는지 여부에 따라</strong> 새로운 함수를 정의하는 것인지, 아니면 전방 선언을 하는 것인지 쉽게 구별할 수 있거든요.
ㅤ
반면, <strong>변수 전방 선언</strong>은 <code>extern</code> 키워드가 반드시 필요합니다. 왜냐하면 초기화되지 않은 변수 정의와 변수 전방 선언이 겉보기에는 똑같이 생겼기 때문에 이를 구별해 주어야 하거든요.</p>
</blockquote>
<pre><code class="language-cpp">// 상수 아님 (Non-constant)
int g_x;        // 변수 정의 (초기화 없음)
int g_x { 1 };  // 변수 정의 (초기화 있음)
extern int g_x; // 전방 선언 (초기화 없음)

// 상수 (Constant)
extern const int g_y { 1 }; // 변수 정의 (const는 반드시 초기화가 필요합니다)
extern const int g_y;       // 전방 선언 (초기화 없음)</code></pre>
<p><img src="https://velog.velcdn.com/images/dj_trace25/post/bedb48a7-ff10-4908-a33b-c56bf4f406a2/image.png" alt=""></p>
<h2 id="인라인-함수와-변수-inline-functions-and-variables">인라인 함수와 변수 (Inline functions and variables)</h2>
<ul>
<li>사용자로부터 입력을 받거나, 파일에 무언가를 출력하거나, 특정 값을 계산하는 등 개별적인 작업을 수행하는 코드를 작성해야 하는 상황을 생각해 볼게요. 이런 코드를 구현할 때, 우리에게는 기본적으로 두 가지 선택지가 있습니다.</li>
</ul>
<blockquote>
<ol>
<li>기존 함수 안에 그 코드를 직접 작성하는 방법 (이를 <strong>&#39;제자리(in-place)&#39;</strong> 또는 <strong>&#39;인라인(inline)&#39;</strong>으로 코드를 작성한다고 해요).</li>
<li>해당 작업을 처리할 새로운 함수(필요하다면 하위 함수들까지)를 만드는 방법.</li>
</ol>
</blockquote>
<ul>
<li>새로운 함수를 만들어서 코드를 분리하면 여러 가지 잠재적인 장점이 생겨요. 함수를 작게 만들면 다음과 같은 이점이 있거든요.</li>
</ul>
<blockquote>
<ol>
<li>전체 프로그램의 맥락에서 코드를 읽고 이해하기가 훨씬 쉬워집니다.</li>
<li>함수는 본질적으로 모듈화되어 있기 때문에 재사용하기가 쉽습니다.</li>
<li>코드를 한 곳에서만 수정하면 되므로 업데이트하기가 편해집니다.</li>
</ol>
</blockquote>
<ul>
<li>하지만, 새로운 함수를 사용하는 것에도 단점은 있어요. 함수가 호출될 때마다 어느 정도의 성능 저하가 발생한다는 점이죠.</li>
<li>이렇게 어떤 작업(이 경우에는 함수 호출)을 설정하고, 실행을 돕고, 끝난 뒤 정리하기 위해 추가로 발생하는 모든 작업과 비용을 <strong>오버헤드(Overhead)</strong> 라고 부릅니다.</li>
<li>크기가 아주 작은 함수들의 경우, 함수 내부의 코드를 실행하는 시간보다 오버헤드로 인한 비용이 더 클 수도 있습니다. </li>
<li>작은 함수가 아주 빈번하게 호출되는 상황이라면, <strong>함수를 따로 만드는 것이 오히려 같은 코드를 제자리에 직접 쓰는 것보다</strong> 눈에 띄는 성능 저하를 가져올 수 있답니다.</li>
</ul>
<hr>
<h2 id="인라인-확장-inline-expansion">인라인 확장 (Inline expansion)</h2>
<ul>
<li>다행히도 <strong>C++ <code>컴파일러</code></strong>에는 이런 오버헤드 비용을 피할 수 있는 방법이 있어요. </li>
<li>바로 <strong>인라인 확장</strong> 이라는 과정입니다. </li>
<li>이는 함수 호출 부분을 <strong>호출된 함수의 실제 내부 코드로 통째로 바꿔치기</strong>하는 것을 말해요.</li>
</ul>
<hr>
<h2 id="역사-속의-인라인-키워드">역사 속의 인라인 키워드</h2>
<ul>
<li>과거에는 컴파일러들이 인라인 확장이 이득이 될지 스스로 판단할 능력이 없거나, 있더라도 성능이 썩 좋지 않았습니다. </li>
<li>이 때문에 C++는 <code>inline</code>이라는 키워드를 제공했어요. </li>
<li>이 키워드의 원래 목적은 개발자가 컴파일러에게 <strong>&quot;이 함수는 인라인으로 확장하는 게 (아마도) 성능에 좋을 거야&quot;</strong>라고 힌트를 주는 용도였답니다. <code>inline</code> 키워드를 사용해 선언된 함수를 <strong>인라인 함수(Inline function)</strong> 라고 부릅니다. </li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

inline int min(int x, int y) // inline 키워드는 이 함수가 인라인 함수임을 의미해요
{
    return (x &lt; y) ? x : y;
}

int main()
{
    std::cout &lt;&lt; min(5, 6) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; min(3, 2) &lt;&lt; &#39;\n&#39;;
    return 0;
}</code></pre>
<ul>
<li>하지만 현대의 C++에서는 더 이상 함수를 인라인 확장해 달라고 요청하기 위해 <code>inline</code> 키워드를 <strong>사용하지 않습니다.</strong></li>
</ul>
<hr>
<h2 id="현대적인-의미의-인라인-키워드-the-inline-keyword-modernly">현대적인 의미의 인라인 키워드 (The inline keyword, modernly)</h2>
<ul>
<li>현대의 C++에서 <code>inline</code>이라는 용어는 진화하여 <strong>&quot;이 함수는 여러 번 정의해도 허용됩니다&quot;</strong> 라는 뜻을 가지게 되었습니다. </li>
<li>즉, 인라인 함수는 (ODR을 위반하지 않으면서)** 여러 번역 단위에 정의될 수 있도록 특별히 허락받은 함수**예요.</li>
<li>인라인 함수가 지켜야 할 주요 요구 사항은 두 가지입니다.</li>
</ul>
<ol>
<li><p>컴파일러는 인라인 함수가 사용되는 각 번역 단위마다 그 함수의 <strong>&#39;전체 정의&#39;</strong>를 볼 수 있어야 합니다. 
단, <strong>한 번역 단위 내에서는 하나의 정의만 존재</strong>해야 합니다. 그렇지 않으면 컴파일 오류가 납니다.</p>
</li>
<li><p>(기본적으로 함수가 가지는) 외부 연결성을 가진 인라인 함수에 대한 <strong>모든 정의는 완전히 동일</strong>해야 합니다. 
조금이라도 다르면 정의되지 않은 동작이 발생해요.</p>
</li>
</ol>
<ul>
<li>링커는 각 파일에 퍼져 있는 인라인 함수의 정의들을 모아서 <strong>단 하나의 정의로</strong> 합쳐줍니다. </li>
<li>그래서 결과적으로 단일 정의 규칙을 만족시키게 되죠.</li>
</ul>
<pre><code class="language-cpp">//main.cpp
#include &lt;iostream&gt;

double circumference(double radius); // 전방 선언(Forward declaration)

inline double pi() { return 3.14159; }

int main()
{
    std::cout &lt;&lt; pi() &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; circumference(2.0) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<pre><code class="language-cpp">//math.cpp
inline double pi() { return 3.14159; }

double circumference(double radius)
{
    return 2.0 * pi() * radius;
}</code></pre>
<ul>
<li>두 파일 모두에 <code>pi()</code> 함수가 정의되어 있는 것을 눈여겨보세요. </li>
<li>하지만 이 함수는 <code>inline</code>으로 표시되어 있기 때문에 문제가 되지 않고, <strong>링커가 알아서 중복을 제거</strong>해 줍니다. </li>
<li>만약 두 곳의 <code>pi()</code> 정의에서 <code>inline</code> 키워드를 지워버린다면, 인라인이 아닌 함수를 중복 정의한 것이 되므로 <strong>ODR 위반 에러가 발생</strong>할 거예요.</li>
</ul>
<ul>
<li>인라인 함수는 일반적으로 <strong>헤더 파일</strong>에 정의합니다.</li>
<li>그래야 함수의 전체 정의를 알아야 하는 모든 코드 파일의 상단에 쉽게 <code>#include</code> 할 수 있으니까요. </li>
<li>이 방식을 쓰면 인라인 함수의 모든 정의가 완벽하게 똑같다는 보장도 할 수 있습니다.</li>
<li>C++17부터는 <strong>인라인 변수(Inline variables)</strong> 라는 개념이 도입되었습니다. </li>
<li>인라인 함수처럼 여러 파일에서 여러 번 정의할 수 있도록 허락된 변수들이죠. </li>
<li>인라인 변수도 인라인 함수와 똑같은 요구 사항을 갖습니다
<img src="https://velog.velcdn.com/images/dj_trace25/post/4c89ecc8-508b-463a-a10d-b45c0ed86e25/image.png" alt=""></li>
</ul>
<h2 id="여러-파일에서-전역-상수-공유하기-인라인-변수-사용">여러 파일에서 전역 상수 공유하기 (인라인 변수 사용)</h2>
<ul>
<li>프로그래밍을 하다 보면, 어떤 기호 상수들은 단 한 곳이 아니라 코드 전체에서 두루 사용되어야 할 때가 있습니다. </li>
<li>변하지 않는 물리나 수학 상수 <code>원주율 파이(pi)</code> <code>아보가드로 수</code> 가 될 수도 있고, 특정 프로그램에 맞춰진 &quot;조정용&quot; 값 <code>마찰 계수</code> <code>중력 계수</code> 이 될 수도 있죠.</li>
</ul>
<ul>
<li>이러한 상수가 필요할 때마다 매번 새로운 파일에 다시 정의하는 것은 중요한 프로그래밍 원칙인 <strong>DRY</strong>를 위반하는 것입니다. </li>
<li>대신, <strong>중앙의 한 곳에 한 번만 선언하고 필요한 곳 어디서든 가져다 쓰는 것</strong>이 훨씬 좋은 방법입니다.</li>
</ul>
<hr>
<h2 id="인라인-변수로-전역-상수-사용하기-c17-이상">인라인 변수로 전역 상수 사용하기 (C++17 이상)</h2>
<ul>
<li>인라인 변수는 모든 정의가 동일하기만 하다면 <strong>여러 번 정의되는 것을 허용</strong>하는 특별한 변수입니다.</li>
<li>우리의 <code>constexpr</code> 변수들을 <code>inline</code>으로 만들면, 헤더 파일에 한 번 정의해 두고 필요한 모든 <code>.cpp</code> 파일에 자유롭게 <code>#include</code> 할 수 있습니다. 이 방법을 사용하면 ODR 위반도 피하고, 변수가 불필요하게 중복 복사되는 단점도 해결할 수 있습니다!
<img src="https://velog.velcdn.com/images/dj_trace25/post/02ef7af9-c1a3-4d07-bc71-506492004ef3/image.png" alt=""></li>
</ul>
<h2 id="정적-지역-변수-static-local-variables">정적 지역 변수 (Static local variables)</h2>
<ul>
<li>지역 변수는 기본적으로 <strong>자동 지속 기간</strong>을 가진다고 배웠습니다. </li>
<li>변수가 <strong>정의되는 시점에 생성</strong>되고, 그 변수가 속한 블록을 벗어나면 <strong>자동으로 소멸</strong>된다는 의미예요.</li>
</ul>
<ul>
<li>하지만 지역 변수에 <code>static</code> 키워드를 사용하면, 그 지속 기간이 자동에서 <strong>정적(Static)</strong>으로 바뀝니다. </li>
<li>이렇게 되면 <strong>지역 변수는 마치 전역 변수처럼</strong> 프로그램이 시작될 때 생성되고, 프로그램이 끝날 때 소멸하게 돼요. </li>
<li>그 결과, 이 정적 변수는 <strong>자신의 범위를 벗어나더라도 예전 값을 잃어버리지 않고 계속 기억</strong>하게 됩니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

void incrementAndPrint()
{
    static int s_value{ 1 }; // static 키워드를 통해 정적 지속 기간을 가집니다. 이 초기화 코드는 단 한 번만 실행됩니다.
    ++s_value;
    std::cout &lt;&lt; s_value &lt;&lt; &#39;\n&#39;;
} // s_value는 여기서 소멸되지 않지만, 범위를 벗어나므로 접근할 수는 없게 됩니다.

int main()
{
    incrementAndPrint();
    incrementAndPrint();
    incrementAndPrint();

    return 0;
}</code></pre>
<ul>
<li>이 코드에서 <code>s_value</code>는 <code>static</code>으로 선언되었기 때문에 프로그램이 <strong>시작될 때 한 번만</strong> 생성됩니다.<ul>
<li><strong>상수 표현식</strong> 초기화 값을 가진 정적 지역 변수는 <strong>프로그램 시작 시에 초기화</strong>될 수 있습니다.</li>
<li><strong>초기화 값이 없거나 상수 표현식이 아닌 초기화 값</strong>을 가진 정적 지역 변수는 프로그램 시작 시 <strong>영 초기화</strong>됩니다.</li>
<li>이후 해당 변수의 정의를 처음 마주칠 때 다시 초기화되죠.</li>
<li>이후에 함수가 호출될 때는 이 정의 부분을 건너뛰기 때문에 더 이상 초기화가 발생하지 않습니다.</li>
<li>정적 지속 기간을 가지므로, 명시적으로 초기화하지 않으면 기본적으로 <code>0</code>으로 초기화된답니다.</li>
</ul>
</li>
<li>여기서 <code>s_value</code>는 <code>1</code>이라는 <strong>상수 표현식</strong> 초기화 값을 가지기 때문에 프로그램 시작 시점에 초기화됩니다.</li>
</ul>
<blockquote>
<p><strong>핵심 통찰 (Key insight)</strong></p>
</blockquote>
<ul>
<li>정적 지역 변수는** 여러 번의 함수 호출 사이에서도 지역 변수의 값을 기억해야 할 때** 사용합니다.</li>
<li>정적 지역 변수는 꼭 초기화해 주세요. </li>
<li>정적 지역 변수는 전체 프로그램 동안 한 번만 초기화되며, 이후 함수 호출에서는 초기화되지 않고 기존 값을 유지합니다.</li>
</ul>
<hr>
<h2 id="정적-지역-상수-static-local-constants">정적 지역 상수 (Static local constants)</h2>
<ul>
<li>정적 지역 변수도 <code>const</code> <code>constexpr</code>로 선언할 수 있습니다.</li>
<li><code>const</code> 정적 지역 변수가 유용하게 쓰이는 대표적인 경우는, 함수에서 어떤 상수 값을 써야 하는데 <strong>그 객체를 생성하거나 초기화하는 비용이 매우 클 때</strong>입니다.</li>
</ul>
<ul>
<li>일반 지역 변수를 사용했다면 <strong>함수가 실행될 때마다 변수를 만들고 초기화</strong>해야 했을 것입니다.</li>
<li>하지만 <code>const/constexpr</code> 정적 지역 변수를 사용하면, 비용이 많이 드는 객체를 딱 한 번만 생성해서 초기화한 후, 함수가 호출될 때마다 계속 재사용할 수 있습니다.
<img src="https://velog.velcdn.com/images/dj_trace25/post/d765fd2c-b986-4bbe-9728-c4086c08cc6a/image.png" alt=""></li>
</ul>
<h2 id="using-선언-using-declarations">using 선언 (Using-declarations)</h2>
<ul>
<li><code>std::</code>를 반복해서 타이핑하는 수고를 덜어주는 첫 번째 방법은 <strong>using 선언문</strong>을 활용하는 것입니다.</li>
<li><strong>using 선언</strong>은 우리가 <strong>스코프가 없는 비한정 이름을 마치 한정된 이름의 별칭처럼</strong> 사용할 수 있게 해줍니다.</li>
<li>using 선언은 선언된 지점부터 해당 선언이 포함된 스코프가 끝날 때까지 활성화됩니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
   using std::cout; // 이 using 선언은 컴파일러에게 cout이 std::cout을 의미한다고 알려줍니다.
   cout &lt;&lt; &quot;Hello world!\n&quot;; // 따라서 여기서는 std:: 접두사가 필요하지 않아요!

   return 0;
} // 이 using 선언은 현재 스코프가 끝나는 시점에서 효력이 만료됩니다.</code></pre>
<hr>
<h2 id="using-지시자-using-directives">using 지시자 (Using-directives)</h2>
<ul>
<li><strong>using 지시자</strong>는 특정 네임스페이스 안에 있는 <strong>모든 식별자</strong>를 using 지시자가 있는 스코프 내에서 한정자 없이 사용할 수 있게 해줍니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;iostream&gt;

int main()
{
   using namespace std; // 이제 std 네임스페이스의 모든 이름을 한정자 없이 접근할 수 있습니다.
   cout &lt;&lt; &quot;Hello world!\n&quot;; // 따라서 여기서는 std:: 접두사가 필요하지 않아요.

   return 0;
} // 이 using 지시자는 현재 스코프가 끝나는 시점에서 만료됩니다.</code></pre>
<ul>
<li><code>using namespace std;</code> 라는 지시자는 컴파일러에게 std 네임스페이스 안의 모든 이름들을 현재 스코프(이 경우 <code>main()</code> 함수 내부)에서 한정자 없이 접근할 수 있도록 하라고 지시합니다.</li>
</ul>
<hr>
<h2 id="using-문의-스코프-the-scope-of-using-statements">using 문의 스코프 (The scope of using-statements)</h2>
<ul>
<li>만약 <code>using 선언</code>이나 <code>using 지시자</code>가 어떤 블록 <code>{}</code> 안에서 사용되었다면, 그 이름들은 오<strong>직 해당 블록 안에서만 유효</strong>합니다</li>
<li>반면, <strong>네임스페이스 안에서(전역 네임스페이스 포함)</strong> 사용되었다면, 그 이름들은 해당 파일의 나머지 <strong>전체 부분에 적용됩니다</strong> (파일 스코프를 가집니다).</li>
</ul>
<hr>
<h2 id="헤더-파일이나-include-지시자-이전에-using-문을-사용하지-마세요">헤더 파일이나 #include 지시자 이전에 using 문을 사용하지 마세요!</h2>
<ul>
<li>헤더 파일 안에서나 <code>#include</code> 지시자 앞에서는 <strong>절대로 using 문을 사용해서는 안 됩니다.</strong></li>
<li>만약 여러분이 헤더 파일의 전역 네임스페이스에 using 문을 넣는다면, 그 헤더를 <code>#include</code> 하는 모든 다른 파일들도 강제로 그 using 문을 갖게 됩니다.</li>
<li>헤더 파일 내부의 네임스페이스 안에 넣는 것도 정확히 같은 이유로 피해야 합니다.
<img src="https://velog.velcdn.com/images/dj_trace25/post/94bd3869-1f7f-413d-b172-c94b179eb5b6/image.png" alt=""></li>
</ul>
<h2 id="이름-없는-네임스페이스">이름 없는 네임스페이스</h2>
<ul>
<li>이름 없는 네임스페이스에 선언된 모든 내용은 마치 <strong>부모 네임스페이스의 일부인 것처럼 취급</strong>됩니다.</li>
<li>부모 네임스페이스에 <strong>전역 네임스페이스가 포함</strong>됩니다.</li>
<li>네임스페이스의 진짜 중요한 효과는, 그 안의 모든 식별자가 <strong>내부 연결</strong>을 가진 것처럼 취급된다는 점입니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[LearnCPP - O]]></title>
            <link>https://velog.io/@dj_trace25/LearnCPP-O</link>
            <guid>https://velog.io/@dj_trace25/LearnCPP-O</guid>
            <pubDate>Sat, 14 Feb 2026 11:45:31 GMT</pubDate>
            <description><![CDATA[<h2 id="stdbitset을-활용한-비트-조작bit-manipulation">std::bitset을 활용한 비트 조작(Bit manipulation)</h2>
<ul>
<li>불리언 타입은 오직 <code>참</code>이나 <code>거짓</code>, 이 두 가지 상태만 가지잖아요? 이 상태를 저장하는 데는 단 1비트면 충분하거든요.</li>
<li>그런데 변수가 <strong>최소</strong> 1바이트(8비트) 크기를 가져야 한다면, 불리언 값 하나를 저장하기 위해 1비트만 쓰고 나머지 7비트는 사용하지 않고 버려둔다는 뜻이 됩니다.</li>
</ul>
<ul>
<li>대부분의 상황에서는 크게 신경 쓰지 않아도 괜찮아요.</li>
<li>버려지는 7비트를 아끼는 것보다 코드를 이해하고 유지 보수하기 쉽게 만드는 게 더 중요하니까요.</li>
<li>하지만 저장 공간이 매우 중요한 일부 상황에서는, 8개의 개별 불리언 값을 하나의 바이트 안에 꽉꽉 눌러 담아 저장 효율을 높이는 것이 아주 유용할 수 있어요.</li>
</ul>
<ul>
<li>이렇게 하려면 객체를 비트 수준에서 다룰 수 있어야 하는데요.</li>
<li>다행히 C++는 우리에게 딱 맞는 도구들을 제공해 준답니다!</li>
<li>이렇게 객체 내의 개별 비트를 수정하는 것을 <strong>비트 조작(Bit manipulation)</strong>이라고 불러요.</li>
</ul>
<ul>
<li><strong>비트 조작</strong>은 <code>그래픽</code> <code>암호화</code> <code>데이터 압축</code> <code>최적화</code> 같은 특정 프로그래밍 분야에서 아주 많이 쓰이지만, 일반적인 프로그래밍에서는 자주 쓰이지는 않아요.</li>
<li>그래서 이 챕터 전체는 선택 사항이랍니다. </li>
<li>가볍게 훑어보시거나 건너뛴 다음, 나중에 필요할 때 다시 돌아와서 읽으셔도 전혀 문제없어요!</li>
</ul>
<hr>
<h2 id="비트-플래그bit-flags">비트 플래그(Bit flags)</h2>
<ul>
<li>지금까지 우리는 변수 <strong>하나에 하나의 값</strong>만 저장해 왔어요.<pre><code class="language-cpp">int foo { 5 };</code></pre>
</li>
<li>하지만 객체가 하나의 값만 가진다고 생각하는 대신, <strong>객체 안의 각 비트 하나하나를 독립적인 불리언 값으로 취급</strong>할 수도 있어요.</li>
<li>이렇게 개별 비트들이 <strong>불리언 값처럼 사용</strong>될 때, 이 비트들을 <strong>비트 플래그(Bit flags)</strong>라고 부릅니다.</li>
<li>비트 플래그들의 집합을 정의하기 위해, 우리는 보통 필요한 플래그 개수에 맞는 적절한 크기의 <code>부호 없는 정수</code>나 <code>std::bitset</code> 을 사용한답니다.</li>
</ul>
<hr>
<h2 id="비트-번호-매기기와-비트-위치bit-positions">비트 번호 매기기와 비트 위치(Bit positions)</h2>
<ul>
<li>일련의 비트들이 있을 때, <strong>오른쪽에서 왼쪽으로</strong> 번호를 매깁니다.</li>
<li>이때 <code>1</code>이 아니라 <code>0</code>부터 시작합니다.</li>
<li>각 숫자는 <strong>비트 위치</strong>를 나타내요.</li>
</ul>
<pre><code class="language-cpp">76543210 비트 위치 (Bit position)
00000101 비트 시퀀스 (Bit sequence)</code></pre>
<ul>
<li>위의 <code>0000 0101</code>이라는 비트 시퀀스를 보면, <code>위치 0</code>과 <code>위치 2</code>에 있는 비트가 <code>1</code>의 값을 가지고 있고, 나머지 비트들은 모두 0의 값을 가지고 있음을 알 수 있어요.</li>
</ul>
<hr>
<h2 id="stdbitset으로-비트-조작하기">std::bitset으로 비트 조작하기</h2>
<ul>
<li>std::bitset은 비트 조작에 매우 유용한 <strong>4가</strong>지 핵심 <strong>멤버 함수</strong>를 제공해요.</li>
</ul>
<ul>
<li><code>test()</code> 특정 비트가 0인지 1인지 알아볼 때 사용해요.</li>
<li><code>set()</code> 특정 비트를 켜고 싶을(1로 만들고 싶을) 때 사용해요. (이미 켜져 있다면 아무 일도 일어나지 않아요.)</li>
<li><code>reset()</code> 특정 비트를 끄고 싶을(0으로 만들고 싶을) 때 사용해요. (이미 꺼져 있다면 아무 일도 일어나지 않아요.)</li>
<li><code>flip()</code> 비트 값을 0에서 1로, 또는 1에서 0으로 반대로 뒤집고 싶을 때 사용해요.</li>
</ul>
<ul>
<li>이 함수들은 모두 우리가 조작하고 싶은 <strong>비트의 위치</strong> 하나를 유일한 인수로 받습니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;bitset&gt;
#include &lt;iostream&gt;

int main() {
    std::bitset&lt;8&gt; bits{ 0b0000&#39;0101 }; // 8비트가 필요하고, 초기 비트 패턴은 0000 0101로 시작해요.
    bits.set(3);   // 위치 3의 비트를 1로 설정해요 (이제 0000 1101이 됩니다)
    bits.flip(4);  // 위치 4의 비트를 뒤집어요 (이제 0001 1101이 됩니다)
    bits.reset(4); // 위치 4의 비트를 다시 0으로 꺼요 (이제 0000 1101이 됩니다)

    std::cout &lt;&lt; &quot;모든 비트: &quot; &lt;&lt; bits &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;비트 3의 값: &quot; &lt;&lt; bits.test(3) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;비트 4의 값: &quot; &lt;&lt; bits.test(4) &lt;&lt; &#39;\n&#39;;

    return 0;
}</code></pre>
<ul>
<li>이 코드를 실행하면 이렇게 출력됩니다.</li>
</ul>
<pre><code class="language-cpp">모든 비트: 00001101
비트 3의 값: 1
비트 4의 값: 0</code></pre>
<ul>
<li>비트들에게 의미 있는 이름을 붙여주면 코드를 훨씬 더 읽기 쉽게 만들 수 있어요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;bitset&gt;
#include &lt;iostream&gt;

int main(){
    [[maybe_unused]] constexpr int  isHungry   { 0 };
    [[maybe_unused]] constexpr int  isSad      { 1 };
    [[maybe_unused]] constexpr int  isMad      { 2 };
    [[maybe_unused]] constexpr int  isHappy    { 3 };
    [[maybe_unused]] constexpr int  isLaughing { 4 };
    [[maybe_unused]] constexpr int  isAsleep   { 5 };
    [[maybe_unused]] constexpr int  isDead     { 6 };
    [[maybe_unused]] constexpr int  isCrying   { 7 };

    std::bitset&lt;8&gt; me{ 0b0000&#39;0101 }; // 8비트가 필요하고, 초기 비트 패턴은 0000 0101이에요.
    me.set(isHappy);      // 위치 3(isHappy)의 비트를 1로 설정해요 (이제 0000 1101이 됩니다)
    me.flip(isLaughing);  // 위치 4(isLaughing)의 비트를 뒤집어요 (이제 0001 1101이 됩니다)
    me.reset(isLaughing); // 위치 4(isLaughing)의 비트를 다시 0으로 꺼요 (이제 0000 1101이 됩니다)

    std::cout &lt;&lt; &quot;모든 비트: &quot; &lt;&lt; me &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;나는 행복한가?: &quot; &lt;&lt; me.test(isHappy) &lt;&lt; &#39;\n&#39;;
    std::cout &lt;&lt; &quot;나는 웃고 있는가?: &quot; &lt;&lt; me.test(isLaughing) &lt;&lt; &#39;\n&#39;;
    return 0;
}</code></pre>
<hr>
<h2 id="한-번에-여러-비트를-가져오거나-설정하고-싶다면요">한 번에 여러 비트를 가져오거나 설정하고 싶다면요?</h2>
<ul>
<li>안타깝게도 <code>std::bitset</code>으로는 한 번에 여러 비트를 조작하는 게 조금 번거로워요.</li>
<li>이런 작업을 하고 싶거나 <code>std::bitset</code> 대신 <strong><code>부호 없는 정수</code></strong>를 직접 <strong>비트 플래그</strong>로 사용하고 싶다면, 조금 더 전통적인 방식을 사용해야 합니다.</li>
</ul>
<hr>
<h2 id="stdbitset의-크기에-대한-비밀">std::bitset의 크기에 대한 비밀</h2>
<ul>
<li><code>std::bitset</code>은 메모리를 아끼기보다는 <strong>속도를 최적화하는데 초점</strong>이 맞춰져 있답니다.</li>
<li><code>std::bitset</code>의 크기는 비트들을 담는 데 필요한 바이트 수를 <code>sizeof(size_t)</code> 단위로 올림하여 결정돼요.</li>
<li><code>size_t</code>는 32비트 컴퓨터에서는 4바이트, 64비트 컴퓨터에서는 8바이트예요.</li>
</ul>
<ul>
<li>따라서 <code>std::bitset&lt;8&gt;</code>은 기술적으로는 8개의 비트를 저장하기 위해 단 1바이트만 있으면 되지만, 실제로는 시스템에 따라 4바이트나 8바이트의 메모리를 차지하게 됩니다.</li>
<li>즉, <code>std::bitset</code>은 메모리 절약이 목적일 때보다는 <strong>편리함이 필요할 때 사용</strong>하는 것이 가장 좋습니다.</li>
</ul>
<hr>
<h2 id="stdbitset에-질문-던지기">std::bitset에 질문 던지기</h2>
<ul>
<li>앞서 배운 4가지 외에도 자주 쓰이는 유용한 멤버 함수들이 몇 가지 더 있어요.</li>
</ul>
<ul>
<li><code>size()</code> 비트셋 안에 총 몇 개의 비트가 있는지 개수를 알려줘요.</li>
<li><code>count()</code> <code>1</code>으로 설정된 비트가 몇 개인지 세어줘요.</li>
<li><code>all()</code> 모든 비트가 참인지 확인해서 불리언 값으로 알려줘요.</li>
<li><code>any()</code> 참인 비트가 하나라도 있는지 확인해서 불리언 값으로 알려줘요.</li>
<li><code>none()</code> 참인 비트가 단 하나도 없는지 확인해서 불리언 값으로 알려줘요.</li>
</ul>
<hr>
<h2 id="비트-단위-연산자란">비트 단위 연산자란?</h2>
<ul>
<li>C++은 6가지의 비트 조작 연산자를 제공하는데, 이를 흔히 <strong>비트 단위 연산자(bitwise operators)</strong>라고 불러요.</li>
<li>비트 연산자는 <code>부호 없는 정수</code> 또는 <code>std::bitset</code>과 함께 사용하세요.</li>
</ul>
<table>
<thead>
<tr>
<th>연산자</th>
<th>기호</th>
<th>형식</th>
<th>연산 결과 설명</th>
</tr>
</thead>
<tbody><tr>
<td>왼쪽 시프트 (Left shift)</td>
<td>&lt;&lt;</td>
<td>x &lt;&lt; n</td>
<td>x의 비트들을 왼쪽으로 n칸 이동. 새로 생기는 빈칸은 0으로 채움.</td>
</tr>
<tr>
<td>오른쪽 시프트 (Right shift)</td>
<td>&gt;&gt;</td>
<td>x &gt;&gt; n</td>
<td>x의 비트들을 오른쪽으로 n칸 이동. 새로 생기는 빈칸은 0으로 채움.</td>
</tr>
<tr>
<td>비트 부정 (Bitwise NOT)</td>
<td>~</td>
<td>~x</td>
<td>x의 모든 비트를 뒤집음 (0→1, 1→0).</td>
</tr>
<tr>
<td>비트 AND (Bitwise AND)</td>
<td>&amp;</td>
<td>x &amp; y</td>
<td>x와 y의 대응 비트가 둘 다 1일 때만 1.</td>
</tr>
<tr>
<td>비트 OR (Bitwise OR)</td>
<td>|</td>
<td>x | y</td>
<td>x와 y의 대응 비트 중 하나라도 1이면 1.</td>
</tr>
<tr>
<td>비트 XOR (Bitwise XOR)</td>
<td>^</td>
<td>x ^ y</td>
<td>x와 y의 대응 비트가 서로 다를 때 1.</td>
</tr>
</tbody></table>
<hr>
<h2 id="비트-왼쪽-시프트와-오른쪽-시프트-연산자">비트 왼쪽 시프트(&lt;&lt;)와 오른쪽 시프트(&gt;&gt;) 연산자</h2>
<ul>
<li>비트 왼쪽 시프트 <code>&lt;&lt;</code> 연산자는 비트들을 왼쪽으로 이동시켜요.</li>
<li>예를 들어 <code>x &lt;&lt; 2</code>라고 쓰면, <strong>&quot;x의 비트들을 왼쪽으로 2칸 이동시킨 값을 만들어라&quot;</strong>라는 뜻이죠.</li>
<li>이때 원래 변수의 값은 건드리지 않고, 비트들을 왼쪽으로 밀어낼 때 오른쪽에 생기는 빈자리에는 무조건 <code>0</code>을 채워 넣습니다.</li>
</ul>
<ul>
<li><code>0011</code>이라는 비트를 왼쪽으로 이동시키는 예시를 볼까요?</li>
</ul>
<pre><code class="language-cpp">0011 &lt;&lt; 1 은 0110
0011 &lt;&lt; 2 은 1100
0011 &lt;&lt; 3 은 1000</code></pre>
<ul>
<li>세 번째 경우를 잘 보세요.</li>
<li>끝에 있던 1이 밖으로 밀려났죠? 비트 배열의 끝을 넘어간 비트들은 영원히 사라집니다.</li>
<li>비트 오른쪽 시프트 <code>&gt;&gt;</code> 연산자도 비슷하게 작동합니다. 방향이 오른쪽이라는 점만 달라요.</li>
</ul>
<hr>
<h2 id="비트-부정-bitwise-not">비트 부정 (Bitwise NOT)</h2>
<ul>
<li>비트 부정 연산자는 개념적으로 아주 간단해요.</li>
<li>그냥 모든 비트를 <code>0</code>에서 <code>1</code>로, 혹은 그 반대로 뒤집는 거예요.</li>
</ul>
<pre><code class="language-cpp">~0011 은 1100
~0000 0100 은 1111 1011</code></pre>
<hr>
<h2 id="비트-or-bitwise-or">비트 OR (Bitwise OR)</h2>
<ul>
<li>비트 OR은 논리 OR 연산자와 비슷하게 작동해요.</li>
<li>논리 OR는 둘 중 하나라도 참(true)이면 결과가 참이 되었죠?</li>
<li>비트 OR는 전체가 아니라 각 비트 자리마다 OR 연산을 수행해요.</li>
</ul>
<ul>
<li><code>0b0101 | 0b0110</code> 이라는 식을 예로 들어볼게요.</li>
</ul>
<pre><code class="language-cpp">0 1 0 1 OR
0 1 1 0
-------
0 1 1 1</code></pre>
<ul>
<li>결과는 이진수 <code>0111</code>이 됩니다.</li>
</ul>
<pre><code class="language-cpp">std::cout &lt;&lt; (std::bitset&lt;4&gt;{ 0b0101 } | std::bitset&lt;4&gt;{ 0b0110 }) &lt;&lt; &#39;\n&#39;;

출력 결과: 0111</code></pre>
<ul>
<li>여러 개를 한꺼번에 연산 할 때도 마찬가지예요. <code>(0b0111 | 0b0011 | 0b0001)</code></li>
<li>각 열에 1이 하나라도 있으면 그 열의 결과는 1이 됩니다.</li>
</ul>
<pre><code class="language-cpp">0 1 1 1 OR
0 0 1 1 OR
0 0 0 1
--------
0 1 1 1</code></pre>
<hr>
<h2 id="비트-and-bitwise-and">비트 AND (Bitwise AND)</h2>
<ul>
<li>비트 AND도 비슷하지만, OR 대신 AND 논리를 사용해요.</li>
<li>즉, 두 비트가 모두 1일 때만 결과가 1이 되고, 아니면 0이 됩니다.</li>
</ul>
<ul>
<li><code>0b0101 &amp; 0b0110</code>을 계산해 볼까요?</li>
</ul>
<pre><code class="language-cpp">0 1 0 1 AND
0 1 1 0
--------
0 1 0 0</code></pre>
<ul>
<li>두 번째 비트만 둘 다 <code>1</code>이라서 결과가 <code>1</code>이 되었네요.</li>
<li>여러 개를 연산할 때는, 해당 열의 비트가 모두 <code>1</code>이어야만 결과가 <code>1</code>이 됩니다. <code>0b0001 &amp; 0b0011 &amp; 0b0111</code></li>
</ul>
<pre><code class="language-cpp">0 0 0 1 AND
0 0 1 1 AND
0 1 1 1
--------
0 0 0 1</code></pre>
<hr>
<h2 id="비트-xor-bitwise-xor">비트 XOR (Bitwise XOR)</h2>
<ul>
<li>마지막은 비트 XOR또는 <strong>배타적 논리합</strong>이라고 부르는 연산자예요.</li>
<li>이 친구는 두 비트가 *<em>서로 다를 때 *</em><code>1</code>이 되고, 같으면 <code>0</code>이 됩니다.</li>
<li>쉽게 말해 &quot;<strong>너랑 나랑 다르면 참!</strong>&quot;이라는 거죠.</li>
</ul>
<ul>
<li><code>0b0110 ^ 0b0011</code>을 계산해 봅시다.</li>
</ul>
<pre><code class="language-cpp">0 1 1 0 XOR
0 0 1 1
-------
0 1 0 1</code></pre>
<ul>
<li>여러 개를 XOR 연산할 때는 어떨까요?</li>
<li>각 열에서 <code>1</code>의 개수가 <strong>홀수</strong> 개이면 결과가 <code>1</code>이 되고, <strong>짝수</strong> 개이면 <code>0</code>이 됩니다.</li>
</ul>
<pre><code class="language-cpp">0 0 0 1 XOR
0 0 1 1 XOR
0 1 1 1
--------
0 1 0 1</code></pre>
<hr>
<h2 id="비트-대입-연산자-bitwise-assignment-operators">비트 대입 연산자 (Bitwise assignment operators)</h2>
<ul>
<li>산술 연산자 <code>+=</code> <code>-=</code> 처럼 비트 연산자도 <strong>대입과 동시에 연산을 수행하는 단축형</strong>이 있어요.</li>
<li>이 연산자들은 왼쪽 변수의 값을 변경합니다.</li>
<li>예를 들어, <code>x = x &gt;&gt; 1;</code> 대신 <code>x &gt;&gt;= 1;</code> 이라고 간단히 쓸 수 있죠.</li>
</ul>
<table>
<thead>
<tr>
<th>연산자</th>
<th>기호</th>
<th>형식</th>
<th>연산 및 대입 설명</th>
</tr>
</thead>
<tbody><tr>
<td>왼쪽 시프트 대입</td>
<td>&lt;&lt;=</td>
<td>x &lt;&lt;= n</td>
<td>x를 n만큼 왼쪽 시프트하고 그 결과를 x에 저장해요.</td>
</tr>
<tr>
<td>오른쪽 시프트 대입</td>
<td>&gt;&gt;=</td>
<td>x &gt;&gt;= n</td>
<td>x를 n만큼 오른쪽 시프트하고 그 결과를 x에 저장해요.</td>
</tr>
<tr>
<td>비트 AND 대입</td>
<td>&amp;=</td>
<td>x &amp;= y</td>
<td>x와 y를 비트 AND 연산하고 결과를 x에 저장해요.</td>
</tr>
<tr>
<td>비트 OR 대입</td>
<td>|=</td>
<td>x |= y</td>
<td>x와 y를 비트 OR 연산하고 결과를 x에 저장해요.</td>
</tr>
<tr>
<td>비트 XOR 대입</td>
<td>^=</td>
<td>x ^= y</td>
<td>x와 y를 비트 XOR 연산하고 결과를 x에 저장해요.</td>
</tr>
</tbody></table>
<hr>
<h2 id="비트-연산자는-작은-정수-타입을-승격promote시킵니다">비트 연산자는 작은 정수 타입을 승격(promote)시킵니다</h2>
<ul>
<li>int보다 작은 정수 타입(예: <code>char</code> <code>short</code>)을 비트 연산자에 사용하면, 이 값들은 자동으로 <code>int</code>나 <code>unsigned int</code>로 <strong>승격</strong>되어 계산됩니다.</li>
<li>그리고 <strong>결과값</strong>도 <code>int</code>나 <code>unsigned int</code>로 반환되죠.</li>
<li>가능하다면 <code>int</code>보다 작은 정수 타입에는 비트 시프트 연산을 피하는 것이 좋습니다.</li>
<li>만약 꼭 해야 한다면, <code>static_cast</code>를 통해 결과를 원하는 타입으로 다시 변환해 주세요.</li>
</ul>
<hr>
<h2 id="비트-마스크-bit-masks">비트 마스크 (Bit masks)</h2>
<ul>
<li>우리가 개별 비트를 조작하려면, 수많은 비트 중에서 <strong>&#39;내가 건드리고 싶은 특정 비트&#39;</strong>가 무엇인지 컴퓨터에게 알려줘야 해요.</li>
<li>하지만 아쉽게도 비트 단위 연산자들은 <strong>&quot;3번째 비트를 바꿔줘&quot; 같은 위치 기반 명령을 직접 알아듣지 못합니다.</strong></li>
<li>대신 그들은 <strong>비트 마스크</strong>라는 도구를 사용해요.</li>
<li>비트 마스크란, <strong>뒤이어 올 연산에 의해 수정될 특정 비트들을 선택하기 위해 미리 정의해 둔 비트들의 집합</strong>을 말합니다.</li>
</ul>
<ul>
<li>이해를 돕기 위해 페인트칠을 예로 들어볼게요.</li>
<li>창문 틀에 페인트를 칠하려고 하는데, 유리에 페인트가 묻으면 안 되겠죠?</li>
<li>그래서 우리는 유리에 마스킹 테이프를 붙입니다.</li>
<li>그러면 페인트를 칠해도 테이프가 붙은 곳(유리)은 보호되고, 테이프가 없는 곳(창문 틀)만 색이 칠해지죠.</li>
</ul>
<ul>
<li>비트 마스크도 똑같은 역할을 해요.</li>
<li>우리가 원하지 않는 비트는 건드리지 않도록 막아주고, 우리가 원하는 비트에만 접근할 수 있게 해 준답니다.</li>
</ul>
<hr>
<h2 id="c14에서-비트-마스크-정의하기">C++14에서 비트 마스크 정의하기</h2>
<ul>
<li>가장 기본적인 비트 마스크는 <strong>각 비트 위치(0번, 1번...)마다 하나씩 마스크</strong>를 만들어 두는 거예요.</li>
<li>상관없는 비트는 <code>0</code>으로, 조작하고 싶은 비트는 <code>1</code>로 표시합니다.</li>
<li>보통은 나중에 다시 쓰기 편하도록 의미 있는 이름을 붙여서 <strong>상수</strong>로 정의해 둡니다.</li>
<li>C++14부터는 이진수 리터럴을 지원하기 때문에 아주 직관적으로 만들 수 있어요.</li>
</ul>
<pre><code class="language-cpp">#include &lt;cstdint&gt;

constexpr std::uint8_t mask0{ 0b0000&#39;0001 }; // 0번 비트를 나타냅니다
constexpr std::uint8_t mask1{ 0b0000&#39;0010 }; // 1번 비트를 나타냅니다
constexpr std::uint8_t mask2{ 0b0000&#39;0100 }; // 2번 비트를 나타냅니다
constexpr std::uint8_t mask3{ 0b0000&#39;1000 }; // 3번 비트를 나타냅니다
constexpr std::uint8_t mask4{ 0b0001&#39;0000 }; // 4번 비트를 나타냅니다
constexpr std::uint8_t mask5{ 0b0010&#39;0000 }; // 5번 비트를 나타냅니다
constexpr std::uint8_t mask6{ 0b0100&#39;0000 }; // 6번 비트를 나타냅니다
constexpr std::uint8_t mask7{ 0b1000&#39;0000 }; // 7번 비트를 나타냅니다</code></pre>
<hr>
<h2 id="비트-상태-확인하기-testing-a-bit">비트 상태 확인하기 (Testing a bit)</h2>
<ul>
<li>특정 비트가 켜져 있는지(1인지) 꺼져 있는지(0인지) 알고 싶을 땐, 비트 단위 <code>AND 연산자(&amp;)</code>를 사용합니다.</li>
</ul>
<pre><code class="language-cpp">#include &lt;cstdint&gt;
#include &lt;iostream&gt;

int main() {
    // ... 마스크 정의 생략 (위와 동일) ...
    [[maybe_unused]] constexpr std::uint8_t mask0{ 0b0000&#39;0001 }; 
    [[maybe_unused]] constexpr std::uint8_t mask1{ 0b0000&#39;0010 }; 

    std::uint8_t flags{ 0b0000&#39;0101 }; // 8개의 플래그 중 0번과 2번이 켜져 있네요.

    // 0번 비트 확인
    std::cout &lt;&lt; &quot;bit 0 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask0) ? &quot;on\n&quot; : &quot;off\n&quot;);

    // 1번 비트 확인
    std::cout &lt;&lt; &quot;bit 1 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask1) ? &quot;on\n&quot; : &quot;off\n&quot;);
    return 0;
}

출력 결과
bit 0 is on
bit 1 is off</code></pre>
<ul>
<li>원리 설명: <code>flags &amp; mask0</code>를 하면 두 비트가 모두 <code>1</code>인 자리만 <code>1</code>이 남습니다.</li>
</ul>
<pre><code class="language-cpp">0000 0101 (flags)
0000 0001 (mask0)
--------- (AND 연산)
0000 0001 (결과: 0이 아님 -&gt; True)</code></pre>
<ul>
<li>반면 flags &amp; mask1은:</li>
</ul>
<pre><code class="language-cpp">0000 0101 (flags)
0000 0010 (mask1)
--------- (AND 연산)
0000 0000 (결과: 0임 -&gt; False)</code></pre>
<hr>
<h2 id="비트-켜기-setting-a-bit">비트 켜기 (Setting a bit)</h2>
<ul>
<li>특정 비트를 켜고 싶을 때는 비트 단위 <code>OR 대입 연산자(|=)</code>를 사용합니다.</li>
</ul>
<pre><code class="language-cpp">// ... 마스크 정의 생략 ...

std::uint8_t flags{ 0b0000&#39;0101 };
std::cout &lt;&lt; &quot;bit 1 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask1) ? &quot;on\n&quot; : &quot;off\n&quot;);

flags |= mask1; // 1번 비트를 켭니다!

std::cout &lt;&lt; &quot;bit 1 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask1) ? &quot;on\n&quot; : &quot;off\n&quot;);</code></pre>
<ul>
<li><code>OR 연산(|)</code>을 사용하면 여러 비트를 한 번에 켤 수도 있습니다.</li>
</ul>
<pre><code class="language-cpp">flags |= (mask4 | mask5); // 4번과 5번 비트를 동시에 켭니다.</code></pre>
<hr>
<h2 id="비트-끄기--초기화-resetting-a-bit">비트 끄기 / 초기화 (Resetting a bit)</h2>
<ul>
<li>특정 비트를 끄고 싶을 때는 비트 단위 <code>AND(&amp;)</code>와 비트 단위<code>NOT(~)</code>을 함께 사용해야 해요.</li>
<li>조금 복잡해 보일 수 있지만 원리를 알면 간단합니다.</li>
</ul>
<pre><code class="language-cpp">// ... 마스크 정의 생략 ...

std::uint8_t flags{ 0b0000&#39;0101 };

std::cout &lt;&lt; &quot;bit 2 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask2) ? &quot;on\n&quot; : &quot;off\n&quot;);

flags &amp;= ~mask2; // 2번 비트를 끕니다!

std::cout &lt;&lt; &quot;bit 2 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask2) ? &quot;on\n&quot; : &quot;off\n&quot;);</code></pre>
<ul>
<li><ol>
<li><code>mask2</code>는<code>0000 0100</code>입니다.</li>
</ol>
</li>
<li><ol start="2">
<li><code>~mask2</code>를 하면 <code>1111 1011</code>이 됩니다.</li>
</ol>
</li>
<li><ol start="3">
<li>이제 <code>flags</code>와 <code>1111 1011</code>을 AND 연산합니다.</li>
</ol>
</li>
<li><ol start="4">
<li><code>0</code>과 만나는 <code>번 비트는 무조건</code>0`이 되고, 1과 만나는 나머지 비트는 원래 값을 유지합니다.</li>
</ol>
</li>
</ul>
<h3 id="주의사항">주의사항</h3>
<ul>
<li><code>~mask2</code>를 할 때 컴파일러가 경고를 낼 수 있습니다.</li>
<li><code>mask2</code>는 작은 자료형인데 <code>~</code> 연산을 하면서 <code>int</code>로 승격되기 때문이에요. </li>
<li>이럴 때는 다시 원래 타입으로 캐스팅해주면 됩니다.</li>
</ul>
<pre><code class="language-cpp">flags &amp;= static_cast&lt;std::uint8_t&gt;(~mask2);</code></pre>
<hr>
<h2 id="비트-뒤집기-flipping-a-bit">비트 뒤집기 (Flipping a bit)</h2>
<ul>
<li>비트의 상태를 반대로(0은 1로, 1은 0으로) 바꾸고 싶다면 비트 단위 XOR 연산자 <code>^</code> 를 사용합니다.</li>
</ul>
<pre><code class="language-cpp">// ... 마스크 정의 생략 ...

std::uint8_t flags{ 0b0000&#39;0101 };

std::cout &lt;&lt; &quot;bit 2 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask2) ? &quot;on\n&quot; : &quot;off\n&quot;);

flags ^= mask2; // 2번 비트를 뒤집습니다.

std::cout &lt;&lt; &quot;bit 2 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask2) ? &quot;on\n&quot; : &quot;off\n&quot;);

flags ^= mask2; // 다시 뒤집습니다.

std::cout &lt;&lt; &quot;bit 2 is &quot; &lt;&lt; (static_cast&lt;bool&gt;(flags &amp; mask2) ? &quot;on\n&quot; : &quot;off\n&quot;);</code></pre>
<hr>
<h2 id="비트-마스크와-stdbitset">비트 마스크와 std::bitset</h2>
<ul>
<li>C++ 표준 라이브러리의 <code>std::bitset</code>을 사용해도 비트 조작이 가능합니다.</li>
<li><code>test()</code> <code>set()</code> <code>reset()</code> <code>flip()</code> 같은 함수를 제공해서 편리하죠.</li>
<li>하지만 여러 비트를 <strong>한꺼번에 조작하려면 여전히 비트 연산자</strong>를 사용하는 것이 좋습니다.</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>