【C++篇】OOP下部分:友元、运算符重载与多态

news2024/12/23 13:54:34

友情链接:C/C++系列系统学习目录

知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
 
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上


文章目录

  • 🚀一、友元
    • ⛳(一)友元函数
    • ⛳(二)友元类
  • 🚀二、运算符重载
    • ⛳(一)运算符重载的基本用法
      • 🎈1.使用成员函数重载运算符
      • 🎈2.使用非成员函数【友元函数】重载运算符
    • ⛳(三)运算符重载实例
      • 1.重载赋值运算符=
      • 🎈2.重载关系运算符>、<、==
      • 🎈3.重载下标运算符[]
      • 🎈4.重载<<和>>运算符
        • (1)重载<<运算符
        • (2)重载>>运算符
      • 🎈5.重载类型转换运算符函数
        • (1)类的自动转换和强制类型转换(普通类型 -> 类类型)
        • (2)类类型 -> 普通类型
        • (3)类类型A -> 类类型B
        • (4)explicit关键字
  • 🚀三、多态
    • ⛳(一)多态的实现:虚函数
      • 🎈1.基础
      • 🎈2.虚函数表
        • (1)单个类的虚函数表
        • (2)使用继承的虚函数表
        • (3)多重继承的虚函数表
      • 🎈3.C++11 override和final
    • ⛳(二)纯虚函数和抽象类


🚀一、友元

C++是面向对象的,目的之一:封装

优点:优点之一,就是安全。

缺点:在某些特殊的场合,不是很方便

解决方案:使用友元

使用前提:某个类需要实现某种功能,但是这个类自身,因为各种原因,无法自己实现。需要借助于“外力”才能实现。

友元有两种使用形式:友元函数和友元类。

⛳(一)友元函数

使用全局函数作为友元函数(友元函数)

通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限,创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字friend:

  • 第一,虽然upgrade()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
  • 第二,upgrade()函数不是成员函数,但它与成员函数的访问权限相同。
Computer.h
class Computer
{
public:
    Computer();
    
    // 使用全局函数作为友元函数
    friend void upgrade(Computer* computer);
    
    std::string description();
    
private:
    std::string cpu; //CPU芯片
};

Computer.cpp
Computer::Computer()
{
	cpu = "i7";
}

main.cpp
 //它不是成员函数,所以不要使用Computer::限定符,另外,不要在定义中使用关键字friend
void upgrade(Computer* computer) {
	computer->cpu = "i9"; //非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。这样就可像成员函数一样直接访问对象的私有数据成员
}    

int main(void) {
    
    upgrade(&shanxing);
    
    std::cout << shanxing.description() << std::endl;
    
    system("pause");
    return 0;
}

使用另一个类的成员函数作为友元函数(友元成员函数)

Computer.h
#pragma once
#include <string>

// class ComputerService;
// 仅仅声明ComputerService不够,需要包含头文件,因为要使用类当中的方法
#include "ComputerService.h"

class Computer
{
public:
    Computer();
    
    // 使用全局函数作为友元函数
    friend void upgrade(Computer* computer);
    
    // 使用类的成员函数,作为友元函数
    friend void ComputerService::upgrade(Computer* comptuer);
    
    std::string description();
    
private:
	std::string cpu; //CPU芯片
};

ComputerService.h
#pragma once
class Computer;

class ComputerService
{
public:
	void upgrade(Computer* computer);
};

ComputerService.cpp
#include "ComputerService.h"
#include "Computer.h"
    
void ComputerService::upgrade(Computer* computer) {
	computer->cpu = "i9";
}

main.cpp
#include <stdio.h>
#include <iostream>
#include <Windows.h>
#include "Computer.h"
#include "ComputerService.h"
int main(void) {
    Computer shanxing;
    ComputerService service;
    
    std::cout << shanxing.description() << std::endl;
    
    service.upgrade(&shanxing);
    
    std::cout << shanxing.description() << std::endl;
    
    system("pause");
    return 0;
}

功能上,这两种形式,都是相同,应用场合不同。

一个是,使用普通的全局函数,作为自己的朋友,实现特殊功能。

一个是,使用其他类的成员函数,作为自己的朋友,实现特殊功能。

⛳(二)友元类

为什么要使用友元类

一个独立的咨询师, 给其他企业做服务时,这个咨询师作为企业的“友元函数”即可。

一个大型的咨询服务公司,比如 IBM(IT 事务), 普华永道(会计事务),给其他企业做服务时,使用友元函数就不是很方便了,因为需要设计很多友元函数,不方便。

解决方案:使用“友元类”

友元类的作用

如果把 A 类作为 B 类的友元类,

那么 A 类的所有成员函数【在 A 类的成员函数内】,就可以直接访问【使用】B 类的私有成员。即,友元类可以直接访问对应类的所有成员!!!

使用注意

友元类,和友元函数,使用 friend 关键字进行声明即可,与访问权限无关,所以,可以放在 private/pulic/protected 任意区域内。

Computer.h

#pragma once
#include <string>

class ComputerService;

class Computer
{
public:
    Computer();
    std::string description();
    
private:
    std::string cpu; //CPU芯片
    
    // 友元类
    friend class ComputerService;
};

Computer.cpp

#include "Computer.h"
#include <sstream>
Computer::Computer()
{
	cpu = "i7";
}

std::string Computer::description()
{
    std::stringstream ret;
    ret << "CPU:" << cpu;
    return ret.str();
}

ComputerService.h

#pragma once

class Computer;

class ComputerService
{
public:
    void upgrade(Computer* computer);
    void clean(Computer* computer); //计算机清理
    void kill(Computer* computer); //杀毒
};

ComputerService.cpp

#include "ComputerService.h"
#include "Computer.h"
#include <iostream>

void ComputerService::upgrade(Computer* computer) {
	computer->cpu = "i9";
}

void ComputerService::clean(Computer* computer)
{
    std::cout << "正在对电脑执行清理[CPU:"
    << computer->cpu << "]..."
    << std::endl;
}
void ComputerService::kill(Computer* computer)
{
    std::cout << "正在对电脑执行杀毒[CPU:"
    << computer->cpu << "]..."
    << std::endl;
}

main.cpp

#include <stdio.h>
#include <iostream>
#include <Windows.h>
#include "Computer.h"
#include "ComputerService.h"

int main(void) {
    Computer shanxing;
    ComputerService service;
    
    std::cout << shanxing.description() << std::endl;
    
    service.upgrade(&shanxing);
    service.clean(&shanxing);
    service.kill(&shanxing);
    
    std::cout << shanxing.description() << std::endl;
    system("pause");
    return 0;
}

🚀二、运算符重载

为什么要使用运算符重载

C/C++的运算符,支持的数据类型,仅限于基本数据类型。

问题:

一头牛+一头马 = ?(牛马神兽?)

一个圆 +一个圆 = ? (想要变成一个更大的圆)

一头牛 – 一只羊 = ? (想要变成 4 只羊,原始的以物易物:1 头牛价值 5 只羊)

解决方案:

使用运算符重载,运算符重载是一种形式的C++多态,实际上,很多C++(也包括C语言)运算符已经被重载。例如,将*运算符用于地址,将得到存储在这个地址中的值;但将它用于两个数字时,得到的将是它们的乘积。C++根据操作数的数目和类型来决定采用哪种操作。

⛳(一)运算符重载的基本用法

🎈1.使用成员函数重载运算符

要重载运算符,需使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:

operatorop(argument-list)

例如,operator +( )重载+运算符,operator*( )重载*运算符。op必须是有效的C++运算符,不能虚构一个新的符号。例如,不能有operator@( )这样的函数,因为C++中没有@运算符。operator[ ]函数将重载[ ]运算符,因为[ ]是数组索引运算符

district2 = sid + sara;
//编译器发现,操作数是Salesperson类对象,因此使用相应的运算符函数替换上述运算符:
district2 = sid.operator+ (sara) ;

这两种表示法都将调用operator +( )方法。注意,在运算符表示法中,运算符左侧的对象(这里为sid)是调用对象,运算符右边的对象(这里为sara)是作为参数被传递的对象。函数将隐式地使用sid(因为它调用了方法),而显式地使用sara对象(因为它被作为参数传递),来计算总和,并返回这个值

运算符重载的禁区和规则:

  1. 为了防止对标准类型进行运算符重载,C++规定重载运算符的操作对象至少有一个不是标准类型,而是用户自定义的类型比如不能重载 1+2

    但是可以重载 cow + 2 和 2 + cow // cow 是自定义的对象

  2. 不能改变原运算符的语法规则, 比如不能把双目运算符重载为单目运算

  3. 不能修改运算符的优先级。因此,如果将加号运算符重载成将两个类相加,则新的运算符与原来的加号具有相同的优先级。

  4. 不能创建新运算符。例如,不能定义operator **( )函数来表示求幂。

  5. 不能对以下这四种运算符,使用友元函数进行重载

    = 赋值运算符,()函数调用运算符,[ ]下标运算符,->通过指针访问类成员

    大多数运算符都可以通过成员或非成员函数(一般都是友元函数)进行重载,但这四个运算符只能通过成员函数进行重载。

    重载的运算符(有些例外情况)不必是成员函数

  6. 不能对禁止重载的运算符进行重载

在这里插入图片描述

Cow.h
#pragma once

class Pork;
class Goat;

class Cow
{
public:
    Cow(int weight);
    
    
    Pork operator+(const Cow& cow) ; //同类型进行运算,很频繁
    // 1、参数此时定义为引用类型,更合适,避免拷贝
    // 2、实际上,这里的参数前面还有一个参数不用指定,默认是本对象
    
    Pork operator+(const Goat& goat) ; //不同类型进行运算,比较少见
private:
    int weight = 0;
};


Cow.cpp 
#include "Cow.h"
#include "Pork.h"
#include "Goat.h"
Cow::Cow(int weight)
{
	this->weight = weight;
}

// 规则:
// 一斤牛肉:2斤猪肉
// 一斤羊肉:3斤猪肉
Pork Cow::operator+(const Cow &cow)
{
    int tmp = (this->weight + cow.weight) * 2;
    return Pork(tmp);
}
Pork Cow::operator+(const Goat& goat)
{
    // 不能直接访问goat.weight
    //int tmp = this->weight * 2 + goat.weight * 3;
    int tmp = this->weight * 2 + goat.getWeight() * 3;
    return Pork(tmp);
}

Goat.h 
#pragma once
class Goat
{
public:
    Goat(int weight);
    int getWeight(void) const;
private:
	int weight = 0;
};   

Goat.cpp   
#include "Goat.h"
Goat::Goat(int weight) {
	this->weight = weight;
}
int Goat::getWeight(void) const
{
	return weight;
}

Pork.h
#pragma once
#include <iostream>
class Pork
{
public:
	Pork(int weight);
	std::string description(void);
private:
	int weight = 0;
};

Pork.cpp
#include "Pork.h"
#include <sstream>
Pork::Pork(int weight)
{
	this->weight = weight;
}
std::string Pork::description(void)
{
    std::stringstream ret;
    ret << weight << "斤猪肉";
    return ret.str();
}

main.cpp
#include <iostream>
#include "Pork.h"
#include "Cow.h"
#include "Goat.h"
int main(void) {
    Cow c1(100);
    Cow c2(200);
    // 调用c1.operator+(c2);
    // 相当于:Pork p = c1.operator+(c2);
    Pork p = c1 + c2;
    std::cout << p.description() << std::endl;
    
    Goat g1(100);
    p = c1 + g1;
    std::cout << p.description() << std::endl;
    
    system("pause");
    return 0;
}

🎈2.使用非成员函数【友元函数】重载运算符

Cow.h
#pragma once
class Pork;
class Goat;

class Cow
{
public:
    Cow(int weight);
    // 有友元函数实现运算符重载
    friend Pork operator+(const Cow& cow1, const Cow& cow2);
    friend Pork operator+(const Cow& cow1, const Goat& goat); //这里就要加上第一个参数
private:
	int weight = 0;
};

main.cpp
Pork operator+(const Cow &cow1, const Cow &cow2)
{
    int tmp = (cow1.weight + cow2.weight) * 2;
    return Pork(tmp);
}
Pork operator+(const Cow& cow1, const Goat& goat)
{
    int tmp = cow1.weight * 2 + goat.getWeight() * 3;
    return Pork(tmp);
}

为何需要友元函数:

A= B*2.75 ;
//将被转换为下面的成员函数调用:
A= B.operator*(2.75;

但下面的语句又如何呢?
A= 2.75 * B;//cannot correspond to a member function

从概念上说,2.75 * B应与B *2.75相同,但第一个表达式不对应于成员函数,因为2.75不是一种类型的对象。记住,左侧的操作数应是调用对象,但2.75不是对象。因此,编译器不能使用成员函数调用来替换该表达式。

可以使用非成员函数来解决(记住,大多数运算符都可以通过成员或非成员函数来重载)非成员函数不是由对象调用的,它使用的所有值(包括对象)都是显式参数,如此:

A= 2.75 * B;
//与下面的非成员函数调用匹配:
A=operator* (2.75,B);

该函数的原型如下:

Time operator* (double m,const Time & t) ;

对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数

使用非成员函数可以按所需的顺序获得操作数(先是double,然后是Time),但引发了一个新问题:非成员函数不能直接访问类的私有数据,至少常规非成员函数不能访问。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。

实际上,按下面的方式对定义进行修改(交换乘法操作数的顺序),可以将这个友元函数编写为非友元函数:

Time operator* (double m,const Time & t){
	return t *m; // use t.operator* (m)
}

两种方式的区别

使用成员函数来实现运算符重载时,少写一个参数,因为第一个参数就是 this 指针。对于成员函数版本来说,一个操作数通过this指针隐式地传递,另一个操作数作为函数参数显式地传递;对于友元版本来说,两个操作数都作为参数来传递。

两种方式的选择:

  1. 一般情况下,单目运算符重载,使用成员函数进行重载更方便(不用写参数)

  2. 一般情况下,双目运算符重载,使用友元函数更直观

方便实现 a+b 和 b+a 相同的效果,成员函数方式无法实现。

例如:

100 + cow; 只能通过友元函数来实现

cow +100; 友元函数和成员函数都可以实现

特殊情况:

(1) = () [ ] -> 不能重载为类的友元函数!!!(否则可能和 C++的其他规则矛盾),只能使用成员函数形式进行重载。

(2)如果运算符的第一个操作数要求使用隐式类型转换,则必须为友元函数(成员函数方式的第一个参数是 this 指针)

注意:

同一个运算符重载, 不能同时使用两种方式来重载,会导致编译器不知道选择哪一个(二义性)

⛳(三)运算符重载实例

1.重载赋值运算符=

就是前面讲的赋值构造函数

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    Boy& operator=(const Boy& boy);
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑马值,潜力系数
    unsigned int id; // 编号
    static int LAST_ID;
};

Boy.cpp

#include "boy.h"
#include <string.h>
#include <sstream>

int Boy::LAST_ID = 0; //初始值是0

// 注意返回类型 和参数类型
Boy& Boy::operator=(const Boy& boy)
{
    if (name) {
    delete name; //释放原来的内存
    }
    name = new char[strlen(boy.name) + 1]; //分配新的内存
    strcpy_s(name, strlen(boy.name)+1, boy.name);
    
    this->age = boy.age;
    this->salary = boy.salary;
    this->darkHorse = boy.darkHorse;
    //this->id = boy.id; //根据需求来确定是否要拷贝id
    return *this;
}
...

main.cpp

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

int main(void) {
    Boy boy1("Rock", 38, 58000, 10);
    Boy boy2, boy3;
    
    std::cout << boy1.description() << std::endl;
    std::cout << boy2.description() << std::endl;
    std::cout << boy3.description() << std::endl;
    
    boy3 = boy2 = boy1;
    std::cout << boy2.description() << std::endl;
    std::cout << boy3.description() << std::endl;
    system("pause");
    return 0;
}

注意:

注意赋值运算符重载的返回类型和参数类型。

  • 返回引用类型,便于连续赋值
  • 参数使用应用类型, 可以省去一次拷贝
  • 参数使用 const, 便于保护实参不被破坏

赋值运算符的重载,应该使用这种方式:

Boy& operator=(const Boy &boy);

就是:参数要使用引用!

如果定义成:

Boy& operator=(const Boy *boy);

将会没有效果,编译器不会识别为赋值运算符的重载,

也就是:boy2 = boy1 时不会调用这个函数

如果定义:

Boy& operator=(const Boy boy);

有效果,但是在调用时,会执行参数的传递:

比如:boy2 = boy1;

就会执行: boy2.operator=(boy1);

就会执行: const Boy boy = boy1;

就会执行: Boy 类的赋值构造函数

有两个影响:

1) 浪费性能

2) 如果没有自定义的拷贝构造函数,而且这个类又有指针成员时,就会调用自动生成的拷贝构

造函数,导致浅拷贝

如果析构函数中,对这个指针指向的内存做了释放,那就导致数据损坏或崩溃!

小结:

1)赋值运算符的重载,一定要使用引用参数

2)如果一个类有指针成员,而且使用了动态内存分配,那么一定要定义自己的拷贝构造函数【要使

用深拷贝】,避免调用自动生成的拷贝构造函数

因为自动生成的拷贝构造函数,是浅拷贝

🎈2.重载关系运算符>、<、==

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    
    Boy& operator=(const Boy& boy);
    
    bool operator>(const Boy& boy);
    bool operator<(const Boy& boy);
    bool operator==(const Boy& boy);
    
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑马值,潜力系数
    unsigned int id; // 编号
    static int LAST_ID;
};

Boy.cpp

bool Boy::operator>(const Boy& boy)
{
    // 设置比较规则:
    // 薪资 * 黑马系数 + (100-年龄)*100
    if (power() > boy.power()) {
    	return true;
    }
    else {
    	return false;
    }
}

bool Boy::operator<(const Boy& boy)
{
    if (power() < boy.power()) {
    	return true;
    }
    else {
    	return false;
        }
}

bool Boy::operator==(const Boy& boy)
{
    if (power() == boy.power()) {
    	return true;
    }
    else {
    	return false;
    }
}

main.cpp

int main(void) {
    Boy boy1("Rock", 38, 58000, 5);
    Boy boy2("Jack", 25, 50000, 10);
    
    if (boy1 > boy2) {
    	std::cout << "选择boy1" << std::endl;
    }
    else if (boy1 == boy2) {
    	std::cout << "难以选择" << std::endl;
    }
    else {
    	std::cout << "选择boy2" << std::endl;
    }
    
    system("pause");
    return 0;
}

🎈3.重载下标运算符[]

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    
    Boy& operator=(const Boy& boy);
    
    bool operator>(const Boy& boy);
    bool operator<(const Boy& boy);
    bool operator==(const Boy& boy);
    
    int operator[](std::string index);
	int operator[](int index);
    
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑马值,潜力系数
    unsigned int id; // 编号
    static int LAST_ID;
};

Boy.cpp

int Boy::operator[](std::string index)
{
    if (index == "age") {
    	return age;
    }
    else if (index == "salary") {
    	return salary;
    }
    else if (index == "darkHorse") {
    	return darkHorse;
    }
    else if (index == "power") {
    	return power();
    }
    else {
    	return -1;
    }
}

int Boy::operator[](int index)
{
    if (index == 0) {
    	return age;
    }
    else if (index == 1) {
    	return salary;
    }
    else if (index == 2) {
    	return darkHorse;
    }
    else if (index == 3) {
    	return power();
    }
    else {
    	return -1;
    }
}

main.cpp

int main(void) {
    Boy boy1("Rock", 38, 58000, 5);
    Boy boy2("Jack", 25, 50000, 10);
    
    std::cout << "age:" << boy1["age"] << std::endl;
    std::cout << "salary:" << boy1["salary"] << std::endl;
    std::cout << "darkHorse:" << boy1["darkHorse"] << std::endl;
    std::cout << "power:" << boy1["power"] << std::endl;
    
    std::cout << "[0]:" << boy1[0] << std::endl;
    std::cout << "[1]:" << boy1[1] << std::endl;
    std::cout << "[2]:" << boy1[2] << std::endl;
    std::cout << "[3]:" << boy1[3] << std::endl;
    
    system("pause");
    return 0;
}

🎈4.重载<<和>>运算符

(1)重载<<运算符

一个很有用的类特性是,可以对<<运算符进行重载,使之能与cout一起来显示对象的内容。

前面我们都是采用一个成员函数description()来对对象进行描述,现在我们通过重载使用以下操作:

cout << Father;

实际上,它已经被重载很多次了。最初,<<运算符是C和C++的位运算符,将值中的位左移,ostream类对该运算符进行了重载,将其转换为一个输出工具。前面讲过,cout是一个ostream对象,它是智能的,能够识别所有的C++基本类型。这是因为对于每种基本类型,ostream类声明中都包含了相应的重载的operator<<( )定义。因此,要使cout能够识别Time对象,一种方法是将一个新的函数运算符定义添加到ostream类声明中。但修改iostream文件是个危险的主意,这样做会在标准接口上浪费时间。相反,通过类声明来让类知道如何使用cout。

①<<的第一种重载版本

要使类知道如何使用cout,必须使用友元函数。这是什么原因呢?因为下面这样的语句使用两个对象,其中第一个是ostream类对象(cout):

cout << Father;

如果使用一个Father成员函数来重载<<,Father对象将是第一个操作数,就像使用成员函数重载*运算符那样。这意味着必须这样使用<<:

Father << cout;

这样会令人迷惑。但通过使用友元函数,可以像下面这样重载运算符:

void operator<< (ostream & os,const Time & t){
	os << t.hours << " hours, " << t.minutes << " minutes" ;
}

新的Time类声明使operatro<<( )函数成为Time类的一个友元函数。但该函数不是ostream类的友元(尽管对ostream类并无害处)。operator<<( )函数接受一个ostream参数和一个Time参数,因此表面看来它必须同时是这两个类的友元。然而,看看函数代码就会发现,尽管该函数访问了Time对象的各个成员,但从始至终都将ostream对象作为一个整体使用。因为operator<<( )直接访问Time对象的私有成员,所以它必须是Time类的友元。但由于它并不直接访问ostream对象的私有成员,所以并不一定必须是ostream类的友元。这很好,因为这就意味着不必修订ostream的定义。

新的operator<<( )定义使用ostream引用os作为它的第一个参数。通常情况下,os引用cout对象,如表达式cout << trip所示。但也可以将这个运算符用于其他ostream对象,在这种情况下,os将引用相应的对象:另一个ostream对象是cerr,它将输出发送到标准输出流——默认为显示器,但在UNIX、Linux和Windows命令行环境中,可将标准错误流重定向到文件。另外,第6章介绍的ofstream对象可用于将输出写入到文件中。通过继承(参见第13章),ofstream对象可以使用ostream的方法。这样,便可以用operator<<( )定义来将Time的数据写入到文件和屏幕上,为此只需传递一个经过适当初始化的ofstream对象(而不是cout对象)。
调用cout << trip应使用cout对象本身,而不是它的拷贝,因此该函数按引用(而不是按值)来传递该对象。这样,表达式cout << trip将导致os成为cout的一个别名;而表达式cerr << trip将导致os成为cerr的一个别名。Time对象可以按值或按引用来传递,因为这两种形式都使函数能够使用对象的值。按引用传递使用的内存和时间都比按值传递少。

②<<的第二种重载版本

前面介绍的实现存在一个问题。不允许像通常那样将重新定义的<<运算符与cout一起使用:

cout <c "Trip time: " <c trip <c "( Tuesday)|n" ; // can't do

cout << x < y;
//C++从左至右读取输出语句,意味着它等同于:
(cout << x) << y;

正如iosream中定义的那样,<<运算符要求左边是一个ostream对象。显然,因为cout是ostream对象,所以表达式cout << x满足这种要求。然而,因为表达式cout << x位于<< y的左侧,所以输出语句也要求该表达式是一个ostream类型的对象。因此,ostream类将operator<<( )函数实现为返回一个指向ostream对象的引用。具体地说,它返回一个指向调用对象(这里是cout)的引用。因此,表达式(cout << x)本身就是ostream对象cout,从而可以位于<<运算符的左侧。

可以对友元函数采用相同的方法。只要修改operator<<( )函数,让它返回ostream对象的引用即可:

ostream & operator<<(ostream & os,const Time & t){
    os << t.hours << " hours, " << t.minutes << " minutes";
    return os;
}

这个operator<<( )版本还可用于将输出写入到文件中:

#include <fstream>
...
ofstream fout;
fout.open ( "savetine.txt ");
Time trip (12,40);
fout << trip;

其中最后一条语句将被转换为这样:

operator<<(fout, trip);

(2)重载>>运算符

和<<一样,使用成员函数的方式不方便,同样使用友元函数

Boy.h

#pragma once
#include <string>

class Boy
{
public:
    Boy(const char* name=NULL, int age=0, int salary=0, int darkHorse=0);
    ~Boy();
    
    Boy& operator=(const Boy& boy);
    
    bool operator>(const Boy& boy);
    bool operator<(const Boy& boy);
    bool operator==(const Boy& boy);
    
    int operator[](std::string index);
	int operator[](int index);
    
    // 该方式不适合
    //ostream& operator<<(ostream& os) const;
    
    friend ostream& operator<<(ostream& os, const Boy& boy);
    friend istream& operator>>(istream& is, Boy& boy);
    
    std::string description(void);
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑马值,潜力系数
    unsigned int id; // 编号
    static int LAST_ID;
};

Boy.cpp

//ostream& Boy::operator<<(ostream& os) const
//{
// 		os << "ID:" << id << "\t姓名:" << name << "\t年龄:" << age << "\t薪资:"
// 		   << salary << "\t黑马系数:" << darkHorse;
// 		return os;
//}

ostream& operator<<(ostream& os, const Boy& boy) {
    os << "ID:" << boy.id << "\t姓名:" << boy.name << "\t年龄:" << boy.age << "\t薪资:"
       << boy.salary << "\t黑马系数:" << boy.darkHorse;
    return os;
}

istream& operator>>(istream& is, Boy& boy)
{
    string name2;
    is >> name2 >> boy.age >> boy.salary >> boy.darkHorse;
    boy.name = (char*)malloc((name2.length()+1) * sizeof(char));
    strcpy_s(boy.name, name2.length() + 1, name2.c_str());
    return is;
}

main.cpp

int main(void) {
   Boy boy1("Rock", 38, 58000, 5);
    Boy boy2("Jack", 25, 50000, 10);
    
    cout << boy1 << endl;
    cin >> boy1;
    cout << boy1;
    
    system("pause");
    return 0;
}

🎈5.重载类型转换运算符函数

(1)类的自动转换和强制类型转换(普通类型 -> 类类型)

可以将类定义成与基本类型或另一个类相关,使得从一种类型转换为另一种类型是有意义的。在这种情况下,程序员可以指示C++如何自动进行转换,或通过强制类型转换来完成。

可以提供一些将整数或浮点值转换为对象的方法,其实我们已经这样做了,在C++中,接受一个参数的构造函数为将类型与该参数相同的值转换为类提供了蓝图,例如,下面的构造函数用于将int类型的值转换为Boy类型:

Boy(int salary);
 
//也就是说,可以编写这样的代码:
Boy chenQi; 
chenQi = 19; 

程序将使用构造函数Boy(int salary)来创建一个临时的Boy对象,并将19作为初始化值。随后,采用逐成员赋值方式将该临时对象的内容复制到chenQi中。这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换。

只有接受一个参数的构造函数才能作为转换函数。但是如果有两个参数或者更多,可以给第一个后面的参数都提供默认值,它便可作为转换函数

要达到类似于:stone = 1000,从整数转换为类类型,其实就是要重载构造函数,就需要重载类型转换运算符

class Boy
{
public:
    //Boy(const char* name = NULL, int age = 0, int salary = 0, int darkHorse = 0);
    Boy(const char* name, int age, int salary, int darkHorse);
    ~Boy();
    
    Boy(int salary);
    Boy(const char* name);
    ...
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑马值,潜力系数
    unsigned int id; // 编号
    static int LAST_ID;
    int power() const; //综合能力值
};

Boy::Boy(int salary)
{
    const char *defaultName = "未命名";
    name = new char[strlen(defaultName) + 1];
    strcpy_s(name, strlen(defaultName) + 1, defaultName);
    
    age = 0;
    this->salary = salary;
    darkHorse = 0;
    this->id = ++LAST_ID;
}

Boy::Boy(const char* name) {
    this->name = new char[strlen(name) + 1];
    strcpy_s(this->name, strlen(name) + 1, name);
    
    age = 0;
    this->salary = 0;
    this->sdarkHorse = 0;
    this->id = ++LAST_ID;
}

int main()
{
    Boy boy1 = 10000;
    Boy boy2 = "Rock";
    
    cout << boy1 << endl;
    cout << boy2 << endl;
    
    boy1 = 20000; //boy1 = Boy(20000);
    cout << boy1 << endl;
    
    return 0;
}

只接受一个参数的构造函数定义了从参数类型到类类型的转换。如果使用关键字explicit限定了这种构造函数,则它只能用于显示转换,否则也可以用于隐式转换。

1.编译器在什么时候将使用Boy(int salary)函数呢?如果在声明中使用了关键字explicit,则Boy(int salary)将只用于显式强制类型转换,否则还可以用于下面的隐式转换。

  • 将Boy对象初始化为int值时。
  • 将int值赋给Boy对象时。
  • 将int值传递给接受Boy参数的函数时。
  • 返回值被声明为Boy的函数试图返回int值时。

在上述任意一种情况下,使用可转换为int类型的内置类型时。

2.函数原型化提供的参数匹配过程,允许使用Boy(int)构造函数来转换其他数值类型。也就是说,下面两条语句都首先将double转换为int,然后使用Boy(int)构造函数。

Boy chenQi(19.3); 
chenQi = 20.5

然而,当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了构造函数Boy(double),则编译器将拒绝这些语句,可能指出:double可被转换为double或int,因此调用存在二义性。

(2)类类型 -> 普通类型

普通类型到类类型是使用构造函数,但是构造函数只用于从某种类型到类类型的转换。要进行相反的转换,必须使用特殊的C++运算符函数——转换函数。转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。例如:

Boy chenQi(19); 
int test1 = int(chenQi);       //syntax #1
int test2 = (int)chenQi;       //syntax #2

//也可以让编译器来决定如何做:
Boy chenQi(19); 
int test3 = chenQi;
//编译器发现,右侧是chenQi类型,而左侧是int类型,因此它将查看程序员是否定义了与此匹配的转换函数。(如果没有找到这样的定义,编译器将生成错误消息,指出无法将chenQi赋给int。)

转换函数的形式:

operator typeName();
  • 转换函数必须是类方法;意味着它需要通过类对象来调用,从而告知函数要转换的值。因此,函数不需要参数。
  • 转换函数不能指定返回类型;
  • 转换函数不能有参数。
class Boy
{
public:
    //Boy(const char* name = NULL, int age = 0, int salary = 0, int darkHorse = 0);
    Boy(const char* name, int age, int salary, int darkHorse);
    ~Boy();
    
	// 特殊的运算符重载:类型转换函数,不需要写返回类型
    operator int() const;
    operator char* () const;	
    ...
private:
    char* name;
    int age;
    int salary;
    int darkHorse; //黑马值,潜力系数
    unsigned int id; // 编号
    static int LAST_ID;
    int power() const; //综合能力值
};

Boy::operator int() const
{
	return power();
}

Boy::operator char* () const
{
	return name;
}

int main()
{
    Boy boy1("Rock", 28, 10000, 5);
    Boy boy2("Rock");
    
    int power = boy1;
    char* name = boy2;
    
    cout << power << endl;
    cout << name << endl;

    system("pause");
    return 0;
}

1.当类定义了两种或更多的转换时,仍可以用显式强制类型转换来指出要使用哪个转换函数。可以使用下面任何一种强制类型转换表示法:

int power = (int)boy1;
char* name = (char*)boy2;

2.在C++98中,关键字explicit不能用于转换函数,但C++11消除了这种限制。因此,在C++11中,可将转换运算符声明为显式的

3.用一个功能相同的非转换函数替换该转换函数即可,但仅在被显式地调用时,该函数才会执行。

int Boy::Boy_to_Int() const
{
	return power();
}

int test4 = Boy.Boy_to_Int();

(3)类类型A -> 类类型B

调用对应的只有一个参数【参数的类型就是类类型 A】的构造函数

也可以使用类型转换函数,但是使用对应的构造函数更合适。

实例:

把 Boy 类型,转换为 Man 类型

Man.h

class Boy;

class Man
{
public:
    Man(const char *name, int age, int salary);
    Man(const Boy& boy);
    ~Man();
    friend ostream& operator<<(ostream &os, const Man& man);
private:
    char* name;
    int age;
    int salary;
};

ostream& operator<<(ostream &os, const Man& man)

Man.cpp

Man::Man(const Boy& boy)
{
    int len = strlen((char*)boy) + 1;
    name = new char[len];
    strcpy_s(name, len, (char*)boy);
    age = boy[AGE];
    salary = boy[SALARY];
}

main.cpp

int main()
{
    Boy boy("Rock", 28, 10000, 5);
    Man man = boy;
    
    cout << boy << endl;
    cout << man << endl;
    
    system("pause");
    return 0;
}

(4)explicit关键字

将构造函数用作自动类型转换函数似乎是一项不错的特性。然而,当程序员拥有更丰富的C++经验时,将发现这种自动特性并非总是合乎需要的,因为这会导致意外的类型转换。因此,C++新增了关键字explicit,用于关闭这种自动特性。也就是说,可以这样声明构造函数:

explicit Boy(int salary);

作用是表明该构造函数是显示的, 而非隐式的.不能进行隐式转换,但仍然允许显式转换,即显式强制类型转换! 跟它相对应的另一个关键字是 implicit, 意思是隐藏的,类构造函数默认情况下即声明为 implicit(隐式).

//示例1:
Boy chenQi; 
chenQi = 19;       //not valid if Boy(int)is declared as explicit
chenQi = Boy(19);  //ok,an explicit conversion
chenQi = (Boy)19;  //ok, old form for explicit typecast

//示例2:
#include <iostream>
#include <string>

using namespace std;
class student {
public:
    student(int _age)
    {
        age = _age;
        cout << "age=" << age << endl;
    }
    
    student(int _age, const string _name)
    {
        age = _age;
        name = _name;
        cout << "age=" << age << "; name=" << name << endl;
    }
    
    ~student()
    {
    }
    	
    int getAge()
    {
    	return age;
    }
    
    string getName() {
    
        return name;
    }
private:
	int age;
    string name;
};

int main(void) {
    student xiaoM(18); //显示构造
    student xiaoW = 18; //隐式构造
    //student xiaoHua(19, "小花"); //显示构造
    //student xiaoMei = { 18, "小美" }; //隐式构造 初始化参数列表,C++11 前编译不能通过,C++11 新增特性
    system("pause");
    return 0;
}

🚀三、多态

在C++中有两种多态性,一种是静态的多态、一种是动态的多态;

静态的多态:函数重载,看起来调用同一个函数却有不同的行为。静态:原理是编译时实现。

动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态:原理是运行时实现。

⛳(一)多态的实现:虚函数

🎈1.基础

#include <iostream>
using namespace std;

class Father {
public:
    void play() {
    	cout << "到 KTV 唱歌..." << endl;
    }
};

class Son :public Father {
public:
    void play() {
    	cout << "一起打王者吧!" << endl;
    }
};
	
void party(Father **men, int n) {
    for (int i = 0; i<n; i++) {
    	men[i]->play();
    }
}
int main(void) {
    Father father;
    Son son1, son2;
    
    //这里父类指针可以指向子类型对象,Father* P,P=&son1,p->play()因为指针是父类的,所以这里还是会调用父类的play()方法
    Father* men[] = { &father, &son1, &son2 };
    
    
    party(men, sizeof(men) / sizeof(men[0]));
    system("pause");
    return 0;
}

解决方案:通过虚函数,实现多态

class Father {
public:
    virtual void play() {
    	cout << "到 KTV 唱歌..." << endl;
    }
};

class Son :public Father {
public:
    //子类中可以写也可以不写
    virtual void play() {
    	cout << "一起打王者吧!" << endl;
    }
};
  • 多态构成条件:

    在继承中要构成多态还有两个条件:

    1. 必须通过基类的指针或者引用调用虚函数。

    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

  • 多态的本质:

    使用virtual指明虚函数,如果方法是通过引用或指针而不是对象调用的,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针指向的对象的类型来选择方法。

    程序执行时,父类指针指向父类对象,或子类对象时,在形式上是无法分辨的!只有通过多态机制,才能执行真正对应的方法。

  • 虚函数:

    1. 虚函数的定义:

    在函数的返回类型之前使用 virtual,只在成员函数的声明中添加 virtual, 在成员函数的实现中不要加 virtual

    1. 虚函数的继承:

    如果某个成员函数被声明为虚函数,那么它的子类【派生类】,以及子类的子类中,所继承的这个成员函数,也自动是虚函数。经常在基类中将派生类会重新定义的方法声明为虚方法,方法在基类中被声明为虚的后,它在派生类中将自动成为虚方法。但建议子类中也写上virtual

  • 虚析构函数:

    如果基类的析构函数不使用虚函数的形式:派生类开始从基类继承,基类的指针指向派生类的对象时,delete基类的指针时,只会调用基类的析构函数,不会调用派生类的析构函数。

    为了避免内存泄漏,而且是当子类中会有指针成员变量时才会使用到。即虚析构函数使得在删除指向子类对象的基类指针时,会先调用派生类的析构函数,再自动调用基类中的析构函数。

    基类声明了一个虚析构函数。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数

    #include <iostream>
    #include <Windows.h>
    #include <string.h>
    
    using namespace std;
    class Father {
    public:
    	Father(const char* addr ="中国"){
            cout << "执行了 Father 的构造函数" << endl;
            int len = strlen(addr) + 1;
            this->addr = new char[len];
            strcpy_s(this->addr, len, addr);
    	}
    	
        // 把 Father 类的析构函数定义为 virtual 函数时,
        // 如果对 Father 类的指针使用 delete 操作时,
        // 就会对该指针使用“动态析构”:
        // 如果这个指针,指向的是子类对象,
        // 那么会先调用该子类的析构函数,再调用自己类的析构函数
        virtual ~Father(){
        	cout << "执行了 Father 的析构函数" << endl;
            if (addr) {
                delete addr;
                addr = NULL;
            }
        }
    private:
    	char* addr;
    };
    
    class Son :public Father {
    public:
        Son(const char *game="吃鸡", const char *addr="中国")
            :Father(addr){
            cout << "执行了 Son 的构造函数" << endl;
            int len = strlen(game) + 1;
            this->game = new char[len];
            strcpy_s(this->game, len, game);
        }
        ~Son(){
            cout << "执行了 Son 的析构函数" << endl;
            if (game) {
                delete game;
                game = NULL;
            }
        }
    private:
    	char* game;
    };
    
    int main(void) {
        cout << "----- case 1 -----" << endl;
        Father* father = new Father();
        delete father;
        
        cout << "----- case 2 -----" << endl;
        Son* son = new Son();
        delete son;
        
        cout << "----- case 3 -----" << endl;
        father = new Son();
        delete father;
        
        system("pause");
        return 0;
    }
    

如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。

注意事项:

  • 构造函数不能是虚函数。创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没什么意义。

  • 析构函数应当是虚函数,除非类不用做基类。例如,假设Employee是基类,Singer是派生类,并添加一个char *成员,该成员指向由new分配的内存。当Singer对象过期时,必须调用~Singer( )析构函数来释放内存:

    Employee * pe = new Singer; // legal because Employee is base for Singer
    ...
    delete pe;                  //~Employee() or ~singer ()
    
  • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。

  • 如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,

🎈2.虚函数表

虚函数表指针:

class Father {
public:
    virtual void func1() { cout << "Father::func1" << endl; }
private: 
	int m_b = 1;
	char m_ch = 'A';
};

int main(void) {
	
	Father father;
	cout << sizeof(father) << endl;
	
    return 0;
}

在C语言讲结构时,讲到了结构体对齐规则,对于类来说也有这样一个规则,我们统一叫作内存对齐,而在类中还需要注意一个点,除了各种数据要占内存外,当我们使用虚函数时,会产生一个虚函数表指针_vfptr占一个指针的内存的。即8个字节(64位系统)

在这里插入图片描述

所以这里father类应该占4*4=16个字节

(1)单个类的虚函数表

对象中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都有一个虚表指针。那么这个虚表中到底是什么呢?我们通过下面的程序来进行分析:

//case1:类中只有变量
class Base1
{
public:
    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
    
private:
    int base1_1;
    int base1_2;
};

int main() {
    Base1 b1;
    Base1 b2;
}
  • 对象的非虚函数,保存在类的代码中!对象的内存,只存储虚函数表,即一个指针和数据成员(类的静态数据成员,保存在数据区中,和对象是分开存储的)

  • 添加多个虚函数后,对象的内存空间不变,始终存储一个虚函数指针!仅虚函数表中添加条目

  • 虚函数指针为一个二级指针,指向一个虚函数表,表中存储我们的虚函数,同一个类的多个对象,共享同一个虚函数表:

在这里插入图片描述

在这里插入图片描述

  • 对象内,首先存储的是“虚函数表指针”,又称“虚表指针”。然后再存储非静态数据成员。由此我们可以访问到整个虚函数表:

    //通过虚指针访问虚函数表并且调用虚函数表内函数实现多态,并能够任意访问虚函数
    #include <iostream>
    using namespace std;
    
    class Father {
    public:
        virtual void func1() { cout << "Father::func1" << endl; }
        virtual void func2() { cout << "Father::func2" << endl; }
        virtual void func3() { cout << "Father::func3" << endl; }
        void func4() { cout << "非虚函数:Father::func4" << endl; }
    public: //为了便于测试,特别该用 public
        int x = 100;
        int y = 200;
        static int z;
    };
    
    typedef void (*func_t)(void);
    
    int Father::z = 1;
    
    
    int main(void) {
    
        Father father;
        // 含有虚函数的对象的内存中,最先存储的就是“虚函数表”
        cout << "对象地址:" << (int*)&father << endl;
    
        int* vptr = (int*)*(int*)&father;
        cout << "虚函数表指针 vptr:" << vptr << endl;
    
        cout << "调用第 1 个虚函数: ";
        ((func_t) * (vptr + 0))();
    
        cout << "调用第 2 个虚函数:";
        ((func_t) * (vptr + 1))();
    
        cout << "调用第 3 个虚函数: ";
        ((func_t) * (vptr + 2))();
    
        cout << "第 1 个数据成员的地址: " << endl;
        cout << &father.x << endl;
        cout << std::hex << (int)&father + 4 << endl;
        cout << "第 1 个数据成员的值:" << endl;
        cout << std::dec << father.x << endl;
        cout << *(int*)((int)&father + 4) << endl;
    
        cout << "第 2 个数据成员的地址: " << endl;
        cout << &father.y << endl;
        cout << std::hex << (int)&father + 8 << endl;
        cout << "第 2 个数据成员的值:" << endl;
        cout << std::dec << father.y << endl;
        cout << *(int*)((int)&father + 8) << endl;
    
        cout << "sizeof(father)==" << sizeof(father) << endl;
    
        Father father2;
        cout << "father 的虚函数表:";
        cout << *(int*)(*(int*)&father) << endl;
        cout << "father2 的虚函数表:";
        cout << *(int*)(*(int*)&father2) << endl;
    
        system("pause");
        return 0;
    }
    

    对于类的每个对象,编译器都会为其生成一个虚函数表指针,位于该对象内存中的开头,并指向了虚函数表的位置。

    ①虚函数表指针:(int *) &father

    解释:&father得到对象father的首地址,强制转换为(int *),意为将从&father开始的4个字节看作一个整体,而&father就是这个4字节整体的首地址,就是一个指向虚函数表的指针

    (二级指针,存储的虚函数表的地址)

    ②虚函数表地址:* (int *) &father

    解释:虚函数表指针是个指向虚函数表二级指针,所以再*解引用,就是虚函数表的地址

    (虚函数表是个函数指针数组)

    ③虚函数指针(以虚函数表中的第二个虚函数指针为例子):

    解释:(int *)*(int *)&father取到的就是指向第一个虚函数的指针,那么,我们直接让这个指针+1,也就是地址移动4个字节,就是第二个虚函数指针的地址了

    ④虚函数地址(以虚函数表的第一个虚函数指针指向的虚函数为例子):*(int*) * (int *) &father

    解释:*(int *)&father就是虚函数表的地址,然后取前4个字节作为一个int*指针,这个指针就是指向第一个虚函数的指针,最后用*解引用即可

在这里插入图片描述

//通过虚指针访问虚函数表并且调用虚函数表内函数实现多态,并能够任意访问虚函数
#include <iostream>
using namespace std;

class Father {
public:
 virtual void func1() { cout << "Father::func1" << endl; }
 virtual void func2() { cout << "Father::func2" << endl; }
 virtual void func3() { cout << "Father::func3" << endl; }
 void func4() { cout << "非虚函数:Father::func4" << endl; }
private: 
 int x = 100;
 int y = 200;
 static int z;
};

typedef void (*func_t)(void);

int Father::z = 1;

int main(void) {

 Father father;
 // 含有虚函数的对象的内存中,最先存储的就是“虚函数表”
 cout << "对象地址:" << &father << endl << endl;

 cout << "得到虚函数表指针(二级指针存储的虚函数表的地址):" << (int*)&father << endl << endl;

 //前面也可以加一个int * 表示转换成指针显示,因为是个数组,其实和下面的第一个函数指针起始地址是一样的,
 //形式上自然也一样的,但是含义上不一样,和上面的对象地址到得到虚函数表指针一样
 cout << "得到虚函数表地址(虚函数表是个函数指针数组):" << * ((int*)&father) << endl << endl;


 cout << "得到第一个虚函数指针" << (int*)*(int*)&father << endl << endl;
 int* vptr = (int*)*(int*)&father;

 /*
 cout << "访问第一个虚函数" << (int*)*(int*)&father << endl << endl;
 func_t test1 = (func_t)(int*)*(int*)&father;
 test1();
 //或者
 ((func_t) * (vptr + 0))();


 存在的问题:
 1.虚函数指针之间相差的内存为4,并不是为8
 2.这两种访问虚函数的方法都有访问内存错误
 */

 cout << "得到第二个虚函数指针" << (int*) *(int*)&father+1 << endl << endl;
 int* vfptr2 = vptr + 1;

 cout << "得到第三个虚函数指针" << (int*) *(int*)&father+2 << endl << endl;
 int* vfptr3 = vptr + 2;

 system("pause");
 return 0;
}

在这里插入图片描述

网上的代码全部报错,这个问题待解决

(2)使用继承的虚函数表

#include <iostream>
using namespace std;

class Father {
public:
    virtual void func1() { cout << "Father::func1" << endl; }
    virtual void func2() { cout << "Father::func2" << endl; }
    virtual void func3() { cout << "Father::func3" << endl; }
	void func4() { cout << "非虚函数:Father::func4" << endl; }
public: //为了便于测试,特别该用 public
    int x = 100;
    int y = 200;
};

class Son : public Father {
public:
    void func1() { cout << "Son::func1" << endl; }
    virtual void func5() { cout << "Son::func5" << endl; }
};

typedef void (*func_t)(void);

int main(void) {
    Father father;
    Son son;
    
    // 含有虚函数的对象的内存中,最先存储的就是“虚函数表”
    cout << "son 对象地址:" << (int*)&son << endl;
    
    int* vptr = (int*)*(int*)&son;
    cout << "虚函数表指针 vptr:" << vptr << endl;
    
    for (int i = 0; i < 4; i++) {
    	cout << "调用第" << i + 1 << "个虚函数:";
    	((func_t) * (vptr + i))();
    }
    
    for (int i = 0; i < 2; i++) {
        // +4 是因为先存储了虚表指针
        cout << *(int*)((int)&son + 4 + i * 4) << endl;
    }
    
    system("pause");
    return 0;
}

执行效果:

在这里插入图片描述

内存分布:

在这里插入图片描述

在这里插入图片描述

(3)多重继承的虚函数表

#include <iostream>
using namespace std;
class Father {
public:
    virtual void func1() { cout << "Father::func1" << endl; }
    virtual void func2() { cout << "Father::func2" << endl; }
    virtual void func3() { cout << "Father::func3" << endl; }
    void func4() { cout << "非虚函数:Father::func4" << endl; }
public:
    int x = 200;
    int y = 300;
    static int z;
};
	
class Mother {
public:
    virtual void handle1() { cout << "Mother::handle1" << endl; }
    virtual void handle2() { cout << "Mother::handle2" << endl; }
    virtual void handle3() { cout << "Mother::handle3" << endl; }
public: //为了便于测试,使用 public 权限
    int m = 400;
    int n = 500;
};

class Son : public Father, public Mother {
public:
    void func1() { cout << "Son::func1" << endl; }
    virtual void handle1() { cout << "Son::handle1" << endl; }
    virtual void func5() { cout << "Son::func5" << endl; }
};

int Father::z = 0;

typedef void(*func_t)(void);

int main(void) {
    Son son;
    int* vptr = (int*) * (int*)&son;
    
    cout << "第一个虚函数表指针:" << vptr << endl;
    for (int i = 0; i < 4; i++) {
        cout << "调用第" << i + 1 << "个虚函数:";
        ((func_t) * (vptr + i))();
    }
    
    for (int i = 0; i < 2; i++) {
   		cout << *(int*)((int)&son + 4 + i * 4) << endl;
    }
    
    int* vptr2 = (int*) * ((int*)&son + 3);
    for (int i = 0; i < 3; i++) {
        cout << "调用第" << i + 1 << "个虚函数:";
        ((func_t) * (vptr2 + i))();
    }
    
    for (int i = 0; i < 2; i++) {
    	cout << *(int*)((int)&son + 16 + i * 4) << endl;
    }
    
    system("pause");
    return 0;
}

执行结果:

在这里插入图片描述

内存分布:

在这里插入图片描述

  • Father在前面同样取决于声明时的顺序
  • 两个类就有两张虚函数表

🎈3.C++11 override和final

(1)override:

override 仅能用于修饰虚函数。

作用:

  • 提示程序的阅读者,这个函数是重写父类的功能。
  • 防止程序员在重写父类的函数时,把函数名写错。

override 只需在函数声明中使用,不需要在函数的实现中使用。

#include <iostream>
using namespace std;

class XiaoMi {
public:
	virtual void func() { cout << "XiaoMi::func" << endl; };
};

class XiaoMi2 : public XiaoMi {
public:
    void func() override {}
    //void func() override; 告诉程序员 func 是重写父类的虚函数
    //void func1() override{} 错误!因为父类没有 func1 这个虚函数
};

int main(void) {
    XiaoMi2 xiaomi;
    return 0;
}

(2)final:

  • 用来修饰类,让该类不能被继承,理解:使得该类终结!
class XiaoMi {
public:
	XiaoMi(){}
};

class XiaoMi2 final : public XiaoMi {
	XiaoMi2(){}
};

class XiaoMi3 : public XiaoMi2 { //不能把 XiaoMi2 作为基类
};
  • 用来修饰类的虚函数,使得该虚函数在子类中,不能被重写,理解:使得该功能终结!
class XiaoMi {
public:
	virtual void func() final;
};

void XiaoMi::func() { //不需要再写 final
	cout << "XiaoMi::func" << endl;
}

class XiaoMi2 : public XiaoMi {
public:
	void func() {}; // 错误!不能重写 func 函数
};

⛳(二)纯虚函数和抽象类

什么时候使用纯虚函数

某些类,在现实角度和项目实现角度,都不需要实例化(不需要创建它的对象),

这个类中定义的某些成员函数,只是为了提供一个形式上的接口,准备让子类来做具体的实现。此时,这个方法,就可以定义为“纯虚函数”, 包含纯虚函数的类,就称为抽象类。

纯虚函数的使用方法

用法:纯虚函数,使用 virtual 和 =0

#include <iostream>
#include <string>

using namespace std;

class Shape {
public:
        Shape(const string& color = "white") { this->color = color; }
        virtual float area() = 0; //不用做具体的实现
        string getColor() { return color; }
    private:
    	string color;
};

class Circle : public Shape {
public:
	Circle(float radius = 0, const string& color="White")
		:Shape(color), r(radius){}
	float area();
private:
	float r; //半径
};

float Circle::area() {
	return 3.14 * r * r;
}

int main() {
    //使用抽象类创建对象非法!
    //Shape s;
    
    Circle c1(10);
    cout << c1.area() << endl;
    
    Shape* p = &c1;
    cout << p->area() << endl;
    
    system("pause");
    return 0;
}
  • 不能使用抽象类创建对象

  • 可以在实现文件中提供方法的定义,也可以在继承的子类中实现这个虚函数

  • 父类声明某纯虚函数后,那么它的子类:

    要么实现这个纯虚函数 (最常见)

    要么继续把这个纯虚函数声明为纯虚函数,这个子类也成为抽象类

    要么不对这个纯虚函数做任何处理,等效于上一种情况(该方式不推荐)


行文至此,落笔为终。文末搁笔,思绪驳杂。只道谢不道别。早晚复相逢,且祝诸君平安喜乐,万事顺意。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/676360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

精选MyBatis面试题

什么是MyBatis&#xff1f; MyBatis是一个半ORM&#xff08;对象关系映射&#xff09;框架&#xff0c;它内部封装了JDBC&#xff0c;加载驱动、创建连接、创建statement等繁杂的过程&#xff0c;开发者开发时只需要关注如何编写SQL语句&#xff0c;可以严格控制sql执行性能&a…

Python高级语法--迭代器和生成器的区别

迭代器 在 Python 中&#xff0c;迭代器&#xff08;iterator&#xff09;是访问集合元素的一种方式&#xff0c;它可以遍历一个序列中的元素&#xff0c;而无需事先确定序列的大小&#xff08;即无需全部载入到内存中&#xff09;&#xff0c;且支持惰性求值。使用迭代器可以…

程序员 35 岁以后就真的要返乡种田了么?如果家里没田怎么办?

前言 在科技互联网高速发展的当下&#xff0c;程序员这个职业无疑是备受关注的。然而&#xff0c;这个行业似乎总是被一种说法所笼罩&#xff1a;程序员年龄一旦超过35岁&#xff0c;就会面临职业生涯的下坡路&#xff0c;甚至需要考虑“返乡种田”。这种说法是否真实呢&#x…

【Software Testing】【期末习题库】【2023年春】【仅供参考】

文章目录 ①单选②多选③填空④判断⑤大题 类型总分占比平时成绩40%考试/考查60% 题型题量分值备注单选201’多选103’全对3’&#xff0c;错1个0’&#xff0c;少选-1’填空102’判断52’大题210’ 平时习题&#xff08;3次&#xff09;&#xff1a; ①软件测试概述 ②黑盒测…

【五子棋实战】第5章 开发五子棋前端页面

【五子棋实战】第5章 开发五子棋前端页面 页面设计原则 开发页面 ## 基础HTML骨架 ## 添加页面响应式功能 编写JS ## 获取画布对象与DOM对象 ## 定义棋子、棋盘对象 ## 定义绘画对象&#xff08;重要&#xff01;&#xff01;&#xff09; ## 初始化绘制棋盘 ## 添加点…

Network 之十三 NC-SI 原理、拓扑结构、RBT 接口及仲裁、协议格式

最近&#xff0c;正在学习 NC-SI 的使用方法&#xff0c;于是开始各种 Google 查找 NC-SI 的资料进行学习。最详细还是得直接看 NC-SI 规范文档 DSP0222&#xff0c;以下就是记录的一些感觉 NC-SI 规范中比较重要的点以及我的一些理解&#xff0c;以备后续查阅。 有任何疑问&am…

OpenGL之模板测试

文章目录 模板测试模板函数物体轮廓源代码 模板测试 当片段着色器处理完一个片段之后&#xff0c;模板测试(Stencil Test)会开始执行&#xff0c;和深度测试一样&#xff0c;它也可能会丢弃片段。接下来&#xff0c;被保留的片段会进入深度测试&#xff0c;它可能会丢弃更多的片…

Git下:Git命令使用-详细解读

目录 一、Git 安装 二、Git 配置 三、Git 工作流程 四、Git 工作区、暂存区和版本库 五、常用 Git 命令清单 1. 创建仓库 2. 增加/删除文件 3. 代码提交 4. 分支管理 5. 标签 6. 查看历史提交 7. 远程仓库同步 8. 撤销操作 六、Git 常用命令速查表 七、Git 电子…

时序预测的深度学习算法全面盘点

1.概述 深度学习方法是一种利用神经网络模型进行高级模式识别和自动特征提取的机器学习方法&#xff0c;近年来在时序预测领域取得了很好的成果。常用的深度学习模型包括循环神经网络&#xff08;RNN&#xff09;、长短时记忆网络&#xff08;LSTM&#xff09;、门控循环单元&…

第五十一章 协助调查

眼前一个红彤彤的东西缓缓升起。 旭日东升&#xff1f;可现在才升未免太晚了些&#xff0c;升起的速度也未免太快了些&#xff0c;这红日么&#xff0c;也未免太小了些&#xff0c;而且&#xff0c;刚升起的朝阳&#xff0c;也未免显得太红太亮了些。 “是谁呀&#xff0c;水烧…

C语言数据存储 —— 浮点型篇

C语言数据存储 —— 浮点型篇 前言1. 一个常见问题2. 浮点数存储规则2.1 有效数字M一些特别的规定2.2 有效数字E一些特别的规定2.2.1 E如何存入内存2.2.2 E如何从内存中取出 3. 前面问题的解释。4. 结尾 前言 浮点数在内存中的存储方式对程序员来说非常重要。理解浮点数的存储…

数据结构:二叉树经典例题(单选题)-->你真的掌握二叉树了吗?(第一弹)

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关二叉树的经典例题&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数…

消防通道堵塞识别 opencv

消防通道堵塞识别系统通过opencvpython网络模型技术&#xff0c;消防通道堵塞识别对消防通道的状态进行实时监测&#xff0c;检测到消防通道被堵塞时&#xff0c;将自动发出警报提示相关人员及时采取措施。OpenCV的全称是Open Source Computer Vision Library&#xff0c;是一个…

linux-centOS7.9通过docker安装cwmp server:drumsergio/genieacs

一、安装环境 #查看centOS版本 [rootMiWiFi-R4CM-srv network-scripts]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) #自动查找最新安装包并升级 [rootMiWiFi-R4CM-srv ~]# sudo yum upgrade 二、关闭firewalld、NetworkManager、selinux 关闭防火墙、…

【黑马头条-Java微服务项目】

黑马头条-Java微服务项目 (一)、项目介绍1.项目背景介绍(1).基本介绍(2).业务说明(3).项目术语介绍 2.技术栈说明(1).技术栈-基础六层技术(2).技术栈-服务四层技术(3).技术栈-分布 (二)、nacos环境搭建 (一)、项目介绍 1.项目背景介绍 (1).基本介绍 随着智能手机的普及&…

在线选课的微信小程序(微信前端+网站后端)

目录 一、前言 二、微信小程序端&#xff08;老师、学生&#xff09; 1.学生用户前端小程序界面 2.老师前端小程序界面 三、后端&#xff08;管理员、老师、学生&#xff09; 3.老师后端 4.管理员后端 四、代码获取与调试 一、前言 这是一个在线选课的微信小程序&#…

PID控制算法: 3、Tuning Changes(参数调整)

改变控制参数积分项对输出结果的影响 可靠的控制系统应该有能力实时变更系统的参数 The Beginner’s PID acts a little crazy if you try to change the tunings while it’s running. Let’s see why. Here is the state of the beginner’s PID before and after the param…

STM32F4 点亮灯泡【顺序点亮、按键点亮】

一、顺序点亮灯泡 ①初始化 在user.c文件中&#xff0c;我们需要对LED进行初始化设置。 在函数LED_GPIO_Config中&#xff0c;可以修改代码如下&#xff1a; /*********************************************************************** LED初始化 备注 LED 接在GPC14引脚上…

【Springboot集成Neo4j完整版教程】

&#x1f680; Neo4j &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;C…

基于Python+OpenCV图像识别的连连看辅助工具(深度学习+机器视觉)含全部工程源码及视频演示

目录 前言总体设计系统整体结构图系统流程图 运行环境Python 环境Pycharm 环境 模块实现1. 获取句柄2. 图像划分3. 建立矩阵4. 矩阵求解 系统测试工程源代码下载其它资料下载 前言 本项目目标是利用pywin32来获取游戏图像信息&#xff0c;并利用OpenCV进行识别和处理&#xff…