C++ 포인터, 문자열(String)

윤준혁·2025년 2월 24일

포인터

포인터란?

  • 메모리 주소를 저장하는 변수
  • C++에서 포인터를 사용하면 동적 메모리 할당, 배열 조작, 효율적인 함수 호출 등을 수행할 수 있다

선언

  • * 기호를 사용하여 선언
int* ptr;  // 정수형 포인터 선언
int num = 10;
int* ptr = #  // num의 주소를 ptr에 저장

연산

  • 포인터를 사용하면 주소에 저장된 값을 참조할 수 있다
#include <iostream>

int main() {
    int num = 10;
    int* ptr = &num;
    
    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 = &num;
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)

  • 초기화되지 않은 포인터는 가리키는 주소가 정해지지 않아, 예측할 수 없는 동작을 초래할 수 있다

해결 방법

  1. 초기값을 nullptr로 설정하여 안정성을 확보
int* ptr = nullptr;
  1. 즉시 유효한 메모리 주소를 할당
int a = 10;
int* ptr = &a;
  1. 메모리를 동적으로 할당할 경우 반드시 유효한 주소를 부여
int* ptr = new int(10);

메모리 릭(Memory Leak)

  • 동적으로 할당된 메모리를 적절히 해제하지 않아, 사용되지 않는 메모리가 계속 남아있는 현상
  • 장시간 실행되는 프로그램에서는 심각한 성능 문제를 초래할 수 있다

해결 방법

  1. 동적으로 할당된 메모리는 꼭 delete 또는 delete[]로 해제
int* ptr = new int(100);
delete ptr;
  1. 배열의 경우 delete[] 사용
int* arr = new int[10];
delete[] arr;
  1. 스마트 포인터(std::unique_ptr, std::shared_ptr) 활용
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(100);

허상 포인터(Dangling Ptr)

  • 이미 해제된 메모리를 참조하려고 할 때 발생하는 문제
  • 프로그램이 비정상적으로 동작하거나, 심각한 보안 취약점을 초래할 수 있다

해결 방법

  1. 포인터 해제 후 nullptr로 설정하여 다시 접근하지 못하도록 방지
int* ptr = new int(42);
delete ptr;
ptr = nullptr;
  1. 스마트 포인터 사용(자동 메모리 관리)
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>(42);

String(문자열)

String이란?

  • 문자의 집합을 의미한다
  • 표현방식으로는 2가지가 있다
    1. char 배열(char array)
    2. 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() : 역순 끝 반복자 반환

0개의 댓글