类与结构体
结构体与类(C++/Java/Python)
引言:为啥要有结构体/类?
想象一下,学校让你统计期末考试成绩(可以点开对应链接获取题面)。每个同学都有好几项信息:姓名、语文分、数学分、英语分。
如果让你处理 3 个同学的信息,你可能会写出类似这样的代码(伪代码):
// 同学1
string name1 = "shy";
int chinese1 = 114;
int math1 = 51;
int english1 = 4;
// 同学2
string name2 = "re0hg";
int chinese2 = 114;
int math2 = 10;
int english2 = 23;
// 同学3
string name3 = "hgcode";
int chinese3 = 51;
int math3 = 42;
int english3 = 60;
才 3 个同学就有点乱了,对吧?如果 N=100 个同学呢?变量名得起飞了!而且,name1
和 chinese1
、math1
、english1
明显是属于同一个同学的,它们应该“待在一起”。
💡 结构体(C++ 里常用 struct)和类(C++、Java、Python 里都用 class)就是为了解决这个问题而生的!它们就像一个自定义的“数据收纳盒”,可以把描述同一个事物(比如一个学生)的所有相关信息(姓名、各科成绩)打包在一起。这样,我们就可以把一个学生当作一个整体来处理,代码会清晰、有条理得多。
接下来,我们就来看看他们具体该如何使用。
结构体/类的声明与使用 (基础篇)
目标: 定义一个“学生”数据类型,并创建具体的学生对象。
C++ (struct
/class
)
-
在 C++中我们用
struct
关键字定义一个结构体类型,用class
关键字来定义一个类。大括号{}
内部是它的成员变量(它包含哪些信息)。别忘了结尾的分号;
(千万不要忘记)。#include <string> // 定义 Student 结构体 struct Student { std::string name; // 姓名 int chinese; // 语文 int math; // 数学 int english; // 英语 }; // <-- 注意分号 class Student{ public: std::string name; // 姓名 int chinese; // 语文 int math; // 数学 int english; // 英语 }
-
这样我们就定义号了我们的学生结构体了,定义好类型后,就可以像使用
int
,double
一样,创建该类型的变量(我们称之为“对象”或“实例”)。// 在 main 函数或其他地方创建 Student 对象 Student s1; // 创建了一个名为 s1 的学生对象 Student s2; // 可以创建多个,这样就解决了我们遇到的代码冗余的问题
-
对于结构体和类我们使用点运算符
.
来访问或修改对象的成员变量(前提是成员变量和函数是public
的)。// 给 s1 对象的成员赋值 s1.name = "senpai"; s1.chinese = 114; s1.math = 51; s1.english = 4; // 访问 s1 对象的信息并打印 #include <iostream> std::cout << "姓名: " << s1.name << std::endl; std::cout << "语文: " << s1.chinese << std::endl;
此外,对于结构体/类的指针,我们还可以通过
->
来访问或修改对象的成员变量Student * s = new Student(); // 给 s 对象的成员赋值 s->name = "senpai"; s->chinese = 114; s->math = 51; s->english = 4; // 访问 s 对象的信息并打印 #include <iostream> std::cout << "姓名: " << s->name << std::endl; std::cout << "语文: " << s->chinese << std::endl;
-
温馨小提示:C++ 的
struct
默认成员是public
(公开的),可以直接访问。class
默认是private
(私有的),需要特殊方式访问,初学算法用struct
或public class
更方便。
Java (使用 class
)
Java 中没有 struct
关键字,统一使用 class
来定义这种数据结构。
-
Java 使用 class 关键字定义类。成员变量(字段)在类的大括号 {} 内声明。通常我们会加上 public 修饰符,表示它们是公开的。
// 定义 Student 类 class Student { public String name; // 姓名 public int chinese; // 语文 public int math; // 数学 public int english; // 英语 }
-
创建类的对象需要使用 new 关键字。变量(如 s1)存储的是对象的引用(地址)
// 在 main 方法或其他地方创建 Student 对象 Student s1 = new Student(); // 使用 new 创建实例 Student s2 = new Student();
-
同样使用点运算符
.
来访问对象的成员变量。// 给 s1 对象的成员赋值 s1.name = "senpai"; s1.chinese = 114; s1.math = 51; s1.english = 4; // 访问 s1 对象的信息并打印 System.out.println("姓名: " + s1.name); System.out.println("语文: " + s1.chinese);
Python (使用 class
)
Python 也使用 class
关键字,并且 Python 的对象模型比较灵活。
-
Python 用
class
定义类,后面通常跟方法定义(比如__init__
,下一小节会为大家讲解)。一个简单的类可以先只写pass
,用来表示这是一个空类的定义,。# 定义 Student 类 class Student: pass # 空类定义,之后再添加内容
-
创建对象直接调用类名加括号
()
。# 创建 Student 对象 s1 = Student() s2 = Student()
-
Python 非常灵活,你可以直接给对象添加属性并赋值(更规范的做法是在类的
__init__
方法中统一定义和初始化),并且也是用点运算符.
来进行访问。# 给 s1 对象动态添加属性并赋值 s1.name = "senpai" s1.chinese = 114 s1.math = 51 s1.english = 4 # 访问 s1 对象的属性并打印 print(f"姓名: {s1.name}") # 使用 f-string print(f"语文: {s1.chinese}")
现在,我们已经学会了如何把一个学生的各项信息打包到一个 Student
对象里了!这比用一堆零散的变量要清晰多了吧?
构造函数:创建对象时的初始化工作
在每次创建学生对象时,我们如果都要手动设置姓名、成绩,那也是很麻烦的。能不能在创建对象的那一刻,就直接把这些信息传进去呢?接下来为大家介绍的构造函数就是干这个的!
C++
-
在 c++中构造函数是一个特殊的成员函数,它的名字和结构体/类名完全相同,并且没有返回值类型(连
void
都不写)。struct Student { std::string name; int chinese, math, english, total_score; // 构造函数,接收初始值 Student(std::string n, int c, int m, int e) { name = n; chinese = c; math = m; english = e; total_score = c + m + e; } };
-
此外,我更推荐使用“初始化列表”的方式来初始化成员,写在函数名和 {} 之间,用冒号 : 开始,这对于
const
成员或引用成员是必须的。struct Student { // ... 成员变量同上 ... // 使用初始化列表的构造函数 Student(std::string n, int c, int m, int e) : name(n), chinese(c), math(m), english(e) { total_score = chinese + math + english; // 可以在函数体内做其他事 } };
-
当我们使用带参数的构造函数创建对象时,需要在类型名后的括号里传入对应的参数。
// 使用构造函数创建并初始化 s1 Student s1("senpai", 114, 51, 4); std::cout << s1.name << " 的总分是: " << s1.total_score << std::endl;
Java
-
Java 中的构造方法(Constructor)和 C++ 类似,方法名与类名相同,无返回类型,通常使用 public 来修饰。
class Student { public String name; public int chinese, math, english, totalScore; // 构造方法 public Student(String n, int c, int m, int e) { // 使用 this 区分成员变量和参数 this.name = n; this.chinese = c; this.math = m; this.english = e; this.totalScore = c + m + e; } }
-
this
关键字代表当前正在创建的对象本身。当参数名和成员变量名相同时,用this.成员名
来明确指定是给成员变量赋值,这样可以避免混淆。 -
在 Java 中,我们使用 new 调用构造方法创建对象,并且在定义了带参构造方法后,默认的无参构造方法 new Student() 就没了,需要的话得自己写 public Student() { ... }。
// 使用构造方法创建并初始化 s1 Student s1 = new Student("senpai", 114, 51, 4); // 对象已初始化 System.out.println(s1.name + " 的总分是: " + s1.totalScore);
Python
Python 中,构造函数是一个特殊的方法,名字固定为
__init__
(前后都有两个下划线)。
-
Python 的构造函数是特殊方法 init。第一个参数必须是 self,代表对象实例。
class Student: # 构造函数 __init__ def __init__(self, n, c, m, e): # 使用 self.属性名 = 值 来初始化 self.name = n self.chinese = c self.math = m self.english = e self.total_score = c + m + e print(f"学生 {self.name} 已创建!") # 可以加些调试信息
-
self
参数在调用时不需要我们传递,Python 会自动把创建的对象实例传给它。 -
创建对象时,在类名后的括号里传入 init 中
self
之后的所有参数。# 调用 __init__ 创建并初始化 s1 s1 = Student("senpai", 114, 51, 4) # 对象已初始化 print(f"{s1.name} 的总分是: {s1.total_score}")
有了构造函数,我们在创建和初始化对象是不是一步到位,清爽多了?
让对象学会“比较”:运算符重载 / 接口实现
现在我们有了学生对象,但怎么比较两个学生谁更“厉害”呢?比如,我们要按总分给学生排序,或者找出总分最高的学生。程序需要知道如何判断
studentA
是不是比studentB
“更好”(或“更大”、“更小”),在我们的题目中是总分高的更厉害。
C++ (运算符重载)
C++ 允许我们重载(重新定义)标准运算符(如 <
, >
, ==
等)的行为,让它们能用于我们自定义的 Student
类型。这对于使用标准库的排序 (std::sort
) 或优先队列 (std::priority_queue
) 非常方便。
bool operator<(const Student& other) const { ... }
是重载<
的标准形式。我们需要在函数体内部实现你的比较逻辑。返回true
表示当前对象 (this
) 小于other
对象。struct Student { // ... (成员变量和构造函数同上) ... int getTotalScore() const { return total_score; } // 获取总分的方法 // 重载小于号 (<) 运算符 // const Student& other: 传入另一个对象的常量引用,高效且安全 // 末尾的 const: 表示这个函数不修改当前对象的状态 bool operator<(const Student& other) const { // 定义比较规则:按总分比较。总分低的算“小于” return this->getTotalScore() < other.getTotalScore(); // 如果总分相同,可以加次要比较条件,例如按姓名 // if (this->getTotalScore() != other.getTotalScore()) { // return this->getTotalScore() < other.getTotalScore(); // } // return this->name < other.name; // 总分相同比姓名 } };
- 定义了
operator<
后,就可以直接用<
比较两个Student
对象,或者将它们用于需要比较大小的标准库函数(如std::sort
,std::priority_queue
)。 ps:std::sort
默认使用operator<
来排序。Student s1("senpai", 114, 51, 4); // 169 Student s2("lxl", 114, 10, 23); // 147 if (s2 < s1) { // 这里就会调用我们定义的 operator< std::cout << s2.name << " 的总分低于 " << s1.name << std::endl; }
Java (实现 Comparable
接口)
- Java 不允许直接重载运算符(如
<
)。替代方案是让你的类实现java.lang.Comparable
接口。这个接口要求你提供一个compareTo
方法。// 让 Student 类实现 Comparable<Student> class Student implements Comparable<Student> { // ... (成员变量和构造方法同上) ... public int getTotalScore() { return totalScore; } // 必须实现 compareTo 方法 @Override // 表明是实现接口方法 public int compareTo(Student other) { // 规则:返回负数表示 this < other, 0表示相等, 正数表示 this > other // 按总分升序比较 (分数低的在前) // return Integer.compare(this.totalScore, other.totalScore); // 按总分降序比较 (分数高的在前) return Integer.compare(other.totalScore, this.totalScore); // 注意 other 在前 // 也可以组合比较: // int scoreCompare = Integer.compare(other.totalScore, this.totalScore); // if (scoreCompare == 0) { // 总分相同 // return this.name.compareTo(other.name); // 比姓名 (String自带compareTo) // } // return scoreCompare; } }
- 在Java中,Integer.compare(x, y) 是比较整数的安全方式。String 类本身也实现了 Comparable,可以直接调用 this.name.compareTo(other.name) 比较字符串。
- 实现了 Comparable 接口的类,其对象列表或数组可以直接用 Collections.sort() 或 Arrays.sort() 排序。
Python (特殊方法)
Python 像 C++ 一样,支持通过定义特殊方法(dunder methods, double underscore methods)来重载运算符。
-
Python 通过定义特殊方法(如
__lt__
)来支持比较运算。其中__lt__
对应小于号 <。from functools import total_ordering # functools.total_ordering 可以自动帮你生成其他的比较方法(>, <=, >=) # 但对于算法题,通常实现 __lt__ 就够用了,或者需要哪个实现哪个 #@total_ordering # (可选) 帮助自动生成其他比较方法 (> <= >=) class Student: # ... (__init__ 同上) ... def get_total(self): return self.total_score # 实现 __lt__ (less than) def __lt__(self, other): if not isinstance(other, Student): # 检查类型是个好习惯 return NotImplemented # 表示无法比较 # 按总分升序比较 return self.get_total() < other.get_total() # 复杂比较逻辑: # if self.get_total() != other.get_total(): # return self.get_total() < other.get_total() # # 总分相同,按姓名升序 # return self.name < other.name # 如果用了 @total_ordering, 还需要实现 __eq__ (equal) def __eq__(self, other): if not isinstance(other, Student): return NotImplemented # 认为总分和姓名都相同时才相等 return self.get_total() == other.get_total() #and self.name == other.name
-
isinstance(other, Student)
检查other
是不是Student
类型的对象。NotImplemented
是一个特殊返回值。 -
定义了
__lt__
后,可以直接用<
比较对象,列表的sort()
方法或sorted()
函数也会默认使用它。s1 = Student("senpai", 114, 51, 4) # 169 s2 = Student("lxl", 114, 10, 23) # 147 if s2 < s1: # 调用 __lt__ print(f"{s2.name} 的总分低于 {s1.name}") students = [s1, s2] students.sort() # 使用 __lt__ 排序 (默认升序) print(students) # 需要定义 __repr__ 或 __str__ 来看清晰的输出
-
值得一提的是,为了方便打印列表等,我们最好给类定义
__repr__
或__str__
方法
现在,你的 Student
对象不仅能存储数据,还能相互比较了!这是使用很多标准算法(如排序、优先队列)的前提。
例题讲解
让我们回到最初的题目:最厉害的学生
题意
现有 名同学参加了期末考试,并且获得了每名同学的信息:姓名(不超过 8 个字符的仅有英文小写字母的字符串)、语文、数学、英语成绩(均为不超过 150 的自然数)。总分最高的学生就是最厉害的,请输出最厉害的学生各项信息(姓名、各科成绩)。如果有多个总分相同的学生,输出靠前的那位(即输入顺序在前的)。
解题思路分析
- 数据结构:我们需要一个
Student
结构体/类来存储每个学生的信息(姓名、三科成绩)。为了方便比较,最好能计算并存储总分,或者提供一个计算总分的方法。 - 核心逻辑:我们要找总分最高的。关键在于“如果总分相同,输出靠前的那位”。这意味着,我们不能简单地把所有学生读进来,然后用我们上面定义的比较函数(只比较总分)进行排序来找最大值。因为排序可能会打乱原始的输入顺序,导致总分相同时,我们无法确定哪个是“靠前”输入的。
- 正确方法:线性扫描 + 维护最优解
- 我们需要一个变量(比如
best_student
)来记录到目前为止找到的最厉害的学生。 - 再用一个变量(比如
max_total_score
)记录当前找到的最高总分。 - 我们按输入顺序,一个一个地处理学生信息。
- 对于读入的第一个学生,他/她暂时就是最厉害的。记录下他/她的信息和总分。
- 对于之后读入的每个学生 (
current_student
):- 计算
current_student
的总分current_total
。 - 关键比较:如果
current_total
严格大于max_total_score
,那么这个新学生比我们之前找到的都要厉害,更新best_student = current_student
,并且更新max_total_score = current_total
。 - 如果
current_total
等于max_total_score
,根据题目要求(输出靠前的),我们不需要做任何事,因为当前的best_student
是更早输入的。 - 如果
current_total
小于max_total_score
,更不用管了。
- 计算
- 处理完所有 N 个学生后,
best_student
里存储的就是最终答案。
- 我们需要一个变量(比如
代码实现
#include <iostream>
#include <string>
#include <vector> // 虽然本题可以直接边读边比较,但用 vector 存一下也可以
struct Student {
std::string name;
int chinese;
int math;
int english;
int total_score;
// 构造函数方便创建
Student(std::string n="", int c=0, int m=0, int e=0) : name(n), chinese(c), math(m), english(e) {
total_score = c + m + e;
}
int getTotalScore() const { return total_score; }
// 输出学生信息的方法
void printInfo() const {
std::cout << name << " " << chinese << " " << math << " " << english << std::endl;
}
};
int main() {
int n;
std::cin >> n;
Student best_student; // 用于存储最终结果
int max_total_score = -1; // 初始化一个不可能达到的低分数
for (int i = 0; i < n; ++i) {
std::string current_name;
int current_chinese, current_math, current_english;
std::cin >> current_name >> current_chinese >> current_math >> current_english;
// 创建当前学生对象
Student current_student(current_name, current_chinese, current_math, current_english);
int current_total = current_student.getTotalScore();
// 核心比较逻辑
if (current_total > max_total_score) {
max_total_score = current_total;
best_student = current_student; // 更新最优学生
}
// 如果 current_total == max_total_score,根据题意,不做任何事
}
// 输出最终找到的最厉害的学生信息
best_student.printInfo();
return 0;
}
import java.util.Scanner;
class Student {
public String name;
public int chinese;
public int math;
public int english;
public int totalScore;
public Student(String n, int c, int m, int e) {
this.name = n; this.chinese = c; this.math = m; this.english = e;
this.totalScore = c + m + e;
}
// 无参构造,方便初始化 bestStudent
public Student() { this.totalScore = -1; } // 初始化一个特殊值
public int getTotalScore() { return totalScore; }
public void printInfo() {
System.out.println(name + " " + chinese + " " + math + " " + english);
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
Student bestStudent = new Student(); // 初始化一个总分很低(或无效)的学生
// int maxTotalScore = -1; // 或者用 bestStudent.totalScore 来跟踪
for (int i = 0; i < n; ++i) {
String currentName = scanner.next();
int currentChinese = scanner.nextInt();
int currentMath = scanner.nextInt();
int currentEnglish = scanner.nextInt();
Student currentStudent = new Student(currentName, currentChinese, currentMath, currentEnglish);
int currentTotal = currentStudent.getTotalScore();
// 核心比较逻辑
if (currentTotal > bestStudent.getTotalScore()) {
bestStudent = currentStudent; // 更新最优学生引用
}
// 如果 currentTotal == bestStudent.getTotalScore(),不做处理
}
// 输出结果
bestStudent.printInfo();
scanner.close();
}
}
class Student:
def __init__(self, n="", c=0, m=0, e=0): # 提供默认值方便初始化
self.name = n
self.chinese = c
self.math = m
self.english = e
self.total_score = c + m + e
def get_total(self):
return self.total_score
def print_info(self):
print(f"{self.name} {self.chinese} {self.math} {self.english}")
# --- 主程序 ---
n = int(input())
best_student = Student(total_score=-1) # 初始化一个总分无效的学生对象
# max_total_score = -1 # 也可以单独维护最大总分
for _ in range(n):
line = input().split() # 读取一行,按空格分割
current_name = line[0]
current_chinese = int(line[1])
current_math = int(line[2])
current_english = int(line[3])
current_student = Student(current_name, current_chinese, current_math, current_english)
current_total = current_student.get_total()
# 核心比较逻辑
if current_total > best_student.get_total(): # 或者 > max_total_score
best_student = current_student
# max_total_score = current_total # 如果用了单独变量,也要更新
# 输出结果
best_student.print_info()
看到了吗?通过定义 Student
类/结构体,我们的代码逻辑非常清晰:创建学生对象、获取总分、比较总分、更新最优解。这种“面向对象”的思维方式在处理复杂数据时特别有用。
课后习题
汗牛充栋,学海无涯。<br/> 内含算法知识点讲解,以及牛客题库精选例题。<br/> 学习算法,从牛栋开始。