IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 4. c++进阶 -> 正文阅读

[C++知识库]4. c++进阶

类和对象

基本概念

1. 类的封装性

有三种权限:private, protected, public
只有public能够被外部访问,默认为private

class Data{
public:
    int a;
private:
    int c;
}

声明类时不要初始化,因为此时没有分配空间
类相当于一个类型,需要实例化之后再使用

2. 示例

#include <iostream>
#include "fun.h"
using namespace std;

class Cube{
private:
    int c;//长
    int k;//宽
    int g;//高
public:
    void cubeInit(int cl, int kl, int gl){
        c = cl;//注意不能同名,同名需要用this
        k = kl;
        g = gl;
    }
    int getS(void){
        return (c*k+c*g+k*g)*2;//求面积
    }
    int getV(void){
        return c*k*g;//获取体积
    }
};

int main(void){
    Cube ob;
    ob.cubeInit(10,20,30);
    cout<<ob.getS()<<endl;
    cout<<ob.getV()<<endl;
    return 0;
}

比较两个立方体

bool cubeCompare(Cube &ob1, Cube &ob2){
    //if(ob1.c == ob2.c) //报错,私有变量不允许外部访问
    //需要在类内部实现一个返回长的函数
    if(ob1.getC() == ob2.getC()){}
}

3. 成员函数在类外实现

内部声明,外部实现

class Data{
private:
    int mA;
public:
    void setA(int a);
    int getA(void);
};

void Data::setA(int a){//加上作用域修饰
    mA = a;
}

4. 类在其他源文件中实现

data.h(C++ Class)

#ifndef DATA_H
#define DATA_H

class Data{
private:
    int mA;
public:
    void setA(int a);
    int getA(void);
};

#endif // DATA_H

data.cpp(alt+enter)

#include "data.h"

void Data::setA(int a){
    mA = a;
}

int Data::getA(){
    return mA;
}

然后就可以在main函数中使用了

构造函数

1. 概述

构造函数和析构函数用于完成对象初始化和对象清理,由编译器自动调用
如果用户不提供构造函数,编译器会自动添加一个默认的构造函数(空函数);用户添加构造函数,编译器就不再设置默认构造函数

构造函数和类名相同,没有返回值类型,可以有参数,可以重载
先给对象开辟空间(实例化),然后调用构造函数(初始化)
构造函数在public区

class Data{
public:
    int mA;
public:
    Data(){
        mA = 0;//无参构造函数
    }
    Data(int a){
        mA = a;//有参构造函数
    }
}

2. 调用构造函数

int main(void){
    //隐式调用构造函数
    Data ob1;
    Data ob2(10);
    //显式调用构造函数
    Data ob3 = Data();
    Data ob4 = Data(10);
    //构造函数隐式转换(类中只有一个数据成员)
    Data ob5 = 100;//相当于ob4
    return 0;
}

Data()是一个匿名对象(存活周期为当前语句),在这里申请空间,然后执行构造函数,然后给ob3接管

析构函数

当对象生命周期结束后,系统自动调用析构函数
先调用析构函数进行清理,再释放空间

析构函数和类名相同,再函数名前加~(为了和构造函数区别),没有返回值,没有参数,不能重载

class Data{
public:
    Data(){
        cout<<"构造函数"<<endl;
    }
    ~Data(){
       //析构函数
        cout<<"析构函数"<<endl;
    }
};

Data ob1;//报错
void test(){
    Data ob2();
    {
        Data ob3();
    }
    Data ob4();
}

采用栈的原理,先构造的对象后析构
一般情况下,空的析构函数就可以,但是如果类中有指针成员,必须写析构函数释放指针成员所指空间
释放时释放的是对象自身的空间,不会释放成员变量指向的堆空间

#include <iostream>
#include <string.h>
using namespace std;

class Data{
public:
    char *name;
    Data(){}
    Data(char *str){
        name = new char[strlen(str)+1];
        strcpy(name, str);
        cout<<"有参构造"<<endl;
    }
    ~Data(){
        if(name != NULL) delete []name;
        cout<<"析构函数"<<endl;
    }
};

int main(void){
    Data ob("hello world");
    cout<<ob.name<<endl;

    return 0;
}

拷贝构造函数

Data ob1(10);
Data ob2 = ob1;//一次构造,一次拷贝构造,两次析构

1. 概述(浅拷贝)

拷贝构造:本质是构造函数
拷贝构造的调用时机:旧对象 初始化 新对象 才会调用拷贝构造函数,取别名&不算拷贝构造
用户不提供拷贝构造,编译器会提供一个默认的拷贝构造,完成赋值动作——浅拷贝

Data(const Data &ob){//拷贝构造的定义形式
    //一旦自定义拷贝构造,则会使系统默认的失效。此时必须完成赋值操作
    mA = ob.mA;
}

2. 拷贝构造,无参构造,有参构造

如果定义了拷贝构造和有参构造,会屏蔽系统默认的无参构造。
也就是说,如果只定义了有参构造和拷贝构造,再执行Data ob1会报错,因为此时无法找到系统默认的无参构造,因为被屏蔽了

如果定义了无参构造或有参构造,不会屏蔽拷贝构造

3. 拷贝构造的几种调用形式

(1)旧对象给新对象初始化,调用拷贝构造

Data ob2 = ob1;//一次构造,一次拷贝构造,两次析构

(2)给对象取别名,不会调用拷贝对象

Data &p = ob;

(3)普通对象作为函数参数,调用函数时会发生拷贝构造

(4)函数返回普通值对象

Data ob2 = getObject();//返回一个Data对象ob1

ob1开辟空间,触发构造函数。getObject函数结束时,ob1的值放在临时空间(返回值小于4字节放寄存器,大于4字节放临时空间)
在临时区域申请一个匿名对象,再把ob1的值赋值过去——在这个过程中调用拷贝构造
ob2与拷贝构造毫无关系,即便不用ob2去接,也会调用拷贝构造
ob2将占用了匿名空间,这一过程不会发生拷贝构造(在vs中)

(5)QTcreator、linux不会发生拷贝构造

Data getObject(void){
    Data ob1(10);
    return ob1;
}
void test(){
    Data ob2 = getObject(); //仅一次构造,一次析构;无其他
}

QT和linux进一步优化,ob1构造,ob2析构,不再经过中间的匿名对象;
ob2直接接管ob1的空间,并导致作用范围改变

4. 拷贝对象的浅拷贝和深拷贝

如果没有指针成员,不必实现拷贝构造和析构函数
如果有指针成员,且指向堆区空间,必须实现析构释放堆区空间,必须实现拷贝构造完成深拷贝动作(否则会导致多次释放)

Data(const Data &ob){
    name = new char[strlen(ob.name)+1];
    strcpy(name, ob.name);//深拷贝的拷贝构造
}

初始化列表

1. 对象成员

在类中定义的对象,先调用对象成员的构造函数,再调用本身的构造函数;先构造的后析构

B中有A的对象变量,那么依次调用A类构造 - B类构造 - B类析构 - A类析构

类会自动调用对象成员的无参构造

2. 初始化列表

类想调用对象成员 有参构造 必须使用 初始化列表

class B{
    int mb;
    A oa;
    B(int a, int b){
        oa.ma = a;
    }
}
B ob(10,20);

(1)B(int a, int b)构造函数希望将a传给oa中的ma,b传给自己的mb;
(2)执行B ob(10, 20);时,先初始化A oa;此时会调用A的构造函数。
(3)由于B的构造函数B(int a, int b)在A构造之后才会执行,所以参数a=10无法在A执行构造函数时传过去创建oa时先执行一边A的
(4)现在希望在构造A的时候,在构造B之前,将a传过去,此时就需要初始化列表

3. 初始化列表示例

class B{
    int mb;
    A oa;
    B(int a, int b):oa(a){
        oa.ma = a;
    }
}
B ob(10, 20);

(1)B ob(10, 20); 找到构造函数 B(int a, int b),但此刻不会执行,因为需要先做A的构造
(2)加上:ob(a)表示初始化列表,B(int a, int b):ob(a)语句就是忽略掉B(int a, int b)的构造函数,先执行初始化列表ob(a)

不加初始化列表时,默认调用无参构造;定义了初始化列表后,可以调用有参构造

explicit关键字

禁止构造函数进行的隐式转换,针对单参数的构造函数(或除第一个参数外其余参数都有默认值的多参构造)而言

class Data{
public:
    explicit Data(int n){
        cout<<n<<endl;
    }
};

void test(){
    //Data str = 1;//禁止这种转换,不加explicit可以运行
}

类的对象数组

A arr[5] = {A(10),A(20),A(30),A(40),A(50)};

动态对象创建

malloc和free在C++不能很好地完成初始化和析构函数

C++创建对象时:(1)分配内存空间;(2)调用构造初始化,调用析构清理

new动态分配

C++将创建对象所需要的操作结合在一个new的运算符中。
当new一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化

Person *person = new Person;

new能确定在调用构造函数初始化之前内存分配是成功的,存在内置的长度计算,类型转换和安全检查

detele释放动态空间

delete先调用析构函数,然后释放内存

Person *person = new Person("John",33);
delete person;

在栈上面的对象,所在作用域结束后,会自动执行析构函数,而new出来的在堆上的对象,不调用delete,即使它所在的作用域已经结束,也不会调用析构函数

类成员有vector也指向了堆上的内存,就需要在析构函数中同样使用delete释放这块内存

静态成员

static修饰的静态成员属于类,而不是对象,即所有对象共享一份静态成员数据
static修饰的成员,在定义类的时候,必须分配空间
static修饰的静态成员数据,必须类中定义,类外初始化

class Data{
public:
    int a;//a为每个实例化对象独有
    static int b;//b为所有实例化对象共有
};
//类外初始化
int Data::b = 100;
void test(){
    //cout<<Data::a<<endl;//报错
    cout<<Data::b<<endl;//输出:100
}

对b的修改是共有的

Data ob1;
Data ob2;

ob1.b = 200;
ob2.b = 400;

cout<<ob1.b<<endl;//输出:400
cout<<ob2.b<<endl;//输出:400

案例:统计创建对象的个数

class Data{
public:
    int a;
    static int cnt;
public:
    Data(){
        cnt++;
    }
};

静态成员函数与静态成员相似,但是静态成员函数只能访问静态成员数据
同样需要类中定义,类外初始化

单例模式设计

一个类只实例化一个对象

1. 使不能实例化

class SingleTon{
    //1. 防止该类在外界实例化对象,构造函数私有化
private:
    SingleTon(){}
    SingleTon(const SingleTon &ob){}
    ~SingleTon(){}
    
};

2. 单例模式

//单例模式类
class SingleTon{
    //1. 防止该类在外界实例化对象,构造函数私有化
private:
    SingleTon(){}
    SingleTon(const SingleTon &ob){}
    ~SingleTon(){}
private:
    //2. 定义一个静态的指针变量保存唯一实例的值
    static SingleTon * const p;
public:
    //3. 获得唯一的实例地址
    static SingleTon * getSingleTon(void){
        return p;
    }
    //4. 用户定义的任务函数
    void printString(char *str){
        cout<<str<<endl;
    }
};

SingleTon * const SingleTon::p = new SingleTon;

int main(void){
    //获取单例的地址
    SingleTon *p1 = SingleTon::getSingleTon();
    p1->printString("p1");

    SingleTon *p2 = SingleTon::getSingleTon();
    p1->printString("p2");

    return 0;
}

this指针(面对对象模型)

成员变量和函数的存储

C++中非静态数据成员直接内含在类对象中,成员函数虽然内含在class声明之内,却不出现在对象中。每一个非内联成员函数只会诞生一份函数实例

也就是说非静态变量是对象独有的,函数是所有对象共有的

class Data{
public:
    char a;
    int b;
    static int c;
    void func(){};
};
//a和b为每个对象独有的,c和func是共享的
Data ob;//sizeof(ob) == 8,即便只有a和b也是8(可能是由于字节对齐的原因),不会将c和func的大小计算在内
//sizeof测量的是Data实例化对象的大小

this指针

1. 概述

成员函数是怎么知道是哪个对象调用自己的?this指针

class Data{
private:
    int ma;
public:
    void seta(int a){
        ma = a;
    }
};

Data ob1;
Data ob2;
Data ob3;

ob1.seta(10);//既然setA是公用的,那么setA怎么知道是谁调用的自己,并将a的值赋值到该对象中

实际上,在setA函数中,ma=a是this.ma = a的缩写,this中保存调用该成员函数的对象的地址,由编译器自动完成
this指针是一种隐含指针,隐含于每个类的非静态成员函数,无需定义,直接使用
注意:静态成员函数没有this指针,静态成员函数在对象出现之前出现,如果有this,在对象出来之前this不知道指向谁,并且静态成员函数只能操作静态成员变量

2. 形参和变量同名时可以使用this指针

class Data{
private:
    int a;
public:
    Data(int a){
        this->a = a;
        cout<<this->a<<endl;
    }
};
Data ob1(10);//&ob1 == this

this指向ob1的地址,因此能够找到正确的对象

3. 链式操作

class Data{
public:
    Data &myprintf(char *str){
        cout<<str<<endl;
        return *this;//返回调用该成员函数的对象(匿名对象)的别名
    }
};

void test(){
    Data().myprintf("one").myprintf("two").myprintf("three");
}

const修饰成员函数

const修饰的函数不允许对类中变量进行修改,mutable修饰的变量除外

class Data{
public:
    int a;
    int b;
    mutable int c;
public:
    Data(int a, int b, int c){
        this->a = a;
        this->b = b;
        this->c = c;
    }
    void showData(void) const{
        //a = 100;//报错
        c = 100;//成功
    }
};

友元

类的私有成员不允许在外部访问,友元将允许这种访问

使用关键字friend修饰
friend只出现在声明处,一个函数或一个类成为了另一个类的友元,就可以直接访问另一个类的私有数据
友元重要用在运算符重载上

1. 普通全局函数作为类的友元

class Data{
    friend void visit(Data &data);
private:
    int a;
public:
    Data(int a){
        this->a = a;
    }
};

void visit(Data &data){
    cout<<data.a<<endl;
}
void visit2(Data &data){
    cout<<data.a<<endl;
}

int main(void){
    Data data(10);
    visit(data);
    visit2(data);//报错:不能访问私有变量

    return 0;
}

2. 类中的某个成员函数作为另一个类的友元

class B;//必须先声明
class A{
private:
    int aa;
public:
    A(int aa){
        this->aa = aa;
    }
public:
    void visit(B &obB);
};

class B{
    friend void A::visit(B &obB);
private:
    int bb;
public:
    B(int bb){
        this->bb = bb;
    }
};

void A::visit(B &obB){
    cout<<obB.bb<<endl;
}

int main(void){
    A obA(10);
    B obB(20);
    obA.visit(obB);

    return 0;
}

如果要使得A的函数能够访问B中的私有变量,那么只需要在B中增加该函数的友元

3. 整个类作为另一个类的友元

如A要访问B的私有成员,那么A类要成为B类的友元。在B类中增加语句:

friend class A;

4. 友元的注意事项

友元不能被继承
友元关系是单向的
友元不具备传递性。B是A的朋友,C是B的朋友,不能得到C是A的朋友

运算符重载

运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
语法: 定义重载的运算符就像定义函数,只是该函数的名字是operator@,这里的@代表了被重载的运算
符。

1. 全局函数实现

class Person{
    friend ostream &operator<<(ostream &out, Person &ob);//必须设置友元
    friend istream &operator>>(istream &in, Person &ob);
    friend Person operator+(Person &ob1, Person &ob2);
private:
    int num;
    string name;
    float score;
public:
    Person(){}
    Person(int num, string name, float score):num(num),name(name),score(score){}
};

//全局函数重载operator<<
//cout<<lucy<<endl;//报错,cout<<lucy无返回值,将变成void<<endl,没有这种运算符,因此报错。需要设置返回值
/*
void operator<<(ostream &out, Person &ob){
    out<<ob.num<<" "<<ob.name<<" "<<ob.score<<endl;
}
*/
//cout<<lucy<<endl;成功
ostream &operator<<(ostream &out, Person &ob){
    out<<ob.num<<" "<<ob.name<<" "<<ob.score;
    return out;
}
//全局重载函数operator>>
istream &operator>>(istream &in, Person &ob){
    in>>ob.num>>ob.name>>ob.score;
    return in;
}
Person operator+(Person &ob1, Person &ob2){
    Person tmp;
    tmp.num = ob1.num+ob2.num;
    tmp.name = ob1.name+ob2.name;
    tmp.score = ob1.score+ob2.score;
    return tmp;
}

int main(void){
    Person lucy(100,"lucy",99.8f);
    Person bob(100,"bob",88.0f);
    cout<<lucy<<endl;//不定义运算符重载则报错
    //相当于operator<<(cout, lucy)
    Person tmp = lucy+bob;
    cout<<tmp;//这样可以
    //cout<<lucy+bob;//报错,因为operator<<的参数为Person &ob,而lucy+bob返回的是一个临时数据,不能取引用。可以将Person &ob改成Person ob

    return 0;
}

可以重载的运算符

在这里插入图片描述

2. 成员函数实现(推荐)

class Person{
    friend ostream &operator<<(ostream &out, Person ob);//不用设置operator+的友元
private:
    int num;
    string name;
    float score;
public:
    Person(){}
    Person(int num, string name, float score):num(num),name(name),score(score){}
    Person operator+(Person ob){
        Person tmp;
        tmp.num = num+ob.num;
        tmp.name = name+ob.name;
        tmp.score = score+ob.score;
        return tmp;
    }
};

ostream &operator<<(ostream &out, Person ob){
    out<<ob.num<<" "<<ob.name<<" "<<ob.score;
    return out;
}

int main(void){
    Person lucy(100,"lucy",99.8f);
    Person bob(100,"bob",88.0f);
    //lucy+bob == lucy.operator+(bob)
    //cout<<lucy+bob;

    return 0;
}

3. 重载++/–运算符

++a:operator++(a)
a–:operator++(a, int);

Person operator++(int){//a没有传,因为this替代了
    Person old = *this;
    this->num++;
    this->name = this->name+this->name;
    this->score++;
    return old;
}
cout<<lucy++<<endl;
cout<<lucy<<endl;

4. 重载函数调用运算符

class Print{
public:
    //重载函数调用运算符
    void operator()(char *str){
        cout<<str<<endl;
    }
};

int main(void){
    Print ob;
    //对象和()结合触发operator()调用;也称为仿函数
    ob("hello");
    Print()("world");//匿名对象结合()也可以触发

    return 0;
}

智能指针

解决堆区空间的释放问题

class Data{
public:
    Data(){
        cout<<"Data的无参构造"<<endl;
    }
    ~Data(){
        cout<<"Data的析构函数"<<endl;
    }
    void func(){
        cout<<"Data的func函数"<<endl;
    }
};

class SmartPointer{
private:
    Data *p;
public:
    SmartPointer(){};
    SmartPointer(Data *p){
        this->p = p;
    }
    ~SmartPointer(){
        delete p;
    }
    //重载->运算符
    Data *operator->(){
        return p;
    }
    Data &operator*(){
        return *p;
    }
};

int main(void){
    SmartPointer sp(new Data);

    sp.operator ->()->func();
    sp->func();
    (*sp).func();
    return 0;
}

继承

继承的定义

class 父类{};
class 子类:继承方式 父类名{
	//新增子类数据
};

继承方式:private protected public(推荐)

public继承:父类的public为自己的public,protected为自己的protected,父类的private不可访问
protected继承:父类的public为自己的protected,protected为自己的protected,父类的private不可访问
private继承:父类的public为自己的private,protected为自己的private,父类的private不可访问

继承中的构造和析构

父类构造——内部对象构造——子类构造
子类析构——内部对象析构——父类析构

子类会默认调用成员对象、父类的默认构造
子类实例化对象时,必须使用初始化列表调用成员对象、父类的有参构造(父类写类名称,成员对象写对象名)

子类和父类的同名处理

最简单、最安全的处理方式:加作用域

1. 子类和父类同名成员数据

如父类中有int a,子类中也有int a;
子类优先访问自己的成员数据
要想访问父类的成员,需要加作用域

cout<<ob.a<<endl;//优先访问子类的变量
cout<<ob.Base::a<<endl;//作用域访问父类的变量,访问方法同理

2. 子类重定义父类的同名函数

子类一旦重定义父类的同名函数,父类的同名函数(重载函数)都将被屏蔽,但仍然可以通过作用域的方式访问

不能被继承的函数

构造函数、析构函数、operator=
如果子类没有自己创建这些函数,编译器会自动生成

多继承

class 父类1{};
class 父类2{};
class 子类:继承方式1 父类1, 继承方式2 父类2{
	//新增子类数据
};

菱形继承

有公共祖先的继承,叫菱形继承

B和C都继承于A,D多继承于B和C,于是D就有多份A的成员,访问时会产生二义性。解决方法:加作用域

如果只想要一份公共数据:虚继承

虚继承

在继承方式前面加virtual继承,保留一份公共数据

class B:virtual public A{};
class C:virtual public A{}
class D:public B, public C{};

在这里插入图片描述

虚继承 会在子类中产生 虚基类指针(vbptr) 指向 虚基类表(vbtable), 虚基类表纪录的是通过该指针访问公共祖先的数据的偏移量。

注意:虚继承只能解决具备公共祖先的多继承所带来的二义性问题,不能解决没有公共祖先的多继承的。工程开发中真正意义上的多继承是几乎不被使用,因为多重继承带来的代码复杂性远多于其带来的便利,多重继承对代码维护性上的影响是灾难性的,在设计方法上,任何多继承都可以用单继承代替。

多态

静态多态(编译时多态,早绑定):函数重载、运算符重载、重定义
动态多态(运行时多态,晚绑定):虚函数

父类指针保存子类空间

需求:设计一个算法,可以操作父类派生的所有子类
方法:父类指针保存子类的空间地址

class Animal{
public:
    void speak(void){
        cout<<"动物在说话"<<endl;
    }
};
class Dog:public Animal{
public:
    void speak(void){
        cout<<"狗在说话"<<endl;
    }
};

int main(void){
    Dog *p1 = new Dog;
    p1->speak();//狗在说话

    Animal *p2 = new Dog;
    p2->speak();//动物在说话
    
    Dog *p3 = new Animal;
    p3->speak();//报错!

    return 0;
}

p2输出“动物在说话”的原理:
一个指针变量能操作空间的大小是由它所指向的类型决定的。new Dog给出的空间是父类+自身,而p2只能有父类大小的空间,因此p2指向的是new Dog中父类的部分

虚函数

成员函数前加virtual修饰

//1. 在父类中定义虚函数
virtual void speak(void){
    cout<<"动物在说话"<<endl;
}
//2. 在子类中重写函数;此时返回类型、函数名、参数必须完全一致,但这里的virtual可写可不写
virtual void speak(void){
    cout<<"狗在说话"<<endl;
}
//3. 此时输出的是子类函数内容
Animal *p2 = new Dog;
p2->speak();//狗在说话

如果一个类中的成员函数被virtual修饰,那么这个函数就是虚函数。类就会产生一个虚函数指针(vfptr)指向了一张虚函数表(vftable)。如果这个类没有涉及到继承, 这时虚函数表中纪录的就是当前虚函数入口地址。

子类继承父类的虚函数指针,并且一旦子类重写父类虚函数,就会用子类的虚函数地址覆盖掉虚函数表中原来的入口地址

当执行Animal *p2 = new Dog时,p2仍然指向父类的空间,但是由于父类的虚函数表中的入口地址被覆盖,因此会执行子类函数

在这里插入图片描述

重载、重定义、重写的区别

重载:同一作用域,同名函数,参数的顺序、个数、类型不同 都可以重载。函数的返回值类型不能作为重载条件(函数重载、运算符重载)
重定义:有继承,子类 重定义 父类的同名函数(非虚函数), 参数顺序、个数、类型可以不同。子类的同名函数会屏蔽父类的所有同名函数(可以通过作用域解决)
重写(覆盖):有继承,子类 重写 父类的虚函数。返回值类型、函数名、参数顺序、个数、类型都必须一致。

纯虚函数

如果基类一定会派生出子类,且子类一定会重写父类的虚函数,那么父类中的函数体就显得无意义

class Animal{
public:
    //纯虚函数
    virtual void speak(void) = 0;
};
Animal p;//报错

有纯虚函数的类被称为抽象类,不能实例化对象

子类必须重写父类的所有纯虚函数。因为只要有一个纯虚函数,就是抽象类,抽象类不能实例化

虚函数不会导致父类变成抽象类,但纯虚函数会

纯虚函数案例 - 制作饮品

//抽象制作印品
class AbstractDrinking{
public:
    //烧水
    virtual void Boil() = 0;
    //冲泡
    virtual void Brew() = 0;
    //倒入杯中
    virtual void PourInCup() = 0;
    //加入辅料
    virtual void PutSomething() = 0;
    //规定流程
    void markDrink(){
        this->Boil();
        Brew();
        PourInCup();
        PutSomething();
    }
};
//制作咖啡
class Coffee:public AbstractDrinking{
public:
    //烧水
    void Boil(){
        cout << "煮农夫山泉!" << endl;
    }
    //冲泡
    virtual void Brew(){
        cout << "冲泡咖啡!" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "将咖啡倒入杯中" << endl;
    }
    //加入辅料
    virtual void PutSomething(){
        cout << "加入甲醛!" << endl;
    }
};
//制作茶水
class Tea:public AbstractDrinking{
public:
    //烧水
    void Boil(){
        cout << "煮自来水!" << endl;
    }
    //冲泡
    virtual void Brew(){
        cout << "冲泡茶叶!" << endl;
    }
    //倒入杯中
    virtual void PourInCup(){
        cout << "将茶水倒入杯中" << endl;
    }
    //加入辅料
    virtual void PutSomething(){
        cout << "加入乙醚!" << endl;
    }
};
//业务函数
void doBussiness(AbstractDrinking *drink){
    drink->markDrink();
    delete drink;
}
int main()
{
    doBussiness(new Coffee);
    cout<<"-------------------"<<endl;
    doBussiness(new Tea);
    return 0;
}

虚析构

问题:只调用了父类的析构
原因:p执行父类的空间,析构也只能析构父类空间
需求:父类和子类都需要析构

Animal *p = new Dog;
p->speak();//动物在说话
delete p;

依次执行:Animal的构造 - Dog的构造 - speak函数 - Animal的析构

解决方法:在父类析构函数上加上virtual,就可以释放子类的空间

virtual ~Animal(){
	cout<<"Animal的析构"<<endl;
}

此时依次执行:Animal的构造 - Dog的构造 - speak函数 - Dog的析构 - Animal的析构

原理:通过父类指针释放子类的所有空间。与虚函数类似,但并不是重写入口地址

  1. 父类的虚函数指针,指向虚函数表,这个表中记录的是父类的析构函数入口地址

  2. 子类继承时会吧虚析构指针和虚函数表继承过来,此时会修改使指向子类的析构函数(此时非重写)

  3. delete p的时候,依然去父类空间,但指针此时指向的是子类析构函数,于是执行子类的析构

  4. 子类析构完之后,会自动调父类析构

纯虚析构

class Animal{
public:
    virtual void speak(void) = 0;
    //纯虚析构
    virtual ~Animal() = 0;//可以在内部实现函数体,但不推荐。有的编译器不支持
}
Animal::~Animal(){
    cout<<"Animal的析构函数"<<endl;
}

模板

C++面向对象编程:封装、继承、多态
C++泛型编程思想:模板

模板分类:函数模板、类模板

将功能相同,类型不同的函数(类)的类型抽象成虚拟的类型。当调用函数(类实例化对象)的时候,编译器自动将虚拟的类型具体化。这个就是函数模板(类模板)。

函数模板

模板关键字 template

//T只能对当前函数有效;T会自动推导数据类型
template<typename T> void swapAll(T &a, T &b){
    T tmp = a;
    a = b;
    b = tmp;
    return;
}
//也可以分行写
template<typename T>
void swapAll(T &a, T &b){}//作用范围仍然是swapAll

函数模板 会编译两次:
第一次:是对函数模板 本身编译
第二次:函数调用处 将T的类型具体化
函数模板目标:模板是为了实现泛型,可以减轻编程的工作量,增强函数的重用性。

函数模板的注意点

  1. 函数模板和普通函数(重名)都存在时,优先选择普通函数

  2. 强行调用模板函数时,可以使用<>

swapAll<>(a, b);//自动推导
swapAll<int>(a,b);//不必推导,已告知类型
  1. 函数模板自动类型推导时,不能对函数的参数进行自动类型转换
template<typename T> void myprint(T &a, T &b){}//函数模板
void myprint(int &a, int &b){}//普通函数
int main(void){
    myprint(10,20);//普通函数
    myprint('a','b');//函数模板
    myprint(10,'b');//普通函数;T必须是一样的,模板函数类型推导失败(不能自动类型转换,但可以强制转换),因此会调用普通函数
    myprint<int>(10,'b');//函数模板
}

函数模板的局限性

当函数模板推导出T为数组或其他自定义类型数据时,可能导致运算符不识别

解决方法一:运算符重载(推荐)

class Data{
    friend ostream &operator<<(ostream &out, Data ob);
private:
    int data;
public:
    Data(){}
    Data(int data){
        this->data = data;
    }
};
ostream &operator<<(ostream &out, Data ob){
    out<<ob.data;
    return out;
}
template<typename T> void myprint(T a){
    cout<<a<<endl;
}
int main(void){
    myprint(10);

    Data ob(20);//如果不重写运算符,cout无法识别
    myprint(ob);

    return 0;
}

解决方法二:具体化函数模板

//依然先找myprint,但发现不支持Data,于是再去找有没有具体化的模板,然后找到<>printAll;
class Data{
private:
    int data;
public:
    Data(){}
    Data(int data){
        this->data = data;
    }
    int getData(void){
        return data;
    }
};
template<typename T> void myprint(T a){
    cout<<a<<endl;
}
template<> void myprint<Data>(Data a){
    cout<<a.getData()<<endl;
}
int main(void){
    myprint(10);

    Data ob(20);//如果不重写运算符,cout无法识别
    myprint(ob);

    return 0;
}

类模板

类模板不能自动推导类型,只能先指定

template <class T1, class T2> class Data{
private:
    T1 a;
    T2 b;
public:
    Data(){}
    Data(T1 a, T2 b){
        this->a = a;
        this->b = b;
    }
    void showData(){
        cout<<a<<" "<<b<<endl;
    }
};

//Data ob;//报错,推导不出类型
//Data ob(10,20);//报错,不能自动推导类型
Data<int,int> ob(10,20);
  1. 类模板的成员函数在类外实现
template <class T1, class T2> class Data{
private:
    T1 a;
    T2 b;
public:
    Data(){}
    Data(T1 a, T2 b);
    void showData();
};
template <class T1, class T2> Data<T1,T2>::Data(T1 a, T2 b){
    this->a = a;
    this->b = b;
}
template <class T1, class T2> void Data<T1, T2>::showData(){
    cout<<a<<" "<<b<<endl;
}
  1. 函数模板作为类模板的友元
template <class T1, class T2> class Data{
    template<typename T3, typename T4> friend void myprint(Data<T3, T4> &ob);
private:
    T1 a;
    T2 b;
public:
    Data(){}
    Data(T1 a, T2 b);
    void showData();
};
template<typename T3, typename T4> void myprint(Data<T3, T4> &ob){
    cout<<ob.a<<" "<<ob.b<<endl;
}
int main(void){
    Data<int, char> ob(100,'a');
    myprint(ob);

    return 0;
}
  1. 普通函数作为类模板的友元
friend void myprint(Data<int, char> &ob);
void myprint(Data<int, char> &ob){
    cout<<ob.a<<" "<<ob.b<<endl;
}
  1. 模板头文件和源文件分离问题

模板文件需要二次编译,第一次是在编译阶段,第二次是在函数调用处。
头文件包含是在预处理阶段。当模板文件和源文件分离时,第一次编译能够引入模板文件,但第二次编译确定类型时就不会再去编译模板.cpp文件了。因此会报错undefine

因此对于模板文件,需要将template.h和template.cpp都include进来。
进一步地,可以将template.cpp中的内容复制到template.h中,并删除template.cpp,然后将template.h改为template.hpp
类模板所在文件一般用.hpp标识

数组类模板

问题:为什么第二遍输出会乱码?
推测:和string name的空间释放有关

main.cpp

#include<iostream>
#include<myarray.hpp>
#include<string>
using namespace std;

class Person{
    friend ostream& operator<<(ostream &out, Person ob);
private:
    int num;
    string name;
    float score;
public:
    Person(){}
    Person(int num, string name, float score){
        this->num = num;
        this->name = name;
        this->score = score;
    }
    bool operator>(Person ob){
        return score>ob.score;
    }
};
//不识别某个类时,应该去该类中重载
ostream& operator<<(ostream &out, const Person ob){
    out<<ob.num<<" "<<ob.name<<" "<<ob.score;
    return out;
}
int main(void){
    Person lucy = Person(100,"lucy",88.8f);
    Person bob = Person(101,"bob",98.8f);
    Person tom = Person(102,"tom",78.8f);
    MyArray<Person> arr1;
    arr1.pushback(lucy);
    arr1.pushback(bob);
    arr1.pushback(tom);
    cout<<arr1;
    cout<<arr1;//第二次报错的原因?
    arr1.sortArray();
    cout<<arr1;
    return 0;
}

myarray.hpp

#ifndef MYARRAY_HPP
#define MYARRAY_HPP
#include <iostream>
#include <cstring>
using namespace std;

template<class T>
class MyArray{
    template<typename T1> friend ostream& operator<<(ostream &out, MyArray<T1> ob);
private:
    T *arr;
    int size;
    int capacity;
public:
    MyArray();
    MyArray(int capacity);
    MyArray(const MyArray &ob);
    ~MyArray();
    MyArray& operator=(MyArray &ob);

    void pushback(T elem);
    void sortArray();
};
#endif // MYARRAY_HPP

template<class T>
MyArray<T>::MyArray(){
    this->size = 0;
    this->capacity = 5;
    this->arr = new T[capacity];
    memset(this->arr, 0, sizeof(T)*this->capacity);
}
template<class T>
MyArray<T>::MyArray(int capacity){
    this->capacity = capacity;
    this->size = 0;
    this->arr = new T[this->capacity];
    memset(this->arr, 0, sizeof(T)*this->capacity);
}
template<class T>
MyArray<T>::MyArray(const MyArray &ob){
    this->capacity = ob.capacity;
    this->size = ob.size;
    this->arr = new T[this->capacity];
    memset(this->arr, 0, sizeof(T)*this->capacity);
    memcpy(this->arr, ob.arr, sizeof(T)*this->capacity);
}
template<class T>
MyArray<T>::~MyArray(){
    delete[] arr;
}
//运算符重载,实现深拷贝
template<class T>
MyArray<T>& MyArray<T>::operator=(MyArray &ob){
    //判断arr是否存在旧空间
    if(this->arr != NULL){
        delete[] this->arr;
        this->arr = NULL;
    }
    this->capacity = ob.capacity;
    this->size = ob.size;
    this->arr = new T[this->capacity];
    memset(this->arr, 0, sizeof(T)*this->capacity);
    memcpy(this->arr, ob.arr, sizeof(T)*this->capacity);
    //为了完成链式操作,即ob1=ob2=ob3,需要返回
    return *this;
}
template<class T1>
ostream& operator<<(ostream &out, MyArray<T1> ob){
    for(int i=0; i<ob.size; ++i) out<<ob.arr[i]<<" ";
    out<<endl;
    return out;
}
template<class T>
void MyArray<T>::pushback(T elem){
    if(size == capacity){
        capacity *= 2;
        T *tmp = new T[capacity];
        if(arr != NULL){
            memcpy(tmp, arr, sizeof(T)*size);
            delete[] arr;
        }
        arr = tmp;
    }
    arr[size] = elem;
    size++;
}
template<class T>
void MyArray<T>::sortArray(){
    if(size == 0){
        cout<<"容器为空"<<endl;
        return;
    }
    int i = 0, j = 0;
    for(i=0; i<size-1; ++i){
        cout<<"this is in before"<<endl;
        cout<<arr;
        for(j=0; j<size-i-1; ++j){
            if(arr[j] > arr[j+1]){
                T tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
        cout<<"this is in after"<<endl;
    }
    cout<<"this is the end of sort"<<endl;
}

类模板的继承

子类继承类模板,那么子类也是类模板

template<class T1, class T2>
class Base{
private:
    T1 a;
    T2 b;
public:
    Data(){}
    Data(T1 a, T2 b);
    void showData();
};

template<class T1, class T2, class T3>
class Son1:public Base<T1,T2>{
public:
    T3 c;
public:
    Son1(T1 a, T2 b, T3 c):Base<T1,T2>(a,b){
        this->c = c;
    }
};

类型转换

上行、下行转换

子类空间 > 父类空间 可以把大的转给小的
上行:安全。子类转为父类,父类指针保存子类空间,只访问子类的一部分,虽然有一部分不会再访问到,但不会越界
下行:不安全。父类转为子类,子类指针保存父类空间,父类只分配了自己空间,子类访问会造成越界

在这里插入图片描述

static_cast静态类型转换

  1. 基本类型
int num = static_cast<int>(3.14);//ok
  1. 上行转换
Base* p = static_cast<Base*>(new Son);//ok
  1. 下行转换
Son* p = static_cast<Son*>(new Base);//通过编译,使用出问题;
  1. 不相关类型转换
Son* p = static_cast<Son*>(new Other);//报错

dynamic_cast静态类型转换(推荐)

  1. 基本类型
int num = dynamic_cast<int>(3.14);//报错
  1. 上行转换
Base* p = dynamic_cast<Base*>(new Son);//ok
  1. 下行转换
Son* p = dynamic_cast<Son*>(new Base);//报错
  1. 不相关类型转换
Son* p = dynamic_cast<Son*>(new Other);//报错

const_cast常量转换

  1. 将const 转换为非const
const int *p1;
int *p2 = const_cast<int *>(p1);//必须加()

const int &ob = 10;
int &ob1 = const_cast<int &>(ob);//必须加()
  1. 将非const转换成const
int *p3;
const *p4 = const_cast<int *>(p3);//不用const_cast也能转换成功

int data = 10;
const int &ob2 = const_cast<const int &>(data);//不用const_cast也能转换成功

重新解释转换(reinterpret_cast)(最不安全)

  1. 基本类型,不支持
int num = reinterpret_cast<int>(3.14f);//报错
  1. 基本类型指针,支持
float *p;
int *q = reinterpret_cast<int *>(p);//ok
  1. 上行转换,支持
Base* p = reinterpret_cast<Base*>(new Son);//ok
  1. 下行转换,支持
Son* p = reinterpret_cast<Son*>(new Base);//通过编译
  1. 不相关类型转换:支持
Son* p = reinterpret_cast<Son*>(new Other);//通过编译

异常

C语言通过返回值提示异常

!!!异常是一个类

异常的抛出和捕获

try{
	throw 异常值;
}catch(异常类型1 异常值1){
	处理异常的代码1;
}catch(异常类型2 异常值2){
	处理异常的代码2;
}catch(...){//任何异常都捕获
	处理异常的代码3;
}

案例

try{
    //throw 1;
    throw 2.14f;//如果抛出异常而没有捕获,程序结束
}catch(int e){
    cout<<"int异常值为:"<<e<<endl;
}catch(char e){
    cout<<"char异常值为:"<<e<<endl;
}catch(...){//捕获所有异常
    cout<<"其他异常值为:"<<endl;
}

!!如果抛出异常而没有捕获,程序结束。即上述代码如果没有catch(…)会报错

栈解旋

异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋

try{
    Data ob1;
    Data ob2;
    Data ob3;
    throw 1;//这里抛出异常后,ob3和ob2和ob1依次自动释放,即栈解旋
}

栈解旋之后再捕获和处理

异常的接口声明

描述可以抛出哪些类型的异常

1. 默认可以抛出任意异常(推荐)

2. 只能抛出特定类型的异常

void func() throw(int, char){
    throw "hello";//抛出,不能被捕获,于是系统接管,终止程序
}

3. 不能抛出任何异常

void func() throw(){
    throw 1;
}

异常变量的声明周期

定义异常类

class MyException{
public:
    MyException(){
        cout<<"异常变量构造"<<endl;
    }
    MyException(const MyException &e){
        cout<<"拷贝构造"<<endl;
    }
    ~MyException(){
        cout<<"异常变量析构"<<endl;
    }
};

1. 普通变量接异常

void test01(){
    try{
        throw MyException();//抛出一个对象
    }catch(MyException e){//普通对象e接异常,拷贝构造
        cout<<"普通对象接异常"<<endl;
    }
}

依次发生:异常变量构造 - 拷贝构造 - 异常变量析构 - 异常变量析构

2. 对象指针接异常

void test02(){
    try{
        throw new MyException;//抛出一个对象,这个对象在堆上
    }catch(MyException *e){//指针e接异常,需要delete释放
        cout<<"指针接异常"<<endl;
        delete e;
    }
}

依次发生:异常变量构造 - 异常变量析构

3. 引用接异常(推荐)

void test03(){
    try{
        throw MyException();//抛出一个对象
    }catch(MyException &e){//引用e接异常,不需要delete释放
        cout<<"引用接异常"<<endl;
    }
}

依次发生:异常变量构造 - 异常变量析构

异常的多态

用父类异常去接所有的子类异常,类似于虚函数原理

//异常基类
class BaseException{
public:
    virtual void printError(){};
};
//空指针异常
class NullPointerException:public BaseException{
public:
    virtual void printError(){
        cout<<"NullPointerException"<<endl;
    }
};
//越界异常
class OutOfRangeException:public BaseException{
public:
    virtual void printError(){
        cout<<"OutOfRangeException"<<endl;
    }
};
void dowork(){
    //throw OutOfRangeException();
    throw NullPointerException();
}
int main(void){
    try{
        dowork();
    }catch(BaseException& e){
        e.printError();
    }

    return 0;
}

C++标准异常

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

使用方法

try{
    throw out_of_range("越界异常!");
}catch(exception &e){
    cout<<e.what()<<endl;
}

编写自己的异常

需要基于标准异常基类,编写自己的异常

#include<exception>
//必须继承exception类
class NewException:public exception{
private:
    string msg;
public:
    NewException();
    NewException(string msg){
        this->msg = msg;
    }
    //重写父类的what
    virtual const char *what() const throw(){//防止父类在子类前抛出标准异常
        return this->msg.c_str();
    }
    ~NewException(){}
};

int main(void){
    try{
        throw NewException("自定义异常!");//如果不加const throw,则输出std::exception
    }catch(exception &e){
        cout<<e.what()<<endl;
    }

    return 0;
}
  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-09-13 10:54:41  更:2022-09-13 10:57:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 15:16:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码