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 입문자를 위한 도서
- "C++ Primer" (Stanley Lippman)
- "Effective C++" (Scott Meyers)
- "A Tour of C++" (Bjarne Stroustrup)
4.2 중급자를 위한 도서
- "Modern C++ Design" (Andrei Alexandrescu)
- "Effective Modern C++" (Scott Meyers)
- "C++ Templates: The Complete Guide" (David Vandevoorde)
4.3 온라인 자료
- cppreference.com – 공식 레퍼런스
- isocpp.org – C++ 표준 위원회
- modernescpp.com – 모던 C++ 블로그
4.4 유용한 도구들
-
컴파일러
- GCC/G++
- Clang
- MSVC
-
개발 도구
- Visual Studio
- CLion
- VS Code with C++ extensions
-
정적 분석 도구
- Clang-Tidy
- Cppcheck
- SonarQube