高程复习

高程复习

C++与C的关系

  • C++包含了C的所有成分
  • 添加了:
    1. 更好的支持过程式编程,提高与类型相关的安全性
    2. 支持面向对象
    3. 支持泛型
  • 带参数的宏定义的缺点:
    1. 需要加上很多括号
    2. 会出现重复计算 (e.g.
      ```max(x+1,y*2)```)
    3. 不进行类型检查和转换
    4. 不利于一些工具对程序的处理
  • 内联函数(inline)
    1. 建议编译程序直接把该函数的函数体展开到调用点
    2. 属于函数,会进行参数类型检查和转换
    3. 递归函数不能作为内联函数
    4. 内联函数名具有文件作用域。
  • 带缺省值的形式参数
    1. 仅用于函数声明中
    2. 在不同的源文件中可以对参数指定不同的默认值。
    3. 同一个源文件中,对同一个函数的声明只能对他的每一个参数指定一次默认值。
void f(int a, int b, int c=2); //Ok,函数f的声明
......
void f(int a, int b=1, int c=0);  //Error,对参数c指定了两次默认值
  • 具有文件作用域的标识符可以用无名的名空间来定义

例如,对于下面用static说明的具有文件作用域的全局变量:

static int x,y; //C语言的做法

可用下面的无名的名空间来代替:

namespace
{  
    int x,y; //x和y只能在本源文件中使用!
}
  • new会调用对象类的构造函数进行对象初始化,malloc不会,delete会调用对象类的析构函数,free则不会。

  • 引用类型比指针类型安全,可以保证通过形参访问的永远是实参的数据。

  • 不要把局部量的引用返回给调用者(内存空间已无效)
  • 绑定:确定一个对重载函数的调用对应着哪一个重载函数的定义过程称为绑定,一般由编译时刻根据实参与形参的绑定状况决定
  • 绑定规则:从形参个数与实参个数相同的重载函数中,按下面的次序选择一个:
    1. 精确匹配
    2. 提升匹配
    3. 标准转换匹配
    4. 自定义转换匹配
    5. 匹配失败
  • 标准转换匹配
    1. 任何算术类型可以互相转换
    2. 枚举类型可以转换成任何算术类型
    3. 零可以转换成任何算术类型或指针类型
    4. 任何类型的指针可以转换成void *
    5. 每个标准转换都是平等的,不存在哪个优先:如果存在多个标准转换后能精确匹配,则失败!(具有歧义)

抽象与封装

  • 对于一个程序实体而言,

    1. 抽象是指该程序实体外部可观察到的行为,使用者不考虑该程序实体的内部是如何实现的。(复杂度控制)
      2.封装是指把该程序实体内部的具体实现细节对使用者隐藏起来,只对外提供一个接口。(信息保护)
  • 过程抽象和封装
    • 过程抽象:用一个名字来代表一段完成一定功能的程序代码,代码的使用者只需要知道代码的名字以及相应的功能,而不需要知道对应的程序代码是如何实现的。
    • 过程封装:把命名代码的具体实现隐藏起来(对使用者不可见,或不可直接访问),使用者只能通过代码名字来使用相应的代码。命名代码所需要的数据是通过参数来获得,计算结果通过返回值机制返回。
    • 实现过程抽象与封装的程序实体通常称为子程序。在C/C++语言中,子程序用函数来表示。
    • 过程抽象与封装是基于功能分解与复合的过程式程序设计的基础。
    • 数据是公开的,缺乏保护。
  • 数据抽象和封装
    • 数据抽象:只描述对数据能实施哪些操作以及这些操作之间的关系,数据的使用者不需要知道数据的具体表示形式(数组或链表等)。
    • 数据封装:把数据及其操作作为一个整体(封装体)来进行实现,其中,数据的具体表示被隐藏起来(使用者不可见,或不可直接访问),对数据的访问(使用)只能通过封装体对外接口中提供的操作来完成。
    • 与过程抽象与封装相比,数据抽象与封装能够实现更好的数据保护。
  • 不封装可能导致的问题:
    1. 操作必须知道数据的具体表示形式(如:数组)。
    2. 数据表示形式发生变化(数组改成链表)将会影响操作。
    3. 麻烦并易产生误操作,因此不安全。
    4. 忘了写初始化操作
  • 数据封装可能导致的问题:
    1. 数据定义与操作的定义是分开的,二者之间没有必然的联系,仅仅依靠操作的参数类型把二者关联起来。
    2. 数据表示仍然是公开的,无法防止使用者直接操作栈数据,因此也会面临直接操作栈数据所面临的问题。
    3. 忘记初始化

面向对象程序设计概述

  • 面向对象程序设计具有以下几个特征:
    • 程序由若干对象组成,每个对象是由一些数据以及对这些数据
      所能实施的操作所构成的封装体;
    • 对数据的操作是通过向包含数据的对象发送消息(调用对象对
      外接口中的操作)来实现的,体现了数据抽象;
    • 对象的特征(包含哪些数据与操作)由相应的类来描述;
    • 一个类所描述的对象特征可以从其它的类继承(获得)。
  • 类和对象
    • 对象是由数据以及能对其实施的操作所构成的封装体。
    • 类描述了对象的特征(包含什么类型的数据和哪些操
      作),实现数据抽象。
  • 在程序中,多态通常体现为:
    • 一名多用:
    • 函数名重载
    • 操作符重载(语言预定义和用户自定义)
    • 类属:
    • 类属函数:一个函数能对多种类型的数据进行相同的操作。
    • 类属类型:一个类型可以描述多种类型的数据。
  • 影响软件开发效率和软件质量的因素主要包括:
    • 抽象(控制复杂度)
    • 封装(保护信息)
    • 模块化(组织和管理大型程序)
    • 软件复用(缩短开发周期)
    • 可维护性(延长软件寿命)
    • 软件模型的自然度(缩小解题空间与问题空间之间的语义间隙,实现从问题到解决方案的自然过渡)

对象与类

  • 对象构成了面向对象程序的基本计算单位,而对象的特征则由相应的类来描述。
  • 在类中说明一个数据成员的类型时,如果未见到相应类型的定义,或相应的类型未定义完,则该数据成员的类型只能是这些类型的指针或引用类型.
  • 若类中出现了函数体,则他是一个内联函数。
  • 类的protected可以被派生类访问

构造函数和析构函数

  • 默认构造函数在构造时不能写成A a();
  • 类中的常量和引用函数在C++11前不可以在构造函数中赋值,应在成员构造表中说明
  • 成员构造表的顺序无关,初始化次序和在类中的定义有关。
  • 析构函数可以显式调用,这时并不是让对象消亡,而是暂时归还对象额外申请的资源。如调用栈的析构函数=清空栈。
  • 析构次序是在类中定义的次序的逆序。

拷贝构造函数

  • 三种情况会调用拷贝构造函数:
    1. 构造时显式指出
    2. 函数调用作为形参
    3. 函数作为返回值

常成员函数和静态成员

  • 为了防止在一个获取对象状态的成员函数中无意中修改对象数据成员的值,可以把它说明成常成员函数
  • 有些修改对象状态的操作不会报错
void f() const 
{   
    x = 10; //Error
    p = new char[20]; //Error 
    strcpy(p,"ABCD"); //因为没有改变p的值,编译程序认为OK!
}
  • 对常量对象只能调用类中的常成员函数
  • 使用全局变量表示一个类中的共享变量缺点:
    1. 共享的数据与对象之间缺乏显式的联系
    2. 数据缺乏保护(可以不通过A类访问数据)
  • 静态成员函数
    • 只能访问静态成员
    • 没有this参数
  • 静态成员变量需在main函数外定义A::x
  • 定义后可用A::x或a.x访问

友元

  • 友元是数据保护和数据访问效率之间的一种折衷方案。
  • 友元需要在类中用friend显式指出,它们可以是
    • 全局函数
    • 其它类的所有成员函数
    • 其它类的某个成员函数
  • 友元不具有传递性和对称性

类作为模块

  • 模块
    • 从物理上对程序中定义的实体进行分组,是可以单独编写和编译的程序单位。
    • 模块化是组织和管理大型程序的一个重要手段。
  • 一个模块包含接口和实现两部分:
    • 接口:是指在模块中定义的、可以被其它模块使用的一些程序实体的声明描述。
    • 实现:是指在模块中定义的所有程序实体的具体实现描述。
  • 划分模块的基本准则:
    • 内聚性最大:模块内的各实体之间联系紧密。
    • 耦合度最小:模块间的各实体之间关联较少。
    • 便于程序的设计、理解和维护,能够保证程序的正确性。
  • 过程式程序(C语言支持的)的模块划分: 子程序
  • Demeter法则:
    • 减少类之间的关联度(耦合度)
    • 要减少类间的关联度,可以对类中成员函数能访问的其它类/对象的集合作一定的限制,尽量使该集合为最小。
  • 对于类C中的任何成员函数M,M中只能向以下类的对象发送消息:
    • 类C本身。
    • 成员函数M的参数类。
    • M或M所调用的成员函数中创建的对象所属的类。
    • 全局对象所属的类。
    • 类C的成员对象所属的类。

操作符重载

  • 操作符重载函数可以作为:
    • 一个类的非静态的成员函数(操作符new和delete除外)。
    • 一个全局(友元)函数。
  • 可以重载C++中除下列操作符外的所有操作符:“. ”, “.* ”,“?: ”,“:: ”,“sizeof ”
  • 操作符重载的形式:a+b 或 a.operator+(b)
  • 操作符重载有时候只能用全局函数(第一个参数不是原类的对象)
  • 操作符++重载:
    • ++x为左值表达式,x++是右值表达式 ((++x)++ x加2)
    • 为了能够区分++(--)的前置与后置用法,可以为后置用法再写一个重载函数,该重载函数应有一个额外的int型参数
const Counter operator ++(int);
c++;
String s1("xyz"),s2("abcdefg");
s1 = s2;  

-
+ s1.str原来指向的空间丢失了(内存泄露)
+ s1和s2互相干扰
+ s1和s2消亡时,"abcdefg"所在的空间将会被释放两次!
+ 区别以下=使用:
A a=b;(构造函数)
a=b;
+ 函数调用操作符重载operator()主要用于具有函数性质的对象(函数对象,functor),该对象通常只有一个操作,它除了具有一般函数的行为外,它还可以拥有状态。
+ 重载操作符new:
- 必须作为静态的成员函数来重载(static说明可以不写),其格式为: void operator new(size_t size);
- 返回类型必须为void *
- 参数size表示对象所需空间的大小,其类型为size_t(unsigned int)
- 可以带其它参数void
operator new(size_t size, int x);
+ 重载操作符delete:
- 其格式为: void operator delete(void *p, size_t size);
- 返回类型必须为void。
- 第一个参数类型为void *,指向对象的内存空间。
- 第二个参数可有可无,如果有,则必须是size_t类型。
+ 当类(或基类和成员对象类)中有析构函数时,传给new[]重载函数的参数size的实际值会比对象数组需要的空间多4个字节,用于存储元素个数!例如,假设类A有析构函数,则

A *p=new A[10]; //size:sizeof(A)*10+4
  • 也可以定义从一个类到其它类型的转换。例如:
class A
{  
  int x,y;
  public:
   ......
   operator int() //类型转换操作符int的重载函数
   { return x+y; 
   }
};
A a;
int i=1;
(i + a) //将调用类型转换操作符int的重载函数
        //把对象a隐式转换成int型数据。
  • 可能造成歧义问题:使用explicit关键字加载构造函数或operator int()前

继承

  • 即使派生类中定义了与基类同名但参数不同的成员函数,基类的同名函数在派生类的作用域中也是不直接可见的,仍然需要用基类名受限方式来使用
  • 也可以在派生类中使用using声明把基类中某个的函数名对派生类开放(using A::f)
  • 不同继承关系如下:
    下方是继承方式,右边是基类成员
private protected public
private 不可访问 private private
protected 不可访问 protected protected
public 不可访问 protected public
  • 如果一个类D既有基类B、又有成员对象类M,则
    • 在创建D类对象时,构造函数的执行次序为:B->M->D
    • 当D类的对象消亡时,析构函数的执行次序为:D->M->B

消息的动态绑定

  • C++默认采用的是静态绑定,例:
void func1(A& x)
{   
  x.f(); //调用A::f还是B::f ?A
}
void func2(A *p)
{   
  p->f(); //调用A::f还是B::f ?A
}

A a;
func1(a);
func2(&a);
B b;
func1(b);
func2(&b);
  • 在C++中,在基类中用虚函数指明动态绑定。
void func1(A& x)
{   ......
  x.f(); //调用A::f或B::f
  ......
}
void func2(A *p)
{   ......
  p->f(); //调用A::f或B::f
  ......
}
......
A a;
func1(a); //在func1中调用A::f
func2(&a); //在func2中调用A::f
B b;
func1(b); //在func1中调用B::f
func2(&b); //在func2中调用B::f
  • 对于基类中的一个虚函数,在派生类中定义的、与之具有相同型构的成员函数是对基类该成员函数的重定义(或称覆盖,override)。
  • 相同的型构是指:
    • 派生类中定义的成员函数的名字、参数个数和类型与基类相应成员函数相同;
    • 其返回值类型与基类成员函数返回值类型或者相同,或者是基类成员函数返回值类型的public派生类。
  • 只有类的成员函数才可以是虚函数,但静态成员函数不能是虚函数。
  • 构造函数不能是虚函数,析构函数可以(往往)是虚函数。
  • 只要在基类中说明了虚函数,在派生类、派生类的派生类、...中,与基类同型构的成员函数都是虚函数(virtual可以不写)。
  • 只有通过基类的指针或引用访问基类的虚函数时才进行动态绑定。
  • 基类的构造函数和析构函数中对虚函数的调用不进行动态绑定。(事实上,基类的this指针是基类)

抽象类

  • 包含纯虚函数的类称为抽象类,抽象类的作用是为派生类提供一个基本框架和一个公共的对外接口。

多继承

  • 基类的声明次序决定:
    • 对基类数据成员的存储安排。
    • 对基类构造函数/析构函数的调用次序
  • 多继承带来的两个主要问题:
    • 名冲突问题:基类名受限
    • 重复继承问题:虚基类
  • 虚基类
    • class A: virtual public B
    • 创建包含虚基类的类对象时:
    1. 虚基类的构造函数由该类的构造函数直接调用。
    2. 虚基类的构造函数优先非虚基类的构造函数执行。

聚合和组合

  • 在聚合关系中,被包含的对象与包含它的对象独立创建和消亡,被包含的对象可以脱离包含它的对象独立存在。例如,一个公司与它的员工之间是聚合关系。class Employee; class Company{Employee* e[xxx]};
  • 在组合关系中,被包含的对象随包含它的对象创建和消亡,被包含的对象不能脱离包含它的对象独立存在。例如,一个人与他的头、手和脚之间则是组合关系。
  • 组合不存在破坏封装的问题

输入输出

  • scanf类型不安全
  • istream和ostream:
    • 输入
      istream in(...); //创建istream类的一个对象in
      in.get(ch); //读入一个字节,ch为一个字符变量
      in.read(p,100); //读入100个字节,p是内存空间首地址
      in.get(ch).read(p,100);
      
    • 输出
      ostream out(...); //创建ostream类的一个对象out
      out.put(ch); //输出一个字节,ch为一个字符变量
      out.write(p,100); //把p指向的内存中100个字节输出
      out.put(ch).write(p,100);
      
  • cerr带缓冲 clog不带
  • 输出指向字符的指针不会输出指针地址,需转换为其他类型(void *)指针才能输出地址
  • 输出操纵符
    • hex, dec, oct 十六进制,十进制,八进制(永久改变)
    • setprecision(int n)设定精度
    • setiosflags(long flag) 其中flag取ios::fixed 或者 ios::scientific
    • 如果不设置fixed或scientific输出格式会自动确定
  • setw可以设置输入最多的字符数(setw(10)后输入char*最多输入9个)
  • cin.ignore(20,'\n'); //跳过输入缓存中20个字符,或碰到回车
  • ofstream out_file(,ios::app或者ios::out)
    • ios::app向末尾加
    • ios::out清空
    • ios::out | ios::binary 或 ios::app | ios::binary 二进制
    • 按文本打开在某些平台上会把'\n'变为'\r''\n'但按二进制不会
    • .is_open()判断是否成功打开
  • ifstream in_file(,ios::in)
    • 若按二进制读取x:in_file.read((char *)&x,sizeof(x));
    • 以二进制方式存取文件不利于程序的兼容性和可移植性。例如,
      在不同计算机平台上,int数的各个字节在内存中的存储次序可能不一样。在不同的编译环境下,同样的结构类型数据所占的内存大小(字节数)可能不一样。
  • fstream
    • ios::in|ios::out(可在文件任意位置写)
    • ios::in|ios::app(只能在文件末尾写)
    • 下面的操作用来指定文件内部读指针的位置:
      istream& istream::seekg(<位置>);//指定绝对位置
      istream& istream::seekg(<偏移量>,<参照位置>);  //指定相对位置
      streampos istream::tellg();  //获得指针位置
      
    • 下面的操作来指定文件内部写指针的位置:
      ostream& ostream::seekp(<位置>);//指定绝对位置
      ostream& ostream::seekp(<偏移量>,<参照位置>); //指定相对位置
      streampos ostream::tellp();  //获得指针位置 
      
    • <参照位置>可以是:ios::beg(文件头),ios::cur(当前位置)和ios::end(文件尾)。
    • 文件的随机存取一般用于以二进制方式存贮的文件。

基于事件驱动的程序设计

  • Windows是一种基于图形界面的多任务操作系统。
    • 系统中可以同时运行多个程序。
    • 每个程序通过各自的“窗口”与用户进行交互。
    • 用户通过鼠标的单击/双击/拖放、菜单选择以及键盘输入来与程序进行交互。
  • Windows的功能以两种方式提供:
    • 工具(程序):资源管理器、记事本、画图、......,供用户(人)使用。
    • 函数库:以C语言函数形式出现(在windows.h等头文件中申明),作为应用程序接口(API),供Windows应用程序(运行在Windows系统上的应用程序)使用。
  • Windows应用程序类型:
    • 单文档应用
    • 只能对一个文档的数据进行操作。
    • 必须首先等当前文档的所有操作结束之后,才能进行下一个文档的操作。
    • 多文档应用
    • 同时可以对多个文档的数据进行操作。
    • 不必等到一个文档的所有操作结束,就可以对其它文档进行操作,对不同文档的操作是在不同的子窗口中进行的。
    • 对话框应用
    • 以对话框的形式操作一个文档数据。
    • 对文档数据的操作以各种“控件”来实现。
    • 程序以按<确定>或<取消>等按钮来结束。
  • Windows应用程序采用的是一种事件(消息)驱动的交互式流程控制结构:
    • 程序的任何一个动作都是由某个事件激发的。
    • 事件可以是用户的键盘、鼠标、菜单等操作。
  • 每个事件都会向应用程序发送一些消息
    • WM_KEYDOWN/WM_KEYUP(键盘按键)
    • WM_CHAR(按键有对应的字符)
    • WM_LBUTTONDOWN/WM_LBUTTONUP/WM_LBUTTONDBLCLK WM_MOUSEMOVE (鼠标按键)
    • WM_COMMAND(菜单选取)
    • WM_PAINT(窗口内容刷新)
    • WM_TIMER(设置的定时器时间到了)
  • 每个应用程序都有一个消息队列。
    • Windows系统会把属于各个应用程序的消息放入各自的消息队列。
    • 应用程序不断地从自己的消息队列中获取消息并处理之。
  • “取消息-处理消息”的过程称为消息循环。
    • 当取到某个特定消息(如:WM_QUIT)后,消息循环结束。
    • 每个窗口都有一个消息处理函数。
    • 大部分的消息都关联到某个窗口。
    • 应用程序取到消息后将会去调用相应窗口的消息处理函数。
  • 注意:每个消息的处理时间不宜太长,否则会造成程序“假死”现象(程序不响应其它消息)。
  • 每个Windows应用程序都必须提供一个主函数WinMain,其主要功能是:
    • 注册窗口类(定义程序中要创建的窗口类型):
    • 窗口的基本风格、消息处理函数、图标、光标、背景颜色以及菜单等。
    • 每类窗口(不是每个窗口)都需要注册。
    • 根据注册的窗口类创建应用程序的主窗口(程序的其它窗口等到需要时再创建)。
    • 进入消息循环,直到接收到WM_QUIT消息时,消息循环结束。
#include <windows.h> //Windows所提供的API声明文件。
int APIENTRY WinMain(HINSTANCE hInstance, //本实例标识(Handle)
                        HINSTANCE hPrevInstance, //上一个实例标识
                        LPSTR lpCmdLine, //命令行参数    
                        int  nCmdShow ) //主窗口的初始显示方式
{   //注册窗口类(下面是个示意,函数参数实际为一个结构WNDCLASS)
    RegisterClass(..., WindowProc, "my_window_class"); //示意
    ......
    //创建并显示主窗口
    HWND hWnd; //窗口的id
    hWnd=CreateWindow("my_window_class",…,x,y,width,height,...); 
    ShowWindow(hWnd, nCmdShow);
    ......
    //消息循环,直到接收到WM_QUIT消息
    while (GetMessage(&msg, NULL, 0, 0)) //从消息队列中取消息。
    {   ......
        DispatchMessage(&msg); //把消息发送到程序相应的窗口。
    }
    return msg.wParam;
}
LRESULT CALLBACK WindowProc(HWND hWnd, //窗口标识
                                UINT message, //消息标识
                                WPARAM wParam, //消息的参数1
                                LPARAM lParam ) //消息的参数2
{   switch (message)
    {   case WM_KEYDOWN:
           ...wParam... //wParam为按键的名字
           ......
        case WM_CHAR: //字符键消息
            ...wParam... //wParam是按键对应字符的编码
            ......
        case WM_COMMAND: //菜单消息
            switch (wParam) //wParam是菜单项的标识
            { case ID_FILE_OPEN: //打开文件
            ......
               case ID_START_TIMER: //启动定时器
            SetTimer(hWnd,1,1000,NULL);
            ......
               ......
            }
            ......  
        case WM_LBUTTONDOWN: //鼠标左键按下消息
            ...lParam... //lParam为鼠标在窗口中的位置
            ......
        case WM_PAINT: //窗口刷新消息
           ......
        case WM_TIMER: //定时器消息
           ...wParam... //wParam是定时器编号
           ......
        case WM_CLOSE: //关闭窗口消息
           DestroyWindow(hWnd); //撤销窗口
           break;
        case WM_DESTROY: //窗口被撤销消息
           PostQuitMessage(0); //往本应用的消息队列中放入WM_QUIT
           break;
        default: //由系统进行默认的消息处理
            return DefWindowProc(hWnd,message,wParam, lParam);
    }
    return 0;
}
  • 基于Windows API的事件驱动程序设计属于过程式程序设计范式。
    通过调用API函数编写程序的粒度太细、太繁琐,开发效率不高。

  • Windows应用程序中的对象

    • 窗口对象
    • 显示程序的处理数据。
    • 处理Windows的消息、实现与用户的交互。
    • 窗口对象类之间可以存在继承和组合关系。
    • 文档对象
    • 管理在各个窗口中显示和处理的数据。
    • 文档对象与窗口对象之间可以存在关联关系。
    • 应用程序对象
    • 管理属于它的窗口对象和文档对象。
    • 实现消息循环。
    • 它与窗口对象及文档对象之间构成了组合关系。

异常处理

  • 就地异常处理通常使用cstdlib中的exit 或 abort函数
    • abort立即终止程序的执行,不作任何的善后处理工作。
    • exit在终止程序的运行前,会做关闭被程序打开的文件、调用全局对象和static存储类的局部对象的析构函数(注意:不要在这些对象类的析构函数中调用exit)等工作。
  • 使用函数的返回值来进行异地异常处理:
    • 通过函数的返回值返回异常情况会导致正常返回值和异常返回值交织在一起,有时无法区分。
    • 通过指针/引用类型的参数返回异常情况,需要引入额外的参数,给函数的使用带来负担。
    • 通过全局变量返回异常情况会导致使用者会忽略这个全局变量的问题。(不知道它的存在)
    • 程序的可读性差!程序的正常处理代码与异常处理代码混杂在一起,不能显式地区分它们。
  • catch语句采用精确匹配与throw所产生的异常对象进行绑定
  • try/catch语句可以嵌套,找不到会逐步向外面找。
  • 有了异常处理机制后,一个函数的对外接口说明:除了要包含函数的功能描述以及函数的参数和返回值的含义描述之外,还要包含函数可能产生的异常描述。
  • 断言的作用:
    • 帮助对程序进行理解和形式化验证。
    • 在程序开发阶段,帮助开发者发现程序的错误和进行错误定位。

泛型

  • 一个程序实体能对多种类型的数据进行操作的特性称为类属(Generics)。具有类属特性的程序实体通常有:
    • 类属函数:一个能对不同类型的数据完成相同操作的函数。
    • 类属类:一个成员类型不同、但数据表示和操作相同的类。
  • 要使用函数模板所定义的函数,首先必须要对函数模板进行实例化:
    给模板参数提供一个具体的类型,从而生成具体的函数。
    函数模板的实例化通常是隐式的:
    由编译程序根据函数调用的实参类型自动地把函数模板实例化为具体的函数。
    这种确定函数模板实例的过程叫做模板实参推导(template argument deduction)。
  • 显式实例化:max(int,double)可以用max\(int,double)解决
  • 带非类型参数的函数模板需要显示实例化
  • 模板也属于一种多态,称为参数化多态:
    • 一段带有类型参数的代码,给该参数提供不同的类型就能得到多个不同的代码,即,一段代码有多种解释。
    • 模板的复用是通过对模板进行实例化(用一个具体的类型去替代模板的类型参数)来实现的。
    • 由于实例化是在编译时刻进行的,它一定要见到相应的源代码,因此,模板属于源代码复用。
  • 类模板的友元函数:与实例是一对一的友元
template <class T> //类模板A的定义
class A
{ T x,y;
  ......
  friend void f(A<T>& a); //f是多个重载的函数,它们与A的实例是一对一友元!
};
void f(A<int>& a) { ...... } //该f仅是A<int>的友元,
void f(A<double>& a) { ...... } //该f仅是A<double>的友元
......
A<int> a1; //实例化A<int>
A<double> a2; //实例化A<double>
A<char *> a3; //实例化A<char *>
f(a1); //调用f(A<int>&)
f(a2); //调用f(A<double>&)
f(a3); //调用f(A<char *>&),但连接时出错,该函数不存在!

应该修改为:

template <class T> class A; //类模板A的声明(f的定义中要用到)
template <class T> void f(A<T>& a) { ...... } //f是函数模板

template <class T> //类模板A的定义
class A
{ T x,y;
  ......
  template <class T1> friend void f(A<T1>& a); //整个模板f是友元,
                    //f的实例与A的实例是多对多友元
};
......
A<int> a1; //实例化A<int>
A<double> a2; //实例化A<double>
A<char *> a3; //实例化A<char *>
f(a1); //实例化f<int>并调用之,它是A所有实例的友元
f(a2); //实例化f<double>并调用之,它是A所有实例的友元
f(a3); //实例化f<char *>并调用之,它是A所有实例的友元

基于STL的编程

  • STL中包含:
    • 容器
    • 迭代器
    • 算法
  • STL中的主要容器:
    • vector 用于需要快速定位(访问)任意位置上的元素以及主要在元素序列的尾部增加/删除元素的场合 使用动态数组实现
    • list 用于经常在元素序列中任意位置上插入/删除元素的场合 使用双向链表实现
    • deque 用于主要在元素序列的两端增加/删除元素以及需要快速定位(访问)任意位置上的元素的场合,用分段的连续空间结构实现。
    • stack 用于仅在元素序列的尾部增加/删除元素的场合。基于deque、list或vector来实现。
    • queue 用于仅在元素序列的尾部增加、头部删除元素的场合,基于deque或list来实现。
    • priority_queue 基于dequeu或vector实现
    • 后三种容器作为适配器实现,(可以选择底层容器的实现)
  • 迭代器的类型:
    • 输出迭代器(output iterator,记为:OutIt)
    • 只能修改它所指向的容器元素
    • 间接访问(*)
    • ++
    • 输入迭代器(input iterator,记为:InIt)
    • 只能读取它所指向的容器元素
    • 间接访问(*)和元素成员间接访问(->)
    • ++、、!=。
    • 前向迭代器(forward iterator,记为:FwdIt)
    • 可以读取/修改它所指向的容器元素
    • 元素间接访问(*)和元素成员间接访问(->)
    • ++、、!=
    • 双向迭代器(bidirectional iterator,记为:BidIt)
    • 可以读取/修改它所指向的容器元素
    • 元素间接访问(*)和元素成员间接访问(->)
    • ++、--、、!=操作
    • 随机访问迭代器(random-access iterator,记为:RanIt)
    • 可以读取/修改它所指向的容器元素
    • 元素间接访问(*)、元素成员间接访问(->)和下标访问元素([])
    • ++、--、+、-、+=、-=、、!=、<、>、<=、>=
    • 注意:指向数组元素的普通指针可以看成是随机访问迭代器
    • 对于vector、deque以及basic_string容器类,与它们关联的迭代器类型为随机访问迭代器(RanIt)
    • 对于list、map/multimap以及set/multiset容器类,与它们关联的迭代器类型为双向迭代器(BidIt)。
  • 算法:
    • count_if(begin,end,f)
    • 目标范围内已有元素个数不能小于源范围中元素个数,如果操作需要在目标范围内增加元素,则目标范围的迭代器要用某个插入迭代器。
      例如,下面第三个copy操作的目标范围采用了可在容器尾部增加元素的迭代器(用函数back_inserter获得):
    vector<int> v1{1,2,3,4},v2{5,6,7,8},v3;
    copy(v1.begin(),v1.end(),v2.begin()); //v2:1,2,3,4
    copy(v1.begin(),v1.end(),v3.begin()); //运行出错
    copy(v1.begin(),v1.end(),back_inserter(v3)); 
    
    • T accumulate(InIt first, InIt last, T val); //按“+”操作
    • T accumulate(InIt first, InIt last, T val, BinOp op);
    • transform:
    int f1(int x) { return x*x; }
    int f2(int x1, int x2) { return x1+x2; }
    vector<int> v1,v2,v3,v4;
    ...... //往v1和v2容器中放了元素
    transform(v1.begin(),v1.end(),back_inserter(v3),f1); //v3中的元素是v1相应元素的平方
    transform(v1.begin(),v1.end(),v2.begin(),back_inserter(v4),f2);//v4中的元素是v1和v2相应元素的和
    
    • copy_if(nums.begin(),nums.end(), back_inserter(positives),[](int x) {return x>0;});
    • for_each(positives.begin(),positives.end(),[](int x) { cout << x <<','; });
  • 使用函数对象解决有状态的匹配函数问题:

class MatchMajor
{      Major major;
    public:
       MatchMajor (Major m)
       {    major = m;
       }
       bool operator ()(Student& st)
       { return st.get_major() == major;
       }
};
count_if(students.begin(),students.end(),MatchMajor(COMPUTER));

函数式编程

  • Fty2 bind(Fty1 fn, T1 t1, T2 t2, ..., TN tn);

fn为一个带n个参数的函数(或函数对象);

t1~tn是给函数fn提供的参数值,其中的ti可以是:

  • 一个固定值
  • 未绑定的值:用下划线加上一个数字这样的特殊名称来表示(如:_1、_2、...等,在名空间std::placeholders中定义),其中的数字表示未绑定值的参数在新函数参数表中的位置;
    bind返回一个新函数,该函数的参数表由函数fn的所有未绑定值的参数构成。
  • 柯里化是指把一个多参数的函数变换成一系列单参数的函数,它们分别接收原函数的第一个参数、第二个参数、

右值引用

  • 临时变量,如f(),是右值,用A&& x作为参数

显示类型转换

  • static_cast 基于隐式类型转换,对于不安全的转换,会在编译的时候报错。
  • dynamic_cast 基于显式类型转换,在运行时刻进行检查,对于不安全的转换,会返回空指针。
  • reinterpret_cast转换相当于C语言的强制类型转换,因此不安全
  • const_cast转换可以去掉对象的const属性,若进行修改,是未定义行为。

类成员指针

  • .*指针
    class A
    { public:
     int x;
     void f();
    };
    int A::*pm_x = &A::x; //OK
    void (A::*pm_f)() = &A::f; //OK
    A a;
    a.*pm_x = 0;
    
  • ->*是a的指针。

版权声明:
作者:carott
链接:https://blog.hellholestudios.top/archives/1411
来源:Hell Hole Studios Blog
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录