참조타입(class)의 생성자 #1
객체 생성시 생성자
-
참조타입의 객체를 생성하면 “메모리가 먼저 0으로 초기화” 되고 생성자가 호출된다.
-
사용자가 생성자를 제공하지않으면 컴파일러는 매개변수가 없는 생성자 제공(.ctor)
-
예외사항
- Abstract class : protected(family:IL)생성자
- static class: 생성자가 제공되지않는다.
using System;
public class Point
{
public int x;
public int y;
public Point(int x, int y)
{
Console.WriteLine("Point Constructor");
this.x = x;
this.y = y;
}
}
class Program
{
static void Main(string[] args)
{
Point pt = new Point(1, 2);
Console.WriteLine($"{pt.x}, {pt.y}");
}
}
Point 클래스의 객체 생성시 생성자를 설정. 메모리가 먼저 0으로 초기화된후 생성자가 호출된다.
using System;
abstract class AAA { }
static class BBB { }
public class Point
{
public int x;
public int y;
/*
public Point(int x, int y)
{
}
*/
}
class Program
{
static void Main()
{
//Point pt = new Point(1, 2);
Point pt = new Point();
Console.WriteLine($"{pt.x}, {pt.y}");
}
}
기존에 만들어놓았던 생성자를 제거해보았다. 매개변수가 없는 생성자를 컴파일러가 제공해주며 메모리는 0으로 초기화된다.
상속과 생성자
- 파생클래스의 객체를 생성하면
- 기반클래스의 생성자가 먼저 호출된다.
- 기본적으로는 인자없는 생성자가 호출된다.
- 컴파일러는 파생클래스의 생성자안에서 기반클래스의 인자없는 생성자를 호출하는 코드가 추가된다. (아래 코드에서 :Base())
- 기반클래스의 인자있는 생성자를 호출되게 하려면 파생클래스에서 기반클래스의 생성자를 명시적으로 호출.
- 기반클래스에 인자없는 생성자가 없다면 반드시 파생클래스에서 기반클래스의 생성자를 명시적으로 호출해야한다.
using System;
using static System.Console;
class Base
{
// public Base() { WriteLine("Base()"); }
public Base(int n) { WriteLine("Base(int)"); }
}
class Derived : Base
{
public Derived() : base(0) { WriteLine("Derived()"); }
//컴파일러가 base()를 자동으로 넣어준다.
public Derived(int n): base() { WriteLine("Derived(int)"); }
//내가 이렇게 만들면 기반클래스의 인자있는 생성자를 호출한다.
public Derived(int n): base(n) { WriteLine("Derived(int)"); }
}
class Program
{
public static void Main()
{
Derived d = new Derived(1);
}
}
기반클래스와 파생클래스의 생성자 호출부분을 잘 살펴보자.
- 생성자를 protected에 놓으면
- 자신은 객체를 생성할수없지만(추상적존재)
- 파생클래스의 객체는 생성할수있다(구체적존재)
- abstract로 두어도되지만 이때에는 컴파일러가 생성자를 안만듬.(기반클래스를 protected로 두는것이 경우에 따라서 괜찮을수있다)
using System;
class Animal
{
protected Animal() { }
}
class Dog : Animal
{
public Dog() { }
}
class Program
{
static void Main()
{
//# 다음중 에러는 ?
//Animal a = new Animal(); // 1. error 외부에서는 부를수없다.
Dog d = new Dog(); // 2. ok 자신의 생성자에서 부르기때문에 부를수있다.
}
}
기반클래스의 생성자가 protected권한이기때문에 외부에서는 기반클래스에대한 객체생성이 불가능하다(추상적존재). 파생클래스의 객체는 생성이 가능하다.(구체적존재)
참조타입의 생성자 #2
using System;
using static System.Console;
class Base
{
public Base() { Foo(); }
public virtual void Foo() { WriteLine("Base.Foo"); }
}
class Derived : Base
{
public int a = 100;
public int b;
public Derived()
{
b = 100;
}
public override void Foo()
{ WriteLine($"Derived.Foo : {a}, {b}"); }
}
class Program
{
public static void Main()
{
Derived d = new Derived(); //이 순간 화면에서는 어떻게 출력될까?? ->100,0
}
}
파생클래스에서 필드초기화를 한후 화면출력은 Foo함수에서 할때 해당함수를 기반클래스에서 호출한다면 a,b는 어떤 값이 들어가있을까? output은 100, 0 이다.
생성자와 가상함수
- 초기화 순서
- 필드초기화시 컴파일러는 이런식으로 코드를 바꿔서 이해한다.(아래 코드)
필드초기화의 원리
- 초기화 순서
- 필드초기화
- 기반 클래스 생성자
- 생성자안에 있는 초기화 코드
- 따라서 생성자안에서는 가상함수를 사용하지않는게 좋다.
- 참고: c++에서는 생성자에서는 가상함수가 동작하지않는다. (생성자에서 가상함수를 불러도 기반클래스의 함수를 부름. 왜냐하면 자식클래스의 함수에서 초기화되지않은 변수들을 사용할수있기때문에)
기존
class Derived: Base
{
public int a=100; //필드 초기화
public int b;
public Derived()
{
b=100;
}
}
변경
class Derived: Base
{
public int a;
public int b;
public Derived()
{
a=100;
Base();
b=100;
}
}
이런식으로 필드초기화시 컴파일러는 코드를 바꿔서 이해한다.
가상함수의 선택적파라미터 (C++에서는 디폴트파라미터)
- 가상함수에서는 Optional Parameter를 사용하지말자
- optional parameter는 컴파일 시간에 결정되고 가상함수 호출은 실행시간에 결정된다.
using System;
class Base
{
public virtual void Foo(int a = 10)
{
Console.WriteLine($"Base.Foo( {a} )");
}
}
class Derived : Base
{
public override void Foo(int a = 20)
{
Console.WriteLine($"Derived.Foo( {a} )");
}
}
class Program
{
public static void Main()
{
Base b = new Derived();
b.Foo(); // 컴파일 할때
//객체(실행시간에조사하는 코드).Foo(10)
//b.foo() =>컴파일할때는 컴파일러는 b가 base로 인식. 실행시간에 바뀔수있는거라서..
}
}
컴파일시 b는 Base로 인식된다. 런타임시 바뀔수있는것이기때문에 컴파일러는 선언당시의 객체로 인식을 한다. 따라서 선택적파라미터는 Base의 것으로 들어가게 된다.
값타입의 생성자 #1
값타입과 참조타입의 생성자 비교
- Reference Type
- 사용자가 “생성자를 제공하지않은경우, 컴파일러는 인자가 없는 생성자를 제공”한다.
- 사용자가 생성자를 제공하면, 컴파일러는 인자없는 생성자를 제공하지않는다.
- Value Type
- 사용자가 인자가없는 생성자를 만들수없다.
- 컴파일러가 인자가 없는 생성자를 제공하지않는다.
- CLR “값타입의 객체는 언제라도 생성할수있도록(생성자가 없어도) 허용”한다
- 정리
- 참조타입: 객체를 만드려면 생성자가 필요하다. 사용자는 인자없는 생성자와 인자를 가지는 생성자 모두를 만들수있다.
- 값타입: 생성자가 없어도 객체를 만들수있다. 사용자는 인자를 가지는 생성자만 만들수있다.
- =>C#언어만의 제약 : IL언어나 다른 .net언어에서는 값타입도 인자없는 생성자를 만들수있다.
- 생성자 호출과 IL코드
- 참조타입: newobj instance void CPoint::ctor(int32,int32) (인자있는 생성자 호출하여 객체생성)
- 값타입: call instance void SPoint::ctor(int32,int32) (인자있는 생성자를 call) / initObj Spoint (초기화만)
using System;
class CPoint
{
public int x;
public int y;
public CPoint(int a, int b) { x = a; y = b; }
}
struct SPoint
{
public int x;
public int y;
//public SPoint() { }
public SPoint(int a, int b) { x = a; y = b; }
}
class Program
{
public static void Main()
{
CPoint cp1 = new CPoint(1, 2); // ok
//CPoint cp2 = new CPoint(); // error
SPoint sp1 = new SPoint(1, 2); //ok
SPoint sp2 = new SPoint(); //ok
}
}
값타입의 생성자와 참조타입의 생성자 비교. 값타입의 생성자는 인자없는 생성자를 만들수없다(인자있는 생성자만 만들수있음)
값타입과 필드초기화
- 필드초기화시에 인자없는 생성자에서 초기화를 진행한다고 알고있으나, 값타입은 인자없는 생성자를 만들수없다.
- 따라서 값타입에서는 필드초기화를 사용할수없다.
using System;
struct SPoint
{
public int x;// = 0;
public int y;// = 0;
}
class Program
{
public static void Main()
{
SPoint sp1 = new SPoint();
}
}
값타입은 필드초기화 사용이 불가능하다.
값타입의 객체 생성방법과 초기화
- 아래 코드 참고 (값타입은 객체 생성시, new SPoint() 하게되면 초기화가 일어난다. )
using System;
class CPoint
{
public int x;
public int y;
}
struct SPoint
{
public int x;
public int y;
}
class Program
{
public static void Main()
{
CPoint cp1; //# 객체 생성 아님. 참조 변수 생성
CPoint cp2 = new CPoint(); //# 객체 생성.
SPoint sp1; //# 객체 생성 (초기화안함)
SPoint sp2 = new SPoint(); //# 객체 생성, initobj(모든멤버가 0으로 초기화됨)
sp1.x = 10;
sp2.x = 10;
Console.WriteLine($"{sp1.x}");
Console.WriteLine($"{sp2.x}");
}
}
값타입은 객체 선언자체가 객체생성을 의미하며, new키워드는 생성한 객체에대한 초기화를 의미한다.
using System;
struct SPoint
{
public int x;
public int y;
}
class CCircle
{
public SPoint center;
}
struct SCircle
{
public SPoint center;
}
class Program
{
public static void Main()
{
CCircle cc1; //# 객체 아님. 참조 변수
CCircle cc2 = new CCircle(); //# 객체 생성, 모든 멤버가 0으로 초기화(참조변수라면 null로 초기화)
SCircle sc1; //# 객체 생성.
SCircle sc2 = new SCircle(); //#
int n1 = cc1.center.x; //# error. x가 메모리에 없음.
int n2 = cc2.center.x; //# ok. 0
int n3 = sc1.center.x; //# error. x가 초기화 안됨.(x가 메모리에 없는게아니라 초기화의 문제)
int n4 = sc2.center.x; //# ok. x가 초기화 됨.
}
}
주석을 참고하자. 값타입은 객체생성이 안되어서 메모리에없는게 아니라 초기화가 안되었다는데에 초점을 맞출필요가있다.
값타입의 생성자 #2
값타입의 생성자 주의사항
- 참조타입의 객체생성시 모든멤버는 자동으로 0또는 null로 초기화된다.
- 즉 생성자안에서 모든멤버를 초기화하지않아도 된다.
- 값타입은 new없이 객체 생성시 자동으로 초기화 되지않는다.
- 반드시 “값타입의 생성자안에서는 모든멤버의 초기화를 제공”해야한다.
- this
- 참조타입은 상수
- 값타입은 상수가 아님.
using System;
struct SPoint
{
public int x;
public int y;
public int cnt;
public SPoint(int a, int b)
{
//this = new SPoint(); //멤버변수가 너무 많을때 사용하던 방법. 좀이상하게 보여서 그냥 모든멤버초기화를 써주는게 낫다.
x = a;
y = b;
cnt = 0;
}
}
class Program
{
public static void Main()
{
SPoint pt = new SPoint(1, 2);
}
}
값타입은 객체생성시 자동으로 초기화 되지않는다. 따라서 생성자안에 모든멤버의 초기화를 제공해야한다.
using System;
class CPoint
{
public int x;
public int y;
public CPoint(int a = 1, int b = 1) { x = a; y = b; }
}
struct SPoint
{
public int x;
public int y;
public SPoint(int a = 1, int b = 1) { x = a; y = b; }
}
class Program
{
public static void Main()
{
CPoint cp1 = new CPoint(5, 5); // newobj
SPoint sp1 = new SPoint(5, 5); // call 생성자
CPoint cp2 = new CPoint(2);
SPoint sp2 = new SPoint(2);
CPoint cp3 = new CPoint();
SPoint sp3 = new SPoint(); // initobj
Console.WriteLine($"{cp1.x}, {cp1.y}"); // 5, 5
Console.WriteLine($"{sp1.x}, {sp1.y}"); // 5, 5
Console.WriteLine($"{cp2.x}, {cp2.y}"); // 2, 1
Console.WriteLine($"{sp2.x}, {sp2.y}"); // 2, 1
Console.WriteLine($"{cp3.x}, {cp3.y}"); // 1, 1
Console.WriteLine($"{sp3.x}, {sp3.y}"); // 0, 0 //값타입 초기화.
}
}
주석을 참고하자. 값타입의 생성자는 new시 initObj(IL code상에서)를 해준다. (참조타입생성자는 new 시 newObj)
Static생성자(타입생성자)
Static생성자?
- 스태틱멤버를 생성자에서 초기화하면 문제가생길수있다.
- 따라서 스태틱멤버는 생성자에서 초기화하지않고,스태틱생성자를 만들어 초기화한다.
- 타입생성자(클래스 생성자, 정적생성자)
- 생성자 앞에 static 이 붙는 문법
- 접근지정자를 표기하지않는다 (컴파일러가 private을 자동으로 추가한다)
- 인자없는 생성자만 만들수있다.
- 여러개의 객체를 생성해도 단한번만 호출한다.
using System;
class Point
{
public int x;
public int y;
public static int cnt;
public Point(int a, int b)
{
x = a;
y = b;
// cnt = 0;
}
static Point()
{
cnt = 0;
}
}
class Program
{
public static void Main()
{
Point pt1 = new Point(1, 1);
Point pt2 = new Point(2, 2);
}
}
static 변수는 생성자에서 초기화하지않고 스태틱생성자를 만들어서 초기화한다. 이 스태틱생성자는 여러 객체를 생성한다고해도 단한번만 호출된다.
타입생성자의 호출
- 객체를 생성하면 static 생성자가 먼저호출되고, instance생성자가 호출된다.
- 여러개의 객체를 생성해도 단한번만 호출된다.
- 객체를 생성하거나 정적멤버에 접근하는 코드가 있으면 호출된다.
- 멀티스레드 환경에도 안전(thread-safe)하다.
- 여러개의 타입의 static 생성자가 상호참조하는 코드를 작성하면 안된다.
using System;
class Point
{
public int x;
public int y;
public static int cnt;
public Point(int a, int b) { Console.WriteLine("instance ctor"); }
static Point() { cnt = 0; Console.WriteLine("static ctor"); }
}
class Program
{
public static void Main()
{
int n = Point.cnt;
// Point pt1 = new Point(1, 1);
// Point pt2 = new Point(1, 1);
int n2 = A.a;
}
}
class A
{
public static int a;
static A()
{
Console.WriteLine($"A : {B.b}");
a = 10;
}
}
class B
{
public static int b;
static B()
{
Console.WriteLine($"B : {A.a}");
b = 10;
}
}
필드 초기화와 생성자
- 생성자 안에 멤버를 초기화하는 코드는 없지만 생성자 호출전에 메모리가 0으로 초기화된다.
- 정적멤버는 필드초기화할경우 자동으로 스테틱생성자가 만들어진다.
- 필드초기화와 정적생성자가 있을경우, 필드초기화부분이 먼저 일어나고 정적생성자초기화가 일어난다.
using System;
class Point
{
public int x = 0;
public int y = 0;
public static int cnt = 0;
public Point()
{
x = 100;
y = 100;
}
static Point()
{
cnt = 100;
}
}
class Program
{
public static void Main()
{
Point pt1 = new Point();
}
}
- 값타입과 static 생성자
- 값타입은 인자없는 인스턴스생성자는 만들수없지만 스태틱생성자는 만들수있다.
using System;
struct Point
{
public int x;
public int y;
public static int cnt = 0; //ok
// public Point() { } // error
// static Point() { } // ok
}
class Program
{
public static void Main()
{
}
}
Deconstructor(C#7.0)
- 객체의 필드값을 꺼낼때 사용
- Deconstuct라는 이름을 가지는 메소드. out parameter 사용
- 반환된 결과는 tuple로 받는다
- 소멸자(destory)와 혼동하지말것.
- C#에서 제공하는 기능.
using System;
class Point
{
public int x;
public int y;
public Point(int a, int b) { x = a; y = b; }
public void Deconstruct(out int a, out int b)
{
a = x;
b = y;
}
}
class Program
{
public static void Main()
{
Point pt = new Point(1, 2);
int a = pt.x;
int b = pt.y;
var (a1, b1) = pt;
pt.Deconstruct(out int a2, out int b2);
Console.WriteLine($"{a1}, {b1}");
}
}
댓글남기기