포인터 시작 (C)
다음 동영상 강의는 제가 2007년 01월 10일 오프라인 강의했던 자료입니다. 오래 전 자료이지만 지금도 개념은 동일합니다.
코드: 01.포인터시작.c
// 포인터시작.c
// pointer-start-c.c
#include <stdio.h>
int main(void)
{
int a, b, c; // 값형 변수
int* p; // 포인터(참조;주소)형 변수
a = 100;
b = 200;
p = &c;
c = a + b;
printf("a : %d\n", a);
printf("b : %d\n", b);
printf("c : %d\n", c);
printf("p : %p\n", p); // 번지
printf("*p : %d\n", *p); // 참조하고 있는 공간의 값 == 역참조
return 0;
}
결과
a: 100
b: 200
c: 300
p: 00EFFE80
*p: 300
예제: 02.포인터연산.c
// 포인터연산.c
#include <stdio.h>
int main(void)
{
// 값형 변수
int a, b, c;
// 포인터(참조)형 변수
int* pa, * pb, * pc;
a = 10;
b = 20;
c = 0;
pa = &a;
pb = &b;
pc = &c;
*pc = *pa + *pb;
printf("%d, %d, %d\n", *pa, *pb, *pc);
return 0;
}
실행
10, 20, 30
예제: 03.정수형포인터.c
// 03.정수형포인터.c
#include <stdio.h>
int main(void)
{
// 값형 변수
int a = 365;
// 참조형 변수
int* pa = &a;
printf("a : %d\n", a);//365
printf("&a : %p\n", &a);//1244992
//printf("*a : %d\n", *a);
// pa에 들어있는 다른 변수(공간)의 번지 수
printf("pa : %p\n", pa);//1244992번지
// pa가 참조하고 있는 변수에 들어있는 값
printf("*pa : %d\n", *pa);//365
// pa 변수 자체의 고유 어드레스(주소)
printf("&pa : %p\n", &pa);//1244980
return 0;
}
실행
a : 365
&a : 00EBFBF4
pa : 00EBFBF4
*pa : 365
&pa : 00EBFBE8
포인터_실수형.c
/*
7.4. 예제. 실수형 포인터 변수 선언 : 포인터_실수형.c
*/
#include <stdio.h>
int main(void)
{
float a, b, c;
float* p;
a = 100.0, b = 200.0;
p = &c; // c의 주소를 p에 기억
*p = a + b; // c = a + b
printf("%.1f + %.1f = %.1f\n", a, b, *p);
printf("%x %x %x %x %x\n", &a, &b, &c, p, &p);
}
//100.0 + 200.0 = 300.0
//12ff7c 12ff78 12ff74 12ff74 12ff70
100.0 + 200.0 = 300.0
eff780 eff774 eff768 eff768 eff75c
포인터 관련 오프라인 강의 소스 모음
01.포인터변수.c
#include <stdio.h>
int main(void)
{
int a = 10;
int* b;
b = &a;//a주소 : 별칭
printf("%d %d\n", a, *b);
return 0;
}
10 10
포인터 변수의 값 가져오기
단항 연산자인 *
연산자를 사용하면 포인터가 가리키는 변수의 값을 가져옵니다. 단항 *
연산자를 포인터 간접 참조 연산자로 부릅니다.
* 역참조(dereference)
포인터 변수가 가리키는 곳의 실제 데이터를 가져올 때에는 * 기호를 포인터 변수에 붙여 값을 가져올 수 있습니다. 프로그래밍에서 포인터에 포함된 주소의 정보에 액세스하는 것을 역참조라고 합니다. 물론, 그냥 참조로 표현해도 됩니다.
정수형 포인터 변수를 선언 및 참조 후 사용하는 방법을 코드로 살펴보겠습니다.
코드: IntegerPointer.c
#include <stdio.h>
int main(void)
{
// 정수형 변수 선언 및 초기화
int width = 1920;
int height = 1080;
// 정수형 포인터 선언 및 참조
int* w = &width;
int* h = &height;
// 역참조로 값 출력
printf("FHD: %d X %d\n", *w, *h);
// 역참조(dereference)로 값 설정
*w = 3840;
*h = 2160;
// 역참조로 값 출력
printf("UHD: %d X %d\n", *w, *h);
return 0;
}
FHD: 1920 X 1080
UHD: 3840 X 2160
일반 변수와 포인터 변수
C 언어 포인터로 정수 형식을 참조하는 정수형 포인터를 소개합니다.
다음 코드를 그림으로 표현해 보겠습니다.
코드: pointer_integer.c
int a = 20, b = 35, c = 0; // 정수 변수 선언
int* pa, * pb, * pc; // 정수 참조 변수 선언
pa = &a; pb = &b; pc = &c; // 각각의 변수를 참조
*pc = *pa + *pb; // 연산
printf("%d + %d = %d\n", *pa, *pb, *pc); // 20 + 35 = 55
그림: 일반 변수를 참조하는 포인터 변수
포인터_포인터변수.c
포인터_포인터변수.c
/*
포인터 변수: 특정 변수의 주소를 담을 수 있는 그릇
*/
#include <stdio.h>
int main(void)
{
int i, j, k; // 일반 변수 : 값
int* p; // 포인터 변수 : 다른 변수의 주소, 레퍼런스(참조) 변수
i = 10;
j = 20;
k = 0;
p = &k; // 변수 k의 주소를 p에 기억
printf("%d %d %d %d\n", i, j, k, *p); // *p => 포인터 변수가 참조하고 있는 변수의 값
*p = i * j; // k = i * j;
printf("%d %d %d %d\n", i, j, k, *p);
return 0;
}
10 20 0 0
10 20 200 200
포인터_정수형.c
pointer_integer.c, 포인터_정수형.c
#include <stdio.h>
int main(void)
{
int i = 10;
int j = 20;
int k = 0;
int* p1;
int* p2;
int* p3;
// 각각의 일반변수를 포인터(참조형) 변수에 참조
p1 = &i;
p2 = &j;
p3 = &k;
*p3 = *p1 * *p2;
printf("%d * %d = %d\n", *p1, *p2, *p3);
return 0;
}
10 * 20 = 200
포인터_포인터변수의주소출력.c
pointer_variable_address.c, 포인터_포인터변수의주소출력.c
/*
변수(일반, 포인터)의 주소 출력: &
*/
#include <stdio.h>
int main(void)
{
int i;
int* p;
i = 100;
p = &i;
printf("일반변수 i의 값: %d\n", i); // 100
printf("포인터변수 p가 참조하고 있는 변수의 값: %d\n", *p); // 100
printf("일반변수 i의 주소값: %u\n", &i); // %u(unsigned): 부호없는 정수
printf("포인터변수 p의 주소값: %u\n", &p); //
printf("포인터변수 p의 값: %u\n", p); // i변수의 주솟값과 동일
return 0;
}
일반변수 i의 값: 100
포인터변수 p가 참조하고 있는 변수의 값: 100
일반변수 i의 주소값: 2707094068
포인터변수 p의 주소값: 2707094104
포인터변수 p의 값: 2707094068
C 언어 포인터 변수 값 출력 방법 3가지
코드: 03.포인터변수의주소.c
#include <stdio.h>
int main(void)
{
int a, * pa;
a = 10;
pa = &a;
//[1] 값형 a변수
printf("%d\n", a);//a의 값
printf("%d\n", &a);//a의 메모리상의 주소 : 15727580
//[2] 포인터형 pa변수
printf("%d\n", pa);//pa변수의 실제값:a의주소값 : 15727580
printf("%d\n", &pa);//pa변수의 주소:15727568
printf("%d\n", *pa);//pa변수가참조하는곳의실제데이터
return 0;
}
실행
10
15727580
15727580
15727568
10
강의
동영상 강의: C 언어_포인터_03_포인터변수의주소
포인터_실수형포인터.c
pointer_float_type.c, 포인터_실수형포인터.c
#include <stdio.h>
int main(void)
{
float i;
float* p; // 특정 주소(번지;Address)값을 담을 수 있는 그릇
i = 100.0;
p = &i;
printf("p가 참조하고 있는 곳의 값: %.2f\n", *p);
printf("p의 주소 값: %#x\n", &p); // 0xFFFFFF
printf("p의 자체 값: %#x\n", p); // 변수 i의 주소값
printf("i의 주소 값: %#x\n", &i); // 변수 i의 주소값
return 0;
}
실행 결과의 주솟값은 실행 시점의 상황에 따라 다르게 나오는게 정상입니다.
p가 참조하고 있는 곳의 값: 100.00
p의 주소 값: 0x2daffb88
p의 자체 값: 0x2daffb64
i의 주소 값: 0x2daffb64
p가 참조하고 있는 곳의 값: 100.00
p의 주소 값: 0x8a93fad8
p의 자체 값: 0x8a93fab4
i의 주소 값: 0x8a93fab4
값형 변수와 참조형 변수
C 언어에서 변수 선언할 때 값형 변수와 참조형 변수로 구분할 수 있습니다. 참조형 변수가 포인터입니다.
코드: pointer_variable_five.c
// pointer_variable_five.c*** ```C
/*
값(Value)형 변수/배열
포인터(참조;Reference)형 변수/배열
*/
#include <stdio.h>
int main(void)
{
// Value Type 변수
int a = 10;
// Reference Type 변수: 포인터 변수
int* pa;
// 참조 추가
pa = &a; // 포인터 변수에 주소 대입
*pa = 20; // 역참조를 사용하여 포인터 변수가 참조하는 변수에 값 대입
// 값형 변수 표현
printf("[1] %d\n", a); // 20
printf("[2] %p\n", &a); // 000000FFC78FFC24 번지: 계속 바뀌는 부분
// 포인터형 변수 표현
printf("[3] %p\n", pa); // 000000FFC78FFC24 번지
printf("[4] %p\n", &pa); // 000000FFC78FFC48 번지
printf("[5] %d\n", *pa); // 20
return 0;
}
[1] 20
[2] 000000FFC78FFC24
[3] 000000FFC78FFC24
[4] 000000FFC78FFC48
[5] 20
[2]번과 [3]번은 같은 주솟값이 나오고 [4]번 출력 결과는 이웃된 근처의 주솟값이 나옵니다. 이 주솟값은 계속 바뀌는 데이터입니다.
C 언어 포인터 - 값형 변수와 참조형 변수의 차이
동영상 강의: 값형 변수와 참조형 변수의 차이
예제: valuetype-referencetype.c
// valuetype-referencetype.c
// 값형 변수와 참조형 변수의 차이
#include <stdio.h>
void plus(int* p) // 매개 변수로 포인터를 받음
{
*p = *p + 10; // 역참조에 의해서 main 함수의 i 변수 값을 증가
printf("*p = %d\n", *p);
}
int main(void)
{
int i = 10; // main과 plus에서 데이터 저장 공간으로 사용
int j = 20; // 값형 변수 선언과 동시에 초기화
int* p; // 포인터 변수 선언
p = &i; // 참조형 변수에 값형 변수 참조
plus(p); // 넘겨준 포인터 변수의 값을 10 증가
printf("i : %d, j = %d\n", i, j);
return 0;
}
실행
plus function: *p: 20
main function: i: 20, j: 20, *p: 20
포인터 변수는 값형 변수를 참조
포인터 변수는 참조형 변수로도 불립니다. 포인터 변수는 변수 위치에 직접 값이 저장되는 값형 변수를 참조합니다.
먼저 다음 샘플 코드를 살펴보세요.
// pointer_reference.c
int v = 10; // 값형 변수
int* p = &v; // 참조형(포인터) 변수
위 코드를 그림으로 표현해보면 다음과 같습니다.
[그림] 참조형 변수는 값형 변수를 참조
코드
// pointer_reference.c
// 포인터 변수는 값형 변수를 참조
#include <stdio.h>
int main(void)
{
int v = 10; // 값형 변수
int* p = &v; // 참조형(포인터) 변수
printf("%d\n", *p); // 10
}
실행
10
값 형 변수와 참조형 변수의 차이
코드: valuetype-referencetype.c
// valuetype-referencetype.c
// 값형 변수와 참조형 변수의 차이
#include <stdio.h>
void plus(int* p) // 매개 변수로 포인터를 받음
{
*p = *p + 10; // 역참조에 의해서 main 함수의 i 변수 값을 증가
printf("plus function: *p: %d\n", *p);
}
int main(void)
{
int i = 10; // main과 plus에서 데이터 저장 공간으로 사용
int j = 20; // 값형 변수 선언과 동시에 초기화
int* p; // 포인터 변수 선언
p = &i; // 참조형 변수에 값형 변수 참조
plus(p); // 넘겨준 포인터 변수의 값을 10 증가
printf("main function: i: %d, j: %d, *p: %d\n", i, j, *p);
return 0;
}
plus function: *p: 20
main function: i: 20, j: 20, *p: 20
정수 포인터와 실수 포인터
포인터는 이미 있는 정수 포인터와 실수 포인터를 참조해서 사용할 수 있습니다.
코드: NumberPointer.c
#include <stdio.h>
int main(void)
{
int age = 21;
const float PI = 3.14F;
int* p1 = &age;
float* p2 = &PI;
printf("변수 age 값: %d\n", age);
printf("변수 age 주소: %p\n", &age);
printf("포인터 변수 p1의 값: %p\n", p1);
printf("포인터 변수 p1의 참조 값(역참조): %d\n", *p1);
printf("포인터 변수 p1의 주소: %p\n", &p1);
printf("\n");
printf("상수 PI 값: %.2f\n", PI);
printf("상수 PI 주소: %p\n", &PI);
printf("포인터 변수 p2의 값: %p\n", p2);
printf("포인터 변수 p2의 참조 값(역참조): %.2f\n", *p2);
printf("포인터 변수 p2의 주소: %p\n", &p2);
return 0;
}
변수 age 값: 21
변수 age 주소: 00F6FE5C
포인터 변수 p1의 값: 00F6FE5C
포인터 변수 p1의 참조 값(역참조): 21
포인터 변수 p1의 주소: 00F6FE44
상수 PI 값: 3.14
상수 PI 주소: 00F6FE50
포인터 변수 p2의 값: 00F6FE50
포인터 변수 p2의 참조 값(역참조): 3.14
포인터 변수 p2의 주소: 00F6FE38
현재 예제의 실행 결과의 주소값은 매번 달라집니다. 포인터 변수에는 참조하고 있는 변수의 주솟값이 저장됩니다. 포인터 변수 p1의 주소를 &p1 형태로 출력해보면 변수 age의 주소가 나오는 것을 확인할 수 있습니다. 이러한 포인터에 들어 있는 값 자체를 얻고자할 때에는 * 기호를 사용하여 역참조하면 됩니다.
널 포인터 사용하기
NULL
키워드를 사용하는 널 포인터(null pointer)는 단어 그대로 아무것도 가리키지 않는 상태를 의미합니다.
포인터 변수에 NULL
로 초기화하면 메모리가 할당되지 않은 상태입니다.
코드: null_pointer.c
특정 포인터 변수가 NULL인지를 비교해서 널 포인터라면 새롭게 메모리를 할당하는 식으로 사용될 수 있습니다.
포인터 변수 주소 증가 또는 감소
- *p : 역참조로 포인터 변수가 가리키는 곳의 값을 반환
- *p++ : *p가 가리키는 곳의 값을 1 증가
- *++p : p를 1증가(포인터 주소를 다음 주소로 증가 후)한 다음에 그곳의 값을 가져오기
- ++*p : *p가 가리키는 곳의 값을 가져와서 1증가해서 반환
- (*p)++ : *p가 가리키는 곳의 값을 가져와서 사용 후 나중에 1증가
포인터로 문자열 저장하기
C 언어에서는 문자 포인터를 사용해서 문자열을 저장할 수 있습니다. 문자열을 저장할 때에는 포인터 변수 선언과 동시에 문자열 리터럴을 초기화하거나 선언 따로 초기화 따로를 할 수 있습니다.
코드: StringPointer.c
#include <stdio.h>
int main(void)
{
//[1] 문자열 선언과 동시에 초기화
char* name = "김서울";
//[2] 문자열 포인터 선언 후 초기화
char* address;
address = "SEOUL"; // 문자열(문자 배열)의 시작 주소를 포인터에 저장
// 전체 출력
printf("%s - %s\n", name, address);
// 부분 출력(인덱스 이후로 출력): 한글은 2byte 문자
printf("%s - %s\n", (name + 2), (address + 3));
return 0;
}
김서울 - SEOUL
서울 – UL
코드에서처럼 문자열 포인터 선언 후 따로 초기화할 때에는 *address가 아닌 address에 문자열 리터럴을 할당해야 합니다. 왜냐하면 문자열 리터럴은 그 자체가 배열의 시작 주소를 담고 있기 때문입니다.
포인터_문자열입출력.c***
코드: 포인터_문자열입출력.c
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(void)
{
//char* s;
char s[10];
//s = "안녕";
scanf("%s", s);
printf("%s\n", s);
return 0;
}
안녕
안녕
포인터로 문자열 저장하고 출력하기
pointer_array_string.c, 포인터배열_문자열.c
/*
포인터를 사용하여 문자배열(문자열)을 저장하기
*/
#include <stdio.h>
int main(void)
{
char c;
char* p;
c = 'A';
p = "Hello World"; // "Hello World\0"
printf("%c\n", c); // A
printf("%s\n", p); // Hello World
return 0;
}
A
Hello World
C 언어 포인터 전체 복습 설명
C 언어 포인터에 대한 전체 복습 강좌입니다.
강의
코드: 포인터설명_참조형.c
// 포인터설명_참조형.c
// 포인터(Pointer) : 참조형
#include <stdio.h>
void main()
{
// 값형(Value Type) 변수 : 해당 변수 이름공간에 데이터 저장
int a;
a = 10;
// 포인터형(Pointer Type;Reference Type) 변수 :
// 다른 변수의 공간을 참조(가리키는)하는 형태
int* p;
p = &a;//a변수의 주소(Address)를 포인터형 변수에 기록
// 출력
printf("a의 값 : %d\n", a);//10
printf("a의 주소 : %d\n", &a);//1244992
printf("p의 값 : %d\n", p);//1244992
printf("p가 가리키는 값 : %d", *p);//10
printf("p의 주소 : %d\n", &p);//1244992+-???=1244980
}
실행
a의 값 : 10
a의 주소 : 20184744
p의 값 : 20184744
p가 가리키는 값 : 10p의 주소 : 20184732
코드: 포인터배열설명.c
// 포인터배열설명.c
// 포인터 배열
#include <stdio.h>
void main()
{
int i;
int a[] = { 2, 1, 5, 4, 3 };
// 포인터 형 변수 선언
int* p;
p = a;//&a[0];//배열명 자체가 배열의 시작 주소
for (i = 0; i < 5; i++)
{
printf("배열: %d, 포인터: %d\n", a[i], *(p + i));
}
}
실행
배열: 2, 포인터: 2
배열: 1, 포인터: 1
배열: 5, 포인터: 5
배열: 4, 포인터: 4
배열: 3, 포인터: 3
코드: 포인터_문자열처리설명.c
// 포인터_문자열처리설명.c
#include <stdio.h>
void main()
{
char c = 'A'; // 값형
char* s; // string s; // 참조형
s = "안녕하\0세요.";
printf("%c %s\n", c, s);
printf("%s\n", s + 7);//영문1Byte, 한글2Byte
}
실행
A 안녕하
세요.
코드: 포인터_포인터배열설명.c
// 포인터_포인터배열설명.c
// 포인터 배열
#include <stdio.h>
void main()
{
char* s[3];
s[0] = "AA";
s[1] = "BB";
s[2] = "CC";
printf("%s %s %s\n", s[0], s[1], s[2]);
}
실행
AA BB CC
11 포인터를 사용한 문자열 출력 제어
pointer_array_string_literal.c, 포인터배열_문자열출력.c
// 포인터를 사용한 문자열 출력 제어
#include <stdio.h>
int main(void)
{
// C언어: 문자열 처리 키워드: X, Java/C#: String 키워드
//string s = "안녕하세요...";
char* str;
str = "안녕하세요. 반갑습니다."; // 한글/일어/중국어: 2byte코드, 영어: 1byte코드
printf("%s\n", str); // 전체 출력
printf("%s\n", str + 12); // 반갑습니다.
printf("%s\n", str + 3); // '녕'을 반쪽만 출력하다보니 깨져서 보여짐
str = "상수에 값을 덮어쓰기"; // 허용, scanf() 접근 불가
printf("%s\n", str); // 전체 출력
return 0;
}
안녕하세요. 반갑습니다.
반갑습니다.
聆究셀? 반갑습니다.
상수에 값을 덮어쓰기
포인터로 문자열 배열 선언하기
C 언어에서 문자열 형식을 만들 수 있는 char* 데이터 타입으로 하나의 이상의 문자열을 담을 수 있는 문자열 배열을 선언할 수 있습니다. 문자열 배열은 선언 후 따로 각각의 요소를 문자열 리터럴로 저장할 수 있습니다.
코드: StringArray.c
#include <stdio.h>
int main(void)
{
char* languages[3]; // 문자열 배열 선언
languages[0] = "C";
languages[1] = "C++";
languages[2] = "C#";
for (int i = 0; i < 3; i++)
{
printf("%s\n", languages[i]);
}
return 0;
}
C
C++
C#
문자열 리터럴은 그 자체가 첫 번째 문자의 시작 주소를 담고 있기에 포인터 배열의 각 요소에 대입되어 사용할 수 있습니다.
포인터로 배열의 배열 표현하기
pointer_array_zigzag.c, 포인터배열_배열의배열.c
// 배열의 배열
#include <stdio.h>
int main(void)
{
char* arr[3]; // 기본: 문자 3개 저장, 포인터 배열: 문자열 3개를 저장해 놓을 그릇
arr[0] = "C언어";
arr[1] = "C++";
arr[2] = "C#";
for (int i = 0; i < 3; i++)
{
printf("%s \n", arr[i]);
}
return 0;
}
C언어
C++
C#
gets 함수로 문자열 입력
pointer_array_gets.c, 포인터배열_문자열입력.c
// 화면으로부터 문자열을 입력받아서 출력하는 프로그램: scanf() 대신에 gets() 함수 사용
#include <stdio.h>
int main(void)
{
char str[100];
printf("문자열을 입력하시오 : ");
//scanf("%s", str);
gets(str); // 한줄의 문자열을 읽어오는 함수
printf("%s\n", str);
return 0;
}
문자열을 입력하시오 : C Language
C Language
포인터를 사용한 기초 알고리즘
포인터를 사용하여 최댓값 구하기
algorithm_pointer_max.c, 알고리즘_포인터_최댓값.c
// 최대값을 구하는 알고리즘
#include <stdio.h>
#include <limits.h> // INT_MIN, INT_MAX
void main(void)
{
//[1] Input
int intNum[] = { -3, -2, -1, -4, -5 };
int intMax = INT_MIN;
int i;
int* p; p = intNum; // &intNum[0];
//[2] Process: MAX
for (i = 0; i < 5; i++)
{
if (intMax < *(p + i))
{
intMax = *(p + i);
}
}
//[3] Output
printf("최대값: %d\n", intMax);
return 0;
}
최대값: -1
포인터를 사용하여 배열 정렬하기
algorithm_pointer_selectionsort.c, 알고리즘_포인터_선택정렬.c
#include <stdio.h>
// 포인터를 사용한 선택 정렬 함수
void SelectionSort(int* p)
{
int temp;
for (int i = 0; i < 5 - 1; i++) {
for (int j = i + 1; j < 5; j++) {
if (*(p + i) > *(p + j)) {
temp = *(p + i);
*(p + i) = *(p + j);
*(p + j) = temp;
}
}
}
}
int main(void)
{
//[1] Input
int intNum[5] = { 3, 2, 1, 5, 4 };
int temp;
//[2] Process: 정렬알고리즘(선택, 버블, 퀵, ...)
SelectionSort(intNum);
//for (int i = 0; i < 5 - 1; i++)
//{
// for (int j = i + 1; j < 5; j++)
// {
// if (intNum[i] > intNum[j])
// {
// temp = intNum[i];
// intNum[i] = intNum[j];
// intNum[j] = temp;
// }
// }
//}
//[3] Output
for (int i = 0; i < 5; i++)
{
printf("%d\n", intNum[i]);
}
return 0;
}
1
2
3
4
5
값 형식(Value Type)과 참조 형식(Reference Type)
클래스와 구조체와 같은 데이터 타입을 말할 때 값 형식(Value Type)과 참조 형식(Reference Type)으로 구분짓기도 합니다.
- 값 형식: 지금까지 다룬 int, double과 같은 데이터 구조는 내부적으로 구조체로 되어 있고 전형적인 값 형식의 데이터 구조입니다.
- 참조 형식: 모든 클래스의 부모 역할을 하는 최상위 클래스인 Object 클래스와 이를 나타내는 object 키워드는 전형적인 참조 형식의 데이터 구조입니다.
값 형식(Value Type)
값 형식은 개체에 값 자체를 담고 있는 구조입니다.
그림: 값 형식
참조 형식(Reference Type)
참조 형식은 개체가 값을 담고 있는 또 다른 개체를 포인터로 바라보는 구조입니다. 여러 값들이 동일한 개체를 가리킬 수 있습니다.
그림: 참조 형식
멤버 액세스 및 요소 액세스 연산자
포인터에서 사용할 수 있는 연산자에는 ->
(멤버 액세스) 연산자와 []
(요소 액세스) 연산자가 있습니다.
퀴즈
다음 중 동적으로 메모리를 할당하는 방법을 고르세요.
a. int *p = malloc(80);
정답: a
TODO:
포인터 관련 연산자
다음 연산자를 사용하여 포인터로 작업할 수 있습니다.
-
- 산술 연산자:
+
,-
,++
및--
- 비교 연산자:
==
,!=
,<
,>
,<=
및>=
NOTE
포인터를 사용한 작업에는 안전하지 않은 컨텍스트가 필요합니다. 안전하지 않은 블록을 포함하는 코드는 AllowUnsafeBlocks 컴파일러 옵션을 사용하여 컴파일해야 합니다.
Address-of 연산자 &
단항 &
연산자는 해당 피연산자의 주소를 반환합니다.
unsafe
{
int number = 27;
int* pointerToNumber = &number;
Console.WriteLine($"Value of the variable: {number}");
Console.WriteLine($"Address of the variable: {(long)pointerToNumber:X}");
}
// Output is similar to:
// Value of the variable: 27
// Address of the variable: 6C1457DBD4
&
연산자의 피연산자는 고정 변수여야 합니다. 고정 변수는 가비지 수집기의 작동에 영향을 받지 않는 스토리지 위치에 있는 변수입니다. 앞의 예제에서 로컬 변수 number
는 스택에 있으므로 고정 변수입니다. 가비지 수집기에 의해 영향을 받을 수 있는 스토리지 위치에 상주하는 변수(예: 재배치됨)를 이동 가능한 변수라고 합니다. 개체 필드 및 배열 요소는 이동 가능한 변수의 예입니다. fixed
문으로 "fix" 또는 "pin"으로 할 경우 이동 가능한 변수의 주소를 가져올 수 있습니다. 가져온 주소는 fixed
문 블록 내에서만 유효합니다. 다음 예제에서는 fixed
문과 &
연산자를 사용하는 방법을 보여줍니다.
unsafe
{
byte[] bytes = { 1, 2, 3 };
fixed (byte* pointerToFirst = &bytes[0])
{
// The address stored in pointerToFirst
// is valid only inside this fixed statement block.
}
}
변수
, 배열
과 달리 상수
또는 리터럴 값
의 주소를 가져올 수 없습니다.
포인터 간접 참조 연산자 *
단항 포인터 간접 참조 연산자 *
는 피연산자가 가리키는 변수를 가져옵니다. 역참조 연산자라고도 합니다. *
연산자의 피연산자는 포인터 형식이여야 합니다.
PointerIndirection.cs
unsafe
{
char letter = 'A';
char* pointerToLetter = &letter;
Console.WriteLine($"Value of the `letter` variable: {letter}");
Console.WriteLine($"Address of the `letter` variable: {(long)pointerToLetter:X}");
*pointerToLetter = 'Z';
Console.WriteLine($"Value of the `letter` variable after update: {letter}");
}
// Output is similar to:
// Value of the `letter` variable: A
// Address of the `letter` variable: DCB977DDF4
// Value of the `letter` variable after update: Z
*
연산자를 void*
유형의 식에 적용할 수 없습니다.
산술 연산자 학습을 통해서 알고 있는 단항 연산자가 아닌 이항 연산자로서 사용되는 *
연산자는 해당 숫자 피연산자의 곱을 컴퓨팅합니다.
포인터 멤버 액세스 연산자 ->
화살표 연산자로 불리는 ->
연산자는 포인터 간접 참조와 멤버 액세스를 결합합니다. 즉, x
가 T*
형식의 포인터이고 y
가 T
의 액세스 가능한 멤버인 경우 다음과 같이 사용될 수 있습니다.
x->y
위의 식은 아래의 삭과 동일합니다.
(*x).y
다음 예제에서는 ->
연산자의 사용법을 보여 줍니다.
public struct Coords
{
public int X;
public int Y;
public override string ToString() => $"({X}, {Y})";
}
public class PointerMemberAccessExample
{
public static unsafe void Main()
{
Coords coords;
Coords* p = &coords;
p->X = 3;
p->Y = 4;
Console.WriteLine(p->ToString()); // output: (3, 4)
}
}
You cannot apply the ->
operator to an expression of type void*
.
->
연산자를 void*
유형의 식에 적용할 수 없습니다.
포인터 요소 액세스 연산자 []
포인터 형식의 식 p
의 경우 p[n]
양식의 포인터 요소 액세스는 *(p + n)
으로 평가됩니다. 여기서 n
은 암시적으로 int
, uint
, long
또는 ulong
으로 전환할 수 있는 형식이어야 합니다.
다음 예제에서는 포인터 및 []
연산자를 사용하여 배열 요소에 액세스하는 방법을 보여 줍니다.
unsafe
{
char* pointerToChars = stackalloc char[123];
for (int i = 65; i < 123; i++)
{
pointerToChars[i] = (char)i;
}
Console.Write("Uppercase letters: ");
for (int i = 65; i < 91; i++)
{
Console.Write(pointerToChars[i]);
}
}
// Output:
// Uppercase letters: ABCDEFGHIJKLMNOPQRSTUVWXYZ
앞의 예제에서 stackalloc
식은 스택에 메모리 블록을 할당합니다.
NOTE
포인터 요소 액세스 연산자는 범위 이탈 오류를 검사하지 않습니다.
void
* 형식의 식으로 포인터 요소 액세스에 []
형태를 사용할 수 없습니다.
배열 요소 또는 인덱서 액세스에 대해 []
연산자를 사용할 수도 있습니다.
포인터 산술 연산자
포인터를 사용하여 다음과 같은 산술 연산을 수행할 수 있습니다.
- 포인터에서 정수 값 추가 또는 빼기
- 두 포인터 빼기
- 포인터 증가 또는 감소
void*
형식의 포인터를 사용하여 이러한 작업을 수행할 수 없습니다.
포인터에서 정수 값 더하기 또는 빼기
T*
형식의 포인터 p
및 int
, uint
, long
또는 ulong
으로 암시적으로 변환할 수 있는 형식의 n
식에 대한 더하기와 빼기가 다음과 같이 정의됩니다.
p + n
및n + p
식 모두p
에서 지정한 주소에n * sizeof(T)
를 추가한 결과T*
유형의 포인터를 생성합니다.p - n
식은p
에 의해 지정된 주소에서n * sizeof(T)
를 뺀 결과T*
유형의 포인터를 생성합니다.
sizeof
연산자는 바이트 단위의 형식 크기를 가져옵니다.
다음 예제에서는 포인터로 +
연산자를 사용하는 방법을 보여줍니다.
unsafe
{
const int Count = 3;
int[] numbers = new int[Count] { 10, 20, 30 };
fixed (int* pointerToFirst = &numbers[0])
{
int* pointerToLast = pointerToFirst + (Count - 1);
Console.WriteLine($"Value {*pointerToFirst} at address {(long)pointerToFirst}");
Console.WriteLine($"Value {*pointerToLast} at address {(long)pointerToLast}");
}
}
// Output is similar to:
// Value 10 at address 1818345918136
// Value 30 at address 1818345918144
포인터 빼기
T*
형식의 두 가지 포인터 p1
및 p2
에 대해 p1 - p2
식은 p1
과 p2
에 의해 지정된 주소 간의 차이를 sizeof(T)
로 나누어 생성합니다. 결과의 형식은 long
입니다. 즉, p1 - p2
는 ((long)(p1) - (long)(p2)) / sizeof(T)
로 계산됩니다.
다음 예제에서는 포인터 빼기를 보여줍니다.
unsafe
{
int* numbers = stackalloc int[] { 0, 1, 2, 3, 4, 5 };
int* p1 = &numbers[1];
int* p2 = &numbers[5];
Console.WriteLine(p2 - p1); // output: 4
}
포인터 증가 및 감소
++
증가 연산자는 포인터 피연산자에 1을 추가합니다. --
감소 연산자는 포인터 피연산자에서 1을 뺍니다.
두 연산자는 접미사(p++
및 p--
)와 접두사(++p
및 --p
)의 두 가지 형태로 지원됩니다. p++
와 p--
는 해당 라인에서의 실행 이전의 p
값입니다.
++p
와 --p
는 해당 라인에서의 실행 이후의 값입니다.
이 내용은 우리가 앞서 증감 연산자 파트에서 학습한 내용과 동일합니다.
The following example demonstrates the behavior of both postfix and prefix increment operators: 다음 예제에서는 접미사 및 접두사 증가 연산자의 동작을 보여줍니다.
unsafe
{
int* numbers = stackalloc int[] { 0, 1, 2 };
int* p1 = &numbers[0];
int* p2 = p1;
Console.WriteLine($"Before operation: p1 - {(long)p1}, p2 - {(long)p2}");
Console.WriteLine($"Postfix increment of p1: {(long)(p1++)}");
Console.WriteLine($"Prefix increment of p2: {(long)(++p2)}");
Console.WriteLine($"After operation: p1 - {(long)p1}, p2 - {(long)p2}");
}
// Output is similar to
// Before operation: p1 - 816489946512, p2 - 816489946512
// Postfix increment of p1: 816489946512
// Prefix increment of p2: 816489946516
// After operation: p1 - 816489946516, p2 - 816489946516
포인터 비교 연산자
==
, !=
, <
, >
, <=
및 >=
연산자를 사용하여 void*
를 포함한 모든 포인터 형식의 피연산자를 비교할 수 있습니다. 이러한 연산자는 두 피연산자가 지정한 주소를 부호 없는 정수인 것처럼 비교합니다.
연산자 우선 순위
다음 목록에서는 포인터 관련 연산자를 가장 높은 우선순위부터 가장 낮은 것으로 정렬합니다.
- 접미사 증가
x++
및 감소x--
연산자와->
및[]
연산자 - 접두사 증가
++x
및 감소--x
연산자와&
및*
연산자 - 가감 연산자인
+
및-
연산자 - 비교 연산자인
<
,>
,<=
및>=
연산자 - 같음
==
및!=
연산자
괄호 연산자인 ()
를 사용하여 연산자 우선순위에 따라 주어진 평가 순서를 변경합니다.
연산자 오버로드 가능성
사용자 정의 형식은 포인터 관련 연산자 &
, *
, ->
및 []
(을)를 오버로드할 수 없습니다.
안전하지 않은 코드, 포인터 형식 및 함수 포인터
NOTE
현재 문서는 다음 경로의 Microsoft Learn 자료를 번역했습니다. Unsafe code, pointer types, and function pointers. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
작성하는 대부분의 C# 코드는 "확인 가능한 안전한 코드"입니다. 확인 가능한 안전한 코드는 .NET 도구가 코드가 안전한지 확인할 수 있음을 의미합니다. 일반적으로 안전한 코드는 포인터를 사용하여 메모리에 직접 액세스하지 않습니다. 또한 원시 메모리를 할당하지 않습니다. 대신 관리형 개체를 만듭니다.
C#은 unsafe
컨텍스트를 지원하는데, 이 컨텍스트에서는 확인할 수 없는 코드를 작성할 수 있습니다. unsafe
컨텍스트에서 코드는 포인터를 사용하고, 메모리 블록을 할당 및 해제하고, 함수 포인터를 사용하여 메서드를 호출할 수 있습니다. C#의 안전하지 않은 코드가 반드시 위험한 것은 아닙니다. 단지 CLR에서 안전을 확인할 수 없을 뿐입니다.
안전하지 않은 코드에는 다음과 같은 속성이 있습니다.
- 메서드, 형식 및 코드 블록은 안전하지 않은 것으로 정의할 수 있습니다.
- 경우에 따라, 안전하지 않은 코드는 배열 범위 검사를 제거하여 애플리케이션의 성능을 향상할 수 있습니다.
- 포인터가 필요한 네이티브 함수를 호출하는 경우 안전하지 않은 코드가 필요합니다.
- 안전하지 않은 코드를 사용하면 보안 및 안정성 위험이 발생합니다.
- 안전하지 않은 블록을 포함하는 코드는 AllowUnsafeBlocks 컴파일러 옵션을 사용하여 컴파일해야 합니다.
포인터 형식
안전하지 않은 컨텍스트에서, 형식은 포인터 형식일 수도 있고 값 형식 또는 참조 형식일 수도 있습니다. 포인터 형식 선언은 다음 형식 중 하나를 사용합니다.
type* identifier;
void* identifier; // 사용 가능하나 권장하지 않음
포인터 형식에서 *
앞에 지정된 형식을 참조 형식이라고 합니다. 비관리형 형식만 참조 형식일 수 있습니다.
포인터 형식은 개체
에서 상속되지 않으며 포인터 형식과 object
는 서로 변환되지 않습니다. 또한 박싱과 언박싱은 포인터를 지원하지 않습니다. 그러나 다른 포인터 형식 간의 변환 및 포인터 형식과 정수 형식 사이의 변환은 허용됩니다.
When you declare multiple pointers in the same declaration, you write the asterisk (*
) together with the underlying type only. It isn't used as a prefix to each pointer name. For example:
동일한 선언에서 여러 포인터를 선언하는 경우 별표(*
)는 기본 형식에만 함께 사용됩니다. 각 포인터 이름의 접두사로는 사용되지 않습니다. 예를 들면 다음과 같습니다.
int* p1, p2, p3; // 허용
int *p1, *p2, *p3; // C#에선 불가능
개체 참조는 포인터가 해당 개체 참조를 가리키는 경우에도 가비지 수집될 수 있으므로 포인터는 참조나 참조가 들어 있는 구조체를 가리킬 수 없습니다. 가비지 수집기는 포인터 형식에서 개체를 가리키는지 여부를 추적하지 않습니다.
MyType*
형식의 포인터 변수 값은 MyType
형식의 변수 주소입니다. 다음은 포인터 형식 선언의 예제입니다.
int* p
:p
는 정수에 대한 포인터입니다.int** p
:p
는 정수에 대한 포인터를 가리키는 포인터입니다.int*[] p
:p
는 정수에 대한 포인터의 1차원 배열입니다.char* p
:p
는 문자에 대한 포인터입니다.void* p
:p
는 알 수 없는 형식에 대한 포인터입니다.
포인터 간접 참조 연산자 *
를 사용하면 포인터 변수가 가리키는 위치의 내용에 액세스할 수 있습니다. 예를 들어, 다음 선언을 참조하십시오.
int* myVariable;
...
*myVariable
*myVariable
식을 사용하면 myVariable
변수가 가리키는 곳의 int
형식의 값을 가져옵니다.
fixed
문에 대한 문서에는 몇 가지 포인터 예제가 있습니다. 다음 예제는 unsafe
키워드 및 fixed
문을 사용하고 정수 포인터를 증분하는 방법을 보여줍니다. 이 코드를 실행하려면 콘솔 애플리케이션의 Main 메서드에 붙여 넣습니다. 이러한 예제는 AllowUnsafeBlocks 컴파일러 옵션으로 컴파일되어야 합니다.
void*
형식의 포인터에는 간접 참조 연산자를 적용할 수 없습니다. 그러나 캐스트를 사용하여 void 포인터를 다른 포인터 형식으로 변환하거나 반대로 변환할 수 있습니다.
포인터는 null
일 수 있습니다. null 포인터에 간접 참조 연산자를 적용할 때 발생하는 동작은 구현에 따라 다릅니다.
메서드 사이에 포인터를 전달하면 정의되지 않은 동작이 발생할 수 있습니다. in
, out
또는 ref
매개 변수를 통해, 또는 함수 결과로 지역 변수에 포인터를 반환하는 메서드를 고려합니다. fixed
블록에서 포인터가 설정되면 이 포인터가 가리키는 변수의 고정 상태가 해제될 수 있습니다.
다음 표에서는 안전하지 않은 컨텍스트에서 포인터에 대해 수행할 수 있는 연산자와 문을 보여 줍니다.
연산자/문 | 설명 |
---|---|
* |
포인터 간접 참조를 수행합니다. |
-> |
포인터를 통해 구조체 멤버에 액세스합니다. |
[] |
포인터를 인덱싱합니다. |
& |
변수 주소를 가져옵니다. |
++ 및 -- |
포인터를 증가 및 감소시킵니다. |
+ 및 - |
포인터 산술 연산을 수행합니다. |
== , != , < , > , <= 및 >= |
포인터를 비교합니다. |
stackalloc |
스택에 메모리를 할당합니다. |
fixed 문 |
해당 주소를 찾을 수 있도록 임시로 변수를 고정합니다. |
모든 포인터 형식을 암시적으로 void*
형식으로 변환할 수 있습니다. 모든 포인터 형식에 값 null
을 할당할 수 있습니다. 캐스트 식을 사용하여 모든 포인터 형식을 명시적으로 다른 포인터 형식으로 변환할 수 있습니다. 정수 형식을 포인터 형식으로 변환하거나, 포인터 형식을 정수 형식으로 변환할 수도 있습니다. 이 변환에는 명시적 캐스트가 필요합니다.
다음 예제에서는 int*
를 byte*
로 변환합니다. 포인터는 변수의 최하위 주소 지정 바이트를 가리킵니다. int
크기(4바이트)까지 결과를 연속적으로 증가할 경우 변수의 나머지 바이트를 표시할 수 있습니다.
고정 크기 버퍼
fixed
키워드를 사용하여 데이터 구조에 고정 크기 배열이 있는 버퍼를 만들 수 있습니다. 고정 크기 버퍼는 다른 언어 또는 플랫폼의 데이터 원본과 상호 운용되는 메서드를 작성할 때 유용합니다. 고정 크기 버퍼는 일반 구조체 멤버에 허용되는 모든 특성 또는 한정자를 사용할 수 있습니다. 배열 형식이 bool
, byte
, char
, short
, int
, long
, sbyte
, ushort
, uint
, ulong
, float
, 또는 double
이어야 한다는 것이 유일한 제한 사항입니다.
private fixed char name[30];
안전한 코드에서 배열을 포함하는 C# 구조체는 배열 요소를 포함하지 않습니다. 대신 구조체에 요소에 대한 참조가 포함되어 있습니다. unsafe
코드 블록에서 사용될 경우 구조체에 고정 크기 배열을 포함할 수 있습니다.
pathName
이 참조이므로 다음 struct
의 크기는 배열에 있는 요소의 수에 따라 달라지지 않습니다.
구조체는 안전하지 않은 코드에 포함된 배열을 포함할 수 있습니다. 다음 예제에서 fixedBuffer
배열은 고정 크기입니다. fixed
문을 사용하여 첫 번째 요소에 대한 포인터를 가져옵니다. 이 포인터를 통해 배열의 요소에 액세스합니다. fixed
문은 fixedBuffer
의 인스턴스 필드를 메모리의 특정 위치에 고정합니다.
128개 요소 char
배열의 크기는 256바이트입니다. 고정 크기 문자 버퍼는 인코딩에 관계없이 항상 문자당 2바이트를 사용합니다.
또 다른 일반적인 고정 크기 배열은 bool
배열입니다. bool
배열의 요소 크기는 항상 1바이트입니다. bool
배열은 비트 배열이나 버퍼를 만드는 데 적합하지 않습니다.
이전 예제에서는 고정 크기 버퍼가 있는 unsafe struct
방법을 보여줍니다.
internal unsafe struct Buffer
{
public fixed char fixedBuffer[128];
}
Buffer
에 대해 컴파일에서 생성된 C#은 다음과 같은 특성이 있습니다.
internal struct Buffer
{
[StructLayout(LayoutKind.Sequential, Size = 256)]
[CompilerGenerated]
[UnsafeValueType]
public struct <fixedBuffer>e__FixedBuffer
{
public char FixedElementField;
}
[FixedBuffer(typeof(char), 128)]
public <fixedBuffer>e__FixedBuffer fixedBuffer;
}
고정 크기 버퍼는 다음과 같은 방법으로 일반 배열과 다릅니다.
unsafe
컨텍스트에서만 사용할 수 있습니다.- 단지 구조체의 인스턴스 필드일 수 있습니다.
- 항상 벡터 또는 1차원 배열입니다.
- 선언에는
fixed char id[8]
와 같은 길이가 포함되어야 합니다.fixed char id[]
를 사용할 수 없습니다.
포인터를 사용하여 바이트 배열을 복사하는 방법
다음 예제에서는 포인터를 사용하여 배열 간에 바이트를 복사합니다.
이 예제에서는 Copy
메서드에서 포인터를 사용할 수 있도록 하는 unsafe
키워드를 사용합니다. fixed
문은 소스 및 대상 배열에 대한 포인터를 선언하는 데 사용됩니다. fixed
문은 소스 및 대상 배열의 위치가 메모리에 고정되므로 가비지 수집에 의해 이동되지 않습니다. fixed
블록이 완료되면 배열에 대한 메모리 블록이 고정 해제됩니다. 이 예제의 Copy
메서드는 unsafe
키워드를 사용하므로 AllowUnsafeBlocks 컴파일러 옵션으로 컴파일해야 합니다.
이 예제에서는 두 번째 관리되지 않는 포인터보다는 인덱스를 사용하여 두 배열의 요소에 액세스합니다. pSource
및 pTarget
포인터의 선언은 배열을 고정합니다. 이 기능은 C# 7.3부터 사용할 수 있습니다.
함수 포인터
C#은 안전한 함수 포인터 개체를 정의하는 delegate
형식을 제공합니다. 대리자를 호출하려면 System.Delegate
에서 파생된 형식을 인스턴스화하고 해당 Invoke
메서드에 대한 가상 메서드 호출을 수행해야 합니다. 이 가상 호출은 callvirt
IL 명령을 사용합니다. 성능이 중요한 코드 경로에서는 calli
IL 명령을 사용하는 것이 더 효율적입니다.
delegate*
구문을 사용하여 함수 포인터를 정의할 수 있습니다. 컴파일러는 delegate
개체를 인스턴스화하고 Invoke
를 호출하는 대신 calli
명령을 사용하여 함수를 호출합니다. 다음 코드에서는 delegate
또는 delegate*
를 사용하여 같은 형식의 두 개체를 결합하는 두 메서드를 선언합니다. 첫 번째 메서드는 System.Func<T1,T2,TResult>
대리자 형식을 사용합니다. 두 번째 메서드는 동일한 매개 변수 및 반환 형식이 포함된 delegate*
선언을 사용합니다.
다음 코드에서는 정적 로컬 함수를 선언하고 해당 로컬 함수에 대한 포인터를 사용하여 UnsafeCombine
메서드를 호출하는 방법을 보여 줍니다.
위의 코드는 함수 포인터로 액세스되는 함수에 대한 몇 가지 규칙을 보여 줍니다.
- 함수 포인터는
unsafe
컨텍스트에서만 선언할 수 있습니다. delegate*
를 사용하거나delegate*
를 반환하는 메서드는unsafe
컨텍스트에서만 호출할 수 있습니다.- 함수의 주소를 가져오는
&
연산자는static
함수에서만 사용할 수 있습니다. 이 규칙은 멤버 함수와 로컬 함수에 모두 적용됩니다.
구문은 delegate
형식 선언 및 포인터 사용과 유사합니다. delegate
의 *
접미사는 선언이 함수 포인터임을 나타냅니다. 메서드 그룹을 함수 포인터에 할당할 때 &
는 연산에 메서드의 주소가 사용됨을 나타냅니다.
managed
및 unmanaged
키워드를 사용하여 delegate*
에 대한 호출 규칙을 지정할 수 있습니다. 또한 unmanaged
함수 포인터의 경우 호출 규칙을 지정할 수 있습니다. 다음 선언에서는 각각의 예제를 보여 줍니다. 첫 번째 선언은 managed
호출 규칙(기본값)을 사용합니다. 다음 4개에서는 unmanaged
호출 규칙을 사용합니다. 각각은 ECMA 335 호출 규칙(Cdecl
, Stdcall
, Fastcall
또는 Thiscall
) 중 하나를 지정합니다. 마지막 선언은 플랫폼에 unmanaged
대한 기본 호출 규칙을 선택하도록 CLR에 지시하는 호출 규칙을 사용합니다. CLR은 런타임에 호출 규칙을 선택합니다.