从0到1入门C++编程——04 类和对象之封装、构造函数、析构函数、this指针、友元

发布时间:2024年01月08日

一、封装

C++面向对象的三大特性为:封装、继承、多态。
C++认为万事万物皆为对象,对象都有其属性和行为。比如人作为对象,属性有姓名、年龄、身高、体重、性别等,行为有走、跑、跳等。
具有相同性质的对象,可以抽象称为类。
封装是C++面向对象的三大特性之一,封装的意义是将属性和行为作为一个整体表现对象或事务,同时将属性和行为加以权限控制。
类中的属性和行为统一称为成员。类中的属性称为成员属性或者成员变量,类中的行为称为成员函数或成员方法。
访问的权限有三种,公共权限(public)、保护权限(protected)和私有权限(private)。
公共权限类内可以访问,类外也可以访问;保护权限和私有权限类内可以访问,类外不可以访问。保护权限和私有权限的区别在继承时有所体现,子类可以访问父类保护权限下的属性和行为,但是子类不可以访问父类私有权限下的属性和行为。
类的默认权限是私有权限(private)。结构体的默认权限是公共。
下面代码就是一个创建类并实例化类的的例子。

#include <iostream>
using namespace std;
#define PI 3.14

//圆类
class Circle
{
	//访问权限
public:   //公共权限
	//属性  一般就是变量
	int radius;
	//行为  一般用函数来实现
	double perimeter()
	{
		return 2*PI*radius;
	}
	double area()
	{
		return PI*radius*radius;
	}
};

int main()
{
	//通过圆类创建一个具体的圆对象——实例化
	Circle c1,c2;
	//给圆对象的属性赋值
	c1.radius = 1;
	c2.radius = 10;
	cout<<"半径为"<<c1.radius<<"的圆的周长为:"<<c1.perimeter()<<endl;
	cout<<"半径为"<<c2.radius<<"的圆的面积为:"<<c2.area()<<endl;

	system("pause");
	return 0;
}

上面程序的运行结果如下图所示。
在这里插入图片描述
创建一个学生类,设置姓名、学号属性,显示学生信息的行为,姓名和学号可以由用户输入,示例如下。
在这里插入图片描述
在类中也可以通过行为为属性赋值。

#include <iostream>
#include <string>
using namespace std;

class Student
{
public:
	string name;
	string num;
	void show()
	{
		cout<<"姓名:"<<name<<" 学号:"<<num<<endl;
	}
	void set_name(string s_name,string s_num)
	{
		name = s_name;
		num = s_num;
	}
};

int main()
{
	Student s;
	s.set_name("Jones","20240105");
	cout<<"学生信息"<<endl;
	s.show();

	system("pause");
	return 0;
}

在设计类的时候,可以把属性和行为放在不同的权限下加以控制。
如果在主函数中访问保护权限或私有权限下的属性,代码中会直接报错。
在这里插入图片描述
将类中成员的属性设置为私有,然后通过在类中写一些公共权限的行为来控制读写权限。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	void setAttribute(string s,int a,int b)
	{
		name = s;
		age = a;
		password = b;
	}
	string getName()
	{
		return name;
	}
	int getAge()
	{
		return age;
	}
	int getPassword()
	{
		return password;
	}

private:
	string name;   //可读可写
	int age;
	int password;
};

int main()
{
	Person p;
	p.setAttribute("Jones",20,123456);
	cout<<"姓名:"<<p.getName()<<endl;
	cout<<"年龄:"<<p.getAge()<<endl;
	cout<<"密码:"<<p.getPassword()<<endl;

	system("pause");
	return 0;
}

虽然上面代码中的属性不能再main函数中直接访问修改,但是可以通过调用类中的公有方法来修改或者读取其属性。
立方体的案例
下面代码中设计了一个立方体类,然后通过成员函数求立方体的面积和体积,并分别利用全局函数和成员函数判断两个立方体是否相同。

#include <iostream>
#include <string>
using namespace std;

class Cube
{
public:
	void set_attr(double a,double b,double c)
	{
		c_l = a;
		c_w = b;
		c_h = c;
	}
	double get_length()
	{
		return c_l;
	}
	double get_wide()
	{
		return c_w;
	}
	double get_height()
	{
		return c_h;
	}
	double cube_area()
	{
		return 2*(c_l*c_w+c_l*c_h+c_w*c_h);
	}
	double cube_volume()
	{
		return c_l*c_w*c_h;
	}
	//利用全局函数判断两个立方体是否相同
	bool isSame(Cube &a)  //成员函数可以与全局函数同名
	{
		if(a.get_length()== c_l && a.get_wide()==c_w && a.get_height()==c_h)
			return true;
		return false;
	}
private:
	double c_l;
	double c_w;
	double c_h;
};

//利用全局函数判断两个立方体是否相同
bool isSame(Cube &a,Cube &b)
{
	if(a.get_length()==b.get_length() && a.get_wide()==b.get_wide() && a.get_height()==b.get_height())
		return true;
	return false;
}

int main()
{
	Cube c1,c2;
	c1.set_attr(10,10,10);
	cout<<"立方体的面积:"<<c1.cube_area()<<endl;
	cout<<"立方体的体积:"<<c1.cube_volume()<<endl;
	c2.set_attr(10,10,10);
	//利用全局函数判断
	if(isSame(c1,c2))
		cout<<"1.两个立方体相同!"<<endl;
	else
		cout<<"1.两个立方体不相同!"<<endl;
	//利用成员方法判断
	if(c1.isSame(c2))
		cout<<"2.两个立方体相同!"<<endl;
	else
		cout<<"2.两个立方体不相同!"<<endl;

	system("pause");
	return 0;
}

程序运行结果如下图所示。
在这里插入图片描述
判断点和圆关系的案例 ——在类中可以让另一个类作为本类的成员。
点和圆的关系有三种:点在圆外、点在圆上、点在圆内。
下面代码完成的就是点和圆位置关系的判断。

#include <iostream>
#include <string>
using namespace std;

class Point
{
public:
	//设置坐标
	void set_xy(int a,int b)
	{
		x = a;
		y = b;
	}
	int get_x()
	{
		return x;
	}
	int get_y()
	{
		return y;
	}
private:
	int x;
	int y;
};

class Circle
{
public:
	//设置半径
	void set_r(int a)
	{
		radius = a;
	}
	int get_r()
	{
		return radius;
	}
	//设置圆心
	void set_center(Point p)
	{
		center = p;
	}
	Point get_center()
	{
		return center;
	}
private:
	int radius;  //半径
	Point center;  //圆心
};

//判断点与圆的关系
void point_circle(Circle &c,Point &p)
{
	int x,y,r;
	r = c.get_r();
	x = p.get_x()-c.get_center().get_x();
	y = p.get_y()-c.get_center().get_y();
	if(x*x+y*y > r*r)
		cout<<"点在圆外!"<<endl;
	else if(x*x+y*y < r*r)
		cout<<"点在圆内!"<<endl;
	else
		cout<<"点在圆上!"<<endl;
}

int main()
{
	Point p1,p2;
	p1.set_xy(0,0);
	p2.set_xy(1,1);
	Circle c;
	c.set_center(p1);
	c.set_r(1);
	point_circle(c,p2);

	system("pause");
	return 0;
}

二、项目文件拆分

项目文件拆分的作用可以使得功能更加清楚。
将上面判断点和圆关系的案例代码中的类都独立的写到头文件和各自的C++文件中实现。
项目中包含圆类和点类的头文件,源文件中有圆类方法的实现、点类方法的实现文件以及包含主函数的文件。
在这里插入图片描述
point.h文件中只做变量和函数的声明,不做函数实现。

#pragma once  //防止头文件重复包含
#include <iostream>
using namespace std;

//在头文件只做变量和函数的声明,不做函数实现
class Point
{
public:
	//设置坐标
	void set_xy(int a,int b);
	int get_x();
	int get_y();
private:
	int x;
	int y;
};

点类中成员函数的实现放在point.cpp文件中,不过要在每个函数前面加上作用域。

#include "point.h"   //包含对应的头文件

//在C++文件做成员函数实现
void Point::set_xy(int a,int b)  //需要告知成员函数的作用域 Point::
{
	x = a;
	y = b;
}
int Point::get_x()
{
	return x;
}
int Point::get_y()
{
	return y;
}

同样地,circle.h文件中只做变量和函数的声明,不做函数实现,且Circle类用到了Point类,需要将点类的头文件包含进来。

#pragma once  //防止头文件重复包含
#include <iostream>
#include "point.h"
using namespace std;

//在头文件只做变量和函数的声明,不做函数实现
class Circle
{
public:
	//设置半径
	void set_r(int a);
	int get_r();
	//设置圆心
	void set_center(Point p);
	Point get_center();
private:
	int radius;  //半径
	Point center;  //圆心
};

圆类中成员函数的实现放在circle.cpp文件中,同样要在每个函数前面加上作用域。

#include "circle.h"   //包含对应的头文件

//在C++文件做成员函数实现
//设置半径
void Circle::set_r(int a)  //需要告知成员函数的作用域 Circle::
{
	radius = a;
}
int Circle::get_r()
{
	return radius;
}
//设置圆心
void Circle::set_center(Point p)
{
	center = p;
}
Point Circle::get_center()
{
	return center;
}

main.cpp文件中需要包含上面的两个头文件,然后在主函数中进行函数调用即可完成相应的功能。

#include <iostream>
#include <string>
#include "point.h"
#include "circle.h"
using namespace std;

//判断点与圆的关系
void point_circle(Circle &c,Point &p)
{
	int x,y,r;
	r = c.get_r();
	x = p.get_x()-c.get_center().get_x();
	y = p.get_y()-c.get_center().get_y();
	if(x*x+y*y > r*r)
		cout<<"点在圆外!"<<endl;
	else if(x*x+y*y < r*r)
		cout<<"点在圆内!"<<endl;
	else
		cout<<"点在圆上!"<<endl;
}

int main()
{
	Point p1,p2;
	p1.set_xy(0,0);
	p2.set_xy(1,1);
	Circle c;
	c.set_center(p1);
	c.set_r(1);
	point_circle(c,p2);

	system("pause");
	return 0;
}

三、构造函数和析构函数

C++中的每个对象都会有初始设置以及对象销毁前的数据清理。
对象的初始化和清理是两个非常重要的安全问题。一个对象或者变量没有初始状态,对其使用后果是未知;使用完一个对象或者变量,没有及时清理的话,也会造成一定的安全问题。
C++利用了构造函数和析构函数分别解决初始化和清理问题,这两个函数将会被编译器自动调用,完成对象的初始化和清理工作。 对象的初始化和清理工作是编译器强制做的事情,如果我们不提供构造函数和析构函数,编译器会执行编译器提供的构造函数和析构函数,不过都是空实现。
构造函数:创建对象时为对象的成员属性赋值,由编译器自动调用,无须手动调用。
析构函数:对象销毁前系统自动调用,执行清理工作。
构造函数语法:类名(){}
构造函数没有返回值,也不用写void;函数名称与类名相同;构造函数可以有参数,可以发生函数重载;程序在调用对象的时候会自动调用构造函数,而且只调用一次,无须手动调用。
析构函数语法:~类名(){}
析构函数没有返回值,也不用写void;函数名称与类名相同,不过要在类名前面加上~符号;析构函数不可以有参数,因此不可以发生函数重载;程序在对象销毁前会自动调用析构函数,而且只调用一次,无须手动调用。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	//构造函数
	Person()
	{
		cout<<"Person构造函数调用!"<<endl;
	}
	//析构函数
	~Person()
	{
		cout<<"Person析构函数调用!"<<endl;
	}
};

void fun()
{
	Person p;  //存放在栈区,函数执行完以后会释放空间,因此会调用析构函数
}

int main()
{
	fun();

	system("pause");
	return 0;
}

上面代码运行后的结果如下图所示,虽然只是在函数中实例化了一个Person类对象,但是构造函数和析构函数都执行了。
在这里插入图片描述
如果不是通过函数实例化对象,而是将函数中的代码直接放在主函数中,析构函数输出的内容需要按下任意键后才会执行。

1.构造函数的分类及调用

构造函数按照参数的有无分为有参构造和无参构造,无参构造也称默认构造;按类型分为普通构造和拷贝构造。
构造函数的调用方式:括号法、显示法、隐式转换法。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	//构造函数
	Person()
	{
		cout<<"Person无参构造函数调用!"<<endl;
	}
	Person(int a)
	{
		age = a;
		cout<<"Person有参构造函数调用!"<<endl;
	}
	Person(const Person &p)  //不能改变原有类的属性,要用const
	{
		age = p.age;  //将参数中的所有属性加以拷贝
		cout<<"Person拷贝构造函数调用!"<<endl;
	}
	//析构函数
	~Person()
	{
		cout<<"Person析构函数调用!"<<endl;
	}
public:
	int age;
};

void fun()
{
	//1.括号法调用构造函数
	Person p1;   //默认(无参)构造函数的调用 
	//Person p1();   不能实现无参构造函数的调用,编译器会认为这是函数声明
	Person p2(20);   //有参构造函数的调用
	Person p3(p2);   //拷贝构造函数的调用
	cout<<"p2年龄:"<<p2.age<<endl;
	cout<<"p3年龄:"<<p3.age<<endl;

	//2.显式法调用构造函数
	Person p4 = Person(20);   
	//Person(20)单独成行叫做匿名对象,执行该行后系统会回收掉匿名对象
	Person p5 = Person(p4);   //匿名对象前有等号,其就不是匿名对象了
	//Person(p4);   //不要利用拷贝函数初始化匿名对象,这行等价于 Person p2; 其已经定义过了

	//3.隐式法调用构造函数
	Person p6 = 20;   //编译器会将其转换为  Person p6 = Person(20);
	Person p7 = p6;
}

int main()
{
	fun();

	system("pause");
	return 0;
}

下面的构造函数调用方式不能实现无参构造函数的调用,编译器会认为这是函数声明。

Person p1();   

Person(20)单独在一行叫做匿名对象,执行完该行后系统会立即回收掉匿名对象。

Person(20); 

对于匿名对象,函数执行完匿名对象这行后,立即调用了析构函数回收清理,然后才执行后面的打印行。
在这里插入图片描述
不要利用拷贝函数初始化匿名对象,其效果相当于自动去掉括号,会发生重定义错误。

Person(p1);

2.拷贝函数调用时机

拷贝函数调用时机:使用一个已经创建完毕的对象来初始化一个新对象;值传递的方式给函数传值;值方式返回局部对象。
1.使用一个已经创建完毕的对象来初始化一个新对象。

Person p1(20);  //有参构造函数调用
Person p2(p1);  //拷贝构造函数调用
//拷贝完成后对一个对象属性的修改不会影响到另一个对象

2.值传递的方式给函数传值这种情况下,函数形参这里会进行拷贝函数的调用。

void fun(Person p)
{
	...
}

int main()
{
	Person p;
	fun(p);
	...
}

3.值方式返回局部对象

Person fun()
{
	Person p;
	return p;  //返回的局部变量是一份拷贝
}

int main()
{
	Person p = fun(p);
	...
}

3.构造函数调用规则

默认情况下,C++编译器至少给一个类添加3个函数:默认构造函数(空实现)、默认析构函数(空实现)、默认拷贝函数(值拷贝)。
构造函数调用规则:如果用户定义了有参构造函数,C++不再提供无参构造函数,但会提供默认拷贝函数;如果用户定义了拷贝构造函数,C++不会再提供其他的构造函数。
按照无参构造函数、有参构造函数、拷贝构造函数由前及后的顺序,若定义了前边的构造函数,后面的构造函数编译器仍然提供,若定义了后面的构造函数,前面的构造函数编译器便不再提供,需要使用的话就要自己定义实现。
下面的代码中没有写拷贝构造函数,但是在函数体中调用了拷贝构造函数。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	//构造函数
	Person()
	{
		cout<<"Person无参构造函数调用!"<<endl;
	}
	Person(int a)
	{
		age = a;
		cout<<"Person有参构造函数调用!"<<endl;
	}
	//析构函数
	~Person()
	{
		cout<<"Person析构函数调用!"<<endl;
	}
public:
	int age;
};

void fun()
{
	Person p1(20);  //有参构造函数调用
	Person p2(p1);  //拷贝构造函数调用
	cout<<"p1的年龄:"<<p1.age<<endl;
	cout<<"p2的年龄:"<<p2.age<<endl;
}

int main()
{
	fun();

	system("pause");
	return 0;
}

代码的执行结果如下图所示。
在这里插入图片描述

4.深拷贝与浅拷贝

浅拷贝就是简单的赋值拷贝操作。
深拷贝是在堆区重新申请空间进行拷贝的操作。
如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,防止浅拷贝带来的问题。
下面这段程序实现的就是浅拷贝,默认拷贝函数中的内容就是下面写的这样,将属性逐一复制。在析构函数中对构造函数中申请的堆区内存进行了释放,fun()函数中先后实例化了p1和p2两个对象,p2对象后入栈,所以先执行析构函数对堆区内存进行释放,这里不会出问题。当p1对象执行析构函数的时候,成员变量height的值是非空的,但是申请的堆区内存已经被p2释放掉了,这个时候程序就会出现问题。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	//构造函数
	Person()
	{
		cout<<"Person无参构造函数调用!"<<endl;
	}
	Person(int a,int b)
	{
		age = a;
		height = new int(b);
		cout<<"Person有参构造函数调用!"<<endl;
	}
	//拷贝构造函数
	Person(const Person &p)
	{
		//默认的拷贝构造函数将属性逐一复制
		age = p.age;
		//height = p.height;  //浅拷贝
		height = new int(*p.height);  //深拷贝
		cout<<"Person拷贝构造函数调用!"<<endl;
	}
	//析构函数
	~Person()
	{
		if(height != NULL)
		{
			delete height;
			height = NULL;
		}
		cout<<"Person析构函数调用!"<<endl;
	}
public:
	int age;
	int *height;
};

void fun()
{
	Person p1(20,175);  //有参构造函数调用
	cout<<"p1的年龄:"<<p1.age<<" p1的身高:"<<*p1.height<<endl;
	Person p2(p1);  //有参构造函数调用
	cout<<"p2的年龄:"<<p2.age<<" p2的身高:"<<*p2.height<<endl;
}

int main()
{
	fun();

	system("pause");
	return 0;
}

深拷贝的实现也很简单,把拷贝构造函数中涉及堆区内存变量拷贝的,独自申请一块堆区内存来接收,这样一来,虽然拷贝后指针指向的位置不再与原来的地址相同,但是里面存放的值是一样的。
对于浅拷贝而言,不写拷贝构造函数也是可以的,采用默认的拷贝构造函数,但是对于深拷贝来说,拷贝构造函数中需要重新申请堆区内存存放值。

Person(const Person &p)
{
	//默认的拷贝构造函数将属性逐一复制
	age = p.age;
	//height = p.height;  //浅拷贝
	height = new int(*p.height);  //深拷贝
	cout<<"Person拷贝构造函数调用!"<<endl;
}

改为深拷贝后,程序的运行结果如下图所示。
在这里插入图片描述

5.初始化列表

初始化列表赋值的语法:构造函数(): 属性1(值1),属性2(值2), …,属性n(值n){}
也可以在构造函数中加入参数,并将参数值一一赋给属性值,这样在赋值时更加灵活,比如:构造函数(int a, int b): 属性1(a),属性2(b){}

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	//传统方式赋值
	Person(int a,int b)
	{
		x = a;
		y = b;
	}
	//初始化列表方式赋值
	Person():x(10),y(20){}

public:
	int x;
	int y;
};

void fun()
{
	Person p1;  //初始化列表方式赋值
	Person p2(11,22);  //传统方式赋值
	cout<<"1.a="<<p1.x<<" b="<<p1.y<<endl;
	cout<<"2.a="<<p2.x<<" b="<<p2.y<<endl;
}

int main()
{
	fun();

	system("pause");
	return 0;
}

6.类对象作为类成员

C++类中的成员可以是另一个类的对象,称该成员为对象成员。
当其他类对象作为本类的成员时,构造的时候先构造类对象,然后再构造自身。
比如下面的例子,B类中有对象A作为成员,A为对象成员。构造时先执行A的构造函数,再执行B的构造函数。

class A
{
	...
}
class B
{
	A a;
}

构造时先构造类对象,再构造自身;析构的时候先析构自身,再析构类对象。

#include <iostream>
#include <string>
using namespace std;

class Phone
{
public:
	Phone(string s)
	{
		name = s;
		cout<<"Phone的构造函数!"<<endl;
	}
	~Phone()
	{
		cout<<"Phone的析构函数!"<<endl;
	}
	string name;
};

class Person
{
public:
	Person(string s1,string s2):name(s1),brand(s2)
	{
		cout<<"Person的构造函数!"<<endl;
	}
	~Person()
	{
		cout<<"Person的析构函数!"<<endl;
	}

public:
	string name;
	Phone brand;
};

void fun()
{
	Person p("Jones","iphone"); 
	cout <<p.name<<"手机牌子是"<<p.brand.name<<endl;
}

int main()
{
	fun();

	system("pause");
	return 0;
}

上面代码执行后的结果如下图所示。
在这里插入图片描述

7.静态成员

静态成员就是在成员变量或者成员函数前加关键字static。静态成员分为静态成员变量和静态成员函数。
静态成员变量特点:所有对象共享同一份数据,一个对象修改值后,其他对象访问到的就是修改后的新值;在编译阶段分配内存;静态成员变量在类内声明,类外初始化,初始化的时候要加作用域。
静态成员变量可以通过类名直接访问,其也有访问权限。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	static int a;  //类内声明
};

int Person::a = 10;  //类外初始化,要加作用域,否则就是全局变量

void fun()
{
	Person p1; 
	cout <<"1.a="<<p1.a<<endl;
	Person p2; 
	p2.a = 20;
	cout <<"2.a="<<p1.a<<endl;  //所有对象共享一份数据
	cout <<"3.a="<<Person::a<<endl;  //通过类名直接访问静态变量
}

int main()
{
	fun();

	system("pause");
	return 0;
}

上面代码运行后的结果如下图所示。
在这里插入图片描述
静态成员函数特点:所有对象共享同一个函数;静态成员函数只能访问静态成员变量,不可以访问非静态成员变量,非静态成员变量必须与特定的对象相对。
静态成员函数可以通过对象访问,也可以通过类名访问。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	static int a;  //类内声明
	static void fun()
	{
		a = 100;
		cout <<"静态成员函数的调用!"<<endl;
	}
};

int Person::a = 10;  //类外初始化,要加作用域,否则就是全局变量

int main()
{
	Person p;
	cout <<"1.a="<<Person::a<<endl;
	p.fun();
	cout <<"2.a="<<Person::a<<endl;
	Person::fun();

	system("pause");
	return 0;
}

上面代码运行后的结果如下图所示。
在这里插入图片描述


四、C++对象模型和this指针

1.类的对象大小计算

在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。
空对象占用的内存空间为1个字节大小。
C++编译器会为每个空对象分配一个字节的空间,这是为了区分不同对象占用内存的位置,比如声明两个空对象,不能让它们指向同一块内存空间,每个对象应该有一块独一无二的空间,即使它是空的。
在这里插入图片描述
只有非静态成员变量的大小才算在类的对象上,静态变量、静态函数、函数都不算字节空间。
在这里插入图片描述
对象也存在字节对齐。
在这里插入图片描述

2.this指针

C++提供特殊的对象指针——this指针,this指针指向被调用的成员函数所属的对象
this指针是隐含在每一个非静态成员函数内的一种指针,其不需要定义,直接使用。
this指针的用途:当形参和成员变量同名的时候,可以用this指针来进行区分,解决名称冲突;在类的非静态成员函数中返回对象本身,可以使用return *this。
this指针解决名称冲突的例子如下图所示。
在这里插入图片描述
在类的非静态成员函数中返回对象本身的例子如下。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	Person(int age)
	{
		this->age = age;
	}
	Person& Addage(Person &p)  //以引用的方式返回
	{
		this->age += p.age;
		return *this;  //返回对象本身
	}
	int age;
};

int main()
{
	Person p1(10);
	Person p2(20);
	p2.Addage(p1).Addage(p1).Addage(p1);  //链式编程的思想
	cout <<"age="<<p1.age<<endl;
	cout <<"age="<<p2.age<<endl;

	system("pause");
	return 0;
}

因为成员函数返回的是对象本身,因此可以连续调用成员函数,这就是链式编程思想的体现。
上面代码的运行结果如下图所示。
在这里插入图片描述

3.空指针访问成员函数

空指针访问成员变量的时候就会报错,因为类中成员变量前默认有一个this指针,如果指针是空的,自然是访问不到,因此为了程序的健壮性,应当在访问之前先判断所传的指针是否为空再进行下一步操作。

#include <iostream>
#include <string>
using namespace std;

class Person
{
public:
	void show_name()
	{
		cout <<"class name : Person"<<endl;
	}
	void show_age()
	{
		if(this == NULL)
			return;
		cout <<"age="<<age<<endl; //与下一行代码是一样的
		//cout <<"age="<<this->age<<endl;
	}
	int age;
};

int main()
{
	Person *p = NULL;
	p->show_name();
	p->show_age();

	system("pause");
	return 0;
}

4.const修饰成员函数

成员函数后加const称为常函数,常函数内不可以修改成员属性。 成员属性声明时加关键字mutable后,在常函数中仍然可以修改。
声明对象前加const称为常对象,常对象只能调用常函数. 因为普通函数中有可能对成员属性作修改,如果调用相当于变向的修改了属性。成员属性声明时加关键字mutable后,通过常对象访问可以修改。

class Person
{
public:
	//this指针的本质是指针常量,指针的指向不可以修改,但是值是可以修改的
	//this指针相当于Person * const this
	void show_age1()
	{
		this->age = 10;   //可以修改
	}
	//此时this指针再被const修饰,相当于const Person * const this,指针的指向和值都不可以被修改
	void show_age2() const
	{
		this->age = 20;   //不可以修改
		m_age = 20;   //可以修改
	}
	int age;
	mutable int m_age;
};

const Person p;
p.age = 10;   //不可以修改
p.m_age = 20;  //可以修改

五、友元

友元的目的是让一个函数或类访问另一个类中的私有成员(private)。
友元的关键字是friend
友元的实现:全局函数做友元;类做友元;成员函数做友元。

1.全局函数做友元

全局函数做友元的代码如下。

#include <iostream>
#include <string>
using namespace std;

class Room
{
	//全局函数是友元,可以访问类中的私有成员
	friend void fun(Room &r); 
public:
	Room()
	{
		sitting_room = "sitting room";
		bedroom = "bedroom";
	}
public:
	string sitting_room;
private:
	string bedroom;
};

//全局函数
void fun(Room &r)
{
	cout<<"Room name:"<<r.sitting_room<<endl;
	cout<<"Room name:"<<r.bedroom<<endl;  //全局函数以友元的方式才能访问类中私有成员
}

int main()
{
	Room room;
	fun(room);

	system("pause");
	return 0;
}

2.类做友元

可以将类中的成员函数写在类外,不过要在其前面加上作用域,而且在类中要先声明该函数。
类做友元的代码如下。

#include <iostream>
#include <string>
using namespace std;

class Room;
class Person
{
public:
	Person();
	void visit();
private:
	Room *room;
};

class Room
{
	//以类做友元
	friend class Person;
public:
	Room();
	string sitting_room;
private:
	string bedroom;
};

//类外写构造函数,需要说明类作用域
Room::Room()
{
	sitting_room = "sitting room";
	bedroom = "bedroom";
}

Person::Person()
{
	//在堆上开辟内存
	room = new Room;
}

void Person::visit()
{
	cout<<"Room name:"<<room->sitting_room<<endl;
	cout<<"Room name:"<<room->bedroom<<endl;
}

int main()
{
	Person p;
	p.visit();

	system("pause");
	return 0;
}

3.成员函数做友元

成员函数做友元的代码如下。

#include <iostream>
#include <string>
using namespace std;

class Room;
class Person
{
public:
	Person();
	void visit();
private:
	Room *room;
};

class Room
{
	//以类中成员函数做友元
	friend void Person::visit();
public:
	Room();
	string sitting_room;
private:
	string bedroom;
};

//类外写构造函数,需要说明类作用域
Room::Room()
{
	sitting_room = "sitting room";
	bedroom = "bedroom";
}

Person::Person()
{
	//在堆上开辟内存
	room = new Room;
}

void Person::visit()
{
	cout<<"Room name:"<<room->sitting_room<<endl;
	cout<<"Room name:"<<room->bedroom<<endl;
}

int main()
{
	Person p;
	p.visit();

	system("pause");
	return 0;
}

上面三个程序的运行结果相同,如下图所示。
在这里插入图片描述


本文参考视频:
黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难

文章来源:https://blog.csdn.net/weixin_42570192/article/details/135397760
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。