Last Updated: May 17, 2026
A class is the blueprint that describes a kind of thing in your program. An object is an actual thing built from that blueprint, sitting in memory with its own data. Almost every line of C# you write either defines a class or works with an object built from one. This chapter covers what classes and objects are, how to declare a class, how to create and use objects, and how the runtime stores them.
Before the syntax, the reason. Take a small program that tracks two products in a store, written without classes:
Three variables for the first product, three for the second. Adding a third product means three more variables. Adding a fourth field (say, a category) means rewriting every block. The variables for a single product aren't really three unrelated things, they're three pieces of one thing, but C# has no way to know that from the code above.
A class fixes that. It bundles the related fields together under a single type and gives that type a name:
Same output, very different shape. Product is now a type the compiler understands. notebook and pen are two separate objects, each carrying its own Name, Price, and Stock. Adding a third product is one more new Product(). Adding a Category field means one new line in the class, and every existing Product gets it automatically.
That captures the point of classes:
Product belongs together. The class makes that grouping explicit instead of relying on naming conventions like product1Price.class Product, you can use Product anywhere C# expects a type: as a parameter, a return value, an array element. The compiler enforces it.A class can also hold methods, not just data. That's how a Cart knows how to add items, or an Order knows how to compute its total. We'll add methods to classes later in this lesson, but the headline is: a class packages data and the operations on that data into a single unit.
These two words get confused often, so it's worth nailing the difference up front.
A class is a definition. It says "here is a kind of thing called Product, and any Product has a Name, a Price, and a Stock." A class doesn't hold data by itself, it only describes the shape.
An object is an instance of a class. It's a real thing in memory, with real values in those fields. notebook is an object. pen is a different object. They share the same class, Product, but each carries its own data.
The relationship looks like this:
One blueprint, many objects. You write the class once. You can create as many objects from it as your program needs, and each carries its own state.
A useful analogy: a class is like the printed form for a customer record. An object is a single filled-in form. Two filled forms have the same fields (name, email, address) but different answers in them. The blank form has no "data," it just dictates what data goes where.
Two more terms that show up in C# documentation and conversation:
Product" means "create a Product object." The verb is instantiate.The minimum class declaration looks like this:
The keyword class, the type name, and a pair of braces for the body. The body is empty here, which is legal but not useful. To make the class do anything, add members: fields, methods, and (in later chapters) properties, constructors, and more.
A field is a variable that belongs to the class. Each object built from the class gets its own copy of every field:
Three fields, each with a type and a name. The public keyword (an access modifier) means code outside the class can read and write these fields. The other common modifier is private, which keeps a field hidden inside the class. For now, treat public as the keyword that lets your test programs poke at the fields directly. In real code you'd usually expose fields through properties instead, but starting with public fields keeps the examples short while we focus on the class-and-object model.
A method inside a class is declared the same way you saw in the Methods section, except now it lives between the class braces:
A few things to notice. The method has no static keyword, because it operates on a specific Product object, the one the caller called it on. Inside PrintSummary, the names Name, Price, and Stock refer to that object's fields. There is no parameter named product; the object is implicit. The this keyword is the way to refer to "the current object" explicitly, but you usually don't need it.
By C# convention:
Product, ShoppingCart, OrderStatus.public use PascalCase. Name, Price, Stock.private use camelCase, often with a leading underscore. _count, _owner.PrintSummary, AddItem, Cancel.A class can have any number of fields and methods, including zero of each. The order inside the body doesn't matter to the compiler, though most codebases put fields first, then constructors, then methods.
A small but important rule: a .cs file usually contains one class, named after the file. Product goes in Product.cs. You can put multiple classes in one file (the compiler allows it), but most teams discourage it because it makes navigation harder. The exception is small helper classes used in only one place.
newA class on its own doesn't make any object appear. To create an object, use the new keyword followed by the class name and parentheses:
Reading this left to right: declare a variable named notebook of type Product, and assign it a brand-new Product object. The new Product() part is the object creation expression. It allocates a fresh chunk of memory for a Product, initializes its fields to default values, and gives you back a reference to it.
The defaults follow C#'s rules for each type:
| Field type | Default value |
|---|---|
int, long, decimal, double | 0 (or 0m, 0.0) |
bool | false |
string and other reference types | null |
| Other value types | All-zero bits (the type's "zero value") |
So immediately after new Product(), the object has Name = null, Price = 0, and Stock = 0. You then fill the fields in by name:
The dot operator (notebook.Name) is how you reach into an object to read or write one of its members. It works for both fields and methods: notebook.Name accesses the field, and notebook.PrintSummary() calls the method.
C# has a few syntactic shortcuts for the same line. All four of these compile to equivalent code:
The first form is explicit on both sides. The second uses var to infer the type. The third is target-typed new (added in C# 9), which works when the type is already known from the left side. The fourth combines target-typed new with an object initializer, setting fields in one expression. Pick whichever reads best in context. Most modern C# code uses var or target-typed new to avoid repeating the type name.
Calling a method on an object uses the same dot syntax:
Inside PrintSummary, the names Name, Price, and Stock refer to the fields of whichever object the caller called the method on. Here it's notebook, so the values printed are notebook's.
A class without any methods is sometimes called a data class or plain old object. Adding methods turns it into a richer abstraction that knows how to operate on its own data. The shopping cart below shows a small version of that idea:
The Cart object holds its own state (Owner, Total, ItemCount), and its methods (AddItem, Clear) operate on that state. The caller doesn't manipulate Total and ItemCount directly; it calls AddItem, and the cart updates its own fields. That's the everyday shape of object-oriented code.
Cost: Every new SomeClass() allocates memory on the managed heap and adds work for the garbage collector later. Creating one object is trivial. Creating millions of small short-lived objects in a tight loop is where you'd start watching for structs or pooling. We'll come back to this in the memory management section.
The whole point of having a class is that you can build many objects from it, and each object carries its own values. Changing one object doesn't touch the others.
Selling five notebooks subtracted five from notebook.Stock only. The pen and the mug are untouched. Each object has its own copy of every field.
The same applies when methods change fields. A method modifies the fields of the specific object it was called on, not the fields of every object built from the class:
Three calls to AddItem, on two different carts. Alice's cart adds two items, Bob's cart adds one. The two totals are independent because they live on different objects.
This is the property that makes objects useful: each one keeps track of its own state, and you can have as many of them as you need. A web server might have one Cart object per shopper, all using the same Cart class. A game might have one Enemy object per monster on screen. They share the blueprint, not the data.
Classes in C# are reference types. That single fact explains a lot of behavior that surprises beginners. To understand it, you need a rough picture of where memory lives.
C# splits memory into two regions for our purposes:
int, bool, decimal, structs) live here, along with parameters and return addresses.When you write Product notebook = new Product();, two things happen:
Product on the heap, with Name, Price, and Stock set to their defaults.notebook on the stack holds a reference, a small value (effectively a pointer) that points to that heap object.The picture:
The variable notebook is not the object. It's an arrow pointing at the object. This sounds like a pedantic distinction until it changes how your code behaves.
Here's the first consequence. When you assign one object variable to another, you copy the arrow, not the object:
Both variables print 9.99 because they're two arrows pointing at the same heap object. Modifying through one arrow is visible through the other. There is exactly one Product in memory.
The picture after the assignment:
Contrast that with value types. If Product were a struct, var second = first would copy all the fields, and the two variables would be independent. Classes don't work that way. They share.
The second consequence: comparing two object variables with == (by default) checks whether the two references point to the same object, not whether the fields are equal:
a and b are different heap objects that happen to have the same Name. The reference comparison returns False. c is the same reference as a, so a == c returns True. Comparing by content (the way you'd want string.Equals to compare) requires either overriding equality or using a record. For plain classes, == is reference equality.
The third consequence: passing an object to a method passes the reference. The method receives its own copy of the reference, but that reference still points at the same heap object. So the method can mutate the object, and the caller sees the changes:
Sell got its own local variable named product, but that variable was a copy of the same reference. Subtracting from product.Stock reached through the reference and changed the heap object. After the method returns, notebook.Stock reflects the change.
This is one of the most important behaviors to internalize. With reference types, what looks like "pass by value" (the method gets a copy of the variable) is actually "pass by reference" in effect, because the value being copied is itself a reference. We'll see this contrast more when we compare to struct in the Structs & Enums section.
null and What Happens When You Call a Method on ItA reference variable doesn't have to point at an object. It can also point at nothing, which C# represents with the keyword null. A null reference is a reference that doesn't lead to any object on the heap.
The ? after Product is a nullable reference type annotation, introduced in C# 8. It tells the compiler "this variable can hold null." Without the ?, modern C# projects (which usually have nullable reference types enabled by default) warn when you try to assign null. The annotation is a hint to the type system, not a runtime change; at runtime, any reference can still be null.
A null reference doesn't take up space for an object, it just means "no object." A picture:
The danger is that null behaves like any other reference until you try to use it. If you try to read a field or call a method through a null reference, the runtime throws a NullReferenceException:
What's wrong with this code?
The variable notebook is null. Trying to follow it to an object's Name field is asking for the field of an object that isn't there. The runtime can't do that, so it throws NullReferenceException. The error message in .NET is System.NullReferenceException: Object reference not set to an instance of an object.
Fix (check before use):
The is not null check guards the access. Inside the if, the compiler also knows notebook can't be null, so it stops warning about the field access.
The shorter alternative is the null-conditional operator ?., which says "evaluate the right side only if the left side is not null; otherwise produce null":
notebook?.Name is null because notebook is null. The ?? operator (the null-coalescing operator) then substitutes (no product). No exception, no crash.
Modern C# encourages you to design code so that variables are non-null whenever possible. Enabling nullable reference types in the project (the default for new projects since .NET 6) turns the compiler into an ally: it warns when you might be reading or calling through a null. Treat those warnings as errors in your own code and you'll dodge most of these bugs.
A subtle point. Setting a reference variable to null doesn't delete the object from the heap. It just makes that variable stop pointing at it. The garbage collector eventually frees objects that no variable references anymore, but only when nothing reaches them:
That's the garbage collector's job, and it happens behind the scenes. The Memory Management section explains the details. For now, the rule is: a null reference is "no object," and using it for member access throws.
Here is what can live inside a class body:
| Member | What it is |
|---|---|
| Field | A variable that belongs to the class or to each instance |
| Method | A named block of code that operates on the object |
| Constructor | A special method that runs when an object is created |
| Property | A getter/setter that looks like a field but runs code |
| Static member | A member that belongs to the class itself, not to instances |
| Constant / readonly field | A value that can't change after init |
| Nested class | A class declared inside another class |
| Indexer, event, finalizer | Specialized members |
You don't need to use most of these to get value out of a class. The minimum useful class has a few fields and a few methods. Constructors and properties are almost always better than the public fields used in this lesson's examples. The fields are scaffolding for teaching, not the final shape of production code.
For now, the shape to remember is this. A class declaration introduces a new type. Its body lists the data each object will carry (fields) and the operations each object can perform (methods). You create objects with new, reach into them with ., and pass them around as references. Everything else in this section is a refinement of that core idea.
new ClassName(). Access its members with the dot operator (obj.Field, obj.Method()). Modern C# offers var, target-typed new, and object initializers as shorter equivalents.== on class references compares references, not field values. Two objects with the same data are not equal by == unless one of them is the same reference as the other.null, meaning "points at no object." Reading a field or calling a method through a null reference throws NullReferenceException at runtime. Use if (x is not null), the null-conditional operator ?., or the null-coalescing operator ?? to handle null safely.