<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) {} // 初始化列表语法
};
- 构造函数没有返回值,也不写 void
- 构造函数名称和类名相同
- 构造函数有参数,可以重载
- 程序调用对象时会自动调用构造,且仅会调用一次,无需手动调用
- 特别地,只有唯一参数的构造函数也被称为 类型转换构造函数
#7.2.1.2 析构函数
于对象销毁前被系统自动调用。执行一些清理工作
语法:~类名() {析构函数体}
class Test{
public:
int n = -1;
~Test() { //析构函数
cout << "n = " << n << endl;
}
};
- 析构函数没有返回值,也不写 void
- 析构函数名称和类名相同,名称前加上
~
- 析构函数不能有参数
- 程序销毁对象时会自动调用析构,且仅会调用一次,无需手动调用
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 }; //两个参数这样写
}
注意事项:
-
使用括号法调用无参构造时,不要加括号
加了括号的场合,编译器会将其视为一个函数的声明,而不是构造对象
Example wrong();
被认为是:- 方法名
wrong
- 返回值
Example
- 参数列表
()
- 方法名
-
不要用拷贝构造函数初始化匿名对象
若如此做,编译器会将其视为该对象的声明,而不是拷贝构造
Example(e1);
被认为是:Example e1;
这与之前的声明重复,因此可能报错为重定义
7.2.3 拷贝构造函数的调用时机
-
使用一个已创建的对象来初始化新对象
Example e1; //默认构造 Example e2(e1); //拷贝构造
-
值传递的方式给函数参数传值
传入参数实际上是拷贝构造的匿名对象
void met(Example e) { //值传递 }
-
值方式返回局部对象
返回值实际上是拷贝构造的匿名对象
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);
}
这个场合的构造函数、析构函数调用顺序是:
- 类成员构造函数被调用
- 类构造函数被调用
- 类析构函数被调用
- 类成员析构函数被调用
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 继承中构造和析构顺序
子类继承父类后,创建子类对象时,也会调用父类构造函数
- 父类构造函数
- 子类构造函数
- 子类析构函数
- 父类析构函数
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); //传入子类则调用子类方法
}
在满足赋值兼容的情况下,动态多态需满足以下条件:
- 必须声明虚函数
- 通过子类类型的引用或指针调用虚函数
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;
};
有纯虚函数的类被称为抽象类。
抽象类特点:
- 抽象类不能实例化对象
- 子类必须重写抽象类的纯虚函数,否则也属于抽象类
7.7.3 虚析构和纯虚析构
多态使用时,父类指针释放时无法调用子类析构代码
拥有纯虚析构函数的类也属于抽象类
语法:
-
虚析构:
virtual ~类名() {析构代码}
-
纯虚析构:
virtual ~类名() = 0;
和纯虚函数不同。即使是纯虚析构,也要求(在类外)实现代码。
一言以蔽之:take off trouser to fart
只要父类的析构函数是虚析构函数,则子类的虚构函数 无论是否用 virtual 关键字声明,都自动成为虚析构函数
一般来说,只要一个类定义了虚函数,就最好将其析构函数也定义为虚析构函数。
构造函数不能是虚函数。