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++中拷贝构造函数的调用时机通常有三种情况:
- 使用一个已经创建完毕的对象来初始化一个新对象;
- 值传递的方式给函数参数传值;
- 以值方式返回局部对象。
实例如下所示。
#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个函数:
- 默认构造函数(无参,函数体为空);
- 默认析构函数(无参,函数体为空);
- 默认拷贝构造函数,对属性进行值拷贝。
构造函数的调用规则如下:
- 如果用户定义有参构造函数,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的调用!