项目经理带你-零基础学习C/C++
【从入门到精通】
项目十五 C++核心编程-万能择优器
第1节 项目需求
程序员Jack 的团队新接手了一个底层的项目,项目经理要求Jack 实现一个通用的容器,能够支持插入多种不同的普通类型(包含 int char float double 等)和自定义结构体和自定义类的对象,并能根据每种不同类型的比较规则从容器中取得最大或最小的那个值或对象。
示例代码:
// demo 15-1.c #include <vector> #include <iostream> using namespace std; class demo{ public: demo(int _k=0){k=_k;} ~demo(){} int value(){return k;} private: int k; }; int main(void){ vector<int> v1; int i1 = 1; int i2 = 2; v1.push_back(i1); v1.push_back(i2); demo d1(10); vector<demo> v2; v2.push_back(d1); for(unsigned int i=0; i<v1.size(); i++){ printf("vector v1 中的元素%d : %d\n",i ,v1[i]); } cout<<v2[0].value()<<endl; system("pause"); return 0; } |
第2节 项目精讲
1. C++函数模板的使用
项目需求: 实现多个函数用来返回两个数的最大值,要求能支持char类型、int类型、double类型变量
cout<<"max(1, 2) = "<<Max(x, y)<<endl; |
//template 关键字告诉C++编译器 我要开始泛型编程了,请你不要随意报错 cout<<"max(1, 2) = "<<Max(x, y)<<endl; //实现参数类型的自动推导 cout<<"max(1, 2) = "<<Max<int>(x,y)<<endl;//显示类型调用 |
由以下三部分组成: 模板说明 + 函数定义 + 函数模板调用
typename T1 , typename T2 , …… , typename Tn
或 class T1 , class T2 , …… , class Tn
//第一种情况,模板函数和普通函数并存,参数类型和普通重载函数更匹配 |
2. 类模板的使用
类模板与函数模板的定义和使用类似,有时,有两个或多个类,其功能是相同的,仅仅是数据类型不同,我们可以通过如下面语句声明了一个类模板:
//2.子类是一般类,父类是模板类,继承时必须在子类里实例化父类的类型参数 //3.父类和子类都时模板类时,子类的虚拟的类型可以传递到父类中 //2.模板种如果使用了构造函数,则遵守以前的类的构造函数的调用规则 |
结论: 子类从模板类继承的时候,需要让编译器知道 父类的数据类型具体是什么
2.子类是一般类,父类是模板类,继承时必须在子类里实例化父类的类型参数
3.父类和子类都时模板类时,子类的虚拟的类型可以传递到父类中
A<T> A<T>::operator+(const A<T> &other){ |
在同一个cpp 文件中把模板类的成员函数放到类的外部,需要注意以下几点
- 函数前声明 template <类型形式参数表>
- 类的成员函数前的类限定域说明必须要带上虚拟参数列表
- 返回的变量是模板类的对象时必须带上虚拟参数列表
- 成员函数参数中出现模板类的对象时必须带上虚拟参数列表
- 成员函数内部没有限定
5.3 所有的类模板函数写在类的外部,在不同的.h和.cpp中
A<T> A<T>::operator+(const A<T> &other){ |
注意:当类模板的声明(.h文件)和实现(.cpp 或.hpp文件)完全分离,因为类模板的特殊实现,我们应在使用类模板时使用#include 包含 实现部分的.cpp 或.hpp文件。
friend A<T> addA(const A<T> &a, const A<T> &b); A<T> A<T>::operator+(const A<T> &other){ A<T> addA(const A<T> &a, const A<T> &b){ |
friend A<T> addA (A<T> &a, A<T> &b);
A<int> c4 = addA<int>(c1, c2);
- 从类模板实例化的每个模板类有自己的类模板数据成员,该模板类的所有对象共享一个static数据成员
- 和非模板类的static数据成员一样,模板类的static数据成员也应该在文件范围定义和初始化
- static 数据成员也可以使用虚拟类型参数T
2) 将此类中准备改变的类型名(如int要改变为float或char)改用一个自己指定的虚拟类型名(如上例中的T)。
函数类型 类模板名<虚拟类型参数>::成员函数名(函数形参表列) {…}
1) 类模板的类型参数可以有一个或多个,每个类型前面都必须加typename 或class,如:
template <typename T1,typename T2>
2) 和使用类一样,使用类模板时要注意其作用域,只有在它的有效作用域内用使用它定义对象。
3) 模板类也可以有支持继承,有层次关系,一个类模板可以作为基类,派生出派生模板类。
friend ostream &operator<< <T> (ostream &out, const Vector &object); Vector(int size = 128); //构造函数 Vector(const Vector &object); //拷贝构造函数 |
附1:优化Student类, 属性变成 char *pname, 构造函数里面 分配内存
附2:优化Student类,析构函数 释放pname指向的内存空间
附3:优化Student类,避免浅拷贝 重载= 重写拷贝构造函数
3. 异常处理机制
唐僧一行西天取经队伍到达贫困山区,几天要不到吃的,悟空因为要保护师父,只好让沙僧和八戒去远处城里找吃的.
第一天去,空手回来,因为没有钱.第二天去,还是空手,因为没有钱.
沙僧顿时伤心地哭道:"大师兄,原谅我吧!咱们这么多人,就二师兄能卖到25块钱一斤.......
异常无处不在,程序随时可能误入歧途!C++ 提出了新的异常处理机制!
函数是一种以栈结构展开的上下函数衔接的程序控制系统,异常是另一种控制结构,它可以在出现“意外”时中断当前函数,并以某种机制(类型匹配)回馈给隔代的调用者相关的信息.
3.1传统错误处理机制
通过函数返回值来处理错误。
// demo 15-14 #include <stdio.h> #include <stdlib.h> #define BUFSIZE 1024 //实现文件的二进制拷贝 int copyfile(char *dest, char *src){ FILE *fp1 = NULL, *fp2 = NULL; //rb 只读方式打开一个二进制文件,只允许读取数据 fopen_s(&fp1, src, "rb"); if(fp1 == NULL){ return -1; } //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。 fopen_s(&fp2, dest, "wb"); if(fp2 == NULL){ return -2; } char buffer[BUFSIZE]; int readlen, writelen; //如果读到数据,则大于0 while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){ writelen = fwrite(buffer, 1, readlen, fp2); if(readlen != writelen){ return -3 ; } } fclose(fp1); fclose(fp2); return 0; } void main(){ int ret = 0; ret = copyfile("c:/test/dest.txt", "c:/test/src.txt"); if(ret != 0){ switch(ret){ case -1: printf("打开源文件失败!\n"); break; case -2: printf("打开目标文件失败!\n"); break; case -3: printf("拷贝文件时失败!\n"); break; default: printf("出现未知的情况!\n"); break; } } system("pause"); } |
C++ 异常处理机制
// demo 15-15 #include <stdio.h> #include <stdlib.h> #include <string> using namespace std; #define BUFSIZE 1024 //实现文件的二进制拷贝 int copyfile2(char *dest, char *src){ FILE *fp1 = NULL, *fp2 = NULL; //rb 只读方式打开一个二进制文件,只允许读取数据 fopen_s(&fp1, src, "rb"); if(fp1 == NULL){ throw new string("文件不存在"); } //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。 fopen_s(&fp2, dest, "wb"); if(fp2 == NULL){ throw -2; } char buffer[BUFSIZE]; int readlen, writelen; //如果读到数据,则大于0 while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){ writelen = fwrite(buffer, 1, readlen, fp2); if(readlen != writelen){ throw -3 ; } } fclose(fp1); fclose(fp2); return 0; } int copyfile1(char *dest, char *src){ return copyfile2(dest, src); } void main(){ int ret = 0; try{ ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt"); }catch(int error){ printf("出现异常啦!%d\n", error); }catch(string *error){ printf("捕捉到字符串异常:%s\n", error->c_str()); delete error; } system("pause"); } |
3.2 异常处理基本语法
异常发生第一现场,抛出异常
void function( ){
//... ...
throw 表达式;
//... ...
}
在需要关注异常的地方,捕捉异常
try{
//程序
function();
//程序
}catch(异常类型声明){
//... 异常处理代码 ...
}catch(异常类型 形参){
//... 异常处理代码 ...
}catch(...){ //其它异常类型
//
}
注意事项:
- 通过throw操作创建一个异常对象并抛掷
- 在需要捕捉异常的地方,将可能抛出异常的程序段嵌在try块之中
- 按正常的程序顺序执行到达try语句,然后执行try块{}内的保护段
- 如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从try块后跟随的最后一个catch子句后面的语句继续执行下去
- catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并按catch子句中的代码处理异常(或继续抛掷异常)
- 如果没有找到匹配,则缺省功能是调用abort终止程序。
提示:处理不了的异常,我们可以在catch的最后一个分支,使用throw语法,继续向调用者throw。
源码:
// demo 15-16 #include <stdio.h> #include <stdlib.h> #include <string> using namespace std; #define BUFSIZE 1024 //实现文件的二进制拷贝 int copyfile2(char *dest, char *src){ FILE *fp1 = NULL, *fp2 = NULL; //通过throw操作创建一个异常对象并抛掷 throw 0.01f; //rb 只读方式打开一个二进制文件,只允许读取数据 fopen_s(&fp1, src, "rb"); if(fp1 == NULL){ throw new string("文件不存在"); } //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。 fopen_s(&fp2, dest, "wb"); if(fp2 == NULL){ throw -2; } char buffer[BUFSIZE]; int readlen, writelen; //如果读到数据,则大于0 while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){ writelen = fwrite(buffer, 1, readlen, fp2); if(readlen != writelen){ throw -3 ; } } fclose(fp1); fclose(fp2); return 0; } int copyfile1(char *dest, char *src){ try{ copyfile2(dest, src); }catch(float e){ //throw ; printf("copyfile1 - catch ...\n"); //提示:处理不了的异常,我们可以在catch的最后一个分支,使用throw语法,继续向调用者throw。 throw ; } return 0; } void main(){ int ret = 0; //在需要捕捉异常的地方,将可能抛出异常的程序段嵌在try块之中 //按正常的程序顺序执行到达try语句,然后执行try块{}内的保护段 //如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行,程序从try块后跟随的最后一个catch子句后面的语句继续执行下去 try{//保护段 printf("开始执行 copyfile1...\n"); ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt"); printf("执行 copyfile1 完毕\n");
//catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并按catch子句中的代码处理异常(或继续抛掷异常) }catch(int error){ printf("出现异常啦!%d\n", error); }catch(string *error){ printf("捕捉到字符串异常:%s\n", error->c_str()); delete error; }catch(float error){ printf("出现异常啦!%f\n", error); }catch(...){ printf("catch ...\n"); } //如果没有找到匹配,则缺省功能是调用abort终止程序。 system("pause"); } |
3.3异常接口声明
可以在函数声明中列出可能抛出的所有异常类型,加强程序的可读性。
如:
int copyfile2(char *dest, char *src) throw (float, string *, int)
1.对于异常接口的声明,在函数声明中列出可能抛出的所有异常类型
2.如果没有包含异常接口声明,此函数可以抛出任何类型的异常
3.如果函数声明中有列出可能抛出的所有异常类型,那么抛出其它类型的异常讲可能导致程序终止
4.如果一个函数不想抛出任何异常,可以使用 throw () 声明
3.4异常类型和生命周期
main |
copyfile1 |
copyfile2 |
函数调用 |
函数调用 |
| |||
- throw基本类型
// demo 15-17 #include <stdio.h> #include <stdlib.h> #include <string> using namespace std; #define BUFSIZE 1024 //实现文件的二进制拷贝 //第一种情况,throw 普通类型,和函数返回传值是一样的 int copyfile2(char *dest, char *src){ FILE *fp1 = NULL, *fp2 = NULL; //rb 只读方式打开一个二进制文件,只允许读取数据 fopen_s(&fp1, src, "rb"); if(fp1 == NULL){ //int ret = -1; char ret = 'a'; throw ret; } //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。 fopen_s(&fp2, dest, "wb"); if(fp2 == NULL){ throw -2; } char buffer[BUFSIZE]; int readlen, writelen; //如果读到数据,则大于0 while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){ writelen = fwrite(buffer, 1, readlen, fp2); if(readlen != writelen){ throw -3 ; } } fclose(fp1); fclose(fp2); return 0; } int copyfile1(char *dest, char *src){ return copyfile2(dest, src); } void main(){ int ret = 0;
try{//保护段 //printf("开始执行 copyfile1...\n"); ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt"); //printf("执行 copyfile1 完毕\n");
}catch(int error){ printf("出现异常啦!%d\n", error); }catch(char error){ printf("出现异常啦!%c\n", error); } system("pause"); } |
- throw 字符串类型
// demo 15-18 #include <stdio.h> #include <stdlib.h> #include <string> using namespace std; #define BUFSIZE 1024 //第二种情况,throw 字符串类型,实际抛出的指针,而且,修饰指针的const 也要严格进行类型匹配 int copyfile3(char *dest, char *src){ FILE *fp1 = NULL, *fp2 = NULL; //rb 只读方式打开一个二进制文件,只允许读取数据 fopen_s(&fp1, src, "rb"); if(fp1 == NULL){ const char * error = "大佬,你的源文件打开有问题"; printf("throw 前,error 的地址:%p\n", error); throw error; } //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。 fopen_s(&fp2, dest, "wb"); if(fp2 == NULL){ throw -2; } char buffer[BUFSIZE]; int readlen, writelen; //如果读到数据,则大于0 while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){ writelen = fwrite(buffer, 1, readlen, fp2); if(readlen != writelen){ throw -3 ; } } fclose(fp1); fclose(fp2); return 0; } int copyfile1(char *dest, char *src){ return copyfile3(dest, src); } void main(){ int ret = 0; try{//保护段 //printf("开始执行 copyfile1...\n"); ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt"); //printf("执行 copyfile1 完毕\n");
}catch(int error){ printf("出现异常啦!%d\n", error); }catch(char error){ printf("出现异常啦!%c\n", error); }catch(string error){ printf("出现异常啦!%s\n", error.c_str()); }catch(const char *error){ printf("出现异常啦(char *)!%s(地址:%p)\n", error, error); }catch(...){ printf("没捉到具体的异常类型\n"); } system("pause"); } |
3)throw 类对象类型异常
// demo 15-19 #include <stdio.h> #include <stdlib.h> #include <string> using namespace std; #define BUFSIZE 1024 class ErrorException{ public: ErrorException(){ id = 0; printf("ErrorException 构造!\n"); } ~ErrorException(){ printf("ErrorException ~析构!(id: %d)\n", id); } ErrorException(const ErrorException &e){ id = 1; printf("ErrorException 拷贝构造函数!\n"); } int id; }; //第三种情况,throw 类类型,最佳的方式是使用引用类型捕捉,抛出匿名对象 //当然,如果是动态分配的对象,直接抛出其指针 //注意:引用和普通的形参传值不能共存 int copyfile4(char *dest, char *src){ FILE *fp1 = NULL, *fp2 = NULL; //rb 只读方式打开一个二进制文件,只允许读取数据 fopen_s(&fp1, src, "rb"); if(fp1 == NULL){ //ErrorException error1; throw ErrorException(); //throw ErrorException(); } //wb 以只写的方式打开或新建一个二进制文件,只允许写数据。 fopen_s(&fp2, dest, "wb"); if(fp2 == NULL){ throw -2; } char buffer[BUFSIZE]; int readlen, writelen; //如果读到数据,则大于0 while( (readlen = fread(buffer, 1, BUFSIZE, fp1)) > 0 ){ writelen = fwrite(buffer, 1, readlen, fp2); if(readlen != writelen){ throw -3 ; } } fclose(fp1); fclose(fp2); return 0; } int copyfile1(char *dest, char *src){ return copyfile4(dest, src); } void main(){ int ret = 0; try{//保护段 //printf("开始执行 copyfile1...\n"); ret = copyfile1("c:/test/dest.txt", "c:/test/src.txt"); //printf("执行 copyfile1 完毕\n");
}catch(ErrorException error){ printf("出现异常啦!捕捉到 ErrorException 类型 id: %d\n", error.id); }catch(ErrorException &error){ //error.id = 2; printf("出现异常啦!捕捉到 ErrorException &类型 id: %d\n", error.id); }catch(ErrorException *error){ printf("出现异常啦!捕捉到 ErrorException *类型 id: %d\n", error->id); delete error; }catch(...){ printf("没捉到具体的异常类型\n"); } system("pause"); } |
3.5 继承与异常
异常也是类,我们可以创建自己的异常类,在异常中可以使用(虚函数,派生,引用传递和数据成员等)
案例:设计一个数组类容器 Vector,重载[]操作,数组初始化时,对数组的个数进行有效检查
- index<0 抛出异常errNegativeException
- index = 0 抛出异常 errZeroException
3)index>1000抛出异常errTooBigException
4)index<10 抛出异常errTooSmallException
5)errSizeException类是以上类的父类,实现有参数构造、并定义virtual void printError()输出错误。
// demo 15-20 #include <iostream> using namespace std; /* 设计一个数组类容器 Vector,重载[]操作,数组初始化时,对数组的个数进行有效检查 1)index<0 抛出异常errNegativeException 2)index = 0 抛出异常 errZeroException 3)index>1000抛出异常errTooBigException 4)index<10 抛出异常errTooSmallException 5)errSizeException类是以上类的父类,实现有参数构造、并定义virtual void printError()输出错误。 */ class errSizeException{ public: errSizeException(int size){ m_size = size; } virtual void printError(){ cout<<"size: "<<m_size<<endl; } protected: int m_size; }; class errNegativeException : public errSizeException{ public: errNegativeException(int size):errSizeException(size){ } virtual void printError(){ cout<<"errNegativeException size: "<<m_size<<endl; } }; class errZeroException : public errSizeException{ public: errZeroException(int size):errSizeException(size){ } virtual void printError(){ cout<<"errZeroException size: "<<m_size<<endl; } }; class errTooBigException : public errSizeException{ public: errTooBigException(int size):errSizeException(size){ } virtual void printError(){ cout<<"errTooBigException size: "<<m_size<<endl; } }; class errTooSmallException : public errSizeException{ public: errTooSmallException(int size):errSizeException(size){ } virtual void printError(){ cout<<"errTooSmallException size: "<<m_size<<endl; } }; class Vector{ public: Vector(int size = 128); //构造函数 int getLength();//获取内部储存的元素个数 int& operator[](int index); ~Vector(); private: int *m_base; int m_len; }; Vector::Vector(int len){ if(len < 0){ throw errNegativeException(len); }else if(len == 0){ throw errZeroException(len); }else if(len > 1000){ throw errTooBigException(len); }else if(len < 10){ throw errTooSmallException(len); } m_len = len; m_base = new int[len]; } Vector::~Vector(){ if(m_base) delete[] m_base; m_len = 0; } int Vector::getLength(){ return m_len; } int &Vector::operator[](int index){ return m_base[index]; } void main(){ try{ Vector v(10000); for(int i=0; i<v.getLength(); i++){ v[i] = i+10; printf("v[i]: %d\n", v[i]); } }catch(errSizeException &err){ err.printError(); }
/*catch(errNegativeException &err){ cout<<"errNegativeException..."<<endl; }catch(errZeroException &err){ cout<<"errZeroException..."<<endl; }catch(errTooBigException &err){ cout<<"errTooBigException..."<<endl; }catch(errTooSmallException &err){ cout<<"errTooSmallException..."<<endl; }*/ system("pause"); return ; } |
3.6异常处理的基本思想
C++的异常处理机制使得异常的引发和异常的处理不必在同一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以再适当的位置设计对不同类型异常的处理。
异常是专门针对抽象编程中的一系列错误进行处理的,C++中不能借助函数机制实现异常,因为栈结构的本质是先进后出,依次访问,无法进行跳跃,但错误处理的特征却是遇到错误信息就想要转到若干级之上进行重新尝试, 如图:
3.7 标准程序库异常
// demo 15-21 #include <iostream> #include <exception> #include <stdexcept> using namespace std; class Student{ public: Student(int age){ if(age > 249){ throw out_of_range("年龄太大,你是外星人嘛?"); } m_age = age; m_space = new int[1024*1024*100]; } private : int m_age; int *m_space; }; void main(){ try{ for(int i=1; i<1024; i++){ Student * xiao6lang = new Student(18); } }catch(out_of_range &e){ cout<<"捕捉到一只异常:"<<e.what()<<endl; }catch(bad_alloc &e){ cout<<"捕捉到动态内存分配的异常:"<<e.what()<<endl; } system("pause"); } |
- STL(标准模板库)专题
STL主要分为分为三类:
- algorithm(算法) - 对数据进行处理(解决问题) 步骤的有限集合
- container(容器) - 用来管理一组数据元素
- Iterator (迭代器) - 可遍历STL容器内全部或部分元素”的对象
容器和算法通过迭代器可以进行无缝地连接。在STL中几乎所有的代码都采用了模板类和模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
STL 最早源于惠普实验室,早于C++存在,但是C++引入STL概念后,STL就成为C++的一部分,因为它被内建在你的编译器之内,不需要另行安装。
STL被组织为下面的13个头文 件:<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack> 和<utility>。
// demo 15-22 #include <iostream> using namespace std; #include <vector> #include <algorithm> class student{ public: student(int age, const char *name){ this->age = age; strncpy(this->name, name, 64); } student(const student &s){ this->age = s.age; strncpy(this->name, s.name, 64); cout<<"拷贝构造函数被调用!"<<endl; } public: int age; char name[64]; }; //容器中直接存放对象,会发生拷贝构造 void demo2(){ vector<student> v1; student s1(18, "李小美"); student s2(19, "王大帅"); v1.push_back(s1); v1.push_back(s2); cout<<"v1 的学生的个数:"<<v1.size()<<endl; //方式1,下标访问 //for(unsigned int i=0; i<v1.size(); i++){ // cout<<v1[i].name<<": "<<v1[i].age<<endl; //} vector<student>::iterator it = v1.begin(); for( ; it != v1.end(); it++){ cout<< (*it).name<<": "<<(*it).age <<endl; } } //容器中存放指针,不执行拷贝构造,效率较高 void demo3(){ vector<student *> v1; student s1(18, "李小美"); student s2(19, "王大帅"); v1.push_back(&s1); v1.push_back(&s2); cout<<"v1 的学生的个数:"<<v1.size()<<endl; //方式1,下标访问 //for(unsigned int i=0; i<v1.size(); i++){ // cout<<v1[i].name<<": "<<v1[i].age<<endl; //} vector<student *>::iterator it = v1.begin(); for( ; it != v1.end(); it++){ cout<< (**it).name<<": "<<(**it).age <<endl; } } void demo1(){ //第一部分 容器 vector<int> v1; v1.push_back(1);//调用了拷贝构造,是复制操作,值传递 v1.push_back(2); v1.push_back(3); v1.push_back(4); v1.push_back(3); cout<<"v1 的元素个数:"<<v1.size()<<endl; cout<<"v1中保存的元素:"<<endl; //方式1,下标访问 //for(unsigned int i=0; i<v1.size(); i++){ // cout<<v1[i]<<endl; //} //方式2,迭代器访问 //第二部分 迭代器 //1 2 3 4 //it vector<int>::iterator it = v1.begin(); for( ; it != v1.end(); it++){ cout<< *it <<endl; } //第三部分 算法 int ncount = count(v1.begin(), v1.end(), 90); cout<<"v1 中数值为 90 的元素个数:"<< ncount<< endl; } void main(){ demo3(); system("pause"); return ; } |
4.1容器
在实际的开发过程中,数据结构本身的重要性完全不逊于算法的重要性,当程序中存在着对时间要求很高的部分时,数据结构的选择就显得更加重要。
试想: 一条死胡同里面停车,这样的效率会很高嘛?
经典的数据结构数量有限,但是在项目实战中,我们常常重复着一些为了存放不同数据类型而实现顺序表、链表等结构而重复编写的代码,这些代码都十分相似,只是为了适应不同数据类型的变化而在细节上有所出入。STL容器就为我们提供了这样的方便,它允许我们重复利用已有的实现构造自己的特定类型下的数据结构,通过设置一些模板,STL容器对最常用的数据结构提供了支持,这些模板的参数允许我们指定容器中元素的数据类型,避免重复编码。
容器部分主要有由<vector>,<list>,<deque>,<set>,<map>,<stack> 和<queue>组成。
下面是常用的一些容器,可以通过下表总结一下它们和相应头文件的对应关系。
数据结构 | 描述 | 实现头文件 |
向量(vector) | 连续存储的元素 | <vector> |
列表(list) | 由节点组成的双向链表,每个结点包含着一个元素 | <list> |
双向队列(deque) | 连续存储的指向不同元素的指针所组成的数组 | <deque> |
集合(set) | 由节点组成的红黑树,每个节点都包含着一个元素,节点之间以某种作用于元素对的谓词排列,没有两个不同的元素能够拥有相同的次序 | <set> |
多重集合(multiset) | 允许存在两个次序相等的元素的集合 | <set> |
栈(stack) | 后进先出的元素的排列 | <stack> |
队列(queue) | 先进先出的元素的排列 | <queue> |
优先队列(priority_queue) | 元素的次序是由作用于所存储的值对上的某种优先级决定的的一种队列 | <queue> |
映射(map) | 由{键,值}对组成的集合,以某种作用于键对上的谓词排列 | <map> |
多重映射(multimap) | 允许键对有相等的次序的映射 | <map> |
4.1.1 Vector容器
Vector容器概念
vector是将元素置于一个动态数组中加以管理的容器。
vector可以随机存取元素,支持索引值直接存取, 用[]操作符或at()方法对元素进行操作
vector尾部添加或移除元素非常快速。但是在中部或头部插入元素或移除元素比较费时
vector对象的构造
vector采用模板类实现,vector对象的默认构造形式
vector<T> vecT;
//默认构造函数
vector<int> v1; //一个存放int的vector容器
vector<float> v2; //一个存放float的vector容器
vector<student> v2; //一个存放student的vector容器
//带参构造函数
vector(beg,end); //构造函数将[beg, end)区间中的元素拷贝给本身。注意该区间是左闭右开的区间
vector(n,elem); //构造函数将n个elem拷贝给本身
vector(const vector &v1); //拷贝构造函数
// demo 15-23 #include <iostream> using namespace std; #include <vector> #include <algorithm> void demo1(){ //vector 对象的默认构造 //默认构造函数 元素个数为0, 所占内存空间为0 /*vector<int> v1; //vector<float> v2;
cout<<"v1 的元素个数: "<<v1.size()<<endl; cout<<"v1 容器的大小:"<<v1.capacity()<<endl; //当我们使用vector 的默认构造函数时,切记,不能直接通过下标去访问 //v1[0]=1; v1.push_back(1); cout<<"尾部插入1个元素后:"<<endl; cout<<"v1 的元素个数:"<<v1.size()<<endl; cout<<"v1 容器的大小:"<<v1.capacity()<<endl; v1.push_back(2); v1.push_back(3); v1.push_back(4); v1.push_back(5); cout<<"尾部插入5个元素后:"<<endl; cout<<"v1 的元素个数:"<<v1.size()<<endl; cout<<"v1 容器的大小:"<<v1.capacity()<<endl; */ //vector 带参构造函数 //vector<int> v2(10); //构造时就分配空间,同时插入10个元素,元素大小为0 vector<int> v2(10, 666); //vector<int> v3(v2); //vector<int> v3(v2.begin()+3, v2.end()); int test[]={1, 2, 3, 4, 5}; vector<int> v3(test, test+2); cout<<"v2 的元素个数:"<<v2.size()<<endl; cout<<"v2 容器的大小:"<<v2.capacity()<<endl; cout<<"v2调用 assign 后:"<<endl; cout<<"v2 的元素个数:"<<v2.size()<<endl; cout<<"v2 中存储的元素是: "<<endl; for(int i=0; i<v2.size(); i++){ cout<<v2[i]<<endl; } cout<<"v3 中存储的元素是: "<<endl; for(int i=0; i<v3.size(); i++){ cout<<v3[i]<<endl; } } void main(){ demo1(); system("pause"); return ; } |
vector的赋值
vector 的赋值
v2.assign(2, 888);//第一种玩法 改变原来vector 中的元素个数和值
v2.assign(v3.begin(), v3.end());//第二种玩法,使用迭代器重新赋值
int test1[]={1, 2, 3, 4, 5};
v2.assign(test1, test1+3);//第三种玩法,使用指针赋值
v2 = v3;//第四种玩法,赋值运算
vector的大小
vector.size(); //返回容器中元素的个数
vector.empty(); //判断容器是否为空
vector.resize(num); //重新指定容器的长度为num,若容器变长,则以默认值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
vector.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除
vector末尾的添加移除操作
v2.push_back(1); //在容器尾部加入一个元素
v2.pop_back(); //移除容器中最后一个元素
vector的数据存取
第一 使用下标操作 v2[0] = 100;
第二 使用at 方法 如: v2.at(2) = 100;
第三 接口返回的引用 v2.front() 和 v2.back()
注意: 第一和第二种方式必须注意越界
vector的插入
vector.insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
vector.insert(pos,n,elem); //在pos位置插入n个elem数据,无返回值。
vector.insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值
vector的删除
1. 把整个vector 都干掉
v2.clear();
cout<<"调用 v2.clear() 后"<<endl;
2.干掉单个元素
v2[1] = 888;
v2.erase(v2.begin()+1);
3. 干掉多个元素
v2.erase(v2.begin(), v2.begin()+3);
4.1.2 deque容器
deque容器概念
deque是“double-ended queue”的缩写,和vector一样都是STL的容器,唯一不同的是:
deque是双端数组,而vector是单端的。
Deque 特点:
- deque在接口上和vector非常相似,在许多操作的地方可以直接替换。
- deque可以随机存取元素(支持索引值直接存取,用[]操作符或at()方法)
- deque头部和尾部添加或移除元素都非常快速, 但是在中部安插元素或移除元素比较费时。
使用时,包含头文件:#include <deque>
deque对象的默认构造
deque也是采用模板类实现。
deque对象的默认构造形式:deque<T> deqT
例如:
deque <int> deqInt; //存放int的deque容器。
deque <float> deqFloat; //存放float的deque容器。
deque <student> deqStu; //存放student的deque容器。
...
注意:尖括号内还可以设置指针类型或自定义类型。
deque对象的带参数构造
方式1:deque(beg,end); //构造函数将[beg, end)区间中的元素拷贝给本身。
方式2:deque(n,elem); //构造函数将n个elem拷贝给本身。
方式3:deque(const deque &deq); //拷贝构造函数。
deque<int> deqIntA;
deqIntA.push_back(1);
deqIntA.push_back(2);
deqIntA.push_back(3);
deqIntA.push_back(4);
deque<int> deqIntB(deqIntA.begin(),deqIntA.end()); //1 2 3 4
deque<int> deqIntC(8, 666); //8 8 8 8 8
deque<int> deqIntD(deqIntA); //1 2 3 4
deque头部和末尾的添加移除操作
- deque.push_back(element); //容器尾部添加一个数据
- deque.push_front(element); //容器头部插入一个数据
- deque.pop_back(); //删除容器最后一个数据
- deque.pop_front(); //删除容器第一个数据
deque<int> deqIntA;
deqIntA.push_back(1);
deqIntA.push_back(2);
deqIntA.push_back(3);
deqIntA.push_back(4);
deqIntA.push_back(5);
deqIntA.push_back(6);
deqIntA.pop_front();
deqIntA.pop_front();
deqIntA.push_front(7);
deqIntA.push_front(8);
deqIntA.pop_back();
deqIntA.pop_back();
deqIntA 中剩余元素: 8 7 3 4
deque的数据存取
第一 使用下标操作 deqIntA[0] = 100;
第二 使用at 方法 如: deqIntA.at(2) = 100;
第三 接口返回的引用 deqIntA.front() 和 deqIntA.back()
注意: 第一和第二种方式必须注意越界
例如:
deque<int> deqIntA;
deqIntA.push_back(1);
deqIntA.push_back(2);
deqIntA.push_back(3);
deqIntA.push_back(4);
deqIntA.push_back(5);
int i1 = deqIntA.at(0); //i1 = 1
int i2 = deqIntA[1]; //i2 = 2
deqIntA.at(0) = 666; //第一个元素改成666
deqIntA[1] = 888; //第二个元素改成888
int iFront = deqInt.front(); //666
int iBack = deqInt.back(); //5
deqInt.front() = 888; //第一个元素改成 888
deqInt.back() = 666; //最后一个元素改成 666
deque与迭代器
- deque.begin(); //返回容器中第一个元素的迭代器。
- deque.end(); //返回容器中最后一个元素之后的迭代器。
- deque.rbegin(); //返回容器中倒数第一个元素的迭代器。
- deque.rend(); //返回容器中倒数最后一个元素之后的迭代器。
- deque.cbegin(); //返回容器中第一个元素的常量迭代器。
- deque.cend(); //返回容器中最后一个元素之后的常量迭代器。
deque<int> deqIntA;
deqIntA.push_back(1);
deqIntA.push_back(2);
deqIntA.push_back(3);
deqIntA.push_back(4);
deqIntA.push_back(5);
//普通迭代器
for(deque<int>::iterator it = deqIntA.begin(); it!=deqIntA.end(); ++it){
(*it)++; //*it++ (*it)++
cout<<*it;
cout<<" ";
}
//常量迭代器
deque<int>::const_iterator cit = deqIntA.cbegin();
for( ; cit!=deqIntA.cend(); cit++){
cout<<*cit;
cout<<" ";
}
//逆转的迭代器
for(deque<int>::reverse_iterator rit=deqIntA.rbegin(); rit!=deqIntA.rend(); ++rit){
cout<<*rit;
cout<<" ";
}
deque的赋值
- deque.assign(beg,end); //将[beg, end)区间中的数据拷贝赋值给本身。注意该区间是左闭右开的区间。
- deque.assign(n,elem); //将n个elem拷贝赋值给本身。
- deque& operator=(const deque &deq); //重载等号操作符
- deque.swap(deq); // 将deque与本身的元素互换
例如:
deque<int> deqIntA,deqIntB,deqIntC,deqIntD;
deque<int> deqIntA;
deqIntA.push_back(1);
deqIntA.push_back(2);
deqIntA.push_back(3);
deqIntA.push_back(4);
deqIntA.push_back(5);
deqIntB.assign(deqIntA.begin(),deqIntA.end()); // 1 2 3 4 5
deqIntC.assign(4,888); //888 888 888 888
deqIntD = deqIntA; //1 2 3 4 5
deqIntC.swap(deqIntD); //互换
deque的大小
deque.size(); //返回容器中元素的个数
deque.empty(); //判断容器是否为空
deque.resize(num); //重新指定容器的长度为num,若容器变长,则以默认值0填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
deque<int> deqIntA;
deqIntA.push_back(1);
deqIntA.push_back(2);
deqIntA.push_back(3);
deqIntA.push_back(4);
deqIntA.push_back(5);
int iSize = deqIntA.size(); //5
deqIntA.resize(7); //1 2 3 4 5 0 0
deqIntA.resize(8,1); //1 2 3 4 5 0 0 1
deqIntA.resize(2); //1 2
deque的插入
deque.insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据 的位置。
deque.insert(pos,n,elem); //在pos位置插入n个elem数据,无返回值。
deque.insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值
例如:
// demo 15-30 #include <deque> #include <iostream> using namespace std; int main(void){ deque<int> deqIntA; deque<int> deqIntB; deqIntA.push_back(1); deqIntA.push_back(2); deqIntA.push_back(3); deqIntA.push_back(4); deqIntB.push_back(11); deqIntB.push_back(12); deqIntB.push_back(13); deqIntB.push_back(14); deqIntA.insert(deqIntA.begin(), 0); // {0,1,2,3,4} deqIntA.insert(deqIntA.begin()+1, 2, 88); //{0,88,88,1,2,3,4} deqIntA.insert(deqIntA.begin(), deqIntB.rbegin(), deqIntB.rend());{11,12,13,14,0,88,88,1,2,3,4} for(deque<int>::iterator it = deqIntA.begin(); it!=deqIntA.end(); ++it){ cout<<*it; cout<<" "; } system("pause"); } |
deque的删除
- deque.clear(); //移除容器的所有数据
- deque.erase(beg,end); //删除[beg,end)区间的数据,返回下一个数据的位置。
- deque.erase(pos); //删除pos位置的数据,返回下一个数据的位置。
例如:
// demo 15-30-2 #include <deque> #include <iostream> using namespace std; int main(void){ deque<int> deqIntA;
deqIntA.push_back(1); deqIntA.push_back(2); deqIntA.push_back(3); deqIntA.push_back(4); deqIntA.push_back(5); //方式一 单独使用擦除的接口 //deqIntA.erase(deqIntA.begin()+1); //干掉第二个元素 {1,3,4,5} //deqIntA.erase(deqIntA.begin()+1, deqIntA.begin()+3);// 干掉3 和4, 剩下{1, 5} //deqIntA.clear(); //干掉所有的元素 //方式二 使用迭代器遍历删除 for(deque<int>::iterator it = deqIntA.begin(); it!=deqIntA.end();){ if(*it == 4){ it = deqIntA.erase(it); }else { cout<<*it; cout<<" "; it++; } } system("pause"); } |
4.1.3 List容器
List 容器概念
list是一个双向链表容器,可高效地进行插入删除元素。
|
List 特点:
- list不可以随机存取元素,所以不支持at.(position)函数与[]操作符。可以对其迭代器执行++,但是不能这样操作迭代器:it+3
- 使用时包含 #include <list>
list对象的默认构造
list同样采用模板类实现,对象的默认构造形式:list<T> listT; 如:
- list<int> lstInt; //定义一个存放int的list容器。
- list<float> lstFloat; //定义一个存放float的list容器。
- list<string> lstString; //定义一个存放string的list容器。
...
注意:尖括号内还可以设置指针类型或自定义类型。
list对象的带参数构造
方式一:list(beg,end); //将[beg, end)区间中的元素拷贝给本身。
方式二:list(n,elem); //构造函数将n个elem拷贝给本身。
方式三:list(const list &lst); //拷贝构造函数。
list<int> lstInt1;
lstInt1.push_back(1);
lstInt1.push_back(2);
lstInt1.push_back(3);
list<int> lstInt2(lstInt1.begin(),lstInt1.end()); //1 2 3
list<int> lstInt3(5,8); //8 8 8 8 8
list<int> lstInt4(lstIntA); //1 2 3
list头尾的添加移除操作
- list.push_back(elem); //在容器尾部加入一个元素
- list.pop_back(); //删除容器中最后一个元素
- list.push_front(elem); //在容器开头插入一个元素
- list.pop_front(); //从容器开头移除第一个元素
list<int> lstInt;
lstInt.push_back(1);
lstInt.push_back(2);
lstInt.push_back(3);
lstInt.push_back(4);
lstInt.push_back(5);
lstInt.pop_front();
lstInt.pop_front();
lstInt.push_front(11);
lstInt.push_front(12);
lstInt.pop_back();
lstInt.pop_back();
// lstInt {12, 11, 3}
list的数据存取
- list.front(); //返回第一个元素。
- list.back(); //返回最后一个元素。
list<int> lstInt;
lstInt.push_back(1);
lstInt.push_back(2);
lstInt.push_back(3);
lstInt.push_back(4);
lstInt.push_back(5);
int iFront = lstInt.front(); //1
int iBack = lstInt.back(); //5
lstInt.front() = 11; //11
lstInt.back() = 19; //19
list与迭代器
- list.begin(); //返回容器中第一个元素的迭代器。
- list.end(); //返回容器中最后一个元素之后的迭代器。
- list.rbegin(); //返回容器中倒数第一个元素的迭代器。
- list.rend(); //返回容器中倒数最后一个元素的后面的迭代器。
- list.cbegin(); //返回容器中第一个元素的常量迭代器。
- list.cend(); //返回容器中最后一个元素之后的常量迭代器。
list<int> lstInt;
lstInt.push_back(1);
lstInt.push_back(3);
lstInt.push_back(5);
lstInt.push_back(7);
lstInt.push_back(9);
for (list<int>::iterator it=lstInt.begin(); it!=lstInt.end(); ++it)
{
cout << *it;
cout << " ";
}
for (list<int>::reverse_iterator rit=lstInt.rbegin(); rit!=lstInt.rend(); ++rit)
{
cout << *rit;
cout << " ";
}
list的赋值
- list.assign(beg,end); //将[beg, end)区间中的数据拷贝赋值给本身。
- list.assign(n,elem); //将n个elem拷贝赋值给本身。
- list& operator=(const list &lst); //重载等号操作符。
- list.swap(lst); // 将lst与本身的元素互换。
llist<int> lstIntA,lstIntB,lstIntC,lstIntD;
lstIntA.push_back(1);
lstIntA.push_back(3);
lstIntA.push_back(5);
lstIntA.push_back(7);
lstIntA.push_back(9);
lstIntB.assign(lstIntA.begin(),lstIntA.end()); //1 3 5 7 9
lstIntB.assign(++lstIntA.begin(),--lstIntA.end()); //3 5 7
lstIntC.assign(5,8); //8 8 8 8 8
lstIntD = lstIntA; //1 3 5 7 9
lstIntC.swap(lstIntD); //互换
list的大小
- ist.size(); //返回容器中元素的个数
- list.empty(); //判断容器是否为空
- list.resize(num); //重新指定容器的长度为num,若容器变长,则以默认值0填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
- list.resize(num, elem); //重新指定容器的长度为num,若容器变长,则以elem值填充新位置。如果容器变短,则末尾超出容器长度的元素被删除。
list<int> lstIntA;
lstIntA.push_back(1);
lstIntA.push_back(2);
lstIntA.push_back(3);
if (!lstIntA.empty())
{
int iSize = lstIntA.size(); //3
lstIntA.resize(5); //1 2 3 0 0
lstIntA.resize(7,1); //1 2 3 0 0 1 1
lstIntA.resize(5); //1 2 3 0 0
}
list的插入
- list.insert(pos,elem); //在pos位置插入一个elem元素的拷贝,返回新数据的位置。
- list.insert(pos,n,elem); //在pos位置插入n个elem数据,无返回值。
- list.insert(pos,beg,end); //在pos位置插入[beg,end)区间的数据,无返回值。
list<int> listA;
list<int> listB;
listA.push_back(1);
listA.push_back(2);
listA.push_back(3);
listA.push_back(4);
listA.push_back(5);
listB.push_back(11);
listB.push_back(12);
listB.push_back(13);
listB.push_back(14);
listA.insert(listA.begin(), -1); //{-1, 1, 2, 3, 4, 5}
listA.insert( ++listA.begin(), 2, -2); //{-1, -2, -2, 1, 2, 3, 4, 5}
listA.insert(listA.begin() , listB.begin() , listB.end()); //{11, 12, 13, 14, -1, -2, -2, 1, 2, 3, 4, 5}
for(list<int>::iterator it = listA.begin(); it!=listA.end(); it++){
cout<< *it<<endl;
}
list的删除
- list.clear(); //移除容器的所有数据
- list.erase(beg,end); //删除[beg,end)区间的数据,返回下一个数据的位置。
- list.erase(pos); //删除pos位置的数据,返回下一个数据的位置。
- lst.remove(elem); //删除容器中所有与elem值匹配的元素。
// demo 15-32 #include <list> #include <vector> #include <iostream> using namespace std; int main(void){ //list 删除元素 list<int> listA; listA.push_back(1); listA.push_back(2); listA.push_back(3); listA.push_back(4); listA.push_back(5); //erase 的用法 list<int>::iterator itBegin=listA.begin(); ++ itBegin; list<int>::iterator itEnd=listA.begin(); ++ itEnd; ++ itEnd; ++ itEnd; listA.erase(itBegin,itEnd);//此时容器lstInt包含按顺序的1, 4, 5三个元素。 listA.erase(listA.begin());//此时容器lstInt包含按顺序的4, 5三个元素。 listA.push_back(4); // 4, 5, 4 listA.insert(listA.end(), 5, 4); //4, 5, 4, 4, 4, 4, 4, 4 /*remove 删除元素*/ //方式一 直接调用remove 方法 //listA.remove(4); //方式二 遍历然后逐个删除 for(list<int>::iterator it=listA.begin(); it!=listA.end(); ){ if(*it == 4){ it =listA.erase(it); //相当于执行了++ }else { it++; } } for (list<int>::iterator it=listA.begin(); it!=listA.end(); ++it) { cout << *it; cout << " "; }
system("pause"); return 0; } |
list的反序排列
- list.reverse(); //反转链表,比如list包含1, 2, 3, 4, 5五个元素,运行此方
法后,list就包含5, 4, 3, 2, 1元素。
list<int> listA;
listA.push_back(1);
listA.push_back(2);
listA.push_back(3);
listA.push_back(4);
listA.push_back(5);
listA.reverse(); //5, 4, 3, 2, 1
4.1.4 C++11新特性 变参模板、完美转发和emplace
变参模板 - 使得 emplace 可以接受任意参数,这样就可以适用于任意对象的构建
完美转发 - 使得接收下来的参数 能够原样的传递给对象的构造函数,这带来另一个方便性
// demo 15-33 #include <iostream> using namespace std; #include <vector> #include <list> #include <deque> #include <algorithm> class student { public: student() { cout << "无参构造函数被调用!" << endl; } student(int age, string name, int test) { this->age = age; //strncpy_s(this->name, name, 64); cout << "有参构造函数被调用!" << endl; cout << "姓名:" << name.c_str() << " 年龄:" << age << endl; } student(const student &s) { this->age = s.age; //strncpy_s(this->name, s.name, 64); cout << "拷贝构造函数被调用!" << endl; } ~student() { cout << "析构函数被调用" << endl; } public: int age; string name; }; int main(void) { //vector<int> vectInt(10); deque<int> dqInt; list<int> lstInt; vector<student> vectStu(10); cout << "vectStu size:" << vectStu.size() << endl; cout << "vectStu capacity:" << vectStu.capacity() << endl; //插入学生 //方法一 先定义对象,再插入 //student xiaoHua(18, "李校花"); //vectStu.push_back(xiaoHua); //方法二 直接插入临时对象 //vectStu.push_back(student(19, "王大锤")); //c++11 新特性: 变参模板和完美转发的表演啦 vectStu.emplace_back(19, "王大锤", 11); //push_back cout << "vectStu size (1):" << vectStu.size() << endl; cout << "vectStu capacity(1):" << vectStu.capacity() << endl; vectStu.emplace(vectStu.end(), 18, "lixiaohua", 12); //相当于 insert. cout << "vectStu size (2):" << vectStu.size() << endl; cout << "vectStu capacity (2):" << vectStu.capacity() << endl; system("pause"); return 0; } |