1. 배열명의 역할
- 우리가 어떤 자료형을 선언하면 컴퓨터 메모리 공간상에 할당이 된다고 배웠다.
- 각 메모리 공간은 접근할 수 있는 주소가 각각 있고 그 크기는 환경에 따라 다르다.
- 이런 주소들은 숫자로 표현이 가능하지만 모든 연산을 할 수 있는 것은 아니고, 특정한 연산만 수행가능하다.
- 그 중에서 일반적인 정수, 문자, 실수 등의 자료는 주소를 알기 위해서는 &를 붙였어야 했는데,
- 배열은 그 이름 자체가 주소에 해당한다.
1-1. 배열명 활용
- 예를 들어서 int arr[5] 라는 배열이 있다고 해보자.
- 이 arr[5]를 선언한 순간 int 자료형(4 byte)* 5개 (요소의 갯수)로 20byte가 메모리 공간상에 연속적으로 할당된다.
- 우리는 배열을 인덱스로 접근할 수 있다. 뿐만 아니라 반복을 통해서 각 인덱스에 접근하여 작업 할 수도 있다.
- 즉 컴퓨터 공간상에 연속적으로 값이 저장되어 있고, 일정한 크기 만큼 움직이며 접근한다는 소리다.
- 그럼 우리 컴퓨터는 (1)어디를 기준으로 배열의 시작점을 찾아서 (2)얼만큼 움직일까?
- (1) 어디를 기준으로의 답으로는 메모리가 할당되어 있는 첫주소부터 시작한다.
- 배열의 이름은 베열을 가리키는 포인터로서의 역할을 한다. (배열이 포인터다 x 배열의 이름은 주소다 o)
- 배열의 이름은 arr 인데 이는 &arr[0] 와 동일한 의미이다.
- (2) 얼만큼 움직일까? 포인터는 전에 배웠듯이 값을 가리킬 뿐이다. 우리가 정수 int형은 4byte다. 그럼 위의 예시에서는 4byte만큼 움직이면서 값을 읽는다.
- (대개, 그렇다. 16bit 컴파일러에는 2byte다. 또한 향후 컴파일러 bit 수가 늘어나면 8byte, 16byte가 될 수도 있는 것이다.)
- 앞으로 설명하는 용량 단위는 책에서 설명하거나 보편적으로 많이 쓰이는 단위로 설명하겠다.
- 이 때, 배열의 자료형이 유의미하다. 컴파일러는 정수 하나를 읽으려면 시작주소부터 4byte만큼 데이터를 읽어와야 한다는 것을 알기 때문에, 4byte 만큼 읽어서 값을 가지고 올 수 있다.
#include <stdio.h>
int main(void)
{
int ary[3]; //길이 3짜리 배열을 선언했다.
*(ary + 0) = 10; //위에서 설명했듯이 (ary + 0) 는 &ary[0]과 동일하다.
//&ary[0]을 역참조한 값은 ary[0]과 동일하다.
//즉 위 코드는 ary[0] = 10; 과 완전히 동일하다.
*(ary + 1) = *(ary + 0) + 10; //마찬가지로 ary[1] = ary[0] + 10; 과 동일하다.
//ary[1]에는 20이 들어간다.
printf("3번째 요소 입력 받기 : ");
scanf("%d", ary+ 2); //scanf에서는 우리가 넣을 메모리의 공간을 요구 했는데, 배열의 이름 자체가 주소이므로 입력받는 것이 가능하다.
for (int i = 0; i < 3; i++)
printf("%5d", *(ary + i));
return (0);
}
1-2. 배열과 포인터의 차이
- 배열의 이름이 배열의 0번째 주소를 나타내는 것도 맞고, 이 값을 포인터에 할당해서 포인터로 역참조도 가능하다.
- 하지만 배열과 포인터는 크게 2가지의 차이점이 있는데, 필자가 개인적으로 공부해서 요약하면 배열을 선언하면 그 배열은 배열로서의 정체성을 가지고 있다. 로 이해하면 쉽다.
- (1) 배열과 포인터의 첫번째 차이는 크기 차이가 있다. sizeof 연산자로 포인터와 배열을 확인해 보면 포인터는 딱 포인터의 크기만큼만 나오고 배열은 모든 요소를 합친 크기가 나온다.
- 이는 배열의 이름도 주소를 나타내고 포인터도 주소를 나타내지만 (심지어 둘은 같은 주소다) 배열은 배열로서의 정체성을 가지고 있기 때문에 각 요소의 용량 * 요소의 갯수가 sizeof 연산자로 부터 확인이 가능하다.
#include <stdio.h>
int main()
{
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
printf("arr size is : %d\n", sizeof(arr)); //4byte가 5개로 총 20이 출력된다.
printf("ptr size is : %d\n", sizeof(ptr)); //단순 포인터로 8이 출력된다.
}
더보기
출력결과
20
8
- (2) 두번째 차이점으로는 포인터는 주소값을 가지고 있는 단순 변수이지만 배열은 상수다.
- 포인터 자체는 값의 변경이 가능하지만 배열은 변경이 불가능하다.
- ptr = ptr + 1 이나 ptr++ 같은 건 가능하지만 arr = arr + 1, arr++은 불가능하다.
- 배열의 이름은 주소를 가리킨다는 점만 포인터와 동일 할 뿐, 배열자체를 의미한다는 사실을 잊지 말자!
#include <stdio.h>
int main()
{
int arr[3] = {10, 20, 30};
int *ptr = arr;
for (int i = 0; i < 3; i++)
printf("%d, ", *(ptr++)); //ptr은 배열이 저장되어 있는 요소의 주소를 가지고 있기 때문에
//배열의 요소를 하나씩 출력한다. 즉, ptr이 가리키는 값은 얼마든지 변경이 가능하다.
}
#include <stdio.h>
int main()
{
int arr[3] = {10, 20, 30};
int *ptr = arr;
for (int i = 0; i < 3; i++)
printf("%d, ", *(arr++)); //배열의 이름이 들어가면 바꿀 수 없는 값이라며
//컴파일조차 되지 않는다.
}
1-3. [ ] 연산자
- 배열에서 arr[0] 은 배열 arr의 0번째 요소임을 직관적으로 알 수 있다.
- 그런데 어째서 이게 가능한 건지에 대해서는 내부 연산에 달려있다.
- arr[0] 은 컴파일러가 *(arr + 0) 로 바꿔서 연산한다.
- 즉 0[arr] 로 쓰더라도 배열 arr의 첫번째 요소 값을 확인할 수 있다.
1-4. 포인터 연산
- 위에서 계속 보면 ptr++을 사용하는 것을 볼 수 있을 것이다.
- 포인터에서는 2가지 연산을 지원한다. 하나는 포인터 끼리의 뺄셈이고 하나는 정수와의 연산이다. (++이나 --도 해당)
- 알다싶이 포인터는 주소의 값을 가지고 있다. 즉 포인터끼리의 덧셈은 의미가 없음을 알 수 있다. (에러 처리가 되거나 warning이 뜰 것이다.)
- 예를 들면 메모리 주소 5567과 메모리 주소 5568을 더한 값이 무슨 의미가 있겠는가.
- 반면 뺄셈은 의미가 있다.
- 포인터끼리의 뺄셈은 다른식으로 연산하도록 재정의 되어 있다.
- 포인터 - 포인터 => 값의 차이 / 가리키는 자료형의 크기
#include <stdio.h>
int main(void)
{
int ary[5] = {10, 20, 30, 40, 50};
int *pa = ary;
int *pb = pa + 3;
printf("pa : %p\n", pa);
printf("pb : %p\n", pb);
pa++;
printf("pb - pa : %ld\n", pb - pa);
return (0);
}
더보기
출력결과
pa : 0x000010 (임의의 주소)
pb : 0x000022 (임의의 주소)
pb - pa : 2
- 정수와의 연산은 포인터가 가리키고 있는 데이터가 연속적으로 값이 할당 되어 있을 때, 사용할 수 있도록 설계되었다.
- 즉 어떤 포인터 ptr에 + 1을 한다면 주소에 1이 더해지는 것이 아니라 해당 포인터가 가리키는 자료형의 byte만큼 증가한다.
- 뺄셈은 그 반대로 감소한다.
- 즉 정수와의 연산을 통해서 값들을 순회하면서 접근 & 사용할 수 있다.
- ++나 --도 전위나 후위로 1씩 증감하므로 사용할 수 있다.
#include <stdio.h>
int main(void)
{
int ary[3] = {10, 20, 30};
int *pa = ary;
printf("배열의 값 : ");
for (int i = 0; i < 3; i++)
{
printf("%d ", *pa);
pa++; // 실질적인 주소는 4가 증가한다.
}
return (0);
}
2. 매개변수로 배열
2-1. 배열을 인자로
- 함수에서 배열을 매개변수로 지정해서 사용할 수 있을 것이다.
- 우리가 전에 함수 챕터에서 배웠듯이 기본적으로 함수에 들어가는 값은 "복사되어" 들어가는 값이다.
- 위에서 언급한 내용 중에 "배열의 정체성"이라는 단어를 사용한 적이 있었다.
- 필자식으로 설명하면 배열을 함수의 인자로 넘기면 "배열의 정체성"이 사라진 상태로 주소의 값만 넘어간다!
- 코드 3개를 비교하면서 설명하겠다.
- 먼저 기본적인 배열의 인자 출력함수이다.
- 배열은 3의 크기를 가졌고 3번 반복하면서 각 인덱스의 값을 출력한다.
#include <stdio.h>
int main()
{
int arr[3] = {10, 20, 30};
for (int i = 0; i < 3; i++)
printf("%d, ", arr[i]);
}
- 이 값을 출력하는 부분을 함수로 분리해보자
- arr의 주소를 ptr이라는 포인터에 할당하고 이 ptr을 ft_print라는 함수에 인자로 넘겼다.
- 포인터의 정수 덧셈 뺄셈은 가능했으므로 정상적으로 10, 20, 30이 출력되는 것을 확인할 수 있다.
#include <stdio.h>
void ft_print(int *arr)
{
for (int i = 0; i < 3; i++)
printf("%d, ", *(arr++));
}
int main()
{
int arr[3] = {10, 20, 30};
int *ptr = arr;
ft_print(ptr);
}
- 마지막으로 배열 그 자체를 넘기면 어떻게 될까?
- 위 main함수에서 arr자체는 정수 연산이 불가능했기 때문에 arr++ 와 같은 연산이 불가능했다.
- 왜냐하면 배열명 자체는 상수이기 때문이다.
- 그러나 아래 코드는 잘 동작한다.
#include <stdio.h>
void ft_print(int *arr)
{
for (int i = 0; i < 3; i++)
printf("%d, ", *(arr++));
}
int main()
{
int arr[3] = {10, 20, 30};
ft_print(arr);
}
- 이것이 바로 배열을 인자로 넘겼을 경우 배열로서의 정체성을 잃고 포인터로서 주소의 값만 복사되어 들어가기 때문이다.
- 즉 인자로 넘긴 순간 복사된 값은 그냥 배열의 첫 주소를 가리키는 포인터일 뿐으로 바뀌는 것이다.
- sizeof연산자로 확인해도 12가 아닌 8이 나오는 것을 확인할 수 있다.
- 그래서 함수의 매개변수의 자리에 선언된 배열은 다양한 형태로 적힐 수 있다.
- ft_print(int arr[3]), ft_print(int *arr), ft_print(int arr[]) 등등 존재한다.
- 함수의 매개변수 자리에 [ ] 안에 들어있는 숫자는 의미가 없다. 어짜피 함수에 호출에서 인자로 배열을 넣으면 들어가는 건 주소일 뿐이기 때문이다.
- 그래서 int arr[] 처럼 [ ]안이 생략도 가능하다.
3. 혼공C 10장 도전 실전 예제
- 1 ~ 45 중에 서로 다른 수를 배열에 입력하고 출력하는 코드
#include <stdio.h>
void input_nums(int *lotto_nums);
void print_nums(int *lotto_nums);
int main(void)
{
int lotto_nums[6];
input_nums(lotto_nums);
print_nums(lotto_nums);
return (0);
}
void input_nums(int *lotto_nums)
{
int cnt = 0;
int tmp, flag;
while (cnt != 6)
{
printf("번호 입력 : ");
scanf("%d", &tmp);
flag = 0;
for (int i = 0; i < cnt; i++)
{
if (tmp == lotto_nums[i])
{
printf("같은 번호가 있습니다.\n");
flag = 1;
break;
}
}
if (flag == 0)
{
lotto_nums[cnt] = tmp;
cnt++;
}
}
}
void print_nums(int *lotto_nums)
{
printf("로또 번호 : ");
for (int i = 0; i <6; i++)
printf("%d ", lotto_nums[i]);
}
'Language > C' 카테고리의 다른 글
C 저수준 파일 입출력 - open(), close(), write(), read() (0) | 2024.11.20 |
---|---|
혼공C (11장) - 문자와 버퍼 (getchar, putchar) (0) | 2024.04.11 |
혼공C (9장) - 포인터, 상수 포인터와 포인터 상수 (0) | 2024.03.27 |
혼공C (8장) - C 배열, VLA, 문자열 혼공C 8장 실전예제 (1) | 2024.03.24 |
혼공C (7장) - C 함수 기초, 혼공C 7장 실전예제 (0) | 2024.03.22 |