[GeeksForGeeks C++ 문제풀이] Construction(생성자)

Jin Hur·2022년 10월 25일
0

C++

목록 보기
14/18

source: https://www.geeksforgeeks.org/c-plus-plus-gq/constructors-gq/

Q1. getchar()

new_c_questionbrgeeksforgeeks + (입력)

C 언어의 getchar() 함수

  • 버퍼에 데이터가 있을 때: 버퍼의 가장 앞 데이터 반환
  • 버퍼에 데이터가 없을 때: 엔터('\n')가 입력될 때까지 사용자로부터 문자를 받아서 버퍼에 저장하고, 버퍼의 가장 앞 데이터 반환
    • ex) '1', '\n'을 입력하면 '1'이 리턴
  • cf) 다음과 같이 버퍼를 초기화 할 수도 있다.
    • while(getchar() != '\n'){}

Q2. 컴파일러의 기본 생성자/복사 생성자/대입 연산자 자동 생성

클래스를 정의할 때 아무것도 작성하지 않는다면, 컴파일러는 (1) 기본 생성, (2) 복사 생성자, (3) 대입 연산자 를 자동으로 추가해준다.


Q3. 복사 생성자 호출 상황과 값 반환 with 임시 객체

복사 생성자는 아래와 같은 상황에서 호출됨

  • (1) 같은 객체를 통해 생성될 때
  • (2) 객체가 값으로서 다른 함수의 인자로 전달될 때 (call-by-value)
  • (3)객체가 값으로서 반환될 때 (객체를 반환하되 참조형으로 반환하지 않을 경우)
    • 컴파일러가 임시 객체를 생성할 때

객체를 새로 생성하는데, 생성과 동시에 동일한 자료형의 객체로 초기화하는 경우 복사 생성자가 호출된다 볼 수 있다.

값 반환 vs 참조 반환

	class AAA {
	private:
		int num;

	public:
		AAA() {
			cout << "기본 생성자 호출" << endl;
		}
		AAA(int n) {
			num = n;
			cout << "매개변수 존재 생성자 호출 " << endl;
		}
		AAA(const AAA& aaa)
			: num(aaa.num)

		{
			cout << "복사 생성자 호출" << endl;
		}
		// 참조 형태로 반환하는 함수
		AAA& retREF(int n) {
			num += n;
			return *this;
		}
		// 값 형태로 반환하는 함수
		AAA retVAL(int n) {
			num += n;
			return *this;
		}

		void printNum() {
			cout << num << endl;
		}
		void setNum(int n) {
			num = n;
		}
	};

	{
		AAA aaa(7);
		
		// 참조 반환 그러나 기본 변수로 선언
		AAA aaa2 = aaa.retREF(3);	// aaa.num == 7 + 3
									// aaa1.num == 7 + 3

		// 참조 반환, 참조 변수로 선언
		// aaa 객체를 참조
		AAA& aaa3 = aaa.retREF(3);	// aaa.num == 7 + 3 + 3
									// aaa3.num == 7 + 3 + 3 

		aaa.printNum();		// 13
		aaa2.printNum();	// 10
		aaa3.printNum();	// 13

		aaa.setNum(100);
		aaa.printNum();		// 100
		aaa2.printNum();	// 10
		aaa3.printNum();	// 100

		// 값으로 반환, 기본 변수로 선언
		// 값으로 반환할 때 임시 객체가 생성됨. 이 임시 객체는 복사 생성자를 통해 생성
		aaa.retVAL(5).printNum();	// 임시객체를 통해 printNum() 호출
		// 임시객체는 다음 행으로 넘어가면 바로 소멸됨
		// 즉, 접근이 불가능하게 된 임시객체는 바로 소며된다.

		// 반면, const 참조 변수로 임시 객체를 참조한다면 바로 소멸되지 않는다.
		const AAA& aaa4 = aaa.retVAL(5);
		aaa4.printNum();
		
		// 값으로 반환, const 참조 변수가 아닌 일반 참조 변수로 선언 => ERROR
		// AAA& aaa5 = aaa.retVAL();

		aaa.printNum();
		aaa4.printNum();
		aaa.setNum(0);
		aaa.printNum();
		aaa4.printNum();	// aaa4는 임시 객체를 참조할 뿐 aaa를 참조하는 것은 아니다.


		// 임시 객체는 소멸되지만 다음과 같이 임시 객체의 정보를 유지할 수도 있다.
		AAA aaa5 = aaa.retVAL(5);	// 기대와 달리 복사 생성자가 2번 호출되지는 않는다.
		aaa5.printNum();	
	}

객체가 값으로서 반환될 때 (객체를 반환하되 참조형으로 반환하지 않을 경우) => 복사 생성자 호출

  • 반환되는 값을 별도의 변수에 저장하는 것과 별개로, 값을 반환하면 반환된 값은 별도의 메모리 공간이 할당되어서 저장이 된다.
    • int func(){ int a = 3; return 3 }
      cout << func() << endl;
      반환된 값을 메모리 공간에 임시로 저장해 놓지 않는 다면, cout을 통한 출력이 불가능했었을 것이다.
  • 정리하자면, 함수가 반환하면, 별도의 메모리 공간이 할당되고, 이 공간에 반화나 값이 저장된다(반환 값으로 초기화된다).

임시 객체

	class AAA {
	private:
		int num;

	public:
		...
		AAA(const AAA& aaa)
			: num(aaa.num)

		{
			cout << "복사 생성자 호출" << endl;
		}
        
        // 값 형태로 반환하는 함수
		AAA retVAL(int n) {
			num += n;
			return *this;
		}
		...
    }
    
    {
    	// 값으로 반환, 기본 변수로 선언
		// 값으로 반환할 때 임시 객체가 생성됨. 이 임시 객체는 복사 생성자를 통해 생성
		aaa.retVAL(5).printNum();	// 임시객체를 통해 printNum() 호출
		// 임시객체는 다음 행으로 넘어가면 바로 소멸됨
		// 즉, 접근이 불가능하게 된 임시객체는 바로 소며된다.

		// 반면, const 참조 변수로 임시 객체를 참조한다면 바로 소멸되지 않는다.
		const AAA& aaa4 = aaa.retVAL(5);
		aaa4.printNum();
		
		// 값으로 반환, const 참조 변수가 아닌 일반 참조 변수로 선언 => ERROR
		// AAA& aaa5 = aaa.retVAL();

		aaa.printNum();
		aaa4.printNum();
		aaa.setNum(0);
		aaa.printNum();
		aaa4.printNum();	// aaa4는 임시 객체를 참조할 뿐 aaa를 참조하는 것은 아니다.


		// 임시 객체는 소멸되지만 다음과 같이 임시 객체의 정보를 유지할 수도 있다.
		AAA aaa5 = aaa.retVAL(5);	// 기대와 달리 복사 생성자가 2번 호출되지는 않는다.
		aaa5.printNum();	
    }

retVAL(int)의 return 문이 실행되는 순간, SoSimple 객체를 위한 메모리 공간이 할당되고, 이 공간에 할당된 객체는 반환되는 객체(위 상황에서는 자기자신 객체)로 초기화된다.

여기서 초기화가 바로 복사 생성자를 통해 이루어진다.

임시 객체 소멸 시점

임시 객체는 다음 행으로 넘어가면 바로 소멸되어 버린다.
단, const 참조자에 참조되는 임시 객체는 바로 소멸되지 않는다.


Q4. 클래스 멤버의 기본 접근 지정자

	class Point {
		Point() {
			cout << "기본 생성자 호출" << endl;
		}
	};
	
    main()
	{
		Point p1;
		return 0;
	}

C++에서 클래스 멤버의 기본 접근 지정자는 private이다. Point 기본 생성자에 별도의 접근 지정을 하지 않았으므로 private이고, private 접근 지정의 멤버 함수는 외부에서 호출할 수 없으므로 컴파일 에러가 발생한다.


Q5. 객체 변수 선언 및 객체 포인터 변수 선언

객체 변수 선언 시에만 생성자가 호출(=> 객체 크기만큼 메모리 할당)되고, 포인터 변수 선언은 그저 포인터 변수만 선언(=> 4/8바이트의 주소를 담을 공간 할당)하였을 뿐이다.


Q6. 생성자/복사 생성자/대입 연산자 호출 시점

#include<iostream>
using namespace std;
 
class Point {
public:
    Point() { cout << "Normal Constructor calledn"; }
    Point(const Point &t) { cout << "Copy constructor calledn"; }
};
 
int main()
{
   Point *t1, *t2;
   t1 = new Point();	// 기본 생성자 호출
   t2 = new Point(*t1);	// 복사 생성자 호출
   Point t3 = *t1;		// 복사 생성자 호출
   Point t4;			// 기본 생성자 호출
   t4 = t3;				// 복사 대입 연산자(assignment operator) 호출
   return 0;
}


Q7. 클래스의 객체를 구조체처럼 초기화

X a = {10}; 는 에러가 발생할 것처럼 보이지만 잘 동작한다.
마치 구조체처럼 클래스의 객체도 위와 같이 초기화할 수 있다. 다만 컴파일러가 기본으로 추가하는 이니셜라이저를 통한 생성자는 public 접근 지정자의 멤버 변수에만 가능하다.


Q8. 복사 생성자만 존재하는 경우

Point 클래스는 기본 생성자는 없고 복사 생성자만 존재한다.
컴파일러는 어떠한 생성자가 없는 경우 기본 생성자를 기본적으로 만들어주는데, 복사 생성자가 존재하기에 기본 생성자를 만들어 주지 않는다(복사 생성자도 생성자!). 따라서 Point p1; 행에서 컴파일 에러가 발생한다.

그렇다면 다음과 같이 기본 생성자를 구현하면 어떨까?

class Point
{
	int x, y;
public:
	Point() {}
	Point(const Point& p) { x = p.x; y = p.y; }
	int getX() { return x; }
	int getY() { return y; }
};

main()
{
	Point p1;
	Point p2 = p1;
	cout << "x = " << p2.getX() << " y = " << p2.getY();
	return 0;
}

기본 생성자에서 멤버 변수 초기화를 진행하지 않는다. 따라서 쓰레기 값이 대입되어있을 것이고, 복사 생성자를 통해 초기화된 p2 객체는 쓰레기 값을 출력한다.


Q9. 디폴트 매개변수


Q10. new와 malloc의 차이

new를 통한 동적할당과 다르게 malloc()을 통한 동적할당은 생성자를 호출하지 않는다. 단지 객체의 크기만큼 힙 영역에 동적으로 공간을 할당받고, 그 공간을 포인터로 반환해주는 것이다. void 포인터 형으로 반환하기에 요청쪽에서는 이를 해당 객체의 클래스 포인터 형으로 타입 변환 후 사용한다.


Q11. 클래스 정의와 동시에 전역에 객체 생성

class Test {
public:
	Test() {
		cout << "Hello from Test()" << endl;
	}
} a;	// 전역 객체 생성!

int main() {
	cout << "Main Started" << endl;

	return 0;
}

Test 클래스를 정의하면서 동시에 a라는 이름으로 객체를 전역에서 생성하였다. 따라서 Test 클래스 생성자가 main 함수보다 먼저 호출되고, 그 다음 메인함수가 호출된다.


Q12. 기본 복사 생성자 호출로 발생된 얕은 복사

String s2 = s1;에서 멤버 char *str에 대해 얕은 복사가 수행된다.
따라서 s1, s2의 멤버 str이 가리키는 문자배열은 동일하다.

깊은 복사를 위해선 따로 복사 생성자를 정의해야한다.


Q13. 복사 생성자의 매개변수

복사 생성자의 매개변수는 객체의 클래스 const 참조형이어야 한다.


Q14. 이니셜라이저 리스트를 통한 초기화

이니셜라이저 리스트
1) <initializer_list> 헤더 파일에 정의되어 있는 클래스
: 여러 인수를 받는 함수를 작성하는데 쓰인다. vector에 저장할 객체의 타입을 지정할 때처럼 원소 타입에 대한 리스트를 <>로 묶어 지정한다.
ex) int sum(initializer_list<int> lst) {..}

2) 클래스 멤버 초기화에 사용되는 리스트
ex) Point (int i=0, int j=0) : x(i), y(j) {..} // 생성자

아래 Point 클래스의 생성자에서 이니셜라이저 리스트를 통한 초기화를 확인할 수 있다.

#include<iostream>
using namespace std;
 
class Point {
private:
    int x;
    int y;
public:
    Point(int i = 0, int j = 0):x(i), y(j) {}
    /*  The above use of Initializer list is optional as the
        constructor can also be written as:
        Point(int i = 0, int j = 0) {
            x = i;
            y = j;
        }
    */   
     
    int getX() const {return x;}
    int getY() const {return y;}
};
 
int main() {
  Point t1(10, 15);
  cout<<"x = "<<t1.getX()<<", ";
  cout<<"y = "<<t1.getY();
  return 0;
}

이니셜라이져를 통한 초기화는 객체의 멤버들이 선언됨과 동시에 초기화된다. 반면 생성자 몸체 내부에서 멤버들을 초기화한다면, 이는 이미 멤버들에 대한 공간이 할당된 후 이곳에 초기화하는 것과 같다. 이러한 이유 때문에 반드시 이니셜라이저 리스트를 통해 초기화가 필요한 멤버 종류들이 있다.

1. 비정적 const 데이터 멤버 초기화

const 변수는 선언과 동시에 초기화되어야 한다. 이는 클래스의 멤버들도 마찬가지이다. 이러한 비정적 const 멤버를 이니셜라이저 리스트를 통해 초기화한다.

#include<iostream>
using namespace std;
 
class Test {
    const int t;	// 비정적 const 데이터 멤버
public:
    Test(int t):t(t) {}  //Initializer list must be used
    int getT() { return t; }
};
 
int main() {
    Test t1(10);
    cout<<t1.getT();
    return 0;
}

2. 참조 멤버 초기화

참조변수 형태의 멤버 또한 선언과 동시에 초기화되어야 한다.

#include<iostream>
using namespace std;
 
class Test {
    int &t;	// 참조형 멤버 데이터
public:
    Test(int &t):t(t) {}  //Initializer list must be used
    int getT() { return t; }
};
 
int main() {
    int x = 20;
    Test t1(x);
    cout<<t1.getT()<<endl;
    x = 30;
    cout<<t1.getT()<<endl;
    return 0;
}

3. 기본 생성자가 없는 멤버 객체 초기화

다른 클래스의 객체를 멤버로 갖고, 이 클래스의 기본 생성자가 없는 경우 인자를 전달하여 매개변수가 존재하는 생성자를 호출해야 한다. 이러한 경우에도 이니셜라이저 리스트를 통한 초기화를 사용한다.


#include <iostream>
using namespace std;
 
class A {
    int i;
public:
    A(int i);
    // 기본 생성자가 존재하지 않은 클래스 A
};
 
A::A(int arg) {
    i = arg;
    cout << "A's Constructor called: Value of i: " << i << endl;
}
 
// Class B contains object of A
class B {
    A a;	// 기본 생성자가 없는 클래스형 객체이기에 이니셜라이저 리스트를 통해 초기화해야한다. 
public:
    B(int );
};
 
B::B(int x):a(x) {  //Initializer list must be used
    cout << "B's Constructor called";
}
 
int main() {
    B obj(10);
    return 0;
}

4. 생성자 매개변수 이름과 멤버변수 이름이 같은 경우, this를 쓰고 싶지 않을 때

class A {
    int i;
public:
    A(int );
    int getI() const { return i; }
};
 
A::A(int i):i(i) { }  // Either Initializer list or this pointer must be used
/* The above constructor can also be written as
A::A(int i) {
    this->i = i;
}
*/
 
int main() {
    A a(10);
    cout<<a.getI();
    return 0;
}

5. 성능상의 이점

(1) 생성자 몸체에서 초기화

// Without Initializer List
class MyClass {
    Type variable;
public:
    MyClass(Type a) {  // Assume that Type is an already
                     // declared class and it has appropriate
                     // constructors and operators
      variable = a;
    }
};

1) 생성자에 인자로 Type 객체를 전달할 때 복사 생성자 호출
2) variable 객체 생성을 위해 Type 생성자 기본 생성자 호출
3) variable = a; 수행을 통해 복사 생성자 호출
4) 생성자 몸체에서 벗어날 때 a 객체 소멸을 위한 소멸자 호출

=> 복사 생성자 + 기본 생성자 + 복사 생성자 + 소멸자

(2) 이니셜라이저 리스트를 통한 초기화

class MyClass {
    Type variable;
public:
    MyClass(Type a):variable(a) {   // Assume that Type is an already
                     // declared class and it has appropriate
                     // constructors and operators
    }
};

1) 생성자에 인자로 Type 객체를 전달할 때 복사 생성자 호출
2) : variable(a) 수행을 위해 복사 생성자 호출(선언과 동시에 복사를 통한 초기화)
3) 생성자 몸체에서 벗어날 때 a 객체 소멸을 위한 소멸자 호출

=> 복사 생성자 + 복사 생성자 + 소멸자
불필요한 기본 생성자의 호출을 한번 줄일 수 있다.


Q15. 생성자 특징

  • 생성자는 가상함수일 수 없다.
  • 생성자는 private 접근 지정을 할 수 있다.
    • 이를 통해 new 연산자를 통한 객체 생성을 강제한다.
  • new 연산자를 호출하면 자동적으로 생성자가 호출된다.

Q16. static 변수

fun() 함수 내 static 변수 선언은 한번만 일어난다. 따라서 Test 생성자는 한번만 호출된다.


Q17. 변환 생성자(conversion construct)

암시적인 형 변환으로 객체를 생성할 수 있게하는 것이 변환 생성자이다.

reference: https://psychoria.tistory.com/40


Q18. 복사 생성자 매개변수는 const 참조형

이 문제는 기본적인 복사 생성자 구현 방식에 대한 함정 문제

복사 생성자 매개변수는 const 참조형!!

Why copy constructor argument should be const in C++? : https://www.geeksforgeeks.org/copy-constructor-argument-const/


Q20. 생성자는 리턴 타입이 없다.


Q21. 생성자의 암시적인 리턴 타입은 클래스 타입 그 자체


Q22. 복사 생성자 선언


Q23. 시간 복잡도

0개의 댓글