3. 배열

Elenaljh·2023년 6월 20일
0

CS50

목록 보기
4/14

3. 배열

1. 컴파일링(Compiling)

#include <cs50.h>
#include <stdio.h>

int main(void)
{
	string name = get_string("What's your name?\n")
	printf("hello, %s\n", name)
}

이 코드를 컴파일해보자. 컴파일링은 다음 명령어를 입력하면 실행된다.
clang -o hello hello.c -lcs50
make hello
컴파일은 소스코드를 오브젝트코드(기계어)로 변환시키는 과정인데, 총 4단계로 이뤄진다.

1. 전처리 (Preprocessing)

코드 맨 위의

#include <cs50.h>
#include <stdio.h>

는 cs50, stdio 라이브러리의 헤더파일들이다. 컴파일러는 이 라이브러리에 들어가서 hello.c 코드에서 사용하는 함수 프로토타입을 복사해서 붙여넣는다. 그럼 위의 #include ~ 코드는 이렇게 바뀐다. (여전히 소스코드 형태임)

string get_string(string prompt);
int printf(string format, ...);

2. 컴파일 (Compiling)

전처리(1단계)가 완료된 코드

string get_string(string prompt);
int printf(string format, ...);

int main(void)
{
	string name = get_string("What's your name?\n")
	printf("hello, %s\n", name)
}

컴파일링 단계를 거치면 소스코드가 어셈블리어로 바뀐다.

main:									# @main
		.cfi_startproc
# BB#0:
	pushq	%rbp
.Ltmp0:
		.cfi_def_cfa_offset 16
.Ltmp1:
		.cfi_offset %rbp, -16
		movq	%rsp, %rbp
.Ltmp2:
		.cfi_def_cfa_register %rbp
		subq	$16, %rsp
		xorl		%eax, %eax
		movl	%eax, %edi
		movabsq		$.L.str, %rsi
		movb	$0, %al
		callq	get_string
		movabsq		$.L.str.1, %rdi
		movq %rax, -8(%rbp)
		...

이 어셈블리 코드에서 pushq, movq, subq, xorl 등의 언어들은 '명령어'인데, CPU가 알아들을 수 있는 언어에 가깝다.

3. 어셈블 (Assembling)

위의 어셈블리 코드를 머신코드로 바꾸는 과정이다.

0111111001010100111010101100001100000
1001001110101101110101101111001011110
1010100001111110011011010101001010000
...

위와 같은 머신코드로 바뀐다. 만약 컴파일할 파일이 하나뿐이라면 컴파일링은 이 단계에서 완료.

4. 링크 (Linking)

hello.c에는 cs50.c, stdio.c, hello.c(내가 작성한 코드)의 3가지 파일이 사용된다. (cf. stdio.c 안의 printf.c파일과 cs50.c 안의 get_string.c파일 사용)
이런 경우에는 컴파일링 과정에서 세 파일을 연결해야 하는데 이를 링킹이라고 한다. 즉, 기계어로 번역된 세 파일을 하나의 오브젝트 파일로 뭉치는 과정이다.

2. 디버깅

1. help50

주의: 이 기능은 cs50라이브러리의 기능이라 일반적으로 사용하기엔 제한적일 수 있다. 문법 오류(syntax error)에 사용한다.

int  main(void)
{
printf("Hello, world.\n");
}

이걸 컴파일하면 에러 발생. 터미널창에

help50 make buggy1

라고 치면

Asking for help...

buggy1.c:3:5: error: implicitly declaring library function 'printf' with type 'int (const char *, ...)' [-Werror,-Wimplicit-function-
declaration]
    printf("Hello, world.\n");

Did you forget to #include <stdio.h> (in which printf is declared) atop your file?

이라고 도움말이 노란색으로 뜬다.

2. printf()

논리오류 발생시 사용한다. ex. #을 10번 출력하고 싶은데 11번 출력되는 경우

#include  <stdio.h>

int  main(void)
{
	for(int i=0;i<=10;i++)
	{
		printf("i is now %i\n", i); //디버깅 장치
		printf("#\n");
	}
}

커맨드창에는 이런 출력물이 뜬다.

$ ./buggy2
i is now 0
#
i is now 1
#
i is now 2
#
i is now 3
#
i is now 4
#
i is now 5
#
i is now 6
#
i is now 7
#
i is now 8
#
i is now 9
#
i is now 10
#

이렇게 for 구문에서 i<=10i<10으로 바꿔야 함을 알 수 있다.

3. debug50

  1. ide.cs50.io에서 실행 후, 코드 옆에 중지점(빨간 점) 찍기
  2. 터미널창에 debug50 ./duggy2 입력

디버그창 나옴. 여기에서 디버그창 위쪽의 두번째 버튼 클릭하면 for문을 한단계 한단계씩 실행할 수 있고, 어디에서 잘못되었는지 알 수 있음.
디버깅 종료: Ctrl+C

3.1. 중지점

디버거는 프로그램을 특정 행에서 멈추고 한번에 한 행씩 프로그램을 실행할 수 있게 해주기 때문에 버그를 찾는데 도움이 된다. 중지점은 디버깅할 때 프로그램이 멈추는 특정 지점이다.

3.2. GDB

GDB는 자주 쓰는 디버거 중 하나임. C프로그램에서 GDB사용하려면 우선 make 파일명으로 컴파일한 후 gdb 파일명을 쳐서 디버깅을 시작함.
1. 시작

  • 컴파일: make 파일명
  • 시작: gdb 파일명
  • 정지: Ctrl+d 또는 q (quit)
  1. 중지점 설정
  • 중지점 설정: b # (# 멈추고 싶은 행 번호)
  • 현재 모든 중지점 보기: info b
  • 중지점 제거: clear [#]
  1. 프로그램 한단계씩 실행해보기
  • 프로그램 실행: r (run)
    r로 프로그램 실행시 프로그램이 중지점에서 자동으로 멈추고 프롬프트가 나타나는데 이 때 몇가지 옵션이 있음
  • 현재 지점에서 프로그램 변수값 보기: p [var] (print)
  • 현재 모든 지역 변수값 보기: info locals
  • 코드의 다음 행으로 이동: n (next)
  • 함수 내부의 각 행 훑기: s (step)
  • 프로그램 계속 실행: c (continue)
    중지점이 없다면 프로그램 종료됨. 중지점이 있다면 거기서 멈춤

3. 코드의 디자인

cs50 라이브러리에 있는 도구 설명하는 섹션

  • check50: 코드의 정확도 알림(cs50 과제 채점용) check50 hello.c
  • style50: 코드 가독성 개선하는데 도움 style50 hello.c

코드의 디자인이 중요한 이유: 나중에 유지보수할때 코드를 이해할 수 있어야 함.

cf. 고무오리: 계속 버그가 발생하고 해결할 수 없을때. 잠시 휴식을 취하고 다른거 하다가 고무오리에게 코드를 한줄씩 설명해보자.

4. 배열 (1)

1. RAM과 자료

1GB의 RAM은 10억 byte의 정보를 저장할 수 있다. 간단하게 램 하나에 10억개의 칸이 있다고 생각해보자
다음은 각 자료형의 크기이다.
|자료형|크기|
|---|---|
|bool|1 byte|
|char|1 byte|
|int|4 bytes|
|float|4 bytes|
|long|8 bytes|
|double|8 bytes|
|string|? bytes|
각 자료형은 램에서 그 크기만큼의 공간을 차치한다. ex. bool=1칸, double=8칸

2. 각 문자가 RAM에 저장되는 방법

다음 코드를 보자

char c1='H';  //규칙: char 입력할때는 작은따옴표('')를 사용한다.
char c2='I';
char c3='!';
printf("%c %c %c\n", c1, c2, c3);
-> 결과: H I !       //중간에 띄어쓰기 됨

H, I, !는 램에 숫자의 형태로 저장된다(ASCII의 특징이다. 각 문자에 숫자를 할당하는 것). H, I, !에 어떤 숫자가 할당되었는지는 다음과 같이 알 수 있다.

char c1='H';  
char c2='I';
char c3='!';
printf("%i %i %i\n", (int) c1, (int) c2, (int) c3);   //앞에 (int) 붙이는 것을 '형변형(caste)'라고 한다. 
-> 결과: 72 73 33      

cf. 형변환: 하나의 자료형을 다른 종류로 바꾸는 행위. clang은 굳이 변수이름 앞에 (int) 등을 붙이지 않아도 알아서 바꿔줌

72, 73, 33은 실제로 RAM에는 이진수의 형태로 저장된다 (01001000, 01001001, 00100001)

3. 배열 변수 기초

과제 점수의 평균을 구하는 프로그램을 작성해 보자

...
int scores[3];		//scores는 하나의 변수 안에 3개 값을 저장하는 배열변수임.
scores[0]=90;		//배열의 첫번째 값은 0번째 자리에 저장된다. 
scores[1]=70;
scores[2]=55;
printf("avg=%i\n", (scores[0]+scores[1]+scores[2])/3);

5. 배열 (2)

1. 전역변수 (Global Variable, 상수)

#include <stdio.h>

const int N = 3   //전역변수

int main(void)
{
	int scores[N];
	scores[0]=90;
	...
}
  • 전역변수 선언방법: header와 main 사이에 const를 붙여서 변수를 선언한다.
  • 특징
    - 코드 전반에 거쳐 바뀌지 않는 값이 된다. 따라서 본문을 수정할 때 실수로 값을 바꾸는 일이 생기지 않는다.
    - 관례적으로 전역변수의 이름은 대문자로 표기한다.
    - 수정할 때 본문 앞에서 찾아서 한 번만 바꾸면 되므로 유지보수가 편하다.

2. 배열의 동적 선언 및 저장

루프와 함수를 선언해 좀 더 동적인 프로그램을 작성할 수 있다.

#include  <stdio.h>
#include  <cs50.h>

// 함수 스테레오타입
float  average(int  length, int  array[]); 

// 본문: 배열에 점수 입력하고 평균내기.
int  main(void)
{
	int  n = get_int("Number of scores?");  //시험점수 갯수 입력
	int  scores[n]; //주석1
	for (int  i=0; i < n; i++)  //배열 안에 점수 입력
	{
		scores[i]=get_int("Score=");
	}
	printf("Average = %.1f\n",average(n,scores)); //주석2
}

// 평균 계산 함수 정의
float  average(int  length, int  array[]) //주석3
{
	int  sum = 0;
	for (int  i=0; i<length; i++)
	{
		sum = sum + array[i];
	}
	return (float)sum/(float)length; //주석4
}
  • 주석
    1. int scores[n]: 배열에 정수형 데이터 입력 예정이므로 꼭 int 붙여서 선언하기!
    2. 굳이 scores[n]이라고 안써도 되는듯
    3. java나 python에서는 굳이 함수에 인자를 2개 만들어서 length를 입력할 필요가 없으나 C는 length(array[])이런 기능이 없기 때문에 직접 입력해줘야 함. 또한 함수의 출력(return)의 자료형이 float이므로 함수 선언시 함수명 앞에 float 써줌.
    4. int/int=int. 따라서 average를 소수점까지 표시하려면 sum과 length를 float로 바꿔야 함. 참고로 int/float=float(더 강력한 자료형-따라서 분모와 분자 모두를 float로 바꿀 필요는 없음. 하나만 바꿔도 됨.)

2.1. 생각해보기

점수의 평균을 구하는 예제에서, 동적으로 작성한 코드는 그렇지 않은 코드에 비해 어떤 장단점이 있을까요?

  • 장점: 입력하는 데이터의 수가 많을 때, 코드 안에 하나하나 scores[0]=90, scores[1]=95,... 이런식으로 데이터를 입력하는 것보다 사용자에게 데이터를 입력받아 배열에 값을 저장하는 것이 코드가 간결하고 효율적이다.
  • 단점: 입력된 데이터가 완전히 저장되는 것은 아니므로 프로그램을 끄고 다시 켰을 때 기존에 입력받았던 데이터와 계산된 평균이 날라간다. (계산한 평균값은 새로운 리스트를 만들어 append하면 저장할 수 있다고는 한다. 잘은 모름)

6. 문자열과 배열

1. 배열

  1. 배열 선언하기
    int ages[]와 같이 배열에 저장되는 자료의 유형배열의 이름을 지정한다.
  2. ages[]의 대괄호 안에는 배열의 크기가 들어간다. 예를 들어 ages[5] 안에는 5개의 데이터가 들어갈 수 있다.
  3. 대괄호 안에 들어가는 숫자는 인덱스라고 하는데, 몇 번째 값인지를 가리킨다.
    ex. ages = {24, 34, 27, 26, 17} -> ages[3] = 27
  4. 배열의 각 값은 인덱스 숫자로 참조하기 때문에 배열을 반복문으로 돌리기 쉽다.
for(int i=0; i<5; i++)
{
	ages[i] =+ 2
}

이러면 ages의 각 나이를 2살씩 손쉽게 올릴 수 있다.

2. 문자열

문자열(String)은 char로 구성된 배열이다. 즉,
|name|E|l|e|n|a|\0|
|---|---|---|---|---|---|---|
||name[0]|name[1]|name[2]|name[3]|name[4]|name[5]|

  • string name의 Elena는 char(E, l, e, n, a, \0)으로 구성된 배열이다.
  • \0: 널 변수 (널 종단 변수). string은 길이(크기)가 정해져 있지 않은 변수이기 때문에 \0을 이용해 어디에서 string이 끝나는지 표기한다. 따라서 string의 길이는 [글자수 + 1]이 된다.

코드로 증명해보자

#include  <stdio.h>
#include  <cs50.h>

int  main(void)
{
	string  name = "Elena";
	for (int  i=0; i<5; i++)
	{
		printf("name[%i]= %c\n", i, name[i]);
	}
	printf("name[5]= %i", name[5]);
}

코드 실행 결과는 다음과 같다

$ ./string
name[0]= E
name[1]= l
name[2]= e
name[3]= n
name[4]= a
name[5]= 0

7. 문자열의 활용

1. 문자열의 길이 및 탐색

사용자로부터 문자열을 입력받아 한 글자씩 출력하는 프로그램을 만들어보자

1.1. s[i] != '\0' 여부 검사해 출력하기

#include  <stdio.h>
#include  <cs50.h>

//문자열을 출력해보자 (ver1. 각 문자가 \0인지 아닌지 검사한 후 출력)
int  main(void)
{
	string  s = get_string("Input: ");
	printf("Output: ");
	for (int  i = 0; s[i] != '\0'; i++) //\0을 한 글자로 인식하기 때문에 작은 따옴표로 감싼다.
	{
		printf("%c", s[i]);
	}
	printf("\n");
}

1.2. 배열의 길이 정보를 이용해 출력하기 (열등한 버전)

#include  <stdio.h>
#include  <cs50.h>
#include  <string.h>  //주석1

int  main(void)
{
	string  s = get_string("Input: ");
	printf("Output: ");
	for (int  i = 0; i < strlen(s); i++) //주석2
	{
		printf("%c", s[i]);
	}
	printf("\n");
}
  • 주석
    1. 문자열과 관련한 기능 제공하는 라이브러리. strlen()함수가 포함됨
    2. 함수를 호출하는데는 시간이 걸림. 반복해서 함수 호출 후 조건검사하는 것은 시간이 오래 걸린다.

1.3. 배열 길이정보를 이용해 출력하기 (발전된 버전)

#include  <stdio.h>
#include  <cs50.h>
#include  <string.h>

int  main(void)
{
	string  s = get_string("Input: ");
	printf("Output: ");
	for (int  i = 0, n = strlen(s); i < n; i++) //주석1
	{
		printf("%c", s[i]);
	}
	printf("\n");
}    
  • 주석
    1. 함수를 한번만 호출해서 n에 값 저장. 루프 돌릴때마다 함수를 호출하지 않아 시간이 적게 걸리지만 변수를 하나 더 생성하기 때문에 메모리를 더 많이 사용한다.

2. 문자열 탐색 및 수정

입력받은 문자열을 대문자로 바꿔주는 프로그램을 작성해보자.

2.1. ASCII 참고 (소문자-32=대문자)

#include  <cs50.h>
#include  <stdio.h>
#include  <string.h>

int  main(void)
{
	string  s = get_string("Before: ");
	printf("After: ");
	for (int  i = 0, n=strlen(s); i<n; i++)
	{
		if(s[i]>='a' && s[i]<='z')
		{
			printf("%c", s[i]-32);
		}
		else
		{
			printf("%c", s[i]);
		}
	}
	printf("\n");
}   

2.2. ctype 라이브러리의 toupper()함수 이용

#include  <stdio.h>
#include  <cs50.h>
#include  <ctype.h>
#include  <string.h>

int  main(void)
{
	string  s = get_string("Input: ");
	printf("Output: ");
	for (int  i=0, n=strlen(s); i<n; i++)
	{
		printf("%c", toupper(s[i]));
	}
	printf("\n");
}

8. 명령행인자 (Command-line Arguments)

1. 명령행인자란?

터미널창에 입력했던 것들을 기억할 것이다.
1. clang -o hello hello.c
2. make hello
3. help50 hello
여기에서 명령어는 clang, make, help이고, 명령행 인자는 -o hello, hello.c, hello가 된다. 즉, 명령어 뒤에 딸려서 입력되는 것들이 명령행인자다.

2. int main(int argc, string argv[])

여태까지 코드 본문은 int main(void)함수 안에 입력했었다. 이 때 void는 명령행인자가 없음을 뜻한다. 즉, 괄호 안에 인자를 넣으면 그것이 명령행 인자가 된다.
1. string argv[]: 문자열로 된 인자의 배열을 뜻함
2. int argc: argv 배열 안 인자의 갯수

이제 새로 배운 내용을 적용해보자

2.1. argv.c

#include  <stdio.h>
#include  <cs50.h>

int  main(int  argc, string  argv[]) //명령행인자 넣음
{
	if (argc == 2)		//명령행 인자를 입력한 경우
	{
		printf("hello, %s\n", argv[1]);	   //"hello, 명령행 인자" 출력
	}
	else		//명령행 인자를 입력하지 않은 경우
	{
		printf("hello, world\n");
	}
}

터미널창의 결과는 다음과 같다.

$ ./argv 지혜		 -> 명령행 인자로 '지혜'를 입력했음
hello, 지혜
$ ./argv			 -> 명령행 인자를 입력하지 않았음.
hello, world

2.2. error.c

#include  <stdio.h>
#include  <cs50.h>

int  main(int  argc, string  argv[])
{
	if (argc != 2) //주석 1
	{
		printf("missing command-line argument\n");
		return  1; //main함수가 1 반환: 뭔가 문제가 있다는 뜻
	}
	printf("hello, %s\n", argv[1]); //
	return  0; //main함수가 0 반환: 문제가 없다는 뜻
}
  • 주석
    1. 인자를 1개(이름)만 넣을텐데 왜 argc != 2인지 검사할까? (주석 2 보기)
    2. argv[1]을 출력하는 이유(또는 위에서 argc != 2인지 검사한 이유): argv[0]에는 입력한 프로그램 이름, 즉 ./error이 저장된다.
    3. int main(...)에서 앞에 int를 쓰는 이유(또는 error.c에서 return 1, return 0 입력한 이유): main()함수는 일반적으로 0을 반환하는데 이는 프로그램에 이상이 없다는 것을 의미한다. 이상이 있는 경우 main은 -1, 1, -10억 등 0이 아닌 다양한 수를 반환한다.

0개의 댓글