C의 기본 문법

8 minute read

처음의 마음을 절대 잊지 말 것. 초심불망


진법 변환


C 기초 문법

자료형

char
short
int
long
long long
float
double
long double
signed
unsigned

연산자

대입 연산자
= : 치환

산술 연산자
+, -, *, /, &, ++, – : 사칙 연산

관계 연산자
<, >, <=, >=, ++, != : 논리합, 논리곱, 논리부정 두 식의 대소 크기를 비교하는 연산자로서 연산 결과가 참이나 거짓으로 표현한다. C 언어에서 참(true)은 0이 아닌 모든 수를 의미하며, 거짓(false)은 0을 의미한다.

논리 연산자
&&, ||, ! : 대소 및 항등 판정

할당 연산자
+=, -=, *=, /=, %=, …

삼항 연산자
? : 조건 판정

단항 연산자
++ : 1씩 증가
– : 1씩 감소
+, -

비트 연산자
&, |, ~, ^, «, »
연산자 우선순위는 고려되어야 하나, 괄호 등을 효율적으로 사용하면 연산자 우선순위를 모른다고 해서 크게 문제될 부분은 없습니다.

포인터 연산자
& : 메모리의 주소를 구한다.

  • : 포인터 변수가 갖는 주소에 저장된 값을 구한다.

메모리와 포인터

변수 : 변수의 본질은 메모리이며 모든 메모리는 자신의 위치를 식별하기 위한 근거로 고유번호(일련번호)를 같는데, 이 번호를 메모리의 주소라고 합니다.

  • 이름에 부여된 메모리
  • 그 안에 담긴 정보
  • 메모리의 주소

#include <stdio.h>                              

int main(void) {
  // 임의 변경 가능한 메모리 주소의 특정 위치에 nData가 "지정"된다.
  int nData = 10;
  printf("%s\n", "nData");

  // 변수 nData에 들어잇는 값을 출력 
  printf("%d\n", "nData");

  // 변수 nData의 메모리 주소를 출력 
  printf("%p\n", &nData); // 0012FF28
  return 0;
}

위의 코드 프로세스를 아래의 이미지와 같이 주소값으로 조정


#include <stdio.h>

void main() {

  int nData = 300;
  return 0;
}

비주얼 스튜디오에서 메모리창으로 봤을 때,

메모리의 종류

  • Stack 자동변수이고 지역변수인 변수가 사용하는 메모리 영역이며, 임시 메모리의 성격을 가진다. 크기가 작고(기본 설정을 기준으로 최대 1MB 수준) 관리(할당 및 반환)가 자동으로 이루어지는 장점이 있다.
  • Heap 동적 할당 할 수 있는 자유 메모리 영역이며, 개발자 자신 스스로 직접관리(수동)해야 한다. 32비트 응용 프로그램의 경우, 대략 1xGB 정도를 사용할 수 있다. 따라서 대량의 메모리가 필요하거나 필요한 메모리의 크리를 미리 알 수 없을 때 사용한다.
  • PE Image
    • Text section C 언어의 소스코드가 번역된 기계어가 저장된 메모리 영역이며, 기본적으로 읽기 전용 메모리이다. 만일 어떤 식으로 이 영역의 메모리를 변조한다면, 해킹이라 할 수 있다.
    • Read Only 상수 형태로 기술하는 문자열이 저장된 메모리 영역이며, Text 영역처럼 읽기는 가능하나 쓰기는 허용되지 않는다.
    • Read/Write 정적 변수나 전역 변수들이 사용하는 메모리 영역이며, 별도로 초기화하지 않아도 0으로 초기화된다. 관리는 자동이라서 힙 영역 메모리처럼 할당 및 해제를 신경 쓸 필요는 없다.

간접지정


#include <stdio.h>

// Pointer의 기본 
void main() {
  
  // 간접 지정
  int nData = 300;        // 메모리 주소 : 0x0018FF28 - 값 : 2C 01 00 00
  int *pnData = &nData;   // 메모리 주소 : 0x0018FFIC - 값 : 28 FF 18 00
  // 여기에서 pnData는 Integer의 포인터로 Integer의 값 만큼 2개가 늘어나는 것 
  pnData += 2;            // 메모리 주소 : 0x0018FF28 - 값 : 30 FF 18 00 -- 해당 시점에 포인터 변수의 값이 변경됨. 


  *pnData = 300;          // 메모리 주소 : 0x0018FF30 - 값 : 2C 01 00 00 

  return 0;
}

Pointer의 기본 공식


#include 

void main(void) {
  int nData = 300;
  int *pnData = &nData;
  
  *((int*)0x0018FF28) = 600; // 직접 지정 
  *pnData = 600              // 간접 지정

}

간접지정은 변경될 수 있는 임의의 기준주소로 상대적이 위치(주소)를 식별하는 방식입니다. 만일 철수의 절친인 길동이는 늘 철 수 옆집에 살기로 했다고 생각해봅시다. 그렇다면 길동이 집주소를 말할 수 도 있지만, “철수 옆집이요”라고 할 수도 있습니다. 쉽게 생각해서 철수 집주소가 100번지면 길동이 집주소는 101번지가 되는 것입니다. 이처런 어떤 기준을 근거로 상대인 메모리의 위치를 설명하는 방법이 ‘간접지정’입니다.


#include <stdio.h>

int main(void)
{
  // int 형식 변수 선언 및 정의 
  int x = 10;

  // 변수 x가리키는 int형식에 대한 포인터 변수 선언 및 정의 
  int *pnData = &x;
  
  printf("x : %d\n",  x);

  // pnData 포인터 변수가 가리키는 대상 메모리를 int형 변수로 
  // 간접 지정하고 20을 대입한다. 
  // 현대 가리키는 대상 메모리는 x의 메모리이므로 x의 값이 20이 된다. 
  *pnData = 20;

  // *pnData - 포인터 변수 pnData에 저장된 주소를 가진 메모리를 int형 변수로 취급
  
  printf("x : %d\n" , x);

  return 0;
}

포인터를 이용해 다른 공간의 함수에 변수를 전달한다.


#include <stdio.h>

void assign(int *);

int main()
{
  int num = 0;

  assign(&num);

  printf("함수 호출 후의 num 값 : %d\n", num);

  return 0;
}

void assign(int * ip)
{
  *ip = 10; // 함수가 호출된 이후의 값은 10이다. 
}

포인터와 배열

배열을 이해하는 방식


#include <stdio.h>

int main(void) {
  int aList[5] = { 40, 20, 50, 30, 10 };
  int *paList = aList;

  paList + 1;			// 기준 주소 + 정수(옵셋) -> 상대주소 
  *(paList + 1);		// int 형 변수가 지정된다. 
  *(paList + 1) = 5;  // paList[1] = 5 과 동일하다. 
  paList[1] = 5;

  return 0;
}
      

배열의 이름은 0번 요소의 주소이며, 전체 배열을 대표하는 식별자입니다. 그리고 포인터 변수는 주소를 저정하기 위한 변수 입니다. 배열의 이름이 주소이므로, 포인터 변수에 저장할 수 있습니다.


int main(void)
{
  // int 배열 선언 및 정의, 배열의 이름은 연속된 각 요소들 중
  // 전체를 대표하는 0번째 요소에 대한 '주소 상수' 이다. 
  int aList[5] = { 0 };

  // int에 대한 포인터 변수를 배열의 이름으로 정의한다. 
  // int 형 배열의 이름을 "int에 대한 포인터 초깃값"으로 기술하였다. 
  int *pnData = aList; // int *pnData = &aList[0]
 

  /*
    조금 응용한다면 

    pnData = &aList[1];
    pnData = &aList[2];

    도 성립할 수 있다. 
  */


  // 배열의 0인 요소의 값을 출력한다. 
  printf("aList[0] : %d\n", aList[0]);

  // 포인터가 가리키고 있는 배열 0번 요소의 값을 변경하고 출력한다. 
  // 아래 코드에 의해서 간접 지정되는 대상 메모리는 aList 배열의 0번 요소입니다. 
  // "간접지정 연산자" 는 단항 연산이며, "pnData라는 포인터에 저장된 주소의 메모리를 int 형 변수로 보겠다는 의미"
  // "변수에 담긴 주소"를 간접적인 방법으로 지정했으니 간접 지정입니다. 
  *pnData = 20;
 
  /*
    위의 연산은 사실 *(pnData +0)를 의미하며, 이 코드의 의미는 포인터 변수 pnData에 저장된 주소를 기준으로 
    오른쪽으로 int 0개 떨어진 위치(주소)의 메모리를 int형 변수로 지정한다는 것입니다. 
    그리고 *(pnData + 0)를 다른 코드로 표시하면 정확히 pnData[0]입니다 
    배열과 포인터가 문법상 호환되는 이유는 개념적으로나 기술적으로나 사실상 같기 때문입니다. 
    단지 차이가 있다면 포인터 변수는 말 그대로 변수 이고, 배열의 이름은 '주소상수'라는 것 뿐입니다. 

  */

  printf("aList[0] : %d\n", aList[0]);


  return 0;
}


#include <stdio.h>

#include <string.h>

int main(void)
{
  // 문자 배열 (char[16]) 의 선언 및 정의 
  // 선언한 크기는 char[16]이지만 초기화는 char[6] 문자열로 한다. 
  char szBuffer[16] = { "Hello" };
  // 문자 배열을 가리키는 문자 포인터 변수의 선언 및 정의 
  char *pszData = szBuffer;
  int nLength = 0;

  // pszData 포인터 변수가 가리키는 대상에 저장된 char형 데이터가 
  // 문자열의 끝을 의미하는 NULL문자가 될 때까지 반복문 수행 
  while (*pszData != '\0') {
    // 포인터를 다음으로 한 칸 이동 시킨다. 
    // 해당 부분이 수행되면 pszData에 담긴 정보는 주소를 기준으로 1만큼 증가합니다. 이는 정확히는 
    // 오른쪽으로 char 형 크기 만큼 한 칸 이동(주소 증가)했기 때문입니다. 
    pszData++;
    nLength++;
  }

  //  strlen() 함수로 문자열의 길이 ( 바이트 단위 크기 ) 를 출력한다. 
  printf("Length : %d\n", nLength);
  printf("Length : %d\n", strlen(szBuffer));
  printf("Length : %d\n", strlen("World"));

  // 포인터는 변수이니다. 자신이 담고 있는 정보를 바꿔버리는 자체가 상대 주소 계산이 됩니다.
  // 그리고 동시에 새로운 기준주소로 변경됨을 의미하니다. 즉, 
  // 계산을 통해 얻은 상태 주소로 정보를 업데이트 하면 그것이 새로운 기준 주소가 되어 인접한 정보에 
  // 접근할 수 있습니다. 

  return 0;
}

컴파일러가 알려준 주소는 pszData 변수의 주소입니다. 그리고 그 안에 저장된 정보는 szBuffer입니다. 따라서 포인터 변수안에 담긴 정보는 szBuffer의 실제주소입니다. 이를 근거로 메모리 구조를 표현하면 아래와 같습니다.


#include <stdio.h>

int main(void)
{
  char szBuffer[16] = { "Hello" };
  char *pszData = szBuffer;
 
  /*
    pszData의 주소는 최초 기준 주소에서 계속해서 증가했습니다. 
    따라서 반복문을 실행한 후, pszData에 담긴 주소는 szBuffer 보다 크거나 같습니다. 그리고 이 가정에 의해서
    아래의 코드와 같이 연산하면 문자열의 끝인 '\0'이 저장된 배열 요소의 인덱스를 알 수가 있는데 
    이 인덱스와 문자열의 길이가 일치 합니다. 

    문자열 본질에 대한 기본 이해, 상대주소에서 기준주소를 빼 역으로 인덱스를 계산하는 방법, 배열의 인덱스가 0에서 시작할 수 밖에 
    없는 이유 등에 대해서 기술적 근거를 들었다고 할 수 있습니다. 
  */
  while (*pszData != '\0')
  {
    pszData++;
  }

  printf("Length : %d\n", pszData - szBuffer);

  return 0;
}

메모리 동적 할당 및 관리

  • malloc() 과 free()

void ptrmalloc01() {
  int *pList = NULL, i = 0;

  // sizeof(int) * 3 의 결과는 int형이 4 바이트 이므로 12 입니다. 
  // malloc의 함수 반환 자료형은 void* 이기 때문에 int*로 강제 형변환 처리 
  // void * 에서 void는 길이도 해석방법도 없다는 의미 
  // void * 의 본질이 포인터인 것은 맞지만 , 이 주소가 가리키는 대상 메모리를 어떤 형식(자료형)으로 해석할지는 아직 결정되지 않았음을 의미합니다 
  pList = (int*)malloc(sizeof(int) * 3);

  pList[0] = 10;
  pList[1] = 20;
  pList[2] = 30;

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

  // 동적 할당한 메모리를 반환합니다. 
  // 만약 개발자의 실수로 아래의 처리를 안할 경우, 메모리가 사용할 수 없는 상태가 되는 것을 '메모리 누수(memory leak)'라고 부릅니다. 
  free(pList);

  return 0;
}             

메모리 초기화 및 사용


void prtmeminit01() {
  int *pList = NULL, *pNewList = NULL;

  // A. int 형 3개 배열 선언 및 정의   ( 0 초기화 )
  int aList[3] = { 0 };

  // B. int 형 3개를 담을 수 있는 크기의 메모리를 동적으로 할당한 후 
  // 메모리를 모두 0으로 초기화 
  // malloc() 함수가 반환한 메모리의 기준 주소를 시작으로 동적 할당된 크기만큼 모두 0으로 초기화 합니다. 
  // sizeof(int) * 3 이라고 쓰면 동적 할당한 메모리가 int[3]의 형식으로 사용될 가능성을 암시할 수 있습니다. 
  pList = (int*)malloc(sizeof(int) * 3);
  memset(pList, 0, sizeof(int) * 3);

  // C.  int형 3개를 담을 수 있는 메모리를 0으로 초기화 한 후 할당 받음 
  pNewList = (int*)calloc(3, sizeof(int));

  // 동적 할당한 메모리들을 해제 
  free(pList);
  free(pNewList);

  return 0;
}

void ptrmeminit02() {
	// 선언할 배열 요소의 크기를 기술하지 않았지만 초기값을 기준으로 
	// 자동으로 크기가 결정된다. 
	char szBuffer[] = { "Hello" };

  // "Hello" 문자열이 저장된 메모리의 주소로 초기화되는 포인터 변수 
  /*
    11번 행은 char형에 대한 호인터 변수의 선언 및 정의 입니다. 
    그러나 여기 기술된 문자열 "Hello"는 위의 행의 코드와는 다릅니다. 위에는 지연변수인 szBuffer 배열이 사용하는 스택 메모리의 초깃값이 되는 것이지만, 
    아래는 데이터 영역 중 읽기 전용 어딘가에 "Hello"라는 문자열이 저장되고 첫 글자인 'H'가 저장된 기준주소가 포인터의 초깃값이 됩니다. 
    메모리를 동적할당하는 것이 아닌데 동적 할당으로 잘못 생각할 수 있습니다. 
  */
	char *pszBuffer = "Hello";

	// 동적 할당된 메모리의 주소가 저정도리 포인터 선언 및 정의 
	char *pszData = NULL;

	// 메모리를 동적으로 할당하고 "Hello" 문자열로 초기화 
	pszData = (char*)malloc(sizeof(char) * 6);
	pszData[0] = 'H';
	pszData[1] = 'e';
	pszData[2] = 'l';
	pszData[3] = 'l';
	pszData[4] = '0';
	pszData[5] = '\0'; // 문자열의 끝을 의미하는 NULL 문자 

	puts(szBuffer);
	puts(pszBuffer);
	puts(pszData);

	// 동적 할당 메모리 해제 
	free(pszData);
	return 0;
}