OOP随手记-Lec.7:组合与继承

继续进行一些博客的水经验帖。

Lec.7 组合与继承

对象组合:”has a”

组合:“对象套对象”

1、两种访问成员对象的接口:image-20220402135343978

2、子对象构造时若需要参数,则应在当前类的构造函数的初始化列表中进行。若使用默认构造函数来构造子对象,则不用做任何处理。(会自动调用子对象的默认构造函数!)

e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class C3 {//Composite3类别
int num;
S1 sub_obj1; /// 构造函数带参数
S2 sub_obj2; /// 构造函数不带参数
public:
C3() : num(0), sub_obj1(123) /// 构造函数初始化列表中构造子对象
{ cout << "C3()" << endl; }
C3(int n) : num(n), sub_obj1(123)
{ cout << "C3(int)" << endl; }
C3(int n, int k) : num(n), sub_obj1(k)
{ cout << "C3(int, int)" << endl; }
~C3() { cout << "~C3()" << endl; }
};

int main()
{
C3 a, b(1), c(2), d(3, 4);
return 0;
}

image-20220402140118690

3、对象构造与析构函数的次序

•先完成子对象构造,再完成当前对象构造

•子对象构造的次序仅由在类中声明的次序所决定

•析构函数的次序与构造函数相反

4、隐式定义的拷贝构造与拷贝赋值

•如果调用拷贝构造函数且没有给类显式定义拷贝构造函数,编译器将提供“隐式定义的拷贝构造函数”。该函数的功能为:

递归调用所有子对象的拷贝构造函数——具体取决于子对象的拷贝构造函数的定义方式与功能

•对于基础类型,采用位拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
using namespace std;

class C1{
public:
int i;
C1(int n):i(n){}
C1(const C1 &other) /// 显式定义拷贝构造函数
{i=other.i; cout << "C1(const C1 &other)" << endl;}
};

class C2{
public:
int j;
C2(int n):j(n){}
C2& operator= (const C2& right){/// 显式定义赋值运算符
if(this != &right){
j = right.j;
cout << "operator=(const C2&)" << endl;
}
return *this;
}
};
class C3{
public:
C1 c1;
C2 c2;
C3():c1(0), c2(0){}
C3(int i, int j):c1(i), c2(j){}
void print(){cout << "c1.i = " << c1.i << " c2.j = " << c2.j << endl;}
};
int main(){
C3 a(1, 2);
C3 b(a); //C1执行显式定义的拷贝构造, C2执行隐式定义的拷贝构造
cout << "b: ";
b.print();
cout << endl;

C3 c;
cout << "c: ";
c.print();
c = a; //C1执行隐式定义的拷贝赋值, C2执行显式定义的拷贝赋值
cout << "c: ";
c.print();
return 0;
}

对象继承:”is-a”

一般——特殊;父——子;特殊类

•如果类A具有类B全部的属性和服务,而且具有自己特有的某些属性或服务,则称A为B的特殊类,B为A的一般类。

•如果类A的全部对象都是类B的对象,而且类B中存在不属于类A的对象,则A是B的特殊类,B是A的一般类。

1、定义与分类

被继承的已有类,被称为基类(base class),也称“父类”。
通过继承得到的新类,被为派生类(derived class,也称“子类”、“扩展类”。

继承的内容:数据成员,函数成员

常见的继承方式:public, private——继承关键字

  • class Derived : [private] Base { .. }; 缺省继承方式为private继承。
  • class Derived : public Base { … };
  • protected 继承很少被使用
    class Derived : protected Base { … };

2、什么没法被继承?

•构造函数:创建派生类对象时,必须调用派生类的构造函数,派生类构造函数调用基类的构造函数,以创建派生对象的基类部分。C++11新增了继承构造函数的机制(使用using),但默认不继承

•析构函数:释放对象时,先调用派生类析构函数,再调用基类析构函数

•赋值运算符:编译器不会继承基类的赋值运算符(因为其参数为基类)

•但会自动合成隐式定义的赋值运算符(参数为派生类),其功能为调用基类的赋值运算符。

•友元函数:不是类成员

e.g.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
using namespace std;

class Base{
public:
int k = 0;
void f(){cout << "Base::f()" << endl;}
Base & operator= (const Base &right){
if(this != &right){
k = right.k;
cout << "operator= (const Base &right)" << endl;
}
return *this;
}
};
class Derive: public Base{};
int main(){
Derive d, d2;
cout << d.k << endl; //Base数据成员被继承
d.f(); //Base::f()被继承

Base e;
//d = e; //编译错误,Base的赋值运算符不被继承
d = d2; //调用隐式定义的赋值运算符!看前面笔记
return 0;
}

派生类的构造与析构

1、

  • 基类中的数据成员,通过继承成为派生类对象的一部分,需要在构造派生类对象的过程中调用基类构造函数来正确初始化。
    若没有显式调用,则编译器会自动调用基类的默认构造函数。
    若想要显式调用,则只能在派生类构造函数的初始化成员列表中进行,既可以调用基类中不带参数的默认构造函数,也可以调用合适的带参数的其他构造函数。
  • 先执行基类的构造函数来初始化继承来的数据,再执行派生类的构造函数。
  • 对象析构时,先执行派生类析构函数,再执行由编译器自动调用的基类的析构函数。

例子见ppt

2、继承构造函数

  • 如何继承基类的构造函数?使用using Base::Base,相当于给派生类“定义”了相应参数的构造函数

    当基类存在多个构造函数时,使用using会给派生类自动构造多个相应的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base 
{
int data;
public:
Base(int i) : data(i) { cout << "Base::Base(" << i << ")\n"; }
Base(int i, int j)
{ cout << "Base::Base(" << i << “," << j << ")\n";}
};
class Derive : public Base {
public:
using Base::Base; ///相当于 Derive(int i):Base(i){};
///加上 Derive(int i, int j):Base(i,j){};
};
int main() {
Derive obj1(356);
Derive obj2(356, 789);
return 0;
} // g++ 1.cpp –o 1.out -std=c++11
  • 如果基类的某个构造函数被声明为私有成员函数,则不能在派生类中声明继承该构造函数。
  • 如果派生类使用了继承构造函数,编译器就不会再为派生类生成隐式定义的默认构造函数。(如上例,Derive obj;无法通过)
    前面的例子,不能用Derive dr;定义对象

3、几种继承方式的比较

image-20220410111511874

public继承
基类的公有成员,保护成员,私有成员作为派生类的成员时,都保持原有的状态
private继承
基类的公有成员,保护成员,私有成员作为派生类的成员时,都作为私有成员
protected继承
基类的公有成员,保护成员作为派生类的成员时,都成为保护成员,基类的私有成员仍然是私有的。

image-20220402143405086 image-20220402143444476
私有继承中的访问权限距离

”照此实现“:基类接口(公有成员函数)在派生类的成员函数中可以调用,故可以通过基类接口实现派生类功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
using namespace std;
class Base {
public:
void baseFunc() { cout << "in Base::baseFunc()..." << endl; }
};

class Derive2: private Base
{/// 私有继承, is-implementing-in-terms-of:用基类接口实现派生类功能
public:
void deriveFunc() {
cout << “in Derive2::deriveFunc(), calling Base::baseFunc()..." << endl;
baseFunc(); /// 私有继承时,基类接口在派生类成员函数中可以使用
}
};

int main() {
Derive2 obj2;
cout << "calling obj2.deriveFunc()..." << endl;
obj2.deriveFunc();
//obj2.baseFunc(); ERROR: 基类接口不允许从派生类对象调用

return 0;
}

基类中的私有成员在派生类中无法访问!!!!!!“基类私有”的概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base{
private:
int a{0};
protected:
int b{0};
};

class Derive : private Base{
public:
void getA(){cout<<a<<endl;} ///编译错误,不可访问基类中私有成员
void getB(){cout<<b<<endl;} ///可以访问基类中保护成员
};

int main()
{
Derive d;
d.getB();
//cout<<d.b; ///编译错误,派生类对象不可访问基类中保护成员
return 0;
}

”打开权限“后可以访问私有继承的基类部分函数

1
2
3
4
class Base {private:    int data{0};public:    int getData(){ return data;}    void setData(int i){ data=i;}};
class Derive1 : private Base { public: using Base::getData; };
int main() { Derive1 d1; cout<<d1.getData(); //d1.setData(10); ///隐藏了基类的setData函数,不可访问 //Base& b = d1; ///不允许私有继承的向上转换 //b.setData(10); ///否则可以绕过D1,调用基类的setData函数
return 0; }

Summary:”取交集“

image-20220402144526525

解释:

派生类成员函数只有一行no:基类私有一定要彻底私有,无法通过任何方式访问

派生类对象只有一个yes:public∩public=public,可以访问;其他都不行

保护成员:无法通过派生类对象访问,但派生类成员函数可以访问

组合与继承的作用

组合与继承的优点:支持增量开发。
允许引入新代码而不影响已有代码正确性。

  • 相似:
    实现代码重用。
    将子对象引入新类。
    使用构造函数的初始化成员列表初始化。
  • 不同:
    组合:
    嵌入一个对象以实现新类的功能。
    has-a 关系。
    继承:
    沿用已存在的类提供的接口。
    public 继承:is-a。
    private 继承:is-implementing-in-terms-of。

重写隐藏

重载(overload):
目的:提供同名函数的不同实现,属于静态多态。
函数名必须相同,函数参数必须不同,作用域相同(如位于同一个类中;或同名全局函数)。

重写隐藏(redefining):

  • 目的:在派生类中重新定义基类函数,实现派生类的特殊功能
  • 屏蔽了基类的所有其它同名函数。
  • 函数名必须相同,函数参数可以不同

1、重写隐藏发生时,基类中该成员函数的其他重载函数都将被屏蔽掉,不能提供给派生类对象使用

2、可以在派生类中通过using 类名::成员函数名; 在派生类中“恢复”指定的基类成员函数(即去掉屏蔽),使之重新可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
class T {};
class Base {
public:
void f() { cout << "B::f()\n"; }
void f(int i) { cout << "Base::f(" << i << ")\n"; } /// 重载
void f(double d) { cout << "Base::f(" << d << ")\n"; } ///重载
void f(T) { cout << "Base::f(T)\n"; } ///重载
};
class Derive : public Base {
public:
void f(int i) { cout << "Derive::f(" << i << ")\n"; } ///重写隐藏
};
int main() {
Derive d;
d.f(10);
d.f(4.9); /// 编译警告。执行自动类型转换。
// d.f(); /// 被屏蔽,编译错误
// d.f(T()); /// 被屏蔽,编译错误
return 0;
}

3、补充:using关键字的功能

image-20220402150015754

多重继承


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!