第一章:从C到C++

c++核心编码思维是将程序运行时的错误尽可能暴露在编译阶段;

C++中很多关键字不是进行功能实现,而是进行代码检查。

一、概览:从C到C++

1.C++的组成部分

从C到C++多了什么?

image-20250727165342597

C和C++编程范式对比:(划×表示用此语言实现会很麻烦)

image-20250727165551067

2.C++风格的头文件变化

image-20250727171906234

示例:

image-20250727172046805

3.老概念新形式:名称空间

名称空间核心作用:

image-20250727172921946

示例:

image-20250727173122820

名称空间如何使用:

image-20250727173028036

示例:

image-20250727191943281

名称空间总结:

  • 避免程序中,不同代码段中相同变量名的冲突
  • 提高程序的[可读性』、[可维护性』与『可扩展性』
  • 匿名的名称空间,可以取代[静态全局变量』
  • 类是一种特殊的名称空间

4.比指针更方便的[C++引用]

1)区分三个概念 实参、形参、引用参数:

引用参数是实参的别名,形参是实参的拷贝;

2)引用需要在定义时,完成[绑定』

3)左值、右值、左值引用,右值引用(C++重点,也是高频面试题)

左值(Lvalue)与右值(Rvalue)

  • 左值(Lvalue):
    • 具有地址,存储在内存中,生命周期持久。
    • 可以取地址(&),例如变量、函数返回的左值引用、解引用指针(*ptr)。
    • 示例:int a = 5; 中的 a 是左值;std::vector<int> v; v[0] 也是左值。
  • 右值(Rvalue):
    • 通常没有地址,存储在寄存器或临时内存中,生命周期短暂(通常在表达式结束后销毁)。
    • 无法取地址,例如字面量(5)、函数返回的临时对象、表达式结果(a + b)。
    • 示例:int foo() { return 5; } 中,foo() 返回的是右值。

左值引用(Lvalue Reference)与右值引用(Rvalue Reference)

  • 左值引用(T&):
    • 绑定到左值,用于避免拷贝或修改左值。
    • 示例:int a = 5; int& b = a;
    • const 左值引用可绑定到右值(延长临时对象生命周期):const int& c = 5;
  • 右值引用(T&&):
    • 绑定到右值,用于实现移动语义和资源转移。
    • 示例:int&& d = 5;std::string&& s = std::move(str);
    • 通过 std::move 将左值转换为右值引用,表示资源可被“窃取”。

二、C++的输入输出

1.printf的接班人:cout

cout会根据输出数据类型自动选择输出方式,cout支持连续输出,输出顺序用左移运算符连接:

image-20250728010506236

2.scanf的接班人:cin

cin可读入单一变量的值,也可进行连续读入,cin想读入文件末尾时将cin作为while循环条件即可:

image-20250729225640720

3.printf与scanf应该被打入冷宫吗?

  • 结论:不应该,各有优势关键在[用]。

三、体验:C++中的编辑工具

1.string类的基本使用

image-20250816161902405

示例:

image-20250816162645058

2.map的基本使用

map特性之一:访问的下标不存在时,会自动创建一个此下标的元素,值为相关类型默认值

map的底层是红黑树

image-20250816165341532

示例:

image-20250816165237111

补充:map的遍历方式(map的本质是红黑树,遍历是中序)

3.更方便的函数指针:function

function语法更符合我们对定义变量的认知:前面是类型,后面是变量:

image-20250817162637528

由function定义的对象可指向所有和函数相关的对象,比如

普通函数:

image-20250817003526169

函数对象:

image-20250817004250221

lambda表达式:

image-20250817004718335

第二章:封装

一、类的基本封装

1. 初次相识:类与对象

  • 类与对象 相当于 类型与变量

image-20250213161326448

image-20250213161946651

2. 编码安全:访问控制权限

image-20250214163442011

C中结构体只能放属性;c++中没有纯粹的结构体,可以放属性和方法,其实相当于类了

友元函数和友元类示例:

3.生命周期:构造函数与析构函数

对象生命周期:构造、使用和析构

image-20250218110504513

image-20250218111731868

默认构造函数:设置属性默认值,示例:

image-20250218112132972

有参构造示例:

this指针:指向当前对象,示例:

(补充)为什么要使用初始化列表:

1.提升效率:直接构造比用默认构造创建对象再赋值效率更高;

2.必要性:使用对象之前,必须先构造对象,如果没有显性构造,则会使用默认构造,但若没默认构造则会报错,示例:

image-20250218160351011

image-20250218170439674

image-20250218170608811

image-20250219153305797

拷贝构造函数示例:

image-20250219162925130

image-20250220143842614

new、delete和malloc、free的本质区别是malloc只申请一片存储区域,而new会申请并调用构造函数

4.何以为我:属性和方法

类属性和成员属性:

  • 成员属性每个对象都有一份,类属性是所有对象共享一份,类属性可当作全局变量;

  • 类属性的声明和定义需要分开:

成员方法和类方法:

  • 成员方法:必须通过对象进行调用,可访问当前对象this指针
  • 类方法:可通过对象或者类名两种方式进行调用

image-20250221172428961

image-20250221174120568

const类型:

  • const类型的方法只能被const类型的对象调用,const对象也只能调用const方法;
  • 若想在const内部修改属性的值,可以用mutable关键字

image-20250222105248352

二、运算符重载(本章重点)

1. 函数重载

定义:

image-20250222125326916

功能总结:

image-20250222141629381

2.类外运算符重载基础

通过在类外面进行运算符重载,扩展已有运算符的功能

image-20250222142633379

3.类内运算符重载基础

image-20250222173259969

image-20250222173831068

4.函数对象、数组对象与指针对象

函数对象:本质是对象,只是形式像函数的一种对象

image-20250226183319251

数组对象:表现像数组的对象

image-20250226184840899

指针对象:表现像指针的对象

image-20250226185607454

5.总结:运算符重载

不能重载的5个运算符相当于建筑中的承重墙不能动,但不常用

第三章:继承

一、继承基础

1.基础知识:继承的基本语法

image-20250311182447886

protected:仅供此类以及其子类访问;

2.经验传授:继承中的5个注意事项

1)继承以后的子类大小

先了解下字节对齐:

image-20250314192848775

子类的大小=子类原来大小+父类大小 且依然会继承父类的对其值

在C++中,父类的数据存储在子类的前面。

image-20250314193223688

示例:

父类A:int x; 子类B:int y; 则B类大小等于8字节;

父类A:long long x; 子类B:int y; 则B类大小等于16字节;(此时对齐值是父类的8字节)

2)父类的指针与引用指向子类对象

所有用在父类的方法,都可以用在子类上;

因为子类是父类的扩展,其实可以把子类对象当成一个加强版的父类对象。

3)初探子类数据的存储结构

父类存储在子类数据之前,多重继承按继承顺序存储;

示例:

子类A的存储顺序是:Base2、Base1、子类本身

子类B的存储顺序是:Base1、A、子类本身

image-20250317114625984

4)子类与父类的构造与析构顺序

image-20250317100703486

父类先构造,子类后构造,先构造的后析构;

强制转换不是一个好的解决方案,好的解决方案是将父类析构函数改为虚函数;

5)继承中的构造、拷贝与赋值

在对象的值的拷贝过程中,有拷贝构造和赋值运算符;继承时,都需要额外调用父类的拷贝构造和赋值运算符;

3.编码技巧:创造大整数类型

image-20250317163905949

示例:997+45=?

image-20250317164334519

4.一课搞懂:继承权限

二、多重继承

1.多重继承的基础知识

主要作用:进行功能的拼装;

示例:鸭子类继承了Flyable的飞行和Swimmable的游泳功能

image-20250318110313508

2.菱形继承:困扰与解决方案

菱形继承:菱形继承是指在C++中,一个基类通过多个路径被间接继承,导致一个派生类中存在多个该基类的实例的继承结构。

image-20250318114453130

困扰:上面这里B和C类继承的x都是独立的存储空间不一样

解决方案:使用虚继承:合并同类型

image-20250318114248108

3.初识:对象模型

什么是对象模型:

C++ 的对象模型定义了类实例在内存中的布局方式,包括数据成员的存储和虚函数的实现机制(如 vtable 虚表),简单来说对象模型就是系统底层到底是怎么存储数据的。

对象模型的核心作用:

  1. 内存优化:非静态数据成员按声明顺序存储,基类子对象按继承顺序排列,减少内存碎片。
  2. 多态支持:通过虚表(vtable)和虚表指针(vptr)实现运行时动态绑定,支持多态和接口编程。
  3. 性能平衡:在封装性、灵活性与执行效率间权衡,例如空基类优化(EBO)减少空类实例的内存占用。

示例:

image-20250726121939206

image-20250726122011298

第四章:多态

一、多态基础

1.基础概念:多态的基本分类

什么是多态:

image-20250319100910707

函数重载和运算符重载都是多态:

image-20250319100709676

多态的分类:

多态分为:编译时多态 和 运行时多态

image-20250319101435809

2.运行时多态:虚函数与纯虚函数

虚函数特点:普通的成员方法是跟着类走,而虚函数是跟着对象走

不用虚函数时,父类的指针C虽然可以调用子类对象a,但运行C->run()时,任然调用的是父类C的run方法:

image-20250319103625895

解决方法:使用虚函数:

image-20250319105630930

补充:

虚函数最关键的3个关键字:virtual、override和final:

image-20250319114643775

override关键字作用:代码检查:用于指示一个虚函数是重写基类中的虚函数,以便编译器进行检查;

C++中很多关键字不是进行功能实现,而已进行代码检查,为了将程序运行时的错误尽可能暴露在编译阶段,这也是c++核心编码思维,关键字中这些关键字才是重点;

final关键字作用:定义当前函数的最后一个版本,后续派生类不能再修改此方法,用于防止类被继承或者防止虚函数被重写。

析构函数一定要是虚函数;

一个类拥有纯虚函数,那他就是一个抽象类;接口通过抽象类定义接口;一个仅有纯虚函数(没有其他成员函数和成员方法)的抽象类就是一个接口;

image-20250321105634479

C++类型转换:

静态转换 static_cast:同类转换,同类型const和非const转换;主要解决C语言的安全问题和提高代码可读性;(4种类型转换的重点)

动态转换 dynamic_cast:用于在运行时检查和转换多态类型,安全地在类层次结构中向上或向下转换指针或引用。

常量转换 const_cast:用于去除或添加对象的常量性,允许修改原本被声明为const的对象或指针。

指针转换 reinterpret_cast:用于进行底层的、位级别的强制类型转换,如将指针转换为整数或将一种指针类型转换为另一种不相关类型的指针。

3.探究虚函数的对象模型

为什么虚函数能够跟着对象走?

因为拥有虚函数的对象,在它的内存存储区的头部会额外多存储一个地址,这个地址是一个虚函数表指针,指向虚函数表,虚函数表是一个函数指针数组:存储的是虚函数的地址,运行时通过偏移量定位(动态绑定),使其能够准确调用这些虚函数;

image-20250406123617707

  1. 对象携带 vptr 指向虚函数表。
  2. 虚函数表存储函数地址,运行时通过偏移量定位。
  3. 派生类重写时更新虚函数表,从而实现多态。

则程序中有多少类,就有多少个虚函数表:

image-20250406124341602

4.多态的应用:访问者模式

image-20250414184639636

image-20250414190259762

image-20250414190352259

image-20250414190403275

第五章:模板

一、模板基础

模板是将【任意类型】从程序设计中抽象出来;

模板支持的编程范式是泛型编程和模板元编程;(泛型是重点,模板元99%情况下用不到)

image-20250920193547881

1.语法基础:模板函数

定义模板函数:

image-20250920203813981

示例:

image-20250920205301722

2.趁热打铁:实现一个完美的add模板

上述示例add模板函数有两个bug:

1)当a、b两个值的类型不一样的时候,怎么传入两个值不一样的类型? 解决方法:在模板里定义两个参数即可

2)add函数返回值类型并不是由a或者b变量类型去决定的,而是由a+b表达式的类型决定。

解决方法:用decltype关键字在编译期静态推导表达式的类型。

image-20250920235410584

template<typename T1, typename T2>          //使用两个参数
auto add(T1 a, T2 b) -> decltype(a + b) {	//使用decltype判断表达式类型,且配合auto
    return a + b;
}

补充:auto 推导的是值的类型,decltype 推导的是表达式的声明类型

  • C++11环境:必须使用auto add(T1 a, T2 b) -> decltype(a + b),因无C++14的自动推导支持。
  • C++14及以上:若函数体简单且返回类型明确(如单一return a + b;),可用auto add(T1 a, T2 b),编译器能正确推导。若需强调返回类型与表达式的严格关联,或存在多return语句/复杂类型,仍推荐-> decltype(...)以提高代码清晰度与健壮性。

3.深入探索:模板的类型推导机制

隐式推导:让编译器自己去判断模板的参数类型(隐式推导流程:1.匹配模板 2.类型推导)

示例:

image-20250921142644503

显示推导:在调用模板函数的过程中,明确指定模板的参数类型。

示例:

image-20250921155953619

间接推导:通过中间表达式或类型转换间接确定类型。(当模板作为参数的时候,被迫进行推导参数。)

示例:

image-20250921161444663

引用类型的推导:引用类型的推导因其特殊性(如左值引用、右值引用、auto 的行为)需要单独讨论。

通用引用模板既可传入左值,也可传入右值。通用引用模板会触发引用折叠:&超过2个时会对2取余

示例:

image-20250921164115339

4.语法基础:模板类

定义模板类:

image-20250924232754600

示例:

image-20250924232455117

补充:上图重载<<运算符中也利用了模板函数间接推导的特性,可以输出任意类型的Point类

定义模板类+模板函数:

image-20250924234419642

示例:

image-20250924234318154

5.全特化与偏特化

特化是基于原模版的一个功能拓展(可理解为原模版的一个补丁),在原模板没有办法实现相应功能时使用。

全特化:没有模板参数的模板就是全特化模板

image-20250925002348030

示例:

image-20250925001418019

偏特化:原模版扩展出来的有参数的模板

示例:

image-20250925003619941

函数模板不支持偏特化:因为会破坏函数重载的优先级规则,导致歧义性和复杂性。

image-20250925002427873

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐