基本概念(一)

在某些操作系统中,C和C++是同一个编译器,在CentOS中,C的编译器是gcc,C++的编译器是g++;
编译C程序的命令是gcc,编译C++程序的命令是g++,g++命令和gcc命令的用法相同,把gcc改为g++就可以了(当然这都是基于Linux系统下命令行的方式,如果你使用的IDE就当我没说…)

1.变量、指针和引用

1.1
变量名其实就是程序可操作的存储区的名称,不管是普通类型变量还是引用类型变量,与JS一样
变量名就是为了方便我们使用变量这块地址而设计的标签(这个标签指向了一个地址,我们直接用地址也可以访问变量值),变量值就是这块地址中保存的内容
普通类型的变量值就是一个值,而引用类型的变量的变量值是一个地址,指向了内存中的另外一个地方
我们可以通过 &变量名 获取变量名指向的内存中的地址

1.2
指针是一个变量名,其值为另一个变量的地址

  1. int var = 20; // 实际变量的声明
  2. int *ip; // 指针变量的声明 这个语句中*用来声明指针变量
  3. ip = &var; // 在指针变量中存储 var 的地址
  4. // 输出在指针变量中存储的地址
  5. cout << "Address stored in ip variable: ";
  6. cout << ip << endl;
  7. // 访问指针中地址的值
  8. cout << "Value of *ip variable: ";
  9. cout << *ip << endl;//这个语句中的*用来访问指针指向的地址中的值(可能是值也可能还是一个地址)

1.3
引用是一个变量名,其值为另一个变量名

// 声明简单的变量
   int    i;
   double d;

// 声明引用变量
   int&    r = i;
   double& s = d;

2.struct和class

C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 权限的;而使用 struct 时,结构体中的成员默认都是 public 权限的。
  • class 继承默认是 private 继承,而 struct 继承默认是 public 继承(《C++继承与派生》一章会讲解继承)。
  • class 可以使用模板,而 struct 不能(《模板、字符串和异常》一章会讲解模板)。

在编写C++代码时,强烈建议使用 class 来定义类,而使用 struct 来定义结构体,这样做语义更加明确

3.双冒号

通俗理解
A公司开发了一个产品A_com 包含print()函数
B公司开发了一个产品B_com 也包含print()函数
那你写程序导入后怎么区分用的是哪个print()函数呢?
A_com::print() 代表A公司产品中的print()函数
B_com::print() 代表B公司产品中的print()函数
而 A_com 和 B_com 都称为名称空间

基本用法
(1)表示“域操作符”
  例:声明了一个类A,类A里声明了一个成员函数void f(),但没有在类的声明里给出f的定义,那么在类外定义f时,就要写成void A::f(),表示这个f()函数是类A的成员函数。
(2)直接用在全局函数前,表示是全局函数
  例:在VC里,你可以在调用API 函数里,在API函数名前加::
(3)表示引用成员函数及变量,作用域成员运算符
  例:System::Math::Sqrt() 相当于System.Math.Sqrt()

4.int main()

  • int main(void)指的是此函数的参数为空,不能传入参数,如果你传入参数,就会出错
  • int main()表示可以传入参数

类和对象(二)

C++面向对象的三大特性为:封装、继承和多态

1.封装

  • 将属性和行为作为一个整体,表现生活中的事物

语法:class 类名{ 访问权限: 属性 / 行为 };

  • 将属性和行为加以权限控制,访问权限有三种
    1. public 公共权限 类内可以访问,类外可以访问
    2. protected 保护权限 类内可以访问,类外不可以访问
    3. private 私有权限 类内可以访问,类外不可以访问
  • 类的组成由属性和方法组成,属性可以称为成员变量,方法可以称为成员函数

2. struct和class

在C++中 struct和class唯一的区别就在于默认的访问权限不同(所以写类时使用struct关键字也是允许的)

  • struct 默认权限为公共
  • class 默认权限为私有

3.对象

C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置

3.1 构造函数和析构函数

如果我们不提供构造和析构,编译器会提供,编译器提供的构造函数和析构函数是空实现,这两个函数将会被编译器自动调用,完成对象初始化和清理工作

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法:~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

3.2 构造函数的分类及调用

两种分类方式:

  • 按参数分为: 有参构造和无参构造
  • 按类型分为: 普通构造和拷贝构造

三种调用方式:

  • 括号法
  • 显示法
  • 隐式转换法
//无参(默认)构造函数
Person() {
    cout << "无参构造函数!" << endl;
}
//有参构造函数
Person(int a) {
    age = a;
    cout << "有参构造函数!" << endl;
    }
//拷贝构造函数
    Person(const Person& p) {//const为了限制不让修改原对象
        age = p.age;
        cout << "拷贝构造函数!" << endl;
    }
    //2.1 括号法,常用
    Person p1;//括号法调用默认构造函数
    Person p1(10);//括号法调用有参构造函数
    Person p1(p2);//括号法调用拷贝构造函数
    //注意1:调用默认构造函数不能加括号,如果加了编译器认为这是一个函数声明
    //错误示例 Person p2();

    //2.2 显式法
    //等号右边是匿名对象,等号左边相当于取名字了
    //匿名对象,其特点是当前行结束之后,马上析构删除掉
    Person p2 = Person(10);//显式调用有参构造函数
    Person p2 = Person(p1);//显式调用拷贝构造函数

    //注意2:尽量不要利用拷贝构造函数来初始化匿名对象,编译器会认为是对象声明
    //错误示例 Person(p1);
    //编译器会认为是Person(p1)=Person p1,这是一个无参构造函数,假如已经有了一个p1这将导致重定义

    //2.3 隐式转换法
    Person p4 = 10; // Person p4 = Person(10); //有参构造
    Person p5 = p4; // Person p5 = Person(p4); //拷贝构造


3.3 拷贝构造函数的调用时机

C++中需要使用到拷贝构造函数的时机通常有三种情况

  • 使用一个已经创建完毕的对象来初始化一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
    Person(const Person& p) {
          cout << "拷贝构造函数!" << endl;
          mAge = p.mAge;
    }
    
    //1. 使用一个已经创建完毕的对象来初始化一个新对象
    void test01() {
      Person man(100); //man对象已经创建完毕
      Person newman(man); //第一种方式调用拷贝构造函数
    }
    //若Person newman=man这里的newman不存在而man存在则属于调用拷贝构造函数进行对象的初始化
    //若newman=man这里的newman和man均已经存在(已经声明过)则属于对象赋值
    
//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) //定义一个函数
{}

void test02() {
    Person p; //无参构造函数
    doWork(p);//值传递的方式给函数参数传值
}
//3. 以值方式返回局部对象
Person doWork2()
{
    Person p1;
    return p1;//局部对象有一个特点,在函数结束就销毁,可以用值传递方式返回但不能用引用方式返回
}

3.4 构造函数创建规则

默认情况下,c++编译器至少给一个类添加3个函数
1.默认构造函数(无参,函数体为空)
2.默认析构函数(无参,函数体为空)
3.默认拷贝构造函数,对属性进行值拷贝

构造函数创建规则如下:

  • 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造和析构
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数,但是会提供析构

3.5 深拷贝和浅拷贝

系统提供的默认拷贝构造函数工作方式是也就是浅拷贝。如果对象中用到了需要手动释放的对象(即该对象含有指针成员,在调用构造函数时有int m_Height=new int(height)创建堆区数据,该指针成员的值为0x0001;那么在调用默认拷贝构造函数时新对象的指针成员的值因为仅仅只拷贝了值所以仍然为0x0001,导致两个对象的指针成员指向同一个堆区),则会出现问题——在两个对象分别销毁之前会触发两次析构函数(故需要进行delete操作),因为两个对象的指针成员所指内存相同,在程序结束时该内存会被delete两次,造成内存泄漏问题
故在对*含有指针成员
的对象进行拷贝时,必须要自己重载拷贝构造函数,使拷贝后的对象指针成员有自己的内存空间

浅拷贝:简单的赋值拷贝操作;如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用)
深拷贝:在堆区重新申请空间,进行拷贝操作;如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝

//有参构造函数
    Person(int age ,int height) {

        cout << "有参构造函数!" << endl;

        m_age = age;
        m_height = new int(height);

    }
//自定义拷贝构造函数  
    Person(const Person& p) {
        cout << "拷贝构造函数!" << endl;
        m_age = p.m_age;
        m_height = new int(*p.m_height);
    }

3.6 初始化列表

    //传统方式初始化——构造函数初始化
    Person(int a, int b, int c) {
        m_A = a;
        m_B = b;
        m_C = c;
    }

    //初始化列表方式初始化
    Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}

    //创建对象,上面两种方式都可以
    Person p(10,20,30)

3.7 类对象作为类成员

C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
当类中成员是其他类对象时,我们称该成员为 对象成员,构造的顺序是 :先调用对象成员的构造,再调用本类构造,析构顺序与构造相反

3.8 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,统称为静态成员,静态成员并不属于某个对象

  • 静态成员变量static int m_B;
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
    • 静态成员不需要通过对象就能访问
  • 静态成员函数static void func(){}
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量 ```cpp class Person { public: //非静态成员变量占对象空间 int mA; //静态成员变量不占对象空间,sizeof 运算符不会计算成员变量 static int mB; //函数也不占对象空间,所有函数共享一个函数实例 void func() { cout << “mA:” << this->mA << endl; } //静态成员函数也不占对象空间 static void sfunc() { }

} //综上所述,只有非静态成员变量可以说是属于某个类的对象的


<a name="wZMjG"></a>
### 3.9 this指针
根据上节可知C++中成员变量和成员函数是分开存储的,每一个**非静态成员函数**只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码,这一块代码是如何区分哪个对象调用自己的呢?C++通过提供特殊的对象指针,this指针,解决上述问题。

- **this指针指向被调用的成员函数所属的对象**
- this指针是隐含在每一个非静态成员函数内的一种指针,不需要定义,直接使用即可

普通成员函数在参数传递时编译器会隐藏地传递一个this指针.通过this指针来确定调用类产生的哪个对象;但是静态成员函数没有this指针,不知道应该访问哪个对象中的数据,所以在程序中不可以用静态成员函数访问类中的普通变量(只能访问静态成员变量)<br />所以静态成员函数有什么用呢?——用于解决在C++里如果需要调一个在类里,但跟类的实例无关的函数;有一些场景是必须用静态成员函数的,使用普通成员函数会出错(暂时开发中还没有遇到过)**包含静态成员函数的类不需要实例化就能调用该静态函数**

<a name="sEfGS"></a>
### 3.10 const修饰
常函数

- 成员函数后加const后我们称为这个函数为**常函数**
- 常函数内不可以修改成员变量
- 成员属性声明时加关键字mutable后,在常函数中依然可以修改

常对象

- 声明对象前加const称该对象为**常对象**
- 不允许修改普通成员变量
- 常对象只能调用常函数

```cpp
void ShowPerson() const {}
//我们在类中定义的成员函数,是默认使用了this指针的
//this指针是一个指针常量int * const this,它的指向不可修改,默认就是调用它的那个对象
//其次假如我们想对this指针指向的对象的成员变量做限定使其无法修改,我们有两种方法
//第一种是将this指针再次限定为 const int * const this
//第二种就是利用常函数

4.友元

生活中你的家有客厅(Public),有你的卧室(Private),客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是你也可以允许你的好闺蜜好基友进去
在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术
友元的目的就是让一个函数或者类访问另一个类中私有成员
友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元
    class Building
    {
      //在类定义中告诉编译器 goodGay全局函数是Building类的好朋友,可以访问类中的私有内容
      friend void goodGay(Building * building);//friend 全局函数原型
    }
    
    class Building
    {
      //告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容
      friend class goodGay;//friend 类声明
    }
    
    class Building
    {
      //告诉编译器  goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容
      friend void goodGay::visit();//friend 成员函数原型
    }
    

    5.运算符重载

    运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型

5.1 加法运算符重载

针对两个整型做加法可以使用加法运算符,但是对于自定义的数据类型如Person类则需要重载以实现两个自定义数据类型相加的运算(Q:运算符重载应该在哪里定义呢?内外还是类内部呢?A:运算符重载在类的内外,取决于操作数所处的位置)

//此部分看Primer的代码非常简洁

总结1:对于内置的数据类型的表达式的的运算符是不可能改变的(不允许1+1=0这种情况发生)
总结2:不要滥用运算符重载

6.继承

继承是面向对象三大特性之一
定义某些类时,下级别的成员除了拥有上一级的共性,还有自己的特性;这个时候我们就可以考虑利用继承的技术,减少重复代码

继承的好处:可以减少重复的代码
class A : public B; 
A 类称为子类 或 派生类
B 类称为父类 或 基类

派生类中的成员,包含两大部分
一类是从基类继承过来的,一类是自己增加的成员
基类继承过来的表现其共性,而新增的成员体现了其个性

6.1 继承的方式

继承的语法:class 子类 : 继承方式 父类
继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承

image.png

6.2 继承中的对象模型

在继承中子类可以继承父类的所有共性的内容,那么在这些共性的内容在对象中是否属于该对象呢(即是否占用子类对象的内存)
image.png

  1. 父类中的所有非静态成员属性都会被子类继承(包括私有成员属性)
  2. 父类中的私有成员属性无法访问是因为被编译器隐藏了,因此无法访问

6.3 继承中构造和析构

子类继承父类后,当创建子类对象,也会调用父类的构造函数(我们可以认为子类对象就是父类对象,子类对象的类型可以是子类也可以是父类,但反过来就不一定成立)
特点:

  1. 继承中,先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

6.4 继承中的同名成员

Q:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的成员?
A:

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域(在子类对象的后面加父类作用域)
    当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
    如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
    s.func();//调用子类的同名成员
    s.Base::func();//调用父类的同名成员
    

6.5 继承同名静态成员

Q:继承中同名的静态成员在子类对象上如何进行访问?
A:静态成员和非静态成员同名处理方式一致

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域
    //通过对象访问
      Son s;
      cout << "通过对象访问: " << endl;
      s.func();
      s.Base::func();
    //通过类名访问(之所以能通过类名访问是因为所有对象共享同一份数据)
      cout << "通过类名访问: " << endl;
      Son::func();
      Son::Base::func();
      //出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
    

6.6 多继承

C++允许一个类继承多个类(C++实际开发中不建议用多继承),多继承可能会引发父类中有同名成员出现,需要加作用域区分
语法:class 子类 :继承方式 父类1 , 继承方式 父类2…

//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{
    Son s;
    cout << "sizeof Son = " << sizeof(s) << endl;
    cout << s.Base1::m_A << endl;//调用第一个基类的m_A成员
    cout << s.Base2::m_A << endl;//调用第二个基类的m_B成员
}

6.7 菱形继承

image.png
两个派生类继承同一个基类,又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石继承
菱形继承的问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以
//Animal类基类
class Animal
{
public:
    int m_Age;//年龄成员变量
};

//继承前加virtual关键字后,变为虚继承,利用虚继承可以解决菱形继承浪费空间的问题
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};//虚基类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;//菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义

    //使用虚继承后所有的访问方式得到的Age都是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;
}

7.多态

多态是C++面向对象三大特性之一
1.多态分为两类

  • 静态多态: 函数重载和运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数实现运行时多态

2.静态多态和动态多态区别

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

3.动态多态需要满足的条件:

  • 有继承关系
  • 子类重写父类中的虚函数

4.如何使用动态多态:
使用父类指针或引用指向子类对象/子类Animal &animal=Cat cat1 ; Animal *animal=new Cat

class Animal
{
public:
    //Speak函数就是虚函数
    //函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了,只能在运行函数时调用函数
    virtual void speak()
    {
        cout << "动物在说话" << endl;
    }
};

class Cat :public Animal
{
public:
    void speak()
    {
        cout << "小猫在说话" << endl;
    }
};

class Dog :public Animal
{
public:

    void speak()
    {
        cout << "小狗在说话" << endl;
    }

};
/*
我们希望传入什么对象,就调用该对象的对应函数(动态联编)
如果函数地址在编译阶段就能确定,那么就是静态联编(地址早绑定)
如果函数地址在运行阶段才能确定,就是就是动态联编
*/

//假如不令speak成为虚函数,即地址早绑定,在编译的阶段就已经绑定了函数地址,所以无论传入哪个对象都是调用Animal的speak函数
void DoSpeak(Animal & animal)//执行说话的函数
{    //注意此处我们很容易会想为什么不能直接使用 子类对象.对象方法 的方式来调用该对象的方法,因为题目要求就是传入什么对象就执行该对象的方法,开发者事先并不知道会传入哪些对象!!!
    animal.speak();
}
//由于speak是虚函数,不能在编译时确定函数的地址,只有在运行时才会寻找函数具体地址,故实现了动态多态
void test01()
{
    Cat cat1;
    DoSpeak(cat1);


    Dog dog1;
    DoSpeak(dog1);
}


7.1 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将基类中的虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数(只要该类有一个纯虚函数),这个类也称为抽象类

抽象类特点

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类 ```cpp class Base { public: //纯虚函数 virtual void func() = 0; };

class Son :public Base//子类继承 { public: virtual void func() { cout << “func调用” << endl; }; };

<a name="h5Ly6"></a>
### 7.2 虚析构和纯虚析构
多态使用时,如果**子类中有属性开辟到堆区**,那么父类指针(动态多态出现的概念)在释放时无法调用到子类的析构代码,解决方式是将**父类中的析构函数**改为**虚析构**或者**纯虚析构**(如果子类中没有堆区数据,可以不写为虚析构或纯虚析构)

1.虚析构和纯虚析构共性:

- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现

2.虚析构和纯虚析构区别:

- 如果是纯虚析构,该类属于抽象类,无法实例化对象

3.虚析构语法:`virtual ~类名(){};`<br />4.纯虚析构语法:`virtual ~类名() = 0;`  OR  `类名::~类名(){};`

<a name="zv0XZ"></a>
# 文件操作(三)
程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放,通过**文件可以将数据持久化**<br />C++中要对文件操作需要包含头文件 < fstream >(注意该头文件没有.h后缀名)<br />文件操作的三大类:

1. ofstream:写操作
1. ifstream: 读操作
1. fstream : 读写操作

<a name="FMyaM"></a>
## 1.文本文件
<a name="OjWIq"></a>
### 1.1 写文件

1. 包含头文件 #include <fstream>
1. 创建流对象 ofstream ofs;
1. 打开文件ofs.open("文件路径",打开方式);
1. 写数据ofs << "写入的数据";
1. 关闭文件ofs.close();
| **打开方式** | **解释** |
| --- | --- |
| ios::in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置:文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 如果文件存在先删除,再创建 |
| ios::binary | 二进制方式 |

以上操作可以利用`|`操作符配合使用,例如`ios::binary |  ios:: out`用二进制方式写文件
```cpp
ofstream ofs;
ofs.open("test.txt", ios::out);

ofs << "姓名:张三" << endl;
ofs << "性别:男" << endl;
ofs << "年龄:18" << endl;

ofs.close();

1.2 读文件

  1. 包含头文件 #include
  2. 创建流对象 ifstream ifs;
  3. 打开文件并判断文件是否打开成功ifs.open(“文件路径”,打开方式);(写文件不用是因为几乎不可能出错,但是读文件可能会出现文件不存在的情况)
  4. 读文件中数据的方式有四种可以选择
  5. 关闭文件ifs.close(); ```cpp //第一种方式 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; }


<a name="mRhzn"></a>
## 2.二进制文件
以二进制的方式对文件进行读写操作,文件打开方式要指定为 ios::binary(这一点与C对二进制文件和文本文件的打开方式一视同仁不同)
<a name="GaJCh"></a>
### 2.1 写文件
二进制方式写文件主要利用流对象调用成员函数`write`<br />函数原型 :`ostream& write(const char * buffer,int len);`<br />参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数
```cpp
//Person类声明
class Person
{
public:
    char m_Name[64];
    int m_Age;
};

//二进制文件  写文件
void test01()
{
    //1、包含头文件(这一步已经在程序顶部实现了)

    //2、创建输出流对象(这种写法可以相当于包含了第三步)
    ofstream ofs("person.txt", ios::out | ios::binary);

    //3、打开文件
    //ofs.open("person.txt", ios::out | ios::binary);

    //实例化对象
    Person p = {"张三"  , 18};

    //4、写文件
    ofs.write((const char *)&p, sizeof(p));
    //所以你到底write了什么???
    //这个有点类似于C中把一个数据块写进文件中,这里把p这个数据块(数据结构)写入了person.txt文件

    //5、关闭文件
    ofs.close();
}

2.2 读文件

二进制方式读文件主要利用流对象调用成员函数read
函数原型:istream& read(char *buffer,int len);
参数解释:字符指针buffer指向内存中一段存储空间。len是读写的字节数

class Person
{
public:
    char m_Name[64];
    int m_Age;
};

void test01()
{
    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;
}
//这个的使用方式也和C几乎是一样的,使用与写方式中相同的数据结构来接收读取得到的数据

CPP高级(四)

CPP除了面向对象编程的思想,还有一种范型编程的思想(主要利用模板技术实现)

1.模板

1.1 模板的概念

 模板就是建立通用的模具,大大提高复用性  (前面已经接触过函数模板,这里再复习一遍)
  • 模板不可以直接使用,它只是一个框架,需要自己添加内容
  • 模板的通用并不是指某个模板是万能的(只是针对某些数据类型通用)
  • 学习模板并不是为了写模板,是为了在STL中能够运用系统提供的模板

1.2 函数模板

函数模板规范的使用形式是将函数模板放在头文件中,在需要使用模板的源文件中添加该头文件即可

//声明函数模板第一种形式
/*本质上第一种和第二种都是
template <typename Anytype>
函数声明/函数定义
*/
template <typename Anytype>
void swap (Anytype &a,Anytype &b);

//定义函数模板第一种形式
template <typename Anytype>
//template --- 声明创建模板
//typename --- 表明其后面的符号是一种数据类型,可以用class代替
//前两个是关键字,最后一个类型名符合变量标准即可(Anytype)
void swap (Anytype &a,Anytype &b)
{
    Anytype temp;
    temp=b;
    b=a;
    a=temp;
}

//声明函数模板第二种形式
template<class T>
T myAdd02(T a, T b);

//定义函数模板第二种形式
template<class T>
T myAdd02(T a, T b)
{
return a + b;
}
//利用模板实现函数主要有两种方式(这两种都属于隐式实例化)
//1、自动类型推导(一般不推荐使用,容易导致独立使用或自动推导出不一致的Anytype类型)
swap(a, b);
//2、显示指定类型
swap<int>(a, b);

(1)普通函数与函数模板区别:

  • 普通函数调用时可以发生自动类型转换(隐式类型转换)
  • 函数模板调用时,如果利用自动类型推导,不会发生隐式类型转换
  • 如果利用显示指定类型的方式,可以发生隐式类型转换

(2)普通函数与函数模板的调用规则:

  • 如果函数模板和普通函数都可以实现,优先调用普通函数
  • 可以通过空模板参数列表来强制调用函数模板
  • 函数模板也可以发生重载(并非要求所有的参数都是模板参数类型,出现内置类型参数int c也是允许的)
  • 如果函数模板可以产生更好的匹配,优先调用函数模板
  • 既然提供了函数模板,最好就不要提供普通函数,否则容易出现二义性
    ```cpp void myPrint(int a, int b) { cout << “调用的普通函数” << endl; }

template void myPrint(T a, T b) { cout << “调用的模板” << endl; }

template void myPrint(T a, T b, int c) { cout << “调用重载的模板” << endl; }

void test01() { //1、如果函数模板和普通函数都可以实现,优先调用普通函数 int a = 10; int b = 20; myPrint(a, b); //调用普通函数

//2、可以通过空模板参数列表来强制调用函数模板 myPrint<>(a, b); //调用函数模板

//3、函数模板也可以发生重载 int c = 30; myPrint(a, b, c); //调用重载的函数模板

//4、 如果函数模板可以产生更好的匹配,优先调用函数模板 char c1 = ‘a’; char c2 = ‘b’; myPrint(c1, c2); //调用函数模板(因为使用的是char类型的参数,针对普通函数的int类型参数明显不匹配) }

<a name="V7VCp"></a>
#### 1.2.1 模板的局限性
前面已经说过模板的通用性并不是万能的,尽管看上去通用,但是类似于最简单的直接赋值操作,针对int或者string类型是可以的,但是无法直接对数组进行直接赋值的操作
```cpp
int a[3];
int b[3];
a=b
/*
两个数组不可以直接赋值。
目的是想将a的所有元素都赋给b,但是b=a的意义是将指针a的值赋给指针b
这明显和目的背道而驰,而且这种操作也是违规操作
可以使用for循环达到目的
*/
针对这种缺点有许多解决方案,下面介绍一种需要掌握的方式——**显式具体化**<br />显式具体化:即我们定义的函数模板对大多数数据类型都适用,但是对某些数据类型如数组、结构体不适用,这时我们需要为数组或者结构体专门定义一个函数而不使用该模板(即为这个数据类型专门写个函数),之后使用数组类型的参数就会调用专用函数而非通用模板函数<br />优先级:**非模板函数(普通函数)>显式具体化>常规模板**
//非模板函数
void Swap(job &a ,job &b);
//显式具体化
template<>
void Swap<job>(job &a,job &b);
//常规模板
template<typename T>
void Swap(T &a,T &b);
//普通函数模板
template<class T>
bool myCompare(T& a, T& b)
{
    if (a == b)
    {
        return true;
    }
    else
    {
        return false;
    }
}

//具体化
template<> 
bool myCompare(Person &p1, Person &p2)
{
    if ( p1.m_Name == p2.m_Name && p1.m_Age == p2.m_Age)
    {
        return true;
    }
    else
    {
        return false;
    }
}
//内置数据类型可以直接使用通用的函数模板
int a = 10;
int b = 20;
bool ret = myCompare(a, b)

//自定义数据类型,不会调用普通的函数模板,使用具体化的模板
Person p1("Tom", 10);
Person p2("Tom", 10);
bool ret = myCompare(p1, p2);

1.2.2 实例化和具体化

函数模板并不等于函数定义,它只是一个用于生成函数定义的方案。使用函数模板为特定类型生成函数定义,称为函数模板实例化。
实例化方式分为隐式实例化和显式实例化。隐式实例化是指代码在编译时根据特定参数类型生成确定的函数定义(这是编译器帮我们完成的工作,模板并不会减少实际的代码量,仅仅只是减少了程序员的工作;在这之前我们使用的都是隐式实例化的方式来调用模板),显式实例化是指在编译之前就生成了函数定义(这样不会影响运行的效率但是增加了编译时间)。
而我们在编写程序时常常把模板的实现和函数的实现分文件编写,使用显式实例化,可以将实现放在cpp文件中,对外隐藏实现(因为CPP是分文件进行编译,如果隐式实例化会在编译时才生成函数定义,这可能导致找不到函数定义)。

template <typename T>
bool compare(T a,T b)
{
    return a > b;
}

//隐式实例化
compare(100,220);//第一种调用方式
compare<int>(120,99);//第二种调用方式
compare<double>(999,100.0);//第三种调用方式
template <typename T>
bool compare(T a,T b)
 {
     return a > b;
 }

 //显式实例化
 template bool compare<int>(int,int);
 template bool compare<float>(float,float);
 template bool compare<double>(double,double);

1.3 类模板

建立一个通用类,类中的成员的数据类型可以不具体制定(但是这个通用类是固定的,比如下面的Person类模板),用一个虚拟的类型(Anytype | T)来代表

template<typename T>
类声明/类定义
//在头文件中实现类声明,在源文件中实现类定义
//类名遵循首字母大写的原则(当然不遵守这个规则也行,作用是帮助区别类)
//类模板
template<class NameType, class AgeType>
class Person
{
public:
    Person(NameType name, AgeType age)
    {
        this->mName = name;
        this->mAge = age;
    }
    void showPerson()
    {
        cout << "name: " << this->mName << " age: " << this->mAge << endl;
    }
public:
    NameType mName;
    AgeType mAge;
};

//类模板实例化对象(这里仍然使用的是隐式实例化)
void test01()
{
    // 指定NameType 为string类型,AgeType 为 int类型
    Person<string, int>P1("孙悟空", 999);
    P1.showPerson();
}

(1)类模板和函数模板的区别

  • 类模板没有自动类型推导的使用方式,类模板使用只能用显示指定类型方式
  • 类模板中的模板参数列表可以有默认参数

(2)类模板中成员函数和普通类中成员函数创建时机的区别

  • 普通类中的成员函数一开始就可以创建
  • 类模板中的成员函数在调用时才创建

(3)当类模板碰到继承时,需要注意一下几点

  • 当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型
  • 如果不指定,编译器无法给子类分配内存
  • 如果想灵活指定出父类中T的类型,子类也需变为类模板
    template<class T>
    class Base
    {
    T m;
    };
    //class Son:public Base //错误,c++编译需要给子类分配内存,必须知道父类中T的类型才可以向下继承
    class Son :public Base<int> //必须指定一个类型
    {
      };
    

1.3.1 类模板实例化

通过类模板实例化出来的对象,作为参数传入函数,一共有三种方式(尽管我也不知道为什么要用对象作为函数参数传入这么无聊,这种情况在生活中不常见吧…)

//类模板
template<class NameType, class AgeType>
class Person
{
public:
    Person(NameType name, AgeType age)
    {
        this->mName = name;
        this->mAge = age;
    }
    void showPerson()
    {
        cout << "name: " << this->mName << " age: " << this->mAge << endl;
    }
public:
    NameType mName;
    AgeType mAge;
};

//1、指定传入的类型(常用)
void printPerson1(Person<string, int> &p)//函数定义
{
    p.showPerson();//调用对象的showPerson方法
}
void test01()
{
    Person <string, int >p("孙悟空", 100);//使用类模板实例化对象
    printPerson1(p);//调用printPerson1函数,将p对象作为参数传入函数
}

//2、参数模板化
template <class T1, class T2>
void printPerson2(Person<T1, T2>&p)
{//这个方法是直接把类模板的参数模板化了
    p.showPerson();
}
void test02()
{
    Person <string, int >p("猪八戒", 90);
    printPerson2(p);
}

//3、整个类模板化
template<class T>
void printPerson3(T & p)
{//现在直接把整个类都模板化了
    p.showPerson();
}
void test03()
{
    Person <string, int >p("唐僧", 30);
    printPerson3(p);
}

1.3.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) {
    this->m_Name = name;
    this->m_Age = age;
}
//成员函数 类外实现
template<class T1, class T2>
void Person<T1, T2>::showPerson() {
    cout << "姓名: " << this->m_Name << " 年龄:" << this->m_Age << endl;
}

2.STL

  • 长久以来,软件界一直希望建立一种可重复利用的东西
  • C++的面向对象泛型编程(泛型编程主要基于模板技术实现)思想,目的就是复用性的提升
  • 大多情况下,数据结构和算法都未能有一套标准,导致被迫从事大量重复工作
  • 为了建立数据结构和算法的一套标准,诞生了STL
  • 详细使用文档见链接:http://c.biancheng.net/stl/stl_basic/

2.1 STL基本概念

  • STL(Standard Template Library,标准模板库)
  • STL 从广义上分为: 1.容器(container) 2.算法(algorithm) 3.迭代器(iterator)
  • 容器和算法之间通过迭代器进行无缝连接。
  • STL 几乎所有的代码都采用了模板类或者模板函数
  • C++ 对模板(Template)支持得很好,STL 就是借助模板把常用的数据结构及其算法都实现了一遍,并且做到了数据结构和算法的分离。例如,vector 的底层为顺序表(数组),list 的底层为双向链表,deque 的底层为循环队列,set 的底层为红黑树,hash_set 的底层为哈希表。

STL六大组件
前面四部分是为了后面两部分服务的
1. 容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
2. 算法:各种常用的算法,如sort、find、copy、for_each等
3. 迭代器:扮演了容器与算法之间的胶合剂。
4. 仿函数:行为类似函数,可作为算法的某种策略。
5. 适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
6. 空间配置器:负责空间的配置与管理。

2.1.1 容器

容器:置物之所也(这里提到的容器,本质上就是封装有数据结构的模板类,例如 list、vector、set、map 等)
STL容器就是将运用最广泛的一些数据结构实现出来,常用的数据结构:数组, 链表,树, 栈, 队列, 集合, 映射表等(这个模板类只有数据结构,没有算法!!!即没有类方法)
这些容器分为序列式容器和关联式容器两种:

  • 序列式容器:强调值的排序,序列式容器中的每个元素均有固定的位置。
  • 关联式容器:二叉树结构,各元素之间没有严格的物理上的顺序关系 | 容器种类 | 功能 | | —- | —- | | 序列容器 | 主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。 | | 排序容器 | 包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。 | | 哈希容器 | C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。 |

2.1.2 算法

算法:问题之解法也
有限的步骤,解决逻辑或数学上的问题,这一门学科我们叫做算法(Algorithms)
算法分为:质变算法和非质变算法。

  • 质变算法:是指运算过程中会更改区间内的元素的内容。例如拷贝,替换,删除等等
  • 非质变算法:是指运算过程中不会更改区间内的元素内容,例如查找、计数、遍历、寻找极值等等

2.1.3 迭代器

迭代器:容器和算法之间的中介
迭代器提供了一种方法,使之能够依序寻访某个容器所含的各个元素(简单来说就是遍历容器中存储的元素,在使用中熟练其语法),而又无需暴露该容器的内部表示方式,从而以统一的界面向算法传送数据。
每个容器都有自己专属的迭代器,不同容器的迭代器也不同,其功能强弱也有所不同。容器的迭代器的功能强弱,决定了该容器是否支持 STL 中的某种算法(迭代器决定了容器能够使用的算法)。
迭代器使用非常类似于指针,初学阶段我们可以先理解迭代器为指针(其实理解为指针其前向、双向也不好类比)
常用的容器中迭代器种类为前向迭代器、双向迭代器和随机访问迭代器(迭代器概念源自于 C/C++ 中原生指针的一般化推广)
image.png
(1) 前向迭代器(forward iterator)
假设 p 是一个前向迭代器,则 p 支持 ++p,p++,*p 操作,还可以被复制或赋值,可以用 == 和 != 运算符进行比较。此外,两个正向迭代器可以互相赋值。
(2) 双向迭代器(bidirectional iterator)
双向迭代器具有正向迭代器的全部功能,除此之外,假设 p 是一个双向迭代器,则还可以进行 --p 或者 p-- 操作(即一次向后移动一个位置)。
(3) 随机访问迭代器(random access iterator)
随机访问迭代器具有双向迭代器的全部功能。除此之外,假设 p 是一个随机访问迭代器,i 是一个整型变量或常量,则 p 还支持以下操作:

  • p+=i:使得 p 往后移动 i 个元素。
  • p-=i:使得 p 往前移动 i 个元素。
  • p+i:返回 p 后面第 i 个元素的迭代器。
  • p-i:返回 p 前面第 i 个元素的迭代器。
  • p[i]:返回 p 后面第 i 个元素的引用

image.png
尽管不同容器对应着不同类别的迭代器,但这些迭代器有着较为统一的定义方式(注意是迭代器的定义方式!而不能决定迭代器的类型(迭代器的类型是容器已经规定好了的),容器通过这些方式/某些方式可以定义属于自己的迭代器。通过不同的方式定义的容器的迭代器会有不同的附加属性,当然容器的迭代器本身因为属于不同种类所以也会自带属性;而实际上容器也包含了一些与迭代器相关的成员函数,因此可以不用手动定义迭代器而使用某个成员函数的返回值作为迭代器使用
image.png
通过以上几种方式定义的迭代器,就可以读取它指向的元素,*迭代器名就表示迭代器指向的元素(这一点倒是和指针很相似)

  • 对正向迭代器进行 ++ 操作时,迭代器会指向容器中的后一个元素;
  • 对反向迭代器进行 ++ 操作时,迭代器会指向容器中的前一个元素;
  • 通过非常量迭代器能修改其指向的元素;
  • 以上 4 种定义迭代器的方式,并不是每个容器都适用。有一部分容器同时支持以上 4 种方式,比如 array、deque、vector;而有些容器只支持其中部分的定义方式,例如 forward_list 容器只支持定义正向迭代器,不支持定义反向迭代器

    2.2 STL序列式容器

    所谓STL序列式容器,即以线性排列来存储某一指定类型(例如 int、double 等)的数据,其共同的特点是不会对存储的元素进行排序,元素排列的顺序取决于存储它们的顺序
    需要注意的是,序列容器只是一类容器的统称,并不指具体的某个容器。主要包含以下几类容器:
  1. array(数组容器):表示可以存储 N 个 T 类型的元素,是 C++ 本身提供的一种容器。此类容器一旦建立,其长度就是固定不变的,这意味着不能增加或删除元素,只能改变某个元素的值;
  2. vector(向量容器):用来存放 T 类型的元素,是一个长度可变的序列容器,即在存储空间不足时,会自动申请更多的内存。使用此容器,在尾部增加或删除元素的效率最高(时间复杂度为 O(1) 常数阶),在其它位置插入或删除元素效率较差(时间复杂度为 O(n) 线性阶,其中 n 为容器中元素的个数);
  3. deque(双端队列容器):和 vector 非常相似,区别在于使用该容器不仅尾部插入和删除元素高效,在头部插入或删除元素也同样高效,时间复杂度都是 O(1) 常数阶,但是在容器中某一位置处插入或删除元素,时间复杂度为 O(n) 线性阶;

image.png

  1. list(链表容器):是一个长度可变的、由 T 类型元素组成的序列,它以双向链表的形式组织元素,在这个序列的任何地方都可以高效地增加或删除元素(时间复杂度都为常数阶 O(1)),但访问容器中任意元素的速度要比前三种容器慢,这是因为 list 必须从第一个元素或最后一个元素开始访问(这是链表的通病),需要沿着链表移动,直到到达想要的元素。
  2. forward_list(正向链表容器):和 list 容器非常类似,只不过它以单链表的形式组织元素,它内部的元素只能从第一个元素开始访问,是一类比链表容器快、更节省内存的容器。

image.png

2.2.1 常见函数成员

序列容器包含一些相同的成员函数(注意成员函数并不等于算法,算法中的泛型算法函数并不知道每种容器中的内部工作原理,其操作可能并不能对每种容器都适应;在成员函数和算法具有相似功能时我们优先选择成员函数,因为成员函数与容器结合的更好——尽量用成员函数代替同名的算法),它们的功能也相同;
list 和 forward_list 容器彼此非常相似,forward_list 中包含了 list 的大部分成员函数,而未包含那些需要反向遍历的函数
C++ 11 标准库(注意不是标准模板库,标准库由C库、C++库和STL库三组库构成)还新增加了 begin() 和 end() 这 2 个函数,和 array 容器包含的 begin() 和 end() 成员函数不同的是,标准库提供的这 2 个函数的操作对象,既可以是容器,还可以是普通数组。当操作对象是容器时,它和容器包含的 begin() 和 end() 成员函数的功能完全相同;如果操作对象是普通数组,则 begin() 函数返回的是指向数组第一个元素的指针,同样 end() 返回指向数组中最后一个元素之后一个位置的指针(注意不是最后一个元素)
表1 array、vector 和 deque 容器的函数成员

函数成员 函数功能 array vector deque
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器,通常和 begin() 结合使用。
rbegin() 返回指向最后一个元素的迭代器。
rend() 返回指向第一个元素所在位置前一个位置的迭代器。
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
assign() 用新元素替换原有内容。 -
operator=() 复制同类型容器的元素,或者用初始化列表替换现有内容。
size() 返回实际元素个数。
max_size() 返回元素个数的最大值。这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
capacity() 返回当前容量。 - -
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
resize() 改变实际元素的个数。 -
shrink _to_fit() 将内存减少到等于当前元素实际所使用的大小。 -
front() 返回第一个元素的引用。
back() 返回最后一个元素的引用。
operator 使用索引访问元素。
at() 使用经过边界检査的索引访问元素。
push_back() 在序列的尾部添加一个元素。 -
insert() 在指定的位置插入一个或多个元素。 -
emplace() 在指定的位置直接生成一个元素。 -
emplace_back() 在序列尾部生成一个元素。 -
pop_back() 移出序列尾部的元素。 -
erase() 移出一个元素或一段元素。 -
clear() 移出所有的元素,容器大小变为 0。 -
swap() 交换两个容器的所有元素。
data() 返回指向容器中第一个元素的指针。 -

表2 list 和 forward_list 的函数成员

函数成员 函数功能 list forward_list
begin() 返回指向容器中第一个元素的迭代器。
end() 返回指向容器最后一个元素所在位置后一个位置的迭代器。
rbegin() 返回指向最后一个元素的迭代器。 -
rend() 返回指向第一个元素所在位置前一个位置的迭代器。 -
cbegin() 和 begin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
before_begin() 返回指向第一个元素前一个位置的迭代器。 -
cbefore_begin() 和 before_begin() 功能相同,只不过在其基础上,增加了 const 属性,即不能用该指针修改元素的值。 -
cend() 和 end() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 -
crend() 和 rend() 功能相同,只不过在其基础上,增加了 const 属性,不能用于修改元素。 -
assign() 用新元素替换原有内容。
operator=() 复制同类型容器的元素,或者用初始化列表替换现有内容。
size() 返回实际元素个数。 -
max_size() 返回元素个数的最大值,这通常是一个很大的值,一般是 232-1,所以我们很少会用到这个函数。
resize() 改变实际元素的个数。
empty() 判断容器中是否有元素,若无元素,则返回 true;反之,返回 false。
front() 返回容器中第一个元素的引用。
back() 返回容器中最后一个元素的引用。 -
push_back() 在序列的尾部添加一个元素。 -
push_front() 在序列的起始位置添加一个元素。
emplace() 在指定位置直接生成一个元素。 -
emplace_after() 在指定位置的后面直接生成一个元素。 -
emplace_back() 在序列尾部生成一个元素。 -
cmplacc_front() 在序列的起始位生成一个元索。
insert() 在指定的位置插入一个或多个元素。 -
insert_after() 在指定位置的后面插入一个或多个元素。 -
pop_back() 移除序列尾部的元素。 -
pop_front() 移除序列头部的元素。
reverse() 反转容器中某一段的元素。
erase() 移除指定位置的一个元素或一段元素。 -
erase_after() 移除指定位置后面的一个元素或一段元素。 -
remove() 移除所有和参数匹配的元素。
remove_if() 移除满足一元函数条件的所有元素。
unique() 移除所有连续重复的元素。
clear() 移除所有的元素,容器大小变为 0。
swap() 交换两个容器的所有元素。
sort() 对元素进行排序。
merge() 合并两个有序容器。
splice() 移动指定位置前面的所有元素到另一个同类型的 list 中。 -
splice_after() 移动指定位置后面的所有元素到另一个同类型的 list 中。 -

2.2.2 STL array容器

array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。在使用上,它比普通数组更安全(原因后续会讲),且效率并没有因此变差。
和其它容器不同,array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法借由增加或移除元素而改变其大小,它只允许访问或者替换存储的元素
在使用该容器之前,代码中需引入 头文件,并默认使用 std 命令空间

#include <array>
using namespace std;

(1)array容器的创建
//1.
std::array<double, 10> values;
//创建具有 10 个 double 类型元素的 array 容器,array 容器不会做默认初始化操作故各个元素的值不确定

//2.
std::array<double, 10> values {};
//使用该语句,容器中所有的元素都会被初始化为 0.0

//3.
std::array<double, 10> values {0.5,1.0,1.5,,2.0};
//这里只初始化了前 4 个元素,剩余的元素都会被初始化为 0.0

(2)array随机访问迭代器

STL 为 array 容器配备了随机访问迭代器,该类迭代器是功能最强大的迭代器(注意,下面成员函数都是与随机访问迭代器相关的,而其出现的正向迭代器等是随机访问迭代器的附加性质)
在arry 容器模板类中几个成员函数与迭代器相关,begin()end()成员函数返回的都是正向迭代器,它们分别指向「首元素」和「尾元素+1」 的位置。在实际使用时,我们可以利用它们实现初始化容器或者遍历容器中元素的操作;cbegin()cend()成员函数,它们和 begin()/end() 唯一不同的是,前两者返回的是 const 类型的正向迭代器,这就意味着,有 cbegin()cend()成员函数返回的迭代器,可以用来遍历容器内的元素,也可以访问元素,但是不能对所存储的元素进行修改

  • 需要注意的是,STL 标准库,不是只有 array 容器,当迭代器指向容器中的一个特定元素时,它们不会保留任何关于容器本身的信息,所以我们无法从迭代器中判断,它是指向 array 容器还是指向 vector 容器

(3)array访问元素

(3.1)访问array容器中单个元素
注意:列出的方式仅仅只是一些而非全部,理论上可以使用迭代器的组合或者成员函数、模板函数、非成员函数等创造出无数种遍历容器的方法

//1.可以通过容器名[]的方式直接访问和使用容器中的元素,这和 C++ 标准数组访问元素的方式相同
values[4] = values[3] + 2.O*values[1];
//使用如上这样方式,由于没有做任何边界检查,所以即便使用越界的索引值去访问或存储元素,也不会被检测到

//2.为了能够有效地避免越界访问的情况,可以使用 array 容器提供的 at() 成员函数
values.at (4) = values.at(3) + 2.O*values.at(1);
//当传给 at() 的索引是一个越界值时,程序会抛出 std::out_of_range 异常

//3.get<n> ()模板函数(模板函数的重点是函数。表示的是由一个函数模板生成而来的函数),能够获取到容器的第 n 个元素
//参数n的实参必须是一个在编译时可以确定的常量表达式,所以它不能是一个循环变量
array<string, 5> words{ "one","two","three","four","five" };
cout << get<3>(words) << endl; //four

//4.data()成员函数,调用该函数可以得到指向容器首个元素的指针
array<int, 5> words{1,2,3,4,5};
cout << *( words.data()+1);//2

(3.2)访问array容器中多个元素

//1.
for(size_t i = 0 ; i < values.size() ; ++i){
    cout<<values[i];
}
//size() 函数能够返回容器中元素的个数(函数返回值为 size_t 类型)

2.2.3 STL vector容器

vector 常被称为向量容器,因为该容器擅长在尾部插入或删除元素,在常量时间内就可以完成,时间复杂度为O(1);而对于在容器头部或者中部插入或删除元素,则花费时间要长一些(移动元素需要耗费时间),时间复杂度为线性阶O(n)

#include <vector>
using namespace std;

(1)vector容器的创建
//1.创建存储 double 类型元素的一个 vector 容器
std::vector<double> values;
/*这是一个空的 vector 容器,因为容器中没有元素,所以没有为其分配空间。
当添加第一个元素(比如使用 push_back() 函数)时,vector 会自动分配内存*/

//2.除了创建空 vector 容器外,还可以在创建的同时指定初始值以及元素个数
std::vector<int> primes {2, 3, 5, 7, 11, 13, 17, 19};
//这样就创建了一个含有 8 个素数的 vector 容器


//3.在创建 vector 容器时,也可以指定元素个数
std::vector<double> values(20);//圆括号表示元素个数,花括号表示元素值
//values 容器开始时就有 20 个元素,它们的默认初始值都为 0

//4.存储元素类型相同的其它 vector 容器
std::vector<char>value1(5, 'c');
std::vector<char>value2(value1);    
//value2 容器中也具有 5 个字符 'c'

(2)vector随机访问迭代器

表 1 vector 容器支持迭代器的成员函数

成员函数 功能
begin() 返回指向容器中第一个元素的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。
end() 返回指向容器最后一个元素之后一个位置的正向迭代器;如果是 const 类型容器,在该函数返回的是常量正向迭代器。此函数通常和 begin() 搭配使用。
rbegin() 返回指向最后一个元素的反向迭代器;如果是 const 类型容器,在该函数返回的是常量反向迭代器。
rend() 返回指向第一个元素之前一个位置的反向迭代器。如果是 const 类型容器,在该函数返回的是常量反向迭代器。此函数通常和 rbegin() 搭配使用。
cbegin() 和 begin() 功能类似,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
cend() 和 end() 功能相同,只不过其返回的迭代器类型为常量正向迭代器,不能用于修改元素。
crbegin() 和 rbegin() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。
crend() 和 rend() 功能相同,只不过其返回的迭代器类型为常量反向迭代器,不能用于修改元素。

image.png
迭代器基本用法:

#include <iostream>
//需要引入 vector 头文件
#include <vector>
using namespace std;
int main()
{
    vector<int>values{1,2,3,4,5};
    auto first = values.begin();//first迭代器
    auto end = values.end();//end迭代器
    while (first != end)
    {
        cout << *first << " ";
        ++first;
    }
    return 0;
}
//输出结果为1 2 3 4 5

注意:

  • 在初始化空的 vector 容器时,不能使用迭代器
  • vector 容器在申请更多内存的同时,容器中的所有元素可能会被复制或移动到新的内存地址,这会导致之前创建的迭代器失效(values 容器在增加容量之后,首个元素的存储地址发生了改变,此时再使用先前创建的迭代器,显然是错误的)

(3)vector访问元素
//1.像普通数组一样访问存储的元素,甚至对指定下标处的元素进行修改
cout << values[0] << endl;//获取容器中首个元素
values[0] = values[1] + values[2] + values[3] + values[4];//修改容器中下标为 0 的元素的值

//2.vector 容器也提供了 at() 成员函数,尽管vector可以动态调整内存,但是也存在数组边界!
cout << values.at(0) << endl;

//3.front() 和 back(),它们分别返回 vector 容器中第一个和最后一个元素的引用
cout << "values 首元素为:" << values.front() << endl;
cout << "values 尾元素为:" << values.back() << endl;

//4. data() 成员函数,该函数的功能是返回指向容器中首个元素的指针
cout << *(values.data() + 2) << endl;//输出容器中第 3 个元素的值

注意:size就是vector中元素的个数,而capacity就是vector申请的内存空间的大小(数组的容量必须大于0;vector允许容量为0)

//1.size() 成员函数,该函数可以返回 vector 容器中实际存储的元素个数
for (int i = 0; i < values.size(); i++) {//从下标 0 一直遍历到 size()-1 处
    cout << values[i] << " ";
}
/*这里不要使用 capacity() 成员函数,因为它返回的是 vector 容器的容量,
而不是实际存储元素的个数,这两者是有差别的*/

//2.使用 vector 迭代器遍历 vector 容器,这里以 begin()/end() 为例
for (auto first = values.begin(); first < values.end(); ++first) {
    cout << *first << " ";
}

(4)vector添加元素

向 vector 容器中添加元素的唯一方式就是使用它的成员函数,如果不调用成员函数,非成员函数既不能添加也不能删除元素。这意味着,vector 容器对象必须通过它所允许的函数去访问,迭代器显然不行

(4.1)push_back()
该成员函数的功能是在 vector 容器尾部添加一个元素

vector<int> values{};
values.push_back(1);
values.push_back(2);
for (int i = 0; i < values.size(); i++) {
    cout << values[i] << " ";//1 2
}

(4.2)emplace_back()
该函数是 C++ 11 新增加的,其功能和 push_back()相同,都是在 vector 容器的尾部添加一个元素

emplace_back()和push_back()的区别

  • push_back()向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素)
  • emplace_back()在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程
  • emplace_back()的执行效率比 push_back()高。因此,在实际使用时,建议大家优先选用 emplace_back()
  • 由于 emplace_back() 是 C++ 11 标准新增加的,如果程序要兼顾之前的版本,还是应该使用 push_back()

(5)vector插入元素

(5.1)insert()
insert()函数的功能是在 vector 容器的指定位置插入一个或多个元素
image.png

std::vector<int> demo{1,2};
    //第一种格式用法
    demo.insert(demo.begin() + 1, 3);//{1,3,2}
    //第二种格式用法
    demo.insert(demo.end(), 2, 5);//{1,3,2,5,5}
    //第三种格式用法
    std::array<int,3>test{ 7,8,9 };
    demo.insert(demo.end(), test.begin(), test.end());//{1,3,2,5,5,7,8,9}
    //第四种格式用法
    demo.insert(demo.end(), { 10,11 });//{1,3,2,5,5,7,8,9,10,11}

(5.2)emplace()
emplace() 是 C++ 11 标准新增加的成员函数,用于在 vector 容器指定位置之前插入一个新的元素

iterator emplace (const_iterator pos, args...);
/*pos 为指定插入位置的迭代器;args... 表示与新插入元素的构造函数相对应的多个参数;
该函数会返回表示新插入元素位置的迭代器*/
std::vector<int> demo1{1,2};
//emplace() 每次只能插入一个 int 类型元素
demo1.emplace(demo1.begin(), 3);//3 1 2

注意:

  • emplace()insert() 都能完成向 vector 容器中插入新元素,那么谁的运行效率更高呢?答案是 emplace()
  • emplace()在插入元素时,是在容器的指定位置直接构造元素,而不是先单独生成,再将其复制(或移动)到容器中。因此,在实际使用中,推荐大家优先使用 emplace()

(6)vector删除元素

无论是向现有 vector 容器中访问元素、添加元素还是插入元素,都只能借助 vector 模板类提供的成员函数,但删除 vector 容器的元素例外,完成此操作除了可以借助本身提供的成员函数,还可以借助一些全局函数
vector提供了可以动态插入和删除元素的机制,而array和数组则无法做到,或者说array和数组需要完成该功能则需要自己实现完成(理论上普通数组和array容器只能覆盖而不能删除元素)
image.png

//1.pop_back() 成员函数的用法非常简单
vector<int>demo{ 1,2,3,4,5 };
demo.pop_back();//删除了最后一个元素5
//输出 dmeo 容器新的size 容器的大小减了 1
cout << "size is :" << demo.size() << endl;
//输出 demo 容器新的容量 容量没变
cout << "capacity is :" << demo.capacity() << endl;

//2.删除 vector 容器中指定位置处的元素,可以使用 erase() 成员函数
vector<int>demo{ 1,2,3,4,5 };
auto iter = demo.erase(demo.begin() + 1);//删除元素 2
//1.删除容器中某个指定区域内的所有元素 使用 erase() 成员函数实现
std::vector<int> demo{ 1,2,3,4,5 };
auto iter = demo.erase(demo.begin()+1, demo.end() - 2);//删除 2、3
//1.remove()函数
vector<int>demo{ 1,3,3,4,3,5 };
auto iter = std::remove(demo.begin(), demo.end(), 3);
/*
remove() 函数删除掉 demo 容器中的多个指定元素,该容器的大小和容量都没有改变,
其剩余位置还保留了之前存储的元素(这也就是我们对普通数组的删除操作的原理)
可以使用 erase() 成员函数删掉这些 "无用" 的元素
*/
//1.使用 clear() 成员函数
vector<int>demo{ 1,3,3,4,3,5 };
demo.clear();