43课 继承的概念和意义
问题:类之间是否存在直接的关系连接?
组合关系:整体与部分的关系
*组合关系的特点
-将其他类的对象作为当前类成的成员使用
-当前类的对象与成员对象的生命周期相同
-成员对象在用法上与噗通对象完全一致
继承关系:父子关系 (父类:基类 子类:派生类)
面向对象中的继承指类之间的父子关系
-子类拥有父类的所有属性和行为
-子类就是一种特殊的父类
-子类对象可以当做父类对象使用
-子类中可以添加父类没有的方法和属性
C++中通过下面的方式描述继承关系:
class Parent
{
int mv;
public:
void method(){};
};
class Child : public Parent //描述继承关系
{
};
重要规则:
-子类就是一个特殊的父类
-子类对象可以直接初始化父类对象
-子类对象可以直接赋值给父类对象
继承是C++中代码复用的重要手段,通过继承,可以获得父类的所有功能,并且可以在子类中重写已有的功能,或者添加新功能
注意,创建子类对象的时候,会首先创建父类中的成员变量,再调用父类构造,最后调用子类构造
析构子类对象的时候,会首先析构子类对象,然后是父类,最后是父类成员变量,和构造顺序相反。
问题:为什么回调用父类的构造函数
小结:
*继承是面向对象中类之间的一种关系
*子类拥有父类的所有属性和行为
*子类对象可以当做父类对象使用
*子类可以添加父类没有的方法和属性
*继承是面向对象中代码复用的重要手段
=======================================================================================
44课 继承中的访问级别
问题:子类是否可以直接访问父类的私有成员?
面向对象中的访问级别不只是public和private
可以定义protected访问级别
关键字protected的意义
-修饰的成员不能被外界直接访问
-修饰的成员可已被子类直接访问
注意:
protected:
{
protected修饰的变量无论父类对象还是子类对象都不能在外部直接访问
:类似于 a.mv等操作 a为父类或子类对象,mv为父类protected成员变量
父类和子类都只能通过类内部访问protected变量 即在父类或子类中添加如下类似函数: getvalue(){ mv = 100....}
}
private:
{
private修饰的成员变量,不能被外部直接访问,只能通过内部成员函数访问。子类没有权限
}
问题:为什么面向对象中需要protected?
=======================================================================================
45课 不同的继承方式
被忽略的细节:
冒号(:)表示继承关系,Parent表示被继承的类,public的意义是什么?
class Parent
{
};
class Child :public Parent
{
};
是否可以将继承语句的public换成protected或者private? 如果可以,与public继承有什么区别?
C++中支持三种不同的继承方式
-public继承
*父类成员在子类中保持原有访问级别
-private继承
*父类成员在子类中变为私有成员
-protected继承
*父类中的公有成员变为保护成员,其他成员保持不变
C++中默认继承方式为private!
一般而言,C++工程项目中只是用public继承
C++的派生语言语言只支持一种继承方式:public继承
protected和private继承带来的复杂性远大于实用性
小结:
C++中支持3种不同的继承方式
继承方式直接影响父类成员在子类中的访问属性
一般而言,工程中只是用public的继承方式
C++的派生语言中只支持public继承方式
=====================================================================================
46课 继承中的构造与析构
问题:
如何初始化父类成员?
父类构造函数和子类构造函数有什么关系?
知识点:
子类中可以定义构造函数:子类构造函数
-必须对继承而来的成员进行初始化
1 直接通过初始化列表或赋值的方法进行初始化
2 调用父类构造函数进行初始化(通常做法)
思考:在子类中无法直接访问父类中的private成员,因此这时候在子类中直接赋值或者初始化继承而来的的成员是行不通的,所以要调用父类的构造函数进行初始化。
父类构造函数在子类中的调用方式
1 默认调用 :子类在创建对象时会自动调用父类的构造函数,
适用于无参构造函数和使用默认参数的构造函数
2 显示调用:万能调用
通过初始化列表进行调用
适用于所有父类构造函数
父类构造函数的调用
class Child : public Parent
{
public:
Child() /*隐式调用*/
{
cout << "Child()"<< endl;
}
Child(string s) /*显示调用*/
:Parent("Parameter to Parent")
{
cout<< "Child() :"<< s <<endl;
}
};
例1:
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
Parent(string s)
{
cout << "Parent(string s) : " << s << endl;
}
};
class Child : public Parent
{
public:
Child()
{
cout << "Child()" << endl;
}
Child(string s) :
{
cout << "Child(string s) : " << s << endl;
}
};
int main()
{
Child c;
return 0;
}
报错:原因是 Child c 在创建子类对象时,调用子类无参构造函数,但是因为Child类继承了Parent类,那么在构造Child类对象的时候必然会先调用到父类的构造函数,此时采用默认的构造方式,会调用父类的无参构造函数或者使用默认参数的构造函数,而此时父类中没有默认的构造函数。在父类中添加默认构造函数即可,如下例2:
例2
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
Parent()
{
cout << "Parent()" << endl;
}
Parent(string s)
{
cout << "Parent(string s) : " << s << endl;
}
};
class Child : public Parent
{
public:
Child()
{
cout << "Child()" << endl;
}
Child(string s) {
cout << "Child(string s) : " << s << endl;
}
};
int main()
{
Child c;
Child cc("cc");
return 0;
}
说明:Child cc(“cc”) 会调用Child类中带参数的构造函数 Child(string s) , 而 Child(string s) 构造函数会默认调用父类无参构造函数 Parent()。两个子类对象都是先“默认(隐式)”调用父类的构造函数,再调用自己的构造函数。
而若将子类中的带参构造函数显式调用父类的带参构造函数如下例3:
例3:
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
Parent()
{
cout << "Parent()" << endl;
}
Parent(string s)
{
cout << "Parent(string s) : " << s << endl;
}
};
class Child : public Parent
{
public:
Child()
{
cout << "Child()" << endl;
}
Child(string s) : Parent(s)
{
cout << "Child(string s) : " << s << endl;
}
};
int main()
{
Child c;
Child cc("cc");
return 0;
}
构造规则:
1 子类对象在创建时会首先调用父类的构造函数
2 执行父类构造函数再执行子类的构造函数
3 父类构造函数可以被隐式调用 或者 显示调用
对象创建时构造函数的调用顺序
1 调用父类构造函数
2 调用成员变量的构造函数
3 调用自身的构造函数
口诀:先父母 后客人 再自己
例4:
#include <iostream>
#include <string>
using namespace std;
//该Object类中只有带参构造函数(并且参数不是默认参数),所以他的子类的构造函数必须要显式调用Object类的带参构造函数
class Object
{
public:
Object(string s)
{
cout << "Object(string s) : " << s << endl;
}
};
//父类中没有无参构造函数或者默认参数的构造函数,所以Parent类构造函数要显式调用Object类的构造函数
class Parent : public Object
{
public:
Parent() : Object("Default")
{
cout << "Parent()" << endl;
}
Parent(string s) : Object(s)
{
cout << "Parent(string s) : " << s << endl;
}
};
//继承+组合
class Child : public Parent
{
Object mO1;
Object mO2;
public:
//由于父类有无参构造函数,所以不用显式调用父类的构造函数,但是要初始化成员变量,调用成员变量的构造函数
Child() : mO1("Default 1"), mO2("Default 2")
{
cout << "Child()" << endl;
}
//说明不调用父类的无参构造,而是显式调用父类有参构造,并且初始化成员变量
Child(string s) : Parent(s), mO1(s + " 1"), mO2(s + " 2")
{
cout << "Child(string s) : " << s << endl;
}
};
int main()
{
Child cc("cc");
Child aa;
return 0;
}
析构函数的调用顺序和构造函数相反
1 执行自身的析构函数
2 执行成员变量的析构函数
3 执行父类的析构函数
小结:
1 子类对象在创建时需要调用父类构造函数进行初始化
2 先执行父类构造函数然后执行成员的构造函数
3 父类构造函数显示调用需要在初始化列表中进行
4 子类对象在销毁时需要调用父类析构函数进行清理
5 析构顺序与构造顺序对称相反
=======================================================================================
47课 父子间的冲突
问题:子类中是否可以定义父类中的同名成员?如果可以。如何区分?如果不可以,为什么?
如下:
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
int mi;
};
class Child : public Parent
{
public:
int mi;
};
int main()
{
Child c;
c.mi = 100; // mi 究竟是子类自定义的,还是从父类继承得到的?
return 0;
}
1 子类可以定义父类中的同名成员
2 子类中的成员将隐藏父类中的同名成员
3 父类中的同名成员依然存在于子类
4 通过作用域分便符(::)访问父类中的同名成员
访问父类中的同名成员:
Child c;
c.mi = 100; //子类中的mi
c.Parent::mi = 1000; //父类中的mi
注意:名字虽然相同,但是作用于不同,在不同的命名空间。
回顾:类中的成员函数可以进行重载
1 重载函数的本质为多个不同的函数
2 函数名和参数列表是唯一的标识
3 函数重载必须发生在同一个作用域
问题:子类中定义的函数是否能够重载父类中的同名函数?
答案:由于作用域不同 ,所以不能重载,但是会发生覆盖,子类中的同名函数会隐藏父类中的同名函数。
1 子类中的函数将隐藏父类的同名函数
2 子类无法重载父类中的成员函数
3 使用作用域分辨符访问父类中的同名函数
4 子类可以定义父类中完全相同的成员函数
例1:
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
int mi;
void add(int v)
{
mi += v;
}
void add(int a, int b)
{
mi += (a + b);
}
};
class Child : public Parent
{
public:
int mi;
void add(int v)
{
mi += v;
}
void add(int a, int b)
{
mi += (a + b);
}
void add(int x, int y, int z)
{
mi += (x + y + z);
}
};
int main()
{
Child c;
c.mi = 100;
c.Parent::mi = 1000;
cout << "c.mi = " << c.mi << endl;
cout << "c.Parent::mi = " << c.Parent::mi << endl;
c.add(1);
c.add(2, 3);
c.add(4, 5, 6);
cout << "c.mi = " << c.mi << endl;
cout << "c.Parent::mi = " << c.Parent::mi << endl;
return 0;
}
小结:
1 子类可以定义父类中的同名成员
2 子类中的成员将隐藏父类中的同名成员
3 子类和父类中的函数不能构成重载关系
4 子类可以定义父类中完全相同的成员函数
5 使用作用域分辨符访问父类中的同名成员
=======================================================================================
48课 同名覆盖引发的问题
一 父子间的赋值兼容
二 特殊的同名函数
三 当函数重写遇到赋值兼容
1 父子间的赋值兼容
定义1:
-子类对象可以当做父类对象使用(兼容)
1 子类对象可以直接赋值给父类对象
2 子类对象可以直接初始化父类对象
3 父类指针可以直接指向子类对象
4 父类引用可以直接引用子类对象
如下例:
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
int mi;
void add(int i)
{
mi += i;
}
void add(int a, int b)
{
mi += (a + b);
}
};
class Child : public Parent
{
public:
int mv;
void add(int x, int y, int z)
{
mv += (x + y + z);
}
};
int main()
{
Parent p;
Child c;
p = c;
Parent p1(c);
Parent& rp = c;
Parent* pp = &c;
rp.mi = 100;
rp.add(5); // 没有发生同名覆盖?
rp.add(10, 10); // 没有发生同名覆盖?
/* 为什么编译不过? */
// pp->mv = 1000;
// pp->add(1, 10, 100);
return 0;
}
结论:当使用父类指针(引用)指向子类对象时
1 子类对象退化为父类对象
2 只能访问父类中定义的成员
3 可以直接访问被子类覆盖的同名成员
2 特殊的同名函数
定义1:
1 子类中可以重定义父类中已经存在的成员函数
2 这种重定义发生在继承中,叫做函数重写
3 函数重写是同名覆盖的一种特殊情况
注意:
若此时操作:
Child c;
Parent* p = &c;
p->print();//在执行这条语句时候,p所指向的c对象退化为父类对象,因此此p指向父类的print(),但是执行完该语句,c还是子类对象。
3 当函数重写遇到赋值兼容
问题:当函数重写遇到赋值兼容会发生什么?
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
int mi;
void add(int i)
{
mi += i;
}
void add(int a, int b)
{
mi += (a + b);
}
void print()
{
cout << "I'm Parent." << endl;
}
};
class Child : public Parent
{
public:
int mv;
void add(int x, int y, int z)
{
mv += (x + y + z);
}
void print()
{
cout << "I'm Child." << endl;
}
};
void how_to_print(Parent* p)
{
p->print();
}
int main()
{
Parent p;
Child c;
how_to_print(&p); // Expected to print: I'm Parent.
how_to_print(&c); // Expected to print: I'm Child.
return 0;
}
问题分析:
在编译期间,编译器只能根据指针的类型判断所指向的对象
根据赋值兼容,编译器认为父类指针指向的是父类对象
因此,编译结果只可能是调用父类中定义的同名函数
void how_to_print(Parent* p)
{
p->print();
}
在编译这个函数的时候,编译器不可能知道指针p究竟指向了什么。但是编译器没有理由报错,于是,根据指针类型判断所执行的对象,编译器认为最安全的做法是调用父类的print函数,因为父类和子类肯定都有相同的printf函数。
编译的处理方式是合理的吗?是期望的吗?
小结:
1 子类对象可以当做父类对象使用(赋值兼容)
2 父类指针可以正确的指向子类对象
3 父类引用可以正确的代表子类对象
4 子类中可以重写父类中的成员函数
=======================================================================================
49课 多态的概念和意义
函数重写回顾:
1 父类中被重写的函数依然会继承给子类
2 子类中重写的函数将覆盖父类中的函数
3 通过作用域分辨符(::)可以访问到父类中的函数
Child c;
Parent* p = &c;
c. Parent::print();//从父类继承
c.print();//在子类重写
p->print();//从父类调用
函数重写的原因:
父类中的函数版本不能满足我们子类的需求,子类中需要重新定义一个全新的函数,我们希望只要是子类的对象,那么调用的都是子类中的函数版本,而不是父类中的版本。
面向对象中期望的行为
1 根据实际的对象类型判断如何调用重写函数
2父类指针(引用)指向
1 父类对象则调用父类中定义的函数
2 子类对象则调用子类中定义的重写函数
面向对象中多台的概念:
1 根据实际的对象类型决定函数调用的具体目标
2 同样的调用语句在实际运行时有多种不同的表现形态
C++语言直接支持多态的概念
1 通过使用virtual关键字对多态度进行支持
2 被virtual声明的函数被重写后具有多态特性
3 被virtual声明的函数叫做虚函数
例1
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
virtual void print()
{
cout << "I'm Parent." << endl;
}
};
class Child : public Parent
{
public:
void print()
{
cout << "I'm Child." << endl;
}
};
void how_to_print(Parent* p)
{
p->print(); // 展现多态的行为
}
int main()
{
Parent p;
Child c;
how_to_print(&p); // Expected to print: I'm Parent.
how_to_print(&c); // Expected to print: I'm Child.
return 0;
}
多态的意义:
1 在程序运行过程中展现出动态的特性
2 函数重写必须多态实现,否则没有意义
3 多态饰面向对象组件化程序设计的基础特性
理论中的概念
1 静态联编
在程序的编译期间就能确定具体的函数调用,比如函数重载
2 动态联编
在程序实际运行后才能确定具体的函数调用,比如函数重写
小结:
1 函数重写只可能发生在父类与子类之间
2 根据实际对象的类型确定调用的具体函数
3 virtual关键字是C++中支持多态的唯一方式
4 被重写的虚函数可表现出多态的特性
=======================================================================================