- (꒪ꇴ꒪ ),Hello我是祐言QAQ
- 我的博客主页:C/C++语言,数据结构,Linux基础,ARM开发板,网络编程等领域UP🌍
- 快上🚘,一起学习,让我们成为一个强大的攻城狮!
- 送给自己和读者的一句鸡汤🤔:集中起来的意志可以击穿顽石!
- 作者水平很有限,如果发现错误,请在评论区指正,感谢🙏
【探索C++】用实例教你理解面向对象编程(看不懂打我版)_祐言QAQ的博客-CSDN博客
之前写过一篇用最通俗的方法理解面向对象编程思想(链接如上),是对OOP的一种通俗易懂的理解,但这其中还有很多奥秘是一篇简单的举例讲不清的,今天我们就来认真学习一下类和对象中几乎所有的知识点,让你彻底理解C++到底在讲什么鬼。当然,个人拙见,还希望大神在我说错的地方加以指正。
一、编程范式
在学习C++的三大特性以及类和对象的具体使用和区别之前我们先来了解一下编程范式,它是软件开发中的基本思维模式,常见的有面向过程和面向对象两种范式。下面将一下介绍这两种范式的主要特点以及面向对象程序设计的核心概念。
1. 面向过程
- 以函数或过程为中心的编程思想。
- 代表语言:C语言。
- 问题解决的重点在于逻辑流程,先定义问题的逻辑步骤,然后编写相应的函数来实现这些步骤。
- 设计过程通常是先考虑问题的逻辑,然后在此基础上进行抽象。
2. 面向对象
- 以对象为中心的编程思想。
- 代表语言:C++、Java、C#等。
- 问题解决的重点在于对象,每个对象有自己的属性和方法。
- 强调将数据和方法封装在类中,以隐藏内部实现细节,提供清晰的接口。
3. 面向对象程序设计的核心概念
1. 类和对象
类:定义对象的蓝图或模板,包括数据成员和成员函数。
对象:类的实例,包含了类定义的属性和可以执行的方法。
封装:将数据和方法封装在类中,以隐藏内部实现细节,提供清晰的接口。
2. 继承和派生
继承:允许一个类(子类)继承另一个类(父类)的属性和方法。
子类可以重用父类的代码,同时可以添加新的成员或修改继承的成员。
派生:子类从父类继承属性和方法的过程。
3. 多态
多态:允许不同的对象对相同的消息或方法调用作出不同的响应。
多态性使得代码可以更灵活,能够适应不同类型的对象。
多态通常通过虚函数和函数重载实现。
在面向对象程序设计的核心概念里,我们不难发现其真正的核心就是C++的三大特性:封装,继承和多态,那么他们到底是什么呢,接下来我们将其逐一分开解析,争取让你能在这一篇文章中理解C++的精髓所在。
二、抽象和封装
在面向对象编程中,抽象和封装是两个关键概念,它们有助于有效地设计和组织代码。
2.1 抽象
抽象是指对具体对象(事物)进行概括,提取出这一类对象的公共性质以进行描述。在面向对象编程中,抽象可以分为两种主要类型:
(1) 数据抽象(性质抽象)
数据抽象描述了某类对象的属性或状态,这些属性通常是对象之间的区别所在,它们形成了数据成员。以汽车为例,数据抽象可以包括:
- 车的颜色
- 车的窗户数量
- 车的门数量
- 其他与车辆属性相关的信息
这些数据成员用于表示汽车对象的属性。
(2) 方法抽象(行为抽象)
方法抽象描述了某类对象的公共行为或具备的功能,这些行为通常被形成为方法成员(也就是函数成员)。以汽车为例,方法抽象可以包括:
- 开车
- 倒车
- 转弯
- 其他与车辆行为相关的操作
这些方法成员定义了汽车对象可以执行的操作或行为。
抽象有助于将现实世界的事物映射到代码中,使我们能够更好地理解和处理问题领域。它是面向对象程序设计中的关键概念之一。
2.2 封装
封装是将数据和相关操作封装在一个单元(通常是一个类)内部的过程,以隐藏实现细节,并提供清晰的接口供外部使用。封装有以下优点:
- 数据隐藏:数据成员可以被声明为私有,从而防止直接访问和修改,提高了数据的安全性和完整性。
- 接口定义:通过公共成员函数来操作数据,为外部提供了一个清晰的接口,降低了使用代码的复杂度。
- 实现隔离:封装允许类的内部实现细节发生变化,而不会影响外部代码,提高了代码的灵活性和可维护性。
封装是面向对象编程中的核心概念之一,有助于构建模块化和可扩展的代码。
三、类(class)
3.1 概念
类 是面向对象编程中的关键概念,它抽象出具有相同特性(数据成员)和行为(函数成员)的对象。对象是类的实例化。
3.2 基本语法
类的声明:
class 类名
{
public:
// 类的公有数据成员和方法成员protected:
// 类的保护数据成员和方法成员private:
// 类的私有数据成员和方法成员
};
注意事项:
- 类名一般首字母大写。
- 类的成员默认访问权限是私有(private)。
- 类的成员访问权限包括:公有(public)、保护(protected)和私有(private)。
- public:允许在外部直接通过类的对象访问。
- protected:不允许在外部直接通过类的对象访问,只能在类内部进行访问,可以通过类内部的公有接口访问。
- private:不允许在外部直接通过类的对象访问,只能在类内部进行访问,可以通过类内部的公有接口访问。
3.3 类的成员函数实现
类的成员函数可以在类内部或外部进行实现。
(1)内部实现:
class A
{
public:
// 成员函数内部实现,可以访问私有成员x
void set_x(int x1)
{
x = x1;
}
private:
int x;
};
(2)外部实现:
class A
{
public:
// 成员函数外部实现,声明在类内部,定义在外部
int get_x();
private:
int x;
};
// 成员函数的外部定义
int A::get_x()
{
return x;
}
当然在内部实现时可行且简便的,但是如果一个方法函数很长,也就是功能很复杂时,我们将其定义在类内是一种及其不合理的做法,因此一般开发都是将成员函数(方法函数)写在类外实现的,这样不仅美观,修改起来也更方便。
四、对象(Object)
对象 是类的实例化,它包含了类定义的属性和可以执行的方法。在面向对象编程中,对象是操作和交互的主要实体。
4.1 定义对象
(1)直接定义对象
要定义一个类的对象,只需使用该类的名称和对象名称即可。
// 定义一个名为person的Person类对象
Person person;
(2)使用指针定义对象
你也可以使用指针来定义类的对象,这通常与动态内存分配相关。
// 使用指针定义Person类对象
Person* personPtr = new Person;
4.2 通过类的对象访问成员
(1) 访问公有成员
通过对象名,你可以直接访问类的公有成员变量和方法。
// 访问公有成员变量
person.name = "Alice";
int age = person.age;
// 调用公有成员方法
person.displayInfo();
如果你有一个指向对象的指针,可以使用箭头运算符(->)来访问对象的公有成员。
// 使用指针访问公有成员变量
personPtr->name = "Bob";
int age = personPtr->age;
// 使用指针调用公有成员方法
personPtr->displayInfo();
(2) 访问非公有成员
非公有成员通常无法直接访问,必须通过公有函数(公开接口函数)进行访问。这种方式保证了封装性和数据的安全性。
// 在类的公有函数中访问非公有成员
class Person {
public:
// 公有函数用于访问私有成员name
void setName(const std::string& newName) {
name = newName;
}
// 公有函数用于获取私有成员name
std::string getName() {
return name;
}
private:
std::string name;
};
4.3 示例
巴巴一大堆了,演示一个实例吧:设计一个简单的关于类的程序,定义一个点类(Point),包含x,y坐标(私有成员),该类包含以下功能:
(1)设置X坐标,(公有成员,在内部实现);
(2)设置Y坐标,(公有成员,在外部实现);
(3)得到X坐标,(保护成员,在内部实现);
(4)得到Y坐标,(保护成员,在外部实现);
(5)最终显示坐标(10,13)(私有成员,在外部实现);
代码:
#include <iostream>
using namespace std;
class Point
{
public:
// 设置X坐标,在内部实现
void set_x(int x1)
{
x = x1;
}
// 设置Y坐标,在外部实现
void set_y(int y1);
int get_x_value()
{
return get_x();
}
int get_y_value()
{
return get_y();
}
void show()
{
show_xy();
}
protected:
// 得到X坐标,在内部实现
int get_x()
{
return x;
}
int get_y();
private:
int x;
int y;
void show_xy();
};
// 设置Y坐标,在外部实现
void Point::set_y(int y1)
{
y = y1;
}
// 得到y坐标,外部
int Point::get_y()
{
return y;
}
// 显示函数,外部
void Point::show_xy()
{
cout << "(" << get_x_value() << "," << get_y_value() << ")" << endl;
}
int main(int argc, char const *argv[])
{
Point a;
int x1 = 0;
int y1 = 0;
cout << "输入 X 坐标:" << endl;
cin >> x1;
a.set_x(x1);
cout << "输入 Y 坐标:" << endl;
cin >> y1;
a.set_y(y1);
a.show();
// cout << "(" << get_x_value() << "," << get_y_value() << ")" << endl;
return 0;
}
这是一个平平无奇的简单例子,但它却用到了上述几乎所有知识点,如果你能完全看懂并理解,那么我想你的前途无量啊!哈哈哈~
五、类的特殊函数成员
在面向对象编程中,类可以拥有特殊的函数成员,包括构造函数、析构函数和拷贝构造函数。这些函数在对象的创建、销毁和复制过程中发挥重要作用。
5.1 构造函数
class MyClass {
public:
// 构造函数的声明
MyClass(); // 默认构造函数,无参数
// 构造函数的定义
MyClass() {
// 在构造函数中初始化数据成员
// 这里可以添加初始化操作
}
private:
// 数据成员
int data;
};
// 构造函数的实现
MyClass::MyClass() {
// 在构造函数中初始化数据成员
// 这里可以添加初始化操作
}
构造函数用于在实例化对象时初始化对象的数据成员。构造函数具有以下特点:
- 构造函数的名称必须与类名相同。
- 构造函数没有返回值,包括不带返回类型(void)。
- 默认情况下,每个类都有一个默认构造函数,如果没有自己编写构造函数,系统会自动生成一个空的默认构造函数。
- 构造函数必须是公有的(public)。
(1)关于构造函数的重载和默认参数:
构造函数可以重载,允许多个构造函数根据参数不同进行选择。构造函数还可以带有默认参数,但在多个构造函数重载的情况下,要慎用默认参数,以免造成歧义。
(2)关于构造函数的初始化列表:
构造函数可以使用初始化列表的方式来初始化成员变量,这样可以提高效率,特别是在处理类的成员变量是对象类型或常量时。
(3) 示例代码
设计一个基于对象的程序,来实现求长方体的体积与表面积。(提示:抽象出一个长方体类Cuboid,数据成员包括:length,width,high),通过构造函数实现数据成员的初始化。要求:
1、有键盘输入长方体的长宽高;
2、计算长方体的体积(公有函数);
3、显示长方体长宽高跟体积、表面积(公有函数)。
#include <iostream>
using namespace std;
class Cuboid
{
public:
// 构造函数
Cuboid(int x, int y, int z)
{
length = x;
width = y;
high = z;
}
void show_v()
{
cout << length * width * high << endl;
}
void show_all()
{
cout << "长:" << length << endl;
cout << "宽:" << width << endl;
cout << "高:" << high << endl;
cout << "体积:" << length * width * high << endl;
cout << "表面积:" << 2 * (length * width + length * high + width * high) << endl;
}
private:
int length;
int width;
int high;
};
int main(int argc, char const *argv[])
{
int x, y, z;
cout << "请输入长宽高:" << endl;
cin >> x >> y >> z;
Cuboid cuboid(x, y, z);
cuboid.show_v();
cuboid.show_all();
return 0;
}
5.2 析构函数
class MyClass {
public:
// 析构函数的声明
~MyClass();
private:
// 数据成员
int data1;
double data2;
char* data3; // 示例指针成员
};
// 析构函数的定义
MyClass::~MyClass() {
// 在析构函数中清理资源
// 这里可以添加资源释放操作
}
析构函数用于在对象被销毁时清理并释放对象分配的资源和内存。析构函数具有以下特点:
- 析构函数的名称是类名前面加上波浪号(~)。
- 析构函数没有参数,也不能重载。
- 如果没有定义析构函数,编译器会生成一个默认析构函数。
- 析构函数通常用于释放特殊资源,如文件句柄、动态内存等。
示例代码:
设计一个文件相关的类,包含一个数据成员是文件描述符,要求在构造函数里面实现指定文件的打开,在析构函数里面实现文件的关闭。
它没打开是因为我没创建,仅此而已。
#include <iostream>
extern "C"
{
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
}
using namespace std;
class File
{
public:
File(string filename)
{
cout << "这是构造函数" << endl;
fd = open(filename.c_str(), O_RDWR);
if (fd == -1)
{
cout << "文件打开失败" << endl;
}
else
{
cout << "文件打开成功" << endl;
}
}
~File()
{
cout << "这是析构函数" << endl;
close(fd);
}
private:
int fd;
};
int main(int argc, char const *argv[])
{
string filename;
cin >> filename;
File f(filename);
File f1(f); //拷贝构造函数
return 0;
}
5.3 拷贝构造函数
class MyClass {
public:
// 拷贝构造函数的声明
MyClass(const MyClass& other);
private:
// 数据成员
int data1;
double data2;
char* data3; // 示例指针成员
};
// 拷贝构造函数的定义
MyClass::MyClass(const MyClass& other) {
// 在拷贝构造函数中复制其他对象的值
// 这里可以根据需要实现复制逻辑
}
拷贝构造函数用于将一个对象的值复制到另一个新构造的对象。拷贝构造函数具有以下特点:
- 拷贝构造函数的参数是对同类对象的常引用(const 类名& 对象名)。
- 拷贝构造函数可以自定义,但如果没有自定义,编译器会生成一个默认的拷贝构造函数。
- 自定义拷贝构造函数通常用于处理类中特殊资源的复制。
(1)为什么要加 const ?
保证在拷贝过程中不会修改原对象;
const不能转非const,此处加const可以满足两种。
(2)为什么要用引用 ?
如果不加引用,那么在调用拷贝构造函数的时候,实参给形参赋值这个又是一个拷贝过程,这个过程就会一直递归下去。
(3)什么时候会调用拷贝构造函数
语法:
类名(const 类名 &对象名)
{
//代码块;
}示例:
A(const A &a)
{
cout << "拷贝构造函数" << endl;
ax = a.ax;
}
① 类对象的赋值运算:A a3 = a1;
② 用一个对象去构造另外一个对象的时候:A a2(a1);
③ 类的对象作为函数返回的时候,也会调用拷贝构造函数;
④ 类的对象作为函数形参的时候,在函数调用过程中也会发生拷贝构造函数的调用。
注意:
① 类的对象的引用作为函数返回的时候,不会调用拷贝构造函数;
② 类的对象的引用作为函数形参的时候,在函数调用过程中不会发生拷贝构造函数的调用。
(4)什么时候需要自定义拷贝构造函数?
当类持有其他特殊资源(比如指针)的时候,不能用默认拷贝构造函数,要自定义拷贝构造函数:
① 在构造过程中有文件相关操作的时候;
② 动态内存空间(堆空间);
③ 网络连接相关的操作;
④ 指针指向一些其他特殊资源。
(5)深拷贝和浅拷贝
① 默认拷贝构造函数,属于浅拷贝,直接做值的一个赋值,不会去考虑空间的一个申请。
浅拷贝的坏处就是在有特殊成员的时候,直接赋值地址,导致在析构函数释放空间的时候,造成二次释放,报double free这个错误。
class A
{
public:
A(int a, int b)
{
cout << "构造函数" << endl;
ax = a;
ay = new int(b);
}
~A()
{
cout << "析构函数" << endl;
delete ay;
}
//拷贝构造函数
A(const A &a) //这种拷贝方式跟来里面默认拷贝方式一样的,直接赋值,浅拷贝
{
cout << "拷贝构造函数" << endl;
ax = a.ax;
ay = a.ay; //地址赋值
}
private:
int ax;
int *ay;
};
② 将对象所持有的其他资源一并拷贝,并且为新对象单独分配特殊数据资源的方法就是深拷贝。
深拷贝过后,原有对象和新的对象所持有的动态数据是相互独立的,更改一个对象的数据对另一个对象不会有影响的。
class A
{
public:
A(){cout << "默认无参构造函数" << endl;}
A(int a, int b)
{
cout << "构造函数" << endl;
ax = a;
ay = new int(b);
}
~A()
{
cout << "析构函数" << endl;
delete ay;
}
//拷贝构造函数
A(const A &a)
{
cout << "拷贝构造函数" << endl;
ax = a.ax;
ay = new int; //先申请新的空间
*ay = *a.ay; //值传递
}
private:
int ax;
int *ay;
};
更多C/C++语言、Linux系统、数据结构和ARM板实战相关文章,关注专栏:
📢写在最后
- 今天的分享就到这啦~
- 觉得博主写的还不错的烦劳
一键三连喔
~ - 🎉🎉🎉感谢关注🎉🎉🎉