【C++面向对象】封装(上):探寻构造函数的幽微之境

每文一诗 💪🏼

我本将心向明月,奈何明月照沟渠 ------ 元/高明《琵琶记》

**译文:**我本是以真诚的心来对待你,就像明月一样纯洁无瑕;然而,你却像沟渠里的污水一样,对这份心意无动于衷,甚至于不屑一顾。


如果本文对你有所帮助,那能否支持一下老弟呢,嘻嘻🥰

✨✨个人主页 点击✨✨

封装

封装作为C++面向对象的三大特性之一

封装将数据和操作数据的代码组合成一个独立的单元,也就是类。类的成员可以有不同的访问权限,如公有(public)、私有(private)和受保护(protected),以此来控制外部对这些成员的访问。

类的结构

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

  • class类名:指的是类的名称 这个名称可以随意命名例如class hunman
  • 访问权限指的是:如公有(public)、私有(private)和受保护(protected)
  • 属性和行为是指的在这个类当中所定义的变量和方法。

例如这里定义了一个人类

cpp 复制代码
#include <iostream>
class hunman
{
private:
   std::string name;
public:
    hunman();
    ~hunman();
protected:
    std::string ID_card;
};

struct和class区别

struct默认是公共权限,class默认是私有权限

也就是说在sttruct中定义的变量和方法,可以在外部用该对象直接访问;

而对于Class中定义的变量和方法,如果不指定其权限,默认是无法在外部通过其对象访问的。

构造函数和析构函数

在C++中,构造函数 :是指在这个类当中进行对变量的初始化操作,主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。即在该类的对象被实例化后,构造函数会被立即调用。

在C++中,析构函数 :主要作用在于对象销毁前系统自动调用,执行一些清理工作。例如,当程序中有使用动态内存,即使用new操作符,那么可以在析构函数中进行delete,即内存释放。

构造函数的分类

构造函数的语法是:类名(){} 名称和类名相同,可以重载。

  • 无参构造函数:human(){}
  • 有参构造函数:human(std::string name){}
  • 拷贝构造函数:human(const human& h){}

解释:

  • 无参构造函数:是指这个类当中的构造函数不传入任何参数
  • 有参构造函数:是指这个类当中的构造函数传入参数,通常时将传入的参数赋值给类当中的成员变量
  • 拷贝构造函数 :传入的参数是和个类的对象,为什么要传入const +引用的形式呢?,是因为我们不想传入的对象被修改,并且以引用的方式传递,这个可以避免被传入的对象占用的内存非常大时,对其的拷贝,使用引用本质上是在使用指针,避免内存的拷贝,提高程序的效率。

构造函数调用规则

  • 括号法
  • 显示法
  • 隐式转换法

形式1:类名 变量名**hunman p****调用函数:**无参构造函数,析构函数

代码

cpp 复制代码
#include <iostream>
class hunman
{
private:
   std::string name;
public:
    //无参构造函数
    hunman(){
        std::cout<<"无参构造函数"<<std::endl;
    }
    //有参构造函数
    hunman(float heightval){
        height = heightval;
         std::cout<<"有参构造函数"<<std::endl;
    }
    //拷贝构造函数
    hunman(const hunman& h){
        height = h.height;
        std::cout<<"拷贝构造函数"<<std::endl;
    }
    //析构函数
    ~hunman()
    {
        std::cout<<"析构函数"<<std::endl;       
    }

    float height;
protected:
    std::string ID_card;
};

int main(int argc, char const *argv[])
{
    hunman h;
    /* code */
    return 0;
}

输出

形式2:类名() 或者 类名 变量名() hunman(1.80)****或者hunman h(1.80);**调用函数:**有参构造函数,析构函数

代码

cpp 复制代码
#include <iostream>
class hunman
{
private:
   std::string name;
public:
    //无参构造函数
    hunman(){
        std::cout<<"无参构造函数"<<std::endl;
    }
    //有参构造函数
    hunman(float heightval){
        height = heightval;
        std::cout<<"有参构造函数"<<std::endl;
    }
    //拷贝构造函数
    hunman(const hunman& h){
        height = h.height;
        std::cout<<"拷贝构造函数"<<std::endl;
    }
    //析构函数
    ~hunman()
    {
        std::cout<<"析构函数"<<std::endl;       
    }

    float height;
protected:
    std::string ID_card;
};

int main(int argc, char const *argv[])
{
    // hunman h;
    hunman(1.80);
    /* code */
    return 0;
}

输出

形式2:**类名 变量名 = 类名(参数)**hunman h1 = hunman(1.80);**调用函数:**有参构造函数,析构函数

代码

cpp 复制代码
#include <iostream>
class hunman
{
private:
   std::string name;
public:
    //无参构造函数
    hunman(){
        std::cout<<"无参构造函数"<<std::endl;
    }
    //有参构造函数
    hunman(float heightval){
        height = heightval;
        std::cout<<"有参构造函数"<<std::endl;
    }
    //拷贝构造函数
    hunman(const hunman& h){
        height = h.height;
        std::cout<<"拷贝构造函数"<<std::endl;
    }
    //析构函数
    ~hunman()
    {
        std::cout<<"析构函数"<<std::endl;       
    }

    float height;
protected:
    std::string ID_card;
};

int main(int argc, char const *argv[])
{
    // hunman h;
    // hunman(1.80);
    hunman h1 = hunman(1.80);
    // hunman h2 = hunman(h1);
    /* code */
    return 0;
}

输出

形式2:类名 变量名 = 参数hunman h1 = 1.80;**调用函数:**有参构造函数,析构函数

代码

cpp 复制代码
#include <iostream>
class hunman
{
private:
   std::string name;
public:
    //无参构造函数
    hunman(){
        std::cout<<"无参构造函数"<<std::endl;
    }
    //有参构造函数
    hunman(float heightval){
        height = heightval;
        std::cout<<"有参构造函数"<<std::endl;
    }
    //拷贝构造函数
    hunman(const hunman& h){
        height = h.height;
        std::cout<<"拷贝构造函数"<<std::endl;
    }
    //析构函数
    ~hunman()
    {
        std::cout<<"析构函数"<<std::endl;       
    }
    float height;
protected:
    std::string ID_card;
};

int main(int argc, char const *argv[])
{
    // hunman h;
    // hunman(1.80);
    // hunman h1 = hunman(1.80);
    // hunman h2 = hunman(h1);
    hunman h1  = 1.80;
    /* code */
    return 0;
}

输出:

默认情况下,c++编译器至少给一个类添加3个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

构造函数调用规则如下:

如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造

如果用户定义拷贝构造函数,c++不会再提供其他构造函数

拷贝构造函数

拷贝构造函数的调用

当将一个当前类的对象作为参数传入构造函数中后,就会调用拷贝构造函数

cpp 复制代码
    hunman h;
    hunman h2 = hunman(h);

这里为什么析构函数调用了两次呢?

很好理解,因为你实例化了两个对象,分别是h,h2。

深拷贝与浅拷贝

这个问题时对于拷贝构造函数的经典问题

浅拷贝:

是指在定义类时,没有在类中显式的构建拷贝构造函数,将一个类的对象作为参数传入类的构造函数中,编译器会把原对象中栈区的变量的值和堆区指针的值复制,而不复制一个指针指向的值

导致的问题:

在类执行完成,调用析构函数函数时,并且析构函数中有使用delete对指针进行释放时,会给两个对象中的指针分别释放,而两个对象中指针的值时相同的,就比如说第一个对象中指针的值是0x123,另一个对象中指针的值也是0x123,根据栈区先进后出的原则,后被创建的对象会先进行释放对象中的指针0x123,释放之后,另一个对象中的指针0x123也会被释放,但是这时就会报错,因为0x123这个指针已经被释放过了。

代码演示 (先看一下没有构建拷贝构造函数时,栈区和堆区变量是否被复制)

cpp 复制代码
#include <iostream>
class hunman
{
private:
   std::string name;
public:
    //无参构造函数
    hunman(){
        std::cout<<"无参构造函数"<<std::endl;
    }
    //有参构造函数
    hunman(float heightval,int ageval){
        height = heightval;
        age = new int(ageval);
        std::cout<<"有参构造函数"<<std::endl;
    }
    //析构函数
    ~hunman()
    {
        std::cout<<"析构函数"<<std::endl;       
    }
    float height;
    int *age;
protected:
    std::string ID_card;
};

int main(int argc, char const *argv[])
{
    hunman h(1.80,23);
    hunman h2 = hunman(h);
     std::cout<<"第一个对象中的height的值:"<<h.height<<"  第二个对象中的height的值:"<<h2.height<<std::endl;
    std::cout<<"第一个对象中的age的值:"<<h.age<<"  第二个对象中的age的值:"<<h2.age<<std::endl;
    return 0;
}

输出

解析:

这段代码中比没有构建拷贝构造函数,而这时编译器会默认构建一个,刚才说过栈区和堆区变量会被复制,根据输出可以直观的看到,第一个对象中的栈区变量height和堆区中的age变量所存储的值是相同的。

但是上段代码有些问题,因为我们既然创建了一个指针变量,即动态分配内存,那么就应该手动释放这块内存,即使用delete。

在析构函数中添加

cpp 复制代码
 ~hunman()
    {
        if(age != nullptr)
        {
            delete age;
            age = nullptr;
        }
        std::cout<<"析构函数"<<std::endl;       
    }

但是执行后出现问题

通过图中我们可以看到,第一个析构函数已经成功执行,但是第二个析构函数执行时却发生的报错free(): double free detected in tcache 2

这个错误提示表明你试图对同一块已经释放的内存进行了多次释放操作,也就是所谓的 "双重释放" 问题

这个双重释放也很好理解,因为在第二个对象对0x123这个地址释放后,原对象再次对这个地址释放是没有用的,因为他已经被释放过了。

注:

hunman h(1.80,23);原对象h(先进后出,后释放)

hunman h2 = hunman(h);第二个对象h2(先进后出,先释放)

解决方法:

使用深拷贝

深拷贝是指在复制对象时,不仅复制对象的基本数据类型的值,还会为对象中的指针成员分配新的内存空间,并将原指针所指向的内存内容复制到新的内存空间中。这样,原对象和拷贝对象的指针成员会指向不同的内存地址。

代码,显式构建拷贝构造函数

cpp 复制代码
    // 拷贝构造函数
    hunman(const hunman& h){
        height = h.height;
        age = new int(*h.age);
        std::cout<<"拷贝构造函数"<<"地址:"<<std::endl;
    }

在拷贝构造函数中对栈区的变量重新复制,并且对堆区的指针重新分配内存,使其和原对象中的指针的值不是一个地址,这样第二个对象释放一个地址,而另一个对象释放里一个地址,这样互不干涉,程序就不会崩溃了。

由图可知,两个对象当中的age 的值即地址是不一样的,完美解决!

**注:**在显式的构建拷贝构造函数时,在函数中应该对需要复制的值手动赋值,因为在没有构建拷贝苟造函数时,编译器会帮你把所有的变量复制,但是当你显式构建是,就没了,所以需要手动复制。

构造函数初始化列表

使用构造函数初始化列表可以帮助我们快速的初始化成员变量

语法:类名(参数1,参数2,...): 成员变量1(参数1),成员变量2(参数2),...

cpp 复制代码
#include <iostream>
class hunman
{
public:
    //构造函数初始化列表
    hunman(int a,float b,std::string c): age(a),height(b),name(c){
        std::cout<<"age:"<<age<<std::endl;
        std::cout<<"height:"<<height<<std::endl;
        std::cout<<"name:"<<name<<std::endl;
    }
    int age;
    float height;
    std::string name;
};

int main(int argc, char const *argv[])
{
    hunman(21,1.80,"rqtz");
    return 0;
}

类对象作为类成员

当有另一个类a的对象 作为类h的成员变量时候,构造函数和析构函数的调用顺序

构造函数顺序 :先a后h

析构函数顺序:先h后a

代码

cpp 复制代码
#include <iostream>
class animal
{

public:
    animal(){
        std::cout<<"animal无参构造函数"<<std::endl;
    }
    ~animal(){
        std::cout<<"animal析构函数"<<std::endl;
    }
    
};
class hunman
{
public:
    //无参构造函数
    hunman(){
        std::cout<<"hunman无参构造函数"<<std::endl;
    }

    ~hunman(){
        std::cout<<"hunman析构函数"<<std::endl;
    }
    animal a;
};



int main(int argc, char const *argv[])
{
    hunman h;
    return 0;
}

静态成员

在类中使用static关键字所修饰的变量和函数叫做类的静态成员

  • 静态成员不属于任何对象
  • 该类的任何对象共用一分数据,共享一块内存
  • 该类的任何对象都可以修改静态成员的值,并且该值会被更新
  • 静态成员的初始化需要在类外,使用类名加作用域的方式初始化
cpp 复制代码
#include <iostream>

class hunman
{
public:
    //无参构造函数
    hunman(){
        // std::cout<<"hunman无参构造函数"<<std::endl;
    }

    ~hunman(){
        // std::cout<<"hunman析构函数"<<std::endl;
    }
    static void func(){
        std::cout<<"静态成员函数"<<std::endl;
    }
    static int a;
};
//类外初始化
int hunman::a = 1;
int main(int argc, char const *argv[])
{
    hunman h;
    std::cout<<h.a<<std::endl;
    //其他对象改变静态变量的值班
    hunman h2;
    h2.a = 10;

    //在用原对象访问时,值已经更新
    std::cout<<h.a<<std::endl;
    //通过类名加作用域访问静态变量和静态成员函数
    std::cout<<hunman::a<<std::endl;
    hunman::func();
    
    //该类的对象静态变量时统一快内存
    std::cout<<&h2.a<<std::endl;
    std::cout<<&hunman::a<<std::endl;

  
    return 0;
}

输出:

🔥🔥个人主页 🔥🔥

相关推荐
Chandler241 小时前
Go:方法
开发语言·c++·golang
whoarethenext4 小时前
qt的基本使用
开发语言·c++·后端·qt
虾球xz7 小时前
游戏引擎学习第220天
c++·学习·游戏引擎
愚润求学7 小时前
【C++】Stack && Queue && 仿函数
c++·stl·deque·queue·stack·priority queue
New个大鸭8 小时前
ATEngin开发记录_4_使用Premake5 自动化构建跨平台项目文件
c++·自动化·游戏引擎
空雲.9 小时前
牛客周赛88
数据结构·c++·算法
hi0_69 小时前
Linux 第三讲 --- 基础指令(三)
linux·运维·服务器·c++
wht65879 小时前
Linux--进程信号
linux·运维·服务器·开发语言·c++
邪神与厨二病10 小时前
2025蓝桥杯python A组题解
数据结构·c++·python·算法·蓝桥杯·单调栈·反悔贪心
隐世111 小时前
list容器
开发语言·c++