C++面向对象笔记丨类、派生、继承、多态(复习连更中)
概述
复习材料:
- 教材
- 博客包含但不限于Haorical,阿腾木,眼里有光~,Theoyu²,查又恩,Dk只能爬,野渡书生等各位大佬
- Today WA,tomorrow AC.
- Study smart not hard.
结合小题理解
- 不能被派生的函数有构造函数,析构函数,友元函数。
- 构造函数不能被声明为虚函数,析构函数可以被声明为虚函数(虚函数的参数类型,顺序,个数必须一样)
- 构造函数可以被重载,析构函数不可以被重载。
- 很多的const(记得填坑)
- const 用法详解
修饰一般常量常对象/常数组
int const x=2; const int y=2;
int const a[5]={1, 2, 3, 4, 5};
const int b[5]={1, 2, 3, 4, 5};
class A;//已定义
const A a; A const b;
修饰常指针
const int *p; //等价 int const *p;
int const *p; //const修饰指向的对象,p可变,p指向的对象不可变
int * const p; //const修饰指针p, p不可变,p指向的对象可变
const int *const p; //指针p和p指向的对象都不可变
修饰常引用
const double & v;
使用const修饰符也可以说明引用,被说明的引用为常引用,该引用所引用的对象不能被更新
修饰函数的返回值
const修饰符也可以修饰函数的返回值,是返回值不可被改变
const int Fun1();
const MyClass Fun2();
修饰类的成员函数
const修饰符也可以修饰类的成员函数,在调用函数Fun时就不能修改类里面的数据
class ClassName{public:int Fun() const;.....};
常数据成员
- 构造函数对常数据成员进行初始化时只能通过初始化列表进行
- 常数据成员在定义时必须初始化
- 如果类有多个默认构造函数必须都初始化常数据成员
常成员函数 - 只能调用常成员函数和常数据成员。
- 常成员函数可以被其他成员函数调用,但是不能调用其他非常成员函数
- 例如:已知
const A a
, 其中A是一个类名,指向常对象指针的表示为const A *pa
- 多态调用是指借助于指向对象的基类指针或引用调用一个虚函数(以任何方式错,纯虚函错)
- 派生类构造函数的成员初始化列表中,能包含基类的构造函数、派生类中子对象的初始化、派生类中一般数据成员的初始化,但是,不能包含基类中子对象的初始化
- 虚函数具有继承性,是一个成员函数,静态成员函数不可以说明为虚函数
- 继承,子对象的构造函数调用
#include <iostream>
using namespace std;
class point1 {
public: point1();point1(int i);
private: int x;};
point1::point1() {x=1;cout<<"point1's default constructor called!\n";}
point1::point1(int i) {x=i;cout<<"point1's constructor called!\n";}
class point2 : public point1{
public: point2(); point2(int i,int j,int k); point1 p;};
point2::point2() {cout<<"point2's default constructor called\n";}
point2::point2(int i,int j,int k) {cout<<"point2's constructor called\n";}
//②point2::point2(int i,int j,int k):p(j)
int main(){point2 pp1(1,2,3);}
//输出
//point1's default constructor called!
//point1's default constructor called! //②point1's constructor called!
//point2's constructor called
//调用两次point1构造函数,一次是公开继承了point1的构造函数,新建point2时调用了,一次是新建了point1类的p时调用
- 如果子类没有定义构造方法,则调用父类的无参数构造方法
- 如果子类定义了构造方法,不论无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法。
- 在创建子类对象的时候,如果子类的构造函数没有显式调用父类的构造函数,则会调用父类的默认无参构造函数。
- 在创建子类对象时候,如果子类的构造函数没有显式调用父类的构造函数,且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。
- 在创建子类对象时候,如果子类的构造函数没有显示调用父类的构造函数且父类之定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显示调用此带参构造方法)。
- 如果子类调用父类带参数的构造方法,需要初始化父类成员对象的方法。
结合大题理解
初始化列表(为什么,必须吗)
为什么要初始化列表
从效率上看:类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对成员变量的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次赋值操作符的调用,如果是类对象,这样做效率就得不到保障。
另外就是从必要性来看,见下。
必须使用初始化列表的情况
- 需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化):
数据成员是对象,并且这个对象只有含参数的构造函数,没有无参数的构造函数;如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。
class Test{
public:Test (int, int, int){cout <<"Test" << endl;}
private:int x;int y;int z;};
class Mytest {
public:Mytest():test(1,2,3){cout << "Mytest" << endl;}
private:Test test;};
int main(){Mytest aRealTest;return 0;} //output: Test Mytest
- 需要初始化const修饰的类成员或初始化引用成员数据。
当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。
class A{
public: A(int v) : i(v), j(v) {p=v;} //A(int v)不报错,而p可以在初始化列表即p(v)
void h() { cout << "hello" << endl;}
private: const int i; int p; int &j;};
int main(){int pp = 45; A b(pp); b.h();}
- 子类初始化父类的私有成员。
class Test{
public: Test(){ cout << "without parameter"<<endl;};
Test (int x):int_x(x){};
void show(){cout<< int_x << endl;}
private: int int_x;};
class Mytest: public Test{
public: Mytest(): Test(666){}}; //output另一种结果:public: Mytest(){Test(666);}};
int main() {Test *p = new Mytest(); p->show(); return 0;}
* 第一种:output:666,经过有参数初始化构造函数,
* 第二种:output:without parameter 15953808,经历无参数初始化构造函数,尽管Test(666)嵌套在其中,没有报错,但是无法赋予666。实际上666只是构造了一个临时对象,这个临时对象确实也将x初始化为666,但是x这个临时对象在构造函数大括号结束以后生命期就结束了。Mytest()是采用默认构造函数构造的另外一个对象,这个对象的内容没有初始化为0.
Shape类(派生、虚函数、类对象、类指针)
定义一个Shape基类,在此基础上派生出Rectangle
和Circle
类,二者都有GetArea()
函数计算对象的面积,使用Rectangle
类创建一个派生类Square
。
代码
class Shape {
public:
Shape(){}
~Shape(){}
virtual float GetArea() {return -1;}}; //virtual函数
class Circle :public Shape {
private: float r;
public:
Circle(float rr):r(rr){}
float GetArea(){return (3.14*r*r);}}; //写成 virtual float GetArea()也可以
class Rectangle:public Shape {
protected: float l,h;
public:
Rectangle(float ll,float hh):l(ll),h(hh){}
float GetArea(){return (l*h);}}; //写成 virtual float GetArea()也可以
class Square: public Rectangle
{
public:
Square(float ss):Rectangle(ss,ss){}
float GetArea(){return (h*l);}}; //写成 virtual float GetArea()也可以
int main()
{
Shape *sp; //类指针
int radium,length,hight,side;
cin>>radium>>length>>hight>>side;
sp=new Circle(radium);
cout<<"The area of the circle is "<<sp->GetArea()<<endl;
sp=new Rectangle(length,hight);
cout<<"The area of the rectangle is "<<sp->GetArea()<<endl;
sp=new Square(side);
cout<<"The area of the Square is "<<sp->GetArea()<<endl;
delete sp;
return 0;
}
分析
定义类对象:Student a;在定义之后就已经为a这个对象分配了内存,且为内存栈;
定义类指针:Student *b = new Student();在定义*b的时候并没有分配内存,只有执行new后才会分配内存,且为内存堆。
- 定义
- 类对象:利用类的构造函数(构造函数:对类进行初始化工作)在内存中分配一块区域(包括一些成员变量赋值);
- 类指针:是一个内存地址值,指向内存中存放的类对象(包括一些成员变量赋值;类指针可以指向多个不同的对象,这就是多态);
- 使用
- 引用成员:对象使用“.”操作符,指针用“->”操作符;
- 生命周期:若是成员变量,则由类的析构函数来释放空间;若是函数中临时变量,则作用域是函数体内;而指针则需要利用delete在相应的地方释放分配的内存块。
- 注意:new与delete成对存在!!!
3.存储位置 - 类对象:用的是内存栈,是个局部的临时变量;
- 类指针:用的是内存堆,是个永久变量,除非你释放它。
- 访问方式
- 指针变量是间接访问,但可实现多态(通过父类指针可调用子类对象),并且没有调用构造函数;
- 直接声明可直接访问,但不能实现多态,声明即调用了构造函数(已分配了内存)
- 类对象和类指针联系
- 在类的声明尚未完成的情况下,可以声明指向该类的指针,但是不可声明该类的对象;
- 父类的指针可以指向子类的对象。
- 指针与虚函数
要发挥虚函数的强大作用,必须使用指针来访问对象。当类是有虚函数的基类,Func是它的一个虚函数,则调用Func时:类对象:调用的是它自己的Func;类指针:调用的是分配给它空间时那种类的Func。 - 什么情况使用类对象与类指针?
其实作用基本一样,都是为了调用类的成员变量和成员函数用的;当你希望明确使用这个类的时候,最好使用对象;如果你希望使用C++中的动态绑定,则最好使用指针或者引用,指针和引用用起来更灵活,容易实现多态等。而且指针占用资源小,传过去的就是4个字节。如果用对象,参数传递占用的资源就太大了。
水果类
题目内容:
小明经营着一个不大的水果店,只销售苹果、香蕉和桔子。为了促销,小明制定了如下定价策略:苹果:按斤论价,每斤P元,买W斤,则需支付WP元。香蕉:半价,每斤P元,买W斤,则需支付W/2P元。桔子:按斤论价,每斤P元,买W斤。如果W>10,则打半价,即需支付WP/2元;否则如果W>5,则打八折,即需支付WP0.8元;其他情况不打折,即需支付WP元。请用C++来计算某个顾客采购的水果的总价。该程序至少应有:Fruit类:是个抽象类,是Apple、Banana和Orange的父类。支持重载的加法运算。Apple、Banana和Orange类:分别对应于苹果、香蕉和桔子三种水果,每种水果执行不同的定价策略。输入为多行,每行格式为:C W P其中C是水果类型(a、b、o分别代表苹果、香蕉和桔子),W和P分别是顾客购买的相应水果的重量和每斤水果的单价。输入完成后输入字符q结束输入。
代码
#include<iostream>
using namespace std;
class Fruit{
public:
char c; int weight, price;
Fruit(int w, int p){weight = w; price = p;}
virtual double sum() { return 0; }
friend double operator +(Fruit& a, double allsum){return a.sum() + allsum;}}; //加法重载
class Apple :public Fruit{
public:
Apple(int w, int p):Fruit(w,p){}
double sum() {return weight*price;}};
class Banana :public Fruit{
public:
Banana(int w,int p):Fruit(w,p){}
double sum() {return weight*price/2.0;}};
class Orange :public Fruit{
public:
Orange(int w,int p):Fruit(w,p){}
double sum()
{
if (weight <= 5)
return weight * price;
else if (weight > 10)
return weight * price / 2;
else
return weight * price * 0.8;
}
};
int main()
{
char c; int w=0, p=0;
double allsum = 0;
while (1)
{
cin >> c;
if (c=='q') break;
cin >> w >> p;
switch (c){
case 'a':{Apple a2(w, p);allsum+=a2.sum();break;}
case 'b':{Banana b2(w, p);allsum+=b2.sum();break;}
case 'o':{Orange o2(w, p);allsum+=o2.sum();break;}
}
}
cout << allsum;
return 0;
}
//为什么不用面向过程编程呢,大无语。