Day 10 C++面向对象编程 之 对象的初始化和清理

生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用的时候也会删除一些自己的信息数据保证安全。C++中的面向对象来源于生活,每个对象也都会有初始设置以及对象销毁前的清理数据的设置

一、构造函数和析构函数

对象的初始化和清理也是两个非常重要的安全问题:

  • 一个对象或者变量没有初始状态,对其使用的后果是位置的;
  • 使用完一个对象或变量,若没有及时清理,也会造成一定的安全问题

C++利用构造函数和析构函数解决上述两个问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值,进行初始化操作,构造函数都有编译器自动调用,无需手动调用。
  • 析构函数:主要作用在于对象销毁前系统自动调用,进行清理操作,执行一些清理工作。
1. 构造函数语法:类名(){}
  • 构造函数没有返回值也不写void;
  • 函数名称与类(class 类名)名称相同;
  • 构造函数可以有参数,因此可以发生重载;
  • 程序在调用对象时候会自动调用构造函数,无需手动调用,且只会调用一次。
2. 析构函数语法:~类名(){}
  • 析构函数没有返回值也不写void;
  • 函数名称与类名形同,在名称前+符号~;
  • 析构函数不可以有参数,因此不可能发生重载;
  • 程序在对象销毁前会自动调用析构,无需手动调用而且只会调用一次。
#include<iostream>
#include<stdio.h>
using namespace std;

class Person
{
public:
	//1.构造函数,
	//没有返回值,没有void,函数名与类名相同,可以有参数与发生重载,创建对象时会被调用,且只被调用一次
	Person()
	{
		cout << "Person 构造函数的调用" << endl;//若系统自动写的时候,{}里面是没有任何东西的
	}
	//2.析构函数
	//没有返回值,没有void,函数名与类名相同,在名称前面+~,销毁对象时会被调用,不可以有参数和发生重载,只会被调用一次
	~Person()
	{
		cout << "Person的析构函数的调用" << endl;
	}
};

void test01()
{
	Person p;//在栈上的数据,执行完之后,会释放这个对象p
};
int main()
{
	test01();//一次构造一次析构
	//Person p;//只够构造没有析构,但是在main函数执行完之后也会出现析构
	system("pause");
	return 0;
}

对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供这二者,编译器提供的构造函数和析构函数的空实现。

二、构造函数的分类及调用
1. 两种分类方式:
  • 按参数分:有参构造和无参构造
  • 按类型分:普通构造和拷贝构造
class Person
{
public:

	//构造函数
	//普通构造函数:有参构造+无参构造
	Person()//无参构造,也叫默认构造函数,因为我们不写,编译器就会自己提供,提供的就是无参构造
	{
		cout << "构造函数出现" << endl;
	}
	Person(int a)//有参构造
	{
		age = a;
		cout << "构造函数出现" << endl;
	}
	//拷贝构造函数
	Person(const Person &p)
	{
		//把参数的对象的所有属性都赋值一份给另一个新的一个对象,const防止参数对象被修改
		age = p.age;
	}
	//析构函数
	~Person()
	{
		cout << "析构函数出现" << endl;
	}
	int age;
};
2. 三种调用方式:

括号法、显示法、隐式转换法

void test02()
{
	
	//1.括号法
	Person p;//调用默认构造函数,调用默认构造函数的时候不要加括号
	Person p2(7);//调用有参构造函数
	Person p3(p2);//调用拷贝构造函数
	cout << "p2的年龄:" << p2.age << endl;//7
	cout << "p3的年龄:" << p3.age << endl;//7
	
	
	//2.显示法
	Person p1;
	Person p2 = Person(7);//调用有参构造,右边是一个匿名对象,然后给这个匿名对象取名为p2
	Person p3 = Person(p2);//调用拷贝构造
	Person(7);// 为匿名对象,当前行执行结束后,系统会立即回收掉匿名的对象
	
	//3.隐式转换法
	Person p4 = 7;//相当于写了Person p4=Person(10),有参构造
	Person p5 = p4;//拷贝构造

}

在一个函数体内部可以写另一个函数的声明,默认构造函数调用时,不要加括号,若加了括号 Person p(),编译器会认为是一个函数的声明,不会认为在创建对象。匿名对象,匿名对象当前行执行结束后,系统会立即回收掉匿名的对象。不要用拷贝构造函数初始化匿名对象,比如:Person(p3),编译器会认为 Person(p3) ==Person p3;,对象的声明,重定义了

三种构造更推荐第一种括号法,直观明了。

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

C++中拷贝构造函数调用时机通常有三种情况:

  • 使用一个已经创建完毕的对象来初始化另一个新对象
  • 值传递的方式给函数参数传值
  • 以值方式返回局部对象
//拷贝构造函数调用时机
void test03()
{
	Person p1(7);
	//1.使用一个已经创建完毕的对象来初始化另一个新对象
	Person p2(p1);//调用拷贝构造函数
	cout << "p2的年龄:" <<p2.age<< endl;
	cout << "p1的年龄:" <<p1.age<< endl;
}
void doWork(Person p)//值传递,创建副本
{
//2.值传递的方式给函数参数传值
}
void test04()
{
	Person p;//调用默认构造函数,test04函数结束就析构
	doWork(p);//调用拷贝构造函数,test04函数结束就析构
}
//3.以值方式返回局部对象
Person doWork02()
{
	Person p1;//调用默认构造函数
	return p1;//返回,但是使用值的时候要调用拷贝构造函数
}
void test05()
{
	Person p = doWork02();//返回值时,调用拷贝构造函数,拷贝一个新的p1给这个p
}
四、构造函数调用规则
1. 默认情况下,c++编译器至少自动给一个类添加3个函数
  • 默认构造函数(无参,函数体为空);
  • 默认析构函数(无参,函数体为空);
  • 默认拷贝构造函数,对属性进行值拷贝,当使用一个已经创建完毕的对象来初始化另一个新对象:Person p2(p1);时,系统会调用拷贝构造函数,如果用户没有写拷贝构造函数,编译器会提供默认的拷贝构造函数,但这个构造函数只是简单的对值进行拷贝,没有其他作用。
void test03()
{
	Person p1(7);
	//使用一个已经创建完毕的对象来初始化另一个新对象
	Person p2(p1);//调用拷贝构造函数
	cout << "p2的年龄:" <<p2.age<< endl;//7
	cout << "p1的年龄:" <<p1.age<< endl;//7
}

用户写了构造函数

在这里插入图片描述

用户没写构造函数

在这里插入图片描述

2. 构造函数调用规则如下:
  • 如果用户有自己定义的有参构造函数,c++不再提供默认无参构造函数,这个时候再调用无参构造函数的话:Person p,会出错;但是会提供默认拷贝构造;
  • 如果用户有自己定义的拷贝构造函数,c++不会再提供其他构造函数。
五、深拷贝与浅拷贝
  • 深拷贝:在堆区重新申请空间,进行拷贝操作;
  • 浅拷贝:简单的赋值拷贝操作。
class Person
{
...
	~Person()
	{
		//使用new创建的对象一定要释放,要在析构之前被释放,所以,写再析构函数中
		if (m_Height != NULL)
		{
			delete m_Height;
			m_Height = NULL;
		}
		cout << "Person析构函数的调用" << endl;
	}
	int age;
	int *m_Height;//指针,指向的是一个身高,把身高的数据存放到堆区
}
void test04()
{
	Person p1(18,165);
	cout << "P1的年龄为:" << p1.age <<"身高为:"<<*p1.m_Height<< endl;
	Person p2(p1);//自己没有写拷贝构造函数时,编译器会自己提供拷贝构造函数,是浅拷贝
	cout << "P2的年龄为:" << p2.age << "身高为:" << *p2.m_Height<< endl;
}
//会报错
1. 浅拷贝操作:

自己没有写拷贝构造函数时,编译器会自己提供拷贝构造函数,调用拷贝构造函数是是浅拷贝操作。使用new创建的指针存放在堆区,原有空间存放的是堆区地方的地址,而这个地址在堆区中对应的地方存放的是指针所指向的值。使用new创建的对象一定要释放,要在析构之前被释放,所以,写在析构函数中。这时,因为是浅拷贝,所以p2只是简单的把p1中的值赋值过去,这样就造成了p2和p1身高这一个指针指向的是堆区的同一片空间,在函数调用结束之后,会调用析构函数,此时,在解决p1时,就调用了一次析构函数,在析构函数中有我们所写的释放对象的代码,这里,堆区所存放的对象被释放,在析构p2时,又被调用析构函数,又要释放对象,但是指针指向的堆区的对象已经被我们释放了,这里释放就会产生错误(非法访问)。为解决这个问题,就利用深拷贝操作解决。

2. 深拷贝操作:

就在堆区再申请一个空间存放和p1一样的值,复制过去,复制的是指向的值,而不是浅拷贝的“值”,然后p2申请的指针所指的空间就是堆区重新申请的空间,p1所指的空间就是堆区原来的空间

class Person
{...
	Person(const Person &p)
	{
		cout << "Person拷贝构造函数的调用" << endl;
		age = p.age;
		//m_Height = p.m_Height;编译器会默认执行这种指针拷贝操作,是浅拷贝
		m_Height = new int(*p.m_Height);//重新申请空间并把“值(指针指向的值)传递”
	}
	~Person()
	{
		//使用new创建的对象一定要释放,要在析构之前被释放,所以,写再析构函数中
		if (m_Height != NULL)
		{
			delete m_Height;
			m_Height = NULL;
		}
		cout << "Person析构函数的调用" << endl;
	}
	int age;
	int *m_Height;//指针,指向的是一个身高,把身高的数据存放到堆区
};

如果属性有在堆区开辟的,那么一定要自己提供拷贝构造函数,申请新的空间赋值一样的值,要让p1和p2指向不一样的堆区空间,才不会发生二次释放造成非法访问。

六、初始化列表

作用:C++提供了初始化列表的语法,用来初始化属性。语法:构造函数():属性1(值1),属性2(值2)…{}

class Person
{
public:
	/*
	Person(int a,int b,int c)
	{//传统初始化操作
		m_a = a;
		m_b = b;
		m_c = c;
	}
	*/
	//初始化列表初始化属性
	//Person() :m_a(7), m_b(8), m_c(9) {}//固定了,无法灵活修改,写成下面
	Person(int a, int b, int c) :m_a(a), m_b(b), m_c(c)
	{
        //更灵活,不是固定的了
	}
	int m_a;
	int m_b;
	int m_c;
};
void test07()
{
	//传统初始化
	//Person p(7, 8, 9);
	//初始化列表初始化属性
	Person p1(4,5,6);
	cout << "m_a的值:" << p1.m_a << endl;
	cout << "m_b的值:" << p1.m_b << endl;
	cout << "m_c的值:" << p1.m_c << endl;
}
七、类对象作为类成员

c++类中的成员可以是另一个类的对象,我们称为该成员为对象成员。

class A{}
class B
{
    int b;
    A a;//A称为对象成员
}

B类中有对象A作为成员,A为对象成员,创建B类的对象时,会先构造A的对象,会先调用A是构造函数

class Phone
{
public:
	Phone(string pName)//给手机名字赋值,这是方法1,另一种方法就是之前写过的get,set函数
	{
		m_PName = pName;
		cout << "手机的构造" << endl;
	}
	string m_PName;
};
class Person
{
public:
	//m_Phone(pName)相当于Phone m_Phone=pName;隐式转换法
	Person(string name, string pName):m_Name(name),m_Phone(pName)
	{
		//初始化列表进行赋初值的操作
		cout << "人的构造" << endl;
	}
	string m_Name;
	Phone m_Phone;
};
void test08()
{
	Person p("张三", "HONOR");
	cout << "名字:" << p.m_Name << "手机:" << p.m_Phone.m_PName << endl;
}

析构的时候,先析构B的,再析构A的,与构造的顺序相反。

八、静态成员

静态成员就是再成员变量和成员函数前加上关键字static,称为静态成员。静态成员分为:

1. 静态成员变量
  • 所有对象共享同一份数据,成员数据只有一份,别人改了,你用的时候就是改之后的数据;
  • 在编译阶段分配内存,编译阶段(程序还没运行之前就被分配了),在全局区;
  • 类内声明,类外初始化,必须要有初始值,否则不能使用
class Person
{
public:
	//本句为类内声明
	static int m_a;//变成静态成员变量,1.所有对象都共享同一份数据,2.编译阶段就分配内存,3.类内声明,类外初始化操作
private:
    static int m_b;
};
int Person::m_a = 100;//本句为类外初始化,去掉static,并加上Person::来表示这个是Person里面的成员
int Person::m_b = 7;//不可被访问
void test09()
{
	Person p;
	cout << p.m_a << endl;//100
	Person p2;
	p2.m_a = 200;//虽然的不同的对象,但是m_a是静态成员变量,所有对象都共享,所以这里100被改成200
	cout << p.m_a << endl;//200
}
void test10()
{
	//静态成员变量,不属于某个对象,所有对象都共享同意扽数据
	//因此静态成员变量有两种访问方式
	//1.通过对象进行访问
	Person p;
	cout << p.m_a << endl;//100
	//2.通过类名进行访问
	cout <<  Person::m_a << endl;//100
}

静态成员变量也是有访问权限的。public作用域下的可以访问,私有左右用类外不可访问。

2. 静态成员函数
  • 所有对象共享同一个函数
  • 静态成员函数只能访问静态成员变量
class Person
{
public:
	static void func()//静态成员函数
	{
		cout << "静态成员函数的调用" << endl;
		//m_a = 7;//错误,静态成员函数不可以访问非静态成员变量,无法区分到底是哪个对象的m_a
		m_b = 7;//正确,静态成员函数可以访问静态成员变量
		
	}
	int m_a;
	static int m_b;
};
void test11()
{
	//1.通过对象进行访问
	Person p;
	p.func();
	//2.通过类名进行调用
	Person::func();
}

静态成员函数也是有访问权限的,私有作用域下的静态成员函数类外访问不到,不可以访问。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐