在C++基础上理解CSharp-4

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

常量的特点:

  1. 必须在声明时初始化
  2. 值必须是编译时常量(不能是运行时计算的值)
  3. 隐式为静态(不需要也不能加static关键字)
  4. 只能是基本类型、枚举、字符串或 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:带访问修饰符的属性

可以为getset访问器指定不同的访问修饰符:

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_PropertyNameset_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("派生类方法");
    }
}

重要提示:

  • 方法隐藏通常是设计失误,应该尽量避免
  • 如果你想实现多态,应该使用virtualoverride
  • 如果你确实想隐藏基类方法,应该使用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;
    }
}

构造函数的执行顺序:

  1. 先执行base构造函数(如果有)
  2. 然后执行this构造函数(如果有)
  3. 最后执行当前构造函数的主体

与 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++;
    }
}

静态构造函数的特点:

  1. 没有参数
  2. 没有访问修饰符(默认是 private)
  3. 不能手动调用,由 CLR 自动调用
  4. 只调用一次,在第一次访问类的任何静态成员或创建第一个实例之前调用
  5. 一个类只能有一个静态构造函数

C++ 对比:

  • C++ 没有静态构造函数,静态成员必须在类外部初始化
  • C++ 的静态成员初始化顺序是不确定的,而 C# 的静态构造函数执行顺序是确定的

4:私有化构造函数

私有构造函数是使用private关键字声明的构造函数,不能在类外部调用。私有构造函数通常用于:

  1. 防止类被实例化
  2. 实现单例模式
  3. 只包含静态成员的工具类
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;
        }
    }
}

终结器的特点:

  1. 没有参数
  2. 没有访问修饰符
  3. 不能手动调用,由垃圾回收器自动调用
  4. 执行时间不确定,可能在对象不再被引用后的任意时间执行
  5. 会延长对象的生命周期,影响性能

重要提示:

  • 终结器不是 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 更灵活,但需要开发者显式使用
相关推荐
曹牧2 小时前
C#:基类中定义泛型方法
java·开发语言·c#
游乐码2 小时前
c#基础(七)延迟函数
开发语言·unity·c#·游戏引擎
魔法阵维护师2 小时前
从零开发游戏需要学习的c#模块,第二十六章(多种敌人与基础 AI)
学习·游戏·c#
Brilliantwxx2 小时前
【算法题】 面试级别的二叉树题目OJ复习(上)
数据结构·c++·笔记·算法·面试
颖火虫盟主2 小时前
Conan C++ 包管理工具深度解析
java·jvm·c++
神仙别闹2 小时前
基于C++ OpenGL 绘制太阳系
开发语言·c++
froginwe112 小时前
Rust 数据类型
开发语言
LONGZETECH2 小时前
Unity 3D+C/S架构无人机数字孪生实训室:破解实训“三高”难题的底层技术实现
c语言·开发语言·3d·unity·架构·无人机