1. 개요

C++

C 언어를 기반으로 객체지향 프로그래밍 등의 기능을 추가하며 발전한 멀티 패러다임 프로그래밍 언어이다

멀티 패러다임이란 하나의 언어에서 여러 프로그래밍 방식을 지원한다는 뜻이다. C++에서는 절차적 프로그래밍, 객체지향 프로그래밍, 제네릭 프로그래밍 등을 활용할 수 있다

1.1 주요 설계 목표

1.1.1 C언어와의 호환성

C++는 C언어의 문법 체계를 기반으로 하므로 기존 C코드와 라이브러리를 상당 부분 활용할 수 있다

구분의미
소스 수준 호환성기존 C 소스 코드를 C++ 프로그램에서 활용할 수 있다.
링크 수준 호환성C로 컴파일된 목적 파일이나 라이브러리를 C++ 프로그램에 연결할 수 있다.

C와 C++의 호환 범위

C와 C++가 완전히 호환되는 것은 아니다. C에서는 허용되지만 C++에서는 수정이 필요한 코드도 있으므로, 기존 C 코드를 그대로 사용할 수 있다고 단정하면 안 된다

1.1.2 객체지향 개념 도입

캡슐화, 상속, 다형성을 지원하여 코드를 재사용하고, 규모가 큰 프로그램을 유지보수하기 쉽게 한다

개념핵심 의미
캡슐화데이터와 관련 기능을 하나로 묶고 접근을 제어한다.
상속기존 클래스의 기능을 재사용하고 확장한다.
다형성공통된 인터페이스를 통해 서로 다른 동작을 수행하도록 한다.

1.1.3 강력한 자료형 검사

자료형과 관련된 오류를 컴파일 단계에서 발견하도록 하여, 실행 중 발생할 수 있는 오류를 줄이고 디버깅을 돕는다

다만 모든 오류를 컴파일 단계에서 발견할 수 있는 것은 아니다

1.2 컴파일과 실행

파일을 .cpp로 저장한 뒤 다음 명령으로 빌드하고 실행한다

g++ main.cpp -o main
./main
명령의미
g++ main.cpp -o main소스 파일을 빌드하여 main라는 실행 파일을 생성한다.
./main현재 디렉터리에 있는 main를 실행한다.





2. C++의 주요 확장 기능

2.1 함수 오버로딩(Function Overloading)

매개변수의 개수나 자료형이 다르면 같은 이름의 함수를 여러 개 선언할 수 있는 기능이다

int add(int x, int y);           // 정수형 버전
double add(double x, double y); // 실수형 버전

함수를 호출하면 컴파일러가 전달된 인수의 자료형과 개수를 확인하여 함수를 선택한다

add(1, 2);       // int 버전 호출
add(1.5, 2.5);   // double 버전 호출

매개변수의 개수가 달라도 오버로딩할 수 있다

반환형만으로는 오버로딩할 수 없다

매개변수가 같고 반환형만 다른 함수는 오버로딩할 수 없다. 또한 여러 함수 중 호출할 함수를 하나로 결정할 수 없으면 컴파일 오류가 발생한다

int calculate(int x);
double calculate(int x); // 오류: 반환형만 다름

2.2 기본 인수(Default Argument)

함수의 매개변수에 기본값을 지정하여, 호출할 때 해당 인수를 생략할 수 있게 하는 기능이다

void printValues(int x = 10, int y = 20, int z = 30)
{
    std::cout << "Values: "
              << x << " " << y << " " << z << '\n';
}

전달한 인수는 왼쪽 매개변수부터 대응하고, 생략한 인수에는 기본값이 사용된다

printValues(1, 2, 3); // 1 2 3
printValues(1, 2);    // 1 2 30
printValues(1);       // 1 20 30
printValues();        // 10 20 30

일반적인 함수 선언에서는 기본값을 오른쪽 매개변수부터 연속해서 지정한다

void func(int x, int y = 10, int z = 20); // 가능
void func(int x = 10, int y, int z);      // 오류

호출할 때도 오른쪽 인수부터 생략할 수 있으며, 중간 인수만 건너뛰어 전달할 수는 없다

void func(int x, int y = 10, int z = 20); // 가능
void func(int x = 10, int y, int z);      // 오류

2.3 참조(Reference)

참조는 이미 존재하는 변수에 붙이는 또 다른 이름, 즉 별칭(alias)이다

int value = 5;
int& ref = value;

ref는 별도의 정수 객체가 아니라 value의 별칭이다. 따라서 어느 이름으로 값을 변경하더라도 같은 객체가 변경된다

value = 6; // value와 ref 모두 6
ref = 7;   // value와 ref 모두 7

2.3.1 &의 의미 구분

&는 사용되는 위치에 따라 의미가 다르다

코드의미
int& ref = value;참조형을 선언하는 기호
&value변수의 주소를 구하는 연산자
예제: 복사와 참조의 차이
int value = 5;
int copy = value;
int& ref = value;
 
copy = 10; // copy만 변경
ref = 20;  // value가 변경

최종 값은 다음과 같다

변수 이름값
value20
copy10
ref20

copy는 값을 복사한 별도의 변수이고, ref는 원본의 별칭이다

2.4 참조에 의한 호출(Call-by-Reference)

함수의 매개변수를 참조형으로 선언하여 전달된 원본을 참조하는 방식이다

void set10(int& ref)
{
    ref = 10;
}
 
int main()
{
    int x = 1;
 
    std::cout << x << '\n'; // 1
    set10(x);
    std::cout << x << '\n'; // 10
}

set10()의 매개변수 ref는 x의 별칭이다. 따라서 함수 내부에서 ref를 변경하면 원본인 x가 변경된다

값에 의한 호출과 비교

void changeValue(int value)
{
    value = 10;
}
 
void changeReference(int& ref)
{
    ref = 10;
}
구분값에 의한 호출참조에 의한 호출
매개변수 예시int valueint& ref
전달 방식인수의 값을 복사원본의 별칭으로 사용
매개변수에 대입할 때원본은 변경되지 않음원본이 변경됨

크기가 큰 객체를 참조로 전달하면 불필요한 복사를 피할 수 있다. 원본 변경도 막고 싶다면 const 참조를 사용한다

void print(const std::string& text);

위 선언에서 text를 통해 원본 문자열을 변경할 수는 없다. std::string을 사용하려면 <string> 헤더가 필요하다

2.5 new와 delete 연산자

new와 delete는 실행 중 필요한 메모리를 할당하고 해제하는 동적 메모리 관리에 사용한다

int* ptr = new int(5);
delete ptr;
코드의미
new int(5)정수 객체를 동적으로 생성하고 5로 초기화
int* ptr생성된 객체의 주소를 저장하는 포인터
delete ptr해당 객체를 제거하고 메모리를 해제

동적 메모리는 다음과 같은 경우에 활용할 수 있다.

  • 실행 중에 필요한 데이터의 크기가 결정될 때
  • 사용자 입력에 맞추어 메모리를 할당해야 할 때
  • 객체의 수명을 해당 객체를 생성한 함수의 실행 범위보다 길게 유지해야 할 때

2.5.1 배열의 동적 할당

배열을 동적으로 생성할 때는 new[]를 사용하고, 해제할 때는 delete[]를 사용한다

int* arr = new int[10];
 
arr[0] = 100;
 
delete[] arr;

할당 방식과 해제 방식의 대응

new로 생성한 객체는 delete, new[]로 생성한 배열은 **delete[]**로 해제해야 한다. 해제한 메모리에 다시 접근하거나 같은 메모리를 중복 해제하면 정의되지 않은 동작이 발생한다

2.5.2 C의 malloc()과 비교

항목malloc()new
기본 사용 형태malloc(sizeof(int))new int
할당 크기바이트 단위로 지정자료형으로 결정
결과의 자료형void*생성한 객체를 가리키는 포인터형
초기화객체의 초기화를 별도로 처리초기값 지정 가능
클래스 생성자호출하지 않음호출함
대응하는 해제free()delete 또는 delete[]

C++에서 malloc()이 반환하는 void*를 다른 객체 포인터형으로 변환하려면 명시적 형 변환이 필요하다. C에서는 이 형 변환이 필수가 아니다

예제: 사용자 입력 크기의 배열

#include <iostream>
 
int main()
{
    int size;
    std::cin >> size;
 
    if (size <= 0)
    {
        return 0;
    }
 
    int* numbers = new int[size];
 
    for (int i = 0; i < size; ++i)
    {
        numbers[i] = i + 1;
        std::cout << numbers[i] << ' ';
    }
 
    std::cout << '\n';
    delete[] numbers;
 
    return 0;
}

3을 입력하면 다음과 같이 출력된다.

1 2 3

실제 프로그램에서는 직접 메모리를 관리하는 부담을 줄이기 위해 std::vector나 스마트 포인터 등을 활용할 수도 있다

2.6 연산자 오버로딩(Operator Overloading)

+, -, =, ==, *, /, % 등의 기존 연산자가 사용자 정의 자료형에서 수행할 동작을 정의하는 기능이다. 클래스나 구조체를 대상으로 정의할 수 있다

다음은 두 복소수를 더하는 예제이다

class Complex
{
public:
    double real;
    double imag;
 
    Complex(double r = 0.0, double i = 0.0)
        : real(r), imag(i)
    {
    }
 
    Complex operator+(const Complex& other) const
    {
        return Complex(
            real + other.real,
            imag + other.imag
        );
    }
};

operator+()는 Complex 객체 사이에서 +가 수행할 동작을 정의한다

Complex c1(1.0, 2.0);
Complex c2(2.0, 3.0);
Complex c3 = c1 + c2;

실수부와 허수부를 각각 더하므로 결과는 다음과 같다

멤버계산결과
c3.real1.0 + 2.03.0
c3.imag2.0 + 3.05.0

이 예제에서 c1 + c2는 다음과 같은 멤버 함수 호출에 대응한다

c1.operator+(c2);

연산자 오버로딩은 새로운 연산자 기호를 만드는 기능이 아니다. 기존 연산자에 사용자 정의 자료형을 위한 동작을 부여한다

2.7 제너릭 함수와 클래스(Generic Function and Class)

여러 자료형에 공통으로 사용할 수 있는 함수나 클래스를 만드는 기능이다. C++에서는 주로 템플릿으로 구현한다

template <class T>
void swap(T& a, T& b)
{
    T temp;
    temp = a;
    a = b;
    b = temp;
}

T는 실제 자료형이 들어갈 자리이다. 여기서 class T는 T가 클래스형으로만 제한된다는 뜻이 아니다. int, double 같은 기본 자료형도 사용할 수 있다

2.7.1 자료형에 따른 사용

int a = 10;
int b = 20;
 
swapValues(a, b); // T를 int로 추론

호출 후 a는 20, b는 10이 된다

double x = 1.5;
double y = 2.5;
 
swapValues(x, y); // T를 double로 추론

호출 후 x는 2.5, y는 1.5가 된다

컴파일러는 사용된 자료형에 맞는 템플릿의 인스턴스를 만든다. 따라서 자료형마다 비슷한 함수를 반복해서 작성하는 일을 줄일 수 있다

주요 특징

  • 하나의 코드로 여러 자료형을 처리할 수 있다
  • 중복 코드를 줄일 수 있다
  • 함수뿐 아니라 클래스도 템플릿으로 작성할 수 있다
  • std::vector, std::list, std::map 등도 클래스 템플릿이다

다만 모든 자료형에서 무조건 사용할 수 있는 것은 아니다. 위 예제는 T temp;와 대입 연산을 사용하므로, 해당 자료형이 이러한 연산을 지원해야 한다






3. C++의 특성

C++는 객체지향 프로그래밍의 핵심 개념인 캡슐화, 상속, 다형성을 지원한다

3.1 캡슐화

캡슐화(Encapsulation)

객체의 데이터(속성)와 행위(메서드)를 하나의 단위로 묶고, 외부에서 접근할 수 있는 범위를 제어하는 것이다

C++에서는 private, public, protected 등의 접근 지정자를 사용한다. 보호할 데이터를 외부에서 직접 변경하지 못하게 하고, 공개된 멤버 함수를 통해 다루도록 설계할 수 있다

class BankAccount {
private:
    string depositorName_; // 예금주 이름
    string accountNum_;    // 계좌번호
    double balance_;       // 잔액
 
public:
    BankAccount();
 
    void showBankAccount() const;
    void deposit(const double& amount);
    void withdraw(const double& amount);
};

이 예제에서 예금주 이름, 계좌번호, 잔액은 private로 선언되어 외부에서 직접 접근할 수 없다

외부 코드는 deposit()이나 withdraw() 같은 공개 함수를 통해 잔액을 변경하도록 설계한다. 함수 내부에서 입출금 조건을 검사하면 잘못된 변경을 방지할 수 있다. 위 코드는 클래스의 인터페이스를 보여 주는 선언 예제이며, 실제 사용을 위해서는 생성자와 멤버 함수의 정의가 필요하다

장점

  • 중요한 데이터를 외부의 잘못된 접근으로부터 보호할 수 있다
  • 데이터가 변경되는 방식을 클래스 내부에서 통제할 수 있다
  • 내부 구현이 변경되어도 외부 코드에 미치는 영향을 줄일 수 있다

3.2 상속(Inheritance)

상속(Inheritance)

기존 클래스를 기반으로 새로운 클래스(자식 클래스)를 정의하여 속성과 기능을 재사용하고 확장하는 것이다

기존 코드를 활용하면서 새로운 기능을 추가하거나 기존 기능을 확장할 수 있다

class Vehicle {
public:
    void setSpeed();
    void turn();
 
private:
    char* brand;
};

이 Vehicle 클래스를 상속받아 Car, Truck, Bus 클래스를 정의할 수 있다

class Car : public Vehicle {
public:
    void setTurbo();
 
private:
    int turbo;
};
 
class Truck : public Vehicle {
public:
    void setPayload();
 
private:
    int payload;
};
 
class Bus : public Vehicle {
public:
    void setNumPeople();
 
private:
    int numPeople;
};
구분예제다른 명칭
부모 클래스Vehicle기반 클래스, 슈퍼클래스
자식 클래스Car, Truck, Bus파생 클래스, 서브클래스

장점

  • 중복 코드를 줄일 수 있다
  • 기존 프로그램의 기능을 재사용할 수 있다
  • 공통 기능은 부모 클래스에서 한 번에 관리할 수 있다
  • 프로그램의 확장과 유지보수가 쉬워진다

3.3 다형성(Polymorphism)

다형성(Polymorphism)

“많은 형태를 가질 수 있다”는 뜻으로, 동일한 모양의 코드가 상황(어떤 객체가 호출하느냐)에 따라 다르게 동작한다

상속과 가상 함수를 이용하면 상속 구조를 유연하게 활용할 수 있다

가상 함수와 재정의 다음 예제에서는 부모 클래스 Animal의 speak()를 가상 함수로 선언하고, 자식 클래스에서 각각 재정의한다

class Animal {
public:
    virtual void speak() = 0;
    virtual ~Animal() = default;
};
 
class Dog : public Animal {
public:
    void speak() override {
        cout << "Woof\n";
    }
};
 
class Cat : public Animal {
public:
    void speak() override {
        cout << "Meow\n";
    }
};
문법의미
virtual부모 클래스의 포인터나 참조를 통해 호출할 때 실제 객체에 맞는 재정의 함수를 실행하도록 한다.
= 0순수 가상 함수임을 나타낸다. 이 예제의 Animal은 직접 객체를 만들 수 없는 추상 클래스이다.
override부모의 가상 함수를 올바르게 재정의했는지 컴파일러가 검사하도록 한다.
virtual ~Animal()부모 클래스 포인터로 자식 객체를 삭제할 때 올바르게 소멸되도록 한다.

부모 클래스 포인터를 통한 호출이다

Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
 
animal1->speak();
animal2->speak();

두 포인터의 자료형은 모두 Animal*이지만, 가리키는 객체의 자료형은 다르다.

호출실제 객체실행되는 함수
animal1->speak()DogDog::speak()
animal2->speak()CatCat::speak()

이처럼 실행 중 실제 객체의 자료형에 따라 호출할 가상 함수가 결정되는 것을 동적 바인딩(Dynamic Binding)이라고 한다

상속만으로 동적 바인딩이 이루어지지는 않는다

부모 클래스의 포인터나 참조로 호출할 때 실제 객체에 맞는 함수가 실행되려면, 해당 함수가 가상 함수로 선언되어 있어야 한다. 자식 클래스에 같은 이름의 함수를 작성하는 것만으로는 충분하지 않다

오버로딩과 오버라이딩의 구분

구분의미함수 선택 시점
함수 오버로딩같은 이름의 함수를 서로 다른 매개변수로 정의컴파일 시간
연산자 오버로딩사용자 정의 자료형에 대한 기존 연산자의 동작을 정의이 단원 예제에서는 컴파일 시간
함수 오버라이딩자식 클래스가 부모 클래스의 가상 함수를 재정의부모 포인터·참조를 통한 가상 호출에서는 실행 시간

오버라이딩은 함수를 재정의하는 것, 동적 바인딩은 실제 호출할 재정의 함수를 실행 중 결정하는 것으로 구분하면 된다






4. C++ 표준 라이브러리

C++ 프로그램에서 공통으로 사용하는 함수, 클래스, 템플릿 등을 표준으로 제공하는 라이브러리이다. 필요한 헤더를 포함하여 입출력, 문자열 처리, 자료구조, 정렬 등의 기능을 사용할 수 있다

주요 기능을 다음과 같이 나누어 살펴볼 수 있다

4.1 C 라이브러리

기존 C 표준 라이브러리의 기능을 C++에서도 사용할 수 있도록 제공한다. C++에서는 일반적으로 기존 C 헤더 이름에서 .h를 제거하고 앞에 c를 붙인 형태를 사용한다

C 헤더대응하는 C++ 헤더주요 기능
<stdio.h><cstdio>표준 입출력
<stdlib.h><cstdlib>숫자 변환, 메모리 관리 등
<string.h><cstring>C 스타일 문자열과 메모리 처리
<time.h><ctime>날짜와 시간 처리
<signal.h><csignal>신호 처리

C++ 형식의 헤더에서는 일반적인 함수와 자료형 이름을 std 네임스페이스에서 사용할 수 있다. 단, 매크로 등은 네임스페이스의 적용 대상이 아니다

4.2 C++ 입출력 라이브러리

스트림(Stream)을 통해 데이터를 읽고 쓰는 기능을 제공하는 라이브러리이다. 콘솔 입출력과 파일 입출력 등에 사용한다

스트림은 데이터가 들어오거나 나가는 흐름으로 이해할 수 있다

4.2.1 콘솔 입출력

<iostream>을 포함하여 표준 입출력 객체를 사용한다

객체역할주요 연산자
std::cin표준 입력>>
std::cout표준 출력<<
std::cerr표준 오류 출력<<
#include <iostream>
 
std::cout << "Hello";
std::cin >> value;

4.2.2 파일 입출력

<fstream>을 포함하여 파일 스트림을 사용한다

클래스역할
std::ofstream파일 출력
std::ifstream파일 입력
std::fstream파일 입력과 출력
#include <fstream>
 
std::ofstream outputFile("data.txt");
std::ifstream inputFile("data.txt");

첫 번째 문장은 출력용으로, 두 번째 문장은 입력용으로 파일을 연다

기본 설정의 std::ofstream은 같은 이름의 파일이 이미 있으면 기존 내용을 지운 뒤 새로 쓴다

주요 입출력 헤더

헤더주요 기능
<iostream>표준 입출력 객체
<fstream>파일 입출력
<iomanip>출력 너비, 정밀도 등의 서식 조절
<istream>입력 스트림
<ostream>출력 스트림

4.3 C++ STL 라이브러리

STL

제네릭 프로그래밍을 지원하는 템플릿 기반 라이브러리이다. 컨테이너, 알고리즘, 반복자 등을 제공하여 여러 자료형에 공통된 방식으로 데이터를 저장하고 처리할 수 있게 한다

주요 구성요소

구성 요소역할예시
컨테이너(Container)데이터를 저장하고 관리std::vector, std::list, std::map
알고리즘(Algorithm)정렬, 검색, 개수 계산 등의 작업std::sort, std::find, std::count
반복자(Iterator)원소를 가리키고 순회하며 처리 범위를 지정begin(), end()로 얻는 반복자
함수 객체(Function Object)함수처럼 호출되어 작업이나 기준을 제공std::greater<int>

대표적인 컨테이너

분류예
순차 컨테이너vector, list, deque
연관 컨테이너map, set
컨테이너 어댑터stack, queue
알고리즘sort, find, count
기타 기능iterator, functional, memory, numeric

컨테이너는 저장할 자료형을 지정하여 사용한다

std::vector<int> numbers;    // 정수를 저장
std::vector<double> values;  // 실수를 저장

컨테이너, 반복자, 알고리즘의 연결

#include <algorithm>
#include <vector>
 
int main()
{
    std::vector<int> numbers = {3, 1, 2};
 
    std::sort(numbers.begin(), numbers.end());
 
    return 0;
}
코드역할
std::vector<int>정수를 저장하는 컨테이너
numbers.begin()첫 번째 원소를 가리키는 반복자
numbers.end()마지막 원소 바로 다음 위치를 나타내는 반복자
std::sort()지정된 범위를 정렬하는 알고리즘

반복자 범위의 끝은 포함하지 않는다

begin()과 end()로 지정하는 범위는 시작 위치를 포함하고 끝 위치는 포함하지 않는 범위이다. end()는 마지막 원소를 가리키지 않으므로, 해당 위치의 원소 값을 읽으려고 하면 안 된다

C++ 표준 라이브러리 전체와 STL은 같은 의미가 아니다. STL은 표준 라이브러리의 일부를 이루며, 입출력 등 표준 라이브러리의 모든 기능을 포함하는 명칭은 아니다