포인터
포인터란?
- 메모리 주소를 저장하는 변수
- C++에서 포인터를 사용하면 동적 메모리 할당, 배열 조작, 효율적인 함수 호출 등을 수행할 수 있다
선언
int* ptr; // 정수형 포인터 선언
int num = 10;
int* ptr = # // num의 주소를 ptr에 저장
연산
- 포인터를 사용하면 주소에 저장된 값을 참조할 수 있다
#include <iostream>
int main() {
int num = 10;
int* ptr = #
std::cout << "num의 값: " << num << std::endl;
std::cout << "num의 주소: " << &num << std::endl;
std::cout << "포인터 ptr이 가리키는 값: " << *ptr << std::endl;
return 0;
}
출력 예)
num의 값: 10
num의 주소: 0x7ffee1b8cabc
포인터 ptr이 가리키는 값: 10
포인터와 배열
- 배열의 이름은 배열의 첫 번째 요소를 가리키는 포인터와 같다
#include <iostream>
int main() {
int arr[3] = {10, 20, 30};
int* ptr = arr; // 배열의 첫 번째 요소를 가리킴
for (int i = 0; i < 3; i++) {
std::cout << "arr[" << i << "] = " << *(ptr + i) << std::endl;
}
return 0;
}
출력 예)
num의 값: 10
num의 주소: 0x7ffee1b8cabc
포인터 ptr이 가리키는 값: 10
동적 메모리 할당
- C++에서는 new와 delete를 사용하여 동적으로 메모리를 할당하고 해제할 수 있다
정적 메모리 할당
int a = 10; // 정적 메모리 할당
int* p = new int; // 동적 메모리 할당
*p = 20; // 할당된 메모리에 값 저장
std::cout << *p; // 20 출력
delete p; // 메모리 해제
동적 메모리 할당
int* arr = new int[5]; // 크기 5의 동적 배열 생성
for (int i = 0; i < 5; i++) {
arr[i] = (i + 1) * 10;
}
for (int i = 0; i < 5; i++) {
std::cout << arr[i] << " ";
}
delete[] arr; // 메모리 해제
출력 예)
10 20 30 40 50
함수에서 포인터 사용
- 포인터를 함수의 매개변수로 전달하면 원본 데이터를 직접 수정할 수 있다
void modify(int* ptr) {
*ptr = 100;
}
int main() {
int value = 50;
modify(&value);
std::cout << value; // 100 출력
return 0;
}
이중 포인터
int num = 10;
int* ptr = #
int** dptr = &ptr;
std::cout << **dptr; // num의 값(10) 출력
스마트 포인터
- C++11 이후 std::unique_ptr, std::shared_ptr 등의 스마트 포인터를 사용하여 메모리 관리를 자동화 할 수 있다
#include <memory>
#include <iostream>
int main() {
std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::cout << *p1 << std::endl; // 42 출력
return 0;
}
왜 사용하는가?
메모리 구조와 역할
#include <iostream>
int globalVar = 10; // (1) Data Section (전역 변수)
void func() {
int localVar = 20; // (2) Stack (지역 변수)
}
int main() {
int localMainVar = 30; // (3) Stack (지역 변수)
int* heapVar = new int(40); // (4) Heap (동적 할당 변수)
static int staticVar = 50; // (5) Data Section (static 변수)
std::cout << "localMainVar (Stack) : " << &localMainVar << std::endl;
std::cout << "heapVar (Heap) : " << heapVar << std::endl;
std::cout << "globalVar (Data Section) : " << &globalVar << std::endl;
std::cout << "staticVar (Data Section) : " << &staticVar << std::endl;
delete heapVar; // Heap 메모리 해제 (필수)
return 0;
}
메모리 구조
- 프로그램 실행 시 code section, stack, heap, data section 등의 메모리 영역으로 나뉜다
- code section : 프로그램의 기계어 코드가 저장되는 영역
- stack : 지역 변수와 함수 호출 정보를 저장하는 영역(LIFO 방식으로 관리)
- heap : 동적으로 할당된 메모리 영역으로, 프로그램이 직접 할당/해제
- data section : 전역 변수 및 static 변수가 저장되는 영역
프로그램 실행
- 프로그램이 실행되면 CPU는 code section의 기계어 코드를 실행한다
- 실행된 프로그램은 stack과 heap을 포함한 일부 메모리 영역에 접근할 수 있지만, 시스템 보호에 의해 제한된 영역도 있다
메모리 접근
- 포인터를 사용하면 Heap 영역에 접근할 수 있다.
- 하지만 포인터 자체는 Stack에 위치할 수도 있고, Heap에 존재할 수도 있다.
- 포인터는 Heap뿐만 아니라, 다양한 메모리 영역을 가리킬 수 있다.
- Heap, Stack, Data Section의 메모리에 접근 가능
하드웨어 접근
- 포인터는 특정 메모리 주소를 저장하고, 이를 통해 다양한 메모리 영역을 접근할 수 있다
- 포인터는 하드웨어 장치에 접근하는 데도 사용될 수 있다
- 예를 들어, 임베디드 시스템에서는 포인터를 사용하여 특정 하드웨어 레지스터에 접근할 수 있다
동적 메모리 관리
- 일반적인 변수는 프로그램이 실행될 때 크기가 고정되지만, 포인터를 사용하면 필요할 때 메모리를 할당하고 해제할 수 있다
- 예를들어, 사용자가 입력한 크기에 따라 배열을 동적으로 생성하려면 포인터가 필요
- 포인터가 없다면 미리 정해진 크기의 변수를 사용해야해서 메모리가 낭비될 가능성이 있고, 메모리가 부족할 수도 있다
대용량 데이터 처리
- 배열이나 구조체를 다룰 때, 포인터를 사용하면 메모리를 절약하고, 속도를 최적화할 수 있다
- 예를 들어, 1000개의 정수를 저장하려면 정적 배열을 선언할 수도 있지만, 필요할 때만 메모리를 할당하는 것이 더 효율적
- 포인터가 없다면 프로그램이 실행될 때 무조건 1000개의 공간이 확보되서 불필요한 메모리가 낭비될 가능성이 있다
- 포인터를 사용하면 필요할 때만 할당하여 효율적인 메모리 사용이 가능하다
함수에서 원본 데이터 수정
- C++에서 함수의 기본 매개변수 전달 방식은 값에 의한 전달, 즉, 변수의 복사본이 함수로 전달되기 때문에 원본 데이터는 반영되지 않는다
- 하지만 포인터를 사용하면 원본 데이터를 수정할 수 있다
- 포인터가 없다면 함수가 변수를 수정해도, 원본 값은 그대로 유지되어서 비효율적
효율적인 데이터 구조 구현
- 포인터는 연결 리스트, 트리, 그래프 등의 동적 데이터 구조를 구현하는데 필수적이다
- 배열은 크기가 고정되어 있지만, 포인터를 사용하면 필요할 때 새로운 노드를 추가하거나 제거 가능
- 포인터가 없다면 크기가 고정된 배열을 사용해야 해서 유연성이 떨어지고, 삽입/삭제가 어렵다
스마트 포인터를 사용한 자동 메모리 관리(C++11 이후)
- 기존 new와 delete를 사용하면 메모리 해제를 직접 관리해야 하므로 실수할 가능성이 있다
- 스마트 포인터를 사용하면 자동으로 메모리를 관리하여 메모리 누수를 방지할 수 있다
- 자동으로 메모리가 해제되므로 안정적인 프로그램 개발이 가능
유의사항
- 포인터는 강력한 도구이지만, 올바르게 사용하지 않으면 프로그램이 예기치 않은 동작을 하거나 심각한 버그를 일으킬 수 있다. 특히 아래 세 가지 주요 문제를 주의해야 한다
초기화되지 않은 포인터(Oninitialized Ptr)
- 초기화되지 않은 포인터는 가리키는 주소가 정해지지 않아, 예측할 수 없는 동작을 초래할 수 있다
해결 방법
- 초기값을 nullptr로 설정하여 안정성을 확보
int* ptr = nullptr;
- 즉시 유효한 메모리 주소를 할당
int a = 10;
int* ptr = &a;
- 메모리를 동적으로 할당할 경우 반드시 유효한 주소를 부여
int* ptr = new int(10);
메모리 릭(Memory Leak)
- 동적으로 할당된 메모리를 적절히 해제하지 않아, 사용되지 않는 메모리가 계속 남아있는 현상
- 장시간 실행되는 프로그램에서는 심각한 성능 문제를 초래할 수 있다
해결 방법
- 동적으로 할당된 메모리는 꼭 delete 또는 delete[]로 해제
int* ptr = new int(100);
delete ptr;
- 배열의 경우 delete[] 사용
int* arr = new int[10];
delete[] arr;
- 스마트 포인터(std::unique_ptr, std::shared_ptr) 활용
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(100);
허상 포인터(Dangling Ptr)
- 이미 해제된 메모리를 참조하려고 할 때 발생하는 문제
- 프로그램이 비정상적으로 동작하거나, 심각한 보안 취약점을 초래할 수 있다
해결 방법
- 포인터 해제 후 nullptr로 설정하여 다시 접근하지 못하도록 방지
int* ptr = new int(42);
delete ptr;
ptr = nullptr;
- 스마트 포인터 사용(자동 메모리 관리)
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);
String(문자열)
String이란?
- 문자의 집합을 의미한다
- 표현방식으로는 2가지가 있다
- char 배열(char array)
- std::string 클래스(class string)
C 스타일 문자열
- C 스타일 문자열은 char 배열로 표현되며, 문자열 끝을 알리기 위해 널 종료 문자('\0')가 반드시 포함되어야 한다
- C 스타일 문자열을 다룰 때는 <cstring> 헤더를 사용한다.
char s[] = "Hello"; // 크기: 6 (H e l l o \0)
char *S = "Hello"; // 문자열 리터럴 (널 문자 포함)
주요 함수
- strlen(str) : 문자열의 길이(널 문자는 제외)
- strcat(str1, str2) : str1에 str2를 이어 붙임
- strcpy(str1, str2) : str1에 str2를 복사
- strstr(str1, str2) : str1에서 str2가 시작하는 위치 반환
- strchr(str, char) : str에서 char가 처음 등장하는 위치 반환
- strrchr(str, char) : str에서 char가 마지막으로 등장하는 위치 반환
- strcmp(str1, str2) : 문자열 비교 (0: 같음, 양수: str1 > str2, 음수: str1 < str2)
- strtol(str, nullptr, 10) : 문자열을 long 타입으로 변환
- strtof(str, nullptr) : 문자열을 float 타입으로 변환
- strtok(str, delim) : 문자열을 특정 구분자(delim) 기준으로 분리
C++ 스타일 문자열
- std::string 클래스를 사용하여 문자열을 다룰 수 있다
- 이는 char 배열보다 더 편리하고, 강력한 기능을 제공(메모리 관리가 쉬워지고, 코드가 더 간결)
#include <iostream>
#include <string>
using namespace std;
int main() {
string s = "Hello"; // 크기: 5 (널 문자 포함 X)
cout << "문자열: " << s << endl;
cout << "길이: " << s.length() << endl;
return 0;
}
주요 함수
- s.length() : 문자열 길이 반환
- s.size() : length()와 동일
- s.capacity() : 현재 할당된 메모리 크기 반환
- s.resize(n) : 문자열 크기를 n으로 변경
- s.max_size() : 저장 가능한 최대 크기 반환
- s.clear() : 문자열 비우기
- s.empty() : 문자열이 비었는지 확인 (비었으면 true)
- s.append(str) : 문자열 끝에 str 추가
- s.insert(pos, str) : pos 위치에 str 삽입
- s.replace(pos, len, str) : pos부터 len 길이의 문자열을 str로 변경
- s.erase(pos, len) : pos부터 len 길이의 문자열 삭제
- s.push_back(char) : 문자열 끝에 문자 추가
- s.pop_back() : 문자열 끝 문자 제거
- s.swap(str) : 두 문자열 내용 교환
- s.copy(char_array, len, pos) : pos부터 len 길이만큼 char_array에 복사
- s.find(str) : str이 처음 등장하는 위치 반환
- s.rfind(str) : str이 마지막으로 등장하는 위치 반환
- s.find_first_of(chars) : chars에 포함된 문자 중 첫 번째 등장 위치 반환
- s.find_last_of(chars) : chars에 포함된 문자 중 마지막 등장 위치 반환
- s.substr(pos, len) : pos부터 len 길이만큼 문자열 반환
- s.compare(str) : 문자열 비교 (strcmp와 유사)
반복자
- std::string은 반복자를 사용하여 문자열을 순회할 수 있다
string s = "Hello";
for (string::iterator it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
// 출력: H e l l o
주요 함수
- begin() : 문자열의 시작 반복자 반환
- end() : 문자열 끝 반복자 반환
- rbegin() : 역순 시작 반복자 반환
- rend() : 역순 끝 반복자 반환