admin管理员组

文章数量:1487745

初识C++ · 类和对象(中)(1)

1 类的6个默认成员函数

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:

private:

};

这是一个空类,试问里面有什么? 可能你会觉得奇怪,明明是一个空类,却问里面有什么。其实一点也不奇怪,这就像文件操作章节,系统默认有三个流一样,标准输出流(stdout),标准输入流(stdin),标准错误流(stderr),类里面系统是有默认的函数的,一共有6个默认函数。

默认函数是指用户没有显式实现,系统会自己生成的函数,下面依次介绍。


2 构造函数

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	void Init(int year = 2020,int month = 1,int day = 17)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

当我们写了一个日期类之后,我们想要对它进行初始化,我们通常都会写一个函数叫做Init()函数,用来初始化里面的成员变量,这是一般写法。

那么有疑问了,我们介绍的不是构造函数吗,为什么会涉及到构造函数? 这是因为构造函数就是专门用来作为初始化函数的,至于为什么取名为构造函数呢?咱也不知道,咱也不敢问。

构造函数应遵行一下几个点:

1 函数名和类名应相同,并且没有返回值

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	Date()
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Print();
	return 0;
}

里面的Date()函数就是构造函数,因为没有返回值,所以不用加void,只有默认成员函数如果没有返回值就可以不用加上void,其他函数就不可以,可以用print函数试验一下。

2 类实例化的时候编译器自动调用构造函数

这里就这里结合调试:

是会自动跳到构造函数的,留个疑问,如果我们没有显式写默认构造函数会怎么样呢?

3 构造函数支持函数重载

这里就复习一下函数重载的概念,函数名相同,函数的参数不同,包括类型不同,个数不同,顺序不同,就构成函数重载:

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	Date()
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	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;
	Date d2(2024,4,10);
	d1.Print();
	d2.Print();

	return 0;
}

构造函数可以有多个,只要支持函数重载就行,并且不存在调用歧义

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	Date()
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

这种代码就会存在调用歧义,两个函数都构成构造函数的函数重载,但是调用的时候会出现问题,传参的时候如果是无参,则两个函数都行,就会存在调用歧义,所以编译器就会报错。

使用构造函数的时候一般有无参调用和带参调用:

代码语言:javascript代码运行次数:0运行复制
Date d1;
Date d2(2024,4,10);

两种调用方式都可以,取决于带不带参数,都是没有问题的。

4 如果用户没有显示调用构造函数,编译器就会调用默认的构造函数,一旦用户显示定义构造函数,系统就不会生成默认构造函数。

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	Date(int x)
	{
		_year = 2020;
		_month = 1;
		_day = 17;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	d1.Print();
	return 0;
}

这里定义了一个默认构造函数,系统就不会默认生成构造函数,所以这里编译器会报错,说没有合适的默认构造函数,主要就是因为我们已经显式定义了默认构造函数。

5 构造函数只会对自定义类型进行初始化,C++标准没有规定对内置类型要有所处理,初始化自定义类型的时候会调用该自定义类型自己的构造函数

这个点可能有点绕,我们分开来看,一是没有规定对内置类型有所处理, 如下:

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:

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

int main()
{
	Date d1;
	d1.Print();
	return 0;
}

如上,打印出来都是些随机值,说明编译器对这三个内置类型没有进行处理,但是不乏有些编译器会将它们初始化为0,这也不用惊讶,因为对内置类型没有规定要处理,所以可处理可不处理,取决于编译器心情咯。

那么什么是调用自定义类型的构造函数呢?

代码语言:javascript代码运行次数:0运行复制
class Time
{
public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};

当我们进行调试的时候,我们会发现编译器会自动进入到Time类的构造函数,随即初始化Time类的三个内置类型为0,但是如果Time类中我们没有显式定义构造函数呢?

那么就会:

那么Time类的内置类型的成员都会是随机值,有点类似无限套娃,只要我们没有显式定义构造函数,就会被定义为随机值,是不是看起来很鸡肋?

先不着急,C++11的标准中为了给内置成员初始化,添加了一个补丁,即可以在声明的时候给上缺省值:

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
	Time _t;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

打印出来的时候即都是1,这就补上了不给内置成员初始化的缺陷。

那么构造函数是不是很鸡肋没有用处呢?

实际上并不是,如下:

代码语言:javascript代码运行次数:0运行复制
class Stack
{
public:

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

class MyQueue
{
public:


private:
	Stack _st1;
	Stack _st2;
};

在两个栈实现队列的时候,当我们调用MyQueue的时候,调用到MyQueue的构造函数的时候,我们不需要对队列进行初始化,因为使用的是栈,所以在栈里面初始化,队列类里面就不需要了,这个时候就不需要在Queue里面显式构造函数了。

默认构造函数有三种,无参构造函数,全缺省构造函数,系统自动生成的默认构造函数

总结来说就是不需要传参的构造函数就是默认构造函数,而且默认构造函数只能有一个,不然存在调用歧义的问题。


3 析构函数

构造函数是用来初始化的,那么析构函数就是用来做类似销毁的工作的,但是不是对对象本身进行销毁,对象本身是局部变量,局部变量进行销毁是编译器完成的,析构函数是用来进行对象中的资源清理的。

析构函数应遵循如下特点: 函数名是类型前面加个~,没有返回值没有参数

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	~Date()
	{
		_year = 0;
		_month = 0;
		_day = 0;
	}
private:
	int _year;
	int _month;
	int _day;
};

析构函数不能函数重载,如果用户显式定义了析构函数,系统就不会默认生成析构函数

当代码执行到这一步的时候,系统就会开始执行析构函数的代码,下一步语句就会跳转到~Date函数执行代码清理工作,因为析构函数没有参数,所以不支持函数重载,即只能有一个析构函数。

对象的声明周期结束的时候编译器会自己调用析构函数

也就是上图了,因为声明周期一结束,就会自己调用析构函数,如果没有显式定义析构函数的话,就会调用系统自己生成的析构函数。

当我们调用系统给的析构函数的时候就会发现:

内置类型并没有进行处理,这就是析构函数和构造函数相同的点: 对于内置类型没有要求要进行处理,处理自定义类型的时候会调用自定义类型自己的析构函数

代码语言:javascript代码运行次数:0运行复制
class Time
{
public:
	~Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}
private:
	int _hour;
	int _minute;
	int _second;

};
class Date
{
public:

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

同构造函数一样。

那么总结起来也是,比如碰到两个栈实现一个队列的时候,就可以不用写析构函数,其他情况用户都是要显式定义析构函数的。

在类中,如果没有资源申请,那么就可以不用写析构函数,如果有资源申请,那么一定要写析构函数,不然就会导致内存泄露.

内存泄露是一件很恐怖的事,因为它不会报错,内存一点点的泄露,最后程序崩溃了,然后重启一下程序发现又好了,如此往复,就会导致用户的体验很不好

代码语言:javascript代码运行次数:0运行复制
class Stack
{
public:
	Stack(int capacity = 4)
	{
		int* tem = (int*)malloc(sizeof(int) * capacity);
		if (tem == nullptr)
		{
			perror("malloc fail!");
			exit(1);
		}
		arr = tem;
		_capacity = capacity;
		_size = 0;
	}
	~Stack()
	{
		free(arr);
		arr = nullptr;
		_capacity = _size = 0;
	}
private:
	int* arr;
	int _size;
	int _capacity;
};

像这种在堆上申请了空间的,就一定要写析构函数,不然就会导致内存泄露。


3 拷贝构造函数

拷贝构造函数,拷贝就是复制,像双胞胎一样,复制了许多特征,拷贝构造函数就是用来复制对象的,应遵行如下特点: 拷贝构造函数是构造函数的一个重载形式: 既然是构造函数的重载形式,那么拷贝构造函数的函数名也应该是类名,当然,也是没有返回值的。

拷贝构造函数的参数只有一个,是类类型的引用,如果采用传值调用就会触发无限递归,程序就会崩溃: 这个点的信息量有点大,我们一个一个解释

第一个,函数参数只有一个引用类型的参数,使用的时候如下:

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	Date(Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

其中参数是Date& dd的就是拷贝构造函数,拷贝构造函数一共有两种拷贝方法: 一是Date d2 = d1,二是Date d3(d1),两种方式都可以的,最后打印出来的结果都是2020-1-17。

那么,为什么使用传值调用就会触发无限递归呢? 这是因为在传值调用的时候,形参也是一个对象,对象之间的赋值都会涉及到拷贝构造函数的调用,我们结合以下代码:

代码语言:javascript代码运行次数:0运行复制
class Date
{
public:
	Date(int year,int month,int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& dd)
	{
		_year = dd._year;
		_month = dd._month;
		_day = dd._day;
	}
	void Print()
	{
		cout << _year << '-' << _month << '-' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
void Func(Date pd)
{
	cout << "Date pd" << endl;
}
int main()
{
	Date d1(2020, 1, 17);
	Func(d1);
	return 0;
}

当代码段执行到Func的时候,语句就会先跳到拷贝构造函数,赋值完了才会进入到函数Func里面,这时候我们监视形参pd,就会发现pd已经赋值了d1的数值。

也就是说,传值调用的时候,就会自动跳到拷贝函数,那么如果拷贝构造函数也是传值调用的话呢?就会造成拷贝构造函数的形参调用拷贝构造函数的形参,一直循环往复,从而导致了无限递归。

这就是为什么拷贝构造函数的参数必须是引用类型了,但是我们拷贝构造的时候,因为是引用类型,我们不希望引用类型被修改,所以常加一个const进行修饰。

如果用户没有显式定义拷贝构造函数,系统会默认生成拷贝构造函数,拷贝构造函数按字节序进行拷贝,这种拷贝被叫做浅拷贝,与之对应的是深拷贝

默认成员函数都有个特点,如果用户没有显式定义函数,系统都会默认生成该函数。

那么,什么是浅拷贝呢?对于日期类,无非就是赋值,我们不必太过在乎,但是对于Stack这种,我们就需要注意一下了,先看代码:

代码语言:javascript代码运行次数:0运行复制
class Stack
{
public:
	Stack(int capacity = 4)
	{
		int* tem = (int*)malloc(sizeof(int) * capacity);
		if (tem == nullptr)
		{
			perror("malloc fail!");
			exit(1);
		}
		arr = tem;
		_capacity = capacity;
		_size = 0;
	}
	~Stack()
	{
		free(arr);
		arr = nullptr;
		_capacity = _size = 0;
	}
private:
	int* arr;
	int _size;
	int _capacity;
};
int main()
{
	Stack s1;
	Stack s2(s1);
	return 0;
}

对于Stack这种有资源申请的类,我们拷贝构造之后,生成解决方案的时候是成功的,但是当我们

运行程序的时候就会报错:

报错位置是在空指针那里,那么我们可以把重心放在空指针这里,既然是空指针报错,是我们越界访问了吗?还是说我们free了两次空指针?

看这个:

在拷贝构造完成之后,发现s1 和 s1的arr指向的空间居然是一样的:

因为拷贝构造函数内置类型是按照字节序拷贝的,所以拷贝的时候就会出现两个指针指向空间是同一个的情况,那么在析构函数,释放空间的时候,就会free掉空间两次,所以会报错。

浅拷贝对应的就是深拷贝,所以解决方法就是深拷贝,对于这种有空间申请的类,我们进行拷贝构造的时候都要深拷贝,不然析构的时候就会出现问题:

代码语言:javascript代码运行次数:0运行复制
	Stack(const Stack& ss)
	{
		arr = (int*)malloc(sizeof(int) * ss._capacity);
		if (arr == nullptr)
		{
			perror("malloc fail!");
			return;
		}
		memcpy(arr, ss.arr, sizeof(int) * ss._size);
		_size = ss._size;
		_capacity = ss._capacity;
	}

深度拷贝构造无非就是两个指针指向不同的空间,但是里面的数据是一样的,那么拷贝数据我们就可以用到memcpy,然后自己开辟一块空间给s2,最后赋值相关的数据就可以了,这样就不会报错了。

总结:

如果是日期类的拷贝构造,是没有必要进行深拷贝的,用系统默认生成的拷贝构造函数就行

拷贝构造函数报错常常因为析构函数,所以一般情况下拷贝构造函数不用写的话,析构函数也不用写

如果内置成员都是自定义类型,如MyQueue,没有指向资源,默认的拷贝构造函数就可以。

如果内部资源有申请的话,如Stack类,就需要用户自己显式定义拷贝构造函数,防止空间多次释放

感谢阅读!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2024-08-13,如有侵权请联系 cloudcommunity@tencent 删除系统c++int对象函数

本文标签: 初识C类和对象(中)(1)