构造函数和析构函数的作用
对对象的初始化和清理通过构造函数和析构函数。
构造函数是对对象进行初始化 而析构函数是对对象进行数据清理。
简单来说,通过类来实例化一个对象,我们需要调用构造函数。等到程序结束后我们需要对数据进行清理,这个时候我们需要调用析构函数。
//对对象进行初始化和清理是编译器强制要求我们做的事情
//如果我们不提供构造和析构函数的话,编译器会提供
//编译器提供的构造函数和析构函数是空实现
//析构函数:主要用于在创建对象时为对象的成员属性进行赋值,构造函数有编译器自动调用,无需手动调用
//析构函数:主要作用在于对象销毁前 系统 自动调用,用于做一些清理工作
构造函数和析构函数的代码实现
//构造函数语法 类名(){}
//1、构造函数,没有返回值也不写void
//2、函数名称和类名一样
//3、构造函数可以有参数,因此可以发生重载
//4、程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
//析构函数语法 ~类名(){}
//1、构造函数,没有返回值也不写void
//2、函数名称和类名一样,在前面要加上~
//3、析构函数不可以有参数,不能发生重载
//4、程序在调用对象时候会自动调用构造,无需手动调用,而且只会调用一次
具体我们用代码是这样实现的
#include<iostream>
using namespace std;
class Person {
public:
//这里我们手动定义了构造函数,但是我们我们在里面进行了一段语句的输出
//前面提到,如果我们不手动定义的话,系统会自动定义构造函数
//但是函数体里面看似一个函数都没有
//实际上,这时我们没有手动定义构造函数和析构函数
//但是编译器帮助我们定义了这样的函数
Person() {
cout << "HOOK THE FUNCTION" << endl;
}
~Person() {
cout << "hello world" << endl;
}
};
int main() {
Person p1;
//定义了一个Person对象,这会自动调用构造函数
//在栈上开辟空间,在执行完毕后,系统会自动释放空间,调用析构函数
return 0;
}*/
作用域解析运算符::
::是作用域接续运算符号,通过它,我们可以实现在类外定义成员函数,但是我们需要在类内声明相应的函数。
#include<iostream>
using namespace std;
class student {
public:
int no;
string name;
float scorse[3];
public:
student(); //声明了构造函数但是没有去定义它
void account(); //声明了account函数但是没有定义它
void display() {
cout << "the no of the studnet is " << no << endl;
cout << "the name of the student is " << name << endl;
for (int i = 0; i < 3; i++) {
cout << i + 1 << "cause get" << scorse[i] << endl;
}
}
public:
~student() {};
};
// 通过作用域解析运算符::来表明student()函数属于student类,
// 这里是在类外定义student类的构造函数,用于初始化student类对象的成员变量。
student::student() {
no = 99999;
name = "太极张三丰";
for (int i = 0; i < 3; i++) {
scorse[i] = 0;
}
}
void student::account() {
cout << "this is a test of the function in class definition! " << endl;
}
int main() {
student s1;
//创建对象,自动调用了构造函数,构造函数在类外定义
s1.account();
s1.display();
return 0
}
构造函数的两种分类方法和三种调用方法
构造函数可以有两种分类方式,按照第一种分类,我们可以分为普通构造函数和拷贝构造函数,也就是除了拷贝构造其他都是普通构造,按照第二种分类,我们可以分为有参构造和无参构造。第二个很好理解,构造函数的形参列表有参数我们就认为是有参构造,没有参数我们就认为是无参构造。但是第一种分类方法的拷贝构造是什么呢?
我们知道在定义构造函数的时候形参列表可以有参数的,就比如在下面这个类里面。
#include <iostream>
using namespace std;
class Test_Class{
public:
int test_Data;
public:
Test_Class(int a){
test_Data = a;
}
};
int main(){
Test_Class Simple(5);
cout<<Simple.test_Data<<endl;
return 0;
}
这段代码中我们定义了一个Test_Class的类,然后我们实例化了一个对象Simple。然后赋值了5(这个属于构造函数的调用方法知识了,这里先暂用一下),最后得到的Simple.test_Data直接等于5。
这个例子中,似乎没什么问题。但是我们将脑洞放开,假设构造函数形参列表是一个对象而不是一个如例子中写的是一个整形数据,那么会发生什么呢?
#include<iostream>
using namespace std;
class Test{
public:
int data;
public:
Test(int a){
data = a;
}
Test(Test &temp){
data=temp.data;
}
};
int main(){
Test a1(4);
Test a2(a1);
cout<<a2.data<<endl;
return 0;
}
实际上,这就是拷贝构造的知识。我们定义了a1这个对象并通过括号法进行了初始化(这里涉及了三种调用方法,我们假设读者已经理解了)。然后通过拷贝构造构造了一个新的对象a2。这是系统会自动调用我们编写的第二个构造函数。这个函数是拷贝构造函数。
注意这个例子中还有一个魔鬼细节,那就是拷贝构造函数的形参列表我们是一个引用。为什么要是引用呢?如果拷贝构造函数的形参不是引用,而是按值传递(如Test temp
),那么在将实参传递给形参时,会调用拷贝构造函数本身来创建这个形参对象。因为按值传递会创建一个新的对象副本,而创建这个副本又会调用拷贝构造函数,这样就会陷入无限递归。
讲到这里我们,实际上拷贝构造函数还有一些要点没有讲。我们后面会进行展开。下面我们对三种调用方法进行讲解(虽然笔者已经假设了两次读者对此十分了解)。
三种调用方法是:括号法(前面我使用过两次),显示法和隐式转换法。还是用上面的例子。
这就是利用括号法调用构造函数来生成一个对象。
#include<iostream>
using namespace std;
class Test{
public:
int data;
public:
Test(int a){
data = a;
}
Test(Test &temp){
data=temp.data;
}
};
int main(){
Test a1(4);
Test a2(a1);
cout<<a2.data<<endl;
return 0;
}
这是显示法来调用拷贝构造函数来调用拷贝构造函数
#include <iostream>
#include <string>
// 定义一个简单的类
class Person {
public:
std::string name;
int age;
// 默认构造函数
Person() {
name = "Unknown";
age = 0;
}
// 带参数的构造函数
Person(const std::string& n, int a) : name(n), age(a) {}
// 拷贝构造函数
Person(const Person& other) {
name = other.name;
age = other.age;
std::cout << "拷贝构造函数被调用" << std::endl;
}
};
int main() {
// 先创建一个Person对象
Person p1("Alice", 25);
// 显式调用拷贝构造函数的方式一:使用对象初始化另一个对象
Person p2 = p1; // 这里会调用拷贝构造函数
// 显式调用拷贝构造函数的方式二:直接在定义对象时调用拷贝构造函数
Person p3(p1); // 同样会调用拷贝构造函数
return 0;
}
这是利用隐式转换法来调用拷贝构造函数
#include <iostream>
// 简单的类
class Point {
private:
int x;
int y;
public:
Point(int a = 0, int b = 0) : x(a), y(b) {}
// 拷贝构造函数
Point(const Point& other) {
x = other.x;
y = other.y;
std::cout << "拷贝构造函数被调用(隐式转换情况)" << std::endl;
}
void print() const {
std::cout << "(" << x << ", " << y << ")" << std::endl;
}
};
int main() {
Point p1(3, 5);
// 隐式转换调用拷贝构造函数
Point p2 = p1;
p2.print();
return 0;
}
小结1
知道现在我们讲了构造函数和析构函数,它们时干嘛用的呢?创建对象用的。创建对象时候需要调用构造函数,而释放内存的时候需要用到析构函数。所以只要创建了对象,那么编译器就调用对应的构造函数(可以发生重载,所以强调对应)。要释放内存就调用类的析构函数(析构函数没有参数所以不能发生重载)。
构造函数和析构函数时一定要有的,如果不手动定义,那么编译器会自动给我们加上。
然后构造函数的重载,从而我们可以灵活的调用构造函数。例子中我们强调拷贝构造函数。
补充
我发现实际上还有一些知识点没说明白。现在一一说明。
构造函数的优先级
系统会自动的给我们补充构造函数时真。但是在无参构造函数,有参构造函数,拷贝构造函数中有优先级,系统的自动补充要遵循优先级。如果我们定义了高级的构造函数,系统就不会自动补充低级的构造函数。但如果我们定义了低级的构造函数,系统会自动补充高级的构造函数。
//总结一下:
//先记着优先级 拷贝构造函数 大于 有参构造函数 大于 无参构造函数
//定义了拷贝构造函数,编译器不提供有参构造函数和无参构造函数
//定义了有参构造函数,编译器会自动提供您拷贝构造函数 但 不会提供无参构造函数
//定义了无参构造函数,编译器不会提供拷贝构造函数和有参构造函数
拷贝构造函数被调用的时机
//C++种拷贝构造函数调用的实际又三种情况
//1、使用一个已经创建完毕的对象来初始化一个新对象
//2、值传递的方式给函数参数传值
//3、以值传递返回局部对象
这么理解这几段注释呢?
class Person {
public:
int m_Age;
public:
Person() {
cout << "Person类的无参函数调用" << endl;
}
Person(int num) {
m_Age = num;
cout << "Person类的有参函数调用" << endl;
}
Person(const Person& p) {
m_Age = p.m_Age;
cout << "Person类的拷贝构造函数调用" << endl;
}
~Person() {
cout << "Person类的析构函数调用" << endl;
}
};
第一种情况时使用一个已经创建完毕的对象来初始化一个新对象。就如下图中所示会调用构造函数。
void test01() {
Person p1 = Person(20);
Person p2(p1);
cout << "the age of p2 is " << p2.m_Age << endl;
}
第二种情况 值传递的方式给函数参数传值。这个时候会调用拷贝构造函数。系统会先调用构造函数创建一个副本,带函数结束之后调用析构函数。如果我们需要避免这样的情况的话,就需要用引用。
void doWork(Person p) {
}
void test02() {
Person p3;
doWork(p3);
}
第三种情况:以值传递返回局部对象。传递的是一个副本,创建副本的过程会调用拷贝构造函数。传递完成了调用析构函数消除副本。
Person doWork2() {
Person p1;
cout << & p1 << endl;
return p1;
}
void test03() {
Person p4 = doWork2();
cout << &p4 << endl;
}
深拷贝和浅拷贝
浅拷贝就是简单的值传递,而深拷贝则是在堆区申请空间来进行拷贝操作。
#include<iostream>
using namespace std;
//这节课学习 深拷贝 和 浅拷贝
//浅拷贝:简单的估值拷贝操作
//深拷贝: 在堆区 重新申请空间 进行拷贝操作
class Person {
public:
Person() {
cout << "Person类的无参构造函数" << endl;
};
Person(int age,int height) {
m_age = age;
m_Herght = new int(height);
//这句的意思是通过new在堆区开辟一片int空间,然后返回地址给成员m_Height
cout << "Person类的有参构造函数" << endl;
};
~Person() {
//变量m_Height是在堆区开辟的变量,需要被手动释放
//在什么时候释放呢,在程序结束之后,也就是用到析构函数的时候
//在析构函数出通过delete关键字来释放空间
if (m_Herght != NULL) {
delete m_Herght;
m_Herght = NULL;
}
cout << "Person类的析构函数" << endl;
};
void show_Age() {
cout << "the name of your input is " << m_age << endl;
}
void show_Height() {
cout << "the height of yout input is " << *m_Herght << endl;
}
//深拷贝 定义拷贝函数
Person(const Person& p) {
cout << "Person类拷贝构造函数的调用" << endl;
m_age = p.m_age;
//m_Herght = p.m_Herght; //这行代码是如果我们不加说明的时候,编译器自己弄的
//但是我们要避免内存问题,不能按照编译器的默认进行定义
//深拷贝操作 利用深拷贝来解决浅拷贝的问题
m_Herght = new int(*p.m_Herght);
}
private:
int m_age;
int *m_Herght;
};
void test01() {
Person p1 (18,160);
p1.show_Age();
p1.show_Height();
//通过拷贝构造函数来定义一个名为p2的对象,
//我们并没有在类的定义里面写构造函数,这是编译器自己自带的
//并且是简单的值传递过程,这个叫浅拷贝
Person p2 (p1);
p2.show_Age();
p2.show_Height();
}
/*test01是一个示例,那就是通过浅拷贝(单纯的值传递)来拷贝
* 定义对象p1,p1中有一个成员是指针 m_Height
*浅拷贝一个新的对象,这个对象是值传递给到p2,p2中所有的数据和p1一模一样
*包括p1的指针。。。。
* 在程序结束后 调用析构函数,析构函数会释放空间
* 栈是后进去的先出来,故p2的指针先被释放 ,然后p1的指针被释放
* 这会导致一块相同的内存被多次释放,出现问题
*/
void tset02() {
//注意到了吗,在定义类里面我们并没有提供拷贝构造函数
//所以test01里面Person p2(p1)这个语句是利用的系统自己携带的拷贝构造函数
//为了避免释放内存空间的问题,在类的定义里面再添加一段函数
//也就是深拷贝
Person p1(18, 160);
p1.show_Age();
p1.show_Height();
Person p2(p1);
p2.show_Age();
p2.show_Height();
}
int main() {
//test01();
//tset02();
return 0;
}
初始化列表
我们通过设计接口,或者手动定义构造函数等方式可以对对象进行初始化操作。但是还可以有其他的方法,这里我们介绍初始化列表。上代码。
#include<iostream>
using namespace std;
/*舒适化列表的作用:用来初始化成员属性
* 语法
*
* 构造函数():属性1(值1),属性2(值2)……{}
*/
class a_class {
private:
int m_a;
int m_b;
int m_c;
public:
//初始化方法一:有参构造函数
/*/a_class(int a, int b, int c) {
m_a = a;
m_b = b;
m_c = c;
}*/
//初始化方法二:初始化列表
a_class(int a,int b,int c):m_a(a),m_b(b),m_c(c){}
};
int main() {
//A_Class p(30, 20, 10);
return 0;
}