C++ 模板保姆级详解——template<class T>(什么是模板?模板分哪几类?模板如何应用?)

news2024/11/17 15:28:59

目录

一、前言

二、 什么是C++模板

💦泛型编程的思想 

 💦C++模板的分类

 三、函数模板

 💦函数模板概念

 💦函数模板格式

💦函数模板的原理

 💦函数模板的实例化

🍎隐式实例化

 🍉显式实例化

💦模板支持多个模板参数

 💦模板参数的匹配原则

四、类模板 

 💦类模板的概念

 💦类模板格式

 💦类模板的实例化

 💦类模板的分离编译

五、总结

六、共勉 


一、前言

        在我们学习C++时,常会用到函数重载。而函数重载,通常会需要我们编写较为重复的代码,这就显得臃肿,且效率低下重载的函数仅仅只是类型不同,代码的复用率比较低,只要有新类型出现时,就需要增加对应的函数。此外,代码的可维护性比较低,一个出错可能会导致所有的重载均出错。

       那么,模板的出现,就让这些问题有了解决方案,所以本次博客将为大家详细的讲解C++的模板!!

二、 什么是C++模板

        程序设计中经常会用到一些程序实体它们的实现和所完成的功能基本相同,不同的仅 仅是所涉及的数据类型不同。而模板正是一种专门处理不同数据类型的机制。


       模板------是泛型程序设计的基础(泛型generic type——通用类型之意)。
 

        函数、类以及类继承为程序的代码复用提供了基本手段,还有一种代码复用途径——类属类型(泛型),利用它可以给一段代码设置一些取值为类型的参数(注意:这些参数 的值是类型,而不是某类型的数据),通过给这些参数提供一些类型来得到针对不同类 型的代码。

💦泛型编程的思想 

         首先我们来看一下下面这三个函数,如果学习过了C++函数重载 和 C++引用 的话,就可以知道下面这三个函数是可以共存的,而且传值会很方便

void Swap(int& left, int& right)
{
	int temp = left;
	left = right;
	right = temp;
}
void Swap(double& left, double& right)
{
	double temp = left;
	left = right;
	right = temp;
}
void Swap(char& left, char& right)
{
	char temp = left;
	left = right;
	right = temp;
}

        但是真的很方便吗?这里只有三种类型的数据需要交换,若是我们需要增加交换的数据呢?再CV然后写一个函数吗?
        这肯定是不现实的,所以很明显函数重载虽然可以实现,但是有一下几个不好的地方:

  • 重载的函数仅仅是类型不同,代码复用率比较低,只要有新类型出现时,就需要用户自己增加对应的函数
  • 代码的可维护性比较低,一个出错可能所有的重载均出错

       

        那是否能做到这么一点,告诉编译器一个模子,让编译器根据不同的类型利用该模子来生成代码

总结:
        所以,总结上面的这么一个技术,C++的祖师爷呢就想到了【模版】这个东西,告诉编译器一个模子,然后其余的工作交给它来完成,根据不同的需求生成不同的代码

这就是👉泛型编程:编写与类型无关的通用代码,是代码复用的一种手段。模板是泛型编程的基础

 💦C++模板的分类

1️⃣: 函数模板(function tempalte):使用泛型参数的函数(function with generic parameters)

2️⃣:类模板(class template):使用泛型参数的类(class with generic parameters)

 三、函数模板

 知晓了模版的基本概念后,首先我们要来看的就是【函数模版】

 💦函数模板概念

        函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定类型版本
        通过函数模板,可以编写一种通用的函数定义,使其能够适用于多种数据类型,从而提高代码的复用性和灵活性。

 💦函数模板格式

template<typename T1, typename T2,......,typename Tn>
返回值类型 函数名(参数列表)
{
    //……
}
注意:typename是用来定义模板参数关键字,也可以使用class(切记:不能使用struct代替class)

因此,交换函数就可以这样套用模板:

template<typename T>
void Swap(T& left, T& right)
{
	T temp = left;
	left = right;
	right = temp;
}

int main()
{
	
	int x = 2, y = 3;
	cout << x << " " << y << endl << endl;
	Swap(x, y);
	cout << x << " " << y << endl << endl;
	cout << "-------------------------------" << endl;

	char a = 'a', b = 'b';
	cout << a << " " << b << endl << endl;
	Swap(a, b);
	cout << a << " " << b << endl << endl;
	cout << "-------------------------------" << endl;


	double c = 2.1, d = 3.1;
	cout << c << " " << d << endl << endl;
	Swap(c, d);
	cout << c << " " << d << endl << endl;
	cout << "-------------------------------" << endl;
	return 0;
}

 

        我们通过这个函数模版,分别传入不同数据类型的参数,通过结果的观察可以发现这个函数模版可以根据不同的类型去做一个自动推导,继而去起到一个交换的功能。

💦函数模板的原理

 💬 那我现在想问一个问题,请问它们调用的真的是同一个函数吗?
        当然不是,这里我们三次Swap不是调用同一个函数,其实Swap的时候根据不同的类型通过模板定制出专属你的类型的函数,然后再调用,这里可以通过反汇编观察到:

 

        可以发现,在进行汇编代码查看的时候,被调用的函数模版生成了两个不同的函数,它们有着不同的函数地址,因此可以回答上一小节所提出的问题了,两次所调用的函数是不一样的,是根据函数模版所生成的


        函数模板是一个蓝图,它本身并不是函数,是编译器用使用方式产生特定具体类型函数的模具。
        所以其实模板就是将本来应该我们做的重复的事情交给了编译器。

  


        在编译器编译阶段,对于模板函数的使用,编译器需要根据传入的实参类型来推演生成对应类型的函数以供调用。比如:当用double类型使用函数模板时,编译器通过对实参类型的推演,将T确定为double类型,然后产生一份专门处理double类型的代码,对于字符类型也是如此。
  

  • 补充:

 其实库里面有一个swap函数,因此我们也不需要自己写模板了:


直接套用swap即可:

int main()
{
	
	int x = 2, y = 3;
	cout << x << " " << y << endl << endl;
	swap(x, y);
	cout << x << " " << y << endl << endl;
	cout << "-------------------------------" << endl;

	char a = 'a', b = 'b';
	cout << a << " " << b << endl << endl;
	swap(a, b);
	cout << a << " " << b << endl << endl;
	cout << "-------------------------------" << endl;


	double c = 2.1, d = 3.1;
	cout << c << " " << d << endl << endl;
	swap(c, d);
	cout << c << " " << d << endl << endl;
	cout << "-------------------------------" << endl;
	return 0;
}

 💦函数模板的实例化

        用不同类型的参数使用函数模板时,称为函数模板的实例化。模板参数实例化分为:
                         1️⃣: 隐式实例化                                             2️⃣: 显式实例化

🍎隐式实例化

 隐式实例化 让编译器根据实参推演模板参数的实际类型

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, a2); //编译器推出T是int
	Add(d1, d2); //编译器推出T是double
    return 0;
}

⚠ 注意:但是我调用的时候如若这样就会出错:

int main()
{
    int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
	Add(a1, d1); //err 编译器推不出来
	/*
该语句不能通过编译,因为在编译期间,当编译器看到该实例化时,需要推演其实参类型
通过实参a1将T推演为int,通过实参d1将T推演为double类型,但模板参数列表中只有
一个T,编译器无法确定此处到底该将T确定为int 或者 double类型而报错
注意:在模板中,编译器一般不会进行类型转换操作,因为一旦转化出问题,编译器就需要背黑锅
    */
}

此时我们发现在模板函数里面,形参不匹配,就会出现报错


编译器无法确定这里的T到底是int还是double。此时有两种处理方式:

法一:用户自己来强制转化

int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    Add(a1, (int)d1); //强制类型转换。或者Add((double)a1, d1);
}

法二:使用显式实例化
接下来进行讲解。

 🍉显式实例化

 显式实例化:在函数名后的<>中指定模板参数的实际类型

继刚才的例子,法二:使用显式实例化

template<class T>
T Add(const T& left, const T& right)
{
	return left + right;
}
int main()
{
	int a1 = 10, a2 = 20;
	double d1 = 10.0, d2 = 20.0;
    //显示实例化
	Add<int>(a1, d1); //double隐式类型转换成int 
	Add<double>(a1, d2); 
    return 0;
}

💦模板支持多个模板参数

template<class K, class V> //两个模板参数
void Func(const K& key, const V& value)
{
	cout << key << ":" << value << endl;
}
int main()
{
	Func(1, 1); //K和V均int
	Func(1, 1.1);//K是int,V是double
	Func<int, char>(1, 'A'); //多个模板参数也可指定显示实例化不同类型
}


 

 💦模板参数的匹配原则

  • 原则1 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数
//专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
//通用加法函数
template<class T>
T Add(T left, T right)
{
	return left + right;
}
int main()
{
	Add(1, 2); //会调用哪个Add函数?
}

首先,这俩Add可以同时存在,关键是我调用Add时调的是模板函数Add,还是专门的Add?

        通过反汇编得知,调用的是专属Add函数。得出结论:编译器在调用时,有现成的就调用现成的,没有就套用模板。当然,我们也有办法强制让编译器走模板函数,如下:

void Test()
{
    Add(1, 2); // 与非模板函数匹配,编译器不需要特化
    Add<int>(1, 2); // 调用编译器特化的Add版本
}
  • 原则2对于非模板函数同名函数模板如果其他条件都相同,在调动时会优先调用非模板函数而不会从该模板产生出一个实例。如果模板可以产生一个具有更好匹配的函数, 那么将选择模板
// 专门处理int的加法函数
int Add(int left, int right)
{
	return left + right;
}
// 通用加法函数
template<class T1, class T2>
T1 Add(T1 left, T2 right)
{
	return left + right;
}
void Test()
{
	Add(1, 2); // 与非函数模板类型完全匹配,不需要函数模板实例化
	Add(1, 2.0); // 模板函数可以生成更加匹配的版本,编译器根据实参生成更加匹配的Add函数
}

四、类模板 

 💦类模板的概念

        类模板是对成员数据类型不同的类的抽象,它说明了类的定义规则,一个类模板可以生成多种具体的类。与函数模板的定义形式类似 类模板也是使用template关键字和尖括号“<>”中的模板形参进行说明,类的定义形式与普通类相同。

 💦类模板格式

  • 首先来看到的就是其定义格式,函数模版加在函数上,那对于类模版的话就是加在类上
template<class T1, class T2, ..., class Tn>
class 类模板名
{
    // 类内成员定义
};

我们以下面这个Stack类为例来进行讲解

  • 如果你学习了模版的相关知识后,一定会觉得这个类的限制性太大了只能初始化一个具有整型数据的栈,如果此时我想要放一些浮点型的数据进来的话也做不到,代码如下:
// C++ 正常情况下,如果需要不同类型的类(int char double 等)
typedef int DataType;
class Stack
{
public:
    // 构造函数
    Stack(int capacity = 3)   //初始化列表
        :_array(new DataType[capacity])   // 开辟一个DateType的动态数组,并进行初始化
        , _capacity(capacity)
        ,_size(0)
    {}
    void Push(DataType data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }
    // 其他方法...
    ~Stack()
    {
        delete[]_array;
        _array = nullptr;
        _size = _capacity = 0;
    }
private:
    DataType* _array;
    int _capacity;
    int _size;
};
int main()
{
    Stack s1;
    return 0;
}

💬 如果没有模版技术的话你会如何去解决这个问题呢?很简单那就是定义多个类
这是我们同学最擅长的事,CV一下两个栈就有了,StackInt存放整型数据,StackDouble存放浮点型数据

class StackInt
class StackDouble

但是本文我们重点要讲解的就是【模版技术】,技术界有一句话说得好 “不要重复造轮子”

  • 下面就是使用模版去定义的一个类,简称【模板类】,不限制死数据类型,将所有的DataType都改为【T】,代码如下:
template<class T1>
class Stack
{
public:
	// 构造函数
	Stack(int capacity = 4)
		:_a(new T1[capacity])
		,_capacity(capacity)
		,_size(0)
	{}

	void Push(T1 data)
	{
		_a[_size] = data;
		_size++;
	}
	// ...其他方法
	// 析构函数
	~Stack()
	{
		delete[]_a;
		_a = nullptr;
		_capacity = _size = 0;
	}
private:
	T1* _a;
	int _capacity;
	int _size;
};

int main()
{
	Stack<int> s1;
	Stack<double> s2;
	return 0;
}


 

 💦类模板的实例化

 👉 类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类

  • 可以看到因为我们将这个类定义为了类模版,此时便可以去初始化不同数据类型的栈了,上面说到过Stack是类名,但是像Stack<int>Stack<double>这些都是它的类型
int main()
{
	Stack<int> s1;		// int
	Stack<double> s2;	// double
	Stack<char> s3;		// char
	return 0;
}

 💦类模板的分离编译

        上面这样写的栈代码,其实并不是最规范的写法,还记得我们在学习C++类和对象讲到过一个类要声明和定义分离,那对于模板类也同样适用,我们马上来看看

首先进行栈 类的模板声明:

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;

template<class T1>
class Stack
{
public:
	// 构造函数
	Stack(int capacity = 4);

	//插入函数
	void Push(T1 data);

	// 其他方法....

	// 析构函数
	~Stack();
private:
	T1* _a;
	int _capacity;
	int _size;
};

其次进行栈 类的模板定义:先看一下构造函数的定义

// 构造函数
Stack::Stack(int capacity)
	:_a(new T1[capacity])
	,_capacity(capacity)
	,_size(0)
{}

  • 不过呢可以看到直接像我们之前那样去进行类外定义似乎行不通,说缺少类模版“Stack”的参数列表因为这个成员函数内部也使用到了模版参数T,那么这个函数也要变为函数模版才行
  • 这里要强调一点的是对于普通类来说类名和类型是一样的, 像构造函数,它的函数名就是类名;可是对于模板类来说是不一样,类名和类型不一样,这里Stack只是这个模版类的类名罢了,但我们现在需要的是类型,此处就想到了我们在上面所学的【显式实例化】,这个模板类的类型即为Stack<T>

所以正确的写法为:

#include "Stack.h"

template<class T1>
// 构造函数
Stack<T1>::Stack(int capacity)
	:_a(new T1[capacity])
	,_capacity(capacity)
	,_size(0)
{}
template<class T1>
// 插入函数
void Stack<T1>::Push(T1 data)
{
	_a[_size] = data;
	_size++;
}
// 析构函数
template<class T1>
Stack<T1>::~Stack()
{
	delete[]_a;
	_a = nullptr;
	_capacity = _size = 0;
}

⚠ 注意:类模板,是不支持,声明,定义,测试分开写的,会出现链接编译错误,如下:



解决方法:将测试和定义写在一起

这样就不会报错啦!

五、总结

  •  首先呢我们介绍了什么是【函数模版】,新学习了一个关键字叫做template,用它再配合模版参数就可以去定义出一个函数模版,有了它,我们在写一些相同类型函数的时候就无需去进行重复的CV操作了,在通过汇编观察函数模版的原理后,清楚了我们只需要传入不同的类型,此时模版参数就会去进行一个自动类型推导,从而产生不同的函数。函数模版定义好后还要对其实例化才能继续使用,但此时要注意的一点是如果传递进去的类型个数与模版参数的个数不匹配的话,其就无法完成自动类型推导,因为这会产生一个歧义。所以想要真正学好模版,这点是一定要搞清楚的!!!
  • 接下去呢我们又学习了【类模版】,没想到吧,类也可以变成一个模版,以Stack类为例,对于类模版而言,其类名和类型与普通类是不一样的,这点要注意了,尤其体现在类的成员函数放在类外进行定义的时候,也要将其定义为函数模版,函数名前面指明其类型,这才不会出问题。有了类模版之后,我们去显式实例化不同的数据类型后也可以让模版参数去做一个自动类型推导从而得到不同数据类型的栈

六、共勉 

     以下就是我对C++ 模板的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++STL库的理解,请持续关注我哦!!!   

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

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

相关文章

Halcon WPF 开发学习笔记(4):Halcon 锚点坐标打印

文章目录 专栏前言锚点二次开发添加回调函数辅助Model类 下集预告 专栏 Halcon开发 博客专栏 WPF/HALCON机器视觉合集 前言 Halcon控件C#开发是我们必须掌握的&#xff0c;因为只是单纯的引用脚本灵活性过低&#xff0c;我们要拥有Halcon辅助开发的能力 锚点开发是我们常用的…

记录一次某某虚拟机的逆向

导语 学了一段时间的XPosed&#xff0c;发现XPosed真的好强&#xff0c;只要技术强&#xff0c;什么操作都能实现... 这次主要记录一下我对这款应用的逆向思路 apk检查 使用MT管理器检查apk的加壳情况 发现是某数字的免费版本 直接使用frida-dexdump 脱下来后备用 应用分…

【ATTCK】MITRE Caldera - 测试数据泄露技巧

CALDERA是一个由python语言编写的红蓝对抗工具&#xff08;攻击模拟工具&#xff09;。它是MITRE公司发起的一个研究项目&#xff0c;该工具的攻击流程是建立在ATT&CK攻击行为模型和知识库之上的&#xff0c;能够较真实地APT攻击行为模式。 通过CALDERA工具&#xff0c;安全…

Git版本控制系统之分支与标签(版本)

目录 一、Git分支&#xff08;Branch&#xff09; 1.1 分支作用 1.2 四种分支管理策略 1.3 使用案例 1.3.1 指令 1.3.2 结合应用场景使用 二、Git标签&#xff08;Tag&#xff09; 2.1 标签作用 2.2 标签规范 2.3 使用案例 2.3.1 指令 2.3.2 使用示例 一、Git分支&…

55基于matlab的1.高斯噪声2.瑞利噪声3.伽马噪声4.均匀分布噪声5.脉冲(椒盐)噪声

基于matlab的1.高斯噪声2.瑞利噪声3.伽马噪声4.均匀分布噪声5.脉冲&#xff08;椒盐&#xff09;噪声五组噪声模型&#xff0c;程序已调通&#xff0c;可直接运行。 55高斯噪声、瑞利噪声 (xiaohongshu.com)

“第六十六天”

这个我记得是有更优解的&#xff0c;不过还是明天发吧&#xff0c;明天想一想&#xff0c;看看能不能想起来 #include<string.h> int main() {char a[201] { 0 };char b[201] { 0 };scanf("%s %s", a, b);int na strlen(a);int nb strlen(b);int i 0, j …

基于SpringBoot+Vue的在线学习平台系统

基于SpringBootVue的在线学习平台系统的设计与实现~ 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBootMyBatisVue工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 主页 用户界面 登录界面 管理员界面 摘要 本文设计并实现了一套基于Spri…

C语言——贪吃蛇

一. 游戏效果 贪吃蛇 二. 游戏背景 贪吃蛇是久负盛名的游戏&#xff0c;它也和俄罗斯⽅块&#xff0c;扫雷等游戏位列经典游戏的⾏列。 贪吃蛇起源于1977年的投币式墙壁游戏《Blockade》&#xff0c;后移植到各种平台上。具体如下&#xff1a; 起源。1977年&#xff0c;投币式…

Leetcode421. 数组中两个数的最大异或值

Every day a Leetcode 题目来源&#xff1a;421. 数组中两个数的最大异或值 解法1&#xff1a;贪心 位运算 初始化答案 ans 0。从最高位 high_bit 开始枚举 i&#xff0c;也就是 max⁡(nums) 的二进制长度减一。设 newAns ans 2i&#xff0c;看能否从数组 nums 中选两个…

数据结构—二叉树的模拟实现(c语言)

目录 一.前言 二.模拟实现链式结构的二叉树 2.1二叉树的底层结构 2.2通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树 2.3二叉树的销毁 2.4二叉树查找值为x的节点 2.5二叉树节点个数 2.6二叉树叶子节点个数 2.7二叉树第k层节点个数 三.二叉树的遍历 3.1…

基于SpringMVC模式的电器网上订购系统的设计

大家好我是玥沐春风&#xff0c;今天分享一个基于SpringMVC模式的电器网上订购系统的设计&#xff0c;项目源码以及部署相关请联系我&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 本系统利用现在比较广泛的JSP结合后台SpringMybatisAjax编写程序的方式实现的。 在…

深度学习基于python+TensorFlow+Django的花朵识别系统

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 花朵识别系统&#xff0c;基于Python实现&#xff0c;深度学习卷积神经网络&#xff0c;通过TensorFlow搭建卷积神经…

Think-on-Graph:基于知识图的大型语言模型的深层可靠推理11.12

Hink-on-Graph&#xff1a;基于知识图的大型语言模型的深层可靠推理 摘要1 引言2 方法2.1图上思考2.1.1图的初始化2.1.2 探索2.1.3推理 2.2 基于关系的Think on graph 摘要 尽管大型语言模型&#xff08;LLM&#xff09;在各种任务中取得了巨大的成功&#xff0c;但它们经常与…

jupyter lab配置列表清单

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

【 第十章】软件设计师 之 软件工程概述

文章底部有个人公众号&#xff1a;热爱技术的小郑。主要分享开发知识、学习资料、毕业设计指导等。有兴趣的可以关注一下。为何分享&#xff1f; 踩过的坑没必要让别人在再踩&#xff0c;自己复盘也能加深记忆。利己利人、所谓双赢。 备考资料导航 软考好处&#xff1a;软考的…

“总线仲裁”——以CAN总线为例

总线仲裁 1.什么是总线仲裁2.为什么要总线仲裁3.怎么进行总线仲裁&#xff08;总线仲裁机制&#xff09;3.1 如何确定冲突3.1.1 确定冲突前提3.1.2 同时冲突3.1.3 延时冲突 3.2 冲裁逻辑3.2.1 避免延时冲突3.2.1 避免同时冲突 1.什么是总线仲裁 提到总线仲裁的概念&#xff0c…

基于Qt 多线程(继承自QThread篇)

# 简介 我们写的一个应用程序,应用程序跑起来后一般情况下只有一个线程,但是可能也有特殊情况。比如我们前面章节写的例程都跑起来后只有一个线程,就是程序的主线程。线程内的操作都是顺序执行的。恩,顺序执行?试着想一下,我们的程序顺序执行,假设我们的用户界面点击有某…

leetcode-链表经典题

1.反转单链表 206. 反转链表https://leetcode.cn/problems/reverse-linked-list/这里我们使用创建一个变量cur来遍历原链表&#xff0c;再创建一个新节点newnode&#xff0c;首先使用一个循环来遍历原链表&#xff0c;cur为NULL是循环结束&#xff0c;每次进入循环将cur的下一…

nRF5 SDK 入门(三、理解 nRF5 SDK 应用与协议栈分开烧录)

说明一下 Nordic nRF5 SDK 软件 应用程序 和 协议栈分开烧录的理解前言 上一篇文章我们了解了 Nordic nRF5 SDK 目录结构&#xff0c;在那之前我们也已经搭建好了开发环境&#xff0c;实际上我们就已经可以进入我们的开发之旅了&#xff0c;但是如果刚接触 Nordic 蓝牙开发的小…