1 类与对象

GESP C++ 六级:类与对象系统梳理

核心目标:理解面向对象思想,掌握类的定义与对象的创建,理解封装、继承、多态三大特性,掌握构造函数、析构函数、静态成员的用法,能分析类相关程序的输出结果。
类是一张“图纸”,对象是按图纸造出来的“实物”;封装靠访问权限控制数据,继承让子类复用父类,多态靠虚函数在运行时选择正确的实现;构造先父后子,析构先子后父,静态成员全类共享一份。

一、考纲定位

根据CCF官方考纲,GESP六级明确要求:“掌握面向对象的思想,了解封装、继承、多态的基本概念,并掌握类的创建和基本的使用方法”。

六级面向对象模块的考查分布:

考点 重要程度 典型考法
类与对象的概念区分 ★★★★★ 选择题
类的定义与对象创建 代码填空、程序阅读
访问权限(public/private/protected) 选择题高频
构造函数与析构函数 程序阅读、代码填空
静态成员 ★★★★☆ 选择题
继承(基类与派生类) 选择题、程序阅读
多态与虚函数 选择题
类内嵌套类 ★★★☆☆

二、类与对象的基础概念

2.1 什么是类与对象

类(Class) 是自定义数据类型,它规定了这一类事物有哪些数据(成员变量) 和能做什么(成员函数)。对象(Object) 是根据类创建出来的具体实例。

通俗比喻:类是“图纸”或“模具”,对象是按图纸造出来的“房子”或“饼干”。类是抽象概念,不占用内存空间;对象是具体的,占用内存空间。

class Student {
public:
    string name;
    int score;
};

Student a, b;   // a 和 b 是两个具体的对象

Student 是类,a 和 b 是对象。代码中只写类定义并没有创建对象,只有 Student a, b; 这行才真正创建了两个对象。

2.2 class 与 struct 的区别

写法 默认访问权限
struct public
class private
struct A { int x; };     // x 默认 public
class B { int x; };      // x 默认 private

A a; a.x = 10;   // 正确
B b; b.x = 10;   // 编译错误,x 是私有的

如果希望 class 中的成员可以在外部访问,必须加 public:。

2.3 成员变量与成员函数

class Rectangle {
private:
    int width, height;           // 成员变量
public:
    void set(int w, int h) {     // 成员函数
        width = w; height = h;
    }
    int area() {                 // 成员函数
        return width * height;
    }
};

Rectangle r;
r.set(3, 4);
cout << r.area() << endl;   // 输出 12

2.4 对象的创建与访问

普通对象用 . 访问成员:

Point p;
p.x = 3;
p.y = 4;

对象指针用 -> 访问成员:

Point p;
Point* q = &p;
q->x = 3;    // 等价于 (*q).x = 3

静态成员用 类名::成员 访问:

Student::count    // 类名直接访问

三种访问符号的区分:

手里有什么 写法 示例
一个对象 对象.成员 a.add(5)
指向对象的指针 指针->成员 p->add(5)
类名、作用域 类名::成员 Student::count

三、封装与访问权限

3.1 什么是封装

封装是面向对象的三大特性之一。封装就是把数据和操作数据的方法放在一起,并隐藏内部实现细节,对外只提供受控的接口。

封装的好处:防止外部代码随意修改对象内部状态。例如分数 score 设为私有,外部只能通过 setScore() 修改,而 setScore() 可以检查数值是否合法。

3.2 三种访问权限

权限 本类成员函数 派生类成员函数 类外代码
public ✅ ✅ ✅
protected ❌
private ❌

关键规则:

  • private 成员只能在本类内部访问,派生类和外部代码都不能直接访问。
  • protected 成员可以在本类和派生类中访问,但外部代码不能直接访问。
  • public 成员可以在任何地方访问。

真题示例(2026年6月) :Student 的 score 私有,main 中标记了三处操作——调用 getName()、调用 setScore()、直接写 score。哪一处违反权限?答案:直接写 score 的那处,因为 main 是类外代码,不能直接访问私有成员 score。

四、构造函数与析构函数

4.1 构造函数

构造函数在创建对象时自动调用,用于初始化对象。构造函数的特点:

  • 函数名与类名相同。
  • 没有返回类型(连 void 都不写)。
  • 可以重载(多个参数不同的构造函数)。
class Circle {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}   // 带参构造函数
    Circle() : radius(0) {}            // 无参构造函数
    double getArea() { return 3.14 * radius * radius; }
};

构造函数的调用方式:

Circle c1(5.0);              // 直接初始化(推荐)
Circle c2 = Circle(5.0);     // 拷贝初始化
Circle* c3 = new Circle(5.0); // 堆对象

4.2 析构函数

析构函数在对象销毁时自动调用,用于释放资源。特点:

  • 函数名为 ~类名。
  • 没有参数,没有返回值。
  • 一个类只能有一个析构函数。
class MyClass {
public:
    MyClass() { cout << "构造" << endl; }
    ~MyClass() { cout << "析构" << endl; }
};

4.3 构造与析构的调用顺序(高频考点)

构造顺序:先调用基类构造函数,再调用派生类构造函数。
析构顺序:先调用派生类析构函数,再调用基类析构函数(与构造顺序相反)。

class Base {
public:
    Base() { cout << "Base构造" << endl; }
    ~Base() { cout << "Base析构" << endl; }
};
class Derived : public Base {
public:
    Derived() { cout << "Derived构造" << endl; }
    ~Derived() { cout << "Derived析构" << endl; }
};

int main() {
    Derived d;
    return 0;
}
// 输出:Base构造 → Derived构造 → Derived析构 → Base析构

记忆口诀:构造先父后子,析构先子后父。

4.4 构造函数不能是虚函数

构造函数不能声明为虚函数。虚函数机制依赖于对象的虚函数表(vtable),而虚函数表是在构造函数执行过程中才建立的。在构造函数执行之前,对象还没有完整的虚函数表,因此无法实现动态绑定。

析构函数可以是虚函数,而且当基类指针指向派生类对象时,基类的析构函数应该声明为虚函数,否则 delete 基类指针时不会调用派生类的析构函数,导致资源泄漏。

五、静态成员

5.1 静态数据成员

用 static 修饰的数据成员属于整个类,而不是某个对象。所有对象共享同一份静态成员。

class Counter {
public:
    static int count;      // 静态成员声明
    Counter() { count++; }
};
int Counter::count = 0;    // 静态成员在类外定义并初始化

int main() {
    Counter a, b, c;
    cout << Counter::count << endl;   // 输出 3
    cout << a.count << endl;           // 也可以这样访问,输出 3
}

访问方式:可以通过 类名::静态成员 或 对象.静态成员 访问,两种方式都可以。

真题示例(2023年12月) :

static int objCounter; 在类外定义为 int newClass::objCounter = 2;,创建两个对象后,newClass::objCounter 和 classB.objCounter 都能正确访问,输出 2。答案:两行代码都正确。

5.2 静态成员函数

静态成员函数不依赖于对象,可以直接通过类名调用。静态成员函数不能访问非静态成员(因为非静态成员属于具体对象)。

class Math {
public:
    static int square(int x) { return x * x; }
};
cout << Math::square(5) << endl;   // 输出 25

六、继承

6.1 什么是继承

继承允许一个类(派生类/子类)从另一个类(基类/父类)获取成员变量和成员函数。派生类自动拥有基类的 public 和 protected 成员。

class Pet {
public:
    string kind;
    int age;
    Pet(string _kind, int _age) : kind(_kind), age(_age) {}
};

class Dog : public Pet {    // Dog 继承 Pet
public:
    string color;
    Dog(string _kind, int _age, string _color)
        : Pet(_kind, _age), color(_color) {}   // 调用基类构造函数
};

关键规则:

  • 派生类构造函数必须通过初始化列表调用基类构造函数。
  • 如果基类没有默认构造函数,派生类必须显式调用基类的带参构造函数。
  • 派生类不能直接访问基类的 private 成员,但可以访问 protected 和 public 成员。

真题示例(样题) :

Pet 是基类,Dog 是子类。Dog 的构造函数自动调用 Pet 的构造函数。dog 是 Dog 类的实例。最后一行输出 dog.color 是正确的,因为 color 是 Dog 类的成员。答案:D(“输出代码会报错”的说法是错误的)。

6.2 派生类与基类的同名函数

派生类可以定义与基类同名的成员函数,这称为隐藏或覆盖:

  • 如果基类函数不是虚函数,派生类同名函数隐藏基类函数。
  • 如果基类函数是虚函数,派生类同名函数覆盖基类函数,实现多态。

真题示例(样题) :

“派生类可以和基类有同名成员函数,但是否覆盖同名成员函数需要取决于函数参数是否一致。”答案:C。只有参数列表完全一致时才是覆盖(override),否则是隐藏。

七、多态与虚函数

7.1 什么是多态

多态是指同一操作作用于不同对象时,产生不同的行为。C++ 通过虚函数实现运行时多态。

class Device {
public:
    virtual void work() { cout << "设备工作" << endl; }
};

class Printer : public Device {
public:
    void work() override { cout << "打印机打印" << endl; }
};

class Scanner : public Device {
public:
    void work() override { cout << "扫描仪扫描" << endl; }
};

int main() {
    Device* d1 = new Printer();
    Device* d2 = new Scanner();
    d1->work();   // 输出:打印机打印
    d2->work();   // 输出:扫描仪扫描
}

核心机制:基类指针指向派生类对象时,调用虚函数会根据实际对象类型选择对应的实现,而不是根据指针类型。

7.2 虚函数的规则

规则 说明
构造函数不能是虚函数 对象尚未建立虚函数表
析构函数可以是虚函数 基类析构应为虚,支持多态删除
派生类覆盖虚函数 函数签名必须完全一致
虚调用按实际对象分派 运行时动态绑定

真题示例(2026年6月) :

“把构造函数声明为虚函数,可以据此实现运行时多态。”答案:错误。C++ 中构造函数不能是虚函数。

八、类内嵌套类

一个类可以在另一个类的内部定义,称为内部类(嵌套类) 。

class Rectangle {
private:
    class Point {          // 内部类
    public:
        double x, y;
    };
    Point a, b, c, d;
public:
    // ...
};

访问规则:内部类需要通过外部类来访问,即 Rectangle::Point,不能直接使用 Point。

真题示例(2023年12月) :

“内部类可以随便访问,不需要通过外部类来访问。”答案:错误。内部类必须通过外部类访问。

九、真题精讲

真题1:对象创建与成员函数调用(2024年3月)

给定 Circle 类定义,以下哪个语句在类外部正确创建了 Circle 对象并调用了 getArea 函数?
A. Circle c = Circle(5.0); c.getArea(c);
B. Circle c(5.0); getArea(c);
C. Circle c = new Circle(5.0); c.getArea;
D. Circle c(5.0); c.getArea();
答案:D

逐项解析:A 错在 c.getArea(c) 多写了参数;B 错在缺少对象调用符 c.;C 错在 new 返回指针,类型不匹配,且 c.getArea 缺少括号;D 正确。

真题2:空指针访问(2024年12月)

MyClass* obj = NULL; obj->display(); 存在什么问题?
答案:C. 空指针访问错误

解析:通过空指针调用成员函数是未定义行为,程序可能崩溃。

真题3:静态成员访问(2023年12月)

静态成员 objCounter 可以通过 类名::静态成员 和 对象.静态成员 两种方式访问。答案:两种方式都正确。

真题4:构造与析构顺序(判断题)

基类析构没有 virtual,用基类指针 delete 派生类对象,是否保证先执行派生类析构再执行基类析构?
答案:错误。没有虚析构函数时,只调用基类析构,派生类析构不会被调用。

十、易错点汇总

  1. 类定义末尾忘记分号:class Student { ... }; 末尾的 ; 不能丢。
  2. class 默认 private:class 中不写 public:,成员默认是私有的,外部无法访问。
  3. 构造函数写返回类型:构造函数没有返回类型,连 void 都不写。
  4. 构造函数不能是虚函数:C++ 中构造函数不能声明为 virtual。
  5. 非虚析构的多态删除陷阱:基类指针 delete 派生类对象时,基类析构必须是 virtual,否则派生类析构不会被调用。
  6. 内部类访问需要外部类作用域:内部类必须通过 外部类::内部类 访问。
  7. 静态成员类外定义:静态数据成员必须在类外定义并初始化:int Counter::count = 0;
  8. 静态成员两种访问方式都合法:类名::静态成员 和 对象.静态成员 都可以。
  9. 派生类不能访问基类 private 成员:只能访问 public 和 protected 成员。
  10. 构造先父后子,析构先子后父:顺序不能反。
  11. new 返回指针:Circle c = new Circle(5.0); 是错误的,应该写 Circle* c = new Circle(5.0);
  12. 空指针调用成员函数:obj->display() 前必须检查 obj != nullptr。

十一、速查表

需求 写法 关键注意
定义类 class 类名 { ... }; 末尾分号不能丢
创建对象 类名 对象名; 类定义不创建对象
带参构造 类名 对象名(参数); 推荐写法
访问成员 对象.成员 普通对象
指针访问 指针->成员 对象指针
类名访问 类名::成员 静态成员
构造函数 类名(参数) : 初始化列表 {} 无返回类型
析构函数 ~类名() {} 无参数,无返回
静态成员声明 static 类型 成员名; 类外定义
静态成员定义 类型 类名::成员名 = 初值; 类外初始化
继承 class 派生类 : public 基类 { ... }; public 继承
调用基类构造 派生类(...) : 基类(...) { ... } 初始化列表
虚函数 virtual 返回类型 函数名(); 实现多态
虚析构 virtual ~类名(); 基类必须虚析构
构造顺序 先基类,后派生类 记忆口诀
析构顺序 先派生类,后基类 与构造相反

十二、总结

类与对象是 GESP 六级从“面向过程”迈向“面向对象”的关键内容。

  1. 类是自定义类型(图纸),对象是按类创建的具体实例(实物)。
  2. class 默认 private,struct 默认 public。
  3. 封装通过访问权限控制数据:public 公开、private 私有、protected 保护。
  4. 构造函数名与类名相同,没有返回类型,创建对象时自动调用。
  5. 构造函数不能是虚函数,析构函数可以是虚函数。
  6. 基类析构应声明为 virtual,否则基类指针 delete 派生类对象时不会调用派生类析构。
  7. 静态成员全类共享一份,类外定义,两种访问方式都合法。
  8. 继承让派生类复用基类的 public 和 protected 成员。
  9. 多态通过虚函数实现,基类指针调用虚函数时按实际对象类型分派。
  10. 构造先父后子,析构先子后父。

复习建议:先理解类与对象的概念区分,掌握类的定义和对象创建;再重点突破访问权限、构造函数、静态成员三个高频考点;然后理解继承与多态的基本规则,特别是虚函数和虚析构的用法;最后通过真题练习程序阅读题,手动模拟构造/析构顺序和成员访问过程。考试中先圈出类名和对象名,再判断访问权限是否合法,最后逐行跟踪对象的创建、初始化和销毁过程。