[C++] - 4일차(상수 &복사 생성, 대입 연산자)

Jun·2026년 2월 18일

c++

목록 보기
4/6

상수(const)와 객체의 깊은 복사 (String 클래스 구현)

오늘 수업에서는 C++에서 상수를 다루는 방법과, 동적 할당을 포함한 클래스에서 반드시 처리해야 할 '깊은 복사'에 대해 학습했습니다. 특히 직접 String 클래스를 설계해보며 메모리 관리의 중요성을 체감할 수 있었습니다.


1. 상수 (const)

변하지 않는 값을 정의할 때 사용하며, 선언과 동시에 초기화해야 합니다.

  • const 변수: 값을 변경할 수 없습니다.
    const int a = 10;
    (에러: a = 20;)

  • 상수형 메서드: void func() const { ... } 형태. 메서드 내부에서 객체의 멤버 변수를 수정하는 것을 금지합니다.


2. String 클래스 만들기 (1차: 동적 할당과 소멸자)

문자열을 저장하기 위해 char* 포인터를 사용하여 힙(Heap) 메모리를 관리하는 클래스를 설계합니다.

[코드 예시]

//String class 생성
class String { // 'String'이라는 이름의 새로운 설계도(클래스)를 만듭니다.
public:
	String() { // [기본 생성자] 아무런 글자 없이 String 객체를 만들 때 실행됩니다.
		strData = NULL; // 가리킬 곳이 없으므로 주소를 NULL(0)로 비워둡니다.
		len = 0; // 길이는 당연히 0입니다.
	}
    
	String(const char* str) { // [매개변수 생성자] "Hello" 같은 글자를 넣으며 만들 때 실행됩니다. 포인터 형태의 문자열을 받습니다. 
		len = strlen(str); // strlen 함수로 입력받은 글자 수가 몇 개인지 잽니다.
		strData = new char[len + 1]; // 글자 수 + 1(끝을 알리는 '\0' 자리)만큼 힙(Heap) 메모리를 빌려옵니다. 문자열을 저장할 공간을 동적 할당
		strcpy(strData, str); // 빌린 메모리 칸에 입력받은 글자들을 하나씩 복사해 넣습니다. 깊은 복사
		
	}

	~String() { // [소멸자] 객체가 수명을 다해 사라질 때 자동으로 호출됩니다.
		if (strData) { // 만약 빌려 쓰고 있는 메모리(strData)가 있다면?
			delete[]strData; new[]로 빌렸던 메모리를 국가(OS)에 반납합니다. (메모리 누수 방지)
		}
	}


	const char* getStrData() const { // 상수형 메서드: 내부 값을 절대 바꾸지 않겠다고 약속하는 조회 함수입니다.
		if (strData) //저장된 데이터가 있다면
			return strData; // 그 주소값을 돌려줍니다.
		return""; // 데이터가 없다면 빈 문자열의 주소를 돌려줍니다.
	}

	int getLen() const { // 상수형 메서드입니다.
		return len; // 저장된 길이 값을 돌려줍니다.
	}
private:
	char* strData; // 실제 문자열이 저장된 메모리 주소를 가리키는 화살표(포인터)입니다.
	int len; // 문자열의 길이를 숫자로 저장하는 칸입니다.
 
};


int main(void) {
	String str1; // 기본 생성자 호출 -> strData=NULL, len=0인 빈 객체 생성
	String str2("Hello"); // 매개변수 생성자 호출 -> 6칸 빌려서 "Hello\0" 저장

	cout << str1.getLen() << endl; // 0 출력
	cout << str1.getStrData() << endl; // 빈 줄 출력 (또는 아무것도 안 나옴)
	cout << str2.getLen() << endl; // 5 출력
	cout << str2.getStrData() << endl; // Hello 출력

	//런타임 에러 case
    //String str3 = str2; 객체를 복사하면 기본적으로 모든 멤버의 값이 복사된다. (포인터라면 메모리 주소도 같이 복사된다. ==얕은 복사가 이루어진다. ) 
    
    
	return 0;
}

const char*str

  • const char를 가리키는 포인터
    const char* 를 통해서 문자열을 받는다. 읽어오는 것은 가능하지만
    새로운 값을 쓰는 것은 할 수 없다. (상수)

  • 만약 생성자를 String(char* str)로 만들었다고 가정해 봅시다.
나의 의도: String str("test");

컴파일러의 생각:

1. "test"는 상수형 문자열(const char*)이네? 절대 바뀌면 안 되는 데이터야.

2. 그런데 받는 쪽(char* str)const가 없네? 이건 "나중에 이 내용을 바꿀 수도 있다"는 뜻이잖아.

3. "안전하지 않아! 상수를 보관하는데 왜 바꿀 수 있는 통에 담으려고 해? 에러!"             

3. 얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy)

얕은 복사의 문제점

String str3 = str2;와 같이 객체를 복사할 때, 별도의 처리가 없으면 포인터 주소값만 그대로 복사됩니다. (얕은 복사가 이루어집니다.)
1. Double Free Error: 두 객체가 같은 메모리 주소를 가리키고 있어, 소멸자가 호출될 때 이미 해제된 메모리를 또 해제하려다 에러가 발생합니다.
2. 데이터 공유: 한쪽에서 값을 바꾸면 의도치 않게 다른 쪽 데이터도 영향을 받습니다.

예시

String str3 = str2; //객체를 복사하면 기본적으로 모든 멤버의 값이 복사된다. 
//(포인터라면 메모리 주소도 같이 복사된다. ==얕은 복사가 이루어진다. ) 

str2: len=5, strData=0x1000 (예시 주소)
str3: len=5, strData=0x1000 (똑같은 주소!)


main 함수의 마지막 중괄호 }를 만나는 순간,
Stack에 쌓였던 객체들이 생성된 반대 순서로 사라지기 시작합니다.

1. str3 소멸 시작): 가장 마지막에 들어온 str3의 소멸자가 먼저 호출됩니다.

2. str3가 가진 주소 0x1000을 찾아가서 메모리를 반납(delete[])합니다. (성공)

3. (str2 소멸 시작): 그다음 순서인 str2의 소멸자가 호출됩니다.

4. str2도 자기가 가진 주소 0x1000을 찾아가서 반납하려고 합니다.


문제 발생: 하지만 이미 str3가 반납해버린 땅입니다! 이미 비어있는(혹은 권한이 없는) 메모리를 또 삭제하려 하니 운영체제가 "이건 불법이야!"라며 Double Free Error를 일으킵니다.

그래서 이 문제를 해결하기 위해서는
str2의 객체가 생성될 때 생성자 호출 --> 이 때 호출되는 생성자는 복사 생성자이다.

복사 생성자를 새롭게 선언하지 않으면 얕은 복사가 일어난다.
그러므로 깊은 복사가 이루어 지도록 복사 생성자를 선언해주어야 한다.


깊은 복사 (복사 생성자)

새로운 메모리 공간을 할당받고 내용을 복사하여 독립적인 상태를 유지합니다.

예시

//복사 생성자 선언 
String(const String &rob){
	len = rob.len;
    strData = new char[len + 1];
    strcpy(strData, rob.strData);
    
}

...

String str3(str2);

위쪽 예시를 포함한 코드

#include <iostream>
#include <cstring> // strlen, strcpy 사용을 위해 필요

using namespace std;

class String {
public:
    // 1. 기본 생성자
    String() {
        strData = NULL;
        len = 0;
    }

    // 2. 매개변수 생성자 (문자열 입력)
    String(const char* str) {
        len = strlen(str);
        strData = new char[len + 1];
        strcpy(strData, str);
    }

    // 3. 복사 생성자 (깊은 복사) - 핵심 수정 부분!
    String(const String &rob) {
        len = rob.len;
        if (rob.strData != NULL) {
            strData = new char[len + 1]; //(깊은 복사)
            strcpy(strData, rob.strData);
        } else {
            strData = NULL;
        }
    }

    // 4. 소멸자
    ~String() {
        if (strData) {
            delete[] strData;
        }
    }

    // 조회용 메서드
    const char* getStrData() const {
        if (strData) return strData;
        return "";
    }

    int getLen() const {
        return len;
    }

private:
    char* strData;
    int len;
};

int main(void) {
    String str1;                // 기본 생성자
    String str2("Hello");       // 매개변수 생성자
    String str3 = str2;         // 복사 생성자 호출 (이제 에러가 나지 않습니다!)

    cout << "str1: " << str1.getStrData() << " (길이: " << str1.getLen() << ")" << endl;
    cout << "str2: " << str2.getStrData() << " (길이: " << str2.getLen() << ")" << endl;
    cout << "str3: " << str3.getStrData() << " (길이: " << str3.getLen() << ")" << endl;

    return 0;
}


//출력 결과 
str1:  (길이: 0)
str2: Hello (길이: 5)
str3: Hello (길이: 5)

4. 복사 대입 연산자 (Copy Assignment Operator)

복사 생성자와는 다른 내용입니다.
이미 생성된 객체에 다른 객체를 대입할 때(str4 = str2;) 호출됩니다.

복사 생성자 : String str3=str2;(태어나면서 바로 복사)

복사 대입 연산자 : str3=str2;(이미 만들어진 str3에 str2를 대입)
  • 주의사항: 기존에 가지고 있던 메모리를 먼저 delete 해야 메모리 누수(Memory Leak)가 발생하지 않습니다.
String 클래스처럼 동적 할당을 쓰는 경우, 단순히 `=` 만 쓰면 다시 얕은 복사 문제가 
발생. 

이미 str3가 빌려놓은 땅에 str2의 주소를 그냥 덮어씌우면 str3가 쓰던 땅은 미아(메모리 누수)
  • 자기 대입 방지: if (this != &rob)를 통해 자기 자신을 대입하는 낭비를 막습니다.

예시

String& operator=(const String &rob) {
    if (this != &rob) { // 자기 대입 체크
        delete[] strData; // 기존 메모리 해제 (중요!)
        len = rob.len;
        strData = new char[len + 1];
        strcpy(strData, rob.strData);
    }
    return *this;
}

...

String str3 = str2;

위의 예시들을 포함한 예시

#include <iostream>
#include <cstring> // strlen, strcpy 사용을 위해 필요

using namespace std;

class String {
public:
    // 1. 기본 생성자
    String() {
        strData = NULL;
        len = 0;
    }

    // 2. 매개변수 생성자 (문자열 입력)
    String(const char* str) {
        len = strlen(str);
        strData = new char[len + 1];
        strcpy(strData, str);
    }

    // 3. 복사 생성자 (깊은 복사) 
    String(const String &rob) {
        len = rob.len;
        if (rob.strData != NULL) {
            strData = new char[len + 1];
            strcpy(strData, rob.strData);
        } else {
            strData = NULL;
        }
    }

    // 4. 소멸자
    ~String() {
        if (strData) {
            delete[] strData;
        }
    }
    
    //복사 대입 연산자   -> 메모리 누수 현상 일어남
    String &oper=(const String &rob){
		if (this != &rob){
    		len= rob.len;
       		delete[]strData;
        	strData = new char[len+1];
        	strcpy(strData, rob.strData);
    	}
    	return *this;
	}
    

    // 조회용 메서드
    const char* getStrData() const {
        if (strData) return strData;
        return "";
    }

    int getLen() const {
        return len;
    }

private:
    char* strData;
    int len;
};

int main(void) {
    String str1;                // 기본 생성자
    String str2("Hello");       // 매개변수 생성자
    
    String str3 = str2;         // 복사 생성자 호출 (이제 에러가 나지 않습니다!)
	
    String str4("C++ Pro");
    str4 = str2;
    
    cout << "str1: " << str1.getStrData() << " (길이: " << str1.getLen() << ")" << endl;
    cout << "str2: " << str2.getStrData() << " (길이: " << str2.getLen() << ")" << endl;
    cout << "str3: " << str3.getStrData() << " (길이: " << str3.getLen() << ")" << endl;
	cout << "str4: " << str4.getStrData() << " (길이: " << str4.getLen() << ")" << endl;
    cout << "str2: " << str2.getStrData() << " (길이: " << str2.getLen() << ")" << endl;
    
    return 0;
}

//출력 결과

str1:  (길이: 0)
str2: Hello (길이: 5)
str3: Hello (길이: 5)
str4: Hello (길이: 5)
str2: Hello (길이: 5)

	str1: 기본 생성자로 만들어져 내용이 비어 있고 길이는 0입니다.

	str2: "Hello"라는 문자열을 가지고 있으며 길이는 5입니다.

	str3: str2를 복사 생성자로 복사했으므로 똑같이 "Hello"5가 출력됩니다.

	str4: 처음에는 "C++ Pro"였지만, str4 = str2;를 거치면서 
    기존 메모리를 delete 하고 str2의 값을 가져왔습니다. 
    그래서 **Hello**5가 출력됩니다.

	마지막 str2: str4에 값을 대입해줬다고 해서 원본인 str2가 변하지는 않습니다. 
    그대로 **Hello**5가 나옵니다.

profile
Hard Trying

0개의 댓글