※ 인프런 무료강좌 C로 배우는 자료구조(권오흠 교수님)를 보고 개인적인 복습을 위해 정리한 내용입니다.


v2.0 에서는

전화번호부를 파일로 저장하고 로드하고, 알파벳 순으로 정렬할 것이다.

자료구조는 전화번호부 v1.0과 동일하다.

 

전화번호부를 파일로 로드하고 저장하기

 

load 함수

void load() {
	char fileName[BUFFER_SIZE];
	char buf1[BUFFER_SIZE];
	char buf2[BUFFER_SIZE];
	scanf("%s", fileName);
	FILE * fp = fopen(fileName, "r");
	if (fp == NULL) {
		printf("Open failed.\n");
		return;
	}
	while (fscanf(fp, "%s", buf1) != EOF){
		fscanf(fp, "%s", buf2);
		names[n] = strdup(buf1);
		numbers[n] = strdup(buf2);
		n++;
	}
	fclose(fp);	
}

fopen 함수를 이용해 읽기모드("r")로 파일을 열고, 파일타입 포인터 변수 fp에 파일을 저장한다.

if (fp==NULL) { } : 파일을 여는 것에 실패했을 경우의 예외처리를 해준다.

 

fscanf 함수는 파일의 끝에 도달하면 EOF(End of File)를 리턴한다.

fscanf가 EOF를 리턴하기 전까지 while 반복문을 돈다.

 

볼일이 없는 파일은 fclose 함수로 반드시 닫아줘야 한다.

 

save 함수

void save() {
	char fileName[BUFFER_SIZE];
	char tmp[2];
	int i;
	scanf("%s", tmp);
	scanf("%s", fileName);
	
	FILE *fp = fopen(fileName, "w");
	if(fp == NULL) {
		printf("Open failed.\n");
		return;
	}
	
	for(i=0; i<n; i++) {
		fprintf(fp, "%s %s\n", names[i], numbers[i]);
	}
	
	fclose(fp);
}

fopen 함수를 이용해 쓰기모드("w")로 파일을 열어 파일타입 포인터 변수 fp에 담는다.

 

쓰기모드로 파일을 열었을 때는

같은 이름의 파일이 존재하면, 그 파일 위에 내용을 덮어쓰게 되고

같은 이름의 파일이 존재하지 않으면 새로운 파일을 만들어 작성하게 된다.

 

데이터를 정렬된 상태로 유지하기 (알파벳 순서)

 

1) bubblesort 등의 정렬(sorting) 알고리즘 사용

새로운 데이터가 계속 추가되는 상황이므로 부적절함

(새로운 사람을 한 명 추가할 때마다 정렬 알고리즘을 사용해야하기 때문에 비효율적이다)

 

2) 새로운 데이터가 추가될 때마다 제자리를 찾아서 삽입

E를 추가하려면, 맨 뒤에서부터 검사해

E보다 큰 것들을 전부 한 칸씩 뒤로 이동한다.

E보다 작은 것이 나오거나 배열의 시작을 지나치면, 그 다음 자리에 E를 저장한다.

(처음으로 E보다 작은 것이 나오면 그 바로 뒤에 E를 삽입한다.)

 

add 함수

void add() {
	char buf1[BUFFER_SIZE];
	char buf2[BUFFER_SIZE];
	scanf("%s", buf1);
	scanf("%s", buf2);
	
	int i=n-1;
	while (i>=0 && strcmp(names[i], buf1) > 0) {
		names[i+1] = names[i];
		numbers[i+1] = numbers[i];
		i--;
	}
	names[i+1] = strdup(buf1);
	numbers[i+1] = strdup(buf2);
		
	n++;
	printf("%s was added successfully.\n", buf1);
}

i는 n-1에서 시작한다. 배열의 길이가 n일 때 마지막 자리의 인덱스는 n-1이기 때문이다.

 

strcmp 함수는 첫번째 인자가 두번째 인자보다 크면 양수를 반환하고,

첫번째 인자가 두번째 인자보다 작으면 음수를 반환한다.

이 때 크다는 것은 사전식 순서(알파벳 순서)로 더 뒤에 있다는 것이다.

 

i가 -1이 되거나(배열의 시작을 지나치거나), 나보다 작은 항목이 나오면

while문을 종료하고 바로 다음 자리에(n+1) 삽입한다.

 

remove 함수

void delete() {
	char buf[BUFFER_SIZE];
	scanf("%s", buf);
	
	int index = search(buf);
	if (index == -1) {
		printf("No person named '%s' exists.\n", buf);
		return;
	}
	
	int j = index;
	for (; j<n-1; j++) {
		names[j] = names[j+1];
		numbers[j] = numbers[j+1];
	}
	
	n--;
	printf("'%s' was deleted successfully.\n", buf);
}

 

 

search 함수는 일치하는 값을 찾아서

값이 위치하는 인덱스를 반환하고, 만약 일치하는 값이 없을 경우 -1을 반환하는 함수이다.

search 함수

더보기
void search(char *name) { /*char name[]으로 받아도 된다. */
	int i;
	for (i=0; i<n; i++) {
		if (strcmp(name, names[i])==0) {
			return i;
		}
	}
	return -1;
}

 

index가 -1인 경우 존재하지 않는 이름이라는 메세지를 출력한 후 함수를 종료한다.

index가 -1이 아닌 경우

index부터 n-1 전까지 돌면서 배열의 원소들을 앞으로 한칸씩 당겨준다.

(j<n-1인 이유 : 배열의 마지막 원소의 인덱스가 n-1일 때 그 다음 원소(인덱스 n)를 앞으로 한 칸 당겨오게 되는데, n은 빈칸이므로 n-1이 되기 전에 반복문을 종료한다.)

이렇게 하면 정렬을 유지하면서 index에 위치하는 원소만 삭제할 수 있다.

 

find 함수

void find() {
	char buf[BUFFER_SIZE];
	scanf("%s", buf);
	int index = search(buf);
	if(index == -1)
		printf("No person named '%s' exists.\n", buf);
	else 
		printf("%s\n", numbers[index]);
}

 

status 함수는 v1.0과 동일하다.

※ 인프런 무료강좌 C로 배우는 자료구조(권오흠 교수님)를 보고 개인적인 복습을 위해 정리한 내용입니다.


전화번호부 v1.0

이름, 전화번호의 리스트 저장 & 검색 기능 제공하는 전화번호부 프로그램 만들기

add john 01092419488 //새로운 사람 추가

delete john //삭제

find henry //이름으로 전화번호 검색

status //전화번호부에 저장된 모든 사람을 출력

exit //프로그램 종료

 

프로그램을 만들기 위해서는 가장 먼저 '자료구조'부터 생각해보아야 한다.

자료구조는 프로그램에서 다룰 데이터를 어디에 어떤 구조로 저장할 것인가를 말한다.

 

동일한 타입의 데이터가 여러 개 있을 때 사용할 수 있는 가장 기본적인 자료구조는 배열이다.

v1.0에서는 

데이터가 두 가지 종류(이름, 전화번호)이므로

names 배열과 numbers 배열을 만들어 데이터를 저장할 것이다.

두 배열의 인덱스를 통해 이름과 전화번호가 연결된다.

 

배열 names, numbers의 데이터 타입은 char * 이다.

두 배열에는 문자열(이름, 전화번호)을 저장할 것인데, 문자배열의 첫번째 원소의 주소를 저장하는 것이므로 포인터이고 그 주소에 담겨있는 데이터의 타입이 문자이기 때문에 char 타입이다.

*전화번호는 정수로 저장하지않고 문자열로 저장한다.

 

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

#define CAPACITY 100
#define BUFFER_SIZE

char * names[CAPACITY];
char * numbers[CAPACITY];
int n = 0; /* 전화번호부에 저장된 사람의 수 */

void add();
void find();
void status();
void remove();

int main() {
	char command[BUFFER_SIZE];
	while (1) {
		printf("$ ");
		scanf("%s", command);

		if (strcmp(command, "add")==0)
			add();
		else if (strcmp(command, "find")==0)
			find();
		else if (strcmp(command, "status")==0)
			status();
		else if (strcmp(command, "delete")==0)
			remove();
		else if (strcmp(command, "exit")==0)
			break;
	}
	return 0;
}

 

main 함수에서는

무한루프( while (1) {} )로 프롬프트 문자를 출력하고 사용자의 입력을 받는 것을 반복한다.

 

루프 안에서는

strcmp 함수(string.h 라이브러리)로 사용자의 입력값 command를

add/find/status/delete와 비교해 각각 해당되는 함수를 실행하고

사용자의 입력값이 exit인 경우 루프를 종료하며 프로그램을 종료시킨다.

 

사용자가 add명령을 입력하면 실행되는 add 함수이다. 

void add() {
	char buf1[BUFFER_SIZE];
	char buf2[BUFFER_SIZE];
	scanf("%s", buf1);
	scanf("%s", buf2);
	
	names[n] = strdup(buf1);
	numbers[n] = strdup(buf2);
	n++;
	
	printf("%s was added successfully.\n", buf1);
}

 

names[n] = buf1; 와 같이 전역변수인 names[n]에 지역변수 buf1을 직접 할당할 경우

함수가 끝났을 때 buf1이 사라지기 때문에 문제가 생길 수 있다. (buf1은 스택에 할당된 메모리(지역변수)이므로 add 함수가 return되고 나면 소멸된다. )

따라서 strdup 함수를 이용해 buf1에 저장된 문자열을 복제한 후 배열 names[n]에 복제된 배열의 주소를 저장해야 한다.

복제된 배열은 strdup 함수 내에서 malloc으로 힙(heap)에 할당된 메모리이므로, add 함수가 종료된 후에도 소멸되지 않는다.

 

이 때 strdup 함수하나의 문자열을 인자로 받아 복제한 다음 메모리에 할당하고, 메모리의 주소를 반환하는 함수이다.

char * strdup (char *s) {
	char *p;
	p = (char *)malloc(strlen(s)+1); /*+1은 null charactor 자리*/
	if (p != null)
		strcpy(p, s);
	return p;
}

/* strdup 함수 내부를 간단하게 나타낸 코드 */

 

동적 메모리 할당으로 새로운 char 타입 배열을 만들고, 그 배열의 주소를 char 타입 포인터 변수 p에 담는다.

strcpy 함수로 배열 s에 있는 문자열과 null charactor를 배열 p로 복사한다. (데이터타입이 같기 때문에 복사 가능)

배열 p를 리턴한다.

 

그 외의 함수들이다. 

find 함수

void find() {
	int i = 0;
	char buf[BUFFER_SIZE];
	scanf("%s", buf);
	
	while (i < n) {
		if(strcmp(names[i], buf)==0) {
			printf("%s %s\n", names[i], numbers[i]);
			return;
		} 
		i++;
	}
	printf("No person named '%s' exists.\n");
}

strcmp는 두 개의 문자열이 일치할 경우 0을 리턴하는 함수이다.

 

status 함수

void status() {
	int i = 0;
	while (i < n) {
		printf("%s %s\n", names[i], numbers[i]);
		i++;
	}
	printf("Total %d persons.\n", n);
}

 

remove 함수

void remove() {
	int i = 0;
	char buf[BUFFER_SIZE];
	scanf("%s", buf);
	
	for (i=0; i<n; i++) {
		names[i] = names[n-1];
		numbers[i] = numbers[n-1];
		n--;
		printf("'%s' was deleted successfully.\n", buf);
		return;
	}

	printf("No person named '%s' exists.\n", buf);
}

배열의 중간에 빈 칸이 생기지 않도록

맨 마지막 사람을 삭제된 자리로 옮기고

현재 전화번호부에 저장된 사람의 수를 1 감소시킨다(n--)

그리고 성공적으로 삭제되었다는 메세지를 출력한다.

만약 없는 이름일 경우 그런 이름을 가진 사람이 없다는 메세지를 출력한다.

 

C언어에서의 메모리 관리

 

전역변수 : 함수의 외부에 선언된 변수들

프로그램이 시작될 때 메모리가 할당되며 프로그램이 종료될 때까지 유지된다.

Data section이라고 부르는 메모리 영역에 위치한다.

 

지역변수 : 함수의 내부에 선언된 변수들

자신이 속한 함수가 호출될 때 메모리가 할당되며 return될 때 소멸된다.

스택(stack)이라고 부르는 메모리 영역에 위치한다.

 

동적 메모리 할당(dynamic memory allocation)

변수를 선언하지 않고, 아무때나 malloc 등의 함수를 호출하여 필요한 크기의 메모리를 할당할 수 있다.

동적으로 할당된 메모리는 힙(heap)이라고 부르는 영역에 위치한다.

동적으로 할당된 메모리는 명시적으로 free() 함수를 호출하여 반환하지 않는 한, 프로그램이 종료될 때까지 계속 유지된다.

 

c언어의 메모리 레이아웃

stack : 지역변수가 할당되는 영역

heap : 동적으로 할당된 메모리

data section : 전역변수가 할당되는 영역

code section : 코드(기계어)가 저장되는 영역

코드 섹션과 데이터 섹션의 크기는 프로그램이 실행되는 동안 변하지 않는다.

스택의 크기는 일정하지 않고 함수 호출과 종료에 따라 변한다.

힙의 크기도 마찬가지로 고정되어 있지 않다.

 

 

프로그램 전체 소스코드

더보기
#include <stdio.h>
#include <string.h>

#define BUFFER_SIZE 20
#define CAPACITY 100

char *names[CAPACITY];
char *numbers[CAPACITY];
int n = 0;

void add();
void find();
void delete();
void print_status();

int main() {
	char command[BUFFER_SIZE];
	while (1) {
		printf ("$ ");
		scanf("%s", command);
		if(strcmp(command, "add") == 0) 
			add();
		else if(strcmp(command, "delete") == 0)
			delete();
		else if(strcmp(command, "find") == 0)
			find();
		else if(strcmp(command, "status") == 0)
			print_status();
		else if(strcmp(command, "exit") == 0)
			break;
	}
	return 0;
}


void add() {
	char buf1[BUFFER_SIZE];
	char buf2[BUFFER_SIZE];
	scanf("%s", buf1);
	scanf("%s", buf2);
	
	names[n] = strdup(buf1);
	numbers[n] = strdup(buf2);
	n++;
	
	printf("%s was added successfully.\n", buf1);
}

void delete() {
	int i = 0;
	char buf[BUFFER_SIZE];
	scanf("%s", buf);
	
	for(i=0; i<n; i++) {
		if(strcmp(names[i], buf)==0) {
			names[i] = names[n-1];
			numbers[i] = numbers[n-1];
			n--;
			printf("'%s' was deleted successfully.\n");
			return;
		}
	}
	
	printf("No person named '%s' exists.\n");
}

void find() {
	int i = 0;
	int existence = 0;
	char buf[BUFFER_SIZE];
	scanf("%s", buf);
	
	while (i < n) {
		if(strcmp(names[i], buf)==0) {
			printf("%s %s\n", names[i], numbers[i]);
			return;
		} 
		i++;
	}
	printf("No person named '%s' exists.\n");
}

void print_status() {
	int i = 0;
	while (i < n) {
		printf("%s %s\n", names[i], numbers[i]);
		i++;
	}
	printf("total %d persons\n", n);
}

 

※ 인프런 무료강좌 C로 배우는 자료구조(권오흠 교수님)를 보고 개인적인 복습을 위해 정리한 내용입니다.


첫번째 연습문제

사용자가 입력한 문자열을 공백포함 그대로 출력하고, 문자열의 길이(공백포함 길이)도 같이 출력한다.

 

풀이 1

#include <stdion.h>
#include <string.h>

#define BUFFER_SIZE 20

int main() {
	char buffer[40];
	
	while(1) {
		printf("$ ");
		fgets(buffer, BUFFER_SIZE, stdin);
		buffer[strlen(buffer)-1] = '\0';
		printf("%s:%d\n", buffer, strlen(buffer));
}

 

"$ " : 프롬프트(prompt) 문자라고 한다. 사용자의 입력을 받겠다는 의미이다.

 

scanf 함수는 공백을 기준으로 단어를 구분하고, 단어를 하나씩만 입력받기 때문에

공백을 포함한 문자열을 입력받기 위해서는 다른 함수를 사용해야 한다.

 

gets 함수는 C 표준 라이브러리 함수로, 라인 단위로 입력을 받는다. (\n 기준으로 입력값을 구분)

라인 단위로 입력을 받기 때문에 공백을 포함한 문자열을 입력받기에 적합하다.

그러나 gets 함수는 안전하지 않다는 문제가 있다. ( gets 자체를 지원하지 않는 컴파일러도 점점 늘어나고 있다. )

gets는 무조건 문장의 끝까지 다 읽어버리기 때문에, 정해진 buffer size를 초과해서 입력을 받는 일이 일어날 수 있다.

 

이러한 gets 함수의 안전성 문제를 해결하기 위해 gets 대신 fgets 함수를 사용하는 것을 고려해볼 수 있다.

fgets는 데이터를 읽어서 저장할 배열뿐 아니라 배열의 크기도 매개변수로 받기 때문에

배열의 크기를 초과해 데이터를 읽는 경우가 없다. (메모리 위반이 일어나지 않음)

fgets(buffer, BUFFER_SIZE, stdin);

BUFFER_SIZE : 변수 buffer의 size를 매크로를 이용해 상수화한 것(프로그램을 더 안정적으로 만듦)

stdin : 세번째인자로는 파일 포인터를 넣어줘야 하는데, 키보드로 입력한 값을 받을 것이므로 표준입력파일의 파일 포인터인 stdin을 인자로 넣으면 된다. (키보드 = 표준입력파일)

(fgets 함수는 표준 입력파일 뿐만 아니라 임의의 파일로부터도 입력값을 받을 수 있는 함수이다)

 

그러나, fgets는 gets와 달리 new line charactor('\n') 까지 읽고 문자열에 포함시키기 때문에

의도하지 않은 결과를 얻게 될 수 있다.

입력받은 결과에서 '\n'을 제외하고 싶다면, 문자열의 끝에 저장된 '\n'을 '\0'(null charactor)로 바꿔주어야 한다.

buffer[strlen(buffer)-1] = '\0';

 

그 외에도 문제가 되는 부분이 있는데,

fgets 함수는 정해진 사이즈를 초과하는 부분의 문자열은 읽어들이지 않고 넘어간 다음,

다음 순서에 나머지 문자열을 마저 읽어들인다는 것이다.

이는 예제에서 원하는 결과가 아니다.

 

이런식으로 c언어는 다양한 표준 입력 함수를 제공하고, 함수마다 조금씩 차이가 있는데

그런 차이들을 일일히 다 숙지하고 의도에 맞는 적절한 함수를 선택하기가

어려울 수 있다.

그래서 라인 단위로 입력을 받거나 할 때, 그냥 직접 함수를 만들어서 사용하는 경우가 많다.

상황에 따라 조금씩 원하는 바가 다르므로 표준 입력함수를 쓰는 대신 원하는 것을 잘 반영하는 함수를 직접 만들어 사용하는 것도 괜찮은 방법이라고 볼 수 있다.

 

직접 만든 함수 예시

int read_line( char str[], int limit) {
	int ch, i = 0;

	while ((ch = getchar() != '\n')
		if (i < limit)
			str[i++] = ch;

	str[i] = '\0';
	return i;

}

 

 

풀이 2

직접 만든 함수 활용해서 예제 코드 수정하기

#include <stdion.h>
#include <string.h>

#define BUFFER_SIZE 100

int read_line(char str[], int limit);


int main() {
	char buffer[BUFFER_SIZE];
	int len;
	while(1) {
		printf("$ ");
		len = read_line(buffer, BUFFER_SIZE);
		printf("%s:%d\n", buffer, len);
	}

	return 0;
}


int read_line(char str[], int limit) {
	int ch, i = 0;

	while ((ch = getchar()) != '\n')
		if (i < limit)
			str[i++] = ch;

	str[i] = '\0';
	return i;

}

 

read_line 함수는 배열과 배열의 최대 크기를 매개변수로 받는다.

 

while 반복문에서 getchar 함수(표준라이브러리함수)는

한 글자씩 읽어서 ch에 담고, ch에 담긴 문자가 '\n'이 아닌 경우 while반복문이 실행된다.

 

while 반복문에서는 i가 limit를 초과하지 않을 경우,

ch에 담긴 문자를 배열 str의 원소(str[i])에 저장하고,

i를 1증가시키는(i++) 과정을 반복한다.

 

두번째 연습문제

첫번째 연습문제와 달리 사용자가 입력한 문장을 공백 포함 그대로 출력하지 않고,

불필요한 공백을 제거하고 출력한다.

문장의 앞뒤에 있는 공백을 제거하고, 단어 사이 두 개 이상의 연속된 공백은 하나의 공백 문자로 대체하라.

 

풀이

사용자가 입력한 문장을 라인 단위로 통째로 가져오지 않고,

한 문자씩 읽어서 가져오는 과정에서 불필요한 공백들은 저장하지 않고 필요한 것들만 문자 배열에 저장하여 출력하는 방식으로 풀 것이다.

맨 앞의 공백들을 건너 뛰고, 공백이 아닌 문자부터 저장하기 시작한다.

문자를 하나씩 저장할 때마다 인덱스 변수 i 값은 1씩 증가시킨다.

문자 다음에 오는 첫번째 공백은 저장하고, 그 다음 공백들은 건너뛴다.

 

코드 보기

#include <ctype.h> //isspace 함수 제공 라이브러리

int main() {

	char line[80];
	while(1) {
		printf("$ ");
		int length = read_line_with_compression(line, 80);
		printf("%s:%d\n", line, length);
	}
	
	return 0;
}


int read_line_with_compression(char compressed[], int limit) {
	int ch, i = 0;
	while ((ch = getchar() != '\n') {
		if (i < limit-1 && (!isspace(ch)) || (i > 0 && !isspace(compressed[i-1])))
			compressed[i++] = ch;
	}

	if (i > 0 && isspace(compressed[i-1]))
		i--;
	compressed[i] = '\0';2
			
	}

read_line_with_compression 함수

 

1) isspace 함수

white space(공백/탭) 문자인지 검사하는 함수, ctype.h 라이브러리에서 제공

!isspace(ch) : white space 문자인지 체크 (공백, 탭 체크)

ch ! = " " : 공백인지 체크

 

2) while문 내 if문 조건 (한글자씩 검사하며 불필요한 공백인지 체크)

i < limit-1 && (!isspace(ch)) || (i > 0 && !isspace(compressed[i-1]))

우선 i가 limit-1 미만이어야 한다. (마지막 문자(compressed[limit-1])로는 '\0'이 들어가야하기 때문)

그리고 ch가 공백이 아닐 때 while문을 실행한다.

혹은 ch가 공백이지만 이전 문자가 공백이 아니고(!isspace(compressed[i-1])), 첫번째 칸이 아닐 때(i>0)에도 while문을 실행한다.

 

3) while문 실행 (불필요한 공백이 아니라면 저장)

compressed[i]ch를 저장하고, i값을 1 증가시킨다(i++)

 

4) while문 종료 후 if문 실행 (마지막 문자가 공백인지 확인)

ch가 '\n'일 때 while문이 종료된다.

이 때 '\n'직전에 저장한 문자 compressed[i-1]이 공백인 경우, 한 칸 앞당겨서(i--)

'\0'을 저장한다.

compressed[i] = '\0';

※ 인프런 무료강좌 C로 배우는 자료구조(권오흠 교수님)를 보고 개인적인 복습을 위해 정리한 내용입니다.


 

문자열

C언어에서 문자열은 char 타입의 배열에 저장된다. 

 

문자열의 끝에는 null charactor ('\0') 를 저장해 문자열의 끝을 표시해주어야 한다. 

컴퓨터에게 문자열의 끝이 어디인지 알려주지 않으면 에러가 발생한다. 

이렇게 null charactor를 문자열 끝에 저장해 문자열의 끝을 표시하는 것은 C언어 자체의 문법은 아니지만, 

C언어의 많은 표준 라이브러리 함수들이 문자열을 다룰 때 문자열의 끝에 null charactor가 저장되어있다는 가정 하에서 동작한다.

그래서 C언어를 사용할 때는 모든 문자열의 끝에 null charactor를 추가해 문자열의 끝을 표시하는 것이 바람직하다. 

 

null charactor를 담기 위해 배열의 크기는 문자열의 길이보다 적어도 1만큼 길어야 한다. 

(null charactor를 사용하는 대신 배열의 크기를 문자열의 길이에 딱 맞게 설정하면 되지 않을까 하는 생각을 해볼 수 있지만, 그렇게 하면 컴파일러에 따라 오류가 발생할 수 있다. )

 

 

문자열 생성 방법

1. 기본적인 생성 방법

char str[6];

str[0] = 'h';
str[1] = 'e';
str[2] = 'l';
str[3] = 'l';
str[4] = 'o';
str[5] = '\0';

 

2. " " 연산자를 이용한 생성

char str[] = "hello"; //(1)

char *str = "hello"; //(2)

 

" " 연산자를 이용해 문자열을 생성하면, 

C 컴파일러가 자동으로 문자의 갯수를 카운팅해서 문자 갯수 + 1(null charactor) 크기의 배열을 생성하고,

문자를 하나씩 저장한 다음 마지막 원소로 null charactor를 저장한다. 

그리고 배열의 첫번째 원소의 주소를  (1) str[] 변수 또는  (2) *str 변수에 담는다. (둘 다 포인터 변수이다.)

 

(1)과 (2)의 생성 결과는 동일하지만

(1)의 경우, 배열이름 포인터 변수인 str[] 변수는 값을 바꿀 수가 없다. (배열 이름 포인터 변수는 다른 포인터 변수와 달리 값을 바꿀 수가 없다. 원래의 문자열이 아닌 다른 문자열을 가리킬 수가 없다는 뜻이다.)

그리고 (2)로 생성한 문자열의 경우, 배열이 아닌 string literal이므로 다른 생성 방법들과 달리 문자열 수정이 불가능하다. 

 

※ string.h 라이브러리

문자열을 다루는 함수를 제공하는 라이브러리이다.

strcpy : 문자열 복사 

strlen : 문자열의 길이

strcat : 문자열 합치기

strcmp : 문자열 비교

 

scanf 함수

int a;

scanf("%d", &a);

scanf 함수를 사용할 때는 변수 a가 아닌, 변수 a의 주소(&a)로 입력받은 값을 넘겨준다.

scanf 함수의 매개변수로는 자신이 읽은 데이터를 저장할 메모리의 주소를 넘겨주어야 한다. 

(변수에 값을 할당하는 치환문은 (변수의 주소 = 값) 형태이므로 

변수에 값을 할당할 때는 값을 변수의 '주소'로 넘겨주어야 하기 때문이다. )

 

char s[10]; 

scanf("%s", s); 

그러나 문자열을 입력받아 넘겨주는 경우에는 & 연산자를 사용하지 않는다.

문자열은 char 타입의 배열이고, 배열의 이름은 그 배열의 주소를 저장하고 있는 포인터 변수이기 때문에

변수의 주소를 추출하는 & 연산자를 사용하지 않는 것이다. 

 

 

문자열 예제 - 문자열 저장하기

사용자로부터 여러 개의 단어를 입력받아 저장하는 예제이다. (자세한 내용은 강의 참고)

 

여러 개의 단어(=여러 개의 문자열 배열)를 입력받고 각 배열들의 주소를 저장할 배열이 필요하므로

char *words[100];

로 선언해주면 된다. 

입력되는 단어의 최대 개수는 100개 정도로 생각하고 배열의 크기를 설정했다. 

배열의 타입이 char * 인 이유는,  char 타입 원소를 담고 있는 배열들의 주소를 저장할 것이기 때문이다.

 

첫 번째 시도 (실패)

#define BUFFER_SIZE 100

int main() {
	char *words[100];
    int n = 0; // number of strings. 현재까지 저장된 단어의 갯수
    char buffer[BUFFER_SIZE];
    
    //EOF : End of File. 파일이 끝나기 전까지 while문을 반복한다
    while (scanf("%s", buffer) != EOF) {
    	words[n] = buffer;
    	n++;
    }
    
    return 0;
}

이렇게 코드를 작성하면

words 배열의 모든 원소들이 맨 마지막으로 입력했던 단어로 덮어씌워진 상태가 된다. 

words[n] = buffer;

이 부분 때문이다. 

위 코드는 두 개의 포인터 변수 간의 치환문이다. (words[n]에는 배열의 주소가 저장되고, buffer는 배열의 이름이므로 마찬가지로 배열의 주소가 저장되는 포인터 변수.)

이 치환문에 따라 buffer라는 포인터 변수가 가진 배열의 주소가 words[n]이라는 포인터 변수에 저장이 된다.

buffer는 배열의 이름 포인터 변수이므로 값을 변경할 수가 없다. 즉, 다른 배열을 가리킬 수가 없다.

그래서 words의 모든 원소들은 buffer라는 하나의 배열의 주소를 담게 된다. 

words 배열의 각 칸마다 다른 배열들이 할당되는 것이 아니라, 모든 칸에 같은 buffer 배열이 할당되기 때문에

scanf로 새로운 문자 배열(문자열)을 읽어올 때마다 원래 있던 문자열이 지워지고 새로운 문자열로 덮어씌워진다.

 

이 문제를 해결하기 위해서는

배열의 주소를 할당하는 것이 아니라, 배열에 담긴 값을 복사해서 전달해야 한다.

문자열을 카피하려면 strcpy 함수나 strdup 함수를 이용하면 된다. (string.h 라이브러리 함수)

 

strcpy와 strdup

더보기

strcpy(str2, str1)

str1을 복사해 str2에 저장한다. 이 때 str1과 str2의 타입이 같아야 한다. (배열 <- 배열, 정수 <- 정수)

예제에서 strcpy(words[n], buffer); 처럼 작성하면 buffer는 문자배열인 반면 words[n]은 문자배열의 주소를 담는 포인터 변수이기 때문에 타입이 달라 에러가 발생한다.

 

strdup(buffer)

매개변수로 하나의 문자열을 받아서 복제하고, 복제한 문자열의 주소를 반환한다.

C표준 라이브러리 함수는 아니지만 대부분의 컴파일러가 지원하는 함수이다. (호환성 측면에서 약간 문제가 있을 수 있다)

 

strdup 함수의 내부를 간단하게 나타낸 코드

char * strdup(char *s) {
	char *p;
	p = (char *)malloc(strlen(s)+1);

	if (p ! = NULL)
		strcpy(p, s);
        
	return p;
}

strlen 함수로 전달받은 문자열 s의 길이를 구하고,

포인터 변수 p에 (문자열의 길이+1) 만큼의 크기를 가진 메모리를 동적으로 할당한다.

strcpy 함수로 문자열을 복제해서 p가 가리키는 주소에 담는다. 

p를 반환한다. 

 

다음과 같이 strdup 함수를 이용하는 방식으로 코드를 수정하면 의도한 결과를 얻을 수 있다.

#include <stdio.h>
#define BUFFER_SIZE 100

int main() {
	int n = 0;
    char buffer[BUFFER_SIZE];
    
    while (scanf("%s", buffer) != EOF) {
    	words[n] = strdup(buffer);
        n++;
    }


}

strdup 함수는 buffer에 담긴 문자열을 동적으로 할당된 메모리에 복제하고, 문자열이 복제되어있는 메모리의 주소를 반환하여 

words[n]에 담는다. 그러면 우리가 원하던 대로 words 배열의 각 원소 자리에 서로 다른 메모리에 할당되어있는 문자열들의 주소가 담긴다. 

또한 strdup 함수는 내부적으로 문자열의 길이(+1)에 딱 맞는 크기의 메모리를 할당하기 때문에

길이가 다른 여러 개의 단어들을 각 단어에 맞는 크기의 배열에 담을 수가 있다. 

 

 

문자열을 파일로부터 입력받아 저장하기

#include <stdio.h>

void main() {
	FILE * fp = fopen("input.txt", "r");
    char buffer[100];
    
    while (fscanf(fp, "%s", buffer) != EOF)
    	printf("%s ", buffer);
        
    fclose(fp);
}

문자열을 키보드에서 입력받지 않고 파일을 불러와 입력받고 저장하는 예제이다.

 

파일 읽기

FILE *fp = fopen("input.txt", "r") //읽기 모드로 파일 불러와 파일타입 포인터 변수에 저장

fclose(fp); //파일 닫기

 

파일 쓰기

FILE *fp = fopen("output.txt", "w");//쓰기 모드로 파일 불러와 파일타입 포인터 변수에 저장

fclose(fp); //파일 닫기

 

※ 인프런 무료강좌 C로 배우는 자료구조(권오흠 교수님)를 보고 개인적인 복습을 위해 정리한 내용입니다.


포인터 변수

컴퓨터의 메모리(RAM)는 데이터를 보관하는 장소이다.

메모리에는 바이트(8 bits) 단위로 주소가 지정되며, 모든 변수는 주소를 가진다.

예를 들어 정수형 변수 int sum 1004번지~1007번지까지 4바이트의 메모리가 할당되어있다면,

sum의 주소는 1004번지이다. 

 

포인터는 이러한 메모리 주소를 값으로 가지는 변수이다. 

다음과 같이 포인터 변수를 선언할 수 있다. 

type-name * variable-name;

variable-name : 선언된 포인터 변수의 이름

* : variable-name이 포인터 변수임을 표시하는 기호. 

type-name : 포인터 변수 variable-name에 저장될 주소에 저장될 데이터의 유형을 지정

 

다음의 연산자들을 포인터 변수와 함께 사용할 수 있다. 

연산자 & : 변수로부터 그 변수의 주소를 추출하는 연산자 (주소 추출)

연산자 * : 포인터 변수에 담겨있는 주소에 있는 변수를 가져오는 연산자 (값 추출)

 

포인터 변수를 선언할 때 변수명 앞에 있는 * 기호는 이 변수가 포인터 변수라는 의미로, * 연산자와는 다르다. 

 

※ 치환문

더보기

대입연산자를 사용해 치환문을 작성할 때, 좌항은 주소이고 우항은 값이다. 

우항에 있는 값이나 우항에 있는 변수의 값을 

좌항에 있는 변수의 '주소'에 저장한다. 

 

예시 1)

y = 1;

변수 y의 '주소'에 1이라는 '값'을 담아라.

 

예시 2)

*ip = 0;

포인터 변수 ip에 담긴 주소에 있는 변수 x의 '주소'에 0이라는 '값'을 담아라.

 

포인터와 배열

배열의 이름은 배열의 시작 주소(배열의 첫번째 원소의 주소)를 저장하는 포인터 변수로,

다른 포인터 변수와 달리 그 값을 변경할 수 없다.

 

int a[10];

위와 같이 배열을 선언하면,

메모리에는 a라는 변수의 공간이 할당되고

배열의 원소인 a[0], a[1], ..., a[9]의 공간이 연속적으로 할당된다.

이 때 a(배열의 이름)에는 배열의 시작주소가 저장되고, a[0], a[1], ..., a[9] 에는 배열의 각 원소에 담긴 값이 저장된다.

 

함수에서 배열을 매개변수로 받을 때,

int array[]int *array 는 둘 다 배열의 첫번째 원소 주소를 담은 포인터 변수이므로 완전히 같다.

(int array로 받을 경우 배열이 아니라 정수형 변수를 매개로 받는 것이기 때문에, 그 자리에 배열을 전달하면 에러가 발생한다. 배열의 자료형은 int array[]와 같이 []를 붙여서 자료형이 배열임을 알려줘야 한다. )

 

C언어에서는 "배열의 이름이 포인터 변수"라는 것이 많이 활용되므로 이것을 이해해야

C언어를 잘 이해하고 다룰 수 있다.

 

포인터 arithmetic

int a[10]; 을 선언했을 때

*a와 a[0]은 동일한 의미이다(*a == a[0]). 배열의 이름 a가 배열 첫번째 원소의 주소이고, * 연산자는 주소에 저장된 값을 가리키는 연산자이기 때문이다. 

 

포인터로 더하기 연산(+ n)을 할 때는 n이 더해지는 것이 아니라, n x 자료형의 크기만큼이 더해진다. 

예를 들어 int *a이고 a가 1000일 때 a++를 하면, 그 결과는 1001이 아니라 1004가 된다. (정수형 변수의 크기는 보통 4바이트)

이 때, 배열의 원소들은 메모리에서 자료형의 크기만큼 공간을 차지하면서 연속적으로 할당되어 있기 때문에

첫번째 원소의 주소에 4(1x4바이트)가 더해지면 두번째 원소의 주소가 된다. 

두번째 원소의 인덱스는 1이기 때문에, *(a+1) == a[1] 이 된다.

다른 원소들에도 마찬가지로 적용할 수 있기 때문에 *(a+i) == a[i] 이다.

 

동적 메모리 할당 (Dynamic Memory Allocation)

변수를 선언하는 대신, 메모리를 직접 요청해 할당받아서 데이터를 저장하는 것.

 

malloc 함수(비슷한 다른 함수들도 있다)를 호출해 동적 메모리 할당을 요청하면, 

요청받은 크기의 메모리를 할당하고 그 메모리의 시작 주소를 반환한다.

주소값을 반환하므로, 포인터 변수에 반환값을 담아서 사용하면 된다. 

이 때 malloc이 반환하는 주소는 타입이 없는 주소(void pointer, void *) 이다.

그 공간에 어떤 타입의 값이 저장될지 알 수 없기 때문이다.

 

만약 정수를 저장하고 싶다면 다음과 같이 int형 포인터로 변환해 할당하면 된다. 

int *p;

p = (int *)malloc(10*sizeof(int));

if (p==NULL) {
	/* 동적 메모리 할당이 실패할 경우, 예외처리를 해준다 */
}

malloc 함수의 인자로는 할당할 메모리의 크기가 들어가게 되는데,

코드의 호환성을 위해 직접 값을 입력하기보다는 (예 : malloc(40) ) 위 코드에서처럼 sizeof를 이용하는 것이 좋다. 

 

malloc으로 할당받은 메모리는 이렇게 배열처럼 사용할 수 있다.

p[0] = 12;

p[1] = 24;

*(p+2) = 36;

 

배열 키우기

동적으로 할당된 배열은 크기가 부족한 경우 더 큰 배열로 교체할 수가 있다.

원래 배열의 크기를 키운다기보다는

메모리의 다른 곳에 크기가 더 큰 새로운 배열을 할당받은 다음, 원래 배열에 있던 데이터를 그곳으로 복사해 옮기는 것이라고 볼 수 있다. 

int *array = (int )malloc(4**sizeof(int));

array[0] = 1;

array[1] = 2;

*(array+2) = 3;


int *tmp = (int* )malloc(8*sizeof(int));

int i;


for (i=0; i<4; i++)

tmp[i] = array[i];

array = tmp;

 

동적 메모리 할당을 하는 대신 int array[4]; 처럼 일반적인 방법으로 배열을 선언했을 경우

앞에서 말한 것처럼 배열의 이름이 배열의 시작주소를 저장하는 포인터 변수가 된다.

그런데 배열이름 포인터 변수는 다른 포인터 변수와 달리 그 값을 변경할 수가 없다.

그래서 array = tmp; 부분에서 에러가 발생한다. 

 

반면에 동적 메모리 할당을 하면 포인터 변수 array의 값을 수정할 수 있기 때문에 배열의 크기를 키우는 것이 가능해진다.

단, 동적 메모리 할당을 이용해 배열의 크기를 키울 경우 원래 배열이 할당되어있던 메모리가 garbage가 되는데

C언어는 garbage를 자동으로 처리해주지 않기 때문에 메모리의 효율적인 관리에 조금 문제가 생길 수 있다.

 

C언어 표준 라이브러리 파일

studio.h : printf, scanf 등 입출력 함수

stdlib.h : srand, rand 함수

time.h : time 함수

 

 

난수 만들기

srand 함수 : 호출할 때 전달받는 인자를 기반으로 해서 난수를 초기화 시킴

rand 함수 : srand로 생성된 값을 바탕으로 난수를 생성함

time 함수 : 인자값으로 NULL을 넘기면 1970년 1월 1일 0시(UTC 타임존)이후부터 인자값까지 현재까지 흐른 초 수를 리턴한다.

 

난수 생성 코드

#include <studio.h>
#include <stdlib.h>
#include <time.h>

int main() {
	srand(time(NULL)); //시간을 기준으로 난수 초기화
	int random = rand(); //rand함수 호출해 난수 생성, 변수에 담기
	printf("%d", random); //난수 출력
	
	return 0;
}

 

rand() % n : 0~n-1 범위의 난수 생성

 

 

주사위 게임 만들어보기

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

void checkScore(int mScore, int cScore) {
  printf("\n-------------------------");
  printf("\n현재 각 플레이어의 점수입니다.");
  printf("\n플레이어의 점수 : %d", mScore);
  printf("\n컴퓨터의 점수 : %d", cScore);
  printf("\n-------------------------");
}

void showEnding(int mScore, int cScore) {
	if(cScore > mScore) {
		printf("\n컴퓨터의 승리입니다.");
	} else if(cScore == mScore) {
		printf("\n무승부입니다.");
	} else {
		printf("\n플레이어의 승리입니다.");
	}
}

int main() {
  int diceCount = 0;
  int diceEnd = 3;
  int mScore = 0;
  int cScore = 0;
  int selection;
  
  printf("게임 설명 : 주사위를 총 3 번 굴려 합친 주사위 눈의 합이 더 높은 사람이 이기게 됩니다.");
  printf("\n주사위 게임을 시작하겠습니다.");
  printf("\n------------------------");
  
  while(diceCount < diceEnd || selection == 0) {
    printf("\n주사위를 굴려주세요. 1번 주사위 굴리기 2번 현재 눈의 합 확인하기 : ");
    scanf("%d", &selection);
    
    if(selection == 1) {
      srand(time(NULL));
			mScore += (rand() % 6)+1;
			cScore += (rand() % 6)+1;
			diceCount++;
    } else if(selection == 2) {
      checkScore(mScore, cScore);
    } else {
      printf("\n잘못 입력하셨습니다. 다시 입력해주세요.");
			selection = 0;
    }
  }
	
	checkScore(mScore, cScore);
	showEnding(mScore, cScore);
	
  return 0;
}

 

1. 필요한 변수 선언하고 초기화하기

플레이어의 점수를 담을 mScore 변수, 컴퓨터의 점수를 담을 cScore 변수는 0으로 초기화한다.

주사위를 굴린 횟수를 저장할 diceCount 변수도 0에서 시작한다.

주사위를 세 번 굴리면 프로그램이 끝나야 하므로 diceEnd 변수의 값은 3으로 초기화했다.

(diceCount == diceEnd가 되면 프로그램을 종료)

사용자의 선택(1, 2, 혹은 그 외의 입력)을 저장할 selection 변수도 선언했다.

 

2. 프로그램 실행 과정

1) 안내문 출력

main 함수가 시작되면(프로그램이 시작되면),

먼저 게임안내문을 출력한다.

 

2) while 반복문 실행

그런 다음 diceCount의 값이 diceEnd보다 작고, selection의 값이 0이라면 while 반복문을 실행한다.

diceCount 변수는 0으로, diceEnd 변수는 3으로 초기화한 상태이므로

첫번째 조건은 만족한다.

selection 변수는 선언만 하고 초기화는 하지 않았으므로 0(null 값)이 담겨있다. 따라서 두번째 조건도 만족하여

while 반복문이 실행된다.

 

3) 사용자로부터 값 입력받아 변수에 담기

while 반복문이 실행되면, 먼저 사용자가 1번과 2번 둘 중 하나의 값을 입력하도록 한다.

사용자가 입력한 값을 selection 변수에 담는다.

 

4) if문(조건문)으로 사용자가 입력한 값 확인

사용자가 입력한 값을 확인하여

1을 입력했다면(selection == 1), 1과 6 사이의 난수를 두 개 생성해

각각 cScore 변수, mScore 변수 값에 더한다.

주사위를 한 번 굴린 것이므로 diceCount는 1 증가시켜준다.

2를 입력했다면(selection == 2), checkScore 함수를 실행현재 mScore와 cScore의 값을 출력한다.

1이나 2가 아닌 다른 값을 입력했다면

"잘못 입력하셨습니다. 다시 입력해주세요."를 출력하고

selection에는 0을 담아 while문이 다시 실행되도록 한다.

 

5) 주사위를 다 굴린 후 결과 보여주기

주사위 세 번을 다 굴리면(diceCount == diceEnd가 되면) 더 이상 while문의 조건을 만족하지 못하므로 while문 밖으로 빠져나와

checkScore 함수와 showEnding 함수를 실행한다.

checkScore 함수로 현재 각 플레이어의 총점을 보여주고,

showEnding 함수로 누구의 승리인지(혹은 무승부인지) 알려준다.

구조체를 사용하면 자료형이 다른 여러 개의 변수를 편리하게 사용하고 관리할 수 있다.

사용자가 직접 자료형(사용자 정의 자료형)을 만들어 하나 이상의 변수를 묶어서 관리한다.

ex) 회원정보 저장, 관리 - 아이디(문자열), 나이(숫자), 전화번호(숫자) 등

 

구조체 정의하고 사용하기

구조체는 보통 main 함수 전에 선언한다. (main 함수 안에 선언하면 main 함수 안에서만 사용할 수 있다.)

구조체 안의 변수들은 '구조체 멤버'라고 한다.

구조체 선언만으로는 바로 사용할 수 없고(자료형만 선언한 것이기 때문),

main 함수 안에서 선언한 구조체 자료형을 가진 변수를 선언한 후 사용할 수 있다.

 

구조체 선언하기

struct 구조체이름 { 구조체 멤버들 }; 형태로 선언하고,

main 함수에서 struct 구조체이름 변수이름; 과 같이 선언한 후 사용한다.

struct student {
	char name[15];
	int student_id;
	int age;
	char phone_number[14];
}; //구조체 선언

int main(){
	struct student goorm; 
	//자료형이 student 구조체인 goorm 변수 선언

	scanf("%s", goorm.name);
	scanf("%d", goorm.age);
	//변수이름.구조체멤버이름을 통해 구조체멤버에 접근할 수 있다

	return 0;
}

구조체 단위로 묶어서 처리하면 관리가 편하고 가독성도 높일 수 있다.

 

구조체 멤버의 초기화

main에서 구조체를 선언하면서 구조체 멤버에 값을 대입해 초기화할 수 있다.

멤버 연산자. 와 중괄호{}를 사용한다.

int main(){
	struct student codigm = { .age = 20, .phone_number = "010-1234-5678" }; 
	//( .멤버이름 = 값 }으로 초기화
	struct student goorm = { "구름이", 2020110049, 21, "010-4567-8910" };
	/*
	멤버이름을 적지 않고 초기화
	(구조체 정의했던 순서대로 값이 들어감. 값을 넣어주지 않은 멤버는 0으로 초기화 됨.)
	*/
	return 0;
}

 

typedef를 이용한 구조체 선언

위에서 언급한 선언방법 외에도,

typedef 키워드를 사용해 구조체를 편리하게 선언할 수 있다. typedef를 사용하면 main 함수에서 구조체를 선언할 때 매번 struct를 써줄 필요가 없고 구조체 이름도 생략할 수 있다.

typedef는 C언어에서 자료형 이름을 새롭게 붙일 때 쓰는 키워드로,

typedef를 사용할 때에는 구조체 별칭이 필요하다.

구조체 별칭은 구조체를 정의할 때 중괄호 뒤에 써주면 된다.

 

구조체 이름을 생략하지 않는 경우

구조체 이름과 별칭을 둘 다 쓸 때는 일반적으로 구조체 이름 앞에 _를 붙인다.

typedef struct _Student {
	int age;
	char phone_number[14];
} Student;

int main(){
	Student goorm;

	return 0;
}

 

구조체 이름을 생략하는 경우 (익명 구조체)

typedef struct {
	int age;
	char phone_number[14];
} Student;

int main(){
	Student goorm;
	
	return 0;
}

구조체 이름을 생략하고 별칭만 사용하는 익명 구조체로 선언할 수도 있다.

익명 구조체도 다른 구조체와 문법은 같으므로 멤버에 접근할 때는 멤버 연산자. 을 이용하면 된다.

 

구조체 배열

구조체도 다른 자료형처럼 배열에 담아 관리할 수 있다.

구조체 배열은 일반 배열처럼 선언하면 된다.

 

구조체 배열의 선언 및 초기화


typeof struct {
	char name[30];
	int age;
} Student;

int main(){
	Student goorm[3] = { {.name = "Harry"}, {.name = "Hermione"}, {.name = "ron"} };
	
	goorm[0].age = 10;
	goorm[1].age = 10;
	goorm[2].age = 10;

	return 0;
}

구조체도 문자열은 선언과 동시에 초기화해야 한다. 정수인 나이는 선언 후 값을 대입할 수 있다.

 

구조체 포인터

구조체 포인터 : 구조체를 가리키는 포인터 변수

struct student *p;

struct student 부분이 자료형이나 마찬가지이기 때문에 위와 같이 선언해준다.

이 때 p는 구조체를 가리키는 포인터이고, 구조체가 아니다. 헷갈리지 말 것

 

구조체 포인터 선언하기

typedef struct {
	int s_id;
	int age;
} Student;

int main(){
	Student goorm;
	Student *p;
	
	p = &goorm;

	(*p).s_id = 20200105; //괄호 사용
	p->age = 20; //화살표 기호 사용

	return 0;
}

포인터를 사용할 때는 *p.age처럼 바로 사용하는 것이 아니라

(*p).age와 같이 괄호를 사용해야 한다.

괄호를 사용하지 않으면

멤버 연산자. 가 구조체가 아닌 포인터 변수를 구조체처럼 참조하려고 해 오류가 발생하기 때문이다.

괄호 대신 → 라는 기호를 사용할 수도 있다. 이 기호를 이용하면 괄호를 쓰지 않아도 구조체를 참조할 수 있다.

괄호를 쓰는 것보다 화살표 기호를 쓰는 것이 더 편리하고 가독성도 좋다.

*단, 구조체 배열은 포인터로 넘겨받아도 s[i]→kor과 같이 화살표 기호로 참조하지 않으며, 일반적인 구조체처럼 s[i].kor로 참조해야 한다.

 

중첩 구조체

구조체 안에 구조체를 선언해 다른 구조체를 멤버로 포함할 수 있다.

구조체를 중첩해 사용하면 여러가지 정보를 관리하기에 용이하다.

typedef struct {
	char name[15];
	int age;
} Teacher;

typedef struct {
	char name[15];
	int age;
	Teacher teacher; //Teacher 구조체를 Student 구조체의 멤버로 포함시켰다.
} Student;

int main(){
	Student stuent;
	Teacher teacher;
	
	Student.teacher.age = 30; //구조체이름.멤버구조체이름.멤버구조체의멤버이름
	Teacher.name = "Emma";

	return 0;
}

 

자가 참조 구조체

구조체는 자기 자신을 가리키는 포인터를 멤버로 가질 수 있다.

자신과 같은 타입의 구조체를 멤버로 가지고 자기 자신을 참조할 수 있다.

typedef struct {
	char name[15];
	int age;
	sturct Student *p; //구조체 선언이 끝나기 전에 구조체 안에 구조체를 선언했다.
} Student;

자기 참조 구조체는 연결 리스트나 트리(자료구조)를 만들 때 사용된다.

 

구조체와 함수

구조체를 함수에 전달하기

구조체를 함수에 인자로 전달할 때는, 포인터로 전달(바로가기)하거나 구조체 자체를 전달(복사)할 수 있다.

구조체를 전달할 때는 원본 값을 바꿀 필요가 없는 경우에도 주로 포인터를 사용해 전달한다.

구조체를 복사해 전달하기 위해서는 복사할 공간이 필요한데, 구조체 크기가 커질수록 공간이 많이 필요해 비효율적이기 때문이다.

 

구조체를 인자로 전달하기

typedef struct {
	int s_id;
	int age;
} Student;

void print_student(Student s){
	s.s_id = 2000;
	s.age = 25;

	printf("%d, %d\n", s.s_id, s.age);
}

//구조체를 인자로 전달하기

int main(){
	Student s;

	s.s_id = 1000;
	s.age = 20;

	print_student(s);
	printf("%d, %d", s.s_id, s.age);

	return 0;
}

//출력하기

> 2000, 25
1000, 20

/*
구조체를 복사해서 전달했기 때문에
함수에서 값을 변경해도 원본 값에 영향을 미치지 않는다.
*/

 

포인터로 전달하기

typedef struct {
	int s_id;
	int age;
} Student;

void print_student(Student *s){
	s->s_id = 2000;
	s->age = 25;
	
	printf("%d, %d", s->s_id, s->age);
}

//구조체를 포인터로 전달하기

int main(){
	Student s;

	s.s_id = 1000;
	s.age = 20;

	print_student(&s); //매개변수가 포인터이므로 구조체의 주소값을 전달해야 한다. 
	printf("%d, %d", s.s_id, s.age);

	return 0;
}

//출력하기

> 2000, 25
2000, 25

/* 
포인터로 전달했기 때문에
함수를 호출해 구조체 멤버의 값을 변경하면 
원본 값도 같이 변경된다.
*/

포인터

포인터 : 변수의 주소값을 저장하는 변수. 포인터 변수라고도 부름.

 

 

포인터 변수의 선언과 초기화

포인터 변수를 선언할 때는 *(참조 연산자)를 붙여서 선언.

int형 변수의 주소를 담고 싶다면

int *p = null;

과 같이 선언하면 됨.

 

포인터 변수의 크기는 자료형이 달라도 모두 동일하다. (동일한 운영체제 시스템일 경우 주소값의 크기가 동일하기 때문)

 

※ 포인터 변수 선언 시 자료형의 역할

: 가리킬 주소에 담긴 변수가 어떤 자료형을 갖는지 알려줌. (포인터 연산을 할 때, 해당 주소로 찾아가서 자료형에 따라 다른 크기(int-4바이트, double-8바이트 등)를 읽어와야 하기 때문)

 

포인터 변수의 초기화 : null(0)로 초기화한다.

초기화하지 않고 선언만 한 다음 나중에 주소값을 넣어도 되지만, 변수를 초기화하지 않을 경우 초기값이 null이 아닌 쓰레기값이 되므로 변수를 잘못 사용하여 에러가 발생할 수 있다.

 

선언한 포인터 변수에 변수의 주소값 담기

int num = 15;
int *p = null;
p = &num;

변수 num의 주소값(&num)을 포인터 변수 p에 담았다.

포인터 변수 p로 포인터 연산을 하면, (*연산자를 통해)p에 들어있는 주소값으로 찾아가 해당 주소에 담겨 있는 값(15)으로 연산을 하게 된다.

 

 

참조 연산자 *

포인터의 이름이나 주소 앞에 사용. 포인터가 가리키는 주소에 저장되어있는 값을 반환한다.

int main() {
	int *p = null;
	int num = 15;

	p = &num; 

	printf("%d, ", &num);
	printf("%d, ", p);
	printf("%d", *p);

	return 0;
}

> -363432804, -363432804, 15

 

*p : p에 들어있는 주소(-363432804)로 가서 해당 주소에 담겨 있는 변수의 값(15)를 가져온다.

 

포인터를 이용하면 일반 변수처럼 사칙연산을 할 수 있다.(곱셉/나눗셈 제외)

 

그러나 증감 연산자의 경우, 참조 연산자보다 우선순위가 높아

*p++;

와 같이 처리할 경우

p에 담겨있는 주소를 찾아가 해당 주소의 변수(15)의 값을 1증가시키는 것이 아니라,

p에 담겨있는 주소값 자체를 증가시키게 된다.

 

따라서 (*p)++; 처럼 작성해 참조 연산자가 우선처리되도록 한다.

 

 

포인터를 사용하는 이유

함수에서는 인자를 전달할 때, 복사본을 전달한다. 넘겨받은 함수에서 복사본을 수정해도 원래 변수의 값은 변경되지 않는다.

그러나 포인터를 통해 메모리 주소를 넘겨주면(바로가기), 넘겨받은 함수에서 메모리에 직접 접근해 원래 변수의 값을 변경할 수 있다.

 

Call by Value VS Call by Reference

Call by value :

값을 복사해 전달하는 방식. 인자로 전달되는 변수를 함수의 매개변수에 복사함.

C언어에서 기본으로 지원하는 방식이다.

원본 값을 바꿀 필요가 없는 경우에 사용한다.

Call by reference :

값 대신 주소값을 전달하는 방식.

C언어에서 포인터를 사용해 주소값을 넘겨주는 것은 주소값 자체를 복사해서 전달하는 방식이므로 call by value, 혹은 call by adress(주소값을 복사해서 넘겨주는 것)라고 볼 수 있다.

C언어에서는 call by reference를 공식적으로 지원하지 않는다.

그러나 call by adress를 사실상 call by reference처럼 사용할 수있기 때문에, call by reference로 설명하는 곳도 많다.

 

⇒ C언어에서 함수 인자 전달은 원칙적으로 모두 call by value이지만, 포인터를 사용하여 call by reference를 구현할 수 있다.

 

포인터 연산과 배열

포인터로 버블 정렬 함수(오름차순) 만들기

#include <stdio.h>

void BubbleSort(int arr[]); // int arr[] 대신 int *arr을 써도 됨.

int main() {
	int arr[10];
	for(int i=0; i<10; i++) {
		scanf("%d", &arr[i]);
	}
	
	BubbleSort(arr);
	
	for(int i=0; i<10; i++){
		printf("%d ", arr[i]);
	}
	
	return 0;
}

// int arr[] 대신 int *arr을 써도 됨.
void BubbleSort(int arr[]){
	int temp;
	for(int j=0; j<9; j++) {
		for(int i=0; i<9; i++) {
			temp = arr[i];
			if(arr[i] > arr[i+1]) {
				arr[i] = arr[i+1];
				arr[i+1] = temp;
			}
		}
	}
}

1차원 배열은 int *arr과 같이 int형 포인터로 받을 수 있다.

int arr[]도 마찬가지로 배열의 메모리주소를 담고 있는 포인터이다.

포인터로는 배열의 크기를 알 수 없으므로, 다른 매개변수를 통해 배열의 크기를 받아야 한다.

int arr[]의 경우, []안에 크기를 넣어도 무시된다.

(위 코드는 배열의 크기가 정해진 간단한 코드이므로 매개변수로 배열의 크기를 받아 반복문 종결조건에서 사용하는 대신 종결조건에 직접 숫자를 입력해줬다.) 

 

arr을 포인터로 받으면, 함수 안에서 배열의 요소를 변경했을 때 함수 밖에서도 배열의 요소가 변경된다.

 

배열

배열의 이름은 포인터 변수와 같은 기능을 한다.

배열의 이름은 배열 첫번째 요소의 주소값을 나타내며, 배열의 주소는 연속되어 있다.

 

예시 1

int arr[10];

for(int i=0; i<10; i++) {
	scanf("%d", arr[i]);
}

⇒ scanf로 입력받을 때 다른 자료형과 달리 & 연산자를 사용하지 않아도 된다. (문자열 배열도 마찬가지)

 

예시 2

int arr[5] = { 10, 20, 30, 40, 50 };

int *p = arr;

배열 이름 자체가 주소값이므로, & 연산자를 쓰지 않아도 바로 포인터에 대입이 가능하다.

printf("%d, ", *p);
printf("%d\n", arr[0]);

> 10, 10

포인터 변수 p에는 배열 arr의 첫번째 원소의 주소값이 담긴다.

따라서 p에 담긴 주소값이 가리키는 값은 첫번째 원소값인 10이다.

 

포인터 연산

포인터 변수도 일반 변수처럼 값을 담고 있기 때문에 증감 연산과 덧셈/뺄셈 연산을 할 수 있다.

곱셈/나눗셈 연산은 할 수 없다.

포인터 변수의 증감 연산은 일반 변수의 증감 연산과 다르다.

일반 변수의 증감 연산은 값이 1씩 증가하거나 감소하지만,

포인터 변수의 증감 연산은 자료형의 크기만큼 증가/감소한다.

 

⇒ 포인터 변수는 n만큼 더하거나 빼면 자료형의 크기 X n 만큼 증가하거나 감소한다.

 

따라서 포인터를 다음과 같이 배열처럼 사용할 수도 있다.

 

포인터의 이름 = 배열의 첫번째 원소의 주소이고, 

포인터 + i = 배열의 i번째 원소의 주소이므로

*(arr+i) == arr[i]

가 성립된다.

 

 

상수 포인터

일반 변수 중 값을 바꿀 수 없는 상수가 있는 것처럼,

포인터 변수 중에서도 주소값을 바꿀 수 없는 상수포인터가 있다.

상수 포인터를 선언할 때도 상수처럼 const를 붙이는데, const의 위치에 따라 의미가 달라진다.

 

const int *p

포인터가 가리키는 변수를 상수화

포인터를 이용해 변수의 값을 변경할 수 없다.

그러나 변수 자체가 상수가 된 것은 아니므로, 변수의 값을 변경하는 것은 가능하다. (=변수의 값 자체는 변경할 수 있지만, 포인터를 이용해서 변경할 수는 없다.)

 

int *const p

포인터 상수화

포인터 변수 자체가 상수화 된다. 주소값을 변경할 수 없다.

포인터를 이용해서 변수의 값을 변경할 수는 있지만, 포인터가 가리키는 주소값은 변경할 수 없다. (=변수의 값을 변경할 수는 있지만 다른 변수를 가리킬 수는 없다.)

 

※ 원래 포인터를 선언할 때 * 연산자는 어디에 위치해도 상관없지만,

포인터를 상수화시킬 때는 const 전에 * 연산자를 써주어야 한다.

(int const *p 처럼 사용하면, const를 맨 앞에 써준 것과 같이 포인터가 가리키는 변수가 상수화된다.)

 

const int* const p

포인터를 통해 값을 변경할 수 없고, 포인터가 가리키는 주소값도 변경할 수 없다.(=변수의 값을 변경할 수도 없고, 다른 변수를 가리킬 수도 없다.)

 

 

이중 포인터와 포인터 배열

 

이중 포인터

: 포인터의 주소값을 담는 변수. 포인터의 포인터

포인터의 주소값을 담는 주소를 바꾸거나, 함수에서 문자열을 바꿀 때 사용한다.

 

예시

int num = 10;
int *p;
int **pp;

p = &num;
pp = &p;


/*

num == *p == **pp

&num == p == *pp

&p == pp

*/

 

포인터 배열

: 포인터를 담는 배열

int *parr[3]; //참조 연산자를 붙이고, 일반 배열처럼 선언한다.

parr[0] = &num1; //대입할 때는 변수의 주소값을 넣는다.

 

두 변수의 값 바꾸기

#include <stdio.h>

int main() {
	int a;
	int b;
	int temp;
	
	scanf("%d %d", &a, &b);
	
	temp = a;
	a = b;
	b = temp;
	
	printf("%d %d", a, b);

  return 0;
}

 

 

버블 정렬

가장 간단한 정렬 알고리즘. 서로 이웃한 값들을 비교해 큰 값을 뒤로 넘기며 정렬한다.

최종적으로는 작은 값 → 큰 값 순서대로 정렬된다.

값 비교 과정 때문에 다른 정렬에 비해 속도가 느리지만, 구현하기가 매우 간단하다.

 

버블 정렬로 배열 내림차순 정렬하기

#include <stdio.h>

int main() {
  int arr[10] = { 9, 17, 5, 6, 124, 112, 1, 3, 87, 55 };
  int temp; // 두 값을 바꿀 때 사용할 변수
  
	for(int j=0; j<9; j++) {
		for(int i=0; i<9; i++) {
			if(arr[i] < arr[i+1]) {
				temp = arr[i];
				arr[i] = arr[i+1];
				arr[i+1] = temp;
			}
		}
	}
	
	for(int j=0; j<10; j++) {
		printf("%d ", arr[j]);
	}
	
  return 0;
}

바깥쪽 for문의 종결조건이 j<9인 이유 : 
가장 큰 수가 배열의 맨 끝(arr[10])에 있는 경우(=자리를 가장 여러 번 바꿔야 하는 경우의 수), 
맨 앞으로 오기 위해 9번 이동해야(자리를 바꿔야) 함. 

 

삽입 정렬

선택된 원소를 앞에 있는 원소들과 비교하여 넣어줄 위치를 찾고 삽입한다.

선택된 원소의 앞에 있는 원소들은 이미 정렬이 되어있는 상태이기 때문에,

선택된 원소가 앞에 있는 원소들보다 크다면 앞의 원소들과 비교할 필요가 없으므로 바로 다음 원소를 선택한다. → 쓸모없는 비교를 줄이기 때문에 버블 정렬보다 빠르다.

배열이 길어질수록 효율이 떨어지고, 버블 정렬과 마찬가지로 구현이 단순한 편이다. (버블 정렬보다는 조금 더 복잡함.)

 

삽입 정렬로 배열 내림차순 정렬하기

#include <stdio.h>

int main() {
  
  int arr[10] = { 9, 17, 5, 6, 124, 112, 1, 3, 87, 55 };
  int temp; // 두 값을 바꿀 때 사용할 변수
  int length = sizeof(arr) / sizeof(int);
	int j;
	
	for(int i=0; i<length; i++) {
		temp = arr[i];
		j = i - 1;
		while(j>=0 && arr[j] < temp) {
			arr[j+1] = arr[j];
			j--;
		}
		arr[j+1] = temp;
	}
	
	for(int i=0; i<length; i++) {
		printf("%d ", arr[i]);
	}
	

  return 0;
}

 

 

*

오름차순 : 작은 값 -> 큰 값 순서대로 정렬 (값이 올라감)

내림차순 : 큰 값 -> 작은 값 순서대로 정렬 (값이 내려감)

 

이 글에서 삽입 정렬, 버블 정렬 설명은 오름차순 기준이고 예제 코드는 내림차순 기준입니다.

 


※ 출처 : 바로 실행해보면서 배우는 C언어, 구름edu

 

반복문, 조건문 응용해 소수 구하는 코드 작성하기

문제 : 값을 입력받아서 입력받은 값 미만의 수 중 소수를 출력한다. 소수의 갯수는 최대 100개로 제한한다.

 

배열에 담지 않고 바로 출력하기

#include <stdio.h>
int main() {
	int num;
	scanf("%d", &num);
	int i;
	int j;
	
	for(i=2; i<=num; i++) {
		for(j=2; j<i; j++) {
			if(i%j == 0) { 
				break;
			}
		}
		if(i==j) {
			printf("%d ", i);
		} 
	}
	return 0;
}

1. 소수는 1과 자기자신을 제외하고 어떤 수로도 나눠지지 않는 수이다.(2, 3, 5, 7, 13, 17, 19... 등이 있다. 1은 소수가 아님)

1~20 사이의 정수 중 소수를 구하려면(num=20을 입력받았을 때)

for문으로 1부터 20까지 돌면서 소수가 있는지 확인하고, 소수가 있다면 출력하면 된다.

단, 1은 소수가 아니므로 제외해야 한다. 따라서 바깥쪽 반복문의 초기값 i는 0이나 1이 아니라 2부터 시작한다. 

또한 num 자체가 소수일 수도 있으므로(입력받은 num이 2, 3, 5, 7, ... 등 소수인 경우)

바깥쪽 반복문의 종결조건은 i<num이 아니라 i<=num으로 num까지 포함시켜야 한다. 

 

2. 소수인지 확인하는 방법은 다음과 같다.

예를 들어 i=8이라면, 8보다 작은 자연수(1과 8 자기자신을 제외한 2~7사이의 숫자)로 8을 나눴을 때, 나누어 떨어지는 수가 있는지(8%j == 0) 확인하면 된다.

나누어 떨어지는 수가 있다면 8은 소수가 아니다.

만약 2~7까지 모두 확인했는데 나누어 떨어지는 수가 없다면, 8은 소수이므로 printf로 출력하면 된다. 

 

소수인지 확인하는 로직은 안쪽 반복문에서 작성했다.

for(j=2; j<i; j++) {
	if(i%j == 0) { 
		break;
	}
}

i=8일 때, 

8을 2~7까지 차례대로 나누어보면서 나머지가 0인지(나누어 떨어지는지) 확인한다.

만약 나머지가 0이라면, break문을 실행해 안쪽 반복문을 빠져나온다.

-> 나누어 떨어지는 숫자가 없다면(8이 소수라면) 종결조건을 만족할 때까지(j=7) 반복문을 다 돌고, 마지막으로 j가 1증가(j++)한 다음 반복문을 빠져나오기 때문에

변수 j의 값은 최종적으로 8이 된다. (i==j)

-> 나누어 떨어지는 숫자가 있다면(8이 소수가 아니라면) 반복문을 다 돌지 못하고(종결조건(j<8)까지 도달하지 못하고) 중간에 빠져나와

변수 j의 값이 최종적으로 8 미만의 값이 된다. (j<i)

 

=> 안쪽 반복문을 빠져나온 이후 j의 값이 i와 같은지 아닌지 확인함으로써

i가 소수인지 아닌지 확인할 수 있다.

 

따라서 안쪽 반복문을 빠져나온 뒤 조건문을 사용

if(i==j) {
	printf("%d ", i);
} 

위와 같이 i==j인지 확인한다. i==j라면 i는 소수이기 때문에 printf로 출력하고,

그렇지 않다면 바깥쪽 반복문을 계속 진행해 i 다음 숫자(i++)가 소수인지 확인한다.

 

안쪽 반복문의 카운터 변수 j를 안쪽 반복문을 빠져나온 후에도 사용해야하기(i==j 비교) 때문에

j를 반복문 안에서 선언하지 않고, 반복문 밖에서 선언해 반복문을 빠져나온 후에도 사용할 수 있도록 했다.

참고로 바깥쪽 반복문의 카운터 변수 i는 반복문 안에서만 사용되고 있기 때문에 반복문 안에서 선언해도 되고 반복문 밖에서 선언해도 된다. 

 

배열에 담아 출력하기

원래 문제의 의도대로 구한 소수들을 배열에 담은 후 출력하는 방법이다. 

#include <stdio.h>
int main() {
	int num;
	scanf("%d", &num);
	
	int length = 1;
	
	int i;
	int j;
	
	for(i=2; i<=num; i++) {
		for(j=2; j<i; j++) {
			if(i%j == 0) { 
				break;
			}
		}
		if(i==j) {
			length++;
			int arr[length];
			arr[length-1] = i;
			printf("%d ", arr[length-1]);
		} 
	}
	
	return 0;
}

 

배열의 길이(length)를 변수로 따로 선언한다. 마지막 요소로 null값을 담아 어디까지 출력할지 나타내기 위해서 배열의 길이는 1로 초기화했다.

소수를 구했다면, 배열의 길이를 1 늘리고(length++), 

늘린 길이를 이용해 배열을 선언한 다음, 구한 소수를 배열의 요소로 추가한다.

배열의 요소로 추가할 때는 배열의 인덱스(길이-1)를 이용했다.

그리고 추가한 배열 요소를 바로 불러와서 출력한다. 

 

아쉬운 점

- 내가 아직 배열을 잘 못 다뤄서 그런건지, 배열에 담아 출력하는 코드는 약간 지저분하게 됐다.

 

- 문제의 힌트를 보면 int i, j를 반복문 밖에서도 사용할 수 있게 선언하라고 했는데

내 코드에서 i의 경우 반복문 밖에서 선언하지 않아도 정답을 구할 수 있다. i를 반복문 밖에서도 사용해야하는 방식으로 풀려면 어떻게 해야하는지 고민해봤는데 잘 모르겠다.

 

- 소수의 개수 제한 조건은 작성하지 않았다. 

테스트 케이스가 num = 10, num = 30인 경우밖에 없어서 제대로 실행되었지만

입력된 숫자가 더 큰 경우 length >102인 경우 출력을 중단하거나 배열에 요소 추가를 하지 않도록 제한하는 코드가 추가되어야할 것 같다. 

+ Recent posts