[C/C++]详解C++的类和对象

Posted TT在长大

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C/C++]详解C++的类和对象相关的知识,希望对你有一定的参考价值。


类是现实世界或思维世界中的实体在计算机中的反映,它将数据以及这些数据上的操作封装在一起。对象是具有类类型的变量。类和对象是面向对象编程技术中的最基本的概念。

1.面向对象

首先来理解什么是面向对象编程。
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

2.类的引入

在C++中的结构体内不仅可以定义变量,也可以定义函数。在C++中常用class来代替struct

struct Student
{
	void Set(const char* name, const char* gender, int age)
	{
	}
	void Print()
	{
	}
	char _name[20];
	char _gender[3];
	int _age;
};

3.类的定义

class className
{
// 类体:由成员函数和成员变量组成
}; 

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,类定义结束时一定要加后面分号。
类中的元素称为类的成员

  1. 数据称为类的属性或者成员变量;
  2. 函数称为类的方法或者成员函数
    类的两种定义方式
  3. 声明和定义全部放在类体中,成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

class className
{
    public:
    	//公共的行为或属性
 
    private:
    //私有的行为或属性
};
  1. 声明放在.h文件中,类的定义放在.cpp文件中
    在.h中
class className
{
    public:
    	void show();
    	//公共的行为或属性
 
    private:
    //私有的行为或属性
};

在.cpp中

void className::show()
{
	cout<<_name<<endl;
}

一般情况会采用第二种方式。

4.类的访问限定符及封装

4.1 访问限定符

访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
C++的访问限定符三种:public(公有) , protected(保护), private(私有)

  1. public修饰的成员在类外可以直接被访问;
  2. protected和private修饰的成员在类外不能直接被访问;
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;
  4. class的默认访问权限为private,struct为public。

4.2 封装

面向对象的有三大特性:封装、继承、多态。在类和对象阶段,只研究类的封装特性。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理。

5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符指明成员属于哪个类域。

class className
{
    public:
    	void show();
    	//公共的行为或属性
 
    private:
    //私有的行为或属性
};

void className::show()
{
	cout<<_name<<endl;
}

6.类的实例化

类类型创建对象的过程,称为类的实例化。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图。与C语言中的结构体相同。
类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间存储类成员变量

7.类对象模型

7.1 计算类对象的大小

一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。
我们通过对下面的不同对象分别获取大小进行分析:

// 类中既有成员变量,又有成员函数
class A1 {
	public:
	void f1(){}
	private:
	int _a;
};
// 类中仅有成员函数
class A2 {
	public:
	void f2() {}
};
// 类中什么都没有---空类
class A3
{};

sizeof(A1) = 4; sizeof(A2) = 1; sizeof(A3) = 1;

7.2 结构体内存对齐规则

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

8.类成员函数的this指针

this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。

8.1 this指针是什么

C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。 一般存在栈中。

8.2 this指针的特性

  1. this指针的类型:类型 const*
  2. 只能在“成员函数”的内部使用
  3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参,所以对象中不存储this指针
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

9.类的6个默认成员函数

若类中任何成员都没有就称之为空类。但它不会真的什么都没有,编译器会自动生成6个默认的成员函数:
构造函数:完成初始化工作;
析构函数:完成清理函数;
拷贝构造:使用同一个类的对象初始化创建对象;
赋值重载:主要是把一个对象赋值给另一个对象;
取地址重载:有两个函数,主要是对普通对象和const对象取地址;

class A
{
public:
    A();//构造函数
    A(const A& a);//拷贝构造函数
    ~A();//析构函数
    A& operator=(const A& a);//赋值运算符重载
    A* operator &();//取地址运算符重载
    const A* operator &() const;//const修饰的取地址运算符重载
};

下面我将对这六个默认的成员函数进行介绍。

9.1 构造函数

构造函数是一个特殊的成员函数在对象的生命周期内只调用一次。 构造函数名与类名相同。构造函数没有返回值。在对象进行实例化的时候编译器自动调用对应的构造函数。构造函数可以重载。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。
以Date类为例

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)			//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "1" << endl;
	}
private:
	int _year;		//对象的命名风格
	int _month;
	int _day;
};
class Date
{
public:
	// 1.无参构造函数
	Date ()
	{}
	// 2.带参构造函数
	Date(int year , int month, int day)			//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "1" << endl;
	}
private:
	int _year;		//对象的命名风格
	int _month;
	int _day;
};
void Test()
{
	Date d1; // 调用无参构造函数
	Date d2 (2021, 5, 30); // 调用带参的构造函数
	// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
	Date d3();
}
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
  2. 无参的和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
  3. 构造函数会对自定类型成员调用的它的默认成员函数

9.2 析构函数

析构函数:与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
析构函数名也应与类名相同,只是在函数名前面加一个位取反符~stud( ),以区别于构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能重载。如果用户没有编写析构函数,编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数,编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显式的析构函数。

typedef int DataType;
class SeqList
{
public :
	SeqList (int capacity = 10)
	{
		_pData = (DataType*)malloc(capacity * sizeof(DataType));
		assert(_pData);
		_size = 0;
		_capacity = capacity;
	}
	~SeqList()				//析构函数
	{
		if (_pData)
		{
			free(_pData ); // 释放堆上的空间
			_pData = NULL; // 将指针置为空
			_capacity = 0;
			_size = 0;
		}
	}
private :
	int* _pData ;
	size_t _size;
	size_t _capacity;
};

会自定类型成员调用它的析构函数。

9.3 拷贝构造函数

1.概念
构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
2.特性
拷贝构造函数也是特殊的成员函数。
其特征如下:

  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用,会被认为重新定义了一个对象,并且继续使用拷贝构造,这样无限递归。
class Date
{
public:

	Date(int year = 1900, int month = 1, int day = 1)			//构造函数
	{
		_year = year;
		_month = month;
		_day = day;
		cout << "1" << endl;
	}

	Date(Date& d)						//拷贝构造函数,不可以Date(Date d)会造成无限递归
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		cout << "2" << endl;
	}
	~Date()														//析构函数
	{
		
	}

private:
	int _year;		//对象的命名风格
	int _month;
	int _day;
};
  1. 系统会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
  2. 当对象中存在如栈这种结构,我们就需要自己写拷贝构造函数,进行深拷贝。

9.4 赋值操作符重载

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

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型或者枚举类型的操作数
  3. 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
  4. 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
  5. .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。

2.赋值运算符重载
赋值运算符主要有四点:

  1. 参数类型
  2. 返回值
  3. 检测是否自己给自己赋值
  4. 返回*this
  5. 一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
    3.一点点示例
class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1);				//1.构造函数,全缺省。
	void Print();		//打印

	//析构,拷贝构造,赋值重载,可以不写,默认生成
	//stack才写

	//运算符重载,日期加减
	Date& operator+=(int day);			//+=之后,day这个实体还是存在的,所以可以返回引用
	Date operator+(int day);

	Date& operator-=(int day);
	Date operator-(int day);

	//()为空是前置++,增加(int)是为了构成重载,后置++;
	Date& operator++();								//++d 前置可以引用,因为是先加自己身上再返回
	Date operator++(int);							//d++ 用int占位,不需要给实参

	Date& operator--();
	Date operator--(int);

	// 运算符重载
	bool operator>(const Date& d)const;
	bool operator<(const Date& d)const;
	bool operator==(const Date& d)const;
	bool operator<=(const Date& d)const;
	bool operator>=(const Date& d)const;
	bool operator!=(const Date& d)const;

	// 日期-日期 返回天数
	int operator-(const Date& d);

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

inline int GetDay(int year, int month)									//返回当月最大日期,频繁调用所以内联	
{
	static int days[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };		//12个月的日期,只创建一次,放在静态区
	int day = days[month - 1];
	if (month == 2 && ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)) )		//判断闰年
	{
		day = 29; 
	}
	return day;
}

Date::Date(int year, int month, int day)				//指定类域,缺省函数只能在一个地方出现
{
	if (year >= 0
		&& month <= 12 && month >= 1
		&& day > 0 && day <= GetDay(year, month))		//判断日期的合法性
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << " no " << endl;
		assert(false);
	}

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

Date& Date::operator+=(int day)						//
{
	if (day < 0)									//day<0
	{
		_day -= -day;
	}
	else
	{
		_day += day;

		while (_day > GetDay(_year, _month))			//判断day是否合法,不合法就月份+1,月份超出就置1,年++;
		{
			cout << GetDay(_year, _month) << _month<<endl;
			_day -= GetDay(_year, _month);
			_month++;
			if (_month > 12)
			{
				_month = 1;
				_year++;
			}
		}
	}

	return *this;
}

Date Date::operator+(int day)			//需要创建一个新的对象,返回这个对象
{
	Date ret(*this);					//使用默认拷贝构造函数(浅拷贝)

	ret += day;					//直接进行复用,直接操作对象,不要操作成员。

	return ret;

}

Date& Date::operator-=(int day)
{
	if (day < 0)								//减负数
	{
		_day += -day;
	}
	else										
	{
		_day -= day;

		while (_day <= 0)
		{
			_month--;							//先-月,因为要往上一个月算,
			if (_month < 0)						//逻辑参考+=
			{
				_month = 12;
				_year--;
			}
			_day += GetDay(_year, _month);
		}
	}

	return *this;
}

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

	ret -= day;

	return ret;
}



//Date:: 声明域
bool Date::operator>(const Date& d)const
{
	if (_year < d._year)
	{
		return false;
	}
	else if (_year > d._year)
	{
		return true;
	}
	else
	{
		if (_month < d._month)
		{
			return false;
		}
		else if (_month > d._month)
		{
			return true;
		}
		else
		{
			if (_day > d._day)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
	}

}
bool Date::operator<(const Date& d)const
{
	if (_year > d._year)
	{
		return false;
	}
	else if (_year < d._year)
	{
		return true;
	}
	else
	{
		if (_month > d._month)
		{
			return false;
		}
		else if (_month < d._month)
		{
			return true;
		}
		else
		{
			if (_day < d._day)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
	}
}
bool Date::operator==(const Date& d)const
{
	if (*this > d || *this < d)
		return false;
	else
		return true;
}
bool Date::operator<=(const Date& d)const
{
	if (*this > d)
		return false;
	else
		return true;
以上是关于[C/C++]详解C++的类和对象的主要内容,如果未能解决你的问题,请参考以下文章

[C/C++]详解C++中的继承

C/C++: C++可调用对象详解

C++--C++对象模型分析c++中的抽象类和接口

C++的类和对象

php的类和对象详解

C++中的类和对象