C++进阶-->多态(Polymorphism)
1. 多态的概念
多态,顾名思义多种形态;多态分为编译时多态(静态多态)和运行时多态(动态多态),静态多态就是就是我们前面讲的函数重载和函数模板,可以通过传不同类型,然后在编译期间就确定好使用哪个的称为静态多态;动态多态就是一般在程序运行时确定使用指定函数的就称作动态多态。
动态多态,举个生活中的例子,例如铁路12306中买票的时候,当成年且没有特殊情况的人是全价买票,学生买票有特定的学生价买票,军人买票时可以优先买票,而这个过程抽象来说就是传入不同的对象就会完成不同的行为。
2. 多态的定义及使用
2.1 多态的构成条件
多态是一个继承关系下的类对象,去调用同一函数,产生了不同行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。
2.1.1 实现多态的两个必要条件
1. 必须是基类的指针或引用调用函数。
2. 被调用的函数必须是虚函数(virtual修饰的)。
解释一下为什么必须是基类的指针或引用调用函数,因为只有是基类的指针或引用才能做到既接收基类的地址又接收派生类的地址;
第二派生类必须对基类的虚函数进行重写/覆盖,派生类对基类的虚函数重写了才能做到传入不同的类执行不同的操作的效果,才能达到多态。
3. 虚函数
在继承那一章我们也讲到虚继承,虚继承是用来避免菱形继承带来代码的冗余和二义性的问题,关键字是virtual;虚函数的关键字也是virtual,只需要在类成员函数前面加上virtual修饰,那么这个成员函数就被称为虚函数。注意:如果不是成员函数是不能加virtual修饰的。如下代码所示:
class Person
{
public:virtual void BuyTicket(){cout << "买票--全价" << endl;}
};
3.1 虚函数的重写
派生类中有一个虚函数跟基类的虚函数完全一致的虚函数称为虚函数的重写。
完全一致指的是:函数名、参数列表、返回值类型。(注意!这里的参数列表指的是参数的类型,名字相同没有关系);如下代码所示:
class Person
{
public:virtual void BuyTicket(int x){cout << "买票--全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(int y){cout << "买票--半价" << endl;}
};
还有一点需要提一下的是:如果基类的虚函数加了virtual,那么派生类的虚函数可以不加virtual,这里我们可以直接按“在继承的时候就把基类的虚函数的属性继承下来了”理解,这个不太建议这样子用,但在笔试或者考试选择题可能会考。
下面是虚函数、虚函数的重写、实现多态的代码使用:
#include<iostream>
using namespace std;class Person
{
public:virtual void BuyTicket(int x){cout << "买票--全价" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(int y){cout << "买票--半价" << endl;}
};void BuyTicket(Person* ptr)
{ptr->BuyTicket(6);
}int main()
{Person p1;Student s1;BuyTicket(&p1);BuyTicket(&s1);return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
最后再补充一个重重重重要的知识:虚函数的重写本质上是重写虚函数的实现!!!!下面就有一道相关的练习题。(毕竟人教人教不会,事教人一脚就懂)
3.2 多态场景下的选择题
#include<iostream>
using namespace std;class A
{
public:virtual void func(int val = 1){std::cout << "A->" << val << std::endl; }virtual void test() {func(); }
};class B : public A
{
public:void func(int val = 0) {std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
首先如果我们对重写的知识理解的不够全面的话,那么肯定会选择D, 但我们上面说了重写是重写函数的实现,函数的参数不会有变化,所以我们调用的func应该如下图所示:
所以正确的答案是B;
最后我们再来细致分析一下p->test()究竟怎么构成多态,我们都知道在类里面的成员函数会有一个隐藏的参数就是this指针,而test是在A里面的,所以完整的函数应该为 virtual void test(A* this)这里就构成重载了,基类做this指针的返回值,完全符合上面所说的2个多态的构成条件。
3.3 虚函数重写的一些问题
3.3.1 协变
协变:派生类重写基类的虚函数的时候返回值不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用,这个就成为协变。协变用处不多,仅需了解一下即可;如下代码所示:
#include<iostream>
using namespace std;
//基类
class A
{};
//派生类
class B :public A
{};class Person
{
public://基类做返回值virtual A* BuyTicket()//virtual Person* BuyTicket()也可以{cout << "买票-全价" << endl;return nullptr;}
};class Student :public Person
{
public://派生类做返回值virtual B* BuyTicket()//virtual Student* BuyTicket()也可以{cout << "买票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}
3.3.2 析构函数的重写
基类的析构函数只要定义为虚函数后,无论派生类的析构函数写不写virtual,都会与基类的析构函数构成重写。
前面继承有提到一嘴,就是析构函数最后都会被编译器转换为destructor(),那么这时候函数名(destructor)相同构成隐藏(这个是继承那边的)。
所以只要基类的析构加了virtual,派生类的就构成了重写;函数名一样都是destructor、参数一样都没有、返回值类型都没有。
接下来就谈谈为什么要实现析构函数的重写(重点!!!!!)
在思考为什么的时候我们就可以通过反推来证明即如果没有实现会发生什么情况;我们先看一下下面的代码:
#include<iostream>
using namespace std;class A
{
public:~A(){cout << "~A()" << endl;}
};class B :public A
{
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
对代码的解析:我们用基类A指针实例化了两个对象p1接收基类A的地址、p2接收派生类B的地址,如果说我们没有实现析构函数的重写,delete p1和delete p2 是直接走基类的析构函数,因为没有构成多态,也就不会产生不同的行为而只走基类A的析构函数,如果派生类B里面有动态申请空间,那么就会出现内存泄漏。如下图:
但如果我们让基类A的析构函数定义为虚函数,那么就构成多态,从而会根据传入不同的类型调用不同对象的析构函数。
这里为什么会还会调用一次基类A的析构函数呢?前面继承有提到,派生类的析构函数里面其实包含了基类的析构函数,主要是为了达到先子后父的析构顺序。
最后总结一下:C++的这些实现其实都是有关联的,他们为了处理上面的这种情况而实现了析构函数的重写,为了实现析构函数的重写而实现了让析构函数无论函数名为什么最后都会转换成destructor。真的很妙。
3.3.3 override和final关键字
override是用来检查你是否重写错误;
final在继承那里也提到过,如果想实现一个类不被继承就用final,而这里的final是用来实现一个不能被重写的虚函数;
3.3.4 重载/重写/隐藏的对比
3.3.5 纯虚函数和抽象类
纯虚函数只要在虚函数后面加个“=0”即可;纯虚函数不需要定义实现,没有很大作用,但是在语法上是支持实现的 。而包含纯虚函数的类称作抽象类,抽象类不能实例化出对象,但是派生类可以继承抽象类,如果派生类不重写纯虚函数,那么派生类也称为抽象类。纯虚函数其实就是间接强制了派生类要重写纯虚函数。
干说有点难理解,举个实际例子,狗和猫都是动物,狗的叫声是“旺旺”,猫的叫声是“喵喵”,狗和猫都是动物都有动物的特征所以可以继承动物的属性,但是动物是怎么叫的我们不知道,而动物就可以称作是抽象类,“叫”这一个动作可以用纯虚函数实现,猫和狗则是动物的派生类,所以需要重写这个纯虚函数,狗重写“叫”函数为“旺旺”,猫则是“喵喵”。如下代码所示:
#include<iostream>class Animal
{
public:virtual void talk() = 0;
};
class Dog : public Animal
{
public:virtual void talk() {std::cout << "汪汪" << std::endl;}
};class Cat : public Animal
{
public:virtual void talk() {std::cout << "(>^ω^<)喵" << std::endl;}
};
int main()
{Cat cat;Dog dog;cat.talk();dog.talk();return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
4. 多态的原理
首先我们看一道题,用一道题引出下面的知识;
#include<iostream>
using namespace std;class Base {
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
我们知道,计算类的大小是使用对齐的方式计算的,那么计算出来的结果是8,可能我们的答案就选择了C,实则并不然,答案选的是D,首先我们通过调试看一下里面究竟多了什么玩意,如下图所示:
我们发现里面除了成员变量_b和_char,还有一个指针_vfptr,这个指针我们称作虚函数表指针(virtual function pointer);
4.1 虚函数表指针
每个含有虚函数的类中都会有一个虚函数表指针,这个指针是指向一块存着虚函数地址的空间,虚函数表也称作虚表。所以我们传入不同类去调用该类的虚函数的时候就是通过这个指针去调用不同类中的虚函数的。
4.2 多态的实现
首先我们先来看一串代码
#include<iostream>
using namespace std;
class Person {
public:virtual void BuyTicket() {cout << "买票-全价" << endl;}
protected:string _name;
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl;}protected:int _id;
};class Soldier : public Person
{
public:virtual void BuyTicket() { cout << "买票-优先" << endl;}protected:string _codename; // 代号
};void Func(Person* ptr)
{// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket// 但是跟ptr没关系,而是由ptr指向的对象决定的。ptr->BuyTicket();
}int main()
{// 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后// 多态也会发生在多个派生类之间。Student st;Soldier sr;Person pr;Func(&st);Func(&sr);Func(&pr);return 0;
}
我们可能会好奇,Func内的ptr是如何做到传入不同的类对象的地址然后走不同的虚函数执行不同操作的,下面就来讲解一下。
如下图
通过上图我们看到,满足多态后,不再是编译时通过调用对象确定函数的地址,而是运行时到指定的对象内找到对象的__vfptr(虚表)然后确定对应的虚函数的地址,然后执行,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。
4.3 动态绑定和静态绑定
动态绑定则是满足多态条件的函数调用是在运行时绑定,说简单点就是在运行时去虚函数表内找到函数地址然后调用。
静态绑定则是在编译期间确定函数的地址然后调用。
这个可以通过汇编层看一下。
动态绑定👇看着挺复杂的,挺多操作,因为他是在运行时先去找到__vptr这个指针,然后找到虚函数表,再通过虚函数表内的地址找到虚函数,再调用该函数。
静态绑定👇编译器直接确定函数地址然后直接调用。
4.4 虚函数表
1. 基类对象的虚函数表中存放基类所有虚函数的地址。如果同类型实例化对象的话则虚表共用,不同类型虚表各自独立。
2. 派生类由两部分构成,一部分是派生类自己的成员,还有一部分是基类的成员,还有基类的虚表指针,继承下来的基类中有虚表指针的话就不会自己再生成一个新的虚表指针,但继承下来的和虚表指针和基类的虚表指针不是同一个,他们的地址不同。
3. 如果派生类中有虚函数的重写,那派生类虚表中对应的虚函数就会被重写的虚函数覆盖。
4. 派生类的虚表中包含基类的虚函数地址,派生类重写的虚函数地址,还有派生类自己的虚函数地址。
5. 虚函数的本质是一个存虚函数指针的指针数组,一般情况下这个数组后面会放一个0x00000000的标记 (这个C++并没有进⾏规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000 标记,g++系列编译不会放)
6. 虚函数和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址存在虚表中。
7. 虚函数表的存在位置是看编译器的,C++并没有规定,下面将来看一下vs的在哪里;如下代码所示:
#include<iostream>
using namespace std;class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}
👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇
运行结果为:
由此可见,虚表的地址与常量区的地址很接近,那就说明虚表是存在常量区的。
5. 额外补充的知识
首先我们先来看一个代码。
#include<iostream>
using namespace std;class A
{
public: void test(float a) {cout << a; }
}; class B :public A
{
public: void test(int b) {cout << b; }
}; void main()
{A* a = new A;B* b = new B;a = b; a->test(1.1);
}
按照我们的思路,a赋值给b之后,那么A的test的隐式指针this就应该转换成B类的this,应该调用的是cout出b的值为1。
但是编译器不是这么做的,编译器虽然通过指针的指向访问成员变量,但是不能通过指针的指向来访问成员函数,而是通过指针的类型来访问成员函数。也就是说无论a的指针指向了谁,都只能访问到A类内的成员函数。(引用也是一样的!!!!!)
派生类有几个父类,且那几个父类都有虚函数的话,那么派生类就会有几张虚表。而如果在派生类中增加虚函数的话只会放在第一张虚表的最后。
相关文章:

C++进阶-->多态(Polymorphism)
1. 多态的概念 多态,顾名思义多种形态;多态分为编译时多态(静态多态)和运行时多态(动态多态),静态多态就是就是我们前面讲的函数重载和函数模板,可以通过传不同类型,然后…...

python实战项目51:selenium结合requests获取某众点评评论
python实战项目51:selenium结合requests获取某众点评评论 一、selenium获取cookies二、利用requests发送请求三、注意事项四、完整代码一、selenium获取cookies 首先,初始化selenium的webdriver,然后使用webdriver打开某众点评主页,之后手动扫码登录,利用selenium的get_c…...

面试准备第一版ssm spring-springmvc
请写出spring中常用的依赖注入方法: 1、setter 2、构造方法注入 3、字段注入 Setter 注入: 通过公共的 setter 方法进行依赖注入。优点:可选依赖,能更清晰地看到依赖关系。缺点:依赖在构造时不可用,可能导…...

Ubuntu学习笔记 - Day1
文章目录 学习目标:学习内容:学习笔记:Linux简介基于Linux内核的系统 Ubuntu简介GNU简介 远程连接Ubuntu查看Ubuntu的IP地址Mac连接Ubuntu此时可能显示报错,连接被拒绝解决办法连接成功连接退出 学习目标: 一周掌握 Li…...

挑战Java面试题复习第4天,坚持就是胜利
挑战第 4 天 Excption与Error包结构OOM 知识点SOF 知识点线程程序进程知识点有些字段不想序列化,怎么办?说说 IO 流Java IO与 NIO的区别 Excption与Error包结构 运行时异常(RuntimeException): 包括RuntimeException…...

Android 虚拟化框架(AVF)指南
Android 虚拟化框架(AVF)指南 一、项目介绍二、项目特色三、如何使用AVF四、总结 随着移动设备的普及和应用场景的多样化,安全性和隐私保护成为了移动操作系统的重要课题。Android作为全球最广泛使用的移动操作系统之一,一直在不断…...

day-77 超级饮料的最大强化能量
思路 动态规划:因为每一步要么选A,要么选B,所以问题可以转换为求最后一步从A选或从B选中的较大值 解题过程 定义而二维数组dp,dp[i][0]表示最后一步从A取能获得的最大能量,dp[i][1]表示最后一步从B取能获得的最大能量状态转换方程…...

有道小P 1.0.8 | 完全免费的AI全科学习助手,家长的好帮手
有道小P是一款由网易有道出品的完全免费的AI全科学习助手,专为中小学生设计。它支持多种输入方式,包括文字、语音和拍照识别,能够覆盖十个科目的所有题型,提供详细的解析和逐步解答,帮助孩子们理解和吸收知识。此外&am…...

vue项目中如何在路由变化时增加一个进度条
在 Vue.js 项目中,使用路由(如 Vue Router)时,为了提升用户体验,你可能会想要在路由变化时显示一个进度条。这可以通过多种方式实现,其中一种流行的做法是使用第三方库,如 vue-loading-bar 或 n…...

如何解决mingw64安装后配置完环境变量仍然执行不了gcc命令以及Vscode中的环境路径配置中找不到gcc
配置环境变量教程很多,就不多说,说下耗费一小时解决的问题:mingw64安装后配置完环境变量仍然执行不了gcc命令 配置 了N次了,都还是在终端找不到指令,然后,将路径放到第一个,然后再看下…...

3-petalinux2018.3 摸索记录 - 命令驱动 _ 交叉编译链
一、命令行控制GPIO 对于ps端设备,在板卡的linux系统中,切换到/sys/class/gpio路径下可以看到目前挂载的gpio设备。 export: 导入用户空间 gpiochip: 系统中gpio寄存器信息 unexport: 移除用户空间 以MIO40和MIO42…...

【二分查找】——模板
二分查找模板题 一、题目要求 给定一个长度为n的非递减数组和一个数字target,要求找到数组中第一个大于等于target的位置pos,数组下标从 0 开始。如果不存在大于等于target的数字,则输出 -1。 二、输入格式 第一行:为两个正整…...

从可逆计算看DSL的设计要点
低代码平台的可视化设计器本质上是DSL(Domain Specific Language)的结构化编辑器。可视化设计器将编辑的结果序列化成文本格式时所采用的规范就是一种DSL语法定义。 Nop平台基于可逆计算原理,提出了一整套系统化的构建机制来简化DSL的设计和…...

axios竟态问题
竟态问题 在我们日常开发经常遇到一些竟态问题 例子1 现象1 表格分页,如果设置请求loading, 先切换到分页第99页,迅速在又切换到第1页,最后列表显示的是第99页数据。 原因 由于第99页请求数据花费时间可能500ms,第1页数据只需要100ms,第1页…...

如何批量注册多个Outlook邮箱账号并避免关联
批量注册多个Outlook邮箱账号时,如何避免账号之间的关联性是一个重要的考量因素。会在此文一起探讨如何高效且安全地批量注册多个Outlook邮箱账号,并提供一些实用的建议来确保这些账号不会被关联。 一、Outlook邮箱批量注册机制 在深入注册流程之前&…...

如何在安卓設備上設置全局代理?
對於安卓用戶來說,設置全局代理是維護網路隱私一種有效的方法,可以幫助在所有應用中使用同一個代理伺服器。本文將詳細介紹如何在安卓設備上進行全局代理設置。全局代理指的是通過一個代理伺服器來轉發設備上所有應用程式的網路請求。這樣,所…...

操作系统实验记录
实验零:虚拟机安装 一、安装vmware虚拟机 与vmware匹配搜索结果 - 考拉软件 (rjctx.com),下载17.5.1版本即可下载后对照教程安装 二、下载iso虚拟驱动 搜索清华大学镜像网站,点击再搜ubuntu,下载这个4.1GB的iso文件安装后打开vmware虚拟机 三、配置vmware虚拟机 右键管…...

FastAPI 路径参数详解:动态路径与数据校验的灵活实现
FastAPI 路径参数详解:动态路径与数据校验的灵活实现 本文全面介绍了在 FastAPI 中使用路径参数的技巧和实现方式。路径参数允许 API 动态响应不同路径中的请求信息,结合 URL(Uniform Resource Locator)和 URI(Unifor…...

【STM32】SD卡
(一)常用卡的认识 在学习这个内容之前,作为生活小白的我对于SD卡、TF卡、SIM卡毫无了解,晕头转向。 SD卡:Secure Digital Card的英文缩写,直译就是“安全数字卡”。一般用于大一些的电子设备比如:电脑、数码相机、AV…...

我一口气记录下整个接口自动化测试过程!
一、为什么选用postman postman调试工具无论对于开发和测试小白,还是技术大牛来说应该都耳熟能详,在过去的几年里大家对这款工具应用最广的用途是把当作接口调试的测试工具,它能发送几乎所有类型的HTTP请求,操作界面非常简洁美观…...

【VS中Git同步提交 报错:访问.vs/FileContentIndex/xxx.vsidx权限不允许】
参考: Git commit vsidx file access denied in Visual Studio 一劳永逸的方法: 在VSCode里,Git->设置->选项:编辑.gitignore文件,如下图: 忽略整个.vs文件夹,再重新提交就不会有涉及…...

Linux下Nginx的安装与使用
Linux下Nginx的安装与使用 博客: www.lstar.icu 开源地址 Gitee 地址: https://gitee.com/lxwise/iris-blog_parent Github 地址: https://github.com/lxwise/iris-blog_parent 序言 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子…...

飞机布雷盖航程公式
飞机布雷盖航程公式 1. 喷气式飞机布雷盖航程公式推导2. 螺旋桨飞机布雷盖航程公式推导3. 喷气式飞机与螺旋桨飞机的差异分析3.1 喷气式飞机的推力产生机制3.2 螺旋桨推进推力产生机制 布雷盖航程公式(Breguet Range Equation)是描述飞行器巡航飞行阶段航…...

在K8s平台部署个人博客
在K8s平台部署个人博客 实验步骤查看wordpress前端的service浏览器访问http://node_ip:30090 实验步骤 kubectl create secret generic mysql-pass --from-literalpasswordYOUR_PASSWORD把mysql.tar.gz和wordpress.tar.gz上传到K8s工作节点,手动解压即可࿱…...

git入门教程10:git性能优化
一、配置优化 使用SSH协议: 相比HTTP/HTTPS协议,SSH协议在网络传输中更高效,且支持更安全的认证方式。确保你的远程仓库URL使用的是SSH协议,例如:git clone gitgithub.com:username/repo.git。 调整Git缓冲区大小&…...

Redis(2):内存模型
一、Redis内存统计 工欲善其事必先利其器,在说明Redis内存之前首先说明如何统计Redis使用内存的情况。 在客户端通过redis-cli连接服务器后(后面如无特殊说明,客户端一律使用redis-cli),通过info命令可以查看内存使用情…...

深入解析Diffusion和AsymmDiT:Mochi 1的高效AI视频生成之路
随着AI视频生成技术的迅猛发展,各种模型纷纷涌现,各自展现出独特的优势。近期,Genmo 推出了新一代视频生成模型——Mochi 1,以其非对称架构设计和高效生成流程在业界备受瞩目。作为开源模型,Mochi 1不仅在视觉生成质量…...

VMware capacity mismatch for disk错误解决办法:kb-vuln-1靶机
https://www.vulnhub.com/entry/kb-vuln-1,540/ 本机安装有: VMware Workstation 16 Pro 16.2.1 build-18811642VirtualBox 图形用户界面 版本 5.2.30 r130521 (Qt5.6.2) vm16.2支持wsl2,所以我得让vm16.2跑靶机,VirtualBox5.2可以导入靶机,但是无法开机(不支持wsl2),得升级 …...

Java Collection/Executor LinkedTransferQueue 总结
前言 相关系列 《Java & Collection & 目录》《Java & Executor & 目录》《Java & Collection/Executor & LinkedTransferQueue & 源码》《Java & Collection/Executor & LinkedTransferQueue & 总结》《Java & Collection/Execu…...

阿拉伯国家本地化测试的特点
针对阿拉伯国家的应用程序的本地化测试需要详细了解语言、文化背景、地区规范和技术细节,以符合阿拉伯语用户的期望。这些国家包括沙特阿拉伯、阿拉伯联合酋长国、科威特、卡塔尔、巴林和阿曼,具有独特的语言和文化因素,成功地本地化测试解决…...