C++类和对象(二):对象的初始化和清理


1 构造函数和析构函数

1.1 对象初始化和清理的背景

背景:对象的初始化和清理涉及程序安全。
①对象或变量使用前若未初始化,则程序运行结果存在不确定性,即不可预测
②对象或变量使用完毕后若未及时清理,则会导致内存泄露


1.2 构造函数和析构函数的特点

C++中,创建对象时会调用构造函数进行初始化设置;销毁对象时会调用析构函数进行数据清理设置。

C++对象初始化(构造函数)和清理(析构函数)的特点
(1)编译器会强制执行对象的初始化和清理工作;
(2)编译器会自动调用构造函数和析构函数,无须手动调用
(3)若程序员未自定义构造函数和析构函数,则编译器会提供空实现的构造函数和析构函数,即函数体代码为空

构造函数和析构函数的作用
构造函数创建对象时为对象的成员属性赋值,由编译器自动调用,无须手动调用。
析构函数:对象销毁前由编译器自动调用,执行数据清理工作。


1.3 构造函数(可重载)

作用创建对象时为对象的成员属性赋值,由编译器自动调用,无须手动调用。
语法类名(){...}
特点
(1)构造函数无返回值,且不写void
(2)函数名与类名相同
(3)构造函数可包含参数,即可函数重载
(4)创建对象时编译器会自动调用构造函数,无须手动调用,且仅调用一次

注1:若在类外通过调用构造函数创建对象,其访问权限需为公共权限public
注2:C++和Java创建对象的区别:
C++创建对象时,会自动调用构造函数,无需显式调用构造函数,如Person p;即会开辟堆区空间;
Java创建对象时,必须使用new操作符显式手动调用构造方法,才会开辟堆区空间,如Person p = new Person();;否则Person p;仅创建空引用(空的对象引用),未指向/绑定任何对象,不会开辟堆区空间,运行时编译器提示:尚未初始化变量。


1.4 析构函数(不可重载)

作用:对象销毁前由编译器自动调用,执行数据清理工作(如释放堆区数据)。
语法~类名(){...}
特点
(1)析构函数无返回值,且不写void
(2)函数名与类名相同,且在函数名前使用波浪号~
(3)析构函数不可包含参数,即不可函数重载
(4)对象销毁前编译器会自动调用析构函数,无须手动调用,且仅调用一次

注1:若类的成员变量需在堆区开辟内存空间,则需要:
自定义析构函数释放成员变量申请的堆区内存
使用深拷贝自定义拷贝构造函数,防止默认拷贝构造函数的浅拷贝导致重复释放内存。
注2:在堆区创建的对象,对对象指针使用delete操作符后,调用自定义的析构函数

示例1:在main函数中创建对象

#include <iostream>
using namespace std;

class Object
{
    
    
public:
	//自定义构造函数
	Object()
	{
    
    
		cout << "构造函数被自动调用" << endl;
	}

	//自定义析构函数
	~Object()
	{
    
    
		cout << "析构函数被自动调用" << endl;
	}
};

int main() {
    
    
	//创建对象时:“构造函数被自动调用”
	Object obj;	

	system("pause");	//任意键继续【main函数未执行完毕】
	
	//销毁对象前(按下任意键后):“析构函数被自动调用”

	return 0;
}

示例2:在全局函数中创建对象,在main函数中调用该全局函数

#include <iostream>
using namespace std;

class Object
{
    
    
public:
	//自定义构造函数
	Object()
	{
    
    
		cout << "构造函数被自动调用" << endl;
	}

	//自定义析构函数
	~Object()
	{
    
    
		cout << "析构函数被自动调用" << endl;
	}
};

/* 全局函数中创建对象 */
void func(){
    
    
	Object obj;
}

int main() {
    
    
	/* 函数执行时,创建对象;函数结束后,销毁对象 */
	func();		//“构造函数被自动调用”	“析构函数被自动调用”

	system("pause");	//任意键继续【main函数未执行完毕】
	return 0;
}

2 构造函数的分类及调用方式

分类方式
(1)按参数划分:无参构造(默认构造)有参构造
(2)按类型划分:普通构造拷贝构造

拷贝构造函数:使用同类对象初始化新创建的对象。
语法类名(const 类名 &对象引用名){...},例如:Object(const Object &obj){...}
特点
(1)构造函数的形参传入实参同类对象拷贝其属性对新对象进行初始化赋值;
(2)构造函数的形参中被传入的对象不能被修改,使用const关键字限定为只读权限
(3)构造函数的形参使用引用(别名)接收对象变量,可避免数据拷贝,减少内存占用。

调用方式
(1)小括号调用带参构造函数拷贝构造函数
语法类名 对象名(参数值..);,如Object obj(param...);

注:调用默认无参构造函数时,不能使用小括号,如Object obj;
若使用小括号且无参,则编译器会判定为函数声明,如Object obj();,即返回类型为Object类型的函数obj()(函数体内部可包含函数声明)。

Object o;			//调用无参构造函数
Object obj(1);		//调用带参构造函数
Object obj2(obj);	//调用拷贝构造函数使用已有对象obj创建对象obj2

(2)显式初始化调用带参构造函数拷贝构造函数
语法类名 对象名 = 类名(参数值..);,如Object obj = Object(param);

注1:Java中显式初始化需使用关键字new,如Object obj = new Object(args...);
C++中显式初始化不使用关键字new
注2:表达式Object obj = Object(args);中,赋值符的右侧部分Object(args)可表示匿名对象。当匿名对象所在语句执行完毕后,系统会立即回收匿名对象。
注3:不能使用拷贝构造函数初始化匿名对象,编译器会认为是对象的声明,否则编译器报错:(当前对象)重定义。编译器内部若使用已有对象obj,通过拷贝构造函数创建匿名对象Object(obj);,等价于声明对象Object obj;导致对象obj被重复定义。

Object obj = Object(1);		//调用带参构造函数
Object obj2 = Object(obj);	//通过拷贝构造函数使用已有对象obj创建非匿名对象obj2

/* 不能使用拷贝构造函数初始化匿名对象 */ 
//编译器报错:"Object obj2"重定义
//Object(obj);	//错误:通过拷贝构造函数使用已有对象obj创建匿名对象,等价于Object obj;

(3)隐式转换法:对于单参数构造函数,赋值符右侧直接使用传入数据,相当于调用带参构造函数拷贝构造函数
语法类名 对象名 = 参数值 或 对象名;,如Object obj = 1;Object obj2 = obj;

Object obj = 1;		//Object obj = Object(1);		//相当于调用带参构造函数
Object obj2 = obj;	//Object obj2 = Object(obj);	//相当于调用拷贝构造函数

示例

#include <iostream>
using namespace std;

class Person{
    
    
public:
	int _age;

	//无参构造
	Person() {
    
    
		cout << "调用无参构造函数" << endl;
	}

	//带参构造
	Person(int age) {
    
    
		_age = age;
		cout << "调用带参构造函数" << endl;
	}

	//拷贝构造函数
	Person(const Person& p) {
    
    
		_age = p._age;
		cout << "调用拷贝构造函数" << endl;
	}

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


int main() {
    
    
	/* 调用方式1:小括号调用 带参构造函数 或 拷贝构造函数 */
	Person p;			//无参构造
	Person p11(10);		//带参构造
	Person p12(p11);	//拷贝构造


	/* 调用方式2:显式初始化调用 带参构造函数 或 拷贝构造函数 */
	Person p21 = Person(10);	//带参构造
	Person p22 = Person(p21);	//拷贝构造
	
	//不能使用拷贝构造函数创建匿名对象,报错:对象重定义
	//Person(p22);		//等价于对象声明 Person p22; 重复定义对象p22


	/* 调用方式3:隐式转换法调用 带参构造函数 或 拷贝构造函数 */
	Person p31 = 10;	//带参构造
	Person p32 = p31;	//拷贝构造

	return 0;
}

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

C++中使用拷贝构造函数的3类场景:
(1)使用已创建完毕的对象初始化一个新对象,会调用拷贝构造函数;【最常用】

Object obj;		//使用无参构造创建对象obj
Object object(obj);	//调用拷贝构造函数使用对象o创建新对象object

(2)通过值传递的方式向函数形参传递对象,会调用拷贝构造函数,创建实参对象的拷贝并使用形参接收;

void func(Object object){
    
    ...}
Object obj;		//使用无参构造创建对象obj
func(obj);		//值传递:向函数func()传递实参obj对象时,调用拷贝构造函数

注:实参obj对象和接收实参的形参object对象是两个不同的对象,object对象的修改不会影响obj对象。

(3)函数中以值方式返回函数内部创建的局部对象,会调用拷贝构造函数,创建局部对象的拷贝(局部对象会被释放)。

Object func(){
    
    
	Object obj;	//在函数内部创建局部对象
	return obj;	//返回局部对象时会调用拷贝构造函数创建一份局部对象的拷贝;局部对象被释放
}

Object object = func();	//接收函数的返回值(对象类型)

示例

#include <iostream>
using namespace std;

class Person {
    
    
public:
	int _age;

	//无参构造
	Person() {
    
    
		cout << "调用无参构造函数" << endl;
	}

	//带参构造
	Person(int age) {
    
    
		_age = age;
		cout << "调用带参构造函数" << endl;
	}

	//拷贝构造函数
	Person(const Person& p) {
    
    
		_age = p._age;
		cout << "调用拷贝构造函数" << endl;
	}

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

/* 场景2:通过值传递的方式向函数形参传递实参对象,会调用拷贝构造函数 */
void func1(Person person) {
    
    
	person._age = 50;
	cout << "接收实参中对象变量的形参对象地址:" << &person << endl;
}

/* 场景3:函数中以值方式返回函数内部创建的局部对象,会调用拷贝构造函数 */
Person func2() {
    
    
	Person temp;
	cout << "函数中局部对象的地址:" << &temp << endl;
	return temp;
}


int main() {
    
    
	/* 场景1:使用已创建的对象去初始化新对象 */
	//Person p11;
	//Person p12(p11);


	/* 场景2:通过值传递的方式向函数形参传递实参对象,会调用拷贝构造函数 */
	//Person p2;
	//cout << "实参对象p2的地址:" << &p2 << endl;
	//func1(p2);
	/* 场景2输出结果:
		调用无参构造函数
		实参对象p21的地址:00B9F7F8
		调用拷贝构造函数
		接收实参中对象变量的形参对象地址:00B9F714
		调用析构函数
		调用析构函数
	*/


	/* 场景3:函数中以值方式返回函数内部创建的局部对象,会调用拷贝构造函数 */
	Person p3 = func2();
	cout << "函数返回局部对象p3的地址:" << &p3 << endl;
	/* 场景3输出结果:
		调用无参构造函数
		函数中局部对象的地址:0057F6F8
		调用拷贝构造函数
		调用析构函数
		函数返回局部对象p3的地址:0057F7FC
		调用析构函数
	*/

	return 0;
}

4 构造函数的调用规则

创建一个类时,C++编译器默认会对类添加至少3个函数
(1)默认构造函数(无参,函数体为空,即空实现)
(2)默认析构函数(无参,函数体为空,即空实现)
(3)默认拷贝构造函数(对属性进行值拷贝

构造函数的调用规则
(1)若用户自定义带参构造函数,C++编译器不再提供默认无参构造函数(需用户显式提供无参构造),但会提供默认拷贝构造函数属性值拷贝)。

注:若用户自定义带参构造函数,且未定义无参构造函数时,由于编译器不再提供默认无参构造函数,调用无参构造函数时,编译器报错:(XX类)没有合适的默认构造函数可用类“XX”不存在默认构造函数

#include <iostream>
using namespace std;

class Person {
    
    
public:
	int _age;

	//自定义带参构造
	Person(int age) {
    
    
		_age = age;
		cout << "调用自定义带参构造函数" << endl;
	}

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

int main() {
    
    
	/* 若用户自定义带参构造函数,则编译器不再提供默认无参构造 */
	//无法访问默认无参构造函数
	//Person p;	//报错:类“Person”不存在默认构造函数;“Person”没有合适的默认构造函数可用

	/* 若用户自定义带参构造函数,则编译器仍会提供默认默认拷贝构造函数(值拷贝) */
	//可以访问默认拷贝构造函数
	Person p1(18);
	Person p2(p1);
	cout << p2._age << endl;	//18

	return 0;
}

(2)若用户自定义拷贝构造函数,C++编译器不再提供其它普通构造函数(默认无参构造和带参构造均不提供)。

注:若用户自定义拷贝构造函数,且未定义其它普通构造函数(含无参构造和带参构造)时,由于编译器不再提供默认其它普通构造函数,调用其它普通构造函数时,编译器报错:(XX类)没有与参数列表匹配的构造函数

#include <iostream>
using namespace std;

class Person {
    
    
public:
	int _age;

	//自定义拷贝构造函数
	Person(const Person& person) {
    
    
		cout << "自定义拷贝构造函数" << endl;
		_age = person._age;
	}


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

int main() {
    
    
	/* 若用户自定义拷贝构造函数,则编译器不再提供其它普通构造函数 */
	//无法访问默认无参构造函数
	//Person p1;		//报错:类“Person”不存在默认构造函数;“Person”没有合适的默认构造函数可用
	
	//无法访问未定义的带参构造函数
	//Person p2(18);	//报错:没有与参数列表匹配的构造函数;无法将参数1从 int 转换为 const Person &

	return 0;
}

5 浅拷贝与深拷贝

浅拷贝:简单的赋值拷贝(属性值拷贝)。
深拷贝:在堆区申请新的内存空间,前后两块堆区内存的地址不同。

默认拷贝构造函数 VS 自定义拷贝构造函数
默认拷贝构造函数:采用浅拷贝,简单拷贝属性值。
使用浅拷贝的默认拷贝构造函数,会导致指针类型的成员变量指向的(堆区)内存地址相同,对象释放时堆区内存被重复释放(再次释放已释放的内存,为非法操作),导致程序运行崩溃

自定义拷贝构造函数:采用深拷贝,通过解引用操作new操作符,使指针类型的成员变量指向的内存地址不同、对应值相同,即调用拷贝构造函数时,会重新申请一块堆区内存空间,创建的对象不再指向相同的堆区内存地址
例如:pField = new int(*obj.pField);解引用获取指针变量对应的值;new操作符重新申请堆区内存空间。

注:若类的成员变量需在堆区开辟内存空间,则需要:
自定义析构函数释放成员变量申请的堆区内存
使用深拷贝自定义拷贝构造函数,防止默认拷贝构造函数的浅拷贝导致重复释放内存。

示例

#include <iostream>
using namespace std;

class Student {
    
    
public:
	int _age;
	int *pScore;	//指针类型的成员变量

	//自定义无参构造
	Student() {
    
    
		cout << "调用自定义无参构造函数" << endl;
	}

	//自定义带参构造
	Student(int age, int score) {
    
    
		_age = age;
		//new操作符返回堆区内存的地址,使用指针类型成员变量接收
		pScore = new int(score);

		cout << "调用自定义带参构造函数" << endl;
	}

	//自定义拷贝构造函数
	Student(const Student& stu) {
    
    
		_age = stu._age;
		/* 默认拷贝构造的浅拷贝-属性值拷贝,会导致指向的堆区内存为同一个,析构时重复释放出错 */
		//默认拷贝构造函数-浅拷贝:简单属性值拷贝
		//pScore = stu.pScore;	//运行出错

		//自定义拷贝构造函数-深拷贝:使用new操作符申请堆区内存空间
		pScore = new int(*stu.pScore);	//对指针变量解引用获取对应的值

		cout << "调用自定义拷贝构造函数" << endl;
	}

	//自定义析构函数:释放堆区内存
	~Student() {
    
    
		if (pScore != NULL) {
    
    
			delete pScore;	//delete操作符释放堆区内存
			pScore = NULL;	//指针置为NULL,防止出现野指针
		}

		cout << "调用自定义析构函数" << endl;
	}
};


int main() {
    
    
	/* 
		默认拷贝构造函数采用浅拷贝,指针类型成员变量被赋值相同的地址值,指向同一块内存地址;
		程序运行结束后,对象被释放,导致同一块内存被多次释放,导致运行出错。
		需自定义拷贝构造函数,并采用深拷贝,重新申请一块堆区内存空间,创建的对象不再指向同一块内存地址。
	*/
	//使用自定义带参构造函数创建对象
	Student stu1(18, 99);

	//使用默认 / 自定义拷贝构造函数创建对象
	Student stu2(stu1);

	return 0;
}

6 构造函数的初始化列表

作用:构造函数的初始化列表用于初始化属性
用法:初始化列表以冒号开头,后跟一系列以逗号分隔的初始化字段
语法
(1)构造函数():成员属性1(初始值1),成员属性2(初始值2)... {}
例:Object():fieldA(10), fieldB(20), fieldC(30)...{}
(2)构造函数(形参列表):成员属性1(形参1),成员属性2(形参2)... {}
例:Object(int x, int y, int z):fieldA(x), fieldB(y), fieldC(z)...{}

注:构造函数的初始化列表可结合隐式转换法,创建对象成员

示例

#include <iostream>
using namespace std;

class Object {
    
    
private:
	int A;
	int B;
	int C;

public:
	/*
	//构造函数显式初始化
	Object(int a, int b, int c) {
		A = a;
		B = b;
		C = c;
	}
	*/

	//初试化列表-固定初始值
	Object() :A(1), B(2), C(3) {
    
    }
	//初始化列表-显式赋值
	Object(int x, int y, int z) :A(x), B(y), C(z) {
    
    }

	void print() {
    
    
		cout << "A:" << A << endl;
		cout << "B:" << B << endl;
		cout << "C:" << C << endl;
	}
};

int main() {
    
    
	Object obj1;
	obj1.print();		//1  2  3

	Object obj2(6,7,8);
	obj2.print();		//6  7  8

	return 0;
}

7 对象成员:类对象作为类成员

类中成员是另一个类的对象,则该成员为对象成员

注:构造函数的初始化列表可结合隐式转换法,创建对象成员

例:类B中包含成员类A的对象,即对象成员。

class A {
    
    };
class B
{
    
    
    A a;
};

类对象和对象成员的调用顺序
当其它类的对象作为本类成员(对象成员)时,
(1)构造函数的调用顺序:先调用对象成员的构造函数创建对象成员,再调用本类构造函数创建本类对象;
(2)析构函数的调用顺序:先调用本类的析构函数释放本类对象,再调用对象成员的析构函数释放对象成员。

注:构造函数和析构函数的调用时机
①对象成员的构造函数→创建对象成员
②本类的构造函数→创建本类对象
③本类的析构函数→释放本类对象
④对象成员的析构函数→释放对象成员

示例

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

class Car {
    
    
public:
	string brandName;

	Car(string bName) {
    
    
		brandName = bName;
		cout << "Car类的构造函数" << endl;
	}

	~Car() {
    
    
		cout << "Car类的析构函数" << endl;
	}
};

class Person {
    
    
public:
	string name;
	Car car;

	//初始化列表与隐式转换法 创建对象:Car car = bName; 等价于 Car car = Car(bname); 
	Person(string pName, string bName):name(pName),car(bName) {
    
    
		cout << "Person类的构造函数" << endl;
	}

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

int main() {
    
    
	Person p("Tom", "Tesla");
	cout << "姓名:" << p.name << ",座驾:" << p.car.brandName << endl;
	/* //输出结果
		Car类的构造函数
		Person类的构造函数
		姓名:Tom,座驾:Tesla
		Person类的析构函数
		Car类的析构函数
	*/

	return 0;
}

8 explicit关键字

作用:修饰单参数构造函数,使用explicit关键字禁止通过隐式转换创建对象。

注:使用explicit关键字修饰单参数构造函数后,隐式转换时编译器报错:不存在从(单参数数据类型)转换到(类类型)的适当构造函数

示例

#include <iostream>
using namespace std;

class Object1 {
    
    
private:
	int var;

public:
	//单参数构造函数,不使用explicit关键字
	Object1(int value) {
    
    
		var = value;
	}
};

class Object2 {
    
    
private:
	int var;

public:
	//单参数构造函数,使用explicit关键字
	explicit Object2(int value) {
    
    
		var = value;
	}
};

int main() {
    
    
	//隐式转换,等价于Object1 obj1 = Object1(10);
	Object1 obj1 = 10;		//正确

	//对单参数构造函数使用explicit关键字后,不能通过隐式转换法创建对象
	//Object2 obj2 = 20;	//错误:不存在从int转换到Object2的适当构造函数

	return 0;
}

9 动态对象创建(new、delete操作符)

C++创建对象时,需完成的动作:
(1)为对象分配内存
(2)调用构造函数初始化内存

注:若使用未初始化的对象,可能导致程序出错。

C++中可使用new/delete操作符malloc/free标准库函数创建对象。

C语言动态内存分配的方法
兼容C语言的malloc/calloc/realloc库函数free库函数申请和释放内存时,存在如下问题:
(1)申请内存时,必须先确定对象的长度,如malloc(sizeof(Object));
(2)malloc返回void*类型的指针,C++中不允许将void*类型赋值给其它类型指针,必须强制转换
例:Object *obj = (Object*)malloc(sizeof(Object)); //确定对象长度,并强转void*类型
(3)malloc申请内存时可能失败,必须判空处理以确保内存分配成功。

Object *obj = (Object*)malloc(sizeof(Object));
if(obj == NULL){
    
    
	return 0;
}

(4)必须显式地初始化和释放内存。

注:C语言中不存在构造函数或析构函数,需自行封装函数手动显式调用,用于初始化和释放内存。


9.1 new操作符

使用new创建对象时,会在堆区为对象分配内存调用构造函数完成初始化,返回堆区内存地址(可使用指向对应类型对象的指针接收)。

注:使用new操作符创建对象时:
(1)返回指向该类对象的指针,而不是void *类型,无需强制转换
(2)可确保堆区内存分配成功;
(3)可自动调用构造函数完成初始化,无需手动显式调用自行封装的初始化函数。

例:
使用new操作符创建对象

Object *obj = new Object;	//返回Object*类型的指针,无需强转

使用malloc创建对象

Object *obj = (Object*)malloc(sizeof(Object));	//malloc分配内存,强转void*指针

if(obj == NULL){
    
    	//判断内存是否分配成功
	return 0;
}

obj->Init();		//自行封装的初始化函数

开辟堆区变量数据类型 *指针名 = new 数据类型(初始值);
开辟堆区数组数据类型 *指针名 = new 数据类型[数组长度];

注1:若在堆区开辟自定义类型的数组,则该类必须包含默认构造函数。否则,编译器报错:没有合适的默认构造函数可用
注2:若在栈区开辟自定义类型的数组,可以不包含默认构造函数

/* 开辟堆区数组 */
int *pInt = new int[10];		//堆区整型数组
char *pChar = new char[64];		//堆区字符型数组

/* 在堆区开辟自定义类型的数组,则该类必须包含【无参构造函数】 */
//在堆区创建自定义类型数组时,会多次(数组长度)调用构造函数进行初始化
Object *objs = new Object[10];	//堆区自定义类型数组

9.2 delete操作符

使用delete释放对象时,会先调用析构函数再释放内存

注1:molloc函数free函数new操作符delete操作符必须配对使用
注2:delete只用用于释放new创建的对象,而不能释放由malloc/calloc/realloc创建的对象,否则该行为是未定义的。new操作符delete操作符的内部使用了molloc函数free函数,若错误使用,可能会导致在调用析构函数前即释放内存。
注3:若删除的对象的指针为NULL,则无任何影响。建议在删除指针后立即将指针赋值并置为NULL,避免多次删除对象指针时产生问题(多次删除NULL指针则无影响)。


释放堆区变量delete 指针名;
释放堆区数组delete[] 指针名;

示例

#include <iostream>
using namespace std;

class Teacher{
    
    
public:
	Teacher() {
    
    
		cout << "默认无参构造函数" << endl;
	}

	Teacher(int age) {
    
    
		_age = age;
		cout << "带参构造函数" << endl;
	}

	Teacher(const Teacher &t) {
    
    
		_age = t._age;
		cout << "拷贝构造函数" << endl;
	}

	~Teacher() {
    
    
		cout << "默认析构函数" << endl;
	}

	int _age;
};

//在堆区创建和释放对象
void func1() {
    
    
	Teacher *p = new Teacher(15);
	delete p;
}

//不要使用 void*类型接收 new操作符 创建的对象,否则使用delete操作符时不会执行析构函数。
void func2() {
    
    
	void* p = new Teacher(15);
	delete p;	//不会调用析构函数
}

//在堆区开辟数组
void func3() {
    
    
	int* pInt = new int[3];
	char* pChar = new char[3];

	/* 若在堆区开辟自定义类型的数组,则该类必须包含默认构造函数 */
	Teacher* teachers = new Teacher[3];	//在堆区创建对象数组
	delete[] teachers;	//释放堆区对象数组
}

int main() {
    
    
	//func1();
	//func2();
	func3();

	return 0;
}

9.3 molloc/free和new/delete的区别

molloc/free和new/delete的区别
(1)malloc/free属于库函数;new/delete属于运算符
(2)molloc返回类型为void *,需强制转换;new返回类型为相应数据类型的指针,无需强制转换。
(3)molloc对应free;new对应delete。
(4)molloc 不会调用构造函数;new 调用构造函数。
(5)free 不会调用析构函数;delete 调用析构函数。
(6)malloc需要计算分配的内存大小;new不需要计算分配的内存大小。

注:不要使用void*类型接收new操作符创建的对象,否则使用delete操作符不会执行析构函数

void *p = new Object;
delete p;	//若使用void*类型接收new创建的对象,delete不会执行析构函数

10 静态成员

静态成员:在成员变量成员函数前使用关键字static修饰。


10.1 静态成员变量

静态成员变量:被关键字static修饰的成员变量。
(1)所有对象共享同一个静态成员变量,并非某个对象独有,即内存中仅存在一份数据
(2)在编译阶段分配内存(程序运行前),位于全局区
(3)在类内声明,在类外初始化,即静态变量必须具有初始值后才能使用

注:类的大括号内类的作用域内均属于类的内部,在类的大括号外使用类名::成员可访问类的私有化成员私有化构造函数private修饰)。

class Object{
    
    
	static int field;	//类内声明
};

int Object::field = 10;	//类外初始化

静态成员变量的访问方式
(1)通过对象访问:对象.静态成员变量
(2)通过类名访问:类名::静态成员变量

注:静态成员变量存在访问权限:
private修饰的静态成员变量在类外无法访问;
public修饰的静态成员变量在类外可访问。

示例:静态成员变量

#include <iostream>
using namespace std;

class Object {
    
    
/* 静态成员变量存在访问权限:private修饰的在类外无法访问 */
public:
	static int fieldA;

private:
	static int fieldB;
};

/* 静态成员变量的初始化 */
int Object::fieldA = 1;
int Object::fieldB = 2;

int main() {
    
    
	/* 静态成员变量:所有对象共享同一份数据 */
	Object obj1;
	Object obj2;
	obj2.fieldA = 10;
	
	//通过对象访问静态成员变量
	cout << obj1.fieldA << endl;	//10
	cout << obj2.fieldA << endl;	//10
	//通过类名访问静态成员变量
	cout << Object::fieldA << endl;	//10

	/* 静态成员变量存在访问权限:private修饰的在类外无法访问 */
	//cout << Object::fieldB << endl;	//不可访问

	return 0;
}

10.2 静态成员函数

静态成员函数:被关键字static修饰的成员函数。
(1)所有对象共享同一个静态成员函数
(2)静态成员函数只能访问静态成员变量,不可访问非静态成员变量。

注:静态成员函数中不可访问非静态成员变量,无法区分非静态成员变量属于某个对象,编译器报错:非静态成员引用必须与特定对象相对

静态成员函数的访问方式
(1)通过对象访问:对象.静态成员函数()
(2)通过类名访问:类名::静态成员函数()

注:静态成员函数存在访问权限:
private修饰的静态成员函数在类外无法访问;
public修饰的静态成员函数在类外可访问。

示例:静态成员函数

#include <iostream>
using namespace std;

class Object {
    
    
public:
	//静态成员函数
	static void func() {
    
    
		/* 静态成员函数可访问静态成员变量,不可访问非静态成员变量 */
		s_field = 10;
		//field = 20;	//报错:非静态成员引用必须与特定对象相对
		cout << "静态成员函数调用" << endl;
	}
	
	static int s_field;	//静态成员变量
	int field;			//非静态成员变量

private:
	/* 静态成员函数存在访问权限:private修饰的在类外无法访问 */
	static void func2() {
    
    
		cout << "静态成员函数调用" << endl;
	}
};

int Object::s_field = 1;

int main() {
    
    
	/* 静态成员函数:所有对象共享同一份数据 */
	Object obj1;
	Object obj2;

	//通过对象访问静态成员变量
	obj1.func();
	obj2.func();
	//通过类名访问静态成员变量
	Object::func();

	/* 静态成员函数存在访问权限:private修饰的在类外无法访问 */
	//Object::func2();	//不可访问

	return 0;
}

10.3 静态成员的应用:单例模式

单例模式:可保证一个类有且仅有一个实例,且该实例在外部易于访问。通过控制实例对象的个数,节省系统资源。

特点
(1)对外提供静态成员函数getInstance()公共接口,方便外部获取唯一实例,且将实例限定为只读状态(避免外部将唯一实例置为NULL,导致无法再访问);
(2)为防止外部通过构造函数创建对象(实例化),将默认构造函数拷贝构造函数私有化

示例:单例模式案例-Boss类

#include <iostream>
using namespace std;

class Boss {
    
    
private:
	/* 1.默认构造函数私有化 */
	//防止外部通过构造函数创建对象
	Boss() {
    
    }

	/* 2.默认拷贝构造函数私有化 */
	Boss(const Boss& b) {
    
    }

	/* 3.静态成员变量:创建指向对象的指针,指向唯一的实例 */
	static Boss* boss;

public:
	/* 4.对外提供公共接口,获取唯一的实例,且限定为只读状态 */
	//若未限定为只读状态,外部可能会将唯一实例置为 NULL
	//静态成员函数只能访问静态成员变量,不可访问非静态成员变量
	static Boss* getInstance() {
    
    
		return boss;
	}
};

/* 5.初始化唯一的实例:类的作用域内等价于类中,可访问私有构造函数 */
Boss* Boss::boss = new Boss;

int main() {
    
    
	//通过类名访问静态方法
	Boss* b1 = Boss::getInstance();
	Boss* b2 = Boss::getInstance();
	cout << ((b1 == b2) ? "b1 == b2" : "b1 != b2") << endl;		//b1 == b2,即指向同一个对象

	/*
	//默认拷贝构造函数未私有化时,通过解引用对象指针获取对象并拷贝构造
	Boss* b3 = new Boss(*b1);	//*b1解引用获得对象,再调用拷贝构造函数,在堆区创建对象
	cout << ((b1 == b3) ? "b1 == b3" : "b1 != b3") << endl;		//b1 != b3,即指向不同的对象
	*/

	return 0;
}

猜你喜欢

转载自blog.csdn.net/newson92/article/details/113098862