C++基础总结系列之【构造函数】

目录

特点

作用

分类

调用规则

是否自定义构造函数

自定义的构造函数是否有参数

是否自定义拷贝构造函数

初始化列表

作用

必须使用初始化列表的情况

解释一下对象成员为什么必须要在初始化列表中初始化

使用初始化列表的原因

用构造函数初始化

用初始化列表初始化

初始化列表的执行顺序

拷贝构造函数

调用时机

初始化

值传递

值返回 

浅拷贝与深拷贝

解决办法

 


 

特点

没有返回值,函数名与类名相同,可以有重载,只调用一次


作用

用于在创建对象时,给对象中的成员初始化


分类


调用规则

  • 是否自定义构造函数

是——调用自定义构造函数,不再提供默认构造函数

否——编译器自动调用默认构造函数

class Test{
public:
	Test()
	{
		cout<<"执行带参的构造函数"<<endl;
	}
	~Test()
	{
		cout<<"执行析构函数"<<endl;
	}
};
int main()
{
	{
		Test t;
	}
	system("pause");
	return 0;
}

  • 自定义的构造函数是否有参数

是——不再提供默认的无参构造函数,但是依然提供默认的拷贝构造函数

class Test{
public:
	Test(int a)
	{
		cout<<"执行带参的构造函数"<<endl;
	}
	~Test()
	{
		cout<<"执行析构函数"<<endl;
	}
};
int main()
{
	{
		Test t;
	}
	system("pause");
	return 0;
}

编译不通过,产生如下错误

class Test{
public:
	Test(int a)
	{
		cout<<"执行带参的构造函数"<<endl;
	}
	~Test()
	{
		cout<<"执行析构函数"<<endl;
	}
};
int main()
{
	{
		Test t1(10);
		Test t2(t1);
	}
	system("pause");
	return 0;
}

可以通过编译,输出,可以看到构造函数只执行了一次,说明对象t1调用了带参的构造函数,对象t2调用了默认的拷贝构造函数

 

  • 是否自定义拷贝构造函数

是——调用自定义的拷贝构造函数

class Test{
public:
	Test(int a)
	{
		cout<<"执行带参的构造函数"<<endl;
	}
	Test(const Test & a)
	{
		cout<<"执行自定义的拷贝构造函数"<<endl;
	}
	~Test()
	{
		cout<<"执行析构函数"<<endl;
	}
};
int main()
{
	{
		Test t1(10);
		Test t2(t1);
	}
	system("pause");
	return 0;
}

输出

 


初始化列表

  • 作用

与构造函数相同,用于在创建对象时,给对象中的成员初始化

  • 必须使用初始化列表的情况

解释一下对象成员为什么必须要在初始化列表中初始化

class CMember
{
public:
	CMember(int a)
	{
		cout<<"CMember(int a)"<<endl;
	}
};
class CClass
{
public:
	CMember member;
public:
	CClass()
	{
		cout<<"CClass()"<<endl;
	}
};

int main (){

	CClass cl;
	system("pause");
    return 0;
}

编译不通过,会显示

当一个类中有带参数的构造函数时候,不再提供默认的无参构造函数

在另一个类中需要定义它的对象,找不到对应的构造函数来进行初始化,又不能在定义时直接赋值,因此必须在CClass的初始化列表中对其进行初始化才能通过编译

如果成员是一个常量对象或者引用,是一样的道理,因此必须使用初始化列表

  • 使用初始化列表的原因

必要性:上述说到必须使用初始化列表的情况

考虑效率:

进入构造函数体后,进行的是计算,是对成员变量的赋值操作,而赋值和初始化是不同的,需要执行一次构造函数,和一次赋值操作

而调用初始化列表,会少产生一次构造函数的调用,并且少一次赋值的操作

用构造函数初始化

class CFather {
    public:
        CFather() 
		{
			cout << "CFather()" << endl;
		}
        CFather(int a) 
		{
			cout << "CFather(int)" << endl;
		}
		CFather & operator = (const CFather& a)
		{
			cout<<"CFather & operator ="<<endl;
			return *this;
		}
};
class CSon : public CFather {
    public:
        //CSon(): father(2) 
		//{ 
		//	cout << "CSon()" << endl;
		//} //初始化列表
        CSon() 
		{ 
			father = 2;
			cout << "CSon()" << endl;
		} //构造函数

    private:
        CFather father;
};
int main()
{
    CFather father;
    CSon son;
	system("pause");
	return 0;
}

输出为

CFather第一次无参的构造函数,是定义CFather father对象执行的

CFather第二次无参的构造函数,是CSon继承CFather,在初始化列表中执行的

CFather第三次无参的构造函数,是CSon进入构造函数后执行的

CFather第四次带参的构造函数,是通过给对象赋值操作执行的

CFather的重载等号操作符的输出,是通过给对象赋值操作产生的

用初始化列表初始化

输出为

CFather第一次无参的构造函数,是定义CFather father对象执行的

CFather第二次无参的构造函数,是CSon继承CFather,在初始化列表中执行的

CFather第三次带参的构造函数,是CSon在初始化列表中给CFather对象进行初始化执行的

  • 初始化列表的执行顺序

class A
{
public:
	A(int val):b(val),a(b){}
	int a;
	int b;
};

int main()
{
    A a(3);
	cout<<a.a<<" "<<a.b<<endl;
	system("pause");
	return 0;
}

输出为

 

原因在于,构造函数初始化列表的执行顺序与初始化列表的书写顺序无关,而是与变量在类中的声明顺序有关

当定义对象A a(3)时,执行A的带参构造函数,由于a在类中是先声明的,先执行a的初始化,此时b并没有一个确定值,所以a被初始化为一个非确定的值

再执行b的初始化,输出结果为b=3


拷贝构造函数

  • 调用时机

初始化

使用一个已经创建完毕的对象来初始化一个新对象

CPerson p1(20);
CPerson p2(p1);

值传递

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

void work(CPerson p){}
int main()
{
    CPerson p;
    work(p);
}

值返回 

class CPerson
{

};
CPerson work()
{
	CPerson p;
	cout<<(int*)&p<<endl;
	return p;

}
int main()
{
	CPerson p = work();
	cout<<(int*)&p<<endl;
	system("pause");
	return 0;
}

输出为

可以看出,两个对象并非同一个,在值返回时,返回的是调用拷贝构造函数创建的对象

在work函数中,由CPerson对象位于栈区,函数结束后,局部对象p生命周期结束,销毁

  • 浅拷贝与深拷贝

深拷贝与浅拷贝是一个故弄玄虚的概念,其本质上是指针与内存

对于含有指针成员的类

浅拷贝:两个指针指向同一块内存,在析构时,对同一块内存释放两次

深拷贝:两个指针指向不同的内存,在析构时,各自释放自己指向的区域

class Student
{
private:
	int num;
	char *name;
public:
	Student()
	{
		name = new char(20);
		cout << "Student" << endl;
	}
	~Student()
	{
		cout << "~Student "<< endl;
		delete name;
		name = NULL;
	}
};

int main()
{
	{
		Student s1;
		Student s2(s1);// 执行默认拷贝构造函数
	}
	system("pause");
	return 0;
}

正常输出,随后程序崩溃

原因在于,s1的构造函数中已经给指针name在堆区申请了一块内存空间,在用s1给s2赋值时,只是简单地用对象s2的name指针指向与s1的name指针相同的内存空间

在s1、s2的生命周期结束后,s1先调用析构函数,将name指向的堆区空间释放,而s2又调用了一次析构函数,此时空间已经被释放一次,因此程序会崩溃

解决办法

class Student
{
private:
	int num;
	char *name;
public:
	Student()
	{
		name = new char(20);
		cout << "Student" << endl;
	}
	Student(const Student& s)
	{
		name = new char(20);
		memcpy(name,s.name,strlen(s.name));
		cout << "Copy Student" << endl;
	}
	~Student()
	{
		cout << "~Student "<< endl;
		delete name;
		name = NULL;
	}
};

int main()
{
	{
		Student s1;
		Student s2(s1);// 执行重载的拷贝构造函数
	}
	system("pause");
	return 0;
}

输出

重载拷贝构造函数,指针指向一块新的内存空间,析构时,各自释放自己的空间,程序正常运行

浅拷贝带来问题的本质在于析构函数释放多次堆区内存,使用std::shared_ptr,可以完美解决这个问题

进一步了解请参考我的另一篇文章:C++基础总结系列之【智能指针】

猜你喜欢

转载自blog.csdn.net/qq_37348221/article/details/115088776