C++ 개발 환경 구축
프로젝트 구조 및 빌드 시스템
일반적인 C++ 프로젝트 구조는 다음과 같습니다. include 폴더에는 헤더 파일이 포함되어 있으며, src 폴더에는 소스 코드 파일이 포함됩니다. Makefile은 프로젝트를 빌드하는 데 사용되는 빌드 시스템 파일입니다.
project/
│
├── include/
│ ├── header1.h
│ └── header2.h
│
├── src/
│ ├── main.cpp
│ ├── source1.cpp
│ └── source2.cpp
│
└── Makefile
C++ 프로그래밍 기본
변수와 데이터 타입
변수는 데이터를 저장하는 공간으로, C++에서는 변수를 선언할 때 데이터 타입을 지정해야 합니다. 주요 데이터 타입은 다음과 같습니다.
int: 정수
float: 실수
double: 실수 (float보다 더 큰 범위)
char: 문자
bool: 참/거짓 (true/false)
int age = 30;
float weight = 70.5;
double height = 180.25;
char initial = 'A';
bool is_happy = true;
참고로 std::cout과 std::endl은 C++의 표준 라이브러리인 iostream에 정의된 객체로, 콘솔 입출력에 사용됩니다. std::cout은 iostream 라이브러리의 일부이며, << 연산자를 사용하여 표현식의 결과를 출력합니다. std::endl은 콘솔 출력 스트림에 줄바꿈 문자를 삽입하고 버퍼를 비우는 조작자입니다.
연산자와 표현식
연산자의 종류로는 산술 연산자, 비교 연산자, 논리 연산자 등이 있습니다.
int a = 10;
int b = 3;
int sum = a + b; // 13
int diff = a - b; // 7
int product = a * b; // 30
int quotient = a / b; // 3 (정수 나눗셈)
int remainder = a % b; // 1
int x = 5;
int y = 10;
bool isEqual = (x == y); // false
bool isNotEqual = (x != y); // true
bool isGreater = (x > y); // false
bool isLess = (x < y); // true
bool isGreaterOrEqual = (x >= y); // false
bool isLessOrEqual = (x <= y); // true
bool a = true;
bool b = false;
bool result1 = a && b; // false (AND 연산)
bool result2 = a || b; // true (OR 연산)
bool result3 = !a; // false (NOT 연산)
int num = 10;
num += 5; // num = num + 5; // 15
num -= 3; // num = num - 3; // 12
num *= 2; // num = num * 2; // 24
num /= 4; // num = num / 4; // 6
num %= 3; // num = num % 3; // 0
흐름 제어 구문 (조건문, 반복문)
흐름 제어 구문을 사용하면 프로그램의 실행 순서를 변경할 수 있습니다. 조건문(if, switch)과 반복문(for, while, do-while)이 대표적인 흐름 제어 구문입니다.
if-else if-else문: 여러 조건을 순차적으로 검사하여 해당하는 조건의 코드 블록만 실행합니다.
int score = 85;
if (score >= 90) {
std::cout << "Grade: A" << std::endl;
} else if (score >= 80) {
std::cout << "Grade: B" << std::endl;
} else if (score >= 70) {
std::cout << "Grade: C" << std::endl;
} else {
std::cout << "Grade: F" << std::endl;
}
switch-case문: 주어진 표현식의 값에 따라 일치하는 case 문을 실행하고, 일치하는 값이 없으면 default 문을 실행합니다.
char grade = 'B';
switch (grade) {
case 'A':
std::cout << "Excellent!" << std::endl;
break;
case 'B':
std::cout << "Good job!" << std::endl;
break;
case 'C':
std::cout << "Well done!" << std::endl;
break;
default:
std::cout << "Invalid grade." << std::endl;
}
for문: 초기화, 조건, 증감식을 이용하여 코드 블록을 반복 실행합니다.
for (int i = 0; i < 5; ++i) {
std::cout << "Iteration: " << i << std::endl;
}
while문: 조건이 참인 동안 코드 블록을 반복 실행합니다.
int counter = 0;
while (counter < 5) {
std::cout << "Counter: " << counter << std::endl;
++counter;
}
do-while문: 코드 블록을 최소 한 번 실행한 후, 조건이 참인 동안 계속 반복 실행합니다.
int number;
do {
std::cout << "Enter a number between 1 and 10: ";
std::cin >> number;
} while (number < 1 || number > 10);
std::cout << "You entered: " << number << std::endl;
중첩된 반복문: 한 반복문 내부에 다른 반복문이 포함된 구조입니다. 다차원 배열, 행렬 등 복잡한 구조를 처리할 때 사용됩니다.
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << "(" << i << ", " << j << ") ";
}
std::cout << std::endl;
}
break: 반복문 내에서 break 문을 만나면 즉시 반복문을 빠져나옵니다.
for (int i = 0; i < 10; ++i) {
if (i == 5) {
break;
}
std::cout << "i: " << i << std::endl;
}
// 출력: 0 1 2 3 4
continue: 반복문 내에서 continue 문을 만나면 현재 반복의 나머지 코드를 건너뛰고 다음 반복으로 이동합니다.
for (int i = 0; i < 10; ++i) {
if (i % 2 == 1) {
continue;
}
std::cout << "i: " << i << std::endl;
}
// 출력: 0 2 4 6 8
이렇게 다양한 흐름 제어 구문을 이용하여 프로그램의 실행 흐름을 조절하고, 조건에 따라 코드를 실행하거나 반복할 수 있습니다. 이를 통해 프로그램의 로직을 구성하고 원하는 기능을 구현할 수 있습니다.
함수와 매개변수 전달
함수는 독립된 코드 블록으로, 이름이 지정되어 있고 특정 작업을 수행합니다. 함수는 입력 매개변수를 받아서 결과를 반환할 수 있습니다. C++에서 함수를 사용하면 코드를 재사용하고 모듈화할 수 있어 유지 관리가 쉬워집니다.
함수 정의는 다음과 같은 구성 요소를 포함합니다:
반환 타입: 함수가 반환하는 값의 타입입니다.
함수 이름: 함수를 호출할 때 사용하는 이름입니다.
매개변수 목록: 괄호 안에 있는, 함수에 전달되는 값들의 타입과 이름입니다.
함수 본문: 중괄호 안에 있는 함수의 실행 코드입니다.
함수정의
#include <iostream>
// 함수 정의
int sum(int a, int b) {
return a + b;
}
int main() {
int result = sum(3, 5); // 함수 호출
std::cout << "The sum of 3 and 5 is: " << result << std::endl;
return 0;
}
함수에 기본값 설정
#include <iostream>
// 함수에 기본값 설정
double multiply(double a, double b = 1.0) {
return a * b;
}
int main() {
std::cout << "2 * 3 = " << multiply(2, 3) << std::endl;
std::cout << "2 * 1 = " << multiply(2) << std::endl; // 기본값 사용
return 0;
}
참조를 통한 매개변수 전달
#include <iostream>
void swap(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int num1 = 5;
int num2 = 10;
std::cout << "Before swap: num1 = " << num1 << ", num2 = " << num2 << std::endl;
swap(num1, num2);
std::cout << "After swap: num1 = " << num1 << ", num2 = " << num2 << std::endl;
return 0;
}
배열과 문자열
배열은 동일한 데이터 타입의 값들을 연속적으로 저장하는 자료 구조입니다. 배열을 사용하면 여러 값을 하나의 변수에 저장할 수 있습니다. C++에서 배열을 선언하려면 데이터 타입, 변수 이름, 대괄호 안에 배열 크기를 지정해야 합니다.
예시 1: 2차원 배열
#include <iostream>
int main() {
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}
return 0;
}
문자열은 문자의 배열로 표현할 수 있습니다. C++에서 문자열을 처리하기 위해 기본적으로 문자 배열을 사용할 수 있으며, 문자열 끝에는 널 문자('\0')가 있어야 합니다. C++ 표준 라이브러리에서 제공하는 std::string 클래스를 사용하면 문자열 처리를 더 쉽게 할 수 있습니다.
예시 2: 문자열 길이 찾기와 문자열 연결
#include <iostream>
#include <string>
int main() {
std::string str1 = "Hello";
std::string str2 = "World";
// 문자열 길이 찾기
std::cout << "Length of str1: " << str1.length() << std::endl;
// 문자열 연결
std::string combined = str1 + " " + str2;
std::cout << "Combined string: " << combined << std::endl;
return 0;
}
예시 3: 문자열 검색 및 치환
#include <iostream>
#include <string>
int main() {
std::string sentence = "I like C++ programming.";
// 문자열 검색
std::size_t found = sentence.find("C++");
if (found != std::string::npos) {
std::cout << "Found 'C++' at position: " << found << std::endl;
}
// 문자열 치환
sentence.replace(found, 3, "Python");
std::cout << "Modified sentence: " << sentence << std::endl;
return 0;
}
포인터와 메모리 관리
포인터는 메모리 주소를 저장하는 변수입니다. 포인터를 사용하면 변수의 메모리 주소를 참조할 수 있으며, 이를 통해 메모리를 직접 관리할 수 있습니다.
예시 1: 포인터의 기본 사용
다음 예제에서는 int형 변수 num과 int형 포인터 ptr을 선언합니다. num의 주소는 & 연산자를 사용하여 얻고, 이 주소를 ptr에 저장합니다. 그리고 포인터를 사용하여 num의 값을 출력합니다.
#include <iostream>
int main() {
int num = 5;
int *ptr = # // num의 주소를 ptr에 저장
std::cout << "Value of num: " << num << std::endl;
std::cout << "Address of num: " << &num << std::endl;
std::cout << "Value of ptr: " << ptr << std::endl;
std::cout << "Value pointed by ptr: " << *ptr << std::endl;
return 0;
}
Value of num: 5
Address of num: 0x16d5f7478
Value of ptr: 0x16d5f7478
Value pointed by ptr: 5
예시 2: 동적 메모리 할당과 해제
동적 메모리 할당은 실행 중에 프로그램이 필요한 메모리 공간을 할당받는 것입니다. new 연산자를 사용하여 동적 메모리를 할당받을 수 있습니다. 할당받은 메모리는 delete 연산자로 해제해야 합니다.
#include <iostream>
int main() {
int array_size;
std::cout << "Enter the size of the array: ";
std::cin >> array_size;
int *array = new int[array_size]; // 동적 메모리 할당
for (int i = 0; i < array_size; i++) {
array[i] = i * 2;
}
for (int i = 0; i < array_size; i++) {
std::cout << "array[" << i << "] = " << array[i] << std::endl;
}
delete[] array; // 동적 메모리 해제
return 0;
}
입출력 (콘솔, 파일)
C++에서 입출력을 위해 iostream 헤더를 사용하며, 이를 통해 콘솔 입력 및 출력을 처리할 수 있습니다. 파일 입출력을 위해서는 fstream 헤더를 사용합니다.
예시 1: 콘솔 입출력
이 예제에서는 사용자로부터 이름과 나이를 입력 받아 출력하는 프로그램을 작성합니다. getline 함수를 사용하여 공백을 포함한 문자열을 입력 받을 수 있습니다.
#include <iostream>
#include <string>
int main() {
std::string name;
int age;
std::cout << "Enter your name: ";
std::getline(std::cin, name); // 공백을 포함한 문자열 입력 받기
std::cout << "Enter your age: ";
std::cin >> age;
std::cout << "Hello, " << name
객체 지향 프로그래밍
객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그래밍 패러다임 중 하나로, 데이터와 기능을 하나의 객체로 묶어 추상화하고 상속, 다형성 등의 개념을 사용하여 코드의 재사용성과 유지 보수를 용이하게 하는 방법입니다.
4.1 클래스와 객체
클래스(Class)는 객체(Object)를 생성하기 위한 틀로, 변수(멤버 변수)와 함수(멤버 함수)를 가질 수 있습니다. 객체는 클래스를 기반으로 생성되며, 객체 각각은 독립적인 상태를 가지고 있습니다.
예시 1: 클래스와 객체
이 예제에서는 학생 정보를 저장하는 Student 클래스를 정의하고, 객체를 생성하여 사용합니다.
#include <iostream>
#include <string>
class Student {
public:
std::string name;
int age;
float grade;
void introduce() {
std::cout << "Name: " << name << ", Age: " << age << ", Grade: " << grade << std::endl;
}
};
int main() {
Student student1;
student1.name = "Alice";
student1.age = 20;
student1.grade = 4.3;
Student student2;
student2.name = "Bob";
student2.age = 22;
student2.grade = 3.8;
student1.introduce();
student2.introduce();
return 0;
}
4.2 생성자와 소멸자
생성자(Constructor)는 객체가 생성될 때 자동으로 호출되는 함수로, 객체의 초기화 작업을 수행합니다. 소멸자(Destructor)는 객체가 소멸될 때 호출되는 함수로, 객체의 메모리 해제 등의 정리 작업을 수행합니다.
예시 2: 생성자와 소멸자
이 예제에서는 생성자를 사용하여 객체를 초기화하고, 소멸자를 사용하여 객체가 소멸될 때 메시지를 출력합니다. 생성자는 객체가 생성될 때 이름, 나이, 성적을 매개변수로 받아 초기화 작업을 수행합니다. 소멸자는 객체가 소멸될 때 메시지를 출력하는 간단한 예제로 작성되어 있습니다. 이렇게 생성자와 소멸자를 사용하면 객체의 생성 및 소멸 과정에서 필요한 초기화와 정리 작업을 효과적으로 처리할 수 있습니다.
#include <iostream>
#include <string>
class Student {
public:
std::string name;
int age;
float grade;
// 생성자
Student(std::string name, int age, float grade) {
this->name = name;
this->age = age;
this->grade = grade;
}
// 소멸자
~Student() {
std::cout << name << " 객체가 소멸되었습니다." << std::endl;
}
void introduce() {
std::cout << "Name: " << name << ", Age: " << age << ", Grade: " << grade << std::endl;
}
};
int main() {
Student student1("Alice", 20, 4.3);
Student student2("Bob", 22, 3.8);
student1.introduce();
student2.introduce();
return 0;
}
4.3 상속과 다형성
상속(Inheritance)은 기존 클래스의 기능을 상속받아 새로운 클래스를 만드는 것입니다. 상속을 사용하면 코드의 중복을 줄이고, 소프트웨어의 모듈성을 향상시킬 수 있습니다. 다형성(Polymorphism)은 클래스 상속 관계에서 하나의 인터페이스를 통해 다양한 객체를 다룰 수 있는 기능입니다. 다형성을 통해 코드의 유연성과 확장성을 향상시킬 수 있습니다.
예시 4: 상속과 다형성
이 예제에서 Animal 클래스는 가상 함수 speak()를 가지고 있습니다. 이 함수는 Dog와 Cat 클래스에서 오버라이딩(재정의)됩니다. makeSound 함수는 Animal 참조를 통해 다양한 동물 객체를 처리할 수 있는 다형성을 보여줍니다.
#include <iostream>
class Animal {
public:
virtual void speak() = 0; // 순수 가상 함수
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!" << std::endl;
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!" << std::endl;
}
};
void makeSound(Animal& animal) {
animal.speak();
}
int main() {
Dog dog;
Cat cat;
makeSound(dog); // 출력: Woof!
makeSound(cat); // 출력: Meow!
return 0;
}
4.4 추상화와 캡슐화
추상화(Abstraction)는 복잡한 시스템을 단순화하고, 관련 있는 특성만을 표현하는 것입니다. 클래스는 추상화를 통해 객체의 공통 속성과 동작을 정의할 수 있습니다. 캡슐화(Encapsulation)는 객체의 상태와 행동을 하나로 묶고, 외부에서의 접근을 제한하는 것입니다. 캡슐화를 통해 객체의 내부 상태를 보호하고, 객체 간의 상호 작용을 명확하게 정의할 수 있습니다.
예시 5: 추상화와 캡슐화
#include <iostream>
#include <string>
class Vehicle {
private:
std::string make;
std::string model;
int year;
public:
Vehicle(std::string make, std::string model, int year)
: make(make), model(model), year(year) {}
std::string getMake() const {
return make;
}
std::string getModel() const {
return model;
}
int getYear() const {
return year;
}
void setMake(std::string newMake) {
make = newMake;
}
void setModel(std::string newModel) {
model = newModel;
}
void setYear(int newYear) {
year = new
4.5 C++ 표준 템플릿 라이브러리(STL) 소개
C++ 표준 템플릿 라이브러리(Standard Template Library, STL)는 C++ 프로그래밍에서 다양한 일반적인 자료 구조와 알고리즘을 제공하는 라이브러리입니다. STL은 일반화된 코드를 제공하므로 개발자가 공통 자료 구조와 알고리즘에 대해 걱정하지 않고 문제 해결에 집중할 수 있습니다. STL은 주로 컨테이너, 반복자, 알고리즘, 함수 객체, 할당자 등으로 구성되어 있습니다.
예시 6: STL 사용하기
이 예제에서 STL의 std::vector 컨테이너와 알고리즘 std::sort와 std::binary_search를 사용하여 정수를 저장하고, 정렬하고, 특정 값이 있는지 확인합니다.
STL은 개발자의 생산성을 높이고 코드의 가독성과 재사용성을 향상시키는데 큰 도움이 됩니다. 그러나 STL을 사용할 때, 템플릿 코드의 복잡성과 컴파일 시간 증가 등의 단점도 고려해야 합니다.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5};
// 정렬 알고리즘을 사용하여 벡터 정렬
std::sort(numbers.begin(), numbers.end());
// 벡터의 내용을 출력
for (int number : numbers) {
std::cout << number << " ";
}
std::cout << std::endl;
// binary_search 알고리즘을 사용하여 값 찾기
if (std::binary_search(numbers.begin(), numbers.end(), 5)) {
std::cout << "Found 5!" << std::endl;
} else {
std::cout << "5 not found!" << std::endl;
}
return 0;
}
고급 C++ 프로그래밍
5.1 예외 처리
예외 처리는 프로그램에서 예상치 못한 상황이 발생했을 때 적절하게 대응할 수 있는 기능입니다. C++에서는 try, catch, throw 키워드를 사용하여 예외를 처리합니다.
예시: 예외 처리하기
이 예제에서 divide 함수는 분모가 0인 경우 예외를 발생시킵니다. main 함수에서 try 블록 안에서 divide 함수를 호출하고, 예외가 발생하면 catch 블록에서 해당 예외를 처리합니다.
#include <iostream>
#include <stdexcept>
double divide(double numerator, double denominator) {
if (denominator == 0) {
throw std::runtime_error("Attempted to divide by zero.");
}
return numerator / denominator;
}
int main() {
double numerator = 4.0;
double denominator = 0.0;
try {
double result = divide(numerator, denominator);
std::cout << "The result is: " << result << std::endl;
} catch (const std::runtime_error& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
5.2 템플릿과 제네릭 프로그래밍
템플릿은 함수나 클래스를 일반화하여 여러 유형의 데이터에 대해 동작하도록 만들어주는 C++의 기능입니다. 제네릭 프로그래밍은 템플릿을 사용하여 유연하고 재사용 가능한 코드를 작성하는 프로그래밍 패러다임입니다.
예시: 템플릿 사용하기
이 예제에서 add 함수는 템플릿 함수로 선언되어 있어서 다양한 데이터 타입에 대해 동작합니다. 이를 통해 int형과 double형 모두에 대해 add 함수를 사용할 수 있습니다.
템플릿과 제네릭 프로그래밍을 사용하면 다양한 데이터 타입에 대해 동작하는 유연한 코드를 작성할 수 있으며, 코드 중복을 줄일 수 있습니다. 그러나 복잡한 템플릿 코드는 가독성이 떨어지고, 컴파일 시간이 길어질 수 있습니다.
#include <iostream>
// 함수 템플릿 선언
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
int a = 3;
int b = 4;
double c = 1.5;
double d = 3.7;
std::cout << "int addition: " << add(a, b) << std::endl;
std::cout << "double addition: " << add(c, d) << std::endl;
return 0;
}
5.3 람다 함수와 함수 객체
람다 함수와 함수 객체는 C++에서 함수를 일급 객체로 취급하는 데 도움이 되는 기능입니다. 이를 통해 함수를 변수처럼 사용하고, 다른 함수의 인수로 전달하거나 반환할 수 있습니다.
5.3.1 람다 함수
람다 함수는 익명의 인라인 함수입니다. 람다 함수를 사용하면 코드를 간결하게 작성할 수 있습니다.
예시: 람다 함수 사용하기
이 예제에서 std::for_each 알고리즘은 벡터의 각 요소에 대해 람다 함수를 실행합니다. 람다 함수는 벡터의 모든 요소를 합산합니다.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 람다 함수 정의 및 사용
int sum = 0;
std::for_each(numbers.begin(), numbers.end(), [&sum](int n) {
sum += n;
});
std::cout << "The sum is: " << sum << std::endl;
return 0;
}
5.3.2 함수 객체
함수 객체는 함수 호출 연산자 operator()를 오버로딩한 클래스의 객체입니다. 함수 객체를 사용하면 람다 함수와 비슷한 방식으로 함수를 사용할 수 있습니다.
예시: 함수 객체 사용하기
이 예제에서 Adder 클래스는 함수 호출 연산자를 오버로딩하여 벡터의 모든 요소를 합산합니다. std::for_each 알고리즘은 벡터의 각 요소에 대해 함수 객체를 실행합니다.
#include <iostream>
#include <vector>
#include <algorithm>
class Adder {
public:
Adder(int initial = 0) : sum(initial) {}
void operator()(int n) {
sum += n;
}
int get_sum() const {
return sum;
}
private:
int sum;
};
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 함수 객체 생성 및 사용
Adder adder;
std::for_each(numbers.begin(), numbers.end(), adder);
std::cout << "The sum is: " << adder.get_sum() << std::endl;
return 0;
}
5.4 스마트 포인터
C++에서 동적 메모리 할당을 사용하면 개발자가 직접 메모리를 관리해야 합니다. 스마트 포인터는 이러한 메모리 관리를 자동화하여 메모리 누수와 관련된 오류를 최소화하는 데 도움이 되는 기능입니다.
스마트 포인터는 일반 포인터처럼 작동하며, 자동으로 할당된 메모리를 해제해주는 역할을 합니다. C++11부터는 다음과 같은 세 가지 종류의 스마트 포인터를 제공합니다.
1. std::unique_ptr: 고유한 소유권을 가진 객체를 관리하는 포인터입니다. 객체는 한 번에 오직 하나의 unique_ptr에 의해서만 소유될 수 있습니다.
2. std::shared_ptr: 참조 카운팅을 사용하여 여러 개의 shared_ptr이 동일한 객체를 공유할 수 있게 해주는 포인터입니다. 모든 shared_ptr이 객체에 대한 참조를 해제하면, 객체가 자동으로 삭제됩니다.
3. std::weak_ptr: shared_ptr과 함께 사용되며, 참조 카운팅에 영향을 주지 않는 포인터입니다. 순환 참조 문제를 해결하는 데 사용됩니다.
스마트 포인터를 사용하면 메모리 관리를 자동화하여 메모리 누수를 방지하고, 코드의 안정성과 가독성을 향상시킬 수 있습니다. 그러나 스마트 포인터가 모든 상황에서 완벽한 해결책은 아닙니다. 때로는 원시 포인터를 사용해야 하는 경우도 있으며, 성능이 중요한 경우 스마트 포인터의 오버헤드를 고려해야 할 수도 있습니다.
예시: std::unique_ptr 사용하기
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
};
int main() {
{
std::unique_ptr<MyClass> ptr1(new MyClass());
} // ptr1이 범위를 벗어나면 MyClass 객체가 자동으로 삭제됩니다.
std::cout << "End of main function" << std::endl;
return 0;
}
예시: std::shared_ptr 사용하기
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
};
int main() {
std::shared_ptr<MyClass> ptr1;
{
std::shared_ptr<MyClass> ptr2(new MyClass());
ptr1 = ptr2;
} // ptr2가 범위를 벗어나도 MyClass 객체는 삭제되지 않습니다.
std::cout << "End of main function" << std::endl;
return 0;
} // ptr1이 범위를 벗어나면 MyClass 객체가 자동으로 삭제됩니다.
예시: std::make_shared를 사용하여 std::shared_ptr 생성하기
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
};
int main() {
auto ptr = std::make_shared<MyClass>();
std::cout << "End of main function" << std::endl;
return 0;
} // ptr이 범위를 벗어나면 MyClass 객체가 자동으로 삭제됩니다.
예시: std::weak_ptr 사용하기
이 예제에서 weak_ptr는 shared_ptr의 객체를 가리키지만 참조 카운트에 영향을 주지 않습니다. 따라서 sharedPtr가 범위를 벗어나면 객체가 삭제되고, weak_ptr는 무효한 포인터가 됩니다.
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
};
int main() {
std::weak_ptr<MyClass> weakPtr;
{
std::shared_ptr<MyClass> sharedPtr = std::make_shared<MyClass>();
weakPtr = sharedPtr;
std::cout << "Shared pointer count: " << sharedPtr.use_count() << std::endl;
} // sharedPtr가 범위를 벗어나면 MyClass 객체가 삭제됩니다.
std::cout << "End of main function" << std::endl;
return 0;
}
5.5 멀티스레딩과 동시성
멀티스레딩은 하나의 프로그램에서 동시에 여러 작업을 수행할 수 있게 하는 프로그래밍 기법입니다. 이를 통해 시스템의 자원을 효율적으로 활용하고, 병렬 처리를 통해 프로그램의 성능을 높일 수 있습니다. C++11부터는 멀티스레딩을 지원하는 표준 라이브러리가 도입되어, 더 쉽게 멀티스레딩 기능을 구현할 수 있게 되었습니다.
스레드 생성 및 실행
C++11의 <thread> 헤더를 사용하면 스레드를 쉽게 생성하고 실행할 수 있습니다. 예를 들어, 간단한 함수를 별도의 스레드에서 실행하려면 다음과 같이 작성할 수 있습니다.
#include <iostream>
#include <thread>
void print_hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(print_hello);
t.join(); // 기다리기
std::cout << "Hello from main thread!" << std::endl;
return 0;
}
동시성 제어
멀티스레딩에서 가장 중요한 고려사항 중 하나는 동시성 제어입니다. 동시성 제어는 공유 자원에 대한 접근을 관리하여 데이터 무결성을 유지하는데 필요합니다. C++에서는 뮤텍스(mutex)와 락(lock)을 사용하여 동시성을 관리할 수 있습니다. 예를 들어, 두 스레드가 동시에 벡터에 원소를 추가하는 작업을 수행할 때, 동시성 제어를 사용하여 안전하게 원소를 추가할 수 있습니다.
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex mtx;
void add_element(std::vector<int>& vec, int value) {
std::unique_lock<std::mutex> lock(mtx);
vec.push_back(value);
lock.unlock();
}
int main() {
std::vector<int> vec;
std::thread t1(add_element, std::ref(vec), 1);
std::thread t2(add_element, std::ref(vec), 2);
t1.join();
t2.join();
for (int elem : vec) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
조건 변수(Condition Variable)
조건 변수는 스레드가 특정 조건을 충족할 때까지 기다리게 할 수 있는 도구입니다. 이를 통해 스레드 간의 동기화를 수행할 수 있습니다. 예를 들어, 공유 버퍼에 데이터가 있을 때만 처리를 수행하는 스레드를 구현하려면 다음과 같이 작성할 수 있습니다.
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> shared_buffer;
void produce() {
for (int i = 1; i <= 5; ++i) {
std::unique_lock<std::mutex> lock(mtx);
shared_buffer.push(i);
std::cout << "Produced: " << i << std::endl;
lock.unlock();
cv.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
void consume() {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return !shared_buffer.empty(); });
int value = shared_buffer.front();
shared_buffer.pop();
std::cout << "Consumed: " << value << std::endl;
if (value == 5) {
break;
}
}
}
int main() {
std::thread producer(produce);
std::thread consumer(consume);
producer.join();
consumer.join();
return 0;
}
최적화와 성능 고려 사항
멀티스레딩을 사용할 때 성능 최적화와 관련된 몇 가지 고려 사항이 있습니다. 스레드가 너무 많으면 오버헤드가 발생하여 성능이 저하될 수 있으므로, 시스템의 코어 수와 적절한 스레드 수를 고려해야 합니다. 또한, 공유 자원에 대한 동시 접근을 제한하면서도 락 경쟁을 최소화하는 방법을 찾는 것이 중요합니다. 이를 위해 락-프리(lock-free) 데이터 구조나 원자적 연산(atomic operations)을 사용하여 동시성을 관리할 수 있습니다.
6. C++ 프로그래밍 실전 예제
6.1 문자열 처리와 정규 표현식
C++에서 문자열 처리는 다양한 방법으로 수행할 수 있습니다. 문자열 처리의 일반적인 작업에는 문자열을 조작하거나 검색, 변환, 비교 등이 포함됩니다. 이 장에서는 문자열 처리와 관련된 기본적인 작업과 함께 정규 표현식을 사용하여 문자열을 처리하는 방법에 대해 알아보겠습니다.
문자열 조작
C++에서 문자열 조작은 주로 <string> 헤더에 정의된 std::string 클래스를 사용하여 수행됩니다. 이 클래스는 다양한 메서드를 제공하여 문자열을 쉽게 처리할 수 있습니다. 예를 들어, 문자열 결합 및 추가 작업은 다음과 같이 수행할 수 있습니다.
#include <iostream>
#include <string>
int main() {
std::string str1 = "Hello";
std::string str2 = " World";
std::string combined = str1 + str2; // 문자열 결합
std::cout << combined << std::endl;
str1 += str2; // 문자열 추가
std::cout << str1 << std::endl;
return 0;
}
정규 표현식
정규 표현식은 문자열 패턴을 정의하는 데 사용되는 강력한 도구입니다. C++에서 정규 표현식을 사용하려면 <regex> 헤더를 포함해야 합니다. 이 헤더에는 정규 표현식 작업에 필요한 클래스와 함수가 정의되어 있습니다. 예를 들어, 주어진 문자열에서 이메일 주소를 찾으려면 다음과 같이 작성할 수 있습니다.
이 예제에서는 정규 표현식 패턴을 사용하여 문자열에서 이메일 주소를 찾습니다. 이메일 패턴은 std::regex 객체로 생성되며, std::regex_search 함수를 사용하여 문자열에서 해당 패턴이 있는지 확인합니다.
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string text = "Please send an email to example@example.com and another email to john.doe@example.org.";
std::regex email_pattern(R"((\w+[\._]?)*\w+@(\w+\.)+\w+)");
std::smatch match_results;
while (std::regex_search(text, match_results, email_pattern)) {
std::cout << "Found email: " << match_results[0] << std::endl;
text = match_results.suffix();
}
return 0;
}
문자열 치환
문자열에서 특정 패턴을 찾아 다른 문자열로 바꾸려면 std::regex_replace 함수를 사용할 수 있습니다. 예를 들어, 주어진 문자열에서 이메일 주소를 찾아서 별표로 바꾸려면 다음과 같이 작성할 수 있습니다. 이 예제에서는 std::regex_replace 함수를 사용하여 이메일 주소를 찾아서 별표로 치환합니다.
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string text = "Please send an email to example@example.com and another email to john.doe@example.org.";
std::regex email_pattern(R"((\w+[\._]?)*\w+@(\w+\.)+\w+)");
std::string replaced_text = std::regex_replace(text, email_pattern, "*****");
std::cout << "Replaced text: " << replaced_text << std::endl;
return 0;
}
문자열 분할
C++에서 문자열을 특정 구분자를 기준으로 분할하려면 정규 표현식과 std::sregex_token_iterator를 사용할 수 있습니다. 예를 들어, 주어진 문자열을 공백으로 분할하려면 다음과 같이 작성할 수 있습니다. 이 예제에서는 std::sregex_token_iterator를 사용하여 공백을 기준으로 문자열을 분할합니다. 분할된 각 토큰은 반복자를 사용하여 처리할 수 있습니다.
#include <iostream>
#include <string>
#include <regex>
int main() {
std::string text = "This is a sample sentence.";
std::regex space_pattern(R"(\s+)");
std::sregex_token_iterator begin(text.begin(), text.end(), space_pattern, -1);
std::sregex_token_iterator end;
for (auto it = begin; it != end; ++it) {
std::cout << "Token: " << *it << std::endl;
}
return 0;
}
6.2 파일과 디렉터리 조작
C++에서 파일과 디렉터리를 조작하려면, 파일 입출력 및 filesystem 라이브러리를 사용할 수 있습니다. 이를 통해 파일과 디렉터리에 대한 정보를 읽고, 생성, 삭제 및 수정하는 작업을 수행할 수 있습니다.
파일 입출력
C++에서 파일을 읽고 쓰려면 std::ifstream과 std::ofstream을 사용할 수 있습니다. 예를 들어, 주어진 텍스트 파일을 읽어 출력하려면 다음과 같이 작성할 수 있습니다.
이 예제에서는 std::ifstream 객체를 사용하여 주어진 텍스트 파일을 읽습니다. 파일의 각 줄을 읽어 출력한 후, 파일을 닫습니다.
#include <iostream>
#include <fstream>
#include <string>
int main() {
std::string input_file_path = "input.txt";
std::ifstream input_file(input_file_path);
if (!input_file) {
std::cerr << "Could not open the file: " << input_file_path << std::endl;
return 1;
}
std::string line;
while (std::getline(input_file, line)) {
std::cout << line << std::endl;
}
input_file.close();
return 0;
}
디렉터리 조작
C++17부터 std::filesystem 라이브러리를 사용하여 디렉터리를 조작할 수 있습니다. 예를 들어, 주어진 디렉터리에 포함된 파일과 하위 디렉터리를 나열하려면 다음과 같이 작성할 수 있습니다.
이 예제에서는 std::filesystem::directory_iterator를 사용하여 주어진 디렉터리의 내용을 나열합니다.
#include <iostream>
#include <filesystem>
int main() {
std::filesystem::path directory_path = ".";
if (!std::filesystem::is_directory(directory_path)) {
std::cerr << "Path is not a directory: " << directory_path << std::endl;
return 1;
}
std::cout << "Contents of directory: " << directory_path << std::endl;
for (const auto &entry : std::filesystem::directory_iterator(directory_path)) {
std::cout << entry.path() << std::endl;
}
return 0;
}
6.3 네트워킹
C++에서 네트워킹 기능을 사용하려면, 소켓 프로그래밍 및 외부 라이브러리를 사용할 수 있습니다. 여기서는 기본적인 TCP 소켓을 사용한 간단한 클라이언트-서버 예제를 살펴보겠습니다.
서버 예제
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main() {
const int PORT = 8080;
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_size;
// Create a socket
server_socket = socket(AF_INET, SOCK_STREAM, 0);
// Configure the server address
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// Bind the socket to the server address
bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
// Listen for incoming connections
listen(server_socket, 1);
// Accept an incoming connection
addr_size = sizeof(client_addr);
client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_size);
// Receive data from the client
char buffer[256];
int data_size = recv(client_socket, buffer, sizeof(buffer), 0);
buffer[data_size] = '\0';
std::cout << "Received data: " << buffer << std::endl;
// Close the sockets
close(client_socket);
close(server_socket);
return 0;
}
클라이언트 예제
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
const char *SERVER_IP = "127.0.0.1";
const int PORT = 8080;
int client_socket;
struct sockaddr_in server_addr;
// Create a socket
client_socket = socket(AF_INET, SOCK_STREAM, 0);
// Configure the server address
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
// Connect to the server
connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr));
// Send data to the server
const char *data = "Hello, server!";
send(client_socket, data, strlen(data), 0);
// Close the socket
close(client_socket);
return 0;
}
이 예제에서는 소켓 프로그래밍을 사용하여 TCP 서버 및 클라이언트를 작성했습니다. 서버는 8080 포트에서 수신 대기하며, 클라이언트가 연결하면 데이터를 전송하고 연결을 종료합니다.
C++에서 네트워킹을 사용하면, 다양한 네트워크 프로토콜과 통신하는 프로그램을 작성할 수 있습니다. 이러한 기능은 특히 웹 서비스, 채팅 애플리케이션, 파일 전송 프로그램 등 다양한 분야에서 사용할 수 있습니다. 다음 예제에서는 HTTP 클라이언트를 만드는 방법을 설명합니다.
HTTP 클라이언트 예제
C++로 HTTP 클라이언트를 구현하기 위해 외부 라이브러리를 사용할 수 있습니다. 여기서는 cURL을 사용하여 간단한 HTTP GET 요청을 보내는 방법을 살펴봅니다. 먼저, cURL 라이브러리를 설치해야 합니다.
sudo apt-get install libcurl4-gnutls-dev
이제 다음 코드를 사용하여 간단한 HTTP 클라이언트를 작성할 수 있습니다.
#include <iostream>
#include <curl/curl.h>
size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
((std::string *)userp)->append((char *)contents, size * nmemb);
return size * nmemb;
}
int main() {
const char *URL = "https://api.example.com/data";
CURL *curl;
CURLcode res;
std::string read_buffer;
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, URL);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer);
res = curl_easy_perform(curl);
if(res != CURLE_OK) {
std::cerr << "curl_easy_perform() failed: " << curl_easy_strerror(res) << std::endl;
} else {
std::cout << "Received data: " << read_buffer << std::endl;
}
curl_easy_cleanup(curl);
}
curl_global_cleanup();
return 0;
}
이 예제에서는 cURL 라이브러리를 사용하여 HTTP GET 요청을 전송하고 응답을 수신합니다. write_callback 함수는 응답 데이터를 저장하기 위해 cURL에 의해 호출됩니다. 결과적으로, read_buffer에는 HTTP 응답의 본문이 포함됩니다.
6.4 GUI 프로그래밍
C++로 GUI 프로그래밍을 수행하려면, 외부 라이브러리가 필요합니다. Qt는 많이 사용되는 크로스 플랫폼 C++ GUI 프레임워크 중 하나입니다. Qt는 강력한 기능과 직관적인 API를 제공하여 데스크톱, 모바일 및 임베디드 장치에서 사용할 수 있는 사용자 인터페이스를 만드는 데 도움이 됩니다.
Qt를 사용하기 전에, Qt를 다운로드하고 설치해야 합니다. 또한, Qt Creator라는 IDE를 사용하여 Qt 프로젝트를 개발할 수 있습니다.
간단한 Qt 애플리케이션 예제
먼저, Qt 프로젝트를 생성합니다. Qt Creator를 열고 "New Project"를 선택한 다음, "Qt Widgets Application"을 선택하고 프로젝트 이름 및 위치를 지정합니다.
Qt Creator에서 생성된 기본 코드를 사용하여 간단한 애플리케이션을 작성할 수 있습니다. 예를 들어, 다음 코드를 사용하여 기본 Qt 애플리케이션을 작성할 수 있습니다.
#include <QApplication>
#include <QLabel>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QLabel label("Hello, Qt!");
label.show();
return app.exec();
}
이 예제에서는 QApplication 및 QLabel 위젯을 사용하여 간단한 "Hello, Qt!" 라벨을 표시하는 애플리케이션을 만듭니다. 애플리케이션을 실행하면 레이블이 표시되는 작은 창이 나타납니다. 좀 더 복잡한 GUI를 만들려면, Qt 디자이너를 사용하여 인터페이스를 그래픽적으로 디자인할 수 있습니다. Qt Creator에서 "Form" 파일을 추가하고, 디자이너를 사용하여 여러 위젯(버튼, 라벨, 텍스트 상자 등)을 배치합니다.
Qt 디자이너에서 생성된 UI 파일을 C++ 코드로 변환하고, 사용자 정의 슬롯 및 시그널을 사용하여 위젯 간 상호 작용을 구현합니다. 예를 들어, 버튼을 클릭하면 텍스트 상자의 내용이 라벨에 표시되도록 할 수 있습니다.
'IT > C++' 카테고리의 다른 글
C++ Strings(c_str() / Substrings) (0) | 2023.05.29 |
---|---|
C++ References 정리 (0) | 2023.05.28 |
C++이란? (0) | 2023.05.27 |
C++ Pointers 정리 (0) | 2023.05.27 |
ROS(Robot Operating System) 프로그래밍(Hello World Example, C++) (0) | 2023.04.11 |
댓글