이것이 C++이다 - [3장] 클래스

2025. 10. 3. 02:06·교재/이것이 C++이다

1. 객체지향 프로그래밍 개요

그냥 그렇군 하고 넘길 수 있는 개요였지만 객체지향 프로그래밍, 협업에서 중요한 내용을 담고있어서 적어보았다

 

1. 반드시 내 클래스를 가져다 사용하는 사람들을 배려해야한다

2. 사용자의 편의성을 극대화하고, 사용자의 실수 가능성을 제작자가 차단해야 한다

3. 갑·을 관계가 되어서는 안된다

 

교재에 실린 문제되는 코드를 보면 3가지 사실을 알 수 있다

#include <iostream>

typedef struct USERDATA {
	int nAge;
	char szName[32];
}USERDATA;

int main(void) {
	USERDATA user = { 20, "철수" };
	printf("%d, %s\n", user.nAge, user.szName);

	return 0;
}

 

1. 사용자는 제작자가 만든 자료구조(구조체)의 멤버, 구성을 알고 있어야한다

2. 자료구조에 담긴 정보를 출력하고 싶다면 사용자 스스로 직접 멤버에 접근해야 하며 적절한 출력 방법도 선택해야 한다

3. 만일 제작자가 자료구조를 변경한다면 사용자는 제작자의 코드와 관련된 자신의 코드를 몽땅 수정해야 한다.

누굴 골탕 먹일 생각이 아니라면 이렇게 짜지 말라고 하신다...

그렇다면 올바른 코드는 무엇일까?

#include <iostream>

typedef struct USERDATA {
	int nAge;
	char szName[32];
}USERDATA;

void PrintData(USERDATA* pUser) {
	printf("%d, %s\n", pUser->nAge, pUser->szName);
}

int main(void) {
	USERDATA user = { 20,"철수" };
	PrintData(&user);

	return 0;
}

제작자가 구조체를 변경하더라도 사용자가 PrintData(&user)를 변경할 필요는 없다.

함수를 제공받아 사용하기만 하고 그 구조까지 알 필요가 없는 것이다

이와 같은 함수를 '인터페이스 함수'라고 부른다

 

2. 클래스 기본 문법

클래스를 쉽게 생각하면 함수를 포함할 수 있는 구조체이다.

클래스를 선언하는 문법은 다음과 같다.

class 클래스명 {
public(접근제어지시자, 때에 따라 다름):
	멤버변수선언;
	멤버함수선언및정의;
};
#include <iostream>

class USERDATA {
public:
	int nAge;
	char szName[32];
};

void PrintData(USERDATA* pUser) {
	printf("%d, %s\n", pUser->nAge, pUser->szName);
}

int main(void) {
	USERDATA user = { 20,"철수" };
	PrintData(&user);

	return 0;
}

다음 코드에서 유의해야할 점은 nAge와 szName이 printData 함수 내부에 선언된 지역 변수가 아니라 

Print() 함수가 속한 '클래스'의 멤버 변수라는 사실이다.

 

위 예제에서 C언어 다운 부분은 USERDATA user = { 20, "철수" } 부분이다.

사용자가 멤버 변수 초기화라는 일을 직접 해야한다는 근본적인 문제를 가지고 있기 때문이라 한다...

왜 문제지? 싶어 다시 AI에게 질문했다...

 


1. C 언어에서의 구조체 초기화

C 언어에서는 구조체 struct를 만들고 초기화할 때, 멤버 변수를 순서대로 나열된 값으로 초기화합니다.

struct USERDATA {
    int nAge;
    char szName[32];
};

struct USERDATA user = { 20, "철수" };

👉 여기서는 “첫 번째 멤버 → 20, 두 번째 멤버 → '철수'” 이런 식으로 순서에 의존해서 초기화가 일어나요.
즉, 개발자가 구조체 멤버를 다 알고 직접 순서를 지켜 넣어줘야 합니다.


2. C++에서 바라보는 문제점

C++에서는 구조체나 클래스는 단순 데이터 묶음이 아니라 객체로 취급합니다.
그런데 C 스타일 초기화는 다음과 같은 문제를 만들어요:

  1. 캡슐화 위반
    객체는 스스로 초기화 방법을 정해야 하는데, 지금은 사용자가 멤버 변수에 직접 접근해서 값을 넣어야 함.
    (예: user.nAge = 20; strcpy(user.szName, "철수");)
    → 사용자가 내부 구현에 의존하게 되어, 나중에 멤버 구성이 바뀌면 코드 전부 고쳐야 해요.
  2. 순서 의존성
    USERDATA { int nAge; char szName[32]; } 에서 순서를 바꿔서이렇게 되면, {20, "철수"} 초기화가 갑자기 잘못된 의미가 되어버립니다.
    → 유지보수성이 떨어짐.
  3. 객체 지향과 어긋남
    C++은 **생성자(constructor)**라는 기능을 통해, “객체가 어떻게 태어나야 하는지”를 클래스 안에 정의하도록 권장합니다.
    하지만 지금 예제는 생성자가 없으니까 객체 자신이 스스로 초기화 규칙을 정하지 못하고, 사용자가 일일이 멤버를 맞춰야 하는 상황 = C 스타일이라는 거예요.
class USERDATA {
    char szName[32];
    int nAge;
};

 


3. C++다운 방식

C++에서는 보통 이렇게 합니다:

#include <iostream>
#include <cstring>
using namespace std;

class USERDATA {
public:
    int nAge;
    char szName[32];

    // 생성자
    USERDATA(int age, const char* name) {
        nAge = age;
        strcpy(szName, name);
    }
};

void PrintData(USERDATA* pUser) {
    printf("%d, %s\n", pUser->nAge, pUser->szName);
}

int main() {
    USERDATA user(20, "철수");  // 생성자를 통한 초기화
    PrintData(&user);
}

👉 이제는 USERDATA 객체가 스스로 초기화 방법을 알고 있으므로, 사용자는 멤버를 직접 건드릴 필요가 없어요.
이게 C++이 강조하는 객체 지향적 초기화 방식이에요.


4. 정리

  • C 스타일 초기화: {} 안에 순서대로 값을 넣음 → 멤버 순서에 의존 + 캡슐화 위반.
  • C++ 스타일 초기화: 생성자를 정의 → 객체가 스스로 초기화 규칙을 가짐.
  • 그래서 “USERDATA user = {20, "철수"}는 C다운 부분”이라는 말은,
    객체지향적이지 않고 사용자가 직접 멤버를 관리해야 한다는 점에서 나온 비판이에요.

일단 다 안 배우기도 했고 캡슐화, 객체지향의 감을 못 잡은지 오래되어서 좀 더 봐야겠다

대충 C++이 더 알잘딱하다는 얘기를 하고계신 것 같다...

 

클래스 멤버 변수는 생성자를 이용해 초기화한다는 점에서 큰 차이가 있다

생성자를 이용하는 함수의 중요한 부분은

반환 자료형이 없고, 내부에서 자동으로 호출되는 함수라는 것이다.

 

클래스의 멤버 변수 선언은 구조체와 동일하지만, 멤버 함수를 선언-정의할 수 있다는 점이 구조체와의 차이점이다

클래스에서는 멤버 함수의 선언과 정의를 분리할 수도 있는데

멤버 함수의 선언만 안쪽에 하고, 바깥쪽에 반환형 클래스명::함수명(매개변수 자료형){ 정의 } 하면 된다

아래는 예시이다.

 

#include <iostream>
using namespace std;

class CTest {
	public:
	CTest() { //생성자
		m_nData = 10;
	}

	int m_nData; //멤버변수
	void PrintData(void);
};

void CTest::PrintData(void) {
	cout << m_nData << endl;
}

int main(void) {
	CTest t;
	t.PrintData();

	return 0;
}

그리고 생성자 함수의 멤버 변수를 초기화 할 때에는 '생성자 초기화 목록'을 사용해도 된다

#include <iostream>
using namespace std;

class CTest {
public:
    CTest()  //생성자
        : m_nData1(10), m_nData2(20)
    { }

    int m_nData1; //멤버변수
    int m_nData2;

    void PrintData(void);
};

void CTest::PrintData(void) {
    cout << m_nData1 << endl;
    cout << m_nData2 << endl;
}

int main(void) {
    CTest t;
    t.PrintData();

    return 0;
}

생성자 초기화 목록을 사용하려면 함수 원형과 몸체를 이룰 블록 {} 이 사이에 위치한다

그리고 원형 다음에 :(콜론)을 기술하고, 초기화하려는 멤버 변수 뒤에는 세미콜론을 붙이지 않는다. .. . . .(내가실수함)

 

생성자에 일일이 값을 쓰기 귀찮다면 선언과 동시에 멤버 변수를 초기화하면 된다

int m_nData1 = 10; 같은 방식이고, 생성자 초기화 목록에는 그냥 아무것도 없이 함수 원형과 몸체만 두면 된다

 

접근 제어 지시자는 앞서 보았던 public 같은 단어를 칭하는 용어이다.

사용자가 이용할 수 있는 코드를 제한하는 데 쓰인다

근데 왜 사용자가 이용할 수 있는 코드를 제한해야할까?

class BankAccount {
public:
    int balance; // 누구나 접근 가능
};

int main() {
    BankAccount acc;
    acc.balance = -1000000; // 마음대로 음수 잔액 가능!
}

 

예를 들어 은행 게임을 만들었다고 하자, 근데 만약 public:으로 설정해서 모두가 접근 가능하게 만들어놓았다면

코드를 슥슥 건들이면서 유저가 짜잔하고 부자가 될 것이다...

같은 느낌으로 막는 것 같다...

지시자 설명
public 멤버에 관한 모든 외부 접근이 허용됩니다
protected 멤버에 관한 모든 외부 접근이 차단됩니다
단, 상속 관계에 있는 파생 클래스에서의 접근은 허용됩니다
private 외부 접근뿐만 아니라 파생 클래스로부터의 접근까지 모두 차단됩니다. 
기본적으로 아무것도 적지 않으면 private로 선언됩니다.

접근 제어 지시자를 사용하는 것은 사용자가 마구잡이로 조정해 프로그램이 망가지지 않도록 하는 것이다.

마치 스피커를 리모컨(볼륨바)를 이용하지 않은 채로 마구 조정하지 못하게 해 스피커가 망가지지 않도록 막는 것이다

 

3. 생성자와 소멸자

생성자와 소멸자는 클래스 객체가 생성 및 소멸될 때 자동으로 호출 되는 함수이다.

반환 형식이 없고 함수 이름이 클래스의 이름과 동일하다.

 

매개변수가 하나도 없는 생성자는 디폴트 생성자라고 하며,

클래스를 만들 때 생성자/소멸자를 기술하지 않아도 컴파일러가 알아서 만들어 넣는다

우리가 만들지 않아도 컴파일러가 생성하기 때문에 생성자/소멸자가 없는 클래스는 없다

#include <iostream>
using namespace std;

class CTest {
public:
	CTest() {
		cout << "CTest::Ctest()" << endl;
	}

	~CTest() {
		cout << "~CTest::CTest" << endl;
	}
};

int main(void) {
	cout << "Begin" << endl;
	CTest a;
	cout << "End" << endl;

	return 0;
}

a는 main 함수에 속한 지역변수로, 지역 변수의 블록 범위가 끝나면 자동으로 소멸한다.

그래서 main함수의 실행이 끝나면 a도 소멸한다

헷갈렸던 부분은 main함수의 실행이 끝나고 난 뒤에 a가 소멸하는 건지, 아니면 그 전에 소멸을 하는건지 였는데

정답은 둘 다 아니였다.

~CTest::CTest()는 main 함수가 끝나고 프로그램이 종료되기 직전이 아니라,
main 블록에서 지역 변수 a가 소멸되는 시점 (즉, } 도달 시점)에 호출된다.

“main 함수 종료와 동시에(=종료 과정에서) 소멸자가 실행된다”가 정확한 표현이다.

 

우리가 C언어에서 배웠던 말 중에는 C프로그래밍에서 가장 우선으로 실행되는 것은 main()이라는 것이다.

하지만 전역 변수로 선언한 클래스의 생성자는 main()보다 우선시 되어 실행된다

 

특정을 정리하면 다음과 같다.

 

-main()함수가 호출되기 전에 생성자가 호출될 수 있다

-생성자는 다중 정의할 수 있다.

-소멸자는 다중 정의할 수 없다

-main() 함수가 끝난 후에 소멸자가 호출될 수 있다.

-생성자와 소멸자는 생략할 수 있으나 생략할 경우 컴파일러가 만들어 넣는다.

 

소멸자와 생성자는 new / delete 연산을 통해 생성/소멸할 수 있다.

#include <iostream>
using namespace std;

class CTest {
public:
	CTest() {
		cout << "CTest::Ctest()" << endl;
	}

	~CTest() {
		cout << "~CTest::CTest" << endl;
	}
};

int main(void) {
	cout << "Begin" << endl;

	CTest* pData = new CTest;
	cout << "Test" << endl;

	delete pData;
	cout << "End" << endl;

	return 0;
}

new와 delete연산자는 생성자와 소멸자를 호출하기 때문에 객체가 생성/소멸되는 위치를 명확히 알 수 있다.

이 코드와 다르게 (여기는 동적으로 생성된 객체) 지역변수로(대충 정적으로 만들었다는 뜻) 선언했다면

End이후에 ~CTest::CTest가 나왔을 것이다.

 

# 정확히는 "만일 동적으로 생성하지 않고 지역 변수로 선언했다면 End가 출력된 후에 소멸자가 호출됐을 것입니다" 라고 써있는데

CTest *pData는 일단 main 함수 안에 있으니까 지역변수라 생각했는데 위 말을 보면 지역변수가 아니라고 말하는 것 같아서

좀 찾아보았다

정확히는 힙을 가리키는 포인터 pData는 지역변수가 맞지만 pData가 가리키고 있는 객체는 힙에 있기 때문에 지역 변수가 아니라는 것이다.

 

객체를 동적으로 생성할 때 주의할 점은 배열로 생성한 객체들은 반드시 배열로 삭제해야 한다

만약 배열로 생성한 객체를 그냥 삭제하면 첫 번째 요소 하나만 소멸한다.

 

3. 2 참조 형식 멤버 초기화

클래스의 멤버 변수는 참조 형식으로 선언할 수 있다.

다음 코드를 분석해보자

#include <iostream>
using namespace std;

class CRefTest {
public:
	CRefTest(int& rParam) : m_nData(rParam) {};
	int GetData(void) { return m_nData; }

private:
	int& m_nData;
};

int main(void) {
	int a = 10;
	CRefTest t(a);

	cout << t.GetData() << endl;

	a = 20;
	cout << t.GetData() << endl;

	return 0;
}

a(10) ──(복사)──> rParam(10) <── m_nData

 

중요한 부분

1. CRefTest(int& rParam) : m_nData(rParam) {};

2. int& m_nData;

3. CRefTest t(a);

 

CRefTest(int& rParam) : m_nData(rParam) {}은 참조형 매개변수rParam를 받아서 참조형 멤버 변수 m_nData와 연결한다

2. m_nData를 참조형 멤버변수로 선언해 a값을 변경할 수 있게 한다

3. t에 a값을 넣어서 //m_nData로 보낸 다음에 그걸 rParam과 연결한다

 

(1)

  • 매개변수 rParam(참조형)을 받아서 **멤버 변수 m_nData**를 rParam과 연결해
  • 이 과정에서 m_nData는 결국 a를 가리키게 됨. (왜냐하면 t(a)에서 a를 넣었으니까)

#int rParam이였다면?

 

“int& rParam”과 “int rParam” 차이


1. 참조 매개변수(int& rParam)

 
CRefTest(int& rParam) : m_nData(rParam) {}
  • rParam은 원본 변수(a) 자체를 참조해요.
  • 그래서 m_nData도 결국 원본 a와 직접 연결됩니다.
 
a(10) <── rParam <── m_nData

👉 a 값이 바뀌면 m_nData에서도 바뀐 값이 그대로 보임.
즉, 같은 메모리 공유.


2. 값 매개변수(int rParam)

 
CRefTest(int rParam) : m_nData(rParam) {}
  • rParam은 a의 값을 복사해서 가져옴.
  • m_nData는 그 rParam(지역 변수)을 참조하게 됨.
 
a(10) ──(복사)──> rParam(10) <── m_nData

👉 이 경우 m_nData는 rParam을 가리키는데,
문제는 rParam은 생성자 실행이 끝나는 순간 사라져버림.
그럼 m_nData는 죽은 변수를 참조하는 꼴이 됨 → Dangling reference(댕글링 참조) 발생 ⚠️


🔑 핵심 차이

  • int& rParam → 원본 a와 평생 연결 → 안전하고 유용.
  • int rParam → 잠깐 복사본을 쓰다가 없어져버려서 m_nData가 위험해짐.

그래서 참조형 멤버 변수를 초기화할 때는 반드시 참조형 매개변수(int&)를 사용해야 안전

 

3. 3 생성자 다중 정의

생성자도 다중 정의를 할 수 있다.

생성자가 다중 정의되면 사용자는 편하지만, 제작자는 같은 코드를 여러 번 써야하는 귀찮음이 있다

보통은 귀찮음을 이겨내야했었겠지만 C++11 표준부터는 생성자 위임이 지원되어 그러지 않아도 된다

#include <iostream>
using namespace std;

class CMyData {
public:
	CMyData(int nParam) : m_nData(nParam) {};
	CMyData(int x, int y) : m_nData(x + y) {};
	int GetData(void) { return m_nData; }

private:
	int m_nData;
};

int main(void) {
	CMyData a(10);
	CMyData b(3, 4);

	cout << a.GetData() << endl;
	cout << b.GetData() << endl;

	return 0;
}

#매개 변수가 한 개 뿐인 생성자를 '변환 생성자'라고 부른다

 

클래스도 선언과 정의를 분리하여 쓸 수 있는데, defalut 예약어를 사용하면 별도로 정의하지 않아도

선언과 정의를 한 번에 끝낼 수 있다.

내부에 작성해야할 코드가 없고, 컴파일러의 기본 동작이면 된다는 의도를 표현하는 예약어이다.

 

#include <iostream>
using namespace std;

class CTest {
public:
	CTest(void) = default;
	int m_nData = 5;
};

int main(void) {
	CTest a;
	cout << a.m_nData << endl;

	return 0;
}

다만 생상자 다중 정의를 통해 새로운 생성자를 기술하고 디폴트 생성자를 기술하지 않으면 디폴트 생성자는 사라진다.

그럴 때는 

CTest(void) = delete;

이와 같이 디폴트 생성자를 삭제해놓고 CTest a; 같은 코드를 실행하면 삭제된 함수를 참조하려고 한다는 오류메세지가 뜬다

(default, delete 예약어가 다른 형태로 사용되는 경우가 존재하기도 함)

 

4. 메서드

클래스의 멤버 함수를 메서드라고 한다.

"자동차는 '방향'이라는 멤버 변수를 가졌는데, 이 값을 '핸들' 이라는 메서드로 제어할 수 있다"

자동차 붕붕이;
붕붕이.핸들(왼쪽, 10도);

멤버 함수는 '인터페이스 함수'라고도 한다.

static이면 '정적 메서드'라고 하고, const면 '상수화된' 메서드라고 한다. (메서드, 멤버 함수는 상황에 따라 인터페이스 함수라고 함)

종류 일반 상수화 정적 가상
관련 예약어 - const static virtual
this 포인터 접근 가능 가능 불가능 가능
일반 멤버 읽기 가능  가능 가능(제한적) 가능
일반 멤버 쓰기 가능 불가능 가능(제한적) 가능
적정 멤버 읽기 가능 가능 가능 가능
정적 멤버 쓰기 가능 붑ㄹ가능 가능 가능
특징 가장 보편적인 메서드 멤버 쓰기 방지가 목적 C의 전역함수와 유사 상속 관계에서 의미가 큼

 

3. 4. 1 this 포인터

this 포인터는 작성 중인 클래스의 실제 인스턴스에 대한 주소를 가리키는 포인터입니다.

라고 하면 하나도 못 알아듣는다(나는 그럼)

좀 더 쉬운 예시를 들자면, 만들고 있는 아이폰의 모델번호가 있다 치고 작업하는 것과 비슷하다

int main(void) {
	USERDATA user = { 20,"철수" };
	PrintData(&user);

	return 0;
}

C에서는 객체가 선언된 후 인스턴스의 주소를 넘겨주도록 되어있다고 한다.

반면 C++에서는 그 주소가 전달되는 것이 눈에 보이지 않을 뿐 실제로는 존재하고 있다.

...

라고 하면 이해가 안가니

좀 더 직관적인 비유는 없을까 했다

 


교재 설명을 풀어서 해석하면

  1. 현재 설계 중인 제품 → 클래스 정의 중 (아직 객체가 메모리에 없음)
    • class CMyData { ... }; 이 부분이 "설계도" 단계입니다.
    • 객체가 아직 존재하지 않으니 실제 메모리 주소(=시리얼 번호)는 없습니다.
  2. 시리얼 번호는 미래에 결정된다 → 객체가 생성될 때 주소가 정해진다
    • CMyData a(5); 처럼 객체를 만들면 그때 메모리에 올라가고, 고유한 주소가 생깁니다.
  3. 제작자는 아직 결정되지 않은 시리얼 번호를 이용해야 하는 프로그램 코드를 작성한다
    • 클래스 안에서 멤버 함수 코드를 작성할 때, 아직 객체의 주소는 알 수 없어요.
    • 하지만 함수 안에서는 "어떤 객체의 멤버인지"를 다뤄야 하죠.
    • 예:
  4. this 포인터
    • 이때 컴파일러가 "멤버 함수 안에서 쓰는 모든 멤버 접근은 사실상 this->m_nData다"라고 자동으로 바꿉니다.
    • 즉, 미래에 객체가 만들어지면 그 객체의 주소가 this에 들어가고, 그걸 통해 해당 객체의 멤버에 접근할 수 있게 됩니다.

정리된 이해

  • this 포인터는 "아직 생성되지 않은 객체의 주소를 미리 약속해두는 자리표" 같은 거예요.
  • 클래스 설계 시점에서는 주소(시리얼 번호)가 정해지지 않았지만,
    멤버 함수 안에서 멤버 변수들을 다룰 때는 마치 자기 주소를 이미 알고 있는 것처럼 코드가 작성됩니다.
  • 객체가 생성되면 그 주소가 실제 this에 채워지고, 함수 실행 시 이용됩니다.

더 쉬운 비유

  • 설계도에 "이 제품에는 고유번호가 붙고, 앱 실행 시 그 번호를 사용한다"라고 써놓은 상태.
  • 실제 공장에서 제품이 나오면 고유번호가 박히고, 앱 실행 시 그 번호(this)가 자동으로 사용됨.
👉 그래서 교재 문장을 그대로 믿기보다는 이렇게 이해하시면 돼요:
"this는 객체가 만들어진 뒤 채워질 주소를 미리 가리키는 암묵적 매개변수이다."

그래도 뭔가 잘 이해가 됐군 그런 느낌은 없어서

다른 비유를 하나 더 부탁했다

 

비유: 택배와 주소

  • 클래스 = 택배 물건의 "설계도" (아직 물건 없음)
  • 객체 = 실제 공장에서 찍혀 나온 물건 (택배 박스)
  • this 포인터 = 택배 박스에 붙은 주소 스티커

상황 1. 설계 단계

  • 물건(객체)은 아직 안 만들어졌지만, 박스에는 **"주소 스티커가 붙을 자리"**가 반드시 있다고 설계도에 적혀 있습니다.
  • 이 "자리"가 바로 this.
  • 하지만 설계할 때는 아직 그 주소(스티커 내용)를 알 수 없어요.

상황 2. 물건이 만들어질 때

  • CMyData a(5); → 박스가 하나 만들어집니다.
  • 이 박스에는 **실제 주소 스티커(예: 서울시 강남구 101호)**가 붙어요.
  • 이게 곧 this = &a.

상황 3. 멤버 함수 실행

  • 택배 기사(멤버 함수)가 물건을 배송하려고 박스를 보는데, "주소가 어디냐?"라고 확인합니다.
  • 주소 스티커(this)에 적힌 곳으로 가서 작업을 합니다.
  • 그래서 this->m_nData라는 표현이 "이 주소의 박스 안에 든 m_nData"를 의미합니다.
👉 한마디로 정리하면:
**"this는 객체라는 택배 박스에 자동으로 붙는 주소 스티커"**라고 생각하시면 돼요.

  • 객체가 만들어지면 스티커(this)가 채워지고,
  • 함수는 항상 그 스티커를 보고 "아, 이 박스 안에 있는 멤버구나" 하고 동작합니다.

 

다음 예제를 확인하자.

#include <iostream>
using namespace std;

class CMyData {
public:
	CMyData(int nParam) : m_nData(nParam) {};
	void PrintData() {
		cout << m_nData << endl;

		cout << CMyData::m_nData << endl;
		cout << this->m_nData << endl;
		cout << this->CMyData::m_nData << endl;
	}
private:
	int m_nData;
};

int main(void) {
	CMyData a(5), b(10);
	a.PrintData();
	b.PrintData();

	return 0;
}

여기서 a.PrintData();와 b.PrintData()에서 &a와 &b는 보이지 않지만 실제로는 전달된다.

그리고 실제로는 코드가 다음과 같이 작동된다. (형광펜 부분은 문법에는 어긋날 수도 있지만... 실제 작동하는 방법이다)

 

#include <iostream>
using namespace std;

class CMyData {
public:
CMyData(int nParam) : m_nData(nParam) {};
void PrintData(CMyData *pData) {

CMyData *this = pData;

cout << m_nData << endl;

cout << CMyData::m_nData << endl;
cout << this->m_nData << endl;
cout << this->CMyData::m_nData << endl;
}
private:
int m_nData;
};

int main(void) {
CMyData a(5), b(10);
a.PrintData(&a);
b.PrintData(&b);

return 0;
}

 

어려워할 것 없다. . . 뭐 대충 택배 주소칸을 미리 만들어놓는 그런 느낌같은 거다

3. 4. 3 상수형 메서드

읽기 접근만 가능(쓰기는 불가능)한 것, 함수 원형 뒤에 const만 붙이면 된다

 

상수형 메서드는 절대로 멤버 변수의 값을 쓸 수 없고, 상수형 메서드가 아닌 멤버는 호출할 수 없다.

1년뒤의 내가 코드를 본다 생각하고 const를 사용하자...

가는 건 한 순간... const를 쓰자!

 

3. 4. 5 상수형 메서드의 예외 사항

상수형 메서드는 난간 같은 것인데, 때에 따라서는 이 난간을 제거해야할 수도 있다.

그런 순간을 위해 만든 것이 바로 mutable예약어와 C++ 전용 형변환 연산자인 const_cast<> 이다.

 

mutable을 사용한 경우

#include <iostream>
using namespace std;

class CTest {
public:
	CTest(int nParam) : m_nData(nParam) {};
	~CTest() {}

	int GetData() const {
		m_nData = 20;
		return m_nData;
	}

	int setData(int nParam) { m_nData = nParam; }

private:
	mutable int m_nData = 0;
};

int main(void) {
	CTest a(10);
	cout << a.GetData() << endl;

	return 0;
}

const_cast<>를 사용한 경우

#include <iostream>
using namespace std;

void TestFunc(const int& nParam) {
	int& nNewParam = const_cast<int&>(nParam);
	nNewParam = 20;
}

int main(void) {
	int nData = 10;

	TestFunc(nData);

	cout << nData << endl;

	return 0;
}
구분 const_cast  mutable
쓰임새 타입 변환(캐스팅) 클래스 멤버 변수 한정
효과 const 속성을 강제로 제거/추가 const 멤버 함수에서도 해당 변수는 수정 허용
위험성 잘못 쓰면 UB 발생 (진짜 상수 수정 시) 안전, 설계 의도대로 쓰면 문제 없음
적용 범위 변수/포인터/참조 등 모든 const 클래스 멤버 변수에만 적용

 

둘 다 어쩔 수 없는 경우에만 쓰는 거지 남발하면 절대 좋지 않다...신중을 가하도록 하자

 

3. 4. 6 멤버 함수 다중 정의

서로 다른 자료형을 사용해 같은 멤버 함수에 접근할 때

예를 들어 TestFunc은 int nParam만 매개변수로 삼는데, TestFunc(5.5)를 실행하면 실수 5.5가 5로 변경된다

메모리 크기가 일치하지 않으니 잘리는 것이다... 이러한 문제를 해결하기 위해 C++에서 다중정의를 사용할 수 있다

 

1번 방법 (아예 0으로 만듦)

#include <iostream>
using namespace std;

class CMyData {
public:
	CMyData() : m_nData(0) {};

	int GetData(void) { return m_nData; }
	void SetData(int nParam) { m_nData = nParam; }

	void SetData(double dParam) { m_nData = 0; }

private:
	int m_nData;
};

int main(void) {
	CMyData a;

	a.SetData(10);
	cout << a.GetData() << endl;

	CMyData b;

	b.SetData(5.5);
	cout << b.GetData() << endl;

	return 0;
}

2번 방법 (delete로 삭제해서 컴파일 오류 발생)

#include <iostream>
using namespace std;

class CMyData {
public:
	CMyData() : m_nData(0) {};

	int GetData(void) { return m_nData; }
	void SetData(int nParam) { m_nData = nParam; }

	void SetData(double dParam) = delete;

private:
	int m_nData;
};

int main(void) {
	CMyData a;

	a.SetData(10);
	cout << a.GetData() << endl;

	CMyData b;

	b.SetData(5.5); // 삭제된 함수를 참조하려고 합니다 오류
	cout << b.GetData() << endl;

	return 0;
}

오류 가능성을 원천 봉쇄하는 방법을 잘 알아두도록 하자!

 

5. 정적 멤버

특정 클래스의 메서드를 호출하고 싶다면 인스턴스를 선언해서 멤버 접근 연산자를 통해 호출해야한다.

가끔은 전혀 그럴 필요없는 걸 만들어야하는데 그 때는 보통 전역변수로 만들곤 한다.

하지만 객체지향 프로그래밍에서 소속 객체가 없이 스스로 존재하는 전역 변수는 별로 좋지 않다....

정적 멤버는 전역 변수와 다름 없으면서 소속 객체이므로 유용하게 사용할 수 있다

 

정적 멤버를 선언하려면 앞에 static 예약어를 작성하면 된다.

정적 멤버 함수는 인스턴스를 선언하지 않고도 호출할 수 있지만 this 포인터를 사용할 수 없고,

정적 멤버는 선언과 정의를 반드시 분리해야한다

 

 

  • 전역변수는 객체지향 원칙(캡슐화/재사용성)을 깨뜨려서 좋지 않다.
  • 정적 멤버 함수는 객체와 무관하게 호출되므로 this가 존재하지 않는다.
  • 정적 변수의 선언과 정의 분리 → 클래스 안에서는 선언만, 클래스 밖에서 실제 메모리/구현을 제공해야 한다.
#include <iostream>
using namespace std;

class CTest {
public:
	CTest(int nParam) : m_nData(nParam) { m_nCount++; }
	int GetData() { return m_nData; }
	void ResetCount() { m_nCount = 0; }

	static int GetCount() {
		return m_nCount;
	};

private:
	int m_nData;
	static int m_nCount;
};

int CTest::m_nCount = 0;

int main(void) {
	CTest a(5), b(10);

	cout << a.GetCount() << endl;
	b.ResetCount();

	cout << CTest::GetCount() << endl;

	return 0;
}

 

정적 멤버 함수는 인스턴스 및 멤버 접근 연산자를 활용해도 되고 클래스 이름 및 범위 지정 연산자를 사용해서 호출해도된다

'교재 > 이것이 C++이다' 카테고리의 다른 글

이것이 C++이다 - [2장] C++ 함수와 네임스페이스  (0) 2025.09.11
이것이 C++이다 - [1장] C와는 다른 C++  (2) 2025.08.26
'교재/이것이 C++이다' 카테고리의 다른 글
  • 이것이 C++이다 - [2장] C++ 함수와 네임스페이스
  • 이것이 C++이다 - [1장] C와는 다른 C++
피까츄
피까츄
프로그래밍 마스터가 될테야
  • 피까츄
    프로그래밍 마스터
    피까츄
  • 전체
    오늘
    어제
    • 분류 전체보기 (87)
      • 컴퓨터가 이상해요 모음집 (5)
      • 프로그래밍 (0)
      • 회고 (1)
      • 1학년 (21)
        • 명품 HTML+CSS+JS (10)
        • 쉽게 배우는 C언어 Express (2)
        • R언어 (9)
      • 2학년 (3)
        • C언어로 쉽게 풀어쓴 자료구조 (1)
        • 프로그래밍 언어론 (2)
      • 개인공부 (25)
        • 백준 (17)
        • 코드트리 JS (7)
        • 코테 공부 (1)
      • 챌린지 (1)
        • Do it C++ 코테 6주 챌린지 (1)
      • 교재 (14)
        • 이것이 C++이다 (3)
        • 이것이 JAVA다 (0)
        • 혼자 공부하는 컴퓨터구조 + 운영체제 (1)
        • 혼자 공부하는 데이터통신 (0)
        • 코어 자바스크립트 (8)
        • OpenGL로 배우는 3차원 컴퓨터 그래픽스 (2)
      • 유데미 (11)
        • 100일 코딩 챌린지 (3)
        • C# Unity 2D (8)
      • 기타 (0)
        • 24주 게임 프로그래밍 챌린지 (5)
  • 블로그 메뉴

    • 방명록
    • 그림블로그
    • 3D 블로그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    우분투 설치 오류
    우분투C
    가상현실 설정
    js #자바스크립트_기초
    프로그래밍언어론
    HTML5+CSS3+Javascript 웹 프로그래밍 #연습문제 #이론문제 #실습문제 #풀이 #정답
    우분투java
    복습
    0x80370102오류코드
    윈도우 기능 켜기
    vscode자동완성
    작업표시줄클릭안됨
    the package javax.swing is not accessible
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
피까츄
이것이 C++이다 - [3장] 클래스
상단으로

티스토리툴바