C의 기본 문법
처음의 마음을 절대 잊지 말 것. 초심불망
진법 변환
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;
}