C++ 三大特性
封装的意义:
- 将属性和行为作为一个整体,表现生活中的事物
- 将属性和行为加以权限控制
- public protect private
继承的意义:
有些类与类之间存在特殊的关系,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。这个时候我们就可以考虑利用继承的技术,减少重复代码。
多态的意义:
多态分为两类
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
1 指针
1.1 *array++操作
char a[10] = {“hello”};
a++;
这里就会报错,因为a是char[]类型的,它表示的是这个数组的首地址和第一个元素的地址,不能直接去操作a++去移动地址,如果想移动可以char *p = a;然后再操作p,p++这样是完全正确的。
【数组名是常量指针,指针是变量指针】
但当出现sizeof,和&操作符时,数组名不再当成指向一个元素的常量指针来使用,而指针仍当成指向一个元素的变量指针来使用。
1.2 const修饰指针
const修饰指针有三种情况
- const修饰指针 — 常量指针
- const修饰常量 — 指针常量
- const即修饰指针,又修饰常量
示例:
int main() {
int a = 10;
int b = 10;
//const修饰的是指针,指针指向可以改,指针指向的值不可以更改
const int * p1 = &a;
p1 = &b; //正确
//*p1 = 100; 报错
//const修饰的是常量,指针指向不可以改,指针指向的值可以更改
int * const p2 = &a;
//p2 = &b; //错误
*p2 = 100; //正确
//const既修饰指针又修饰常量
const int * const p3 = &a;
//p3 = &b; //错误
//*p3 = 100; //错误
system("pause");
return 0;
}
技巧:看const右侧紧跟着的是指针还是常量, 是指针就是常量指针,是常量就是指针常量
1.3 类数据成员地址 与 类对象数据成员地址
- 类定义是一种类型声明,存在于代码块中,并没有分配内存空间;对类的数据成员取地址,得到的是类的数据成员在类内的相对偏移量;
- 类的对象是类的实例化,分配内存空间给实例化对象使用,类的对象的数据成员取地址,得到的是类的对象的数据成员在内存空间的实际地址;
2 引用
引用的本质在c++内部实现是一个指针常量.
2.1 引用做函数返回值
作用:引用是可以作为函数的返回值存在的
注意:不要返回局部变量引用
用法:函数调用作为左值
示例:
//返回局部变量引用
int& test01() {
int a = 10; //局部变量
return a;
}
//返回静态变量引用
int& test02() {
static int a = 20;
return a;
}
int main() {
//不能返回局部变量的引用
int& ref = test01();
cout << "ref = " << ref << endl;
cout << "ref = " << ref << endl;
//如果函数做左值,那么必须返回引用
int& ref2 = test02();
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
test02() = 1000;
cout << "ref2 = " << ref2 << endl;
cout << "ref2 = " << ref2 << endl;
return 0;
}
2.2 函数重载注意事项
void func(int &a)
{
cout << "func (int &a) 调用 " << endl;
}
void func(const int &a)
{
cout << "func (const int &a) 调用 " << endl;
}
int main() {
int a = 10;
func(a); //调用无const
func(10);//调用有const
}
3 类和对象
3.1 构造函数调用规则:
-
如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
-
如果用户定义拷贝构造函数,c++不会再提供其他构造函数
3.2 深拷贝与浅拷贝
深浅拷贝是面试经典问题,也是常见的一个坑
-
浅拷贝:简单的赋值拷贝操作 (栈区)
-
深拷贝:在堆区重新申请空间,进行拷贝操作
3.3 类外调用类的私有成员函数
3.3.1 通过类的public成员函数调用private成员函数:
#include<iostream>
using namespace std;
class Test
{
public:
void fun2()
{
fun1();
}
private:
void fun1()
{
cout<<"fun1"<<endl;
}
};
int main()
{
Test t;
t.fun2();
return 0;
}
3.3.2 通过类的友元函数调用该类的private成员函数
但是该成员函数必须设为static,这样就可以在友元函数内通过类名调用,否则无法调用
#include<iostream>
using namespace std;
class Test
{
friend void fun2(); //fun2()设为类Test的友元函数
private:
static void fun1()
{
cout<<"fun1"<<endl;
}
};
void fun2()
{
Test::fun1();
}
int main()
{
fun2();
return 0;
}
3.4 this指针
- this指针指向被调用的成员函数所属的对象
- 在类的非静态成员函数中返回对象本身,可使用
return *this
Person& PersonAddPerson(Person p)
{
this->age += p.age;
//返回对象本身
return *this;
}
上述代码返回的是一个引用(打印 p2.age = 40
)。
当返回的是值时(去掉Person后面的&),每次会创建一个新的临时对象,有可能造成链式操作错误(打印 p2.age = 20
)。
Person p1(10);
cout << "p1.age = " << p1.age << endl;
Person p2(10);
p2.PersonAddPerson(p1).PersonAddPerson(p1).PersonAddPerson(p1); //链式操作
cout << "p2.age = " << p2.age << endl;
this 指针是指针常量,函数后加const使得指针指向的值不可修改。
mutable int m_B; //可修改 可变的
- 但添加上述声明后,即使在常函数中,变量亦可修改!
3.5 递增运算符重载
//前置++
MyInteger& operator++() {
//先++
m_Num++;
//再返回
return *this;
}
//后置++
MyInteger operator++(int) {
//先返回
MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;
m_Num++;
return temp;
}
其中,(int)作为占位参数,且只能是int
,用于区分前置和后置递增。注意后置++返回的是值而不是引用
3.6 函数调用运算符重载
- 函数调用运算符 () 也可以重载
- 由于重载后使用的方式非常像函数的调用,因此称为仿函数
- 仿函数没有固定写法,非常灵活
示例:
class MyPrint
{
public:
void operator()(string text)
{
cout << text << endl;
}
};
void test01()
{
//重载的()操作符 也称为仿函数
MyPrint myFunc;
myFunc("hello world");
}
class MyAdd
{
public:
int operator()(int v1, int v2)
{
return v1 + v2;
}
};
void test02()
{
MyAdd add;
int ret = add(10, 10);
cout << "ret = " << ret << endl;
//匿名对象调用
cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}
int main()
{
test01();
test02();
return 0;
}
3.7 菱形继承
- 菱形继承:两个派生类继承同一个基类,又有某个类同时继承者两个派生类
继承前加virtual关键字后,变为虚继承,只保留一份成员变量。
class Animal
{
public:
int m_Age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
//输出均为200
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
3.8 多态满足条件及ptr指针
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件:
- 父类指针或引用指向子类对象
重写:函数返回值类型、函数名、参数列表 完全一致称为重写 |
---|
ptr 指针
在一个类中定义一个virtual修饰的函数时,sizeof这个类,发现类的大小多了恰好一个指针的字节大小,它就是c++编译器给我们添加的vptr指针。
-
当类中声明虚函数时,编译器会在类中生成一个虚函数表;
-
虚函数表是一个存储成员函数指针的数据结构;
-
虚函数表是由编译器自动生成与维护的;
-
virtual成员函数会被编译器放入虚函数表中;
-
存在虚函数时,每个对象都有一个指向虚函数的指针(vptr指针)
-
在实现多态的过程中,父类和派生类都有vptr指针。
定义子类对象时,vptr先指向父类的虚函数表,在父类构造完成之后,子类的vptr才指向自己的虚函数表。(这也就是在父类或者子类的构造函数中调用虚成员函数不会实现多态的原因,这是一道面试题)
3.9 虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。需将父类中的析构函数改为虚析构或者纯虚析构。
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0; //声明
类名::~类名(){} //类外实现
C++默认的析构函数不是虚函数。因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而是只有当需要当作父类时,设置为虚函数。
4 文件操作
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,而通过文件可以将数据持久化。
C++中对文件操作需要包含头文件 < fstream >
文件类型分为两种:
- 文本文件 - 文件以文本的ASCII码形式存储在计算机中
- 二进制文件 - 文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们
操作文件的三大类:
- ofstream:写操作
- ifstream: 读操作
- fstream : 读写操作
文件打开方式 | 解释 |
---|---|
ios::in | 为读文件而打开文件 |
ios::out | 为写文件而打开文件 |
ios::ate | 初始位置:文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建(实际应用时与 ios::out 共同使用才可达到应有效果 ) |
ios::binary | 二进制方式 |
文本文件:
#include <fstream>
//写
void test01()
{
ofstream ofs;
ofs.open("test.txt", ios::out);
ofs << "姓名:张三" << endl;
ofs << "性别:男" << endl;
ofs << "年龄:18" << endl;
ofs.close();
}
//读
void test02()
{
ifstream ifs;
ifs.open("test.txt", ios::in);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
return;
}
//第一种方式
//char buf[1024] = { 0 };
//while (ifs >> buf)
//{ cout << buf << endl; }
//第二种
//char buf[1024] = { 0 };
//while (ifs.getline(buf,sizeof(buf)))
//{ cout << buf << endl; }
//第三种
//string buf;
//while (getline(ifs, buf))
//{ cout << buf << endl; }
//第四种
char c;
while ((c = ifs.get()) != EOF)
{ cout << c; }
ifs.close();
}
二进制文件:
class Person
{
public:
char m_Name[64];
int m_Age;
};
//二进制文件 写文件
void test01()
{
// 创建输出流对象
ofstream ofs("person.txt", ios::out | ios::binary);
// 或者 (上面一行可拆分)
// ofstream ofs;
// ofs.open("person.txt", ios::out | ios::binary);
Person p = {"张三" , 18};
//写文件
ofs.write((const char *)&p, sizeof(p));
ofs.close();
}
//读操作
void test02()
{
ifstream ifs("person.txt", ios::in | ios::binary);
if (!ifs.is_open())
{
cout << "文件打开失败" << endl;
}
Person p;
ifs.read((char *)&p, sizeof(p));
cout << "姓名: " << p.m_Name << " 年龄: " << p.m_Age << endl;
}
5 模板
5.1 函数模板
- 如果函数模板和普通函数都可以实现,优先调用普通函数
// 注意 如果告诉编译器 普通函数是有的,但只是声明没有实现,或者不在当前文件内实现,就会报错。
- 可以通过空模板参数列表来强制调用函数模板
myPrint<>(a, b); //调用函数模板
- 如果函数模板可以产生更好的匹配,优先调用函数模板
5.2 类模板
- 类模板没有自动类型推导的使用方式
- 类模板在模板参数列表中可以有默认参数
//类模板
template<class NameType, class AgeType = int>
class Person
{
public:
Person(NameType name, AgeType age)
{
this->mName = name;
this->mAge = age;
}
public:
NameType mName;
AgeType mAge;
};
Person <string ,int>p("孙悟空", 1000); //必须使用显示指定类型的方式,使用类模板
Person <string> p("猪八戒", 999); //类模板中的模板参数列表 可以指定默认参数
-
类模板中的成员函数并不是一开始就创建的,而是在模板调用时再生成。
-
类模板实例化出的对象,向函数传参的方式有三种:
1. 指定传入的类型 — 直接显示对象的数据类型(最常用)
void printPerson1(Person<string, int> &p)
- 参数模板化 — 将对象中的参数变为模板进行传递
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p)
- 整个类模板化 — 将这个对象类型 模板化进行传递
template<class T>
void printPerson3(T & p)
5.2.1 类模板&继承
当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型。如果不指定,编译器无法给子类分配内存。
如果想灵活指定出父类中T的类型,子类也需变为类模板
template<class T>
class Base
{
T m;
};
//类模板继承类模板 ,可以用T2指定父类中的T类型
template<class T1, class T2>
class Son2 :public Base<T2>
{
};
5.2.2 类模板成员函数类外实现
template<class T1, class T2>
class Person {
public:
//成员函数类内声明
Person(T1 name, T2 age);
void showPerson();
public:
T1 m_Name;
T2 m_Age;
};
//构造函数 类外实现
template<class T1, class T2>
Person<T1, T2>::Person(T1 name, T2 age) {……}
//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {……}
6 STL
-
长久以来,软件界一直希望建立一种可重复利用的东西
-
C++的面向对象和泛型编程思想,目的就是复用性的提升
-
大多情况下,数据结构和算法都未能有一套标准,导致被迫从事大量重复工作
-
为了建立数据结构和算法的一套标准,诞生了STL
6.1 STL基本概念
- STL(Standard Template Library,标准模板库)
- STL 从广义上分为: 容器(container) 算法(algorithm) 迭代器(iterator)
- 容器和算法之间通过迭代器进行无缝连接。
- STL 几乎所有的代码都采用了模板类或者模板函数
6.2 STL六大组件
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器
- 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
- 算法:各种常用的算法,如sort、find、copy、for_each等
- 迭代器:扮演了容器与算法之间的胶合剂。
- 仿函数:行为类似函数,可作为算法的某种策略。
- 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
- 空间配置器:负责空间的配置与管理。
6.3 STL中容器、算法、迭代器
6.3.1 容器 : 置物之所也
STL容器就是将运用最广泛的一些数据结构实现出来
常用的数据结构:数组, 链表,树, 栈, 队列, 集合, 映射表 等
这些容器分为序列式容器和关联式容器两种:
序列式容器: 强调值的排序,序列式容器中的每个元素均有固定的位置。
关联式容器: 二叉树结构,各元素之间没有严格的物理上的顺序关系
6.3.2 算法:问题之解法也
有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms)
算法分为:质变算法和非质变算法。
质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等
非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等
6.3.3 迭代器:容器和算法之间粘合剂
提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
每个容器都有自己专属的迭代器。迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针。
迭代器种类:( 常用的容器中迭代器种类为双向迭代器,和随机访问迭代器)
种类 | 功能 | 支持运算 |
---|---|---|
输入迭代器 | 对数据的只读访问 | 只读,支持++、==、!= |
输出迭代器 | 对数据的只写访问 | 只写,支持++ |
前向迭代器 | 读写操作,并能向前推进迭代器 | 读写,支持++、==、!= |
双向迭代器 | 读写操作,并能向前和向后操作 | 读写,支持++、–, |
随机访问迭代器 | 读写操作,可以以跳跃的方式访问任意数据,功能最强的迭代器 | 读写,支持++、–、[n]、-n、<、<=、>、>= |
6.3.4 示例
容器: vector
算法: for_each
迭代器: vector<int>::iterator
void test() {
//创建vector容器对象,并且通过模板参数指定容器中存放的数据的类型
vector<int> v;
//向容器中放数据
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
//每一个容器都有自己的迭代器,迭代器是用来遍历容器中的元素
//v.begin()返回迭代器,这个迭代器指向容器中第一个数据
//v.end()返回迭代器,这个迭代器指向容器元素的最后一个元素的下一个位置
//vector<int>::iterator 拿到vector<int>这种容器的迭代器类型
vector<int>::iterator pBegin = v.begin();
vector<int>::iterator pEnd = v.end();
//第一种遍历方式:
while (pBegin != pEnd) {
cout << *pBegin << endl;
pBegin++;
}
//第二种遍历方式:
for (vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << endl;
}
cout << endl;
//第三种遍历方式:
//使用STL提供标准遍历算法 头文件 algorithm
for_each(v.begin(), v.end(), MyPrint);
}
(*it)
代表<>里面的类型,此处为int
void MyPrint(int val)
{
cout << val << endl;
}
6.4 STL – 常用容器
6.4.1 string容器
-
string是C++风格的字符串,而string本质上是一个类(类内部封装了很多成员方法)
例如:查找find,拷贝copy,删除delete 替换replace,插入insert
string管理char*所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责
string和char * 区别:
- char * 是一个指针
- string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器。
构造函数原型:
string(); //创建一个空的字符串 例如: string str;
string(const char* s); //使用字符串s初始化
string(const string& str); //使用一个string对象初始化另一个string对象
string(int n, char c); //使用n个字符c初始化
字符串拼接:
string& operator+=(const char* str); //重载+=操作符
string& operator+=(const char c); //重载+=操作符
string& operator+=(const string& str); //重载+=操作符
string& append(const char *s); //把字符串s连接到当前字符串结尾
string& append(const char *s, int n); //把字符串s的前n个字符连接到当前字符串结尾
string& append(const string &s); //同operator+=(const string& str)
string& append(const string &s, int pos, int n);//字符串s中从pos开始的n个字符连接到字符串结尾
查找、替换:
//find找到字符串后返回查找的第一个字符位置,找不到返回-1
int find(const string& str, int pos = 0) const; //查找str第一次出现位置,从pos开始查找
int find(const char* s, int pos = 0) const; //查找s第一次出现位置,从pos开始查找
int find(const char* s, int pos, int n) const; //从pos位置查找s的前n个字符第一次位置
int find(const char c, int pos = 0) const; //查找字符c第一次出现位置
int rfind(const string& str, int pos = npos) const; //查找str最后一次位置,从pos开始查找
int rfind(const char* s, int pos = npos) const; //查找s最后一次出现位置,从pos开始查找
int rfind(const char* s, int pos, int n) const; //从pos查找s的前n个字符最后一次位置
int rfind(const char c, int pos = 0) const; //查找字符c最后一次出现位置
string& replace(int pos, int n, const string& str); //替换从pos开始n个字符为字符串str
string& replace(int pos, int n,const char* s); //替换从pos开始的n个字符为字符串s
比较、存取:
// = 返回 0 >返回 1 < 返回 -1
int compare(const string &s) const; //与字符串s比较
int compare(const char *s) const; //与字符串s比较
char& operator[](int n); //通过[]方式取字符
char& at(int n); //通过at方法获取字符
插入、删除:
string& insert(int pos, const char* s); //插入字符串
string& insert(int pos, const string& str); //插入字符串
string& insert(int pos, int n, char c); //在指定位置插入n个字符c
string& erase(int pos, int n = npos); //删除从Pos开始的n个字符
获取子串:
string substr(int pos = 0, int n = npos) const; //返回由pos开始的n个字符组成的字符串
// for example
string email = "hello@sina.com";
int pos = email.find("@");
string username = email.substr(0, pos);
6.4.2 vector容器 (单端数组)
vector数据结构和数组非常相似,也称为单端数组。不同之处在于数组是静态空间,而vector可以动态扩展:
(并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间)
函数原型:
vector<T> v;
//采用模板实现类实现,默认无参构造函数vector(v.begin(), v.end());
//将v[begin(), end())区间中的元素拷贝给本身。vector(n, elem);
//构造函数将n个elem拷贝给本身。vector(const vector &vec);
//拷贝构造函数。
vector赋值操作:
vector& operator=(const vector &vec);
//重载等号操作符assign(beg, end);
//将[beg, end)区间中的数据拷贝赋值给本身。assign(n, elem);
//将n个elem拷贝赋值给本身
对vector容器的容量和大小操作:
empty();
//判断容器是否为空capacity();
//容器的容量size();
//返回容器中元素的个数resize(int num);
//重新指定容器的长度为num,若容器变长,则以默认值填充新位置。
//如果容器变短,则末尾超出容器长度的元素被删除。resize(int num, elem);
//重新指定容器的长度为num,若容器变长,则以elem值填充新位置。
vector插入和删除 :
push_back(ele);
//尾部插入元素elepop_back();
//删除最后一个元素insert(const_iterator pos, ele);
//迭代器指向位置pos插入元素eleinsert(const_iterator pos, int count,ele);
//迭代器指向位置pos插入count个元素eleerase(const_iterator pos);
//删除迭代器指向的元素erase(const_iterator start, const_iterator end);
//删除迭代器从start到end之间的元素clear();
//删除容器中所有元素
// for example
vector<int> v1;
v1.insert(v1.begin()+1, 100); //vector容器第二个数字处插入100
数据存取:
at(int idx);
//返回索引idx所指的数据operator[];
//返回索引idx所指的数据front();
//返回容器中第一个数据元素back();
//返回容器中最后一个数据元素
swap(); 可用于vector容器元素互换,主要用于收缩内存。
//收缩内存
vector<int>(v).swap(v);
// vector<int>(v)为匿名对象
6.4.3 deque容器 (双端数组)
双端数组,可以对头端进行插入删除操作
deque与vector区别:
- vector对于头部的插入删除效率低,数据量越大,效率越低
- deque相对而言,对头部的插入删除速度回比vector快
- vector访问元素时的速度会比deque快,这和两者内部实现有关
- deque容器和vector容器的构造方式几乎一致
deque内部工作原理:
deque内部有个中控器,维护每段缓冲区中的内容,缓冲区中存放真实数据。
中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间。
deque容器无capacity的概念 !
deque 插入:
insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值
//插入和删除提供的位置是迭代器!
deque<int> d;
d.push_back(10);
d.push_back(20);
d.push_front(100);
d.push_front(200);
// 200 100 10 20
// d2: 1 2 3
d.insert(d.begin(), d2.begin(), d2.end()); // 1 2 3 200 100 10 20
deque 排序:
算法:sort(iterator beg, iterator end) //对beg和end区间内元素进行排序
包含头文件 algorithm 即可 (vector也可用sort())
6.4.4 栈(stack)、队列(queue)容器
stack是一种先进后出(First In Last Out,FILO)的数据结构.
push(elem); //向栈顶添加元素
pop(); //从栈顶移除第一个元素
top(); //返回栈顶元素
empty(); //判断堆栈是否为空
size(); //返回栈的大小
queue是一种先进先出(First In First Out,FIFO)的数据结构.
push(elem); //往队尾添加元素
pop(); //从队头移除第一个元素
back(); //返回最后一个元素
front(); //返回第一个元素
empty(); //判断堆栈是否为空
size(); //返回栈的大小
6.4.5 链表(list)容器
链表(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。STL中的链表是一个双向循环链表。
由于链表的存储方式并不是连续的内存空间,因此链表list中的迭代器只支持前移和后移,属于双向迭代器。
list的优点:
采用动态存储分配,不会造成内存浪费和溢出
链表执行插入和删除操作十分方便,修改指针即可,不需要移动大量元素
list的缺点:
链表灵活,但是空间(指针域) 和 时间(遍历)额外耗费较大
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效,这在vector是不成立的。
总结:
STL中List和vector是两个最常被使用的容器,各有优缺点。
list 的各项操作:
// 插入
insert(pos,elem);//在pos位置插elem元素的拷贝,返回新数据的位置。
// for example
list<int>::iterator it = L.begin();
L.insert(++it, 1000);
新接口:
remove(elem); //删除容器中所有与elem值匹配的元素。
reverse(); //反转链表
list容器中不可以通过[ ]或者at方式访问随机数据
// 仅支持
front(); //返回第一个元素。
back(); //返回最后一个元素。
list<int>::iterator it = L1.begin();
//it++;
//it--; //正确,支持双向
//it = it + 1; //错误,不可以跳跃访问,即使是+1
不支持随机访问的容器迭代器,不可使用标准算法 sort(L.begin(), L.end());
相对应的,其内部会提供对应一些算法 :
L.sort(); //默认的排序规则 从小到大
L.sort(myCompare); //指定规则,从大到小
bool myCompare(int val1 , int val2)
{
return val1 > val2;
}
对于自定义数据类型,必须要指定排序规则,否则编译器不知道如何进行排序
高级排序只是在排序规则上再进行一次逻辑规则制定,并不复杂
// for example
将Person自定义数据类型进行排序,Person中属性有姓名、年龄、身高
排序规则:按照年龄进行升序,如果年龄相同按照身高进行降序
bool ComparePerson(Person& p1, Person& p2) {
if (p1.m_Age == p2.m_Age) {
return p1.m_Height > p2.m_Height;
}
else
{
return p1.m_Age < p2.m_Age;
}
}
……
……
L.sort(ComparePerson); // 高级排序
6.4.6 set / multiset 容器
所有元素都会在插入时自动被排序
本质:set/multiset属于关联式容器,底层结构是用二叉树实现。
set和multiset区别:
- set不允许容器中有重复的元素
- multiset允许容器中有重复的元素
insert(elem); //在容器中插入元素。
clear(); //清除所有元素
erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end); //删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(elem); //删除容器中值为elem的元素。
类似于list的 remove(elem);
set查找、统计&&插入、删除数据:
find(key); //查找key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
count(key); //统计key的元素个数
insert(elem); //在容器中插入元素。
clear(); //清除所有元素
erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end); //删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(elem); //删除容器中值为elem的元素。
pair对组
pair<type, type> p ( value1, value2 );
// pair<string, int> p(string("Tom"), 20);
pair<type, type> p = make_pair( value1, value2 );
// pair<string, int> p2 = make_pair("Jerry", 10);
利用仿函数,可改变set容器(默认从小到大)排序规则:
class MyCompare
{
public:
bool operator()(int v1, int v2) {
return v1 > v2;
}
};
//指定排序规则
set<int,MyCompare> s;
set 存放自定义数据类型
#include <set>
class Person
{
public:
Person(string name, int age)
{
this->m_name = name;
this->m_age = age;
}
public:
string m_name;
int m_age;
};
class Compare //指定排序规则类
{
public:
bool operator()(const Person &p1, const Person &p2)
{
return p1.m_age < p2.m_age;
}
};
void test()
{
set<Person, Compare> s;
Person p1("刘备", 23);
Person p2("关羽", 27);
Person p3("张飞", 25);
Person p4("赵云", 21);
s.insert(p1);
s.insert(p2);
s.insert(p3);
s.insert(p4);
for(set<Person, Compare>::const_iterator it = s.begin(); it != s.end(); it++)
{
cout<<(*it).m_name<<" "<<(*it).m_age<<endl;
}
}
6.4.7 map / multimap容器
- map中所有元素都是pair
- pair中第一个元素为key(键值),起到索引作用,第二个元素为value(实值)
- 所有元素都会根据元素键值key自动排序
本质:map/multimap属于关联式容器,底层结构是用二叉树实现。
map和multimap区别:
- map不允许容器中有重复key值元素
- multimap允许容器中有重复key值元素
map容器 插入、删除数据
insert(elem); //在容器中插入元素。 elem 为对组(pair)
clear(); //清除所有元素
erase(pos); //删除pos迭代器所指的元素,返回下一个元素的迭代器。
erase(beg, end); //删除区间[beg,end)的所有元素 ,返回下一个元素的迭代器。
erase(key); //删除容器中值为key的元素。
//插入
map<int, int> m;
//第一种插入方式
m.insert(pair<int, int>(1, 10));
//第二种插入方式
m.insert(make_pair(2, 20));
//第三种插入方式
m.insert(map<int, int>::value_type(3, 30));
//第四种插入方式 不推荐
m[4] = 40;
第四种插入方式,若map容器无相关值(如m[5]
),则自动插入,value值为0
map 查找和统计
- find()返回迭代器
find(key); //查找key是否存在,若存在,返回该键的元素的迭代器;若不存在,返回set.end();
count(key); //统计key的元素个数
利用仿函数可以指定map容器的排序规则
对于自定义数据类型,map必须要指定排序规则,同set容器
//默认从小到大排序
//利用仿函数实现从大到小排序
map<int, int, Compare> m;
6.5 STL – 函数对象(仿函数)
- 重载函数调用操作符的类,其对象常称为函数对象
- 函数对象使用重载的()时,行为类似函数调用,也叫仿函数
函数对象(=仿函数)本质上是一个类,不是一个函数
// 1、函数对象在使用时,可以像普通函数那样调用, 可以有参数,可以有返回值
class MyAdd
{
public :
int operator()(int v1,int v2)
{
return v1 + v2;
}
};
cout << myAdd(10, 10) << endl;
// 2、函数对象可以有自己的状态
class MyPrint
{
public:
MyPrint()
{ count = 0; }
void operator()(string test)
{
cout << test << endl;
count++; //统计使用次数
}
int count; //内部自己的状态
};
// 3、函数对象可以作为参数传递
void doPrint(MyPrint &mp , string test)
{
mp(test);
}
void test03()
{
MyPrint myPrint;
doPrint(myPrint, "Hello C++");
}
6.5.1 谓词
- 返回bool类型的仿函数称为谓词(即特殊的仿函数)
- 如果operator()接受一个参数,那么叫做一元谓词
- 如果operator()接受两个参数,那么叫做二元谓词
#include <vector>
#include <algorithm>
// 1.一元谓词
struct GreaterFive{
bool operator()(int val) {
return val > 5;
}
};
void test()
{
vector<int> v;
for (int i = 0; i < 10; i++)
{
v.push_back(i);
}
vector<int>::iterator it = find_if(v.begin(), v.end(), GreaterFive());
if (it == v.end()) { cout << "没找到!" << endl; }
else { cout << "找到:" << *it << endl; }
}
find_if(_InIt _First, const _InIt _Last, _Pr _Pred) //find first satisfying _Pred
#include <vector>
#include <algorithm>
// 2.二元谓词
class MyCompare
{
public:
bool operator()(int num1, int num2)
{
return num1 > num2;
}
};
void test()
{
vector<int> v;
v.push_back(10);
v.push_back(40);
v.push_back(20);
v.push_back(30);
v.push_back(50);
//默认从小到大
sort(v.begin(), v.end());
//使用函数对象改变算法策略,排序从大到小
sort(v.begin(), v.end(), MyCompare());
}
6.5.2 内建函数对象
需要引入头文件#include <functional>
分类:
- 算术仿函数
- 关系仿函数
- 逻辑仿函数
(1)算术仿函数
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>
//取反仿函数 一元运算
// for example
#include <functional>
//negate
negate<int> n;
cout << n(50) << endl;
//plus
plus<int> p;
cout << p(10, 20) << endl;
(2)关系仿函数
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>
//小于等于
//自己实现仿函数
//sort(v.begin(), v.end(), MyCompare());
//STL内建仿函数 大于仿函数
sort(v.begin(), v.end(), greater<int>());
sort(_First, _Last, less<>()); // sort()默认小于号
(3)逻辑仿函数
template <class T> bool logical_and<T>
//逻辑与template <class T> bool logical_or<T>
//逻辑或template <class T> bool logical_not<T>
//逻辑非
6.6 STL – 常用算法
-
算法主要是由头文件
<algorithm>
<functional>
<numeric>
组成。 -
<algorithm>
是所有STL头文件中最大的一个,范围涉及到比较、 交换、查找、遍历操作、复制、修改等等 -
<numeric>
体积很小,只包括几个在序列上面进行简单数学运算的模板函数 -
<functional>
定义了一些模板类,用以声明函数对象。
6.6.1 常用遍历算法
for_each(iterator beg, iterator end, _func);
//遍历容器,_func 为函数或者函数对象,较为常用。transform
//搬运容器到另一个容器中
#include <algorithm>
#include <vector>
//普通函数
void print01(int val)
{
cout << val << " ";
}
//函数对象
class print02
{
public:
void operator()(int val)
{
cout << val << " ";
}
};
//for_each算法基本用法
void test() {
vector<int> v;
for (int i = 0; i < 10; i++)
{
v.push_back(i);
}
//遍历算法
for_each(v.begin(), v.end(), print01);
cout << endl;
for_each(v.begin(), v.end(), print02());
cout << endl;
}
transform(iterator beg1, iterator end1, iterator beg2, _func);
//beg1 源容器开始迭代器 ············//end1 源容器结束迭代器
//beg2 目标容器开始迭代器 ············ //_func 函数或者函数对象
搬运的目标容器必须要提前开辟空间,否则无法正常搬运。
vTarget.resize(v.size()); // 目标容器需要提前开辟空间
6.6.2 常用查找算法
find
//查找元素find_if
//按条件查找元素adjacent_find
//查找相邻重复元素binary_search
//二分查找法count
//统计元素个数count_if
//按条件统计元素个数
find(iterator beg, iterator end, value);
// value 查找的元素
// 按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置
find_if(iterator beg, iterator end, _Pred);
// _Pred 函数或者谓词(bool类型仿函数)
// 按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置
adjacent_find(iterator beg, iterator end);
// 查找相邻重复元素,返回相邻元素的第一个位置的迭代器
bool binary_search(iterator beg, iterator end, value);
// 查找指定的元素,查到 返回true 否则false
// 注意: 在无序序列中不可用
6.6.3 常用排序算法
sort(iterator beg, iterator end, _Pred);
//对容器内元素进行排序random_shuffle(iterator beg, iterator end);
//洗牌 指定范围内的元素随机调整次序merge(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);
// 容器元素合并,并存储到另一容器中
// 注意: 两个容器必须是有序的reverse(iterator beg, iterator end);
// 反转指定范围的元素
6.6.4 常用拷贝和替换算法
copy(iterator beg, iterator end, iterator dest);
// 容器内指定范围的元素拷贝到另一容器中replace(iterator beg, iterator end, oldvalue, newvalue);
// 将容器内指定范围的旧元素修改为新元素replace_if(iterator beg, iterator end, _pred, newvalue);
// 容器内指定范围满足条件的元素替换为新元素swap(container c1, container c2);
// 互换两个容器的元素 : c1 容器1, c2 容器2
6.6.5 常用算术生成算法
使用时,需引用头文件 numeric
-
accumulate(iterator beg, iterator end, value);
// value 起始值
// 计算容器元素累计总和 -
fill(iterator beg, iterator end, value);
// 向容器中添加元素(将容器区间内元素填充为指定的值)
6.6.6 常用集合算法
// 注意:两个集合必须是有序序列
set_intersection(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);
// 求两个容器的交集
#include <algorithm>
vector<int> v1;
vector<int> v2;
for (int i = 0; i < 10; i++)
{
v1.push_back(i);
v2.push_back(i+5);
}
vector<int> vTarget;
//取两个里面较小的值给目标容器开辟空间
vTarget.resize(min(v1.size(), v2.size()));
//返回目标容器的最后一个元素的迭代器地址
vector<int>::iterator itEnd =
set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), vTarget.begin());
for_each(vTarget.begin(), itEnd, myPrint());
-
set_union(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);
// 求两个容器的并集 -
set_difference(iterator beg1, iterator end1, iterator beg2, iterator end2, iterator dest);
// 求两个容器的差集
// set_difference() 目标容器开辟空间需要从两个容器取较大值