C++和Java一样是基于面向对象的(面向过程和面向对象混编 —— 因为C++兼容C),关注的是类对象以及对象间的关系,将一件事情拆分成不同的对象,靠对象之间的交互完成,体现到代码层面就是类的设计及类的关系
this指针
类的引入
C++兼容了C中结构体的用法,同时
struct在C++中也升级成了类C++类和结构体不同的是,除了可以定义变量,还可以定义方法/函数
用结构体定义的学生类
|
|
对于上面结构体的定义,C++中更喜欢用class来代替
|
|
调用约定*
C++新的调用约定__thiscall
- 32位系统(x86)
类成员函数:通常使用
__thiscall约定
this指针通过 ECX 寄存器 传递- 其他参数从右向左压栈
- 被调用函数负责清理栈
- 64位系统(x64)
- 统一使用 快速调用约定:
this指针作为隐含的第一个参数,通过 RCX 寄存器 传递- 前4个参数使用寄存器(RCX, RDX, R8, R9),其余压栈
- 调用者负责清理栈空间
类的访问限定符和封装
封装
众所周知,【面向对象的三大特性】:封装、继承、多态
在类和对象阶段,我们只研究类的封装特性,什么是封装?
- 把属性和方法都封装到一个类里面
- 访问限定符 —— 可以给其他类访问的成员定义成公有,不想被其他类访问的成员定义成私有/保护
访问限定符*
C++实现封装的方式,用类将对象的属性和方法结合在一起,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
- 公有:
public修饰的成员所以类均可访问 - 私有和保护:
private修饰的成员仅类内部访问,protected类内部和子类可访问 - class的默认访问权限为
private,struct的为public(因为struct要兼容C) —— 一般定义类的时候,建议明确访问修饰限定符,不要指着class/struct默认限定符 - 访问权限的作用域从这个访问限定符出现的位置开始,到下一个访问限定符出现为止
类的定义*
类定义了一个新的作用域。类的所有成员都在类的作用域中;在类外定义成员,需要::作用解析符指明成员属于哪个类域。
我们可以在头文件中声明一个类,并在.cpp文件中定义类
mystack.h
|
|
mytest.cpp
|
|
类及对象大小
我们用下面代码查看(64位编译器指针为8B,32位为4B)
|
|
计算类的对象的大小 —— 只看成员变量,且要考虑内存对齐,C++内存对齐规则和C一样
- 注意,空类比较特殊,编译器给了空类一个字节来标识这个类。不存储有效数据,只是为了空间占位,标识对象存在
内存对齐
可以通过#pragma pack(n)来设定变量以n字节对齐方式
类对象的存储:和Java一样,C++只保存成员变量,成员函数放在公共的代码段
-
第一个成员在与结构体偏移量为0的地址处。
-
其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。(VS中默认的对齐数为8)
-
结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
类成员函数的this指针
先定义一个日期类
|
|
C++通过引入this指针:C++编译器给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成,this指针本身不能被修改
类的默认成员函数
任何一个类即使是空类,都会自动生成下面6个默认成员函数,这就是C++比较复杂的初始化机制
构造函数*
构造函数是特殊的成员函数。注意,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象**,而是**初始化对象
构造函数特点
- 函数名和类名相同
- 无返回值
- 对象实例化时,编译器自动调用对应的构造函数
- 构造函数可以重载
|
|
但是如果在类中没有写构造函数,则编译器会默认生成一个无参构造函数
默认构造函数有三种 —— 无参构造函数、全缺省构造函数、编译器默认生成的构造函数,而一个类只能有一个
析构函数*
与构造函数功能相反,对象在销毁时会自动调用析构函数,完成对象的一些资源清理工作
析构函数特点
- 析构函数名是在类名前加上字符
~ - 无参数无返回值
- 一个类只能有一个析构函数,不能重载
- 对象生命周期结束时,编译器自动调用析构函数
同样以Data类为例:
|
|
当然,该类中的成员都是在栈上的,不需要清理资源;若有数组成员(在堆上分配空间),则需要析构函数了,比如前面实现的栈类
|
|
注意:若是struct | class这种自定义类型,不需要实现析构函数,编译器默认生成
拷贝构造函数
运算符重载*
运算符重载(Operator Overloading)是 C++ 中允许程序员重新定义已有运算符的行为,使其适用于自定义类型(类或结构体)的特性。这使得自定义类型可以像内置类型一样使用运算符,提高代码的可读性和自然性
运算符重载的基本规则
- 不能创建新运算符:只能重载 C++ 中已有的运算符
- 不能改变运算符优先级:重载后运算符的优先级不变
- 不能改变操作数个数:单目运算符只能有一个操作数,双目运算符两个
- 至少有一个操作数是用户定义类型
- 部分运算符不能重载:
::,.*,.,?:,sizeof,typeid
两种实现方式
- 成员函数形式
|
|
- 非成员函数形式(常为友元)
|
|
常用运算符重载实例
- 算术运算符:
+,-,*,/
|
|
- 比较运算符:
==,!=,<,>
|
|
- 流运算符:
<<,>>
|
|
- 下标运算符:
[]
|
|
- 函数调用运算符:
()
|
|
- 自增/自减运算符:
++,--
|
|
继承*
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,减少重复代码的编写
子类会继承父类(public 和 protected)修饰的成员,因此子类可以赋值给父类
子类的构造函数会调用父类的构造函数
|
|
Java支持多层继承,但不支持多重继承;而C++既支持多层继承,也支持多重继承
从底层角度来看,继承的本质就是复制,private成员在继承同样被复制到子类,但是无法直接使用,可以用指针访问
|
|
这里了解即可,以后需要用再学,思想应该和Java差不多
多态*
多态分为两类
- 静态的多态:函数重载,在编译时决定
- 动态的多态:一个父类的引用或指针调用同一个函数,传递不同的对象,会调用不同的函数,即函数重写,如子类重写父类的方法,在运行时决定
多态有两个条件
-
必须通过基类的指针或者引用调用虚函数,对象没有多态
-
被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写,如:父类的析构函数应定义成虚函数
虚函数:被
virtual修饰的类成员函数(准确的说,只有类的非静态成员函数才能是虚函数,其他函数不能成为虚函数)
虚函数*
当类中有虚函数时,会在当前对象this指针最前面多一个属性,是一个4字节地址,指向虚函数表(存储了所有虚函数地址),有几个直接父类就有几个虚函数表
通过对象调用时,virtual函数和普通函数都是直接调用(E8 …)
如果用指针调用虚函数,是间接调用(call [在该地址取值] -> FF …)
打印虚函数表并调用验证
|
|
练习:
1.单继承无函数覆盖(重写?)(打印Sub对象的虚函数表)
2.单继承有重写(打印Sub对象的虚函数表)
|
|

3.多继承无重写
|
|

4.多继承有重写
|
|

5.多层继承无重写(类似嵌套单继承,子类重写父类的方法,即覆盖对应虚函数地址)
6.多层继承有重写
重载&重写
绑定(Binding)指的是将标识符(如变量名、函数名)与内存中的实际存储位置或具体实现关联起来的过程,绑定本质是建立映射关系,根据发生的时机不同,绑定主要分为两大类:
一、静态绑定
定义:在编译期间确定标识符与具体实现的关联
特点:
- 编译时即确定调用关系
- 高效但缺乏灵活性
- 基于变量/指针的声明类型
应用场景:
- 普通函数调用
- 非虚成员函数调用
- 重载函数选择
- 模板实例化
- 运算符重载
二、动态绑定
定义:在运行期间确定标识符与具体实现的关联
特点:
- 运行时动态确定调用关系
- 通过虚函数机制实现
- 基于对象的实际类型
C++中,只有virtual函数是动态绑定的,应用:多态、接口实现、设计模式
绑定是多态的实现基础,即重载和重写
重载属于编译时多态,如:
|
|
特点:
- 相同作用域(同一个类)
- 函数名相同,参数不同
- 编译器根据调用参数静态绑定具体函数
重写属于运行时多态,如:
|
|
特点:
- 不同作用域(基类与派生类)
- 函数签名完全相同
- 通过虚函数表动态绑定具体实现

模板
C++模板是实现编译时多态的核心机制,它允许开发者编写与类型无关的通用代码,编译器在编译期间根据具体类型生成特化版本
1.函数模板
|
|
2.类模板
|
|
例1:模板实现通用 Swap 函数
|
|
例2:模板实现通用二分查找算法
|
|
引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间
- 引用是变量的别名:引用不是新变量,而是已有变量的另一个名称
- 引用必须初始化:声明时必须绑定到一个已存在的变量
- 引用不可重新绑定:一旦初始化后,不能改变其引用的对象
- 不占用额外内存(通常):引用本身不占用存储空间(编译器实现细节)
引用在函数中的应用
- 避免拷贝
|
|
- 修改函数参数(替代指针)
|
|
- 返回引用
|
|
高级引用用法
- const 引用(只读访问)
|
|
- 引用与范围 for 循环
|
|
- 右值引用(C++11 新特性)
|
|
友元
友元是 C++ 中打破封装性的一种特殊机制,它允许特定的外部函数或类访问另一个类的私有(private)和保护(protected)成员
友元关系的核心特点:
- 授权访问:被声明为友元的实体可以访问类的私有和保护成员
- 单向性:友元关系是单向的(A 是 B 的友元 ≠ B 是 A 的友元)
- 非传递性:友元关系不传递(A 是 B 的友元,B 是 C 的友元 ≠ A 是 C 的友元)
- 不继承:友元关系不被派生类继承
声明位置:
|
|
友元的三种形式
- 友元函数(没有this指针)
|
|
- 友元类
|
|
- 友元成员函数
|
|
友元的使用场景
- 运算符重载
|
|
- 流运算符重载
|
|
- 紧密协作的类
|
|
new-delete
new和delete是 C++ 中用于动态内存管理的核心运算符,它们提供了在程序运行时分配和释放内存的能力。与 C 语言中的malloc和free不同,new和delete是类型安全的,并且会调用对象的构造函数和析构函数
new 运算符
new是在堆中创建对象
- 基本用法
|
|
- 内存分配过程

- new 的变体
(1) 定位 new(Placement new)
|
|
(2) nothrow new
|
|
delete 运算符
- 基本用法
|
|
- 内存释放过程

参考