본문 바로가기

C_Data Structure_Algorithm/C Basic Review

C언어 기초 복습 ( 배열, 포인터 , 문자열, 동적 메모리 할당 )

1) 메모리


컴퓨터 ''메모리''란, 커다란 테이블이고, **각 칸이 '주소'**를 갖는 것.

주소는. bite 단위로, 매겨진다

컴퓨터의 'ram' 즉, '메모리' 라는 것은, 1 bite 를 저장할 수 있는 공간이, 모여서, 위와 같이 커다란 테이블. 을 구성하는 것이라고 생각하면 된다.

각각의 칸은, '주소'를 가지고 있다. 위에서, 각 칸이 갖는 주소는 1000 번째 부터, 1009 번째 까지이다.

**모든 변수는 '주소'**를 가진다. 그리고, '변수'란, 데이터를 저장할 수 있는 '공간' 이다. 메모리의 일정한 '영역' 이다.

ex. int 형 변수를 설정하게 되면 , 위와 같이 4 byte 의 공간이 할당되고, 그 공간에 '정수값'이 씌어진다. / 그리고 int 형 변수의 '주소'는 1004 번지. 라고 할 수 있다.

메모리. 레이아웃.이란, 쉽게 말해서, 메모리의 구조이다.

우선 'code' 가 있고, 그 코드가 저장되는 영역을 code section 이라고 한다.

code section과 data section은 프로그램이 돌아가는 동안에는 변하지 않는다. 그 크기가 더 커지거나 작아지거나 하지는 않는다는 것이다.

왜냐면, 그것은 이미 프로그램 코드에 의해 정해진 것이니까.

stack 이라는 영역은 아래로 자랐다가, 줄어들었다가 한다.

stack 의 크기는 일정하지 않고

어떤 함수가 호출이 되고, 그 안에 지역변수들이 많다면 stack 이 커지고, 그 함수가 리턴되면, 그 지역 변수가 소멸되어 stack 도 작아진다.

malloc 과 할당되는 메모리들은 stack 과 반대에서, 커졌다가, 줄어들었다가 하는 것이다.

즉, 동적 메모리 할당을 하면 heap 은 커지고, free 를 통해 return 하면, 다시 작아지고.

2) 포인터


'포인터'도 일종의 '변수' 이다. ( 즉, '포인터'도, 메모리의 일정 영역을 차지하면서, 특정 '값'을 저장하는 장소 인 것이다. )

'포인터' 는 그러한 '변수' 중에서도, ' 메모리 주소' 를 값으로 가지는 변수

ex. 실수형 변수는 실수를 값으로 갖고, 정수형 변수는 정수를 값으로 갖듯이

그렇다면, '메모리주소'란 무엇인가. 바로 '정수'이다.

즉, '포인터'는 '정수'를 값으로 갖는 변수이고, 그 '정수'는 '메모리 주소' 인 것이다.

Q. int * ptr. 에서 int. 즉, '타입'의 의미는 무엇일까.

앞에 붙어있는 int , 즉, type 의 의미는,

이 ptr 이 포인터 변수이고, 그로 인해 '메모리 주소'를 저장을 하는데, 그 주소에 저장되는 데이터의 타입이 '정수'라는 것이다.

& 라는 것은, 뒤에 붙은 변수, 혹은 값의 '주소'를 추출하는 연산자이다.

&c 는 1008 번지의 주소.를 추출하는 것이고, 그것을 p 에 저장하는 것이다.

그렇다면, c 의 주소를 p 에 저장할 수 있는 이유가 무엇일까?

바로, p 가 메모리 주소를 저장하는 포인터 변수이기 때문이다.

그리고, 이제 p 에는 1008 이라는 값이 들어갔고,

변수 p는 1028 번지라는 주소에 할당이 된 것이다.

첫번째 그림


x = 1, y = 2. 가 변수 x,y 에 저장되었다.

각각은 1028 번지, 1024 번지라는 주소에 할당이 된 것이다.

ip 라는 변수는 1008번지에 할당되었다고 하자.

ip = & x

x 변수의 주소를 ip 변수 값에 할당하라는 것이다. 이로 인해, ip 에는 x 의 주소, 즉, 1028 이라는 값을 갖게 되는 것이다.

어떤 문장에서 포인터 변수 ip 앞에, * 가 등장하면,

이 포인터 변수 ip 가 저장하고 있는 주소.

그 주소에 저장된 값.

혹은 그 주소가 참조하는 자리. 를 의미하게 된다.

여기서는 ip 는 x 의 주소를 저장하고 있고

x 에 저장된 값은 1 이다.

y = * ip. 를 하게 되면, y 에 1 이라는 값을 할당하게 된다.

*ip = 0 을 하게 되면,

이는 곧, x 의 값에 0 을 할당하는 것과 같은 의미가 되는 것이다.

3) 문자열


C 언어에서 포인터와 배열은 긴밀하게 연결되어 있다.

C에서 int a[10] 이런 식으로 선언을 한다.

a 는 배열의 이름. 그 배열의 크기는 10, 즉, 10 칸 짜리 배열, 그 배열의 타입은 int. 즉 '정수' 이다.

실제로, 포인터와 배열의 관계란,

int a [10 ] 을 선언하면, 위와 같은 구조가 만들어진다는 것이다.

일단, 정수형 배열, 10칸짜리, 길이가 10인 정수형 배열이 만들어진다.

이 배열의 이름은, 또 다른 변수가 되는 것이고,

이 변수는, 10칸 짜리 배열의 주소, 즉, 첫번째 칸의 주소.가 추가적으로 만들어지는 포인터 변수 a 에 저장이 된다.

즉, 배열의 이름은, 배열의 첫번째 칸의 주소를 저장하는 포인터 변수이다.

단, 이 변수는 값을 변경할 수 없다.

즉, 보통의 포인터 변수처럼, 수정을 할 수 없다는 것이다.

배열 num이 배열의 이름.

num은, num 배열의 첫번째 칸 주소를 저장하는 포인터 변수.

calculate_sum( num ) 을 통해, 전달하는 값은,

num 이라는 배열의 첫번째 칸의 '주소'를 저장하는 포인터 변수.

이렇게 배열의 이름을 매개변수로 전달하면,

배열의 첫번재 칸의 주소를 전달하는 것이다.

int calcultate_sum( in arrary[ ] ) 와 같이, array 배열로 받지 않고,

int calcultate_sum( in *arrary ) 와 같이, 정수형 포인터로 선언하더라도

완전히 똑같은 일이 일어난다.

포인터 변수에, 덧셈 등의 연산을 허용한다.

arithmetic .

a가 메모리 주소이니까, a 가 1000 이라는 정수값이면, 거기에 +1 을 할 수 있지

그런데 a 가 1000 번지 일때, 1 을 더하면 1001 이 되어야 할 것 같은데

여기서는 1004 가 된다.

왜 ??

a 가, 정수형 변수이고, 정수형 포인터 변수이고

여기 그림 상에서 정수형이 4byte 로 표현이 된다면,

이 경우, a + 1 은 1004 가 되어서, 그 다음 정수의 주소가 되도록,

즉, a[0]에서,주소값이 1000이었다면,

그 다음 정수 a[1] 의 주소가 1004 가 되도록 하는 것이다.


but, type 에 관게없이

a는 첫번째 칸의 주소

a + 1 은 두번째 칸의 주소

a + 2 는 세번째 칸의 주소. 가 되는 것이다.

' * ' 연산자는 항상, 그 주소에 저장된 값을 의미하는 것이다

즉, * ( a + i )는, i 번째 칸에 저장된 값의 주소.를 의미하게 되는 것이다.

이와 같은 표현도 가능하다.

4) 동적 메모리 할당


우리가 일반적으로 데이터를 저장하기 위한, 메모리 공간이 필요하다 ??

그 데이터를 저장할 변수를 선언해서, 그 변수에 데이터를 저장해두는 것이, 데이터를 저장할 메모리 공간을 확보하는 방법이다. ( 가장 일반적인 방법 )

이제, 동적 메모리 할당이란, 그것말고 또다른, 방법으로, 메모리 공간을 확보해주는 것이다.

malloc 함수 : 메모리 할당.을 줄여서 만든 이름

만일, 내가 10개의 정수를 저장하기 위한 메모리 공간이 필요하다.

내가 변수를 선언하지 않고, 10개의 정수를, 즉, 크기가 10인 정수배열과 같은, 어떤 메모리가 필요하다.

그것을 정수 배열을 선언해서 할 수도 있지만

malloc 을 통해, 동적 메모리 할당을 요구할 수 있다.

( int *) malloc ( 40 )

40 : 내가 필요한 메모리 공간 >> malloc 을 통해서, 메모리 어딘가에 내가 필요한 메모리 크기만큼의 메모리를 할당해줄 것이다.

그리고 malloc은, 내가 할당한 공간의 첫번째 byte 의 주소를 반환해주는데, 기껏 반환해준 주소값을 잊어버리면 안되므로, 그것을 p 에다가 저장하는 것이다.

메모리 주소를 어떤 변수 p에 저장하는 것인데, 그 변수의 type은 무엇이 되야 하는 것일까? 당연히 포인터 변수인 것이다. 그래서 정수형 포인터 변수 int * p를 선언해주고, malloc 이 반환해주는 값을 p 에다가 저장하는 것이다.

malloc 이 return 해 주는 값은, 타입이 없는 주소 ( void *) 이다.

나의 목적이 이렇게 할당받은 메모리에 정수를 할당하는 것이 목적이다 ?

그렇다면, ( int * ) 를 통해서, 정수형태로 변환을 해주고, 그것을 정수형 포인터 변수 int *p 에 할당해주는 것이다.

만일 내 목적이 할당받은 메모리에 '문자'를 할당하는 것이다 ?

char * p

p = ( char* ) malloc( 40 ) . 이렇게 할당하는 것이다.

if ( p == NULL ) : malloc 이 실패하는 경우에 대한 예외처리를 해주는 것

즉, 동적 메모리 할당이 어떤 이유로 실패했다면, 그때는 어떻게 할 것인지. 등등

만약. 실패하지 않는다면,

p[0] = 12 . 이렇게 할 수 있고,

만일 첫번째 칸에 이와 같이 '정수형'을 할당하였다면,

마치, 모든 배열 칸이 '정수'형 배열인 것처럼, 쓸 수 있다.

동적 메모리 할당을 쓰는 대표적인 경우중 하나다 reallocation, 즉, 배열의 크기를 재조정할 때이다.

처음에는 array, 즉 배열의 크기를 4 라고 지정했는데,

사용자가 4보다 큰 정보를 입력하게 될 수도 있다.

이때 우리가 배열의 크기를 키워야 한다.

처음에 arrary 라는 이름의 정수형 포인터 변수를 선언하고, 4개 정수가 들어가는 동적 메모리 할당을 한다.

자, 우리가 배웠듯이, int 형은 4byte 를 차지하면, 4개의 정수를 할당하기 위해서 16byte 만 할당하라고 해도 된다

ex ( int * ) malloc( 16 ) 이렇게 하면 안되 ?

음.. 물론, 일반적으로 정수형이 4byte 를 차지하는 것은 맞다.

그러나, 프로그램마다 다를 수가 있다.

따라서 ( int * ) malloc( 4 * sizeof( int ) )

이렇게 함으로써, 프로그램마다 int 형의 크기를 할당해서, 그에 맞는 동적 메모리 할당을 실시하는 것이다.

즉, 설령 이 같은 코드를,

정수형을 2byte 로 인식하는 코드에 갖고 가더라도,

그 컴파일러에서, 목적에 맞게 실행을 하게 된다는 것이다.

만약, 배열의 크기를 키우고 싶다면

int * tmp = ( int * ) malloc ( 8 * sizeof ( int ) )

이런 식으로 다시 동적 메모리를 할당해야 하는 것이다.

계속 '배열의 크기를 키운다' 라고 말을 하지만,

실제로 '배열의 크기를 키우기'는 불가능하다.

왜냐하면, 일반적으로 배열이라는 것은, 메모리의 연속된 공간을 차지하는데,

그 다음 공간은, 사실 이미 다른 용도로 사용될 수 있으므로,

이 배열의 크기 자체를 늘리는 것은 가능하지 않다.

그렇다면, 배열의 크기를 어떻게 키운다는 것이냐?

어딘가 다른 메모리의 다른 공간에, 더 큰 배열을 할당받은 다음

원래 있던 배열의 데이터를 새로 할당받은 배열로 모두 옮기고

그 다음, 그 새로 할당받은 공간을 쓰는 것이다.

즉,

int * tmp = ( int * ) malloc ( 8 * sizeof ( int ) )

이를 통해, 크기가 2배 인 배열을 동적 메모리 할당으로 할당받고

이 할당받은 메모리의 임시 주소를 tmp 에 할당하고 ,

그 다음, array[i]에 있는 데이터를 tmp[i] 에 옮기는 것이다.

( for 문의 역할 )

array = tmp

이것은 무슨 역할이지 ?

array 라는 것은, 그냥 포인터 변수이고

처음 할당 받았던, 16byte 의 메모리 공간의 시작 주소를 저장하고 있고

tmp 는 새로 할당받은 공간의 시작 주소를 저장하고 있다.

array, tmp 둘다 포인터 변수이고,

포인터 변수간의 취합문이고

오른쪽 값을,왼쪽 변수의 값에 저장하는 것이다.

즉, tmp 가 저장하고 있는 값을 array 에 할당하는 것인데

tmp 가 저장하고 있는 값은, 새로 할당한 동적 메모리 공간의 주소이다.

즉, int * tmp = ( int * ) malloc ( 8 * sizeof ( int ) ) 의 주소이다.

그 주소를 arrary 에 저장하는 것이다.

그렇게 되면 array 가 새롭게 할당받은 주소를 저장하게 되는 것이다.

원래 array가 4칸 짜리 배열의 주소,

이제 array는 8칸 짜리 배열의 주소를 갖게 되는 것이다.

그 이후

array[4] = 4

array[5] = 5

이렇게, 확장된 공간에 새로운 값들을 넣을 수 있는 것이다.

int *array = ( int * ) malloc( sizeof(int) * 4 ) ) 로 할당되었을 때는

array[ 3 ] 까지만 할당이 가능했지만, .... 이제는 array[7] 까지 값을 할당하게 되는 것이다.


그런데, 이렇게 되면, 기존의 4칸짜리 배열은 어떻게 되는 거야?

원래 ( int * ) malloc( sizeof(int) * 4 ) ) 라는 동적 메모리가 return 하는 주소값을

array 라는 포인터 변수가 갖고 있었는데,

이제 array라는 포인터 변수는 다른 것을 가리키고 있어.

그렇다면...?

기존의 4칸짜리 배열을 가리키는 애는 아무도 없는 거잖아

그럼 이제 이 메모리 공간은 garbage 가 되는 것이야.

당장 프로그램의 오류가 되지는 않지만,

쓰지 않고, 계속 가지고 있으므로,

이는 필요 이상의 메모리를 가지고 있는 것이고,

이게 쌓이면, 성능의 문제가 생기는 것이다.



배열 이름 ( 수정 불가능 )


즉, 이러한 배열을 보게 되면,

위에서는 int *array 를 통해, 포인터 변수로 선언했지만,

int array[4] 와 같이, 배열.로 선언하게 되면,

array = tmp. 이렇게 수정이 불가능하다. 새로 할당이 불가능하다.

이로 인해 malloc 이라는 동적 메모리 할당을 통해, 배열의 크기를 키운다고 하는 것이다.