1:学习目标
- 理解 C# 纯面向对象的本质,掌握类与对象的核心概念
- 熟练掌握 C# 所有访问修饰符的用法和访问范围
- 深入理解字段、常量、只读字段的区别与应用场景
- 彻底掌握 C# 特有的属性语法,理解属性的本质
- 掌握实例方法、静态方法、虚方法、重写方法的定义与使用
- 深入理解 C# 的构造函数系统,掌握构造函数链的用法
- 理解 C# 的垃圾回收机制,掌握 IDisposable 接口与 using 语句
- 所有知识点均与 C++ 对应概念进行深度对比,消除认知误区
2:类与对象的基本概念
C#是纯面向对象的编程语言,这意味着所有代码都必须定义在类或结构体内部。C#没有全局函数,全局变量,全局常量,这是和C++最根本的区别之一。
1:类的定义
类是对象的模版,定义了对象将拥有的状态(字段)和行为(方法)
C#类定义语法
cs
[访问修饰符] class 类名
{
// 字段
// 属性
// 方法
// 构造函数
}
实例:定义一个Person类
cs
public class Person
{
// 字段
private string _name;
private int _age;
// 方法
public void SayHello()
{
Console.WriteLine($"你好,我是{_name},今年{_age}岁");
}
}
与C++类定义对比:
- 语法基本相似,但 C# 类默认的访问修饰符是
internal,而 C++ 类默认的访问修饰符是private - C# 类不能定义在函数内部,只能定义在命名空间或其他类内部
- C# 类不支持多继承(只能继承一个类,但可以实现多个接口)
2:对象的创建与使用
对象是类的实例,使用new关键字创建:
cs
// 创建Person类的实例
Person person = new Person();
// 访问对象的方法
person.SayHello();
与C++的关键区别:
- 在 C# 中,所有引用类型的对象都必须使用
new关键字在堆上创建 - 在 C++ 中,你可以选择将对象创建在栈上或堆上(使用
new) - C# 的对象变量本质上是一个引用(指针),而 C++ 的对象变量可以是对象本身或指针 / 引用
3:访问修饰符
访问修饰符用于控制类及其成员的可访问性。C# 提供了 5 种访问修饰符,比 C++ 多了internal相关的修饰符。
| 访问修饰符 | 可访问范围 | C++ 对应概念 |
|---|---|---|
public |
任何地方都可以访问 | public |
private |
只能在当前类内部访问 | private |
protected |
只能在当前类和派生类内部访问 | protected |
internal |
只能在同一个程序集(assembly)内部访问 | 无直接对应(类似 C++ 的友元,但范围是整个程序集) |
protected internal |
同一个程序集内部,或者其他程序集中的派生类 | 无直接对应 |
1:程序集
程序集是 C# 代码编译后的基本单元,通常是.dll或.exe文件。一个程序集可以包含多个类和命名空间。internal访问修饰符允许你将代码的可见性限制在同一个程序集内,这对于组织大型项目非常有用。
2:访问修饰符的使用
cs
public class MyClass
{
public int PublicField; // 任何地方都可以访问
private int _privateField; // 只能在MyClass内部访问
protected int _protectedField; // 只能在MyClass和派生类内部访问
internal int InternalField; // 只能在同一个程序集内部访问
protected internal int ProtectedInternalField; // 同一个程序集或派生类
}
重要提示:
- 类成员默认的访问修饰符是
private - 顶级类(不嵌套在其他类中的类)默认的访问修饰符是
internal - 嵌套类默认的访问修饰符是
private
4:字段
字段是类中存储数据的变量,对应C++的成员变量
1:实例字段与静态字段
- 实例字段:属于类的每个实例,每个对象都有自己的副本
- 静态字段 :属于类本身,所有对象共享同一个副本,使用
static关键字声明
cs
public class Person
{
// 实例字段
public string Name;
public int Age;
// 静态字段
public static string Species = "人类";
}
// 使用实例字段
Person person1 = new Person();
person1.Name = "张三";
person1.Age = 25;
Person person2 = new Person();
person2.Name = "李四";
person2.Age = 30;
// 使用静态字段
Console.WriteLine(Person.Species); // 输出 人类
与 C++ 的相同点:
- 语法和行为基本相同
- 静态字段必须在类外部初始化(C# 7.0 之前)
与 C++ 的不同点:
- C# 的静态字段可以在声明时直接初始化
- C# 的静态构造函数可以用于初始化静态字段
2:常量
常量是在编译时确定值,并且永远不能修改的字段,使用const关键字声明。
cs
public class MathConstants
{
public const double Pi = 3.141592653589793;
public const double E = 2.718281828459045;
}
// 使用常量
Console.WriteLine(MathConstants.Pi); // 输出 3.141592653589793
常量的特点:
- 必须在声明时初始化
- 值必须是编译时常量(不能是运行时计算的值)
- 隐式为静态(不需要也不能加
static关键字) - 只能是基本类型、枚举、字符串或 null
3:只读字段
只读字段是只能在声明时或构造函数中初始化,之后不能修改的字段,使用readonly关键字声明。
cs
public class Person
{
public readonly string IdCard;
public Person(string idCard)
{
IdCard = idCard; // 可以在构造函数中初始化
}
public void ChangeIdCard(string newId)
{
// IdCard = newId; // 编译错误:不能在构造函数之外修改readonly字段
}
}
4:const与readonly的区别
| 对比维度 | const | readonly |
|---|---|---|
| 初始化时机 | 编译时 | 运行时(声明时或构造函数中) |
| 值的类型 | 只能是编译时常量(基本类型、枚举、字符串) | 可以是任何类型,包括引用类型 |
| 静态性 | 隐式静态 | 可以是实例或静态 |
| 修改性 | 永远不能修改 | 只能在构造函数中修改一次 |
| 应用场景 | 永远不会改变的值(如数学常数) | 每个实例不同,但一旦创建就不会改变的值(如身份证号) |
C++ 对比:
- C++ 的
const更接近 C# 的readonly,因为 C++ 的const成员变量可以在构造函数初始化列表中初始化 - C++ 没有直接对应 C#
const的概念(编译时常量),C++11 引入的constexpr类似
5:属性
属性是 C# 特有的语法,是对字段的封装,提供了对私有字段的安全访问方式。属性本质上是两个方法:get访问器和set访问器。
1:传统属性写法
在C#3.0之前,属性需要手动定义backing field(backing 字段)和访问器
cs
public class Person
{
// 私有 backing 字段
private string _name;
private int _age;
// Name属性
public string Name
{
get
{
return _name;
}
set
{
// 可以在set访问器中添加验证逻辑
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("姓名不能为空");
}
_name = value;
}
}
// Age属性
public int Age
{
get
{
return _age;
}
set
{
if (value < 0 || value > 150)
{
throw new ArgumentException("年龄必须在0到150之间");
}
_age = value;
}
}
}
使用属性:
cs
Person person = new Person();
person.Name = "张三"; // 调用set访问器
Console.WriteLine(person.Name); // 调用get访问器
2:自动实现的属性
C# 3.0 引入了自动属性,编译器会自动生成 backing 字段和访问器,大大简化了代码:
cs
public class Person
{
// 自动属性,编译器自动生成_backing字段
public string Name { get; set; }
public int Age { get; set; }
}
这行代码等价于上面手动写的属性(除了没有验证逻辑)。当你不需要在访问器中添加额外逻辑时,应该优先使用自动属性。
3:属性的各种形式
1:只读属性
只有get访问器,没有set访问器:
cs
// 传统只读属性
public string FullName
{
get
{
return $"{FirstName} {LastName}";
}
}
// 自动只读属性(C# 6.0+)
public string FirstName { get; }
public string LastName { get; }
2:只写属性
只有set访问器,没有get访问器(很少使用):
cs
public string Password
{
set
{
_password = value;
}
}
3:带访问修饰符的属性
可以为get或set访问器指定不同的访问修饰符:
cs
// 公共get,私有set
public string Name { get; private set; }
// 公共get,受保护set
public int Age { get; protected set; }
这是非常实用的特性,允许你控制属性的读写权限。
4:属性初始化器(C#6.0)
可以在声明自动属性时直接初始化:
cs
public string Name { get; set; } = "未知";
public int Age { get; set; } = 0;
5:表达式体属性
对于简单的只读属性,可以使用表达式体语法进一步简化:
cs
// 传统写法
public string FullName
{
get
{
return $"{FirstName} {LastName}";
}
}
// 表达式体写法
public string FullName => $"{FirstName} {LastName}";
4:属性本质
属性本质上是编译器生成的两个方法:get_PropertyName和set_PropertyName。例如,下面的属性:
cs
public string Name { get; set; }
编译器会生成类似这样的代码:
cs
private string <Name>k__BackingField;
public string get_Name()
{
return <Name>k__BackingField;
}
public void set_Name(string value)
{
<Name>k__BackingField = value;
}
这就是为什么属性可以像字段一样使用,但本质上是方法。
与 C++ 的对比:
- C++ 没有属性语法,必须手动编写
GetXXX()和SetXXX()方法 - C# 的属性提供了更统一、更优雅的语法,提高了代码的可读性
6:方法
方法是类中定义的函数,用于实现类的行为。第三篇博客已经讲解了方法的基本定义和参数,这里重点讲解面向对象相关的方法特性。
1:实例方法和静态方法
- 实例方法:属于类的实例,必须通过对象调用,可以访问实例成员和静态成员
- 静态方法:属于类本身,通过类名调用,只能访问静态成员
cs
public class Person
{
public string Name { get; set; }
public static int PersonCount { get; set; }
// 实例方法
public void SayHello()
{
Console.WriteLine($"你好,我是{Name}");
Console.WriteLine($"总人数:{PersonCount}"); // 可以访问静态成员
}
// 静态方法
public static void PrintPersonCount()
{
Console.WriteLine($"总人数:{PersonCount}");
// Console.WriteLine(Name); // 编译错误:不能访问实例成员
}
}
2:虚方法(virtual)与重写方法(override)
虚方法是可以在派生类中被重写的方法,使用virtual关键字声明。重写方法使用override关键字声明。
cs
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("动物发出声音");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("汪汪汪");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("喵喵喵");
}
}
这就是面向对象的多态性:基类引用可以指向派生类对象,调用方法时会执行派生类的版本。
与 C++ 的相同点:
- 语法和行为基本相同
- 都实现了运行时多态
与 C++ 的不同点:
- C# 的方法默认是非虚的,必须显式声明为
virtual才能被重写 - C++ 的方法默认是非虚的,但如果基类有虚函数,派生类的同名函数会自动成为虚函数(即使不加
virtual) - C# 要求重写方法必须使用
override关键字,而 C++ 可以不加(但推荐加override)
3:密封方法(sealed)
密封方法是不能再被派生类重写的方法,使用sealed关键字声明。密封方法必须是重写方法。
cs
public class Dog : Animal
{
public sealed override void MakeSound()
{
Console.WriteLine("汪汪汪");
}
}
public class Husky : Dog
{
// 编译错误:不能重写密封方法
// public override void MakeSound()
// {
// Console.WriteLine("哈士奇叫");
// }
}
4:抽象方法(abstract)
抽象方法是没有是实现的方法,使用abstract关键字声明。包含抽象方法的类必须是抽象类。抽象方法必须在派生类中被是实现
cs
public abstract class Shape
{
public abstract double CalculateArea();
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double CalculateArea()
{
return Math.PI * Radius * Radius;
}
}
抽象方法和抽象类将在下一篇博客中详细讲解。
5:方法隐藏(new)(可以不用)
如果派生类定义了一个与基类同名但没有使用override关键字的方法,那么这个方法会隐藏基类的方法。方法隐藏是编译时多态,而不是运行时多态。
cs
public class BaseClass
{
public void Method()
{
Console.WriteLine("基类方法");
}
}
public class DerivedClass : BaseClass
{
// 隐藏基类方法
public new void Method()
{
Console.WriteLine("派生类方法");
}
}
重要提示:
- 方法隐藏通常是设计失误,应该尽量避免
- 如果你想实现多态,应该使用
virtual和override - 如果你确实想隐藏基类方法,应该使用
new关键字显式声明,否则编译器会发出警告
7:构造函数
构造函数是在创建对象时自动调用的特殊方法,用于初始化对象的状态。C# 的构造函数系统比 C++ 更强大、更灵活。
1:实例构造函数
实例构造函数用于初始化类的实例。
cs
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
// 无参数构造函数
public Person()
{
Name = "未知";
Age = 0;
}
// 带一个参数的构造函数
public Person(string name)
{
Name = name;
Age = 0;
}
// 带两个参数的构造函数
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
与 C++ 的相同点:
- 构造函数名与类名相同
- 没有返回值
- 可以重载
- 如果没有定义任何构造函数,编译器会自动生成一个默认的无参数构造函数
2:构造函数数链(Constructor Chaining)
C# 允许一个构造函数调用同一个类的另一个构造函数(使用this关键字),或者调用基类的构造函数(使用base关键字)。这可以避免代码重复。
cs
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Person() : this("未知", 0)
{
// 调用带两个参数的构造函数
}
public Person(string name) : this(name, 0)
{
// 调用带两个参数的构造函数
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
构造函数的执行顺序:
- 先执行
base构造函数(如果有) - 然后执行
this构造函数(如果有) - 最后执行当前构造函数的主体
与 C++ 的对比:
- C++11 之前不支持构造函数链,必须手动编写初始化代码
- C++11 引入了委托构造函数,功能类似 C# 的
this构造函数链 - C# 的构造函数链语法更简洁、更直观
3:静态构造函数
静态构造函数用于初始化类的静态成员,使用static关键字声明。
cs
public class Person
{
public static int PersonCount { get; set; }
// 静态构造函数
static Person()
{
PersonCount = 0;
Console.WriteLine("静态构造函数被调用");
}
public Person()
{
PersonCount++;
}
}
静态构造函数的特点:
- 没有参数
- 没有访问修饰符(默认是 private)
- 不能手动调用,由 CLR 自动调用
- 只调用一次,在第一次访问类的任何静态成员或创建第一个实例之前调用
- 一个类只能有一个静态构造函数
C++ 对比:
- C++ 没有静态构造函数,静态成员必须在类外部初始化
- C++ 的静态成员初始化顺序是不确定的,而 C# 的静态构造函数执行顺序是确定的
4:私有化构造函数
私有构造函数是使用private关键字声明的构造函数,不能在类外部调用。私有构造函数通常用于:
- 防止类被实例化
- 实现单例模式
- 只包含静态成员的工具类
cs
public class MathUtils
{
// 私有构造函数,防止类被实例化
private MathUtils()
{
}
public static int Add(int a, int b)
{
return a + b;
}
public static int Subtract(int a, int b)
{
return a - b;
}
}
8:析构函数与垃圾回收
C# 的内存管理与 C++ 有本质区别。C# 使用 ** 垃圾回收(Garbage Collection, GC)** 自动管理内存,不需要手动delete对象。
1:垃圾回收基本原理
CLR 的垃圾回收器会定期扫描堆内存,识别不再被引用的对象,并释放它们占用的内存。垃圾回收是自动进行的,不需要开发者干预。
与 C++ 的对比:
- C++ 需要手动管理内存,容易出现内存泄漏、野指针、重复释放等问题
- C# 的垃圾回收大大简化了内存管理,减少了内存相关的 bug
- 垃圾回收会带来一定的性能开销,但对于大多数应用来说可以忽略不计
2:终结器(Finalizer)
终结器是在对象被垃圾回收时自动调用的特殊方法,用于释放非托管资源(如文件句柄、数据库连接、网络连接等)。终结器使用~类名()语法声明。
cs
public class FileHandler
{
private IntPtr _fileHandle;
public FileHandler(string filePath)
{
// 打开文件,获取非托管句柄
_fileHandle = NativeMethods.OpenFile(filePath);
}
// 终结器
~FileHandler()
{
// 释放非托管资源
if (_fileHandle != IntPtr.Zero)
{
NativeMethods.CloseFile(_fileHandle);
_fileHandle = IntPtr.Zero;
}
}
}
终结器的特点:
- 没有参数
- 没有访问修饰符
- 不能手动调用,由垃圾回收器自动调用
- 执行时间不确定,可能在对象不再被引用后的任意时间执行
- 会延长对象的生命周期,影响性能
重要提示:
- 终结器不是 C++ 的析构函数,两者有本质区别
- C++ 的析构函数是确定性调用的(对象离开作用域时),而 C# 的终结器是不确定的
- 除非必须释放非托管资源,否则不要定义终结器
3:IDisposable接口与using语句
由于终结器的执行时间不确定,C# 提供了IDisposable接口来实现确定性的资源释放。
cs
public interface IDisposable
{
void Dispose();
}
实现IDisposable接口的类应该在Dispose方法中释放所有资源(托管和非托管)。
cs
public class FileHandler : IDisposable
{
private IntPtr _fileHandle;
private bool _disposed = false;
public FileHandler(string filePath)
{
_fileHandle = NativeMethods.OpenFile(filePath);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // 告诉垃圾回收器不需要调用终结器
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// 释放托管资源
}
// 释放非托管资源
if (_fileHandle != IntPtr.Zero)
{
NativeMethods.CloseFile(_fileHandle);
_fileHandle = IntPtr.Zero;
}
_disposed = true;
}
~FileHandler()
{
Dispose(false);
}
}
using 语句 提供了一种简洁的方式来使用实现了IDisposable接口的对象,确保Dispose方法在对象使用完后自动调用:
cs
using (FileHandler handler = new FileHandler("test.txt"))
{
// 使用handler
} // 离开using块时,handler.Dispose()会自动调用
这等价于:
cs
FileHandler handler = new FileHandler("test.txt");
try
{
// 使用handler
}
finally
{
if (handler != null)
{
((IDisposable)handler).Dispose();
}
}
C++ 对比:
- C++ 的 RAII(Resource Acquisition Is Initialization)机制与 C# 的
IDisposable+using类似 - C++ 的析构函数是确定性调用的,而 C# 的
Dispose方法需要手动调用或使用using语句 - C# 的
using语句比 C++ 的 RAII 更灵活,但需要开发者显式使用