目录
前言
一、类继承
1.1 继承概念(基类、派生类)
1.2 类创建顺序
1.3 继承权限
1.4 重定义(遮蔽)
二、虚函数
2.1 纯虚函数、虚析构
2.2 重写(覆盖)
2.3 多态
三、多重继承
3.1 虚基类、虚继承、虚表
3.2 计算类大小(继承关系)
3.3 多次继承初始化顺序
四、C++异常机制
4.1 异常、检测步骤、处理异常
4.2 内联函数、隐式转换与explicit
4.3 虚析构、纯虚析构
五、模板
5.1 模板函数
5.2 类模板
六、STL(标准模板)
6.1 STL 6大组件
6.2 容器(序列、关联)
总结
前言
例如:本章主要总结了类继承的权限,格式、虚函数、虚基类、虚析构、纯虚函数、纯虚析构的作用与区别;重写、重定义、重载的区别及代码举例;多重继承的格式、初始化列表顺序及注意事项以及多次继承下,如何计算所占内存空间大小;C++的异常处理、包括检测、抛出、处理以及多个多种异常的处理情况;同时对模板函数进行了细致分类,举例如何操作;STL标准模板库的 6大组件、容器介绍,明天将进行深入探索!!
提示:以下是本篇文章正文内容,下面案例可供参考
一、类继承
继承的概念:继承允许依据一个类来定义另一个类,使创建和维护一个应用程序变得更容易
优点:提高代码的复用率、执行效率
当创建一个类时,不用重新编写新的数据成员、函数,只需指定新建的类继承了一个已有的类的成员即可。
1.1 继承概念(基类、派生类)
当创建一个类时,指定新建的类继承了一个已有的类的成员即可,已有的类称为基类或父类,新建的类称为派生类或子类。
一个子类可以有多个父类,它继承了多个父类的特性。继承可以实现面向对象代码重用,继承也是实现多态的必要语法
使用方法:class 子类名 : 继承方式 父类名1,继承方式 父类名2,继承方式 父类名3....
示例1:
#include
using namespace std;
class Food
{
public:
};
//添加继承关系:冒号: 继承权限 父类
//kfc:子类 派生类
//Food:基类 父类
class KFC : public Food
{
string chips;
public:
KFC(string chips)
{
this->chips = chips;
}
void show()
{
cout<<"kfc: "<
思考:引伸1 如果父类中定义了同名的变量呢?
引伸2:子类中没有成员变量,父类中定义?
1.2 类创建顺序
父子类创建顺序:先父后子
1.当父类没有显示的定义构造,那么会调用父类默认的构造函数
2.父类显示的定义了构造,那么会调用父类显示的无参的构造
3.先创建父类,后创建子类
示例:
#include
using namespace std;
class Food
{
public:
string chips;
Food(string chips):chips(chips)
{
cout<<"food create"<chips = chips;
cout<<"kfc create"<
using namespace std;
class Food
{
public:
string chips;
Food(string chips):chips(chips)
{
cout<<"food create"<chips = chips;
cout<<"kfc create"<
1.3 继承权限
继承权限:
public继承 子类继承后的访问权限不变
protected继承 子类继承后,父类中的public变成protected,其它不变(protected:只有子类可以用),类外不可用
private继承 子类继承后,父类中的public和protected变成private,其他不变
1.4 重定义(遮蔽)
继承时的重定义(遮蔽问题)
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,是派生类中使用该成员时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
注意:基类成员函数和派生类成员函数不构成重载
基类成员和派生类成员名字一样时会造成遮蔽,不管函数的参数如何,只要名字一样就会造成遮蔽。
基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
重定义(遮蔽):1.继承关系下(父子类中) 2.函数名相同 (参数是否一样 返回值是否一样 )
重载(overload):1.同一个类中 2.函数名相同 3.参数列表不同(参数个数,参数类型)
父类中的函数 和 子类中的函数 不可以构成函数的重载
调用:
1、如果子类中调用函数,父子类中都有时,优先使用子类自己的(内部优先原则)
如果要调用父类成员函数、变量,调用方法为:类名::函数名/变量名
2、如果子类对象没有,则会调用父类的
二、虚函数
c++中定义成员函数时 前边加关键字virtual 这个函数就是虚函数 virtual void eat();
2.1 纯虚函数、虚析构
纯虚函数、抽象类
1.格式:
virtual 返回值类型 函数名(形参列表) = 0 ; //声明此函数是纯虚函数
2.特点(记住)
1)在基类中没有实现
2)必须在派生类中加以实现 (需要派生类实现其函数定义)
3)有纯虚函数的类相当于接口 抽象类 不能直接实例化
3.抽象类:包含纯虚函数的类 不允许创建抽象类对象。
4.抽象类作用:就是用来被继承的,纯虚函数就是用来被重写的。
5.定义纯虚函数的目的: 是为了可以通过抽象类的指针去调用子类对象中重写的函数。
示例代码1:
#include
using namespace std;
//抽象类:包含了纯虚函数,那么这个类就变成了抽象类
//特点:1.不可以单独的实例化(创建)对象,
//作用:用来被继承,重写纯虚函数
//2.有类继承了抽象类之后,必须重写抽象类中的纯虚函数
class Person
{
public:
//纯虚函数: 虚函数 = 0;
virtual void show(int a) = 0;
};
class B : public Person
{
public:
void show(int a)
{
cout
2.2 重写(覆盖)
重写 override: 子类中定义和父类中虚函数同名,同返回值类型,同参数列表的函数,叫重写。
对比重载:overload 在同一个类中,函数名相同,参数列表不同,返回值无所谓,叫重载.
代码示例:重写
#include
using namespace std;
//函数的重写override:1.发生在继承关系中 2.函数名 3.参数列表相同
//函数的重载overload: 1.发生在同一个类中 2.函数名 3.参数列表不同
class Person
{
public:
//虚函数:virtual
virtual void show()
{
cout<<"person show..."<
2.3 多态
多态:指的是同一名字的事物可以完成不同的功能。
编译时多态:函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;
运行时多态:和继承、虚函数等概念有关,重写
c++的虚函数,主要作用是 实现了运行时多态。
父类中虚函数的实现,是为子类提供默认的函数实现;子类可以重写父类的虚函数实现子类的特殊化。
#include
using namespace std;
//一个事务(函数),多种表现形态(多个不同的功能)
class Hero
{
public:
//进攻的函数:提供的默认功能
virtual void attack()
{
cout<<"小刀拉拉"<attack();//子类重写的
h->fun();//父类普通的函数
h->fun1();//不可以的,访问子类特有的函数,只能反问子类重写的
h = new FaShi;
h->attack();//法师类 重写函数
return 0;
}
多态实现步骤
1)父类中包含虚函数
如果使用这种指针方式,若调用父类普通函数,则调用函数时依然是父类的方法;
若调用父类的虚函数,调用函数时调用的是子类重写之后的函数。
2)父类指针指向子类对象
父类* 指针 = new 子类();
Shape* shape = new Rectangle();
使用这种方式,父类的指针可以指向自己的所有派生类对象。
Student xiaoming;
Person& p1 = xiaoming;//父类引用指向子类对象
Person* p2 = &xiaoming;//指针的用法同引用
**********父类类型的指针 可以指向子类类型的对象*******重要
1.画图演示
2. 说明调用顺序 1)看父类有没有虚函数
2)如果有,去子类中找有没有重写,有就执行子类重写的方法;
3)如果子类没有重写,就去执行父类的虚函数。
注意:
父类指针 指向子类对象时,可以调用父类继承的和子类重写的
不能使用 子类特有的变量和函数
引用也可以实现多态
由于引用类似于常量,只能在定义的同时初始化,不能再引用其他数据,所以必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。
当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。
不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以谈及多态时一般是说指针。
三、多重继承
3.1 虚基类、虚继承、虚表
虚基类:以virtual声明的基类 使得在继承间接共同基类时 只对共同的基类初始化一次
虚继承:格式:class 派生类名:virtual 继承方式 基类名
这种继承称作虚继承,只能在派生的时候声明虚基类
作用:
1、为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
2、在继承方式前面加上 virtual 关键字就是虚继承,使得在继承间接共同基类时,只对共同的基类初始化一次
3、消除二义性
***************************错误代码***********************
class A
{
protected:
int money;
public:
A(int money):money(money){ cout<<"a create"<
using namespace std;
//菱形继承
//虚继承的那个类,被叫做虚基类
class A
{
protected:
int money;//1
public:
A(int money):money(money){cout<<"A"<
菱形继承,是非常不科学的继承逻辑,类的关系设计的合理,是不可能出现菱形继承的。
但是C++的语法允许我们出现菱形继承,并且菱形继承逻辑必然出问题。
虚函数表(虚表):当类中定义虚函数后,编译器会生成一个虚函数表(虚表,相当于一堆函数指针),
父类指针指向子类对象时,通过虚表去锁定调用子类中重写的函数。
class Shape//抽象类,因为有纯虚函数
{
public:
virtual int getPerimeter() = 0;//纯虚函数
virtual string getType() = 0;
};
int main()
{
//输出4,因为类中有虚函数,所以会生成一个指向虚表的指针
cout<
3.2 计算类大小(继承关系)
计算类所占空间的大小:
1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
空类可以实例化,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.所以sizeof( A )的大小为1
2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都不占用类对象的存储空间的。
3、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr,该指针指向虚函数表VTable;
4、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐;
6、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(指针指向虚函数表,多继承下可能不止一个)+vbptr指针(指针指向虚基表,多继承下可能不止一个)+编译器额外增加的字节。
示例1:
class G : public E
{
virtual int VirtualMemFuncTest1(int para)=0;
int m_Int;
};
class H : public G
{
int m_Int;
};
sizeof( G ) = ?
sizeof( H ) = ?
可以看出子类的大小是本身成员的大小再加上父类成员的大小.如果父类还有父类,也加上父类的父类。
sizeof( G ) = 16
sizeof( H ) = 20
示例2:
class D
{
D(){}
virtual ~D() {}
virtual int VirtualMemFuncTest1()=0;
virtual int VirtualMemFuncTest2()=0;
virtual int VirtualMemFuncTest3()=0;
};
class I : public D
{
virtual int VirtualMemFuncTest1()=0;
virtual int VirtualMemFuncTest2()=0;
};
sizeof( I ) = ?
父类子类工享一个虚函数指针,虚函数指针保留一个即可。
sizeof( I ) = 4
总结:
空的类也是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。
(一)类内部的成员变量:
普通的变量:是要占用内存的,但是要注意内存对齐(这点和struct类型很相似)。
static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区。
从父类继承的变量:计算进子类中。
(二)类内部的成员函数:
非虚函数(构造函数、静态函数、成员函数等):不占用内存。
虚函数:要占用4个字节(32位的操作系统),用来指定虚拟函数表的入口地址。跟虚函数的个数没有关系。父类子类工享一个虚函数指针。
构成对象本身的只有数据,任何成员函数都不隶属于任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针。成员函数为该类所有对象共享,不仅是处于简化语言实现、节省存储的目的,而且是为了使同类对象有一致的行为。同类对象的行为虽然一致,但是操作不同的数据成员。
3.3 多次继承初始化顺序
初始化顺序和继承的顺序为准,不按照初始化列表来, 和初始化成员变量时相同。
#include
using namespace std;
class A
{
public:
A(int a){cout<<"A create"<
四、C++异常机制
4.1 异常、检测步骤、处理异常
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。
检测异常的三个步骤:检查(try) 抛出(throw) 捕捉(catch)
try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
try...catch语法:
try
{
被检查语句
}
catch(异常处理的类型 变量名)
{
进行异常处理的语句;
}
**********************示例1:*******************
#include
using namespace std;
int mydiv(int a,int b)
{
if(b==0) //分母为0,说明发生了异常
{
cout<<"throw....."<
using namespace std;
class ExceptA//专门用来表示异常的类
{
public:
void show()
{
cout<<"异常A"<
4.2 内联函数、隐式转换与explicit
内联函数格式:
class Person
{
public:
inline void show()//内联函数的声明和定义不能分开
{
}
};
内联函数、普通函数区别:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。好处就是省去切换函数所需要的开销。
比较小的函数(三两行代码的函数),又会被频繁调用的函数,建议定义成内联函数。
如果编译器判断函数不适合展开,不会将函数当做内联函数处理。
声明内联函数,这是向编译器建议把函数在调用处展开,最终有编译器决定要不要展开。
#include
using namespace std;
inline int max(int a, int b)//内联函数,内联函数的声明和定义不能分开。
{
if(a>b)
return a;
else
return b;
}
int main()
{
cout<
4.3 虚析构、纯虚析构
虚析构和纯虚析构
问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数;
解决方式:将父类中的析构函数改为虚析构或者纯虚析构;
示例代码1:
#include
using namespace std;
class A
{
public :
A(){cout<<"A create"<
explicit:explicit用来防止由构造函数定义的隐式转换。
**************************示例1:***********************************
#include
#include
using namespace std;
class Person
{
private:
int age;
string name;
public:
Person()
{
cout<<"no arg"<
using namespace std;
class Person
{
private:
int age;
string name;
public:
Person()
{
cout<<"no arg"<
五、模板
模板:泛型编程(可以将类型(基本类型、自定义类型)当做参数, 进行传递)
5.1 模板函数
只适用于函数体相同 参数个数相同 参数的类型不同的情况
*************************示例1:函数模板
#include
using namespace std;
//模板函数
template //<>放的是 类型参数 int string char.....
void show(T s,T a)
{
cout<<"T .... "<("hello","world");//<>放的是类型参数
show(10,20);//<>放的是类型参数
return 0;
}
*************************示例2:类型当做返回值类型
#include
using namespace std;
//模板函数
template //<>放的是 类型参数 int string char.....
T show(T s,T a)
{
cout<<"T .... "<("hello","world")<放的是类型参数
cout<(10,20)<放的是类型参数
return 0;
}
*************************示例3:类类型
#include
using namespace std;
class Person
{
int age;
public:
Person(int age):age(age){}
void show(){cout/Person
void div(T a,T b,U num)//()参数列表 形参
{
a.show();
b.show();
T c(122);
c.show();
}
int main()
{
Person p(1);
//模板类型:自定义类型
div(Person(12),p,10);
return 0;
}
函数模板和普通函数的区别:
1) 普通函数可以自动类型转换 函数模板必须显示指定类型
2) 同名的普通函数和模板函数出现,优先调用普通函数
3) 函数模板也可以发生重载
5.2 类模板
使用方法:显示指定类型 Person p(10,"王美丽");
泛型编程:类型参数化
注意:类模板不建议分文件编写,所有的函数实现都写在头文件中
******************************示例1:成员变量
#include
using namespace std;
template
class Person{
private:
T1 age;
T2 name;
public:
Person(T1 a,T2 n):age(a),name(n){}
void show(){
cout << name << ":" << age << endl;
}
};
int main()
{
Person p(10,"王美丽");
p.show();
return 0;
}
******************************示例2:
#include
using namespace std;
template
class Person
{
public:
Person(int age):age(age)
{
}
void eat(T food)
{
cout<* p = new Person(10);
p->eat("鸡排");
delete p;
}
六、STL(标准模板)
STL标准模板库:即标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能;该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。
为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。
1.在 C++ 中如果定义一个数组,可以采用如下方式:int a[n];
这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。
2.还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量:
int *p = new int[n];
这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。
但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行扩容:
3.新申请一个较大的内存空间,即执行int * temp = new int[m];
将原内存空间的数据全部复制到新申请的内存空间中,即执行memecpy(temp, p, sizeof(int)*n);
将原来的堆空间释放,即执行delete [] p; p = temp;
4.完成相同的操作,如果采用 STL 标准库,则会简单很多,因为大多数操作细节将不需要程序员关心。
示例:
vector a; //定义 a 数组,当前数组长度为 0,但和普通数组不同的是,
此数组 a 可以根据存储数据的数量自动变长。
//向数组 a 中添加 10 个元素
for (int i = 0; i < 10 ; i++)
a.push_back(i)
//还可以手动调整数组 a 的大小
a.resize(100);
a[90] = 100;
//还可以直接删除数组 a 中所有的元素,此时 a 的长度变为 0
a.clear();
//重新调整 a 的大小为 20,并存储 20 个 -1 元素。
a.resize(20, -1)
对比以上两种使用数组的方式,使用 STL 可以更加方便灵活地处理数据。所以,只需要系统地学习 STL,集中精力去实现程序的功能,而无需纠结某些细节如何用代码实现。
优点:安全、方便、快捷简单
6.1 STL 6大组件
STL:由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的。
STL的组成
含义
容器
一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。
算法
STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中。
迭代器
在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。
函数对象
如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。
适配器
可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。
内存分配器
为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。
C++ STL头文件:
6.2 容器(序列、关联)
STL容器:它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。
两大类:序列容器 关联容器
STL中的容器有:
序列容器 vector(向量容器) list(双向链表) deque(双端队列容器)
关联容器 set(单重集合) multiset(双重集合) map(单重映射表) multimap(多重映射表)
容器适配器 stack(栈) queue(队列) prority_queue(优先级队列)
1、顺序容器(序列式容器):每个元素都有固定位置——取决于插入时机和地点,和元素值无关,vector、deque、list;
vector : 将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),
数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时;
List : 双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),
在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
Deque : 是double-ended queue的缩写,可以随机存取元素(用索引直接存取),
数组头部和尾部添加或移除元素都非常快速。但是在中部安插元素比较费时;
STL 容器种类和功能
容器种类
功能
序列容器
主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。
排序容器
包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。
哈希容器
C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。
STL中的容器有:
序列容器 vector(向量容器) list(双向链表) deque(双端队列容器)
关联容器 set(单重集合) multiset(双重集合) map(单重映射表) multimap(多重映射表)
容器适配器 stack(栈) queue(队列) prority_queue(优先级队列)
1、顺序容器(序列式容器):每个元素都有固定位置——取决于插入时机和地点,和元素值无关,vector、deque、list;
vector : 将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取),
数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时;
List : 双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)),
在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针;
Deque : 是double-ended queue的缩写,可以随机存取元素(用索引直接存取),
数组头部和尾部添加或移除元素都非常快速。但是在中部安插元素比较费时;
总结
这里对文章进行总结:
当创建一个类时,指定新建的类继承了一个已有的类的成员即可,已有的类称为基类或父类,新建的类称为派生类或子类。 一个子类可以有多个父类,它继承了多个父类的特性。继承可以实现面向对象代码重用,继承也是实现多态的必要语法 使用方法:class 子类名 : 继承方式 父类名1,继承方式 父类名2,继承方式 父类名3.... 示例1: #includeusing namespace std; class Food { public: }; //添加继承关系:冒号: 继承权限 父类 //kfc:子类 派生类 //Food:基类 父类 class KFC : public Food { string chips; public: KFC(string chips) { this->chips = chips; } void show() { cout<<"kfc: "< 思考:引伸1 如果父类中定义了同名的变量呢?
引伸2:子类中没有成员变量,父类中定义?1.2 类创建顺序
父子类创建顺序:先父后子 1.当父类没有显示的定义构造,那么会调用父类默认的构造函数 2.父类显示的定义了构造,那么会调用父类显示的无参的构造 3.先创建父类,后创建子类 示例: #includeusing namespace std; class Food { public: string chips; Food(string chips):chips(chips) { cout<<"food create"< chips = chips; cout<<"kfc create"< using namespace std; class Food { public: string chips; Food(string chips):chips(chips) { cout<<"food create"< chips = chips; cout<<"kfc create"< 1.3 继承权限
继承权限: public继承 子类继承后的访问权限不变 protected继承 子类继承后,父类中的public变成protected,其它不变(protected:只有子类可以用),类外不可用 private继承 子类继承后,父类中的public和protected变成private,其他不变1.4 重定义(遮蔽)
继承时的重定义(遮蔽问题) 如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,是派生类中使用该成员时,实际上使用的是派生类新增的成员,而不是从基类继承来的。 注意:基类成员函数和派生类成员函数不构成重载 基类成员和派生类成员名字一样时会造成遮蔽,不管函数的参数如何,只要名字一样就会造成遮蔽。 基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。 重定义(遮蔽):1.继承关系下(父子类中) 2.函数名相同 (参数是否一样 返回值是否一样 ) 重载(overload):1.同一个类中 2.函数名相同 3.参数列表不同(参数个数,参数类型) 父类中的函数 和 子类中的函数 不可以构成函数的重载 调用: 1、如果子类中调用函数,父子类中都有时,优先使用子类自己的(内部优先原则) 如果要调用父类成员函数、变量,调用方法为:类名::函数名/变量名 2、如果子类对象没有,则会调用父类的二、虚函数
c++中定义成员函数时 前边加关键字virtual 这个函数就是虚函数 virtual void eat();2.1 纯虚函数、虚析构
纯虚函数、抽象类 1.格式: virtual 返回值类型 函数名(形参列表) = 0 ; //声明此函数是纯虚函数 2.特点(记住) 1)在基类中没有实现 2)必须在派生类中加以实现 (需要派生类实现其函数定义) 3)有纯虚函数的类相当于接口 抽象类 不能直接实例化 3.抽象类:包含纯虚函数的类 不允许创建抽象类对象。 4.抽象类作用:就是用来被继承的,纯虚函数就是用来被重写的。 5.定义纯虚函数的目的: 是为了可以通过抽象类的指针去调用子类对象中重写的函数。 示例代码1: #includeusing namespace std; //抽象类:包含了纯虚函数,那么这个类就变成了抽象类 //特点:1.不可以单独的实例化(创建)对象, //作用:用来被继承,重写纯虚函数 //2.有类继承了抽象类之后,必须重写抽象类中的纯虚函数 class Person { public: //纯虚函数: 虚函数 = 0; virtual void show(int a) = 0; }; class B : public Person { public: void show(int a) { cout 2.2 重写(覆盖)
重写 override: 子类中定义和父类中虚函数同名,同返回值类型,同参数列表的函数,叫重写。 对比重载:overload 在同一个类中,函数名相同,参数列表不同,返回值无所谓,叫重载. 代码示例:重写 #includeusing namespace std; //函数的重写override:1.发生在继承关系中 2.函数名 3.参数列表相同 //函数的重载overload: 1.发生在同一个类中 2.函数名 3.参数列表不同 class Person { public: //虚函数:virtual virtual void show() { cout<<"person show..."< 2.3 多态
多态:指的是同一名字的事物可以完成不同的功能。 编译时多态:函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态; 运行时多态:和继承、虚函数等概念有关,重写 c++的虚函数,主要作用是 实现了运行时多态。 父类中虚函数的实现,是为子类提供默认的函数实现;子类可以重写父类的虚函数实现子类的特殊化。 #includeusing namespace std; //一个事务(函数),多种表现形态(多个不同的功能) class Hero { public: //进攻的函数:提供的默认功能 virtual void attack() { cout<<"小刀拉拉"< attack();//子类重写的 h->fun();//父类普通的函数 h->fun1();//不可以的,访问子类特有的函数,只能反问子类重写的 h = new FaShi; h->attack();//法师类 重写函数 return 0; } 多态实现步骤 1)父类中包含虚函数 如果使用这种指针方式,若调用父类普通函数,则调用函数时依然是父类的方法; 若调用父类的虚函数,调用函数时调用的是子类重写之后的函数。 2)父类指针指向子类对象 父类* 指针 = new 子类(); Shape* shape = new Rectangle(); 使用这种方式,父类的指针可以指向自己的所有派生类对象。 Student xiaoming; Person& p1 = xiaoming;//父类引用指向子类对象 Person* p2 = &xiaoming;//指针的用法同引用 **********父类类型的指针 可以指向子类类型的对象*******重要 1.画图演示 2. 说明调用顺序 1)看父类有没有虚函数 2)如果有,去子类中找有没有重写,有就执行子类重写的方法; 3)如果子类没有重写,就去执行父类的虚函数。 注意: 父类指针 指向子类对象时,可以调用父类继承的和子类重写的 不能使用 子类特有的变量和函数 引用也可以实现多态 由于引用类似于常量,只能在定义的同时初始化,不能再引用其他数据,所以必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。 当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。 不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以谈及多态时一般是说指针。三、多重继承
3.1 虚基类、虚继承、虚表
虚基类:以virtual声明的基类 使得在继承间接共同基类时 只对共同的基类初始化一次 虚继承:格式:class 派生类名:virtual 继承方式 基类名 这种继承称作虚继承,只能在派生的时候声明虚基类 作用: 1、为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。 2、在继承方式前面加上 virtual 关键字就是虚继承,使得在继承间接共同基类时,只对共同的基类初始化一次 3、消除二义性 ***************************错误代码*********************** class A { protected: int money; public: A(int money):money(money){ cout<<"a create"<using namespace std; //菱形继承 //虚继承的那个类,被叫做虚基类 class A { protected: int money;//1 public: A(int money):money(money){cout<<"A"< 菱形继承,是非常不科学的继承逻辑,类的关系设计的合理,是不可能出现菱形继承的。
但是C++的语法允许我们出现菱形继承,并且菱形继承逻辑必然出问题。
虚函数表(虚表):当类中定义虚函数后,编译器会生成一个虚函数表(虚表,相当于一堆函数指针), 父类指针指向子类对象时,通过虚表去锁定调用子类中重写的函数。 class Shape//抽象类,因为有纯虚函数 { public: virtual int getPerimeter() = 0;//纯虚函数 virtual string getType() = 0; }; int main() { //输出4,因为类中有虚函数,所以会生成一个指向虚表的指针 cout<3.2 计算类大小(继承关系)
计算类所占空间的大小: 1、空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同); 空类可以实例化,每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址.所以sizeof( A )的大小为1 2、一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都不占用类对象的存储空间的。 3、当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr,该指针指向虚函数表VTable; 4、在考虑以上内容所占空间的大小时,还要注意编译器下的“补齐”padding的影响,即编译器会插入多余的字节补齐; 6、类对象的大小=各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和+ vfptr指针(指针指向虚函数表,多继承下可能不止一个)+vbptr指针(指针指向虚基表,多继承下可能不止一个)+编译器额外增加的字节。 示例1: class G : public E { virtual int VirtualMemFuncTest1(int para)=0; int m_Int; }; class H : public G { int m_Int; }; sizeof( G ) = ? sizeof( H ) = ? 可以看出子类的大小是本身成员的大小再加上父类成员的大小.如果父类还有父类,也加上父类的父类。 sizeof( G ) = 16 sizeof( H ) = 20 示例2: class D { D(){} virtual ~D() {} virtual int VirtualMemFuncTest1()=0; virtual int VirtualMemFuncTest2()=0; virtual int VirtualMemFuncTest3()=0; }; class I : public D { virtual int VirtualMemFuncTest1()=0; virtual int VirtualMemFuncTest2()=0; }; sizeof( I ) = ? 父类子类工享一个虚函数指针,虚函数指针保留一个即可。 sizeof( I ) = 4 总结: 空的类也是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。 (一)类内部的成员变量: 普通的变量:是要占用内存的,但是要注意内存对齐(这点和struct类型很相似)。 static修饰的静态变量:不占用内存,原因是编译器将其放在全局变量区。 从父类继承的变量:计算进子类中。 (二)类内部的成员函数: 非虚函数(构造函数、静态函数、成员函数等):不占用内存。 虚函数:要占用4个字节(32位的操作系统),用来指定虚拟函数表的入口地址。跟虚函数的个数没有关系。父类子类工享一个虚函数指针。 构成对象本身的只有数据,任何成员函数都不隶属于任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针。成员函数为该类所有对象共享,不仅是处于简化语言实现、节省存储的目的,而且是为了使同类对象有一致的行为。同类对象的行为虽然一致,但是操作不同的数据成员。3.3 多次继承初始化顺序
初始化顺序和继承的顺序为准,不按照初始化列表来, 和初始化成员变量时相同。 #includeusing namespace std; class A { public: A(int a){cout<<"A create"< 四、C++异常机制
4.1 异常、检测步骤、处理异常
异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。 检测异常的三个步骤:检查(try) 抛出(throw) 捕捉(catch) try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。 throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。 catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。 try...catch语法: try { 被检查语句 } catch(异常处理的类型 变量名) { 进行异常处理的语句; } **********************示例1:******************* #includeusing namespace std; int mydiv(int a,int b) { if(b==0) //分母为0,说明发生了异常 { cout<<"throw....."< using namespace std; class ExceptA//专门用来表示异常的类 { public: void show() { cout<<"异常A"< 4.2 内联函数、隐式转换与explicit
内联函数格式: class Person { public: inline void show()//内联函数的声明和定义不能分开 { } }; 内联函数、普通函数区别:当编译器处理调用内联函数的语句时,不会将该语句编译成函数调用的指令,而是直接将整个函数体的代码插人调用语句处,就像整个函数体在调用处被重写了一遍一样。好处就是省去切换函数所需要的开销。 比较小的函数(三两行代码的函数),又会被频繁调用的函数,建议定义成内联函数。 如果编译器判断函数不适合展开,不会将函数当做内联函数处理。 声明内联函数,这是向编译器建议把函数在调用处展开,最终有编译器决定要不要展开。 #includeusing namespace std; inline int max(int a, int b)//内联函数,内联函数的声明和定义不能分开。 { if(a>b) return a; else return b; } int main() { cout< 4.3 虚析构、纯虚析构
虚析构和纯虚析构 问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构函数; 解决方式:将父类中的析构函数改为虚析构或者纯虚析构; 示例代码1: #includeusing namespace std; class A { public : A(){cout<<"A create"< explicit:explicit用来防止由构造函数定义的隐式转换。 **************************示例1:*********************************** #include#include using namespace std; class Person { private: int age; string name; public: Person() { cout<<"no arg"< using namespace std; class Person { private: int age; string name; public: Person() { cout<<"no arg"< 五、模板
模板:泛型编程(可以将类型(基本类型、自定义类型)当做参数, 进行传递)5.1 模板函数
只适用于函数体相同 参数个数相同 参数的类型不同的情况 *************************示例1:函数模板 #includeusing namespace std; //模板函数 template //<>放的是 类型参数 int string char..... void show(T s,T a) { cout<<"T .... "< ("hello","world");//<>放的是类型参数 show(10,20);//<>放的是类型参数 return 0; } *************************示例2:类型当做返回值类型 #include using namespace std; //模板函数 template //<>放的是 类型参数 int string char..... T show(T s,T a) { cout<<"T .... "< ("hello","world")<放的是类型参数 cout< (10,20)< 放的是类型参数 return 0; } *************************示例3:类类型 #include using namespace std; class Person { int age; public: Person(int age):age(age){} void show(){cout/Person void div(T a,T b,U num)//()参数列表 形参 { a.show(); b.show(); T c(122); c.show(); } int main() { Person p(1); //模板类型:自定义类型 div (Person(12),p,10); return 0; } 函数模板和普通函数的区别: 1) 普通函数可以自动类型转换 函数模板必须显示指定类型 2) 同名的普通函数和模板函数出现,优先调用普通函数 3) 函数模板也可以发生重载 5.2 类模板
使用方法:显示指定类型 Personp(10,"王美丽"); 泛型编程:类型参数化 注意:类模板不建议分文件编写,所有的函数实现都写在头文件中 ******************************示例1:成员变量 #include using namespace std; template class Person{ private: T1 age; T2 name; public: Person(T1 a,T2 n):age(a),name(n){} void show(){ cout << name << ":" << age << endl; } }; int main() { Person p(10,"王美丽"); p.show(); return 0; } ******************************示例2: #include using namespace std; template class Person { public: Person(int age):age(age) { } void eat(T food) { cout< * p = new Person (10); p->eat("鸡排"); delete p; } 六、STL(标准模板)
STL标准模板库:即标准模板库或者泛型库,其包含有大量的模板类和模板函数,是 C++ 提供的一个基础模板的集合,用于完成诸如输入/输出、数学计算等功能;该库包含了诸多在计算机科学领域里所常用的基本数据结构和基本算法。 为广大C++程序员们提供了一个可扩展的应用框架,高度体现了软件的可复用性。 1.在 C++ 中如果定义一个数组,可以采用如下方式:int a[n]; 这种定义数组的方法需要事先确定好数组的长度,即 n 必须为常量,这意味着,如果在实际应用中无法确定数组长度,则一般会将数组长度设为可能的最大值,但这极有可能导致存储空间的浪费。 2.还可以采用在堆空间中动态申请内存的方法,此时长度可以是变量: int *p = new int[n]; 这种定义方式可根据变量 n 动态申请内存,不会出现存储空间浪费的问题。 但是,如果程序执行过程中出现空间不足的情况时,则需要加大存储空间,此时需要进行扩容: 3.新申请一个较大的内存空间,即执行int * temp = new int[m]; 将原内存空间的数据全部复制到新申请的内存空间中,即执行memecpy(temp, p, sizeof(int)*n); 将原来的堆空间释放,即执行delete [] p; p = temp; 4.完成相同的操作,如果采用 STL 标准库,则会简单很多,因为大多数操作细节将不需要程序员关心。 示例: vectora; //定义 a 数组,当前数组长度为 0,但和普通数组不同的是, 此数组 a 可以根据存储数据的数量自动变长。 //向数组 a 中添加 10 个元素 for (int i = 0; i < 10 ; i++) a.push_back(i) //还可以手动调整数组 a 的大小 a.resize(100); a[90] = 100; //还可以直接删除数组 a 中所有的元素,此时 a 的长度变为 0 a.clear(); //重新调整 a 的大小为 20,并存储 20 个 -1 元素。 a.resize(20, -1) 对比以上两种使用数组的方式,使用 STL 可以更加方便灵活地处理数据。所以,只需要系统地学习 STL,集中精力去实现程序的功能,而无需纠结某些细节如何用代码实现。 优点:安全、方便、快捷简单 6.1 STL 6大组件
STL:由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的。
STL的组成
含义
容器
一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。
算法
STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件
中。 迭代器
在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,扮演着容器和算法之间的胶合剂。
函数对象
如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。
适配器
可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。
内存分配器
为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。
C++ STL头文件:
6.2 容器(序列、关联)
STL容器:它就是一些模板类的集合,但和普通模板类不同的是,容器中封装的是组织数据的方法(也就是数据结构)。STL 提供有 3 类标准容器,分别是序列容器、排序容器和哈希容器,其中后两类容器有时也统称为关联容器。 两大类:序列容器 关联容器 STL中的容器有: 序列容器 vector(向量容器) list(双向链表) deque(双端队列容器) 关联容器 set(单重集合) multiset(双重集合) map(单重映射表) multimap(多重映射表) 容器适配器 stack(栈) queue(队列) prority_queue(优先级队列) 1、顺序容器(序列式容器):每个元素都有固定位置——取决于插入时机和地点,和元素值无关,vector、deque、list; vector : 将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取), 数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时; List : 双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)), 在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针; Deque : 是double-ended queue的缩写,可以随机存取元素(用索引直接存取), 数组头部和尾部添加或移除元素都非常快速。但是在中部安插元素比较费时;
STL 容器种类和功能
容器种类
功能
序列容器
主要包括 vector 向量容器、list 列表容器以及 deque 双端队列容器。之所以被称为序列容器,是因为元素在容器中的位置同元素的值无关,即容器不是排序的。将元素插入容器时,指定在什么位置,元素就会位于什么位置。
排序容器
包括 set 集合容器、multiset多重集合容器、map映射容器以及 multimap 多重映射容器。排序容器中的元素默认是由小到大排序好的,即便是插入元素,元素也会插入到适当位置。所以关联容器在查找时具有非常好的性能。
哈希容器
C++ 11 新加入 4 种关联式容器,分别是 unordered_set 哈希集合、unordered_multiset 哈希多重集合、unordered_map 哈希映射以及 unordered_multimap 哈希多重映射。和排序容器不同,哈希容器中的元素是未排序的,元素的位置由哈希函数确定。
STL中的容器有: 序列容器 vector(向量容器) list(双向链表) deque(双端队列容器) 关联容器 set(单重集合) multiset(双重集合) map(单重映射表) multimap(多重映射表) 容器适配器 stack(栈) queue(队列) prority_queue(优先级队列) 1、顺序容器(序列式容器):每个元素都有固定位置——取决于插入时机和地点,和元素值无关,vector、deque、list; vector : 将元素置于一个动态数组中加以管理,可以随机存取元素(用索引直接存取), 数组尾部添加或移除元素非常快速。但是在中部或头部安插元素比较费时; List : 双向链表,不提供随机存取(按顺序走到需存取的元素,O(n)), 在任何位置上执行插入或删除动作都非常迅速,内部只需调整一下指针; Deque : 是double-ended queue的缩写,可以随机存取元素(用索引直接存取), 数组头部和尾部添加或移除元素都非常快速。但是在中部安插元素比较费时;总结
这里对文章进行总结:



