이번 시간에는 IL과 C#의 원리, Constructor에 대해 알아보겠습니다.

C#과 IL

컴파일 언어로(C/C++ 등) 작성된 프로그램의 문제점

  • 컴파일시 타겟된 특정 환경(Intel, Windows OS)에서만 동작함
  • 다른 환경에서 사용하려면 다시 컴파일하거나, 소스 코드 자체를 재작성 해야함

C# 프로그램의 구조

  • C# 코드 -> C# 컴파일러 -> 실행가능한 파일(플랫폼 독립적인 기계어 코드, 중간 언어

    • Intermediate Language or CIL(Common IL)
    • C# -> IL
    • Java -> Byte Code)
  • .net C++ 컴파일러를 사용하면 C++ 언어도 IL로 컴파일 가능(additional VB possible)

  • C# 코드 예제

    using System;
    
    struct Point
    {
        public int x;
        public int y;
    }
    
    class Program
    {
        public static void Main()
        {
            Point pt1; // 멤버 변수 쓰레기값
            Point pt2 = new Point(); // 디폴트값 초기화 발생
    
            Console.WriteLine($"{pt1.x}");
        }
    }
    
    • 위 예제로 생성된 IL 파일
    //  Microsoft (R) .NET Framework IL Disassembler.  Version 4.8.3928.0
    //  Copyright (c) Microsoft Corporation. All rights reserved.
    
    
    
    // Metadata version: v4.0.30319
    .assembly extern mscorlib
    {
      .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
      .ver 4:0:0:0
    }
    .assembly sample
    {
      .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
      .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                                 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.
    
      // --- 다음 사용자 지정 특성이 자동으로 추가됩니다. 주석 처리를 제거하지 마십시오. -------
      //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) 
    
      .hash algorithm 0x00008004
      .ver 0:0:0:0
    }
    .module sample.exe
    // MVID: {8C5F9126-C61E-4997-B6E2-AA38E719E29E}
    .imagebase 0x00400000
    .file alignment 0x00000200
    .stackreserve 0x00100000
    .subsystem 0x0003       // WINDOWS_CUI
    .corflags 0x00000001    //  ILONLY
    // Image base: 0x06B60000
    
    
    // =============== CLASS MEMBERS DECLARATION ===================
    
    .class private sequential ansi sealed beforefieldinit Point
           extends [mscorlib]System.ValueType
    {
      .field public int32 x
      .field public int32 y
    } // end of class Point
    
    .class private auto ansi beforefieldinit Program
           extends [mscorlib]System.Object
    {
      .method public hidebysig static void  Main() cil managed
      {
        .entrypoint
        // 코드 크기       10 (0xa)
        .maxstack  1
        .locals init (valuetype Point V_0,
                 valuetype Point V_1)
        IL_0000:  nop
        IL_0001:  ldloca.s   V_1
        IL_0003:  initobj    Point
        IL_0009:  ret
      } // end of method Program::Main
    
      .method public hidebysig specialname rtspecialname 
              instance void  .ctor() cil managed
      {
        // 코드 크기       8 (0x8)
        .maxstack  8
        IL_0000:  ldarg.0
        IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
        IL_0006:  nop
        IL_0007:  ret
      } // end of method Program::.ctor
    
    } // end of class Program
    
    
    // =============================================================
    
    // *********** 디스어셈블리 완료 ***********************
    // 경고: Win32 리소스 파일 b.res을(를) 만들었습니다.
    

IL 직접 작성해보기

  • exe파일 생성 방법 -> ilasm 이름.il
.assembly ex1 { }

.method static void foo() cil managed
{
    .entrypoint // Main 함수로 인식 처리

    ret
}
  • 함수 호출 예제
.assembly ex2 { }

.method static void foo() cil managed
{
    .entrypoint

    call  void goo()

    ldc.i4.1    // stack에 push 1
    ldc.i4.2    // stack에 push 2
    call void hoo(int32 a, int32 b)
    ret
}

.method static void hoo(int32 a, int32 b) cil managed
{
    ret
}

.method static void goo() cil managed
{
    ret
}
  • 외부 라이브러리 사용(화면 출력) 예제
.assembly ex3 { }
.assembly extern mscorlib {}

.method static void foo() cil managed
{
    .entrypoint
    
    // C# : System.Console.WriteLine()
    call void [mscorlib]System.Console::WriteLine()

    // C# : System.Console.WriteLine("Hello, IL")
    ldstr "Hello, IL"
    call void [mscorlib]System.Console::WriteLine(class System.String)
    ret
}
  • 지역변수 할당 및 전달(박싱) 예제
.assembly ex4 { }
.assembly extern mscorlib {}

//    int x = 2;
//    int y = 20;
//    Console.WriteLine("{0}, {1}", x, y);

.method public static void foo() cil managed
{
    .entrypoint

    .locals init(int32 V_0, int32 V_1)

    ldc.i4.2    // 상수를 스택에 push 2
    stloc.0     // x = 2

    ldc.i4.s 20 // push 20
    stloc.1     // y = 20

    ldstr "{0}, {1}"
    ldloc.0 
    box   int32 // object로 전달하기 위해 박싱

    ldloc.1
    box   int32 // object로 전달하기 위해 박싱

    call void [mscorlib] System.Console::WriteLine(string, object, object)
    ret
}
  • 클래스 사용(static) 예제
.assembly ex5 { }
.assembly extern mscorlib {}

.class public Program
{
    .method public static void foo() cil managed
    {
        ldstr "foo"
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }
    .method public static void Main() cil managed
    {
        .entrypoint

        ldstr "Main"
        call void [mscorlib]System.Console::WriteLine(string)

        call void Program::foo()

        ret
    }
}
  • 객체 생성(instance) 및 사용 예제
.assembly ex06 { }
.assembly extern mscorlib {}

.class public Program
{
    .method public instance void foo() cil managed
    {
        ldstr "foo"
        call void [mscorlib]System.Console::WriteLine(string)
        ret
    }

    .method public specialname rtspecialname instance void .ctor() cil managed
    {
        // 기반 클래스 생성자 호출
        ldarg.0
        call       instance void [mscorlib]System.Object::.ctor()
        ret
    }


    .method public static void Main() cil managed
    {
        .entrypoint

        // Program p = new Program()
        .locals init( class Program V_0 )
        
        newobj  instance void Program::.ctor()

        stloc.0 

        // p.foo()
        //call void Program::foo()
        ldloc.0 
        callvirt instance void Program::foo()

        ret
    }
}

연산자 재정의 함수의 원리

  • C# 코드 표현은 함수 재정의 처럼 보여도, IL로 생성된 코드는 단순히 약속된 이름의 메소드를 호출 하는 것에 불과함
  • 이러한 특징으로 인해, C# 표현 자체에서 제공하는 기법인지, IL에서 다시 정의되는 기법인지 구분이 필요함
using System;

class Point
{
    public int x = 0;
    public int y = 0;

    public Point(int a, int b) { x = a; y = b; }



    public static Point operator+(Point p1, Point p2)
    {
        Point p = new Point(p1.x + p2.x, p1.y + p2.y);
        return p;
    }
    
    /* 이렇게 하면 specialname이 붙지 않아서 불가능한 이름
    public static Point op_Addition(Point p1, Point p2)
    {
        Point p = new Point(p1.x + p2.x, p1.y + p2.y);
        return p;
    }
    */
}

class Program
{
    static void Main(string[] args)
    {
        Point p1 = new Point(1, 1);
        Point p2 = new Point(2, 2);

        Point p3 = p1 + p2;

        Console.WriteLine($"{p3.x}, {p3.y}");
    }
}
//  Microsoft (R) .NET Framework IL Disassembler.  Version 4.8.3928.0
//  Copyright (c) Microsoft Corporation. All rights reserved.



// Metadata version: v4.0.30319
.assembly extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
  .ver 4:0:0:0
}
.assembly operator
{
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
  .custom instance void [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                             63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.

  // --- 다음 사용자 지정 특성이 자동으로 추가됩니다. 주석 처리를 제거하지 마십시오. -------
  //  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 ) 

  .hash algorithm 0x00008004
  .ver 0:0:0:0
}
.module operator.exe
// MVID: {282B947A-9C2B-4F10-82A5-FF8E30CB878D}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003       // WINDOWS_CUI
.corflags 0x00000001    //  ILONLY
// Image base: 0x06F00000


// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit Point
       extends [mscorlib]System.Object
{
  .field public int32 x
  .field public int32 y
  .method public hidebysig specialname rtspecialname 
          instance void  .ctor(int32 a,
                               int32 b) cil managed
  {
    // 코드 크기       37 (0x25)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldc.i4.0
    IL_0002:  stfld      int32 Point::x
    IL_0007:  ldarg.0
    IL_0008:  ldc.i4.0
    IL_0009:  stfld      int32 Point::y
    IL_000e:  ldarg.0
    IL_000f:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0014:  nop
    IL_0015:  nop
    IL_0016:  ldarg.0
    IL_0017:  ldarg.1
    IL_0018:  stfld      int32 Point::x
    IL_001d:  ldarg.0
    IL_001e:  ldarg.2
    IL_001f:  stfld      int32 Point::y
    IL_0024:  ret
  } // end of method Point::.ctor

  .method public hidebysig specialname static 
          class Point  op_Addition(class Point p1,
                                   class Point p2) cil managed
  {
    // 코드 크기       39 (0x27)
    .maxstack  3
    .locals init (class Point V_0,
             class Point V_1)
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  ldfld      int32 Point::x
    IL_0007:  ldarg.1
    IL_0008:  ldfld      int32 Point::x
    IL_000d:  add
    IL_000e:  ldarg.0
    IL_000f:  ldfld      int32 Point::y
    IL_0014:  ldarg.1
    IL_0015:  ldfld      int32 Point::y
    IL_001a:  add
    IL_001b:  newobj     instance void Point::.ctor(int32,
                                                    int32)
    IL_0020:  stloc.0
    IL_0021:  ldloc.0
    IL_0022:  stloc.1
    IL_0023:  br.s       IL_0025

    IL_0025:  ldloc.1
    IL_0026:  ret
  } // end of method Point::op_Addition

} // end of class Point

.class private auto ansi beforefieldinit Program
       extends [mscorlib]System.Object
{
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    // 코드 크기       64 (0x40)
    .maxstack  3
    .locals init (class Point V_0,
             class Point V_1,
             class Point V_2)
    IL_0000:  nop
    IL_0001:  ldc.i4.1
    IL_0002:  ldc.i4.1
    IL_0003:  newobj     instance void Point::.ctor(int32,
                                                    int32)
    IL_0008:  stloc.0
    IL_0009:  ldc.i4.2
    IL_000a:  ldc.i4.2
    IL_000b:  newobj     instance void Point::.ctor(int32,
                                                    int32)
    IL_0010:  stloc.1
    IL_0011:  ldloc.0
    IL_0012:  ldloc.1
    IL_0013:  call       class Point Point::op_Addition(class Point,
                                                        class Point)
    IL_0018:  stloc.2
    IL_0019:  ldstr      "{0}, {1}"
    IL_001e:  ldloc.2
    IL_001f:  ldfld      int32 Point::x
    IL_0024:  box        [mscorlib]System.Int32
    IL_0029:  ldloc.2
    IL_002a:  ldfld      int32 Point::y
    IL_002f:  box        [mscorlib]System.Int32
    IL_0034:  call       string [mscorlib]System.String::Format(string,
                                                                object,
                                                                object)
    IL_0039:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_003e:  nop
    IL_003f:  ret
  } // end of method Program::Main

  .method public hidebysig specialname rtspecialname 
          instance void  .ctor() cil managed
  {
    // 코드 크기       8 (0x8)
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
    IL_0006:  nop
    IL_0007:  ret
  } // end of method Program::.ctor

} // end of class Program


// =============================================================

// *********** 디스어셈블리 완료 ***********************
// 경고: Win32 리소스 파일 operator.res을(를) 만들었습니다.

C# 생성자

참조 타입의 생성자

  • 참조 타입의 객체를 생성하면 메모리가 먼저 0으로 초기화 되고 생성자가 호출됨
  • 사용자가 생성자를 구현하지 않으면
    • 추상 클래스(abstract) : protected 생성자 제공
    • static class : 기본 생성자 제공되지 않음
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}");
    }
}

상속과 생성자

  • 파생 클래스의 객체를 생성하면 -> 기반 클래스의 생성자가 먼저 호출
  • 기반 클래스의 다른 생성자를 호출되게 하려면 -> 파생 클래스의 생성자에 명시적으로 표현
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()"); }
    public Derived(int n): base(n) { WriteLine("Derived(int)"); }
}
class Program
{
    public static void Main()
    {
        Derived d = new Derived(1);
    }
}
  • 다음 코드의 출력 결과는?
    1. 필드 초기화
    2. 기반 클래스 생성자
    3. virtual function 동작(C++은 생성자에서 가상함수 동작하지 않으나 C#은 동작)
    4. 파생 클래스 생성자 내부 코드 동작
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();
    }
}
  • 가상함수와 선택적 파라메터
    • 아래 코드는 무엇이 출력될 것인가?
      • 컴파일러가 디폴트값을 채우는데 Base로 판단하여 기본값 10을 전달
    • 가상 함수는 디폴트 파라메터를 사용하지 말자(버그 유발)
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)
    }
}

값 타입의 생성자

  • 참조 타입

    • 컴파일러가 기본 생성자 제공
    • 사용자가 인자 없는 생성자 작성 가능
    • 객체를 만드려면 생성자가 반드시 필요
  • 값 타입

    • 생성자가 없어도 객체를 만들 수 있다. (쓰레기값 초기화)

    • 컴파일러가 기본 생성자 제공하지 않음

    • 사용자는 인자를 갖는 생성자만 만들 수 있음

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);  
        SPoint sp2 = new SPoint();      
    }
}
  • 생성자 호출과 IL 코드

    구 분 내 용
    참조 타입 newobj instance void CPoint::.ctor(int32, int32)
    값 타입 call instance void SPoint::.ctor(int32, int32)initobj SPoint
  • 값 타입과 필드 초기화 -> 사용 불가

using System;

struct SPoint
{
    public int x;// = 0; 사용 불가
    public int y;// = 0; 사용 불가
}
class Program
{
    public static void Main()
    {
        SPoint sp1 = 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

        sp1.x = 10;
        sp2.x = 10;

        Console.WriteLine($"{sp1.x}");
        Console.WriteLine($"{sp2.x}");
    }
}

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으로 초기화
        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가 초기화 안됨.
        int n4 = sc2.center.x;  //# ok.    x가 초기화 됨.
    }
}
  • 값 타입 생성자 주의사항
    • 참조 타입의 객체 생성시 모든 멤버는 자동으로 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 주의!!!!
    }    
}

타입 생성자

  • static 생성자 : static 멤버 변수 초기화시 사용, 여러 객체를 할당해도 단 1회만 호출
    • 인자 없는 생성자만 만들 수 있음
    • 가장 먼저 호출됨
    • thread-safe함
    • A, B 클래스가 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 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;
    }
}

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;
    }
}
  • 필드 초기화와 생성자
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 생성자
    • 값 타입은 인자 없는 생성자 불가, 인자 없는 static 초기화는 가능
using System;

struct Point
{
    public int x;
    public int y;
    public static int cnt = 0;

 //   public Point() { } // error
 //   static Point() { } // ok
}

class Program
{
    public static void Main()
    {
        
    }
}

Deconstructor(C# 7.0)

  • 객체에서 값을 꺼낼 때 활용 가능
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;

        //pt.Deconstruct(out int a2, out int b2);
        //아래 표현을, 위 표현으로 c# 언어에서 대체 시킴
        var (a1, b1) = pt;

        Console.WriteLine($"{a1}, {b1}");
    }
}