C 개발자를 위한 C++ 입문

By | 2025년 1월 21일
Table of Contents

C 개발자를 위한 C++ 입문

C 언어 개발자를 위한 C++ 강좌의 목차를 작성해드리겠습니다. C 개발자의 관점에서 자연스럽게 C++을 배울 수 있도록 구성해보겠습니다.

Part 1: C++의 기초와 C와의 차이점

1. 입출력과 네임스페이스

1.1 C++의 입출력 시스템

// C 스타일
#include <stdio.h>
int main() {
    int num = 42;
    printf("숫자는 %d입니다.\n", num);

    char str[100];
    scanf("%s", str);
    return 0;
}

// C++ 스타일
#include <iostream>
using namespace std;
int main() {
    int num = 42;
    cout << "숫자는 " << num << "입니다." << endl;

    string str;
    cin >> str;
    return 0;
}

C++의 입출력은 스트림(stream) 개념을 기반으로 합니다:

  • cout: 표준 출력 스트림 (Console OUTput)
  • cin: 표준 입력 스트림 (Console INput)
  • cerr: 표준 에러 스트림
  • endl: 개행문자 + 버퍼 플러시

1.2 네임스페이스의 이해

네임스페이스는 이름 충돌을 방지하기 위한 논리적 영역입니다.

#include <iostream>

// 전체 네임스페이스 사용
using namespace std;

// 특정 요소만 사용
using std::cout;
using std::endl;

// 직접 네임스페이스 지정
std::cout << "Hello" << std::endl;

// 사용자 정의 네임스페이스
namespace MyApp {
    void print() {
        std::cout << "MyApp의 print 함수" << std::endl;
    }
}

int main() {
    MyApp::print();
    return 0;
}

2. 향상된 변수와 타입 시스템

2.1 bool 타입

// C 스타일
int isValid = 1;  // 0은 거짓, 0이 아닌 값은 참

// C++ 스타일
bool isValid = true;  // true 또는 false

2.2 참조자(Reference)

포인터보다 안전하고 직관적인 방법을 제공합니다.

// 포인터 사용 (C 스타일)
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 참조자 사용 (C++ 스타일)
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

int main() {
    int x = 5, y = 10;

    // 포인터 호출
    swap(&x, &y);

    // 참조자 호출
    swap(x, y);
}

2.3 auto 키워드

타입 추론을 통해 코드를 더 유연하게 만들 수 있습니다.

// 명시적 타입 선언
std::vector<int>::iterator it = vec.begin();

// auto 사용
auto it = vec.begin();

// 복잡한 타입에서의 활용
auto result = complicated_function();

2.4 const의 확장된 의미

// 포인터와 const
const int* p1;        // 포인터가 가리키는 값을 수정할 수 없음
int* const p2 = &x;   // 포인터 자체를 수정할 수 없음
const int* const p3;  // 둘 다 수정할 수 없음

// 참조자와 const
void printValue(const int& value) {  // 읽기 전용 참조
    // value = 42;  // 컴파일 에러
    std::cout << value << std::endl;
}

// const 멤버 함수
class MyClass {
    void printData() const {  // 객체의 상태를 수정하지 않음을 보장
        // data = 42;  // 컴파일 에러
    }
};

Part 2: C++의 핵심 기능

1. 함수의 진화

1.1 함수 오버로딩

C++에서는 같은 이름의 함수를 매개변수를 다르게 하여 여러 개 정의할 수 있습니다.

// C 스타일
int add_int(int a, int b) {
    return a + b;
}
double add_double(double a, double b) {
    return a + b;
}

// C++ 스타일
int add(int a, int b) {
    return a + b;
}
double add(double a, double b) {
    return a + b;
}
std::string add(const std::string& a, const std::string& b) {
    return a + b;
}

1.2 디폴트 매개변수

함수 선언 시 매개변수의 기본값을 지정할 수 있습니다.

void printMessage(const std::string& msg, int count = 1, bool newLine = true) {
    for(int i = 0; i < count; i++) {
        std::cout << msg;
        if(newLine) std::cout << std::endl;
    }
}

int main() {
    printMessage("Hello");           // count=1, newLine=true
    printMessage("Hello", 3);        // newLine=true
    printMessage("Hello", 2, false); // 모든 매개변수 지정
}

1.3 인라인 함수

작은 함수를 효율적으로 구현할 수 있습니다.

// 매크로 사용 (C 스타일)
#define MAX(a,b) ((a) > (b) ? (a) : (b))

// 인라인 함수 (C++ 스타일)
inline int max(int a, int b) {
    return a > b ? a : b;
}

1.4 함수 템플릿 기초

타입에 독립적인 함수를 작성할 수 있습니다.

template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

int main() {
    std::cout << max(10, 20) << std::endl;      // int
    std::cout << max(10.5, 20.7) << std::endl;  // double
    std::cout << max("abc", "def") << std::endl; // const char*
}

2. 객체지향 프로그래밍 기초

2.1 구조체에서 클래스로

// C 스타일 구조체
struct Person_C {
    char name[50];
    int age;
};
void Person_C_init(struct Person_C* p, const char* name, int age) {
    strcpy(p->name, name);
    p->age = age;
}

// C++ 스타일 클래스
class Person {
private:  // 접근 제어
    std::string name;
    int age;

public:
    // 생성자
    Person(const std::string& n, int a) : name(n), age(a) {}

    // 메서드
    void printInfo() const {
        std::cout << "Name: " << name << ", Age: " << age << std::endl;
    }
};

2.2 접근 제어와 캡슐화

class BankAccount {
private:  // 외부에서 접근 불가
    double balance;
    std::string accountNumber;

protected:  // 상속받은 클래스에서만 접근 가능
    void updateBalance(double amount) {
        balance += amount;
    }

public:  // 외부에서 접근 가능
    BankAccount(const std::string& accNum) : balance(0), accountNumber(accNum) {}

    void deposit(double amount) {
        if(amount > 0) {
            updateBalance(amount);
        }
    }
};

2.3 생성자와 소멸자

class DynamicArray {
private:
    int* data;
    size_t size;

public:
    // 생성자
    DynamicArray(size_t sz) : size(sz) {
        data = new int[size];
    }

    // 복사 생성자
    DynamicArray(const DynamicArray& other) : size(other.size) {
        data = new int[size];
        memcpy(data, other.data, size * sizeof(int));
    }

    // 소멸자
    ~DynamicArray() {
        delete[] data;
    }
};

2.4 this 포인터

class Counter {
private:
    int count;

public:
    Counter(int count) {
        this->count = count;  // 매개변수와 멤버변수 구분
    }

    Counter& increment() {
        count++;
        return *this;  // 자기 자신을 반환
    }

    Counter& add(int value) {
        count += value;
        return *this;
    }
};

int main() {
    Counter c(0);
    c.increment().add(5).increment();  // 메서드 체이닝
}

2.5 멤버 초기화 리스트

멤버 변수를 초기화하는 효율적인 방법을 제공합니다.

class Point {
private:
    const int x;
    const int y;
    std::string label;

public:
    // 멤버 초기화 리스트 사용
    Point(int xVal, int yVal, const std::string& l) 
        : x(xVal), y(yVal), label(l) {
        // 본문이 비어있어도 됨
    }
};

2.6 friend 클래스와 함수

friend 키워드를 사용하면 외부 함수나 클래스가 현재 클래스의 private 및 protected 멤버에 접근할 수 있습니다.

class Rectangle {
private:
    int width;
    int height;

    friend void printArea(const Rectangle& rect);
    friend class RectangleManager;

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

void printArea(const Rectangle& rect) {
    // private 멤버인 width와 height에 직접 접근
    std::cout << "Area: " << rect.width * rect.height << std::endl;
}

class RectangleManager {
public:
    void adjustSize(Rectangle& rect) {
        // private 멤버에 직접 접근
        if (rect.width < 0) rect.width = 0;
        if (rect.height < 0) rect.height = 0;
    }

    void printDimensions(const Rectangle& rect) {
        std::cout << "Width: " << rect.width 
                 << ", Height: " << rect.height << std::endl;
    }
};
// 연산자 오버로딩에서의 활용 예
class Complex {
private:
    double real;
    double imag;

public:
    Complex(double r, double i) : real(r), imag(i) {}

    // 연산자 오버로딩을 위한 friend 함수
    friend Complex operator+(const Complex& a, const Complex& b) {
        return Complex(a.real + b.real, a.imag + b.imag);
    }

    // 출력 연산자 오버로딩
    friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
        os << c.real << " + " << c.imag << "i";
        return os;
    }
};

int main() {
    Complex a(1.0, 2.0);
    Complex b(3.0, 4.0);
    Complex c = a + b;  // operator+ friend 함수 호출
    std::cout << c;     // operator<< friend 함수 호출
}

주의사항:

  • friend는 캡슐화를 약화시킬 수 있으므로 필요한 경우에만 사용해야 합니다.
  • 과도한 friend 사용은 코드의 유지보수성을 저하시킬 수 있습니다.
  • friend 대신 public 인터페이스를 통한 접근이 가능한지 먼저 고려해야 합니다.

3. C++의 추가 핵심 기능

3.1 연산자 오버로딩

클래스에 대해 연산자의 동작을 재정의할 수 있습니다.

class Complex {
private:
    double real, imag;

public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    // 멤버 함수로 연산자 오버로딩
    Complex operator+(const Complex& other) const {
        return Complex(real + other.real, imag + other.imag);
    }

    // 복합 대입 연산자
    Complex& operator+=(const Complex& other) {
        real += other.real;
        imag += other.imag;
        return *this;
    }

    // friend 함수로 연산자 오버로딩
    friend std::ostream& operator<<(std::ostream& os, const Complex& c) {
        os << c.real << " + " << c.imag << "i";
        return os;
    }
};

int main() {
    Complex a(1, 2), b(3, 4);
    Complex c = a + b;     // operator+ 호출
    a += b;               // operator+= 호출
    std::cout << c;       // operator<< 호출
}

3.2 static 멤버

클래스의 모든 인스턴스가 공유하는 멤버를 정의할 수 있습니다.

class Counter {
private:
    static int totalCount;    // 정적 멤버 변수 선언
    int instanceCount;

public:
    Counter() : instanceCount(0) {
        totalCount++;
    }

    static int getTotalCount() {  // 정적 멤버 함수
        return totalCount;
        // 주의: 정적 멤버 함수에서는 일반 멤버 변수 접근 불가
    }

    void printCounts() const {
        std::cout << "Instance count: " << instanceCount << std::endl;
        std::cout << "Total count: " << totalCount << std::endl;
    }
};

// cpp 파일에서 정적 멤버 변수 정의 (필수)
int Counter::totalCount = 0;

3.3 특수 키워드들

3.3.1 mutable 키워드

const 멤버 함수 내에서도 수정이 가능한 멤버를 선언할 수 있습니다.

class Cache {
private:
    mutable int accessCount;
    mutable std::mutex mtx;
    std::map<std::string, std::string> data;

public:
    std::string getValue(const std::string& key) const {
        std::lock_guard<std::mutex> lock(mtx);  // mutable이라 수정 가능
        accessCount++;  // mutable이라 const 함수에서도 수정 가능
        return data.at(key);
    }
};
3.3.2 explicit 키워드

의도하지 않은 암시적 형변환을 방지합니다.

class MyString {
public:
    explicit MyString(int n) : str(n, 'x') {}
    // explicit이 없다면 MyString str = 10; 같은 암시적 변환이 가능
private:
    std::string str;
};

void processString(const MyString& str) {
    // 처리 로직
}

int main() {
    processString(MyString(10));  // OK
    // processString(10);         // 컴파일 에러: 암시적 변환 불가
}
3.3.3 using 키워드와 타입 별칭

현대적인 타입 별칭을 정의할 수 있습니다.

// C 스타일
typedef std::vector<std::string> StringList_t;
typedef void (*FunctionPtr)(int);

// Modern C++ 스타일
using StringList = std::vector<std::string>;
using FunctionPtr = void(*)(int);
using IntPtr = std::unique_ptr<int>;

// 템플릿 별칭 (typedef로는 불가능)
template<typename T>
using Vec = std::vector<T>;

Vec<int> numbers;  // std::vector<int>와 동일

3.4 enum class

범위가 지정된 열거형을 제공합니다.

// 기존 enum의 문제점
enum Color { RED, GREEN, BLUE };         // 전역 네임스페이스 오염
enum TrafficLight { RED, YELLOW, GREEN }; // 에러: RED, GREEN 중복

// enum class 사용
enum class NewColor { Red, Green, Blue };
enum class Light { Red, Yellow, Green };  // OK

void useColors() {
    NewColor c1 = NewColor::Red;   // 명시적 범위
    Light signal = Light::Red;     // 이름 충돌 없음

    // int x = c1;                 // 에러: 암시적 변환 불가
    int x = static_cast<int>(c1);  // OK: 명시적 변환 필요
}

3.5 초기화 방법들

C++11 이후 통일된 초기화 문법을 제공합니다.

class Widget {
    // 멤버 변수 초기화
    int n{42};                     // 중괄호 초기화
    std::string s{"hello"};        // 직접 초기화
    std::vector<int> v{1, 2, 3};   // 컨테이너 초기화
    int arr[3]{1, 2, 3};          // 배열 초기화

public:
    // 생성자 초기화 리스트
    Widget() : n{42}, s{"hello"} { }
};

int main() {
    // 통일된 초기화 문법
    int x{42};                    // 기본 타입
    std::string s{"hello"};       // 객체
    std::vector<int> v{1, 2, 3};  // 컨테이너
    Widget w{};                   // 사용자 정의 타입
}

Part 3: 고급 기능과 현대적 프로그래밍

1. 상속과 다형성

1.1 클래스 상속의 기본

class Animal {
protected:
    std::string name;

public:
    Animal(const std::string& n) : name(n) {}

    virtual void makeSound() {
        std::cout << "Some sound" << std::endl;
    }
};

class Dog : public Animal {
public:
    Dog(const std::string& n) : Animal(n) {}

    void makeSound() override {
        std::cout << name << " says: Woof!" << std::endl;
    }
};

1.2 가상 함수와 동적 바인딩

void playSound(Animal* animal) {
    animal->makeSound();  // 런타임에 실제 타입의 함수 호출
}

int main() {
    Animal* dog = new Dog("Bobby");
    Animal* cat = new Cat("Kitty");

    playSound(dog);  // "Bobby says: Woof!"
    playSound(cat);  // "Kitty says: Meow!"

    delete dog;
    delete cat;
}

1.3 순수 가상 함수와 추상 클래스

class Shape {
public:
    virtual double getArea() const = 0;  // 순수 가상 함수
    virtual ~Shape() {}  // 가상 소멸자
};

class Circle : public Shape {
private:
    double radius;

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

    double getArea() const override {
        return 3.14159 * radius * radius;
    }
    // 소멸자는 메모리 해제 등 특별한 작업이 필요없는 경우 생략할 수 있다.
};

1.4 final과 override

상속과 관련된 명시적 키워드를 제공합니다.

class Base {
public:
    virtual void foo() = 0; // 오버라이드 필수
    virtual void bar() { }  // 디폴트 구현있음 (오버라이드 선택)
    void baz() { }
};

class Derived final : public Base {  // 더 이상 상속 불가능
    void foo() override { }          // 기반 클래스 함수를 오버라이드
    void bar() override { }          // override 키워드로 (오타 등) 실수 방지
    // void baz() override { }       // 컴파일 에러: 가상 함수가 아님
};

// class Further : public Derived { }; // 에러: final 클래스는 상속 불가

1.5 특수 멤버 함수

C++의 특수 멤버 함수들은 컴파일러가 자동으로 생성할 수 있습니다.

class Example {
public:
   // 기본 생성자 
   Example() = default;  // 명시적으로 기본 구현 사용

   // 복사 생성자
   Example(const Example& other) = delete;  // 복사 생성 금지

   // 이동 생성자 (C++11 이상)
   Example(Example&& other) noexcept { // noexcept 는 예외가 발생하지 않음 명시
       // 리소스 이동
   }

   // 복사 대입 연산자
   Example& operator=(const Example& other) {
       if (this != &other) {
           // 깊은 복사 구현
       }
       return *this;
   }

   // 이동 대입 연산자 (C++11 이상)
   Example& operator=(Example&& other) noexcept = default;

   // 소멸자 (가상 소멸자 XX)
   // 가상 소멸자가 필요한 경우 virtual 을 붙여준다.
   ~Example() {
       // 리소스 정리
   }
};

1.6 special member functions의 제어

컴파일러가 자동으로 생성하는 특수 멤버 함수들을 명시적으로 제어할 수 있습니다.

class NonCopyable {
public:
    NonCopyable() = default;                              // 기본 생성자 사용
    NonCopyable(const NonCopyable&) = delete;            // 복사 생성자 삭제
    NonCopyable& operator=(const NonCopyable&) = delete;  // 복사 대입 연산자 삭제

    // 이동 생성자와 이동 대입 연산자는 자동 생성
    NonCopyable(NonCopyable&&) = default;
    NonCopyable& operator=(NonCopyable&&) = default;
};

class OnlyMoveable {
public:
    OnlyMoveable() = default;

    // 복사 연산 삭제
    OnlyMoveable(const OnlyMoveable&) = delete;
    OnlyMoveable& operator=(const OnlyMoveable&) = delete;

    // 이동 연산 명시적 정의
    OnlyMoveable(OnlyMoveable&& other) noexcept = default;
    OnlyMoveable& operator=(OnlyMoveable&& other) noexcept = default;
};

2. 예외 처리

2.1 기본 예외 처리

// C 스타일 에러 처리
int divide_c(int a, int b, int* result) {
    if (b == 0) return -1;  // 에러 코드 반환
    *result = a / b;
    return 0;  // 성공
}

// C++ 스타일 예외 처리
double divide(int a, int b) {
    if (b == 0) {
        throw std::runtime_error("Division by zero!");
    }
    return static_cast<double>(a) / b;
}

int main() {
    try {
        double result = divide(10, 0);
    }
    catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }
}

2.2 사용자 정의 예외

class DatabaseError : public std::runtime_error {
public:
    DatabaseError(const std::string& message)
        : std::runtime_error(message) {}
};

class Database {
public:
    void connect(const std::string& connectionString) {
        if (connectionString.empty()) {
            throw DatabaseError("Empty connection string");
        }
        // 연결 로직...
    }
};

3. 템플릿과 일반화 프로그래밍

3.1 함수 템플릿 심화

// 여러 타입 매개변수
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}

// 템플릿 특수화
template<typename T>
T abs(T value) {
    return value < 0 ? -value : value;
}

template<>
std::string abs(std::string value) {
    return value;  // 문자열은 절대값이 의미 없음
}

3.2 클래스 템플릿

template<typename T>
class Stack {
private:
    std::vector<T> elements;

public:
    void push(const T& element) {
        elements.push_back(element);
    }

    T pop() {
        if (elements.empty()) {
            throw std::runtime_error("Stack is empty");
        }
        T top = elements.back();
        elements.pop_back();
        return top;
    }
};

// 사용 예시
Stack<int> intStack;
Stack<std::string> stringStack;

3.3 STL 소개

#include <vector>
#include <algorithm>
#include <string>

int main() {
    // 벡터 사용
    std::vector<int> numbers = {1, 5, 3, 4, 2};

    // 알고리즘 사용
    std::sort(numbers.begin(), numbers.end());

    // find 사용
    auto it = std::find(numbers.begin(), numbers.end(), 3);
    if (it != numbers.end()) {
        std::cout << "Found 3!" << std::endl;
    }

    // for_each 사용
    std::for_each(numbers.begin(), numbers.end(), 
        [](int n) { std::cout << n << " "; });
}

3.4 템플릿 메타프로그래밍 기초

constexpr 는 컴파일시점에 상수로 확정됨을 의미합니다.

// 컴파일 시점 팩토리얼 계산
template<unsigned int N>
struct Factorial {
    static constexpr unsigned int value = N * Factorial<N-1>::value;
};

template<>
struct Factorial<0> {
    static constexpr unsigned int value = 1;
};
// 사용 예시
constexpr auto fact5 = Factorial<5>::value;  // 컴파일 시점에 계산

3.5 RAII와 리소스 관리

RAII(Resource Acquisition Is Initialization) 는 리소스 취득과 해제를 자동화하는 패턴입니다.

class File {
private:
    FILE* fp;

public:
    File(const char* filename, const char* mode) {
        fp = fopen(filename, mode);
        if (!fp) throw std::runtime_error("Failed to open file");
    }

    ~File() {
        if (fp) fclose(fp);
    }

    // 복사 방지 (실수로 인한 중복 close 방지)
    File(const File&) = delete;
    File& operator=(const File&) = delete;

    // 파일 작업 메서드들...
};

// 사용 예시
void processFile() {
    File f("data.txt", "r");  // 자동으로 열고
    // 파일 작업...
}  // 스코프를 벗어나면 자동으로 닫힘

Part 4: 실전과 현대 C++

1. STL 활용

1.1 컨테이너 기초

#include <vector>
#include <list>
#include <map>
#include <set>

int main() {
    // 벡터 (동적 배열)
    std::vector<int> vec = {1, 2, 3, 4, 5};
    vec.push_back(6);  // 끝에 추가

    // 리스트 (이중 연결 리스트)
    std::list<int> lst = {1, 2, 3};
    lst.push_front(0);  // 앞에 추가

    // 맵 (키-값 쌍)
    std::map<std::string, int> scores;
    scores["Alice"] = 95;
    scores["Bob"] = 87;

    // 셋 (중복 없는 정렬된 컬렉션)
    std::set<int> uniqueNums = {3, 1, 4, 1, 5};  // 1은 한 번만 저장됨

    // remove_if (조건부 제거) 는 아래에서 설명함
}

1.2 반복자 활용

std::vector<int> numbers = {1, 2, 3, 4, 5};

// 기본 반복자 사용
for (auto it = numbers.begin(); it != numbers.end(); ++it) {
    std::cout << *it << " ";
}

// 역방향 반복자
for (auto rit = numbers.rbegin(); rit != numbers.rend(); ++rit) {
    std::cout << *rit << " ";
}

// 범위 기반 for 루프 (가장 현대적인 방법)
for (const auto& num : numbers) {
    std::cout << num << " ";
}

1.3 알고리즘 라이브러리

#include <algorithm>

std::vector<int> v = {5, 2, 8, 1, 9};

// 정렬
std::sort(v.begin(), v.end());

// 이진 검색
bool found = std::binary_search(v.begin(), v.end(), 8);

// 최대/최소 요소 찾기
auto max_it = std::max_element(v.begin(), v.end());
auto min_it = std::min_element(v.begin(), v.end());

// 특정 값 찾기
auto it = std::find(v.begin(), v.end(), 8);
if (it != v.end()) {
    std::cout << "Found 8 at position: " << std::distance(v.begin(), it) << std::endl;
}

// 조건에 맞는 요소 개수 세기
int count = std::count_if(v.begin(), v.end(), 
    [](int n) { return n % 2 == 0; });  // 짝수 개수

1.4 자주 사용되는 컨테이너 활용

1.4.1 std::deque (Double-ended Queue)
#include <deque>

// deque는 양쪽 끝에서 빠른 삽입/삭제가 가능한 컨테이너
std::deque<int> dq;

// 앞뒤로 추가
dq.push_front(1);  // [1]
dq.push_back(2);   // [1,2]

// 임의의 위치 접근
dq[0] = 10;        // [10,2]

// 앞뒤로 제거
dq.pop_front();    // [2]
dq.pop_back();     // []
1.4.2 std::set과 std::multiset
#include <set>

// set: 중복을 허용하지 않는 정렬된 컨테이너
std::set<int> s = {4, 1, 3, 1, 2};  // {1, 2, 3, 4}

// 삽입
auto [it, success] = s.insert(5);  // 삽입 성공 여부 확인
if (success) {
    std::cout << "5 inserted successfully\n";
}

// 검색
if (s.find(3) != s.end()) {
    std::cout << "3 exists\n";
}

// multiset: 중복을 허용하는 정렬된 컨테이너
std::multiset<int> ms = {4, 1, 3, 1, 2};  // {1, 1, 2, 3, 4}

// 특정 값의 개수 확인
std::cout << "Count of 1: " << ms.count(1) << std::endl;  // 2
1.4.3 std::unordered_set과 std::unordered_map
#include <unordered_set>
#include <unordered_map>

// unordered_set: 해시 테이블 기반의 집합
std::unordered_set<std::string> words = {"apple", "banana", "orange"};

// 빠른 검색
if (words.find("apple") != words.end()) {
    std::cout << "apple exists\n";
}

// unordered_map: 해시 테이블 기반의 키-값 쌍
std::unordered_map<std::string, int> scores = {
    {"Alice", 100},
    {"Bob", 95},
    {"Charlie", 90}
};

// 키로 접근
scores["David"] = 85;  // 새로운 요소 추가

// 안전한 검색
if (auto it = scores.find("Alice"); it != scores.end()) {
    std::cout << "Alice's score: " << it->second << "\n";
}
1.4.4 std::stack과 std::queue
#include <stack>
#include <queue>

// stack: LIFO (Last In First Out)
std::stack<int> st;
st.push(1);
st.push(2);
st.push(3);

while (!st.empty()) {
    std::cout << st.top() << " ";  // 3 2 1
    st.pop();
}

// queue: FIFO (First In First Out)
std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);

while (!q.empty()) {
    std::cout << q.front() << " ";  // 1 2 3
    q.pop();
}
1.4.5 std::priority_queue
#include <queue>

// 기본적으로 최대 힙
std::priority_queue<int> pq;
pq.push(3);
pq.push(1);
pq.push(4);

while (!pq.empty()) {
    std::cout << pq.top() << " ";  // 4 3 1
    pq.pop();
}

// 최소 힙 만들기
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
min_pq.push(3);
min_pq.push(1);
min_pq.push(4);

while (!min_pq.empty()) {
    std::cout << min_pq.top() << " ";  // 1 3 4
    min_pq.pop();
}

1.5 유용한 STL 알고리즘

1.5.1 정렬과 검색
#include <algorithm>

std::vector<int> v = {4, 1, 3, 5, 2};

// 정렬
std::sort(v.begin(), v.end());  // [1,2,3,4,5]

// 역순 정렬
std::sort(v.begin(), v.end(), std::greater<int>());  // [5,4,3,2,1]

// 부분 정렬
std::partial_sort(v.begin(), v.begin() + 3, v.end());  // 앞의 3개만 정렬

// 이진 검색
if (std::binary_search(v.begin(), v.end(), 3)) {
    std::cout << "3 exists\n";
}

// lower_bound: 값 이상의 첫 위치
auto it = std::lower_bound(v.begin(), v.end(), 3);

// upper_bound: 값 초과의 첫 위치
auto it2 = std::upper_bound(v.begin(), v.end(), 3);
1.5.2 수정 알고리즘
std::vector<int> v = {1, 2, 3, 4, 5};

// 값 변경
std::replace(v.begin(), v.end(), 3, 30);  // 3을 30으로

// 조건부 변경
std::replace_if(v.begin(), v.end(),
    [](int n) { return n % 2 == 0; },  // 짝수를
    0);                                // 0으로

// 값 제거
auto new_end = std::remove(v.begin(), v.end(), 3);
v.erase(new_end, v.end());  // 실제로 컨테이너에서 제거

// 조건부 제거
auto new_end2 = std::remove_if(v.begin(), v.end(),
    [](int n) { return n < 0; });  // 음수 제거
v.erase(new_end2, v.end());
1.5.3 순열 알고리즘
std::vector<int> v = {1, 2, 3};

// 다음 순열
do {
    for (int n : v) std::cout << n << ' ';
    std::cout << '\n';
} while (std::next_permutation(v.begin(), v.end()));
// 출력: 1 2 3
//       1 3 2
//       2 1 3
//       2 3 1
//       3 1 2
//       3 2 1

// 이전 순열
while (std::prev_permutation(v.begin(), v.end())) {
    // 처리
}
1.5.4 수치 알고리즘
#include <numeric>

std::vector<int> v = {1, 2, 3, 4, 5};

// 합계
int sum = std::accumulate(v.begin(), v.end(), 0);

// 곱
int product = std::accumulate(v.begin(), v.end(), 1,
    std::multiplies<int>());

// 내적
std::vector<int> v2 = {2, 3, 4, 5, 6};
int dot_product = std::inner_product(
    v.begin(), v.end(),  // 첫 번째 범위
    v2.begin(),          // 두 번째 범위의 시작
    0);                  // 초기값

// 부분합
std::vector<int> partial_sums;
std::partial_sum(v.begin(), v.end(),
    std::back_inserter(partial_sums));
// partial_sums: [1,3,6,10,15]
1.5.5 실용적인 알고리즘 조합 예제
// 학생 성적 처리 예제
struct Student {
    std::string name;
    int score;
};

std::vector<Student> students = {
    {"Alice", 85}, {"Bob", 92}, {"Charlie", 78}
};

// 성적순 정렬
std::sort(students.begin(), students.end(),
    [](const Student& a, const Student& b) {
        return a.score > b.score;  // 내림차순
    });

// 80점 이상인 학생 수 세기
int honor_count = std::count_if(students.begin(), students.end(),
    [](const Student& s) { return s.score >= 80; });

// 전체 평균 계산
double avg = std::accumulate(students.begin(), students.end(), 0.0,
    [](double acc, const Student& s) { return acc + s.score; })
    / students.size();

// 특정 점수 범위의 학생들 찾기
auto [lower, upper] = std::equal_range(students.begin(), students.end(),
    Student{"", 85},  // 85점을 기준으로
    [](const Student& a, const Student& b) {
        return a.score > b.score;
    });

2. 현대 C++ 기능 (C++11 이후)

2.1 람다 표현식

// 기본 람다
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << std::endl;

// 캡처 사용
int multiplier = 10;
auto multiply = [multiplier](int x) { return x * multiplier; };

// 알고리즘과 함께 사용
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::for_each(numbers.begin(), numbers.end(),
    [](int& n) { n *= 2; });  // 모든 요소 2배로

2.2 스마트 포인터

#include <memory>

// unique_ptr: 단일 소유권
std::unique_ptr<int> p1(new int(42));
auto p2 = std::make_unique<int>(42);  // 더 안전한 방법

// shared_ptr: 공유 소유권
std::shared_ptr<int> sp1 = std::make_shared<int>(100);
std::shared_ptr<int> sp2 = sp1;  // 참조 카운트 증가

// weak_ptr: 순환 참조 방지
std::weak_ptr<int> wp = sp1;
if (auto sp = wp.lock()) {  // 유효한 객체인지 확인
    std::cout << *sp << std::endl;
}

2.3 move 시맨틱스

class BigData {
    std::vector<int> data;
public:
    // 이동 생성자
    BigData(BigData&& other) noexcept 
        : data(std::move(other.data)) {}

    // 이동 대입 연산자
    BigData& operator=(BigData&& other) noexcept {
        if (this != &other) {
            data = std::move(other.data);
        }
        return *this;
    }
};

// std::move 사용
std::vector<std::string> v1 = {"hello", "world"};
std::vector<std::string> v2 = std::move(v1);  // v1의 내용이 v2로 이동

std::move 이동되기 전 데이타 접근은 오류가 날수도 나지 않을 수도 있다.
그냥 명시적으로 초기화해주자.

3. 실전 프로젝트와 디버깅

3.1 프로젝트 구조화

// 헤더 파일 (.hpp)
class Project {
public:
    Project();
    void run();
private:
    // 구현 세부사항
};

// 구현 파일 (.cpp)
#include "project.hpp"

Project::Project() {
    // 초기화 코드
}

void Project::run() {
    // 실행 코드
}

3.2 성능 최적화

// 최적화 테크닉들
class OptimizedClass {
public:
    // 불필요한 복사 방지
    void process(const std::vector<int>& data) {
        // data를 참조로 받아 복사 방지
    }

    // 작은 객체는 값으로 전달
    void setCoordinate(Point p) {  // Point가 작은 구조체일 경우
        coord = p;
    }

    // RVO (Return Value Optimization)
    // 임시 vector 생성 -> 반환값으로 복사 -> 임시 객체 소멸 단계를 거치지 않고 즉시 반환값 생성
    std::vector<int> getNumbers() {
        return std::vector<int>{1, 2, 3};  // 컴파일러가 자동으로 최적화
    }
};

3.3 디버깅 기법

class DebugHelper {
public:
    static void log(const std::string& message) {
        #ifdef _DEBUG
            // _DEBUG 매크로가 정의된 경우에만 동작
            std::cerr << "[DEBUG] " << message << std::endl;
        #endif
    }

    // RAII 기반실행 시간 측정 클래스
    class Timer {
        std::chrono::steady_clock::time_point start;
        std::string name;
    public:
        Timer(const std::string& n) : name(n) {
            start = std::chrono::steady_clock::now();
        }
        ~Timer() {
            auto end = std::chrono::steady_clock::now();
            auto diff = end - start;
            std::cout << name << " took " 
                     << std::chrono::duration_cast<std::chrono::milliseconds>(diff).count()
                     << " ms" << std::endl;
        }
    };
};
// 사용 예시
void someFunction() {
    DebugHelper::Timer t("someFunction");
    // ... 코드 실행 ...
}

부록

1. C++ 코딩 스타일 가이드

1.1 일반적인 명명 규칙

// 클래스 이름: 파스칼 케이스
class StudentManager {};

// 함수 및 변수: 카멜 케이스
void calculateGrade();
int totalScore;

// 상수: 대문자와 언더스코어
const int MAX_STUDENTS = 100;

// 멤버 변수: m_ 또는 _ 접두사
class Student {
private:
    std::string m_name;
    int _age;
};

1.2 파일 구조

// header.hpp
#ifndef HEADER_HPP
#define HEADER_HPP

// 1. 시스템 헤더
#include <string>
#include <vector>

// 2. 프로젝트 헤더
#include "project_header.hpp"

// 3. 선언부
class MyClass {
    // ...
};

#endif

// source.cpp
#include "header.hpp"

// 구현부

1.3 모던 C++ 권장 사항

// 권장
auto iter = container.begin();  // auto 사용
const auto& value = getValue(); // const 참조
std::string str{"초기화"};     // 유니폼 초기화

// 비권장
iterator<vector<int>> iter = container.begin();
value = getValue();
std::string str = "초기화";

2. C에서 C++로의 코드 마이그레이션

2.1 기본 데이터 구조 변환

// C 스타일
typedef struct {
    char name[50];
    int age;
} Person_C;

void print_person(const Person_C* p) {
    printf("%s: %d\n", p->name, p->age);
}

// C++ 스타일
class Person {
private:
    std::string name;
    int age;

public:
    Person(const std::string& n, int a) 
        : name(n), age(a) {}

    void print() const {
        std::cout << name << ": " << age << std::endl;
    }
};

2.2 메모리 관리 변환

// C 스타일
int* array = (int*)malloc(size * sizeof(int));
free(array);

// C++ 스타일
std::vector<int> array(size);
// 자동으로 메모리 해제

// 또는 스마트 포인터 사용
auto ptr = std::make_unique<int[]>(size);

2.3 문자열 처리 변환

// C 스타일
char str[100];
strcpy(str, "Hello");
strcat(str, " World");

// C++ 스타일
std::string str = "Hello";
str += " World";

// 문자열 검색
if (str.find("World") != std::string::npos) {
    // 찾음
}

3. 자주 발생하는 실수와 해결 방법

3.1 메모리 관련 실수

// 실수 1: 메모리 누수
class Resource {
    int* data;
public:
    Resource() { data = new int[100]; }
    // 소멸자 없음 - 메모리 누수!
};

// 해결
class Resource {
    std::unique_ptr<int[]> data;
public:
    Resource() : data(std::make_unique<int[]>(100)) {}
    // 자동으로 메모리 해제
};

3.2 참조자 관련 실수

// 실수 2: 댕글링 참조 (수명이 끝난 메모리 영역 참조)
int& getDanglingReference() {
    int local = 42;
    return local;  // 지역 변수 참조 반환 - 위험!
}

// 해결
int getValue() {
    int local = 42;
    return local;  // 값 반환
}

3.3 상속 관련 실수

BaseClass (최상위 클래스) 는 반드시 가상 소멸자를 생성해야 한다.
안그러면 하위 소멸자들이 호출되지 않는다.

// 실수 3: 가상 소멸자 누락
class Base {
public:
    ~Base() {}  // 비가상 소멸자
};

class Derived : public Base {
    int* ptr;
public:
    Derived() : ptr(new int(42)) {}
    ~Derived() { delete ptr; }  // 메모리 누수 가능성
};

// 해결
class Base {
public:
    virtual ~Base() {}  // 가상 소멸자
};

4. 추천 도서 및 참고 자료

4.1 입문자를 위한 도서

  1. "C++ Primer" (Stanley Lippman)
  2. "Effective C++" (Scott Meyers)
  3. "A Tour of C++" (Bjarne Stroustrup)

4.2 중급자를 위한 도서

  1. "Modern C++ Design" (Andrei Alexandrescu)
  2. "Effective Modern C++" (Scott Meyers)
  3. "C++ Templates: The Complete Guide" (David Vandevoorde)

4.3 온라인 자료

  1. cppreference.com – 공식 레퍼런스
  2. isocpp.org – C++ 표준 위원회
  3. modernescpp.com – 모던 C++ 블로그

4.4 유용한 도구들

  1. 컴파일러

    • GCC/G++
    • Clang
    • MSVC
  2. 개발 도구

    • Visual Studio
    • CLion
    • VS Code with C++ extensions
  3. 정적 분석 도구

    • Clang-Tidy
    • Cppcheck
    • SonarQube

답글 남기기