C언어 행렬 곱 함수 - ceon-eo haenglyeol gob hamsu

 평소에 천체사진을 찍는 취미가 있어, 이미지 처리에 관심이 많다.

(아래는 직접 찍은 사진이다.)

M42, M43. 오리온 대성운이라고 부르는 영역이다. (철원)


 아직 구체적인 주제는 정하지 못했지만, "기초전자회로 실험"이라는 과목에서 이미지 또는 영상처리에 관련한 내용을 활용한 프로젝트를 진행하려 한다.

 따라서 2차원 좌표와 유사한 행렬을 C언어로 구현해보고, 행렬의 여러 연산 과정을 C언어로 구현해보고자 한다.

 

 사실 현재 진행하려 하는 방향은 거의 파이썬으로 구현해보고자 하는데, 파이썬과 같은 추상적인 언어는 개발 속도가 다른 언어에 비해 굉장히 빠르고, 라이브러리나 함수 사용이 굉장히 편리하지만, 내부적으로 어떻게 구현되어있는지 알기 어려워 오류가 발생했을 때 원인을 바로 파악하기 조금은 어려운 부분이 조금 있다.

 

 그래서 아직 학부 2학년 과정이기에, 프로그램의 low한 부분을 조금 공부하면서, 파이썬을 통해 high한 프로그래밍 기술도 쌓고싶어 C언어 학습을 하였다.

 

 

파일은 세 가지로 하였다. 프로그램의 동작상에 필수인 main함수가 들어있는 main.c, 그리고 함수를 선언해두는 헤더파일과 함수의 알고리즘을 구현해두는 파일이다.

 

 

행렬의 단순 덧셈, 뺄셈은 사람이 직접 하기에 큰 무리가 없지만,

-      역행렬, 회전변환, 전치행렬, 행렬 곱 등등

위와 같은 연산은 바로바로 해결하기 힘들기 때문에 위의 4가지 기능만이라도 구현해보고자 하였다.

 

 

//----------------------------------------------------------------------------------------------------

우선 main함수를 조금 더 유용하게 다루기 위해,

(좌 -> 우)로 습관을 들이는게 좋긴 하다...

 위 형식을 사용했다.

 간단한 프로그램에서는 아무 소용없지만, 위 기능을 사용하다보면 리눅스의 터미널을 다루듯이 윈도우에서도 cmd창을 활용하여 조금 더 자동적으로 프로그램을 다루는데에 도움이 되지 않을까 한다.

 

그러고 이제 프로그램을 구현하는데,

 

 위와 같이, cmd창에서 실행할 때나 비주얼 스튜디오 - 프로젝트 속성 디버깅 에서 수치를 바꿔 넣었을 때마다 다른 연산을 수행해줄 수 있도록 프로그램하여 switch문을 작성하였다.

 

아직 main함수를 통해 입력해준 인수를 if( ~~~) {~~~} else if(~~~) {~~~} 이런식으로 if 조건문을 통해 분기하느것이 나은지, switch문을 통해 분기하는 것이 효과적인지는 모르겠다. 이 부분에 대해서는 추가적으로 학습해야할 것 같다.

 단순한 연산이나 스쳐가는 과정일 뿐인 프로그램에서는 상관없겠지만, 매 주기마다 신호를 수신해서 처리해야하는 알고리즘과 같은 경우에는 짧은 시간이라도 단축할 수 있다면 좋기 때문에 알아두어야 할 것 같다.

 

그러고, 헤더파일을 작성하였다.

 

단순히 라이브러리를 읽어오고, 함수 선언만 한다.

 

여기서 나는 pragma once라는 지시어를 사용하긴 했지만, 여기서는 소용없는 부분이긴 하다.

 

#pragma once라는 지시어는, 한번 #include된 파일은 열지 않는다는 것이다. (MSVC++에서. 다른 컴파일러는 확인해본적이 없어서 잘 모르겠다.)

 

직관적으로 #include된 파일이 보이는 경우 상관 없지만, 3D프린터 marlin펌웨어와같이 수십, 수백개의 헤더파일을 읽어오는 소스의 경우, 중복으로 include된 파일을 읽어오는 시간을 줄여 컴파일하는 시간을 줄일 수 있다.

include guard로도 가능하다.

(사용은 해보았으나, 아무리 생각해도 그냥 중복으로 #include하지 않도록 유의하는 것이 최고일 것 같다.)

 

stdlib.h를 읽어온 이유는, mallocexit()를 사용하기 위해서이다. ( 지나고 나서의 생각이지만, calloc도 나쁘지 않을 것 같다. )

 

 

위아래로 나누어 선언한 함수에서, 위의 4가지는 행렬 곱, 회전행렬, 전치행렬, 역행렬 순서대로 연산 종류를 나누어둔 것이고, 아래의 4개 함수는 각 연산에서 계속 쓰게되는 함수이다.

 

포인터 변수를 선언할 때,

 

int *p;

이런식으로 애스터리스크를 변수에 붙여쓰는 것이 나는 변수가 어떤형태인지 더 명시적으로 되어있다고 생각하여 꼭 위와같이 하는 편인데, 비주얼스튜디오에서는 코드를

 

Int* p;

이런식으로 처리해버린다.

Vscode를 쓰자.

 

기능을 구현하는데에 쓰이는 함수이기에, utils of utils라고 작성했다.

아래쪽에 선언된 함수 중에서 행렬을 메모리 할당하기위한 함수이다.

 

아예 큰 배열을 선언해두고, 범위를 지정하여 입력받아 처리하는 방법도 있겠지만, 루프를 도는데에 있어 불편함이 생길 수 있고, 메모리 소모적인 부분에서도 전혀 효율적이지 않기 때문에 배척하도록 하자.

 

 함수가 반환하는 형이 matrix라는, 이중포인터이기 때문에 int** 함수명() 이런식으로 작성한다.

 

*** ..! 아직 포인터가 미숙하다면 포인터 배열에 대해서라도 마스터하자..!

위는, 할당한 메모리를 해제해주는 함수이다.

 행렬을 이중포인터에 담아 사용하기 때문에 반복문을 통해 해제해주는 것이 편하다. 복잡한 함수는 아니지만, 가비지 컬렉터가 동작하는 언어를 주로 쓰던 사람은 까먹을 수 있는 과정이다.

 위 과정을 빠뜨릴 경우, 메모리 누수 등의 꽤 심각한 문제가 생길 수 있으니 유의해야 한다.

자바를 씁시다

 

행렬을 반환하기에 함수의 반환형이 int** 이다!!

 위는, 이용자가 원하는 크기의 행렬을 동적할당하기 위한 함수이다.

바로 위에서 설명한, 메모리를 할당하는 함수를 이 함수 내부에서 사용한다.

이렇게 만든 함수를 만든 함수 안에서 사용하고 그러다 보니, 나중에 스파게티 소스라는 단어가 왜 생겨나는지 이해하기 시작됐다.

 

이 함수 또한 이중 포인터 형의 배열을 반환하기 때문에 int** 함수명() 과같이 선언하였다.

 

%3d를 통해 예쁘게 출력하는것을 의도했는데, 행렬 요소의 규모에 따라 더 더럽게 나올 수 있다,,

 위는, 단순히 주소를 받아, 그 주소부터 위치한 행렬을 출력해주는 함수이다.

 반복문이 겹쳐있기도 하고, 행렬을 예쁘게 출력하고싶어 따로 함수를 제작하였다.

 

 

 

 

//----------------------------------------------------------------------------------------------------

지금까지 설명했던 코드는 중복으로 사용되는 함수에 관한 내용이었고,

다음페이지부터 행렬을 연산해주는 코드를 설명해두었다.

 

 

공통적인 알고리즘은 아래와 같다.

 

 

-      연산에 필요한 배열 입력 및 메모리 할당

-      예외처리(구현하지 않은 연산도 있음)

-      연산

-      출력

-      메모리 해제

 

 

Ps. 그동안 대부분 호환을 이유로,

#define _CRT_SECURE_NO_WARNINGS를 위에 달고 scanf 함수를 사용하였는데, 이번에는 마이크로소프트가 권고하는 대로scanf_s를 사용했다.

scanf함수의 보안적인 부분에 대해 추가적으로 학습해야 할 것 같다.

 

 

l  곱행렬

두 행렬의 곱셈. m*n 과 n*m 형식의 행렬끼리만 계산이 가능하다.

행렬의 곱셈을 구현 하였다.

행렬의 scalem*n, n*m과 같이 1st row = 2nd col, 1st col = 2nd row여야 곱셈이 가능한데, 제작하고 있는 행렬 계산기를 이용하는 사람이 그걸 모르고 하지는 않을 것이라는 생각에 예외처리는 따로 하지 않았다.

 

행렬의 곱셈 과정은 아래와 같은데,

출처 : 위키피디아

다른 언어와 마찬가지로, C언어의 인덱싱이 0부터 시작되는 것을 알면 크게 어렵지 않은 과정이다.

 

 

 

 

l  행렬 회전

행렬의 회전을 구현하였다. (90, 180도만)

 

 위의 과정은 회전변환을 구현한 과정이다.

고등학교, 대학교 1학년 동안 행렬의 회전변환을 배우지 못했다. 따라서, 90도 회전과 180도 회전만 구현했다.

 

계산하려는 각도가 90도나 180도가 아니면, 무한루프를 통해 잘못 입력했음을 알리고 다시 물어보도록 하였는데,

C언어에선 try-catch를 사용할 수 없기 때문에 이렇게 구현하였다.

 

 행렬의 회전변환에 관련한 수식이 있다. 따라서 하나의 일반화된 공식에 대입하여 계산할 수 있는 것 같긴 한데, 아직 행렬에 관한 학습이 부족하여, 90도 회전과 180도 회전에 관한 연산과정을 따로 구분하였다.

 

 행렬에 관한 학습이 추가로 필요하다.

 

 

 

l  전치 행렬

전치 행렬. y = -x 그래프를 따르는 요소를 기준으로 대칭으로 바꾸어준다.

 위의 과정은 전치행렬을 구하는 과정이다.

 

전치행렬이 구현하기 가장 쉽다. 하지만 손으로 계산할 때는 꼭 숫자를 잘못 옮기는 경우가 있다.

 

원시행렬이 m*n일 때, 이의 전치행렬은 n*m이다.

출처 : 위키피디아

따라서, rowcol을 맞바꿔주는 것 만으로도 가볍게 완성할 수 있다.

 

 

 

l  역 행렬 (2*2)

역행렬. 2*2 형식의 정사각 행렬만 받고, 역행렬의 존재여부에 대해서도 예외처리를 해주었다.

 위는 역행렬을 연산하는 함수이다.

역행렬은 행렬의 scale이 바뀔수록 과정이 심각하게 복잡해지기 때문에, 2*2스케일의 정 사각 행렬만 받았다.

 

예외처리라고 해도 되는지는 모르겠지만, 위에서 했던 것처럼 2*2 정 사각 행렬이 아닌 행렬을 대입하려 할 경우 조건에 맞지 않다는 메시지를 띄우고 입력을 2*2를 대입할 때까지 무한반복 시켰다.

 

행렬을 입력 받은 후에는, ad-bc라는 판별식을 연산하여 0일 경우 역행렬이 없기 때문에 역행렬이 존재하지 않음을 표시하고, exit(1);을 통해 프로그램을 종료시켰다.

 

Return이 없는 함수의 경우 void 함수명() 이런 식으로 제작하는데, 처음에는 main함수를 종료하듯이 return 0;을 띄워두고 고민했었다. 하지만 반환값이 없다고 오류가 뜨기도 하지만, main함수가 아니므로 return 0;으로 종료되지도 않는다.

 

Exit()함수는 stdlib.h에 있는 함수이다. 넣어주는 인수로 0을 건넬경우 정상종료를 뜻하고, 0이 아닐 경우 비정상종료를 뜻한다. 의도한 바이니 0을 건넬까 고민하다가, 정상적이지 않은 계산을 의뢰하였으니 1을 건넸다.

이제 실행해보자.

 

비주얼 스튜디오의 내부에서 실행할 경우, 위의 메뉴에서 디버그 (프로젝트명)속성 구성속성 디버깅 명령 인수 의 숫자를 바꿔주면 된다.

 

여기서 전달되는 인수는 argv[1]부터 들어가기 때문에, argv[1]부터 읽어야한다.  argv[0]는 아마 실행파일의 주소를 담고있을 것이다.

그리고 숫자로 넣어주지만, 아스키코드로 넘어가기 때문에, 0의 아스키코드를 빼주어야 원하는 숫자로 넘어갈 수 있다.

 

아니면 cmd창에서 바로 넘어가는 방식도 있다. 사실 이걸 주로 사용한다.

윈도우 검색에서 cmd를 쳐보자..!

위와 같은 명령 창에서,

(핑크색을 제일 좋아한다.)

일단 위와같이 cd(change directory) 명령어를 통해 .exe 실행파일이 있는 디렉토리로 넘어간다.

그리고 dir명령어를 통해 안에 .exe파일이 있는지 확인하고,

확장자명을 포함하여 실행파일을 그대로 쳐주면 실행이 된다.

 

바로 뒤에 공백두고 숫자나 문자를 달아서 실행하면 그것은 argv[0]부터 인수로 넘어가진다.

ㄷ..돌아간다!

 

다음과 같이,  1을 넘겨주었더니, 1번으로 지정되어있던 행렬 곱의 연산 초기과정이 실행되는 모습을 볼 수 있다.

Ctrl+C를 누르면 프로그램 실행중에 탈출할 수 있다. 탈출하면 cmd창은 꺼지지 않고, 아까 cd명령어를 통해 진입했던 디렉토리로 돌아와있다.

 

 

 

 

 

 

 

//----------------------------------------------------------------------------------------------------

구현된 행렬 연산을 직접 테스트해보자.

 

l  행렬 곱

두 행렬의 곱셈.

행렬 곱의 연산이 잘 진행되는 것을 확인할 수 있다.

 

 

l  회전변환

행렬의 90도 회전.
행렬의 180도 회전.

위와같이 회전변환도 잘 구현되었음을 확인할 수 있다.

exception..!

예외처리(?)도 나름 잘 구현이 되었음을 볼 수 있다.

 

 

l  전치행렬

전치행렬의 연산.

전치행렬도 잘 구현이 되었다.

 

 

l  역행렬

역행렬... 제한이 가장 많았다.

역행렬도 잘 구현이 되었음을 볼 수 있다.

(알고리즘 연습을 위한 과정이기 때문에, 역행렬을 담는 포인터를 int로 처리해버려 값이 깔끔하고 예쁘게 나오긴 한다. 정확한 값을 얻어내려면 double로 바꾸는 것이 좋을 것 같다.)

2*2 행렬을 연산하려 할 때까지 도망치지 못합니다 휴먼.

예외처리도 잘 되어있는 모습을 보여준다.

{ (a,b), (c,b) }형식의 2*2 matrix일 때, ad-bc = 0 일 경우 역행렬은 존재하지 않는다.

ad-bc == 0, 즉 판별식이 0이어서 역행렬이 존재하지 않는 행렬의 역행렬을 구하려 한 경우, 역행렬이 존재하지 않는다고 프로그램을 끊어버리려 작성한 exit(1);이 잘 동작함을 볼 수 있다.

 

고등학교를 다니면서 C를 공부하고, 이후에 C++의 편리함에 빠져 C는 조금 잊고 살았는데, 오랜만에 다루어 보았더니 헷갈리면서도 고개를 갸우뚱 갸우뚱 하다보니 됐다.

임베디드 공부를 하면서 C++을 제일 열심히 하고있는데, 다시 느꼈지만 C++을 계속 열심히 해도 될 것 같다.

공유하기

게시글 관리

구독하기태라에몽

'C프로그래밍' 카테고리의 다른 글

[C언어] 하노이탑  (1)2020.04.16[C언어] 별 찍기 - 1  (1)2020.03.30

Toplist

최신 우편물

태그