【C++从0到王者】第二十三站:多态的概念、定义以及实现

news2024/11/24 5:33:00

文章目录

  • 前言
  • 一、多态的概念
  • 二、多态的定义及实现
    • 1.虚函数
    • 2.虚函数重写
    • 3.多态的两个条件
    • 4.虚函数重写的两个例外
    • 5.前四点的一些总结
    • 6.析构函数的重写(虚函数重写的第三个例外)
    • 7. C++11之override 和 final
    • 8.如何设计一个类,使得这个类不会被继承
    • 9.重载、隐藏/重定义、重写/覆盖的区别
  • 总结


前言

在现实生活中,除了继承,还有一种场景是多态,所谓多态,就是多种形态


一、多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。

二、多态的定义及实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

1.虚函数

虚函数:即被virtual修饰类成员函数称为虚函数

像如下的写法是错误的,因为这个func必须是成员函数
在这里插入图片描述

像如下的写法就是正确的虚函数写法

2.虚函数重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
要注意的是,这里是重写/覆盖,不是重定义/隐藏,重定义/隐藏是继承中的,子类的成员名与父类的成员名相同的时候,子类会覆盖掉父类,只需要名字相同即可,而这里是都要相同

如下所示,就是重写

class Person {
public:
	virtual void BuyTicket() const { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() const  { cout << "买票-半价" << endl; }
};
void Func(const Person& p)
{
	p.BuyTicket();
}
int main()
{
	Student s;
	Person p;
	Func(p);
	Func(s);
	return 0;
}

在这里插入图片描述

这就是多态,不同的对象执行的结果不一样

但看代码,还是会觉得很神奇的,因为毕竟形参是一个Person类型的对象,居然能调用Student里面的函数。

其实虽然形参是Person类型的,这里的引用切割以后,只是让p指向了s中的Person的那一部分
多态调用看的是指向的对象的类型
普通对象看的是当前者的类型

3.多态的两个条件

多态有两个很重要的条件

  1. 调用函数是重写的虚函数(注意重写的条件是虚函数+三同)
  2. 基类指针或者引用

当我们将引用去掉以后,这里其实就变成了一个对象的切割/切片了。那么此时这个p就只能调用自己里面的函数了,不构成多态了。多态的两个重要条件里面的第二个就不满足了
在这里插入图片描述

如果将父类的virtual给去掉了,那么最终也是不构成多态的,不满足第一个多态的条件
在这里插入图片描述

总之必须得遵循以上两个条件

除此之外,我们调用的时候是正常的调用,不能是指定调用。如果是指定调用的话,那当然不会触发多态了
在这里插入图片描述

上面的例子都是引用的例子,下面来一个指针的例子

如下所示,指针或引用都是可以触发多态的
在这里插入图片描述

4.虚函数重写的两个例外

重写的条件本来是虚函数加三同,但是有一些例外

  1. 子类可以不加virtual,但是父类必须加上去

如下所示,是第一种情况的例子,子类可以不加virtual。但是父类必须加上去。
在这里插入图片描述

原因其实也是比较好想的,因为父类已经加上了virtual,这里的派生类继承了父类以后,里面是有virtual的,这里的多态其实只是改变的函数的实现

关于这一个例外,建议全部加上virtual,但是实际上子类不加上virtual也是可以的。

  1. 协变(返回的值可以不同,但是要求返回值必须是父子关系的指针或者引用)

注意,这一点例外,是C++的大坑之一。

首先正常的返回值不同是会报错的。如下所示,显示重写的虚函数返回值有差异,且不是协变
在这里插入图片描述

也就是说,返回值不同的时候,必须满足协变,否则报错

如下就是满足了协变的条件,可以正常实现多态
在这里插入图片描述

注意,这里的父子类关系的指针,不是必须是自己的,也可以是其他类的,只要满足是父子类关系的指针或引用都是可以的
在这里插入图片描述

还有一点要注意的是,不可以一个是指针,一个是引用,必须同时是指针,或者同时是引用
在这里插入图片描述

还有一个细节需要注意,必须父类中的返回值是父类的指针或者引用,子类的返回值是子类的指针或者引用。不可以反过来
在这里插入图片描述

5.前四点的一些总结

看完前四点,或许已经懵了,但是我们还是要抓住大方向
我们的目的是为了实现多态,而多态的实现有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(也就是要调用重写的虚函数)

在这两个条件下,第一点还好说,无非就是通过基类的指针或者引用来调用虚函数。

而第二点稍微麻烦点,因为它的条件是调用函数是重写的虚函数。也就是说,想要调用的这个函数,必须是一个虚函数,并且派生类已经实现了重写。

对于前半句,必须是一个虚函数,在这里要注意的细节是,基类中的这个函数必须是加上virtual使其成为虚函数,但是派生类中可以不用加上virtual,但是仍然建议加上,提高代码可读性。

对于后半句,派生类要实现重写,也就是要满足重写的条件,重写的条件本身就是虚函数加上三同(派生类中有一个跟基类完全一样的虚函数,他们的返回值类型,函数名字,参数列表完全相同),重写条件除了这个以外,还有两个例外。第一个例外就是子类可以不用加virtual,这个与前半句是存在一定的重复的。第二个例外就是协变。协变的条件就是返回的值可以不同,但是要求返回值必须是父子关系的指针或者引用。这里的父子关系,可以不是本身的类,但是必须是父对父,子对子且不可以一个是指针,一个是引用。

6.析构函数的重写(虚函数重写的第三个例外)

我们先思考一下,析构函数可以是虚函数吗?为什么要是虚函数?

析构函数加上virtual是不是虚函数重写呢?答案当然是的
因为类析构函数都被处理成了destructor这个统一的名字
那么为什么要这么处理呢?其实也很简单,因为要让他们构成重写(重写的条件无论是三同还是例外,都需要让函数名相同的)
那么为什么要让他们构成重写呢?

我们看到如下代码

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

int main()
{
	Person p;
	Student s;
	return 0;
}

运行结果如下所示,符合我们的预期(子类里面有一个父类,它会像栈一样,先析构后面生成的,在析构前面的。所以先析构子类,然后析构子类中的父类,最后析构父类)
在这里插入图片描述

但是如果我们不加virtual好像也没什么问题
在这里插入图片描述

好像似乎我们不需要让析构函数进行重写也是没有任何问题的?编译器还让这个函数名处理成了destructor,同名了,似乎也还构成了隐藏/重定义的关系。因为隐藏/重定义只需要在继承关系中,成员名相同即可。就可以使得子类可以隐藏父类的成员。

总而言之,似乎C++即便不重写析构函数也是没有任何毛病的啊

其实如果不重写的话,有一个场景是过不去的。

class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() 
	{ 
		cout << "~Student()" << endl;
		delete[] _ptr;
	}
protected:
	int* _ptr = new int[10];
};

int main()
{
	Person* p = new Person;
	delete p;
	p = new Student;
	delete p;
	return 0;
}

运行结果为

在这里插入图片描述

我们已经发现了问题,就是内存泄漏了。

如下图所示,Student中int*所指向的那块空间已经被泄露出去了

在这里插入图片描述

那么为什么会发生内存泄漏呢?其实是因为没有调用到派生类的析构函数。才导致的

那么为什么又调不到派生类的析构函数呢?

我们在前面说过,类的析构函数都被处理成了destructor这个函数。

而delete p的本质其实也可以分为两部分,一部分是调用析构函数,即p->destructor(),另外一部分是调用operator delete§。(这里我们在之前的文章说说过,new对于自定义类型会开空间加构造函数,delete则会析构函数加释放空间)

而这里就巧了,恰好编译器处理成一样的函数名了,加之这里并没有virtual构成重写/覆盖,反而是构成了隐藏/重定义了。而且p刚好是Person类型的指针,是一个普通的调用,不是多态调用。而在前文中也说过:普通调用,看的是当前者的类型。多态调用,看的是其指向的类型,编译器就以为调用的是Person的析构函数了。

但是我们肯定是不期望调用的是Person的析构啊。虽然p这个指针的类型是Person*,但是它是一个基类,它有可能指向一个基类,也有可能指向一个派生类对象。我们希望它指向什么类型,就调用什么类型的析构函数,这样才可以保证不会产生内存泄漏。

而指向什么类型,调用什么类型,这不就正好符合多态调用吗?

那么我们就得知了,加上一个virtual进行修饰,刚好就是满足虚函数+三同,不就正好满足了成为多态的第一个条件(调用重写的虚函数)吗?并且这里的p不就是基类的指针吗,这不也满足了成为多态的第二个条件,必须是基类的指针或者引用去调用这个重写的虚函数。如此一来,刚好满足多态的条件。

满足了多态的条件,就可以解决这里的问题了。

在这里插入图片描述

其实一切的源头还是在于,C++允许了切片的行为。才导致父类指针可以指向一个子类对象,造成的一系列问题。

而C++允许切片的行为,也是由于要实现多态,必须要使用切片。如下所示:

在这里插入图片描述

这样一来,多态也成功的自圆其说了,为了实现多态搞出来的切片和虚函数,但又由于切片导致了析构函数存在内存泄漏的问题,所以便通过多态去解决了这个内存泄漏问题。有点因果循环的意味。

这里其实我们也可以称之为虚函数重写的第三个例外,即析构函数的重写。从形式上来看他们的函数名是不一样的,不满足三同,但是由于编译器对函数名进行了处理,导致满足了三同,构成了虚函数的重写。从而解决了一类问题。

而虚函数重写的第一个例外是,基类必须加上virtual,但是子类可以不用加。我们是不是可以这样认为,这个例外正好可以为析构函数服务。因为析构函数正常来说是没有返回值的,加上一个virtual其实挺奇怪的。于是我们可以默认说,基类的析构函数最好加上virtual,其他的照常即可。这样也正好就没有这个隐患了。

加上了virtual的话, 会建立一张虚表,会稍微付出一些代价,但是从总体上来看,还是很有必要的,百利而少害!我们可以从库里面看到,有不少析构函数都加上了virtual。

7. C++11之override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
	virtual void Drive() final {};
};
class Benz : public Car
{
public:
	virtual void Drive()
	{
		cout << "123456" << endl;
	}
};

如上代码所示,当我们想要运行的时候,会报错的,因为声明为final的函数是无法被重写的。

在这里插入图片描述

需要注意的是虚函数重写的第一个例外,子类可以不加virtual,因为下面也是构成重写的,所以也会报错。
在这里插入图片描述

但是如果我们直接改变了虚函数加三同的话,它就不会构成重写了,自然也不会报错了,注意这里看似,我们加上了virtual,但是我们并不满足三同,所以这里不构成重写,但是满足函数名相同,满足了隐藏/重定义。这里的virtual的功能虽然不构成重写,但是万一以后有其他类要继承它的时候,有可能会存在这两个构成重写的。

在这里插入图片描述

  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car
{
public:
	void Drive() {};
};
class Benz : public Car
{
public:
	virtual void Drive() override
	{
		cout << "123456" << endl;
	}
};

int main()
{
	return 0;
}

如上代码所示,当我们运行的时候,毫无疑问的报错了,原因也很简单,因为override修饰派生类中的虚函数是否重写了基类中的某个虚函数,如果没有则报错,显然,我们基类没有加上virtual,就会报错了。这里只是构成了隐藏/重定义。
在这里插入图片描述

只要我们构成了重写,那么自然就不会报错了

在这里插入图片描述

8.如何设计一个类,使得这个类不会被继承

我们的第一个方法是:基类构造函数私有化,如下代码所示

class A
{
private:
	A() {};
};
class B : public A
{};
int main()
{
	B b;
	return 0;
}

我们只需要将基类的构造函数给私有化,这样的话,B自然就无法继承A了,因为B在定义对象的时候,会有一个构造函数,这个构造函数无论如何都会默认先走A的构造函数,但是这里我们又将构造函数给屏蔽了。那肯定没法继承了。这里我们定义的对象b也就无法成功通过编译了
在这里插入图片描述

上面好像听着确实挺合理,但是A类型的对象不是也创建不了吗,我们又该如何解决呢?

class A
{
public :
	static A Creatobj()
	{
		return A();
	}
private:
	A() {};
};
class B : public A
{};
int main()
{
	A a = A::Creatobj();
	return 0;
}

如上代码所示,为了解决这个问题,我们决定使用一个函数来返回这个A的匿名对象,这个是传值返回,所以A的匿名对象会先拷贝构造给临时变量,这个临时变量具有常性,然后我们这里看似是一个=,实际上是一个拷贝构造。利用这个临时对象去拷贝构造给这个a对象。不过上面是我们的分析过程,实际上编译器会对这个过程进行优化处理,即拷贝构造+拷贝构造->拷贝构造。

这里我们还需要加上一个static将其变为静态的成员函数,这是为了避免调用这个函数还需要先创建一个对象去调用,而这个对象本身就是为了创建一个对象的,陷入了先有鸡还是先有蛋的纠缠。而我们加上了static以后,就没有了this指针,自然就可以直接使用类域去访问这个函数了。

第二个方案是析构函数私有化

class A
{
public:
private:
	~A() {};
};
class B : public A
{};
int main()
{
	A a;
	B b;
	return 0;
}

如下代码所示,这个代码也是会报错的,我们使用的方案是析构函数私有化。这样一来也是可以的。但是问题又来了,我们又创建不了A类型的对象了

但是我们可以使用new来解决这个问题,我们的问题无非就是这个对象在栈区,它生命周期结束后会自动调用析构函数,但是析构函数被封锁了,导致调用不了,那我们直接将这个对象放在堆区的话,不就可以解决这个问题了吗。不过这样,我们确又释放不了这个内存了,这又该如何是好呢?其实我们可以和构造函数一样的方法,写一个静态的函数destroy去释放这些资源即可。

在这里插入图片描述

第三种方案是基类使用final

前两种方案未免显得太过于繁琐了,在这里C++11的final不仅可以作用域函数,不让虚函数重写,而且还可以不让类继承

class A final
{};
class B : public A
{};
int main()
{
	return 0;
}

在这里插入图片描述

9.重载、隐藏/重定义、重写/覆盖的区别

如下图所示,是三个的区别。需要注意的是重写和重定义有一部分是重合的,因为重定义的要求更简单,即两个基类和派生类的同名函数只要不构成重写就是重定义

在这里插入图片描述


总结

本节我们讲解了多态的概念,多态的实现、以及多态的条件。多条有两大条件:第一大条件就是基类的指针或者引用去调用虚函数,第二大条件就是被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。而虚函数重写又有两个条件:第一个是必须加上virtual使其成为虚函数,第二个就是两个虚函数必须满足三同。只有这样才能实现虚函数的重写。当然除了三同之外,还有三大例外:第一大例外就是基类必须加virtual,但是派生类可以不用加。第二大例外就是满足协变,即返回值可以不同,但必须是父子类关系的指针或者引用。第三大例外就是析构函数的重写中,函数名是不同的,但是可以构成重写,因为最终都处理为了destructor。

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

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

相关文章

三个视角解读ChatGPT在教学创新中的应用

第一&#xff0c;我们正处于一个学生使用ChatGPT等AI工具完成作业的时代&#xff0c;传统的教育方法需要适应变化。 教育工作者不应该因为学生利用了先进技术而惩罚他们&#xff0c;相反&#xff0c;应该专注于让学生去挑战超越AI能力范围的任务。这需要我们重新思考教育策略和…

Java8新特性之——Lambda表达式

文章目录 一、简介二、格式三、举例四、使用场景五、FunctionalInterface注解六、Lambda表达式的简化方式省略参数类型&#xff1a;如果上下文已经明确了参数的类型&#xff0c;可以省略参数类型的声明。省略参数括号&#xff1a;如果只有一个参数&#xff0c;可以省略参数的括…

队列(Queue):先进先出的数据结构队列

栈与队列https://blog.csdn.net/qq_45467165/article/details/127958960?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22127958960%22%2C%22source%22%3A%22qq_45467165%22%7D 队列&#xff08;Queue&#xff09;是一种常见的线…

VS+Qt 自定义Dialog

与QtCreator不同&#xff0c;刚用VS添加Qt Dialog界面有点懵&#xff0c;后整理了下&#xff1a; 1.右击项目&#xff0c;选择“添加-模块”&#xff0c;然后选择“Qt-Qt Widgets Class” 2.选择基类[1]QDialog,更改[2]ui文件名称&#xff0c;修改定义Dialog[3]对应类名&#…

前端工程化之模块化

模块化的背景 前端模块化是一种标准&#xff0c;不是实现理解模块化是理解前端工程化的前提前端模块化是前端项目规模化的必然结果 什么是前端模块化? 前端模块化就是将复杂程序根据规范拆分成若干模块&#xff0c;一个模块包括输入和输出。而且模块的内部实现是私有的&…

c++学习之内存管理

目录 1.c/c内存分布 2.new与delete/malloc与free c内存管理方式&#xff1a; new/delete操作内置类型&#xff1a; new/delete操作自定义类型 operator new与operator delete函数 new和delete的实现原理 内置类型 自定义类型 malloc/free和new/delete的区别 1.c/c内存分…

Linux——初始

linux是一个操作系统&#xff0c;目前主流就是在服务器后端被选作服务器的操作系统来使用&#xff0c;所以我们没有直接接触到。 Linux的历史和概念 先是国家投钱给科研技术人员&#xff0c;科研技术人员将科研成果部分投入生活用品卖给老百姓&#xff0c;老百姓购买产品同时还…

Java课题笔记~ 综合案例

3.综合案例 3.1 功能介绍 以上是我们在综合案例要实现的功能。除了对数据的增删改查功能外&#xff0c;还有一些复杂的功能&#xff0c;如 批量删除、分页查询、条件查询 等功能 批量删除 功能&#xff1a;每条数据前都有复选框&#xff0c;当我选中多条数据并点击 批量删除 按…

YOLOv5算法改进(4)— 添加CA注意力机制

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。注意力机制是近年来深度学习领域内的研究热点&#xff0c;可以帮助模型更好地关注重要的特征&#xff0c;从而提高模型的性能。在许多视觉任务中&#xff0c;输入数据通常由多个通道组成&#xff0c;例如图像中的RGB通道或…

村口的人家排放污水,污水浸染了整个村子,怎么办

从前有一个很不错的村子里&#xff0c;村子里有很多户人家&#xff0c;随着生活水平越来越好&#xff0c;房子也修起来了&#xff0c;柏油马路也宽敞了&#xff0c;大家进出村子&#xff0c;都要走那条马路&#xff0c;要不就出不去。 目录 1. 修厕所 2. 村口的日家 3. 告诉…

商城的TPS与并发用户数是如何换算的?

商城的TPS与并发用户数的换算关系可以通过以下公式计算&#xff1a; TPS 并发用户数 / 平均事务响应时间 其中&#xff0c;平均事务响应时间是指系统处理一个事务所需的平均时间。 下面是商城性能测试的一些用例示例&#xff1a; 用户登录&#xff1a; 目标&#xff1a;测…

4.物联网LWIP之C/S编程,stm32作为服务器,stm32作为客户端,代码的优化

LWIP配置 服务器端实现 客户端实现 错误分析 一。LWIP配置&#xff08;FREERTOS配置&#xff0c;ETH配置&#xff0c;LWIP配置&#xff09; 1.FREERTOS配置 为什么要修改定时源为Tim1&#xff1f;不用systick&#xff1f; 原因&#xff1a;HAL库与FREERTOS都需要使用systi…

【Python原创毕设|课设】基于Python Flask的上海美食信息与可视化宣传网站项目-文末附下载方式以及往届优秀论文,原创项目其他均为抄袭

基于Python Flask的上海美食信息与可视化宣传网站&#xff08;获取方式访问文末官网&#xff09; 一、项目简介二、开发环境三、项目技术四、功能结构五、运行截图六、功能实现七、数据库设计八、源码获取 一、项目简介 随着大数据和人工智能技术的迅速发展&#xff0c;我们设…

【JavaEE进阶】MyBatis表查询

文章目录 一. 使用MyBatis完成数据库的操作1. MyBatis程序中sql语句的即时执行和预编译1.1 即时执行&#xff08;${}&#xff09;1.2 预编译&#xff08;#{}&#xff09;1.3 即时执行和预编译的优缺点 2. 单表的增删改等操作2.1 增加操作2.2 修改操作2.3 删除操作2.4 like(模糊…

星际争霸之小霸王之小蜜蜂(六)--让子弹飞

目录 前言 一、添加子弹设置 二、创建子弹 三、创建绘制和移动子弹函数 四、让子弹飞 五、效果 总结 前言 小蜜蜂的基本操作已经完成了&#xff0c;现在开始编写子弹的代码了。 一、添加子弹设置 在我的预想里&#xff0c;我们的小蜜蜂既然是一只猫&#xff0c;那么放出的子弹…

基于小波神经网络的短时交通流量预测Matlab代码

1案例背景 1.1小波理论 小波分析是针对傅里叶变换的不足发展而来的。傅里叶变换是信号处理领域中应用最广泛的一种分析手段,然而它有一个严重不足,就是变换时抛弃了时间信息,通过变换结果无法判断某个信号发生的时间,即傅里叶变换在时域中没有分辨能力。小波是长度有限、平均为…

分布式与微服务相关知识

分布式与微服务 1.zookeeper是什么2.zookeeper保证数据一致性3.zookeeper的快速领导者选举是怎么实现的4.CAP理论5.BASE理论6.分布式id生成方案&#xff08;1&#xff09;UUID&#xff08;2&#xff09;数据库自增序列&#xff08;3&#xff09;Leaf-segment&#xff08;4&…

Linux下的系统编程——vim/gcc编辑(二)

前言&#xff1a; 在Linux操作系统之中有很多使用的工具&#xff0c;我们可以用vim来进行程序的编写&#xff0c;然后用gcc来生成可执行文件&#xff0c;最终运行程序。下面就让我们一起了解一下vim和gcc吧 目录 一、vim编辑 1.vim的三种工作模式 2.基本操作之跳转字符 &a…

实现外网访问本地服务

最近开发需要其他项目组的人访问我本地服务测试,但又不在同一个地方,不能使用内网访问,所以需要外网访问本地服务功能. 条件: 1.需要一台具备公网IP的服务器 我用的服务器是windows,电脑也是Windows系统 2.下载frp 软件,只需要下载一份就可以了,分别放到服务器上和本地目录既…

2011-2021年全国各省绿色创新效率数据(原始数据+测算结果)

2011-2021年全国各省绿色创新效率数据&#xff08;原始数据测算结果) 2011-2021年全国各省绿色创新效率 1、时间&#xff1a;2011-2021年 2、范围&#xff1a;全国31省市 3、来源&#xff1a;各省年鉴、科技年鉴、环境年鉴 4、指标&#xff1a;地区、编号、年份、R&D人…