배열(Array)이란 여러 개의 동일한 데이터 타입의 데이터를 한 번에 만들 때 사용된다.
각각의 변수를 선언하는 것보다 배열을 사용하면 인덱스 번호를 가지고 작업을 할 수 있기 때문에 인덱스 번호에 따라 효율적으로 루프를 설정하여 여러 상황에서 간단한 코드를 이용하여 결과를 나타낼 수 있다.
배열의 가장 기본적인 특징은 배열이 <인덱스,요소> 쌍의 집합이라는 것이다.
즉, 인덱스(Index)가 주어지면 해당하는 요소(Element)가 대응되는 자료 구조이다.
C언어에서 배열의 인덱스는 0부터 시작한다.
배열 이름에 그 기반 주소가 저장되기 때문에 배열의 이름은 포인터와 같은 역할을 한다.
따라서 배열의 이름을 전달하면 배열의 포인터가 전달되는 거나 마찬가지이다.
복잡한 객체들은 일반적으로 같은 데이터 타입으로만 되어 있지 않다.
일반적인 객체에는 보통 여러 가지 다른 타입의 데이터들이 한데 묶여 있다.
구조체(Structure)는 타입이 다를 수 있는 데이터들을 묶는 방법이다.
C언어에서는 구조체를 struct를 사용하여 표현한다.
struct 구조체명{
항목1;
항목2;
...
};
구조체의 형식이 위와 같이 정의되었다면 구조체 변수는 다음과 같이 생성한다.
struct 구조체명 변수명;
간단한 예로 사람을 나타내는 이름, 나이, 키를 가진 구조체를 만들어보면 다음과 같다.
struct person{
char name[10];
int age;
float height;
};
struct 예약어 다음에 오는 person은 구조체와 구조체를 구별할 수 있기 해주는 식별자로서 보통 구조체 태그(Tag)라고 한다.
typedef struct person{
char name[10];
int age;
float height;
}person;
만약 typedef을 사용하게 되면 구조체를 새로운 타입으로 선언하는 것이 가능하다.
구조체 변수명 바로 뒤에 '.'을 첨가한 후 항목명을 연이어 기록하면 외부에서 그 항목을 접근할 수 있는 변수명이 된다.
이때 사용하는 '.'을 항목 연산자(Membership operator)라고 한다.
C언어에서는 구조체 변수를 다른 구조체 변수로 대입할 수 있다.
그러나 구조체 변수와 다른 구조체 변수를 비교하는 것은 불가능하다.
따라서 두 개의 구조체 변수를 비교하기 위해서는 프로그래머가 직접 함수를 작성하여야 한다.
자체 참조 구조체(Self-referential structure)는 특별한 구조체로서 구성 요소 중에 자기 자신을 가리키는 포인터가 한 개 이상 존재하는 구조체를 말한다.
자체 참조 구조체는 연결 리스트나 트리를 구성할 때 많이 등장한다.
일반적으로 항목의 개수를 미리 예측할 수 없는 경우에 자체 참조 구조체를 정의해 놓고 동적으로 기억장소를 할당받아서 이들을 포인터로 연결하여 자료 구조를 구성한다.
typedef struct ListNode{
char data[10];
struct ListNode* link;
}ListNode;
위의 ListNode 구조체에서 link 필드가 ListNode를 가리키므로 그 구조체는 자체 참조 구조체이다.
구조체를 요소로 하는 배열도 생성할 수 있다. 또한 구조체를 포함한 구조체도 선언할 수 있다.
포인터 변수(Pointer variable)는 다른 변수의 주소를 가지고 있는 변수이다.
포인터는 다른 변수를 가리킨다고 말할 수 있다.
모든 변수는 주소를 가지고 있음을 기억하라. 컴퓨터 메모리는 바이트로 구성되어 있고 각 바이트마다 순차적으로 주소가 매겨져 있다.
char a='A';
char *p;
p=&a;
먼저 char형의 변수 a가 정의되고 p는 char형을 가리키는 포인터로 정의된다.
p가 a를 가리키게 하려면 a의 주소를 p에 대입한다.
변수의 주소는 &연산자를 변수에 적용시켜서 추출할 수 있다.
포인터 변수가 가리키는 메모리의 내용을 추출하거나 변경하려면 *연산자를 사용한다.
이들 예제 문장에서는 *p와 변수 a가 동일한 메모리 위치를 참조함을 유의해야 한다.
즉, p와 변수 a는 전적으로 동일하다. 이들의 값만 같은 것이 아니고 동일한 객체를 가리키기 때문에 p의 값을 변경하게 되면 변수 a의 값도 바뀌게 된다.
포인터에서 가장 중요한 연산자는 변수로부터 변수의 주소를 추출해내는 '&'와 포인터가 가리키는 변수의 내용을 추출하는 '*' 연산자이다.
포인터는 다음과 같이 여러 가지 타입의 대상에 대하여 선언될 수 있다.
void *p; //p는 아무것도 가리키지 않는 포인터
int *pi; //pi는 정수 변수를 가리키는 포인터
float *pf; //pf는 실수 변수를 가리키는 포인터
char *pc; //pc는 문자 변수를 가리키는 포인터
int **pp; //pp는 포인터를 가리키는 포인터
struct test *ps; //ps는 test 구조체를 가리키는 포인터
void (*f)(int); //f는 int를 매개 변수로 갖고 반환 값을 갖지 않는 함수를 가리키는 포인터
void *p는 아무것도 가리키지 않는 포인터를 의미한다.
void포인터는 필요할 때마다 다른 포인터로 바꾸어서 사용하는 것이 가능하다.
pi=(int *)p; //p를 정수 포인터로 변경하여 pi로 대입
포인터는 함수의 매개 변수로 전달될 수 있다.
특정한 변수를 가리키는 포인터가 함수의 매개 변수로 전달되면 그 포인터를 이용하여 함수 안에서 변수의 값을 변경할 수 있고, 그 결과는 함수 호출자에 영향을 미친다.
만약에 함수가 하나 이상의 값을 반환하여야 한다면 포인터를 사용하는 것이 가장 좋은 방법이다.
C언어에서 return 문장은 하나의 값만을 반환할 수 있다.
따라서 하나 이상의 값을 반환할 때는 함수 매개 변수를 사용하여 반환하는 것이 보통이다.
함수의 매개 변수로 배열이 전달되면 함수 안에서 배열의 내용을 변경할 수 있었다.
그 이유는 배열의 이름이 배열의 시작 부분을 가리키는 포인터이기 때문이다.
실제로 컴파일러가 배열의 이름에 공간을 할당하지는 않는다. 대신에 이름이 배열의 이름이 있는 곳을 배열의 첫번째 요소의 주소로 대치한다.
따라서 배열의 이름이 포인터이기 때문에 배열이 함수의 매개 변수로 전달될 때에 사실은 포인터가 전달되는 것이다.(메모리 공간을 절약하는 기법)
구조체에 대한 포인터에서 구조체의 멤버에 접근하기 위해서는 '.'이 아니라 '->'를 사용해야 한다.
ps->i는 (*ps).i와 동일한 효과를 갖는다.
구조체에 대한 포인터도 자주 함수의 매개 변수로 사용된다.
구조체 자체를 매개 변수로 넘기는 경우, 구조체가 큰 경우에는 상당한 부담이 된다.
구조체 포인터를 넘기게 되면 주소만 함수에 전달된다.
함수는 이 포인터를 이용하여 구조체의 내용을 변경할 수 있다.
포인터도 하나의 변수이므로 포인터의 포인터도 선언이 가능하다.
실제로 앞으로 다룰 몇 개의 자료 구조에서 함수에 포인터의 포인터를 전달할 것이다.
변수의 주소뿐 아니라 아래 표현을 사용하면 함수의 주소를 담는 포인터 변수를 만들 수 있다.
#include <stdio.h>
void foo(int a){
printf("foo : %d\n",a);
}
int main(){
void (*f)(int); //f는 함수의 주소를 담는 포인터 타입이다.
f=foo;
f(10);
(*f)(10);
}
위 코드에서 foo(10), f(10), (*f)(10)은 완전히 동일한 표현이다.
포인터에 대한 연산은 보통의 연산과 다른 의미를 지난다.
정수의 포인터에 값을 더하거나 빼보자. 다음과 같이 정수의 포인터 pi를 생성하고 정수 변수를 가리키도록 하자.
int A[6], int *pi;
pi=&A[3];
pi+1, pi-1의 값은 어떻게 될까?
주소 값이 하나 감소되고 증가하는 것이 아니라 pi-1은 pi가 가리키는 객체 하나 뒤에 있는 객체를 가리키고 pi+1은 pi가 가리키는 객체 하나 앞에 있는 객체를 가리킨다.
즉 앞의 코드에서는 pi+1은 A[4]를, pi-1은 A[2]를 가리킨다.
포인터에 대한 사칙 연산은 일반적인 사칙 연산과 다른 의미를 가진다.
p / 포인터
p / 포인터가 가리키는 값
p++ / 포인터가 가리키는 값을 가져온 다음, 포인터가 한 칸 증가한다.
p-- / 포인터가 가리키는 값을 가져온 다음, 포인터가 한 칸 감소한다.
(p)++ / 포인터가 가리키는 값을 증가시킨다.
포인터는 강력한 개념이지만 프로그램에서 오류를 발생시키는 원천이기도 하다.
포인터를 사용할 때는 상당히 많은 주의를 해야 하는데 다음과 같은 점들을 주의해야 한다.
- 포인터가 어떤 값을 가리키고 있지 않을 때는 NULL로 설정하는 것이 바람직하다. (int *pi=NULL;)
- 포인터가 초기화가 안 된 상태에서 포인터가 가리키는 곳에 자료를 저장하면 안 된다.(char pi; pi = 'E'; , 포인터 pi가 초기화가 되어 있지 않으므로 위험한 코드)
- 포인터 타입 간의 변환 시에는 명시적인 타입 변환을 사용한다. (int pi; float pf; pf=(float *)pi;)
동적 메모리 할당(Dynamic memory allocation)이란 프로그램이 실행 도중에 동적으로 메모리를 할당받는 것을 말한다.
보통 프로그램이 메모리를 할당받는 방법에는 정적과 동적의 두 가지 방법이 있다.
정적 메모리 할당이란 프로그램이 시작되기 전에 미리 정해진 크기의 메모리를 할당받는 것이다.
이 경우, 메모리의 크기는 프로그램이 시작하기 전에 결정되며 프로그램의 실행 도중에 그 크기가 변경될 순 없다.
ex)배열 선언
int buffer[100];
char name[] = "data structure";
정적 메모리 할당은 아주 간단하게 메모리를 할당할 수 있는 방법이지만 경우에 따라 비효율적일 수 있다.
예를 들면 프로그램이 처리해야 하는 입력의 크기를 미리 알 수 없는 경우에도 고정된 크기의 메모리를 할당할 수밖에 없다.
만약 처음에 결정된 크기보다 더 큰 입력이 들어온다면 처리하지 못할 것이고 더 작은 입력이 들어온다면 남은 메모리 공간은 낭비될 것이다.
동적 메모리 할당은 '프로그램의 실행 도중에 메모리를 할당받는 것'이다.
프로그램에서는 필요한 만큼의 메모리를 시스템으로부터 동적으로 할당받아서 사용하고, 사용이 끝나면 시스템에 메모리를 반납한다.
필요한 만큼만 할당을 받고 또 필요한 때에 사용하고 반납할 수 있기 때문에 메모리를 매우 효율적으로 사용할 수 있다.
int main(){
int *pi;
pi = (int *)malloc(sizeof(int)); //동적 메모리 할당
...
동적 기억 장소 사용
...
free(pi);
}
여기서 주의할 점은 malloc함수가 반환했던 포인터 값을 잊어버리면 안 된다는 것이다.
void* malloc(int size) : size바이트만큼의 메모리 블록을 할당한다. 새로운 메모리 블록의 시작 주소(즉 포인터)를 반환한다.
이때 반환되는 포인터의 타입은 void* 이므로 이를 적절한 타입의 포인터로 타입 변환시켜야 한다.
메모리 확보가 불가능하면 NULL을 함수의 반환 값으로 반환한다.
(char *)malloc(100);
(int *)malloc(sizeof(int));
(struct Book *)malloc(sizeof(struct Book));
void free(void *ptr) : 동적으로 할당되었던 메모리 블록을 시스템에 반납한다. ptr은 할당되었던 메모리 블록을 가리키는 포인터 값이다.
void *calloc(int num, int size) : 배열 형식의 메모리를 할당한다. 배열 요소의 크기는 size바이트이고 개수는 num이 된다. 요소들은 0으로 초기화된다는 점에서 특이하다. 반환 값은 malloc함수와 동일하다.
sizeof 연산자 : sizeof 키워드는 변수나 타입의 크기를 숫자로 반환한다. 크기의 단위는 바이트이며 대상은 식별자이거나 데이터 타입일 수 있다.
구조체의 경우, 접근 속도를 빠르게 하기 위한 패딩 바이트에 의하여 그 크기가 예상과는 다를 수 있다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Example{
int number;
char name[10];
};
void main(){
struct Example *p;
p=(struct Example *)malloc(2*sizeof(struct Example));
if(p==NULL){
fprintf(stderr,"Can't allocate memory");
exit(1);
}
p->number=1;
strcpy(p->name,"Park");
(p+1)->number=2;
strcpy((p+1)->name,"Kim");
free(p);
}