c++

概述

  • 编辑器
    • Qt Creator
#include <iostream> //包含系统头文件iostream(标准的输入输出流)
#include "myxxx" //包含自定义头文件
using namespace std;//使用命名空间,std是系统定义的一个命名空间

//c++有且仅有一个main函数,是程序的唯一入口
int main(int argc, char * argv[]){
    // cout输出设备(终端、屏幕) <<右移 endl换行;输出顺序是从左开始的,即先输出a再输出b
    cout << "a" << endl "b" << endl;
    int num = 0, num2 = 0;
    cin >> num >> num2;//获取键盘输入的值赋给num和num2,内部会根据变量的类型自动转化,默认以空白符作为分隔标识符,如果类型转化失败int类型的变量将赋值为0
    return 0;
}
  • 关键字
    • auto bool break case catch char class const等

变量-数据类型

  • 常量:只读,值不可修改
  • 变量:可以修改
    • 由类型加变量名组成
    • 命名规则:字母、数字、下划线,区分大小写
    • 变量的空间大小右变量类型决定

整型

  • int 4字节
  • short 2字节
  • long 4(32位系统)/8(64位系统)字节
  • 全局变量未初始化默认0,普通局部变量未初始化默认随机(可能是很大的一个数)
  • 二进制 0b开头
  • 八进制 0开头
  • 十六进制 0x开头
  • c++不直接支持二进制的输入输出,可以使用 bitset<位数>(数值) 函数输出二进制
#include <iostream> 
#include <bitset>
cout << bitset<8>(0b00001010) << endl;//输出8个位数的二进制
cout << oct << 0123 << endl;//输出八进制
cout << hex << 0xab << endl;//输出十六进制

int num = 0;//尽量给初始值,如果不确定建议赋值0

字符型

  • char
  • 专为存储字符而设计的,计算机存储数字简单,但存储字符则是另一回事,编程语言通过使用字符的数值编号解决了这个问题
  • 它是够长能够表示目标计算机中的所有基本符号,实际上很多系统支持的字符都不超过128个,因此用一个字节就可以表示,因此char虽然最常用来处理字符,但也可以将它用做比short更小的整型
  • 默认值不确定
char c = '\0';// '\0'的ASCII码值是0,'0'的ASCII码值是48 0在内存中存储的就是0
// char c2 = 97;//'a' 可以这么写但是不推荐
cout << 'a' << endl;//输出字符a
cout << (int)'a' << endl;//输出字符a的ASCII值

char c3 = '\0';
cin >> c3;//将键盘输入的数据赋值给字符c3,只取输入数据的第一个有效字符

//字符大小写判断
char cc = 'a';
if(cc >= 'a' && cc <= 'z'){
    cout >> "小写字母" >> endl;
} else if(cc >= 'A' && cc <= 'Z'){
    cout >> "大写字母" >> endl;
}

//'a'和'A'的ASCII码差值是固定的,可以利用这个特点进行字母大小写转换
char c4 = 'a';
c4 = c4 - ('a'-'A');//小写字母转大写字母
char c5 = 'A';
c5 = c5 + ('a'-'A');//大写字母转小写字母

实型/符点型

  • 浮点数就是能够表示带小数部分的数值,如:3.14
  • 计算机将这样的值分成两部分存储,一部分表示值,另一部分用于对值进行放大或缩小。如数值22.123可以看成0.22123(基准值)和100(缩放因子),缩放因子对作用是移动小数点的位置。c++内部表示浮点数的方法与此相同,只不过它基于的是二进制数,因此缩放因子是2的次幂,不是10的次幂
  • float 单精度 4字节,以f结尾
  • double 双精度 8字节
float f = 0.0f;
double d = 0.0;
  • 有符号数
    • 计算机中以二进制中的最高位表示符号位,其他位为数据位
    • 1表示负数
    • 0表示正数
  • 无符号数
    • 没有符号位,所有二进制都是数据位
// 有符号数
signed int num = 0;//有符号关键字signed可以省略
11111111 //-127
10000000 // -0 因计算机无法表示-128所有-0也代表-128
00000000 //+0
01111111 //+127

//无符号位
unsigned int num = 0; 
00000000 //0
11111111 //255

原码、反码、补码

  • 计算机存储的是数据的补码
  • 源码:计算机中对数字的二进制定点表示方法
  • 对于无符号数,补码、反码、源码都是相等的
  • 有符号数
    • 正数:补码、反码、源码都是相等的
    • 负数:
      • 反码 == 源码符号位不变,其他位按位取反
      • 补码 == 反码+1
  • 负数在计算机中是以补码的方式存储的
  • 非负数在计算机中是以源码的方式存储的
  • 补码的意义
    1. 统一了0的编码
      • +0 补码 00000000
      • -0 补码 00000000
    2. 使减法运算变可以用加法运算来计算
  • 计算机对数据的取值
    • 如果是对无符号的变量进行取值,输出内存的原样数据
    • 如果是对有符号变量进行取值,系统会去看内存的最高位,如果最高位为0表明整数,内存原样输出,如果最高位为1表明负数,将内存数据求补码(得到源码)输出
// 如果将-10赋值给无符号的变量,计算机会先将-10转成无符号数(-10的补码)
unsigned short sn = -10;//源码 1000 0000 0000 1010;补码 1111 1111 1111 0110
cout << sn << endl;//1111 1111 1111 0110 转成十进制后为65526

关键字修饰符

  • const 修饰普通变量,只读模式
    • 如果以常量初始化const修饰的只读变量,那么只读变量的值事先存储在 '符号常量表中',不会立即开辟空间
    • 如果以变量初始化const修饰的只读变量,会立即开辟空间,没有符号常量表
    • const修饰自定义类型变量(如结构体)时立即开辟空间,没有符号常量表
  • register 修饰寄存器变量
    • 尽量将register修饰对变量放入寄存器,不一定成功
    • 编译器优化:高频繁使用的变量系统会将其存入寄存器中,如果用户想将变量直接存入寄存器中可使用register修饰
    • 不建议对register修饰的寄存器变量进行取地址操作
  • volatile 强制从内存访问变量,防止编译器优化
  • sizeof 测量类型变量的字节长度
  • typedef 给已有对类型取别名
//如果以常量初始化const修饰的只读变量
const int cn = 100;//只读变量的值事先存储在 '符号常量表中',不会立即开辟空间
int * p = (int*)&cn;//当对常量取地址时才开辟一个空间,将符号常量表中对应的值放入该空间,并将该空间地址编号给对应的指针
*p = 101;
cout << *p << endl;//101
cout << cn << endl;//100

//如果以变量初始化const修饰的只读变量
int n = 200;
const int cn2 = n;//会立即开辟空间,没有符号常量表
int * p2 = (int*)&cn2;
*p2 = 201;
cout << *p2 << endl;//201
cout << cn2 << endl;//201

//数组类型别名
typedef int MYARRPY[5];
MYARRPY  arr = {1,2,3,4,5}
//给指针取别名
int n2 = 0;
typedef int* MYINTP;
MYINTP p = &n2;

转义字符

  • '\0' '\n' '\t' '\r' '\a'
  • 八进制转义
    • '\ddd'每个d的范围必须是0-7,三个d表示最多识别3位八进制数据
  • 十六进制
    • '\xhh' 每个h的范围0-9a-f,两个h表示最多识别2位十六进制

类型转换

  • 自动转换:遵循一定的规则由编译系统完成
    1. 字节少的类型向字节多的类型转换,以保证精度不降低
    2. 无符号和有符号的变量参加运算,有符号将转成无符号
    3. int和double参加运算,会将int转成double
    4. char和short只要参与运算,都会转成int
  • 强制转换:指定转换规则
    • (类型说明符)(表达式)
  • 不管是自动还是强制转换,都是临时的,不改变原变量的类型

运算符

  • 算术运算符
    • + - / * += -= *= /=
  • / 的取整
    • 如果/号所有的运算对象都是整数,称为取整运算
  • / 的除法运算
    • 如果/号中有一个运算对象是实型,则称为除法运算
  • % 取余运算
  • 自增自减
    • ++ --
  • 关系运算符
    • > >= < <= != ==
  • 逻辑运算符
    • && || !
  • 位运算
    • & 按位与
      • 语法:全1为1,有0为0
      • 特点:和1与保持不变,和0与为0
      • 使用场景:将指定位清0
    • &= 与等
    • | 按位或
      • 语法:有1为1,全0为0
      • 特点:和1或为1,和0或不变
      • 使用场景:将指定位置1
    • |= 或等
    • ~ 按位取反:0变1,1变0
    • ^ 按位异或
      • 语法:相同为0,不同为1
      • 使用场景:将指定位发生翻转
    • << 左移运算:左边丢弃右边补0
    • >> 右移运算:右边丢弃
      • 无符号数:左边补0
      • 有符号数:正数左边补0;负数逻辑右移左边补0;负数算术右移左边补1
      • 算术右移、逻辑右移都是编译器决定的
// 按照下面的左移推理,得出一个数左移n位就相当于自身乘以2的n次方
//如果data=0000 0001,data=data<<0,data=0000 0001 == 1 == data*2^0
//如果data=0000 0001,data=data<<1,data=0000 0010 == 2 == data*2^1
//如果data=0000 0001,data=data<<2,data=0000 0100 == 4 == data*2^2

//右移,假设data为无符号数
//如果data=1000 0000,data=data>>0,data=1000 0000 == 128 == data除以2^0
//如果data=1000 0000,data=data>>1,data=0100 0010 == 64 == data除以2^1
//如果data=1000 0000,data=data>>2,data=0010 0100 == 32 == data除以2^2

//案例1:data为1字节,将data的第3、4位清0,其他位保持不变
/**
data = data & 1110 0111
    1110 0111 == -(0001 1000) == -(0001 0000 | 0000 1000) == -(0000 0001 << 4 | 0000 0001 << 3) == -(0x01<<4 | 0x01<<3)
data &= -(0x01<<4 | 0x01<<3)
 */
//案例2:data为1字节,将data的第5、6位置1,其他位保持不变
/**
data = data | 0110 0000
    0110 0000 == 0100 0000 | 0010 0000 == 0000 0001 << 6 | 0000 0001 << 5
data |= -(0x01<<6 | 0x01<<5)
 */
  • 三目运算符
    • 表达式 ? 值1 : 值2
  • 输出运算符 <<
  • 输入运算符 >>
  • 运算符优先级
    • 同一优先级,看结合性
int n1 = 10;
int n2 = 20;

(n1>n2 ? n1 : n2) = 100;//c语言中不支持这种写法,c++可以
cout << n1 << endl;//10
cout << n2 << endl;//100

// cout << n1>n2 ? n1 : n2 << endl;//错误,输出运算符<<优先级高于三目运算符
cout << (n1>n2 ? n1 : n2) << endl;//正确

控制语句

  • 选择语句
    • if else
    • switch case
  • 循环控制语句
    • for
    • while
    • do while
  • 辅助控制语句:break、goto、continue、return

数组

  • 用一段连续的空间,存放相同变量的集合
  • 下标从0开始
  • 数组的大小 = 数组元素个数 * 每个元素的大小
  • 数组中未初始化的元素自动补0
// 一维数组
int arr[5] = {1,2,3,4,5};
// int arr[5] = {1,2,3};//部分初始化

//二维数组
int arr1[2][3] = { {1,2,3}, {4,5,6} };
// int arr1[2][3] = { 1,2,3, 4,5,6 };
int row = sizeof(arr1)/sizeof(arr1[0]);//获取二维数组行数
int col = sizeof(arr1[0])/sizeof(arr1[0][0]);//获取二维数组列数

//函数数组:定义一个数组,有2个元素,每个元素为函数地址,函数参数有两个int形参,返回值类型int
int (*arr2[2])(int,int);

int arr3[5] = {0};//0 0 0 0 0,建议不确定内容的数组全部初始化为0
int len = sizeof(arr3)/sizeof(arr3[0]);//获取数组元素个数

//指定数组下标初始化数据
int arr4[5] = { [2]=10, [3]=30 };//0 0 10 30 0 

字符数组-字符串

  • 字符串的方式初始化一维字符数组,编译器会自动在字符串后面添加 '\0'作为字符串的结束标志
  • cin 获取键盘输入的字符串时,遇到空白符即结束
  • cin.getline 可以获取输入中带空格的字符串
char carr[5] = {'a','b','c','d','e'};
char carr1[10] = "hello";//字符串的方式初始化一维字符数组
char carr2[10] = "aa\0bb";
cout << carr2 << endl;//aa cout输入字符串时,遇到\0就结束

//二维字符数组
char carr3[2][3] = { "aaa", "bbb" };

函数

  • 返回值类型 函数名(形参列表){ 函数题 return 返回值 }
  • 函数返回值
    • 在函数执行时,会先将返回值内容放在暂存区
    • 如果>4字节会暂存在栈区,如果<=4字节会暂存在寄存器中
    • 当外部有变量接收返回值时,将暂存区的内容给指定的变量
    • 如果外部没有变量接收返回值,则会立即释放暂存区中对应的内容
  • 函数分类
    • 库函数
    • 自定义函数
    • 系统调用(内核提供给用户的接口)
  • 函数的声明、定义、调用
  • 大量调用函数(如:在fn1函数中调用fn2函数,再fn2中又调用了fn3...),会发生'出入栈'的开销
  • 函数的传参
    • 普通变量作为参数-单项传递值传递
    • 数组作为函数的参数-函数内部可以操作(读写)外部数组的元素

预处理

  • c++编译的四个阶段:预编译、编译、汇编、链接
  • 在32位系统中每一个进程需要占用4G的虚拟空间,其内存分区如下:
描述 内存 描述
可读可写 堆区 使用malloc、calloc、realloc动态申请
可读可写 栈区 局部变量、函数形参、返回值>4字节
可读可写 全局区 普通全局变量、静态全局变量、静态局部变量
只读 文字常量区 数值常量、字符常量、字符串常量、符号常量
只读 代码区 代码的二进制指令
  • 变量存储
    • c++支持通过作用域访问外部变量
    • 使用其他文件的全局变量,需使用 extern 声明
    • 静态变量使用 static 修饰,默认值为0,只初始化一次,函数结束不释放,只能在当前文件使用
#include <iostream>
using namespace std;
// extern int outNum;

//c++支持通过作用域访问外部变量
int n = 10;
namespace A {
    int n = 30;
}
void fn(){
    int n = 20;
    cout << A::n << endl;//10 当内部变量和外部变量重名,又想访问外部变量时,可以这样写,A省略时代表当前文件的全局作用域
}
  • 头文件包含
    • 在预处理阶段将头文件的内容装入目标文件
    • 关键字:#include
  • 宏定义
    • 关键字:#define
    • 预处理阶段会将使用宏变量的地方全部进行宏替换,称为宏展开
    • 不需要分号
    • 建议名字大小
  • 宏函数和普函数的区别
    • 宏函数不需要压栈和弹栈,以空间换时间,普通函数需要压栈和弹栈以时间换空间
    • 宏函数没有作用域限制不能作为类的成员,普通函数有作用域限制可以作为类的成员
#define PI 3.14 //定义宏
#define MY_MUL(a,b) a*b //带参宏
//...
#undef PI //解除/结束宏

指针

  • 32位系统中,为内存的每一个字节分配一个32位的编号(虚拟地址),称为地址
  • 32位系统中任何类型的指针变量都是4字节
  • & 取变量地址
  • * 取指针指向地址对应的内容
  • 指针变量默认值是不确定的,建议用NULL作为初始值
  • 不允许对空指针或未初始化的指针进行操作
int num = 10;
int * p = &num;//指针,p指针的自身类型是int *,p指针的指向类型是int,指针的指向类型决定了指针的取值宽度是多少字节,也决定了p+1的跨度
int * p1,data;//p1是int *类型的指针,data是int型的变量

int (*p2)[5] = NULL;//数组指针
int (*p3)(int,int) = NULL;//函数指针,函数指针指向一个函数后可用指针变量调用函数,不要用*对函数指针取内容没有意义
int * p8[2] = {p, p};//指针数组
struct stu *p4 = NULL;//结构体指针
int ** p5 = NULL;//指针的指针
void * p6 = &num;//野指针,可以保存任意类型的地址,也指针取值时需要强转,如:*(int *)p6;
char * p7 = "aabb";//字符指针变量-字符串
// 指针字符串数组
char * p9[2] = {"hello","world"};
cout << p9[0] << endl;//hello
cout << *p9[0] << endl;//h

//取数组元素时[]其实就是*()的缩写
int arr[2] = {10,20};
cout << arr[1] << endl;//20
cout << *(arr+1) << endl;//20
cout << *(1+arr) << endl;//20
cout << 1 [arr] << endl;//20
// 因为 &arr[0] = &*(arr+0) = arr+0 = arr; 所以数组名就是数组的首地址

//二维字符数组 和 指针字符串数组
char * cp[2] = { "aa", "bb" };//数组中存放的是字符串首地址
char carr[2][3] = { "aaa","bbb" };//数组中存放的就是字符串
  • 指针与函数
    • 基本类型指针作为函数参数
      • 可以在函数内部修改外部变量的值
    • 一维数组作为函数参数
      • 在c++中将函数名传给函数,会被优化成指针变量
    • 二维数组作为函数参数
      • 在c++中将二维数组名传给函数,会被优化为一维数组指针
    • 注意函数不要返回普通局部变量的地址,因为在函数调用结束后普通局部变量会被释放掉
    • 函数指针作为函数的参数
      • 可在函数内部调用外部函数
void fn(int arr[5]){}// 优化后 -> void fn(int * arr){}
void fn(int arr[2][5]){}// 优化后 -> void fn(int (*arr)[5]){}

//给函数指针取别名
typedef int (*FNP)(int,int);
FNP fnp = NULL;

动态内存分配

  • new 申请空间
  • delete 释放空间
int * intp = new int;//从堆区动态申请int类型大小的空间
delete intp;//释放空间
int * intp2 = new int(100);//动态申请空间,并初始化内容

int * iarrp = new int[2]{1,2};//动态申请数组空间,并初始化内容
delete [] iarrp;//释放数组空间

//对一个指针重新赋值前,记得先释放其不需要的空间,防止内存泄露
// if(pstr != NULL){
//     delete pstr;
//     pstr = NULL;
// }
// pstr = new char((leng+1)*sizeof(char))

字符串处理函数

  • 以str开头的字符串处理函数默认遇到\0结束操作
  • 头文件 string.h
    • strlen
      • 测量字符串长度
    • strcpy
      • 字符串拷贝
    • strcat
      • 字符串追加函数
    • strcmp
      • 字符串比较函数

结构体

  • 关键字 struct
  • 先定义结构体类型,再定义结构体变量
  • 定义结构体类型时,不能给成员赋值,c++11版本后支持定义类型时给成员赋值
  • 定义结构体类型时,不会开辟空间,只有定义结构体变量时才开辟空间
  • 未初始化的结构体变量,内容是随机的不确定的
struct Student {
    char name[100];
    int age;
};
Student boy = { "aa", 18 };
cout << boy.name << endl;
memset(&boy, 0, sizeof(boy))//清空结构体变量内容,注意:memset需要string.h头文件

Student boy1 = { "aa", 18 };
memcpy(&boy2, &boy1, sizeof(Student));//相同类型的结构体变量可以使用memcpy函数赋值
Student boy2 = boy1;//相同类型的结构体变量也可以互相赋值,其底层逻辑就是通过memcpy实现的

struct Stu {
    int age;
};
struct Stu2 {
    char name[100];
    Stu st;//结构体嵌套
};
Student starr[1] = {boy}//结构体数组
Student * stp = NULL;//结构体指针

//结构体位段/位域
struct Stu3 {
        unsigned int a:2;//指定a成员只占2位空间
        unsigned int b:6;//指定a成员只占6位空间
        unsigned int c;//int占4字节,即占32位
        unsigned :2;//无意义的位段,表示主动浪费两位空间
        unsigned int d:2;
};
  • 结构体的浅拷贝
    • 相同类型的结构体变量可以相互赋值,默认为钱拷贝
    • 当结构体中有指针成员时,浅拷贝会带来多次释放同一个堆区空间的问题
  • 结构体的深拷贝
    • 为结构体的每一个指针成员,单独申请空间,再拷贝内容

共用体

  • 结构体:所有成员拥有独立空间
  • 共用体:所有成员共享一块空间
  • 关键字 union
  • 共用体空间,由空间最大的成员决定
  • 虽然所有空间共享同一个空间,但是每个成员所能操作的空间是由其自身的类型决定的

枚举

  • 关键字 enum
enum WEEK { mon, tue, wed, thu, fri, sat, sun };
WEEK week = mon;

//无名枚举,一般在类中会使用
enum { a,b,c };

链表

  • 静态数组:必须事先确定数组元素个数,删除/插入数据效率低(需要移动大量的数据),遍历/查找效率高
  • 动态数组:不需要事先知道元素个数,在使用中动态申请,删除/插入数据效率低,遍历/查找效率高
  • 链表:不需要事先知道数据个数,在使用中动态申请,删除/插入数据效率高(不需要移动数据),遍历/查找效率低
  • 链表是由一个个节点组成,节点没有名字,每个节点从堆区动态申请,节点在物理上是非连续的,但是每个节点通过指针域保存下一个节点的位置,达到逻辑上的连续
  • 节点:有自身的内容,同时又保存了下一个节点的地址
  • 带头的链表:第一个元素为节点
  • 不带头的链表:第一个元素为指针,直接保存下一个节点的地址
struct stu {
    //数据域
    int num;
    char name[10];

    //指针域
    struct stu * next;
};
struct stu node1 = {10, "aa", NULL};
struct stu node2 = {10, "bb", NULL};
struct stu node3 = {10, "cc", NULL};
//定义
struct stu * head = &node1;
node1.next = &node2;
node2.next = &node3;
node3.next = NULL;
//遍历
struct stu * pb = head;
while(pb != NULL){
    cout << pb->num << pb->name << endl;
    pb = pn.next;//向下循环
}

c++对c的扩展

  • 命名空间
    • 关键字:namepace、using
    • 命名空间需要定义在全局,不能定义在函数内部
    • 命名空间可以嵌套定义
    • 命名空间可重复定义,系统会把相同名字的命名空间中的变量自动合并到一起
    • 命名空间中的变量声明和定义可以分离
    • 可以定义无名的命名空间(只能在当前文件使用),相当于加了static
    • using声明命名空间中的成员后,可以直接使用。如果命名空间包含一组相同名字重载的函数,using声明对所有的同名函数都有效
  • 作用域操作符
    • ::
//创建一个命名空间
namespace A {
    int n = 10;
    namespace B {
        int n = 20;
    }
}
cout << A::B::n << endl;//20

//命名空间可重复定义
namespace A1 {
    int n = 10;
}
namespace A1 {
    void fun(){}
}

//命名空间中的变量声明和定义可以分离
namespace A2 {
    void fun(int n);
}
void A2::fun(int n){
    cout << "fun" << endl;
}
void test(){
    namespace A3 = A2;//给命名空间起别名,起别名的语句可以写在函数内部
    using A3::fun;
    fun();//using声明后,可直接使用fun函数
}
//using声明整个命名空间,后续使用其成员变量时不再需要加作用域,如果成员变量和局部变量有重名的,优先使用局部变量
// using namespace A;
//在访问一个变量时,先从局部作用域找,找不到再到全局找,再找不到再到命名空间中找
  • struct 类型增强
    • c中定义结构体变量时需要加struct关键字,c++不需要,c中的结构体只能定义成员变量,不能定义成员函数,c++即可以定义成员变量也可以定义成员函数
  • bool 类型关键字
    • 标准c++的bool类型有两种内建的常量true和false表示态
    • 占1个字节
    • 给bool类型赋值时,非0值都会转为true,0值会转为false
//定义bool类型变量
bool flag = true;
  • 引用 reference
    • 在c/c++中指针的作用基本都是一样的,但是c++增加了另外一种给函数传递地址的途径,这就是按引用传递,变量名实质上是一段连续内存空间的别名,是一个标号,程序中通过变量来申请并命名内存空间通过变量的名字可以使用存储空间。c++中新增了引用的概念,可以作为一个已定义变量的别名
    • 引用的本质就是给变量名取个别名
    • 使用 & 符号定义引用变量
    • 函数内部可以通过引用操作外部变量
    • c++建议多用引用,少用指针
    • 常引用
      • 给常量取别名,称为常引用,不能通过常引用修改常量内容
      • 常引用作为函数参数时,可以避免通过引用修改外部变量的内容
int n = 10;
int &n1 = n;//定义引用时必须初始化,n1就是n的别名(引用),n1和n就代表同一块空间
n1 = 20;//操作n1就等于操作n

//数组的引用
int arr[2] = {1,2};
int (&arr1)[2] = arr;

//指针变量的引用
int * p = NULL;
int * &p1 = p;

//函数的引用
void fun(void){}
void (&fun1)(void) = fun;
//引用的最常用的地方:作为函数参数以便操作外部变量
void swapFn(int &x, int &y){
    int tmp = x;
    x = y;
    y = tem;
}
int a = 10;
int b = 20;
swapFn(a,b);

//引用作为函数返回值,可以完成链式操作
struct Stu {
    Stu & fn(Stu &ob, int n){
        cout << n << endl;
        return ob;
    }
}
Stu stu;
stu.fn(stu,1).fn(stu,2).fn(stu,3);

//常引用
const int &cn = 10;
  • 内联函数
    • 在定义时使用 inline 关键字修饰,不能在声明的时候使用 inline
    • 在编译阶段,会将内联函数中的函数体替换到函数调用处,避免函数调用时的开销,以空间换时间
    • 类中的成员函数默认都是内联函数
    • inline修改的函数,不一定会被当作内联函数处理,编译器会根据函数的下述特点来决定是否把其当作内联函数
      • 不能对内联函数取地址
      • 内联函数不能存在循环、条件判断语句,函数体不能太大
  • 内联函数和宏函数的区别
    • 宏函数在预处理阶段展开;内联函数在编译阶段展开
    • 宏函数的参数没有类型;内联函数的参数有类型
    • 宏函数没有作用域的限制不能作为命名空间、结构体、类的成员;内联函数有作用域的现在
void fun(int x, int y);//声明
inline void fun(int x, int y){//定义
    cout << "inline" << endl;
}
  • 函数重载
    • 函数重载是c++的多态特性
    • 用同一个函数名,参数条件(类型/个数/顺序)不同,代表不同的函数功能
    • 返回值不能作为重载的条件,因为有时候在调用函数时可能会忽略函数的返回值,导致编译器不确定调用的是哪个函数
    • 底层实现原理:相同的函数名,编译器会根据其参数条件生成不同的函数名
  • 函数的默认参数
    • c++支持在声明函数时,可以为一个或多个参数指定默认值
    • 如果函数的某个参数指定联默认值,那么它后面的参数也需要指定默认值
    • 如果函数声明和定义分开时,默认参数应写在声明处,不要写在定义处
  • 占位参数
    • 只有类型,没有名称,可以设置默认值
void fn(int a = 10, int b = 20){}
fn(30);//只传一个参数,第二个参数就走默认值

//如下,当默认参数和函数重载同时出现时,要注意出现二义性
// void fn(int a){}
// void fn(int a = 10, int b = 20){}
// fn(10)//这里调用fn时,即符合第一个fn也符合第二个fn,编译器就不知道具体使用哪个函数了

//占位参数
void fn1(int a = 10, int){}
void fn2(int a = 10, int = 20){}
  • extern "c"浅析
    • 在c语言中一个函数void fn(){}被编译后的函数名类似于fn,二在c++中因为要支持重载函数void fn(){}被编译后的函数名类似于_Z6fn。这就使得在c++项目中无法使用c语言编写的代码,使用extern "c"可以解决这个问题
//a.h 头文件
#ifndef __XXX__
#define __XXX__

#if __cplusplus //__cplusplus是固定的宏
extern "C"{
#endif
    // 在这里声明c语言编写的函数
    extern void fn();
    extern void fn2(int a, int b);
#if __cplusplus
}
#endif

#endif

面向对象编程 - 类

  • 面向过程:以过程为中心的编程思想
  • 面向对象:Object Oriented Programming 简称OOP技术,以对象为中心的编程思想
    • 封装、继承、多态

类和对象的基本概念

  • 类将具有共性的数据和方法封装在一起,加以权限区分,用来描述或抽象出一个事物
  • 类的权限分为:private私有的 protected受保护的 public公共的
  • 类中不写权限修饰符时,默认是私有的
  • 类的成员函数可以只在类中声明,在类外实现/定义
  • 类可以作为单独的文件,使用时引入类的头文件即可
  • 类的构造函数:完成对象数据的初始化,由编译器自动调用,如果不显示声明构造函数时,默认会有个空的构造函数
    • 函数名和类名相同
    • 没有返回值
    • 可以有参数,可以重载
    • 写在public权限下
  • 类的析构函数:当对象生命周期结束时,系统会自动调用 以完成对象的清理工作,默认会有个空的析构函数
    • 先调用析构函数,再释放对象空间
    • 函数名和类名相同,前面加关键符~
    • 一般情况下空的析构函数就够了,如果一个类有指针成员,就必须声明显示的析构函数函数,手动释放掉所有指针的空间
    • 析构函数是用栈的形式管理的,先进后出
  • c++不建议在定义类时,对成员变量进行初始化。
//定义一个类
class Test {
    private:
        int num;
        // int num = 10;//仅高版本的c++才支持
    protected:
        int n2;
    public:
        int n3;
        Test(){
            num = 1;
            n2 = 2;
            n3 = 3;
        }
        Test(int pn){
            num = pn;
        }
        void getNum(){
            return num;//在类中可以直接访问当前作用域中的变量,不受权限限制
        }
        void show(void){
            cout << n << n2 << n3 << endl;
        }
        bool compareObj(Test &obj);
        ~Test(){//析构函数
            cout << '析构函数' << endl;
        }
}
//在类外部实现类的方法
bool Test::compareObj(Test &obj){
    if(num() == obj.getNum()){//因为在Test的作用域中所以直接访问num时,指定是当前对象的num
        return true;
    }
    return false;
}

//通过类实例化对象
Test tobj;//隐式调用无参构造
tobj.show();//调用实例对象公共方法

Test tobj2 = Test();//使用无参构造
Test tobj3 = Test(10);//使用有参构造传参
Test tobj5(10);//隐式使用有参构造传参

//当类中只有一个成员时,可以使用构造函数的隐式转换来生成对象
class Test2 {
    int n;
    public:
        Test2(int pn){
            n = pn;
        }
}
Test2 tobj4 = 100;//会转换成Test2 tobj4 = Test2(100);

//析构函数是用栈的形式管理的,先进后出,
//在同作用域中:先构造的后析构
//局部对象先析构,全局对象后析构
Test2 ob1(1);
void fn(){
    Test2 ob2(2);
    {
        Test2 ob3(3);
    }
    Test2 ob4(4);
}
//构造函数被调用顺序:1构造 2构造 3构造 4构造
//析构函数被调用顺序:3析构 4析构 2析构 1析构
//整体顺序:1构造 2构造 3构造 3析构 4构造 4析构 2析构 1析构//一个块级作用域结束时就会立即释放其内部的对象
  • 拷贝构造函数
    • 本质也是构造函数
    • 旧对象初始化新对象时,才会调用拷贝构造函数
    • 如果不现式声明拷贝构造函数,编译器会自动提供一个默认的拷贝构造函数(完成赋值动作-浅拷贝)
    • 拷贝构造函数名和类名一致,参数为类的常引用
    • 主动实现拷贝构造函数时,必须完成属性的拷贝,否则属性将会是随机值
    • 如果类中有指针并且指针指向堆区,就必须主动实现拷贝函数,完成堆区的内容拷贝
class Test3 {
    int n;
    public:
        Test3(){
            n = 1;
        }
        Test3(const Test3 &ob){
            n = ob.n;
            cout << '拷贝构造' << endl;
        }
}
Test3 ob();
//拷贝构造的几种情况
Test3 ob1 = ob;//就对象初始化新对象-调用拷贝构造函数
Test3 fn(Test3 pob){}
fn(ob);//作为函数参数传递时-调用拷贝构造函数

Test3 fn2(){
    Test3 ob2;
    return ob2;
}
//(VS编辑器中)作为函数返回值,在调用该函数时,会执行拷贝构造函数生成匿名对象先存储在暂存区,如果外部有变量接收就交给外部变量,否则立即释放
//(QtCreater、linux中)作为函数返回值,如果外部有变量接收函数返回值,则不会调用拷贝构造函数,也不会释放函数中的ob2,而会把ob2的内容直接交给ob3变量,这样省去了往暂存区存储的过程,提高了性能
Test3 ob3 = fn()

初始化列表

  • 对象成员:在类中定义的数据成员一般是基本数据类型,也可以是类对象,称为对象成员
  • 类中有对象成员时
    • 初始化顺序为:先调用对象成员的构造函数,再调用类的构造函数,析构函数与之相反
    • 默认调用对象成员的无参构造
  • 当需要使用对象成员有参构造时,需要使用初始化列表
class A {
    int an;
    A(){}
    A(int a){
        an = a;
    }
}
class B {
    A oa;
    A oa2;
    int n;
    public:
        B(){}
        B(int a, int b):oa(a),oa2(b) {//初始化列表,在调用B的有参构造时,把参数a传递给A的有参构造

        }
}

explicit 关键字

  • c++提供了关键字explicit,禁止通过构造函数进行的隐式转换,声明为explicit的构造函数不能在隐式转换中使用
  • explicit修饰,是针对单参数或者除了第一个参数其余都有默认值的构造函数
class Test{
    public:
        explicit Test(int n){}
}
// Test ob = 1;//构造函数被explicit修饰后,就不能在用这种语句了
Test ob(1);

类的对象数组

  • 对象数组的本质是数组,数组的每个元素是对象
class A{
    public:
        A(){}
        A(int n){}
}
A arr[5];//当创建元素时,其中的每个对象元素都会调用无参构造函数,当程序结束或数组被释放前,会调用对象元素的析构函数

A arr1[2] = {A(10),A(20)};//初始化数组时,可以使用有参构造

动态对象创建

  • c语言创建动态对象需使用malloc或realoc等函数
    • 需要进行类型强转换
    • 只创建对象空间,不会自动执行构造函数,需要程序员自行处理
    • 使用free释放空间时,不会调用析构函数,需要程序员自行处理
  • c++动态创建对象
    • 使用new运算符,它在堆区为对象分配内存时,会自动调用其构造函数完成初始化
    • 不需要强制类型转换
    • delete释放动态对象时,会先调用析构函数,然后释放内存
class A{
    public:
        A(){}
        A(int a){}
        fn(){}
}
A * oa = new A;
A * oa1 = new A(10);
oa->fn();
delete oa;
delete oa1;

//动态对象数组
A * oarr = new A[20];
delete [] oarr;

静态成员

  • 用static关键字修饰的成员为静态成员,不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享
  • static修饰的静态成员属于类,而不属于对象
  • static修饰的静态成员,在定义类时,必须分配空间
  • static修饰的静态成员,须在类中定义,类外初始化
class Data{
    private:
        static int spn;//私有静态成员变量
    public:
        int a;
        static int b;//静态成员变量
        static void fn(){//静态成员函数
            spn = 10;//静态成员函数只能操作静态成员变量
        }
}
int Data::b = 100;//初始化静态成员数据

Data ob;
cout << ob.b << endl;//100 通过对象访问静态成员
cout << Data::b << endl;//100 可以直接通过类名访问静态成员
ob.b = 200;
Data ob2;
cout << ob2.b << endl;//200 静态成员数据是共享的,一个对象修改其他对象访问时也是修改后的数据

单例模式设计

  • 单例模式是一种常用的软件设计模式,在他的核心结构中只包含一个被称为单例的特殊类,通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源,如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案
//定义单例模式的类
class SingleTest {
    private://1. 防止该类在外界实例化对象 构造函数私有化
        SingleTest(){}
        SingleTest(const SingleTest &ob){}
        ~SingleTest(){}
    private://2. 定义一个静态指针变量保存唯一实例地址
        static SingleTest * const p;
    public://3. 定义获取唯一实例地址的静态成员函数
        static SingleTest * getSingleTest(){
            return p;
        }
        void fn(){}
}
SingleTest * const SingleTest::p = new SingleTest;// 4. 实例化唯一对象

//使用
SingleTest * stp = SingleTest::getSingleTest();
stp->fn();

c++面向对象模型

  • c++实现了'封装',变量数据和处理数据的操作函数是分开存储的,c++中的非静态数据成员直接内含在类对象中,成员函数虽然内含class声明之内,却不出现在对象中,每个非内联成员函数只会诞生一份函数实例
  • sizeof侧一个类的大小时,静态成员不占空间,成员函数不占空间
  • this指针
    • c++提供了特殊的对象指针this,this指针指向被调用的成员函数所属的对象
    • this指针是一种隐含指针,无需定义,无变量重名是可省略this
    • 静态成员函数内部没有this指针
    • 函数内部返回this可以完成链式操作
  • const修饰成员函数
    • const修饰成员函数时,const修饰的是this指针指向的内存区域,表示不可以修改本类中任何普通成员,当成员变量类型符前用mutable修饰时例外
class A {
    int a;
    mutable b;
    public:
        A(int a){
            this->a = a;
        }
        A & fn(){
            a = 1;//this可以省略
            return *this;//返回this,即调用该成员函数的对象
        }
        void show() const {//const修饰成员函数
            // a = 1//Error
            b = 1;//mutable修饰的变量在const函数中可以修改
            cout << a << endl;
        }
}

友元

  • 友元函数是一种特权函数,c++允许这个特权函数访问私有成员
  • 可以把一个全局函数,某个类中的成员函数或者整个类声明为友元
  • 关键字 friend 只需在声明处使用
  • 一个函数或者类作为了另一个类的友元,那么这个函数或类就可以直接访问另一个类的私有数据
  • 友元主要用在运算符重载上
  • 特点
    • 友元关系不能被继承
    • 友元关系是单向的,不能反向使用,A是B的朋友,但是B不一定是A的朋友
    • 友元关系不具有传递性,B是A的朋友,C是B的朋友,但是C不一定是A的朋友
class Room;//向前声明类的名称,供下面使用
class Test{
    public:
        void visit1(Room &room);//想要把该函数当作其他类的友元时,需在外部定义函数体
}
class Test2{
    public:
        void visit2(Room &room);//想要把该函数当作其他类的友元时,需在外部定义函数体
}
class Room {
    friend class Test2;//指定一个类为友元,则该友元类中的所有函数可以直接访问Romm的资源属性
    friend void Test::visit1(Room &room);//指定某个类的函数为友元函数,Test需要定义在Room类前面
    friend void visit3(Room &room);//指定全局函数为友元函数,表示在visitFn这个函数中可以直接访问Room的私有属性
    private:
        string bedRoom;
    public:
        Room(string bedRoom){
            this->bedRoom = bedRoom;
        }
}
void visit1(Room &room){
    cout << room.bedRoom << endl;
}
void visit2(Room &room){
    cout << room.bedRoom << endl;
}
void visit3(Room &room){
    cout << room.bedRoom << endl;
}
Room room("卧室");
visit2(room);

string 类

  • 需要 #include <string>
  • C++中对于string的定义为:typedef basic_string string; 也就是说C++中的string类是一个泛型类,由模板而实例化的一个标准类,本质上不是一个标准数据类型
  • string类的字符串加法运算,会进行字符串拼接
  • api
    • c_str
      • 将string类对象转成char*
#include <string>
string str = "aa";
string str1 = "bb";
cout << str+str1 << endl;//aabb

运算符重载

  • 运算符重载,就是对已有对运算符重新定义,赋予其另一种功能,以适应不同的数据类型
  • 语法:定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里@代码被重载的运算符
  • 思路
    • 运算符的运算对象的个数,决定了重载函数的参数个数
    • 运算符左边的运算对象是类的对象时,推荐用成员函数实现。其他用全局函数实现
  • 不建议重载的运算符
    • && ||
    • 因为无法实现其短路特性
  • 不可以重载的运算符
    • . :: .* ?: sizeof
  • 如果希望对类的实例对象进行赋值运算,则必须进行赋值运算符重载,否则如果有指针参与时,容易造成内存泄露
  • 重载=赋值运算符时,如果参数(即参与运算的对象)中有指针时,必须进行深拷贝
class Person{
    friend ostream& operator<<(ostream &out, Person ob);
    friend istream& operator<<(istream &in, Person &ob);
    friend Person operator+(Person &ob1, Person &ob2);
    private:
        int num;
        string name;
    public:
        Person(){}
        Person(int num, string name):num(num),name(name){}
        //成员函数完成加法运算符重载
        Person operator+(Person &ob){
            Person temp;
            tem.num = num + ob.num;
            tem.name = name + ob.name;//字符串加法运算,会进行字符串拼接
            return temp;
        }
        //成员函数完成相等运算符重载
        bool operator==(Person &ob){
            return num == ob.num && name == ob.name;//string声明的字符串可以直接用==进行判断,字符数组字符串需要使用字符串比较函数
        }
}
ostream& operator<<(ostream &out, Person ob){//重载<<运算符,支持输出打印Person实例对象,cout的类型是ostream
    out << ob.num << ob.name << endl;
    return out;
}
Person pa(10, "aa");
Person pb(20, "bb");
cout << pa << pb << endl;//<<运算符默认只支持输出基本数据类型,要输出自定义的类对象,就需要自己进行运算符重载,可以认为底层转化调用了operator<<(cout, pa)函数

//重载>>运算符
istream& operator<<(istream &in, Person &ob){
    in >> ob.num >> ob.name;
    return in;
}
cin >> pa >> pb;//>>运算符默认也只支持基本数据类型,需要自定义相关的输入重载

//利用重载加法运算符,对类的实例对象进行加法运算
cout << pa + pb << endl;//pa + pb ==> pa.operator+(pb)
  • 重载++/--运算符
    • 重载++/--运算符有点不一样,因为我们总是希望能根据它们出现在所作用对象的前面还是后面来调用不同的函数
    • ++a ===> operator++(a)
    • a++ ===> operator++(a,int)
class Person{
    private:
        int num;
    public:
        Person(){}
        Person(int num):num(num){}
        //重载后置++
        Person operator++(int){//在类的成员函数中this可以看作第一个参数,就可以少声明一个参数
            Person old = *this;
            this->num++;
            return old;
        }
        //重载前置++
        Person operator++(){//在类的成员函数中this可以看作第一个参数,就可以少声明一个参数
            this->num++;
            return *this;
        }
}
// class MyString{}
// - 有时间可以找资料练习一下自定义string类,实现各种运算符重载
  • 重载函数调用运算符
    • 重载 () 运算符,一般用于为算法提供策略
    • 当对象和()结合时,就会触发函数调用重载函数
class Test{
    public:
        void operator()(char * str){//重载 () 运算符
            cout << str << endl;
        }
}
Test testObj;
testObj("aa");//对象和()结合,也叫仿函数

Test()("bb");//因为Test()会生成匿名对象,所以加上()也可以触发重载函数
  • 智能指针-指针运算符重载
    • 在开发中有时会忘记释放堆区空间,导致内存泄露
    • 利用普通对象,在程序运行完毕后/对象生命周期结束时,会自动指向析构函数的特点,解决堆区空间对象自动释放的问题
class Test{
    public:
        void fn(){}
}
class SmartPointer{
    private:
        Test *p;
    public:
        SmartPointer(){}
        SmartPointer(Test *p){
            this->p = p;
        }
        ~SmartPointer(){//析构函数
            delete p;
        }
        //重载->运算符
        Test* operator->(){
            return p;
        }
        //重载*运算符
        Test& operator*(){
            return p;
        }
}
int main(){
    SmartPointer sp(new Test);//通过SmartPointer生成的对象,当sp声明周期结束时会自动释放其中p指针的堆内存
    sp->fn();
    (*sp).fn();
    return 0;
}

继承

  • c++重要的特征就是代码重用,通过继承机制可以利用已有的数据类型定义新的数据类型,新的数据类型不仅有旧类的成员,也有新的成员
  • B类继承A类,也称从A类派生B类,这样A类为基类,B类为派生类
  • 继承过来的成员表现其共性,新增的成员体现了其个性
  • private 私有继承,以私有的方式继承,继承的成员全变成子类的私有成员,默认的继承修饰符为private
  • protected 保护继承,以保护的方式继承,继承的成员全变成子类的受保护成员
  • public 共有继承,以公共的方式继承,继承的成员在父类是什么,在子类依然是什么
  • 所有父类中的私有属性在子类中都不可访问
  • 子类构造函数顺序:
    • 父类构造->子类成员构造->子类构造->子类析构->子类成员析构->父类析构
  • 子类实例化对象时,会自动调用成员对象、父类对象的默认构造
  • 子类必须使用初始化列表调用成员对象、父类的有参构造
  • 初始化列表时,父类用类名,成员对象用对象名
  • 子类成员和父类成员重名时
    • 默认访问自己的成员
    • 需要访问父类成员时,加作用域
  • 重定义
    • 子类可以重新重新定义父类同名的函数,参数可以不同,非虚函数
    • 子类一旦重定义类父类同名函数,不管参数是否相同,子类中将屏蔽父类中所有的同名函数,可以加作用域来访问
  • 不能继承的函数
    • 构造函数
    • 析构函数
    • 运算符重载函数
class Parent{
    int a;
    public:
        Parent(){}
        Parent(int a){
            this->a = a;
        }
        fn(){}
}
class Son:public Parent{
    int sa;
    public:
        Son(){}
        Son(int a):Parent(a) {
            sa = a;
        }
        fn(){}
}
Son sonObj;
sonObj.Parent::fn();//重名时调用父类方法
  • 多继承
    • c++中可以从一个类继承,也可以从多个类继承,用逗号分隔,称为多继承
    • 从多个类继承,可能会导致函数、变量有较多的重名情况
class Parent1{}
class Parent2{}
class Son:public Parent1, protected Parent2 {}
  • 菱形继承
    • 有公共祖先的继承,称为棱形继承
    • 最底层的子类,包含多份公共祖先的数据
class A{}
class B:public A{}
class C:public A{}
class D:public B, protected C {}
  • 虚继承
    • 用于解决棱形继承中会出现多份数据的问题
    • 关键字 virtual
    • 子类中只会保存一份公共祖先的数据
class A{}
class B:virtual public A{}
class C:virtual public A{}
class D:public B, protected C {}

多态

  • 多态是面向对象程序设计语言中数据抽象和继承之外的第三个基本特征,多态性提供接口与具体实现之间的另一层隔离,从而将what和how分离开来,多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性
  • 静态多态(编译时多态,早绑定):函数重载、运算符重载、重定义
  • 动态多态(运行时多态、晚绑定):虚函数

虚函数

  • 父类指针/引用保存子类空间地址
  • 被关键字 virtual
  • 虚函数的动态绑定机制
    • 如果一个类中的成员函数被virtual修饰,那么这个函数就是虚函数,类就会产生一个虚函数指针vfptr,指向了一张虚函数表vftable,如果这个类没有被继承,那么这个虚函数表中记录的就是虚函数的入口地址
    • 如类被继承了,子类也会继承父类中的虚函数地址,如果子类重写了父类的虚函数,那么在实例化时就会将子类的虚函数入口地址替换到虚函数表中,只后再调用该虚函数时从虚函数表中找到的就是子类重新的虚函数的入口地址,就间接地调用了子类重新的函数
class Parent{
    public:
        virtual void fn(int n){// 1 在成员函数前加virtual修饰
            cout << "parent" << n << endl;
        }
}
class Son1:public Parent {
    public:
        //virtual可省略,返回值、函数名、参数必须全都和父类保持一致
        virtual void fn(int n){//2 在子类中重定义父类的同名函数
            cout << n << "son 1" << endl;
        }
}
class Son2:public Parent {
    public:
        virtual void fn(int n){
            cout << n << "son 2" << endl;
        }
}
Parent * op = new Son1;//父类指针保存子类空间地址,通过虚共用函数实现个性化功能
Parent * op2 = new Son2;

void testFn(Parent * p){
    p->fn(100);
}
testFn(op);//100 son 1
testFn(op2);//100 son 2

纯虚函数

  • 如果基类一定派生出子类,而子类一定重新父类的虚函数,那么父类的虚函数就可以不写函数体,称为纯虚函数
  • 有纯虚函数的类,就是一个抽象类,不可以直接实例化
  • 抽象类主要的目的就是设计子类的接口
  • 抽象类的子类,必须重新父类所有的纯虚函数
class Base {
    public:
        virtual void fn(int a) = 0;//用=0代替函数体
}

虚析构函数

  • 通过父类指针,释放子类的空间
  • 纯虚析构
    • 纯虚析构的本质是虚构函数,完成各个类的回收工作,而且析构函数不能被继承
    • 纯虚析构函数需要有函数题,而且必须在类外定义函数体
    • 纯虚析构的类也是抽象类
class Parent {
    public:
        virtual ~Parent(){
            cout << "Parent 析构" << endl;
        }
}
class Son:public Parent {
    public:
        ~Son(){
            cout << "Son 析构" << endl;
        }
}
Parent * p = new Son;
delete p;//如果父类析构函数没有用virtual修饰,则这里只会释放p指向空间中父类成员所占用的内容也就是只调用父类的析构函数,virtual修饰后,既可以释放所有的空间,即父类和子类的析构函数都会被调用

//纯虚析构
class Parent2 {
    public:
        virtual ~Parent() = 0;
}
Parent2::~Parent(){
    cout << "Parent2 析构" << endl;
}

模板

  • c++编程思想有两种:
    1. 面向对象编程
    2. 泛型编程(模板)

模板的概述

  • c++提供了函数模板,实际上是建立一个通过函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表,这个通用函数就成为函数模板。凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数。在函数调用时系统会根据实际参数的类型来取代模板中的虚拟类型,从而实现不同的功能。c++提供两种模板机制:函数模板和类模板

函数模板

  • 关键字 template
  • 函数模板会编译两次
    1. 对函数模板本身编译
    2. 函数调用处,将虚拟的类型具体化
  • 函数模板目标:模板是为了实现泛型,可以减轻编程的工作量,增强函数的重用性
  • 注意点:
    • 函数模板和普通函数都识别时,优先选择普通函数
    • 函数模板和普通函数都识别时,可以加<>强制使用函数模板
    • 函数模板自动类型推导时,不能对函数的参数进行隐式类型转换
  • 函数模板可以重载
template<typename T> void fn(T &a, T &b){}
fn(1, 2);//自动完成类型推导
fn('a', 'b');//自动完成类型推导

template<typename T>//T同一时间只能是一个类型
void fn2(T &a, T &b){}//模板函数

void fn2(int &a, int &b){}//普通函数
fn2(1, 2);//普通函数
fn2<>(1, 2);//强制使用函数模板
fn2<int>(1, 2);//强制使用函数模板,并显示声明泛型为int类型
fn2(1, 'a');//普通函数
fn2<>(1, 'a');//识别的是普通函数,函数模板无法对函数的参数进行隐式类型转换,T同一时间只能是一个类型
fn2<int>(1, 'a');//函数模板,相当于主动把参数进行了强制类型转换
  • 函数模板的局限性
    • 当模板函数推导出虚拟类型为数组或其他自定义类型数据时,可能导致运算符不识别
    • 解决办法:
      1. 运算符重载
      2. 具体化函数模板
class CA {
    friend ostream & operator<<(ostream & out, CA ob);
    private:
        int num;
    public:
        CA(){}
        CA(int num){
            this->num = num;
        }
}
friend ostream & operator<<(ostream & out, CA ob){
    out << ob.num << endl;
    return out;
}

template<typename T>
void myPrint(T a){
    cout << a << endl;
}
// template<> void myPrint<CA>(CA a){//具体化函数模板,当参数为CA类型时,就走这个函数的逻辑
//     cout << a.num << endl;
// }

myPrint(10);//10

CA caObj(100);
myPrint(caObj);//100

类模板

  • 有时有两个或多个类,其功能是相同的,仅仅是数据类型不同,类模板用来声明类所需的数据的类型参数化
  • 类模板,在实例化对象时支持自动类型推导,必须手动指定虚拟类型的具体类型
  • 类模板的声明和定义分开后(如:xxx.cpp文件中定义和xxx.h中声明),在其他文件中使用时需要把两个文件全部引入才能使用。开发中一般习惯把类模板的声明和定义写到一个文件中.hpp为后缀名,这样直接#include <xxx.hpp>就行了
template<class T, class T2> class CA {
    private:
        T a;
        T2 b;
    public:
        CA(){}
        CA(T a, T b){
            this->a = a;
            this->b = b;
        }
        void fn(T a, T2 b);
}

CA<int, char> caObj(10, 'a');
  • 类模板的成员函数在类外实现时,需要单独声明模板,因为template只作用域和它最近的语句
template<class T, class T2> class CA {
    private:
        T a;
        T2 b;
    public:
        CA(){}
        CA(T a, T b);
        void fn(T a, T2 b);
}

template<class T, class T2>
CA<T, T2>::CA(T a, T b){
    this->a = a;
    this->b = b;
}

template<class T, class T2>
void CA<T, T2>::fn(T a, T2 b){
    cout << a << b << endl;
}
  • 函数模板作为类模板的友元
template<class T, class T2>
class CA {
    template<class T3, class T4>
    friend void mySprint(CA<T3, T4> &ob);
    private:
        T a;
        T2 b;
    public:
        CA(){}
        CA(T a, T b){
            this->a = a;
            this->b = b;
        }
}
template<class T3, class T4>
void mySprint(CA<T3, T4> &ob){
    cout << ob.a << ob.b << endl;
}
CA<int, char> caObj(10, 'a');
mySprint(caObj);
  • 普通函数作为类模板的友元,必须指明具体类型
template<class T, class T2>
class CA {
    friend void mySprint(CA<int, char> &ob);
    private:
        T a;
        T2 b;
    public:
        CA(){}
        CA(T a, T b){
            this->a = a;
            this->b = b;
        }
}
void mySprint(CA<int, char> &ob){
    cout << ob.a << ob.b << endl;
}
CA<int, char> caObj(10, 'a');
mySprint(caObj);
// 有时间可以找资料练习一下自定义数组类模板
  • 类模板的继承
template<class T, class T2>
class Parent {}

//类模板派生出普通类,需指明类型
class Son:public Parent<int, char> {}

//类模板派生出类模板
template<class K, class K2, class K3>
class Son2:public Parent<K, K2> {
    private:
        K1 pa;
        K2 pb;
        K3 pc;
}

c++类型转换新语法-模板转换函数

  • c++支持c语言的强制类型转换语法,有提出了一种新的可以更好地控制转换过程的强制类型转换
  • 上行转换:将子类转换为父类,子类空间交给父类指针(安全)
  • 下行转换:将父类转换为子类,父类空间交给子类指针(不安全,会造成内存越界)

static_cast

  • 静态类型转换
  • 用于类层次结构中基类和派生类之间指针或引用的转换
class Parent {}
class Son:public Parent {}

//基本类型 - 支持
int num = static_cast<int>(3.14);
//上行转换 - 支持 安全
Parent * p = static_cast<Parent *>(new Son);
//下行转换 - 支持 不安全
Son * p2 = static_cast<Son *>(new Parent);
//不相关类型转换 - 不支持

dynamic_cast

  • 静态类型转换
  • 用于类层次间的上行转换
class Parent {}
class Son:public Parent {}

//上行转换 - 支持 安全
Parent * p = dynamic_cast<Parent *>(new Son);

//下行转换 - 不支持 不安全
//基本类型 - 不支持支持
//不相关类型转换 - 不支持

const_cast

  • 常量转换
//将const修饰的指针转换成非const - 支持
const int * p;
int * p1 = const_cast<int *>(p);
const int &num = 10;
int &num2 = const_cast<int &>(num);
//将非const转成const - 支持
int * p2;
const int * p3 = const_cast<const int *>(p2);
int num3 = 10;
const int &num4 = const_cast<const int &>(num3);

reinterpret_cast

  • 重新解释转换
  • 最不安全的一种转换
class Parent {}
class Son:public Parent {}
class Other {}

int num = reinterpret_cast<int>(3.14);
//上行转换 - 支持 安全
Parent * p = reinterpret_cast<Parent *>(new Son);
//下行转换 - 支持 不安全
Son * p2 = reinterpret_cast<Son *>(new Parent);
//不相关类型转换 - 支持
Parent * p3 = reinterpret_cast<Parent *>(new Other);
//基本类型指针转换 - 支持
float * fp;
int * intp = reinterpret_cast<int *>(fp);

//基本类型 - 不支持

异常

  • 抛出异常、捕获异常
  • 如果没有主动捕获抛出的异常,程序一旦出错将被系统接管并停止运行
  • 栈解旋
    • 异常被抛出后,从进入try块起,到异常抛出(throw)前,这期间在栈上构造的对象,都会被自动析构,析构的顺序与构造函数顺序相反,这一过程称为栈的解旋
  • 内置的 exception类 是所有异常的基类,自定义异常类时必须继承此类,并重新what函数
// try {
//     throw "抛出异常";
// } catch(异常类型1 异常值1) {
//     //只捕获类型匹配的异常
// } catch(异常类型2 异常值1) {
//     //只捕获类型匹配的异常
// } catch(...) {
//     //任何异常都捕获
// }
try {
    // throw 1;
    // throw 'a';
    throw 1.12f;
} catch (int a) {
    cout << a << endl;
} catch (char b) {
    cout << b << endl;
} catch (...) {
    cout << "未匹配的异常" << endl;
}
  • 异常的接口说明
    • 描述可以抛出哪些异常
    • 没有描述的异常将被系统接管
    • 函数默认可以抛出任何类型的异常
//指定抛出特定的异常
void fn() throw(int,char){}
try {
    fn();//因fn指定了抛出异常为int和char类型,所以这个try后的catch只能捕获到int和char类型的异常,即便写了flat异常捕获也捕获不到
} catch (int a) {
} catch (char a) {
} catch (float a) {
}

//不能抛出任何异常
void fn2() throw(){}
  • 异常变量的声明周期
    • 普通对象接异常值,会触发:异常对象构造、拷贝构造、普通对象接异常、异常对象析构、普通对象析构
    • 对象指针接异常值,会触发:异常对象构造、普通对象接异常、异常对象析构
      • 需要手动delete释放接异常的对象指针
    • 对象引用接异常值,会触发:异常对象构造、普通对象接异常、异常对象析构
      • 不需要手动delete
//推荐使用 对象引用接异常值
class MyException:public exception {
    string msg;
    public:
        MyException(){}
        MyException(int msg){
            this->msg = msg;
        }
        virtual const char * what() const throw()//防止父类在子类前抛出标准异常
        {
            return this->msg.c_str();//c_str将string类对象转成char*
        }
}
try {
    throw MyException('错误信息');
} catch (MyException &e){
    cout << e.what() << endl;
}
  • 异常的多态
    • 通过一个基类异常派生出多个子孙类的异常
    • 可使用基类异常去接由该基类派生出的子类异常
  • c++常见的内置标准异常
    • exception 所有异常的基类
      • 其有一个虚函数 what 用于打印异常信息,所有子类都会重写这个虚函数
    • logic_error
    • runtime_error
    • out_of_range
try {
    throw out_of_range('越界了');
} catch (exception &e) {
    cout << e.what() << endl;
}

STL之容器

  • 为了建立数据结构和算法的一套标准,并且降低它们之间的耦合关系,以提升各自的独立性、弹性、交互操作性(相互合作性),提出了STL的概念(Standard Template Library 标准模板库)
  • STL是惠普实验室开发的一系列软件的统称,现在主要出现在c++中
  • STL从广义上分为:容器、算法、迭代器
    • 容器和算法之间通过迭代器进行无缝连接
  • STL六大组件
    1. 容器-存放数据
    2. 算法-操作数据
    3. 迭代器-算法只能借助迭代器操作容器数据
      • 类似于指针
    4. 仿函数-为算法提供更多的策略
    5. 适配器-为算法提供更多参数的接口
    6. 空间配置器-为算法和容器动态分配、管理空间
  • STL的一个重要特性是将数据和操作分离,数据由容器类别加以管理,操作则由特定的算法完成
  • 算法分为质变算法和非质变算法
    • 质变算法:是指运算过程中会更改区间内的元素的内容,如:拷贝、删除等
    • 非质变算法:是指运算过程中不会更改区间内的元素内容,如:查找、计数
  • 容器通用的api
    • size()
      • 获取容器的大小(可以认为是有实际数据的空间)
    • capacity()
      • 获取容器的容量(可以认为是所占的全部空间)
    • empty()
      • 是否为空

string类

  • string是char型的容器,对char*进行了封装和管理
  • npos
    • string::npos的定义: staticconstsize_type npos=-1
    • string::npos作为string的成员函数的一个长度参数时,表示“直到字符串结束(until the end of the string)”
  • 构造函数
    • string()
    • string(const strimg & str)
      • 使用一个string对象构造另一个string对象
    • string(const char * s)
    • string(int n, char c)
  • 基本赋值api
    • string& operator=(const char * s)
    • string& operator=(const string &s)
    • string& operator=(char c)
    • string& assign(const char * s)
    • string& assign(const char * s, int n)
    • string& assign(const string &s)
    • string& assign(int n, char c)
    • string& assign(const string &s, int start, int n)
  • 存取操作api
    • char& operator[](int n)
      • 越界不会抛出异常
    • char& at(int n)
      • 越界会抛出异常
  • 拼接api
    • string& operator+=(const string &str)
    • string& operator+=(const char * str)
    • string& operator+=(const char c)
    • string& append(const char * s)
    • string& append(const char *s, int n)
    • string& append(const string &s)
    • string& append(const string &s, int pos, int n)
    • string& append(int n, char c)
  • 查找和替换api
    • int find(const string &str, int pos = 0) const
      • 找不到返回-1
    • int find(const char * s, int pos = 0) const
    • int find(const char * s, int pos, int n) const
    • int find(const char c, int pos = 0) const
    • int rfind(const string &str, int pos = npos) const
      • 从最后一个位置查找
    • int rfind(const char * s, int pos = npos) const
    • int rfind(const char * s, int pos, int n) const
    • int rfind(const char c, int pos = 0) const
    • string& replace(int pos, int n, const string &str)
      • 替换
    • string& replace(int pos, int n, const char * s)
  • 比较操作api
    • int compare(const string &s) const
      • 大于返回1,小于返回-1,等于返回0
    • int compare(const char * s) const
    • string除了提供上述方法外,也重载了 > < == 等操作符
  • 提取子串api
    • string substr(int pos = 0, int n = npos) const
  • 插入和删除
    • string& insert(int pos, const char * s)
    • string& insert(int pos, const string &str)
    • string& insert(int pos, int n, char c)
    • string& erase(int pos, int n = npos)
      • 删除
// string容器和c语言指针字符串的转换
// c++支持指针字符串自动隐式转换为string类对象,但是不支持string类对象隐式转换为指针字符串,需要使用c_str成员函数
string str = "aaa";//指针字符串可之间赋值给string对象
char * str2 = (char *)str.c_str();//string对象需要通过c_str转为指针赋值给指针字符串

vector向量容器

  • 需要头文件 #include<vector>
  • vector与数组非常相似,数组是静态空间,而vector是单端动态数组,是动态分配空间
  • vector的数据结构是线性连续空间,
  • vector的未雨绸缪机制:vector实际配置的大小可能比实际需求大一些(多开辟几个字节空间),以备将来可能的扩充,这便是容量的概念,后续如果空间满了就会自动扩充旧空间的2倍空间
  • 注意:所谓动态增加大小,并不是在原空间之后续接新空间(因为无法保证原空间之后有可配置的空间),而是开辟一块更大的内存空间,然后将原数据拷贝到新空间,并释放原空间,因此对vector的任何操作,一旦引起空间的重新配置,指向原vector的所有迭代器就都失效了
  • vector是类模板,使用时需指定类型
  • begin()
    • 获取容器的起始迭代器(指向第0个元素的位置)
  • end()
    • 获取容器的结束迭代器(指向最后一个元素的下一个位置)
  • push_back()
    • 添加元素
  • 构造函数
    • vector<T> v;
    • vector(v.begin(), v.end());
    • vector(n, elen);
    • vector(const vector &vec);
  • 常用赋值操作
    • assign(beg, end)
    • assign(n, elen)
    • vector& operator=(const vector &vec)
    • swap(vec)
      • 交换容器的指向,交换后容量会发生变化
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
//通过迭代器遍历容器
vector<int>::iterator it = v.begin();
while(it != v.end()){
    cout << *it << endl;//*it等价于v容器中的元素数据
    it++;
}

//巧用swap收缩vector空间
//1 如果一开始用reserve预留了较大的空间
vector<int> v1;
v1.reserve(1000);
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
//后续又觉得用不完,觉得有点浪费,可使用swap将释放掉无用的空间
cout << v1.capacity() << v1.size() << endl;// 1000 3
vector<int>(v1).swap(v1);//先用v1创建一个匿名对象(只会拷贝内容的所以容量是3),然后交换匿名对象和v1的指向,匿名函数结束后会自动释放空间,而v1已经指向了新的容量只有3的内存空间
cout << v1.capacity() << v1.size() << endl;// 3 3
  • vector大小操作api
    • empty()
      • 内容是否为空
    • resize(int n)
      • 重新指定容器内容的长度,超过旧长度默认补0,小于旧长度删除多余部分
      • 并不改变容器容量的大小
    • resize(int n, elen)
      • 重新指定容器内容的长度,超过旧长度补elen
      • 并不改变容器容量的大小
    • reserve(int n)
      • 用于请求向量分配的改变,指定向量容器所需空间,可用于给容器提前预留足够的空间
  • 存取api
    • at(int idx)
      • 获取idx所在位置的数据
    • operator[]
    • front()
      • 返回第一个元素
    • back()
      • 返回最后一个元素
  • 插入和删除api
    • insert(const_iterator pos, int count, ele)
      • 在pos迭代器位置后面插入count个ele元素
      • 注意迭代器不是int类型的下标,但是和下标的用法相似
    • push_back()
      • 追加元素
    • pop_back()
      • 删除最后一个
    • erase(const_iterator start, const_iterator end)
      • 删除在start和end之间的元素,不包含end位置的元素
    • erase(const_iterator pos)
      • 删除指定迭代器指向的元素
    • clear()
      • 清空内容大小,容量不变
//存放vector的vector类似于二维数组
vector<int> v1;
vector<int> v2;
vector<int> v3;

vector< vector<int> > vv;
vv.push_back(v1);
vv.push_back(v2);
vv.push_back(v3);
//通过迭代器遍历
vector< vector<int> >::iterator it = vv.begin();
while(it != vv.end()){
    vector<int>::iterator mit = (*it).begin();
    while(mit != (*it).end()){
        cout << *mit << endl;
        mit++;
    }
    it++;
}

deque 容器

  • deque是一种双向开口的连续线性空间,可以在头尾两端分别添加和删除元素,也可以在中间插入元素
  • deque没有容量的概念,他是动态的以分段连续空间组合而成的,不需要提供预留空间,当旧空间不足时,开辟一段新空间存储新内容,不会把旧内容拷贝到新空间,而是通过中空器把新旧不同的空间在逻辑上联系到一起
  • 需头文件 #include<deque>
  • 常用api
    • push_front() 头部添加
    • pop_front() 头部删除
    • push_back() 尾部添加
    • pop_back() 尾部删除
    • insert() 中间插入
    • begin() 获取开始位置
    • end() 获取尾部位置
    • front() 获取头部内容
    • back() 获取尾部内容
  • deque 构造函数
    • deque(T) deqT;
    • deque(beg, end);
    • deque(n, elen);
    • deque(const deque &deq);
  • deque 赋值操作
    • assign(beg, end);
    • assign(n, elen);
    • deque operator=(const deque &deq);
    • swap(deq);
  • deque 大小操作
    • size()
    • empty()
    • resize(num)
    • resize(num, elen)
  • deque 数据存取
    • at(idx);
    • operator[];
    • front();
    • back();
  • deque 插入操作
    • insert(pos, elen);
    • insert(pos, n, elen);
    • insert(pos, beg, end);
  • deque 删除操作
    • clear();
    • erase(beg, end);
    • erase(pos);

stack 容器

  • #include<stack>
  • stack是一种先进后出的栈数据结构容器,只有一个口
  • first in last out
  • stack没有迭代器,不提供遍历操作,可以间接遍历
  • push()入栈 pop()出栈 top()获取栈顶内容
  • stack 构造器
    • stack<T> stkT;
    • stack(const stack &stk);
  • stack 赋值操作
    • stack& operator=(const stack &stk);
  • stack 数据存取操作
    • push(elen);
    • pop()
    • top()

queue 容器

  • #include<queue>
  • queue是一种先进先出的队列数据结构容器,有两个口,允许从一端新增元素,从另一端删除元素
  • first in first out
  • 不提供迭代器,不提供遍历操作,可以间接遍历
  • push() back() front() pop()
  • queue 构造函数
    • queue<T> queT;
    • queue(const queue &que);
  • api
    • push(elen);
      • 入栈添加
    • pop()
      • 出栈删除
    • back()
      • 返回最后一个元素内容
    • front()
    • queue& operator=(const queue &que)

list 链表容器

  • #include<list>
  • 链表是一种物理存储单元上非连续,非顺序的存储结构,在逻辑上的顺序是通过链表中的指针链接次序实现的,由一系列节点组成
  • list容器是一个双向链表
  • list采用动态存储分配,不会造成内存浪费和溢出,链表执行插入和删除容易,修改指针即可,查询复杂
  • list提供的迭代器是双向迭代器,不支持+n操作,仅支持++操作
  • list 构造函数
    • list<T> lstT;
    • list(beg, end);
    • list(n, elen);
    • list(const list &lst);
  • list 数据元素插入和删除操作
    • push_back(elen)
    • pop_back()
    • push_front(elen)
    • pop_front()
    • insert(pos, elen)
    • insert(pos, n, elen)
    • insert(pos, beg, end)
    • clear()
    • erase(beg, end)
    • erase(pos)
    • remove(elen)
  • list 大小操作
    • size()
    • empty()
    • resize(num)
    • resize(num, elen)
  • list 赋值操作
    • assign(beg, end)
    • assign(n, elen)
    • list& operator=(const list &lst)
    • swap(lst)
  • list 数据的存取
    • front()
    • back()
  • list 反转排序
    • reverse()
    • sort()
      • STL标准的算法,只支持随机访问迭代器,而list是双向迭代器,所以不能用STL的sort函数排序,所以list容器自己实现了sort函数用于排序

set/multiset 容器

  • #include<set>
  • set所有的元素都会根据元素的键值自动被排序,set的元素即是键值友是实值
  • set不允许有相同的键值
  • multiset和set用法相同,唯一的不同是它可以有重复的键值
  • set内部是用二叉树的结构存储数据的,提供的迭代器是只读迭代器,不能通过迭代器修改内容
  • set 构造函数
    • set<T> st;
    • set<T, 自定义排序规则仿函数> st;
    • multiset<T> mst;
    • set(const set &st);
  • set 赋值操作
    • set& operator=(const set &st);
    • swap(st)
  • set 大小操作
    • size()
    • empty()
  • set 插入和删除操作
    • insert(elen)
    • clear()
    • erase(pos)
    • erase(beg, end)
    • erase(elen)
  • set 查找操作
    • find(key)
      • 查找key是否存在,存在返回对应的迭代器,不存放返回最后位置的迭代器
    • count(key)
      • 统计key个数
    • lower_bound(keyelen)
      • 上限,返回第一个>=keyelen的元素的迭代器
    • upper_bound(keyelen)
      • 下限,返回第一个>keyelen的元素的迭代器
    • equal_range(keyelen)
      • 以对组的方式,返回上下限两个迭代器
//仿函数-重载函数调用运算符()的类
class MyGreater {
    public:
        bool operator()(int v1, int v2){
            return v1 > v2;
        }
}
set<int, MyGreater> s1;
s1.insert(1);
s1.insert(3);
s1.insert(2);
s1.insert(5);
s1.insert(4);
set<int, MyGreater>::const_iterator it = s1.begin();
for(; it != s1.end(); it++){
    cout << *it << endl;
}

pair 对组

  • pair将一对值组合成一个值,这对值可以具有不同的数据类型,分别为pair的两个公有属性first和second
//创建对组方式一
pair<int, string> p1(100, "aaa");
pair<int, string> p2(101, "bbb");
//创建对组方式二(推荐)
pair<int, string> p3 = make_pair(102, "ccc");
cout << p3.first <<< p3.second << endl;

map/multimap 容器

  • #include<map>
  • map的特性是,所有元素都会根据元素的键值自动排序,所有的元素都是pair,同时拥有实值和键值,pair的第一元素被视为键值,第二元素被视为实值,map不允许两个元素有相同的键值
  • map提供只读迭代器,不能通过map的迭代器改变map的键值,可以改变实值
  • map和list一样,也是分段连续存储
  • multimap和map功能一样,唯一不同的是multimap的键值可以重复
  • map 构造函数
    • map<T1, T2> mapTT;
    • map<T1, T2, 指定键值的排序规则> mapTT;
    • map(const map &mp);
  • map 赋值操作
    • map& operator=(const map &mp)
    • swap(mp)
  • map 插入操作
    • insert(...)
  • map 删除操作
    • clear()
    • erase(pos)
    • erase(beg, end)
    • erase(keyelen)
  • map 查找操作
    • find(key)
    • count(keyelen)
    • lower_bound(keyelen)
    • upper_bound(keyelen)
    • equal_range(keyelen)
map<int, string> m;
m.insert(pair<int, string>(1, "aa"));
m.insert(make_pair(2, "bb"));
m.insert(map<int, string>::value_type(3, "cc"));
m[2] = "dd";
// cout << m[5] << endl;//注意防止索引越界,当访问一个map中不存在的键时,即便是访问也会导致map中自动添加上对应的一个元素pair(int, string)(5, 0)

算法

  • 头文件 #include<algorithm>

函数对象 - 仿函数

  • 一个对象和()结合,就会触发()重载运算符的调用
  • 一个类中如果重载了()运算符函数,则这个类的实例对象就称为函数对象,也叫仿函数
  • 一元函数对象
    • 一个参数
  • 二元函数对象
    • 两个参数
  • 多元函数对象
    • 多个参数
class CA {
    int n;//函数对象可以有其他成员
    public:
        void operator()(char * str){//重载()运算符函数
            cout << str << endl;
        }
}
CA()("aa");

谓词

  • 返回值为bool类型的函数或仿函数,称为谓词
  • 一元谓词、二元谓词、多元谓词

c++ 内建函数对象

  • 算术类函数对象
    • template<class T> T plus<T> 加法
    • template<class T> T minus<T> 减法
    • template<class T> T multiplies<T> 乘法
    • template<class T> T divides<T> 除法
    • template<class T> T modulus<T> 取模
    • template<class T> T negate<T> 取反
  • 关系运算类函数对象
    • template<class T> bool equal_to<T> 等于
    • template<class T> bool not_equal_to<T> 不等于
    • template<class T> bool greater<T> 大于
    • template<class T> bool greater_equal<T> 大于等于
    • template<class T> bool less<T> 小于
    • template<class T> bool less_equal<T> 小于等于
  • 逻辑运算类函数对象
    • template<class T> bool logical_and<T> 逻辑与
    • template<class T> bool logical_or<T> 逻辑或
    • template<class T> bool logical_not<T> 逻辑非

适配器

  • 为算法提供接口

函数对象适配器

  • bind2nd
  • bind1st
//第二部:公共继承binary_function类 <参数 参数 返回值>
class Myprint:public binary_function<int, int, void>
{
    public:
        //第三步:const修饰operator()重载函数
        void operator()(int val, int param) const {
            cout << val * tmp << endl;
        }
}
vector<int> vv;
vv.push_back(1);
vv.push_back(2);
vv.push_back(3);
//第一步:使用 bind2nd 或 bind1st 绑定函数参数
for_each(vv.begin(), vv.end(), bind2nd(Myprint(), 100));//把100参数绑定到重载函数的第二个参数上
// for_each(vv.begin(), vv.end(), bind1st(Myprint(), 100));//把100参数绑定到重载函数的第一个参数上

函数指针适配器

  • 普通函数作为适配器
  • ptr_fun
    • c++中函数名代表不了函数地址(函数地址是由函数名和参数共同决定的),可以用ptr_fun获取函数地址
void myprintFn(int val, int param) {
    cout << val * tmp << endl;
}
vector<int> vv;
vv.push_back(1);
vv.push_back(2);
vv.push_back(3);
for_each(vv.begin(), vv.end(), bind2nd( ptr_fun(myprintFn) , 100));

成员函数作为适配器

  • mem_fun_ref
class CA {
    public:
        int num;
        CA(){}
        CA(int num){
            this->num = num;
        }
        void fn(int param){
            cout << num * param << endl;
        }
}
vector<CA> vv;
vv.push_back(CA(10));
vv.push_back(CA(20));
vv.push_back(CA(30));

for_each(vv.begin(), vv.end(), bind2nd( mem_fun_ref(&CA::fn) , 100));

取反适配器

  • not1 一元取反
  • not2 二元取反
  • c++11才支持lamuda表达式语法
vector<int> vv;
vv.push_back(1);
vv.push_back(2);
vv.push_back(3);

find_if(vv.begin(), vv.end(), bind2nd(greater<int>(), 30));//找大于等于30对元素
find_if(vv.begin(), vv.end(), not1( bind2nd(greater<int>(), 30) ));//取反

sort(vv.begin(), vv.end(), greater<int>()));//指定从大到小排序
sort(vv.begin(), vv.end(), not2(greater<int>()));//取反,即从小到大排序,这里需要两个参数作比较所以需要用二元取反not2
//greater:大于等于谓词函数

lamuda 表达式

  • c++11才支持lamuda表达式语法
vector<int> vv;
vv.push_back(1);
vv.push_back(2);
vv.push_back(3);

//[] 里面啥都不写,lamuda不能识别外部数据
//[=] lamuda 能对外部数据 读操作
//[&] lamuda 能对外部数据 写操作 
for_each(vv.begin(), vv.end(), [&](int val){
    cout << val << endl;
});

常见的算法

  • for_each(iterator beg, iterator end, _callback)
    • 遍历
  • transform(iterator beg1, iterator end1, iterator beg2, _callback)
    • 搬运,将容器区间的内容搬到另外的容器
    • 搬运过程中不会给目标容器分配内存,需提前自己分配好内存
vector<int> vv;
vv.push_back(1);
vv.push_back(2);
vv.push_back(3);

vector<int> vv2;
vv2.resize(vv.size());

transform(vv.begin(), vv.end(), [=](int val){
    return val + 10;
})
  • find(iterator beg, iterator end, value)
    • 查找,返回找到元素的迭代器位置
  • find_if(iterator beg, iterator end, _callback)
    • 按条件查找
  • adjacent_find(iterator beg, iterator end, _callback)
    • 茶灶相邻重复元素
  • bool binary_search(iterator beg, iterator end, value)
    • 二分查找,被查找的容器必须是有序的
    • 返回bool值,可以用于检测一个容器中是否有某个值
  • count(iterator beg, iterator end, value)
    • 统计元素出现次数
  • count_if(iterator beg, iterator end, _callback)
    • 按条件统计
  • min(int n1, int n2)
    • min函数用来比较两个参数的大小,并返回较小值

常见的排序算法

  • merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest)
    • 合并算法,将两个容器的内容合并到第三个容器中
    • 合并过程会进行排序,默认哪个元素的值小,哪个元素先移动到第三容器中,即升序
  • sort(iterator beg, iterator end, _callback)
    • 按条件排序
  • random_shuffle(iterator beg, iterator end)
    • 打乱容器顺序
    • 内部是根据随机数进行打乱的,需要提前设置好随机数
  • reverse(iterator beg, iterator end)
    • 反转容器顺序

常见的拷贝替换算法

  • copy(iterator beg, iterator end, iterator dest)
    • 拷贝
//向终端迭代器输出拷贝内容
#include<iterator>

vector<int> vv;
vv.push_back(1);
vv.push_back(2);
vv.push_back(3);
//ostream_iterator 终端输出流迭代器 ostream_iterator<类型>(模式, 分隔符)
copy(vv.begin(), vv.end(), ostream_iterator<int>(cout, " "))
  • replace(iterator beg, iterator end, oldvalue, newvalue)
    • 替换
  • replace_if(iterator beg, iterator end, _callback, newvalue)
    • 按条件替换
  • swap(container c1, container c2)
    • 交换两个容器元素

算术生成算法

  • accumulate(iterator beg, iterator end, value)
    • 求和算法
  • fill(iterator beg, iterator end, value)
    • 填充

常用的集合算法

  • set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest)
    • 求交集,返回实际的内容大小
  • set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest)
    • 求并集,返回实际的内容大小
  • set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest)
    • 求差集,返回实际的内容大小

常见报错信息

  • no match for operator
    • 没有匹配到重载运算符,你可能需要自定义相关的重载运算符

推荐文章