【C++】类和对象(上)

1. 类的定义

class className
{
       // 类体:由成员函数和成员变量组成
};    // 一定要注意后面的分号

c++中struct 可以做结构体,也可以做类。
struct定义的类默认访问权限是public,class定义的类默认访问权限是private。

类的两种定义方式:

  1. 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
  2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名 ::

2. 类的实例化

用类类型创建对象的过程,称为类的实例化。
对象未创建时,类不占空间。创建后,类占空间,类对象也占空间。类对象相当于房子,类相当于图纸。

3. 类对象的存储方式

在这里插入图片描述

  1. 类中仅有成员函数,或为空类:类大小为1字节,分配1byte,不存储数据,只是占位,表示对象存在过
  2. 有成员变量的类的大小计算方法和结构体大小计算方法一样

结构体内存对齐规则

  1. 第一个成员在与结构体偏移量为0的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
    VS中默认的对齐数为8
  3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
    体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

为什么要内存对齐:提高编译器数据读取的效率,因为编译器是4个字节,4个字节地读取数据的

4. this指针

在这里插入图片描述
其实是Data* const this(this指针不可更改,指向内容可更改)

  1. this在实参和形参位置不能显示写,但是在类里面可以显示的用,例如:可以在类函数内使用和打印 this。
  2. this指针是一个形参,一般存在栈帧里,VS一般会用ecx寄存器直接传递
  3. 类函数的调用:
dl.Init(1,1,1);   //==  Init(&dl,1,1,1);
class A
{
public:
 void Print()
 {
 cout << "Print()" << endl;
 }
private:
 int _a;
};
int main()
{
 A* p = nullptr;
 p->Print();
(*p).Print();  //都运行成功,因为编译器非常聪明,看到Print()函数不存在类对象中,就不会进行解引用,当作传参。
//其实相当于是Init(p,1,1,1);该函数只是打印函数,不含_a,也不是解引用
 return 0;
}

5. 默认成员函数

我们不写,编译器自动生成。
在这里插入图片描述

6. 构造函数

是构造函数的主要任务并不是开空间创建对象,而是初始化对象

6.1 特征

其特征如下:

  1. 函数名与类名相同。
  2. 无返回值,且不需要写void
  3. 构造函数可以重载(本质是可以写多个构造函数,提供多种初始化方式)
  4. 对象实例化时编译器自动调用对应的构造函数。
    在这里插入图片描述

6.2 默认构造函数

默认构造函数:不用传参就可以调用的构造函数
三种类型:

  1. 无参的构造函数
  2. 全缺省的构造函数
  3. 编译器默认生成的构造函数

但是默认构造函数只能存有一个,多个并存会导致调用二异性。

6.3 全缺省的构造函数的使用

全缺省的构造函数的使用:不能加(),防止对象的定义和函数的声明混淆。(对象定义和初始化是同一个语句)

class Date
{
public:
	Date(int year=1,int month=1,int day=1 )
	{
		_year = year;
	    _month=month;
		_day= day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;  //d1的_year=1,_month=1, _day=1;
	return 0;
}

6.4 编译器生成的默认构造函数

构造函数,也是默认成员函数,我们不写,编译器会自动生成
编译器生成的默认构造函数的特点:

  1. 我们不写才会生成,我们写了任意一个构造函数就不会生成了
  2. 内置类型的成员会给个随机值(C++11,声明支持给缺省值)
  3. 自定义类型的成员回去调用这个成员的默认构造函数

总结:一般情况都需要我们自己写构造函数,决定初始化方式;
    成员变量全是自定义类型,可以考虑不写构造函数

7. 析构函数

作用:不是销毁对象,当栈帧结束,对象会自动销毁,而是对象在销毁时自动调用析构函数,完成对象中资源的清理工作(相当于Destroy函数)。

  1. 先定义的对象,后销毁,后调用析构函数。(栈:先进后出)
  2. 析构函数名是在类名前加上字符 ~。
  3. 无参数无返回值类型。
  4. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数
    • 内置类型的成员不会处理,自定义类型的成员也会处理回去-----调用自定义类型成员的默认构造函数
  5. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

什么时候析构函数需要自己写呢?
当在堆上开了空间,就需要自己写析构函数若没在堆开空间,则无需写析构函数。因为堆上的空间没法随栈的销毁而释放。

8.拷贝构造函数

8.1 拷贝

拷贝不等于赋值:

  1. 拷贝构:一个已经存在的对象去初始化另一个要创建对象
  2. 赋值:两个已经存在对象进行拷贝

c++规定:内置类型的拷贝都是浅拷贝,自定义类型的拷贝都是深拷贝,深拷贝用的是拷贝构造函数。

8.2 浅拷贝

又叫值拷贝。如下,就是将a拷贝给了x。是值的拷贝。

int Add(int x)
{
	return x;
}
int main()
{
	int a = 1;
	Add(a);
	return 0;
}

类的浅拷贝也是一样的。如下:

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;
	}
	
private:
	// 内置类型
	int _year;
	int _month;
	int _day;
};

void func1(Date d)  //d1拷贝给了d。
{
	d.Print();
}

int main()
{
	Date d1(2023, 7, 21);
	func1(d1);   
	return 0;
}

上面两例都不会出错,但是下面栈的例子会报错。

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)  //栈的增加数据的函数
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

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

		free(_array);     //将_array指向的堆的空间释放
		_array = nullptr;
		_size = _capacity = 0;
	}
private:
	// 内置类型
	DataType* _array;
	int _capacity;
	int _size;
};

void func2(Stack s)  //值拷贝,所以s,s1的_capacity,_size和_array都是一样的
{                    //类对象s在这个函数栈结束时,也会销毁,就会自动调用s的析构函数
	s.Push(1);       //所以_array指向的空间会被销毁
	s.Push(2);
}

int main()
{
	Stack s1;
	func1(s1);   
	return 0;   //此时,s1销毁,调用s1的析构函数,_array指向的空间被释放第二次,出现错误
}

解决措施一:改为引用,直接使用本尊。

//只有改变一点点
void func2(Stack& s)
{
	s.Push(1);       
	s.Push(2);
}

坏处:本尊会被改变。
解决措施二:c++的拷贝构造函数

8.3 拷贝构造函数的特点(深拷贝)

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

  1. 拷贝构造函数是构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

8.4 拷贝构造函数 无引用–>无穷递归

若拷贝构造函数无引用,如下面的情况:

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}

		_capacity = capacity;
		_size = 0;
	}

	void Push(DataType data)  //栈的增加数据的函数
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

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

		free(_array);     //将_array指向的堆的空间释放
		_array = nullptr;
		_size = _capacity = 0;
	}

   	Stack(Stack s)
	{
		cout << "Stack(Stack& s)" << endl;
		// 深拷贝
		_array = (DataType*)malloc(sizeof(DataType) * s._capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
	    }

		memcpy(_array, s._array, sizeof(DataType) * s._size);
		_size = s._size;
		_capacity = s._capacity;
	}
private:
	// 内置类型
	DataType* _array;
	int _capacity;
	int _size;
};

int main()
{
	Stack s1;
	Stack s2(s1);   //将s1拷贝给了s2,拷贝构造函数是构造函数的一种重载形式
	return 0; 
}

上面没有用到引用的代码会有无穷递归问题。
因为 Stack(Stack s) 中的s其实编译器自动进行的一次拷贝得来的。(拷贝方法就是上面代码的方法,就是说又会需要一个s,又要拷贝)
在这里插入图片描述

8.5 拷贝构造函数的形式

//相对于上面代码,加个引用就好了
Stack(const Stack& s)  //加个const防止s被改
{
   ...
}
...
Stack s1;
Stack s2(s1);   //将s1拷贝给了s2,拷贝构造函数是构造函数的一种重载形式
//Stack s2=s1;  这样也是一样的

8.6 编译器生成的默认拷贝构造函数

我们不写,编译默认生成的拷贝构造,跟之前的构造函数特性不一样

  1. 内置类型, 值拷贝
  2. 自定义的类型,调用他的拷贝
  3. 总结:Date不需要我们实现拷贝构造,默认生成就可以用
    Stack需要我们自己实现深拷贝的拷贝构造,默认生成会出问题
class MyQueue
{
private:
	Stack _pushst;
	Stack _popst;
};

int main()
{
	Date d1(2023, 7, 21);
	Date d2 = d1;
    
	/*
	Stack st1;
	Stack st2(st1);
	*/

	// MyQueue对于默认生成的几个函数非常受用
	MyQueue mq1;
	MyQueue mq2 = mq1;

	return 0;
}

9. 赋值重载函数

9.1 运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
函数名字为:operator后面接需要重载的运算符符号
函数原型:返回值类型  operator操作符(参数列表)
注意:

  1. 不能创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  5.   .*   ::   sizeof  ?:  .  注意以上5个运算符不能重载。
  6. 不可以改变操作符的操作数个数,一个操作符有几个操作数,那么重载时就有几个参数,且操作数的顺序与参数顺序一致

牛客题目:日期累加

9.2 赋值重载函数

赋值重载函数也是默认成员函数。
不写,默认生成赋值重载函数:

  1. 内置类型值拷贝
  2. 自定义类型调用赋值重载函数
#include<iostream>
using namespace std;

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

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

		return *this;
	}*/                   //可以省略,因为编译器会自动生成,且生成的函数符合条件

	void Print()
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}

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

int main()
{
	Date d1(1,1,1);
	Date d2(2, 2, 2);
	d2 = d1;
	d2.Print();

	return 0;
}

对于Date类型,赋值重载函数可以不写,编译器生成的赋值重载函数,符合条件。
对于Stack类型,赋值重载函数要自己写,因为编译器生成的会将 指针a 也赋值,使两个栈指向同一个空间。

9.3 前置++和后置++重载

#include<iostream>
using namespace std;

class Date
{
public:
     ...
	// ++d1 -> d1.operator++()
	Date& operator++();

	// d1++ -> d1.operator++(0)
	Date operator++(int);     // 加一个int参数,进行占位,跟前置++构成函数重载进行区分
	                          // 本质后置++调用,编译器进行特殊处理         
	Date& operator--();
	Date operator--(int);

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

//前置++:返回值为原值,*this++;
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

//前置++:返回值为++后的值,*this++;
Date Date::operator++(int)
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

int main()
{
     Date d1(1,1,3);
     d1--;   
     --d1;
     return 0;
}

9.4 const 成员函数

关于const,总而言之,就是

  1. Date前加了const,函数定义声明后一定都要加const。(权限的平移)
  2. Date没加const,函数也可加const,表示*this的成员不能改。(权限的缩小)
class Date
{
  public:
      ...
      void Print() const;    
  private:
	  int _year;
	  int _month;
   	  int _day;
}
                                       //void Date::Print(const Date* this)  
void Date::Print() const               //这个const保护this指向的内容,*this在函数中不能变
{
	cout << _year << "年" << _month << "月" << _day << "日" << endl;
}


int main()
{
     Date d1(1,1,3);
     d1--;   
     --d1;
     
     Date d2(1,1,3);
     d2.Print();     //权限的缩小
     
     const Date d3(1,1,3);
     d3.Print();     //权限的平移      调用时不加const
     
     return 0;
}
  1. void Print() const; 和 void Print(); 可同时存在; 因为两函数为重载函数,编译器会按d的类型来判断调用哪一个。
 void Print() const;       //1号
 void Print();             //2号   
 ...
 Date d;           d.Print();      //1,2号都可以,若两个都存在,则编译器选择2号,因为2号更匹配
 const Date d;     d.Print();      //1号
  1. 只读函数,加const,防止被改

10. 取地址操作符重载

取地址操作符重载是 默认成员函数 。

class Date
{
  public:
   ...
   const Date* Date::operator&() const;
   Date* Date::operator&();
  private:   
	  int _year;
	  int _month;
   	  int _day;
}  

const Date* Date::operator&() const   //适用于读
{
      return this;
} 

Date* Date::operator&()             //适用于读/写
{
      return this;
}                                  //两函数可不写,因为这是默认成员函数。

int main()
{
     Date d;
     cout<<&d<<endl;
     return 0;
}

取地址操作符重载函数在99%情况下可以不写,但有一种情况要写:当你不想被取到有效地址时。(这时可以写下面的取地址操作符重载函数)

const Date* Date::operator&() const   //适用于读
{
      return nullptr;
} 

Date* Date::operator&()             //适用于读/写
{
      return nullptr;
}               
  • 12
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值