C++--类和对象(二)

类和对象的基础定义可参看:C++--类和对象(一)-CSDN博客

本篇讲述类和对象里相当重要的几个成员函数

目录

类的默认成员函数:

1.构造函数

2.析构函数

3.拷贝构造函数

(1)无限递归调用拷贝构造

(2)浅拷贝和深拷贝的区别

4.赋值运算符重载

(1)运算符重载(operator)

(2)赋值运算符重载(operator=)


类的默认成员函数:

1.默认的成员函数是指在类中没有主动编写的,编译器会自动生成的函数

2.编译器默认生成的成员函数不显式实现

一个类中在我们不写的情况下会默认生成6个成员函数,这里只讲前四个,取地址重载函数除非有特殊情况一般不会主动写,毕竟C++有引用这个好东西

1.构造函数

构造函数它的名字叫做构造,但它并不负责对于对象的空间开辟,对象在实例化的时候空间就开好了,而它的作用是对象实例化时初始化对象(局部对象在栈帧创建时,空间就开好了)。构造函数的本质是要替代stack(栈)中写的初始化Init函数的功能,且构造函数会自动调用就很完美的替代了它,初始化的方式有函数体初始化和列表初始化,这里先讲函数体初始化方便后续的理解

class stack
{
public:
    //stack(int n)    //需要传参的构造函数
    //stack()         //无参构造函数

    stack(int n=4)    //全缺省构造函数
    {
        arr=(int*)malloc(sizeof(int)*n);    
        top=0;
        capacity=n;
    }
private:
    int* arr;
    size_t top;
    size_t capacity;
};

构造函数的特点:

1.函数名和类名一致

2.它没有返回值(不要写void,C++规定这样的不用想太多)

3.在对象实例化的时候会自动调用构造函数

4.构造函数时可以重载的

5.在类中没有显式定义构造函数,那么C++编译器会自己生成一个无参的默认构造函数,一旦显式定义构造函数那么编译器就不会生成

6.无参构造函数,全缺省构造函数,及编译器默认生成的构造函数,都叫作默认构造函数。他们都有一个共同点那就是都可以无传参调用,虽然无参构造和全缺省构造可以构成函数重载,但是调用时会出现歧义的情况(你都显式定义了,编译器默认生成的就肯定不可能存在了),所以这三个函数只能存在其中一个。

7.默认生成的构造函数,它对内置类型成员变量的初始化没有要求,也就是说它是不确定的,有可能他会给你初始化成0,也有可能不作为(随机值)。而对于自定义类型成员变量,要求调用这个成员自己的默认构造(如果没有,那么就报错)

2.析构函数

析构函数对应的是stack(栈)中的Destroy功能。它不是对对象本身的销毁,对象的销毁会在生命周期结束时销毁栈帧所以不用管,而它主要是对对象中资源的清理和释放工作,它也会在函数结束时自动调用

class stack
{
public:
    stack(int n=4)    //全缺省构造函数
    {
        arr=(int*)malloc(sizeof(int)*n);    
        top=0;
        capacity=n;
    }
    ~stack()    //析构函数
    {
        free(arr);   
        capacity=top=0;
    }
private:
    int* arr;
    size_t top;
    size_t capacity;
};

析构函数的特点:

1.名字是在类名的前面加上 ~

2.无参数,无返回(和构造一样可以不写void

3.一个类只能有一个析构,不显式实现的话编译器自己会生成默认的析构函数

4.只有在对象生命周期结束时,会自动调用析构函数

5.默认生成的析构对内置类型成员不做处理,但自定义类型自动会调用他自己的析构

6.我们显式实现析构函数时自定义类型在析构函数中,会自动调用他自己的析构,也就是不管啥情况都会自动调用自己的析构函数

7.类中如果没有申请资源的情况下或者默认生成的析构函数足以解决问题,析构函数可以不写;但是有申请资源就一定要写析构函数,否则会出现内存泄漏,像stack(栈)就需要显式实现析构函数

8.一个局部域内多个对象,后定义的先析构

小知识:可以通过有没有存在析构函数显式实例化来判断一个类有没有存在申请资源,有基本上就是有存在申请资源的情况,没有就是没有申请资源

3.拷贝构造函数

如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,那么此构造函数也叫作拷贝构造,他是一个很特殊的构造函数

拷贝构造的特点:

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

2.拷贝构造函数的参数只有一个且必须是类的引用,使用传值方式会导致无限循环导致编译器报错,因为在逻辑上会引发无限递归调用

3.C++规定了自定义类型对象如果进行拷贝行为必须调用拷贝构造函数,所以这里自定义类型传参或者传值返回都会调用拷贝构造

4.没有显式实现拷贝构造,编译器会自动生成一个默认的拷贝构造,自动生成的拷贝构造对内置类型成员变量会进行浅拷贝(一个字节一个字节的拷贝),自定义类型则是调他自己的拷贝构造

5.函数传值返回会产生一个临时对象调用拷贝构造;传值引用返回,那么返回的就是这个对象的别名,但是函数局部域的对象出了域就会销毁,所以使用引用返回应该保证对象在出了局部域还存在,否则会出现空引用的情况(空引用类似野指针都是指向一块未知的空间),应尽力避免此情况

(1)无限递归调用拷贝构造

这里是个人理解来讲述为什么传值会导致无限递归调用拷贝构造:

也就是:传值->是拷贝行为->触发拷贝构造->拷贝构造需要传值->传值->是拷贝行为->...

这就在语法逻辑上出现了无限递归调用拷贝造,而使用引用就直接使用那个对象自然就不会触发无限拷贝构造

注意:记得给引用加上const避免修改被拷贝对象(能修改在逻辑上那就是倒反天罡了)

(2)浅拷贝和深拷贝的区别

上面就讲过浅拷贝是一个字节一个字节进行拷贝,这种就非常适合没有申请资源的类,而有申请资源的话就不好使了,下面为有申请空间的对象使用浅拷贝的错误演示:

这里是使用默认的拷贝构造(浅拷贝),可以看到在对b进行拷贝构造时a的栈里是1,2,3所以b的内容应该是1,2,3,但是我在拷贝构造后对a栈里的值3进行了修改,这时错误就体现出来了,对a进行出栈是4,2,1没问题,但b不应该啊,b不应该是3,2,1为什么也变成了4,2,1呢?

通常申请空间都会有一个指针指向那块空间,浅拷贝是一个一个字节进行拷贝,把指针指向的地址拷给新对象的指针会怎么样就不用多说了吧

这就会导致两个对象里的指针会同时指向一块空间

这里就引入了第二个问题:

两个对象的指针都指向一块空间,当生命周期结束时调用析构函数时会析构两次会发生什么呢?这就和对一个已经释放空间后的指针再访问,就会导致程序崩溃

解决问题的方法就是使用深拷贝

深拷贝就是开辟新空间,将被拷贝的对象空间里的资源拷过来,这时再指向新空间就可以了

	stack(const stack& st)        //深拷贝
	{
		int* arr = new int[st._top];    //这里就当作malloc一样的效果就好了
		memcpy(arr, st._acc, sizeof(int) * st._top);    //拷贝内容

		_acc = arr;       //指向开辟出来的空间
		_top = st._top;
		_capacity = st._capacity;
	}
    int main()
    {
        stack a;
        stack b(a);//这是拷贝构造
        stack b=a; //这也是拷贝构造 可按自己习惯使用    
        return 0;
    }

4.赋值运算符重载

(1)运算符重载(operator)

C++语言允许我们在类中可以重新定义运算符的定义,可以随自己的想法重新写一个+或者+=,使用运算符重载要求类类型或者枚举类型的参数

1.C++中类类型对象在使用运算符的时候,必须转换成调用对应的运算符重载,若没有对应的运算符重载那么就会报错

2.运算符重载函数是一个有独特名字的函数,它的名字由operator关键字和想重新定义的运算符构成,它和其他函数一样具有返回值和参数列表及函数体

//作为类成员函数声明
返回类型 operator+=(const int& x);//重载一个+=

3.重载运算符的参数个数与该运算符的作用对象一样多,一元运算符有一个参数,二元就有两个,多个参数的运算符传参,如二元运算符左侧运算对象对应第一个参数,右侧对应第二个

4.如果一个重载运算符函数是成员函数,第一个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数比运算对象少一个

5.运算符重载后优先级,优先级和结合性与内置类型的运算符一致

6.不能使用语法中没有的符号重新定义新的操作符

7.作用域操作符(::)   sizeof   ?:  (.)   .*  注意这个五个操作符不能重载  

8.重载操作符至少有一个类类型对象不能通过运算符重载改变内置类型对象的含义

9.重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,不好区分。 C++规定,后置++重载时,增加一个int形参,跟前置++构成函数重载,方便区分

class add
{
public:
	add(int x = 0)        //构造函数
	{
		_x = x;
	}
	add& operator+=(int y)
	{
		_x += y;
		return *this;
	}
	add& operator++()//前置++
	{
		*this += 1;
		return *this;
	}
	add operator++(int)//后置++
	{
		add a = *this;
		*this += 1;
		return a;
	}
	void print()
	{
		cout << _x<<endl;
	}
private:
	int _x;
};
int main()
{
	add a(1);
	add b(a);

	(a++).print();
	a.print();
	cout << endl;

	(++b).print();
	return 0;
}

        

10.重载<<和>>的时候需要注意,他们作为成员函数时第一个参数位置会默认被*this占走,导致出现<<cout,不符合使用的习惯和操作性,所以应该重载为全局函数,这样就可以将ostream(cout)/istream(cin)放到第一个形参的位置上了,而第二个位置为类类型对象

注意:这里的重载无法访问对象中私有权限的成员,如果需要访问私有成员需要将函数设为那个类的友元函数(friend),友元函数是什么会在下篇讲到

//流输出
ostream& operator<<(ostream& out, const date& da);
//流输入
istream& operator>>(istream& in, date& da);

(2)赋值运算符重载(operator=)

赋值运算符重载是一个默认成员函数,用于两个已经实例化对象之间的拷贝赋值,这里千万不要和拷贝构造弄混,拷贝构造是使用一个实例化对象去创建初始化一个对象,注意两者的区分

赋值运算符需要注意的点:

1.赋值运算符重载规定上必须重载成成员函数,且参数最好使用const修饰的引用,因为成员函数属于类,而传值传参需要拷贝数据必然会触发拷贝构造导致降低效率

2.它具有返回值,也建议使用引用返回,可以提升效率,有返回值的目的是为了支持连续赋值

3.不显式实现时编译器也会默认生成一个赋值运算符重载函数,它的行为和默认构造函数在一定程度上类似,且对内置成员会进行浅拷贝,对自定义类型则会调用它自己的拷贝构造

4.如果类成员变量都是内置类型的话,使用编译器自己默认生成的赋值预算符重载就足以满足需求,但是有申请资源的话就一定要自己手写了,默认生成的重载默认是浅拷贝,道理和拷贝构造函数同理

stack& operator=(stack& st)
{
	int* newstack = new int[st._top];
	memcpy(newstack, st._acc, sizeof(int)*st.size());
	delete[]_acc;    //删除旧空间

	_acc = newstack;
	_top = st._top;
	_capacity = st._capacity;
	return *this;
}
//拷贝构造
stack(const stack& st)
{
	int* arr = new int[st._top];
	memcpy(arr, st._acc, sizeof(int) * st._top);

	_acc = arr;
	_top = st._top;
	_capacity = st._capacity;
}

下面是简单实现一个栈 :

class stack
{
public:
	stack(int n = 4)
	{
		_acc = new int[n];
		_top = 0;
		_capacity = n;
	}
	void reverse(size_t n)
	{
		if (n > _capacity)
		{
			int* newstack = new int[n];
			memcpy(newstack, _acc, sizeof(int) * _top);

			delete[]_acc;
			_acc = newstack;
			_capacity = n;
		}
	}
	void push_back(const int& x)
	{
		if (_top == _capacity)
		{
			reverse(_capacity * 2);
		}
		_acc[_top++] = x;
	}
	void pop_back()
	{
		assert(!empty());
		_top--;
	}
	int& stacktop()
	{
		assert(!empty());
		return _acc[_top-1];
	}
	bool empty()
	{
		return _top == 0;
	}
	size_t size()
	{
		return _top;
	}
	int& operator[](size_t pos)
	{
		assert(!empty());
		return _acc[pos];
	}
	//赋值运算符重载
	stack& operator=(stack& st)
	{
		int* newstack = new int[st._top];
		memcpy(newstack, st._acc, sizeof(int)*st.size());
		delete[]_acc;
		_acc = newstack;
		_top = st._top;
		_capacity = st._capacity;
		return *this;
	}
	//拷贝构造
	stack(const stack& st)
	{
		int* arr = new int[st._top];
		memcpy(arr, st._acc, sizeof(int) * st._top);

		_acc = arr;
		_top = st._top;
		_capacity = st._capacity;
	}
private:
	int* _acc=nullptr;
	size_t _top=0;
	size_t _capacity=0;
};

 填补一些小知识点:

类比较重要的几个点都已经讲完了,一些没讲到的走这边:C++--类和对象(三)-CSDN博客


本篇文章就到这里了,如果能对你产生帮助就行,感谢你的阅读

  • 17
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值