1. 개요
C++
C++는 C 언어를 기반으로 객체지향 프로그래밍 등의 기능을 추가하며 발전한 멀티 패러다임 프로그래밍 언어이다
멀티 패러다임이란 하나의 언어에서 여러 프로그래밍 방식을 지원한다는 뜻이다. C++에서는 절차적 프로그래밍, 객체지향 프로그래밍, 제네릭 프로그래밍 등을 활용할 수 있다
주요 설계 목표
- C 언어와의 호환성
- C++는 C 언어의 문법 체계를 기반으로 하므로 기존 C 코드를 상당 부분 활용할 수 있다
- 객체지향 개념 도입
- 캡슐화, 상속, 다형성을 지원해 쉽게 코드를 유지보수 할 수 있다
- 강력한 자료형 검사
- 자료형과 관련된 오류를 컴파일 단계에서 발견하도록 하여 실행 중 발생할 수 있는 오류를 줄이고 디버깅을 돕는다
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 버전 호출단, 반환형만 다른 함수는 오버로딩할 수 없다. 또한 호출할 함수를 하나로 결정할 수 없는 경우에는 컴파일 오류가 발생한다
2.2 디폴트 매개변수(Default Parameter)
함수의 매개변수에 기본값을 지정하는 기능이다. 함수 호출 시 일부 인수를 생략하면 지정된 기본값이 사용된다. 흔히 ‘디폴트 매개변수’라고 부르지만, 기본값 자체를 가리키는 표준 용어는 **기본 인수(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); // 가능호출할 때도 오른쪽 인수부터 생략할 수 있으며, 중간 인수만 건너뛰어 전달할 수는 없다
2.3 참조(Reference)
참조는 이미 존재하는 변수에 붙이는 또 다른 이름, 즉 별칭(alias)이다
int value = 5;
int& ref = value;여기서 ref는 별도의 정수 객체를 만드는 것이 아니라, value의 별칭으로 사용된다
value = 6; // value와 ref 모두 6
ref = 7; // value와 ref 모두 7참조 선언에 사용된 &는 주소를 구하는 연산자가 아니라 참조형을 선언하는 기호이다
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도 변경된다
특징
- 참조 매개변수는 전달된 인수의 별칭으로 사용된다
- 위 예제에서는 인수의 값을 복사하여 별도의 매개변수 객체를 만들지 않는다
- 함수 내부에서 참조 매개변수를 변경하면 원본도 변경된다
- 크기가 큰 객체를 전달할 때 불필요한 복사를 피할 수 있다
원본을 변경하지 않으면서 복사만 피하려면 const 참조를 사용한다
void print(const std::string& text);2.5 new와 delete 연산자
new와 delete는 실행 중 필요한 메모리를 할당하고 해제하는 동적 메모리 관리에 사용한다
int* ptr = new int(5);
delete ptr;new int(5): 정수형 메모리를 할당하고 값5로 초기화delete ptr: 할당한 메모리를 해제
배열을 동적으로 생성했다면 delete[]로 해제해야 한다
int* arr = new int[10];
delete[] arr;동적 메모리가 필요한 경우
- 프로그램 실행 전에는 필요한 데이터의 크기를 알 수 없을 때
- 사용자가 입력한 크기에 맞추어 메모리를 할당해야 할 때
- 객체의 수명이 특정 함수의 실행 범위를 넘어가야 할 때
C의 malloc()과 비교
| 항목 | new와 delete의 특징 |
|---|---|
| 헤더 | 위와 같은 기본적인 new·delete 표현식은 <stdlib.h>를 포함하지 않아도 사용할 수 있다. |
| 할당 크기 | 자료형을 통해 필요한 크기가 결정되므로 sizeof를 직접 지정하지 않는다. |
| 반환 자료형 | 해당 객체를 가리키는 포인터를 반환하므로 명시적인 포인터 형 변환이 필요하지 않다. |
| 값 초기화 | new int(5)처럼 초기값을 지정할 수 있다. |
| 객체 초기화 | 클래스 객체를 생성할 때 생성자를 호출하여 초기화할 수 있다. |
| 객체 소멸 | 클래스 객체를 delete로 제거하면 소멸자가 호출된 후 메모리가 해제된다. |
참고로 malloc()이 반환하는 void*를 다른 객체 포인터형으로 변환할 때, C에서는 명시적 형 변환이 필요 없지만 C++에서는 필요하다
Warning
현대 C++에서는 메모리 누수를 줄이고 자원의 수명을 안전하게 관리하기 위해, 직접
new와delete를 사용하는 것보다std::vector,std::string,std::unique_ptr등을 우선 활용하는 것이 일반적으로 권장된다
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)
{
return Complex(
real + other.real,
imag + other.imag
);
}
};Complex c1(1.0, 2.0);
Complex c2(2.0, 3.0);
Complex c3 = c1 + c2;c1 + c2를 실행하면 operator+()가 호출되어 실수부와 허수부를 각각 더한다. 따라서 c3의 실수부는 3.0, 허수부는 5.0이 된다
연산자 오버로딩은 새로운 연산자 기호를 만드는 기능이 아니다. 기존 연산자에 사용자 정의 자료형을 위한 동작을 부여하는 기능이다
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는 실제 자료형이 들어갈 자리이다. 함수를 사용하면 컴파일러는 전달된 인수에서 자료형을 추론하고, 필요한 함수 템플릿의 인스턴스를 만든다
int a = 10;
int b = 20;
swap(a, b); // T가 int인 함수 생성
double x = 1.5;
double y = 2.5;
swap(x, y); // T가 double인 함수 생성호출 후 a와 b, x와 y의 값이 각각 교환된다
특징
- 하나의 코드로 여러 자료형을 처리할 수 있다
- 중복 코드를 줄일 수 있다
- 컴파일러가 지정된 자료형에 맞는 함수나 클래스 코드를 생성한다
std::vector,std::list,std::map등 C++ 표준 라이브러리의 많은 기능이 템플릿으로 구현되어 있다
다만 모든 자료형에 무조건 사용할 수 있는 것은 아니다. 해당 템플릿에서 사용하는 연산을 자료형이 지원해야 한다. 예를 들어 위 swap()은 T temp;와 대입 연산을 사용하므로, T가 기본 초기화와 해당 대입 연산을 지원해야 한다
3. C++의 특성
C++는 객체지향 프로그래밍의 핵심 개념인 캡슐화, 상속, 다형성을 지원한다
3.1 캡슐화(Encapsulation)
캡슐화(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: 부모 클래스의 가상 함수를 재정의한다는 의도를 표시하며, 올바른 재정의인지 컴파일러가 검사하도록 한다- 가상 소멸자: 부모 클래스 포인터를 통해 자식 객체를 삭제할 때 올바르게 소멸되도록 한다
부모 클래스 포인터를 통한 호출이다
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->speak();
animal2->speak();두 포인터의 자료형은 모두 Animal*이지만, 가리키는 실제 객체는 각각 Dog와 Cat이다
animal1->speak()는 개의 동작을 실행하여 짖는 소리를 낸다animal2->speak()는 고양이의 동작을 실행하여 우는 소리를 낸다 이처럼 실행 중 실제 객체의 자료형에 따라 호출할 함수가 결정되는 것을 동적 바인딩이라고 한다
다형성과 관련된 기능
| 기능 | 설명 |
|---|---|
| 함수 오버로딩 | 같은 이름의 함수 중 매개변수 등에 맞는 함수를 선택한다. |
| 연산자 오버로딩 | 같은 연산자가 사용자 정의 자료형에 맞는 동작을 수행하게 한다. |
| 함수 오버라이딩 | 자식 클래스가 부모 클래스의 가상 함수를 재정의한다. |
| 동적 바인딩 | 실행 시간에 실제 객체의 자료형에 맞는 가상 함수를 결정한다. |
4. C++ 표준 라이브러리
C++ 표준 라이브러리는 크게 세 가지 그룹으로 구분할 수 있다
4.1 C 라이브러리
기존 C 표준 라이브러리의 기능을 C++에서도 사용할 수 있도록 제공한다. C++에서는 일반적으로 기존 C 헤더 이름에서 .h를 제거하고 앞에 c를 붙인 형태를 사용한다
| C 헤더 | C++ 헤더 |
|---|---|
<stdio.h> | <cstdio> |
<stdlib.h> | <cstdlib> |
<string.h> | <cstring> |
<time.h> | <ctime> |
<signal.h> | <csignal> |
C++ 형식의 헤더에서는 일반적인 함수와 자료형 이름을 std 네임스페이스에서 사용할 수 있다. 단, 매크로 등은 네임스페이스의 적용 대상이 아니다 |
4.2 C++ 입출력 라이브러리
콘솔과 파일의 입력·출력을 처리하는 라이브러리이다
콘솔 입출력
#include <iostream>
std::cout << "Hello";
std::cin >> value;std::cout: 표준 출력std::cin: 표준 입력std::cerr: 오류 메시지 출력
파일 입출력
#include <fstream>
std::ofstream outputFile("data.txt");
std::ifstream inputFile("data.txt");std::ofstream: 파일 출력std::ifstream: 파일 입력std::fstream: 파일 입력과 출력을 모두 수행
관련 헤더로는 <iostream>, <fstream>, <iomanip>, <istream>, <ostream> 등이 있다
4.3 C++ STL 라이브러리
STL(Standard Template Library)은 제네릭 프로그래밍을 지원하는 템플릿 기반 라이브러리이다. STL은 주로 다음 요소로 구성된다
- 컨테이너(Container): 데이터를 저장하는 자료구조
- 알고리즘(Algorithm): 검색, 정렬, 비교 등의 작업
- 반복자(Iterator): 컨테이너의 원소를 순회하는 객체
- 함수 객체(Function Object): 함수처럼 호출할 수 있는 객체
대표적인 STL 요소는 다음과 같다
| 분류 | 예 |
|---|---|
| 순차 컨테이너 | vector, list, deque |
| 연관 컨테이너 | map, set |
| 컨테이너 어댑터 | stack, queue |
| 알고리즘 | sort, find, count |
| 기타 기능 | iterator, functional, memory, numeric |
#include <vector>
#include <algorithm>
std::vector<int> numbers = {3, 1, 2};
std::sort(numbers.begin(), numbers.end());위 코드의 동작은 다음과 같다.
std::vector에 정수3,1,2를 저장한다begin()과end()로 정렬할 범위를 지정한다std::sort()로 원소를 오름차순 정렬한다- 정렬 후 원소의 순서는
1,2,3이 된다