c++中的继承(下)

news2024/11/24 20:13:53

首先我们先回忆一下,在派生类(子类)中默认的成员函数做了什么事情?我们现在可以这么认为对于普通类来说呢?只需要看待两个部分的成员:内置类型和自定义类型。而对于派生类而言序言看待三个部分的成员:内置类型,自定义类型以及父类

类型

构造和析构

拷贝构造

普通类

对于内置类型一般不处理,自定类型调用自定义类型的构造和析构

对于内置类型完成值拷贝,对于自定义类型调用自定义类型的拷贝构造函数

派生类

和上面的基本一样,但是还需要去显示调用父类的构造和析构

依旧和上面的一样,但是依旧需要显示的去调用父类

即派生类是将父类有看成了一个整体,无论你是显示的去写派生类的构造和析构还是编译器自己生成的构造和析构,都是去显示的调用父类的构造和析构的。

下面我们来实现一个小问题:即如何实现一个不能被继承的类(继承无意义的类)?

思路很简单:把构造函数私有化

原因很简单,当你创建B类对象的时候,肯定需要调用父类的构造函数,但是因为父类的构造函数为私有,无法在除了父类以外的地方调用构造导致B无法调用父类的构造,所以无法创建对象,即使你要在B中显示的调用父类的构造也无法调用因为父类的private成员和成员函数,对于子类而言是不可见的。

但是这种方法呢有点不够直观,即你B类还是继承了A类只是无法创建对象而已。而在c++11中添加了一个关键字final,能够让子类在继承的时候就直接报错。

继承与友元

这里需要注意一点:友元关系是不能够被继承的也就是说,基类(父类)的友元函数不能访问子类私有成员和保护成员。

可以用一句通俗的话来讲:即你父亲的朋友不一定是你的朋友。若想要让友元也能够访问子类,也需要在子类中声明友元。

继承与静态成员

我们首先思考一个问题静态成员会不会被继承下来呢?

下面便存在一个父类成员中含有一个静态变量,那么我们在子类中能否访问这个静态变量呢?

可以看到正常的运行打印了,没有报错。

但是我们现在在思考一个问题,此时子类的这个静态成员变量是独立的还是是父类的那个静态成员变量呢?

我们可以对这两个静态成员变量去一个地址看一看

可以看到地址一摸一样。那么这里我们可以认为静态成员并没有被继承,被继承的是静态成员的使用权,因为如果静态成员是被继承了的,在子类中应该会产生一份独立的静态成员,而这里显然是没有。

除此之外,静态成员通常是属于一个类的,只要你的静态成员是public并且突破了类域,那么你就能够访问到静态成员,所以这里子类应该是继承了父类静态成员的使用权。

当然上面的静态成语还有一个能力,即能够记录你创建了几个student和person对象

请看下面的代码:

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
 string _seminarCourse; // 研究科目
};
Student func()
{
	Student c;
	return c;
}
int main()
{
	Person a;//创建person对象会调用person的构造让_count++
	Student b;//子类依旧会去调用父类的构造
	Student c;
	func();
	cout << Student::_count << endl;
}

菱形继承和菱形虚拟继承

首先我们来看一下下面的单继承和多继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

记住是一个直接父类

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

记住是两个或以上直接父类

那么c++为什么要存在多继承呢?

因为在现实生活中,是存在一个人具有多个角色的,例如某些人因为经济原因,不得不一个人在白天工作完成之后,晚上还会出去跑滴滴。那么这个人白天的时候,可能是个厨师,那么晚上的时候,可能就是个司机,这是很正常的。还有一个例子假设存在一片桃园,那么在春天的时候,可能这就是一片观赏桃园。而在夏天的时候,这里又会变成一个果园。那么这个时候,果园和花园都是这个桃园的属性。那么在现实生活中,一定会有一些对象会兼具多个属性,从这一点出发,那么多继承是很合理的。

但是你使用多重继承就有可能导致下面的情况:菱形继承

菱形继承会导致Assistant类中出现二义性,和数据冗余的问题。为什么呢?我们从Assistant的角度出发,Assistant类继承了一个Studet类和一个Teacher类,而这两个类中又分别含有了一个Person类,这就导致Assistant类中出现了两份Person类。这也就导致了数据冗余,同时两个person类也就意味着存在了两个_name,也就导致了二义性,请看下面的实际代码:

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
void Test()
{
	// 这样会有二义性无法明确知道访问的是哪一个
	Assistant a;
	a._name = "peter";//这里在a中存在两份Person类对象,所以就存在两个_name导致出现了二义性
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";//指定一个类域,能够解决二义性,但是无法解决数据冗余的问题。
	a.Teacher::_name = "yyy";
}

但是也许你可以认为我作为老师的时候,别人叫我XX老师,作为学生的时候,别人叫我XX学生是很合理的,但是这只是因为我在上面的Person类中只设定了_name而已,如果我在设定一个_age呢?难道你作为学生和老师的时候,你的年龄就不一样了吗?所以并不是所有人的信息都需要两份,只有少部分需要两份。

这个代码的第一个问题二义性:

从监控就可以看到在a中Student里面有一个名字,在Teacher中也存在一个名字,所以如果你直接使用a._name就会导致歧义,这也就是二义性。

而解决二义性的第一个方法也就是我上面写的指定类域,

从上图也能看出,在a中确实是存在两个Person对象的。虽然指定类域能够解决二义性的问题,但是无法解决数据冗余的问题。

那么为了解决这个问题c++提供了一个关键字virtual(虚),而这个关键字的引入也就引起了一个虚继承。

我们下面来看使用了虚继承后的效果:

从上面就可以看出,在a中的Teacher对象和Student对象中的Person对象变成了同一份。所以我们任意修改一份/或者不指定类域都可以进行修改。

当然现在的这个监视窗口变得不太准确了。我们可以认为在a中的Teacher对象和Student对象中存了一份Person对象的引用。

对于下面的这张图我们的virtual是加在了Student和Teacher那里。

那么如果是下面的这种情况呢?

应该这么加:

这里就需要记住,我们现在是哪里出现了数据冗余,是Person。所以我们就加在直接继承Person的那些类上。

那么virtual是如何解决这个问题的呢?

首先我们先拿下面的代码作为参考的例子:

class A
{
public:
	int _a;
};
class B : public A
//class B : virtual public A
{
public:
	int _b;
};
class C : public A
//class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}
int main()
{
	return 0;
}

这里简化实现了一个菱形继承,类型ABCD中都存在一个小写的abcd变量。

继承关系:

因为监视窗口已经不能很准确的表明内存中各个类的关系了,所以下面我们要采用内存窗口去查看这些类对象。

上图中的那个地址也就是d的地址,现在调试的时候,并没有增加virtual关键字,所以是会出现数据冗余的。其中在B中出现了一个_a为1,而在C中也出现了一个_a等于2。

那么c++的virtual是如何解决二义性和数据冗余的问题呢?首先这两个问题会出现的根本原因也就是在D中出现了2份A。那么为了解决这个问题,所以我们不能在B中存一份A,也不能在C中存一份A,我们需要重新找一个地方存这个A。怎么存的呢?我们首先将上面的代码修改为使用虚继承的代码。

然后依旧是使用内存窗口来查看d里面内容的储存。

此时我用d:B去访问的a在红色标记处。(上图红色处)

使用c去访问的a发现也在这里(上图红色处)。

然后d._b储存的位置也确定了(上图红色处)

d._c储存的位置也确定了(上图红色处)

d._d储存的位置也确定了(上图红色处)

因为我的vs2022的内存窗口不太好看所以请看下面的图来理解其中深层次的原理:

所以这个虚继承就是这样去解决多份储存A的问题,此时的这个A既不存在了B中,也不存在了C中,而是存在了一个额外的地方。此时自然也就不存在二义性,以及数据冗余的问题了。但是我们看B和C里面多了一行东西,这个东西是什么呢?为啥要需要这两个东西呢?

我们先不思考这个问题,我们假设如果没有虚继承,然后遇到下面的代码:

那么此时的这个代码就非常的好理解:利用切片将d中B类型的成员赋值过来(第一行)

第二行利用切片将d中B所对应的地址直接拿取出来即可。因为此时的整个D类型的B中是含有A的所以可以这样理解去使用切片。

但是如果是使用了虚继承呢?

此时如果还是和上面一样使用切片的话,是无法完成的,因为B中应该是要含有A的但是此时的B中是没有A的(因为A已经被放到了一个其它的位置),所以我们这里就猜测一下B中的这个东西应该是能够帮助B,找到A所在的位置的。那么帮助B找到A的位置的方法有很多种这里选择的是下图中的这种方法。

此时我们回到B中dc的那个位置往下移动20个字节(1行是4个字节),刚好就能够到达A的位置。

同理对于C也是这样从e4的那个位置开始往下移动12个字节,刚好也能到达A的位置。

这里编译器在DC的那个地址处生成了一个表,这个表里面储存的就是距离A的偏移量,至于为什么不是在dc7b的那个位置储存而是在下一个位置储存,是因为在dc7b的那个位置需要保存一些多态才会学的东西。我们暂时可以不用管。

那么如果我们现在执行下图中的第三步是如何执行的呢?

这里是直接让ptr的指针指向了A中的_a吗?并不是,他是先通过B中的dc7b地址找到偏移量表,从表中得到此时的B距离A的字节数,然后再去移动ptr指针,找到A然后找到A中的_a,再让_a++。

还有更加神奇的地方,我们下面去创建一个B类型的对象,然后去看一下这个B对象的对象模型

也就是说,当B类虚继承以后,B类会保持和下面的类一样的模型,同理这对于C类型也是一样的。那么为什么要这么设计呢?

因为可能还会存在下面的这种场景。

即一个B类型的指针,他可能指向的是B对象也有可能指向是D对象,如果你光看这一个代码(ptr->_a++),你是无法分清这个ptr指向的是B对象还是D对象的。所以为了统一操作都是通过这个指针找到对应的这个表,再表里面找到对应的偏移量。通过偏移量找到A,然后让_a++,这样就统一了操作,无论你的这个指针指向的是B对象还是D对象。如果B对象不和下面的保持一致,那么又要进行单独的处理,更加的麻烦。因为ptr指向的是哪一个对象我们是不知道的。当然将A成员放到下面只是vs编译器做的处理,其它的编译器可能放到上面,只要能够保证虚继承能够解决二义性和数据冗余的问题,就可以了。但是大部分的编译器都是按照我们上面的规则做的。

那么现在我们再提出一个问题,我能不能在下图的cc 7b那里直接存A的地址可不可以,或者说我直接在这个位置存偏移量可不可以。

答案是都可以,但是为什么要选择这一种呢(即找到偏移量表,再来找偏移量)?

那是因为我们现在并没有考虑到多态,或者说难道那个表中就只会保存偏移量吗?肯定不是,如果没有使用偏移量表的方式,在遇到多态的时候,我们将其他的什么偏移量都放到对象当中就会导致对象内存变大。

初次之外难道D对象就只有一个吗?如果存在多个D对象,如果采用的是在类中储存偏移量的方法(或是其他的方法)都会造成一定量的大小变大,而如果使用的是表的方法,那么无论多少个D对象,都只需要指向这个表就能够通过这个表找到偏移量,然后找到A。

如下图

其中的dd为左图内存监视,右图为ddd内存监视。使用这种方法很明显是更加省空间的。如果创建的D对象很少的话,无论使用那个方法都是差别不太大的,但是如果存在很多个D对象呢?此时很明显使用表的方法更加省空间。

使用这个方法A越大你赚的就越多,因为这里解决的就是储存了多个A的情况。

到这里菱形继承就已经基本完全了,但是在日常中还是尽量不要去使用菱形继承。菱形继承如果在套上其它的语法是会变得非常复杂的。

但是在库中还是存在使用菱形继承的情况的

这就是库中使用菱形继承的地方,如果这里要解决数据冗余的话,要在istream和ostream处加上virtual

继承的总结和反思

1.很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。

  1. 多继承可以认为是C++的缺陷之一,很多后来的面向对象语言都没有多继承,如Java。

继承和组合

首先公有继承的关系是is-a的关系,而组合的关系则是has-a的关系,那么什么是组合呢?

下面就是组合:

下面是继承:

首先组合和继承的共同点都是可以复用。

然后从大小上来看呢?从下图可以得到两者的大小是一样大的。

那么其它方面有没有什么区别呢?

首先从权限上面来说:

父类的公有子类能够使用,父类的保护子类也能够使用,对于组合来说,组合类的共有子类能够使用,但是组合类的保护子类不能使用。当然无论是父类还是组合类的private,子类都不能使用。所以从这里能够得出继承的权限是大于组合的。

下面请看详细的说明:

总的来说继承被认为是白箱复用(白箱即我知晓底层的逻辑),黑箱复用(黑箱即我不知晓底层实现的原理),在这里还存在一个白盒测试:即我知晓底层实现的原理,从底层实现的逻辑上去测试。而黑盒测试即我不知晓底层实现的原理,而只是从用的角度去测试。这也是测试时使用的方法。那么换回到这里白盒就非常的适合继承,因为对于继承而言,除了private我子类不能访问外,其余的细节我子类都是可以访问的。对于组合而言,被组合的那个类对于我而言是不可见的(不能用)。这个类对于我和对于其它类都是一样的,其它类只能使用这个被组合类公有的部分,对于组合了这个类的我而言,我也依旧只能使用这个被组合类的公有部分。

从方便使用的角度而言使用继承更好,因为没有很多的限制,但是从更长远的角度来看使用组合更好。因为组合类的依赖关系很弱,耦合度也就越低。

那么为什么使用组合比使用继承好呢?

如果这一个大框是一个大的功能模块,而其中的这些小圈就是一个又一个代码,如果耦合度高(上图),那么任何一个代码出现了错误,都会导致这一整个代码出现错误。

而上图的这个耦合度就不是很高,即使一个代码出现了问题,并不会影响一整个代码。

所以在工业的软件开发的时候,可能就会让我们画类关系图。尽可能地让耦合度低一点。

所以尽可能的去使用组合,但是这也并不是说,继承就一定不能用,一切都要看你现在所在的情况而定。如果两个类的关系是has-a的关系,你使用组合更好。如果两个类的关系是is-a的关系你使用继承更好。除此之外,如果你要使用多态那么你必须使用继承,多态就是建立在继承的基础上的。

例如学生和人,你使用继承的关系更好,但是轮胎和车很明显是组合更为符合,如果两者的关系都是符合的那就考虑使用组合。

下面我们来看一道菱形继承的题目:

上面的class A为S1 class B 为S2 class C 为S3 ,class D为S4

然后要求你输出程序的打印。

首先这里创建了一个D对象,那么就去看D但是,D是存在继承的,然后因为

这里B在前C在后所以这里就先会去构建B然后去构建C对象。既然要构建B对象,所以就去B的构造因为B也是子类,所以需要先构建A,所以这里首先就会打印class A,因为构建了一个A对象,构建完了A对象之后,构建B对象,所以接着会打印class B,然后会去构建C对象,因为这里的虚继承不会让A多次创建所以,这里的C直接构建一个C对象即可。所以会打印class C,但是我们看到在D的初始化列表中含有一个A(s1),但是不要忘了我们这里是使用了虚继承的,所以这里只需要存在一份A即可。所以这里最后打印一个class D即可。

所以这道题目最后打印的结果是 class A,class B,class C,class D。

希望这篇博客能帮助到正在阅读的你。写的不好请见谅,也请提出您的评判,如果发现了任何错误,也烦请指出,感谢。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1100453.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Mybatis 配置自动生成代码飘红 解决方法

在配置Mybatis自动生成代码的时候&#xff0c;明明按照教程一字不落地拷贝下来了&#xff0c;还是报了大片的错&#xff0c;gpt也无法给出有效的解决方法。 后来发现&#xff0c;是引用的头文件不对。 例如&#xff1a; 无法解析 StringUtils 中的方法 isNotBlank gpt给出的…

VS2022:结构化诊断

对于许多 C 开发人员来说&#xff0c;如果编译器爆出了大量的编译错误&#xff0c;且错误之间还没有明显的关联&#xff0c;这可就不好玩了。 如果开发环境可以提供更容易理解的错误提示和诊断体验&#xff0c;则开发人员就更加容易的找到错误原因并修复它。 我曾经写了一篇文…

【软考-中级】系统集成项目管理工程师-风险管理历年案例

持续更新。。。。。。。。。。。。。。。 目录 2020 下 试题三(20分) 2020 下 试题三(20分) 2018 年底&#xff0c;某公司承接了大型企业数据中心的运行维护服务项目&#xff0c;任命经验丰富的王伟为项目经理。 2019 年1月初项目启动会后&#xff0c;王伟根据经验编制了风险管…

超声波清洗机需要注意什么?不能错过的超声波清洗机

超声波清洗机在当今社会已经越来越受到人们的欢迎&#xff0c;它利用超声波的振动来清洁物品表面&#xff0c;能够快速、高效地清除污垢、油脂等。但是&#xff0c;在购买超声波清洗机时&#xff0c;需要注意哪些问题呢&#xff1f;本文将为您介绍购买超声波清洗机需要注意的几…

2015款奔驰B200车发动机故障灯异常点亮

杭州捷盛行汽车服务有限公司 乐翔 乐翔&#xff0c;现任杭州市捷盛行汽车服务有限公司技术经理、开思杭州地区技术顾问及博世车联技术支持&#xff1b;2015年获保时捷全球认证技师资质&#xff1b;2016年取得汽车维修高级技师资格证书。 故障现象 一辆2015款奔驰B200车&#x…

python批量将多年降水的nc数据处理为季节性平均降水量或年降水量

本代码目的: 1.批量读取nc降水数据集。 2.按照季节平均来粗略绘制降水量图。 3.保存所有处理后的数据集,以备下次精细化绘图。 原始数据请见美国2013-2021年每日降水的nc数据集资源-CSDN文库 ##1.导入需要的库和函数 import xarray as xr import os from netCDF4 impo…

Oracle基础学习

文章目录 1. oracle数据库安装2. sqlplus连接数据库方式3. 创建用户信息4. 基本概念5. 基本SQL语句6. Springboot开发 1. oracle数据库安装 安装教程 安装包地址 2. sqlplus连接数据库方式 无用户信息登录 使用用户信息登录 登录最高权限管理员&#xff0c;如果不加上as …

如何利用PHP快速抓取音频数据?

以下是一个使用Dusk库和PHP编写的爬虫程序&#xff0c;用于爬取海量的音频数据。这个程序使用了https://www.duoip.cn/get_proxy的代码。 <?php // 引入Dusk库 require Dusk.php;// 创建Dusk对象 $dusk new Dusk();// 设置代理 $dusk->setProxy(127.0.0.1, 8080);// 使…

《算法通关村第一关——链表青铜挑战笔记》

《算法通关村第一关——链表青铜挑战笔记》 Java如何构造出链表 概念 如何构造出链表&#xff0c;首先必须了解什么是链表&#xff01; 单向链表就像一个铁链一样&#xff0c;元素之间相互链接&#xff0c;包含多个节点&#xff0c;每个节点有一个指向后继元素的next指针。…

无纸记录仪接线方法和接线图

一、彩色无纸记录仪输入和输出信号接线 彩色无纸记录仪主要有以下几种接线方法 1、通讯线的连接: RS-485通讯线的连接 RS485通讯线请使用屏蔽双绞线&#xff0c;通讯线长度不能超过1000米。在通讯线长度大于100米的条件下进行通讯时&#xff0c;为减少反射和回波&#xff…

轻松搞定99%以上物联网应用系统快速交付:揭秘宏电三三开源物联网平台

万物互联的时代物联网技术涉及到了终端、通讯、平台、应用等多个层级&#xff0c;覆盖工业、电力、农业、商业等多领域。物联网需求是海量的、碎片化的&#xff0c;导致多元异构的物联网平台技术堆栈复杂&#xff0c;开发周期长、实现难度大。 如何低成本、高效地接入基于各类…

什么牌子的电容笔性价比高?口碑好的电容笔推荐

如今&#xff0c;随着生活的智能化程度不断提高&#xff0c;一些人已经把传统的笔记本电脑换成了平板电脑。我发现&#xff0c;用iPad平板画图&#xff0c;或者用iPad平板记笔记&#xff0c;真的很方便。根据我对电容笔的深刻理解&#xff0c;如果你只是想要用电容来记录&#…

这些并发测试知识点,你掌握了吗?

一、Apache Benchmark ab 命令会创建很多的并发访问线程&#xff0c;模拟多个访问者同时对某一 URL 进行访问&#xff0c;可用来测试 Apache 的负载压力&#xff0c;也可以测试 Web 服务器的压力。 安装 liunx 安装&#xff1a;yum install httpd Windows安装&#xff1a;下…

lvgl 页面管理器

lv_scr_mgr lvgl 界面管理器 适配 lvgl 8.3 降低界面之间的耦合使用较小的内存&#xff0c;界面切换后会自动释放内存内存泄漏检测 使用方法 在lv_scr_mgr_port.h 中创建一个枚举&#xff0c;用于界面ID为每个界面创建一个页面管理器句柄将界面句柄添加到 lv_scr_mgr_por…

JMeter安装及环境配置

1. JMeter 介绍 Apache组织开发的基于Java的压力测试工具 100%纯Java开发、完全的可移植性 可以用于测试静态和动态资源 多协议—HTTP/FTP/socket/Java/数据库(JDBC) 完全多线程 高可扩展性 2. 安装jdk并配置jdk环境 因为jmeter运行依赖jdk环境&#xff0c;所以在安装j…

以太网UDP数据回环实验

一、TCP/IP协议簇 前面说到TCP/IP是一个协议簇&#xff0c;其中包含有IP协议、TCP协议、UDP协议、ARP协议、DNS协议、FTP协议等。设备之间要想完成通信&#xff0c;就必须通过这些网络通信协议。 物理层的主要作用就是传输比特流&#xff08;将1、0转化为电流强弱来进行传输&am…

SpringBoot + 自定义注解 + AOP 高级玩法打造通用开关

前言 最近在工作中迁移代码的时候发现了以前自己写的一个通用开关实现&#xff0c;发现挺不错&#xff0c;特地拿出来分享给大家。 为了有良好的演示效果&#xff0c;我特地重新建了一个项目&#xff0c;把核心代码提炼出来加上了更多注释说明&#xff0c;希望xdm喜欢。 案例 …

图解Dubbo,Dubbo 服务治理详解

目录 一、介绍1、介绍 Dubbo 服务治理的基本概念和重要性2、阐述 Dubbo 服务治理的实现方式和应用场景 二、Dubbo 服务治理的原理1、Dubbo 服务治理的架构设计2、Dubbo 服务治理的注册与发现机制3、Dubbo 服务治理的负载均衡算法 三、Dubbo 服务治理的实现方式1、基于 Docker 容…

Flowable介绍及使用示例

文章目录 Flowable简介底层实现JavaSpring FrameworkMyBatisActiviti Flowable的使用示例引入依赖创建流程定义部署流程定义启动流程实例启动流程实例处理任务监控流程实例 高级用法流程监听器事件驱动定时任务其他高级功能 使用时可能遇到的问题和注意事项结论参考文献 Flowab…

微信群发消息怎么发?群发消息,只要这4个步骤!

微信是我们日常生活中使用最广泛的社交软件之一。用户通过微信可以向好友、家人、同事等联系人发送文字、图片、视频、语音、文件等信息&#xff0c;是一款非常实用的即时通信应用程序。 除了与好友进行单独聊天&#xff0c;我们有时候可能也需要将信息进行群发。但是还有很多…