C++核心编程(四)—— 类和对象(2) :对象的初始化和清理

C++系列内容的学习目录 → \rightarrow C++学习系列内容汇总

2. 对象的初始化和清理

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

2.1 构造函数和析构函数

  对象的初始化和清理也是两个非常重要的安全问题。如果一个对象或者变量没有初始状态,对其使用后果是未知的;同样,如果使用完一个对象或变量,没有及时清理,也会造成一定的安全问题。

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

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

  • 构造函数: 主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
    构造函数的语法:类名(){}
      1. 构造函数,没有返回值也不写void;
      2. 函数名称与类名相同;
      3. 构造函数可以有参数,因此可以发生重载;
      4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次。

  • 析构函数: 主要作用在于对象销毁前系统自动调用,执行一些清理工作。
    析构函数的语法: ~类名(){}
      1. 析构函数,没有返回值也不写void;
      2. 函数名称与类名相同,在名称前加上符号~
      3. 析构函数不可以有参数,因此不可以发生重载;
      4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次。

  实例如下所示。

#include<iostream>
using namespace std;

//对象的初始化和清理
//1. 构造函数  进行初始化操作
class Person
{
public:
	//1.1 构造函数
	//没有返回值,不用写void
	//函数名与类名相同
	//构造函数可以有参数,可以发生重载
	//创建对象的时候,构造函数会自动调用,而且只调用一次
	Person()
	{
		cout << "Person的构造函数的调用!" << endl;
	}

	//2. 析构函数  进行清理的操作
    //没有返回值,不用写void
	//函数名和类名相同,在名称前面加~
	//析构函数不可以有参数,不可以发生重载
	//对象在销毁前会自动调用析构函数,而且只会调用一次
	~Person()
	{
		cout << "Person的析构函数的调用!" << endl;
	}
};

//构造和析构都是必须有的实现,如果我们自己不提供,编译器会提供一个空实现的构造和析构
void test01()
{
	Person p;  //在栈上的数据,test01执行完毕后,释放这个对象
}

int main()
{
	test01();

	system("pause");

	return 0;
}

Person的构造函数的调用!
Person的析构函数的调用!

2.2 构造函数的分类及调用

  两种分类方式: 1. 按参数分为: 有参构造和无参构造;
          2. 按类型分为: 普通构造和拷贝构造。

  三种调用方式: 1. 括号法; 2. 显示法; 3. 隐式转换法。

  实例如下所示。

#include<iostream>
using namespace std;

//构造函数的分类及调用
//分类:按照参数分类——无参构造(默认构造)和有参构造
//      按照类型分类——普通构造和拷贝构造
class Person
{
public:
	//构造函数
	Person()
	{
		cout << "Person的无参构造函数的调用!" << endl;
	}
	Person(int a)
	{
		age = a;
		cout << "Person的有参构造函数的调用!" << endl;
	}
	//拷贝构造函数
	Person(const Person &p)
	{
		//将传入的人身上的所有属性拷贝到我身上
		age = p.age;
		cout << "Person的拷贝构造函数的调用!" << endl;
	}

	//析构函数  
	~Person()
	{
		cout << "Person析构函数的调用!" << endl;
	}

	int age;
};

//调用
void test01()
{
	//调用的三种方式:
	//1.括号法
	Person p1;    //默认构造函数调用  //1.打印 Person的无参构造函数的调用!  //12.打印 Person的析构函数的调用!
	Person p2(10);  //有参构造函数调用  //2.打印 Person的有参构造函数的调用!  //13.打印 Person的析构函数的调用!
	Person p3(p2);  //拷贝构造函数调用  //3.打印 Person的拷贝构造函数的调用!  //14.打印 Person的析构函数的调用!

	//注意事项1:调用默认构造函数时,不要加()
	//Person p1();   //编译器会认为这是一个函数声明,不会认为在创建对象

	//cout << "p2的年龄:" << p2.age << endl;
	//cout << "p3的年龄:" << p3.age << endl;

	//2.显示法
	Person p4;  //默认构造函数调用  //4.打印 Person的无参构造函数的调用!  //15.打印 Person的析构函数的调用!
	Person p5 = Person(10);  //有参构造函数调用  //5.打印 Person的有参构造函数的调用!  //16.打印 Person的析构函数的调用!
	Person p6 = Person(p5);  //拷贝构造函数调用  //6.打印 Person的拷贝构造函数的调用!  //17.打印 Person的析构函数的调用!

	Person(10);  //匿名对象,特点:当前行执行结束后,系统会立即回收匿名对象  //7.打印 Person的有参构造函数的调用!  //8.打印 Person的无参构造函数的调用!
	cout << "abcde" << endl;  //9.打印 abcde
	//注意事项2:不要利用拷贝构造函数来初始化匿名对象
	//Person(p6);  //编译器会认为Person(p6) === Person p6 是对象声明

	//3.隐式转换法
	Person p7 = 10;  //相当于写了Person p7 = Person(10); 有参构造  //10.打印 Person的有参构造函数的调用!  //18.打印 Person的析构函数的调用!
	Person p8 = p7;  //拷贝构造  //11.打印 Person的拷贝构造函数的调用!  //19.打印 Person的析构函数的调用!

}

int main()
{
	test01();

	system("pause");

	return 0;
}

Person的无参构造函数的调用!
Person的有参构造函数的调用!
Person的拷贝构造函数的调用!
Person的无参构造函数的调用!
Person的有参构造函数的调用!
Person的拷贝构造函数的调用!
Person的有参构造函数的调用!
Person的析构函数的调用!
abcde
Person的有参构造函数的调用!
Person的拷贝构造函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!

2.3 拷贝构造函数的调用时机

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

  1. 使用一个已经创建完毕的对象来初始化一个新对象;
  2. 值传递的方式给函数参数传值;
  3. 以值方式返回局部对象。

  实例如下所示。

#include<iostream>
using namespace std;

//拷贝构造函数的调用时机:
//1.使用一个已经创建完毕的对象来初始化一个新对象
//2.值传递的方式给函数参数传值
//3.值方式返回局部对象

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

	//析构函数  
	~Person()
	{
		cout << "Person的析构函数的调用!" << endl;
	}

	int m_Age;
};

//1.使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
	Person p1(20);  //1.打印 Person的有参构造函数的调用!  //4.打印 Person的析构函数的调用!
	Person p2(p1);  //2.打印 Person的拷贝构造函数的调用!  //5.打印 Person的析构函数的调用!
	cout << "p2的年龄:" << p2.m_Age << endl;   //3.打印 p2的年龄:20
}

//2.值传递的方式给函数参数传值
void doWork1(Person p)
{

}
void test02()
{
	Person p;  //默认构造函数  //6.打印 Person的默认构造函数的调用!  //8.打印 Person的析构函数的调用!
	doWork1(p);  //拷贝构造函数  //7.打印 Person的拷贝构造函数的调用!  //9.打印 Person的析构函数的调用!
}

//3.值方式返回局部对象
Person doWork2()
{
	Person p1;  //10.打印 Person的默认构造函数的调用!  //13.打印 Person的析构函数的调用!(注:析构的是p1,p1是一个局部变量,当doWork2()执行完时就会释放)
	cout << (int*)&p1 << endl;  //11.打印 00F5F610
	return p1;   
	            //12.打印 Person的拷贝构造函数的调用!(注:Person调用一个拷贝构造函数p',给test03()中的p)
}
void test03()
{
	Person p = doWork2();  //15.打印 Person的析构函数的调用!
	cout << (int*)&p << endl;  //14.打印 00F5F708(注:两个p的地址不同,不是同一个对象)
}                              

int main()
{
	test01();
	test02();
	test03();

	system("pause");

	return 0;
}

Person的有参构造函数的调用!
Person的拷贝构造函数的调用!
p2的年龄:20
Person的析构函数的调用!
Person的析构函数的调用!
Person的默认构造函数的调用!
Person的拷贝构造函数的调用!
Person的析构函数的调用!
Person的析构函数的调用!
Person的默认构造函数的调用!
001FF540
Person的拷贝构造函数的调用!
Person的析构函数的调用!
001FF638
Person的析构函数的调用!

2.4 构造函数的调用规则

  默认情况下,C++编译器至少给一个类添加3个函数:

  1. 默认构造函数(无参,函数体为空);
  2. 默认析构函数(无参,函数体为空);
  3. 默认拷贝构造函数,对属性进行值拷贝。

  构造函数的调用规则如下:

  • 如果用户定义有参构造函数,C++不再提供默认无参构造函数,但是会提供默认拷贝构造函数;
  • 如果用户定义拷贝构造函数,C++不会再提供其他构造函数。

  实例如下所示。

#include<iostream>
using namespace std;

//构造函数的调用规则
//1.创建一个类,C++编译器会给每个类都添加至少3个函数
// 默认构造(空实现)
// 析构函数(空实现)
// 拷贝构造函数(值拷贝)

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

	//析构函数  
	~Person()
	{
		cout << "Person的析构函数的调用!" << endl;
	}

	int m_Age;
};

void test01()
{
	Person p;  //1.打印 Person的默认构造函数的调用!  //3.打印 Person的析构函数的调用!
	p.m_Age = 18; 

	Person p2(p);  //4.打印 Person的析构函数的调用!(注:因为把拷贝构造函数注释掉了,编译器会自己提供一个拷贝函数进行值传递,但不会打印“Person的拷贝构造函数的调用!”了)

	cout << "p2的年龄:" << p2.m_Age << endl;  //2.打印 p2的年龄:18
}

//void test02()
//{
//	//如果我们写了有参构造函数,编译器就不再提供默认构造函数,依然会提供拷贝构造函数
//	Person p3;  //如果只留下自己写的有参构造函数,把默认构造函数注释掉,则会报错,因为编译器也不会提供
//	Person p4(28);  用户提供的有参构造函数
//	Person p5(p4);  //如果只留下自己写的有参构造函数,把拷贝构造函数注释掉,可以正常运行,因为编译器会提供
//
//	cout << "p5的年龄:" << p5.m_Age << endl;
//
//	//如果我们写了拷贝构造函数,编译器就不再提供其他普通构造函数了
//	Person p6;  //如果只留下自己写的拷贝构造函数,把默认构造函数注释掉,则会报错,因为编译器也不会提供
//	Person p7(20); //如果只留下自己写的拷贝构造函数,把有参构造函数注释掉,则会报错,因为编译器也不会提供
//	Person p8(p7); //用户自己提供的拷贝构造函数
//}

int main()
{
	test01();
	//test02();

	system("pause");

	return 0;
}

Person的默认构造函数的调用!
p2的年龄:18
Person的析构函数的调用!
Person的析构函数的调用!

2.5 深拷贝与浅拷贝

  浅拷贝: 简单的赋值拷贝操作。

  深拷贝: 在堆区重新申请空间,进行拷贝操作。

  实例如下所示。

#include<iostream>
using namespace std;

//深拷贝和浅拷贝

class Person
{
public:
	//构造函数
	Person()
	{
		cout << "Person的默认构造函数的调用!" << endl;
	}
	Person(int age, int height)
	{
		m_Age = age;
		m_Height = new int(height);  //利用new操作符在堆区开辟数据
		cout << "Person的有参构造函数的调用!" << endl;
	}

	//浅拷贝带来的问题是堆区的内存重复释放,要利用深拷贝来解决此问题
	//自己实现拷贝构造函数解决浅拷贝带来的问题
	Person(const Person &p)
	{
		cout << "Person的拷贝构造函数的调用!" << endl;
		m_Age = p.m_Age;
		//m_Height = p.m_Height;  //编译器默认实现就是这行代码
		//深拷贝操作
		m_Height = new int(*p.m_Height);
	}

	//析构函数  
	~Person()
	{
		//析构函数代码将堆区开辟数据做释放操作
		if (m_Height != NULL) 
		{
			delete m_Height;
			m_Height = NULL;
		}
		cout << "Person的析构函数的调用!" << endl;
	}

	int m_Age;  //年龄
	int *m_Height;  //身高
};

void test01()
{
	Person p1(18, 160);  //1.打印 Person的有参构造函数的调用!  //5.打印 Person的析构函数的调用!
	cout << "p1的年龄:" << p1.m_Age << "    p1的身高:" << *p1.m_Height << endl;  //2.打印 p1的年龄:18    p1的身高:160

	Person p2(p1);  //3.打印 Person的拷贝构造函数的调用!//6.打印 Person的析构函数的调用!
	cout << "p2的年龄:" << p2.m_Age << "    p2的身高:" << *p2.m_Height << endl;  //4.打印 p2的年龄:18    p1的身高:160
}

int main()
{
	test01();

	system("pause");

	return 0;
}

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

  总结: 1. 如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题;
      2. 析构函数代码用于对在堆区开辟的内存做释放操作。

  • 浅拷贝

在这里插入图片描述

  • 深拷贝
    在这里插入图片描述

2.6 初始化列表

  初始化列表的作用: C++提供了初始化列表语法,用来初始化属性。

  初始化列表的语法:构造函数():属性1(值1),属性2(值2)... {}

  实例如下所示。

#include<iostream>
using namespace std;

//初始化列表

class Person
{
public:
	//传统的初始化操作
	//Person(int a, int b, int c)
	//{
	//	m_A = a;
	//	m_B = b;
	//	m_C = c;
	//}

	//初始化列表初始化属性
	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 test01()
{
	//Person p(10, 20, 30);  //传统的初始化操作实例化
	Person p(10, 20, 30);	//初始化列表初始化属性实例化

	cout << "m_A = " << p.m_A << endl;
	cout << "m_B = " << p.m_B << endl;
	cout << "m_C = " << p.m_C << endl;
}

int main()
{
	test01();

	system("pause");

	return 0;
}

m_A = 10
m_B = 20
m_C = 30

2.7 类对象作为类成员

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

class A {}
class B
{
    A a;
}

  B类中有对象A作为成员,A为对象成员。那么当创建B对象时,A与B的构造和析构的顺序是谁先谁后?
  构造的顺序为先调用A的构造,再调用B构造;析构的顺序与构造的顺序相反。

  实例如下所示。

#include<iostream>
using namespace std;
#include<string>

//类对象作为类成员

//手机类
class Phone
{
public:
	Phone(string pName)
	{
		cout << "Phone的构造函数的调用!" << endl;
		m_PName = pName;
	}
	~Phone()
	{
		cout << "Phone的析构函数的调用!" << endl;
	}

	//手机品牌名称
	string m_PName;
};

//人类
class Person
{
public:
	//Phone m_Phone = pName;  //隐式转换法
	Person(string name, string pName):m_Name(name), m_Phone(pName)
	{
		cout << "Person的构造函数的调用!" << endl;
	}

	~Person()
	{
		cout << "Person的析构函数的调用!" << endl;
	}

	//姓名
	string m_Name;
	//手机
	Phone m_Phone;
};

//当类中成员是其他类对象时,我们称该成员为对象成员
//当其它类对象作为本类成员,构造的顺序为:先调用对象成员的构造,再调用本类构造
//析构的顺序与构造相反
void test01()
{
	Person p("张三", "苹果MAX");

	cout << p.m_Name << "拿着" << p.m_Phone .m_PName << endl;
}

int main()
{
	test01();

	system("pause");

	return 0;
}

Phone的构造函数的调用!
Person的构造函数的调用!
张三拿着苹果MAX
Person的析构函数的调用!
Phone的析构函数的调用!

2.8 静态成员

  静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。

  静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化。

  静态成员分为静态成员变量静态成员函数

  • 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  • 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量

  静态成员函数与普通成员函数的区别

  • 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数);
  • 普通成员函数有 this 指针,可以访问类中的任意成员。

  静态成员变量的实例如下所示。

#include<iostream>
using namespace std;

//静态成员变量

class Person
{
public:
	// 1. 所有对象共享同一份数据
    // 2. 在编译阶段分配内存
    // 3. 类内声明,类外初始化
	static int m_A;  //类内声明

	//静态成员变量也是有访问权限的
private:
	static int m_B;
};

int Person::m_A = 100;  //类外初始化
int Person::m_B = 1000;

void test01()
{
	Person p;
	cout << p.m_A << endl;

	Person p2;
	p2.m_A = 200;
	cout << p.m_A << endl;  //所有对象共享同一份数据,由于p2的修改,p也变为200
}

void test02()
{
	//静态成员变量不属于某个对象上,所有对象都共享同一份数据
	//静态变量有两种访问方式:
	// 1. 通过对象进行访问
	Person p;
	cout << p.m_A << endl;

	// 2. 通过类名进行访问
	cout << Person::m_A << endl;
	//cout << Person::m_B << endl;  //私有权限,类外无法访问
}

int main()
{
	test01();
	test02();

	system("pause");

	return 0;
}

100
200
200
200

  静态成员函数的实例如下所示。

#include<iostream>
using namespace std;

//静态成员函数
//所有对象共享同一个函数
//静态成员函数只能访问静态成员变量

class Person
{
public:
	//静态成员函数
	static void func1()
	{
		m_A = 100;  //静态成员函数可以访问静态成员变量,因为m_A是共享的,不需要区分
		//m_B = 200;  //静态成员函数可以访问非静态成员变量,无法区分到底是哪一个对象的m_B属性
		cout << "static void func1的调用!" << endl;
	}

	static int m_A;  //静态成员变量
	int m_B;  //非静态成员变量

	//静态成员函数也是有访问权限的
private:
	static void func2()
	{
		cout << "static void func2的调用!" << endl;
	}
};

int Person::m_A = 0;

//静态成员函数有两种访问方式:
void test01()
{
	//1.通过对象访问
	Person p;
	p.func1();

	//2.通过类名访问(原因是所有对象共享同一个函数,它不属于某一个对象,所以不需要创建对象也可以进行访问)
	Person::func1();
	//Person::func2();  //类外访问不到私有的静态成员函数
}

int main()
{
	test01();

	system("pause");

	return 0;
}

static void func1的调用!
static void func1的调用!

2.9 总结

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值