基础算法–C++基础回顾
开始之前先说明一下本篇文章的定位,如果你已经掌握如下内容可以跳过本篇文章
- 尝试梳理一些
C++
语言特性 - 最终希望阅读者能够尝试自己梳理更多的
C++
语言特性、或用同样的思路学习其他语言
C++
的功能非常强大,特性也非常多,但是限于篇幅问题,这里不能面面具到。下面只是简单帮大家回顾一些基本特性,如果想系统的学习C++
这里还是推荐大家选修一门C++
编程的课程或是读一本C++
编程的书
关于内存
-
内存是什么
- 我们可以这样简单的想象内存条
- 它是一条非常非常长的纸带,每个格子可以填写
0~255
的一个状态(8
个0/1
比特,一个字节) - 例如
16G
的内存,一共能填写16 * 1024 * 1024 * 1024
个字节 - 然后这个纸带卷啊卷,卷成一根内存条这么大
- 这就是操作系统所拥有的内存资源,操作系统会将内存分配给正在执行的程序
- 它是一条非常非常长的纸带,每个格子可以填写
- 我们可以认为,程序处理的数据存储在内存中
- 现在常用的内存条,包含若干内存颗粒(半导体集成电路)
- 物理上,通过一些微小的元器件来表示
0
1
状态 - 能存储的比特数取决集成电路里的元器件数目
- 我们可以这样简单的想象内存条
-
内存如何分配给程序
- 内存分配的细节与编译器,操作系统,运行时环境等等有关。这里只给出一些概念上的初略过程
- 计算机上有多个程序同时运行,操作系统也会预留一部分内存,而内存是有限的。因此,程序只能在操作系统分配给它的范围内使用内存
- 操作系统一开始就会给程序分配一些内存,用来存储全局变量,局部变量,函数参数返回值、程序代码等。其中,全局变量、程序代码分配在
static
内存区域(程序从开始到结束,这些内存都会被占用)。局部变量、函数参数返回值等,被分配在栈内存区域(函数调用栈) - 函数被调用一次时,在函数调用栈中分配一个大小合适的栈桢,存储这一次的局部变量,参数和返回值。从函数中返回时,释放栈桢的内存(在操作系统看来,整个函数调用栈还是在程序那里)
- 另外,程序在运行时,可以向操作系统动态地申请和释放一些内存(堆内存)
- 内存分配的细节与编译器,操作系统,运行时环境等等有关。这里只给出一些概念上的初略过程
-
变量/指针/引用
- 变量:一块具有类型的内存(类型:数据的存储表示方式以及你可以对它进行的操作)
- 指针:一块内存的地址,指针的类型可能说明这个指针指向特定类型的变量
- 引用:可以理解为指针的一种“语法糖”(左值引用/右值引用)
- 数组:内存中连续排列的多个同类型变量。数组名称可以用作指向第一个元素指针
- 自定义的类型(
class/struct
):一组成员变量在内存里面的排列方式以及可以对它进行的操作(这里可以了解一下struct
中变量的排布等) - 一个对象:按照特定排列方式存储在内存里的一组成员变量
-
构造函数/析构函数
C++
中两个运算符号new/delete
替代了C
语言的malloc/dealloc
库函数- 通过
new/delete
动态分配或释放一个对象会发生new
分配内存,然后调用对应的构造函数(递归调用各个成员变量的构造函数)delete
调用对应的析构函数,然后释放内存
- 动态内存管理的两种风格
Resource Acquisition Is Initialization (RAII)
资源获取即初始化(C++
语言中可通过恰当实现构造/析构函数、恰当调用new/delete
)- 垃圾回收(
C++
语言中可通过智能指针实现)
关于函数
-
减少函数调用(内联函数/预处理宏)
- 调用函数时,处理参数/返回值/栈桢的产生和销毁会带来一定的开销。因此,对简单的函数,将调用函数改为直接嵌入一段代码,可以节约一些计算开销
C
语言采用宏定义,#define min(a, b) ((a < b) ? a : b)
C++
使用inline
关键字建议编译器进行内联(但并不代表编译器一定会这么做)C++
中建议非必要不使用宏
- 调用函数时,处理参数/返回值/栈桢的产生和销毁会带来一定的开销。因此,对简单的函数,将调用函数改为直接嵌入一段代码,可以节约一些计算开销
-
传值与传引用
- 传值,将变量拷贝一份传递给函数,函数里面和函数外面是两份数据
- 传引用,只是将变量的引用传递给函数,函数里面和函数外面是同一份数据
-
浅/深拷贝
- 当一个类中包含动态分配的资源时
- 浅拷贝:不会分配第二份资源,使得拷贝后的对象和之前的对象指向同一份数据(如:数组
int a[10];int b[10] = a;
)。这其实在很多时候是一个bug
,如果真的需要这样使用,应该采用CoW/Move
来代替 - 深拷贝:对所有成员变量逐个拷贝。合理的拷贝行为应该满足:等价性、独立性
- 浅拷贝:不会分配第二份资源,使得拷贝后的对象和之前的对象指向同一份数据(如:数组
- 当一个类中包含动态分配的资源时
-
拷贝/移动
-
有时并不需要进行时拷贝,因为在完成拷贝之后,旧的元素就失去了使用价值
-
可以通过
move
来避免不必要的拷贝(右值引用表示一个可以被销毁的临时值)template<class T> void swap(T &a, T &b) { const T tmp = a; // put a copy a into tmp a = b; // put a copy of b into a b = tmp; // put a copy of tmp into b }
template<class T> void swap(T &a, T &b) { T tmp = std::move(a); // std::move 会返回一个右值引用 a = std::move(b); b = std::move(tmp); }
-
-
函数指针
- 通过函数指针,可以将函数作为一个参数传递给要调用的函数或者说传入一个谓词
- 实质上是函数的代码所在的地址
- 可以列举两个例子
- 给排序函数出入一个比较函数的函数指针作为参数
bool cmp_func(const int &a, const iny &b) { retutn abs(a) < abs(b); } std::sort(array_a.begin(), array_a.end(), cmp_func);
- 设置回调函数(比如:操作系统中的中断向量表)
-
函数对象
- 函数对象是重载了函数的运算符
()
的对象 - 在
C++
中,应当倾向于使用函数对象或lambda
表达式而非函数指针struct cmp { bool operator()(const int &a, const int &b) { retutn a < b; } }
- 函数对象是重载了函数的运算符
-
Lambda
表达式C++11
标准中引入的匿名函数,用于更加方便定义一个匿名函数对象- 可以在
lambda
中`捕获当前作用域的变量,定义参数列表,也可以有返回值
关于多态
-
虚函数
- 在成员函数前标注
virtual
,子类可以重新实现这个函数,编译器和运行时环境通过虚表保证调用正确版本的函数 - 纯虚函数要求子类必须重新实现这个函数
- 虚表可以认为是子类隐藏的一个成员数组,数组中标注每个虚函数具体指向哪一个实现版本(通常是继承关系上,最近的一个类所实现的版本,例如如果这个类自身有实现,就调用自身实现的版本,如果自身没有实现,就会调用向上查找)(虚表中可能会保存指向一些函数实现的函数指针)
- 在成员函数前标注
-
多态
- 通过继承和虚函数,可以实现这样的行为。某个类的类型的指针,它可以指向某个子类的对象,并正确调用子类对虚函数的具体实现
-
多态的实现
- 通过继承、虚函数可以实现运行时多态
- 我们将一个子类作为参数传递给一个函数,函数如果不用指针和引用而是直接使用一个对象来接收这个参数,就有可能导致意外的切片,因为这里存在一个隐士转换,将子类转换为基类,丢了子类的数据
- 通过模版也可以实现编译时多态
- 通过继承、虚函数可以实现运行时多态
-
模版简介
- 函数:对于两段只有参数值不同的代码,不用重复编写
- 模版:对于两个只有参数类型不同的函数,不用重复编写,编译器自动生成程序中用到的不同类型的函数。模版较多的代码往往编译起来非常慢,最后的编译的机器码也会比较大
-
为什么模版类的函数声明和定义要放在一起
- 从模版代码生成的过程考虑:
- 对于编译器来说,模版本身并不是一个能直接拿来链接的函数,而是需要利用模版来生成一些其他的函数
- 将函数声明和定义拆开编写,其实是在链接阶段再去处理函数名称和函数实现的绑定
- 链接器通常没有办法在链接阶段再去处理模版参数的替换
- 从模版代码生成的过程考虑: