C++는 운영체제, 게임 엔진, 금융 시스템, 고성능 컴퓨팅 등 성능이 시스템의 가치를 결정하는 거의 모든 분야에서 핵심적인 역할을 수행하는 프로그래밍 언어이다.1 C++ 학습은 단순히 하나의 언어를 배우는 것을 넘어, 컴퓨터 시스템의 근본적인 동작 원리와 현대 소프트웨어 공학의 정수를 이해하는 과정이다.
C++의 수십 년에 걸친 생명력과 영향력의 본질은 단순히 ‘빠른 속도’라는 단어로만 설명되지 않는다. 그 핵심은 C언어로부터 물려받은 하드웨어에 대한 직접적이고 정밀한 ‘통제력’을 유지하면서, 시대의 요구에 맞춰 객체 지향, 제네릭, 함수형 프로그래밍과 같은 고수준의 추상화 패러다임을 성공적으로 흡수해 온 ‘진화하는 능력’에 있다.2 1985년 처음 등장한 이래로 , C++는 C++11, C++14, C++17, C++20 등 지속적인 표준 개정을 통해 끊임없이 발전하며 현대 소프트웨어의 복잡한 요구사항을 해결하는 강력한 도구로 자리매김했다.2
따라서 C++를 배운다는 것은 과거의 유물을 탐구하는 것이 아니라, 저수준의 메모리 관리부터 고수준의 설계 패턴까지 아우르는 폭넓은 시야를 갖추는 것이다. 이 자습서는 C++의 문법적 지식을 넘어, 그 안에 담긴 설계 철학과 원리를 깊이 있게 전달함으로써 학습자가 C++를 통해 강력하고 효율적이며 우아한 소프트웨어를 구축할 수 있는 능력을 갖추도록 돕는 것을 목표로 한다.
C++의 역사를 이해하는 것은 언어의 설계 목표와 핵심 철학을 파악하는 첫걸음이다. C++는 1979년, 덴마크의 컴퓨터 과학자 비야네 스트롭스트룹(Bjarne Stroustrup)이 AT&T 벨 연구소에서 ‘C with Classes’라는 이름의 프로젝트로 시작했다. 당시 C언어는 시스템 프로그래밍에 있어 최고의 효율성을 자랑했지만, 소프트웨어의 규모가 커짐에 따라 코드 관리의 어려움이 대두되었다. 스트롭스트룹의 목표는 C언어의 강력한 성능과 저수준 제어 능력을 그대로 유지하면서, 대규모 소프트웨어 개발을 용이하게 하는 객체 지향 프로그래밍(Object-Oriented Programming, OOP) 개념을 도입하는 것이었다.4
이러한 배경 속에서 C++는 1985년에 공식적으로 발표되었으며 2, 다음과 같은 핵심 설계 목표를 가지고 발전해왔다.
이러한 철학을 바탕으로 C++는 1998년 첫 공식 국제 표준인 C++98을 시작으로 C++03, C++11, C++14, C++17, 그리고 최신 표준인 C++20에 이르기까지 주기적인 개정을 통해 현대 프로그래밍 환경의 요구를 지속적으로 반영하고 있다.2
C++ 프로그래밍을 시작하기 위해서는 코드를 작성할 편집기(Editor)와 작성된 코드를 기계어로 번역할 컴파일러(Compiler), 그리고 오류를 잡기 위한 디버거(Debugger)가 필요하다. 개발 환경을 구축하는 방법은 크게 두 가지로 나눌 수 있다.
개발 환경을 선택하는 것은 단순히 도구를 고르는 것을 넘어, 개발 워크플로우에 대한 두 가지 다른 철학 중 하나를 선택하는 것을 의미한다. Visual Studio와 같은 통합 개발 환경(IDE)은 디버거, 컴파일러, 프로젝트 관리 시스템이 긴밀하게 통합된 ‘올인원(All-in-One)’ 솔루션으로, 특히 대규모 Windows 기반 프로젝트에서 생산성을 극대화한다.11 반면, VS Code와 MinGW를 조합하는 방식은 사용자가 컴파일러, 링커, 디버거 등 각 요소를 직접 구성하는 ‘모듈화된’ 접근법으로, 경량성과 플랫폼 독립성을 중시하는 현대 개발 트렌드를 반영한다.14 초심자에게 후자의 방식은 빌드 프로세스에 대한 더 깊은 이해를 강제하는 효과적인 학습 도구가 될 수 있다.
Visual Studio는 Microsoft가 개발한 강력한 통합 개발 환경(IDE)으로, C++ 개발에 필요한 모든 도구를 한 번에 설치하고 관리할 수 있어 편리하다. 개인 개발자, 학생, 오픈소스 기여자는 무료 버전인 Community Edition을 사용할 수 있다.11
Visual Studio Code(VS Code)는 가볍고 확장성이 뛰어난 코드 편집기로, C++ 컴파일러를 별도로 설치하고 연동하여 개발 환경을 구축할 수 있다. Windows 환경에서는 MinGW를 통해 GCC(GNU Compiler Collection) 컴파일러를 사용할 수 있다.
mingw64 폴더를 C:\와 같이 경로에 한글이나 공백이 없는 곳으로 이동시킨다.14C:\mingw64\bin과 같이 g++.exe 파일이 위치한 경로를 복사한다.16Path를 편집한다.14g++ --version을 입력했을 때 버전 정보가 출력되면 성공적으로 설정된 것이다.16C/C++ Extension Pack을 검색하여 설치한다. 이 확장팩은 코드 자동 완성(IntelliSense), 디버깅, 코드 탐색 등 필수적인 기능을 제공한다.14main.cpp)을 생성한다..vscode 폴더 안에 JSON 형식의 설정 파일을 사용한다.
c_cpp_properties.json: 컴파일러 경로와 IntelliSense 설정을 담당한다.tasks.json: 빌드 명령(컴파일)을 정의한다.launch.json: 디버거 실행 방법을 정의한다.C/C++: g++.exe 활성 파일 빌드를 선택하면 tasks.json 파일이 자동으로 생성된다. 디버깅을 처음 시작(F5 키)할 때 launch.json 파일도 유사한 방식으로 생성할 수 있다.14개발 환경 구축이 완료되었다면, 가장 기본적인 C++ 프로그램을 작성하고 실행하여 모든 것이 정상적으로 동작하는지 확인한다.
#include <iostream>
int main()
{
std::cout << "Hello, World!" << std::endl;
return 0;
}
이 간단한 코드는 C++ 프로그램의 핵심적인 구조를 보여준다. 각 구성 요소를 자세히 살펴보자.
#include <iostream>: 전처리기 지시문(Preprocessor Directive)이다. 컴파일이 시작되기 전에 #include로 지정된 파일을 소스 코드에 포함시키도록 전처리기에 지시한다. <iostream>은 C++ 표준 라이브러리의 일부로, 콘솔 입출력(Input/Output Stream)에 필요한 기능들을 담고 있다.18int main(): 모든 C++ 프로그램의 시작점(Entry Point)이 되는 특별한 함수이다. 운영체제는 프로그램을 실행할 때 main 함수를 가장 먼저 호출한다. int는 이 함수가 정수(integer) 값을 반환함을 의미한다.19{... }: 중괄호는 함수의 시작과 끝을 나타내는 코드 블록(Code Block)이다.std::cout: 표준 출력 스트림(Standard Output Stream) 객체이다. 기본적으로 콘솔 화면에 연결되어 있다. std::는 cout이 std라는 이름 공간(namespace)에 속해 있음을 명시하는 것이다.18 이름 공간은 이름 충돌을 방지하기 위해 관련 기능들을 그룹화하는 역할을 한다.<<: 스트림 삽입 연산자(Stream Insertion Operator)이다. 오른쪽에 있는 데이터를 왼쪽의 스트림 객체(여기서는 std::cout)로 보낸다(삽입한다).22"Hello, World!": 출력할 문자열 리터럴(String Literal)이다.std::endl: End Line의 약자로, 출력 스트림에 개행 문자(\n)를 삽입하고 출력 버퍼를 비우는(flush) 역할을 하는 조작자(Manipulator)이다.22return 0;: main 함수의 실행을 종료하고 운영체제에 값을 반환한다. 관례적으로 0은 프로그램이 성공적으로 종료되었음을 의미한다.19;: 세미콜론은 C++에서 하나의 문장(Statement)이 끝났음을 알리는 기호이다.이 코드를 컴파일하고 실행하면 콘솔 화면에 “Hello, World!”가 출력될 것이다.
프로그래밍에서 데이터는 변수(variable)라는 이름이 붙은 메모리 공간에 저장된다.24 C++는 정적 타입 언어(statically-typed language)이므로, 변수를 사용하기 전에 반드시 어떤 종류의 데이터를 저장할 것인지 타입을 명시하여 선언해야 한다.
변수는 타입 변수명; 형식으로 선언한다. 선언과 동시에 값을 할당하는 것을 초기화(initialization)라고 한다.26
int age; // 정수형 변수 age 선언
age = 30; // 값 할당
double weight = 75.5; // 실수형 변수 weight 선언과 동시에 초기화
// C++11 이후 권장되는 유니폼 초기화(Uniform Initialization)
int score {100};
변수의 이름을 정할 때는 몇 가지 규칙이 있다. 이름은 영문자, 숫자, 밑줄(_)로 구성할 수 있지만, 숫자로 시작할 수 없으며 C++ 키워드(예: int, if, class)는 사용할 수 없다.24
C++는 다양한 종류의 데이터를 표현하기 위해 여러 내장 데이터 타입을 제공한다. 이들의 크기와 표현 범위는 시스템 아키텍처나 컴파일러에 따라 다를 수 있으나, 일반적으로 통용되는 기준은 다음과 같다.25
| 데이터 타입 | 크기 (Bytes) | 값의 범위 | 설명 |
|---|---|---|---|
bool |
1 | true, false |
논리적 참과 거짓을 표현한다.28 |
char |
1 | -128 ~ 127 | 단일 문자를 저장한다 (ASCII 코드).27 |
unsigned char |
1 | 0 ~ 255 | 부호 없는 문자를 표현한다. |
short |
2 | -32,768 ~ 32,767 | 작은 범위의 정수를 저장한다. |
int |
4 | -2,147,483,648 ~ 2,147,483,647 | 가장 일반적으로 사용되는 정수 타입이다.28 |
unsigned int |
4 | 0 ~ 4,294,967,295 | 0과 양의 정수만을 표현한다.26 |
long long |
8 | 약 ±9.22e18 | 매우 큰 범위의 정수를 저장한다.27 |
float |
4 | 약 ±3.4e±38 (유효숫자 7자리) | 단정밀도 부동소수점 수를 저장한다.26 |
double |
8 | 약 ±1.7e±308 (유효숫자 15자리) | 배정밀도 부동소수점 수를 저장한다.26 |
sizeof 연산자를 사용하면 특정 데이터 타입이나 변수가 메모리에서 차지하는 크기를 바이트 단위로 확인할 수 있다.29
하나의 데이터 타입을 다른 타입으로 변환하는 것을 형 변환이라고 한다.
double d = 5;라는 코드에서 정수 5는 double 타입인 5.0으로 자동 변환된다. 이 과정에서 데이터 손실이 발생할 수 있으므로 주의해야 한다 (예: int i = 3.14; -> i는 3이 됨).24static_cast를 사용하는 것이 권장된다.29double pi = 3.14159;
int integer_pi = static_cast<int>(pi); // 명시적 형 변환, integer_pi는 3이 된다.
연산자는 변수나 값에 대한 연산을 수행하는 기호이다. C++는 풍부한 연산자를 제공한다.
+ (덧셈), - (뺄셈), * (곱셈), / (나눗셈), % (나머지).24== (같음), != (다름), > (큼), < (작음), >= (크거나 같음), <= (작거나 같음). 연산 결과는 bool 타입(true 또는 false)이다.30&& (논리 AND), || (논리 OR), ! (논리 NOT). bool 값을 피연산자로 사용한다.30= (대입). a += 5; (a = a + 5;와 동일)와 같은 복합 대입 연산자도 있다.24++ (1 증가), -- (1 감소). 변수 앞에 붙으면 전위(prefix), 뒤에 붙으면 후위(postfix) 연산자가 된다. 전위는 값을 먼저 증감시킨 후 연산을 수행하고, 후위는 연산을 먼저 수행한 후 값을 증감시킨다.24&, |, ^, ~, <<, >>. 데이터를 비트 수준에서 조작할 때 사용한다.조건? 값1 : 값2. 조건이 참이면 값1을, 거짓이면 값2를 반환한다.31여러 연산자가 하나의 수식에 사용될 때는 연산자 우선순위와 결합 규칙에 따라 계산 순서가 결정된다. 예를 들어, 곱셈과 나눗셈은 덧셈과 뺄셈보다 우선순위가 높다. 복잡한 수식에서는 괄호 ()를 사용하여 계산 순서를 명확히 지정하는 것이 코드의 가독성과 안정성을 위해 바람직하다.24
프로그램이 사용자와 상호작용하기 위한 가장 기본적인 방법은 표준 입출력 스트림을 사용하는 것이다.
#include <iostream>: 표준 입출력 기능을 사용하기 위해 반드시 포함해야 하는 헤더 파일이다.18
표준 출력 std::cout:
std::cout 객체와 스트림 삽입 연산자 «를 사용하여 콘솔에 데이터를 출력한다. « 연산자는 연쇄적으로 사용하여 여러 데이터를 한 번에 출력할 수 있다.18 C언어의 printf와 달리 출력할 데이터의 타입에 따라 서식 지정자(%d, %f 등)를 바꿀 필요가 없어 편리하고 타입에 안전하다.18
int age = 30;
std::string name = "Bjarne";
std::cout << "Name: " << name << ", Age: " << age << std::endl;
표준 입력 std::cin:
std::cin 객체와 스트림 추출 연산자 »를 사용하여 키보드로부터 입력을 받는다. » 연산자는 기본적으로 공백(스페이스, 탭, 개행)을 구분자로 인식하여 데이터를 분리한다.18
int number;
std::cout << "Enter a number: ";
std::cin >> number;
std::cout << "You entered: " << number << std::endl;
이름 공간(Namespace)과 using 지시문:
cout, cin, endl 등은 모두 std라는 이름 공간에 속해 있어, 사용할 때마다 std:: 접두사를 붙여야 한다. using namespace std; 지시문을 소스 코드 상단에 선언하면 이 접두사를 생략할 수 있다.18 하지만 이 방식은 대규모 프로젝트에서 이름 충돌을 일으킬 수 있으므로, using std::cout;, using std::cin;과 같이 사용할 기능만 개별적으로 선언하거나, 함수 내에서만 using 지시문을 사용하는 것이 더 안전한 방법으로 권장된다.35
제어 흐름 구문은 프로그램의 실행 경로를 조건에 따라 변경하거나 특정 코드 블록을 반복 실행하게 한다.
if, else if, else:
가장 기본적인 조건문으로, if 뒤의 조건식이 true이면 해당 코드 블록을 실행한다. if 조건이 false일 경우, 이어지는 else if의 조건을 검사하거나, 모든 조건이 false일 경우 else 블록을 실행한다.37
int score = 85;
if (score >= 90) {
std::cout << "A" << std::endl;
} else if (score >= 80) {
std::cout << "B" << std::endl; // 이 블록이 실행된다.
} else {
std::cout << "C" << std::endl;
}
중첩된 if 문을 사용할 때 else는 가장 가까운 if에 연결되므로, 의도치 않은 동작을 피하기 위해 중괄호 {}를 명확하게 사용하는 습관이 중요하다.38
switch-case:
하나의 변수 값을 여러 상수 값과 비교하여 실행 경로를 결정할 때 유용하다. 비교 대상은 정수, 문자, 열거형(enum) 타입이어야 한다.39
case: 비교할 상수 값을 지정한다.break: 해당 case의 실행을 마치고 switch 문을 빠져나간다. break가 없으면 다음 case의 코드가 연이어 실행되는 ‘fall-through’ 현상이 발생하므로 주의해야 한다.37default: 어떤 case와도 일치하지 않을 때 실행된다.39int choice = 2;
switch (choice) {
case 1:
std::cout << "Menu 1 selected." << std::endl;
break;
case 2:
std::cout << "Menu 2 selected." << std::endl; // 이 블록이 실행된다.
break;
default:
std::cout << "Invalid choice." << std::endl;
break;
}
조건의 수가 많을 경우, switch문은 내부적으로 점프 테이블을 사용하여 if-else 체인보다 더 효율적으로 동작할 수 있다.39
for 루프:
for (초기식; 조건식; 증감식)의 구조를 가지며, 주로 정해진 횟수만큼 코드를 반복할 때 사용된다. 초기식은 루프 시작 전 한 번만 실행되고, 매 반복마다 조건식을 검사하며, 반복이 끝날 때마다 증감식이 실행된다.43
int sum = 0;
for (int i = 1; i <= 10; ++i) {
sum += i; // 1부터 10까지의 합을 계산한다.
}
범위 기반 for 루프 (Range-based for loop):
C++11부터 도입된 문법으로, 배열이나 STL 컨테이너의 모든 원소를 매우 간결하게 순회할 수 있다. for (원소타입 변수 : 컨테이너) 형태로 사용한다.46
int numbers = {1, 2, 3, 4, 5};
for (int num : numbers) {
std::cout << num << " ";
}
while 루프:
while (조건식)의 구조를 가지며, 조건식이 true인 동안 코드 블록을 계속해서 반복한다. 반복 횟수가 명확하지 않고 특정 조건이 만족될 때까지 반복해야 할 경우에 유용하다.43
int n = 5;
while (n > 0) {
std::cout << n << " ";
--n;
} // 출력: 5 4 3 2 1
do-while 루프:
while 루프와 유사하지만, 조건을 나중에 검사하므로 코드 블록이 최소 한 번은 반드시 실행된다. do {… } while (조건식); 형태로 사용하며, while 뒤에 세미콜론이 붙는다는 점에 유의해야 한다.43
break와 continue:
break: 반복문이나 switch 문을 즉시 탈출한다.continue: 현재 반복의 나머지 부분을 건너뛰고, 다음 반복을 시작한다.함수(Function)는 특정 작업을 수행하는 코드의 집합을 하나의 단위로 묶은 것이다. 함수를 사용하면 코드의 중복을 피하고, 프로그램을 논리적인 단위로 분해하여 재사용성과 유지보수성을 높일 수 있다.21
C++ 함수는 네 가지 주요 요소로 구성된다.21
void를 사용한다.() 안에 타입 변수명 형태로 선언한다.{} 안에 함수의 실제 동작을 정의하는 코드 블록.// 함수의 정의(Definition)
int add(int a, int b) // 반환 타입: int, 함수 이름: add, 매개변수: int a, int b
{
// 함수 몸체
return a + b; // return 문을 통해 결과값 반환
}
함수 선언 (Declaration) 또는 프로토타입 (Prototype): 컴파일러에게 함수의 이름, 반환 타입, 매개변수 목록을 미리 알려주는 것이다. main 함수보다 뒤에 함수를 정의하려면, main 함수 앞에 반드시 선언이 있어야 한다. 이는 컴파일러가 main 함수에서 해당 함수를 호출하는 코드를 만났을 때, 그 함수가 어떤 형태인지 알 수 있게 하기 위함이다.49
#include <iostream>
int add(int a, int b); // 함수 선언 (프로토타입)
int main() {
int result = add(5, 3); // 함수 호출
std::cout << "Result: " << result << std::endl;
return 0;
}
// 함수 정의
int add(int a, int b) {
return a + b;
}
함수 정의 (Definition): 함수의 실제 코드를 구현하는 부분이다.49
함수에 인자(argument)를 전달하는 방식은 크게 세 가지로 나눌 수 있으며, 이는 함수가 원본 데이터를 어떻게 다룰지를 결정하는 중요한 개념이다.
값에 의한 호출 (Call-by-Value):
가장 기본적인 방식으로, 함수를 호출할 때 인자의 값이 복사되어 함수의 매개변수에 전달된다. 따라서 함수 내에서 매개변수의 값을 변경하더라도, 함수 호출에 사용된 원본 변수에는 아무런 영향을 미치지 않는다.51 이는 데이터의 안정성을 보장하지만, 구조체나 클래스처럼 크기가 큰 객체를 전달할 경우 복사로 인한 성능 저하가 발생할 수 있다.
void increment(int x) {
x++; // x의 복사본이 증가. 원본은 그대로.
}
int num = 5;
increment(num); // num은 여전히 5
주소에 의한 호출 (Call-by-Address / Call-by-Pointer):
인자의 메모리 주소를 포인터(pointer) 매개변수로 전달하는 방식이다. 함수는 전달받은 주소를 통해 원본 변수의 메모리 공간에 직접 접근할 수 있다. 따라서 역참조 연산자(*)를 사용하여 원본 변수의 값을 읽거나 수정하는 것이 가능하다.51
void increment(int* p) {
(*p)++; // p가 가리키는 주소의 값을 증가.
}
int num = 5;
increment(&num); // num의 주소를 전달. num은 6이 된다.
참조에 의한 호출 (Call-by-Reference):
C++에서 추가된 방식으로, 인자의 별명(alias) 역할을 하는 참조자(reference)를 매개변수로 사용한다. 함수 내에서 참조자 매개변수를 조작하면 원본 변수가 직접 변경된다. 포인터와 달리 주소 연산자(&)나 역참조 연산자(*) 없이 일반 변수처럼 사용할 수 있어 코드가 더 간결하고 직관적이다.52
void increment(int& r) {
r++; // r이 참조하는 원본 변수의 값을 증가.
}
int num = 5;
increment(num); // num을 참조로 전달. num은 6이 된다.
함수 오버로딩은 C++의 다형성(polymorphism)을 구현하는 방법 중 하나로, 같은 이름의 함수를 여러 개 정의할 수 있게 해주는 기능이다. 컴파일러는 함수의 이름뿐만 아니라 매개변수의 개수나 타입을 종합적으로 고려하여 호출할 함수를 결정한다.54
#include <iostream>
void print(int i) {
std::cout << "Integer: " << i << std::endl;
}
void print(double d) {
std::cout << "Double: " << d << std::endl;
}
void print(const char* s) {
std::cout << "String: " << s << std::endl;
}
int main() {
print(10); // print(int) 호출
print(3.14); // print(double) 호출
print("Hello"); // print(const char*) 호출
return 0;
}
주의사항:
배열은 동일한 데이터 타입의 원소들을 연속된 메모리 공간에 저장하는 자료구조이다.
선언: 타입 배열이름[크기]; 형태로 선언한다. C++의 고정 배열(fixed array)에서 크기는 반드시 컴파일 시점에 결정되는 상수여야 한다.60
int scores; // 5개의 int를 저장할 수 있는 배열
초기화: 선언과 동시에 중괄호 {}를 사용하여 초기화할 수 있다.60
int numbers = {10, 20, 30, 40, 50};
int fib = {0, 1, 1, 2, 3, 5}; // 크기를 생략하면 초기값 개수로 자동 결정
int zeros = {0}; // 모든 원소를 0으로 초기화
일부만 초기화하면 나머지 원소들은 0으로 자동 초기화된다.61
원소 접근: 배열의 각 원소는 인덱스(index)를 통해 접근한다. C++에서 인덱스는 항상 0부터 시작하여 크기-1까지이다.60
numbers = 5; // 첫 번째 원소에 5를 할당
std::cout << numbers; // 세 번째 원소인 30을 출력
주의: C++는 배열의 경계를 검사하지 않는다. numbers와 같이 유효 범위를 벗어난 인덱스에 접근하면 예측할 수 없는 동작(undefined behavior)을 일으켜 프로그램에 심각한 오류를 유발할 수 있다.60
구조체는 서로 다른 타입의 변수들을 하나의 논리적 단위로 묶어 새로운 사용자 정의 데이터 타입을 만드는 기능이다. 예를 들어 ‘학생’이라는 데이터를 표현하기 위해 이름(string), 학번(int), 학점(double)을 하나의 구조체로 묶을 수 있다.65
#include <string>
#include <iostream>
struct Student {
std::string name;
int studentID;
double gpa;
};
C++의 struct는 단순한 데이터 묶음 이상의 의미를 지닌다. 이는 C언어와의 호환성을 유지하면서 객체 지향 프로그래밍으로 나아가는 중요한 징검다리 역할을 한다. C++에서 struct는 멤버 함수를 가질 수 있으며, 유일한 차이점은 기본 접근 지정자가 struct는 public이고 class는 private라는 점이다.68 이러한 설계는 C의 절차적 패러다임에서 C++의 객체 지향 패러다임으로 자연스럽게 개념을 확장하려는 의도를 보여준다.
변수 선언 및 멤버 접근:
구조체 타입으로 변수를 선언한 뒤, 멤버 선택 연산자 . (점)를 사용하여 각 멤버에 접근한다.65
Student s1;
s1.name = "Grace Hopper";
s1.studentID = 19061209;
s1.gpa = 4.5;
초기화: 배열과 유사하게 중괄호 {}를 사용하여 선언과 동시에 초기화할 수 있다.66
Student s2 = {"Alan Turing", 19120623, 4.4};
구조체와 포인터:
구조체를 가리키는 포인터를 사용할 때는 화살표 연산자 ->를 사용하여 멤버에 접근한다. ptr->member는 (*ptr).member와 완전히 동일한 표현이다.70
Student* ptr_s = &s2;
std::cout << "Name: " << ptr_s->name << std::endl; // "Alan Turing" 출력
포인터는 C++의 가장 강력하면서도 복잡한 기능 중 하나로, 변수의 메모리 주소를 직접 저장하고 조작할 수 있게 해준다. 이를 통해 매우 효율적인 프로그램 작성이 가능하지만, 동시에 오류가 발생하기 쉬워 세심한 주의가 필요하다.
개념: 포인터는 다른 변수가 저장된 메모리의 위치(주소)를 값으로 갖는 특별한 변수이다.72
주소 연산자 (&): 변수 앞에 붙여 해당 변수의 메모리 주소를 얻는다.73
역참조 연산자 (\*): 포인터 변수 앞에 붙여, 해당 포인터가 가리키는 메모리 주소에 저장된 실제 값에 접근한다. 이를 통해 값을 읽거나 수정할 수 있다.73
선언: 타입* 포인터이름; 형태로 선언한다. *는 해당 변수가 포인터임을 나타낸다.73
int var = 10;
int* ptr; // int형 변수를 가리킬 포인터 ptr 선언
ptr = &var; // ptr에 var의 메모리 주소를 할당
std::cout << "Value of var: " << var << std::endl; // 출력: 10
std::cout << "Address of var: " << &var << std::endl; // var의 주소 출력
std::cout << "Value of ptr: " << ptr << std::endl; // ptr의 값(var의 주소) 출력
std::cout << "Value pointed by ptr: " << *ptr << std::endl; // 출력: 10
*ptr = 20; // ptr이 가리키는 곳의 값을 20으로 변경
std::cout << "New value of var: " << var << std::endl; // 출력: 20
NULL 포인터: 아무것도 가리키지 않는 상태를 나타내는 포인터이다. C++11부터는 0이나 NULL 매크로 대신 타입에 안전한 nullptr 키워드를 사용하는 것이 강력히 권장된다. 포인터를 사용하기 전에는 항상 nullptr인지 확인하는 습관이 중요하다.
참조자는 C++에 도입된 개념으로, 이미 존재하는 변수에 대한 또 다른 이름(별명)을 부여하는 기능이다.
개념: 참조자는 선언될 때 참조하는 변수와 동일한 메모리 공간을 공유한다. 따라서 참조자를 수정하면 원본 변수도 함께 수정된다.53
선언: 타입& 참조자이름 = 원본변수; 형태로 선언하며, 선언과 동시에 반드시 초기화해야 한다.76
int original = 100;
int& ref = original; // ref는 original의 별명
std::cout << "Original: " << original << ", Ref: " << ref << std::endl; // 출력: 100, 100
ref = 200; // 참조자를 통해 값을 변경
std::cout << "Original: " << original << ", Ref: " << ref << std::endl; // 출력: 200, 200
참조자는 포인터의 강력한 기능(원본 데이터에 대한 직접 접근)은 유지하면서, 포인터 사용 시 발생할 수 있는 가장 흔한 오류의 원인(NULL 역참조, 엉뚱한 주소 가리키기)을 문법적으로 차단하기 위해 고안되었다. 이는 성능을 위해 저수준 제어를 허용하되, 더 안전하고 추상화된 도구를 제공하여 개발자의 실수를 줄이려는 C++의 실용적인 설계 철학을 반영한다. “사용할 수 있다면 참조자를, 어쩔 수 없다면 포인터를 써라” 77는 이러한 철학을 잘 보여주는 지침이다.
| 특징 | 포인터 (Pointer) | 참조자 (Reference) |
|---|---|---|
| NULL 값 | nullptr로 초기화 가능 (아무것도 가리키지 않음).75 |
NULL을 허용하지 않음. 반드시 유효한 변수를 참조해야 함.75 |
| 재할당 | 다른 변수의 주소를 가리키도록 재할당 가능.78 | 한번 초기화되면 다른 변수를 참조하도록 변경할 수 없음.76 |
| 초기화 | 선언 후 나중에 주소를 할당할 수 있음. | 선언과 동시에 반드시 초기화해야 함.76 |
| 문법 | 주소 할당 시 &, 값 접근 시 * 또는 -> 연산자 필요.77 |
일반 변수와 동일한 문법으로 사용. 별도의 연산자 불필요.53 |
이러한 차이점 때문에, 함수에 매개변수를 전달하여 원본을 수정하고자 할 때, NULL이 전달될 가능성이 없다면 포인터보다 참조자를 사용하는 것이 더 안전하고 코드가 간결해진다.
객체 지향 프로그래밍(Object-Oriented Programming)은 실제 세계의 개념들을 객체(Object)라는 단위로 모델링하여 프로그래밍하는 패러다임이다. C++는 C언어에 객체 지향 개념을 도입하여 탄생했으며, OOP는 C++의 핵심적인 정체성이다.
struct와 유사하지만, 기본 접근 지정자가 private이라는 점에서 차이가 있다.68this 포인터: 모든 멤버 함수 내에서 암묵적으로 사용할 수 있는 포인터로, 현재 객체 자신을 가리킨다. 멤버 변수와 매개변수의 이름이 같을 때 이를 구분하거나, 객체 자신의 주소를 반환할 때 사용된다.82class Car {
public: // 접근 지정자
// 멤버 변수
std::string color;
int speed;
// 멤버 함수
void accelerate() {
this->speed += 10; // this 포인터를 사용하여 멤버 변수 speed에 접근
}
void brake() {
speed -= 10;
}
};
int main() {
Car myCar; // Car 클래스의 객체(인스턴스) myCar 생성
myCar.color = "blue";
myCar.speed = 60;
myCar.accelerate(); // myCar 객체의 accelerate 멤버 함수 호출
return 0;
}
캡슐화와 정보 은닉은 객체 지향 프로그래밍의 핵심 원리 중 하나로, 객체의 내부 데이터를 보호하고 프로그램의 안정성을 높이는 기법이다.
public: 클래스 외부 어디에서든 접근할 수 있다. 객체의 공개 인터페이스를 구성한다.private: 해당 클래스의 멤버 함수 내에서만 접근할 수 있다. 외부 접근이 차단되어 정보 은닉을 실현한다.protected: 해당 클래스와 이 클래스를 상속받은 파생 클래스 내에서만 접근할 수 있다.88class BankAccount {
private: // 정보 은닉
double balance;
public: // 공개 인터페이스
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
double getBalance() {
return balance;
}
};
위 예제에서 balance는 private으로 선언되어 외부에서 직접 접근할 수 없다. myAccount.balance = -10000;과 같은 위험한 조작이 불가능하다. 잔액을 변경하거나 조회하려면 반드시 public으로 공개된 deposit과 getBalance 함수를 통해서만 가능하다.
생성자와 소멸자는 객체의 생명 주기(life cycle)를 관리하는 특별한 멤버 함수이다.
delete 연산자로 동적 할당된 객체를 해제할 때) 자동으로 호출되어, 생성자에서 할당했던 자원을 해제하는 역할을 한다. 메모리 누수(memory leak)를 방지하는 데 필수적이다.92~(틸드)를 붙이며, 매개변수와 반환 타입이 없다. 클래스당 오직 하나만 존재할 수 있다.92class MyArray {
private:
int* arr;
int size;
public:
// 생성자: 동적 메모리 할당
MyArray(int s) {
size = s;
arr = new int[size];
std::cout << "Array created." << std::endl;
}
// 소멸자: 동적 메모리 해제
~MyArray() {
delete arr;
std::cout << "Array destroyed." << std::endl;
}
};
상속은 기존 클래스(기반 클래스 또는 부모 클래스)의 멤버(변수, 함수)를 물려받아 새로운 클래스(파생 클래스 또는 자식 클래스)를 만드는 기능이다. 코드의 재사용성을 높이고 클래스 간의 계층 구조를 형성할 수 있게 한다.
class 파생클래스명 : 접근지정자 기반클래스명 {... };public 상속: 기반 클래스의 public 멤버는 파생 클래스에서도 public으로, protected 멤버는 protected로 유지된다. “is-a” 관계(예: ‘학생’은 ‘사람’이다)를 표현할 때 주로 사용되는 가장 일반적인 상속 방식이다.protected 상속: 기반 클래스의 public과 protected 멤버가 모두 파생 클래스에서 protected 멤버가 된다.private 상속: 기반 클래스의 public과 protected 멤버가 모두 파생 클래스에서 private 멤버가 된다.private 멤버는 파생 클래스에서 직접 접근할 수 없다.98// 기반 클래스
class Person {
public:
std::string name;
void introduce() { std::cout << "My name is " << name << std::endl; }
};
// 파생 클래스
class Student : public Person { // Person을 public 상속
public:
int studentID;
void study() { std::cout << "I am studying." << std::endl; }
};
int main() {
Student s;
s.name = "John"; // 기반 클래스의 public 멤버 사용
s.studentID = 12345; // 파생 클래스의 멤버 사용
s.introduce(); // 기반 클래스의 public 멤버 함수 사용
s.study(); // 파생 클래스의 멤버 함수 사용
return 0;
}
다형성(Polymorphism)은 ‘여러 가지 형태를 가짐’이라는 의미로, 객체 지향 프로그래밍에서 하나의 인터페이스(예: 함수 호출)가 서로 다른 타입의 객체에 대해 각기 다른 방식으로 동작하는 능력을 말한다.102
파생 클래스에서 기반 클래스로부터 물려받은 멤버 함수를 동일한 이름, 동일한 매개변수, 동일한 반환 타입으로 재정의하는 것을 오버라이딩이라고 한다.103
C++의 다형성은 주로 가상 함수를 통해 실현된다. Java와 같은 언어에서는 모든 메서드가 기본적으로 가상(동적 바인딩)이지만, C++는 기본적으로 정적 바인딩(Static Binding)을 사용한다. 즉, 컴파일 시점에 호출될 함수가 결정된다. 이는 “지불하지 않는 기능에 대해서는 비용을 치르지 않는다(You don’t pay for what you don’t use)”는 C++의 핵심 철학을 보여준다. 가상 함수 테이블과 간접 호출로 인한 미세한 성능 저하조차 다형성이 필요 없는 경우에는 허용하지 않겠다는 것이다.4
virtual 키워드를 붙인다. 이렇게 선언된 함수를 가상 함수라고 한다. virtual 함수가 호출될 때, 컴파일러는 실행 시간에 포인터가 실제로 가리키고 있는 객체의 타입을 확인하여 해당 타입에 맞게 오버라이딩된 함수를 호출한다. 이것이 동적 바인딩이다.103class Animal {
public:
virtual void speak() { std::cout << "Animal speaks" << std::endl; }
virtual ~Animal() {} // 상속 계층에서는 소멸자를 가상으로 선언하는 것이 안전하다.
};
class Dog : public Animal {
public:
void speak() override { // 'override' 키워드는 오버라이딩을 명시 (C++11)
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
};
void doSpeak(Animal* animal) {
animal->speak(); // 동적 바인딩 발생
}
int main() {
Animal* pDog = new Dog();
Animal* pCat = new Cat();
doSpeak(pDog); // 출력: Woof!
doSpeak(pCat); // 출력: Meow!
delete pDog;
delete pCat;
return 0;
}
virtual void func() = 0;과 같이 함수 몸체 없이 선언된 가상 함수를 순수 가상 함수(Pure Virtual Function)라고 한다. 순수 가상 함수를 하나 이상 포함하는 클래스는 추상 클래스(Abstract Class)가 되어 객체를 직접 생성할 수 없다. 추상 클래스를 상속받는 파생 클래스는 반드시 모든 순수 가상 함수를 오버라이딩해야만 객체를 생성할 수 있다. 이는 파생 클래스에게 특정 인터페이스의 구현을 강제하는 역할을 한다.101템플릿은 데이터 타입에 의존하지 않는 범용(Generic) 코드를 작성하게 해주는 C++의 강력한 기능이다. 동일한 로직을 다른 데이터 타입에 대해 반복적으로 작성할 필요 없이, 하나의 템플릿으로 여러 타입에서 동작하는 함수나 클래스를 만들 수 있다.
함수 템플릿 (Function Template):
특정 타입 대신 임의의 타입을 나타내는 템플릿 매개변수(예: T)를 사용하여 함수를 정의한다. template
template <typename T>
T getMax(T a, T b) {
return (a > b)? a : b;
}
int main() {
std::cout << getMax(5, 10) << std::endl; // T가 int로 인스턴스화
std::cout << getMax(3.14, 2.71) << std::endl; // T가 double로 인스턴스화
}
클래스 템플릿 (Class Template):
임의의 타입을 멤버 변수나 멤버 함수의 타입으로 사용하는 범용 클래스를 정의한다.109 클래스 템플릿으로 객체를 생성할 때는 꺾쇠괄호 <> 안에 사용할 구체적인 타입을 명시해야 한다.111
template <typename T>
class Box {
private:
T content;
public:
void setContent(T c) { content = c; }
T getContent() { return content; }
};
int main() {
Box<int> intBox; // T가 int인 Box 클래스
intBox.setContent(123);
Box<std::string> stringBox; // T가 std::string인 Box 클래스
stringBox.setContent("Hello Template");
}
예외 처리는 프로그램 실행 중에 발생하는 예측 불가능한 오류(예: 파일 열기 실패, 메모리 할당 실패, 0으로 나누기 등)를 구조적으로 처리하는 메커니즘이다. if 문으로 오류를 처리하는 방식보다 코드의 가독성과 안정성을 높여준다.114
try, catch, throw:
try: 예외가 발생할 가능성이 있는 코드를 try 블록으로 감싼다.114throw: try 블록 내에서 예외적인 상황이 발생하면, throw 키워드를 사용하여 예외를 발생시킨다. throw는 정수, 문자열, 객체 등 어떤 타입의 값이든 던질 수 있다.114catch: throw에 의해 던져진 예외를 받아서 처리하는 블록이다. catch는 처리할 예외의 타입을 명시해야 하며, 던져진 예외의 타입과 일치하는 catch 블록이 실행된다. catch(...)는 모든 타입의 예외를 처리할 수 있다.115#include <iostream>
#include <stdexcept>
double divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Division by zero error!");
}
return static_cast<double>(a) / b;
}
int main() {
try {
double result = divide(10, 0);
std::cout << "Result: " << result << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
return 0;
}
위 예제에서 divide 함수는 0으로 나누려는 시도가 있을 때 std::runtime_error 타입의 예외를 throw한다. main 함수에서는 try 블록 안에서 divide 함수를 호출하고, 예외가 발생하면 catch 블록이 이를 잡아 처리한다.
C++에서는 <fstream> 헤더 파일을 통해 파일에 데이터를 쓰거나 파일로부터 데이터를 읽어오는 스트림 기반의 파일 입출력 기능을 제공한다.
ofstream): ofstream (Output File Stream) 객체를 사용하여 파일에 데이터를 쓴다. cout과 마찬가지로 << 연산자를 사용한다.118ifstream): ifstream (Input File Stream) 객체를 사용하여 파일에서 데이터를 읽는다. cin과 마찬가지로 >> 연산자나 getline 함수를 사용한다.118open() 멤버 함수를 사용할 수도 있다.118ios::app은 이어쓰기, ios::binary는 바이너리 모드)를 지정할 수 있다.118close() 함수로 명시적으로 닫거나, 객체가 범위를 벗어나 소멸될 때 자동으로 닫힌다 (RAII 원칙).118#include <iostream>
#include <fstream>
#include <string>
int main() {
// 파일 쓰기
std::ofstream outFile("example.txt");
if (outFile.is_open()) {
outFile << "This is a line." << std::endl;
outFile << "This is another line." << std::endl;
outFile.close();
} else {
std::cerr << "Unable to open file for writing." << std::endl;
}
// 파일 읽기
std::ifstream inFile("example.txt");
std::string line;
if (inFile.is_open()) {
while (getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
} else {
std::cerr << "Unable to open file for reading." << std::endl;
}
return 0;
}
표준 템플릿 라이브러리(Standard Template Library, STL)는 C++ 표준 라이브러리의 핵심으로, 프로그래머가 직접 구현할 필요 없이 바로 사용할 수 있는 효율적인 자료구조와 알고리즘의 집합이다.122
STL의 진정한 혁신은 단순히 유용한 자료구조와 알고리즘을 제공하는 데 있지 않다. 그 핵심은 컨테이너(데이터 저장 방식), 반복자(데이터 접근 방식), 알고리즘(데이터 처리 방식)을 완전히 분리한 설계에 있다.123 이 세 구성 요소의 ‘직교성(Orthogonality)’ 덕분에, sort라는 단 하나의 알고리즘이 vector뿐만 아니라 deque, 심지어 C-스타일 배열에도 동일하게 적용될 수 있다. 필요한 것은 단지 컨테이너가 표준을 따르는 ‘반복자’를 제공하는 것뿐이다. 이는 n개의 컨테이너와 m개의 알고리즘이 있을 때 n*m개의 구현이 아닌 n+m개의 구현만으로 시스템을 구성할 수 있게 하는, 소프트웨어 공학적으로 매우 우아하고 효율적인 해결책이다.
vector(동적 배열), list(이중 연결 리스트), deque(양방향 큐) 등 데이터를 선형 순서로 저장한다.125map(키-값 쌍), set(키 집합) 등 키를 기반으로 데이터를 저장하며, 자동으로 정렬된다.125unordered_map, unordered_set 등 해시 테이블을 사용하여 정렬 없이 더 빠른 삽입/삭제/검색을 제공한다.125<algorithm> 헤더에 정의된, 컨테이너의 데이터에 대해 정렬, 검색, 수정 등의 작업을 수행하는 템플릿 함수들이다. sort, find, copy, for_each 등이 있다.125vector는 자동으로 크기가 조절되는 동적 배열로, STL에서 가장 널리 사용되는 컨테이너 중 하나이다.
#include <vector> 헤더가 필요하다. vector<타입> 이름; 형태로 선언한다.128push_back()으로 끝에 원소를 추가하고, pop_back()으로 끝의 원소를 제거한다.129at() 함수를 사용할 수 있다.129size() 함수로 현재 원소의 개수를 알 수 있다.129#include <iostream>
#include <vector>
#include <string>
int main() {
std::vector<std::string> names;
names.push_back("Alice");
names.push_back("Bob");
names.push_back("Charlie");
std::cout << "First name: " << names.front() << std::endl; // names과 동일
std::cout << "Last name: " << names.back() << std::endl;
// 범위 기반 for 루프를 이용한 순회
for (const std::string& name : names) {
std::cout << name << " ";
}
std::cout << std::endl;
names.pop_back(); // "Charlie" 제거
std::cout << "Size after pop: " << names.size() << std::endl;
return 0;
}
map은 키(Key)와 값(Value)을 쌍으로 저장하는 연관 컨테이너이다. 키는 유일해야 하며, 키를 기준으로 자동 정렬된다.
#include <map> 헤더가 필요하다. map<키타입, 값타입> 이름; 형태로 선언한다.133insert(make_pair(key, value)) 또는 map[key] = value 형태로 삽입한다. 후자의 방식이 더 간편하다.135map[key]로 값에 접근할 수 있다. find(key) 함수는 해당 키를 가진 원소의 반복자를 반환하며, 키가 없으면 end() 반복자를 반환한다.134#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> ages;
ages["Alice"] = 30;
ages = 25;
ages.insert(std::make_pair("Charlie", 35));
std::cout << "Bob's age: " << ages << std::endl;
// map 순회 (키 기준 정렬)
for (const auto& pair : ages) {
std::cout << pair.first << " is " << pair.second << " years old." << std::endl;
}
return 0;
}
아래 표는 C++ 연산자들의 우선순위와 결합 규칙을 정리한 것이다. 우선순위가 높을수록 먼저 계산되며, 같은 우선순위의 연산자들은 결합 방향에 따라 계산 순서가 결정된다. 복잡한 표현식에서는 괄호 ()를 사용하여 의도를 명확히 하는 것이 권장된다.31
| 우선순위 | 연산자 | 설명 | 결합 방향 |
|---|---|---|---|
| 1 | :: |
범위 지정 | 없음 |
| 2 | () `` . -> ++ -- typeid const_cast dynamic_cast reinterpret_cast static_cast |
함수 호출, 배열 첨자, 멤버 접근, 후위 증감, 타입 식별, 타입 캐스팅 | 왼쪽 –» 오른쪽 |
| 3 | ! ~ ++ -- + - & * sizeof new delete (type) |
단항 연산자 (논리 NOT, 비트 NOT, 전위 증감, 부호, 주소/역참조, 크기, 동적 할당/해제), C-스타일 캐스트 | 오른쪽 –» 왼쪽 |
| 4 | .* ->* |
멤버에 대한 포인터 | 왼쪽 –» 오른쪽 |
| 5 | * / % |
곱셈, 나눗셈, 나머지 | 왼쪽 –» 오른쪽 |
| 6 | + - |
덧셈, 뺄셈 | 왼쪽 –» 오른쪽 |
| 7 | << >> |
비트 시프트 | 왼쪽 –» 오른쪽 |
| 8 | < <= > >= |
관계 연산자 | 왼쪽 –» 오른쪽 |
| 9 | == != |
동등 연산자 | 왼쪽 –» 오른쪽 |
| 10 | & |
비트 AND | 왼쪽 –» 오른쪽 |
| 11 | ^ |
비트 XOR | 왼쪽 –» 오른쪽 |
| 12 | ` | ` | 비트 OR |
| 13 | && |
논리 AND | 왼쪽 –» 오른쪽 |
| 14 | ` | ` | |
| 15 | ? : = += -= *= /= %= <<= >>= &= ^= ` |
= throw` |
조건(삼항), 대입, 복합 대입, 예외 발생 |
| 16 | , |
쉼표 | 왼쪽 –» 오른쪽 |
| Visual Studio에서 C 및 C++ 지원 설치 | Microsoft Learn, 8월 3, 2025에 액세스, https://learn.microsoft.com/ko-kr/cpp/build/vscpp-step-0-installation?view=msvc-170 |
| C++ | 03.01 연산자 우선순위와 결합법칙(Operator precedence and associativity ), 8월 3, 2025에 액세스, https://heroine-day.tistory.com/30 |
| [C++] 기본 입출력 ( cin | cout | scanf | printf ) - 뚜벅뚜벅 컴공맨 - 티스토리, 8월 3, 2025에 액세스, https://where-i-am.tistory.com/entry/C-%EA%B8%B0%EB%B3%B8-%EC%9E%85%EC%B6%9C%EB%A0%A5-cin-cout-scanf-printf |
| [C] 구조체(struct). 구조체의 개념과 배열 사용 예시 | by EunJin | Medium, 8월 3, 2025에 액세스, https://jin0904.medium.com/c-%EA%B5%AC%EC%A1%B0%EC%B2%B4-struct-a82bae699581 |
| C++ | 좋은 클래스의 조건, 정보은닉과 캡슐화, 8월 3, 2025에 액세스, https://koey.tistory.com/60 |
| [C++] Class Template | 클래스 템플릿 - Archive - 티스토리, 8월 3, 2025에 액세스, https://dad-rock.tistory.com/175 |
| STL 개요 | STL | 템플릿 | C++ 프로그래밍 - YouTube, 8월 3, 2025에 액세스, https://www.youtube.com/watch?v=AYYZQ9apCOk |
| C++ 기본 제공 연산자, 우선 순위 및 결합성 | Microsoft Learn, 8월 3, 2025에 액세스, https://learn.microsoft.com/ko-kr/cpp/cpp/cpp-built-in-operators-precedence-and-associativity?view=msvc-170 |