혼자 연구하는 C/C++ 11장 정리(배열과 포인터)

2015. 3. 2. 20:12창고

728x90
728x90
1. 첨자연산

배열의 내부적 처리

배열과 포인터는 긴밀한 관계를 가지고 있다. 유사점과 차이점을 분명하게 살펴보고 넘어가야 한다.


C에서 배열의 특징

- 1차원 배열만 지원한다. 2차원 이상의 다차원 배열은 1차원 배열의 확장에 불과하다.

- 배열을 구성하는 배열 요소의 타입에는 전혀 제한이 없다. T형 변수를 선언할 수 있으면 T형 배열도 선언할 수 있다.


배열끼리 중첩되어 있을 때 다른 배열에 포함된 배열을 부분배열이라고 부르고 

부분 배열을 배열 요소로 가지는 배열을 전체 배열이라 한다.


int ar[3][4]가 있다고 가정했을 때. 이 배열을 그림으로 그려보면 다음과 같다.

이것을 부분 배열과 전체 배열을 이용하여 선형적으로 그려보면 다음과 같다.


- 전체 배열 ar은 arr[0], arr[1], arr[2] 세 개의 부분 배열을 요소로 가지는 크기는 3의 1차 배열이다.

- 부분 배열 ar[0]는 ar[0][0], ar[0][1], ar[0][2] 세 개의 부분 배열을 요소로 가지는 크기 3의 1차 배열이다.


[] 연산자

배열의 한 요소를 참조할 때 사용하는 [] 연산자는 괄호 안에 읽고자 하는 배열 요소의 첨자를 적는다.

ar[3]은 ar배열의 3번째 요소를 읽는다.(0번째가 있다고 가정하고)


ptr이 임의의 배열을 가리키는 포인터고 n이 정수일 때

ptr[n]= *(ptr+n)


1
2
3
4
int ar[5= {1,2,3,4,5};
printf ar[2]
printf *(ar+2)
printf 2[ar]
cs




2[ar]이라는 표현식이 어색할 수 있다. 정의에 의해 2[ar]은 *(2+ar)이 되고 덧셈은 교환 법칙이 성립하므로 *(ar+2)와 같다.(충격!!)


사실상 ptr[n] 표현식은 컴파일러에 의해 *(ptr+n)으로 바뀐 후 컴파일되며 생성되는 기계어 코드도 완전히 동일하다.

ar[2][1]은 정의에 의해 *(*(ar+2)+1)이 되며 컴파일러는 이 포인터 연산식을 실행할 것이다.


- ar+2

전체 배열명인 ar은 선두 번지를 가리키는 포인터 상수이다. 

이 번지에 2를 더했으니 ar 배열 요소 중 2번째 요소를 가리키게 될 것이다.

- *(ar+2)

이 연산식은 정의에 의해 ar 배열의 2번째 부분 배열 ar[2]이다.

*연산자는 피연산자의 크기만큼 읽어내는데 ar+2가 부분 배열이므로 읽혀지는 값은 정수형이 아니라

크기 4의 변수 배열형이 될 것이다.

여기서 sizeof(*(ar+2))를 해보면 16의 값이 나오는데 배열 그 자체로 평가되기 때문이다.

*(ar+2)는 ar의 부분 배열 ar[2]와 완전히 같고 그 값은 ar[2] 부분 배열의 선두 번지(&ar[2][0])로 평가된다.

- *(ar+2)+1

부분 배열 ar[2]에서 1번째 배열 요소를 가리키는 포인터가 된다.*(ar+2)가 부분 배열 명이므로 포인터 상수이고

포인터 상수에 정수를 더했으므로 결과는 포인터가 되는 것이다.

- *(*(ar+2)+1)

마지막으로 * 연산자를 사용하여 3단계에서 구한 포인터에 들어 있는 값을 읽었다.

2. 포인터 배열

정의

요소가 포인터형인 배열이다.


1
int* arpi[5];
cs


이 선언문에 사용된 *와 []는 둘 다 구두점이다.

*가 앞쪽에 있으므로 arpi는 먼저 정수형 포인터가 된다.

[]에 의해 그런 포인터 변수 5개를 모아 배열을 선언하게 된다.


1
char* arps[] = {"cat","dog","pig"};
cs



초기식에 사용된 "car", "dog", "pig" 같은 문자열 상수는 문자열의 시작 번지를 가리키는 포인터 상수인데

arps의 각 요소들은 이 번지들로 초기화된 것이다.


포인터 배열의 활용

1
2
3
4
5
6
7
8
9
10
11
int* ar[3];
int num[3];
int i;
 
for 3
    printf number of student
    scanf num
    ar[i] = (int*)malloc(num[i]*sizeof(int));
 
for 3
    free(ar[i]);
cs


각 학급의 학생수를 실행 중에 입력받아 num 배열에 저장한다.

입력된 수만큼 메모리를 동적할당 후 ar 배열에 차례대로 그 번지를 저장했다.

학생 수가 1반부터 12, 7, 9명이라 가정하면 다음과 같은 그림처럼 할당된다.

포인터 배열

ar[n]은 정수형 변수 n개를 모아 놓은 정수형 배열이다. (n은 변수가 아니라 임의의 상수)

sizeof(ar)은 20이 될 것이다. ar이라는 배열의 이름은 배열의 시작 번지를 가리키는 주소값으로 평가된다.

pi는 정수형 변수 하나의 위치를 가리킬 수 있는 포인터이다. 

pi 자체는 하나의 포인터 변수일 뿐 pi를 위해 할당하는 메모리를 4바이트이다.

sizeof(pi)는 4가 된다. pi는 &i 같은 정수형 변수의 주소값을 대입받는다.

그러나 동적으로 메모리를 할당하면 pi도 벙수형 배열을 가리킬 수 있다.


pi = (int*)malloc(n*sizeof(int));


이렇게 하면 pi는 배열 이름인 ar과 똑같은 역할을 할 수 있다.


배열과 포인터는 공통점을 가진다.

- 일단 동일 타입의 변수 집합을 다룰 수 있다는 점에서 긴으적으로 같다.

- 둘 다 범위 점검을 할 수 없다는 제약도 동일하다.

- 개발자가 크기를 넘지 않도록 알아서 조심해야 한다.


배열과 포인터의 차이점

- 포인터는 변수인데 비해 배열은 상수이다.

- pi가 가리키는 배열의 크기는 동적으로 결정할 수 있지만 ar이 가리키는 배열의 크기는 선언할 때 정적으로 결정된다.

- 배열은 그 자체가 크기 때문에 함수의 인수로 전달할 수 없지만 포인터는 대상체가 무엇이든간에 4바이트의 크기밖에 차지하지 않으므로 함수로 전달할 수 있다.

- 배열로 요소를 읽는 것과 포인터로 대상체를 읽는 동작은 속도 차이가 있다.

배열은 첨자 연산을 매번 배열 선두에서부터 출발하지만 포인터는 대상체로 직접 이동해서 바로 읽으므로 액세스 속도가 빠르다.

*pi는 pi가 가리키는 곳을 바로 읽지만 ar[n]은 *(ar+n)으로 일단 번지를 더한 후 읽어야 하므로 조금 느리다.

대규모 루프에선 이 속도차를 무시 못한다. 대략 포인터가 배열보다 두 배 정도 빠르다.


3. 배열 포인터

배열 포인터

포인터 배열(Array of Pointer) - 그 원소가 포인터인 배열이다. 결국 배열.

배열 포인터(Pointer to Array) - 배열의 번지를 담는 포인터 변수이다. 결국 포인터.


배열 포인터는 배열이 여러개 모여 있는 배열의 배열, 2차원 이상의 배열에서만 의미가 있다.

1차원 배열에 대한 배열 포인터라는 것은 없다.


다차원 배열의 대표격인 2차원 배열의 포인터를 선언하는 방법은 다음과 같다.

요소형(*포인터명)[2차 첨자 크기]


1
2
3
4
5
6
7
8
char arps[5][9= {"cat","dog","pig","bird","donkey"};
char (*ps)[9];
 
ps = arps;
int i;
 
for    5
    printf *ps++
cs


*ps9로 읽으면 크기 9의 문자형 배열(곧 문자열)이 읽혀지고 ps++, ps--는 대상체의 크기인 9바이트 앞뒤로 이동하게 된다.


배열 인수

배열 포인터는 자신이 가리키는 대상체 배열의 크기를 정확하게 알고 있다. 그래서 타입이 다른 배열은 이 변수에 대입할 수 없다.


1
2
3
4
5
6
7
int ari[][7]={1...14};
 
int (*pa)[7];
int (*pb)[8];
 
pa = ari; //O
pb = ari; //Error
cs

다. ari는 2차 첨자의 크기가 7인 정수형 2차 배열이다. 

pa는 이 배열의 부분 배열을 가리킬 수 있는 배열 포인터로 선언되었으므로 대입할 수 있다. 

반면 pb는 2차 첨자가 8인 배열을 가리키는 배열 포인터이므로 대입할 수 없다.

pb = (int(*)[8])ari;로 강제 캐스팅은 가능!

배열 인수 표기법
배열을 함수로 전달할 필요가 있을 때 어떻게 할까?

1
2
3
void OutArray(int ar[]);
void OutArray(int *ar);
void OutArray(int ar[5]);
cs


다음 세 함수표기는 완전히 동일하다.

int ar[]이라는 표기는 int* ar과 완전히 동일라며 배열이 아니라 포인터이다.

++, -- 연산자로 앞뒤로 이동할 수 있다. 상수가 아님!

ar[5]에서 상수값은 완전히 컴파일러가 무시한다.


int* ar과 int ar[]의 차이?

int* ar - 이 인수가 포인터라는 것을 강조한다. 호출원에서 &i 등을 넘길때는 이런 표기법을 쓰는 것이 좋다.

int ar[] - 이 인수가 배열로부터 온 포인터라는 것을 강조한다. 호출원에서 arScore, arValue 같은 배열명으로 넘어오는 것이 좋다.


이차 배열 인수

이차 이상의 배열을 함수로 넘길 때는 배열 포인터를 사용해야 한다.

단순 포인터를 넘기면 배열의 시작 위치만 알 수 잇으며 개수를 함께 넘기더라도 배열의 모양은 알 수 없을 것이다.


1
2
3
4
5
6
7
void func(int ar[][3], int size);
 
void main()
{
    int ar[2][3= {{1,2,3},{4,5,6}};
    func(ar, 2);
}
cs


첨자가 3이 아닌 배열은 전달 받지 못한다.

이차원 배열을 인수로 전달하는 방법은 사실 거의 실용성이 없고 실제 프로젝트에서 사용할 일도 없다.


이차 배열 할당

배열과 포인터에 대한 이론적인 연구를 위한 학술적인 내용이다.

동적 할당 기능을 사용하면 실행 중에 원하는 크기대로 배열을 할당한다. 2차원 배열도 동적으로 할당할 수 있을까?


1
2
char* p = (char*)malloc(3*4*sizeof(char));
free(p);
cs


배열의 최종 요소가 char형이므로 char*로 받았다. 

지만 이것은 길이 12의 문자형 일차 배열을 할당하는 것이지 2차 배열은 아니다.

이를 해결하려면 배열 포인터로 받아야 한다.


1
2
3
4
5
char (*p)[4= (char (*)[4])malloc(3*4*sizeof(char));
strcpy "dog"
strcpy "cat"
strcpy "pig"
free(p);
cs


이렇게 하면 char ar[3][4] 정적 배열을 동적으로 할당하는 것이며 2차원 배열처럼 사용할 수 있다.

2차원 배열이 아니라 1차원 배열이라고 보는 것이 타당하다.


1
2
3
int n = 5;
int* pi = (int *)malloc(n*sizeof(int));
free(pi);
cs


크기 5의 정수형 1차 배열을 할당하는데 이때 크기를 지정하는 n은 상수!

2차 배열도 변수로 폭과 높이를 지정해볼까?


1
2
3
int n = 3;
char (*p)[4= (char(*)[4])malloc(n*4*sizeof(char));
free(p);
cs


일차 첨자의 크기를 변수 n으로 지정할 수 있다.

하지만 이차 첨자는 변수로 바꿔서 할당하면 에러가 난다.

두 첨자의 크기를 실행 중에 마음대로 변경해서 그러므로 제대로 2차원 배열을 할당했다고 할 수 없다!

1
2
3
4
5
6
7
int i;
typedef char c4[4];
c4 *p=(c4*)malloc(3*sizeof(c4));
strcpy "dog"
strcpy "cow"
srtcpy "cat"
free(p);
cs


c4를 크기 4인 문자형 배열로 타입을 정의하고 c4의 1차원 배열을 동적으로 할당한다.

이 때 malloc의 첫 번째 인수 4은 상수가 아닌 변수로 줄 수 있다!

하지만 c4의 정의문에 있는 4는 타입의 일부이므로 변수로 지정할 수 없으며 컴파일시에 정해져야 한다.

결국 원래 의도한대로 하려면 이중 포인터를 사용해야 한다.


1
2
3
4
5
6
7
8
char** p;
= (char**)malloc*n*sizeof(char*));
for n
    p[i] = (char*)malloc(m*sizeof(char));
 
for n
    free(p[i]);
free(p);
cs


메모리의 여기저기에 흩어져서 할당되어 있기는 하지만 분명히 실행 중에 할당된 이차원 배열이라고 할 수 있다.


&ar

ar을 부분 배열로 가지는 가상의 전체 배열에 대한 이차 배열 포인터 상수이다.


4. 배열과 문자열

문자열 상수

컴파일러는 문자열 상수를 다른 상수들과 달리 아주 특별하게 취급한다.

문자열 상수는 실행 파일에 같이 기록되며 정확히는 정적 데이터 영역에 기록된다.

함수 내에서 지역적으로 사용되는 문자열 상수더라고 이곳에 저장된다.


문자열 상수는 왜 실행파일에 기록해놓고 따로 저장하는가?

문자열 상수는 그 자체가 배열이고 길이가 굉장히 길 수 있다.

그래서 기계어 코드로 mov[str] "아주 긴 문자열" 이런 명령을 CPU는 지원하지 않는다.

그래서 문자열을 어딘가에 기록해 놓고 포인터를 사용해 메모리끼리 복사해서 사용하는 것이다.

그 장소가 바로 프로그램 뒤쪽의 정적 데이터 영역이다.


문자열 상수는 정적 데이터 영역에 기록되며 그 자체는 문자열의 시작 번지를 가리키는 문자형 포인터 상수이다.


문자열 상수를 특별하게 취급한다는 두 가지 사항!

- 두 번 이상 사용하면 그 문자열은 한 번만 기록된다.

- 컴파일러는 공백이나 탭, 개행 코드 등으로 구분된 콤마나 영문자 등이 없는 연속된 문자열 상수들을 하나로 합쳐서 기록한다.


행 계속 문자 \를 행의 끝에다 붙이면 다음 줄을 하나도 합쳐 준다.


문자 배열 초기화

C는 가장 깁노적인 자료형인 문자열형이 없다.

왜 없는가? 배열을 사용하면 문자열을 훌륭하게 표현할 수 있기 때문이다.


최대 10문자를 저장하고 싶다면?

1
char str[11];
cs


여기서 왜 배열의 크기를 10으로 하지 않고 11로 하는가?

문자열은 끝 표시를 위해 항상 제일 끝에 NULL 종료 문자(\0)를 붙이기 때문이다.


1
char message[] = "이름을 入力하시오.";
cs


정확한 문자열 길이를 몰라도 공백으로 초기화 할 수 있다.

만약 이런 초기화 방식을 허용하지 않는다면 한글 코드를 일일이 조사해서 써야 하므로 아주 불편할 것이다.


문자형 포인터

문자열이란 문자들의 집합이다. 그 시작 번지를 곧 문자열이라 할 수 있다.

문자형 포인터로 문자열을 다룰 수 있다.


1
char* ptr = "Korea";
cs


정적 데이터 영역에 "Korea"라는 문자열 상수가 저장되고 ptr은 번지를 가리키는 포인터로 이 문자열 상수를 가리킨다.


1
2
char str[10];
str = "Korea"// ERROR
cs


배열명 그 자체는 포인터 상수이기 때문에 한 번 정해지면 다른 번지를 가리킬 수 없다. 

1
2
3
4
5
6
7
8
9
10
11
void main()
{
    char str[] = "Korea";
    char* ptr = "Korea";
    
    ptr="China";
    str="China"// Error
 
    ptr[0= 'C';// Error
    str[0= 'C';
}
cs


ptr은 포인터기 때문에 다른 문자열 상수 "China"를 대입하는 것이 가능하다.

그러나 str은 배열명이기 때문에 포인터 상수이므로 대입이 불가능하다


반면 str 배열은 문자열의 사본을 가지고 있으므로 그 내용을 바꿀 수 있다.

str[0]에 문자 'C'를 대입하면 'COREA'가 된다.

정적 데이터에 있는 값을 복사해서 가져오기 때문에 가능한 것이다.

ptr은 그 자체가 포인터 변수이므로 다른 번지를 가리킬 수 있지만

ptr이 가리키는 내용을 함부로 변경하진 못한다.

ptr[0]은 정적 데이터 영역의 값을 바꾸는 것이 되므로 Win32 환경에선 불가능하다.


문자열 배열

문자열은 문자의 배열로 표현된다. 문자열 여러 개를 모아서 저장하고 싶다면 어떻게 할까?

이럴 땐 2차원 문자 배열로 저장할 수 있다.

예시를 보자.

1
2
3
4
5
6
7
void main()
{
    char arCon[][32= {"Korea","America","Iran","Russia"};
    int i;
    for    sizeof(arCon)/sizeof(arCon[0])
        puts arCon[i]
}
cs


arCon[0] ~ arCon[3]까지 부분 배열을 출력하면 나라 이름을 출력할 수 있을 것이다.


문자형 포인터를 써보면 어떨까?

1
2
3
4
5
6
7
8
void main()
{
    char *pCon[] = {"Korea","America","Iran","Russia"};
    int i;
    for    sizeof(arCon)/sizeof(arCon[0])
        puts pCon[i]
}
 
cs


문자형 포인터 배열을 4개의 국가명으로 초기화 했다.

문자형 2차 배열로 만들면 최대 길이가 동일한 직사각형(Rectangular) 배열이고

문자형 포인터 배열은 오른쪽 끝이 들쭉 날쭉한(Ragged) 배열이라고 한다.


포인터를 쓰는 것이 메모리 용량면에선 유리하지만 실행 중에 이 문자열을 바꿀 수 없다는 단점이 있다.


실행중에 편집이 가능한 Ragged 문자열 배열을 만드는 것도 가능하다!

필요한 최대 개수만큼 char* 배열을 선언하고 각 요소에 대해 원하는 길이만큼 동적할당하면 된다.

문자열의 개수까지도 실행 중에 결정하고 싶다면 char** 변수를 선언하고 포인터 배열을 동적할당하고 각 배열 요소인 포인터를 또 동적할당 하면 된다. - 이 부분은 연구가 좀 더 필요할듯.


이것만을 알아두고 가자!

- &,* 연산자의 기능과 포인터로 변수를 간접적으로 참조하는 방법

- 함수가 참조 호출로 실인수 값을 변경하는 방법과 원리

- 포인터가 타입을 가지는 이유와 포인터 연산의 동작

- 포인터를 이용한 동적 메모리 할당


이 정도만 알면 실무의 92.8%를 별 문제 없이 수행할 수 있다. - 이 수치는 어떻게 나온거지?!??!?!?

728x90
반응형