C++基础课程
- 形参带默认值的函数
- inline内联函数和普通函数的区别
- 函数重载
- C++为什么支持函数重载,C语言不支持函数重载?
- C++可以直接调用C代码吗?
- C可以直接调用C++代码吗?
- const怎么理解?
- C 和 C++ 中 const 的区别是什么?
- const修饰的量和普通变量的区别是什么?
- const和一级指针的结合
- 总结const和指针的类型转换公式
- 引用是一种更安全的指针
- 右值引用
- 如何判断引用使用是否正确?
- new 和 malloc 的区别是什么?
- delete 和 free 的区别是什么?
- 使用 new 开辟内存的方式有多少种?
- 类和对象、this指针
- 构造函数和析构函数
- 对象的浅拷贝和深拷贝
- 构造函数的初始化列表
- 类的各种成员方法以及区别
- 指向类成员(成员变量和成员方法)的指针
- C++ 函数模版
- C++ 类模版
- 类模版的应用:实现一个C++ STL向量容器 vector
- 容器空间配置器 allocator
- 学习复数类CComplex
- 模拟实现C++的string类代码
- 迭代器
- 容器的迭代器失效问题
- 深入理解new和delete的原理
- new 和 delete 重载实现的对象池代码
- 继承的本质和原理
- 默认的继承方式是什么?
- 派生类的构造过程
- 重载、隐藏、覆盖
- 虚函数、静态绑定和动态绑定
- 虚析构函数
- 虚函数的调用一定就是动态绑定吗?
- 如何理解多态?
- 理解抽象类
- 理解虚基类和虚继承
- 分析C++多重继承可能存在的菱形继承的问题
- C++语言级别提供的四种类型转换方式
- vector :向量容器
- deque :双端队列容器
- list :链表容器
- vector 和 deque 之间的区别
- vector 和 list 之间的区别
- 详解容器适配器
- 关联容器
- 容器的迭代器
- 函数对象
- 泛型算法
形参带默认值的函数
- 给默认值的时候,从右向左给
- 调用效率的问题
- 定义处和声明处都可以为形参指定默认值
- 不管是在定义处还是声明处给定形参默认值,形参只能出现一次
inline内联函数和普通函数的区别
- inline内联函数在编译过程中,没有函数的调用开销,在函数调用点直接把函数的代码进行展开处理;普通函数在调用时,参数压栈、函数栈帧的开辟和回退过程等都会产生开销
- inline内联函数不会生成相应的函数符号
- inline只是建议编译器把函数处理成内联函数,但是不是所有的inline都会被编译器处理成内联函数,比如递归函数
- 在进行函数调试时,debug版本上,inline是不起作用的,inline只有在release版本下才能出现
函数重载
- 一组函数处于同一个作用域中,其中函数名相同,参数列表的个数或者类型不同,那么这一组函数就称作函数重载。
- 一组函数要称得上重载,一定先是处在同一个作用域当中的。
- 函数参数中包含 const 或者 volatile 的时候,是怎么影响形参类型的。
- 一组函数,函数名相同,参数列表也相同,仅仅的返回值类型不同,不是重载函数。
C++为什么支持函数重载,C语言不支持函数重载?
因为C++代码产生的函数符号是由函数名和参数列表类型组成的,而C代码产生的函数符号由函数名来决定。
C++可以直接调用C代码吗?
不可以,因为C++代码中生成的函数符号和调用的C代码生成的函数符号不相同,导致在C++代码无法解析调用的C代码的函数符号,解决办法就是在C++代码中,把调用的C函数的声明括在 extern "C"{}
里面,表示该函数在C++代码中按照C的方式生成函数符号,最终也就能够在链接阶段重定向函数符号。
C可以直接调用C++代码吗?
不可以,因为C代码中生成的函数符号和调用的C++代码生成的函数符号不相同,导致在C代码无法解析调用的C++代码的函数符号,解决办法就是在C++代码中,把C++源码括在 extern "C"{}
里面,表示该函数在C++代码中按照C的方式生成函数符号,最终在C代码链接阶段可以重定向函数符号。
注意⚠️:只要是C++编译器,都内置了__cplusplus
这个宏名。
const怎么理解?
- const 修饰的变量不能够再作为左值,初始化完成后,值不能被修改。
- 不能把常量的地址泄露给一个普通的指针或者普通的引用变量。
C 和 C++ 中 const 的区别是什么?
-
const的编译方式不同:
-
在 C 代码中,const 就是被当作一个常量来编译生成指令的。
-
在 C++ 代码中,所有出现 const 常量名字的地方,在编译阶段就被常量初始化的值替换了。
-
-
在C++代码中,定义const常量时,必须对其进行初始化;而在C代码中,const修饰的量,可以不用初始化,也不叫常量,叫作常变量。
const修饰的量和普通变量的区别是什么?
- 编译方式不同;
- const 修饰的量不能作为左值
const和一级指针的结合
- C++ 的语言规范:const 修饰的是离它最近的类型
const int *p = &a;
:指针可以指向不同的int类型的内存,但是不能通过指针间接修改指向的内存的值int *const p = &a;
:指针现在是个常量,不能再指向其他内存,但是可以通过指针解引用修改指向的内存的值const int *const p = &a;
:指针指向的内存和指针指向的内存的值都不能修改
注意⚠️:如果const
右边没有指针*
的话,const
是不参与类型的,比如int *const p = &a;
中的p
指针类型为int *
,接着写int *p1 = p;
没有问题,只是p
指针是个常量,不能作为左值。
总结const和指针的类型转换公式
- 从
const int*
转换成int*
,是错误的!!! - 从
int *
转换成const int*
,是可以的; - 从
const int**
转换成int**
,是错误的!!! - 从
int**
转换成const int**
,是错误的!!! - 从
int *const*
转换成int**
,是错误的!!! - 从
int**
转换成int *const*
,是正确的。
引用是一种更安全的指针
- 引用是必须初始化的,指针可以不用初始化。
- 引用只有一级引用,没有多级引用;指针可以有一级指针,也可以有多级指针。
- 定义一个引用变量和定义一个指针变量,其汇编指令是一模一样的。通过引用变量修改所引用内存的值,和通过指针解引用修改指针指向的内存的值,其底层指令也是一模一样的。
右值引用
int &&c = 20;
专门用来引用右值类型,指令上,可以自动产生临时量,然后直接引用临时量,同时可以通过右值引用c
修改临时量内存c = 40
。- 右值引用变量本身是一个左值,只能用左值引用来引用它。
- 不能用一个右值引用变量来引用一个左值。
如何判断引用使用是否正确?
int a = 10;
int *p = &a;
const int *&q = p;
上面第三行的引用使用是错误的,在判断引用是否使用正确时,可以把引用改写成指针进行判断,比如上边的例子可以改写为const int **q = &p;
,也就是说要把int **
类型转换为const int **
,结果肯定是错误的。
new 和 malloc 的区别是什么?
-
malloc 称作C的库函数,而 new 被称作运算符。
-
new 不仅可以开辟内存空间,还可以对内存做初始化操作;而 malloc 只负责开辟内存,不负责初始化。
-
由于 new 在开辟内存时是指定数据类型的,所以返回值不需要进行类型的转换;而 malloc 只是按字节数开辟内存的,返回值永远是 void* 类型,因此需要对返回值进行类型的转换。
-
malloc 开辟内存失败,返回 nullptr 指针;而 new 开辟内存失败,是通过抛出 bad_alloc 类型的异常来判断的。
delete 和 free 的区别是什么?
- free 称作C的库函数,而 delete 称作运算符。
- free 不管是释放单个元素的内存还是释放数组的内存,就是一个标准的函数调用,只需要传入内存的起始地址;而 delete 在释放数组内存时,需要添加中括号。
- delete底层包含free,delete先调用析构函数,然后再调用 operator delete() 释放内存空间,在operator delete() 中调用 free()。
使用 new 开辟内存的方式有多少种?
-
// 抛出异常的new int *p1 = new int(20);
-
// 不抛出异常的new int *p2 = new (nothrow) int;
-
// 在堆上生成常量对象的new const int *p3 = new const int(40);
-
// 定位new int data = 0; int *p4 = new (&data) int(50); // 在指定地址的内存上,划分一块整型大小的内存空间,并将其初始化为50
类和对象、this指针
- 类是实体的抽象数据类型(ADT,Abstract Data Type),实体的属性和行为就是类中定义的成员变量和成员方法。对类进行实例化,即可得到对象。
- OOP语言的四大特征:抽象、封装/隐藏、继承、多态。
- 访问限定符:
public
公有的、private
私有的、protected
保护的。 - 属性一般都是私有的成员变量,要想访问私有的属性,可给外部提供公有的成员方法。
- 类体内实现的方法,会自动处理为内联函数。
- 类可以定义无数的对象,每个对象都有自己的成员变量,但是它们共享一套成员方法。
- 如何知道调用的成员方法是处理哪个对象的数据呢?类的成员方法一经编译,所有的方法参数,都会加一个
this
指针,用来接收调用该方法的对象的地址。 - 对象内存的大小只和成员变量所占空间有关,同时通过占用空间最大的成员变量进行对齐。
构造函数和析构函数
- 构造函数是可以带参数的,因此可以提供多个构造函数,称作构造函数的重载。
- 析构函数是不带参数的,所以析构函数只有一个。
- 对于栈上的类对象,定义处自动调用构造函数,出作用域时自动调用析构函数;对于全局的对象,定义处自动调用构造函数,程序结束时自动调用析构函数;对于堆上的对象,比如定义一个类型为
SeqStack
的类对象,定义处SeqStack *ps = new SeqStack(10);
相当于先malloc
开辟内存空间,接着自动调用构造函数,对象使用完后,需要对堆上的空间进行释放,delete ps;
相当于先调用ps->~SeqStack()
,然后free(ps)
释放内存。
对象的浅拷贝和深拷贝
- 对象默认的拷贝构造函数是做内存的数据拷贝,如果对象占用外部资源,也就是说如果有对象的成员变量是一个指针,指向在堆中开辟的内存空间,调用默认构造函数会使得新的对象的成员变量也指向堆中相同的内存空间,这种情况下,调用析构函数会导致浅拷贝带来的重复释放堆中内存的问题。
- 要想解决浅拷贝所产生的重复释放堆中内存的问题,可以采用深拷贝,也就是自定义拷贝构造函数,在堆中重新开辟一块内存,让新的对象的相关成员变量指向这一块新开辟的内存。
- 默认的赋值函数也是做直接的内存数据拷贝,如果对象占用外部资源,那么就会产生浅拷贝,同时会造成内存泄漏(对象属性指向的堆空间没有被释放,但是此时指针已经指向了另外一块堆空间),此时需要自定义赋值重载函数进行深拷贝,也就是实现赋值运算符重载,在赋值重载函数中,首先防止自赋值,然后需要释放对象占用的外部资源(防止内存泄漏),最后在重新在堆中开辟一块内存,让新的对象的成员变量指向这一块新开辟的内存。
构造函数的初始化列表
- 可以指定当前对象成员变量的初始化方式。
- 自定义了一个构造函数,编译器就不会再产生默认构造了。
a
类中包含成员对象时,如果成员对象的类型b
中自定义了构造函数,也就没有默认构造函数,那么需要通过a
类的构造函数的初始化列表来对成员对象进行初始化,会调用成员对象的自定义构造函数。- 成员变量的初始化和它们定义的顺序有关,和构造函数初始化列表中出现的先后顺序无关。
类的各种成员方法以及区别
static
成员变量在类内声明,类外进行定义并且初始化。所有对象共享static成员变量,static成员变量不属于类的某个对象,而是属于类级别的,保存在数据段中。- 普通成员方法和静态成员方法的区别:普通成员方法是通过对象对其进行进行调用,在调用时会自动传入this指针;而调用静态成员方法时不会产生this指针,也就不需要通过对象对其进行调用,在调用静态成员方法时只需要指明类作用域即可。
- 在静态成员方法中不能访问普通成员变量,只能访问静态成员变量。
- 常量对象只能调用常成员方法。只要是只读操作的成员方法,一律实现成const常成员方法,这样普通对象和常对象均可调用。
总结:
- 普通的成员方法 => 编译器会添加一个
this
形参变量- 属于类的作用域
- 调用该方法时,需要依赖一个对象(常对象是无法调用的,常对象的实参类型为
const CGoods*
,而普通成员方法的形参为CGoods *this
) - 可以任意访问对象的私有成员
static
静态成员方法 => 不会生成this
形参- 属于类的作用域
- 用类名作用域来调用方法
- 可以任意访问对象的私有成员,仅限于不依赖对象的成员(只能调用其他的
static
静态成员)
const
常成员方法 => 形参为const CGoods *this
- 属于类的作用域
- 调用依赖一个对象,普通对象或者常对象都可以
- 可以任意访问对象的私有成员,但是只能读,而不能写
指向类成员(成员变量和成员方法)的指针
- 定义指向普通类成员的指针时,需要在指针前加上类的作用域,比如:
- 指针
p
指向Test
类中的成员变量ma
的定义语句为int Test::*p = &Test::ma;
,然后通过指针间接访问的时候,必须在指针前面指明对象,Test t1; t1.*p = 20;
。 - 指针
pfunc
指向Test
类中的成员方法void func()
的定义语句为void (Test::*pfunc)() = &Test::func;
,调用成员方法时,需要在指针前面指明对象,Test t1; (t1.*pfunc)();
。
- 指针
- 定义指向静态成员的指针,不需要在指针前加上类的作用域,也不用通过对象来调用类的成员变量和成员行为,和指向普通变量和函数的定义以及调用方法相同。
C++ 函数模版
/*
模版的意义:可以对类型进行参数化
函数模版:函数模版的类型不知道,因此函数模版是不进行编译的。
模版的实例化:函数调用点进行实例化。
模版函数:模版函数才是要被编译器所编译的。
模版类型参数:typename/class T,可定义多个模版类型参数,并用逗号隔开。
模版的非类型参数:参数必须是整数类型(整数、地址、引用)的常量,只能使用,而不能修改。
函数模版实参的推演:可以根据用户传入的实参的类型,来推导出模版类型参数的具体类型。
模版的特例化(专用化):特殊的实例化,不是编译器提供的,而是用户提供的。
*/
//函数模版
template<typename T> // 定义一个模版参数列表
bool compare(T a, T b) // compare是一个函数模版
{
cout << "template compare" << endl;
return a > b;
}
/*
在函数调用点,编译器用用户指定的类型,从原模版实例化一份代码出来
bool compare<int>(int a, int b)
{
cout << "template compare" << endl;
return a > b;
}
*/
// 模版特例化:针对compare函数模版,提供const char*类型的特例化版本
template<>
bool compare<const char*>(const char *a, const char *b)
{
cout << "compare<const char*>" << endl;
return strcmp(a, b) > 0;
}
// 非模板函数:普通函数
bool compare(const char*a, const char* b)
{
cout << "nomal compare" << endl;
return strcmp(a, b) > 0;
}
int main()
{
// 函数调用点
compare<int>(10, 20);
compare<double>(10.5, 20.5);
// 函数模版实参的推演
compare(20, 30);
compare<int>(30, 36.5);
// 对于某些类型来说,依赖编译器默认实例化的模版代码,代码处理逻辑是有错误的
// 编译器优先把compare处理成函数名,如果找不到非模版函数,才去调用conpare函数模版
compare("aaa", "bbb");
return 0;
}
注意⚠️:模版代码是不能在一个文件中定义,在另外一个文件中进行使用。模版代码调用之前,一定要看到模版定义的地方,这样模版才能够进行正常的实例化,产生能够被编译器编译的代码。所以,模版代码都是被放在头文件中,然后在源文件代码中直接通过#include
包含相关头文件。
C++ 类模版
- 类名由模版名称与类型参数列表组成,对于构造函数和析构函数的类型参数列表
<T>
可省略。 - 类模版通过实例化得到模版类,模版类中只包含已调用的成员方法。
类模版的应用:实现一个C++ STL向量容器 vector
#include <iostream>
#include <cstdlib>
using namespace std;
template<typename T>
class vector
{
public:
vector(int size = 10)
{
_first = new T[size];
_last = _first;
_end = _first + size;
}
~vector()
{
delete []_first;
_first = _last = _end = nullptr;
}
vector(const vector<T> &rhs)
{
int size = rhs._end - rhs._first;
_first = new T[size];
int len = rhs._last - rhs._first;
for (int i = 0; i < len; i++)
{
_first[i] = rhs._first[i];
}
_last = _first + len;
_end = _first + size;
}
vector<T>& operator=(const vector<T> &rhs)
{
if (this == &rhs)
return *this;
delete[]_first;
int size = rhs._end - rhs._first;
_first = new T[size];
int len = rhs._last - rhs._first;
for (int i = 0; i < len; i++)
{
_first[i] = rhs._first[i];
}
_last = _first + len;
_end = _first + size;
return *this;
}
void push_back(const T &val) // 向容器末尾添加元素
{
if (full())
expand();
*_last++ = val;
}
void pop_back() // 从容器末尾删除元素
{
if (empty())
return;
_last--;
}
T back() const // 返回容器末尾元素
{
return *(_last - 1);
}
bool full()const {return _last == _end;}
bool empty()const {return _first == _last;}
private:
T *_first; // 指向数组起始的位置
T *_last; // 指向数组中有效元素的后继位置
T *_end; // 指向数组空间的后继位置
void expand() // 容器的二倍扩容
{
int size = _end - _first;
T *ptmp = new T[2 * size];
for (int i = 0; i < size; i++)
{
ptmp[i] = _first[i];
}
delete []_first;
_first = ptmp;
_last = _first + size;
_end = _first + 2 * size;
}
};
int main()
{
vector<int> vec;
for (int i = 0; i < 20; i++)
{
vec.push_back(rand() % 100);
}
while (!vec.empty())
{
cout << vec.back() << " ";
vec.pop_back();
}
cout << endl;
return 0;
}
容器空间配置器 allocator
- 容器空间配置器allocator可实现容器底层内存开辟、内存释放、对象构造和对象析构的分开处理。
学习复数类CComplex
C++ 的运算符重载:使对象的运算表现和编译内置类型一样。
#include <iostream>
using namespace std;
class CComplex
{
friend CComplex operator+(const CComplex &lhs, const CComplex& rhs);
friend ostream& operator<<(ostream& cout, const CComplex& src);
friend istream& operator>>(istream& in, CComplex& src);
public:
CComplex(int r = 0, int i = 0)
:mreal(r),mimage(i){}
// 指导编译器怎么做CComplex类对象的加法操作
//CComplex operator+(const CComplex& src)
//{
// CComplex comp;
// comp.mreal = mreal + src.mreal;
// comp.mimage = mimage + src.mimage;
// return comp;
//return CComplex(mreal + src.mreal, mimage + src.mimage);
//}
CComplex operator++(int)
{
// CComplex comp = *this;
// mreal += 1;
// mimage += 1;
// return comp;
return CComplex(mreal++, mimage++);
}
CComplex& operator++()
{
mreal += 1;
mimage += 1;
return *this;
}
void operator +=(const CComplex& src)
{
mreal += src.mreal;
mimage += src.mimage;
}
void show() {cout << "mreal: " << mreal << " mimage: " << mimage << endl;}
private:
int mreal;
int mimage;
};
CComplex operator+(const CComplex &lhs, const CComplex& rhs)
{
return CComplex(lhs.mreal + rhs.mreal, lhs.mimage + rhs.mimage);
}
ostream& operator<<(ostream& cout, const CComplex& src)
{
cout << "mreal: " << src.mreal << " mimage: " << src.mimage << endl;
return cout;
}
istream& operator>>(istream& in, CComplex& src)
{
in >> src.mreal >> src.mimage;
return in;
}
int main()
{
CComplex comp1(10, 10);
CComplex comp2(20, 20);
// comp1.operator+(comp2) 加法运算符的重载函数
CComplex comp3 = comp1 + comp2;
comp3.show();
CComplex comp4 = comp1 + 20; // comp1.operator+(20) 类型int的整数20会自动转换为CComplex类型的对象CComplex(20, 0);
comp4.show();
// 编译器做对象运算的时候,会调用对象的运算符重载函数(优先调用成员方法);如果没有成员方法,就在全局作用域下找合适的运算符重载函数
CComplex comp5 = 30 + comp1; // 可提供全局的运算符重载函数::operator+(30, comp1);
comp5.show();
// CComplex operator++(int) 后置++
comp5 = comp1++; // ++ -- 是单目运算符
comp1.show();
comp5.show();
// CComplex operator++() 前置++
comp5 = ++comp1;
comp1.show();
comp5.show();
comp1 += comp2;
comp1.show();
cout << comp1 << endl;
cin >> comp1 >> comp2;
cout << comp1 << comp2;
return 0;
}
模拟实现C++的string类代码
#include <iostream>
#include <cstring>
using namespace std;
class String
{
friend String operator+(const String &str1, const String &str2);
friend ostream& operator<<(ostream& cout, String &str);
public:
String(const char *p = nullptr)
{
if (p != nullptr)
{
_pstr = new char[strlen(p) + 1];
strcpy(_pstr, p);
}
else
{
_pstr = new char[1];
*_pstr = '\0';
}
}
~String()
{
delete []_pstr;
_pstr = nullptr;
}
String(const String &str)
{
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
}
String& operator=(const String& str)
{
if (this == &str)
return *this;
delete[] _pstr;
_pstr = new char[strlen(str._pstr) + 1];
strcpy(_pstr, str._pstr);
return *this;
}
bool operator>(const String& str)const
{
return strcmp(_pstr, str._pstr) > 0;
}
bool operator<(const String& str)const
{
return strcmp(_pstr, str._pstr) < 0;
}
bool operator==(const String& str)const
{
return strcmp(_pstr, str._pstr) == 0;
}
int length()const
{
return strlen(_pstr);
}
const char& operator[](int i)const
{
return _pstr[i];
}
char& operator[](int i)
{
return _pstr[i];
}
const char* c_str()const
{
return _pstr;
}
private:
char *_pstr;
};
ostream& operator<<(ostream& cout, String &str)
{
cout << str._pstr;
return cout;
}
String operator+(const String &str1, const String &str2)
{
// char* ptmp = new char[strlen(str1._pstr) + strlen(str2._pstr) + 1];
String tmp;
tmp._pstr = new char[strlen(str1._pstr) + strlen(str2._pstr) + 1];
strcpy(tmp._pstr, str1._pstr);
strcat(tmp._pstr, str2._pstr);
// String tmp(ptmp);
// delete []ptmp;
return tmp;
}
int main()
{
String str1;
String str2 = "aaa";
String str3 = "bbb";
String str4 = str2 + str3;
String str5 = str2 + "ccc";
String str6 = "ddd" + str2;
cout << "str6: " << str6 << endl;
if (str5 > str6)
{
cout << str5 << " > " << str6 << endl;
}
else
{
cout << str5 << " < " << str6 << endl;
}
int len = str6.length();
for (int i = 0; i < len; ++i)
{
cout << str6[i] << " ";
}
cout << endl;
// string -> char*
char buf[1024] = {0};
strcpy(buf, str6.c_str());
cout << "buf: " << buf << endl;
return 0;
}
迭代器
- 迭代器的功能是提供一种统一的方式,来透明的遍历容器。
- 泛型算法是全局的函数,可以给所有的容器使用,泛型算法参数接收的都是迭代器,泛型算法使用迭代器这种方式能够统一的遍历所有容器的元素。
- 范围遍历foreach的底层原理就是通过容器的迭代器来实现容器遍历的。
容器的迭代器失效问题
- 哪些情况会导致容器迭代器出现失效?
- 当容器调用
erase
方法后,当前位置到容器末尾元素的所有的迭代器全部失效了。 - 当容器调用
insert
方法后,当前位置到容器末尾元素的所有的迭代器全部失效了。 - 当容器调用
insert
方法并且引起了容器内存扩容,那么原来容器的所有的迭代器全部失效了。 - 不同容器的迭代器是不能进行比较运算的,否则将导致迭代器失效。
- 当容器调用
- 迭代器失效了以后,问题该如何解决?
对插入/删除点的迭代器进行更新操作。
通过迭代器对容器中的元素进行插入/删除操作时,成员方法insert
和erase
会返回一个更新后的迭代器。
深入理解new和delete的原理
- 在C++中,如何设计程序可以用来检查内存泄漏的问题?
可以在全局中自定义new和delete运算符重载函数,在函数中对内存的开辟和释放进行记录,最后就可以检查是否发生内存泄漏。
- new和delete能够混用吗?C++为什么要区分单个元素和数组的内存分配和释放呢?
对于普通的编译器内置类型来说,可以混用new
和delete[]
、new[]
和delete
。因为普通的内置类型,在开辟内存空间和释放内存空间过程中不会调用构造函数和析构函数,因此也就可以混用new和delete。
对于自定义的类类型来说,由于有析构函数,为了调用正确的析构函数,那么在开辟对象数组的时候,会多开辟4个字节的空间用于记录对象的个数,因此不能混用new和delete。
因为在对数组进行释放时,需要知道数组中对象的个数,因此会多开辟4个字节的空间用于记录对象的个数,而对单个元素进行释放时无需记录个数,所以需要区分单个元素和数组的内存分配和释放。
new 和 delete 重载实现的对象池代码
#include <iostream>
using namespace std;
template<typename T>
class Queue
{
public:
Queue()
{
_front = _rear = new QueueItem;
}
~Queue()
{
QueueItem *cur = _front;
while (cur != nullptr)
{
_front = _front->_next;
delete cur;
cur = _front;
}
}
void push(const T &val) // 入队操作
{
QueueItem* item = new QueueItem(val);
_rear->_next = item;
_rear = item;
}
void pop()
{
if (empty())
{
return;
}
QueueItem *first = _front->_next;
_front->_next = first->_next;
delete first;
if (_front->_next == nullptr)
_rear = _front;
}
T front() const
{
return _front->_next->_data;
}
bool empty()const {return _front == _rear;}
private:
// 产生一个QueueItem的对象池(10000个QueueItem节点)
struct QueueItem
{
QueueItem(T data = T()): _data(data), _next(nullptr) {}
// 给QueueItem提供自定义内存管理
void* operator new(size_t size)
{
if (_itemPool == nullptr)
{
_itemPool = (QueueItem*) new char[POOL_ITEM_SIZE*sizeof(QueueItem)];
QueueItem *p = _itemPool;
for (; p < _itemPool + POOL_ITEM_SIZE - 1; ++p)
{
p->_next = p + 1;
}
p->_next = nullptr;
}
QueueItem *p = _itemPool;
_itemPool = _itemPool->_next;
return p;
}
void operator delete(void *ptr)
{
QueueItem *p = (QueueItem*) ptr;
p->_next = _itemPool;
_itemPool = p;
}
T _data;
QueueItem *_next;
static QueueItem *_itemPool;
static const int POOL_ITEM_SIZE = 100000;
};
QueueItem *_front; // 指向头节点
QueueItem *_rear; // 指向队尾
};
template<typename T>
typename Queue<T>::QueueItem *Queue<T>::QueueItem::_itemPool = nullptr;
int main()
{
Queue<int> que;
for (int i = 0; i < 1000000; ++i)
{
que.push(i);
que.pop();
}
cout << que.empty() << endl;
return 0;
}
继承的本质和原理
继承的本质:(继承的好处是什么?)
- 代码的复用
- 在基类中给所有派生类提供统一的虚函数接口,让派生类进行重写,然后就可以使用多态了。
类与类之间的关系:
- 组合:一个类是另一个类的一部分(a part of …)
- 继承:一个类是另一个类的其中一种(a kind of …)
继承方式 | 基类的访问权限 | 派生类的访问权限 | (main)外部的访问权限 |
---|---|---|---|
public | public | public | Y |
protected | protected | N | |
private | 不可见的 | N | |
protected | public | protected | N |
protected | protected | N | |
private | 不可见的 | N | |
private | public | private | N |
protected | private | N | |
private | 不可见的 | N |
注意⚠️:在派生类中对基类成员的访问权限是不可能超过继承方式的。
总结:
- 外部只能访问对象public的成员,protected和private的成员无法直接访问。
- 在继承结构中,派生类可以从基类继承private的成员,但是派生类却无法直接访问。
- protected 和 private 的区别?如果在基类中定义的成员想被派生类访问,但是不想被外部访问,那么在基类中把相关成员定义成 protected保护的;如果派生类和外部都不打算访问,那么在基类中,就把相关成员定义成 private 私有的。
默认的继承方式是什么?
-
如果是使用
class
定义的派生类,默认的继承方式就是private
私有的。 -
如果是使用
struct
定义的派生类,默认的继承方式就是public
公有的。
派生类的构造过程
派生类可以从基类继承来所有的成员(变量和方法),除了基类的构造函数和析构函数。
派生类如何初始化从基类继承来的成员变量呢?通过调用基类相应的构造函数来对从基类继承而来的成员变量进行初始化操作。
派生类的构造函数和析构函数,只负责初始化和清理派生类部分的成员;派生类从基类继承而来的成员的初始化和清理是由基类的构造函数和析构函数来负责。
总结:
派生类对象构造和析构的过程:
- 派生类调用基类的构造函数,初始化从基类继承来的成员。
- 派生类调用自己的构造函数,初始化派生类自己特有的成员。
…派生类对象出作用域后
- 调用派生类的析构函数,释放派生类成员可能占用的外部资源(堆内存、文件等)。
- 调用基类的析构函数,释放派生类内存中从基类继承来的成员可能占用的外部资源(堆内存、文件等)。
重载、隐藏、覆盖
-
重载关系
一组函数要重载,必须处在同一个作用域中,而且函数名称相同,参数列表不同。
-
隐藏关系(作用域的隐藏)
在继承结构中,派生类的同名成员把基类的同名成员给隐藏调用了。
-
覆盖关系(重写函数)
基类和派生类的方法,返回值、函数名以及参数列表都相同,而且基类的方法是虚函数,那么派生类的方法就自然处理成虚函数,它们之间成为覆盖关系。
覆盖其实就是虚函数表中虚函数地址的覆盖。
-
继承结构,也通常说成从上(基类)到下(派生类)的结构。
-
从派生类对象向基类对象的转换,也就是类型从下到上的转换,是可行的。
-
从基类对象向派生类对象的转换,也就是类型从上到下的转换,是不可行的。
-
基类指针可以指向派生类对象,也就是派生类对象向基类对象的转换,类型从下到上的转换,由于指针的类型只是基类类型,因此指针只能访问派生类从基类继承而来的成员。
例如:
Base *pb = &d
,这里的pb
是基类指针,d
是派生类对象。 -
派生类指针无法指向基类对象,也就是基类对象向派生类对象的转换,类型从上到下的转换,由于派生类指针访问基类中不存在的成员时会出现非法访问内存,因此不能让派生类指针指向基类对象。
例如:
Derive *pd = &b
,这里的pd
是派生类指针,b
是基类对象,这个等式是不成立的。
总结:在继承结构中进行上下的类型转换,默认只支持从下到上的类型的转换。
虚函数、静态绑定和动态绑定
一个类添加了虚函数,对这个类有什么影响?
-
一个类里面定义了虚函数,那么在编译阶段,编译器给这个类类型产生一个唯一的
vftable
虚函数表,虚函数表中主要存储的内容就是 RTTI 指针和虚函数的地址。当程序运行时,每一张虚函数表都会加载到内存的.rodata
区。注意⚠️:RTTI(run-time type information):运行时的类型信息,也就是说RTTI实际上就是指向一个表示类型的字符串。
-
一个类里面定义了虚函数,那么这个类定义的对象,其运行时,在内存中的开始部分,多存储了一个虚函数指针
vfptr
,指向相应类型的虚函数表vftable
。一个类型定义的多个对象,它们的虚函数指针vfptr
指向的是同一张虚函数表。 -
一个类里面虚函数的个数,不影响对象的内存大小(即使类里面有多个虚函数,在内存中对于对象都只存储一个虚函数指针
vfptr
),影响的是虚函数表的大小。 -
如果派生类中的方法和从基类继承而来的某个方法,返回值、函数名、参数列表都相同,而且基类的方法是
virtual
虚函数,那么派生类中的这个函数自动处理成虚函数。
静态绑定和动态绑定
- 当调用的函数是一个虚函数时,在运行阶段确定函数的地址,就是对函数进行动态绑定。
- 当调用的函数是一个普通函数时,在编译阶段确定函数的地址,就是对函数进行静态绑定。
Base *pb = &d
,当一个基类指针指向派生类对象时,如何确定*pb
的类型呢?- 检查基类
Base
中有没有虚函数- 如果
Base
没有虚函数,那么*pb
识别的就是编译时期的类型,也就是Base
类型。 - 如果
Base
有虚函数,那么*pb
识别的就是运行时期的类型,也就是RTTI
类型。
- 如果
- 检查基类
虚析构函数
虚函数依赖:
- 虚函数能产生地址,存储在
vftable
中。 - 对象必须存在,通过内存存储的对象的
vfptr
可查找到虚函数表vftble
,然后通过虚函数表vftable
可查找到虚函数的地址。
哪些函数不能实现成虚函数?
- 构造函数。由于构造函数完成后,才会产生对象,因此不能实现成虚函数。构造函数中调用任何函数,都是静态绑定的,即使调用虚函数,也不会发生动态绑定。
static
静态成员方法。静态成员方法不依赖于对象,就不需要对象一定存在,因此不能实现成虚函数。
虚析构函数
析构函数调用的时候,对象是存在的,因此析构函数可以实现成虚析构函数。
什么时候必须把基类的析构函数实现成虚函数
当基类的指针(引用)指向堆上new
出来的派生类对象,delete pb
(pb
为基类的指针),它调用析构函数的时候必须发生动态绑定,否则会导致派生类的析构函数无法调用。
虚函数的调用一定就是动态绑定吗?
不一定。1. 构造函数中调用任何函数,都是静态绑定的,即使调用虚函数,也不会发生动态绑定。2. 用对象本身调用虚函数,是静态绑定的。
如果不是通过指针或引用变量调用虚函数,那就是静态绑定。
如何理解多态?
静态(编译时期)的多态:函数重载、模版(函数模版和类模版)
动态(运行时期)的多态: 在继承结构中,基类指针(引用)指向派生类对象,通过该指针(引用)调用同名覆盖方法(虚函数),基类指针指向哪个派生类对象,就会调用哪个派生类的覆盖方法,称为 多态 。
多态底层是通过动态绑定来实现的。
#include <iostream>
#include <string>
using namespace std;
// 动物的基类
class Animal
{
public:
Animal(string name) :_name(name) {}
virtual void bark() {}
protected:
string _name;
};
class Cat : public Animal
{
public:
Cat(string name) :Animal(name) {}
void bark() { cout << _name << " bark: miao miao!" << endl; }
};
class Dog : public Animal
{
public:
Dog(string name) :Animal(name) {}
void bark() { cout << _name << " bark: wang wang!" << endl; }
};
class Pig : public Animal
{
public:
Pig(string name) :Animal(name) {}
void bark() { cout << _name << " bark: heng heng!" << endl;}
};
// bark() 满足软件设计要求的“开-闭原则”:对扩展开放,对修改关闭
void bark(Animal *p)
{
p->bark(); // Animal::bark虚函数,动态绑定
}
int main()
{
Cat cat("猫咪");
Dog dog("二哈");
Pig pig("佩奇");
bark(&cat);
bark(&dog);
bark(&pig);
return 0;
}
理解抽象类
- 拥有纯虚函数的类,叫作抽象类。
- 抽象类不能实例化对象,但是可以定义指针和引用变量。
- 定义基类的初衷并不是为了让基类抽象某个实体的类型,定义基类主要是为了让派生类可以通过继承基类直接复用基类中的成员变量,同时给所有派生类保留统一的覆盖/重写接口。因此,一般把基类设计成抽象类。
- 派生类必须重写抽象类中的纯虚函数,否则也属于抽象类。
理解虚基类和虚继承
virtual
:1. virtual
修饰成员方法是虚函数;2. virtual
修饰继承方式是虚继承。被虚继承的类称作 虚基类 。而抽象类是指有纯虚函数的类。
基类指针指向派生类对象时,永远指向的是派生类中基类部分数据的起始地址。
对于基类A
中含有成员变量ma
,派生类B
虚继承基类A
,那么就会内存在存储派生类B
的对象时就会多存储一个虚基类指针vbptr
,虚基类指针vbptr
指向虚基类表vbtable
,虚基类表中存储有基类A
成员地址的偏移量。-----
分析C++多重继承可能存在的菱形继承的问题
C++多重继承的好处:可以做更多代码的复用。
C++多重继承的缺点:可能会出现菱形继承的问题,派生类有多份间接基类的数据,这是属于设计的问题。
如果在菱形继承中,B、C类采用直接继承A类,则A的对象在内存分布如下:
如果在菱形继承中,B、C类采用虚继承A类,则A的对象在内存分布如下:
注意⚠️:在B、C类虚继承A类时,需要在D类的构造函数中间接指定A类合适的构造函数,对A中的成员变量ma进行初始化操作。
如果在D中没有指定A的构造函数,就需要保证A会自动调用默认构造函数才行。
#include <iostream>
using namespace std;
class A
{
public:
A(int data) :ma(data) { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
protected:
int ma;
};
class B : virtual public A
{
public:
B(int data) :A(data), mb(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
protected:
int mb;
};
class C : virtual public A
{
public:
C(int data) :A(data), mc(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
protected:
int mc;
};
class D : public B, public C
{
public:
// 注意:由于B和C都是虚继承了A,在D中需要间接指定A的构造函数,对A中的成员变量ma进行初始化操作。
// 如果在D中没有指定A的构造函数,就需要保证A会自动调用默认构造函数才行。
D(int data) :A(data), B(data), C(data), md(data) { cout << "D()" << endl; }
~D() { cout << "~D()" << endl; }
protected:
int md;
};
int main()
{
D d(10);
return 0;
}
C++语言级别提供的四种类型转换方式
const_cast
:去掉(指针或引用)常量属性的一种类型转换。static_cast
:提供编译器认为安全的类型转换。(没有任何联系的类型之间的转换就被否定了)reinterpret_cast
:类似于C风格的强制类型转换。dynamic_cast
:主要用在继承结构中,可以支持RTTI类型识别的上下转换。
注意⚠️:
- 使用
const_cast
去掉常量属性时,只能对指针或者引用的常量属性进行类型转换。例如:
const int a = 10;
int *b = const_cast<int*>(&a); // int b = const_cast<int>(a);是错误的。
- 基类类型与派生类类型可以使用
static_cast
进行类型转换。 - 对于
dynamic_cast
使用举例说明
#include <iostream>
using namespace std;
class Base
{
public:
virtual void func() = 0;
};
class Derive1 : public Base
{
public:
void func() { cout << "call Derive1::func" << endl; }
};
class Derive2 : public Base
{
public:
void func() { cout << "call Derive2::func" << endl; }
// Derive2实现新功能的API接口函数
void derive02func()
{
cout << "call Derive2::derive02func" << endl;
}
};
void showFunc(Base *p)
{
// dynamic_cast会检查p指针是否指向的是一个Derive2类型的对象
// p->vfptr->vftable RTTI信息 如果p指向的是一个Derive2类型的对象,dynamic_cast转换类型成功,返回Derive2对象的地址给pd2,否则返回nullptr。
// static_cast编译时期的类型转换,denamic_cast运行时期的类型转换,支持RTTI信息识别
Derive2 *pd2 = dynamic_cast<Derive2*>(p);
if (pd2 != nullptr)
{
pd2->derive02func();
}
else
{
p->func();
}
}
int main()
{
Derive1 d1;
Derive2 d2;
showFunc(&d1);
showFunc(&d2);
return 0;
}
运行结果为
call Derive1::func
call Derive2::derive02func
vector :向量容器
底层的数据结构:动态开辟的数组,每次以原来空间大小的2倍进行扩容。
vector<int> vec;
-
增加
vec.push_back(20);
在容器末尾添加元素,时间复杂度为O(1)
,可能导致容器扩容。vec.insert(it, 20);
在it
迭代器指向的位置添加一个元素20,时间复杂度为O(n)
,可能导致容器扩容。 -
删除
vec.pop_back();
删除容器的末尾元素,时间复杂度为O(1)
。vec.erase(it);
删除it
迭代器指向的元素,时间复杂度为O(n)
。vec.clear();
删除容器中的所有元素。 -
查询
operator[]
下标的随机访问vec[5]
,时间复杂度为O(1)
。vec.at[i]
获取容器中索引i
所指的数据。vec.front();
获取容器中第一个元素的值。vec.back();
获取容器中最后一个元素的值。iterator
迭代器进行遍历find
、for_each
foreach
通过iterator
来实现的注意⚠️:对容器进行连续插入或者删除操作(
insert
、erase
),一定要更新迭代器,否则第一次insert
或者erase
完成,迭代器就失效了。 -
常用方法介绍
size()
:获取容器中元素的个数。empty()
:判断容器是否为空。resize(int num);
:重新指定容器的长度为num
,若容器变长,则在新位置上填充默认值0
;若容器变短,则末尾超出容器长度的元素被删除。resize(int num, elem);
:重新指定容器的长度为num
,若容器变长,则在新位置上填充elem
值;若容器变短,则末尾超出容器长度的元素被删除。capacity();
:获取容器的容量。swap(vec1);
:将容器本身的元素与vec1
容器的元素互换。swap可以将本身和一个匿名对象的元素互换,以达到实用的收缩内存的效果。reserve(int len);
:容器预留len
个元素的长度,只给容器底层开辟指定大小的内存空间,预留位置不初始化,元素不可访问。可以减少vector
在动态扩展容器时的扩展次数。
deque :双端队列容器
底层数据结构:动态开辟的二维数组,第一维数组从2
开始,以2
倍的方式进行扩容,第二维是固定长度4096/sizeof(T)
的数组空间。每次扩容后,原来第二维的数组,从新的第一维数组的下标为oldsize/2
的位置开始存放,上下都预留相同的空间,方便deque
在首尾添加元素。
容器需要扩容时,第一维以2倍的方式进行扩容
deque<int> deq;
-
增加
deq.push_back(20);
在容器末尾添加元素,时间复杂度为O(1)
,可能导致容器扩容。deq.push_front(20);
在容器首部添加元素,时间复杂度为O(1)
,可能导致容器扩容。deq.insert(it, 20);
在it
迭代器指向的位置添加一个元素20,时间复杂度为O(n)
,可能导致容器扩容。insert(it, n, elem);
、insert(it, beg, end);
-
删除
deq.pop_back();
删除容器的末尾元素,时间复杂度为O(1)
。deq.pop_front();
删除容器的首部元素,时间复杂度为O(1)
。deq.erase(it);
删除it
迭代器指向的元素,时间复杂度为O(n)
。deq.erase(beg, end);
删除迭代器指定的区间中的元素。vec.clear();
删除容器中的所有元素。 -
查询
operator[]
下标的随机访问deq[5]
,时间复杂度为O(1)
。deq.at[i]
获取容器中索引i
所指的数据。deq.front();
获取容器中第一个元素的值。deq.back();
获取容器中最后一个元素的值。iterator
迭代器进行遍历find
、for_each
foreach
通过iterator
来实现的注意⚠️:对容器进行连续插入或者删除操作(
insert
、erase
),一定要更新迭代器,否则第一次insert
或者erase
完成,迭代器就失效了。 -
常用方法参考
vector
。
list :链表容器
底层数据结构:双向的循环链表。
链表由一系列的结点组成,结点由数据域和指针域组成。
链表的存储方式并不是连续的内存空间,因此链表list
中的迭代器只支持前移和后移,属于双向迭代器。
-
增加
push_back(elem);
在容器末尾添加元素,时间复杂度为O(1)
。push_front(elem);
在容器首部添加元素,时间复杂度为O(1)
。insert(it, elem)
在it
迭代器指向的位置添加一个元素elem
,时间复杂度为O(1)
,在插入元素时,先要进行一个查询操作,对于链表来说,查询操作效率比较低。insert(it, n, elem);
、insert(it, beg, end);
-
删除
pop_back();
删除容器的末尾元素,时间复杂度为O(1)
。pop_front();
删除容器的首部元素,时间复杂度为O(1)
。erase(it);
删除it
迭代器指向的元素,时间复杂度为O(1)
。erase(beg, end);
删除迭代器指定的区间中的元素。clear();
删除容器中的所有元素。remove(elem);
删除容器中值为elem
的元素。 -
链表反转和排序
由于链表不支持随机访问迭代器,因此提供对应的函数
reverse()
反转链表、sort()
链表排序。
vector 和 deque 之间的区别
- 底层数据结构不同:
vector
底层的数据结构是动态开辟的数组,而deque
底层的数据结构是动态开辟的二维数组。 - 插入删除元素的时间复杂度不同:
vector
在容器首部添加删除元素的时间复杂度为O(n)
,而deque
在容器首部添加删除的时间复杂度为O(1)
。 - 对于内存的使用效率不同:
vector
要求内存空间一定是连续的,而deque
可以分块进行数据存储,不要求内存空间一定连续。 - 由于
deque
的第二维内存空间不是连续的,所以在deque
中间插入删除元素时,造成元素的移动比vector
要慢。
vector 和 list 之间的区别
- 底层数据结构不同:
vector
底层的数据结构是动态开辟的数组,而list
底层的数据结构是双向的循环链表。 - 插入删除元素的时间复杂度不同:在
vector
中间插入删除元素的时间复杂度为O(n)
,而在list
插入删除元素的时间复杂度为O(1)
,只不过list
在搜索插入删除元素的时间复杂度为O(n)
。 - 访问元素的时间复杂度不同:
vector
支持随机访问元素,时间复杂度为O(1)
,而list
访问元素的时间复杂度为O(n)
。
详解容器适配器
- 容器适配器没有自己的数据结构,它是另外一个容器的封装,它的方法全部由底层依赖的容器进行实现的。
- 没有实现自己的迭代器。
- 栈
stack
的方法:push(elem)
元素入栈、pop()
元素出栈、top()
查看栈顶元素、empty()
判断栈是否为空、size()
返回栈中的元素个数。 - 队列
queue
的方法:push(elem)
元素入队、pop()
元素出队、front()
查看队头元素、back()
查看队尾元素、empty()
判断队列是否为空、size()
返回队列中的元素个数。 - 优先队列
priority_queue
的方法:push(elem)
元素入队、pop()
元素出队、top()
查看队顶元素、empty()
判断优先队列是否为空、size()
返回优先队列中的元素个数。
为什么stack和queue是基于deque实现的,而不依赖vector实现呢?
- vector的初始内存使用效率比deque低:vector以2倍大小动态扩容,而deque初始开辟的内存的第二维是固定长度
4096/sizeof(T)
的数组空间。 - 对于queue来说,需要支持尾部插入、头部删除,如果queue依赖vector,其出队效率很低。vector容器在头部删除元素的时间复杂度为
O(n)
,而deque在头部删除元素的时间复杂度为O(1)
。 - vector要求内存空间一定是连续的,而deque只需要分段的内存,当存储大量数据时,显然deque对于内存的利用率更好一些。
为什么priority_queue是基于vector实现的,而不依赖duque实现呢?
因为priority_queue底层默认把数据组成一个大根堆结构,一个节点和它的孩子节点是通过下标进行关联的,所以可以在一个内存连续的数组上构建一个大根堆或者小根堆的优先队列。由于duque在内存中不连续,因此priority_queue基于vector实现更好一些。
关联容器
无序关联容器采用链式哈希表的方式存储元素,增删查的时间复杂度为O(1)
。
unordered_set
:无序集合unordered_multiset
:无序多重集合unordered_map
:无序映射表unordered_multimap
:无序多重映射表
有序关联容器采用红黑树存储元素,增删查的时间复杂度为O(logn)
。
set
:集合 keymultiset
:多重集合map
:映射表 [key, value]multimap
:多重映射表
关联容器常用的增删查方法
- 增加:
insert(val)
- 删除:
erase(key)
、erase(it)
- 遍历:
iterator
搜索、调用find
成员方法
容器的迭代器
const_iterator
:常量的正向迭代器,不能通过迭代器来修改变量的值。
iterator
:普通的正向迭代器,const_iterator
是 iterator
的基类。
const_reverse_iterator
:常量的反向迭代器。
reverse_iterator
:普通的反向迭代器。
rbegin()
:返回的是最后一个元素的迭代器。
rend()
:返回的是首元素前驱位置的迭代器。
函数对象
函数对象 => C语言的函数指针。
把有operator()
小括号运算符重载函数的对象,称作 函数对象 或者 仿函数 。
通过函数指针调用函数是没有办法内联的,效率很低,因为有函数调用开销。
使用函数对象的好处
- 通过函数对象调用
operator()
,在编译阶段就能确定所调用的是哪个函数对象的operator()
,可以省略函数的调用开销,比通过函数指针调用函数(不能够inline
内联调用)效率高。 - 因为函数对象是用类生成的,所以可以添加相关的成员变量,用来记录函数对象使用时更多的信息。
#include <iostream>
#include <vector>
using namespace std;
/*
template<typename T>
bool mygreater(T a, T b)
{
return a > b;
}
template<typename T>
bool myless(T a, T b)
{
return a < b;
}
*/
template<typename T>
class mygreater
{
public:
bool operator() (T a, T b) // 二元函数对象
{
return a > b;
}
};
template<typename T>
class myless
{
public:
bool operator() (T a, T b)
{
return a < b;
}
};
// compare是C++的库函数模版
template<typename T, typename Compare>
bool compare(T a, T b, Compare comp)
{
return comp(a, b); // operator()(a, b);
}
int main()
{
cout << compare(10, 20, mygreater<int>()) << endl;
cout << compare(10, 20, myless<int>()) << endl;
return 0;
}
集合set
中的元素从大到小的顺序排列
#include <iostream>
#include <set>
using namespace std;
int main()
{
set<int, greater<int>> set1;
for (int i = 0; i < 10; ++i)
{
set1.insert(rand() % 100);
}
for (auto &v : set1)
{
cout << v << " ";
}
cout << endl;
return 0;
}
优先队列priority_queue
大根堆、小根堆实现
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int main()
{
priority_queue<int> que1; // 默认大根堆实现
for (int i = 0; i < 10; ++i)
{
que1.push(rand() % 100);
}
while (!que1.empty())
{
cout << que1.top() << " ";
que1.pop();
}
cout << endl;
priority_queue<int, vector<int>, greater<int>> que2; // 小根堆实现
for (int i = 0; i < 10; ++i)
{
que2.push(rand() % 100);
}
while (!que2.empty())
{
cout << que2.top() << " ";
que2.pop();
}
cout << endl;
return 0;
}
泛型算法
泛型算法 = template + 迭代器 + 函数对象
-
特点一:泛型算法接收的参数都有迭代器
-
特点二:泛型算法的参数还可以接收函数对象(C函数指针)
二元函数对象通过 绑定器 可以变为一元函数对象。
-
bind1st
:把二元函数对象的operator()(a, b)
的第一个形参绑定起来。 -
bind2nd
:把二元函数对象的operator()(a, b)
的第二个形参绑定起来。
#include <iostream>
#include <vector>
#include <algorithm> // 包含了C++ STL里面的泛型算法
#include <functional> // 包含了函数对象和绑定器
using namespace std;
int main()
{
int arr[] = { 12, 4, 3, 9, 23, 76, 85, 8, 19, 66 };
vector<int> vec(arr, arr + sizeof(arr)/sizeof(arr[0]));
for (int v: vec)
{
cout << v << " ";
}
cout << endl;
// 对容器中的元素从小到大进行排序
sort(vec.begin(), vec.end());
for (int v: vec)
{
cout << v << " ";
}
cout << endl;
// 有序容器中进行二分查找
if (binary_search(vec.begin(), vec.end(), 19))
{
cout << "binary_search 19 存在" << endl;
}
auto it1 = find(vec.begin(), vec.end(), 66);
if (it1 != vec.end())
{
cout << "find 66 存在" << endl;
}
// 传入函数对象greater,改变容器元素排序时的比较方式
sort(vec.begin(), vec.end(), greater<int>());
for (int v: vec)
{
cout << v << " ";
}
cout << endl;
// 85 76 66 23 19 12 9 8 4 3
// 把48按序插入到vector容器当中,找到第一个小于48的数字的位置
// find_if 需要的是一个一元函数对象
// greater a > b less a < b
auto it2 = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 48)); // 也可以使用 bind2nd(less<int>(), 48);
vec.insert(it2, 48); // [](int val)->bool {return val < 48;}
for (int v: vec)
{
cout << v << " ";
}
cout << endl;
// for_each可以遍历容器的所有元素,可以自行添加合适的函数对象对容器的元素进行过滤
for_each(vec.begin(), vec.end(),
[](int val)->void
{
if (val % 2 == 0)
{
cout << val << " ";
}
});
cout << endl;
return 0;
}