类和对象基础

类和对象基础

一:类的概念和封装

我们前面了解过C语言是一门面向过程的编程语言,而C++是一门面向对象的编程语言,

这个“面向对象”其实就体现在类和对象上,我们画上一幅图可以清楚认识一下:

 

我们之前了解过C++的三大特征:封装、继承、多态

继承和多态我们放在后面进行了解,这里主要搞清楚什么是封装:

  • 封装是面向对象程序设计的基础特征。它是将数据(属性)与函数(方法)进行合并成一个整体,也就是我们上面所说的类。(把属性和方法进行封装
  • 将客观事物封装成抽象的类,类可以将自己的属性与方法对指定的用户开放,对其他用户进行隐藏。(对属性和方法进行访问权限控制

 

这里我们提到了访问权限控制,就不得不说一下类的三种成员访问限定符

  • public: 任意位置都可以访问
  • protected: 只有子类和本类类中允许访问
  • private: 只允许本类类中访问

 

并且我们需要了解到:

  • 只有const static修饰的成员数据(静态常量)才可以在类中初始化
  • const static修饰的数据是在声明类的时候直接初始化的,而非const修饰的static的成员数据必须在类外进行初始化。
  • 在类中不使用任何访问限定符的时候,默认权限是private私有的。

 

我们这时候会发现,类和我们在C语言中学过的struct结构体很相似,那么我们可以提出两个问题:

①:C语言中的struct结构体和C++中的struct结构体有没有区别?

②:C++中的struct结构体和class类有没有区别?

我们依次来解答:

第一个问题:

结论:struct在C语言和C++中是有区别的。

  1. 在C语言中,struct是用户自定义数据类型,而在C++中,struct是抽象的数据类型,支持成员定义函数。
  2. C语言中,struct只是变量的聚合体,可以封装数据,但是不可以隐藏,不可以定义函数成员,但是C++中的struct可以定义函数成员。

第二个问题:

结论:C++中struct结构体和class类是有区别的。

  1. 成员访问权限:class的默认访问权限是private私有的,而struct在C++中相当于类,不过struct的默认访问权限是public公有的。
  2. 默认的继承方式:class类默认是以private私有的方式继承的,而struct默认是以public共有的方式继承的。

 

这里有一些细节,我们需要注意一下:

  • 类中实现的成员方法默认是内联的(系统会默认在函数前加上inline),不过我们都知道内联函数只是我们对编译器的建议,采不采纳还得看编译器自己根据实际情况判断。
  • 成员方法也可以在类内声明,而在类外定义,不过在类外定义时类名前要加上类作用域。(需要注意的是类作用域加在函数名前,而不是返回值前)
  • 一般我们使用输出型参数带回数值,而不使用return,return会带出成员变量的地址,不安全,存在修改的风险。

 

我们再来认识一下类中常出现的this指针(用来接收对象的地址):

  • this指针的类型 => 类名* const this
  • 普通的成员方法使用thiscall的调用约定
  • 普通的成员方法依赖对象调用(依赖:一定要有不依赖:可以有也可以没有

 

 

二:C++中默认的六个函数

C++中有默认的六个函数,分别是:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 赋值运算符的重载函数
  5. 取地址操作符的重载函数
  6. const修饰的取地址操作符的重载函数

 

我们在介绍这些函数之前,先得知道对象的生成步骤与对象的销毁步骤:

对象的生成,分为两步:

  1. 对象开辟内存空间
  2. 调用构造函数,对对象开辟的内存空间进行初始化。

对象的销毁,也分为两步:

  1. 调用析构函数,释放对象所占的其他资源
  2. 释放对象自身所占的内存空间

 

我们这时再来依次介绍一下这些函数,这里只介绍了最重要的前四种函数:

①:构造函数(用来初始化对象的内存空间的)

  • 构造函数是一种特殊的成员函数,与其他成员函数不同,它不需要对象调用,而是在创建对象的时候自动调用。
  • 构造函数可以重载(比如人,生而不同),若是类有编写好的构造函数,则系统提供的默认构造函数失效,自己写的构造函数可以有参数列表,但是不能有函数返回值
  • 当类中没有定义任何构造函数的时候,C++编译器默认为我们定义一个无参构造函数,函数体为空。

这里我们要区分清楚构造函数调用和构造函数声明的区别:

比如类Student,我们这样调用“Student st1();”,你这时是否以为我们是调用了没有参数的构造函数实例化了对象st1,但是错了,系统会默认为这里是一个函数声明

这样调用“Student st1;”才是调用没有参数的构造函数实例化了对象st1,上边的那种有返回值类型Student,有函数名st1,有参数列表(),显然是一个函数声明,是不会生成一个对象的,其实很简单,但是一般不仔细的话,可能会出错,我之前做题的时候就遇到出错了,所以这个陷阱我们要知道并注意。

 

我们这里给一个简单的构造函数代码:

#include <iostream>

class Student//一个学生的对象  属性只有两个  一个姓名  一个年龄
{
public:
	Student(char *name, int age)//自定义的构造函数,默认的构造函数失效
	{
		mname = new char[strlen(name) + 1]();
		strcpy(mname, name);
		mage = age;
	}

	void show()
	{
		std::cout << mname << std::endl;
		std::cout << mage << std::endl;
	}
private:
	char *mname;
	int mage;
};


int main()
{
	Student st1("quanwudi", 21);
	st1.show();

	return 0;
}

 

②:析构函数(用来释放对象所占的其他资源的)

  • 析构函数使用~类名进行调用(在类名前多一个~波浪线)
  • 析构函数不可以重载(比如人,死都一样)
  • 先构造的,后析构(也可以说后构造的,先析构)
  • 当类中没有定义任何析构函数的时候,C++编译器默认为我们定义一个无参析构函数,函数体为空,什么都不干,不会为我们释放其他额外申请的资源,所以一般我们会自己提供析构函数。

这里我们要清楚几点:

  1. 构造函数是不能手动调用的,必须系统调用(因为如果要手动调用构造函数,那么这个对象先要生成好,因为它依赖对象调用,但是生成对象却需要调用构造函数,这就成死循环了,比如鸡生蛋,蛋生鸡问题,无限循环,所以构造函数要交给系统调用)
  2. 析构函数是可以手动调用的,它手动调用时会退化成普通的成员方法,不会影响对象在生存周期到了之后自动再次调用析构函数
  3. 这两个就比如一个人的出生时间是无法自己控制的,但是死亡时间是可以控制的。(呸呸呸,晦气走开)

 

我们这里给一个简单的构造函数代码:

#include <iostream>

class Student//一个学生的对象  属性只有两个  一个姓名  一个年龄
{
public:
	Student(char *name, int age)//自定义的构造函数,默认的构造函数失效
	{
		mname = new char[strlen(name) + 1]();
		strcpy(mname, name);
		mage = age;
	}

	~Student()
	{
		delete []mname;//将申请的额外空间资源释放掉,不然会造成内存泄漏
		mname = NULL;//一般,将一个指针指向的空间释放掉之后,将其置NULL,防止出现野指针。
	}

	void show()
	{
		std::cout << mname << std::endl;
		std::cout << mage << std::endl;
	}
private:
	char *mname;
	int mage;
};


int main()
{
	Student st1("quanwudi", 21);
	st1.show();

	return 0;
}

 

③:拷贝构造函数(用一个已存在的对象来生成一个相同类型的新对象)

  • 拷贝构造函数的形参是const + 一个类对象的引用,这样做是防止无限递归构造形参对象(因为形参若是一个类对象,那么值传递的时候会默认生成一个临时对象,而这个临时对象的生成还是得要靠调用拷贝构造函数,这样子就形成了无限递归调用,最终会导致栈满,溢出,而使用类对象的引用的好处就是,引用不会造成新对象的生成)
  • 当类中没有定义任何拷贝构造函数的时候,C++编译器默认为我们定义一个拷贝构造函数,这个默认的拷贝构造函数是一个浅拷贝,所以有可能会让两个指针指向同一块内存,对象生存周期到了之后会将这块内存重复释放,会崩溃掉,所以我们一般成员变量有指针时,会自己提供一份深拷贝的拷贝构造函数。

 

拷贝构造函数的四种调用方式:

  • 当用对象1初始化对象2的时候调用拷贝构造函数
  • 当用对象1括号方式初始化对象2的时候调用拷贝构造函数
  • 当用对象(此处是对象而非指针或引用)做函数参数的时候,实参传递给形参的过程中会调用拷贝构造函数(会调用拷贝构造函数生成一个临时对象)
  • 当用对象(此处是对象而非指针或引用)做函数返回值的时候,若用同一类型来接收该返回值的时候,会调用拷贝构造函数(会调用拷贝构造函数生成一个临时对象,接收的其实是这个临时对象)

 

我们这里给一个简单的拷贝构造函数代码:

#include <iostream>

class Student//一个学生的对象  属性只有两个  一个姓名  一个年龄
{
public:
	Student(char *name, int age)//自定义的构造函数,默认的构造函数失效
	{
		mname = new char[strlen(name) + 1]();
		strcpy(mname, name);
		mage = age;
	}

	Student(const Student& rhs)//自定义的拷贝构造函数,我们自己实现一份深拷贝的
	{
		mname = new char[strlen(rhs.mname) + 1]();
		strcpy(mname, rhs.mname);
		mage = rhs.mage;
	}


	~Student()
	{
		delete []mname;//将申请的额外空间资源释放掉,不然会造成内存泄漏
		mname = NULL;//一般,将一个指针指向的空间释放掉之后,将其置NULL,防止出现野指针。
	}

	void show()
	{
		std::cout << mname << std::endl;
		std::cout << mage << std::endl;
	}
private:
	char *mname;
	int mage;
};


int main()
{
	Student st1("quanwudi", 21);
	st1.show();

	Student st2 = st1;
	st2.show();

	return 0;
}

 

④:赋值运算符的重载函数(把一个已存在的对象赋值给相同类型的已存在对象)

  • 赋值运算符的重载函数的形参也是const + 一个类对象的引用,加const是因为(1)我们不希望对传进来的对象进行任何修改(2)可以成功接收隐式生成的临时对象。而加引用的原因和上个函数一致,可以避免在函数调用的时候生成临时对象,提高了效率。
  • 和以上三个函数不同的是,赋值运算符的重载函数是有返回值的,一般的,返回值是被赋值者的引用,即*this,原因是在函数返回时避免了生成临时对象,提高了效率,更重要的是,这样可以实现连续赋值,即类似a=b=c这样,如果不是返回引用而是返回值类型,那么执行a=b的时候,返回时返回的其实是生成的临时对象,而这个临时对象不能做左值,再执行=c就会报错。
  • 当类中没有显式地提供一个以本类或本类的引用为参数的赋值运算符的重载函数时,编译器才会为我们提供一个默认的赋值运算符的重载函数。系统提供的默认的赋值运算符的重载函数还是一个浅拷贝模式,所以我们一般自己提供一个深拷贝的赋值运算符的重载函数。

 

赋值运算符的重载函数的实现:

  1. 通过if判断,防止自赋值
  2. 释放旧资源,防止内存泄漏
  3. 开辟新资源
  4. 赋值

 

注意:我们调用赋值运算符的重载函数时,如果“=”号两边类型不匹配,则在构造函数中找参数为右值的类型,若可以找到,则根据这个右值调用这个构造函数生成临时对象,再进行赋值运算符的重载函数。(比如:st1 = 10;)

 

这种临时对象的生成方式有两种,分别为:

  • 隐式生成临时对象,编译器会推演需要的对象类型(比如:st1 = 10;)
  • 显式生成临时对象,程序指明生成的对象类型类型(比如:st1 = (int) 10;)
  • 这里存在一个优化如果临时对象的生成是为了生成新对象,则以生成临时对象的方式来生成新对象。

 

我们这里给一个简单的赋值运算符的重载函数代码:

#include <iostream>

class Student//一个学生的对象  属性只有两个  一个姓名  一个年龄
{
public:
	Student(char *name, int age)//自定义的构造函数,默认的构造函数失效
	{
		mname = new char[strlen(name) + 1]();
		strcpy(mname, name);
		mage = age;
	}

	Student(const Student& rhs)//自定义的拷贝构造函数,我们自己实现一份深拷贝的
	{
		mname = new char[strlen(rhs.mname) + 1]();
		strcpy(mname, rhs.mname);
		mage = rhs.mage;
	}

	Student& operator =(const Student &rhs)//自定义的赋值运算符的重载函数,我们这里实现一份深拷贝的
	{
		if(this != &rhs)//防止自赋值
		{
			delete []mname;//释放旧资源
			mname = new char[strlen(rhs.mname) + 1]();//开辟新资源
			strcpy(mname, rhs.mname);//赋值
			mage = rhs.mage;
		}

		return *this;
	}

	~Student()
	{
		delete []mname;//将申请的额外空间资源释放掉,不然会造成内存泄漏
		mname = NULL;//一般,将一个指针指向的空间释放掉之后,将其置NULL,防止出现野指针。
	}

	void show()
	{
		std::cout << mname << std::endl;
		std::cout << mage << std::endl;
	}
private:
	char *mname;
	int mage;
};


int main()
{
	Student st1("quanwudi", 21);
	st1.show();

	Student st2 = st1;
	st2.show();

	Student st3("wudiquan", 22);
	st3.show();
	st2 = st3;
	st2.show();

	return 0;
}

 

 

三:其他零碎知识点

①对象的生存周期

  1. 一般遇到表达式结束。(如“,”逗号//“?”问号//“;”分号)
  2. 不过引用会提升临时对象的生存周期,使临时变量的生存周期变得和引用对象一样。
  3. 需要注意的是指针不会提升临时对象的生存周期

 

②不同临时量的类型

  1. 内置类型生成的临时量的类型为 ---> 常量
  2. 自定义类型生成的临时量的类型为 ---> 变量 内存中
  3. 隐式类型生成的临时量的类型为 ---> 常量

猜你喜欢

转载自blog.csdn.net/IT_Quanwudi/article/details/87051486