Chapter 10 배열과 포인터
1. 배열
- 동일한 하나의 데이터형을 가진 연속된 원소들로 구성된다.
- 배열 선언을 사용하여 컴파일러에게 알려야한다. (원소들의 데이터형을 알린다.)
- 배열은 배열의 크기와 초기값 리스트에 들어있는 항목의 개수가 일치해야한다.
- 배열의 크기를 생략하면, 컴파일러가 스스로 초기값 리스트에 맞게 배열크기를 설정한다.
* 배열 선언 예시
int main(void)
{
float candy[326]; /* 326개의 float형 값을 가지는 배열 */
char code[12]; /* 12개의 char형 값을 가지는 배열 */
int states[50]; /* 50개의 int형 값을 가지는 배열 */
}
1-1. 초기화
- 변수와 같이 초기화가 필요하다.
- 배열은 콤마로 분리된 값들의 리스트를 중괄호({ })로 감싸서 초기화 한다.
- 원한다면 값과 콤마 사이에 스페이스를 넣을 수도 있다.
- 이 형식으로 배열 초기화 시 신택스 오류 발생한다면, 버전이 낮은 컴파일러 이기 때문에 배열 선언 앞에 static을 붙이면 해결된다.
* 배열 초기화 예시
int main(void)
{
int power[8] = {1,2,4,6,8,16,32,64};
}
*** 배열 예제1(10.1)
/* 배열 선언 출력*/
#include <stdio.h>
#define MONTHS 12
int main(void)
{
// 배열 초기화
int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
int index;
for (index = 0; index < MONTHS; index++)
printf("Month %d has %2d days.\n", index +1,
days[index]);
return 0;
}
- #define MONTHS 12 상수 선언으로 days[MONTHS] -> days[12] 로 선언된다.
*** 배열 예제2(10.2)
- 배열을 초기화 시키지 않았을 시
/* 배열 초기화 시키지 않았을때*/
#include <stdio.h>
#define SIZE 4
int main(void)
{
int no_data[SIZE]; /* 초기화 시키지 않은 배열 */
int i;
printf("%2s%14s\n",
"i", "no_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, no_data[i]);
return 0;
}
- 시스템에 따라 다르게 나온다.
- C언어에서는 초기화를 시키지 않으면 아무값(쓰레기값)이 가지게 된다.
*** 배열 예제3(10.3)
- 배열의 크기와 초기화 항목의 개수가 맞지 않을 때
/* 배열의 크기와 초기화값의 개수가 맞지 않을때 */
#include <stdio.h>
#define SIZE 4
int main(void)
{
int some_data[SIZE] = {1492, 1066};
int i;
printf("%2s%14s\n",
"i", "some_data[i]");
for (i = 0; i < SIZE; i++)
printf("%2d%14d\n", i, some_data[i]);
return 0;
}
- 컴파일러는 값이 들어있는 리스트에만 넣어주고 나머지는 0으로 초기화 했다.
- 즉, 배열을 일부분만 초기화하면 나머지 원소들은 0으로(쓰레기값 : 사용하지않는값) 설정된다.
*** 배열 예제4(10.4)
- 배열의 크기를 생략하면, 컴파일러가 스스로 초기값 리스트에 맞게 배열크기를 설정
/* 배열의 크기를 생략하면, 자동으로 리스트에 맞게 배열크기설정 */
#include <stdio.h>
int main(void)
{
const int days[] = {31,28,31,30,31,30,31,31,30,31};
int index;
for (index = 0; index < sizeof days / sizeof days[0]; index++)
printf("Month %2d has %d days.\n", index +1,
days[index]);
return 0;
}
- for 루프 제어문 : 컴퓨터가 배열 크기를 계산해서 우리에게 알려주도록 부탁한다.
- sizeof 연산자를 사용하여 days의 바이트 수로 배열전체의 크기를 알 수 있다.
- sizeof days[0] : 바이트 수로 배열 원소 하나의 크기다.
- sizeof days / sizeof days[0]
- 배열 전체의 크기를 배열원소 하나의 크기로 나누면, 그 배열에 몇개의 원소가 있는지 알 수 있다.
- 단점 : 원소의 개수가 틀렸어도 컴파일러가 에러를 잡아내지 못한다.
1-2. 지정 초기화자
- 지정 초기화자(Designated Initialalizer)기능을 사용하면 초기화할 원소들을 사용자가 선택할 수 있다.
- 초기화 리스트에서 각괄호 안에 인덱스를 사용하여 특정원소를 초기화 하도록 지정할 수 있다.
int arr[6] = { [5] = 212}; /* arr[5]를 212로 초기화 한다. */
*** 배열 예제4(10.4)
// 지정 초기화자를 사용한다.
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int days[MONTHS] = {31,28, [4] = 31,30,31, [1] = 29};
int i;
for (i = 0; i < MONTHS; i++)
printf("%2d %d\n", i + 1, days[i]);
return 0;
}
- 지정초기화자 뒤에 여러개의 값이 붙으면, 여분으로 붙는 값들은 이어지는 다음 원소들을 초기화 하는데 사용된다.
즉, days[4]가 31로 초기화되고, days[5]와 days[6]이 30과 31로 초기화된다.
- 특정원소가 어떤값으로 초기화되면, 마지막에 행해진 초기화가 유효하다.
ex.) days[1] = 28로 초기화 되지만, 나중에 지정 초기화자 [1] = 29에 의해 초기화값이 지워지고 29가 된다.
1-3. 배열에 값 대입하기
- 배열을 선언한 후에는, 하나의 배열 인덱스를 사용하여 배열 원소들의 값들을 대입할 수 있다.
*** 배열에 값 대입하기 예제
// 배열에 짝수의 값 대입하기
#include <stdio.h>
#define SIZE 50
int main(void)
{
int counter, evens[SIZE];
for(counter = 0; counter < SIZE ; counter++)
{
evens[counter] = 2 * counter ;
printf("evens[%d] = %d\n",counter, evens[counter]);
}
}
![]() |
![]() |
*** 허용되지 않는 배열에 값 대입하기 예제
// 허용되지 않는 배열에 값 대입하기
#include <stdio.h>
#define SIZE 5
int main(void)
{
int oxen[SIZE] = {5,3,2,8}; // 허용된다.
int yaks[SIZE];
yaks = oxen; // 허용되지 않는다.
yaks[SIZE] = oxen[SIZE]; // 유효하지 않는다.
yaks[SIZE] = {5,3,2,8}; // 동작하지 않느다.
}
1-4. 배열의 범위
- 유효한 범위 내에 있는 배열 인덱스를 사용하고 있는지 반드시 확인해야 한다!!!
- 컴파일러가 인덱스에러를 모두 잡아낼 수 있는 것은 아니다. 왜냐하면 프로그램이 실행을 시작한 후에도 인덱스 값이 결정되지 않을 수도 있기 때문이다.
- 안정을 보장하려면, 실행하는 동안에 인덱스 값을 검사하는 가외의 코드를 추가해야하고, 그것은 프로그램의 실행을 느리게 만든다.
*** 배열범위 예제1(10.6)
// 배열의 범위를 벗어난다.
#include <stdio.h>
#define SIZE 4
int main(void)
{
int value1 = 44;
int arr[SIZE];
int value2 = 88;
int i;
printf("value1 = %d, value2 = %d\n", value1, value2);
for (i = -1; i <= SIZE; i++)
arr[i] = 2 * i + 1;
for (i = -1; i < 7; i++)
printf("%2d %d\n", i , arr[i]);
printf("value1 = %d, value2 = %d\n", value1, value2);
printf("arr[-1]의 주소: %p\n", &arr[-1]);
printf("arr[4]의 주소: %p\n", &arr[4]);
printf("value1의 주소: %p\n", &value1);
printf("value2의 주소: %p\n", &value2);
return 0;
}
- 컴파일러는 인덱스들이 유효한지 검사하지 않는다. 잘못된 인덱스를 사용했을 때 어떤결과가 나올지 정의되지않는다.
- arr[-1]은 value2와 동일한 메모리 위치에 대응되고, arr[4]는 value1과 동일한 메모리 위치에 대응된다.
- 하지만 실행결과를 보면 내 컴파일러는 잘 찾아냈지만 컴파일러 by 컴파일러로 value1 = -1 / value2 = 9를 찾아야한다.
- 배열 범위를 벗어나는 인덱스를 사용하면, 프로그램이 다른 변수의 값을 변경하는 결과를 낳는다.
1-5. 배열 크기 지정하기
- 코드를 보고 가능한것을 익히자!!!!
#include<stdio.h>
#define SIZE 4
int main(void)
{
int arr[SIZE]; // 기호 정수 상수
double lots[144]; // 리터럴 정수 상수
int n = 5;
int m = 8;
float a1[5]; // 가능
float a2[5*2 +1]; // 가능
float a3[sizeof(int) +1]; // 가능
float a4[-4]; // 불가능 , 배열 크기는 0보다 커야한다.
float a5[0]; // 불가능, 배열 크기는 0보다 커야한다.
float a6[2.5]; // 불가능, 배열 크기는 정수여야한다.
float a7[(int)2.5]; // 가능, float형을 int constant형으로 캐스트한다.(형변환)
float a8[n]; // C99 이전에 허용되지 않았다.
float a9[m]; // C99 이전에 허용되지 않았다.
}
2. 다차원 배열
(2차원 배열만 알면된다!!!)
- 배열의 배열을 사용하는것
float rain[5][12]; // float형 원소 12개인 배열이, 5개의 원소인 배열
- 먼저 안쪽 부분을 해석한다. rain[5] : rain은 원소가 5개인 배열이다.
- float rain[5][12]; // 12개의 float형 원소를 가지는 배열로 해석하면 쉽게 이해된다.
- rain의 원소가 float[12]형이라는 것을 나타낸다. 즉, rain을 구성하는 5개의 원소는 각각 12개의 float형 값을 가지는 배열들이다.
- ex.) rain[2][3] (배열 인덱스는 0부터 카운트되므로, 2행은 세번째 3열을 가리킨다.)
- ex.) rain[5][12] (5행 12열을 뜻함)
*** 다차원배열 예제1(10.7)
/* 강우량 데이터로부터 각 해의 총 강우량과 몇년에 걸쳐 계산한 연평균
강우량, 각 달의 월평균 강우량을 구한다.*/
#include <stdio.h>
#define MONTHS 12 // 1년의 달 수
#define YEARS 5 // 데이터를 수집한 햇수
int main(void)
{
// 2010 - 2014 까지의 강우량 데이터의 초기화
const float rain[YEARS][MONTHS] =
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}
};
int year, month;
float subtot, total;
printf(" 년도 강우량 (인치)\n");
for (year = 0, total = 0; year < YEARS; year++)
{ // 각 해에 대해 12달치 강우량을 더한다.
for (month = 0, subtot = 0; month < MONTHS; month++)
subtot += rain[year][month];
printf("%5d %15.1f\n", 2010 + year, subtot);
total += subtot; // 여러 해에 걸친 총 강우량을 구한다.
}
printf("\n연평균 강우량은 %.1f 인치입니다.\n\n",
total/YEARS);
printf("월평균 강우량은 다음과 같습니다.\n\n");
printf(" Jan Feb Mar Apr May Jun Jul Aug Sep Oct ");
printf(" Nov Dec\n");
for (month = 0; month < MONTHS; month++)
{ // 각 달에 대해 5년치 강우량을 더한다.
for (year = 0, subtot =0; year < YEARS; year++)
subtot += rain[year][month];
printf("%4.1f ", subtot/YEARS);
}
printf("\n");
return 0;
}
- for (year = 0, total = 0; year < YEARS; year++) : 각 해에 대해 처리한다.
- for (month = 0, subtot = 0; month < MONTHS; month++) : 각 달에 대해 처리한다.
2-1. 2차원 배열의 초기화
- 2차원 배열을 초기화 하려면, 콤마로 분리된 이와 같은 리스트가 필요하다.
const float rain[YEARS][MONTHS] =
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}
};
- const float rain[5][12]로 해석된다.
3. 포인터와 배열
- 배열표기는 실제로는 포인터의 변장된 사용에 불과하다.
- flizny가 배열이라면, flizny == &flizny[0] // 배열의 이름은 첫 번째 원소의 주소다.
- flizny와 &flizny[0]은 둘 다 첫 번째 원소의 메모리 주소를 나타낸다.
* & 연산자는 주소 연산자라는 것을 기억하고 있어라!!!
* flizny와 &flizny[0]은 고정값으로 상수이다. 그러나 이들은 둘 다 포인터 변수에 값으로 대입될 수 있다.
*** 포인터와 배열 예제1(10.8)
// 포인터 덧셈
#include <stdio.h>
#define SIZE 4
int main(void)
{
short dates [SIZE];
short * pti;
short index;
double bills[SIZE];
double * ptf;
pti = dates; // 배열의 주소를 포인터에 대입한다.
ptf = bills;
printf("%23s %15s\n", "short", "double");
for (index = 0; index < SIZE; index ++)
printf("포인터 + %d: %10p %10p\n",
index, pti + index, ptf + index);
return 0;
}
- 바이트 단위로 주소가 매겨진다.
- short형은 2바이트를 사용하고, double형은 8바이트를 사용한다.
* 스칼라 변수 (개념만 알고가자)
- 두가지 유형이 있다. (가변 변수 와 불변 변수)
- 가변 변수(Variable) : 재할당이 가능하다.
- 불변 변수(Value) : 한 번 값이 정해지면 변경되지 않기때문에 데이터 처리에 있어 단순하게 처리할 수 있는 장점이 있다.
- 스칼라는 동시처리를 중요하게 생각하기 때문에 변경 불가능한 데이터를 중요하게 생각합니다. 데이터의 변경이 가능하면 동시처리시에 데이터에 대한 고민이 많아지게 됩니다. 따라서 불변 변수로 설정하는 것을 선호합니다.
var variable = 10
val value = 20
// var 값의 재할당은 처리 가능
scala> variable = 30
variable: Int = 30
/// value 값의 재할당은 오류 발생
scala> value = 40
<console>:12: error: reassignment to val value = 40 ^
* 포인터 (개념)
- 포인터의 값은 원소 객체의 주소이다.
- 바이트 단위로 주소를 매긴다. 이것은 메모리의 각 바이트들이 연속적으로 번호가 매겨져 있다는 것을 의미한다.
- 포인터에 *연산자를 적용하면, 그 포인터가 가리키는 객체에 저장되어 있는 값을 얻는다.
- 포인터에 1을 더하면, 그 포인터가 가리키는 객체의 바이트 수 크기만큼 포인터 값이 증가한다.
- double형 변수와 같이, 큰 객체의 주소는 첫번째 바이트안에 주소값을 갖는다!!!!!
- ex.) int array [2] = {1,2} => int는 4바이트 이기때문에 array[0]의 주소값은 첫번째에 갖는다. array[1]의 주소값은 array[0]번째 주소값에 + 4를 더하면 array[1]의 주소값을 찾을 수 있다.
- ex.) double형이면 , +8을 해줘야 array[1]의 주소값을 찾을 수 있다.
dates + 2 == &dates[2] // 주소가 같다
*(dates + 2) == &dates[2] // 값이 같다
- C언어 표준은, 배열 표기를 포인터로 서술한다. 즉, ar[n]이 *( ar + n )을 의미하도록 정의한다.
- *( ar + n )을 "메모리 위치 ar로 가라. n개의 기억단위 만큼 이동하라. 거기에 있는 값을 꺼내라)
- ex.) *( dates + 2) // dates의 세번째 원소의 값이다.
- ex.) *dates + 2 // dates의 첫번째의 원소값에 2를 더한다.
*** 배열과 포인터예제1 (10.9)
/* 포인터 표기를 사용한다. */
#include <stdio.h>
#define MONTHS 12
int main(void)
{
int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
int index;
for (index = 0; index < MONTHS; index++)
printf("%2d월: 날짜수 %2d\n", index +1,
*(days + index)); // days[index]와 같다.
return 0;
}
- days는 그 배열의 첫 번째 원소의 주소이다. days + index는 원소 days[index]의 주소다.
- *(days + index)는 그 원소의 값 즉, days[index]다.
- 배열의 표기와 포인터 표기가 동등한 방법이다.
4. 함수, 배열, 포인터 (통합하여 사용하는 방법)
- 어떤 배열의 원소들의 합을 리턴하는 함수를 원할때 , marbles가 int형의 원소들을 가지는 배열이라고 가정하자
- 함수호출 모습 : total = sum(marbles);
- 프로토 타입 : int sum(int *ar); // 배열의 이름은 첫번째 원소의 주소라는 사실을 기억해라 + sum()은 그 위치에서 하나의 int형을 찾을 수 있다는 사실을 알게 된다. 단, 원소의 개수는 알 수가 없다.
- 함수 정의 :
[1번째 방법]
고정된 배열의 크기를 함수 안에 코딩하는 것
- 배열을 찾을 장소와 배열에 있는 데이터형을 함수에게 알린다.
#include <stdio.h>
int sum(int *ar);
int main()
{
int total;
int marbles[10];
total = sum(marbles);
}
int sum(int *ar) // 대응하는 함수정의
{
int i;
int total =0;
for(i=0;i<10;i++) // 원소가 10개라고 가정한다.
{
total += ar[i]; // ar[i]는 *(ar+i)와 같다
// total = 원소들의 누계
}
return total;
}
[2번째 방법]
고정된 배열의 크기를 함수 안에 코딩하는 것
- 그 배열에 몇 개의 원소가 있는지 함수에게 알린다.
- int sum (int *ar, int n) 추가
#include <stdio.h>
int sum(int *ar, int n);
int main()
{
int total;
int marbles[10];
total = sum(marbles);
}
int sum(int *ar, int n) // 대응하는 함수정의
{
int i;
int total =0;
for(i=0;i<10;i++) // 원소가 10개라고 가정한다.
{
total += ar[i]; // ar[i]는 *(ar+i)와 같다
// total = 원소들의 누계
}
return total;
}
4-1. 배열 매개변수의 선언
- 배열 이름은 첫 번째 원소의 주소이기 때문에, 배열 이름을 실전달인자로 사용하려면 대응하는 형식 매개변수가 포인터가 되어야한다.
int ar[ ]를 int * ar와 같은 의미로 즉, ar이 int형을 가리키는 포인터형이라고 해석한다.
- int sum(int *ar, int n);
- int sum(int *, int);
- int sum(int ar[ ], int n);
- int sum(int [ ], int);
프로토 타입에서는 이름을 생략 가능하다 이와같이 4개의 이름은 동등하다.
단, 함수정의에서는 이름을 생략할 수 없다. 그래서 함수 정의에서는 다음 두가지 형식으로 나타낸다.
1. int sum(int *ar, int n)
{
// 여기에 실행 할 코드 작성
}
2. int sum(int ar[ ], int n)
{
// 여기에 실행 할 코드 작성
}
*** 배열 매개변수예제 1(10.10)
- 최초 배열의 크기와 그 배열을 나타내는 함수 매개변수의 크기를 출력한다.
// 배열 원소들의 합을 구하는 예제
// %zd가 동작하지 않는다면 %u, %lu를 사용하세요.
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);
int main(void)
{
int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
long answer;
answer = sum(marbles, SIZE);
printf("구슬의 전체 개수는 %ld개 입니다.\n", answer);
printf("marbles의 크기는 %zd 바이트 입니다.\n",
sizeof marbles);
return 0;
}
int sum(int ar[], int n) // how big an array?
{
int i;
int total = 0;
for( i = 0; i < n; i++)
total += ar[i];
printf("ar의 크기는 %zd 바이트입니다.\n", sizeof ar);
return total;
}
4-2. 포인터 매개변수의 이용
*** 포인터 배열 함수 사용 예제1(10.11)
- 함수가 포인터 자체의 값을 변경할 수 있다.
/* 배열 원소들의 합을 구한다. */
#include <stdio.h>
#define SIZE 10
int sump(int * start, int * end);
int main(void)
{
int marbles[SIZE] = {20,10,5,39,4,16,19,26,31,20};
long answer;
answer = sump(marbles, marbles + SIZE);
printf("구슬의 전체 개수는 %ld입니다.\n", answer);
return 0;
}
/* 포인터 계산을 사용한다. */
int sump(int * start, int * end)
{
int total = 0;
while (start < end)
{
total += *start; // total에 값을 더한다.
start++; // 포인터를 증가시켜 다음 원소를 가리킨다.
}
return total;
}
*** 포인터 전위 후위 연산 (10.12)
/* 포인터 연산에서의 우선순위 */
#include <stdio.h>
int data[2] = {100, 200};
int moredata[2] = {300, 400};
int main(void)
{
int * p1, * p2, * p3;
p1 = p2 = data;
p3 = moredata;
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n",
*p1 , *p2 , *p3);
printf("*p1++ = %d, *++p2 = %d, (*p3)++ = %d\n",
*p1++ , *++p2 , (*p3)++);
printf(" *p1 = %d, *p2 = %d, *p3 = %d\n",
*p1 , *p2 , *p3);
return 0;
}
'LAB > C' 카테고리의 다른 글
[24.08.13] c언어의 기초 11 (0) | 2024.08.13 |
---|---|
[24.08.12] c언어의 기초10 (1) | 2024.08.12 |
[24.08.10] c언어의 기초8 (0) | 2024.08.10 |
[24.08.09] c언어의 기초7 (1) | 2024.08.09 |
[24.08.08] c언어의 기초6 (2) | 2024.08.08 |