10 분 소요

Java와 C++차이점

필자는 제일 처음 C를 공부하였고, C를 통해 다수의 프로그램을 구현하였습니다.

이 후 객체지향언어인 C++를 배웠고, C에서 구현했던 프로그램들을 C++로 옮기는 작업을 거쳤습니다.

또한 C로 옮긴 프로젝트로는 객체지향에 대해서 제대로 공부할 수 없었기 때문에 객체지향언어의 기본은 상속 및 추상클래스, 디자인패턴 등을 적용할 수 있는 프로그램을 구현하였습니다.

그래서 Java를 공부할 때 같은 객체지향언어인 C++관점에서 이해하려고 노력했습니다.

객체지향적인 관점에서는 다를 게 크게 없었으나 좀 더 깊게 들어가서 C와의 차이로 인해 조금 고생을 했던 것 같습니다.

물론 C++에서 구현했던 치환연산자, 산술연산자, 첨자연산자 등 이런 것들을 Java에서 구현이 안되는 것으로 알고 있습니다.

또한 복사생성자는 구현은 할 수 있으나, 기본서를 보니 Java에서는 Cloneable인터페이스를 구현하여 clone을 이용하는 것 같아 복사생성자 대신에 clone을 구현하였습니다.

그리고 new를 써서 자꾸 객체를 힙에 할당하는데 할당해제에 대한 처리를 제가 스스로 안한다는게 뭔가 너무 어색하고 중요한 걸 빼먹은 느낌이 들었습니다.

아무튼 이런 것들은 그냥 구현을 안하면 되고, 복사생성자는 clone이랑 마찬가지고 할당해제야 가비지컬렉터가 알아서 해주니까 이런거는 크게 문제는 없었습니다.

C++에서 메소드 반환값 개수

필자가 C++에서 Java로 넘어갈 때 가장 어려웠던 점은 바로 이겁니다…

C와 C++에서는 함수에서 반환값으로 2개든 3개든 내가 원하는대로 반환할 수 있습니다.

물론 return을 사용하는 것은 아닙니다.

반환형은 void를 이용하고, 출력하고 싶은 대상은 자료형 옆에 포인터(*)를 붙여서 주소를 매개변수로 입력받으면 몇 개든 반환할 수 있습니다.

예시로 제가 구현한 C++코드를 첨부하도록 하겠습니다.

먼저 main함수에서 find메소드를 호출합니다.

c++ 예제코드

int main(int argc, char* argv[])
{
    //c++는 객체를 생성할 때 반드시 new를 할 필요가 없습니다.
    //힙에 생성하지 않고 스택영역에 내용으로 생성할 수 있습니다.
    AddressBook addressBook = AddressBook();
    //'홍길동'을 찾아서 위치배열(indexes)에 위치를 저장하고,
    //'홍길동'으로 addressBook에서 찾은 개수(count)를 반환한다.
	Long(*indexes) = 0;//포인터에 0을 대입하면 null과 같습니다.
	Long count = 0;
	addressBook.Find("홍길동", &indexes, &count);//&는 주소값을 의미함.
	/*
    찾은 홍길동으로 내용을 변경하든 지우든 처리를 합니다.
    */
    //이후에 더이상 위치배열을 사용할 일이 없으면 할당해제를 합니다.
	if (indexes != 0)
	{
		delete[] indexes;
	}
}
//Find
void AddressBook::Find(string name, int* (*indexes), int* count)
{
    //c++는 메소드 내부에서 포인터를 이용하여 메소드를 호출한 main함수의 indexes를
    //힙에 할당할 수 있습니다. 어차피 indexes는 잠깐 이용하고 이용이 끝나면 
    //main함수에서 바로 할당해제를 할 거라서 크기는 넉넉하게(?) addressBook에
    //저장된 Personal전체크기로 정합니다.
    *indexes = new int[this->length];
	//c++는 이렇게 메소드 내부에서 main함수의 count에 접근하여 값을 바꿀 수 있습니다.
	*count = 0;
    int i = 0;//반복제어변수
    int j = 0;//indexes배열의 첨자
    //addressBook에 저장된 personal의 개수만큼 반복을 합니다.
	while (i < this->length)
	{
		//이름이 서로 같으면
		if (this->personals.GetAt(i).GetName(), name)==0)
        //getAt과 GetName은 전형적인 getter함수입니다.
        //addressBook에서 i번째에 있는 personal(개인)의
        //이름을 구해서 입력받는 name과 비교합니다.
		{
            //위치배열에 해당위치를 저장합니다.
			(*indexes)[j] = i;
            //배열첨자를 증가시킵니다.
			j++;
			//똑같은 이름의 개수를 셉니다.
			(*count)++;
		}
        //반복제어변수를 증가시킵니다.
		i++;
	}
}

c++는 위 코드에서 보시다시피 포인터를 이용하여 find메소드 내부에서 자신을 호출한 main함수의 기본자료형 변수에 접근하여 그 값을 변경하거나 main함수의 있는 배열의 주소값에 접근하여 힙에 배열을 할당할 수 있습니다.

그리고 Find 메소드가 종료되어도 이것은 그대로 main함수에 저장이 됩니다.

왜냐하면 애초에 매개변수로 main함수에 있는 indexes와 count의 주소(int * (*indexes), int * count)를 입력받아서 main에 있는 indexes와 count를 변경시켰기 때문입니다.

이를 이해하기 쉽게 메모리맵으로 그려보면 다음과 같습니다.

C++ Find메소드 메모리맵

Find메소드 메모리맵

Java에서 메소드 반환값 개수

처음에는 위의 메모리맵처럼 똑같이 Java에서 구현하려고 했습니다.

근데 문제는 Java는 반환값이 최대 1개라는 것이었습니다.

그래서 반환값은 int[]배열로 정하고, 매개변수에 Integer라는 wrapper클래스(int를 클래스로 만듬)이용하면 이것이 가능할 것이라는 착각(?)을 하게 됩니다.

Integer를 매개변수로 입력하고, 메소드 내부에서 Integer의 값을 바꾸면 메소드가 종료되더라도 Integer는 힙에 할당되어 있기 때문에 저장된 값이 남아 있을거라는 심각한 착각을 하게 됩니다;;

그래서 아래와 같이 실험을 해봤습니다.

Java에서 C++ 따라하기(Integer매개변수)

public class AddressBook
{
    public int[] find(String name, Integer indexesCount)
    {
        //ArrayList 중에서 같은 이름의 배열요소의 위치를 담을 공간
        int[] indexes = new int[this.personals.size()];
        //ArrayList 의 처음부터 끝까지 반복을 돌리면서 찾으려는 이름이 있는지 확인하기
        indexesCount = 0;//같은 배열 개수
        int index = 0;//배열 첨자
        Personal personal =null;
        for(int i = 0; i < this.personals.size(); i++)
        {
            personal = this.personals.get(i);
            if(name.compareTo(personal.getName())==0)
            {
                //위치를 저장한다.
                indexes[index] = i;
                //위치배열 첨자를 증가시킨다.
                index++;
                //개수를 증가시킨다
                indexesCount++;
            }
        }
        return indexes;
    }
    //인스턴스 멤버
    private ArrayList<Personal> personals;
}
public static void main(String[] args)
{
    //AddressBook 기본생성자 호출
    AddressBook addressBook = new AddressBook();
    //AddressBook에서 이름(홍길동)으로 배열요소 찾기  
    Integer indexesCount = 0;
    int [] indexes = addressBook.find("홍길동", indexesCount);
    int index = 0;
    Personal personal = null;//찾은 personal객체를 출력하기 위한 임시 저장공간
    if(indexes != null)
    {
        while(index < count)
        {
            personal = addressBook.getAt(indexes[index]);
            System.out.printf(" 13.%d %s, %s, %s, %s\n", index+1,
                personal.getName(), personal.getAddress(),
                personal.getTelephoneNumber(), personal.getEmailAddress());
            index++;
        }
    }
}

위 코드처럼 int[] 배열은 반환값으로 반환하고 Integer(indexesCount)는 매개변수로 입력 받은 뒤에 메소드 내부에서 그 값을 증가시켜줬습니다.

main에 있는 Integer(indexesCount) 객체의 참조변수값을 find 메소드에 넘겨 줬고, 그 참조변수를 통해 find 메소드 내부에서 Integer(indexesCount)의 값을 증가시켰기 때문에 당연히 find 메소드가 종료되더라도 Integer객체에는 그 값이 반영이 되있을 거라는 착각(?)을 하고 위 코드를 실행시켜봤습니다.

그런데 실행을 시켜보니 출력이 하나도 안되었습니다;;

그래서 디버깅을 해봤습니다.

indexes 배열은 당연히 반환값을 받았으니 정상적으로 배열요소에 위치값을 저장하고 있었습니다.

문제는 Integer에 있었습니다.

Integer는 main메소드에서 처음에 0으로 초기화했는데 find메소드 내부에서 Integer객체의 값을 변경시켰음에도 불구하고, find 메소드가 종료된 후에 여전히 그 값이 0이었습니다;;

Integer를 매개변수로 해도 main에서 값이 바뀌지 않는 이유

그 이유는 나중에 알아보니 기본자료형 int의 wrapper클래스인 Integer의 경우 값을 변경하면 String과 마찬가지로 기존의 객체는 그대로 두고, 변경된 값을 가지는 새로운 객체를 할당하기 때문입니다.

Java프로젝트하면서 String의 특성으로 인한 깊은 복사 설명하기

find 메소드의 매개변수 Integer(indexesCount = 0)는 그냥 참조변수입니다.

Integer indexesCount가 find매개변수 입력될 때 이 참조변수는 C++의 포인터처럼 main에 있는 참조변수를 가리키는 것이 아닙니다.

main에 있는 참조변수와 같은 값을 가지는, 즉, 같은 객체를 가리키고 있는 find메소드의 지역변수입니다.

Integer indexesCount는 객체 자체가 아니라 그냥 힙에 할당된 Integer의 주소값을 저장하고 있는 지역참조변수일 뿐입니다.

이를 c언어의 포인터(main함수의 Integer count를 가리키고 있는 주소를 저장하고 있는 변수)라고 생각하면 절대 안됩니다

그래서 find메소드에서 Integer의 값을 변경하는 순간(ex. indexesCount =2)에 새로운 Integer객체(2)가 생성되고, 이 주소값을 find함수의 지역참조변수 값에 저장합니다.

이시점에서 당연히 main메소드에 있었던(매개변수로 입력 받았던)Integer참조변수는 여전히 그대로 똑같은 Integer객체(0)을 가리키고 있습니다.

그리고 당연히 find메소드에서는 값을 변경한 새로운 Integer객체(2)의 주소값을 반환하지 않았기 때문에 find메소드가 종료되면 아무도 이 새로운 Integer객체의 주소를 모릅니다.

(그러면 자동으로 GC에 의해 정리대상이 됩니다.)

그래서 find메소드가 끝나면 새로 생성한 객체 Integer(2)의 주소를 저장하고 있던 지역참조변수는 find함수 스택과 함께 소멸되고, main에 있던 Integer를 가리키고 있는 참조변수는 변함없이 Inteter(0)의 객체를 가리키고 있습니다.

그래서 반복구조에 아예 들어가지도 못했기 때문에(index초기값이 0이라서) 출력이 되지 않았던 것입니다;;

Java에서 Integer를 매개변수로 하는 Find메소드 메모리맵

위의 설명에 대한 이해를 돕기 위해 제가 직접 그렸던 메모리맵을 첨부합니다.

저도 직접 메모리맵을 그려보니 확실하게 이해할 수 있었습니다.
Java에서 Integer를 매개변수로 하는 Find 메모리맵

Java에서 C++ 따라하기(int[]매개변수)

이쯤하면 멈출만도 한데 여기서 한번 더 파보고 싶었습니다.

‘이건 Integer특성상 그럴거야, 차라리 배열을 매개변수로 입력하고, int를 반환값으로 바꿔보자.’라는 미친(?)생각을 하게 됩니다.

그래서 아래와 같이 시도해봤습니다.

public class AddressBook
{
    //name 으로 Personal 객체 찾기
    public int find(String name, int[] indexes)
    {
        //ArrayList 중에서 같은 이름의 배열요소의 위치를 담을 공간
        indexes = new int[this.personals.size()];
        //ArrayList 의 처음부터 끝까지 반복을 돌리면서 찾으려는 이름이 있는지 확인하기
        int index = 0;//위치배열첨자
        int count = 0;//위치배열에 저장되는 개수
        Personal personal =null;
        for(int i = 0; i < this.personals.size(); i++)
        {
            personal = this.personals.get(i);
            if(name.compareTo(personal.getName())==0)
            {
                //위치를 저장한다.
                indexes[index] = i;
                //위치배열 첨자를 증가시킨다.
                index++;
                //위치배열에 저장되는 개수를 증가시킨다.
                count++;
            }
        }
        return count;
    }
}
public static void main(String[] args)
{
    //복사본의 내용을 변경하기 위해 복사본에서 변경할 배열요소를 이름(홍길동)으로 찾기  
    int[] indexes = null;
    int count = copyAddressBook.find("홍길동", indexes);
    int index = 0;
    if(indexes != null)
    {
        while(index < count)
        {
            personal = copyAddressBook.getAt(indexes[index]);
            System.out.printf(" 13.%d %s, %s, %s, %s\n", index+1,
                personal.getName(), personal.getAddress(),
                personal.getTelephoneNumber(), personal.getEmailAddress());
            index++;
        }
    }
}

이렇게 코드를 짜고 보니 이미 main 메소드에서 find매개변수의 indexes에 이미 경고가 있길래 읽어보니 indexes는 항상 null일 것이라고 하네요;;

(이때부터 안될 것을 알고 있었지만 그냥 바보같이 계속 시도했습니다.)

그렇다고 indexes에 초기값을 안주니 초기화하라고 해서 일단 null로 초기화하고 실행시켜봅니다.

그 결과 역시 아무것도 출력되지 않고, 뒤에서 이 indexes배열을 사용하는 코드가 있었는데 바로 ‘nullPointerException’에러가 뜨며 프로그램이 종료됩니다.

Int[]를 매개변수로 해도 main에서 값이 바뀌지 않는 이유

Integer와 정확하게 같은 이유입니다.

즉, main메소드에서 int[] indexes의 참조변수 값은 null입니다.

그리고 이 참조변수값을 find메소드의 매개변수로 넣는 순간에 이제 이 int[] indexes는 find 메소드의 지역참조변수입니다.

이 지역 참조변수는 당연히 처음에는 main에서 받은 값 그대로 null입니다.

그리고 find메소드에서 힙에 배열을 할당하는 순간 그 주소를 find 메소드의 지역참조변수에 저장하게 됩니다.

이 시점에서 main의 indexes참조변수는 여전히 null입니다.

그리고 find 메소드가 종료될 때 이 새롭게 힙에 할당한 이 배열주소를 반환값으로 넘겨주지 않으면 main에 있는 indexes는 여전히 null일 것입니다.

그리고 기껏 find에서 새롭게 위치배열(indexes)를 힙에 할당했는데 이 주소를 저장한 지역참조변수는 find메소드가 끝나면 같이 소멸되기 때문에 아무도 이 배열에 접근할 수 없게 되고, CG처리대상이 되버리고 맙니다ㅠ

그래서 main의 indexes는 당연히 null이기 때문에 애초에 선택구조(indexes!=null)에 들어갈 수 조차 없었던 것입니다.

Int[]를 매개변수로 하는 Find메소드 메모리맵

이 역시도 이해를 돕기 위해 메모리맵을 첨부합니다. Java에서 Int배열을 매개변수로 하는 Find메소드

결론

첫번째, 자바에서는 main에 있는 기본자료형의 값을 메소드에서 바꿀 수 있는 유일한 방법은 메소드로부터 기본자료형 값을 반환받아 대입하는 방법뿐입니다.

두번째, 자바에서 참조변수의 값을 c++의 포인터처럼 착각하여 사용하면 안됩니다!

참조변수도 매개변수로 입력 되는 순간에 그냥 지역참조변수값이 되버립니다.

즉, 이 지역참조변수는 main메소드와 똑같은 객체를 가리키고 있는것이지 main에 있는 참조변수를 가리키고 있는 것이 아닙니다.

그래서 이 지역참조변수에서 new를 하는 순간에 이 지역참조변수는 main메소드와는 다른 새로운 객체를 가리키게 되고, 거기서 값을 변경했을 때, 당연히 새로 생성된 객체의 값만 변경되며, main메소드의 참조변수가 가리키는 객체의 값은 그대로입니다.

결국에 메소드 내에서 새로 생성된 참조변수를 main메소드의 참조변수에서 반환값으로 받아주지 않으면 메소드 종료와 함께 새로 생성된 객체의 참조변수는 사라지게 되고, 새로 생성한 객체에 접근할 수 있는 방법이 없습니다.

따라서 자바에서는 c++처럼 포인터를 사용(메소드 내부에서 main메소드의 변수를 가리켜 main메소드이 변수값을 변경시키고 객체를 생성하는 행위) 할 수 없기 때문에 메소드에서 출력값을 여러 개로 설정할 수 없고, 최대 하나의 출력값만을 설정할 수 있습니다.

유의사항

여기서 헷갈리면 안되는 것이 예를 들어, main에서 Personal객체(String name을 멤버로 가지고 있는)를 가리키는 참조변수의 값을 AddressBook객체의 void chanegeName(Personal personal, String name)메소드의 매개변수로 입력하여 changeName메소드 내부에서 Personal객체의 name을 변경하면 메소드 종료 후에 main메소드의 Personal객체의 name도 바뀐다는 점입니다!!!

왜냐하면 changeName에서 Personal객체를 새로 생성한 적이 없고, 매개변수로 입력 받은 main의 참조변수값을 그대로 이용하여 main의 참조변수와 같은 Personal 객체에 접근하여 name만 바꿨기 때문입니다.

즉, main메소드의 Personal참조변수값과 changeName의 지역참조변수값이 서로 동일한 Personal객체를 가리키고 있습니다.

changeName에서 name을 바꾸고 종료되면, changeName의 지역참조변수값은 사라지지만 이와 동일한 객체를 가리키는 주소값이 main의 참조변수값에 저장되어 있습니다.

따라서 changeName메소드의 지역참조변수값에서 Personal에 접근하여 name을 바꿨을 때 main 메소드의 참조변수값도 동일한 객체를 가리키고 있기 때문에 changeName메소드가 종료되고 나서 main에서 확인했을 때 Personal의 name은 바뀌는 것입니다!!!

이 역시 쉬운 이해를 위해 코드와 메모리맵을 첨부합니다.

changeName코드

public class AddressBook
{
    //매개변수로 입력한 Personal의 name을 매개변수로 입력한 name으로 바꾸기
    public void changeName(Personal personal, String name)
    {
        personal.setName(name);
    }
}
public static void main(String[] args)
{
    //changeName메소드를 가진 AddressBook 객체를 생성
    AddressBook addressBook = new AddressBook();
    //홍길동을 name으로 가지는 Personal객체 생성
    Personal personal = new Personal("홍길동"); 
    //personal 객체의 name 출력하기
    System.out.println(personal.getName());//"홍길동"이 콘솔에 출력됨.
    //AddressBook의 changeName메소드를 통해 Personal객체의 name변경하기
    addressBook.changeName(personal, "김길동");
    //changeName메소드 종료 후에 main의 Personal객체의 name 출력하기
    System.out.println(personal.getName());//"김길동"이 콘솔에 출려됨.
}

changeName 메모리맵

ChangeName메모리맵

출력 값이 2개인 C++코드를 Java스타일로 수정하기

그래서 코드를 수정했습니다.

첫번째 방법은 int[]배열을 활용한 비효율적인 방법입니다.

두번째 방법은 ArrayList를 활용한 효율적인 방법입니다.

반환값이 int[]인 find 메소드

//name 으로 Personal 객체 찾기
public int[] find(String name)
{
    //배열값 초기화
    int[] indexes = null;
    int count = 0;//찾은 개수 초기화
    //ArrayList 의 처음부터 끝까지 찾고자 하는 이름과
    // 비교하여 찾은 개수를 센다.
    for (Personal personal: this.personals)
    {
        if(name.compareTo(personal.getName())==0)
        {
            //찾은 개수를 증가시킨다.
            count++;
        }
    }
    //찾은게 있으면
    if(count > 0)
    {
        //배열을 할당한다.
        indexes = new int[count];
    }
    //ArrayList 의 다시 처음부터 끝까지 반복을 돌리면서
    // 같은 배열요소의 위치를 저장한다.
    int index = 0;//배열첨자
    Personal personal =null;
    for(int i = 0; i < this.personals.size(); i++)
    {
        personal = this.personals.get(i);
        if(name.compareTo(personal.getName())==0)
        {
            //위치를 저장한다.
            indexes[index] = i;
            //배열첨자를 증가시킨다.
            index++;
        }
    }
    return indexes;
}

위치배열을 만들 때 문제가 배열은 생성할 때 그 크기를 알아야만 생성할 수 있다는 점입니다.

근데 문제는 name을 비교하면서 찾아보기 전에는 배열 크기를 몇으로 해야할 지 알 수 없다는 문제가 있습니다.

그래서 배열을 생성하기 전에 먼저 addressBook에 저장된 personal의 name과 매개변수로 입력 받은 name을 비교하여 같은 이름이 있는지 확인하여 같은 이름의 개수(count)를 셉니다.

이 후 count크기 만큼의 배열을 할당하고(count = 0일 경우 null반환), 다시 ArrayList의 처음부터 마지막까지 name과 같은 이름이 있는지 비교하면서 같은 경우 그 위치를 위치배열(int[] indexes)에 저장한 뒤에 위치배열을 반환합니다.

이 방법을 이용하면 딱 맞는 크기만큼의 배열을 할당하기 때문에 메모리 낭비는 줄일 수 있으나 똑같은 행위를 2번 하기 때문에 시간 낭비가 있습니다.

반환값이 ArrayList인 find 메소드

그래서 2번째 방법은 ArrayList를 활용합니다.

ArrayList의 경우 크기를 유동적으로 변경할 수 있기 때문에 같은 이름을 찾기 위한 행위를 한 번만 수행하면 됩니다.

//name 으로 Personal 객체 찾기
public ArrayList<Integer> find(String name)
{
    //name으로 찾은 Personal 객체들의 위치를
    // 저장할 위치ArrayList 생성하기
    ArrayList<Integer> indexes = new ArrayList<>();
    //주소록의 ArrayList 의 처음부터 끝까지 반복을 돌리면서
    // name과 같은 배열요소의 위치를 저장한다.
    Personal personal =null;
    for(int i = 0; i < this.personals.size(); i++)
    {
        //해당 위치의 Personal객체를 구한다.
        personal = this.personals.get(i);
        //해당 Personal의 name이 찾고자 하는 name과 같으면
        if(name.compareTo(personal.getName())==0)
        {
            //위치를 저장한다.
            indexes.add(i);
        }
    }
    return indexes;
}

마치며

이상으로 “C++와 Java의 차이점에 대한 고찰 - 참조변수와 포인터, 메소드 반환(리턴) 개수”에 대한 글을 마칩니다.

저도 이번 주제를 통해 확실하게 포인터와 참조변수의 차이를 이해할 수 있었고, 자바와 C++의 특성에 대해서 좀 더 깊데 이해할 수 있었습니다.

혹시나 틀린 내용이나 미흡한 내용이 있을 수 있습니다.

잘못된 또는 궁굼한 사항은 언제든지 댓글 또는 이메일을 남겨주시면 답변을 드리도록 하겠습니다.

다음 글은 “Java프로젝트하면서 문자열 특화 입출력인 Reader/Writher클래스 사용하기 및 CUI 프로그래밍”에 대하 작성하도록 하겠습니다.

긴 글 읽어주셔서 감사합니다^^

댓글남기기