C++类与对象(中)第一篇

目录

前言:

类的六个默认成员函数

构造函数

析构函数

拷贝构造函数

拷贝场景一:函数参数类型为类类型对象

拷贝场景二:利用已存在的对象创建新对象

拷贝场景三:函数返回值类型为类类型对象


前言:

编译器编译类的详细步骤:

  1. 先识别类名;
  2. 识别类中有哪些成员变量;
  3. 识别类中有哪些成员函数;
  4. 编译器对成员函数进行预处理,加上隐藏的this指针;

因而成员变量类域的前后顺序程序编译没有影响;

类的六个默认成员函数

//空类中什么都没有吗?
class Date
{

};
//任何类在什么都不写时,编译器会自动生成6个默认成员函数;
//默认成员函数:用户没有显示实现,编译器会生成的成员函数称为默认成员函数;

C++类与对象(中)第一篇_第1张图片

构造函数

对于学生类,创建一个学生对象,这个学生就应该具有姓名、年龄 、学号等,但是这些数据成员没有初始化,该学生的姓名 、年龄、 学号等数据成员的值将为随机值或0;此时这个学生对象无任何意义!创建对象时,C++自动初始化对象的工作专门由该类的构造函数完成;

构造函数的作用在创建对象,系统自动调用初始化数据成员构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

构造函数的特点:

  1. 类名即为函数名;
  2. 没有返回值(函数名前什么也不写,也没有void);
  3. 类创建对象时,编译器自动调用构造函数;
class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
    //构造函数
	Date()//函数名为类型名&&返回值没有
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Date d;//创建对象d时自动调用构造函数Date()
	d.print();//只调用了print()函数
	return 0;
}

运行结果:

构造函数的特点:

4.  构造函数支持函数重载 ;

注:函数重载(同一作用域中的同名函数,同名函数的参数个数 参数类型 类型顺序不同);

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date()
	{
		//声明
		_year = 2000;
		_month = 12;
		_day = 18;
	}
	Date(int year, int month, int day)
	{
	_year = year;
	_month = month;
	_day = day;
	}
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};

int main()
{
	Date d1;
	d1.print();

	Date d2(2023, 12, 18);
	d2.print();

	return 0;
}

 运行结果:

C++类与对象(中)第一篇_第2张图片

默认构造函数:

  1.  用户在类中没有显示定义构造函数, 编译器自动生成的构造函数,叫默认构造函数;
  2.  无参构造函数也可以叫默认构造函数;
  3.  全缺省也可以叫默认构造函数;

可以不传参数可以调用构造函数,都可以叫默认构造函数

三个默认构造函数不能同时存在,只能存在一个默认构造函数

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date()//无参的构造函数--->默认构造函数
	{
		_year = 2021;
		_month = 12;
		_day = 18;
	}
	Date(int year=2023, int month=10, int day=18)//全缺省参数的构造函数--->默认构造函数
	{
	_year = year;
	_month = month;
	_day = day;
	}  
    //默认构造函数只能存在一个;
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Date d1;
	d1.print();
	return 0;
}

 运行结果:

C++类与对象(中)第一篇_第3张图片

类中用户没有显示定义构造函数,C++编译器自动生成一个无参的默认构造函数;当用户显示定义构造函数时,C++编译器不再自动生成

用户没有显示定义, 编译器自动生成默认构造函数,默认构造函数初始化数据成员时,对于内置类型(int float dobule ...... 指针)的数据,不做任何处理内置类型的数据为随机值

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
    //用户未定义默认构造函数,编译器自动生成默认构造
    //对于Date类中的内置类型不做任何处理
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
};
int main()
{
	Date d1;
	d1.print();
	return 0;
}

监视窗口:

C++类与对象(中)第一篇_第4张图片

 用户没有显示定义, 编译器自动生成默认构造函数,默认构造函数初始化数据成员时,对于自定义类型(struct class union)的数据,调用自定义类型中的默认构造函数

class Time
{
private:
	int hour; 
	int minute;
	int second;
public:
	//显示定义构造函数
	Time()
	{
		cout << "Time()" << endl;
		int hour = 0;
		int minute = 0;
		int second = 0;
	}
};

class Date
{
private:
	int _year;
	int _month;//内置类型--->_year _month _day
	int _day;
	Time _t;//自定义类型----> _t
public:
	void print()
	  {
		cout << _year << "-" << _month << "-" << _day << endl;
	  }
};
int main()
{
	Date d;
	d.print();
	return 0;
}

运行结果:

C++类与对象(中)第一篇_第5张图片

由于默认构造函数对于内置类型成员不做任何处理,为弥补这种缺陷,C++11允许在类中声明时给缺省值

class Date
{
private:
	int _year = 2023;
	int _month = 12;//内置类型成员声明时给缺省值
	int _day = 18;
public:
	void print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

};
int main()
{
	Date d;
	d.print();
	return 0;
}

运行结果:

析构函数

析构函数的作用是在对象生命周期结束时自动调用,用于完成对象中资源的清理工作,例如释放内存、关闭文件等操作;

析构函数的特性:

  1. 析构函数的函数名:  ~ 类名
  2. 析构函数无参无返回值;
  3. 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数;

      ( 注意:析构函数不能重载

     4. 对象生命周期结束时,C++编译系统系统自动调用析构函数;

class Stack
{
private:
	int* _a;
	int _top;
	int _capacity;
public:
	//默认构造函数
	Stack(int capacity=4)
	{
		cout << "Stack()" << endl;
		_a = (int*)malloc(sizeof(int)* capacity);
		if (_a == nullptr)
		{
			perror("malloc failed:");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	//析构函数
	~Stack() //函数名:~Stack && 没有参数
	{
		cout << "~stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
};

int main()
{
	Stack st;//创建对象时调用默认构造函数
	return 0;
}//对象st出作用域时自动调用析构函数

运行结果:

析构函数对于内置类型成员不做任何处理;对于自定义类型中的成员,清空哪个类的对象中的数据就调用哪个类的析构函数;

class Time
{
private:
	int _hour;
	int _minute;
	int _second;
public:
	~Time()
	{
		cout << "~Time" << endl;
	}
};
class Date
{
private:
	int _year;
	int _day; //内置类型: _year _month _day
	int _month;
	Time _t; //自定义类型: _t
};
int main()
{
	Date d;
	return 0;
}

运行结果:

如果类中没有申请资源时,用户可以不显示定义析构函数,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类;
class Stack
{
private:
	int* _a;
	int _top;
	int _capacity;
public:
	//默认构造函数
	Stack(int capacity=4)
	{
		cout << "Stack()" << endl;
		_a = (int*)malloc(sizeof(int)* capacity);
		if (_a == nullptr)
		{
			perror("malloc failed:");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	//用户显示实现析构函数
	~Stack() 
	{
		cout << "~stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}
};

int main()
{
	Stack st1;
    Stack st2;
	return 0;
}

C++类与对象(中)第一篇_第6张图片

由于main()函数建立函数栈帧时先创建st1对象,再创建st2对象,所以当main()函数函数栈帧销毁时,先对st2开辟的空间调用析构函数,再对st1开辟的空间调用析构函数;

C++类与对象(中)第一篇_第7张图片

拷贝构造函数

拷贝构造函数的作用:利用已经存在的对象创建一个新对象时(拷贝对象),就会调用新对象的拷贝构造函数初始化新对象的成员变量

拷贝构造函数的特征:

  1. 拷贝构造函数是构造函数的重载形式(拷贝构造函数是一个特殊的构造函数);
  2. 拷贝构造函数的参数是同类型的对象并且参数只有一个而且是类类型对象的引用;

拷贝场景一:函数参数类型为类类型对象

func()函数传参时将类对象d1作为实参传递给形参d,而传参的本质是拷贝,因为形参和实参空间是独立的,但数据内容是相同的;对象(结构体)可以传值调用,也可以传址调用;

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
	Date(int year = 2000, int month = 1, int day = 1)
		{
			_year = year;
			_month = month;
			_day = day;
		}
	void Print()
		{
		    cout << _year << "/" << _month << "/" << _day << endl;
		}

};
void func(Date d)
{
  d.Print();
}
int main()
{
	Date d1(2023, 12, 20);
	func(d1);
	return 0;
}

结构体作为实参,采用传值传参,结构体多大,传参时函数栈帧开辟的空间就有多大; 而且形参的改变并不能影响实参(空间是独立的);C++中,为了传参时不发生拷贝,可以采取传址传参传址传参只是获取到实参的地址,不会发生拷贝;还可以让引用作为func()函数的形参,因为引用只是实参的别名,形参和实参共用同一块内存空间,所以也不会发生拷贝

 浅拷贝/值拷贝: 拷贝构造函数对象按内存存储按字节序完成拷贝;

通过监视窗口查看日期类对象d1与拷贝对象d

C++类与对象(中)第一篇_第8张图片

假设极端场景下,不得不采用传值传参时,会发生什么?

class Stack
{
private:
	int* _a;
	int _capacity;
	int _top;
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc failed: ");
		}
		_capacity = capacity;
		_top = 0;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_capacity = _top = 0;
		_a = nullptr;
	}
};
void func2(Stack st)
{
	//...
}
int main()
{
	Stack st1;
	func2(st1);

	return 0;
}

运行结果:

C++类与对象(中)第一篇_第9张图片

原因如下:

C++类与对象(中)第一篇_第10张图片

由于是值拷贝,对象st1的成员变量_a与对象st的成员变量_a数值相同,那么这两个指针指向同一块内存空间,然而C++的析构函数在对象销毁时自动调用,func2()函数调用结束后,调用析构函数释放对象st中的成员变量_a所指向的空间,当主函数(main())调用结束后,调用析构函数释放对象st1中的成员变量_a所指向的空间,造成同一块空间的二次释放;

如何解决值拷贝所出现的问题?

C++规定,自定义类型的对象拷贝时,调用拷贝构造函数实现深拷贝;

拷贝场景二:利用已存在的对象创建新对象

class Date
{
private:
	int _year;
	int _month;
	int _day;
public:
//构造函数
    Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//假设日期类的拷贝构造函数如下
	Date(Date d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
	    cout << _year << "/" << _month << "/" << _day << endl;
	}

};
int main()
{
	Date d1(2023, 12, 20);//调用构造函数
	Date d2(d1);//调用拷贝构造函数 d1-已存在的对象 d2-拷贝对象

	return 0;
}

为什么使用拷贝构造函数使用传值方式编译器直接报错

因为会引发无穷递归调用;

原因如下:

C++类与对象(中)第一篇_第11张图片

 如何解决传值调用所引发的无穷递归问题?

 采用引用作为拷贝构造函数的形参

C++类与对象(中)第一篇_第12张图片

返回到拷贝场景一,浅拷贝对于栈类,对于同一块空间释放两次,如何解决?
C++类与对象(中)第一篇_第13张图片
class Stack
{
private:
	int* _a;
	int _capacity;
	int _top;
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)* capacity);
		if (nullptr == _a)
		{
			perror("malloc failed: ");
		}
		_capacity = capacity;
		_top = 0;
	}
	//拷贝构造函数
	//this指向st stt为st1的别名
	Stack(Stack& stt)
	{
		//开辟与st1一样大的空间
		this->_a = (int*)malloc(sizeof(int)*stt._capacity);
		if (nullptr ==this->_a)
		{
			perror("malloc failed:");
			exit(-1);
		}
		//拷贝指向的资源
		memcpy(this->_a, stt._a, sizeof(int)*stt._top);
		this->_top = stt._top;
		this->_capacity = stt._capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_capacity = _top = 0;
		_a = nullptr;
	}
};
void func2(Stack st)
{
	//...
}
int main()
{
	Stack st1;
	func2(st1);
	return 0;
}

运行结果:

监视窗口:

C++类与对象(中)第一篇_第14张图片

用户未显示定义拷贝构造函数,编译器会生成默认的拷贝构造函数;

默认的拷贝构造函数对内置类型成员完成值拷贝,对于自定义类型成员调用其拷贝构造函数完成拷贝;

编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?
类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝;

拷贝场景三:函数返回值类型为类类型对象

class Stack
{
private:
	int* _a;
	int _capacity;
	int _top;
public:
	//构造函数
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int)* capacity);
		if (nullptr == _a)
		{
			perror("malloc failed: ");
		}
		_capacity = capacity;
		_top = 0;
	}
	//拷贝构造函数
	//this指向st stt为st1的别名
	Stack(Stack& stt)
	{
		//开辟与st1一样大的空间
		this->_a = (int*)malloc(sizeof(int)*stt._capacity);
		if (nullptr ==this->_a)
		{
			perror("malloc failed:");
			exit(-1);
		}
		//拷贝指向的资源
		memcpy(this->_a, stt._a, sizeof(int)*stt._top);
		this->_top = stt._top;
		this->_capacity = stt._capacity;
	}
	//析构函数
	~Stack()
	{
		free(_a);
		_capacity = _top = 0;
		_a = nullptr;
	}
};

Stack func()
{
	Stack st;

	return st;//传值返回,返回谁?-->返回时先将局部变量st保存于寄存器中;
	//局部变量st出作用域销毁,因此返回的是st的拷贝;所以此处调用拷贝构造函数;
}
int main()
{
	func();
	return 0;
}

当局部变量st被static修饰,此时st存放于静态区,出作用域并不会被销毁,可以考虑返回类型为传引用返回,共用同一块内存空间,不会调用拷贝构造函数,少了一次拷贝;

Stack& func()
{
	static Stack st;

	return st;//传值返回,返回谁?-->返回时先将局部变量st保存于寄存器中;
	//局部变量st出作用域销毁,因此返回的是st的拷贝;所以此处调用拷贝构造函数;
}
int main()
{
	func();
	return 0;
}

你可能感兴趣的:(c++,开发语言)