깃허브에 로그인 한 후 오른쪽 상단의 + 버튼을 선택하여 ‘New repository’ 를 선택합니다.
저장소의 이름과 간단한 설명을 작성하고 ‘Create repository’ 버튼을 눌러 저장소를 만듭니다.
저는 ‘git-start-guide-1’라는 레포지토리를 만들었는데요, 저장소가 만들어지면 위와 같은 화면을 보실 수 있습니다. 위 스크린샷의 내용 중 ‘https://github.com/…. .git’ 이 부분이 깃 저장소의 위치로 이따가 로컬에 복제(clone)할 때 사용됩니다.
배열 (Array) 이란, 어떤 한가지 자료형을 연속적으로 나열하는 것을 말합니다. 만약 100명의 이름을 저장하여 사용하고자 한다면, 100개의 변수를 선언해서 각각 저장하고 사용해야 하는데, 배열을 사용하면 변수 하나로 아주 쉽게 사용할 수 있습니다.
선언을 할때는
자료형 변수이름[갯수];
와 같이 사용하며, 아래와 같이
int a[5]= {1, 2, 3, 4, 5};
int형 a라는 변수는 5개 만큼을 저장할 수 있는 배열로 선언되었고, 각각의 요소에 1, 2, 3, 4, 5를 저장하도록 초기화 되었습니다. 이런식으로 선언할 수 있고, 초기화는 중괄호 {}로 묶어 각각의 요소를 콤마로 구분하여 합니다.
초기화 이외에 이렇게 사용해선 안됩니다.
int a[5];
a = {1, 2, 3, 4, 5};
배열의 초기화는 변수를 선언할때처럼 생략하거나, 갯수보다 적은 수로 초기화 할 수 있습니다.
int a[5];
int b[5] = {1, 2};
int형 배열 a는 초기값을 주지 않아 모두 쓰레기값이 들어가게 됩니다. 반면에 int형 배열 b는 자신의 크기보다 작은 갯수의 1, 2는 정상적으로 넣고, 그 이후는 0으로 초기화가 됩니다. 그래서 배열을 모두 0으로 초기화 하고자 할때에는
int a[5] = {0};
이렇게 사용하는 것이 편리합니다.
또, 배열을 선언할때 다음과 같이 갯수를 지정하지 않고 쓰는 방법도 있습니다.
int a[] = {1, 2, 3};
이렇게 사용하게 되면, a란 배열은 초기화된 값의 갯수만큼의 크기를 자동으로 가지게 됩니다. 그러나 다음과 같이
int a[];
초기값을 정해주지 않으면 에러메시지를 만나게 됩니다.
저의 경우 []와 같이 크기를 자동으로 지정하도록 사용은 잘 하지 않습니다. 그 이유는 배열의 크기가 커질수록 얼마인지 한눈에 파익하기 쉽지 않기 때문입니다. 편리한 만큼 리스크도 있다는걸 기억하고 계셨다가 적절하게 사용하시면 되겠습니다.
그리고 배열을 선언할때 갯수를 변수로 넣을 수 없습니다.
int a = 5;
int b[a] = {0};
위처럼 배열 b의 크기를 변수로 지정할 수 없고, 반드시 상수값으로 직접 숫자로 써 줘야 합니다.
2. 배열의 사용
이제 선언된 배열을 사용해 봅시다.
int a[5] = {0};
위와같이 int형 배열 a의 갯수를 5개로 지정을 해주고 0으로 초기화를 해줬습니다. 여기에 각각 1, 2, 3, 4, 5를 넣어 보겠습니다.
a[0] = 1;
a[1]= 2;
a[2] = 3;
a[3] = 4;
a[4] = 5;
이런 식으로 대괄호 [] 안의 숫자를 바꿔주면서 값을 넣어주면 됩니다. 이런 배열 전체가 아닌 'a[0]', 'a[1]'와 같은 것을 '배열의 원소' 라고 합니다.
[a[0]][a[1]][a[2]][a[3]][a[4]]
메모리상에는 이렇게 int형 변수가 나란히 붙어 있습니다. 선언을 할때는 갯수로 써주지만, 실제 사용을 할때에는 0부터 갯수만큼의 크기를 가지기 때문에 사용할때 주의하여야 합니다. 그리고,이 각각의, 예를들어 'a[2]' 라는건 보통 int형 변수와 똑같이 사용할 수 있습니다.
배열은 반복문과 사용될때 굉장히 편리합니다.
int a[5]= {1, 2, 3, 4, 5};
int i = 0;
for (i = 0; i < 5; i++)
printf("%d\n", a[i]);
실행 결과
1
2
3
4
5
이런식으로 서로다른 이름의 변수를 사용할 때보다 소스코드가 간결해지고 편리해 집니다.
지난 '문자열' 시간에 char형으로 문자열 변수를 선언했떤걸 기억하실 겁니다. 그것도 정확히 표현하자면 '문자열 변수' 가 아니라 '문자형 배열' 입니다.
char str1[10] = "abc";
char str2[10] = {'a', 'b', 'c'};
printf("%s\n", str1);
printf("%s\n", str2);
실행결과
abc
abc
지난시간까지는 str1의 경우처럼 큰따음표 ""로 묶어 초기화를 해주고 사용했었습니다. 이것을 str2의 경우와 같이 일반 배열처럼 사용해도 결과는 같게 나오는걸 볼 수 있습니다. 하지만 '문자열형'이 없는 C언어이기 때문에 편의상 '문자형 배열'의 경우 큰따음표 ""로 초기화가 가능하게 만들어졌고, 프로그램내에서도 특별하게 다루는 것처럼 보일 뿐입니다.
3. 다차원 배열
지금까지의 배열은 갯수가 하나인 배열이었씁니다. 이것을 '1차원 배열'이라고도 하며, 그 이상으로도 선언하고 사용이 가능합니다.
int a[2][3] = { {1, 2, 3}, {4, 5, 6} };
int i, k;
for (i = 0; i < 2; i++)
for (k = 0; k < 3; k++)
printf("%d\n", a[i][k]);
실행 결과
1
2
3
4
5
6
위 예의 첫번째 줄처럼 대괄호를 하나 더 써서 선언했고, 1차원 배열에서 초기화할때의 값을 콤마 ','로 구분하여 중괄호 {}로 묶어 주었습니다. 그리고 각각 배열에 저장된 값을 출력하고 있습니다.
위 예의 배열 'a'는 이런 모양이라고 생각해볼 수 있습니다.
[ 1 ][ 2 ][ 3 ]
[ 4 ][ 5 ][ 6 ]
사실 메모리상에는 이것들이 한줄로 들어가 있지만 개념을 잡기 위해 입체적으로 나타내본 것입니다. 어쨋든, 이런 형태를 '2차원 배열' 이라고 하고, 예에서 '1, 2, 3' 과 '4, 5, 6' 과 같이 가로방향의 한줄을 '행', '1, 4', '2, 5', '3, 6' 과 같이 세로로 한줄을 '열' 이라고 합니다. 그래서 2차원 배열은 이런식으로 선언이 됩니다.
자료형 변수명[행의 갯수][열의 갯수];
이런식으로 3차원의 배열일 경우 큐브 같은 6면체 모양 생각하시면 되는데, 배열을 이런식으로 우리가 머리속에 어떤 사물이나 그림으로 연상하여 이해하게 되면, 고차원 배열일 수록 상상할 수 있는 모양이 떠오르지 않게 되어 햇깔리기만 합니다.
아래를 봅시다.
int a[2][3][4][5][6][7];
int형 배열 a를 6차원으로 선언하였습니다. 이런걸 그리 쓸일은 많이 없습니다만, 배열의 이해를 위해 예를 들어보았습니다. 6차원이기에 이것을 어떤 입체적인 도형으로 머리속에서 연상하기가 사실상 불가능 합니다. 그래서 그런 행이니 열이니 하는건 그냥 '그렇구나' 하고 알고만 계시고, 다음과 같이 선언의 맨 뒤에서부터 단순하게 생각하면 됩니다.
7개를 넣을 수 있는 공간이 있는데 → 1차원
그게 6개 있고 → 2차원
또 그게 5개 있고 → 3차원
...
이 모든게 2개 있다. → 6차원
그림이 그려지지 않아도 충분히 알 수 있을겁니다. 원소의 갯수를 한번 계산해봅시다.
7 → 1차원
7 * 6 → 2차원
7 * 6 * 5 → 3차원
...
7 * 6 * 5 * 4 * 3 * 2 → 6차원
사실 배열 원소의 갯수 구할 일도 거의 없습니다. 배열의 개념을 잡는데 도움이 되시라고 한번 적어봤습니다.
4. 프로그램 적용
오늘 알아본 '배열'을 이용해서 '성적관리 프로그램'에 여러명의 성적을 입력받아 출력하도록 수정해 봅시다. 우선 변수 선언부터 다음과 같이 고쳐줍니다.
void main()
{
char name[20][20] ={'\0'};// 이름
char grade = '\0'; // 등급
int scoreKOR[20]={0}; // 국어점수
int scoreMAT[20]={0}; // 수학점수
int scoreENG[20]={0}; // 영어점수
int scoreSCI[20]={0}; // 과학점수
char comment[20][200]={'\0'}; // 평가
int index = 0; // 입력받을 배열위치
...
오늘 알아본 배열을 사용해서, 입력받는 부분에 사용한 변수들을 20개만큼의 배열로 선언을 해 주었습니다. 20개라는건 결국 20명까지만 입력을 받을 수 있다는 이야기입니다. 또한 초기화할때 배열은 {}를 사용해야 하므로 해 주었습니다. 뒤의 int형 변수 index는 현재 입력받아야 하는 배열의 위치를 저장하게 됩니다.
다음으로, 입력 부분을 다음과 같이 고쳐줍니다.
...
switch (menuChoice)
{
case '1':
{
// 사용자 입력
do
{
printf("이름을 입력하세요 : ");
scanf_s("%s",temp, sizeof(temp));
len = strlen(temp) + 1;
if (len > sizeof(name[index]))
printf("너무 긴 이름을 입력하셨습니다.\n");
else
strcpy_s(name[index], sizeof(name[index]), temp);
} while (len > sizeof(name[index]));
printf("국어점수를 입력하세요 :");
scanf_s("%d", &scoreKOR[index]);
printf("수학점수를 입력하세요 :");
scanf_s("%d", &scoreMAT[index]);
printf("영어점수를 입력하세요 :");
scanf_s("%d", &scoreENG[index]);
printf("과학점수를 입력하세요 : ");
scanf_s("%d", &scoreSCI[index]);
getchar();
printf("평가를 입력하세요 :");
gets_s(comment[index], sizeof(comment[index]));
index++;
}
break;
...
일반 변수였던 것들을 배열로 선언하였기 때문에, 모두 배열로 바꿔주었고, []안에는 index 변수를 넣어 주었습니다. 모든 입력을 받고 마지막에 index를 1 증가시켜 입력을 다음 배열로 받도록 하였습니다.
출력문도 고쳐보겠습니다. 이전까지의 출력문은 여러명의 성적을 출력하기에는 보기 좋지 않으므로 출력 모양도 바꿔보겠습니다.
기본적으로 입력받는 코드와 같이 일반 변수로 선언된 것들을 배열로 바꿔 주었습니다. 맨 위에 printf() 두줄로 출력 내용이 무엇인지를 먼저 출력해주고, for문으로 i가 index보다 작을때까지 반복하면서 출력합니다.사용한 출력문에 '\t' 서식문자는 키보드 'TAB'키를 누른것과 같은 것으로, 줄을 맞추기 위해 사용했습니다. 이제 잘 되는지 확인해봅시다.
실행을 한 후, 5명을 입력하고 출력해 보았습니다. 만약 배열을 쓰지 않았다면 상당히 많은 변수를 따로 선언해줘야 했을 것입니다.
포인터를 알아보기에 앞서, 우리가 지금까지 사용한 변수들은 어떤 식으로 메모리에 저장이 되는가에 대해 알아보도록 하겠습니다. 다음과 같은 변수가 있다고 합시다.
int a = 1;
int형 a라는 변수를 1로 초기값을 주었습니다. 이것이 메모리에 저장될때는 현재 사용 가능한 메모리 공간의 특정 '주소'에 int형의 크기인 4바이트 만큼 윈도우, 리눅스 등 해당 운영체제에서 공간을 할당받고, 여기에 '값'인 1을 저장하게 됩니다.
그림을 그려보면 이런 식입니다. 각각 바이트단위로 주소가 정해져 있고, 연결된 다음 주소는 1씩 증가하여 부여됩니다. char형으로 하나만 더 예를 들어 보겠습니다.
char str[5] = "abc";
이렇게 선언된 변수는 메모리에 다음과 같이 저장됩니다.
char형은 1바이트만큼의 크기를 가지므로 5개만큼의 배열은 5바이트만큼의 크기를 할당받습니다. 여기에 차례대로 'a', 'b', 'c' 가 들어가게 되는 것입니다. (나머지 두 바이트만큼은 지난번 배열을 알아볼때 이야기했던 것처럼 널문자가 들어가게 됩니다.)
위의 예에서 0x01, 0x02 같은 주소값을 흔히 '번지'라고도 합니다. 우리가 살고 있는 집에도 번지수가 있는데, 이와 같은 맥락이라 보시면 되겠습니다.
요약하자면 변수 (상수 등 프로그램에서 사용하는 대부분의 것들도 마찬가지입니다.) 는 메모리의 일정 위치를 운영체제로부터 자신의 크기만큼 할당을 받고, 여기에 값을 저장하여 사용한다는 것입니다. 그리고 메모리는 각각 바이트 단위로 주소가 있고, 바이트가 증가할때마다 1씩 커진다는 것입니다.
위의 예에서 0x01, 0x02 같이 써놓은건 그냥 예일 뿐입니다. 실제로는 32비트 운영체제의 경우 32비트 만큼의 주소를 가지고, 64비트 운영체제의 경우 64비트 만큼의 주소를 가지게 됩니다. (64비트 운영체제에서 32비트 프로그램을 실행하면 프로그램 내부에서는 32비트 주소를 사용합니다.)
다른건 몰라도 저 위에 요약 3줄은 기억하고 계시기 바랍니다.
2. 포인터
위에서 메모리의 주소에 대해 알아봤는데, 포인터는 이 메모리 주소를 '참조' 하는 녀석입니다. 다음과 같이 변수를 선언했을때
int a = 1;
포인터로 변수 a를 참조하게 되면
이런식으로 변수 a의 시작 주소를 가리키게 됩니다. a의 주소를 가리키기 때문에 포인터로 값을 바꾸면 a의 값도 똑같이 바뀌게 되고, a의 값을 바꾸고 포인터로 값을 읽으면 바뀐 값이 나오게 됩니다.
여기서 이해가 가지 않는다면 바탕화면의 아이콘 중 구석에 화살표가 그려진 '바로가기 아이콘'을 살펴봅시다.
이런 모양을 하고 있을겁니다. 이 아이콘 중 아무거나 하나에 마우스 커서를 올려놓고 오른쪽 버튼을 눌러 메뉴가 나오게 한 후, '속성' 혹은 '등록정보' 를 눌러줍니다.
그러면 다음과 같은 창이 열립니다.
이런 '바로가기 아이콘'들은 위의 창에서 보는 것처럼, 실제 프로그램은 다른 곳에 설치되어 있지만 바로가기 아이콘은 실제 프로그램이 설치된 경로를 가리키고 있습니다. 이 바로가기 아이콘을 누르면 실제 설치된 프로그램을 직접 실행한것과 똑같이 실행하여 사용할 수 있습니다.
포인터도 이런 바로가기 아이콘처럼 다른 변수나 상수 등의 주소를 가리키고 있다가, 마치 자기가 그 변수인양 값을 넣거나 변형할 수 있게 됩니다.
그래도 어렵다면 일단은 포인터란건 주소를 저장한다고 생각하는것도 틀리지 않으니, 일단은 그렇게 알고 아래 부분을 보신 후 이 단락을 다시 한번 읽어봐주세요.
3. 포인터 변수
가장 보편적으로 사용하는 포인터 변수의 선언은 다음과 같습니다.
자료형 *변수명;
주소의 크기는 일정합니다. (32비트, 64비트) 하지만 자료형을 앞에 써주는 이유는, 여기에 저장할 주소가 가리키는 곳에 저장된 '값'이 어떤 형이냐를 정해주는 것입니다. 일단은 int형 변수의 주소를 저장하려면 int형으로 맞춰주는 식이라고만 생각해봅시다.
포인터 변수 선언의 예를 들어봅시다.
int a = 0; // 일반 변수
int *p = NULL; // 포인터 변수
일반 변수의 선언에서 변수명 앞에 '*'를 붙여주는 것 이외에는 별반 다를게 없습니다. 이렇게 선언하게 되면 포인터 변수로 선언이 되어 여기에 주소를 담을 수 있게 됩니다. 초기화를 해주지 않으면 다른 변수들과 마찬가지로 엉뚱한 값이 들어가 있기 때문에 초기화를 해주는 것이 좋은데, 아무것도 없게 초기화할때는 위와 같이 'NULL'이라는 것을 대입해주면 됩니다. 이렇게 값이 'NULL'인 포인터를 '널 포인터' 라고도 부르니 참고하시기 바랍니다.
포인터 변수를 선언할 때에는 반드시 초기화를 해주는 습관을 들이도록 합시다. 포인터 변수 뿐만 아니라 모든 변수를 선언할때 초기화를 해주는 것이 좋은데, 초기화를 해주지 않으면 기본적으로는 전혀 엉뚱한 값이 들어가 있기 때문에 코드를 작성하다 보면 얘기치 않은 오류를 발생시킬 확률이 높습니다. 또한 포인터 변수는 주소를 다루므로 잘못된 주소값으로 인해 프로그램이 멈추는 현상, 강제종료, 심각한 경우 운영체제가 멈추는 경우도 있습니다.
4. 포인터 연산자 (&, *)
그럼 위에서 선언한 포인터 변수에 '주소'를 넣어보겠습니다.
p = &a;
위의 문장은 int형 포인터 변수 p에, int형 변수 a의 주소를 넣습니다. 여기서 '&'라는 것은 '포인터 연산자' 또는 '참조 연산자'라고 부르는 것 중 하나로, 위에서는 a의 주소를 반환하게 됩니다.
다음은 포인터 연산자 '&'의 사용 예입니다.
int a = 0;
int *p1 = &a; // p1에 a의 주소를 저장
int *p2; // a의 주소를 저장하려고 한다.
p2 = &p1; // p2에 p1의 주소를 저장 (X)
p2 = p1; // p2에 p1의 값인 a의 주소를 저장 (O)
위의 예의 4번째 줄 같이 사용할 경우 포인터 변수 자체의 주소를 '&' 연산자를 이용해 대입하기 때문에 원하는 결과를 얻을 수 없습니다. 예의 포인터 변수 p1의 값은 a의 주소이므로 '&' 연산자를 붙이지 않고 직접 대입해야 원하는 결과를 얻을 수 있습니다.
다음은 포인터 변수에 '값'을 넣어보겠습니다.
int a = 0;
int *p = &a; // a의 주소를 저장
*p = 1;
printf("a의 값 = %d\n", a);
실행 결과
a의 값 = 1
3번째 줄에서 포인터 변수 p 앞에 '*'를 붙여주었고, 1을 대입하였습니다. 이 '*' 역시 포인터 연산자 중의 하나로, 포인터 변수에 저장된 주소에 들어있는 '값'을 참조하게 해줍니다. 알기쉽게 포인터 변수에 들어있는 값을 꺼내준다고 생각하셔도 됩니다.
위의 예에서 포인터 변수 p가 가리키는 주소의 값을 1로 대입하였습니다. 그렇기 때문에 그 주소의 원래 주인인 a의 값도 당연히 바뀌어 1이 출력이 되었습니다. 이처럼 포인터 변수를 이용해 다른 변수의 값을 읽거나 변형할 수 있습니다.
정리해보면
& : 변수 또는 상수의 주소
* : 포인터가 가리키는 주소에 저장된 값
으로 요약할 수 있습니다.
이번시간에는 포인터의 기본 개념과 포인터 변수를 선언하고, 간단하게 사용하는 방법에 대해 알아보았습니다. 다음시간에는 좀 더 다양한 포인터의 사용법에 대해 다루도록 하겠습니다.