c++—封装:构造函数、析构函数、成员操作

1. 封装的主要目的是解决代码的维护性问题,经过封装的函数代码独立性高;

2. 封装的演变历史,以栈为例子介绍:

        ①成员(top、data[ ])都在main函数里,动作方法(push、pop)等也在main函数里(尼泊尔电线);

        ②成员(top、data[ ])在结构体体struct里面,动作方法(push、pop)等还在main函数里面(相当于未完成装修的房间,虽然电线整体布置,但是裸露的);

        ③成员(top、data[ ])和动作方法(push、pop)都在结构体(类)里面,动作方法(push、pop)对外只提供接口,达到既知其接口,又可以访问其数据;(C语言重在函数,c++重在类)

3. 构造函数的作用及特点

        ①无函数返回值,存在默认和自定义等多种情况(引导面试官);

        ②函数名与类名相同;

        ③自动调用:在通过类实例化对象时,构造函数会自动调用;

        ④可以重载;

4. 构造函数的种类及优缺点

(1)默认无参构造函数 VS 自定义无参构造函数

        当类中无任何其他构造函数时,系统会默认生成;

        若有其他构造函数,则该默认无参构造函数不会生成;其中自定义无参构造函数,可以在函数体里面初始化自定义初始化成员变量的值;注意若自定义的有参构造函数只有一个形参时,即只对一个成员变量进行初始化,即形成了类型转换构造函数,容易产生歧义,慎用!

        下例中同样含有int类型(或char*)等类型与类类型(Student)之间转换的方法;

#include <iostream>
#include <string.h>

using namespace std;

class Student
{
public:
    //Student() = delete;  //使用delete可以禁止系统自动生成默认构造函数;
    Student() = default; //使用default可以显式的让系统自动生成默认构造函数;
    Student(int num, char *name, int age)
    {
        m_num = num;
        m_age = age;
        int len = strlen(name)+1;
        m_name = new char[len];
        strcpy(m_name,name);
    }
    explicit Student(int num)    //该构造函数只有一个参数,称为类型转换构造函数;int型→类类型Student;本例中前面加了关键字explicit,禁止系统隐式转换,所以s3无效,会报错;
    {
        m_num = num;
        m_age = 0;
        m_name = nullptr;
    }

    operator int()    //重载了int类型,即可以将类类型转换为int类型;
    {
        return m_num;
    }


private:
    int m_num;
    char * m_name;
    int age;
};

int main(int argc, char **argv)
{
    Student s1;  //会报错,因为此时已经有自定义有参构造函数,默认无参构造函数失效,系统不生成;
    Student s2(1,"zhangsan",12);
    Student s3 = 1;  //调用类型转换构造函数,慎用!容易产生歧义;这里系统内部发生隐式转换,将int型转换为Student(类类型);应习惯使用s4样例;因为有explicit,这里会报错;
    Student s4(1);

    int num1 = static_cast<int>(s4);  //利用重载operator int()将类类型装换为int类型;
    int num2 = s4.operator int();     //方式二;

    return 0;
}

(2)默认拷贝构造函数 VS 自定义拷贝构造函数

        利用已有对象初始化一个新的对象,两个成员之间的变量对等赋值,默认情况下是浅拷贝,但是存在“多次释放同一个空间的问题,即free() : double free”;

         自定义拷贝构造函数是深拷贝,参与拷贝的两个对象各自的指针成员变量指向自己独立的堆空间;

 引申1:面试题:解释浅拷贝与深拷贝?

        ①类对象拷贝是指利用已有对象初始化一个新的对象,若无自己定义,则默认浅拷贝,系统默认的拷贝构造函数和等号运算符重载(operator=)都是按照浅拷贝的方式,即两个成员之间的变量对等赋值(包含指针变量),但是当对象内有指针成员时,会导致出现“多次释放同一个空间的问题,即free() : double free”的段错误;

        ②深拷贝就是自己定义,新的类对象中的指针成员是新生成的独立的空间(new),其大小等同于待拷贝的对象指针成员所指向的空间大小,其他变量则是对等赋值,解决了浅拷贝中的段错误;并且可以利用下面的移动对象构造函数进行优化;

引申2:面试题:拷贝函数的调用时机?

        ①用已有的对象初始化新的对象;

        ②对象作为形参时;

        ③对象作为函数返回值时;

(3)赋值运算符重载函数

        默认运算符重载函数是浅拷贝,当优化为深拷贝后,其主要不足是当大量的接收函数创建的临时类对象时,需要不断地生成临时对象→拷贝对象→释放临时对象;整个过程耗时较长,且降低了程序性能;

        优化思路为直接将新建的临时对象的空间交给左值,不用经历“新建→拷贝→释放”这样的耗时过程了,就形成了移动拷贝构造函数;

(4)移动拷贝(对象)构造函数

        内部机制是将临时对象的指针成员空间赋给新对象,然后将临时对象的指针指向置为空(原对象就不可以再使用),其他值对等赋值,有移动拷贝构造函数和已移动等号运算符重载两种实现方式;在两种情况下系统自动调用移动对象函数:

        ①新对象先定义后接收函数的返回值(临时对象);

        ②利用std::move()进行强制转换(告诉编译器括号内的是右值,匹配移动对象构造函数);

        通常在编译时加上-fno-elide-constructors的选项,作用是关闭函数返回值优化,同时减少拷贝构造函数的调用,转换为调用对象移动函数,提高程序运行性能;

Test (Yest &&other)  //方式一:移动拷贝构造函数
{
    cout<<"move Test copy"<<endl;
    this->n_name = other.m_name;
    other.m_name = nullptr
}

Test & operator = (Test &&other)  //方式二:移动等号运算符重载
{
    cout<<"move operator = "<<endl;
    this->m_name = other.m_name;
    other.m_name = nummptr;
}

(5)委托构造函数

        ①该类型主要是为了减少多种构造函数形成的代码冗余,内部机制是调用已有的构造函数,初始化新的构造函数;

        ②执行的顺序是委托构造函数会将控制权交给代理构造函数,在代理构造函数执行完之后,在执行委托构造函数的主体;

        ③实现思路:先写全参的构造函数(称为代理函数),后写委托构造函数;

        ④缺陷:如果一个构造函数为委托构造函数,那么其初始化列表就不能对成员和基类进行初始化,只能使用调用委托构造函数时带来的参数值去匹配代理构造函数;

几个例子:

 5. 左值、右值、左引用、右引用

(1)左值的特点:

        ①指向特点内存的具有名称的值(具有对象名或者变量名);

        ②有一个相对稳定的内存地址;

        ③有一段较长的生命周期,长期存在的值;

(2)右值的特点:

        ①不指向稳定内存地址的匿名值(例如常数1,3,5...)(怎样理解?);

        ②生命周期很短,通常是暂时的;

(3)区分左右值,可以通过&运算符获取地址的就是左值,否则就是右值;

(4)左值转右值,以下两种均可以将对象转换为右值,进而匹配类对象中的移动对象构造函数(避免了拷贝函数的费时低性能):

        ①static_cast<type&&>(对象);

        ②std::move(对象);

(5)引用

        ①int num = 5;

        ②int &l_num = num;     //l_num是左值引用,绑定左值;

      ③const int &c_num = 5;  //c_num是const左值引用,const左值引用即可以绑定左值(常量),也可以绑定右值;

        ④int &&k_num = 5;       //k_num是右值引用,注意是两个&&,右引用绑定右值;

 6. 初始化列表

        初始化列表含有双重含义(定义+初始化)主要是解决那些定义就必须要初始化的变量,有以下几种:

        ①const修饰的成员变量;例如:const int index;

        ②验收引用的成员变量;例如:int &l_num;

        ③成员对象(另一个类的对象),即类对象里面含有另一个类的对象;

7. 类的平凡属性

        (1)当类对象中含有①自定义无参构造函数、②自定义拷贝构造函数、③赋值运算符重载函数、④移动拷贝构造函数、⑤类型转换构造函数、⑥委托构造函数时,就是非平凡类;否则,不带有以上函数时,就是平凡类;

即除了默认的构造函数(无参构造函数、默认拷贝构造函数),外都会破坏函数的平凡属性;

        (2)(带以上函数)非平凡类:需频繁进行赋值等操作,运行耗时较长;

        (3)(不带)平凡类:内存对内存,运行效率高,因为不用类对象复制、转移等操作;

        (4)采用default关键字,可以采用系统默认生成的构造函数,可以尽量保持类的平凡属性,从而提高程序的运行性能;是良好的代码习惯;示例如下:

Struct Node
{
    Node() = default;
    int num;
};

8. 对类中的成员进行操作

(1)static修饰(成员、函数)

        ①保持在C语言中的相关作用(修饰局部变量时,改变了局部变量的生存周期,由函数生存周期增加为程序生存周期,作用域未改变,还是函数内;修饰函数或者全局变量时,改变了函数或者全局变量的作用域,仅限该声明的.c文件内使用,在其他文件无法通过extern使用);

        ②static修饰类对象中的成员变量时,该成员变量成为类的静态变量(也称作类成员),可以直接通过类名访问而不用通过实例化对象,该静态变量被该类所有的对象共享,适用于对象之间的通信;且修饰的成员变量需要在类外初始化(需要加上类名::格式);

        此时这个成员不是该对象的成员,而是称作类成员(被类型所共有);

#include <iostream>

using namespace std;

class A
{
public:
    void set_num(int num)
    {
        m_num = num;
    }

    int get_num()
    {
        return n_num;
    }
 
    static void func()  //静态成员函数,也称类的成员函数;
    {
        cout<<m_index<<endl;   //不能访问非静态成员,只可以访问静态成员
    }
    
//private:    //若想利用类成员的共享特点,需要将该成员属性放到public,否则也会受private属性限制;
    int m_num;
    static int m_index;
}
int A::m_index = 7;  //用static修饰的成员属性,一定要在类外初始化;

int main(int argc, char **argv)
{
    A a;
    a.m_index = 1;    //访问类成员方式一:对象名.类成员名
    A a2;
    a2.m_index = 2;

    cout<<A::m_index<<endl;    //访问类成员方式二:类名::类成员名
                               //输出2
    return 0;
}

        ③static修饰成员函数时,该成员函数称为类的静态成员函数(也称作类的成员函数),可以直接通过类名访问而不用通过实例化对象,静态成员函数没有默认的this指针指针生成,所以该类函数不能访问非静态成员(即没有被static修饰的成员),主要用于与C语言编程中用于混合编程用于回调函数;

        ④static同样受限于private,所以static只有放到public区域才可以发挥作用;

        ⑤工程应用:主要有两点,一是修饰的静态成员变量可以用于该类对象之间的通信(因为可以共享);二是应用于C与c++混合接口编程;

(2)const修饰,const将修饰的变量转换为常量,既可以修饰函数,也可以修饰成员变量,

        ①修饰函数时,该函数只可以访问成员(输出),不可以改变成员的值,但是可以在对应成员前面加上mutable,那么该成员即可被该函数修改;

        ②修饰对象时,该对象只可以调用const修饰的函数;

(3)mutable修饰成员

         ①可以在被const修饰的函数中被修改;

9. noexept、explicit、default、delete、mutable关键字在类对象函数中的作用?

        (1)noexept修饰的函数,表明程序员向编译器保证该函数不会发射异常,那么编译器也就没有必要为处理这个“可能”发生的异常添加一些事先预备好的目标代码,这在一定程度上减少了函数编译后生成的目标代码;c++11为所有类的析构函数都加上了“隐式”noexcept声明;

使用的时机一般:当设计移动函数时,为其加上noexcept,以便此类的对象在使用时可以用移动操作来代替拷贝,提高了程序运行性能(因为有些标准库容器除非知道移动操作是无异常的,否则就会进行拷贝)。

        (2)explicit是禁止隐式转换的;例如当使用功能类型转换构造函数时,加上explicit则无法进行隐式转化,类型转换构造函数即失效(因为其内部进行了隐式转换);

        (3)default关键字通常修饰类对象中无参的构造函数,尽量保持类的平凡属性(单纯的结构体),对象之间赋值通过内存之间赋值操作,从而提高代码的运行性能;

        (4)delete关键字用以修饰类对象中的函数,禁止修饰的函数生成,表明在该类型对象中没有该函数,根据其位置分为两种情况:

         ①放在public时,该修饰的函数在编译时期检查与优化(节省时间,相当于错误提前出现,节省编译时间);

        ②放在private时,该修饰的函数在链接时检查优化(在编译期后,错误检查的晚,容易导致浪费编译时间);

        (5)mutable修饰的成员可以在被const修饰的函数中被修改,通常在类对象含有const修饰的函数时使用;

10.  封装为什么能够提高代码的维护性?

        ①封装的本质是将数据和函数(动作)绑定在同一个类对象里面,后续可以通过对象来完成操作;

         ②封装后的函数对外简单的接口,操作方便;

        ③封装的对象内部含有权限属性设置,函数与数据相对独立于安全;

        ④封装后的对象里面含有大量的构造函数、析构函数,极大地方便了开发者的对象定义与操作;

猜你喜欢

转载自blog.csdn.net/m0_72814368/article/details/130874949