C++ 类和对象

1. 概述

C++ 面向对象的三大特性为 : 封装, 继承, 多态.

万事万物均可作为对象, 每个对象都有其属性和行为.

对于某些具有 相同属性相同行为 的对象, 可以抽象为 .

比如所有的圆形都属于圆类, 人都属于人类.

2. 封装

2.1 封装的意义

封装的意义在于 :

将 属性 和 行为 作为一个整体, 用于表现生活中的事物 ;

并将属性和行为加以权限控制.

圆形都有 半径, 直径, 周长, 面积等属性, 通过半径属性和圆周率, 可以求得周长和面积.

求周长和面积的过程就称为 行为.  

类 的属性 就像结构体的属性, 类 的行为 就像函数.

类 的 属性 和 行为 , 统称为 类 的 成员.  

属性又被称为 成员属性 / 成员变量 ;

行为又被称为 成员函数 / 成员方法 .

语法:

//定义一个类
class 类名称
{
访问权限 :   //冒号
    属性
    行为
};   //记住末尾这个分号


//创建一个对象(也叫类的实例化、具象化)

类名称 对象名称;     //和结构体一样.


//访问属性

对象名称.属性名称 = 0;   //将该对象的该属性赋值为0

//访问行为 

对象名称.行为函数();     //特指行为函数为无参无返类函数时

示例1 : 封装圆类, 给出半径, 求圆的周长.

示例2 : 封装学生类, 由用户对学生的 姓名, 年龄, 性别, 成绩 进行赋值, 然后打印.

#include <iostream>
using namespace std;

#include <string>

//将圆周率设为全局常量
const double PI = 3.141592653589;

//创建一个类
class Circle
{
	//访问权限
public:    //公共权限

	//属性
	double radius;  //半径

	//行为(函数)
	double perimeter()
	{
		return 2 * radius * PI;  //返回周长的值
	}
};   //注意类结束后需要分号, 和结构体一样

class Student
{
public:
	string name;
	int age;
	string sex;
	double score;

	void allocate()
	{
		cout << "请输入学生姓名: " << endl;
		getline(cin, name);

		cout << "请输入学生年龄: " << endl;
		cin >> age;

		cout << "请输入学生性别: " << endl;
		cin.ignore();            //输入字符串前需要把用户之前输入的换行符忽略掉
		getline(cin, sex);

		cout << "请输入学生成绩: " << endl;
		cin >> score;            //直接读取数字, 所以不需要忽略换行符
	}

	void printInfo()
	{
		cout << "学生姓名为: " << name << endl;
		cout << "学生年龄为: " << age << endl;
		cout << "学生性别为: " << sex << endl;
		cout << "学生成绩为: " << score << endl;
	}
};

int main()
{
	//创建一个圆类对象 (实例化 / 具象化)
	Circle c1;  
	c1.radius = 5;
	cout << c1.perimeter() << endl;   //相当于调用函数, 但这个函数不在全局区中, 而在类中

	Student s1;
	s1.allocate();
	s1.printInfo();
	return 0;
}

其实就是将 函数 封装到 结构体 里面.

2.2 封装权限

类 在封装时, 可以将属性和行为放在 不同的权限 下, 加以控制.

访问权限分为以下三种 :

public 公共权限, 类内可以访问, 类外也可以访问

protected 保护权限, 类内可以访问, 但类外无法访问

private 私有权限, 类内可以访问, 但类外无法访问

其中 protected 和 private 的区别, 在后面学习 继承 的时候会体现.

简单的说, 子类 可以访问 父类 的 protected , 但是无法访问 父类 的 private.

class Person
{
public:
	string name;
	void function()          //注意, 函数本身也在 public 权限下
	{
		name = "张三";       
		car = "拖拉机";      //类内, 可以访问
		password = 123456;   //类内, 可以访问
	}

protected:
	string car;

private:
	int password;

};

int main()
{
	Person p1;
	p1.name = "李四";        //public 权限, 类外可以访问
	//p1.car = "奔驰";       //报错, protected 权限, 类外不能访问
	//p1.password = 654321;  //报错, private 权限, 类外不能访问
	p1.function();           //但是函数本身是 public 权限, 因此可以访问.
	return 0;
}

2.3 struct 和 class 的区别

在 C++ 中, struct 和 class 唯一的区别就在于 默认访问权限不同.

struct 默认权限为 公共权限.

class 默认权限为 私有权限.

所以上面的代码经常用到 public , 因为在 class 中, 没有写权限的属性默认为 私有权限.

2.4 读写权限

在 C++ 中, 可以通过 将成员属性设置为私有 的方式, 划分读写权限.

已知 私有权限 无法在类外访问, 所以还需要写 公共权限 的接口函数.

这样 main 函数 无法直接访问成员属性, 必须通过 公共权限 的接口函数来操作属性.

如此就可以根据功能需求写函数, 划分读写权限.

同时, 可以在接口函数中加入条件判断, 检查 写入的合法性.

class Permissions
{
//将成员属性全部设为私有权限, 方便读写权限设置
private:
	string m_name;       //需要将姓名设置为可读可写
	int m_age;           //需要将年龄设置为只读
	string m_secret;     //需要将秘密设置为只写

public:                //公共权限接口
	//姓名可写
	void setName(string name)
	{
		m_name = name;   //将调用函数时传入的姓名作为成员姓名, 因为这个操作在类内实现,所以合法
	}
	//姓名可读
	string getName()
	{
		return m_name;   //直接返回成员姓名作为函数表达式的值, 因为这个操作在类内实现,所以合法
	}
	//年龄只读
	int getAge()
	{
		m_age = 0;       //写年龄封装在行为函数中, main 无法修改
		return m_age;    //只留下了 读取年龄 的公共函数接口, main函数中无法直接访问 m_name, 也没有对应函数接口写年龄.
	}
	//秘密只写
	void setSecret(string secret)  //读写权限的另一个作用, 检查 写入内容 的合法性
	{
		if (secret == "非法")     //因为这是函数, 所以可以加入条件判断语句,来检查 main 或者 用户 写入的内容
		{
			cout << "您违法了!" << endl;
			return;               //写入非法的时候, 警告并退出函数
		}
		m_secret = secret;        //写入合法的时候, 才进行写入
	}
};


int main()
{
	Permissions pm1;

	pm1.setName("张三");    //写姓名
	cout << pm1.getName() << endl;  //读姓名

	cout << pm1.getAge() << endl;  //读年龄

	pm1.setSecret("非法");   //非法写入,被警告,写入失败
	pm1.setSecret("合法");   //合法写入
	return 0;
}

2.5 封装设计案例

2.5.1 立方体

需求: 设计立方体类, 求立方体面积和体积, 分别通过 全局函 数和 成员函数 判断两个立方体的长宽高是否完全相等.

源代码:

#include <iostream>
using namespace std;

//立方体类
class Cube
{
private:
	double m_length;
	double m_width;
	double m_hight;

public:
	void setLength(double length)
	{
		m_length = length;
		//cout << "立方体长已设置为" << m_length << endl;
	}

	double getLength()
	{
		//cout << "立方体的长为" << m_length << endl;
		return m_length;
	}

	void setWidth(double width)
	{
		m_width = width;
		//cout << "立方体宽已设置为" << m_width << endl;
	}

	double getWidth()
	{
		//cout << "立方体的宽为" << m_width << endl;
		return m_width;
	}
	void setHight(double hight)
	{
		m_hight = hight;
		//cout << "立方体高已设置为" << m_hight << endl;
	}

	double getHight()
	{
		//cout << "立方体的高为" << m_hight << endl;
		return m_hight;
	}

	bool isSameByClass(Cube & c)
	{
		if (m_length == c.getLength() && m_width == c.getWidth() && m_hight == c.getHight())
		{
			cout << "两个立方体的长宽高均相等!(成员函数)" << endl;
			return true;
		}
		cout << "两个立方体的长宽高并不完全相等!(成员函数)" << endl;
		return false;
	}

	double volume()
	{
		return m_length * m_width * m_hight;
	}

	double superficial_area()
	{
		return 2 * (m_length * m_width + m_length * m_hight + m_width * m_hight);
	}
};

//全局函数
bool isSame(Cube& c1, Cube& c2)
{
	if (c1.getLength() == c2.getLength() && c1.getWidth() == c2.getWidth() && c1.getHight() == c2.getHight())
	{
		cout << "两个立方体的长宽高均相等!(全局函数)" << endl;
		return true;
	}
	cout << "两个立方体的长宽高并不完全相等!(全局函数)" << endl;
	return false;
}

int main()
{
    Cube cube1;
	Cube cube2;
	
	cube1.setLength(10);
	cube1.setWidth(10);
	cube1.setHight(10);

	cout << "立方体 1 的体积为:" << cube1.volume() << endl;
	cout << "立方体 1 的表面积为:" << cube1.superficial_area() << endl;

	cube2.setLength(10);
	cube2.setWidth(10);
	cube2.setHight(11);

	cout << "立方体 2 的体积为:" << cube2.volume() << endl;
	cout << "立方体 2 的表面积为:" << cube2.superficial_area() << endl;

	bool b1 = cube1.isSameByClass(cube2);
	bool b2 = isSame(cube1, cube2);
	
	return 0;   
}

2.5.2 球和点

需求: 设计 圆类 和 点类, 分别用全局函数和成员函数判断点和圆的关系 (圆内 / 圆上 / 圆外)

进阶: 圆类 改为 球类

拓展:

C++中的平方、开方、绝对值怎么计算_cpp平方_赵大寳Note的博客-CSDN博客

c++ 如何开N次方?速解 - 知乎 (zhihu.com)

源代码:

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

//等下球类成员函数要用到点类, 所以先写点类
class Point
{
private:   //点的空间直角坐标
	double m_x;
	double m_y;
	double m_z;

public:   
	void set_x(double x)   //通过公共函数接口读写坐标
	{
		m_x = x;
	}

	double get_x()
	{
		return m_x;
	}

	void set_y(double y)
	{
		m_y = y;
	}

	double get_y()
	{
		return m_y;
	}

	void set_z(double z)
	{
		m_z = z;
	}

	double get_z()
	{
		return m_z;
	}
};

class Ball
{
private:
	double m_center_x;   //球心的空间直角坐标
	double m_center_y;
	double m_center_z; 
	double m_radius;     //球的半径
	 
public:
	void setCenter_x(double center_x)   //球心与点同理
	{
		m_center_x = center_x;
	}

	double getCenter_x()
	{
		return m_center_x;
	}

	void setCenter_y(double center_y)
	{
		m_center_y = center_y;
	}

	double getCenter_y()
	{
		return m_center_y;
	}

	void setCenter_z(double center_z)
	{
		m_center_z = center_z;
	}

	double getCenter_z()
	{
		return m_center_z;
	}

	void setRadius(double radius)     //半径读写
	{
		m_radius = radius;
	}

	double getRadius()
	{
		return m_radius;
	}

	void isInBallByClass(Point & p)    //成员函数判断球内外
	{
		//空间直角坐标系的点距离计算: 求出两点间x,y,z差值, 三个差值分别平方后相加, 所得和再开平方
		//函数 pow(x, 2) 代表平方x , pow(x, 1.0/2) 代表开平方x
		if (pow((pow(p.get_x() - m_center_x, 2) + pow(p.get_y() - m_center_y, 2) + pow(p.get_z() - m_center_z, 2)), 1.0 / 2) < m_radius)
		{
			cout << "点在球内(成员函数)" << endl;
		}
		else if (pow((pow(p.get_x() - m_center_x, 2) + pow(p.get_y() - m_center_y, 2) + pow(p.get_z() - m_center_z, 2)), 1.0 / 2) == m_radius)
		{
			cout << "点在球面上(成员函数)" << endl;
		}
		else
		{
			cout << "点在球外(成员函数)" << endl;
		}
	}
};

//全局函数判断球内外
void isInBall(Ball & b, Point& p)
{
	if (pow((pow(p.get_x() - b.getCenter_x(), 2) + pow(p.get_y() - b.getCenter_y(), 2) + pow(p.get_z() - b.getCenter_z(), 2)), 1.0 / 2) < b.getRadius())
	{
		cout << "点在球内(全局函数)" << endl;
	}
	else if (pow((pow(p.get_x() - b.getCenter_x(), 2) + pow(p.get_y() - b.getCenter_y(), 2) + pow(p.get_z() - b.getCenter_z(), 2)), 1.0 / 2) == b.getRadius())
	{
		cout << "点在球面上(全局函数)" << endl;
	}
	else
	{
		cout << "点在球外(全局函数)" << endl;
	}
}



int main()
{
    Ball ball;               //创建球类实体
	ball.setCenter_x(0);     //写入球心空间直角坐标, 半径
	ball.setCenter_y(0);
	ball.setCenter_z(0);
	ball.setRadius(1);

	Point p;                 //创建球类实体
	p.set_x(0.5);            //写入点的空间直角坐标
	p.set_y(0.5);
	p.set_z(0.5);

    ball.isInBallByClass(p); //成员函数判断球内外
    isInBall(ball, p);       //全局函数判断球内外
	 
	return 0;
}

写完才发现, 球心也是点, 干了好多重复工作, 艹 

类中是可以嵌套类的, 比如可以给 Ball 类 增加一个 Point 类型的成员

2.6 类的分文件编写

当代码量越来越大时, 若还将所有代码放在同一个文件中, 则不利于阅读和维护. 

C++ 函数_AusrEnder的博客-CSDN博客

此篇第六节简单介绍了函数的分文件编写.

本节主要介绍类的分文件编写.

补充一点, 可以在头文件开始 加上 

#pragma once

防止一个头文件被多次包含.

C/C++ 中的 #pragma once 作用是什么?_程序员编程指南的博客-CSDN博客

和函数分文件编写相同的是, 类的分文件编写也需要 至少 3 个文件.

头文件 :

#pragama once

#include <iostream>

using namespace std;

//类的声明
class Point
{
   private:
       double m_x;
       double m_y;
       double m_z;

  
   public:
       void set_x(double x);   //这里的函数声明就可以了
       void set_y(double y);
       void set_z(double z);
       
       double get_x():
       double get_y():
       double get_z():
};

如代码所示, 类的分文件编写需要写 类的声明.

类的声明包含 权限 / 属性 / 成员函数的声明.

源文件 (子函数) :

#include "头文件名称.h"

using namespace 类名;

//类的成员函数定义
//不需要写类了, 也不用写权限, 把各个行为函数的定义写出来就好了
void set_x(double x)
{
   m_x = x;
}

……

该源文件中不需要声明 类 和 类的成员 , 只需要写函数的具体实现.

函数的分文件编写是 可以不写命名空间 的.

但是 类 中的成员函数, 必须要写命名空间. 若函数分文件编写中, 存在子函数, 则也要写命名空间.

命名空间有两种写法.

第一种, using namespace 命名空间 ;   (暂时没搞懂,不要使用)

这就好比你没写 using namespace std; 的时候 要写很多 std:: 一样.

第二种, 函数名前加作用域名, 比如 

void Point::set_x(double x)
{
    m_x = x;
}

注意, 作用域名 位于 返回值类型之后, 函数名之前.

c++类的分文件编写规则_c++ class 分文件_milaiko的博客-CSDN博客

main 函数文件就不多说了.

#include <iostream>
using namespace std;

#include "头文件名.h"

3. 构造函数和析构函数

3.1 对象的初始化和清理

若一个变量没有初始化, 则对其使用时, 后果未知;

使用完一个对象或者变量, 没有及时清理, 也会造成一定的安全问题

C++ 利用构造函数和析构函数解决了上述两个问题, 这两个函数会被编译器自动调用, 完成对象的初始化和清理工作. 

对象的初始化和清理 是必须要做的工作, 如果程序员不提供构造和析构, 编译器会自动提供.

编译器提供的构造函数和析构函数为空实现. (函数后的大括号中没有内容)

构造函数 : 创建对象时,为对象的成员属性赋值.

析构函数 : 销毁对象时, 执行清理工作.

3.2 语法

//构造函数
类名 {}

class Person 
{
   Person()   //构造
   ~Person()  //析构
};

注意, 构造函数与析构函数 没有返回值, 连 void 也不需要写.

          构造函数名称与类名相同, 析构函数则需要在类名前加 "~" (波浪号)

          构造函数可以有参数, 可以发生重载. 析构函数不允许有参数, 因此无法重载.

          构造函数在程序调用对象时自动调用, 无需手动调用, 且只会调用一次

          析构函数在程序销毁对象前自动调用, 无需手动调用, 且只会调用一次

构造与析构需要写到 类 里面, 而且必须在 public 权限下.(若不写 public , 默认权限为 private )

完成 类定义 后, 需要实例化一个对象, 才能调用构造与析构.

下面是调用构造与析构的详细代码.

#include <iostream>
using namespace std;

class Person
{
public:
	Person()
	{
		cout << "构造函数的调用" << endl;
	}

	~Person()
	{
		cout << "析构函数的调用" << endl;
	}
};
void test01()
{
	Person p;    //创建一个类的对象, 才能调用构造与析构
	             //对象 p 在函数中, 为局部变量, 存放在栈区.
	             //调用 test01 时会调用构造函数, test01 结束前会调用析构函数.
}


int main()
{
	test01();    //这一行中就发生了构造和析构的调用.
	             //如果在 main 中创建 p , 则 main 结束前都不会调用析构,但程序结束的瞬间可以看到析构 "一闪而过"
	return 0;
}

3.3 构造函数的分类和调用

3.3.1 构造函数的分类

构造函数有两种分类方式 :

按参数分 : 有参构造 和 无参构造(默认构造)   (有参数和无参数)

按类型分 : 普通构造 和 拷贝构造   (若不是拷贝构造, 就是普通构造)

无参构造又称为默认构造, 若程序员不提供构造函数, 则编译器自动调用无参构造作为默认构造.

有参构造的注意事项:

使用有参构造时, 编译器认为已经存在构造函数, 就不再提供默认构造. 这会使无参调用报错.

class Person
{
public:
	Person(int a)   //写了构造函数之后, 编译器就不再提供默认的无参构造
	{
		cout << a << endl;
		cout << "有参构造函数的调用" << endl;
	}
	~Person()
	{
		cout << "析构函数的调用" << endl;
	}
};

void test01()
{
	Person p(11);    //创建对象时会调用构造, 一定要给参数

  //Person p;        //因为构造函数是有参函数, 这里又没给参数, 所以就会报错
}


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

当然也可以使用 带默认参数 的有参构造来解决这个问题.

但是不要同时使用 默认构造 和 全默认参数的有参构造.

否则, 若你调用时没有给参数, 编译器不知道该调用哪个构造函数.

class Person
{
public:
	Person(int a = 10)   //写了默认参数, 调用的时候就可以不写参数了                
	{
		cout << a << endl;
		cout << "有参构造函数的调用" << endl;
	}
	~Person()
	{
		cout << "析构函数的调用" << endl;
	}
};

void test01()
{
    Person p;      //现在就不会报错了, 正常构造析构
}


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

拷贝构造函数的注意事项:

拷贝构造函数相当于 将另一个对象的成员属性拿到当前构造的对象中来

这一行为的前提是 被拷贝者 不能发生改变, 所以最好 const 同时修饰 变量 和 指针

#include <iostream>
using namespace std;

class Person
{
public:

	int age;    //注意这里新加了一个age成员

	Person()
	{
		cout << "无参构造函数的调用" << endl;
	}
 
	Person(int a  ) 
	{
		age = a;
		cout << a <<" " ;
		cout << "有参构造函数的调用" << endl;
	}

	Person(const Person& p)   //相当于 cosnt Person * const p
	{
		age = p.age;          //将引用对象的成员 拷贝至 正在构造的对象的成员
		cout << "age = :" << age << endl;
		cout << " 拷贝构造函数的调用" << endl;
	}

	~Person()
	{
		cout << "析构函数的调用" << endl;
	}
};
void test01()
{
	Person p1;    //创建一个类的对象, 才能调用构造与析构
	              //对象 p 在函数中, 为局部变量, 存放在栈区.
	              //调用 test01 时会调用构造函数, test01 结束前会调用析构函数.
	Person p2(11);//传入11 , 调用有参构造
	Person p3(p2);//传入p2 , 调用拷贝构造
}


int main()
{
	test01();    //这一行中就发生了构造和析构的调用.
	             //如果在 main 中创建 p , 则 main 结束前都不会调用析构,但程序结束的瞬间可以看到析构 "一闪而过"
	return 0;
}

3.3.2 构造函数的调用

构造函数有三种调用方式 :

括号法  ,  显示法  ,  隐式转换法

上面的代码就是括号法调用.

需要注意的是, 调用默认构造时, 不要写括号.

因为 Person p(); 这样的命令在编译器看来是 函数的声明,  Person 就是其返回值类型, 函数无参.

Person p ;    //正确地调用默认构造

显示法:

void test02()
{
	//显示法调用
	cout << "显示法" << endl;
	Person p4;                   //默认构造
	Person p5 = Person(12);      //有参构造
	Person p6 = Person(p5);      //拷贝构造 

	cout << "匿名对象" << endl;
	Person(10);                  //匿名对象,当前行执行结束后,系统立即回收
	cout << "回收验证" << endl;  //在输出回收验证之前,系统已经输出析构了

	//不要利用拷贝构造函数来初始化一个匿名对象
	//Person(p4);   编译器会忽略(),认为这是 p4 的默认构造, 或者说 对象 p4 的声明
}

显示法的右表达式是一个 匿名对象.

匿名对象的特点是, 匿名对象当前所在行执行完后, 就会被系统回收(析构)掉.

需要注意的是, 不要直接用拷贝构造初始化匿名对象(但拷贝构造的匿名对象作为右值是可以的).

因为 Person(p4);  在编译器看来, 是 对象 p4 的声明.(编译器忽略了括号)

Person p6 = Person(p5); 才是正确的显示法调用拷贝构造.

隐式转换法:

本质上是显示法的一种简写. 隐式转换法会被编译器自动转换为显示法.

void test03()
{
	//隐式转换法调用
	cout << "隐式转换法" << endl;

	//编译器会自动转换为显示法
	Person p7 = 13;      //有参构造
	Person p8 = p7;      //拷贝构造 
}

3.4 拷贝构造函数的使用时机

3.4.1 用已经创建完毕的对象初始化一个新对象

这个就不多说了, 上节的拷贝构造已经讲的很清楚了.

#include <iostream>
using namespace std;

class Person
{
public:
	int m_age;
	Person()
	{
		cout << "Person的无参构造调用" << endl;
	}

	Person(int age)
	{
		m_age = age;
		cout << "Person的有参构造调用" << endl;
	}

	Person(const Person &p)
	{
		m_age = p.m_age;
		cout << "Person的拷贝构造调用" << endl;
	}

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

void function1()
{
	Person p1(5);      //创建对象 p1, 调用有参构造 , p1.m_age == 5
 	Person p2(p1);     //创建对象 p2, 调用拷贝构造, 这就是利用已经创建完毕的对象 p1 来初始化 p2
}

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

3.4.2 利用值传递的方式, 给函数参数传值

其实这就是 函数-值传递 中所说的, 值传递会占用内存创建一个临时副本.

void doWork(Person p)  //将 Person 类数据 p 值传给函数 doWork, 值传递就是拷贝构造
//但是平时用拷贝构造最好加上 const 和引用
{

}

void function1()
{
	Person p1(5);      //创建对象 p1, 调用有参构造 , p1.m_age == 5
 	Person p2(p1);     //创建对象 p2, 调用拷贝构造, 这就是利用已经创建完毕的对象 p1 来初始化 p2
	doWork(p2);        //将 实参 p2 传递给 形参 p ,这就是利用值传递的方式给函数参数传值, 依然是调用拷贝构造
}

*3.4.3 利用返回值的方式, 返回局部对象 (已被优化)

就像返回值类型是 int 的函数, 可以用 return 的方式 返回一个 int 类型的值

当返回值类型是 类 类型 时, return 返回一个 类 类型的值.

Person doWork2()
{
	Person p1;         //调用默认构造
	cout << &p1 << endl;
	return p1;         //调用拷贝构造,但 C++ 11 中已经优化了这种拷贝构造
}

void function1()
{
	Person p3 = doWork2();
	cout << &p3 << endl;
}

有的教程中说, return p1 的时候会调用拷贝构造, 将这个临时副本返回.

所以 p1 和 p3 的地址会不同.

但经过本人实测, Visual Studio 2022 Community 中, 既没有调用拷贝构造, 且两个地址仍相同.

该技术已被 C++11 优化, 称为 copy elision (复制省略 / 复制消除)

浅谈C++11标准中的复制省略(copy elision,也叫RVO返回值优化)_利用复制省略提高性能_知行合一2018的博客-CSDN博客

该文章的结论是, 返回值优化彻底消除了拷贝构造的调用.

3.5 构造函数的调用规则

创建一个 类 时, 编译器都会自动提供至少 3 个函数

1. 默认构造 (无参, 空实现)

2.析构函数 (无参, 空实现)

3.拷贝构造 (对全部属性进行拷贝)

当程序员不写任何构造函数时, 编译器会自动提供上述三个函数.

当程序员定义了有参构造, 编译器就不再提供 默认构造, 但仍提供 拷贝构造;(详见3.3.1)

当程序员定义了拷贝构造, 编译器就仅提供析构函数.

class Person
{
public:
	int m_age;
	Person()
	{
		cout << "Person的无参构造调用" << endl;
	}

	Person(int age)
	{
		m_age = age;
		cout << "Person的有参构造调用" << endl;
	}

	//Person(const Person &p)      //拷贝构造被我注释掉了
	//{
	//	m_age = p.m_age;
	//	cout << "Person的拷贝构造调用" << endl;
	//}

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

void function1()
{
	Person p1(5);      
 	Person p2(p1);     
	cout << p2.m_age << endl;   //依然能输出5 , 说明程序默认提供了拷贝构造.
           

可以看到, 在我没有写拷贝构造的情况下, 依然能够调用拷贝构造, p2.m_age 就拷贝自 p1.

不同于自定义拷贝构造函数的 可以仅拷贝部分属性,  默认拷贝构造函数会拷贝所有属性.

3.6 浅拷贝与深拷贝

首先, 程序提供的默认拷贝构造函数为浅拷贝.

当 类 的成员包含指针变量时,  根据指针是否初始化可以分为以下两种情况 :

第一, 有初始化 (指向栈区)

class Person
{
public:
	int m_age;
	int a;
	int* m_height =&a;   //m_height 是一个有初始化(初始指向)的指针
	Person()
	{
		cout << "Person的无参构造调用" << endl;
	}

	Person(int age, int height)
	{
		m_age = age;
		*m_height = height;
		cout << "Person的有参构造调用" << endl;
	}
};

void function1()
{
	Person p1(5,160);  //创建对象 p1, 调用有参构造
 	Person p2(p1);     //创建对象 p2, 调用默认拷贝构造(因为这次我没写拷贝构造)
	cout << p2.m_age << " " << *p2.m_height << endl;  //访问指针指向的值需要解引用
}

第二种, 无初始化 (指向堆区)

int m_age;
int* m_height;

这样的代码虽然能够通过编译, 执行时也不会报错, 但是执行到一半就中止了.

因为在有参构造中, 程序不知道把 160 这个值放在哪里, 因为我们没有给 m_height 一个明确指向.

这时, 可以通过开辟堆区内存来解决这个问题 :

	Person(int age, int height)
	{
		m_age = age;
		m_height = new int(height);
		cout << "Person的有参构造调用" << endl;
	}

完整的写法应该是 int * m_height = new int(值); 但前面已经定义过 m_height, 这里就不重定义了.

这样程序就可以正常运行.

但是, 堆区的内存并没有回收 !

这时候就需要利用析构函数来释放堆区内存.

	~Person()
	{
		if(m_height != nullptr)
		{
			delete m_height;       //释放堆区内存
			m_height = nullptr;    //指针指向空处
		}
		cout << "Person的析构函数调用" << endl;
	}

有的教程中会让你写 m_height = NULL, 但 C++ 中 NULL 就是0, 所以空指针最好还是用 nullptr.

这时候编译没问题, 结果一运行就异常了.

这是因为, 浅拷贝只是简单粗暴地将 p1 的内容 复制到 p2 中

这导致 p1.m_height 和 p2.m_height 这两个指针指向同一处.

根据栈区 先进后出 的原则, 第一次执行析构函数,

会释放 p2.m_height 的堆区内存, 然后 p2.m_height 指向空处.

但是, p1.m_height 仍然指向堆区内存地址, 程序在析构 p1 的时候, p1.m_height 不是空指针,

if 条件仍然满足, 这会导致堆区内存被释放两次, 从而使程序崩溃

总的来说, 浅拷贝 会导致 堆区内存重复释放 问题.

那么这时候就需要用 深拷贝 来解决.

	Person(const Person &p)
	{
		m_age = p.m_age;
		//m_height = p.m_height 浅拷贝, 也是编译器默认拷贝会写的内容
		m_height = new int(*p.m_height); //深拷贝, 在堆区申请新内存, 把 p 中指针解引用得到的值作为新对象指向的值
		cout << "Person的拷贝构造调用" << endl;
	}

这样就不会报错了. 其实关键就在于, 在堆区申请新的内存.

C++ 浅复制、深复制详解_c++深复制_杨 戬的博客-CSDN博客

总结: 如果属性有在堆区开辟的, 一定要自己写拷贝函数, 对应的属性要写成深拷贝

3.7 初始化列表

就像函数有默认参数一样,  类 的成员也可以写初始化属性

而 类 的初始化属性要写在构造函数中.

#include <iostream>
using namespace std;

class Person
{
public:
	int m_A;
	int m_B;
	int m_C;

	Person()      //传统初始化方式
	{
		m_A = 10;
		m_B = 20;
		m_C = 30;
	}
};

void function()
{
	Person p1;
	cout << "m_A = :" << p1.m_A << endl;
	cout << "m_B = :" << p1.m_B << endl;
	cout << "m_C = :" << p1.m_C << endl;
}

int main()
{
	function();

	return 0;
}

但是这样写行数较多, C++ 中允许写初始化列表, 这种方式行数较少.

而初始化列表分为无参构造和有参构造两种. 通常来说, 有参构造更加灵活.

初始化列表的本质是 多行 隐式转换法.

但要注意, 一旦写了有参构造, 编译器就不再提供默认构造.

如果还需要调用无参构造, 请自己再写一个无参构造.

#include <iostream>
using namespace std;

class Person
{
public:
	int m_A;
	int m_B;
	int m_C;

	//Person()      //传统初始化方式
	//{
	//	m_A = 10;
	//	m_B = 20;
	//	m_C = 30;
	//}

	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)  //有参构造的初始化列表
	{

	}

	Person() :m_A(30), m_B(20), m_C(10)   //无参构造的初始化列表
	{

	}
};

void function()
{
	Person p1;                           //无参构造
	cout << "m_A = :" << p1.m_A << endl;
	cout << "m_B = :" << p1.m_B << endl;
	cout << "m_C = :" << p1.m_C << endl;

	Person p2(10,20,30);                 //有参构造
	cout << "m_A = :" << p2.m_A << endl;
	cout << "m_B = :" << p2.m_B << endl;
	cout << "m_C = :" << p2.m_C << endl;
}

int main()
{
	function();

	return 0;
}

4. 对象特性

4.1 类对象作为成员 (类的嵌套)

C++ 类中的对象可以作为另一个类中的成员. 这种成员称为 对象成员

(就是一个类中包含另一个类,作为自己的属性.)

class A
{
};

class B
{
   A a;
}

创建大类时, 优先调用小类的构造函数, 再调用大类的构造函数.

因为小类没有构造完, 大类就不是完整的.

但是根据栈区 先进后出 原则, 析构时先析构大类, 再析构小类.

这就像 人穿多件衣服 一样, 先穿里面的, 再套外面的. 先脱外面的, 再脱里面的.

4.2 静态成员

静态成员 是类中一种特殊的成员, 通过在普通成员和函数前加关键字 static, 就可以成为静态成员

静态成员分为:

1.静态成员变量

·  所有该类对象共享这个静态成员变量值, 若 p1 和 p2 同属于一类, 且 m_A 为静态成员变量, 则 p1.m_A 改变时 p2.m_A也改变.(本质是共享了内存空间)

·  静态成员变量在编译阶段就会被分配内存.

·  无论权限如何, 静态成员变量必须在类内声明一次, 然后在类外用作用域声明一次, 且必须要写到类的后面(最好顺便初始化), 否则无法正常使用. 类外要加作用域是为了编译器将其与全局变量区分开.

·  静态成员变量也是有访问权限的, 私有权限的静态成员变量在类外无法访问.

·  静态成员变量可以通过具体对象访问, 也可以通过类名进行访问.

2.静态成员函数

·  所有该类对象共享这个静态成员函数.

·  静态成员函数不需要在类外进行声明.

·  静态成员函数也是有访问权限的, 私有权限的静态成员函数在类外无法访问.

·  静态成员函数可以通过具体对象访问, 也可以通过类名进行访问.

·  静态成员函数只能访问静态成员变量, 但非静态成员函数可以访问静态成员变量.

#include <iostream>
using namespace std;

class Person
{
public:                   //静态成员变量也是有访问权限的
	static int m_A;
	int m_C;            //非静态成员变量
	static void function()    //静态成员函数也是有访问权限的
	{
		//cout << m_C << endl; //静态成员函数只能访问静态成员变量, 非静态成员变量无法访问
		cout << m_B << endl;   //类内可以访问私有权限
		cout << "static void funtion 的调用" << endl;
	}

	void function2()
	{
		cout << "m_A = :" << m_A << endl;    //但是非静态成员函数可以访问静态成员变量
	}

private:                 
	static int m_B;        //静态成员变量,但是在私有权限下
	static void staticfuntion()        
	{
		cout << "static void staticfuntion 的调用" << endl;
	}

}; 

//int Person::m_A;      //静态成员变量必须在类外声明一次,否则报错,最好经过初始化, 不初始化默认为 0
int Person::m_A = 100;  //必须要用类名写上作用域, 如 Person:: , 否则编译器认为是全局变量
                 
int Person::m_B = 10;   //即便是在类内调用私有权限的静态成员变量, 也需要在类外进行声明,否则报错
                       
                        //静态成员函数不需要在类外声明
						
//静态成员变量
void test01()
{
	//1.通过对象访问静态成员变量
	Person p1;
	cout << p1.m_A << endl;
	//cout << p1.m_B << endl;     //私有权限, 类外无法访问
	Person p2;
	p2.m_A = 200;
	cout << p1.m_A << endl;      //200, 因为静态成员变量的值,是该类所有对象共享的
	                             //改变对象 p1 中的某个静态成员变量, 则所有 Person 类对象下该变量都一起改变

	//2.通过类名访问静态成员变量
	cout << Person::m_A << endl; //200,因为静态成员变量是该类所有对象共享的, 所以也可以用类名进行访问

	p1.function2();
}

void test02()
{
	//1.通过对象访问静态成员函数
	Person p3;
	p3.function();
	//p3.staticfuntion();        //私有权限, 类外无法访问
	
	//2.通过类名访问静态成员函数
	Person::function();

}
int main()
{
	test01();
	test02();
	return 0;
}

4.3 成员变量和成员函数分开存储

在C++中, 类 的 成员变量 和 成员函数 是分开存储的.

并且, 静态成员不在 类 的对象上.

也就是说, 通过 sizeof 求对象占用的内存空间大小, 仅包括非静态成员变量.

//成员存储方式
class A
{

};

class B
{
	int m_A;
};

class C 
{
	void Cfunction()
	{

	}
	static int mc_A;
	static void staticCfunction()
	{

	}
};

int C::mc_A;


void test03()
{
	A a1; 
	A a2;
	cout << sizeof(a1) << endl;    //空对象固定占据 1 个字节内存
	cout << sizeof(a2) << endl;    //空对象固定占据 1 个字节内存
	cout << sizeof(A) << endl;    //空类固定占据 1 个字节内存

	B b1;
	B b2;
	cout << sizeof(b1) << endl;    //4,仅包含一个 int 变量
	cout << sizeof(b2) << endl;    //4,仅包含一个 int 变量
	cout << sizeof(B) << endl;    //4,仅包含一个 int 变量

	C c1;
	cout << sizeof(c1) << endl;    //1,因为成员函数无论静态与否都不存放在对象上
	                               //静态成员变量也不存放在对象上
	                               //所以 C 实际上是一个空类, 空类就占 1字节
}


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

4.4 this 指针

对于 类 中的 每个非静态成员函数, 都只会生成一个函数实例. 也就是说, 无论同类对象有多少个, 使用的都是同一段函数代码.

那么非静态成员函数是如何区分到底是哪个对象在调用它呢?

这就需要 对象指针 this 指针来解决.

this 指向被调用的成员函数所属的对象.

如 p1.function(), 则此时function()中的 this ==&p1  ,  且有  *this == p1.

this 指针是隐含于每个非静态成员函数中的一种指针. "隐含"指的是隐含定义.

因此在 非静态成员函数中 , this 指针无需经过定义就可以使用.

this 指针有以下两种用途:

4.4.1 解决命名冲突

若调用成员函数来为成员属性赋值, 通常需要一定的命名规范.

未规范命名容易引起命名冲突, 导致赋值无效. 如下

#include <iostream>
using namespace std;

class Person
{
public:
	int money;

	 Person(int money)
	{
		money = money;      //命名不规范, 形参值没有传递给成员属性
	}
};

void test01()
{
	Person p1(10);
	cout << "p1.money 的值为: " << p1.money << endl;
}

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

通常来说更推荐用规范命名来解决, 如 成员属性 添加前缀 "m_" (m意为 member)

class Person
{
public:
	int m_money;

	 Person(int money)
	{
		m_money = money;      //命名不规范, 形参值没有传递给成员属性
	}
};

void test01()
{
	Person p1(10);
	cout << "p1.money 的值为: " << p1.m_money << endl;
}

但是也可以通过 this 指针来解决, 前面已经说过, this 指向调用的对象本身.

由于 this 是指针, 所以需要 -> 运算符来指向. 当然, 也可以通过解引用的方式来解决.

class Person
{
public:
	//int m_money;            //规范命名
	int money;

	 Person(int money)       //有参构造
	{
	    //m_money = money;   //规范命名
		//money = money;       //命名不规范, 形参值没有传递给成员属性

		this->money = money;
		//(*this).money = money;    //与上一行等价

	}
};

4.4.2 返回对象本身 与 链式编程思想

this 的另一个用途是在 非静态成员函数 中返回对象本身.

若函数返回对象本身, 那么通过连续的" . " 就可以单行实现多次函数套用.

这就是 链式编程思想.

假如想创建一个 p2, 并让 p1 的 money 加在 p2 的身上. ( p1 的 money 不变.)

class Person
{
public:
	//int m_money;            //规范命名
	int money;

	 Person(int money)       //有参构造
	{
	    //m_money = money;   //规范命名
		//money = money;       //命名不规范, 形参值没有传递给成员属性

		this->money = money;
		//(*this).money = money;    //与上一行等价
	}

	 void personAddMoney(Person& p)  //将 p1 的 money 加到 p2 上
	 {
		 this->money += p.money;
	 }
};

void test01()
{
	//解决命名冲突
	Person p1(10);
	cout << "p1.money 的值为: " << p1.money << endl;

	//返回对象本身与链式编程思想
	Person p2(10);
	p2.personAddMoney(p1);
	cout << "p2.money 的值为: " << p2.money << endl;
}

这时, 如果想加多次, 能不能多次调用函数呢?

p2.personAddMoney(p1).personAddMoney(p1).personAddMoney(p1);   //不行, 返回值类型是 void

答案是不行, 因为这个函数的返回值类型是void.

那么将返回值类型修改为 Person , 并且 return *this  

class Person
{
public:
	//int m_money;            //规范命名
	int money;

	 Person(int money)       //有参构造
	{
	    //m_money = money;   //规范命名
		//money = money;       //命名不规范, 形参值没有传递给成员属性

		this->money = money;
		//(*this).money = money;    //与上一行等价
	}

	 Person personAddMoney(Person& p)  //将 p1 的 money 加到 p2 上
	 {
		 this->money += p.money;
		 return *this;
	 }
};

void test01()
{
	//解决命名冲突
	Person p1(10);
	cout << "p1.money 的值为: " << p1.money << endl;

	//返回对象本身与链式编程思想
	Person p2(10);
	//p2.personAddMoney(p1);

	p2.personAddMoney(p1).personAddMoney(p1).personAddMoney(p1);   
	cout << "p2.money 的值为: " << p2.money << endl;
}

结果是20, 不是我们想要的40.

来看看程序都做了什么 :

首先, p2.personAddMoney(p1) , 函数执行过程中, this 指向 p2, 所以 p2. money 加10.

但是在C++中, 以值的方式返回局部对象, 会调用拷贝构造函数, 最终返回的不是 p2 本身, 而是一个新的匿名对象.

p2.personAddMoney(p1)  这就是新的匿名对象

p2.personAddMoney(p1).personAddMoney(p1)
等于
新的匿名对象.personAddMoney(p1)

那么下一个 this 就会指向新的匿名对象, 然后 this 不断地指向更新的匿名对象, p2.money 无变化.

所以最后得到 p2.money 只加了最开始那一次 10.

为了解决这个问题, 只需要在返回值类型后加 &. 这表示引用返回.

	 Person& personAddMoney(Person& p)  //将 p1 的 money 加到 p2 上
		                                //一定要返回引用, 直接返回值会返回 p2 的拷贝副本, 导致下个指针不指向 p2
	 {
		 this->money += p.money;
		 return *this;
	 }
};

那么编译器就不会返回 p2 的副本, 而是切实地返回 p2 本身.

输出值也变成我们想要的 40 了. 

p2.personAddMoney(p1)  引用返回,返回的还是p2

p2.personAddMoney(p1).personAddMoney(p1)
等于
p2.personAddMoney(p1)

4.5 空指针访问成员函数

在 C++ 中, 允许使用 对象指针 中的 空指针 访问 成员函数.

比如 int * 代表指向整型数据的指针, 那么 类名 * 就代表指向 类 类型(对象)的指针, 或者叫对象指针.

空指针访问成员函数是有条件的 : 成员函数中不能用到 this 指针.

下面这段代码就有一处违反了条件 , 程序虽然可以编译运行, 但是main返回值不为 0 ,说明有异常.

#include <iostream>
using namespace std;

class Person
{
public:
	int m_age;

	void showClassName()
	{
		cout << "类名为: Person" << endl;
	}

	void showPersonAge()
	{
		cout << m_age << endl;
	}
};

void test01()
{
	Person* p = nullptr;

	p->showClassName();

	p->showPersonAge();
}

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

明明没有写 this 指针, 为什么还是违反规则了呢?

这是因为, 在非静态成员函数中, 自动对成员属性隐含了 this 指针.

	void showPersonAge()
	{
		//cout << m_age << endl;       //等于下面这行
        cout << this->m_age << endl;   //非静态成员函数中隐含 this 指针
	}

因为 p 本身就是一个空指针, 没有指向明确对象, 当然无法访问对象中的属性了.

但是 showClassName() 这个函数执行时没有用到成员属性, 也没有 this 指针, 所以没有违反规则.

这就是空指针访问成员函数.

解决 空指针与 this 冲突有以下几种方式 :

1. 前置判断(推荐)

    void showPersonAge()
	{
		if (this == nullptr)
		{
			return;
		}
		cout << m_age << endl;  //等价于 cout << this->m_age <<endl;
	}

检测到 this(也就是 p) 为空指针的时候, 自动退出这个函数.

因为返回值类型是 void , 所以 return 后面直接分号, 不能加其他东西.

2. 静态成员

静态成员函数中不允许存在 this 指针, 当然就不会冲突, 但静态成员函数只能使用静态成员变量

class Person
{
public:
	static int m_age;

	void showClassName()
	{
		cout << "类名为: Person" << endl;
	}

	static void showPersonAge()
	{
		cout << m_age << endl;  //等价于 cout << this->m_age <<endl;
	}
};

int Person::m_age;  //使用静态成员变量要在类外声明

3. 规范代码

注意函数中不要调用成员属性, 也不要调用 this 指针, 或者明确指针对象, 主动避免问题. 

4.6 常对象与常函数

在成员函数的 函数名() 后面加 const 修饰 可以使其变为常函数.

常函数内, 不可以修改普通成员属性. 若想修改, 需要在属性前加 mutable 修饰.

声明对象时, 在 对象名前面加 const 修饰 可以使其变为常对象.

常对象只能调用常函数.

#include <iostream>
using namespace std;

class Person
{
public:
	int m_A;
	mutable int m_B;

	void Personcosnt() const  //隐含this 是指针常量, 指向不允许改变, 如 Person * const this
		                      //现在又加了 const , 变成了 const Person * const this, 指向的值也不允许改变了
	{
		//m_A = 100;     //报错, 常函数内不允许修改普通成员变量
		m_B = 100;       //正常, 常函数内可以修改 mutable 修饰的成员变量
		//this = nullptr;   //this 的指向本身就是不允许修改的
	}

	void function()  //非常函数
	{

	}
};

void test01()
{
	Person p1;
	p1.Personcosnt();    //非常对象可以访问常函数
	p1.function();       //非常对象可以访问非常函数

	const Person p2;
	p2.Personcosnt();    //常对象可以访问常函数
	//p2.function();     //常对象不允许访问非常函数
}

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

5. 友元

友元的目的是让一个 函数 或者一个 去访问 另一个类 中的 私有成员.

友元的关键字为 friend

5.1 全局函数做友元

即使用全局函数访问类中的私有成员.
这样做需要在目标类中写 友元声明, 其实就是函数声明前面加一个 friend.

友元声明不需要写权限就能生效, 且声明中的内容实现不需要写在该类的前面.

首先需要实例化一个对象, 然后将对象传给全局函数的参数.

根据传递方式的不同, 可以分为 值传递, 地址传递 和 引用传递.

若使用地址传递, 请注意传递的是地址, 而不是对象, 因此需要用 -> 操作符.

#include <iostream>
using namespace std;

class Person
{
	friend void functionptr(Person* p);
	friend void functionref(Person& p);
	friend void functionval(Person p);

public:
	Person()
	{
		name = "张三"; 
		age = 18;
	}

	string name;
private:
	int age;
	
};

void functionptr(Person* p)
{
	cout << "name = : " << p->name << endl;
	cout << "age = : " << p->age << endl;
}

void functionref(Person& p)
{
	cout << "name = : " << p.name << endl;
	cout << "age = : " << p.age << endl;
}

void functionval(Person p)
{
	cout << "name = : " << p.name << endl;
	cout << "age = : " << p.age << endl;
}

void test01()
{
	Person p;
	functionptr(&p);
	functionref(p);
	functionval(p);
}


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

可以看到三种传递方式都成功访问到了私有属性.

5.2 友元类

即 类 做友元. 友元类可以访问目标类中的私有属性成员.

类做友元时, 友元类不需要在被访问的类之前声明. 如下面代码中, Brotheres 是 Person 的友元, 

Brotheres 中的成员属性和成员函数可以访问 Person 中的私有属性, 尽管 Person 的定义包含 

friend class Brotheres ; 这一行, 也不需要在 Person 之前就定义 Brotheres.

总而言之, 友元声明对其中涉及的类没有前置声明需求.这一点很重要.

有两种方式, 第一种需要分别将友元类和目标类进行实例化.

此处 Person 的构造函数采用类类外实现的方式, 即类内声明, 类外实现.

#include <iostream>
using namespace std;

class Person
{
	friend class Brotheres;

public:
	Person();
	string m_Name;

private:
	string m_Room;
};

class Brotheres
{
public:
	void visit(Person &p)
	{
		cout << p.m_Name << endl;
		cout << p.m_Room << endl;
	}
};


Person::Person()
{
	m_Name = "张三";
	m_Room = "卧室";
}

void test01()
{
	Person p;
	Brotheres b;
	b.visit(p);
}

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

第二种只需要对友元类进行实例化, 实现起来麻烦一些, 但是不用再临时实例化一个目标类对象.

在友元类 Brotheres 中添加 目标类指针 (Person * 类型), 同时在友元类 Brotheres 默认构造中开辟堆区内存, 存放数据类型为目标类类型 (Person 类型), 并将指针指向该地址.

其实就是在友元类 Brotheres 中通过函数的方式隐含了目标类 Person 的实例化.

这样只需要调用 Brotheres 的函数, 且无需传参, 就能完成整个私有成员访问的过程.

#include <iostream>
using namespace std;

class Person
{
	friend class Brotheres;

public:
	Person();
	string m_Name;

private:
	string m_Room;
};

class Brotheres
{
public:
	Brotheres()
	{
		p = new Person;
	}

	void visit()
	{
		cout << p->m_Name << endl;
		cout << p->m_Room << endl;
	}

	Person* p;
};


Person::Person()
{
	m_Name = "张三";
	m_Room = "卧室";
}

void test01()
{
	Brotheres b;
	b.visit();
}

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

学到这里容易迷糊, 为什么 Brotheres 里面非得用 Person 的指针, 还得在堆区开辟内存?

首先, 这里可以不用指针, 但在下面的成员函数做友元时, 你不得不用指针.因此在写友元类(即类内属性可以访问它类私有属性的类)的时候, 如果需要将其他类的对象作为自己的成员, 最好写成指针的形式, 这是一个好习惯.

然后, new 开辟堆区内存是因为, 如果单纯的 Person * p ; 实际上并没有给指针明确指向, 而且你也没有对目标类进行实例化, 又怎么访问目标类中的私有属性呢? 对象 b 睁眼一看, 你这 p 是个空指针啊,  你让我 visit , 我 visit 谁啊? 所以, 必须调用 Brotheres 的构造函数来给 p 赋值, 比如说 new 一块堆区内存, new Person 代表这块内存存储 Person 类型的数据, 而这个表达式会返回一个 Person * 类型的数据, 恰好和 p 的数据类型相符. 而在这个过程中, 也会调用 Person 的构造函数, 那么也就相当于完成了实例化, 等下 b 再找过来的时候, 就不会出现找不到的问题了.

如果你不想这么麻烦的话, 那还是老老实实地把友元类和目标类都实例化一遍吧.

但如果在这方面偷懒, 接下来的成员函数做友元学起来就更困难了.

5.3 成员函数做友元

成员函数做友元 原理非常简单, 都是同一个模板, 加个作用域就完事了!

friend Brotheres::visit();

但是实际实现起来却有一堆问题, 你会被各种报错搞到怀疑人生.

而这一切都是声明引起的.

之前我们说过, 友元声明对前置声明没有需求.

但是, 作用域有需求, 而且声明都不行, 还必须定义.

你用 Brotheres:: 这个作用域,  Brotheres 就必须是已经定义过的类.

那就只能把 Brotheres 的定义写在 Person 上面.

然后你又发现, Brotheres 里面有 Person 的对象, 又报错.

又尝试在 Brotheres 前面再加 Person 的声明, 还报错.

因为你必须先定义 Person 才能用 Person 的对象.

这下尴尬了, 你俩没完了 ? ! 那到底怎么写 ?

首先, 作用域这个事是没办法的, Brotheres 必须在 Person 前面定义. 然后, 我们可以通过 前置声明 的方式, 来使用一个不完全的类(即经过声明, 但尚未定义的类)

C++里类的前置声明分析_爱就是恒久忍耐的博客-CSDN博客

对于一个不完全的类, 只能用以下两种方式使用 :

1. 定义指向这种类型的数据的指针.

Person * p ;

那能不能直接在创建指针的时候 new , 一步到位呢?

不行. new Person 需要定义过的 Person 类.

2. 声明一个返回值类型为该类型的函数.

Person function ();

是的, 只能是声明, 定义都不行.

这下知道为什么要大费周章地用 指针 和 new 了吧?

说白了就是 Brotheres 不得不在 Person 前面, 那么对于 Brotheres 的属性来说, Person 必然是不完全类型, 因此只能用 Person * , 你还不能 Person * p = new Person 一步到位, 因为 new Person 必须放在 Person 的定义后面, 所以还得把 Brotheres 的构造函数在类外实现, 还得把这个构造函数写在 Person 定义的后面.

绷不住了.

总结一下, 就是

Person 声明 -> Brotheres 定义(+构造函数声明) -> Person 定义 -> Brotheres 构造函数定义

#include <iostream>
using namespace std;

class Person;

class Brotheres
{
public:
	Brotheres();

	void visit();


private:
	Person* p;
};


class Person
{
	//friend class Brotheres;
	friend void Brotheres::visit();

public:
	Person();
	string m_Name;

private:
	string m_Room;
};

Person::Person()
{
	m_Name = "张三";
	m_Room = "卧室";
}

Brotheres::Brotheres()
{
	p = new Person;
}

void Brotheres::visit()
{
	cout << p->m_Name << endl;
	cout << p->m_Room << endl;
}

void test01()
{
	//Person p;
	Brotheres b;
	b.visit();
}

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

C++ 类声明 类前置声明范例_c++ 类模板 前置声明_zhangatong的博客-CSDN博客

C++知识分享:前置声明及其解析_头文件前置声明_一起学编程的博客-CSDN博客

6. 运算符重载

通常来说, 两个整型数据可以相加, 但是两个类的对象无法相加, 因为编译器不知道怎么处理对象的属性.比如类 A 里面有两个属性, 实例化两个 A 类对象, 分别为 a1, a2.

若给出 a1 + a2 的指令, 那么这四个属性之间到底怎么相加呢? 得到的结果又怎么和新对象对应呢?

这时候就需要用到运算符重载.

运算符重载的作用是, 让程序员自己定义未定义的操作符运算. 

原始的运算符重载 :

上面的问题, 可以通过函数来进行解决.

class A
{
public:
   int m_A ;
   int m_B ;
   
   A()
     {   
         m_A = 10;
         m_B = 10;
     }      
};

A add_A(A & a1, A & a2)
{
   A temp ;
   temp.m_A = a1.m_A + a2.m_A ;
   temp.m_B = a1.m_B + a2.m_B ;
   return temp;
}


int main()
{
  A a1;
  A a2;
  A a3 = add_A(a1,a2);
  cout << a3.m_A << a3.m_B << endl;
  return 0;
}   

  • 3
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值