C 언어 문법 snippet
2019, Oct 20
목차
-
C언어 컴파일, 링크, 빌드의 의미
-
declaration(선언)과 definition(정의) 차이
-
char[]와 char*의 차이
-
const char *와 char const *의 차이
-
문자 상수 리스트
-
조건 연산자 (? : )
-
쉬프트 연산자 («, »)
-
printf 출력 관련
-
scanf 입력 관련
-
변수 주소 구조
-
sizeof 관련
-
int main(int argc, char* argv[])
-
중복 선언 방지 팁
C언어 컴파일, 링크, 빌드의 의미
- 출처 : https://opentutorials.org/module/1594/9734
- 초창기의 컴퓨터는 기계어로 프로그래밍을 했습니다. 그러나 기계어는 사람이 이해하기 아주 어려워서, 이를 보다 편하게 사용하기 위해 다음과 같은 방법을 생각하였습니다.
- 기계어의 집합을 더 간단하게 표현하는 텍스트 문서를 만드는 방법입니다. 예를 들어 C는 긴 코드를 간단하게 표현하기 위해 함수나 매크로(macro)를 사용할 수 있는데, 기계어로 10줄짜리의 코드를 매크로 A로 정의하고 문서에는 A만 써넣는 경우를 생각해보겠습니다.
- 먼저 이 텍스트 문서를 기계어로 자동 번역하는 프로그램 A를 만듭니다.
- 텍스트 문서를 프로그램 A를 이용하여 기계어로 자동 번역한다. 이 프로그램을 실행하면 위에서 예시로 작성한 문서의 A가 기계어 10줄로 번역됩니다.
- 이렇게 하면 필요할 때 마다 텍스트 문서만 수정하여 프로그램을 간단하게 만들 수 있습니다. 위 내용을 순서에 맞게 다시 설명해 보겠습니다.
- ① 일정한 형식으로 작성된 문서를 기계어로 자동 번역하는 프로그램 A를 먼저 만듭니다.
- ② 프로그램을 만들 때마다 A가 번역할 수 있도록 일정한 형식으로 문서를 작성합니다.
- ③ 문서 작성이 완료되면 프로그램 A를 실행하고 파일을 넘겨서, A가 자동으로 번역해준 기계어 파일을 얻습니다.
- 바로 여기서 사용되는 프로그램 A를
컴파일러(compiler)라고 하고, 이 때 작성한 일정한 형식의 컴퓨터 명령을소스 코드(source code), 소스 코드가 저장된 텍스트 파일을소스 코드 파일(source code file)또는 간단히소스 파일(source file), 그리고 이를 번역하는 행위를컴파일(compile)이라고 합니다.
- 컴퓨터가 발전하고 작성하는 소스 코드의 양이 늘어남에 따라, 한 파일에 모든 소스 코드를 작성하는 방식이 불편하다는 것을 깨닫게 되었습니다. 따라서 사람들은 소스 코드를 다른 파일에 분리하는 방법을 생각해 냅니다. - 원래 하나였던 파일을 분리했으므로, 프로그램을 완성하려면 분리했던 파일은 모두 연결해야 합니다.
- 이렇듯 분리된 파일을 모아 하나의 실행 가능한 파일을 만들면 이를 두고 파일들을
링크(link)했다고 하고, 이때 사용되는 프로그램을링커(linker)라고 한다.
- 종합하면, 우리는 기계어를 이용하지 않고 실행 파일을 생성하기 위해 다음의 순서를 거치게 됩니다.
- ① 소스 코드를 작성하고 파일로 저장합니다.
- ② 저장한 소스 파일을 컴파일러를 이용하여
컴파일합니다. 이 때,오브젝트 파일이 생성됩니다. - ③ 컴파일러가 생성한 오브젝트 파일들을 링커를 이용하여
링크합니다. 이 떄, 실행 가능한 오브젝트 파일이 생성됩니다. - ④ 링커는
실행 가능한 오브젝트 파일을 생성합니다. 컴파일러가 생성하는 파일과 링커가 생성하는 파일의 차이는 생성한 목적 파일이 실행 가능 하느냐에 있습니다. - ⑤
컴파일과링크과정을 합쳐빌드(build)라고 하고, 이때 사용되는 프로그램을빌더(builder)라고 합니다.
declaration(선언)과 definition(정의) 차이
- C/C++ 언어 관련 글을 읽다가 보면
declaration과definition이라는 용어가 나오는데 그 차이점에 대하여 알아보겠습니다. - 먼저
declaration(선언)에 대하여 알아보도록 하겠습니다. - 변수 선언은 컴파일러에 변수명, 변수 타입 그리고 초깃값이 정의되어 있다면 초깃값 까지 전달하는 역할을 합니다. 즉,
변수 선언은 컴파일러에 어떤 변수에 대한 상세한 정보를 주는 역할을 합니다. - 그러면
definition(정의)는 무슨 역할을 할까요? - 정의는 선언한 변수가 어디에 저장되는지를 나타냅니다. 즉, 변수가 메모리 영역에 할당되는 순간을
정의라고 합니다.
- 보통 C언어에서는 선언과 정의가 동시에 발생합니다. 그래서 전혀 차이가 없어 보입니다.
- 예를 들면 선언과 정의가 동시에 발생하는 것이 다음과 같습니다.
int a; //선언과 정의가 동시에 적용
- C언어의 변수에서는 선언과 정의가 보통 동시에 발생하기 때문에 차이점을 잘 모를 수 있으나 함수에서는 차이점이 확연히 나타납니다.
- 함수의 선언 또한 컴파일러에게 함수의 상세한 정보를 알려줍니다. 즉,함수명, 리턴타입, 매개변수 등등을 컴파일러에 전달해줍니다.
int add(int, int) // 함수의 선언
- 위 함수의 선언은 함수명, 리턴 타입 그리고 2개의 매개변수가 있다는 것을 컴파일러에 알려줍니다.
- 하지만 이 단계에서는 아직 함수가 메모리에 할당되지는 않습니다.
// 함수의 정의
int add(int a, int b)
{
return (a+b);
}
- 정의 단계에서는 위 함수가 메모리 영역에 할당됩니다.
- 정리해 보겠습니다.
Declaration(선언): 변수나 함수는 여러번 선언될 수 있고 이 단계에서는 메모리에 할당되지 않습니다. 이 단계에서는 컴파일러에 변수나 함수의 정보만 알려줍니다.Definition(정의): 변수나 함수는 딱 한번 정의되고 이 단계에서 메모리에 할당됩니다.
char[]와 char*의 차이
- 문자열을 정의하는 방법에는 두가지 방법인
char[]와char*가 있습니다. - 두 방법의 차이는
char str[] = "abc";에서 str은 문자열 변수이고char* s = "abc";는 문자열 상수입니다. 즉 문자열 변수는 변경이 가능하지만 문자열 상수는 변경이 불가능 합니다.
const char *와 char const *의 차이
const char *와char const *의 차이점에 대하여 알아보도록 하겠습니다.
const char * : pointer to constant
- pointer to constant는 포인터가 가리키는 값이 변경될 수는 없고 포인터가 변경될 수는 있습니다.
- 즉 위 그림과 같이 포인터가 가리키는 주소 자체를 주소1 에서 주소2로 변경하는 것은 가능하지만 만약 주소1의 값을 값1’로 바꾸려고 하면 에러가 납니다.
- 따라서 pointer to constant에서는 포인터가 가리키는 값이
상수가 됩니다. - 아래 코드를 보면 포인터는 변경될 수 있지만 포인터가 가리키는 값이 변경되면 에러가 발생합니다.
#include<stdio.h>
#include<stdlib.h>
int main()
{
char a = 'A', b = 'B';
const char* ptr = &a;
//*ptr = b; 포인터가 가리키는 값이 변경되는 것은 에러가 발생합니다.
// 포인터는 변경될 수 있습니다.
printf("value pointed to by ptr: %c\n", *ptr);
ptr = &b;
printf("value pointed to by ptr: %c\n", *ptr);
}
char const * : constant pointer
- 반면에
char const *는const의 위치가 변경되어 상수화 되는 대상이 변경됩니다. 먼저 이런 방법은 constant pointer라고 합니다. - constant pointer는 포인터 주소값을 상수화 시킵니다. 즉, 주소값이 변경이 안됩니다.
- 반면에 주소가 가리키는 값에 대한 제한은 없기 때문에 값은 변경할 수 있습니다.
- 예를 들면 위 코드와 같이 주소값은 변경할 수 없지만 주소값이 가리키는 값은 변경이 가능합니다.
- 이 내용을 코드로 알아보면 다음과 같습니다.
// char* const p
#include<stdio.h>
#include<stdlib.h>
int main()
{
char a = 'A', b = 'B';
char* const ptr = &a;
printf("Value pointed to by ptr: %c\n", *ptr);
printf("Address ptr is pointing to: %d\n\n", ptr);
//ptr = &b; 주소값을 변경하는 것은 오류가 발생합니다.
// 포인터가 가리키는 주소의 값을 변경하는 것은 가능합니다.
*ptr = b;
printf("Value pointed to by ptr: %c\n", *ptr);
printf("Address ptr is pointing to: %d\n", ptr);
}
- 정리하면
const char*즉, pointer to constant는 포인터가 가리키는 대상(값)이 상수화 됩니다. 따라서 포인터 변수가 가지는 주소값은 변경 가능하지만 주소가 가리키는 값은 변경 불가합니다. - 반면
char const *즉, constant pointer는 포인터 주소값이 상수화가 됩니다. 따라서 포인터 변수가 가지고 있는 주소값은 변경 불가능 합니다. 하지만 주소값이 가리키는 값에 대한 제약은 없으므로 값 변경은 가능합니다.
const char const * : constant pointer to constant
- 앞에서 설명한 pointer to constant 조건과 constant pointer의 조건을 결합한 형태입니다.
- 따라서 포인터 변수의 주소값을 변경하는 것도 불가능하고 주소값이 가리키는 값을 변경하는 것도 불가능합니다.
// C program to illustrate
//const char * const ptr
#include<stdio.h>
#include<stdlib.h>
int main()
{
char a ='A', b ='B';
const char *const ptr = &a;
printf( "Value pointed to by ptr: %c\n", *ptr);
printf( "Address ptr is pointing to: %d\n\n", ptr);
// ptr = &b; illegal statement (assignment of read-only variable ptr)
// *ptr = b; illegal statement (assignment of read-only location *ptr)
}
- 위 코드를 보면
const char *cost ptr로 생성된 포인터 변수는 주소값을 바꾸는 것과 주소값이 가리키는 값을 변경하는 것 모두가 금지됩니다.
문자 상수 리스트
\n: printf() 함수 등에 의해 출력을 다음 줄로 이동하는 역할\t: 4개 또는 8개의 공백을 띄는 역할\\: 역슬래쉬를 문자 또는 문자열에서 사용\0: 널 문자임을 표시
조건 연산자 (? : )
(조건) ? (조건이 참) : (조건이 거짓)형태로 사용되며 예를 들어1 < 3 ? 1 : 0이라는 예제가 있으면 1이 선택됩니다.- 조건 연산자는 중첩해서 사용할 수 있으며 예를 들어
max_value = (x > y) ? x : (y > 5) ? y : (x + y);와 같이 사용할 수 있습니다.
쉬프트 연산자 («, »)
- 쉬프트 연산자인
<<를 이용하면 *2 와>>/2 를 효과적이고 빠르게 계산할 수 있습니다. 왜냐하면 비트 자체를 이동시키는 것이기 때문입니다. a << 3이란 연산의 결과를 쉽게 이해하려면 \(a \times 2^{3}\)과 같이 이해하면 됩니다.a >> 3의 결과는 \(\text{int}(a \times 2^{-3})\)으로 이해하면 됩니다.
printf 출력 관련
printf의conversion specifier(변환(형식) 지정자)는%[flags][width][.precision][length]specifier형태를 갖습니다.- 예를 들면 아래와 같습니다.
printf("%10.5hi", 256);
- 먼저 위 형식 중
flag부터 알아보도록 하겠습니다. -: -를 flag로 사용하면 출력할 때, width를 고려하여 왼쪽으로 붙여서 출력합니다. 물론 기본 값은 width를 고려하여 오른쪽으로 붙여서 출력하는 방법입니다.+: +를 flag로 사용하면 출력할 때, 양의 값에도 +를 붙여서 출력합니다. 물론 기본값은 양의 값에는 부호를 붙이지 않습니다.- ` ` (space) : flag로 빈칸을 사용하면
#:0: width를 지정하였을 때, 남는 자리를 0으로 채웁니다.
- 그 다음
width를 알아보도록 하겠습니다. 숫자: width로 숫자를 넣으면 출력할 최소 문자 갯수가 됩니다. 출력할 문자의 갯수가 width 보다 작으면 기본적으로 빈칸으로 출력되고 출력할 문자의 갯수가 width 보다 많으면 width와 상관 없이 출력할 모든 문자열을 출력합니다.*:
- 그 다음
.precision에 대하여 알아보겠습니다. .number:*:
conversion specifier의 형식들을 하나씩 살펴보도록 하겠습니다.%a: 부동 소수점 수, 16진수, p 표기법%A: 부동 소수점 수, 16진수, P 표기법%c: 한 글자%d또는%i: 부호가 있는 10진(decimal) 정수(integer)%e: 부동 소수점 수, e 표기법%E: 부동 소수점 수, E 표기법%f: 부동 소수점 수, 10진수 표기. double의 경우%lf사용%g: 값에 따라서%e또는%f를 자동으로 적용. 소수점 아래 자리수가 4자리 보다 많을 때 또는 값이 6자리 수 보다 클 때,%e적용됨%G: 값에 따라서%E또는%f를 자동으로 적용. 소수점 아래 자리수가 4자리 보다 많을 때 또는 값이 6자리 수 보다 클 때,%E적용됨%o: 부호가 없는 8진(octal) 정수%p: 포인터 주소값%s: 문자열%u: 부호가 없는 10진 정수%x: 부호가 없는 16진 정수, 소문자 알파벳 사용%X: 부호가 없는 16진 정수, 대문자 알파벳 사용%%: 퍼센트 기호 출력
printf("%9d\n", 12345);: ` 12345` 출력됨. 9자리 포맷을 맞추되 부족한 자릿수는 빈칸으로 둡니다.printf("%09d\n", 12345);:000012345출력됨. 9자리 포맷을 맞추되 부족한 자릿수는 0으로 채웁니다.printf("%.2f\n", 3,141592);:3.14출력됨. 소숫점 2자리 까지 출력하고 그 아래는 반올림합니다.
- printf 함수의 return 값은 출력한 문자의 갯수입니다. 예를 들어
n = printf("hello")이면 n에 5가 저장됩니다. 5는 출력된 문자열인 hello의 길이 입니다.
scanf 입력 관련
- scanf의 서식에 빈칸을 두면 whitespace 문자들을 입력받습니다. 예를 들어
scanf("%d %d", &a, &b);라고 입력 받으면 변수 a와 b를 구분해서 입력 받을 때, 스페이스(), 탭(\t), 엔터(\n)를 통해 구분할 수 있습니다. - whitespace 끼리는 동일하게 적용되므로
scanf("%d\n%d", &a, &b);또는scanf("%d\t%d", &a, &b);라고 정의하여도 스페이스(), 탭(\t), 엔터(\n)를 통해 구분할 수 있습니다. 물론 스페이스를 통해 구분하는 것이 입력이 편하고 보기에도 편하기 때문에 스페이스로 구분하는 것이 일반적입니다.
- scanf의 서식에 whitespace가 아닌 특정 문자가 들어오면 입력 시 반드시 그 문자를 입력해주어야 합니다. 왜냐하면 그 문자를 구분자로 삼아서 입력받기 때문입니다.
- 예를 들어
scanf("%d,%d", &a, &b);로 입력하면 반드시,를 입력해 주어야 두 정수를 구분해서 입력받을 수 있습니다. - 같은 원리로
scanf("%dA%d", &a, &b);라면 반드시A를 입력해 주어야 두 정수를 구분해서 입력받을 수 있습니다. - 여기서 조심할 것은
%d,%d와 같은 경우1 ,2와 같이 입력하면 정상적으로 정수 2가 입력되지 않는데 그 이유는%d,%d에는 공백문자가 없지만 입력할 때에는,앞에 공백 문자가 있기 때문입니다. 이런 점을 고려하여 문자를 입력해 주어야 문제가 없습니다. - 만약에 어떤 구분 문자도 없이
scanf("%d%d", &a, &b);로 입력된다면 whitespace 문자를 기준으로 구분하게 됩니다.
- scanf에서 입력 받은 문자를 무시할 때에는
*를 이용합니다. - 예를 들어
scanf("%*d %d", &a);를 실행한 후1 2를 입력하면 첫번째 입력 받은 1은 무시되고 변수 a에 2가 입력됩니다. - 이 방법 또한
scanf("%*d,%d", &a);와 같이 구분자로 특정 문자들과 섞어서 사용할 수 있습니다.
문자열을 입력 받을 때, 특정 문자를 무시하면서 받고싶을 수 있습니다. 예를 들어Hello world를 한번에 입력받아야 하는데 중간에 공백 문자가 있으면 구분되어 받기 때문입니다.- 이 때, 사용할 수 있는 방법이
[^문자]입니다. 이 방법은^뒤에 오는 문자들이 입력될 때 까지 계속 받는다는 뜻입니다. - 예를 들어
scanf("%[^\n]s", str);라고 입력 하면\n이 입력 될 때 까지 계속 입력 받고\n이 입력되면 구분자로 삼아서 입력을 마칩니다. 이 방법으로Hello world를 입력하고 마지막에 엔터를 누르면 문자열 전체가 str에 입력 됩니다. 이 방법은 문자열을 입력 받을 때에만 유효합니다. [^문자]에서 문자에는 여러 문자들이 들어갈 수 있습니다. 예를 들어scanf("%[^,-]s", str);라고 입력하면,와-가 들어오면 구분자로 입력하게 됩니다. 즉, 이 때에는 기존에 사용하던 whitespace 문자들은 구분자가 되지 않습니다.
변수 주소 구조
- 포인터를 이용하여 변수의 시작 주소와 끝 주소를 입력하면 변수의 크기를 알 수 있습니다.
- 다음 코드는 int형 (4바이트), char형 (1바이트), 구조체(선언된 자료형 크기들의 총합)의 크기를 알 수 있습니다.
- 특히 포인터 변수에 증감 연산자를 이용하면 데이터형의 크기만큼 증감된다는 것을 확인할 수 있습니다. 일반 변수에 대한 증감 연산은 크기가 1씩 증감하는 것과 차이가 있습니다.
sizeof 관련
sizeof를 이용할 때, 정적 배열과 동적 배열에서의 사용 시 고려해야 할 점에 대하여 확인해 보겠습니다.- 정적 배열의 주소에 sizeof를 이용하면 배열 전체의 크기가 반환되는 반면, 동적 배열의 주소에 sizeof를 이용하면 포인터의 크기만 반환됩니다.
- 정적 배열 주소의 크기를 구하는 경우 비록 주소의 크기이지만 배열 전체의 크기가 반환되는 이유는 정적 배열의 경우 compile 단계에서 배열의 크기를 알 수 있기 때문에 배열의 대표 주소의 크기를 구하면 배열의 크기가 반환 되도록 설정되었습니다. 반면 동적 배열의 경우 runtime 단계에서 배열의 크기를 알 수 있기 때문에 배열의 대표 주소의 크기는 단순히 포인터 변수의 크기만 반환하도록 되어있습니다.
- 또한 함수의 파라미터로 정적 배열을 입력 받은 경우에는 주소의 크기가 포인터 변수의 크기만 반환하도록 되어 있습니다. 파라미터로 넘겨 받을 때에는
call by pointer방식으로 넘겨 받기 때문에 포인터의 크기 만큼만 함수로 전달됩니다.
- 위에서 설명한 이유를 이해하여 문자열 배열의 크기와 문자열의 길이의 차이점에 대하여 명확하게 이해할 수 있습니다.
- 위 코드의
str1은 배열의 크기는 100이지만 문자열의 길이는 5가 됩니다. 문자열의 길이에서 NULL 문자는 제외됩니다. str2의 경우 배열의 크기는 NULL 문자를 포함하여 5이지만 문자열의 길이는 5가 됩니다.str3,str4도 동일한 원리로 이해할 수 있습니다.- 마지막으로
str5는 동적 할당을 통하여 문자열의 공간을 생성한 것입니다. 이 때에는 sizeof의 결과가 배열의 크기가 아니라 문자열 포인터의 크기가 반환됩니다. 반면 strlen을 이용한 문자열 길이는 앞선 예와 똑같이 5가 출력됩니다.
int main(int argc, char* argv[])
- 컴파일을 통하여 실행 파일을 만들었을 때, 그 실행 파일에
parameter를 전달하려면main함수를 다음과 같이 선언해야 합니다.
int main(int argc, char* argv[]){
if(argc > 2){
// argv[1] 부터 이용한다.
}
}
argc는 파라미터의 갯수를 저장하고argv는 각 파라미터를 문자열 형태로 저장하게 됩니다.- 컴파일 후 생성된 실행 파일의 이름이
main이라고하면 실행할 때,./main와 같이 실행할 수 있다고 가정하겠습니다. - 이 때, 기본적으로
argc는 1이고argv[0]은./main이 저장됩니다. 즉, 실행한 명령어가 저장됩니다. 만약 실행할 때, 절대 경로를 이용하여 실행하였다면 현재 경로도 이용할 수 있으니, 응용을 잘 하면 유용하게 사용할 수 있습니다. - 만약
./main aaa와 같이 실행하면 뒤에aaa라는 파라미터가argv[1]로 전달됩니다. 즉 문자열로 전달됩니다../main aaa bbb와 같이 실행하면argv[2]에는bbb가 전달됩니다. 즉, 공백 문자를 통하여 파라미터를 구분합니다.
중복 선언 방지 팁
- 한 프로젝트에서 많은 코드를 다루다 보면 같은 헤더를 중복 참조하여 그 헤더의 변수나 함수들을 중복으로 선언하는 에러가 발생하곤 합니다.
-
이 때, 다음 팁들을 차례대로 이용하여 문제를 해결해 나아갈 수 있습니다.
- ①
#ifndef를 사용하는 방법 입니다. 예를 들어 다음과 같습니다.
// sample.h
#ifndef __SAMPLE_H__
#define __SAMPLE_H__
// 코드 //
#endif
- 위 코드를 설명하면
#ifndef __SAMPLE_H__즉, SAMPLE_H 이란 매크로가 선언되지 않았으면 아래 코드를 실행하는 것입니다. - 이 때
#ifndef다음에 바로 SAMPLE_H 을 선언하는 영역이 있습니다. 따라서 매크로를 이 시점에 선언하기 때문에 다음에 이 헤더 파일을 접근하면#ifndef라인을 통과할 수 없어서 중복 선언하지 않게 됩니다.
- ② 어떤 헤더 파일에 어떤 목적으로 변수를 선언하여 여러 파일에서 접근하여 사용하려고 할 때가 있습니다. 이 때에도 중복 선언 문제가 발생할 수 있습니다.
- 이 문제를 쉽게 해결 할 수 있는 방법은
static키워드를 사용하여 헤더 파일에 있는 변수를 선언하는 것입니다. - 이 방법이 효과가 있는 이유는
static키워드를 이용하여 선언된 변수는 전체 프로그램에서 딱 한번 메모리에 영역이 할당되기 때문입니다.