c++中的封装、继承与多态

一、封装

(1)类的定义与简单调用

(2)对象赋值与取值

(3)对不同属性和行为进行权限划分

[(4) c++中的类和结构体的区别](#(4) c++中的类和结构体的区别)

二、继承

(1)派生类中的成员组成

(2)继承的类别

(3)继承中的对象模型

(4)继承中构造和析构顺序

(5)继承同名成员处理方式

(6)继承同名静态成员的处理方式

(7)多继承

三、多态

(1)函数重写

(2)函数隐藏

(3)多态的基本条件:

(4)多态的类别

(5)多态的使用

(6)纯虚函数和抽象类

(7)虚析构和纯虚析构


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

一、封装

封装的意义:将属性(变量)和行为(函数)作为一个整体,表现生活中的事物;将属性和行为加以权限控制。

语法:class 类名 { 访问权限 : 属性/行为 }

(1)类的定义与简单调用

cpp 复制代码
#include<iostream>
using namespace std;

class Circle{
	//权限 
	public:
		//属性 
		int r;
		//行为:求解半径为r的圆的周长 
		double calculateC() {
			return 2 * 3.14 * r;
		}
}; 
int main(){
	//创建对象
	Circle c;
	//调用 
	c.r = 10;
	cout << c.calculateC() << endl;
	return 0;	
} 

上面的代码中,Circle类中包含public访问权限的属性和行为(函数),可以通过创建对象进行调用。

(2)对象赋值与取值

一般通过下面的格式进行对对象进行赋值和取值。

cpp 复制代码
#include<iostream>
using namespace std;

class Student{
	private:
		int s_age;
		string s_name;
		
	public:
		void setName(string name){
			s_name = name;
		} 
		void setAge(int age){
			s_age = age;
		}
        int getAge(){
            return s_age;
        }
		
		void show_student(){
			cout << "name: " << s_name << " age: " << s_age << endl; 
		}
}; 
int main(){
	//创建对象
	Student s;
	//调用 
	s.setName("小刚");
	s.setAge(18); 
	s.show_student();//输出 name: 小刚 age: 18
}

其中,私有属性只能通过类内部的成员函数(通常是get/set方法)访问和修改,避免外部代码直接操作属性,从根本上防止数据被非法、错误地修改;这样也能检测数据的有效性。

(3)对不同属性和行为进行权限划分

访问权限有三种:分别为

|-----------|------|----------|----------|
| 权限 || 类内访问 | 类外访问 |
| public | 公共权限 | √ | √ |
| protected | 保护权限 | √ | × |
| private | 私有权限 | × | × |

(4) c++中的类和结构体的区别

两者其实非常相像,都可以在其中定义属性和行为,但是类和结构体是具有本质区别的:

  • 类(class)和结构体(struct)的基本格式有所不同(这里不做赘述)
  • 类和结构体的默认访问权限不同: 类中默认权限为private、结构体的默认权限为public
  • 类和结构体的默认继承权限不同。(后续做讲述)
cpp 复制代码
#include<iostream>
using namespace std;
class C1{
	int a = 1;// 访问失败 
};
struct C2{
	int a = 2;
};
int main(){
	C1 c1;
	cout << c1.a << endl;
	C2 c2;
	cout << c2.a << endl;
	return 0; 
} 

二、继承

**继承的意义:**子类可以从父类根据相应的权限继承相应的成员函数或成员变量,从而减少冗余代码,提高代码利用率。

**语法:**class 子类名:子类从父类继承的权限 父类名{}

以下面的示例代码为例:

cpp 复制代码
#include<iostream>
using namespace std;
class A {

};
class B :public A {

};
int main() {

}

这里就是类B继承类A,类A称为父类/基类,类B称为子类/派生类。

(1)派生类中的成员组成

派生类中的成员组成 = 从父类继承下来的 + 子类新增的成员。

父类继承→共性,子类新增→个性

(2)继承的类别

根据子类从父类中继承的权限,可以将继承分为公共继承、保护继承和私有继承。

这是由关键字public、protected和private决定的,而子类继承父类的权限事实上只能缩小权限,不能放大权限。下面的例子能够说明:

cpp 复制代码
#include<iostream>
using namespace std;
class A {
public:
	int a;
protected:
	int b;
private:
	int c;
};
class B :public A {
	void f() {
		this->a;
		this->b;
		this->c;
    //不可访问,是因为父类中的private权限到子类中尽管提供的继承权限是public,但是也是无法访问的。
	//不能放大权限,只能缩小权限或权限不变
	}
};
int main() {

}

这里要注意:子类是能够继承到父类中受保护的权限的

(3)继承中的对象模型

从父类中继承的成员都会包含在子类的对象中,包含父类私有。这里"能否访问"和"是否继承下来"是不同的问题。

cpp 复制代码
#include<iostream>
using namespace std;
class A {
public:
	int a;
protected:
	int b;
private:
	int c;
};
class B :public A {
public:
	int d;
};
int main() {
	cout << sizeof(B);//输出 16
	return 0;
}

(4)继承中构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数,构造函数和析构的调用顺序可以通过下面的程序得知(和对象成员与本类的调用顺序相似):

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
    A()
    {
        cout << "父类构造函数!" << endl;
    }
    ~A()
    {
        cout << "父类析构函数!" << endl;
    }
};

class B : public A
{
public:
    B()
    {
        cout << "子类构造函数!" << endl;
    }
    ~B()
    {
        cout << "子类析构函数!" << endl;
    }

};
int main() {
    B b;
    return 0;
}

顺序为:父类构造→子类构造→子类析构→父类析构

这种顺序的原因如下:

  1. 构造顺序原因:
  • 依赖关系:子类对象通常包含父类的部分(通过继承获得的成员变量和成员函数)。在创建子类对象时,首先需要确保父类部分被正确初始化,因为子类的功能可能依赖于父类的正确状态
  • 资源分配和初始化顺序:父类可能需要在构造函数中打开一个文件或者建立一个网络连接,这些资源是子类可能会使用的基础资源
  1. 析构顺序原因:
  • 资源释放顺序:子类对象包含父类部分,所以在析构时应该先释放子类特有的资源,然后再释放父类的资源。如果先析构父类,可能会导致子类在使用父类资源时出现错误,因为这些资源已经被释放了
  • 保证完整性:避免在子类析构过程中依赖已被破坏的父类状态

(5)继承同名成员处理方式

这里只需要记住:访问子类同名成员 直接访问即可; 访问父类同名成员 需要加作用域

比如下面的类A和类B中都有public权限的成员变量Bs,那么按理来说B类继承A类,也会继承A类的这个成员的,但事实上子类对象正常访问成员的成员就是子类的,不是父类的。

如果想要访问父类的成员,可以通过下面两种方式访问:

  • 父类对象直接访问
  • 父类名::成员名 的格式访问,也就是要在前面加上作用域
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
    int Bs = 1;
};

class B : public A
{
public:
    int Bs = 100;
    void f() {
        cout << Bs << endl;     //输出100
        cout << A::Bs << endl;  //输出1
    }
};
int main() {
    B b;
    b.f();
    return 0;
}

(6)继承同名静态成员的处理方式

访问子类同名成员 直接访问即可; 访问父类同名成员 需要加作用域

cpp 复制代码
#include<iostream>
using namespace std;
class A {
public:
    static int a;
    static void show() { cout << "A::show()" << endl; }
};
int A::a = 100;

class B : public A {
public:
    static int a;
    static void show() { cout << "B::show()" << endl; }

    void test() {
        cout << a << endl;          //输出200 访问子类B的a(默认)
        cout << A::a << endl;       //输出100 显式访问父类A的a
        show();                     //输出"B::show()" 访问子类B的show()(默认)
        A::show();                  //输出"A::show()" 显式访问父类A的show()
    }
};
int B::a = 200;
int main() {
    B b;
    b.test();
    return 0;
}

这个程序里面,分别在类A和类B中创建了静态成员变量a和静态成员函数show(),如果在子类中直接访问同名成员,那就直接访问,否则就要加上父类的作用域,进而访问。

(7)多继承

多继承指的是一个类继承多个类,多继承过程中可能导致父类子类中同名成员的产生,需要加上作用域进行调用。在C++实际开发中不建议多继承。

下面就是典型的多继承的例子:

cpp 复制代码
#include<iostream>
using namespace std;
class A1 {

};
class A2 {

};
class B :public A1, public A2 {

};

多继承可能会导致菱形继承的产生,即多继承之后出现一种情况------子类继承两个父类,这两个父类同时继承于其他父类。

比如说↓

这样会导致类B收到两份类A中的继承成员,可能会导致内存浪费或内存不一致。

可以通过在中间类A1和A2的声明中加**关键字"virtual"**解决。

cpp 复制代码
#include<iostream>
using namespace std;
class A {

};
class A1 :virtual public A {

};
class A2 :virtual public A {

};
class B :public A1, public A2 {

};

三、多态

(1)函数重写

子类重新定义父类中有++相同的名称,返回值和参数++ 的虚函数(有virtual关键字),主要在继承关系中出现。必须位于基类和子类中。重写的函数和被重写的函数的返回值、函数名和参数必须完全一致。

子类中的某个函数后如果声明了override关键字,说明其一定是有对基类的对应函数进行重写的。

cpp 复制代码
class Shape {
public:
	virtual void draw() = 0;
};
class Circle :public Shape {
public:
	void draw() override {
		cout << "绘制圆形" << endl;
	}
};

(2)函数隐藏

子类重新定义父类中有相同的名称,只要不是重写就是隐藏。

(3)多态的基本条件:

  • 发生函数重写
  • 重写和被重写的函数必须是virtual函数,并且分别位于基类和子类中。
cpp 复制代码
#include<iostream>
using namespace std;
class A {
public:
	virtual void f() {

	}
};
class B :public A{
	virtual void f() {
		cout << "重写后的函数f" << endl;
	}
};
int main() {

}

(4)多态的类别

  • 静态多态:包括函数重载、运算符重载;更早绑定函数地址,编译阶段确定

  • 动态多态:通过派生类和虚函数实现运行时多态;更晚绑定函数地址,运行阶段确定

(5)多态的使用

多态的使用:父类的指针指向子类的对象。

cpp 复制代码
#include<iostream>
using namespace std;
class A {
public:
	virtual void f() {
		cout << "函数f" << endl;
	}
};
class B :public A{
public:
	virtual void f() {
		cout << "重写后的函数f" << endl;
	}
};
int main() {
	A* a = new B();
	a->f();
	return 0;
}

(6)纯虚函数和抽象类

纯虚函数: 格式:virtual 类型 函数名() = 0

**抽象类:**当类中有了纯虚函数,这个类就成为抽象类。

**抽象类特点:**无法实例化对象;子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

问:下面那个类是抽象类?

cpp 复制代码
#include<iostream>
using namespace std;
class A {
	//纯虚函数
	virtual void f() = 0;
};
class B :public A{
	
};
class C :public A {
	void f() {

	}
};
int main() {
	
	return 0;
}

上面的程序中类A里面有一个纯虚函数f,所以类A是抽象类,类B和类C都继承类A,但是类B因为没有重写纯虚函数f,所以类B是抽象类,类C因为重写了纯虚函数f,所以类C不是抽象类。

(7)虚析构和纯虚析构

多态使用过程中,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。这可以通过++将父类中的析构函数改为虚析构或者纯虚析构++解决。

共同点: 这两个析构可以**解决父类指针释放子类对象(这是核心用途!)**的问题,且都需要有具体的函数实现。

**区别:**如果该类中包含纯虚析构,那么该类属于抽象类,无法实例化对象。

**注:**一个类能够实例化对象当且仅当其能够创建对象。

**纯虚析构的格式为:**virtual ~类名() = 0

**虚析构的格式为:**virtual ~类名()

cpp 复制代码
#include<iostream>
using namespace std;

class A {
public:
    int* p; // 动态内存成员
    A() {
        p = new int(10);
        cout << "[A] 构造:p分配内存,值=" << *p << endl;
    }

    // 关键:虚析构(有函数体,类内实现)
    virtual ~A() {
        delete p;
        p = nullptr;
        cout << "[A] 析构:p已释放" << endl;
    }
};

class B : public A {
public:
    int* p1; // B自己的动态内存成员
    B() {
        p1 = new int(20);
        cout << "B构造:p1分配内存,值=" << *p1 << endl;
    }

    // 子类析构
    ~B() {
        delete p1; // 释放B的动态内存
        p1 = nullptr;
        cout << "B析构:p1已释放" << endl;
    }
};

int main() {
    cout << "测试1:基类指针指向子类对象" << endl;
    A* ptr = new B(); // A指针指向B对象
    delete ptr;       // 虚析构触发:先B析构 → 再A析构

    cout << "\n测试2:直接实例化A(虚析构允许)" << endl;
    A a; // A可实例化,析构时正常释放p

    return 0;
}

运行结果如下:

cpp 复制代码
#include<iostream>
using namespace std;

// 基类A:纯虚析构(抽象类,不可实例化)
class A {
public:
    int* p;
    A() {
        p = new int(10);
        cout << "[A] 构造:p分配内存,值=" << *p << endl;
    }

    virtual ~A() = 0;
};

// 纯虚析构的函数体
A::~A() {
    delete p;
    p = nullptr;
    cout << "[A] 析构:p已释放" << endl;
}

class B : public A {
public:
    int* p1;
    B() {
        p1 = new int(20);
        cout << "[B] 构造:p1分配内存,值=" << *p1 << endl;
    }

    // 子类析构(自动覆盖纯虚析构)
    ~B() override {
        delete p1;
        p1 = nullptr;
        cout << "[B] 析构:p1已释放" << endl;
    }
};

int main() {
    cout << "测试1:基类指针指向子类对象" << endl;
    A* ptr = new B();
    delete ptr; // 仍触发:B析构 → A析构

    // cout << "\n===== 测试2:尝试实例化A(编译报错) =====" << endl;
    // A a; // A是抽象类(纯虚析构导致)

    return 0;
}

运行结果如下:

相关推荐
爱喝可乐的老王2 小时前
机器学习监督学习模型--朴素贝叶斯
人工智能·算法·机器学习
踏过山河,踏过海2 小时前
vs2019报错:Failed to connect to VCTIP: ‘CreateFile‘ failed with 2
c++
Mr -老鬼2 小时前
UpdateEC - EasyClick 项目热更新系统(Rust构建)
开发语言·后端·rust
码农幻想梦2 小时前
KY221 打印日期
开发语言·模拟
爱上妖精的尾巴2 小时前
7-13 WPS JS宏 this 用构造函数自定义类-2,调用内部对象必须用this
开发语言·javascript·wps·jsa
wm10432 小时前
代码随想录第十天 栈和队列
开发语言·python
啊阿狸不会拉杆2 小时前
《机器学习》完结篇-总结
人工智能·算法·机器学习·计算机视觉·ai·集成学习·ml
Java后端的Ai之路2 小时前
【Java教程】- 并发编程核心知识解读
java·开发语言·并发编程
Sheep Shaun2 小时前
C++11核心特性详解:从右值引用到现代C++编程
开发语言·数据结构·c++·算法