C++类与对象

类与对象的初步认识

初识类

C语言是面向过程的,注重的是求解问题的步骤,通过函数调用之间逐步解决,
C++基于C语言的,但是C++却是基于面向对象的,所以C++关注的是对象,将同一事件拆分成不同的对象,靠对象之间的联系来解决问题。

类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象。
类与我们之前学过的结构体类似,只不过在C语言的结构体中,只能定义变量,而在C++的类中,还可以定义函数。

说完了类,那对象又是怎么一回事。可以把类理解为一张建筑图纸,而对象则是根据这张图纸建造出来的一个又一个房子。
类是对象的抽象,对象是类的实例化。

定义类

在C语言中,用户自定义类型使用struct关键字,在C++中,用户自定义类型使用class关键字。

class classname
{
	//类体
};

classname为类名,大括号内是类的主体,类中的元素为类的成员
类中的数据为类的属性或者成员变量,类中的函数为类的方法或成员函数

类的访问限定符

C++通过对对象进行封装,把对象的属性和方法用类结合在一块,再通过访问权限将其接口提供给用户使用

类的访问限定符分为三种:public(公有),private(私有),protected(保护)
需要注意的是,类成员的默认访问方式是私有的。一般来说类中的成员变量都是私有的。

public修饰的成员在类外可以直接被访问
private和protected修饰的成员在类外不能被直接访问,必须使用类提供给用户的接口来进行访问
访问权限的作用域是从该访问限定符开始的位置到下一个访问限定符出现的位置

C++为了兼容C,所以struct在C++中也可以去定义类,不过区别是struct定义的类的成员默认访问方式是public。

类的作用域

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

class Person{
public:
	void PrintPerInfo();
private:
	char _name[20];
	char _gender[10];
	int _age;
};
void Person::PrintPerInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

类的大小

我们都知道,在C语言中求结构体的大小时需要内存对齐,那为什么要内存对齐?
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
内存对齐规则:

第一个成员变量在与结构体偏移量为0的地址处
其他成员对齐到对齐数的整数倍的地址处。对齐数=编译器默认的对齐数与该成员大小的较小值,在程序中可以通过#pragma pack(n)来改变
如果嵌套了结构体,则嵌套的结构体对齐到自己的最大对齐数的整数倍处。
结构体的总大小就是最大对齐数(包含嵌套的结构体的对齐数)的整数倍。

C++中计算类的大小也需要进行内存对齐,但是还有区别。我们可以看几个例子

class A{
public:
	void fun1(){}
private:
	int _a;
};

class B{
public:
	void fun2(){}
};

class C{
};

通过计算三个类的大小,我们可以看出B和C类的大小都为1,但是B中有成员函数,为何还与空类的大小一致。而且空类的大小并不是0,而是1。基于此,我们可以猜测:

所以在计算对象的大小时,可以不用计算成员函数的大小,编译器防止让别的代码占据类的地址,所以用一个字节来标识空类,所以空类的大小为1。

类的隐含this指针

先来定义一个类

class Date{
public:
	void PrintDate()
	{
		cout << _year << " " << _month << " " << _day;
	}
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1, d2;
	d1.SetDate(2018, 11, 21);
	d2.SetDate(2018, 08, 08);
	d1.PrintDate();
	d2.PrintDate();
	return 0;
}

我们知道,在一个对象中是没有成员函数的,所以一个对象要调用函数并不是在自己的空间中调用的,那当d1调用SetDate时,函数知道是给d1对象设置,而不是给d2对象设置,所以在函数中肯定存在着一个属性用来标识类对象。这个属性就是this指针。

C++在编译过程中,给每个成员函数增加了一个隐藏的this指针参数,让this指向当前对象,在函数体中所有成员变量的操作都是通过指针访问。
this指针只能在成员函数的内部来使用,this指针是存储在栈上的
this指针本质上是一个成员函数的形参,对象在调用成员函数时编译器会自动将对象地址作为实参传递给this形参
this指针是成员函数的第一个隐含的指针形参,一般由编译器通过ecx寄存器自动传递

所以上述的PrintDate函数就可以这样认为:

void PrintDate(Date *this)
{
	cout << this->_year << " " << this->_month << " " << this->_day;cout 
}

要注意的是,只有在成员函数体内部才有this指针,在外部使用this指针是非法的。

类的成员及成员函数

构造函数

在类中,如果像上面的日期类在每次实例化新对象时都需要调用SetDate来设置新对象的值,这样显得太麻烦了,有没有一种方法在创建对象时就将信息设置进去呢,这就是构造函数

 构造函数是一个特殊的成员函数,名字与类名相同,创建类对象时由编译器自动调用,保证每个数据都有一个合适的初始值,并且在对象的声明周期内值调用一次。构造函数的主要任务不是开辟空间创建对象,而是要初始化对象。

上面的日期类可以改写使用构造函数:

class Date{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void PrintDate()
	{
		cout << _year << " " << _month << " " << _day;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2018, 11, 21);
	Date d2(2018, 8, 8);
	d1.PrintDate();
	d2.PrintDate();
	return 0;
}

当然,为了应对对象实例化的各种场景,C++允许构造函数重载。

如果类中没有显式定义构造函数,编译器会自动生成一个默认的构造函数,但是用户显式定义构造函数之后,编译器不再生成。
构造函数只负责对象的初始化,并不负责对象的构建。
与函数重载相同,无参构造函数和缺省的构造函数只能出现一次,为了防止编译器调用出错,并且无参构造函数和缺省构造函数都称作默认构造函数。

当一个类中定义另一个类的对象,那么编译器在初始化这个类的时候会自动去调用另一个对象的构造函数。

使用上述方法构造对象,在调用构造函数时,对象中已经有了一个初始值,不能将其称作是类对象成员的初始化,初始化只能初始化一次,但是构造函数体内可以进行多次赋值,将构造函数体中的语句称为赋初值更为合适一些。通过初始化列表的方法来对类进行初始化

初始化列表:以一个冒号开始,接着是一个一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式
class Date{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
}

每个成员变量在初始化列表只能出现一次(初始化只能有一次)
当类中包含引用成员变量const 成员变量类类型成员(没有默认构造函数)必须在初始化列表位置进行初始化
对于自定义类型成员变量,构造函数会先使用初始化列表进行初始化
成员变量在类中的声明次序就是初始化列表的初始化顺序,与其在初始化列表中的先后次序无关

析构函数

通过构造函数来初始化一个对象,那么就可以通过析构函数来完成对象的销毁。在对象销毁时编译器会自动调用析构函数,完成一些资源的清理。

析构函数名就是在类名前面加上~
析构函数没有参数没有返回值
一个类可以有多个构造函数,但是只能有一个析构函数,如果用户未显式定义,那么系统会自动生成一个默认的析构函数
在对象生命周期结束时,C++编译器自动调用析构函数

下面这种在类中有资源管理的情况下就需要用户显式给出析构函数,否则会造成资源受损。

class SeqList{
public:
	SeqList(int capacity)
	{
		_pNode = (int *)malloc(capacity * sizeof(int));
		assert(_pNode);
		
		_size = 0;
		_capacity = capacity;
	}
	~SeqList()
	{
		if(_pNode){
			free(_pNode);
			_pNode = NULL;
			_size = 0;
			_capacity = 0;
		}
	}
private:
	int *_pNode;
	size_t _size;
	size_t _capacity;
};

拷贝构造函数

在创建对象时,我们有可能需要创建一个与其一模一样的新对象,这就需要拷贝构造函数。
拷贝构造函数只有单个形参,而且是对本类对象的引用(一般用const修饰),有编译器自动调用
class Date{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& da)
	{
		_year = da._year;
		_month = da._month;
		_day = da._day;
	}
	void PrintDate()
	{
		cout << _year << " " << _month << " " << _day;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

拷贝构造函数是构造函数的一个重载形式。
拷贝构造函数的参数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。这是因为在调用拷贝构造函数时需要形参实例化,实例化的也是一个对象,也需要调用拷贝构造函数,这样就会造成无穷递归调用。
如果未显式定义,系统会默认生成一个拷贝构造函数。默认的拷贝构造函数是按存储字节序来完成字节序的拷贝。通俗来讲就是值拷贝。

系统会自动生成默认的拷贝构造函数,那什么时候我们需要显式定义,可以看下面的代码

class Str{
public:
	Str(const char *str)
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}
	~Str()
	{
		free(_str);
	}
private:
	char *_str;
};
int main()
{
	Str s1("hello");
	Str s2(s1);
}

如果使用系统自动生成的拷贝构造函数,那么最终造成的结果就是s1和s2指向同一块空间,程序执行完后编译器会报错。

复制运算符重载

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

我们可以重载==,使其可以判断两个日期对象是否相等

class Date{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& da)
	{
		_year = da._year;
		_month = da._month;
		_day = da._day;
	}
	void PrintDate()
	{
		cout << _year << " " << _month << " " << _day;
	}
	bool operator==(const Date& d)
	{
		return _year == d._year 
			&& _month == d._month
			&& _day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2018, 11, 21);
	Date d2(2018, 8, 8);
}

在使用复制运算符重载时,要注意有参数类型,有返回值
重载运算符必须有一个类类型或者枚举类型的操作数
用于内置类型的操作符不能改变其含义
作为类成员函数时,形参第一个参数为隐含的this指针
.*、::、sizeof、?:、.不能被重载
一个类没有显式声明复制运算符重载,编译器也会生成一个完成按对象字节序的拷贝

const成员

将const修饰的类成员函数称为const成员函数,const修饰的成员函数实际上是修饰的是隐含的this指针,表明在该成员函数不能对类的任何成员进行任何修改。

const对象不能调用其他非const成员函数
非const对象可以调用非const成员函数const成员函数
const成员函数内可以调用其它const成员函数
非const成员函数可以调用其他非const成员函数和const成员函数

取地址操作符重载和const修饰的取地址操作符重载

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

这两个默认成员函数不需要重新定义,编译器会默认生成。

static成员

声明为static的类成员变是类的静态成员,用static修饰的成员变量,称之为静态成员变量。静态的成员必须要在类外进行初始化。

下面是一个用静态成员实现的一个类,用来计算程序中创建了多少个类对象。

class Sum{
public:
	Sum()
	{
		_count++;
	}
	Sum(const Sum& s)
	{	
		_count++;
	}
	static int GetCount()
	{	
		return _count;
	}
private:
	static int _count;
};

静态成员为所有类对象所共享,不属于某个具体的实例
静态成员变量在类外定义,定义时不添加static关键字
类静态成员可用类名::静态成员或者对象.静态成员函数来访问
静态成员没有隐含的this指针,不能访问任何非静态成员
静态成员和普通成员一样,也有三中访问级别,可以具有返回值,const修饰等参数。
静态成员函数可以调用非静态成员函数,但是非静态成员函数可以调用静态成员函数

友元

友元函数

友元函数是指某些虽然不是类成员却能够访问类的所有成员的函数。类授予它的友元特别的访问权。

在重载operator<<时是没有办法将operator<<重载成成员函数,因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数,但是在使用中cout需要是第一个形参对象,所以就需要将operator<<重载成为全局函数,但是这样就会导致类外无法访问成员,这里就需要友元来解决

class Date{
friend ostream& operator<<(ostream& _cout, const Date& d);
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{	
	_cout << d._year << "-" << d._month << "-" << d._day << endl;
	return _cout;
}

友元函数可以访问类的私有成员,但不是类的成员函数
友元函数不能用const修饰
友元寒素可以定义在类的任何地方,不受访问限定符限制
一个函数可以是多个类的友元函数

友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。
class Date;
class Time{
	friend class Date;
public:
	Time(int hour, int min, int sec)
		: _hour(hour)
		, _min(min)
		, _sec(sec)
	{}
private:
	int _hour;
	int _min;
	int _sec;
}

class Date{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	void SetTime(int hour, int min, int sec) //直接对Time类的成员变量进行设置
	{
		_t._hour = hour;
		_t._min = min;
		_t._sec = sec;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
}

友元关系是单向性的,不具有交换性
友元关系不能传递

内部类

如过一个类定义在另一个类的内部,那么这个类就是内部类。这个内部类是不属于外部类的,也不能通过外部类的对象去调用内部类,外部类没有任何权限去访问内部类。但是内部类可以通过外部类的对象参数来访问外部类中的所有成员,所以内部类是外部类的友元类。

内部类定义在外部类的任意地方,内部类也可以直接访问外部类的static和枚举成员,sizeof(外部类)和内部类没有任何关系。

©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值