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 派生类对象,是否保证先执行派生类析构再执行基类析构?
答案:错误。没有虚析构函数时,只调用基类析构,派生类析构不会被调用。
十、易错点汇总
- 类定义末尾忘记分号:
class Student { ... };末尾的;不能丢。 - class 默认 private:
class中不写public:,成员默认是私有的,外部无法访问。 - 构造函数写返回类型:构造函数没有返回类型,连
void都不写。 - 构造函数不能是虚函数:C++ 中构造函数不能声明为
virtual。 - 非虚析构的多态删除陷阱:基类指针 delete 派生类对象时,基类析构必须是
virtual,否则派生类析构不会被调用。 - 内部类访问需要外部类作用域:内部类必须通过
外部类::内部类访问。 - 静态成员类外定义:静态数据成员必须在类外定义并初始化:
int Counter::count = 0; - 静态成员两种访问方式都合法:
类名::静态成员和对象.静态成员都可以。 - 派生类不能访问基类 private 成员:只能访问
public和protected成员。 - 构造先父后子,析构先子后父:顺序不能反。
new返回指针:Circle c = new Circle(5.0);是错误的,应该写Circle* c = new Circle(5.0);- 空指针调用成员函数:
obj->display()前必须检查obj != nullptr。
十一、速查表
| 需求 | 写法 | 关键注意 |
|---|---|---|
| 定义类 | class 类名 { ... }; |
末尾分号不能丢 |
| 创建对象 | 类名 对象名; |
类定义不创建对象 |
| 带参构造 | 类名 对象名(参数); |
推荐写法 |
| 访问成员 | 对象.成员 |
普通对象 |
| 指针访问 | 指针->成员 |
对象指针 |
| 类名访问 | 类名::成员 |
静态成员 |
| 构造函数 | 类名(参数) : 初始化列表 {} |
无返回类型 |
| 析构函数 | ~类名() {} |
无参数,无返回 |
| 静态成员声明 | static 类型 成员名; |
类外定义 |
| 静态成员定义 | 类型 类名::成员名 = 初值; |
类外初始化 |
| 继承 | class 派生类 : public 基类 { ... }; |
public 继承 |
| 调用基类构造 | 派生类(...) : 基类(...) { ... } |
初始化列表 |
| 虚函数 | virtual 返回类型 函数名(); |
实现多态 |
| 虚析构 | virtual ~类名(); |
基类必须虚析构 |
| 构造顺序 | 先基类,后派生类 | 记忆口诀 |
| 析构顺序 | 先派生类,后基类 | 与构造相反 |
十二、总结
类与对象是 GESP 六级从“面向过程”迈向“面向对象”的关键内容。
- 类是自定义类型(图纸),对象是按类创建的具体实例(实物)。
class默认private,struct默认public。- 封装通过访问权限控制数据:
public公开、private私有、protected保护。 - 构造函数名与类名相同,没有返回类型,创建对象时自动调用。
- 构造函数不能是虚函数,析构函数可以是虚函数。
- 基类析构应声明为
virtual,否则基类指针 delete 派生类对象时不会调用派生类析构。 - 静态成员全类共享一份,类外定义,两种访问方式都合法。
- 继承让派生类复用基类的
public和protected成员。 - 多态通过虚函数实现,基类指针调用虚函数时按实际对象类型分派。
- 构造先父后子,析构先子后父。
复习建议:先理解类与对象的概念区分,掌握类的定义和对象创建;再重点突破访问权限、构造函数、静态成员三个高频考点;然后理解继承与多态的基本规则,特别是虚函数和虚析构的用法;最后通过真题练习程序阅读题,手动模拟构造/析构顺序和成员访问过程。考试中先圈出类名和对象名,再判断访问权限是否合法,最后逐行跟踪对象的创建、初始化和销毁过程。