任务五 构造函数
一、构造函数的由来
我们先从一个熟悉的场景说起。假设我们定义了一个"学生类",包含姓名、年龄这些属性:
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public string StudentId { get; set; }
}
当我们创建学生对象时,通常会这样写:
Student student = new Student();
student.Name = "张三";
student.Age = 18;
student.StudentId = "2023001";
这段代码没问题,但如果我们每次创建学生对象都要重复这几步赋值操作,不仅麻烦,还可能因为忘记给某个重要属性赋值而导致错误。比如,如果忘记设置学号,这个学生对象就是不完整的。
有没有办法在创建对象的同时,就完成这些必要的初始化工作呢?当然有,这就是构造函数的作用!
二、构造函数是什么
在C#中,构造函数是一种特殊的方法,专门用于对象的初始化(如设置初始值、分配资源等)。它的名字和类名完全相同,而且没有返回值。当我们用new关键字创建对象时,构造函数会自动执行,帮我们完成对象的初始化工作。
👉 定义:构造函数是一种与类同名的特殊方法,在创建对象时自动执行,用于初始化对象的状态。
- 返回值:没有返回值(连
void都不能写)。 - 作用:
- ✅ 给对象的字段赋初始值
- ✅ 执行一些初始化逻辑(比如显示欢迎信息、加载数据)
- ✅ 确保对象在使用前处于“正确状态”在创建对象时自动执行,完成对象的初始化工作(如给字段/属性赋初始值、建立数据库连接等)。
- 触发时机:每次用
new关键字创建类的实例时,都会调用对应的构造函数。
示例:
class Cat
{
public string Name;
public int Age;
// 构造函数
public Cat()
{
Name = "未命名";
Age = 1;
Console.WriteLine("一只小猫诞生了!");
}
}
class Program
{
static void Main()
{
Cat cat1 = new Cat(); // 构造函数自动执行
Console.WriteLine($"猫的名字:{cat1.Name},年龄:{cat1.Age}");
}
}
输出:
一只小猫诞生了!
猫的名字:未命名,年龄:1
💡 解释: 当执行 new Cat() 时,C# 自动调用 Cat() 这个构造函数,用于“初始化”对象。
我们来给Student类添加一个构造函数:
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public string StudentId { get; set; }
// 构造函数
public Student(string name, int age, string studentId)
{
Name = name;
Age = age;
StudentId = studentId;
}
}
现在,当我们创建学生对象时,就可以在创建的同时完成赋值:
Student student = new Student("张三", 18, "2023001");
这样是不是简洁多了?而且通过构造函数,我们可以强制要求创建对象时必须提供必要的信息,避免了忘记赋值的问题。
构造函数的语法特点**
| 特点 | 说明 |
|---|---|
| 名字 | 必须与类名相同(区分大小写) |
| 没有返回类型 | 不写 void、int 等 |
| 自动执行 | 用 new 创建对象时会自动调用 |
| 可重载 | 可以有多个构造函数(参数不同) |
三、默认构造函数
可能有同学会问,如果我没有手动定义构造函数,之前的代码为什么还能正常工作呢?那是因为当一个类中没有定义任何构造函数时,C#编译器会自动为我们生成一个无参数的默认构造函数。它什么都不做,但保证对象能被正常创建。就像这样:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
// 编译器自动生成的默认构造函数(隐式存在)
// public Student() { }
}
// 使用默认构造函数创建对象
Student stu = new Student(); // 调用默认构造函数
stu.Name = "张三";
stu.Age = 18;
四、自定义构造函数
当需要在创建对象时就初始化数据(如强制设置姓名、年龄),可以显式定义构造函数。
重载原则:构造函数名必须与类名相同。
1. 无参数构造函数(手动定义)
如果手动定义了构造函数,编译器就不会再生成默认构造函数。如需保留无参构造,需手动声明:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
// 手动定义无参数构造函数
public Student()
{
// 初始化逻辑:如设置默认值
Name = "未知姓名";
Age = 0;
}
}
// 创建对象时自动调用无参构造函数
Student stu = new Student();
Console.WriteLine(stu.Name); // 输出:未知姓名(构造函数中设置的默认值)
2. 带参数的构造函数
最常用的构造函数形式,用于在创建对象时直接传入初始值:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
// 带参数的构造函数:强制传入姓名和年龄
public Student(string name, int age)
{
// 用参数初始化属性
Name = name;
Age = age;
}
}
// 创建对象时必须传入参数(与构造函数匹配)
Student stu = new Student("张三", 18); // 调用带参构造函数
Console.WriteLine($"{stu.Name},{stu.Age}岁"); // 输出:张三,18岁
优势:确保对象创建时就有有效的初始值,避免“未初始化就使用”的问题。
3.修饰符
构造函数还可以和访问修饰符配合使用。比如,我们可以把构造函数设为private,这样就只能在类内部创建对象,这在一些特殊设计模式中非常有用。
class Singleton
{
// 私有构造函数
private Singleton()
{
}
// 在类内部创建唯一实例
public static Singleton Instance = new Singleton();
}
五、什么时候可以不写构造函数
这个问题核心要理解C#的默认构造函数机制——编译器会自动为类生成无参构造函数,满足基础初始化需求时,就可以不手动写。
核心结论
当你的类满足以下任一条件时,可以不手动编写构造函数:
- 类的所有属性/字段只需要使用C#默认值(如
string默认null、int默认0、引用类型默认null),无需自定义初始化逻辑; - 类仅作为“数据容器”(如简单的DTO类),属性通过对象初始化器(
new Class { Prop = Value })赋值,无需提前初始化; - 类是静态类(
static class),无法实例化,因此不需要构造函数。
分场景详解(附代码示例)
场景1:类的成员使用默认值即可(最常见)
C#编译器会为没有任何手动构造函数的类,自动生成一个无参的默认构造函数,这个自动生成的构造函数会把所有成员初始化为C#默认值:
- 值类型:
int→0、bool→false、double→0.0; - 引用类型:
string→null、自定义类→null。
using System;
// 不写构造函数,编译器自动生成默认无参构造函数
class Person
{
// 自动属性,无手动初始化
public string Name { get; set; }
public int Age { get; set; }
public bool IsStudent { get; set; }
}
class Program
{
static void Main()
{
// 使用编译器自动生成的构造函数实例化
Person p = new Person();
// 输出默认值:null、0、false
Console.WriteLine(p.Name); // null
Console.WriteLine(p.Age); // 0
Console.WriteLine(p.IsStudent);// false
}
}
这种场景下,因为只需要默认值,完全不需要手动写构造函数。
场景2:通过对象初始化器赋值(替代构造函数)
即使成员需要自定义值,但如果通过对象初始化器({ Prop = Value })赋值,也可以不写构造函数,这是C#简化初始化的常用方式:
using System;
// 无手动构造函数
class Book
{
public string Title { get; set; }
public double Price { get; set; }
}
class Program
{
static void Main()
{
// 用对象初始化器赋值,替代构造函数的初始化逻辑
Book b = new Book
{
Title = "C#入门",
Price = 59.9
};
Console.WriteLine(b.Title); // 输出:C#入门
Console.WriteLine(b.Price); // 输出:59.9
}
}
这种场景下,初始化逻辑由外部赋值完成,类内部无需写构造函数。
场景3:静态类(无需实例化)
静态类(static class)的所有成员都是静态的,无法被实例化(不能new 静态类()),因此完全不需要构造函数,写了反而会编译报错:
using System;
// 静态类,不写构造函数(也不能写)
static class Tool
{
// 静态方法,直接通过类名调用
public static int Add(int a, int b)
{
return a + b;
}
}
class Program
{
static void Main()
{
// 直接调用静态方法,无需实例化
int result = Tool.Add(1, 2);
Console.WriteLine(result); // 输出:3
}
}
反例:这些情况必须手动写构造函数
如果不满足上述条件,就必须手动写构造函数,否则无法实现需求:
- 需要自定义成员的初始值(如之前
Animal类要把Name初始化为“未命名”,Age初始化为3); - 需要带参数的构造函数(如
public Person(string name) { Name = name; }); - 类有继承关系,子类需要调用父类的带参构造函数(
base(参数)); - 实例化时需要执行额外逻辑(如初始化数据库连接、读取配置)。
总结
- 可以不写构造函数的核心条件:类无需自定义初始化逻辑,成员用默认值或通过对象初始化器赋值,或类是静态类;
- 编译器自动生成的构造函数:仅在类无任何手动构造函数时生效,且只有无参版本;
- 关键提醒:如果手动写了任意构造函数(哪怕是带参的),编译器就不会再生成默认无参构造函数,此时如果需要无参初始化,必须手动补充。
简单记:只要不需要“提前给成员赋非默认值”“执行初始化逻辑”,就可以不写构造函数,让编译器帮你生成。
六、构造函数的重载
和普通方法一样,构造函数也支持重载(同一个类中定义多个同名构造函数,通过参数的数量、类型或顺序区分),满足不同的初始化需求:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
public string Major { get; set; }
// 构造函数1:无参数(设置默认值)
public Student()
{
Name = "未知";
Age = 0;
Major = "未分配";
}
// 构造函数2:仅传入姓名
public Student(string name)
{
Name = name;
Age = 0;
Major = "未分配";
}
// 构造函数3:传入姓名、年龄、专业
public Student(string name, int age, string major)
{
Name = name;
Age = age;
Major = major;
}
}
// 调用不同的构造函数
Student stu1 = new Student(); // 用构造函数1
Student stu2 = new Student("李四"); // 用构造函数2
Student stu3 = new Student("王五", 20, "计算机"); // 用构造函数3
重载原则:构造函数名必须与类名相同,参数列表必须不同(数量、类型、顺序至少有一个不同)。
但是,一旦我们手动定义了构造函数,这个默认的无参数构造函数就会消失。如果还想像以前那样创建对象,就需要手动添加一个无参数构造函数:
class Student
{
// 其他成员...
// 无参数构造函数
public Student()
{
// 可以在这里设置一些默认值
StudentId = "未知";
}
// 带参数的构造函数
public Student(string name, int age, string studentId)
{
Name = name;
Age = age;
StudentId = studentId;
}
}
这种一个类中定义多个构造函数的情况,叫做构造函数的重载,它体现了面向对象的多态特性。
七、构造函数的使用场景
-
强制初始化:确保对象创建时必须提供关键信息(如
Id、Name等)。
例:User类必须传入用户名才能创建对象,避免无意义的空对象。 -
设置默认值:为属性设置合理的初始值(如年龄默认为0,状态默认为“正常”)。
-
资源初始化:在创建对象时分配资源(如打开文件流、建立数据库连接)。
public class FileHandler
{
private FileStream _stream;
// 构造函数中打开文件
public FileHandler(string filePath)
{
_stream = File.Open(filePath, FileMode.Open);
}
}
八、总结
核心要点总结
- 构造函数与类同名,无返回值,用于对象初始化。
- 未手动定义时,编译器自动生成无参默认构造函数。
- 支持重载,可通过
this关键字复用初始化逻辑。 - 每次用
new创建对象时,必然调用对应的构造函数。
掌握构造函数是面向对象编程的基础,它能保证对象在使用前处于“就绪”状态,是封装思想的重要体现。
总结一下,构造函数是类中用于初始化对象的特殊方法,它具有以下特点:
- 构造函数的名字和类名相同,没有返回值
- 创建对象时会自动调用
- 主要作用是初始化对象的属性和状态
- 可以重载,实现多种初始化方式
- 可以使用访问修饰符控制访问权限
掌握构造函数的使用,能让我们的代码更加简洁、安全,也更符合面向对象的设计思想。下节课,我们将继续学习类的其他特性,大家课后可以尝试给之前定义的类添加构造函数,体验一下它的便利之处。
| 关键词 | 含义 |
|---|---|
| 构造函数 | 创建对象时自动执行的方法 |
| 名称 | 与类名相同,没有返回值 |
| 作用 | 初始化对象 |
| 类型 | 无参构造、带参构造 |
| 特点 | 可以重载、可用 this() 互相调用 |
练习题
第1题:最简单的构造函数
要求: 创建一个名为 Student 的类,定义一个无参数构造函数,在构造函数中输出 “一个学生对象被创建!”。
目标: 理解构造函数的基本定义与自动调用特性。
第2题:带参数的构造函数
要求: 在 Student 类中添加 name 和 age 字段,通过构造函数为它们赋值,并在控制台输出它们的值。
目标: 掌握带参数构造函数的写法和使用方法。
第3题:多个对象实例
要求: 使用上题的 Student 类,创建 3 个学生对象,分别传入不同的姓名和年龄,观察输出结果。
目标: 理解不同实例拥有各自独立的数据。
第4题:字段与属性结合
要求: 将 name 和 age 改为属性(Name、Age),并在构造函数中通过属性赋值。
目标: 理解构造函数与属性的配合使用。
第5题:默认值构造函数
要求: 创建一个 Book 类,包含 Title 和 Price 两个属性。 定义一个无参构造函数,为其设置默认值(例如:“未命名”, 0)。
目标: 掌握构造函数初始化默认值的技巧。
第6题:构造函数重载
要求: 创建 Car 类:
- 一个无参构造函数(输出“默认构造函数被调用”)
- 一个带参数构造函数(设置车名和价格)
创建两个对象,分别调用这两个构造函数。
目标: 理解构造函数的重载机制。
第7题:在构造函数中调用方法
要求: 创建一个 Dog 类,包含 name 字段。 构造函数中为 name 赋值并调用 Bark() 方法(输出“汪汪!”)。
目标: 理解构造函数中可执行初始化逻辑或方法调用。
第8题:this关键字的使用
要求: 创建一个 Person 类,带参数构造函数中使用 this 关键字区分同名的字段与参数。
目标: 掌握 this 关键字在构造函数中的作用。
第9题:构造函数之间的相互调用
要求: 创建一个 Rectangle 类,包含:
- 一个无参构造函数(默认长宽为 1)
- 一个带两个参数的构造函数 无参构造函数调用带参构造函数。
目标: 学习使用 this(...) 调用其他构造函数。
第10题:构造函数与对象初始化器对比
要求: 创建 Phone 类,包含品牌和价格两个属性。 使用构造函数创建一个对象,再使用对象初始化器 {} 创建另一个对象,对比两者区别。
目标: 理解构造函数与对象初始化器在对象创建时的不同用法。
练习题答案
第1题:最简单的构造函数
using System;
class Student
{
public Student()
{
Console.WriteLine("一个学生对象被创建!");
}
}
class Program
{
static void Main()
{
Student stu1 = new Student();
}
}
运行结果:
一个学生对象被创建!
要点: 构造函数名称与类名相同,无返回类型,在创建对象时自动执行。
第2题:带参数的构造函数
using System;
class Student
{
public string name;
public int age;
public Student(string n, int a)
{
name = n;
age = a;
Console.WriteLine($"学生姓名:{name},年龄:{age}");
}
}
class Program
{
static void Main()
{
Student stu1 = new Student("小明", 18);
}
}
运行结果:
学生姓名:小明,年龄:18
要点: 带参数的构造函数用于在创建对象时初始化字段或属性。
第3题:多个对象实例
using System;
class Student
{
public string name;
public int age;
public Student(string n, int a)
{
name = n;
age = a;
Console.WriteLine($"学生姓名:{name},年龄:{age}");
}
}
class Program
{
static void Main()
{
Student s1 = new Student("小明", 18);
Student s2 = new Student("小红", 19);
Student s3 = new Student("小刚", 20);
}
}
运行结果:
学生姓名:小明,年龄:18
学生姓名:小红,年龄:19
学生姓名:小刚,年龄:20
要点: 每个对象都有自己独立的数据空间,构造函数在每次创建对象时都会执行。
第4题:字段与属性结合
using System;
class Student
{
public string Name { get; set; }
public int Age { get; set; }
public Student(string name, int age)
{
Name = name;
Age = age;
Console.WriteLine($"姓名:{Name}, 年龄:{Age}");
}
}
class Program
{
static void Main()
{
Student stu = new Student("小李", 17);
}
}
运行结果:
姓名:小李, 年龄:17
要点: 构造函数可以通过属性来初始化对象的公开数据。
第5题:默认值构造函数
using System;
class Book
{
public string Title { get; set; }
public double Price { get; set; }
public Book()
{
Title = "未命名";
Price = 0;
}
}
class Program
{
static void Main()
{
Book b = new Book();
Console.WriteLine($"书名:{b.Title},价格:{b.Price}");
}
}
运行结果:
书名:未命名,价格:0
要点: 无参构造函数可以为对象设置默认状态。
第6题:构造函数重载
using System;
class Car
{
public string Name { get; set; }
public double Price { get; set; }
public Car()
{
Console.WriteLine("默认构造函数被调用");
}
public Car(string name, double price)
{
Name = name;
Price = price;
Console.WriteLine($"车型:{Name},价格:{Price}");
}
}
class Program
{
static void Main()
{
Car c1 = new Car();
Car c2 = new Car("特斯拉", 299999);
}
}
运行结果:
默认构造函数被调用
车型:特斯拉,价格:299999
要点: 构造函数可以重载,根据参数不同选择不同版本。
第7题:构造函数中调用方法
using System;
class Dog
{
public string name;
public Dog(string n)
{
name = n;
Bark();
}
void Bark()
{
Console.WriteLine($"{name}:汪汪!");
}
}
class Program
{
static void Main()
{
Dog d = new Dog("旺财");
}
}
运行结果:
旺财:汪汪!
要点: 构造函数中可以调用类的其他方法,完成初始化逻辑。
第8题:this关键字的使用
using System;
class Person
{
private string name;
public Person(string name)
{
this.name = name; // 使用this区分字段和参数
}
public void Show()
{
Console.WriteLine($"姓名:{name}");
}
}
class Program
{
static void Main()
{
Person p = new Person("小华");
p.Show();
}
}
运行结果:
姓名:小华
要点: this 用于区分当前对象的字段和同名的构造函数参数。
第9题:构造函数之间的相互调用
using System;
class Rectangle
{
public int Width { get; set; }
public int Height { get; set; }
public Rectangle() : this(1, 1)
{
Console.WriteLine("无参构造函数被调用");
}
public Rectangle(int width, int height)
{
Width = width;
Height = height;
Console.WriteLine($"带参构造函数被调用:{Width}x{Height}");
}
}
class Program
{
static void Main()
{
Rectangle r1 = new Rectangle();
}
}
运行结果:
带参构造函数被调用:1x1
无参构造函数被调用
要点: 可以使用 this(...) 调用同类中的其他构造函数,避免重复代码。
第10题:构造函数与对象初始化器对比
using System;
class Phone
{
public string Brand { get; set; }
public double Price { get; set; }
public Phone(string brand, double price)
{
Brand = brand;
Price = price;
}
public Phone() { } // 空构造函数
}
class Program
{
static void Main()
{
// 使用构造函数
Phone p1 = new Phone("iPhone", 6999);
Console.WriteLine($"构造函数创建:{p1.Brand}, {p1.Price}");
// 使用对象初始化器
Phone p2 = new Phone { Brand = "小米", Price = 2999 };
Console.WriteLine($"对象初始化器创建:{p2.Brand}, {p2.Price}");
}
}
运行结果:
构造函数创建:iPhone, 6999
对象初始化器创建:小米, 2999
要点: 对象初始化器 {} 适合简化赋值操作;构造函数适合执行逻辑初始化。
完全理解你的感受!很多初学者练习题要么太简单(比如只让写一个构造函数打印一句话),要么脱离实际。下面给你3道循序渐进、真正锻炼思维的构造函数练习题,每道题都有明确的输入输出示例,并且覆盖了笔试面试中的高频考点。
练习题 1:图书类(基础:构造函数重载 + 默认值)
题目描述
创建一个 Book 类,包含三个私有字段:
title(标题,字符串)author(作者,字符串)price(价格,浮点数)
要求:
- 编写两个构造函数:
- 无参构造函数:标题设为
"未知书名",作者设为"佚名",价格设为0.0 - 带参构造函数:接收标题、作者、价格三个参数,用它们初始化字段。
- 无参构造函数:标题设为
- 编写一个
ShowInfo()方法,输出书的信息,格式如:《红楼梦》曹雪芹 ¥59.8 - 在
Main中分别使用两个构造函数创建对象,并调用ShowInfo()。
示例输出:
《未知书名》佚名 ¥0.0
《三体》刘慈欣 ¥68.0
考察点:
- 构造函数重载
this的隐式使用(区分字段和参数)- 默认值的设置
练习题 2:学生类(中级:this 调用 + 参数校验)
题目描述
创建一个 Student 类,包含:
name(姓名,string)age(年龄,int,要求 0~120)score(成绩,double,0~100)
要求:
- 提供三个构造函数:
- 无参构造函数:调用 “只传姓名” 的构造函数,姓名设为
"新生"。 - 构造函数
Student(string name):调用Student(name, 18, 0)(年龄默认18,成绩默认0)。 - 构造函数
Student(string name, int age, double score):直接初始化所有字段。在内部必须校验年龄和成绩的有效性(无效则 age 设为18,score 设为0)。
- 无参构造函数:调用 “只传姓名” 的构造函数,姓名设为
- 编写
Show()方法输出学生信息。 - 在
Main中测试以下三种创建方式:new Student()new Student("李明")new Student("王芳", 25, 95.5)- 故意给非法年龄(比如
new Student("坏蛋", 200, 80)),观察输出。
示例输出:
姓名:新生, 年龄:18, 成绩:0
姓名:李明, 年龄:18, 成绩:0
姓名:王芳, 年龄:25, 成绩:95.5
姓名:坏蛋, 年龄:18, 成绩:80 (年龄被修正)
考察点:
- 构造函数链(
this(...)调用) - 参数校验与防御性编程
- 理解构造函数的执行顺序
练习题 3:计数器类(进阶:静态构造函数 + 复制构造函数)
题目描述
实现一个 Counter 类,用于记录计数器的当前值,并附加一个全局最大限制。
要求:
- 实例字段:
_currentValue(int,当前计数)。 - 静态只读字段:
MaxLimit(int,所有计数器允许的最大值)。- 该值通过静态构造函数初始化:从配置读取(为了模拟,直接写死在静态构造函数中为
100),且不可更改。
- 该值通过静态构造函数初始化:从配置读取(为了模拟,直接写死在静态构造函数中为
- 实例构造函数:
Counter():将当前值初始化为0。Counter(int initialValue):如果initialValue > MaxLimit则抛出ArgumentOutOfRangeException,否则赋值。
- 复制构造函数:
Counter(Counter other),用 other 的当前值初始化新计数器。 - 方法:
Increment():当前值加1,不能超过MaxLimit,否则抛出异常。GetValue()返回当前值。
- 在
Main中演示:- 输出
Counter.MaxLimit - 创建两个计数器,其中一个通过复制另一个得到
- 验证修改复制后的计数器不影响原计数器
- 输出
示例输出(假设 MaxLimit = 100):
最大允许值: 100
原始计数器: 0 -> 递增后: 1
复制得到的计数器: 1 (独立的对象)
尝试创建超过限制的计数器抛出异常: 参数 initialValue 不能大于 100
考察点:
- 静态构造函数的作用与调用时机
- 复制构造函数的理解(深拷贝/浅拷贝概念入门)
- 异常的基本处理
- 静态成员与实例成员的区别
为什么这三道题是“经典”?
| 题目 | 核心练习目标 | 常见错误陷阱 |
|---|---|---|
| 图书类 | 构造函数重载 + 默认值 | 忘记在无参构造函数中初始化字段;参数名和字段名冲突 |
| 学生类 | this 调用链 + 参数校验 | 不注意调用顺序导致字段重复赋值;忘记校验边界值 |
| 计数器类 | 静态构造 + 复制构造 + 业务限制 | 误以为复制构造是引用复制;静态构造被多次调用 |
做完这三道题,你将彻底掌握:
- 构造函数的各种形态(默认、有参、重载、链式调用、静态、复制)
- 对象初始化时的常见设计模式
- 简单的健壮性处理
你可以先把每个题目自己动手写出来,遇到卡壳的地方再对照我提供的提示(见下方)。写完再看标准参考答案,进步会非常快!
👉 点击查看提示(不建议一开始就看)
题1提示:
- 字段命名建议用
_title或title,但参数同名时必须用this.title = title。 - 无参构造方法体内可以直接调用带参构造并传入默认值,但为了练习,建议手动赋值。
题2提示:
- 构造函数链写法:
public Student(string name) : this(name, 18, 0) { } - 校验逻辑放在最完整的构造函数中(被其他构造最终调用)。
题3提示:
- 静态构造函数语法:
static Counter() { MaxLimit = 100; } - 复制构造函数:
public Counter(Counter other) { _currentValue = other._currentValue; } - 记得判断初始值有效性时使用
if (initialValue > MaxLimit) throw ...
动手试试吧!如果你写完需要我帮你检查或者提供参考答案,随时告诉我。
下面是三道练习题的标准答案,每道题都包含完整的类实现和 Main 方法演示。建议你写完自己的代码后再对照参考,效果最好。
答案 1:图书类(构造函数重载 + 默认值)
using System;
public class Book
{
// 私有字段
private string title;
private string author;
private double price;
// 无参构造函数:设置默认值
public Book()
{
title = "未知书名";
author = "佚名";
price = 0.0;
}
// 带参构造函数
public Book(string title, string author, double price)
{
this.title = title; // this 区分字段和参数
this.author = author;
this.price = price;
}
// 显示信息的方法
public void ShowInfo()
{
Console.WriteLine($"《{title}》{author} ¥{price}");
}
}
class Program
{
static void Main()
{
Book book1 = new Book(); // 使用无参构造
Book book2 = new Book("三体", "刘慈欣", 68.0); // 使用带参构造
book1.ShowInfo();
book2.ShowInfo();
}
}
输出:
《未知书名》佚名 ¥0
《三体》刘慈欣 ¥68
答案 2:学生类(this 调用 + 参数校验)
using System;
public class Student
{
private string name;
private int age;
private double score;
// 无参构造函数:调用单参数构造,姓名设为"新生"
public Student() : this("新生")
{
}
// 单参数构造函数:调用三参数构造,年龄默认18,成绩默认0
public Student(string name) : this(name, 18, 0)
{
}
// 完整构造函数:包含参数校验
public Student(string name, int age, double score)
{
this.name = name;
// 校验年龄 (0~120)
this.age = (age >= 0 && age <= 120) ? age : 18;
// 校验成绩 (0~100)
this.score = (score >= 0 && score <= 100) ? score : 0;
}
public void Show()
{
Console.WriteLine($"姓名:{name}, 年龄:{age}, 成绩:{score}");
}
}
class Program
{
static void Main()
{
Student s1 = new Student(); // 无参
Student s2 = new Student("李明"); // 单参
Student s3 = new Student("王芳", 25, 95.5); // 完整参数
Student s4 = new Student("坏蛋", 200, 80); // 年龄非法
s1.Show();
s2.Show();
s3.Show();
s4.Show();
}
}
输出:
姓名:新生, 年龄:18, 成绩:0
姓名:李明, 年龄:18, 成绩:0
姓名:王芳, 年龄:25, 成绩:95.5
姓名:坏蛋, 年龄:18, 成绩:80
答案 3:计数器类(静态构造函数 + 复制构造函数)
using System;
public class Counter
{
// 实例字段
private int _currentValue;
// 静态只读字段
public static readonly int MaxLimit;
// 静态构造函数:初始化 MaxLimit(只执行一次)
static Counter()
{
// 模拟从配置读取,这里直接写死为 100
MaxLimit = 100;
}
// 实例构造函数:初始值为 0
public Counter()
{
_currentValue = 0;
}
// 实例构造函数:带初始值,并验证
public Counter(int initialValue)
{
if (initialValue > MaxLimit)
throw new ArgumentOutOfRangeException(nameof(initialValue),
$"参数 initialValue 不能大于 {MaxLimit}");
_currentValue = initialValue;
}
// 复制构造函数
public Counter(Counter other)
{
if (other == null)
throw new ArgumentNullException(nameof(other));
_currentValue = other._currentValue;
}
// 增加计数
public void Increment()
{
if (_currentValue + 1 > MaxLimit)
throw new InvalidOperationException($"当前值已达上限 {MaxLimit},无法递增");
_currentValue++;
}
// 获取当前值
public int GetValue()
{
return _currentValue;
}
}
class Program
{
static void Main()
{
// 输出静态成员
Console.WriteLine($"最大允许值: {Counter.MaxLimit}");
// 创建两个计数器
Counter c1 = new Counter(); // 值为 0
c1.Increment(); // 变为 1
Console.WriteLine($"原始计数器: {c1.GetValue()}");
// 复制构造函数
Counter c2 = new Counter(c1);
Console.WriteLine($"复制得到的计数器: {c2.GetValue()} (独立的对象)");
// 修改 c2 不影响 c1
c2.Increment(); // c2 变为 2
Console.WriteLine($"c2 递增后: {c2.GetValue()}, c1 仍然是: {c1.GetValue()}");
// 测试超出限制的异常
try
{
Counter c3 = new Counter(150);
}
catch (ArgumentOutOfRangeException ex)
{
Console.WriteLine($"尝试创建超过限制的计数器抛出异常: {ex.Message}");
}
// 测试递增到超限
Counter c4 = new Counter(Counter.MaxLimit);
try
{
c4.Increment();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"尝试递增超过限制: {ex.Message}");
}
}
}
输出示例(可能因异常消息文本略有不同):
最大允许值: 100
原始计数器: 1
复制得到的计数器: 1 (独立的对象)
c2 递增后: 2, c1 仍然是: 1
尝试创建超过限制的计数器抛出异常: 参数 initialValue 不能大于 100 (Parameter 'initialValue')
尝试递增超过限制: 当前值已达上限 100,无法递增
总结
这三道题覆盖了:
- 基础的重载和默认值
- 构造函数链(
this调用) - 参数校验与防御性编程
- 静态构造函数的执行时机
- 复制构造函数(值类型拷贝)
认真完成并理解这些代码,你对 C# 构造函数的掌握会更扎实。如果有任何疑问(例如为什么用 readonly 而不是 const,或者复制构造函数中的深拷贝问题),欢迎继续提问!