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 모두 72.3.1 &의 의미 구분
&는 사용되는 위치에 따라 의미가 다르다
| 코드 | 의미 |
|---|---|
int& ref = value; | 참조형을 선언하는 기호 |
&value | 변수의 주소를 구하는 연산자 |
| 예제: 복사와 참조의 차이 |
int value = 5;
int copy = value;
int& ref = value;
copy = 10; // copy만 변경
ref = 20; // value가 변경최종 값은 다음과 같다
| 변수 이름 | 값 |
|---|---|
value | 20 |
copy | 10 |
ref | 20 |
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 value | int& 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.real | 1.0 + 2.0 | 3.0 |
c3.imag | 2.0 + 3.0 | 5.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() | Dog | Dog::speak() |
animal2->speak() | Cat | Cat::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은 표준 라이브러리의 일부를 이루며, 입출력 등 표준 라이브러리의 모든 기능을 포함하는 명칭은 아니다