【C++的面向对象】------- C++的构造函数和析构函数详解

目录

  • 一,典型的C++面向对象编程
  • 二,C++的构造函数和析构函数
    • 1.构造函数和析构函数的引入
    • 2.在构造和析构函数中使用动态内存
    • 3.构造函数与类的成员初始化
  • 三,拷贝构造函数的引入
    • 1.浅拷贝与深拷贝


一,典型的C++面向对象编程


1、三要素
(1)头文件hpp中类的定义
(2)源文件cpp中类的实现(构造函数、析构函数、方法)
(3)主程序

2、案例
(1)用C++来编程“人一天的生活”
(2)“人”的属性:name、age、male
(3)“人”的方法:eat、work(coding/shopping)、sleep
(4)人的生活:eat->work->sleep
实战中,一般一个cpp和一个hpp文件配对,描述一个class,class的名字和文件名相同的。

person.hpp

#ifndef __PERSON_H__
#define __PERSON_H__

#include 
using namespace std;

// 声明这个类
class person
{
// 访问权限
public:
	// 属性
	string name;			// 名字
	int age;				// 年龄
	bool male;				// 性别,男为true,女为false
	
	// 方法
	void eat(void);
	void work(void);
	void sleep(void);
		
private:

};
#endif

person.cpp

#include "person.hpp"
#include 
using namespace std;

// class的成员函数中可以引用class的成员变量,但是要考虑public和private这些访问限制
void person::eat(void)
{
	cout << name << " eat" << endl;
}

void person::work(void)
{
	if (this->male)
	{
		cout << this->name << " coding" << endl;
	}
	else
	{
		cout << this->name << " shopping" << endl;
	}
}

void person::sleep(void)
{
	cout << this->name << " sleep" << endl;
}

main.cpp

#include "person.hpp"

int main(void)
{
	// 人的一天的生活
	person zhangsan;			// 创建了一个person的对象,分配在栈上
	
	zhangsan.name = "zhangsan";
	zhangsan.age = 23;
	zhangsan.male = 1;
	
	zhangsan.eat();
	zhangsan.work();
	zhangsan.sleep();
		
	return 0;
}

makefile

all:
	g++ person.cpp main.cpp -o app

3、C++面向对象式编程总结
(1)整个工作分为2大块:一个是建模和编写类库,一个是使用类库来编写主程序完成任务。
(2)有些人只负责建模和编写类库,譬如开发opencv的人。
(3)有些人直接调用现成类库来编写自己的主任务程序,譬如使用opencv分析一张图片中有没有电动车。
(4)难度上不确定,2个都可能很难或者很简单。

4、C++学习的三重境界
(1)学习C++第一重境界就是语法层面,先学会如何利用C++来建模、来编程,学习语法时先别解决难度大的问题。
(2)学习C++第二重境界是解决问题层面,学习如何理解并调用现成类库来编写主程序解决问题。
(3)学习C++第三重境界是编写类库和sample给别人用,需要基础好且有一定架构思维。


二,C++的构造函数和析构函数


1.构造函数和析构函数的引入


1、什么是构造函数
(1)constructor,字面意思是用来构造对象的函数;destructor,字面意思是用来析构对象的函数
(2)可以理解为语言自带的一种hook函数(回调函数)。
用一个例子说明一下到底说明是回调函数:

你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。

在这个例子里,你的电话号码就叫回调函数你把电话留给店员就叫登记回调函数店里后来有货了叫做 触发回调事件店员给你打电话叫做 调用回调函数你到店里去取货叫做 响应回调事件

(3)当对象产生时constructor会自动被调用,一般用于初始化class的属性、分配class内部需要的动态内存
(4)对对象消亡时destructor会自动被调用,一般用于回收constructor中分配的动态内存,避免内存丢失

2、构造和析构一般用法
(1)不写时C++会自动提供默认的构造和析构,也可以显式提供默认构造和析构函数

/*-----person.hpp------*/
	person();				// 默认构造函数
	person(string name);	// 自定义构造函数
	~person();				// 默认析构函数
/*-----person.cpp------*/
person::person()
{
	// 默认构造函数是空的
	cout << "default constructor" << endl;
}

person::person(string name)
{
	// 自定义构造函数
	this->name = name;			// 构造对象后,同时对对象中的name属性进行初始化
	cout << "userdefined constructor" << endl;
}

person::~person()
{
	// 默认析构函数是空的
}

(2)构造和析构函数不需要返回值类型,构造函数可以带参或不带参,析构函数不带参
(3)构造函数可以重载(overload),析构函数不需要重载

3、为什么需要构造函数和析构函数

(1)构造函数可以看作是对象的初始化式,注意对比对象和变量的初始化区别。
(2)构造函数可以为对象完成动态内存申请,同时在析构函数中再释放,形成动态内存的完整使用循环。
(3)C语言中struct无构造函数概念,所以struct中需要用到动态内存时必须在定义struct变量后再次单独申请和释放,而这些操作都需要程序员手工完成。
(4)C++ class的构造和析构特性,是C++支持面向对象编程的一大语言特性。


2.在构造和析构函数中使用动态内存


1、析构函数的使用
析构函数在对象对销毁时自动调用,一般有下面几种情况

()分配在栈上的对象,当栈释放时自动析构

/*-----person.cpp------*/
person::person()
{
	cout << "默认构造函数" << endl;
}
person::person(string name)
{
	this->name = name;			// 构造对象后,同时对对象中的name属性进行初始化
	cout << "自定义构造函数" << endl;
}

person::~person()
{
	cout << "析构函数" << endl;
}

/*-----main.cpp------*/
    person Person;      //创建一个对象在栈上
	Person.age = 23;
	Person.male = 1;

	Person.eat();
	Person.work();

在这里插入图片描述
(2)用new分配的对象,用delete显式析构

/*-----main.cpp------*/
	string s1 = "linux";
	person *pPerson = new person(s1);	

	pPerson->age = 23;
	pPerson->male = 1;
	pPerson->eat();
	pPerson->work();
	
// 用完对象后就销毁它
	delete pPerson;

在这里插入图片描述
(3)普通情况下析构函数都是空的,因为不必做什么特别的事情

2、在class中使用动态内存变量
(1)什么情况下用动态内存?需要大块内存,且需要按需灵活的申请和释放,用栈怕爆、用全局怕浪费和死板时
(2)在class person中增加一个int *指针,用于指向一个int类型元素的内存空间,将动态内存从int变量升级到int数组变量
(3)在构造函数中分配动态内存
(4)在析构函数中回收动态内存
(5)实战中C++常用的动态内存往往是容器vector那些,后面会讲到

/*-----person.cpp------*/
person::person(string name)
{
	this->name = name;			// 构造对象后,同时对对象中的name属性进行初始化
	
	 //在构造函数中对class中需要分配动态内存的指针进行动态分配
//	this->pInt = new int(55);        //分配了一个int型元素的4个字节的内容,并把它初始化为55
	this->pInt = new int[100];		// 分配了100个int元素的数组
	
	cout << "自定义构造函数" << endl;
}

person::~person()
{
	//delete this->pInt;				// 释放单个内存
	delete[] this->pInt;			// 释放数组内存
	cout << "析构函数" << endl;
}

/*-------------申请到的内容可以在方法里面使用--------------*/
void person::sleep(void)
{
	cout << "value of this->pInt = " << *(this->pInt) << endl;
	
	for (int i = 0; i<100; i++)
	{
		this->pInt[i] = i;
	}
	
	for (int i = 0; i<100; i++)
	{
		cout << "pInt[" << i << "] = " << pInt[i] << endl;
	}
	
	cout << this->name << " sleep" << endl;
}


3.构造函数与类的成员初始化


1、构造函数一大功能就是初始化成员变量
(1)默认构造函数不带参,无初始化功能
(2)若无其他构造函数,则默认构造函数可以省略。但若有哪怕1个其他构造函数,则默认构造函数不能省,必须写上,否则创建默认对象的时候就无法调用默认构造函数。
(3)栈上分配对象时,若使用默认构造函数,则对象变量后面不加空的(),若用带参构造才需要加(初始化参数)。

/*-----main.cpp------*/

string s1 = "zhu";
person person;   //在栈上创建默认的不带参数的对象,调用的是默认的构造参数
person Person();  //报错,编译器会以为person()是一个函数
person person(s1);	//在栈上创建带参数的对象,调用的是自定义的构造函数	

2、C++的成员初始化列表
(1)一般用于带参构造函数中,用来给属性传参赋值
(2)成员初始化列表和构造函数之间用冒号间隔,多个列表项之间用逗号间隔。
(3)初始化列表可以替代构造函数内的赋值语句,达到同样效果

/*-----person.hpp------*/
class person
{
public:
 //成员变量
	string name;			// 名字
	int age;				// 年龄
	bool male;				// 性别,男为true,女为false

	person(){};				// 默认构造函数
	person(string myname, int myage, bool mymale);
	~person();				// 默认析构函数
	
	// 方法
	void print(void);	
private:
};

/*-----person.cpp------*/
                                                 //:成员变量(形参) ,成员变量(形参),成员变量(形参)
person::person(string myname, int myage, bool mymale):name(myname),age(myage),male(mymale)
{
	cout << "userdefined constructor" << endl;
}

// 打印出对象中所有成员的值
void person::print(void)
{
	cout << "name = " << name << endl;
	cout << "age = " << age << endl;
	cout << "male = " << male << endl;
}

:person::~person()
{
	cout << "userdefined destructor" << endl;
}

/*-----main.cpp------*/

	string s1 = "zhu";
	person pPerson(s1, 35, true);			// 创建了一个person的对象,并传参
	pPerson.print();

person::person(string myname, int myage, bool mymale):name(myname),age(myage),male(mymale)
{
}

等价于
person::person(string myname, int myage, bool mymale)
{
	this->name = myname;
	this->age = myage;
	this->male = mymale;
}

3、构造函数使用参数默认值
(1)class声明时可以给函数形参赋值一个默认值,实际调用时若不传参就使用默认值

person(string myname, int myage , bool mymale = false);   
person(string myname , int myage = 33, bool mymale = false);
person(string myname = "aston", int myage = 33, bool mymale = false);

 //赋值默认参数时只能从后面往前面赋值
person(string myname = "aston", int myage , bool mymale);   //错误

(2)方法实现时形参可以不写默认值,但是实际是按照声明时的默认值规定的
(3)有默认值情况,要注意实际调用不能有重载歧义否则编译不能通过

/*-----person.hpp------*/
person(){};				// 默认构造函数
person(string myname = "aston", int myage = 33, bool mymale = false);

/*-----main.cpp------*/
person person;       //编译器报错,有歧义,编译器不知道你是要调用默认构造函数还是调用有三个默认值的构造函数

(4)所有参数都带默认值的构造函数,1个可以顶多个构造函数(举例说明)

/*-----person.hpp------*/
//只用定义一个全部带默认参数的构造函数
person(string myname = "aston", int myage = 33, bool mymale = false);

/*-----main.cpp------*/
person person;       
person person("aston");
person person("aston",35);
person person("aston",35,false);   //都可以调用

三,拷贝构造函数的引入


1、初始化变量和对象的本质
(1)简单变量定义时,可以直接初始化,也可以用另一个同类型变量来初始化

int a = 4;		// 直接初始化,用一个值来直接对新定义的变量初始化
int b = a;		// 间接初始化,也就是用另一个变量来初始化这个刚新定义的变量

原理:变量的直接初始化,是变量在被分配内存之后直接用初始化值去填充赋值完成初始化;变量用另一个变量来初始化,是给变量分配了内存后执行了一个内存复制操作来完成的初始化

(2)用class来定义对象时,可以直接初始化,也可以用另一个对象来初始化

	// 方式1:直接初始化
	person p1("aston", 35, true);
	
	// 方式2:用另一个对象来初始化新定义的对象
	person p2(p1);		// p2是新对象,并且值已经被用p1来初始化了
	person p2 = p1;		// 和上面的写法本质上是一样的

原理:对象的直接初始化,是对象在分配内存之后调用了相应构造函数来完成的初始化;对象的用另一个对象来初始化,是对象在分配之后调用了相应的拷贝构造函数来完成初始化。

2、拷贝构造函数
(1)拷贝构造函数是构造函数的一种,符合构造函数的一般性规则
(2)拷贝构造函数的引入是为了让对象在初始化时能够像简单变量一样的被直接用=来赋值
(3)拷贝构造函数不需要重载,他的参数列表固定为const classname& xx
(4)拷贝构造函数很合适用初始化列表来实现

person(const person& pn);		// 默认拷贝构造函数声明

person::person(const person& pn):name(pn.name),age(pn.age),male(pn.male)    //默认拷贝构造函数的定义
{
	/*
	this->name = pn.name;
	this->age = pn.age;
	this->male = pn.male;
	*/
	cout << "copy constructor" << endl;
}

1.浅拷贝与深拷贝


1、浅拷贝的缺陷

(1)上节讲的只有普通成员变量初始化(没有动态分配内存)的拷贝构造函数,就是浅拷贝
(2)如果不显式提供,C++会自动提供一个全部普通成员被浅拷贝的默认拷贝构造函数
(3)浅拷贝在遇到有动态内存分配时就会出问题

/*-----person.hpp------*/
class person
{
public:
	
	string name;			
	int age;				
	bool male;				
	int *pInt;				// 只是分配了p本身的4字节内存,并没有分配p指向的空间内存
	
	person(string myname, int myage, bool mymale);    //带参数构造函数
	person(const person& pn);		// 拷贝构造函数
	~person();				     // 默认析构函数

private}

/*-----person.cpp------*/
person::person(string myname, int myage, bool mymale):name(myname),age(myage),male(mymale)    //自定义构造函数
{
	this->pInt = new int(5);		// 分配了1个int元素
}

// 默认拷贝构造函数
person::person(const person& pn):name(pn.name),age(pn.age),male(pn.male)
{
}
person::~person()
{
	delete this->pInt;				// 释放单个内存
}

/*-----main.cpp------*/
person p1("aston", 35, true);
person p2 = p1;			
*p1.pInt = 44;
cout << *p2.pInt << endl;     //发生段错误,原因是解引用野指针

2、如何解决
(1)不要用默认拷贝构造函数,自己显式提供一个拷贝构造函数,并且在其内部再次分配动态内存
(2)这就叫深拷贝,深的意思就是不止给指针变量本身分配内存一份,也给指针指向的空间再分配内存(如果有需要还要复制内存内的值)一份。

// 提供深拷贝构造函数
MAN::person::person(const person& pn):name(pn.name),age(pn.age),male(pn.male)
{
	pInt = new int(*pn.pInt);			// 深拷贝
}

(3)一般如果不需要深拷贝,根本就不用显式提供拷贝构造函数,所以提供了的基本都是需要深拷贝的。
(4)拷贝构造函数不需要额外的析构函数来对应,用的还是原来的析构函数

3、如何深度理解浅拷贝和深拷贝
(1)这个问题不是C++特有的,Java等语言也会遇到,只是语言给封起来了,而C++需要类作者自己精心处理。
(2)从编程语言学角度讲,本质上是值语义和引用语义的差别。值语义(value symatics):例如int a = 1;定义a的时候同时给a分配4字节的空间引用语义(reference symatics)例如:double *p ;定义时只给p本身分配了4字节的内存,而P所指向的内存地址要程序员分配

你可能感兴趣的:(#,C++完全学习,c++,编程语言,构造与析构,深拷贝和浅拷贝)