-keyword-
1, 2, 3차원 배열, 1, 2, 3차원 배열 포인터, 다차원 배열 포인터 메모리 동적 할당
Unit 36. 배열 사용하기
-배열(Array)-
c언어의 배열은 파이썬의 list와 비슷한 기능을 한다. 여러 값들을 한 변수에 저장하는 것이다. 파이썬과 똑같이 0부터 시작하는 인덱스를 갖고 여러 개의 값을 한 변수에 사용이 가능하다. 단 파이썬과 달리 c언어에서의 배열은 같은 자료형의 값들만 저장할 수 있다.
이 배열은 파이썬과 비슷하게 반복문으로 값을 한꺼번에 저장하거나 빼낼 때 주로 사용한다. 앞서 코드업에서 바둑판, 격자판 등도 이 배열을 이용해서 풀었다.
사용은 <자료형> <배열명>[<크기>] = {<값들>};로 한다. 꼭 배열을 선언하자마자 크기만큼 값들을 넣어줄 필요 없이 선언만 하고 나중에 값들을 넣어줘도 된다. 마찬가지로 배열 안의 요소들은 인덱스 값을 갖고 첫 번째 요소부터 0, 1, 2...가 된다.
배열의 요소는 <배열명>[<인덱스>]로 특정 인덱스의 값을 가져올 수 있다.
참고로 배열의 범위를 벗어난 인덱스에 접근하면 오류가 발생했던 파이썬과 다르게 c언어에서는 배열의 범위를 벗어난 인덱스에 접근해도 오류가 뜨지 않는다. 대신 그 인덱스를 출력한다면 쓰레기 값이 출력되는 것을 볼 수 있다.
앞서 정리한 메모리를 어느 정도 이해한다면 그 이유를 이해할 수 있는데, 이 배열도 마찬가지로 선언하면 메모리를 할당받는다. 근데 이 배열이 저장된 메모리의 위치를 벗어난 곳(인덱스)에 접근하다면 그 위치에 저장된 쓰레기 값을 가져오거나 아무 영향이 없는 위치에 값을 할당할 수도 있는 것이다. 또한 잘못 할당하면 이미 사용되고 있는 메모리에 값을 할당해 프로그램이 제대로 실행이 안될 수도 있다.
c언어는 속도가 빠른 대신 취약함, 메모리 등의 잘못됨을 검사하지 않기 때문에 컴파일 오류를 발생시키지 않는 것 같다.
#include <stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 }; // 크기가 5인 int형 배열 선언 및 값들 할당
printf("%d\n", arr[3]); // arr 배열에서 인덱스가 3인 요소의 값 출력
arr[2] = 10; // arr 배열에서 인덱스가 2인 요소의 값을 10으로 변경
printf("%d", arr[2]); // arr 배열에서 인덱스가 2인 요소의 값 출력
return 0;
}
위 코드는 크기가 5인 int형 배열을 선언하고 값들을 할당한 후 해당 배열의 인덱스 3인 요소의 값 출력, 인덱스 2인 요소의 값을 10으로 변경 후 출력한다.
결과를 보면 첫 줄에는 arr 배열의 인덱스 3인 요소의 값인 4가 출력되고, 두 번째 줄에는 10으로 변경된 arr 배열의 인덱스 2인 요소가 출력된다.
#include <stdio.h>
int main()
{
int arr[5] = {0, }; // 크기가 5인 int형 배열 선언 및 요소들을 0으로 초기화
printf("%d %d", arr[3], arr[4]); // arr 배열에서 인덱스가 3, 4인 요소 출력
return 0;
}
위 코드처럼 배열의 요소를 0, 로 설정하면 해당 배열의 요소들이 모두 0이 된다.
출력 결과를 보면 배열 전체가 0으로 초기화됐음을 알 수 있다.
int arr[5] = {1, };
시험 삼아 위처럼 작성해봤는데, 위처럼 작성할 경우에는 첫 번째 요소는 1이 되고 나머지 요소들이 0이 된다. ,를 써주면 그 뒤의 요소들은 모두 0으로 초기화되는 것 같다.
-sizeof(<배열>);
sizeof(<배열>);을 사용하면 배열의 크기를 구할 수 있다.
#include <stdio.h>
int main()
{
int arr[5] = {0,}; // 크기가 5인 int형 배열 선언
printf("%d", sizeof(arr)); // arr 배열의 크기 출력
return 0;
}
위는 sizeof(arr)로 arr 배열의 크기를 출력하는 코드다.
실행결과를 보면 20이 출력되는데, 이는 단순히 배열의 크기를 출력하는 것이 아닌 메모리에서 배열이 차지하는 공간을 출력하기 때문에 4(int형 자료형의 크기)*5(배열의 크기)한 값이 20이 출력되는 것이다.
sizeof(arr) / sizeof(int)
이 sizeof를 이용해 위처럼 작성하면 배열의 크기인 5를 구할 수 있다. c언어는 파이썬처럼 반복 가능한 객체를 반복문으로 간단하게 출력해주는 기능(for i in <반복 가능한 객체> 등)이 없기 때문에, 위 코드로 배열의 크기만큼 출력하도록 for문 등의 반복식을 설정하면 유용하다.
-반복문 X 배열
위 sizeof를 응용해 for 등의 반복문으로 배열의 요소들을 출력할 수 있다.
#include <stdio.h>
int main()
{
int arr[5]; // 크기가 5인 int형 배열 선언
for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
arr[i] = i+1;
// 배열의 크기 만큼 반복하며 배열에 1~5를 할당
for (int i = (sizeof(arr) / sizeof(int))-1; i >= 0; i--)
printf("%d ", arr[i]);
// 배열의 크기 만큼 반복하며 배열의 요소를 역순으로 출력
return 0;
}
위는 크기가 5인 int형 배열을 선언하고 for문으로 1~5까지 배열의 요소에 할당한 후 요소들을 역순으로 출력하는 코드다. sizeof(arr) / sizeof(int)를 이용해 배열의 크기만큼 반복하도록 하고 배열의 인덱스 i에 i+1 값을 넣어줌으로써 1~5까지의 값이 차례로 배열의 요소로 할당이 된다. 두 번째 for문 역시 마찬가지로 배열의 크기를 sizeof로 구하는데, 이번에는 i를 배열의 크기 -1 값(배열의 크기가 5일 경우 1~5가 아닌 0~4기 때문)으로 할당하고 -= 1을 해주면서 i가 0과 같거나 클 때까지 반복한다. 당연히 i가 4에서 0으로 변해가기 때문에 이 i번째 인덱스를 출력하면 역순으로 출력되는 것이다.
arr 배열의 요소인 1,2,3,4,5가 역순으로 5 4 3 2 1로 출력되는 것을 볼 수 있다.
#include <stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 }; // 크기가 5인 int형 배열 선언 및 초기화
int sum = 0; // 매 반복마다 더한 값을 누적할 변수
for (int i = 0; i < (sizeof(arr) / sizeof(int)); i++)
sum += arr[i] * 2;
// 배열의 크기 만큼 반복하며 배열의 요소를 *2해 sum 변수에 더해줌
printf("%d", sum); // sum 값 출력
// 2 + 4 + 6 + 8 + 10 = 30
return 0;
}
응용하면 위 코드처럼 배열 안의 요소를 *2 해서 모두 더한 값을 구할 수 있다. 원리는 비슷하게 배열만큼 반복문을 돌리며 각 인덱스의 요소를 *2 해서 더한 값을 구하는 용도로 쓰이는 변수(sum)에 더하면 된다. 그 후 누적된 값을 출력.
각 요소를 *2한 값인 2, 4, 6, 8, 10을 더한 30이 출력됐다.
이 말고도 배열의 값을 특정 값만큼 증감하는 등 여러 가지 용도로 사용할 수 있다.
-배열을 활용한 10진수 -> 2진수 변환 알고리즘
예전에 정보처리기능사 자격증을 딸 때 배웠던 것인데 10진수를 2로 계속 나누며 각 나머지를 역순으로 나열하면 해당 10진수 값이 2진수 값으로 변환된다.
#include <stdio.h>
int main()
{
int dec = 10; // 10진수 값 변수
int bin[20] = { 0, }; // 2진수 배열 변수
int pos = 0; // 자릿수 변수
while (1)
{
bin[pos] = dec % 2; // 10진수를 2로 나눴을 때의 값을 순차적으로 배열에 저장
dec /= 2; // 10진수 값을 2로 나눴을 때의 몫을 저장
pos++; // 자릿수 + 1
if (dec == 0)
break;
// 10진수를 2로 나눴을 때의 몫이 0일 경우(더 이상 나눠지지 않는 경우) 반복 종료
}
for (int i = pos -1; i >= 0; i--) // 역순으로 2진수 배열을 출력
{
printf("%d", bin[i]);
}
return 0;
}
위는 10진수를 2진수로 바꿔주는 코드다.
10진수, 2진수를 각각 담을 변수들을 선언하고 2진수 자릿수를 나타낼 변수를 선언했다. 그리고 무한 루프를 돌린다.(얼마나 나눠야지 10진수를 더 이상 나눌 수 있는지가 정해져 있지 않기 때문에 무한적으로 돌리는 것이다.)
매 반복마다 2진수 배열 변수의 자릿수 번째의 인덱스의 요소를 10진수 변수를 2로 나눴을 때의 나머지로 바꿔주고 10진수 변수는 해당 변수를 2로 나눴을 때의 몫을 저장한다. 그리고 자릿수를 + 1 해준 후 더 이상 10진수 변수를 나눌 수 없는지 조건문으로 확인한다. 더 이상 나눌 수 없는 경우에는 반복을 멈추고 역순으로 2진수 배열을 출력한다.(나머지를 역순으로 배열해야지 해당 10진수 값의 2진수 값을 구할 수 있기 때문)
10을 2진수로 바꾼 값인 1010이 출력된다.
dec | bin | 몫 | 나머지
10 | | 5 | 0
5 | 0 | 2 | 1
2 | 01 | 1 | 0
1 | 010 | 0 | 1
0101을 역순으로 하면 1010
-배열 포인터
배열도 메모리에 저장되기에 포인터에 주소 값을 넣을 수 있다. 배열 포인터 선언은 일반 포인터 선언과 다를 게 없다.(사실 배열, 일반 포인터는 똑같은 개념이다.) 그냥 넣을 배열의 자료형에 맞춰서 포인터를 선언하면 된다.
배열의 주소를 넣은 포인터를 역참조한 값을 출력하면 첫 번째 값만 출력된다.
0x000000 | 01 00 02 00 03 00 04 00 05 00
그 이유는 사실 배열은 요소들의 주소를 하나하나 다 갖고 있는 게 아닌 첫 번째 요소의 주소만 담고 있기 때문에 위처럼 int형 배열의 요소 1,2,3,4, 5가 000000 위치(예시)에 저장됐다 가정할 때 이 배열을 넣은 포인터를 역참조하면 000000 주소의 값을 int형의 크기인 4바이트만큼만 접근하기 때문에 첫 값만 출력되는 것이다.
따라서 포인터에 배열을 넣을 때는 & 없이 <포인터> = <배열>로 하면 되고,
다른 요소들에 접근할 때는 역참조 없이 <포인터>[<인덱스>]를 작성해주면 해당 인덱스의 값에 접근 가능하다.
#include <stdio.h>
int main()
{
int arr[5] = { 1, 2, 3, 4, 5 }; // 크기가 5인 int형 배열 선언 및 초기화
int *p = arr; // 포인터에 배열을 넣음
printf("%d\n", p[1]); // arr의 두 번째 요소 출력
p[3] = 10; // arr의 네 번째 요소를 10으로 변경
printf("%d", p[3]); // arr의 네 번째 요소 출력
return 0;
}
위는 배열을 담은 포인터를 인덱스를 이용해 각 요소에 접근하고 값을 바꿔본 것이다.
첫 줄에는 p[1]의 요소(arr[1])인 2가, 두 번째 줄에는 10으로 바뀐 p[3]의 요소(arr[3])가 출력됐다.
참고로 sizeof로 배열의 크기를 확인하면 (자료형의 크기) * (배열의 크기)가 구해졌지만, sizeof로 배열을 담은 포인터의 크기를 확인하면 포인터의 크기, 즉 배열의 주소의 크기가 출력된다.
Unit 37. 2차원 배열 사용하기
-2차원 배열-
2차원 배열은 파이썬의 2차원 리스트와 유사하다. 다른 점은 앞에 정리한 배열과 리스트의 차이다.
마찬가지로 2차원 배열은 [행][열] 형태로 이루어져 있고 인덱스는 0부터 시작한다.
유의해야 할 점이 코드업 기초 100제를 풀면서도 정리했지만 일반적으로 2차원 공간은 x, y로 이루어져 있지만 배열에서는 [x], [y]가 아닌 [y][x] 이렇게 반대로 이루어져 있다.
선언은 위에서 정리한 배열과 파이썬의 2차원 리스트와 비슷하게
<자료형> <변수명>[<세로크기><가로크기> =
{
{<값들>}, {<값들>} ....
};
로 하면 된다. { }로 여러 개의 {} 행 값들을 감싸줘야 한다는 점을 유의해야 한다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int arr[2][2] = { { 1,2 }, { 3,4 } }; // int형 2차원 배열 선언
printf("%d\n", arr[0][1]); // arr 배열의 [0][1] 요소 출력
arr[1][0] = 10; // arr 배열의 [1][0] 요소를 10으로 바꿈
printf("%d", arr[1][0]); // arr 배열의 [1][0] 요소 출력
return 0;
}
위는 2 2 크기의 2차원 배열을 선언하고 0 1 인덱스를 출력, 1 0 인덱스의 값을 10으로 변경 후 출력한 코드다.
위 사진이 arr 배열의 초기 모습이다.
첫 줄에는 [0][1] 요소의 값인 2가 두 번째 줄에는 10으로 바뀐 [1][0] 요소의 값이 출력된 것을 볼 수 있다.
int arr[2][2]= { 0, };
마찬가지로 2차원 배열 역시 ,를 써주면 그 뒤의 모든 요소들이 0으로 채워진다. 위 코드는 앞에 0을 써줬으니 모든 요소가 0이 될 것이다.
또한 인덱스를 벗어나게 돼도 똑같다. 파이썬처럼 오류는 없지만 다른 변수 등이 쓰고 있는 메모리 공간에 잘못 접근할 수도, 쓰레기 값에 접근할 수도 있다.
-sizeof
1차원 배열처럼 2차원 배열 역시 sizeof를 통해 쉽게 배열의 크기를 구할 수 있는데, 1차원과는 방법이 좀 다르다.
1차원은 sizeof(<배열>) / sizeof(int)를 썼지만 2차원은 가로, 세로의 크기를 구해야 하므로
가로 : sizeof(<배열>[0]) / sizeof(int)
세로 : sizeof(<배열>) / sizeof(<배열>[0])
가로는 배열의 첫 한 행의 크기를 구한 후 int 크기로 나눠주면 되고, 세로는 전체 크기를 가로 한 줄의 크기로 나눠주면 된다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int arr[2][2] = { { 1,2 }, { 3,4 } }; // int형 2차원 배열 선언
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++) // 세로 크기만큼 반복
{
for (int j = 0; j < sizeof(arr[0]) / sizeof(int); j++) // 가로 크기만큼 반복
{
printf("%d ", arr[i][j]); // 해당 요소를 출력
}
printf("\n"); // 가독성을 위해 한 행이 끝날 때 개행해줌
}
return 0;
}
위는 앞서 만든 2 2 크기의 arr 배열의 요소들을 중첩 for문으로 모두 출력하는 코드다. 첫 번째 for문(i)는 세로 크기만큼 반복을, 두 번째 for문(j)은 가로 크기만큼 반복하면서 i, j 인덱스에 해당하는 요소를 출력해준다. 두 번째 for문이 끝났다는 건 가로 한 줄을 출력한 것이므로 가독성으로 위해 \n로 개행을 해줬다.
1 2
3 4
로 arr 배열의 요소들이 잘 출력됨을 볼 수 있다.
-2차원 배열 포인터
2차원 배열도 역시 포인터에 넣을 수 있다. 2차원 배열이면 뭔가 이중 포인터에 넣어야 할 것 같은 느낌이 들지만, 이중 포인터에 넣어보면 컴파일 경고와 에러가 뜨는 것을 볼 수 있다.
2차원 배열을 담을 포인터는 <자료형> (*<포인터 명>)[<가로 크기>]로 선언하면 된다. 만약 괄호 없이 *<포인터 명>[<가로 크기>]로 사용한다면 2차원 배열 포인터가 아닌 그냥 여러 개의 주소를 담을 수 있는 포인터 배열이 돼버리니 조심해야 한다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int arr[2][2] = { { 1,2 }, { 3,4 } }; // int형 2차원 배열 선언
int(*ptr)[sizeof(arr[0]) / sizeof(int)] = arr; // arr를 담을 2차원 포인터 선언
printf("%p %p\n", *arr, *ptr); // arr, ptr 역참조 값 출력
printf("%d", ptr[0][1]); // 포인터가 가리키는 메모리의 [0][1] 인덱스 요소 값 출력
return 0;
}
위는 2차원 배열을 선언하고 2차원 배열을 담는 포인터도 선언, 그리고 역참조 했을 때의 값과 2차원 배열을 담은 포인터에서 [0][1] 인덱스 요소 값을 출력하는 코드다.
보면 배열과 배열을 담은 포인터를 역참조 했을 때의 값은 세로 첫 번째 주소가 출력되고, 두 번째 줄에서는 [0][1] 인덱스의 값이 제대로 출력되는 것을 볼 수 있다.
bp를 걸고 디버깅을 해보면 역참조를 했을 때의 값이 배열의 세로 첫 번째 주소인 것을 알 수 있다.
printf("%d", **ptr);
코딩도장에 나와있지 않지만 호기심에 ** 이렇게 이중 역참조를 해보니 [0][0]의 요소가 출력되는 것을 확인했다.(역참조를 했을 때 [0][0]의 주소가 구해지니 그 상태에서 역참조를 한 번 더 하면 [0][0]의 값에 접근할 수 있는 것이다.)
1차원 배열과 똑같이 sizeof를 사용해보면 배열은 배열이 메모리에 차지하는 공간(가로 크기 * 세로 크기 * 자료형의 크기)이 구해지고, 포인터는 주소의 크기가 구해진다.
-3차원 배열-
3차원 배열은 말 그대로 3차원이다. 정육면체를 상상하면 이해가 쉽다.
선언은 <자료형> <배열 명>[<높이>][<세로 크기>][<가로 크기>]로 하면 되고 요소 접근 방법은 1, 2차원과 동일하니 따로 작성하지는 않겠다.
-3차원 배열 포인터
3차원 배열 포인터는 <자료형> (*<포인터 명>)[<세로>][<가로>]로 선언하면 된다. 원리 역시 1, 2차원과 유사하니 따로 설명하지는 않겠다.
Unit 38. 포인터와 배열 응용하기
-포인터, 배열 응용-
지금까지는 크기가 고정된(크기를 선언한) 배열을 사용했다. 코드업에서도 많이 접했지만 알고리즘 문제나 특정 프로그램을 개발할 때 얼마나 입력될지 모르는 경우가 있다. 코드업 문제의 경우 최대 입력 개수를 크기로 배열을 선언해서 반복문을 통해 입력을 받았지만, 개인적인 생각이지만 이러면 메모리 낭비가 될 수도 있을 것 같다.
예를 들어 최대 1000번까지 정수 입력을 받는다 해서 1000 크기의 int형 배열을 선언했지만 정작 5번만 입력을 받는다면 20(5*4) 바이트를 제외한 3980 바이트(995*4)의 공간은 사용하지 않고 남겨두는 것이다.
무작정 입력받는 경우는 최대 크기로 선언하는 게 맞긴 하지만 코드업 문제 같은 경우 첫 줄에 무조건 입력의 개수가 주어졌으므로 뒤에 정리하는 포인터를 배열처럼 사용하는 방법이 유용할 것이다.
-포인터를 1차원 배열처럼 사용
포인터를 배열처럼 사용하는 방법은 malloc 함수로 동적으로 메모리를 할당해주면 된다.
<자료형> *<포인터 명> = malloc(sizeof(<자료형> * <크기>));
malloc으로 확보하는 메모리(힙)의 크기를 자료형 * 크기로 해주는 이유는 배열에 사용될 요소만큼 공간을 확보하기 위해서다. 예를 들어 크기가 4인 int형 배열을 선언하면 메모리(스택)에는 4 * int형의 크기만큼 공간이 확보된다. 즉 malloc에서 자료형 * 크기를 해주는 이유도 넣을 값의 개수(4)만큼 * 자료형의 크기(4)만큼 총 16 바이트의 메모리 공간을 확보해야 하기 때문이다.
위는 코딩도장에 나와있는 포인터를 배열처럼 사용 관련 사진이다. 위 사진의 경우 int *numPtr = malloc(sizeof(int)*10);으로 40만큼 동적 할당한 것이다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int size;
scanf("%d", &size);
//size 변수 선언 후 입력
int *ptrArr = malloc(size * sizeof(int)); // size*4만큼 메모리 확보
for (int i = 0; i < size; i++) // size만큼 반복하며 ptrArr에 입력을 받음
{
scanf("%d", &ptrArr[i]);
}
for (int i = 0; i < size; i++) // size만큼 반복하며 ptrArr의 요소들 출력
{
printf("%d ", ptrArr[i]);
}
free(ptrArr); // 메모리 해제
return 0;
}
위는 포인터를 배열처럼 사용해 코드업 문제와 비슷하게 횟수를 입력받고 횟수만큼 반복하며 입력을 받은 후, 입력받은 값을 차례대로 출력하는 코드다.
먼저 첫 줄에서 횟수를 입력받고 그 횟수*4만큼 malloc함수로 포인터 ptrArr에 메모리를 동적으로 할당받는다. 그리고 size만큼 반복하며 ptrArr[i] 주소에 입력을 받은 후 size만큼 반복하며 ptrArr의 요소들을 출력한다.
첫 줄에 5를, 두 번째 줄에 1 2 3 4 5를 입력하니 5만큼의 동적으로 메모리를 확보해 해당 공간에 1 2 3 4 5를 저장한 후 이 1 2 3 4 5를 그대로 출력된 것을 볼 수 있다.
malloc으로 동적으로 메모리를 할당한 후 free로 메모리를 해제해야 한다는 것을 잊으면 안 된다.
-포인터를 2차원 배열처럼 사용
2차원 배열 역시 포인터와 malloc 함수로 동적으로 할당받아 사용할 수 있다.
2차원 배열 포인터 사용 방법
- <자료형> **<포인터 명> = malloc(sizeof(<자료형> *) * <세로크기>);
- 세로 크기만큼 반복하며 <포인터>[i] = malloc(sizeof(<자료형>) * <가로크기>;
- 사용
- 세로 -> 가로 순서로 만들었으니 이제는 가로 -> 세로 순서로 해제시킨다. 세로 크기만큼 반복하며 free(<포인터>[i]);로 가로 메모리 해제
- free(<포인터>);로 세로 메모리 해제
1차원 배열보다는 좀 복잡한 과정을 거친다.
위 사진은 Unit 38.3에 나와있는 세로 공간 할당 관련이다.
2차원 배열 포인터 사용 방법의 1번을(자료형은 int, 포인터 명은 m, 크기는 3 3) 실행했을 때 메모리 공간에서 발생하는 것을 사진으로 나타낸 것이다.
1번을 실행했을 때 malloc으로 int 포인터의 주소 크기 * 3(세로 크기)만큼의 메모리 공간을 확보하고 그 주소를 이중 포인터 m에 담는다. 이 이중 포인터 m에 배열의 가로에 해당하는 주소들이 담길 것이다.
위 사진은 Unit 38.3에 나와있는 가로 공간 할당 관련이다.
위 사진은 2차원 배열 포인터 사용 방법의 2에 해당한다. 반복문으로 세로 크기만큼 반복하며 malloc으로 int의 크기, 세로의 크기만큼 메모리 공간을 할당하는데, 이 공간의 주소들을 이중 포인터 m에 순차적으로 저장한다.
이 과정을 끝내면 이중 포인터 m은 세로 크기만큼의 가로의 주소들을 배열로 담게 되고 담긴 가로의 주소는 각각 가로 크기만큼의 공간을 가리키니 [][]로 인덱스를 지정해 값을 할당하고 변경할 수 있다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int sizeY, sizeX;
scanf("%d %d", &sizeY, &sizeX);
// 세로, 가로 크기를 담을 변수 선언 및 입력
int **p = malloc(sizeof(int *) * sizeY);
// 세로 크기 * int형 포인터 주소의 크기 만큼 메모리를 확보하고 그 주소를 이중 포인터 p에 저장
for (int i = 0; i < sizeY; i++)
{
p[i] = malloc(sizeof(int) * sizeX);
}
// 세로 크기만큼 반복하며 4*가로의 길이만큼 크기의 메모리 확보 후 p[i]에 차례대로 저장
for (int i = 0; i < sizeY; i++)
{
for (int j = 0; j < sizeX; j++)
{
scanf("%d", &p[i][j]);
}
}
// sizeY * sizeX 만큼 반복하며 p[i][j]의 주소에 입력 값 저장
for (int i = 0; i < sizeY; i++)
{
for (int j = 0; j < sizeX; j++)
{
printf("%d ", p[i][j]);
}
printf("\n");
}
// sizeY * sizeX 만큼 반복하며 p[i][j]의 값 출력
for (int i = 0; i < sizeY; i++)
{
free(p[i]);
}
// 세로의 크기만큼 반복하며 p[i]의 주소 메모리 해제
free(p); // 이중 포인터 p의 주소 메모리 해제
}
위는 2차원 배열 포인터 사용 방법에 맞춰서 작성한 코드다. 사용자가 입력한 크기의 2차원 배열을 생성하고 값을 입력받은 후 그 값들을 출력해준다.
먼저 세로, 가로의 크기를 담을 변수를 선언하고 scanf로 입력을 받았다. 그 후 2차원 배열 포인터 사용 방법에 맞게 코드를 작성했다.
- malloc 함수로 세로 크기 * int형 포인터 주소의 크기만큼 메모리 공간을 확보하고 이중 포인터 p를 선언과 동시에 그 주소를 저장했다.
- 세로 크기만큼 반복하며 malloc으로 4 * 가로 크기만큼 메모리를 확보하고 그 주소를 p[i]에 저장했다.
- 가로 크기 * 세로 크기만큼 반복하며 해당 인덱스에 입력 값을 저장했고, 가로 크기 * 세로 크기만큼 또 한 번 반복하여 입력된 요소들을 모두 출력했다.
- 세로 크기만큼 반복하여 p[i]에 저장된 주소의 메모리를 해제했다.
- 이중 포인터 p에 저장된 주소의 메모리를 해제했다.
첫 줄에 2 3을 입력하고 두 번째 줄부터 입력한 크기만큼 요소들을 입력했다. 입력한 크기만큼 요소들을 모두 입력하니 입력한 요소들을 출력해준 것을 볼 수 있다.
처음에는 2차원 배열 포인터를 구현하는 코드의 분량이 많고 메모리 관련 사진도 복잡해서 어려워 보였지만 원리를 이해하니 생각보다 쉽고 예전에 아무 생각 없이 사용했던 코드들의 비밀을 컴퓨터의 관점에서 파 해쳐 보는 것 같아서 재밌었다.
-번외
#define _CRT_SECURE_NO_WARNING
#include <stdio.h>
int main()
{
int size;
scanf("%d", &size); // 배열의 크기를 입력받음
int numArr[size]; // GCC에서는 사용 가능, Visual Studio 2017에서는 컴파일 에러
return 0;
}
위 코드는 c언어 코딩도장 Unit 38.0의 코드다. 지금까지 배열을 선언할 때 무조건 크기를 지정해줬지만(안 지정해주면 visual studio 2017 기준으로 오류가 뜬다..), 리눅스의 c 컴파일러인 gcc에서는 오류가 뜨지 않는다고 한다.
리눅스에서 개발한다면 위에 정리한 내용들이 필요하지는 않겠지만(실무에서는 쓰일지도 모른다.) 어디까지나 프로그래머가 되기 위해 c언어를 공부하는 게 아닌 보안의 관점, c언어를 배우는 입장에서 공부하는 것이니 열심히 배웠다.
참고로 gcc에서만 가능한 저 기능을 가변 길이 배열이라고 부른다.
Unit 36 ~ Unit 38 문제
요소가 100개인 배열은 <자료형> <배열명>[100];으로 선언하면 된다.
답 : c
요소가 11개인 배열의 마지막 요소에 접근하는 방법은 <배열명>[10]으로 하면 된다.
답 : d
요소 개수를 알아내는 방법은 sizeof(<배열>) / sizeof(<배열의 자료형>)으로 구하면 된다.
답 : e
a : 파이썬과 달리 c언어에서는 끝 값을 -로 접근할 수 없다. X
e : 크기가 100인 배열은 1~100까지가 아닌 0~99까지의 인덱스를 갖는다. X
답 : a, e
scores 배열에 저장된 값의 평균을 출력하는 것이 목적이다. 첫 번째 칸에서 변수 sum을 통해 배열의 값들을 더해서 누적하고 두 번째 칸에서 누적된 값을 배열의 개수인 10으로 나눈 값을 average에 넣어주면 될 것 같다.
#include <stdio.h>
int main()
{
float scores[10] = { 67.2f, 84.3f, 97.0f, 87.1f, 71.9f, 63.0f, 90.1f, 88.0f, 79.7f, 95.3f };
float sum = 0.0f;
float average;
for (int i = 0; i < sizeof(scores) / sizeof(float); i++)
{
sum += scores[i]; // 매 반복마다 배열의 요소들을 sum에 더해서 누적시킴
}
average = sum / sizeof(scores) / sizeof(float); // sum의 값을 배열의 크기로 나눈 값 저장
printf("%f\n", average);
return 0;
}
모든 요소들을 더한 값을 sum에 누적시키기 위해 배열의 크기만큼 반복하는 for 반복문 안에 sum += scores[i]를 작성했고, 반복을 통해 누적된 값을 배열의 크기로 나눠서 평균을 구하고 그 값을 average에 저장하기 위해 average = sum / sizeof(scores) / sizeof(float);를 작성했다.
2진수를 10진수로 변환해서 출력하는 것이 목적이다. 2진수를 각 자리마다 1을 자릿수로 왼쪽 시프트 해서 더하면 10진수가 되므로 이를 이용하면 될 것 같다.
#include <stdio.h>
int main()
{
int decimal = 0;
int binary[4] = { 1, 1, 0, 1 };
for (int i = 0; i < sizeof(binary) / sizeof(int); i++) // 2진수 배열의 요소만큼 반복
{
if(binary[i] == 1) // 해당 요소가 1이라면 해당 자릿수만큼 <<해서 10진수에 더해줌
decimal += 1 << sizeof(binary) / sizeof(int) - i - 1;
}
printf("%d\n", decimal);
return 0;
}
2진수 배열의 요소만큼 반복하는 for문을 만들고, for문 안에 해당 요소가 1이면 특정 코드를 실행하는 조건문을 작성했다. 이 조건이 참일 시 1 << (2진수 배열의 크기 - i - 1)을 해주는데 그 이유는 사실상 2진수 배열의 첫 번째 요소가 곧 끝 자리 요소이므로 끝 자리만큼 <<를 해줘야 하기 때문이다. 위 코드의 경우 끝 자리가 4이므로 첫 번째 반복은 4 - 0 -1로 3만큼 <<를 해주게 된다. -1을 해준 이유는 맨 첫자리는 0만큼 <<를 해야 하기 때문이다.
입력되는 정수 5개가 numArr 배열에 차례대로 저장되는데, 이 값들 중 가장 작은 값을 구하는 것이 목적이다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int numArr[5];
int smallestNumber;
scanf("%d %d %d %d %d", &numArr[0], &numArr[1], &numArr[2], &numArr[3], &numArr[4]);
smallestNumber = numArr[0]; // 첫 번째 입력 값을 초기 기준으로 저장
for (int i = 0; i < sizeof(numArr) / sizeof(int); i++) // 배열의 크기만큼 반복
{
if (smallestNumber > numArr[i])
smallestNumber = numArr[i];
/* 만약 smallestNumber가 해당 요소보다 크다면 해당 요소가 현재 시점에서
가장 작은 값이므로 smallestNumber에 저장 */
}
printf("%d\n", smallestNumber);
return 0;
}
먼저 가장 작은 값을 담을 변수인 smallestNumber에 일단 작음의 기준이 될 값으로 첫 번째 입력 값을 넣는다. 그리고 요소만큼 반복하는 for문을 작성해 해당 요소(i번째 인덱스)가 smallestNumber보다 작다면 현재 반복 시점에서 가장 작은 값이므로 smallestNumber를 해당 요소의 값으로 변경한다.
최종적으로 반복이 끝났을 때 smallestNumber 변수에는 가장 작은 값이 저장된다.
세로 크기 5, 가로 크기 3인 float형 2차원 배열은 float <배열 명>[5][3]으로 선언하면 된다.
답 : c
세로 크기를 알아내는 방법은 sizeof(<배열>) / sizeof(<배열>[0])으로 하면 된다.
답 : d
가로 크기는 sizeof(<배열>[0]) / sizeof(<자료형>)으로 하면 된다.
답 : c
b : [2] [2] 크기는 [0 ~ 1] [0 ~ 1]로 이루어지므로 2에는 접근할 수 없다. X
d : 파이썬과 달리 c언어에서 음수 인덱스는 사용할 수 없다. X
e : [2] [2] 크기는 [0 ~ 1] [0 ~ 1]로 이루어지므로 3에는 접근할 수 없다. X
답 : b, d, e
주어진 2차원 배열의 요소를 사진에 표시한 대각선 요소들을 출력하는 것이 목적이다.
#include <stdio.h>
int main()
{
int matrix[8][8] = {
{ 1, 2, 3, 4, 5, 6, 7, 8 },
{ 9, 10, 11, 12, 13, 14, 15, 16 },
{ 17, 18, 19, 20, 21, 22, 23, 24 },
{ 25, 26, 27, 28, 29, 30, 31, 32 },
{ 33, 34, 35, 36, 37, 38, 39, 40 },
{ 41, 42, 43, 44, 45, 46, 47, 48 },
{ 49, 50, 51, 52, 53, 54, 55, 56 },
{ 57, 58, 59, 60, 61, 62, 63, 64 }
};
for (int i = 0; i < sizeof(matrix) / sizeof(matrix[0]); i++) // 세로의 크기만큼 반복
{
printf("%d ", matrix[i][i]); // 해당 인덱스의 요소를 출력
}
return 0;
}
중첩 for문으로 접근할까 구상하다가 생각해보니 굳이 중첩할 필요가 없는 것 같아서 for문 하나만 써줬다.(정답 코드와 비슷해서 기분이 좋았다!)
대각선으로 출력한다는 것은 매번 인덱스가 가로 세로 모두 1씩 증가해야 한다는 뜻이니, 0부터 배열의 세로 크기까지 반복하는 for문 작성(가로, 세로의 크기가 똑같기 때문에 그냥 세로 크기를 구했다.) 한 후 1씩 증가하는 [i][i] 인덱스의 값이 출력되도록 작성했다.
5*5 크기의 배열에 입력 값들이 저장되는데, 이 배열의 전차 행렬(왼쪽 위에서 오른쪽 아래까지의 대각선을 기준으로 뒤집은 것)을 출력하는 것이 목적이다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
int matrix[5][5];
scanf("%d %d %d %d %d",
&matrix[0][0], &matrix[0][1], &matrix[0][2], &matrix[0][3], &matrix[0][4]);
scanf("%d %d %d %d %d",
&matrix[1][0], &matrix[1][1], &matrix[1][2], &matrix[1][3], &matrix[1][4]);
scanf("%d %d %d %d %d",
&matrix[2][0], &matrix[2][1], &matrix[2][2], &matrix[2][3], &matrix[2][4]);
scanf("%d %d %d %d %d",
&matrix[3][0], &matrix[3][1], &matrix[3][2], &matrix[3][3], &matrix[3][4]);
scanf("%d %d %d %d %d",
&matrix[4][0], &matrix[4][1], &matrix[4][2], &matrix[4][3], &matrix[4][4]);
for (int i = 0; i < 5; i++) // 25번 반복
{
for (int j = 0; j < 5; j++)
{
printf("%d ", matrix[j][i]); // [i][j] 대신 가로 세로를 바꿔서 [j][i]로 출력
}
printf("\n");
}
return 0;
}
처음에는 조금 멍청하게 answer 배열을 하나 만들어서 matrix의 [i][j] 요소를 answer의 [j][i]에 넣은 후 answer의 배열을 출력하는 식으로 작성했지만 생각해보니 그냥 matrix [j][i]의 요소를 출력하면 될 것 같아서, 이중 for문으로 주어진 가로 세로 크기에 의거해 25번 반복하게 하고 가로 세로를 바꿔서 출력했다.
포인터를 1차원 배열처럼 사용하는 방법은 <자료형> *<포인터 명> = malloc(<자료형 크기>*<배열 크기(요소 개수)>);로 하면 된다.
답 : c
2차원 배열 포인터는
<자료형> **<포인터 명> = malloc(sizeof(<자료형> *) * <세로 크기>);
for(int i = 0; i < <세로 크기>; i++)
<포인터>[i] = malloc(sizeof(<자료형> * <가로 크기>);
로 만들면 된다.
답 : b
높이 2, 세로 크기 3, 가로 크기 5인 3차원 배열을 포인터로 만들고 [1][2][4]에 100을 저장하고 출력하는 것이 목적이다. 3차원 배열이니 삼중 포인터를 베이스로 높이 -> 세로 -> 가로 메모리를 확보하고 [1][2][4]에 100을 저장하면 될 것 같다.
#include <stdio.h>
#include <stdlib.h>
int main()
{
long long ***m = malloc(sizeof(long long **) * 2);
for (int i = 0; i < 2; i++) // 높이만큼 반복
{
m[i] = malloc(sizeof(long long *) * 3);
// long long형 포인터의 주소 크기 * 세로 크기만큼 메모리를 할당하고 m[i] 주소를 저장
for (int j = 0; j < 3; j++) // 세로 크기만큼 반복
{
m[i][j] = malloc(sizeof(long long) * 5);
// 가로 크기 * 8만큼 메모리 확보 후 m[i][j]에 그 주소를 저장
}
}
m[1][2][4] = 100;
printf("%lld\n", m[1][2][4]);
for (int i = 0; i < 2; i++) // 높이만큼 반복
{
for (int j = 0; j < 3; j++) // 세로 크기만큼 반복
{
free(m[i][j]); // m[i][j]에 저장된 주소의 메모리 해제
}
free(m[i]); // m[i]에 저장된 주소의 메모리 해제
}
free(m);
return 0;
}
높이만큼 반복하며 long long형 포인터 주소의 크기 * 세로의 크기만큼의 메모리를 할당하고 그 주소를 m[i]에 넣었다. 그 다음 중첩으로 세로 크기만큼 반복하는 for문을 작성해 이 안에서는 long long형의 크기(8) * 가로의 크기만큼 메모리를 할당하고 그 주소를 m[i][j]에 저장했다.
해제도 마찬가지로 높이만큼 반복하는데 해제는 가로 -> 세로 -> 높이 배열 순으로 해야하므로 가로만큼 반복하는 중첩 for문을 바로 작성해 m[i][j]에 저장된 주소의 메모리를 해제 시켜준다. 그 후 m[i]에 저장된 주소의 메모리를 해제해줬다.
정사각행렬의 크기가 입력되면 왼쪽 위부터 오른쪽 아래까지 대각선으로 1로 채우고 나머지는 0으로 채운 상태로 출력하는 것이 목적이다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int size;
scanf("%d", &size);
// 정사각행렬의 크기를 저장하는 size 변수 선언 및 입력
for (int i = 0; i < size; i++) // size * size만큼 반복
{
for (int j = 0; j < size; j++)
{
if (i == j)
printf("%d ", 1);
else
printf("%d ", 0);
} // 대각선 위치면 1을 아니라면 0을 출력한다.
printf("\n"); // 한 행을 출력한 후 개행해준다.
}
return 0;
}
원래는 2차원 배열 포인터를 만들어 문제를 해결하는 것이 문제의 목적이었겠지만 생각해보니 굳이 2차원 배열이 필요가 없어서 최적화를 위해 사용을 하지 않았다.
입력 된 size * size만큼 중첩 for문으로 반복하면서 i와 j가 같은 경우 대각선에 해당되므로 그 경우에만 1을 출력하고 나머지의 경우에는 0을 출력한다면 대각선만 1로, 나머지는 0으로 출력된다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
int size;
scanf("%d", &size);
int **p = malloc(sizeof(int *) * size);
for (int i = 0; i < size; i++)
{
p[i] = malloc(sizeof(int) * size);
}
// 2차원 배열 포인터 생성
for (int i = 0; i < size; i++) // size*size만큼 반복
{
for (int j = 0; j < size; j++)
{
if (i == j)
p[i][j] = 1;
else
p[i][j] = 0;
// i == j라면 대각선 위치이므로 해당 위치의 값은 1로,
// i != j라면 대각선 위치가 아니므로 해당 위치의 값은 0을 저장한다.
printf("%d ", p[i][j]); // 현재 위치(인덱스)의 요소 출력
}
printf("\n"); // 행을 출력한 후 개행
}
for (int i = 0; i < size; i++)
{
free(p[i]);
}
free(p);
// 2차원 배열 포인터 메모리 해제
return 0;
}
아무래도 배우는 것이 2차원 배열 포인터이므로 2차원 배열 포인터를 이용한 풀이도 해봤다. 위 코드의 설명은 생략하겠다.
파이썬 코딩도장에서 가장 오랫동안 붙잡고 있었던 문제가 c언어 코딩도장에서도 나왔다. 지뢰찾기 규칙에 맞게 입력된 행렬에 지뢰와 주변의 지뢰의 개수를 표시하는 것이 목적이다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h> // malloc, free 함수 사용
int main()
{
int sizeY, sizeX;
scanf("%d %d", &sizeY, &sizeX);
//세로, 가로 크기를 담는 변수 선언 및 입력
sizeX++; // 문자열 입력이므로 가로의 크기 + 1
char **board = malloc(sizeof(int *) * sizeY);
for (int i = 0; i < sizeY; i++)
{
board[i] = malloc(sizeof(int) * sizeX);
}
// 세로, 가로 크기의 2차원 포인터 메모리 확보
for (int i = 0; i < sizeY; i++)
{
for (int j = 0; j < sizeX; j++)
{
scanf("%c", &board[i][j]);
}
}
// 세로 크기 * 가로 크기만큼 반복하며 요소들을 입력 받음
int count = 0; // 지뢰의 개수를 담는 변수
for (int i = 0; i < sizeY; i++)
{
for (int j = 0; j < sizeX; j++)
{ // 세로 크기 * 가로 크기만큼 반복
if (board[i][j] == '*') // board[i][j]의 값이 '*'(지뢰)이라면 그대로 *를 출력
printf("*");
else if (board[i][j] == '.') // board[i][j]의 값이 '.'(지뢰가 아니)이라면...
{
for (int a = i - 1; a <= i + 1; a++)
{
for (int b = j - 1; b <= j + 1; b++)
{ // i - 1 ~ i + 1, j - 1 ~ j + 1 범위로 반복
if (!(a < 0 || b < 0 || a >= sizeY || b >= sizeX) && board[a][b] == '*')
count++;
// 인덱스 범위에 벗어나지 않기 위해 a, b가 0보다 크고 세로, 가로 크기보다 작으며
// 해당 인덱스의 값이 '*'(지뢰)이라면 count을 +1 해줌
}
}
printf("%d", count); // 해당 위치에 count(지뢰의 개수)를 출력
count = 0; // count를 0으로 초기화
}
}
printf("\n"); // 가독성을 위해 한 줄 개행
}
for (int i = 0; i < sizeY; i++)
{
free(board[i]);
}
free(board);
// 2차원 배열 포인터 메모리 해제
return 0;
}
2차원 배열 포인터 메모리 확보는 위에서 여러 번 정리했으니 생략하고 로직 부분만 다루겠다.
예전 파이썬으로 이 문제를 풀 때는 if문으로 지뢰가 아닌 부분을 기준으로 상, 하, 좌, 우, 대각선들에 지뢰가 있는지 검사해 있으면 1씩 더해주는 식으로 풀었는데, 이번 문제를 풀 때는 더 좋은 방법이 떠올라 좀더 좋은 코드로 작성해봤다.
먼저 크기를 입력 받고 크기만큼 2차원 배열 포인터를 생성한 후 입력을 받는다. 주변 지뢰의 개수를 담을 count 변수를 선언하고 이중 for문으로 2차원 배열의 모든 요소에 접근하는데, 이 때 해당 요소가 *(지뢰)라면 그대로 *를 출력해야하므로 if문으로 *일 경우 *를 그대로 출력하도록 작성했다. 그리고 else if로 만약 해당부분이 .(지뢰가 아님)라면 주변에 지뢰가 있는지 검사하는 로직을 돌렸다.
주변 지뢰를 검사하는 로직은 간단하다. 여기서도 이중 for문을 돌리는데 범위는 첫 번째 for문은 i - 1 ~ i + 1, 두 번째는 j - 1 ~ j + 1로 설정한다. 여기서 i는 세로, j는 가로 인덱스를 나타낸다. 이렇게 작성한다면 해당 인덱스를 기준으로 상, 하, 좌, 우, 대각선의 요소에 접근할 수 있고 이 안에 if문으로 해당 요소가 지뢰라면 count + 1를 해줌으로써 상, 하, 좌, 우, 대각선에 존재하는 지뢰의 개수만큼 count를 증가시킬 수 있다. 여기서 주의해야하는 것이 i, j가 0일 경우 -1 인덱스에 접근할 수도, i, j가 가로 세로의 최대 인덱스에 해당하는 경우 인덱스 범위에 벗어나게 되는 일이 생긴다. 따라서 해당 인덱스가 지뢰인지 검사하기전에 a(i - 1 ~ i + 1), b(j - 1 ~ j + 1)가 0보다 작거나 가로, 세로와 같거나 클 경우 해당 요소를 검사하지 않도록 조건식을 작성했다. 인덱스를 벗어나는지 확인하는 조건문과 해당 요소가 지뢰인지 검사하는 조건문은 &&으로 연결함으로써 인덱스가 벗어나게 된다면 해당 요소를 검사하지 않게 된다.(&&은 앞의 식이 거짓이라면 뒤의 식을 검사하지 않는다.)
'Old (2021.01 ~ 2021.12) > Programming' 카테고리의 다른 글
c언어 코딩도장 Unit 42 ~ Unit 44 (0) | 2021.03.13 |
---|---|
c언어 코딩도장 Unit 39 ~ Unit 41 (0) | 2021.03.13 |
c언어 코딩도장 Unit 34 ~ Unit 35 (0) | 2021.03.01 |
c언어 코딩도장 Unit 24 ~ 33 (0) | 2021.02.21 |
c언어 코딩도장 Unit 12 ~ 23 (4) | 2021.02.17 |