C++

第1单元 初识C++

1.1 C++简介

1.1.1 C++的发展简史

1.1.1.1 课程介绍

书籍介绍
  • C++ Prime
  • 大话数据结构

1.1.1.2 编程环境

推荐使用Visual Studio 2019

不过我用的是VS Code

1.1.1.3 C++发展史

前期的阶段老师没讲

重要的C++版本
  • C++98
  • C++11
  • C++20

我们学的是C++11版本的

同时编译器使用的是2019版本的,因为编译器往往需要过两年才能适配新版本的语法。

C++的主要用途
  • 服务器端编程
  • 算法设计
  • 通过Qt进行Linux、国产化操作系统的桌面应用程序开发,自主可控的软件开发。

1.1.2 C++的特点

1.1.2.1支持四种编程范式

Java号称是纯面向对象的语言,他的main封装在class类里面。

四种编程范式:面向过程、面向对象、泛型编程、函数式编程

  • 面向过程
    • 瀑布式开发模型
      • 其实就是正常的程序流程构设
      • 非常理想化,没有考虑在实际开发过程中可能产生的需求变化和将来的软件升级。
      • 在软件开发过程中唯一不变的是变化!
  • 面向对象,OOP(Object-Oriented Programming)
    • 三个基本特征:封装、继承、多态
  • 泛型编程
    • 与类型无关的编程,是通过模板实现的。以模板为基础,STL实现通用的容器类。
    • 感觉上就是调用库。
  • 函数式编程

1.1.2.2 适合编写大型应用程序

  1. 不适合编写操作系统,操作系统的内核还是由C语言完成的。
  2. C++的一些语法设计,考虑了很多人同时开发,例如命名空间namespace。
  3. 面向对象程序设计(OOP),根本上解决了需求发生变化、软件升级的问题

1.1.2.3 可复用、可扩充、可维护和灵活性好

1.1.2.4 C++的缺点

  • C++的强大在于提供高级抽象的同时又不放弃对程序的细节控制。
  • 过于频繁的更换,除了增加功能以外,也使得C++变得越来越复杂。苦了人脑,幸福电脑。

1.2 第一个C++程序

1.2.1 Hello World

1
2
3
4
#include <iostream>
int main(){
std::cout<<"Hello World!\n";
}

1.2.1.1 C++与C的不同

  1. 头文件没有后缀名
  2. C++兼容C,可以在C语言头文件名称前加上c后去除后缀名进行引用。
  3. 基本输出不同
1
2
std::cout << "Hello,World!\n";
//std::cout是标准命名空间中定义、ostream类型的全局变量,代表显示器。
  • std是标准命名空间(standard)
  • ::是域操作符(相当于的)
  • cout是控制台输出设备
  • <<是输出运算符

1.3 C++对C语言的扩充

1.3.1 命名空间

为了解决合作开发时命名冲突的问题,C++引用了命名空间。

1
2
3
4
5
6
7
8
9
namespace Li{
int a=100;
void printInt(){std::cout << a << std::endl;}
//endl是标准命名空间定义的换行符
}
namespace Han{
int a=200;
void printInt(){std::cout << a << std::endl;}
}

在不同的namespace中可以重复定义相同的函数或相同的变量。

1
2
3
4
5
6
//调用
int main(){
Han::a = 300;
Li::printInt();//输出100
Han::printInt();//输出300
}

1.3.1.1 命名空间简写方法

  1. 使用 :: 引用命名空间中定义的元素
    std::cout << "C++" << std::endl;
  2. 使用using引用命名空间中的某个元素
1
2
using std::cout;
cout << "C++" << std::endl;
  1. 使用using引用命名空间
    1
    2
    using namespace std;
    cout << "C++" << endl;

1.3.2 控制台输入输出

C++的I/O解决方案

  • 使用 cin 接收从键盘输入的数据,用 cout 向屏幕上输出数据(这2个过程又称为“标准I/O”)。
  • C++也对从文件中读取数据和向文件中写入数据做了支持(统称为“文件I/O”)。

C++标准库中包含了“流类”

  • istream:常用语接收从键盘输入的数据;
  • ostream:常用语将数据输出到屏幕上;
  • ifstream: 用于读取文件中的数据;
  • ofstream: 继承自istream和ostream类,因为该类的功能兼两者于一身,既能用于输入,也能用于输出;
  • fstream:兼ifstream和ofstream类功能于一身,既能读取文件中的数据,又能向文件中写入数据。
  • cin是istream类的对象,cout是ostream类的对象,在头文件中声明。
  • 使用cin和cout需要包含头文件,当然还需要使用std命名空间。

输入

  • 输入流对象cin和输入运算符>>配合,用于用户输入
  • 在连续输入多个变量时,以空白(空格、回车、制表符)为分隔符
1
2
3
int a,b;
cin>>a>>b;
//如果是字符型的话,分隔符会被识别成数据。

输出

  • 输出对象cout和输出运算符<<配合,用于用户输出
  • 可以连续输出多个不同类型的常量或变量
1
2
cout<<"Hello,C++"<<endl;
//注意流的方向,要指向cout
  1. 两个运算符都是从左向右结合
  2. cin>>a的返回值时cin,cout>>”Hello,C++”的返回值是cout。所以可以连续执行。

小例子

1
2
3
4
5
6
7
8
#include<iostream>
using namespace std;
int main(){
int a=0,b=0;
cout << "Please Enter Two Integers:"
cin >>a >> b;
cout << a << "+" << b << "=" << a+b <<endl;
}

C语言中的scanf是不安全的函数,如果要使用需要#define _CRT_SECURE_NO_WARNINGS

1.3.3 增强类型

  1. const常变量
  2. bool布尔类型
  3. enum枚举类型

1.3.3.1 const的基本概念

  • 定义只读变量的关键字
1
2
3
4
const int a = 100;
//const的量一定要赋值进行初始化
const int * pa = &a;
//const int * 类型的值不能用于初始化int*类型的实体
  • 在C语言中const变量不能定义数组长度,但是在C++中可以。
1
2
const int size = 100;
int arr[size];

1.3.3.2 const与指针

  • 指向常量的指针,称为常量指针
    • 解释:所指的量是一个常量。
    • int * const p
    • 当使用是形参时,传入变量地址就可以。
  • 不能指向其他变量的指针,称为指针常量
    • const int * p;
    • int const * p;
    • 解释:只能指向某一量,设定后无法更改。
    • 数组就是指针常量
  • 指向整型常量的指针常量。
    • const int * const p;

1.3.3 布尔类型

在C语言的C99版本中可以引用<stdbool.h>来使用bool类型

  • enum
1
2
enum SEASON{SPRIG,SUMMER,AUTUMN,WINTER};
SEASON s1 = SUMMER;

1.3.4 参数默认值

直接在形参中初始化

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
using namespace std;
void add(int x,int y=1,int z=2){
cout<<x+y+z<<endl;
}
int main(){
add(1); //4
add(1,2); //5
add(1,2,3); //6
return 0;
}
  1. 参数默认值可在函数声明中出现一次,如果没有函数声明,只有函数定义,那么可以在函数定义中设定
1
2
3
4
5
6
//fun.h
int Add(int a,int b=1,int c=2);
//fun.cpp
int Add(int a,int b,int c){
return a+b+c;
}
  1. 默认参数赋值的顺序时自右向左
1
2
int Add(int a,int b=0,int c)
//这种是错的

1.3.5 函数重载

  • 所谓函数重载,是指在同一个作用域内、函数名相同、参数列表不同的多个函数。
  • 编译器会根据所给的参数自动选择相应的函数。
  • 参数列表不同:
    1. 参数类型不同
    2. 参数个数不同
    3. 参数类型、个数均不同
    • 形参变量名不同不构成重载!
  • 当重载函数有默认值时,要防止二义性!
    • 两个同名函数之间,只差若干个默认值,当未指定默认值时,程序就不知道使用哪个函数了。

1.3.6 引用

1.3.6.1 引用的基本概念

  • 引用就是给变量取的一个别名
1
2
3
4
int a = 100;
int &ra = a;
ra = 200;
cout<<a<<endl;
  • 引用的语法
    • 类型 & 引用名 = 变量名;
  • &的含义
    1. int & ra = a; 在定义变量的时候使用&,表示引用
    2. int *p;p=&a; 一元操作,表示取地址
    3. a & b; 二元运算符,表示运算中的按位与
    4. a && b; 表示逻辑与
  • 使用引用的注意事项
    1. 引用必须初始化
    2. 不能引用常量
    3. 不能引用数组
    4. 引用只能时某个固定变量的引用,不能再引用其他变量。
  • 没必要在一个函数内使用引用,通常在两个函数之间使用引用
    • 形参是引用
      • 在C语言中,函数参数的两种形式
        • 传值
        • 传地址
    • 返回值是引用

1.3.6.2 函数参数的三种形式

1. 传值
1
2
3
4
5
void Swap1(int x,int y){int temp=x;x=y;y=temp}
int main(){
int a=100,b=200;
Swap1(a,b);
}
  • 传值,子函数无法改变调用函数中变量的值。
  • 每个函数的一次运行都会有一个自动分配的栈,局部变量位于栈中。形参也是局部变量。

函数每一次运行都会有一个独立的栈,用来存放局部变量。这就导致了主函数自定义函数中分别定义的变量不互通,主函数传入变量的时候仅仅传入了值,而非变量本身。

2. 传地址
1
2
3
4
5
void Swap2(int *p,inty *q){int temp=*p;*p=*q;*q=temp;}
int main(){
int a=100,b=200;
Swap2(&a,&b);
}
  • 传地址,子函数可以修改调用函数中变量的值
  • 下面是比较常见的初学者的错误写法
1
Swap3(int *p,int *q){int *temp=p;p=q;q=temp;}

这种方式直接传输地址,使得不同函数可以直接读取或写入其他函数栈相应地址变量

3. 传引用
1
2
3
4
5
void Swap4(int &x,int &y){int temp=x;x=y;y=temp}
int main(){
int a=100,b=200;
Swap4(a,b);
}
  • 传引用,可以达到传指针同样的效果,但是调用更方便。
  • 与传地址不同的地方就是函数定义不是*x了,而是&x,这样传入的可以直接传入变量
  • 个人理解:直接将传入函数命名别名后,在函数中进行使用。
  • 尽管在形参中没有认为进行初始化,但是从形参到实参的过程就是初始化!

1.3.6.3 引用——函数的返回值

不会在一个函数内部使用引用,引用仅应用于两个函数之间

  1. 函数参数是引用类型
  2. 函数的返回值是引用
1
2
3
4
5
6
7
8
9
int & At(int b[],int index){
return b[index];
}
int main(){
int a[3] = {1,2,3};
At(a,1) = 100;
//b[index]等价于a[1]
//等价于a[1] = 100
}
  • 也就是说,可以让函数调用作为左值
  • 可以避免返回值被拷贝
  • 注意:不能返回局部变量的引用
    • 因为局部变量存在函数的栈中,函数一旦运行结束,栈会被释放清空,那么这个数据就不存在了。

1.3.6.4 引用——常引用

  • int & a=b;a就是b。
  • 针对引用类型的函数参数,形参就是实参。这带来两种用途:
    1. 子函数可以通过引用类型的参数来修改调用函数中的变量,并且不用使用指针(都不喜欢指针,能不用尽量不用)。
    2. 避免了形参到实参的初始化过程,从而提高了效率。
  • 如果只想2,而不想1,可以使用常引用
    1
    void fun(const XYZ & r){……}
  • 在面向对象编程中,类类型属于自定义类型,比简单类型占用更多的内存空间。如果函数的参数类型是类类型,为了提高效率,我们往往使用常引用。

1.3.7 内存管理——堆、栈

  • 不是数据结构中的堆和栈
  1. C语言的内存区域
      • 堆是用于存放进程运行中被动态分配的内存段。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被剔除(堆被缩减)。
      • 栈又称堆栈,存放程序的局部变量(但不包括static声明的变量)和函数被调用时的参数和返回值
  • 从低位地址到高位地址逐个是:

    1. 代码区:函数代码块的二进制代码。
    2. 数据区
    3. 文字常量区:常量字符串存放于此
    4. 未初始化静态变量区:没有初始化的全局变量和静态变量
    5. 已初始化的静态变量区:初始化的全局变量和静态变量
    6. 堆区:动态分配的数据
    7. 栈区:局部变量存放于此
    8. 命令行参数区:命令行参数和局部变量
  1. C++内存区域
  • C++中内存分成5个区:
    1. 栈:内存由编译器在需要时自动分配和释放。通常用来存储局部变量和函数参数。(为运行函数而分配的局部变量、函数参数、返回地址等存放在栈区
    2. 堆:内存使用new进行分配,使用delete释放。如果未能对内存进行正确的释放,会造成内存泄漏
    3. 自由存储区:使用malloc进行分配,使用free进行回收。
    4. 全局/静态存储区:全局变量和静态变量被分配到同一块内存中,C语言中区分初始化和未初始化的,C++中不再区分了。(全局变量、静态数据、常量存放在全局数据区)
    5. 常量存储区:存储常量,不允许被修改。
  • 栈和堆的对比
      1. 内个函数的第一次运行都会有一个独立的栈
      2. 自动分配、自动释放
      3. 通过变量直接使用
      1. 全局只有一个堆
      2. 手动分配、手动释放
      3. 通过指针间接使用
1
2
3
4
5
6
7
8
9
void fun(){
int a = 0;
a++;
}//函数运行结束,栈被自动释放
int main(){
fun();
fun();
}
//运行了两次,有两个栈,栈中的a都为1
1
2
3
4
5
6
7
void fun(){
int * p =new int;
//这里*p是局部变量,存在栈中。同时堆中开辟了一块int类型大小的空间。
*p = 100;//p指向的内存单元为100,也就是堆中相应内存单元为100.
delete p;
//将堆中数据回收,但*p还在,仍然指向堆中的那个地址,称为“野指针”
}

1.3.8 new/delete

  • 在C语言中,动态分配内存用malloc()函数,释放内存用free()函数。

    1
    2
    3
    4
    int *p = (int*)malloc(sizeof(int));//分配一个int型的内存空间
    //将int大小的空间分配给p并将无类型的p进行强制类型转换
    free(p);//释放内存
    //分配在堆中
  • C++新增了两个关键字,使用起来更简介

    • new用来动态分配内存
      1
      int *p = new int;//分配一个int型的内存空间
    • delete用来释放内存
      1
      delete p;
  1. 根据后面的数据类型来自动推断所需空间大小
  2. 返回具体类型的指针,不需要进行类型转换
  • 分配一组连续的数据(动态数组)

    • C语言版本
      1
      2
      int *p = (int *) malloc(sizeof(int)*10);
      free(p);
    • 版本
      1
      2
      int *p = new int[10];
      delete []p;
  • new/delete的优点

    • new可以自动推断所需空间大小,而malloc需要通过sizeof进行计算。
    • new自动返回所需类型指针,而malloc返回的是void *,还需要类型转换
    • new在分配空间的同时还可以初始化,而malloc只能分配空间
      1
      2
      int *p = new int(100);
      //将p初始化为100
    • 我们可以笼统的说newmalloc都是在中分配内存,但仍然有差别,所以new/deletemalloc/free不能混用。
    • new/delete支持C++的新特性,包括:重载、调用构造/析构函数。

1.3.8 静态数组、动态数组

  • 静态数组
    1
    2
    const int SIZE = 100;//SIZE是常量
    int a[SIZE]; // 定义数组时,数组的大小必须是常量
  • 动态数组
    1
    2
    int size = 100;//size是变量
    int *p = new int[size];//可以在内存中分配任意大小的内存
  • 指针p和数组名a都指向内存中一段内存的首地址,且用法都是一样的
    1
    2
    3
    *(a+10)=123;
    p[10]=123;
    //相互等价
  • 差别在于,a是指针常量,p是指针变量,p++可以,a++不行。

1.3.9 各种指针和指针数组

因为指针的存在,所以C/C++的程序可以直接访问内存,从而提高程序的运行速度。

在大型程序中,核心数据不可能存放在栈中,一定是存放在堆中,所以只有通过指针进行访问。换言之,核心数据一定是new出来的

  • 在开发的过程中,能不用指针运算,尽量不用指针运算。
    • 使用下标运算,而不是用*运算,例如:*(P+1)等价于p[i],不过*pp[0]好看一些。
    • 能够使用引用,尽量使用引用,例如:通过指针和引用都可以实现交换函数,swap(&a,&b)肯定不如swap(a,b)

struct和class中指针类型的成员变量,习惯上不会使用引用,而是指针

各种指针

  1. 指针和const组合:
    • 常量指针:指向常量的指针
    • 指针常量:int a[100];
  2. 指针和数组结合:
    • 指向数组的指针:int *p = a
    • 指针数组:数组的每一个元素都是指针。
      • int *a[100]
  3. 指针的指针:
    • 指针和自己结合,int **p
    • 不仅仅数组名是指针常量,函数名也是指针常量
      • 通过函数指针,C语言也能做到面向对象
      • 函数指针->仿函数->Lambda表达式

1.4 C++中的两个新语法

1.4.1 nullptr——野指针

野指针主要是因为这些疏忽而出现的删除或申请访问受限内存区域的指针。

  1. 指针变量未初始化
    • 任何指针变量刚被创建时不会自动称为空指针,它的缺省值是随机的,它会乱指一气。这个时候,通过指针访问内存就会出错。所以,指针变量在创建的同时应该被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
  2. 指针释放后未置空
    • 有时指针在free或delete后未赋值NULL,便会是人认为是合法的。他们只是吧指针所指的内存给释放掉,并没有吧指针本身干掉,此时指针指向的就是垃圾内存。释放后的指针应立即将指针归置为NULL,防止产生“野指针”。
  3. 指针操作超越变量作用域
    • 在子函数中定义的局部变量指针,当函数运行结束后会被释放,此时再通过指针访问内存,就会报错。

不能将nullptr转换为int类型。

1.4.2 基于范围的for循环

类似于Python的for语法

1
2
3
4
5
6
7
8
9
int v[] = {1,2,3,4,5,6};
for(int e:v)
cout<<e<<" ";

//等价于

int v[] = {1,2,3,4,5,6}
for(int i=0;i<6;i++)
cout<<v[i]<<" ";

第2单元 类与对象

2.1 面向对象程序设计思想

2.1.1 面向对象的编程思想

对于面向对象程序设计而言,最重要的一个特征就是数据封装

所谓数据封装,就是通过类来实现信息的抽象和隐藏。学习了类的相关知识,才能真正走进面向程序设计的世界。

面向过程程序设计

面向过程程序设计对于较为简单的需求通常能够很好地满足。如果问题比较复杂,在项目开始之初就完成模块的合理划分往往比较困难。当数据结构改变时,所有相关的处理过程都要进行相应的修改,程序的可用性极差

在程序中使用对象映射现实中的事物,利用对象之间的关系描述事物之间的联系,这种思想就是面向对象。

Object-Oriented,简称OO。

把构成问题的事物按照一定的规则划分为多个独立的对象,然后通过调用对象的方法解决问题。

当应用程序功能发生变更时,只需要修改个别对象就可以了。

面向对象程序设计思想

  1. 分析(OOAnalyse)
  2. 设计(OODesign)
  3. 开发(OOProgramming)
  4. C++是一个面向对象的编程语言(OOPLanguage)

概述

面向对象程序设计描述的是客观世界中的事物,以对象代表一个具体的事物,把数据和数据的操作方法放在一起而形成的一个相互依存又不可分割的整体。

由此可见,面向对象程序设计所强调的基本原则就是直接面对客观存在的事实,将人们在日常生活中习惯的思维方式和思维表达式应用软件开发中,使软件开发从过分专业化的方法、规则中回到客观世界,回到人们通常的思维方式面向对象的思想更适合用于大型系统项目的开发。

2.1.2 面向对象的三大特征

面向对象的三大特征

三大特征具有承上启下的关系,而且适用于所有面向对象的编程语言。

  • 封装
    • 封装就是隐藏。它将数据(属性)和数据处理过程(行为)封装成一个独立性很强的模块(类)。对外提供接口,不需要让外界知道具体的实现细节。
    • 不封装会有哪些问题?
      • 容易因为传参错误出现逻辑错误。
      • 面向对象的程序设计过程中,数据和处理数据的函数封装在一个类中,不存在跨模块处理数据的问题。
  • 继承
    • 继承描述的是父类和子类的关系。通过继承,子类可以扩展父类的功能,从而提高了代码的可重用性,降低了代码维护的难度。
    • 共同的功能写在父类中
    • 不同的功能写在子类中
  • 多态
    • 是指不同事物对统一信息产生的不同行为。

2.2 初始类和对象

2.2.1 类的定义

C++中的类(class)可以看做C语言中的结构体(struct)的升级版。

结构体是一种构造类型,可以包含若干成员变量,每个成员变量的类型可以不同;可以通过结构体来定义结构体变量,每个变量拥有相同的成员,只是他们的取值不一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Student{
public:
const char* name;
int age;
float score;
void Display(){
cout << name << "今年" << age << "岁,";
cout << "考了" << score << "分" << endl;
}
};

int main()
{
Student s1;
s1.name = "小明";
//这里有问题,会在之后的章节里探讨
sl.age = 15;
sl.score = 95;
sl.Display();
}
  1. 关键字从struct变成了class
  2. 访问权限public表示共有,是为了可以从外部访问。
  3. Display从全局函数变成了成员函数,也就是对象s1的成员函数,所以函数参数、函数中通过形参引用name等成员,都省略了
  4. Display(s1)写成了s1.Display();

类的语法格式

1
2
3
4
5
6
7
class 类名
{
成员访问限定符:
数据成员;
成员访问限定符:
成员函数;
}

标识符的命名规范:

  1. 只能包含字母、数字、下划线
  2. 只能以字母、下划线开头
  3. 不能是关键字

限定访问规则:
puiblic > protected > private

类是事物的抽象描述,若想定义类就需要抽象出事物的属性及方法。

2.2.2 类外定义成员函数

成员函数的函数体既可以写在类中,也可以类外实现。

1
2
3
4
5
6
7
8
9
10
//Student.h
class Student{
void Study();
}

//Student.cpp
#include "Student.h"
void Student::Stundy(){
cout << "学习C++";
}

Stundent.cpp -> Stundent.lib,然后和Stundent.h一起发布供第三方调用

第2次上课

1
2
int* p = new int;
//在Visual Studio中,*要跟int后面

一定要提防野指针,编译器无法识别!人工纠错非常的困难!

1
2
3
4
5
6
7
8
int* p = new int;
//在堆中分配内存,新建栈中指针p并指向堆
delete p;
//回收堆中内存,但p的指向还在
*p = 100
//编译器不会报错,运行的时候会随机报错,因为p指向的空间已经无法使用,无法写入数据!
p = new int;
//重新申请内存并使p指向这个内存。

网课

2.2.3 对象的创建和使用

  • 对象的定义语法:
1
2
类名 对象名 [= 初始值];
类名 对象名 [(初始值列表)];
  • 上述定义的对象和变量一样,仍然在栈中(自动分配和释放)
  • 访问对象的公有成员(含成员变量、成员函数),和结构体变量
  • 访问成员的方法一致:
    • 对象名.成员变量
    • 对象名.成员函数(实例列表)
  • 也可以在堆中创建对象:
    • Student* ps = new Student; delete ps;
  • 指针是栈里的局部变量,指向堆(手动分配和释放)里面的对象
  • 访问对象的公有成员(含成员变量、成员函数),和结构体指针访问成员的方法一致:
    • 指针->成员变量
    • 指针->成员函数(实例参数)

字符串类string的使用

  • C语言不存在字符串类型,都是用字符数组(字符指针)处理字符串

  • C++支持字符数组,另外还提供了字符串类:string。使用前必须#include <string>

  • 使用string定义字符串,无须担心长度、空间等问题,且string重载了大量运算符,实现了大量成员函数,足以满足字符串的日常处理操作。

  • 用法

    • 访问字符串中的字符与数组相同,可以连等。
    • 字符串间可以用加号链接
      • cout<<S1+S2<<endl;
      • cout<<S1+=S2<<endl;
    • 字符串的比较
      • cout<<(S1<S2)<<endl;
    • 计算字符串的长度
      • cout<<S1.length()<<endl
      • 一个汉字两个长度
    • 字符串交换
      • S1.swap(S2)

2.3 封装

  • C++的封装是通过类类型(简称类)实现的,通过类把具体事物抽象成一个由属性(成员变量)和行为(成员函数)组成的独立单位(即类)。

  • 在类的封装设计中,通过访问权限控制类成员访问,

    • 需要隐藏的、内部实现的细节设为私有private,仅供内部访问;
    • 允许子类访问的设为保护protected
    • 需要对外提供访问接口的设为共有public
  • 成员函数的简单分类

    • 构造函数和析构函数
      • 构造函数用于对象的创建和初始化
      • 析构函数用于对象的释放
    • 针对成员变量的Set/Get、Add/Del函数
      • 大多数变量往往设为私有,通过共有的Set/Get函数可以访问私有成员
      • 针对数组类型的成员变量,往往有Add/Del函数。
    • 其他功能性函数
      • 与应用程序具体的功能、业务规则有关

通常,成员变量是私有的,对每个成员变量会对应有一对共有的Set/Get函数

2.4 this指针

  • 类中每个对象的数据成员都占用独立空间,但成员函数是共享的,可是各个对象调用相同的函数时,显示的是对象各自的信息
  • this是C++中的一个关键字,也是一个常量指针,它指向当前对象,通过它可以访问当前对象的所有成员。
  • this实际上是成员函数的一个隐式的形参,在调用成员函数时将对象的地址作为实参传递给this。所谓“隐式”,是说它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
  • 三个作用:
    • 如果成员函数的形参与类的成员变量重名,可以用this指针解决。
    • 如果成员函数需要返回当前对象,应该写成return *this;
    • 可以在成员函数中,以this指针为实参,调用其他函数。
  • 三点注意事项:
    • this只能在成员函数内部使用,用在其他地方没有意义,也是非法的。
    • this是指针常量,它的值是不能被修改的,一切企图修改该指针的操作,如复制、递增、递减等都是不允许的。
    • 只有当对象被创建后this才有意义,因此不能在static成员函数中使用

2.5 构造函数

2.5.1 自定义构造函数

在C++中,如何自动进行对象初始化,并在对象撤销时,自动执行清理任务

  • 构造函数是类的特殊成员函数,用于初始化对象。
  • 构造函数在创建对象时会自动/隐式调用。
  • C++中的每个类至少要有一个构造函数
  • 如果类中没有定义构造函数,系统会提供一个默认构造函数
  • 默认构造函数没有参数,也没有函数体,不具有实际的初始化意义。
  • 构造函数有严格的接口形式,有四个特点:
    • 与类同名
    • 不能设置返回值类型,void也不写,不能使用return语句返回
    • 可以由参数,可以重载;
    • 一般设为public

自定义有参构造函数

  • 参数可以由默认值
  • 参数默认值写在声明处
  • 建议尽量使用初始化表。某些情况下,必须使用初始化表进行初始化!
  • 常变量、引用必须初始化,所以常成员变量、引用类型的成员变量,只能通过初始化进行初始化!成员对象也需要通过初始化表初始化。

2.5.2 重载构造函数

  • 函数重载:
    • 同一作用域内
    • 函数名相同
    • 但参数列表不同
  • 三点注意:
    • 不以返回值不同来作为重载的条件;
    • 形参变量名不同不意味着参数列表不同;
    • 有参数默认值时,要防止二义性

2.5.3 含有成员对象的类的构造函数

  • 什么是成员对象?
    • C++允许将一个对象作为另一个类的成员变量,即类中的成员变量可以是其他类的对象,这样的成员变量称为类的子对象或成员对象。
    • 创建含有成员对象的对象时,先执行成员对象的构造函数,再执行类的构造函数。

委托

2.5.4 三角形项目的分析与设计

  1. 分析
    • 需求分析,开发人员经过深入细致的调研和分析,准确理解用户和项目的功能、性能、可靠性等具体要求,将用户非形式的需求表述转化为完整的希求定义,从而确定系统必须做什么的过程。
    • 在学习过程中,老师就是用户,main函数中的测试代码,以及预期的运行结果就是我们的需求。
  2. 设计
    • 要把软件“做什么”转换为“怎么做”。即确定程序是由哪些模块组成的,以及模块之间的关系。
    • 在学习过层中,一个或若干个类就是一个模块。对应UML中的类图,也就是类的.h头文件。
  3. 实现
    • 也就是编码的过程。也就是.cpp文件中每个成员函数具体是如何实现的。

分析

  1. 能够根据三个顶点的坐标,或者三条边的长度,构造三角形对象
  2. 能够按照三个点的坐标显示三角形,其中每个顶点的x、y坐标能用方括号括起来,三个点用花括号括起来
  3. 能够计算三角形的面积,如果三条边长不合理,则面积为零。

设计

  • 三类成员函数
    • 构造函数/析构函数
    • Set/Get函数
    • 功能函数
  • 从调用者的角度来讲,应该把公有成员写在前面。但是在编码过程中,首先写成员变量。这二者不矛盾!

点类的设计:

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
public :
Point(float xx = 0, float yy = 0);
float Distance(const Point& other);
void Show();
void SetX(float xx);
void SetY(float yy);
private :
float x;
float y;

}

三角形类的设计:

1
2
3
4
5
6
7
8
9
10
11
class Triangle{
public:
Triangle(const Point& ppl,const Point& pp2, const Point& pp3);
Triangle(float a, float b, float c);
float Area();
void Show();
private:
Point p1;
Point p2;
Point p3;
}

2.5.5 三角形项目的实现

没听懂这里为什么float不用常引用而正常的要用

2.5.6 三角形项目的调试

形参是int类型,实参是float类型,变量赋值转换的时候0.9会被转成0!

2.6 析构函数

  • 构造函数
    • 分配内存
    • 初始化工作
  • 析构函数
    • 回收内存
    • 清理工作

析构函数的语法:

1
2
3
4
5
6
7
8
9
10
// ABC.h 类的声明
class ABC{
ABC();//构造函数的声明
~ABC();//析构函数的声明
};

//ABC.cpp 成员函数的类外实现
#include "ABC.h"
ABC::ABC(){……;}//构造函数的类外实现
ABC::~ABC(){……;}//析构函数的类外实现
  • 析构函数时类的特殊成员函数,用于释放对象

  • 析构函数在释放对象时会自动/隐式调用

  • C++中的每个类有且只有一个析构函数

  • 如果类中没有定义析构函数,系统会提供一个默认析构函数

  • 默认析构函数没有参数,也没有函数体,不具有实际的意义

  • 与类同名,前面加~

  • 不能设置返回值类型,不写void,不能使用return语句返回

  • 没有参数,不能重载

  • 父类的析构函数通常是虚拟析构函数

  • 释放对象的三种时机:

    1. 在函数中定义对象;当函数调用结束->释放栈->释放对象
    2. 使用static修饰的静态对象,程序运行结束->释放静态内存空间->释放静态对象
    3. 使用new运算符创建的对象,调用delete释放对象
  • 栈中对象的析构函数的调用顺序,与析构函数的调用顺序相反

    • 因为栈是先进后出的顺序,从最后一个开始释放

2.6.1 指针类型的成员变量

  • 如果一个类中有指针类型的成员变量,就需要在构造函数中通过new分配了内存,也就需要使用手动释放该段内存,即在析构函数中通过delete释放内存。
  • 从另一个角度讲:如果没有指针类型的成员变量,程序员也不需要实现析构函数,默认析构函数就可以

类函数传入指针字符串变量的时候,类使用的是堆内的字符串,是公共的,类内没有自己的字符串,如果通过外部修改了这个字符串,那么类内的字符串也会被修改!因此强烈建议在类内开辟单独的内存,然后把字符串复制进去!

2.6.2 正确处理指针类型的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#define _CRT_SECURE_NO_WARNINGS //忽略安全问题,继续使用strcpy等字符串函数
#include<iostream>
using namespace std;

class Student{
public:
Student(const char *s,int a):age(a){ //const char *对应字符串常量
name = new char[strlen(s)+1];
strcpy(name,s);
}
~Student(){delete[] name;} //new[]对应deletep[]
private:
char* name;
int age;
};

Student::age(int a){
age = a;
}

int main(){
Student s1("Alice",20); //调用构造函数->new->拷贝Alice
Student* s2 = new Student("Bob",30);
delete s2; //调用析构函数->释放Bob
} //调用析构函数->释放Alice
  • 如何处理void Student::SetName(const char* s)
    1. 不能让name=s,只能使用strcpy,以保证对象有自己独立的字符串。
    2. 要考虑新名字比老名字长的问题。
1
2
3
4
5
6
//释放老名字
delete[] name;
//重新分配内存空间
name = new char[strlen(s)+1];
//字符串拷贝,以拥有自己的名字
strcpy(name,s);

2.7 拷贝构造函数

2.7.1 拷贝构造函数

  • 简单类型变量:

    • int a(0);
    • int b(a);
  • 类类型

    • Triangle t1(3.4.5); //自定义带参构造函数
    • Triangle t2(t1); //拷贝构造函数-自身类型的变量/对象
  • 拷贝构造函数

    • 共性:是一种特殊的构造函数,拥有构造函数的所有特性。
    • 特性:并且使用自身类型的对象的引用作为构造函数的参数。
    • 目的:通过一个对象初始化另一个对象。

语法:

1
2
3
4
class 类名{
public:
构造函数名(const 类名& 对象名){……}
};

函数调用中的拷贝构造函数

结论:函数参数为引用类型,因为实参就是形参,所以不会调用拷贝构造函数

2.7.2 浅拷贝

  • 如果程序没有自定义拷贝构造函数,C++会提供一个默认拷贝构造函数
  • 默认拷贝构造函数的运行方式是:按位(bit by bit)复制,相当于自然界的克隆。
  • 如果有指针类型的成员变量,简单的按位复制会导致两个对象中的指针指向同一段内存。而由于构造函数使用new分配内存,析构函数使用delete释放内存,进而导致一段内存会被释放两次,程序崩溃。

类成员套类成员的时候,不会调用构造函数,如果构造函数有cpy的拷贝功能的话,会造成两个成员的字符串指针相同。同时释放函数栈的时候会调用两次析构函数,如果析构函数中有内存释放的话,会造成程序崩溃!

  • 结论
    • 如果没有指针类型的成员变量,默认析构函数、默认拷贝构造函数已经足够用了。
    • 如果有指针类型的成员变量,必须自定义析构函数、自定义拷贝构造函数。
    • 两个指针指向同一段内存,最容易造成指针悬挂
    • 两个指针指向非法的内存地址,那么这个指针就是悬挂指针,也叫野指针。意为无法正常使用的指针。

2.7.3 深拷贝

  • 在含有指针类型成员变量的情况下,使用默认拷贝构造函数,按照按位复制的方法,则会产生浅拷贝。所以,我们要自定义拷贝构造函数以实现深拷贝
  • 新拷贝可以为新对象的指针分配一块内存空间,并将数据复制到新空间,以确保两个对象的指针指向各自的空间。
1
2
3
4
Demo::Demo(const Demo& d){
name = new char[strlen(d.name)+1];//分配空间
strcpy(name,d.name);//拷贝函数
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Demo{
public:
Demo(const char* s); //构造函数
Demo(const Demo& d); //拷贝构造函数
~Demo();
char* name; //设置为公有成员变量,是为了方便观察
}

Demo::Demo(const cahr* s){
name = new char][strlen(s)+1]; //分配空间
strcpy(name,s); //拷贝数据
}

Demp::Demo(const Demo& d){
name = new char[strlen(d.name)+1]; //分配空间
strcpy(name,d.name); // 拷贝数据
}

Demo::~Demo(){
delete[] name;
}

提前剧透:赋值运算符

  • 在定义的同时进行赋值叫做初始化,定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值。初始化只能有一次,赋值可以有多次。

  • 编译器提供默认赋值运算符,其机制也是“按位复制”。

  • int a(10); //Student s1(“Alice”);//带参构造函数

  • int b(a); Student s2(s1);//拷贝构造函数

  • a = b; s2 = s1; //赋值运算符

“空”类的五个默认

  • class A{};是一个空类
  • 空类中至少包含四个成员函数
    1. 默认构造函数
    2. 默认析构函数
    3. 默认拷贝构造函数
    4. 默认赋值运算符
  • 非静态成员函数拥有默认参数this

2.8 关键字修饰类的成员

  • const

  • static

  • &

  • 他们都是int类型

  • 他们都需要初始化

    • 差别在于静态变量默认初始化为0

2.8.1 常成员

  • const int a

  • const和指针结合,有指向常量的指针、指针常量两种形式,而数字名是指针常量。

  • 在类中:

    1. const修饰成员变量,常成员变量
    2. const修饰成员函数,常成员函数

常成员变量

  • 常成员变量,也是常变量,具有只读属性,所以不能作为左值(即不能出现在赋值运算符的左侧),所以只能通过初始化表进行初始化。

常成员函数

  • 使用const修饰的成员函数称为常成员函数

  • 常成员函数的类内声明如下:

    • 返回值类型 函数名(参数列表)const;
  • 长成员函数的类外实现如下:

    • 返回值类型 类名::函数名(参数列表) const{函数体}
  • 常成员函数的用法

    1. 常成员函数只能读取成员变量,而不能修改成员变量
    2. 常成员函数只能调用类内的其他成员函数,不能调用非常成员函数
    3. 常对象只能调用常函数
    4. 类中的常成员函数和非常成员函数,若函数名相同,及时参数列表也相同,也构成重载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Demo{
public:
void f1(){x=100;} //f1是非常成员函数
void f2()const{ //f2是常成员函数
x = 100; //常成员函数不能修改成员变量对的值
f1(); //常成员函数不能调用非常成员函数
}
private:
int x;
};

int main(){
Domo d1; //d1是一个对象
d1.f1(); //d1既可以调用普通的成员函数
d1.f2(); //也能调用常成员函数
const Demo d2; //d2是一个常对象
d2.f1(); //常对象不能调用非常成员函数
d2.f2(); //常对象只能调用常成员函数
}

2.8.2 静态成员

  • 回忆静态static局部变量的知识点

  • 静态变量存储在静态区域中

  • 静态局部变量默认初始值为0

  • 静态局部变量可以被一个函数的每次调用所 “共享”

  • 一个类的静态成员变量可以被该类的所有对象所“共享”

  • 静态成员变量需要类外初始化

    • 类型 类名::静态成员变量(初始值);
  • 使用static修饰的成员函数,称为静态成员函数。

  • 成员函数都有默认参数this指针,指向当前对象。由于静态成员属于类、而不属于某个对象,所以静态成员函数的参数中没有默认的this指针。所以,静态成员函数无法调用非静态成员。

2.8.3 单例模式

  • 单例模式属于23种经典设计模式中最简单的一个,是唯一一个非面向对象的设计模式。

  • 设计模式是软件开发人员你在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。

  • 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。

  • 动机:在软件系统中,经常有这样一些特殊的类,必须保证他们在系统中只存在一个实例,才能确保它们逻辑正确性以及良好的效率。

  • 这应该是类的设计者的责任,而不是使用者的责任。

    • 应该通过技术来保证,而非文字或口头来约束!
  • 单例模式属于创建型模式。单利类负责创建自己的对象,同时确保对象的唯一性。即:

    1. 单例类只能有一个实例。
    2. 单例类必须自己创建自己的唯一实例。
    3. 单例类必须给所有其他对象提供这一实例。
  • 单例模式到底怎么实现呢?需要用到哪些技术呢?

    • 静态成员属于类而不属于某个对象,具备唯一性。
    • 可以通过类名直接访问静态成员,而不是创建对象,提供了良好的访问方式。
  • 第一步,置之死地而后生

    • 由于构造函数一般都是共有的,所以程序员可以任意的创建或定义对象。
    • 为了确保对象的唯一性,我们从“任意创建”的极端走向另一个极端,通过私有构造函数,确保无法从外部创建对象,而只能从内部创建对象,以确保对象的数量可控。
  • 第二步:为了确保唯一性应该是类的功能,而不是调用者的责任,所以内部要封装一个唯一实例的指针

  • 第三步:根据成员变量设计静态成员变量读取函数。

  • 第四步:实现静态成员变量读取函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Singleton{
public:
static Singleton* GetInstance();
void SetName(const char* s);
string GetName();
private:
Singleton();
static Singleton* _instance;
string name;
};

Singleton* Singleton::GetInstance(){
if(_instance == nullptr)
_instance = new Singleton;
return _instance;
}

void Singleton::SetName(const char* s){
name = s;
}

string Singleton::GetName(){
return name;
}

void fun(){
Singleton* p = Singleton::GetInstance();
p->SetName("Alice");
}

int main(){
Singleton* p = Singleton::GetInstance();
q->SetName("Bob");
fun();
cout << q->GetName() << endl1;
}
  1. 无需实例化一个对象
  2. 任何地方的修改都是针对同一个对象

2.9 友元

  • 友元,friend,好朋友
1
2
3
4
5
6
7
8
9
10
11
12
class Demo{
friend void fun();//声明fun为友元
public:
void SetX(int xx){_x=xx;}
private:
int _x;
};

void fun(){
Demo d;
d._x = 100;
}
  • friend关键字把“外部”细分成了两类,好友的权限扩大了,非好友仍然严格执行访问权限。
1
2
3
4
5
void otherModule(){
Demo d;
d._x = 100;//错误
d.Set(100);
}
  • 一般来说,只把用一个模块内部的函数或类声明为友元。友元一般用来给同一个模块内部的其他函数、类开了一个后门,可以直接访问私有成员。

在类中将某一函数添加为友元后,可以直接在函数内部调用私有成员变量。

  • 友元函数
    • 把普通函数声明为友元函数
    • 把其他类的成员函数声明为友元函数
  • 友元类
1
2
3
4
class A{};
class B{
friend class A;
};
  • 关于友元函数
    • 友元函数不是成员函数
    • 友元函数的声明可以写在类中任意位置
    • 友元函数不受访问权限的影响
  • 关于友元
    • 友元是单向的;如果需要互为好友,就必须各自单独声明
    • 友元不具备传递性
    • 友元关系不能被继承——好朋友的好朋友不是好朋友

高内聚、低耦合

  • 高内聚低耦合,是软件工程中的概念,是判断软件设计好坏的标准,主要用于程序的面向对象的设计。
  • 目的是使程序模块的可重用性、移植性大大增强。通常程序结构中各模块的内聚程度越高,模块间的耦合程度就越低。
  • 内聚是从功能角度来度量模块内的联系,一个好的内聚模块应当恰好做一件事,它描述的是模块内的功能联系
  • 耦合是软件结构中各模块之间相互连接的一种度量,耦合强弱取决于模块间接口的复杂程度、进入或访问一个模块的点以及通过接口的数据。

2.10 总结

  • 对象数组如果没有初始化,则要求对象的无参构造函数。
    • 默认可以传数组进
    • 设置了一个构造函数,数组可能会不能进去
    • 想要值和数组都近要么设置两个构造函数,要么带默认参数
  • 函数以char*或者const char*作为参数,如果需要默认值,则该是"",表示空字符串,而非nullptr
    • 如果使用空指针的话,程序会直接报错。
  • 表示访问权限的三个关键字在类中可以出现多次,也没有顺序限制。
  • 为什么有一些成员变量名要以_开头?
    • 系统级核心变量,避免重名,是一种习惯
    • 私有成员变量有些人也习惯在前面加上_
  • class的默认访问权限是private
  • 在C++中,struct具有和class完全相同的功能,只是它的默认访问权限是public

第3单元 运算符重载

3.1 运算符重载概述

3.1.1 运算符重载的语法

  • C++的一大特性就是重载Overload
  • 重载使得代码简洁高效
  • 不仅仅可以针对函数进行重载,也可以针对运算符进行重载。
1
2
3
4
5
6
7
8
9
10
class Point{
public:
Point(float xx = 0, float yy = 0): x(xx),y(yy){}
Point Add(const Point& other){
return Point(x+other.x,y+other.y);//这里用了匿名函数,直接定义了一个函数省略了变量名,一次性的。
}
void Show(){cout<<"["<<x<<","<<y<<"]";}
private:
float x,y;
};
  • 运算符重载的本质就是函数重载。注:但是不能说运算符的本质是函数,运算符和函数对应的二进制代码还是有很大差别的。
  • 运算符的操作数,等价于函数参数
  • 使用operator运算符替代函数名即可
  • 例如,用operator+替换add
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point{
public:
Point(float xx = 0, float yy = 0):x(xx),y(yy){}
Point Add(const Point & other);
Point operator +(const Point & other);//加号运算符重载,重新定义+的功能。
private:
float x,y;
};

Point Point::Add(const Point & other){
return Point(x + other.x,y + other.y);
}

int main(){
Point a(1.2),b(3.4),c;
c = a.Add(b);
c = a.operator+(b); //按照函数的方式调用operator+
c = a + b;//按照运算符的方式调用operator+
}

充分证明:运算符重载的本质就是函数重载

运算符重载的规则

  • 只能重载C++中已有的运算符,且不能创建新的运算符。
  • 重载后运算符不能改变优先级和结合性,也不能改变操作数和语法结构。
  • 运算符重载的目的是针对实际运算数据类型的需要,重载要保持原有运算符的语义,且要避免没有目的地使用运算符重载。
  • 并非所有C++运算符都可以重载。不可重载的运算符包括:
    • ::
    • ?:
      • 这个是那个三元表达式
    • .
    • .*
      • 成员指针运算符
    • sizeof
    • typeid
  • 可重载的运算符
    • 单目运算符
    • 双目运算符
      • []下标运算符是双目运算符

3.1.3 运算符重载的形式

  • 运算符重载有两种形式
    • 重载为类的成员变量
    • 重载为全局函数,往往声明为类的友元函数

重载为类的成员函数

  • 双目运算符
    • 左操作数是对象本身,由this指针指出
      • 运算符能否作为成员函数重载的条件
    • 右操作数通过函数参数传递
  • 单目运算符
    • 操作数就是对象本身,由this指针指出;参数列表为空
    • 如果运算符区分前置、后置两种操作,例如++、–,则函数需要带一个整形参数,即operator++(int)
      • 这是人为规定的。由于该参数不参与运算,所以无需变量名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Point {
public:
Point(float xx = 0, float yy = 0):x(xx),y(yy){}
void Show();
Point operator++();//前置++的声明
Point operator++(int)//后置++的声明
private:
float x,y;
};

void Point::Show(){
cout<<"["<<x<<","<<y<<"]";
}

Point Point::operator++(){//前置++的实现
++x;//先++
++y;//先++
return *this; //再返回
}

Point Point::operator++(int){//后置++的实现
//先返回,再++
Point r(*this);//用r记录需要返回的数据
++(*this);//当前对象++
return r;//返回以前的数据
}

3.1.4 重载为全局函数

  • 重载为全局函数,往往声明为类的友元函数
  • 重载为全局函数时,所有操作数都需要通过参数进行传递。
  • 重载为全局函数,还是成员函数,一般没有差别。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point {
friend Point operator-(cosnt Point& p1.const Point& p2);//减法声明为友元
public:
Point(float xx = 0, float yy = 0):x(xx),y(yy){}
void Show();
Point operator+(const Point& other); //加法重载为的成员函数
private:
float x,y;
};

void Point::Show(){cout<<"["<<x<<","<<y<<"]";}

Point Point::operator+(const Point& other){ //加法重载为成员函数
return Point(x+other.x,y+other.y);
}

Point operator-(const Point& p1, const Point& p2){ //减法重载为全局函数
return Point(p1.x - p2.x, p1.y-p2.y);
}

3.2 常用运算符重载

3.2.1 输入输出运算符的重载

  • C++的IO系统提供了>><<两个运算符执行IO操作。但标准库只定义了基本数据类型的IO……
  • 输入输出运算符重载是双目运算符
  • 双目运算符重载为成员函数
    • 左操作数十对象本身,由this指针指出
    • 右操作数通过函数参数传递
  • 输入输出运算符只能重载为全局函数。
1
2
3
4
ostream operator<<(ostream o, const Point& p){
……;
return o;//输出运算符从左往右的顺序结合,左边运算符的返回值就是下一个运算符的左操作数
}
  • 外部设备具有唯一性,代表外部设备的cout对象也不能有两个!
  • 形参到实参的拷贝,函数的返回值,都会调用拷贝构造函数
  • 为了避免在内存中有两个对象代表同一个设备,因此左操作数和返回值都是ostream&
  • 输入和输出肯定要涉及数据的读写操作,所以不能是const

自定义class Demo的输出运算符虫子啊,往往是一下形式:

1
2
3
4
5
6
class Demo{
friend ostream& operator<<(ostream& o,const Demo& d);
friend istream& operator>>(istream& i,const Demo& d);
};
ostream& operator<<(ostream& o,const Demo& d){……;return o;}
istream& operator>>(istream& i,const Demo& d){……;return i;}
  • 流输出运算符重载的知识点
    • 为什么流输出运算符只能重载为全局函数,不能重载为成员函数?
      • 重载为成员函数的条件是:左操作数是当前类型的对象,而流输出运算符的左操作数cout的类型是ostream。
    • 流输出运算符重载的函数原型,使用了三处引用,有什么区别?
      • 右操作数是当前类型的常引用,是为了避免实参到形参的拷贝以及起到制度作用;左操作数和返回值实际上都是cout,代表设备,类型都是ostream&,是为了确保设备对象的唯一性
    • 流输出运算符为什么要返回左操作数?
      • 返回左操作数,也就是返回cout,是为了实现流输出运算符的连续使用。

3.2.2 关系运算符重载

  • 让数据有序是改进算法、提高程序运行速度的最基本的方法。
  • 如何对一个Point数组按照到原点的距离从小到大进行排序?
    • 难点:需要重载>运算符,因为Point类型无法进行比较
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define SIZE 4

void bubbleSort(Point arr[],int len){……}

ostream& operator<<(ostream& o, Point arr[]){
for(int i = 0; i < SIZE; i++)
o << arr[i] << " ";
return o;
}

int main(){
Point allPoints[SIZE] = {Point(3,4),Point(1,8),Point(3,6),Point(4,7)};
cout<<"排序前:"<<allPoints<<endl;
bubbleSort(allPoints,SIZE);
cout<<"排序后"<<allPoints<<endl;
}
  • 两个浮点数不能用逻辑==判断是否相等

(abs(x-other.x)<1e-7)&&(abs(y-other.y)<1e-7)

例子:

1
2
3
4
5
6
7
8
//在Point中重载<
//规则:首先判断x,x小的,point对象也小,x相等的,在判断y
bool Point::operator<(const Point& other){
if(abs(x-other.x)<1e-7)//即x==other.x
return y<other.y;
else
return x<other.x;
}

3.2.3 重载赋值运算符

  • 初始化运算符和赋值运算符是两个运算符,但都使用了=

  • 在定义变量(或对象)的时候,使用=,是初始化运算符。针对对象,会调用构造函数。例如:int a = 3

  • 针对已经存在的变量(或对象)使用=,则是赋值。例如:int a;a=3;

  • 赋值运算符可以重载

  • 如果程序员不提供赋值运算符,则编译器会提供一个默认的赋值运算符。

    • 默认复制运算符按位拷贝。除了调用时机不一样,赋值运算符与拷贝构造函数基本一致。
    • 如果有指针类型的成员变量,则是浅拷贝。此时,应重载赋值运算符实现深拷贝
  • 赋值运算符的结合顺序是:从右向左,一致Point a(1,2),b,c;

  • 左侧=的右操作数是表达式b=a的值,所以赋值运算符的返回值类型是Point。为了提高效率(减少一次拷贝构造函数的调用),返回值类型是Point&

  • MyString s1("Hello");

  • MyString s2(s1);

  • MyString s3(6,'A');

  • s1=s3;

  • cout << s1 << "," << s2 << "," << s3 << endl;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
MyString::MyString(const char* str){
_s = new char[strlen(str)+1];//分配内存空间
strcpy(_s,str);//拷贝数据
}

MyString::MyString(const MyString& other){
_s = new char[strlen(other,_s)+1];//分类内存空间
strcpy(_s,other._s);//拷贝数据
}

MyString& MyString::operator = (const MyString& other){
delete[] _s; //对象创建的时候已经new[]了,必须先释放
_s = new char[strlen(other._s)+1]; //才能分配新的空间
strcpy(_s,other._s); //拷贝数据
return *this; //返回当前
}

MyString::MyString(int count,char c){
_s = new char[count+1]; //分配内存空间
memset(_s,c,count); //前面设置count个c
_s[count]='\0'; //最后一位设置结束标志
}

MyString::~MyString(){
delete[] _s;
}

ostream& operator<<(ostream& o,const MyString& str){
o<<"\""<<str._s<<"\""; //转义字符
return o;
}

3.2.4 下标运算符重载

  • []运算符
    • 通常用于读、写数组中的某一个元素
    • 是一个双目运算符
1
2
3
4
5
6
7
8
char MyString::operator[](int index){
return _s[index];//相当于*(_s+index)
}

main(){
MyString str("abc");//str的成员变量_s指向了"abc"
cout<<str[1]<<endl;//输出str.s[1],也就是'b'
}
  • 重载下标运算符的目的
    • 采用“下标”这一程序员习惯的编写形式,读取/修改对象的部分内容。
    • 下标运算符的右操作数不一定是从0开始的自然数,可以是任意类型。
    • 可以实现关键字到值的映射
    • 可以对下标进行越界检查

3.2.6 MyString

  • 前++
1
2
3
4
5
6
7
8
9
10
11
MyString MyString::operator++(){
int len = strlen(_s);
if(len==0)return *this;//空串直接返回

char* temp = new char[len+2];
temp[0] = _s[0]; //复制最前的字符串
strcpy(temp+1,s); //复制旧字符串
delete[] _s; //释放旧字符串
_s=temp; //更新当前对象
return *this;
}
  • 后++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MyString MyString::operator++(int){
int len = strlen(_s);
if(len==0)return *this;//空字符串直接返回

MyString r(_s);//准备好返回值,即当前字符串
char* temp = new char[len+2];
strcpy(temp,_s); //复制旧字符串
temp[len]=_s[len-1]; //复制最后的字符
temp[len+1]='\0'; //结束标志
delete[] _s; //释放旧字符串
_s = temp; //更新当前对象

return r;
}
  • 前–
1
2
3
4
5
6
7
8
MyString MyString::operator--(){
char* temp = _s;
if(*temp)//如果不为空,等价于*temp!='\0'
//吧后面的字符依次向前拷贝,
while(*temp=*(temp+1))//遇见'\0'为止
++temp;
return *this;
}
  • 后–
1
2
3
4
5
6
7
8
9
10
MyString MyString::operator--(int){
MyString r(_s);//准备好返回的当前对象

char* temp = _s;
if(*temp){ //如果不为空,等价于*temp!='\0'
while(*(temp+1))++temp;//找到最后一个字符
*temp = '\0';//把最后一个字符置零
}
return r;
}
  • -=删除字符’b’
    • 需要复制字符
      • 拷贝
      • src指向下一个字符
      • dst指向下一个字符
    • 需要删除字符
      • src指向下一个字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//算法一
MyString MyString::operator-=(char c){
int src = 0,dst = 0;
int len = strlen(_s);
while(src<len){ //循环遍历
if(_s[src]==c)//如果要删除字符
src++; //准备处理下一个字符
else //如果要复制字符,先复制、再移动
_s[dst++]=_s[src++];
}
_s[dst] = 0; //设置结束标志
return *this;
}

//算法二
MyString MyString::operator-=(char){
int src = 0,dst = 0;
int len = strlen(_s);l
while(src<len){ //循环遍历
if(_s[src]!=c)
_s[dst++]=_s[src];
src++;
}
_s[dst] = 0;
return *this;
}
  • 原字符串复制n变*=
1
2
3
4
5
6
7
8
9
10
11
MyString MyString::operator*=(int n){
if(n<2) return *this;
int len = strlen(_s);
char* temp = new char[len * n+1];
memset(temp,0,len*n+1);
for(int i= 0; i < n;i++)
strcat(temp,s);
delete[] _s;
_s = temp;
return *this;
}

3.3 类型转换

3.3.1 转换构造函数

3.3.2.1 转换构造函数

  • 如果构造函数只有一个参数,且参数不是本类的常引用,称之为转换构造函数
1
2
3
4
5
class A{
public:
A(B b); //B是一个简单类型
A(const C& c); //C是一个复杂类型
};
  • 将简单类型的变量,或者复杂类型的对象,转换成当前类型。

  • 调用函数的时候,强调的是实参和形参类型匹配。

  • 如果不一致,但是匹配,则编译器会自动调用默认类型转换或者类型转换函数。

3.3.2.2 返回匿名对象、返回引用、转换构造函数、运算符重载

  • 匿名对象、临时对象

  • 匿名对象仅在当前语句有效,当前语句执行完毕后会被释放(调用析构函数)。

  • 生存周期仅在当前这一行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //类的声明
    class A{
    public:
    A(int x=0){……;}
    };

    //第一个场景
    A f1(){
    //A result(100);
    //return result;
    //return A result(100);
    return A(100);//这里就是一个匿名函数,省略了对象名
    }

    //第二个场景
    void f2(const A& a){}
    int main(){
    f2(A(1));
    }

    //第三个场景
    void f3(){
    A allAs[] = {A(1),A(2),A(3)};
    }
  • 编译器会尝试把int转换成匿名对象

  • 建议

    • 直接返回匿名对象,编译器会自动优化,把匿名对象和返回值合二为一
  • 能否返回引用?

    • 函数不能返回局部变量的引用
    • 在能够返回引用的情况下,返回引用能够提高程序的运行效率
  • 全局函数和成员函数不构成重载(函数之间没有重载关系),但能构成某一运算符的重载

3.3.3 类型转换函数

  • 转换构造函数可以实现任意类型=>当前类类型的转换
  • C++提供了类型转换函数实现当前类类型到任意类型的转换。
  • 自定义类型可以自己设计,但是如果要重新定义封装好的类型,就只能类型转换了
1
2
3
class A{
operator B();//A=>B
};
  • 如果B是自定义类类型

    1
    2
    3
    class B{
    B(const A& a);//A=>B
    };
  • 实例:

1
2
3
4
5
6
7
8
9
10
11
12
class MyString{
public:
MyString(const char* str){……;}
/*int*/operator int(){return strlen(_s);}//省略了返回值类型
//一是为了保持简洁的风格,而是因为省略了返回值类型不会造成歧义
private:
char* _s;
};
int main(){
MyString s("Hello,World!");
int len=s;
}
  • 另一个实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Name{
friend ostream& operator<<(ostream& o, const Name& name);
public:
operator string();
string& operator[](const string& index);
private:
string firstName; //名字
string midName; //中间名
string lastName; //姓氏
};

Name::operator string(){
stringstream ss; //定义字符串流对象,#include <sstream>
ss<<firstName<<" "<<midName<<" "<<lastName;
return ss.str(); //把流对象转换成字符串并返回
}

int main(){
Name James;
James["first"]="James";
James["mid"]="Robert";
James["last"]="Smith";
string str = James; //两侧类型不一致,编译器自动进行类型转换
cout << str << endl;
}

3.4 仿函数——重载()运算符

  • (),函数运算符,是函数的标志,用来标志参数列表。

  • 不是有()就是函数

  • 仿函数指的是:在类中重载了()之后,这个类的对象可以像函数一样使用。

  • 仿函数在STL的算法中使用非常广泛。Lambda表达式也是对仿函数的扩展。

  • C语言中通过函数指针也可以达到仿函数、Lambda表达式的效果。

  • 例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
#include<string>
using namespace std;
class Show{
public:
void operator()(const string str)//()运算符重载函数
{
cout<<str<<endl;
}
float operator()(const float num)//()运算符重载函数
{
return num*num;
}
};
int main(){
Show s;
s("abcdef"); // 把对象当做函数调用
cout<<s(4)<<endl;
return 0;
}

第4单元 继承与派生

4.1 继承

4.1.1 继承的概念

  • 面向对象提供了继承机制

    • 可以在原有类的基础上,通过简单的程序构造功能更加强大的新类,从而实现代码复用,提高软件开发效率。
    • 换言之,继承就是一个类从另一个类获取成员变量和成员函数的过程。例如类B继承与类A,那么B就拥有A的成员变量和成员函数。
  • 派生

    • 在C++中,派生(Derive)和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。
  • 新构建的类称为子类派生类,现有类称为父类基类

  • C++中可以通过派生形成类的层次结构,称之为类族,即一个基类可以是另一个更高层次的派生类,而派生类也可以继续产生派生类。

  • 派生类的语法

1
2
3
class 派生类名:继承方式 基类名{
派生类成员声明;
};
  • 小例子:
1
2
3
class Animal{};
class Fish:public Animal{};
class Bird:public Animal{};
  • 派生类和基类的关系也称之为“is-a”,也就是:鱼是一种动物

  • 还有一种关系叫做“has-a”,即组合。

  • 几点注意事项

    1. 积累的构造/析构函数不能被继承
      • 派生类拥有自己的构造/析构函数。同时,也拥有基类的所有成员(包括基类的构造/析构函数),只是不能作为派生类的构造/析构函数来使用。
    2. 派生类继承基类的全部成员
      • 拥有但不一定有访问权限
    3. 基类和派生类是多对多的关系
      1. 一个基类可以对多个派生类
      2. 一个派生类可以对应多个基类
    4. 派生类可以新增成员,用于实现新功能

Animal Project实践

AnimalProject.cpp

1
2
3
4
5
6
7
8
#include<iostream>
#include"Animal.h"

int main(){
Fish fish("海棠");
fish.Eat();
fish.Swim();
}

Animal.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma once
#include<string>
#include<iostream>
using namespace std;

class Animal{
public:
Animal(string str);
string GetName();
void Eat();
private:
string name;
};

class Fish: //派生类拥有基类的所有成员
public Animal
{
public:
Fish(string str);
void Swim();
};

Animal.cpp

1
2
3
4
5
6
7
8
9
10
11
#include"Animal.h"

Animal::Animal(string str):name(str){}

string Animal::GetName(){return name;}

void Animal::Eat(){cout<<name<<" eating"<<endl;}

Fish::Fish(string str):Animal(str);

void Fish::Swim(){cout<<GetName()<<" swiming"<<endl;}
  • 总结
    • 继承的主要作用是实现代码复用。把类的相同成员那些在基类中,把类的不同成员写在派生类中。
    • 派生类拥有基类的所有成员,但是,派生类不能直接访问基类的私有成员。

4.1.2 继承方式

  • 派生类拥有基类的所有成员,但是能否访问不仅受到成员自身的访问权限属性影响,还受继承方式影响。
  • 类的继承方式有三种:
    • 共有public继承
      • 最常用继承方式
      • 类内、子类、类外可以访问
    • 保护protect继承
      • 类内、子类可以访问
    • 私有private继承
      • 默认继承方式
      • 类内可以访问,子类不能访问
  • 继承方式和访问权限是两个概念,只是使用了相同的关键字。
  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限
    • 继承方式中的public private是用来指明基类成员在派生类中的最高访问权限的。
  2. 不管继承方式如何,基类中的private成员以及不可访问成员,在派生类中始终不能直接使用
  • 在当前类中应该如何使用继承方式和访问权限?
    1. 当前类中不允许在派生类中使用、更不允许向外暴露的成员,应声明为private
    2. 当前类中允许派生类中使用、但不向外暴露(不能通过对象访问),声明为protected
    3. 当前类中即允许派生类使用、也允许向外暴露的,声明为public

4.1.3 类型兼容

不同类型的数据在一定条件下可以进行转换,称为类型转换,也称为类型兼容。

在C++中,基类与派生类之间也存在类型兼容。

  • 类型兼容的形式
    1. 使用公有派生类对象为基类对象赋值
    • 子类变量可以赋值给父类变量
    • 父类变量不能赋值给子类变量
    1. 使用公有派生类对象为基类对象的引用赋值
    2. 使用公有派生类对象的指针为基类指针赋值
    3. 如果函数的参数是基类对象、基类对象的引用、基类指针,则函数在调用时,可以使用公有派生类对象、公有派生类对象的地址作为实参。

4.1.4 父类指针指向子类对象

父类指针数组指向所有子类对象

1
2
3
4
5
6
7
8
//元素类型 数组名[数组大小];
Animal* allAnis[4]; //父类指针数组
allAnis[0] = new Fish("大鱼");//父类指针指向子类对象
allAnis[1] = new Bird("大鸟");
allAnis[2] = new Fish("小鱼");
allAnis[3] = new Bird("小鸟");
for(int i=0;i<4;++i)
allAnis[i]->Eat(); //通过父类指针数组的元素调用公有成员
  • RTTI(运行时类型识别),它使程序能够获取由基类指针(引用)所指向的成员的实际派生类型。
  • 在C++中,为了支持RTTI提供了两个操作符
    • dynamic_cast
    • typeid

4.2 派生类

4.2.1 派生类的构造函数与析构函数

派生类的成员变量包括从基类继承的成员变量和新增的成员变量,因此,派生类的构造函数除了要初始化派生类中新增的成员变量,还要初始化基类的成员变量,即派生类的构造函数要负责调用基类的构造函数。

1
2
3
4
派生类构造函数(参数列表):基类构造函数(基类构造函数参数列表)
{
派生类新增成员的初始化语句
}

在定义派生类构造参数时,通过:运算符在后面完成基类构造函数的调用。基类构造函数的参数从派生类构造函数的参数列表中获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base{ //基类
public:
Base(int aa,int bb):ma(aa),mb(bb){}
private:
int ma,mb;
};

class Derive:public Base{ //派生类
public:
Derive(int aa, int bb,int xx,int yy,int zz):Base(aa,bb),mx(xx),my(yy),mz(zz){}//初始化各变量
private:
int max,my,mz; //除此之外还有基类的两个
}
  • 如果基类有无参构造函数(包括默认构造函数),则在定义派生类构造函数时可以省略对基类构造函数的调用

  • 尽管没有调用基类无参构造函数,但依然会执行基类无参构造函数

  • 如果基类定义了有参构造函数,派生类必须定义构造函数,提供基类构造函数的参数,完成基类成员变量的初始化。

  • 构造函数的调用顺序:基类=>成员对象=>派生类

  • 析构函数的调用顺序反过来

4.2.2 在派生类中隐藏基类成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<iostream>
using namespace std;
class Vehicle{ //定义交通工具类Vehicle
public:
void run(); //交通工具普通成员函数run()
};

void Vehicle::run(){ //类外实现run()函数
cout<<"基类run()函数:行驶"<<endl;
}

class Car::public Vehicle{ //定义小汽车类,继承Vehicle
public:
void run();
};

void Car::run(){ //小汽车类普通成员函数run()实现
cout<<"小汽车需要燃烧汽油,行驶速度快"<<endl;
}

int main(){
Car car;
car.run(); //派生类run
car.Vehicle::run(); //通过基类名与作用域限定符调用run()函数
Vehicle* pv=&car;
pv->run(); //基类指针调用基类run()函数
return 0;
}

4.3 多继承

派生类有多个基类

1
2
3
class Base1{}
class Base2{}
calss Derive:public Base1,public Base2 {}

4.4 虚继承

间接基类的成员变量在底层派生类中只有一份拷贝,从而避免成员访问的二义性。

1
2
3
class 派生类名: virtual 权限控制符 基类名{
派生类成员
};

第5章 多态与虚函数

5.1 概述

  • 封装

  • 继承

  • 多态

  • 向不同的对象发送同一个消息(即成员函数的调用),会产生不同的行为。

  • 多态是一种:调用同名函数却因上下文不同会有不同实现的一种机制。

  • 多态带来的好处

    • 不用记大量的函数名
    • 会一句调用时的上下文来自动确定实现
    • 带来了面向对象的编程
  • 静态多态(静态联编)

    • 编译时实现,不影响运行速度
    • 重载 Overload
  • 动态多态(动态联编)

    • 运行时实现,需要查找虚函数表,影响运行速度
    • 继承和虚函数

5.2 虚函数实现多态

虚函数是运行时多态,若某个基类函数声明为虚函数,则其公有派生类将定义与其基类虚函数原型相同的函数,这时,当使用基类指针或基类引用操作派生类对象时,系统会自动用派生类中的同名函数代替基类虚函数。

  • 虚函数的声明及类外实现格式
1
2
3
4
5
6
class 类名{
访问权限:
virtual 函数类型 函数名(参数列表);
};

函数类型 类名::函数名(参数列表){函数体;}
  • 小例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Enemy{  //基类 
public:
virtual void Attack(); //基类的虚函数
};

class Dog:public Enemy{ //共有派生类
void Attack(); //因为基类是虚函数,所以这里只能是虚函数,一虚到底
};

int main(){
Dog dog;
Enemy* pe = &dog;
pe->Attack(); //基类指针
Enemy& re = dog;
re.Attack(); //基类引用
}
//都会输出"狗咬了你一口"

void Enemy::Attack(){
cout<<"敌人发起攻击"<<endl;
}

void Dog::Attack(){
cout<<"狗咬了你一口"<<endl;
}
  • 类比常成员函数、静态成员函数
1
2
3
4
5
6
7
8
9
10
11
12
class 类名{
访问权限:
函数类型 函数名(参数列表)const //常成员函数
static 函数类型 函数名(参数列表) //静态成员函数
static 变量类型 变量名; //静态成员变量
virtual 函数类型 函数名(参数列表) //虚函数
};

函数类型 类名::函数名(参数列表)const{函数体} //常成员函数的类外实现
函数类型 类名::函数名(参数列表){函数体;} //静态成员函数的类外实现
变量类型 类名::变量名 = 初始值; //静态成员变量的类外初始化
函数类型 类名::函数名(参数列表){函数体;} //虚函数的在外实现
  • 重载、隐藏和覆盖的区别

    • 重载
      • 都是以函数名相同为条件
      • 重载的规则最简单,因为没有跨类,不涉及类型兼容问题
    • 隐藏
      • 规则相对复杂
      • 参数不同,无论是否是虚函数,基类的函数都将被隐藏;
      • 参数相同,基类不是虚函数,构成隐藏(是虚函数,构成覆盖)
    • 覆盖
      • 基类与派生类有同名函数,参数相同,并且是虚函数
      • 虚函数的特点是“虚到底”。
      • 在覆盖的情况下,函数的调用取决于对象的类型
  • 小总结

    • 重载就是重载
    • 隐藏与虚函数无关,出现同名函数的时候,基类的函数会被隐藏,除非使用基类类型指针或引用指向派生类,才有可能调用到被隐藏的函数。
    • 覆盖就是虚函数的隐藏,看最终指向的对象类型是啥就调用啥。

5.2.1 所有敌人发起攻击!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Enemy{
public:
virtual void Attack(){
cout<<"敌人发起了攻击"<<endl;
}
};

class Cat:public Enemy{
public:
Cat(){cout<<"来了一只猫"<<endl;}
virtual void Attack(){cout<<"猫挠了你一把"<<endl;}
};

class Dog:public Enemy{
public:
Cat(){cout<<"来了一只狗"<<endl;}
virtual void Attack(){cout<<"狗咬了你一口"<<endl;}
};

int main(){
srand(time(0)); //初始化随机数种子
const int size = 4;
Enemy* allEnemies[size]; //元素类型 数组名[数组大小]

for(int i=0;i<size;++i) //初始化
if(rand()%2 == 0)
allEnemies[i]=new Cat;
else
allEnemies[i]=new Dog;

for(int i=0;i<size;++i) //所有敌人发起攻击
allEnemies[i]->Attack();
}

5.2.2 数函数实现的机制

  • 在编写程序时,我们需要根据函数名、返回值类型、参数列表等信息正确调用函数,这个匹配过程称之为绑定。

  • C++提供了两种函数绑定机制:静态绑定和动态绑定。

  • 静态多态(重载)实现的机制是静态绑定(静态联编),它是指在编译时期就能确定调用的函数。

  • 动态多态(覆盖)实现的机制是动态绑定(动态联编),它是指在运行时才能确定要调用的函数。

    1
    2
    for(Enemy* pe:allEnemies)
    pe->Attack();
  • 虚函数是通过虚函数表vtable实现动态绑定的。

  • 虚函数对应一个指向vtable虚函数表的指针,而这个指向vtable的指针是存储在对象的内存空间的。换言之,必须在对象穿件之后才能访问虚函数,而构造函数就是用来创建对象的,所以构造函数不能是虚函数。

5.2.3 虚析构函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Enemy{
public:
virtual void Attack(){cout<<"敌人发起了攻击"<<endl;}
~Enemy(){cout<<"释放敌人"<<endl;} //析构函数
};

class Cat:public Enemy{
public:
Cat(){cout<<"来了一只猫"<<endl;}
virtual void Attack(){cout<<"猫挠了你一把"<<endl;}
};

class Dog:public Enemy{
public:
Cat(){cout<<"来了一只狗"<<endl;}
virtual void Attack(){cout<<"狗咬了你一口"<<endl;}
};

int main(){
srand(time(0)); //初始化随机数种子
const int size = 4;
Enemy* allEnemies[size]; //元素类型 数组名[数组大小]

for(int i=0;i<size;++i) //初始化
if(rand()%2 == 0)
allEnemies[i]=new Cat;
else
allEnemies[i]=new Dog;

for(int i=0;i<size;++i) //所有敌人发起攻击
allEnemies[i]->Attack();

for(int i=0;i<4;i++)
delete allEnemies[i]; //释放第i个敌人
}

//这样对象会没有被完全释放,这个结果是错误的。正常情况下,首先要调用派生类的析构函数,再调用基类的析构函数。
  1. 通过delete释放对象时,会调用析构函数
  2. 目前析构函数不是虚函数,所以函数的调用取决于变量的类型
  3. 是通过基类指针释放的,所以会调用基类的析构函数,并且不会调用派生类的析构函数。
  • 为了确保整个对象被完全释放,所以应该声明析构函数为虚析构函数
1
2
3
4
class 类名{
public:
virtual ~类名(); //基类声明虚析构函数后,所有派生类的析构函数也自动称为虚析构函数。
};
  • 声明虚析构函数后,在通过基类指针释放派生类对象时,才会按照正常的释放流程,首先调用派生类的析构函数、在调用基类的析构函数,确保对象被正常释放。
  • 建议:即使默认析构函数已经满足功能需求,但是仍然建议习惯性的给基类设置虚析构函数!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Enemy{
public:
virtual void Attack(){cout<<"敌人发起了攻击"<<endl;}
virtual ~Enemy(){cout<<"释放敌人"<<endl;} //析构函数
};

class Cat:public Enemy{
public:
Cat(){cout<<"来了一只猫"<<endl;}
virtual void Attack(){cout<<"猫挠了你一把"<<endl;}
virtual ~Cat(){cout<<"释放猫"<<endl;} //猫虚析构函数
};

class Dog:public Enemy{
public:
Cat(){cout<<"来了一只狗"<<endl;}
virtual void Attack(){cout<<"狗咬了你一口"<<endl;}
virtual ~Dog(){cout<<"释放狗"<<endl;} //狗虚析构函数
};

int main(){
srand(time(0)); //初始化随机数种子
const int size = 4;
Enemy* allEnemies[size]; //元素类型 数组名[数组大小]

for(int i=0;i<size;++i) //初始化
if(rand()%2 == 0)
allEnemies[i]=new Cat;
else
allEnemies[i]=new Dog;

for(int i=0;i<size;++i) //所有敌人发起攻击
allEnemies[i]->Attack();

for(int i=0;i<4;i++)
delete allEnemies[i]; //释放第i个敌人
}

5.3 纯虚函数与抽象类

  • 基类的虚函数没有什么意义,只需要声明好就行。
  • virtual void Attack() = 0;
1
2
3
4
class Enemy{
public:
virtual void Attack() = 0;
};
  • 纯虚函数的作用是在基类中为派生类保留一个结构,方便派生类根据需要实现接口,进而实现多态。

  • 如果一个类中包含纯虚函数,则该类称为抽象类。

    • 保留接口
    • 方便派生类实现接口
    • 实现多态
  • 抽象类只能用来派生新类,而不能实例化对象

    • 可以定义抽象类指针或引用,并且抽象类指针指向派生类对象,这是面向对象编程常用的做法
  • 如果派生类没有实现基类的全部接口,则派生类仍然是抽象类

  • 针对接口进行编程

  • 派生类必须实现基类的所有接口才行

5.4 强制类型转换

  • 转换构造函数
  • 类型转换函数
1
2
3
4
5
6
7
8
9
10
11
class A{
public:
A(const B&); //转换构造函数
operator B(); //类型转换函数
};//强调的是如何实现转换

int main(){
B b;
A a(b); //调用转换构造函数
b = (B)a; //电泳类型转换函数
}
  • C语言的强制类型转换简单粗暴,没有任何安全机制

  • C++提供了四个类型转换运算符,对应不同类型数据之间的转换

    • static_cast<type>(expression)
      • 用于代替C语言中通常的转换操作
    • dynamic_cast<type>(expression)
      • 用于类层次间的向上转换和向下转换
    • const_cast<type>(expression)
      • 用于用于去除常引用和常指针的const属性
    • reinterpret_cast<type>(expression)
      • 为操作数的位模式提供底层的重新解释
  • static_cast<type>(expression)

    • 基本数据类型之间的转换
      • 例:int i = 1;double j = static_cast<double>(i);
    • 把任何类型转换成void类型
    • 把空指针转换成目标类型的空指针
    • 向上类型转换是安全的;向下类型转换则不安全。

基类指针指向派生类对象

1
2
Base* pb = new Derive;  //偷懒写法
Base* pb = static_cast<Base*>(new Derive);
  • dynamic_cast<type>(expression)
    • 主要应用于类层次间的向上转换Upcasting和向下转换Downcasting
    • 只适用于含有虚函数的类型之间的转换
  • RTTI,Run-time Type Identification, 运行时类型识别
    • 通过typeiddynamic_cast两个运算符实现RTTI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Animal{
public:
Animal(string str);
void Eat();
virtual void Speak() = 0;
virtual ~Animal(){}
protected:
string name;
};

class Dog:public Animal{
public:
Dog(string str);
void Speak();
void Run();
};

class Bird:public Animal{
public:
Bird(string str);
void Speak();
void Fly();
};

int main(){
const int size = 4;
Animal* allAnis[size]; //基类指针数组,父类指针数组指向所有派生类对象

cout<<"-------------初始化-------------"<<endl;
allAnis[0] = new Dog("黑狗");
allAnis[1] = new Bird("翠鸟");
allAnis[2] = new Dog("白狗");
allAnis[3] = new Bird("黄鸟");

cout <<"-------------运行中-------------"<<endl;
for(Animal* pa:allAnis){ //基于范围的循环
pa->Speak();
pa->Eat();
if(typeid(*pa)==typeid(Dog)){
Dog* pd = dynamic_cast<Dog*>(pa);
pd->Run();
}
else if(typeid(*pa) == typeid(Bird)){
Bird* pb = dynamic_cast<Bird*>(pa);
pb->Fly();
}
}
}

第6单元 异常

6.1 异常处理方式

  • 语法错误

    • 在编译和连接阶段就能发现、只有100%符合语法规则的代码才能编译通过,属于最容易发现、定位、排除、最不用担心的错误。
  • 逻辑错误

    • 编写代码的思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决
  • 运行时错误

    • 在运行期间发生的错误
    • 除数为0、内存分配失败、数组越界、文件不存在等
    • 放任不管的话会发生程序崩溃
  • C++通过trythrowcatch结构实现了异常的检测、抛出和捕获

1
2
3
4
5
6
7
try{
可能产生异常的语句或函数;
}
catch(异常类型1){处理异常的语句;}
catch(异常类型2){处理异常的语句;}
...
catch(...){处理异常的语句}
  • 通常情况下,try语句中不包含抛出异常的语句,而是调用的函数如果发生异常,就通过throw关键字抛出异常。函数的上层调用者通过try...catch语句检测、捕获异常,并对异常进行处理。
  • 如果函数调用者也不能处理异常,则异常会即系向上一层调用者传递,直到异常被处理为止。如果最终异常没有被处理,则C++运行系统就会捕捉异常,终止程序运行。
  • C++的异常处理机制使得异常的引发和处理不必在同一函数中完成,函数的调用者可以在适当的位置对函数排除的异常进行处理。这样,底层的函数可以着重解决具体的业务问题而不必考虑对异常的处理,而整个程序中相同类型的异常可以进行集中处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyString{
public:
MyString(const char* s = ""){_s = new char[strlen(s)+1];strcpy(_s,s);}
~MyString(){delete[] _s;}
friend ostream& operator<<(ostream& o,const MyString& s);
char& operator[](int index){
if(index<0 || index >= strlen(_s)) throw ("下标越界");
return _s[index];
}
private:
char* _s;
};

int main(){
MyString str("abc");
cout <<str <<endl;
try{
str[3]='C';
cout<<str<<endl;
}
catch (const char* s){
cout<<"异常:"<<s<<endl;
}
}
  • 使用try...catch的注意事项
    • 一个语句中只能有一个try块,但可以有多个catch语句块,以便与不同的异常类型匹配。catch语句必须有参数,如果try语句块中的代码抛出了异常,无论抛出的异常的值是什么,只要异常的类型与catch的参数类型匹配,异常就会被catch语句捕获。最后一个catch语句参数为...符号,表示可以捕获任意类型的异常。
    • 一旦某个catch语句捕获到了异常,后面的catch语句将不再被执行,其用法类似switch...case语句。
    • trycatch语句块中的代码必须使用大括号{}括起来,即使语句块中只有一行代码。
    • try语句和catch语句不能单独使用,必须连起来一起使用。

6.2 栈解旋

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Demo{
public:
Demo(int nn):no(nn){
cout <<"创建对象"<<no<<"..."<<endl;
++cout;
if(cout>2) throw("对象数量达到上限");
cout<<"创建成功!!"<<endl;
}
~Demo(){
--cout;
cout<<"释放对象"<<no<<endl;
}
private:
int no;
static int count;
};

int Demo::cout = 0;

int main(){
Demo x(1);
try{
Demo y(2);
Demo z(3);
}
catch (const char* s){
cout <<"异常:"<<s<<endl;
}
}
  • C++在异常处理前释放所有局部对象。释放的顺序与创建的顺序相反。
  • 但是,栈解旋只能释放栈对象,不能释放堆对象。要在catchdelete

6.3 标准异常

  • 在大多数关于异常的初级文章中,往往会以各种简单类型作为异常类型。

  • C++提供了一系列标准异常,它们时以父子类层次结构组织起来的。

  • C++提供了一系列标准异常,它们是以父子类层次结构组织的。我们可以:

    1. 在程序中使用这些标准异常
    2. 通过继承和重载exception类来定义新的异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyString{
public:
MyString(const char* s = ""){_s = new char[strlen(s)+1];strcpy(_s,s);}
~MyString(){delete[] _s;}
friend ostream& operator<<(ostream& o,const MyString& s);
char& operator[](int index){
if(index<0 || idnex >= strlen(_s)) throw out_of_range("下标越界");
return _s[index];
}
private:
char* _s;
};

int main(){
MyString str("abc");
cout <<str <<endl;
try{
str[3]='C';
cout<<str<<endl;
}
catch (exception& e){
cout<<"异常:"<<e.what()<<endl;
}
}
  • 异常规范说明的语法:
    • 函数返回值类型函数名(形参表) throw(异常类型1,异常类型2,...);
    • 函数返回值类型函数名(形参表) throw();
  • C++11有了noexcept,用来不抛出异常

6.4 断言

  • C++从C语言继承了assert#error两个方法,用来检查错误的。
  • assert是运行时的断言,它用来发现运行期间的错误,不能提前到编译期发生错误,也不具有强制性,也谈不上改善编译信息的可读性。既然是运行期检查,对性能肯定是有影响的,所以在发型版本中,assert都会被关掉。
  • #error仅仅能在预编译时显示一个错误信息,可以配合#ifdef/ifndef参与预编译的条件检查。由于它无法获得编译信息,当然,也就做不了进一步分析了。
1
2
3
4
5
6
7
8
9
10
using namespace std;

#include <exception>
#include <cassert>

int main(){
int age = -1;
assert(age > 0);
cout<<age<<endl;
}
  • C++11中引入了static_assert这个关键字,用来做编译期间的断言,因此叫做静态断言。语法:
  • static_assert(常量表达式,”提示字符串”)
1
2
3
int main(){
static_assert(sizeof(int)==8,"64-bit code generation is not supported");
}
  • 断言还是异常?
    • 断言表示程序写错了,只要发生断言(准确的说,应该是断言失败),意味着至少有一个人得修改代码。它的性质如同编译错误。
    • 如果代码书写完全正确,但因外界环境或用户操作仍然可能发生的事件,都不适合用断言,可以使用异常,或者条件判断处理。

第7章 IO流

7.1 IO流类库

  • 输入/输出 用于完成数据传输。C++支持两种I/O操作:

    1. C语言的I/O函数
    2. 另一种是面向对象的I/O流类库。流是对数据从一个对象到另一个对象的传送过程的抽象。
  • 在针对文本类型的数据,还可以和插入运算符<<和提取运算符>>配合使用。

  • C语言的IO解决方案

    • 使用scanf()gets()等函数从键盘读取数据。
    • 使用printf()puts()等函数向屏幕上输出数据。
    • 使用fscanf()fgets()等函数读取文件中的数据。
    • 使用fprintf()fputs()等函数向文件中写入数据。
  • C++的IO解决方案

    • 标准IO:cin接收从键盘输入的数据,用cout向屏幕上输出数据
    • 文件IO:通过定义ifstreamofstream的对象,进行数据的读写操作
    • 字符串流(stringstream)
  • IO流类库的层次结构

    • 流类库具有两个独立的基类:streambuf和ios类,所有流类均是其中一个的子类
    • 最常用的三个类:
      • iostream,控制台的输入、输出
      • fstream,文件的读写
      • stringstream,字符串流的处理
  • streambuf类库

    • streambuf提供了缓冲区操作的接口,它的子类隐藏了大量操作的细节。
    • stringstream包含了一个它的子类stringstreambuf,以提供字符串的格式化读取和输出操作。
    • fstream也包含了它的一个子类filebuf,以避免大量的IO操作。

7.2 标准IO流

  • 预定义流对象
对象名 所属类 对应设备 含义
cin istream 键盘 标准输入,有缓冲
cout ostream 屏幕 标准输出,无缓冲
cerr ostream 屏幕 标准错误输出,无缓冲
clog ostream 屏幕 标准日志输出,有缓冲
  • ostream有19个重载!真他妈的强!

  • 同printf类似,也可以进行格式化输出,例如选择进制、对齐方式、精度等等

  • 除<<之外,还提供了很多成员函数,包括:

    • put,输出字符,例如cout.put(‘A’).put(‘\n’);
    • write,输出字符串
  • 输入流对象cin和提取运算符>>结合使用,可以用于各种类型数据的输入。

  • 以空白(包括空格、Enter、Tab)为分隔符。

  • 流输入运算符的重载

    • 除>>之外,还提供了很多成员函数,包括:
      • get()函数,用于读取指定长度的字符或者遇到结束标志(可以指定)为止。
      • getline()函数,用于读取指定长度的字符或者遇到结束标志(可以指定)为止。
      • read()读取指定字符个数的字符串。
      • ignore()跳过n个字符。
      • gcount()统计上次读取的字符个数。
      • peek()检测输入流中读取的字符。
      • putback()将上一次读取的字符放回输入流中。
  • 如何判断输入结束?

    • 在输入数据的多少不确定,且没有结束标志的情况下,该如何判断输入数据已经读完了呢?
      1. 读取文件时,到达文件末尾就读取结束。
      2. 在控制台输入特殊的控制字符就表示输入结束。
        • 在Windows系统中,按Ctrl+Z组合键后再按回车键,代表输入结束。
        • 在UNIX/Linux/Mac OS系统中,Ctrl+D代表输入结束。

7.3 文件流

  • 常见的C++文件操作,包括(但不限于)打开文件、读取和追加数据、插入和删除数据、关闭文件、删除文件等。

  • 为了方便用户实现文件操作,C++提供了3个文件流类

    • ofstream(实现写文件)
    • ifstream(实现读文件)
    • fstream(实现读写文件)
    • 均包含在
  • 关于文件操作,虽然在C++程序中可以继续沿用C语言的那套文件操作方式,但更推荐使用适当的文件流类来读写文件。

  • 调用无参构造函数创建文件流对象

    1
    2
    3
    ifstream ifs; //定义一个文件输入流对象
    ofstream ofs; //定义一个文件输出流对象
    fstream fs; //定义一个文件输入、输出流对象
  • 调用有参构造函数创建文件流对象

    1
    2
    3
    4
    ifstream ifs("filename",ios::in);
    ofstream ofs("filename",ios::out);
    fstream fs("filename",ios::in|ios::out);
    //ifstream类默认文件打开模式为ios::in,ofstream类默认文件打开模式为ios::out,fstream类默认文件打开模式为ios::in|ios::out
  • 打开文件

    1
    2
    3
    4
    void open(const char* filename, int mode);  //封装的声明

    ofstream ofs; //创建文件流对象
    ofs.open("Hellp.txt",ios::in|ios::out|ios::binary);//多种打开模式组合使用
文件打开模式 含义
ios::in 以只读模式打开文件,若文件不存在,则发生错误
ios::out 以只写模式打开文件,写入时覆盖写入;若文件不存在,则创建一个新文件
ios::app 以追加模式打开文件,若文件不存在,则创建一个新文件
ios::ate 打开一个已存在文件,并将文件位置指针移动到文件末尾
ios::trunc 打开一个文件,将文件内容删除。若文件不存在,则创建一个新文件
ios::binary 以二进制方式打开文件
  • 关闭文件

    1
    2
    3
    ifstream ifs;
    ifs.open("Hello.txt",ios::in);
    ifs.close(); //关闭文件
  • 文件读写

    • 文本文件
      • 和<<

      • 文件流类的成员函数
    • 二进制文件
      • 文件流类的成员函数
  • 二进制文件

    • 节省空间
    • 方便检索
    • 还可以进行随机读写
  • 小例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    #define _CRT_SECURE_NO_WARNINGS
    #include <iostream>
    #include <list>
    #include <fstream>
    using namespace std;

    void main(){
    list<int> intList; //整数线性表
    ifstream ifs;
    ifs.open("in.txt");
    if(!ifs) exit(0);
    ofstream ofs;
    ofs.open("out.txt");
    if(!ofs) exit(0);
    //从in.txt中读取数字,编码需要时ANSI编码,是默认的C++编码
    int temp;
    while(ifs>>temp)//Ctrl+Z
    //while(cin>>temp)
    intList.push.back(temp);

    //排序
    intList.sort();

    //输出结果到屏幕
    for(int x:intList)
    cout<<x<<endl;
    //cout<<x<<endl;
    //基于范围的for循环

    ifs.close();
    ofs.close();
    }

7.4 字符串流

  • 字符串流是以string对象为输入/输出对象的数据流,这些数据流的传输再内存中完成,因此也被称为内存流。
    1
    2
    3
    4
    #include<sstream>
    istringstream//把一个数字字符串转换成对应的数值
    ostringstream//构造字符串
    stringstream
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <iostream>
    #include <sstream>
    using namespace std;

    int main(){
    stringstream ss("3.1415926");
    float f;
    ss >> f; //把一个数字字符串转换成对应的数值
    cout << "f=" << f << endl;

    ss.clear();
    ss << "π=" << f << endl; //构造字符串
    string str = ss.str();
    cout << str << endl;
    }

第8章 模板

8.1 模板的概念

  • 模板是C++支持参数化多态的工具,是泛型编程的基础。

  • 模板可以实现类型参数化,即把类型定义为参数,真正实现了代码的可重用性,减小了编程级维护的工作量,降低了编程难度。

  • 模板分为函数模板类模板

  • 下面三个重载函数

    • int max(int a,int b){return a>b?a:b;}
    • float max(float a,float b){return a>b?a:b;}
    • std::string max(std::string a,std::string b){return a>b?a:b;}
  • 这三个函数,除了类型不一致,其他都是一模一样的。

  • C++属于强类型的编程语言

    • 编译器在程序运行前就要进行类型检查并分配内存。
  • 导致程序员为逻辑结构相同而具体数据类型不同的对象编写基本一样的代码。

  • C++提供了模板机制,可以把类型参数化。带类型参数的函数称为函数模板,带类型参数的类称为类模板。

  • 实例化

    • 在运行时,编译器会根据实际的数据类型参数生成一段相应的可执行代码,这个过程称为模板实例化。

8.2 函数模板

  • 语法格式

    1
    2
    template<typename T1 [,TYPENAME T2,……]> //后面没有;
    返回值类型 函数名(参数列表){/**函数体/}
  • template(模板)和typename(类型名)都是关键字;

    • 早期的C++使用class表示模板参数,但是不能使用struct
  • T1,T2……表示模板参数(类型形参)

    • 使用<>括起来,将来在调用函数模板时也需要<>把类型实参括起来
    • 常用的标识符如T、T1、T2、U、V等。
  • 在返回值类型、参数列表、函数体中,均可使用模板参数。

8.2.2 模板的实例化

1
2
3
4
5
6
7
8
9
10
//编译时将 函数模板 转换为 模板函数
template<typename T>
T Add(T a,T b){return a + b;}
int main(){
cout<<Add<int>(1,2)<<endl;
cout<<Add<double>(1.2,3.4)<<endl;
cout<<Add(1.2f,3.4f)<<endl; //隐式实例化
cout<<Add('A',32)<<endl; //这个会报错
cout<<Add<char>('A',32)<<endl;
}
  • 编译器会根据具体的调用情况,在目标文件中,把函数模板实例化成若干个函数。
  • 通过函数模板,可以定义一系列的函数,这些函数都基于同一份代码,但是可以作用在不同类型的数据上。

8.2.3 函数模板重载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename T>
T Add(T a,T b){return a+b;}

template <typename T>
T Add(T a,T b,T c){return a + b + c;}

double Add(double a, double b){
cout<<"这是一个非模板函数"<<endl;
return a+b;
}

int main(){
cout<<Add(1,2)<<endl;
cout<<Add(1,2,3)<<endl;
cout<<Add(1.2,3.4)<<endl;
}
  • 函数模板也可以重载
  • 非模板函数优先于模板函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Complex{
friend ostream& operator<<(ostream& o, const Complex& c);
public:
Complex(float xx,float yy);
bool operator>(const Comlpex& other)const;//这里得是常函数
private:
float x,y;
};
//T对应的类型实参,必须要支持函数模板中所有的操作。

template<typename T>
T Max(const T& a,const T& b){ //这里是常对象
return a > b ? a : b;
}//常对象只能调用常函数

int main(){
Complex c1(1,2),c2(3,4);
cout <<Max(c1,c2)<<endl;
}

8.3 类模板

1
2
template<typename T1[,typename T2,……]>
class 类名{……};
  • 关键字的含义和函数模板相同。
  • 类模板实例化的过程与函数模板实例化的过程类似,但是模板函数允许隐式实例化,而类模板的实例化不能省略模板的参数。
    1
    2
    3
    4
    template<typename T>T max(T a,T b){……;}
    int a=max(1,2); //等价于int a = max<int>(1,2);
    template<typename T>Array{……};
    Array<int>intArray; //不能省略模板参数

类模板的成员函数的类外实现

1
2
3
4
5
6
7
8
template<typename T>
class Demo{
函数类型 函数名(参数列表);
};

template <typename T>
函数类型 类名::函数名(参数列表){函数体;}
//模板的声明和实现都必须写在.cpp文件中
  • 小例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//Array.h
#pragma once
#include<iostream>
using namespace std;

template<typename T>
class Array{
template <typename T>
friend ostream& operator<<(ostream& o,const Array<T>& a);
public:
Array(T a[],int size);
~Array();
private:
T* _ptr; //用指针指向动态数组
int _size; //数组的大小
}; //动态数组

template <typename T>
ostream& operator<<(ostream& o,const Array<T>& a){
for(int i = 0;i<a._size;++i)
o<<a._ptr[i]<<" ";
return o;
}

template<typename T>
Array<T>::Array(T a[],int size):_size(size){
_ptr = new T[_size];
for(int i = 0;i<_size;++i)
_ptr[i] = a[i];
}

template<typename T>
Array<T>::~Array(){
delete[] _ptr;
}
1
2
3
4
5
6
7
8
9
10
11
12
//main.cpp
#include <iostream>
#include "Array.h"
int main(){
int a[]={1,2,3,4,5,6};
Array<int> intArr(a,6); //用普通数组初始化Array对象
cout << intArr << endl;

char charA[]="Hello";
Array<char> charArr(charA,5);
cout << charArr << endl;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//Array.h
#pragma once
#include<iostream>
using namespace std;

template<typename T> class Array;
template <typename T> ostream& operator<<(ostream& o,const Array<T>& a);

template<typename T>
class Array{
friend ostream& operator<< <>(ostream& o,const Array<T>& a);
public:
Array(T a[],int size);
~Array();
private:
T* _ptr; //用指针指向动态数组
int _size; //数组的大小
}; //动态数组

template <typename T>
ostream& operator<<(ostream& o,const Array<T>& a){
for(int i = 0;i<a._size;++i)
o<<a._ptr[i]<<" ";
return o;
}

template<typename T>
Array<T>::Array(T a[],int size):_size(size){
_ptr = new T[_size];
for(int i = 0;i<_size;++i)
_ptr[i] = a[i];
}

template<typename T>
Array<T>::~Array(){
delete[] _ptr;
}
1
2
3
4
5
6
7
8
9
10
11
12
//main.cpp
#include <iostream>
#include "Array.h"
int main(){
int a[]={1,2,3,4,5,6};
Array<int> intArr(a,6); //用普通数组初始化Array对象
cout << intArr << endl;

char charA[]="Hello";
Array<char> charArr(charA,5);
cout << charArr << endl;
}

类模板与友元函数

  1. 将普通函数声明为友元函数
    • 多个全局函数只需要声明一次友元
  2. 约束模板友元函数
    • 在类模板内部声明模板函数为友元
  3. 非约束模板友元函数
    • 在类模板内部声明函数模板为友元

区别

  • 类模板
  • 模板类
  • 函数模板
  • 模板函数

指定了的类型就会具体化成某一个类或函数,也就成为了模板类或模板函数,否则就是一个通用的模板,就称为类模板或函数模板

第9章 STL

9.1 STL组成

  • 六大组件
    • 容器Container
    • 迭代器Iterator
    • 算法Algorithm
    • 仿函数Functor
    • 适配器Adapter
    • 分配器Allocator

容器

容器是存储其他类型对象(类类型、简单类型均可)。这些被存储的对象必须是同一数据类型,称为容器的元素。容器是通过类模板实现的,包含了处理这些数据的常见方式。而且,不同的容器的同一个功能,其函数名也是相同的。这大大降低了使用者的学习成本。

迭代器

  • 重复repeat
  • 迭代iterate
  • 针对容器进行循环遍历时候的一种工具,扮演着容器和算法之间的胶合剂:对容器中数据的读和写,是通过迭代器完成的。
  • 迭代器模式(Iterator),提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。
  • 数组:下标,链表:指针。迭代器是二者的合体,更像指针。
  • STL提供了输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器五种类型的迭代器。

算法

  • 算法是STL定义的一系列函数模板
  • STL算法不依赖于容器的实现细节,只要容器的迭代器符合算法要求,算法就可以通过迭代器处理容器中的元素。

仿函数

  • 仿函数也称为函数对象,通过重载()运算符实现,使类具有函数一样的行为。
  • 大多数STL算法可以使用一个仿函数作为参数,以达到某种数据操作的目的,使STL的应用更加灵活方便,增强了算法的通用性。
  • 例如,在排序算法中,可以使用仿函数less或greater作为参数,以实现数据从大到小或从小到大的排序。

适配器

  • 仿函数适配

  • 迭代器适配

  • 容器适配:采用特定的数据管理策略,能够使容器在操作数据时表现出另一种行为。

  • STL的三个容器适配器

    • stack(栈)
    • queue(队列)
    • priority_queue(优先队列)

空间配置器

  • Allocate,分配,C语言中的malloc就是memory allocate的缩写
  • C++标准库采用了分配器实现对象内存空间的分配和释放,封装了容器在内存管理上的低层细节。默认情况下,程序员也可自行定值分配器以替代之

9.2 序列容器

也叫作顺序容器,序列容器各元素之间有顺序关系,每个元素都有固定位置,除非使用插入或删除操作改变这个元素的位置。序列容器是一种线性结构的有序群集。序列容器有连续存储链式存储两种存储方式。

容器 基本功能 对应头文件
vector 动态数组 #include<vector>
list 双向链表 #include<list>
deque Double-ended queue双端队列 #include<deque>
array C++11,大小固定的数组 #include<array>
forward_list C++11,单向(向后)链表 #include<forward_list>

vector向量

  • 动态数组,在插入或删除元素时能够自动调整自身大小。元素放置在连续内存空间中,可以使用迭代器对其进行访问和遍历。
  • 插入/删除元素时,之后的元素都要被顺序地向后/向前移动,因此,vector容器插入/删除操作效率并不高。插入/删除位置越靠前,执行所需时间就越多,但在vector容器尾部插入/删除元素的效率比较高。

容器的使用,首先要记住它是一个类模板,也就是说在STL的源代码中,应该有以下类似的代码template<typename T>class vector{};

  1. 创建vector容器
    • 要指定元素类型,还要匹配构造函数
      1
      2
      vector<int> v1; //定义一个整数的动态数组,大小未定。
      vector<string> v2(5);//定义一个大小为5的字符串数组。
  • vector对象在定义后所有元素都会被初始化,如果是基本数据类型的容器,则都会被初始化为0;如果是其他类型容器,则由类的默认构造函数初始化。
1
2
3
4
5
vector<int> v3(10,1);//定一个整数动态数组,包含10个1.
vector<int> v4{1,2,3};//列表初始化。
vector<string> v5 = {"abc","efg","xyz"};//有没有=均可。
vector<int> v6(v4); //等价于vector<int> v6 = v4;拷贝构造函数
vector<Enemy*>allEnemies; //父类指针数组,所有敌人。
  • 容量和个数
1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<vector>
using namespace std;

int main(){
vector<int> vi;
cout<<vi.capacity()<<","<<vi.size()<<endl; //0,0
vi.push_back(1);
cout<<vi.capacity()<<","<<vi.size()<<endl; //1,1
vi.pop_back();
cout<<vi.capacity()<<","<<vi.size()<<endl; //1,0
}
  • 访问容器中的元素

    • vector重载了下标运算符[],用来对元素进行读/写。
      1
      2
      vector<int> vi = {1,2,3,4,5,6};
      for(int i=0;i<6;++i) cout<<vi[i]<<" "<<endl;
    • 同样会有下标越界的问题。
    • vector还提供了at函数vi.at(1)等价于vi[1];
  • 获取头部和尾部

1
2
vector<int> vi{1,2,3,4,5,6};
cout<<vi.front()<<","<<vi.back()<<endl;
  • 从尾部插入和删除元素
1
2
3
4
5
6
vector<int> vi;
for(int i=0;i<5;++i)vi.push_back(i+1); //尾部插入1……5
for(int x:vi)cout<<x<<" "; //输出:1 2 3 4 5
cout<<endl;
vi.pop_back(); //删除尾部的5
for(int x:vi)cout<<x<<" "; //输出:1 2 3 4

迭代器的基本概念

  • 迭代器:迭代的工具
  • 数据结构
    • 连续
      • for(int i=0;i!=size;++i)a[i]……
      • for(int *p=a;p!=a+size;++p)*p……
    • 不连续
      • for(Node* p=head;p!=nullptr;p=p->next)p->data……
  • STL使用迭代器统一了循环变量和指针,来表示容器中元素的位置,并提供了对外访问的接口。
1
2
3
vector<int> vi;
vector<int> ::iterator itr; //不同的容器,迭代器实现的方式不一样。
for(itr = vi.begin();itr!=itr.end();++itr)*itr……;
  • 循环变量或指针
    • 定义
    • 初始值
    • 结束条件
    • 指向下一个元素
    • 操作元素

迭代器的基本运算

  • 通常,我们把迭代器对象就称之为迭代器。

  • 迭代器可以执行++、–、与整数相加减的操作

    1
    2
    3
    4
    5
    vector<int> vi{1,2,3,4,5,6,7};
    vector<int>::iterator itr = vi.begin();
    cout<<*itr++<<endl; //1
    cout<<*--itr<<endl; //1
    cout<<*(itr+3)<<endl; //4
  • 原因

    • 指向数组元素的指针、下标,都能够++、–、与整数相加减。
    • 指向双向链表的指针,可以++、–,但是不能一次性跳过几个结点。
  • vector获取迭代器的函数

函数 含义
begin() 首元素的位置
end() 最后一个元素的下一个位置
rbegin() 返回容器结束位置作为起始位置的反向迭代器
rend() 返回反向迭代的最后一个元素之后的位置的反向迭代器
cbegin() 返回容器中起始位置的常量迭代器,不能修改迭代器指向的内容
cend() 返回迭代器的结束位置的常量迭代器
crbegin() 返回容器结束位置作为起始位置的迭代器
crend() 返回第一个元素之前位置的常量迭代器
  • 使用迭代器遍历容器
    1
    2
    3
    4
    5
    容器类型<元素类型>container;  //  定义容器container
    ……; //各种操作,比较常见的是container.push_back(……);
    容器类型<元素类型>::iterator itr; //定义**某容器**的迭代器
    for(itr=container.begin();itr!=container.end();++itr)
    *itr……; //迭代器重载了取值运算符*,使用*itr表示元素
1
2
3
4
list<int> li;
for(int i=0;i<10;++i)li.push_back(rand()%100);
list<int>::iterator itr = li.begin();
for(;itr!=li.end();++itr)cout<<*itr<<" "<<endl;
1
2
3
4
5
6
7
8
vector<int> vi{21,32,3,54,25};
vector<int>::iterator itr = vi.begin();
for(;itr!=vi.end();++itr)cout<<*itr<<" "<<endl;
for(int i=0;i!=vi.size();++i)cout<<vi[i]<<" "<<endl;
//取出首元素和尾元素
cout<<*vi.begin()<<","<<*vi.end(); //错误
cout<<*vi.begin()<<","<<*prev(vi.end());
sort(vi.begin(),vi.end()); //排序
  • vector的赋值函数

    • vector重载了assign(),用于完成赋值操作
      • 将n个elem赋值给vi,原有数据会被覆盖
        • vector<int>vi; vi.assign(n,elem);
      • 将[begin,end)的元素赋值给容器
        • vector<int>vi; vi.assign(begin,end);
        • int arr[]={1,2,3,4,5}; vi.assign(arr,arr+3);
  • 在STL的函数语法中,往往用begin和end表示一个区间

  • [begin,end)指左闭右开区间

  • 指向数组的指针,也可以当做迭代器。

    1
    2
    3
    4
    //STL中算法的头文件
    #include<algorithm>
    int arr[4]={……};
    sort{arr,arr+4};
  • 迭代器的主要功能

    • 指定容器中元素的范围
      1
      2
      3
      4
      vector<int> vi{32,23,44,9};
      list<int>li;
      li.assign(vi.begin(),vi.end());
      sort(vi.begin(),vi.end()); //li.sort();正确,sort(li.begin(),li.end());错误
    • 遍历容器
    • 对元素进行读写
      1
      2
      list<int>::letrator itr = li.begin();
      for(;itr!=li.end();++itr)*itr*=2;

list

  • list容器以双向链表形式实现,list容器通过指针将前面的元素和后边的元素链接到一起
  • 同vector相比,list容器只能通过迭代器访问元素,不能通过索引方式访问元素。
  • 同为序列容器,list容器的接口大部分与vector都相同,所以读者学习起来也比较容易。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//创建list容器对象
list<T>lt; //创建空list容器,元素类型为T
list<T>lt(n); //创建list容器,大小为n
list<T>lt(n,elem);//包含n个elem
list<T>lt(begin,end);//用[begin,end)区间的元素初始化
list<T>lt(lt1); //使用lt1进行初始化
//赋值
lt.assign(n.elem); //将n和elem赋值给lt
lt.assign(begin,end);//将[begin,end)区间的元素赋值给lt
//元素访问
list<int>li{1,2,3,4,5};
cout<<li.front()<<","<<li.back()<<endl; //1,5
list<int>::iterator itr = li.begin();
for(;itr!=li.end();++itr)*itr *= 2;
for(itr=li.begin();itr!=li.end();++itr) //2 4 6 8 10
cout<<*itr<<" ";
  • list容器提供了以下四种迭代器,以及获取这些迭代器的成员函数。
    • iteratorconst_iteratorreverse_iteratorconst_reverse_iterator
1
2
3
4
5
6
7
8
9
10
11
12
//插入元素
lt.push_back(); //在尾部插入元素
lt.push_front(); //在头部插入元素
lt.insert(pos,elem); //在pos位置插入元素elem
lt.insert(pos,n,elem); //在pos位置插入n个元素elem
lt.insert(pos,begin,end); //在pos位置插入[begin,end)区间的值作为元素
//删除元素
lt.pop_back(); //从尾部删除元素
lt.pop_front(); //从头部删除元素
lt.erase(pos); //从中间删除元素
lt.erase(begin,end); //删除[begin,end)区间的元素
lt.remove(elem); //从容器中删除所有与elem匹配的元素
  • 小例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ostream& operator<<(ostream& out, const list<int>& l){
for(int x:l) out << x << " ";
return out;
}

int main(){
list<int>lt;
for(int i = 0; i < 10; ++i) lt.push_back(i+1);
cout << lt << endl; //1 2 3 4 5 6 7 8 9 10
lt.push_front(5);
cout << lt << endl; //5 1 2 3 4 5 6 7 8 9 10
lt.remove(5);
cout << lt << endl; //1 2 3 4 6 7 8 9 10
}
1
2
3
4
5
6
7
8
9
10
11
//这段代码会生成无效迭代器,类似野指针,会导致程序崩溃
int main(){
list<int>lt;
for(int i = 0;i<10;++i)lt.push_back(i+1);
cout << lt << endl; //1 2 3 4 5 6 7 8 9 10
list<int>::iterator itr;
for(itr = lt.begin();itr!=lt.end();++itr)
if((*itr)%2==0)
lt.erase(itr); //无效迭代器,会导致崩溃
cout<<lt<<endl; //1 3 5 7 9
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//解决上一个问题的稍微复杂的形式,用了中间变量
int main(){
list<int>lt;
for(int i=0;i<10;++i) lt.push_back(i+1);
cout<<lt<<endl; //1 2 3 4 5 6 7 8 9 10
list<int>::iterator itr,temp;
for(itr = lt.begin();itr!=lt.end();/*++itr*/)
if((*itr)%2==0){
//temp指向要删除的元素,itr指向下一个
temp = itr++;
lt.erase(temp);
}
else
++itr;
cout<<lt<<endl; //1 3 5 7 9
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//解决上一个问题的标准形式
int main(){
list<int>lt;
for(int i=0;i<10;++i) lt.push_back(i+1);
cout<<lt<<endl; //1 2 3 4 5 6 7 8 9 10
list<int>::iterator itr;
for(itr = lt.begin();itr!=lt.end();/*++itr*/)
if((*itr)%2==0){
//temp指向要删除的元素,让itr指向下一个,erase会返回下一个位置
itr = lt.erase(itr);
}
else
++itr;
cout<<lt<<endl; //1 3 5 7 9
}

综合项目(多态、文件、容器)

文件流和容器

  • 从文件中读入数据放进容器的一般步骤
    1. 打开文件;
    2. while(文件不为空)
      1. 文件对象>>容器;
    3. 关闭文件
  • 定义listli;把in.txt中的数据导入li中并遍历输出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//main.cpp
#include<iostream>
#include<list>
#include<fstream>
using namespace std;

void operator>>(ifstream& file, list<int>& l){
int temp;
file>>temp;
l.push_back(temp);
}
int main(){
ifstream ifs;
ifs.open("in.txt",ios::in); //打开文件
if(!ifs){
cout<<"文件打开失败"<<endl;
exit(0);
}

list<int>li;
while(!ifs.eof()) //文件不为空
ifs>>li; //文件对象中的数据>>容器
ifs.close(); //关闭文件

for(int x : li)cout<<x<<" ";
cout<<endl;
}

项目:所有学生

  • 功能需求:
    1. 所有学生Student都有名字name,都要学习Study;
    2. 所有同学都要上体育课PE,但是男生打篮球play basketball,女生打排球play volleyball
    3. 放学以后,男生打游戏PlayGame,女生去购物Shopping
  • 基本数据来自于student.txt
  • 容器采用list或vector
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//main.cpp

#include<iostream>
#include<list>
#include<fstream>
#include"Student.h"

void operator>>(istream& in,list<student*>& allStus){
string type,name;
in>>type>>name;
if(type=="男生")
allStus.push_back(new Boy(name));
else
allStus.push_back(new Girl(name));
}

int main(){
list<Student*> allStudents;
ifstream ifs;
ifs.open("student.txt",ios::in);
if(!ifs) exit(0);

while(!ifs.eof())
ifs>>allStudents;
ifs.close();

for(Student* ps:allStudents){
ps->Study();
ps->PE();
//RTTI,运行时类型识别
if(typeid(*ps)==typeid(Boy)){
Boy* pb = dynamic_cast<Boy*>(ps);//向下类型转换
pb->PlayGame();
}
else{
Girl* pg=dynamic_cast<Girl*>(ps);
pg->Shopping();
}
}
}

//Student.h
#pragma once
#include<string>
#include<iostream>
using namespace std;

class Student{
public:
Student(string str);
void Study();
virtual void PE()=0;
protected:
string name;
};

class Boy:public Student{
public:
Boy(string str);
void PE();
void PlayGame();
};

class Girl:public Student{
public:
Girl(string name);
void PE();
void Shopping();
};

//Student.cpp
Student::Student(string str):name(str){}
void Student::Study(){
cout<<name<<"在学习..."<<endl;
}
Boy::Boy(string str):Student(str){}
void Boy::PE(){
cout<<name<<"在打篮球..."<<endl;
}
void PlayGame(){
cout<<name<<"在打游戏..."<<endl;
}
Girl::Girl(string str):Student(str){}
void Girl::PE(){
cout<<name<<"在打排球..."<<endl;
void Girl::Shopping(){
cout<<name<<"在购物..."<<endl;

第10章 设计模式

策略模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

//Strategy.h
#pragma once
#include<string>
using namespace std;
class Strategy{
public:
Strategy(string str);
operator string ();
virtual float GetMoney(float price, float count) = 0;
protected:
string describe;
};

class Discount : public Strategy{
public:
Discount(string str,float d);
float GetMoney(float price, float count);

private:
float discount;
};

class GiveGoods : public Strategy{
public:
GiveGoods(string str,float m,float give);
float GetMoney(float price, float count);
private:
float total;
float give;
};

class GiveMoney : public Strategy{

};

1
2
3
4
5
6
7
8
9
10
11
//Strategy.cpp
Strategy::Strategy(string str): describe(str){}
Strategy::operator string(){
return describe;
}

Discount::Discount(string str,flaot d):Strategy(str),discount(d){}
float Discount::GetMoney(float price, float count){return discount* price*count;}

GiveGoods::(string str,float m,float give): Strategy(str),total(m+give),give(g){}
float GiveGoods::GetMoney(float price, float count){return price * (count - give * (int) (count/total));}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include<iostream>
#include<list>
#include<string>
#include "Strategy.h"
using namespace std;

class Goods{
friend ostream& operator<<(ostream& out,const Goods& g);
public:
Goods(string s,float p,float c,Strategy* ss = nullptr);
float GetMoney() const;
private:
string name;
float price;
float count;
Strategy* s;
};

int main(){
Strategy* s1 = new Discount("打八折",0.8);
Strategy* s2 = new GiveGoods("买二送一",2,1);
list<Goods*> cart;
cart.push_back(new Goods("铅笔",1,10));
cart.push_back(new Goods("橡皮",2,5,s1));
cart.push_back(new Goods("本子",5,4,s2));

float money = 0.0f;
for(Goods* pg:cart){
money +=pg->GetMoney();
cout<<*pg<<endl;
}
cout<<"总金额:"<<money<<endl;
}

Goods::Goods(string s, float p, float c,float d,Strategy* ss): name(s),price(p),count(c),discount(d){}

float Goods::GetMoney()const{
if(s)
return s->GetMoney(price,count);
return price*count;}

ostream& operator<<(ostream& out,const Goods& g){
out<<g.name<<":"<<g.price<<"*"<<g.count;
if(g.s)
out<<"("<<(string)(*(g.s))<<")";
cout<<"="<<g.GetMoney();
return out;
}

第11章 复习

初识C++

一些基本知识

  • 集成开发环境
    • 编辑器
    • 编译器
    • 连接器
    • 调试器
  • 程序的运行从main函数开始而开始,结束而结束
  • 编译器是从上到下逐行编译
  • 在语法描述中,[]表示可选的
  • C++语言集结构化编程面向对象编程泛型编程函数式编程于一身,特别适合大型应用程序开发。
  • C++的头文件是不带.h扩展名的
  • C++的所有关键字都是小写的
  • C++11,空指针nullstr

new/delete内存管理

  • 栈:局部变量位于栈中,每个函数每一次运行,都会自动的分配/释放栈;形参(引用类型除外)也是局部变量
  • 堆:每个进程只有一个堆。只能手动分配和释放。只能通过指针指向堆,不能通过变量名使用堆。
  • new/delete和malloc/free不能混用
  • new[]newdelete[]delete是不同的运算符
  • new[]只要用于创建动态数组

例如

1
2
3
4
int* p = new int[变量];
class Demo{……};
Demo* pd = new Demo[变量]; //此时调用Demo类的无参构造函数
int arr[3] = {1,2,3}; // 静态数组的定义和初始化

参数默认值

  • 只能按照从右往左的顺序给出默认值。
  • 如果既有声明、又有实现,则只需要声明中给出默认值。
  • 特别地,成员函数的类外实现不应该有参数默认值。

重载

  • 所谓重载函数就是指在同一作用域内函数名相同参数列表不同的函数。
  • 目的:使得代码简洁
  • 参数列表不同的含义
    • 类型不同
    • 个数不同
    • 类型个数都不同
    • 不包括变量名不同
  • 不以函数类型作为重载的依据
  • 当使用具有默认参数的函数重载时,需注意防止调用的二义性

内联函数

之前是为了提高效率,后来被编译器替代了,这里老师没讲,就说知道关键字就行了。

  • inline

引用

  • 概念:别名。
  • 本质:指针
  • 用途:函数参数、函数返回值(可以让函数作为左值)、成员变量、父类引用指向子类对象。
  • 一个函数内部不会给变量定义相同类型的引用。

类与对象(封装)

面向对象的三大特征

  • 封装,隐藏内部实现
  • 继承,复用现有代码
  • 多态,改写对象行为

成员

  • 成员可以是自身类型的指针、自身类型的引用、其他类型的对象(不能循环定义),但不能是自身类型的对象(造成错误的递归)。
  • 三类成员函数
    • 构造/析构函数
    • Get/Set函数
    • 其他功性能函数
  • 针对某一个成员变量,往往有一对Set/Get函数。
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    class Demo{
    public:
    void Set XXX(类型 形参){
    参数有效性检查;
    XXX = 形参;
    }
    private:
    类型 XXX;
    };
  • 成员访问运算符
    • 对象.成员变量
    • 对象.成员函数(参数);
    • 指针->成员变量
    • 指针->成员函数(参数); //前提是指针指向对象

访问权限

  • private
    • 默认访问权限
    • 个人财产。类内访问,子类、类外均不能访问。
    • 如果没有派生类,则成员变量一般设置为private,然后设置公有的set/get函数。
  • protected
    • 家族财产。类内访问,子类可以访问,类外不能访问。
    • 一般基类的成员变量设为protected,方便派生类访问
  • public
    • 公共财产。类内、派生类、类外均可访问
  • C++中的struct
    • 其默认访问权限是public。

构造函数与析构函数

  • 每创建一个对象,就必然要调用一次构造函数。

  • 注意:Demo* pd[4];//指针数组只是定义了四个指针,还没有创建对象

  • 作用

    • 构造函数用来创建和初始化对象,析构函数用于释放对象
  • 四个特点

构造函数 析构函数
1 与类同名 与类同名,前面加~
2 不能有类型,void、return都不要
3 可以带参数,能够重载 没有参数,不能重载
4 一般为公有函数 基类一般采用虚析构函数

无参构造函数

  • 默认构造函数,是无参构造函数
  • 这四行代码都调用无参构造函数
1
2
3
A x;
A* pa = new A;
A* pArr = new A[size];
  • 通过new[]创建对象的动态数组时,只能调用该类的无参构造函数。

  • 通过参数的默认值,也能达到无参构造函数的作用。

  • 默认函数

    • 一个空类class Demo{};
    • 如果程序员没有提供构造函数,则编译器会自动提供默认构造函数。
    • 类似的还有:默认析构函数、默认构造函数、默认赋值运算符
    • 默认拷贝构造函数、默认赋值运算符按照“按位复制”,如果类中又封装了指针类型的成员变量,则会造成指针悬挂。也就是新的对象的指针变量和上一个对象的指针变量指向的可能是同一段内存,这样其中一个指针释放了内存,另一个对象也就用不了了。是一种浅拷贝

常成员函数,不修改对象的数据成员

1
2
3
4
class Demo{
void fun() const; //常成员函数
};
void Demo::fun() const{……;}
  • 常对象只能调用常函数

静态成员函数

  • 关键字:static
  • 普通成员函数,编译器会为其加上默认的this指针,但是静态成员函数除外。参数列表中没有默认的this指针
  • 属于类,不属于对象。也就是说,该类的所有对象拥有同一个静态成员。静态成员类似于一个类范围内的全局变量。
  • 既可以通过对象调用,也可以通过类调用类名::静态成员
  • 静态成员变量必须类外初始化
  • 典型应用:单例模式

友元friend

  • 友元函数
    • 声明全局函数为友元,则该函数可以访问所有成员变量。
    • 开后门,破坏了封装。
    • 友元函数不是成员函数,和访问权限没有关系。
  • 友元类
    • 没讲

运算符重载

  • 运算符的本质是函数重载
    • 主要目的是代码简洁,而不是代码复用;
    • 但是不能说运算符就是函数(算数运算是铜鼓CPU的指令直接实现的)。

两种重载形式

  • 成员函数
  • 全局函数,**往往(不是必须)**声明为友元。

重载为成员函数

  • 左操作数必须是自身类型
  • =、)、[]、->,这四个符号只能重载为成员函数

重载为全局函数

  • 流输出运算符只能重载为全局函数
1
2
3
4
5
6
7
8
class Demo{
friend ostream& <<(ostream& out, const Demo& d);
};

ostream& operator<<(ostream& o,const Demo& d){
……;
return o;
}
  • 三处引用的作用

    • 两处ostream&是为了确保设备的唯一性连续使用
    • 右操作数采用常引用,是为了避免形参到实参拷贝,避免修改实参。
  • 流入运算符只能重载为全局函数

1
istream& operator>>(istream& in, 引用类型)//需要写数据,不能是常引用

继承与派生

  • 基类和派生类,父类和子类
  • C++支持单继承(只有一个基类)和多继承(多个基类)
  • UML类图中,由派生类指向基类(由下而上)

继承方式

  • 针对基类的公有和保护成员,继承方式决定其最高访问权限
  • 针对基类的私有和不可访问成员(继承自基类的基类),都是不可访问成员
  • 默认的继承方式是私有继承,是最常见的继承方式是公有继承。

派生类对象

  • 派生类对象拥有基类的所有成员(但是不一定能够使用)
  • 构造函数的调用顺序:基类->成员对象->派生类
  • 析构函数的调用顺序:派生类->成员对象->基类

多态与虚函数

虚函数

  • 关键字:virtual
  • 实现虚函数的核心数据结构是虚函数表

成员函数,区分三个概念:重载、隐藏、覆盖

  • 同一类中:重载
  • 分别在基类、派生类中
    • 同时满足
      1. 原型相同
      2. virtual,即为覆盖
    • 否则(两个条件有一个不满足),即为隐藏

同名函数的调用原则

  • 隐藏的情况下,通过指针调用函数,取决于左侧变量的类型
  • 覆盖的情况下,通过指针调用函数,取决于对象的类型

多态的概念

多态是一种:调用同名函数却因上下文不同会有不同实现的一种机制。

多态是指:不同的对象调用同名函数,会有不同的实现

  • 静态多态,通过重载编译阶段完成
  • 动态多态,通过继承和虚函数运行阶段完成。

纯虚函数与抽象类

  • 虚函数没有函数体(=0)称之为纯虚函数
  • 拥有纯虚函数的类,称之为抽象类
  • 抽象类不能实例化对象

RTTI,运行时类型识别

  • typeid
  • dynamic_cast<目标类型>(表达式);

异常处理

  • try…catch…throw
  • throw用于(通常在子函数中)抛出异常
  • try用于检测异常,把可能出现异常的语句放在try块中(只能有一个)
  • 多个catch块依次对异常按照类型进行匹配,用于捕获处理异常。

IO流

四个预定义流对象

  • 包括cin、cout、cerr和clog
  • >>提取运算符、<<插入运算符

文件读写

  • 文本文件,既可以通过>>、<<进行读写,也可以通过成员函数进行读写。
  • 二进制文件,只能通过成员函数进行读写。

典型应用,把文本文件中的数据读入到某一个STL的容器中。

1
2
3
4
5
6
7
istream& operator>>(istream& in, 容器引用){...;return in}
int main(){
ifstream ifs;
ifs.open("文件名",ios::in);
if(!ifs)exit(0);
while(!ifs.eof()) ifs>>容器
}

模板编程

  • 模板编程也称为泛型编程。
  • 模板编程的主要目的是代码复用

函数模板实例化为函数

1
temple<typename>T

类模板实例化为类

必须显式实例化

STL

基本概念

  • Standard Template Library,标准模板库
  • 三大核心组件:容器、迭代器、算法
  • 六大组件,再加上:适配器、仿函数(函数对象)、分配器
  • 内部实现
模板 结构
vector 动态数组
list 双向链表
map 红黑树

vector

  • 遍历vector的方法:迭代器、下标、基于范围的for循环、for each函数
  • 连续的动态数组,可以在尾部快速插入和删除

list

  • 遍历list的方法:迭代器、基于范围的for循环、for each函数
  • 双向链表,可以在任意位置插入和删除

迭代器

  • 定义迭代器时,必须要指定容器类型和元素类型
  • 在调用一些特殊的迭代器时,需要包含。比如说,插入迭代器、IO流迭代器

算法

  • 需要包含
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <list>
#include <algorithm>
using namespace std;

int main(){
list<int>li; //整数链表
//不使用循环。而是通过STL::algorithm实现插入10个随机数,遍历输出
generate_n(back_inserter(li),
10,
[](){return rand()%100;}//0~99的随机数
);
copy(li.begin(),li.end(),ostream_iterator<int>(cout," "));
}

设计模式

面对对象程序设计的思想

应用程序是对现实“花花世界”的仿真,主要特征是:种类很多、数量很多。

  • 在软件开发过程中唯一不变的是变化

  • 在基类指针指向派生类对象时,基类指针代表抽象稳定的,派生类对象代表具体变化的。只有针对稳定的抽象进行编程,才能达到“以不变应万变”的效果。

  • 三大设计原则:

    1. 封装变化点
    2. 针对抽象进行编程
    3. 优先使用组合
    • 继承被称为“is-a”关系,组合被称为“has-a”关系
    • 继承和组合均能实现代码复用,优先使用组合。
    • 针对抽象(稳定)的接口进行编程,才能做到以不变应万变。
  • 开闭原则:对扩展开放,对修改关闭。(五大设计原则之一)

基本概念

  • 最早提出这个概念的人是,建筑设计领域的亚历山大·克里斯托弗
  • 三种设计模式:创造型模式(如:单例模式)、行为型模式(如:策略模式)、结构型模式。