1) C++ 中类的三大特性是什么?请简要解释。
C++中类的三大特性是封装、继承和多态。
一、封装
封装是将数据和操作数据的方法封装在类中,对外部隐藏类的内部实现细节。通过将数据成员设为私有(private),并提供公有的(public)成员函数来访问和修改这些数据,从而实现对数据的保护和控制。这样可以防止外部代码直接访问和修改类的内部数据,提高了代码的安全性和可维护性。
二、继承
继承允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法。子类可以继承父类的公有和保护成员,同时可以添加自己的新成员或重写父类的成员函数。
继承的好处:可以减少重复的代码
class A : public B;
A 类称为子类 或 派生类
B 类称为父类 或 基类
继承方式:
凡是基类中私有的,派生类都不可访问。
基类中除了私有的成员,其他成员在派生类中的访问属性总是 以(继承方式,基类的访问属性)中 安全性高的方式呈现。(安全性级别:私有>保护>公有)
继承权限会缩小父类中的成员在子类中的权限,不会扩大成员的权限
子类继承父类其中私有成员只是被隐藏了,但是还是会继承下去
继承同名成员处理方式:
访问子类同名成员 直接访问即可
访问父类同名成员 需要加作用域
- 当父类和子类中有同名函数时,子类中的函数会将父类中的同名函数隐藏(函数隐藏);如果此时想通过子类对象访问父类对象,需要作用域
- 父类对象不可以访问子类对象中的成员变量,因为创建父类对象不会创建子类对象
三、多态
函数重写(覆盖):
定义:子类重新定义父类中有相同名称,返回值和参数的虚函数,主要在继承关系中出现。
父类中的fun函数是虚函数,子类中有返回值,名字 参数 相同的fun函数,那么子类中的fun函数(无论加不加virtual)都是虚函数
基本条件:
- 重写的函数和被重写的函数必须都虚(virtual)函数,并分别位于基类和派生类中
- 重写的函数和被重写的函数,返回值,函数名和函数参数必须完全一致;
函数隐藏:子类中只要和父类函数名字相同不是重写,一定是函数隐藏。
多态的基本概念:
- 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态: 派生类和虚函数实现运行时多态(父类的指针或引用指向子类对象,并且调用子类的重写函数)
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
父类指针或引用指向子类对象时,函数隐藏不会调用子类中的函数而是调用父类中的函数
调用的函数是隐藏函数时,调用函数的指针或对象是什么类型调用哪里的函数
总结:
多态满足条件
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象
重写:函数返回值类型 函数名 参数列表 完全一致称为重写
多态的实现:
为了实现 C++ 的多态,C++ 使用了一种动态绑定的技术。这个技术的核心是虚函数表。下面介绍虚函数表是如何实现动态绑定的。
类的虚函数表:
- 每个包含了虚函数的类都包含一个虚表(存放虚函数指针的数组)。
- 当一个类(B)继承另一个类(A)时,类 B 会继承类 A 的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
- 以下的代码。类 A 包含虚函数vfunc1,vfunc2,由于类 A 包含虚函数,故类 A 拥有一个虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A{//此时类B也拥有自己的虚表
};
纯虚函数和抽象类:
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类拥有纯虚函数,这个类也称为抽象类(一般作为基类)
抽象类特点:
- 无法实例化对象(抽象类不能创建对象)
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
- 纯虚函数在虚表中存放的是 0
虚函数在虚表中存放的是函数地址
虚析构和纯虚析构:
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码(如果不加virtual将父类的析构改为虚析构或者纯虚析构 子类的析构会将父类的析构隐藏 从而造成内存泄露)
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
父类指针指向子类对象,只能调用父类成员函数
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法: virtual ~类名(){}
纯虚析构语法: virtual ~类名() = 0;
2)什么是构造函数和析构函数?它们的作用是什么?
构造函数:
作用:在创建对象的时候,给对象的成员变量赋值,创建对象的时候自动调用,无需手动调用
当没有实现构造函数的时候,编译器会自动提供默认的无参构造
创建对象一定会调用构造函数,当实现了任意一个构造函数时,编译器则不提供默认构造函数
语法:
- 没有返回值也不写void
- 名字与类名相同
- 有参数,可以重载
- 有参构造函数是必须的,如果你定义了至少一个有参的构造函数,C++编译器不会为你创建一个默认的无参构造函数
- 无参(默认)构造函数是可选的,如果你没有定义任何构造函数,C++编译器将为你创建一个无参构造函数。它不执行任何操作,只是创建一个空的对象
构造函数三种调用方式:
- 括号法
- 显式法
- 隐式法 (可以通过关键字explicit避免隐式法调用构造函数)
class pointer {
int* p;
public:
pointer(int n) {
p = new int[n];
cout << "有参" << endl;
}
pointer() {
cout << "无参" << endl;
}
};
int main() {
pointer p1(4);//括号法
pointer p2 = 4;//隐式法
pointer p3 = pointer(4);//显式法
}
析构函数(一个类只能定义一个析构):
- 在对象被释放的时候调用
- 没有实现析构函数的时候编译器会自动提供析构函数(什么也不做)
- 没有返回值 不写void
- 析构函数不可以有参数,因此不可以发生重载
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
语法:~类名(){}
class pointer {
int* p;
public:
//创建对象时用构造函数
pointer() {
p = nullptr;
}
pointer(int n) {
p = new int[n];
}
//释放对象时用析构函数
~pointer() {
//释放对象时用析构函数,释放成员变量指向的堆区内存
if (p)delete[]p;
}
};
int main() {
//在作用域结束时,被释放,被释放的时候调用析构函数
pointer p1(3);
pointer* p2 = new pointer(3);
delete p2;//delete p2会先调用p2的析构函数然后在释放p2
}
拷贝构造函数
拷贝构造(参数类型为引用类型):通过当前的对象复制出来一份一模一样的对象 拷贝构造的调用时机:通过对象初始化对象的时候会调用拷贝构造
- 通过已经存在的对象初始化新的对象
- 函数参数以值的形式传递的时候 (实参初始化形参)
- 函数的返回值以值的形式传递的时候
参数或者返回值为引用时,可以避免调用拷贝构造
(如果拷贝构造的参数不是引用,则会陷入无限递归,如果不加const,可能出现形参修改实参的情况)
- 浅拷贝:就是简单的赋值操作(浅拷贝问题:如果有指针指向堆区内存时,不同对象的指针成员指向同一块堆区内存,在对象释放的时候,该堆区内存会被释放两次,当一个对象修改堆区内存时,另一个对象也随着变化。)
- 深拷贝:申请相同大小的堆区内存,保证两个对象的堆区的值相同(如果属性有堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来问题)