c,c++

[C언어] 포인터 및 메모리 정리

YoonJongSeok 2022. 8. 18. 19:04

포인터의 의미

포인터는 주소를 저장하는 것을 의미한다. 이것만 알고 있다면 헷갈릴 필요가 없다.

포인터의 용도, 유용성

포인터는 값들을 복사하는것이 아니라 해당 값을 가지고 있는 주소를 복사해오기 때문에 큰 용량의 데이터를 다룰 때 유용하다.

 

C언어에서 함수의 반환값은 하나이기 때문에 둘 이상의 반환을 하기 위해서는 포인터의 이용이 필수적이다. 반환한다기보다는 함수 내에서 원본을 수정하여 둘 이상을 반환하는 효과를 보는 것이다.

 

동적 메모리를 할당할 때 포인터가 필수이다. 동적 메모리 할당 시에 기존 정적 메모리와 할당하는 위치나 방식이 다르기도 하고 또 크기도 다르므로 꼭 알 필요가 있다.

 

연결리스트, 트리 등 데이터 구조에서 포인터의 이용이 필수이다.

 

역참조 연산자와 포인터 선언

포인터의 선언은

	int* num;

자료형* 변수이름으로 읽는 방식은 오른쪽에서 왼쪽이다

즉 변수 num은 포인터인데 int형이다 라고 읽는 것이다.

그러나 연산자가 무엇이 붙느냐에 따라 읽는 것이 다른데 우선순위는 보통 찾아서 보는 편이다.

https://dojang.io/mod/page/view.php?id=188 

 

C 언어 코딩 도장: 25.0 연산자 우선순위 알아보기

수학에서 35 + 1 * 2의 결과는 무엇일까요? 이 식에서는 덧셈 다음에 곱셈으로 나와있지만 곱셈이 덧셈보다 우선순위가 높으므로 72가 아니라 37이 됩니다. 즉 1 * 2를 먼저 계산해서 2가 나오고, 앞

dojang.io

포인터 연산자의 우선순위는 2번째로 만약 해당 연산자보다 더 빠른 우선순위의 연산자가 나온다면 처리 방식과 읽는 방식을 다르게 해야한다.

 

const int* p = &num1;
int const* p = &num1;

해당 변수도 읽는 법이 다르다. const는 값을 고정시키는 예약어인데 위의 두개는 고정시키는 대상이 다르다.

맨 윗줄은 p는 포인터인데 const int이다 int const로 읽어도 상관은 없다. 해당 의미는 주소의 값을 고정시킨다는 것이다.

두번째 줄은 p는 const 포인터인데 int형이다. 즉 주소를 고정시키는 것이다.

 

포인터 변수는 4byte로 어떤 자료형을 선언하여도 4byte 고정이다.

값에 의한 전달, 참조에 의한 전달

값에 의한 전달은 값을 복사해서 넘겨주는 것이고 참조에 의한 전달은 값을 가리키고 있는 주소를 복사해서 넘겨주는 것이다. 값에 의한 전달로 변수를 조작할 시에 원본 데이터는 따로 복사되어 있어서 변경되지 않지만 참조에 의한 전달로 변수를 조작하게 되면 원본 값이 변경되어 버린다.

참조에 의한 전달은 데이터에 간접적으로 접근한다고 말할 수 있다.

포인터와 배열

배열도 포인터로 볼 수 있는데

int num[] = {10,20,3};
&num // 배열의 시작 주소를 나타냄

으로 볼 수 있다.

다음 값으로 넘어가기 위해서는 num+1 을 하면 되는데 num+1 을 할 시에 num(주소) + sizeof(int)*1 을하게 되는 것이다. 해당 연산은 포인터에서 주로 하는 연산이다. 두 주소간의 연산에서는 뺄셈을 이용하는데 보통 해당 주소 사이에 몇 개의 자료가 저장되어 있는지 확인할 때 이용한다고 볼 수 있다. (num + 3) - (num) 주소로 봤을 때 sizeof(int)로 나눠줄 시에 데이터의 개수를 구할 수 있다.

*, /, + 의 경우는 두 주소간의 연산에서 이용하지 않는다.

함수의 매개변수로 1차원 배열, 2차원 배열

함수의 매개변수로 배열을 넣을 수 있는데 포인터로 넘겨줄 수도 있다. 그러나 함수에 배열을 넣어줘도 함수의 시작 위치만을 넘겨주기 때문에 함수 내에서 배열의 데이터 개수를 구할 수가 없다. 그래서 보통 데이터의 개수도 매개변수에 같이 넣어주는 편이다. 2차원 배열을 넘겨줄 때는

void test(int arr[][4], int col, int row)

이차원 배열로 넘겨주고 뒤의 row (가로) 값을 설정해줘야한다. 앞의 col (세로) 값은 숫자를 써줘도 무시된다. 그리고 배열의 크기를 나타내주는 col과 row를 매개변수로 받는다.

메모리 종류

메모리의 종류는 스택, 힙, 코드, 데이터가 있다. 데이터의 크기는 힙이 가장 크고 그 다음 스택이고 나머지는 비슷하거나 작거나 같다.

스택 메모리는 함수나 지역변수 등 정적 선언을 했을 때 해당 크기에 맞게 가져다 쓰는 메모리이다. 속도가 빠르지만 크기에 제한이 있기 때문에 함수 안에서 함수를 부르는 재귀호출 방식을 너무 많이 쓰게 된다면 런타임에러가 뜰 수 있다. 스택메모리는 할당과 해제가 자동이고 함수호출 완료 시 해제된다.

힙 메모리는 크기가 가장 크고 속도가 상대적으로 느리다. 힙 메모리는 동적할당으로 이용할 수 있는데 용량 제한 없이 자신의 컴퓨터가 수용할 수 있는 크기만큼 쓸 수 있다. 힙 메모리는 할당과 해제가 자동이 아니기 때문에 할당 후에 해제도 직접 해줘야한다.

코드 메모리는 실행할 코드가 저장되는 메모리이다. 오직 읽을수만 있다.

데이터 메모리는 전역변수, 정적변수가 저장되는 메모리이다. 프로그램이 시작되고나서 종료되어야 사라진다.

동적 할당, 정적 할당

정적할당은 함수나, 지역변수를 선언하고 정의했던 것을 이용하는 것으로 스택 메모리를 이용한다. 속도가 빠르고 메모리 누수 걱정없이 안정적이다. 그러나 스택 메모리 자체 크기에 제한이 있고, 메모리의 크기를 개발자 임의적으로 이용하기가 힘들기 때문에 용량 최적화에서 조금 어려움이 있다.

동적할당은 자바, C++ 같은 곳에서 new 키워드나 C에서 malloc 함수를 이용하거나 해서 할당을 받을 수 있다. 동적할당은 크기 제한이 없지만 속도가 느리다. 그렇지만 개발자가 원하는대로 크기를 조절해나갈 수 있기 떄문에 최적화 용도로 이용된다.

동적 할당 2차원 배열

동적할당을 받아서 2차원 배열처럼 쓸 수 있는데 malloc함수 구현 시 이름은

void* malloc(size_t size);

이런 식으로 되어있다. 그러므로 선언할 때 해당 자료형에 맞게 해주어야한다. 즉 형변환을 이용해서 선언을 해주어야한다.

동적할당을 하게되면 size만큼의 배열을 만든 것과 마찬가지이다. 2차원 배열을 만든다고 생각했을 때 주소들이 들어가는 1차원 배열이 size개수 만큼 만들어진 것이고 2차원 배열을 만드려면 1차원 배열에 또 주소 즉 배열들이 들어가야하는 것이다. 그렇다면 이중포인터가 필요하다.

int** num = (int**)malloc(sizeof(int*)*n)
for(int i = 0; i < n; i++){
	num[i]= (int*)malloc(sizeof(int)*n);
}

num 변수에 int* x n 짜리 크기의 1차원 배열(포인터나 배열이나) 을 선언한 것이고 해당 num은 2차원 포인터 배열이므로 주소를 담을 배열이라는 것이다. 그리고 각 배열의 인덱스를 돌면서 동적할당을 또 해주어야한다. 해당 배열은 값이 들어갈 것이므로 sizeof(int) * n 짜리의 크기를 넣어줘야한다. 그리고나서는 num[i][j] 방식으로 배열에 접근할 수 있다.

댕글링 포인터(dangling pointer)

동적할당을 하고 나중에 다 쓴 후에 해제를 한 데이터에 접근하게 될 시에 해당 문제를 댕글링 포인터라고 한다. 예를 들어 함수에서 동적할당을 하고 리턴을 하던 아니면 해당 동적할당을 한 데이터에 해제 후에 접근하려 하면 해당 메모리는 관리되어지지 않은 메모리이므로 대단히 위험할 수 있다.