배열(Array)이란 말은 파이썬을 해본 사람은 쉽게 슬라이싱이 안되는 list와 같다고 생각하면 될 것 같다.
하지만 처음을 C언어로 시작하는 사람은 배열?? 무언가 나열할 때 "배열한다" 라고 한다. 딱 그 느낌을 가져가면 될 것 같다.
만약 여러명의 친구의 성적을 저장한다고 생각해보자. 현대대수학을 "호진,종욱,지욱,민석,수환,상준,성준"이 듣는다고 할때 이들의 성적을 저장해야한다면 다음과 같이 할 수 있다
short int hojin = 0
short int jonguk = 0
short int jiuk = 0
short int minseok = 0
short int suhwan = 0
short int sangjun = 0
short int sungjoon = 0
변수만 초기화 했는데도 벌써 복잡해졌다. 이러지 말고 Score라는 하나의 변수를 설정해놓고 그 안에서 관리 할 수 있다면 좋을 것이다.
이러한 동기에서 나온 것이 바로 배열이다.
배열을 선언할때는 다음과 같이 한다.
short int student[20] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
하나씩 설명해주겠다.
일단 위에 short int는 앞에서 말한 점수를 저장해놓을 박스의 크기이다 한사람의 점수를 2바이트 크기의 박스에 담아놓겠다는 의미이고, [20] → 그런 박스가 20개가 있다는 것이다.
그후 {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}
로 초기화 했음을 볼 수 있는데, 저렇게 함으로써 박스에 전부 0을 담아놓겠다는 것이다. 이때 {}
를 이용해서 초기화 하는 것은 일반 변수를 초기화 하는 것과 다름을 볼 수 있다.
이를 좀더 편하게 하는 방법은 {0,}
를 사용하는 것이다
그렇다면 초기화를 하지 않으면 어떤일이 일어날까?
void main(void){
short int score[7];
printf("%d",score[0]);
}
위의 코드를 설명해보면 short int score[7]
로 2바이트 크기 박스 7개를 만들어 놓은 후에, printf("%d",score[0]);
에서 0번째 박스에 있는 값을 출력하라고 한 것이다. 이렇게 박스의 번호(index)를 명시하는 것을 인덱싱(indexing)이라고 하고 이때 통념과 다르게 index는 0번부터 시작한다.
따라서 위의 코드는 0번 박스부터 6번박스까지 있는 것이다. 위의 index의 개념은 python에서도 상통한다.
다시 돌아와서 우리는 0번째 박스에 있는 값을 출력하라고 명령했는데, 64라는 값을 출력했다. 우리는 0번째 박스에 값을 넣어놓은적이 없음에도 0번째 박스가 할당된 컴퓨터 메모리에 원래부터 있던 값을 출력하는 것이다.
변수를 선언하고 배열을 선언하는 과정은 컴퓨터에서 그에 해당하는 메모리를 '할당'하는 과정임을 잊으면 안된다. 이러한 개념에서 포인터라는 개념이 나오기 때문에 명심해야한다.
그렇다면 우리는 초기화 하지 않는다면 박스안에 쓰레기값이 들어있음을 확인했다. 그래서 초기화를 {0,}
로 하는 것을 확인했다. 이때 다른 숫자를 0대신에 넣게 되면 {2,0,0,0,0,0}로 인식함을 알 수 있다.
이를 통해 유추할 수 있는점은 갯수가 부족하면 ,
뒤에서 C언어에서 알아서 0으로 인식함을 알 수 있다.
void main(void){
short int score[7]={2,};
printf("%d",score[6]);
}
위의 것을 좀더 잘 설명할 수 있는 예시를 보여주겠다
void main(void){
short int score[7]={2,1,};
printf("%d\n",score[3]);
printf("%d\n",score[0]);
printf("%d\n",score[1]);
}
숫자를 지정한 2와1은 박스에 그대로 담겼음을 볼 수 있지만 ,
뒤는 0으로 초기화 됐음을 확인할 수있다.
이러한 개념을 파이썬 numpy에선 BroadCasting이라고 한다
참고만 하길 바란다.
배열을 선언할때 크기를 지정하지 않을수도 있다
예로써 보여주겠다.
void main(void){
short int number[]={1,2,3};
printf("%d%d%d",number[0],number[1],number[2]);
}
위처럼 배열의 요소를 명확하게 명시할 경우 배열의 크기를 생략 할 수 있다
질문> 배열을 선언한 후 {0,}를 사용하지 말고 반복문을 이용해서 초기화 해보길 바란다
정답>
void main(void){
short int score[7],i;
for(i=0;i<7;i++)score[i]=0;
printf("%d",score[6]); //배열의 마지막값 출력
}
→ for문을 사용해서 일일히 값을 넣어줄수도 있지만{0,}
라는 편리한 도구가 있는 만큼 적극활용하길 바란다.
배열을 배운후에는 배열을 이용해서 문자열을 표현할 수 있다.
C언어에서 문자를 저장하는데 가장 적합한 자료형은 char자료형이다.
한글을 출력하기 위해서 short int 배열을 만든후에 출력해보고자 했는데 안됐다. 이와 같은 시도를 하지 않기를 바란다
자연스러운 생각으로 문자열을 배열을 이용해서 표현을 해보면 다음과 같이 표현할 수 있다
void main(void){
char string[6]={'A','B','C','D','E',0};
printf("%c\n",string[0]);
printf("%c\n",string[1]);
printf("%c\n",string[2]);
printf("%d\n",string[5]);
printf("%s\n",string);
}
하나씩 설명해보겠다. 위는 1바이트 박스를 6개를 만든것과 같다 하나마다 A,B,C,D,E를 저장해 놓은 것이고 마지막에는 0을 저장했다. 0을 저장한 이유는 End Of Line(EOL)을 표현해주고자 한것이다. 그냥 문자열이 끝났다는걸 알려주는 것이다.
하지만 위는 너무 불편하다. 나조차도 이전에 저렇게 문자열을 써본적은 거의 없는 것 같다. 조금 더 받아들이기 쉬운 코드는 다음과 같다.
void main(void){
char string[]="ABCDEFGHIJKLMN";
printf("%c\n",string[0]);
printf("%c\n",string[1]);
printf("%c\n",string[2]);
printf("%d\n",string[14]);
printf("%s",string);
}
위와 같이 " "
을 이용해서 표현할경우 자동으로 EOL을 포함하게된다.
질문> 위의 문자열의 길이를 어떻게 하면 구할 수 있을까?
위 질문에 대한 아이디어만 생각해보고 넘어가자.(책에 답이 있으므로 구현해보는 것도 좋을 것이다.)
→ 문자열의 처음부터 쭉 따라가면서 count를 늘리다가 0이 나오게 되면 멈추면 될 것이다.
왜 넘어갔냐? 라고 물어본다면
기본적으로 위의 기능을 제공해주는 라이브러리가 C에 존재한다.
시험을 위해 공부하는게 아니라면 남이 구현해놓은 걸 쓰는게 좋다는게 내 생각이다.
→ 위의 라이브러리 파일을 사용하고 싶으면 헤더파일인 #include <string.h>
를 사용하면 된다.
위의 라이브러리 파일에 있는 세 함수에 대해서 공부해보겠다.
1. strlen()
위의 질문에 해당하는 함수이다.
void main(void){
char string[]="ABCDEFGHIJKLMN";
printf("%d length\n",strlen(string));
printf("%c\n",string[0]);
printf("%c\n",string[1]);
printf("%c\n",string[2]);
printf("%d\n",string[14]);
printf("%s",string);
}
그냥 함수안에 내가 출력하고 싶은 문자열을 넣어주면 된다.
strcpy()
string copy를 의미한다.strcat()
string concatenate를 의미한다.void main(void){
char first_string[] = "ABC";
char second_string[] = "DEF";
char copy_string[strlen(second_string)]; //strlen(second_string)을 배열의 크기로 씀
strcpy(copy_string,second_string); //second_string을 copystring으로 copy
strcat(first_string,copy_string);
printf("%s",first_string);
}
위의 두함수는 전형적인 in-place function인 것 같다.(이것이 무엇이냐면 함수를 사용함으로써 원래 변수에 반영되는 것이라고 생각하면 될 것같다.) 이해가 가지 않는다면 return을 하지 않고 직접적으로 변화를 주기 때문에 값을 어떤 새로운 변수의 초기화 값으로 할당하고자 하면 오류가 발생한다. 다음과 같다
char new_string[] = strcat(first_string,copy_string);
위와 같이 사용하고자 하면 오류가 발생하니 초기화 값으로 쓰고자 하지 말자.
자 이제 2차원 배열을 한번 해보자. 별로 어렵게 생각할 필요가 없다. 수학과에서 선형대수학을 배웠다면 쉽게 이해할 수 있다.
2차원 배열을 요약하자면 이전까진 선위에 값들을 저장했지? 이제 면위에 값들을 저장할거야. 이 말이다.
그렇다면 당연하게도 3차원 배열도 생각해볼수 있을 것이다.(이 개념을 Tensor라고 일컫는다)
면에 저장하기 위한 가장 효과적인 방법은 행렬이다. 행렬은 행과 열로 구성돼 있는데, C언어에서 다음과 같은 구조를 갖고있다.
{{열1,열2,열3,...},{열1,열2,열3,...},{열1,열2,열3,...}, ... }
행1 행2 행3 , ... }
큰 { }안에 있는 작은 { }에 열의 원소가 순서대로 들어있고 그 작은 { }의 순서가 행 번호인 것이다.
예를 보여주겠다.
void main(void){
char TwoDArray[5][3] = {
{'A','B','C'},{'D','E','F'},
{'G','H','I'},{'J','K','L'},
{'M','N','O'}
};
printf("%d",sizeof(TwoDArray));
}
위의 TwoDArray는 5행3열을 가진 행렬이다. 1행 1열에는 A 2행 1열에는 D 3행 1열에는 G 이런식으로 들어있다.
하지만 실제적으로는 행렬형태로 저장되는게 아니고 1차원의 선 형태로 저장이 되는데, 위의 예시로 설명을 하자면 총
15개의 박스가 다음과 같은 형태로 저장된다.(행렬의 index를 그대로 따와서 그렸다)
그렇다면 indexing은 어떻게 해야할까 우리는 4행 1열에 있는 'J'를 출력하고자 한다.
1차원 배열의 확장이라고 생각하면 쉽다. 당연하게도 TwoDArray[3][0]가 J에 해당할 것이다. 확인해보겠다.
void main(void){
char TwoDArray[5][3] = {
{'A','B','C'},{'D','E','F'},
{'G','H','I'},{'J','K','L'},
{'M','N','O'}
};
printf("%c",TwoDArray[3][0]);
}
자 그렇다면 Quiz> 2차원 배열구조를 사용해서 다음을 출력해보아라
Hint> 행+열을 해서 홀수인곳에는 1 짝수인곳에는 0을 입력하기
정답>
void main(void){
char Baduk[10][10] = {
{0,},{0,},{0,},{0,},{0,},{0,},{0,},{0,},{0,},{0,}
},cnt = 0,num = 0;
for(cnt;cnt<10;cnt++){
for(num;num<10;num++){
if((cnt+num)%2!=0)Baduk[cnt][num]=1;
printf("%d",Baduk[cnt][num]);
}
num = 0;
printf("%d",num);
printf("\n");
}
}
아래에 있는 문제는 C언어입문책에 활용에 있는 문제이다.
int data[7] = {6,3,9,7,2,4,1}
을 오름차순으로 정렬하는 문제이다.
사진자료출처
위와 같은 결과가 나온다.
정답>
//버블정렬 수행하기
void main(void){
int data[7]={6,3,9,7,2,4,1},i=0,j=0,cnt=0;
for(i;i<sizeof(data)/4;i++){
while(cnt+1<sizeof(data)/4){
if(data[cnt]>data[cnt+1]){
int tmp = data[cnt+1];
data[cnt+1] = data[cnt];
data[cnt] = tmp;
}
cnt++;
}
cnt=0;
}
for(j;j<sizeof(data)/4;j++)printf("%d",data[j]);
}
sizeof은 크기를 출력한다 즉 바이트수를 출력하는 것인데 파이썬을 쓰다가 쓰니 파이썬 라이브러리의 size()와 헷갈려서 한참을 해맸다.
위의 코드는 일반성을 위해 sizeof()을 사용했고, 이때 데이터의 바이트 크기를 4로 나누었는데 이는 long int가 4바이트 크기여서 그렇다. 앞으로 scanf를 통해 데이터를 받은후 정렬하는 문제를 만나도 위의 코드를 사용하면 정렬이 될 것이다.
포인터도 마찬가지로 필요한 이유에 대해서 먼저 언급하겠다. C언어는 컴파일(compile)을 하는 순간 기계어로 바꿔주는데, 이때 우리가 선언한 변수들을 컴퓨터 메모리의 주소로 바꿔서 그 메모리 주소에 해당하는 위치에 값을 배달해준다. 따라서 핵심은 메모리의 주소이다. 메모리의 주소만 알 수 있다면, 우리는 여러 함수들의 지역변수들을 서로 연결할 수 있다는 기대를 할 수 있다.
만약 직접 지역변수를 연결하고자 한다면 다음과 같이 코드를 작성할 수 있다.
void Test(int data){
int new = data;
printf("%d",new);
}
void main(void){
int num = 5;
Test(num);
}
위의 방식을 책에서는 직접주소방식이라고 한다. 하지만 여기서 문제는 우리는 data라는 새로운 주소에 num의 값을 대입한것이지 본질적으로 data = num이 아닌 것이다 따라서 위의 Test함수에서 data값을 수정해도 num값이 변하지 않는다. 다음을 보자.
void Test(int data){
data = 4;
printf("data:%d\n",data);
return;
}
void main(void){
int num = 5;
Test(num);
printf("num:%d\n",num);
}
위와 같은 문제를 해결하기 위해서,포인터라는 개념이 나왔다. 포인터는 간접주소지정방식을 사용하는데 간접 주소방식은 지하철 사물함을 생각하면 좋다.
우리는 위와 같이 주소에 직접 값을 대입하는 것이 아니라, 컴퓨터에게 주소를 알려주고 그 주소에 찾아가서 값을 확인하게 만드는 것이다. 다음과 같이 이해하면 된다
이때 사물함은 컴퓨터의 다른 메모리라고 생각하면 된다. 예시를 보여주겠다.
예시>
void Test(short*data){ //num의 메모리 시작주소를 data라는 사물함의 할당
*data = 4;
//*은 번지지정 연산자로써 data라는 사물함에 있는 번지에 찾아가서
// 4를 할당함
return;
}
void main(void){
short int num = 5; //num의 메모리 주소에 2바이트 5가 할당됨
Test(&num); //&연산자는 주소를 출력하는 연산자 따라서 num의 메모리 시작주소
printf("num:%d\n",num); //이전과 달리 num 값이 변함
}
위의 코드를 설명하자면 num이 할당된 메모리 시작주소를 data라는 사물함에 넣어놓은 것이다 그이후 C언어가 *(번지지정연산자)를 사용해서 data라는 사물함안에 있는 번지를 보고 찾아가서 4를 넣었다고 이해하면 된다. 따라서 다른 함수임에도 불구하고 지역변수를
변화시킬 수 있었던 것이다.
여기서 우리는 포인터를 선언하는
short*data
를 자세히 살펴볼 필요가 있다. 앞에 있는 short는 data 즉, data라는 포인터가 갖고있는 시작주소로부터 2바이트(short) 크기를 사용하겠다는 것이다. 그림으로 이해해보자.
그렇다면 data에 따로 시작주소를 넣어주고 번지지정 연산자로 찾아가서 값을 넣어주는 것도 가능한 것인가?
→ 그렇다. 가능하다.
위의 예시는 다음과 같다.
void main(void){
short int num = 5;
short*ptr;
ptr = #
*ptr = 6;
printf("num:%d\n",num);
}
만약 여기서 다음과 같이 num의 코드를 short int(2byte)가 아닌 int(4byte)로 설정할경우 경고가 발생한다.
void main(void){
int num = 5;
short*ptr;
ptr = #
*ptr = 6;
printf("num:%d\n",num);
}
하지만 코드는 위와같이 잘 실행 되는데 만약 값이 커져서 2byte안에 저장을 다 못해놓는 경우가 생긴다면 확실히 오류가 생길 것이다.
다시 한번 정리하자면
short*ptr
→ ptr에 저장한 번지에 찾아가서 short(2바이트) 크기만큼 사용할거야!
라고 선언해주는 것과 같다고 생각하면 된다.
위와 같이 나눠서 쓰지 않고
short*ptr = &num
이라고 쓰는것도 가능하다.
short*ptr = &num
이라고 써도 ptr이라는 사물함에 num의 주소가 들어가게 된다.
여태까지 설명을 하지 않고 넘어간 것이 있다 그것은 바로 주소의 크기이다 주소의 크기는 32비트 운영체제에서는 항상 4바이트인데 따라서 주소를 담아두는 사물함의 크기(ex>위에서 사용한 ptr의 크기)도 항상 4바이트이다
Quiz 포인터를 사용해서 두변수의 값을 Swap해주는 Swap Function을 만들어보길 바란다.(이때 A = 3, B = 6)
void Swap(int*ptr1,int*ptr2){
int tmp = *ptr1;
*ptr1 = *ptr2;
*ptr2 = tmp;
printf("%d\n",*ptr1); //포인터가 가리키는 값 출력
return;
}
void main(void){
int A = 3, B = 6;
Swap(&A,&B);
printf("A Value:%d\nB value:%d\n",A,B);
}
→ 중간에 포인터가 가리키는 값을 출력해봤다
void Swap(int*ptr1,int*ptr2){
int tmp = *ptr1;
ptr1 = ptr2;
*ptr2 = tmp;
printf("%d\n",*ptr1); //포인터가 가리키는 값 출력
return;
}
void main(void){
int A = 3, B = 6;
Swap(&A,&B);
printf("A Value:%d\nB value:%d\n",A,B);
}
위와 같이 사용하면 ptr2에 저장돼있는 주소를 ptr1에 대입하겠다는 것이어서 오류가 발생하진 않지만 원치않는 결과가 나오게 된다. 따라서 다음과 같이 const키워드를 활용할 수 있다
int*const ptr1,int*const ptr2
←이와 같이 하면 주소가 상수로 고정된다는 말이다 따라서
void Swap(int*const ptr1,int*const ptr2){
int tmp = *ptr1;
ptr1 = ptr2;
*ptr2 = tmp;
printf("%d\n",*ptr1); //포인터가 가리키는 값 출력
return;
}
void main(void){
int A = 3, B = 6;
Swap(&A,&B);
printf("A Value:%d\nB value:%d\n",A,B);
}
위와 같이 작성하면 오류가 발생한다
참고
만약 int앞에 const키워드를 사용하면(const int*ptr1
) (번지지정 연산자) 를 사용해서 ptr1 =5와 같이 값을 바꾸려고 하면 오류를 발생시킨다
&num
을 하게 되는 순간 num이라는 변수가 저장돼있는 메모리의 시작주소 를 갖고 오게된다.short*ptr
에서 short가 해당역할을 하는 것을 이미 앞에서 봐왔다.그렇다면 주소에 + 1 을 하게 되면 어떻게 될까?
1번이 시작주소라고 가정했을때 주소에 +1을 하면 2번이 나오게 되나?
→아니다. 위와 같은 경우는 시작주소를 기준으로 4바이트가 포인트 변수가 가리키는 대상이기 때문에, +1을 하게되면 다음 메모리인 5를 가리키게 된다.
Quiz> 그렇다면
위그림에서 int*ptr1
에서 ptr1이 담고있는 주소가 1번이라고 하자 ptr1 = ptr1+3
이라고 연산한다면 ptr1이 가리키는 주소는 몇번인가?
정답: 13
위에서는 int*ptr1
을 선언했기때문에 그렇지만 int*
이 아닌 char* short* double*
등등 또한 맞춰서 생각을 하면 된다.
short * pointer
→ pointer to short
&num
(이때 num은 int형)→ Address of int
이때 pointer의 자료형은 short이고 &num의 자료형은 int이다. 이경우에 자료형을 맞춰주어야 하는데 이 맞춰주는 과정이 바로 Casting 이다. 다음과 같이 맞춰주게 된다.
short*pointer;
pointer = (short*)# //앞에 붙은 short*이 Casting을 말한다
void*
형 포인터void*
형 포인터에 관해서 설명 하겠다.
void*
형 포인터는 앞에서 해왔던 논의 그대로 생각을 해보면 포인터가 가리키는 형태를 지정해주지 않은 상태이다.
이 void형 포인터를 그대로 사용하고자 하면 오류가 생기는데 다음과 같다.
void main(void){
short int A = 3;
void*p1 = &A;
*p1 = 4;
}
왜냐면 시작주소는 알지만 그로부터 얼만큼의 영역에 값을 저장해야할지 모르기 때문이다. 따라서 위에서 말한 Casting을 통해 영역의 크기를 정해줄 수 있다. 다음과 같다.
void main(void){
short int A = 3;
void*p1 = &A;
*(short*)p1 = 4;
printf("%d",A);
}
void형 포인터는 포인터의 크기를 정하는 것을 뒤로 미룰 수 있다는 장점이 있다. 예로써 한가지 상황을 생각해보자
우리가 입력받은 값을 어떤 메모리에 저장을 해두고 싶다. 따라서 좀더 유동성이 좋은 void형 포인터로 메모리의 시작주소만을 저장해두고, 나중에 입력받은 값의 메모리 크기에 따라*(short*)p1
이와 같이 casting을 이용해서 값을 저장해준다면 값의 손실없이 저장할 수 있을 것이다.
만약 포인트의 크기를 정하는 것을 뒤로 미루지 않는다면 int형 포인터 short형 포인터 char형 포인터 등 메모리구조의 크기별로 포인터를 따로 만들어서 사용해야할 것이다
void main(void){
char A = 125; // 외부에서 입력해준 값이라고 가정
void*ptr = &A;
if(sizeof(A)==1){
*(char*)ptr =A;
printf("%d",A);
}
else if(sizeof(A)==2){
*(short*)ptr =A;
printf("%d",A);
}
else if(sizeof(A)==4){
*(int*)ptr =A;
printf("%d",A);
}
}
위와 같이 입력해준 값의 자료형에 따라 pointer가 가리키는 크기를 유동적으로 조절 할 수 있다.
다음시간에는 표준입력함수부터 동적메모리 할당까지 진행하겠다.
출첵이연