C++构造函数、析构函数及深拷贝

一、类和对象

类和对象是面向对象编程(OOP)的两个核心概念,它们之间的关系是抽象与具体的关系。

1.封装

封装是面向对象编程的核心概念之一,它指的是将数据和操作这些数据的函数捆绑在一起,形成一个类。封装的目的是保护对象的数据,防止外部代码直接访问对象内部的数据结构,减少错误并简化复杂性。

封装的优点:

  • 封装可以隐藏类的实现细节,只暴露必要的接口给外部使用,这样可以提高代码的安全性和可维护性。
  • 通过封装将相关的数据和操作封装在一起,提供简洁的接口。外部只需要关注接口,而不需要关心实现细节,这样可以简化代码的使用方式。
  • 封装将数据和操作组织成类,提高了代码的可重用性。

2.类

类是对象的抽象,它定义了一组具有相同属性和行为的对象的模板。

类包括数据成员和成员函数,数据成员描述对象的属性,而成员函数则定义了对象的行为。

类的声明形式为:
     class 类名{
     public:
     公有数据和函数
     protected:
     保护数据和函数
     private:
     私有数据和函数
     }

类的成员访问限定符包括public(公有)、private(私有)和protected(保护),它们决定了类的成员函数和数据成员的访问权限。

  • public:公有成员,可以被类的外部访问
  • private:私有成员,只能被类的内部访问
  • protected:保护成员,可以被类的派生类访问
class Person {
private:
	string p_name;
	int p_age;
public:
	void setName(string name) {
		p_name = name;
	}
	string getName() {
		return p_name;
	}
	void setAge(int age) {
		p_age = age;
	}
	int getAge() {
		return p_age;
	}
};

3.对象

对象是类的实例。当我们根据类创建一个实体时,这个实体就是对象。每个对象都有其自身的状态和行为,这些都由类定义。

对象一般通过成员函数来操作自己的数据成员,从而完成特定的功能。

一个类可以创建多个对象,每个对象都有自己的数据成员,但它们共享相同的成员函数。

Person p1;
p1.setName("Tom");
p1.setAge(20);
cout << p1.getName() << " " << p1.getAge() << endl;
//输出Tom 20
Person p2;
p2.setName("Mike");
p2.setAge(18);
cout << p2.getName() << " " << p2.getAge() << endl;
//输出Mike 18

二、构造函数和析构函数

构造函数和析构函数是类中特殊的成员函数,它们在对象的创建和销毁过程中发挥重要的作用。

1.构造函数

构造函数用于创建对象时初始化类成员,它的名称与类名相同,并且没有返回类型。

特点:

  • 构造函数可以重载,即一个类中可以定义多个参数或参数类型不同的构造函数。
  • 如果在类中没有定义构造函数,编译器一般会自动生成一个无参数的默认形式的构造函数。
  • 为类定义了非默认构造函数后,一般需要提供默认构造函数。
  • 在设计类时,通常应提供对所有类成员函数做隐式初始化的默认构造函数。

2.构造函数的分类

  • 无参构造函数:当不提供任何参数时被调用,通常用于初始化对象的默认状态。
  • 带参数的构造函数:当创建对象时可以传入参数,用于初始化对象的特定状态。
  • 拷贝构造函数:用于从一个已经存在的对象初始化一个新对象。
  • 移动构造函数:用于初始化一个新对象,从一个右值引用对象移动资源。

3.析构函数

析构函数用于在对象销毁前执行必要的清理工作,如释放分配给对象的内存资源。它的名称是在类名前面加上~,并且没有返回类型。

特点:

  • 析构函数没有返回值,并且不能有参数,因此它不能被重载。
  • 一般情况下,析构函数会被自动调用,例如在对象作用域结束时或使用delete运算符时。
  • 如果类中的数据成员进行了内存的申请,通常需要手动编写析构函数来释放这些内存。
  • 如果在类中没有定义析构函数,编译器会自动生成一个默认的析构函数。

4.析构函数调用时机

  • 如果对象是动态变量,则当执行完定义该对象的程序块时,将调用该对象的析构函数。
  • 如果对象是静态变量(外部、静态、静态外部或来自名称空间),则在程序结束时调用对象的析构函数。
  • 如果对象是用new创建的,则仅当用户显式地使用delete删除对象时,其析构函数才会被调用。
class Person {
public:
	Person() {
		cout << "构造函数调用" << endl;
	}
	~Person() {
		cout << "析构函数调用" << endl;
	}
};

5.构造函数的调用方式

即对象初始化的方式:

  • 括号法:直接使用类名和括号来调用构造函数。
  • 显式法:通过将构造函数的调用表达式赋给一个对象来调用构造函数。
  • 隐式法:在某些情况下,C++允许将构造函数的调用隐式转换为另一种形式。
class Person {
public:
	Person(string name,int age) {
		p_name = name;
		p_age = age;
	}
	Person() {
		p_name = " ";
		p_age = 0;
	}
    //拷贝构造函数
	Person(const Person& p) {
		p_name = p.p_name;
		p_age = p.p_age;
	}
private:
	string p_name;
	int p_age;
};
Person p1;  //不要加括号
//括号法
Person p2("Tom", 10);
Person p3(p1);
//显式法
Person p4 = Person("Tom", 10);
Person p5 = Person(p1);
//隐式法
Person p6 = { "tom", 10 };
Person p7 = p1;

Person("tom",10);  //匿名对象:当前执行结束后,系统立即回收
//Person(p1);  //不要利用拷贝构造函数初始化匿名对象

③调用规则:

  • 如果用户定义有参构造函数,编译器不会再提供默认无参构造,但是会提供默认拷贝构造。
  • 如果用户定义拷贝构造函数,编译器不会再提供其它构造函数。

6.拷贝构造函数

当使用已存在的对象来初始化新创建的对象时,编译器会自动调用拷贝构造函数。

特点:

  • 拷贝构造函数的名称与类的名称相同,没有返回值。
  • 拷贝构造函数必须使用引用作为参数,以防止在函数中修改原对象。
  • 拷贝构造函数可以被显式调用,也可以由编译器在需要时隐式调用。
  • 如果类中没有显式声明拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。

拷贝构造函数调用时机:

  • 使用一个已创建的对象来初始化一个新对象时。
  • 以值传递的方式给函数参数传值时。
  • 函数以值的方式返回局部对象时。
  • 生成临时对象或临时副本时。

三、动态内存分配

在C++中,类可以包含指针作为其成员变量。指针成员变量通常用于动态内存管理、实现多态性以及资源共享等。

1.内存分配注意事项

如果类使用new来分配类成员指向的内存,在设计时应采取一些预防措施:

  • 对于指向的内存是由new分配的所有类成员,都应在类的析构函数中对其使用delete,用来释放分配的内存。
  • 如果析构函数通过对指针类成员使用delete来释放内存,则每个构造函数都应当使用new来初始化指针,或将它设置为空指针。
  • 如果构造函数使用的是new,则析构函数应使用delete,如果构造函数使用的是new[],则析构函数应使用delete[]。
  • 应定义一个分配内存(而不是将指针指向已有内存)的拷贝构造函数。这样程序将能够将类对象初始化为另一个类对象。
  • 应定义一个重载赋值运算符的类成员函数,以确保赋值的安全性。

动态分配数组举例:

class Array {

public:
	Array(int size) {
		data = new int[size];
		len = size;
		for (int i = 0; i < size; i++) {
			data[i] = i;
		}
	}

	~Array() {
		delete[] data;
	}

	void print() {
		for (int i = 0; i<len; i++) {
			cout << data[i] << " ";
		}
		cout << endl;
	}

private:
	int* data;
	int len;
};
Array arr(5);
arr.print();
//输出0 1 2 3 4

2.浅拷贝与深拷贝

①浅拷贝:

  • 浅拷贝通常指的是在对象复制时,只对对象的数据成员进行简单的赋值操作。
  • 当对象中包含指针成员时,浅拷贝只会复制指针本身,而不会复制指针所指向的内存区域。这意味着原始对象和拷贝对象将共享同一块内存,当其中一个对象释放内存时,可能会导致另一个对象中的指针变成悬空指针,从而引发程序错误。
  • 如果类中没有定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,这个构造函数可能会进行浅拷贝。
  • 浅拷贝后的两个对象中的指针成员将指向相同的内存地址,如果其中一个对象修改了通过指针访问的数据,另一个对象的数据也会随之改变。
  • 浅拷贝适用于对象中不含有动态分配内存或其他资源的情况,或者当拷贝的目的是为了快速创建一个结构相似的对象时。

②深拷贝:

  • 深拷贝则是在复制对象时,不仅仅是简单的复制对象的数据成员,还会为指针成员分配新的内存空间,并复制指针所指向的内容。
  • 深拷贝可以避免浅拷贝带来的问题,确保原始对象和拷贝对象各自拥有独立的内存空间,互不影响。
  • 实现深拷贝通常需要自定义拷贝构造函数或重载赋值运算符,以确保指针成员指向的内存区域被正确复制。
  • 深拷贝适用于对象包含动态分配内存或其他需要独立管理的资源时,以避免潜在的资源冲突和悬挂指针问题。

举例:

class Copy {
public:
	int* data;

	Copy(int value) {
		data = new int(value);
	}

//  浅拷贝,默认拷贝构造函数实现方式
//	Copy(const Copy& c) {
//		data = c.data;
//	}

//  深拷贝
	Copy(const Copy& c) {
		data = new int(*c.data);
	}

	~Copy() {
		if (data != nullptr) {
			delete data;
			data = nullptr;
		}
	}

};
Copy c1(10);
Copy c2(c1);
cout << *c1.data << endl;  //10
cout << *c2.data << endl;  //10
*c1.data = 20;
cout << *c1.data << endl;  //20
cout << *c2.data << endl;  //10

3.定位new运算符

C++中的定位new运算符是一种特殊的new运算符,它允许用户指定动态内存分配的具体位置。

①定位new运算符的语法:

ptr = new (ptr) TypeName;

其中,ptr是一个指向已分配内存的指针,TypeName是要构造的对象类型。这个运算符会调用对象的构造函数来初始化新创建的对象,并在完成后返回指向该对象的指针。

②定位new运算符与普通new运算符的区别:

  • 普通new运算符会在堆中找到一个足以满足要求的内存块,并返回指向该内存块的指针。
  • 而定位new运算符则允许用户直接指定内存地址,从而可以在特定的位置创建对象。这种灵活性使得定位new运算符在处理内存时更加精确和高效。

③特点:

  • delete可与常规new运算符配合使用,但不能与定位new运算符配合使用。
  • 定位new运算符不会自动调用构造函数,因此必须显式调用对象的析构函数来清理内存。
  • 如果在未正确分配的内存上使用定位new运算符,或者忘记调用析构函数释放内存,可能会导致未定义行为或内存泄漏。
  • 定位new运算符允许用户实现更精细的内存控制,以便更好地优化内存的使用。

举例:

class A {
public:
	A() {
		cout << "A()" << endl;
	}
	~A() {
		cout << "~A()" << endl;
	}
};
char* buffer = new char[512];
A* p1 = new (buffer) A;
A* p2 = new (buffer+sizeof(A)) A;
//delete p2;  //no
//delete p1;  //no
p2->~A();
p1->~A();
delete[] buffer;

上述代码为p1和p2提供两个位于缓冲区的不同地址,确保这两个内存单元不重叠,并显式地为使用定位new运算符创建的对象调用析构函数来清理内存。

对于使用定位new运算符创建的对象,应以与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于早创建的对象。另外,仅当所有对象都被销毁后,才能释放用于储存这些对象的缓冲区。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值