<C++>7 类和对象

本文最后更新于:2023年6月27日 上午

7 类和对象

面向对象程序设计方法是 20 世纪 90 年代以来软件开发方法的主流。其将问题看成是相互作用的事物的集合,即对象的集合。

对象有两个特征:状态和行为。状态即对象本身的信息,也称为属性;行为是对对象的操作。

通过对事物的抽象找出同一类对象的共同特征(静态特征)和行为(动态特征)从而得到类的概念。对象是类的一个具象,类是对象的一个抽象。

C++ 面向对象的三大特性:封装、继承、多态

7.1 封装

封装是面向对象三大特征之一

封装的意义:

  • 将属性和行为作为整体,表现生活中的事物
  • 将属性和行为加以权限限制

示例:

class Example {
public:									//[1] 访问权限:公共
	bool b = true;
	void doit() {
		cout << (b ? "Just! Do it! Make your dream come true!" : "这里头水很深,你把握不住!") << endl;
	}
};

三种访问权限:

  • public:公共权限

    随意访问

  • private:私有权限

    仅自身类内可以访问。爸爸的快乐你想象不到

    不进行说明时,成员默认为私有

  • protected:保护权限

    仅自身及子类内可以访问。爸爸的快乐我想象到了

struct 和 class 的区别:

  • struct 默认权限为 public
  • class 默认权限为 private

成员属性私有的优点:

  • 便于控制读写权限
  • 可以检测数据的有效性

下面展示一个封装:

  • 头文件(headF1.h):

    #include<iostream>
    using namespace std;
    
    class Point {
    private:
    	double x;
    	double y;
    public:
    	Point(double x, double y);
    	double getX();
    	double getY();
    };
  • 源文件(point.cpp):

    #include<iostream>
    using namespace std;
    
    #include "headF1.h"			//包含该自定义头文件
    
    Point::Point(double x, double y) {
    	this->x = x;
    	this->y = y;
    }
    double Point::getX() {		//Point:: 即,该函数是 Point 的成员函数
    	return x;
    }
    double Point::getY() {
    	return y;
    }

7.2 对象的初始化和清理

初始化就是指进行初始化,而清理就是说要清理。

怎么样,明白了吧

程序中涉及的基本数据类型的变量要先初始化再使用:

  • 对于全局变量,如果程序员声明时未进行初始化,则系统将其进行默认初始化
  • 对于局部变量,系统不会进行默认初始化。不初始化就调用时,是一个随机值

对象也要先进行初始化才能使用。因为对象的结构和行为更加复杂,需要使用构造函数完成对其初始化。

7.2.1 构造函数和析构函数

如果一个对象没有初始状态,对其使用的后果是未知

使用完的对象没有及时清理,会造成一定的安全问题

C++ 中使用 构造函数析构函数 解决上述问题。这两个函数会被编译器自动调用,完成初始化和清理工作。

如果我们不提供构造和析构,编译器有默认方法。默认方法是空实现。

  • 构造函数:在创建对象时被系统自动调用。为对象的成员属性赋值
  • 析构函数:于对象销毁前被系统自动调用。执行一些清理工作

#7.2.1.1 构造函数

在创建对象时被系统自动调用。为对象的成员属性赋值

语法:类名(参数) {构造函数体}

初始化列表语法:类名(参数): 属性1(值1), 属性2(值2)... {构造函数体}

class Test{
public:
    int n;
    int j;
    
    Test(int i = 0) {							// 构造函数
        this->n = i;
    }
    
    Test(int a, int b): n(a - 1), j(b) {}		// 初始化列表语法
};
  1. 构造函数没有返回值,也不写 void
  2. 构造函数名称和类名相同
  3. 构造函数有参数,可以重载
  4. 程序调用对象时会自动调用构造,且仅会调用一次,无需手动调用
  5. 特别地,只有唯一参数的构造函数也被称为 类型转换构造函数

#7.2.1.2 析构函数

于对象销毁前被系统自动调用。执行一些清理工作

语法:~类名() {析构函数体}

class Test{
public:
    int n = -1;
    ~Test() {					//析构函数
        cout << "n = " << n << endl;
    }
};
  1. 析构函数没有返回值,也不写 void
  2. 析构函数名称和类名相同,名称前加上 ~
  3. 析构函数不能有参数
  4. 程序销毁对象时会自动调用析构,且仅会调用一次,无需手动调用

7.2.2 构造函数的分类及调用

分类方式:

  • 按参数分类:有参构造、无参构造(默认构造)
  • 按类型分类:普通构造、拷贝构造

调用方式:

  • 括号法
  • 显式法
  • 隐式转换法

示例:

class Example{
public:
    int n;
    
    Example() {						//无参构造
        this->n = 0;
    }
    
    Example(int a) {				//有参构造
        this->n = a;
    }
    
    Example(const Example& e) {		//拷贝构造
        this->n = e.n;
    }
    
    Example(int a, int b) {
        this->n = a + b;
    }
};

int main() {
    Example e1;						//[1]括号法
    Example e2(10);
    /* Example wrong(); */
    Example e3 = Example(15);		//[2]显式法
    Example e4 = Example(e1);
    /* Example(e1); */
    Example e5 = 20;				//[3]隐式转换法
    								//等于 Example e4 = Example(20);
    Example e6 = e2;
    Example e7 = { 25, 1 };			//两个参数这样写
}

注意事项:

  1. 使用括号法调用无参构造时,不要加括号

    加了括号的场合,编译器会将其视为一个函数的声明,而不是构造对象

    Example wrong(); 被认为是:

    • 方法名 wrong
    • 返回值 Example
    • 参数列表 ()
  2. 不要用拷贝构造函数初始化匿名对象

    若如此做,编译器会将其视为该对象的声明,而不是拷贝构造

    Example(e1); 被认为是:

    • Example e1;

    这与之前的声明重复,因此可能报错为重定义

7.2.3 拷贝构造函数的调用时机

  1. 使用一个已创建的对象来初始化新对象

    Example e1;					//默认构造
    Example e2(e1);				//拷贝构造
  2. 值传递的方式给函数参数传值

    传入参数实际上是拷贝构造的匿名对象

    void met(Example e) {		//值传递
    }
  3. 值方式返回局部对象

    返回值实际上是拷贝构造的匿名对象

    Example met2() {			//返回局部对象
        return Example();		//这里其实是 显式法
    }

7.2.4 构造函数的调用规则

C++ 编译器会为一个类添加 3 个默认函数

  • 默认构造函数(无参,函数体为空)

    用户定义了任何构造函数的场合,编译器不再提供默认构造函数

    Example() {}				//这就是默认构造函数
  • 默认析构函数(无参,函数体为空)

    ~Example() {}				//这就是默认析构函数
  • 默认拷贝构造函数,进行值拷贝

    用户定义了拷贝构造函数的场合,编译器不再提供默认拷贝构造函数

7.2.5 深拷贝与浅拷贝

  • 浅拷贝:简单的赋值拷贝工作。

    浅拷贝可能导致堆区内存的重复释放

    浅拷贝示例:

    class E{
    public:
        int* n;
        E(int a) {
            this->n = new int(a);		//堆区开辟空间
        }
        
        E(E& e) {						
            this->n = e.n;				//浅拷贝。默认拷贝函数即浅拷贝
        }
        
        ~E() {							//析构函数
            if (n != NULL) {
                delete n;				//释放空间
                n = NULL;
            }
        }
    };
    
    int main() {
        E e1(10);
        E e2(e1);						//浅拷贝。会导致问题
    }

    由于是浅拷贝,此时,e1.n 储存的地址和 e2.n 相同

    因此,第二次执行析构函数时,已经释放的空间不能被二次释放,就会出错。

  • 深拷贝:在堆区重新申请空间,进行拷贝工作

    深拷贝修改后的代码:

    class E{
    public:
        int* n;
        E(int a) {
            this->n = new int(a);		//堆区开辟空间
        }
        
        E(E& e) {				
            this->n = new int(*e.n);	//深拷贝
        }
        
        ~E() {							//析构函数
            if (n != NULL) {
                delete n;				//释放空间
                n = NULL;
            }
        }
    };
    
    int main() {
        E e1(10);
        E e2(e1);						//此时就不会有问题了
    }

7.2.6 类对象作为类成员

C++ 类中的成员可以是另一个类的对象。我们称之为成员对象。包含成员对象的类叫封闭类

生成封闭类对象的语句应说明那些成员对象是如何初始化的。否则,编译器会使用默认构造函数(或能省略参数的构造函数)进行初始化。

class A{
public:
    int a;
    A(int n = 10) : a(n) {}
};

class B{
public:
    A Ca;			//A 类对象为 B 类成员
    int b;
    B(int an, int bn) : Ca(an), b(bn) {}
    //该构造器中,Ca(an) 即 A Ca = an; 隐式转换法
};

int main(){
    B cb = B(15, 6);
}

这个场合的构造函数、析构函数调用顺序是:

  1. 类成员构造函数被调用
  2. 类构造函数被调用
  3. 类析构函数被调用
  4. 类成员析构函数被调用

7.2.7 静态成员

#静态变量:

全局变量是指在大括号外声明的变量,其作用域范围是全局可见。

使用 static 修饰的全局变量称为静态全局变量。其作用域限于定义该变量的源文件中

使用 static 修饰的局部变量是静态局部变量。其具有局部作用域,却有全局生存期。就是说,静态局部变量在整个运行期间存在,其占用空间直到程序结束才释放,但只能在其定义块内访问。

静态局部变量只进行一次初始化。如果显式给出初始值,则其初始化发生在该静态变量所在的块第一次执行时。

#类的静态成员:

在类体内定义类成员时,在前方添加 static 关键字,即使其成为静态成员。

类的静态成员被类的所有对象共享。

在类体外为静态成员赋初始值时,那个赋值时前面不能加 static,以免和静态全局变量混淆

静态成员分为:

  • 静态成员变量
    • 所有对象共享一份数据
    • 在编译阶段分配内存(在全局区)
    • 类内声明,类外初始化。
  • 静态成员函数
    • 所有对象共享一个函数
    • 静态成员函数只能访问静态成员变量

访问静态成员:

  • 通过类对象:类对象.静态成员
  • 通过类名:类名::静态成员

7.3 C++ 对象模型和 this 指针

C++ 中,类内的成员变量和成员函数分开存储。只有非静态成员变量才属于类的对象

  • C++ 编译器会为每个空对象分配 1 字节的空间,以区分空对象占用内存的位置
  • 非静态成员变量,属于类的对象
  • 静态成员变量,不属于类的对象
  • 非静态成员函数,不属于类的对象
  • 静态成员函数,不属于类的对象

7.3.1 this 指针

this 指针是隐含在每一个非静态成员函数内的一种指针。指向被调用的成员函数所属的对象。

this 指针不需要定义,可以直接使用。类的每个成员函数中都包含该指针。

this 的本质是 指针常量。其指向不能修改***(见 [4.1.3.2 指针常量])***

this 指针的用途:

  • 形参和成员变量同名时,使用 this 指针进行区分
  • 在类的非静态成员函数中返回对象本身
class E {
public:
    void setN(int n) {
        this->n = n;
    }
    E& doit() {
        this->n += 10;
        cout << n << endl;
        return *this;
    }
private:
    int n;
};

int main() {
    E e;
    e.setN(10);
    e.doit().doit().doit();
}

7.3.2 空指针访问成员函数

C++ 中空指针也能调用成员函数,但要注意是否用到 this

如果用到 this,需要判断以保证代码健壮性

class E{
public:
    int n;
    void met1() {
        cout << "Method1" << endl;
    }
    void met2() {
        cout << "Method2" << n << endl;
    }
};

int main() {
    E* e = NULL;
    e.met1();						//这个正常
    /* e.met2() */					//这个报错
}

7.3.3 const 修饰成员函数

可以用 const 定义成员变量或成员函数,甚至是类对象。

使用 const 修饰的成员变量称为常量成员变量

使用 const 修饰的函数称为常量函数(常函数)

定义类对象时添加 const 关键字,该对象称为常量对象(常对象)

class A {
public:
	int value();				// 成员函数
	int value() const;			// 常量函数。因为有 const,语法上构成重载
};

int A::value() {
	return 1;
}

int A::value() const {
	return 100;
}
  • 常函数:

    • 成员函数后加 const 则成为常函数

      void say() const{}		//常函数
    • 常函数不能修改成员属性

    • 成员属性声明时加关键字 mutable 后,可以被常函数修改

    • 在成员函数后加 const,本质上是修饰了 this 指针

      const E* const this;

      这样一来,this 指针指向的值也不能修改了

    • 常对象只能访问常函数,普通对象既能访问普通函数也能访问常函数

      对于重载的常函数,根据调用对象是常对象或普通对象,系统会进行自动匹配

    • 如果一个成员函数中没有调用非常函数,也没有修改成员变量,那么建议将其写成常函数

  • 常对象:

    • 声明对象前加 const 称为常对象

      const E e;				//常对象
    • 常对象的所有属性值都不能修改,除非其属性有 mutable 关键字

    • 常对象只能调用常函数。因为普通成员函数能修改属性

7.4 友元

让一个函数或类访问另一个函数中的私有成员。友元关系是单向的

友元关键字:friend

友元不受类中访问关键字限制。将其放在类的公有、私有、保护部分,结果是相同的

友元的三种实现:

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元
class InazumaCastle;

class LaSignora {
public:
    string name = "女士";
    void announce(InazumaCastle* q);
};

class RaidenEi {
public:
    string name = "雷电影";
    void visit(InazumaCastle* q);
};

class InazumaCastle {
    friend void visit(InazumaCastle* q);				//将全局函数做友元
    friend class RaidenEi;								//类做友元
    friend void LaSignora::announce(InazumaCastle* q);	//成员函数做友元
public:
    InazumaCastle(){}
private:
    string npc = "雷电将军";
};


void visit(InazumaCastle* q) {							//全局函数
    cout << q->npc << endl;
}

void RaidenEi::visit(InazumaCastle* q) {				//成员函数
    cout << "雷军亦未寝" << endl;
}

void LaSignora::announce(InazumaCastle* q) {			//成员函数
    cout << q->npc << "听仔细了!" << endl;
    cout << "你要战,我便战,我有愚人众兄弟千千万!单挑群挑你们也不是对手,别把我们惹急了!" << endl;
    cout << "明天我就上你们几个神家里挨个收神之心,不给别怪我不客气!" << endl;
}

7.5 运算符重载

对已有运算符重新定义,以适应不同数据类型

对于类运算的运算符通常都要重载。系统提供了两个运算符的默认重载版本:

= 系统默认进行对象成员变量的值拷贝。& 系统默认返回任何类对象的地址

运算符重载的格式是:

返回值 operator运算符 (形参列表) {
    函数体
}

可以重载的运算符有:

双目算术运算符 +、-、*、/、%
关系运算符 ==、!=、<、>、<=、>=
逻辑运算符 ||、&&、!
单目运算符 +(正)、-(负)、*(指针)、&(取址)
自增自减运算符 ++、--
位运算符 <<、>>、~、|、&、^
赋值运算符 =、+=、-=、*=、/=、&=、|=、^=、<<=、>>=、~=
空间申请与释放 new、delete、new[]、delete[]
其他运算符 ()、->、[]、,

不能重载的运算符有:

成员访问运算符 .
成员指针访问运算符 .*、->*
域运算符 ::
长度运算符 sizeof
条件运算符 ?:
预处理符号 #

#注意事项:

  • 重载运算符的含义应符合原有用法习惯。
    • 避免给自己的猫取名字叫 “小狗” 或者 “笔记本电脑”
  • 运算符重载不能改变运算符原有的语义,包括运算符的优先级和结合性
    • 不要让自己的猫睡在床上,而自己睡在猫窝里
  • 运算符重载不能改变运算符的操作数个数及语法结构
    • 给猫投食时,食物从嘴巴进入,而不能从菊花进入
  • 不能创建新的运算符。即,重载运算符不能超出 C++ 语言允许重载的运算符范围
    • 可以去养一只猫,但不要和猫生小孩
  • 重载运算符 ()[]->= 时,只能将它们重载为成员函数,不能重载为全局函数
    • 把猫养在家里,不要养在大街上
  • 运算符重载不能改变该运算符用于基本数据类型对象的含义(即其原本含义)。
    • 不要指望猫能变成猫娘

7.5.1 加号运算符重载

+-*/ 同理

  • 通过成员函数重载:

    class E{
    public:
        int n;
        E operator+ (E& e) {			//成员函数
            E ne;
            ne.n = e.n + this->n;
            return ne;
        }
    };
  • 通过全局函数重载:

    class E{
    public:
        int n;
    };
    
    
    E operator+ (E& e1, E& e2) {		//全局函数,参数是两个
        E ne;
        ne.n = e1.n + e2.n;
        return ne;
    }

注意事项:

  • 内置数据类型的运算符不能改变

  • 不要滥用运算符重载

    + 结果实现的是减法,那不是找麻烦吗

7.5.2 左移运算符重载

重载左移运算符,配合友元,可以输出自定义数据类型

  • 通常不会利用成员函数重载 << 运算符,因为无法使 cout 在左侧

  • 通过全局函数重载:

    class E {
    friend ostream& operator<< (ostream& cout, E& e);
    private:
        int n = 100;
    };
    
    ostream& operator<< (ostream& cout, E& e) {		//ostream 对象是唯一的
        cout << e.n;
        return cout;
    }

7.5.3 递增运算符重载

  • 用成员函数重载:

    class E {
    public:
        int operator++(int) {			//后置 ++。用占位符 int 作为区分
            int ret = this->n;
            this->n += 1;
            return ret;
        }
             
        E& operator++() {				//前置 ++
            this->n++;
            return *this;
        }
    private:
        int n = 100;
    };
  • 通过全局函数重载:

    class E {
        friend E& operator-- (E& e);
        friend int operator-- (E& e, int);
    private:
        int n = 100;
    };
    
    E& operator-- (E& e) {				//前置 --
        e.n -= 1;
        return e;
    }
    
    int operator-- (E& e, int) {		//后置 --。用占位符 int 作为区分
        int ret = e.n;
        e.n -= 1;
        return ret;
    }

7.5.4 赋值运算符重载

C++ 编译器至少给一个类添加 4 个函数

  • 默认构造函数
  • 默认析构函数
  • 默认拷贝构造函数,对属性进行值拷贝
  • 赋值运算符 =,对属性进行值拷贝
class E {
public:
    E& operator=(E& e) {			//重载喵
        if (this->n != NULL) {
            delete this->n;
            this->n = NULL;
        }
        this->n = new int(*e.n);	//深拷贝喵
        return *this;
    }
    
    ~E() {							//析构喵
        if (n != NULL) {
            delete n;
            n = NULL;
        }
    }
    
private:
    int* n = new int(100);			//在堆里喵
};

7.5.5 关系运算符重载

略。

就略了,你看着办吧。

关系运算符就是 ==>>= 之类。返回值显然是布尔类型。

7.5.6 仿函数

  • 函数调用运算符 ( ) 居然也可以重载
  • 重载后的使用方式像函数的调用,所以被称为 仿函数
  • 仿函数没有固定写法,非常灵活
class P{
public:
    void operator()(string str) {
        cout << str << endl;
    }
    int operator()(int n1, int n2 = 0) {
        return n1 + n2;
    }
};

int main() {
    P p;
    p("函数调用运算符重载!");		//仿函数
    cout << P()(15, 27) << endl;	//P() 匿名对象调用仿函数,即匿名函数对象
}

7.5.7 类型转换操作符重载

C++ 中,类型的名字(包括类的名字)本身也是一种操作符,即强制类型转换操作符。

强制类型转换操作符是单目运算符,仅能被重载为成员函数,不能重载为全局函数。

重载后,(类型名)对象 就等价于 对象.operator类型名()

class A {
public:
	operator double() {					// 重载强制类型转换操作符 (double)
		return 100.001;
	}
};

int main() {
	A a;
	cout << (double)a << endl;			// 输出 100.001
	cout << a - 100 << endl;			// 输出 0.001
}

重载类型转换运算符时,不能指定返回值类型。因为返回值类型是确定的。

有了类型转换运算符的重载,在本该出现该转换类型的地方,如果出现了一个重载类型,则语法上视为正确。

7.6 继承

面向对象三大特性之一。

利用继承技术,减少重复代码

语法:class S: public F{...}

  • S 类即子类(派生类)

    子类中包含父类继承的成员及子类自己的成员

    子类的大小等于:继承的父类大小 + 子类自身大小

  • F 类即父类(基类)

7.6.1 继承方式

继承方式共 3 种:

  • 公共继承 class B: public A{}

    成员继承后的访问权限不变

  • 保护继承 class B: protected A{}

    private(访问不到)外,其余成员继承后的访问权限变为 protected

  • 私有继承 class B: private A{}

    private(访问不到)外,其余成员继承后的访问权限变为 private

    不进行说明时,默认进行私有继承

    ——访问修饰符见 [7.1 封装]

7.6.2 继承中的对象模型

  • 父类中所有非静态成员属性都会被子类继承(包括 private)

  • 父类中 private 成员属性是被编译器隐藏了,故访问不到

  • 使用 VS 开发人员命令提示符 跳转到项目目录下,输入指令:

    cl /d1 reportSingleClassLayout类名 所属文件名

    以查看类的构造。

  • 父类持有的友元,在继承后仍只能访问父类成员对象。

    其他类持有父类作为友元,继承后子类也是那些类的友元

  • 子类构造器中使用父类构造器:

    子类名(参数): 父类1(参数), 父类2(参数) {
        ...
    }

    同一父类的构造器只能调用一次,不声明时默认调用无参构造器

    这个调用的顺序和类声明时的继承声明顺序 相关,与该初始化列表中的顺序 无关

7.6.3 继承中构造和析构顺序

子类继承父类后,创建子类对象时,也会调用父类构造函数

  1. 父类构造函数
  2. 子类构造函数
  3. 子类析构函数
  4. 父类析构函数

7.6.4 继承同名成员的处理方式

  • 访问子类同名成员:直接访问

  • 访问父类同名成员:加作用域

  • 子类同名函数会隐藏所有父类同名函数及其重载函数。此时必须加作用域

    class A {
    public:
        int n = 10;
        static void doiy() {
            cout << 1 << endl;
        }
    };
    
    class B : public A {
    public:
        int n = 20;
        static void doiy() {
            cout << 4 << endl;
        }
    };
    
    int main() {
        B b;
        B::doiy();						//直接访问子类成员
        b.doiy();						//直接访问子类成员
        B::A::doiy();					//加作用域以访问父类成员
        cout << b.n << endl;			//直接访问子类成员
        cout << b.A::n << endl;			//加作用域以访问父类成员
    }

7.6.5 多继承语法

语法:class A: public Ba, public Bb, private Bc{...}

多继承可能引发父类中有同名成员出现。访问时必须加上作用域

C++ 中不建议使用多继承。

7.6.6 菱形继承

菱形继承(钻石继承):

  • 两个子类继承同一父类
  • 某个类同时继承这两个子类

菱形继承的场合,两个父类有相同数据,需要用作用域加以区分

两个父类数据重复,只需要一份。可以利用虚继承,解决菱形继承问题。

  • 虚继承:class A: virtual public B, public C{...}

    其中加入 virtual 的是虚继承,类 B、C 的共有父类被称为 虚基类

7.7 多态

面向对象三大特性之一

“一种接口,多种实现”。实现了多态机制的程序,可以使用同一名字实现不同功能

多态分为:静态多态、动态多态

#静态多态

静态多态通过函数名、参数个数及类型,在编译阶段即建立函数代码与函数调用的对应关系。

静态多态也称 静态连编 或 静态绑定

静态多态的函数地址早绑定:编译阶段确定地址

静态多态使程序可读性好,但在控制程序运行和对象行为多样性方面存在局限性

class Father {
public:
    void say() {						//静态多态
        cout << "哒哒哒" << endl;;
    }
};

class Son : public Father {
public:
    void say() {
        cout << "芭芭拉冲呀" << endl;
    }
};

void check(Father& f) {
    f.say();
}

int main() {
    Son s;
    check(s);							//即使传入子类,也是调用父类方法
}

#动态多态

由于多态的存在,编译器不能在编译阶段得知指针指向父类还是继承子类,也就不能确定调用的函数版本。函数调用与代码入口地址的绑定发生在运行时。

动态多态也称 动态连编 或 动态绑定。

动态多态的函数地址晚绑定:运行阶段确定函数地址、

实现方法:加入 virtual 关键字

class Father {
public:
    virtual void say() {				//虚函数
        cout << "哒哒哒" << endl;;
    }
};

/* 其余同上,略 */

int main() {
    Son s;
    check(s);							//传入子类则调用子类方法
}

在满足赋值兼容的情况下,动态多态需满足以下条件:

  1. 必须声明虚函数
  2. 通过子类类型的引用或指针调用虚函数

7.7.1 虚函数

编译器看到哪个指针,就认为要通过该指针访问哪个类的成员,并不会分辨指针是否指向子类对象。仅凭借继承机制,无法实现动态绑定。

在函数声明时加上 virtual 关键字,即成为虚函数。

class A {
public:
	virtual void act();
};

class B: public A {
public:
	virtual void act();
};

void A::act() {
	cout << 'A' << endl;
}

void B::act() {
	cout << 'B' << endl;
}

只有类的成员函数声明时才能声明为虚函数。成员函数的类外实现 和 静态成员函数 不能包含 virtual 关键字。

#虚函数原理

虚函数在类内部保存一个虚函数指针。该指针指向该类的虚函数表内,该虚函数的地址

子类会继承该指针和虚函数表

子类重写该虚函数的场合,指针指向的虚函数被重写后的函数覆盖

#注意事项

  • 如果想要调用虚函数对象,需要在调用前加上基类名及作用域限定符

    A* n = new B();
    n->act();						// B
    n->A::act();					// A
  • 虽然不会引起错误,但一般不将虚函数声明为内联函数。

    这是因为:内联函数在编译阶段即进行静态处理,而虚函数是在运行时动态绑定。

  • 只有类的成员函数声明时才能声明为虚函数。静态成员函数、友元函数不能定义为虚函数。

  • 虚函数类外实现时,只有类内声明有 virtual 关键字,类外实现不加该关键字

  • 构造函数不能定义为虚函数。最好也不要将 operator= 定义为虚函数,以免混淆。

    最好将父类的析构函数声明为虚函数。

  • 不要在构造函数、析构函数中调用虚函数。因为在构造函数、析构函数中,对象是不完整的,可能出现未定义的行为。

7.7.2 纯虚函数和抽象类

多态中,父类虚函数的实现往往是无意义的,主要调用子类重写的内容。因此,可以将虚函数改为纯虚函数。

语法:cirtual 返回值类型 函数名 (参数列表) = 0;

class Vir{
public:
    virtual void met() = 0;
};

有纯虚函数的类被称为抽象类。

抽象类特点:

  1. 抽象类不能实例化对象
  2. 子类必须重写抽象类的纯虚函数,否则也属于抽象类

7.7.3 虚析构和纯虚析构

多态使用时,父类指针释放时无法调用子类析构代码

拥有纯虚析构函数的类也属于抽象类

语法:

  • 虚析构:virtual ~类名() {析构代码}

  • 纯虚析构:virtual ~类名() = 0;

    和纯虚函数不同。即使是纯虚析构,也要求(在类外)实现代码。

    一言以蔽之:take off trouser to fart

只要父类的析构函数是虚析构函数,则子类的虚构函数 无论是否用 virtual 关键字声明,都自动成为虚析构函数

一般来说,只要一个类定义了虚函数,就最好将其析构函数也定义为虚析构函数。

构造函数不能是虚函数。


<C++>7 类和对象
https://i-melody.github.io/2022/04/17/C++/入门阶段/7 类和对象/
作者
Melody
发布于
2022年4月17日
许可协议