[C/C++] 포인터는 왜 배우는 것일까? (21.03.23)

Suh, Hyunwook·2021년 3월 23일
0
post-thumbnail

※ 해당 글은 포인터를 처음 배운 '비전공자'입장에서 쓴 것으로, 포인터를 왜 사용하는지를 적어본 글이며 꾸준한 '개정'이 필요하니, 고수분들은 언제든지 태클해주세요👐
감사합니다🙇‍♂️

먼저, 포인터란 메모리의 주소를 '참조'하는 변수이다.
예시) int a = 4;
intp = &a;
a = 4;

너무나도 뚱딴지 같은 추상적인 개념이었으며, 메모리에 변수를 선언하고 값을 저장하면 그만인 것을 왜 사용하는 지에 대해 의문을 가지게 되었으나, 점점 포인터의 쓸모를 알게 되었다.

1) Call by reference
함수를 사용하는 방법에는 일반적으로 Call by value와 Call by reference 두 가지 방법이 있는데, 전자는 함수에 변수 값을 넘겨주는 것이고, 후자는 함수에 주소를 넘겨주는 것이다.

Call by reference의 편의성은 Swap함수와 같은 부분에서도 찾을 수가 있다.

#include <iostream>
using namespace std;
void change(int a, int b) {

    int temp;
    temp = a;
    a = b;
    b = temp;

}
int main()
{
    int a = 3, b = 4;
    change(a, b);

    return 0;
}

위의 코드는 Call by value를 사용한 것으로, change 함수에 int형 변수 a와 b를 넘겨준 것인데, temp라는 임시 저장용 변수를 사용하여 값을 바꿔주었음에도 여전히 main 함수에서 값은 같게 처리된다.

이는 함수에 값을 넘겨줄 때, main함수의 변수를 복사하여 넘겨주기 때문에, 함수 '세계'의 변수와 main함수 세계의 변수가 달라, change함수를 마쳤음에도 변수가 바뀌지 않는다고 이해할 수 있다.

또한, 함수에서 main 함수로 return할 수 있는 value는 1개이기 때문에, 변수를 바꿔주는 함수를 만들기 어렵다

이에, call by reference는 좋은 해법이 된다.

#include <iostream>
using namespace std;
void change(int*a, int*b) {

    int temp;
    temp = *a;
    *a = *b;
    *b = temp;

}
int main()
{
    int a = 3, b = 4;
    change(&a, &b);

    return 0;
}

Call by reference에서는 함수에 a,b의 value가 아닌 주소값(reference)을 보낸다.
함수에서 주소를 바꾸게 되면, 연산 시 참조하는 주소가 달라지기 때문에 main 함수에서도 값이 달라지게 된다.

2) 링크드리스트 (Linkded List)

  • 배열
  • Linked List

링크드리스트를 구현하기 위해서는 포인터를 알아야 한다. 다음 노드가 연결된 이전 노드의 주소값을 저장하기 때문이다.
링크드리스트는 보통 배열과 비교가 되는데, 먼저 배열의 경우 다음과 같은 특징이 있다.

배열은 생성 시에 크기가 결정되고, 변경이 불가능한 고정적인 형태를 지닌다. 따라서 탐색 시에 유리하지만, 이러한 고정적인 특성 때문에 배열 중간에 있는 값을 삭제/삽입하기가 제한적이다. 중간에 삽입하기 위해서 뒤에 있는 값을 전부 밀어줘야 하며, 값의 삽입을 염두하여 배열 선언 시 사용되는 배열보다 넉넉하게 선언하므로, 메모리의 낭비가 있다.

예를 들어, 크기가 10인 배열인 arr[10]를 선언하고, 배열에 "apple"을 담는다고 해본다. 그 후, apple에 E를 중간에 넣어, appEle로 만드는 작업을 한다고 해보면, 배열에서는 다음과 같이 구현할 수 있다.

#include <iostream>
using namespace std;
int main()
{
    char arr[10] = "apple";
    char ch = 'E';

    for (int i = 4; i >= 3; i--) {
        arr[i + 1] = arr[i];
    }
    arr[3] = ch;

    return 0;
}

즉, E 뒤에 있는 l,e를 다음 칸으로 옮긴 뒤, E 값을 넣어줘야 하는 것이다. 이 경우, 뒤에 있는 값을 일일히 바꿔줘야 하는 번거로움이 있다.

그런데 만약, E가 아니라 다른 값, 예를 들어 A,B도 넣어줘야 하는 가능성이 있다면? 이 경우에는 배열을 애초에 크게 만들어서 이러한 상황에 대비해야하는 낭비가 있다.

NULL문자 포함 차지하는 데이터는 6인데도 불구하고 추가적인 데이터를 선언했기 때문이다.

이 경우, 포인터를 사용하여 링크드리스트를 구현하면 이러한 낭비를 막을 수가 있다.

다음과 같이, 노드(Node)가 '가리키는' 주소만 바꿔줌으로서, 위와 같은 번거로움을 막을 수가 있다.

예를 들어, 노드(Node) A,B,C가 있고, 'A → B → C'로 이어져있다고 가정해보자. 이 때, D 를 중간에 넣는다면,
'A → D → B → C' 와 같은 구조로 바꿀 수 있다. 또한, 이 경우 포인터로 노드 간의 연결을 구현할 수 있다.

#include <iostream>
using namespace std;
struct Node {
    int x;
    Node* next;
};
int main()
{
    Node a, b, c, d;
    a.x = 1, a.x = 2, a.x = 3, a.x = 4;
    

    return 0;
}

또한, 노드 간의 연결인 링크드리스트(Linked List)는 다음과 같이 구현할 수 있다

#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Node {
	int n;
	Node* next;
};
Node* head;
Node* last;
void addNode(int val) 
{
	if (head == NULL) {
		head = new Node();
		head->n = val;
		last = head;
	}
	else {
		last->next = new Node();
		last = last->next;
		last->n = val;
	}
}
int main() {

	addNode(1);
	addNode(2);
	addNode(3);
	addNode(4);
	addNode(5);

	return 0;
}

1개의 댓글

comment-user-thumbnail
2021년 3월 29일

많운 도움 되었습니다!!

답글 달기