c++类和对象进阶一【含构造函数,析构函数,拷贝构造函数】



前言

前面我们学习了类的实例化,还对类实例化有疑问的同志可以查看前面的文章
类和对象初阶


提示:以下是本篇文章正文内容

一.类的6个默认成员函数:

1.1 引入:

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

在这里插入图片描述

2.1 构造函数:

2.1.1 构造函数的引入:

在C语言中,初始化一个结构体或变量通常需要手动编写代码,甚至有时甚至忘记初始化手动写或者不写这可能会导致代码重复、出错的可能性增加, 特别是在面对复杂的数据结构时。c++的构造函数,可以在对象被创建时自动调用,用于初始化对象的状态。 构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.1.2 构造函数的用法:

1. 函数名与类名相同。
2. 无返回值。
3. 对象实例化时编译器自动调用对应的构造函数。

作用:确保类的对象在被创建时能够进行适当的初始化,使得对象的状态是可控的、合理的,从而提高程序的健壮性和可维护性。
示例:

class MyClass {
public:
    // 构造函数重载
    MyClass() {
        // 默认构造函数
    }
    
};

2.1.3 构造函数的注意事项:

2.1.3.1 构造函数可以重载:

重载的定义可以参考c++重载详解

当构造函数重载了多个,想要调用具体构造函数的那个如下面的用例

class MyClass {
public:
    // 构造函数重载
    MyClass() {
        // 默认构造函数
    }
    
    MyClass(int val) {
        // 带参数的构造函数
    }
    
    MyClass(int val1, int val2) {
        // 可以有多个参数
    }
};

在这里插入图片描述

在这里插入图片描述

“MyClass s4(void)”: 未调用原型函数(是否是有意用变量定义的?) MyClass s4();像是一个函数的声明,此时本来是想调用MyClass(),但是编译器不知道是声明还是调用 。所以想调用无参的构造,是直接Myclass s1;这样就行

在这里插入图片描述

2.1.3.2 编译器生成的构造函数:不可靠

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

验证:
#include<iostream>
using namespace std;
class MyClass {

public:
    void print() {
        cout << "_val1" << _val1 << endl;
        cout << "_val2" << _val2 << endl;
    }
    int _val1;
    int _val2;
};
int main() {
    MyClass s1;
    s1.print();
}

在这里插入图片描述

此时打印的随机值,怎么能说是调用了构造函数,我们接着看下面的例子:

#include<iostream>
using namespace std;
class Stack1 {
public:
    Stack1(int max=4) {
        capacity = max;
        size = 0;
        arr = (int*)malloc(capacity * sizeof(int));
    }
    int* arr;
    int capacity;
    int size;
};
class MyClass {
public:
    void print() {
        cout << "_val1:" << _val1 << endl;
        cout << "_val2:" << _val2 << endl;
        cout <<"a1.capacity:"<< a1.capacity << endl;
        cout <<"a1.size:"<< a1.size << endl;
    }
    int _val1;
    int _val2;
    Stack1 a1;
};
int main() {
    MyClass s1;
    s1.print();
}

此处我们是引入了类Stack1并手动的完成它的构造函数 Stack1(int max=4),这时我们来观察:

在这里插入图片描述

此时的_val1和_val2做了初始化,但是前面没有。

我们再把_val1和_val2变量去掉再次观察:

在这里插入图片描述

发现a1还是做了初始化;

这是因为:内置类型(C++ 的内置类型指的是语言本身提供的基本数据类型,这些类型在 C++ 标准中被定义,并且通常在编译器中直接支持什么int,char…)编译器一般是不做处理(但是有的编译器会做处理,看编译器的具体操作,但是vs编译器处理的不好根本不知道初不初始化),自定义类型(在 C++ 中,你可以通过使用类 (class) 或结构体 (struct) 来定义自定义类型。)会去调用它的默认构造

如果我们的Stack1类的构造函数不手动写,用编译自己写的,由于 Stack1类的成员变量是内置类型,不显示的写构造函数,编译器生成的默认构造是对内置类型不做处理看到是随机值

在这里插入图片描述

结论:

1.一般情况:有内置类型的成员,就需要自己写构造,不能用编译器自己生成的
2.对于全部是自定义类型,就可以用编译器生成的默认构造:本质是调用默认自定义类型的默认构造,自定义类型的成员是内置类型,自定义类型就得自己完成构造函数

补丁:

C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

#include <iostream>
using namespace std;

class MyClass {
public:
    int x = 5;  // 内置类型成员变量在声明时给予默认值
    double y = 3.14;
    
    void print() {
        cout << "x: " << x << ", y: " << y << endl;
    }
};

int main() {
    MyClass obj;
    obj.print();

    return 0;
}

在这里插入图片描述

新结论:

一般的情况下,构造函数都需要我们自己写:
不用写的情况:1.内置类型成员函数都有却省值,且初始化符合我们的要求。
2.全是自定义类型,且这些类型都定义了默认构造

2.1.3.3 默认构造函数:

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

我们没写编译器默认生成的构造函数只有一个可以理解,对于无参构造函数、全缺省构造函数为什么只有一个看下面的实例:

#include <iostream>
using namespace std;

class MyClass {
public:
    // 构造函数重载
    MyClass() {
        // 默认构造函数
        _val1 = 1;
    }

    MyClass(int val=1) {
        // 带参数的构造函数
        _val1 = val;
    }
    int _val1;
   
};
int main() {
    MyClass s1;
}

在这里插入图片描述
在这里插入图片描述

此时如果是无参初始化,调Myclass()说的过去,Myclass(int val=1)因为给了缺省值也说得过去,就会出现调用歧义

3.1 析构函数:

3.1.1 析构函数的引入:

析构函数(Destructor)是在对象生命周期结束时被自动调用的特殊成员函数,用于释放对象占用的资源、执行清理工作等。 防止如c语言一般要手动调用,甚至有时候忘记调用。析构函数的引入是为了确保在对象不再需要时,可以正确地释放资源,防止资源泄漏和内存泄漏等问题。

3.1.2 析构函数的用法:

1. 析构函数名是在类名前加上字符 ~。
2. 无参数无返回值类型。
3. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

#include<iostream>
using namespace std;
class Stack1 {
public:
    Stack1(int max = 4) {
        capacity = max;
        size = 0;
        arr = (int*)malloc(capacity * sizeof(int));
    }
    int* arr;
    int capacity;
    int size;
};
class MyClass {
public:
    MyClass() {
       _val1 = 1;
       _val2 = 2;
    }
    ~MyClass() {//析构函数
        cout <<"~MyClass()"<< endl;
    }
    int _val1;
    int _val2;
    Stack1 a1;
};
int main() {
    MyClass s1;
    
}

在这里插入图片描述

我们可以看到对象生命周期结束自动调用。

3.1.3 析构函数的注意事项:

3.1.3.1 析构函数不能重载:

一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
与其他成员函数不同,析构函数的名称由编译器固定为波浪号(~)后跟类名,并且不接受任何参数,也没有返回类型 , 所以不支持重载。

3.1.3.2 编译器生成的析构函数:
验证:
#include<iostream>
using namespace std;
class Stack1 {
public:
    Stack1(int max = 4) {
        capacity = max;
        size = 0;
        arr = (int*)malloc(capacity * sizeof(int));
    }
    ~Stack1() {
        cout << "~Stack1" << endl;
        free(arr); // 释放动态分配的内存
        capacity = size = 0;

    }
    int* arr;
    int capacity;
    int size;

};
class MyClass {
public:
    MyClass() {
       _val1 = 1;
       _val2 = 2;
    }

    int _val1;
    int _val2;
    Stack1 a1;
};
int main() {
    MyClass s1;
   
}

在这里插入图片描述

此时是我们没写Myclass 的析构函数但是却调用了Stack1的析构函数原因:编译器生成的析构函数对于内置类型不做处理,自定义类型调用它的默认构造 。由于编译器做的处理,不好观察。如果此时Stack1的析构函数不自己写的化,调用编译器写的析构函数,此时会出现内存泄漏,因为Stack1的成员是内置类型,内置类型不做处理

结论:

1.一般情况涉及动态开辟内存,就得手动写析构函数来释放开辟的资源
2.没有动态申请资源的,不需要显示的写析构
3.需要释放资源的全是自定义类型,就不需要显示的写析构

4.1 拷贝构造函数:

4.1.1 拷贝构造的引入:

拷贝构造函数是C++中的特殊成员函数之一,还是特殊的构造函数,用于在创建对象时,通过已有对象创建一个新对象,并且新对象与原对象具有相同的值。

4.1.2 拷贝构造的用法:

1. 拷贝构造函数是构造函数的一个重载形式。
2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

#include <iostream>

class MyClass {
public:
    // 构造函数
    MyClass(int val) {
    	_val=val;
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        std::cout << "拷贝构造函数被调用" << std::endl;
        _val = other._val; // 简单的拷贝值
    }

    int getVal() const { return _val; }

private:
    int _val;
};

int main() {
    MyClass obj1(10); // 创建一个对象obj1

    MyClass obj2(obj1); // 使用obj1来初始化obj2,这里会调用拷贝构造函数

    std::cout << "obj1的值为: " << obj1.getVal() << std::endl;
    std::cout << "obj2的值为: " << obj2.getVal() << std::endl;

    return 0;
}

4.1.3 解释为什么是引用:

c++规定自定义类型传值调用时会调用拷贝构造来实现拷贝

传值调用与传引用调用的区别:可以参考传引用与传值调用的区别

#include <iostream>
using namespace std;

class MyClass {
public:
    // 构造函数
    MyClass(int val) {
        _val = val;
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        std::cout << "拷贝构造函数被调用" << std::endl;
        _val = other._val; // 简单的拷贝值
    }

    int getVal() const { return _val; }

    int _val;
};
void test1(MyClass other) {
    cout << "测试函数" << endl;
    cout << "拷贝副本的值变化前:" << other._val << endl;
    other._val *= 2;
    cout <<"拷贝副本的值变化后:"<< other._val << endl;

}
int main() {
    MyClass obj1(10); // 创建一个对象obj1

    test1(obj1);

    cout << "当前对象的值:" << obj1._val << endl;


    return 0;
}

在这里插入图片描述

我们可以看到,用test1函数传MyClass对象obj1时,调用了拷贝构造,在test1函数里面的other就是因为调用了拷贝构造生成的与obj1相同的副本我们对副本做的操作不会影响原来的本体

c++规定自定义类型传值调用时会调用拷贝构造来实现拷贝那跟我拷贝构造函数的参数必须是类类型对象的引用有什么关系?

在这里插入图片描述

我传引用就解决了这个问题,因为是值的别名,是对值本身进行操作,不会有无穷递归的问题

4.1.4 解释为什么要加const:

拷贝构造函数参数通常会声明为 const 引用,这是因为拷贝构造函数的主要目的是复制另一个对象的值,而不是修改它。
1. 防止修改源对象:通过将参数声明为 const,可以确保在拷贝过程中不会意外修改原始对象的状态。这符合拷贝构造函数的语义,即复制对象的值而不影响原始对象。
2. 允许传递临时对象:如果参数不是 const 引用,那么无法将临时对象(即匿名对象)传递给拷贝构造函数,因为临时对象无法绑定到非常量引用。通过使用 const 引用,可以接受临时对象作为参数,提高了函数的灵活性。
3. 保证对象的常量性:如果拷贝构造函数参数不是 const,那么无法使用它来拷贝常量对象,因为常量对象只能传递给 const 引用参数的函数。因此,通过声明参数为 const,可以确保拷贝构造函数可以用于拷贝常量对象。

4.1.5 编译器生成的拷贝构造:只完成浅拷贝

若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

#include <iostream>
using namespace std;

class MyClass {
public:
    // 构造函数
    MyClass(int val) {
        _val = val;
    }

    int getVal() const { return _val; }

    int _val;
};

int main() {
    MyClass obj1(10); // 创建一个对象obj1
    cout << obj1._val << endl;
    MyClass obj2(obj1);
    cout << obj2._val << endl;


    return 0;
}

在这里插入图片描述

这种用编译器的没问题,相当于把obj1._val的值拷贝给obj2.val,是一种值拷贝没问题,对于下面的就不合适了

#include<iostream>
using namespace std;
class Stack1 {
public:
    Stack1(int max = 4) {
        capacity = max;
        size = 0;
        arr = (int*)malloc(capacity * sizeof(int));
    }
    ~Stack1() {
        cout << "~Stack1" << endl;
        free(arr); // 释放动态分配的内存
        capacity = size = 0;

    }
    int* arr;
    int capacity;
    int size;

};
int main() {
    Stack1 s1(10);
    Stack1 s2(s1);
    return 0;
}

在这里插入图片描述

它指出了一个在堆(heap)上的内存分配问题,即程序尝试使用了无效的堆指针。这可能是由于以下原因之一引起的:
使用了已经被释放的内存指针。内存越界或者缓冲区溢出导致了堆指针被破坏。在释放内存后尝试继续使用该内存。

我们来调试看看

在这里插入图片描述

在这里插入图片描述

此时的s1.arr与s2.arr指向同一块地址这就出大问题了,main函数的生命周期结束,s1和s2的生命周期就结束了,此时他们两个都要自动调用析构函数,那同一块内存被释放两次,就会出现断言错误,由于是同一块空间,对一方增加删除都会影响另一方

#include<iostream>
using namespace std;
class Stack1 {
public:
    Stack1(int max = 4) {
        capacity = max;
        size = 0;
        arr = (int*)malloc(capacity * sizeof(int));
    }
    ~Stack1() {
        cout << "~Stack1" << endl;
        free(arr); // 释放动态分配的内存
        capacity = size = 0;

    }
    Stack1(const Stack1& other) {
        capacity = other.capacity;
        size = other.size;
        arr = (int*)malloc(capacity * sizeof(int)); // 分配新的内存空间

        // 将other对象中的元素复制到新的内存空间中
        for (int i = 0; i < size; ++i) {
            arr[i] = other.arr[i];
        }
    }
    int* arr;
    int capacity;
    int size;

};
int main() {
    Stack1 s1(10);
    Stack1 s2(s1);
    return 0;
}

在这里插入图片描述

这种就没问题了

结论:

如果类中包含指针成员变量,并且在拷贝构造函数的默认行为下(即浅拷贝)不能满足你的需求,你需要手动编写拷贝构造函数来实现深拷贝。
如果类管理了某种资源(如动态内存、文件句柄等),在进行拷贝时需要正确地管理这些资源。默认的拷贝构造函数执行的是浅拷贝,这可能导致资源的多重释放或者不正确释放。因此,你需要手动编写拷贝构造函数来确保资源被正确地管理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值