C++核心语法整理
C++核心语法整理
C与C++

C++程序介绍
g++编译器安装
- sudo apt install g++
源文件名称
- .cc
- .cpp
C++程序模板设置
- /home/st/.vim/plugged/prepare-code/snippet
hello world程序分析
#include C++标准库中头文件 没有.h
cin
标准输入流
- 默认输入设备 从键盘接收数据
cout
标准输出流
- 默认输出设备 屏幕
int main(int argc , char *argv[]){}
返回值为int
argc
- 命令行参数的个数
argv
- 具体的命令行参数
vim中启动鼠标
- 编辑.vimrc文件
- 子主题
命名空间
命名空间是什么, 有什么作用?
C++中的一种避免名字冲突的机制 主要作用区分同名实体
实体
- 变量、常量、函数、结构体、类、对象、模板、命名空间等
基本语法
- namespace wd{
// 变量
// 函数
}
- namespace wd{
命名空间如何使用
方式一:使用作用域限定符::
命名空间名字::实体
- 使用稍微麻烦 每次都要加wd::
方式二:使用using编译指令
using namespace 命名空间名;
- 要清楚命名空间中都有什么
方式三:使用using声明机制
using 命名空间名::实体
- 用哪个实体 声明哪个
- using std::cout
- using std::endl;
注意事项
- using编译指令尽量写在局部作用域 , 这样using编译指令的效果也会在其作用域结束时结束
- 采用using编译指令使用命名空间中的实体时,要注意避免命名空间中实体与全局位置实体同名。
- 在不清楚命名空间中实体的具体情况时,尽量不使用using编译指令
- 在同一作用域内用using声明机制, 不同的命名空间的实体,不能是同名的,否则会发生冲突。
特殊的命名空间
嵌套命名空间
- 命名空间里面嵌套一个命名空间
- namespace outer{
// 变量
// 函数....
namespace inner{
// 变量
// 函数...
}
} - 可以使用任意方式访问
匿名命名空间(了解)
没有名字的命名空间
1.可以直接使用 2.可以使用作用域限定符:::实体
注意:
- 不要定义与匿名命名空间同名的全局实体
- 匿名命名空间不能跨模块调用
跨模块调用问题
什么是模块?
- 一个.c/.cc/.cpp文件
哪些结构可以跨模块
- 1.有名字的命名空间
- 2.全局的函数 变量
如何跨模块
通过一个关键字extern
对于全局实体 A.cc B.cc
- 在B.cc中引入A.cc的全局实体 注意名字别写错了
窗口进行切换 使用tab
使用方式
1.引入全局的实体
- extern int gNum
- extern void func()
2.引入命名空间的实体
- 定义一个同名的命名空间--->多个命名空间被视作同一个命名空间
- 在命名空间中通过extern引入
注意:
当某个模块中全局的实体跟命名空间中的实体同名,
使用extern时- 直接访问的是全局实体
- 通过::访问的是命名空间的实体
命名空间的扩展
- 同一个文件中 同名的命名空间认为是同一个命名空间
头文件规范
- 先放自定义的头文件 放C语言中的头文件 放C++中的头文件 放第三方的头文件
const
回忆一下C语言中如何使用常量
- C中通常使用宏 #define NUMBER 1
- 宏的本质其实是文本替换
- C++中使用const 替代宏
const修饰内置类型
基本语法
- 使用const关键字修饰变量--->常量
- const int num = 1;
- int const num = 2;
特点
常量 不能修改
- 具有只读属性
宏 VS const
发生的时机不同
- 宏是预处理
- const编译时
是否安全检查
- 宏没有类型检查 纯文本替换
- const是有类型检查
const修饰指针类型
根据const位置不同 修饰指针的不同形式
- const int * p
- int const * p
- int * const p
const修饰指针的不同类型
指向常量的指针 pointer to constant
const int *p
int const *p
const 在*左边
特点:
- 不能通过指针指向修改数据 但是可以修改修改指针的指向
- 子主题
常量指针 constant pointer
int * const p
const在*的右边
特点
- 可以通过指针修改数据 但是不能修改指针的指向
- 子主题
特殊情况
变量本身是一个常量
- const int num = 1
- 不能使用普通指针指向num 需要使用带const的指针 const int *p
双重const限定的指针
- 既不能修改指针指向 又不能他通过指针修改数据
- 子主题
补充相似概念
数组指针 指针数组
数组指针
pointer to array
- 本身是个指针 指向了一个数组
- int (*p)[3]
指针数组
array of pointers
- 本身是个数组 里面的元素是一个个指针
- int *p[3]
函数指针 指针函数
函数指针
pointer to function
- 本身是一个指针 指针指向函数
- 函数返回值类型 (*函数指针名) (形参列表) = &func;
指针函数
function return a pointer
- 函数的返回值是一个指针
- int * func() { }
const 修饰 函数
- 函数
- 返回值
const 修饰引用
const 修饰入参
new与delete运算符
C语言中的动态内存分配
基本使用步骤
- malloc进行内存分配 ----> void *
- void * 强转为相应类型的指针
- 初始化
- 使用
- free回收空间
malloc
free
如果malloc free 报错
- home目录下 打开该文件设置一下
C++中的动态内存分配
new
一般类型
- new int()
- new int(1)
- new int{}
- new int{100}
数组类型
- new int 3
- new int[3]{ }
- new int[3] { 1, 2, 3}
- 可以填变量
delete
一般类型
- delete p
数组类型
- delete [] p
注意安全回收
- 给指针置空 nullptr
valgrind工具安装
安装
- sudo apt install valgrind
使用
- valgrind --tool=memcheck ./a.out
简化配置
vim .bashrc增加配置信息
- alias memcheck='valgrind --tool=memcheck --leak-check=full --show-reachable=yes'
重新加载 source .bashrc
简化使用
- 直接memcheck ./a.out
几个参数信息
- (1)definitely lost: 绝对泄漏了;
(2)indirectly lost: 间接泄漏了;
(3)possibly lost: 可能泄漏了,基本不会出现;
(4)still reachable: 没有被回收,但是不确定要不要回收;
(5)suppressed :被编译器自动回收了,不用管
malloc/free VS new/delete
- malloc 指定空间大小 new不用
- malloc返回的是void* 还需要强转 使用new不需要强转 直接就可以使用相应类型的指针接收
- malloc / free 库函数 new / delete 运算符
- 3对用法
- malloc / free
- new / delete
- new xx[] / delete []
- 安全回收
- C NULL
- C++ nullptr
引用
什么是引用
- 一个已存在的变量或者对象的别名
基本语法
- 变量的类型 & 引用名 = num
- &在这里不是取地址 引用符号
- 引用的类型要和变量的类型保持一致
- 引用一经绑定就不能修改
引用本质
- 操作受限的指针 常量指针
引用和指针的联系
联系
- 普通指针 特殊指针(常量指针)
- 间接访问
区别
- 指针只声明 不初始化 引用必须要初始化
- 引用取地址--->所绑定变量的地址 指针取地址--->指针变量的地址
使用场景
引用作为函数参数
普通变量作为函数参数
值传递
- 做不到通过形参修改实参
指针作为函数参数
地址传递
- 做到通过形参修改实参
引用作为函数参数
引用传递
通过形参修改实参-->相当于通过别名操作本体
更推荐使用引用
- 1.可以起到跟指针相同的效果
- 2.使用比指针简单
引用作为函数返回值
int func(){ return xx}
- return语句会进行一个copy
int * func(){return xxx地址}
- 不会进行copy
int & func() { return }
- 不会进行copy 操作简单
小结
注意
- 函数不要返回一个局部变量的引用 因为生命周期的问题 函数执行完 局部变量就销毁了
- 函数尽量不要返回一个堆上的变量, 妥善空间回收 否则可能会有内存的泄漏
- const修饰的引用--->常引用 ---> 不希望通过函数的形参 去修改实参数据的时候 ---> 使用const int & ref
强制转换
C语言中的强制转换
- (目标类型)待转化的目标变量
C++中的强制转换
static_cast
基本语法
- static_cast(待转化的目标)
基本数据类型之间的转换
指针类型的转换
- void*与其他类型指针的转换
- 其他任意两个指针类型不能转换
好处
- 查找方便 grep -rn "static_cast" ./ 查找那个具体文件中使用了强制转换,要比C中的强转方便一些.
const_cast(了解)
基本语法
- const_cast(待转化的目标)
基本作用
- 去除const属性
基本使用
将指向const常量的指针转换为普通指针
- 转换后再操作 可能出现未定义行为
指向常量的指针指向的是一个普通变量 可以使用
将常引用转换为非常量引用
dynamic_cast
- 继承部分用
reinterpret_cast
- 作用跟C语言中的强转一样
函数重载
什么是函数重载?
- 同一作用域内,函数名相同,形参不同,功能相似, 输入数据不同的一组函数
- void add(int , int)
- void add(double, double)
函数重载的意义?
- 同一函数名可以作用于不同的数据类型或者参数组合,适用于处理相似功能,
但是输入类型不同的情况,这样做减少了函数名的数量,对于程序的可读性有很大的好处。
- 同一函数名可以作用于不同的数据类型或者参数组合,适用于处理相似功能,
函数重载的规则/条件?
1.函数名必须相同
2.形参列表不同
- 1.参数个数
- 2.参数类型
- 3.个数类型相同 可以通过位置顺序区分
注意
- 函数重载跟返回值没有关系
- 避免二义性,编译器无法确定调用哪个函数 涉及到隐式转换的时候
函数重载的原理
名字改编机制 name mangling
生成.o文件
- nm xxx.o
- printi
- printd
extern
有些代码希望用C方式编译
- extern "C" {
// xxxxx
// C的代码
}
- extern "C" {
函数的默认参数
什么是函数的默认参数?
- 定义函数的时候 给形参设置一个默认值
函数默认参数的目的
- 1.可以进行缺省的调用
- 2.减少重载
默认参数的声明
声明和实现可以分类写
- 建议把默认参数设置在声明位置 实现位置就不加默认参数了
- 声明和实现都加了默认值----> error 重复定义
默认参数的顺序要求
- 设置函数的参数的默认值时, 从最右边那个参数开始 往左设置
注意:
如果有函数重载, 同时使用了默认参数
- 可能会有二义性的问题
布尔类型介绍
bool类型
- true 1
- false 0
- 使用cout打印的时候, true ---> 1 false--->0
大小
- 1个字节
bool类型跟数值类型之间的转换
- 数值为0 ----> false
- 非0的数据---->true
内联函数
什么是内联函数
- inline void func(){}
内联函数原理
- 主要是为了替代宏 主要在函数调用时 用函数体进行替换
适用场景
- 函数代码比较简洁 简短
宏 VS 内联函数
- 主要--->发生的时机不一样
内联函数注意事项
函数声明与定义分开写的时候, 在同一源文件时,建议前面都加inline
函数声明在头文件时,函数的定义也要在头文件中
- 如果放到了2个文件中 ---> undefined referencexxxx
- 1.要么把内联函数实现直接写到.hpp头文件
- 2.或者在.hpp头文件中把.cc包含进来 #include "print.cc"
异常处理(仅了解)
什么异常?
- 描述程序运行中的错误
异常处理
- 是处理异常的机制
关键字
throw
- 抛出异常
try
- 关键字 try{ 可能出现的异常的代码}
catch
- 捕获异常
基本使用
- try{
}catch(){
}catch(){
}...
- try{
执行逻辑
- 如果try中有异常, 就会执行异常处理逻辑
- 从上到下匹配catch 如果匹配成功 ----> 进入到相应的catch中执行具体的内容
- 从catch出来后, 接着catch后面继续执行
不推荐使用
内存布局(32位)
分类
内核态
- 对用户空间不可见
用户态(由高地址到低地址)
栈区
- 操作系统控制
堆区
- 程序员分配
- new / malloc
全局静态区
- 读写段
- 全局变量/ 静态变量
文字常量区
只读段
字符串常量
- "hello"
程序代码区
- 只读段
- 函数二进制代码
细节: 编译器的优化->后定义的局部变量的地址高于先定义的局部变量
C风格字符串
两种形式
字符数组
- char str[6] = "hello"
- char str2[6] = {'', '', '', '', '', '\0'}
字符指针
- 需要使用const char * p 去指向字符串字面值常量 (C++中的标准)
常规操作
复制
new开辟空间
- strlen() + 1
strcpy
拼接
- new开辟空间
- strcpy
- strcat
注意:
- 涉及到堆空间 delete nullptr
类与对象

面向对象思想介绍
封装
概念
- 封装是指将数据和操作数据的方法绑定在一起,形成一个独立的单元,
同时对外部隐藏对象的内部实现细节。
- 封装是指将数据和操作数据的方法绑定在一起,形成一个独立的单元,
数据隐藏
- 借助于权限修饰符private
隐藏内部的实现细节
提供外界访问的入口
- 可以提供权限为public的方法
- 读操作 getXXX()方法
- 写操作 setXXX(参数)方法
继承
- 为了成员的复用
- 让子类去扩展父类
多态
- 建立在继承的基础上 不同的子类对象 在同一场景表现出不同的行为
类的声明定义
声明
- class 类名(自定义);
定义
基本语法
- class 类名{// 成员函数 数据成员...};
成员函数
不同个体的共有的行为的集合
声明和实现可以放一起
- 默认是内联函数
声明和实现也可以分开
可以在同一个文件
- 把具体的实现写在类的外部 要使用类名::作用域
- void Point::print(){ .....}
分成头文件 实现文件
数据成员
- 不同个体中共有的属性的集合
访问权限修饰符
public
- 类内外都可以
protected
- 类内可以访问 类外不能访问
private
- 类内可以访问 类外不能访问
如果类中有指针类型的数据成员 堆内存的分配
struct VS class
相同点
- 都可以定义数据 函数
- 使用上跟class一样
不同点
- class的默认权限是private
- struct的默认权限是public
对象的创建
特殊的成员函数 -> 构造函数
基本语法
- 类名(形参列表){ // do sth}
- 没有返回值类型
- 函数名字必须跟类名一模一样
作用
- 主要进行初始化操作
- 在对象创建过程中自动调用的一个函数
注意事项
如果类中没有构造函数 编译器给提供一个默认的构造函数 默认无参构造函数 如果有其他构造函数, 就没有那个默认无参构造函数
如果还想使用这个默认无参构造---->显式的写出来 完整的或者简写 Point() = default;构造函数可以进行重载
利用无参构造函数创建对象
- Point pt ; 不要加() 可以加{ }
可以设置默认值, 但是尽量避免进行重载 ---> 出现二义性的问题
如果有多个构造函数, 没有显式的写出来默认构造, 系统就不再提供默认无参构造
- 如果还想使用默认无参 显式提供出来
- 可以完整的写出来Point(){}
- Point() = default;
对象中数据成员的初始化
初始化列表
构造函数的形参列表后面 : 数据成员名(参数) , 数据成员名(参数)
- 如果有多个 中间有,分隔
构造函数中也可以使用默认值
注意
- 初始化顺序只跟声明顺序相关 跟在初始化列表中的位置无关
- C++11中可以声明式就进行初始化
对象的大小
跟类中数据成员大小相关
内存对齐规则
- 按照类中所占空间最大的数据成员大小倍数对齐
- 对象大小跟数据成员声明顺序相关
- 如果有数组, 除了数组外 其他类型找最多大的那个类型的倍数对齐
类中没有定义数据成员
- 空对象的大小为1
类中有指针类型的数据成员
- 可能内存泄漏
对象的销毁
析构函数
基本语法
~类名(){//.....}
~一定要有
名字跟类名保持一致
形参列表为空
不能进行重载
- 只有1份
类中没有析构函数 ----> 提供默认的析构函数(啥也不干)
调用时机
- 一般在对象销毁时进行自动调用
- 虽然可以手动调用 但是不要这样做 建议自动调用
作用
主要进行资源回收
- 空间资源
- 文件资源
- 网络资源
- 数据库连接
当类中有指针类型的数据成员时, 析构函数的写法
- 1.先判断指针是否为空
- 2.如果不为空 执行delete
- 3.将指针设置为nullptr
注意事项
- 不建议手动调用析构
对于不同类型对象,析构函数调用时机
全局对象
程序结束
- 全局对象销毁--->调用析构
静态对象
程序结束
- 同上
局部对象
- 作用域失效时, 方法执行完毕后 自动调用
堆空间对象
delete时调用析构函数
子主题
Computer * p = new Computer{3999, "小米"}
- p在栈上
- 对象是在堆上
- 通过指针p->成员
- 通过对p解引用 , 再通过对象.成员
本类型对象的复制
拷贝构造函数
基本语法
- Point(const Point & rhs){}
- 特殊的构造函数
- 形参列表const 类名 引用 对象名
- 使用初始化列表的方式进行对数据成员的初始化操作 跟普通构造函数一样
特点
- 使用一个已经存在的对象初始化 新对象
- Point pt(1,2);
Point pt2 = pt;
Point pt3(pt);
浅拷贝与深拷贝
浅拷贝
- 子主题
深拷贝
- 写法
- 1.开辟新空间
- 2.把rhs对象的字符串数据拷贝到新空间中
调用时机
1.用已经存在的对象初始化一个新对象
2.对象作为函数参数的时候, 用实参初始化形参的时候
- void func(Point pt){}
3.对象作为函数的返回值
- Point func2(){return xxx}
可以取消编译器优化 -fno-elide-constructors --std=c++11
左值与右值
左值
能取地址的值
- 普通的变量 对象...
右值
不能取地址的值
- 临时的变量 对象 匿名的对象 字面值常量
const引用既可以绑定左值, 也可以绑定右值
非const引用只能绑定左值
拷贝构造函数的形式探究
为啥要用const
1.const引用可以接收一个右值
- 此时接收的是一个临时的对象
- const Computer & rhs = 临时Computer对象
2.不能修改
为啥要加&
1.语法角度 不加& 报错
2.为了避免递归调用
- 用实参初始化形参---> 拷贝构造的第二个调用场景 ---> 递归调用
赋值运算符函数
this指针
本质
特殊的指针 常量指针(Type * const p) 指向的是当前对象
- 哪个对象调用这个方法 哪个对象就是当前对象
在所有的成员函数中 都有一个隐式的参数 this
作用
- 可以通过this-> 访问成员
赋值运算符函数基本语法
Point & operatror=(const Point & rhs)
该成员函数的返回值类型为自身类型对象的引用
方法名
- operator=
形式参数
- const Point & rhs
调用时机
- 使用一个已存在的对象赋值另一个已存在的对象
- Point pt(1,1)
Point pt2(2,2);
pt2 = pt;
pt2.operator=(pt);
赋值运算符的定义细节
当类中有指针类型成员申请堆内存浅拷贝
- 两个指针m_brand 指向了同一片空间 ---> double free
深拷贝
- 当前对象的原来申请的空间 没释放
规范写法
- 1.自赋值判断
- 2.回收当前对象指针原来申请的空间
- 3.深拷贝操作
- 4.返回当前对象 *this
注意事项
为什么要返回&引用?
- 避免copy
返回值可以是void吗?
- 不建议设置为void 为了能够连续赋值 pc1 = pc2 = pc3
参数为什么是&引用?
- 为了避免copy
参数为什么是const?
- const可以接收左值 又可以接收右值
三合成原则
- 拷贝构造 / 析构 / 赋值运算符函数一起手动定义
特殊的数据成员
常量数据成员
必须在初始化列表中进行初始化
- 如果有多个构造函数, 都要初始化
- 声明即初始化相当于默认值
引用数据成员
需要在初始化列表中进行初始化
- 引用需绑定一个已存在的变量或对象, 且在引用数据成员生命周期内有效
对象数据成员
需要在初始化列表中进行初始化
通过对象数据成员的类的构造函数完成初始化
默认无参构造
- 隐式可以不写出来
有参构造
显式写出来 成员名(具体参数)
- 调用有参的构造函数
多个对象数据成员时, 对象创建流程
跟类中声明的对象数据成员顺序有关
执行相应的构造函数
执行相应的析构函数
- 输出语句 是相反的
静态数据成员
特点
- 存储在静态/全局区, 不占用对象存储空间
- 不依赖于某个对象, 被所有该类型对象共享
- 建议使用类名作用域方式方式访问
注意事项
初始化要放在类外
- 初始化时不用再加static 要使用类名作用域
- int Student::m_classID = 1;
同样受到权限影响
特殊的成员函数
静态成员函数
基本语法
- 在普通的成员函数前加上关键字static
特点
不依赖与某个对象
静态成员函数没有this, 不能直接访问非静态成员
可以间接访问
- 在static函数中 创建该类型的对象 通过对象.方式访问非静态的东西
非静态成员函数可以访问静态成员 还可以访问非静态的
使用
一般通过类名作用域
- Myclass::静态成员函数名()
- 类的内部可以不用类名作用域::
const成员函数
基本语法
- void func() const {}
特点
不能修改对象的状态(非静态数据成员)
内置基本类型
- 不能修改值
指针类型
- 不能修改指向
- 但是可以修改内容
对象类型
- 不能修改对象的数据成员
这里this指针被修改为双重const指针 即 const Type* const pointer
对象的组织
const对象
- 1.const对象, 只能调用const成员函数
- 2.当出现重载的成员函数时,const版本和非const版本时, const对象调用const函数, 非const对象调用非const函数
- 3.const成员函数, 普通对象,const对象都可以调用
指向对象的指针
栈对象
- Point pt(1,2);
Point * p = &pt;
- Point pt(1,2);
堆对象
- Point *p = new Point(1,2);
p->print();
delete p;
p = nullptr;
- Point *p = new Point(1,2);
对象数组
使用跟基本类型数组基本一致
几种构建方式
- 利用左值对象构建
- 利用临时对象构建
- 利用初始化列表方式构建
堆对象
- 注意内存释放
new/delete过程(了解)
new过程
- 调用operator new标准库函数申请未类型化的空间
- 在该空间上调用该类型的构造函数初始化对象
- 返回指向该对象的相应类型的指针
- //默认的operator new
void * operator new(size_t sz){
void * ret = malloc(sz);
return ret;
}
delete过程
- 调用析构函数,回收数据成员申请的资源(堆空间)
- 调用operator delete库函数回收本对象所在的空间
- //默认的operator delete
void operator delete(void * p){
free(p);
}
执行过程
创建堆上的对象需要什么条件?
- 需要公有的operator new、operator delete、构造函数
创建栈上的对象需要什么条件?
- 需要公有的构造函数、析构函数
只能创建堆上的对象?
- 可以将析构函数设为私有
只能创建栈上的对象?
- 可以将operator new/operator delete 设为私有
单例设计模式
方式一: 对象创建在静态区
实现步骤
- 私有化构造函数
- 提供一个静态的成员函数 返回这个创建好的唯一的对象
- 禁用拷贝 赋值运算符函数
为什么要返回对象引用?
注意:
- 使用delete 禁用拷贝构造 赋值运算符函数
方式二:对象创建在堆区
实现步骤
私有化构造函数
提供一个静态的成员函数 返回这个创建好的唯一的对象
提供一个自身类型的static类型的指针
- 类外进行初始化为nullptr
禁用拷贝赋值运算符函数
注意
- 静态方法中进行逻辑判断是否为第一次调用该静态方法
- 禁用拷贝构造 赋值运算符函数
- 数据成员有申请堆空间, 注意回收
应用场景
- 创建时耗时过多或耗资源过多,但又经常使用的对象可以考虑单例模式
C++字符串std::string
构造方式
- 无参构造
- count + 字符
- 接收一个string对象(拷贝)
- 接收一个C风格字符串
- 直接拼接
(string对象、C风格字符串,加号连接)
常用函数
c_str() 将string对象转换成C风格字符串
data() 同上
empty() 返回bool值,判空
size() 获取string对象大小(不存在‘\0’)
length() 同上
substr(pos,count)
截取子串append() 字符串尾部补充
- 接收string对象
- 接收count个字符
- 接收C风格字符串
find() 查找,返回位置(下标)
查找子串
- find(str,pos,count)
查找单个字符
string的遍历
使用下标访问运算符
- 1.可以使用str[i]
- 2.可以使用str.at(i)
增强for循环
auto关键字 自动推导类型
如果想要修改原始数据 需要使用&
- for(auto & : str){}
迭代器方式
begin()/end()
返回的是迭代器
- string::iterator it
- auto it
迭代器std::iterator
迭代器是 C++ 中用于遍历容器元素的对象,
它提供了一种统一的方式来访问各种容器(如 vector、list、map 等)中的元素,
而不需要关心容器的内部实现细节。迭代器是一种广义的指针- 可以指向容器中的某个元素, 通过迭代器,
我们可以读取或修改它指向的元素。
- 可以指向容器中的某个元素, 通过迭代器,
示意图
容器类名::iterator
- 容器的begin()
获取容器中第一个元素的地址 - 容器的end()
获取容器中最后一个元素后的地址 - vector::iterator
- string::iterator
- 容器的begin()
动态数组std::vector
构造方式
vector numbers
- 无参构造,创建一个可存放int型数据的空vector
vector numbers(10)
- 可存放long型数据,初始化存放10个0
vector numbers(arr,arr + 5)
- 迭代器方式,传入两个地址作为起始和结束,将这些地址上存放的数据存入vector(左闭右开)
vector numbers{1,2,3,4,5}
- 直接用大括号将所有需要存入的元素传递给vector
常用操作
empty() 判空
size() 当前容器中元素个数
capacity() 该容器最多能存放的元素个数
扩容原理
- 1.当size()结果与capacity()结果相同时,即容器存满
- 2.再往容器存储元素,就会开辟出一片原空间大小2倍的空间(GCC)
- 3.将容器中的元素全部复制到新的空间,在最后一个元素之后添加新的元素
- 4.回收原容器空间
push_back() 将元素添加到容器末尾
pop_back() 删除容器中最后一个元素
clear() 清除容器中所有元素,但不回收空间
shrink_to_fit() 释放容器中多余的空间
reserve() 申请空间,不存放元素
- 预计容器需要多大的空间,直接申请,避免空间浪费
底层实现
vector对象是由3个指针组成
_M_start指向当前容器中第一个元素存放的位置
_M_finish指向当前容器中最后一个元素存放的下一个位置
- size() : _finish - _start
_M_end_of_storage指向当前容器能够存放元素的最后一个空间的下一个位置
- capacity() : _end_of_storage - _start
C++输入输出流

流的四种状态
iostate分类
goodbit
- 流处于正常状态
badbit
流发生严重故障,无法恢复
- 一般IO错误 物理因素
failbit
流发生可恢复的错误
- 比如cin读取了无效的数据(与期待输入数据类型不匹配)
eofbit
流进入终止状态
- 比如输入过程中按下了ctrl + d,终止输入流
ios_base
通过函数获取流状态
- good()
- bad()
- fail()
- eof()
恢复流的状态
1.clear()恢复流的状态为goodbit
2.ignore 舍弃指定大小的缓冲区内容
函数参数为
- 需要头文件
std::numeric_limits::max() - '\n'
- 需要头文件
通用输入输出流
包含在头文件iostream
istream
- 输入流
ostream
- 输出流
标准输入输出流
cin/cout标准输入流
cin
本质是istream类型的一个全局对象
默认从键盘读取数据
程序中的变量使用输入流运算符(内容提取运算符>>)从流中提取数据
通常跳过输入流中的空格、 tab 键、换行符等空白字符, 会把这些空白字符作为分隔符
注意
- cin对象作为条件时,隐式转换为布尔类型
- cin对象完成一次输入后,返回值为自身对象,可以进行连续链式的输入
标准输出流
cout
本质是ostream类型的一个全局对象
默认向屏幕(终端)输出数据
- 在缓冲区刷新时将数据输出到终端
- 缓冲区大小1024
缓冲区
全缓冲
- 缓冲区满后,才会指向刷新操作
行缓冲
- 碰到换行符,进行刷新
非缓冲
不带缓冲区
- cerr
文件输入输出流
包含在头文件fstream
常用文件模式
- in
- out
- app
- ate
- ios_base
文件输入流ifstream
作用
- 将数据由文件传输到流对象
构造
无参构造,再通过open函数将输入流与文件绑定
- 文件必须存在
接收C风格字符串形式的文件名进行构造,直接绑定文件,后续操作输入流对象就是操作这个文件
- 文件必须存在
接收文件名和打开模式
- 打开模式默认为in模式
- 打开模式设为ate模式,将在打开后立即寻位到流结尾
读取操作
单个字符读取
使用ifstream成员函数get
- get()
- get(char & ch)
单个单词读取
- 使用>>运算符读取
按行读取(也可以按别的分隔符读取)
ifstream中的成员函数 getline(接收char数组, 大小))
- 兼容C的写法
std::string中非成员函数getline(流, string对象)
按字节读取
read
- 接收指针和长度参数,从文件中读取相应长度的内容,存放到堆空间上
seekg
在文件内容中放置游标(设置输入位置指示器)
传入数值
- 绝对位置
传入数值和基准
- 相对位置
tellg
- 从文件内容中读取游标位置(返回输入位置指示器的位置)
关闭流
- close()
文件输出流ofstream
作用
- 数据由流对象传输到文件
构造
接收一个字符串代表文件名,预备写入内容到此文件
- 此文件可以不存在
- 内容传给ofstream对象,该对象再传输到文件(进行写入)
接收字符串(文件名)和写入模式
默认写入模式为out模式
- 每次清除掉文件内容,重新写入新的内容
写入模式可选app模式
- 每次在文件末尾写入数据
写操作
利用<< 写数据
利用ofstream中的成员函数write写数据
接收C风格字符串和count
- 在文件中从游标位置开始,将字符串的count个字符写入文件
seekp
在文件内容中放置游标(设置输入位置指示器)
传入数值
- 绝对位置
传入数值和基准
- 相对位置
tellp
- 从文件内容中读取游标位置(返回输入位置指示器的位置)
close
- 关闭流,安全操作
动态查看文件内容
tail 文件名 -F
- ctrl + C退出查看
字符串输入输出流
包含在头文件sstream
字符串输入流istringstream
将字符串类型数据转换成其他类型
- string---> 其他类型的数据
操作
将字符串传输给istringstream对象,存在缓冲区
istream对象通过输入>>运算符将缓冲区中的数据输出给相应变量
- 可用于读取配置文件
- 子主题
字符串输出流ostringstream
将其他类型数据转换成字符串类型
其他类型--->string
str()
函数
操作
将其他类型数据传输给ostringstream对象,存在缓冲区
- ostring对象调用str()
将缓冲区中的数据转换成字符串
- ostring对象调用str()
友元与运算符重载

友元
目的
- 访问一个类的私有成员
形式
普通函数形式
- 类中将普通函数声明为友元(友元函数)
成员函数形式
- 目标类A需要进行前向声明,操作类B的成员函数在类中仅声明,
操作目标类A私有成员的B类成员函数在A类定义之后进行定义
- 目标类A需要进行前向声明,操作类B的成员函数在类中仅声明,
友元类
- 若A类的多个成员函数都需要访问B类的数据成员,可以将A类声明为B类的友元类
注意
- 友元是单向的
- 友元破坏了封装性
- 友元不具备传递性
- 友元不能被继承
运算符重载的认识
哪些运算符不能重载
- 带点的运算符不能重载,再加一个sizeof
运算符重载规则
- 运算符的操作数需为自定义类型才能进行重载
- 其优先级和结合性不变
- 操作数个数不变
- 运算符重载时,不能设置默认参数
- 不能臆造一个不存在的运算符
初识运算符重载
案例: 实现一个复数类,复数分为实部和虚部 重载+运算符,
使其能够处理两个复数之间的加法运算(实部加实部,虚部加虚部)- 普通函数实现
- 友元函数实现
- 成员函数实现
重载形式的选择
友元函数
- 不会修改操作数的值的运算符
成员函数
- 会修改操作数的值的运算符
赋值=、下标[ ]、调用()、成员访问->、成员指针访问->* 运算符必须是成员函数形式重载
与给定类型密切相关的运算符,如递增、递减和解引用运算符
- 会修改操作数的值的运算符
运算符重载思路
- 重载的实现形式
- 重载函数的返回类型
- 重载函数的参数
- 重载函数的运算逻辑
运算符重载基础案例
不会修改操作数的值的运算符,
倾向于采用友元函数方式重载operator+
加法运算符重载operator<<
输出流运算符重载形式
- std::ostream & operator<<(std::ostream & os, const MyClass & obj);
输出流运算符的重载不改变自定义类型对象的内容,输入流改变操作数的值,但仍采用友元函数形式。
因为流对象需为左操作数,而如果作为成员函数会由于this指针的存在使自定义类型对象成为左操作数
(无法与内置类型的使用方式保持一致)std::ostream &
- 确保能进行链式调用
const MyClass & obj
- 要输出的目标对象
输入/输出流运算符的返回值是 输入/输出流对象
operator>>
输入流运算符重载形式
- std::istream & operator>>(std::istream & is, MyClass & obj);
会修改操作数的值的运算符,
倾向于采用成员函数形式重载operator+=
加等运算符重载自增运算法重载
前置++
++a;
- 先+1 后取值
数据成员改变后,直接返回本对象
后置++
a++;
- 先取值 在+1
先拷贝,改变对象的数据成员,返回拷贝的对象的副本
运算符重载函数的参数中写一个int来区分
CharArray案例
定义一个CharArray类,模拟char数组
,需要通过下标访问运算符能够对对应下标位置字符进行访问[ ]下标访问运算符
形式
- Type & operator[](size_t index); // size_t无符号
- 返回类型为引用
注意
- 处理下标越界,如果越界返回终止符
如果只能通过下标访问 不能修改 , 如何修改?
- const Type & operator[](size_t index);
多层指针成员案例
成员访问运算符
operator->
- Type * operator->();
- 箭头运算符只能以成员函数的形式重载,其返回值必须是一个指针或者重载了箭头运算符的对象
- ->运算符会继续对返回的指针进行成员访问, 编译器会自动递归调用operator->()直到得到原生指针。
operator*
- Type & operator*();
- 解引用运算符的目的是使类对象可以表现得像指针一样,通过解引用访问封装的对象。
两层结构
优化前
- 改进后
三层结构
优化前
- 改进后
箭头运算符
B类包含A类指针类型的数据成员,想用B类对象通过箭头运算符调用A类成员函数
- B类中的重载箭头运算符函数返回值为A类指针
C类包含B类指针类型的数据成员,想用C类对象通过箭头运算符调用A类成员函数
- C类中的重载箭头运算符函数返回值为B类对象的引用
解引用运算符
解引用运算符函数返回本层指针成员的解引用,即上一层的对象
- 使用两次解引用,得到A类对象,再使用成员访问运算符调用A类成员函数
在C类中定义解引用运算符函数,使解引用的结果直接返回A类对象
- 一步到位,只需要使用一次解引用
智能指针的雏形
- 通过对象的生命周期来管理资源
可调用实体
函数对象
定义
- 重载了函数调用运算符的类的对象称为函数对象
operator()
函数调用运算符重载形式
- 返回值类型operator()(形参列表);
使用
- 对于一个类,在重载函数调用运算符后,这个类的对象可以如同函数名一样去实现相应函数功能
意义
- 可以携带状态
由于参数列表可以随意扩展 ,所以可以有很多重载形式
函数指针
typedef定义特定函数指针类型
指向函数的指针
可以用指针变量名,接函数调用运算符()进行调用
- f() 或 (*f)(1)
也可以对指针解引用,再接函数调用运算符()进行调用
成员函数指针
typedef定义特定成员函数指针类型
指向成员函数的指针,使用此类指针对类的成员进行访问使用成员指针访问运算符(两种形式)
.*
- 通过栈对象访问成员函数指针
->*
- 通过堆对象/空指针 访问成员函数指针
普通函数
成员函数
空指针使用
- 不涉及数据成员的情况下,空指针可以访问本类成员函数
- 空指针没有指向有效的对象,不能访问本类数据成员
类型转换函数
转换方向对比
内置类型向自定义类型转换
隐式转换
- 使用explicit禁止隐式转换
自定义类型向内置类型转换
类型转换函数
- operator int()
{
return m_x + m_y;
}
- operator int()
自定义类型向自定义类型转换
- 类型转换函数
- 通过特殊的构造函数实现类似隐式转换的效果
形式
- operator 目标类型( ){ }
注意点
- 需为成员函数
- 没有返回值类型、没有参数
- 在函数执行体中必须要返回目标类型的变量
嵌套类
全局作用域
类定义在全局区域,则称为全局类,拥有全局作用域
- 可以直接用类名创建对象
类作用域
类定义内部的范围
- class A
{
// 类作用域
}
- class A
类名作用域
可以通过类名访问的作用域
- 主要用于访问类的静态成员、嵌套类型
嵌套类
一个类Inner被定义在另一个类Outer之中,称为嵌套类
Inner: 内部类
Outer: 外部类在外部类的外部创建内部类对象
内部类对象的创建
Inner被定义在Outer的public区域
- 创建Inner类对象需要使用Outer::Inner方式
Inner被定义在Outer的private区域
在外部类的外部无法直接Outer::Inner创建 (private)
- 使用friend可以解决权限问题
在外部类的内部创建内部类对象
无论权限是什么 pubic/ private 都可以直接创建Inner对象
- Inner inner;
- Outer::Inner inner;
内部类对外部类成员的访问
可以直接访问(通过对象)
- 内部类相当于是外部类的友元类
外部类对内部类成员的访问
- public的成员可以通过对象访问, 但是private成员不能直接访问(通过对象),需要在内部类中再做友元声明
pimpl模式应用(了解)
pimpl模式
- Pointer to Implementation
- 一种常用的 C++ 编程技巧,用于隐藏类的实现细节,并帮助维护代码的封装性。
- 通常做法是将类的实现(数据成员、私有成员函数等)放到一个单独的内部类中,并通过指针将其与外部类关联
pimpl结构
- 外部类(接口类):只包含公有的接口(方法声明),不包含实现细节(即不包含私有成员的定义)。
- 内部类(实现类):该类定义了所有的实现细节(私有数据成员、私有成员函数等)。
它通常是一个封装类,且通常在.cpp
文件中定义,不在头文件中暴露。
- 内部类(实现类):该类定义了所有的实现细节(私有数据成员、私有成员函数等)。
- 指针:外部类通过一个指向内部实现类的指针来访问实现细节。
以Line类为例,在头文件中仅对可见类Line需要的数据成员、成员函数和内部类LineImpl做声明,并确保声明一个私有指针用以访问内部类对象
实现文件中完成内部类LineImpl的实现,在LineImpl之外实现Line所需的成员函数
将实现文件打包成静态库,把头文件+库文件交给第三方
打包库文件
- 安装: sudo apt install build-essential
编译 : g++ -c LineImpl.cc
打包 : ar rcs libLine.a LineImpl.o
- 安装: sudo apt install build-essential
生成libLine.a库文件
编译:g++ Test.cc(测试文件) -L(加上库文件地址) -lLine(就是库文件名中的lib缩写为l,不带后缀)
此时的编译指令为 g++ Test.cc -L. -lLine
- 隐藏代码的底层实现
- 好处
- 实现信息隐藏
- 实现文件修改方便
- 可以实现库的平滑升级
关联式容器

set
set容器
- 定义在头文件中, 用来存储单个的数据
set特点
- 底层是红黑树实现的
- 存储的数据是有序的
- 存储的数据不重复
使用时需指明类型
- set
set构建
- 创建空容器 利用无参构造函数
- 初始化列表构建
- 迭代器方式
- 拷贝构造
执行查找操作,查看是否有元素
count
- size_type count( const Key& key ) const;
- 参数是目标数据
- 返回值为一个整数, 找到了1 没找到为0
find
- iterator find( const Key& key );
- 参数是目标数据
- 返回值为迭代器 找到了返回目标元素对应的迭代器 没找到返回end()
执行插入操作
insert
单个数据插入
- std::pair insert( const value_type& value );
批量插入
初始化列表方式
- box.insert({1,2,3,4,5});
迭代器方式 begin() end()
- box.insert(itBegin, itEnd);
std::pair
- 定义在头文件
- pair存储的是2个数据 first second
遍历方式
增强for循环
- for(auto & element: s) ....
迭代器方式遍历
- auto itBegin = s.begin()
for.....
while.....
- auto itBegin = s.begin()
不支持下标访问运算符
适用场景
- 单个数据排序的
- 数据去重的
map
map容器
- map存储的是双列数据, 键值对数据(key-value) ---> 具有自我描述性的数据
- 底层使用的是红黑树
- 定义在头文件
- 举例: city = beijing key = value 属性名 = 属性值
age = 20
name = zs
password = 123456
map特点
- 存储的是k-v数据 pair对象 一对数据
- 存放的关键字key不重复
- 按照key升序排列的
- 可以通过key 获取 对应value数据
map使用
- map
map构建
1.无参构造创建空容器
2.初始化列表方式
- 创建pair对象方式
- 初始化列表方式
- std::make_pair函数
- 迭代器方式
4.拷贝构造方式
map的遍历
增强for循环方式
- 从容器中获取的每个元素都是一个pair对象 .first .second
迭代器方式
- (*itBegin)--->得到的是pair对象 .first .second
- itBegin->first itBegin->second
执行查找操作,查看是否有元素
count
- size_type count( const Key& key ) const;
- 参数为key
- 返回的结果 找到了为1 没找到0
find
- iterator find( const Key& key );
- 参数为key
- 找到了返回的结果是指向pair对象的迭代器 没有找到就返回end()
执行插入操作
insert
单组数据插入
创建一个pair对象 放到容器里
pair构造函数
- m.insert(pair{1,"zs})
make_pair()
初始化列表
- m.insert({1,"zs);
批量数据插入
初始化列表
- insert({
{ },
{ },
})
- insert({
迭代器方式
支持下标访问运算符
1、查找key对象的value
- m["name"]
2、如果查询时对用的key不存在,会直接创建该key的记录 但是value为默认值
3、可以修改key对应的value
使用场景
做数据统计使用 存储双列数据(key-value)
需求: 统计一下班里同学们 分别来自哪些省份 以及每个省份有多少人?
- map : key-->省份 value-->人数
m[湖北] = 20;
m[广东] = 10;
- map : key-->省份 value-->人数
需求: 统计一下80以上, 60-80有多少同学, 60以下的有多少人?
继承

继承的基本概念
概念
- 用原有类型来定义一个新类型,定义的新类型既包含了原有类型的成员,
也能自己添加新的成员,而不用将原有类的内容重新书写一遍。
原有类型称为“基类”或“父类”,在它的基础上建立的类称为“派生类”或“子类”。
- 用原有类型来定义一个新类型,定义的新类型既包含了原有类型的成员,
基本语法
- class Son
: 权限 Father
{
// 成员
};
- class Son
特点
子类复用父类成员
- 普通数据成员
- 普通的成员函数
子类可以添加新成员
protected权限
- 类的内部使用, 跟private没区别
- 类的外部使用, 跟private也没啥区别
- 在子类中有区别, private权限的成员在子类中无法访问
protected权限的成员在子类中可以访问
不能继承的结构
父类(基类)对象和子类(派生类)对象的创建与销毁是独立的
- 构造函数
- 析构函数
父类(基类)对象和子类(派生类)对象的复制控制操作是独立的
- 拷贝构造函数
- 赋值运算符函数
友元不能继承
- 友元破坏封装性,不允许继承,以降低影响
三种继承方式
public公有继承
父类(基类)public成员在子类(派生类)中保持public属性
- 子类(派生类)对象可以直接访问父类(基类)public成员
父类(基类)protected成员在子类(派生类)中保持protected属性
- 子类(派生类)对象能直接访问父类(基类)protected成员
- 子类(派生类)还可以往下继续派生,同样可以在类中访问顶层父类(基类)的protected成员
父类(基类)私有成员不能在子类(派生类)中访问
子主题
protected保护继承
父类(基类)public成员在子类(派生类)中可以访问(变为protected属性)
- 子类(派生类)对象能直接访问父类(基类)public成员
- 子类(派生类)还可以往下继续派生,同样可以在类中访问顶层父类(基类)的public成员
父类(基类)protected成员在子类(派生类)中保持protected属性
- 子类(派生类)对象不能直接访问父类(基类)protected成员
- 子类(派生类)还可以往下继续派生,同样可以在类中访问顶层父类(基类)的protected成员
父类(基类)私有成员不能在子类(派生类)中访问
子主题
private私有继承
父类(基类)public成员在子类(派生类)中可以访问(变为private属性)
- 子类(派生类)对象不能直接访问父类(基类)public成员
- 子类(派生类)还可以往下继续派生,不能在类中访问顶层父类(基类)的public成员
父类(基类)public成员在子类(派生类)中可以访问(变为private属性)
- 子类(派生类)对象不能直接访问父类(基类)protected成员
- 子类(派生类)还可以往下继续派生,不能在类中访问顶层父类(基类)的protected成员
父类(基类)私有成员不能在子类(派生类)中访问
子主题
private继承和protected继承的区别 : 断子绝孙 / 千秋万代
单继承下子类(派生类)对象的创建和销毁
子类对象内存结构
通过sizeof()获取对象大小
- 子主题
- 子主题
子类(派生类)对象的创建
子类(派生类)中没有显式定义构造函数时,会调用父类(基类)的默认构造函数
子类(派生类)中有显式定义构造函数时,默认情况下仍会调用父类(基类)的默认构造函数
子类(派生类)中有显式定义构造函数,初始化父类(基类)时不希望调用父类(基类)默认构造函数时,需要显式地在子类(派生类)的初始化表达式中调用父类(基类)的其他构造函数
关于构造函数的调用顺序
- 创建子类(派生类)对象,先调用子类(派生类)构造函数,在执行子类(派生类)构造函数的过程中,先初始化父类(基类)部分,此过程中调用了父类(基类)构造函数
当子类(派生类)对象有对象成员时,在子类(派生类)的构造中注意区分父类(基类)部分初始化和子对象初始化的写法
子类(派生类)对象的销毁
- 子类(派生类)对象销毁时,先执行子类(派生类)的析构函数,再执行父类(基类)对象的析构函数
- 当子类(派生类)对象有对象成员时,子类(派生类)对象销毁,先执行子类(派生类)的析构函数,再执行成员子对象的析构函数,最后执行父类(基类)的析构函数
子类(派生类)隐藏父类(基类)的成员
子类(派生类)中重新定义父类(基类)数据成员
- 父类(基类)原本的数据成员被隐藏
- 想通过子类(派生类)对象获取父类(基类)的数据成员要加上作用域限定
子类(派生类)中重新定义父类(基类)成员函数
只要成员函数名字相同,即使参数列表不同,也只能看到子类(派生类)版本,父类(基类)的同名函数发生隐藏
想调用父类(基类)隐藏的成员函数,也需要加上作用域限定
- 不推荐实际使用
多继承
基本语法
class Son
:public Father1
,public Father2
{ };注意
- 子类(派生类)对每个父类(基类)的继承方式作单独的声明,否则按默认私有继承的方式进行继承
多继承下对象创建与销毁流程
- 子类(派生类)的构造函数中按继承声明的顺序调用父类(基类)构造函数
- 按照声明顺序的逆序调用析构函数
多继承的问题
(以菱形继承为例)成员名访问二义性
- 解决方法:访问时加上作用域限定
存储二义性
- 解决方法:中间层次的类采用虚拟继承的方式
子主题
- 子主题
多继承的存储布局
(以菱形继承为例)无虚继承情况
- 中间层的子类(派生类)都包含顶层父类(基类)的对象,底层子类(派生类)包含所有的中间层子类(派生类)对象,
所以存有两份顶层父类(基类)对象的拷贝(因此产生存储二义性问题)
- 中间层的子类(派生类)都包含顶层父类(基类)的对象,底层子类(派生类)包含所有的中间层子类(派生类)对象,
中间层次虚继承情况
- 中间层的子类(派生类)存有一个虚基指针,将父类(基类)内容存在最低地址;
底层子类(派生类)继承了两个中间层子类(派生类),存有两个虚基指针,
只存储一个顶层父类(基类)的对象,在最低地址(解决存储二义性问题)
- 中间层的子类(派生类)存有一个虚基指针,将父类(基类)内容存在最低地址;
没加virtual继承前
- 子主题
加了virtual继承后
- 子主题
vs中打印对象结构的结果 (64位)
父类(基类)和子类(派生类)之间的转化
一般情况下,父类(基类)对象占据的空间小于子类(派生类)
转化情况
赋值
可以用子类(派生类)对象赋值给父类(基类)对象
- 向上转型
不能用父类(基类)对象赋值给子类(派生类)对象
指针
父类(基类)指针可以指向子类(派生类)对象
能操纵的只有继承自父类(基类)的部分
- 向上转型
子类(派生类)指针不能指向父类(基类)对象
- 除了操纵父类(基类)对象的空间,还需要操纵一片空间,只能是非法空间,所以会报错
引用
父类(基类)引用可以绑定子类(派生类)对象
能操纵的只有继承自父类(基类)的部分
- 向上转型
子类(派生类)引用不能绑定父类(基类)对象
- 除了操纵父类(基类)对象的空间,还需要操纵一片空间,只能是非法空间,所以会报错
强制转换
向上转型是可行的,向下转型有风险,如果使用C的方式进行强制转换,无法规避风险
若父类(基类)中存在多态内容,可以使用dynamic_cast进行强制转换
合理的向下转型
- 返回有效指针
不合理的向下转型
- 返回空指针
向下转型什么时候能够成功?
1.先看对象是什么类型. 2.该类型以及父类型的指针才能指向这个对象
子类(派生类)对象间的复制控制
当子类(派生类)中没有显式定义复制控制函数时,会自动完成父类(基类)部分的复制控制操作
当子类(派生类)中有显式定义复制控制函数时,不会再自动完成父类(基类)部分的复制控制操作
只有数据成员出现指针时,才需要复制控制函数
关于复制控制函数的调用顺序
- 子类(派生类)对象进行复制时会马上调用子类(派生类)的复制控制函数,
在进行复制时会首先复制父类(基类)的部分,此时调用父类(基类)的复制控制函数
- 子类(派生类)对象进行复制时会马上调用子类(派生类)的复制控制函数,
多态

多态的基本概念
多态概念:同一指令,针对不同对象,产生不同行为
静态多态:函数重载、运算符重载、模板(发生的时机是在编译时)
动态多态:发生时机是运行时,体现形式:虚函数
虚函数的概念:在成员函数前加virtual修饰
类内部形式
- virtual void func(){ xxxx}
类外部形式
- 声明需要加virtual 类外实现不加virtual
父子类中定义同名的虚函数
- 函数同名
- 返回值类型相同
- 函数参数类型、个数、顺序相同
没有虚函数时
加了虚函数后的对象结构 ---> 多了个vfptr虚函数指针 ---> 指向虚表 (存放虚函数地址)
- 多继承 且都有虚函数
虚函数的实现机制
画图理解- 虚函数指针vfptr:指向虚表
- 虚函数表(虚表):存放的是虚函数的入口地址
- 子主题
多态被激活的条件(五条)
- 1、父类(基类)要定义虚函数
- 2、子类(派生类)重写虚函数
- 3、创建子类(派生类)对象
- 4、父类(基类)的指针(引用)指向(绑定)到子类(派生类)对象
- 5、使用父类(基类)指针(引用)调用同名的虚函数
虚函数
哪些函数不能定义为虚函数
静态成员函数
- 1、编译时就绑定
- 2、虚函数的调用需要对象,需要this指针,而static没有this指针,
可以不使用对象调用,可以使用类名加作用域限定符调用
普通函数(非成员函数)
友元函数
inline函数
- 因为inline函数在编译期间完成替换,而在编译期间无法展现动态多态机制,起作用的时机是冲突的
构造函数
- 1、从继承观点来看,构造函数不能被继承,虚函数可以被子类(派生类)重写,
所以不能设置为虚函数 - 2、从存储角度,如果构造函数是虚函数,则需用通过虚表来调用,但是对象还没有实例化,
也就是内存空间都还没有,就无法找到虚函数指针找到虚表。 - 3、从语义角度,构造函数就是为了初始化数据成员而产生了,然而虚函数目的是为了在完全不了解细节情况下也能正确处理对象。
虚函数要对不同类型的对象产生不同的动作,如果构造函数是虚函数,那么对象都没有产生,如何完成想要的动作
- 1、从继承观点来看,构造函数不能被继承,虚函数可以被子类(派生类)重写,
虚函数的访问
- 1、指针(指向子类(派生类)对象,就会使用动态联编,体现多态性)
- 2、引用(绑定到子类(派生类)对象,就会使用动态联编,体现多态性)
- 3、对象(采用静态联编,不体现多态性)
- 4、其他成员函数调用虚函数(通过父类(基类)指针或引用调用,需要指定或者绑定到子类(派生类)对象,如果是子类(派生类)对象,也可以,
主要是通过父类(基类)this指针(成员函数不要写在子类(派生类))) - 5、构造函数或析构函数调用虚函数(采用静态联编)
纯虚函数
形式:virtual void func() = 0
作用:父类(基类)不给出实现,留给子类(派生类)实现
抽象类
概念
包含一个或多个纯虚函数的类
两种形式
- 父类是抽象类
- 子类继承父类, 但是没完全给出虚函数的实现
特点
- 1、纯虚函数
- 2、建议构造函数被protected修饰(public修饰也可以)-->主要为了强调父类的构造函数只能在派生类中使用
- 不能进行实例化
虚析构函数
将父类(基类)的析构函数设置为虚函数,
子类(派生类)的析构函数自动成为虚函数,- 原理:根据虚函数可以被重写这个特性,如果父类(基类)的析构函数设置为虚函数后,
那么子类(派生类)的析构函数就会重写父类(基类)的析构函数。
但是他们的函数名不相同,看起来违背了重写的规则,但是实际上编译器对析构函数的名称做了特殊的处理,
编译后析构函数的名称统一为destructor。之所以可以这样做,
是因为在每个类里面,析构函数是独一无二的,不能重载,所以可以这么设计。
- 原理:根据虚函数可以被重写这个特性,如果父类(基类)的析构函数设置为虚函数后,
目的
- 防止内存泄漏
三个基本概念
重载
- 同一个作用域(在这里是同一个类域),函数名相同,参数不同(参数类型、参数个数、参数顺序)
覆盖(重定义、重写)
- 父类(基类)与子类(派生类),虚函数,函数名,参数类型(参数类型、参数个数、参数顺序)相同
隐藏
- 父类(基类)与子类(派生类),函数名相同
- 子类(派生类)隐藏父类(基类)的同名数据成员
虚表的存在
- 在只读段(GCC)
- 一个类可能没有虚表,可能有一张虚表,可能有多张虚表
- 验证虚表的存在
带虚函数的多继承
布局规则
- 1 . 每个基类都有自己的虚函数表(前提是基类定义了虚函数)
- 2 . 派生类如果有(自己的)虚函数,会被加入到第一个虚函数表之中 —— 希望尽快访问到虚函数
- 内存布局中,其基类的布局按照基类被声明时的顺序进行排列(带虚函数的基类会往上放)
- 4 . 派生类会覆盖基类的虚函数,只有第一个虚函数表中存放的是真实的被覆盖的函数的地址;
其它的虚函数表中对应位置存放的并不是真实的对应的虚函数的地址,而是一条跳转指令
—— 指示到哪里去寻找被覆盖的虚函数的地址
多基派生的二义性
通过对象调用
- 不会经过虚表, 取决于对象的静态类型
父指针指向子对象 , 通过父指针调用
- 子类重写虚函数, 动态多态 访问子类虚函数
- 子类没重写虚函数 , 访问父类虚函数
- 父指针调用非虚函数, 取决于对象静态类型 父指针只能访问自己部分
子指针指向子对象, 通过子指针调用
调用虚函数时,也会通过虚表去访问虚函数
- 子类重写虚函数-->子类结果
- 子类没重写---->父类结果
虚拟继承
虚的含义:存在、间接、共享
虚函数中虚的含义
- 强调调用时的动态性(动态多态)
虚拟继承中虚的含义
强调继承结构的共享性(避免重复)
- 解决菱形问题
虚拟继承时子类(派生类)对象的构造与析构
- 注意在子类(派生类)中需要显示调用父类(基类)的构造函数
模板

为什么要提出模板?
引例
add函数的重载
需要定义好多个参数
- 可以使用模板来简化
类型参数化 将数据类型作为参数
- 模板是一种通用编程技术(泛型)编程技术
好处
- 代码可以复用
模板的分类
函数模板
生成
- 模板函数
类模板
生成
- 模板类
基本原理
模板不是一个具体的类或者函数,而是编译器通过模板生成具体的类或者函数
这个过程叫做实例化 发生在编译时期
隐式实例化
- 通过传入的参数类型确定出(推导出)模板类型
显式实例化
- <>中指明具体类型, 类似set, vector的使用
函数模板 --》 生成相应的模板函数 --》编译 ---》链接 --》可执行文件
模板的定义
template
<>
模板参数列表
T1, T2可以是任意类型 通常为大写字母
T
- type
K
- key
V
- value
E
- element
模板参数
类型参数
- T, U ....
非类型参数
整型
- bool/char/short/int/long
可以设置默认值
优先级
- 显式指定的优先级 > 自动推导的 > 默认值
函数模板
形式
temlate
T add(T x, T y)
{ return x + y;}声明和实现写一起
声明和实现分开写
可以在同一个文件中
也可以在不同文件中
- 需要把实现文件include到头文件中, 类似于vector.tcc
实例化
概念
- 由函数模板到模板函数的过程称之为实例化 即根据模板参数生成具体类型或函数的过程
两种方式
隐式实例化
- add(1, 2);
- 不指名类型, 借助编译器自动推导, 这种方式用的更多一些
显式实例化
- add(1.1, 2.2);
- 指明具体类型
函数重载
函数模板与普通函数重载
- 如果都匹配, 则普通函数优先执行
函数模板与函数模板构成重载
- 尽量别写位置不同的函数模板与函数模板重载
- 如果2个模板都匹配 会选择更"匹配的"那个模板
模板的特化
当某一些类型不能使用通用版本时,需要给出一个特别的版本,就称为模板的特化(specialization)
形式
- template <> ---> <>中不写类型
声明函数的时候再写类型
void func(参数列表){
// xxxxx
}
- template <> ---> <>中不写类型
案例
- C风格字符串相加
注意
- 使用模板特化时, 要先有基础的函数模板
模板的参数类型与默认值
模板参数
类型参数
- T, U ....
非类型参数
整型
- bool/char/short/int/long
可以设置默认值
优先级
- 显式指定的优先级 > 自动推导的 > 默认值
成员函数模板
可以写在类内, 也可以把实现写在类外
如果写在类外
- 1.作用域要加上
- 2.template声明再写一遍
- 3.如果模板中有默认值, 写在类外不要把默认值再写一遍,默认值只写在声明处
可变模板参数
形式
template
返回值 函数名(Args... args)
{}- template
void print(Args... args);
- template
解释
Args
- 模板参数包
args
- 函数参数包
... 在参数包的左边时,称为打包
- 在声明时使用
... 在参数包的右边时,称为解包
在实际调用时使用
print(args...);
- print(1, 2.2, 3.3, 'a');
求取可变参数的个数
- sizeof...(Args)
- sizeof...(args)
处理可变参数模板
通常是使用递归方式去做
- 1.递归体
- 2.递归出口
类模板
template
class Stack
{
public:
bool empty()const;
};成员函数的实现
在类内部实现,与非模板的实现类似
在类之外实现时,要注意加上模板参数列表
- template
bool Stack::empyt() const
{}
- template
注意
- 类模板中有默认参数时, 可以不指定类型, 但是要加上<>
移动语义与资源管理

为什么提出移动语义?
在程序执行的过程中,会产生大量的临时对象
临时对象只是作为过渡来使用的,用完之后马上就会被销毁 带来了不必要的资源浪费提出需求:
- 希望将临时对象直接转移到新对象中
左值与右值
左值
指表达式执行结束后依然存在的持久对象
可以取地址
常见左值
- 有名字的变量, 对象, 字符串字面值常量("hello")
右值
指表达式执行结束后就不再存在的临时对象
不能取地址
常见右值
- 字面值常量、临时对象(匿名对象)、临时变量(匿名变量)
问题:
希望将临时对象直接转移到新对象中, 但是临时对象是右值
当临时对象作为函数参数传递过来时
- 在C++11之前,只有const引用可以绑定
const & 它还可以绑定左值
当const引用作为函数参数时,无法确定传递过来的到底是左值还是右值
解决方案:
- C++11提出了右值引用
左值引用与右值引用
左值引用
非const引用
- int & ref
- 一般引用哪些希望改变值的对象
const引用
- const int & ref
- 一般引用哪些不希望改变值的对象(比如常量)
右值引用
形式
- int && ref = 1;
- 一般所引用对象的值再使用后无须保留(比如临时变量对象)
特点
- 右值引用只能绑定到右值,无法绑定到左值
- 当右值引用作为函数参数时,它可以识别出右值
具有移动语义的函数
移动构造函数
测试代码
String s = "hello"- 先创建临时对象 再调用拷贝构造函数
形式
- String(String && rhs)
具体实现
1.浅拷贝 来复用临时对象的空间
2.将临时对象指针设置为nullptr
- 避免临时对象销毁调用析构函数 回收空间 避免double free
特点
- 1.如果没有显式定义构造函数、拷贝构造、赋值运算符函数、析构函数,编译器会自动生成移动构造,对右值的复制会调用移动构造
- 2.如果显式定义了拷贝构造,而没有显式定义移动构造,那么对右值的复制会调用拷贝构造
- 3.如果显式定义了拷贝构造和移动构造,那么对右值的复制会调用移动构造。
移动赋值函数
测试代码
String s3("hello");
s3 = String("wangdao");- 使用临时对象 对s3进行赋值操作 调用赋值运算符函数
形式
- String & operator=(String && rhs);
具体实现
1.原本赋值运算符函数的深拷贝改为浅拷贝 来复用临时对象的空间
2.将临时对象指针设置为nullptr
- 避免临时对象销毁调用析构函数 回收空间 避免double free
特点
- 1.如果没有显式定义构造函数、拷贝构造、赋值运算符函数、析构函数,编译器会自动生成移动赋值函数。使用右值的内容进行赋值会调用移动赋值函数。
- 2.如果显式定义了赋值运算符函数,而没有显式定义移动赋值函数,那么使用右值的内容进行赋值会调用赋值运算符函数。
- 3.如果显式定义了移动赋值函数和赋值运算符函数,那么使用右值的内容进行赋值会调用移动赋值函数。
std::move函数
作用: 将一个左值转换成右值
- 本质为强制转换 左值-->右值
- 本身并不移动数据或资源,只是为移动操作提供条件使得移动构造函数和移动赋值运算符函数能够被调用
具有复制控制语义的函数
拷贝构造函数
- String(const String & rhs);
赋值运算符函数
- String & operator=(const String & rhs);
禁止复制
- 将以上两个函数从类中删除
当类中提供了两类函数时,传递右值时,都可以绑定;此时有一个规则:
- 具有移动语义的函数会优先调用
对拷贝构造调用时机补充
当函数的返回值是对象时- 根据返回的对象生命周期来决定
- 如果返回的对象生命周期即将结束(局部对象),此时调用的是移动构造函数
- 如果返回的对象生命周期大于函数的,此时调用的才是拷贝构造函数
资源管理
RAII
全称
Resource Acquisition Is Initialization
- 资源获取即初始化时机
本质特征
利用对象的生命周期管理资源
- 内存资源, 文件资源 网络资源....
类似于之前的单例模式自动释放资源
特点
- 当创建对象时,托管资源
- 当对象销毁时,释放资源
3.提供若干访问资源的方法
- 一般要表达对象语义,不能进行复制或者赋值
- 一般来说,获取的是系统资源
智能指针
auto_ptr
C++0x
- 在C++17中已经被弃用了
子主题
在语法形式上,执行复制或者赋值操作时,表达的值语义
但在底层实现上,已经完成了资源的所有权的转移,表达的是移动语义
存在缺陷
auto_ptr
unqiue_ptr
shared_ptr
常见操作get()
- 获取原生指针(指向所管理资源的指针)
reset()
- 替换被管理的对象
- 访问资源
->
- 访问资源
unique_ptr
独占所有权的智能指针
子主题
可以访问资源
- 重载了->箭头运算符和*解引用运算符
不能进行复制或者赋值
- 禁止复制
可以作为容器的元素
- std::move构建右值
- unique_ptr的构造函数构造右值
可以表达移动语义
- 提供了移动构造函数和移动赋值函数
shared_ptr
共享所有权的智能指针
子主题
可以访问资源
- 重载了箭头运算符和解引用运算符
内部使用了引用计数
- 原子操作
当shared_ptr对象执行复制或者赋值操作时,引用计数加1
当shared_ptr对象被销毁时,引用计数减1
直到引用计数减为0,才真正释放所托管的对象
内部提供了两类函数
- 表达复制控制语义
- 表达移动语义
问题:
循环引用
- 导致内存泄漏
解决方案:
- weak_ptr
weak_ptr
它的诞生就是为了解决shared_ptr的问题而出现的
可以获取所托管对象的引用计数
知道所托管的对象是否还存活
- 引用计数为0时,所托管的对象就被销毁了
当它进行复制或者赋值时,不会导致引用计数加1
不能直接访问所托管的对象
- 没有重载箭头运算符和解引用运算符
- 需要访问时,必须要进行提升 提升为一个shared_ptr去访问资源
- 使用lock()方法返回一个shared_ptr对象
删除器
有些资源不是空间资源, 默认的delete方式就不适用, 此时要为智能指针定制资源释放的方式
unique_ptr对应的删除器
- 删除器是模板参数
shared_ptr对应的删除器
- 删除器是构造函数参数
小结
- 如果管理的是普通的资源,不需要写出删除器,就使用默认的删除器即可,
只有针对FILE或者socket这一类创建的资源,才需要改写删除器,使用fclose之类的函数。
- 如果管理的是普通的资源,不需要写出删除器,就使用默认的删除器即可,
智能指针的误用
智能指针的误用基本上都是使用了不同的智能指针托管了同一块堆空间(同一个裸指针)
unique_ptr和shared_ptr误用
- 将一个原生裸指针交给了不同的智能指针进行托管,而造成尝试对一个对象销毁两次
shared_ptr误用
- 使用不同的智能指针托管同一片堆空间,只能通过shared_ptr开放的接口——拷贝构造、赋值运算符函数
- 不能直接以裸指针的形式将一片资源交给不同的智能指针对象管理
另一种误用
Point中增加一个函数
- Point * addPoint(Point * pt){
m_x += pt->m_x;
m_y += pt->m_y;
return this;
}
- Point * addPoint(Point * pt){
shared_ptr sp(new Point(1,2));
shared_ptr sp2(new Point(3,4));
//创建sp3的参数实际上是sp所对应的裸指针
//效果还是多个智能指针托管了同一块空间
shared_ptr sp3(sp->addPoint(sp2.get()));
cout print();
解决方案
通过this指针获取本对象的shared_ptr
shared_ptr addPoint(Point * pt)
{
m_ix += pt->m_ix;
m_iy += pt->m_iy;
return shared_ptr(this);
}
- 但是这样写,在addPoint函数中创建的匿名智能指针对象接收的还是sp对应的裸指针,
那么这个匿名对象和sp所托管的空间还是同一片空间。匿名对象销毁时会delete一次,sp销毁时又会delete一次
- //注意!!
//addPoint的返回值与sp共用了同一个裸指针,返回值在当前行结束后销毁,会回收掉第一个Point对象
//sp管理的空间实际上已经被回收了
//验证如下
sp->addPoint(sp2.get());
delete sp.get();
cout << "over" << endl;
- 最终解决
——使用智能指针辅助类enable_shared_from_this的成员函数shared_from_this
小结
智能指针的误用全都是使用了不同的智能指针托管了同一块堆空间(同一个裸指针)