第二章 类与对象

第二章 类与对象,第1张

1.面向过程和面向对象初步认识(学习要求:了解) C语言是面向过程的 关注 的是 过程 ,分析出求解问题的步骤,通过函数调用逐步解决问题。 C++是基于面向对象的 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完成。

通俗理解的话可以把编写程序比作造手机,A手机厂商是从最基础的电子电路设计开始,一步一步来,一直到手机成型,这叫面向过程;B手机厂商则从各个电子厂进口零部件直接组装成手机,这叫面向对象。

2.类和对象的引入(学习要求:熟悉) C 语言中,结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。

例如

struct Student
{
 void SetStudentInfo(const char* name, const char* gender, int age)    //定义函数
 {
 strcpy(_name, name);
 strcpy(_gender, gender);
 _age = age;
 }
 
 void PrintStudentInfo()                                               //定义函数
 {
 cout<<_name<<" "<<_gender<<" "<<_age<
上面结构体的定义,在C++中常用 class 来代替 struct 称为 ,而“Student s”中的“s”就是类定义出来的 对象

3.类的定义(学习要求:掌握)
class className
{
 // 类体:由成员函数和成员变量组成
 
}; // 一定要注意后面的分号
class 定义类的 关键字, ClassName 为类的名字, {} 中为类的主体,注意 类定义结束时后面 分号 类中的元素称为 类的成员: 类中的 数据 称为 成员变量 ; 类中的 函数 称为 成员函数 类的两种定义方式: 1. 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

2. 声明放在.h文件中,类的定义放在.cpp文件中

 一般情况下,更期望采用第二种方式。

4.类的访问限定符及封装(学习要求:掌握)

4.1 访问限定符 C++实现封装的方式:通过访问权限选择性的将对象的接口提供给外部的用户使用。

【访问限定符说明】

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

注意: C++ 需要兼容 C 语言,所以 C++ struct 可以当成结构体去使用。另外 C++ struct 还可以用来定义类。 和class 是定义类是一样的,区别是 struct 的成员默认访问方式是 public class 是的成员默认访问方式是private。

4.2 封装 面向对象的三大特性: 封装、继承、多态 在类和对象阶段,我们只研究类的封装特性,那什么是封装呢? 封装:将数据和 *** 作数据的方法进行有机结合,隐藏对象的成员变量和实现细节,仅对外公开接口来和对象进行 交互。 封装本质上是一种管理 :我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们 首先建了一座房子把兵马俑给封装 起来。但是我们目的全封装起来,不让别人看。所以我们 开放了售票通 ,可以买票突破封装在合理的监管机制下进去参观。 类也是一样,我们将类数据和方法都封装一下。不想给别人看到的,我们使用 protected/private 把成员 封装 起来。 开放 一些共有的成员函数对成员进行合理的访问。所以封装本质是一种管理。 5.类的作用域(学习要求:熟悉) 类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员,需要使用 :: 作用域解析符 指明成员属于哪个类域。 例如
class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};


// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " " << _gender << " " << _age << endl;
}

6.类的实例化(学习要求:掌握) 用类类型创建对象的过程,称为类的实例化 1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它 2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量 3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什 么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间

7.类对象模型(学习要求:了解) 7.1 类对象的存储方式 1.对象中包含类的各个成员

缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。

2.只保存成员变量,成员函数存放在公共的代码段

问题:对于上述两种存储方式,那计算机到底是按照那种方式来存储的? 我们再通过对下面的不同对象分别获取大小来分析看下
int main()
{
	// 类中既有成员变量,又有成员函数
	class A1 
	{
	public:
		void f1() {}
	private:
		int _a;
	};


	// 类中仅有成员函数
	class A2 
	{
	public:
		void f2() {}
	};


	// 类中什么都没有---空类
	class A3
	{};

	cout << sizeof(A1) << endl << sizeof(A2) << endl << sizeof(A3) << endl;
	return 0;
}

输出结果

4
1
1

结论: 一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

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

8.this指针(学习要求:掌握)

8.1 成员变量和成员函数是如何存储的

在C++中,类的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。

//成员变量和成员函数是分开存储的

//测试用例1,创建一个空类
class Person
{
	//测试用例2,创建一个非静态成员变量
	int m_A;

	//测试用例3,创建一个静态成员变量
	static int m_B;                        //类内创建

	//测试用例4,创建一个非静态成员函数
	void func1()
	{
		
	}

	//测试用例,创建一个静态成员函数
	static void func2()
	{

	}

};

int Person::m_B = 100;                     //类外初始化

//测试案例1,查看空对象的大小
void test1()
{
	Person p1;

	//空对象占用的空间大小
	cout << "size of p1=" << sizeof(p1) << endl;
}



//测试用例2,
void test2()
{
	Person p2;

	//对象中加入一个非静态成员变量后,对象占用的空间大小
	//由输出结果可以看出非静态成员变量在类的对象上
	cout << "size of p2=" << sizeof(p2) << endl;
}


//测试用例3,
void test3()
{
	Person p3;

	//对象中加入一个非静态成员变量以及静态成员变量后,对象占用的空间大小
	//由输出结果可以看出静态成员变量不在类的对象上
	cout << "size of p3=" << sizeof(p3) << endl;
}


//测试用例4,
void test4()
{
	Person p4;

	//对象中加入一个非静态成员变量以及静态成员变量后,对象占用的空间大小
	//由输出结果可以看出非静态成员函数不在类的对象上
	cout << "size of p4=" << sizeof(p4) << endl;
}


//测试用例5,
void test5()
{
	Person p5;

	//对象中加入一个非静态成员变量以及静态成员变量后,对象占用的空间大小
	//由输出结果可以看出静态成员函数不在类的对象上
	cout << "size of p5=" << sizeof(p5) << endl;
}



int main()
{
	//test1();
	//test2();
	//test3();
	//test4();
	test5();
	return 0;
}

输出结果依次为

test1()

size of p1=1

test2()

size of p2=4

test3()

size of p3=4

test4()

size of p4=4

test5()

size of p5=4

8.2  this 指针的引出 8.2.1 this 指针的概念 每一个非静态成员函数只有一份,也就是说,多个同类型的对象会公用一个 非静态成员函数,那么这个非静态成员函数是如何知道是哪个对象调用了自己的了? C++中通过引入this指针解决该问题,即: C++ 编译器给每个 非静态的成员函数 增加了一个隐藏的指针参数,让该指针指向调用它的哪个对象。而该过程对用户是透明的,即用户不需要来自己写this指针,全部工作由编译器自动完成 8.2.2 this 指针的用途 ①当形参和成员变量同名时,可以用this指针来区分 ②在类的非静态成员函数中返回对象本身,可使用return *this 具体使用方法看下面的例子
class Person
{
public:
	Person(int age)
	{
		//age = age;       //不加this时编译器默认这三个age是同一个age,且都是形参,故成员变量age没有被赋值成功
		this->age = age;   //加上this指针后,成员变量age被赋值成功
	}


	
	Person& PersonAgeAdd(Person& p)     //返回p2的引用
  //Person PersonAgeAdd(Person& p)      //注意当不用引用返回时,返回的只是p2的一个拷贝,p2.age的值再用链式编程时不再有效
	{
		this->age = this->age + p.age;

		return *this;                   //此时this是指向p2的指针,而*this则就是p2
	}

	int age;
};

//this指针用途
//1.解决名称冲突
void test1()
{
	Person p1(18);
	cout << p1.age << endl;
}

//2.返回对象本身用*this
void test2()
{
	Person p1(10);
	Person p2(10);

	p2.PersonAgeAdd(p1);
	cout << p2.age << endl;

	//链式编程思想
	p2.PersonAgeAdd(p1).PersonAgeAdd(p1).PersonAgeAdd(p1);
	cout << p2.age << endl;
}


int main()
{
	//test1();
	test2();
	return 0;
}

输出结果依次为

test1()

10

test2()
20
50
8.3 用类定义空 指针访问成员函数 C++中用类定义空指针也是可以调用函数的,但是也要注意有没有用到this指针,如果用到this指针,需要加以判断保证代码的健壮性。 具体说明看下面的例子
class Person
{
public:
	void ShowClassName()
	{
		cout << "this is Person class" << endl;
	}

	void ShowPersonAge()
	{
		/*if (this == NULL)             //3.避免错误可以加上这个判断
		{
			return;
		}*/
		cout << "age= " << m_age << endl;
	}

	int m_age;
};

void test1()
{
	Person* p = NULL;
	p->ShowClassName();
}

void test2()              
{
	Person* p = NULL;
	p->ShowPersonAge();
}

输出结果依次为

test1()

this is Person class

test2()
报错
8.4 const修饰成员函数和对象 常函数: 1.成员函数 const,我们称这个函数为常函数 2.常函数内不可以修改成员属性 3.成员属性声明时加关键字 mutable,在常函数中依然可以被修改 常对象: 1.声明对象 const称该对象为常对象 2.常对象只能调用常函数
class Person
{
public:

	//this指针的本质是指针常量;指针的指向是不可修改的,相当于Person*const this
	//如果还要指针指向的内容不能改变,则还需要加一个const,而这个const就放在了成员函数后面

	//常函数
	void ShowPerson()const       //放在了成员函数后面
	{
		//this->m_A = 100;       //加上之后m_A的值不可改变
		//this->m_B = 100;       //加上mutable之后,值又可以改变了

	}

	//普通函数
	void func()
	{

	}


	int m_A;
	mutable int m_B;    //特殊变量,即使在常函数中,也可以修改这个值,加关键字mutable就行
};



void test1()
{
	Person p1;
	p1.ShowPerson();
}

void test2()
{
	Person p2;
	p2.ShowPerson();
}


void test3()
{
	const Person p3;      //在对象前加const,变为常对象
	//p3.m_A = 100;       //m_A不可以修改
	//p3.m_B = 100;       //m_B可以修改


	//常对象只能调用常函数
	p3.ShowPerson();
	//p3.func();          //常对象无法调用普通成员函数,因为普通成员函数可以修改普通成员变量
}

int main()
{
	
	return 0;
}

8.5 友元 生活中你的家有客厅(public),有你的卧室(private)。 客厅所有来的客人都可以进去,但是你的卧室是私有的,也就是说只有你能进去,但是你也可以允许你的好闺蜜好基友基友进去。 在程序里,有些私有属性也想让类外特殊的一些函数或类进行访问,就需要用到友元的技术,友元的目的就是让一个函数或类访问另一个类中私有成员。 友元的关键字为: friend 友元的三种实现 Ⅰ.全局函数做友元 Ⅱ.类做友元 Ⅲ.成员函数做友元 Ⅰ.全局函数做友元
//建筑物类
class Building
{
	//2.加上friend void goodfriend(Building* building);函数声明后,该函数便可以访问私有成员
	friend void goodfriend(Building* building);

public:
	Building()
	{
		m_SittingRoom = "客厅";
		m_RedRoom = "卧室";
	}

public:
	string m_SittingRoom;      //客厅(公有)

private: 
	string m_RedRoom;          //卧室(私有)
};


//全局函数
void goodfriend(Building* building)
{
	cout << building->m_SittingRoom << endl;
	//cout << building->m_RedRoom << endl;            //1.访问私有函数会报错
	cout << building->m_RedRoom << endl;              //3.加上友元函数声明后,便可以访问私有函数
}


void test1()
{
	Building building;
	goodfriend(&building);
}


int main()
{
	test1();
	return 0;
}

输出结果

客厅
卧室

Ⅱ.类做友元
//声明一个类
class Building;

//类做友元
class Goodfriend
{
public:
	Building* building;
	Goodfriend();                  //类内声明

	void Visit();                  //类内声明参观函数,访问Building中的成员
};


class Building
{
	//2.Goodfriend类便可以访问Building类的私有成员变量
	friend class  Goodfriend;
public:
	Building();                    //类内声明

public:
		string m_SittingRoom;      //客厅(公有)
	
private: 
		string m_RedRoom;          //卧室(私有)
};



//类外实现成员函数Building()
Building::Building()
{
	m_SittingRoom="客厅";
	m_RedRoom = "卧室";
}



//类外实现成员函数Goodfriend()
Goodfriend::Goodfriend()
{
	//创建建筑物对象
	building = new Building;
}



void Goodfriend::Visit()
{
	cout << building->m_SittingRoom << endl;
	//cout << building->m_RedRoom << endl;    //1.此行会报错,无法访问私有成员变量
	cout << building->m_RedRoom << endl;      //3.将friend class  Goodfriend放入Building类后便可以访问私有成员       
}



void test1()
{
	Goodfriend gg;
	gg.Visit();
}

int main()
{
	test1();
	return 0;
}

Ⅲ.成员函数做友元
class Building;
class Goodfriend
{
public:
	Goodfriend();

	void visit1();             //让visit1()函数可以访问Building中私有成员

	void visit2();             //让visit2()函数不可以访问Building中私有成员

	Building* building;
};


class Building
{

	friend void Goodfriend::visit1();   //3.告诉编译器Goodfriend类下的成员函数作为本类的好朋友,可以访问私有成员
public:
	Building();                //类内声明
public:
	string m_SittingRoom;      //客厅(公有成员)

private:
	string m_BedRoom;          //卧室(私有成员)
};

Building::Building()           //类外实现
{
	m_SittingRoom = "客厅";
	m_BedRoom = "卧室";
}

Goodfriend::Goodfriend()
{
	building = new Building;
}

void Goodfriend::visit1()                      //让visit1()函数可以访问Building中私有成员
{
	cout << building->m_SittingRoom << endl;   //1.访问客厅
	//cout << building->m_BedRoom << endl;     //2.此行会报错,因为无法访问私有成员
	cout << building->m_BedRoom << endl;       //3.此时访问不会报错,因为已经在Building类下加上了friend void Goodfriend::visit1()
}

void Goodfriend::visit2()                      //让visit2()函数不可以访问Building中私有成员
{
	cout << building->m_SittingRoom << endl;   //1.访问客厅
	//cout << building->m_BedRoom << endl;     //2.此行会报错,因为无法访问私有成员
}

void test1()
{
	Goodfriend gg;
	gg.visit1();
	gg.visit2();
}

int main()
{
	test1();
	return 0;
}

9.类的6个默认成员函数(学习要求:掌握) 如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的,任何一个类在我们不写的情况下,都会自动生成下面6个默认成员函数。

例如

class Date {};

9.1构造函数

9.1.1 构造函数的 概念 首先看一般情况下,类的赋值 *** 作,
class Date
{ 

public:

 void SetDate(int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 
 void Display()
 {
 cout <<_year<< "-" <<_month << "-"<< _day <
对于 Date 类,可以通过 SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢? 构造函数 是一个 特殊的成员函数,名字与类名相同 , 创建类类型对象时由编译器自动调用 ,保证每个数据成员 都有 一个合适的初始值,并且 在对象的生命周期内只调用一次

9.1.2 构造函数的8大特性

构造函数 是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象 其特征如下: 1. 函数名与类名相同。 2. 无返回值。 3. 对象实例化时编译器自动调用对应的构造函数。 4. 构造函数可以重载。

例如

class Date
{

public :
 // 1.无参构造函数
 Date ()                                //特性1:函数名与类名相同
 {}                                     //特性2:无返回值
 
 // 2.带参构造函数
 Date (int year, int month , int day )
 {
 _year = year ;
 _month = month ;
 _day = day ;
 }


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


void TestDate()
{
 Date d1;             // 特性3:对象实例化时编译器自动调用对应的构造函数,调用无参构造函数
 Date d2 (2015, 1, 1);// 特性3:对象实例化时编译器自动调用对应的构造函数,调用带参的构造函数
                      // 特性4:构造函数可以重载
 
 // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
 // 以下代码注意与Date d1;区分:作用是声明了d3函数,该函数无参,返回一个日期类型的对象
 Date d3(); 
}

5. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
class Date
{
public:

 /*
 // 如果用户显式定义了构造函数,编译器将不再生成
 Date (int year, int month, int day)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 */
private:

 int _year;
 int _month;
 int _day;
};


void Test()
{
 // 特性五:没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
 Date d; 
}

6. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数全缺省构造函数没写构造函数,都可以认为是默认构造函数。 例如
// 默认构造函数
class Date
{ 

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


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


// 以下测试函数能通过编译吗?
void Test()
{
 Date d1; //不能编译通过,因为这个语句无法确定调用无参构造函数还是全缺省构造函数,默认构造函数 
          //只能有一个
}

7. 关于编译器生成的默认成员函数,很多童鞋会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?观察下面的程序,d对象调用了编译器生成的默认构造函数,但是d对象_year、_month、_day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么卵用? 解答:C++把类型分成 内置类型(基本类型) 、和 自定义类型 。内置类型就是语法已经定义好的类型:如 int char ...,自定义类型就是我们使用 class struct union 定义的类型,看看下面的程序,就会发现 编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认构造函数
class Time
{

public:
	Time()
	{
		_hour = 0;
		_minute = 0;
		_second = 0;
	}

public:
	int _hour;
	int _minute;
	int _second;
};


class Date
{
public:
	// 基本类型(内置类型)
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;                    //自动调用类里面的默认构造函数
};


int main()
{
	Date d;

	cout << d._year << " " << d._month << " " << d._day << endl;
	cout << d._t._hour << " " << d._t._minute << " " << d._t._second << endl;

	return 0;
}

输出结果

-858993460 -858993460 -858993460
0 0 0

8. 成员变量的命名风格
// 我们看看这个函数,是不是很僵硬?
class Date
{

public:
 Date(int year)
 {
 // 这里的year到底是成员变量,还是函数形参?
 year = year;
 }

private:
 int year;
};


// 所以我们一般都建议这样
class Date
{
public:
 Date(int year)
 {
 _year = year;
 }

private:
 int _year;
};

9.2.析构函数

9.2.1析构函数的概念

前面通过构造函数的学习,我们知道一个对象是怎样自动初始化的,那一个对象能不能做到使用结束时自动销毁了? 析构函数 与构造函数功能相反,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

9.2.2析构函数的5大特性

析构函数是特殊的成员函数。 其特征如下: 1. 析构函数名是在类名前加上字符 ~。 2. 无参数无返回值。 3. 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。 4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

例如

typedef int DataType;

class SeqList
{ 

public :
 SeqList (int capacity = 10)
 {
 _pData = (DataType*)malloc(capacity * sizeof(DataType));
 assert(_pData);
 
 _size = 0;
 _capacity = capacity;
 }
 
 ~SeqList()           //特性1:析构函数名是在类名前加上字符 ~
 {                    //特性2:无参数无返回值
 if (_pData)          //特性3:一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析              
 {                    //构函数
 free(_pData );       // 释放堆上的空间
 _pData = NULL;       // 将指针置为空
 _capacity = 0;
 _size = 0;
 }

 }
  
private :
 int* _pData ;
 size_t _size;
 size_t _capacity;

};

5. 关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,会对自定类型成员调用它的析构函数
class String
{

public:
 String(const char* str = "jack")
 {
 _str = (char*)malloc(strlen(str) + 1);
 strcpy(_str, str);
 }
 ~String()
 {
 cout << "~String()" << endl;
 free(_str);
 }

private:
 char* _str;
};


class Person
{
private:
 String _name;
 int _age;
};


int main()
{
 Person p;
 return 0; 
}

输出结果

~String()

9.3  拷贝构造函数 9.3.1 拷贝构造函数的概念 在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。 那在创建对象时,可否创建一个与一个对象一某一样的新对象呢? 构造函数 只有单个形参,该形参是对本类 类型对象的引用(一般常用const修饰),在用已存在的类 类型对象创建新对象时由编译器自动调用。 9.3.2 拷贝构造函数的4大特性 拷贝构造函数也是特殊的成员函数,其特征如下: 1. 拷贝构造函数是构造函数的一个重载形式。 2. 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。 例如
class Date
{

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

	Date(const Date& d)   //特性2:拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会                       
	{                     //引发无穷递归调用
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

public:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1;
	Date d2(d1); 

	cout << d2._year << " " << d2._month << " " << d2._day << endl;

	return 0;         
}     

输出结果

1900 1 1

3. 若不是显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝

例如

	class Date
	{

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

	public:
		int _year;
		int _month;
		int _day;
	};


	int main()
	{
		Date d1;          // 自动调用默认构造函数
		Date d2(d1);      // 这里d2调用的默认拷贝构造完成拷贝,d2和d1的值是一样的。
		
		cout << d2._year << " " << d2._month << " " << d2._day << endl;
		return 0;
	}

输出结果:

1900 1 1

4. 那么编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,我们还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
// 这里会发现下面的程序会崩溃掉?这里就需要以后讲的深拷贝去解决。
class String
{

public:
 String(const char* str = "jack")
 {
 _str = (char*)malloc(strlen(str) + 1);
 strcpy(_str, str);
 }
 ~String()
 {
 cout << "~String()" << endl;
 free(_str);
 }

private:
 char* _str;
};


int main()
{
 String s1("hello");
 String s2(s1);
}

输出结果

这里出现崩溃,原因是对象进行了C++的默认拷贝构造,复制情况如下图所示,先析构 s2(对象构造与析构的关系是栈数据结构中的入栈和出栈的关系,所以对象析构的顺序与对象创建的顺序正好相反),将存有hello的空间先行释放,轮到 s1析构时,hello已经不存在了,因此会出现了崩溃。在这种情况下只能自己写一个拷贝构造函数。

例如

class String
{

public:
	String(const char* str = "jack")
	{
		_str = (char*)malloc(strlen(str) + 1);
		strcpy(_str, str);
	}

	String(const String& s)                         //自定义拷贝函数
	{
		_str = (char*)malloc(strlen(s._str) + 1);
		strcpy(_str, s._str);
	}

	~String()
	{
		free(_str);
	}

public:
	char* _str;
};


int main()
{
	String s1("hello");
	String s2(s1);
	cout << s2._str<< endl;
}

输出结果

hello

上面的现象就涉及到一个知识点就是浅拷贝和深拷贝。

5.浅拷贝和深拷贝

5.1 浅拷贝和深拷贝的定义

浅拷贝:简单的赋值拷贝 *** 作。

深拷贝:在堆区重新申请空间,进行拷贝 *** 作。

现在假设创建一个旧类,再把旧类拷贝给新类来创建新类。系统默认拷贝函数自定义拷贝函数的区别就是,默认的拷贝函数对于自定义类型,是将自定义变量的地址赋给新类的自定义变量,因此新旧两个类的自定义变量都指向同一块堆空间,当程序运行结束自动调用析构函数时,同一块堆空间会被释放两次(新旧类都会释放一次),因此会发生重复释放空间的错误;而自定义拷贝函数是我们重新在堆上申请一块空间,然后将旧类的自定义变量的赋给新类的自定义变量,因此新旧两个类的自定义变量指向不同的堆空间,当程序运行结束自动调用析构函数时就不会发生重复释放空间的错误,他们各自释放各自的堆空间。

对于基本类型变量默认拷贝函数自定义拷贝函数都是在栈上重新开辟空间,然后传值给新类的基本类型变量,因此基本类型变量调用默认拷贝函数自定义拷贝函数都是没问题的。

总结:如果类中有在堆区开辟的的变量,则一定要自己写拷贝构造函数,防止浅拷贝带来的问题。

6. 补充语法讲解

6.1初始化列表

作用:C++提供了初始化列表语法,用来初始化属性

语法:构造函数() : 属性1(值1) ; 属性2(值2) . . .{} 

例如

class Person
{
	public:
		//传统的初始化 *** 作
		/*Person(int a, int b, int c)
		{
			m_A = a;
			m_B = b;
			m_C = c;
		}*/


		//使用初始化列表进行初始化,版本1(初始化数据写死无法更改)
		/*Person() :m_A(10), m_B(20), m_C(30)
		{
			
		}*/


		//使用初始化列表进行初始化,版本2(使用变量传值)
		Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
		{

		}


		int m_A;
		int m_B;
		int m_C;
};


int main()
{
	//传统的初始化 *** 作
	/*Person p(10, 20, 30);
	cout << p.m_A << " " << p.m_B << " " << p.m_C << " " << endl;*/

	//使用初始化列表进行初始化,版本1
	/*Person p;
	cout << p.m_A << " " << p.m_B << " " << p.m_C << " " << endl;*/


	//使用初始化列表进行初始化,版本2
	Person p(10, 20, 30);
	cout << p.m_A << " " << p.m_B << " " << p.m_C << " " << endl;

	return 0;
}

依次传统初始化、版本1、版本2,可以发现结果是一样的。

输出结果

10 20 30

6.2 类对象作为类成员

例如

//手机类
class Phone
{
public:
	Phone(string pname)
	{
		cout << "Phone构造函数调用" << endl;
		m_pname = pname;
	}

	~Phone()
	{
		cout << "Phone析构函数调用" << endl;
	}

	//手机名称
	string m_pname;
};


class Person
{
public:

	Person(string name, string pname):m_name(name), m_phone(pname)  //上面将的初始化列表
	{
		cout << "Person构造函数调用" << endl;
	}

	~Person()
	{
		cout << "Person析构函数调用" << endl;
	}


	//姓名
	string m_name;
	//手机
	Phone m_phone;
};


int main()
{
	Person p("张三", "华为");
	cout << p.m_name << " " << p.m_phone.m_pname << endl;

	return 0;
}

输出结果

Phone构造函数调用
Person构造函数调用
张三 华为
Person析构函数调用
Phone析构函数调用

从上面的输出结果就就可以知道,当类中包含类对象时,它的构造和析构函数的调用顺序。

6.3 类中的静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员

静态成员分为:

①静态成员变量

Ⅰ.所有对象共享同一份数据

Ⅱ.在编译阶段分配内存

Ⅲ.类内声明,类外初始化

②静态成员函数

Ⅰ.所有对象共享一个函数

Ⅱ.静态成员函数只能访问静态成员变量

①静态成员变量

//静态成员变量
class Person
{
public:

	static int m_A;           //类内声明

	//静态成员变量也是有访问权限的
private:
	static int m_B;           //类内声明

};

int Person::m_A = 100;        //类外初始化

int Person::m_B = 200;        //类外初始化


void test1()
{
	//1.所有对象都共享同一份数据
	//2.编译阶段就分配内存
	//3.类内声明,类外初始化 *** 作

	//演示如下
	
	//100
	Person p1;
	cout << p1.m_A << endl;         //输出p1.m_A

	//200
	Person p2;
	p2.m_A = 200;                   //给p2.m_A赋值
	cout << p1.m_A << endl;         //但依然用输出p1.m_A
}


void test2()
{
	//静态成员变量不属于某个对象上,所有对象都共享同一份数据
	//因此静态成员变量有两种访问方式

	//1.通过对象进行访问
	Person p1;
	cout << p1.m_A << endl;

	//2.由于m_A属于静态区,所有可以不创建对象直接通过类名进行访问
	cout << Person::m_A << endl;

}




/*void test3()
{
	cout << Person::m_B << endl;
}*/


int main()
{
	//test1();

	//test2();

	//test3();

	return 0;
}

输出结果依次为

test1();

100
200

test2();

100
100

test3();报错,无法访问私有成员。

②静态成员函数

//静态成员函数
//1.所有对象共享同一个函数
//2.静态成员函数只能访问静态成员变量

class Person
{
public:
	//静态成员函数
	static void func1()
	{
		m_A = 100;                               //静态成员函数可以访问静态成员变量
	  //m_B = 200;                               //静态成员函数不可以访问静态变量,此行代码会报错,因为无法区分到底是哪个对象的m_B
		cout << "static void func调用" << endl;
	}

	//静态成员变量
	static int m_A;    //类内声明
	//非静态成员变量
	int m_B;


	//静态成员函数也是有访问权限的
private:
	static void func2()
	{
		cout << "static void func2调用" << endl;
	}
};

int Person::m_A = 0;   //类外初始化

void test1()
{
	//1.通过对象访问;
	Person p;
	p.func1();

	//2.通过类名访问
	Person::func1();

	//3.类外无法调用私有静态函数
	//Person::func2();               //此行代码会报错,类外无法访问私有函数
}


int main()
{
	//静态成员函数的两种调用方式
	test1();

	return 0;
}

输出结果

test1

100

100

9.5 赋值运算符重载

9.5.1 运算符重载的概念

所谓重载,就是赋予新的含义。函数重载(Function Overloading)可以让一个函数名有多种功能,在不同情况下进行不同的 *** 作。运算符重载(Operator Overloading)也是一个道理,同一个运算符可以有不同的功能。

实际上,我们已经在不知不觉中使用了运算符重载。例如,+号可以对不同类型(int、float 等)的数据进行加法 *** 作;<<既是位移运算符,又可以配合 cout 向控制台输出数据。C++本身已经对这些运算符进行了重载。
C++ 也允许程序员自己重载运算符,这给我们带来了很大的便利。

9.5.2 加号运算符重载

9.5.2.1 加号运算符重载的作用

作用:实现两个自定义数据类型相加的运算

9.5.2.2 加号运算符的实现

//加号运算符重载
//1.通过成员函数重载 + 号
//2.通过全局函数重载 + 号


class Person
{
public:
	//1.通过成员函数重载 + 号
	/*Person operator+(Person& p)
	{
		Person temp;
		temp.m_A = p.m_A + p.m_A;
		temp.m_B = p.m_B + p.m_B;
		return temp;
	}*/

	int m_A;
	int m_B;
};


//2.通过全局函数重载 + 号
Person operator+(Person& p1, Person& p2)
{
	Person temp;
	temp.m_A = p1.m_A + p2.m_A;
	temp.m_B = p1.m_B + p2.m_B;
	return temp;
}


//3.通过全局函数重载 + 号,实现类与整型相加
Person operator+(Person& p1, int x)
{
	Person temp;
	temp.m_A = p1.m_A + x;
	temp.m_B = p1.m_B + x;
	return temp;
}


void test1()
{
	Person p1;
	p1.m_A = 10;
	p1.m_B = 10;

	Person p2;
	p2.m_A = 10;
	p2.m_B = 10;


	//1.成员函数调用重载运算符的本质Person p3=p1.operator+(p2);即p1调用自己的成员函数operator+,将p2传给p1
	//2.全局函数调用重载运算符的本质Person p3=operator+(p1,p2);即调用全局函数operator+,将p1,p2传给p3
	Person p3 = p1 + p2;      //等价于Person p3=p1.operator+(p2)或Person p3=operator+(p1,p2)

	//3.通过全局函数重载 + 号,实现类与整型相加
	Person p4 = p1 + 100;


	cout << p3.m_A << endl;
	cout << p3.m_B << endl;

	cout << p4.m_A << endl;
	cout << p4.m_B << endl;
}


int main()
{
	test1();
	return 0;
}

注意:对于内置的数据类型的表达式的运算符是不可能改变的。

9.5.3 左移运算符重载

9.5.3.1 左移运算符重载的作用

作用:可以输出自定义的类型

9.5.3.2 左移运算符的实现

//左移运算符重载
//1.利用全局函数来实现左移运算符重载(注意:成员函数无法实现左移运算符重载)

class Person
{
	friend ostream& operator <<(ostream& cout, Person& p);    //友元

public:
	Person(int a, int b)
	{
		m_A = a;
		m_B = b;
	}
private:
	int m_A;
	int m_B;
};

//2.实现左移运算符重载
ostream& operator <<(ostream& cout, Person& p)      //本质 operator<<(cout,p) 简化 cout<

输出结果

10 10

9.5.3 递增运算符重载

9.5.3.1 递增运算符的作用

作用:通过重载递增运算符,实现自己的整型数据

9.5.3.2 递增运算符重载的实现

//重载递增运算符
//自定义整型

class MyInteger
{
	friend ostream& operator << (ostream& cout, MyInteger myint);
public:
	MyInteger()
	{
		m_Num = 0;
	}


	//2.重载前置++运算符
	MyInteger& operator++()
	{
		//先进行++运算
		m_Num++;

		//再返回自身
		return *this;
	}


	//3.重载后置++运算符
	MyInteger operator++(int)    //其中int代表占位参数,只有这样才能与上面的operator++构成重载
	{
		//先记录当时结果
		MyInteger temp = *this;

		//后递增
		m_Num++;

		//最后将记录结果返回
		return temp;
	}

private:
	int m_Num;
};

//1.重载<<左移运算符
ostream& operator << (ostream & cout, MyInteger myint)
{
	cout << myint.m_Num;
	return cout;
}


void test1()
{
	MyInteger myint1;
	cout << myint1 << endl;          //1.1
	cout << ++myint1 << endl;        //1.2
}


void test2()
{
	MyInteger myint2;
	cout << myint2++ << endl;        //2.1
	cout << myint2 << endl;          //2.2
}


int main()
{
	test1();
	test2();
	return 0;
}

输出结果

0
1
0
1

9.5.4 赋值运算符重载

C++编译器至少给一个类添加4个函数

1.默认构造函数(无参,函数体为空)

2.默认析构函数(无参,函数体为空)

3.默认拷贝构造函数,对属性进行值拷贝

4.赋值运算符operator=对属性进行值拷贝

注意:如果类中有属性指向堆区,做赋值 *** 作时也会出现深浅拷贝

9.5.4.1 赋值运算符重载的实现

class Person
{
public:
	Person(int age)
	{
		m_age = new int(age);        //在堆区申请空间
	}
	int* m_age;

	~Person()
	{
		if (m_age != NULL)
		{
			delete m_age;
			m_age = NULL;
		}
	}

	//重载 赋值运算符                    //2.自定义重载赋值运算符
	Person& operator=(Person& p)
	{
		//编译器提供浅拷贝
		//m.age = p.m_age;

		//应该先判断是否属性在堆区,如果有先释放干净,然后再深拷贝
		if (m_age != NULL)
		{
			delete m_age;
			m_age = NULL;
		}

		//深拷贝
		m_age = new int(*p.m_age);
		return *this;
	}

};


void test1()
{
	Person p1(10);
	cout << "p1=" << *(p1.m_age) << endl;

	Person p2(20);
	cout << "p2=" << *(p2.m_age) << endl;

	//p2 = p1;                            //1.由于浅拷贝,程序运行结束时会报错
	//cout << *(p2.m_age) << endl;

	p2 = p1;                            //2.自定义重载赋值运算符后,程序运行结束时不会报错
	cout << "p2=" << *(p2.m_age) << endl;

	Person p3(30);
	cout << "p3=" << *(p3.m_age) << endl;

	p3 = p2 = p1;
	cout << "p3=" << *(p3.m_age) << endl;

}


int main()
{
	test1();
	return 0;
}

输出结果

p1=10
p2=20
p2=10
p3=30
p3=10

9.5.5 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比 *** 作

class Person
{
public:
	Person(string name, int age)
	{
		m_name = name;
		m_age = age;
	}

	//重载等号
	bool operator==(Person& p)
	{
		if (this->m_name == p.m_name && this->m_age == p.m_age)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	bool operator!=(Person& p)
	{
		if (this->m_name == p.m_name && this->m_age == p.m_age)
		{
			return false;
		}
		else
		{
			return true;
		}
	}


	string m_name;
	int m_age;
};

void test1()
{
	Person p1("Tom", 18);
	Person p2("Tom", 18);

	if (p1 == p2)
	{
		cout << "p1和p2是相等的!" << endl;
	}
	else
	{
		cout << "p1和p2是不相等的!" << endl;
	}
}

void test2()
{
	Person p1("Tom", 18);
	Person p2("Jack", 18);

	if (p1 == p2)
	{
		cout << "p1和p2是相等的!" << endl;
	}
	else
	{
		cout << "p1和p2是不相等的!" << endl;
	}
}

void test3()
{
	Person p1("Tom", 18);
	Person p2("Tom", 18);

	if (p1 != p2)
	{
		cout << "p1和p2是不相等的!" << endl;
	}
	else
	{
		cout << "p1和p2是相等的!" << endl;
	}
}

void test4()
{
	Person p1("Tom", 18);
	Person p2("Jack", 18);

	if (p1 != p2)
	{
		cout << "p1和p2是不相等的!" << endl;
	}
	else
	{
		cout << "p1和p2是相等的!" << endl;
	}
}

int main()
{
	test1();
	test2();
	test3();
	test4();
	return 0;
}

输出结果

p1和p2是相等的!
p1和p2是不相等的!
p1和p2是相等的!
p1和p2是不相等的!

9.5.6 函数调用运算符重载

1.函数调用运算符()也可以重载

2.由于重载后使用方法非常像函数的调用,因此称为仿函数

3.仿函数没有固定写法,非常灵活

//函数调用运算符重载

//1.函数调用运算重载_字符串函数重载
class MyPrint
{
public:
	//重载函数调用运算符
	void operator()(string test)
	{
		cout << test << endl;
	}
};


//1.测试打印函数
void test1()
{
	MyPrint myprint;
	myprint("hello world");
}


//2.函数调用运算重载_字符串函数重载
class MyAdd
{
public:
	int operator()(int num1, int num2)
	{
		return num1 + num2;
	}
};


//2.测试相加函数
void test2()
{
	MyAdd myadd;
	int ret = myadd(10, 10);
	cout << ret << endl;
}


//3.匿名对象调用函数
void test3()
{
	cout << MyAdd()(100, 100) << endl;
}


int main()
{
	test1();
	test2();
	test3();
	return 0;
}

 

持续更新中。。。

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/web/1294705.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-06-10
下一篇2022-06-10

发表评论

登录后才能评论

评论列表(0条)