C++学习课件(三)

一、宏

宏替换是C/C++系列语言的技术特色,C/C++语言提供了强大的宏替换功能,源代码在进入编译器之前,要先经过一个称为“预处理器”的模块,这个模块将宏根据编译参数和实际编码进行展开,展开后的代码才正式进入编译器,进行词法分析、语法分析等等

1. 宏变量

宏变量和const 修饰的在定义语义上没有什么不同,都是可以用来定义常量,但在与const的定义进行对比时,没有任何优势可言,所以建议使用const来定义常量。

#define MAX 30

int main(){
	int scores[MAX]; //表示一个班30个人的成绩数组。

	return 0 ;
}

2. 条件宏

条件宏最常出现在头文件中,用于防止头文件被反复包含。

  • 头文件的条件宏
#ifndef STUDENT_H
#define STUDENT_H
……
……
#endif
  • 用于判断编译的条件宏

通过DEBUG宏,我们可以在代码调试的过程中输出辅助调试的信息。当DEBUG宏被删除时,这些输出的语句就不会被编译。更重要的是,这个宏可以通过编译参数来定义。因此通过改变编译参数,就可以方便的添加和取消这个宏的定义,从而改变代码条件编译的结果.

#define DEBUG
#define REALEASE

#include<iostream>

using namespace std;

int main() {
    #ifdef DEBUG
        cout <<"debug模式下打印" << endl;
    #elif REALEASE
        cout <<"release模式下打印" << endl;
    #else
        cout <<"普通模式下打印" << endl;
    #endif
    
    //下面可言继续编写原有的逻辑
    cout << "继续执行逻辑代码~~~"<<endl;
    
    return 0 ;
}

二、枚举

在C++里面定义常量,可以使用 #define和const创建常量, 除此之外,还提供了枚举这种方式,它除了能定义常量之外,还表示了一种新的类型,但是必须按照一定的规则来定义。在枚举中定义的成员可以称之为 枚举量 ,每个枚举量都能对应一个数字,默认是他们的出现顺序,从0开始。

C++的枚举定义有两种方式,限定作用域不限定作用域 ,根据方式的不同,定义的结构也不同。

1. 两种定义方式

1. 限定作用域

使用 enum class 或者 enum struct 关键字定义枚举,枚举值位于 enum 的局部作用域内,枚举值不会隐式的转化成其他类型

enum class Week{MON,TUS,WEN,THU,FRI,STU,SUN};


int main(){

	int val =int)Week::TUS ; //打印会是1 
    
	return 0 ;
}

2. 不限定作用域

使用 enum关键字定义,省略 class | struct, 枚举值与枚举类型位于同一个作用域,枚举值会隐式的转化成整数, 默认是从0开始,依次类推。 不允许有重复枚举值,因为他们属于同一个作用域。

enum traffic_light{red, yellow , green};

//匿名的未限定作用域
enum{red, yellow , green}; //重复定义会报错,因为red\yellow\green 已经定义过了。

//手动给枚举量 设置对应的数值
enum{red = 10, yellow =20 , green =30};

int main(){
	
    //使用 域操作符来获取对应的枚举量
	int a=  traffic_light::red;
	int b = ::red; 

	return 0 ;
}

2. 枚举的使用

枚举的目的:增加程序的可读性。枚举类型最常见也最有意义的用处之一就是用来描述状态量。

固定一个名字, 固定的几个名字。不允许程序员在外来的编码中修改名称。

性别: 男 、 女 nan nv man women male female 1 0

#include <iostream>
#include <vector>
#include <string>

using namespace std;

enum Gender{MALE , FEMALE};

class teacher{

public:
    string name;
    Gender gender;

    teacher(string name , Gender gender):name(name),gender(gender){}
};

int main(){

    teacher t1("张三" ,  Gender::MALE);
    teacher t2("李丽丽" ,  Gender::FEMALE);
    teacher t3("李四" ,  Gender::MALE);

    vector<teacher> v;
    v.push_back(t1);
    v.push_back(t2);
    v.push_back(t3);

    for(teacher t : v){
        switch (t.gender){
            case Gender::MALE: //男
                cout <<"男老师" << endl;
                break;
            case Gender::FEMALE:
                cout <<"女老师" << endl;
                break;
            default:
                cout <<"性别错误" << endl;
                break;
        }
    }
    return 0 ;
}

练习:

​ 一个容器里面有若干个学生 ,现在需要统计一下里面的男生有多少个,女生有多少个。 请使用枚举来定义性别。

三、异常处理

1. 异常处理

异常时指存在于运行时的反常行为,这些行为超出了函数的正常功能的范围。和python一样,c++ 也有自己的异常处理机制。 在c++中,异常的处理包括 throw表达式try 语句块 以及 异常类 。如果函数无法处理某个问题,则抛出异常,并且希望函数的调用者能够处理,否则继续向上抛出。如果希望处理异常,则可以选择捕获。

void test(){
    try{
        autoresult = do_something();
    }catch(Some_error){
        //处理该异常
    }
}

int dosomething(){
    
    if(条件满足){
        return result;
    }else{
        throw Some_error(); //抛出异常
    }
}

2. 不使用异常机制

  • 终止程序

可以使用 abort | exit 来终止程序的执行

int getDiv( int a , int b){
    if(int b == 0 ){
    	abort(); // 或者是 exit(4) //括号内为错误的代码,可以使用常量定义
	}
    return a / b;
}
  • 显示错误代码

与直接终止程序的突兀对比,错误代码显得更为友好些,同时也可以根据错误代码给出相应的提示。

int getDiv( int a , int b){
    if(int b == 0 ){
    	//abort(); // 或者是 exit(4) //括号内为错误的代码,可以使用常量定义
        return -1000001;// 外部可以对此代码进行处理
	}
    return a / b;
}

3. 使用异常机制

1. 捕获异常

若程序想对异常进行处理,以达到让程序继续友好的执行下去,可以使用捕获异常。 exception 是所有异常的父类 , runtime_error 可以作为运行时出现的异常捕获。一旦发生异常,那么后面的语句将不会执行。

一般捕获异常,采用try{} catch(){}的结构来捕获异常,catch可以有多个。可以使用catch(...) 来表示捕获所有异常 , 但它必须出现在所有catch的最后。

try{
    //执行的代码逻辑
}catch(runtime_error err ){ //捕获的异常类型。
    //捕获到异常,执行的逻辑
    cout << err.what() << endl; //打印错误信息
}

2. 抛出异常

函数内部如果不想处理异常,可以选择抛出异常(throw) , 进而由调用的它函数处理,若该函数仍未处理,则继续往上抛出。注意: 若抛出的语句位于try语句块内,则优先匹配try 语句匹配的异常,若没有匹配上,才选择向上抛出。 throw可以抛出任意类型的异常,要求是这些类型必须是这些类的对象可复制和移动。同样抛出异常时,后面的语句不会执行。

int calcDiv(int a, int b){

    if(b == 0){
        throw  runtime_error("除数不能为0 ");
    }
    return a / b;
}

3. noexcept

如果预先知道某个函数不会抛出异常,那么可以在函数定义的参数列表后面跟上关键字 noexcept , 通常会存在于移动构造函数 和 移动赋值函数中。即便某个函数声明不会抛出异常,但是在内部真的抛出异常,编译器仍然允许编译通过,但是在运行时为了确保不抛出异常的承诺,会调用terminate 终止程序的执行,甚至不会向上传递异常。

stu(stu && s) noexcept { //移动赋值函数
}

void operator=(stu && s) noexcept{ //表示不会抛出异常。
    
}

四、I/O操作

在针对I/O操作时,标准库提供一组操作符(manipulator),允许程序堆输出的内容进行格式化,比如:输出数字的十六进制、浮点值的精度等。类似以前的 endl 就是一个操作符,但它并不是一个普通的值,是用于输出一个换行符并且兼具刷新缓冲区的功能。

1. 基本输入输出

1. 输出布尔数据

在c/c++中,在对bool类型的数据做输出的时候,打印的是 0 、1 ,如果希望看到的是 true 和 false ,那么可以使用 boolalpha 操作符。

#include <iostream>

using namespace std;

int main (){

	bool flag = false;
    cout << "flag的值是:" << flag << endl; // 打印 0

    //操作符只会影响后续的输出  打印 0  false
    cout << "flag的值是:" << flag  <<" 添加操作符后:"<<boolalpha << flag << endl;

	return 0 ;
}

2. 输出整形数字

在输出数字时,可以选择使用十进制八进制十六进制 输出 ,它们只会影响整形数字, 默认会采用十进制输出数字

#include <iostream>
using namespace std;
int main (){

	cout <<"十进制:" << dec  <<9 << endl;  // 9
    cout <<"八进制:" << oct  <<9 << endl;  // 10 
    cout <<"十六进制:" << hex <<10 << endl; // a

    //若想在输出前面表示打印的数值显示前缀进制标识,可以使用showbase关键字
    cout <<showbase;
    //默认即采用十进制输出
    cout <<"十进制:" << dec  <<9 << endl; 9 
    cout <<"八进制:" << oct  <<9 << endl; 011
    cout <<"十六进制:" << hex <<10 << endl;  //0xa
    cout<<noshowbase;

	return 0 ;
}

3. 输出浮点数

c++ 对浮点数的输出默认只会输出六位 ,那么在应对较多浮点数的时候,则常常会丢失精度,导致后面的小数位无法输出。标准库也提供了针对浮点数的输出格式cout.precision() | setprecision(),允许指定以多高的精度输出浮点数。

#include<iostream>
#include <iomanip>

using namespace std;

int main(){

    double a = 10.12345678901234;
    cout.precision(3); //  设置输出多少位数字 ,该函数有返回值,为设置的精度值。
    cout  <<" a ="<<a << endl;  // 10.1 

    //或者使用setprecision() ,不过需要导入 #include <iomanip>
    //做一个标记位的设置,所以还是要连上  << 
    cout  << setprecision(5)<<" a ="<<a << endl;  // 10.123

    return 0 ;
}

小数点后面都是0,默认是不会被输出的,若想强制输出,可以使用showpoint关键字,配合precision 可以精确打印

#include<iostream>
using namespace std;

int main(){

	float  f =10.00;
	cout.precision(4);
	cout <<showpoint <<"f="<< f <<noshowpoint<< endl;  //10.00

	return 0 ;
}

4. 禁止忽略空白符号

默认情况下,获取键盘的输入内容时会自动忽略空白符(空格符 、制表符、换行符、回车符) , 若不想忽略可以使用 noskipws 禁止忽略空白符, 使用 skipws 还原忽略即可。

#include<iostream>

using namespace std;

int main(){

	 cin>>noskipws;
     char c ;
     cout <<"请输入字符:" << endl;
     while(cin >> c){
         cout << c ;
     }
     cin >> skipws;

	return 0 ;
}

2. string 流

c++中的I/O 操作,有提供专门三个针对字符串操作的流对象 ,它们定义在 #include < sstream> 头文件中

istringtream :从string读取数据

ostringstream :向string写入数据

stringstream : 既能读取数据,也能写入数据

1. istringstream

istringstream 是以空格为分隔符,将字符串从字符串流中在依次拿出,比较好的是它不会将空格作为流。这样就实现了字符串的空格切割。

#include<sstream>

using namespace std;

int main(){
    string s = "我是黑马程序员 我爱黑马程序员";
    istringstream stream(str); //或者使用  stream.str(s);
    string s;
    while(stream>>s) { // 抽取stream中的值到s
        cout<<s<<endl;      //依次输出s
    }
    return 0 ;
}

2. ostringstream

我们需要格式化一个字符串,但通常并不知道需要多大的缓冲区。为了保险常常申请大量的缓冲区以防止缓冲区过小造成字符串无法全部存储。这时我们可以考虑使用ostringstream类,该类能够根据内容自动分配内存,并且其对内存的管理也是相当的到位。

#include<sstream>
#include <string>

using namespace std;

int  main(){

     int a = 55;
     double b = 65.123;
     string str = "";

     //头文件是sstream
     ostringstream oss;
     oss << a << "---" << b;

     str = oss.str();
     cout << str << endl;
     return 0;
}

3. 文件操作

c++的文件操作和Python的文件操作有许多相似之处,其实不止它们两,大多数编程语言在对待文件处理上都大同小异。 在Python中针对文件的操作,使用了几个函数即可 openreadwrite ,而在C++中处理文件操作的有三个主要对象 istreamostreamfstream 。 需要添加头文件 #include<fstream>

  • 文件操作常用类

到底是输入还是输出,是站在程序的角度看就ok.

数据类型描述
ofstream表示输出文件流,用于创建文件并向文件写入信息。
ifstream表示输入文件流,用于从文件读取信息。
fstream示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,也可以从文件读取信息。
  • 文件操作模式
模式标志描述
ios::app追加模式。所有写入都追加到文件末尾。
ios::ate文件打开后定位到文件末尾。
ios::in打开文件用于读取。
ios::out打开文件用于写入。
ios::trunc如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。

1. 读取文件

标准库中提供了针对文件读写操作的类,可以对文件的每行进行读取,由于后续操作矩阵数据的机会更多,所以此处就以文件存储的是矩阵数据为例。

a. 简单示例

文件仅仅是一些简单的字符串。

#include <iostream>
#include <string>
#include <fstream>

using namespace std; 

int main(){

    //构建文件对象, ../表示上一级目录,因为执行的时候是exe文件,它位于debug目录,并不和
    //test.txt 处于同级目录, in表示仅仅是读取文件内容 
    //ios:: 表示域操作符,表示使用ios这个对象里面的in静态常量
    fstream file{"../test.txt" , ios::in};

    //文件是否能正常打开
    if(file.is_open()){
        string line ;

        //getline 用于读取文件的整行内容,直到碰到文件末尾或者超过字符串可存储的最大值才会返回
        while (getline(file , line)){
            cout << line << endl;
        }
        //关闭文件
        file.close();
    }else{
        cout << "文件无法正常打开! "<<endl;
    }
	return 0 ;
}

b. 二维vector操作示例

文件中存放如下所示的 3 * 4 的矩阵数据,需要读取出来,然后使用vector来存储。

1, 6, 2, 10.5
11, 15.2, 2, 21
3, 9, 1, 7.5

  • 示例代码
#include <iostream>
#include <string>
#include <fstream>

using namespace std; 

int main(){

        fstream matrixFile{"../matrix.txt" , ios_base::in};

        string line;

        if(matrixFile.is_open()){
          //循环读取每一行,直到文末

          //针对每一行操作的对象,用于字符串切割、转化
          stringstream ss ;

          //用于接收每一个数字
          float number ;

          //整个矩阵数据
          vector <vector<float>> matrixVector;


          //存储每一行的矩阵
          vector <float> rowVector;


          while(getline(matrixFile , line)){
              cout << "line = " << line << endl;


              //每次开始前都清空行vector的数据
              rowVector.clear();

              //每次包装前,都清空内部数据
              ss.clear();

              //包装line数据,以便一会进行切割,
              ss.str(line);


              //循环进行转化。
              while(ss >> number){

                  //获取下一个字符,如果是逗号或者空白 则忽略它,丢弃它。
                  //然后while循环继续执行,获取下一个数字
                  if(ss.peek() == ',' || ss.peek() == ' '){
                      ss.ignore();
                  }
                  //往行里面追加数据
                  rowVector.push_back(number);
              }
              //每一行填充完毕之后,再存储到大的vector中
              matrixVector.push_back(rowVector);

          }

          //关闭文件
          matrixFile.close();


          //最后在这里,遍历打印二维vector即可。

            for (int i = 0; i <matrixVector.size() ; ++i) {

                for (int j = 0; j <matrixVector[i].size() ; ++j) {
                    cout << matrixVector[i][j] << " ";
                }

                cout << endl;

            }
      }else{
          cout << "文件打开失败"<<endl;
      }
    return 0 ;
}

2. 写入文件

实际上和读取文差不多,如果能从文件中读取内容,那么往文件中写入内容也没有多大难处。

a. 简单示例
#include <iostream>
#include <fstream>

using namespace std; 

int main(){

    //若问写入操作,文件不存在,会自动创建文件
    //out: 每次写入都会覆盖源文件内容
    //app: 在末尾处追加内容
    fstream file{"../test2.txt",ios::app};

    if(file.is_open()){
        cout << "正常打开文件"<<endl;

        //写入数据
        file << ",hi c++";

        //写入换行
        file << endl;
        //写入结束
        file.close();
    }else{
        cout << "无法正常打开文件" << endl;
    }
	return 0 ;
}

b. 二维vector操作示例

现在要做的是把二维vector的矩阵数据,写入到文件中。

#include <iostream>
#include <vector>
#include <fstream>

using namespace std; 

int main(){
  //构建vector
    vector<vector<float >> matrixVector{
            {1, 6, 2, 10.5},
            {11, 15.2, 2, 21},
            {3, 9, 1, 7.5}
    };
    
    //构建文件对象
    fstream matrixFile{"../matrix.txt",ios::app};
    //文件能否正常使用
    if(matrixFile.is_open()){
        //开始遍历vector
        for (int i = 0; i < matrixVector.size(); ++i) {
            //遍历每一行
            for (int j = 0; j <matrixVector[i].size(); ++j) {
                //往文件写入数字
                matrixFile << matrixVector[i][j] ;

                //只要不是末尾的数字,那么都追加一个 ,
                if(j != matrixVector[i].size()-1){
                    matrixFile << ",";
                }
            }
            //写入换行
            matrixFile << endl;
        }
        //关闭文件
        matrixFile.close();
        cout << "文件写入完毕" << endl;
    }else{
        cout << "文件写入失败" << endl;
    }
  
    return 0 ;
} 

五、模板编程

1.模板编程的必要性

在c++中,变量的声明必须指出它的类型,提高了编译运行效率,但是在某些场合下就有点缺陷。比如:需要定义计算两个数之和的函数,由于未来计算的数值有可能是整数、也有可能是浮点数,所以需要为这些类型准备对应的函数,但是这些函数的内部逻辑都是一样的,他们的唯一区别就是所接收的数据类型不同而已。那么有没有一种情况使得编码的时候暂时的忽略掉类型这个特性,等运行时在动态决定。

#include <iostream>

using namespace std;

int add(inta ,int b){
    return a + b;
}

float add(float x , float y ){
    return x + y;
}

int main(){
    
    int result = add(3, 4 ) ;
    cout << "result  = " << result << endl;
    
    int result2 = add(3.5 ,4.5 );
    cout << "result2 = " << result2 << endl;
    
    return 0 ;
}

2. 函数模板

函数模板是通用函数的描述,使用泛型来定义函数,让编译器暂时忽略掉类型,使用参数把类型传递给模板,进而让编译器生成对应类型的函数。函数模板仅仅是表示一种模型罢了,并不是真正可以直接调用的函数,需要在使用的时候传递对应类型,生成对应类型的函数。

模板的定义以template关键字开始,后面跟随着参数列表,使用<> 包含

#include <iostream>

using namespace std;

template<typename T>
T add(const T& t1 ,const T& t2){
    return t1 + t2;
}

int main(){
    
    int result = add<int>( 3, 5);
    cout <<"result = " << result << endl;


    int result = add( 3, 5);
    cout <<"result = " << result << endl;

    return 0 ;
}

1. 函数模板重载

如普通函数一般,函数模板也可以重载,以便灵活应对更多的需求

#include <iostream>

using namespace std;

template<typename T>
T add(const T& t1 ,const T& t2){
    return t1 + t2;
}

template<typename T>
T add(const T& t1 , const T& t2 , const T& t3){
    return t1 + t2 + t3;
}

int main(){
    
    int result1 = add( 1, 2 );
    int result2 = add( 1, 2 ,3);

    cout <<"result1 = " << result1 << endl;
    cout <<"result2 = " << result2 << endl;
    
    return 0 ;
}
  • 模板可变参数

如果一个函数接收的参数个数不确定,并且都是同一种类型的参数,那么可以使用可变参数的写法来简化代码。 可变参数使用 … 来表示,通常可变参数也称之为 **参数包 ** ,用于表示它可以对多个参数打包成一个整体。

3. 可变参数

在C++ 中一般函数的参数个数都是固定的,但是也有一种特殊的函数,他们的参数个数是可变的。针对这种情况C++中提供了initializer_list省略号两种方法,其中initializer_list要求可变参数的类型必须都一致,而省略号方式则不具备这种限制,要更加灵活。

a. initializer_list 方式

c++ 11引入的一种方式,它需要导入#include , 参数必须放在一组{} 里面,并且元素的类型必须是一样的。相比于前面省略号的方式,initializer_list 就显得简单许多。

#include <iostream>
using namespace std;

int add(initializer_list<int> il) {
    int sum = 0;
    for (auto ptr = il.begin(); ptr != il.end(); ptr++){ 
        sum += *ptr;
    }
    return sum;
}

int main(){
    int result = add({10,20,30}); //传递的参数必须放置在一个 {} 里面
    
    cout << "result = " << result << endl;
   
    return 0 ;
}

4. 可变参数模板

函数模板解决了相同功能、不同数据类型造成多个方法重载的局面,而当模板的参数类型都是一样的,或者有多个一样的参数类型一样,那么使用可变参数来改进模板函数,就显得更为美观。对于参数包来说,除了获取大小之后,程序员更想关注的是,如何获取里面的数据,解构里面的元素,也有个名字:扩展

//AA
int add1(int a ){

}

// BB 可以接受可变参数。
int add2(initializer_list<int> list){

}

// CC 模板。
template <typename  T>
T add3(initializer_list<T> list){
    int total = 0 ;
    for(auto i = list.begin() ; i < list.end() ; i++){
        total += *i ;
    }
    return total ;
}

int main() {
    cout << add3( {1,2,3} ) <<endl;

    return 0;
}

5. 类模板编程

有时候继承、包含并不能满足重用代码的需要,这一般在容器类里面体现的尤为突出。例如: 我们定义了一个容器类,Container, 这个Container类可以实现类似verctor一样的工作,能保存数据,能修改数据,并且数据的类型不限制,但是针对数据的操作都是一样的。那么类模板编程就成了不二之选了。

1. 类模板编程的必要性

这里以栈作为参照对象,定义一个模板类,实现栈一样的功能。

class Stack{
private :
    int top =0 ; //表示最顶上的索引位置
    string items[10]; //定义一个数组,以便一会装10个元素
public:
    bool isempty(){
        return top == 0;
    }
    bool isfull(){
        return top == MAX;
    }
    //压栈
    int push(string val){
        if(isfull()){
            return -1;
        }
        //没有满就可以往里面存
        items[top++] = val;
    }
    //出栈
    string pop(){
        if (isempty()){
            return "";
        }
        //如果不是空 top 只是指向位置,而数组获取数据,索引从0开始,所以先--
        return items[--top] ;
    }

    string operator[](int index){
        if(isempty() || index > --top){
            cout <<"容器为空或者超出越界" << endl;
            return "";
        }
        return items[index];
    };
};

2. 类模板

上面的Stack容器仅仅只能针对string这种数据类型,如果想存自定义的类型或者其他类型,那么Stack就无法满足了。 要定义类模板,需要在类的前面使用template , 然后替换里面的所有string 即可,这样Stack就能为所有的类型工作了。 如果是自定义类型,那么需要自定义类型提供无参构造函数,因为数组的定义会执行对象的构造。若想避免构造的工作发生,可以使用allocator来操作。

template <typename T> class Stack{
private :
    enum{MAX = 10}; //表示这个Stack容器最多只能装10个。
    int top =0 ; //表示最顶上的索引位置
    T items[MAX]; //定义一个数组,以便一会装10个元素
public:
    bool isempty(){
        return top == 0;
    }
    bool isfull(){
        return top == MAX;
    }
    //压栈
    int push(const T& val){
        if(isfull()){
            return -1;
        }
        //没有满就可以往里面存
        items[top++] = val;
    }
    //出栈
    T pop(){
        if (isempty()){
            return "";
        }
        //如果不是空 top 只是指向位置,而数组获取数据,索引从0开始,所以先--
        return items[--top] ;
    }

    T operator[](int index){
        if(isempty() || index > --top){
            cout <<"容器为空或者超出越界" << endl;
            return "";
        }
        return items[index];
    };
};

六、容器

1. 顺序容器

所谓的顺序容器指的是,在容器的内部,元素的摆放是有顺序的。通常 vector已经足以满足大部分开发了。

容器描述
string与vector相似,尾部插入|删除速度快
array固定大小数组,支持快速随机访问,不能添加和删除
vector可变大小数组,支持快速随机访问,在尾部之外的位置插入和删除 速度慢
deque | queue双端(区分前后)队列,支持快速随机访问,两端插入和删除速度快
forward_list单向链表、只支持单向顺序访问,插入和删除快,查询和更新慢
list与单向链表不同,它是双向链表,其他一样。
  • deque
#include <deque>
#include <iostream>

using namespace std;

int main(){
    deque<int> deque;

    deque.push_back(10);
    deque.push_back(20);
    deque.push_back(30);

    cout <<"第一个是:" <<deque.front() << endl;
    cout <<"最后一个是:"<< deque.back()<<endl;
    cout <<"长度是:" << deque.size() <<endl;
    cout << deque[0] << " " << deque.at(1) << endl;

    deque.at(0) = 100;


    for(int i :deque){
        cout <<" --> " << i <<endl;
    }
    return 0 ;
}
  • forward_list
#include <forward_list>
#include <iostream>

using namespace std;

int main(){


    forward_list<int> flist{80, 70, 90};

    //1. 添加:: 只能在头的位置追加
    flist.push_front(100);

    //2. 添加:: 在第一个位置的后面,插入一个新的元素。 需要使用迭代器来完成
    flist.insert_after(flist.begin() , 35);

    //3. 删除:: 直接给元素值即可
    flist.remove(70);

    //4. 修改元素值: 也可以使用迭代器的方式修改值
    flist.front() = 10;
    *flist.begin()  = 20;
    cout <<"第一个元素是:" << flist.front() << endl;



    cout << "排序后打印:" << endl;
    flist.sort();
    for(auto i = flist.begin() ; i != flist.end() ; i++){
        cout << "--->" << *i << endl;
    }
    return 0 ;
}

  • list
#include <list>
#include <iostream>

using namespace std;

int main(){


    list<int> list1{80, 70, 90};

    //添加数据
    list1.push_front(10);
    list1.push_back(20);

    //删除元素
    list1.remove(70);

    //修改元素
    list1.front() = 10;
    cout << "第一元素:" <<list1.front() << endl;
    *list1.end() = 20 ;
    cout << "第一元素:" <<list1.back() << endl;

    //遍历链表:
    for (auto i  = list1.begin();  i != list1.end() ; i++) {
        cout <<" ---> "<< *i << endl;
    }


    return 0 ;
}

2. 迭代器

早前访问数组、vector、字符串时,都可以使用索引下标的方式访问,实际上还有一种更为通用的机制 : 迭代器 。所有标准库中的容器都可以使用迭代器,但是只有少数几个支持下标索引的方式。与指针相似,迭代器也能对对象进行间接访问,但是不能简单认为,指针就是对象,也不能直接认为迭代器就是对象。只是可以通过迭代获取到对象。

迭代器就是迭代器,容器是容器,元素是元素。他们是三个东西。

1. 迭代器介绍

迭代器通常都是靠容器的 begin()end() 函数获取。 其中begin 返回的是指向第一个元素的迭代器, 而end函数返回的值指向最后一个元素的下一个位置,也就是指向容器里面不存在的尾后元素。所以一般end()返回的迭代器仅是一个标记而已,并不用来获取数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ijOawnaP-1588903506825)(E:/备课专用/备课笔记/C++/day08/img/iterator-01.png)]

2. 迭代器运算 背后就是 指针的运算。

由于begin() 的返回值只会指向第一个元素,若想获取到后面的元素,可以对迭代器进行运算,它的用法和指针的运算是一样的。通过 + - 的操作,来让返回前后元素对应的迭代器。 在迭代器的内部,重载了*运算符函数。通过它可以直接获取到数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TZzuwLX2-1588903506828)(E:/备课专用/备课笔记/C++/day08/img/iterator-2.png)]

3. 使用迭代器

#include <iostream>
#include <vector>

using namespace std;

int main(){
    
    vector<int> s{88,85,90,95,77};
    cout <<*s.begin() << endl; //88
    cout <<*(s.begin()+1) << endl; //85
    //...
    cout <<*(s.end()-1) << endl;//77

    //遍历容器
    for(auto i = s.begin() ; i != s.end() ; i++){
        cout << *i << endl;
    }
    
    return 0 ;
}

3. 关联容器

关联容器和顺序容器有很大的不同,顺序容器的元素在容器中是有顺序的(按照添加先后计算) , 而关联容器并不计较顺序的概念,因为他们是按照关键字来访问元素的。C++中常用的关联容器有两个:mapset , map 有点类似 python 里面的字典,使用键值对的形式来存储

1. pair介绍

pair定义在头文件#include 中,一个pair保存两个数据成员,它是类模板,所以在创建对象的时候,需要添加泛型参数,以用来表示所保存的数据类型。

#include <iostream>
#include <utility>
#include <string>

using namespace std;

int main(){
    pair<string ,int> p("张三",17) ;
	cout << p.first  << p.second <<endl;
    return 0 ;
}

2. map操作

map 只允许产生一对一的关系,也就是一个关键字对应一个值,如生活中大多数人,一人一套房差不多。但是也有人是名下多套房,这时候可以使用multimap, 它允许一个关键字对应多个值。 它们都定义在头文件 #include 中。

a. 添加

不允许key 有重复的。

#include <iostream>
#include <map>
#include <string>

using namespace std;

int main (){

    map<string , string> address_map ;

    //匹配可变参数列表
    address_map.insert({"张三" , "星湖花园1号"});
    address_map.insert(make_pair("李四" , "星湖花园1号"));
    address_map.insert(pair<string ,string>("王五" , "星湖花园1号"));

    //有疑问
    address_map.insert({"张三" , "星湖花园2号"}); //与第一个同关键字,会覆盖原有数据

    return 0 ;
}

b. 访问

map可以使用【】的方式来获取指定的元素,要求传递进来的是key关键字

#include <iostream>
#include <map>
#include <string>

using namespace std;

int main(){
    //访问指定元素
    string address = address["张三"] ;
    cout << address << endl;

    //使用at函数访问
    string address2 = map.at("张三2")
    cout << address2 << endl;    
    return 0 ;
}

c. 删除

除了能使用迭代器的方式删除之外,关联容器由于使用了关键了记录数据,所以删除的时候也可以根据关键字来删除数据。

#include <iostream>
#include <map>
#include <string>

using namespace std;

int main(){
    //迭代器方式删除。
    for(auto i = address_map.begin() ; i != address_map.end() ; i++){
        cout <<i->first << " =  "<< i->second << endl;
        if(i->first == "李四"){
            address_map.erase(i);
        }
    }

    //使用关键字删除
    address_map.erase("王五");

    //清空整个map
    address_map.clear();
 	return 0 ;   
}

d. 修改

修改其实就是替换,但是如果还是用insert 是无法起作用的,因为它会执行唯一检查。使用 at函数,对某一个特定关键字的位置修改值。

#include <iostream>
#include <map>
#include <string>

using namespace std;

int main(){
    map<string , int> map;
    map.insert( {"张三1" ,18});
    map.insert( {"张三2" ,28});
    map.insert( {"张三3" ,38});

    cout <<map["张三1"] << endl; //18

    map.at("张三1") = 99;

    cout <<map["张三1"] << endl; //99
	return 0 ;
}

  • 容量查询

判断是否为空、获取大小、判断是否存在key

#include <iostream>
#include <map>
#include <string>

using namespace std;

int main(){
    map<string , int> map;
    map.insert( {"张三1" ,18});

    //判断是否为空
    bool empty = map.empty();
    cout <<"empty = " << empty << endl;

    //获取大小
    int size = map.size();
    cout <<"size = " << size << endl;

    //查询该key在map中的个数,可用来判断是否存在该key
    int count = map.count("张三1");
    cout <<"count = " << count << endl;
	return 0 ;
}

3. set操作

set就是关键字的简单集合,并且容器内部元素不可重复且有顺序。当只是想知道一个值是否存在时,set是最有用的。 set 为不可重复容器,而multiset为可重复容器。

在set中每个元素的值都唯一,而且系统能根据元素的值自动进行排序。set中元素的值不能直接被改变。set内部采用的是一种非常高效的平衡检索二叉树:红黑树,也称为RB树(Red-Black Tree)。RB树的统计性能要好于一般平衡二叉树。使用set需要导入头文件 #include

a. 添加元素
#include <set>

using namespace std;

int main(){
    
    //创建空白set容器
   // set<int>s ;

    //创建容器并初始化,可变参数往里面赋值,但是这里添加了重复的3. 后面的3不会被添加进去。
    set<int> s1{3,4,5,6,3};

    s.insert(16);
    s.insert({7,8,9,10}); //也可以接收可变参数
    
    return 0 ;
}

b. 遍历set

遍历的逻辑和其他含有迭代器的容器一样。

#include <set>
#include <iostream>

using namespace std;

int main(){
    set<int> s1({3,4,5,6,3});
    for(auto p = s1.begin(); p != s1.end() ; p++){
        cout <<*p << endl;
    }
    return 0 ;
}

c. 删除指定元素

还是使用erase 函数删除 。

#include <set>
#include <iostream>

using namespace std;

int main(){

    set<int> s1({3,4,5,6,3});
    for(auto p = s1.begin(); p != s1.end() ; p++){
        cout <<*p << endl;
        if(*p == 4){
            s1.erase(p);
        }
    }

    //清空这个容器
    s1.clear();
 
    return 0 ;
}

  • 容量查询

判断是否为空、获取大小、判断是否存在key

set<int> s1({3,4,5,6});

//判断是否为空
bool empty = s1.empty();
cout <<"empty = " << empty << endl;

//获取大小
int size = s1.size();
cout <<"size = " << size << endl;

//查询该key在map中的个数,可用来判断是否存在该key
int count = s1.count("张三1");
cout <<"count = " << count << endl;

上一篇C++学习课件(二)

下一篇C++学习课件(四)

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~简

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值