c++总结(updating)

c++总结笔记

基础知识与简单程序设计

词法记号

  • 关键字:c++预定义的单词

  • 标识符:程序员声明的单词,它命名程序正文中的一些实体

  • 文字:在程序中直接使用符号表示的数据

  • 操作符:用于实现各种运算的符号

  • 分隔符:(){},:;

  • 空白符:空格、制表符、垂直制表符、换行符、回车符、注释

关键字

alignas alignof asm auto bool break case catchchar char16_t char32_t 
class const constexpr const_cast continue decltype default delete do 
double dynamic_cast else enum explicit export extern false float for 
friend goto if inline int long mutable namespace new noexcept nullptr 
operator private proteceted public reinterpret_cast return short signed 
sizeof static static_cast struct switch template this thread_local throw 
true try typedef typeid typename union unsigned using virtual void volatile 
wchar_t while

数据

基本数据类型:整数、浮点数、字符、布尔

程序中的数据:

  • 常量:不可改变值的量

  • 变量:允许改变的量

  • 文字常量:如1,2,‘a’等不可改变的

初始化

int a = 0;
int a(0);
int a = {0};
int a{0};

符号常量

const 加常量值
constexpr 加常量表达式

类型别名

typedef int Nautural
using area = double;

分别表示Naturalint的别名,areadouble的别名

头文件

​ 头文件中含有c++自带的一些函数功能,这些函数实用但实现难度高,包含在c++自带头文件中

​ 同时,也可将自写的".h"文件引用到程序中,作为头文件

#include//万能头
#include//标准输入输出库
#include//数学库
#include//输入输出流
#include//算法库
#include"marx"//自写库

命名空间

命名空间的意义在于区分重名变量、函数,实现封装

namespace Segtree{
  query();
  update();
  ...
}

若想使用其中的函数或者变量,需要声明作用域,或用using语句

std::query();
using namespace Segtree;

数据类型及存储

在计算机中,我们一般通过二进制存储信息

位:

位(bit),即比特,用“b“来表示,表示一位二进制

字节:

字节(byte),用”B“来表示,是计算机中数据处理的基本单位,规定一个字节由八个二进制位构成

  • 八位二进制数可在十进制下表示0~255
原码、反码、补码:

在计算机中,正数的二进制最高位为0,负数则为1,以下原码、反码、补码的变换中均不涉及最高位的变化

对于正数来说,原码、反码、补码相同,都为其二进制本身

对于负数来说:

  • 原码:二进制位本身

  • 反码:按位取反

  • 补码:反码+1

如此,在实现负数运算时,便可用加法实现

如在最高位为第四位的基础下:6,-6的原码补码分别为:

6的原码、反码、补码:0110

-6的原码:1110 反码:1001 补码:1010

而6的原码和-6的反码相加后等于10000,由于最高位是第四位,第五位的1被舍去,即为0

数据类型

c++中的数据类型分为两种:预定义类型和自定义数据类型

预定义类型:整型、字符型、布尔型、浮点型、空类型、指针类型

自定义类型:数组、结构体、联合体、枚举

​ 类型修饰符:signedunsignedshortlong

所占字节

​ 整型:

  • short(短整型),2字节;

  • int(整型),4字节;

  • long(长整型),4字节

  • long long(长长整型),8字节

​ 整型用来表示一个整数

​ 浮点型:

  • float(单精度浮点型),占4字节

  • double(双精度浮点型),占8字节

  • long double(长精度浮点型),占8字节(有些编译器中占用10、12字节)

字符型:

  • char(字符型),占1字节

与ASCII码一一对应

布尔类型:

  • bool(逻辑性变量的定义符),占1字节
占位符

在输入输出函数中,我们可能需要用到占位符,会在稍后讲到

占位符 含义
%d int
%ld long
%f float
%lf double
%p 地址
%x/%X 十六进制
%o 八进制
%s 字符串
%u 无符号十进制整数
%e/%E 科学计数法
其他:auto、delctype、enum、union、struct

auto:可自动判断变量的类型

delctype:可以定义与某一变量相同类型的变量

decltype(i) j = 5;

enum:枚举类型,定义格式为

enum<类型名>{<枚举常量表>};
enum week{Sun,Mon,Tue,Wed,Thu,Fri,Sat};

对于上述 w e e k week week枚举, S u n = 0 Sun=0 Sun=0,且此后依次递增

enum week{Sun = 7,Mon = 1,Tue,Wed,Thu,Fri,Sat};

此时, T u e Tue Tue从2开始依次递增

枚举成员只能以标识符形式,不能是常量

枚举可增加程序的可读性,也可以用来描述状态量

例如:

enum weekday{mon,tue,wed,thur,fri,sat,sun};
enum weekday day;
day = mon;
//等价于int day = 0;

通常,枚举类型和 s w i t c h switch switch等语句一起使用

union:联合体类型

联合体中有多个成员,且对一个成员的修改可影响到其他成员

union ascii{
  char ch;
  int number;
}
ascii a;
a.ch = 'A';
//此时,a.number = 65
a.number = 66
//此时,a.ch = 'B'

同时,一个联合体所占的内存为成员字节数最大值

struct:结构体类型

结构体中可定义其他数据类型,以及函数

在c++中,class可替代结构体,将在后续详细讲解

算术运算符和算术表达式

基本算术运算符
  • + - * / (若整数相除,结果取整)%(取余)

  • 优先级和结合性

    先乘除,后加减,同级自左至右

  • ++,–(自增,自减)

赋值运算符和复制表达式

赋值符号=,将右侧的值赋给左侧

n = n + 5; n n n增大5

逗号运算和逗号表达式
  • 格式

    表达式1,表达式2

  • 求解顺序及结构

    先求表达式1,再求解表达式2,最终结果为表达式2的值

a = 3*5 , a*4;最终结果为60

关系运算和关系表达式
  • 关系运算是比较简单的逻辑运算,优先次序为:

< <= > >= 高优先级 == != 低优先级

  • 关系表达式的结果类型为bool,值只能为truefalse
逻辑运算与逻辑运算符
  • 逻辑运算符

    !(非) &&(与)||(或)

    优先级由高到低

  • 逻辑表达式

    (a>b)&&(x>y)

    其结果类型为bool

条件运算符与条件表达式
  • 格式

    表达式1?表达式2:表达式3

  • 执行顺序:

    • 先求解表达式1
    • 若为真,则求解表达式2
    • 若为假,则求解表达式3
  • 例子: x = a > b ? a : b ;

位运算
  • 按位与&
  • 按位或|
  • 按位异或^
  • 按位取反~
  • 位移<< >>

I/O流以及输入输出

I/O流

在c++中,将数据从一个对象到另一个对象的流动抽象为“流” 流在使用前要被建立,使用后要被删除

从流中获取数据的操作称为提取操作,向流中添加数据的操作称为插入操作

数据的输入与输出是通过I/O流来实现的,cin和cout是预定于的流类对象 cin用来处理标准输入,即键盘输入,cout用来处理标准输出,即屏幕输出

<<是预定义的插入符,作用在流类对象cout上便可实现标准输出设备输出,标准输入是将提取符>>作用在流类对象cin上

int x;
cin >> x;//键盘输入x
cout << x << endl;//屏幕输出x的值,并换行
cout << "x" << ends;//屏幕输出x字符,并空格

而对于cout,有许多流操作符,可控制输出格式

操作符名 含义
dec 以十进制表示
hex 以十六进制表示
oct 以八进制表示
ws 提取空白符
endl 插入换行符,并刷新流
ends 插入空字符
setprecision(int) 设置浮点数的小数位数
set(int) 设置域宽
输入输出函数
  • getchar()putchar(),可读取、输出一个字符

  • gets()puts(),可读取、输出一个字符串,读到空格或者换行时终止

  • print(),括号内引号中写输出的字符串及变量,若想输出变量,则在输出的位置用占位符代替,并在引号外按顺序注明变量

  • scanf(),括号内引号中写入占位符,与输入变量一致,引号外注明变量地址

int x;
scanf("%d",&x);
printf("%d",x);

在输出中,我们可以用转义字符来控制格式,转义字符以反斜线"\"开头,后跟一个或几个字符。转义字符具有特定的含义,不同于字符原有的意义,故称“转义”字符

转义符 字符名
单引号
" 双引号
\ 反斜杠
\0 空格
\a 感叹号
\b 退格
\f 换页
\n 换行
\r 回车
\t 水平tab
\v 垂直tab
printf("\",\\,\0,\n1");
//"\ 
//1

程序执行流程的控制

程序的执行流程不总是顺序的,因此

  • 程序能够对执行流程进行选择
  • 程序要能够反复进行

条件语句

if语句
if(表达式){
  
}else if(表达式){
  
}else{
  
}
  • 表达式若位真,则执行大括号中的程序
  • 若为假,则继续判断下一个else if中的表达式是否成立
  • 若都不成立,执行else中的程序
  • 必须有ifelse ifelse不必需
  • 若表达式中值非false,则会被转换成true
switch语句
switch(表达式){
  case 常量表达式:语句
    break;
  case 常量表达式:语句
    break;
  default:语句
}
  • 根据表达式的值,进入相应的case并执行语句
  • 若都不符合,则进入default

循环语句

while语句
while(表达式){
  语句
}
  • 先判断表达式是否为真,若是,则执行语句,若不是,则跳出循环
  • 执行完语句后反复执行上过程
do while语句
do{
  语句
}while(表达式)
  • 先执行一遍语句,然后同while一样
for语句
for(语句1;表达式;语句2){
  语句3
}
  • 先执行语句1,再判断表达式,再执行语句3,再执行语句2,之后除语句1外循环执行

  • 同时,c++允许将迭代器遍历容器简化成范围循环:专门针对容器类型的for循环,使用冒号``:```语句来迭代容器中的每一个元素

    int a[] = {1,2,3};
    for(const auto &it:a){
      cout << it << ends;
    }
    

其他控制语句

goto语句
goto 标识符;
语句
标识符:
  • 当代码运行到标识符时,跳转至标识符:
  • 非必要时不使用goto

函数

在程序设计中,我们可能会多次使用一些功能,为了在减少重复代码的同时增加程序的泛用性,我们引入函数

声明以及定义

声明

类型标识符 函数名(形式参数表);
  • 在声明后,函数才可以调用

定义

类型标识符 函数名(形式参数表){
  语句
}
  • 定义是对函数具体的描述,声明后必须定义

函数的调用

函数名(实际参数表);

根据函数的类型不同,调用函数可有不同作用

  • void无返回值类型函数
  • int等其他数据类型函数,可返回同样数据类型的对象,可作为右值

参数和初始化

  • 在函数定义中的为形式参数

  • 在程序运行中定义的,为实际参数

  • 在一般情况下,函数中对形式参数的值修改不会影响到实际参数,即单向传递,如下

    void swap(int x,int y){
      int t = x;
      x = y;
      y = t;
    }
    

    该函数不会真正交换xy的值,若想改变则需要传引用或传指针,稍后再详细介绍

  • 形参被调用后才被创建

  • 默认参数

    我们可以在定义函数的时候定义参数的默认值

    int Add(int x = 0,int y = 1){
      return x + y;
    }
    int Z = Add();
    
    • 此时,由于在调用时没有写进参数,程序会使用默认参数
    • 对于默认参数的定义,需从右往左,而调用函数时的实际参数会从左至右赋给形式参数
  • 冒号表达式可以进行函数的赋值语句,多用于类的构造函数中常量的赋值

    class Point{
      Point(int x,int y,int m);
      private:
      int x,y;
      const int m;
    }
    Point::Point(int x,int y,int m):x(x),y(y),m(m){}
    

    关于构造函数的详细知识将在讲解类时详细说明

函数的嵌套与递归

  • 函数的嵌套:在函数中调用其他函数

    void func1();
    void func2(){
      func1();
    }
    void func1(){
      
    }
    

    注意:在嵌套前需要声明函数

  • 函数的递归:在函数中调用本身

    int solve(){
      return solve();
    }
    

    注意:递归需设立递归边界,并注意栈空间

内联函数

  • 内联函数是指在调用函数时,将调用处替换为函数体,节省了参数传递、控制转移等的开销花费
  • 将函数定义为内联形式,只需要在函数前加inline
  • 若函数中有递归、循环,则不可用内联函数
  • 内联函数的定义必须出现在第一次调用前
  • 对内联函数不能进行异常接口声明
  • 大部分编译器会讲有条件成为内联函数的函数传化成内联函数以提高效率

函数的重载

  • 对于功能相近的函数,其函数名往往相同。而c++允许同名的函数在相同的函数名声明,只要有不同的参数表即可

    int add(int x,int y){
      return x+y;
    }
    int add(int x,int y,int z){
      return x + y + z;
    }
    double add(double x,double y){
      return x + y;
    }
    

    以上三个函数可同时存在,在使用时编译器会通过参数自动选择调用相对应的函数

  • 注意:重载函数的形参个数或者类型必须不同,名称不同,否则无法重载,如下面的反例

    int add(int x,int y);
    int add(int a,int b);
    //无法实现重载
    
  • 同样,如果参数表相同而函数类型不同,也无法进行重载

    int add(int x,int y);
    void add(int x,int y);
    //无法进行重载
    

类和对象

类是指一类事物的抽象化,而对象是指某一类的特定事物

类的一般格式

class 类名{
  public:
  
  private:
  
  protected:

}
  • 在类中,我们不仅可以定义变量,也可以定义函数

  • 其中,privateproteceted中的对象无法被外部直接调用,需要通过外部接口public访问,例如

    class Point{
      public:
      void setPoint(int x,int y);
      void printPoint(int x,int y);
      private:
      int x,y;
    }
    

    在该类中,x,y无法直接操作,而我们可以通过public中的函数对它们进行操作

  • 值得注意的是,类的函数有一个隐藏的形参,指针this,存储着该对象的地址,同时,该对象中的变量可在函数中直接被调用

  • 在类外定义函数的时候,需要加上作用域,作用域的名称为类名

    Point::setPoint(int xx,int yy){
      	xx = x;
      	yy = y;
    }
    

    也可以用this指针实现该函数

    Point::setPoint(int x,int y){
      this -> x = x;
      this -> y = y;
    }
    

构造函数、析构函数

对于类,有两种特殊的函数,构造函数和析构函数,用于类的初始化和删除

构造函数
  • 对于构造函数,函数名必须和类名相同,并且不可定义返回值,可以重载,可以使用默认参数,如对上述Point类,我们可以不使用setPoint函数,而使用构造函数

    class Point{  
      public:  Point(int x,int y); 
      void printPoint(int x,int y);  
      private:  int x,y;
    }
    Point::Point(int x,int y):x(x),y(y){}
    
  • 如此,我们就可以在声明对象的时候直接进行初始化

  • Point p(1,1);
    

    而如果没有定义构造函数,系统则会调用默认构造函数,其定义如下:

  • Point(){}
    
  • 值得注意的是,若定义了构造函数,则无法调用默认构造函数

委托构造函数
  • 类中往往有多个构造函数,只是参数表和初始化方式不同,为了避免重复,可以通过一个构造函数定义另一个,形成委托构造函数

     Point::Point(int x,int y):this->x(x),this->y(y){}
     Point::Point(){Point(0,0);}
    
复制构造函数
  • 如果希望用某个已有的对象来初始化另一个对象,则可以使用复制构造函数,格式如下

    Point(const Point&p){
      x = p.x;
      y = p.y;
    }
    Point x(1,1);
    Point p(x);
    
  • 上述代码实现了在创建p时用x的值复制创建

  • 值得注意的是,形参使用了常引用,可在确保安全性的同时提高效率,这点将在之后详细说明

移动构造函数
  • 复制构造函数通过复制的方式构造新的对象,而很多时候被复制的对象仅作复制之用后销毁,在这时,如果使用移动已有对象而非复制对象将大大提高性能。c++11标准引入了左值和右值,定义了右值引用的概念,以表明被引用对象在使用后被销毁,不会再继续使用

  • 直观来看,左值是位于赋值语句左侧的对象变量, 右值是赋值语句右侧的值,不依附于对象,在函数章节中对持久存在的变量的引用,称之为左值引用,相对的对短暂存在可被移动的右值的引用称为右值引用。因此,可通过移动右值引用对象来安全地构建新对象,并且避免冗余复制对象的代价

    float n = 6;
    float &lr_n = n;
    //float &&rr_n = n;
    float &&rr_n = n * n;
    //float &lr_n = n * n;
    

    注意一个左值对象不能绑定到一个右值引用上,但实际应用中,可能某个对象的作用仅限在初始化其他新对象使用后销毁,标准库utility中提供了move函数,将左值对象移动成为右值

    float n = 10;
    float &&rr_n = std::move(n);
    

    move函数告诉编译器变量n转换为当右值来使用,承诺除对n重新赋值或者销毁它以外,将不再通过rr_n右值引用以外的方式使用它

    基于右值引用的新设定,可以通过移动而不复制实参的高性能方式构建新对象,即移动构造函数。类似于复制构造函数,移动构造函数的参数为该类对象的右值引用,在构造中移动源对象资源,构造后源对象不再指向被移动的资源,源对象可重新赋值或者被销毁

    class MyStr{
      public:
      	string s;
      	
      	MyStr():s(""){};
      
      	MyStr(string_s) : s(std::move(_s)) {};
      	MyStr(MyStr &&str) noexcept
          	:s(std::move(str.s)){}
    };
    
析构函数
  • 析构函数会在对象消亡时自动调用,格式如下

    ~Point(){}
    
  • 默认析构函数与默认构造函数相同,语句为空

  • 注意,析构函数的参数表必须为空

  • 对于析构函数的应用将在“数据的共享与保护”处说明

类的组合

  • 类中的成员可以是另一个类的对象,从而实现更抽象的类

  • 例如,我们可以用上述Point类来定义一个Line

    class Line{
      Line(Point p1,Point p2);
      Line(const Line&l);
      private:
      Point a,b;
    }
    Line::Line(Point p1,Point p2){
    	a = p1;
      b = p2;
    }
    Line::Line(const &l){
      a = l.a;
      b = l.b
    }
    

数据共享和保护

作用域

  • 作用域:作用域是指一个标识符在程序正文中有效的区域
  • 可分为:函数原型、局部作用域、类作用域、命名空间作用域、限定作用域的枚举类
函数原型作用域
  • 函数原型中的参数,其作用域始于(,结束于)

  • 例如:

    double area(double r);
    

    其中,r只作用于()

局部作用域
  • 函数的形参,在块中(大括号)声明的标识符,其作用域自声明处起,限于块中

    void solve(int a){
      if(...){
        int b;
        for(...){
          int c;
          // c ends
        }
        // b ends
      }
      // a ends
    }
    

    这种变量也被称作局部变量

类作用域
  • 类作用域是指类体和非内联成员函数的函数题
  • 如果在类作用域外访问类的成员,要通过类名(静态),或者该类的对象名、对象引用、对象指针(非静态)
命名空间作用域
  • 见上“命名空间”
  • 特殊的命名空间:
    • 全局命名空间,即默认命名空间
    • 匿名命名空间:对每个源文件是唯一的,在namespace后不标注名字即可,对其他文件不显示
限定作用域的枚举类
  • 不限定作用域的枚举类,即enum color{red,yellow,green};

    此时,无法定义枚举名不同,而枚举成员相同的枚举类型

  • 而对于enum class color2{red,yellow,green};由于限定了作用域,不会造成定义重复

    color c = red;
    color2 c2 = color2::red;
    

可见性

  • 可见性是从对标识符的引用角度来谈的,表示从内层作用域向外层作用域能“看”到什么

  • 如果标识符在某处可见,就可以在该处引用此标识符

  • 如果某个标识符在外层中声明,且在内层中没有同一标识符声明,则该标识符在内层可见

  • 对于两个嵌套的作用域,如果在内层作用域声明了与外层作用域同名的标识符,则外层作用域的标识符在内层不可见

    int i;
    int main(){
      i = 5;
      {
        int i;
        i = 7;
        cout << i << endl;
      }
      cout << i << endl;
    }
    //7 5
    

生存期

  • 对象从产生到结束的这段时间就是生存期,在对象生存期内,对象将保持它的值,直到被更新

  • 静态生存期:与程序的运行周期相同,在文件作用域中声明的对象具有这种周期(全局变量),如果在函数内部声明静态生存期对象,要冠以static

  • 动态生存期:块区域中声明,运行至其作用域外失去寿命

    int i = 1;//静态全局变量,有全局寿命
    void other(){
      static int a = 2;
      static int b ;
      int c = 10;
      a += 2; i += 32; c += 5;
      cout << "-----Other-----"<< endl;
      cout << "i:" << i << "a:" << a << "b:" << b << "c: "<< c << endl;
      b = a;
    }
    int main(){
      static int a;//静态局部变量,有全局寿命,局部可见
      int b = -10;
      int c = 0;
    	cout << "---Main---\n";
      cout << "i:" << i << "a:" << a << "b:" << b << "c: "<< c << endl;
    	c+=8;
      other();
      cout <<"---Main---\n";
      cout << "i:" << i << "a:" << a << "b:" << b << "c: "<< c << endl;
    	i+=10;
      other();
      return 0;
    }
    /*
    ---Main---
     i:1 a:0 b:-10 c: 0
    -----Other-----
     i:33 a:4 b:0 c: 15
    ---Main---
     i:33 a:0 b:-10 c: 8
    -----Other-----
     i:75 a:6 b:4 c: 15
    */
    

    初始化只会对未诞生的对象进行,对于函数other,在第一次调用后,由于函数中的a仍有寿命,所以不会重复初始化将其赋值为2

对象间的共享

同类对象数据共享
  • 同类对象数据共享,可用静态数据成员

  • 静态数据成员用关键字static声明,为该类的所有对象共享,静态数据成员具有静态生存期,一般在类外初始化,用::来指明所属的类

  • c++11支持静态常量(constconstexpr修饰)类内初始化,此时类外仍可定义该静态成员,但不可再次初始化操作

    class Point{
      public:
      	Point(int x = 0,int y = 0):x(x),y(y){
          count ++;
        }
      Point(Point &p){
        x = p.x;
        y = p.y;
        count ++;
      }
      ~Point(){count--;}
      int getx(){return x;}
      int gety(){return y;}
      void showCount(){
        cout << "count = " << count << endl;
      }
      private:
      int x,y;
      static int count;
    };
    int Point::count = 0;
    int main(){
      Point a(4,5);
      cout << "a :" << a.getx() << "," << a.gety() << endl;
      a.showCount();
      Point b(a);
      cout << "b :" << b.getx() << "," << b.gety() << endl;
      b.showCount();
      return 0;
    }
    //a: 4,5
    //1
    //b: 4,5
    //2
    
同类对象函数共享
  • 在刚才的代码中,我们发现如此只能实现通过对象来访问showCount函数,十分不合理,所以我们引入静态函数来实现同类对象函数共享

    static void showCount(){
      cout << count << endl;
    }
    

    值得注意的是,静态函数不属于任何对象,不存在this指针

类与外部数据共享
  • 在实现外部函数的时候,我们常常需要类中的私有成员,若是仅仅通过外部接口来实现会减慢程序的运行效率,所以我们引入友元

  • 友元是一种破坏封装和隐藏的机制,通过将一个模块声明为另一个模块的友元,一个模块能够引用到另一个模块中本身被隐藏的信息

  • 可使用友元函数和友元类

  • 为了确保封装和隐藏,应少使用友元

    class Point{
      public:
      ...
      friend float dist(Point &a,Point &b){ //引用可节省空间
       		double x = a.x - b.x;
        	double y = a.y - b.y;
        	return static_cast<float>(sqrt(x*x+y*y));//强制类型转换
      private:
        int x,y;
      }
    }
    

    以上为友元函数的实例

    class A{
      friend class B;
      public:
      void display(){
        cout << x << endl;
      }
      private:
      int x;
    }
    class B{
      public:
      void set(int i);
      void display();
      private:
      A a;
    }
    void B::set(int i){
      a.x = i;
    }
    void B::display(){
      a.display();
    }
    

    以上为友元类的实例

  • 值得注意的是,友元仅仅是单向的

数据的保护

  • 对于需要共享, 有需要防止改变的数据,应该声明为常类型,不改变对象类型的应定义为常函数

    class A{
      public:
      A(int i,int j){x=i;y=j};
      private:
      int x,y;
    };
    A const a(3,4);
    
  • 若想以常量为参数,可在参数中标明常量

  • 对于常量对象,可调用常函数

    void A::print()const{
      cout << x << endl << y << endl;
    }
    

    注意,常函数无法改变对象中的值

  • 常引用:声明引用的同时加const修饰,即常引用,该引用在函数中无法被更新

  • 常引用的目的在于可以在节省空间的同时保障数据的安全性。即引用可避免形式参数的新建,对于某些内存占用较大的对象,新建会占用很多不必要的内层,所以用引用可避免,同时定义为常量可防止在函数中对其进行修改

指针、数组、字符串

数组

  • 数组是具有一定顺序关系的若干相同类型变量,数组的变量称为该数组的元素

  • 数组的定义:

    类型说明符 数组名[常量表达式][常量表达式]...;
    

    int a[10],表示a为整形数组,有十个元素,a[0],a[1]...,a[9]

  • 数组的使用

    • 数组必须先定义,后使用
    • 可以逐个引用数组元素
  • 数组的遍历

    for(const auto &e:a){
      cout << e << ends;
    }
    

    通过范围for循环来实现

    for(int i=0;i<len;i++){
      cout << a[i] << ends;
    }
    

    通过访问下标来实现

一维数组的储存

数组元素在内存中顺次存放,它们的地址是连续的。元素间物理地址上的相邻,对应着逻辑次序上的相邻

  • 数组名字是数组首元素的内存地址
  • 数组名是一个常量,不能被赋值
一维数组的初始化
  • 列出全部元素的初始值

    static int a[10] = {0,1,2,3,4,5,6,7,8,9};
    
  • 可以只给一部分元素指定初值

    int a[10] = {0,1,2,3,4};
    
  • 在列出全部数组元素初值时,可以不指定数组长度

    static int a[] = {0,1,2,3,4,5,6,7,8,9};
    
二维数组的存储
  • 按行存放

    int a[3][4]

    a[0]存放着第一行的首地址,即a[0][0]的地址,同理,a[1],a[2]也存放着首地址

  • 数组的存储顺序为

    a[0][0],a[0][1],a[0][2],a[0][3],a[1][0],a[1][1],a[1][2],a[1][3],a[2][0],a[2][1],a[2][2],a[2][3]

二维数组的初始化
  • 将所有的初值写在一个{}中,按顺序初始化

    int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
    
  • 分行列出二维数组的初值

    int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
    
  • 可以只对部分元素初始化

    int a[3][4] = {{1},{0,6},{0,0,11}};
    
  • 列出全部的初始值,第一位下标个数可以省略

    int a[][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
    int a[][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
    
数组作为函数参数
  • 使用数组名做函数的参数,则实参和形参都应该是数组名,且类型要相同。和普通变量做参数不同,使用数组名传递数据的时候,传递的是地址。形参数组和实参数组的首地址重合,后面的元素按照各自在内存中的存储顺序进行对应,对应元素使用相同的数据储存地址,因此实参数组的元素个数不应少于形参数组的元素个数。如果在被调函数中对形参数组元素值进行修改,主调函数中实参数组相应元素也会改变
  • 把数组作为参数时,一般不指定数组第一维的大小,即使指定,也会被忽略

指针

  • 内存空间的访问方式分为两种,一种是通过变量名,一种是通过地址,地址即是变量、函数等存放的位置

  • 指针变量的声明

    数据类型 *标识符;
    int *ptr;
    
  • 指针运算可通过&,*来实现,&为取地址符,*为解引用符

    • 取地址符可获取某一变量的地址,如

      int x;
      cout << &x << endl;
      

      可输出x的地址

    • 解引用符可获得地址所存储的值

      int *p = &x;
      *p = 1;
      

      可将x的值改为1

    • 值得注意的是,在声明语句中,&表示引用,*表示指针

  • 指针的赋值

    • 可在声明的时候进行初始化赋值

      int *ptr = nullptr;
      

      空指针也可赋为0,也可为nullptr

      对于无初值的指针,应赋为空

    • 数组名实际上是一个不能被赋值的指针,即指针常量

      int a[10];
      int *ptr = a;
      
    • 可以声明指向常量的指针

      int a;
      const int *p1 = &a;
      int b;
      p1 = &b;
      *p1 = 1;//编译错误
      

      上述代码中,最后的语句,*p1 = 1;不合法,因为p1的类型为指向常量的指针,无法通过该指针改变变量的值,而该指针可以修改指向的对象

    • 可以声明指针类型的常量,此时指针本身的值不能被改变

      int *const p2 = &a;
      p2 = &b;//编译错误
      

      指针指向的对象无法被改变

    • 可以定义void类型的指针,可以存储任何类型的对象地址

  • 指针运算

    • 指针可以进行和整数的加减运算

      p1+x 代表p1向后移动x个数的地址,减法同理。同时,也可进行自加与自减操作

    • *(p1+x)表示p1x位的内容,也可简写成p1[x]

  • 用指针处理数组元素

    • 数组的加减运算的特点使得其十分适合处理一段连续内存空间中的同类数据。而数组恰好符合此特,而数组名就是数组存储的首地址,这样,我们就可以利用指针进行对数组快速高效的访问

    • 把数组作为函数的形参,等价于把指向数组元素类型的指针作为形参,如下列三种写法是等价的

      void f(int p[]);
      void f(int p[3]);
      void f(int *p);
      
    • c++标准库引入了begin函数和end函数用于获取数组的首地址和尾地址

      int a[10];
      int i = 0;
      for(int *p1 = begin(a);p1<=end(a);p1++){
        *p1 = i;
        i++;
      }
      
  • 指针数组

    • 如果一个数组的每个元素都是指针变量,那么这个数组就是指针数组,指针数组的每个元素都必须是同一类型的指针

    • 声明一维指针数组的语法形式为

      数据类型 *数组名[下标表示式]

      例:用指针存储单位矩阵

      int line1[] = {1,0,0};
      int line2[] = {0,1,0};
      int line3[] = {0,0,1};
      int *pline[3] = {line1,line2,line3};
      
  • 用指针作为函数参数

    • 以指针作为参数有以下三个作用

      • 使形参和实参共用存储空间,进行双向传递
      • 减少函数调用时数据的开销
      • 通过指向函数的指针传递函数代码的手第一

      注:如果函数体不需要通过指针改变指针所指向的对象的内容,应在参数表中将其声明为指向常量的指针,这样使得常对象被取地址后也可作为该函数的参数

      在程序设计时,当某个函数中以指针或引用作为形参都可以达到同样目的,使用引用可使程序的可读性更好些

  • 指针型函数

    • 当一个函数的返回值是指针类型时,这个函数就是指针型函数,使用指针型函数的最主要目的就是要在函数结束时吧大量的数据从被调函数返回到主调函数中

    • 函数返回数组指针

      由于数组不能被复制,因此函数不能返回数组,但可以返回数组

      声明方法一:利用别名

      using arr = int[10];
      arr*func(int i);
      

      方法二:直接声明

      int (*foo(int i))[10];
      
      • foo(int i)定义了一个函数foo,需要一个int i的参数
      • (*foo(int i))意味着对函数返回的结果进行解析操作
      • (*foo(int i))[10]表示解析返回结果得到的是一个大小为10的数组
      • int (*foo(int i))[10]说明数组是int类型

      方法三:尾置类型

      auto foo(int i) -> int(*)[10];
      

      方法四:decltype声明

      int a[] = {0,1,2,3,4};
      int b[] = {5,6,7,8,9};
      decltype(a)*func(int i){
          return (i%2)?&a:&b;
      }
      int main(){
          for(int i=0;i<5;i++){
              cout << (*func(2))[i] << ends; 
          }
          return 0;
      }
      
  • 指向函数的指针

    • 同数组名一样,函数名也是表示函数的代码在内存中的起始地址

    • 定义形式:函数数据类型 (*函数名)(参数表)

    • 可实现相似函数的调用,减少代码重复性

      int compute(int a,int b,int (*func)(int,int)){return func(a,b);}
      int max(int a,int b){return (a>b)?a:b;}
      int sum(int a,int b){return a+b;}
      res = compute(1,2,&max);//2
      res = compute(2,3,&sum);//5
      
    • 指向常成员函数的指针在声明时应写出const关键字

动态内存分配

  • 虽然通过数组,可以对大量的数据和对象进行有效的管理,但很多情况下,在程序运行之前,并不能够准确的知道数组有多少个元素,于是我们可以使用newdelete来进行

  • ``new 类型名T(初始化参数)`,可以不进行初始化

  • 结果值:成功:指针;失败:抛出异常

  • 同时,也可以动态分配数组,new 类型名T[表达式][常量表达式]...()

    • 在程序执行期间,申请用于存放T类型对象数组的内存空间,()中必须为空
    • 同样,()也可以没有
    • 第一维可以是变量
  • 返回值同单个变量

    double *arr = new double[n];
    char (*fp)[3];
    fp = new char[n][3];
    
  • 释放内存操作符delete

    delete 指针p

  • 功能:释放指针p所指向的内存。p必须是new创建的

  • 为避免内存泄漏,new出的内存必须进行delete

  • delete只会删除内存分配的空间,不会删除指针本身

  • 对于类,new时调用构造函数,delete调用析构函数

  • 如果new时使用[]创建多维空间,则需delete []

  • 利用newdelete,我们可以实现一个动态内存分配的数组,或是可以使用 S T L STL STL中自带的vector

字符串

c语言风格字符串
  • 字符串常量的存储

    字符串常量是用一对双引号括起来的字符序列,每个字符占1字节,并在末尾添加\0作为结尾标记。这实际上是一个隐含创建的类型为char的数组,一个字符串常量就表示这样一个数组的首地址。因此,可以把字符串常量赋给字符串指针,由于常量值是不能改的,应将字符串常量赋给指向常量的指针,例如

    const char *string1 = "This is a string.";
    
  • 对于变量的存取, 可以通过上述方法进行,也可以通过char数组,并以\0结束

    char str[8] = {'p','r','o','g','r','a','m','\0'};
    char str[8] = "program";
    char str[] = "program";
    
string类

c++标准库将面向对象的串的概念加入到c++语言中,预定义了字符串类(string类),字符串类提供了对字符串进行处理需要的操作。使用string类需要包含头文件string

严格来说,string并非一个独立的类,而是类模板的一个特殊化实例

更多信息,可在STL中学习,此处省略

类的继承

基类与派生类

  • 类的继承和派生的层次结构,可以说是人们对自然界中的事物进行分类、分析和认识的过程在程序中的体现,现实世界中的事物都是相互联系、相互作用的,人们在认识过程中,根据其实际特征,抓住其共同特性和细小差别,利用分类的方法进行分析和描述。最高层是抽象程度最高的,是最具有普遍和一般意义的概念,下层具有了上层的特性,同时加入了自己的新特征,而最下层是最为具体的,上下层之间的关系就可以看作基类与派生类的关系

  • 类的继承,是新的类从已有类那里得到已有的特性,从已有类产生新类的过程就是类的派生,其中,原有的类称为基类或父类,产生的新类称为派生类或子类

  • 派生类的定义

    class 派生类名:继承方式 基类名1,继承方式 基类名2,···,继承方式 基类名n{
      派生类成员声明;
    };
    

    假如,基类Base1 Base2是已经定义的类,下面的语句定义了一个名为Derived的派生类,该类由上述两个基类派生而来

    class Derived : public Base1,private Base2{
      public:
      	Derived();
      	~Dervied();
    };
    
  • 吸收基类成员

    在c++的类继承中,第一步是将基类的成员全盘接收,这样,派生类实际上就包含了它的全部基类中除构造和析构函数之外的所有成员。

  • 改造基类成员

    如果派生类声明了一个和某基类成员同名的新成员,新生的新成员就隐藏或覆盖外层同名成员

  • 添加新的成员

    派生类增加新成员使派生类在功能上有所发展

不同继承方式及类成员的访问控制

  • 不同继承方式的影响体现在

    派生类成员对基类成员的访问权限

    通过派生类对象对基类成员的访问权限

  • 三种继承方式

    公有继承

    私有继承

    保护继承

公有继承
  • 当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问。

    也就是说基类的公有成员和保护成员被继承到派生类中访问属性不变,仍作为派生类的公有成员和保护成员,派生类的其他成员可以直接访问它们。在类族之外只能通过派生类的对象访问从基类继承的公有成员,而无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员

    class Point{
      public:
      	void initPoint(float x = 0,float y = 0){this -> x = x;this ->y = y;}
      	void move(float offX,float offY){x+=offX,y+=offY;}
      	float getX(){return x;}
      	float getY(){return y;}
      	private:
      		float x,y;
    };
    class Rectangle:public Point{
      public:
      	void initRectangle(float x,float y,float w,float h){
          initPoint(x,y);
          this->w = w;
          this->h = h;
        }
      	float getH()const {return h;}
     		float getW()const {return w;}
      private:
      	float w,h;
    };
    

    主函数部分

    int main(){
      Rectangle rect;
      rect.initRectangle(2,3,20,10);
      rect.move(3,2);
      cout << rect.getX() << ends << rect.getY();
      return 0;
    }
    //5,5
    
    私有继承
    • 当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问

    • 也就是说基类的公有和保护成员被继承后作为派生类的私有成员,派生类的其他成员可直接访问它们,但在类外通过派生类的对象无法直接访问它们。无论是派生类的成员还是通过派生类的对象,都无法直接访问从基类继承的私有成员

    • 经过私有继承之后,所有的基类成员都成为派生类的私有成员或不可直接访问的成员,如果进一步派生,基类的全部成员就无法在以后的派生类中直接发挥作用,实际是相当于终止了基类功能的继续派生,出于这种原因,一般情况下私有继承的使用比较少

    • 为了在私有继承的同时,保留一些基类的外部接口特征,则需要重新定义,如下

      class Rectangle:private Point{
        public:
        	void move(float offX,float offY){Point::move(offX,offY);}
        	...
      };
      

      注意,声明作用域

    保护继承
    • 保护继承中,基类的公有和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问
    • 这样,派生类的其他成员就可以直接访问从基类继承来的公有和保护成员,但在类外通过派生类的对象无法访问它们

    类型兼容规则

    类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类对象来替代。

    类型兼容规则中所指的替代包括以下情况:

    • 派生类的对象可以隐含转换为基类对象
    • 派生类的对象可以初始化基类的引用
    • 派生类的指针可以隐含转换为基类的指针

    在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员

    class B{...};
    class D:public B{...};
    B b1,*pb1;
    D d1;
    
    • 派生类对象可以隐含转换成基类对象,即用派生类对象中从基类继承来的成员,逐个赋值给基类对象的成员

      b1 = d1;
      
    • 派生类的对象也可以初始化基类对象的引用

      B &rb = d1;
      
    • 派生类对象的地址也可以隐含转换为指向基类的地址

      pb1 = &d1;
      

    由于类型兼容规则的引入,对于基类及其公有派生类的对象,可以使用相同的函数统一进行处理,因为当函数的形参为基类的对象(或引用、指针)时,实参可以是派生类的对象(或指针),而没有必要为每一个类设计单独的模块,大大提高了程序的效率。可以说,类型兼容规则是多态性的重要基础之一。以下是代码示例:

    class Base1{
      public:
      	void display()const {cout << "Base1::display()"<<endl;}
    };
    class Base2:public Base1{
      public:
      	void display() const{cout <<"Base2::display()"<<endl;} 
    };
    class Derived:public Base2{
    	public:
      	void display() const{cout <<"Derived::display()"<<endl;}
    };
    void func(Base1 *ptr){
    	ptr->display();
    }
    int main(){
      Base1 b1;
      Base2 b2;
      Derived d;
    
      func(&b1);
      func(&b2);
      func(&d);
      
      return 0;
    }
    
    // B1::display();
    // B1::display();
    // B1::display();
    

派生类的构造和析构函数

由于基类的构造函数和析构函数不能被继承,如果在派生类中想要对新增的成员进行初始化,就必须为派生类添加新的构造函数。但是派生类的构造函数只负责对派生类新增的成员进行初始化,对所有从基类继承下来成员,其初始化的工作还是由基类的构造函数完成,析构函数同理

构造函数

派生类对于基类的很多成员对象是不能直接访问的,因此要完成对基类成员对象的初始化工作,需要通过调用基类的构造函数。派生类的构造函数需要以合适的初值作为参数,其中一些参数要传递给基类的构造函数,用于初始化相应的成员。在构造派生类的对象时,会首先调用基类的构造函数,来初始化它们的数据成员,然后按照构造函数初始化列表中指定的方式初始化派生类新增的成员对象,最后才执行派生类构造函数的函数体

派生类名::派生类名(参数表):基类1(基类1初始化参数表),...,基类n(基类n初始化参数表),成员对象1(成员对象1初始化参数表),...,成员对象m(成员对象m初始化参数表),基本类成员初始化{
派生类构造函数的其他初始化操作;
}

对于使用默认构造函数的基类、成员对象,可以不给出类名

class Derived:public Base1,public Base2,public Base3{
public:
	Derived(int a,int b,int c,int d):Base1(a),Base2(b),member1(c),member2(d){}
	private:
  	Base1 member1;
	Base2 member2;
	Base3 member3;
};

​ 注:在上述基类中,Base1 Base2的构造函数中有一个参数,而Base3使用默认构造函数

析构函数
  • 析构函数没有类型,也没有参数,和构造函数相比情况略为简单些。

  • 派生类的析构函数声明方法和类中的完全相同,只要在函数体中负责把派生类新增的非对象成员的清理工作做好就够了,系统会自动调用基类及对象成员的析构函数来对基类及对象成员进行清理。但它的执行次序和构造函数正好严格相反,首先进行析构函数的函数体,再对派生类新增的类类型成员对象进行清理,最后对所有从基类继承来的成员进行清理

删除delete构造函数

通过delete可以实现禁止默认构造函数或者删除复制构造函数以阻止复制的做法,在积累中删除掉的构造函数,在派生类中也对应是删除状态,即如果基类中的默认构造函数、复制构造函数、移动构造函数是删除或者不可访问的,则派生类中对应的成员函数将是被删除的。

派生类成员的表示与访问

在派生类中,成员可以按访问属性划分为4种

  • 不可访问的成员。这是从基类私有成员继承而来的,派生类或是建立派生类对象的模块都没有办法访问到它们
  • 私有成员。这里可以包括从基类继承过来的成员以及新增加的成员,在派生类内部可以访问,但是建立派生类对象的模块中无法访问,继续派生,就变成了新的派生类中的不可访问成员
  • 保护成员。可能是新增也可能是从基类继承过来的,派生类内部成员可以访问,建立派生类对象的模块无法访问,进一步派生,在新的派生类中可能成为私有成员或者保护成员
  • 公有成员。派生类、建立派生类的模块都可以访问,继续派生,可能是新派生类中的私有、保护、或者公有成员

在对派生类的访问中,实际上有两个问题需要解决:第一是唯一标识问题,第二个问题是成员本身的属性问题,严格讲应该是可见性问题。我们只能访问一个能够唯一标识的可见成员。如果通过某一个表达式能引用的成员不止一个,称为有二义性

作用域分辨符

作用域分辨符就是我们经常见到的::,它可以用来限定要访问的成员所在的类的名称,一般的使用形式是:

类名::成员们
类名::成员名(参数表)
  • 如果存在多个具有包含关系的作用域,外层声明了一个标识符,而内层没有再次声明同名标识符,那么外层标识符在内层依然可见,如果内层存在同名标识符,那么外层标识符在内层不可见,这时称内层标识符隐藏了外层同名标识符,这种现象称为隐藏规则

  • 如果派生类中声明了基类中的同名变量,那么基类中的该变量将被隐藏。如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定

  • 对于多继承情况,首先考虑各个基类之间没有任何继承关系,同时也没有共同基类的情况,如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将隐藏所有基类的同名成员。使用对象名可直接访问派生类的成员,使用作用域分辨符和基类名也可访问基类的成员。如果派生类没有新增同名成员,那么就不可以通过对象直接访问该成员

  • 如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来限定

    如,有一个基类Base,声明了数据成员var和函数func,由Base公有派生产生了类Base1Base2,再以Base1Base2作为基类共同公有派生了新类Derived类,就含有通过Base1Base2继承来的Base中的同名成员

    现在来讨论同名成员的标识与访问问题。间接基类Base的成员经过两次派生之后,通过不同的派生路径以相同的名字出现在Derived中,这时如果使用基类名Base来限定,同样无法表明成员到底是从Base还是Base2继承过来,因此必须使用直接基类Base1或者Base2的名称来限定,才能够唯一标识和访问程序

    虚基类

    当某类的全部或部分直接基类是从另一个共同基类派生而来时,在这些直接基类中从上一级共同基类继承来的成员就拥有相同的名称。在派生类对象中,这些同名数据成员在内存中同时拥有多个副本,同一个函数名会有多个映射。我们可以使用作用域标识符来唯一标识并分别访问它们,也可以将共同基类设置为虚基类,这时从不同的路径继承过来的同名数据成员在内存中就只有一个,同一个函数名也只有一个映射

    • 定义形式

      class 派生类名:virtual 继承方式 基类名
      

      上述语句声明基类为派生类的虚基类。再多继承情况下,虚基类关键字的作用范围和继承方式相同,只对紧跟其后的基类起作用。声明了虚基类机会,虚基类的成员在进一步派生过程中和派生类一起维护一个内存数据

      class Base0{
        public:
        	int var0;
        	void fun0(){
            cout<<"Member of Base0"<<endl;
          }
      };
      class Base1:virtual public Base0{
        public:
        int var1;
      };
      class Base2:virtual public Base0{
        public:
        int var2;
      }
      class Derived:public Base1,public Base2{
        public:
        	int var;
        	void func(){
            cout << "Memver of Derived" << endl;
          }
      }
      int main(){
        Derived d;
        d.var0 = 2;
        d.fun0();
        return 0;
      }
      // Member of Base0
      
      虚基类及其派生类构造函数

      如果虚基类声明有非默认形式的构造函数,并且没有声明默认形式的构造函数,就会比较麻烦。这时,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始表中列出对虚基类的初始化表中列出对虚基类的初始化,例如,上述代码就应改成如下形式

      class Base0{
        public:
        	Base0(int var):var0(var){}
        	int var0;
        	void fun0(){
            cout<<"Member of Base0"<<endl;
          }
      };
      class Base1:virtual public Base0{
        public:
        Base1(int var):Base0(var){}
        int var1;
      };
      class Base2:virtual public Base0{
        public:
        Base2(int var):Base0(var){}
        int var2;
      }
      class Derived:public Base1,public Base2{
        public:
        	Derived(int var):Base0(var),Base1(var),Base2(var);
        	int var;
        	void func(){
            cout << "Memver of Derived" << endl;
          }
      }
      int main(){
        Derived d;
        d.var0 = 2;
        d.fun0();
        return 0;
      }
      

      我们可以将Derived称为最远派生类,在建立对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数进行初始化的,而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用都会被自动忽略

    构造一个类的对象的一半顺序是

    • 如果该类有直接或间接的虚基类,则先执行虚基类的构造函数
    • 如果该类有其他基类,则按照它们在继承声明列表中出现的次序,分别执行它们的构造函数,但在构造过程中,不再执行它们的虚基类的构造函数
    • 按照在类定义中出现的顺序,对派生类新增的成员对象进行初始化。对于类类型成员,如果出现在构造函数初始化列表中,则以其中指定的参数执行构造函数,如果未出现,则执行默认构造函数,对于基本数据类型的成员对象,如果出现在了初始化列表中,则使用其中指定的值作为初始值,否则什么也不做

多态性

概述

多台是指相同的消息被不同类型的对象接收时导致不同的行为,所谓消息是指对类的成员函数的调用,不同的行为是指不同的实现,也就是调用了不同的函数。最简单的例子就是运算符,如+可以应用在整数类型、浮点数类型等,同样的消息——相加,被不同类型的对象接收后,不同类型的变量采用不同的方法进行加法运算,如果是不同类型的变量想加,如浮点数和整形数,则要先将整形数转换为浮点数,然后再进行加法运算,这就是典型的多态现象

多态性可分为四类:重载多态、强制多态、包含多态、参数多态。前两种统称为专用多态,后两种称为通用多态

  • 普通函数及类的成员函数的重载都属于重载多态,以及接下来讲述的运算符重载
  • 强制多态是指将一个变元的类型加以变化,以符合一个函数或者操作的要求
  • 包含多态是类族中定义于不同类中的同名成员函数的多态行为,主要是通过虚函数来实现。
  • 参数多态与类模板相关联,在使用时必须赋予实际的类型才可以实例化

多态从实现的角度来讲可以分为两类:编译时的多态和运行时的多态。前者是在编译过程中确定了同名操作的具体操作对象,而后者则是在程序运行过程中才动态地确定操作所针对的具体对象。这种确定操作的具体对象的过程就是绑定。绑定是指计算机程序自身彼此关联的过程,也就是把一个标识符名和一个存储地址联系在一起的过程,用面向对象的术语讲,就是把一条消息和一个对象的方法相结合的过程。按照绑定进行的阶段的不同,可以分为两种不同的绑定方法:静态绑定和动态绑定,这两种绑定过程中分别对应着多态的两种实现方式

运算符重载

  • 除了少数几个外,其余运算符都可以重载,而且只能重载c++中已经存在的运算符

  • 重载之后运算符的优先级和结合性都不会改变

  • 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造,一般来讲,重载的功能应当与原有功能类似,不能改变原运算符的操作对象个数,同时至少要有一个操作对象是自定义类型

  • 有些运算符不能重载,它们是类属关系运算符.、成员指针运算符.*、作用域运算符::、三目运算符?:

  • 运算符的重载形式有两种,重载为类的非静态成员函数和重载为非成员函数。

    返回类型 类名:: operator 运算符(形参表){
      
    }
    //重载为类的成员函数
    返回类型 operator 运算符(形参表){
      
    }
    //重载为非函数成员
    
    • 返回类型指定了重载运算符的返回值类型,也就是运算结果类型
    • operator是定义运算符重载函数的关键字
    • 运算符是要重载的运算符名称
    • 当以非成员函数形式重载运算符时,有时需要访问运算符参数所涉及类的私有成员,这时可以把该函数声明为类的友元函数
    • 当运算符重载为类的成员函数时,函数的参数个数比原来的操作数要少一个(后置++ --除外),当重载为非成员函数时,参数个数与原操作数相同
  • 然而在实际应用中,总是通过类的对象来访问重载的运算符,所以重载为类的成员函数更加实用。如果是双目运算符,左操作数是对象本身的数据,由this指针指出,右操作数则需要通过运算符重载函数的参数表来传递,如果是单目运算符,就不需要任何参数

    • 对于双目运算符B,为了实现x B y ,其中xA类的对象,则应当把B重载为A类的成员函数,该函数只有一个形参,形参的类型是y所属的类型
    • 对于前置单目运算符U,用来实现表达式U x,则应当重载为成员函数,没有形参
    • 再来看后置运算符++--,如果将它们重载为类的成员函数,用来实现x++ x--,那么运算符就应当重载为成员函数,这时函数要带一个整型形参,这里的形参在运算中不起任何作用,只是用于区分前置和后置
    class Clock{
      public:
      	Clock(int hour = 0,int minute = 0,int second = 0);
      	void showTime() const;
      	Clock& operator++();
    		Clock operator++(int);
      private:
      	int hour,minute,second;
    };
    Clock::Clock(int hour = 0,int minute = 0,int second = 0):hour(hour),minute(minute),second(second){}
    void Clock::showTime(){
      cout << "Hour: "<< hour << ends << "Minute: "<< ends << "Second: "<< second << endl;
    }
    Clock& Clock::operator++(){
      second ++;
      if(second >= 60){
        second -= 60;
        minute ++;
      }
      if(minute >= 60){
        minute -= 60;
        hour ++;
      }
      hour %= 24;
      return *this;
    }
    Clock Clock::operator++(int){
      Clock c = *this;
      ++(*this);
      return c;
    }
    
  • 重载为非成员函数

    • 运算符也可以重载为非成员函数。这时,运算所需要的操作数都需要通过函数的形参表来传递,在形参表中形参从左到右的顺序就是运算符操作数的顺序。如果需要访问运算符参数对象的私有成员,可以将函数声明为类的友元函数
    • 对于双目运算符B,如果要实现x B y,其中x y中只要有一个具有自定义类型,就可以将B重载为非成员函数
    • 对于前置单目运算符U,可以重载为非成员函数,函数的形参为类的对象
    • 对于后置运算符++--,可以重载为非成员函数,并在参数表中加int
    • 将运算符重载为类的非成员函数,必须把操作数全部通过形参的形式传递给函数。<<操作符的左操作数是ostream类型的引用,ostreamcout类型的一个基类,右操作数是类的引用。函数把通过第一个参数传入的ostream对象以引用形式返回,这是为了支持形如cout << c1 << c2的连续输出
  • 运算符的两种重载形式各有千秋。成员函数的重载方式更加方便,但又是出于以下原因,需要使用非成员函数的重载方式

    • 要重载的操作符的第一个操作数不是可以更改的类型
    • 以非成员函数形式重载,支持更灵活的类型转换,如可将实数通过构造函数自动转换成类类型,而以成员函数形式时,左操作数必须为类类型,不能是其他类型,而有操作数可以被转换
    • =[]()->只能被重载为成员函数,而且派生类中的=总会隐藏基类中的=运算符函数

虚函数

现在我们遇到一个问题:如何利用一个循环结构处理同一类族中不同类的对象。为了解决这种问题,我们引入虚函数

虚函数是动态绑定的基础,虚函数必须是非静态的成员函数,虚函数经过派生之后,在类族中就可以实现运行过程中的多态

根据赋值兼容原则,可以使用派生类的对象代替基类对象。如果用基类类型的指针指向派生类对象,就可以通过这个指针访问该对象,问题是访问到的只是从基类继承来的同名成员。解决这一问题的方法是,如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数

如下代码

void fun(Base1 *ptr){
  ptr -> display();
}
int main(){
  Base1 b1;
  Base2 b2;
  Derived d;
  fun(&ba1);
  fun(&ba2);
  fun(&d);
}

其中,Base1Base2的基类,Base2Derived的基类,由于fun的形参是指向Base1类型的指针,所以只会调用Base1类型的display()

声明语法为:

virtual 函数类型 函数名(形参表);

虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候

运行过程中的多态要满足三个条件

  • 赋值兼容原则
  • 声明虚函数
  • 有成员函数来调用或者是通过指针、引用访问虚函数,如果是使用对象名来访问虚函数,则绑定在编译过程中就可以进行

我们考虑改写上述代码

#include
using namespace std;
class Base1{
  public:
    virtual void display() const;
};
void Base1::display()const{
  cout << "Base1" << endl;
}
class Base2:public Base1{
  public:
    virtual void display() const;
};
void Base2::display()const{
  cout << "Base2" << endl;
}
class Derived:public Base2{
  public:
    void display() const;
};
void Derived::display()const{
  cout << "Derived" << endl;
}
void func(Base1 *ptr){
  ptr -> display();
}
int main(){
  Base1 b1;
  Base2 b2;
  Derived d;
  func(&b1);
  func(&b2);
  func(&d);
  return 0;
}

程序中类Base1 Base2 Derived属于同一个类族,而且通过公有派生而来,因此满足赋值兼容规则,同时,基类将函数声明为虚函数,程序中使用对象指针来访问函数成员,这样的绑定过程就是在运行中完成,实现了运行中的多态。通过基类类型的指针就可以访问到正在指向的对象的成员,这样,能够对同一类族的对象进行统一的处理,抽象程度更高,程序更简洁、更高效

  • 虚表
    • 每个多态类有一个虚表
    • 虚表中有当前类的各个虚函数的入口地址
    • 每个对象有一个指向当前类的虚表的指针(虚指针vptr)
  • 动态绑定的实现
    • 构造函数中为对象的虚指针赋值
    • 通过多态类型的指针或引用调用成员函数时,通过虚指针找到虚表,进而找到调用的虚函数的入口地址
    • 通过该入口地址调用虚函数

在本程序中,派生类并没有显式给出虚函数声明,这时系统就会遵循以下规则来判断派生类的一个函数成员是不是虚函数

  • 该函数是否与基类的虚函数有相同的名称
  • 该函数是否与基类的虚函数有相同的参数表
  • 该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容原则的指针、引用型的返回值

如果从名称、参数、返回值三个方面检查之后,派生类的函数满足了上述条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了基类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的所有重载形式

  • 一般非静态成员函数可以是虚函数
  • 构造函数不能是虚函数
  • 析构函数可以是虚函数(可用于通过基类指针删除派生类对象)

纯虚函数

具有纯虚函数的类叫做抽象类,抽象类不能定义其对象

  • 纯虚函数的声明:

    virtual 函数类型 函数(形参表) = 0;
    

纯虚函数在该基类中没有定义具体的操作内容,要求各派生类根据自己需要定义自己的板本

抽象类的作用

  • 抽象类为抽象和设计的目的而声明
  • 将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为
  • 对于暂时无法实现的函数,可以声明为纯虚函数,留给派生类趋势线
  • 注意:抽象类只能作为基类来使用

overridefinal

override
  • 显式函数覆盖

  • 声明该函数必须覆盖基类的虚函数,编译器可发现“未覆盖”错误

  • 覆盖要求:函数签名完全一致

    class Base{
      virtual void func(int)const;
    };
    class Derived : public Base{
      virtual void func(int) override;//报错,无法覆盖
      virtual void func(int)const override;//正确
    }
    
final
  • 用来避免类被继承,或是基类的函数被覆盖

    class Base1 final{};
    class Derived1 : public Base1{};//编译错误,无法继承Base1
    class Base2{
      virtual void func() final;
    };
    class Derived : public Base2{
      void func();//错误,func不允许被覆盖
    };
    

模板与群体数据

为了实现代码重用,代码必须具有通用性。通用代码需要不受数据类型的影响,并且可以自动适应数据类型的变化。这种程序设计类型称为参数化程序设计。模板是c++支持参数化程序设计的工具,通过它可以实现参数化多态性。所谓参数化多态性,就是将程序所处理的对象的类型参数化,使得一段程序可以用于处理多种不同类型的对象

函数模板

很多情况下,一个算法是可以处理多种数据类型的,但是用函数实现算法时,即使设计为重载函数也只是使用相同的函数名,函数体仍然要分别定义。使用函数模板则可避免这种情况,其定义形式是

template <模板参数表>
类型名 函数名(参数表){
  函数体
}

所有函数模板的定义都是用关键字template开始的,该关键字之后是使用<>扩起来的模板参数表,模板参数表由用逗号分隔的模板参数构成,可以包括以下内容

  • class或者typename标识符,指明可以接受一个类型参数,这些类型参数代表的是类型,可以是预定义或自定义类型
  • 类型操作符 标识符,指明可以接受一个由类型操作符所规定类型的常量作为参数
  • template<参数表> class,表明可以接收一个类模板名作为参数

类型参数可以用来指定函数模板本身的形参类型、返回值类型,以及声明函数中的局部变量,函数模板中的函数体的定义方式与定义普通函数类似

template <typename T>
T abs(T x){
  return x<0?-x:x;
}

再如:

template<typename T>
void outputArray(const T* array,int count){
  for(int i=0;i<count;i++) cout << array[i] << " ";
  cout << endl;
}

注意,当模板为类类型时,应当对其用到的运算符进行重载

类模板

使用类模板使得用户可以为类定义一种模式,使得类中的某些数据成员、某些成员函数的参数、函数返回值、局部变量能取到不同的类型

类模板声明的语法形式是:

template<模板参数表>
class 类名{
  
};

template <typename T = double> //默认参数
class Point{
  public:
  	Point(T _x = 0,T _y = 0):x(_x),y(_y){}
  private:
  	T x;
  	T y;
}

如果需要在类模板外定义其成员函数,则要用以下形式

template<模板参数表>
类型名 类名<模板参数标识符列表>:: 函数名(参数表);

使用模板类来建立对象时,应按以下形式声明

模板名<模版参数表> 对象名1,...,对象名n;

如下

#include
using namespace std;
template <class T>
class Store{
  private:
  	T item;
  	bool haveValue;
  public:
  	friend T;
  	Store();
  	T &getElem();
  	void putElem(const T &x);
};

template <class T>
Store<T>::Store():haveValue(false){ }

template <class T>
T &Store<T> :: getElem(){
  if(!haveValue){
    exit(1);
  }
  return item;
}

template <class T>
void Store<T>::putElem(const T &x){
  haveValue = true;
  item = x;
}

struct Student{
  int id;
  float gpa;
};

int main(){
    Student g = {1000,23};
    Store<Student> s;
    s.putElem(g);
    cout << s.getElem().id << " " << s.getElem().gpa << endl;
}

数组类模版的实现

#ifndef ARRAY_H
#define ARRAY_H

#include

template <class T>
class Array{
    private:
        T* list;
        int size;
    public:
        Array(int sz = 50);
        Array(const Array<T>&x);
        ~Array();
        Array<T>& operator = (const Array<T> &x);
        T& operator[](int i);
        const T& operator[](int i)const;
        operator T*();
        operator const T*() const;
        int getSize()const;
        void resize(int sz);
};

template <class T> Array<T>::Array(int sz){
    size = sz;
    list = new T[size];
}

template <class T> Array<T> :: ~Array(){
    delete [] list;
}

template <class T>
Array<T>::Array(const Array<T> &x){
    size = x.size;
    list = new T[size];
    for(int i=0;i<size;i++){
        list[i] = x.list[i];
    }
}

template<class T>
Array<T>& Array<T>::operator = (const Array<T> &x){
    if(&x!=this){
        if(size!=x.size){
            delete[] list;
            size = x.size;
            list = new T[size];
        }
        for(int i=0;i<size;i++)
            list[i] = x.list[i];
    }
    return *this;
}

template<class T>
Array<T>::Array(const Array<T> &a){
    size = a.size;
    list = new T[size];
    for(int i=0;i<size;i++)
    list[i] = a.list[i];
}

template<class T>
T& Array<T>::operator[](int i){
    return list[i];
}

template<class T>
const T& Array<T>::operator[](int i)const{
    return list[i];
}

template<class T>
void Array<T>::resize(int sz){
    if(sz == size) return;
    T* newList = new T[sz];
    int n = (sz < size) ? sz : size;
    for(int i=0;i<n;i++)
        newList[i] = list[i];
    delete[]list;
    list = newList;
    size = sz;
}

template<class T>
Array<T>::operator T*(){
    return list;
}

template<class T>
Array<T>::operator const T*()const{
    return list;
}

template <class T>
int Array<T>::getSize()const{
    return size;
}

#endif
  • 对于类类型的+ -等运算符而言,即使其返回值不是引用,也可以作为左值,可利用a+b直接调用其成员函数。不过如果对该左值赋值,则没有意义,所以可以在重载+ -等运算符时,令其返回常对象
  • c++中,如果想将自定义类型T的对象隐含或显式地转化为S类型,可以将operator S定义为T的成员函数,这样,在把T类型对象显式隐含转换成S类型,或用static_cast显式转换时,该成员函数会被调用。转换操作符的重载函数不用指定返回值的类型,这是由于这种情况下重载函数的返回类型与操作符名称一致

链表类模版的实现

节点类

#ifndef NODE_H
#define NODE_H

template<class T>
class Node{
    private:
        Node<T>* next;
    public:
        T data;
    Node(const T& data,Node<T>*next = nullptr);
    void insertAfter(Node<T> *p);
    Node<T>* deleteAfter();
    Node<T>* nextNode();
    const Node<T>*nextNode()const;
};

template<class T>
Node<T>*Node<T>::nextNode(){
    return next;
}

template<class T>
const Node<T>* Node<T>::nextNode()const{
    return next;
}

template<class T>
Node<T>::Node(const T& data,Node<T>*next = nullptr){
    this -> data = data;
    this -> next = next;
}

template<class T>
void Node<T>::insertAfter(Node<T>*p){
    p->next = next;
    next = p;
}

template<class T>
Node<T> *Node<T>::deleteAfter(){
    Node<T> *temptr = next;
    if(next == 0) return 0;
    next = temptr -> next;
    return temptr;
}

#endif

链表类

#ifndef LINKEDLIST_H
#define LINKEDLIST_H

#include"Node.h"

template<typename T>
class Linkedlist{
    private:
        Node<T>*front,*rear;//表头和表尾指针
        Node<T>*prevPtr,*currPtr;//记录表当前遍历位置的指针
    
        int size;
        int position;//当前元素在表中的序号位置
        //函数成员
        //生成新节点,数据域为item,指针域为ptrNext
        Node<T>*newNode(const T&item,Node<T>*ptrNext=nullptr);
        //释放节点
        void freeNode(Node<T> *p);
        //将链表L复制到当前表
        void copy(const Linkedlist<T>&L);

    public:
        LinkedList();//构造函数
        LinkedList(const LinkedList<T>&L);//复制构造函数
        ~LinkedList();//析构函数
        LinkedList<T> &operator={const LinkedList<T>&L};//赋值运算符重载

        int getSize()const;//返回链表大小
        bool isEmpty()const;//返回链表为空

        void reset(int pos=0);//初始化游标的位置
        void next();//移动右标到下一位
        bool endOfList()const;//返回在表尾
        int currentPosition()const;//返回现在游标的位置

        void insertFront(const T& item);//在表头插入结点
        void insertRear(const T& item);//在表尾插入结点
        void insertAt(const T& item);//在当前结点之前插入
        void insertAfter(const T& item);//在当前结点之后插入

        T deleteFront();//删除头结点
        void deleteCurrent();//删除当前结点

        T&data();
        const T&data()const;

        void clear();//清空列表,释放所有节点的内存
};

#endif

具体实现略

栈类模版的实现

#ifndef STACK_H
#define STACK_H

template<class T,int SIZE = 50>
class Stack{
    private:
        T list[SIZE];
        int top;
    public:
        Stack();
        void push(const T&item);
        T pop();
        void clear();
        const T&peek()const;//访问栈顶元素
        bool isEmpty()const;
        bool isFull()const;
};  

template<class T,int SIZE>
Stack<T,SIZE> :: Stack() :top(-1){}

template<class T,int SIZE>
void Stack<T,SIZE>::push(const T&item){
    lisk[++top] = item;
}

template<class T,int SIZE>
T Stack<T,SIZE>::pop(){
    return list[top--];
}

template<class T,int SIZE>
const T& Stack<T,SIZE>::peek()const{
    return list[top];
} 

template<class T,int SIZE>
bool Stack<T,SIZE>::isEmpty()const{
    return top==-1;
}

template<class T,int SIZE>
bool Stack<T,SIZE>::isFull()const{
    return top == SIZE-1;
}

template<class T,int SIZE>
void Stack<T,SIZE>::clear(){
    top = -1;
}

#endif
用栈实现整数计算器

本程序实现一个简单的整数计算器,能够进行加减乘除和乘方运算,使用时算式采用后缀输入法,每次计算在前次结果基础上进行,若要将前次运算结果清除,可输入c,输入q时程序结束

//Calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

#include"Stack.h"

class Calculator{
    private:
        Stack<double>s;
        void enter(double num);
        bool getTwoOperands(double &op1,double &op2);
        void compute(char op);
    public:
        void run();
        void clear();
};


#endif
//Calculator.cpp
#include"Calculator.h"
#include
#include
#include

using namespace std;

void Calculator:: enter(double num){
    s.push(num);
}

bool Calculator::getTwoOperands(double &op1,double &op2){
    if(s.isEmpty()){
        cerr << "Missing operand!" << endl;
        return false;
    }
    op1 = s.pop();
    if(s.isEmpty()){
        cerr << "Missing operand!" << endl;
        return false;
    }
    op2 = s.pop();
    return true;
}

void Calculator::compute(char op){
    double op1,op2;
    bool result = getTwoOperands(op1,op2);
    if(!result){s.clear();return;}

    switch(op){
        case '+':
            s.push(op2 + op1);
            break;
        case '-':
            s.push(op2 - op1);
            break;
        case '*':
            s.push(op2 * op1);
            break;
        case '/':
            s.push(op2 / op1);
        case '^':
            s.push(pow(op2,op1));
            break;
        default:
            cerr << "Unrecognized operator" << endl;
            break;
    }
    cout << "=" << s.peek() << " ";
}

inline double stringToDouble(const string &str){
    istringstream stream(str);
    double result;
    stream >> result;
    return result;
}

void Calculator::run(){
    string str;
    while(cin >> str , str != "q" ){
        switch(str[0]){
            case 'c':
                s.clear();
                break;
            case '-':
                if(str.size()>1)
                    enter(stringToDouble(str));
                else
                    compute(str[0]);
                break;
            case '+':
            case '*':
            case '/':
            case '^':
                compute(str[0]);
                break;
            default:
                enter(stringToDouble(str));
                break;
        }
    }
}

void Calculator::clear(){
    s.clear();
}
//Calculator_test.cpp
#include"Calculator.h"

int main(){
    Calculator c;
    c.run();
    return 0;
}

队列模版的实现

#ifndef QUEUE_H
#define QUEUE_H

template<class T,int SIZE = 50>
class Queue{
    private:
        int front,rear,count;
        T list[SIZE];
    public:
        Queue();
        void insert(const T&item);
        T remove();//出队
        void clear();
        const T&getFront()const;
        int getLength()const;
        bool isEmpty()const;
        bool isFull()const;
};

template<class T,int SIZE>
Queue<T,SIZE>::Queue():front(0),rear(0),count(0){}

template<class T,int SIZE>
void Queue<T,SIZE>::insert(const T&item){
    count++;
    list[rear] = item;
    rear = (rear+1) % SIZE;//循环队列
}

template<class T,int SIZE>
T Queue<T,SIZE>::remove(){
    int temp = front;
    count --;
    front = (front + 1) % SIZE;
    return list[temp];
}

template<class T,int SIZE>
void Queue<T,SIZE>::clear(){
    count = rear = front = 0;
}

template<class T,int SIZE>
const T& Queue<T,SIZE>::getFront()const{
    return list[front];
}

template<class T,int SIZE>
int Queue<T,SIZE>::getLength()const{
    return count;
}

template<class T,int SIZE>
bool Queue<T,SIZE>::isEmpty()const{
    return count==0;
}

template<class T,int SIZE>
bool Queue<T,SIZE>::isFull()const{
    return count == SIZE;
}

#endif

泛型程序设计与c++语言标准模版库

泛型程序设计和STL的结构

基本概念

泛型程序设计就是编写不依赖于具体数据类型的程序。c++中,模版是泛型程序设计的主要工具

泛型程序设计的主要思想是将算法从特定的数据结构中抽象出来,使算法成为通用的、可以作用于不同的数据结构

而在一些算法中,数据结构需要有某些特性,我们用概念来描述泛型程序设计中作为参数的数据类型所需具备的功能,它的外延是具备这些功能的所有数据结构。具备一个概念所需要功能的数据类型称为这一概念的一个模型,对于两个不同的概念A和B,如果概念A所需求的所有功能也是B需求的功能,那就说B是A的子概念

STL简介

STL定义了一套概念体系,为泛型程序设计提供了逻辑基础

基本组件:

  • 容器(container)
  • 迭代器(iterator)
  • 函数对象(function object)
  • 算法(algorithms)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OZ2Ecwio-1666665427884)(/Users/zhiweijin/Library/Application Support/typora-user-images/image-20220924224130350.png)]

  • 迭代器是算法和容器的桥梁
    • 将迭代器作为算法的参数,通过迭代器来访问容器而不是把容器直接作为算法的参数
  • 将函数对象作为算法的参数而不是将函数所执行的运算作为算法的一部分
  • 使用STL中提供的或自定义的迭代器和函数对象,配合STL的算法,可以组合出各种各样的功能
容器

容器是指容纳、包含一组元素的对象

  • 基本容器类模板

    • 顺序容器

      array、vector、deque、forward_list、list

    • 有序关联容器

      set、multiset、map、multimap

    • 无序关联容器

      unordered_set、unordered_multiset、unordered_map、unordered_multimap

  • 容器适配器(受限制的容器)

    ​ stack、queue、priority_queue

    迭代器
  • 迭代器是泛化的指针

  • 提供了顺序访问容器中每个元素的方式

  • 可以使用++运算符来获得指向下一个元素的迭代器

  • 可以使用*运算符来访问一个迭代器所指向的元素,如果元素类型是类或结构体,还可以使用->运算符直接访问该元素的一个成员

  • 有些迭代器还支持通过’–'运算符获得指向上一个元素的迭代器

  • 迭代器是泛化的指针,指针本身也是一种迭代器

例:transform算法

  • transform算法顺序遍历firstlast两个迭代器所指向的元素
  • 将每个元素的值作为函数对象op的参数
  • 遍历完成后result迭代器指向的是输出的最后一个元素的下一个位置,transform会将该迭代器返回
template <class InputIterator,class OutputIterator,class UnaryFunction>
OutputIterator transform(InputIterator first,InputIterator last,OutputIterator result,UnaryFunction op){
  for(;first != last;++first,++result){
    *result = op(*first);
  }
  return result;
}
函数对象
  • 一个行为类似函数的对象,对它可以像调用函数一样调用
  • 函数对象是泛化的函数,任何普通的函数和任何重载了()运算符的类的对象都可以作为函数对象使用
  • 使用STL的函数对象,需要包含头文件
算法
  • STL包含70多个算法
  • 可以广泛用于不同对象和内置的数据类型
  • 使用STL的算法,需要包含头文件
综合应用
#include
#include
#include
#include
#include
using namespace std;
int main(){
  const int N = 5;
  vector<int> s(N);
  for(int i=0;i<N;i++)
    cin >> s[i];
  transform(s.begin(),s.end(),ostream_iterator<int>(cout," "),negate<int>());
  return 0;
}

迭代器

  • 迭代器是算法和容器的桥梁
    • 迭代器用作访问容器中的元素
    • 算法不直接操作容器中的数据,而是通过迭代器简介操作
  • 算法和容器独立
    • 增加新的算法,不影响迭代器的实现
    • 增加新的容器,原有的算法也能适用
输入流迭代器和输出流迭代器
  • 输入流迭代器

    • 输入流迭代器用来从一个输入流中连续地输入某种类型的数据,它是一个类模版,如:

      templateistream_iterator

    • 由于STL设计得非常灵活,很多STL模版都有三四个模版参数,但排在后面的参数一般都有默认的参数值,绝大部分程序中都会省略这些参数而使用它们的默认值。例如,istream_iterator实际上有多达4个模板参数,我们只给出一个默认值的模板参数,后面的模板直接省略

    • 其中,T是使用该迭代器从输入流中输入数据的类型,类型T要满足两个条件:有默认构造函数、对该类型的数据可以使用>>从输入流输入,一个输入流迭代器的实例需要由下面的构造函数来实现

      istream_iterator(stream& in)

    • 在该构造函数中,需要提供用来输入数据的输入流作为参数。一个输入流迭代器实例可以支持*->++等几种运算符,用*可以访问刚刚读取的数据,用++可以从输入流中读取下一个元素,若T是类类型,用->可以直接访问刚刚读取元素的成员。判断一个输入流是否结束,istream_iterator类模版有一个默认构造函数,用该函数构造出的迭代器指向的就是输入流的结束为止,将一个输入流与这个迭代器进行比较就可以判断输入流是否结束

  • 输出流迭代器

    • 输入流迭代器也是一个类模版,例如templatestream_iterator

    • 其中的T表示向输出流输出数据的类型

    • 可用下面两种构造函数来构造

      ostream_iterator(ostream& out);
      ostream_iterator(ostream& out,const char* delimiter);
      

      其中,out是输出流,而delimiter表示分隔符

    • 输出流迭代器的解引用只能作为左值

  • 二者都属于适配器

    • 适配器是用来为已有对象提供新的接口的对象
    • 输入流和输出流适配器为流对象提供了迭代器的结构
#include
#include
#include
using namespace std;

double square(double x){
  return x*x;
}

int main(){
  transform(istream_iterator<double>(cin),istream_iterator<double>(),ostream_iterator<double>(cout,"\t"),square);
  cout << endl;
  return 0;
}
迭代器的分类

STL根据迭代器的功能,将迭代器分为了5类,这5类对应5个概念

输入迭代器

输入迭代器可以用来从序列中读取数据,但是不一定能够向其中写入数据。输入迭代器支持对序列进行不可重复的单向遍历。前面介绍的输入流迭代器就是一种输入迭代器

需要注意的是,如果p1==p2,并不能保证++p1==++p2,由于这一点,用输入迭代器读入的序列不保证是可重复的

输出迭代器

输出迭代器允许向序列中写入数据,但是并不保证可以从其中读取数据。输出迭代器也支持对序列进行单向遍历,前面介绍的输出流迭代器就是一种输出迭代器

另外,使用输出迭代器,写入元素的操作和使用自增操作必须是交替进行,否则其行为都是不确定的

前向迭代器

前向迭代器这一概念是输入迭代器和输出迭代器这两个概念的字概念,它既支持数据读取,也支持数据写入,前向迭代器支持对序列进行可重复的单向遍历

它去掉了输入迭代器和输出迭代器这两个概念中的一些不确定性,对于向前迭代器,++p1 == ++p2是一定成立的,也没有自增和写入必须交替进行的情况

双向迭代器

双向迭代器这一概念是单向迭代器的子概念,在单向迭代器的基础上,又支持反向移动,即自减

随机访问迭代器

随机访问迭代器这一概念是双向迭代器的子概念,在双向迭代器的基础上,它又支持直接将迭代器向前或向后移动n个元素,因此随机访问迭代器的能力几乎和指针一样

迭代器的区间

STL算法中的形参往往包括一对输入迭代器,用它们所构成的区间来表示输入数据的序列。

p1 p2是两个输入迭代器,以后将使用[p1,p2)形式来表示它们所构成的区间,这样一个区间是一个有序序列,包括p1p2两个迭代器所指向元素之间所有元素但不包括p2所指向的元素。当且仅当对p1进行了n次++运算后,表达式p1==p2的值为true,此时[p1,p2)才是一个合法区间

综合运用迭代器的示例

#include
#include
#include
#include

using namespace std;

template <class T,class InputIterator,class OutputIterator>
void mySort(InputIterator first,InputIterator last,OutputIterator result){
  vector<T> s;
  for(;first != last;first++)
		s.push_back(*first);
  sort(s.begin(),s.end());
  copy(s.begin(),s.end(),result);
}

int main(){
  double a[5] = {1.2,2.4,0.8,3.3,3.2};
  mySort<double>(a,a+5,ostream_iterator<double>(cout," "));
  cout << endl;
  mySort<int>(istream_iterator<int>(cin),istream_iterator<int>(),ostream_iterator<int>(cout," "));
}
迭代器的辅助函数

STL为迭代器提供了两个辅助函数模版,advancedistance,它们为所有迭代器提供了一些原本只有随机访问迭代器才有的访问能力:前进或后退多个元素,以及计算两个迭代器之间的距离

advance函数的模版原型是

template<class InputIt,class Distance>
void advance(InputIt& iter,Distance n);

distance函数的模版原型是

template<class FirstIt,class LastIt>
void distance(FirstIt& first,LastIt last);

容器

STL有13种容器,每种都具有不尽相同的功能和用法

S表示一种容器类型,s1 s2都是S类型的实例,容器支持的基本功能如下:

  • S s1
  • s1 op s2,其中,op为比较运算符,可对两个容器之间的元素按字典序进行比较
  • s1.begin()
  • s1.end()
  • s1.clear()
  • s1.empty()
  • s1.size()
  • s1.swap(s2)

在前面的示例程序中,由于我们都是直接把一个容器的begin()end()函数的返回值提供给了一个算法,算法的参数类型由编译器自动解析,因此无须显式写出迭代器类型。但有时显式写出一个容器的迭代器类型还是有必要的,与类型S的容器相关的迭代器类型可以用下面的方法表示

S::iterator表示与S相关的普通迭代器类型,迭代器指向元素的类型为T

S::const_iterator表示与S相关的常迭代器类型,迭代器指向元素的类型为const T,因此无法通过迭代器改写元素

当使用迭代器不需要写访问时,建议使用s1.cbegin() s1.cend()

容器作为一种STL的概念,有许多子概念。容器分为顺序容器和关联容器,这就是容器的两个子概念,这种划分是基于容器中元素的组织方式的,另一方面,按照容器与所关联的迭代器类型划分,容器又具有可逆容器这一子概念,可逆容器又具有随机访问容器这一子概念

使用一般容器的begin() end()成员函数所得到的迭代器都是前向迭代器,也就是说可以对容器的元素进行单向的遍历,而可逆容器所提供的迭代器是双向迭代器,可以对容器的元素进行双向的遍历

事实上,STL提供的标准容器都至少是可逆容器,但有些非标准的模版库提供诸如slist单向链表这样的仅提供前向迭代器的容器

对一个可逆容器进行逆向遍历时,可以通过对其迭代器使用--运算来进行,但有时这样做不够方便,因为STL算法的输入都是用正向区间来表示的,为此,STL为每个可逆容器都提供了逆向迭代器,逆向迭代器可以通过下面的成员函数得到

s1.rbegin()//得到指向容器的最后一个元素的逆向迭代器
s1.end()//得到指向容器第一个元素的前一个位置的逆向迭代器

逆向迭代器的类型名的表示方式如下

S::reverse_iterator//表示与S相关的普通迭代器类型,迭代器指向元素的类型为`T`
S::reverse_iterator

逆向迭代器实际上是普通迭代器的适配器,逆向迭代器的++运算被映射为普通迭代器的--,反之同理

一个迭代器和它的逆向迭代器之间可以互相转换。逆向迭代器都有一个构造函数,用它可以构造一个迭代器的逆向迭代器,如可以用S::reverse_iterator r1(p1)就可以得到逆向迭代器;另一方吧,逆向迭代器提供一个成员函数base,用它可以得到用于构造该逆向函数的迭代器,r1.base()p1,关于迭代器与逆向迭代器,有以下的等式关系

s1.rbegin() == S::reverse_iterator(s1.end()) , s1.rbegin().base() == s1.end();
s1.rend() == S::reverse_iterator(s.begin()) , s1.rend().base() == s1.begin();

随机访问容器所提供的迭代器是随机访问迭代器,支持对容器的元素进行随机访问。使用随机访问容器,可以直接通过一个整数来访问容器中的指定元素

s1[n] 等价于 s1.begin()[n]

下表给出了容器所属概念

容器名 中文名 所属概念
vector 向量 随机访问容器、顺序容器
deque 双端队列 随机访问容器、顺序容器
list 列表 可逆容器、顺序容器
forward_list 单向链表 单向访问容器、顺序容器
array 数组 随机访问容器、顺序容器
set 集合 可逆容器、关联容器
multiset 多重集合 可逆容器、关联容器
map 映射 可逆容器、关联容器
multimap 多重映射 可逆容器、关联容器
unordered_set 无序集合 可逆容器、关联容器
unordered_multiset 无序多重集合 可逆容器、关联容器
unordered_map 无序映射 可逆容器、关联容器
unordered_multimap 无序多重映射 可逆容器、关联容器
顺序容器
  • 元素线性排列,可以随时在指定位置插入元素和删除元素
  • 必须符合Assignable这一概念
  • array对象的大小固定,forward_list有特殊的添加和删除操作
顺序容器的基本功能
  • 构造函数

    S s(n,t)构造一个由nt元素构成的容器实例s

    S s(n)构造一个有n个元素的容器实例s,每个元素都是T()

    S s(q1,q2)使用[q1,q2)区间内的数据作为s的元素构造s,其中q1 q2为迭代器

  • 赋值函数

    assign将指定的元素赋给顺序容器,顺序容器中原先的元素会被清除,赋值函数的形式和构造函数相同

  • 插入函数

    可以一次插入一个或多个指定元素,也可以将一个迭代器区间中的序列插入,通过一个指向当前容器元素的迭代器来指示插入位置,返回值为指向新插入位置的元素中第一个元素的迭代器

    s.insert(p1,t)s容器中p1所指向的位置插入一个t的复制,插入后的元素夹在原p1,和p1-1所指向的元素之间

    s.insert(p1,n,t)sp1指向的位置插入nt的复制

    s.insert(p1,q1,q2)[q1,q2)的元素插入

    s.emplace(p1,args),将参数args传递给T的构造函数构造新元素t,在s容器中插入t

  • 其他函数

    erase,clear,pop_front,pop_back,front,back,resize

向量

特点

  • 一个可以扩展的动态数组
  • 随机访问、在尾部插入或删除元素快
  • 在中间或头部插入或删除元素慢

向量的容量

  • 容量:实际分配空间的大小
  • s.capacity():返回当前容量
  • s.reserve(n):将容量扩充至n
  • s.shrink_to_fit():回收未使用的元素空间,即sizecapacity函数返回值相等
双端队列

特点

  • 在两端插入或删除元素快
  • 在中间插入或删除元素慢
  • 随机访问较快,但比向量慢

例:先按从大到小顺序输出奇数,再按照从小到大顺序输出偶数

#include
using namespace std;
int main(){
  istream_iterator<int> i1(cin),i2;
  vector<int> s1(i1,i2);
  sort(s1.begin(),s1.end());
  deque<int> s2;
  for(vector<int>::iterator iter = s1.begin(); iter != s1.end(); iter++){
    if(*iter %2 == 0)
      s2.push_back(*iter);
    else
      s2.push_front(*iter);
  }
  copy(s2.begin(),s2.end(),ostream_iterator<int>(cout," "));
  cout << endl;
  return 0;
}
列表

特点

  • 在任意位置插入和删除元素都很快

  • 不支持随机访问

  • 接合splice操作

    s1.splice(p,s2,q1,q1):将s2中[q1,q2)移动到p之前

数组
  • array是对内置数组的封装,提供了更安全、更方便的使用数组的方式
  • array的对象的大小是固定的,定义时除了需要指定元素类型,还需要指定容器大小
  • 不能动态地改变容器大小
单向链表
  • 单向链表每个结点只有指向下个结点的指针,没有简单的方法来获取一个节点的钱去
  • 定义了insert_afteremplace_aftererase_after操作
适配器

以顺序容器为基础构造一些常用数据结构,是对顺序容器的封装

  • 栈:后进先出
  • 队列:先进先出
  • 优先队列:最大的先出
顺序容器的插入迭代器

在顺序容器(除array)中插入元素,还可以通过插入迭代器。插入迭代器是一种适配器,使用它可以通过输出迭代器的接口来向指定元素的指定位置插入元素,因而如果在调用STL算法时使用输出迭代器,可以将结果顺序插入到容器的指定位置,而无需覆盖已有的元素。插入迭代器有3种:

template<class Container> class front_insert_iterator
template<class Container> class back_insert_iterator
template<class Container> class insert_iterator

插入迭代器的实例可以通过构造函数来实现,但一般无须直接调用构造函数,而是可以通过下面这个辅助函数

template<class Container>
front_insert_iterator<Container>front_inserter(Contain&s);

对于另外两种迭代器同理

由于辅助函数是函数模版,调用时可以直接根据实参自动推出类型参数,可以使代码更加简洁

关联容器

关于关联容器,详细见数据结构

流类库与输入输出

与c语言一样,c++语言中没有输入输出语句,但c++标准库中有一个面向对象的输入输出软件包,它就是I/O流类库,流是I/O流类的中心概念

I/O流的概念及流类库结构

当程序与外界环境进行信息交换时,存在着两个对象,一个是程序中的对象,另一个是文件对象程序建立一个流对象,并指定这个流对象与某个文件对象建立联系,程序操作流对象,流对象通过文件系统对所连接的文件对象产生作用。由于流对象是程序中的对象与文件对象进行交换的洁面,对程序对象而言,文件对象有的特性,流对象也有,所以程序将流对象看作是文件对象的化身

凡是数据从一个地方传输到另一个地方的操作都是流的操作。

一般意义下的读操作在流数据抽象中被称为从流中提取,写操作被称为向流中插入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WbA9clS3-1666665427885)(/Users/zhiweijin/Library/Application Support/typora-user-images/image-20220927181543028.png)]

输出流

一个输出流对象是信息流动的目标,最重要的3个输出流是ostream,ofstream,ostringstream

预先定义的ostream类对象用来完成向标准设备的输出

  • cout是标准输出流
  • cerr是标准错误输出流,没有缓冲,发送给它的内容立即被输出
  • clog类似于cerr,但是有缓冲,缓冲区满时被输出

ofstream类支持磁盘文件输出,如果你需要一个只输出的磁盘文件,可以构造一个ofstream类的对象,在打开文件之前或之后可以指定ofstream对象接受二进制或文本模式数据。很多格式化选项和成员函数可以应用于ofstream对象,包括基类iosostream的所有功能

如果在构造函数中指定一个文件名,当构造这个文件时该文件是自动打开的。否则,你可以在调用默认构造函数之后使用open成员函数打开文件,或者在一个由文件指示符表示的打开文件基础上构造一个ofstream对象

构建输出流对象

如果仅仅使用预先定义的coutcerrclog对象,就不需要构造一个输出流。如果要使用文件流将信息输出到文件,便需要使用构造函数来建立流对象

构造输出流的常用方式如下:

  • 使用默认构造函数,然后调用open成员函数

    ofstream myFile;
    myFile.open("");
    
  • 在调用构造函数时指定文件名

    ofstream myFile("");
    
  • 也可以使用同一个流先后打开不同的文件(在同一时刻只有一个是打开的)

    ofstream file;
    file.open("FILE1");
    //...
    file.close();
    file.open("FILE2");
    file.close();
    //当对象file离开它的作用域时就会消亡
    
  • c++11标准以前只支持c风格字符数组作为文件名,c++11标准引入string作为文件名

    string filename = "file.txt";
    ofstream myFile(filename);
    
使用插入运算符和操纵符

插入运算符用于传送字节到一个输入流对象。插入运算符与预先定义的操纵符一起工作,可以控制输出格式

  • 输出宽度

    为了调整输出,可以通过在流中放入setw操纵符或调用width成员函数为每个项指定输出宽度,下面的例子在一列中以至少10个字符宽按右对齐输出数值

    #include
    using namespace std;
    
    int main(){
      double values[] = {1.23,35.36,653.2,3456.1};
      for(int i=0;i<4;i++){
        cout.width(10);
        cout << values[i] << endl;
      }
      return 0;
    }
    /*
          1.23
         35.36
         653.2
        3456.1
    */
    

    从程序的输出结果可以看到,在少于10个字符宽的数值前加入了引导空格

    空格是默认的填充符,当输出的数据不能充满指定的宽度时,系统会自动以空格填充,也可以指定用别的字符来填充,使用fill成员函数可以为已经指定宽度的域设置填充字符的值,如可在上述函数for前加入代码:

    cout.fill('*');
    

    如果说要为同一行中输出的不同数据项分别指定宽度,也可以使用setw操纵符

    #include
    #include
    #include
    using namespace std;
    
    int main(){
      double values[] = {1.23,35.36,653.2,3456.1};
      string names[] = {"LHQ","CYK","YJQ","JZW"};
      for(int i=0;i<4;i++){
        cout << setw(6) << names[i] << setw(10) << values[i] << endl;
      }
      return 0;
    }
    

    setwwidth都不截断数值,仅仅影响紧随其后的域,在一个域输出完后域宽度恢复成它的默认值。但其他流格式选项保持有效直到发生改变,如fill

  • 对齐方式

    输出流默认为右对齐文本,为了在上述代码中实现左对齐姓名和右对齐数值,可将程序修改如下

    #include
    #include
    #include
    using namespace std;
    
    int main(){
      double values[] = {1.23,35.36,653.2,3456.1};
      string names[] = {"LHQ","CYK","YJQ","JZW"};
      for(int i=0;i<4;i++){
        cout << setiosflags(ios_base::left) 
          	 << setw(6) << names[i] 
          	 << rsetiosflags(ios_base::left)
          	 << setw(10) << values[i] << endl;
      }
      return 0;
    }
    

    这个程序中,通过使用带参数的setiosflags操纵符来设置左对齐,setiosflags定义在头文件iomanip中。参数ios_base::leftios_base的静态常量。这里需要用resetiosflags操纵符关闭左对齐标志,其影响时持久的

    setiosflags的参数是该流的格式标志值,这个值由如下位掩码指定,并可用位或运算符进行组合

    • skipws跳过空白
    • left左对齐
    • right右对齐
    • internal在规定的宽度内,指定前缀符号之后,数值之前,插入指定的填充字符
    • dec以是十进制形式格式化数值
    • oct以八进制形式格式化数值
    • hex以十六进制形式格式化数值
    • showbase插入前缀符号已表明整数的数制
    • showpoint对浮点数值显示小数点和尾部的0
    • uppercase对于十六进制数显示大写字母AF,对于科学格式显示大写字母E
    • showpos对于非负数显示正号
    • scientific以科学形式显示浮点数值
    • fixed以定点格式显示浮点数值(没有指数部分)
    • unitbuf在每次插入之后转储并清除缓冲区内容
  • 精度

    浮点数输出精度的默认值是6,为了改变精度,可使用操作符setprecision,此外,还有两个标志会改变浮点数的输出格式,即ios_base::fixed ios_base::scientific,如果设置了fixed,精度值表示小数点之后的位数,如果设置了scientific,精度值表示小数点之后的位数

  • 文件输出流成员函数

    输出流成员函数有如下3种类型

    • 与操纵符等价的成员函数
    • 执行非格式化写操作的成员函数
    • 其他修改流状态且不同于操纵符或插入运算符的成员函数

    对于顺序的格式化输出,可以仅使用插入运算符和操纵符,对于随机访问二进制磁盘输出,使用其他成员函数,可以使用或不使用成员函数

    • 输出流的open函数

      要使用一个文件输出流,必须在构造函数或open函数中把该流与一个特定的磁盘文件关联起来,在这两种情况下,描述文件的参数是相通的

      打开一个与输出流关联的文件时,可以指定一个open_mode标志,可以用按位或运算符组合这些标志,它们作为枚举常量定义在ios_base类中,例如

      ofstream file("filename",ios_base::out | ios_base::binary);
      

      其中第二个表示打开模式的参数具有默认值ios_base::out,可以省略

      下面是一些常用标志

      • app:打开一个输出文件用于在文件尾添加数据
      • ate:打开一个现存文件并查找到结尾
      • in:打开一个输入文件,对于一个ofstream文件,使用ios_base::in作为一个open-mode可避免删除一个现存文件中现有的内容
      • out,打开一个文件,用于输出
      • trunc:打开一个文件,如果它已经存在则删除其中原有的内容,如果指定了out,但没有指定ate app in,则隐含为此模式
      • binary:以二进制模式打开一个文件
    • 输出流的close函数

      close成员函数关闭与一个文件输出流关联的磁盘文件。文件使用完毕后必须将其关闭以完成所有磁盘输出。虽然ofstream析构函数会自动完成关闭,但如果需要在同一流对象上打开另外的文件,就需要使用close函数

    • put函数

      put函数把一个字符写到输出流中,下面两个语句默认是相通的,但第二个受该流的格式化参量的影响:

      cout.put('A');
      cout << 'A';
      
    • write函数

      write函数把一个内存中的一块内容写到一个文件输出流中,长度参数指出写的字节数,下面的例子建立一个文件输出流并将Date结构的二进制值写入文件

      #include
      using namespace std;
      struct Data{
        int monday,day,year;
      };
      int main(){
        Date dt = (6,10,92);
        ofstream file("data.dat",ios_base::binary)l
        file.write(reinterpret_cast<char*>(&dt),sizeof(dt));
        file.close();
        return 0;
      }
      

      write函数当遇到空字符的时候并不停止,因此能够写入完整的类结构,该函数带有两个参数:char指针和一个所写的字节数,注意需要用reinterpret_cast将该对象的地址显式转换成char*类型

    • seekptellp函数

      一个文件输出流保存一个内部指针指出下一次写数据的位置,seekp成员函数设置这个指针,因此可以以随机方式向磁盘文件输出,tellp成员函数返回该文件位置指针值

    • 错误处理函数

      错误处理成员函数的作用是在写到一个流时进行错误处理,各函数及其功能

      • bad:如果出现一个不可恢复的错误,则返回一个非0
      • fail:如果出现一个不可恢复的错误或一个预期的条件,例如一个转换错误或者文件未找到,则返回一个非0值,在用零参量调用clear之后,错误标记被清除
      • eof:遇到文件结尾条件,则返回一个非0
      • clear:设置内部错误状态,如果用默认参量调用,则清除所有错误为
      • rdstate:返回当前错误状态

      !运算符经过了重载,它与fail函数执行相同的功能,因此表达式if(!cout)等价于if(cout.fail())

      void*运算符也是经过重载的,与!预算符相反,因此表达式if(cout)等价于if(!cout.fail())

  • 二进制输出文件

    最初设计流的目的是用于文本,因此默认的输出模式是文本方式,在不同操作系统中,文本文件的行分隔符不太一样,例如Linux操作系统下的文本文件以一个换行符’\n’作为行分隔符,而Windows操作系统下的文本文件以一个换行符和一个回车符’\r’作为行分隔符。在以文本模式输出时,没输出一个换行符,都会将当前操作系统下的行分隔符写入文件中,这意味着Windows下输出换行符后还会被自动扩充一个回车符,这种自动扩充有时可能出问题

    同时,使用二进制输出可以增加效率

    #include
    using namespace std;
    
    int a[2] = {99,10};
    int main(){
      ofstream os("test.dat");
      os.write(reinterpret_cast<char*>(a),sizeof(a));
      return 0;
    }
    
  • 字符串输出流

    输出流除了可以用于向屏幕或文件输出信息外,还可以用于生成字符串,这样的流叫做字符串输出流,ostringstream类就用来表示一个字符串输出流

    ostringstream类有两个构造函数,第一个函数有一个形参,表示流的打开模式,与文件输出流中的第二个参数功能相同,表示打开方式,具有默认值ios_base::out,通常使用它的默认值,例如可以用下列方式来创建一个字符串输入流

    ostringstream os;
    

    第二个构造函数接收两个形参,第一个形参是string型常对象,用来为这个字符串流的内容设置初始值,第二个形参表示打开模式,与第一种构造函数的形参具有相同的意义

    专用于文件操作的open close函数是ostringstream类所不具有的

    ostringstream类还有一个特有的函数str,它返回一个string对象,表示用该输出流所生成字符串的内容

    ostringstream类的一个典型应用就是将一个数值转化为字符串

    #include
    #include
    #include
    using namespace std;
    
    template<class T>
    inline string toString(const T&v){
        ostringstream os;
        os << v;
        return os.str();
    }
    
    int main(){
        string s1 = toString(4);
        cout << s1 << endl;
        string s2 = toString(1.2);
        cout << s2 << endl;
        return 0;
    }
    

输入流

构建输入流对象

一个输入流对象是数据流出的源头,3个最重要的输入流类是istreamifstreamistringstream

istream类最适合用于顺序文本模式输入

ifstream类支持磁盘文件输入,如果需要一个仅用于输入的磁盘文件,可以构造一个ifstream类的对象,并且可以指定使用二进制或文本模式,如果在构造函数中指定一个文件名,在构建该对象的时候该文件便自动打开,否则,需要在调用默认构造函数之后用open函数来打开文件

使用提取运算符

提取运算符对于所有标准c++数据类型都是预先设计好的,它是从一个输入流对象获取字节最容易的方式

提起运算符用于格式化文本输入,在提取数据时,以空白符为分隔,如果要输入一段包含空白符的文本,用提取运算符就很不方便,在这种情况下,可以选择使用非格式化输入或者成员函数getline,这样就可以读一个包含有空格的文本块,然后再对其进行分析

输入流操作符

定义在ios_base类中和iomanip头文件中的操纵符可以应用于输入流。但是只有少数几个操纵符对输入流对象具有实际影响,其中最重要的是进制操纵符dec oct hex

在提取中,hex操纵符可以接收处理各种输入流格式,例如c C 0xc 0xC 0Xc 0XC都被解释为十进制数12,任何除0~9 A~F a~fX之外的字符都引起数值变换终止。例如,序列124n5将变换成数值124,并且使fail函数返回true

输入流相关函数
输入流的open函数

如果要使用一个文件输入流,必须在构造函数中的或者open函数把该流与一个特定磁盘文件关联起来,无论用哪种方式,参数是相通的

当打开与一个输入流关联的文件时,通常要指定一个模式标志。模式标志如下所示,该标志可以用按位或运算符进行组合,用ifstream打开文件时,模式的默认值是ios_base::in

输入流的close函数

close成员函数关闭与一个文件输入流关联的磁盘文件

虽然ifstream类的析构函数可以自动关闭文件,但是如果需要使用同一流对象打开另一个文件,则首先要用close函数关闭当前文件

get函数

非格式化get函数的功能与提取运算符很相像,主要的不同点是get函数在读入数据包括空白字符

当输入流结束时,程序读入的值是EOF

getline函数

istream类具有成员函数getline,其功能是允许从输入流中读取多个字符,并且允许指定输入终止字符(默认为换行字符),在读取完成后,从读取的内容中删除该终止字符,然而该成员函数只能将输入结果存在于字符数组中,字符数组的大小是不能自动扩展的,造成了使用上的不便,非成员函数getline可以完成相同的功能,但是可以将结果保存在string类型的对象中,更加方便,这一函数可以接收两个参数,前两个分别表示输入流和保存结果的string对象,第三个参数可选,表示终止字符

string line;
getline(cin,line,"\t");
read函数

read成员函数从一个文件读字节到一个指定的存储器区域,由长度参数确定要读的字节数,如果给出长度参数,当遇到文件结束或者在文本模式中遇到文件结束标记的字符时结束

seekgtellg函数

在文件输入流中,保留着一个指向文件中下一个将读取数据的位置的内部指针,可以用seekg函数来设置这个指针

#include
using namespace std;
int main(){
    int values[] = {3,7,5,0,4};
    ofstream os("integers",ios_base::out|ios_base::binary);
    os.write(reinterpret_cast<char*>(values),sizeof(values));
    os.close();

    ifstream is("integers",ios_base::in|ios_base::binary);
    if(is){
        is.seekg(3*sizeof(int));
        int v;
        is.read(reinterpret_cast<char* >(&v),sizeof(int));
        cout << "The 4th integer in the file is:" << v;
    }
}

使用seekg可以实现面对记录的数据管理系统,用固定长度的记录尺寸乘以记录号便得到相对于文件末尾的位置,然后使用get读这个纪录

tellg成员函数返回当前文件读指针的位置,这个值是streampos类型

#include
using namespace std;
int main(){
    ifstream file("integers",ios_base::in|ios_base::binary);
    if(file){
        while(file){
            streampos here = file.tellg();
            int v;
            file.read(reinterpret_cast<char*>(&v),sizeof(int));
            if(file && v == 0){
                cout << "Position " << here << "is 0" << endl;
            }
        }
    }
    file.close();
    return 0;
}
字符串输入流

字符串输入流提供了与字符串输出流相对应的功能,它可以从一个字符串中读取数据,istringstream类就用来表示一个字符串输出流

istringstream类有两个构造函数,最常用的构造函数接收两个参数,分别表示要输入的string对象和流的打开方式,打开模式默认为in,可以用下列方式创建一个字符串输出流

string str = ...;
istringstream is(str);

例如,可以把字符串变成数值

template <class T>
  T StringToValue(const string &str){
  istringstream is(str);
  T v;
  is>>v;
  return v;
}

输入输出流

一个iostream对象可以是数据的源或母的,有两个重要的I/O流类都是从iostream派生的,它们是fstreamstringstream,这些类继承了前面描述的istreamostream类的功能

fstream类支持磁盘文件输入和输出,如果需要在同一个程序中从一个特定磁盘文件读并写到该磁盘文件,可以构建一个fstream对象,一个fstream对象是有两个逻辑子流的单个流,两个子流一个用于输入,另一个用于输出

stringstream类支持面向字符串的输入和输出,可以对同一个字符串的内容交替读写

异常处理

在编写应用软件时,不仅要保证软件的正确性,而且应该具有容错能力,也就是说,不仅在正确的环境条件下,在用户正确操作时要运行正确,而且在环境条件出现意外或用户使用操作不当的情况下,也应该有正确合理的处理方法,不能轻易出现死机,更不能出现灾难性的后果,由于环境条件和用户操作的正确性是没有保障的,所以我们在设计程序时,就要充分考虑到各种意外情况,并继续恰当的处理,这就是异常处理

异常处理的基本思想

  • 异常就是程序在运行过程中出现问题
  • 异常问题并不经常出现
  • 异常处理技术使我们可以写程序解决出现的异常问题
  • 很多情况下,处理了异常就能使程序像未发生问题一样继续进行
  • 严重的问题也许使程序不能继续执行,使用异常处理可以用可控方式对用户发出警示,然后再终止程序
  • 本章讲的是异常处理技术,使我们能够编写更健壮、更具有容错性的程序,能处理发生的问题使程序继续进行,或者优雅地终止
应该如何处理异常的思考
  • 若频繁检测条件,如下

    执行任务
    如果任务执行不正确
      	执行错误处理
    执行下一项任务
    如果任务执行不正确
      执行错误处理
    
  • 上面的伪代码中,没执行以下任务就要检测成功与否,使得程序逻辑与错误处理逻辑混在一起,使得程序的可读性、可修改性、可维护性差,难以调试

  • 异常处理技术使我们可以将错误处理代码从主逻辑中移出,使程序更清晰,能增强程序的可维护性

  • 同时,我们可以有选择地处理异常:所有异常、某种特定类型的所有异常、一组相关类型的所有异常

何时使用异常处理
  • 异常处理用于处理同步错误,也就是执行语句时发生的错误
  • 异常处理不是用于处理异步事件相关的错误的,例如:磁盘读写结束、网络信息到达、,这些与程序的控制流程并行、独立
  • 异常处理机制还适用于处理程序与其他软件元素,例如成员函数、构造函数、析构函数、类交互时出现的问题
  • 复杂的应用程序通常由预定义的软件组件,和调用预定义组建的软件组件构成
  • 当预定义组件遇到异常问题时,需要一种机制将问题通知给调用者,预定义组件无法事先预知到不同的应用程序打算怎样处理异常问题

异常处理的语法

  • 抛掷异常的程序段

    ...
    throw 表达式;
    ...
    
  • 捕获并处理异常的程序段

    try
      复合语句
    catch(异常声明)
      复合语句
    catch(异常声明)
      复合语句
    
  • 若有异常则通过throw创建一个异常对象并抛掷

  • 将可能抛出异常的程序段嵌在try块之中,通过正常的顺序执行到达try语句,然后执行try块后的最后一个catch子句后面的语句继续执行

  • catch子句按其在try块后出现的顺序被检查,匹配的catch子句将捕获并处理异常或继续抛掷异常,或部分处理然后继续抛掷

  • 如果匹配的处理器未找到,则库函数terminate将被自动调用,其默认是调用abort终止程序

异常声明

  • 可以在函数的声明中列出这个函数可能抛掷的所有异常类型

    如:void fun() throw(A,B,C,D);

  • 若无异常接口声明,则此函数可以抛掷任何类型的异常

  • 不抛掷任何类型异常的函数声明如下

    void fun() throw();
    void fun() noexcept;
    

    尽量避免使用异常声明的情况有三种

    • 带类型参数的函数模版
    • 使用回调函数时
    • 系统可能抛出的异常

异常处理中的构造与析构

自动的析构

  • 找到一个匹配的catch异常处理后
    • 初始化异常参数
    • 将从对应的try块开始到异常被抛掷处之间的构造(且尚未析构)的所有自动对象进行析构
    • 从最后一个catch处理之后开始恢复执行

标准异常类的继承关系

标准异常类的基础
  • exception标准程序库异常类的公共基类
  • logic_error表示可以在程序中被预先检测到的异常
  • runtime_error表示难以被预先检测到的异常

例:三角形面积计算

#include
#include
#include
using namespace std;
double area(double a,double b,double c) throw(invalid_argument){
    if(a <= 0 || b <= 0 || c <= 0)
        throw invalid_argument("the side length should be positive");
    if(a + b <= c || b + c <= a || c + a <= b)
        throw invalid_argument("the side length should fit the triangle inequation");
    double s = (a + b + c) / 2;
    return sqrt(s * (s - a) * (s - b) * (s - c));
}
int main(){
    double a,b,c;
    cin >> a >> b >> c;
    try{
        double s = area(a,b,c);
        cout << "Area:" << s;
    }catch(exception &e){
        cout << "Error:" << e.what() << endl; 
    }
    return 0;
}

关于是否使用异常的争议(By 知乎)

  • 异常的好处很明显
    • 异常不能被忽略
    • 简化代码,使正常代码的逻辑结构看起来更加清晰
    • 异常可以让构造函数更加方便地报错
  • 异常带来的问题也很多
    • 保证代码异常安全,难度大
    • 引入异常,也就引入了一个隐含的执行路径,很多时候,你根本不知道一个函数会不会抛出异常,可能抛出哪些异常
    • c++异常不带调用栈信息,当在外层捕获到下层自动传播来的异常时,下层已经没了,这时候反而可能是手动向上层层传递的错误码+错误日志更有利于定位到真正的问题
    • 性能
    • 由于需要stack unwind,二进制代码文件的大小会比较大
  • 小结
    • 错误码和异常是两种不同的错误处理风格,使用错误码或异常其实各有优劣。c++没有强制开发者只能使用其中一种,重要的是在团队和项目中保持一致。在一般团队中,要用好异常还是有一定挑战的

你可能感兴趣的:(c++基础,c++)