目录
- 指针与数组
- 指针数组
- 指针
- 指针作为函数参数
- 指针类型的函数
- 指向函数的指针
- 对象指针
- 动态分配和释放内存
- 申请和释放动态数组
- 智能指针
- VECTOR对象
- image.pngimage.png
- 对象的复制
- 浅层复制和深层复制
- 移动构造
- 字符串
- C风格字符串
- C++中的STRING类
- 继承
- 继承的基本概念和语法
- 继承方式简介及公有继承
- 私有继承和保护继承
- 基类与派生类类型转换
- 派生类的构造函数
- image.pngimage.pngimage.png
- 派生类的复制构造函数
- 派生类的析构函数
- 访问从基类继承的成员
- 虚基类
- 多态性
- 运算符重载
- 双目运算符重载为成员函数
- 单目运算符重载
- 运算符重载为非成员函数
- 虚函数
- 虚析构函数
- 虚表与动态绑定
- 抽象类
- OVERRIDE与FINAL
- C++模板
- 函数模板
- 类模板
- 线性群体
- 数组类
- 链表类
- 链表类模板
指针与数组
指针数组
- 指针数组:数组的元素是指针类型
Point *pa[2];//定义了两个指向Point类型的指针
#include
using namespace std;
int mian(){
int line1[] = {1,0,0};
int line2[] = {0,1,0};
int line3[] = {0,0,1};
int *pLine[3] = {line1,line2,line3};
cout << "Matrix test: " << endl;
for(int i = 0; i < 3; i++){
for(int j = 0; j < 3; j++){
cout << pLine[i][j] << " " << endl;
}
}
return 0;
}
指针
指针作为函数参数
- 为什么用指针做函数参数
- 需要数据双向传递时(引用可以达到此效果)
- 需要传递一组数组,只传首地址运行效率比较高
指针类型的函数
- 函数返回类型时指针类型
存储类型 数据类型 *函数名(){
//函数体语句
}
- 不要将非静态局部地址作为函数返回值(是能够返回给接受变量,但离开函数体以后该地址就失效了)
- 举例:在子函数中定义局部变量后将其地址返回给主函数,就是非法地址
//危险的访问
int main(){
int* function();
int* ptr = function();
*ptr = 5;//危险的访问
return 0;
}
int* function(){
int local = 0;//非静态局部变量作用域和寿命都限于本函数体内
return &local;
}//函数运行结束时,变量local被释放
- 返回的指针要确保在主函数中是有效、合法的地址
- 正确的例子:在子函数中通过动态内存分配new操作取得的内存地址返回给主函数是合法有效的。(new分配的地址离开函数体后不会自动释放,需要使用delete语句才会释放)
指向函数的指针
- 运行中的代码,在内存中也是占有存储空间的
- 指向函数的指针:就是指指针容纳的是函数代码的起始地址
- 函数指针的定义
存储类型 数据类型 (*函数指针名)(参数表);
- 函数指针的典型用途
- 函数指针作为参数传递给一个函数,使得在处理相似事件时可以灵活使用不同方法
- 调用者不关心谁是被调用者
- 函数指针举例
#include
using namespace std;
int compute(int a, int b, int (*func)(int int)){
return func(a,b);
}
int max(int a, int b){
return ((a>b)?a:b);
}
int min(int a, int b){
return ((a> a;
cin >> b;
res = compute(a,b,&max);//这里痛compute(a,b,max)也行,函数名也代表地址
cout << "max is: " << res << endl;
res = compure(a,b,&min);
cout << "min is: " << res << endl;
res = compute(a,b,&sum);
cout << "sum is: " << res << endl;
return 0;
}
- 函数名就是函数地址,所以通过函数指针func调用函数,只需要语句
func(a,b);
对象指针
- 对象指针的定义
类名 *对象指针名;
- 例
Point a(5,10);
Point *ptr;
ptr = &a;
- 通过指针访问对象成员
对象指针名->成员名
//等价于
(*对象指针名).成员名;
- this指针
- point.getx()语句隐含的使用了this指针用于指明调用getx函数的对象
- point.getx()语句隐含的使用了this指针用于指明调用getx函数的对象
- 例子
- 见到完整定义前不能使用细节
- 见到完整定义前不能使用细节
动态分配和释放内存
- 动态申请内存操作符
- 功能:在程序执行期间,申请用于存放T类型对象的内存空间,并依据初始值列表赋以初值
new 类型名T(初始化参数列表);
- 释放内存操作符
- 功能:释放指针P指向的内存,且P必须是new操作的返回值
delete 指针P
- 功能:释放指针P指向的内存,且P必须是new操作的返回值
- 动态分配内存
申请和释放动态数组
- 分配:
new 类型名T[数组长度];
- 释放:p必须是用new分配的数组首地址
delete[] 数组名P;//不写方括号,会仅仅释放数组首元素地址
- 动态创建多维数组
- 语法
new 类型名T[第一维长度][第二维长];
- 语法
- 重点难点
char (*fp)[3];//申请的是指向一维数组的指针(而非指向数组元素的指针)
fp = new char[2][3];//申请一维数组空间返回的是首元素地址,申请二维数组返回的应该是第一个一维数组的地址,指向的是数组
智能指针
- C++11提供的智能指针
VECTOR对象
- C++标准模板库中的类模板
- 封装任何类型的动态数组,自动创建和删除
- 数组下标越界检查
- vector对象的定义
//vector<元素类型> 数组对象名(数组长度)
vector arr(5);
- vector对象的引用
- vector对象名不表示数组的首地址
vector对象名 [下标表达式];
- 获得数组长度
vector对象名.size();
- 基于范围的for循环配合auto举例
#include
#include
using namespace std;
int main(){
vector v = {1,2,3};
for(auto i = v.begin(); i != v.end(); i++){
cout << *i << endl;
}
for(auto e: v)
cout << e << endl;
return 0;
}
对象的复制
浅层复制和深层复制
- 浅层复制:
- 实现对象间数据元素的一一对应复制
- 深层复制
- 当被复制的对象数据成员是指针类型时,不是复制该指针成员本身,而是将指针所指的对象进行复制
- 对象的浅层复制:
- 没有把数组对象一起复制,会出问题
- 没有把数组对象一起复制,会出问题
- 对象的深层复制
移动构造
- 有些复制构造不需要真的发生复制,只需要对象换个地方
- C++11引入移动语义:
- 源对象资源控制权全部交给目标对象
- 复制构造和移动构造的区别
- 移动构造函数:
class_name(class_name &&)
字符串
C风格字符串
- 字符串常量:比如
"program"
- 各个字符连续、顺序存放,每个字符占一个字节,以'\0'结尾,相当于一个隐含创建的字符串常量数组
"program"
出现在表达式中,表示这一char数组的首地址- 首地址可以赋给char常量指针
const char *STRING1 = "program";
- 用字符数组存储字符串
- 例如:
char str[8] = {'p','r','g','r','a','m','\0'}; char str[8] = "program"; char str[] = "program";
C++中的STRING类
- STRING类相当于字符数组,且具有更好用的功能
- string常用操作
- 使用cin输入字符串时,以空格为分隔符
- 希望整行输入字符串 ,包括空格
- 例如:
getline(cin,s2);
- 输入字符串时,可以设置其他分隔符作为结束标志。
getline(cin,s2,',');//以逗号为结束标志
- 例如:
继承
继承的基本概念和语法
- 继承和派生时同一过程从不同角度看
- 保持已有类的特性而构造新类的过程称为继承
- 在已有类的基础上新增自己的特性而产生新类的过程称为派生
- 继承的目的
- 实现设计与代码的重用
- 派生的目的:
- 当新问题出现,原有程序无法解决时,需要对原有程序进行改进
- 单进程时派生类的定义
class 派生类名:继承方式 基类名
{
成员声明;//除了继承基类以外,派生类自己新增的成员
}
//例子
class Derived:public Base
{
public:
Derived();
~Derived();
}
- 多继承时派生类的定义
class 派生类名:继承方式1 基类名1,继承方式2 基类名2,...
{
成员声明;
}
- 派生类的构成
- 吸收基类成员(原封不动的部分)
- 改造基类成员
- 添加新的成员、
- 吸收基类成员
- 默认下派生类包含全部基类中除去构造和析构函数以外的 所有成员
- C++规定可以用using语句继承基类构造函数
- 改造基类成员
- 可以在派生类中声明一个和基类成员同名的新成员,就把原来成员覆盖了
- 添加新的成员
- 定义新的成员
继承方式简介及公有继承
- 三种继承方式
- 公有继承
- 私有继承
- 保护继承
- 公有继承
- 继承访问控制:
- 基类的public和protected:访问属性在派生类中保持不变
- 基类的private成员:不可直接访问
- 访问权限:
- 派生类中的成员函数:可以直接访问基类中的public和protected成员,但不能直接访问基类的private成员
- 通过派生类对象:只能访问public成员
- 继承访问控制:
私有继承和保护继承
- 保护继承
- 保护成员的特点是派生类的成员函数可以直接访问它
- 保护成员的特点是派生类的成员函数可以直接访问它
基类与派生类类型转换
- 类型转换
- 下面程序进行了隐含的类型转换,只保留了base1类中的成员
派生类的构造函数
- 问题:构造派生类的时候,继承过来的成员怎么初始化
- 默认情况下:
- 基类构造函数不被继承
- 派生类需要定义自己的构造函数:需要负责向基类初始化功能传递参数
- 可以用using语句继承基类的构造函数
- 只能初始化基类继承来的成员
using B::B;
- 只能初始化基类继承来的成员
- 不继承基类的构造函数
- 派生类新增的成员:派生类定义构造函数初始化
- 继承来的成员:自动调用基类构造函数初始化
- 派生类构造函数需要给基类的构造函数传递参数
- 单继承时构造函数的语法定义
派生类名::派生类名(基类所需形参,本类成员所需形参):基类名(参数表),本类成员初始化列表
{
//其他初始化
}
- 多继承时构造函数的定义语法
派生类名::派生类名(参数表):基类名1(基类1初始化参数表),基类名2(基类2初始化参数表),...,基类名n(基类n初始化参数表),本类成员初始化列表
{
//其他初始化
};
- 多继承且有对象成员时派生的构造函数定义语法
派生类名::派生类名(形参表):基类名1(参数表),基类名2(参数表),...,基类名n(参数表),本类成员包括对象成员初始化列表
{
//其他初始化
};
- 构造函数的执行顺序
- 举例
- 调用顺序Base2,Base1,Base3,member1,menber2,member3
派生类的复制构造函数
- 若派生类没有声明复制构造函数
- 编译器会在需要时生成隐含的复制构造函数
- 先调用基类的复制构造函数
- 在为派生类新增的成员执行复制
- 若派生类定义了复制构造函数
- 复制构造函数只能接收一个参数,既用来初始化派生类定义的成员,也将被传递给基类的复制构造函数
C::C(const C &c1):B(c1){};
派生类的析构函数
- 析构函数和构造函数一样不被继承,派生类如果需要,要自行声明析构函数
- 不需要显式地调用基类的析构函数,系统会隐式调用
- 先执行派生类析构函数的函数体,在调用基类的析构函数(与构造相反)
访问从基类继承的成员
- 当派生类与基类中有相同成员时
- 例子
- 二义性问题
虚基类
- 需要解决的问题:
- 当派生类从多个基类派生,而这些基类又有共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性
- 虚基类声明
- 以virtual说明基类继承方式
class B1:virtual public B;
- 以virtual说明基类继承方式
- 作用:
- 主要用来解决多继承时可能发生的对同一基类继承多次产生的二义性问题
- 为最远的派生类提供唯一的基类成员,而不重复产生多次复制
- 注意:
- 虚基类带来麻烦:派生类的构造函数怎么写
- 如果是虚继承,需要在最远派生类中给虚继承类传递参数
- 如果是虚继承,需要在最远派生类中给虚继承类传递参数
多态性
- 多态性:操作接口具有表现多种不同形态的能力
- 多态性通过绑定实现
- 绑定
- 编译时的绑定:早绑定
- 运行时的绑定:晚绑定运行时才将标识符和相应代码结合
- 函数重载:是一种编译时的绑定
运算符重载
- C++几乎可以重载全部运算符,而且只能重载C++已有的
- 不能重载的运算符:"."、".*"、"::"、"?:"
- 重载之后运算符的优先级和结合性都不会改变
- 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造
- 例如:使复数类对象可以用“+”运算符实现加法
- 使时钟类对象可以用“++”运算符实现时间增加1秒
双目运算符重载为成员函数
- 重载为类成员函数的定义形式
函数类型 operator 运算符(形参)
{
......
}
- 双目运算符重载规则
- 完成复数加复数例题(但实数加复数不能通过成员函数实现)
单目运算符重载
- 前置单目运算符,例子
- 后置单目运算符
- 怎么区分:函数名一致,通过参数表
- 怎么区分:函数名一致,通过参数表
- 例子
运算符重载为非成员函数
- 如果双目运算符左操作数不是类的对象;或者左操作数不是自己定义的类,那么需要将运算符重载为类外的全局函数
- 例子
虚函数
- 虚函数就是实现动态绑定的函数
- 例子:希望实现通用的display函数,却在使用fun()时发生了转换
- 利用virtual关键字,使运行时决定类型
- 函数实现写在类体里面,就是内联函数,而内联函数就是编译阶段处理的
- 所以virtual都要在类外部实现函数体
- 认识虚函数
- 用virtual说明的函数
- 虚函数是实现运行时多态性的基础
- 是动态绑定的函数
- 虚函数必须是非静态成员函数(即属于对象,而非属于整个类),经过派生之后,就可以实现运行过程的多态性
- 有了虚函数,就可以在派生类中重写,实现对基类中成员函数的覆盖
虚析构函数
虚表与动态绑定
- 虚函数为什么能实现运行时的动态绑定,谁帮助我们选择函数体
- 其实还是编译器预先做好了准备,这就是虚表
- 虚表
- 每个多态类都有一个虚表
- 虚表中有当前类的各个虚函数的入口地址
- 每个对象中都一个指向当前类的虚表的指针(虚指针)
- 动态绑定的实现
- 构造函数中为对象的虚指针赋值
- 通过多态类型指针或者引用调用成员函数时,通过虚指针找到虚表,静儿找到所调用虚函数入口地址
- 通过该入口地址调用虚函数
- 例子
抽象类
- 抽象类:用来描述较为抽象的概念,带有纯虚函数的类就是抽象类,抽象类不能实例化,但是能够规定家族类的对外接口
- 抽象类不能实例化,即不能定义对象
- 纯虚函数
- 由于在基类中定义的信息不够具体,暂时无法是实现
- 为了规定家族统一的行为和接口,需要在高层基类中定义这么一个函数
- 纯虚函数是一个在基类中声明的虚函数,它在基类中没有定义具体操作内容,要求各派生类根据实际需要定义自己的版本,声明格式为
virtual 函数类型 函数名(参数表) = 0;//=0表示没有函数体
- 抽象类的语法
- 抽象类作用
- 例子
OVERRIDE与FINAL
- Override
- 多态行为基础:基类声明虚函数,本意想在派生类中覆盖基类虚函数,但函数签名不一致
- 多态行为基础:基类声明虚函数,本意想在派生类中覆盖基类虚函数,但函数签名不一致
- 疏忽造成没有能实现多态性的例子
- derived中f1不用写virtual关键字也是虚函数
- 但是,这里函数签名不一致,没有完成覆盖
- 怎么解决上面问题,显式函数覆盖功能
- 有时候定义了一个类,它非常关键,以至于你不希望它被继承(修改)
- 有时候是个别成员函数,他的算法你希望固定
C++模板
- 模板允许我们将处理问题的逻辑将不同的数据类型中抽离出来,形成容器和算法
- 指定模板和数据类型,就能生成适合这种数据类型的数据容器以及算法函数
函数模板
- 问题:当我们写函数时,主题逻辑都是一样的,但我们需要处理不同的数据类型时,需要写不同的重载函数
- 例子:求绝对值函数的模板
- 函数模板定义语法
template <模板参数表>
函数定义
- 例子
- class关键字作用和typename一样
- class关键字作用和typename一样
类模板
- 使用类模板使用户可以为类声明一种模式,使得类中的某些数据成员、某些成员函数的参数、默写成员函数的返回值,能取任意类型
- 类模板声明
template<模板参数表>
class类名
{类成员声明}
//如果要在类模板以外定义其成员函数,采用以下形式
template<模板参数表>
类型名 类名<模板参数标识符列表>::函数名(参数表)
{
//函数体
}
- 用类模板声明对象
类名<类型名> 对象名;
- 例子
线性群体
- 群体:相同类型的一组数据的结合体
- 线性群体:元素按照位置排列有序
- 非线性群体:不使用位置顺序来标识元素
- 线性群体:元素按照位置排列有序
数组类
- 直接访问的线性群体-数组类(直接指的是通过下标可以直接访问)
- 静态数组:具有固定元素个数的群体,其中的元素可以通过下标直接访问
- 缺点: 大小在编译时已经确定,在运行时无法修改
- 动态数组:位置连续,任意数量的相同类型元素组成
- 有点:元素个数可以在运行时改变
- 动态数组类模板
链表类
- 数组不管是动态静态的,插入时都要频繁移动
- 链表是一种动态数据结构,可以用来表示顺序访问的线性群体
- 顺序访问的线性群体:不能一下就指向序号指定的元素
- 单链表的结点类模板
链表类模板
- 链表类的基本操作
- 生成链表
- 插入结点
- 查找结点
- 删除结点
- 遍历链表
- 清空链表
- 具体代码略
- 例子