C언어

C언어의 기초 - 변수와 배열

머리큰개발자 2021. 2. 3. 20:50

*주의 - 혼자 학습하여 틀린 내용이 있을 수 있으니 알려주시면 진심으로 감사드리겠습니다.

1. 변수

변수의 종류는 2가지로, 전역변수(Global variables)와 지역변수(Local variables)로 이루어져있다.
둘의 구분은 선언시의 위치이며, 특정 블럭 {} 안에 선언하면 지역변수라고 부르며 그 외는 지역변수라고 부른다.
지역 변수는 기본적으로 블럭 안에서만 작동하며 지역변수로 선언하기 전에 이미 선언될 경우, 먼저 선언된 변수가 블럭을 벗어날 동안 보이지 않는 상태(invisible)가 된다.

변수는 앞선 페이지에서 언급한 것처럼 선언이 반드시 필요하며, 동시에 초기화도 진행할 수 있다.
초기화를 하지 않고 선언만할 시, 지역변수는 원래 메모리에 저장되어있던 쓰레기값(garbage)을 가지므로 초기화나 값을 대입하고 나서 사용해야 한다.
반대로 전역변수는 초기화하지 않아도 자동으로 0으로 초기화된다. 하지만 전역변수는 지역변수와 달리 변수식을 사용할 수 없다. (사용시 오류)
예시 ) int y= 10; int x= y; 전역변수는 불가, 지역변수는 가능

원래라면 변수의 선언은 상당히 까다롭다. 함수 외부에서 초기화가 아닌 코드를 쓸 수 없었으며, 함수 내에서 지역변수를 선언할 때 {}앞부분에만 선언이 가능했다.
하지만 근래에 나오는 컴파일러들은 위 사항을 오류로 인식하지 않고 그대로 사용할 수 있어 변수 사용에 있어 편의성이 크게 증가했다.
즉, 이제 원할 때 아무때나 변수를 선언해도 좋다.(가시성엔 안좋을 수도 있지만)

여기서 외울 것은 전역변수는 초기화하지 않으면 0이란 것이고, 변수식을 초기화자로 사용할 수 없다는 것이다.
(단, sizeof 연산자의 경우 컴파일시에 계산이 되므로 상수식처럼 사용할 수 있다.)
지역변수를 이해하기 위해선 저장공간과 유효범위에 대한 개념이 필요하다.

기본적으로 전역변수는 정적 메모리(static memory)에 저장되고 지역변수는 프로그램을 시작하면 할당되는 스택(stack)에 저장된다.
정적 메모리는 스택의 외부에 있기 때문에 심지어 파일 외부에서도 사용이 가능하고, 컴파일 단계에서 미리 메모리에 저장하기 때문에 프로그램이 시작되기 전에 이미 저장이 되어있다.
전역변수는 특정 경우를 제외하고는 모두 스택에서 선언되고 임시적으로 저장된다.
특히 블럭 안에서 선언되기 때문에, 블럭이 시작되면 저장되고 끝나면 컴퓨터가 변수의 존재를 잊는다. 이 개념을 유효범위라고 칭한다.
유효범위는 크게 3가지다. 1. 여러 파일에 걸쳐 영향을 미칠 수 있는 프로그램 전체에 해당하는 범위 2. 해당 파일 내부에서만 사용가능한 범위 3. 블록 {} 안에서만 사용가능한 범위
유효범위를 지정하는 방식은 아래에서 소개하며, 유효범위에 대한 간단한 예시를 보자.

#if 1
#include <stdio.h>
int a,b=1; // 전역변수의 선언, a=0 b=1 으로 초기화됨
Int main(){
printf(“%d”\n, a); // 0 출력 선언된 지역변수가 없을 경우에 정적 메모리에서 찾는다. 정적 메모리에도 없을 경우 오류
int a=100; //지역변수 선언, 앞서 선언된 전역변수 a는 일시적으로 보이지 않는다.
printf(“%d”\n, a); // 100 출력, 전역변수 a 는 나오지 않는다.
{
int a= 1000; // 지역변수 선언, 앞서 선언된 지역변수와 전역변수는 일시적으로 보이지 않는다.
printf(“%d\n”, a); // 1000 출력
} 

printf(“%d\n”, a); // 100 출력, 블럭이 끝날경우, 블럭 안의 지역변수의 존재를 잊는다.
}
#endif



변수들이 유효한 범위를 정해놓음으로서 함수 각각에 있는 변수의 독립성이 보장된다. 또한, 외부의 다른 파일에서 사용될 때 함수 내의 변수를 고려하지 않아도 된다는 장점이 있다.

지역변수들과 함수들을 담는 스택은 선입후출의 구조로 되어있다. 먼저 들어온 녀석보다 후에 들어온 녀석이 먼저 나가는 구조로, 블럭을 실행하거나 함수를 호출할 시 진행하던 스택의 영역 위에 다른 스택이 쌓인다.
스택 메모리는 스택 포인터로 주소로 접근하며 단순히 메모리 상에서 구현한 것이다. 새로운 스택이 쌓이거나 빠져나갈 경우 스택 포인터가 움직이며 바뀐 주소를 저장한다.
그렇기 때문에 같은 스택 메모리 안에서는 서로 같은 지역변수를 사용할 수 있으나, 기존 스택 메모리와 새로 들어온 별개의 스택 메모리에선 서로의 지역변수를 알 수 없으므로 사용에 유의해야한다.
하지만, 정적 메모리에 저장된 전역 변수들은 스택과는 별개로 접근이 가능하므로 스택 내부에 없는 요소는 정적 메모리에서 찾게 된다.

유효범위를 설정할 수 있는 명령어는 int, char 같은 변수 타입의 앞에다가 붙인다.
예시) static int a;

전역변수는 extern, static, default 가 있다.
extern: 해당 파일 외부에 있는 변수를 공유하며(다른 파일에 없을 시 새로 선언됨) 이 때는 변수가 생성되지 않는다.
Static : 해당 파일 내부에서만 사용되는 변수를 말한다.
Default : 선언할 때 타입 앞에 따로 붙이지 않으면 기본적으로 정적 메모리에 저장되어 프로그램 전체에서 사용할 수 있는 전역변수를 선언한다.

지역변수에 붙일 수 있는 선언자는 extern, static, auto, register, default가 있다.
extern : 외부의 변수를 참조하며, 컴파일시 정적 메모리에 선언되지만 해당 블럭{}을 벗어날 경우 존재를 잊는다. 하지만 해당 블럭으로 다시 들어오면 존재를 기억한다.
Static : 컴파일시에 정적 메모리에 선언되지만 extern 과 마찬가지로 해당 블럭을 벗어날 경우 존재를 잊었다가 다시 들어오면 기억한다.
Auto, register, default : 요즘은 굳이 구분하여 사용하지 않지만, 스택과 register memory 중 사용할 공간을 정한다.

정적 메모리에 저장되는 변수는 프로그램이 끝나야지만 사라지지만 스택이나 register에 저장되는 변수들은 해당 블럭이 끝날 경우 사라진다.
이들의 활용은 여러 파일을 활용하거나, 함수를 여러번 이용하는 등의 경우에 해당하므로 알고리즘 등에 많이 쓰이진 않지만, 규모가 큰 프로그램을 만들시에 아주 중요하게 작동한다.
물론 그럴 계획은 없다고 해도 기본적으로 유효범위는 c언어에서 굉장히 중요하게 작동하므로 꼭 여러 문제를 풀어보고 감을 잡아야한다.

2. 배열

변수를 하나만 사용할 경우엔 위의 선언으로 충분하다. 하지만 숫자가 많아지고 그것의 이름을 하나하나 정해 값을 대입하기는 쉽지 않은 일이다. 효율적이지도 않고.
그에 따라 배열의 필요성이 대두되고, 같은 타입의 변수로 이루어진 커다란 공간을 정의할 수 있다.
예시) int a[4];

예시의 경우 integer type 이 4개 들어갈 수 있는 공간을 뜻하고, 그 공간에 접근하기 위한 변수명으로써 a를 줌을 의미한다. 즉 a의 첫 번째(컴퓨터에서 0번째)부터 4번째까지 한 번에 메모리에 저장됨을 의미한다.
접근은 변수명으로하고 몇 번째 요소인지를 전달하여 값에 접근할 수 있다.
예시 ) a[2];
이 때 대괄호[]를 사용하여 요소에 접근하며, 직접 값을 대입하거나 꺼내올 수 있다.
예시) int b= a[2]; a[2] = 15;
하나 중요한 사실은 배열의 범위를 반드시 기억하고 사용해야 한다는 것이다.
물론 배열의 범위를 벗어나도 메모리상에선 연속적인 공간이기 때문에 접근하여 값을 얻을 수 있다. (Visual studio 2019에선 안되는 것 같다.)
그렇기 때문에 유효한 범위를 항상 유의해두고 숫자가 범위를 벗어나지 않도록 주의해야 한다.

배열도 변수와 마찬가지로 선언과 동시에 초기화가 가능하다.
예시) int a[4] = {1,2,3,4};
초기화자가 부족할 경우 나머지는 0으로 채워지고(visual studio의 경우) 많을 경우 에러를 발생시킨다.
하지만 만약 초기화자가 아닐경우 {} 식으로 대입시킬 수 없다. 이유는 포인터에서 다룬다.

또한 배열은 배열에 한 번에 대입할 수 없다.
int b[]= {1,2,3,4};
int a[4] = b ;
가 불가능하다.
요소 하나하나 접근해 대입해주어야해서 상당히 불편하다.

단, 문자열로 초기화할 시 항상 문자열의 크기보다 하나 크게 배열을 잡아야한다.
예시) char a[4] = “sdf”;

이유는, 문자열의 끝에는 항상 null(\0)값이 들어가는데, 문자열의 끝을 이 null값으로 인식하기 때문에 만일 같은 값을 줄 경우 char a[3] = “sdf”; 문자열이 끝남을 인식하지 못해
Printf(“%s”, a) 를 실행했을시에 다음 null 값이 나올 때까지 출력해버리기 때문이다.

물론 요소수를 생략할 수 있다. char a[] = "sdf"; 이럴 경우 a는 4칸으로 잡아주고 마찬가지로 끝에 null 값이 포함된다.


또한 초기화가 아닐시에 문자열을 그대로 넣을 수 없다.
예시) char a[4]; a = “sdf”; 불가
물론 가능하게 바꿀수도 있지만, 포인터를 다룰 때 다시 보기로 한다.

배열의 크기는 그 요소들이 차지하는 메모리에 의존한다. 만일 int a[4]; 의 배열이 있을 경우 integer type 의 크기 4B 가 4개 있는 것이므로 16B의 공간을 차지하게 된다.
연산자 sizeof(a)를 사용해 그 크기를 볼 수 있으며, 배열의 요소 중 하나의 크기를 알고 싶을 경우 sizeof(a[0]) 로 구할 수 있다.

배열은 여러 차원으로도 선언이 가능하다.
예시) int a[4][4]; int b[4][5][6];
이들은 배열과 마찬가지로 a[3][3] , b[2][3][4] 등으로 개별 요소에 접근이 가능하다. 특이한 점은, 사람이 사용하기엔 훨씬 직관적이지만, 연속된 공간으로 표현하는 컴퓨터 메모리상에서는
조금 헷갈릴 수 있다.
가령 int a[3][3] = {{1,2,3},{4,5,6},{7,8,9}}; 로 선언되어 있을시, 우리는 a[1][1] 은 a[1] 의 [1]번째라고 직관적으로 생각하여 5를 얻을 수 있지만, 컴퓨터 공간에서는
{1,2,3,4,5,6,7,8,9} 로 저장되어있다. 그렇기 때문에 int a[3][3] 의 경우 (a[1])[1] 로 볼 수 있고 (근치 순서) a[1]을 3개짜리 배열의 출발점에 있는 이름으로 이해할 수 있다.

a[1] 은 {4,5,6} 의 시작점이고, 거기서부터 출발하여 2번째 요소에 접근하라는 뜻으로 이해해야한다.(물론 선언은 배열식으로 해야 보기 좋고 편하다.)
더 자세한 것은 포인터를 다룰 때 더 다루기로 한다.

또한 특이한 것은 배열은 초기화할 때 요소수를 생략할 수 있다는 점이다. 가령 int a[] = {1,2,3,4}; 로 선언하면 자동으로 a의 크기를 4개로 잡아준다.
마찬가지로 2차원 배열에서도 int a[][] ={{1,2,3},{4,5,6}}; 이 가능한가? 하면 그건 또 아니다.
연속된 메모리에서 선언을 해야하고, 컴퓨터는 {{1,2,3},{4,5,6}} 을 연속된 수로 이해하기 때문에, 이름 안에 몇 개의 요소가 들어가는지는 선언을 해줘야한다.
즉 ,int a[][3] = {{1,2,3},{4,5,6}}; 으로 선언해야 오류가 발생하지 않는다.
마찬가지로 int a[][3][4] = {{{},{},{},{}},{{},{},{},{}},{{},{},{},{}}}; 처럼 선언이 가능하다.(쉽게 가장 큰 바깥 블럭은 선언하지 않아도 된다.)

조금 더 자세한건 포인터에서 많이 다루고, 알고리즘으로 넘어가서 많이 다뤄보도록 한다.