C++ | 메모리와 포인터

heige·2023년 12월 27일

CPP

목록 보기
4/12
post-thumbnail

메모리와 포인터

메모리

컴퓨터의 메모리는 메모리 셀의 연속과 같으며 각 셀의 크기는 1바이트이고 16진수로 이뤄진 고유한 주소가 있다.

int i;

이 코드를 작성하면 메모리에서 4바이트 정수 타입인 int타입의 변수를 저장하기 위해 4바이트 메모리 영역을 예약한다.

이렇게 예약한 메모리 영역인 0x0000, 0x0001.. 등의 주소가 가리키는 영역에 int i라는 변수가 들어가게 된다. 변수의 메모리 주소는 변수가 사용하는 메모리 주소의 첫번째를 가리킨다.

#include <bits/stdc++.h>
using namespace std;
int i;
int main(){
  cout << &i << '\n';
  i = 0;
  cout << &i << '\n';
  return 0;
}

i에 0을 할당하면 다음 그림처럼 방금 예약한 메모리 영역에 해당 값을 저장하게 된다. 0이든 2든 3이든 다른 값을 넣어도 주소는 변하지 않는다.

포인터

메모리 관리는 언어마다 조금은 다르게 관리가 된다. 자바, 파이썬, 자바스크립트라는 언어로는 개발자가 직접 변수에 메모리를 할당하거나 해제할 수 없고 가비지컬렉터를 통해 이를 수행한다.
하위레벨 언어인 C, C++ 등은 가비지컬렉터가 없으며 대신 개발자가 직접 필요한 메모리를 예약하고 해제할 수 있으며 포인터 또한 지원한다.

포인터의 개념

  • 포인터는 변수의 메모리 주소를 담는 타입이다.
  • 포인터는 메모리 동적할당, 데이터를 복사하지 않고 함수 매개변수로 사용, 클래스 및 구조체를 연결할 때 사용된다.

ex) 연결리스트의 노드

class Node { 
	public:
	int data;
    Node* next;
};

next는 다음 노드의 주소값을 가리키는 포인터다. 데이터를 다 가지고 있지 않아도 주소값만 가지고 데이터를 참조할 수 있다. 포인터는 아래와 같이 선언한다.

<타입> * <변수명> = <해당 타입의 변수의 주소>

다음 코드를 보면 int * a라는 &i라는 i의 주소를 담는 포인터를 정의했다. <타입> * 형태로 포인터를 정의한다. 예를 들어 string 타입 변수의 메모리주소를 담을 때는 string * 하고 선언을 해야 한다.(주소를 담고자 하는 변수의 자료형 타입과 일치하도록 맞춰줘야 함)
C++에서 *라는 별표는 에스터리스크(asterisk operator)라고도 불린다.

#include<bits/stdc++.h>
using namespace std; 
int i;
string s = "kundol"; 
int main(){
  i = 0;
  int * a = & i; 
  cout << a << '\n'; 
  string * b = &s; 
  cout << b << '\n';
  return 0;
}

포인터의 크기

포인터의 크기는 OS가 32bit라면 4바이트, 64bit라면 8바이트로 고정되어 있다. 어떠한 타입이든(string, char, int 등) 상관없이 무조건 4바이트 아니면 8바이트로 고정된다. 이는 집 주소의 크기는 집의 크기와 관련이 없다는 것을 생각하면 됨

역참조 연산자

C++에서 *은 기호는 사용하는 위치에 의해 다양한 용도로 사용된다.
1. 이항 연산자로 사용하면 곱셈 연산으로 사용
2. 포인터 타입의 선언
3. 역참조(dereference)로 메모리를 기반으로 변수의 값에 접근할 때도 사용

b라는 포인터를 정의했고 이 포인터에 * 연산자를 통해 역참조를 걸어 주소값을 기반으로 값을 다시 알아냈다.

#include<bits/stdc++.h>
using namespace std; 

int main(){
  string a = "abcda"; 
  string * b = &a; 
  cout << b << "\n"; 
  cout << *b << "\n"; 
  return 0;
}
/*
0x16fa72e60
abcda
*/


주소값을 담은 포인터에 역참조연산자를 통해 값을 참조할 수 있다.

array to pointer decay

배열의 이름을 주소값으로 쓸 수 있는 것은 'array to pointer decay'를 의미한다. 배열이 포인터로 부식(decay)되는 현상을 말한다.
포인터에다가 배열의 이름을 할당할 수 있다.
T[N]이라는 배열의 이름을 T*라는 포인터에 할당하면서 T[N]이란 배열의 크기 정보 N이 없어지고(decay) 첫 번째 요소의 주소가 바인딩 되는 현상을 의미한다. 그것과 동시에 T의 첫 번째 주소가 이름에다가 바인딩 된다.

배열의 크기 정보가 있었는데 없어진다..!ㄷㄷ

#include<bits/stdc++.h>
using namespace std; 
int a[3] = {1, 2, 3}; 
int main(){
  int * c = a;
  cout << c << "\n"; //배열의 첫번째 값의 주소 반환
  cout << &a[0] << "\n"; 
  cout << c + 1 << "\n"; //그 다음 주소 반환
  cout << &a[1] << "\n"; 
  return 0;
}
/*
0x10429c000
0x10429c000
0x10429c004
0x10429c004
*/

이를 통해 배열의 이름은 배열의 첫 번째 주소로써 쓸 수 있다. (vector는 안 되고 array만 가능!)

프로세스 메로리 구조와 정적할당과 동적할당

코드를 작성해서 컴파일하고 프로그램을 실행시킬 때 어떠한 구조로 메모리에 할당이 되는가?
운영체제는 다음의 구조를 기반으로 프로세스에 적절한 메모리를 할당한다.

스택은 위 주소부터 할당되고 힙은 아래 주소부터 할당된다

  • 스택 : 지역변수, 매개변수, 함수가 저장되고 컴파일 시에 크기가 결정된다. 함수가 함수를 호출 하는 등에 따라 런타임시에도 크기가 변경된다. - 동적
  • 힙 : 힙은 동적할당할 때 사용되며 런타임시 크기가 결정된다. - 동적
  • 데이터 영역 : BSS 영역과 Data 영역으로 나뉘고 정적할당에 관한 부분을 담당한다. - 정적
  • 코드 영역 : 소스코드가 해당된다. - 정적

정적할당

정적할당은 컴파일단계에서 메모리를 할당하는 것을 말한다.

  • BSS segment에는 전역변수, static, const로 선언되어있는 변수 중 0으로 초기화 또는 초기화가 어떠한 값으로도 되어 있지 않은 변수들이 할당된다.
  • Data segment에는 전역변수, static, const로 선언되어있는 변수 중 0이 아닌 값으로 초기화된 변수가 할당된다.
  • code / text segment는 프로그램의 코드가 들어간다.

동적할당

동적할당은 런타임단계에서 메모리를 할당받는 것이며 Stack과 Heap으로 나눠진다.

  • Stack은 지역변수, 매개변수, 실행되는 함수에 의해 늘어나거나 줄어드는 메모리 영역이다. 함수가 호출될 때마다 호출될 때의 환경 등 특정 정보가 stack에 계속해서 저장된다.
    또한, 재귀함수가 호출된다고 했을 때 새로운 스택 프레임이 매번 사용되기 때문에 함수 내의 변수 집합이 해당 함수의 다른 인스턴스 변수를 방해하지 않는다.
    즉, 재귀함수 내의 지역변수로 선언하게 되면 해당 변수는 독립적으로 작용하며 다른 함수에 있는 변수에 영향을 끼치지 않는다.

  • Heap은 동적으로 할당되는 변수들을 담는다.
    malloc(), free() 함수를 통해 관리할 수 있으며 동적으로 관리되는 자료구조의 경우 Heap영역을 사용한다.
    예를 들어 vector는 내부적으로 heap영역을 사용한다.


📝 큰돌의터전 <10주완성 C++ 코딩테스트>

profile
웹 백엔드와 클라우드 정복을 위해 탄탄한 기반을 쌓아가고 있는 예비개발자입니다. 'IT You Up'은 'Eat You Up'이라는 표현에서 비롯되어, IT 지식을 끝까지 먹어치운다는 담고 있습니다.

0개의 댓글