为了保证内容完整性,将day6的继承搬运一下。
可以看到,5天入门变为7天入门了(就站在门框上,这门还进不进了?!)。
感谢大佬几款优秀的支持C、C++在线编译器
stage1——7天入门阶段
教程网站:C++ 教程
在线编译器:compile c++ gcc online
刷题网站:阶段1第一关:基本数据类型
day7 planA
教程2(2-7),刷题3(1+4),复习3(2)
教程完成度100%,刷题完成度25%,复习完成度100%
主要原因:摸鱼是最大的敌人!!!
Q&A
1.继承
继承允许我们依据另一个类
来定义一个类,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类
,新建的类称为派生类
。
当然不止类,还有结构体
。
继承代表了is a
关系。
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {//这里就声明了public继承
// bark() 函数
};
使用派生类的注意事项:
1)派生类需要结合:
符号使用,在派生类名之前也可以加上继承方式,不加默认private
;
2)派生类自己的定义和基类是一样的,比如函数在类外定义要用派生类名::
3)派生类只有public
才可以调用基类的protected
,所以一定记得在派生类中加上public
;
4)派生类可以访问继承基类的非private
成员,所以可以直接用派生类名.基类成员
来访问基类成员;
5)但是这些不可以继承:友元函数、构造函数、析构函数、拷贝构造函数、重载运算符(看2中内容)。
ps.这里说不可以继承构造函数,应该指的是不能直接在派生类中什么也不定义,然后在主函数中用
派生类名.
这样调用,但是是可以在构造派生类的构造函数时直接用类的构造函数的。比如:
class A{ public:A(){} }; class B:public A{ public:B(){A();} };
继承时不显示声明时,则默认是private
继承。在struct
中默认public
继承。
注意:class
和struct
定义时,{}
后一定要加;
1.1 基类 & 派生类
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名:
class derived-class: access-specifier base-class
· 访问修饰符 access-specifier 是 public、protected 或 private 其中的一个
· base-class 是之前定义过的某个类的名称
1.2 继承类型
几乎不使用protected
或private
继承,通常用public
继承。
在类和对象
的访问修饰符
中整理过,是一样的。
1.3 多继承
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,…
{
<派生类类体>
};
#include <iostream>
using namespace std;
class box
{
public:
void geta(int aa)
{
a=aa;
}
void getb(int bb)
{
b=bb;
}
protected:
int a;
int b;
};
class mul
{
public:
int multi(int c)
{
return c*50;
}
};
class p:public box,public mul
{
public:
int getall()
{
return a*b;
}
};
int main()
{
p test;
test.geta(2);
test.getb(3);
int t=test.getall();
cout << t <<endl;
int tt=test.multi(t);
cout << tt <<endl;
return 0;
}
2 重载运算符和重载函数
C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)
不相同。
当您调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
#include <iostream>
using namespace std;
class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}
void print(double f) {//形参不同,内容不同
cout << "浮点数为: " << f << endl;
}
void print(char c[]) {//注意!!!!形参是字符串数组要加上[],不然分不清是字符还是字符串数组
cout << "字符串为: " << c << endl;
}
};
int main(void)
{
printData pd;
// 输出整数
pd.print(5);
// 输出浮点数
pd.print(500.263);
// 输出字符串
char c[] = "Hello C++";
pd.print(c);
return 0;
您可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
Box operator+(const Box&);
和一般定义函数是一样的,只是在函数名那里需要加上重载运算符号
大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。
如果我们定义重载函数为类的非成员函数,而需要2个类形参,那么我们需要为每次操作传递两个参数;
如果是成员函数就直接用this指针。
#include <iostream>
using namespace std;
class Box
{
public:
double getVolume(void)
{
return length * breadth * height;
}
void setLength( double len )
{
length = len;
}
void setBreadth( double bre )
{
breadth = bre;
}
void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符,用于把两个 Box 对象相加
Box operator+(const Box& b)
{
Box box;
box.length = this->length + b.length;
box.breadth = this->breadth + b.breadth;
box.height = this->height + b.height;
return box;
}
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
// 程序的主函数
int main( )
{
Box Box1; // 声明 Box1,类型为 Box
Box Box2; // 声明 Box2,类型为 Box
Box Box3; // 声明 Box3,类型为 Box
double volume = 0.0; // 把体积存储在该变量中
// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// Box1 的体积
volume = Box1.getVolume();
cout << "Volume of Box1 : " << volume <<endl;
// Box2 的体积
volume = Box2.getVolume();
cout << "Volume of Box2 : " << volume <<endl;
// 把两个对象相加,得到 Box3
Box3 = Box1 + Box2;
// Box3 的体积
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;
return 0;
}
可重载运算符
这里的例子都是在类中重载运算符,所以作用对象一般也是类。
不可重载运算符
2.1 一元运算符、赋值运算符
重载调用自增++
或自减--
重载自增前缀返回类型 operater++()
,重载自增后缀返回类型 operator ++(int)
,重载自减类似。
重载调用取负-
和逻辑非!
就是返回类型 operater-()
、返回类型 operater!()
赋值运算符=
重载类似取负。
注意:作用对象是本身的,没有形参;有其他作用对象的,有形参,要用&
引用符
2.2 二元运算符、关系运算符
二元运算符一般就是加减乘除,方式是一样的,参见上面的例子。
关系运算符<
、>
、<=
、>=
、==
等等。因为同样涉及到2个参数,所以和二元是类似的。
2.3 输入输出运算符
在这里,有一点很重要,我们需要把运算符重载函数声明为类的友元函数,这样我们就能不用创建对象而直接调用函数。
和前面的重载有点区别。
返回类型是ostream
或istream
,而且需要在operator
前加上&
,形参也是不同的,需要加上原本的输入输出流类型形参,和需要输出的参数。
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
friend ostream &operator<<( ostream &output,
const Distance &D )
{
output << "F : " << D.feet << " I : " << D.inches;
return output;
}
friend istream &operator>>( istream &input, Distance &D )
{
input >> D.feet >> D.inches;
return input;
}
};
int main()
{
Distance D1(11, 10), D2(5, 11), D3;
cout << "Enter the value of object : " << endl;
cin >> D3;
cout << "First Distance : " << D1 << endl;
cout << "Second Distance :" << D2 << endl;
cout << "Third Distance :" << D3 << endl;
return 0;
}
2.4 函数调用运算符重载
创建一个可以传递任意数目参数的运算符函数,所以不是改变所有定义用()
的函数,即不影响构造函数等。使用的时候定义多少形参就要调用多少参数,和一般函数调用一样,形参前的函数名部分则是类名。
#include <iostream>
using namespace std;
class Distance
{
private:
int feet; // 0 到无穷
int inches; // 0 到 12
public:
// 所需的构造函数
Distance(){
feet = 0;
inches = 0;
}
Distance(int f, int i){
feet = f;
inches = i;
}
// 重载函数调用运算符
Distance operator()(int a, int b, int c)
{
Distance D;
// 进行随机计算
D.feet = a + c + 10;
D.inches = b + c + 100 ;
return D;
}
// 显示距离的方法
void displayDistance()
{
cout << "F: " << feet << " I:" << inches << endl;
}
};
int main()
{
Distance D1(11, 10), D2;
cout << "First Distance : ";
D1.displayDistance();
D2 = D1(10, 10, 10); // invoke operator()
cout << "Second Distance :";
D2.displayDistance();
return 0;
}
2.5 下标运算符重载
下标操作符[]
通常用于访问数组元素。重载该运算符用于增强操作 C++ 数组的功能。
下面的例子就是,对类用[]
进行值访问,就像数组的操作一样。
#include <iostream>
using namespace std;
const int SIZE = 10;
class safearay
{
private:
int arr[SIZE];
public:
safearay()
{
register int i;
for(i = 0; i < SIZE; i++)
{
arr[i] = i;
}
}
int& operator[](int i)//这个符号&加不加都可以,不影响输出结果,但如果在主函数需要对A[i]进行修改的时候,就要加引用符&
{
if( i >= SIZE )
{
cout << "索引超过最大值" <<endl;
// 返回第一个元素
return arr[0];
}
return arr[i];
}
};
int main()
{
safearay A;
cout << "A[2] 的值为 : " << A[2] <<endl;
cout << "A[5] 的值为 : " << A[5]<<endl;
cout << "A[12] 的值为 : " << A[12]<<endl;
return 0;
}
2.6 类成员访问运算符重载
类成员访问运算符->
可以被重载。
它被定义用于为一个类赋予"指针"行为。运算符 -> 必须是一个成员函数。如果使用了 -> 运算符,返回类型必须是指针或者是类的对象。
运算符 -> 通常与指针引用运算符 * 结合使用,用于实现"智能指针"的功能。
间接引用运算符 -> 可被定义为一个一元后缀运算符。类 Ptr 的对象可用于访问类 X 的成员,使用方式与指针的用法十分相似。
class Ptr{
//...
X * operator->();
};
void f(Ptr p )
{
p->m = 10 ; // (p.operator->())->m = 10
}
巨复杂的例子
#include <iostream>
#include <vector>
using namespace std;
// 假设一个实际的类
class Obj {
static int i, j;
public:
void f() const { cout << i++ << endl; }
void g() const { cout << j++ << endl; }
};
// 静态成员定义
int Obj::i = 10;
int Obj::j = 12;
// 为上面的类实现一个容器
class ObjContainer {
vector<Obj*> a;//声明一个vector容器a,其中元素为指向Obj类型的指针
public:
void add(Obj* obj)//调用Obj类的指针,存入容器a
{
a.push_back(obj); // 调用向量的标准方法
}
friend class SmartPointer;//声明友元类
};
// 实现智能指针,用于访问类 Obj 的成员
class SmartPointer {
ObjContainer oc;
int index;
public:
SmartPointer(ObjContainer& objc)//构造函数,调用Obj类,对当前类的private类型参数赋值
{
oc = objc;
index = 0;
}
// 返回值表示列表结束
bool operator++() // 前缀版本
{
if(index >= oc.a.size() - 1) return false;//判断Obj类数组的地址容器是否为空(是否没有Obj类),为空返回false
if(oc.a[++index] == 0) return false;//判断Obj类数组是否访问完,如果后面没有Obj类,返回false
//这一步也是索引index的自增
return true;
}
bool operator++(int) // 后缀版本
{
return operator++();//和前缀效果一样
}
// 重载运算符 ->
Obj* operator->() const
{
if(!oc.a[index])//判断Obj类有没有内容,判空
{
cout << "Zero value";
return (Obj*)0;
}
return oc.a[index];//返回Obj类的地址(指针)
}
};
int main() {
const int sz = 10;
Obj o[sz];//定义一个Obj类数组
ObjContainer oc;//存放Obj类数组地址的容器
for(int i = 0; i < sz; i++)
{
oc.add(&o[i]);//将每个Obj类的地址写到容器里
}
SmartPointer sp(oc); // 创建一个迭代器
do {
sp->f(); // 智能指针调用
sp->g();
} while(sp++);
return 0;
}
输出---------
10
12
11
13
12
14
13
15
14
16
15
17
16
18
17
19
18
20
19
21
3.多态
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
稍微和重载区别一下:
重载一般是形参类型不一样,调用的时候输入不同类型形参即可;
多态则主要是在基类和派生类之间有同名函数的时候,依据对象区分调用函数。
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
//定义2个派生类
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
// 存储矩形的地址
shape = &rec;
// 调用矩形的求面积函数 area
shape->area();//指针访问对象成员,用->
// 存储三角形的地址
shape = &tri;
// 调用三角形的求面积函数 area
shape->area();
return 0;
}
ps.疑问,为什么shape作为基类的指针,可以把派生类的指针赋给它的,粗浅理解基类应该是被派生类所包含的,那为什么可以这样作呢。大概就是两者的类不是完全相同的,即使有继承关系,为什么可以跨类型赋指针?那么可不可以把派生类的指针给基类用呢?
输出--------------
Parent class area :
Parent class area :
调用函数area()
被编译器设置为基类中的版本,这就是所谓的静态多态
,或静态链接
,即函数调用在程序执行前就准备好了
。有时候这也被称为早绑定
,因为area()
函数在程序编译期间就已经设置好了。
如果对程序稍作修改,在Shape
类中,area()
的声明前放置关键字virtual,就可以输出派生类的结果。
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
};
输出--------------
Rectangle class area :
Triangle class area :
3.1 虚函数virtual
加了virtual
关键字的函数就是虚拟函数,在派生类中就可以通过重写虚拟函数来实现对基类虚拟函数的覆盖。
在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
例子如刚才那里例子,如果把派生类地址赋给基类指针,则调用同名函数的时候使用的是派生类里的函数。主要还是看地址。
3.2 纯虚函数
如果在基类中不能对虚函数给出有意义的实现,可以用纯虚函数。
在定义虚函数的时候不用函数主体,而是直接给虚函数赋0
值。
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
4.数据抽象
数据抽象
是指,只向外界提供关键信息,并隐藏其后台的实现细节,即只表现必要的信息而不呈现细节。
C++ 中,我们使用类来定义我们自己的抽象数据类型ADT
。
我们使用访问标签来定义类的抽象接口。一个类可以包含零个或多个访问标签。
1)使用public
标签定义的成员都可以访问该程序的所有部分。一个类型的数据抽象视图是由它的公共成员来定义的。
2)使用private
标签定义的成员无法访问到使用类的代码。私有部分对使用类型的代码隐藏了实现细节。
指定的访问级别会一直有效,直到遇到下一个访问标签或者遇到类主体的关闭右括号为止。
数据抽象好处:
- 类的内部受到保护,不会因无意的用户级错误导致对象状态受损
- 类实现可能随着时间的推移而发生变化,以便应对不断变化的需求,或者应对那些要求不改变用户级代码的错误报告
如果只在类的私有部分定义数据成员,编写该类的作者就可以随意更改数据。如果实现发生改变,则只需要检查类的代码,看看这个改变会导致哪些影响。如果数据是公有的,则任何直接访问旧表示形式的数据成员的函数都可能受到影响。
#include <iostream>
using namespace std;
class Adder{
public:
// 构造函数
Adder(int i = 0)
{
total = i;
}
// 对外的接口
void addNum(int number)
{
total += number;
}
// 对外的接口
int getTotal()
{
return total;
};
private:
// 对外隐藏的数据
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
公有成员addNum
和getTotal
是对外的接口,用户需要知道它们以便使用类。私有成员total
是用户不需要了解的,但又是类能正常工作所必需的。
5.数据封装
程序基本要素:
- 程序语句(代码):这是程序中执行动作的部分,它们被称为函数。
- 程序数据:数据是程序的信息,会受到程序函数的影响。
封装
是面向对象编程中的把数据和操作数据的函数绑定在一起的一个概念,这样能避免受到外界的干扰和误用。数据封装引申出了另一个重要的 OOP 概念,即数据隐藏。
数据封装是一种把数据和操作数据的函数捆绑在一起的机制,数据抽象是一种仅向用户暴露接口而把具体的实现细节隐藏起来的机制。
C++ 通过创建类
来支持封装和数据隐藏(public、protected、private)
把一个类定义为另一个类的友元类,会暴露实现细节,从而降低了封装性。
#include <iostream>
using namespace std;
class Adder{
public:
// 构造函数
Adder(int i = 0)
{
total = i;
}
// 对外的接口
void addNum(int number)
{
total += number;
}
// 对外的接口
int getTotal()
{
return total;
};
private:
// 对外隐藏的数据
int total;
};
int main( )
{
Adder a;
a.addNum(10);
a.addNum(20);
a.addNum(30);
cout << "Total " << a.getTotal() <<endl;
return 0;
}
6.接口(抽象类)
接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。
class Box
{
public:
// 纯虚函数
virtual double getVolume() = 0;
private:
double length; // 长度
double breadth; // 宽度
double height; // 高度
};
设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。
如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,即必须重写虚函数,这也意味着 C++ 支持使用 ABC 声明接口。
一般来说,虚函数在基类里,那么重写虚函数就要在派生类中。
即,面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
可用于实例化对象的类被称为具体类。
#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}