文章目录
- 实现和声明的分离
- 不使用 using namespace std;
- C++对C语言的增强
- 重点1 引用
- 成员函数调用const修饰对象实例
- 构造和析构函数实例
- 深拷贝浅拷贝
- 初始化列表
- 类对象作为类成员的案例
- explicit 关键字
- new动态对象创建 完美代替malloc
- 静态成员变量
- 单例模式
- 成员变量和成员属性分开处理
- this指针
- 空指针访问成员的函数
- 常函数和常对象
- 全局函数做友元函数
- 整个类做友元类
- 让成员函数做友元函数
- 运算符重载
- 自定义string类
- 继承
- 多态
- 模板
- 排序实例
- 普通函数和函数模板的区别
- 模板机制内部原理
- 函数模板的局限性
- 类模板的使用
- 成员函数的创建时机
- 类模板做函数的参数
- 类模板的继承问题
- 类模板类外实现成员函数
- 类模板的分文件编写代码
- 类模板的友元函数
- 类模板的应用 写一个通用数组类
- C++类型转换
- 异常
- 输入输出流
实现和声明的分离
源文件中引用同名头文件,头文件只声明,源文件中进行定义
比如一个game1.cpp #include一个"game1.h" 然后再在main.cpp中调用"game1.h" 便可以使用game1.cpp中定义的函数,这个便是实现和声明的分离
不使用 using namespace std;
用std::来替代
C++对C语言的增强
各种检测增强,比如类型转换增强
C++中使用struct 可以不需要再写struct
C语言中没有bool类型,C++中有
C++中三目运算符增强
a>b? a:b;
因为C++最后返回的是变量,上面最后一个操作是b=100
C++中const 是真正的常量不会分配内存,放在符号表中;
C++中用地址修改,只是在修改临时空间里的值
而C语言中const是个伪常量可以通过地址来修改,编译器会分配内存
重点1 引用
(1)引用基本语法 Type &别名=原名
(2) 引用必须初始化 引用初始化不能修改
(3)对数组建立引用
int arr[10];
//给数组起别名
int(&pArr)[10]=arr;
cout<<pArr[i]<<endl;
(4)引用传递
void mySwap3(int &a,int &b)
{
}
mySwap2(&a,&b);
引用的注意事项
1.引用必须引一块合法的内存空间
2.不要返回一个局部变量的引用
int &a=10;//引用必须引一块合法的内存空间 这样子是会报错的
注意如果一个函数的返回值是引用,那么这个函数调用可以作为左值
引用的本质
ref=100;//ref转换这一步是C++隐式完成的
指针引用
利用指针开辟空间 相同的目的 但是不需要**这种操作
可以用一级指针引用可以代替二级指针
常量型引用
其实常量引用也是可以改的
常量引用场景1:
成员函数调用const修饰对象实例
要在函数后面加 ‘const’ 明确指出这个成员函数不会对const对象进行修改 不然常规的成员函数不能调用const对象 因为编译器不知道成员函数是否对其进行了修改。
构造和析构函数实例
构造和析构一定要在public下
没有参数就不能重载(析构不能重载)
注意当我们提供了有参的构造函数后,系统便不再提供默认构造函数 但是默认的拷贝构造和析构都还在
拷贝构造函数
之所以要用引用形式是因为 如果不用引用就会出现死循环 传值的时候又会调用拷贝构造
调用方式 第二种就是拷贝构造
拷贝构造函数的调用时机
深拷贝浅拷贝
class Person
{
Person(){}
Person(char * name, int age){
//m_Name=name;//char * 放到堆上好点
m_Name=(char *)molloc(strlen(name)+1);//堆上内容的析构 不能用默认 需要手动free
strcpy(m_Name,name);
m_age=age;
}
char * m_Name;
int m_age;
//拷贝构造如果用默认此时就会报错
~Person(){
if(m_Name!=NULL)
{
free(m_Name);
m_Name=NULL;//防止野指针
}
}
};
这样子调用拷贝构造就会报错
报错原因
在p1的mName被释放后 p2的mName也是那个地址 又一次被释放 ,但此时地址已经为空,因此报错。
这种简单地把地址复制过去的拷贝叫做浅拷贝
深拷贝
自己创建一个全新的空间,不能用系统默认的拷贝构造
初始化列表
或者
类对象作为类成员的案例
explicit 关键字
class Mystring{
public:
Mystring(const char *str)
{
}
explicit Mystring(int a)//这行关键字就是防止隐式类型转换的
{
msize=a;
}
char * mstr;
int msize;
};
void test()
{
Mystring str="mystring";
Mystring str3(10);//这是对的 很明显在调用int类型 有参构造
Mystring str2=12;//会引发歧义 代码用途不明确 str2字符串为‘10’,字符串长度为10 不知道调用的是哪个有参构造
//会引发一个隐式类型转换
// 此时加上一个 explicit 这行代码会报错
}
所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换。
C++隐式转换的原则
基本数据类型 基本数据类型的转换以取值范围的作为转换基础(保证精度不丢失)。
隐式转换发生在从小->大的转换中。比如从char转换为int。
从int-》long。
自定义对象子类对象可以隐式的转换为父类对象。
** s4和s5分别把一个int型和一个char型,隐式转换成了分配若干字节的空字符串**
new动态对象创建 完美代替malloc
原始malloc操作
存在的问题
一行解决
Person person= new Person;//自动调用构造函数
class Person2{
public:
Person2()
{}
//拷贝构造
~Person2()
{}
};
void test2(){
//Person2 person; 栈区开辟
Person2 *p2=new Person2;//堆区开辟 此时不会自动释放
//所有new出来的对像都会返回该类型的指针
//malloc不会自动调用构造 但这里new会自动调用构造 new是个运算符 malloc是一个函数
//要释放堆区空间 需要delete
delete p2;//delete也是一个运算符 要配合着一个new用 不要和free malloc弄混
};
void test3()
{
void *p2=new Person2;//当用void *接受new出来的指针时会出现释放的问题
delete p2;//将无法释放要避免这种写法
}
void test4()
{
//通过new开辟数组 注意一定要提供默认构造函数 写了自己的构造之后 也要写一个默认构造函数
Person2 * pArray=new Person2[10];
//当栈上开辟数组 可以指定有参构造 堆区必须要有默认 栈上不用
//Person2 pArray[10]={Person2(1),Person2(2)};
delete [] pArray;//必须要加中括号 释放数组时
}
如果没有中括号 delete就不知道那个数组大小记录3 不知道该调用几次析构
静态成员变量
class Person2{
public:
Person2()
{}
static int m_Age;//加入static就是静态成员变量 会共享数据
//静态成员变量,在类内声明,类外进行初始化
~Person2()
{}
private:
static int m_other;//私有权限 在类外不能访问 但可以初始化
};
int Person2::m_Age=10;//类外初始化实现
int Person2::m_other=10;//用Person::就相当于是类内
静态成员函数
class Person2{
public:
Person2()
{}
static int m_Age;//加入static就是静态成员变量 会共享数据
int m_time;
//静态成员变量,在类内声明,类外进行初始化
~Person2()
{}
static void func()//不可以访问 普通成员变量 无法区分这个普通成员变量是谁传的
{
m_time=15;//错误
m_Age=10;//正确 可以访问静态成员变量
cout<<"func调用"<<endl;
}
private:
static int m_other;//私有权限 在类外不能访问 但可以初始化
};
原因
可以访问静态成员变量
静态成员函数也是有权限的 私有的不可类外访问
单例模式
比如系统的任务管理器作为一个对象 不管怎么右键打开 都只有一个窗口 不管怎么右键 创建都是同一个对象 如果一个类中只能实例出来一个对象 就叫单例模式
比如 主席类 只能 new一个主席对象
//创建主席类
//需求 单例模式 为了创建类中的对象 并且保证只有一个对象实例
class ChairMan
{
private://构造函数私有化
ChairMan()
{}
public:
static ChairMan *singlemen;//类内声明 不需要通过对象得到这个主席
};
ChairMan * ChairMan::singlemen=new ChairMan;//通过命名空间使得可以访问私有构造函数
void test11()
{
// ChairMan c1;
// ChairMan * c2= new ChairMan;
//不论创建几个对象都是同一个主席
ChairMan *cm=ChairMan::singlemen;//这样就保证了只有一个主席 并且这个创建是在编译过程就完成了 在main运行之前就创建好了这个对象
ChairMan *cm2=ChairMan::singlemen;//这样就保证了只有一个主席 并且这个创建是在编译过程就完成了 在main运行之前就创建好了这个对象
//为了防止有人手贱修改这个主席 比如
ChairMan::singlemen=NULL;
//需要将所有的singleMan属性设置为私有 对外提供接口来进行访问
}
属性全部私有化之后
//创建主席类
//需求 单例模式 为了创建类中的对象 并且保证只有一个对象实例
class ChairMan
{
private://构造函数私有化
ChairMan()
{}
private:
static ChairMan *singlemen;//类内声明 不需要通过对象得到这个主席
public:
static ChairMan * getinstance()
{
return singlemen;
}
};
ChairMan * ChairMan::singlemen=new ChairMan;//通过命名空间使得可以访问私有构造函数
void test11()
{
// ChairMan c1;
// ChairMan * c2= new ChairMan;
//不论创建几个对象都是同一个主席
//ChairMan *cm=ChairMan::singlemen;//这样就保证了只有一个主席 并且这个创建是在编译过程就完成了 在main运行之前就创建好了这个对象
//ChairMan *cm2=ChairMan::singlemen;//这样就保证了只有一个主席 并且这个创建是在编译过程就完成了 在main运行之前就创建好了这个对象
////为了防止有人手贱修改这个主席 比如
//ChairMan::singlemen=NULL;
//需要将所有的singleMan属性设置为私有 对外提供接口来进行访问
ChairMan* cm1=ChairMan::getinstance();
}
拷贝构造特殊情况
此时cm2和cm3不同 会有两个对象
所以还要进行拷贝构造函数私有化
成员变量和成员属性分开处理
通过this指针区分成员函数
一个空类的大小为1
此时类对象大小等于4 并不是想象中的8(函数指针4+成员变量4)
原因:
因为非静态的成员函数会单独放在另外一个地方 不在类对象中
同理静态变量也不属于类对象
字节对齐问题 下面的类对象不是12 而是16
加一句
#pragma pack()影响对齐方式
this指针
编译器会给成员函数偷偷加一个this指针参数 用以保存这个对象的地址
此时就把p1p2的地址传进去了
谁调用了就指向谁
左边是普通的代码 右边是编译器转换后的代码
this指针的使用
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200220160824632.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zOTU1ODIyNw==,size_16,color_FFFFFF,t_70)
又比如![在这里插入图片描述](https://img-blog.csdnimg.cn/20200220160927809.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zOTU1ODIyNw==,size_16,color_FFFFFF,t_70)
不加this其实也是可以的age就是this.age
年龄相加例子
如果像下面这样调用会报错
因为返回值是一个void
这里要注意 如果希望返回值是一个左值(这样才可以用上面这种形式 函数调用作为左值) 返回值需要是一个引用类型
*
上面就是正确写法 返回的依旧是p1
p1.PlusAge(p2)返回的依旧是一个p1
注意这里返回的必须是引用
空指针访问成员的函数
如果成员函数没有用到this 那么空指针可以直接访问
防止空指针访问成员函数(如果用到了this指针 就不能用空指针访问)
常函数和常对象
const 修饰成员函数
指针指向不能修改
比如不能this=NULL;
但是指针指向的值可以修改
为了使得这个指针指向的值不能修改 实现类似 const Person * const this 使用常函数
如果执意要修改
使用mutable 关键字
常对象 const Person p2;
常对象不能调用普通成员函数 因为普通成员函数中可能会修改属性 所以要用常函数 后面加const
全局函数做友元函数
朋友可以访问私有的属性 类内声明的时候前面加一个friend
外面全局函数的定义就可以访问私有的内容
整个类做友元类
先写类的声明 让编译器知道
要让好基友类访问卧室 需要改成好朋友
注意关键字 class不能少了
让成员函数做友元函数
让单独一个函数做友元 不用整个类做友元
前面加friend 声明一下就行
自定义数组实例
需要myarray.h myarray.cpp main.cpp
首先是类的声明 在myarray.h 中
//
// Created by user1 on 2020/2/21.
//
#include <iostream>
#pragma once
using namespace std;
#ifndef UNTITLED_MYARRAY_H
#define UNTITLED_MYARRAY_H
class myarray{
public:
//无参构造
myarray();//默认100容量
//有参 用户指定容量初始化
explicit myarray(int capacity);
//拷贝构造
myarray(const myarray& array);
//用户操作接口
//根据位置添加元素
void SetData(int pos,int val);
//获得指定位置数据
int GetData(int pos);
// 尾插法
void PushBack(int val);
//获得长度
int GetLength();
//得到容量
int getCapacity();
//析构函数,释放数组空间
~ myarray();
private:
int mCapacity;//数组一共可以容纳多少个元素 一定大于mSize
int mSize;//当前有多少个元素
int *pAddredd;//指向存储数据的空间
};
#endif //UNTITLED_MYARRAY_H
然后是类的定义
在myarray.cpp中
//
// Created by user1 on 2020/2/21.
//
#include <iostream>
#pragma once
using namespace std;
#include "myarray.h"
myarray::myarray() {
this->mCapacity=100;
this->mSize=0;
this->pAddredd=new int [this->mCapacity];//开辟一个数组
}
myarray::myarray(const myarray &array) {
this->pAddredd=new int[array.mCapacity];//这里不能用array的地址去赋值 不然就会出现同个对象析构两次的问题
this->mSize=array.mSize;
this->mCapacity=array.mCapacity;
//在大小一致后 还要将数据拷贝过来
for(int i=0;i<array.mSize;i++)
{
this->pAddredd[i]=array.pAddredd[i];
}
}
//提供数组容量
myarray::myarray(int capacity) {
this->mCapacity=capacity;
this->mSize=0;
this->pAddredd=new int [this->mCapacity];//开辟一个数组
}
myarray::~myarray() {
if (this->pAddredd != NULL)
{
delete[] this->pAddredd;
}
}
void myarray::PushBack(int val) {
//需要判断下越界问题?用户自己处理
this->pAddredd[this->mSize]=val;
this->mSize++;
}
int myarray::GetData(int pos) {
}
void myarray::SetData(int pos, int val) {
this->pAddredd[pos]=val;
}
int myarray::GetLength() {
return this->mCapacity;
}
int myarray::getCapacity() {
return this->mSize;
}
最后是main的调用和类初始化
void test01()
{
//堆区创建数组
myarray *array=new myarray(30);
//下面很重要
myarray * arr2=new myarray(*array);//这是用new方式制定调用拷贝构造的方式1
myarray arr3=*arr2;//构造函数返回的本体 也是调用拷贝构造方式2
myarray *array2=array;//这种形式并不调用拷贝构造 只是声明一个指针和array执行的地址相同 所以不会调用拷贝构造
for(int i=0;i<10;i++)
{
array2->PushBack(i);
}
for(int i=0;i<10;i++)
{
cout<<array2->GetData(i)<<endl;
}
delete array;
cout<<arr2->getCapacity()<<endl;
cout<<arr2->GetLength()<<endl;
//如果我们想要获取设置数组内容 用更符合直观地方式
//cout<<array[20]<<endl;
//array2[0]=1000;
//就需要重载运算符
}
int main()
{
test01();
}
如果我们想要获取设置数组内容 用更符合直观地方式
cout<<array[20]<<endl;
array2[0]=1000;
就需要重载运算符
关于拷贝构造初始化的细节
运算符重载
成员函数写法
全局函数写法
二元重载
用全局来看几元 全局有几个参数就有几元
左移右移的重载的注意点
此时还不能用cout<<调用 必须修改返回值
属性私有后 要使用友元来进行重载
前置后置递增运算符
class myint
{
public:
myint(){};
myint(int a){
this->ma=a;
}
//前置++重载 参数里不用加
myint& operator++()
{
this->ma++;
return *this;
};
//后置++
myint& operator++(int){// 这是前置后置的一个固定搭配 前置返回引用
//先保存好之前的数据
myint tmp=*this;
ma++;
return tmp;
};
friend ostream & operator<<(ostream &cout,myint &p1);
private:
int ma;
};
ostream & operator<<(ostream &cout, myint &p1)
{
cout<<p1.ma<<endl;
return cout;
}
void test6()
{
myint p1;
//cout<<p1++<<endl; 需要重载两处 一个是右移的重载一个是自加的重载
cout<< ++p1<<endl;//自加的重载返回值需要是自身
cout<< p1++<<endl;//后置++
}
这也是为啥前置比后置效率高的原因
前置和后置返回值不同的原因:
第一个例子输出是2
当把前置++的返回值改成引用后
返回的是2,1
为了保证返回之后前置++本体要成2 第一次运算完结果是它本体 而不是一个临时变量 所以要用引用。
而后置数据 返回的是一个临时值 临时数据返回引用可能有报错
指针运算符重载(自定义一个智能指针)
用来托管 指针对象 就不用显示地delete
维护住这个指针
new person的写法也要改
解释上述代码 sp对象的构造函数是个有参的构造
里面维护的指针来自于new的那个Person对象
sp对象是开辟到栈上的会自动释放
所以智能delete操作就可以放进这个智能指针的析构中,就不管官Person的析构
对*进行重载
赋值运算符重载
情况2:
开辟了动态内存
(加入析构后)这时候就会产生错误
等号运算符 会进行简单值传递 p1和p2的pname都指向同一个对象 会被重复析构 就会崩溃。
此时就需要重载等号运算符
在里面重复开辟一个新的空间
特殊情况连续的等号的处理
原始的 p2==p1返回值是个void
修改返回值
要返回一个引用
[] 运算符的重载
上面的实现返回一个 值 不能作为左值 所以会报错
关系运算符重载
实现这种操作
实现不等号
函数调用运算符重载
对小括号的重载 构成仿函数
比如这种形式
不要重载||和&&
重载后可能会失去短路特性
自定义string类
cin的重载
两种等号的重载
以及析构
[]的重载
字符串拼接
也是两个版本
关系运算符的重载
继承
能够拥有basepage的所有属性
继承方式
继承方式是对于子类来说的 不可能从私有—》公有
如果父类中是私有的 永远是私有 但通过改变继承方式可以使的父类中的公有变成私有
继承中的对象模型
利用下述工具显示继承结构
输入 里面的son是类名 报告单个类的布局 test.cpp是属于哪个文件 要进行该文件路径进行操作
cl /d1 reportSingleClassLayoutSon test.cpp
继承中的构造和析构顺序
=,构造,析构不能被继承
继承中同名成员的处理
就近原则
若想调用父类的 加一个成员的作用域
必须显式说明
继承中静态成员的处理
如果子类父类中都有 则也是就近原则或者显式调用
Son::func() //静态成员函数的调用
多继承
二义性问题 俩爹都有同名成员
解决方法1 命名空间
菱形继承
解决二义性的方法
造成了资源浪费
菱形继承的解决方法 叫虚继承
虚继承后的图
可以看到m_age只出现了一次 并且多了vbptr 这个虚指针
虚基类指针指向了虚基类表
虚基类表中有偏移量
虚表是一个数组 偏移量是8
0+8=8 找到m_age 4+4=8找到m_age 所以只要一份m_age的存储 数据没有浪费
现在没有二义性 可以直接访问
虚继承工作原理
(1) 找偏移量操作
1.&st 先取得对象地址
2.(int*)&st 强转成 int类型 用来后续改变步长操作
3. *(int *)&st 取星操作后才能得到真正的虚表
4.(int*)*(int *) &st+1 找到虚表后 改变类型用来改步长
5.改变步长后 继续int 转变类型 再取* 得到真正的8 找到偏移量操作完成
(2)得到m_age的操作
1.
2.改变类型后把之前得到的偏移量加上去
3.此时再改变类型 是个animal类型
4.然后整体括起来 ->m_age
就找到了
多态
父类的引用或者指针 指向子类对象
静态编联和动态编联
class Animal{
public:
void speak(){
cout<<"animal talking";
}
};
class Cat:public Animal{
public:
void speak()
{
cout<<"cat talking";
}
};
void dospeak(Animal & ani)
{
ani.speak();
}
void test1(){
Cat cat;
dospeak(cat);
}
上面就是一个静态编联的例子
早就在编译阶段就绑定了animal类 输出是‘animal talking’
如果希望锚说话 就不能提前绑定函数的地址 需要在运行时候确定函数的地址
此时就叫动态联编
把父类中的speak改成虚函数
}
class Animal{
public:
void virtual speak(){//这里加一个virtual
cout<<"animal talking";
}
};
class Cat:public Animal{
public:
void speak()
{
cout<<"cat talking";
}
};
void dospeak(Animal & ani)
{
ani.speak();
}
void test1(){
Cat cat;
dospeak(cat);
}
此时输出就是‘cat talking’ 此时发生了动态多态
此时 Animal &animal=cat 父类的引用指向了子类对象
多态原理解析
不加virtual
class Animal{
public:
void speak(){
cout<<"animal talking";
}
};
class Cat:public Animal{
public:
void speak()
{
cout<<"cat talking";
}
};
void dospeak(Animal & ani)
{
ani.speak();
}
void test1(){
Cat cat;
dospeak(cat);
}
void test3()
{
Animal an1;
cout<< sizeof(Animal)<<endl;
}
Animal 此时大小是1 一个空类
如果加上virtual
输出会是4 此时Animal类已经发生了变化
类此时内部包含了一个虚指针
指向了animal内部函数的地址
cat继承之后 但不写speak
继承虚指针和虚表 同时改变虚指针的指向
重写了speak之后
重写必须返回值参数都相同
将父类虚表中的内容进行替换
用父类指针或者引用子类对象时 此时就调用虚表cat中的speak
多态案例1 计算器
原始状态 只有加法和减法
#include <iostream>
using namespace std;
class Calculator{
public:
Calculator(){};
void setv1(int v){
this->val1=v;
};
void setv2(int v){
this->val2=v;
};
int get_result(string oper){
if (oper=="+")
{
return val1+val2;
}
else if (oper=="-"){
return val1-val2;
}
};
int val1;
int val2;
};
void test1()
{
Calculator ca1;
ca1.setv1(10);
ca1.setv2(10);
cout<<ca1.get_result("+");
}
int main() {
test1();
return 0;
}
然后要添加乘法和除法功能
此时要利用多态实现
class Calculatorbase{
public:
int virtual get_result(){
return 0;
};
void setv1(int v){
this->val1=v;
};
void setv2(int v){
this->val2=v;
};
int val1;
int val2;
};
//加法计算器
class Calculatorplus:public Calculatorbase{
public:
int get_result(){
return val1+val2;
};
};
//减法计算器
class Calculatorsub:public Calculatorbase{
public:
int get_result(){
return val1-val2;
}
};
void test4()
{
Calculatorbase * abc=new Calculatorplus;//声明了一个加法计算器 发生多态 父类指向了子类
abc->setv1(10);
abc->setv2(12);
cout<<abc->get_result()<<endl;
}
int main() {
test4();
return 0;
}
此时要添加新功能 不需要改原来的代码 只要加新的功能的子类代码
抽象类和纯虚基类
我们发现父类中的 get_result 没有任何意义 此时进行修改
将不需要的函数实现都删掉 直接等于0 变成纯虚函数
1.如果父类中有纯虚函数 那么子类继承父类必须实现这个纯虚函数
2.如果父类中有纯虚函数 父类此时就不能实例化了
3.这个类有个纯虚函数,也叫作抽象类
虚析构和纯虚析构函数
原始版本
class Animal{
public:
void virtual speak()
{
cout<<"animal talking";
}
~Animal(){
cout<<"animal ~";
}
};
class Cat:public Animal{
public:
Cat(const char * name)
{
this->m_Name=new char[strlen(name)+1];
strcpy(this->m_Name,name);
}
void virtual speak()
{
cout<<"cat talking";
}
char * m_Name;//名字
~Cat(){
cout<<"cat ~"<<endl;
if (this->m_Name!=NULL)
{
delete [] this->m_Name;
this->m_Name=NULL;
}
}
};
void test5()
{
Animal * animal=new Cat("TOM");
animal->speak();
delete animal;
}
此时会析构掉animal
这就会导致 没有释放干净 这种情况下就需要一个虚析构
普通的析构 不会调用子类的析构 要一个虚析构解决该问题
在animal的析构之前加一个~ 就变成了虚析构
此时输出为
先调用了cat的析构又调用了animal的析构 然后是纯虚析构 一样的方式=0
直接改成这样会报错:无法解析的外部命令
纯虚析构的注意点:
如果直接把父类的析构改成纯虚 会只有声明没有实现 就会有无法解析的问题
所以纯虚析构需要声明还需要实现:类内声明 ,类外实现!!
class Animal{
public:
void virtual speak()
{
cout<<"animal talking";
}
virtual ~Animal()=0;
};
Animal::~Animal() {
}
此时输出
如果此时有纯虚析构 这个类也叫作抽象类
向上类型转换向下类型转换
安全不安全指的是指针的寻址范围
把animal转成cat cat的寻址范围比animal大 可能会操纵到不是自己的内容
记一下家族谱 向下转换
基类转成子类 将是安全的
这就叫向上类型转换
如果发生了多态就一定是安全的
一开始寻址范围就是子类的大范围
游戏多态实例
首先设计类的属性和功能
模板
类型多,逻辑又非常相似
//类型参数化,泛型编程—模板技术
会自动进行类型替换 一开始参数不确定
必须要能够推导出来 ,下面就是一个推导不出来的二义性错误
调用的时候明确说明类型
class也可以写成typename 是等价关系的
使用时尽量使用显式指定类型 不用去自动推导
排序实例
template <class T> //告诉编译器 下面出现了T不要报错 T是一个通用类型
void myswap(T &a,T &b){
T temp=a;
a=b;
b=temp;
}
template <class T>
void mySort(T arr[],int len)
{
for (int i =0;i<len;i++)
{
int max=i;
for (int j=i+1;j<len;j++)
{
if (arr[max]<arr[j])
{
max=j;
}
}
if (max!=i)
{
myswap(arr[max],arr[i]);
}
}
}
void test6()
{
char charArr[]="helloworld";
int num=sizeof(charArr)/ sizeof(char);//求数组长度的通用方法
mySort(charArr,num);
}
char 类型会按照asicii码来排序
普通函数和函数模板的区别
函数模板也能重载
用模板不用多进行一步隐式类型转换,是更好的匹配
模板机制内部原理
自定义的数据类型可能就无法处理
会进行两次编译!
函数模板的局限性
解决方法:需要模板的具体化
模板的具体化
如下面的例子,普通类型是可以使用模板比较的
放入自定义类型后 就会报错 能知道是Person类 但无法进行比较
//通过具体化自定义数据类型,解决上述问题
class Person{
public:
Person(string name,int age)
{
m_name=name;
m_age=age;
}
string m_name;
int m_age;
};
template <class T>
bool myCompare( T &p1,T &p2)
{
if (p1==p2)
{
return true;
}
return false;
}
template<> bool myCompare <Person>(Person &p1,Person &p2)//此时类型已经不是T
{
if (p1.m_age==p2.m_age)
{
return true;
}
return false;
}
如果Person对两个模板都能匹配优先选择具体化的模板
类模板的使用
使用和函数模板类似
template <class T1,class T2>
class Person2{
public:
Person2(T1 age, T2 name)
{
m_age=age;
m_name=name;
};
T1 m_age;
T2 m_name;
};
区别1 :类模板不支持自动类型推导
必须显式指定类型
区别2: 类模板可以有默认类型
成员函数的创建时机
成员函数一开始不创建出来而是运行时创建
class Person1
{
public:
void showPerson1()
{
cout<<"diao Person1"<<endl;
}
};
class Person3
{
public:
void showPerson3()
{
cout<<"diao Person3"<<endl;
}
};
template <class T>
class myclass
{
public:
T obj;
void func1()
{
obj.showPerson1();
}
void func2()
{
obj.showPerson2();
}
};
void test8()
{
myclass <Person1> m;
m.func1();
}
m.func2() //将会报错
类模板做函数的参数
方法1:指定传入类型
调用
方法2
参数模板化
不告诉具体类型了 函数模板配合类模板实现
方法3
整体类型化 把整个Person对象进行了模板化
查看T的类型
类模板的继承问题
正确方法 具体化类型
更灵活的写法,把子类也变成模板
把一个T2传递给父类用于初始化分配内存
这样子之后就能由用户来指定类型
类模板类外实现成员函数
类内实现的方法
类外实现
普通类的类外实现
模板类就是多了一个
和
再次强调 类模板生成对象的时候使用的时候必须要显式指定类型
类模板的分文件编写代码
1.头文件 Person.h
//
// Created by user1 on 2020/2/24.
// 头文件声明
#ifndef FENWENJIAN_PERSON_H
#define FENWENJIAN_PERSON_H
#pragma once
using namespace std;
#include <iostream>
#include <string>
template <class T1,class T2>
class Person{
public:
Person(T1,T2);
void showPerson();
T1 mname;
T2 mage;
};
#endif //FENWENJIAN_PERSON_H
- Person.cpp
#include <iostream>
#include "person.h"
using namespace std;
//
// Created by user1 on 2020/2/24.
template <class T1,class T2>
Person<T1,T2>::Person(T1 name,T2 age) {
this->m_name=name;
this->mage=age;
}
template <class T1,class T2>
void Person<T1,T2>::showPerson() {
cout<<this->mname<<" "<<this->mage<<endl;
}
3.main.cpp
#include <iostream>
#include "person.h"
using namespace std;
int main() {
std::cout << "Hello, World!" << std::endl;
Person <string,int>p1("dasha",12);
p1.showPerson();
return 0;
}
上面的写法会报错 两个无法解析的外部命令
把#include “person.h” 改成 #include “person.cpp”
原因
因为在链接的时候找不到代码
main调用头文件的时候只看到头文件 类模板的成员方法并不会生成源文件的代码 类模板中的成员函数一开始不会创建而是运行时创建出来的,所以链接的时候找不到这些代码。
而调用person.cpp的时候就相当于把实现代码都放到main里面去了
所以模板一般不做分文件编写,都写到头文件上去
头文件就改成了.hpp的形式
.hpp就是用来表示模板类的头文件 约定俗成
总结
类模板的友元函数
友元函数是为了访问一个类中的私有属性
下面是原始状态
友元函数类外实现 --难
按下面的写法将会报错 这个友元函数的声明编译器看成了一个普通全局函数 只不过调用了模板类,而它的类外实现 编译器却看成了一个模板函数,这样子编译器优先去找普通全局函数的实现,但是找不到!!
解决方法:
1.利用空参数列表告诉编译器这个是模板函数的声明
但还是有问题!!
必须提前告诉编译器有这个声明 而不能仅仅在类内声明
这样写还是有问题!
需要让编译器看到Person的声明
类模板的应用 写一个通用数组类
#include <iostream>
using namespace std;
//
// Created by user1 on 2020/2/24.
//
template <class T>
class Myarray {
public:
Myarray( )
{
};
explicit Myarray(int capacity)//防止隐式类型转换
{
this->mcapacity = capacity;
this->msize = 0;
this->paddress = new T[this->mcapacity];
};
Myarray(const Myarray &array) {
this->msize = array.msize;
this->mcapacity = array.mcapacity;
this->paddress = new T[this->mcapacity];
for (int i = 0; i < msize; i++) {
this->paddress[i] = array[i];
}
}
~Myarray() {
if (this->paddress != NULL) {
delete[] this->paddress;
this->paddress = NULL;
}
}
//赋值操作符重载
Myarray& operator=(Myarray &arr)
{
//先判断原始数据 有就清空
if(this->paddress!=NULL)
{
delete [] this->paddress;
this->paddress=NULL;
}
for (int i = 0; i < msize; i++) {
this->paddress[i] = array[i];
}
return *this;
}
//[]重载
T &operator[](int index)
{
return this->paddress[index];
}
//尾插法
void pushback(T val)//不能使用引用这里
{
this->paddress[this->msize]=val;
this->msize++;
}
//获取容量
int getsize()
{
return this->msize;
}
//获取cap
int getcap()
{
return this->mcapacity;
}
private:
T *paddress;//指向堆区指针
int mcapacity;
int msize;
};
C++类型转换
尽量少用类型转换
静态类型转换
派生类转换为基类是安全的,反之不安全
必须要有父子关系的类之间才能转换
引用也可以转换
动态类型转换
1.基础类型不能动态转换
因为只要失去精度或者不安全都不可以转换
2.子类和父类之间转换 父类转子类会报错(如果没有发生多态)
如果发生了多态(比如父类指针指向子类),那么可以让基类转为派生类,向下转换
这样就能正常父转子,因为创建空间的时候就是按子类空间创建的!
常量转换
用于移除类型的const属性 或者加上const属性
只能用于引用和指针的变量
重新解释转换(没用)
非常不安全!!
异常
C++异常处理的优势
C++中异常的基本使用
先来看一下早期C的处理方式
C++的处理
抛出的-1是一个int类型所以要catch(int)
如果不用异常捕获 程序会被粗暴地终止(计算机对异常进行处理了)
用了异常捕获 得到了异常 将检测和处理阶段分开了
也可以抛出其他类型的异常
并且要知道异常必须要处理 要catch住
要写一个catch double类型去接
怎么处理那么多类型的异常:
C++提供了一种机制可以一试多用处理多种类型的异常
自定义异常类
myException()就是一个匿名对象 对象不需要起名字了
捕获自定义异常,并且调用成员函数
总结
栈解旋
异常的接口声明
为了让异常可读性更高
抛出3.14就会报错
异常变量的生命周期
用引用能节省开销
用指针来控制 比较繁琐 还是引用好
异常的多态使用
父类的引用指向子类的对象 出现多态
因此实现的时候只需要catch一个基类就行了,使得代码更简洁
系统标准异常
what方法打印的就是上面的我们设置的“年龄越界了”这句话
自己实现一个异常类
使用系统的exception作为基类
主要是重写what方法和析构方法
string转 char*使用.c_str()
输入输出流
标准io:
输入文件===键盘
输出文件===屏幕
文件io:
串IO:
标准输入
缓冲区概念
一次读入一个字符
第一次拿走了缓冲区中的a,后来拿走了s
第三次拿到了换行符
cin.get()读取字符串时不会把换行符拿走,把它遗留在缓冲区中
getline是会把换行符读取并且扔掉 并且等待下次输入
没有参数代表忽略一个字符
缓冲区中拿走之后又放回去了
标准输入流案例
把偷窥完的数字放回缓冲区后又重新冲进num中进行输出
2.
错误案例
输入一个char类型 标志类就会毁坏 出现严重错误
需要用cin.fail()
此时输入字符类型就会输出不正常的标志位
标准输出流
输出格式控制
要先使用该头文件
前面会有18个空格
fill 空格填充成*
setf 设置输出格式状态
把十进制改成了十六进制
卸载十六进制安装八进制
文件读写操作
把内容输出到文件中。
从文件中读数据
第一种方式比较好
来源:CSDN
作者:yuyijie_1995
链接:https://blog.csdn.net/weixin_39558227/article/details/104321685