第三章:类

218 阅读46分钟

第一节:成员函数、对象拷贝、私有成员

类基础

  1. 一个类就是一个用户自己定义的数据类型,我们可以把类想象成一个命名空间,包着一堆东西(成员函数、成员变量)
  2. 一个类的构成:成员变量、成员函数(有很多特殊成员函数)。
  3. 我们访问类成员时:如果是类的对象,我们就使用对象名.成员名来访问成员。如果是指向这个对象的指针,我们就用指针名->成员名来访问成员.
class student {
public:
    int number;
    char name[100];
};


int main() {
    student someone; // 定义类的对象
    someone.student = 1000;
    student *psomeone = &someone;
    psomeone->number = 1005;
}
  1. public成员提供类的接口,暴露给外界,供外界调用。private成员提供各种实现类功能的细节方法,但不暴漏给使用者,外界无法使用这些private
  2. struct是成员默认为public的class,struct A {......}。而class成员默认是private(私有)class A{......}struct A {......}等价于class A{ public: ......}

成员函数

tmp_time.cpp文件中内容

class Time {
public:
    int hour;
    int minute;
    int second;
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second) {
        hour = tmp_hour; // 成员函数中,可以直接使用成员变量名
        minute = tmp_minute;
        second = tmp_second;
    }
}

int main() {
    Time myTime; // 类对象
    myTime.initTime(11, 14, 5);
}

上述写法,Time类只能在tmp_time.cpp中调用。规范写法是类定义类实现分别放在Time.h文件Time.cpp文件中。后续其他类需要使用Time类只需要include "Time.h"

Time.h文件中内容

#ifndef __MYTIME__
#define __MYTIME__

// int global = 5; //  不允许在不同源文件被多次include

// 类定义(类声明)
// 定义一个时间类
class Time {
public:
    int hour;
    int minute;
    int second;  
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second);
}

Time.cpp文件中内容

include "Time.h"
// 成员函数initTime()的实现
// 其中这两个冒号叫 作用域运算符,表示initTime函数属于Time类
// 哪个对象调用的该成员函数,这些成员变量就属于哪个对象,大家可以理解为类成员函数知道哪个对象调用自己。
void Time::initTime(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; // 成员函数中,可以直接使用成员变量名
    minute = tmp_minute;
    second = tmp_second;
}

类是一个特殊的存在,在不同源文件(cpp文件)中include重复类定义是被系统允许的,但全量变量不被允许。

对象的拷贝

对象本身是可以拷贝的,默认情况下,这种类对象的拷贝,是每个成员变量逐个拷贝。如果在类Time中我们定义适当的“赋值运算符”,就能控制对象的这种拷贝行为。

include "Time.h"

int main() {
    Time myTime; // 类对象
    myTime.hour = 12;
    myTime.minute = 15;
    myTime.second = 40;
    
    // 下面4个对象用myTime的值进行初始化,但地址各不相同
    Time myTime2 = myTime;
    Time myTime3(myTime);
    Time myTime4{ myTime };
    Time myTime5 = { myTime };
    
    
}

第二节:构造函数详解、explicit、初始化列表

构造函数

在类中,有一种特殊的成员函数,它的名字和类名相同,我们在创建类对象的时候,这个特殊的成员函数就会被系统自动调用,这个成员函数,就叫构造函数。因为构造函数会被系统自动调用,所以我们可以简单理解为:构造函数的目的就是初始化类对象的数据成员。

  1. 构造函数没有返回值
  2. 不可以手动调用构造函数,否则编译器会出错
  3. 正常情况下,构造函数应该被声明为public,因为创建一个对象时,系统要替我们调用构造函数,说明构造函数要死public。

Time.h文件中内容

#ifndef __MYTIME__
#define __MYTIME__

// int global = 5; //  不允许在不同源文件被多次include

// 类定义(类声明)
// 定义一个时间类
class Time {
public:
    int hour;
    int minute;
    int second;  
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second);

public:
    // 构造函数
    Time(int tmp_hour, int tmp_minute, int tmp_second);
    Time();

}

Time.cpp文件中内容

include "Time.h"
// 成员函数initTime()的实现
// 其中这两个冒号叫 作用域运算符,表示initTime函数属于Time类
// 哪个对象调用的该成员函数,这些成员变量就属于哪个对象,大家可以理解为类成员函数知道哪个对象调用自己。
void Time::initTime(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; // 成员函数中,可以直接使用成员变量名
    minute = tmp_minute;
    second = tmp_second;
}

// 构造函数的实现
Time::Time(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; 
    minute = tmp_minute;
    second = tmp_second;

}

// 构造函数的实现
Time::Time() {
    hour = 12; 
    minute = 59;
    second = 59;
}

多个构造函数

include "Time.h"

int main() {
    // 下面4个对象用myTime的值进行初始化,但地址各不相同
    Time myTime = Time(12, 13, 52); 
    Time myTime2(12, 13, 52); 
    Time myTime3 = Time{12, 13, 52};
    Time myTime4{12, 13, 52};
    Time myTime5 = {12, 13, 52};
    
    Time myTime10 = Time(); 
    // Time myTime11(); / 这种写法是错误的 
    Time myTime12;
    Time myTime13 = Time{};
    Time myTime14{};
    Time myTime15 = {};
    
    // 对象拷贝
    Time myTime20; // 可以调用构造函数
    // 如下4个对象并没有调用传统意义上的构造函数,他们调用的是拷贝构造函数
    Time myTime22 = myTime20;
    Time myTime23(myTime20);
    Time myTime24 { myTime20 };
    Time myTime25 = { myTime20 };
    
}

函数默认参数

  1. 默认值只能放在函数声明中,除非该函数没有函数声明。
  2. 在具有多个参数的函数中指定默认值时,默认参数都必须出现在不默认参数的右边,一旦某个参数开始指定默认值,它右边的所有参数必须指定默认值。

隐式转换和explicit

#ifndef __MYTIME__
#define __MYTIME__

// int global = 5; //  不允许在不同源文件被多次include

// 类定义(类声明)
// 定义一个时间类
class Time {
public:
    int hour;
    int minute;
    int second;  
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second);

public:
    // 构造函数
    Time(int tmp_hour, int tmp_minute, int tmp_second);
    Time();
    Time(int tmp_hour, int tmp_minute);
    Time(int tmp_hour);

}

Time.cpp文件中内容

include "Time.h"
// 成员函数initTime()的实现
// 其中这两个冒号叫 作用域运算符,表示initTime函数属于Time类
// 哪个对象调用的该成员函数,这些成员变量就属于哪个对象,大家可以理解为类成员函数知道哪个对象调用自己。
void Time::initTime(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; // 成员函数中,可以直接使用成员变量名
    minute = tmp_minute;
    second = tmp_second;
}

// 构造函数的实现
Time::Time(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; 
    minute = tmp_minute;
    second = tmp_second;
}

Time::Time() {
    hour = 12; 
    minute = 59;
    second = 59;
}

Time::Time(int tmp_hour, int tmp_minute) {
    hour = 12; 
    minute = 59;
}

Time::Time(int tmp_hour) {
    hour = 12; 
}
include "Time.h"

void func(Time time) {

}

int main() {
    // 编译系统肯定有个行为,把14这个数字,转换成了一个Time类类型,调用了单参数的构造函数
    Time myTime40 = 14; 
    Time myTime41 = (12, 13, 14, 15, 16);  // 调用了单参数的构造函数
    func(16); // 16被转换成了一个临时Time对象导致func能调用成功 调用了单参数的构造函数
    
    Time myTime40 = {16}; // 正常写法,带一个参数16,可以让系统明确知道调用哪个构造函数
    Time myTime40 = 16; // 含糊不清的写法,就存在临时对象隐式转换
    func(16); // 含糊不清的写法,存在临时对象隐式转换    
}

如果构造函数声明中带有explicit,则这个构造函数只能用于初始化和显示类型转换。

对于单参数的构造函数,一般声明成explicit,除非有特别原因。

// 将Time的无参构造函数声明成explicit后:
Time myTime200{};
// Time myTime201 = {}; // 这种写法不可以

构造函数初始化列表

构造函数初始化列表效率高(使用初始化列表不需要在{}中进行赋值操作)

不要在初始化列表中用一个成员变量给另一个成员变量赋值。

include "Time.h"
// 成员函数initTime()的实现
// 其中这两个冒号叫 作用域运算符,表示initTime函数属于Time类
// 哪个对象调用的该成员函数,这些成员变量就属于哪个对象,大家可以理解为类成员函数知道哪个对象调用自己。
void Time::initTime(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; // 成员函数中,可以直接使用成员变量名
    minute = tmp_minute;
    second = tmp_second;
}

// 构造函数的实现
Time::Time(int tmp_hour, int tmp_minute, int tmp_second)
:hour(tmp_hour), minute(tmp_minute), second(tmp_second) // 构造函数初始化列表
// :hour(tmp_hour), minute(hour) // 初始化列表中hour先有值还是minute先有值取决于定义顺序,因为在Time.h中hour是先定义的,所以hour先有值。
{
    hour = tmp_hour; 
    minute = tmp_minute;
    second = tmp_second;
}

第三节:inline、const、mutable、this、static

在类定义中实现成员函数inline

类内的成员函数实现其实也叫类内的成员函数定义,这种直接在类的定义中实现的成员函数,会被当作inline内联函数处理。

成员函数末尾的const

const:常量,在成员函数屁股后增加一个const,不但要在成员函数声明中增加const,也要在成员函数定义中增加const。

作用:告诉系统,这个成员函数不会修改对象里任何成员变量的值等。也就是说,这个成员函数不会修改类Time的任何状态。

屁股后边加一个const后缀的成员函数:常量成员函数

const成员函数:不管是const对象,还是非const对象,都可以调用const成员函数。而非const成员函数,不能够被const对象调用,只能被非const对象调用。

mutable

用mutable修饰一个成员变量,一个成员变量一旦被mutable修饰,就表示这个成员变量永远处于可以被修改的状态,即使是在const结尾的成员函数中,也可以修改。

返回自身对象的引用this

如何理解this:在调用成员函数时,编译器负责把这个对象的地址传递给这个成员函数中的一个隐藏的this形参。 Time.cpp文件中内容

include "Time.h"

static int global = 5; // 保存在静态存储区,限制该全局变量只能够用于本文件中

void Time::initTime(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; // 成员函数中,可以直接使用成员变量名
    minute = tmp_minute;
    second = tmp_second;
}

// 构造函数的实现
Time::Time(int tmp_hour, int tmp_minute, int tmp_second) {
    hour = tmp_hour; 
    minute = tmp_minute;
    second = tmp_second;
}

Time::Time() {
    hour = 12; 
    minute = 59;
    second = 59;
}

Time::Time(int tmp_hour, int tmp_minute) {
    hour = 12; 
    minute = 59;
}

Time::Time(int tmp_hour) {
    hour = 12; 
}

Time& Time::add_hour(int tmp_hour) // Time& Time::add_hour(Time *const
 this, int tmp_hour)
{
    hour += tmp_hour;  // 等价于:this.hour += tmp_hour;
    this->hour;
    return *this; // 把对象自己返回回去了
}

在系统看来,任何对类成员的直接访问都被看作是通过this隐式调用的。

Time time;
time.add_hour(3); // time.add_hour(&time, 3);
  1. this指针只能在成员函数中使用,全局函数、静态函数都不能使用this指针
  2. 在普通成员函数中,this是一个指向非const对象的const指针(类型为Time,那么this就是Time *const this),表示this只能指向当前Time对象
  3. 在const成员函数中,this指针是一个指向const对象的const指针(类型为Time,那么this就是const Time *const this类型的指针)

static成员

引用static类型的成员变量时,用的是类名::成员变量名

成员函数前面也可以加一个static构成静态成员函数,调用时类名::成员函数名(......)

如何定义静态成员变量(分配内存):一般会在某个.cpp源文件的开头来定义这个静态成员变量,这样就能够保证在调用任何函数之前这个静态成员变量已经被成功初始化。 Time.h文件

public
    static int global; // 声明静态成员变量,还没有分配内存,所以也不能在这里初始化
    static void func();

project.cpp文件

include "Time.h"

// 静态成员变量定义(分配内存)
int Time::global = 15; // 可以不给初值,那么系统默认给0,定义时不需要使用static

Time.cpp文件

include "Time.h"

// 定义静态成员函数时不需要使用static
// 静态成员函数中不能使用和对象相关的成员变量
void Time::func() {
}

第四节 类内初始化、默认构造函数、=default

类相关非成员函数

类相关非成员函数:与类有关系,在类所在的.h文件和.cpp文件中声明和实现。但不在类中声明和实现。使用时直接使用 Time.cpp

include "Time.h"

// 普通函数,不是成员函数
void write_time(Time &time) {
    std::cout << time.hour << std::endl;
}

// 定义静态成员函数时不需要使用static
// 静态成员函数中不能使用和对象相关的成员变量
void Time::func() {
}

Time.h

#ifndef __MYTIME__
#define __MYTIME__

class Time {
public:
    int hour;
    int minute;
    int second;  
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second);

public:
    // 构造函数
    Time(int tmp_hour, int tmp_minute, int tmp_second);

};

void write_time(Time &time); // 普通函数声明放在.h文件中

project.cpp

include "Time.h"

int main() {
    Time time(12, 15, 17);
    write_time(time);
}

类内初始化

在c++11里,我们可以为类内成员变量提供一个初始值,那么我们在创建对象时,这个初始化值就用来初始化该成员变量。(如果构造函数的初始化列表为该变量赋值了,则变量的值为初始化列表中值)

const成员变量的初始化

const成员变量的初始化需要在构造函数的初始化列表中进行,或者在定义变量时直接初始化,不可以通过赋值初始化。

默认构造函数

默认构造函数:没有参数的构造函数。

生成对象时会调用类的构造函数,没有构造函数,这些类对象会进行“默认初始化”。也就是说,这个类通过一个特殊的构造函数来执行默认的初始化过程,这个特殊的构造函数就叫做“默认构造函数”,也就是无参数的构造函数。

类定义中,如果没有构造函数的情况下,编译器就会为我们隐式的自动定义一个默认构造函数(无参)。称为“合成的默认构造函数”。一旦自己写了构造函数,编译器就不会创建“合成的默认构造函数”。

“合成的默认构造函数”作用:普通成员变量赋值一个随机值,对于有初始化值的成员变量,将初始值赋给成员变量。

=default

#ifndef __MYTIME__
#define __MYTIME__

class Time {
public:
    int hour;
    int minute;
    int second;  
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second);

public:
    // Time() {};
    Time() = default; // 编译器能够为这种我们自动生成函数体{}
};

=default只能出现在默认构造函数、复制/移动构造函数、复制/移动赋值运算符和析构函数中。

=delete

=delete:让程序员显示的禁用某个函数。

#ifndef __MYTIME__
#define __MYTIME__

class Time {
public:
    int hour;
    int minute;
    int second;  
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second);

public:
    // Time() {};
    Time() = delete; // 进展系统为Time产生“合成的默认构造函数”
};

第五节:拷贝构造函数

拷贝构造函数:如果一个类的构造函数的第一个参数,是所属的类类型的引用,如果还有其他额外参数,那么这些额外的参数还都有默认值,则这个构造函数就叫拷贝构造函数。(函数默认参数必须放在函数声明中,除非该函数没有函数声明)

拷贝构造函数的作用:会在一定的时机,被系统自动调用。

// 拷贝构造函数
Time(Time &time, int a = 5);
  1. 一个类只能有一个拷贝构造函数
  2. 建议拷贝构造函数第一个参数总是带着const
  3. 拷贝构造函数一般不要声明成explicit
Time time;              // 调用构造函数
Time time1 = time;      // 调用了拷贝构造函数
Time time2(time);       // 调用了拷贝构造函数
Time time3{ time };     // 调用了拷贝构造函数
Time time4 = { time };  // 调用了拷贝构造函数
Time time5;             // 调用构造函数

默认情况下,类对象的拷贝是每个成员变量逐个拷贝。但“成员逐个拷贝”的功能会因为我们自己定义的拷贝构造函数的存在而丢失了作用,或者说我们自己的“拷贝构造函数”取代了系统默认的“每个成员变量逐个拷贝”的这种行为。

  1. 如果我们没有为类定义一个拷贝构造函数,编译器就会帮我们定义一个“合成拷贝构造函数”
  2. 如果是编译器给我们合成的拷贝构造函数,这个合成拷贝构造函数一般也是将参数逐个拷贝到正在创建的对象中。每个成员的类型决定它如何拷贝,如果成员类型是整型,则直接把值拷贝过来,如果成员变量是类类型,则调用这个类的拷贝构造函数来拷贝。
  3. 如果自己定义了拷贝构造函数,就取代了系统合成的拷贝构造函数,这种时候就必须在自己的拷贝构造函数中给类成员赋值,以免出现类成员没有赋值就使用的情况发生。

将一个对象作为实参传递给一个非引用类型的形参时,会调用拷贝构造函数。

void func(Time time) {
    return 1;
}

Time time;
func(time); // 调用拷贝构造函数

从一个函数中返回一个对象时,系统产生了一个临时对象并且调用类的拷贝构造函数

Time func() {
    Timet time;
    return time; // 系统产生了一个临时对象并且调用了类的拷贝构造函数
}

Timet time = func();

第六节: 重载运算符、拷贝赋值运算符、析构函数

#ifndef __MYTIME__
#define __MYTIME__

class Time {
public:
    int hour;
    int minute;
    int second;  
    // 成员函数
    void initTime(int tmp_hour, int tmp_minute, int tmp_second);

public:
    // Time() {};
    Time() = delete; // 进展系统为Time产生“合成的默认构造函数”
};

重载运算符

=、==、!=、>、>=、<、<=、<<、>>、+、-、++、--、+=、-=、coutcin
int a = 4, b = 5;
if (a = b) {
    ...
}

Time time1;
Time time2;
//if (time1 == time2) {
//  ...
//}

两个对象在重载==运算符后可以进行比较。说白了,就是我们要写一个成员函数,成员函数名为opreator==,这个成员函数体里面,要写一些比较逻辑。

重载运算符本质上是一个函数,函数的正式名字:operator关键字 + 运算符

既然重载运算符本质上是一个函数,那么就会有返回类型和参数列表。

有一些运算符,如果我们不写该运算符的重载,系统会自动生成一个,例如赋值运算符的重载。

拷贝赋值运算符

Time time1; // 调用构造函数
// / 定义时初始化
Time time2 = time1; // 调用拷贝构造函数

// / 定义时初始化
Time time3 = {time1}; // 调用拷贝构造函数

// 定义 
Time time4; // 调用构造函数

// 这个是赋值预算符
time4 = time3; // 既没调用构造函数,也没调用拷贝构造函数,系统会调用一个拷贝赋值运算符。

定义时直接用=给值是定义时初始化。而定义时没有给值,在单独的一行用=给值,这个就属于用赋值运算符赋值。

我们可以自己重载赋值预算符,如果我们自己不重载,编译器也会为我们生成一个(编译器格外喜欢赋值预算符。)

编译器生成的赋值运算符重载比较粗糙,一般就是将非const成员赋值给赋值运算符左侧的对象的对应成员中去。如果类中有对象是个对象,还会调用这个对象对应类的拷贝赋值运算符。为了精确控制类的赋值动作,往往会自己来重载赋值运算符。

重载赋值运算符:有返回类型和参数列表,这里的参数就表示运算符的运算对象,比喻time2就是运算对象。

Time.h中增加如下代码:

Time & operator=(const Time&); // 重载的赋值运算符,参数用const修饰防止被修改

Time.cpp中增加如下代码:

Time& Time::operator=(const Time& tmp_opj) {
    hour = tmp_opj.hour;
    minute = tmp_opj.minute;
    second = tmp_opj.second;
    return *this; // 把对象自身返回回去
}

以上面的time4 = time3为例,time4就是this对象,time3就是operator=里面的参数

析构函数

相对于构造函数,对象在销毁的时候,会自动调用析构函数。

如果我们不自己写析构函数,编译器也会生成一个默认的析构函数。默认析构函数的函数体为空{},表示析构函数啥也没干。

析构函数也是类的成员函数,它的名字是由~ + 类名组成,没有返回值,不接受任何参数,不能被重载,所以一个给定的类,只有唯一一个析构函数。

Time::~Time() {
    // ...
}

析构函数的成员初始化

干了两件事:

  • 函数体之前:成员变量的初始化。
  • 函数体之中:成员变量的赋值等。

析构函数的成员销毁

干了两件事:

  • 执行析构函数的函数体。
  • 函数体之后,系统接管,销毁对象及对象中的各种成员。

析构函数中销毁自己new出来的东西。成员变量不是在析构函数中销毁的,是函数执行完之后,由系统隐含销毁的。

成员变量初始化与销毁时机

先定义的先初始化,销毁时先定义的后销毁。

new对象和delete对象

对于new出来的对象,什么时候delete,系统就会在什么时候去调用类的析构函数。

第七节:派生类、调用顺序、访问等级、函数遮蔽

派生类概念

类之间有一种层次关系,有父亲类,有孩子类。车这个类,当成父类(也叫基类/超类),派生出卡车、轿车,他们属于孩子类(子类/派生类)。

继承:有父亲类,有孩子类,构成了层次关系。继承这种概念,是面向对象程序设计核心思想之一。继承说白了就是,我们先定义一个类,父类中定义一些公用的成员变量、成员函数。通过继承父类来构建新的类:子类。所以,写代码时,我们只需要写和子类相关的一些内容即可。

子类一般比父类更加庞大。

继承的格式class 子类名 : 继承方式 父类名 继承方式(访问等级/访问权限):public / protected / private

定义基类human.h

#ifndef __HUMAN__
#define __HUMAN__

#include<iostream>

// 类定义/类声明
class Human {

public:
    Human();
    Human(int);

public:
    int m_age;
    char name[100];

}; // 类定义/类声明的末尾要加";"

#endif

定义派生类men.h

#ifndef __MEN__
#define __MEN__

include "human.h"

class Men : public Human { // 表示Men是Human的派生类

public:
    Men();

};

#endif

派生类对象定义时调用构造函数顺序

include "men.h"
int man {
    Men men;
}

当定义子类对象时,是要调用父类和子类的构造函数的,而且,先执行父类的构造函数的函数体,后执行子类的构造函数的函数体。

public、protected、private

image.png

函数遮蔽

  1. 当父类存在名为func()的函数,而子类总不存在同名函数时,子类对象.func()会调用父类中的函数。
  2. 当子类中存在与父类中同名的函数时,那么父类中该同名函数及其重载函数,子类均无法访问。
  3. 可以在子类中,使用父类::函数名的方式强制访问父类函数。
  4. c++11中,通过using关键字,让父类的同名函数在子类中可见,说白了就是"让父类同名函数在子类中以重载的方式使用"

关于第四点的说明:

  1. 使用方式:using 父类名::父类中函数名,只能指定函数名,凡是指定的且在基类中是public的函数,在子类中都可见,若函数存在重载,全部重载函数均可见,无法让部分重载函数可见。
  2. 引入using的目的是用来实现子类对象中调用父类的重载版本。该函数在父类中的参数(类型或者个数)应该与子类不同。

第八节:基类指针、虚纯虚函数、多态性、虚析构

基类指针、派生类指针

定义派生类human.h

#ifndef __HUMAN__
#define __HUMAN__

// 类定义/类声明
class Human {

public:
    Human();

public:
    virtual void eat();

public:
    int m_age;

}; // 类定义/类声明的末尾要加";"

#endif

定义派生类men.h

#ifndef __MEN__
#define __MEN__

#include "human.h"

class Men : public Human { // 表示Men是Human的派生类

public:
    Men();

public:
    void eat() override;
};

#endif

定义派生类women.h

#ifndef __WOMEN__
#define __WOMEN__

#include<iostream>
#include "human.h"

class Women : public Human {

public:
    Women();

public:
    void eat() override;
};

#endif

父类指针可以new一个子类对象并指向子类成员对象,这样可以使我们只定义一个对象指针,就能够调用父类,以及各个子类的同名函数。但这个对象指针必须是父类类型。

Human *ptr = new Men();
ptr->eat();

虚函数

如果我们想通过一个父类指针调用父类、子类中的同名同参同返回值的函数的话,在父类中,该函数声明之前必须加上virtual关键字将函数声明成虚函数。

一旦某个函数(在基类)被声明成了虚函数,那么所有派生类(子类)中该函数都是虚函数。

Human *ptr = new Men(); 
ptr->eat();  // 调用子类(Men)的eat函数
ptr->Human::eat(); // 调用父类(Human)的eat函数
delete ptr;

ptr = new Women();
ptr->eat();  // 调用子类(Women)的eat函数
delete ptr;

ptr = new Human();
ptr->eat();  // 调用父类(Human)的eat函数
delete ptr;

override

override就是用来说明派生类中的虚函数,虚函数在子类中用override关键字修饰以后,编译器会认为使用子类中的函数覆盖父类中的同名函数(只有虚函数才存在子类可以覆盖父类中同名函数的问题),那么编译器就会在父类中找同名同参同返回值的虚函数,如果没找到,编译就会报错,这样,如果不小心在子类中把虚函数写错了名字或者参数或者返回值,编译器可以帮忙纠错。

    void eat() override;

final

final也是虚函数专用,是用在父类中的,如果将父类中的虚函数声明为final,那么任何尝试覆盖该函数的操作都将引发错误。

调用虚函数执行的是“动态绑定”。

动态:表示的就是在程序运行时才能知道调用了哪个子类的虚函数。

动态绑定:运行的时候才决定父类对象指针绑定到那个函数上运行。

Human *ptr = new Men(); 
ptr->eat();  // 动态绑定,程序运行时才知道调用的是Men

Men men;
men.eat(); // 调用的肯定是Men

Women women;
women.eat(); // 调用的肯定是Women

Human human;
human.eat(); // 调用的肯定是Human

多态性

多态性是针对虚函数来说的。

多态性:体现在具有继承关系的父类和子类之间,子类重新定义(重写)父类的成员函数,同时父类把这个成员函数声明称virtual虚函数。通过父类指针。只有到了程序运行时期,找到动态绑定到父类指针上的对象,这个对象有可能是某个子类对象,也有可能是父类对象。然后系统内部实际上要查一个虚函数表,找到虚函数的入口地址,从而调用父类或者子类的虚函数,这就是运行时期的多态性。

纯虚函数

    virtual void eat() = 0; // 纯虚函数,没有函数体,只有一个函数声明

纯虚函数是在基类中声明的虚函数,但它在基类中没有定义,但是要求任何派生类都要定义该虚函数自己的实现方法。

基类中实现纯虚函数的方法是在函数原型后增加 = 0

注意:

  1. 一旦一个类中有纯虚函数了,那么就不能生成这个类的对象了,这个类就成了“抽象类”。“抽象类”不能用来生成对象,主要目的是用来统一管理子类对象,即主要用于当作基类来生成子类。
  2. 子类中必须要实现该基类中定义的纯虚函数。

基类的析构函数一般写成虚函数(虚析构函数)

Human *ptr = new Men(); 
ptr->eat();  
delete ptr; // 只执行了父类(human)的析构函数,没有执行子类(Men)的析构函数

用基类指针new子类对象时,在delete时系统不会自动调用派生类的析构函数。

解决方式: 将基类的析构函数声明为virtual

virtual ~Human();

在public继承中,基类对派生类及其对象的操作,只能影响到那些从基类继承下来的成员,如果想要用基类对非继承成员进行操作,则要把基类的这个函数定义为虚函数,析构函数自然也是如此。另外就是基类中析构函数的虚属性会被继承给子类,这样的话子类中的析构函数也就自然而然成为了虚函数(虽然名字和基类中析构函数不同)。delete ptr时要调用父类的析构函数,但在父类的析构函数中要是想要你调用子类的析构函数,那么父类中的析构函数要声明为virtual的,也就是说c++中为了获得运行时的多态行为,所调用的成员函数必须得是virtual的。

结论:

  1. 如果一个类要做基类,务必要把这个类的析构函数写成virtual析构函数。
  2. 如果基类的析构函数是虚函数,就能够保证delete基类指针时运行正确的析构函数版本。

第九节:友元函数、友元类、友元成员函数

友元函数

友元函数:通过将函数声明为某个类的友元函数,那么这个函数就可以访问类的所有成员(成员变量、成员函数),不受权限修饰符的限制。 func.h

#ifndef FUNC_H
#define FUNC_H

#include <iostream>
#include "men.h"

void func(const Men &men);

#endif //FUNC_H

func.cpp

#include "func.h"

void func(const Men &men) {
    men.test_friend();
}

men.h

#ifndef __MEN__
#define __MEN__

#include "human.h"

using namespace std;

class Men : public Human { // 表示Men是Human的派生类

public:
    Men();

public:
    void eat() override;

private:
    void test_friend() const {
        cout << "调用函数test_friend" << endl;
    }

    // 因为友元函数不属于类成员,所以友元函数不受public、protected、private的限制。
    friend void func(const Men &men); //  该函数是men的友元函数
};

#endif

友元类

可以把类定义为友元类,例如将类B定义为类A的友元类,此时在类B的成员函数中可以访问类A的所有成员,不受权限修饰符限制。

using namespace std;

class B; // 类B的声明

class A {
private:
    int data;
    friend class B; // 友元类的声明,虽然此时类A没有报错,编译器此时没有判断类B有没有存在,不同编译器可能会报错,所以最好在上面添加类B的声明
};

class B {
public:
    void call(int num, A &a) {
        a.data = num; // 正常情况下不行,友元类可以
        cout << a.data << endl;
    }
};

注意:

  1. 每个类负责控制自己的友元类和友元函数
  2. 友元关系不能继承
  3. 友元关系是单向的,比如上面类B是类A的友元类,但不表示类A是类B的友元类
  4. 友元类没有传递性,比如类B是类A的友元类,类C是类B的友元类,但不表示类C是类A的友元类

友元成员函数

a.h

#ifndef PRACTISE_A_H
#define PRACTISE_A_H

#include "b.h" //因为在a.cpp中需要用到B类的成员函数了,因此不能只是声明B类了,而是要直接将B的定义包含进来

class A {
// call既是类b中的成员函数,也是类a的友元函数,因此叫做是类a的友元成员函数
    friend void B::call(int num, A &a); // 该函数是友元成员函数的声明

private:
    int data;
};

#endif //PRACTISE_A_H

a.cpp

#include "a.h"

b.h

#ifndef PRACTISE_B_H
#define PRACTISE_B_H
//#include"a.h"
//此时如果你把整一个类A的定义都包含进来了,反而会出错!
//因为此时直接告诉你a中的data是私有的成员变量,在类外一定是不可以给访问的!
//这样把整一个A类包含进来反而会报错!
//因为在b.h中只需要用到A类这个类名字而已,不需要用到A类的成员,因此只需要声明一下A类即可!

//但凡是用到其他头文件中的类时,我们最好还是 事先 来声明一下这个类
//先告知编译器我后面会用到A这个类,先不要"急着"报错!
//虽然在VS2022中你不事先来声明A这个类也是ok的,也不报错,但是你并不能确保其他编译器也不报错!
class A; // 类A的声明,表示有类A这个类型
class B {
public:
    void call(int num, A &a); // 只有public的成员函数才能成为其他类的友元成员函数
};

#endif //PRACTISE_B_H

b.cpp

#include <iostream>
#include "a.h"
//因为在B.cpp中需要用到A类的成员变量了,因此不能只是声明A类了,而是要直接将A的定义包含进来
#include "b.h"

void B::call(int num, A &a) {
    a.data = num;
    std::cout << a.data << std::endl;
}

main.cpp

#include "a.h"
#include "b.h"

int main() {
    A a;
    B b;
    b.call(10, a);
}

总结,友元的优缺点:

优点:允许在特定的请看下,让某些非成员函数or另一个类的成员函数可以来访问我这个类的protected以及private访问权限的成员,友元的概念就是为了这么干而被提出来的!这样会使得在这个类的外部去访问protected以及private访问权限的成员的操作更加灵活!

缺点:但这也会破坏类的封装性,降低了类的可靠性和可维护性。

第十节:RTTI、dynamic_cast、typeid、虚函数表

RTTI介绍

RTTI(Run Time Type Identification):运行时类型识别。

具体一点就是:在程序运行时,程序能够使用基类的指针or引用来检查这些指针or引用所指向的对象的实际派生类型。我们也可以把RTTI这种称呼看成啥一种系统提供给我们的一种能力,或者说是一种功能。这种功能是通过2个运算符来体现到:

  1. dynamic_cast运算符:能够将基类的指针or引用安全地转换为派生类的指针or引用。
  2. typeid运算符:返回指针or引用所指向对象的实际的类型。

补充

要想让上述两个运算符能够正常地工作,那么基类中必须至少要有一个虚函数,不然这2个运算符工作的结果就可能与我们预想的有天壤之别。因为,只有存在虚函数时,这2个运算符才会使用指针or引用所绑定的对象的动态类型。

结论

基类中必须有至少一个虚函数,才能让RTTI的这2个运算符发挥正确的作用!

(更加深层次的原理可能得到学习深度探索对象模型的第三章,但是其实我们不用这么折腾地把这种概念性的东西理解地很深奥,说白了你当前阶段会用虚函数来重写子类中与基类同名的函数就可以了)

dynamic_cast运算符

格式:

类名 * 指针名 = dynamic_cast<另一个类名* >(对应的指针);

注意:

程序运行时,dynamic_cast关键字的操作数必须要包含多态类的类型(什么叫多态?什么叫多态类的类型呢?其实我们并不需要这么学术地去称呼这些听起来这么深奥的概念,就是一句话:要使用dynamic_cast关键字对基类的指针or引用做类型转换的话就必须要让基类的函数中至少有一个是虚函数,否则你根本就没法通过dynamic_cast关键字do类型转换进而简化你写的多态代码)

为什么用dynamic_cast关键字可以简化我们写的多态的代码呢?其实很简单,你用基类指针指向子类的对象时,要是想让子类重写其与基类中同名的函数的话,就必须要用虚函数(有时我们甚至直接把基类用做是一个抽象类,直接用写为纯虚函数呢),那么你每一个同名函数就都需要给它在基类的声明中加上virtual关键字,有时这样的操作就会是复杂的繁琐的操作。因此我们才有dynamic_cast关键字来给我们做简化的事情

请看以下代码:(以上讲解了这么多文字,那么下面我就直接通过代码来带你学习一下哈!)

用dynamic_cast转指针类型:

Human.h

#ifndef __HUMAN_H__
#define __HUMAN_H__
#include<iostream>
using namespace std;
class Human {
public:
	int m_Age;
	Human():m_Age(0) {}
	virtual void eat() { cout << "Human要吃饭!" << endl; }
};
#endif __HUMAN_H__

Man.h

#ifndef __MAN_H__
#define __MAN_H__
#include<iostream>
#include"Human.h"
using namespace std;
class Man :public Human{
public:
	Man()  {}
	virtual void eat() { cout << "Man要吃饭!" << endl; }
	void manfunc() { cout << "调用了manfunc()!" << endl; }
};
#endif __MAN_H__
main.cpp

#include <iostream>
#include"Human.h"
#include"Man.h"
using namespace std;
void test() {
    Human* pHuman = new Man;
    Man* pman = dynamic_cast<Man*>(pHuman);
    if (pman != nullptr) {//类型转换成功!
        cout << "pHuman实际上是指向一个Man类型的指针!" << endl;
        //在这里因为用dynamic_cast动态的把pHuman指针指向Man类的对象了,因此
        //在这里边去操作Man类的成员函数、成员变量就是非常安全的了!
        pman->manfunc(); 
    }
    else {//类型转换失败!
        cout << "pHuman实际上并不是指向一个Man类型的指针!" << endl;
        //pHuman->manfunc();//do不了!因为dynamic_cast类型转换失败了!
    }
}
int main(){
    test();
    return 0;
}

用dynamic_cast转引用类型:

Human* pHuman = new Man;
Human& q = *pHuman;
//用try-catch语句来接受转换失败时可能抛出的bad_cast的异常!
try {
    Man& manbm = dynamic_cast<Man&>(q);
    //if转换不成功,则流程直接进入到catch里边去。如果转换成功,则流程继续走下去!
    //走到这里,表示转换成功
    cout << "pHuman实际上是一个Man类型!" << endl;
    manbm.manfunc();
}
catch (std::bad_cast) {
    cout << "pHuman实际上不是一个Man类型!" << endl;
}

typeid运算符

typeid的作用:拿到对象类型信息,并返回一个常量const对象的引用。这个常量对象其实就是一个标准库中定义的type_info类的类型。(我们如果想输出这个常对象的引用,就可以调用该type_info类的成员函数name()来输出!)

格式:

typeid(内置/自定义类型);or typeid(表达式)

请看以下代码:(直接通过代码来学习)

当在Human类和Man类中不声明任何的虚函数时

Human.h

#ifndef __HUMAN_H__
#define __HUMAN_H__
#include<iostream>
using namespace std;
class Human {
public:
	int m_Age;
	Human():m_Age(0) {}
	void eat() { cout << "Human要吃饭!" << endl; }
};
#endif __HUMAN_H__
Man.h

#ifndef __MAN_H__
#define __MAN_H__
#include<iostream>
#include"Human.h"
using namespace std;
class Man :public Human{
public:
	Man()  {}
	void eat() { cout << "Man要吃饭!" << endl; }
	void manfunc() { cout << "调用了manfunc()!" << endl; }
};
#endif __MAN_H__

main.cpp

#include <iostream>
#include"Human.h"
#include"Man.h"
#include"Woman.h"
using namespace std;
void test() {
    Human* pHuman = new Man;
    cout << typeid(*pHuman).name() << endl;
    delete pHuman;
}
int main(){
    test();
    return 0;
}

执行结果

image.png

当在Human类中声明一个虚函数时(只需要在Human.h中将eat函数声明为virtual)

Human.h

#ifndef __HUMAN_H__
#define __HUMAN_H__
#include<iostream>
using namespace std;
class Human {
public:
	int m_Age;
	Human():m_Age(0) {}
	virtual void eat() { cout << "Human要吃饭!" << endl; }
};
#endif __HUMAN_H__

运行结果:

image.png

通过上述学习typeid的使用的代码,这也侧面地说明了如果你在写多态代码时,如果写了virtual虚函数时,就可以让基类指针动态绑定到子类的对象上;而如果你没写virtual虚函数的话,那么你就算用了多态(也即让基类指针指向子类对象),你也没办法让基类指针动态绑定到子类的对象上,也即该基类指针实际上还只是指向基类的对象而已。上面的运行结果很好地说明了这一点。

typeid可以用于查看内置的数据类型的类型:(此时typeid就是简单地把定义该变量/对象时的数据类型返回回来而已)

int arr[10]{ 5,1 };
int b = 120;
cout << typeid(arr).name() << endl;//int [10]
cout << typeid(b).name() << endl;//int
cout << typeid(19.68).name() << endl;//double
cout << typeid(pair<int,int>(19, 68)).name() << endl;//std::pair<int,int>
cout << typeid("MyNameIs").name() << endl;//const char [9] or char const [9]

image.png

但是,typeid多用于比较两个指针是否都指向同一种类型的对象

case 1 : 若两个指针定义的类型相同,则不管他们new的是啥,其指针的typeid都相同

//还是以上述写的类作为代码的例子:
Human* phuman = new Man;
Human* phuman2 = new Human;
if (typeid(phuman).name() == typeid(phuman2).name()) {
    cout<<"phuman和phuman2是同一种类型[看指针定义的类型是啥"
    <<"那么typeid(pointer_Name)就是啥指针类型]"<<endl;
}

运行结果:

image.png

case 2:若两个指针所指向的对象的类型相同,其*指针(解引用)的typeid都相同

Human* phuman = new Man;
Human* phuman2 = new Man;
Man* pman3 = new Man;
if (typeid(*phuman).name() == typeid(*phuman2).name()) {
    cout << "*phuman和*phuman2所指向的对象是同一种类型[看指针所指向的实际对象的类型是啥"
    << "那么typeid(pointer_Name)就是啥指针类型]" << endl;
}
if (typeid(*pman3).name() == typeid(*phuman2).name()) {
    cout << "*pman3和*phuman2所指向的对象是同一种类型[看指针所指向的实际对象的类型是啥"
    << "那么typeid(*pointer_Name)就是啥对象类型]" << endl;
}

所以,从上面的代码我们()可以看出来,比较两个指针所指向的对象的类型是否一样时,一定要这样写:

typeid(*指针1名).name() == typeid(*指针2名).name()

否则的话如果你把*星号拉下了的话,这只是比较一下两个指针定义时的类型而已,这样是不能达到我们的原始目的的。

type_info类

在我们使用typeid时,其会返回一个常量对象的引用,而这个常量对象引用就是一个标准库中定义好了的类的类型,而这个类就是type_info类

type_info类的一些常用的成员变量/方法

name()

返回typeid(里面的类型)的类型名

int a = 1, arr[10]{ 1,2, };
float f = 1.1;
double b = 2.288;
    
//常量 对象的引用
const type_info& tpi1 = typeid(a);
cout << tpi1.name() << endl;//int
cout << typeid(a).name() << endl;//int
cout << "-------------------------------" << endl;
const type_info& tpi2 = typeid(arr);
cout << tpi2.name() << endl;//int [10]
cout << typeid(arr).name() << endl;//int [10]
cout << "-------------------------------" << endl;
const type_info& tpi3 = typeid(f);
cout << tpi3.name() << endl;//float
cout << typeid(f).name() << endl;//float
cout << "-------------------------------" << endl;
const type_info& tpi4 = typeid(b);
cout << tpi4.name() << endl;//double
cout << typeid(b).name() << endl;//double

image.png

如果说在写多态的代码时,你的基类不加些任何的虚函数,那么你的就算用基类指针指向子类的对象,实际上这个基类的指针也还是指向的是基类的对象的!一旦你在基类中写了至少一个虚函数,那么此时你的基类指针实际上就回动态地绑定到子类的对象中,也即此时你的基类指针会指向子类对象了! 当Human基类中不含有任何虚函数时,运行下面这段代码:

Human* phuman1 = new Human;
Human* phuman2 = new Man;
const type_info& tpiHuman1 = typeid(*phuman1);
cout << tpiHuman1.name() << endl;
 
const type_info& tpiHuman2 = typeid(*phuman2);
cout << tpiHuman2.name() << endl;

image.png

当Human基类中含有至少一个虚函数时,运行刚才那段代码:

运行结果:

image.png

operator==()等号以及operator!=()不等号

这是标准库中给type_info对象重载的运算符函数

int a1 = 1, a2 = 1;
double a3 = 2.1;
const type_info& tpia1 = typeid(a1);
const type_info& tpia2 = typeid(a2);
const type_info& tpia3 = typeid(a3);
if (tpia1 == tpia2)cout << "tpia1 == tpia2" << endl;
if (tpia1.operator==(tpia2))cout << "tpia1 == tpia2" << endl;
cout<<"--------------------------" << endl;
if (tpia1 != tpia3)cout << "tpia1 != tpia3" << endl;
if (tpia1.operator!=(tpia3)) cout << "tpia1 != tpia3" << endl;

运行结果:

image.png

RTTI与虚函数表

在C++中,如果类中含有虚函数,那么编译器就会为该类产生一个虚函数表。而虚函数表中有很多项,每一项都是一个指针。每个指针所指向的是这个类中的各个虚函数的入口地址。虚函数表的项中,第一个表项很特殊,它指向的不是虚函数的入口地址,它指向的实际上是咱们这个类所关联的type_info对象。现在我只是简要地介绍一下虚函数表的概念而已,具体学习的话还是要等学习到《深度探索对象模型》

第十一节:基类与派生类关系的详细再探讨

派生类对象模型简述

一个类,继承自一个基类,那么该类称之为子类(派生类),该子类的对象中包含两种成分(也即包含多个子对象的意思!):

  1. 一个子对象含有子类自己的对象成分(包括子类自己的成员函数以及成员变量)
  2. 另一个子对象含有基类的对象成分(包括基类自己的成员函数以及成员变量)

为什么基类的指针可以指向子类的对象呢?也即为什么可以做多态这种操作呢?基类和子类不应该是不同的类类型吗?为啥继承之后就让基类和子类的对象相关了呢?

答:基类指针可以用来new一个子类对象本质上是因为子类对象中含有基类的成分,因此,子类对象也可以当做是一个特殊的父类对象了。实际上,编译器在我们用多态时,帮我们做了隐式的,从派生类到基类的类型转化。而这种转换的好处就是,当需要用到基类引用的地方,你可以用这个派生类对象的引用来代替or当需要用到派生类引用的地方,你可以用这个基类引用来代替。因此我们就可以用多态这种知识来实现更加复杂的代码。

派生类构造函数

当我们创建一个派生类的对象时,会既调用派生类的构造函数,又会调用基类的构造函数(当然,顺序是:先调用基类的构造函数,再调用子类的构造函数)。这说明:即便派生类对象是从基类中继承过来的,派生类也无法给其基类的成员变量初始化,也即派生类的函数是没办法构造其基类成分的!

派生类实际上是使用基类的构造函数来初始化其基类成分的。也即,基类就只控制基类部分都成员初始化,而派生类就只控制派生类部分的成员的初始化。

那派生类似如何使用基类的构造函数来初始化其基类成分的呢?

答:用派生类构造函数的成员初始化列表。那该怎么做呢?很简单,

请看以下代码:(当然,这里默认了基类的构造函数是有参的,无参的话子类根本就不需要调用基类的构造函数了)

class Dad {
public:
	int m_Age;
	string m_Name;
	Dad(int age,const string& name):m_Age(age), m_Name(name) { 
		cout << "this is Dad 的构造函数!" << endl;
	}
	vitrual ~Dad() {}//为基类析构声明为virtual的!
};
class Son : public Dad {
public:
	int girlFriendNums;
    //在子类的初始化列表中,直接调用父类的构造函数并传参进进去!
	Son(int nums, int age, const string& name) :girlFriendNums(nums), Dad(age,name) {
		cout << "this is Son 的构造函数!" << endl;
	}
	virtual ~Son() {}//此时子类的析构其实本质上也是virtual的,因为你继承自Dad
};

既当父类又当子类

一个类可以既作为另一个类的子类,也可以作为再另一个类的父类。

class GrandDad{/.../}//爷爷类
class Dad: public GrandDad{//父亲类
/.../}//GrandDad类为Dad类的直接基类,为Son类的间接基类
class Son: public Dad{//孙子类
/.../}

在实际开发中,尽量少用这种多继承来写代码,不然很容易造成你写的代码难维护,也不易读。

不想当基类的类(使用final关键字)

对于不想用于基类的类,C++中给出了final关键字,可以防止我们写代码时误用了不想当基类的类作为基类!

注意: 此处的final与之前我们在函数后面加final不一样。给一个类的成员函数的原型声明后加final关键字的作用其实就是不让子类继承该类时重写该成员函数

请看以下代码:

#include<iostream>
#include<string>
using namespace std;
class Dad {
public:
	int m_Age;
	string m_Name;
	Dad(int age,const string& name):m_Age(age), m_Name(name) {}
	virtual void showInfo() final{//让不想被重写的成员函数声明为final
		cout << "Dad's Info:" << endl;
		cout << "m_Name: " << this->m_Name << endl;
		cout << "m_Age: " << this->m_Age << endl;
	}
	~Dad() {}
};
class Son : public Dad {
public:
	int m_girlFriendNums;
	Son(int nums, int age, const string& name) :m_girlFriendNums(nums), Dad(age,name) {}
	virtual void showInfo()final {
		cout << "Son's Info:" << endl;
		cout << "m_Name: " << this->m_Name << endl;
		cout << "m_Age: " << this->m_Age << endl;
		cout << "m_girlFriendNums: " << this->m_girlFriendNums << endl;
	}
	virtual ~Son() {}
};

image.png

类类型加final请看以下代码:

类型1
class Dad  final {//让不想做基类的Dad类声明为final的!
public:
/*...*/
};
class Son : public Dad {//错误!Dad不能用作基类
public:
/*...*/
};
类型2
class Dad {
public:
/*...*/
};
class Son final : public Dad {//让不想做基类的Son类声明为final的!
public:
/*...*/
};
class GrandSon:public Son{//错误!Son不能用作基类
public:
/*...*/
}

image.png

总结C++11中引入的final关键字的用法:

  1. 对于不想给重写的成员函数,用final对成员函数进行声明后,子类就不再有权限对该成员函数进行重写了
  2. 对于不想当做基类的类,用final对类进行声明后,该类就不可以给其他类用作继承时的基类了

静态类型与动态类型

静态类型:变量声明时候的类型。静态类型在代码编译时就是已知的了。

动态类型:指的是这个指针/引用 所代表的(所表达的)内存中的对象的类型。动态类型是程序运行时才已知的(因为你要看该指针or引用到底是动态绑定哪一个类型的对象上了)

注意: 静态类型和动态类型这种概念只有基类指针or引用才可能存在这种静态类型和动态类型不一致的case。

请看以下代码:

// 静态类型
Human human;//是静态类型,为Human类型
Man man;//是静态类型,为Man类型
// 动态类型:
Human* pHuman1 = new Man;//是动态类型,为Man类型,基类指针指向派生类对象
Human& p1 = *pHuman1;//是动态类型,为Man类型,基类引用绑定到派生类对象上
Human* pHuman2 = new Woman;//是动态类型,为Woman类型
Human& p2 = *pHuman2;//是动态类型,为Woman类型

派生类向基类的隐式类型转换

当我们使用多态时,编译器是隐式地帮我们执行了派生类到基类的转化工作的。

这种转换之所以以成功,是因为每一个派生类对象中都包含着基类的成分,所以基类的指针or引用是可以那么绑定到子类对象的基类部分上的。也就是说,基类对象可以独立存在,也可以作为派生类对象的一部分存在。

请看以下子类向基类do隐式转换的例子代码:

Human* pHuman1 = new Man;//Man类对象隐式地转换为pHuman1对象
Human* pHuman2 = new Woman;//Woman类对象隐式地转换为pHuman2对象

但注意:并不存在从基类到派生类的自动类型转换。(因为子类是从基类中继承过来的,因此子类中含有的成分基类中不一定含有,而基类中含有的成分子类必然是含有的,也即子类是一个全集,基类是子集的意思。因此身为子集的基类是不可以帮身为全集的子类初始化的!也根本帮不了呀!)

Man* man = new Human;//非法!不能将基类转为派生类
Human human;
Man& m = human;//非法!不能将基类转为派生类(派生类的引用不能绑定到基类对象上)
Man* pm = &human;//非法!不能将基类转为派生类(派生类指针不能指向基类地址)
Man man;
Human* phm = &man;//编译器是通过静态类型来推断是否转换为派生类的!
Man* pmy = phm;//非法!不能将基类转为派生类
//但是有一种case可以强制转!即:
//if基类中含有至少一个虚函数的话,就可以通过dynamic_cast<Type*>进行类型转换!
Man* pmy = dynamic_cast<Man* >(phm);//合法!

父类子类之间的拷贝与赋值

用子类对象初始化(拷贝给)基类对象是合法的。

Man man;
Human human(man);// 用子类对象初始化(拷贝给)基类对象

此时调用的是基类的拷贝构造函数。将其形参const Human& thuman中的thuman动态绑定到了子类对象man上。

Human(const Human& thuman) {
	cout << "拷贝构造函数!" << endl;
}

用子类对象赋值给基类对象也是合法的。

Man man;
Human human;
human = man;//用子类对象赋值给基类对象

此时调用的是基类的拷贝赋值运算符的重载函数,将其形参const Human& thuman中的thuman动态绑定到了子类对象man上。

Human& operator=(const Human& thuman) {
	cout << "拷贝赋值运算符函数!" << endl;
	return *this;
}

总结: 当子类对象用于初始化(拷贝给)or赋值给基类对象时,只会将子类中的基类成分初始化(拷贝给)or赋值给基类对象对应的成分而已,不会做多余的操作!