C++ 类和对象 对象特性 构造函数和析构函数 深拷贝和浅拷贝

C++中的面向对象来源于生活,每个对象也都会有初始设置以及 对象销毁前的清理数据的设置

1.构造函数和析构函数作用与意义

  1. 对象的初始化清理重要性:

  • 一个对象或者变量没有初始状态,对其使用后果是未知

  • 同样的,使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

  1. C++中利用构造函数完成对象的初始化,利用析构函数完成对象的清理。

构造函数析构函数会被编译器自动调用,程序员可以不写,但是编译器提供的构造函数和析构函数是空实现,也就是这个函数体是空的。但如果程序员写了,编译器会自动调用。

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。

  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

2.构造函数和析构函数的语法

构造函数的语法

  1. 类名() {};

  1. 构造函数,没有返回值也不写void;

  1. 函数名称与类名相同;

  1. 构造函数可以有参数,因此可以发生重载;也可以没有参数;

  1. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。

如果没有构造函数,编译器也会有写一个Person构造函数,只是这是一个空函数,即:Person() { }

析构函数的语法

  1. ~类名(){}

  1. 析构函数,没有返回值也不写void

  1. 函数名称与类名相同,在名称前加上符号 ~

  1. 析构函数不可以有参数,因此不可以发生重载

  1. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次

注意:析构函数前面有~;析构函数不能有参数,构造函数可以有参数

语法理解——main函数只运行test1

构造和析构函数都调用的原因:

首先,test1创建的对象p是一个局部变量,也就是局部对象,放在栈区。而栈区的数据,在test1执行完毕后,就会被释放。也就是说test1运行完后,这个对象才被释放。

然后,释放对象前,编译器会自动调用析构函数,并且只调用一次。也就是,看到的输出结果了。

构造和析构函数都是必须有的实现,如果程序员不提供,编译器会提供一个空实现的构造和析构

语法理解——main函数中只创建对象

没有调用析构函数的原因:

首先,main函数中创建局部变量p,这个p还没有被释放。

然后,程序创建完p之后,运行下一行”system("pause");“就中断了,也就是窗口的”请按任意键继续“。只有在这行代码运行完之后,p就会被释放,这时候才调用析构函数。

但是,再下一行”return 0;“,也就是关闭窗口,有可能我们就看不到析构函数的调用了。视力好的话,在关闭的一瞬间可以看到这个函数的调用。

3.构造函数的两种分类方式:

  1. ​按参数分为: 有参构造和无参构造(默认构造);

  1. ​按类型分为: 拷贝构造和普通构造(除了拷贝构造都是普通)

  1. 有参构造就是有参数的构造函数

  1. 拷贝构造把本体构造函数的的所有属性都拷贝

  1. 注意拷贝构造函数的写法,类名 (const 类名& 对象) { 拷贝属性 }

4.构造函数的三种调用方式:

有三种调用方式,分别是括号法、显示法、隐式转换法

  1. 括号法

test2运行:

首先是p1被创建,调用无参构造函数,即1;

然后,p2被创建,调用有参构造函数,即2;

接着,p3被创建,调用拷贝构造函数,即3。

这时p1被释放,调用析构,即4;

再p2释放,调用析构,即5;

最后,p3释放,调用析构,即6,结束。

拷贝的作用

p3的年龄也是18,是因为p3是直接拷贝p2的年龄的.

注意事项1:调用默认构造函数时,不要加()。否则编译器会认为是一个函数的声明,不会认为是创建对象的过程。

  1. ​显示法

注意事项2:Person1(18)是一个匿名对象,当前行执行结束后,系统会立即回收掉匿名对象。对”Person1 p2 = Person1(18)”这行代码而言,p2是Person1(18)这个匿名对象的名。可以看到,这个匿名对象创建完之后,还没等test2执行完,就立马被释放了。

注意事项3:不要利用拷贝构造函数来初始化匿名对象,否则编译器会认为 Person(p3) == Person p3;编译器会认为这是一个对象的声明,也就是重定义。

  1. ​隐式转换法

5.拷贝构造函数调用时机

C++中拷贝构造函数调用时机通常有三种情况:

  1. 使用一个已经创建完毕的对象来初始化一个新对象

前面的分类有提到这种方式,不赘诉。

  1. 值传递的方式给函数参数传值

首先,p使用默认构造函数创建对象;然后,通过doWork函数以值传递的方式(值传递就是临时拷贝一份数据)把test4中的p拷贝给doWork中的形参p,相当于是拷贝构造的用法。

另外,如果在doWork中,加上p.age=1000;也不会改变test4中p的年龄,形参不改变实参。

  1. 以值方式返回局部对象

2022版的VS返回值做了优化,没有拷贝。老师的版本有拷贝。其实就是值传递的方式,p值传递给doWork2,值传递(拷贝新对象)的时候会触发拷贝构造函数,而新拷贝的对象就是doWork2返回的值。

6.构造函数调用规则

  1. 默认三个函数

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

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

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

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

  1. 构造函数调用规则如下

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

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

  1. 属性值拷贝

自己写了定义了拷贝函数,会调用拷贝函数。首先创建默认构造函数,然后调用拷贝构造,把p1拷贝给p2

如果把拷贝函数注释掉,此时还是输出p2的年龄,C++中做了简单的值拷贝(所有属性做了赋值操作),但没有调用拷贝函数,只是把p1的年龄拷贝给p2,也就是“age = p.age;“。

  1. 调用规则1

写了有参构造时,系统不在提供默认构造

编译器会提供拷贝构造做值拷贝

  1. 调用规则2

如果程序员提供了拷贝构造,编译器不会提供默认和有参构造

调用的规则总结来说就是,3个函数的优先级是拷贝>有参>默认。有拷贝,不提供有参和默认;有有参,不提供默认,但提供拷贝。

7. ★深拷贝和浅拷贝

浅拷贝:值拷贝,p1的值赋值给p2

class Person {
public:
    Person() {
        cout << "默认构造函数" << endl;
    }
    Person(int age, int height) {
        cout << "有参构造函数" << endl;
        m_age = age;
        m_height = new int(height);
    } 

    //析构函数
    ~Person() {
        cout << "析构函数!" << endl;
        if (m_height != NULL)
        {
            delete m_height;
        }
    }
public:
    int m_age;
    int* m_height;
};

void test01()
{
    Person p1(18, 180);
    Person p2(p1);
    cout << "p1的年龄: " << p1.m_age << " 身高: " << *p1.m_height << endl;
    cout << "p2的年龄: " << p2.m_age << " 身高: " << *p2.m_height << endl;
}

int main() {

    test01();

    system("pause");

    return 0;
}

指针在堆区中,先进后出。调用析构时,先释放p2,所以堆区0x0011这块被释放了。等到p1释放时,这块已经没有了,所以程序浅拷贝时,指针这块会导致程序崩掉。也就是说,浅拷贝的话,堆区的内存会重复释放。

深拷贝

深拷贝就是在堆区重新申请空间,进行拷贝操作,解决浅拷贝重复释放堆区内存的问题。自己实现一个拷贝构造函数,在堆区新开一块内存来存放相同的数据,这样释放时不会干涉。也就是在堆区新开一块内存,存放160,p2存放的是这个新内存的地址。

//添加拷贝构造函数    
Person(const Person& p) {
    cout << "拷贝构造函数" << endl;
    //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
    //m_age = p.m_age;//编译器默认实现的就是这行代码
    m_height = new int(*p.m_height);
    }

总结:

  1. 浅拷贝就是编译器提供的值拷贝,”m_age = p.m_age;“这种等号赋值操作。

  1. 浅拷贝会导致堆区数据重复释放的问题。

  1. 深拷贝就是自己实现拷贝构造函数,重新在堆区创建一块内存,实现拷贝操作,”m_height = new int(*p.m_height);“。

  1. 使用析构函数的场景:如果在堆区开辟了内存,那么需要通过析构代码将堆区的内存释放干净。

黑马老师

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值