지침 변수를 참고 목적에 전달할 때에는 반드시 자세하게 참고해야 한다

8430 단어 C++tech

개시하다


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과 같은 절차다.
어셈블러의 결과만 보다
왜냐하면 납품 가격인지 참고 납품인지 잘 모르겠어요.
추측은 컴파일러에서 매우 좋은 해석을 진행하였다.
결론이 없어서 죄송하지만 여기까지로 확인합니다.

좋은 웹페이지 즐겨찾기