본문 바로가기
GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~

[C/C++ 프로그래밍] 11. 포인터

by GDNGY 2023. 5. 16.

2023.05.16 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍] 10. 문자열

Chapter 11. 포인터

C/C++ 프로그래밍에서 핵심적인 개념인 포인터에 대한 깊이 있는 이해를 제공합니다. 포인터의 개념부터 선언, 초기화, 그리고 포인터와 함께 작동하는 변수, 함수 등에 대해 설명합니다. 이 장에서는 포인터 연산과 다중 포인터의 활용, 그리고 동적 메모리 할당과의 관계에 대해서도 배울 수 있습니다. 다양한 예제코드와 주석을 통해 이론적인 지식을 실제 코드에 적용하는 방법을 보여줍니다.

 

반응형

 


11.1 포인터의 이해

포인터의 기본 개념에 대해 배웁니다. 메모리와 주소의 이해로 시작하여, 포인터의 정의, 그리고 포인터의 선언 및 초기화 방법에 대해 배웁니다. 이 부분은 포인터를 사용하려는 모든 프로그래머에게 꼭 필요한 기본적인 지식입니다. 이해를 돕기 위해 간단하고 직관적인 예제를 제공합니다.

11.1.1. 메모리와 주소

프로그램이 실행되면, 그 프로그램의 변수나 함수 등은 컴퓨터의 메모리에 저장됩니다. 각 메모리 공간은 고유한 주소를 가지고 있습니다. 이 주소를 이용해 특정 메모리 공간에 접근하거나 값을 저장할 수 있습니다. 이 주소는 숫자로 나타내지며, 이것이 '포인터'의 기본 개념입니다.

 

[예제]

int num = 20;  // 변수 num이 메모리 어딘가에 할당됩니다.
printf("%p\n", &num);  // 변수 num의 메모리 주소를 출력합니다.

 

11.1.2. 포인터란?

포인터는 간단히 말하면 '주소를 저장하는 변수'입니다. 메모리 주소를 담고 있으며, 이 주소를 통해 해당 메모리에 직접 접근할 수 있습니다. 포인터는 '*'를 사용하여 선언하며, 주소는 '&' 연산자를 사용하여 얻습니다.

 

[예제]

int num = 10;
int *ptr = #  // num의 주소를 ptr에 저장합니다.

cout << ptr << endl;  // ptr에 저장된 주소를 출력합니다.

 

11.1.3. 포인터의 선언 및 초기화

포인터는 일반적으로 다음과 같이 선언합니다: type *ptr_name;. 여기서 type은 포인터가 가리키는 변수의 타입이며, *는 이 변수가 포인터임을 나타냅니다. 포인터는 메모리 주소를 담기 때문에 초기화할 때 반드시 주소를 사용해야 합니다.

[예제]

int num = 20;
int *ptr = &num;  // num의 주소를 ptr에 저장하여 초기화합니다.

printf("%d\n", *ptr);  // *ptr을 사용하여 ptr이 가리키는 주소에 저장된 값을 출력합니다.

 

이렇게 포인터를 이해하고 사용하면 메모리에 직접 접근하거나, 함수에서 여러 값을 반환하거나, 동적 메모리 할당 등 다양한 기능을 구현할 수 있습니다.

 


11.2. 포인터와 변수

포인터가 변수와 어떻게 상호작용하는지에 대해 배웁니다. 포인터는 주소를 저장하므로, 변수의 주소를 저장함으로써 해당 변수에 접근하거나 값을 변경할 수 있습니다. 이를 통해 우리는 직접적인 메모리 접근 및 관리, 함수에서 여러 값을 반환하는 등의 다양한 기능을 구현할 수 있게 됩니다.

11.2.1 변수의 주소와 포인터

포인터는 변수의 주소를 저장하는 특별한 변수입니다. 변수의 주소를 알면 그 변수를 가리키거나 접근할 수 있습니다. 아래의 예제는 변수와 포인터를 어떻게 사용하는지 보여줍니다.

 

[예제] C

#include <stdio.h>

int main() {
    int num = 10;
    int *pNum = &num;

    printf("num의 주소: %p\n", &num);
    printf("pNum에 저장된 값: %p\n", pNum);
    printf("pNum이 가리키는 값: %d\n", *pNum);

    return 0;
}

 

[예제] C++

#include <iostream>

int main() {
    int num = 10;
    int *pNum = &num;

    std::cout << "num의 주소: " << &num << std::endl;
    std::cout << "pNum에 저장된 값: " << pNum << std::endl;
    std::cout << "pNum이 가리키는 값: " << *pNum << std::endl;

    return 0;
}

 

위 코드에서는 num 변수에 10을 할당하고, 포인터 pNum에 num의 주소(&num)를 할당합니다. 이렇게 하면 pNum은 num의 주소를 가리키게 됩니다. *pNum을 통해 num의 값을 가져올 수 있습니다.

 

11.2.2. 포인터를 이용한 변수 값 변경

포인터는 변수의 값을 변경하는 데도 사용할 수 있습니다. 포인터를 통해 변수의 메모리 주소를 참조하면 원래 변수의 값을 변경할 수 있습니다.

 

[예제] C

#include <stdio.h>

int main() {
    int num = 10;
    int *pNum = &num;

    printf("변경 전 num의 값: %d\n", num);

    *pNum = 20;
    printf("변경 후 num의 값: %d\n", num);

    return 0;
}

 

[예제] C++

#include <iostream>

int main() {
    int num = 10;
    int *pNum = &num;

    std::cout << "변경 전 num의 값: " << num << std::endl;

    *pNum = 20;
    std::cout << "변경 후 num의 값: " << num << std::endl;

    return 0;
}

 

위 코드에서 *pNum = 20;라는 구문은 pNum이 가리키는 변수, 즉 num의 값을 20으로 바꾼다는 의미입니다. 이를 통해 포인터를 사용하여 변수의 값을 변경할 수 있습니다.

 

아래에는 좀 더 복잡한 예시가 있습니다. 이 예제에서는 함수 내에서 포인터를 사용하여 변수의 값을 변경합니다.

 

[예제] C

#include <stdio.h>

void changeValue(int *p) {
    *p = 30;
}

int main() {
    int num = 10;
    printf("변경 전 num의 값: %d\n", num);

    changeValue(&num);
    printf("변경 후 num의 값: %d\n", num);

    return 0;
}

 

[예제] C++

#include <iostream>

void changeValue(int *p) {
    *p = 30;
}

int main() {
    int num = 10;
    std::cout << "변경 전 num의 값: " << num << std::endl;

    changeValue(&num);
    std::cout << "변경 후 num의 값: " << num << std::endl;

    return 0;
}

 

changeValue 함수에 num의 주소를 전달하여 num의 값을 변경합니다. 이렇게 포인터를 사용하면 함수에서 원본 변수의 값을 직접 변경할 수 있습니다.

 

포인터를 통해 원본 변수의 값을 변경하는 것을 이해하는 것은 C/C++ 프로그래밍에서 중요한 부분입니다. 포인터를 사용하면 원본 데이터를 복사하지 않고도 그 데이터에 접근하고 변경할 수 있으므로, 프로그램의 효율성을 높일 수 있습니다. 이는 특히 대량의 데이터를 다루는 프로그램에서 중요하게 작용합니다.

 

다음은 포인터 연산에 대해 자세히 살펴보겠습니다. 이는 배열과 함께 사용될 때 포인터의 강력함을 표현하는 데 도움이 될 것입니다. 포인터의 본질적인 이해는 C/C++ 프로그래밍의 핵심적인 부분이므로 이를 잘 이해하는 것이 중요합니다.

 


11.3. 포인터 연산

포인터에 적용할 수 있는 다양한 연산에 대해 알아봅니다. 이는 포인터의 본질적인 이해에 깊게 관련되어 있습니다. 이 부분은 포인터를 이용하여 메모리 내의 다양한 위치를 접근하고 이동하는 방법을 배우는 과정입니다. 포인터 연산은 주로 배열과 함께 사용되어 프로그래밍의 효율성을 극대화하는데 도움을 줍니다. 특히 포인터 증감 연산, 포인터 간 연산, 그리고 포인터와 배열의 관계에 초점을 맞추어 이해하게 됩니다. 포인터 연산의 이해는 데이터 구조와 알고리즘, 시스템 프로그래밍 등 고급 주제에 진입하는 데 필요한 기본적인 도구입니다.

11.3.1. 포인터 증감 연산

포인터 증감 연산이란, 포인터 변수가 가리키는 주소를 증가시키거나 감소시키는 연산을 의미합니다. C/C++에서는 ++ 연산자를 이용해 주소 값을 1 증가시키거나, -- 연산자를 이용해 주소 값을 1 감소시킬 수 있습니다. 이때 1 증가 혹은 감소하는 것은 실제 메모리 주소의 1바이트가 아니라, 포인터가 가리키는 데이터 타입의 크기만큼 변하는 것을 의미합니다.


[예제]

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // p is pointing to the first element of arr

p++; // Now p is pointing to the second element of arr

 

위의 C 코드에서 p는 처음에 arr의 첫번째 원소를 가리키고 있습니다. p++를 실행하면, p는 arr의 두번째 원소를 가리키게 됩니다. 여기서 p의 값이 1 증가했다는 것은 p가 가리키는 주소가 'int'의 크기만큼 증가했다는 것을 의미합니다.

 

11.3.2. 포인터 간 연산

포인터 간의 연산은 주로 두 포인터 간의 차이를 구하는 데 사용됩니다. 같은 배열 내의 두 포인터의 차이는 그 사이에 있는 원소의 개수를 나타냅니다.

[예제]

int arr[5] = {1, 2, 3, 4, 5};
int* p1 = &arr[0]; // pointing to the first element of arr
int* p2 = &arr[3]; // pointing to the fourth element of arr

int diff = p2 - p1; // diff will be 3


위의 C++ 코드에서 p2 - p1의 결과는 3입니다. 이는 p1과 p2가 가리키는 배열 원소 사이에 3개의 원소가 있음을 의미합니다.

 

11.3.3. 포인터와 배열

포인터와 배열은 매우 밀접한 관계를 가지고 있습니다. 배열의 이름은 사실 배열의 첫번째 원소를 가리키는 포인터입니다. 따라서 포인터 증감 연산을 이용하면 배열의 모든 원소를 쉽게 접근할 수 있습니다.


[예제]

int arr[5] = {1, 2, 3, 4, 5};
for (int* p = arr; p != arr + 5; p++) {
    std::cout << *p << " "; // will print 1 2 3 4 5
}


위의 C++ 코드에서 포인터 p를 사용하여 arr의 모든 원소에 접근했습니다. 이는 포인터 증감 연산과 배열이 함께 사용될 때 발휘하는 강력한 기능입니다.


포인터 연산은 프로그래밍에서 중요한 도구로, 이를 잘 이해하고 사용하면 메모리에 대한 더욱 효율적인 제어가 가능합니다. 다음 섹션에서는 포인터를 이용한 함수 호출 방식에 대해 알아보겠습니다. 

 


11.4. 포인터와 함수

함수와 포인터가 어떻게 상호작용하는지에 대해 알아봅니다. 이 과정에서 함수 호출 방식, 포인터를 이용한 매개변수 사용, 그리고 함수 포인터에 대해 알아볼 것입니다. 이러한 지식은 C/C++에서 고급 프로그래밍 기법을 사용하는 데 중요한 부분입니다.

11.4.1. Call-by-value와 Call-by-reference

C/C++에서 함수 호출 방식에는 크게 두 가지가 있습니다. Call-by-value와 Call-by-reference입니다. Call-by-value는 값을 복사해서 함수에 전달하는 방식으로, 함수 내에서 해당 값을 변경하더라도 원래 값에는 영향을 미치지 않습니다.

 

[예제]

void updateValue(int value) {
    value = 10;
}

int main() {
    int value = 5;
    updateValue(value);
    printf("%d\n", value); // 출력: 5
    return 0;
}

 

반면에, Call-by-reference는 주소를 통해 값을 함수에 전달하는 방식입니다. 이 경우 함수 내에서 해당 값이 변경되면 원래 값도 바뀝니다. 이 때 주소 전달을 위해 포인터를 사용합니다.

 

[예제]

void updateValue(int* value) {
    *value = 10;
}

int main() {
    int value = 5;
    updateValue(&value);
    printf("%d\n", value); // 출력: 10
    return 0;
}

 

11.4.2. 함수에 포인터 매개변수 사용

포인터는 함수의 매개변수로 넘겨질 수 있습니다. 이는 원본 변수의 메모리 주소를 전달함으로써 원본 변수에 대한 수정을 가능하게 합니다. 이는 Call-by-reference의 기본 개념입니다.

위의 예제에서 updateValue 함수의 매개변수는 int 타입의 포인터입니다. main 함수에서 value의 주소를 updateValue 함수에 전달하면, updateValue 함수 내에서 해당 주소를 따라가 value의 값을 직접 변경할 수 있습니다.

포인터를 사용한 이런 방식은 함수에서 여러 개의 값을 반환하고자 할 때 유용하게 사용됩니다. 예를 들어, 함수 내에서 두 변수의 값을 변경하고 싶다면, 두 변수의 주소를 포인터로 전달하여 함수 내에서 변경할 수 있습니다.

 

[예제]

void updateValues(int *pValue1, int *pValue2) { 
    *pValue1 = 10;
    *pValue2 = 20; 
} 

int main() { 
    int value1 = 5, value2 = 6; 
    updateValues(&value1, &value2); 
    printf("%d, %d\n", value1, value2);  // 출력 결과는 10, 20
    return 0; 
}

 

이 예제에서 updateValues 함수는 두 개의 int 타입 포인터를 매개변수로 받아 각각 참조하는 변수의 값을 변경합니다. 이처럼 포인터를 이용하면 함수에서 여러 값을 동시에 변경할 수 있습니다.

 

이러한 특성 덕분에 포인터는 배열과 같은 큰 데이터 구조를 함수에 전달할 때, 데이터의 복사 없이 참조만을 전달하여 성능을 향상시키는 데에도 사용됩니다. 즉, 데이터의 복사를 최소화하면서도 데이터를 수정할 수 있는 방법을 제공합니다.

 

그러나 포인터를 사용할 때는 주의가 필요합니다. 포인터가 가리키는 메모리 주소가 실제로 유효한지, 예상치 못한 메모리 영역을 변경하거나 접근하지 않는지 항상 확인해야 합니다. 이렇게 포인터를 통한 메모리 접근은 C/C++의 강력한 기능 중 하나지만, 동시에 주의 깊게 사용해야 하는 도구입니다.

 

11.4.3. 함수 포인터

함수 포인터는 그 이름이 의미하듯이, 함수를 가리키는 포인터입니다. 즉, 함수의 주소를 저장할 수 있으며, 이를 통해 동적으로 함수를 호출하는 것이 가능해집니다. 함수 포인터의 선언은 다음과 같습니다:

 

[예제]

return_type (*pointer_name)(parameter_types);

 

예를 들어, int를 반환하고 int 두 개를 매개변수로 받는 함수의 포인터는 다음과 같이 선언합니다:

 

[예제]

int (*func_ptr)(int, int);

 

이제 func_ptr는 이와 같은 서명을 가진 함수의 주소를 저장할 수 있습니다.

[예제]

int add(int a, int b) {
    return a + b;
}

int main() {
    int (*func_ptr)(int, int) = add;
    printf("%d\n", (*func_ptr)(2, 3));  // 출력 결과는 5
    return 0;
}


이 예제에서 func_ptr는 add 함수를 가리키며, (*func_ptr)(2, 3)을 통해 add 함수를 호출합니다.

 

함수 포인터의 중요한 용도 중 하나는 콜백 함수입니다. 콜백 함수는 다른 함수의 매개변수로 전달되어 그 함수 내부에서 호출될 함수를 의미합니다. 이를 통해 코드의 재사용성을 높이고, 구조화된 프로그래밍을 가능하게 합니다.

 

[예제]

void apply(int* arr, int size, int (*func)(int)) {
    for(int i = 0; i < size; ++i) {
        arr[i] = func(arr[i]);
    }
}

int square(int x) {
    return x * x;
}

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    apply(arr, 5, square);
    for(int i = 0; i < 5; ++i) {
        printf("%d ", arr[i]);  // 출력 결과는 1 4 9 16 25
    }
    return 0;
}

 

이 예제에서 apply 함수는 배열의 모든 원소에 함수를 적용하는 함수입니다. square 함수를 매개변수로 전달하여 배열의 모든 원소를 제곱합니다.

 

이와 같이, 포인터를 이해하고 사용하는 것은 C/C++ 프로그래밍에 있어 중요한 개념입니다. 다음 섹션에서는 다중 포인터에 대해 배워보도록 하겠습니다.

 


11.5. 다중 포인터

다중 포인터란 포인터를 가리키는 포인터를 의미합니다. 다중 포인터를 사용하면, 한 단계 이상의 간접 참조를 통해 데이터에 접근할 수 있습니다. 이 과정에서 다중 포인터의 선언, 초기화, 그리고 이를 활용한 예제 코드를 다룰 것입니다. 이를 이해하면 보다 복잡한 코드 구조를 효과적으로 관리하고 이해하는 데 도움이 될 것입니다.

11.5.1. 다중 포인터의 이해 및 선언

다중 포인터는 포인터를 가리키는 포인터를 의미합니다. 이는 복잡하게 들릴 수 있지만, 기본적인 포인터의 개념을 이해한다면 쉽게 접근할 수 있습니다.

 

일반적인 포인터가 특정 변수의 주소를 저장하고 있어 그 변수를 가리킨다면, 다중 포인터는 또 다른 포인터의 주소를 저장하고 있어 그 포인터를 가리킵니다.

 

다중 포인터는 아래와 같이 선언됩니다:

 

[예제]
int **pp;

이 코드에서 'pp'는 int형 포인터를 가리키는 포인터라고 해석할 수 있습니다. 이것은 pp가 int형 포인터의 주소를 저장할 수 있다는 것을 의미합니다.

다음은 이중 포인터의 간단한 사용 예입니다:

[예제] C

int num = 5;
int *p = &num;
int **pp = &p;

printf("num의 값: %d\n", num);
printf("*p의 값: %d\n", *p);
printf("**pp의 값: %d\n", **pp);

 

[예제] C++

int num = 5;
int *p = &num;
int **pp = &p;

std::cout << "num의 값: " << num << std::endl;
std::cout << "*p의 값: " << *p << std::endl;
std::cout << "**pp의 값: " << **pp << std::endl;

 

이 예제에서 'num'은 정수 변수이고, 'p'는 'num'의 주소를 가리키는 포인터입니다. 그리고 'pp'는 'p'의 주소를 가리키는 포인터입니다. 그래서 '**pp'를 출력하면 원래 변수인 'num'의 값이 출력됩니다.

 

이처럼 다중 포인터는 주소를 통해 변수를 여러 단계 거쳐 가리키는 것을 가능하게 합니다. 처음에는 이해하기 조금 복잡할 수 있지만, 포인터의 주소를 가리키는 포인터라는 것을 기억하면 도움이 될 것입니다. 그리고 이런 개념은 3중 포인터, 4중 포인터 등으로 확장될 수 있습니다.

 

하지만 다중 포인터를 사용할 때 주의해야 할 점이 있습니다. 다중 포인터를 선언했다고 해서 자동으로 메모리가 할당되지는 않습니다. 따라서 포인터에 대한 메모리를 할당하거나, 이미 존재하는 주소를 대입해야 합니다. 메모리를 제대로 할당하거나 초기화하지 않은 상태에서 포인터를 사용하면 예기치 않은 결과를 초래할 수 있으니 주의해야 합니다.

 

11.5.2. 다중 포인터의 활용

다중 포인터는 주로 동적으로 할당된 2차원 배열, 함수 포인터 배열 등 복잡한 데이터 구조에 사용됩니다. 또한 다중 포인터는 함수에서 주소를 직접 수정하거나 반환하는 등의 작업을 할 때 유용하게 사용됩니다.

 

  • 동적 2차원 배열 : C/C++에서는 1차원 배열만큼 직관적이지 않지만, 다중 포인터를 이용하여 동적으로 2차원 배열을 생성하고 접근할 수 있습니다.


[예제] C

int row = 5, col = 4;
int **arr = (int**)malloc(sizeof(int*) * row);
for(int i = 0; i < row; i++)
    arr[i] = (int*)malloc(sizeof(int) * col);

[예제] C++

int row = 5, col = 4;
int **arr = new int*[row];
for(int i = 0; i < row; i++)
    arr[i] = new int[col];

 

위의 코드에서 arr는 int형 포인터를 가리키는 포인터로, arr[i]는 i행을 가리키는 int형 포인터입니다. 이렇게 만들어진 2차원 배열은 arr[i][j]와 같이 접근할 수 있습니다.

 

다만 이런 식으로 메모리를 동적으로 할당한 후에는 반드시 사용이 끝난 후에 free (C) 혹은 delete (C++)를 사용하여 메모리를 해제해야 합니다.

 

  • Call by reference : 다중 포인터는 함수의 매개변수로 전달되어, 함수 내부에서 원래의 포인터 변수에 대한 변경을 가능하게 합니다. 이를 'call by reference'라고 부릅니다.

예를 들어, 아래의 예제는 다중 포인터를 이용하여 함수 내에서 동적 메모리를 할당하는 방법을 보여줍니다.

[예제] C

void allocate(int **p, int size) {
    *p = (int*)malloc(sizeof(int) * size);
}

int main() {
    int *arr = NULL;
    allocate(&arr, 5);
    free(arr);
    return 0;
}


[예제] C++

void allocate(int **p, int size) {
    *p = new int[size];
}

int main() {
    int *arr = nullptr;
    allocate(&arr, 5);
    delete[] arr;
    return 0;
}

 

위의 코드에서 allocate 함수는 int형 포인터를 가리키는 포인터를 매개변수로 받아, 그 주소에 동적 메모리를 할당합니다. 이렇게 하면 함수 내부에서 원래의 포인터 변수에 대한 변경이 가능해집니다.

 

이처럼 다중 포인터는 복잡한 문제를 해결하는데 유용하게 사용될 수 있습니다. 다만 사용 시 주의사항이 있으니, 자세히 알아보고 사용해보세요.

 


11.6 동적 메모리 할당과 포인터

프로그램이 실행 중에 필요한 만큼의 메모리를 할당하고 해제하는 방법을 배웁니다. C/C++에서는 'malloc', 'free' 함수(C), 'new', 'delete' 연산자(C++)를 사용해 동적 메모리를 관리합니다. 또한, 동적으로 할당된 메모리를 가리키는 포인터와 이 포인터를 활용한 자료구조(연결 리스트 등)에 대한 이해를 집중적으로 다룹니다. 이를 통해 프로그램의 효율성과 유연성을 높일 수 있습니다.

11.6.1. 동적 메모리 할당의 이해

동적 메모리 할당에 대해 알아보겠습니다. "동적"이라는 단어는 프로그램이 실행되는 도중에 변할 수 있다는 의미를 가지고 있습니다. 즉, 동적 메모리 할당은 프로그램 실행 중에 필요한 만큼의 메모리를 할당하는 방법을 의미합니다.

 

동적 메모리 할당은 프로그램이 필요한 만큼의 메모리를 요청하고 사용 후에 반환하는 것을 의미하며, 이를 가능하게 하는 것이 '힙(Heap)'이라는 메모리 영역입니다. 힙은 운영체제가 관리하며, 프로그램은 이 영역에서 필요한 만큼의 메모리를 할당받아 사용할 수 있습니다.

 

C언어에서는 malloc 함수를 사용하여 동적 메모리를 할당합니다. malloc 함수는 주어진 크기만큼의 메모리를 할당하고, 그 메모리의 시작 주소를 반환합니다.

 

[예제] C

#include <stdlib.h> // malloc 함수를 사용하기 위해 필요한 헤더 파일

int main() {
    int *ptr = (int*)malloc(sizeof(int)); // int 크기의 메모리를 동적으로 할당하고 그 주소를 ptr에 저장
    if (ptr != NULL) { // 메모리 할당이 제대로 이루어졌는지 확인
        *ptr = 10; // 동적으로 할당받은 메모리에 10을 저장
        printf("%d\n", *ptr); // 10
        free(ptr); // 동적으로 할당받은 메모리 반환
    }
    return 0;
}

 

C++에서는 new 연산자를 사용하여 동적 메모리를 할당하고, delete 연산자를 사용하여 메모리를 반환합니다.

[예제] C++

#include <iostream>

int main() {
    int *ptr = new int; // int 크기의 메모리를 동적으로 할당하고 그 주소를 ptr에 저장
    if (ptr != NULL) { // 메모리 할당이 제대로 이루어졌는지 확인
        *ptr = 10; // 동적으로 할당받은 메모리에 10을 저장
        std::cout << *ptr << std::endl; // 10
        delete ptr; // 동적으로 할당받은 메모리 반환
    }
    return 0;
}

 

11.6.2. malloc, calloc, realloc 함수

동적 메모리 할당을 이용하면 프로그램의 유연성이 향상되며, 메모리 사용 효율을 개선할 수 있습니다. 하지만 반드시 할당된 메모리는 사용 후에 반환해야 하며, 그렇지 않으면 메모리 누수가 발생하게 됩니다. 이 점을 잘 기억하시기 바랍니다.

 

동적 메모리 할당을 위해 C언어에서는 주로 malloc, calloc, realloc 등의 함수를 사용합니다.

 

malloc: malloc은 메모리 할당(Memory ALLOCation)을 의미하며, 지정된 바이트 크기만큼의 메모리를 할당한 후, 해당 메모리의 시작 주소를 반환합니다. 할당된 메모리의 초기값은 정해져 있지 않습니다.

 

[예제] C

#include <stdlib.h> // malloc을 사용하기 위해 필요한 헤더 파일

int main() {
    int *ptr = (int*)malloc(sizeof(int)*5); // int 크기의 메모리를 5개 할당하고 그 주소를 ptr에 저장
    if (ptr != NULL) { // 메모리 할당이 제대로 이루어졌는지 확인
        for (int i = 0; i < 5; i++) {
            ptr[i] = i; // 동적으로 할당받은 메모리에 값 저장
        }
        for (int i = 0; i < 5; i++) {
            printf("%d ", ptr[i]); // 0 1 2 3 4
        }
        free(ptr); // 동적으로 할당받은 메모리 반환
    }
    return 0;
}


calloc: calloc 함수는 "Contiguous ALLOCation"의 줄임말로, 지정된 수의 지정된 크기 메모리를 할당하고, 할당된 메모리를 모두 0으로 초기화한 후 그 메모리의 시작 주소를 반환합니다.

 

[예제] C

#include <stdlib.h> // calloc을 사용하기 위해 필요한 헤더 파일

int main() {
    int *ptr = (int*)calloc(5, sizeof(int)); // int 크기의 메모리를 5개 할당하고 0으로 초기화한 후 그 주소를 ptr에 저장
    if (ptr != NULL) { // 메모리 할당이 제대로 이루어졌는지 확인
        for (int i = 0; i < 5; i++) {
            printf("%d ", ptr[i]); // 0 0 0 0 0
        }
        free(ptr); // 동적으로 할당받은 메모리 반환
    }
    return 0;
}


realloc: realloc 함수는 "Reallocation"의 줄임말로, 이미 할당된 메모리 영역의 크기를 변경합니다. 변경 후의 크기가 원래의 크기보다 크다면, 추가적인 메모리 영역을 할당하며, 변경 후의 크기가 원래의 크기보다 작다면, 초과하는 메모리 영역을 해제합니다. 변경된 메모리의 주소를 반환합니다.


[예제] C

#include <stdlib.h> // realloc을 사용하기 위해 필요한 헤더 파일

int main() {
    int *ptr = (int*)malloc(sizeof(int)*5); // int 크기의 메모리를 5개 할당하고 그 주소를 ptr에 저장
    if (ptr != NULL) { 
        for (int i = 0; i < 5; i++) {
            ptr[i] = i; 
        }
        ptr = (int*)realloc(ptr, sizeof(int)*10); // 메모리 영역을 10개의 int 크기로 재할당
        for (int i = 5; i < 10; i++) {
            ptr[i] = i; // 추가로 할당받은 메모리에 값 저장
        }
        for (int i = 0; i < 10; i++) {
            printf("%d ", ptr[i]); // 0 1 2 3 4 5 6 7 8 9
        }
        free(ptr); // 동적으로 할당받은 메모리 반환
    }
    return 0;
}

 

이처럼 동적 메모리 할당은 프로그램의 메모리 사용 효율을 높이는 중요한 도구입니다. 필요에 따라 메모리를 할당하고 해제하여 메모리를 효율적으로 관리하실 수 있습니다. 다만, 반드시 사용한 메모리는 해제하여야 합니다. 이렇게 메모리 관리를 제대로 하지 않을 경우, 메모리 누수가 발생하여 프로그램의 성능에 영향을 미칠 수 있습니다. 이를 잘 기억하시기 바랍니다.

 

11.6.3. free 함수와 메모리 누수

동적 메모리 할당을 사용하면서 중요하게 봐야할 부분이 바로 메모리 누수와 그 해결 방법인 free 함수입니다.

 

  • free 함수 : 이 함수는 동적으로 할당된 메모리를 해제하는 역할을 합니다. 즉, 프로그램에서 더 이상 사용하지 않는 메모리를 운영 체제에 반환하여 메모리의 효율적인 사용을 돕습니다.

 

[예제] C

#include <stdlib.h> // free를 사용하기 위해 필요한 헤더 파일

int main() {
    int *ptr = (int*)malloc(sizeof(int)*5); 
    // 메모리 할당...
    free(ptr); // 동적으로 할당받은 메모리 반환
    ptr = NULL; // 반환 후, 포인터는 NULL로 초기화. 잘못된 메모리 접근을 막음
    return 0;
}


하지만 free 함수를 사용할 때는 주의할 점이 있습니다. 이미 해제된 메모리를 다시 해제하려고 시도하거나, 동적으로 할당되지 않은 메모리를 해제하려고 시도하는 등의 잘못된 동작을 수행하면, 런타임 오류가 발생할 수 있습니다. 따라서 메모리를 해제한 후에는 항상 해당 포인터를 NULL로 설정하여 이러한 문제를 방지하는 것이 좋습니다.

 

메모리 누수: 메모리 누수란 프로그램이 동적으로 할당한 메모리를 해제하지 않아, 더 이상 사용할 수 없는 메모리가 계속 늘어나는 현상을 말합니다. 이는 프로그램의 성능을 저하시키고, 심각한 경우 시스템 전체의 성능을 저하시키거나 시스템이 정상적으로 작동하지 못하게 할 수 있습니다.

 

[예제] C

int main() {
    int *ptr = (int*)malloc(sizeof(int)*5); 
    // 메모리 할당...
    // ... 
    // 메모리 해제를 하지 않음. 메모리 누수 발생
    return 0;
}


이처럼 동적 메모리 할당은 매우 효율적이지만, 반대로 메모리 누수와 같은 문제를 일으킬 수 있습니다. 따라서 동적으로 할당된 메모리는 반드시 free 함수를 이용해 해제해야 합니다. 동적 메모리 할당과 메모리 누수는 프로그래밍에서 중요한 개념이므로 꼭 기억하시기 바랍니다.

 

 

 

2023.05.16 - [GD's IT Lectures : 기초부터 시리즈/C, C++ 기초부터 ~] - [C/C++ 프로그래밍] 10. 문자열

 

[C/C++ 프로그래밍] 10. 문자열

Chapter 10. 문자열 C/C++ 프로그래밍에서 문자의 집합을 처리하는 방법에 대해 다룹니다. 이 챕터에서는 문자열의 개념, 선언, 초기화, 사용법, 그리고 문자열을 함수의 인자로 전달하는 방법과 문

gdngy.tistory.com

 

반응형

댓글