C++系列学习

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在向我招手。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值