C++入门:构造函数,析构函数,拷贝构造函数,运算符重载详解

目录

类的6个默认成员函数

一.构造函数

1.概念

2.特征如下:

(1) 函数名与类名相同。

(2)无返回值。

(3)对象实例化时编译器自动调用对应的构造函数。

(4)构造函数可以重载。

(5)如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

(6)默认构造函数:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

(7)默认构造函数可以不写吗?

(8)我们不写构造函数编译器默认生成的构造函数干了些什么事情?

(9)(了解)C++11打的补丁:

3.对构造函数的考察题

二.析构函数

1.概念

动态开辟的资源不用析构释放会永远内存泄漏吗?

2.特征

(1. 析构函数名是在类名前加上字符 ~。

(2. 无参数无返回值。(所以析构函数不能构成函数重载)

(3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

(4. 对象生命周期结束时(最近那层大括号里面就是生命周期),C++编译系统系统自动调用析构函数。

(5. 我们不写析构函数编译器默认生成的析构函数干了些什么事情?

3.构造函数和析构函数自动调用的顺序

考察构造,析构顺序的题:

三.拷贝构造

1.大体的概念:创建一个新对象时,把一个已有对象完全拷贝给这个新对象:Date d2(d1)

2.特征

<1. 拷贝构造函数是构造函数的一个重载形式。

<2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

<3. 若未显示定义,系统生成默认的拷贝构造函数。

<4.我们不写拷贝构造函数编译器默认生成的拷贝构造函数干了些什么事情?

<5.日期类用默认,栈类需要自己写深拷贝

3.拷贝构造需要自己写的场景只有Stack这种类!而且是深拷贝。

4.拷贝构造的易错题

(1)题目1

(2)题目2:构造,析构,拷贝构造综合题

(3)题目2的变形——匿名对象

(4)继续变形

(5)题目4的变形

(6) 终极复合题

5.栈类,日期类,MyQueue类对于构造,析构,拷贝构造的需求

(1)日期类:

(2)栈类:

(3)MyQueue类(内置类型和自定义类型混合):

四.运算符重载

1.目的:

2.函数格式及细节:

3.探索如果正确使用运算符重载:

>:1 不能把 运算符重载operator写在类的外面

>:2 使用if (d1 == d2) 不是 if (operator==(d1, d2))

>:3 运算符重载operator写进类

>:4 传参用引用传参&,并加上const

>:5 运算符重载operator类外面和类里面都写了,优先调用类里面的

五.日期类的运算符重载

1.练习:写一下 if (d1 < d2) 的运算符重载

2.练习:写一下 d1 = d2 的运算符重载

(1)d1 = d2,if (d1 < d2),if (d1 == d2)运算符重载的代码

(2)赋值运算符重载和拷贝构造的区别

3.有意义的运算符才会重载,我们还可以重载很多日期类运算符。比如== , < , <= , > , >= , !=

Date.h

Date.cpp

test.cpp

(2)把定义写在类中(== , < , <= , > , >= , != 的运算符重载。)

4.+ ,+= 的运算符重载

Date.h

 Date.cpp

test.cpp

(1)此处的细节:const对于赋值运算符重载或拷贝构造可以防止权限被放大。

 (2)+和+=的互相复用,+复用+=更优一些!

5.- ,-= 的运算符重载

Date.cpp

test.cpp

6.前置,后置++

7.日期-日期 返回天数

8.const的细节

(1)权限放大问题

 (2)const修饰类的成员函数:

 (2)const修饰类的成员函数的格式:

(3)权限放大问题的解决方法:

六.取地址及const取地址操作符重载


类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数。

学习这几个函数分为两个大方面:
1、基本语法特性。 函数名、参数、返回值、什么时候调用...
2、我们不写编译器默认生成这个函数干了些什么事情!
 

一.构造函数

1.概念

构造函数是特殊的成员函数,需要注意的是,构造函数的名称虽然叫构造,但是需要注意的是构造函数的主要任务并不是开空间创建对象,而是 初始化对象

2.特征如下:

(1) 函数名与类名相同。

(2)无返回值。

(3)对象实例化时编译器自动调用对应的构造函数。

(4)构造函数可以重载。

比如写一个非默认构造函数和一个默认构造函数可形成重载:

#include<iostream>
using namespace std;
class Date
{
public:
	Date()    //不传参写法(不建议这么写)
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

	Date(int year, int month, int day)    //传参写法(不建议这么写)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{

	Date d1;	//无参写法
	d1.Print();

	Date d2(2022, 5, 15);	//含参写法,全缺省函数传3个参数可以
	d2.Print();


	return 0;
}

class Date
{
public:
	//Date()    //不传参写法(不建议这么写)
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}

	//Date(int year, int month, int day)    //传参写法(不建议这么写)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}

	Date(int year = 1, int month = 1, int day = 1)	//全缺省写法是最佳写法,相传几个参数都行
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	//Date d1(); // 无参不能这么写,编译器无法区分函数声明还是类的定义
	Date d1;	//无参写法
	d1.Print();

	Date d2(2022, 5, 15);	//含参写法,全缺省函数传3个参数可以
	d2.Print();

	Date d3(2022);    //全缺省函数传1个参数可以
	d3.Print();

	Date d4(2022, 10);    //全缺省函数传2个参数可以
	d4.Print();

	return 0;
}

(5)如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

     我们不写,编译器会生成一个默认无参构造函数
     内置类型/基本类型:int/char/double/指针...
     自定义类型:class/struct去定义类型对象
     默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理
class Date 不写构造函数时:d1中的内置类型 _year,int _month,int _day 都是随机值,不会初始化,但自定义类型 _aa 会去class A中调用类型A的构造函数, 如果A也不写构造函数 A() ,在类A中int _a 也是内置类型,那么_aa._a 也就是随机值了
class A
{
public:
	A()
	{
		cout << " A()" << endl;
		_a = 0;
	}
private:
	int _a;
};

class Date
{
public:
	// 我们不写,编译器会生成一个默认无参构造函数
	// 内置类型/基本类型:int/char/double/指针...
	// 自定义类型:class/struct去定义类型对象
	// 默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日

	A _aa;
};
int main()
{
	Date d1;
	d1.Print();

	return 0;
}

总结:如果一个类中的成员全是自定义类型,我们就可以用默认生成的函数。如果有内置类型的成员,或者需要显示传参初始化,那么都要自己实现构造函数。90%都需要自己写构造函数

二次总结:一般情况一个C++类,都要自己写构造函数。一般只有少数情况可以让编译器默认生成。
 1、类里面成员都是自定义类型成员,并且这些成员都提供了默认构造函数
 2、如果还有内置类型成员,声明时给了缺省值(在private中给成员变量缺省值,这是C++11才有的操作)

举例:不需要自己写构造函数的情况详细代码:class MyQueue 这种只要自定义类型的类就可以不用写构造函数了

class Stack
{
public:
	Stack()
	{
		_a = nullptr;
		_top = _capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	void push(int x) {
	}

	int pop() {
	}

private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue q;
	Stack st;

	return 0;
}

(6)默认构造函数:无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:我们写的无参的构造函数  Stack()我们写的全缺省的构造函数  Stack(int capacity = 10)我们没写编译器默认生成的构造函数  (上面两个都不写,编译器就会自己生成一个),都可以认为是默认构造函数。——>不用传参就可以调用的 Stack(int capacity)这个需要传参,所以不是默认构造函数

class Stack
{
public:
	/*Stack()	//1.我们写的无参的构造函数
	{
		_a = nullptr;
		_top = _capacity = 0;
	}*/

	/*Stack(int capacity = 10)	//2.我们写的全缺省的构造函数
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}*/
	//3.或者上面两个都不写编译器默认生成的构造函数
	Stack(int capacity)	//但是这个需要传参数就,不是默认构造函数,就会导致默认构造函数生成失败
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	void push(int x) {
	}

	int pop() {
	}

private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue q;
	Stack st;

	return 0;
}

用上面注释的那三种默认构造函数都可以正常运行,但是如果像上面这样用的是Stack(int capacity),这个带参的就不是默认构造函数,因为class MyQueue中我们没写默认构造函数所以编译器应该自己生成默认构造函数,但是MyQueue的默认构造函数无法生成,因为生成的条件是要先调用 Stack 的默认构造函数来初始化 _st1 和 _st2 ,因为我们写的不是默认构造函数,而是一个带参的函数,所以无法调用Stack 的默认构造函数,则MyQueue的默认构造函数无法生成,进而导致报错。

(7)默认构造函数可以不写吗?

一般是要写的,首先如果是下面这种非默认构造函数,初始化d1这样写是正确的,因为自己带实参了;初始化d2这样写是错误的,因为他自己没有带实参,而且也没有缺省形参,就错误。

class Date
{
public:
 Date(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 
private:
 int _year;
 int _month;
 int _day;
};

int main()
{
    Date d1(2022,5,22);   //正确
    Date d2;  //错误
}    

结论:不写默认构造函数也是可以的,就看调用是否带参数,非默认构造函数->传参有实参->正确,非默认构造函数->传参无实参->错误。

为了在我们传参时有时调用写实参,有时调用不写实参,我们最好是用默认构造函数,默认构造函数d1和d2调用形式都对。

(8)我们不写构造函数编译器默认生成的构造函数干了些什么事情?

在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用? 对象调用了编译器生成的默认构造函数,但是 对象 year/ month/_day ,依旧是随机值。也就说在这里 编译器生成的默认构造函数并没有什么卵 用??
解答: C++ 把类型分成内置类型 ( 基本类型 ) 和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用 class/struct/union 自己定义的类型,编译器生成默认的构造函数会对自定类型成员 调用的它的默认成员函数( 默认生成构造函数对于内置类型成员变量不做处理,对于自定义类型成员变量才会处理

(9)(了解)C++11打的补丁:

针对编译器自己生成默认成员函数不初始化的问题:当既有内置类型也有自定义类型时,我们不写默认构造函数而用编译器自己生成的默认构造函数时内置类型就会是随机值, 为了使内置类型也初始化,C++11使成员变量声明时可以被赋值缺省参数,默认构造函数调用时会使用这个缺省参数

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);
	
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
 
class MyQueue {
public:
	// 默认生成构造函数就可以用了

	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	// C++11打的补丁,针对编译器自己生成默认成员函数不初始化的问题
	// 给的缺省值,编译器自己生成默认构造函数用
	int _size = 0;

	Stack _st1;
	Stack _st2;
};
	
int main()
{
	MyQueue q;
	
	return 0;
}

3.对构造函数的考察题

下列关于构造函数的描述正确的是( )

A.构造函数可以声明返回类型

B.构造函数不可以用private修饰

C.构造函数必须与类名相同

D.构造函数不能带参数

答案解析:

A.构造函数不能有返回值,包括void类型也不行

B.构造函数可以是私有的,只是这样之后就不能直接实例化对象

C.这是必须的

D.构造函数不光可以带参数,还可以有多个构造函数构成重载

所以选C

二.析构函数

1.概念

前面通过构造函数的学习,我们知道一个对象时怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作(通常是动态开辟的空间        )

动态开辟的资源不用析构释放会永远内存泄漏吗?

所有动态开辟的资源,如果你不释放的话呢,因为它都是属于进程地址空间的。进程是正常结束的,那他这个时候进程会把这个虚拟地址和物理地址的这个页给解掉,正常进程会清理动态开辟的资源。所以说内存泄露是存在于一个进程持续运行的过程当中,那这个内存泄漏才会有持续的影响,如果比如说你一个你new了,你没释放,但是现在进程马上结束,并且是正常结束,那这个时候就不会有什么影响

2.特征

析构函数是特殊的成员函数。

特征:

(1. 析构函数名是在类名前加上字符 ~

(2. 无参数无返回值。(所以析构函数不能构成函数重载)

(3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

类比构造函数:类里面成员都是自定义类型成员,并且这些成员都提供了析构函数,就可以使用默认的析构函数

class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()        //析构函数
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	// 默认生成析构函数就可以用了
	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	/*Date d1;
	int i = 10;
	if (i > 0)
	{
		Stack st;
	}*/

	// 栈里面定义对象,析构顺序和构造顺序是反的
	Stack st1(1);
	Stack st2(2);

	MyQueue q;

	return 0;
}

(4. 对象生命周期结束时(最近那层大括号里面就是生命周期),C++编译系统系统自动调用析构函数。

(5. 我们不写析构函数编译器默认生成的析构函数干了些什么事情?

仍然和构造函数一样:编译器生成默认的析构函数会对自定义类型成员调用它的默认析构函数内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可

日期类没必要写析构函数,栈类这种malloc和new出新空间的才用写析构函数
class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	//~Date()
	//{
	//	cout << "~Date()" << endl;
	//}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

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

3.构造函数和析构函数自动调用的顺序

在下面这个栈里面定义对象,析构顺序和构造顺序是反的

class Stack
{
public:
	// Init
	// Destroy

	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue {
public:
	// 默认生成构造函数就可以用了
	// 默认生成析构函数就可以用了
	void push(int x) {
	}
	
	int pop() {
	}
	
private:
	Stack _st1;
	Stack _st2;
};

int main()
{
	// 栈里面定义对象,析构顺序和构造顺序是反的
	Stack st1(1);
	Stack st2(2);

	MyQueue q;

	return 0;
}

构造函数调用:st1先,st2后

析构函数调用:st2先,st1后

考察构造,析构顺序的题:

设已经有A,B,C,D4个类的定义,程序中A,B,C,D析构函数调用顺序为?( )

C c;
int main()
{

    A a;
    B b;
    static D d;
    return 0;
}

A.D B A C

B.B A D C

C.C D B A

D.A B D C

答案解析:

1、类的析构函数调用一般按照构造函数调用的相反顺序进行调用,但是要注意static对象的存在, 因为static改变了对象的生命周期,需要等待程序结束时才会析构释放对象

   2、全局对象先于局部对象进行构造

   3、局部对象按照出现的顺序进行构造,无论是否为static

   4、所以构造的顺序为 c a b d

   5、析构的顺序按照构造的相反顺序析构,只需注意static改变对象的生命周期之后,会放在局部 对象之后进行析构。

总结来说:static修饰的局部变量的生命周期是到整个程序结束,他的生命周期肯定是出了大括号还存在的,当出了大括号肯定要先析构大括号里的局部变量,所以静态变量析构在局部变量之后;全局变量析构一定放在局部变量之后。

   6、因此析构顺序为B A D C

三.拷贝构造

1.大体的概念:创建一个新对象时,把一个已有对象完全拷贝给这个新对象:Date d2(d1)

构造函数 只有单个形参,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存在的类类型对象 创建新对象时由编译器自动调用

2.特征

拷贝构造函数也是特殊的成员函数,其 特征 如下:

<1. 拷贝构造函数是构造函数的一个重载形式

<2. 拷贝构造函数的参数只有一个必须使用引用传参,使用传值方式会引发无穷递归调用

解释:

Date d2(d1); 对象调用类中的拷贝构造函数 Date(Date d) 时,先传参,实参d1传参给形参Date d,因为是 自定义类型对象,拷贝初始化规定要调用拷贝构造完成 ,因为这里传参就是把实参拷贝给形参,所以传参时还要调用一次拷贝构造,这次拷贝构造相当于是 Data data(d1) 去调用的(这里的data是第一次拷贝构造的形参,d1是第一次拷贝构造的实参) Date(Date d),调用这次拷贝构造又要传参,传参又要调用拷贝构造,所以形成无穷递归。所以务必加上引用, Date(Date& d)传参时就不用调用拷贝构造了,因为形参就是实参的别名(当实参,形参是内置类型时就直接赋值就行,没有内置类型的拷贝构造函数)
加const的意义
①防止写反,写成this的年月日赋值给d中年月日,如果加了const就会报错。
②const对于赋值运算符重载或拷贝构造可以防止权限被放大。详情见下文的四.7

<3. 若未显示定义,系统生成默认的拷贝构造函数。

<4.我们不写拷贝构造函数编译器默认生成的拷贝构造函数干了些什么事情?

(1)内置类型的成员会完成值拷贝,浅拷贝。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
(2)自定义类型的成员,默认生成的拷贝构造函数会 去调用这个成员的拷贝构造

<5.日期类用默认,栈类需要自己写深拷贝

那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。栈类有必要深拷贝,深拷贝现在不在我们的学习范围。

浅拷贝:只适用于日期类这种,不适用栈类
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	// Date d2(d1);
	//Date(Date& d)
	Date(const Date& d)    //拷贝构造函数
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		//d._year = _year;  //形参加上const如果写反了会报错,就可以轻松看出错误
		//d._month = _month;
		//d._day = _day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
//void Func(Date& d)
// 自定义类型对象,拷贝初始化规定要调用拷贝构造完成
void Func(Date d)
{}

int main()
{
	Date d1(2022, 5, 15);
	Func(d1);
	int x = 0;
	Func(x);

	Date d2(d1); // 拷贝构造
	d2.Print();
	d1.Print();

	return 0;
}

 错误例子:栈类用浅拷贝就会运行崩溃:

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);

		_top = 0;
		_capacity = capacity;
	}

	// st2(st1)
	// 只能深拷贝实现
	/*Stack(const Stack& st)
	{
	_a = st._a;
	_top = st._top;
	_capacity = st._capacity;
	}*/

	~Stack()
	{
		cout << "~Stack():" << this << endl;

		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack st1(10);
	Stack st2(st1);
	return 0;
}

 把st1拷贝给st2,会导致st1和st2中的指针都指向同一个空间,这样有问题的,有2个问题:

①修改数据,增删查改会互相影响:当分别在st1和st2插入数据时,因为top是独立的,所以st1插入数据后 st2再插入时就会覆盖st1插入的数据,就会有问题。

②这块空间析构时会释放两次,程序会崩溃:st1和st2会分别调用析构函数,但是同一个空间不能free两次。

解决方案:要自己实现拷贝构造并且必须是深拷贝,深拷贝后面再讲。

把栈类进行浅拷贝运行结果必然会崩溃:

(但如果这里拷贝的指针变成数组就可以使用浅拷贝,指针malloc开辟的空间在堆上,数组在栈上,两个数组是独立的空间)

3.拷贝构造需要自己写的场景只有Stack这种类!而且是深拷贝。

一般的类,自己生成拷贝构造就够用了。只有像Stack这样类,自己直接管理资源,需要自己实现深拷贝,深拷贝的实现此处不提后面再说。无论是只有内置类型,还是只有自定义类型,还是内置类型和自定义类型混合MyQueue类,只要不是直接管理资源的,就不需要写拷贝构造函数。

内置类型利用默认生成的拷贝构造函数进行值拷贝,类里有自定义类型的话,类默认生成的拷贝构造函数会去调用自定义类型的拷贝构造函数,比如MyQueue类中有Stack,那么MyQueue类默认生成的拷贝构造函数就会去调用Stack类的深拷贝构造函数去拷贝这个对象的成员变量。

class MyQueue
{

private:
	int _size = 0;
	Stack _st1;
	Stack _st2;
};

int main()
{
	MyQueue mq1;
	MyQueue mq2(mq1);

	return 0;
}

4.拷贝构造的易错题

(1)题目1

 当时做这个题我一连串的疑问:B:选项说参数是对某个对象的引用,和答案不矛盾吧?C:自动生成的拷贝构造是缺省的吗?我们写的默认拷贝构造函数都没给缺省值,怎么证明自动生成的有缺省参数呢? D:选项的描述感觉在说浅拷贝,深拷贝也是把数据成员拷贝给另一个对象吗?

 答:实际上还是我的理解不够:

B说的是 "对某个对象的引用" 这个是不够准确的,必须是自身的类型的对象 才叫拷贝构造,B错。

C这里的缺省跟缺省参数没关系,缺省拷贝构造函数=默认拷贝构造函数。默认拷贝构造函数不能 保护private,而应该是共用public,C错。

D:深拷贝也要拷贝数据,D说的没问题。

所以选D。

官方解析:

A.拷贝构造函数也是一构造函数,因此不能有返回值

B.该函数参数是自身类型的对象的引用

C.自动生成的缺省拷贝构造函数,作为该类的公有成员,否则无法进行默认的拷贝构造

D.用对象初始化对象这是拷贝构造函数的使命,故正确

(2)题目2:构造,析构,拷贝构造综合题

下面分别调用了多少次构造函数,拷贝构造函数,析构函数?

答:调用1次构造,调用4次拷贝构造函数,调用5次析构函数

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}

int main()
{
	Weight x;
	f(x);
	return 0;
}

调用1次构造:Weight x; 创建对象x时调用唯一 一次构造函数

调用4次拷贝构造函数:

①Weight f(Weight u) :调用f(x),把x传参给u时,因为是传值传参不是引用传参,所以调用一次拷贝构造。(若此处为Weight f(Weight& u) 引用传参,则会减少一次拷贝构造的调用)

②Weight v(u); :经典的拷贝构造初始化对象v 。

③Weight w = v; :如果是初始化就是调用拷贝构造,如果是前面已经初始化,现在是赋值 w = v 就是调用赋值运算符重载,这里显然是调用拷贝构造初始化对象w。

④return w; :返回值返回w,我们看返回值的类型是Weight ,说明是传值返回,则需要拷贝构造出一个临时对象返回,也调用了一次拷贝构造。(若此处为Weight& f(Weight u)返回引用,则会减少一次拷贝构造的调用)

总计调用了4次拷贝构造。

 调用5次析构函数:

①②③出函数f的作用域时(走完return w;后)会析构 函数内的三个对象u,v,w,调用3次析构函数。

④return w;时返回值是w拷贝构造出来的一个临时变量(临时对象),走完f(x)后,这个临时对象生命周期结束,所以析构了这个临时对象,这是第4次调用析构函数。

 ⑤走完return 0;时 析构main函数中的对象x,这是第5次调用析构函数。

总共调用了5次析构函数。

运行结果:

 

(3)题目2的变形——匿名对象

匿名对象只存在于构造该对象的那行代码,离开构造匿名对象的那行代码后立即调用析构函数。

下面分别调用了多少次构造函数,拷贝构造函数,析构函数?

答:调用1次构造,调用3次拷贝构造函数,调用4次析构函数

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	//Weight x;
	//f(x);
	f(Weight());   //  Weight(); 匿名对象,声明周期只在这一行

	return 0;
}

f(Weight());  中实参是匿名对象,本应该是先构造匿名对象,随后传值传参时把实参拷贝构造给形参u,但由于编译器优化,把先构造后立刻拷贝构造优化为直接构造,实参匿名对象和形参u合二为一,直接构造一个对象,所以相比题目2,这里是少调用了一次拷贝构造和一次析构函数。

 所以说 Weight x;  f(x); 和 f(Weight()); 相比,匿名对象这种传参效率更高。

(4)继续变形

下面分别调用了多少次构造函数,拷贝构造函数,析构函数?

答:调用1次构造,调用4次拷贝构造函数,调用5次析构函数

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	Weight x;
    Weight ret=f(x);

	return 0;
}

 正常情况是调用5次拷贝构造: ①传值传参x->u 调用一次。②Weight v(u); 调用一次。③Weight w = v; 调用一次。④传值返回w->临时对象 调用一次。⑤临时对象给ret 调用一次。

但是由于编译器优化,一个表达式中,连续步骤的构造+拷贝构造,或者拷贝构造+拷贝构造,胆大的编译器可能就会优化,合二为一。因此④⑤合并为一次,所以一共调用4次拷贝构造

析构调用5次:分别是对象 u,v,w,ret,x。

(5)题目4的变形

答:调用2次构造,调用4次拷贝构造函数,调用6次析构函数,1次赋值运算符重载

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	Weight x;
	Weight ret ;
	ret = f(x);
	return 0;
}

(6) 终极复合题

问调用了几次拷贝构造?

class Weight
{
public:
	Weight()
	{
		cout << "Weight()" << endl;
	}

	Weight(const Weight& w)
	{
		cout << "Weight(const Weight& w)" << endl;
	}

	Weight& operator=(const Weight& w)
	{
		cout << "Weight& operator=(const Weight& w)" << endl;
		return *this;
	}

	~Weight()
	{
		cout << "~Weight()" << endl;
	}

};
Weight f(Weight u)
{
	Weight v(u);
	Weight w = v;

	return w;
}
int main()
{
	Weight x;
	Weight y=f(f(x)) ;
	return 0;
}

拷贝构造图解分析:

步骤④和⑤:w拷贝构造一个临时对象,这个临时对象拷贝构造了u,然后编译器优化合二为一;相当于步骤⑧w拷贝构造一个临时对象,这个临时对象拷贝构造了y,然后编译器优化合二为一;

 

5.栈类,日期类,MyQueue类对于构造,析构,拷贝构造的需求

(1)日期类:

需要自己写的:

        构造函数(全缺省构造函数 初始化年月日的值,默认生成的不会初始化内置类型)

不需要自己写的:

        析构函数(把年月日置0好像没什么意义,主要是日期类没有新开辟的空间)

        拷贝构造函数(内置类型值拷贝用默认生成的值拷贝就够用,没有指向的资源)

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)     //构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
 
//  Date d2(d1);
//  Date(Date& d);
//	Date(const Date& d)    //默认拷贝构造函数可写可不写,这个只是展示默认拷贝构造函数的用途
//	{
//		_year = d._year;
//		_month = d._month;
//		_day = d._day;
//	}
 
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
 
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};

int main()
{
	Date d1(2022, 5, 15); 
	Date d2(d1);     //调用拷贝构造

	return 0;
}

(2)栈类:

都需要自己写:

        构造函数(需要初始化,默认生成的不会初始化内置类型)

        析构函数(清理指向资源)

        拷贝构造函数(必须是深拷贝,虽然都是内置类型,但是有指向的资源,浅拷贝会导致析构两次这种情况)

        赋值函数(后面会说)

class Stack
{
public:

	Stack(int capacity = 10)    //构造函数
	{
		_a = (int*)malloc(sizeof(int)*capacity);
		assert(_a);
 
		_top = 0;
		_capacity = capacity;
	}
 
	~Stack()                    //析构函数
	{
		cout << "~Stack():" << this << endl;
 
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
                                //拷贝构造函数是深拷贝,这里暂时写不了
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
    Stack St;
    return 0;
}

(3)MyQueue类(内置类型和自定义类型混合):

class MyQueue
{

private:
	int _size = 0;   
//C++11使成员变量声明时可以被赋值缺省参数,默认构造函数调用时会使用这个缺省参数,使内置类型初始化
	Stack _st1;
	Stack _st2;
};

都不需要自己写:

        构造函数(默认生成的构造函数会去调用Stack类中的构造函数去初始化成员栈,内置类型有缺省参数可以初始化给值)

        析构函数(默认生成的析构函数会去调用Stack类中的析构函数去清理成员栈的指向资源,内置类型本身就不需要析构函数)

        拷贝构造函数(前提是里面的栈已经写了深拷贝,默认生成的拷贝构造函数会去调用Stack类中的拷贝构造函数去拷贝成员栈,默认拷贝构造也直接拷贝内置类型)

四.运算符重载

1.目的:

内置类型,可以直接用各种运算符;自定义类型,不能直接用各种运算符
为了自定义类型可以使用各种运算符,就使用运算符重载的规则(注意:这里的重载和函数重载无关,函数重载是函数名相同,参数不同,运算符重载:重新定义去控制运算符的行为和规则)

C++为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

2.函数格式及细节:

函数名字为:关键字operator后面接需要重载的运算符符号

函数原型:返回值类型 operator操作符(参数列表)
细节:
①不能通过连接其他符号来创建新的操作符:比如operator@
②重载操作符必须有一个类类型或者枚举类型的操作数(即只能对自定义类型进行运算符重载
③用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义让它去当-去用
④作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
.*   ::   sizeof   ?:   . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
*是可以重载的,.*不可以重载(点号.  三目运算?:  作用域访问符::  运算符sizeof   以及.*)
C++规定=,[ ],(),->这四个运算符只能被重载为类的非静态成员函数,其他的可以被友元重载(比如流提取运算符<<必须用友元),主要是因为其他的运算符重载函数都会根据参数类型或数目进行精确匹配,这四个不具有这种检查的功能,用友元定义就会出错

3.探索如果正确使用运算符重载:

判断日期类是否相等:可以写出运算符重载函数,这里没有const和引用,是未完善的版本
bool operator==(Date d1, Date d2)        //运算符重载未完善的版本
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}

>:1 不能把 运算符重载operator写在类的外面

类外无法访问类中的成员变量,如果非要访问,解决办法是:①把private去掉,让成员变量成为共有,显然这样是不安全的;②在类中写一个GetYear函数用于访问类中的成员变量,然后在外面利用这个函数访问成员变量,这样显然是很啰嗦很麻烦的。③利用友元才能写在外面,但是会破坏封装,这里仅做了解即可。
总结:这三种方法都有缺陷,所以不能把 运算符重载operate 写在类的外面!
错误的示例:
​​​​​​​错误的示例:
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	int GetYear()
	{
		return _year;
	}

//private:
	int _year;
	int _month;
	int _day;
};

bool operator==(Date d1, Date d2)        //运算符重载最初始的版本
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
//operator++()

int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	if (operator==(d1, d2))
	{
		cout << "==" << endl;
	}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (operator==(d1, d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

>:2 使用if (d1 == d2) 不是 if (operator==(d1, d2))

如果我们还只是用 if (operator==(d1, d2))  的写法,那不如我们定义的时候把它写成一个普通函数得了:bool equate(Date d1, Date d2)  。这里使用if (d1 == d2) C++会做处理,编译器会处理成对应重载运算符调用 if (operator==(d1, d2))。

>:3 运算符重载operator写进类

operator写进类有this指针,所以应少写一个参数

bool operator==(Date d1, Date d2) 写成  bool operator==( Date d2) ,在编译器眼中就是:

bool operator==(Date* const this, Date d)

调用时用 if (d1 == d2) ,编译器会处理成 if ( d1.operator==(d2) ),在编译器眼中就是:d1.operator==(&d1,d2)


class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

    bool operator==( Date d)       
    {
    	return _year == d._year
	    	&& _month == d._month
	    	&& _day == d._day;
    }

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
  
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

>:4 传参用引用传参&,并加上const

自定义类型传参会调用拷贝构造,没必要,我们用引用传参就能省去拷贝构造了,同时如果我们不改变形参,最好加上const进行保护,①防止你里面的赋值写反。②对于赋值运算符重载可以防止权限被放大,详情见 ——后面的 四.7


class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

   bool operator==(const Date& d)       
    {
    	return _year == d._year
	    	&& _month == d._month
	    	&& _day == d._day;
    }

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
  
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

>:5 运算符重载operator类外面和类里面都写了,优先调用类里面的

运算符重载operator类外面和类里面都写了,编译器优先去类里面的查看是否有运算符重载,有就调用,没有就去类外面找。

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	bool operator==(const Date& d)
	{
		cout << "里面" << endl;
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

//private:
	int _year;
	int _month;
	int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
	cout << "外面" << endl;
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 16);

	//if (d1.operator==(d2))
	//{
	//	cout << "==" << endl;
	//}

	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	return 0;
}

​​​​​​​

五.日期类的运算符重载

1.练习:写一下 if (d1 < d2) 的运算符重载

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//bool operator<(Date d)
	//{
	//	if (_year < d._year)
	//	{
	//		return true;
	//	}
	//	else if(_year == d._year)
	//	{
	//		if(_month < d._month)
	//		{
	//			return true;
	//		}
	//		else if (_month == d._month)
	//		{
	//			if (_day < d._day)
	//			{
	//				return true;
	//			}
	//		}
	//	}
	//	return false;
	//}
	bool operator<(const Date& d)
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && d._day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 3, 16);
	Date d2(2022, 5, 16);

	if (d1 < d2) // 编译器会处理成对应重载运算符调用 if (d1.operator<(d2))
	{
		cout << "<" << endl;
	}
	return 0;
}

2.练习:写一下 d1 = d2 的运算符重载

(1)d1 = d2,if (d1 < d2),if (d1 == d2)运算符重载的代码

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	bool operator==(Date d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

	bool operator<(const Date& d)
	{
		if ((_year < d._year)
			|| (_year == d._year && _month < d._month)
			|| (_year == d._year && _month == d._month && d._day < d._day))
		{
			return true;
		}
		else
		{
			return false;
		}
	}
	Date& operator=(const Date& d)        //d2 = d1的运算符重载
	{
		if (this != &d)     //自己赋给自己,当成员变量太多时浪费时间效率,防止自己赋给自己
//写成if (*this != d) 比较内容不行,因为!=没有运算符重载,只能通过比较指针判断;
//假设有运算符重载,那也是调用函数,调用函数肯定没原生指针比较效率高
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;          //为了满足连等d3 = d2 = d1;要给返回值 
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2022, 3, 16);
	Date d2(2022, 5, 16);
    Date d3(d1); // 拷贝构造 
	if (d1 == d2) // 编译器会处理成对应重载运算符调用 if (d1.operator==(d2))
	{
		cout << "==" << endl;
	}
	if (d1 < d2) // 编译器会处理成对应重载运算符调用 if (d1.operator<(d2))
	{
		cout << "<" << endl;
	}
    d3 = d2 = d1;     // 赋值重载/复制拷贝 -- 两个已经存在对象之间赋值
	//d2 = d1;
	d2.Print();

	return 0;
}

解释: d1 = d2 

①形参的const Date& d,const防止形参改变,引用传参减少拷贝构造函数调用

②if (this != &d)     防止自己赋给自己,当成员变量太多时浪费时间效率

③为了满足连等d3 = d2 = d1;的情况,赋值运算符重载要给返回值,比如d2=d1,赋值完后返回Date& *this,也就是d2的引用了,则可以继续进行赋值 d3=d2 了。

④返回值类型给Date的话是传值拷贝,这个返回值再赋值给main函数的一个变量会调用拷贝构造,所以改成引用,这里的引用是*this,*this就是调用的那个对象d2,返回的是d2的引用。

(2)赋值运算符重载和拷贝构造的区别

用一个对象初始化另一个对象时调用拷贝构造,如果是给已初始化对象赋值就调用运算符重载。

class Date中:

    Date& operator=(const Date& d)
	{
		if (this != &d)
		{
    		_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
和
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
    
    void TestDate2()
    {
	    Date d1(2022, 5, 18);
	    Date d2 = d1 + 15;   //调用拷贝构造    
	    Date d3 = d1;        //调用拷贝构造 Date d3(d1);
	    d3 = d1 + 15;        //调用赋值运算符重载
    }

3.有意义的运算符才会重载,我们还可以重载很多日期类运算符。比如== , < , <= , > , >= , !=

(1)此处是声明写在类中,定义写在类外的形式(== , < , <= , > , >= , != 的运算符重载。)

(构造函数就必须改成下面格式,使输入的日期合法化。)

Date.h

#pragma once
#include<assert.h>
#include<iostream>
using std::cin;    
using std::cout;
using std::endl;
class Date
{
public:
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1);	

	// 拷贝构造函数
  // Date d1(d2);
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	// 析构函数
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

	//运算符重载
	bool operator ==(const Date& d);	//只写<和==,剩下的复用
	bool operator < (const Date& d);	
	bool operator <= (const Date& d);	// <= 复用 < 和 ==
	bool operator >(const Date& d);		// > 复用 <=
	bool operator >= (const Date& d);	// >= 复用 <
	bool operator != (const Date& d);	// != 复用 ==

private:
	int _year;
	int _month;
	int _day;
};

Date.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
int Date::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);
	const static int monthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };  
//1.static细节:不改变数组内容时,加上static防止每次调用都开辟额外空间
	if (month == 2 && isLeapYear(year))
	{
		return 29;
	}
	return monthDayArray[month];
}

Date::Date(int year, int month , int day )	//2.声明给缺省,定义就不给
{
	if (year >= 1 && month >= 1 && month <= 12 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
	}
}

// ==运算符重载	只写==和<,其他都复用
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

//
bool Date::operator < (const Date& d)
{
	if (_year < d._year 
		||(_year == d._year && _month < d._month)
		||(_year == d._year && _month == d._month && _day < d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}
// d1 <= d2 
bool Date::operator <= (const Date& d)
{
	return *this < d || *this == d;
}
bool Date::operator >(const Date& d)
{
	return !(*this <= d);
	//return d < *this;  
这样复用<也是可以的,但是d是const,传给<的非const this指针 
是权限的放大,所以务必在<的运算符重载后加上const,
原则:成员函数内只要不改成成员变量,建议都加const
}
bool Date::operator >= (const Date& d)
{
	return *this > d || *this == d;
}
bool Date::operator != (const Date& d)
{
	return !(*this == d);
}

test.cpp

void TestDate1()
{
	Date d1(2022, 5, 18);
	Date d2(2023, 3, 20);
	Date d3(2023, 3, 20);

	cout << (d1 < d2) << endl;
	cout << (d1 > d2) << endl;
	cout << (d1 == d3) << endl;
	cout << (d2 <= d3) << endl;
	cout << (d2 == d3) << endl;
}

int main()
{
	TestDate1();

	return 0;
}

(2)把定义写在类中(== , < , <= , > , >= , != 的运算符重载。

我们发现复用的那几个定义都比较短,所以我们可以把他们写成内联函数在调用处展开可以提高效率,复用的运算符重载函数把声明写在类中,定义写在类外的时候如果在定义处直接前面加上inline是不能内联的,原因是:内联函数不能声明和定义分离(即:inline不支持声明和定义分别放到.h 和.cpp),(Date.h 在Date.cpp中被展开,因为声明是inline,符号表不会生成函数地址,当test.cpp 中调用函数f时,call(函数名) 这个指令去符号表找函数名和地址映射关系时,找不到函数地址,则无法展开。)

如果想把复用函数变成内联,只需要利用类的特性:在类里面定义的函数默认是inline所以成员函数中要成为inline最好直接在类里面定义

(写完整过程的函数 == 和 < 函数比较长就不内联了)

Date.h

#pragma once
#include<assert.h>
#include<iostream>
using std::cin;
using std::cout;
using std::endl;
class Date
{
public:
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1);
	

	// 拷贝构造函数
  // Date d1(d2);
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	// 析构函数
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

	//运算符重载
	bool operator ==(const Date& d);	//只写<和==,剩下的复用
	bool operator < (const Date& d);	
	bool operator <= (const Date& d)	// <= 复用 < 和 ==
	{
		return *this < d || *this == d;
	}
	bool operator >(const Date& d)	// > 复用 <=
	{
		return !(*this <= d);
		//return d< *this; 暂时不行,权限放大
	}
	bool operator >= (const Date& d)	// >= 复用 <
	{
		return *this > d || *this == d;
	}
	bool operator != (const Date& d)	// != 复用 ==
	{
		return !(*this == d);
	}
private:
	int _year;
	int _month;
	int _day;
};

4.+ ,+= 的运算符重载

Date.h

#pragma once
#include<assert.h>
#include<iostream>
using std::cin;    
using std::cout;
using std::endl;
class Date
{
public:
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);
	
	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1);	

	// 拷贝构造函数
  // Date d1(d2);
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d)		
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	// 析构函数
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}

	// 日期+=天数
	Date& operator+=(int day);
	// 日期+天数
	Date operator+(int day);

private:
	int _year;
	int _month;
	int _day;
};

 Date.cpp

#define _CRT_SECURE_NO_WARNINGS
#include"Date.h"
int Date::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);
	const static int monthDayArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };  
//1.static细节:不改变数组内容时,加上static防止每次调用都开辟额外空间
	if (month == 2 && isLeapYear(year))
	{
		return 29;
	}
	return monthDayArray[month];
}

Date::Date(int year, int month , int day )	//2.声明给缺省,定义就不给
{
	if (year >= 1 && month >= 1 && month <= 12 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
	}
}

// 官方写法
// d1 += 100 日期+=天数
Date& Date::operator+=(int day)
{
	if (day < 0)                //+=负天数会有问题,直接取正复用下面的-=即可
		return *this -= -day;

	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	return *this;
}

//个人的写法 日期+=天数 (仅仅是部分细节不同,思路相同)
//Date& Date::operator+=(int day)
//{
//	_day += day;
//	int daymax = GetMonthDay(_year, _month);
//	while (_day > daymax)
//	{
//		_day -= daymax;
//		++_month;
//		if (_month > 12)
//		{
//			_year++;
//			_month -= 12;
//		}
//		daymax = GetMonthDay(_year, _month);
//	}
//	return *this;
//}

// 官方复用写法
Date Date::operator+(int day)
{
	Date ret(*this);
	ret += day;

	return ret;
}

//官方常规写法
//Date Date::operator+(int day)
//{
//	Date ret(*this);
//
//	ret._day += day;
//	while (ret._day > GetMonthDay(ret._year, ret._month))
//	{
//		ret._day -= GetMonthDay(ret._year, ret._month);
//		ret._month++;
//		if (ret._month == 13)
//		{
//			++ret._year;
//			ret._month = 1;
//		}
//	}
//
//	return ret;
//}

//个人的写法:
//Date Date::operator+(int day)
//{
//	Date ret(*this);
//	ret._day += day;
//	int daymax = GetMonthDay(ret._year, ret._month);
//	while (ret._day > daymax)
//	{
//		ret._day -= daymax;
//		++ret._month;
//		if (ret._month > 12)
//		{
//			ret._year++;
//			ret._month -= 12;
//		}
//		daymax = GetMonthDay(ret._year, ret._month);
//	}
//	return ret;
//	//3.ret是临时变量,所以返回类型不能是引用(不能返回ret的引用,ret出作用域就销毁)
//}

test.cpp


void TestDate2()
{
	Date d1(2022, 5, 18);
	Date d2 = d1 + 15;
    Date d3;
	d3 = d1 + 15;
	d1.Print();
	d2.Print();

	d1 += 15;
	d1.Print();
}
int main()
{
	TestDate2();

	return 0;
}

 运行结果:

​​​​​​​

(1)此处的细节:const对于赋值运算符重载或拷贝构造可以防止权限被放大。

比如这里 Date d2 = d1 + 15; 右边d1 + 15后类型是Date,他不是引用,而是函数内ret的一个临时拷贝,临时拷贝具有常性,自带const小权限,而 Date d2 = d1 + 15; 整体是拷贝构造Date(const Date& d),若不加const,形参就是非const大权限,实参给形参,小权限变成大权限属于权限放大,会报错

 (2)+和+=的互相复用,+复用+=更优一些!

 从细节上来讲:+复用+=更优一些,原因如下:

+复用+=:调用+=时没有产生临时拷贝,调用+时拷贝构造产生一次临时拷贝,返回值产生一次临时拷贝,一共2次;

+=复用+:调用+时拷贝构造产生一次临时拷贝,返回值产生一次临时拷贝,一共2次;调用+=时因为是复用了+,所以也会产生2次临时拷贝。相比之下还是+复用+=临时拷贝少,更优

5.- ,-= 的运算符重载

Date.cpp

Date Date::operator-(int day)
{
	Date ret = *this;
	ret -= day;
	return ret;
}

// d1 -= 100
Date& Date::operator-=(int day)
{
	if (day < 0)            //天数为负时,-=就会有问题,直接取正复用+=即可
		return *this += -day;

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

test.cpp

void TestDate3()
{
	Date d1(2022, 5, 18);
	Date d2 = d1 - 30;
	d2.Print();
	d1 -= 30;
	d1.Print();

}
int main()
{
	TestDate3();
	
	return 0;
}

运行结果:

6.前置,后置++

规定:因为参数名一样,为了区分需要写函数重载

Date& operator++() 是前置++ ;Date operator++(int ) 带参数的是后置++,(一般那个参数都不写,没必要写)

细节1:后置++的形参不能给全缺省参数,错误示例:Date operator++(int i=0),调用时传参如果带参肯定传给后置++,如果不带参就不知道传给前置还是后置++了,因为不带参既可以给没参数的前置++,也可以给全缺省的后置++。

细节2:Date operator++(int ) 后置++的形参规定必须是int型,别的类型不可以。

定义在类中:(类在头文件中)
    // 前置++
	Date& operator++()
	{
		*this += 1;
		return *this;
	}
	// 后置++
	Date operator++(int i)
	{
		Date tmp(*this);
		*this += 1;
		return tmp;
	}

——————————————————test.cpp
void TestDate4()
{
	Date d3(2022, 5, 18);
	d3.Print();
	Date ret1 = ++d3;	//d3.operator++()
	ret1.Print();
	d3.Print();

	Date ret2 = d3++;	//d3.operator++(0)
	ret2.Print();
	d3.Print();
}
int main()
{
	TestDate4();
	
	return 0;
}

7.日期-日期 返回天数

直接减需要考虑进位很麻烦,直接复用加法的进位

————————————————写在类中
// 日期-日期 返回天数
int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (max < min)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (min != max)
	{
		n++;
		min++;
	}
	return n * flag;
}

——————————————test.cpp
void TestDate5()
{
	Date d1(2022, 5, 18);
	Date d2(2020, 2, 4);
	cout << (d1 - d2) << endl;
	cout << (d2 - d1) << endl;
}
int main()
{
	TestDate5();
	
	return 0;
}

8.const的细节

(1)权限放大问题

在TestDate6函数中用非const修饰的对象d1调用Print函数没问题,但是在Func中调用就不行:由于Func中参数const,调用Func函数时,d1传参给d,d1的权限是非const,d的权限是const,在Func中用const修饰的对象d调用Print函数,&d的类型是const Date*,但是Print函数中隐含的this指针就是非const类型的,把const 的 &d 传给非const的 this指针 是权限的放大,会报错,应该想办法把Print函数的this指针改成const修饰,因此有 const修饰类的成员函数:

 (2)const修饰类的成员函数:

const 修饰的类成员函数称之为 const 成员函数 const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。写法是在函数最后加const

 (2)const修饰类的成员函数的格式:

在函数最后加const,如果声明定义分离,声明和定义后面都要加const

void Print(Date* const this)
{
	cout << _year << "-" << _month << "-" << _day << endl;
}


void Print() const     //相当于 void Print(const Date* const this)
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

void Print(const Date* const this) 中, 红色的const是我们加的,蓝色的const是this指针自带的不能修改指针本身,

(3)权限放大问题的解决方法:

所以在.h文件 类 中的Print函数后加上const即可

.h文件 类 中的Print函数:

void Print() const
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

void Func(const Date& d)
{
	d.Print(); // d1.Print(&d); -> const Date*
}

test.cpp中:

void TestDate6()
{
	Date d1(2022, 5, 18);
	d1.Print(); // d1.Print(&d1); -> Date* 
	Func(d1);
}
int main()
{
	TestDate5();
	
	return 0;
}

其他的不用修改this指针指向内容的运算符重载函数我们都需要在后面加上const,==,<,>,+,-等等,+=和-=因为要改变this指针指向内容的不能加。

(4)const使用规则

建议成员函数中不修改成员变量的成员函数,都可以加上const,普通对象和const对象都可以调用
 

1. const对象可以调用非const成员函数吗?

答:不可以,调用时实参是const修饰的,形参是非const,传参就是const给非const,权限放大,不可以。
2. 非const对象可以调用const成员函数吗?
答:可以,同理,权限缩小(可读可写改成只读)是可以的。
3. const成员函数内可以调用其它的非const成员函数吗?
答:不可以。下图所示当给+运算符重载函数用const修饰时,内部使用到了非const指针GetMonthDay函数就报错!!!,如果想正常运行还得给 GetMonthDay函数 加const,GetMonthDay函数 里面还有个非const的isLeapYear也得改成非const,使用就很麻烦。不如直接把+复用+=就很简单。

 +复用+=

 

 4. 非const成员函数内可以调用其它的const成员函数吗?

答:可以的,类似的是权限缩小。

六.取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

.h的类中
	Date* operator&()    //取地址操作符重载
	{
		return this;
	}

	const Date* operator&() const     //const取地址操作符重载
	{
		return this;
	}

test.cpp中:

void Func(const Date& d)
{
	d.Print(); // d1.Print(&d); -> const Date*

	cout << &d << endl;
}
void TestDate6()
{
	Date d1(2022, 5, 18);
	d1.Print(); // d1.Print(&d1); -> Date* 
	Func(d1);
}
int main()
{
	TestDate6();
	
	return 0;
}

  • 49
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 30
    评论
谭浩强教授,我国著名计算机教育专家。1934年生。1958年清华大学毕业。学生时代曾担任清华大学学生会主席、北京市人民代表。他是我国计算机普及和高校计算机基础教育开拓者之一,现任全国高等院校计算机基础教育研究会会长、教育部全国计算机应用技术证书考试委员会主任委员。 谭浩强教授创造了3个世界纪录:(1)20年来他(及和他人合作)共编著出版了130本计算机著作,此外主编了250多本计算机书籍,是出版科技著作数量最多的人。(2)他编著和主编的书发行量超过4500万册,是读者最多的科技作家。我国平均每30人、知识分子每1.5人就拥有1本谭浩强教授编著的书。(3)他和别人合作编著的《BASIC语言》发行了1200万册,创科技书籍发行量的世界纪录。此外,他编著的《C程序设计》发行了600万册。他曾在中央电视台主讲了BASIC,FORTRAN,COBOL,Pascal,QBASIC,C,Visual Basic七种计算机语言,观众超过300万人。 在我国学习计算机的人中很少有不知道谭浩强教授的。他善于用容易理解的方法和语言说明复杂的概念。许多人认为他开创了计算机书籍贴近大众的新风,为我国的计算机普及事业做出了重要的贡献。 谭浩强教授曾获全国高校教学成果国家级奖、国家科技进步奖,以及北京市政府授予的“有突出贡献专家”称号。《计算机世界》报组织的“世纪评选”把他评为我国“20世纪最有影响的IT人物”10个人之一(排在第2位)。他的功绩是把千百万群众带入计算机的大门。 1 C语言概述 1.1 C语言的发展过程 1.2 当代最优秀的程序设计语言 1.3 C语言版本 1.4 C语言的特点 1.5 面向对象的程序设计语言 1.6 C和C++ 1.7 简单的C程序介绍 1.8 输入和输出函数 1.9 C源程序的结构特点 1.10 书写程序时应遵循的规则 1.11 C语言的字符集 1.12 C语言词汇 1.13 Turbo C 2.0 集成开发环境的使用 1.13.1 Turbo C 2.0 简介和启动 1.13.2 Turbo C 2.0 集成开发环境 1.13.3 File菜单 1.13.4 Edit 菜单 1.13.5 Run 菜单 1.13.6 Compile 菜单 11.13.7 Project 菜单 1.13.8 Options菜单 1.13.9 Debug 菜单 1.13.10 Break/watch 菜单 1.13.11 Turbo C 2.0 的配置文件 2 程序的灵魂—算法 2.1 算法的概念 21 2.2 简单算法举例 21 2.3 算法的特性 24 2.4 怎样表示一个算法 24 2.4.1 用自然语言表示算法 24 2.4.2 用流程图表示算法 24 2.4.3 三种基本结构和改进的流程图 28 2.4.4 用N-S 流程图表示算法 29 2.4.5 用伪代码表示算法 30 2.4.6 用计算机语言表示算法 31 2.5 结构化程序设计方法 31 3 数据类型、运算符与表达式 3.1 C语言的数据类型 32 3.2 常量与变量 33 23.2.1 常量和符号常量 33 3.2.2 变量 33 3.3 整型数据 34 3.3.1 整型常量的表示方法 34 3.3.2 整型变量 35 3.4 实型数据 37 3.4.1 实型常量的表示方法 37 3.4.2 实型变量 38 3.4.3 实型常数的类型 39 3.5 字符型数据 39 3.5.1 字符常量 39 3.5.2 转义字符 39 3.5.3 字符变量 40 3.5.4 字符数据在内存中的存储形式及使用方法 41 3.5.5 字符串常量 41 3.5.6 符号常量 42 3.6 变量赋初值 42 3.7 各类数值型数据之间的混合运算 43 3.8 算术运算符和算术表达式 44 3.8.1 C运算符简介 44 3.8.2 算术运算符和算术表达式 45 3.9 赋值运算符和赋值表达式 47 33.10 逗号运算符和逗号表达式 48 3.11 小结 49 3.11.1 C的数据类型 49 3.11.2 基本类型的分类及特点 49 3.11.3 常量后缀 49 3.11.4 常量类型 49 3.11.5 数据类型转换 49 3.11.6 运算符优先级和结合性 50 表达式 50 4 最简单的 C程序设计—顺序程序设计 4.1 C语句概述 51 4.2 赋值语句 53 4.3 数据输入输出的概念及在 C 语言中的实现 54 4.4 字符数据的输入输出 54 4.4.1 putchar 函数(字符输出函数) 54 4.4.2 getchar函数(键盘输入函数) 55 4.5 格式输入与输出 55 4.5.1 printf 函数(格式输出函数) 56 4.5.2 scanf函数(格式输入函数) 58 顺序结构程序设计举例 60 45 分支结构程序 5.1 关系运算符和表达式 61 5.1.1 关系运算符及其优先次序 61 5.1.2 关系表达式 61 5.2 逻辑运算符和表达式 62 5.2.1 逻辑运算符极其优先次序 62 5.2.2 逻辑运算的值 63 5.2.3 逻辑表达式 63 5.3 if 语句 64 5.3.1 if语句的三种形式 64 5.3.2 if语句的嵌套 67 5.3.3 条件运算符和条件表达式 69 5.4 switch语句 70 5.5 程序举例 71 6 循环控制 6.1 概述 71 6.2 goto 语句以及用goto 语句构成循环 71 6.3 while语句 72 6.4 do-while语句 74 6.5 for 语句 76 6.6 循环的嵌套 79 56.7 几种循环的比较 79 6.8 break 和 continue语句 79 6.8.1 break 语句 79 6.8.2 continue 语句 80 6.9 程序举例 81 7 数组 7.1 一维数组的定义和引用 82 7.1.1 一维数组的定义方式 82 7.1.2 一维数组元素的引用 83 7.1.3 一维数组的初始化 84 7.1.4 一维数组程序举例 84 7.2 二维数组的定义和引用 86 7.2.1 二维数组的定义 86 7.2.2 二维数组元素的引用 86 7.2.3 二维数组的初始化 87 7.2.4 二维数组程序举例 89 7.3 字符数组 89 7.3.1 字符数组的定义 89 7.3.2 字符数组的初始化 89 7.3.3 字符数组的引用 90 7.3.4 字符串和字符串结束标志 91 67.3.5 字符数组的输入输出 91 7.3.6 字符串处理函数 92 7.4 程序举例 94 本章小结 97 8 函 数 8.1 概述 98 8.2 函数定义的一般形式 99 8.3 函数的参数和函数的值 100 8.3.1 形式参数和实际参数 101 8.3.2 函数的返回值 102 8.4 函数的调用 106 8.4.1 函数调用的一般形式 106 8.4.2 函数调用的方式 106 8.4.3 被调用函数的声明和函数原型 107 8.5 函数的嵌套调用 108 8.6 函数的递归调用 109 8.7 数组作为函数参数 110 8.8 局部变量和全局变量 112 8.8.1 局部变量 113 8.8.2 全局变量 119 8.9 变量的存储类别 120 78.9.1 动态存储方式与静态动态存储方式 120 8.9.2 auto变量 120 8.9.3 用static 声明局部变量 121 8.9.4 register 变量 122 用extern 声明外部变量 123 9 预处理命令 9.1 概述 124 9.2 宏定义 125 9.2.1 无参宏定义 126 9.2.2 带参宏定义 127 9.3 文件包含 128 9.4 条件编译 130 9.5 本章小结 10 指针 10.1 地址指针的基本概念 131 10.2 变量的指针和指向变量的指针变量 132 10.2.1 定义一个指针变量 133 10.2.2 指针变量的引用 133 10.2.3 指针变量作为函数参数 137 10.2.4 指针变量几个问题的进一步说明 140 810.3 数组指针和指向数组的指针变量 141 10.3.1 指向数组元素的指针 142 10.3.2 通过指针引用数组元素 143 10.3.3 数组名作函数参数 146 10.3.4 指向多维数组的指针和指针变量 148 10.4 字符串的指针指向字符串的针指变量 150 10.4.1 字符串的表示形式 152 10.4.2 使用字符串指针变量与字符数组的区别 158 10.5 函数指针变量 159 10.6 指针型函数 160 10.7 指针数组和指向指针的指针 161 10.7.1 指针数组的概念 161 10.7.2 指向指针的指针 164 10.7.3 main 函数的参数 166 10.8 有关指针的数据类型和指针运算的小结 167 10.8.1 有关指针的数据类型的小结 167 10.8.2 指针运算的小结 167 10.8.3 void 指针类型 168 11 结构体与共用体 11.1 定义一个结构的一般形式 170 11.2 结构类型变量的说明 172 911.3 结构变量成员的表示方法 174 11.4 结构变量的赋值 174 11.5 结构变量的初始化 175 11.6 结构数组的定义 175 11.7 结构指针变量的说明和使用 177 11.7.1 指向结构变量的指针 177 11.7.2 指向结构数组的指针 179 11.7.3 结构指针变量作函数参数 180 11.8 动态存储分配 181 11.9 链表的概念 182 11.10 枚举类型 184 11.10.1 枚举类型的定义和枚举变量的说明 184 11.10.2 枚举类型变量的赋值和使用 185 11.11 类型定义符typedef 12 位运算 12.1 位运算符C语言提供了六种位运算符: 189 12.1.1 按位与运算 191 12.1.2 按位或运算 192 12.1.3 按位异或运算 192 12.1.4 求反运算 193 12.1.5 左移运算 193 1012.1.6 右移运算 193 12.2 位域(位段) 194 12.3 本章小结 13 文件 13.1 C文件概述 197 13.2 文件指针 198 13.3 文件的打开与关闭 199 13.3.1 文件的打开(fopen 函数) 200 13.3.2 文件关闭函数(fclose函数) 202 13.4 文件的读写 204 13.4.1 字符读写函数fgetc 和fputc 204 13.4.2 字符串读写函数fgets 和fputs 208 13.4.3 数据块读写函数fread 和fwtrite 209 13.4.4 格式化读写函数fscanf和fprintf 201 13.5 文件的随机读写 202 13.5.1 文件定位 202 13.5.2 文件的随机读写 203 13.6 文件检测函数 204 13.6.1 文件结束检测函数 feof函数 204 13.6.2 读写文件出错检测函数 205 1113.6.3 文件出错标志和文件结束标志置 0 函数 206 13.7 C库文件 208 13.8 本章小结 第1篇 基本知识 第1章 C++的初步知识 *1.1 从C到C++ *1.2 最简单的C++程序 1.3 C++程序的构成和书写形式 1.4 C++程序的编写和实现 1.5 关于C++上机实践 习题 第2章 数据类型与表达式 2.1 C++的数据类型 2.2 常量 2.2.1 什么是常量 2.2.2 数值常量 2.2.3 字符常量 2.2.4 符号常量 2.3 变量 2.3.1 什么是变量 2.3.2 变量名规则 2.3.3 定义变量 2.3.4 为变量赋初值 2.3.5 常变量 2.4 C++的运算符 2.5 算术运算符与算术表达式 2.5.1 基本的算术运算符 2.5.2 算术表达式和运算符的优先级与结合性 2.5.3 表达式中各类数值型数据间的混合运算 2.5.4 自增和自减运算符 2.5.5 强制类型转换运算符 2.6 赋值运算符与赋值表达式 2.6.1 赋值运算符 2.6.2 赋值过程中的类型转换 2.6.3 复合的赋值运算符 2.6.4 赋值表达式 2.7 逗号运算符与逗号表达式 习题 第2篇 面向过程的程序设计 第3章 程序设计初步 3.1 面向过程的程序设计和算法 3.1.1 算法的概念 3.1.2 算法的表示 3.2 C++程序和语句 3.3 赋值语句 3.4 C++的输入与输出 *3.4.1 输入流与输出流的基本操作 *3.4.2 在输入流与输出流中使用控制符 3.4.3 用getchar和putchar函数进行字符的输入和输出 3.4.4 用scanf和printf函数进行输入和输出 3.5 编写顺序结构的程序 3.6 关系运算和逻辑运算 3.6.1 关系运算和关系表达式 3.6.2 逻辑常量和逻辑变量 3.6.3 逻辑运算和逻辑表达式 3.7 选择结构和if语句 3.7.1 if语句的3种形式 3.7.2 if语句的嵌套 3.8 条件运算符和条件表达式 3.9 多分支选择结构和switch语句 3.10 编写选择结构的程序 3.11 循环结构和循环语句 3.11.1 用while语句构成循环 3.11.2 用do-while语句构成循环 3.11.3 用for语句构成循环 3.11.4 几种循环的比较 3.12 循环的嵌套 3.13 break语句和continue语句 3.14 编写循环结构的程序 习题 第4章 函数与预处理 4.1 概述 4.2 定义函数的一般形式 4.2.1 定义无参函数的一般形式 4.2.2 定义有参函数的一般形式 4.3 函数参数和函数的值 4.3.1 形式参数和实际参数 4.3.2 函数的返回值 4.4 函数的调用 4.4.1 函数调用的一般形式 4.4.2 函数调用的方式 4.4.3 对被调用函数的声明和函数原型 *4.5 内置函数 *4.6 函数的重载 *4.7 函数模板 *4.8 有默认参数的函数 4.9 函数的嵌套调用 4.10 函数的递归调用 4.11 局部变量和全局变量 4.11.1 局部变量 4.11.2 全局变量 4.12 变量的存储类别 4.12.1 动态存储方式与静态存储方式 4.12.2 自动变量 4.12.3 用static声明静态局部变量 4.12.4 用register声明寄存器变量 4.12.5 用extern声明外部变量 4.12.6 用static声明静态外部变量 4.13 变量属性小结 4.14 关于变量的声明和定义 4.15 内部函数和外部函数 4.15.1 内部函数 4.15.2 外部函数 4.16 预处理命令 4.16.1 宏定义 4.16 2 “文件包含”处理 4.16.3 条件编译 习题 第5章 数组 5.1 数组的概念 5.2 一维数组的定义和引用 5.2.1 定义一维数组 5.2.2 引用一维数组的元素 5.2.3 一维数组的初始化 5.2.4 一维数组程序举例 5.3 二维数组的定义和引用 5.3.1 定义二维数组 5.3.2 二维数组的引用 5.3.3 二维数组的初始化 5.3.4 二维数组程序举例 5.4 用数组名作函数参数 5.5 字符数组 5.5.1 字符数组的定义和初始化 5.5.2 字符数组的赋值与引用 5.5.3 字符串和字符串结束标志 5.5.4 字符数组的输入输出 5.5.5 字符串处理函数 5.5.6 字符数组应用举例 *5.6 C++处理字符串的方法——字符串类与字符串变量 5.6.1 字符串变量的定义和引用 5.6.2 字符串变量的运算 5.6.3 字符串数组 5.6.4 字符串运算举例 习题 第6章 指针 6.1 指针的概念 6.2 变量与指针 6.2.1 定义指针变量 6.2.2 引用指针变量 6.2.3 指针作为函数参数 6.3 数组与指针 6.3.1 指向数组元素的指针 6.3.2 用指针变量作函数参数接收数组地址 6.3.3 多维数组与指针 6.4 字符串与指针 6.5 函数与指针 6.5.1 用函数指针变量调用函数 6.5.2 用指向函数的指针作函数参数 6.6 返回指针值的函数 6.7 指针数组和指向指针的指针 6.7.1 指针数组的概念 6.7.2 指向指针的指针 6.8 有关指针的数据类型和指针运算的小结 6.8.1 有关指针的数据类型的小结 6.8.2 指针运算小结 *6.9 引用 6.9.1 什么是变量的引用 6.9.2 引用的简单使用 6.9.3 引用作为函数参数 习题 第7章 自定义数据类型 7.1 结构体类型 7.1.1 结构体概述 7.1.2 结构体类型变量的定义方法及其初始化 7.1.3 结构体变量的引用 7.1.4 结构体数组 7.1.5 指向结构体变量的指针 7.1.6 结构体类型数据作为函数参数 *7.1.7 动态分配和撤销内存的运算符new和delete 7.2 共用体 7.2.1 共用体的概念 7.2.2 对共用体变量的访问方式 7.2.3 共用体类型数据的特点 7.3 校举类型 7.4 用typedef声明类型 习题 第3篇 基于对象的程序设计 第8章 类和对象 8.1 面向对象程序设计方法概述 8.1.1 什么是面向对象的程序设计 8.1.2 面向对象程序设计的特点 8.1.3 类和对象的作用 8.1.4 面向对象的软件开发 8.2 类的声明和对象的定义 8.2.1 类和对象的关系 8.2.2 声明类类型 8.2.3 定义对象的方法 8.2.4 类和结构体类型的异同 8.3 类的成员函数 8.3.1 成员函数的性质 8.3.2 在类外定义成员函数 8.3.3 inline成员函数 8.3.4 成员函数的存储方式 8.4 对象成员的引用 8.4.1 通过对象名和成员运算符访问对象中的成员 8.4.2 通过指向对象的指针访问对象中的成员 8.4.3 通过对象的引用变量来访问对象中的成员 8.5 类的封装性和信息隐蔽 8.5.1 公用接口与私有实现的分离 8.5.2 类声明和成员函数定义的分离 8.5.3 面向对象程序设计中的几个名词 8.6 类和对象的简单应用举例 习题 第9章 关于类和对象的进一步讨论 9.1 构造函数 9.1.1 对象的初始化 9.1.2 构造函数的作用 9.1.3 带参数的构造函数 9.1.4 用参数初始化表对数据成员初始化 9.1.5 构造函数重载 9.1.6 使用默认参数的构造函数 9.2 析构函数 9.3 调用构造函数析构函数的顺序 9.4 对象数组 9.5 对象指针 9.5.1 指向对象的指针 9.5.2 指向对象成员的指针 9.5.3 this指针 9.6 共用数据的保护 9.6.1 常对象 9.6.2 常对象成员 9.6.3 指向对象的常指针 9.6.4 指向常对象的指针变量 9.6.5 对象的常引用 9.6.6 const型数据的小结 9.7 对象的动态建立和释放 9.8 对象的赋值和复制 9.8.1 对象的赋值 9.8.2 对象的复制 9.9 静态成员 9.9.1 静态数据成员 9.9.2 静态成员函数 9.10 友元 9.10.1 友元函数 9.10.2 友元类 9.11 类模板 习题 第10章 运算符重载 10.1 什么是运算符重载 10.2 运算符重载的方法 10.3 重载运算符的规则 10.4 运算符重载函数作为类成员函数和友元函数 10.5 重载双目运算符 10.6 重载单目运算符 10.7 重载流插入运算符和流提取运算符 10.7.1 重载流插入运算符“<<” 10.7.2 重载流提取运算符“>>” 10.8 不同类型数据间的转换 10.8.1 标准类型数据间的转换 10.8.2 转换构造函数 10.8.3 类型转换函数 习题 第4篇 面向对象的程序设计 第11章 继承与派生 11.1 继承与派生的概念 11.2 派生类的声明方式 11.3 派生类的构成 11.4 派生类成员的访问属性 11.4.1 公用继承 11.4.2 私有继承 11.4.3 保护成员和保护继承 11.4.4 多级派生时的访问属性 11.5 派生类的构造函数析构函数 11.5.1 简单的派生类的构造函数 11.5.2 有子对象的派生类的构造函数 11.5.3 多层派生时的构造函数 11.5.4 派生类构造函数的特殊形式 11.5.5 派生类的析构函数 11.6 多重继承 11.6.1 声明多重继承的方法 11.6.2 多重继承派生类的构造函数 11.6.3 多重继承引起的二义性问题 11.6.4 虚基类 11.7 基类与派生类的转换 11.8 继承与组合 11.9 继承在软件开发中的重要意义 习题 第12章 多态性与虚函数 12.1 多态性的概念 12.2 一个典型的例子 12.3 虚函数 12.3.1 虚函数的作用 12.3.2 静态关联与动态关联 12.3.3 在什么情况下应当声明虚函数 12.3.4 虚析构函数 12.4 纯虚函数与抽象类 12.4.1 纯虚函数 12.4.2 抽象类 12.4.3 应用实例 习题 第13章 输入输出流 13.1 C++的输入和输出 13.1.1 输入输出的含义 13.1.2 C++的I/O对C的发展——类型安全和可扩展性 13.1.3 C++的输入输出流 13.2 标准输出流 13.2.1 cout,cerr和clog流 13.2.2 格式输出 13.2.3 用流成员函数put输出字符 13.3 标准输入流 13.3.1 cin流 13.3.2 用于字符输入的流成员函数 13.3.3 istream类的其他成员函数 13.4 文件操作与文件流 13.4.1 文件的概念 13.4.2 文件流类与文件流对象 13.4.3 文件的打开与关闭 13.4.4 对ASCII文件的操作 13.4.5 对二进制文件的操作 13.5 字符串流 习题 第14章 C++工具 14.1 异常处理 14.1.1 异常处理的任务 14.1.2 异常处理的方法 14.1.3 在函数声明中进行异常情况指定 14.1.4 在异常处理中处理析构函数 14.2 命名空间 14.2.1 为什么需要命名空间 14.2.2 什么是命名空间 14.2.3 使用命名空间解决名字冲突 14.2.4 使用命名空间成员的方法 14.2.5 无名的命名空间 14.2.6 标准命名空间std 14.3 使用早期的函数库 习题 附录A 常用字符与ASCII代码对照表 附录B 运算符与结合性 参考文献 《清华大学计算机系列教材:数据结构(第2版)》第二版在保持原书基本框架和特色的基础上,对主要各章,如第一、二、三、四、六及九章等,作了增删和修改。   《清华大学计算机系列教材:数据结构(第2版)》系统地介绍了各种类型的数据结构和查找、排序的各种方法。对每一种数据结构,除了详细阐述其基本概念和具体实现外,并尽可能对每种操作给出类PASCAL的算法,对查找和排序的各种算法,还着重在时间上作出定量或定性的分析比较。最后一章讨论文件的各种组织方法。   《清华大学计算机系列教材:数据结构(第2版)》概念清楚,内容丰富,并有配套的《数据结构题集》(第二版),既便于教学,又便于自学。   《清华大学计算机系列教材:数据结构(第2版)》可作为计算机类专业和信息类相关专业的教材,也可供从事计算机工程与应用工作的科技工作者参考。 第一章 绪论 1.1 什么是数据结构 1.2 基本概念和术语 1.3 数据结构的发展简史及它在计算机科学中所处的地位 1.4 算法的描述和算法分析 1.4.1 算法的描述 1.4.2 算法设计的要求 1.4.3 算法效率的度量 1.4.4 算法的存储空间需求 第二章 线性表 2.1 线性表的逻辑结构 2.2 线性表的顺序存储结构 2.3 线性表的链式存储结构 2.3.1 线性链表 2.3.2 循环链表 2.3,3 双向链表 2.4 一元多项式的表示及相加 第三章 栈和队列 3.1 栈 3.1.1 抽象数据类型栈的定义 3.1.2 栈的表示和实现 3.2 表达式求值 **3.3 栈与递归过程 3.3.1 递归过程及其实现 3.3.2 递归过程的模拟 3.4 队列 3.4.1 抽象数据类型队列的定义 3.4.2 链队列——队列的链式存储结构 3.4.3 循环队列——队列的顺序存储结构 3.5 离散事件模拟 第四章 串 4.1 串及其操作 4.1.1 串的逻辑结构定义 4.1.2 串的基本操作 4.2 串的存储结构 4.2.1 静态存储结构 4.2.2 动态存储结构 4.3 串基本操作的实现 4.3.1 静态结构存储串时的操作 4.3.2 模式匹配的一种改进算法 4.3.3 堆结构存储串时的操作 4.4 串操作应用举例 4.4.1 文本编辑 **4.4.2 建立词索引表 第五章 数组和广义表 5.1 数组的定义和运算 5.2 数组的顺序存储结构 5.3 矩阵的压缩存储 5.3.1 特殊矩阵 5.3.2 稀疏矩阵 5.4 广义表的定义 5.5 广义表的存储结构 **5.6 m元多项式的表示 **5.7 广义表的递归算法 5.7.1 求广义表的深度 5.7.2 复制广义表 5.7.3 建立广义表的存储结构 第六章 树和二叉树 6.1 树的结构定义和基本操作 6.2 二叉树 6.2.1 定义与基本操作 6.2.2 二叉树的性质 6.2.3 二叉树的存储结构 6.3 遍历二叉树和线索二叉树 6.3.1 遍历二叉树 5.3.2 线索二叉树 6.4 树和森林 6.4.1 树的存储结构 6.4.2 森林与二叉树的转换 6.4.3 树的遍历 **6.5 树与等价问题 6.6 哈夫曼树及其应用 6.6.1 最优二叉树(哈夫曼树) 6.6.2 哈夫曼编码 **6.7 回溯法与树的遍历 **6.8 树的计数 第七章 图 7.1 图的定义和术语 7.2 图的存储结构 7.2.1 数组表示法 7.2.2 邻接表 7.2.3 十字链表 7.2.4 邻接多重表 7.3 图的遍历 7.3.1 深度优先搜索 7.3.2 广度优先搜索 7.4 图的连通性问题 7.4.1 无向图的连通分量和生成树 **7.4.2 有向图的强连通分量 7.4.3 最小生成树 **7.4.4 关节点和重连通分量 7.5 有向无环图及其应用 7.5.1 拓扑排序 7.5.2 关键路径 7.6 最短路径 7.6.1 从某个源点到其余各顶点的最短路径 7.6.2 每一对顶点之间的最短路径 **7.7 二部图与图匹配 第八章 动态存储管理 8.1 概述 8.2 可利用空间表及分配方法 8.3 边界标识法 8.3.1 可利用空间表的结构 8.3.2 分配算法 8.3.3 回收算法 8.4 伙伴系统 8.4.1 可利用空间表的结构 8.4.2 分配算法 8.4.3 回收算法 8.5 无用单元收集 8.6 存储紧缩 第九章 查找 9.1 静态查找表 9.1.1 顺序表的查找 9.1.2 有序表的查找 9.1.3 静态树表的查找 9.1.4 索引顺序表的查找 9.2 动态查找表 9.2.1 二叉排序树和平衡二叉树 9.2.2 B_树和B+树 9.2.3 键树 9.3 哈希表 9.3.1 什么是哈希表 9.3.2 哈希函数的构造方法 9.3.3 处理冲突的方法 9.3.4 哈希表的查找及其分析 第十章 内部排序 10.1 概述 10.2 插入排序 10.2.1 直接插入排序 10.2.2 其它插入排序 10.2.3 希尔排序 10.3 快速排序 10.4 选择排序 10.4.1 简单选择排序 10.4.2 树形选择排序 10.4.3 堆排序 10.5 归并排序 10.6 基数排序 10.6.1 多关键字的排序 10.6.2 链式基数排序 10.7 各种内部排序方法的比较讨论 第十一章 外部排序 11.1 外存信息的存取 11.2 外部排序的方法 11.3 多路平衡归并的实现 11.4 置换-选择排序 **11.5 缓冲区的并行操作处理 11.6 最佳归并树 **11.7 磁带归并排序 11.7.1 平衡归并 11.7.2 多步归并 第十二章 文件 12.1 有关文件的基本概念 12.2 顺序文件 12.3 索引文件 12.4 ISAM文件和VSAM文件 12.4.1 ISAM文件 12.4.2 VSAM文件 12.5 直接存取文件(散列文件) 12.6 多关键字文件 12.6.1 多重表文件 12.6.2 倒排文件 附录一 类PASCAL语言扩充部分的语法图 附录二 名词索引 附录三 过程和函数索引 参考书目 《面向对象的C++数据结构算法实现与解析》是采用面向对象的c++语言数据结构教材的学习辅导书,主要内容包括采用c++语言的类、模板、虚函数、友元、友类编写的各种主要数据存储结构的算法、基本操作成员函数、调用这些成员函数的主程序和程序运行结果以及各主要数据存储结构的图示。《面向对象的C++数据结构算法实现与解析》还介绍了stl模板的应用。   《面向对象的C++数据结构算法实现与解析》结合存储结构和算法,配合大量的图示,对于一些较难理解的算法,还配有文字说明。   《面向对象的C++数据结构算法实现与解析》适用于高等学校学生和自学者,同时也是很好的考研参考书。 第1章 线性表 1.1 顺序存储结构 1.2 链式存储结构 1.2.1 单链表 1.2.2 单循环链表 1.2.3 向循环链表 1.2.4 不设头结点的链表 1.3 静态链表存储结构 第2章 栈和队列 2.1 栈 2.1.1 栈的顺序存储结构 2.1.2 栈的链式存储结构 2.2 栈的应用与递归 2.2.1 数制转换 2.2.2 表达式求值 2.2.3 汉诺塔问题与递归的实现 2.2.4 迷宫问题 2.2.5 皇后问题 2.2.6 马踏棋盘问题 2.2.7 背包问题 2.3 队列 2.3.1 队列的链式存储结构 2.3.2 队列的顺序存储结构 2.4 队列的应用——排队和排队机的模拟 第3章 字符串和矩阵 3.1 字符串 3.1.1 字符串的按需(堆)存储结构 3.1.2 字符串的模式匹配算法 3.2 矩阵 3.2.1 多维数组的顺序存储结构 3.2.2 矩阵的压缩存储 第4章 树与二叉树 4.1 二叉树的顺序存储结构 4.2 二叉树的链式存储结构 4.3 二叉树的遍历 4.4 线索二叉树 4.5 二叉排序树 4.6 平衡二叉树 4.7 红黑树 4.8 伸展树 4.9 树的存储结构 4.10 赫夫曼树和赫夫曼编码 第5章 图 5.1 图的邻接矩阵存储结构 5.2 图的邻接表存储结构 5.3 图的深度优先遍历和广度优先遍历 5.4 图的应用 5.4.1 无向图的连通分量和生成树 5.4.2 最小生成树 5.4.3 关节点和重连通分量 5.4.4 拓扑排序和关键路径 5.4.5 最短路径 第6章 查找 6.1 静态查找表 6.2 静态树表 6.3 哈希表的插入、删除及查找 6.4 动态查找表 6.4.1 b树 6.4.2 键树 第7章 内部排序 7.1 插入排序 7.2 冒泡排序 7.3 简单选择排序 7.4 希尔排序 7.5 快速排序 7.6 堆排序 7.7 二路归并排序 7.8 静态链表排序 7.9 基数排序 第8章 外部排序 8.1 多路平衡归并 8.2 置换-选择排序 第9章 动态存储管理 9.1 边界标识法 9.2 伙伴系统 参考文献
构造函数析构函数C++类中的两个重要函数。 构造函数用于初始化对象的数据成员,在对象创建时自动调用,可以有参数也可以没有参数。 析构函数用于清理对象占用的资源,在对象销毁时自动调用,没有参数也没有返回值。 下面是一个简单的类示例,包含了构造函数析构函数的实现: ```c++ #include <iostream> using namespace std; class MyClass { public: MyClass(); // 声明默认构造函数 MyClass(int num); // 声明带参数构造函数 ~MyClass(); // 声明析构函数 private: int *ptr; }; // 默认构造函数 MyClass::MyClass() { cout << "Default constructor called." << endl; ptr = new int; *ptr = 0; } // 带参数构造函数 MyClass::MyClass(int num) { cout << "Parameterized constructor called." << endl; ptr = new int; *ptr = num; } // 析构函数 MyClass::~MyClass() { cout << "Destructor called." << endl; delete ptr; } int main() { MyClass obj1; // 调用默认构造函数 MyClass obj2(10); // 调用带参数构造函数 return 0; } ``` 输出结果: ``` Default constructor called. Parameterized constructor called. Destructor called. Destructor called. ``` 在上面的例子中,我们定义了一个名为MyClass的类,并实现了默认构造函数、带参数构造函数析构函数。 在main函数中,我们创建了两个MyClass对象,一个使用默认构造函数创建,一个使用带参数构造函数创建。在程序结束时,析构函数会自动被调用,释放对象占用的资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值