类与对象:构造函数和析构函数

一、构造函数与析构函数的定义

        构造函数:是在类被初始化后调用的函数,含参数,可重载。

        析构函数:在类被销毁时调用的函数,不含参数,不可重载,且只调用一次。

        其中,析构函数的语法为:

        name ()

{

        information......

}

        细狗函数语法仅需在构造函数前添加“~”符号即可。

        ~name()

{

        information......

        下面我们来看一个简单的例子,来对这两个函数有一个初步的认识。

        

#include<iostream>
using namespace std;



class dog
{
public:

    dog()

    {

        cout << "构造函数被调用了!" << endl;

    }

    ~dog()

    {

        cout << "析构函数被调用了!" << endl;

    }

};

void test()
{
    dog a;
}

int main()
{

    test();

    system("pause");

    return 0;

}

        

        运行上述代码,会得到以下结果:

        构造函数被调用了!

        析构函数被调用了! 

        请按任意键继续......

        如果我们不使用test函数(记得使用test函数的时候要加小括号哦!),那么会得到下面的效果:

#include<iostream>
using namespace std;



class dog
{
public:

    dog()

    {

        cout << "构造函数被调用了!" << endl;

    }

    ~dog()

    {

        cout << "析构函数被调用了!" << endl;

    }

};

void test()
{
    dog a;
}

int main()
{

    dog a;

    system("pause");

    return 0;

}

        运行结果如下:

        构造函数被调用了!

        请按任意键继续......

        析构函数被调用了!

      您会发现,析构函数的调用发生在程序运行之后。

      这是因为函数调用完毕之后,会自动释放函数体的内存,这时会动用析构函数。

      而直接在主函数内创造dog类的话,它将被存储在栈区,将在整个程序运行完之后由编译器来销毁。


二、构造函数的分类

        构造函数可分为默认构造函数、有参构造函数和拷贝构造函数;其中,拷贝构造函数又分为浅拷贝和深拷贝两种。 

        默认构造函数

        以下是一个默认构造函数:

dog()
{
	cout << "默认构造函数调用了!" << endl;
}

        这样的构造函数不带参数,也可称其为无参构造函数。

        在编译器中,如果您不声明构造函数,编译器将自动为您生成一个空实现的构造函数,其效果等同于:

        

dog()
{
    //我是空的,嘻嘻......
}

        这将不会显示在代码区,由编译器自动为您写好。

        值得注意的是,在调用默认构造函数的时候,不要添加小括号,写成如下的样子:

        dog a();

        默认构造函数不需要您添加括号。与之相反,倘若您加上括号,编译器会把这种声明当作函数体的声明,意义为“创建了一个dog类的函数”,编译器会由于下方没有函数体报错,而不是“调用dog的构造函数”。

        为了区分二者,您只能选择去掉这个小括号,写成如下形式:

        dog a;

        有参构造函数 

        有参构造函数正如它的名字一样,是带参数的构造函数,其一般语法为:

dog(int a)
{
	cout << "有参函数被调用了!" << endl;
}

        其中,a是该构造函数的形参,需要我们输入。

        有参构造函数与默认构造函数整体上没有区别,唯一突出的是,有参构造函数多了参数,可以在构建类的时候初始化一些成员的值,方便我们进行操作,具体可参照下方的示例:

#include<iostream>
using namespace std;

class dog
{
public:
	int age;
	dog(int a)
	{
		age = a;
		cout << "有参构造函数被调用了!" << endl;
	}


};

void test()
{
    dog a(10);
    cout<<"a的年龄是:"<<"a.age<<endl;
}

int main()
{
    test();
    system("pause");
    return 0;
}

        您可以在有参构造函数内同时建立多个参数,以便您达到想要的效果。

        值得注意的是,当您构建了一个有参构造函数或默认构造函数时,编译便不会再为您创建默认构造函数,而是使用您所创建的构造函数。

        后续在浅拷贝与深拷贝的部分我们将详细介绍这一特性。

        拷贝构造函数

        前文我们提到过,拷贝构造函数分为浅拷贝与深拷贝两种。为了方便您的理解,笔者暂将两者区别搁置,放到后续文段中介绍。

        拷贝函数的使用目的是将一个类中的成员复制到另一个类里面去。

        一般语法如下:

dog(const dog& b)
{
	age = b.age;
	cout << "拷贝构造函数调用!" << endl;
}

        通过这样的操作,我们可以将b中的age复制到另一个类的age里面(前提是age属性为public)。

        几个地方需要提醒您注意:

        1、关于const修饰符:

        这里我们使用了const修饰符,意在保护dog b中的成员,防止其被修改(这是有必要的)。

        2、关于引用符的使用:

        这里的引用符意在节省内存空间,不用将b中成员拷贝一份传到函数内。

        3、 关于参数的使用:

        这里可以导入多个形参,可依据情况使用。

        在了解了三种构造函数之后,我们将学习调用它的三种方式。


三、调用方式的分类

      调用方法分为括号法、显式法、隐式转换法。

  •  括号法

       括号法是一种较为直接的调用构造函数的方法。

       在上面代码的基础上,我们可以通过下面的test01函数来实现:

 void test01()

{

      dog a;//不要使用dog a()!

      dog b(10);//有参构造函数的调用

      dog c(b);//拷贝构造函数的调用

}

       您可以直观地根据构造函数参数(即括号内的内容)的不同来区分三者,因此我们将这种调用方式称为括号法。

  • 显式法

       显式法正如它的名称一样,通过明显的赋值操作来实现构造函数的调用。

void test02()

{

      dog a;//默认

      dog b=dog(10);//有参,相当于dog b(10)

      dog c=dog(b);//拷贝,相当于dog c(b)

}
 匿名对象 

        值得注意的是,倘若您单独使用类似于以下的语句:

        dog(10); 

        编译器会将dog(10)视作“匿名对象”。

        匿名,即没有名称。

        它只为某一个类内的成员规定了值,但这个类没有具体的名字来指代,故称其为匿名对象。

        如果没有对象接收匿名对象,那么在当前行结束后,系统会立即回收匿名对象。

        从上述例子中我们可以看出,显式法相较于括号法,只是多了一步赋值的操作。

        注意:

        不要利用拷贝构造函数来构造匿名对象!
        编译器会自动把类似dog (a)视为dog a,会导致重定义以及代码可读性下降等问题。

  • 隐式转换法

       隐式转换法省去了一些繁琐的表示,示例如下:

void test03()
{
    dog a = 10;//等于dog a=dog (10);
	dog b = a;//等于dog b=dog a;
	dog c = b;//等于dog c=dog b;
	cout << "a为:  " << a.age << endl << "b为:  " << b.age << endl << "c为:  " << c.age << endl;
}

        输出结果为:

        a为:10

        b为:10

        c为:10 

        值得一提的是,在dog a=10;这行代码中,相当于我们构造了一个匿名对象dog(10),但因为有已知对象a来接收它,所以这里的行为被认为是合理的。

  • 三种方法的特性

        整体来说,我们使用括号法会多一些,因为它简单易懂、便于操作。

        当然,三种方法都是合理且可用的,您可以根据您的个人喜好来选择。

        但是,在您与其他程序员一起合作的时候,您最好根据实际需要来选择。

        (不要为难您的同事qaq......尤其要避免用拷贝函数构造匿名对象)

        您可以复制上面的test01、test02、test03函数到main函数中测试运行效果,笔者在此不再赘述。


四、构造函数的调用规则

        在您创建一个类时,编译器将自动为您构造好默认构造函数、拷贝构造函数和析构函数,只不过它们都是空实现(即函数体为空,不执行任何操作)。

        当您自己设定了这三个函数的某一个后,编译器便会以您为准,调用您所构造好的函数,而非空实现的函数。

        有意思的是,当您设定了有参构造函数而没有设定默认构造函数时,编译器不会再调用默认构造函数,而是只调用您构建好了的有参构造函数。

        如果发生了这种情况,那么您的某些操作将会受限。

        例如:

class person
{
public:
	person()
	{
		cout << "默认构造函数调用!" << endl;
	}
    /*如果没有这一段,当您在其他地方使用类似dog a;这样的语句时会报错。
     因为存在有参构造函数之后,编译器就不会构造默认函数了,但依然会提供拷贝构造函数。*/

	~person()
	{
		cout << "析构函数调用!" << endl;
	}
	person(int age)
	{
		m_age = age;
		cout << "有参构造函数的调用!" << endl;
	}
	person(const person& p)
	{
		m_age = p.m_age;
		cout << "拷贝构造函数的调用!" << endl;
	}/*如果去掉这一段,系统自动帮你写这些代码(只是没有Cout而已)*/

	int m_age;
};

        因此,如果您要使用构造函数,最好将三种函数全部构造好(或者只提供默认构造函数)。

        或许这张表可以帮助您更好的理解三者的关系:

        将上述三个函数设为A, B, C,分别代表默认构造、有参构造和拷贝构造。
则有:  

                 我们是否提供了?        编译器提供?
        A                  X                              √
        B                  X                              X
        C                  X                              √
                我们是否提供了?        编译器提供?
        A                 X                               X
        B                 √                               X
        C                 X                              √
                我们是否提供了?        编译器提供?
        A                 X                               √
        B                 X                               X
        C                 √                               X
 

         我们来看一个具体的例子:

#include<iostream>
using namespace std;

class person
{
public:
    int m_age;
	person()
	{
		cout << "默认构造函数调用!" << endl;
	}
	~person()
	{
		cout << "析构函数调用!" << endl;
	}
	person(int age)
	{
		m_age = age;
		cout << "有参构造函数的调用!" << endl;
	}
	person(const person& p)
	{
		m_age = p.m_age;
		cout << "拷贝构造函数的调用!" << endl;
	}

};

void test01()
{
	person p;
	p.m_age = 18;
	person p1(10);
	person p2(p1);
	cout << "p的年龄:" << p.m_age << endl;
	cout << "p1的年龄:" << p1.m_age << endl;
	cout << "p2的年龄:" << p2.m_age << endl;
}

int main()
{
	test01();
	system("pause");
	return 0;
}

        运行结果为:

        默认构造函数调用!
        有参构造函数的调用!
        p的年龄:18
        p1的年龄:10
        p2的年龄:10
        析构函数调用!
        析构函数调用!
        析构函数调用! 

 五、拷贝构造函数调用时机

        拷贝函数一般有三个用途:

  1. 使用一个已经存在的对象来初始化一个新对象
  2. 值传递的方式给函数参数传值
  3. 值方式返回局部对象

        我们依次来看这三种用法。

  • 使用一个已经存在的对象来初始化一个新对象

        这种用法比较常见,上文中已多次用到,所以笔者在此只提供示例代码:

void test01()
{
	person p1(10);
	person p2(p1);
	cout << "p2年龄为:" << p2.m_age << endl;
}

         在这段代码中,我们先是使用有参构造函数构造了p1类,并将其内的m_age设定为10,再使用拷贝构造函数来将p1拷贝给p2,使得p2中的m_age也被设定为10。

  • 值传递的方式给函数参数传值

        拷贝函数除了可以用作初始化对象外,还可以通过值传递的方式为某个函数传值。

        例如:

void test_(person p)
{
}

void test02()
{
	person p3;//默认构造函数调用
	test_(p3);//拷贝函数调用
}

        在这段代码中,先调用了默认构造函数(person p3;时候调用),再通过 test_函数将p3

的值传给其中的形参p。

        在运行完毕后,test_函数中的p先被释放,调用了一次析构函数;接着test02函数运行完毕,释放p3,再调用一次析构函数。

  • 值方式返回局部对象

        和上面的功能类似,在函数中,我们同样可以通过拷贝构造函数来实现局部对象的值返回。

person dowork1()
{
	person p(10);
	return person(p);//x86中,此处括号可省略
}

void test03()
{
	person p = dowork1();
	cout << "p的值: " <<p.m_age<< endl;
}

        (若您的编译器是x86版本,person(p)中的括号可省略) 

        运行结果为:

        有参函数调用了!
        拷贝构造函数调用了!
        析构函数调用了!
        p的值: 10
        析构函数调用了!

        此处我们也能更直观的看出两个函数释放的先后顺序,这与栈的后进先出有关(可移步笔者即将写好的另一篇文章:内存四区的功能)。 

六、深拷贝与浅拷贝

        深拷贝:在内存中开辟空间,再进行拷贝。

        浅拷贝:直接拷贝。

        这部分内容比较重要,希望读者能够细心看完。

  • 浅拷贝

        浅拷贝部分比较简单,若没有特殊声明,我们构造的拷贝构造函数都为浅拷贝。

        (包括编译器帮我们设定的拷贝构造函数)

  • 深拷贝

        深拷贝是通过在内存中开辟一片空间,来达到拷贝的目的。

        根据我们上面的内容,在大多数编译器中,如果我们没有设定拷贝构造函数,那么编译器将会自动为我们写好一个拷贝构造函数。

        但是,这个拷贝构造函数属于浅拷贝,会存在一些缺陷。

        我们来看一个例子:

#include<iostream>
using namespace std;

//浅拷贝:直接拷贝
//深拷贝:开辟空间再拷贝
class person {
public:
	int m_age;
	int* m_height;
	person()
	{
		cout << "默认构造函数调用!" << endl;
	}

	person(const person& p)
	{
		m_age = p.m_age;
		//m_height = p.m_height;编译器写的,会造成重复
		//指向了同一块内存,释放了两次!非法操作
		m_height = new int(*p.m_height);//开辟空间,以备拷贝操作执行
		cout << "拷贝构造函数调用了!" << endl;
	}

	person(int age, int height)
	{
		m_age = age;
		m_height = new int(height);//开辟空间,以备设定成员初始值
		cout << "有参构造函数的调用!" << endl;
	}

	~person()
	{
		if (m_height != nullptr)
		{
			delete m_height;
			m_height = nullptr;
		}//若m_height为被释放,则将其占用的空间释放
		cout << "析构函数调用!" << endl;
	}
};

void test01()
{
	person p1(18, 160);//设定初始值
	cout << "p1年龄:" << p1.m_age << "身高为:" << *p1.m_height << endl;
	person p2(p1);//深拷贝的调用
	cout << "p2年龄:" << p2.m_age << "身高为:" << *p2.m_height << endl;
}

int main()
{
	test01();
	system("pause");
	return 0;
}

        在上面这个程序中,我们首先设定了 成员m_age和m_height,其中m_age为int型,而m_height为int*型。

        然后,我们构建了一个拷贝构造函数,并针对指针型m_height开辟了一块空间用于拷贝。

        接着,我们创建了一个有参构造函数,用于设定成员值,并且在这个函数中,我们同样开辟了一块空间来执行拷贝操作。

        紧接着,我们使用析构函数,来释放被拷贝成员在拷贝过程中占用的内存。

        最后,test01函数调用,整个程序完成。

        我们来看运行结果:

有参构造函数的调用!
p1年龄:18身高为:160
拷贝构造函数调用了!
p2年龄:18身高为:160
析构函数调用!
析构函数调用!

        在上面的案例中,我们不难发现:

        我们以指针的类型构建了成员m_height。

        在构建了指针成员m_height后,我们可以利用深拷贝的方式来拷贝该成员。

person(const person& p)
{
	m_age = p.m_age;
	//m_height = p.m_height;编译器写的,会造成重复
	//指向了同一块内存,释放了两次!非法操作
	m_height = new int(*p.m_height);
	cout << "拷贝构造函数调用了!" << endl;
}

        但值得注意的是,如若我们什么都不声明,写成这样:

person(const person& p)
{
	cout << "拷贝构造函数调用了!" << endl;
}

        或者由编译器帮我们写,这两种写法编译器都会报错。

        原因在于,此时调用的构造函数属于浅拷贝,他会完全复制前一个类的成员给另一个类,这样做就会产生极大的安全隐患,即两个不同类中的m_height都指向了同一块内存。

        在后面我们使用析构函数释放该内存的时候,会产生同一块内存释放多次的非法操作。

~person()
	{
		if (m_height != nullptr)
		{
			delete m_height;
			m_height = nullptr;
		}
		cout << "析构函数调用!" << endl;
	}

        因此,我们一定要在拷贝构造函数中声明:

        m_height = new int(*p.m_height);

         从而使拷贝构造函数在调用时开辟新的内存,让两块内存中的数据相同、地址不同,从而避免了后面析构函数多次释放同一块空间的操作。

七、内联函数

        内联函数通常会与类一起使用。

        如果您足够细心的话,当您创建一个成员函数后,将鼠标光标移动到该函数的名称上,编译器会提示您以下内容:

        inline type class_name::func_name()

        其中,type为函数类型,class_name和fuc_name分别为类名和函数名。

        这里的inline,正是内联函数的声明

       什么是内联函数?

        简单来说,若某个函数是内联的,那么编译器会将该函数的代码拷贝到每一个使用到它的地方。

        下面是一个简单的使用内联函数的例子:

#include<iostream>
using namespace std;

inline int max(int x, int y)
{
	return x > y ? x : y;
}

int main()
{
	cout << max(20, 10) << endl;
	cout << max(70, 10) << endl;
	cout << max(0, 10) << endl;
	return 0;
}

        输出结果为:

        20

        70

        10

       内联函数的意义

        内联函数本质上是用代码的空间效率来换取时间效率

        由于编译器会将代码拷贝到每一处使用到内联函数的地方,内联函数的调用将区别于普通函数的调用,会直接在使用函数的地方直接进行调用,省去了运行的时间。

        因此,内联函数一般都是行数较少的函数,且一般不在函数中使用循环语句开关语句

        若内联函数过于庞大,会造成空间和时间的双重浪费,无意义;

        而在内联函数中使用循环和开关语句,会产生意想不到的效果,有时语句会正常运行,有事循环和开关都不会被触发,笔者在此不作详细解释,读者可自行尝试。

        同时,内联函数的定义必须出现在它被调用之前。

        以及,类中的成员函数都是内联函数。

八、this指针的使用

        首先明确的一点,this是一个指针。

        它可以指代当前类中的对象,以便区分。

        例如:

//实例:两狗比较属性值(属性值:年龄*身高)
class dog
{
	int age;
	double height;
public:
	dog(int a=1,double h=1.0)
	{
		cout << "构造函数调用!" << endl;
		age = a;
		height = h;
	}//使用构造函数,初始化age和height
	int cal_dog()
	{
		return age*height;
	}
	void setage(int a)
	{
		this->age = a;//等效于age=a;
	}

	void setheight(double h)
	{
		this->height = h;//这里等效于height=h;
	}

	bool compare(dog dog1)
	{
		return this->cal_dog() > dog1.cal_dog();//若前者大于后者,返回true,相当于一个判断
	}
};

int main()
{
	dog dog1;
	dog dog2;
	dog1.setage(10);
	dog1.setheight(23.0);
	dog2.setage(10);
	dog2.setheight(30.0);
	cout << "dog1的属性值是:" << dog1.cal_dog() << endl;
	cout << "dog2的属性值是:" << dog2.cal_dog() << endl;

	if (dog1.compare(dog2))
	{
		cout << "dog1的属性值大于dog2的属性值!" << endl;
	}

	if (!(dog1.compare(dog2)))
	{
		cout << "dog1的属性值小于等于dog2的属性值!" << endl;
	}
	
	return 0;
}

//this 指针的类型可理解为 Box*。

         您也可以使用this指针来指代整个对象,例如:

void krdog(dog &d)
{
    *this=d;
}

        这种用法可以用来比较两个类之间的成员等操作。

九、指向类的指针

        我们可以通过创建一个指向类的指针来对类进行操作。

#include<iostream>
using namespace std;

class Box {
	double length;
	double height;
	double width;
public:
	Box(double l=1.0,double h=1.0,double w=1.0)
	{
		cout << "调用构造函数!" << endl;
		length = l;
		height = h;
		width = w;
	}
	double calvol()
	{
		return length * width * height;
	}
};

int main()
{
	Box box1(1.3, 2.4, 7.8);//有构造函数后,就不需要使用setheight函数了
	cout << "box1的体积是:" << box1.calvol() << endl;
	Box* prebox=nullptr;//创造box型的指针
	prebox = &box1;//让prebox指向box1
	cout << "prebox的体积是: " << prebox->calvol() << endl;//prebox->calvol相当于box1->calvol
	system("pause");
	return 0;
}

十、静态成员 

        在类中,我们可以创造一个静态成员(变量和函数),对于所有的对象而言,静态成员有且只有一个。

        例如:

#include<iostream>
using namespace std;

//静态成员变量:当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
//静态成员在类的所有对象中是共享的。
//如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。
//我们不能把静态成员的初始化放置在类的定义中,
//但是可以在类的外部通过使用范围解析运算符::来重新声明静态变量从而对它进行初始化
//例如:
class dog {
	int age;//实例变量
	double height;//实例变量
public:
	static int count;//静态变量count用来计算dog的个数
	dog(int a=1,double h=1.0)
	{
		cout << "构造函数调用!" << endl;
		age = a;
		height = h;
		count++;//每次调用构造函数时,count++,计数器加一
	}
	double calvol()
	{
		return age * height;
	}
	static int getcount()
	{
		return count;
	}
};


int dog::count = 0;//类外定义静态成员变量(一定要做,不然编译器会报错)->为其分配内存

int main()
{
	dog dog1(1, 2.0);
	dog dog2(2, 3.0);
	cout << "dog1属性值:" << dog1.calvol() << endl;
	cout << "dog2属性值:" << dog2.calvol() << endl;
	cout << "等效于下面!count为:" << dog::count << endl;
	cout << "总共有" << dog::getcount() << "只狗!" << endl;//一定要这样使用,因为在dog内部,count有且仅有一个
	return 0;
}

//调用静态成员变量和静态成员函数时,可以声明它的作用域dog::,因为对于dog类,无论其内的情况如何,
//count有且仅有一个,无论是dog1.count还是dog2.count都是它。
//但推荐使用dog::count,更能突出静态成员变量的特性。

        到此处,我们就将构造函数和析构函数以及类的大致概念学习完毕了 ,接下来,我们将进入到继承和多态的学习。

        相信您已经做好了准备,从下面的内容开始,我们将进一步接触到C++的核心理念,希望您在读完文章后能有所收获。

        


        (创作不易,感谢支持!)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值