(黑马C++)L03 对象的构造和析构

对象的初始化和清理

构造函数和析构函数会被编译器自动调用,完成对象初始化和对象清理工作。即使自己不提供初始化操作和清理操作,编译器也会增加默认的操作,所以编写类就应该顺便提供初始化函数。

构造函数和析构函数必须写在public下才可以调用到。

1)构造函数和析构函数的概念

构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无需手动调用;析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。

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

构造函数与类名相同,没有返回值,不写void,可以发生重载(可以有参数),由编译器自动调用而不是手动,而且只会调用一次。

析构函数与类名相同,但类名前面需要加"~",没有返回值,不写void,不可以有参数(不能发生重载)。

class Person{
public:
    Person(){
        cout << "构造函数调用" << endl; //自动调用
    }

    ~Person(){
        cout << "构造函数调用" << endl; //自动调用,在函数结束后
    }

}

3)构造函数的分类及调用

构造函数按照参数进行分类,分为无参构造函数(默认构造函数)和有参构造函数;

class Person{
public:
    //默认构造函数调用时不要加小括号
    //加上括号编译器认为是函数声明,不会调用构造和析构
    Person() {
        cout << "默认构造函数" << endl;
    }
    
    Person(int a) {
        cout << "有参构造函数" << endl;
    }
};

按照类型进行分类,可分为普通构造函数和拷贝构造函数。

class Person{
public:
    //拷贝构造函数
    Person(const Person& p) {
        cout << "拷贝构造函数" << endl;
    }
    
    Person(int a) {
        cout << "有参构造函数" << endl;
    }
};

构造函数的调用方式分为括号法调用、显示法调用以及隐式类型转换。

使用括号法调用时,默认构造函数不要加(),加括号编译器会认为是函数声明;

Person(100)叫做匿名对象,匿名对象特定,如果编译器发现对象是匿名的,那么在这行代码结束就释放这个对象;

不能用拷贝构造函数初始化匿名对象,如果写成Person(p5)编译器会认为写的是Person p5,按照对象的声明处理,写成右值才会按拷贝构造进行处理。

void test01(){
    //括号法调用
    Person p1(1); //有参
    p1.m_age = 10;
    Person p2(p1); //拷贝

    Person p3; //默认构造函数,不要加(),加括号编译器会认为是函数声明
    
    //显示法调用
    Person p4 = Person(100); //有参函数调用
    Person p5 = Person(p4); //拷贝函数调用
    Person(100); //叫做匿名对象

    //隐式类型转换
    Person p = 100; //会使用隐式类型转换,转换成Person p = Person(100);
    Person p6 = p5; //会转换成拷贝函数
    
}

4)拷贝构造函数调用时机

1.用已经创建好的对象来初始化新的对象

void test01(){
    Person p1;
    p1.m_age = 10;
    Person p2(p1);
}

2.以值传递的方式给函数参数传值

//值传递会调用拷贝构造,引用传递不会调用拷贝构造
void doWork(Person p1) { //Person p1 = Person(p)
    
}

void test02() {
    Person p;
    p.m_age = 10;
    
    doWork(p);
}

3.以值的方式返回局部对象

Person doWork2(){
    Person p1;
    return p1;
}

void test03() {
    Person p = doWork2();
}

5)构造函数的调用规则

系统默认会给一个类提供三个函数:默认构造,拷贝构造,析构函数。

1.当提供了有参构造函数,系统就不会再提供默认构造函数,但是系统还会提供默认的拷贝构造函数;

2.当提供了拷贝构造,系统就不会提供其他构造了。

6)深拷贝和浅拷贝

有参构造函数就是为了初始化属性。

class Person {
public:
    Person() {}

    //初始化属性
    Person(char * name, int age) {
        m_Name = (char *)malloc(strlen(name) + 1);
        strcpy(m_Name, name);

        m_age = age;
    }

    //拷贝函数系统会默认提供,并且是简单的值拷贝
    

    char * m_Name;
    int m_age;
};

void test01() {
    Person p1("老王", 10);
    Person p2(p1); //调用了拷贝构造
}

当上述代码加上拷贝函数后,无法运行成功,因为当p1调用析构函数之后指针指向的地址里的内容已经被释放,当p2再调用析构时,m_Name也不为空,进行释地址内容时会发生异常。

//析构函数
    ~Person() {
        cout << "析构函数调用" <<endl;
        if(m_Name != NULL) { //m_Name是一个指针,需要释放
            free(m_Name);
            m_Name = NULL;
        }
    }

上述拷贝方式叫做浅拷贝,浅拷贝直接将p1的地址拷贝给p2,导致析构时释放堆区空间两次发生异常,解决方法是使用深拷贝。深拷贝不会直接拷贝地址,而是开辟一块新的空间,此时都调用析构函数时不会再发生错误。

class Person {
public:
    Person() {}

    //初始化属性
    Person(char * name, int age) {
        m_Name = (char *)malloc(strlen(name) + 1);
        strcpy(m_Name, name);

        m_age = age;
    }

    //拷贝函数系统会默认提供,并且是简单的值拷贝
    //深拷贝写法
    Person(const Person&p) {
        m_age = p.m_age;
        m_Name = (char *)malloc(strlen(p.m_Name) + 1);
        strcpy(m_Name, p.m_Name);
    }

    //析构函数
    ~Person() {
        cout << "析构函数调用" <<endl;
        if(m_Name != NULL) { //m_Name是一个指针,需要释放
            free(m_Name);
            m_Name = NULL;
        }
    }

    char * m_Name;
    int m_age;
};

void test01() {
    Person p1("老王", 10);
    Person p2(p1); //调用了拷贝构造
}

7)初始化列表

数据的初始化可以使用有参构造函数以及初始化列表。

利用构造函数初始化数据:

class Person {
public:
    //有参构造函数进行数据初始化
    Person(int a, int b, int c) {
        ma = a;
        mb = b;
        mc = c;
    }

    int ma;
    int mb;
    int mc;
};

void test() {
    Person p1(10, 20, 30);
}

 利用初始化列表初始化数据:构造函数后面 + : 属性(参数),属性(参数)...

class Person {
public:
    //有参构造函数进行数据初始化
    Person(int a, int b, int c) : ma(a), mb(b), mc(c) {}

    int ma;
    int mb;
    int mc;
};

void test() {
    Person p1(10, 20, 30);
}

8)类对象作为类成员的实例

class Phone {
public:
    Phone(){
        cout << "手机的默认构造函数调用" << endl;
    }
        
    Phone(string name){
        Phone_Name = name;
    }

    ~Phone(){
        cout << "手机的析构函数调用" << endl;
    }

    string Phone_Name;
};


class Game {
public:
    Game(){
        cout << "游戏的默认构造函数调用" << endl;
    }
    
    Game(string name){
        Game_Name = name;
    }
 
    ~Game(){
        cout << "游戏的析构函数调用" << endl;
    }

    string Game_Name;
};


class Person{
public:
    
    Person(){
        cout << "Person的默认构造函数调用" << endl;
    }
    
    Person(string name, string phoneName, string gameName){
        m_name = name;
        m_phone = phoneName;
        m_game = gameName;
    }
 
    ~Person(){
        cout << "Person的析构函数调用" << endl;
    }
    
    string m_name;
    Phone  m_phone;
    Game m_game;
};

void test() {
    Person p("老王", "华为", "斗地主");
}

类对象作为类成员的时候,构造顺序先将类对象一一构造,然后构造自己,析构的顺序相反。

9)explicit关键字作用

使用了explicit关键字就不能用隐式类型转换来构造对象,explicit用来防止隐式类型转换。

10)动态对象创建

C语言提供了动态内存分配函数malloc和free,用于从堆中分配和释放存储单元,然而这些函数在C++中不能很好的运行,因为它不能很好完成对象的初始化工作。

1.使用malloc存在的问题:

程序员必须确定对象的长度;malloc返回一个void*指针,C++不允许将void*赋值给其他指针,必须强制类型转换;malloc可能申请内存失败,所以必须判断返回值确保内存分配成功;用户在使用对象之前必须对他初始化,构造函数不能显示调用初始化,用户可能会忘记调用。

2.可以使用new来开辟空间,new的对象会默认调用构造函数,使用方法如下:

Person * p2 = new Person;

3.new和malloc的区别:

new在堆区开辟空间,所有new出来的对象都会返回该类型的指针;

malloc不会调用构造,new会调用构造;

new是一个运算符,malloc是一个函数。

释放堆区的空间用delete,delete也是一个运算符,要配合new使用;malloc配合free使用。

void * p = new Person;

void*接收new的对象会造成无法释放,所以要避免这种写法。

4.通过new开辟数组

void test(){
    Person * array = new Person[10]; //会调用十次默认构造函数
    
    delete [] array; //释放数组对象必须要delete[]
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值