# C++基础13-类的继承

本文是《C++ Primer Plus》的笔记,本文中的案例均自己实践过,如需转发请在转发开头贴上原文地址,谢谢

继承是面向对象的一个重要特性,它能重用已有的代码而不是在原有代码基础上直接修改。因此,如果购买的类库只提供类的头文件和编译后的代码,我们仍然可以重用库中的类。

使用《C++基础12》中的StringM类来创建一个实例:

Book.h 的文件内容:
#pragma once
#include "StringM.h"
class Book
{
private:
    StringM bookName;
    StringM authorName;
public:
    Book();
    Book(StringM bookNameL, StringM authorNameL);
    void GetBookName();
    void GetAuthorName();
    ~Book();
};

Book.cpp 的文件内容:

#include "pch.h"
#include "Book.h"


Book::Book()
{
    cout << "this is base book class" << endl;
}

Book::Book(StringM bookNameL, StringM authorNameL):bookName(bookNameL),authorName(authorNameL)
{
    cout << "base book with bookname authorname constructor is called!" << endl;
}

void Book::GetBookName()
{
    cout << "BookName :" << bookName << endl;
}

void Book::GetAuthorName()
{
    cout << "AuthorName :" << authorName << endl;
}


Book::~Book()
{
    cout << "base book is destroyed!" << endl;
}

ArtBook.h 文件的内容:

#pragma once
#include "Book.h"
class ArtBook :
    public Book
{
public:
    ArtBook();
    ArtBook(StringM bn,StringM an);
    void GetBaseBook();
    void GetArtBook();
    ~ArtBook();
};

ArtBook.cpp 文件的内容:

#include "pch.h"
#include "ArtBook.h"


ArtBook::ArtBook()
{
    cout << "artbook is created!" << endl;
}

ArtBook::ArtBook(StringM bn, StringM an):Book(bn,an)
{
    cout << "art book parameter constructor is called!" << endl;
}

void ArtBook::GetBaseBook()
{
    GetBookName();
}

void ArtBook::GetArtBook()
{
    cout << "get art book !" << endl;
}


ArtBook::~ArtBook()
{
    cout << "artbook is destroyed!" << endl;
}

main 的内容:

int main(int argc,char *argv[])
{
    {
        Book b1("computer", "xie");
        ArtBook ab1;
        //先调用基类默认构造函数再调用该类的默认构造函数
        ArtBook ab2("scene", "li");
        Book b2;
        b2 = ab2;
        //上述并不会让b2指向ab2,没有Book & operator=(const Book & ) 函数
        b2.GetAuthorName();
        Book* b3 = &ab2;
        b3->GetBookName();
        Book& b4 = ab2;
        b4.GetBookName();
        cout << "inner block ended!" << endl;
    }
    cout << "main function ended!" << endl;
}

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

上面的实例说明:

  1. 派生类可以调用基类的方法,但是只能访问基类public或protect的成员,不能访问基类private成员。
  2. 派生类如果不显式指定调用基类的哪个构造函数,默认会调用基类的默认构造函数。
  3. 基类指针可在不进行显式类型转换的情况下指向派生类,基类引用可在不显式类型转换情况下引用派生类,但是只能调用基类的成员。

# 继承:is-a关系

C++的继承分公有继承,私有继承,保护继承。它们的相同点如下:

基类成员 基类对象 派生类
公有成员 可见 可见
保护成员 不可见 可见
私有成员 不可见 不可见

不同点在于派生类对基类的成员访问级别的继承方式,公有继承中,公有成员和保护成员在派生类中还是公有和保护级别,私有继承中,公有成员和保护成员在派生类中都是私有级别,保护继承中,公有成员和保护成员在派生类中都是保护级别。

派生类和基类的继承机制在逻辑上比较像is-a关系,即is-a-kind-of,比如香蕉是一种水果,香蕉类就可以继承水果类。其他关系有has-a,比如午餐和水果,午餐可以包含水果;is-like-a,比如律师像鲨鱼;is-implemented-as-a,作为...的实现,比如数组可以实现栈,但是用在继承上是不合适的;uses-a,比如公司可以使用打印机。注意继承在逻辑上要判断是否属于is-a的关系,然后再使用继承。

# 公有继承的多态

来看一个老虎类和动物类的实例:

animal.h 文件内容:

#pragma once
class animal
{
public:
    animal();
    void sleep();
    virtual void bellow();
    virtual ~animal();
};

animal.cpp 文件内容:

#include "pch.h"
#include "animal.h"


animal::animal()
{
    cout << "an animal is created!" << endl;
}

void animal::sleep()
{
    cout << "I am an animal,I will sleep!" << endl;
}


void animal::bellow()
{
    cout << "I am an animal,but I really don't know why?" << endl;
}

animal::~animal()
{
    cout << "animal is destroyed!" << endl;
}

tiger.h 文件内容:

#pragma once
#include "animal.h"
class tiger :
    public animal
{
public:
    tiger();
    void sleep();
    virtual void bellow();
    ~tiger();
};

tiger.cpp 文件内容:

#include "pch.h"
#include "tiger.h"


tiger::tiger()
{
    cout << "a tiger is created!" << endl;
}

void tiger::sleep()
{
    cout << "I am a tiger,I will sleep!" << endl;
}

void tiger::bellow()
{
    cout << "I am a tiger,I am bellowing!" << endl;
}


tiger::~tiger()
{
    cout << "tiger is destroyed!" << endl;
}
}

main 函数:

int main(int argc,char *argv[])
{
    {
        animal animalObj;
        tiger tigerObj;
        animalObj.bellow();
        tigerObj.bellow();
        animal* t1=&animalObj;
        animal* t2=&tigerObj;
        t1->bellow();
        t2->bellow();
        t1->sleep();
        t2->sleep();

        animal& t3 = animalObj;
        animal& t4 = tigerObj;
        t3.bellow();
        t4.bellow();
        t3.sleep();
        t4.sleep();

        tigerObj.~tigerObj();

        cout << "inner block ended!" << endl;
    }
    cout << "main function ended!" << endl;
}

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111

实例说明:

  1. 方法被声明为virtual后,使用基类的指针或引用调用方法会根据对象的类型变而变,指向对象类型是animal,则会调用animal的该方法,指向对象是tiger,则会调用tiger的该方法。
  2. 虚析构函数和其他虚函数不一样的是,它还调用基类的析构函数。

# 动态联编和静态联编

程序调用函数时,要使用哪个可执行代码块呢?编译器解决这个问题,将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)。在C语言中,这比较简单,因为每个函数名都对应一个不同函数,但是在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名来确定使用哪个函数。在编译过程中进行联编被称为静态联编(static binding)或早期联编(early binding)。然而有些是没有办法在编译阶段完成这件事的,比如虚函数,在编译阶段是无法确定对象,编译器必须生成能够在程序运行时选择正确的虚方法,这被称为动态联编(dynamic binding)或晚期联编(late binding)。

# 指针和引用类型使用

将派生类引用或指针转换为基类引用或指针被称为向上强制转换(upcasting),比如前面的tiger继承了animal的成员,可以对animal执行的任何操作都适用于tiger,而且向上强制转换是可传递的。

将基类指针或引用转换为派生类指针或引用被称为向下强制转换(downcasting),如果不使用强制转换,向下强制转换是不允许的。原因是is-a关系通常是不可逆的,比如tiger有animal没有的方法,对tiger做的操作并不一定都可以应用在animal上。

# 虚成员函数和动态联编

这两种联编有什么样的关系呢?首先静态联编要比动态联编运行效率高,动态联编基类和派生类都要维护一个虚函数表,运行时需要根据对象的类型来查询每个类对应的虚函数表,这样不管在空间和时间上都存在着损耗。但是动态联编要比静态联编更灵活,更能实现复杂的多态性。

# 虚函数的注意事项

  1. 在基类和派生类中,要使目标方法使用关键字virtual声明为虚的。
  2. 需要使用指向对象的引用或指针来调用虚方法,程序将根据对象类型调用相应的方法。
  3. 构造函数不能是虚函数,派生类构造函数会使用基类的一个构造函数,这种机制不同于继承,将类构造函数声明为虚的没有意义。析构函数应该是虚函数,除非类不用做基类。通常应该给基类提供一个虚析构函数,即使它并不需要析构函数。
  4. 友元不能是虚函数,因为友元不是类成员,只有成员才能是虚函数,但是可以让友元函数调用虚函数。
  5. 如果派生类没有重新定义函数,将使用该函数的基类版本。如果重新定义了函数,应确保与原来的原型相同,如果返回的是基类引用或指针,则应该修改为指向派生类的引用或指针。并且如果该函数有多个重载版本,则应在基类中重新定义所有的基类版本,否则没有定义的版本将会被隐藏,无法使用。
在animal.h 中添加
virtual void bellow(int a);

在animal.cpp 中添加
void animal::bellow(int a)
{
    cout << "I will bellow integer:" << a << endl;
}

在main函数中
tigerObj.bellow(2);
这是错误的。会显示没有相应的函数,但是
t2->bellow(1);
是可以执行的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# protected关键字

protected关键字声明的成员,对于类对象而言和private是一样的,都不能被外部和类对象访问,但是对于派生类来说,protected能被派生类访问,private不能被派生类访问。

如果数据成员的输入输出有一定的限制,最好对数据成员进行私有访问控制,不要使用保护访问控制,因为派生类可以直接访问这些数据成员而不管限制,同时通过基类方法使派生类能够访问基类数据,这样能继续对这些数据成员提供限制,当然如果对数据输入输出时没有限制的话,则可以使用保护访问控制。

# 抽象基类

# 适用的场景

前面我们提到tiger和animal这种is-a关系,但是还有很多关系类似is-a关系但并不完全是,比如说椭圆和园,它们其实更像一种一般与特殊的关系,而这种关系如果用继承的话,会比较不方便,比如椭圆有半长轴和半短轴,方向角,而圆只有一个半径。但是它们也有不少共同点,比如都有中心坐标,都需要移动,这种情况下如果使用继承,圆继承椭圆,那半长轴和半短轴就没有用,但是如果不使用继承,那一些共同点就要分开写,这种情况下,就要使用抽象基类(abstract base class,ABC)。

我们不能直接从抽象基类生成对象,只有当其派生类实现了相应的虚函数,从派生类生成对象,如果派生类没有实现纯虚函数,则依旧不能生成对象,一个类要成为ABC,必须至少包含一个纯虚函数。原型中虚函数末尾=0都会使函数成为纯虚函数,注意只能在虚函数后面加。C++允许纯虚函数有定义。

可以将ABC看作是一种必须实施的接口。ABC要求具体派生类覆盖其纯虚函数——迫使派生类遵循ABC设置的接口规则。这种模型在基于组件的编程模式中很常见,在这种情况下,使用ABC使得组件设计人员能够制定"接口约定",确保了从ABC派生的所有组件都至少支持ABC指定的功能。

# 实例

以上面的圆和椭圆为例,使用BaseEllipse来抽象出Ellipse和Circle的共性,不同的地方使用纯虚函数来实现,比如Area。

BaseEllipse.h 文件内容:

#pragma once
#include <cmath>
#define PI (atan(1.0)*4)
class BaseEllipse
{
private:
    double xCorrd;
    double yCorrd;
public:
    BaseEllipse();
    void move(double x1,double y1);
    virtual double Area() const = 0;
    virtual ~BaseEllipse();
};

BaseEllipse.cpp 文件内容:

#include "pch.h"
#include "BaseEllipse.h"

BaseEllipse::BaseEllipse()
{
}

void BaseEllipse::move(double x, double y)
{
    xCorrd = x;
    yCorrd = y;
}

double BaseEllipse::Area() const {
    cout << "this is only for area calc!" << endl;
    return 0.0;

}

BaseEllipse::~BaseEllipse()
{
}

Ellipse.h 文件内容:

#pragma once
#include "BaseEllipse.h"
class Ellipse :
    public BaseEllipse
{
private:
    double a;
    double b;
public:
    Ellipse();
    Ellipse(double a1,double b1);
    virtual double Area() const;
    ~Ellipse();
};

Ellipse.cpp 文件内容:

#include "pch.h"
#include "Ellipse.h"

Ellipse::Ellipse()
{
}

Ellipse::Ellipse(double a1, double b1)
{
    a = a1;
    b = b1;
}

double Ellipse::Area() const
{
    return PI*a*b;
}

Ellipse::~Ellipse()
{
}

Circle.h 文件内容:

#pragma once
#include "BaseEllipse.h"
class Circle :
    public BaseEllipse
{
private:
    double r;
public:
    Circle();
    Circle(double r1);
    virtual double Area() const;
    ~Circle();
};

Circle.cpp 文件内容:

#include "pch.h"
#include "Circle.h"

Circle::Circle()
{
}

Circle::Circle(double r1)
{
    r = r1;
}

double Circle::Area() const
{
    return PI*r*r;
}

Circle::~Circle()
{
}

main 函数的内容:

int main(int argc,char *argv[])
{
    {
        Ellipse e1(2,3);
        cout << "e1 area:" << e1.Area() << endl;
        Circle c1(2);
        cout << "c1 area:" << c1.Area() << endl;
        BaseEllipse * b1 = &e1;
        BaseEllipse * b2 = &c1;
        cout << "b1 area:" << b1->Area() << endl;
        cout << "b2 area:" << b2->Area() << endl;
        cout << "inner block ended!" << endl;
    }
    cout << "main function ended!" << endl;
}

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

从上面看出,不能直接从BaseEllipse创建对象,但是可以用它的指针来指向Ellipse和Circle对象,Ellipse和Circle类必须得实现Area纯虚函数。

# 继承和动态内存分配

以上面的BaseEllipse和Ellipse为例,来说明类和其派生类关于内存的管理。

BaseEllipse.h 文件内容:

#pragma once
#include <cmath>
#define PI (atan(1.0)*4)
class BaseEllipse
{
private:
    double xCorrd;
    double yCorrd;

protected:
    char * name;
public:
    BaseEllipse();
    BaseEllipse(const char * name);
    BaseEllipse(const BaseEllipse & be);
    BaseEllipse & operator=(const BaseEllipse & be);
    void CopyCharArray(char * &tName,const char * nameValue);
    void GetName();
    void move(double x1,double y1);
    virtual double Area() const = 0;
    virtual ~BaseEllipse();
    friend std::ostream & operator<<(std::ostream & os, const BaseEllipse & be);
};

BaseEllipse.cpp 文件内容:

#include "pch.h"
#include "BaseEllipse.h"


BaseEllipse::BaseEllipse()
{
    cout << "BaseEllipse non parameter constructor is called!" << endl;
    name = nullptr;
}

BaseEllipse::BaseEllipse(const char * nameT)
{
    CopyCharArray(name, nameT);
    cout << "BaseEllipse :" << nameT << " created!" << endl;
}

BaseEllipse::BaseEllipse(const BaseEllipse & be)
{
    CopyCharArray(name, be.name);
    cout << "BaseEllipse :" << name << " is initted by a BaseEllipse!" << endl;
}

BaseEllipse & BaseEllipse::operator=(const BaseEllipse & be)
{
    // TODO: 在此处插入 return 语句
    if (this == &be) {
        cout << "assign itself to itself" << endl;
        return *this;
    }
    CopyCharArray(name, be.name);
    cout << "BaseEllipse :" << name << " is copied!" << endl;
    return *this;
}

void BaseEllipse::CopyCharArray(char * &tName, const char * nameValue)
{
    int len = std::strlen(nameValue) + 1;
    tName = new char[len];
    strcpy_s(tName, len, nameValue);
}

void BaseEllipse::GetName()
{
    if (name == nullptr) {
        cout << "BaseEllipse GetName is null!" << endl;
        return ;
    }
    cout << "BaseEllipse GetName:" << name << endl;
}


void BaseEllipse::move(double x, double y)
{
    xCorrd = x;
    yCorrd = y;
}

double BaseEllipse::Area() const {
    cout << "this is only for area calc!" << endl;
    return 0.0;
}


BaseEllipse::~BaseEllipse()
{
    if (name == nullptr) {
        cout << "BaseEllipse name is nullptr" << endl;
        return;
    }
    cout << name << " BaseEllipse is destroyed!" << endl;
    delete[] name;
}

std::ostream & operator<<(std::ostream & os, const BaseEllipse & be)
{
    // TODO: 在此处插入 return 语句
    if (be.name == nullptr) {
        cout << "BaseEllipse name is nullptr" << endl;
        return os;
    }
    os << "BaseEllipse << :" << be.name << endl;
    return os;
}

Ellipse.h 文件内容:

#pragma once
#include "BaseEllipse.h"
class Ellipse :
    public BaseEllipse
{
private:
    double a;
    double b;
    char * name;
public:
    Ellipse();
    Ellipse(double a1,double b1, const char * n1);
    Ellipse(const Ellipse & e1);
    Ellipse & operator=(const Ellipse &e1);
    virtual double Area() const;
    ~Ellipse();
    friend std::ostream & operator<<(std::ostream & os, const Ellipse & e1);
};

Ellipse.cpp 文件内容:

#include "pch.h"
#include "Ellipse.h"


Ellipse::Ellipse() :BaseEllipse("no name")
{
}

Ellipse::Ellipse(double a1, double b1,const char * n1):BaseEllipse(n1)
{
    a = a1;
    b = b1;
    CopyCharArray(name, "world");
}

Ellipse::Ellipse(const Ellipse & e1)//:BaseEllipse(e1)
{
    CopyCharArray(name,e1.name);
    cout << "Ellipse :" << name << " is initted by a Ellipse!" << endl;
}

Ellipse & Ellipse::operator=(const Ellipse & e1)
{
    if (this == &e1) {
        cout << "assign itself to itself" << endl;
        return *this;
    }
    //BaseEllipse::operator=(e1);
    CopyCharArray(name, e1.name);
    cout << "Ellipse :" << name << " is copied!" << endl;
    return *this;
}

double Ellipse::Area() const
{
    return PI*a*b;
}


Ellipse::~Ellipse()
{
    delete[] name;
    cout << "Ellipse is deleted!" << endl;
}

std::ostream & operator<<(std::ostream & os, const Ellipse & e1)
{
    os << (BaseEllipse &)e1 << endl;
    os << "Ellipse <<:" << e1.name << endl;
    return os;
}

main 函数的内容:

int main(int argc,char *argv[])
{
    {
        Ellipse e1(1,2,"hi");
        cout << "---------------e2-----------------" << endl;
        Ellipse e2;
        e2 = e1;
        cout << "e1 is copied to e2:" << endl << e2 << endl;
        e2.GetName();
        cout << "----------------e3-----------------" << endl;
        Ellipse e3 = e1;
        cout << "e1 is initted to e3:" << endl << e3 << endl;
        e3.GetName();
        cout << "-----------------inner block ended!----------------" << endl;
    }
    cout << "main function ended!" << endl;
}

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207

注意几点:

  1. 类和其派生类各自管理内存,也就是哪个里面使用了new开辟内存,就要有相应的复制构造函数,赋值运算符,析构函数。
  2. 派生类复制构造函数如果也要调用父类的,那么使用初始化成员列表,否则调用父类的默认构造函数;复制运算符要调用父类的则是使用BaseEllipse::operator=(e1);形式;析构函数自动调用父类的。
  3. 注意友元函数的<<运算符重载,调用父类的形式是os << (BaseEllipse &)e1 << endl;

# 类设计回顾

# 编译器生成的成员函数

编译器会自动生成一些特殊成员函数

# 默认构造函数

默认构造函数要么没有参数,要么所有的参数都有默认值。如果派生类构造函数的成员初始化列表中没有显示调用基类构造函数,则编译器将调用基类的默认构造函数,在这种情况下,如果基类没有构造函数,将导致编译阶段错误。如果定义了某种构造函数,编译器将不会定义默认构造函数,这种情况下需要自己提供。

构造函数的作用:

  1. 确保对象总能被正确地初始化。
  2. 如果类包含指针成员,需要初始化。
  3. 为所有类的数据成员初始化为合理的值,调用基类默认构造函数以及调用本身是对象的成员的所属类的默认构造函数。

# 复制构造函数

复制构造函数接受其所属类的对象作为参数,在这些情况下使用复制构造函数:

  1. 将新对象初始化为一个同类对象
  2. 按值将对象传递给函数
  3. 函数按值返回对象
  4. 编译器生成临时对象,看具体编译器实现

如果没有定义该函数,程序将新对象的每个成员都被初始化为原始对象相应成员的值,如果成员为类对象,则调用该类的复制构造函数,如果成员变量里面有new初始化的成员,这种浅复制是不合适的,需要深复制,或者类可能修改某些静态变量,这些情况下需要定义自己的复制构造函数。

# 赋值运算符

注意需要判断对象是不是自身,默认的赋值运算符用于处理同类对象之间的赋值,编译器不会将一种类型赋给另一种类型,比如把字符串赋值给StringM,要实现有两种方法:

  1. 显示定义运算符StringM & StringM::operator=(const char *){...}
  2. 使用只有一个字符串参数的构造函数,将字符串转成StringM对象,然后将StringM赋值给StringM

第一种方法运行速度较快,但是需要代码较多,第二种使用转换函数可能导致语法未料到的转换,导致混乱。

# 其他类方法

# 构造函数

构造函数不同于其他类方法,因为它创建新的对象,而其他类只是被现有的对象调用。这是构造函数不能被继承的原因之一。继承意味着派生类对象可以使用基类方法,然而构造函数在完成其工作之前,对象并不存在。

# 析构函数

如果构造函数使用new来为成员变量分配内存,一定要在析构函数显式释放该部分内存,对于基类,即使它不需要析构函数,也应该提供一个虚析构函数。

# 转换

使用只有一个参数的构造函数可将参数类型转换到类类型,将类类型转换成其他类型需要定义转换函数,转换函数即使没有声明返回类型,函数也应返回所需的转换值,注意转换函数可能导致类和转换类型运算时造成二义性(是类-》转换类型还是转换类型-》类),使用explicit可禁止隐式转换。

# 按值传递对象与传递引用

使用对象作为参数的函数,一般使用引用而不是按值来传递对象,这样做可提高效率,避免临时对象生成,复制对象要比使用引用花费的时间多的多,如果不修改对象,应该将参数声明为const。另外在函数中被定义为接收基类引用参数的函数可以接收派生类。

# 返回对象和返回引用

返回对象会生成临时对象,还会调用对象的构造和析构函数,这都会降低效率,所以优先返回引用。但是如果返回局部对象时一定要返回对象,否则返回引用无效。

# 使用const

使用const来确保方法不修改参数,不修改调用它的对象。

# 公有继承的考虑因素

# 继承关系

继承的类之间要遵循is-a关系,如果派生类不是一种特殊的基类,则不要使用公有派生,最好是创建包含纯虚函数的抽象数据类,并从它派生出其他类。

# 类的成员

  1. 使用私有数据成员比使用保护数据成员更好,派生类不会直接访问该数据,使用成员函数来访问可以防止派生类越过对成员数据的限制。
  2. 如果希望派生类能够重新定义方法,则应在基类中将方法定义为虚,这样可以启用动态联编,如果不希望重新定义方法,则不必将其声明为虚的,这样虽然无法禁止他人重新定义方法,但是也表达了你不希望它被重新定义。
  3. 基类的析构函数应当是虚的,当使用基类指针或引用来删除派生类对象时,会先调用派生类的析构函数,再调用基类的析构函数。
  4. 友元函数并非类成员,因此不能继承,派生类友元调用基类友元,可先转换派生类类型。

# 什么不能被继承

构造函数是不能继承的,创建派生对象时,必须调用派生类的构造函数。派生类构造函数通常使用成员初始化列表语法来调用基类构造函数,以创建派生对象的基类部分。如果派生类构造函数没有使用成员初始化列表显式调用基类构造函数,将使用基类的默认构造函数。继承链中每个类都可以使用成员初始化列表将信息传递给相邻的基类。

析构函数是不能继承的,在释放对象时,程序将首先调用派生类的析构函数,然后调用基类的析构函数。通常对于基类其析构函数设置为虚。

# 赋值运算

当一个对象赋给同一个类的另一个对象时,如果没有定义赋值运算符,那么会默认提供一个,将原对象的相应成员赋给目标对象的每个成员。如果对象属于派生类,编译器将使用基类赋值运算符来处理派生对象中基类部分的赋值。如果显式定义了赋值运算符,将使用该定义版本。相似的,如果成员是一个对象,则赋值该成员时,会调用该类的赋值运算符。

如果类构造函数使用new来初始化指针,需要提供一个显式赋值运算符来深度复制。对于派生类对象的基类部分,C++使用基类的赋值运算符。如果把派生类赋值给基类,则调用基类的赋值运算符,如果把基类赋值给派生类,除非派生类有转换的构造函数或相应的赋值运算符,否则是无法赋值的。

# 关于使用基类方法的说明

以公有方式派生的类对象可通过多种方式来使用基类方法:

  1. 派生类对象自动使用基类继承来的方法,如果派生类没有定义该方法。
  2. 派生类的构造函数默认自动调用基类的默认构造函数。如果在成员初始化列表中指定了构造函数则调用相应的构造函数。
  3. 派生类方法可使用作用域解析运算符来调用公有和受保护的基类方法。
  4. 派生类可以使用作用域解析运算符来调用基类公有和受保护的基类方法。派生类友元函数可通过类型转换调用基类的友元函数。

# 类函数总结

类函数总结