33.C++核心语法知识总结

  1. 33.总结
    1. 33.1 知识点
      1. 学习的主要内容
      2. 强调
    2. 33.2 核心要点速览
      1. 面向对象基本概念
      2. 封装
        1. 类和对象
        2. 访问修饰符(3P)
        3. 成员方法
        4. 构造函数与析构函数
        5. 静态成员
      3. const 成员变量 vs const 成员函数
      4. const 修饰函数参数
      5. 友元
        1. 嵌套类
        2. 命名空间
        3. 运算符重载
      6. 继承
        1. 继承的基本规则
        2. 继承方式与访问控制
        3. 继承后的构造函数和析构函数
        4. 多重继承
        5. 菱形继承 & 虚继承
        6. 继承中的友元 & 运算符重载
        7. 里氏替换原则
      7. 多态
        1. 虚函数与多态实现
        2. 虚析构函数
        3. 抽象类与纯虚函数
        4. 禁止重写(final关键字)
        5. 虚函数表(V-Table)
      8. 面向对象关联知识点
        1. 接口(C++模拟实现)
        2. 多继承同名虚函数
        3. 空类型的内存占用
        4. 结构体与类的区别
        5. 字符数组与string对比
    3. 33.3 面试题精选
      1. 基础题
        1. 1. const 成员函数的调用规则
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 静态成员变量为什么通常要在类外定义
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 友元关系会不会被继承
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. 为什么多态基类常见写法是虚析构
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 什么是对象切片,和多态有什么关系
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 菱形继承出了什么问题,虚继承怎么修
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 虚函数表大致如何工作,为何会影响 sizeof
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. override 与 final 分别在防什么 bug
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 抽象类与纯虚函数在设计上的用途
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

33.总结


33.1 知识点

学习的主要内容

强调



33.2 核心要点速览

面向对象基本概念

  • 核心理念:万物皆对象,用类抽象特征和行为。

封装

类和对象

核心要点 说明
类的定义 class声明,包含成员变量、构造/析构函数、成员方法,是对象的模板。
对象实例化 - 栈上:ClassName obj;
- 堆上:ClassName* ptr = new ClassName();
内存分布 类定义在代码段,对象实例存储在栈或堆,成员变量占用实际内存。

访问修饰符(3P)

修饰符 作用 访问范围
public 公共成员,类内外均可访问。 类内、类外、子类(继承时)
private 私有成员,仅类内部可访问(默认修饰符)。 仅限类内
protected 保护成员,类内和子类可访问。 类内、子类(继承时)

成员方法

核心要点 说明
声明与定义 - 声明在类内,定义可在类内(内联)或类外(.cpp)。
内联函数 inline修饰,编译时插入代码,减少函数调用开销,适合短函数。
this指针 指向当前对象,用于区分参数与成员变量(如this->age = age;)。

构造函数与析构函数

类型 构造函数 析构函数
声明 ClassName(); / ClassName(int a); ~ClassName();
作用 初始化对象成员,可重载。 释放对象资源(如堆内存),不可重载。
调用时机 实例化对象时自动调用。 对象释放时自动调用(栈对象出作用域/堆对象delete)。

静态成员

类型 静态成员变量 静态成员函数
声明 static int count; static void func();
初始化 .cpp中定义:int ClassName::count = 0; 直接定义在类内或.cpp,无this指针。
访问方式 ClassName::count / obj.count ClassName::func() / obj.func()
特性 所有对象共享,生命周期贯穿程序运行。 不能访问非静态成员,属于类而非对象。

const 成员变量 vs const 成员函数

特性 const 成员变量 const 成员函数
声明示例 const int age = 18;
const int id;(需初始化列表)
void print() const;
初始化规则 - 声明时直接赋值
- 构造函数初始化列表赋值
- 在函数声明和定义处均需加 const 后缀
修改限制 初始化后不可修改(即使在构造函数体内也不可赋值) - 不可修改非 mutable 成员变量
- 可修改 mutable 成员变量
调用限制 - const 对象只能调用 const 成员函数
- 非 const 对象可调用 const 与非 const 成员函数
典型用途 - 固定常量值(如 PI)
- 对象标识(如 ID)
- 访问器(getter)方法
- 不修改状态的操作

const 修饰函数参数

参数类型 语法示例 限制 作用
基本类型 void func(const int i); 函数内不可修改参数值(值传递) 防止意外修改,无实际数据保护(值拷贝)
指向常量的指针 void func(const int* ptr); 可改指针指向,不可改指向的值 保护数据不被修改,允许指针重定向
指针常量 void func(int* const ptr); 不可改指针指向,可改指向的值 固定指针指向,允许修改数据
指向常量的指针常量 void func(const int* const ptr); 不可改指针指向和指向的值 完全保护数据和指针指向
常量引用 void func(const int& ref); 不可修改引用对象,避免拷贝开销 高效传递对象,保护数据不被修改

友元

类型 友元函数 友元类 友元成员函数
声明 friend void func(Class& obj); friend class FriendClass; friend void FriendClass::func(Class& obj);
本质 全局函数,需在类中声明为friend 整个类被声明为友元,所有成员可访问 某类的成员函数被声明为友元,允许访问
作用 允许单个函数访问类的私有/保护成员 允许整个FriendClass类访问另一个类的私有/保护成员 允许某个类的特定成员函数访问私有/保护成员
访问范围 函数内可访问目标类的私有/保护成员 友元类的所有成员函数均可访问目标类成员 仅被声明的成员函数可访问目标类成员
调用方式 直接调用(全局函数) 通过友元类对象调用成员函数 通过友元类对象调用被声明的成员函数
注意事项 - 非类成员,需通过对象参数访问
- 无this指针
- 友元关系单向(A是B的友元≠B是A的友元)
- 不继承
- 需明确作用域(FriendClass::func
- 属于友元类的成员
代码示例
class A {
friend void f(A& a);
};
void f(A& a) { a.privateMember; }

class A {
friend class B;
};
class B {
void access(A& a) { a.privateMember; }
};

class A {
friend void B::access(A& a);
};
class B {
void access(A& a) { a.privateMember; }
};

嵌套类

核心要点 说明
定义 在类内声明的类,如class Outer { class Inner { ... }; };
访问限制 受外层类访问修饰符影响(public可外部使用,private仅限外层类内)。
独立性 与外层类相互独立,可定义自己的成员和方法。

命名空间

核心要点 说明
作用 避免命名冲突,封装相关功能(如namespace Math { int add(); })。
声明 namespace Name { int val; void func(); }
使用 - 限定访问:Name::val
- 导入:using namespace Name;
嵌套 可多层嵌套,如namespace A::B::C { ... },用A::B::C::func();访问。

运算符重载

核心要点 说明
语法 返回类型 operator符号(参数列表),如Point operator+(const Point& p);
类型 - 成员函数:左操作数为类对象
- 非成员函数:可定义其他类型左操作数
常见重载 + - * / == != ++ -- [] ()等,不可重载. :: ?:等符号。
注意 需保持运算符原始语义,避免滥用,建议成对实现关系运算符(如==!=)。

继承

继承的基本规则

特性 说明 注意
“是一个”关系 子类获得父类字段、方法、属性;不含构造、析构、友元 支持多继承(C++)、传递性(前提是访问权限允许)
语法 class 子 : 访问修饰 父 { … }; 访问修饰符决定基类成员在子类中的新可见性
同名隐藏 子类定义同名成员会隐藏基类所有同名重载 父类::member 或在子类写 using 父类::member; 恢复访问
禁止继承 C++ 用 final,C# 用 sealed 只对类生效,不影响已有成员访问

继承方式与访问控制

父类原级别 public 继承后 protected 继承后 private 继承后
public public protected 不可访问
protected protected protected 不可访问
private 不可访问 不可访问 不可访问

继承后的构造函数和析构函数

阶段 执行顺序 要点/注意
构造 ① 递归调用顶层基类
② 构造子类成员(字段/对象,声明顺序)
③ 执行子类自身构造体
默认调用基类无参构造;若无,则初始化列表必须显式调用带参基构造;C++ 可 using 父::父; 引入基类构造
析构 ① 执行子类自身析构
② 析构子类成员
③ 调用各级基类析构
析构不继承,顺序与构造相反

多重继承

特性 说明 关键点/注意
语法 class 子 : 父1, 父2 { … }; 构造按声明顺序调用;析构逆序;若同名需 父::成员 指定
同名二义 多个父有同名成员 必须 父类::member

菱形继承 & 虚继承

类型 问题或方案 关键点/注意
普通菱形继承 B、C 各继 A ⇒ D 得到两份 A 成员访问二义、内存冗余
虚继承 class B : virtual public A D 只保留一份 A;虚基类由最底层派生类负责构造(中间 A(...) 不生效);访问时需额外偏移/指针跳转,开销略增

继承中的友元 & 运算符重载

特性 继承行为 关键点/注意
友元 (friend) 不会被继承 父子友元关系独立;子类私有/保护成员仍不可由父友元访问
运算符重载 (operator) 会被继承 子类可直接使用父类重载运算符

里氏替换原则

场景 表现 关键点/注意
栈上替换 GameObject obj = Player(); 对象切片(子类部分丢失)、不可再转回子类,不推荐
堆上替换 GameObject* p = new Player(); 不切片,可 dynamic_cast<子*>(父*) 安全下转;常用于多态场景

多态

虚函数与多态实现

特性 说明 关键点/注意事项
多态本质 同一父类的不同子类对象,执行相同方法时表现不同行为(运行时多态) 通过虚函数(virtual)和重写(override)实现
虚函数声明 virtual 返回值 函数名(参数) {}(需在父类声明,子类用override重写) - 父类参与多态的成员函数需 virtual;子类重写建议写 override(编译期检查签名)
- 重写时参数/返回值须与父类虚函数一致(协变返回等规则除外)
保留父类逻辑 子类重写函数中用父类名::函数名()显式调用父类实现 Son::Test() { Father::Test(); cout << “子类逻辑”; }
多态调用场景 父类指针/引用指向子类对象时,自动调用子类重写方法(堆上里氏替换) 栈上对象切片会丢失多态性(仅调用父类方法)

虚析构函数

特性 说明 核心作用
必要性 基类析构函数声明为 virtual 后,通过基类指针 delete 派生类对象会先调用派生类析构再调用基类析构。若基类析构非虚,该场景下派生类析构可能不被调用。工程上多态基类通常提供虚析构。 避免派生类资源未释放(如堆成员)造成泄漏或使用已释放资源
执行顺序 子类析构 → 父类析构(与构造顺序相反) 多层继承时只需顶层基类析构为虚函数即可
栈上调用 栈上对象按正常析构顺序调用(先子后父),但切片时子类特有析构可能不执行 Father* f = new Son(); delete f; // 正确调用Son析构
Father f2 = Son(); // 切片,仅调用Father析构

抽象类与纯虚函数

特性 说明 规则
抽象类定义 包含至少一个纯虚函数的类(不能直接实例化,仅作基类) class Shape {
virtual void draw() = 0; // 纯虚函数
};
纯虚函数语法 virtual 函数() = 0;(无函数体,子类必须重写) 子类未实现纯虚函数则自身也为抽象类
应用场景 定义接口规范(如图形基类、游戏对象基类) 抽象类可包含普通函数和成员变量,子类继承后需实现所有纯虚函数

禁止重写(final关键字)

特性 说明 语法示例
禁止类继承 class 类名 final {};(防止类被继承) class FinalClass final {};
禁止函数重写 虚函数后加finalvirtual void func() override final; class Father {
virtual void func() final; // 子类不可重写
};

虚函数表(V-Table)

特性 说明 底层原理
本质 存储虚函数指针的数组,每个含虚函数的类对应一张表 类的所有对象共享虚函数表,对象含虚表指针(vptr)指向该表
内存位置 存储在数据段/常量段(全局共享) 对象sizeof会增加一个指针大小(64位8字节)
多继承实现 每个父类对应一张虚表,子类对象含多个vptr指向各父类虚表 子类虚函数放入第一个父类虚表中(主流编译器实现)

面向对象关联知识点

接口(C++模拟实现)

核心要点 说明 实现方式
本质 行为的抽象规范,C++中通过纯虚函数的抽象类模拟接口 class IFly { virtual void Fly()=0; };(仅含纯虚函数的抽象类)
命名规范 接口类名前缀加I(如IFly class IWalk { virtual void Walk()=0; };
继承与实现 子类继承接口类后必须实现所有纯虚函数 class Bird : public IFly { void Fly() override { ... } };
应用场景 统一管理不同类型但有相同行为的对象(如飞机、鸟、超人都实现IFly接口) IFly* obj[2] = {new Plane(), new Bird()};(里氏替换原则应用)

多继承同名虚函数

核心要点 说明 实现规则
同名处理 多继承时父类若有同名虚函数,子类只需重写一次即可覆盖所有父类版本 class Father { virtual void Eat(); };
class Mother { virtual void Eat(); };
class Son : public Father, public Mother {
void Eat() override { Father::Eat(); Mother::Eat(); }
}
底层原理 多个父类虚函数表中的该函数指针均指向子类重写的函数 子类虚函数表中该函数地址统一指向子类实现,调用时自动分发

空类型的内存占用

核心要点 说明 示例
空类大小 空类/结构体占1字节(确保对象有唯一内存地址) sizeof(EmptyClass) → 1(64位/32位平台均为1)
继承优化 子类包含成员时,空基类的1字节会被优化(不额外占用空间) class Base {}; // sizeof=1
class Derived : Base { int i; }; // sizeof=4(Base的1字节被优化)
含虚函数空类 包含虚函数的空类占8字节(64位平台,虚表指针大小) class VirtualEmpty { virtual void Fun(); };sizeof=8(64位)

结构体与类的区别

核心要点 结构体(struct) 类(class)
默认访问权限 public(成员和继承默认均为public) private(成员和继承默认均为private)
设计初衷 侧重数据集合(如坐标、配置数据) 侧重面向对象封装(数据+行为)
继承语法 struct Son : Father {};(默认public继承) class Son : Father {};(默认private继承)
应用场景 简单数据结构(如struct Point { int x,y; } 复杂对象(如class Player { int hp; void Attack(); }

字符数组与string对比

核心要点 字符数组(char[]) string类
内存管理 固定长度,需手动管理(易越界) 动态扩容,自动管理内存
安全性 无边界检查,易导致缓冲区溢出 有边界检查,安全系数高
常用操作 需要调用C函数(如strcat_sstrcpy_s 支持运算符重载(++=)和成员函数(appendsubstr
空终止符 需手动保证\0终止,否则打印乱码 自动管理终止符,size()返回有效长度
推荐场景 底层开发、嵌入式(空间敏感) 日常字符串处理、C++标准库交互

33.3 面试题精选

基础题

1. const 成员函数的调用规则

题目

const 成员函数谁能调用?const 对象能调用哪些成员函数?

深入解析
  • const 成员函数承诺不修改对象逻辑状态(mutable 除外)。
  • const 对象只能调用 const 成员函数;非 const 对象既能调用 const 成员函数,也能调用非 const 成员函数。
  • 常见踩坑:仅有非 const 版本的成员函数时,const 对象无法调用。
答题示例

const 成员函数给的是「只读接口」:const 对象只能用这组接口。非 const 对象权限更大,const 和非 const 重载版本都能调。设计 API 时,能只读就标 const,既方便 const 对象使用,也约束实现里别乱改状态。

参考文章
  • 9.面向对象-封装-const成员.md

2. 静态成员变量为什么通常要在类外定义

题目

类里声明了 static 成员变量,为什么还要在 .cpp 里再写一遍定义?

深入解析
  • 类内声明是「声明存在」,不占每个对象实例的份;整体程序里仍需唯一定义以分配存储。
  • 类外写法 int ClassName::count = 0; 完成定义与初始化。
  • 静态成员函数一般类内类外定义均可,但同样不访问非静态成员、无 this
答题示例

static 成员变量属于类、不属于某个对象,所以类里只是声明。链接期要有一块真实内存,就在类外做唯一定义并给初值。静态成员函数不绑对象,只能碰静态数据,这是和实例方法的根本区别。

参考文章
  • 8.面向对象-封装-静态成员.md

3. 友元关系会不会被继承

题目

父类的友元函数或友元类,能否自然访问子类的私有成员?

深入解析
  • 友元关系不继承;父类的友元不等于子类的友元。
  • 若需访问子类私有实现,需在子类上另行声明友元,或提供受保护/公有接口。
  • 友元破坏封装,仅用于边界清晰的协作(如运算符重载、紧密协作类)。
答题示例

友元是单向、不继承的契约:A 把 B 当友元,只解决 A 这一层;子类 C 的私有成员,父类友元 B 照样不能随便碰。要访问就再开友元或走接口,否则设计会越来越「全友元」难以维护。

参考文章
  • 11.面向对象-封装-友元函数.md
  • 21.面向对象-继承-继承中的友元和运算符重载.md

进阶题

1. 为什么多态基类常见写法是虚析构

题目

什么时候必须把基类析构声明为 virtual?不声明会怎样?

深入解析
  • 存在「基类指针/引用实际指向派生类对象」,且可能通过基类指针 delete 时,基类析构应为 virtual
  • 非虚析构时,该 delete 可能按基类类型析构,派生类析构链不完整 → 派生类资源泄漏/C++ 未定义行为风险。
  • 无多态、不会用基类指针删派生对象的类型,不必强行虚析构(仍有「非多态基类」的实践讨论,但面试重点在多态场景)。
答题示例

只要存在用基类指针删真实派生对象的用法,基类析构就该是虚的,这样析构从派生类一层层往上走。没写 virtual 的话,释放路径可能停在基类,派生类里托管的资源可能泄漏,也可能触发未定义行为。

参考文章
  • 24.面向对象-多态-虚析构函数.md

2. 什么是对象切片,和多态有什么关系

题目

栈上 Base b = Derived(); 与堆上 Base* p = new Derived(); 在虚函数表现上差在哪?

深入解析
  • 按值赋值/拷贝到基类对象时发生切片:派生类独有成员被切掉,对象静态类型为基类。
  • 通过基类对象调用虚函数通常没有通过指针/引用那样稳定的动态绑定语义预期(对象本身已是基类子对象形态)。
  • 多态惯用法是基类指针/引用指向派生类;需要向下转型时用 dynamic_cast 并做好失败处理。
答题示例

切片是「按基类大小拷贝了一份」,子类多出来的数据和虚表布局预期一起丢了,所以别指望靠值语义玩多态。多态要靠指针或引用保留完整动态类型,这也是和里氏替换、虚函数一起记的一套话。

参考文章
  • 22.面向对象-继承-里氏替换原则.md
  • 23.面向对象-多态-虚函数.md

3. 菱形继承出了什么问题,虚继承怎么修

题目

普通多重继承下的菱形结构有什么隐患?virtual 继承在语义上改变了什么?

深入解析
  • 无虚继承时,最末派生类可能包含多份间接基类子对象 → 二义性、冗余存储。
  • 虚继承(中间层 virtual 基类)合并为一份共享基类子对象;构造上由最终派生类负责虚基初始化等规则需结合编译器实现理解。
  • 仍有性能与布局复杂度成本,设计上优先评估是否改用组合、接口拆层。
答题示例

菱形不处理的话,远端基类在子对象里重复两份,访问同一成员都要写 B::x / C::x 消歧义。虚继承把远端基类收成一份共享,代价是对象布局和构造规则更绕,所以能不用菱形就不用,用也要团队约定清楚初始化责任。

参考文章
  • 20.面向对象-继承-菱形继承.md

深度题

1. 虚函数表大致如何工作,为何会影响 sizeof

题目

有虚函数的类为什么实例往往更大?多继承时虚表可能有几张?

深入解析
  • 编译器为含虚函数的类生成虚函数表(函数指针数组),每个对象藏一个(或多个)虚表指针指向对应表。
  • 单继承常见「一颗 vptr + 一张主虚表」;多继承常见实现了多张虚表 / 多个 vptr(与 ABI、编译器相关),调用虚函数通过 vptr 间接寻址。
  • 动态绑定的成本:多一次间接跳转;收益:运行时按真实类型分派。
答题示例

虚函数把「调哪个实现」从编译期挪到运行期:对象里塞 vptr,类对应一张虚表。多继承时往往不止一张表,所以布局更复杂、sizeof 里也得多算指针。面试把「vptr + vtable + 间接调用」说清就够扎底。

参考文章
  • 27.面向对象-多态-虚函数表.md

2. overridefinal 分别在防什么 bug

题目

子类重写时为什么要写 overridefinal 用在类或虚函数上各限制什么?

深入解析
  • override:编译器检查是否确实重写基类虚函数,签名一改就报错,避免「以为 override 了其实没有」的静默 bug。
  • final 虚函数:禁止派生类再重写该虚函数。
  • final 类:禁止被继承,用于封闭实现或安全审计边界。
答题示例

override 是编译期安全带:名字或参数飘了一点,你以为多态了,其实藏了个新函数。final 一边是锁虚函数,一边是锁类,用途是「到这里不许再派生/重写」,和接口演进策略有关。

参考文章
  • 26.面向对象-多态-禁止重写.md
  • 23.面向对象-多态-虚函数.md

3. 抽象类与纯虚函数在设计上的用途

题目

含纯虚函数的类为什么不能直接实例化?和「接口」类比怎么用 C++ 表达?

深入解析
  • 纯虚函数 = 0 使类变为抽象类:仅规定接口契约,强制派生类实现。
  • 仍可有数据成员、非虚函数、 protected 辅助;与「只含纯虚的抽象类」模拟接口习惯用法不同。
  • 抽象类用于分层:上层依赖抽象,下层换实现,配合智能指针/工厂可落地。
答题示例

纯虚函数等于告诉编译器「这个行为还没实现」,所以抽象类不能 new 出完整对象。C++ 没有 interface 关键字,常用「全是纯虚的类」当协议。设计上是把变化点塞到派生层,基类稳住调用代码。

参考文章
  • 25.面向对象-多态-抽象类.md
  • 28.面向对象关联知识点-接口.md


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏