개요

 

안녕하세요 피터입니다.

오늘은 C언어의 포인터(Pointer)에 대해 알려드리겠습니다.

 

포인터는 C언어를 배우는 많은 사람들이 어려워 하는 개념 중에 하나입니다.

동시에 C언어에서 가장 중요한 개념 중 한가지이기도 합니다.

 

사실 포인터(Pointer)라는 것은 하나의 데이터 타입(DataType)일 뿐이며 실제 값(Value)를 저장하는 대신에

값이 저장되어 있는 변수의 주소(Address)가 저장된다는 것만 기억하면 어렵지 않습니다.

 

 

문법(Syntax)

 

포인터 타입 정의 (Definition)

 

포인터 타입을 정의할 때는 기존에 정의된 데이터 타입 뒤에 * (Asterisk) 를 붙이면 됩니다. (애스터리스크 라고 발음합니다)

예를들면 다음과 같이 정의할 수 있습니다.

 

int*
char*
float*
double*
struct node*

 

기본형 데이터 타입 외에도 파생형 타입도 포인터 타입으로 만들 수 있습니다.

[C언어 강좌-4] 자료형 (DataType)

 

모든 포인터 타입의 변수에는 주소값이 저장되기 때문에 포인터 변수가 차지하는 메모리 크기는 모두 같습니다.

그렇기 때문에 원래 1 Bytechar 형의 데이터 타입도 char* 포인터 타입이 되면 4 Bytes의 크기를 갖게 되는 것입니다.

마찬가지로 8 Bytes의 크기를 차지하는 double의 경우에도 double* 포인터 타입이 되면 4 Bytes가 됩니다.

굉장히 중요하고 헷갈리기 쉬운 부분이니 잘 기억해두세요!

 

 

포인터타입으로 변수 선언 (Declaration)

 

포인터 타입으로 변수를 선언하는 방법은 기존의 변수 선언하는 방법과 동일합니다.

int* a; // or int *a;

 

이렇게 선언된 포인터 변수 a 는 4 Bytes 만큼 메모리 공간을 할당받게 되고 이 공간에 주소값을 저장할 수 있습니다.

일반적으로 변수를 할당하고 아무런 값을 셋팅하지 않으면 가비지(Garbage)값이 들어있기 때문에 초기화를 해줘야 합니다.

 

포인터 변수를 초기화 할 때는 보통 NULL (널) 값으로 초기화 합니다.

이것은 나중에 포인터 변수에 주소값이 할당되었는지 체크하기 위해서 아무런 주소값이 할당되지 않았다는 의미를 갖게됩니다.

따라서 포인터 변수에 접근(access)하기 위해서는 반드시 NULL 인지 체크하는 루틴이 필요합니다.

 

int* a = NULL;
if(a != NULL)
  printf("%d", *a);

NULL이 들어있는 포인터 변수에 접근하게 되면 NullptrException 이라는 무시무시한 에러를 만나실 수 있으니 이 부분은 습관처럼 익숙해지셔야 합니다.

 

NULL은 C에서 일반적으로 0으로 define 되어 있는데 메모리 주소 체계에서 0번지는 시스템 영역이므로 이러한 접근을 차단하기 위해서 Exception(예외)을 발생시키는 것입니다.

 

포인터 변수에 접근할 때에는 당연하게도 주소를 다루기 때문에 일반적인 변수와는 조금 다르게 취급해야 되겠죠?

포인터 변수에 값을 저장하거나 불러올 때는 아래와 같이 특별한 연산자가 사용됩니다.

 

 

& 연산자 (Operator &)

 

int b = 100;
a = &b;

& (Ampersand) 연산자는 변수 앞에 붙는 단항연산자입니다.

(앰퍼샌드라고 발음합니다)

이 연산자는 뒤에오는 변수의 주소값을 반환합니다.

따라서 위 코드의 의미는 int 형 변수 b 의 주소를 얻어서 포인터 변수 a 에 저장한다는 의미입니다.

 

 

* 연산자 (Operator *)

 

printf("address: %d, value: %d\n", a, *a);

* 연산자는 & 연산자와 마찬가지로 단항연산자이지만 정반대의 동작을 수행합니다.

뒤에 오는 포인터변수가 가리키고 있는 주소에 저장되어 있는 실제 값(Value)를 반환합니다.

 

따라서 포인터 변수 a에 int 형 변수 b의 주소가 저장되어 있다면 *a 는 b에 저장되어 있는 값을 의미합니다.

 

ex) 포인터 변수 a 에 int 형 변수 b의 주소를 저장한 뒤 주소값과 실제값을 출력하는 코드

int* a = NULL; // or int *a;
int b = 100;
a = &b;

printf("address: %d, value: %d\n", a, *a);

 

참고

 

Call by reference

 

이후 함수(function) 포스팅에서 자세히 다루게 될 내용이지만 간략하게 소개드리겠습니다.

함수의 매개변수(Parameter)를 포인터 타입으로 사용하게 되면 해당 함수의 외부에서 선언된 변수에 직접 접근하여 값을 저장할 수 있습니다.

따라서 Call by value 방식과는 다르게 함수 호출이 끝나고 return 되어도 포인터 타입으로 선언된 매개변수를 이용해 저장된 값은 그대로 유지가 된다는 의미입니다.

 

배열과 포인터 (Pointer and Array)

 

[C언어 강좌-10] 배열 (Array)

이전 포스트에서 배열의 이름만 쓰게 되면 첫 번째 요소의 주소의 의미가 된다고 말씀드린 적이 있습니다.

 

int cost[10];

즉, 위와 같이 선언된 배열이 있다고 한다면 cost 라고만 쓰면 &cost[0] 과 의미가 같다는 뜻입니다.

내부적으로 배열의 인덱스 연산자 [ ] 도 첫 번째 요소의 포인터 값에 [ ] 안에 들어있는 숫자 만큼 더한다는 의미이기 때문에 배열과 포인터는 매우 밀접한 관련이 있다고 할 수 있습니다.

 

 

-Peter의 우아한 프로그래밍

여러분의 공감과 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

 

 

블로그 이미지

친절한 Peter Ahn

IT 정보 공유, 프로그래밍 지식 공유

댓글을 달아 주세요



안녕하세요 피터입니다.

오늘은 C언어에서 사용되는 연산자(Operator)에 대해서 알려드리겠습니다. 

연산자란 컴파일러에게 특정한 수학적 or 논리적 동작을 수행하도록 알려주는 기호(Symbol)입니다.  C언어에는 아래와 같은 다양한 연산자들이 제공되고 있습니다. 

연산자의 종류별 설명과 쓰임새에 대해 다루고 각 연산자들이 갖는 우선순위에 대해 설명드리겠습니다.


1. 연산자 종류

1.1. 산술 연산자 (Arithmetic Operator)

가장 익숙한 산술 연산자부터 살펴보겠습니다. 산술 연산자는 말 그대로 수학적인 계산을 하는데 필요한 연산자들입니다. 

C언어에서는 다음과 같이 사칙연산을 포함한 다양한 산술 연산자를 제공합니다. 

예제는 A = 10, B = 20 일때 결과입니다.

 연산자

설명 

 예제

 +

 두 값을 더함.

 A + B = 30 

 -

 좌항의 값에서 우항의 값을 뺌.

 A - B = -10 

 *

 두 값을 곱함. 

 A * B = 200 

 /

 좌항의 값을 우항의 값으로 나눔.

 B / A = 2

 %

 좌항의 값을 우항의 값으로 나눈 나머지 값.

 B % A = 0

 ++

 숫자값을 1만큼 증가.

 A++ (or ++A) = 11

 --

 숫자값을 1만큼 감소. 

 A-- (or --A) = 9


1.2. 관계 연산자 (Relational Operator)

관계 연산자는 비교 연산자라고도 하는데요 수학에서 부등호를 떠올리시면 이해가 빠르실 겁니다.

예제는 A = 10, B = 20 일 때 결과입니다.

 연산자

설명 

 예제

 ==

 좌항의 값과 우항의 값이 같은지 확인.

 (A == B) is False 

 !=

 좌항의 값과 우항의 값이 다른지 확인.

 (A != B) is True

 >

 좌항의 값이 더 큰지 확인.

 (A > B) is False 

 <

 우항의 값이 더 큰지 확인.

 (A < B) is True

 >=

 좌항의 값이 크거나 같은지 확인.

 (A >= B) is False

 <=

 우항의 값이 크거나 같은지 확인.

 (A <= B) is True


1.3. 논리 연산자 (Logical Operator)

논리 연산자는 주로 조건문(if)이나 반복문(while, for) 등에서 여러 개의 조건절을 처리할 때 사용합니다. 

결과는 Boolean 값으로 True 또는 False 값을 가집니다.

예제는 A = 1, B = 0 일 때 결과입니다.

 연산자

설명 

 예제

 &&

 논리곱. 두 값이 모두 True인 경우에만 True.

 (A && B) is False

 ||

 논리합. 두 값중 하나라도 True이면 True.

 (A || B ) is True

 !

 논리부정. 단항연산자이며 반대값을 취함.

 !(A && B) is True


1.4. 비트 연산자 (Bitwise Operator)

비트연산자는 좌항과 우항에 대해서 비트 단위로 처리해서 결과를 계산하도록 하는 연산자입니다. 

주로 옵션을 저장하는 Flag 값을 셋팅할 때나 반대로 Mask를 적용해서 특정 옵션값을 추출할 때 사용합니다. 

비트단위 연산은 처리 속도가 빠른 편이기 때문에 잘 활용할 수 있다면 성능 향상을 기대해볼 수 있습니다. 

예제는 A = 60, B = 13 일 때 결과입니다.

이해를 돕기 위해 2진법으로 표기하였습니다. 

A = 0011 1100

B = 0000 1101

-----------------

A&B = 0000 1100

A|B = 0011 1101

A^B = 0011 0001

~A = 1100 0011


 연산자

설명 

 예제

 &

 좌항과 우항을 비트 단위로 논리곱 연산을 수행. 결과값은 둘 다 1일 때 1, 나머지 경우는 0이 된다.

 (A & B) = 12, i.e., 0000 1100

 |

 좌항과 우항을 비트 단위로 논리합 연산을 수행. 결과값은 둘 다 0일 때 0, 나머지 경우는 1이 된다.

 (A | B) = 61, i.e., 0011 1101

 ^

 좌항과 우항을 비트 단위로 배타적논리합 연산을 수행. 결과값은 둘 다 0이거나 1이면 0, 두 값이 다르면 1이 된다.

 (A ^ B) = 49, i.e., 0011 0001

 ~

 단항 연산자로 모든 비트를 반전시킨다. 0은 1로 1은 0으로. 비트단위 논리부정. 부호비트가 바뀌기 때문에 음수가 된다. 

 (~A ) = -61, i.e,. 1100 0011

 <<

 비트단위 이동 연산자로 좌항의 값을 우항의 수 만큼 왼쪽으로 이동.

 A << 2 = 240 i.e., 1111 0000

 >>

 비트단위 이동 연산자로 좌항의 값을 우항의 수 만큼 오른쪽으로 이동. 

 A >> 2 = 15 i.e., 0000 1111


아래와 같이 비트 연산자 &, |, ^ 에 대한 결과를 함께 비교해보면 이해가 좀 더 쉽게 되실 겁니다.

 p

 q 

 p & q

 p | q

 p ^ q

0

0

 0

0

0

0

1

 0

1

1

1

1

 1

1

0

1

0

 0

1

1


1.5. 대입 연산자 (Assignment Operator)

대입 연산자는 기본적으로 우항의 값을 좌항에 대입할 때 사용합니다. 

다음과 같이 산술 연산자나 비트 연산자 등과 결합하여 축약형으로 사용할 수 있습니다.

 연산자

설명 

 예제

 =

 우항의 값을 좌항으로 대입.

 C = A + B

 +=

 좌항의 값에 우항을 더한 값을 좌항에 대입.

 C += A is C = C + A

-=

 좌항의 값에서 우항을 뺀 값을 좌항에 대입. 

 C -= A is C = C - A

 *=

 좌항의 값과 우항의 값을 곱한 값을 좌항에 대입.

 C *= A is C = C * A

 /=

 좌항의 값에서 우항의 값을 나눈 값을 좌항에 대입.

 C /= A is C = C / A

 %=

 좌항의 값을 우항으로 나눈 나머지 값을 좌항에 대입.

 C %= A is C = C % A

 <<=

 좌항의 값을 우항의 수만큼 왼쪽으로 비트 단위 이동을 한 값을 좌항에 대입.

 C <<= 2 is C = C << 2

 >>=

 좌항의 값을 우항의 수만큼 오른쪽으로 비트 단위 이동을 한 값을 좌항에 대입.

 C >>= 2 is C = C >> 2

 &=

 좌항의 값과 우항을 비트 단위 논리곱한 값을 좌항에 대입.

 C &= 2 is C = C & 2

 ^=

 좌항의 값과 우항을 비트 단위 배타적논리합한 값을 좌항에 대입.

 C ^= 2 is C= C ^ 2

 |=

 좌항의 값과 우항을 비트 단위 논리합한 값을 좌항에 대입.

 C |= 2 is C = C | 2


1.6. 기타 연산자 (ETC Operator)

위에서 설명한 연산자들 외에도 C언어에서 자주 사용되는 중요한 연산자가 있습니다. 

앞으로 배우게 될 중요한 개념중 하나인 포인터 관련 연산은 굉장히 자주 사용되고 중요합니다. 

sizeof()같은 경우 자료형이나 변수, 구조체의 크기를 구할 때 자주 사용됩니다. 

 연산자

설명 

 예제

 sizeof()

 괄호 안에 주어진 변수 또는 자료형의 크기를 반환.

 sizeof(int) is return 4

 &

 단항연산자로 변수의 주소값을 반환.

 &a is 변수 a의 주소값 반환

 *

 포인터의 값을 반환.

 *p is 포인터변수 p의 값 반환

 ? :

 조건 연산자. 조건에 따라 두 값중 하나를 반환. (조건절)? A : B 에서 조건절이 True이면 A조건절이 False이면 B 반환

 (A<0) A is A가 0보다 작으면 0, 크면 A를 반환


2. 연산자 우선순위

C언어에서 사용되는 모든 연산자에는 우선순위가 있습니다. 

예를 들어 x = 7 + 3 * 2; 과 같이 여러 연산자가 연달아서 사용된 문장의 경우 연산자 우선순위에 따라 + 보다 * 이 우선시되므로 결과값이 20이 아니라 13이 됩니다. 

아래 표에서 상위에 위치한 연산자일 수록 우선순위가 높고 맨 아래 위치한 연산자가 가장 우선순위가 낮습니다.

 분류

연산자 

결합법칙

 후위(Postfix)

 () [] -> . ++ --

 왼쪽에서 오른쪽

 단항(Unary)

 + - ! ~ ++ -- (type)* & sizeof

 오른쪽에서 왼쪽

 곱셈(Mutiplicative)

 * / %

 왼쪽에서 오른쪽

 덧셈(Additive)

 + - 

 왼쪽에서 오른쪽

 비트이동(Shift)

 << >>

 왼쪽에서 오른쪽

 관계(Relational)

 < <= > >=

 왼쪽에서 오른쪽

 균등(Equality)

 == !=

 왼쪽에서 오른쪽

 비트곱(BitwiseAND)

 &

 왼쪽에서 오른쪽

 비트배타합(BitwiseXOR)

 ^

 왼쪽에서 오른쪽

 비트합(BitwiseOR)

 |

 왼쪽에서 오른쪽

 논리곱(LogicalAND)

 &&

 왼쪽에서 오른쪽

 논리합(LogicalOR)

 ||

 왼쪽에서 오른쪽

 조건(Conditional)

 ?:

 오른쪽에서 왼쪽

 대입(Assignment)

 = += -= *= /= %=>>= <<= &= ^= |=

 오른쪽에서 왼쪽

 콤마(Comma)

 ,

 왼쪽에서 오른쪽



여러분의 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

-Peter의 우아한 프로그래밍

블로그 이미지

친절한 Peter Ahn

IT 정보 공유, 프로그래밍 지식 공유

댓글을 달아 주세요

안녕하세요 피터입니다.

오늘은 C언어에서 사용되는 Storage Class에 대해 설명드리겠습니다.

기억 영역 분류 (Storage Class)


Storage Class는 C언어에서 기억 영역을 분류하기 위해서 사용되는 용어입니다. 이 용어는 한글로 번역하기가 애매한 부분이 있는데 일반적으로 기억 영역 분류 또는 기억류 라고 번역되어 쓰이고 있습니다. 

이 stroage class는 class 라는 단어가 들어있습니다만, 여기서의 쓰임새는 C++ 의 'class' 키워드와는 전혀 관련이 없습니다. 단지 어떤 것의 종류를 의미하는 사전적인 의미로 class가 쓰인 것입니다. 

C언어의 storage class에는 auto, register, static, extern 4가지 종류가 있습니다.

각각의 class는 상호 배타적이라서 두 가지 이상의 키워드를 함께 사용할 수 없습니다. 


1. auto

auto는 지역변수를 선언할 때 아무것도 지정하지 않았을 때 기본값으로 지정되는 storage class 입니다. 

{
   int count;
   auto int count;
}

auto는 함수 내에서만 사용이 가능하며(지역 변수) 위 두 구문은 의미가 같습니다.

(사실 실제로 auto 키워드를 사용할 일은 거의 없습니다)


2. register

register는 변수가 메모리 대신 레지스터에 저장되면 좋겠다는 희망사항을 컴파일러에게 알려주는 storage class 입니다. 

레지스터에 저장된 변수에는 메모리에 저장된 변수보다 훨씬 더 빠르게 접근할 수 있으므로 성능 향상을 꾀할 수 있습니다. 

레지스터에 저장되려면 변수의 크기가 레지스터 사이즈보다 같거나 작아야 합니다. 일반적으로 1 word를 저장할 수 있습니다. 그리고 레지스터는 메모리처럼 주소값을 갖을 수 없기 때문에 변수의 주소값을 구하는 & 연산자는 사용할 수 없습니다.

{
   register int  miles;
}

register 지시자를 사용했다고 해서 반드시 레지스터에 저장되는 것은 아닙니다. 그저 '레지스터에 저장했으면 좋겠다' 정도인 것입니다. 

최근 나온 컴파일러들은 최적화를 수행하면서 레지스터를 잘 활용하기 때문에 register 지시자를 무시할 확율이 높습니다. 

(결론적으로는 실제로 사용할 일이 거의 없습니다.)


3. static

이전 강좌에서 변수의 Scope에 대해서 설명드렸던 것 기억하시나요?

지역 변수(Local variable)의 경우에는 함수 내에서만 유효하다고 말씀드렸습니다. 함수를 빠져나가는 순간. 즉 '}' 를 만나는 순간 지역 변수는 스택에서 제거됩니다. 

같은 함수를 다시 호출했을 때 예전에 사용하던 지역 변수는 이미 삭제되어 값을 유지하지 못합니다. 같은 이름의 새로운 지역 변수가 스택에 생성되었기 때문이죠. 

그런데 static 지시자를 사용해서 생성한 지역 변수는 함수를 빠져나가도 값을 유지할 수 있습니다. 변수의 생명 주기(Life cycle)이 달라지게 되는거죠.  그렇다고 해서 다른 함수에서 접근이 가능해지는 것은 아니니 혼동하지 말아주세요.

static은 전역 변수에도 사용할 수 있습니다. 전역 변수를 static으로 지정하면 해당 변수의 Scope을 선언된 파일 내로 제한합니다. 

아래 예제 코드를 봐주세요. 

#include <stdio.h>
 
/* function declaration */
void func(void);
 
static int count = 5; /* global variable */
 
main() {

   while(count--) {
      func();
   }
	
   return 0;
}

/* function definition */
void func( void ) {

   static int i = 5; /* local static variable */
   i++;

   printf("i is %d and count is %d\n", i, count);
}

count라는 전역 변수에 static을 사용해서 Scope를 파일 내로 제한했습니다. 

func() 함수 내에서 선언된 지역 변수 i 의 경우 함수 호출이 끝나더라도 값을 유지해서 다음 호출 시 늘어나는 것을 아래 실행 결과에서 볼 수 있습니다.  

static int i = 5; 이 초기화 구문은 변수가 처음 생성될 때만 유효하기 때문에 첫 번째 함수 호출 시에만 실행됩니다. 그 이후 호출 시에는 무시되죠. 

함수가 호출될 때마다 생성 - 파괴가 반복되는 일반적인 지역변수와는 전혀 다르게 동작하죠. static을 빼고 테스트 해보시기 바랍니다. 

실행 결과

i is 6 and count is 4

i is 7 and count is 3

i is 8 and count is 2

i is 9 and count is 1

i is 10 and count is 0


4. extern

extern은 프로그램을 구성하는 파일들이 여러 개일 때 다른 파일에서 정의된 전역 변수나 함수를 접근할 수 있게 참조(reference)를 제공해주는 지시자입니다. 

다만 extern으로 선언된 외부 전역 변수는 초기화할 수 없습니다. 

아래 예제 코드를 봐주세요.

main.c

#include 
 
int count ;
extern void write_extern();
 
main() {

   count = 5;
   write_extern();
}

support.c

#include 
 
extern int count;
 
void write_extern(void) {
   printf("count is %d\n", count);
}

main.c 파일에서 선언된 전역 변수 count를 support.c 에서 extern으로 선언하여 printf하고 있습니다. 

또한 support.c에서 선언된 write_extern 함수를 main.c에서 extern으로 선언하여 호출하고 있습니다. 

이런식으로 여러 개의 파일에서 전역 변수를 공유하고 싶을 때는 extern을 사용하여 같은 이름의 변수 또는 함수를 선언하면 됩니다. 

컴파일 및 실행 결과

$gcc main.c support.c
$./a.out

count is 5


여러분의 댓글은 저에게 크나큰 힘이 됩니다. 오류 및 의견 주시면 감사하겠습니다.

-Peter의 우아한 프로그래밍

블로그 이미지

친절한 Peter Ahn

IT 정보 공유, 프로그래밍 지식 공유

댓글을 달아 주세요