2022.04.29 TIL (C 언어)

이진호·2022년 5월 1일
0

TIL

목록 보기
7/11

C언어

반복문

#include <stdio.h>

int main(){
	printf("Hello goorm!");
}

조건문

#include <stdio.h>

int main()
{
	int number;  
	printf("정수를 입력하세요. : ");
	scanf("%d", &number);

	if (0 < number)
	{
		printf("양수입니다.");
	}
	else if (number < 0)
	{
		printf("음수입니다.");
	}
	else
	{
		printf("양수도 음수도 아닌 0입니다.");
	}
	
	return 0;
}

c언어의 기본 구조

#include <stdio.h>

int main()
{  
  return 0;
}
  • #include <stdio.h>
    :#은 전처리기를 의미하며 컴파일 하기전에 미리 처리해야 하는 명령어를 의미함.

  • stdio.h
    :'.h' 확장자를 가지는 파일을 헤더 파일이라 부름. 헤더 파일이란 이미 만들어져 있는 함수가 어떤게 있는지 정리한 목차라고 생각하면 됨. 전처리기를 통해 헤더파일을 추가하면, 컴퓨터는 헤더 파일을 보면서 이 함수가 존재하는지 판단하고 기능을 가져올 수 있음.
    그 중에서도 stdio.h(STanDard Input Output)는 표준 입출력 헤더파일이라 하며, 이를 추가하면 입출력, 반복문, 조건문 등을 사용할 수 있도록 해줌.

  • int main()
    : C 언어의 가장 기본적인 함수로, 실행 버튼을 누르면 main 함수 안에 있는 코드가 제일 먼저 실행됨.

  • return 0;
    : 함수가 종료되었을 때 함수 내의 변수 혹은 어떤 값을 돌려주는 역할을 함.

  • ;
    코드가 끝날 때 세미콜론을 무조건 사용해야 함.

  • 이스케이프 시퀀스
    \와 특정 문자를 결합하여 C언어 특성상 표한할 수 없는 기능 혹은 문자를 표시하는 문자를 의미함.
    ex) \n: 줄바꿈, \t: 탭

int level; //변수 선언
level = 1; // 값 대입

int damage = 0; //변수 선언과 동시에 대입. -> 초기화

자료형(Data Type)

정수형

char은 정수와 문자를 표시할 때 사용함.
short, int, long, long long는 정수, 즉 숫자를 나타낼 때 사용됨.
정수형의 경우 signed(부호 있는 변수), unsigned(부호 없는 변수)로 나뉘어짐.

실수형

실수형float, double, long double 순으로 좀 더 큰 숫자, 많은 소수점을 나타낼 수 있으며, 정수형과 달리 unsigned가 존재하지 않음.

자료형의 크기

비트는 컴퓨터가 처리할 수 있는 데이터의 최소 단위로, 하나의 비트는 0 혹은 1의 값을 가지고 있음. 하나의 비트로는 표현할 수 있는 영역이 0과 1 밖에 없으므로 값을 표현하기 위해 비트 8개가 모여 만들어진 것이 바이트.
비트 8개가 모이면 이진수를 이용하여 값을 2의 7승인 128개의 양수와 128개의 음수를 만들 수 있는데, 양수는 0부터 127까지 128개로 이루어지고, 음수는 -1부터 -128까지 표현 가능함.

숫자를 저장할 땐 해당 자료형의 영역만큼 사용할 수 있는데, 문자의 경우 1바이트는 영문자, 숫자, 특수문자 한 글자를 저장할 수 있을 정도의 크기고, 2바이트는 1바이트로는 처리하기 어려운 한글, 일어, 중국어 등의 문자 하나를 저장할 수 있는 크기.

#include <stdio.h>

int main()
{
	int a;
	int b;
	int c;
	a = 10;
	b = 20;
	c = 30;	

	printf("a 는 %d이고 b 는 %d이고 c 는 %d입니다.", a, b, c);

	return 0;
}

변수가 할당된 뒤 초기화 하지 않은 즉, 값을 대입하지 않고 출력하면 사람이 알아볼 수 없는 값으로 출력되는데 이는 변수가 할당된 메모리 값이 출력된 것으로 사람이 이해하기 힘든 숫자이기 때문에 쓰레기 값이라 부름.

int main()
{  
  float a = 4.5f; # float 임을 확실히 하기 위해 f를 붙여줌
  double b = 1.234;

  printf("a 는 %.4f 입니다.", a); // 소수점 넷째 자리까지 출력
  printf("b 는 %.1f 입니다.", b);

  return 0;
}

cc++ 의 경우 실수값은 모두 double만큼 큰 실수로 인식하기 때문에 float 크기의 숫자도 double 형 크기의 숫자로 인식함. 따라서, f를 붙이지 않으면 1.234 같은 실수도 double형의 실수값으로 인식하게 됨.
그렇기 때문에 이 실수를 float 변수에 넣어주려고 하면 정밀도가 큰 숫자를 작은 float에 넣으므로 숫자가 잘릴 수 있다는 에러 메시지를 출력함.

  • 소수점 정하기
    실수형은 출력시 기본적으로 소수점 이하 6자리까지 표기되는데(float는 6자리, double은 15자리) 소수점을 길게 10자리 정도로 늘려서 출력시키면 소수점이 정확하지 않게 나오는 것을 확인할 수 있음. 이는 컴퓨터가 2진수(0,1)로 구성되어 있어 소수점인 10진수를 정확하게 표현할 수 없기 때문.

  • scanf

printf("정수 b 와 실수 d 입력 : ");
scanf("%d %f", &b, &d);
printf("입력받은 b d : %d %f\n", b, d);

상수

값을 항상 마음대로 바꿀 수 있는 변수와 반대로, 값을 영원히 바꿀 수 없는 상수가 존재함.

상수를 선언할떄는 자료형 앞에 const만 붙이면 됨. pi 같이 값을 실수로 바꾸면 안되는 경우에 적용하면 좋음.

사칙연산 예시

int a = 10;
int b = 3;
  
float c = 1.5;
float d = 2.5;

printf("a + b = %d\n", a+b);
printf("a - b = %d\n", a-b);
printf("c * d = %f\n", c*d);
printf("c / d = %f\n", c/d);
printf("a %% b = %d\n", a%b); // 출력 문자를 실수로 받으니 0.6의 잘못된 결과가 출력됨

증감 연산자의 전위 및 후위 연산

#include <stdio.h>

int main()
{
	int input;
	scanf("%d", &input);

	printf("%d\n", input++); # 값을 출력하고 1을 더해줌
	printf("%d\n", ++input); # 1을 더한 값을 출력함
	printf("%d\n", input--);
	printf("%d\n", --input);

	return 0;
}

논리 연산자

비트 연산자

비트 연산은 정수나 정수로 변환 가능한 타입만 가능하며, 실수나 포인터 등은 비트 연산을 할 수 없음. 다른 연산자들처럼 흔하게 사용되진 않지만 적절한 떄에 사용하면 메모리 공간의 효율성을 높이고 연산의 수를 줄일 수 있고, 비트 단위로 계산하기 떄문에 일반적인 사칙연산 연산자보다 훨씬 속도가 빠름.

& 연산자(AND)

논리 연산자의 &&와 헷갈릴 수 있는데, 논리 연산true와 false를 반환하고 비트 연산을 반환한다는 것을 명심.
둘 다 1이어야 1을 반환함.

또한, & 연산자는 주소값도 가리킴. 하지만, 주소값을 가리키는 & 연산자는 단항 연산자로서, 피연산자가 다음과 같이 하나만 필요함.
&b : 하나의 변수 앞에서 쓰여질 때는 주소값을 나타냄.
a & b: 비트의 AND 연산자를 의미함.

MSB(Most Significant Bit)

signed의 경우 "부호 있는 정수"라 하여 맨 왼쪽 비트는 부호비트라고 부르며 0이면 양수, 1이면 음수를 나타냄. 따라서 MSB가 1이 되면 음수로 계산 되어 보수연산을 하게 됨.

| 연산자(OR)

두 개의 비트 중 하나라도 1이면 1을 반환함.

^ 연산자(XOR)

^ 연산은 XOR 연산으로 두 개의 비트가 다르면 1을, 같으면 0을 반환함.

~ 연산자(NOT)

~ 연산은 NOT 연산자로, AND, OR, XOR과는 다르게 피연산자가 하나임. 즉 한 값의 비트를 모두 반전시킴. 비트가 1이라면 0을, 0이면 1을 반환함.

unsigned char의 경우 &hhu로 입력 받아야 하며, 출력 형식도 마찬가지. 단, 외우기 힘들다면 출력은 &d도 사용 가능함.

보수 내용은 https://www.bloger.kr/34 여기 참조

비트 이동 연산자

<<, >> 는 지정한 횟수대로 비트의 자리를 각각 왼쪽, 오른쪽으로 이동시키는 연산자. 왼쪽 쉬프트의 경우, 왼쪽으로 비트의 한 자리가 이동할 떄마다 정수의 값은 두배가 됨. 2진수이므로 2칸을 이동하면 네배가 되고, 3칸을 이동하면 8배가 됨. 즉, 2의 n승.

for 반복문

for (초기식; 조건식; 증감식)
{
	반복할 내용
}
#include <stdio.h>

int main()
{
	int input;
	scanf("%d", &input);
  for(int i=1; i<=9; i++)
	{
    printf("%d X %d = %d\n", input, i, input*i);
  }
  return 0;
}

while 반복문

// while 문으로 "Hello, world!\n" 를 5번 출력

int main()
{
	int i = 0; // 변수 선언
	while(i<5) // 조건식
	{
		printf("Hello, world!\n");
		i++; // 증감식
	}

	return 0;
}

do while 반복문

do while 문은 do(먼저 한 번 코드를 실행)한 후 while(조건 확인 및 반복)을 함.

#include <stdio.h>

int main()
{
	int number = 0;
	int sum = 0;

	do
	{
		number++; # 증감식 기입
		sum += number; # 반복문동안 진행할 식
	}
	while(0<number && number<10); # 조건식

	printf("1~10 까지의 합 : %d", sum);

	return 0;
}

for, while 복습

#include <stdio.h>

int main()
{
	int input;
	scanf("%d", &input); // 입력
	
	int sumFor = 0; // for 문으로 합산할 변수 초기화
	for(int i=0; i<=input; i++)
	{
		sumFor += i; // 복합 대입 연산자 사용해보세요!
	}
	
	int sumWhile = 0; // while 문으로 합산할 변수 초기화
	int i = 1; // while 문에 사용될 반복자 변수 초기화
	while(i<=input)
	{
		sumWhile += i; // 복합 대입 연산자를 사용해보세요!
		i++; // 증감 연산자를 사용해보세요!
	}
	
	printf("%d %d", sumFor, sumWhile);
}

배열

자료형 배열이름[크기(요소의 개수)] = {값1, 값2, 값3, ...}
ex)

자료형 배열이름[크기(요소의 개수)] = {1,2,3, ...}
int main()
{
  int arr1[5] = {1, 33 , 47, 102, 155}; // 선언과 동시에 초기화
  int arr2[5] = {5}; // 0 번째 값을 5 로 초기화하고 나머지는 모두 0 으로 초기화
  int arr3[5] = {5, 10}; // 0 번째 값을 5, 1 번째 값을 10으로 초기화하고 나머지는 모두 0 으로 초기화
  int arr4[5] = {}; // 모두  0 으로 초기화
  int arr5[5]; // 초기화 하지 않음
  int arr6[] = {11, 22 , 33, 44}; // 배열의 크기가 4로 정해지면서 자동으로 초기화

  return 0;
}

배열의 개수를 넘어서는 범위를 출력하거나 초기화하지 않은 배열을 출력했을 경우, 컴퓨터마다 결과가 조금씩 다르지만 대부분 쓰레기값이 나오게 됨.

변수를 선언함과 동시에 변수는 시스템 메모리 상에서 한 부분을 차지하게 되는데, 변수의 값을 정해주었다면 해당 메모리 영역의 값은 변수의 값이 되지만, 정해주지 않았을 경우에는 메모리 자체에서 가지고 있는 값을 보여주게 됨. 이 값이 바로 쓰레기 값

배열의 주소

일반적으로 우리가 사용하는 변수들은 모두 메모리의 특정한 주소에 저장되어 있음.
&가 주소값을 나타내는데, scanf를 사용할 때 주소값을 알려주기 위해 &을 사용함. 각 변수들은 선언될 때 메모리에 무작위로 저장되고, 각각 그 주소값을 가지고 있음.

하지만 배열의 경우는 선언한 크기만큼 연속적으로 연결되어 있음.

int arr[3] // 원소의 크기가 4byte이고, 배열의 크기가 3인 배열 선언

배열은 메모리 주소가 연결되어 있으므로 주소는 배열 원소의 크기인 4씩 증가함. arr 배열의 0번째 원소가 주소 1000번지에 저장되어 있다고 가정하면 다음과 같이 저장되어 있다고 생각할 수 있음. c언어에서는 주소를 나타낼 때 시작번지만을 나타냄. 시작번지와 자료형만 알면 끝이 어딘지는 금방 계산할 수 있음. 1바이트인 char 배열의 경우 1씩, 8바이트인 double의 경우에는 8씩 증가. 즉, 연속된 각 원소끼리의 주소는 자료형의 크기만큼 차이난다는 것임.

배열의 크기

sizeof(배열) // 배열의 크기를 계산할 수 있음. 만일 배열이 int로 선언됐을 경우
			// 배열 원소의 갯수에서 4를 곱한 값이 계산됨.
            // 따라서, 배열의 크기를 계산하고 싶으면 sizeof[배열]을 원소의 크기로 나누면 됨.

문자열 출력과 입력

char은 자료형 강의에서 언급했듯 문자숫자를 표현할 때 사용되는 자료형으로 1바이트의 크기이며, -128 ~ 127까지의 총 256의 범위를 지니고 있음.

문자의 경우 숫자를 글자에 대응시키는 방식으로 수행되는데 이런식으로 문자 하나 하나를 숫자에 매칭시키고 표현하는 방식임. 이를 아스키 코드라고 함.

숫자와 문자를 매칭시키기 때문에 256개의 글자 밖에 못쓰는 것은 아닌가? 256이라는 제한된 숫자 때문에 유니코드(UNICODE)라는 것이 생겼음. 유니코드는 문자를 1바이트가 아닌 2바이트로 처리하여 256 배인 65,536개의 글자를 표현할 수 있음. 이건 현재까지 여러 언어와 특수 문자를 표시하고도 2만 개 가량 가짓수가 남을 정도.

char ch = 'a';

printf("%d\n", ch); // a와 매칭되는 97 출력
printf("%c\n", ch); // a 출력

char ch = 'ab';

printf("%d\n", ch); // b와 매칭되는 98만 출력
printf("%c\n", ch); // b 출력

char 자료형은 1 바이트로 한 글자만 담을 수 있으므로 ab 중 마지막으로 입력했던 b만이 ch에 담기게 됨. 따라서, 숫자의 경우 98 문자열은 b만 출력됨.

정수형을 여러 개 담고 싶었을 때 int형 배열을 만들 듯이 여러 글자를 담고 싶다면 char 형 배열을 만들어주면 됨.

지금까지는 한글자씩 출력했기 때문에 %c를 이용했지만, 이렇게 여러 글자가 담긴 문자열을 출력할 때에는 정수형 변수 출력 강의에서 배웠듯이 %s를 사용하면 됨.

char ch[5] = "abcd";

printf("ch 는 %s", ch); // abcd 출력

return 0

배열의 크기가 4 글자인데 4가 아닌 5만큼을 차지하고 있는 것을 볼 수 있는데, 이는 컴퓨터의 동작 방식 때문임. ch 배열을 출력하라고 했을 때 사용자가 원하는 것은 abcd 전체의 출력을 의미하는 것이지만, 실제로 컴퓨터가 인식할 때는 얼마만큼의 길이인지 알려주어야 함.

하지만 출력할 때마다 이 배열은 4글자야, 5글자야 알려주는 건 번거롭기 때문에 마지막 남는 자리에 0, NULL, \0 등의 값을 넣어줌. 이 세 문자는 전부 같은 것으로 종료 문자를 가르킵니다. 위에 올려놓은 아스키 코드 표의 0 부분을 참조할 것.

  char ch[7] = { 'a', 'b', 'c', 'd', 0, 'e', 'f' };
  
  printf("ch 는 %s", ch);

위 코드를 실행해보면 abcd까지만 출력이 되는데 문자열에 값을 대입하고 싶다면 지금까지 썼던 코드와 같이 선언할 때 바로 값을 넣어주어야 함.
선언 이후에 값을 대입하려 하면 오류가 나는데, 만약 꼭 선언을 한 이후에 코드에서 값을 대입하고 싶다면

ch[0] = 'a';
ch[1] = 'b';

와 같이 하나하나 저장해야 함. 문자열을 입력 받는 것은 아래와 같이 하면 됨.

// 한 글자 입력 받기
char ch;
scanf("%c", &ch);
printf("%c", ch);

// 200자 이내 글자 입력 받기
char ch[201]; 크기가 201인 배열 생성, 아직 값이 대입되지 않은 상태 
scanf("%s", &ch); // 문자열 배열의 경우 & 생략 가능
printf("%s", ch);

scanf를 사용해서 값을 입력받을 때는 주소를 전달해주기 위해 &를 꼭 붙여줘야 하는데 문자열 배열의 경우 &를 쓰지 않고 그냥 변수 이름만 써주어도 됨. 배열의 경우, 배열의 이름에 주소를 담고 있기 떄문. 이 개념은 후에 포인터파트에서 자세히 나오므로 지금은 배열의 이름은 주소를 담고 있다라고 가볍게 생각하면 됨.

30자 이내의 문자열을 입력받고, 문자열의 길이를 출력하는 프로그램을 작성하세요.

#include <stdio.h>

int main()
{
	char ch[31];
	scanf("%s", ch);
	
	int cs = sizeof(ch); // 초기에 배열을 선언한 크기인 31이 나옴
	int i = 0; // 반복문을 위한 변수 선언
	while (ch[i] != 0) // 문자열 배열에서 0이 아닌 경우에만 반복문 실행
	{
		i++;
	}
	printf("%d", i); // 문자열이 끝났을 때인 i가 문자열의 길이가 됨.
	return 0;
}

2차원 배열

2차원 배열의 경우 초기화 할 때 이중 반복문처럼 깊이가 있어야 함. 깊이가 있기 떄문에 출력과 마찬가지로 입력 또한 이중 반복문을 이용해야 함.

#include <stdio.h>

int main()
{
	int tarr[2][3];
	printf("2차원 배열 입력\n");
	for(int i=0; i<2; i++)
	{
		for(int j=0; j<3; j++)
		{
			printf("tarr[%d][%d] 입력 : ", i, j);
			scanf("%d", &tarr[i][j]);
		}
	}
	printf("\n2차원 배열 출력\n");
	for(int i=0; i<2; i++)
	{
		for(int j=0; j<3; j++)
		{
			printf("\t%d", tarr[i][j]);
		}
		printf("\n");
	}
	return 0;
}

2차원 배열을 이용한 문자열 입력받기

#include <stdio.h>

int main()
{
	char tarr[3][10];

	printf("이차원 배열 문자열 입력\n");
	for(int i=0; i<3; i++)
	{
		printf("10자 이내의 문자열을 입력해주세요 : ");
		scanf("%s", tarr[i]); # 문자열이라 & 생략 가능
	}

	printf("\n이차원 배열 문자열 출력\n");
	for(int i=0; i<3; i++)
	{
		printf("%s\n", tarr[i]);
	}

	return 0;
}

조건문

이름 그대로 조건을 판별하는 제어문으로 if와 switch문이 존재함.

if(num>10 && num%2==0)
	{
		printf("%d(은)는 10 초과의 짝수입니다.", num);
	}
	else if(num>10 && num%2!=0)
	{
		printf("%d(은)는 10 초과의 홀수입니다.", num);
	}

switch

switch 문은 if 문과 마찬가지로 조건을 체크하고 그 케이스를 실행하지만 if 문처럼 관계식을 쓰지는 못하고 특정한 정수값이나 문자만을 확인할 수 있음. 실수 값의 경우 오류가 발생하게 됨.

또한, if문처럼 하나하나 조건을 체크하면서 내려가는 것이 아니라, 입력받은 값의 케이스로 바로 이동하게 됨. 그렇기 때문에 if문보다 빠름.(일반적으로 4개 이상의 조건일 때 switch문을 사용하면 성능이 더 좋다고 함. 하지만 요즘에는 컴파일러가 많이 발전했기 때문에 switchif의 성능차이가 크게 나지 않음.

switch(기준값)
{
	case 비교값1:
		기준값과 비교값1이 같을 때 실행
	case 비교값2:
		기준값과 비교값2가 같을 때 실행
	
	default:
		기준값과 비교값들이 같지 않을 때 실행
}

break를 안쓰면 switch문을 빠져나가지 못하기 때문에 해당되는 케이스의 아래 케이스들까지 전부 실행하게 됨. 따라서, 해당 케이스만 실행하고 싶은 경우 break를 반드시 사용해주어야하고, 이 특징을 이용하여 여러 조건을 검사하고 싶은 경우 break를 인위적으로 사용 안할 수도 있음.

	{
		case 10:
		case 9:
		case 8:
		case 7:
			printf("학점은 A입니다.\n");
			break;
		case 6:
		case 5:
		case 4:
			printf("학점은 B입니다.\n");
			break;
		// ...
}

두 변수의 값 바꾸기

	int a = 5;
	int b = 10;
	int temp;

	temp  = a; // temp에 a 넣기
	a = b; // a에 b 넣기
	b = temp; // b에 temp 넣기

삽입 정렬

#include <stdio.h>

int main()
{  
	int arr[10] = {9, 17, 5, 6, 124, 112, 1, 3, 87, 55};
	int length = sizeof(arr)/sizeof(int);

	int j;
	int temp;
	for(int i=1; i<length; i++)
	{
		temp = arr[i];
		j = i-1;
		while(j>=0 && temp>arr[j])
		{
			arr[j+1] = arr[j];
			j--;
		}
		arr[j+1] = temp;
		}
	
	for(int i=0; i<length; i++)
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

함수

함수란 특정한 기능을 따로 분리해놓은 것으로 수학에서 사용하는 함수의 개념과 비슷.

int arrayPlus(int arr[], int length) // [반환형] [함수명] (매개변수)
{ //호출 시 작동될 함수 내부 코드
	int i;
	int result = 0;

	for(i = 0; i < length; i++)
	{
		result += arr[i];
	}

	return result;
}

반환형이 있는 함수는 return이 필수적이며, int형의 경우 반환하는 건 정수라는 뜻.
C언어에서 main 함수의 경우 return 0;을 써서 프로그램이 종료되었고, 에러없이 끝났다는 것을 운영체제에 알려줌. main 함수는 운영체제에서 호출되었기 때문(main 함수의 return 값을 탈출코드라 해서 에러를 운영체제에 알려주는 역할을 함).

[함수명] (매개변수)
{
    [호출 시 작동될 함수 내부 코드]
}

반환형이 없는 함수의 경우 void라는 반환형을 사용하면 됨. void는 사전적인 의미로 "존재하지 않는다"는 뜻임.
아무런 인자를 넘기지 않을 때에도 void를 사용하면 됨. 인자를 넘기지 않을 때는 void를 써도 되고, () 형태로 아무것도 적어주지 않아도 됨. 단 반환형은 반드시 void를 적어주어야 함. int main() 함수도 넘겨주는 인자가 없기 때문에 비워져 있는 것으로 int main(void)와 동일한 것을 의미.

함수의 선언

C언어는 절차지향언어이기 때문에 위에서 아래로 차례대로 소스 코드를 해석하므로 함수 선언이 main 함수 아래에 있으면 인식을 못함.

따라서, 반드시 함수 정의와 선언을 main 함수 위에 해주거나, 또는 함수의 원형을 main 함수 위에 선언해주어야 함.
함수 원형반환형, 함수이름, 인자 목록으로 이것만 선언한 후 함수의 정의는 main 함수 아래에 써주면 됨.

int func1(void);

전역변수와 지역변수

  • 지역변수
    지역변수란 이름에서 느끼듯이 한 지역 내에서만 사용할 수 있는 변수로 함수에서 중괄호에 의해 만들어지는 영역을 의미함. main 함수내의 변수들도 다 지역변수이므로 다른 함수내에서 사용할 수 없음.
    main 함수에서 사용하던 변수를 다른 함수에서도 쓰려면 전달인자로 넘겨주거나, 전역변수로 선언한 후 사용해야 함. 다른 함수에서 선언된 지역 변수를 main에서 쓰려면 변수를 return 한 후 main에서 따로 저장해두어야 함.

  • 전역변수
    전역변수는 지역변수와 반대로 어느 지역에서나 사용할 수 있는변수로 괄호 안에 쓴 변수가 지역변수였다면, 괄호 밖에 쓴 변수는 전역변수임.

    이 전역변수는 프로그램의 시작과 동시에 메모리 공간에 할당되어서 프로그램이 종료될때까지 존재함. 또한 지역변수와는 다르게 별도의 값으로 초기화하지 않으면 0으로 초기화 됨.

지역변수와 전역변수 중에 이름이 같은 변수가 있는 경우에는 지역변수를 우선적으로 접근함. 코드가 길어질수록 지역변수와 전역변수의 이름이 겹칠 가능성이 높아지는데, 이렇게 되면 개발자가 의도하지 않았던 결과가 나오게 될 수 있음. 따라서, 꼭 필요한 경우가 아니라면 지역변수를 사용해야 함.

포인터

포인터는 주소를 가리킴. 이름만 포인터라고 다를 뿐이지 int, char 같이 다를 바 없는 변수이지요. 포인터 변수라고 부르기도 합니다.
int형은 정수를, char문자를 저장하듯이 포인터변수의 주소값을 저장함.

#include <stdio.h>

int main()
{
	int *p = NULL;  // int* p == int * p 모두 같음 포인터 선언
	int num = 15;

	p = &num;

	printf("int 변수 num의 주소 : %d \n", &num);
	printf("포인터 p의 값 : %d \n", p);
	printf("포인터 p가 가리키는 값 : %d \n", *p);

	return 0;
}

포인터 변수를 선언할 때는 담고자 하는 자료형에 *(참조 연산자)를 붙여서 선언함.
ex) int형 변수의 주소를 담고 싶으면 int*
하지만, 자료형에 따라 주소값의 크기는 변하지 않음. 동일한 운영체제 시스템일 경우 주소값이 동일한 크기를 갖기 때문. 32비트 시스템이면 4바이트, 64비트 시스템이면 8바이트.

자료형에 따라 선언하는 자료형이 달라지는 이유는, 가리킬 주소가 어떤 자료형을 갖는지 알려주기 위함.
포인터 연산을 할 때에는 그 주소로 찾아가 int형이면 4바이트만큼, double형이면 8바이트씩 읽어들여야 함. 따라서, 어떤 자료형의 주소를 가리키는지 알려주어야 하는 것.

가리키는 변수에 맞춰 포인터 변수도 자료형을 맞춰 준다고 생각하면 됨.

int나 double 같은 경우는 정수형이기 때문에 처음에 우리가 원하는 숫자를 넣어서 초기화 할 수 있었음. ex)int num = 10;
하지만 포인터는 주소값을 담는 변수이기 때문에 어떤 특정한 숫자로 할 수 없으므로 숫자가 아닌 NULL(0)로만 가능함.

물론 NULL로 초기화하지 않고 그냥 선언만 한 후 주소값을 넣어도 괜찮음. 포인터를 초기화할 때 쓰는 0은 0번지를 가리키는 것이 아니라, 아무것도 없다는 뜻의 NULL(0)임.

변수를 초기화하지 않았을 때의 초기값은 쓰레기값이 들어가 있다는 사실은 포인터 변수에도 동일하게 적용됨.
특히 포인터주소값을 다루기 때문에 초기화를 하지 않고 사용하다가 실수로 잘못된 주소를 다루게 되면 프로그램이 예상치 못하게 종료되거나 다른 오류를 일으킬 수도 있으므로 선언 후 바로 다른 변수의 주소값을 넣더라도, 되도록이면 NULL로 초기화 해주는 것을 권장함.

포인터 p에는 num의 주소값(1000)이 들어있고, 그렇게 되면 포인터 연산을 할 때 p에 들어있는 주소값(1000)으로 찾아가 그 값으로 연산을 하게 됨.
즉, num을 찾아가는 것.
그런데 변수 p를 출력할 때 분명히 주소값이 출력됐는데, 어떻게 num의 주소로 사용한다는 것일까? 이는 참조연산자에 의해 달려있음.

참조 연산자 *

& 연산자가 and 연산자와 주소 연산자로 쓰이듯이, 마찬가지로 두개의 피연산자(a*b)가 있으면 곱셈 연산자이고, 하나의 피연산자만 있으면 참조 연산자임.

참조 연산자는 포인터의 이름이나 주소 앞에 사용하며, 포인터가 가리키는 주소에 저장된 값을 반환함.

#include <stdio.h>

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

	(*p)++;
	printf("포인터 p가 가리키는 값 : %d\n", *p);
	printf("num 값 : %d\n\n", num);

	*p++;
	printf("포인터 p가 가리키는 값 : %d\n", *p);
	printf("num 값 : %d\n", num);

	return 0;
}
  • (*p)++: p가 가리키는 값에 증감연산자가 적용됨.
  • *p++: 쓰레기값이 출력됨.

증감연산자가 참조 연산자보다 우선순위가 높음.

후자의 경우, 주소를 먼저 찾아가지 않고, 주소값이 들어있는 변수 p를 먼저 증가시키게 되어 포인터 변수에 들어있는 주소값이 증가하는 것임. 하지만 증가한 그 주소에는 아무것도 선언되어 있지 않으므로 쓰레기 값이 들어있음.

그런데 굳이 포인터에 주소를 넣어서 간접적으로 사용할 필요없이, 그냥 지금까지처럼 변수를 바로 사용하면 되지 않을까? 왜 포인터를 이용하는 것일까? 이게 바로 포인터의 핵심

포인터는 함수를 사용할 때 진가를 발휘함. 함수에서는 인자를 전달할 때 복사해서 사용함. 즉, 전달해주는 원래 변수는 함수에서 수정할 수 없다는 뜻임.

하지만 포인터로 메모리의 주소를 넘겨주면 함수에서도 메모리에 직접적으로 참조할 수 있기 때문에, 변수의 값을 바로 수정하는 것이 가능함.

Call by value & Call by reference

인자를 전달하는 방식에는 Call by value와 Call by reference가 존재함.

Call by value

기본적으로 C언어에서 지원하는 방식이고, 함수에서 값을 복사해서 전달하는 방식으로, 인자로 전달되는 변수를 함수의 매개변수에 복사함.

이렇게 복사되면 인자로 전달한 변수와는 별개의 변수가 되며, 매개변수를 변경해도 원래의 변수에는 영향을 미치지 않으므로 원본 값을 바꿀 필요가 없을 경우에는 call by value 방식을 이용하면 됨.

Call by reference

함수에서 값을 전달하는 대신 주소값을 전달하는 방식을 call by reference라고 함.

하지만 엄밀히 따지면 C언어에서의 방식은 주소값 자체를 복사해서 넘겨주는 것이므로 call by value이고, 이렇게 주소값을 복사해서 넘겨주는 것을 call by address 방식이라 함.

C언어에서는 call by reference를 공식적으로 지원하지 않지만 의미적, 결과적으로 call by address를 이용해서 call by reference와 같이 사용할 수 있기 때문에 일반적으로 C언어에서 포인터를 이용해 주소값을 넘겨주는 방식을 call by reference라고 설명하는 곳도 많음.

#include <stdio.h>

void swap(int *a, int *b)
{
	int temp;

	temp = *a;
	*a = *b;
	*b = temp;
}

int main()
{
	int a, b;

	a = 10;
	b = 20;

	printf("swap 전 : %d %d\n", a, b);

	swap(&a, &b);

	printf("swap 후 : %d %d\n", a, b);

	return 0;
}

포인터 연산과 배열

배열의 이름은 포인터 변수와 같은 기능을 하며, 첫번째 요소의 주소값을 나타냄.

#include <stdio.h>

int main()
{
	int arr[5] = {10, 20, 30, 40, 50};
	int *arrPtr = arr; // 포인터 선언과 동시에 초기화, arr 이름은 배열의 첫번째 원소의 주소값

	printf("%d\n", *arrPtr); // arrPtr이 가리키는 값은 배열의 첫번째 원소값
	printf("%d\n", arr[0]);

	return 0;
}
// 출력: 10, 10

위와 같이 & 연산자를 쓰지 않아도 arr 이름 자체가 주소값이기 때문에, 바로 포인터에 대입이 가능함.
arr 이름은 배열의 첫번째 원소의 주소값이므로 arrPtr이 가리키고 있는 값을 출력해보면 첫번째 원소값인 10이 출력됨.

scanf로 입력받을 때 다른 자료형들은 다 &연산자를 붙여줘야 하지만, 문자열은 붙여주지 않아도 되는 이유와 일맥상통함.

포인터 연산

포인터 변수도 일반 변수처럼 값이 들어가 있는 변수기 때문에 증감 연산을 할 수 있음. ++, --와 같은 전/후위 연산과 +,- 연산자 같은 일반 덧셈과 뺄셈이 가능하지만 곱셈, 나눗셈은 불가능함.

포인터 변수의 연산은 일반 변수에서 사용했던 전/후위 연산과 다르게 자료형에 따라 달라짐.

int형 포인터는 1을 더할때마다 4씩 증가하고, double형 포인터는 8씩 증가함.

즉, 포인터변수가 n만큼 더하거나 뺄 때 자료형의 크기 x n만큼 증가함. 감소도 마찬가지.

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

arr은 배열의 첫번째 원소의 주소값을 가리키므로 인덱스 만큼의 크기를 포인터에 더해주면 해당 인덱스의 값을 가리킬 수 있음.

#include <stdio.h>

void bubbleSort(int *p) // 배열의 첫번째 원소의 주소값을 가리킴. 포인터 값을 받을 포인터 변수 선언
{
	int temp;	
	for(int i=0; i<9; i++)
	{
		for(int j=0; j<9-i; j++)
		{
			if(*(p+j) > *(p+j+1))
			{
				temp = *(p+j);
				*(p+j) = *(p+j+1);
				*(p+j+1) = temp;
			}
		}		
	}
}

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;
}

상수 포인터

일반 변수에는 값을 절대 바꿀 수 없는 상수가 존재하는데 포인터에도 마찬가지로, 주소값을 바꿀 수 없는 상수 포인터가 존재함.

const를 이용하여 상수 포인터를 만들 수 있음.

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

const int *ptr과 같이 const를 가장 앞에 사용하게 되면, 이 포인터를 이용하여 변수의 값을 변경하는 것을 막을 수 있음.

#include <stdio.h>

int main()
{
	int num = 10;
	int *ptr1 = &num;
	const int *ptr2 = &num;

	*ptr1 = 20; // const가 아니므로 ptr1이 가리키는 값은 수정 가능
	num = 30; // 변수 num 자체가 상수가 된 것은 아니므로 변수 값은 변경이 가능함

	*ptr2 = 40; // const로 선언한 포인터 변수 값을 이용하여 수정하려 하면 오류 발생

	return 0;
}

포인터 상수화

int* const ptr과 같이 자료형 다음에 const를 선언하게 되면 포인터 변수 자체가 상수화 됨. -> 주소값을 변경할 수 없다는 뜻임.

여기서 주의할 점은, 포인터를 상수화 시킬때에는 const 전에 * 연산자를 써주어야 함. 만약 int const *ptr처럼 사용한다면 const를 제일 앞에 써준것과 같은 효과가 발생함. -> 포인터가 가리키는 변수를 상수화 하는 효과를 말하는듯.

#include <stdio.h>

int main()
{
	int num1 = 10, num2 = 20;
	int *ptr1 = &num1;
	int* const ptr2 = &num1;
	
	ptr1 = &num2;
	
	*ptr2 = 30; // 포인터가 가리키는 값을 변경하는 것은 가능함
	ptr2 = &num2; // 포인터의 값인 즉, 주소값을 바꾸는 것은 불가능함.
	
	return 0;
}

포인터를 통해 값을 변경하는 것도, 다른 변수를 가리키는 것도 불가능하게 하고 싶다면, const를 두번 써주는 것이 가능함.

ex)

const int* const ptr2 = &num;

이중 포인터와 포인터 배열

이중 포인터는 포인터의 주소값을 담는 변수로, 포인터의 포인터라고 할 수 있음.

#include <stdio.h>

int main()
{
	int num = 10;
	int *ptr;
	int **pptr;

	ptr = &num;
	pptr = &ptr;

	printf("num : %d, *ptr : %d, **ptr : %d\n", num, *ptr, **pptr);
	printf("num 주소 : %d, ptr 값 : %d, **ptr 값 : %d\n", &num, ptr, *pptr);
	printf("ptr 주소 : %d, pptr 값 : %d", &ptr, pptr);

	return 0;
}
num : 10, *ptr : 10, **ptr : 10
num 주소 : 98023828, ptr 값 : 98023828, *pptr 값 : 98023828
ptr 주소 : 98023832, pptr 값 : 98023832

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

포인터 배열

일차원 배열에 값들을 넣을 수 있듯이, 포인터를 담을 수 있는 배열이 존재함. 이를 포인터 배열이라 함.

#include <stdio.h>

int main()
{
		int num1 = 10, num2 = 20, num3 = 30;
		int *parr[3]; 배열의 길이가 3인 포인터 배열을 생성함

		parr[0] = &num1;
		parr[1] = &num2;
		parr[2] = &num3;

		for(int i=0; i<3; i++)
		{
			printf("parr[%d] : %d\n", i, *parr[i]);
		}

		return 0;
}
parr[0] : 10
parr[1] : 20
parr[2] : 30

%lf는 double형으로 데이터를 받는데 따로 지정해주지 않으면 default는 소수점 6자리까지 출력함.

구조체

구조체란, 하나 이상의 변수를 묶어서 좀 더 편리하게 사용할 수 있도록 도와주는 도구.

예시)

C언어를 이용해서 콘솔로 간단한 동아리 주소록 시스템을 만든다고 가정하면 주소록에는 동아리에 가입한 학생의 이름, 학번, 나이, 전화번호를 저장해야 함. 여기서, 이름과 전화번호문자열, 학번과 나이정수형으로 선언해야 함.

만약 구조체를 사용하지 않는다면, 동아리에는 학생이 여러 명 가입되어 있기 때문에 이 정보들을 저장하기 위해서는 일일히 변수를 하나하나 선언해주어야 함. 조금 더 간단히 하자면, 배열로 만드는 방법도 있지만 배열은 같은 자료형으로만 묶을 수 있기 때문에 결국 다른 자료형을 가진 변수들은 따로 선언을 해주어야 함. 또한, 관리하기에 매우 까다롭기 때문에 비효율적임.

구조체를 사용하면 무척 편리하게 여러개의 변수를 사용하고 관리할 수 있음. 사용자가 직접 자료형을 만들어서 사용한다고 생각하면 됨.

구조체는 새로운 자료형을 만드는 것과 같다고 할 수 있기 때문에, 위와 같이 보통 main 함수 전에 선언합니다. 어떤 함수 안에 선언하면 그 함수 안에서만 사용할 수 있기 때문인데, 만약 main 함수 안에 선언한다면 main 함수 안에서만 사용할 수 있음.

struct 구조체 이름 {구조체 멤버들};
#include <stdio.h>

struct student
{
	char name[15];
	int s_id;
	int age;
	char phone_number[14];
};

int main()
{
	struct student goorm; // struct student까지가 int와 같은 자료형을 의미함.
	
	printf("이름 : ");
	scanf("%s", goorm.name);
	printf("학번 : ");
	scanf("%d", &goorm.s_id);
	printf("나이 : ");
	scanf("%d", &goorm.age);
	printf("번호 : ");
	scanf("%s", goorm.phone_number);
	
	printf("이름 : %s, 학번 : %d, 나이 : %d, 번호 : %s\n", goorm.name, goorm.s_id, goorm.age, goorm.phone_number);
	
	return 0;
}

사용할 변수들은 안에 묶어서 적어주는데, 이 변수들을 구조체 멤버라고 함.

선언한 것만으로는 구조체를 바로 사용할 수 없고, main 함수 안에서 따로 선언을 해주어야 함. struct로 선언했던 구조체의 이름과 앞으로 사용할 변수 이름을 써주면 되는데, struct [구조체 이름] 까지가 int와 같은 자료형이라고 생각하면 됨.
따라서, 위 코드에서는 struct student goorm으로 선언했음. 이렇게 되면 goorm의 자료형은 student 구조체가 되는 것.

선언한 후에는 구조체 멤버를 사용함. 사용할 때에는 변수이름.구조체 멤버의 이름과 같은 형태로 작성하면 되고, 이렇게 구조체를 이용하면 묶어진 구조체 단위로 처리할 수 있기 때문에 가독성도 높아지고 관리하기 편함.

문자열에 11글자를 저장하기 위해서는 12칸짜리 배열이 필요함. 문자열의 마지막엔 NULL 값이 들어감.

구조체 멤버의 초기화

구조체 멤버의 값을 main에서 선언을 할 때 대입해서 초기화 할 수 있습니다. 초기화 할 때에는 멤버 연산자 .와 중괄호를 사용함.

구조체는 배열처럼 멤버 전체를 초기화 할 수도 있고, 원하는 변수만을 초기화할 수도 있음.


#include <stdio.h>

struct student
{
	int age;
	char phone_number[14];
	int s_id;
};

int main()
{
	struct student goorm = { .age = 20, .phone_number = "010-1234-5678", 10 };
	struct student codigm = { 22, "010-8765-4321"};

	printf("나이 : %d, 번호 : %s, 학번 : %d\n", goorm.age, goorm.phone_number, goorm.s_id);
	printf("나이 : %d, 번호 : %s, 학번 : %d\n", codigm.age, codigm.phone_number, codigm.s_id);

	return 0;
}

초기화 할 때에는 {.멤버이름=값} 과 같은 형태로 초기화 할수도 있으며, 멤버이름을 적지 않고 초기화 할 수도 있습니다. 멤버 이름을 적지 않을 떄는 구조체를 정의했던 순서대로 값이 들어가고, 값을 따로 넣어주지 않은 멤버는 0으로 초기화 됨.

typedef를 이용한 구조체 선언

구조체를 선언할 때는 struct 구조체이름 {구조체 멤버들}; 형태로 선언하고, main 함수에서 struct [구조체 이름] [변수 이름]과 같이 선언한 후 사용함.

하지만 typedef 키워드를 사용하면 구조체 선언을 조금 더 쉽게 할 수 있음. typedef를 사용할 떄는 구조체 이름을 생략할 수 있음.

typedef

typedef키워드는 C언어에서 자료형을 새롭게 이름을 붙일 때 쓰는 키워드입니다. typedef를 이용하면 main 함수에서 구조체를 선언할 때 매번 struct를 써줄 필요가 없음.
이 typedef를 사용할 때에는 구조체 별칭이 필요한데, 구조체 별칭은 구조체를 정의할 때 중괄호 뒤에 써주면 됨.

#include <stdio.h>

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

int main(){
	Student goorm;
	
	printf("나이 : ");
	scanf("%d", &goorm.age);
	printf("번호 : ");
	scanf("%s", goorm.phone_number);
	
	printf("----\n나이 : %d\n번호 : %s\n----", goorm.age, goorm.phone_number);
	
	return 0;
}

위와 같이 typedef와 별칭을 써주면 main 함수에서 struct [구조체이름]을 써줄 필요 없이 별칭만 써도 구조체 선언이 가능함. 구조체 별칭은 구조체 이름과 동일하게 써주어도 무관하지만, 일반적으로 구조체 이름과 별칭을 둘 다 쓸 때는 구조체 이름 앞에 _를 붙여줍니다.

익명 구조체

typedef를 쓰면 구조체 이름을 쓰지 않고 별칭만 사용하는 것도 가능 함.

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

int main(){
	Student goorm;

구조체 배열

일반 변수에 배열이 있는 것과 동일하게 구조체에도 배열이 존재함. 학생 정보를 저장하는 구조체를 만들어서 수십 수백명의 정보를 저장해야 한다고 하면, 일반 변수처럼 구조체도 일일히 선언하는 것보다 배열을 쓰는 것이 훨씬 편리함.

#include <stdio.h>

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

int main(){
	Student goorm[3] = { {.name = "해리 포터"}, {.name = "헤르미온느 그레인저"}, {.name = "론 위즐리"} };
	
	goorm[0].age = 10;
	goorm[1].age = 10;
	goorm[2].age = 10;
	
	printf("이름 : %s / 나이 : %d\n", goorm[0].name, goorm[0].age);
	printf("이름 : %s / 나이 : %d\n", goorm[1].name, goorm[1].age);
	printf("이름 : %s / 나이 : %d\n", goorm[2].name, goorm[2].age);
	
	return 0;
}

구조체도 문자열은 선언할 때만 초기화 할 수 있으므로 먼저 선언과 동시에 초기화를 해주고, 나이는 선언 후에 값을 대입함.

구조체 포인터

int형 포인터는 int *ptr; 형식으로 선언했지만 구조체는 struct [구조체 이름]이 자료형이나 마찬가지이기 때문에 struct student *ptr;과 같이 선언해야함.

Student goorm;
Student *ptr;

ptr = &groom;

(*ptr).s_id = 1004;
(*ptr).age = 20;

온점도 연산자의 일종이기 때문에 *ptr.age를 이용하면 구조체가 아닌 포인터 변수를 구조체처럼 참조하려고 하기 때문에 오류가 발생함. 따라서, 매번 구조체 포인터를 사용할 떄마다 괄호를 써주려면 매우 귀찮으므로 ->라는 기호를 이용하면 괄호를 사용하지 않아도 알아서 주소로 찾아가서 구조체를 참조함.

ptr->age = 20;

중첩 구조체

구조체에서는 독특한 점이 있는데 바로 구조체 안에 구조체를 선언할 수 있다는 부분임.

#include <stdio.h>

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

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

int main(){
	Student Student;
	Teacher Teacher;
	
	Student.teacher.age = 30;
	Teacher.age = 40;
	
	return 0;
}

자기 참조 구조체

구조체는 자기 자신을 참조하도록 자기와 똑같은 타입의 구조체를 멤버로 가질 수 있음.

typedef struct {
	char name[15];
	int age;
	struct Student *ptr; 
} Student;

자기 참조 구조체는 연결 리스트트리를 만들 때 사용되는데, 이 연결리스트와 트리는 자료구조에서 나오는 내용임.

구조체와 함수

구조체 전달

구조체를 인자로 전달할 때에는 두가지 방법이 있음. 포인터로 전달하는 것과, 구조체 그대로 전달하는 것

포인터를 사용하지 않고 넘겨주게 되면 매개변수에 복사되는 것이므로 원본 값에는 영향을 끼치지 않음. 따라서, 어떤 변수의 값을 가져와서 출력을 한다거나, 원본 값을 수정할 필요가 없는 경우에는 포인터를 사용하지 않고 그냥 복사해서 넘겨주면 됨.

하지만 구조체는 여러가지 자료형을 묶어서 새로운 자료형으로 만든 것이기 때문에, 구조체 크기가 커질 수록 복사할 공간이 더 필요하게 됩니다. 따라서, 공간이 낭비되어 비효율적이기 떄문에, 매개변수로 구조체를 전달할 때에는 보통 포인터를 사용함. 값을 바꿀 필요가 없는 경우에도!

#include <stdio.h>

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\n", s.s_id, s.age);
}
int main()
{
	Student s[3];
	
	for(int i=0; i<3; i++)
	{
		scanf("%s", s[i].name);
		scanf("%d", &s[i].kor);
		scanf("%d", &s[i].eng);
		scanf("%d", &s[i].math);
		s[i].avg = (s[i].kor + s[i].eng + s[i].math) / 3.0;
	}

	for(int i=0; i<3; i++)
	{
		printf("%s %.1f", s[i].name, s[i].avg);
		printf("\n");
	}
	
	return 0;
}

0개의 댓글