CPP LEARNING
1. new
- 使用new来开辟内存空间 delete来删除已开辟的空间
- new返回的是该数据类型的指针
- 使用delete释放数组时需要用 delete[]
2.引用
- 引用必须要初始化
- 初始化后引用就不能改了
- 不要返回局部变量的引用
- 返回值是引用的函数可以作左值
- 引用的本质在C++内部的实现就是一个指针常量
- 可以用const修饰防止形参修改实参
3.函数
1. 函数的默认参数
- C++的函数可以有默认参数,但是从第一个默认参数开始,后面的参数都必须有默认值
- 函数的声明和实现只能有一个有默认参数
2.函数的重载
- C++的函数可以同名,提高函数的复用性。
- 函数的重载必须在同一个作用域下,且名称相同。
- 函数的重载要求满足函数的参数或者类型不同,或者个数不同,或者顺序不同。 函数的返回值不能做函数重载的条件
- 引用可以做函数重载的条件 (const修饰与无const修饰) 传入常量区数据,调用const引用,传入其他数据,调用非const引用。
5.当重载函数有默认参数时,默认参数可能会无法重载
1 |
|
func2(int a,int b = 10)
具有一个默认参数,这使得func2(10)具有二义性
类和对象
C++ 面向对象三大特性 封装、继承、多态
1. 封装
将属性和行为写在一起,并加入一些权限控制。 ———— 类
与struct
不同的是 类——class
的权限是默认私有的。
通过一个类来创建一个对象的过程称为 “实例化” 。
访问权限
- 公共 成员在类内可以访问 类外也可以访问
- 保护 成员在类内可以访问 类外不可以访问 父类的保护权限,子可以访问
- 私有 成员在类内可以访问 类外不可以访问 父类的私有权限,子也不可以访问 (继承)
构造函数和析构函数
使用构造函数
来初始化,析构函数
来清理。 构造函数和析构函数由编译器自动调用 但如果不自己实现,调用的是空实现。
构造函数
不需要写返回值类型,函数名称与类相同,可以有参数,可以发生重载。
在创建对象时,程序会自动调用构造函数,不需要手动调用,并且只会调用一次
析构函数
没有返回值,没有参数,函数名前要加上~
由于不可以有参数,所以不能发生重载。
和构造函数一样,析构函数会在对象销毁前自动调用析构函数。
构造和析构函数都是必须要有的 如果自己不提供,程序会提供空实现
构造函数的分类
按参数:有参构造和无参构造
按类型:普通构造和拷贝构造
构造函数的调用方式
- 括号法
person p(10);
- 显示法
person p = person(10);
- 隐式转换法
person p = {10,"name"};
person(10)
创建的是一个匿名对象,匿名对象在当前行结束后就会被释放。显示法相当于给匿名对象找到一个名字。
不要利用拷贝构造函数来初始化一个匿名的对象。
拷贝构造函数的调用时机
C++中拷贝构造函数的调用时机通常有三种情况
- 使用一个已经创建完的对象来初始化一个新对象
person p(p0);
- 以值传递的方式给函数参数传参
doWorK(p);
值传递的本质会拷贝一个临时副本 这个过程会调用拷贝构造函数 - 以值方式返回局部对象
1 | Person func() |
函数内的p1在函数运行完就会被是放掉,return 的p1是创建的新变量。
构造函数的调用规则
C++会为一个类提供至少三个默认函数
- 默认构造函数(空实现)
- 默认析构函数(空实现)
- 默认拷贝构造函数 => 有默认内容(值拷贝)
如果你写了一个有参构造函数,C++不会提供默认无参构造,但会提供拷贝构造函数
如果你写了一个拷贝构造函数,C++就不会再提供默认的其他构造函数了
深浅拷贝
深拷贝 重新申请空间
浅拷贝 直接赋值
初始化列表
传统操作:
1 | Person(int a_,int b_,int c_) |
1 | Person():a(10),b(20),c(30){} //行参列表 |
1 | Person(int a_,int b_,int c_):a(a_),b(b_),c(c_){} //更灵活的写法 |
类对象作为类成员
1 | class A{}; |
当A作为B的成员时,创建一个类B的对象,会先创建一个类A的对象 [先有手机后有人]
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段就分配内存 (还没有运行可执行文件前已经分好了)
- 类内声明,类外初始化 (必需操作) (static修饰的成员变量在类外初始化时分配内存) (静态成员并不具体作用在某个对象上)
- 静态成员不能在类内初始化
- C++中声明和定义是有区别的,在类内进行的是成员变量的声明,而不是定义。所以在类外初始化静态成员变量时仍然要加上数据类型。
- 由于静态成员变量不属于某个类,所以他有两种访问方式
- 1.通过对象访问
- 2.通过类名访问
1 | person::static_member_ |
- 静态成员变量也是有访问权限的 (类外访问不到静态成员变量的内容)
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
成员函数的大小并不算在类里!成员变量和成员函数在存储上是分离的
静态成员变量的调用有两种访问方式
- 通过对象
- 通过类名
静态成员函数只能访问静态成员变量
- 静态成员函数也是有访问权限的
C++对象模型和this指针
C++中 只有非静态成员变量才属于类的对象上
空对象占用的内存大小为 1 字节
C++编译器会给每个空对象也分配一个字节的空间,是为了区分每个对象占用的位置
不是存储对象的地址,而是把对象的地址占用住,防止别的对象存在同一地址
在C++中,空结构体的大小也是 1<br$$>
在C语言中 空结构体的大小是 0
static成员变量和对象中的成员变量是分开存储的,不算在对象的大小里 (存储在全局区)
this
指针
非静态成员函数的实例只有一份,它通过this指针来区分是哪个对象调用的自己。
用途
- 当形参和成员变量同名时,可以用this指针来区分
- 在类的非静态成员函数中返回对象本身,可用 return *this
静态成员函数不具备this
指针
空指针访问成员函数
C++的空指针也是可以访问成员函数的
但是要判断this指针是否为空 加强代码的健壮性
const修饰成员函数
常函数:
- 成员函数后加
const
的函数叫做常函数 - 常函数内不可以修改成员属性
- 成员属性声明时加关键字
mutable
后,在常函数中依然可以修改
常对象:
- 在声明对象时,在对象前面加
const
来定义一个常对象 - 常对象只能调用常函数
mutable
在常对象下也适用
在成员函数后加const修饰的其实是this指针,让指针指向的值也不可以修改
常对象只能调用常函数的原理:常对象的this
指针是常量指针,而this
指针是作为隐含参数
传给类内函数的,只有常函数的this
指针是常量指针常量,可以接收常对象的this
指针 (其实就是普通指针可以向const修饰的指针转换,而const修饰的指针不能向普通指针转换)
友元 friend
通过关键字friend
可以让类外访问类内的私有属性 可以让一个函数或者类 访问另一个类中的私有成员
1 | class Friend{}; |
运算符重载
可以对已经存在的运算符进行重新定义,赋予另一种功能
- 加号运算符重载
- 左移运算符重载
- 递增运算符重载
- 赋值运算符重载
- 关系运算符重载
- 函数调用运算符重载 (仿函数)
运算符可以以全局函数或成员函数的形式重载,拓展更多使用方法
cout是 ostream 类的一个对象 通过成员函数 operator<<
来输出内容1
my_integer& operator++(int) //在()里写int以表明这是一个后置递增
2. 继承
基本语法
继承可以减少重复的代码。
1 | class Base // 父类 (基类) |
类 byBase
会继承 类 Base
的函数 func()
从而拥有两个成员函数 func()
和 func2()
继承方式
1 | protected(受保护)成员变量或函数与私有成员十分相似,但有一点不同,protected(受保护)成员在派生类(即子类)中是可访问的。 |
使用共有继承方式,最大权限为公有
使用保护继承方式,最大权限为保护
使用私有继承方式,最大权限为私有
以私有方式继承,子类的子类什么都访问不到
继承的对象模型
不管怎样继承,父类中的非静态成员属性都会被子类继承下去。
每个子类都会有一份继承自父类的成员
构造和析构顺序
当创建子类对象时,先有父类对象还是子类对象?
先构造 父类 , 后构造 子类 。
析构的顺序和构造的顺序相反。
继承中同名成员的处理方式
在继承中遇到同名成员变量 需要在.
后加父类的作用域
1 | s.Base::a = 10; // 表示使用的是继承自父类的成员变量 |
对于成员函数 同理
1 | s.Base::func(); |
如果子类中出现了和父类中同名的成员函数,那么子类会将父类中的所有成员函数隐藏掉,包括重载的函数
必须加作用域才可以访问。
继承中静态成员变量的处理方式
静态成员变量可以通过类名访问。
1 | // 通过子类访问父类继承的静态成员变量 |
C++多继承
C++允许一个类继承多个类
当父类中出现同名成员 需要加作用域区分
在开发中不建议采用多继承写法
菱形继承 (virtual)
两个派生类继承同一个基类,又被同一个派生类继承。
1 | A |
E通过C,D继承A的成员时,只需要一份,却继承了两份,造成资源浪费
此时可以通过虚继承
实现
继承时加上virtual
后,继承方式变为虚继承 , 数据只存一份
并且虚继承默认访问的是第一个基类
虚继承的派生类中存的是vbptr
『虚基类指针』 指向 vbtable
『虚基类表』 记录了派生类成员相对基类成员的偏移量
3. 多态
静态多态和动态多态
静态多态:函数重载和运算符重载
动态多态:派生类和虚函数来实现动态多态
区别:
- 静态多态的函数地址早绑定 => 在编译阶段就确定了函数的地址
- 动态多态的函数地址晚绑定 => 程序运行时才能确定函数地址
C++允许父类的引用指向一个子类对象 不需要进行类型转换
通过在基类的函数前加入关键字virtual
,可以实现地址晚绑定,使一个函数表现出不同的效果
动态多态的条件:
- 有基础关系
- 子类要重写父类的虚函数
1 | 重写:函数的返回值,形参和函数名都要相同 |
动态多态的使用: 要用父类的指针或引用指向子类的对象 调用虚函数
多态的原理
有虚函数的基类中会存一个被称为vfptr
的指针 即虚函数指针
(aka虚函数表指针)
它会指向一个虚函数表vftable
该表内部会记录一个虚函数的地址
子类会继承父类的vfptr
指向自己的虚函数表 父类和子类不会共享一个虚函数表
类的对象的隐藏成员保存的类的虚函数指针(vfptr),指向类的虚函数表 虚函数表是一个顺序表 表中有许多槽(slot),每个槽中存放的是一个虚函数的指针(地址)
如果派生类重写了基类的虚函数,派生虚函数表将基类虚函数的地址替换为派生类虚函数的地址,
如果派生类中没有重写虚函数,派生虚函数表将保存基类虚函数的地址。如果派生类中还定义了虚函数,那么该虚函数的地址也会被添加到虚派生类表里去。
为了保证虚函数表的性能,C++会保证虚函数表的指针存在于对象实例的最前面的位置。
当父类的指针或者引用指向子类对象时,就会发生多态
在创建派生类的对象时,虚函数指针会被指向派生类虚函数表。
纯虚函数和抽象类
多态中父类的虚函数大多没什么用,可以将虚函数改为纯虚函数。
1 | virtual int func(int a,int b) = 0; // 定义一个纯虚函数 |
只要类中有一个纯虚函数,类就会成为抽象类
无法实例化一个对象
抽象类的子类必须重写抽象类的纯虚函数,否则该子类也属于抽象类,无法实例化对象
虚析构和纯虚析构
多态使用时,父类的指针无法释放子类中的析构代码,所以子类数据无法释放,会造成内存泄漏。
此时需要利用虚析构
和纯虚析构
虚析构和纯虚析构都需要函数的实现
如果类中有了纯虚析构,该类也会属于抽象类
只要在父类的析构函数前加上virtual
即可调用子类的析构函数
纯虚析构也能有这个效果,但是纯虚析构必须在类外实现函数体
1 | Animal::~Animal() |
泛型和STL
C++支持泛型编程
和 STL
泛型编程
主要靠模板
实现,可大大提高复用性。
模板
- 模板不可以直接使用,它只是一个框架。
- 模板的通用并不是万能的。
模板的语法
- 函数模板
- 类模板
函数模板
语法:
1 | template<typename T> |
写模板时可以先不指定返回值和形参是什么数据类型 使用的时候再确定数据类型
template: 声明创建模板
typename: 表明T是数据类型的名称 可以用class替代
T 是可以替换的数据类型 (自定义)
***36_STL
STL
STL
即 Standard Template Library
标准模板库
STL 从广义上分为 容器(container)
算法(algorithm)
迭代器(iterator)
迭代器连接了容器与算法
STL中的所有类基本上都使用了模板类和模板函数
STL的六大组件 – 容器、算法、迭代器、仿函数、适配器、空间配置器
- 容器 是各种数据结构 用来存储数据
- 算法 各种常用的算法 (sort,find,copy,for_each)
- 迭代器 迭代器 交流容器和算法
- 仿函数 函数调用运算符的重载
- 适配器 用来修饰容器或者仿函数或迭代器接口
- 空间配置器 负责空间的配置和管理
容器分为序列式容器和关联式容器
序列式容器强调值的排序,每个元素都有固定的位置
关联式容器各元素之间没有严格的物理上的顺序关系
算法分为质变算法和非质变算法
质变算法在运行期间会更改元素的内容 (增,删,改)
非质变算法在运行期间不会改变元素的内容 (查找,计数)
算法要通过迭代器才能访问容器里的元素 每种容器都有自己专属的迭代器
迭代器非常类似于指针
迭代器的种类: 输入、输出、向前、双向、随机访问
输入迭代器
对数据进行只读访问
输出迭代器
对数据进行只写访问
前向迭代器
可以向前推进,并且可以进行读写操作
**双向迭代器
**可以进行读写操作,并且可以向前和向后操作
**随机访问迭代器
**支持读写操作,并且可以以跳跃的方式访问任意数据
vector容器
vector容器可以理解为一个数组。
string容器
封装了C语言的字符串