类和对象(二)

67 阅读10分钟
class A
{};

首先,空类中并不是什么都没有,编译器会给我们自动生成以下6个默认成员函数。

image-20240218204426718

class Stack
{
public:
	void Init(int n = 4)
	{
		st = (int*)malloc(sizeof(int) * n);
		if (nullptr == st)
		{
			perror("malloc");
			return;
		}

		size = 0;
		capacity = n;
	}

	void Push(int x)
	{
		st[size++] = x;
	}

	void Pop()
	{
		// ..
	}

private:
	int* st;
	int size;
	int capacity;
};

int main()
{
	Stack st;

	// 用c语言实现栈的时候时常会忘记初始化,也会忘记st.Destory()函数
	// 所以最好的方式是交给编译器让它自动来做。
	st.Push(1);
	st.Push(2);
	st.Push(3);
	
	return 0;
}

之前使用C语言写数据结构时经常忘记对某些数据结构的初始化和销毁资源工作,从而引起C++里面的默认构造函数。

构造函数

概念

名字与类名相同,创建类类型对象时由编译器自动调用。

特性

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

有如下特征:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 可以重载。(多种初始化方式)
  4. 对象实例化时编译器自动调用对应的构造函数。
class Stack
{
public:

	// 作用就是类似Init函数的功能
	// 无参的构造函数
	Stack()
	{
		_st = nullptr;
		_size = _capacity = 0;
	}

	// 带参的构造函数
	Stack(int n)
	{
		_st = (int*)malloc(sizeof(int) * n);
		if (nullptr == _st)
		{
			perror("malloc");
			return;
		}

		_size = 0;
		_capacity = n;
	}


	void Push(int x)
	{
		_st[_size++] = x;
	}

private:
	int* _st;
	int _size;
	int _capacity;
};

int main()
{
	// 无参
	//Stack st;

	// 带参
	Stack st(8);

	st.Push(1);
	st.Push(2);
	st.Push(3);

	return 0;
}
  • 自动调用无参的构造

image-20240218093153830

image-20240218093226125

image-20240218093251422

  • 自动调用带参的构造

image-20240218093728871

为什么定义变量时无参和带参的不一样

Stack st为什么不写成Stack st(),因为这与函数声明一样:无参返回值为Stack的函数

Stack st(8)的函数声明是Stack st(int)或者Stack st(int a)。不冲突

Date类

对构造函数的简单改写。

class Date
{
public:
	// 无参
	Date()
	{
		_year = 2000;
		_month = 1;
		_day = 1;
	}

	// 带参
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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


int main()
{
	Date d1;
	Date d2(2000, 1, 1);
	return 0;
}

image-20240218120100205

  • 我们可以将无参的构造和带参的构造合并成全缺省构造,满足两者功能
class Date
{
public:
	// 全缺省
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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


int main()
{
	Date d1;
	Date d2(2000, 1, 1);
	return 0;
}

image-20240218120617990

  • 无参和全缺省的构造函数能否同时存在?
Date()
{
    _year = 2000;
    _month = 1;
    _day = 1;
}
	
Date(int year = 2000, int month = 1, int day = 1)
{
    _year = year;
    _month = month;
    _day = day;
}

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

语法上没错,但是调用时不能同时存在, Date d时会报错,对函数重载的调用不明确。(存在歧义)

默认构造函数

  1. 默认构造函数:不用传参数的构造(无参、全缺省和编译器默认生成的)。
  2. 我们自己能实现带参和无参的构造函数,当我们不显示的实现构造函数时,编译器会自动帮我们生成一个==无参的默认构造函数==。但是如果我们实现了任意一个构造函数(即带参和无参的任何一个),编译器就不会自动生成了。

特性

  1. 对于内置类型成员不处理。
    • 内置类型:int,char, .... , 任何类型的指针
  2. 对于自定义类型的成员,会去调用它的默认构造。
    • 自定义类型:struct, class ,union
  3. C++11中对内置类型不初始化的缺陷打了补丁:内置类型成员变量在类中声明时可以给缺省值
  • 对内置类型
class A
{
public:
	/*A()
	{
		_a = _b = _c = 10;
	}*/

private:
	int _a;
	int _b;
	int _c;
};


class B
{
	// 默认生成构造函数,对自定义类型,会去调用它的默认构造函数
private:
	A _aa1;
};

int main()
{
	A a;
	B b;
	return 0;
}

image-20240218195803776

  • 对自定义类型:
class A
{
public:
	A()
	{
		_a = _b = _c = 10;
	}
private:
	int _a;
	int _b;
	int _c;
};

class B
{
	// 默认生成构造函数,对自定义类型,会去调用它的默认构造函数
private:
	A _aa1;
};

int main()
{
	B b;
	return 0;
}

image-20240218184959132

  • C++11打的补丁:

image-20240218201140109

析构函数

概念

与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

特性

  1. 析构函数名是在类名前面加上字符~
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。如果没有显示定义,系统会自动生成默认的析构函数。析构函数不能重载。
  4. 对象生命周期结束时,C++编译系统会自动调用析构函数。

默认析构函数

和默认构造函数的处理一样,对内置类型不处理,对自定义类型调用它的析构函数。

  • 如下就是编译器自动调用析构函数
class Stack
{
public:

	// 作用就是类似Init函数的功能
	// 无参的构造函数
	Stack()
	{
		_a = nullptr;
		_size = _capacity = 0;
	}

	// 带参的构造函数
	Stack(int n)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (nullptr == _a)
		{
			perror("malloc");
			return;
		}

		_size = 0;
		_capacity = n;
	}


	void Push(int x)
	{
		_a[_size++] = x;
	}


	~Stack()
	{
		free(_a);
		_a = nullptr;
		_capacity = _size = 0;
        cout << "~Stack()" << endl;
	}

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

int main()
{
	// 无参
	//Stack st;

	// 带参
	Stack st(8);

	st.Push(1);
	st.Push(2);
	st.Push(3);
	
	return 0;
}

image-20240218203921640

image-20240218110813995

拷贝构造

概念

只有单个形参,该形参是对本类 类型对象的引用,常用const修饰,在用已存在的类类型对象创建新对象时由编译器自动调用。

特性

  1. 拷贝构造函数是构造函数的一个重载形式。所以满足构造函数的特性。
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
    // d是d1的别名,d1可读可写,d变成只读的。权限的缩小
    // 权限不能放大,只能缩小。
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2020, 1, 1);

	// 想用d1初始化d2
	Date d2(d1);
    
    // 拷贝构造也可以这样写
    Date d2 = d1;
	return 0;
}

为什么传值传参会引发无穷递归?

image-20240218211650062

Date d2(d1):想用d1初始化d2

  • Func(int a):调用Func()函数首先要传参。

这一行代码首先会引发拷贝构造,但还没调到这个拷贝构造,就要先传值传参,而自定义类型传传值参会调用自己的拷贝构造,,而调用拷贝构造又要进行传值传参,,,反反复复,,引发无穷递归。

解决方式:传引用传参,不会引起拷贝。

上述代码的执行结果

image-20240218212721444

image-20240218212745450

如果我们不写拷贝构造会怎么样?

对于内置类型:

拷贝构造对于内置类型而言,不像构造函数那样不处理。因此,对于成员变量是内置类型的,我们可以不写拷贝构造函数。

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

	// 普通对象和const对象都可以传,权限可以缩小
	/*Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}*/

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

	~Date()
	{}
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2020, 1, 1);
	Date d2(d1);
	d1.Print();
	d2.Print();
	return 0;
}

image-20240219135800768

对于自定义类型:

比如栈结构中有指向栈的指针,进行值拷贝会指向同一块空间,会存在如下问题:

插入删除数据会互相影响。

析构两次,程序崩溃。

class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (int*)malloc(sizeof(int) * n);
		if (_a == nullptr)
		{
			perror("malloc");
			return;
		}

		_capacity = n;
		_size = 0;
	}

	void Push(int x)
	{
		_a[_size++] = x;
	}

	~Stack()
	{
        cout << "~Stack()" << endl;
		free(_a);
		_capacity = _size = 0;
	}

private:
	int* _a;
	size_t _capacity;
	size_t _size;
};

int main()
{
	Stack st1;
	st1.Push(1);

	Stack st2(st1);

	return 0;
}

image-20240219140921091

image-20240219141234679

为什么析构两次,程序崩溃了呢?

st2先出栈,调用完析构函数后,资源被释放,_a被置为空指针,但是st1里面的_a不为空指针,只是资源被释放,此时调用st1的析构,就是野指针问题了。

image-20240219143051620

一般当你自己实现了析构函数释放空间,就需要实现拷贝构造!!!

实现深拷贝

// Stack st2(st1);

Stack(const Stack& st)
{
    _a = (int*)malloc(sizeof(int) * st._capacity);
    if (nullptr == _a)
    {
        perror("malloc");
        return;
    }
    memcpy(_a, st._a, sizeof(int) * st._size);
    _size = st._size;
    _capacity = st._capacity;
}

image-20240219150717377

默认拷贝构造函数

对内置类型完成值拷贝(浅拷贝) -- 按字节拷贝。

对自定义类型成员,去调用这个成员的拷贝构造。

应用场景

显示的调用拷贝构造。

传值传参:对于自定义类型,传值传参会去调用拷贝构造。

传值返回:对于自定义类型,传值返回也会去临时拷贝一个对象。

对于后两种,调用拷贝构造都是会造成浪费资源。所以能用引用就用引用。

赋值重载

运算符重载

C++ 为了增强代码可读性引入了运算符重载(比如之前写的Date类的对象比较大小,而不用写很多函数来比较自定义类型对象的大小或者关系),运算符重载是具有特殊函数名的函数。同时也具有返回值类型,函数名字以及参数列表,返回值类型和参数列表与普通的函数类似。

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

函数原型:返回值类型 operator操作符(参数列表)

注意:

不能通过连接其他符号来创建新的操作符:如operator@。

重载操作符必须有一个类类型参数。(自定义类型)

用于内置类型的运算符,不能改变含义。

作为类成员函数重载时,其形参看起来比操作数少1,因为隐藏了this。

.* :: sizeof ?: . 这5个运算符不能重载。

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

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

bool operator==(const Date& d1, const Date& d2)
{

	// 因为Date的成员变量是私有的,解决方法要么将成员变量公开要么在类内写个获取成员变量的函数
    // 最好的方法就是放在类内
	//return d1._year == d2._year
	//	&& d1._month == d2._month
	//	&& d1._day == d2._day;
}


int main()
{
	Date d1(2020, 1, 1);
	Date d2(2020, 1, 19);

	// 这样不是很明显嘛,自己实现有意义的运算符,比如日期+日期就没意义
	d1 == d2; // 会被编译器转换成operator==(d1,d2);

    // 自定义类型的比较编译器识别不了。
	//d1 < d2;
	//d1 + d2;
	return 0;
}

赋值运算符重载

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
	

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

返回值是Date是为了满足连续赋值,像内置类型i=j=k这样,而传引用返回是赋值重载的对象存在。自己给自己赋值有拷贝过程,所以去掉自己给自己赋值这一行为。

默认生成的赋值重载

对内置类型完成值拷贝(浅拷贝) -- 按字节拷贝。

对自定义类型成员,去调用这个成员的赋值重载。