본문 바로가기
자료구조

2. 배열, 포인터 및 구조체

by spaul 2023. 5. 28.

 C언어를 조금이라도 공부했던 사람이라면 배열과 포인터는 들어봤을 겁니다. 둘 다 개념은 어렵지 않지만 2차원 이상의 배열은 조금 헷갈릴 수 있는 부분이 있습니다.

 

▣ 배열(Array)

■ 배열은 연속된 메모리 주소 공간 상에 element가 존재하는 자료구조입니다. 쉽게 말하면, 메모리내에 배열의 각 요소들은 서로 떨어져 있지 않고 붙어 있다는 뜻입니다.

 

■ C에서 1차원 배열 변수를 선언하는 방법은 아래와 같습니다.

1
int vector[5= {1,2,3,4,5};
cs

 int 자리에는 type(char, float, double, long long, 구조체 등)이 들어갈 수 있습니다. 여기서 vector변수의 type은 뭘까요? 

 

그냥 int 배열이라고 말하면 틀린 답입니다. 정확히는 "integer 5개 저장되는 배열"(완전히 똑같이 말해야 될 필요는 없지만 이런 방식으로)이라고 해야합니다. 그 이유는?

 

 우리가 변수의 type이라고 말하는 것들은, 사실 메모리 주소에 어떻게 변수가 저장되는지에 대한 이야기이기 때문입니다. 위에서 배열의 각 요소는 메모리상의 연속된 주소공간에 존재한다고 얘기했습니다. int가 4byte라고 하고 arr[0]가 메모리 주소의 1000번지에 저장된다고 가정하면, a[1]은 1004번지, a[2]는 1008번지, a[3]은 1012번지, a[4]는 1016번지에 저장됩니다. 결과적으로 vector변수는 20bytes의 메모리를 차지하게 됩니다.

 

 또한 각 배열의 원소값을 element라고 하는데(1, 2, 3, 4, 5 각각이 vector배열의 element입니다.), 각 element는 type의 크기만큼 메모리를 차지합니다.

 

 예를 들어 char은 1byte이므로 char vector[5] = {'a','b','c','d','e'};라고 선언한다면 vector의 메모리 공간을 차지하는 크기는 5bytes가 될 것입니다. 즉 변수명 앞에 붙는 type은 각각의 element가 필요로하는 메모리 상의 공간을 나타낸다고 보시면 됩니다.

(여담으로 int와 float는 둘 다 4byte로 메모리를 차지하는 비중은 같지만 int는 정수형, float는 실수형이기 때문에 메모리에 저장되어 표현되는 방식이 다릅니다.)

 

 2차원 배열의 선언은 어떨까요?.

1
int matrix[2][3= {{1,2,3}, {4,5,6}};
cs

 

1 2 3
4 5 6

 2차원 배열은 우리가 흔히 아는 행렬의 구조입니다. matrix변수는 2개의 행(가로축)을 갖고, 3개의 열(세로축)을 갖는 2차원 배열입니다. 여기서 matrix변수가 차지하는 메모리 주소공간의 크기는 얼마일까요?

 

 다들 아시겠지만 4bytes * 6 = 24bytes이므로 24bytes의 공간을 차지하게 됩니다. 2차원 배열도 1차원 배열과 마찬가지로 메모리 공간에 각 element가 연속적으로 위치합니다. 2차원 배열의 첫 번째 행부터

1, 1000
2, 1004
3, 1008
4, 1012
5, 1016
6, 1020

 위와 같은 방식으로 저장됩니다. 메모리 그림의 왼쪽은 배열의 element의 값, 두 번째는 element가 저장되는 메모리의 시작주소 입니다.

 

 또한 n(n>=2)차원 배열은 n-1차원 배열의 1차원 배열입니다. 좀 어렵나요? 예를 들어 봅시다.

 

 {{1,2,3},{4,5,6},{7,8,9}} 라는 2차원 배열이 있다고 하면, {1,2,3}, {4,5,6}, {7,8,9}는 각각 1차원 배열입니다. 즉 2차원 배열은 1차원 배열의 1차원 배열이라고 할 수 있겠죠. 마찬가지로 3차원 배열도 2차원배열의 1차원 배열이라고 할 수 있습니다.

 

# 2.포인터(Pointer)

■ 변수(Variable)는 생애(lifetime) 도중에 memory에 저장됩니다. 이 때 lifetime이란 변수가 속한 범위(local, global, 루프 내부 등)의 수행이 종료되는 시점을 의미합니다. 예를 들어 전역변수 int a;가 있다고 한다면, a는 프로그램 수행이 종료될 때 까지 memory내에 존재할 것입니다.

 

■ 64bits 운영체제에서 Memory의 최대 공간은 2^(64)이고, 32bits 운영체제에서 Memory의 최대 공간은 2^(32)입니다. 즉, 32bit 운영 체제에서는 아무리 메모리를 많이 쓰고 싶어도 4GB이상 사용할 수 없습니다. 2010년대 초부터 나오는 PC의 운영체제는 거의 대부분 64bits라고 보면 됩니다.

 

 64bits 운영체제에서 메모리 주소는 8bytes 크기를 가지는 수입니다. 보통 16진수로

0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF로 표현합니다. 그리고 각 메모리 주소마다 1byte크기의 공간을 가집니다. 만약 프로그램 내에서  int형 변수가 선언된다면 4bytes이므로 4개의 메모리 주소 공간을 차지하게 되겠네요. 중요한 것은 각 자료형마다 메모리내에 차지하는 공간이 고정된다는 것입니다. int a = 10; 이라고 선언한다고 해서 1byte크기의 메모리 공간을 차지하는 것이 아니라 항상 4bytes를 차지하게 됩니다. 

 

 배열을 설명할 때도 얘기했지만 포인터에 대해 이야기하기 전에 메모리에 대해 한번 더 설명했습니다. 결국 변수는 변수가 프로그램 내에서 속한 범위에 따라 메모리의 특정 공간(heap, stack 등)에 위치하게 됩니다.

 

■ 포인터 변수(Pointer Variable)는 변수의 주소를 저장하는 "변수"입니다. 변수라는 글자에 따옴표를 한 이유는, 결국 포인터 또한 변수라는 것을 강조하기 위해서입니다. char 포인터 변수는 char형 변수가 저장된 메모리의 주소를 저장하며, int 포인터 변수는 int형 변수가 저장된 메모리의 주소를 저장합니다.

1
2
3
char *cp; // 변수 cp의 type은 char *
 
int *ip; // 변수 ip의 type은 int *
cs

■ C에서 포인터 변수를 선언하는 방법은 위와 같습니다. 포인터 변수 cp와 ip는 각각 char형 변수, int형 변수가 저장된 메모리 공간의 주소를 가리키는 변수입니다. 예를들어 char c가 메모리 주소 1000번지(이해를 돕기 위해 10진법 주소로 표현합니다.)에 저장되어 있고 cp가 c를 가리키게 한다면, cp에 저장되어 있는 값은 1000이 될 것입니다.

 

■ 64bits 운영체제의 경우 포인터 변수의 메모리 공간 크기는 항상 8bytes입니다. 앞서 말했듯이 포인터는 변수의 메모리주소를 담고있는 변수이고 메모리 주소의 번지는 8bytes로 표현되기 때문에, char변수를 가리키는 포인터이든, int변수를 가리키는 포인터이든, double변수를 가리키는 포인터이든, 구조체를 가리키는 포인터이든 상관없이 항상 8bytes의 메모리 공간을 차지하게 됩니다.

 

■ 포인터는 함수의 인자로 전달할 때, 그리고 구조체에 접근할 때 많이 사용됩니다. 일일이 포인터의 사용법에 대해 설명하는 것은 제 포스팅의 목적과 맞는 것 같지 않아 생략하겠습니다.

 

■ 또한 포인터라는 용어와 개념을 C, C++  등 C계열의 일부 언어에서만 사용하는 것처럼 보이지만 그렇지 않습니다. Python, Java 등의 언어도 참조형(Reference Type)이라는 개념이 있는데, 변수가 저장된 메모리 주소에 접근하여 각 변수의 값을 읽어오는 방식입니다. 포인터와 거의 같은 개념이라고 보시면 되고, 결국 C 이외의 다른 언어를 이해하기 위해서도 포인터에 대한 이해는 필수적입니다.

 

# 3.구조체(Structure)

■ C에서 구조체는 아래와 같은 형식으로 정의됩니다.

1
2
3
4
5
struct Student { // 구조체 이름 정의
    char name[20]; // 멤버 변수 선언
    int studentNumber; // 멤버 변수 선언
    char major; // 멤버 변수 선언
};
cs

 student는 구조체 이름이고, name, number 및 major는 구조체의 member 변수입니다. 구조체를 정의한다는 것은 개발자가 구조체 type을 정의하는 것과 같습니다. 즉 위의 예에서 student라는 이름을 가지는 구조체 type을 정의하는 것입니다. 변수를 선언할 때 int, double과 같은 type을 변수명 앞에 붙이는 것과 마찬가지로, 구조체 변수를 사용할 때도 구조체의 이름을 변수명 앞에 붙여 선언합니다. 또한 구조체형은 관례적으로 첫글자를 대문자로 정의합니다.

 

■ 위의 예에서 한 것은 구조체에 대한 정의입니다. 절대 구조체를 선언한 것이 아닙니다. 구조체 정의만으로는 메모리 공간을 차지하지 않으며, 지역 변수 또는 전역 변수로 구조체 변수를 선언할 때 비로소 메모리 주소 공간을 차지하게 됩니다.

 

■ 구조체의 member변수에 접근하는 방법은 두 가지가 있습니다. 

1. "." 연산자 사용 : 구조체 변수.멤버 변수 형식으로 접근

2. "->" 연산자 사용 : 구조체 포인터 변수 -> 멤버 변수 형식으로 접근

1
2
3
struct student s1 = {"Hong Gill Dong"123477};
struct student s2 = {"Kim Yong Su"567877};
struct student *sp;
cs

 "." 연산자를 사용하여 구조체의 멤버 변수에 접근하는 것은 직관적이고 쉽습니다.

가령 위의 코드에서 printf("%s", s1.name)이라는 코드를 실행한다면, s1의 멤버변수 name에 저장되어 있는 Hong Gill Dong이 출력 될 것입니다. 

 

 위에서 struct student *sp라는 코드를 통해 student 구조체를 가리키는 포인터 변수 *sp를 선언했습니다. 이 포인터는 구조체 변수를 가리키는 포인터이며, 구조체가 메모리 공간을 차지하는 크기를 알 수 있게 되고, 이를 통해 구조체 변수에 접근할 수 있게 됩니다.

 

 여기서 student 구조체의 size는 몇 bytes일까요? 20bytes(char[20]) + 4bytes + 1byte = 25bytes일 것 같지만, 실제로 Visual Studio에서 printf("%d", sizeof(Student))코드를 실행하면 28이 출력됩니다. 이는 메모리 주소 공간의 단편화를 방지하고, 더 효율적으로 메모리에 접근하기 위하여 컴파일러가 의도적으로 구조체의 메모리 공간을 할당한 것입니다 (혹시 아니라면 알려주세요 ㅠㅠ)

 

 하여튼 student형 sp를 선언하고 나면 sp는  "->" 연산자를 통해 구조체의 멤버 변수에 접근할 수 있게 됩니다. 아래와 같은 방식으로 말이죠

1
2
3
4
struct student* sp;
 
printf("name=%s, number=%d, major=%d\n",
     sp->name, sp->studentNumber, sp->major);
cs

 "."연산자를 통해 구조체 멤버 변수에 접근하는게 더 직관적이고 쉬워 보이지만, 구조체 포인터 변수를 통해 구조체의 멤버 변수에 접근 하는 것이 더 효율적이기 때문에 구조체 포인터 변수를 사용한다고 합니다. 하지만 절대적인 것은 아니므로 상황에 따라 적절하게 사용하면 될 것입니다.

 

■ 구조체와 비슷한 개념으로 객체지향 언어의 클래스(class)가 있습니다. 단, 절대 같지는 않습니다. 구조체 내부에는 함수(method)를 정의할 수 없지만, 클래스 내부에는 method를 정의할 수 있습니다. 클래스에 함수를 포함하지 않고 멤버 변수(attribute)만 포함시키면 구조체와 비슷하게 사용할 수 있습니다. (물론 객체지향 언어에서 이런식으로 클래스를 사용하진 않으며, 구조체와 클래스는 비슷해 보이지만 매우 다릅니다. 그저 그런 것도 있다라는 참고 용도로만 알아두시면 좋을 것 같습니다.)

 

※ 글을 읽으시다가 오류 및 문의 사항이 있다면 언제든 댓글로 남겨주세요! 저도 공부하는 입장이기 때문에 오개념이 있을 수 있습니다 :)

 

[참고자료] 

1. 신동하, 자료구조

2. 생능출판사, C언어로 쉽게 풀어쓴 자료구조

'자료구조' 카테고리의 다른 글

4. 큐(Queue)  (0) 2023.10.05
3. 스택(Stack)  (0) 2023.09.27
1. 순환(Recursion)  (0) 2023.05.16