C++ 类与面向对象核心知识整理
本文把 C++ 类相关的核心知识点串成一条线:继承 → 函数重写 → 虚函数 → 多态 → 抽象类,再补上 static、const、this、友元等常用特性。
一、头文件保护:#pragma once
写多文件程序时,同一个头文件可能被重复 #include(比如 A 和 B 都包含了 shape.h),会导致类被重复定义的编译错误。#pragma once 放在头文件最开头,保证它在一个编译单元里只被展开一次:
1 | // shape.h |
它与老式的 include guard 等价,但更简洁、不易写错:
1 | // 老式写法,效果相同 |
#pragma once 是编译器扩展(主流编译器都支持),写头文件时优先用它。
二、继承
2.1 继承关系示意
graph TD
A[Shape] --> B[Circle]
A --> C[Polygon]
C --> D[Rectangle]
C --> E[Triangle]
Shape 是基类,Circle 和 Polygon 继承它,Rectangle、Triangle 又继承 Polygon。
2.2 基本语法
1 | class 派生类 : public 基类名称 { /* ... */ }; |
三种继承方式决定基类成员在派生类中的访问级别:
| 基类中的访问级别 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| public | public | protected | private |
| protected | protected | protected | private |
| private | 不可访问 | 不可访问 | 不可访问 |
注意:
class的默认继承方式是 private,而struct的默认是 public。所以写明public是好习惯。
2.3 构造函数的初始化列表
构造函数后面加 : 可以初始化成员变量,这叫初始化列表:
1 | class Triangle { |
初始化列表和「在函数体里赋值」的区别:初始化列表是直接构造,函数体内赋值是先默认构造再赋值。对 const 成员、引用成员、以及没有默认构造函数的对象成员,只能用初始化列表,这是必须掌握的写法。
初始化列表的执行顺序只和成员声明顺序有关,与列表书写顺序无关——写反了容易踩坑,建议按声明顺序写。
三、函数重写:普通函数 vs 虚函数
3.1 普通函数重写:看指针类型(静态绑定)
1 |
|
输出:
1 | 调用了子类 |
关键:没有 virtual 时,调用哪个函数在编译期就按指针/引用的静态类型决定了,p 是 Shape*,所以就调基类版本。
3.2 虚函数:看对象的实际类型(动态绑定)
在基类函数前加 virtual:
1 |
|
输出:
1 | 调用了子类 |
加上 virtual 后,p->area() 会在运行时根据 p 实际指向的对象类型(Circle)去调用子类版本。这种「同一份调用代码,因对象不同而产生不同行为」的现象,就是多态。
四、多态
4.1 构成多态的条件
四个条件缺一不可:
- 基类中该函数声明为虚函数;
- 派生类用 public 继承;
- 派生类重写了该虚函数(函数名、参数列表、返回类型都要一致);
- 通过基类的指针或引用调用该函数。
理解要点:基类指针/引用可以指向不同的派生类对象,调用同一个虚函数时就会执行各自重写的版本,从而产生不同结果。
4.2 虚析构函数
只要类可能被继承,基类析构函数就应该声明为 virtual,否则用基类指针 delete 派生类对象时,派生类的析构函数不会被调用,造成资源泄漏:
1 |
|
去掉 virtual 后只会输出 ~Base,Derived 里的资源就泄漏了。这是面试和实际开发中都非常常见的坑。
五、纯虚函数与抽象类
函数头后面加 = 0 就是纯虚函数,它只有声明没有实现:
1 | class Shape { |
包含纯虚函数的类叫抽象类(abstract class),有时也叫接口类:
- 不能直接实例化抽象类;
- 必须派生一个类并重写全部纯虚函数,派生类才能被实例化;
- 如果派生类没有重写全部纯虚函数,它仍然是抽象类,依旧不能实例化。
1 |
|
抽象类的价值在于定义接口规范:调用方只依赖 Shape*,不关心具体是圆还是矩形。
六、override 与 final 限定符
这两个是 C++11 引入的,能显著减少多态相关的手误。
6.1 override:让编译器检查是否真的重写
下面这段代码有个隐蔽的 bug——派生类把参数从按值传递改成了按引用传递,于是它不再构成重写,而是变成了一个全新的函数:
1 |
|
代码能编译、能运行,但结果和预期完全不同——这种「语法没错、逻辑错了」的 bug 最难排查。
在函数后面加 override,编译器会主动检查这个函数是否真的重写了基类的虚函数,参数类型不一致就直接报错:
1 | class Circle : public Shape { |
编译报错(GCC 风格):
1 | error: 'void Circle::dosth(int&)' marked 'override', but does not override |
建议:重写虚函数时一律加上 override,让编译器帮你把关(现代 IDE 也能实时标红提示,不能只依赖它)。
6.2 final:禁止继续重写
final 可以放在两处:
放在函数末尾 —— 禁止子类重写这个虚函数:
1 | class Shape { |
放在类名后面 —— 禁止这个类被继承:
1 | class Shape final { |
七、静态成员
7.1 静态成员变量
静态成员变量不属于任何对象,整个类只有一份,相当于「被限定在类作用域内的全局变量」,用 类名:: 访问:
1 |
|
要点:
- 类内只是声明,必须在类外定义(分配存储),否则链接报错;
- 访问方式:
类名::成员,不需要创建对象; - 所有对象共享同一份数据,一个对象改了,其他对象看到的也变了。
7.2 静态成员函数
静态成员函数没有 this 指针,因此只能访问静态成员,不能直接访问非静态成员:
1 | class Demo { |
报错信息:
1 | error: invalid use of member 'Demo::b' in static member function |
原因:非静态成员 b 依附于具体的对象而存在,而静态函数不绑定任何对象,编译期无法确定该用哪个对象的 b。
解决办法:把对象作为参数传进去,通过对象访问其成员(无论公有私有都行):
1 | class Demo { |
八、单例模式(Singleton)
单例保证一个类在整个程序中只有一个实例,并提供全局访问点。实现要点是「把构造函数藏起来」:
1 | class Singleton { |
这是最常用的 Meyers Singleton 写法:靠「私有构造 + 删除拷贝 + 静态局部变量」三件套,既禁止了外部创建,又保证了唯一性,还自带线程安全。
九、const 成员函数
在函数声明和定义的末尾加 const,表示这个函数不会修改对象的成员:
1 |
|
三条规则:
- const 成员函数内不能修改任何成员变量;
- const 成员函数内不能调用非 const 成员函数(因为后者可能改成员);
- const 对象只能调用 const 成员函数。
实践建议:所有「只读」的 getter 都加上 const,否则 const 对象和 const 引用就没法用它们。
十、this 指针
this 是成员函数隐含的第一个参数,指向调用该函数的对象本身:
this->member:把this当指针用;(*this).member:先解引用得到对象,再用.访问成员,等价于this->member(*是解引用运算符)。
1 |
|
最常见的用途就是解决形参名与成员变量同名的问题。
10.1 链式调用
要实现 obj.add(1).add(2).add(3) 这种链式调用,函数必须返回当前对象的引用 类名&:
1 |
|
易错写法:返回值写成 Demo(不是 Demo&)——
1 | Demo add(int num) { |
后果:obj.add(1).add(1) 中,第一次 add 修改的是 obj,但返回的是 obj 的副本,第二次 add 改的是那个副本,obj 本身不再变化。链多少层都没用。
这个 bug 的三个特点:
- 语法完全正确,不报编译错误——属于逻辑 bug,最难排查;
- 值返回会触发拷贝构造函数,产生额外的临时对象(有性能开销);
- 想连续修改同一个对象,必须返回引用
Demo&。
十一、友元 friend
友元允许类外部的函数或其他类访问这个类的 private / protected 成员,相当于定向「开一个后门」:
1 |
|
需要注意:
- 友元关系单向:A 声明 B 是友元,只意味着 B 能访问 A,反过来不行;
- 友元关系不能被继承,也不具有传递性(B 是 A 的友元、C 是 B 的友元,不代表 C 能访问 A);
- 友元破坏了封装,应谨慎使用。常见合理场景是运算符重载(如
operator<<)和需要紧密协作的两个类。
十二、小结
- 头文件用
#pragma once防止重复包含; class默认 private 继承,写public更明确;成员初始化优先用初始化列表;- 普通函数按静态类型调用;加
virtual后按实际对象类型调用,这就是多态; - 多态四条件:虚函数、public 继承、重写、基类指针/引用;
- 基类析构函数应声明为
virtual,否则delete派生类对象会漏掉派生类析构; - 纯虚函数
= 0构成抽象类,不能实例化,只能被继承; - 重写虚函数加
override让编译器检查,final禁止重写或继承; - 静态成员属于类而非对象,类外定义、
类名::访问;静态函数只能直接访问静态成员; - const 成员函数不修改成员,const 对象只能调 const 函数;
- 链式调用必须返回
引用(类名&),返回值会让后续修改丢失; - 友元是单向、不可继承的「定向开权限」,慎用。
