GESP 六级:类与对象核心知识点
2026-08-29 14:07:07
发布于:广东
这里的“对象”主要指类的对象。C++ 的广义对象还包括整型变量等,不要把“对象”误解为只有自己写的类实例。
六级考纲要求理解面向对象思想、封装、继承和多态,并会创建、使用类。本专题按“看懂对象 → 会使用对象 → 看懂继承与多态”的顺序展开。静态成员、拷贝构造、嵌套类和运算符重载是历年客观题的补充,不必一开始全部背完。
01 类和对象:类型与具体实例
先用一句话区分
类规定“有哪些数据、能做什么”;对象是按这个类创建出来的具体实例。
例如 Student 表示“学生这种类型”;a 和 b 是两个具体学生。不能反过来说“类是对象的实例”。
#include <iostream>
using namespace std;
class Student {
public:
int score = 0;
void add(int x) {
score += x;
}
};
int main() {
Student a, b;
a.add(5);
b.add(2);
cout << a.score << ' ' << b.score;
}
这段程序输出 5 2。先不要急着背术语,按下表读:
| 代码 | 意思 |
|---|---|
class Student |
定义一个名为 Student 的类 |
int score = 0; |
每个对象各自保存一份分数 |
void add(int x) |
对象可以执行的操作,叫成员函数,也常叫方法 |
Student a, b; |
创建两个对象;只写类定义并没有创建这两个对象 |
a.add(5) |
给 a 加分,不会给 b 加分 |
最后的 }; |
类定义结束,分号不能丢 |
做输出题,给每个对象单独记账
| 执行到哪里 | a.score | b.score |
|---|---|---|
| 创建 a、b | 0 | 0 |
a.add(5) |
5 | 0 |
b.add(2) |
5 | 2 |
普通数据成员通常是“每个对象一份”;成员函数的代码不需要给每个对象复制一份。后面学到的 static 数据成员则是“整个类共享一份”。
三种符号不要混
| 手里有什么 | 常见写法 | 示例 |
|---|---|---|
| 一个对象 | 对象.成员 |
a.add(5) |
| 指向对象的有效指针 | 指针->成员 |
p->add(5) |
| 类名、作用域 | 类名::成员 |
访问公有静态成员或类外定义成员函数 |
Student a;
Student* p = &a;
p->add(3); // 和 a.add(3) 操作同一个对象
Student* p = nullptr; 只创建了一个空指针,没有创建 Student 对象。不能接着用它调用普通成员函数。
本节记住:先圈出类名,再圈出对象名。读到一次成员操作,就只更新相应对象的数据。
02 封装与权限:谁可以访问数据
为什么不让所有人随便改分数
上一节把 score 设为公有,外部可以直接写 a.score = -10;。如果我们希望分数不能为负,就可以把数据藏起来,只允许通过检查过的操作修改它。
class Student {
private:
int score = 0;
public:
void setScore(int x) {
if (x >= 0 && x <= 100)
score = x;
}
int getScore() const {
return score;
}
};
a.setScore(90) 合法;在普通的 main 中写 a.score = 90 不合法。这里体现了封装:把数据和相关操作组织在一起,对外提供接口,隐藏内部细节。封装不是“把代码加密”,也不是“不准任何人读取数据”。
三种权限
下表按普通成员访问讨论,不考虑友元和特殊访问路径;派生类一列指在派生类成员函数中访问继承来的成员。
| 权限 | 本类成员函数 | 派生类成员函数 | 普通类外代码 |
|---|---|---|---|
public 公有 |
可以 | 可以 | 可以 |
protected 保护 |
可以 | 可以,须遵守保护成员访问规则 | 不可以直接访问 |
private 私有 |
可以 | 不可以直接访问 | 不可以直接访问 |
“私有”不等于“对象里没有这个数据”。数据仍然存在,只是当前这段代码没有直接访问的权限。
class 和 struct 的默认值
| 写法 | 没写访问标签的成员 | 没写继承方式 |
|---|---|---|
class |
默认 private | 默认 private 继承 |
struct |
默认 public | 默认 public 继承 |
C++ 的 struct 也可以有构造函数、成员函数和继承关系,并不是“只能存数据”。
访问权限题固定做法
- 找到被访问的成员,向上看最近的
public:、protected:或private:。 - 看访问发生在哪里:本类里、派生类里,还是
main中。 - 对照权限,不要被函数名或对象名干扰。
一个常见错误:“公有成员函数是访问私有数据的唯一途径”不能当成绝对规则。友元也可以被授权访问;课堂基础例子不涉及友元时,先通过公有接口访问即可。
03 构造与初始化:对象刚创建时做什么
构造函数的三个识别点
名字和类名相同;没有返回类型,连 void 也不写;创建对象时参与初始化。
class Student {
int score;
public:
Student(int s) : score(s) {}
int getScore() const { return score; }
};
Student a(80);
Student b(95);
Student a(80) 会把 80 交给构造函数的参数 s,再用 s 初始化 a.score。
Student(int s) : score(s) {} 中,冒号后面是成员初始化列表。它发生在函数体执行之前,不是调用一个名叫 score 的函数。
同一个类可以有多个构造函数
class Student {
int score;
public:
Student() : score(0) {}
Student(int s) : score(s) {}
};
Student a; // 选无参数的版本
Student b(80); // 选接收一个 int 的版本
这叫构造函数重载:同名、参数列表不同。仅仅改参数名不算不同,靠返回类型也不能重载构造函数。
默认构造函数最容易丢分
| 类的情况 | Student a; 是否一定合法 |
|---|---|
| 没有用户声明的构造函数 | 编译器会隐式声明默认构造函数,但它可能因成员或基类限制而被删除 |
只有 Student(int s) |
不合法,没有可用的无参构造版本 |
自己写了可访问的 Student() |
若其定义合法,通常可以 |
“编译器会生成”有前提,不能漏掉“没有用户声明的构造函数”。
class Point {
public:
double x, y;
Point(double a, double b) : x(a), y(b) {}
};
Point p(1, 2); // 合法
// Point q; // 错误:缺少无参构造函数
还有一个独立问题:有默认构造函数不等于所有数字自动变成 0。 struct A { int x; }; 的普通局部对象 A a; 中,a.x 没有被初始化,不应读取它。教学中明确写初值,别赌默认值。
初始化次序看声明,不看列表摆放
class Pair {
int a;
int b;
public:
Pair() : b(2), a(1) {}
};
仍然先初始化 a,再初始化 b,因为数据成员按类内声明顺序初始化。基础题先认清每个成员的初值;遇到相互依赖时,再核对次序。
本节记住:数实参,找能匹配的构造函数;不要把“定义指针”误算成“创建对象”。
04 析构与生命周期:对象什么时候结束
析构函数负责结束时的清理
class Item {
public:
Item() { cout << "C "; }
~Item() { cout << "D "; }
};
int main() {
Item a;
{
Item b;
}
cout << "X ";
}
假设已包含 <iostream> 并使用 std::cout,输出为 C C D X D。
| 时刻 | 发生的事 | 输出 |
|---|---|---|
| 定义 a | 构造 a | C |
| 进入内层,定义 b | 构造 b | C |
| 离开内层大括号 | 析构 b | D |
| 执行普通输出 | 对象 a 仍在 | X |
| main 正常结束 | 析构 a | D |
析构函数写作 ~类名(),没有返回类型,没有参数。在本专题采用的 C++11 中,一个类不能通过不同参数重载多个析构函数。
普通局部对象与 new 创建的对象
| 情况 | 典型结束时机 |
|---|---|
普通局部对象 Item a; |
正常离开其作用域时自动析构 |
Item* p = new Item; 指向的对象 |
对应 delete p; 时析构并释放存储 |
| 指针变量 p 自己离开作用域 | 不会因此自动 delete 它指向的动态对象 |
对 new[] 分配的数组使用 delete[],不能混配。这里只讨论正常控制流,不用异常终止的情况干扰基础题。
不手写析构函数,可以吗
可以。 编译器可以隐式声明析构函数。普通整数成员、string、vector 等成员都有各自的处理规则。
但若类自己管理 new 申请的原始内存,默认析构不会替你 delete 指针指向的内容。不要把“语法上可以不写”误记成“任何资源都会自动释放”。
创建派生类对象的顺序
对普通单继承:先构造基类部分,再初始化派生类的数据成员,最后执行派生类构造函数体。销毁按相反方向进行,先执行派生类析构函数体,再销毁其成员,最后销毁基类部分。
本节记住:画大括号,标创建点与结束点;先建后拆。
05 拷贝、引用与赋值:是否创建了新对象
三行看着像,含义不一样
Student a(80);
Student b = a; // 新建 b:拷贝初始化
Student& r = a; // r 是 a 的别名,不新建 Student 对象
b = a; // b 已经存在:赋值,不是再次构造 b
判断依据不是“有没有等号”,而是这一行有没有在创建新对象。
拷贝构造也属于构造
#include <iostream>
using namespace std;
class Item {
public:
Item() { cout << "default "; }
Item(const Item&) { cout << "copy "; }
};
int main() {
Item a;
Item b = a;
Item& r = a;
}
输出 default copy:默认构造 1 次,拷贝构造 1 次;引用 r 不增加一次构造。
即使没有手写拷贝构造函数,只要隐式拷贝构造可用,Item b = a 仍然会进行拷贝构造。没有打印内容,不等于没有构造。
| 题目问什么 | 该数什么 |
|---|---|
Item() 函数体执行几次 |
只数无参版本 |
某行 cout 输出几次 |
只数真正执行了这行输出的调用 |
| 构造函数一共调用几次 | 还要考虑拷贝构造等其他版本 |
2025 年 3 月的相关单选题存在计数口径问题,真题模块已把原卷答案与严格解释分开。不要从它推出“拷贝构造不算构造”。
值传参和引用传参
对已经存在的对象 a:调用 void f(Student s) 通常要用 a 拷贝构造形参 s;调用 void g(const Student& s) 则让形参引用原对象,不为形参创建新的 Student 对象。
默认拷贝原始指针时,复制的是地址,不会自动复制指向的整片内存。本专题只需先理解这一点,不展开资源管理和完整拷贝控制设计。
本节记住:新对象、别名、已有对象赋值,先分成三类再计数。
06 static 与 this:共享数据和当前对象
普通成员各一份,静态成员共一份
#include <iostream>
using namespace std;
class Counter {
public:
static int total;
int value = 0;
Counter() { ++total; }
static int getTotal() { return total; }
};
int Counter::total = 0;
int main() {
Counter a, b;
a.value = 7;
cout << a.value << ' ' << b.value << ' '
<< Counter::getTotal();
}
输出 7 0 2。a.value 和 b.value 分开存;total 是共享计数。普通 C++11 的非 const 静态数据成员,上例用类外定义提供存储。
公有静态数据可以通过 Counter::total 访问,也可以通过 a.total 访问;二者是同一份数据。更推荐前一种,含义清楚。
静态成员函数没有“当前对象”
调用 Counter::getTotal() 时,没有指定是 a 还是 b,所以它不能直接使用某个对象才有的普通数据成员。
但不能把这句话简化成“静态函数绝对不能访问普通成员”。若显式给它一个对象,它可以在权限允许的情况下通过该对象访问,例如:
static int read(const Counter& c) {
return c.value;
}
this 是什么
在普通非静态成员函数里,this 指向正在操作的对象;*this 就是那个对象。
class Score {
int value = 0;
public:
Score& add(int x) {
value += x;
return *this;
}
int get() const { return value; }
};
Score a;
a.add(2).add(3);
第一次 add 返回 a 自身的引用,所以后面可以接着调用 a 的 add,最后分数为 5。
链式调用能否成立,先看前一次调用的返回类型。 返回 void,就不能直接接 .add(...);返回当前对象的引用,则可以继续操作同一个对象。这与“数组容量够不够”是两个不同问题。
07 继承:在已有类型上扩展
先认识“基类”和“派生类”
class Person {
protected:
int age;
public:
Person(int a) : age(a) {}
int getAge() const { return age; }
};
class Student : public Person {
int score;
public:
Student(int a, int s) : Person(a), score(s) {}
void birthday() { ++age; }
};
Person 是基类,也叫父类;Student 是派生类,也叫子类。学生复用人的年龄与查询年龄的方法,再增加分数等内容,这就是继承。
构造函数冒号后,谁给谁初始化
Student(12, 90) 的初始化分两块:
Person(a)把 12 交给基类构造函数,初始化基类部分。score(s)初始化派生类自己的分数为 90。
假如基类只有 Person(int),却不写 Person(a),编译器会尝试默认初始化基类部分;找不到可用的无参版本就会编译失败。
C++ 此处不写 super(a),也不写 Person::Person(a)。
公有继承不代表父类数据全部变公有
在 public 继承下:基类的 public 成员仍为 public,protected 成员仍为 protected。基类 private 成员不能被子类普通成员函数直接访问。
如果上例把 age 改成 private,子类中的 ++age 就不合法;但仍可以调用父类提供的公有方法。
基类指针指向派生类对象
Student s(12, 90);
Person* p = &s;
cout << p->getAge();
对于上例的公有、无歧义继承,转换合法,因为 Student 对象中有 Person 部分。反方向不能随便把任意 Person 都当作 Student。
C++ 也支持多继承,但不必在六级基础复习中先钻研菱形继承。
本节记住:找冒号后的父类,再看继承方式;父类私有数据不是子类随便可用的数据。
08 虚函数与多态:同一个接口,不同的实现
一个最小例子
#include <iostream>
using namespace std;
class Device {
public:
virtual void work() { cout << "device"; }
virtual ~Device() {}
};
class Printer : public Device {
public:
void work() override { cout << "printer"; }
};
int main() {
Printer x;
Device* p = &x;
p->work();
}
输出 printer。p 的声明类型是 Device*,但它实际指向 Printer 对象;work 是虚函数,于是此次调用选用 Printer 中的覆盖版本。
输出题只追三个信息
| 要看什么 | 上例中是什么 |
|---|---|
| 指针或引用的声明类型 | Device* |
| 实际对象类型 | Printer |
| 调用的函数在基类中是否为 virtual | 是 |
运行时多态的常见组合是:继承关系 + 虚函数被覆盖 + 通过基类指针或引用调用。 不能只看到两个函数同名,就认定一定发生运行时多态。
若把基类 work 前面的 virtual 和子类的 override 一起去掉,则通过 Device* 调用普通 work 会使用基类版本。若只去掉 virtual 却留下 override,则会编译报错,不能直接讨论输出。
override 与重载不是一回事
| 名称 | 关注点 | 典型例子 |
|---|---|---|
| 重载 overload | 同一作用域同名函数,参数列表不同 | add(int) 与 add(int,int) |
| 覆盖 override | 派生类提供与基类虚函数匹配的版本 | void work() override |
override 让编译器检查你是否真的覆盖成功。返回类型、参数、const 等都要满足覆盖规则。基类函数已经是虚函数,子类正确覆盖后即使不再写 virtual,也仍是虚函数。
构造不能虚,析构可以虚
构造函数不能声明为 virtual。若要通过基类指针 delete 派生类对象,基础 C++11 场景中应提供可访问的虚析构函数:
Device* p = new Printer;
delete p; // 上例 Device 有虚析构,正常依次清理派生类、基类
若基类析构非虚,却这样删除派生类对象,属于未定义行为。不能保证“只执行基类析构”,也不能保证“一定崩溃”。未定义行为意味着语言规则不保证结果。
再认识一个符号:= 0
virtual void work() = 0; 是纯虚函数。仍有未实现纯虚函数的类是抽象类,不能直接创建该类对象;可以用指针、引用指向具体派生类对象。这是补充识别点,不要求从零设计复杂接口。
本节记住:不要只看指针写着谁,必须追到它实际指向谁。
09 真题补充:const、嵌套类与运算符
const 成员函数:承诺不通过它修改普通成员
int getScore() const { return score; }
const 写在参数列表后,修饰成员函数。const 对象通常只能调用相应的 const 非静态成员函数;静态成员函数不依赖对象,不要混入这条口诀。
这里不是说“函数绝不能改变任何东西”:它仍可能输出内容,也存在 mutable、外部数据等进阶情况。初学时把它理解为对当前对象普通成员的只读接口即可。
嵌套类:类里面还可以定义类
class Board {
public:
class Cell {
public:
int row = 0;
};
};
Board::Cell c;
类外使用公开嵌套类型时可写 Board::Cell。:: 解决“这个名字属于哪个作用域”,不能绕过访问权限。若把 Cell 放在 Board 的 private 区域,普通 main 中不能靠加 Board:: 强行访问。
自己的类型,不会自动支持所有运算
有一个类对象 a,不代表 cout << a、a < b、a[0] 都天然合法。是否支持,要看是否有相应运算符重载或其他合法转换。
class Score {
int value;
public:
Score(int x) : value(x) {}
bool operator<(const Score& other) const {
return value < other.value;
}
};
这里明确规定了 < 比较分数。在 C++11 中,自定义 operator[] 必须是非静态成员函数。不要据此说“所有运算符都只能在类内重载”。
构造函数也可能参与类型转换
上例可接收一个整数的非 explicit 构造函数,可以参与相应的隐式转换。加上 explicit 后,Score a(80) 仍合法,但不能再写 Score a = 80。
成员类型不是毫无限制
class Node {
int value;
Node* next; // 可以:只保存地址
// Node child; // 不可以:一个 Node 内再完整包含自身
};
如果每个 Node 都完整包含一个 Node,就会无限包含下去,无法确定对象布局。普通非静态数据成员也不能直接是 void、未完成类型对象或抽象类对象;指向这些类型的指针要另行判断。
静态整型常量有特例
在 C++11 中,static const int limit = 10; 可以在类内给出常量初始化。不能背成“所有静态常量都只能在类外初始化”。若发生需要定义的使用,仍可能需要类外定义,这属于进一步的细节。
本节记住:遇到“任意、只能、一定、全部”,先找适用条件,不机械背绝对结论。
10 读题四步:先判断合法,再计算输出
第一步:认人
在草稿上写清楚:类名、对象名、指针名、引用名分别是谁。A* p; 不等于创建了 A 对象;A& r = a; 不等于复制一个 a。
第二步:查门
查访问权限、参数匹配、对象是否存在。若题目给出的代码无法编译,就先停止模拟,不要继续算不存在的输出。
第三步:分账
普通数据按对象分别列表;静态数据单独记一份;创建和销毁各记一次。看到 A b = a,先认出拷贝构造。
第四步:选函数
调用构造时看参数;调用普通成员函数时看对象和类型;通过基类指针调用虚函数时看实际对象。最后只记录真正执行的输出语句,别把返回值也当作自动输出。
一张检查表
| 题面特征 | 第一动作 | 常见陷阱 |
|---|---|---|
class、对象访问 |
圈类名、对象名 | 类与对象说反 |
obj.x |
查 x 权限与调用位置 | private 当 public |
new、-> |
查指针是否指向有效对象 | 指针存在但对象不存在 |
| 构造次数 | 标新对象及构造版本 | 漏算拷贝构造 |
| 大括号与析构 | 标作用域结束点 | 以为对象只在程序结束时销毁 |
static |
共享值只记一份 | 每个对象都记一份静态数据 |
return *this |
看返回类型是否为引用 | 把链式调用误判成语法错误 |
virtual、基类指针 |
追实际对象 | 只看指针声明类型 |
area(); |
找里面真正的 cout | 以为 return 会打印 |
三个概念,最后再对照一次
- **封装:**内部数据与操作放在一起,对外提供受控接口。
- **继承:**新类型复用已有类型的内容,再进行扩展。
- **多态:**同一接口可表现出不同实现;六级代码题重点看虚函数的运行时分派。
抽象也是面向对象设计中的重要思想。模块化则是更一般的软件设计方法,并非只有面向对象才能做到。C++ 支持面向对象,也支持面向过程等其他编程方式。
复习重点
先掌握第 1~4 节,保证能读懂一个简单类;再掌握第 5~8 节,处理计数、继承和虚函数;第 9 节随对应真题补齐,不用先背成术语表。真正掌握的标志是能说明“当前调用的是哪个对象、哪个函数、为什么”。
核对资料
知识讲解与示例为本专题编写,历年题目、原卷答案与必要的口径说明见独立的“历年真题”模块。最新草案中与本专题无关的新版本特性不作为 C++11 考试规则使用。
这里空空如也










有帮助,赞一个