C++

[강의] 12월 23일 수업정리

k-codestudy 2024. 12. 23. 17:44

오늘은 탬플릿, 클래스 탬플릿 특수화, 간단한 STL에 대한 수업을 들었다.

 

1. 탬플릿 

1.1 탬플릿에서 클래스 설계 시 함수 인자 처리

  •  C++에서 템플릿 클래스를 설계할 때, 함수 인수는 래퍼런스(&)로 전달하는 것이 일반적이다. 이는 call by value를 막기 위함으로 기본적으로 C++에서 클래스를 call by value를 한다는 것 자체는 문제인 것이다.

1.2 참조로 구성해도 복사가 발생하는 이유

  • 함수 인수를 참조로 전달하더라고 내부적으로 복사가 필요할 때가 있다.
    예를 들어 템플릿을 사용하면서 클래스의 인스턴스를 값으로 복사해야 하는 경우가 발생한다.
    이러한 경우에서 복사 생성자를 막아놓은 클래스는 컴파일 시점에 "삭제된 복사 생성자를 호출하려 한다"라는 오류를 발생시키게 된다. 그렇다면 클래스를 call by value 하는 게 맞는 걸까 하는 문제가 있다.

1.3 템플릿 설계의 현실적인 접근법

  • 템플릿 설계 시, 기본 자료형을 제외하고는 클래스 객체를 직접 저장하지 않고, 포인터로 저장하는 것이 일반적이다.

    1. C++의 기본 동작은 값 전달이다.
    - 템플릿을 사용할 경우, 객체가 복사되거나 대입 연산이 발생하게 되는데 이러한 동작을 깊이 이해하지 못하면 불필요한 복사가 빈번하게 발생할 수 있다.
    2. 포인터 사용이 일반적이다.
    - 클래스 객체를 포인터로 저장하면 직접 복사를 피할 수 있다. 그러나 포인터를 사용하면 메모리 관리 문제를 해결해야 한다. 동적 할당과 해제는 반드시 명시적으로 처리해야 한다. 

1.4 템플릿과 코드 관리 문제

  • 템플릿은 일반 클래스와 달리 해더 파일(. h), 소스 파일(. cpp)로 분리할 수 없다. 템플릿 클래스나 함수의 정의와 구현은 반드시 같은 파일에 있어야 구현이 가능하다.
  • 이유는 템플릿은 컴파일 시점에 인스턴스화되기 때문에, 구현부가 헤더 파일에 포함되어 있지 않으면 링크 오류가 발생하기 때문이다.
  • 그렇기에 헤더 파일에 템플릿의 선언과 정의를 모두 헤더 파일에 포함을 시키는 것이다. 이러한 이유 때문에 코드 관리가 힘들어진다.

 

2. 클래스 탬플릿 특수화 

 

클래스 템플릿 특수화는 특정 자료형에 대해 템플릿 클래스의 동작을 다르게 정의할 수 있는 방법이다.

이를 통해 특정 타입에 최적화된 구현을 제공 가능하다.

#pragma once

template<class T>// 일반 
class C_DATA
{
private:
    T m_tData;
public:
    C_DATA() = default;
    void setData(T tData);
    T getData();
};


template<> // 특수화 탬플릿 
class C_DATA<char> 
{
private:
    char m_tData;
public:
    C_DATA() = default;
    void setData(char tData);
    char getData();
};

 

  • 템플릿 구현 위치 문제:
    일반 템플릿 클래스는 헤더 파일에 선언과 구현을 함께 두는 것이 일반적이다.
    하지만 템플릿 특수화를 별도의 소스 파일(. cpp)에 구현할 경우, 링커에서 중복 정의 오류가 발생할 수 있다.
    이는 템플릿 특수화가 여러 소스 파일에서 참조될 때, 각 참조마다 정의가 시도되기 때문이다.

  • 정의 중복 오류:
    템플릿 특수화를 헤더 파일과 소스 파일 모두에 포함시키면 링커가 중복 정의로 인한 오류를 발생시킨다.

그렇기에 오류를 발생시키지 않게 하기 위해 해더 파일에 일반 템플릿 클래스의 선언과 구현, 탬플릿 특수화의 선언만 포함하고 소스 파일에 템플릿 특수화의 구현을 포함시키고 해당 특수화에 대한 명시적 인스턴스화를 추가하면 된다.

#pragma once

// 일반 템플릿 클래스
template <class T>
class C_DATA
{
private:
    T m_tData;
public:
    C_DATA() = default;
    void setData(const T& tData)
    {
    	m_tData = tData;
    }
    T getData() const
    {
    	return m_tData;
    }
};

// char 타입에 대한 템플릿 클래스 특수화 선언
template <>
class C_DATA<char>
{
private:
    char m_tData;
public:
    C_DATA() = default;
    void setData(char tData);
    char getData() const;
};

 

 

#include "data.h"

// char 타입에 대한 템플릿 클래스 특수화 구현
void C_DATA<char>::setData(char tData)
{
    m_tData = tData;
}

char C_DATA<char>::getData() const
{
    return m_tData;
}

// char 타입에 대한 명시적 템플릿 인스턴스화
template class C_DATA<char>;

 

요약해 보자면 

 

  • 일반 템플릿 클래스는 헤더 파일에 선언과 구현을 함께 포함시킨다
  • 템플릿 특수화는 헤더 파일에 선언만 포함시키고, 소스 파일에 구현을 작성한다.
  • 명시적 인스턴스화를 사용하여 템플릿 특수화의 구현을 링커가 인식하도록 한다.
  • 이를 통해 템플릿 특수화 시 발생할 수 있는 중복 정의 오류를 방지할 수 있다.

 

 

3. STL ( Standard Template Library)

STL은 다양한 자료구조, 함수, 알고리즘 등을 쉽게 사용할 수 있도록 정형화한 C++의 표준 라이브러리이다.

 

3.1 기본 문법 및 특징

  • 템플릿 기반
    - STL은 템플릿을 기반으로 하여, 자료형에 의존하지 않고 다양한 자료구조를 사용할 수 있다.
  • 자료구조:
    - std::list: 열거형 데이터를 다루는 자료구조이다.
    - C++의 std::list: 양방향 링크드 리스트이며, C#의 List: 배열 리스트입니다.

3.2 자료구조와 주요 기능

 

자료구조는 일반적으로 세 가지 주요 기능 존재하는데 검색, 삽입, 삭제이다.

  • 삽입과 삭제 전에 검색이 선행되는 경우
    - 검색의 효율이 삽입 및 삭제의 효율을 좌우한다.
    - 예: std::list에서 특정 위치에 값을 삽입하려면 해당 위치를 찾는 과정(검색)이 필요합니다.

3.3 STL의 간접 접근 방식

  • 포인터를 간접적으로 흉내:
    - STL은 직접적인 포인터 대신 **이터레이터(iterator)**라는 간접 접근자를 제공한다.
    - 이터레이터는 내부적으로 포인터와 유사한 방식으로 동작하며, 컨테이너 요소에 대한 접근 및 순회를 지원한다.
#include <iostream>
#include <list>


int main()
{
    std::list<int> listData{};

    listData.push_back(1);
    listData.push_back(2);
    listData.push_back(3);
    listData.push_back(4);

    std::list<int>::iterator iter = listData.begin();
    while (iter != listData.end())
    {
        printf("%d\n", *iter);
        iter++;
    }
}

 

 

3.4 전역 함수에서의 std::list 사용

 

전역 함수를 사용할 때, 기본 자료형이 아닌 STL 컨테이너를 매개변수로 넘길 경우, 반드시 포인터나 레퍼런스를 사용해야 한다. 그렇지 않으면 call by value로 인해 객체가 복사가 되기 때문이다.

#include <iostream>
#include <list>

void print(std::list<int>& list);  // 기본 자료형 말고는 밸류는안됨 네버
//void print(std::list<int>* pList);

int main()
{
    std::list<int> listData{};

    listData.push_back(1);
    listData.push_back(2);
    listData.push_back(3);
    listData.push_back(4);
    listData.push_back(5);
    listData.push_back(6);

    print(listData);
}

void print(std::list<int> &list)
{
    std::list<int>::iterator iter = list.begin();
    while (iter != list.end())
    {
        printf("%d\n", *iter);
        iter++;
    }
    printf("\n");
}


//void print(std::list<int>* pList)
//{
//
//    std::list<int>::iterator iter = pList->begin();
//    while (iter != pList->end())
//    {
//        printf("%d\n", *iter);
//        iter++;
//    }
//}

중요한 포인트

  1. 전역 함수에서 STL 컨테이너를 넘길 때:
    - 값 전달(call by value)로 넘기면 복사가 발생하므로 피해야 한다.
    - 대신, 레퍼런스(&) 또는 포인터(*)를 사용해 컨테이너를 함수에 전달해야 한다

  2. 레퍼런스와 포인터의 차이:
    - 레퍼런스를 사용하면 함수 호출이 간결해지고, 컨테이너를 직접 다룰 수 있다.
    - 포인터를 사용하면 함수 호출 시 명시적으로 주소를 넘겨야 한다.

  3. 이터레이터의 역할:
    - 이터레이터는 STL 컨테이너의 요소를 순회할 때 사용되는 간접 접근자이다.
    - 내부적으로 포인터와 비슷한 방식으로 동작하지만, STL의 다양한 컨테이너에서 일관된 방식으로 사용될 수 있도록 설계되어 있다.