<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[chamdom's tech]]></title><description><![CDATA[공부하는것 정리!!]]></description><link>https://blog.chamdom.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 01 Jun 2026 22:13:39 GMT</lastBuildDate><atom:link href="https://blog.chamdom.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[EIP - 55]]></title><description><![CDATA[EIP-55: 이더리움 주소 체크섬
왜 필요한가?
이더리움 주소는 40자리 hex 문자열이다. 복사하다 한 글자 틀리면 다른 주소로 전송되고, 되돌릴 수 없다. EIP-55는 대소문자 패턴을 이용해 오타를 감지한다. 주소 형식 자체는 바꾸지 않는다.
0xab5801a7d398351b8be11c439e05c5b3259aec9b   ← 체크섬 없음
0xAb58]]></description><link>https://blog.chamdom.dev/eip-55</link><guid isPermaLink="true">https://blog.chamdom.dev/eip-55</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Thu, 09 Apr 2026 13:17:06 GMT</pubDate><content:encoded><![CDATA[<h1>EIP-55: 이더리움 주소 체크섬</h1>
<h2>왜 필요한가?</h2>
<p>이더리움 주소는 40자리 hex 문자열이다. 복사하다 한 글자 틀리면 다른 주소로 전송되고, 되돌릴 수 없다. EIP-55는 <strong>대소문자 패턴</strong>을 이용해 오타를 감지한다. 주소 형식 자체는 바꾸지 않는다.</p>
<pre><code class="language-plaintext">0xab5801a7d398351b8be11c439e05c5b3259aec9b   ← 체크섬 없음
0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B   ← 체크섬 적용
</code></pre>
<h2>Keccak-256 vs SHA-3</h2>
<p>EIP-55는 Keccak-256 해시를 사용한다. SHA-3와 핵심 알고리즘은 같고, 차이는 패딩 한 바이트뿐이다.</p>
<ul>
<li><p><strong>Keccak</strong>: 패딩에 <code>0x01</code></p>
</li>
<li><p><strong>SHA-3</strong>: 패딩에 <code>0x06</code></p>
</li>
</ul>
<p>이더리움이 Keccak을 쓰는 이유는 단순히 타이밍이다. 이더리움 설계(2013~2014) 당시에는 Keccak만 존재했고, NIST가 패딩을 바꿔 SHA-3를 표준화한 건 2015년 8월(이더리움 출시 이후)이다. 이미 돌아가는 시스템을 굳이 바꿀 이유가 없었다.</p>
<h2>동작 방식</h2>
<ol>
<li><p>주소에서 <code>0x</code>를 떼고 전부 소문자로 만든다</p>
</li>
<li><p>그 문자열의 Keccak-256 해시를 구한다</p>
</li>
<li><p>해시의 각 자리(니블)가 <strong>8 이상이면 대문자</strong>, 미만이면 소문자로 변환한다</p>
</li>
</ol>
<pre><code class="language-javascript">const { keccak256 } = require("js-sha3");

function toChecksumAddress(address) {
  const addr = address.replace("0x", "").toLowerCase();
  const hash = keccak256(addr);

  let result = "0x";
  for (let i = 0; i &lt; 40; i++) {
    result += parseInt(hash[i], 16) &gt;= 8
      ? addr[i].toUpperCase()
      : addr[i];
  }
  return result;
}
</code></pre>
<p>검증은 같은 로직을 돌려서 결과가 일치하는지 비교하면 끝이다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (13)]]></title><description><![CDATA[1. STL의 개념
1-1. 배경
C++로 프로그래밍을 하다 보면 동적 배열, 연결 리스트, 정렬, 검색 같은 자료구조와 알고리즘을 반복적으로 구현하게 된다. 프로젝트마다 매번 새로 만들면 시간도 낭비되고, 버그가 생길 가능성도 높아진다.
이런 문제를 해결하기 위해 자주 사용되는 자료구조와 알고리즘을 미리 만들어서 표준 라이브러리에 포함 시킨 것이 STL이]]></description><link>https://blog.chamdom.dev/c-13</link><guid isPermaLink="true">https://blog.chamdom.dev/c-13</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Wed, 01 Apr 2026 00:05:13 GMT</pubDate><content:encoded><![CDATA[<h2>1. STL의 개념</h2>
<h3>1-1. 배경</h3>
<p>C++로 프로그래밍을 하다 보면 동적 배열, 연결 리스트, 정렬, 검색 같은 자료구조와 알고리즘을 반복적으로 구현하게 된다. 프로젝트마다 매번 새로 만들면 시간도 낭비되고, 버그가 생길 가능성도 높아진다.</p>
<p>이런 문제를 해결하기 위해 자주 사용되는 자료구조와 알고리즘을 <strong>미리 만들어서 표준 라이브러리에 포함</strong> 시킨 것이 STL이다.</p>
<h3>1-2. STL이란?</h3>
<p>STL(Standard Template Library)은 <strong>C++ 표준 라이브러리에 포함된 템플릿 기반의 자료구조와 알고리즘 모음</strong> 이다. 이름에 "Template"이 들어있는 것처럼, 앞서 배운 클래스 템플릿과 함수 템플릿으로 만들어져 있다. 그래서 <code>vector&lt;int&gt;</code>, <code>vector&lt;string&gt;</code>처럼 어떤 타입이든 담을 수 있다.</p>
<h3>1-3. 성능</h3>
<p>STL은 단순히 편리하기만 한 것이 아니라 <strong>성능도 보장</strong> 된다. 각 컨테이너와 알고리즘의 시간 복잡도가 표준에 명시되어 있다. <code>vector</code>의 인덱스 접근은 O(1), <code>map</code>의 검색은 O(log n), <code>unordered_map</code>의 검색은 평균 O(1)이 보장된다.</p>
<p>직접 구현한 것보다 STL이 느릴까 걱정할 수 있지만, STL은 수십 년간 최적화되어 온 코드다. 대부분의 경우 직접 구현하는 것보다 STL을 사용하는 것이 더 빠르고 안전하다.</p>
<hr />
<h2>2. 구성요소</h2>
<p>STL은 크게 <strong>컨테이너</strong>, <strong>반복자</strong>, <strong>알고리즘</strong> 세 가지로 구성된다. 컨테이너가 데이터를 저장하고, 반복자가 컨테이너의 요소를 가리키며, 알고리즘이 반복자를 통해 데이터를 처리한다.</p>
<h3>2-1. 컨테이너</h3>
<p>컨테이너(Container)는 <strong>데이터를 저장하는 객체</strong> 다. 직접 만들었던 동적 배열, 연결 리스트, 해시 테이블 같은 것들을 STL이 제공하는 것이다. 컨테이너는 데이터를 저장하는 방식에 따라 <strong>순차 컨테이너</strong> 와 <strong>연관 컨테이너</strong> 로 나뉜다.</p>
<h3>2-2. 순차 컨테이너</h3>
<p>순차 컨테이너는 데이터를 <strong>삽입한 순서대로</strong> 저장한다.</p>
<h4>벡터 (vector)</h4>
<p><code>vector</code>는 <strong>동적 배열</strong> 이다. 이전에 직접 구현했던 <code>DynamicArray</code>와 같은 원리로, 크기가 자동으로 늘어나는 배열이다.</p>
<pre><code class="language-cpp">#include &lt;vector&gt;

int main() {
    vector&lt;int&gt; v;

    // 삽입
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);

    // 인덱스 접근 — O(1)
    cout &lt;&lt; v[0] &lt;&lt; endl;   // 10
    cout &lt;&lt; v[1] &lt;&lt; endl;   // 20

    // 크기 확인
    cout &lt;&lt; v.size() &lt;&lt; endl;      // 3
    cout &lt;&lt; v.capacity() &lt;&lt; endl;  // 할당된 전체 용량

    // 마지막 요소 제거
    v.pop_back();  // 30 제거

    // 범위 기반 for문으로 순회
    for (int x : v) {
        cout &lt;&lt; x &lt;&lt; " ";
    }
    // 출력: 10 20

    return 0;
}
</code></pre>
<p><code>push_back()</code>으로 뒤에 추가하고, <code>pop_back()</code>으로 뒤에서 제거한다. JavaScript의 <code>push()</code>/<code>pop()</code>과 같은 동작이다. 인덱스로 바로 접근할 수 있어서 O(1)이지만, 중간 삽입/삭제는 뒤의 요소를 밀어야 하므로 O(n)이다.</p>
<p><code>vector</code>는 가장 많이 사용되는 컨테이너다. 특별한 이유가 없으면 <code>vector</code>를 기본 선택으로 쓰는 것이 일반적이다.</p>
<p><code>at()</code> 메서드를 사용하면 범위를 벗어났을 때 <code>out_of_range</code> 예외를 던져주므로 더 안전하다. <code>[]</code> 연산자는 범위 체크를 하지 않는다.</p>
<pre><code class="language-cpp">cout &lt;&lt; v.at(10) &lt;&lt; endl;  // 범위 초과 시 예외 발생
cout &lt;&lt; v[10] &lt;&lt; endl;     // 범위 초과해도 예외 없음 — 위험
</code></pre>
<h4>데크 (deque)</h4>
<p><code>deque</code>(Double-Ended Queue)는 <strong>양쪽 끝에서 삽입/삭제가 빠른 자료구조</strong> 다. <code>vector</code>는 뒤에서만 빠르게 추가/삭제할 수 있지만, <code>deque</code>는 앞에서도 O(1)으로 가능하다.</p>
<pre><code class="language-cpp">#include &lt;deque&gt;

int main() {
    deque&lt;int&gt; dq;

    // 뒤에 삽입
    dq.push_back(10);
    dq.push_back(20);

    // 앞에 삽입
    dq.push_front(5);
    dq.push_front(1);

    // 현재 상태: 1, 5, 10, 20

    // 인덱스 접근 가능
    cout &lt;&lt; dq[0] &lt;&lt; endl;  // 1
    cout &lt;&lt; dq[3] &lt;&lt; endl;  // 20

    // 앞에서 제거
    dq.pop_front();  // 1 제거

    // 뒤에서 제거
    dq.pop_back();   // 20 제거

    for (int x : dq) {
        cout &lt;&lt; x &lt;&lt; " ";
    }
    // 출력: 5 10

    return 0;
}
</code></pre>
<p><code>vector</code>와 마찬가지로 인덱스 접근이 O(1)이지만, 내부 구조가 다르다. <code>vector</code>는 하나의 연속 메모리 블록이고, <code>deque</code>는 여러 개의 블록을 연결한 구조다. 앞뒤 삽입/삭제가 모두 빈번한 경우에 <code>deque</code>가 적합하다.</p>
<h4>리스트 (list)</h4>
<p><code>list</code>는 <strong>이중 연결 리스트(Doubly Linked List)</strong> 다. 이전에 직접 구현했던 단일 연결 리스트와 달리, 각 노드가 앞 노드와 뒷 노드 모두를 가리킨다.</p>
<pre><code class="language-cpp">#include &lt;list&gt;

int main() {
    list&lt;int&gt; lst;

    lst.push_back(10);
    lst.push_back(20);
    lst.push_back(30);
    lst.push_front(5);

    // 현재 상태: 5, 10, 20, 30

    // 인덱스 접근 불가! lst[0]은 에러
    // 반복자로 순회해야 함
    for (int x : lst) {
        cout &lt;&lt; x &lt;&lt; " ";
    }
    // 출력: 5 10 20 30

    cout &lt;&lt; endl;

    // 중간 삽입 — 반복자를 사용
    auto it = lst.begin();
    advance(it, 2);          // 2번째 위치로 이동
    lst.insert(it, 15);      // 10과 20 사이에 15 삽입

    for (int x : lst) {
        cout &lt;&lt; x &lt;&lt; " ";
    }
    // 출력: 5 10 15 20 30

    // 특정 값 삭제
    lst.remove(20);  // 값이 20인 요소 전부 삭제

    return 0;
}
</code></pre>
<p><code>list</code>는 어디서든 삽입/삭제가 O(1)이지만(위치를 이미 알고 있을 때), 인덱스로 바로 접근할 수 없다. <code>lst[3]</code> 같은 접근이 불가능하고, 처음부터 순서대로 따라가야 한다. 앞서 배운 연결 리스트의 특성 그대로다.</p>
<p>세 순차 컨테이너의 선택 기준을 정리하면 이렇다. 대부분의 경우 <code>vector</code> 를 쓴다. 앞쪽 삽입/삭제가 빈번하면 <code>deque</code> 를 쓴다. 중간 삽입/삭제가 매우 빈번하고 인덱스 접근이 필요 없으면 <code>list</code> 를 쓴다.</p>
<h3>2-3. 정렬 연관 컨테이너</h3>
<p>연관 컨테이너는 데이터를 <strong>키(key) 기반으로 자동 정렬</strong> 하여 저장한다. 삽입 순서가 아니라 키 값에 따라 위치가 결정된다.</p>
<h4>셋 (set)</h4>
<p><code>set</code>은 <strong>중복 없는 값들의 집합</strong> 이다. 값을 넣으면 자동으로 정렬되고, 같은 값을 두 번 넣으면 하나만 유지된다.</p>
<pre><code class="language-cpp">#include &lt;set&gt;

int main() {
    set&lt;int&gt; s;

    s.insert(30);
    s.insert(10);
    s.insert(50);
    s.insert(20);
    s.insert(10);  // 중복 — 무시됨

    // 자동 정렬되어 저장
    for (int x : s) {
        cout &lt;&lt; x &lt;&lt; " ";
    }
    // 출력: 10 20 30 50

    cout &lt;&lt; endl;

    // 검색 — O(log n)
    if (s.find(20) != s.end()) {
        cout &lt;&lt; "20 존재" &lt;&lt; endl;
    }

    // 개수 확인 (set에서는 0 또는 1)
    cout &lt;&lt; s.count(10) &lt;&lt; endl;  // 1
    cout &lt;&lt; s.count(99) &lt;&lt; endl;  // 0

    // 삭제
    s.erase(30);

    cout &lt;&lt; "크기: " &lt;&lt; s.size() &lt;&lt; endl;  // 3

    return 0;
}
</code></pre>
<p><code>set</code>은 내부적으로 <strong>균형 이진 탐색 트리(레드-블랙 트리)</strong> 로 구현되어 있어서, 삽입·검색·삭제 모두 <strong>O(log n)</strong> 이다. 수학에서의 집합과 같은 개념이다.</p>
<p>중복을 허용하지 않으므로, 데이터에서 고유한 값만 추출하고 싶을 때 유용하다. 예를 들어 배열의 중복을 제거하려면 <code>set</code>에 전부 넣으면 된다.</p>
<pre><code class="language-cpp">int arr[] = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
set&lt;int&gt; unique(arr, arr + 10);
// unique: {1, 2, 3, 4, 5, 6, 9}
</code></pre>
<h4>맵 (map)</h4>
<p><code>map</code>은 <strong>키-값(key-value) 쌍을 키 기준으로 정렬하여 저장하는 컨테이너</strong> 다. 이전에 배운 해시맵(<code>unordered_map</code>)과 비슷하지만, <code>map</code>은 키가 <strong>자동 정렬</strong> 된다는 차이가 있다.</p>
<pre><code class="language-cpp">#include &lt;map&gt;

int main() {
    map&lt;string, int&gt; scores;

    // 삽입
    scores["홍길동"] = 90;
    scores["김철수"] = 85;
    scores["이영희"] = 95;
    scores["박민수"] = 78;

    // 키 기준으로 자동 정렬 (문자열은 사전순)
    for (auto &amp;pair : scores) {
        cout &lt;&lt; pair.first &lt;&lt; ": " &lt;&lt; pair.second &lt;&lt; endl;
    }
    // 김철수: 85
    // 박민수: 78
    // 이영희: 95
    // 홍길동: 90

    // 검색 — O(log n)
    if (scores.find("김철수") != scores.end()) {
        cout &lt;&lt; "김철수의 점수: " &lt;&lt; scores["김철수"] &lt;&lt; endl;
    }

    // 삭제
    scores.erase("박민수");

    // 존재하지 않는 키에 []로 접근하면 자동으로 생성됨 (주의!)
    cout &lt;&lt; scores["없는사람"] &lt;&lt; endl;  // 0 (int의 기본값으로 생성됨)

    return 0;
}
</code></pre>
<p><code>map</code>은 <code>set</code>과 마찬가지로 레드-블랙 트리로 구현되어 삽입·검색·삭제가 <strong>O(log n)</strong> 이다. <code>unordered_map</code>의 평균 O(1)보다는 느리지만, 키가 정렬되어 있어야 하는 경우(범위 검색, 순서대로 출력 등)에는 <code>map</code>이 필요하다.</p>
<p>주의할 점은 <code>[]</code> 연산자로 존재하지 않는 키에 접근하면 <strong>해당 키가 자동으로 생성</strong> 된다는 것이다. 단순히 존재 여부를 확인하려면 <code>find()</code>나 <code>count()</code>를 사용해야 한다.</p>
<p><code>map</code>과 <code>unordered_map</code>의 선택 기준을 정리하면 이렇다. 키 순서가 필요하면 <code>map</code>(O(log n))을 쓰고, 순서가 필요 없고 속도가 중요하면 <code>unordered_map</code>(평균 O(1))을 쓴다.</p>
<hr />
<h2>3. 반복자</h2>
<h3>3-1. 반복자란?</h3>
<p>반복자(Iterator)는 <strong>컨테이너의 요소를 가리키는 객체</strong> 다. 포인터와 비슷하게 동작하지만, 포인터보다 추상화된 개념이다.</p>
<p>배열에서는 포인터로 요소를 순회할 수 있었다. 하지만 <code>set</code>이나 <code>map</code>은 연속된 메모리가 아니라 트리 구조이므로 포인터로 순회할 수 없다. 반복자는 <strong>컨테이너의 내부 구조에 관계없이 동일한 방식으로 요소를 순회</strong> 할 수 있게 해준다.</p>
<pre><code class="language-cpp">vector&lt;int&gt; v = {10, 20, 30, 40, 50};

// 반복자 선언과 사용
vector&lt;int&gt;::iterator it;

for (it = v.begin(); it != v.end(); it++) {
    cout &lt;&lt; *it &lt;&lt; " ";
}
// 출력: 10 20 30 40 50
</code></pre>
<p><code>v.begin()</code>은 첫 번째 요소를 가리키는 반복자를 반환하고, <code>v.end()</code>는 마지막 요소의 <strong>다음 위치</strong> 를 가리킨다. <code>*it</code>로 반복자가 가리키는 값에 접근하고, <code>it++</code>로 다음 요소로 이동한다. 포인터와 사용법이 거의 같다.</p>
<p><code>auto</code> 키워드를 쓰면 타입을 직접 쓰지 않아도 된다.</p>
<pre><code class="language-cpp">for (auto it = v.begin(); it != v.end(); it++) {
    cout &lt;&lt; *it &lt;&lt; " ";
}
</code></pre>
<p>범위 기반 for문(<code>for (int x : v)</code>)은 사실 내부적으로 반복자를 사용하는 것이다. 대부분의 순회에서는 범위 기반 for문이 간편하지만, 요소를 삭제하거나 특정 위치를 조작해야 할 때는 반복자를 직접 사용해야 한다.</p>
<pre><code class="language-cpp">// 반복자로 특정 요소 삭제
vector&lt;int&gt; v = {10, 20, 30, 40, 50};
auto it = v.begin() + 2;  // 세 번째 요소 (30)
v.erase(it);               // 30 삭제
// v: {10, 20, 40, 50}
</code></pre>
<p>모든 STL 컨테이너는 반복자를 제공한다. <code>vector</code>, <code>deque</code>, <code>list</code>, <code>set</code>, <code>map</code> 전부 <code>begin()</code>과 <code>end()</code>를 가진다. 컨테이너가 바뀌어도 반복자를 쓰는 방식은 동일하다. 이것이 반복자의 핵심 가치다.</p>
<pre><code class="language-cpp">set&lt;int&gt; s = {30, 10, 50, 20};

for (auto it = s.begin(); it != s.end(); it++) {
    cout &lt;&lt; *it &lt;&lt; " ";
}
// 출력: 10 20 30 50 (자동 정렬)
</code></pre>
<p><code>map</code>의 반복자는 <code>pair</code>를 가리킨다. <code>it-&gt;first</code>로 키에, <code>it-&gt;second</code>로 값에 접근한다.</p>
<pre><code class="language-cpp">map&lt;string, int&gt; m = {{"a", 1}, {"b", 2}, {"c", 3}};

for (auto it = m.begin(); it != m.end(); it++) {
    cout &lt;&lt; it-&gt;first &lt;&lt; ": " &lt;&lt; it-&gt;second &lt;&lt; endl;
}
</code></pre>
<hr />
<h2>4. 알고리즘</h2>
<p>STL 알고리즘은 <code>&lt;algorithm&gt;</code> 헤더에 포함되어 있으며, <strong>반복자를 통해 컨테이너의 데이터를 처리</strong> 한다. 컨테이너의 종류에 관계없이 동일한 알고리즘을 적용할 수 있다.</p>
<h3>4-1. find</h3>
<p><code>find</code>는 범위 안에서 <strong>특정 값을 검색</strong> 한다. 순차 검색을 수행하며, 찾으면 해당 요소를 가리키는 반복자를, 못 찾으면 <code>end()</code>를 반환한다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;vector&gt;

int main() {
    vector&lt;int&gt; v = {10, 20, 30, 40, 50};

    auto it = find(v.begin(), v.end(), 30);

    if (it != v.end()) {
        cout &lt;&lt; "찾음: " &lt;&lt; *it &lt;&lt; endl;           // 찾음: 30
        cout &lt;&lt; "인덱스: " &lt;&lt; (it - v.begin()) &lt;&lt; endl;  // 인덱스: 2
    } else {
        cout &lt;&lt; "못 찾음" &lt;&lt; endl;
    }

    return 0;
}
</code></pre>
<p><code>find(시작 반복자, 끝 반복자, 찾을 값)</code> 형태로 호출한다. <code>vector</code>뿐 아니라 <code>deque</code>, <code>list</code> 등 어떤 순차 컨테이너에서도 같은 방식으로 사용할 수 있다.</p>
<p>다만 <code>set</code>이나 <code>map</code>에서는 <code>find</code> 알고리즘 대신 <strong>컨테이너 자체의</strong> <code>find</code> <strong>멤버함수</strong> 를 쓰는 것이 좋다. <code>set::find()</code>는 트리 구조를 활용해 O(log n)이지만, 알고리즘 <code>find()</code>는 순차 검색이라 O(n)이기 때문이다.</p>
<pre><code class="language-cpp">set&lt;int&gt; s = {10, 20, 30, 40, 50};

// 좋은 방법 — O(log n)
auto it = s.find(30);

// 나쁜 방법 — O(n)
auto it2 = find(s.begin(), s.end(), 30);
</code></pre>
<h3>4-2. sort</h3>
<p><code>sort</code>는 범위 안의 요소를 <strong>정렬</strong> 한다. 기본적으로 오름차순이며, 비교 함수를 넘겨서 정렬 기준을 바꿀 수 있다.</p>
<pre><code class="language-cpp">#include &lt;algorithm&gt;
#include &lt;vector&gt;

int main() {
    vector&lt;int&gt; v = {50, 20, 40, 10, 30};

    // 오름차순 정렬
    sort(v.begin(), v.end());

    for (int x : v) {
        cout &lt;&lt; x &lt;&lt; " ";
    }
    // 출력: 10 20 30 40 50

    cout &lt;&lt; endl;

    // 내림차순 정렬
    sort(v.begin(), v.end(), greater&lt;int&gt;());

    for (int x : v) {
        cout &lt;&lt; x &lt;&lt; " ";
    }
    // 출력: 50 40 30 20 10

    return 0;
}
</code></pre>
<p><code>sort(시작, 끝)</code> 으로 오름차순, <code>sort(시작, 끝, greater&lt;타입&gt;())</code> 으로 내림차순 정렬한다. 시간 복잡도는 <strong>O(n log n)</strong> 으로, 내부적으로 인트로소트(Introsort)라는 효율적인 알고리즘을 사용한다.</p>
<p>비교 함수를 직접 만들어서 커스텀 정렬을 할 수도 있다. C에서 <code>qsort</code>에 비교 함수를 넘겼던 것과 같은 원리다.</p>
<pre><code class="language-cpp">// 절댓값 기준으로 정렬
bool absCompare(int a, int b) {
    return abs(a) &lt; abs(b);
}

vector&lt;int&gt; v = {-5, 3, -1, 4, -2};
sort(v.begin(), v.end(), absCompare);
// 결과: -1 -2 3 4 -5
</code></pre>
<p>람다 표현식을 사용하면 비교 함수를 인라인으로 작성할 수도 있다.</p>
<pre><code class="language-cpp">// 구조체 벡터를 점수 기준으로 정렬
struct Student {
    string name;
    int score;
};

vector&lt;Student&gt; students = {
    {"홍길동", 90},
    {"김철수", 75},
    {"이영희", 85}
};

sort(students.begin(), students.end(), [](const Student &amp;a, const Student &amp;b) {
    return a.score &gt; b.score;  // 점수 내림차순
});

for (auto &amp;s : students) {
    cout &lt;&lt; s.name &lt;&lt; ": " &lt;&lt; s.score &lt;&lt; endl;
}
// 홍길동: 90
// 이영희: 85
// 김철수: 75
</code></pre>
<p><code>sort</code>는 <strong>임의 접근 반복자(Random Access Iterator)</strong> 를 요구하므로 <code>vector</code>와 <code>deque</code>에서 사용할 수 있다. <code>list</code>는 임의 접근이 안 되므로 <code>sort</code> 알고리즘을 쓸 수 없고, 대신 <code>list</code> 자체의 <code>sort()</code> 멤버함수를 사용해야 한다.</p>
<pre><code class="language-cpp">list&lt;int&gt; lst = {50, 20, 40, 10, 30};
lst.sort();  // list 자체의 sort 멤버함수
</code></pre>
<hr />
<h2>마무리</h2>
<p>STL은 C++ 프로그래밍의 생산성을 크게 높여주는 도구다. 직접 구현했던 동적 배열은 <code>vector</code>로, 연결 리스트는 <code>list</code>로, 해시맵은 <code>unordered_map</code>으로 대체할 수 있다. 반복자라는 추상화 덕분에 컨테이너가 바뀌어도 순회하는 코드는 동일하게 유지되고, <code>find</code>나 <code>sort</code> 같은 알고리즘도 컨테이너에 관계없이 적용할 수 있다. 자료구조의 원리를 이해한 상태에서 STL을 사용하면, 적재적소에 맞는 컨테이너를 선택하고 효율적인 코드를 작성할 수 있다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (12)]]></title><description><![CDATA[1. 해시
1-1. 해시의 개념
배열에서 특정 값을 찾으려면 순차 검색은 O(n), 이분 검색은 O(log n)이 걸린다. 그런데 해시(Hash) 를 사용하면 O(1), 즉 데이터가 아무리 많아도 거의 한 번에 찾을 수 있다.
해시의 핵심 아이디어는 간단하다. 데이터를 저장할 때 "어디에 넣을지"를 데이터 자체로부터 계산 하는 것이다. 찾을 때도 같은 계산]]></description><link>https://blog.chamdom.dev/c-12</link><guid isPermaLink="true">https://blog.chamdom.dev/c-12</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:54:40 GMT</pubDate><content:encoded><![CDATA[<h2>1. 해시</h2>
<h3>1-1. 해시의 개념</h3>
<p>배열에서 특정 값을 찾으려면 순차 검색은 O(n), 이분 검색은 O(log n)이 걸린다. 그런데 <strong>해시(Hash)</strong> 를 사용하면 <strong>O(1)</strong>, 즉 데이터가 아무리 많아도 거의 한 번에 찾을 수 있다.</p>
<p>해시의 핵심 아이디어는 간단하다. 데이터를 저장할 때 <strong>"어디에 넣을지"를 데이터 자체로부터 계산</strong> 하는 것이다. 찾을 때도 같은 계산을 해서 바로 그 위치를 확인하면 된다. 처음부터 끝까지 훑을 필요 없이, 계산 한 번으로 위치를 알 수 있으니 빠른 것이다.</p>
<h3>1-2. 해시 테이블</h3>
<p>해시 테이블(Hash Table)은 해시를 구현하기 위한 자료구조다. 구조를 이해하려면 <strong>버킷(Bucket)</strong>, <strong>슬롯(Slot)</strong> 이라는 개념을 알아야 한다.</p>
<p>해시 테이블은 <strong>여러 개의 버킷</strong> 으로 구성된다. 각 버킷은 <strong>여러 개의 슬롯</strong> 으로 구성된다. 그리고 슬롯은 <strong>데이터가 실제로 저장되는 단위</strong> 다.</p>
<p>쉽게 비유하면 사물함이다. 사물함 전체가 해시 테이블이고, 각 칸(1번, 2번, 3번...)이 버킷이다. 한 칸 안에 물건을 여러 개 넣을 수 있다면 각 물건 자리가 슬롯이다.</p>
<pre><code class="language-plaintext">해시 테이블
┌─────────────────────────┐
│ 버킷 0: [슬롯0][슬롯1][슬롯2] │
│ 버킷 1: [슬롯0][슬롯1][슬롯2] │
│ 버킷 2: [슬롯0][슬롯1][슬롯2] │
│ 버킷 3: [슬롯0][슬롯1][슬롯2] │
│ 버킷 4: [슬롯0][슬롯1][슬롯2] │
└─────────────────────────┘
</code></pre>
<p>이 구조를 보면 알 수 있듯이 해시 테이블은 본질적으로 <strong>2차원 배열</strong> 이다. 행이 버킷, 열이 슬롯이다.</p>
<p>버킷 수와 슬롯 수를 어떻게 잡느냐에 따라 성능이 달라진다. 총 용량이 같더라도 <strong>버킷 수가 많고 슬롯 수가 적은 것</strong> 이 검색에 유리하다. 버킷이 10개이고 슬롯이 5개(총 50칸)인 경우, 해시 함수로 버킷 번호를 구하면 해당 버킷의 슬롯 5개만 확인하면 된다. 반대로 버킷이 2개이고 슬롯이 25개(총 50칸)라면, 한 버킷 안에서 최대 25개를 뒤져야 한다. 데이터가 여러 버킷에 골고루 분산될수록 각 버킷에서 찾아야 할 양이 줄어든다.</p>
<h3>1-3. 2차원 배열과 메모리 구조</h3>
<p>해시 테이블이 2차원 배열이라고 했으니, 2차원 배열의 메모리 구조를 짚고 넘어가자.</p>
<p>C/C++에서 2차원 배열은 메모리에 <strong>행 우선(row-major)</strong> 으로 저장된다. <code>int table[3][4]</code>라면 0행의 4개 원소가 먼저 연속으로 놓이고, 이어서 1행, 2행 순서다.</p>
<pre><code class="language-cpp">int table[3][4];
</code></pre>
<pre><code class="language-plaintext">메모리 상의 배치:
[0][0] [0][1] [0][2] [0][3] [1][0] [1][1] [1][2] [1][3] [2][0] [2][1] [2][2] [2][3]
│──────── 0행 ────────│──────── 1행 ────────│──────── 2행 ────────│
</code></pre>
<p>해시 테이블로 사용하면 <code>table[버킷번호][슬롯번호]</code>로 접근하게 된다. 해시 함수가 버킷 번호(행)를 결정하면, 그 행 안의 슬롯(열)을 순차적으로 확인하는 구조다.</p>
<pre><code class="language-cpp">#define BUCKET_SIZE 5
#define SLOT_SIZE 3

int hashTable[BUCKET_SIZE][SLOT_SIZE];

memset(hashTable, 0, sizeof(hashTable));
</code></pre>
<h3>1-4. 해시 함수</h3>
<p>해시 함수는 <strong>입력된 키 값으로 버킷의 번호를 찾아내는 함수</strong> 다. 어떤 데이터를 넣으면 그 데이터가 어느 버킷에 들어갈지를 결정한다.</p>
<p>가장 기본적인 해시 함수는 <strong>나머지 연산(모듈러)</strong> 이다.</p>
<pre><code class="language-cpp">int hashFunction(int key) {
    return key % BUCKET_SIZE;
}
</code></pre>
<p>버킷이 5개라면 <code>key % 5</code>를 하면 결과가 항상 0~4 사이에 나온다. 키 값이 뭐든 5개 버킷 중 하나로 매핑되는 것이다.</p>
<pre><code class="language-plaintext">키 13 → 13 % 5 = 3 → 버킷 3에 저장
키 27 → 27 % 5 = 2 → 버킷 2에 저장
키 8  →  8 % 5 = 3 → 버킷 3에 저장 (13과 같은 버킷!)
</code></pre>
<p>키 13과 키 8이 같은 버킷 3에 들어간다. 서로 다른 키가 같은 버킷에 매핑되는 것을 <strong>충돌(Collision)</strong> 이라 한다. 슬롯이 여러 개인 이유가 이것이다. 충돌이 발생하면 같은 버킷의 다음 슬롯에 저장한다. 만약 슬롯이 전부 차면 <strong>오버플로우</strong> 가 발생한다.</p>
<p>좋은 해시 함수는 데이터를 최대한 균등하게 분산시켜 충돌을 줄이는 함수다.</p>
<p>간단한 해시 테이블 구현을 보자.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;cstring&gt;
using namespace std;

#define BUCKET_SIZE 7
#define SLOT_SIZE 3

int hashTable[BUCKET_SIZE][SLOT_SIZE];

int hashFunction(int key) {
    return key % BUCKET_SIZE;
}

bool insert(int key) {
    int bucket = hashFunction(key);

    for (int i = 0; i &lt; SLOT_SIZE; i++) {
        if (hashTable[bucket][i] == 0) {  // 빈 슬롯 찾기
            hashTable[bucket][i] = key;
            return true;
        }
    }
    return false;  // 슬롯 전부 참 → 오버플로우
}

bool search(int key) {
    int bucket = hashFunction(key);

    for (int i = 0; i &lt; SLOT_SIZE; i++) {
        if (hashTable[bucket][i] == key) {
            return true;  // 찾음
        }
    }
    return false;  // 못 찾음
}

int main() {
    memset(hashTable, 0, sizeof(hashTable));

    insert(13);
    insert(27);
    insert(8);
    insert(42);

    cout &lt;&lt; search(27) &lt;&lt; endl;  // 1 (찾음)
    cout &lt;&lt; search(99) &lt;&lt; endl;  // 0 (못 찾음)

    return 0;
}
</code></pre>
<p><code>insert</code>에서 해시 함수로 버킷 번호를 구한 뒤, 해당 버킷에서 빈 슬롯을 찾아 저장한다. <code>search</code>에서도 같은 해시 함수로 버킷을 찾고, 그 버킷의 슬롯만 확인하면 된다. 전체 데이터를 훑을 필요 없이 <strong>하나의 버킷만 확인</strong> 하면 되므로 빠르다.</p>
<h3>1-5. 해시맵</h3>
<p>해시맵(HashMap)은 해시 자료구조를 사용해서 <strong>키-값(key-value) 쌍</strong> 을 저장하는 자료구조다. 키를 해시 함수에 넣어 저장 위치를 결정하므로 <strong>검색 속도가 매우 빠르다.</strong></p>
<p>JavaScript의 객체(<code>{}</code>)나 <code>Map</code>이 바로 해시맵이다. <code>obj["name"] = "홍길동"</code> 하면 <code>"name"</code>이라는 키를 해시해서 값을 저장하고, <code>obj["name"]</code>으로 읽을 때도 같은 해시를 계산해서 바로 찾아간다.</p>
<p>C++에서는 STL의 <code>unordered_map</code>이 해시맵 구현체다.</p>
<pre><code class="language-cpp">#include &lt;iostream&gt;
#include &lt;unordered_map&gt;
using namespace std;

int main() {
    unordered_map&lt;string, int&gt; scores;

    // 삽입
    scores["홍길동"] = 90;
    scores["김철수"] = 85;
    scores["이영희"] = 95;

    // 검색 — O(1)
    cout &lt;&lt; scores["홍길동"] &lt;&lt; endl;  // 90

    // 존재 여부 확인
    if (scores.find("김철수") != scores.end()) {
        cout &lt;&lt; "김철수 발견: " &lt;&lt; scores["김철수"] &lt;&lt; endl;
    }

    // 순회
    for (auto &amp;pair : scores) {
        cout &lt;&lt; pair.first &lt;&lt; ": " &lt;&lt; pair.second &lt;&lt; endl;
    }

    return 0;
}
</code></pre>
<p><code>unordered_map</code> 은 내부적으로 해시 테이블을 사용하므로 삽입, 검색, 삭제 모두 <strong>평균 O(1)</strong> 이다. 순차 검색의 O(n), 이분 검색의 O(log n)과 비교하면 압도적으로 빠르다.</p>
<p>참고로 STL에는 <code>map</code>도 있는데, 이것은 해시가 아니라 <strong>레드-블랙 트리(균형 이진 탐색 트리)</strong> 로 구현되어 있어서 O(log n)이다. 대신 키가 자동으로 정렬된다는 장점이 있다. 정렬이 필요 없고 속도가 중요하면 <code>unordered_map</code>을, 키 순서가 필요하면 <code>map</code>을 사용한다.</p>
<p>문자열을 해시하는 간단한 예시도 보자. 문자열의 각 문자 ASCII 값을 합산하는 방식이다.</p>
<pre><code class="language-cpp">int stringHash(const string &amp;key, int bucketSize) {
    int hash = 0;
    for (char c : key) {
        hash += (int)c;
    }
    return hash % bucketSize;
}

// "abc" → 97+98+99 = 294 → 294 % 7 = 0 → 버킷 0
// "bca" → 98+99+97 = 294 → 294 % 7 = 0 → 버킷 0 (충돌!)
</code></pre>
<p>단순 합산은 문자 순서가 달라도 같은 해시값이 나오는 문제가 있다. 실제 해시 함수는 문자의 위치까지 고려한 더 정교한 수식을 사용한다.</p>
<hr />
<h2>2. 싱글톤 패턴</h2>
<h3>2-1. 싱글톤 패턴이란?</h3>
<p>싱글톤 패턴(Singleton Pattern)은 <strong>클래스의 인스턴스가 프로그램 전체에서 단 하나만 존재하도록 보장하는 디자인 패턴</strong> 이다.</p>
<p>프로그램에서 특정 객체가 하나만 있어야 하는 경우가 있다. 데이터베이스 연결 관리자, 로그 기록기, 설정(Configuration) 관리자 같은 것들이다. DB 연결 객체가 여러 개 만들어지면 연결이 낭비되고, 설정 객체가 여러 개면 어떤 것이 진짜 설정인지 혼란스러워진다.</p>
<h3>2-2. 일반적인 방식의 문제</h3>
<p>그냥 전역변수를 쓰면 되지 않을까? 가능은 하지만 문제가 있다.</p>
<pre><code class="language-cpp">// 전역 객체 — 누구든 새로 만들 수 있음
Config config1;
Config config2;  // 두 번째 객체 생성을 막을 수 없음
</code></pre>
<p>클래스를 <code>new</code>로 만들든 직접 선언하든, 아무런 제약 없이 여러 개를 만들 수 있다. 언어 차원에서 "하나만 만들어라"를 강제할 방법이 필요하다.</p>
<h3>2-3. 싱글톤 구현</h3>
<p>핵심 아이디어는 세 가지다.</p>
<p><strong>생성자를</strong> <code>private</code><strong>으로</strong> 만들어서 외부에서 객체를 직접 생성하지 못하게 한다. <strong>유일한 인스턴스를</strong> <code>static</code> <strong>멤버</strong> 로 클래스 내부에 보관한다. <code>static</code> <strong>멤버함수</strong> 로 그 인스턴스에 접근하는 유일한 통로를 제공한다.</p>
<pre><code class="language-cpp">class Singleton {
private:
    static Singleton *instance;  // 유일한 인스턴스를 가리키는 포인터
    int data;

    // 생성자를 private으로 — 외부에서 new, 직접 생성 불가
    Singleton() : data(0) {
        cout &lt;&lt; "싱글톤 생성" &lt;&lt; endl;
    }

    // 복사 생성자, 대입 연산자도 막음
    Singleton(const Singleton &amp;) = delete;
    Singleton&amp; operator=(const Singleton &amp;) = delete;

public:
    // 인스턴스를 얻는 유일한 방법
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }

    void setData(int d) { data = d; }
    int getData() { return data; }

    // 프로그램 종료 시 정리
    static void destroyInstance() {
        delete instance;
        instance = nullptr;
    }
};

// static 멤버변수 초기화
Singleton* Singleton::instance = nullptr;

int main() {
    // Singleton s;              // 에러! 생성자가 private
    // Singleton *p = new Singleton();  // 에러! 생성자가 private

    Singleton *s1 = Singleton::getInstance();
    Singleton *s2 = Singleton::getInstance();

    s1-&gt;setData(42);
    cout &lt;&lt; s2-&gt;getData() &lt;&lt; endl;  // 42 — 같은 객체이므로

    cout &lt;&lt; (s1 == s2) &lt;&lt; endl;  // 1 (true) — 같은 주소

    Singleton::destroyInstance();
    return 0;
}
</code></pre>
<p><code>getInstance()</code>를 처음 호출하면 <code>instance</code>가 <code>nullptr</code>이므로 <code>new Singleton()</code>으로 객체를 생성한다. 두 번째 호출부터는 이미 만들어진 객체의 주소를 그대로 반환한다. 결과적으로 <code>s1</code>과 <code>s2</code>는 <strong>같은 객체를 가리킨다.</strong></p>
<p>복사 생성자와 대입 연산자를 <code>= delete</code>로 삭제한 것도 중요하다. 이것을 하지 않으면 <code>Singleton s3 = *s1;</code>으로 복사본을 만들어서 싱글톤의 의미가 깨질 수 있다.</p>
<h3>2-4. 더 간결한 구현 (C++11 이후)</h3>
<p>C++11부터는 지역 <code>static</code> 변수의 초기화가 스레드 안전하게 보장되므로, 더 간결하게 구현할 수 있다.</p>
<pre><code class="language-cpp">class Singleton {
private:
    int data;

    Singleton() : data(0) {}
    Singleton(const Singleton &amp;) = delete;
    Singleton&amp; operator=(const Singleton &amp;) = delete;

public:
    static Singleton&amp; getInstance() {
        static Singleton instance;  // 최초 호출 시 한 번만 생성
        return instance;
    }

    void setData(int d) { data = d; }
    int getData() { return data; }
};

int main() {
    Singleton &amp;s1 = Singleton::getInstance();
    Singleton &amp;s2 = Singleton::getInstance();

    s1.setData(42);
    cout &lt;&lt; s2.getData() &lt;&lt; endl;  // 42

    cout &lt;&lt; (&amp;s1 == &amp;s2) &lt;&lt; endl;  // 1 (true)
    return 0;
}
</code></pre>
<p><code>static Singleton instance</code> 는 함수가 처음 호출될 때 한 번만 생성되고, 프로그램 종료 시 자동으로 소멸된다. 포인터 대신 레퍼런스를 반환하므로 <code>delete</code> 를 신경 쓸 필요도 없고, <code>nullptr</code> 체크도 불필요하다.</p>
<h3>2-5. 싱글톤의 활용과 주의점</h3>
<p>싱글톤이 적합한 경우는 시스템에 하나만 존재해야 하는 자원을 관리할 때다. 데이터베이스 커넥션 풀, 로그 매니저, 앱 설정 관리자 같은 것들이 대표적이다.</p>
<pre><code class="language-cpp">class Logger {
private:
    Logger() {}
    Logger(const Logger &amp;) = delete;
    Logger&amp; operator=(const Logger &amp;) = delete;

public:
    static Logger&amp; getInstance() {
        static Logger instance;
        return instance;
    }

    void log(const string &amp;message) {
        cout &lt;&lt; "[LOG] " &lt;&lt; message &lt;&lt; endl;
    }
};

// 프로그램 어디서든 같은 로거 사용
Logger::getInstance().log("서버 시작");
Logger::getInstance().log("요청 처리 완료");
</code></pre>
<p>다만 싱글톤을 남용하면 문제가 생긴다. 싱글톤은 본질적으로 <strong>전역 상태</strong> 이므로, 많이 사용하면 전역변수를 남용하는 것과 같은 문제(결합도 증가, 테스트 어려움)가 발생한다. 꼭 하나만 존재해야 하는 명확한 이유가 있을 때만 사용하는 것이 좋다.</p>
<hr />
<h2>마무리</h2>
<p>해시는 데이터를 빠르게 저장하고 찾기 위한 핵심 자료구조다. 해시 함수로 버킷 번호를 계산하고, 해당 버킷의 슬롯만 확인하면 되므로 평균 O(1)의 검색 속도를 달성한다. 싱글톤 패턴은 클래스의 인스턴스를 하나로 제한하는 디자인 패턴으로, <code>private</code> 생성자와 <code>static</code> 접근 함수가 핵심이다. 둘 다 실무에서 빈번하게 사용되는 개념이므로 원리를 이해해두면 큰 도움이 된다.</p>
]]></content:encoded></item><item><title><![CDATA[알고리즘 (1)]]></title><description><![CDATA[C++ 알고리즘 — 검색, 동적 배열, 연결 리스트

1. 검색
검색은 데이터 집합에서 원하는 값을 찾는 것 이다. 검색 방식에 따라 성능이 크게 달라진다.
1-1. 순차 검색 (Linear Search)
순차 검색은 배열의 처음부터 끝까지 하나씩 비교 하며 찾는 방법이다. 가장 단순하고 직관적이다.
int linearSearch(int arr[], int]]></description><link>https://blog.chamdom.dev/1</link><guid isPermaLink="true">https://blog.chamdom.dev/1</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:46:18 GMT</pubDate><content:encoded><![CDATA[<h1>C++ 알고리즘 — 검색, 동적 배열, 연결 리스트</h1>
<hr />
<h2>1. 검색</h2>
<p>검색은 데이터 집합에서 <strong>원하는 값을 찾는 것</strong> 이다. 검색 방식에 따라 성능이 크게 달라진다.</p>
<h3>1-1. 순차 검색 (Linear Search)</h3>
<p>순차 검색은 배열의 처음부터 끝까지 <strong>하나씩 비교</strong> 하며 찾는 방법이다. 가장 단순하고 직관적이다.</p>
<pre><code class="language-cpp">int linearSearch(int arr[], int size, int target) {
    for (int i = 0; i &lt; size; i++) {
        if (arr[i] == target) {
            return i;  // 찾으면 인덱스 반환
        }
    }
    return -1;  // 못 찾으면 -1
}

int main() {
    int arr[] = {4, 2, 7, 1, 9, 3, 8};
    int result = linearSearch(arr, 7, 9);

    if (result != -1) {
        cout &lt;&lt; "인덱스 " &lt;&lt; result &lt;&lt; "에서 발견" &lt;&lt; endl;  // 인덱스 4에서 발견
    } else {
        cout &lt;&lt; "찾지 못함" &lt;&lt; endl;
    }
    return 0;
}
</code></pre>
<p>장점은 <strong>정렬되지 않은 데이터에서도 사용 가능</strong> 하다는 것이다. 별다른 준비 없이 바로 쓸 수 있다.</p>
<p>단점은 <strong>느리다</strong> 는 것이다. 데이터가 <code>n</code>개면 최악의 경우 <code>n</code>번 비교해야 한다. 시간 복잡도는 <strong>O(n)</strong> 이다. 데이터가 100만 개면 최대 100만 번 비교해야 한다.</p>
<h3>1-2. 이분 검색 (Binary Search)</h3>
<p>이분 검색은 <strong>정렬된 배열</strong> 에서 범위를 반씩 줄여가며 찾는 방법이다. 매번 비교할 때마다 탐색 범위가 절반으로 줄어든다.</p>
<pre><code class="language-cpp">int binarySearch(int arr[], int size, int target) {
    int low = 0;
    int high = size - 1;

    while (low &lt;= high) {
        int mid = (low + high) / 2;

        if (arr[mid] == target) {
            return mid;            // 찾음
        } else if (arr[mid] &lt; target) {
            low = mid + 1;         // 오른쪽 절반 탐색
        } else {
            high = mid - 1;        // 왼쪽 절반 탐색
        }
    }
    return -1;  // 못 찾음
}

int main() {
    int arr[] = {1, 3, 5, 7, 9, 11, 13, 15};  // 반드시 정렬되어 있어야 함
    int result = binarySearch(arr, 8, 7);

    if (result != -1) {
        cout &lt;&lt; "인덱스 " &lt;&lt; result &lt;&lt; "에서 발견" &lt;&lt; endl;  // 인덱스 3에서 발견
    }
    return 0;
}
</code></pre>
<p>동작 과정을 따라가보자. 배열 <code>{1, 3, 5, 7, 9, 11, 13, 15}</code>에서 <code>9</code>를 찾을 때의 흐름이다.</p>
<pre><code class="language-plaintext">[1, 3, 5, 7, 9, 11, 13, 15]에서 9를 찾기

1단계: mid=3, arr[3]=7 &lt; 9  → 오른쪽으로 (low=4)
2단계: mid=5, arr[5]=11 &gt; 9 → 왼쪽으로 (high=4)
3단계: mid=4, arr[4]=9 == 9 → 찾음!
</code></pre>
<p>8개 데이터에서 3번 만에 찾았다. 시간 복잡도는 <strong>O(log n)</strong> 이다. 데이터가 100만 개여도 최대 약 20번이면 찾을 수 있다. 순차 검색의 100만 번과 비교하면 엄청난 차이다.</p>
<p>단, 이분 검색은 <strong>배열이 정렬되어 있어야만</strong> 사용할 수 있다. 정렬되지 않은 데이터에 이분 검색을 적용하면 올바른 결과를 보장할 수 없다.</p>
<hr />
<h2>2. 동적 배열</h2>
<p>일반 배열은 크기가 고정되어 있다. 선언 시 크기를 정하면 나중에 늘리거나 줄일 수 없다. 동적 배열은 <strong>필요에 따라 크기가 자동으로 늘어나는 배열</strong> 이다.</p>
<p>기본 원리는 이렇다. 처음에 일정 크기의 배열을 <code>new</code>로 할당한다. 공간이 부족해지면 더 큰 배열을 새로 할당하고, 기존 데이터를 복사한 뒤, 이전 배열을 해제한다.</p>
<pre><code class="language-cpp">class DynamicArray {
private:
    int *data;
    int size;       // 현재 저장된 요소 수
    int capacity;   // 전체 할당된 크기

    void resize() {
        capacity *= 2;  // 용량을 2배로 늘림
        int *newData = new int[capacity];

        for (int i = 0; i &lt; size; i++) {
            newData[i] = data[i];  // 기존 데이터 복사
        }

        delete[] data;   // 이전 배열 해제
        data = newData;  // 새 배열로 교체
    }

public:
    DynamicArray() : size(0), capacity(4) {
        data = new int[capacity];
    }

    ~DynamicArray() {
        delete[] data;
    }

    void push(int value) {
        if (size &gt;= capacity) {
            resize();  // 공간 부족하면 확장
        }
        data[size++] = value;
    }

    int get(int index) {
        if (index &lt; 0 || index &gt;= size) {
            throw out_of_range("인덱스 범위 초과");
        }
        return data[index];
    }

    int getSize() {
        return size;
    }
};

int main() {
    DynamicArray arr;

    for (int i = 0; i &lt; 10; i++) {
        arr.push(i * 10);
    }

    for (int i = 0; i &lt; arr.getSize(); i++) {
        cout &lt;&lt; arr.get(i) &lt;&lt; " ";
    }
    // 출력: 0 10 20 30 40 50 60 70 80 90
    return 0;
}
</code></pre>
<p><code>capacity</code>는 현재 할당된 전체 크기이고, <code>size</code>는 실제로 데이터가 들어있는 수다. <code>push</code>할 때 <code>size</code>가 <code>capacity</code>에 도달하면 <code>resize()</code>가 호출되어 용량이 2배로 늘어난다.</p>
<p>용량을 2배로 늘리는 것이 핵심이다. 매번 1씩 늘리면 <code>push</code>할 때마다 전체 복사가 일어나서 성능이 나빠진다. 2배로 늘리면 복사 빈도가 급격히 줄어든다. 이 전략을 <strong>상각 분석(amortized analysis)</strong> 으로 분석하면, <code>push</code>의 평균 시간 복잡도가 <strong>O(1)</strong> 이 된다.</p>
<p>C++ STL의 <code>vector</code>가 바로 이 동적 배열을 구현한 것이다. <code>vector&lt;int&gt;</code>를 쓰면 위의 로직을 직접 구현할 필요 없이 동적 배열을 사용할 수 있다. JavaScript의 배열도 내부적으로 이와 비슷한 방식으로 동작한다.</p>
<hr />
<h2>3. 연결 리스트</h2>
<p>연결 리스트(Linked List)는 배열과는 완전히 다른 방식으로 데이터를 저장하는 자료구조다. 배열은 데이터를 <strong>연속된 메모리</strong> 에 나란히 저장하지만, 연결 리스트는 각 데이터를 <strong>노드(Node)</strong> 라는 단위에 담고, 노드끼리 <strong>포인터로 연결</strong> 한다.</p>
<p>각 노드는 <strong>데이터</strong> 와 <strong>다음 노드의 주소(포인터)</strong> 를 가진다.</p>
<pre><code class="language-plaintext">[데이터|다음] → [데이터|다음] → [데이터|다음] → NULL
</code></pre>
<pre><code class="language-cpp">struct Node {
    int data;
    Node *next;

    Node(int d) : data(d), next(nullptr) {}
};
</code></pre>
<p>연결 리스트를 클래스로 구현하면 이렇다.</p>
<pre><code class="language-cpp">class LinkedList {
private:
    Node *head;

public:
    LinkedList() : head(nullptr) {}

    ~LinkedList() {
        Node *current = head;
        while (current != nullptr) {
            Node *next = current-&gt;next;
            delete current;
            current = next;
        }
    }

    // 맨 앞에 삽입
    void insertFront(int value) {
        Node *newNode = new Node(value);
        newNode-&gt;next = head;
        head = newNode;
    }

    // 맨 뒤에 삽입
    void insertBack(int value) {
        Node *newNode = new Node(value);

        if (head == nullptr) {
            head = newNode;
            return;
        }

        Node *current = head;
        while (current-&gt;next != nullptr) {
            current = current-&gt;next;
        }
        current-&gt;next = newNode;
    }

    // 특정 값 삭제
    void remove(int value) {
        if (head == nullptr) return;

        if (head-&gt;data == value) {
            Node *temp = head;
            head = head-&gt;next;
            delete temp;
            return;
        }

        Node *current = head;
        while (current-&gt;next != nullptr &amp;&amp; current-&gt;next-&gt;data != value) {
            current = current-&gt;next;
        }

        if (current-&gt;next != nullptr) {
            Node *temp = current-&gt;next;
            current-&gt;next = temp-&gt;next;
            delete temp;
        }
    }

    // 전체 출력
    void print() {
        Node *current = head;
        while (current != nullptr) {
            cout &lt;&lt; current-&gt;data;
            if (current-&gt;next != nullptr) cout &lt;&lt; " → ";
            current = current-&gt;next;
        }
        cout &lt;&lt; " → NULL" &lt;&lt; endl;
    }
};

int main() {
    LinkedList list;
    list.insertBack(10);
    list.insertBack(20);
    list.insertBack(30);
    list.insertFront(5);
    list.print();  // 5 → 10 → 20 → 30 → NULL

    list.remove(20);
    list.print();  // 5 → 10 → 30 → NULL

    return 0;
}
</code></pre>
<p>연결 리스트와 배열의 핵심 차이를 정리하면 이렇다.</p>
<p><strong>삽입/삭제</strong> 에서 연결 리스트가 유리하다. 배열은 중간에 삽입하려면 뒤의 요소를 전부 밀어야 하지만, 연결 리스트는 포인터만 바꾸면 된다. 맨 앞에 삽입하는 것은 배열이 O(n)이지만 연결 리스트는 O(1)이다.</p>
<p><strong>접근(탐색)</strong> 에서는 배열이 유리하다. 배열은 인덱스로 바로 접근할 수 있어서 O(1)이지만, 연결 리스트는 <code>head</code>부터 하나씩 따라가야 하므로 O(n)이다. <code>list[5]</code>처럼 바로 접근하는 것이 불가능하다.</p>
<p><strong>메모리</strong> 측면에서 배열은 연속된 메모리가 필요하고, 연결 리스트는 흩어진 메모리를 포인터로 연결하므로 연속 공간이 필요 없다. 다만 각 노드마다 포인터를 추가로 저장해야 하므로 노드 하나당 메모리 사용량은 더 크다.</p>
<p>데이터의 삽입·삭제가 빈번하면 연결 리스트가, 인덱스로 빠르게 접근해야 하면 배열(또는 동적 배열)이 적합하다. 상황에 맞는 자료구조를 선택하는 것이 중요하다.</p>
<hr />
<h2>마무리</h2>
<p>검색 알고리즘은 데이터를 어떻게 찾느냐에 따라 성능이 극적으로 달라진다는 것을 보여준다. 순차 검색의 O(n)과 이분 검색의 O(log n)은 데이터가 클수록 차이가 벌어진다. 동적 배열과 연결 리스트는 각각 다른 상황에 최적화된 자료구조로, 배열의 빠른 접근과 연결 리스트의 빠른 삽입/삭제라는 트레이드오프를 이해하는 것이 핵심이다. 이 개념들은 STL의 <code>vector</code>, <code>list</code>, <code>map</code> 같은 컨테이너를 사용할 때의 기반이 된다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (11)]]></title><description><![CDATA[1. 템플릿
1-1. 템플릿의 정의
템플릿(Template)은 타입을 매개변수로 받아서 함수나 클래스를 자동으로 생성하는 틀 이다. "어떤 타입이든 동작하는 코드"를 한 번만 작성하면, 컴파일러가 실제 사용되는 타입에 맞춰 코드를 찍어낸다.
1-2. 템플릿의 필요성
int 두 개를 비교해서 큰 값을 반환하는 함수를 만들었다고 하자.
int getMax(in]]></description><link>https://blog.chamdom.dev/c-11</link><guid isPermaLink="true">https://blog.chamdom.dev/c-11</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:36:56 GMT</pubDate><content:encoded><![CDATA[<h2>1. 템플릿</h2>
<h3>1-1. 템플릿의 정의</h3>
<p>템플릿(Template)은 <strong>타입을 매개변수로 받아서 함수나 클래스를 자동으로 생성하는 틀</strong> 이다. "어떤 타입이든 동작하는 코드"를 한 번만 작성하면, 컴파일러가 실제 사용되는 타입에 맞춰 코드를 찍어낸다.</p>
<h3>1-2. 템플릿의 필요성</h3>
<p><code>int</code> 두 개를 비교해서 큰 값을 반환하는 함수를 만들었다고 하자.</p>
<pre><code class="language-cpp">int getMax(int a, int b) {
    return (a &gt; b) ? a : b;
}
</code></pre>
<p>그런데 <code>double</code>에도 같은 함수가 필요하다. <code>string</code>에도 필요하다. 로직은 완전히 같은데 타입만 다르다.</p>
<pre><code class="language-cpp">int getMax(int a, int b) { return (a &gt; b) ? a : b; }
double getMax(double a, double b) { return (a &gt; b) ? a : b; }
string getMax(string a, string b) { return (a &gt; b) ? a : b; }
</code></pre>
<p>오버로딩으로 해결할 수는 있지만, 본질적으로 <strong>같은 코드를 타입만 바꿔서 반복 작성</strong> 하는 것이다. 새로운 타입이 추가될 때마다 함수를 하나 더 만들어야 한다. 이 문제를 해결하는 것이 템플릿이다.</p>
<h3>1-3. 템플릿의 구문 구조</h3>
<p>템플릿은 <code>template</code> 키워드와 꺾쇠(<code>&lt; &gt;</code>) 안에 <strong>타입 매개변수</strong> 를 지정해서 만든다.</p>
<pre><code class="language-cpp">template &lt;typename T&gt;
</code></pre>
<p><code>T</code>는 관례적으로 사용하는 이름이지만, <code>Type</code>, <code>U</code>, <code>DataType</code> 등 아무 이름이나 쓸 수 있다. <code>typename</code> 대신 <code>class</code>를 써도 동일하게 동작한다. <code>template &lt;class T&gt;</code>와 <code>template &lt;typename T&gt;</code>는 같은 의미다.</p>
<p>타입 매개변수는 여러 개 사용할 수도 있다.</p>
<pre><code class="language-cpp">template &lt;typename T, typename U&gt;
</code></pre>
<h3>1-4. 함수 템플릿</h3>
<p>함수 템플릿은 <strong>타입에 독립적인 함수</strong> 를 만든다. 아까의 <code>getMax</code> 함수를 템플릿으로 만들면 이렇다.</p>
<pre><code class="language-cpp">template &lt;typename T&gt;
T getMax(T a, T b) {
    return (a &gt; b) ? a : b;
}

int main() {
    cout &lt;&lt; getMax(10, 20) &lt;&lt; endl;            // T가 int로 추론
    cout &lt;&lt; getMax(3.14, 2.72) &lt;&lt; endl;        // T가 double로 추론
    cout &lt;&lt; getMax&lt;string&gt;("abc", "xyz") &lt;&lt; endl;  // 명시적으로 타입 지정
    return 0;
}
</code></pre>
<p><code>getMax(10, 20)</code>을 호출하면 컴파일러가 인자의 타입을 보고 <code>T</code>를 <code>int</code>로 <strong>추론</strong> 한다. 그리고 <code>int getMax(int a, int b)</code> 함수를 자동으로 생성한다. <code>getMax(3.14, 2.72)</code>를 호출하면 <code>double</code> 버전이 자동으로 만들어진다.</p>
<p>꺾쇠로 타입을 직접 지정할 수도 있다. <code>getMax&lt;string&gt;("abc", "xyz")</code>처럼 쓰면 "T를 <code>string</code>으로 쓰겠다"고 명시하는 것이다.</p>
<p>중요한 점은 템플릿 자체가 함수가 아니라 <strong>함수를 만들어내는 틀</strong> 이라는 것이다. 실제 함수는 특정 타입으로 호출될 때 컴파일러가 생성한다. 이 과정을 <strong>템플릿 인스턴스화(instantiation)</strong> 라고 한다.</p>
<p>여러 타입 매개변수를 사용하는 예시도 보자.</p>
<pre><code class="language-cpp">template &lt;typename T, typename U&gt;
void printPair(T first, U second) {
    cout &lt;&lt; first &lt;&lt; ", " &lt;&lt; second &lt;&lt; endl;
}

int main() {
    printPair(1, 3.14);         // T=int, U=double
    printPair("Hello", 42);     // T=const char*, U=int
    return 0;
}
</code></pre>
<h3>1-5. 클래스 템플릿</h3>
<p>클래스 템플릿은 <strong>타입에 독립적인 클래스</strong> 를 만든다. 어떤 타입의 데이터든 담을 수 있는 범용 컨테이너를 만들 때 유용하다.</p>
<pre><code class="language-cpp">template &lt;typename T&gt;
class Box {
private:
    T value;

public:
    Box(T v) : value(v) {}

    T getValue() {
        return value;
    }

    void setValue(T v) {
        value = v;
    }
};

int main() {
    Box&lt;int&gt; intBox(42);
    Box&lt;string&gt; strBox("Hello");
    Box&lt;double&gt; dblBox(3.14);

    cout &lt;&lt; intBox.getValue() &lt;&lt; endl;   // 42
    cout &lt;&lt; strBox.getValue() &lt;&lt; endl;   // Hello
    cout &lt;&lt; dblBox.getValue() &lt;&lt; endl;   // 3.14
    return 0;
}
</code></pre>
<p>함수 템플릿과 달리 클래스 템플릿은 <strong>타입을 명시적으로 지정해야</strong> 한다. <code>Box&lt;int&gt;</code>, <code>Box&lt;string&gt;</code>처럼 꺾쇠 안에 타입을 넣어서 객체를 생성한다.</p>
<p>좀 더 실용적인 예시로, 간단한 스택을 템플릿으로 만들어보자.</p>
<pre><code class="language-cpp">template &lt;typename T&gt;
class Stack {
private:
    T *data;
    int top;
    int capacity;

public:
    Stack(int cap) : capacity(cap), top(-1) {
        data = new T[capacity];
    }

    ~Stack() {
        delete[] data;
    }

    void push(T value) {
        if (top &lt; capacity - 1) {
            data[++top] = value;
        }
    }

    T pop() {
        if (top &gt;= 0) {
            return data[top--];
        }
        throw "스택이 비어있습니다";
    }

    bool isEmpty() {
        return top == -1;
    }
};

int main() {
    Stack&lt;int&gt; intStack(10);
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    cout &lt;&lt; intStack.pop() &lt;&lt; endl;  // 3

    Stack&lt;string&gt; strStack(5);
    strStack.push("Hello");
    strStack.push("World");
    cout &lt;&lt; strStack.pop() &lt;&lt; endl;  // World

    return 0;
}
</code></pre>
<p><code>Stack&lt;int&gt;</code>는 정수를 담는 스택이 되고, <code>Stack&lt;string&gt;</code>은 문자열을 담는 스택이 된다. 코드는 하나지만 타입에 따라 다른 클래스가 자동 생성된다. JavaScript의 배열이 <code>push()</code>/<code>pop()</code>으로 스택처럼 동작하는 것과 비슷하지만, C++ 템플릿은 타입 안전성까지 보장한다.</p>
<p>실제로 C++ 표준 라이브러리(STL)의 <code>vector</code>, <code>stack</code>, <code>queue</code>, <code>map</code> 같은 컨테이너들이 전부 클래스 템플릿으로 만들어져 있다. <code>vector&lt;int&gt;</code>, <code>vector&lt;string&gt;</code> 형태로 쓰는 것이 바로 클래스 템플릿의 인스턴스화다.</p>
<hr />
<h2>2. 예외처리</h2>
<p>프로그램 실행 중에는 예상치 못한 상황이 발생할 수 있다. 0으로 나누기, 메모리 할당 실패, 배열 범위 초과, 파일 열기 실패 등이 그렇다. 이런 상황을 <strong>예외(Exception)</strong> 라 하고, 이를 처리하는 메커니즘이 <strong>예외처리</strong> 다.</p>
<p>C에서는 반환값으로 에러를 알렸다. 함수가 <code>-1</code>이나 <code>NULL</code>을 반환하면 에러인 식이다. 이 방식은 반환값을 매번 체크해야 하고, 정상 반환값과 에러 반환값을 구분하기 어려울 때가 있다.</p>
<p>C++에서는 <code>try</code>, <code>catch</code>, <code>throw</code> 키워드로 예외를 처리한다.</p>
<pre><code class="language-cpp">try {
    // 예외가 발생할 수 있는 코드
} catch (예외타입 변수) {
    // 예외 처리 코드
}
</code></pre>
<p><code>throw</code>는 예외를 <strong>던지는</strong> 것이고, <code>try</code> 블록 안에서 발생한 예외를 <code>catch</code> 블록이 <strong>잡는</strong> 것이다.</p>
<pre><code class="language-cpp">double divide(int a, int b) {
    if (b == 0) {
        throw "0으로 나눌 수 없습니다";  // 예외 던지기
    }
    return (double)a / b;
}

int main() {
    try {
        cout &lt;&lt; divide(10, 2) &lt;&lt; endl;   // 5
        cout &lt;&lt; divide(10, 0) &lt;&lt; endl;   // 여기서 예외 발생
        cout &lt;&lt; "이 줄은 실행되지 않음" &lt;&lt; endl;
    } catch (const char *msg) {
        cout &lt;&lt; "에러: " &lt;&lt; msg &lt;&lt; endl;  // 에러: 0으로 나눌 수 없습니다
    }

    cout &lt;&lt; "프로그램 계속 실행" &lt;&lt; endl;
    return 0;
}
</code></pre>
<p>예외가 발생하면 <code>try</code> 블록의 나머지 코드는 건너뛰고 바로 <code>catch</code> 블록으로 점프한다. 예외처리 후 프로그램은 <code>catch</code> 블록 이후부터 정상적으로 계속 실행된다.</p>
<p>여러 종류의 예외를 다르게 처리할 수도 있다.</p>
<pre><code class="language-cpp">try {
    // ...
} catch (int e) {
    cout &lt;&lt; "정수 에러 코드: " &lt;&lt; e &lt;&lt; endl;
} catch (const char *msg) {
    cout &lt;&lt; "메시지: " &lt;&lt; msg &lt;&lt; endl;
} catch (...) {
    cout &lt;&lt; "알 수 없는 예외" &lt;&lt; endl;  // 모든 예외를 잡음
}
</code></pre>
<p><code>catch (...)</code>는 앞의 <code>catch</code>에서 잡지 못한 모든 종류의 예외를 잡는다. 마지막에 두는 것이 일반적이다.</p>
<p>실무에서는 문자열이나 정수 대신 <strong>예외 클래스</strong> 를 정의해서 사용한다.</p>
<pre><code class="language-cpp">class DivideByZeroException {
private:
    string message;

public:
    DivideByZeroException(string msg) : message(msg) {}

    string getMessage() const {
        return message;
    }
};

double divide(int a, int b) {
    if (b == 0) {
        throw DivideByZeroException("0으로 나눌 수 없습니다");
    }
    return (double)a / b;
}

int main() {
    try {
        divide(10, 0);
    } catch (const DivideByZeroException &amp;e) {
        cout &lt;&lt; e.getMessage() &lt;&lt; endl;
    }
    return 0;
}
</code></pre>
<p>C++ 표준 라이브러리는 <code>&lt;stdexcept&gt;</code> 헤더에 <code>runtime_error</code>, <code>invalid_argument</code>, <code>out_of_range</code> 같은 예외 클래스를 제공한다. 이들은 모두 <code>exception</code> 클래스를 상속받는다.</p>
<pre><code class="language-cpp">#include &lt;stdexcept&gt;

double divide(int a, int b) {
    if (b == 0) {
        throw runtime_error("0으로 나눌 수 없습니다");
    }
    return (double)a / b;
}

try {
    divide(10, 0);
} catch (const exception &amp;e) {
    cout &lt;&lt; e.what() &lt;&lt; endl;  // what()으로 메시지 확인
}
</code></pre>
<p>예외처리의 핵심 원칙은 <strong>정상 흐름과 에러 처리를 분리</strong> 하는 것이다. <code>try</code> 블록에는 정상적인 로직만 두고, 에러 대응은 <code>catch</code> 블록에 모아둔다. 코드의 가독성이 높아지고, 에러 처리를 빼먹을 위험도 줄어든다.</p>
<hr />
<h2>마무리</h2>
<p>템플릿은 타입에 독립적인 코드를 작성하게 해주는 C++의 강력한 기능이다. 같은 로직을 타입만 바꿔서 반복 작성하는 문제를 해결하며, STL의 모든 컨테이너가 클래스 템플릿으로 만들어져 있다. 예외처리는 <code>try</code>/<code>catch</code>/<code>throw</code>로 정상 흐름과 에러 처리를 깔끔하게 분리한다. C의 반환값 기반 에러 처리보다 직관적이고 안전하다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (10)]]></title><description><![CDATA[1. 부모·자식 클래스 사이의 변환
상속 관계에서 부모 클래스와 자식 클래스 사이에는 타입 변환 규칙 이 존재한다. 이 규칙을 이해하는 것이 다형성의 출발점이다.
자식 → 부모 (업캐스팅)
자식 클래스 객체는 부모 클래스 타입으로 자연스럽게 변환 된다. 이것을 업캐스팅(Upcasting) 이라 한다.
class Animal {
public:
    void]]></description><link>https://blog.chamdom.dev/c-10</link><guid isPermaLink="true">https://blog.chamdom.dev/c-10</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:33:53 GMT</pubDate><content:encoded><![CDATA[<h2>1. 부모·자식 클래스 사이의 변환</h2>
<p>상속 관계에서 부모 클래스와 자식 클래스 사이에는 <strong>타입 변환 규칙</strong> 이 존재한다. 이 규칙을 이해하는 것이 다형성의 출발점이다.</p>
<h3>자식 → 부모 (업캐스팅)</h3>
<p>자식 클래스 객체는 부모 클래스 타입으로 <strong>자연스럽게 변환</strong> 된다. 이것을 <strong>업캐스팅(Upcasting)</strong> 이라 한다.</p>
<pre><code class="language-cpp">class Animal {
public:
    void breathe() {
        cout &lt;&lt; "숨을 쉰다" &lt;&lt; endl;
    }
};

class Dog : public Animal {
public:
    void bark() {
        cout &lt;&lt; "멍멍!" &lt;&lt; endl;
    }
};

int main() {
    Dog dog;
    Animal *pAnimal = &amp;dog;   // 업캐스팅 — 자식을 부모 포인터로
    Animal &amp;rAnimal = dog;    // 레퍼런스로도 가능

    pAnimal-&gt;breathe();  // OK
    // pAnimal-&gt;bark();  // 에러! — 부모 타입이므로 자식 고유 멤버 접근 불가

    return 0;
}
</code></pre>
<p><code>Dog</code>은 <code>Animal</code>을 상속받았으므로 <code>Animal</code>이 가진 모든 것을 포함하고 있다. <code>Dog</code> 객체를 <code>Animal *</code>로 가리켜도 <code>Animal</code> 부분은 온전히 존재하기 때문에 안전하다. 다만 부모 타입 포인터로는 <strong>부모가 가진 멤버만 접근</strong> 할 수 있다. 자식 고유의 <code>bark()</code>는 호출할 수 없다.</p>
<p>업캐스팅은 <strong>암시적(자동)</strong> 으로 일어난다. 캐스팅 연산자를 쓸 필요가 없다.</p>
<h3>부모 → 자식 (다운캐스팅)</h3>
<p>반대로 부모 타입을 자식 타입으로 변환하는 것을 <strong>다운캐스팅(Downcasting)</strong> 이라 한다. 이것은 <strong>위험할 수 있어서 명시적 캐스팅이 필요</strong> 하다.</p>
<pre><code class="language-cpp">Animal *pAnimal = new Dog();       // 업캐스팅
Dog *pDog = (Dog *)pAnimal;       // 다운캐스팅 — 명시적 캐스팅 필요
pDog-&gt;bark();                      // OK — 실제로 Dog 객체이므로 안전
</code></pre>
<p>위 경우는 <code>pAnimal</code>이 실제로 <code>Dog</code> 객체를 가리키고 있으므로 안전하다. 하지만 실제로 <code>Animal</code> 객체를 가리키는 포인터를 <code>Dog *</code>로 다운캐스팅하면 존재하지 않는 멤버에 접근하게 되어 프로그램이 비정상 동작한다.</p>
<pre><code class="language-cpp">Animal *pAnimal = new Animal();
Dog *pDog = (Dog *)pAnimal;  // 위험! 실제로는 Animal 객체
pDog-&gt;bark();                 // 정의되지 않은 동작
</code></pre>
<p>안전한 다운캐스팅을 위해 C++에서는 <code>dynamic_cast</code>를 제공한다. 실제 타입이 맞지 않으면 포인터의 경우 <code>nullptr</code>을, 레퍼런스의 경우 예외를 반환한다.</p>
<pre><code class="language-cpp">Animal *pAnimal = new Dog();
Dog *pDog = dynamic_cast&lt;Dog *&gt;(pAnimal);

if (pDog != nullptr) {
    pDog-&gt;bark();  // 안전하게 호출
} else {
    cout &lt;&lt; "변환 실패" &lt;&lt; endl;
}
</code></pre>
<p><code>dynamic_cast</code>는 런타임에 실제 타입을 검사하므로, 부모 클래스에 <strong>최소 하나의</strong> <code>virtual</code> <strong>함수</strong> 가 있어야 사용할 수 있다.</p>
<h3>객체 슬라이싱</h3>
<p>업캐스팅을 포인터나 레퍼런스가 아닌 <strong>값으로</strong> 하면 문제가 생긴다.</p>
<pre><code class="language-cpp">Dog dog;
Animal a = dog;  // 객체 슬라이싱 발생!
</code></pre>
<p><code>Dog</code> 객체를 <code>Animal</code> 변수에 값으로 대입하면, <code>Dog</code>에만 있는 멤버가 잘려나간다. <code>Animal</code> 크기만큼만 복사되기 때문이다. 이것을 <strong>객체 슬라이싱(Object Slicing)</strong> 이라 한다. 그래서 다형성을 활용할 때는 반드시 <strong>포인터 또는 레퍼런스</strong> 를 사용해야 한다.</p>
<hr />
<h2>2. 다형성</h2>
<p>다형성(Polymorphism)은 <strong>같은 인터페이스가 상황에 따라 다르게 동작하는 것</strong> 이다. C++에서 다형성을 구현하는 두 가지 방법이 바로 <strong>오버로딩</strong> 과 <strong>오버라이딩</strong> 이다.</p>
<h3>2-1. 오버로딩이 보여주는 다형성</h3>
<p>오버로딩은 <strong>같은 이름의 함수가 매개변수에 따라 다르게 동작</strong> 하는 것이다. 함수 이름이라는 하나의 인터페이스 뒤에 여러 구현이 숨어있다.</p>
<pre><code class="language-cpp">void print(int x) {
    cout &lt;&lt; "정수: " &lt;&lt; x &lt;&lt; endl;
}

void print(double x) {
    cout &lt;&lt; "실수: " &lt;&lt; x &lt;&lt; endl;
}

void print(string x) {
    cout &lt;&lt; "문자열: " &lt;&lt; x &lt;&lt; endl;
}

int main() {
    print(42);          // 정수: 42
    print(3.14);        // 실수: 3.14
    print("Hello");     // 문자열: Hello
    return 0;
}
</code></pre>
<p><code>print</code>라는 같은 이름을 호출하지만, 넘기는 인자의 타입에 따라 다른 함수가 실행된다. 이 결정은 <strong>컴파일 타임</strong> 에 이루어진다. 컴파일러가 인자의 타입을 보고 어떤 함수를 호출할지 미리 결정하기 때문이다.</p>
<h3>2-2. 오버라이딩이 보여주는 다형성</h3>
<p>오버라이딩은 <strong>부모의 함수를 자식이 재정의하여, 실제 객체에 따라 다르게 동작</strong> 하는 것이다.</p>
<pre><code class="language-cpp">class Shape {
public:
    virtual void draw() {
        cout &lt;&lt; "도형을 그린다" &lt;&lt; endl;
    }
};

class Circle : public Shape {
public:
    void draw() override {
        cout &lt;&lt; "원을 그린다" &lt;&lt; endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout &lt;&lt; "사각형을 그린다" &lt;&lt; endl;
    }
};

int main() {
    Shape *shapes[3];
    shapes[0] = new Shape();
    shapes[1] = new Circle();
    shapes[2] = new Rectangle();

    for (int i = 0; i &lt; 3; i++) {
        shapes[i]-&gt;draw();  // 실제 객체에 따라 다른 함수 호출
    }
    // 출력:
    // 도형을 그린다
    // 원을 그린다
    // 사각형을 그린다

    for (int i = 0; i &lt; 3; i++) {
        delete shapes[i];
    }
    return 0;
}
</code></pre>
<p><code>shapes</code> 배열은 전부 <code>Shape *</code> 타입이지만, 실제 가리키는 객체가 <code>Circle</code>인지 <code>Rectangle</code>인지에 따라 다른 <code>draw()</code>가 호출된다. 이 결정은 <strong>런타임</strong> 에 이루어진다. 프로그램이 실행되면서 실제 객체의 타입을 확인하고 적절한 함수를 호출하기 때문이다.</p>
<p>오버로딩과 오버라이딩 모두 "같은 이름, 다른 동작"이라는 다형성의 본질을 보여주지만, 결정 시점이 다르다. 이 차이를 더 정확하게 설명하는 개념이 바로 <strong>정적 결합</strong> 과 <strong>동적 결합</strong> 이다.</p>
<hr />
<h2>3. 동적 결합과 정적 결합</h2>
<p>"결합(Binding)"이란 함수 호출 코드가 <strong>실제로 실행될 함수의 주소와 연결되는 것</strong> 을 말한다. 이 연결이 언제 이루어지느냐에 따라 정적 결합과 동적 결합으로 나뉜다.</p>
<h3>3-1. 정적 결합 (Static Binding)</h3>
<p>정적 결합은 <strong>컴파일 타임</strong> 에 호출할 함수가 결정되는 것이다. <strong>Early Binding</strong> 이라고도 한다.</p>
<p>일반 함수 호출, 오버로딩된 함수 호출, 그리고 <code>virtual</code>이 아닌 멤버함수 호출이 여기에 해당한다.</p>
<pre><code class="language-cpp">class Animal {
public:
    void speak() {  // virtual 아님
        cout &lt;&lt; "..." &lt;&lt; endl;
    }
};

class Dog : public Animal {
public:
    void speak() {  // 재정의했지만 virtual이 아님
        cout &lt;&lt; "멍멍!" &lt;&lt; endl;
    }
};

int main() {
    Dog dog;
    Animal *p = &amp;dog;  // 업캐스팅
    p-&gt;speak();         // "..." 출력 — Animal의 speak()이 호출됨!
    return 0;
}
</code></pre>
<p><code>p</code>의 타입은 <code>Animal *</code>이다. <code>speak()</code>에 <code>virtual</code>이 없으므로, 컴파일러는 포인터의 타입만 보고 <code>Animal::speak()</code>를 호출하기로 <strong>컴파일 시점에 결정</strong> 한다. 실제로 <code>Dog</code> 객체를 가리키고 있어도 무관하다.</p>
<p>정적 결합은 빠르다. 컴파일 타임에 함수 주소가 확정되므로 런타임에 추가적인 탐색 비용이 없다.</p>
<h3>3-2. 동적 결합 (Dynamic Binding)</h3>
<p>동적 결합은 <strong>런타임</strong> 에 호출할 함수가 결정되는 것이다. <strong>Late Binding</strong> 이라고도 한다. <code>virtual</code> 키워드가 이것을 가능하게 한다.</p>
<pre><code class="language-cpp">class Animal {
public:
    virtual void speak() {  // virtual 함수
        cout &lt;&lt; "..." &lt;&lt; endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout &lt;&lt; "멍멍!" &lt;&lt; endl;
    }
};

int main() {
    Dog dog;
    Animal *p = &amp;dog;
    p-&gt;speak();  // "멍멍!" 출력 — Dog의 speak()이 호출됨!
    return 0;
}
</code></pre>
<p><code>virtual</code>을 붙이면 컴파일러는 포인터의 타입이 아니라 <strong>실제 객체의 타입</strong> 을 기준으로 함수를 호출한다. <code>p</code>의 타입은 <code>Animal *</code>이지만 실제로 <code>Dog</code> 객체를 가리키고 있으므로 <code>Dog::speak()</code>가 호출된다.</p>
<p>이것이 가능한 이유는 <strong>가상 함수 테이블(vtable)</strong> 때문이다. <code>virtual</code> 함수가 있는 클래스는 내부적으로 vtable이라는 함수 포인터 배열을 가진다. 각 객체는 자신의 클래스에 맞는 vtable을 가리키는 포인터(vptr)를 숨겨진 멤버로 가지고 있다. 런타임에 <code>p-&gt;speak()</code>를 호출하면 vptr을 통해 vtable을 찾고, vtable에서 적절한 함수 주소를 가져와 호출한다.</p>
<pre><code class="language-plaintext">Animal vtable                Dog vtable
┌──────────────┐            ┌──────────────┐
│ Animal::speak │            │ Dog::speak   │
└──────────────┘            └──────────────┘

Animal 객체                  Dog 객체
┌──────┐                    ┌──────┐
│ vptr ──→ Animal vtable    │ vptr ──→ Dog vtable
│ ...  │                    │ ...  │
└──────┘                    └──────┘
</code></pre>
<p>동적 결합은 vtable 탐색이라는 간접 비용이 있어서 정적 결합보다 약간 느리다. 하지만 이 비용은 매우 작고, 다형성이 주는 유연함에 비하면 무시할 수 있는 수준이다.</p>
<h3>정적 결합과 동적 결합 비교</h3>
<p>핵심 차이를 정리하면 이렇다.</p>
<p><strong>정적 결합</strong> 은 컴파일 타임에 결정되고, <code>virtual</code> 없는 함수와 오버로딩에 적용된다. 포인터/레퍼런스의 <strong>선언된 타입</strong> 을 기준으로 함수를 선택한다.</p>
<p><strong>동적 결합</strong> 은 런타임에 결정되고, <code>virtual</code> 함수와 오버라이딩에 적용된다. 포인터/레퍼런스가 가리키는 <strong>실제 객체의 타입</strong> 을 기준으로 함수를 선택한다.</p>
<p>다형성을 제대로 활용하려면 <code>virtual</code> 키워드가 필수다. <code>virtual</code> 없이 자식에서 같은 이름의 함수를 정의하면 재정의가 아니라 <strong>함수 숨김(hiding)</strong> 이 되어, 의도한 대로 동작하지 않는다.</p>
<h3>가상 소멸자</h3>
<p>동적 결합에서 중요한 실전 규칙이 하나 있다. 부모 클래스의 소멸자에는 <strong>반드시</strong> <code>virtual</code><strong>을 붙여야</strong> 한다.</p>
<pre><code class="language-cpp">class Animal {
public:
    virtual ~Animal() {  // 가상 소멸자
        cout &lt;&lt; "Animal 소멸" &lt;&lt; endl;
    }
};

class Dog : public Animal {
private:
    int *data;

public:
    Dog() {
        data = new int[100];
    }

    ~Dog() {
        delete[] data;
        cout &lt;&lt; "Dog 소멸" &lt;&lt; endl;
    }
};

int main() {
    Animal *p = new Dog();
    delete p;  // Dog 소멸자 → Animal 소멸자 순서로 호출
    return 0;
}
</code></pre>
<p>소멸자에 <code>virtual</code>이 없으면 <code>delete p</code>할 때 <code>Animal</code>의 소멸자만 호출되고 <code>Dog</code>의 소멸자는 호출되지 않는다. <code>Dog</code>이 동적 할당한 <code>data</code> 메모리가 해제되지 않아 <strong>메모리 누수</strong> 가 발생한다. <code>virtual</code>을 붙이면 실제 객체의 소멸자(<code>Dog</code>)가 먼저 호출되고, 이어서 부모의 소멸자(<code>Animal</code>)가 자동으로 호출된다.</p>
<p>상속을 사용할 가능성이 있는 클래스라면, 소멸자에 <code>virtual</code>을 붙이는 것을 습관으로 만들어야 한다.</p>
<hr />
<h2>4. 추상 클래스</h2>
<h3>4-1. 추상 클래스란?</h3>
<p>추상 클래스는 <strong>순수 가상 함수(Pure Virtual Function)를 하나 이상 가진 클래스</strong> 다. 직접 객체를 만들 수 없고, 반드시 자식 클래스에서 상속받아 사용해야 한다.</p>
<p>순수 가상 함수는 <strong>본체(구현)가 없는 가상 함수</strong> 다. 선언부 끝에 <code>= 0</code>을 붙여서 표시한다.</p>
<pre><code class="language-cpp">class Shape {
public:
    virtual void draw() = 0;      // 순수 가상 함수
    virtual double area() = 0;    // 순수 가상 함수

    virtual ~Shape() {}
};
</code></pre>
<p><code>draw()</code>와 <code>area()</code>에 <code>= 0</code>이 붙어있다. "이 함수는 여기서 구현하지 않겠다. 자식 클래스가 반드시 구현해라"는 뜻이다.</p>
<pre><code class="language-cpp">// Shape s;  // 에러! 추상 클래스는 객체 생성 불가
</code></pre>
<p>추상 클래스는 직접 인스턴스화할 수 없다. "도형"이라는 것은 추상적인 개념이지, 실제로 존재하는 구체적인 형태가 아니기 때문이다. 실제 존재하는 것은 원, 사각형, 삼각형 같은 구체적인 도형이다.</p>
<h3>4-2. 추상 클래스의 사용</h3>
<p>자식 클래스에서 순수 가상 함수를 <strong>전부 구현하면</strong> 그 자식 클래스는 객체를 만들 수 있다.</p>
<pre><code class="language-cpp">class Circle : public Shape {
private:
    double radius;

public:
    Circle(double r) : radius(r) {}

    void draw() override {
        cout &lt;&lt; "원을 그린다 (반지름: " &lt;&lt; radius &lt;&lt; ")" &lt;&lt; endl;
    }

    double area() override {
        return 3.14159 * radius * radius;
    }
};

class Rectangle : public Shape {
private:
    double width, height;

public:
    Rectangle(double w, double h) : width(w), height(h) {}

    void draw() override {
        cout &lt;&lt; "사각형을 그린다 (" &lt;&lt; width &lt;&lt; " x " &lt;&lt; height &lt;&lt; ")" &lt;&lt; endl;
    }

    double area() override {
        return width * height;
    }
};
</code></pre>
<p>만약 자식 클래스에서 순수 가상 함수를 하나라도 구현하지 않으면, 그 자식 클래스도 추상 클래스가 되어 객체를 만들 수 없다.</p>
<h3>4-3. 추상 클래스의 힘</h3>
<p>추상 클래스의 진짜 가치는 <strong>부모 타입 포인터로 여러 자식 객체를 통일적으로 다룰 수 있다</strong> 는 것이다.</p>
<pre><code class="language-cpp">int main() {
    Shape *shapes[3];
    shapes[0] = new Circle(5.0);
    shapes[1] = new Rectangle(4.0, 6.0);
    shapes[2] = new Circle(3.0);

    double totalArea = 0;
    for (int i = 0; i &lt; 3; i++) {
        shapes[i]-&gt;draw();
        totalArea += shapes[i]-&gt;area();
    }

    cout &lt;&lt; "전체 면적: " &lt;&lt; totalArea &lt;&lt; endl;

    for (int i = 0; i &lt; 3; i++) {
        delete shapes[i];
    }
    return 0;
}
</code></pre>
<p><code>Shape *</code> 배열 하나로 <code>Circle</code>이든 <code>Rectangle</code>이든 상관없이 <code>draw()</code>와 <code>area()</code>를 호출할 수 있다. 새로운 도형(<code>Triangle</code>, <code>Ellipse</code> 등)을 추가하더라도 <code>Shape</code>을 상속받고 순수 가상 함수를 구현하기만 하면 된다. 기존 코드를 수정할 필요가 없다.</p>
<p>이것이 바로 추상 클래스가 <strong>인터페이스 역할</strong> 을 하는 것이다. "이 클래스를 상속받으면 반드시 이 함수들을 구현해야 한다"는 계약을 강제한다. JavaScript나 Java의 인터페이스와 비슷한 역할이다. 다만 C++에는 <code>interface</code> 키워드가 따로 없고, 순수 가상 함수만으로 이루어진 추상 클래스가 그 역할을 한다.</p>
<h3>4-4. 일반 가상 함수와 순수 가상 함수의 차이</h3>
<p><strong>일반 가상 함수(</strong><code>virtual void draw() { ... }</code><strong>)</strong> 는 기본 구현이 있다. 자식이 재정의하지 않으면 부모의 구현이 사용된다. 부모 클래스의 객체를 만들 수 있다.</p>
<p><strong>순수 가상 함수(</strong><code>virtual void draw() = 0</code><strong>)</strong> 는 기본 구현이 없다. 자식이 반드시 구현해야 한다. 부모 클래스는 추상 클래스가 되어 객체를 만들 수 없다.</p>
<p>기본 동작이 의미 있는 경우에는 일반 가상 함수를 쓰고, 자식마다 반드시 다르게 동작해야 하는 경우에는 순수 가상 함수를 쓴다.</p>
<hr />
<h2>마무리</h2>
<p>부모·자식 사이의 타입 변환(업캐스팅/다운캐스팅)이 다형성의 토대가 되고, <code>virtual</code> 키워드가 동적 결합을 가능하게 하여 런타임 다형성을 실현한다. 추상 클래스는 이 다형성을 설계 수준에서 강제하는 도구다. "순수 가상 함수를 반드시 구현하라"는 계약을 통해, 서로 다른 자식 클래스들을 하나의 인터페이스로 통일적으로 다룰 수 있게 해준다. 이 개념들은 C++뿐 아니라 대부분의 객체지향 언어에서 핵심적으로 사용되므로, 확실히 이해해두면 다른 언어로 넘어갈 때도 큰 도움이 된다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (9)]]></title><description><![CDATA[1. new/delete 연산자와 동적 메모리
1-1. 동적 메모리의 필요성
동적 메모리는 실행시간(런타임)에 할당되어 사용되는 메모리 블록 이다. 프로그램이 돌아가는 도중에 필요한 만큼 메모리를 확보하고, 다 쓰면 반환하는 방식이다.
동적 메모리의 반대는 정적 메모리 다. 정적 메모리는 컴파일 타임에 크기가 결정되는 메모리로, 일반 변수나 배열이 여기에 ]]></description><link>https://blog.chamdom.dev/c-9</link><guid isPermaLink="true">https://blog.chamdom.dev/c-9</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:31:03 GMT</pubDate><content:encoded><![CDATA[<h2>1. new/delete 연산자와 동적 메모리</h2>
<h3>1-1. 동적 메모리의 필요성</h3>
<p>동적 메모리는 <strong>실행시간(런타임)에 할당되어 사용되는 메모리 블록</strong> 이다. 프로그램이 돌아가는 도중에 필요한 만큼 메모리를 확보하고, 다 쓰면 반환하는 방식이다.</p>
<p>동적 메모리의 반대는 <strong>정적 메모리</strong> 다. 정적 메모리는 컴파일 타임에 크기가 결정되는 메모리로, 일반 변수나 배열이 여기에 해당한다. <code>int arr[100]</code>은 컴파일 시점에 400바이트가 필요하다는 것이 확정된다.</p>
<p>그런데 프로그램을 작성할 때 <strong>얼마만큼의 메모리가 필요한지 알지 못하는 경우</strong> 가 많다. 사용자가 데이터를 몇 개 입력할지, 파일에 몇 줄이 있는지, 네트워크로 얼마나 큰 데이터가 올지는 프로그램이 실행되어야 알 수 있다. 배열 크기를 넉넉하게 <code>int arr[10000]</code>으로 잡으면 대부분의 메모리가 낭비되고, 그마저도 10000개를 넘으면 부족해진다.</p>
<p>동적 메모리는 이 문제를 해결한다. 필요한 시점에 필요한 만큼만 할당하고, 다 쓰면 해제하면 된다.</p>
<p>동적 메모리가 할당되는 영역을 <strong>힙(Heap) 영역</strong> 이라 한다. C에서 <code>malloc</code>과 <code>free</code>로 힙을 관리했던 것처럼, C++에서는 <code>new</code><strong>로 생성하고</strong> <code>delete</code><strong>로 소멸</strong> 시킨다.</p>
<h3>1-2. new와 delete</h3>
<p>C에서는 <code>malloc</code>과 <code>free</code>를 사용했다. C++에서도 쓸 수는 있지만, C++에는 더 나은 방법이 있다.</p>
<pre><code class="language-cpp">// C 방식
int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);

// C++ 방식
int *p = new int;
*p = 10;
delete p;
</code></pre>
<p><code>new</code>는 <code>malloc</code>보다 간결하다. <code>sizeof</code>를 쓸 필요도 없고, 타입캐스팅도 필요 없다. <code>new int</code>라고 쓰면 <code>int</code> 크기만큼 힙에 메모리를 할당하고 그 주소를 반환한다.</p>
<p>선언과 동시에 초기화할 수도 있다.</p>
<pre><code class="language-cpp">int *p = new int(42);      // 42로 초기화
double *d = new double(3.14);
</code></pre>
<p>배열을 동적으로 할당할 때는 <code>new[]</code>와 <code>delete[]</code>를 사용한다.</p>
<pre><code class="language-cpp">int *arr = new int[5];     // int 5개 크기의 배열 할당

for (int i = 0; i &lt; 5; i++) {
    arr[i] = (i + 1) * 10;
}

delete[] arr;  // 배열은 반드시 delete[]로 해제
</code></pre>
<p><code>delete</code>와 <code>delete[]</code>를 혼동하면 안 된다. 단일 객체는 <code>delete</code>, 배열은 <code>delete[]</code>다. 배열에 <code>delete</code>를 쓰면 정의되지 않은 동작이 발생한다.</p>
<h3>1-3. new/delete가 malloc/free보다 나은 이유</h3>
<p>C++에서 <code>new</code>/<code>delete</code>를 쓰는 가장 큰 이유는 <strong>생성자와 소멸자가 호출되기 때문</strong> 이다.</p>
<pre><code class="language-cpp">class Student {
private:
    string name;
    int score;

public:
    Student(string n, int s) : name(n), score(s) {
        cout &lt;&lt; name &lt;&lt; " 생성" &lt;&lt; endl;
    }

    ~Student() {
        cout &lt;&lt; name &lt;&lt; " 소멸" &lt;&lt; endl;
    }

    void print() {
        cout &lt;&lt; name &lt;&lt; ": " &lt;&lt; score &lt;&lt; "점" &lt;&lt; endl;
    }
};

int main() {
    Student *p = new Student("홍길동", 90);  // 생성자 호출됨
    p-&gt;print();
    delete p;  // 소멸자 호출됨
    return 0;
}
</code></pre>
<p><code>new</code>는 메모리 할당 + 생성자 호출을 한 번에 처리한다. <code>delete</code>는 소멸자 호출 + 메모리 해제를 한 번에 처리한다. <code>malloc</code>은 메모리만 잡아줄 뿐 생성자를 호출하지 않고, <code>free</code>는 메모리만 해제할 뿐 소멸자를 호출하지 않는다.</p>
<p>객체를 다루는 C++에서는 생성자와 소멸자가 제대로 호출되는 것이 매우 중요하다. 소멸자에서 동적 메모리를 해제하거나, 파일을 닫거나, 자원을 정리하는 코드가 있을 수 있기 때문이다. 그래서 C++에서는 <code>malloc</code>/<code>free</code> 대신 <code>new</code>/<code>delete</code>를 사용한다.</p>
<p>정리하면 <code>new</code>/<code>delete</code>가 <code>malloc</code>/<code>free</code>보다 나은 점은 세 가지다. 타입캐스팅이 불필요하다. <code>sizeof</code> 계산이 불필요하다. 그리고 가장 중요한 것으로, <strong>생성자와 소멸자가 자동 호출</strong> 된다.</p>
<h3>1-4. 동적 메모리와 소멸자</h3>
<p>클래스 내부에서 동적 메모리를 사용하면, 소멸자에서 반드시 해제해야 한다.</p>
<pre><code class="language-cpp">class IntArray {
private:
    int *data;
    int size;

public:
    IntArray(int s) : size(s) {
        data = new int[size];  // 생성자에서 동적 할당
    }

    ~IntArray() {
        delete[] data;  // 소멸자에서 해제
        cout &lt;&lt; "메모리 해제 완료" &lt;&lt; endl;
    }

    int&amp; operator[](int index) {
        return data[index];
    }
};

int main() {
    IntArray arr(5);
    arr[0] = 10;
    arr[1] = 20;
    return 0;
}  // 여기서 arr의 소멸자가 호출되어 data가 해제됨
</code></pre>
<p>생성자에서 <code>new</code>로 할당하고 소멸자에서 <code>delete</code>로 해제하는 패턴은 C++에서 매우 흔하다. 이것을 <strong>RAII(Resource Acquisition Is Initialization)</strong> 패턴이라고 한다. 자원의 획득을 초기화 시점에, 반환을 소멸 시점에 묶어서 관리하는 방식이다.</p>
<hr />
<h2>2. 대입 연산자 오버로딩</h2>
<h3>2-1. 대입 연산자란?</h3>
<p>대입 연산자(<code>=</code>)는 이미 생성된 객체에 다른 객체의 값을 복사하는 연산자다. 생성과 동시에 초기화하는 복사 생성자와는 다르다.</p>
<pre><code class="language-cpp">Student s1("홍길동", 90);
Student s2("김철수", 85);

s2 = s1;  // 대입 연산자 — 이미 존재하는 s2에 s1을 복사
</code></pre>
<pre><code class="language-cpp">Student s3 = s1;  // 이건 복사 생성자! (새 객체 생성 시 초기화)
</code></pre>
<p><code>s2 = s1</code>은 대입 연산자, <code>Student s3 = s1</code>은 복사 생성자다. 모양이 비슷해서 헷갈리지만, 핵심 차이는 <strong>왼쪽 객체가 이미 존재하는지 여부</strong> 다.</p>
<h3>2-2. 기본 대입 연산자의 문제</h3>
<p>대입 연산자를 직접 정의하지 않으면 컴파일러가 <strong>기본 대입 연산자</strong> 를 자동 생성한다. 기본 대입 연산자는 멤버를 하나씩 그대로 복사하는 <strong>얕은 복사(Shallow Copy)</strong> 를 수행한다.</p>
<p>멤버가 <code>int</code>나 <code>double</code> 같은 기본 타입뿐이면 문제없다. 하지만 멤버에 <strong>동적 할당된 포인터</strong> 가 있으면 심각한 문제가 생긴다.</p>
<pre><code class="language-cpp">class MyString {
private:
    char *str;
    int len;

public:
    MyString(const char *s) {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    ~MyString() {
        delete[] str;
    }
};
</code></pre>
<pre><code class="language-cpp">int main() {
    MyString a("Hello");
    MyString b("World");
    b = a;  // 기본 대입 연산자 → 얕은 복사
    return 0;
}
</code></pre>
<p>기본 대입 연산자가 실행되면 <code>b.str = a.str</code>이 된다. 두 객체의 <code>str</code>이 <strong>같은 메모리 주소를 가리킨다.</strong> 이때 두 가지 문제가 발생한다.</p>
<p>첫째, <code>b</code>가 원래 가리키던 <code>"World"</code> 메모리가 <strong>누구도 가리키지 않게 되어 메모리 누수</strong> 가 발생한다.</p>
<p>둘째, 프로그램이 끝나면 <code>a</code>와 <code>b</code>의 소멸자가 각각 호출되면서 <strong>같은 메모리를 두 번 해제(double free)</strong> 한다. 이로 인해 프로그램이 크래시한다.</p>
<pre><code class="language-plaintext">얕은 복사 후 상태:

a.str ──┐
        ├──→ [ H | e | l | l | o | \0 ]
b.str ──┘

b가 원래 가리키던 메모리:
         [ W | o | r | l | d | \0 ]  ← 누구도 안 가리킴 (메모리 누수!)
</code></pre>
<h3>2-3. 대입 연산자 오버로딩</h3>
<p>이 문제를 해결하려면 대입 연산자를 직접 정의해서 <strong>깊은 복사(Deep Copy)</strong> 를 수행해야 한다.</p>
<pre><code class="language-cpp">class MyString {
private:
    char *str;
    int len;

public:
    MyString(const char *s) {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    ~MyString() {
        delete[] str;
    }

    // 대입 연산자 오버로딩
    MyString&amp; operator=(const MyString &amp;other) {
        if (this == &amp;other) {  // 자기 자신 대입 방지
            return *this;
        }

        delete[] str;  // 기존 메모리 해제

        len = other.len;
        str = new char[len + 1];  // 새 메모리 할당
        strcpy(str, other.str);   // 내용 복사

        return *this;
    }
};
</code></pre>
<p>깊은 복사 대입 연산자의 핵심 단계를 하나씩 보면 이렇다.</p>
<p><strong>자기 자신 대입 체크</strong> — <code>a = a</code>처럼 자기 자신을 대입하는 경우를 먼저 걸러낸다. 체크하지 않으면 기존 메모리를 먼저 해제한 뒤 이미 해제된 메모리에서 복사하려는 상황이 생긴다.</p>
<p><strong>기존 메모리 해제</strong> — 대입을 받는 쪽(<code>this</code>)이 이미 가지고 있던 동적 메모리를 먼저 <code>delete</code>한다. 이렇게 해야 메모리 누수가 발생하지 않는다.</p>
<p><strong>새 메모리 할당 후 복사</strong> — 원본과 같은 크기의 새 메모리를 할당하고, 내용을 복사한다. 이러면 두 객체가 각자의 독립적인 메모리를 가진다.</p>
<p><strong>자기 자신의 참조 반환</strong> — <code>return *this</code>로 대입된 객체의 참조를 반환한다. 이렇게 해야 <code>a = b = c</code> 같은 <strong>연쇄 대입</strong> 이 가능해진다.</p>
<pre><code class="language-plaintext">깊은 복사 후 상태:

a.str ──→ [ H | e | l | l | o | \0 ]
b.str ──→ [ H | e | l | l | o | \0 ]  ← 별도의 새 메모리

각 객체가 독립적인 메모리를 가지므로 double free 문제 없음
</code></pre>
<p>JavaScript에서 객체를 <code>=</code>로 대입하면 참조만 복사되는 것(얕은 복사)과 비슷한 문제다. JS에서 스프레드 연산자(<code>{...obj}</code>)나 <code>structuredClone</code>으로 깊은 복사를 하는 것처럼, C++에서는 대입 연산자를 오버로딩해서 깊은 복사를 구현하는 것이다.</p>
<h3>2-4. Rule of Three</h3>
<p>동적 메모리를 가진 클래스에서는 다음 세 가지를 반드시 함께 정의해야 한다. 이것을 <strong>Rule of Three</strong> 라고 한다.</p>
<p><strong>소멸자</strong> — 동적 메모리를 해제한다. <strong>복사 생성자</strong> — 새 객체를 만들 때 깊은 복사를 수행한다. <strong>대입 연산자</strong> — 기존 객체에 대입할 때 깊은 복사를 수행한다.</p>
<p>이 중 하나라도 빠지면 얕은 복사로 인한 문제가 발생할 수 있다. 세 가지는 항상 세트로 생각해야 한다.</p>
<pre><code class="language-cpp">class MyString {
public:
    MyString(const char *s);                    // 생성자
    ~MyString();                                // 소멸자
    MyString(const MyString &amp;other);            // 복사 생성자
    MyString&amp; operator=(const MyString &amp;other); // 대입 연산자
};
</code></pre>
<p>셋 중 하나를 직접 정의해야 할 상황이라면, 나머지 둘도 반드시 정의해야 한다는 규칙이다. 컴파일러가 자동 생성하는 기본 버전은 전부 얕은 복사를 하므로, 동적 메모리가 있는 클래스에서는 기본 버전에 의존하면 안 된다.</p>
<hr />
<h2>마무리</h2>
<p><code>new</code>/<code>delete</code>는 C++에서 동적 메모리를 관리하는 핵심 도구다. <code>malloc</code>/<code>free</code>와 달리 생성자와 소멸자를 자동 호출해주므로, 객체의 생명주기를 제대로 관리할 수 있다. 그리고 동적 메모리를 가진 객체를 복사하거나 대입할 때는 반드시 깊은 복사를 구현해야 한다. 대입 연산자 오버로딩은 그 핵심 방법이고, 소멸자, 복사 생성자와 함께 Rule of Three로 묶어서 관리하는 것이 안전하다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (8)]]></title><description><![CDATA[1. 객체 배열
1-1. 객체 배열이란?
C에서 int arr[5]로 정수 5개를 묶었듯이, C++에서는 객체 여러 개를 배열로 묶을 수 있다. 학생 100명의 정보를 관리해야 한다면 Student 객체를 100개 일일이 선언하는 대신 배열로 만들면 된다.
1-2. 객체 배열의 선언 형태
class Student {
private:
    string na]]></description><link>https://blog.chamdom.dev/c-8</link><guid isPermaLink="true">https://blog.chamdom.dev/c-8</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:28:37 GMT</pubDate><content:encoded><![CDATA[<h2>1. 객체 배열</h2>
<h3>1-1. 객체 배열이란?</h3>
<p>C에서 <code>int arr[5]</code>로 정수 5개를 묶었듯이, C++에서는 <strong>객체 여러 개를 배열로 묶을 수 있다.</strong> 학생 100명의 정보를 관리해야 한다면 <code>Student</code> 객체를 100개 일일이 선언하는 대신 배열로 만들면 된다.</p>
<h3>1-2. 객체 배열의 선언 형태</h3>
<pre><code class="language-cpp">class Student {
private:
    string name;
    int score;

public:
    Student() : name("없음"), score(0) {}
    Student(string n, int s) : name(n), score(s) {}

    void print() {
        cout &lt;&lt; name &lt;&lt; ": " &lt;&lt; score &lt;&lt; "점" &lt;&lt; endl;
    }
};

int main() {
    Student students[3];  // 기본 생성자가 3번 호출됨
    return 0;
}
</code></pre>
<p>객체 배열을 선언하면 각 요소마다 <strong>기본 생성자(매개변수 없는 생성자)</strong> 가 호출된다. 기본 생성자가 없으면 컴파일 에러가 발생하므로, 객체 배열을 사용하려면 반드시 기본 생성자를 정의해야 한다.</p>
<p>초기화 리스트로 선언과 동시에 값을 넣을 수도 있다.</p>
<pre><code class="language-cpp">Student students[3] = {
    Student("홍길동", 90),
    Student("김철수", 85),
    Student("이영희", 95)
};

for (int i = 0; i &lt; 3; i++) {
    students[i].print();
}
</code></pre>
<p>배열의 각 요소는 독립적인 객체다. <code>students[0]</code>, <code>students[1]</code>, <code>students[2]</code> 각각이 자신만의 <code>name</code>과 <code>score</code>를 가진다. 일반 배열과 마찬가지로 인덱스는 0부터 시작하고, 점(<code>.</code>) 연산자로 멤버에 접근한다.</p>
<p>동적으로 객체 배열을 만들 수도 있다. 이때는 <code>new[]</code>로 할당하고 <code>delete[]</code>로 해제한다.</p>
<pre><code class="language-cpp">Student *students = new Student[5];  // 기본 생성자 5번 호출

for (int i = 0; i &lt; 5; i++) {
    students[i].print();
}

delete[] students;  // 반드시 delete[]로 해제
</code></pre>
<p><code>delete</code>가 아니라 <code>delete[]</code>를 써야 한다. <code>delete[]</code>는 배열의 각 객체에 대해 소멸자를 호출한 뒤 메모리를 해제한다. <code>delete</code>를 쓰면 첫 번째 객체의 소멸자만 호출되어 메모리 누수나 정의되지 않은 동작이 발생한다.</p>
<hr />
<h2>2. 객체 포인터</h2>
<h3>2-1. 객체 포인터란?</h3>
<p>객체 포인터는 <strong>객체의 메모리 주소를 저장하는 포인터</strong> 다. C에서 구조체 포인터를 사용했던 것과 같은 개념이다.</p>
<pre><code class="language-cpp">Student s1("홍길동", 90);
Student *p = &amp;s1;

p-&gt;print();        // 화살표 연산자로 멤버 접근
(*p).print();      // 역참조 후 점 연산자 (같은 의미)
</code></pre>
<p>구조체 포인터에서 배웠던 것처럼, 포인터로 멤버에 접근할 때는 <strong>화살표(</strong><code>-&gt;</code><strong>) 연산자</strong> 를 사용한다. <code>p-&gt;print()</code>는 <code>(*p).print()</code>와 같다.</p>
<p><code>new</code>로 힙에 객체를 동적 생성하면 포인터로 받아야 한다.</p>
<pre><code class="language-cpp">Student *p = new Student("홍길동", 90);
p-&gt;print();
delete p;  // 사용 후 반드시 해제
</code></pre>
<p>객체 포인터 배열을 사용하면 각 객체를 독립적으로 동적 생성하고, 다형성을 활용할 수도 있다.</p>
<pre><code class="language-cpp">Student *students[3];
students[0] = new Student("홍길동", 90);
students[1] = new Student("김철수", 85);
students[2] = new Student("이영희", 95);

for (int i = 0; i &lt; 3; i++) {
    students[i]-&gt;print();
    delete students[i];
}
</code></pre>
<hr />
<h2>3. this 포인터</h2>
<h3>3-1. this 포인터가 필요한 이유</h3>
<p>클래스의 멤버함수는 여러 객체가 공유한다. <code>Student</code> 객체가 100개 있어도 <code>print</code> 함수의 코드는 메모리에 <strong>하나만</strong> 존재한다. 그러면 <code>print</code> 함수가 호출될 때 "지금 나를 호출한 객체가 누구인지"를 어떻게 알까?</p>
<p>바로 <strong>this 포인터</strong> 가 그 역할을 한다. <code>this</code>는 <strong>자기 자신(현재 객체)을 가리키는 포인터</strong> 로, 멤버함수가 호출될 때 컴파일러가 자동으로 전달한다.</p>
<pre><code class="language-cpp">class Student {
private:
    string name;
    int score;

public:
    void setName(string name) {
        this-&gt;name = name;  // this-&gt;name은 멤버, name은 매개변수
    }
};
</code></pre>
<p>매개변수 이름과 멤버변수 이름이 같을 때 <code>this</code>가 특히 유용하다. <code>this-&gt;name</code>은 "이 객체의 <code>name</code> 멤버"를 명확히 가리키고, 그냥 <code>name</code>은 매개변수를 가리킨다. <code>this</code>가 없으면 둘 다 매개변수를 가리키게 되어 멤버변수에 값이 대입되지 않는다.</p>
<p>JavaScript의 <code>this</code>와 개념적으로 같다. JS에서 <code>this.name = name</code> 하는 것과 C++에서 <code>this-&gt;name = name</code> 하는 것은 같은 맥락이다. 다만 JS의 <code>this</code>는 호출 방식에 따라 바인딩이 달라지는 반면, C++의 <code>this</code>는 항상 해당 멤버함수를 호출한 객체를 가리킨다.</p>
<p><code>this</code>는 포인터이므로 <code>this-&gt;</code> 또는 <code>(*this).</code>으로 멤버에 접근한다. <code>*this</code>는 객체 자체를 의미하며, 메서드 체이닝에 활용할 수 있다.</p>
<pre><code class="language-cpp">class Builder {
private:
    int width;
    int height;

public:
    Builder() : width(0), height(0) {}

    Builder&amp; setWidth(int w) {
        width = w;
        return *this;  // 자기 자신을 반환
    }

    Builder&amp; setHeight(int h) {
        height = h;
        return *this;
    }
};

int main() {
    Builder b;
    b.setWidth(100).setHeight(200);  // 메서드 체이닝
    return 0;
}
</code></pre>
<p><code>return *this</code>로 자기 자신의 참조를 반환하면, 반환된 객체에서 다시 메서드를 호출할 수 있다. JavaScript에서 jQuery의 <code>$('#id').css('color', 'red').show()</code> 체이닝과 같은 원리다.</p>
<hr />
<h2>4. 전달인자가 객체인 함수</h2>
<h3>4-1. 객체 전달 방식</h3>
<p>함수에 객체를 넘기는 방법은 크게 <strong>값 전달</strong>, <strong>포인터 전달</strong>, <strong>레퍼런스 전달</strong> 세 가지가 있다.</p>
<h3>4-2. 객체에 대한 값 전달 방식</h3>
<p>값으로 전달하면 객체의 <strong>복사본</strong> 이 만들어진다. C에서 구조체를 함수에 넘길 때와 같은 원리다.</p>
<pre><code class="language-cpp">void printStudent(Student s) {  // 복사본이 생성됨
    s.print();
}

int main() {
    Student s1("홍길동", 90);
    printStudent(s1);  // s1의 복사본이 함수에 전달됨
    return 0;
}
</code></pre>
<p>함수 안에서 <code>s</code>를 수정해도 원본 <code>s1</code>에는 영향이 없다. 하지만 문제가 있다.</p>
<p><strong>복사 비용이 크다.</strong> 객체가 크면(멤버가 많거나 문자열 등을 포함하면) 복사하는 데 시간과 메모리가 든다.</p>
<p><strong>복사 생성자가 호출된다.</strong> 값으로 넘기면 복사 생성자가 호출되어 새 객체가 만들어지고, 함수가 끝나면 소멸자가 호출된다. 동적 메모리를 가진 객체라면 깊은 복사/얕은 복사 문제까지 신경 써야 한다.</p>
<h3>4-3. 레퍼런스 형식</h3>
<p>레퍼런스(참조)로 전달하면 복사 없이 <strong>원본 객체를 직접 사용</strong> 한다. 포인터와 비슷하지만 문법이 더 깔끔하다.</p>
<pre><code class="language-cpp">void printStudent(const Student &amp;s) {  // 복사 없이 원본 참조
    s.print();
}
</code></pre>
<p><code>&amp;</code>를 붙이면 레퍼런스로 전달된다. 내부적으로는 포인터와 비슷하게 동작하지만, 사용할 때는 일반 객체처럼 <code>.</code>으로 멤버에 접근한다. <code>-&gt;</code> 연산자를 쓸 필요가 없다.</p>
<p><code>const</code>를 붙이면 함수 안에서 객체를 수정할 수 없다. 읽기만 하는 함수에서는 <code>const &amp;</code>로 받는 것이 가장 효율적이고 안전하다.</p>
<p>원본을 수정해야 하는 경우에는 <code>const</code> 없이 레퍼런스로 받는다.</p>
<pre><code class="language-cpp">void addBonus(Student &amp;s, int bonus) {  // 원본 수정 가능
    s.addScore(bonus);
}
</code></pre>
<p>정리하면, 객체를 함수에 넘길 때는 <strong>값 전달보다 레퍼런스 전달이 낫다.</strong> 수정이 필요 없으면 <code>const &amp;</code>로, 수정이 필요하면 <code>&amp;</code>로 받는다. 값 전달은 불필요한 복사가 발생하므로 특별한 이유가 없는 한 피하는 것이 좋다.</p>
<p>포인터로 전달하는 것도 가능하지만, C++에서는 레퍼런스가 더 자연스러운 선택이다. 포인터는 <code>NULL</code>이 될 수 있고 <code>-&gt;</code> 연산자를 써야 하지만, 레퍼런스는 반드시 유효한 객체를 가리키고 <code>.</code>으로 접근할 수 있기 때문이다.</p>
<hr />
<h2>5. const 멤버함수와 const 객체</h2>
<h3>5-1. const 멤버함수</h3>
<p><code>const</code> 멤버함수는 <strong>객체의 멤버변수를 수정하지 않겠다고 약속하는 함수</strong> 다. 함수 선언 뒤에 <code>const</code>를 붙인다.</p>
<pre><code class="language-cpp">class Student {
private:
    string name;
    int score;

public:
    Student(string n, int s) : name(n), score(s) {}

    void print() const {  // const 멤버함수
        cout &lt;&lt; name &lt;&lt; ": " &lt;&lt; score &lt;&lt; "점" &lt;&lt; endl;
    }

    void setScore(int s) {  // 일반 멤버함수 (멤버 수정)
        score = s;
    }
};
</code></pre>
<p><code>print()</code> 뒤에 <code>const</code>가 붙어있다. 이 함수 안에서는 멤버변수의 값을 바꿀 수 없다. 만약 <code>const</code> 함수 안에서 멤버를 수정하려 하면 컴파일 에러가 발생한다.</p>
<h4>읽기 전용 함수</h4>
<p><code>const</code> 멤버함수는 곧 <strong>읽기 전용 함수</strong> 다. 값을 읽기만 하고 변경하지 않는 함수에는 <code>const</code>를 붙이는 것이 좋다. <code>getScore()</code>, <code>print()</code>, <code>getName()</code> 같은 함수들이 여기에 해당한다.</p>
<p><code>const</code>를 붙이는 것이 중요한 이유는 <code>const</code> 객체 때문이다. <code>const</code>로 선언된 객체는 <code>const</code> <strong>멤버함수만 호출할 수 있다.</strong></p>
<pre><code class="language-cpp">const Student s1("홍길동", 90);
s1.print();        // OK — const 멤버함수
// s1.setScore(95);  // 에러! — const 객체에서 non-const 함수 호출 불가
</code></pre>
<p>함수 매개변수로 <code>const &amp;</code>를 받을 때도 마찬가지다. <code>const</code>로 받은 객체에서는 <code>const</code> 멤버함수만 호출할 수 있다.</p>
<pre><code class="language-cpp">void display(const Student &amp;s) {
    s.print();        // OK — print()이 const 함수이므로
    // s.setScore(100);  // 에러!
}
</code></pre>
<p><code>print()</code> 함수에 <code>const</code>를 붙이지 않았다면, <code>const &amp;</code>로 받은 객체에서 <code>print()</code>를 호출할 수 없다. 실제로 값을 수정하지 않더라도 컴파일러는 <code>const</code>가 없으면 수정할 수도 있다고 판단하기 때문이다. 읽기 전용 함수에는 반드시 <code>const</code>를 붙이는 습관을 들이는 것이 좋다.</p>
<hr />
<h2>6. static 멤버</h2>
<h3>6-1. 은행 예금 계좌의 예</h3>
<p>은행 시스템을 생각해보자. 계좌 객체가 여러 개 있는데, "총 계좌 수"나 "전체 이자율"같은 정보는 어디에 저장해야 할까?</p>
<pre><code class="language-cpp">class Account {
private:
    string owner;
    int balance;

public:
    Account(string name, int money)
        : owner(name), balance(money) {}
};
</code></pre>
<p><code>owner</code>와 <code>balance</code>는 계좌마다 다르다. 하지만 "총 계좌 수"는 특정 계좌에 속하는 정보가 아니라, 모든 계좌가 공유하는 정보다.</p>
<h3>6-2. 전역변수의 문제점</h3>
<p>간단한 해결책은 전역변수를 쓰는 것이다.</p>
<pre><code class="language-cpp">int totalCount = 0;  // 전역변수

class Account {
public:
    Account(string name, int money) {
        totalCount++;
    }
};
</code></pre>
<p>동작은 하지만 문제가 있다. <code>totalCount</code>는 <code>Account</code> 클래스와 논리적으로 관련된 데이터인데, 클래스 바깥에 따로 떨어져 있다. 누구든 <code>totalCount = -999</code> 같이 마음대로 바꿀 수 있고, 캡슐화가 깨진다.</p>
<h3>6-3. 우리가 원하는 것</h3>
<p>"모든 객체가 공유하면서도, 클래스 안에 묶여 있는 변수"가 필요하다. 이것이 바로 <strong>static 멤버</strong> 다.</p>
<p><code>static</code> 멤버변수는 <strong>클래스에 속하지만 모든 객체가 공유하는 변수</strong> 다. 객체를 아무리 많이 만들어도 <code>static</code> 변수는 메모리에 <strong>하나만</strong> 존재한다.</p>
<pre><code class="language-cpp">class Account {
private:
    string owner;
    int balance;
    static int totalCount;    // static 멤버변수 선언
    static double interestRate;

public:
    Account(string name, int money)
        : owner(name), balance(money) {
        totalCount++;
    }

    ~Account() {
        totalCount--;
    }

    static int getTotalCount() {  // static 멤버함수
        return totalCount;
    }

    static void setInterestRate(double rate) {
        interestRate = rate;
    }
};

// static 멤버변수는 클래스 외부에서 반드시 초기화해야 함
int Account::totalCount = 0;
double Account::interestRate = 0.05;
</code></pre>
<pre><code class="language-cpp">int main() {
    cout &lt;&lt; Account::getTotalCount() &lt;&lt; endl;  // 0

    Account a1("홍길동", 10000);
    Account a2("김철수", 20000);

    cout &lt;&lt; Account::getTotalCount() &lt;&lt; endl;  // 2

    Account::setInterestRate(0.03);

    return 0;
}
</code></pre>
<p><code>static</code> 멤버변수는 클래스 안에 선언하지만, 실제 메모리 할당과 초기화는 <strong>클래스 외부에서</strong> 해야 한다. <code>int Account::totalCount = 0;</code> 이 줄이 없으면 링크 에러가 발생한다.</p>
<p><code>static</code> 멤버함수는 <strong>객체 없이 클래스 이름으로 직접 호출</strong> 할 수 있다. <code>Account::getTotalCount()</code>처럼 사용한다. <code>static</code> 멤버함수 안에서는 <code>this</code> 포인터가 없으므로 일반 멤버변수에 접근할 수 없고, <code>static</code> 멤버변수만 사용할 수 있다.</p>
<p>C에서 정적변수가 함수 안에서만 접근 가능하면서 값이 유지되었던 것처럼, <code>static</code> 멤버변수는 클래스 안에서만 접근 가능하면서(private이면) 모든 객체가 공유한다. 전역변수의 공유 기능과 캡슐화의 보호 기능을 동시에 얻을 수 있다.</p>
<hr />
<h2>7. 프렌드 (friend)</h2>
<p>C++에서 <code>private</code> 멤버는 클래스 외부에서 접근할 수 없다. 하지만 특정 함수나 클래스에게만 예외적으로 접근을 허용하고 싶을 때가 있다. 이때 사용하는 것이 <code>friend</code>다.</p>
<p><code>friend</code>로 지정된 함수나 클래스는 해당 클래스의 <code>private</code> <strong>멤버에 접근할 수 있다.</strong></p>
<pre><code class="language-cpp">class Student {
private:
    string name;
    int score;

public:
    Student(string n, int s) : name(n), score(s) {}

    friend void printInfo(const Student &amp;s);  // friend 함수 선언
};

void printInfo(const Student &amp;s) {
    // Student의 private 멤버에 직접 접근 가능
    cout &lt;&lt; s.name &lt;&lt; ": " &lt;&lt; s.score &lt;&lt; "점" &lt;&lt; endl;
}
</code></pre>
<p><code>printInfo</code>는 <code>Student</code>의 멤버함수가 아니라 <strong>외부 함수</strong> 다. 그런데 <code>friend</code>로 선언되었기 때문에 <code>private</code> 멤버인 <code>name</code>과 <code>score</code>에 직접 접근할 수 있다.</p>
<p>클래스끼리도 <code>friend</code>를 선언할 수 있다.</p>
<pre><code class="language-cpp">class Engine {
private:
    int horsepower;

public:
    Engine(int hp) : horsepower(hp) {}

    friend class Car;  // Car 클래스에게 접근 허용
};

class Car {
public:
    void showEngine(const Engine &amp;e) {
        // Engine의 private 멤버에 접근 가능
        cout &lt;&lt; "마력: " &lt;&lt; e.horsepower &lt;&lt; endl;
    }
};
</code></pre>
<p><code>friend class Car</code>로 선언하면, <code>Car</code> 클래스의 모든 멤버함수가 <code>Engine</code>의 <code>private</code> 멤버에 접근할 수 있다.</p>
<p><code>friend</code>는 편리하지만 <strong>캡슐화를 약화시킨다</strong> 는 점을 알아야 한다. <code>private</code>으로 보호한 데이터를 외부에 열어주는 것이므로, 남용하면 데이터 은닉의 의미가 퇴색된다. <code>friend</code>는 두 클래스가 밀접하게 협력해야 하거나, 연산자 오버로딩에서 외부 함수가 <code>private</code> 멤버에 접근해야 할 때 등 명확한 이유가 있을 때만 사용하는 것이 좋다.</p>
<p><code>friend</code> 관계의 특징을 정리하면 이렇다. <strong>단방향</strong> 이다 — <code>A</code>가 <code>B</code>를 <code>friend</code>로 선언해도, <code>B</code>가 <code>A</code>의 <code>private</code>에 접근할 수 있는 것이지 그 반대는 아니다. <strong>비전이적</strong> 이다 — <code>A</code>가 <code>B</code>의 <code>friend</code>이고 <code>B</code>가 <code>C</code>의 <code>friend</code>여도 <code>A</code>가 <code>C</code>의 <code>friend</code>가 되지는 않는다. <strong>상속되지 않는다</strong> — 부모가 <code>friend</code>라고 자식까지 <code>friend</code>가 되지는 않는다.</p>
<hr />
<h2>마무리</h2>
<p>이번 글에서 다룬 내용들은 C++에서 객체를 실제로 다루는 실전적인 기법들이다. 객체 배열과 포인터로 여러 객체를 관리하고, <code>this</code>로 자기 자신을 참조하고, <code>const</code>로 읽기 전용 보장을 하고, <code>static</code>으로 클래스 전체가 공유하는 데이터를 관리하고, <code>friend</code>로 필요한 곳에만 접근을 열어준다. 이 개념들은 이후 상속, 연산자 오버로딩, 복사 생성자를 배울 때 기반이 된다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (7)]]></title><description><![CDATA[1. 구조적 프로그래밍
구조적 프로그래밍(Structured Programming)은 프로그램을 함수 단위로 분해 하여 구성하는 방식이다. C언어가 대표적인 구조적 프로그래밍 언어다.
기본 단위는 함수 다. 프로그램의 전체 흐름을 작은 함수들로 쪼개고, 각 함수가 하나의 작업을 담당한다. main에서 시작해서 필요한 함수를 호출하고, 그 함수가 또 다른 함]]></description><link>https://blog.chamdom.dev/c-7</link><guid isPermaLink="true">https://blog.chamdom.dev/c-7</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Tue, 31 Mar 2026 23:23:35 GMT</pubDate><content:encoded><![CDATA[<h2>1. 구조적 프로그래밍</h2>
<p>구조적 프로그래밍(Structured Programming)은 프로그램을 <strong>함수 단위로 분해</strong> 하여 구성하는 방식이다. C언어가 대표적인 구조적 프로그래밍 언어다.</p>
<p>기본 단위는 <strong>함수</strong> 다. 프로그램의 전체 흐름을 작은 함수들로 쪼개고, 각 함수가 하나의 작업을 담당한다. <code>main</code>에서 시작해서 필요한 함수를 호출하고, 그 함수가 또 다른 함수를 호출하는 식으로 프로그램이 진행된다.</p>
<pre><code class="language-c">// 구조적 프로그래밍 방식
void inputData(int *arr, int size);
void sortData(int *arr, int size);
void printData(int *arr, int size);

int main(void) {
    int arr[10];
    inputData(arr, 10);
    sortData(arr, 10);
    printData(arr, 10);
    return 0;
}
</code></pre>
<p>이 방식은 직관적이고 이해하기 쉽다. 하지만 프로그램이 커지면 문제가 생긴다.</p>
<p><strong>데이터와 함수가 분리되어 있다.</strong> 데이터는 변수에, 로직은 함수에 따로 존재한다. <code>arr</code>라는 데이터를 다루는 함수가 여러 개인데, 이 함수들이 <code>arr</code>와 논리적으로 묶여 있다는 것이 코드 구조에 드러나지 않는다.</p>
<p><strong>전역변수 의존</strong> 이 심해지기 쉽다. 여러 함수가 같은 데이터를 써야 할 때 전역변수로 공유하게 되고, 프로그램이 커질수록 누가 어떤 데이터를 수정했는지 추적하기 어려워진다.</p>
<p><strong>코드 재사용이 어렵다.</strong> 비슷한 기능을 하는 프로그램을 만들 때 함수를 가져다 쓸 수는 있지만, 데이터 구조가 달라지면 함수도 전부 수정해야 한다.</p>
<p>이런 한계를 극복하기 위해 등장한 것이 <strong>객체지향 프로그래밍</strong> 이다.</p>
<hr />
<h2>2. 객체지향 프로그래밍</h2>
<p>객체지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그램을 <strong>객체(Object)</strong> 단위로 구성하는 방식이다. 객체는 <strong>데이터(속성)와 그 데이터를 다루는 함수(동작)를 하나로 묶은 것</strong> 이다.</p>
<p>C에서는 구조체에 데이터만 넣을 수 있었다. C++에서는 구조체(그리고 클래스)에 <strong>함수까지 함께 넣을 수 있다.</strong> 데이터와 로직이 하나의 단위로 묶이는 것이다.</p>
<pre><code class="language-cpp">// C 방식 — 데이터와 함수가 분리
struct Student {
    char name[20];
    int age;
};
void printStudent(struct Student *s);

// C++ 방식 — 데이터와 함수가 하나로 묶임
class Student {
    string name;
    int age;
public:
    void print() {
        cout &lt;&lt; name &lt;&lt; ", " &lt;&lt; age &lt;&lt; endl;
    }
};
</code></pre>
<p>객체지향의 핵심 개념은 <strong>추상화</strong>, <strong>데이터 은닉(캡슐화)</strong>, <strong>다형성</strong>, 그리고 <strong>상속</strong> 이다.</p>
<h3>2-1. 추상화</h3>
<p>추상화(Abstraction)는 <strong>복잡한 내부 구현을 숨기고, 핵심적인 기능만 외부에 드러내는 것</strong> 이다.</p>
<p>자동차를 운전할 때 엔진 내부의 폭발 과정을 알 필요 없다. 핸들, 페달, 기어라는 <strong>인터페이스</strong> 만 알면 운전할 수 있다. 프로그래밍에서도 마찬가지다. 객체를 사용하는 쪽에서는 "무엇을 할 수 있는지"만 알면 되고, "어떻게 하는지"는 몰라도 된다.</p>
<pre><code class="language-cpp">class Calculator {
public:
    int add(int a, int b) { return a + b; }
    int subtract(int a, int b) { return a - b; }
};

int main() {
    Calculator calc;
    cout &lt;&lt; calc.add(3, 5) &lt;&lt; endl;  // 내부 구현을 몰라도 사용 가능
    return 0;
}
</code></pre>
<p>사용자는 <code>add</code>가 내부적으로 어떻게 동작하는지 신경 쓸 필요 없다. "두 수를 넘기면 합을 돌려준다"는 것만 알면 된다. 이것이 추상화다.</p>
<p>클래스를 설계할 때는 "이 클래스를 사용하는 사람이 알아야 할 것은 무엇인가?"를 기준으로 <code>public</code> 인터페이스를 결정하고, 나머지 세부 구현은 감추는 것이 좋다.</p>
<h3>2-2. 데이터 은닉</h3>
<p>데이터 은닉(Data Hiding)은 객체 내부의 데이터에 <strong>외부에서 직접 접근하지 못하게 막는 것</strong> 이다.</p>
<p>C의 구조체는 모든 멤버가 외부에 공개되어 있다. 누구든 <code>s.age = -5</code> 처럼 비정상적인 값을 넣을 수 있고, 막을 방법이 없다.</p>
<p>C++에서는 <strong>접근 제어 지시자</strong> 로 이 문제를 해결한다. <code>private</code>으로 선언한 멤버는 클래스 외부에서 접근할 수 없다. <code>public</code>으로 선언한 멤버만 외부에서 접근할 수 있다. <code>protected</code>는 상속 관계에서 자식 클래스까지만 접근을 허용한다.</p>
<pre><code class="language-cpp">class Student {
private:
    string name;
    int age;

public:
    void setAge(int a) {
        if (a &gt;= 0 &amp;&amp; a &lt;= 150) {
            age = a;
        } else {
            cout &lt;&lt; "유효하지 않은 나이입니다." &lt;&lt; endl;
        }
    }

    int getAge() {
        return age;
    }
};
</code></pre>
<p><code>age</code>에 직접 접근하는 대신 <code>setAge</code>를 통해 값을 설정한다. 이 함수 안에서 유효성 검사를 할 수 있으므로, 잘못된 값이 들어가는 것을 방지할 수 있다.</p>
<h4>캡슐화</h4>
<p>캡슐화(Encapsulation)는 데이터 은닉과 밀접한 개념으로, <strong>데이터와 그 데이터를 조작하는 함수를 하나의 캡슐(클래스)로 묶는 것</strong> 이다.</p>
<p>데이터 은닉이 "외부 접근을 차단한다"는 방어적인 측면이라면, 캡슐화는 "관련된 것들을 하나로 묶는다"는 구조적인 측면이다. 둘은 함께 작동한다.</p>
<pre><code class="language-cpp">class BankAccount {
private:
    string owner;
    int balance;

public:
    BankAccount(string name, int initial)
        : owner(name), balance(initial) {}

    void deposit(int amount) {
        if (amount &gt; 0) {
            balance += amount;
        }
    }

    void withdraw(int amount) {
        if (amount &gt; 0 &amp;&amp; amount &lt;= balance) {
            balance -= amount;
        }
    }

    int getBalance() {
        return balance;
    }
};
</code></pre>
<p><code>balance</code>를 직접 건드릴 수 없고, 반드시 <code>deposit</code>이나 <code>withdraw</code>를 통해서만 변경할 수 있다. 입출금 로직과 잔액 데이터가 하나의 클래스 안에 캡슐화되어 있으므로, 잔액이 음수가 되는 등의 비정상적인 상태를 방지할 수 있다.</p>
<p>JavaScript에서는 클로저나 <code>#</code> 프라이빗 필드로 비슷한 효과를 낸다. C++에서는 <code>private</code> 키워드로 언어 차원에서 명확하게 지원하는 것이다.</p>
<h3>2-3. 다형성</h3>
<p>다형성(Polymorphism)은 <strong>같은 이름의 함수가 상황에 따라 다르게 동작하는 것</strong> 이다. "poly(여러)" + "morph(형태)"라는 뜻 그대로, 하나의 인터페이스가 여러 형태를 가질 수 있다.</p>
<p>C++에서 다형성은 크게 <strong>오버로딩</strong> 과 <strong>오버라이딩</strong> 으로 나뉜다.</p>
<h4>오버로딩 (Overloading)</h4>
<p>오버로딩은 <strong>같은 이름의 함수를 매개변수의 타입이나 개수를 다르게 하여 여러 개 정의하는 것</strong> 이다. 컴파일 시점에 어떤 함수를 호출할지 결정되므로 <strong>컴파일 타임 다형성</strong> 이라고도 한다.</p>
<pre><code class="language-cpp">int add(int a, int b) {
    return a + b;
}

double add(double a, double b) {
    return a + b;
}

int add(int a, int b, int c) {
    return a + b + c;
}

int main() {
    cout &lt;&lt; add(3, 5) &lt;&lt; endl;          // int 버전 호출
    cout &lt;&lt; add(3.14, 2.72) &lt;&lt; endl;    // double 버전 호출
    cout &lt;&lt; add(1, 2, 3) &lt;&lt; endl;       // 3개짜리 버전 호출
    return 0;
}
</code></pre>
<p>C에서는 이것이 불가능했다. 함수 이름이 같으면 컴파일 에러가 난다. <code>add_int</code>, <code>add_double</code>, <code>add_three</code> 처럼 이름을 전부 다르게 만들어야 했다. C++에서는 컴파일러가 인자의 타입과 개수를 보고 어떤 함수를 호출할지 알아서 결정해준다.</p>
<p>주의할 점은 <strong>반환 타입만 다른 것으로는 오버로딩이 되지 않는다</strong> 는 것이다. 컴파일러는 호출 시점에 반환값을 보고 함수를 구분할 수 없기 때문이다.</p>
<pre><code class="language-cpp">int getValue();
double getValue();  // 에러! 매개변수가 같으면 반환 타입만으로 구분 불가
</code></pre>
<h4>오버라이딩 (Overriding)</h4>
<p>오버라이딩은 <strong>부모 클래스의 함수를 자식 클래스에서 재정의하는 것</strong> 이다. 상속 관계에서 동작하며, 런타임에 어떤 함수를 호출할지 결정되므로 <strong>런타임 다형성</strong> 이라고도 한다.</p>
<pre><code class="language-cpp">class Animal {
public:
    virtual void speak() {
        cout &lt;&lt; "..." &lt;&lt; endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        cout &lt;&lt; "멍멍!" &lt;&lt; endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {
        cout &lt;&lt; "야옹!" &lt;&lt; endl;
    }
};

int main() {
    Animal *animals[3];
    animals[0] = new Animal();
    animals[1] = new Dog();
    animals[2] = new Cat();

    for (int i = 0; i &lt; 3; i++) {
        animals[i]-&gt;speak();
    }
    // 출력:
    // ...
    // 멍멍!
    // 야옹!

    for (int i = 0; i &lt; 3; i++) {
        delete animals[i];
    }
    return 0;
}
</code></pre>
<p><code>animals</code> 배열은 전부 <code>Animal *</code> 타입이지만, 실제로 가리키는 객체가 <code>Dog</code>인지 <code>Cat</code>인지에 따라 <strong>다른</strong> <code>speak</code> <strong>함수가 호출</strong> 된다. 이것이 런타임 다형성이다.</p>
<p>여기서 핵심은 부모 클래스의 함수에 <code>virtual</code> 키워드를 붙이는 것이다. <code>virtual</code>이 없으면 포인터의 타입(<code>Animal *</code>)에 따라 부모의 <code>speak</code> 가 호출된다. <code>virtual</code>이 있으면 실제 객체의 타입에 따라 적절한 <code>speak</code> 가 호출된다.</p>
<p>자식 클래스에서 <code>override</code> 키워드를 붙이는 것은 필수는 아니지만, "이 함수는 부모의 함수를 재정의한 것이다"라는 의도를 명확히 하고, 오타 등으로 인한 실수를 컴파일 타임에 잡아주므로 붙이는 것이 좋다.</p>
<p>오버로딩과 오버라이딩을 헷갈리기 쉬운데, 핵심 차이를 정리하면 이렇다.</p>
<p><strong>오버로딩</strong> 은 같은 이름, 다른 매개변수로 함수를 여러 개 만드는 것이다. 같은 클래스 안에서 일어나고, 컴파일 타임에 결정된다. <strong>오버라이딩</strong> 은 부모의 함수를 자식이 재정의하는 것이다. 상속 관계에서 일어나고, 런타임에 결정된다.</p>
<hr />
<h2>마무리</h2>
<p>구조적 프로그래밍에서 객체지향 프로그래밍으로의 전환은 "함수 중심"에서 "객체 중심"으로의 사고 전환이다. 추상화로 복잡성을 숨기고, 캡슐화로 데이터를 보호하고, 다형성으로 유연한 코드를 작성한다. C의 구조체와 함수 포인터로 흉내냈던 것을 C++에서는 언어 차원에서 깔끔하게 지원하는 것이다. 이 개념들은 이후 상속, 가상 함수, 연산자 오버로딩을 배우면서 더 구체적으로 활용하게 된다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (6)]]></title><description><![CDATA[1. 전처리
1-1. 전처리란?
C 소스 코드가 실행 파일이 되기까지의 과정을 다시 떠올려보자.
소스파일(.c) → [전처리] → 전처리 파일(.i) → [컴파일] → 목적파일(.o) → [링크] → 실행파일

전처리(Preprocessing) 는 컴파일이 시작되기 전에 소스 코드를 먼저 가공하는 단계다. #으로 시작하는 지시문들을 처리하는 것이 이 단계에]]></description><link>https://blog.chamdom.dev/c-6</link><guid isPermaLink="true">https://blog.chamdom.dev/c-6</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Sun, 29 Mar 2026 20:15:16 GMT</pubDate><content:encoded><![CDATA[<h2>1. 전처리</h2>
<h3>1-1. 전처리란?</h3>
<p>C 소스 코드가 실행 파일이 되기까지의 과정을 다시 떠올려보자.</p>
<pre><code class="language-plaintext">소스파일(.c) → [전처리] → 전처리 파일(.i) → [컴파일] → 목적파일(.o) → [링크] → 실행파일
</code></pre>
<p><strong>전처리(Preprocessing)</strong> 는 컴파일이 시작되기 전에 소스 코드를 먼저 가공하는 단계다. <code>#</code>으로 시작하는 지시문들을 처리하는 것이 이 단계에서 일어나는 일이다. <code>#include</code>로 헤더 파일의 내용을 붙여넣고, <code>#define</code>으로 정의된 매크로를 치환하고, <code>#ifdef</code> 같은 조건부 컴파일 지시자를 평가한다.</p>
<p>전처리가 끝나면 <code>#</code>으로 시작하는 지시문은 전부 사라지고, 순수한 C 코드만 남는다. 이 결과물이 전처리 파일(<code>.i</code>)이고, 이것이 컴파일러에 전달된다.</p>
<h3>1-2. 전처리의 규칙</h3>
<p>전처리 지시문에는 몇 가지 규칙이 있다.</p>
<p>전처리 지시문은 항상 <code>#</code>으로 시작한다. <code>#</code> 앞에 공백은 있어도 되지만, 일반적으로 줄 맨 앞에 쓴다.</p>
<p>전처리 지시문은 <strong>한 줄에 하나씩</strong> 작성한다. 세미콜론으로 끝나지 않는다. C 코드의 문장은 <code>;</code>으로 끝나지만, 전처리 지시문은 줄바꿈이 곧 끝이다. 한 줄에 다 쓰기 어려울 때는 줄 끝에 <code>\</code>를 붙여서 다음 줄로 이어쓸 수 있다.</p>
<pre><code class="language-c">#define LONG_MACRO(x, y) \
    ((x) &gt; (y) ? (x) : (y))
</code></pre>
<p>전처리 지시문은 코드 어디에든 올 수 있지만, 관례적으로 파일 상단에 모아두는 것이 일반적이다.</p>
<h3>1-3. 전처리기 지시자의 종류</h3>
<p>자주 사용하는 전처리기 지시자를 정리하면 이렇다.</p>
<p><code>#include</code> 는 다른 파일의 내용을 현재 위치에 삽입한다. <code>&lt;stdio.h&gt;</code>처럼 꺾쇠로 감싸면 시스템 헤더 경로에서 찾고, <code>"myheader.h"</code>처럼 따옴표로 감싸면 현재 디렉토리에서 먼저 찾는다.</p>
<p><code>#define</code> 은 매크로를 정의한다. 단순 상수 치환부터 함수처럼 동작하는 매크로까지 만들 수 있다.</p>
<p><code>#undef</code> 는 이미 정의된 매크로를 해제한다.</p>
<p><code>#ifdef</code>, <code>#ifndef</code>, <code>#if</code>, <code>#elif</code>, <code>#else</code>, <code>#endif</code> 는 조건부 컴파일을 위한 지시자들이다.</p>
<p><code>#pragma</code> 는 컴파일러에게 특별한 지시를 내린다. 컴파일러마다 지원하는 내용이 다르다.</p>
<hr />
<h2>2. 매크로</h2>
<h3>2-1. 매크로 상수</h3>
<p><code>#define</code>으로 이름에 값을 대응시키는 것이 매크로 상수다. 전처리 단계에서 해당 이름이 등장하는 곳을 전부 지정한 값으로 <strong>텍스트 치환</strong> 한다.</p>
<pre><code class="language-c">#define PI 3.14159
#define MAX_SIZE 100
#define MESSAGE "Hello, World!"

int main(void) {
    double area = PI * 5 * 5;
    int arr[MAX_SIZE];
    printf("%s\n", MESSAGE);
    return 0;
}
</code></pre>
<p>전처리 후에는 이렇게 바뀐다.</p>
<pre><code class="language-c">int main(void) {
    double area = 3.14159 * 5 * 5;
    int arr[100];
    printf("%s\n", "Hello, World!");
    return 0;
}
</code></pre>
<p><code>PI</code>, <code>MAX_SIZE</code>, <code>MESSAGE</code>라는 이름이 전부 대응하는 값으로 치환된 것이다. 매크로 이름은 관례적으로 <strong>대문자</strong> 로 쓴다. 일반 변수와 구분하기 위해서다.</p>
<h3>2-2. 매크로 상수를 사용하면 좋은 점</h3>
<p><strong>매직 넘버를 제거한다.</strong> 코드 곳곳에 <code>100</code>이라는 숫자가 흩어져 있으면, 그것이 배열 크기인지 점수 기준인지 알 수 없다. <code>MAX_SIZE</code>라는 이름을 붙이면 의도가 명확해진다.</p>
<p><strong>유지보수가 쉬워진다.</strong> 배열 크기를 200으로 바꿔야 할 때, <code>#define MAX_SIZE 100</code>을 <code>#define MAX_SIZE 200</code>으로 한 줄만 수정하면 된다. <code>100</code>이 등장하는 모든 곳을 찾아 바꿀 필요가 없다.</p>
<p><strong>배열 크기 지정에 사용할 수 있다.</strong> C에서 <code>const int</code>는 진정한 컴파일 타임 상수가 아니라서 배열 크기로 사용하지 못하는 경우가 있다. 하지만 <code>#define</code> 매크로는 전처리 단계에서 숫자로 치환되므로 배열 크기로 사용할 수 있다.</p>
<pre><code class="language-c">#define SIZE 10
int arr[SIZE];       // OK

const int size = 10;
// int arr2[size];   // C89에서는 에러
</code></pre>
<h3>2-3. 매크로의 특징</h3>
<p>매크로는 <strong>텍스트 치환</strong> 이라는 점이 핵심이다. 컴파일러가 처리하는 것이 아니라 전처리기가 단순히 텍스트를 바꿔치기하는 것이다. 이 때문에 주의해야 할 점들이 있다.</p>
<p><strong>매크로 함수</strong> 를 만들 수도 있다.</p>
<pre><code class="language-c">#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) &gt; (b) ? (a) : (b))

int result = SQUARE(5);      // ((5) * (5)) → 25
int bigger = MAX(10, 20);    // ((10) &gt; (20) ? (10) : (20)) → 20
</code></pre>
<p>매개변수를 괄호로 감싸는 것이 매우 중요하다. 괄호가 없으면 연산자 우선순위 때문에 의도하지 않은 결과가 나올 수 있다.</p>
<pre><code class="language-c">#define BAD_SQUARE(x) x * x

int result = BAD_SQUARE(3 + 2);
// 치환 결과: 3 + 2 * 3 + 2
// 의도: 25, 실제: 11
</code></pre>
<p><code>BAD_SQUARE(3 + 2)</code> 는 <code>3 + 2 * 3 + 2</code>로 치환된다. 곱셈이 먼저 계산되어 <code>3 + 6 + 2 = 11</code>이 된다. <code>((x) * (x))</code>로 썼다면 <code>((3 + 2) * (3 + 2))</code> = 25로 의도한 대로 동작한다.</p>
<p>매크로는 <strong>타입 검사를 하지 않는다.</strong> 함수는 매개변수와 반환값의 타입을 컴파일러가 검사하지만, 매크로는 텍스트 치환이므로 어떤 타입이든 들어갈 수 있다. 이것이 장점이 될 수도 있지만(제네릭하게 사용 가능), 타입 관련 버그를 잡기 어렵게 만들 수도 있다.</p>
<p>매크로는 <strong>디버깅이 어렵다.</strong> 전처리 단계에서 이미 치환되어 사라지기 때문에, 디버거에서 매크로 이름으로 중단점을 잡거나 값을 확인할 수 없다.</p>
<p>매크로 함수의 장점은 <strong>함수 호출 오버헤드가 없다</strong> 는 것이다. 일반 함수는 호출 시 스택 프레임 생성, 인자 복사, 점프 등의 비용이 발생하지만, 매크로는 코드가 직접 삽입되므로 이런 비용이 없다. 아주 작고 자주 호출되는 로직에는 매크로가 유리할 수 있다. 다만 현대 컴파일러는 작은 함수를 자동으로 인라인 처리하므로, 성능 차이는 대부분의 경우 미미하다.</p>
<hr />
<h2>3. 조건부 컴파일</h2>
<p>조건부 컴파일은 <strong>특정 조건에 따라 코드의 일부를 컴파일에 포함시키거나 제외하는 기능</strong> 이다. <code>if-else</code>와 비슷하지만, 런타임이 아니라 <strong>전처리 단계</strong> 에서 평가된다. 조건에 맞지 않는 코드는 아예 컴파일러에 전달되지 않는다.</p>
<h3>3-1. 지시자의 형식</h3>
<h4>#ifdef / #ifndef</h4>
<p><code>#ifdef</code> 는 매크로가 정의되어 있으면 해당 코드를 포함한다. <code>#ifndef</code> 는 반대로 정의되어 있지 않으면 포함한다.</p>
<pre><code class="language-c">#define DEBUG

#ifdef DEBUG
    printf("디버그 모드입니다.\n");
    printf("x = %d\n", x);
#endif
</code></pre>
<p><code>DEBUG</code>가 정의되어 있으므로 <code>printf</code>문이 컴파일에 포함된다. <code>#define DEBUG</code>를 지우면 이 코드는 아예 컴파일되지 않는다. 디버깅용 출력을 넣었다 뺐다 할 때 유용하다. 일일이 주석 처리할 필요 없이 <code>#define</code> 한 줄만 제어하면 된다.</p>
<h4>#if / #elif / #else / #endif</h4>
<p>더 세밀한 조건 분기가 필요하면 <code>#if</code>를 사용한다.</p>
<pre><code class="language-c">#define VERSION 3

#if VERSION == 1
    printf("버전 1\n");
#elif VERSION == 2
    printf("버전 2\n");
#elif VERSION &gt;= 3
    printf("버전 3 이상\n");
#else
    printf("알 수 없는 버전\n");
#endif
</code></pre>
<p><code>#if</code>의 조건에는 정수 상수 표현식만 올 수 있다. 변수나 함수 호출은 사용할 수 없다. 전처리 단계에서는 아직 변수가 존재하지 않기 때문이다.</p>
<p><code>defined()</code> 연산자를 <code>#if</code>와 함께 쓰면 <code>#ifdef</code>와 같은 효과를 낸다. 여러 조건을 논리 연산자로 조합할 때 유용하다.</p>
<pre><code class="language-c">#if defined(WINDOWS) &amp;&amp; defined(DEBUG)
    printf("Windows 디버그 모드\n");
#elif defined(LINUX)
    printf("Linux 환경\n");
#endif
</code></pre>
<h4>헤더 가드 (Include Guard)</h4>
<p>조건부 컴파일의 가장 대표적인 활용 사례가 <strong>헤더 가드</strong> 다. 같은 헤더 파일이 여러 번 <code>#include</code>되면 구조체나 함수 선언이 중복되어 컴파일 에러가 발생한다. 헤더 가드는 이 문제를 방지한다.</p>
<pre><code class="language-c">// student.h
#ifndef STUDENT_H
#define STUDENT_H

typedef struct {
    char name[20];
    int age;
} Student;

void printStudent(const Student *s);

#endif
</code></pre>
<p>처음 <code>#include "student.h"</code>가 실행되면 <code>STUDENT_H</code>가 정의되어 있지 않으므로 <code>#ifndef</code> 조건이 참이 되어 내용이 포함된다. 동시에 <code>#define STUDENT_H</code>로 매크로가 정의된다. 두 번째 <code>#include "student.h"</code>가 실행되면 이미 <code>STUDENT_H</code>가 정의되어 있으므로 <code>#ifndef</code> 조건이 거짓이 되어 전체 내용이 건너뛰어진다.</p>
<p>헤더 파일을 작성할 때는 항상 헤더 가드를 넣는 것이 기본이다. 일부 컴파일러는 <code>#pragma once</code>라는 비표준 지시자로 같은 효과를 제공한다.</p>
<pre><code class="language-c">// student.h (pragma once 방식)
#pragma once

typedef struct {
    char name[20];
    int age;
} Student;
</code></pre>
<p><code>#pragma once</code> 는 간결하지만 표준이 아니므로, 모든 컴파일러에서 동작을 보장하지는 않는다. 이식성이 중요한 코드에서는 <code>#ifndef</code> 방식을 사용하는 것이 안전하다.</p>
<h4>플랫폼별 분기</h4>
<p>조건부 컴파일은 하나의 소스 코드로 여러 운영체제를 지원할 때도 사용된다.</p>
<pre><code class="language-c">#ifdef _WIN32
    #include &lt;windows.h&gt;
    #define CLEAR "cls"
#elif defined(__linux__)
    #include &lt;unistd.h&gt;
    #define CLEAR "clear"
#elif defined(__APPLE__)
    #include &lt;unistd.h&gt;
    #define CLEAR "clear"
#endif

system(CLEAR);  // 플랫폼에 맞는 화면 지우기 명령
</code></pre>
<p>Windows에서는 <code>_WIN32</code>가 자동으로 정의되고, Linux에서는 <code>__linux__</code>가, macOS에서는 <code>__APPLE__</code>이 정의된다. 같은 코드를 컴파일하더라도 플랫폼에 따라 다른 코드가 포함되는 것이다.</p>
<p>Visual Studio에서 <code>scanf_s</code>를 쓰고 macOS에서 <code>scanf</code>를 써야 하는 상황도 이 방식으로 해결할 수 있다.</p>
<pre><code class="language-c">#ifdef _MSC_VER
    scanf_s("%d", &amp;x);
#else
    scanf("%d", &amp;x);
#endif
</code></pre>
<hr />
<h2>마무리</h2>
<p>전처리기는 컴파일이 시작되기도 전에 소스 코드를 가공하는 도구다. 매크로 상수로 매직 넘버를 제거하고 유지보수를 쉽게 만들 수 있고, 조건부 컴파일로 디버그 코드를 관리하거나 플랫폼별 분기를 처리할 수 있다. 다만 매크로는 텍스트 치환이라는 점을 항상 기억해야 한다. 괄호를 빼먹으면 찾기 어려운 버그가 생기고, 타입 검사도 되지 않는다. 단순한 상수 정의와 헤더 가드에는 매크로가 적합하지만, 복잡한 로직은 함수로 작성하는 것이 안전하다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (5)]]></title><description><![CDATA[1. Null 포인터
포인터를 선언만 하고 초기화하지 않으면 쓰레기값 이 들어있다. 이 상태에서 역참조(*p)를 하면 엉뚱한 메모리에 접근하게 되어 프로그램이 터지거나, 더 위험하게는 조용히 잘못된 데이터를 건드릴 수 있다.
이런 상황을 방지하기 위해 "아직 아무것도 가리키지 않는다"는 의미로 NULL 을 대입한다.
int *p = NULL;

NULL 은]]></description><link>https://blog.chamdom.dev/c-5</link><guid isPermaLink="true">https://blog.chamdom.dev/c-5</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Sun, 29 Mar 2026 20:12:35 GMT</pubDate><content:encoded><![CDATA[<h2>1. Null 포인터</h2>
<p>포인터를 선언만 하고 초기화하지 않으면 <strong>쓰레기값</strong> 이 들어있다. 이 상태에서 역참조(<code>*p</code>)를 하면 엉뚱한 메모리에 접근하게 되어 프로그램이 터지거나, 더 위험하게는 조용히 잘못된 데이터를 건드릴 수 있다.</p>
<p>이런 상황을 방지하기 위해 "아직 아무것도 가리키지 않는다"는 의미로 <strong>NULL</strong> 을 대입한다.</p>
<pre><code class="language-c">int *p = NULL;
</code></pre>
<p><code>NULL</code> 은 보통 <code>0</code> 또는 <code>(void *)0</code>으로 정의되어 있다. 유효한 메모리 주소가 아니므로, <code>NULL</code> 포인터를 역참조하면 프로그램이 확실하게 크래시한다. 쓰레기값으로 인한 알 수 없는 동작보다는 차라리 확실하게 터지는 것이 디버깅하기 훨씬 쉽다.</p>
<p>포인터를 사용하기 전에 <code>NULL</code> 인지 확인하는 것은 C 프로그래밍에서 기본적인 안전 습관이다.</p>
<pre><code class="language-c">int *p = NULL;

if (p != NULL) {
    printf("%d\n", *p);  // 안전하게 접근
} else {
    printf("포인터가 초기화되지 않았습니다.\n");
}
</code></pre>
<p>메모리를 해제한 뒤에도 포인터에 <code>NULL</code> 을 대입하는 것이 좋다. <code>free</code> 한 뒤에도 포인터 변수에는 이전 주소값이 남아있는데, 이런 포인터를 <strong>댕글링 포인터(dangling pointer)</strong> 라고 한다. 이미 해제된 메모리를 다시 접근하면 큰 문제가 생길 수 있으므로, <code>free</code> 직후 <code>NULL</code> 을 넣어주면 실수를 방지할 수 있다.</p>
<pre><code class="language-c">int *p = malloc(sizeof(int));
*p = 42;
free(p);
p = NULL;  // 댕글링 포인터 방지
</code></pre>
<hr />
<h2>2. 구조체와 공용체</h2>
<h3>2-1. 구조체란?</h3>
<p>구조체는 <strong>서로 다른 타입의 데이터를 하나로 묶는 사용자 정의 자료형</strong> 이다.</p>
<p>배열은 같은 타입만 묶을 수 있다. 하지만 현실의 데이터는 그렇지 않다. 학생 한 명의 정보를 표현하려면 이름(문자열), 나이(정수), 평균 점수(실수)가 필요한데, 이걸 각각 별도의 변수로 관리하면 코드가 지저분해진다. 구조체는 이 문제를 해결한다.</p>
<pre><code class="language-c">struct Student {
    char name[20];
    int age;
    double gpa;
};
</code></pre>
<p>JavaScript의 객체(<code>{ name: "홍길동", age: 20, gpa: 3.8 }</code>)와 비슷한 개념이다. 다만 C의 구조체는 타입을 미리 정의해야 하고, 메서드를 가질 수 없다는 차이가 있다. C++에서 구조체에 함수를 넣을 수 있게 되고, 그것이 곧 클래스로 발전한다.</p>
<h3>2-2. 구조체 사용</h3>
<h4>구조체 변수</h4>
<p>구조체를 정의한 뒤 변수를 선언하고 사용한다.</p>
<pre><code class="language-c">struct Student {
    char name[20];
    int age;
    double gpa;
};

int main(void) {
    struct Student s1;  // 구조체 변수 선언

    strcpy(s1.name, "홍길동");  // 문자열은 strcpy로 대입
    s1.age = 20;
    s1.gpa = 3.8;

    printf("이름: %s\n", s1.name);
    printf("나이: %d\n", s1.age);
    printf("학점: %.1f\n", s1.gpa);

    return 0;
}
</code></pre>
<p>구조체 멤버에 접근할 때는 <strong>점(</strong><code>.</code><strong>) 연산자</strong> 를 사용한다. <code>s1.age</code>는 "<code>s1</code>이라는 구조체 안의 <code>age</code> 멤버"라는 뜻이다.</p>
<p>선언과 동시에 초기화할 수도 있다.</p>
<pre><code class="language-c">struct Student s1 = {"홍길동", 20, 3.8};
</code></pre>
<p>매번 <code>struct Student</code>라고 쓰는 것이 번거로우면 <code>typedef</code>로 별칭을 만들 수 있다.</p>
<pre><code class="language-c">typedef struct {
    char name[20];
    int age;
    double gpa;
} Student;

Student s1 = {"홍길동", 20, 3.8};  // struct 키워드 생략 가능
</code></pre>
<h3>2-3. 구조체의 배열과 포인터</h3>
<p>구조체도 배열로 만들 수 있다. 학생 여러 명의 데이터를 관리할 때 유용하다.</p>
<pre><code class="language-c">Student students[3] = {
    {"홍길동", 20, 3.8},
    {"김철수", 21, 3.2},
    {"이영희", 19, 4.0}
};

for (int i = 0; i &lt; 3; i++) {
    printf("%s: %.1f\n", students[i].name, students[i].gpa);
}
</code></pre>
<p>구조체 포인터도 사용할 수 있다. 구조체 포인터로 멤버에 접근할 때는 점(<code>.</code>) 대신 <strong>화살표(</strong><code>-&gt;</code><strong>) 연산자</strong> 를 사용한다.</p>
<pre><code class="language-c">Student s1 = {"홍길동", 20, 3.8};
Student *p = &amp;s1;

printf("이름: %s\n", p-&gt;name);   // 화살표 연산자
printf("나이: %d\n", p-&gt;age);
</code></pre>
<p><code>p-&gt;age</code>는 <code>(*p).age</code>와 같은 의미다. 포인터를 역참조한 뒤 점으로 멤버에 접근하는 것을 화살표 연산자로 줄여 쓴 것이다.</p>
<p>구조체가 크면 함수에 넘길 때 통째로 복사하는 것은 비효율적이다. 포인터로 넘기면 주소 하나만 전달하므로 훨씬 효율적이다.</p>
<pre><code class="language-c">void printStudent(const Student *s) {
    printf("이름: %s, 나이: %d\n", s-&gt;name, s-&gt;age);
}

int main(void) {
    Student s1 = {"홍길동", 20, 3.8};
    printStudent(&amp;s1);
    return 0;
}
</code></pre>
<p>매개변수에 <code>const</code>를 붙이면 함수 안에서 구조체를 수정할 수 없게 된다. 읽기만 할 때는 <code>const</code>를 붙이는 것이 안전하다.</p>
<h3>2-4. 공용체</h3>
<p>공용체(<code>union</code>)는 구조체와 비슷하게 생겼지만, 핵심적인 차이가 있다. 구조체는 모든 멤버가 <strong>각자의 메모리 공간</strong> 을 가진다. 공용체는 모든 멤버가 <strong>같은 메모리 공간을 공유</strong> 한다.</p>
<pre><code class="language-c">union Data {
    int i;
    float f;
    char c;
};
</code></pre>
<p>이 공용체의 크기는 가장 큰 멤버(<code>int</code> 또는 <code>float</code>, 4바이트)의 크기와 같다. <code>int</code>, <code>float</code>, <code>char</code> 세 멤버가 전부 같은 4바이트를 나눠 쓴다.</p>
<pre><code class="language-c">union Data d;
d.i = 42;
printf("%d\n", d.i);  // 42

d.f = 3.14f;
printf("%f\n", d.f);  // 3.140000
printf("%d\n", d.i);  // 엉뚱한 값 (f를 쓰면서 i가 덮어써짐)
</code></pre>
<p>한 멤버에 값을 넣으면 다른 멤버의 값은 의미 없어진다. <strong>한 번에 하나의 멤버만 유효하다.</strong> 메모리를 절약해야 하거나, 같은 데이터를 다른 타입으로 해석하고 싶을 때 사용한다. 실무에서는 프로토콜 파싱이나 하드웨어 레지스터 접근에서 가끔 쓰인다.</p>
<h3>2-5. 열거형 (enum)</h3>
<p>열거형은 <strong>관련된 상수들에 이름을 붙이는 자료형</strong> 이다. 매직 넘버(의미를 알 수 없는 숫자) 대신 의미 있는 이름을 사용해서 코드의 가독성을 높인다.</p>
<pre><code class="language-c">enum Day {
    MON,   // 0
    TUE,   // 1
    WED,   // 2
    THU,   // 3
    FRI,   // 4
    SAT,   // 5
    SUN    // 6
};
</code></pre>
<p>별도로 값을 지정하지 않으면 0부터 순서대로 정수가 부여된다. 값을 직접 지정할 수도 있다.</p>
<pre><code class="language-c">enum StatusCode {
    OK = 200,
    NOT_FOUND = 404,
    SERVER_ERROR = 500
};
</code></pre>
<pre><code class="language-c">enum Day today = WED;

if (today == SAT || today == SUN) {
    printf("주말\n");
} else {
    printf("평일\n");
}
</code></pre>
<p><code>#define</code>으로 상수를 정의하는 것과 비슷하지만, <code>enum</code> 은 관련된 값들을 하나의 타입으로 묶어준다는 점에서 더 체계적이다. 디버거에서도 숫자 대신 이름으로 보여주므로 디버깅이 편해진다.</p>
<hr />
<h2>3. 동적 메모리 할당과 메모리 표준함수</h2>
<h3>3-1. 메모리의 구조</h3>
<p>C 프로그램이 실행되면 OS로부터 메모리를 할당받는데, 이 메모리는 크게 4개 영역으로 나뉜다.</p>
<p><strong>코드 영역(Code/Text Segment)</strong> 에는 컴파일된 기계어 코드가 올라간다. 작성한 함수들의 실행 코드가 여기에 저장되며, 읽기 전용이다.</p>
<p><strong>데이터 영역(Data Segment)</strong> 에는 전역변수와 정적변수(<code>static</code>)가 저장된다. 프로그램 시작 시 할당되어 프로그램이 종료될 때까지 유지된다. 초기화된 변수와 초기화되지 않은 변수(BSS 영역)로 다시 나뉘기도 한다.</p>
<p><strong>스택 영역(Stack)</strong> 에는 지역변수와 함수 호출 정보(매개변수, 반환 주소 등)가 저장된다. 함수가 호출되면 공간이 확보되고, 함수가 끝나면 자동으로 해제된다. 높은 주소에서 낮은 주소 방향으로 자란다.</p>
<p><strong>힙 영역(Heap)</strong> 은 프로그래머가 직접 할당하고 해제하는 공간이다. <code>malloc</code>, <code>calloc</code> 등으로 할당하고 <code>free</code>로 해제한다. 낮은 주소에서 높은 주소 방향으로 자란다. 스택과 힙이 서로를 향해 자라는 구조다.</p>
<pre><code class="language-plaintext">높은 주소
┌────────────────┐
│   스택 (Stack)    │  ← 지역변수, 함수 호출 정보 (위에서 아래로 성장)
│       ↓        │
│                │
│       ↑        │
│    힙 (Heap)    │  ← 동적 할당 (아래에서 위로 성장)
├────────────────┤
│  데이터 (Data)   │  ← 전역변수, static 변수
├────────────────┤
│  코드 (Code)    │  ← 실행 코드 (읽기 전용)
└────────────────┘
낮은 주소
</code></pre>
<h3>3-2. 동적 메모리 할당</h3>
<p>배열을 선언할 때 크기를 상수로 지정해야 한다고 했다. 그런데 실제 프로그램에서는 필요한 메모리 크기를 런타임에야 알 수 있는 경우가 많다. 사용자가 몇 개의 데이터를 입력할지, 파일에 데이터가 몇 줄인지는 프로그램이 실행되어야 알 수 있기 때문이다.</p>
<p>이때 <strong>힙 메모리</strong> 에 원하는 크기만큼 공간을 확보하는 것이 동적 메모리 할당이다.</p>
<blockquote>
<p><strong>타입캐스팅(Type Casting)</strong></p>
<p>동적 메모리 할당 함수들을 이해하려면 타입캐스팅을 알아야 한다. 타입캐스팅은 하나의 자료형을 다른 자료형으로 변환하는 것이다.</p>
<p><strong>암시적 캐스팅</strong> 은 컴파일러가 자동으로 수행한다. <code>int</code>를 <code>double</code>에 대입하면 자동으로 변환된다. <strong>명시적 캐스팅</strong> 은 프로그래머가 <code>(타입)</code>을 직접 붙여서 강제로 변환하는 것이다.</p>
<pre><code class="language-c">int a = 10;
double b = a;          // 암시적 캐스팅 (int → double)
int c = (int)3.14;     // 명시적 캐스팅 (double → int, 소수점 버림)
</code></pre>
<p><code>malloc</code> 은 <code>void *</code> 를 반환하는데, 이것을 원하는 포인터 타입으로 캐스팅해서 사용한다. <code>void *</code> 는 "어떤 타입인지 정해지지 않은 포인터"라는 뜻으로, 어떤 포인터 타입으로든 변환할 수 있다.</p>
</blockquote>
<h4>malloc</h4>
<p><code>malloc</code>(memory allocation)은 지정한 바이트 수만큼 힙에 메모리를 할당하고, 그 시작 주소를 반환한다.</p>
<pre><code class="language-c">#include &lt;stdlib.h&gt;

int *p = (int *)malloc(sizeof(int) * 5);  // int 5개 크기 할당
</code></pre>
<p><code>malloc(sizeof(int) * 5)</code> 는 20바이트를 힙에 확보한다. 반환 타입이 <code>void *</code> 이므로 <code>(int *)</code>로 캐스팅해서 <code>int</code> 포인터에 저장한다. C에서는 <code>void *</code> 가 다른 포인터 타입으로 자동 변환되므로 캐스팅을 생략해도 되지만, C++에서는 반드시 캐스팅해야 한다. 명시적으로 쓰는 습관을 들이는 것이 좋다.</p>
<p>할당한 메모리는 배열처럼 사용할 수 있다.</p>
<pre><code class="language-c">int *p = (int *)malloc(sizeof(int) * 5);

if (p == NULL) {
    printf("메모리 할당 실패\n");
    return 1;
}

for (int i = 0; i &lt; 5; i++) {
    p[i] = (i + 1) * 10;
}

for (int i = 0; i &lt; 5; i++) {
    printf("%d ", p[i]);  // 10 20 30 40 50
}

free(p);
p = NULL;
</code></pre>
<p><code>malloc</code> 은 할당에 실패하면 <code>NULL</code> 을 반환한다. 메모리가 부족하면 실패할 수 있으므로, 반드시 <code>NULL</code> 체크를 해야 한다.</p>
<p><code>malloc</code> 으로 할당한 메모리는 <strong>초기화되지 않는다.</strong> 쓰레기값이 들어있다.</p>
<h4>free</h4>
<p><code>free</code>는 <code>malloc</code>으로 할당한 메모리를 OS에 반환한다.</p>
<pre><code class="language-c">int *p = (int *)malloc(sizeof(int) * 10);
// ... 사용 ...
free(p);     // 메모리 해제
p = NULL;    // 댕글링 포인터 방지
</code></pre>
<p><code>free</code> 는 반드시 <code>malloc</code>, <code>calloc</code>, <code>realloc</code>으로 할당한 메모리에만 사용해야 한다. 스택에 있는 지역변수의 주소를 <code>free</code> 하면 정의되지 않은 동작이 발생한다. 같은 메모리를 두 번 <code>free</code> 하는 것(double free)도 위험하다.</p>
<p>JavaScript에서는 가비지 컬렉터가 알아서 메모리를 해제해주지만, C에서는 프로그래머가 직접 해야 한다. 이것이 C에서 메모리 관리가 어렵다고 하는 핵심 이유다.</p>
<h4>calloc</h4>
<p><code>calloc</code>(contiguous allocation)은 <code>malloc</code>과 비슷하지만 두 가지 차이가 있다. 매개변수를 "요소 개수"와 "요소 크기"로 나눠서 받고, 할당한 메모리를 <strong>0으로 초기화</strong> 해준다.</p>
<pre><code class="language-c">int *p = (int *)calloc(5, sizeof(int));  // int 5개, 전부 0으로 초기화
</code></pre>
<p><code>malloc(sizeof(int) * 5)</code> 와 같은 크기를 할당하지만, <code>calloc</code> 은 모든 바이트를 0으로 채워준다. 배열을 0으로 시작해야 하는 경우에 편리하다.</p>
<h4>realloc</h4>
<p><code>realloc</code>(reallocation)은 이미 할당한 메모리의 <strong>크기를 변경</strong> 한다. 배열 크기가 부족해졌을 때 늘리거나, 너무 클 때 줄일 수 있다.</p>
<pre><code class="language-c">int *p = (int *)malloc(sizeof(int) * 3);
p[0] = 10;
p[1] = 20;
p[2] = 30;

// 크기를 5로 늘림
p = (int *)realloc(p, sizeof(int) * 5);
p[3] = 40;
p[4] = 50;
</code></pre>
<p><code>realloc</code> 은 가능하면 기존 위치에서 크기를 늘린다. 공간이 부족하면 새로운 위치에 메모리를 할당하고, 기존 데이터를 복사한 뒤, 이전 메모리를 해제한다. 기존 데이터는 보존된다.</p>
<p>다만 <code>realloc</code> 도 실패하면 <code>NULL</code> 을 반환한다. 이때 원래 포인터에 바로 대입하면 기존 메모리 주소를 잃어버려 메모리 누수가 발생한다. 안전하게 사용하려면 임시 포인터를 쓰는 것이 좋다.</p>
<pre><code class="language-c">int *temp = (int *)realloc(p, sizeof(int) * 10);
if (temp == NULL) {
    printf("재할당 실패\n");
    free(p);  // 기존 메모리는 수동으로 해제
    return 1;
}
p = temp;
</code></pre>
<h4>메모리 누수</h4>
<p>메모리 누수(memory leak)는 <code>malloc</code>으로 할당한 메모리를 <code>free</code> 하지 않아서 반환되지 않은 메모리가 쌓이는 현상이다.</p>
<pre><code class="language-c">void leak(void) {
    int *p = (int *)malloc(sizeof(int) * 100);
    // free(p)를 하지 않고 함수 종료
}  // p는 지역변수이므로 사라지지만, 힙 메모리는 그대로 남음
</code></pre>
<p><code>p</code>는 스택에 있는 지역변수이므로 함수가 끝나면 사라진다. 하지만 <code>p</code>가 가리키던 힙 메모리는 <code>free</code> 하지 않았으므로 그대로 남아있다. 이 메모리에 접근할 방법도 없고, 해제할 방법도 없다. 이것이 메모리 누수다.</p>
<p>짧은 프로그램에서는 큰 문제가 되지 않을 수 있다(프로그램 종료 시 OS가 전부 회수하므로). 하지만 서버처럼 오래 실행되는 프로그램에서는 메모리가 점점 쌓여서 결국 시스템을 다운시킬 수 있다.</p>
<p>원칙은 간단하다 — <code>malloc</code> <strong>한 만큼</strong> <code>free</code> <strong>한다.</strong></p>
<h3>3-3. 메모리 표준함수</h3>
<p><code>&lt;string.h&gt;</code>에 포함된 메모리 조작 함수들이다. 문자열뿐 아니라 모든 메모리 영역에 사용할 수 있다.</p>
<h4>memset</h4>
<p>메모리 블록을 <strong>특정 값으로 채운다.</strong> 배열을 0으로 초기화하거나 특정 값으로 채울 때 사용한다.</p>
<pre><code class="language-c">#include &lt;string.h&gt;

int arr[5];
memset(arr, 0, sizeof(arr));  // 전부 0으로 채움
</code></pre>
<p><code>memset</code> 은 <strong>바이트 단위</strong> 로 값을 채운다. 두 번째 인자가 각 바이트에 들어갈 값이다. <code>0</code>으로 채우는 것은 문제없지만, <code>int</code> 배열을 <code>1</code>로 채우겠다고 <code>memset(arr, 1, sizeof(arr))</code>를 하면 각 바이트가 <code>0x01</code>이 되어 의도한 정수 <code>1</code>이 아니라 <code>0x01010101</code>(약 1677만)이 들어간다. <code>0</code>이 아닌 값으로 <code>int</code> 배열을 초기화하려면 반복문을 사용해야 한다.</p>
<h4>memcpy</h4>
<p>한 메모리 블록의 내용을 다른 곳으로 <strong>복사</strong> 한다.</p>
<pre><code class="language-c">int src[5] = {1, 2, 3, 4, 5};
int dest[5];

memcpy(dest, src, sizeof(src));
// dest는 이제 {1, 2, 3, 4, 5}
</code></pre>
<p>첫 번째 인자가 목적지, 두 번째가 출발지, 세 번째가 복사할 바이트 수다. <code>strcpy</code>는 문자열만 복사할 수 있지만, <code>memcpy</code>는 어떤 타입의 메모리든 복사할 수 있다.</p>
<p>단, 출발지와 목적지 메모리가 <strong>겹치면 안 된다.</strong> 겹치는 경우에는 <code>memmove</code>를 사용해야 한다.</p>
<h4>memcmp</h4>
<p>두 메모리 블록을 <strong>바이트 단위로 비교</strong> 한다.</p>
<pre><code class="language-c">int a[3] = {1, 2, 3};
int b[3] = {1, 2, 3};
int c[3] = {1, 2, 4};

printf("%d\n", memcmp(a, b, sizeof(a)));  // 0 (같음)
printf("%d\n", memcmp(a, c, sizeof(a)));  // 음수 (a &lt; c)
</code></pre>
<p>반환값이 <code>0</code>이면 같고, 양수면 첫 번째가 크고, 음수면 두 번째가 크다. <code>strcmp</code>가 문자열을 비교하는 것처럼, <code>memcmp</code>는 임의의 메모리를 비교한다. 구조체 두 개를 통째로 비교할 때도 사용할 수 있다.</p>
<h4>memmove</h4>
<p><code>memcpy</code>와 같은 기능이지만, 출발지와 목적지 메모리가 <strong>겹쳐도 안전하게 동작</strong> 한다.</p>
<pre><code class="language-c">int arr[5] = {1, 2, 3, 4, 5};

// arr[0~2]의 내용을 arr[1~3]으로 이동 (겹침 발생)
memmove(&amp;arr[1], &amp;arr[0], sizeof(int) * 3);
// arr는 이제 {1, 1, 2, 3, 5}
</code></pre>
<p><code>memcpy</code> 는 앞에서부터 순서대로 복사하기 때문에, 겹치는 영역에서는 아직 복사하지 않은 데이터를 덮어쓸 수 있다. <code>memmove</code> 는 내부적으로 겹침을 감지하고, 필요하면 뒤에서부터 복사하는 등의 방법으로 안전하게 처리한다.</p>
<p>성능은 <code>memcpy</code>가 약간 더 빠를 수 있다. 메모리가 겹치지 않는 것이 확실하면 <code>memcpy</code>를, 겹칠 가능성이 있으면 <code>memmove</code>를 사용한다.</p>
<hr />
<h2>마무리</h2>
<p>이번 글에서는 C언어의 중급 개념들을 다뤘다. <code>NULL</code> 포인터로 안전한 포인터 사용의 기초를 다지고, 구조체로 복잡한 데이터를 하나로 묶는 방법을 배웠다. 동적 메모리 할당은 C 프로그래밍에서 가장 강력하면서도 가장 위험한 기능이다. <code>malloc</code>으로 할당하고 <code>free</code>로 해제하는 것, 그리고 그 사이에서 <code>NULL</code> 체크와 메모리 누수에 주의하는 것이 핵심이다. 메모리 표준함수들은 이런 메모리를 효율적으로 조작하는 도구다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (4)]]></title><description><![CDATA[1. 포인터
1-1. 포인터란?
포인터는 다른 변수의 메모리 주소를 저장하는 변수 다.
일반 변수가 값을 직접 담고 있다면, 포인터는 "그 값이 어디에 있는지"를 담고 있다. 비유하자면 일반 변수는 서랍 안의 물건이고, 포인터는 그 서랍의 위치를 적어둔 메모지다.
int x = 10;
int *p = &x;  // p는 x의 주소를 저장

printf("x]]></description><link>https://blog.chamdom.dev/c-4</link><guid isPermaLink="true">https://blog.chamdom.dev/c-4</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Sun, 29 Mar 2026 20:07:35 GMT</pubDate><content:encoded><![CDATA[<h2>1. 포인터</h2>
<h3>1-1. 포인터란?</h3>
<p>포인터는 <strong>다른 변수의 메모리 주소를 저장하는 변수</strong> 다.</p>
<p>일반 변수가 값을 직접 담고 있다면, 포인터는 "그 값이 어디에 있는지"를 담고 있다. 비유하자면 일반 변수는 서랍 안의 물건이고, 포인터는 그 서랍의 위치를 적어둔 메모지다.</p>
<pre><code class="language-c">int x = 10;
int *p = &amp;x;  // p는 x의 주소를 저장

printf("x의 값: %d\n", x);       // 10
printf("x의 주소: %p\n", &amp;x);    // 0x7ffeefbff4ac (예시)
printf("p의 값: %p\n", p);       // 0x7ffeefbff4ac (같은 주소)
printf("p가 가리키는 값: %d\n", *p);  // 10
</code></pre>
<p>여기서 두 가지 연산자가 핵심이다.</p>
<p><code>&amp;</code> 는 <strong>주소 연산자</strong> 로, 변수의 메모리 주소를 반환한다. <code>&amp;x</code>는 "x가 메모리 어디에 있는지"를 알려준다.</p>
<p><code>*</code> 는 <strong>역참조(dereference) 연산자</strong> 로, 포인터가 가리키는 주소에 저장된 값을 가져온다. <code>*p</code>는 "p가 가리키는 곳에 가서 값을 읽어라"는 뜻이다.</p>
<p>선언할 때의 <code>*</code>와 사용할 때의 <code>*</code>는 의미가 다르다. <code>int *p</code>에서 <code>*</code>는 "p가 포인터다"라는 선언이고, <code>*p</code>에서 <code>*</code>는 "p가 가리키는 값"이라는 역참조다. 모양은 같지만 맥락이 다르다.</p>
<p>포인터를 통해 원본 변수의 값을 바꿀 수도 있다.</p>
<pre><code class="language-c">int x = 10;
int *p = &amp;x;
*p = 50;
printf("%d\n", x);  // 50 (원본이 바뀜)
</code></pre>
<p>이전에 함수에서 call by value 때문에 원본을 수정할 수 없다고 했는데, 포인터를 사용하면 이 문제를 해결할 수 있다.</p>
<pre><code class="language-c">void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void) {
    int x = 10, y = 20;
    swap(&amp;x, &amp;y);
    printf("x=%d, y=%d\n", x, y);  // x=20, y=10
    return 0;
}
</code></pre>
<p>함수에 주소를 넘기고, 함수 안에서 그 주소를 통해 원본을 직접 수정하는 것이다. <code>scanf</code>에서 <code>&amp;</code>를 붙였던 이유도 이것과 같다.</p>
<h3>1-2. 포인터의 연산</h3>
<p>포인터에 정수를 더하거나 빼면 <strong>자료형 크기만큼</strong> 주소가 이동한다. 이것을 <strong>포인터 산술(pointer arithmetic)</strong> 이라고 한다.</p>
<pre><code class="language-c">int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;  // 배열 이름은 첫 번째 요소의 주소

printf("%d\n", *p);       // 10 (arr[0])
printf("%d\n", *(p + 1)); // 20 (arr[1])
printf("%d\n", *(p + 2)); // 30 (arr[2])
</code></pre>
<p><code>p + 1</code>은 주소값에 1을 더하는 게 아니다. <code>int</code> 포인터이므로 <strong>4바이트(int 크기)만큼</strong> 주소가 이동한다. <code>p</code>가 <code>0x1000</code>이라면 <code>p + 1</code>은 <code>0x1004</code>다.</p>
<p>이 덕분에 포인터와 배열은 밀접하게 연결된다. <code>arr[i]</code>는 사실 <code>*(arr + i)</code>와 같은 표현이다. 배열의 인덱스 접근은 내부적으로 포인터 연산으로 변환된다.</p>
<pre><code class="language-c">int arr[3] = {100, 200, 300};

printf("%d\n", arr[2]);       // 300
printf("%d\n", *(arr + 2));   // 300 (같은 의미)
</code></pre>
<p>포인터끼리의 뺄셈도 가능하다. 두 포인터 사이에 요소가 몇 개 있는지를 반환한다.</p>
<pre><code class="language-c">int arr[5] = {10, 20, 30, 40, 50};
int *p1 = &amp;arr[1];
int *p2 = &amp;arr[4];
printf("%ld\n", p2 - p1);  // 3 (요소 3개 차이)
</code></pre>
<p>단, 포인터끼리의 덧셈은 의미가 없으므로 허용되지 않는다. 주소 + 주소는 아무런 의미를 가지지 않기 때문이다.</p>
<p><code>++</code>와 <code>--</code>도 사용할 수 있다. <code>p++</code>는 다음 요소로, <code>p--</code>는 이전 요소로 이동한다.</p>
<pre><code class="language-c">int arr[3] = {10, 20, 30};
int *p = arr;

printf("%d\n", *p);  // 10
p++;
printf("%d\n", *p);  // 20
p++;
printf("%d\n", *p);  // 30
</code></pre>
<h3>1-3. 포인터에 여러 가지 자료형이 있는 이유</h3>
<p>포인터는 결국 메모리 주소를 저장하는 것이고, 주소는 시스템에 따라 4바이트 또는 8바이트 정수다. 그러면 왜 <code>int *</code>, <code>char *</code>, <code>double *</code>처럼 타입을 구분해야 할까? 어차피 주소 크기는 같은데 전부 <code>void *</code> 하나로 쓰면 안 될까?</p>
<p>이유는 <strong>역참조할 때 몇 바이트를 읽어야 하는지</strong> 를 결정하기 위해서다.</p>
<p><code>int *p</code>로 <code>*p</code>를 하면 그 주소에서 4바이트를 읽어서 정수로 해석한다. <code>char *p</code>로 <code>*p</code>를 하면 1바이트만 읽어서 문자로 해석한다. <code>double *p</code>로 <code>*p</code>를 하면 8바이트를 읽어서 실수로 해석한다.</p>
<p>만약 타입 정보가 없다면, <code>*p</code>를 했을 때 그 주소에서 몇 바이트를 읽어야 하는지, 그리고 읽은 비트를 정수로 해석할지 실수로 해석할지 알 수 없다.</p>
<pre><code class="language-c">int x = 10;
int *ip = &amp;x;
char *cp = (char *)&amp;x;

printf("%d\n", *ip);  // 10 (4바이트를 int로 읽음)
printf("%d\n", *cp);  // 10일 수도, 아닐 수도 (1바이트만 읽음)
</code></pre>
<p>포인터 산술에서도 타입이 중요하다. 앞서 <code>p + 1</code>이 자료형 크기만큼 주소를 이동한다고 했다. <code>int *</code>에서 <code>p + 1</code>은 4바이트 뒤지만, <code>char *</code>에서 <code>p + 1</code>은 1바이트 뒤다. 타입이 없으면 이 계산도 불가능하다.</p>
<p>정리하면 포인터의 타입은 <strong>역참조 시 읽을 크기</strong>와 <strong>포인터 연산의 단위</strong> 를 결정한다.</p>
<hr />
<h2>2. 배열과 포인터</h2>
<p>배열과 포인터는 C에서 밀접하게 연결되어 있지만, <strong>같은 것은 아니다.</strong></p>
<p>배열 이름은 첫 번째 요소의 주소를 가리키는 <strong>상수 포인터</strong> 처럼 동작한다. "상수"라는 것은 배열 이름 자체에 다른 주소를 대입할 수 없다는 뜻이다.</p>
<pre><code class="language-c">int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;  // OK — 배열의 시작 주소를 포인터에 대입

p++;       // OK — 포인터는 이동 가능
// arr++;  // 에러! — 배열 이름은 이동 불가 (상수)
</code></pre>
<p>배열을 포인터로 순회하는 것은 C에서 매우 흔한 패턴이다.</p>
<pre><code class="language-c">int arr[5] = {10, 20, 30, 40, 50};

// 인덱스 방식
for (int i = 0; i &lt; 5; i++) {
    printf("%d ", arr[i]);
}

// 포인터 방식
int *p = arr;
for (int i = 0; i &lt; 5; i++) {
    printf("%d ", *(p + i));
}

// 포인터 증가 방식
int *q = arr;
for (int i = 0; i &lt; 5; i++) {
    printf("%d ", *q);
    q++;
}
</code></pre>
<p>세 가지 모두 같은 결과를 출력한다. <code>arr[i]</code>, <code>*(arr + i)</code>, <code>*(p + i)</code> 는 전부 같은 의미다.</p>
<p>함수에 배열을 넘기면 배열이 통째로 복사되는 것이 아니라 <strong>첫 번째 요소의 주소(포인터)만 전달</strong> 된다. 그래서 함수 안에서 배열을 수정하면 원본이 바뀐다.</p>
<pre><code class="language-c">void doubleAll(int *arr, int size) {
    for (int i = 0; i &lt; size; i++) {
        arr[i] *= 2;  // 원본 배열이 수정됨
    }
}

int main(void) {
    int nums[3] = {1, 2, 3};
    doubleAll(nums, 3);
    // nums는 이제 {2, 4, 6}
    return 0;
}
</code></pre>
<p>함수 매개변수에서 <code>int arr[]</code>와 <code>int *arr</code>는 같은 의미다. 둘 다 포인터를 받는다.</p>
<hr />
<h2>3. 함수 포인터</h2>
<p>C에서는 함수도 메모리에 존재하므로 주소를 가진다. <strong>함수 포인터</strong> 는 함수의 주소를 저장하는 포인터다.</p>
<p>선언 문법이 조금 복잡하다.</p>
<pre><code class="language-c">반환타입 (*포인터이름)(매개변수타입들);
</code></pre>
<pre><code class="language-c">int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main(void) {
    int (*op)(int, int);  // 함수 포인터 선언

    op = add;
    printf("%d\n", op(3, 5));  // 8

    op = subtract;
    printf("%d\n", op(3, 5));  // -2

    return 0;
}
</code></pre>
<p><code>int (*op)(int, int)</code> 에서 <code>op</code>는 "int 두 개를 받아서 int를 반환하는 함수"를 가리킬 수 있는 포인터다. <code>add</code>든 <code>subtract</code>든, 시그니처가 같은 함수라면 어디든 가리킬 수 있다.</p>
<p>함수 이름 자체가 함수의 주소이므로 <code>op = add</code>에서 <code>&amp;</code>를 붙이지 않아도 된다. 배열 이름이 배열의 주소인 것과 같은 원리다.</p>
<p>괄호 위치가 중요하다. <code>int (*op)(int, int)</code>는 함수 포인터이고, <code>int *op(int, int)</code>는 <code>int *</code>를 반환하는 함수 선언이다. <code>*op</code>를 괄호로 감싸야 포인터라는 뜻이 된다.</p>
<p>함수 포인터가 유용한 대표적인 사례는 <strong>콜백(callback)</strong> 패턴이다. 어떤 함수에 "실행할 함수"를 인자로 넘기는 것이다. JavaScript에서 <code>addEventListener</code>에 콜백을 넘기는 것과 같은 개념이다.</p>
<pre><code class="language-c">void calculate(int a, int b, int (*operation)(int, int)) {
    printf("결과: %d\n", operation(a, b));
}

int main(void) {
    calculate(10, 3, add);       // 결과: 13
    calculate(10, 3, subtract);  // 결과: 7
    return 0;
}
</code></pre>
<p><code>calculate</code> 함수는 어떤 연산을 할지 모른다. 호출할 때 연산 함수를 넘겨주면 그걸 실행할 뿐이다. 이렇게 하면 <code>calculate</code> 함수를 수정하지 않고도 새로운 연산을 추가할 수 있다.</p>
<p>C 표준 라이브러리의 <code>qsort</code>도 함수 포인터를 활용한 대표적인 예다. 정렬 기준을 함수 포인터로 받아서, 오름차순/내림차순 등 다양한 방식으로 정렬할 수 있게 한다.</p>
<pre><code class="language-c">#include &lt;stdlib.h&gt;

int compare(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int main(void) {
    int arr[5] = {50, 20, 40, 10, 30};
    qsort(arr, 5, sizeof(int), compare);
    // arr는 이제 {10, 20, 30, 40, 50}
    return 0;
}
</code></pre>
<hr />
<h2>마무리</h2>
<p>포인터는 C언어에서 가장 중요하면서도 가장 어렵다고 느끼는 개념이다. 핵심은 <code>&amp;</code>(주소를 구함)와 <code>*</code>(주소로 가서 값을 읽음) 이 두 연산자의 의미를 정확히 아는 것이다. 포인터의 타입은 역참조할 때 몇 바이트를 읽을지, 포인터 연산에서 몇 바이트씩 이동할지를 결정한다. 이 원리를 이해하면 배열과 포인터의 관계, 함수 포인터까지 자연스럽게 따라온다.</p>
]]></content:encoded></item><item><title><![CDATA[자료구조 (1)]]></title><description><![CDATA[1. 배열
1-1. 배열이란?
배열은 같은 타입의 데이터를 연속된 메모리 공간에 나란히 저장하는 자료구조 다.
변수 하나에는 값 하나만 저장할 수 있다. 학생 100명의 점수를 저장해야 한다면 int score1, score2, score3, ... score100; 이렇게 변수를 100개 만들어야 할까? 비현실적이다. 배열을 쓰면 int score[100]]></description><link>https://blog.chamdom.dev/1</link><guid isPermaLink="true">https://blog.chamdom.dev/1</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Sun, 29 Mar 2026 20:03:11 GMT</pubDate><content:encoded><![CDATA[<h2>1. 배열</h2>
<h3>1-1. 배열이란?</h3>
<p>배열은 <strong>같은 타입의 데이터를 연속된 메모리 공간에 나란히 저장하는 자료구조</strong> 다.</p>
<p>변수 하나에는 값 하나만 저장할 수 있다. 학생 100명의 점수를 저장해야 한다면 <code>int score1, score2, score3, ... score100;</code> 이렇게 변수를 100개 만들어야 할까? 비현실적이다. 배열을 쓰면 <code>int score[100];</code> 한 줄로 해결된다.</p>
<p>JavaScript의 배열과 비슷한 개념이지만, 결정적인 차이가 있다. JavaScript 배열은 서로 다른 타입을 섞어 넣을 수 있고 크기도 자유롭게 변한다. C 배열은 <strong>하나의 타입만</strong> 저장할 수 있고, <strong>크기가 고정</strong> 되어 있다.</p>
<h3>1-2. 배열의 이해와 사용</h3>
<h4>배열의 선언 구조</h4>
<pre><code class="language-c">자료형 배열이름[크기];
</code></pre>
<pre><code class="language-c">int scores[5];        // int 5개를 담는 배열
double temps[7];      // double 7개를 담는 배열
char name[20];        // char 20개를 담는 배열 (문자열용)
</code></pre>
<p><code>int scores[5]</code> 를 선언하면 메모리에 <code>int</code> 크기(4바이트) × 5 = 20바이트의 연속된 공간이 확보된다. 각 요소는 <code>scores[0]</code>부터 <code>scores[4]</code>까지 인덱스로 접근한다. 인덱스는 <strong>0부터 시작</strong> 한다.</p>
<pre><code class="language-c">scores[0] = 90;
scores[1] = 85;
scores[2] = 78;
printf("%d\n", scores[1]);  // 85
</code></pre>
<h4>배열의 속성</h4>
<p>배열 이름은 <strong>배열 첫 번째 요소의 주소</strong> 를 가리킨다. 즉 <code>scores</code>와 <code>&amp;scores[0]</code>은 같은 값이다.</p>
<pre><code class="language-c">int scores[5] = {90, 85, 78, 92, 88};
printf("%p\n", scores);       // 배열 시작 주소
printf("%p\n", &amp;scores[0]);   // 같은 주소
</code></pre>
<p>배열의 전체 크기는 <code>sizeof</code> 연산자로 구할 수 있다. 요소 개수는 전체 크기를 요소 하나의 크기로 나누면 된다.</p>
<pre><code class="language-c">int arr[5];
printf("전체 크기: %lu\n", sizeof(arr));        // 20 (4 × 5)
printf("요소 개수: %lu\n", sizeof(arr) / sizeof(arr[0]));  // 5
</code></pre>
<p>다만 배열을 함수에 매개변수로 넘기면 <strong>포인터로 변환</strong> 되기 때문에, 함수 안에서 <code>sizeof</code>로 배열 크기를 구할 수 없다. 그래서 배열을 함수에 넘길 때는 크기도 함께 전달하는 것이 일반적이다.</p>
<pre><code class="language-c">void printArray(int arr[], int size) {
    for (int i = 0; i &lt; size; i++) {
        printf("%d ", arr[i]);
    }
}
</code></pre>
<h4>배열의 크기는 왜 상수여야 하는가?</h4>
<blockquote>
<p><strong>컴파일타임과 런타임</strong></p>
<p>이 개념을 이해하려면 먼저 <strong>컴파일타임(compile time)</strong> 과 <strong>런타임(runtime)</strong> 의 차이를 알아야 한다.</p>
<p><strong>컴파일타임</strong> 은 소스 코드가 기계어로 번역되는 시점이다. 컴파일러가 코드를 읽고, 문법을 검사하고, 실행 파일을 만드는 과정이 여기에 해당한다. <strong>런타임</strong> 은 그 실행 파일이 실제로 실행되는 시점이다. 사용자 입력을 받거나, 파일을 읽거나, 계산 결과가 나오는 것은 전부 런타임에 일어난다.</p>
<p>쉽게 말해 컴파일타임은 "코드를 번역하는 시점", 런타임은 "프로그램이 돌아가는 시점"이다.</p>
</blockquote>
<p>C에서 일반 배열(정적 배열)은 <strong>스택 메모리</strong> 에 할당된다. 스택은 함수가 호출될 때 필요한 공간을 미리 계산해서 확보하는 구조다. 이 계산은 <strong>컴파일타임</strong> 에 이루어진다.</p>
<pre><code class="language-c">int arr[5];  // 컴파일러가 "20바이트 필요하겠구나" 판단 가능
</code></pre>
<p>컴파일러는 <code>5</code>라는 숫자를 보고 4 × 5 = 20바이트를 확보하면 된다는 걸 바로 알 수 있다. 그런데 크기가 변수라면 어떻게 될까?</p>
<pre><code class="language-c">int n;
scanf("%d", &amp;n);
int arr[n];  // n은 런타임에야 알 수 있음
</code></pre>
<p><code>n</code>의 값은 사용자가 입력해야 알 수 있다. 컴파일 시점에서는 배열에 얼마만큼의 메모리를 잡아야 할지 결정할 수 없다. 그래서 C89/C90 표준에서는 배열 크기를 반드시 <strong>상수</strong> 로 지정해야 한다.</p>
<pre><code class="language-c">int arr[5];            // OK — 상수
const int SIZE = 5;
int arr2[SIZE];        // 컴파일러에 따라 다름 (C에서 const는 진정한 상수가 아님)
#define MAX 100
int arr3[MAX];         // OK — 매크로 상수는 전처리 단계에서 치환됨
</code></pre>
<p>C99부터는 <strong>가변 길이 배열(VLA, Variable Length Array)</strong> 이 도입되어 <code>int arr[n]</code> 같은 문법이 허용되기도 한다. 하지만 Visual Studio에서는 VLA를 지원하지 않으므로, 수업 환경에서는 배열 크기를 항상 상수로 지정해야 한다.</p>
<p>크기가 런타임에 결정되어야 하는 경우에는 <code>malloc</code>을 사용한 <strong>동적 메모리 할당</strong> 으로 해결한다. 이건 이후에 포인터와 함께 다루게 된다.</p>
<h4>배열의 초기화</h4>
<p>배열을 선언하면서 동시에 값을 넣을 수 있다.</p>
<pre><code class="language-c">int arr[5] = {10, 20, 30, 40, 50};
</code></pre>
<p>요소 수보다 적게 초기화하면 나머지는 <strong>자동으로 0</strong> 이 된다.</p>
<pre><code class="language-c">int arr[5] = {10, 20};  // {10, 20, 0, 0, 0}
int arr2[5] = {0};       // 전부 0으로 초기화
</code></pre>
<p>크기를 생략하면 초기화한 요소 수에 맞춰 자동으로 크기가 결정된다.</p>
<pre><code class="language-c">int arr[] = {10, 20, 30};  // 크기 3짜리 배열
</code></pre>
<p>초기화하지 않은 지역 배열에는 <strong>쓰레기값</strong> 이 들어있다. 전역이나 <code>static</code> 배열은 자동으로 0으로 초기화된다.</p>
<pre><code class="language-c">int globalArr[5];  // 전역 — 전부 0

void foo(void) {
    int localArr[5];         // 지역 — 쓰레기값
    static int staticArr[5]; // 정적 — 전부 0
}
</code></pre>
<h3>1-3. 배열과 문자열</h3>
<p>C에는 JavaScript의 <code>string</code> 같은 문자열 타입이 없다. 대신 <code>char</code> <strong>배열</strong> 로 문자열을 표현한다.</p>
<h4>문자열 변수</h4>
<p>C에서 문자열은 <strong>널 문자(</strong><code>\0</code><strong>)로 끝나는</strong> <code>char</code> <strong>배열</strong> 이다. 널 문자는 문자열의 끝을 알려주는 표식이다. 모든 문자열 처리 함수(<code>printf</code>의 <code>%s</code>, <code>strlen</code>, <code>strcpy</code> 등)는 이 <code>\0</code>을 기준으로 문자열의 끝을 판단한다.</p>
<pre><code class="language-c">char name[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("%s\n", name);  // Hello
</code></pre>
<p>매번 이렇게 쓰면 불편하니까, 문자열 리터럴로 간단하게 초기화할 수 있다. 이 경우 <code>\0</code>이 자동으로 붙는다.</p>
<pre><code class="language-c">char name[6] = "Hello";  // 자동으로 끝에 '\0' 추가
</code></pre>
<p><code>"Hello"</code>는 문자 5개지만 <code>\0</code>까지 포함하면 6바이트가 필요하다. 배열 크기를 5로 잡으면 <code>\0</code>이 들어갈 자리가 없어서 문자열 함수들이 <strong>오작동</strong> 한다. 배열 크기는 항상 <strong>문자 수 + 1</strong> 이상으로 잡아야 한다.</p>
<pre><code class="language-c">char name[] = "Hello";  // 크기를 생략하면 자동으로 6 (5 + 널 문자)
</code></pre>
<p>문자 하나(<code>'A'</code>)와 문자열 하나(<code>"A"</code>)는 다르다. <code>'A'</code>는 <code>char</code> 타입으로 1바이트이고, <code>"A"</code>는 <code>'A'</code> + <code>'\0'</code>으로 구성된 2바이트짜리 <code>char</code> 배열이다.</p>
<p>문자열을 입력받을 때는 <code>scanf</code>에서 <code>%s</code>를 사용한다. 이때 배열 이름 자체가 주소이므로 <code>&amp;</code>를 붙이지 않는다.</p>
<pre><code class="language-c">char name[20];
printf("이름을 입력하세요: ");
scanf("%s", name);  // &amp; 없이 배열 이름만
printf("안녕하세요, %s\n", name);
</code></pre>
<p>다만 <code>scanf</code>의 <code>%s</code>는 공백을 만나면 입력을 멈춘다. "Hong Gil Dong"을 입력하면 "Hong"만 저장된다. 공백을 포함한 문자열을 입력받으려면 <code>fgets</code>를 사용한다.</p>
<pre><code class="language-c">char line[100];
fgets(line, sizeof(line), stdin);
</code></pre>
<p>문자열 관련 주요 함수들은 <code>&lt;string.h&gt;</code>에 들어있다. <code>strlen</code>은 문자열 길이를 반환하고, <code>strcpy</code>는 문자열을 복사하고, <code>strcmp</code>는 두 문자열을 비교한다. C에서는 <code>=</code> 연산자로 문자열을 대입하거나 <code>==</code>로 비교할 수 없으므로, 반드시 이 함수들을 사용해야 한다.</p>
<pre><code class="language-c">#include &lt;string.h&gt;

char src[] = "Hello";
char dest[20];

strcpy(dest, src);              // 복사
printf("%lu\n", strlen(dest));  // 5 (널 문자 제외한 길이)

if (strcmp(src, dest) == 0) {
    printf("같은 문자열\n");
}
</code></pre>
<hr />
<h2>마무리</h2>
<p>배열은 C언어에서 데이터를 묶어서 관리하는 가장 기본적인 방법이다. 핵심은 세 가지다 — 크기가 고정이라는 것, 인덱스가 0부터 시작한다는 것, 그리고 배열 이름이 첫 번째 요소의 주소라는 것. 특히 문자열이 <code>\0</code>으로 끝나는 <code>char</code> 배열이라는 점은 C 프로그래밍에서 끊임없이 등장하는 개념이니 확실히 이해해두는 것이 좋다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (3)]]></title><description><![CDATA[1. 함수(Function)
1-1. 함수란?
함수는 특정 작업을 수행하는 코드 묶음 이다. 반복되는 코드를 하나로 묶어 이름을 붙이고, 필요할 때마다 호출해서 사용한다.
JavaScript에서 function으로 함수를 만들어 쓰는 것과 같은 개념이다. 다만 C에서는 반환 타입을 명시해야 하고, 매개변수에도 타입을 지정해야 한다는 차이가 있다.
함수를 사]]></description><link>https://blog.chamdom.dev/c-3</link><guid isPermaLink="true">https://blog.chamdom.dev/c-3</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Sun, 29 Mar 2026 19:58:26 GMT</pubDate><content:encoded><![CDATA[<h2>1. 함수(Function)</h2>
<h3>1-1. 함수란?</h3>
<p>함수는 <strong>특정 작업을 수행하는 코드 묶음</strong> 이다. 반복되는 코드를 하나로 묶어 이름을 붙이고, 필요할 때마다 호출해서 사용한다.</p>
<p>JavaScript에서 <code>function</code>으로 함수를 만들어 쓰는 것과 같은 개념이다. 다만 C에서는 반환 타입을 명시해야 하고, 매개변수에도 타입을 지정해야 한다는 차이가 있다.</p>
<p>함수를 사용하는 이유는 크게 세 가지다.</p>
<p><strong>코드 재사용</strong> — 같은 로직을 여러 번 작성할 필요 없이, 한 번 정의하고 호출만 하면 된다. <strong>가독성</strong> — 복잡한 프로그램을 작은 단위로 쪼개면 코드를 읽고 이해하기 쉬워진다. <strong>유지보수</strong> — 로직을 수정할 때 함수 하나만 고치면 호출하는 곳 전체에 반영된다.</p>
<h3>1-2. 함수의 형태</h3>
<p>C언어의 함수는 <strong>반환타입</strong>, <strong>함수이름</strong>, <strong>매개변수</strong>, <strong>함수 본체</strong> 로 구성된다.</p>
<pre><code class="language-c">반환타입 함수이름(매개변수) {
    // 함수 본체
    return 반환값;
}
</code></pre>
<p>실제 예시를 보면 이렇다.</p>
<pre><code class="language-c">int add(int a, int b) {
    return a + b;
}
</code></pre>
<p><code>int</code> 는 이 함수가 정수를 반환한다는 뜻이고, <code>add</code> 는 함수 이름이다. <code>int a, int b</code> 는 매개변수로, 함수를 호출할 때 전달받는 값이다. <code>return a + b</code> 로 결과를 돌려준다.</p>
<p>반환할 값이 없으면 반환타입을 <code>void</code> 로 지정한다. 이 경우 <code>return</code> 은 생략하거나 <code>return;</code> 만 쓴다.</p>
<pre><code class="language-c">void printHello(void) {
    printf("Hello!\n");
}
</code></pre>
<p>매개변수가 없을 때는 괄호 안에 <code>void</code> 를 쓰거나 비워둔다. C에서는 <code>void</code> 를 명시하는 것이 정확한 표현이다. 괄호를 비워두면 "매개변수가 정해지지 않았다"는 의미가 되어 의도와 다를 수 있다.</p>
<h3>1-3. 함수의 사용방법</h3>
<p>함수를 사용하려면 <strong>선언(프로토타입)</strong>, <strong>정의(구현)</strong>, <strong>호출</strong> 세 단계를 알아야 한다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int add(int a, int b);  // 함수 선언 (프로토타입)

int main(void) {
    int result = add(3, 5);  // 함수 호출
    printf("%d\n", result);  // 8
    return 0;
}

int add(int a, int b) {  // 함수 정의 (구현)
    return a + b;
}
</code></pre>
<p><strong>함수 선언(프로토타입)</strong> 은 컴파일러에게 "이런 함수가 있을 것이다"라고 미리 알려주는 것이다. 반환타입, 함수이름, 매개변수 타입만 적고 세미콜론으로 끝낸다. <code>main</code> 보다 아래에 함수를 정의할 때 필요하다.</p>
<p><strong>함수 정의</strong> 는 함수가 실제로 무슨 일을 하는지 구현하는 것이다.</p>
<p><strong>함수 호출</strong> 은 정의된 함수를 실행하는 것이다. 함수 이름 뒤에 괄호를 쓰고 인자를 전달하면 된다.</p>
<p>만약 함수를 <code>main</code> 위에 정의하면 프로토타입 선언 없이도 사용할 수 있다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int add(int a, int b) {
    return a + b;
}

int main(void) {
    printf("%d\n", add(3, 5));
    return 0;
}
</code></pre>
<p>C에서 함수의 인자 전달은 <strong>값에 의한 전달(call by value)</strong> 이다. 함수에 변수를 넘기면 원본이 아니라 <strong>복사본</strong> 이 전달된다. 함수 안에서 매개변수 값을 바꿔도 원본 변수에는 영향이 없다.</p>
<pre><code class="language-c">void change(int x) {
    x = 100;  // 복사본만 바뀜
}

int main(void) {
    int a = 5;
    change(a);
    printf("%d\n", a);  // 여전히 5
    return 0;
}
</code></pre>
<p>원본을 수정하려면 포인터를 사용해서 주소를 넘겨야 한다. 이건 포인터를 배울 때 다시 다루게 된다.</p>
<h3>1-4. 함수의 범위</h3>
<p>함수의 범위란 함수가 <strong>어디서 호출될 수 있는지</strong> 를 의미한다.</p>
<p>기본적으로 같은 소스 파일 안에서는 어디서든 함수를 호출할 수 있다. 단, 호출하는 시점에서 컴파일러가 그 함수의 존재를 알고 있어야 한다. 그래서 프로토타입 선언을 위에 적거나, 함수 정의 자체를 호출보다 위에 두는 것이다.</p>
<p>여러 소스 파일로 나뉜 프로젝트에서는 <strong>헤더 파일(</strong><code>.h</code><strong>)</strong> 에 함수 프로토타입을 모아두고, 각 소스 파일에서 <code>#include</code> 로 포함시켜 사용한다.</p>
<pre><code class="language-c">// math_utils.h
int add(int a, int b);
int subtract(int a, int b);

// math_utils.c
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

// main.c
#include &lt;stdio.h&gt;
#include "math_utils.h"

int main(void) {
    printf("%d\n", add(3, 5));
    printf("%d\n", subtract(10, 4));
    return 0;
}
</code></pre>
<p>함수 앞에 <code>static</code> 을 붙이면 해당 소스 파일 안에서만 사용할 수 있는 <strong>내부 함수</strong> 가 된다. 다른 파일에서는 호출할 수 없다.</p>
<pre><code class="language-c">static int helper(int x) {
    return x * 2;
}
</code></pre>
<hr />
<h2>2. 변수</h2>
<p>변수는 선언되는 위치와 키워드에 따라 <strong>생존 범위(scope)</strong> 와 <strong>생존 기간(lifetime)</strong> 이 달라진다.</p>
<h3>2-1. 지역변수 (Local Variable)</h3>
<p>함수나 블록(<code>{}</code>) 안에서 선언된 변수다. 해당 블록 안에서만 사용할 수 있고, 블록이 끝나면 메모리에서 사라진다.</p>
<pre><code class="language-c">void foo(void) {
    int x = 10;  // 지역변수
    printf("%d\n", x);
}  // 여기서 x는 소멸

int main(void) {
    foo();
    // printf("%d\n", x);  // 에러! x는 foo 안에서만 존재
    return 0;
}
</code></pre>
<p>지역변수는 <strong>스택(stack) 메모리</strong> 에 할당된다. 함수가 호출되면 생성되고, 함수가 끝나면 자동으로 해제된다. 초기화하지 않으면 <strong>쓰레기값</strong> 이 들어있다.</p>
<p>같은 이름의 지역변수가 서로 다른 함수에 있어도 완전히 별개의 변수다.</p>
<pre><code class="language-c">void funcA(void) {
    int x = 10;
    printf("A: %d\n", x);  // 10
}

void funcB(void) {
    int x = 20;
    printf("B: %d\n", x);  // 20
}
</code></pre>
<p><code>if</code>, <code>for</code>, <code>while</code> 같은 블록 안에서 선언한 변수도 지역변수다. 해당 블록이 끝나면 사라진다.</p>
<pre><code class="language-c">for (int i = 0; i &lt; 5; i++) {
    int temp = i * 2;  // 매 반복마다 생성/소멸
}
// i와 temp 모두 여기서 사용 불가
</code></pre>
<h3>2-2. 전역변수 (Global Variable)</h3>
<p>함수 바깥에서 선언된 변수다. 프로그램이 시작될 때 생성되고, 프로그램이 종료될 때까지 유지된다. 모든 함수에서 접근할 수 있다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int count = 0;  // 전역변수

void increment(void) {
    count++;
}

void printCount(void) {
    printf("count: %d\n", count);
}

int main(void) {
    increment();
    increment();
    increment();
    printCount();  // count: 3
    return 0;
}
</code></pre>
<p>전역변수는 <strong>데이터 영역(data segment)</strong> 에 할당된다. 초기화하지 않으면 지역변수와 달리 <strong>자동으로 0으로 초기화</strong> 된다.</p>
<p>편리해 보이지만 전역변수의 남용은 위험하다. 어디서든 값을 바꿀 수 있기 때문에, 프로그램이 커지면 어떤 함수가 값을 변경했는지 추적하기 어려워진다. 버그의 원인이 되기 쉽고 유지보수도 힘들어진다. 꼭 필요한 경우가 아니라면 지역변수를 사용하고, 함수 간 데이터 전달은 매개변수와 반환값으로 처리하는 것이 좋다.</p>
<p>전역변수와 지역변수의 이름이 같으면 <strong>지역변수가 우선</strong> 한다.</p>
<pre><code class="language-c">int x = 100;  // 전역변수

void foo(void) {
    int x = 5;  // 지역변수가 전역변수를 가림
    printf("%d\n", x);  // 5
}
</code></pre>
<h3>2-3. 정적변수 (Static Variable)</h3>
<p>지역변수에 <code>static</code> 키워드를 붙이면 정적변수가 된다. 지역변수처럼 <strong>해당 함수 안에서만 접근</strong> 할 수 있지만, 함수가 끝나도 <strong>값이 사라지지 않고 유지</strong> 된다.</p>
<pre><code class="language-c">void counter(void) {
    static int count = 0;  // 최초 호출 때만 초기화됨
    count++;
    printf("호출 횟수: %d\n", count);
}

int main(void) {
    counter();  // 호출 횟수: 1
    counter();  // 호출 횟수: 2
    counter();  // 호출 횟수: 3
    return 0;
}
</code></pre>
<p>일반 지역변수였다면 함수가 호출될 때마다 <code>count</code>가 0으로 초기화되어 항상 1이 출력될 것이다. 하지만 <code>static</code> 으로 선언했기 때문에 초기화는 처음 한 번만 실행되고, 이후에는 이전 값이 그대로 유지된다.</p>
<p>정적변수는 전역변수와 마찬가지로 <strong>데이터 영역</strong> 에 저장된다. 초기화하지 않으면 자동으로 0이 된다. 다만 접근 범위는 선언된 함수 안으로 제한되므로, 전역변수의 단점(어디서든 접근 가능)을 피하면서도 값을 유지할 수 있다.</p>
<p>정리하면 이렇다.</p>
<p><strong>지역변수</strong> 는 함수 안에서만 접근 가능하고, 함수가 끝나면 사라진다. <strong>전역변수</strong> 는 어디서든 접근 가능하고, 프로그램이 끝날 때까지 유지된다. <strong>정적변수</strong> 는 함수 안에서만 접근 가능하지만, 프로그램이 끝날 때까지 값이 유지된다. 지역변수의 스코프와 전역변수의 수명을 합쳐놓은 것이라고 보면 된다.</p>
<hr />
<h2>마무리</h2>
<p>함수는 코드를 구조화하는 가장 기본적인 단위이고, 변수의 범위는 프로그램의 데이터 흐름을 결정한다. 함수와 변수의 스코프를 정확히 이해하면, 코드가 길어져도 데이터가 어디에 있고 어디까지 영향을 미치는지 파악할 수 있다. 이 개념은 이후 포인터와 동적 메모리 할당을 배울 때 더욱 중요해진다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (2)]]></title><description><![CDATA[1. 입력함수
1-1. scanf
printf 가 화면에 출력하는 함수라면, scanf 는 사용자로부터 값을 입력받는 함수다.
int age;
printf("나이를 입력하세요: ");
scanf("%d", &age);
printf("입력한 나이: %d\n", age);

scanf 의 첫 번째 인자는 서식 문자(%d, %f, %c 등)이고, 두 번째 인자부]]></description><link>https://blog.chamdom.dev/c-2</link><guid isPermaLink="true">https://blog.chamdom.dev/c-2</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Sun, 29 Mar 2026 19:50:14 GMT</pubDate><content:encoded><![CDATA[<h2>1. 입력함수</h2>
<h3>1-1. scanf</h3>
<p><code>printf</code> 가 화면에 출력하는 함수라면, <code>scanf</code> 는 사용자로부터 값을 입력받는 함수다.</p>
<pre><code class="language-c">int age;
printf("나이를 입력하세요: ");
scanf("%d", &amp;age);
printf("입력한 나이: %d\n", age);
</code></pre>
<p><code>scanf</code> 의 첫 번째 인자는 서식 문자(<code>%d</code>, <code>%f</code>, <code>%c</code> 등)이고, 두 번째 인자부터는 입력값을 저장할 변수의 <strong>주소</strong> 를 넘긨다. 여러 값을 한 번에 입력받을 수도 있다.</p>
<pre><code class="language-c">int x, y;
scanf("%d %d", &amp;x, &amp;y);
</code></pre>
<p>이때 사용자는 값 사이에 공백이나 엔터를 넣어서 구분하면 된다.</p>
<p>주의할 점이 하나 있다. 문자(<code>%c</code>)를 입력받을 때는 이전 입력에서 남은 개행문자(<code>\n</code>)가 버퍼에 남아있어서 의도하지 않은 값이 들어갈 수 있다. 이 경우 <code>scanf</code> 의 서식 문자 앞에 공백을 넣어주면 해결된다.</p>
<pre><code class="language-c">char ch;
scanf(" %c", &amp;ch);  // %c 앞에 공백 추가
</code></pre>
<h3>1-2. &amp; 를 사용하는 이유</h3>
<p><code>scanf</code> 에서 변수 앞에 <code>&amp;</code> 를 붙이는 이유는 <strong>메모리 주소를 넘겨야 하기 때문</strong> 이다.</p>
<p><code>scanf</code> 는 사용자가 입력한 값을 특정 메모리 공간에 써넣는 함수다. 그러려면 "어디에 쓸지"를 알아야 하고, 그 정보가 바로 변수의 주소다. <code>&amp;</code> 연산자는 변수의 메모리 주소를 반환한다.</p>
<pre><code class="language-c">int x;
scanf("%d", &amp;x);   // x의 주소를 넘겨서 그 위치에 값을 저장
</code></pre>
<p>만약 <code>&amp;</code> 를 빼면 <code>scanf</code> 는 <code>x</code> 에 들어있는 <strong>값</strong> 을 주소로 해석하려 한다. 초기화되지 않은 변수라면 쓰레기값이 주소로 쓰이면서 프로그램이 비정상 종료될 수 있다.</p>
<p>단, 배열 이름은 그 자체가 배열 첫 번째 원소의 주소이므로 <code>&amp;</code> 를 붙이지 않는다.</p>
<pre><code class="language-c">char name[20];
scanf("%s", name);  // 배열 이름 자체가 주소이므로 &amp; 불필요
</code></pre>
<hr />
<h2>2. 연산자</h2>
<h3>2-1. 산술 연산자</h3>
<p>사칙연산과 나머지를 구하는 연산자다.</p>
<p><code>+</code> 는 덧셈, <code>-</code> 는 뺄셈, <code>*</code> 는 곱셈, <code>/</code> 는 나눗셈, <code>%</code> 는 나머지를 구한다.</p>
<pre><code class="language-c">int a = 10, b = 3;
printf("%d\n", a + b);   // 13
printf("%d\n", a - b);   // 7
printf("%d\n", a * b);   // 30
printf("%d\n", a / b);   // 3 (정수끼리 나누면 소수점 버림)
printf("%d\n", a % b);   // 1
</code></pre>
<p>여기서 주의할 점은 <strong>정수끼리의 나눗셈</strong> 이다. <code>10 / 3</code> 의 결과는 <code>3.333...</code> 이 아니라 <code>3</code> 이다. 소수점 이하가 버려지기 때문이다. 실수 결과를 얻으려면 피연산자 중 하나를 실수형으로 만들어야 한다.</p>
<pre><code class="language-c">printf("%f\n", 10.0 / 3);    // 3.333333
printf("%f\n", (double)10 / 3);  // 캐스팅으로도 가능
</code></pre>
<h3>2-2. 대입 연산자</h3>
<p>변수에 값을 저장하는 연산자다. <code>=</code> 가 기본이고, 산술 연산과 결합한 복합 대입 연산자도 있다.</p>
<pre><code class="language-c">int x = 10;
x += 5;   // x = x + 5  → 15
x -= 3;   // x = x - 3  → 12
x *= 2;   // x = x * 2  → 24
x /= 4;   // x = x / 4  → 6
x %= 4;   // x = x % 4  → 2
</code></pre>
<p><code>x += 5</code> 는 <code>x = x + 5</code> 와 같다. 코드가 짧아지고 의도도 더 명확해진다.</p>
<h3>2-3. 증감 연산자</h3>
<p>변수의 값을 1 증가시키거나 1 감소시키는 연산자다. <code>++</code> 가 증가, <code>--</code> 가 감소다.</p>
<pre><code class="language-c">int a = 5;
a++;   // a = 6
a--;   // a = 5
</code></pre>
<p>중요한 건 <strong>전위(prefix)와 후위(postfix)의 차이</strong> 다.</p>
<pre><code class="language-c">int a = 5;
int b = a++;  // b = 5, a = 6 (먼저 대입하고 나중에 증가)
int c = ++a;  // c = 7, a = 7 (먼저 증가하고 나중에 대입)
</code></pre>
<p><code>a++</code> 는 현재 값을 먼저 사용한 뒤 증가시키고, <code>++a</code> 는 먼저 증가시킨 뒤 그 값을 사용한다. 단독으로 <code>a++;</code> 처럼 쓸 때는 차이가 없지만, 다른 연산과 함께 쓸 때는 결과가 달라지므로 주의해야 한다.</p>
<h3>2-4. 관계 연산자</h3>
<p>두 값을 비교해서 참(<code>1</code>) 또는 거짓(<code>0</code>)을 반환한다. 조건문과 반복문에서 핵심적으로 사용된다.</p>
<p><code>==</code> 는 같은지, <code>!=</code> 는 다른지, <code>&gt;</code> 는 큰지, <code>&lt;</code> 는 작은지, <code>&gt;=</code> 는 크거나 같은지, <code>&lt;=</code> 는 작거나 같은지 비교한다.</p>
<pre><code class="language-c">int a = 5, b = 3;
printf("%d\n", a == b);  // 0 (거짓)
printf("%d\n", a != b);  // 1 (참)
printf("%d\n", a &gt; b);   // 1 (참)
</code></pre>
<p>C언어에는 <code>boolean</code> 타입이 기본적으로 없다. 대신 <code>0</code> 이면 거짓, <strong>0이 아닌 모든 값</strong> 이면 참으로 취급한다. C99부터 <code>&lt;stdbool.h&gt;</code> 를 포함하면 <code>true</code>, <code>false</code> 를 사용할 수 있긴 하다.</p>
<p>흔한 실수로 <code>==</code>(비교)와 <code>=</code>(대입)를 혼동하는 경우가 있다. <code>if (a = 5)</code> 는 <code>a</code> 에 5를 대입하고 항상 참이 되므로, <code>if (a == 5)</code> 로 써야 한다.</p>
<h3>2-5. 논리 연산자</h3>
<p>여러 조건을 조합할 때 사용한다.</p>
<p><code>&amp;&amp;</code> 는 논리 AND(둘 다 참일 때 참), <code>||</code> 는 논리 OR(하나라도 참이면 참), <code>!</code> 는 논리 NOT(참과 거짓을 반전)이다.</p>
<pre><code class="language-c">int age = 25;
int score = 80;

if (age &gt;= 20 &amp;&amp; score &gt;= 70) {
    printf("합격\n");
}

if (age &lt; 18 || score &lt; 50) {
    printf("불합격\n");
}

if (!0) {
    printf("0의 NOT은 참\n");
}
</code></pre>
<p><strong>단축 평가(Short-circuit evaluation)</strong> 라는 특성이 있다. <code>&amp;&amp;</code> 에서 왼쪽이 거짓이면 오른쪽은 평가하지 않는다. 어차피 결과가 거짓이기 때문이다. 마찬가지로 <code>||</code> 에서 왼쪽이 참이면 오른쪽은 평가하지 않는다. JavaScript의 <code>&amp;&amp;</code>, <code>||</code> 와 동일한 동작이다.</p>
<hr />
<h2>3. 분기문</h2>
<p>프로그램의 실행 흐름을 조건에 따라 나누는 것이 분기문이다.</p>
<h3>3-1. if 문</h3>
<p>조건이 참일 때만 특정 코드를 실행한다.</p>
<pre><code class="language-c">int score = 85;

if (score &gt;= 90) {
    printf("A학점\n");
}
</code></pre>
<p>조건식이 0이 아닌 값을 반환하면 참으로 판단하고 블록 안의 코드를 실행한다. 실행할 문장이 한 줄이면 중괄호를 생략할 수 있지만, 가독성과 버그 방지를 위해 항상 붙이는 것이 좋다.</p>
<h3>3-2. else 문과 else if</h3>
<p><code>if</code> 의 조건이 거짓일 때 실행할 코드를 지정한다. <code>else if</code> 로 여러 조건을 순차적으로 검사할 수도 있다.</p>
<pre><code class="language-c">int score = 75;

if (score &gt;= 90) {
    printf("A학점\n");
} else if (score &gt;= 80) {
    printf("B학점\n");
} else if (score &gt;= 70) {
    printf("C학점\n");
} else {
    printf("F학점\n");
}
</code></pre>
<p><code>else if</code> 는 위에서부터 순서대로 검사하고, 처음으로 참인 조건을 만나면 해당 블록만 실행한 뒤 나머지는 건너뛴다. 어떤 조건도 만족하지 않으면 <code>else</code> 블록이 실행된다.</p>
<h3>3-3. switch 문</h3>
<p>하나의 변수 값에 따라 여러 갈래로 분기할 때 사용한다. <code>if-else if</code> 체인이 길어질 때 더 깔끔하게 쓸 수 있다.</p>
<pre><code class="language-c">int menu = 2;

switch (menu) {
    case 1:
        printf("짜장면\n");
        break;
    case 2:
        printf("짬뽕\n");
        break;
    case 3:
        printf("볶음밥\n");
        break;
    default:
        printf("메뉴에 없습니다\n");
        break;
}
</code></pre>
<p><code>switch</code> 는 괄호 안의 값과 일치하는 <code>case</code> 로 점프한다. <code>break</code> <strong>를 빼먹으면</strong> 일치하는 <code>case</code> 이후의 모든 코드가 연달아 실행되는 <strong>fall-through</strong> 가 발생하므로 주의해야 한다.</p>
<pre><code class="language-c">// break 없으면 이렇게 됨
switch (1) {
    case 1:
        printf("1\n");   // 실행됨
    case 2:
        printf("2\n");   // 이것도 실행됨 (fall-through)
    case 3:
        printf("3\n");   // 이것도 실행됨
}
</code></pre>
<p><code>default</code> 는 어떤 <code>case</code> 에도 해당하지 않을 때 실행되며, <code>if-else</code> 에서의 <code>else</code> 와 같은 역할이다.</p>
<p><code>switch</code> 는 정수형과 문자형(<code>char</code>)에만 사용할 수 있다. 실수형이나 문자열은 사용할 수 없으므로 그런 경우에는 <code>if-else if</code> 를 써야 한다.</p>
<hr />
<h2>4. 반복문</h2>
<p>같은 코드를 여러 번 실행해야 할 때 사용한다.</p>
<h3>4-1. while 문</h3>
<p>조건이 참인 동안 계속 반복한다.</p>
<pre><code class="language-c">int i = 0;
while (i &lt; 5) {
    printf("%d\n", i);
    i++;
}
// 출력: 0, 1, 2, 3, 4
</code></pre>
<p>동작 순서는 <strong>조건 검사 → 실행 → 조건 검사 → 실행 → ...</strong> 이다. 조건이 처음부터 거짓이면 한 번도 실행되지 않는다.</p>
<p>무한 루프를 만들 때는 <code>while (1)</code> 을 사용한다. 서버 프로그램이나 게임 루프처럼 프로그램이 계속 돌아야 하는 경우에 쓰인다.</p>
<pre><code class="language-c">while (1) {
    // 무한 반복
    // break로 탈출
}
</code></pre>
<h3>4-2. do~while 문</h3>
<p><code>while</code> 과 거의 같지만, <strong>최소 한 번은 무조건 실행된다</strong> 는 차이가 있다. 조건 검사가 뒤에 오기 때문이다.</p>
<pre><code class="language-c">int num;
do {
    printf("양수를 입력하세요: ");
    scanf("%d", &amp;num);
} while (num &lt;= 0);
</code></pre>
<p>동작 순서는 <strong>실행 → 조건 검사 → 실행 → 조건 검사 → ...</strong> 이다. 위 예시처럼 일단 입력을 받고 나서 유효한지 검사해야 하는 경우에 유용하다. 조건이 처음부터 거짓이라도 블록 안의 코드가 한 번은 실행된다는 점이 <code>while</code> 과의 핵심 차이다.</p>
<p><code>do~while</code> 문 끝에는 반드시 세미콜론(<code>;</code>)을 붙여야 한다.</p>
<h3>4-3. for 문</h3>
<p>반복 횟수가 명확할 때 가장 많이 사용하는 반복문이다. 초기식, 조건식, 증감식을 한 줄에 모아서 쓴다.</p>
<pre><code class="language-c">for (int i = 0; i &lt; 5; i++) {
    printf("%d\n", i);
}
// 출력: 0, 1, 2, 3, 4
</code></pre>
<p>실행 순서는 <strong>초기식 → 조건 검사 → 실행 → 증감식 → 조건 검사 → 실행 → 증감식 → ...</strong> 이다. 초기식은 맨 처음 한 번만 실행된다.</p>
<p>중첩 <code>for</code> 문으로 2차원 반복도 가능하다.</p>
<pre><code class="language-c">for (int i = 0; i &lt; 3; i++) {
    for (int j = 0; j &lt; 3; j++) {
        printf("(%d, %d) ", i, j);
    }
    printf("\n");
}
</code></pre>
<p><code>for</code>문의 세 가지 구성요소(초기식, 조건식, 증감식)는 전부 생략할 수 있다. <code>for (;;)</code> 는 <code>while (1)</code> 과 같은 무한 루프다.</p>
<h3>4-4. break</h3>
<p>반복문을 즉시 탈출한다. 가장 가까운 반복문 하나만 빠져나온다.</p>
<pre><code class="language-c">for (int i = 0; i &lt; 10; i++) {
    if (i == 5) {
        break;
    }
    printf("%d\n", i);
}
// 출력: 0, 1, 2, 3, 4 (5에서 탈출)
</code></pre>
<p>중첩 반복문에서 바깥쪽 루프까지 한 번에 빠져나오고 싶다면, <code>break</code> 만으로는 안 된다. 플래그 변수를 사용하거나, 해당 로직을 함수로 분리해서 <code>return</code> 으로 탈출하는 방법이 있다.</p>
<pre><code class="language-c">int found = 0;
for (int i = 0; i &lt; 10 &amp;&amp; !found; i++) {
    for (int j = 0; j &lt; 10; j++) {
        if (arr[i][j] == target) {
            found = 1;
            break;  // 안쪽 루프 탈출
        }
    }
    // found가 1이면 바깥 루프 조건도 거짓 → 탈출
}
</code></pre>
<h3>4-5. continue</h3>
<p>현재 반복을 건너뛰고 다음 반복으로 넘어간다. <code>break</code> 와 달리 반복문 자체를 탈출하지는 않는다.</p>
<pre><code class="language-c">for (int i = 0; i &lt; 10; i++) {
    if (i % 2 == 0) {
        continue;  // 짝수면 건너뜀
    }
    printf("%d\n", i);
}
// 출력: 1, 3, 5, 7, 9
</code></pre>
<p><code>for</code> 문에서 <code>continue</code> 를 만나면 증감식으로 점프한다. <code>while</code> 문에서는 조건식으로 점프한다. <code>while</code> 에서 <code>continue</code> 를 쓸 때 증감식을 <code>continue</code> 아래에 둔 경우, 증감이 실행되지 않아 무한 루프에 빠질 수 있으므로 주의해야 한다.</p>
<pre><code class="language-c">// 위험한 패턴
int i = 0;
while (i &lt; 10) {
    if (i == 5) {
        continue;  // i++에 도달 못 함 → 무한 루프
    }
    printf("%d\n", i);
    i++;
}

// 안전한 패턴
int i = 0;
while (i &lt; 10) {
    i++;
    if (i == 5) {
        continue;  // 문제없음
    }
    printf("%d\n", i);
}
</code></pre>
<hr />
<h2>마무리</h2>
<p>입력함수로 데이터를 받고, 연산자로 데이터를 처리하고, 분기문으로 흐름을 나누고, 반복문으로 반복 작업을 수행한다. 이 네 가지가 C언어 프로그래밍의 기본 뼈대다. 특히 <code>&amp;</code> 의 의미, 전위/후위 증감의 차이, <code>break</code> 와 <code>continue</code> 의 동작 방식은 헷갈리기 쉬우니 직접 코드를 돌려보면서 익히는 것이 좋다.</p>
]]></content:encoded></item><item><title><![CDATA[C언어 (1)]]></title><description><![CDATA[1. C언어의 기본 구조
C언어는 절차지향 프로그래밍 언어로, 위에서 아래로 순차적으로 코드를 실행한다. 모든 C 프로그램은 main() 함수에서 시작된다.
#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

#include <stdio.h>는 표준 입출력 ]]></description><link>https://blog.chamdom.dev/c-1</link><guid isPermaLink="true">https://blog.chamdom.dev/c-1</guid><dc:creator><![CDATA[Chamdom]]></dc:creator><pubDate>Sun, 29 Mar 2026 19:41:45 GMT</pubDate><content:encoded><![CDATA[<h2>1. C언어의 기본 구조</h2>
<p>C언어는 <strong>절차지향 프로그래밍 언어</strong>로, 위에서 아래로 순차적으로 코드를 실행한다. 모든 C 프로그램은 <code>main()</code> 함수에서 시작된다.</p>
<pre><code class="language-c">#include &lt;stdio.h&gt;

int main(void) {
    printf("Hello, World!\n");
    return 0;
}
</code></pre>
<p><code>#include &lt;stdio.h&gt;</code>는 표준 입출력 라이브러리를 포함시키는 전처리 지시문이다. <code>printf</code>같은 함수를 사용하려면 반드시 필요하다.</p>
<h3>1-1. 컴파일</h3>
<p>C언어는 <strong>컴파일 언어</strong>다. JavaScript처럼 한 줄씩 해석하며 실행하는 인터프리터 방식이 아니라, 소스 코드 전체를 기계어로 번역한 뒤 실행한다.</p>
<p>컴파일 과정은 크게 4단계를 거친다.</p>
<p>먼저 <strong>전처리(Preprocessing)</strong> 단계에서 <code>#include</code>, <code>#define</code> 등의 전처리 지시문을 처리한다. 그 다음 <strong>컴파일(Compilation)</strong> 단계에서 전처리된 소스 코드를 어셈블리어로 변환한다. 이어서 <strong>어셈블(Assembly)</strong> 단계에서 어셈블리어를 기계어(오브젝트 파일, <code>.o</code>)로 변환하고, 마지막으로 <strong>링킹(Linking)</strong> 단계에서 여러 오브젝트 파일과 라이브러리를 합쳐 하나의 실행 파일을 만든다.</p>
<pre><code class="language-plaintext">소스코드(.c) → 전처리 → 컴파일 → 어셈블(.o) → 링킹 → 실행파일(.exe / a.out)
</code></pre>
<h3>1-2. 실행</h3>
<p>컴파일이 완료되면 생성된 실행 파일을 OS 위에서 직접 실행한다. 컴파일 시점에 이미 기계어로 번역되어 있기 때문에, 인터프리터 언어보다 <strong>실행 속도가 빠르다.</strong></p>
<pre><code class="language-bash">gcc main.c -o main   # 컴파일
./main                # 실행
</code></pre>
<hr />
<h2>2. 변수</h2>
<h3>2-1. 변수를 사용하는 이유</h3>
<p>프로그램은 결국 <strong>데이터를 처리하는 것</strong>이다. 데이터를 저장하고, 읽고, 수정하려면 메모리 공간이 필요한데, 그 메모리 공간에 이름을 붙인 것이 <strong>변수</strong>다.</p>
<p>변수가 없다면 메모리 주소를 직접 다뤄야 하는데, <code>0x7ffeefbff4ac</code> 같은 주소를 사람이 기억하며 코드를 짜는 것은 비현실적이다. 변수는 이 문제를 해결해준다.</p>
<h3>2-2. 자료형에 따라 메모리 크기가 다르다</h3>
<p>모든 데이터가 같은 크기의 메모리를 쓰는 건 아니다. 저장할 값의 종류와 범위에 따라 필요한 공간이 달라진다.</p>
<p><code>char</code>는 1바이트로 -128에서 127까지, <code>short</code>는 2바이트로 약 -3만에서 3만까지, <code>int</code>는 4바이트로 약 -21억에서 21억까지 저장할 수 있다. 실수형인 <code>float</code>는 4바이트로 소수점 약 6<del>7자리 정밀도를, <code>double</code>은 8바이트로 약 15</del>16자리 정밀도를 제공한다.</p>
<p>작은 숫자를 저장할 때 <code>double</code>(8바이트)을 쓰면 메모리 낭비다. 반대로 큰 숫자를 <code>char</code>(1바이트)에 넣으면 <strong>오버플로우</strong>가 발생한다. 적절한 자료형 선택이 중요한 이유다.</p>
<h3>2-3. 메모리에 할당된 변수를 어떻게 참조하는가?</h3>
<p>변수를 선언하면 컴파일러가 해당 자료형 크기만큼 메모리를 확보한다. 이후 변수 이름을 사용하면, 컴파일러가 그 이름을 실제 메모리 주소로 변환해준다.</p>
<pre><code class="language-c">int x = 10;
printf("값: %d\n", x);       // 변수 이름으로 값 접근
printf("주소: %p\n", &amp;x);    // &amp; 연산자로 메모리 주소 확인
</code></pre>
<p>즉 프로그래머는 이름으로 접근하고, 컴파일러가 주소로 바꿔주는 구조다.</p>
<h3>2-4. 메모리 주소값은 OS가 할당한다</h3>
<p>변수를 선언하면 실제로 어떤 메모리 주소에 배치될지는 **운영체제(OS)**가 결정한다. 프로그래머가 "이 변수를 주소 0x1000에 넣어라"라고 지정하는 것이 아니다.</p>
<p>OS는 프로그램이 실행될 때 가상 메모리 공간을 할당하고, 그 안에서 변수들의 위치를 정한다. 같은 프로그램을 두 번 실행하면 변수의 주소가 달라질 수 있는 이유가 이것이다.</p>
<h3>2-5. 메모리에 변수 이름 부여</h3>
<p>결국 변수 선언이란 다음 세 가지를 한 번에 하는 것이다.</p>
<p><strong>메모리 공간 확보</strong> — 자료형 크기만큼 공간을 잡는다. <strong>이름 부여</strong> — 그 공간에 사람이 읽을 수 있는 이름을 붙인다. <strong>타입 지정</strong> — 그 공간에 어떤 종류의 데이터가 들어갈지 정한다.</p>
<pre><code class="language-c">int age = 25;
</code></pre>
<p>이 한 줄로 4바이트 메모리가 확보되고, <code>age</code>라는 이름이 붙고, 정수형 데이터가 저장된다.</p>
<h3>2-6. 변수의 종류</h3>
<h4>기본형 (Primitive Type)</h4>
<p>언어가 기본으로 제공하는 자료형이다. 정수형으로는 <code>char</code>, <code>short</code>, <code>int</code>, <code>long</code>, <code>long long</code>이 있고, 실수형으로는 <code>float</code>, <code>double</code>, <code>long double</code>이 있다. 그리고 반환값이 없음을 나타내는 <code>void</code>도 있다.</p>
<h4>유도형 (Derived Type)</h4>
<p>기본형을 조합하거나 확장해서 만든 자료형이다.</p>
<p><strong>배열(</strong><code>int arr[10]</code>**)<strong>은 같은 타입의 데이터를 연속된 메모리에 저장한다. <strong>포인터(</strong><code>int *p</code></strong>)<strong>는 다른 변수의 메모리 주소를 저장한다. <strong>구조체(</strong><code>struct</code></strong>)<strong>는 서로 다른 타입의 데이터를 하나로 묶고, <strong>공용체(</strong><code>union</code></strong>)<strong>는 같은 메모리 공간을 여러 타입이 공유한다. <strong>열거형(</strong><code>enum</code></strong>)**은 관련된 상수들에 이름을 부여한다.</p>
<h3>2-7. 객체지향과의 관계</h3>
<p>C언어 자체는 <strong>절차지향 언어</strong>이지만, 구조체와 함수 포인터를 조합하면 객체지향적인 설계를 흉내낼 수 있다. 하지만 본격적인 객체지향(클래스, 상속, 다형성, 캡슐화)은 C++에서 지원한다.</p>
<pre><code class="language-c">typedef struct {
    char name[50];
    int age;
    void (*greet)(const char*);
} Person;

void sayHello(const char* name) {
    printf("안녕하세요, %s입니다.\n", name);
}
</code></pre>
<p>C++로 넘어가면 이런 패턴이 <code>class</code>로 깔끔하게 정리된다. C의 구조체를 잘 이해하면 C++의 클래스를 이해하는 데 큰 도움이 된다.</p>
<h3>2-8. 변수 선언 방법</h3>
<pre><code class="language-c">// 선언과 동시에 초기화
int x = 10;
float pi = 3.14f;
char ch = 'A';

// 선언만 (초기화 안 하면 쓰레기값이 들어있음)
int y;

// 여러 변수 동시 선언
int a, b, c;
int d = 1, e = 2;

// 상수 선언 (값 변경 불가)
const int MAX = 100;
</code></pre>
<p>변수 이름은 영문자, 숫자, 언더스코어(<code>_</code>)만 사용할 수 있고, 숫자로 시작할 수 없다. <code>int</code>, <code>return</code> 같은 예약어도 사용 불가하며, 대소문자를 구분하므로 <code>age</code>와 <code>Age</code>는 서로 다른 변수다.</p>
<hr />
<h2>3. 자료형과 서식 문자</h2>
<h3>3-1. 서식 문자의 종류</h3>
<p><code>printf</code>와 <code>scanf</code>에서 데이터를 입출력할 때 자료형에 맞는 서식 문자를 사용해야 한다.</p>
<p>정수를 출력할 때는 <code>%d</code>를 쓴다. <code>long</code>이면 <code>%ld</code>, <code>long long</code>이면 <code>%lld</code>를 사용한다. 부호 없는 정수는 <code>%u</code>다.</p>
<p>실수는 <code>printf</code>에서 <code>%f</code>를 쓰고, <code>scanf</code>에서 <code>double</code>을 입력받을 때는 <code>%lf</code>를 써야 한다. 지수 표기법으로 출력하고 싶으면 <code>%e</code>를 사용한다.</p>
<p>문자 1개는 <code>%c</code>, 문자열은 <code>%s</code>다. 포인터 주소를 출력할 때는 <code>%p</code>, 16진수는 <code>%x</code>, 8진수는 <code>%o</code>를 사용한다. <code>%</code> 문자 자체를 출력하려면 <code>%%</code>로 쓴다.</p>
<pre><code class="language-c">int num = 42;
float pi = 3.14159f;
char grade = 'A';

printf("정수: %d\n", num);
printf("실수: %.2f\n", pi);     // 소수점 2자리까지
printf("문자: %c\n", grade);
printf("주소: %p\n", &amp;num);
</code></pre>
<h3>3-2. 자료형 상세</h3>
<h4>정수형</h4>
<p><code>char</code>는 1바이트로, 문자를 저장하거나 작은 정수를 담을 때 사용한다. <code>unsigned char</code>로 선언하면 0부터 255까지 양수만 저장할 수 있다. <code>short</code>는 2바이트, <code>int</code>는 4바이트로 약 ±21억 범위를 가지며 가장 많이 사용하는 정수형이다. 매우 큰 정수가 필요하면 8바이트인 <code>long long</code>을 사용한다.</p>
<p><code>signed</code>와 <code>unsigned</code> 키워드로 부호 유무를 지정할 수 있다. <code>unsigned int</code>는 음수를 포기하는 대신 양수 범위가 두 배로 늘어난다(0 ~ 약 42억).</p>
<h4>실수형</h4>
<p><code>float</code>는 4바이트로 소수점 약 6<del>7자리 정밀도를 제공하고, <code>double</code>은 8바이트로 약 15</del>16자리 정밀도를 제공한다. 일반적인 실수 연산에는 <code>double</code>을 사용하고, 메모리 절약이 필요할 때만 <code>float</code>를 쓴다.</p>
<p>실수는 컴퓨터에서 IEEE 754 표준에 따라 <strong>부동소수점</strong> 방식으로 저장된다. 이 때문에 <code>0.1 + 0.2 == 0.3</code>이 <code>false</code>가 될 수 있다. JavaScript에서도 동일한 현상이 발생하는데, 이유가 같다.</p>
<pre><code class="language-c">printf("%.20f\n", 0.1 + 0.2);  // 0.30000000000000004441...
</code></pre>
<h4>문자형</h4>
<p><code>char</code>는 사실 1바이트 정수형이다. 문자를 저장하는 게 아니라 **문자에 대응하는 ASCII 코드 값(정수)**을 저장한다.</p>
<pre><code class="language-c">char ch = 'A';       // 실제로 65가 저장됨
printf("%c\n", ch);  // A
printf("%d\n", ch);  // 65
</code></pre>
<h3>3-3. 데이터베이스와의 연관성</h3>
<p>C언어의 자료형 개념은 데이터베이스 설계와 직접적으로 연결된다. DB 테이블의 컬럼을 정의할 때도 적절한 데이터 타입을 선택해야 한다.</p>
<p>C의 <code>int</code>는 SQL의 <code>INT</code>나 <code>INTEGER</code>에, <code>float</code>이나 <code>double</code>은 <code>FLOAT</code>, <code>DOUBLE</code>, <code>DECIMAL</code>에 대응한다. <code>char</code>는 고정 길이 문자열인 <code>CHAR(n)</code>에, <code>char[]</code> 배열은 가변 길이 문자열인 <code>VARCHAR(n)</code>에 해당한다.</p>
<p>프로그래밍에서든 데이터베이스에서든 핵심은 같다 — <strong>저장할 데이터의 성격에 맞는 타입을 고르는 것</strong>이 메모리 효율성과 프로그램 안정성을 결정한다.</p>
<hr />
<h2>마무리</h2>
<p>C언어의 핵심은 <strong>메모리를 직접 다루는 것</strong>이다. 변수 선언 하나에도 메모리 할당, 주소 지정, 타입 결정이 함께 일어난다. 이 기본기를 확실히 잡아두면 포인터, 동적 메모리 할당, 그리고 C++의 클래스까지 자연스럽게 이해할 수 있다.</p>
]]></content:encoded></item></channel></rss>