• C++基础学习笔记----第十四课(new和malloc的区别、单例模式等深入)


    本节主要讲new关键字和malloc函数的差别,编译器对构造函数调用的实质,单例模式的实现等。

    new和malloc的差别

    1.malloc和free是C语言的库函数,以字节为单位申请堆空间。new和delete是C++的关键字,以类型为单位申请堆空间。malloc和free单纯的对内存申请和释放,对于类类型new和delete还负责构造函数和析构函数的调用

    2.malloc只是单纯的申请一块内存空间,并不负责调用构造函数。构造函数的本质是用来初始化对象的,而malloc不具备初始化的功能,所以不具备调用构造函数来对申请的对象进行初始化。

    3.delete关键字不单单是将内存归还给系统,还调用析构函数来销毁对象。但是free函数只是单纯的释放内存,并不能够调用析构函数来销毁对象,所以在涉及到类类型的时候free函数的释放内存可能会造成内存泄露。

    4.在针对普通的函数数组和变量的时候,使用malloc和new不会有差别(存在差别可能是执行效率上,但是这个涉及到C++内部的机制,暂时不讨论),但是涉及到类类型的时候new和malloc以及free和delete可能会产生完全不同的效果。

    基本例程如下:

    #include <stdio.h>
    #include <stdlib.h>
    
    class A
    {
    private:
    	int a;
    	int b;
    public:
    	A()
    	{
    		printf("This is A()!
    ");
    	}
    	~A()
    	{
    		printf("This is ~A()!
    ");
    	}
    	void print()
    	{
    		printf("123
    ");
    	}
    };
    
    
    int main()
    {
    	int *a = new int;
    	
    	/*使用强制类型转换将申请的内存转换为int*类型*/
    	int *b = reinterpret_cast<int*>(malloc(sizeof(int)));
    	
    	*a = 9;
    	*b = 8;
    
    	printf ("*a = %d
    ",*a);
    	printf ("*b = %d
    ",*b);
    
    	free(b);
    	delete a;
    	
    	A* c = new A;
    	A* d = reinterpret_cast<A*>(malloc(sizeof(A)));
    	
    	c->print();
    	d->print();
    
    	delete c;
    	//free(c);
    
    	free(d);
    	
    	return 0;
    }

    程序打印结果如下:

    通过打印结果可以发现:当使用new和malloc为普通变量申请内存空间的时候程序执行的结果没有差别,当使用new和malloc分别申请类的对象的空间时候,malloc和free这两个函数都不调用构造和析构函数。同时,使用new关键字来申请的空间,可以使用free函数来释放,当空间中的内容是普通变量的时候没有差别,当是类类型的内存的时也将不再调用析构函数。

    编译器对构造函数的调用及explicit关键字

    假定类A,如果类中定义了普通构造函数和拷贝构造函数,在其他函数中定义三个该类的对象,代码如下:

    A a1(1);
    A a2 = 2;
    A a3 = A(3);

    整体程序实现代码如下:

    #include <stdio.h>
    
    class A
    {
    private:
    	int a;
    	int b;
    public:
    	A(int i)
    	{
    		printf ("A(int)
    ");
    	}
    
    	A(const A& aj)
    	{
    		printf("A(const)
    ");
    	}
    
    	~A()
    	{
    		printf ("~A()
    ");
    	}
    };
    
    void func()
    {
    	A a1(1);
    	A a2 = 2;
    	A a3 = A(3);
    }
    
    int main()
    {
    	func();
    	return 0;
    }

    程序打印结果如下:


    通过上面的打印结果可以发现,在编译器中,三种类的初始方法都是调用了普通构造函数,同时在调用结束调用了三次构造函数,并没有涉及到拷贝构造函数的调用,这是.........现代C++编译器的优化。

    古代C++编译器中,A a1(1);这种格式调用析构函数和调用普通的函数没有差别,因为函数的调用和参数都是符合类中定义的构造函数的。A a1 = 2;这种格式调用析构函数可以发现,这里的2是一个字面量且为整形,但是等号的左面是一个A类型的对象,所以等号两面的类型并不匹配。编译器的在默认的情况下自动调用拷贝构造函数。编译器将

    A a1 = 2;转化为A a1 = A(2);这里是直接调用A的构造函数A(int i),所以这里将会产生一个临时对象,也就是转化成了使用一个类的对象去初始化一个新定义的这个类的对象,这是合法的。最后调用拷贝构造函数A(const A& aj)使用临时对象进行初始化。但是现在的C++编译器其实已经将上面的步骤省略掉了,现代C++编译器直接是将 A a1 = 2转化成为A a1(2);这样大大的节省了程序的编译运行时间。

    C++编译器调用构造函数的基本原则如下图:


    explicit关键字

    explicit的作用是剥夺C++编译器对构造函数的主动的调用尝试。

    基本例程如下:

    #include <stdio.h>
    
    class A
    {
    private:
    	int a;
    	int b;
    public:
    	explicit A()
    	{
    		printf("This is A()!
    ");
    	}
    	
    	explicit A(int i)
    	{
    		printf("This is A(int)
    ");
    	}
    
    	~A()
    	{
    		printf("This is ~A()!
    ");
    	}
    	void print()
    	{
    		printf("123
    ");
    	}
    };
    
    
    int main()
    {
    	A a1;
    	//a1.print();
    
    	A a2(1);
    	//A a3 = 2;
    
    	return 0;
    }

    程序打印结果如下:


    通过上面的结果可以发现:定义对象a1的代码如下:

    A a1;

    这里程序依然正常调用了经过explicit修饰的构造函数,因为这是默认的初始化,并不是C++主动调用的,同样A a2(1);这个初始化类的对象也正常调用了构造函数。但是

    A a3 = 2;

    这条初始化类的对象的函数将不会编译通过,因为这里编译器在编译的过程中将会 主动调用经过explicit修饰的构造函数,所以程序将会编译出错

    单例模式

    单例模式是C++语言的一种设计模式,我觉得面向对象的语言都存在设计模式的问题,悄悄的想起从买了就没看过的大话设计模式了~

    在实际编写程序过程中,有些场景要求一个类智能有一个对象存在于系统之中,称为单例模式。例如:一个汽车对象只能有一个发动机对象。
    基本例程如下:

    #include <cstdlib>
    #include <iostream>
    
    using namespace std;
    
    class Singleton
    {
    private:
    	/*定义静态成员变量*/
        static Singleton* cInstance;
        
        Singleton()
        {
        }
    public:
    	/*实现一个静态成员函数,这个静态成员函数可以直接访问上面定义的静态成员变量*/
        static Singleton* GetInstance()
        {
            if( cInstance == NULL )
            {
                cout<<"new Singleton()"<<endl;
    			/*这里是可以调用这个类中的普通成员函数的,无论是否是private还是public的*/
                cInstance = new Singleton();
            }
    
            /*这里返回的是指针*/
            return cInstance;
        }
        
        void print()
        {
            cout<<"I'm Singleton!"<<endl;
        }
    };
    
    /*在类的外部定义类的静态成员变量*/
    Singleton* Singleton::cInstance = NULL;
    
    void func()
    {
    	/*
    		定义一个类的指针,使这个指针指向Singleton类的函数的返回值,实际上就是完成了一个对象的建立
    		这里s所指向的对象与cInstance所指向的对象是完全相同的
    	*/
        Singleton* s = Singleton::GetInstance();
        Singleton* s1 = Singleton::GetInstance();
        Singleton* s2 = Singleton::GetInstance();
        
        cout<<s<<" "<<s1<<" "<<s2<<endl;
        
        s->print();
    }
    
    int main(int argc, char *argv[])
    {
        func();
    
        return EXIT_SUCCESS;
    }
    

    程序打印结果如下图:


    通过程序打印结果可以发现,无论在函数中申请调用几次类的对象,包括s,s1,s2,但是它们都指向同一个地址,实现了单例模式的设计。这里应该存在一块非法空间,程序在new之后并没有进行delete.

    状态函数和无状态函数以及斐波拉契数列的实现

    基本概念:

    无状态函数的调用结果只与实参值相关。状态函数的调用结果不仅仅与实参值相关还与之前的函数调用有关。

    例程实现:

    #include <stdio.h>
    
    int fib1(int i)
    {
    	int a = 0;
    	int b = 1;
    	int ret = b;
    
    	while(i >= 1)
    	{
    		ret = a + b;
    		a = b;
    		b = ret;
    		i--;
    	}
    
    	return ret;
    }
    
    int fib2()
    {
    	static int a = 0;
    	static int b = 1;
    
    	int ret = b;
    	int g = b;
    
    	b = a + b;
    	a = g;
    
    	return ret;
    }
    
    class A
    {
    private:
    	int a;
    	int b;
    public:
    	A()
    	{
    		a = 0;
    		b = 1;
    	}
    	int fib3()
    	{
    		int ret = b;
    		int g = b;
    
    		b = a + b;
    		a = g;
    
    		return ret;
    	}
    };
    
    int main()
    {
    	A a;
    
    	for (int i = 0; i < 5; i++)
    	{
    		printf("%d
    ",fib1(i));
    	}
    	
    	/*for (int i = 0; i < 5; i++)
    	{
    		printf("%d
    ",fib1(i));
    	}*/
    
    	printf ("
    ");
    
    	for (int j = 0; j < 5; j++)
    	{
    		printf("%d
    ",fib2());
    	}
    
    	/*for (int j = 0; j < 5; j++)
    	{
    		printf("%d
    ",fib2());
    	}*/
    
    	printf("
    ");
    	
    	for (int k = 0; k < 5; k++)
    	{
    		printf("%d
    ",a.fib3());
    	}
    	
    	/*A a1;
    
    	for (int m = 0; m < 5; m++)
    	{
    		printf("ea.fib3() %d
    ",a1.fib3());
    	}*/
    
    	return 0;
    }

    程序打印结果如下:


    通过上面的打印结果可以发现函数fib1()、fib2()和对象a调用的函数fib3()打印结果都相同, 其中fib1()是以无状态函数的方式实现的,求解斐波拉切数列的每一项时都会做重复循环,时间复杂度为O(n),fib2()是以状态函数方式实现的,每一次调用就可以得到数列当前项的数值,时间复杂度为O(1),但是如果想要再求前面第几个数的值将无法实现。通过类A定义的对象a调用成员函数fib3(),时间复杂度是O(1),同时如果想要求前面数的值,那么可以重新定义一个对象即可。


  • 相关阅读:
    零拷贝
    RxJava2源码解析
    一次博客崩溃日志分析
    Spring循环依赖的解决
    解决网络卡顿问题
    软工第一次作业
    3月26-27号训练笔记
    Codeforces Round #708 (Div. 2)题解A,B,C1,C2,E1,E2
    求出所有LIS的可行起点
    2020小米邀请赛决赛补题G,I,J(三DP)
  • 原文地址:https://www.cnblogs.com/riasky/p/3469215.html
Copyright © 2020-2023  润新知