C# Classes and Objects Explained for Beginners
Classes are the blueprints. Objects are the real things you build from them.
Think about a car. A car has a make, a model, a colour, an engine size, and a speed. It can accelerate, brake, and honk.
Now think about a specific car — your neighbour's red Tesla, for example. It has actual values for those attributes, and it can actually perform those actions.
In C#, the class is the blueprint — the description of what a car is and what it does. The object is the actual instance — your neighbour's red Tesla, with real data and real behaviour.
Classes and objects are the heart of object-oriented programming in C#. They let you model real-world things, organise code, and build applications that are easier to understand, maintain, and extend.
What Is It?
The Simple Explanation
A class is a template or blueprint. It defines the data (fields/properties) and behaviour (methods) that objects of that class will have.
An object is an actual instance of a class — a concrete thing that exists in memory with its own state.
The Technical Definition
A class in C# is a reference type that defines a data structure and the operations that can be performed on that data. It is the fundamental building block of object-oriented programming.
An object is an instance of a class — allocated on the managed heap, with its own copy of the instance fields declared by the class.
Class = the blueprint. It exists in your source code.
Object = the building built from the blueprint. It exists in memory at runtime.
You can have one class and create many objects from it — just like one blueprint can build many houses.
Key facts:
- A class can contain fields (data) and methods (behaviour).
- A class can also contain properties, constructors, events, and more.
- An object's state is stored in its fields/properties.
- Objects are created with the
newkeyword.
Why Does It Exist?
The Problem
Without classes, your program's data and behaviour are scattered. You might have arrays of primitive values, and functions that operate on them. But as the system grows, it becomes hard to:
- Keep related data together (e.g., a customer's name, address, and order history).
- Ensure that data is valid and consistent.
- Reuse logic across different parts of the application.
- Model real-world relationships clearly.
The Solution
Classes provide a way to encapsulate data and behaviour into a single unit. This gives us:
- Encapsulation — data and methods that operate on it live together.
- Abstraction — you interact with a class through its public interface, without knowing its internals.
- Reusability — you can create many objects from one class.
- Maintainability — changes to a class are isolated from the rest of the system.
Big Picture
Here's how classes and objects relate to each other and to your application:
new Car() → objectSpeed: 0 → 60
Speed: 20 → 80
Speed: 0 → 45
How It Works
Let's trace what happens when you define a class and create objects from it.
public class Car
{
public string Model { get; set; }
public int Speed { get; private set; }
public void Accelerate()
{
Speed += 10;
}
}This defines what a Car looks like: it has a Model, a Speed, and it can Accelerate().
Car myCar = new Car();
myCar.Model = "Tesla Model 3";The new keyword allocates memory on the heap, initialises the object, and returns a reference. Now myCar points to a real Car object in memory.
myCar.Accelerate(); // Speed becomes 10
myCar.Accelerate(); // Speed becomes 20
Console.WriteLine(myCar.Speed); // 20Calling a method on the object runs the behaviour defined in the class, using the object's own data.
Car anotherCar = new Car();
anotherCar.Model = "BMW i4";
anotherCar.Accelerate(); // Speed: 10
// myCar.Speed is still 20 — independent!Each object has its own copy of the instance data. Changing one does not affect the other.
- Objects live on the managed heap.
- When no references point to an object, it becomes eligible for garbage collection.
- The GC reclaims the memory automatically.
Simple Example
Let's build a simple Person class and create objects from it.
public class Person
{
// Fields
private string _name;
private int _age;
// Constructor
public Person(string name, int age)
{
_name = name;
_age = age;
}
// Properties
public string Name => _name;
public int Age => _age;
// Method
public void Introduce()
{
Console.WriteLine($"Hi, I'm {_name} and I'm {_age} years old.");
}
// Method with behaviour
public void HaveBirthday()
{
_age++;
Console.WriteLine($" {_name} is now {_age}!");
}
}
// Usage
Person alice = new Person("Alice", 30);
Person bob = new Person("Bob", 25);
alice.Introduce(); // Hi, I'm Alice and I'm 30 years old.
bob.Introduce(); // Hi, I'm Bob and I'm 25 years old.
alice.HaveBirthday(); // Alice is now 31!
bob.HaveBirthday(); // Bob is now 26!
Console.WriteLine($"{alice.Name} is {alice.Age}"); // Alice is 31Code → Meaning → Result
Person alice = new Person("Alice", 30);— creates a newPersonobject, passing values to the constructor.alice.Introduce()— calls a method onalice.alice.HaveBirthday()— modifiesalice's state but notbob's.alice.Name— property access returns the value, but the field isprivateso it cannot be changed directly.
Real-World Example
Imagine an e-commerce system that manages orders. Each order has items, a total, and a status. Here's how you might model this:
public class OrderItem
{
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal Total => Quantity * UnitPrice;
}
public class Order
{
private List<OrderItem> _items = new();
public int OrderId { get; set; }
public DateTime OrderDate { get; set; }
public string CustomerName { get; set; }
public string Status { get; private set; } = "Pending";
public IReadOnlyList<OrderItem> Items => _items;
public void AddItem(OrderItem item)
{
_items.Add(item);
}
public decimal TotalAmount => _items.Sum(i => i.Total);
public void Ship()
{
if (Status != "Pending")
throw new InvalidOperationException("Order already processed.");
Status = "Shipped";
}
public void Deliver()
{
if (Status != "Shipped")
throw new InvalidOperationException("Order must be shipped first.");
Status = "Delivered";
}
}
// Usage in a real application:
var order = new Order
{
OrderId = 1001,
OrderDate = DateTime.UtcNow,
CustomerName = "Jane Doe"
};
order.AddItem(new OrderItem { ProductName = "Laptop", Quantity = 1, UnitPrice = 1200.00m });
order.AddItem(new OrderItem { ProductName = "Mouse", Quantity = 2, UnitPrice = 25.99m });
Console.WriteLine($"Order total: {order.TotalAmount:C}"); // $1,251.98
order.Ship();
order.Deliver();
Console.WriteLine($"Status: {order.Status}"); // DeliveredIn this example:
Orderencapsulates the order's state and behaviour.OrderItemis a separate class because each item is a distinct thing with its own data.- The order controls its own status transitions — you can't deliver before shipping.
- The list of items is private; callers can only read it via
IReadOnlyList.
Analogy
A class is like an architect's blueprint for a house. The blueprint specifies:
- How many rooms there are (fields)
- What you can do in the house (methods)
- The structure and layout (class definition)
An object is the actual house built from that blueprint. You can build many houses from the same blueprint:
- Each house has its own occupants (state).
- Each house can have different paint colours (property values).
- But all houses share the same design (class definition).
The blueprint doesn't change when you paint one house red. Each object is independent.
Under the Hood
What actually happens inside the .NET runtime when you define a class and create objects?
- When the compiler sees a
classdefinition, it emits type metadata into the assembly. - The metadata describes the class name, fields, methods, properties, and their accessibility.
new Car()instructs the runtime to allocate memory on the managed heap.- The allocation includes space for all instance fields plus an object header (type info, sync block).
- The runtime initialises the object's fields to their default values (
0,null,false).
- After allocation, the constructor runs, initialising the object with specific values.
- Constructors can call base constructors, and they can be overloaded.
- Instance methods receive an implicit
thisreference to the object. - The runtime uses the object's type metadata to locate the method implementation.
- Virtual methods use a vtable (virtual method table) for dynamic dispatch.
- When an object has no references pointing to it, it becomes unreachable.
- The Garbage Collector (GC) will eventually reclaim its memory.
- You can help the GC by disposing of unmanaged resources (
IDisposable).
Common Confusion
1. Class vs Object
This is the most common confusion. A class is the definition — it's like the text of a recipe. An object is the actual cake you bake — it exists in the real world (memory).
- Exists in source code
- Defines structure & behaviour
- Created once per type
- No state (it's a template)
- Exists in memory at runtime
- Has actual data (state)
- Many can be created
- Each has its own state
2. class vs struct
A class is a reference type (stored on the heap; passed by reference). A struct is a value type (stored on the stack or inline; passed by value). Classes are used for complex objects with behaviour; structs are used for small, simple data containers.
3. Fields vs Properties
A field is a variable directly stored in the object. A property is a pair of methods (getter/setter) that control access to a field. Properties allow you to add logic (validation, notifications) while keeping the public API stable.
public class Example
{
private int _value; // field
public int Value // property
{
get => _value;
set
{
if (value < 0) throw new ArgumentException("Must be non-negative");
_value = value;
}
}
}4. Static vs Instance
Instance members belong to a specific object. Static members belong to the class itself — they are shared across all objects and exist even if no objects are created.
public class Counter
{
public int InstanceCount; // each object has its own
public static int TotalCreations; // shared across all objects
public Counter()
{
InstanceCount = 0;
TotalCreations++;
}
}Common Mistakes
Mistake 1 — Forgetting to use new
Wrong:
Person p; // p is null — no object exists!
p.Introduce(); // NullReferenceException!Correct:
Person p = new Person("Alice", 30);
p.Introduce(); // Works!Mistake 2 — Confusing static and instance
Wrong:
Console.WriteLine(Counter.TotalCreations); // OK, static
Console.WriteLine(Counter.InstanceCount); // Error! Can't access instance member from classCorrect:
var c = new Counter();
Console.WriteLine(c.InstanceCount); // OK, instance
Console.WriteLine(Counter.TotalCreations); // OK, staticMistake 3 — Exposing mutable collections directly
Wrong:
public class Order
{
public List<OrderItem> Items { get; set; } // Anyone can modify!
}Correct:
public class Order
{
private readonly List<OrderItem> _items = new();
public IReadOnlyList<OrderItem> Items => _items; // Read-only view
public void AddItem(OrderItem item) => _items.Add(item);
}Mistake 4 — Using == for object equality incorrectly
Wrong:
Person a = new Person("Alice", 30);
Person b = new Person("Alice", 30);
Console.WriteLine(a == b); // False — different objects, even if data is sameCorrect:
Console.WriteLine(a.Name == b.Name && a.Age == b.Age); // True
// Or override Equals and == in your class for value equalityWhen Should I Use It?
Use classes when:
- You need to model a real-world entity with both data and behaviour.
- You want to encapsulate complex logic and maintain state.
- You need inheritance or polymorphism.
- You want to create many objects with similar structure.
Consider records (record) when:
- You primarily need immutable data containers.
- Value-based equality is important.
- You have DTOs (Data Transfer Objects).
When classes might be overkill:
- If you only need a simple grouping of data without behaviour, consider a
structorrecord struct. - If you're writing a small helper function, a class may not be needed — static methods can suffice.
- For very high-performance, low-level scenarios, you might use spans, pointers, or arrays directly.
Mental Model
Object = the real thing built from the blueprint
new = the construction crew that builds the object
Field = a piece of data inside the object
Method = an action the object can perform
Property = a controlled way to access data
Constructor = the setup instructions when building
Remember:
· One class, many objects
· Each object has its own state
· Objects are independent
· Static members are shared across all objects
Key Takeaway
- A class is a blueprint — it defines what an object is and does.
- An object is an instance — it exists in memory with its own state.
- Use
newto create objects — then call methods and access properties. - Encapsulate data — keep fields private; expose controlled properties and methods.
- Each object is independent — changing one doesn't change others.
- Use classes to model real-world entities — customers, orders, products, users, etc.
Check Your Understanding
You've seen how classes define blueprints and objects are the real instances. Let's test your understanding.
1. What is the difference between a class and an object?
Show answer
Correct: B
Why B is correct: A class is the definition (the blueprint) that describes what data and behaviour an object will have. An object is a concrete instance of that class — a real thing in memory with its own state.
Why A is incorrect: Both classes and objects are reference types (stored on the heap in .NET). The storage location depends on the type, not the class/object distinction.
Why C is incorrect: Both classes and objects have data and behaviour. A class defines both; an object has both.
Why D is incorrect: You use new to create objects from a class. The class itself is defined in source code.
Reinforcement: Class = blueprint, Object = building built from that blueprint.
2. Given the following class, what is the output?
public class Counter
{
public int Value = 0;
public static int Total = 0;
public Counter()
{
Total++;
}
public void Increment()
{
Value++;
Total++;
}
}
var a = new Counter();
var b = new Counter();
a.Increment();
b.Increment();
b.Increment();
Console.WriteLine($"{a.Value} {b.Value} {Counter.Total}");Show answer
Correct: A — 1 2 6
Why A is correct:
- Two objects are created → each constructor increments
Totalto 2. a.Increment()→a.Value = 1,Total = 3.b.Increment()→b.Value = 1,Total = 4.b.Increment()→b.Value = 2,Total = 5.- Total count: 2 (constructors) + 3 (increments) = 6.
Why B, C, D are incorrect: They miscount the static Total increments. The static field is shared and incremented in every constructor and every Increment() call.
Reinforcement: Static members belong to the class, not to individual objects. They are shared across all instances.
3. Which of the following correctly demonstrates encapsulation?
Show answer
Correct: B
Why B is correct: Encapsulation means hiding internal details and controlling access through a public interface. Making fields private and exposing them via properties or methods with validation is the textbook way to achieve encapsulation.
Why A is incorrect: Making all fields public breaks encapsulation — external code can directly modify the object's state, bypassing any validation or logic.
Why C is incorrect: A class with only public fields and no methods is a data container, not an encapsulated object.
Why D is incorrect: A class with no fields and only static methods isn't an object-oriented design — it's more like a module or utility class.
Reinforcement: Encapsulation = hide internal state, expose controlled public API.
4. What does the following code print?
public class Person
{
public string Name { get; set; }
public Person(string name) => Name = name;
public void ChangeName(string newName) => Name = newName;
}
Person p1 = new Person("Alice");
Person p2 = p1;
p2.ChangeName("Bob");
Console.WriteLine(p1.Name);Show answer
Correct: B — Bob
Why B is correct: Classes are reference types. p2 = p1 copies the reference, not the object. Both variables point to the same Person object. Changing the name through p2 modifies the same object that p1 references, so p1.Name is also "Bob".
Why A is incorrect: This would be true if Person were a struct (value type) where assignment copies the data. But class is a reference type.
Why C is incorrect: The object is never set to null.
Why D is incorrect: This is perfectly valid code and will not throw an exception.
Reinforcement: With reference types (classes), assignment copies the reference, not the object. Multiple variables can point to the same object.
5. Which of the following is a valid reason to use a private field with a public property instead of a public field?
Show answer
Correct: B
Why B is correct: A property can contain logic in its getter and setter. This lets you validate values, raise events, log changes, or compute derived values — all without changing the public API.
Why A is incorrect: Properties have a slight overhead compared to field access (though it's usually negligible). They don't make code run faster.
Why C is incorrect: Properties have no effect on garbage collection. Objects are collected when unreferenced.
Why D is incorrect: Properties do not automatically provide thread safety. You still need locks or other synchronization if multiple threads access the same object.
Reinforcement: Use properties to encapsulate access to fields — they give you a control point for validation, logging, and future changes.
You now have a solid understanding of classes and objects — the foundational building blocks of object-oriented programming in C#!