14. C++继承与虚函数

news2024/10/24 20:21:38


【继承基础概念】

继承可以让本类使用另一个类的非私有成员,提供共用成员的类称为父类或基类,使用共用成员的类称为子类或派生类,子类创建对象时会包含继承自父类的成员。

继承的优势是减少重复定义数据,当本类需要在另一个类的基础上开发新功能时可以使用继承,这样可以简化代码并节省内存。

若父类中的某些成员禁止被外界使用,可以定义为私有成员,若在禁止外界使用时却允许被子类使用,可以定义为保护成员。

重设访问权限

子类继承自父类的成员需要重新设置在子类的访问权限,重设关键词如下:
public,继承的成员保持原有访问权限,公有成员还是公有权限,保护成员还是保护权限。
private,继承的成员设置为私有权限。
protected,继承的成员设置为保护权限。

若不重设访问权限,则默认为私有权限。

子类创建对象

子类创建对象时,并非将继承自父类的成员合并到子类然后创建合并后的对象,而是会首先创建父类对象,然后创建子类对象,在子类对象中调用父类成员时会转换为调用自动创建的父类对象成员,虽然子类不会继承父类私有成员,但是自动创建的父类对象包含父类私有成员,这是为了被父类公有成员调用,每个子类对象都有自己专用的父类对象。

自动创建父类对象时,其构造函数由子类构造函数调用,若父类有构造函数而子类没有,编译器会为子类自动创建一个构造函数,作用只是调用父类构造函数执行,子类构造函数会首先调用父类构造函数执行,之后再执行子类构造函数自身代码,若两个构造函数功能有冲突,则以最后执行的子类构造函数为准。

若父类构造函数无参数,则子类构造函数无需手动管理父类构造函数,编译器会自动在子类构造函数中调用父类构造函数。
若父类构造函数有参数,则子类构造函数需要为父类构造函数参数赋值,此时必须手动定义子类构造函数并为父类构造函数参数赋值,编译器自动生成的子类构造函数无法为父类构造函数参数赋值。

子类对象使用完毕后,子类析构函数负责调用父类析构函数,若父类有析构函数而子类没有则编译器自动创建一个子类析构函数,作用只是调用父类析构函数执行。
子类析构函数会在自身代码执行完毕后调用父类构造函数,若两者功能有冲突则以最后执行的父类析构函数为准。


子类创建对象时不能直接赋值,因为原理上子类需要同时为继承自父类的成员数据赋值,但这会与父类构造函数产生冲突,即使父类没有构造函数也是如此,这样限制的目的是统一语法规则。

子类对象可以使用如下方式赋值:
1.为成员数据设置默认值。
2.使用构造函数赋值。
3.使用其它公有函数赋值,创建子类对象后手动执行此函数。


在C++中有3种对象不能在创建时直接赋值:
1.包含私有成员数据的对象。
2.有构造函数的对象。
3.子类对象。

#include <iostream>
class base
{
protected:
	int a,b;
	
public:
	base(int i1, int i2)
	{
		a = i1;
		b = i2;
		
		printf("父类构造函数\n");
	}
	
	~base()
	{
		printf("父类析构函数\n");
	}
	
	void add() const
	{
		printf("a+b=%d\n", a+b);
	}
};

/* derive继承base类,继承成员保持原有访问权限 */
class derive : public base
{
public:
	
	/* 父类构造函数有参数,需要手动调用父类构造函数,并为其参数赋值 */
	derive(int i1, int i2) : base(0,0)
	{
		a = i1;
		b = i2;
		
		printf("子类构造函数\n");
	}
	
	~derive()
	{
		printf("子类析构函数\n");
	}
	
	void mul() const
	{
		printf("a*b=%d\n", a*b);
	}
};

int main()
{
	derive derobj(1,2);    //创建子类对象,观察父类与子类构造函数与析构函数的执行顺序
	derobj.add();
	derobj.mul();
	
	return 0;
}

对象类型转换

子类对象可以转换为父类类型,转换后的子类对象将丢失自建成员,只保留继承自父类的成员,所以父类对象可以引用子类对象赋值,编译器会自动将子类对象转换为父类类型。

父类对象不能转换为子类类型,因为父类中不包含子类成员,子类对象不能引用父类对象赋值。

若需要手动转换子类对象类型,可以使用如下语法: (类型名)对象名。

#include <iostream>
class base
{
public:
	base()
	{
		printf("base构造函数\n");
	}
	~base()
	{
		printf("base析构函数\n");
	}
	
	void output()
	{
		printf("父类\n");
	}
};

class derive : public base
{
public:
	derive()
	{
		printf("derive构造函数\n");
	}
	~derive()
	{
		printf("derive析构函数\n");
	}
	
	void output()    //子类可以定义与父类同名的函数
	{
		printf("子类\n");
	}
};

int main()
{
	derive derobj;
	derobj.output();            //调用子类同名函数
	((base)derobj).output();    //转换为父类类型,调用父类同名函数
	
	return 0;
}

子类对象转换为父类类型后,会额外执行一遍父类析构函数,上述代码中,base析构函数会执行两遍。

对象指针类型转换

子类类型指针,可以自动转换为父类类型,但是此时不能通过指针调用子类对象成员,可以将指针再次转换为子类类型从而调用子类对象成员。

父类类型指针,不能自动转换为子类类型,因为父类对象不包含子类成员,若使用代码手动转换则通过指针调用子类成员时会出错。


【多继承】

间接多继承

继承可以一直传递下去,比如A派生B,B派生C,那C也会间接继承A,创建C对象时会自动创建A、B两个对象,C调用B构造函数,B调用A构造函数。

#include <iostream>
class baseA
{
protected:
	int a,b;
	
public:
	baseA(int i1, int i2)
	{
		a = i1;
		b = i2;
		printf("baseA构造函数\n");
	}
	~baseA()
	{
		printf("baseA析构函数\n");
	}
	
	void add() const
	{
		printf("a+b=%d\n", a+b);
	}
};

class baseB : public baseA
{
public:
	baseB(int i1, int i2) : baseA(0,0)
	{
		a = i1;
		b = i2;
		printf("baseB构造函数\n");
	}
	~baseB()
	{
		printf("baseB析构函数\n");
	}
	
	void sub() const
	{
		printf("a-b=%d\n", a-b);
	}
};

class derive : public baseB
{
public:
	derive(int i1, int i2) : baseB(0,0)
	{
		a = i1;
		b = i2;
		printf("derive构造函数\n");
	}
	~derive()
	{
		printf("derive析构函数\n");
	}
	
	void mul() const
	{
		printf("a*b=%d\n", a*b);
	}
};

int main()
{
	derive derobj(1,2);
	derobj.add();
	derobj.sub();
	derobj.mul();
	
	return 0;
}


间接多继承时,可继承成员由直接继承的父类决定,而非更上层的父类决定。

#include <iostream>
class baseA
{
public:
	void baseAput()
	{
		printf("baseA\n");
	}
};

class baseB : private baseA    //继承成员重设为私有权限
{
public:
	void baseBput()
	{
		printf("baseB\n");
	}
};

class derive : public baseB
{
public:
	void derput()
	{
		//baseAput();    //错误,虽然baseAput定义为公有权限,但是被baseB重设为私有权限,derive继承baseB,不能调用baseB私有成员
		baseBput();
	}
};

int main()
{
	derive derobj;
	derobj.derput();
	
	return 0;
}

直接多继承

一个类可以直接继承多个类,此时每个父类都可以单独重设成员访问权限,子类创建对象时会自动创建所有父类的对象,每个父类对象的构造函数都由此子类构造函数调用,析构函数同理。

#include <iostream>
class baseA
{
protected:
	int a,b;
	
public:
	baseA(int i1, int i2)
	{
		a = i1;
		b = i2;
		
		printf("baseA构造函数\n");
	}

	int add() const
	{
		return a+b;
	}
};

class baseB
{
protected:
	float a,b;
	
public:
	baseB(float f1, float f2)
	{
		a = f1;
		b = f2;
		
		printf("baseB构造函数\n");
	}

	float add() const
	{
		return a+b;
	}
};

class derive : public baseA, public baseB
{
public:
	derive(int i1, int i2, float f1, float f2) : baseA(0,0), baseB(0,0)
	{
		baseA::a = i1;
		baseA::b = i2;
		baseB::a = f1;
		baseB::b = f2;
		
		printf("derive构造函数\n");
	}

	//......
};

int main()
{
	derive derobj(1, 2, 1.3, 1.5);
	
	printf("整数加法结果:%d\n"
		"小数加法结果:%f\n",
		((baseA)derobj).add(), ((baseB)derobj).add());
	
	return 0;
}

直接多继承很容易导致混乱,尤其是在多层继承关系中,在大型项目中经常搞不清一个类的所有上级父类又直接继承了多少个类,类成员的管理非常麻烦,很多高级编程语言都会禁用直接多继承,若你非常注重程序性能,其实使用C语言更合适,而非使用C++直接多继承。

菱形继承

菱形继承是一种复杂的多继承,继承关系网组成一个菱形,具体方式为:A派生出B和C,D又同时继承B和C,D创建对象时会自动创建B、C两个对象,B、C又会各自创建一个A对象,此时D对象就有两个可以使用的A对象,这将导致混乱。

为了解决混乱问题,C++规定在菱形继承关系中B和C继承A时需要添加virtual关键词定义为虚继承,此时创建D对象时只会创建一个A对象,并且三个父类的构造函数都将由D负责调用,B和C的构造函数不再调用A构造函数,等同于将代码转换为D直接继承A、B、C三个类,但是单独创建B或C对象时不受影响。

#include <iostream>
class baseA
{
public:
	baseA()
	{
		printf("baseA构造函数\n");
	}
	//......
};

class baseB : virtual public baseA    //虚继承
{
public:
	baseB()
	{
		printf("baseB构造函数\n");
	}
	//......
};

class baseC : virtual public baseA    //虚继承
{
public:
	baseC()
	{
		printf("baseC构造函数\n");
	}
	//......
};

class derive : public baseB, public baseC
{
public:
	derive()
	{
		printf("derive构造函数\n");
	}
};

int main()
{
	derive derobj;
	
	return 0;
}


【虚函数】

C语言通过函数实现程序功能模块化,程序经常需要使用函数指针调用不同的模块,C++将函数封装在类中管理,函数指针只能指向本类的成员函数,无法像C语言那样使用函数指针随意调用函数,为此C++增加了虚函数功能,虚函数的作用是通过指针调用同源继承关系中所有类的成员函数。

虚函数是父类中定义的特殊函数,子类继承后可以直接使用,也可以重写内部代码,但是不能改变虚函数的参数和返回值,否则就不能实现使用函数指针统一调用。

虚函数使用父类对象指针调用执行,编译器会转换为通过函数指针调用虚函数(而普通函数会转换为直接调用),父类指针可以赋值为子类对象地址,赋值为哪个子类对象地址就会调用哪个子类重写的虚函数,同时虚函数的this参数也会赋值为所在类的对象地址,若子类没有重写虚函数则调用父类定义的虚函数。

虚函数定义方式如下:
1.父类在函数返回值之前使用virtual关键词定义虚函数。
2.子类重写虚函数时可以添加virtual关键词,也可以不添加,为了方便识别虚函数一般会加上。
3.子类重写虚函数时可选在参数之后添加override关键词,用于强制编译器检查重写的虚函数,若重写的虚函数与原型不同(参数、返回值不同),则编译报错。


注:虚函数可以当做普通函数使用,直接使用函数名调用它,此时等于不使用虚函数机制,与使用普通函数无异。

#include <iostream>
class base
{
public:
	
	/* 父类定义虚函数 */
	virtual void f1()
	{
		//......
		printf("通用模块\n");
	}
	
	/* 父类定义虚析构函数,原因之后介绍 */
	virtual ~base(){}
};

class deriveA : public base
{
public:
	
	/* 子类重写虚函数 */
	virtual void f1() override
	{
		//......
		printf("业务模块1\n");
	}
};

class deriveB : public base
{
public:
	
	virtual void f1() override
	{
		//......
		printf("业务模块2\n");
	}
};

class deriveC : public base
{
public:
	
	virtual void f1() override
	{
		//......
		printf("业务模块3\n");
	}
};

int main()
{
	base baseobj;
	deriveA derAobj;
	deriveB derBobj;
	deriveC derCobj;
	
	base * p1;    //定义父类对象指针
	
	int i;
	scanf("%d", &i);    //模块调用变量
	
	if(i == 1)
	{
		p1 = &derAobj;
	}
	else if(i == 2)
	{
		p1 = &derBobj;
	}
	else if(i == 3)
	{
		p1 = &derCobj;
	}
	else
	{
		p1 = &baseobj;
	}
	
	p1->f1();    //使用父类指针调用虚函数
	
	return 0;
}

上述C++代码功能等同于如下C语言代码:

#include <stdio.h>
struct k
{
	//......
};
void f0(struct k * this)
{
	//......
	printf("通用模块\n");
}
void f1(struct k * this)
{
	//......
	printf("业务模块1\n");
}
void f2(struct k * this)
{
	//......
	printf("业务模块2\n");
}
void f3(struct k * this)
{
	//......
	printf("业务模块3\n");
}
int main()
{
	void (*p1)();
	
	int i;
	scanf("%d", &i);
	
	if(i == 1)
	{
		p1 = f1;
	}
	else if(i == 2)
	{
		p1 = f2;
	}
	else if(i == 3)
	{
		p1 = f3;
	}
	else
	{
		p1 = f0;
	}
	
	p1();    //使用函数指针统一调用模块
	
	return 0;
}


禁止虚函数重写

虚函数机制会随继承关系遗传下去,若一个类间接继承了提供虚函数的父类,则此类也会有虚函数,也可以使用虚函数机制。

若一个类希望虚函数机制到此为止,其子类不再重写、不再使用虚函数,可以在此类的虚函数中添加final关键词。

virtual void f1() override final { //...... }


虚析构函数

若使用虚函数机制的子类对象需要使用new申请内存进行存储,将此子类对象地址赋值给父类类型指针后,对象使用完毕执行delete释放内存时会出现如下情况。

#include <iostream>
class base
{
public:
    
    ~base()
    {
        printf("base析构函数\n");
    }
    
    virtual void f1()
    {
        printf("base虚函数\n");
    }
};

class derive : public base
{
public:
    
    ~derive()
    {
        printf("derive析构函数\n");
    }
    
    virtual void f1() override
    {
        printf("derive虚函数\n");
    }
};

int main()
{
    derive *p1 = new derive;
    
    base *p2 = p1;
    p2->f1();
    
    delete p2;    //通过父类指针释放子类对象,只会执行父类析构函数
    
    return 0;
}

上面代码中,子类类型指针p1赋值给父类类型指针p2,两个指针指向同一个对象,使用delete释放内存时,原理上通过任何一个指针释放都可以,但是释放对象之后还会涉及到自动执行析构函数的问题,p2是父类类型,通过p2释放内存时编译器只会调用父类析构函数,不会执行子类析构函数,所以实际上只能通过子类类型指针释放内存,或者将父类类型指针强制转换为子类类型再释放,这限制了C++代码的灵活性,为此C++规定使用虚函数机制时父类需要定义一个虚析构函数(即使此函数什么也不做),此时通过父类类型指针释放子类对象时会调用子类析构函数。

#include <iostream>
class base
{
public:

    /* 父类定义虚析构函数 */
    virtual ~base()
    {
        printf("base析构函数\n");
    }
    
    virtual void f1()
    {
        printf("base虚函数\n");
    }
};

class derive : public base
{
public:
    
    ~derive()
    {
        printf("derive析构函数\n");
    }
    
    virtual void f1() override
    {
        printf("derive虚函数\n");
    }
};

int main()
{
    derive *p1 = new derive;
    
    base *p2 = p1;
    p2->f1();
    
    delete p2;    //通过父类类型指针释放子类对象,会调用子类析构函数
    
    return 0;
}


纯虚函数

使用虚函数机制时,若父类不需要创建对象,父类虚函数也不需要直接调用,父类存在的唯一作用就是提供父类类型指针,从而统一调用子类重写的虚函数,此时可以将父类虚函数定义为纯虚函数,纯虚函数只有函数主体代码,没有任何内容,定义有纯虚函数的类称为抽象类。

纯虚函数与虚函数的区别如下:
1.包含纯虚函数的类不能创建对象。
2.纯虚函数没有内容,子类必须重写纯虚函数,否则子类的虚函数也是纯虚函数,子类也不能创建对象。

class base
{
public:
    
    /* 定义纯虚函数,=0表示纯虚函数 */
    virtual void f1() = 0;
    
    /* 抽象类也需要定义虚析构函数 */
    virtual ~base()
    {
        printf("base析构函数\n");
    }
};


 

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

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

相关文章

变量提升,函数提升

一、变量提升 只有var存在变量提升。变量提升就是将变量提至当前作用域的最前面&#xff0c;只提升声明&#xff0c;不提升赋值。 console.log(n) // undefined&#xff0c;不会报错 var n 10 等价于 var n console.log(n) // undefined&#xff0c;不会报错 n 10 因为n是…

【Spring底层原理高级进阶】Spring Batch清洗和转换数据,一键处理繁杂数据!Spring Batch是如何实现IO流优化的?本文详解!

&#x1f389;&#x1f389;欢迎光临&#xff0c;终于等到你啦&#x1f389;&#x1f389; &#x1f3c5;我是苏泽&#xff0c;一位对技术充满热情的探索者和分享者。&#x1f680;&#x1f680; &#x1f31f;持续更新的专栏《Spring 狂野之旅&#xff1a;从入门到入魔》 &a…

算法题 — 三个数的最大乘机

三个数的最大乘机 整型数组 nums&#xff0c;在数组中找出由三个数字组成的最大乘机&#xff0c;并输出这个乘积。&#xff08;乘积不会越界&#xff09; 重点考察&#xff1a;线性扫描 排序法&#xff1a; public static void main(String[] args) {System.out.println(so…

海外IP代理应用:亚马逊使用什么代理IP?

代理IP作为网络活动的有力工具&#xff0c;同时也是跨境电商的必备神器。亚马逊作为跨境电商的头部平台&#xff0c;吸引了大量的跨境电商玩家入驻&#xff0c;想要做好亚马逊&#xff0c;养号、测评都需要代理IP的帮助。那么应该使用什么代理IP呢&#xff1f;如何使用&#xf…

Jmeter高效组织接口自动化用例

1、善用“逻辑控制器”中的“简单控制器”。可以把简单控制器像文件夹一样使用&#xff0c;通过它来对用例进行分类归档&#xff0c;方便后续用例的调试和执行。 2、同编写测试用例一样&#xff0c;这里的接口测试用例应该进行唯一性编号&#xff0c;这样在运行整个用例计划出现…

《TCP/IP详解 卷一》第15章 TCP数据流与窗口管理

目录 15.1 引言 15.2 交互式通信 15.3 延时确认 15.4 Nagle 算法 15.4.1 延时ACK与Nagle算法结合 15.4.2 禁用Nagle算法 15.5 流量控制与窗口管理 15.5.1 滑动窗口 15.5.2 零窗口与TCP持续计时器 15.5.3 糊涂窗口综合征 15.5.4 大容量缓存与自动调优 15.6 紧急机制…

汽车小车车灯无痕修复用的胶是什么胶?

汽车小车车灯无痕修复用的胶是什么胶&#xff1f; 可以使用在小车车灯无痕修复中的胶水&#xff0c;通常使用的车灯无痕修复专用UV胶。 车灯无痕修复专用胶主要成份是改性丙烯酸UV树脂&#xff0c;主要应用在车灯的专业无痕修复领域。它可以用于修复车灯壳的裂缝或破损&#xf…

阿珊解析Vuex:实现状态管理的利器

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

如何将字体添加到 ONLYOFFICE 桌面编辑器8.0

作者&#xff1a;VincentYoung 为你写好的文字挑选一款好看的字体然而自带的字体列表却找不到你喜欢的怎么办&#xff1f;这只需要自己手动安装一款字体即可。这里教你在不同的桌面操作系统里的多种字体安装方法。 ONLYOFFICE 桌面编辑器 ONLYOFFICE 桌面编辑器是一款免费的办…

第一讲 计算机组成与结构(初稿)

计算机组成与结构 计算机指令常见CPU寄存器类型有哪些&#xff1f;存储器分类&#xff1f;内存&#xff1f;存储器基本组成&#xff1a; 控制器的基本组成主机完成指令的过程以取数指令为例以存数指令为例ax^2bxc程序的运行过程 机器字长存储容量小试牛刀&#xff08;答案及解析…

【腾讯云】 爆款2核2G3M云服务器首年 61元,叠加红包再享折上折

同志们&#xff0c;云服务器行业大内圈&#xff0c;腾讯云各个活动都已开始卷中卷&#xff0c;我整理一下各个活动&#xff0c;加油冲了 【腾讯云】 爆款2核2G3M云服务器首年 61元&#xff0c;叠加红包再享折上折&#xff0c;最低只要51 【腾讯云】多款热门AI产品新春巨惠&…

计网《一》|互联网结构发展史|标准化工作|互联网组成|性能指标|计算机网络体系结构

计网《一》| 概述 计算机网络在信息时代的作用什么是互联网呢&#xff1f;互联网有什么用呢&#xff1f;为什么互联网能为用户提供许多服务 互联网基础结构发展的三个阶段第一个阶段&#xff1a;第二阶段&#xff1a;第三个阶段&#xff1a; 互联网标准化的工作互联网的组成边缘…

CCF-B推荐会议 Euro-Par‘24延期10天! 3月25日截稿!抓住机会!

会议之眼 快讯 第30届Euro-Par(International European Conference on Parallel and Distributed Computing)即国际欧洲并行和分布式计算会议将于 2024 年 8月26日-30日在西班牙马德里举行&#xff01;Euro-Par是欧洲最主要的会议之一&#xff0c;提供了一个广泛而综合的平台&a…

数字孪生10个技术栈:数据处理的六步骤,以获得可靠数据。

一、什么是数据处理 在数字孪生中&#xff0c;数据处理是指对采集到的实时或历史数据进行整理、清洗、分析和转化的过程。数据处理是数字孪生的基础&#xff0c;它将原始数据转化为有意义的信息&#xff0c;用于模型构建、仿真和决策支持。 数据处理是为了提高数据质量、整合数…

Java面试挂在线程创建后续,不要再被八股文误导了!创建线程的方式只有1种

写在开头 在上篇博文中我们提到小伙伴去面试&#xff0c;面试官让说出8种线程创建的方式&#xff0c;而他只说出了4种&#xff0c;导致面试挂掉&#xff0c;在博文中也给出了10种线程创建的方式&#xff0c;但在文章的结尾我们提出&#xff1a;真正创建线程的方式只有1种&…

Kakarot:当今以太坊的未来

1. 引言 前序博客&#xff1a; Kakarot&#xff1a;部署在Starknet上的ZK-EVM type 3 随着 Kakarot zkEVM 即将发布测试网&#xff0c;想重申下 Kakarot zkEVM 的愿景为&#xff1a; 为什么在rollup空间中还需要另一个 zkEVM&#xff1f; 开源代码见&#xff1a; https:/…

第三百八十七回

文章目录 1. 概念介绍2. 使用方法3. 示例代码 我们在上一章回中介绍了DateRangePickerDialog Widget相关的内容,本章回中将介绍Radio Widget.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在这里说的Radio Widget是指单选按钮&#xff0c;没有选中时是圆形…

PEIS源码 健康体检中心源码 C/S

目录 一、系统概述 二、系统开发环境 三、系统功能 检前管理 检中管理 检后管理 设备对接-PACS 设备对接-彩超 LIS-结果录入、审核、外送结果自动导入 一、系统概述 体检系统&#xff0c;是专为体检中心/医院体检科等体检机构&#xff0c;专门开发的全流程管理系…

创建Net8WebApi自动创建OpenApi集成swagger

问题&#xff1a;用Net8创建WebAPI时勾选启动OpenAPI&#xff0c;项目自动集成了Swagger&#xff0c;但是接口注释没有展示&#xff1f; 解决&#xff1a; 1.需要生成Api项目的XML文件。操作如下&#xff1a; 2.生成XML文件后&#xff0c;还需要在启动类Program.cs里面配置Sw…

快速批量将图片变成圆角怎么弄?教你一键将图片批量加圆角

在我们日常工作中&#xff0c;在设计图片的时候会要求将直角变成圆角&#xff0c;那么为什么要这么做呢&#xff1f;首先从圆角的设计语言上来说说&#xff0c;圆角看起来很现代&#xff0c;传达给人的感觉是温和友善的&#xff0c;被广泛的应用在产品中的图标、按钮等地方。而…