C++学习笔记day43-----C++98-构造函数的初始化表,this指针,常函数,析构函数

时间:2022-08-23 19:44:05

构造函数的初始化表:
我们用以下例子来讲解一个对象在初始化的时候,是经过怎样的顺序:

#include <iostream>
using namespace std;
class A{
public:  
    int m_data;
    A(int data){
        cout << "A的构造函数" << endl;
        m_data = data;
    }
};
class B{
public:
    A m_a;
    //在创建B类型对象时,要先初始化里面包含的成员子对象(m_a),
    //然后执行B的构造函数代码
    B(void):m_a(1234){
        cout << "B的构造函数" << endl;
    }
};
int main(void){
    B b;
    cout << b.m_a.m_data << endl;
    return 0;
}

在主函数中定义了一个B类型的对象b。编译器会按照B类中成员变量的声明顺序,对它们进行初始化,现在B类中就只有一个成员子对象,所以先通过构造函数的初始化表将成员子对象初始化(如果不使用初始化表的方式初始化成员子对象,那么编译器就会寻找A类有没有无参构造函数,如果A类没有,那么直接编译报错;如果A类有,则调用无参构造函数),然后在调用B类的构造函数。构造函数调用结束后,定义对象的过程结束。
明确一下通过初始化表和通过构造函数函数体对成员变量初始化的区别。
当使用构造函数的函数体初始化成员变量的时候,是先创建好各个成员的内存(如果成员变量是一个类类型的,则需要调用其对应的默认构造函数),然后对内存进行赋值。整个过程类似于如下语句:

int num; num = 100;

当使用构造函数的初始化表对成员变量初始化的时候,是在分配内存的同时对内存初始化,整个过程类似于如下语句:

int num = 100;

this指针:
要了解this指针是什么,必须要知道对象调用成员函数的过程。用以下代码为例解释:

#include <iostream>
using namespace std;
class User{
public:
    User(const string &m_name,int m_age){
            this -> m_name = m_name;
            this -> m_age = m_age;
            cout << "构造函数" << this << endl;
        }
    void print(void){
        cout << m_name << " , " << m_age << endl;
    }
    /* void print(User *this){ * cout << this -> m_name << " , " * << this -> m_age << endl; * } **/
private:
    string m_name;
    int m_age;
};
int main(void){
    User u1("张三",25);
    cout << "&u1:" << &u1 << endl;
    User u2("李四",26);
    cout << "&u2:" << &u2 << endl;
    u1.print();
    u2.print();
    return 0;
}

在上述例子中,对象u1和u2都调用了print()函数,那么print()函数是怎么知道哪个对象调用了我,我应该输出哪个对象的成员变量的值?
当print()函数进行编译时,编译器会做类似以下的动作:
1、它将void print(void);解释成 void print(User *this),
2、然后当u1对象调用print()函数的时候,变成这样 User::print(&u1);
经过上述两个步骤,print就知道是要调用哪个对象的数据了。
那么this指针的本质,就是编译器为成员函数增加的一个类类型的形式参数,用于接受调用成员函数的对象的地址。

常函数:

#include <iostream>
using namespace std;
class A{
public:
    A(int data = 0)
        :m_data(data){}
    //不能通过被const修饰的this指针修改成员变量
    void show(void) const {
        cout <<  ++m_data << endl;
    }
private:
    //即使被const修饰的this的指针也能修改它
    mutable int m_data;
};
int main(void){
    A a(123);
    a.show();
    return 0;
}

成员函数show的形参列表和左花括号之间加了一个const修饰符,这个const是用来修饰this指针的。一切妄图通过 const A* this对成员变量修改的动作,都会导致编译器报错。但是!,如果成员变量用mutable修饰过,那么const将不起作用,还是可以修改。
只要理解了this的本质,然后将它当作一个普通的指针来对待,结合const去分析问题,就不会出错。

析构函数:

#include <iostream>
using namespace std;
class A{
public:
    A(void){
        cout << "A的构造函数" << endl;
    }
    ~A(void){
        cout << "A的析构函数" << endl;
    }
};
class B{
public:
    B(void){
        cout << "B的构造函数" << endl;
    }
    ~B(void){
        cout << "B的析构函数" << endl;
    }
private:
    A m_a;
};
int main(void){
    {
        B b;
    }
    return 0;
}

析构函数是在对象即将被释放的时候,被调用,有点类似进程的遗言函数的味道。它用来处理对象回收前要处理的东西。
比方所,如果我在构造函数中,使用new得到了一块堆的空间,那么当对象即将释放的时候,就需要把这块内存释放掉,否则会造成内存泄漏。这个时候就需要析构函数来完成对堆空间的释放。
析构函数调用的顺序和调用构造函数的顺序是相反的,如果创建一个对象调用了多个构造函数,那么第一个调用的构造函数,最后调用析构函数。
—————————————————————————————————————————————————————————————–

十四、构造函数和初始化表
……
5 初始化表
1) 语法:
class 类名{
类名(构造形参表):成员变量1(初值),…{
//函数体
}
};
2) 在介绍两者区别之前先了解一下两种方式的流程,不使用初始化表的构造函数,就像是这样,int a; a = 100;分两步走,先定义,后初始化。使用了初始化表的构造函数,就像是 int a = 100;定义的同时,初始化。理解了这一点之后,下面的区别就可以很好的理解。

还有,在类的开头,是对成员变量的声明,只有在构造函数那里,才是对变量的定义。这也是为什么有的人,把成员变量的生命放在构造函数之下。

多数情况下使用初始化表对成员变量初始化,和在构造函数体中对成员变量赋初值没有太大的区别,两种形式可以任选。在有些特殊的场景必须使用初始化表的形式。

-->如果有类类型的成员变量,而该类又没有无参构造函数,则必须使用初始化表来初始化该成员。(原本调用一个类的无参构造函数的时候,编译器会调用其子对象的无参构造对象。现在可以通过初始化列表,让编译器选择调用子对象的其他构造函数)

–>如果类中包含const类型,或者引用类型成员变量,必须在初始化表中初始化。

/在类中,成员变量的定义顺序是根据声明顺序来的/
/不要使用一个成员变量去初始化另一个成员变量/

include

using namespace std;
class Dummy{
    public:
        Dummy(const char* str)
            //:m_str(str),m_len(m_str.length()){}
            :m_str(str?str:""),m_len(strlen(str?str:"")){}
        string m_str;
        size_t m_len;
};

int main(void){
Dummy d(“hello world!”);
cout << d.m_str << ” , ” << d.m_len << endl;
return 0;
}

练习:使用初始化表改写电子时钟类,为电子时钟类增加计时器功能,如果使用系统时间作为构造实参创建对象,表现为时钟功能:如果使用无参的方式创建对象,则使用初始化表将时间初始化为“00:00:00”,则表现为计时器功能。

十五、this指针和常成员函数(常函数)
1) this是C++中的关键字,其本质就是类中成员函数或构造函数一个隐藏的形参变量,类型是当前类类型的指针。

2) 在类中的构造函数或者成员函数去访问其他成员,实际上都是通过this指针来完成的。

3) 对于普通的成员函数,this指针就是只想调用该函数的对象;对于构造函数,this指向的就是正在被创建的对象
eg:
class A{
public:
void print(int data)
:m_data(data){
cout << m_data << endl;
}
private:
int m_data;
}/*
经过C++编译器编译之后:
void print(A *this){
cout << this -> m_data << endl;
}
*/
A a1(100);
A a2(200);
a1.print();//100,A::print(&a1)
a2.print();//200,A::print(&a2)

4) 需要显式的使用this指针的场景
–>区分作用域
class User{
public:
User(const string &m_name,int m_age){
this -> m_name = m_name;
this -> m_age = m_age;
cout << “构造函数” << this << endl;
}
private:
string m_name;
int m_age;
};

-->从成员函数返回调用对象自身
cout << a << b << c << endl;

-->从类的内部销毁对象自身

2 常成员函数
1) 在一个普通的成员函数参数表后面加const修饰,该函数就是常成员
class 类名{
返回类型 函数名(形参表) const {函数体}
};

2) 这个const修饰的是隐藏参数,this指针。只读this指针。
注:被mutable修饰的成员变量可以在常函数中被修改


3) 非const对象既可以调用常函数也可以掉用非 常函数,但是常对象只能调用常函数,不能调用非 常函数。
从本质上理解,在内存的层面考虑当前的访问方式的动作,是否超出了当前访问方式的权限。
void func(const int *p){…}
int main(void){
int num = 100;
int *p_num = #
func(p_num);
}
4) 函数名和形参表相同的成员函数,其常版本和非常版本可以构成重载关系;常对象调用常版本,非 常对象掉用的是非 常版本。
试一下一般函数const能否构成重载。
试过了,不行,仅当成员函数,才可以重载。

十六、析构函数
1 语法形式
class 类名{
~类名(void){
//主要负责清理对象创建时分配的动态资源

}

};
1) 函数名必须是”~类名”
2) 没有返回类型,也没有参数
3) 不能被重载,即一个类,只有一个析构函数、

2 从内存层面上理解对象的生命周期,析构函数的执行时期是在生命周期的最后。
当对象销毁时,该类析构函数自动被执行
1) 栈区对象当离开作用域,其析构函数被作用域种植的右花括号调用;
2) 堆区对象的析构函数被delete操作符调用

3 当一个类没有写明析构函数时,编译器会为该类自动添加一个缺省的析构函数:
1) 对基本类型的成员变量什么也不做
2) 对类类型的成员变量,调用该类的析构函数

4 对象创建和销毁的过程
1) 对象创建
–>分配内存
–>构造成员子对象(按照声明顺序)
–>执行构造函数代码

2) 对象的销毁
–>执行析构函数代码
–>析构成员子对象(按照声明的逆序)
–>释放内存