컴파일러가 소스 코드를 컴파일 할 때는 파일 단위로 컴파일 한다. 그렇기 때문에 다른 파일의 정보를 참조하여 컴파일을 진행하지 않는다. 그렇기 때문에 저렇게 세 파일로 나누게 되면 number 변수가 어디에 있는지, set/getNumber 함수가 어디에 있는지 무엇인지 알 수 없기 때문에 에러가 난다. 그렇기 때문에 컴파일러에게 현재 컴파일하고 있는 소스의 외부 소스에 선언 및 정의가 되어있음을 컴파일러에게 알려야 한다.
다음과 같이 수정한다. 먼저 number 변수에 대해서 외부 변수라고 알린다. 그리고 두 함수에 대해서 외부 함수라는 것을 알린다. 바로 extern 키워드를 통해서 컴파일러에게 이를 알릴 수 있다.
< number.c / func.c / main.c >
< Makefile과 실행 결과 >
각 소스에서 찾을 수 없는 변수나 함수들에 대해서 extern 키워드를 이용하여 외부에 존재한다는 선언을 내려주면 정상적으로 컴파일 되는 것을 확인할 수 있다. 그리고 함수에 대해서는 extern 선언은 생략해도 상관 없다. 이와 반대로 외부에서 접근을 못하게 하고 싶다면 해당 변수나 함수에 static 키워드를 붙여주면 다른 외부 파일에서 extern을 이용한 알림이 불가능하다. static 전역 변수는 접근의 범위를 파일의 내부로 제한하는 경우에 사용한다.
헤더 파일의 활용
#include 지시자는 선행 처리기에게 다음과 같은 지시를 전달한다.
해당 #include <…> 문장의 위치에 해당 헤더의 저장된 내용을 가져다 놓기!
그리고 다음 예제를 통해서 #include 지시자를 완전히 이해할 수 있다.
< m.c / h1.h / h2.h >
헤더 파일을 include 하는 두 가지 방법
1.#include <헤더 파일 이름> -> 표준 헤더 파일이 저장되어 있는 디렉터리에서 해당 헤더 파일을 찾는다.
2.#include “ 헤더 파일 이름 “ -> 이 문장을 포함한 소스 파일이 저장된 디렉터리에서 해당 헤더 파일을 찾는다. ->상대 경로(.과 ..을 이용한) 및 절대 경로를 이용하여 헤더 파일을 삽입 가능하다.
헤더 파일에 담아야 할 요소
1.외부에 존재하는 변수나 함수에 대해서 extern 하는 선언들을 헤더 파일에 담고, 필요할 때마다 헤더 파일만 삽입하여 외부 정의를 알릴 수 있게끔 구성한다. 그렇지 않으면 모든 소스 파일에서 필요한 extern 선언을 반복해서 해야하기 때문이다.
2.하나의 구조체 등의 선언 및 정의를 여러 소스 파일에서 사용할 때, 이를 각 소스 마다 명시하여 중복 선언 및 정의를 없앤다. 왜냐하면 중복 정의가 되어 있으면 해당 구조체를 수정하고, 확장할 때 불편함이 따른다. 그렇기 때문에 헤더 파일을 이용하여 구조체의 선언 및 정의가 하나만 존재할 수 있도록 개선한다. -> 헤더 파일의 중복 문제가 발생할 수 있다.
헤더 파일의 중복 발생 및 조건부 컴파일을 활용한 중복 삽입 문제의 해결
헤더 파일의 중복 삽입 자체는 문제가 되지 않는다.
extern int number; extern void setNumber(int n); extern int getNumber(void);
위와 같은 선언은 여러 번 삽입된다고 하더라도 오류가 발생하지 않는다. 그저 컴파일러에게 메시지를 전달하는 것에 지나지 않기 때문이다. 그러나 변수의 선언, 구조체 정의, 구조체 변수 선언, 함수 정의 등은 컴파일러에게 메시지, 알림을 알려주는 차원이 아니라 실행 파일의 내용에 직접적인 연관이 있는, 실행 파일의 내용이 달라질 수 있는 정보이기 때문이다.
조건부 컴파일을 활용한 중복 삽입 문제 해결
위 헤더를 처음 포함하는 소스 파일은 __HEADER_H__ 매크로 상수가 정의되지 않았기 때문에 조건부 if 절을 실행하지만, 중복 포함하는 소스의 경우는 매크로 상수가 정의되어 있기 때문에 헤더를 삽입하더라도 내용이 실행되지 않는다.
선행 처리 과정은 프로그래머가 삽입해 놓은 선행 처리 명령문 대로 소스 코드의 일부를 수정한다. 수정은 단순 치환 형태를 띠는 것이 대부분이다. 다음은 간단한 선행 처리에 대한 예제이다.
위의 예에서 선행 처리 명령문은 2번 라인과 4번 라인, 두 가지가 존재한다. 선행 처리 명령문은 # 문자로 시작하고, 명령의 끝에는 세미콜론을 붙이지 않으며 컴파일 이전에 컴파일러가 아닌 선행 처리기에 의해 처리된다. 먼저, 4번 라인의 경우, PI라는 기호를 만나면 3.1412로 치환해라 라는 의미이다. 이 때 PI 기호를 만난다는 것은 쌍 따옴표로 둘러 쌓인 문자열 내의 PI가 아닌 모든 PI 기호를 이야기한다. 선행 처리기에 의해 선행 처리가 진행되면 오른쪽 그림과 같이 치환이 된다. 그리고 2번 라인에서 헤더 파일을 삽입하는 것 또한 선행 처리기에 의해 진행된다. stdio.h라는 파일의 내용을 include 명령이 쓰인 해당 파일에 가져다 놓으라는 의미를 가지고 있다.
Object-like macro(오브젝트와 유사한 매크로, 매크로 상수) : #define
지시자 매크로 매크로_몸체 #definePI3.1412
지시자는 #include, #define 등과 같은 것을 가리켜 지시자라고 한다. 그리고 이어서 매크로라고 하는 기호와 대치되어야 할 매크로의 몸체가 등장한다. 선행 처리기는 이 부분을 보고 프로그래머가 지시하는 바를 파악한다. 그 중 #define 지시자는 다음과 같은 내용을 지시한다.
이어서 등장하는 매크로를 마지막에 등장하는 매크로 몸체로 치환!
다음은 이에 대한 예이다.
4번 라인부터 6번 라인까지 매크로 상수를 정의 및 지시가 이루어 졌고, 10번 라인부터 12번 라인에서 컴파일 이전에 치환(대치)이 이루어진다. 특이한 점은 6번 라인처럼 함수의 호출 자체 또한 매크로 몸체로 올 수 있다는 점이다.
Function-like macro(함수와 유사한 매크로, 매크로 함수) : #define
형태는 매크로 상수와 동일하다. 다음은 매크로 함수의 예이다.
지시자 매크로 매크로_몸체 #define SQUARE(X)X*X
기존 매크로 상수에 괄호를 등장시킴으로써 함수의 형태를 보이고 치환을 진행할 수 있다. 여기서 X는 정해 지지않은 임의의 값 또는 문장을 이야기 하는데 어떤 문자로 든 올 수 있으나 매크로와 매크로_몸체에서 사용되는 문자는 같은 문자로 통일 되어야 한다. 위의 예에서 해석을 해보자면 SQUARE(X)와 같은 패턴이 등장하게 되면 X*X 형태로 바꿔라 라는 의미다. 함수 호출과 매우 유사하고, 이렇게 변환되는 과정 자체를 매크로 확장이라고 한다. 다음은 관련 예제이다.
매크로 함수의 문제(우선순위)
위의 예제는 매크로 함수의 잘못된 사용의 결과이다. 3+2 값의 Square 연산은 5^2으로 25가 나와야 하지만 전혀 다른 결과가 나왔다. 그 이유는 매크로 함수 치환을 풀어보면 나타난다. 3+2*3+2 형태로 치환이 되었기 때문에 우선 순위 연산에 의해서 곱셈 연산자가 먼저 연산 되는 바람에 11이라는 값이 나왔다. 이를 해결하기 위해서는 매크로 함수 정의 시 괄호를 치고 구분을 해주어야 한다.
위와 같은 정의로 매크로 함수를 구성하면 정확한 결과를 얻을 수 있다.
매크로 기타 사항
먼저, 매크로 몸체가 복잡해서 두 줄 이상에 걸쳐서 정의를 할 경우에는 두 줄에 걸쳐서 매크로를 정의해야 하는 경우도 생기는데 이 때 그냥 임의로 줄을 변경하면 에러가 발생한다. 이럴 때에는 \문자를 이용해서 줄이 바뀐 매크로 정의임을 명시한다.
그리고 또한 이미 정의한 매크로를 이용하여 또 다른 매크로 정의가 가능하다. 즉, 매크로 정의 시, 먼저 정의된 매크로도 포함 및 사용이 가능하다.
매크로 함수의 장점과 단점
매크로 함수는 보통 작은 크기의 함수 / 호출의 빈도 수가 높은 함수를 매크로 함수로 보통 정의한다. 함수의 크기가 작아야 에러의 발생 확률 또한 낮고, 호출의 빈도 수가 높아야 매크로 함수가 가져다주는 성능 상의 이점을 최대한 활용할 수 있다.
조건부 컴파일
#if ~ #endif : 참이라면
조건부 컴파일을 이용한 예이다. 7번의 매크로 상수를 FALSE로 정의하면 12번 라인은 실행하지 않는다.
#ifdef ~ #endif : 정의되었다면
12번 라인에서 ifdef는 해당 매크로 상수가 정의되어 있으면 ifdef 내부를 실행한다. 그리고 13번 라인처럼 비교 연산 또한 가능하다. 그리고 8번 라인처럼 공백으로 매크로 상수를 정의할 수 있다. 이 경우에는 선행 처리 과정에서 공백으로 대체되므로 소멸 의미와 같다.
#elif의 삽입 : #if만 가능
#elif를 통해서 else if 와 같은 효과를 #else를 통해서 else와 같은 효과를 볼 수 있다.
매개 변수의 결합과 문자열화
다음과 같은 문장이 가능한지 보자.
#define CONCATENATE(A,B) “A B”
위와 같은 문장은 일단 불가능하다 왜냐하면 문자열 안에서는 매크로의 매개변수 치환이 발생하지 않기 때문이다. 이러한 경우 # 연산자를 이용한다.
#define STR(A) #A
위 문장의 의미는 A로 전달되는 인자를 문자열 “A의 내용” 형식으로 치환하라는 의미이다.
따옴표를 넣고 안 넣고 의 차이를 확인한다.
이런 치환 또한 가능하다. 9번 라인에서 두 문자열을 따로 썼지만 합쳐진 결과가 나타났다. 이를 활용해보면.
#define STRCAT(A,B,C) ABC
위의 경우 STRCAT(YOO, SEUNG, HO);라고 하면 “YOO SEUNG HO”라는 문자열이 조합될 것 같지만, 실제로는 조합되지 않는다. 왜냐하면 ABC를 A와 B와 C의 문자열 결합으로 보지 않고 ABC를 하나의 새로운 매크로 상수로 보기 때문이다. 이 경우 ABC로 그냥 치환이 될 뿐이다.
#define STRCAT(A,B,C) A B C
이 경우에는 STRCAT(as,vf,sd); 라고 호출될 경우 as vf sd 로 치환이 될 뿐이다.
이러한 문제를 해결하기 위해서는 ##연산자를 사용해야 한다.
필요한 형태로 단순 결합 : 매크로 ## 연산자
## 연산자를 통해 인자로 전달된 요소들을 단순 결합을 시킨다. 다음은 그에 따른 예제이다.
합쳐진 것을 볼 수 있다.
참고로 소스 코드 위에 단순 치환이기 때문에 AB라는 문자열과 1234라는 문자열을 치 ## 연산할 경우 AB1234라는 문자열이 생기는 것이 아니라 소스 코드 상에 AB1234로 대치가 되는 것이다 그렇기 때문에 AB1234는 소스 코드 상에서 변수나 함수 등의 이름으로 인지될 것이고, 이는 없는 이름이기 때문에 에러가 발생한다. 이 점을 혼동하지 않아야 한다.
프로그램 실행 시 운영체제에 의해서 마련되는 메모리 구조는 위와 같이 네 개의 영역으로 구분된다. 여러 영역으로 나뉘는 이유는 각기 다른 성향을 가진 여러 공간들을 유사한 성향의 공간끼리 분류해서 해당 성향을 지닌 데이터들을 분류한 공간 별로 저장하면 관리가 용이해지고 메모리의 접근속도가 향상된다.
메모리의 동적 할당
함수가 매번 호출될 때마다 새롭게 할당되고 또 함수를 빠져나가도 유지가 되는 유형의 변수의 생성은 생성과 소멸의 시기가 지역 변수나 전역 변수와는 다른 유형의 변수를 생성하는 것인데 이를 가능케하는 개념이 메모리의 동적 할당이다.
malloc 함수와 free 함수
힙 영역은 프로그래머가 관리하는 메모리 공간이다. 필요한 메모리 공간을 프로그래머에 의해서 동적 할당하기 때문이다. 그렇기 때문에 메모리 할당 및 해체에 대해서 전적으로 프로그래머가 담당하기 때문에 malloc 함수로 할당한 메모리 공간은 free 함수로 꼭 해체해줘야 하며, malloc 함수에 의해 할당된 메모리 공간은 쓰레기 값으로 구성된다. 다음은 예제이다.
9번 라인을 통해 포인터 변수를 선언하고, 11번 라인을 통해 힙 영역 메모리 공간을 동적 할당하는데 이 때 인자로는 바이트 단위의 사이즈가 계산되어 전달되었고, 형 변환이 일어나고 있다. 원래 malloc 함수는 void * 형을 반환하도록 되어 있기 때문에 대입 연산을 위해서는 void * 형에서 원하는 형으로 변환을 거친 후에 참조를 진행해야 한다. malloc 함수가 void * 형을 반환하는 이유는? malloc 함수를 호출할 때 전달되는 것은 바이트 단위의 크기 정보 밖에 전달되지 않기 때문이다. 그렇기 때문에 malloc 함수는 어떤 자료형으로 참조할지 알 수 없는 상황이기 때문에 직접 형 변환 연산을 통해 변환을 진행하는 것이 그 이유이다. 그리고 18번, 19번 라인을 통해 동적 할당한 메모리 영역에 값을 대입하고, 출력한다. 그리고 free 함수를 통해 동적 할당 해체를 진행한다.
free 함수의 호출이 필요한 이유
힙 메모리 영역은 프로그래머가 관리하는 영역이기 때문에 메모리의 동적 할당만큼 해체 또한 매우 중요하다. 할당한 만큼 해체해주지 않으면 운영체제에 의해서 메모리가 자동 해체되지만 그럼에도 free 함수를 통한 동적 할당 공간 해체가 필요한 이유는 더 큰 규모의 프로그램에서는 실행되는 동안 메모리 부족 등의 문제가 일어날 수 있기 때문에 습관적인 해체는 꼭 필요하다.
calloc 함수
malloc 함수와 똑같은 기능을 제공하나, 함수의 인자 전달 방식에 차이와 초기화 값의 차이가 있을 뿐이다. elt_size 크기의 블록 elt_count개를 힙 영역에 할당하는데, 해당 메모리 공간을 0으로 초기화 하라 라는 내용의 함수이다.
realloc 함수
realloc 함수는 ptr이 가리키는 메모리의 크기를 size 크기로 다시 조절하는 함수이다.
파일 입/출력을 위해서는 파일과 프로그램 사이에 입출력 스트림을 형성해야 한다. 파일은 운영체제에 의해서 그 구조가 결정되고 관리되는 대상이기 때문에, 파일 뿐만 아니라 스트림의 형성 또한 운영체제의 몫이다.
파일 스트림의 형성 : fopen 함수와 FILE 구조체
fopen 함수는 파일과 프로그램 사이의 스트림을 형성하도록 하는 함수이다. 첫 번째 인자는 스트림을 형성할 파일의 이름(입/출력의 대상 파일 경로), 두 번째 인자에는 형성할 스트림의 종류에 대한 정보를 문자열 형태로 전달(읽기/쓰기 모드 등)한다. 그리고 나서 이 함수는 첫 인자에 해당하는 파일과 스트림을 형성하고, 스트림 정보를 FILE 구조체 변수에 담아서 그 변수의 주소 값을 반환한다. FILE 구조체는 어떤 형태로 어떤 내용을 구성하고 있는지 알 필요가 없다. 단지 FILE 구조체에는 fopen함수가 호출 성공 시 해당 파일에 대한 정보가 담기고, 그러한 FILE 구조체 변수의 주소 값을 반환함으로써 FILE 구조체 포인터를 통해 가리킨다. FILE 구조체 포인터 변수는 파일(파일의 정보가 담긴 FILE 구조체 변수)을 가리키고 있는데 이러한 FILE 구조체 포인터를 이용해서 파일에 데이터를 저장하거나 파일에 저장된 데이터를 읽는다.
입력 스트림과 출력 스트림 생성의 예
먼저, 스트림 생성 및 소멸에 필요한 함수 소개부터 진행한다.
스트림의 소멸을 요청하는 fclose 함수이다.
파일로 출력하는, 출력 스트림을 생성하는 예제
먼저, 8번 라인에서 파일 포인터를 생성하고, 10번 라인을 통해 fopen 함수를 호출함으로써 파일 출력 스트림을 생성한다. fopen 함수는 파일에 대해서 열지 못할 경우 NULL 포인터를 반환하므로 11번 라인에서 이에 대한 검사를 진행한다. (사실, 파일에 출력을 할 때에는 파일이 존재하지 않으면 파일을 생성하고 출력을 하기 때문에 딱히 검사를 하지 않아도 되긴 하다.) 그리고 16번 라인에서 fputc함수를 이용하여 ‘a’ 문자를 출력한다. 그리고 형성한 출력 스트림을 소멸 요청하는 fclose 함수를 통해 outputFilePointer에 해당하는 출력 스트림을 소멸한다. 그리고 그 결과 파일이 생성되었고, ‘a’ 문자가 잘 출력된 것을 확인할 수 있다.
파일로부터 입력하는, 입력 스트림을 생성하는 예제
다음은 위에서 생성한 파일 속 문자를 읽어오는 예제이다.
먼저, 8번 라인에서 파일 포인터를 생성하고, 11번 라인을 통해 fopen 함수를 호출함으로써 파일 입력 스트림을 생성한다. fopen 함수는 파일에 대해서 열지 못할 경우 NULL 포인터를 반환하므로 12번 라인에서 이에 대한 검사를 진행한다. 그리고 17번 라인에서 fgetc함수를 이용하여 문자 하나를 파일로부터 입력 받아온다. 그리고 형성한 입력 스트림을 소멸 요청하는 fclose 함수를 통해 inputFilePointer에 해당하는 입력 스트림을 소멸한다. 그리고 읽어온 ‘a’ 문자가 잘 출력된 것을 확인할 수 있다.
파일의 스트림에도 중간에 입출력 버퍼가 존재한다. 이 버퍼에 값이 저장되고 차례차례 파일로 출력을 하는 상황에서 컴퓨터의 전원이 꺼질 경우 출력되다가 처리가 되지 않은 문자들은 저장되지 않는다. 그러나 fclose 함수를 통해 파일을 닫아주면 출력 버퍼에 저장되어 있던 데이터가 파일로 이동하여 출력 버퍼가 비워지고 저장 또한 일어난다. 즉 파일을 모두 사용할 경우에는 바로 fclose 함수를 호출해줘야만 한다.
그리고 fflush 함수의 특성은 그대로 적용된다.
1.출력 버퍼를 비움 -> 출력 버퍼에 저장된 데이터를 목적지로 전송
2.입력 버퍼를 비움 -> 입력 버퍼에 저장된 데이터를 소멸 (버퍼의 내용을 읽으면 소멸)
3.fflush 함수는 출력 버퍼를 비우는 함수
4.fflush 함수는 입력 버퍼를 대상으로 호출하더라도 결과가 보장되지 않는다.
출력 버퍼를 비우는 것은 fflush 함수를 호출하면 되지만 입력 버퍼를 비우는 것은 함수가 필요하지 않는다. 왜냐하면 파일에 저장된 데이터는 콘솔 입력과는 다르게 언제든 데이터를 읽을 수 있고, 파일 대상의 입력 버퍼를 비워야만 하는 상황이라는 것은 특별히 존재하지 않기 때문이다.
파일의 개방 모드(fopen 함수의 두 번째 인자에 대한 고찰)
스트림을 구분하는 기준
1.읽기 위한 스트림/쓰기 위한 스트림
+는 읽기와 쓰기가 모두 가능한 스트림의 형성을 이야기하고 a는 append로 덧붙임을 의미한다. 읽기와 쓰기가 모두 가능한 모드의 경우 메모리 버퍼를 비워줘야 하고 잘못된 사용으로 이어질 수 있다는 점을 주의.
2.텍스트 데이터를 위한 스트림/바이너리 데이터를 위한 스트림
사람이 인식할 수 있는 문자를 담고 있는 파일을 텍스트 파일이라고 하고, 컴퓨터가 인식할 수 있는 데이터를 담고 있는 파일을 바이너리 파일이라 한다. 이러한 사실과 관련해서 신경 써야하는 부분은 문장의 끝을 의미하는 개행의 표현 방식이다.
‘\n’, 개행을 나타내는 문자이지만 이것은 C언어에서만 나타낸 약속이다. MS-DOS는 \r\n, Mac은 \r, Unix 환경에서는 \n이 개행이다. 그렇다면 위의 시스템들 간에 파일을 공유할 때에는 각 시스템마다 개행을 나타내는 방식으로 변환이 필요한데 파일을 텍스트 모드로 개방하게 되면 자동으로 변환이 진행된다. 반대로 바이너리 모드로 파일을 개방하게 되면 아무런 변환이 이루어지지 않는다. 텍스트 모드로 파일을 여는 경우는 사람이 읽도록 출력이 완벽하게 되어야 하기 때문에 각 시스템에 맞도록 변환이 진행되지만, 바이너리 모드로 파일을 여는 경우는 기계가 이해해야 하기 때문에 하나의 손상도 일어나선 안된다. 변환을 통한 손상 또한 일어나서는 안된다. 그렇기 때문에 바이너리 모드는 변환을 일으키지 않는다.
앞서 두 기준을 취합하여 fopen 함수의 두 번째 인자, 스트림 형성을 완성한다.
텍스트 기반의 입/출력 함수
이미 언급한 함수 모두 사용 방법 및 설명이 같다. stream의 인자 부분에 표준 입/출력 디스크립터 대신 파일에 대한 파일 포인터를 넣으면 된다.
파일이 종료되었는지 확인 : feof 함수
바이너리 기반의 입/출력 함수
바이너리 데이터 입/출력의 경우 텍스트 데이터 입/출력의 경우보다 단순하기 때문에 다양한 함수를 제공하지 않는다.
바이너리 데이터를 입력하는 함수
fread 함수는 size 크기의 데이터 count 개를 stream으로부터 읽어 들여서 buffer에 저장하라는 의미이다.
바이너리 데이터를 출력하는 함수
fwrite 함수는 buffer에 있는 size 크기의 데이터 count 개 stream에 저장하라는 의미이다.
서식에 따른 데이터 입/출력: fprintf, fscanf
사용 방법은 첫 번째 인자에 stream을 넣어 파일을 대상으로 하는 것을 제외하고는 printf/scanf랑 완전히 똑같다.
임의 접근을 위한 ‘파일 위치 지시자’의 이동
파일 위치 지시자란? FILE 구조체의 멤버 중에는 파일의 위치 정보를 저장하고 있는 멤버가 존재한다. 이 멤버의 값은 각종 읽기 및 쓰기 함수들이 호출될 때마다 참조 및 갱신이 된다. 이 멤버에 저장된 위치 정보의 갱신을 통해서 데이터를 읽고 쓸 위치 정보가 유지되는 것이다. 이 멤버를 파일 위치 지시자라 부른다.
그런데 파일을 읽을 때 항상 앞부터 읽는 것이 아닌, 중간부터 혹은 뒤부터 읽을 필요가 종종 생기는데 이 때 이 파일 위치 지시자 멤버를 조작하여 읽을 위치를 지정하는 것이다.
위 함수는 stream으로 전달된 파일 위치 지시자를 wherefrom에서부터 offset 바이트만큼 이동을 의미한다. offset은 음수/양수 값 모두 가능하며, 다음은 wherefrom에 전달되는 상수와 그 의미를 정리한 것이다.
구조체란 하나 이상의 변수를 묶어서 새로운 자료형을 정의하는 도구이다. 예를 들어, 기존의 자료형과 별개로 3바이트짜리 자료형을 만들어내는 것, 이러한 예처럼 새로운 자료형을 기본 제공 자료형으로 추가하는 것은 불가능 하지만 기존의 자료형들을 이용하여 생성하는 변수들을 묶어서 새로운 자료형을 정의하는 것은 가능하다.
다음은 이름과 학번을 저장하는 새로운 자료형을 정의한 예를 나타낸 것이다.
학생을 관리하는 시스템에서는 학번과 이름은 함께 처리되어야 의미가 있는 자료들이다. 그렇기 때문에 위와 같이 학생의 이름을 나타내는 name 변수와 해당 이름의 학생의 번호를 의미하는 stdNum을 한데 묶어서 관리할 수 있도록 새로운 자료형을 정의한 것이다.
구조체의 정의
구조체의 정의는 다음과 같다
왼쪽은 구조체의 정의를 나타낸 것이다. 오른쪽의 예는 구조체를 정의한 예인데, 20 바이트 문자형 배열과, 4바이트 정수를 저장할 수 있는 변수들로 구성되어 있는 PersonalInfo라는 자료형을 하나 새로 만든 것이다. 이 PerosonalInfo라는 자료형은 int, float, double 등처럼 자료형의 이름이 될 수 있다. 이 이름을 통해서 사용자 정의 자료형 변수를 만들게 되는 것이다.
구조체 변수의 선언과 접근
구조체 변수의 선언
정의한 사용자 정의 자료형(구조체)을 통해 변수를 선언하는 방법은 왼쪽과 같다. 그리고 오른쪽 13번, 14번 라인은 구조체 변수를 선언한 예를 나타내고 있다.
구조체 변수를 통한 멤버 변수 직접 접근과 간접 접근
선언된 구조체 변수를 통해 멤버 변수에 접근할 수 있다. 아니, 해야만 한다. 그러기 위해 만든 구조체이기 때문이다.
왼쪽과 같이 구조체 변수 이름과 ‘ . ‘ (직접 접근 연산자)와 멤버 변수의 이름을 명시함으로써 구조체의 멤버 변수에 직접 접근하여 값을 넣거나 값을 출력 혹은 수정까지 할 수 있다.
위의 예에서는 16번 라인과 17번 라인을 통해 멤버 변수에 접근하고, 대입 및 다른 곳으로의 참조를 시키는 등의 작업이 이루어진다. 중요한 것은 구조체_변수_이름 . 구조체의 멤버 변수 형식을 갖추어 직접 접근한다는 것.
구조체(사용자 정의 자료형) 또한 일반 자료형과 같은 취급을 하기 때문에 포인터로 선언하고 주소 값을 받는 모든 행위들은 허용된다. 다음의 예가 가능하다.
위의 예에서 13번 라인에서 일반 구조체 변수를 선언하고, 15번 라인에서는 구조체 포인터 변수를 선언한다. 그리고 나서 16번, 17번 라인을 통해서 직접 접근 연산자 ‘.’을 이용하여 구조체 멤버에 직접 접근을 통해서 값을 대입한다. 그리고 19번 라인에서 구조체 변수에 기존에 선언했던 일반 구조체 변수의 주소 값을 대입한다. 중요한 것은 21번 라인의 참조 방식인데, ‘->’(간접 접근 연산자)를 통해서 접근하는 형태를 볼 수 있다. 이렇듯 구조체 포인터 변수를 통해서 가리키고 있는 변수의 멤버에 접근할 때에는 간접 접근 연산자(->)를 통해서 접근한다. 이를 간접 접근 이라고 한다.
참조를 통한 직접 접근 또한 가능하다.
22번 라인과 같이 구조체 포인터가 가리키는 대상에 대해서 접근 연산이므로 직접 접근 연산이 맞다. 포인터를 이용해서 참조한 후 직접 접근 연산자를 통해 멤버에 접근할 수 있는 방법이 제시된 예이다.
구조체 변수의 초기화
생성과 동시에 초기화
배열의 초기화와 매우 유사하다.
5번 라인에 해당하는 구조체 정의를 두고, 13번 라인에서 구조체 변수를 선언하면서 생성과 동시에 초기화를 진행하고 있는데 그 초기화 방식은 다음과 같다.
중괄호로 감싼 후, 초기화할 값들을 콤마 연산자를 통해 구분하며 나열하는데 구조체 정의 시 멤버 변수의 선언 순서대로 나열을 한다.
일반 초기화(값의 대입)
다음의 예와 같이 선언 이후에 값의 대입을 통하여 초기화를 진행할 수 있다.
구조체 배열
구조체 배열 또한 일반 변수의 배열과 마찬가지로 다를 바가 없다. 다음은 이전의 예제를 조금 변형한 구조체 배열에 대한 예제이다.
먼저, 15번 라인에서 구조체 배열을 선언한다. 두 명의 학적 정보를 저장할 수 있도록 배열로 선언한다. 그런데 배열을 선언과 동시에 초기화를 이루고 있다. 이 경우 2차원 배열의 초기화와 크게 다를 바가 없다. 구조체 정의 시 나열한 멤버 변수의 순서대로 값들을 나열하고, 그러한 나열을 행 별로 다시 구분 지어 나열한다. 그리고 20번 라인에서는 선언만 이루어졌고, 22번 ~ 26번 라인에서 대입을 통한 초기화가 이루어지고 있다. 그리고 29번 라인부터는 인덱스를 이용하여 구조체 배열의 각 요소들을 출력하는 것을 볼 수 있다.
구조체 포인터 및 구조체 첫 멤버 변수의 주소 / 구조체 변수의 주소 / 구조체 이름
구조체 포인터 또한 일반 변수의 포인터와 다를 바가 없다. 다만 다른 부분은 아래에 제시한다.
위의 결과로 미루어 보았을 때 구조체의 첫 멤버 변수의 주소와 구조체 변수의 주소는 같지만 구조체 이름은 다른 것임을 알 수 있다. (배열 이름과는 다름)
l구조체와 typedef 연산을 통한 자료형 재정의
Typedef 는 자료형을 다른 이름으로 재정의하는 연산자이다. 다음과 같은 형식으로 재정의를 진행한다.
typedef 기존_자료형 새로운_자료형_이름;
다음은 기존 자료형을 통한 자료형 재정의의 예이다.
11번, 12번 라인에서 int 자료형과 char 자료형에 대해서 새로운 이름으로 재정의가 일어났다.
다음으로는 사용자 정의 자료형에 대해서 typedef 연산을 통해 재정의가 일어난 몇 가지 경우를 표현했다. 아래에서 제시하는 모든 경우에 대한 결과는 다음과 같다.
14번 라인과 같이, 일반 자료형을 통한 변수 선언 하듯, struct 키워드를 포함하지 않더라도 구조체 변수의 선언이 가능하다. 밑에서 11번 라인에 들어가는 다양한 경우의 typedef 선언을 제시.
함수 구조체 인자 전달
구조체를 대상으로 가능한 연산
구조체 변수를 대상으로 하는 연산은 제한적으로 대입(=), sizeof, 주소(&) 연산만이 허용된다. 특히 구조체의 변수 간 대입 연산을 진행하게 되면 멤버 대 멤버의 복사가 이루어진다.
15번 라인에서 대입 연산을 진행하고 있고 그 결과, pi1과 pi2의 멤버들이 지니는 값은 모두 같은 것으로 미루어 보았을 때, 값의 복사가 이루어지고 배열의 경우에도 모조리 값의 복사가 일어나는 것을 확인할 수 있다.
구조체의 정의 이유와 중첩된 구조체의 선언
구조체의 정의가 필요한 이유
구조체를 통해서 연관 있는 데이터를 하나로 묶을 수 있는 자료형을 정의하면, 데이터의 표현 및 관리가 용이해지고, 합리적인 코드를 작성할 수 있다.
구조체를 정의하지 않고 구현한 경우 문제가 되는 점 : 인자 전달 시 매개 변수가 늘어남, 묶어서 하나의 인덱스에 대해서 관리가 불편해짐. 높은 차원의 배열이 계속 생성됨. 등
중첩된 구조체의 정의와 변수 선언
구조체 내에 구조체 변수를 선언할 수 있다. 이때 초기화 할 경우에는 순서에 주의하고, 중첩된 구조체 변수에 대한 초기화 시 중괄호를 통해 구분한다.
공용체(union)
공용체는 구조체와 유사한 개념이긴 하나, 다른 점은 멤버 변수들이 하나의 공간을 공유하는지의 차이가 있다.
위의 예제는 구조체와 공용체의 차이를 나타내는 예제이다. 공용체의 정의나 선언 방법은 구조체와 완전히 똑같다고 보면 된다. 그러나 차이는 24번 ~ 28번 라인에서 볼 수 있다.
24번 라인에서는 구조체의 멤버의 각 주소 값을 출력하는데 결과를 보면 자료형 별 차이만큼 주소 값이 잘 할당되어 있는 것을 확인할 수 있는데 25번 라인에서 공용체의 멤버의 각 주소를 출력하는데 결과를 보니 모두 같은 주소(공용체의 시작 주소) 값을 가지고 있었다. 게다가 이상한 것은 이 뿐만이 아니다. 27번 라인에서 sData의 크기는 멤버 변수들의 메모리 공간 할당 크기들을 합한 결과 값이 나오는데, uData의 크기는 멤버 변수 중 가장 할당 크기가 큰 자료형의 길이 값이 나왔다.
위의 그림은 구조체와 공용체의 멤버 변수들의 할당 구성을 나타낸 것이다. 굵은 검정 칸이 실제로 할당된 메모리 공간이 된다. 구조체 할당 구성에서는 각 멤버 변수 별로 크기만큼 할당이 되어 있는 것을 볼 수 있는데 공용체 할당 구성에서는 가장 큰 double 자료형의 크기만큼만 공간을 할당하고 모든 멤버 변수가 이 공간을 공유하는 형태로 할당 구성이 이루어진다.
이러한 공용체는 다음과 같은 예제에서 유용하게 사용될 수 있다.
정수 하나를 입력 받고, 입력 받은 정수의 상위 2바이트와 하위 2바이트의 값을 양의 정수로 출력하고, 상위 1바이트와 하위 1바이트에 저장된 아스키 문자 출력하기
열거형
구조체나 공용체는 자료형을 명시함으로써 멤버에 저장할 값의 유형(type)을 결정하였다면, 열거형의 경우는 저장이 가능한 값 자체를 정수의 형태로 결정한다.
0, 1, 2, 3, 4, 5, 6 값들을 저장 가능한 color형 자료형
Color 자료형으로 선언되는 모든 변수들은 enum Color 정의에서 정의해 둔 상수들을 값으로 갖을 수 있다. 또한 정의된 상수들은 어디서든 지역 범위를 고려하여 사용할 수 있다. enum 정의 및 변수 선언은 구조체, 공용체와 완전히 똑같다. 그리고 상수 정의 시, 어떤 값도 넣지 않으면 순서대로 0 값부터 값이 1씩 올라가는 형태로 값의 배정이 이루어진다.
다음의 예에서 특이한 점을 발견할 수 있다.
위에서는 정의된 상수들에서 RED에 값을 지정하지 않았으므로 0부터 시작하는데 YELLOW는 10이라는 값을 지정했다. 그리고 그 결과 YELLOW 이후 부터는 10부터 1씩 증가한 값들로 배정된다.
열거형은 위와 같이 활용될 수 있다.
열거형의 유용함은 이름있는 상수의 정의를 통한 의미의 부여에 있다. 둘 이상의 연관이 있는 이름을 상수로 함께 같이 묶어서 선언함으로써 프로그램의 가독성을 높인다.
프로그램과 입력 및 출력 장치를 연결해주는 소프트웨어적으로 구현되어 있는 도구, 입출력 장치와 프로그램을 연결해주는 가상의 다리이다. 운영체제가 이를 제공한다.
문자 단위 입/출력 함수
EOF란 End Of File의 약자로 파일의 끝을 표현하기 위해 정의된 상수이다. 위의 함수가 파일이나 콘솔의 출력으로 EOF를 반환하게 되면 파일이나 콘솔로부터 더 이상 입력 받을 것이 없다 라는 의미를 갖는다. 그렇다면 EOF를 반환하는 시기는 다음과 같다.
함수 호출의 실패할 경우 컨트롤 + z / 컨트롤 + d 키가 입력될 경우
문자를 입력 받는 함수인데 반환 형이 정수 int 형인 이유는? 컴파일러에 따라 char을 unsigned char로 처리하는 경우가 존재한다. 그런데 EOF 라는 값은 -1로 정의되어 있는데 char을 unsigned char로 처리하는 컴파일러는 EOF에 대한 처리가 불가능하다. 그렇기 때문에 같은 정수 타입인 int로 반환한다.
문자열 단위 입/출력 함수
기존의 scanf 함수 등은 공백을 포함한 문자열을 입력 받기에 있어서 크게 제한이 되었다. (공백을 기준으로 입력을 나누기 때문)
표준 입/출력 버퍼
표준 입/출력 함수를 통해서 데이터를 입/출력하는 경우, 해당 데이터들은 운영체제가 제공하는 메모리 버퍼를 꼭 통과하게 되어 있다. 메모리 버퍼는 데이터를 임시로 저장해 놓는 메모리 공간인데, 이런 저장하는 작업을 버퍼링 이라고 한다.
키보드로 데이터를 입력하면(문자열 입력 후 엔터를 칠 경우) 해당 데이터들은 입력 버퍼로 전송되어 저장되고(버퍼링), 그 이후에 프로그램으로 데이터들이 전송이 된다.
버퍼링을 하는 이유?
프로그램 연산(CPU 연산)에 비했을 때 I/O 장치들 과의 I/O 작업은 상당히 시간이 걸리는 작업이다. 즉, 데이터를 읽어오는데 걸리는 시간이 CPU의 연산 처리 속도보다 훨씬 느리다. 그렇기 때문에 CPU는 그러한 느린 데이터 입출력 처리를 기다리고 있어야 하는데 데이터를 읽을 때마다 조금씩 전송을 하는 구조로 진행되면 데이터 전송의 효율성이 떨어진다. 그렇기 때문에 중간에 메모리 버퍼를 두고 데이터를 한데 묶어서 보내면 빠르고 효율적인 데이터 전송이 될 수 있다.
출력 버퍼와 입력 버퍼 비우기!
출력 버퍼를 비운다는 것은 출력 버퍼에 저장된 데이터가 버퍼를 떠나서 목적지로 이동되는 것을 이야기하는데, 이러한 역할을 하는 함수가 존재하는데 다음과 같다.
위 함수는 파일을 대상으로도 버퍼 비우기가 가능하고, 출력 버퍼를 비우는 일은 많이 존재하지는 않는다.
반면에, 입력 버퍼를 비운다는 것은 출력 버퍼와는 다르게 데이터의 소멸을 의미한다. fflush 함수를 이용하여fflush(stdin); 과 같은 형태로 호출하면 입력 버퍼는 비워지거나/비워지지 않거나 보장할 수 없는 결과를 나타낸다. (Windows 계열의 컴파일러의 경우는 입력 버퍼를 지워 준다고 한다.)
입력 버퍼를 비우기 이전에 입력 버퍼를 비워야 하는 경우에 대한 예제이다.
먼저, 6번 라인에서 str1의 크기를 10 바이트로 지정해주었고, 10번 라인에서 str1에 문자열을 대입하는데 9자리의 문자를 대입한다. 9자리라고 함은 문자열 9자리와 NULL 문자 1바이트를 고려한 것이다. 그러나 입력될 때에는 “abcdefghi\0\n” 이 입력되고 입력 버퍼에 들어가게 된다. (fgets함수의 ‘\n’이 입력될 때까지 읽어 들이는 특성 상) 그래서 10번 라인에서는 입력한 “abcdefghi” 문자열에 ‘\0’ 문자가 합쳐져서 입력으로 str1에 저장된다. 그리고 13번 라인에서 fgets 함수가 호출되는데 이 때 입력 버퍼에 남은 ‘\n’ 문자를 읽어버리고 바로 호출이 끝나게 된다. 그렇기 때문에 실행 결과에서는 str2에 ‘\n’만 저장된 결과가 나타난 것이다. 이러한 경우 str1의 저장 이후 입력 버퍼를 비웠더라면 다음 문자열 입력을 받을 수 있었다. 이것이 입력 버퍼 비우기의 필요성이다.
입력 버퍼를 비운다는 것은 문자들을 읽어 들이면 지워진다, 즉 비워진다. 다음과 같은 함수로 입력 버퍼를 지울 수 있다.
23번 라인에서 한 문자씩 읽는데 그 읽은 것이 ‘\n’ 개행 문자일 경우 탈출하게 된다. 즉, 개행 문자까지 버퍼로부터 문자를 읽음으로써 버퍼가 비워진다.
문자열의 길이를 반환 : strlen
일단, size_t는 typedef unsigned int size_t; 로 자료형 재정의가 되어 있다.
문자열의 복사 : strcpy
strcpy는 src의 문자열을 dest에 복사한다. strncpy의 경우에는 src의 문자열을 dest에 복사하되, n 크기만큼 복사한다. 단, strncpy의 경우 마지막 문자가 NULL인지에 대한 검사는 하지 않고 주어진 n만큼만 복사를 한다. 즉, 복사된 문자열의 끝에서 NULL에 대한 처리를 진행하지 않기 때문에 직접 NULL 처리를 해줘야 한다. 이후에 기재되는 모든 n이 들어가는 함수는 NULL처리가 진행되지 않는 공통점을 가지고 있다.
문자열 연결 : strcat
Dest 문자열 뒤에 src 문자열을 붙이고 그 값을 dest에 저장한다. 앞 문자열의 NULL 문자를 없애고 그 자리부터 뒷 문자열이 시작된다.
함수 또한 메모리 상에 바이너리 형태로 올라가 호출 시 실행이 되는데, 메모리 상에 저장된 함수의 주소 값을 저장하는 포인터 변수가 함수 포인터 변수이다.
배열의 이름이 배열의 시작 주소 값을 의미하듯이 함수의 이름 또한 함수가 저장된 메모리 공간의 시작 주소 값을 의미한다.
함수 이름의 포인터 형은 반환 타입과 매개변수의 선언을 통해 결정 짓도록 약속. 즉, 반환 타입과 매개변수의 선언이 일치하는 모든 함수들의 포인터 형은 모두 같다.
9번 라인과 10번 라인에서 볼 수 있듯이
반환 타입 (*포인터변수이름) (매개 변수) = 함수 이름
다음과 같은 형태로 함수 포인터 변수를 선언하고 함수의 주소를 받아서 12번, 13번 라인처럼 함수 포인터 변수를 통한 함수 호출이 가능하다.
Void 형 포인터 ( 자료 형이 없는 포인터 )
다음과 같이 선언되는 포인터를 void 형 포인터 변수라고 한다.
Void 형 포인터는 어떤 변수의 주소 값이든 함수의 주소 값이든 상관 없이 ‘주소’ 값의 경우는 무엇이든 전부 담을 수 있다. 다음 예는 void * 변수에 다양한 주소 값을 저장하는 예를 나타낸 것이다.
단, void 형 포인터 p를 이용하여 참조를 하는 순간(=포인터 연산을 하는 순간) 컴파일 에러가 발생한다. 왜냐하면 void * 포인터는 포인터 연산 시 증감의 크기에 대한 정보가 단 한 부분도 없다. 즉, 해당 주소로부터 어느 범위까지 연산을 하여 참조를 해야하는지에 대한 정보가 없기 때문에 참조(포인터 연산)가 불가능하다.
void형 포인터는 주소 값을 저장하는 것에만 의미를 두고 포인터 형은 나중에 결정한 참조하는 경우에 유용하게 사용된다 ->동적 메모리 할당
Main 함수를 통한 인자 전달
위의 예제에서 argc는 파일 이름을 포함한 전달 되어온 인자의 수를 나타내고, argv는 더블 포인터 형태로 싱글 포인터 혹은 싱글 포인터 배열의 각 요소를 가리킨다. 즉, argv는 두 번째 결과에서 “./t”, “yoo”, “seung”, “ho”의 주소 값인 char * 타입을 가리키기 때문에 더블 포인터 형태이다.
정수형 2차원 배열의 배열 이름은 정수형 더블 포인터 형이라고 오해하고 2차원 배열의 이름을 매개 변수 혹은 변수로 주소 값을 받을 때 더블 포인터 변수로 받을 수 있다.
<2차원 배열과 포인터의 잘못된 관계>
더블 포인터로 받을 수 있는 것은 포인터 배열의 이름이다.
2차원 배열의 배열 이름의 포인터 형은?
먼저, 알아야 할 것은 arr과 arr[0]의 차이를 알아야 한다.
arr과 arr[0]은 2차원 배열의 첫 시작 주소를 가리키지만 차이점이 존재한다. sizeof 연산자를 이용하여 11번째 라인, 12번째 라인처럼 사이즈를 체크했을 때 arr은 배열의 전체 길이를 나타냈지만, arr[0]의 경우는 해당 행의 길이를 반환하였다.
그리고 다음으로는 배열 이름을 이용하여 포인터 연산을 행했을 때 어떤 연산 결과가 나타나는지 알아야한다.
2차원 배열 이름에 +1, +2 포인터 연산을 진행하였을 때 각 주소는 8씩 증가하는 것을 볼 수 있다.
이번에는 2차원 배열의 열에 해당하는 인덱스를 2에서 4로 변경한 후 포인터 연산을 진행했는데, 이 때 주소의 증가 폭은 16이 증가하였다.
두 경우를 잘 살펴보면 같은 정수형 2차원 배열 임에도 불구하고 포인터 연산의 증감 폭이 다르다. 포인터 연산 시 증감하는 폭을 결정하는 것은 2차원 배열에서 열의 길이에 따라서 달라짐을 알 수 있다.
두 가지 요인을 이용하여 최종 결론을 내리자면 먼저, 2차원 배열에서도 배열 이름을 통하여 배열 전체를 대표하고, 이 배열 이름을 통하여 포인터 연산을 행한다는 점과 같은 타입의 2차원 배열이라고 할지라도 배열 이름을 통하여 포인터 연산을 행할 때 증감의 차이는 열의 길이에 따라서 증감의 폭이 결정된다. 결국, 이 증감의 폭까지 같아야 비로소 같은 포인터 형이 된다는 점.
위의 결론을 토대로 7번 라인에서 알 수 있는 정보는 2차원 배열 이름(2차원 배열의 첫 주소)과 포인터 연산 시 증감하는 크기 그리고 자료 형을 알 수 있기 때문에 이를 반영하여 8번 라인처럼 2차원 배열을 받는 포인터 변수를 선언할 수 있다. pArr은 정수형 포인터 변수인데 포인터 연산 시 sizeof(int) x 열의 길이(4)만큼 증감하는 2차원 배열의 주소를 받는 포인터다 라고 해석하면 된다.
포인터의 포인터는 포인터 변수를 가리키는 또 다른 포인터 변수를 뜻한다.마찬가지로 포인터의 포인터의 포인터는 포인터 변수를 가리키는 또 다른 포인터 변수를 가리키는 또 다른 포인터 변수를 뜻한다.
포인터 변수는 종류와 상관 없이 주소 값을 저장하는 변수인데, 다만 더블 포인터와 일반 싱글 포인터의 차이점은 가리키는 대상이 또 다른 포인터 변수인지, 일반 변수인지의 차이이다.
위의 예는 일반 변수, 싱글 포인터, 더블 포인터의 사용 예와 세 변수 간의 관계를 값으로 나타낸 예이다.
먼저, 7번 라인에서 일반 정수형 변수를 선언, 그 주소 값을 8번 라인에서 포인터 변수에 저장한다. 그리고 다시 그 포인터 변수의 주소 값을 9번 라인의 더블 포인터를 선언하고 저장한다. 다음은 11번 라인 ~ 13번 라인의 출력에 대한 결과를 나타내었다.
위의 실행 결과에서 첫 번째 결과 라인에서의 일반 정수형 변수의 주소 값을 출력 하였고, 이 주소 값을 두 번째결과 라인에서 싱글 포인터 변수에 값으로 대입되어 싱글 포인터 변수 pNum이 일반 변수 number를 가리키고 있는 형태를 이루었다. 그리고 마찬가지로 두 번째 결과 라인에서 싱글 포인터 변수의 주소 값을 출력 하였고, 이 주소 값을 세 번째 결과 라인에서 더블 포인터 변수에 값으로 대입되어 더블 포인터 변수 dpNum이 싱글 포인터 변수 pNum을 가리키는 형태를 이루고 있다.
두 번째 실행 결과 라인에서 * (애스터리스크) 연산자를 이용하여 pNum이 값으로 가지고 있는 주소에 접근하여 값을 출력한다.(가리키는 변수의 값을 출력) 세 번째 실행 결과 라인에서 **(더블 애스터리스크)를 이용하여 더블 포인터가 가지고 있는 싱글 포인터 주소에 접근하고 또 그 싱글 포인터가 가지고 있는 주소에 접근하여 값을 출력한다.(가리키고 가리키는 변수의 값을 출력)
사실, 참조의 더블 포인터(**)는 *(*더블 포인터 변수) 에서 소괄호가 생략된 형태이다.
포인터 배열의 포인터 형
8번 라인에서 정수형 포인터 타입 배열을 선언하여 n1과 n2의 주소 값을 배열의 각 요소에 대입하였다. 1차원 배열 이름의 포인터 형을 배열 이름이 가리키는 요소의 자료형을 고려하여 결정하듯이 1차원 포인터 배열 이름의 포인터 형 또한 배열 이름이 가리키는 요소의 자료형을 고려하면 된다. 11번 라인에서 배열 이름과 배열의 첫 번째 요소의 주소 값을 출력했는데 그 결과는 배열 이름 arr과 arr 배열의 첫 번째 요소의 주소가 일치했다. 이 때 배열 이름은 int * 형의 요소를 가리킨다. 즉 1차원 포인터 배열의 배열 이름은 더블 포인터 상수라고 할 수 있겠다. 그렇기 때문에 9번 라인에서 더블 포인터 변수에 배열 이름 arr을 아무런 처리 없이 대입하더라도 문제 없이 대입이 된다. 이어서 12번 라인에서 더블 포인터 변수를 이용하여 더블 포인터 연산을 통하여 포인터 배열의 각 요소에 접근하여 값을 출력하는 예제이다.