포인터
변수를 선언하면, 컴파일러는 변수에게 일련번호를 부여하고, 정해진 규칙에 의해 정해진 위치에 변수를 할당하게 됩니다.
할당하는 작업은 실제로 어떠한 작업을 수행하는 것은 아니기 때문에 디버거로 break point를 생성할 수는 없습니다.
또한, 해당 위치를 지정만 하기 때문에 과거에 쓰여진 기록이 있다면 해당 정보를 그대로 사용하게 됩니다.
일반적으로 쓰레기 값(Garbage value)이라고 하며, 활용에 주의를 요하기도 합니다.
변수의 위치를 주소라고, C에서는 pointer라고 부릅니다.
pointer는 주소입니다.
pointer는 오직 C언어의 가장 큰 특징이며, 가장 강력한 무기입니다.
- 포인터의 의미와 문법에 대해 설명합니다.
- 컴파일된 바이너리 코드를 살펴보면, 명령어는 데이터를 저장하거나 불러올 때, 변수의 위치, 즉 주소를 이용해서 제어하게 됩니다.
- 포인터를 쓰지 않는 다른 언어들의 경우, 컴파일러에서 위치를 계산하기 때문에, 작성자(개발자)가 변수의 위치를 알 필요도 없고, 알 수도 없습니다.
- C 언어는 주소를 직접 계산하거나, 직접 참조 할 수도 있습니다. (선언하지 않은 주소도 참조 할 수 있습니다.)
- 또한 C언어는 해당 데이터의 주소를 공유하는 방식을 통해 직접 제어가 가능합니다.
- 포인터 문법은 선언과 참조시 *, 주소값을 가져올 땐 & 기호를 사용합니다.
- 포인터는 중첨되서 선언될 수 있습니다. 이중포인터, 삼중포인터, 그 이상도 가능합니다.
- 함수는 명령어 모음이고, 명령어는 숫자(데이터)이며, 데이터는 어딘가 저장되므로, 포인터를 갖습니다. 함수포인터는 함수를 제어하기 위한 변수입니다.
문법
C언어는 주소를 값으로 취급하여 저장, 연산 할 수 있습니다. 단, 주소를 다룰때는 주소라고 명시합니다.*을 사용합니다.
변수타입 *포인터명;
int *pIndex;
접근
해당 주소의 내용을 참고하고자 할 때, *을 사용합니다.
*포인터명 = 저장할 값;
*pIndex = 10;
변수의 주소값을 가져오고자 할 때는, &(and percent)를 사용합니다.
&변수명;
pIndex = &index;
함수 호출
변수를 전달하는 방법에 따라 Call by Value, Call by Reference, Call by Address가 있습니다.
-
Call by Value 방식은 변수를 복사해서 전달하는 방법이므로, 함수 내에서 내용을 변경하더라도 원래의 값이 변경되지 않습니다.
Call by Valuevoid callbyValue(int);
callbyValue(input);예시void foo(int arg);
foo(arg); -
주소(pointer)를 복사하여 전달합니다. 주소의 값을 참조하여 변경하기 때문에, 원래 값에 변경이 적용됩니다.
Call by Referencevoid callbyRef(int*);
callbyRef(&input);예시void foo(int *arg);
foo(&arg); -
컴파일러가 함수내에서 전달받은 인자의 주소를 기준으로 직접 동작합니다. 값의 변경이 적용됩니다.
Call by Addressvoid callByAddr(int&);
callByAddr(input);예시void foo(int &arg);
foo(arg); - 배열, 구조체, 공용체, 열거체의 구조를 설명하고, 이를 재정의해서 사용하는 방법에 대해 설명합니다.
- 배열Array는 같은 크기의 데이터 형식을 여러개 나열하는 호출 방식을 말합니다. 배열을 사용하면, 반복문을 사용해서 여러개의 변수를 쉽게 제어할 수 있습니다.
- 구조체Struct는 크기가 다양한 데이터 형식을 여러개 나열하는 호출 방식을 말합니다. 구조체를 포함한 구조체를 선언할 수 있습니다. 굉장히 복잡한 구조도 설계할 수 있습니다.
- 공용체union은 하나의 메모리에 여러가지 형식을 중첩하여 선언하는 방식입니다.데이터 표준 등을 구현 할 때, 컴파일 최적화로 인한 에러를 방지할 수도 있습니다.
- 열거체enumerate는 자동으로 넘버링numbering 해주는 기법입니다. 코드의 가독성을 높여주기 위한 기법입니다.
- 재정의란 변수의 타입을 좀 더 직관적으로 관리할 수 있도록 도와줍니다.
- 배열은 같은 구조의 데이터를 여러개 나열합니다.
- 배열은 int array[10];과 같이 선언합니다. array[3]과 같이 접근합니다.
- 배열은 n은 n개의 공간을 0 부터 n-1 까지 갖는 것에 주의해야 합니다.
- 배열은 array[n]과 같이 접근가능하며, 반복문에 사용할 수 있으므로, 관리에 용이합니다.
- 배열은 pointer입니다. 컴파일러는 배열의 크기를 알 수 있지만, 타겟(target, 코드가 실제 돌아가는 기계)는 배열의 크기를 알지 못합니다. 따라서, 오동작에 주의해야 합니다.
- 문자열은 문자가 반복되는 배열입니다. char []로 선언될 수 있으며, 마지막은 '\0'(= 0)이어야 합니다.
2중 포인터
주소(pointer)도 하나의 값이기 때문에, 이 값을 저장한 저장소의 주소도 존재합니다. 따라서, 주소를 저장한 값을 이중주소(double pointer)라고 합니다. pointer는 여러번 중첩될 수 있습니다.
int i = 10;
int* pI = &i;
int** ppI = &pI;
void main() { int i = 10; int* pI = &i; // 포인터 int** ppI = &pI; // 이중 포인터 int*** pppI = &ppI; // 삼중 포인터 int**** ppppI = &pppI; // 사중 포인터 printf("Before : %d\n", i); // 원래값 확인 ****ppppI = 20; // 4중 포인터 제어 printf("After : %d\n", i); // 변경된 값 확인 }
함수 포인터
함수는 명령어의 집합이고, 명령어도 숫자이므로, 숫자는 어딘가에 저장되고, 이 위치도 당연히 주소를 갖습니다.
리턴형 (*함수포인터명)(인자값...)
데이터형 확장
배열
선언
int A[3]; // 배열을 선언하는 방법
A[0] = 10; // 배열을 사용(저장)하는 방법
초기화
int a[10] = { 11, 22, 33, 44, 55, 66, 77, 88, 99, 110 };
반복문과 사용
for (int i = 0; i < 10; i++) { // 0 부터 9까지 반복합니다.
a[i] = i; // n번째 항에 n을 저장합니다.
}
문자열
문자열은 문자의 나열이며, 0을 문자열의 끝으로 인식합니다. 즉, 0이 나올 때까지 출력됩니다.
char string[255] = "hello world!"; // 254개의 문자를 출력합니다. + '0' printf("%s\n", string); // hello world!를 출력합니다. string[7] = 0; // '0'이 등장하면 문자열의 마지막이라고 판단합니다. printf("%s\n", string); // hello w를 출력합니다.
구조체
구조체는 pointer가 아닙니다. 구조체는 자료형입니다. 구조체는 기본자료형, 구조체, 포인터, 배열 등을 담을 수 있는 복합 자료형입니다.
선언 방식
전통적으로 C에서는 구조체명을 대문자로 사용합니다. 프로젝트의 단위가 커지면 자연스럽게 기본자료형(int, char, double 등)을 조합한 자료형을 사용하게 됩니다. 이 자료형들은 기본 자료형들을 기본 자료형들과 구분짓기 위해 대문자로 표기합니다. 다른 코딩 언어에서도 조합된 자료형(클래스)은 대문자로 시작하는 변수명을 사용하는 추세입니다(변수는 소문자로 시작합니다).
struct 구조체명 {
자료형 멤버변수1;
자료형 멤버변수2;
...;
}
typedef struct {
자료형 멤버변수1;
자료형 멤버변수2;
...;
}구조체명;
struct DATE{
int day;
int month;
int year;
};
구조체의 크기
typedef struct { char char1; char char2; char char3; }STRUCT1; // 3 bytes typedef struct { char char1; // 1 => 4 byte int int2; // 4 byte char char3; // 1 => 4 byte }STRUCT2; // 12 bytes typedef struct { char char1; char char2; int int3; }STRUCT3; // 8 bytes
기술된 순서대로 적재합니다. 만약 4byte(32bit)가 채워지지 않았다면, 공간을 비우고 다음 32bit부터 시작합니다. 예를들어, STRUCT2는 int2가 선언될 때, char1이 1byte임에도 불구하고, 4byte를 할당하게 됩니다. 그리고 char3이 호출되었을 때, 4 byte(기존단위)로 호출되게 됩니다.
위 예제는 MinGW-W64 x86_64-ucrt-posix-seh와 Visual Studio 2022를 기준으로 주석이 작성되었습니다.
다른 컴파일러에서 다른 결과가 나올 수도 있습니다.
구조체는 32bit단위로 패키징(packaging)되기 때문에, 예상했던 것과 다른 크기로 할당 될 수도 있습니다.
범용 PC의 경우 64bit로 build하더라도 32bit으로 쪼개어지기도 합니다(호완성 이슈로 추측됩니다).
패키징 결과에 대해서 예상하는 것은 위험합니다.
따라서, 컴파일러의 종류와 버젼에 따라 크기가 달라질 수 있다는 점을 인지하고, 확인하는 방법에 대해 알고 있어야 합니다.
호출 / 접근
"구조체.멤버변수"를 통해서 접근할 수 있습니다.
구조체.멤버변수;
struct foo { int bar; } void main() { foo.bar = 42; printf("%d\n", foo.bar); }
구조체속의 구조체
"구조체.내부구조체.멤버변수"로 접근이 가능합니다.
struct STR_IN { int i; } struct STR_OUT { STR_IN in; } void main() { STR_OUT out; // 구조체 선언 out.in.i = 10; // 구조체 접근 }
구조체 포인터
구조체는 기본자료형과 마찬가지로 함수의 인자로 사용할 수 있습니다.
이 때 구조체는 한 번에 여러가지 정보를 전달하므로 함수의 형태를 간단하게(=가독성을 높이는) 효과가 있습니다.
단, 구조체의 포인터로부터, 구조체를 접근하기 위해서는 닷(.)연산자가 아닌 화살표(->)연산자를 사용해야 합니다.
typedef struct { int i; }STR1; int main() { STR1 str1 = { 10 }; STR1* pStr1 = &str1; str1.i = 20; // 직접지정 printf("%d\n\n", str1.i); // 출력 : 20 pStr1->i = 30; // 포인터지정 printf("%d\n\n", str1.i); // 출력 : 30 }
구조체속의 배열, 그리고...
각각의 예제를 암기하지 않습니다. C언어에 제약이란 없습니다.
struct STR1 { int i; int j; int arr[5]; // 구조체 속의 배열; STR1 * p; // 구조체 속의 내 구조체; }; void main() { STR1 str1[10]; // 배열 속의 구조체; str1[2].i = 3; str1[0].p = &(str1[0]); str1[0].p->p->p->p->p->p->p->p->p->p->p->p->p->i = 40; // 나의 나의 나의 나의 나의 나의 나의 나의 나의 나의 멤버 변수 }
공용체
공용체는 구조체와 예약어만 다를뿐, 문법 규칙은 동일합니다. 단, 공용체는 메모리를 공유합니다.(영역이 겹칩니다.) 구조체는 공유하지 않습니다.
typedef union { char c[4]; short s[2]; int i; }U1;
열거체
우리 전등가게에서 판매하는 전등에는 색 전환 레버가 있습니다. 기본 모드는 빨간색이고, 레버를 한 번 돌리면 노란색이 나옵니다.
if(mode == 0) { light(0); // 빨강 출력 } else { light(1); // 노랑 출력 }
고급형 전등을 판매하기 시작했습니다. 주황색이 추가되었습니다.
switch(mode) { case 1: light(1/* 주황 출력 */); break; case 2: light(2/* 노랑 출력 */); break; default: light(0); // 빨강 출력 }
enum을 사용하면 좀 더 수월하게 코드를 관리 할 수 있습니다.
enum Color { RED, ORENGE, YELLOW } switch(mode) { case 1: light(ORANGE); break; case 2: light(YELLOW); break; default: light(RED); }
문제가 생겼습니다. 저가형에서 1번모드는 노란색이지만 고가형에서는 주황색이 할당되어야 합니다.
제품군이 늘어날 때마다, 코드의 갯수를 늘리는 것은 현명한 방법은 아닐 것입니다.
당장의 비용도 비용이지만, 수정 포인트가 늘어날수록 버그발생이 늘어나는 것은 당연한 이치입니다.
아마도 빨, 주, 노 순서대로 배치했어야 했는지 의문이 들 수 있습니다.
실례에서는 문, 풍량, 타이어, 색 등 요소의 배치등을 고려했을 때, 순차적인 할당이 관리의 이점이 있을 것이라고 예상되는 사례는 얼마든지 발생할 수 있습니다.
물론 직접 할당하는 것도 방법입니다.
#define RED_MODE 0 #define YELLOW_MODE 1 #define ORENGE_MODE 2 switch(mode) { case ORENGE_MODE: light(ORANGE); break; case YELLOW_MODE: light(YELLOW); break; default: light(RED); }
enum과 #define의 목적과 기능은 같습니다. 코드의 가독성을 높여줍니다. 다만, 코드를 가변적으로 할당하는 enum과 고정적으로 할당하는 #define을 상황에 맞게 사용하면 될 것입니다.
건너뛰기
enum Color { RED, // 0 할당 ORENGE, // 1 할당 YELLOW, // 2 할당 BLACK = 10 // 10 할당 };
* #define을 포함한 전처리기 명령어는 'C 예약어들'목록에서 다시 다루겠습니다.