类与结构体

结构体与类(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 个同学呢?变量名得起飞了!而且,name1chinese1math1english1 明显是属于同一个同学的,它们应该“待在一起”。

💡 结构体(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(私有的),需要特殊方式访问,初学算法用 structpublic 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 会自动把创建的对象实例传给它。

  • 创建对象时,在类名后的括号里传入 initself之后的所有参数。

      # 调用 __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 的自然数)。总分最高的学生就是最厉害的,请输出最厉害的学生各项信息(姓名、各科成绩)。如果有多个总分相同的学生,输出靠前的那位(即输入顺序在前的)。

解题思路分析

  1. 数据结构:我们需要一个 Student 结构体/类来存储每个学生的信息(姓名、三科成绩)。为了方便比较,最好能计算并存储总分,或者提供一个计算总分的方法。
  2. 核心逻辑:我们要找总分最高的。关键在于“如果总分相同,输出靠前的那位”。这意味着,我们不能简单地把所有学生读进来,然后用我们上面定义的比较函数(只比较总分)进行排序来找最大值。因为排序可能会打乱原始的输入顺序,导致总分相同时,我们无法确定哪个是“靠前”输入的。
  3. 正确方法:线性扫描 + 维护最优解
    • 我们需要一个变量(比如 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 类/结构体,我们的代码逻辑非常清晰:创建学生对象、获取总分、比较总分、更新最优解。这种“面向对象”的思维方式在处理复杂数据时特别有用。

课后习题

习题 1:两点间距离

习题 2:学生综合评估系统

习题 3:点到直线距离

习题 4:三角形面积

习题 5:直线与圆交点间距

习题 6:两直线交点

牛客代码笔记-牛栋 文章被收录于专栏

汗牛充栋,学海无涯。<br/> 内含算法知识点讲解,以及牛客题库精选例题。<br/> 学习算法,从牛栋开始。

全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务