C & C++/C

[C] C언어 질문: 두 배열 a와 b에서 b = a 에서 에러가 발생하는 이유는 무엇일까?

Razelo 2021. 11. 21. 10:35

새벽에 누군가가 재밌는 질문을 올려놨다. 짧은 글이라 그냥 휙 보고 넘기려 했다. 그런데 글을 보고 고민했는데 나도 감이 잡히질 않아서 아침에 직접 dev C++를 켜고 값을 찍어보았다. 

 

올라온 질문은 다음과 같았다. 


배열은 포인터와 유사하지만 포인터 상수이다. 즉 대입이 불가능하다. 근데 왜 전자의 경우에선 오류가 나지 않고

후자의 경우에서 오류가 발생하는가? 

void function(int a[]){ 
	int b[5];
	a = b;
}
void main(){
	int a[10];
	function(a); 
}
void main(){
	int a[10];
    int b[4];
    b = a; 
}

배열은 배열의 첫 번째 요소의 주소값을 가지고 있는 것이다. 배열 변수는 주소값을 받을 수 있으니 b = a; 와  같이 만든다면 b가 a배열을 가리킬 수 있는 것 아니냐? 어느 부분이 틀린것인가? 


말을 줄이기 위해 위 질문의 전자 코드를 1번 코드, 후자 코드를 2번 코드라고 하겠다. 핵심을 알면 너무나 간단한 내용이지만 가끔 이렇게 직접 디버깅해가며 이유를 찾아내는걸 좋아해서 미리 찍어보며 이유를 찾아보았다. 결론만 빠르게 보고 싶은 사람은 맨 마지막 줄의 결론 부분에서 빨간 글씨의 내용을 읽으면 바로 이해가 된다. 

 

댓글이 여럿 달렸는데 도통 나도 이유가 잡히지 않아서 그냥 막연하게 고민해봤는데 명확하게 생각이 나질 않았다. 애초에 그냥 컴파일러에서 막았다던지 혹은 파라미터 패싱할적에 copy를 하는 방식으로 값을 전달하는데 이때 callee의 파라미터는 callee만의 local variable로 판정되니 여기서 완전히 다른 변수이기에 뭔가 좀 달라지지 않을까 라고 생각해봤는데 좀더 생각해보니 그건 문제가 아니었다. local variable로 판정되더라도 배열의 이름은 배열의 첫 주소를 나타내니 전혀 이상할 점이 없었다. 즉 내가 모르고 있는 무언가가 더 있다고 판단되었다. 그리고 어느정도 납득이 되는 답을 찾아냈다. 해당 내용은 아래와 같다. 중간에 주석으로 이런저런 얘기를 많이 써놨는데 모르는 문제이다보니 이것 저것 상황을 많이 가정해놨다. 주석은 신경쓰지 않고 코드만 실행시켜보면 된다. 

#include<stdio.h>

void function(int a[]){ 
	printf("inner function a = : %d\n",a);  
	printf("inner function &a = : %d\n",&a);
	printf("inner function *a = : %d\n",*a);
	printf("inner function size of a: %d\n",sizeof(a));
	
	int i; 
	for(i = 0; i<4; i++){
		printf("inner function a value: %d, address: %d\n", a[i], &a[i]);
	}
	
	int b[5] = {5, 6, 7, 8};
	printf("inner function b = : %d\n",b);  
	printf("inner function &b = : %d\n",&b);
	printf("inner function *b = : %d\n",*b);
	printf("inner function size of b: %d\n",sizeof(b));
	//b = a; // 답 나왔다. b = a 에서 b는 상수이다. 즉 메모리셀의 주소를 나타내는데 이 셀에 무언가를 할당하려 하는 행위는 lvalue인 상수에 할당을 하려 하는 행위이다.  
	// b = a; // b = a 는 할당 불가임 lvalue가 modifiable인 object라는 조건 하에서만 할당가능한데 array type은 modifiable한 object가 아님 
	// in c11: 
	// 1. assignment operator shall have a modifiable lvalue as its operand
	// 2. A modifiable lvalue is an lvalue that does not have array type.  
	a = b;    // 반면 a = b 는 할당가능함? 왜냐면 포인터이기 때문에  
	//int *c = b; 
	

	for(i = 0; i< 4; i++){
		printf("array a value: 여기서 5 6 7 8 이 나올까요? %d\n", a[i]); // 5 6 7 8 이 나올까?  
	}
	
	// 그렇다면 modifiable하지 않은 array type대신 포인터를 사용하면 어떨까?  
	int *c;
	c = b;
	for(i = 0; i< 4; i++){
		printf("포인터 c를 통해 배열을 출력해보면?  %d\n", c[i]); // 5 6 7 8 이 나올까?  
	}
	
}

void main(){
	int a[4] = {1,2,3,4};
	int b[4] = {5,6,7,8};
	
	printf("outer function a = : %d\n",a);  
	printf("outer function &a = : %d\n",&a);
	printf("outer function *a = : %d\n",*a);
	printf("outer function size of a: %d\n",sizeof(a));
	int i; 
	for(i = 0; i<4; i++){
		printf("outer function a value: %d, address: %d\n", a[i], &a[i]);
	}
	function(a);
}

위 코드를 실행시키면 아주 흥미로운 결과가 나온다. 

outer function a = : 6487552
outer function &a = : 6487552
outer function *a = : 1
outer function size of a: 16
outer function a value: 1, address: 6487552
outer function a value: 2, address: 6487556
outer function a value: 3, address: 6487560
outer function a value: 4, address: 6487564
inner function a = : 6487552
inner function &a = : 6487504
inner function *a = : 1
inner function size of a: 8
inner function a value: 1, address: 6487552
inner function a value: 2, address: 6487556
inner function a value: 3, address: 6487560
inner function a value: 4, address: 6487564
inner function b = : 6487440
inner function &b = : 6487440
inner function *b = : 5
inner function size of b: 20
array a value: 여기서 5 6 7 8 이 나올까요? 5
array a value: 여기서 5 6 7 8 이 나올까요? 6
array a value: 여기서 5 6 7 8 이 나올까요? 7
array a value: 여기서 5 6 7 8 이 나올까요? 8
포인터 c를 통해 배열을 출력해보면?  5
포인터 c를 통해 배열을 출력해보면?  6
포인터 c를 통해 배열을 출력해보면?  7
포인터 c를 통해 배열을 출력해보면?  8

 

 

outer function a = : 6487552
outer function &a = : 6487552
outer function *a = : 1
outer function size of a: 16

위 출력문을 보자. main에서 outer function에 대한 내용을 출력한 것이 처음 존재한다. 배열의 이름인 a와 그 주소인 &a를 출력했고 그리고 *a를 통해 직접 참조해보았다. main에는 현재 int a[4] = {1,2,3,4}; 정의되어있는데 그 배열의 이름인 a를 줬을때  6487552를 출력했고 역시나 &a를 통해 그 주소를 출력하고자 했을때는 6487552 를 당연히 출력했다. 그리고 여기에 확실하게 *a를 통해서 첫번째 요소인 1을 출력할 수 있었다. 그러니 배열의 이름이 배열 첫 요소의 주소라는 말은 너무나 당연해진다. 그리고 size of a를 출력했을때 그 길이가 16으로 (int형 4개 = 4x4) 임을 알 수 있다. 

그리고 이어서 for 문을 돌면서 아래의 출력문을 확인할 수 있다. 

outer function a value: 1, address: 6487552
outer function a value: 2, address: 6487556
outer function a value: 3, address: 6487560
outer function a value: 4, address: 6487564

요소 4개가 int형사이즈만큼 차곡차곡 메모리셀에 들어가있는 것을 확인할 수 있었다.

그리고 다음 출력문인 아래의 출력문에서 흥미로운 점이 하나 등장한다. 

inner function a = : 6487552
inner function &a = : 6487504
inner function *a = : 1
inner function size of a: 8

신기하게도 a를 출력했을때는 6487552가 나오지만 &a 를 출력했을때는 6487504라는 결과가 나온다. 처음엔 갸우뚱할 수 있지만 당연한 일이다. 왜냐면 callee 에서 선언된 그 어느 것도 즉 대표적으로 파라미터의 경우 local variable로 취급된다. 그러니 함수 파라미터부에 정의된 int a[] 에 대한 새로운 메모리 셀이 잡히는 것은 당연하다. 그리고 그 location이 6487504 이라는 소리다. 그리고 파라미터에 정의된 int a[] 에서 a는 포인터로 작동하게 된다. 왜냐면 그 값이 주소를 담고 있기 때문이다.(6487552) 그리고 포인터도 당연히 자신의 메모리셀이 존재하므로 자신의 주소인 6487504 에 잡혀있는 것이다. 이를 확인할 수 있는 방법은 간단하다. 위의 예시 코드에서 function(int a[])를 function(int *a)로 바꾸어도 단 하나도 변하지 않고 정확히 동일하게 작동한다. 또한 *a가 가리키는 것이 caller의 a = {1, 2, 3, 4}의 첫번쨰 요소인 1을 가리키는 것을 알 수 있다. <다만 여기서 아직도 이해가지 않는 부분이 있는데 이 부분은 조금 더 생각해봐야겠다. 그부분은 바로 inner function size of a: 8이다. 왜 여기서 8이라는 사이즈가 나왔을까? double로 잡힌것도 아니고 int형이 2개인것도 아닌데 어떻게 8이라는 값이 나올 수 있는지에 대해 아직 감이 잡히지 않는데 이부분은 핵심이 아니기에 일단은 건너뛰고 나중에 조금 더 확인해보겠다. >

 

inner function a value: 1, address: 6487552
inner function a value: 2, address: 6487556
inner function a value: 3, address: 6487560
inner function a value: 4, address: 6487564

그 다음 출력문을 보면 outer function 에 대한 값과 메모리 location을 출력했던 이전의 출력문과 동일한 것을 볼 수 있다. 즉 파라미터로 정의된 포인터 a에 대해서 *(a + i)를 연산하면서 값을 찍는 것이 a[i]를 찍는 것과 동일하다고 보면 된다. 이것도 이해가 안간다면 직접 찍어보면 된다. 아래의 코드를 보자. 

for(i = 0; i<4; i++){
		printf("inner function a value: %d, address: %d\n", a[i], &a[i]);
		printf("inner function a value: %d, address: %d\n", *(a+i), &(*(a+i)));
	}

이 코드는 똑같을까? 똑같다. 위의 예시에서 나온 코드의 function함수의 for 문을 하단의 printf문으로 바꾸어도 아무런 변화없이 정확히 똑같이 연산된다. 그러므로 여기까지 얻은 내용은 function함수의 파라미터부의 변수가 포인터로 작동한다는 점을 알았다. 그점만 알면 된다. 그리고 당연히 local variable이니 자신만의 고유한 메모리셀 어딘가에 주소를 잡고 자리하고 있다는 점을 알면 된다. 

 

이제 슬슬 핵심에 접근한다. 그 다음 출력문을 보자. 

inner function b = : 6487440
inner function &b = : 6487440
inner function *b = : 5
inner function size of b: 20
array a value: 여기서 5 6 7 8 이 나올까요? 5
array a value: 여기서 5 6 7 8 이 나올까요? 6
array a value: 여기서 5 6 7 8 이 나올까요? 7
array a value: 여기서 5 6 7 8 이 나올까요? 8

여기서는 b에 대해 출력해주고 있다. function에서 int b[5] = {5, 6, 7, 8}; 를 선언했다. 그러니 당연히 이렇게 출력문이 나올 것이다. 그런데 지금 보면 array a value: 여기서 5 6 7 8 이 나올까요? 라는 출력문이 있다. 

이 출력문 이전에 a = b를 해주었다. 즉 a라는 포인터 변수에 b즉 배열의 이름을 준 것이다. 그러니 당연히 for문을 돌면서 a[i]즉 *(a+i)를 하면서 값을 찍으면 b의 값이 5 6 7 8 순으로 나오는 것이 분명하다. 

그럼 여기서 질문에 대한 요지가 나온다. 

void function(int a[]){ 
	int b[5];
	a = b;
}
void main(){
	int a[10];
	function(a); 
}

이 코드는 문제없이 동작한다고 했다. 당연한 말이다. a = b; 코드는 당연히 잘 동작한다. 그 이유는 바로 윗문단에서 설명했다. 그렇다면 질문 하신 분이 말씀하신 아래의 코드는 왜 동작하지 않는 것인가? 여기서 신기한 점은 위의 코드에서 function 내의 a = b; 를 b = a; 로 바꾸면 실행이 되지 않는다. 그러나 아래의 코드에서 b = a를 a = b로 바꾼다고 해서 아래 코드가 실행되는 것은 아니다. 슬슬 감이 잡힌다. 

void main(){
	int a[10];
    int b[4];
    b = a; 
}

1번 코드에서 b = a가 왜 동작하지 않을까? 2번 코드에서도 왜 b = a가 동작하지 않을까? 에러 사유도 

[Error] assignment to expression with array type 와 같은 에러가 나온다. 즉 b는 배열 첫 요소의 주소를 나타낸다. 그런데 주소에 무언가를 할당하려 한다? 프로그래밍 언어에서 가장 주요한 법칙은 lvalue는 상수일 수 없다는 말이 나온다. 즉 반드시 변수인 무언가일 경우에만 할당 가능하다는 뜻이다. 하지만 위 코드가 작동하지 않는 이유는 메모리셀의 주소를 나타내는 변수가 아닌 대상에 값을 할당하려 하기 때문이다. 또한 c11의 말을 빌리자면 다음과 같은 표현을 찾아볼 수 있다. 

in c11: 
1. assignment operator shall have a modifiable lvalue as its operand
2. A modifiable lvalue is an lvalue that does not have array type. 

 

할당 연산은 반드시 수정가능한 lvalue를 가지고 진행되야 한다고 한다. (그런데 질문에서처럼 포인터 상수에 값을 assignment 하려 한다? 당연히 에러가 발생되는 것이다. )

그리고 2번을 보면 뜻이 명확해진다. lvalue는 array type일 수 없다는 것이다. 그래서 b = a가 작동하지 않는 것이다. 

 

이제 궁금증이 풀렸다. 다만 질문하신 분께서 원하는 걸 어떻게 이뤄낼 수 있는지에 대한 방법을 소개하겠다. 

질문자가 원한 것은 main에 선언된 배열 a를 다른무언가를 통해 가리키고 싶어했다. 즉 배열 a의 첫요소의 시작 주소를 누군가에게 줌으로써 배열 a에 접근하는 다른 누군가를 만들어내고 싶던 거였다. 방법은 간단하다. 포인터를 사용하면 된다. 

아래와 같은 코드를 살펴보자. 

int *c;
	c = b;
	for(i = 0; i< 4; i++){
		printf("포인터 c를 통해 배열을 출력해보면?  %d\n", c[i]); // 5 6 7 8 이 나올까?  
	}

포인터변수 c를 만들어주고 배열의 시작주소인 b를 할당해주었다. 그리고 for loop을 돌면서 *(c + i)를 찍어주면 

배열 b의 값이 출력되는 것을 확인할 수 있다. c는 단지 시작 주소만 갖고 있을 뿐이다. 그리고 i즉 정수값을 더해주면서 메모리셀을 거슬러 올라가면서 그곳에 존재하는 값을 찍어주는게 전부다. 아래와 같은 출력문 처럼 말이다. 

포인터 c를 통해 배열을 출력해보면?  5
포인터 c를 통해 배열을 출력해보면?  6
포인터 c를 통해 배열을 출력해보면?  7
포인터 c를 통해 배열을 출력해보면?  8


결론

 

즉 결론은 lvalue와 포인터 상수라는 개념이 핵심이었다. 

배열의 이름은 그 값을 변경할 수 없는 상수라는 점을 제외하면 포인터와 같다. 

그렇기 때문에 배열의 이름은 포인터 상수 즉 constant pointer 이다. 

유의점: 

포인터 상수(constant pointer)란 포인터 변수가 가리키고 있는 주소 값을 변경할 수 없는 포인터를 의미하며,
상수 포인터(pointer to constant)란 상수를 가르키는 포인터를 의미합니다.

위의 빨간 글씨의 내용들을 알고 있다면 위에서 말한 내용들은 한번에 알아차렸을 거이다. 배열의 이름 = 포인터 상수 라는 점을 기억하고 있으면 된다.  

 

마지막 의문점:

이유를 찾던 중간에 주황색으로 표기된 내용은 사실 아직도 이해가 가지 않는다. 어째서 size가 8이 나온 것일까? 이부분은 차차 생각해보도록 하겠다. 

 

반응형