C (프로그래밍 언어)

C1972년에 데니스 리치에 의해 UNIX를 개발하기 위하여 만들어진 프로그래밍 언어이다.

UNIX 시스템의 창시자라고 할 수 있는 켄 톰슨에 의하면, C 언어와 UNIX를 만들고자 했던 결정적인 동기는 당시 그들이 몸담고 있던 벨 연구소의 직원들이 게임덕후였기 때문에 컴퓨터의 종류나 연식에 관계없이 자기들이 하던 게임을 돌리고자 했기 때문이라고 한다.

1 특징[편집]

절차적 프로그래밍
최근에 나온 함수형, 객체지향 프로그래밍 언어와는 달리 초기에 나온 절차적 프로그래밍 언어이다. 프로그래머에 따라 객체지향처럼 쓸 수도 있지만…….[1]
강 타입언어
C 언어는 강 타입 언어지만 부동소수점 실수형과 정수형의 상호변환은 암시적으로도 가능하다.
포인터를 이용한 저수준의 메모리 접근
C 언어의 알파이자 오메가. C 언어가 하드웨어에 가깝다고 평가받는 요소이다. 사람들이 C 언어를 약 타입 언어 같다고 느끼는 것도 포인터 때문인 경우가 많다.
중괄호를 이용한 스코프 지정
기존의 프로그래밍 언어보다 C 언어를 사용한 코드가 간결해진 이유. 후대 언어에게도 영향을 미쳤다.
컴파일 언어
현대(2000년도 이후)에 주류로 사용되는 프로그래밍 언어(Java, C#, Objective C, Python 등) 중 몇 안 되는 컴파일 언어이다.

2 장점[편집]

  • 사용자가 프로그램의 모든 부분을 관리할 수 있다.

모든 것을 사용자에게 맡긴다. 다른 프로그램이라면 컴파일 오류를 내뱉을 만한 구문이라도 C에서는 정상적으로 실행된다.

  • 익혀야 할 문법의 양이 굉장히 적다. 그리고 피해야 하는 함정들이 굉장히 많다.
  • 비트, 바이트 단위의 데이터 관리가 편리하다. 다른 언어들의 경우 비트, 바이트 단위의 데이터 접근에 상당한 제약이 있는 경우가 많다.
  • 이식성이 뛰어나다. 하드웨어가 바뀌고 OS가 바뀌어도, 해당 플랫폼의 컴파일러로 바꾸어 주고, 플렛폼 종속적인 코드만 재구현 하고 컴파일하면, 멀쩡하게 돌아간다. 절대 쉽다고는 안했다...
  • 다른 언어에 비하여 컴파일러의 제작이 비교적 쉬우며, 코드의 투명성이 높다. 그래서 새로 개발되는 프로그램 가능한 칩들은 가장먼저 C컴파일러부터 구현한다.

3 단점[편집]

  • 사용자가 프로그램의 모든 부분을 관리해야만 한다.
위에서 적은 장점은 곧 단점이기도 하다. 배열의 범위를 벗어나도 컴파일러는 오류를 내뱉지 않는다. 사용자가 메모리를 꼼꼼하게 관리하지 않으면 메모리 누수가 발생한다.
  • 문법은 단순하지만 언어 사용 기술은 단순하지 않다.
문법의 종류가 적다고 코딩이 쉬운 것은 아니다. 문법에서 지원하는 기능이 몇 개 없다는 건 바꿔 말하면 지원하지 않는 기능을 모두 손수 구현해서 사용한다는 뜻이 된다. 더불어 포인터와 관련되어 개발된 각종 트릭들은 C 언어를 본격적으로 사용할 때의 난이도를 대폭 상승시킨다.
  • 상당히 보수적인 언어다.
물론, C에도 최신 규격인 C99나 C11이 있다. 다만 C99나 C11 등 최신 규격이 발표됨에도 사람들이 대부분 ANSI C나 C89/90을 고집하고, C11에 이르러선 아예 있는지도 모르는 이유는 컴파일러 호환성 문제 등이 있기 때문이다. 기실, 현재까지도 생산성이 높지 않은 C를 쓴다는 것은 C를 쓸만한 당위성이 있는 경우라는 것인데, 이런 경우가 대부분 저수준 임베디드 같은 컴파일러 호환성이 낮은 분야나, 아니면 성능에 굉장히 민감해서 사람이 수동으로 제어를 해주는게 더 나을 수 있는 경우(거의 없어지기는 했다) 정도인데, 이런 분야에서 굳이 C99나 C11에 들어간 기능들이 추가적으로 더 필요하지도 않고, 보수성만 더 나쁘게 만들기 때문이다. 이런 점에서 ANSI C 등은 기준이 명확하고, 과거에 쭉 써온 많은 코드와 라이브러리, 인력 풀 등이 남아있기 때문에 C는 오히려 최신 규격보다 과거 규격이 훨씬 더 안정적이라 여겨지는 것이다. 하지만 C++과 같이 생산성도 중시하는 언어 같은 경우는 덜 보수적이다보니 최신 규격의 채용에 있어서도 C에 비해 훨씬 더 적극적인 경우가 많다.

4 버전[편집]

4.1 K & R[편집]

국제 표준이 나오기 이전의 C문법이다, C 개발자가 사용했던 문법이다.

4.2 ANSI C[편집]

국제 표준으로 등록된 문법 및 라이브러리며, 현재까지도 대다수의 C프로그래머들은 이 표준을 사용한다. C99,C11은 장식이다 소스코드 정적 분석을 하는 사람들도 대상 코드를 ANSI C로 잡는 경우가 대다수라서 앞으로도 대격변이 있지 않은 이상, 이 표준 이외의 표준이 주류가 되기는 어려울 것으로 보인다.

4.3 C99[편집]

그동안 발전이 뒤쳐져서 새로운 표준이 등록되었다. (ISO-IEC-9989) 진위형(bool)이나 인라인 함수, 변수 선언 제약의 완화등이 있으나, VC에서 지원하지 않아 잊혀졌다. gcc의 경우 c99 표준 명세를 대부분 지원하며, -std=c99 옵션을 통해 적용 가능하다.

ANSI C에비해 아래와 같은 기능이 추가되었다.

4.3.1 inline function 추가[편집]

 
inline int func(int n){
    return n + 2;
}

인라인 함수는 C++에서는 일반적으로 사용하는 문법적으로 문법적으로는 함수의 형태를 띠고 있지만, 컴파일할 때 콜스텍을 이용하여 호출하는 함수와 달리 인라인 함수의 내용을 해당 함수가 호출된 위치에 치환하는 형태로 동작한다. ANSI C에서는 이러한 기능을 제공하지 않아. 아래와 같은 매크로 함수를 사용하여 해당 기능을 사용했었다.

#define func(n) ((n) + 2)

그러나 이러한 방식의 매크로함수는 지정된 라인을 소스코드에 직접 치환하는 방식이기 때문에, 예상치 못한 에러를 유발할 가능성이 높아 사용이 까다롭다. 인라인 함수는 자신의 내용을 소스코드에 치환하는 것은 동일하나 코드 치환시 발생할 수 있는 문제에 대한 안전장치를 가지고 있다.

4.3.2 변수의 선언이 블록의 처음에 제한되지 않음[편집]

ANSI C의 경우 모든 변수는 반드시 블록의 처음부분 (다른 함수가 호출되기 이전)에 선언하지 않으면 컴파일 에러가 발생하였다. C99에서는 이러한 제한을 없애고, 블록의 어디에서든 변수를 선언하여 사용할 수 있도록 하였다.

int foo()
{
  printf("test");
  int a = 0;
  return a;
}

4.3.3 복소수를 나타내기 위한 complex 자료형 등 새로운 자료형 도입[편집]

아래와 같은 형태로 복소수의 표현과 연산이 가능하다.

#include <complex.h>  
int foo() 
{
   double complex x1 = 1.0 + 3.0 * I;
   double complex x2 = 1.0 - 4.0 * I;

   printf("x1 = %.2f + %.2fi\tx2 = %.2f %+.2fi\n",
          creal(x1),
          cimag(x1),
          creal(x2),
          cimag(x2));
}

4.3.4 가변 길이 배열(VLA: variable-length array)[편집]

ANSI C에서는 배열의 선언시 배열 길이를 반드시 상수로만 사용할 수 있었다. C99에서는 이러한 제한이 사라져 가변길이 배열의 선언이 가능하다.

 int arr1[123]; //ansi C에서 정상동작
 int length = 10;
 int arr2[length]; // ansi C에서 에러 c99에서 정상동작

4.3.5 inttypes.h stdbool.h 와 같은 헤더 지원[편집]

inttypes.h : 플랫폼에 따라 사이즈가 달라지지 않는 자료형을 제공한다.
stdbool.h : 표준 bool 타입을 제공한다.

사실 C99 에 새로 탑재된것은 _Bool 타입이다. 하지만, stdbool.h 를 include 할 경우, 그냥 bool 로도 사용이 가능해진다. 왜 이런식으로 했냐면, 기존 표준에 bool 이 없었기때문에 이 이름을 사용한 코드가 있을 경우를 고려하여, 기존 표준에서 사용하지 말라고 했던 밑줄+대문자로 시작하는 이름인 _Bool 로 만들어놓았고, 여기에 해당되지 않는 경우는 보다 평범한 이름인 bool 로 사용하라고 stdbool.h 를 추가한 것. stdbool.h 을 사용할 경우 추가적으로 true 와 false 도 사용이 가능하다.

4.3.6 지정된 이니셜라이저 지원[편집]

배열의 초기화가 아래와 같이 이루어질 수 있다.

 int a[6] = { [4] = 29, [2] = 15 }; 
 int b[6] = { 0,0,15,0,29,0 };

4.3.7 가변인수 매크로[편집]

define 매크로 함수에 가변인자가 들어갈 수 있게 되었다.

 #define FN(a,...) printf(a,_VA_ARGS_);

4.3.8 컴파운드리터럴[편집]

복합상수의 지정이 가능해졌다.

 int *p = (int [3]) {1,2,3};

4.4 C11[편집]

C++기술위원회와 협의하여 C++11와 같이 나온 표준이다. 역시 C++11처럼 표준 스레드 라이브러리등이 추가 되었으나, 추가 되었는지도 모르는 사람이 태반이다.

5 문법[편집]

  • 위키문서에서 C언어의 모든 문법을 다루기는 어렵다. 그러니까 책을 사라 따라서 본문에서는 개발에 필수적인 항목만을 간단하게 다룬다.
  • C언어로 입문을 하고싶다면 이 글을 보는게 더 도움된다. 시리즈:쉽게 배우는 프로그래밍 입문/C

5.1 변수[편집]

  • 데이터를 저장하기 위해 할당하는 메모리 공간이다. 할당과 해제를 컴파일러가 관리하기 때문에 개발자가 메모리 관리를 신경쓰지 않아도 된다.
  • 자료형은 아래와 같다.
  • 저장 가능한 수의 범위는 2를 자료형의 비트 수로 제곱하면 나온다. short의 비트는 16비트 216=65536 양수 음수로 나누면 -32768 ~ 32767 이 범위에 들어가는 숫자만을 저장할 수가 있다. short 앞에 unsigned를 입력하면 0에서 65535에 해당하는 숫자를 저장할 수 있는 자료형이 선언된다.
  • char: 하나의 문자를 저장한다. 비트 수는 8비트로 1바이트그러면 글자 여러 개는 어떻게 저장하는데? 다 방법이 있음 아래에서 보셈
char c = 'a';
  • short : 작은 정수를 저장한다. 비트 수는 16비트로 2바이트
short x = 3;
  • int : 정수를 저장한다. 비트 수는 32비트로 4바이트 [2][3]
int x = 4;
  • long : 정수를 저장한다. 비트 수는 32비트로 4바이트 CPU마다 int와 long의 크기가 다르기도 하지만 대부분 같다.
long x = 9;
  • long long : 큰 정수를 저장한다. 비트 수는 64비트로 8바이트
long long x = 9;
  • float : 작은 실수를 저장한다. 비트 수는 32비트
float x = 3.141592;
  • double : 큰 실수를 저장한다. 비트 수는 64비트
double x = 3.141592597;

아래는 배열을 이용해 여러 개의 값을 한번에 저장하는 방법이다.

  • char x[] : 문자열을 저장한다. 대괄호 안에 숫자를 넣어서 들어갈 수 있는 문자 개수를 정해주기도 한다. 4를 입력하면 3개를 저장할 수 있다.
char x[] =  "Hello";

5.2 함수[편집]

  • Ctrl+CV 대신 써야 하는거
  • 반복적으로 쓰이는 코드를 짧은 키워드로 불러 쓸 수 있도록 하는 기능제대로된 정의와는 일억오천만광년정도 떨어져 있다.
  • 인자를 가지고 특정 어드레스 주소로 이동할 수 있도록 하는 기능. 스택 추가는 덤.
//----------------------여기서부터
void func(int a)
{
   printf("a : %d\n,"a);
}
//----------------------여기까지 함수
 int main()
 {
   func(1);  //괄호안에 값을 입력해서 호출
   func(2);
   func(3);
   return 0;
 }
 출력
 a : 1
 a : 2
 a : 3
void func(int a)
{
   printf("a : %d\n,"a);
}

void func(int a)처럼 괄호 안에 (자료형 변수명)을 입력해서 함수라는 기계에 들어갈 변수의 자료형이나 개수를 정해준다. void func(int b, char ch)처럼 쓰면 정수형 변수 b, 문자 정수형 변수 ch를 정해줄 수 있다.

void //func(int a)

void 얘는 뭘까 함수에서 숫자를 반환하지 않는다는 소리다. 여기다 int나 float를 적어주면 정수, 실수를 반환하는 함수가 된다.

6 연산자[편집]

  • + : 더하기
  • - : 빼기, 더하기와 빼기는 곱셈, 나눗셈, 나머지 연산보다 우선 순위가 낮다. 2+2*2는 8이 아니라 6이라는 거다.
  • *: 곱하기, x가 아니라 이 기호를 쓴다.
  • /: 나누기, 보통 쓰는 나누기 기호가 아니라 슬래시를 쓴다.
  • %: 나머지 연산. A%B라고 쓰면 A를 B로 나누었을 때의 나머지를 반환한다. 예를 들면 5%2 = 1 이런식으로. 당연히 만약 B가 A보다 더 크면 그냥 A를 반환한다.
  • =: 대입연산자. 일상에서 사용하는 등호와 방식이 조금 다르다. 좌항에는 반드시 대입이 가능한 변수가 있어야 하고, 우항에는 변수에 대입할 값이 있어야 한다.
  • +=, -=, *=, /=, %=: A+=B라 할 때, A = A + B와 같다. (수학적으로는 말도 안되지만 프로그래밍 언어의 =는 수학의 등호와는 다르다. 변수 A의 자리에 A+B의 값을 대입한다고 봐야한다.) 예를 들어서, a=1; a+=2;를 하면 a가 3이 된다. -=는 이하도 해당 연산자 버전으로 마찬가지이다.
  • ++, --: 각각 '+=1, '-=1'과 같다. 때문에 연산자 양쪽 모두 변수가 위치하지는 않는다. a++ 또는 ++a로 쓰는데, 적용되는 방식이 미묘하게 다르다.

6.1 조건문과 반복문[편집]

조건문과 반복문에 대하여 설명한다. 코딩을 하다 보면 상황에 따라 처리를 달리 해야 하는 것이나 반복 작업이 많기 때문에 자주 쓰게 될 것이다. goto는 사용하는 것이 권장되지 않고 실제로도 거의 쓰이지 않는다. 절차지향 프로그래밍의 핵심인 '절차'를 깨트리기도 하고, goto를 쓰면 코드 내에서 왔다갔다 해대니 코드 분석에도 어려움을 주기 때문.

6.1.1 if[편집]

  • 소괄호 안의 값을 확인하여 0이면 그냥 지나가고 0이 아니면, 블록 안의 코드를 실행하는 문법. 조건을 검사하는 기능같은거 없다.
  • 평범한 사용법
if(0)
{
  //실행 안 된다.
}
if(2312)
{
 // 실행된다.
}
  • 비범한 사용법
int a = 10;
if(a - 10)
{
  //a가 10이 아니면 실행된다.
}
  • 좀 더 비범한 사용법
    • C에서 1 == 1 -> 1이고, 1 == 2 -> 0 이다.
    • 1 < 2 -> 1이고, 1 > 2 -> 0이다. 어떻게 되먹은 산수지...? [4]
int a = 10;
if(a < 11)
{
 //a가 11보다 작으면 실행된다.
}

6.1.2 switch[편집]

여러 선택지에 따라 다른 동작을 수행하는 문법이다. 조건이 여러 개 붙어 있을 때 유용하다. 선택지로 int나 char형의 변수를 받을 수 있다. 다음은 switch문의 예시이다.

  • 정수형 선택지
#include <stdio.h>
int main(void){
  int no; //이 변수로 switch문을 제어한다.
  printf("어떤 치킨을 주문하시겠습니까?: \n1. 후라이드 2. 양념 3. 간장\n답 입력: ");
  scanf("%d",no);
  switch(no) //이렇게
  {
   case 1: printf("17000원이 결제됩니다.\n"); break; //1을 고르면 실행됨
   case 2: printf("18000원이 결제됩니다.\n"); break; //2를 고르면 실행됨
   case 3: printf("17500원이 결제됩니다.\n"); break; //3을 고르면 실행됨
   default: printf("번호를 다시 입력해주세요.\n"); //나머지 경우
  }
  return 0;
}

모든 case마다

break;

를 쓴 것을 알 수 있는데 만약 break를 써서 case에서 빠져나오지 않으면 그 case 아래의 내용도 모조리 실행되기 때문이다. default에는 break가 붙지 않았는데, 그것은 default문이 맨 아래에 있으므로 break문을 쓰지 않더라도 더 이상 실행될 것이 없기 때문에 굳이 붙이지 않아도 되기 때문이다. 만약 default를 쓰지 않는 switch문이라면 마지막 case에 break를 쓰지 않아도 된다.

6.1.3 while[편집]

while(조건식)
  • 조건식이 참(또는 값이 1)이면 반복한다.
  • 무한 반복
while(1)printf("당신의 CMD는 반달당했습니다?\n");

이 소스대로 작동시키면, 당신이 창을 닫지 않는 이상 printf에 있는 글로 도배가 될 것이다.

6.1.4 for[편집]

for(초기식;조건식;증가식)
  • 조건문 안에서 사용할 변수 정의, 반복 조건, 반복할 때만 실행할 코드까지 정의하기에 알맞다.
  • 단순 반복
int sum = 0;
for(int i = 1;i<=1000;i++)sum += i; // sum = sum + i

1부터 1000까지 더하는 코드이다.[5]

6.1.5 삼항연산자[편집]

if/else 문과 같은 용도로 쓰인다. 간단한 조건문의 경우 짧은 코드로 작성할 수 있다는 장점이 있지만 반면 가독성이 떨어진다는 문제가 있기 때문에 상황에 맞게 조건문과 삼항연산자를 쓰는 것이 좋다.

  • 참이면 앞에거, 거짓이면 뒤에거..
  • 소괄호 안의 값을 확인하여 0이면 뒤에 것을 실행하고 0이 아니면, 앞에 것을 실행한다.
(1==2) ? 실행안됨 : 실행됨

6.2 사용자정의변수[편집]

C언어에서는 typedef 키워드로 사용자 변수 타입을 정의할 수 있다. 즉 typedef int bool; 을 이용해 int와 같은 성질을 갖는 자료형을 bool 자료형의 이름으로 사용 가능하다. 또한 여러 자료형과 변수를 한 단위로 묶기 위한 구조체와 공용체, 상수의 집합을 보다 알아보기 쉽게 정의할 수 있는 열거형 또한 사용자 정의 자료형으로 취급한다.

  • 구조체
struct xxx_t {
  int index;
  int age;
  int id;
};
int main()
{
  struct xxx_t x;
  x.index = 1;
  x.age = 2;
  x.id = 3;
  printf("%d %d %d\n",x.index,x.id,x.age);
  return 0;
}
  • 공용체

공용체와 구조체의 가장 큰 차이점은, 구조체는 각 속성들의 메모리 영역이 독립적으로 나뉘어 있고 공용체는 모두 같은 공간을 공유한다는 점이다.

 union my_union{
   int age;
   char gender;
 };
 int main(){
   union my_union un;
   un.age = 20;
   printf("%d", un.age);
   un.gender = 'f';
   printf("%c", un.gender);
   printf("%d", un.age);
   return 0;
 }
  • 열거형

열거형은 위의 두 자료형과는 조금 다르며 정수 상수의 집합을 선언해 다음과 같이 프로그래밍 과정을 좀 더 쉽게 하기 위해 주로 사용된다. 예를 들면 다음과 같다.

 int main(){
   int day = 3;
   if(day == 0 || day == 6){
     printf("weekend!\n");
   }
   else{
     printf("weekdays!\n");
   }
   return 0;
 }

위와 같은 코드를 아래와 같이 바꿀 수 있다.

  enum daykind { sunday = 0, monday, tuesday, wednesday, thursday, friday, saturday};
  int main(){
    enum daykind day = thursday;
     if(day == sunday|| day == saturday){
      printf("weekend!\n");
    }
    else{
      printf("weekdays!\n");
    }
    return 0;
  }

위의 예시보다 이해하고 읽기 쉬움을 알 수 있다.

6.3 포인터[편집]

6.3.1 포인터[편집]

C언어를 배우면서, 조건문, 루프문등 기초적인 문법에 대해서 학습할때까지는 큰 장벽을 못 느끼다가, 포인터로 넘어오면서 C언어를 포기하는 경우가 많다. 그 이유는 앞에서의 문법들은 사실 사람의 말을 배우듯이 따라가면 되는 것이고, 또한 논리적인 추론능력만 충분하다면 컴퓨터를 처음 하는 사람들도 쉽게 배울 수 있다. 하지만, 포인터가 시작되면 거창하게 얘기하면 컴퓨터의 구조, 좁게 보면 메모리의 구조에 대한 이해와 C언어가 메모리를 어떻게 다루는가, 메모리 계층 구조가 어떻게 되는가에 대한 이해가 없다면 포인터의 개념을 이해하고 활용하는 데에 상당한 어려움을 겪게 될 것이다. 다음은 포인터를 사용한 간단한 예시이다.

#include <stdio.h>

int main()
{
  int a=5;
  int *pa;
  pa=&a;
  printf("%d %d %d",a,pa,*pa);
  return 0;
}

위의 예제에서 어떤 값이 출력되는지 알아보자. 우선 첫번째 값은 당연히 a가 출력될 것이다. 그러면 2, 3번째 값은 어떤 값이 출력되는가? 먼저, 2번째 값은 a가 저장된 주소가 출력된다. 여기에서 알 수 있듯이 포인터 변수 pa에 저장된 값이 a의 주소임을 알 수 있다. &a가 바로 a의 주소를 나타낸다. 왜 그런지는 다음 단락에 설명하겠다. 그리고 3번째 값은 a의 값인 5가 출력되는데, 왜 그런 것일까?

우선 주소 참조와 관련된 두 가지 연산자를 알아둘 필요가 있다. 이 둘은 이미 위에서 모두 나왔다. 그렇다, &과 *이다. 이 두 연산자의 사용법은, 변수명 앞에 이 두 연산자를 붙이는 것이다. 이 둘의 기능은 정반대라고 생각하면 되는데, &를 사용할 경우 결과는 그 변수가 저장되어 있는 주소가 되고, *를 사용할 경우 결과는 그 변수에 저장된 주소값에 위치한 변수에 저장된 값이 된다. 즉 간단히 말하자면 *의 경우 변수에 저장된 주소값을 통해 다른 변수를 참조하게 해주고, &의 경우 그 반대의 기능을 수행하는 것이다. 그래서 *을 참조 연산자, &의 이름을 역참조 연산자라고 부르기도 한다. 이제 포인터에 대한 기본적인 내용을 익혔으니 그 응용으로 넘어가 보자.

  • array를 이용한 string
#include <stdio.h>

int main()
{
  char str[20] = "hello? librewiki!\n";
  printf("%s",str);
  str[5] = '!';
  str[16] = '?';
  printf("%s",str);
  return 0;
}
  • pointer를 이용한 string
#include <stdio.h>

int main()
{
  char *str = "hello? librewiki!\n";
  printf("%s",str);
  str[5] = '!';
  str[16] = '?';
  printf("%s",str);
  return 0;
}

이 중에서 어느 프로그램은 정상적으로 작동하고 어느 프로그램은 런타임 에러가 걸리는지 이해 하기 힘들 것이다.

메모리의 구조는 크게 5개의 영역으로 나뉜다. CODE, DATA, BSS, HEAP, STACK 일단 이것을 두개로 쪼개면 컴파일시 결정되는 CODE, DATA, BSS(CODE는 프로그램의 코드, DATA,BSS는 전역변수와 문자열 static 변수들이 저장된다. 완전히 정확한 것은 아니지만 ROM영역이라 실행 이후에는 접근 권한이 없다고 생각 하면 된다.) 실행 하는 동안에(Run time동안에)결정되는 HEAP, STACK 그리고 (stack은 지역변수들이 저장되고, HEAP은 동적 메모리이다. 이곳은 RAM영역이다.) 일단, 첫번째 소스가 왜 작동이 잘되는 이유를 알아보자. 일단 문자열은 DATA영역에 컴파일이 되면서 먼저 기록되고, char array는 main함수 안에 있으므로 stack영역에 들어있다. char array는 실제 메모리의 연속된 덩어리 이므로 여기에 DATA영역에 있던 문자열을 그대로 복사하는 것이다. 그렇기에 실제 "hello? librewiki!\n" 라는 문자열은 지금 stack 과 data 영역 모두에 존재라게 된다. 그렇기에 str[5]= '!'; 이런식으로 접근하여 수정해도 아무런 오류가 없다.

하지만 두번째 소스에서 char *str = "hello? librewiki!\n"; 이 선언은 포인터 즉, DATA 에 있는"hello? librewiki!\n" 문자열의 주소만을 가져 온것이다. 실제 stack영역에은 DATA영역의 "hello? librewiki!\n" 주소만 있지, 실제 이 문자열 내용은 존재 하지 않는다. 그렇기에 str[5]= '!' 로 접근하는 것은 ROM영역을 침범한다는 뜻이므로 런타임 에러가 나는 것이다.

6.3.2 메모리의 동적 할당[편집]

메모리에는 CODE, DATA, BSS, HEAP, STACK이 있고, 이 중 CODE, DATA, BSS는 컴파일 과정에서 결정되고 HEAP, STACK은 실행 중에 결정되는 메모리라고 윗 문단에서 언급했다. 그런데 HEAP과 STACK 중에서도 사실 우리는 STACK만을 써왔다. HEAP 메모리에 접근하는 방법은 바로 이 문단에서 다룰 메모리의 동적 할당 뿐이기 때문이다. 그러면 메모리의 동적 할당 방법을 예시로 알아보자.

#include <stdio.h>
int main()
{
  int *ptr;
  ptr=(int *)malloc(sizeof(int));
  *ptr=4;
  printf("%d",*ptr);
  free(ptr);
  ptr=0;
  return 0;
}

malloc 함수를 통해 int 포인터인 ptr에 int 자료형의 크기만큼의 HEAP 메모리를 할당한다. malloc 함수 앞에 (int *)가 붙은 것을 알 수 있는데, malloc함수는 기본적으로 void 포인터 형태로 값을 반환하기 때문에 자료형을 casting해주는 것이다. Visual Studio C++ 2010까지는 꼭 이 casting을 해주어야만 컴파일이 되었으나, 그 다음 버전부터는 굳이 casting을 해주지 않아도 자동으로 변환해 준다. 하지만 코드의 호환성을 위해서라도 가급적 casting을 해 주자. 그리고 HEAP메모리에 할당하고 그 메모리를 다 사용한 후에는 꼭 할당을 해제해 주어야 하는데, 이 때 free 함수를 사용한다. 주의할 것이, free로 해제한 후에 포인터의 값이 자동으로 지워지지 않는다는 것이다. 즉, 할당을 해제한 이후에는 포인터에 저장되어 있는 주소값을 STACK 메모리의 주소로 인식하여 그 주소를 가리키게 된다는 뜻이다! 따라서, 예기치 못한 오류를 막기 위해서는 할당 해제 후 포인터 변수를 반드시 NULL 상태로 두어야 한다.

6.3.3 다중 포인터[편집]

다중 포인터는 포인터 변수의 주소를 가지고 있는 포인터이다. 다음은 다중 포인터의 사용 예시이다.

#include <stdio.h>
int main(void)
{
 int a=5;
 int* pa;
 int** ppa; //이중 포인터
 pa=&a; //포인터 pa에 a의 주소 저장
 ppa=&pa; //이중 포인터 ppa에 pa의 주소 저장
 printf("%d %d %d",*pa,*ppa, **ppa);
 return 0;
}

이 코드의 실행 결과는 어떻게 될까? 이미 우리는 윗 문단을 통해 첫 번째 값은 pa에 저장된 a의 주소를 통해 참조한 a의 값인 5임을 알고 있다. 다중 포인터에서도 똑같은 논리가 그대로 적용되는데, 2번째 값의 경우 ppa에 저장된 pa의 주소를 통해 참조한 pa의 값, 그러니까 a의 주소가 출력될 것이다. 3번째 값은 ppa에서 2번 참조한 것이 출력되는데, 이것은 2번째 경우에서 참조를 한 번 더 실행한 경우를 뜻하므로, ppa에 저장된 pa의 주소를 통해 pa를 참조하고, pa에 저장된 a의 주소를 통해 a를 참조하여 첫 번째 값과 같은 a의 값인 5가 출력될 것이다. 이를 통해 C++의 Call By Reference를 비슷하게 흉내내는것 또한 가능하다.

// 할당된 메모리 주소는 이해를 돕기 위한 임의의 값.
#include <stdlib.h>

int AllocMe(void **ptr, size_t sz)
{
  if(!*ptr) // 3. **ptr은 2번 *myInt 의 주소인 0x01를 가르키고 있으므로 이를 dereferencing 하면 *myInt가 가르키고 있는 값인 NULL값을 읽음.
  {
    *ptr = malloc(sz); // 4. *myInt의 주소의 값에 malloc(); 1.번의 myInt는 malloc()이 반환한 0x10주소를 저장. (메모리 주소 0x1에는 0x10이라는 값이 들어 있음)
    return *ptr ? 1 : 0; // 5. *ptr에 malloc()이 반환한 주소(0x10)가 들어있다.
  }
}

int main()
{
  int *myInt = NULL; // 1. NULL 포인터 - *myInt := 스택 메모리 0x01, 0x01에 저장된 값은 0 (NULL)
  if(AllocMe(&myInt, 8)) // 2. 1번의 포인터가 위치한 주소를 전달 (Call by value) 주소값인 0x01이 전달 
  {
    free(myInt); // 6. AllocMe()에서 할당한 메모리 주소(0x10)를 해제
    myInt = NULL; // myInt (0x1)의 값은 다시 NULL
    return 0;
  }
  return -1;
}

그러면 다중 포인터는 대체 어디에 쓰는가? 다중 포인터는 보통 다차원 동적 배열을 만들 때 쓴다. 이 다차원 배열중 흔히 볼 수 있는 예시는 프로세스 진입 지점인 int main()에서 찾을 수 있다.

#include <stdio.h>

int main(int argc, const char **argv)
{
  for (int i = 0; i < argc; ++i)
  {
    printf("%d: %s\n", i, argv[i]); // *(argv + 1)과 동일
  }
}

C언어에서 문자열(String)또한 단순 1차원 배열에 불과하지만 이런 배열을 포함하는 또다른 배열을 만드는 것이 가능하다. 이를 통해 char* 는 배열의 주소를 담고 있지만 이 배열의 주소를 담고 있는 또다른 포인터를 만듬으로써 2차원의 배열을 만드는 것이 가능하다. 이를 통해 3차원은 물론 그 이상의 다차원 배열을 만드는 것 또한 가능하다.

6.3.4 함수포인터[편집]

함수 포인터는 말 그대로 함수를 가르키는 포인터이며 이 값은 함수 심볼의 진입 주소가 된다.

주로 플러그인과 같이 동적 라이브러리에서 링킹 없이 동적으로 심볼만 가져다 쓰는 상황에서 주로 사용되며 C에서 객체지향을 흉내낼 때도 사용하는것이 가능하다.

문법은 일반 함수 선언과 비슷하다.

<반환 자료형> (*<함수 포인터 이름>)(<매개변수>);

예를 들어 void* MyFunction(int, void*) 꼴의 함수를 담을 수 있는 함수 포인터는 다음과 같이 선언할 수 있다.

void* (*MyFunction_ptr)(int, void*);
void* (*MyFunction_ptr)(int, void*) = &MyFunction;
MyFunction_ptr(0, NULL); // MyFunction(0, NULL)과 동일

6.4 매크로[편집]

#define <매크로 이름> <매크로 값(옵션)>

변수나 상수와 달리 매크로는 단순히 컴파일단계 중 전처리기에 의해 값이 단순히 치환된다. 예를 들어

#define SIZE 64;
int myArray[SIZE];

는 전처리기에 의해 SIZE 부분이 64로 치환되며 결과적으로 int myArray[64]; 가 된다.

매크로 값은 생략하는것이 가능한데 보통 생략하는 경우 해당 매크로는 0이 아닌 값(true)로 바뀐다. 이를 통해 다음과 같이 응용할 수 있다.

#define BUILD_PRINT_DEBUG_MESSAGE
...
#ifdef BUILD_PRINT_DEBUG_MESSAGE
printf("[DEBUG] Print Debug Message!\n");
#endif // BUILD_PRINT_DEBUG_MESSAGE

또한 매크로는 코드의 일부가 될 수 있다.

#include <stdio.h>

#define PROCEDURE(x) int x
#define PRINTHELLOWORLD printf("Hello World!\n");

#define BEGIN {
#define END }

PROCEDURE(main) // int main
(int argc, const char *argv[])
BEGIN // {
    PRINTHELLOWORLD; // printf("Hello World!\n);
    return 0;
END // }

7 각주

  1. 객체지향이라는 개념은 일부 프로그래밍 언어의 언어적 특징이기도 하지만, 기본적으로는 프로그래밍 패러다임의 한 종류이기 때문에 C와 같은 절차적 프로그래밍 언어로 비슷하게 구현하는 것이 가능하다. 대표적인 예로 gtk+ 라이브러리가 있다. C의 매크로 기능을 십분 활용하여 상속, 오브젝트 등을 구현하였다. 리눅스의 VFS(Virtual File System)는 모듈화/추상화의 좋은 예시이지 객체지향의 예는 아니다. 모듈화는 언어의 특징이라기 보다는 프로그래밍 방법론에 해당한다.
  2. int 자료형은 short, long, long long과는 다르게 CPU에서 한번에 처리할 수 있는 단위인 워드의 크기에 따라 달라지는데 16비트 CPU면 16비트 32비트 CPU면 32비트
  3. 그럼 64비트면 64비트임? 아마 32비트를 확장시킨 x86_64기 때문에 32비트를 유지하는가보다.
  4. 산수라기 보다는, 논리에 가깝다. 진실일때 1을 출력하고, 거짓일때 0을 출력한다고 가정하자, 1==2는 "1과 2가 같다"라는 말이므로 거짓이 된다. 그러므로 0을 출력한다. 반대로, 1==1은 1은 1이다라는 진실이므로 1을 출력한다. 그리고 1보다는 2가 크므로 진실, 1을 출력하고, 1이 2보다 큰것은 거짓이므로 0을 출력한다.
  5. 참고로, 고등학교 수학을 배웠다면 알겠지만 코드 길이나 속도나 1000*1001/2 를 계산하는 편이 빠르고 효율적이다.