지침 변수를 참고 목적에 전달할 때에는 반드시 자세하게 참고해야 한다
개시하다
C 언어에서 쉽게 넘어질 수 있다고 여겨지는 지침.
익숙해지면 별 문제 없을 것 같아요.
만약 적당한 코드를 썼다면, 약간의 지루한 일을 일으킬 수도 있다.
문제 코드
이런 느낌의 코드를 써 봤어요.
void creator(int* p){
p = new int;
*p = 1;
}
void release(int* p){
if(p != nullptr) delete p;
}
int main(){
int* test = nullptr;
creator(test);
std::cout << "value: "<< *test << std::endl;
release(test);
}
결과는 다음과 같다.value: 1
실제로는 Segmentation Fault가 발생합니다.왜 바늘 위의 동적 메모리에 입력 값이 없습니까?
알아볼게요.
void creator(int* p){
std::cout << "pre_creator_address=" << p << std::endl;
p = new int;
*p = 1;
std::cout << "post_creator_address=" << p << std::endl;
}
void release(int* p){
if(p != nullptr) delete p;
}
int main(){
int* test = nullptr;
std::cout << "pre_main_address=" << test << std::endl;
creator(test);
std::cout << "post_main_address=" << test << std::endl;
//std::cout << "value: "<< *test << std::endl;
release(test);
}
출력은 다음과 같다.pre_main_address=0
pre_creator_address=0
post_creator_address=0x55fcad30d280
post_main_address=0
출력을 보면 알 수 있습니다."크리에이터에게 맡긴 테스트와 크리에이터 안의 p는 다른 것이다."
즉, "바늘 변수 값은 나에게 맡겼다."그러니까
참고 목적이라면 이렇게 할게요.
void creator(int*& p){
std::cout << "pre_creator_address=" << p << std::endl;
p = new int;
*p = 1;
std::cout << "post_creator_address=" << p << std::endl;
}
void release(int*& p){
if(p != nullptr) delete p;
}
int main(){
int* test = nullptr;
std::cout << "pre_main_address=" << test << std::endl;
creator(test);
std::cout << "post_main_address=" << test << std::endl;
std::cout << "value: "<< *test << std::endl;
release(test);
}
참고교환을 통해 명확히 표시한다면 다음과 같이 출력을 기대합니다.pre_main_address=0
pre_creator_address=0
post_creator_address=0x55f7d50c3280
post_main_address=0x55f7d50c3280
value: 1
지침 변수의 지침 전달이 작용하는 조건
근데 평소에 이런 거 신경 안 써도 움직일 수 있는 코드가 있죠?
void creator(int* p){
std::cout << "pre_creator_address=" << p << std::endl;
*p = 1;
std::cout << "post_creator_address=" << p << std::endl;
}
void release(int* p){
if(p != nullptr) delete p;
}
int main(){
int* test = new int;
std::cout << "pre_main_address=" << test << std::endl;
creator(test);
std::cout << "post_main_address=" << test << std::endl;
std::cout << "value: "<< *test << std::endl;
release(test);
}
이때creator에 건네주는 매개 변수는 지침으로 전달된다.pre_main_address=0x55b7db98ce70
pre_creator_address=0x55b7db98ce70
post_creator_address=0x55b7db98ce70
post_main_address=0x55b7db98ce70
value: 1
main측지침 변수가 참조된 상태일 때 이동합니다.하지만 코드만 보면 읽을 수 없겠지, 이건...
최후
원래 이론이었어요.
바늘 변수를 처리할 때는 선언의 범위 내에서 동적 확보를 해야 한다.
C++에는 이런 탈출로가 있지만, C에서도 같은 상황이 발생할 수 있다
C의 경우 포인터 변수의 참조를 만들어 이 탈출로에 건네주면
아래처럼 쌍바늘로 매개 변수의 유형을 실현하기 때문에 좀 힘들다.
void creator(int** p){
printf("pre_creator_address: 0x%x\n", *p);
*p = (int*) malloc(1);
**p = 1;
printf("post_creator_address: 0x%x\n", *p);
}
int main(){
int* test = NULL;
printf("pre_creator_address: 0x%x\n", test);
creator(&test);
printf("pre_creator_address: 0x%x\n", test);
printf("value: %d\n", *test);
free(test);
}
가능하면 이런 일은 없었으면 좋겠어요.설령 이런 상황을 실시하려고 하더라도 참고 납품이라고 명기하는 것이 가장 좋다
가독성 면에서도 다양한 편의가 있어야 한다.
Appendix
역어셈블리 차이 확인
인코딩 등급이 어떻게 다른지 볼 수 있습니까?
입금 코드는 그 정도까지는 읽을 수 없지만 아는 범위 내에서 한번 보고 싶어요.
아래의 치밀한 코드로 관측해 보았다.
※ 실행 환경은 x86-64, 컴파일러는 gcc, 비최적화 옵션으로 출력
시도한 환경에 따라 출력 결과가 달라지고 싶습니다.
[test0]
void creator(int* p){
}
int main(){
int* test = nullptr;
creator(test);
}
[test1]
void creator(int*& p){
}
int main(){
int* test = nullptr;
creator(test);
}
[test2]
void creator(int* p){
}
int main(){
int* test = new int;
creator(test);
}
는 먼저creator 측의 반조립 결과를 비교했다.[test0]
000000000000073a <_Z7creatorPi>:
73a: 55 push %rbp
73b: 48 89 e5 mov %rsp,%rbp
73e: 48 89 7d f8 mov %rdi,-0x8(%rbp)
742: 90 nop
743: 5d pop %rbp
744: c3 retq
[test1]
00000000000007aa <_Z7creatorRPi>:
7aa: 55 push %rbp
7ab: 48 89 e5 mov %rsp,%rbp
7ae: 48 89 7d f8 mov %rdi,-0x8(%rbp)
7b2: 90 nop
7b3: 5d pop %rbp
7b4: c3 retq
creator 함수에서 실행된 명령은 모두 같다.(개인적으로 흥미로운 것은test2모드의 참조교부는 함수명 뒤에 R이 부여된다는 점이다.
설명은test0과 같아도 컴파일러가 참조 교부로 해석되었는지 여부에 따라 달라집니다.
그럼 크리에이터 함수를 호출하는main에 여러 가지 변화가 있나요?
00000000000007fa <_Z7creatorRPi>:
7fa: 55 push %rbp
7fb: 48 89 e5 mov %rsp,%rbp
7fe: 48 89 7d f8 mov %rdi,-0x8(%rbp)
802: 90 nop
803: 5d pop %rbp
804: c3 retq
movq$0x0,-0x8(%rbp)의 명령rbp가 바늘 변수로 NULL(=0)을 안에 저장하는 것을 알 수 있다.
mov-0x8 (%rbp),%rax 명령으로 rax에 -0x8 (%rbp) 의 내용을 저장합니다 NULL.
mov%rax,%rdi 명령어를 rdi (함수의 매개 변수) 에 저장합니다.
크리에이터 함수라는 프로세스입니다.
이 경우 크리에이터의 경우 값인 NULL만 지나간 상태입니다.
가격으로 납품한 것으로 추정할 수 있죠.
[test0]
0000000000000745 <main>:
745: 55 push %rbp
746: 48 89 e5 mov %rsp,%rbp
749: 48 83 ec 10 sub $0x10,%rsp
74d: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
754: 00
755: 48 8b 45 f8 mov -0x8(%rbp),%rax
759: 48 89 c7 mov %rax,%rdi
75c: e8 d9 ff ff ff callq 73a <_Z7creatorPi>
761: b8 00 00 00 00 mov $0x0,%eax
766: c9 leaveq
767: c3 retq
test0과의 차이로mov%fs:0x28,%rax라는 명령이 추가되었습니다.
조사를 해보면 Stack Guard 기능입니다.
(xor%fs:0x28,%rdx에서 스택 오버플로우가 발생했는지 확인합니다.)
여기를 건너뛰고 보니 다음 명령군이 마음에 든다.
[test1]
00000000000007b5 <main>:
7b5: 55 push %rbp
7b6: 48 89 e5 mov %rsp,%rbp
7b9: 48 83 ec 10 sub $0x10,%rsp
7bd: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
7c4: 00 00
7c6: 48 89 45 f8 mov %rax,-0x8(%rbp)
7ca: 31 c0 xor %eax,%eax
7cc: 48 c7 45 f0 00 00 00 movq $0x0,-0x10(%rbp)
7d3: 00
7d4: 48 8d 45 f0 lea -0x10(%rbp),%rax
7d8: 48 89 c7 mov %rax,%rdi
7db: e8 ca ff ff ff callq 7aa <_Z7creatorRPi>
7e0: b8 00 00 00 00 mov $0x0,%eax
7e5: 48 8b 55 f8 mov -0x8(%rbp),%rdx
7e9: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx
7f0: 00 00
7f2: 74 05 je 7f9 <main+0x44>
7f4: e8 77 fe ff ff callq 670 <__stack_chk_fail@plt>
7f9: c9 leaveq
7fa: c3 retq
mov 명령을 통해 rax를 바늘 변수에 저장합니다.그런 다음 0x10(%rbp) 에 0x0 (아마도 NULL) 을 저장합니다.
다음에 lea 명령을 통해 rax를 -0x10 (%rbp) 의 주소로 변경합니다.
자세히 분석할 수는 없지만.
이 경우 -0x10 (%rbp) 주소 자체를creator에 전달하기 때문에
그러니까 이미 참조 넘겼다는 거죠?
mov %rax,-0x8(%rbp)
movq $0x0,-0x10(%rbp)
lea -0x10(%rbp),%rax
중간에 new라는 사람이 있다는 점을 제외하면test1과 같은 절차다.어셈블러의 결과만 보다
왜냐하면 납품 가격인지 참고 납품인지 잘 모르겠어요.
추측은 컴파일러에서 매우 좋은 해석을 진행하였다.
결론이 없어서 죄송하지만 여기까지로 확인합니다.
Reference
이 문제에 관하여(지침 변수를 참고 목적에 전달할 때에는 반드시 자세하게 참고해야 한다), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://zenn.dev/rain_squallman/articles/8c64e85ed18574ad6236텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)