C++系列学习03
前言
美好的一天开始了,众所周知C++是一门面向对象的编程语言,但凡是面向对象编程的语言,则具备必不可少三大特点:封装、继承和多态,今天则来学习第一大特性——封装。
一、面向对象or面向过程?
面向过程编程(Procedural Programming):
面向过程是一种以过程或函数为核心的编程方式。在这种编程模式下,程序被分解成一系列的过程或函数,每个过程负责完成一个特定的任务。数据通常在过程中作为参数传递,并且可以在整个程序中自由访问。
面向对象编程(Object-Oriented Programming, OOP):
面向对象是一种将数据和基于数据的操作封装在一起的编程范式。它通过“类”来定义对象的属性(成员变量)和行为(成员方法),并通过实例化类来创建对象。OOP支持继承、封装和多态等特性,这些特性有助于构建更加模块化和易于维护的软件系统。
二者的区别:
特性 | 面向对象编程 | 面向过程编程 |
---|---|---|
中心思想 | 以对象为中心,强调数据和操作的封装 | 以过程/函数为中心,强调执行流程 |
扩展性 | 更易于扩展,新的功能可以通过新增类或继承现有类实现 | 扩展需要修改已有代码 |
维护性 | 因为封装、抽象等特性,使得代码更加清晰,易于维护 | 当项目规模增大时,维护难度增加 |
安全性 | 对象的数据可以隐藏,外部只能通过接口访问 | 数据通常对外部公开,安全性较低 |
复杂度管理 | 使用封装、继承、多态等机制有效管复理杂度 | 复杂度随着程序规模增长而增加,难以管理 |
典型语言 | C++,Java,Python(支持OOP)等 | C,Pascal,Basic等 |
二、类&对象
简单来说,类是一种模板,类的实例化就是对象,而对象的抽象集成则是类。
类:
类是一种抽象的数据类型,它封装了数据的格式和可以施加于这些数据上的操作。换句话说,类是创建对象的蓝图或模板,定义了一组具有相同属性(成员变量)和行为(成员函数)的对象。通过类,我们可以创建多个具有相似特征的对象实例。
对象:
对象是类的具体实例。如果说类是一个模板或蓝图,那么对象就是根据这个蓝图创建出来的实际物品。每个对象都有自己的状态(成员变量表示)和行为(通过调用成员函数来执行)。创建对象的过程也被称为实例化。
三、访问权限
类将一些成员变量、成员函数或其他的类封装一起,当我们需要访问调用这些成员时,并不是想访问就可以访问的,不然怎么叫封装呢?顾名思义,封装就是将成员集中在一个密闭空间,访问的时候是需要条件的。封装的三大权限等级由严到低分别是:private、protected和public。
private:私有的,只限于类的内部成员函数访问使用。
protected:受保护的,只限于类的内部成员函数和该类下的派生类(子类)可以访问使用。
public:公有的,不仅类的内部成员函数和该类的子类可以访问,类的外部函数也可以访问。
不难看出,private的等级最高最严格,public的等级最低。值得注意的是,当我们没有直接写访问权限时,默认的是private私有访问。在实际项目中,我们也一般推荐类的成员变量设置为私有的,避免外部访问。
四、构造函数&析构函数
在面向对象编程中,构造函数(Constructor)和析构函数(Destructor)是类的两个特殊成员函数,它们在对象的生命周期中起着至关重要的作用。
简单来说:
构造函数用于初始化对象,在创建对象时自动调用,用来设置对象的初始状态。
析构函数用于清理对象占用的资源,在对象生命周期结束时自动调用,如释放动态内存。
当我们什么也不做,编译器会自动为我们创建默认的无参构造函数、拷贝构造函数和析构函数。
1、无参构造函数
无参构造函数没有返回值,函数名必须和类名保持一致,既然是无参构造,也不需要形参。
class Demo{
//默认是private权限
int a;
public:
//无参构造函数
Demo(){
//可以给a初始化
}
};
//1、隐式调用无参构造
Demo demo1;
//2、显式调用无参构造
Demo demo2 = Demo();
编译器默认提供的是函数体为空的无参构造函数,一旦我们自定义了无参构造函数,就会屏蔽编译器默认提供的无参构造函数,而是调用我们自定义的函数。
2、有参构造函数
有参构造函数,那么都叫有参了,就可以有形参列表了,其他都和无参构造一样。
class Demo{
int a;
public:
//有参构造函数
Demo(int ma){
//将形参赋值给成员变量a
a=ma;
}
};
/*
在main函数中,实例化对象时调用有参构造
*/
//1、隐式调用有参构造
Demo demo1(100);
//2、显式调用有参构造
Demo demo2 = Demo(100);
/*
3、当有参构造函数的参数只有一个时,可能发生隐式转换。
看起来似乎是变量的初始化,将99赋值给Demo类型的demo3变量,如int a=100;
实际是调用有参构造函数,可以使用explicit修饰有参构造函数,禁用隐式转换
*/
Demo demo3 = 99;
//4、匿名对象:没有对象名,只在当前行生效
Demo()
一旦我们自定义了有参构造函数,就会屏蔽编译器默认提供的无参构造函数,而是调用我们自定义的函数。
3、拷贝构造函数
拷贝构造函数,用一个已存在的同类对象来初始化一个新创建的对象,它在对象复制时自动调用。
拷贝构造函数的语法格式为:类名(const 类名& other);
class Demo{
public:
int a = 10;
public:
//拷贝构造函数
//other可以自定义任意名称
Demo(const Demo& other){
//将其他对象的a拷贝给当前对象
a = other.a;
}
};
//main函数中,会调用拷贝构造函数的情况
//1.用新对象初始化旧的同类型对象
Demo demo1;
Demo demo2 = demo1;
//2. 函数参数传递(按值传递对象)
void fun1(Demo d){
cout << d.a << endl;
}
fun1(demo2);
//3.函数返回值(按值返回)
/*
现代编译器常使用返回值优化(RVO)或移动语义避免不必要的拷贝
所以可能看不到拷贝构造函数的调用
*/
Demo fun2() {
Demo demo3;
return demo3;
}
类似无参构造函数,当我们不做处理时,编译器会自动创建一个默认的拷贝构造函数,当我们自定义了自己的拷贝构造函数,就会屏蔽默认的拷贝构造函数。
值得注意的是:
如果我们自定义了拷贝构造函数、有参构造函数、无参构造函数中的任意一种,都会屏蔽编译器提供的默认的无参构造函数。
如果我们自定义了无参构造函数或者有参构造函数,不会屏蔽编译器提供的默认的拷贝构造函数。
拷贝构造函数默认的是浅拷贝。
4、析构函数
析构函数没有返回值,没有参数,函数名同样和类名保持一致,函数名前加~。析构函数不支持重载,每个类只能有一个析构函数。
析构函数的语法格式为:~类名(); 注意:类名前加波浪号~
class Demo{
int a;
public:
//析构函数,函数名前加~(波浪号)
~Demo(){
//通常为释放创建的堆空间
}
};
析构函数同样在没有自定义的情况下,编译器会自动提供默认的析构函数。需要自定义析构函数的情况通常是类的成员变量存在指针类型,在堆上开辟了空间,需要手动释放。
五、初始化列表
初始化列表主要用于给类的成员变量提供初始值。常量成员和引用成员必须在对象创建时进行初始化,并且不能在之后修改,因此只能通过初始化列表来进行设置。
初始化列表通常跟在构造函数后面,并且先进行初始化列表的内容,再执行构造函数体内的内容。
构造函数名(形参列表) : 成员变量1(初始值1),成员变量2(初始值2),......
{// 函数体内容
}
class Demo{
int a;
int b;
public:
//有参构造函数+初始化列表
Demo(int ma,int mb) :a(ma),b(mb)
{
//原本的初始化内容
//a = ma;
//b = mb;
}
};
六、浅拷贝&深拷贝
在之前的拷贝构造函数中,默认使用的是浅拷贝。那么什么是浅拷贝和深拷贝呢?二者有什么联系和区别呢?顾名思义,深拷贝比浅拷贝的程度更深,意味着当对象A拷贝给对象B,修改对象A的内容,对于浅拷贝,对象B的内容也会被修改,而对于深拷贝则不会修改。
对于普通成员变量:
浅拷贝和深拷贝都是开辟内存空间,复制数据的值,修改一方的值不会影响原本的值。
对于指针类型:
浅拷贝:只会复制指针本身的值(即内存地址),而不会复制指针所指向的数据。结果是,两个对象的指针成员都指向同一块堆内存。
深拷贝:为指针成员分配新的内存,并将原对象指针所指向的数据完整地复制到新分配的内存中。
6.1 浅拷贝
#include <iostream>
using namespace std;
class ShallowCopy {
public:
char* str;
public:
//无参构造函数
ShallowCopy() {
str = new char[20];
strcpy_s(str, 20, "KFC");
}
//拷贝构造函数
ShallowCopy(const ShallowCopy& other) {
str = other.str;
}
//析构函数
~ShallowCopy() {
delete[] str;
}
};
int main()
{
ShallowCopy obj1;
ShallowCopy obj2 = obj1; //调用拷贝构造,浅拷贝
cout << "obj1.str=" << obj1.str << endl;
cout << "obj2.str=" << obj2.str << endl;
strcpy_s(obj1.str, 20, "McDonald's"); //修改obj1的str
cout << "---------修改obj1后--------" << endl;
cout << "obj1.str=" << obj1.str << endl;
cout << "obj2.str=" << obj2.str << endl;
return 0;
}
代码运行结果如下:
不难发现修改obj1的str的值,obj2的值也跟着修改,这就是浅拷贝的局限性。与此同时,系统提示如下,发现我们在释放指针时出了问题,也就是浅拷贝的双重释放问题。当然,我们的代码看似正常运行(最危险!在 Release 模式下可能侥幸通过,但程序已损坏)
6.2 双重释放
在上文中,obj1和obj2两个对象共享同一块内存。当两个对象在作用域结束时被销毁,它们的析构函数都会尝试释放这块内存,从而引发双重释放。
首先析构obj2,delete语句后成功释放内存空间,再析构obj1的时候,系统发现obj1指向的内存空间已经被释放了,继而报错。
那么如何解决浅拷贝的问题呢?聪明的你肯定已经知道答案了,那就使用深拷贝呗。
6.3 深拷贝
#include <iostream>
using namespace std;
class DeepCopy {
public:
char* str;
public:
//无参构造
DeepCopy() {
str = new char[20];
strcpy_s(str, 20, "KFC");
}
//拷贝构造
DeepCopy(const DeepCopy& other) {
int len = strlen(other.str) + 1;
str = new char[len]; //与浅拷贝的根本不同,重新开辟了新的内存空间
strcpy_s(str, len, other.str);
}
//析构函数
~DeepCopy() {
delete[] str;
}
};
int main()
{
DeepCopy obj1;
DeepCopy obj2 = obj1;
cout << "obj1.str=" << obj1.str << endl;
cout << "obj2.str=" << obj2.str << endl;
strcpy_s(obj1.str, 20, "McDonald's");
cout << "---------修改obj1后--------" << endl;
cout << "obj1.str=" << obj1.str << endl;
cout << "obj2.str=" << obj2.str << endl;
return 0;
}
代码运行结果如下:
可以发现修改完obj1的内容,obj2的内容并没有跟着修改,那是因为深拷贝会独自开辟一片内存空间,和原本的对象互不影响,最后也各自成功析构。所以以后遇到类中的成员变量是指针时,还得是使用深拷贝才放心。
七、小记
今天整理的知识点就是这些,还得是多敲代码,啥也别说了,KFC和McDonald's在向我招手。