C++学习-入门到精通【17】自定义的模板化数据结构

Item43:学习处理模板化基类内的名称 模板化基类的名称在派生类中不可见,是C++模板名称查找规则的必然结果。this->:简洁,适用于非静态成员的常规访问;using声明:适合批量引入成员,避免名称遮蔽;显式基类作用域:明确且强制,适用于静态成员或抑制多态的场景。在模板派生类中访问基类成员时,必须通过上述方法明确告知编译器名称来源,否则会因名称查找失败导致编译错误。这一规则是模板继承中确保代码正确性的基础。 阅读详情

C++学习-入门到精通【17】自定义的模板化数据结构



一、自引用类

一个自引用类包含一个指向与它同类的对象的指针成员。例如:

class  Node
{
public:
	explicit Node(int); // 声明为explicit,保证该构造函数不会被隐式的转换成转换构造函数
	void setData(int);
	int getData() const;
	void setNextNode(Node*);
	Node* getNextNode() const;
private:
	int data;
	Node* nextPtr;
};

这个类包含两个数据成员,一个int类型的成员data和一个指向与所声明的类同类型对象的指针。这个指针成员可以指向另一个Node类型的对象,因此称Node为自引用类。nextPtr成员被称为链接。

自引用类的对象可以链接在一起组成一些有用的数据结构,如链表、队列、堆栈和树等。

二、链表

链表就是一个自引用类对象的线性集合,其中的对象被称为节点(node),它们通过指针链连接起来,因此被称为链表。链表只能通过指向链表第一个元素的指针进行访问,要访问后续节点,必须从第一个元素开始,通过它的指针成员来找到下一个元素,因为链表在内存中的分布是不连续的。所以链表末尾节点的指针域通常被置为空,以此来表明链表的结束。

链表很适合存储那些一开始难以预计数据项有多少的数据,因为链表是动态的,它的长度可以根据需要动态的增加或减少,如一条链表要增加一个新的节点,只需创建一个新的节点,并将当前链表的末尾节点的指针成员指向该新节点即可。

除此之外,由于链表的特性,它在内部插入新节点较数组而言是方便的,它只需改变其中一些节点的指针域的值即可完成插入操作,而数组则需要移动一些元素才可以实现。关于链表的更多具体内容,请在“数据结构”中学习。

链表的实现

下面实现的链表提供4种操作:

  • 在链表开始处插入一个元素;
  • 在链表尾部插入一个元素;
  • 在链表开始处删除一个元素;
  • 在链表尾部删除一个元素;

ListNode.h,定义链表的一个节点

#pragma once
// List类模板的声明,虽然并未定义,
// 但是告诉编译器存在该类型
template<class NODETYPE> class List;

// 创建一个类模板来描述一个链表的一个节点
template<class NODETYPE>
class ListNode
{
	// 声明类模板特化 List<NODETYPE> 是类模板特化 ListNode<NODETYPE> 的友元类
	// 该类中的所有成员函数皆可访问本类中的所有成员
	friend class List<NODETYPE>;

public:
	explicit ListNode(const NODETYPE& info)
		: data(info), nextPtr(nullptr)
	{

	}

	NODETYPE getData() const
	{
		return data;
	}

private:
	NODETYPE data;
	ListNode<NODETYPE>* nextPtr; // 指向链表中下一个节点的指针
};

List.h,定义链表

#pragma once
#include <iostream>
#include "ListNode.h"

template<class NODETYPE>
class List
{
public:
	List()
		: firstPtr(nullptr), lastPtr(nullptr)
	{

	}

	~List()
	{
		// 链表中还有数据
		if (!isEmpty())
		{
			std::cout << "Destorying nodes ...\n";

			ListNode<NODETYPE>* currentPtr = firstPtr;
			ListNode<NODETYPE>* tempPtr = nullptr;

			// 当前的第一个指针指向一个有效节点
			while (currentPtr != nullptr)
			{
				tempPtr = currentPtr;
				std::cout << tempPtr->data << '\n';
				currentPtr = currentPtr->nextPtr;
				delete tempPtr;
			}
		}

		std::cout << "All nodes destoryed\n\n";
	}

	// 头插法
	void insertAtFront(const NODETYPE& value)
	{
		// 创建一个新节点
		ListNode <NODETYPE>* newPtr = getNewNode(value);

		// 当前是一个空链表
		if (isEmpty())
		{
			firstPtr = lastPtr = newPtr;
		}
		else
		{
			newPtr->nextPtr = firstPtr;
			firstPtr = newPtr;
		}
	}

	// 尾插法
	void insertAtBack(const NODETYPE& value)
	{
		ListNode<NODETYPE>* newPtr = getNewNode(value);

		if (isEmpty())
		{
			firstPtr = lastPtr = newPtr;
		}
		else
		{
			lastPtr->nextPtr = newPtr;
			lastPtr = newPtr; // 将last指针指向最新的尾节点
		}
	}

	bool removeFromFront(NODETYPE& value)
	{
		if (isEmpty())
		{
			return false;
		}
		else
		{
			ListNode<NODETYPE>* tempPtr = firstPtr;

			// 链表中只有一个节点
			if (firstPtr == lastPtr)
			{
				firstPtr = lastPtr = nullptr;
			}
			else
			{
				firstPtr = firstPtr->nextPtr;
			}

			value = tempPtr->data;
			delete tempPtr;
			return true;
		}
	}

	bool removeFromBack(NODETYPE& value)
	{
		if (isEmpty())
		{
			return false;
		}
		else
		{
			ListNode<NODETYPE>* tempPtr = lastPtr;

			if (firstPtr == lastPtr)
			{
				firstPtr = lastPtr = nullptr;
			}
			else
			{
				ListNode<NODETYPE>* currentPtr = firstPtr;

				// 找到尾节点的前一个节点
				while (currentPtr->nextPtr != lastPtr)
				{
					currentPtr = currentPtr->nextPtr;
				}

				lastPtr = currentPtr;
				currentPtr->nextPtr = nullptr;
			}

			value = tempPtr->data;
			delete tempPtr;
			return true;
		}
	}

	// 链表为空链表,首节点的指针域肯定为空
	bool isEmpty() const
	{
		return firstPtr == nullptr;
	}

	void print() const
	{
		if (isEmpty())
		{
			std::cout << "The list is empty\n\n";
			return ;
		}

		ListNode<NODETYPE>* currentPtr = firstPtr;

		std::cout << "The list is: ";
		
		while (currentPtr != nullptr)
		{
			std::cout << currentPtr->data << ' ';
			currentPtr = currentPtr->nextPtr;
		}

		std::cout << "\n\n";
	}

private:
	ListNode<NODETYPE>* firstPtr; // 指向链表的首节点
	ListNode<NODETYPE>* lastPtr; // 指向链表的尾节点

	// 工具函数,创建一个新节点
	ListNode<NODETYPE>* getNewNode(const NODETYPE& value)
	{
		return new ListNode<NODETYPE>(value);
	}
};

测试客户程序,test.cpp

#include <iostream>
#include <string>
#include "List.h"
using namespace std;

// 提示用户输入指令
void instructions()
{
	cout << "Enter one of the following:\n"
		<< "  1 - insert at beginning of list\n"
		<< "  2 - insert at ending of list\n"
		<< "  3 - delete from beginning of list\n"
		<< "  4 - delete from ending of list\n"
		<< "  0 - end list processing\n";
}
// 声明一个函数模板,用于对list对象进行测试
template<class T>
void testList(List<T>& listObject, const string& typeName)
{
	cout << "Testing a list of " << typeName << " values\n";
	instructions();

	int choice;
	T value; // 用于保存对链表操作的值

	do
	{
		cout << "? ";
		cin >> choice;

		switch (choice)
		{
		case 1: // 在首部插入
			cout << "Enter " << typeName << ": ";
			cin >> value;
			listObject.insertAtFront(value);
			listObject.print();
			break;
		case 2:
			cout << "Enter " << typeName << ": ";
			cin >> value;
			listObject.insertAtBack(value);
			listObject.print();
			break;
		case 3:
			if(listObject.removeFromFront(value))
				cout << value << " removed from list\n";

			listObject.print();
			break;
		case 4:
			if (listObject.removeFromBack(value))
				cout << value << " removed from list\n";

			listObject.print();
			break;
		case 0:
			cout << "End list test\n\n";
			break;
		default:
			cout << "Wrong choice\n";
			break;
		}
	}while(choice);
}

int main()
{
	List<int> integerList;
	testList(integerList, "integer");

	List<double> doubleList;
	testList(doubleList, "double");
}

运行结果:

在这里插入图片描述

在上面的程序中我们创建了两个类模板,ListNode和List,前者描述了链表中一个节点应该具有的功能及存储的数据,后者则是给出了一个链表的一些基本操作方式。

类模板ListNode
在该模板的开始处,我们将一个类模板特化List<NODETYPE>声明为类模板特化ListNode<NODETYPE>的友元类,使得前者可以访问后者的私有数据成员,这使得这两个类紧耦合,且只有类模板List能够操作类模板ListNode的对象,并没有极大的破坏封装性。

类模板List
该类模板中提供了对于单链表的一些基本操作,头插法、尾插法、删除第一个节点、删除最后一个节点、显示整个链表、析构函数(用以保证当List对象被销毁时,其中所有的ListNode对象也全部被销毁)、判空函数以及用于创建一个新节点的工具函数。

除了上面实现的单链表之外,还有循环单链表、双向链表以及循环双向链表。它们具体的实现,请通过“数据结构”学习。

三、堆栈

我们之前已经使用几次堆栈了,这种数据结构的特点就是只允许在栈的顶部增加或删除节点,也就是后进先出(LIFO)。栈的一种实现方法是将它作为一种有限制的单链表,将链表的尾节点作为栈底,只允许使用头插法和删除首部元素。

对堆栈而言,主要就是两种操作,压栈(push)和出栈(pop)。

利用堆栈和链表的关系,基于List继承实现模板类Stack

Stack.h

#pragma once
#include "List.h"

template<class STACKTYPE>
class Stack : private List<STACKTYPE>
{
	using List<STACKTYPE>::insertAtFront;
public:
	void push(const STACKTYPE& data)
	{
		// 注意虽然这里使用的参数是一个从属名称,但是仍然需要指明此处调用的函数是一个从属名称
		// 因为函数名的查找发生在参数类型检查之前,如果此处不进行显式指明该函数名是一个从属名称
		// 那么它就是一个非从属名称,会在模板定义阶段就被解析,所以会出错
		insertAtFront(data);
	}

	bool pop(STACKTYPE& data)
	{
		return List<STACKTYPE>::removeFromFront(data);
	}

	bool isStackEmpty() const
	{
		return List<STACKTYPE>::isEmpty();
	}

	void printStack() const
	{
		this->print();
	}
};

上面的代码中声明的类模板Stack,它private继承了类模板特化List<STACKTYPE>,这是因为Stack只会使用到部分List的成员函数,有一些成员函数的功能应该受到限制,所以使用private继承,如此List中的public成员函数在Stack中全部变成了声明为private的接口,只在类内可以使用,保证了封装性。并且我们使用了委托的方式来实现模板类Stack中的成员函数,即通过这些成员函数去调用List类的成员函数来实现功能。

类模板的从属名称

在模板编程中,从属名称是一个非常重要的概念。这是因为编译器对于模板的处理分为两个阶段:

  1. 模板定义阶段,处理不依赖于模板参数的部分;
  2. 模板实例化阶段,处理依赖于模板参数的部分;

这应该很容易理解,模板在定义阶段,仅仅是使用在模板参数列表中定义的一个类型占位符来表示该模板在使用过程中可能会出现的类型。但是到底是什么类型只有将一个模板进行特化之后才能确定,所以会分成两个阶段进行处理。

从上面的描述中大家大概也能猜出什么是从属名称了吧,没错,模板中依赖于模板参数的标识符都是从属名称。

那么知道了什么是从属名称之后,这个从属名称到底有什么用呢?我们先来看下面的例子:

template<typename T>
class Base
{
	T::value_type elem;
	// 对elem的使用
	// ...
};

在这个例子中,当我们的模板参数类型T是一个类模板特化,比如vector<int>时,此时的T::value_type elem想表达的意思是,创建一个与模板参数类型的内部元素相同类型的变量elem,value_type是容器中预定义的类型别名。但是在容器中该类型是一个从属名称(它依赖于容器类模板的模板参数),但是在上面的代码中编译器并不知道它是什么,此时会产生二义,value_type是T内部的一个成员变量,或是一个类型。所以此时必须对它进行修改,可以有以下三种修改方式:

template<typename T>
class Base
{
	// 1
	typename T::value_type elem;
	// 对elem的使用
	// ...
	// 2
	typedef typename T::value_type value_type;
	value_type elem;
	// 3
	using ValueType = typename T::value_type;
	ValueType elem;
};

从上面的代码中我们了解到了从属名称的作用,并且在某些情况下,程序员需要显式的指定一个标识符是一个从属名称,以使得它的解析被推迟到模板实例化时才进行,保证程序的正确运行,那么有哪些情况呢?

  • 当在模板中使用的标识符会使用到模板参数内部的子类型(比如,vector<int>中元素的类型为int类型)、模板参数内部的成员函数模板、或使用模板参数内部的子模板(嵌套模板)时,在上述的这些情况中,都需要显示指明该标识符是一个从属名称(使用typename、template或结合使用两者)。
  • 当模板继承了另一个类模板时,在该需要使用到基类模板中的任何成员时,全部都需要显式指明当前标识符是一个从属名称(使用this指针、作用域限定符或using声明),如果使用的基类模板中的嵌套类型、嵌套函数模板、嵌套类模板时除了前面的this指针之类的指明它是一个从属名称的标识符之外,还需要使用typename之类的关键字。

从上面的两个情况我们可以发现其实从属名称也可以分为三类:

  • 从属类型名称:使用关键字typename进行显式声明,注意class是不行的,在模板参数列表中可以使用class作为关键字来声明一个模板参数,是因为在模板编程的早期使用的就是class,因为在当时模板大多用于类,但实际上一个模板的参数可以是任何类型(只要符合模板要求),使用class容易产生误会,所以引用了typename关键字,所以在模板内部要声明一个从属类型名称必须使用typename,模板的参数列表中则是为了兼容性继续保留了class的使用。

  • 从属函数名称:要使用关键字template进行声明。这是因为在没有显式的声明template让编译器将语句当作一个模板来使用的话,编译器会将模板中用于指定类型的尖括号<当成一个小于运算符来处理。

    template <typename T>
    class Sample
    {
    	T obj;
    public:
    	void run()
    	{
    		obj.template function<T>();
    	}
    };
    
  • 从属模板名称:使用关键字typename+template的组合来显式声明。对于从属模板名称而言,也是相同的原因;

    template <typename T>
    class Sample
    {
    	// T::template testClass<double>是一个特化的类型,所以在它的前面使用typename关键字
    	// testClass<>是一个类模板,在使用它之前需要让编译器明确知道这是一个类模板,所以使用template关键字
    	typename T::template testClass<double> obj;
    };
    

对于从基类模板继承而来的类模板而言,在模板定义阶段,整个基类是依赖于模板参数的,所以在该阶段,整个基类都是不确定的,如果要在派生类中使用基类的成员,必须使该标识符成为一个从属名称,这可以使用以下几种方式:

  • this指针,派生类的this指针的类型为Derived <T>*,它依赖于模板参数,所以它是一个从属名称,使用它调用的成员会推迟到模板实例化阶段才进行确定。上面的模板类Stack中的printStack就是一个例子。

  • 显式作用域限定,在使用基类成员前面显式的指明该标识符是基类的成员,例如,上面的模板类Stack中的isStackEmpty函数。

  • 使用using声明,例如,isStackEmpty函数可以改成如下代码:

    // 在派生类内部
    using List<STACKTYPE>::empty; 
    
    // isStackEmpty函数内部
    bool isStackEmpty()
    {
    	return empty();
    }
    

测试堆栈类模板Stack

#include <iostream>
#include "Stack.h"
using namespace std;

int main()
{
	Stack<int> intStack;

	cout << "Processing an integer Stack" << endl;

	for (int i = 0; i < 3; ++i)
	{
		intStack.push(i);
		intStack.printStack();
	}

	int popInteger;
	while (!intStack.isStackEmpty())
	{
		intStack.pop(popInteger);
		cout << popInteger << " popped from stack" << endl;
		intStack.printStack();
	}

	Stack<double> doubleStack;
	double value = 1.1;

	cout << "Processing an double Stack" << endl;

	for (int i = 0; i < 3; ++i)
	{
		doubleStack.push(value);
		doubleStack.printStack();
		value += 1.1;
	}

	double popDouble;
	while (!doubleStack.isStackEmpty())
	{
		doubleStack.pop(popDouble);
		cout << popDouble << " popped from stack" << endl;
		doubleStack.printStack();
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

利用List对象组合实现类模板Stack

下面的代码是使用了一个List类对象来实现Stack。

#pragma once
#include "List.h"

template <typename STACKTYPE>
class Stack
{
public:
	void push(const STACKTYPE& data)
	{
		stackList.insertAtFront(data);
	}

	bool pop(STACKTYPE& data)
	{
		return stackList.removeFromFront(data);
	}

	bool isStackEmpty() const
	{
		return stackList.isEmpty();
	}

	void printStack() const
	{
		stackList.print();
	}

private:
	List<STACKTYPE> stackList;
};

测试代码与上面的相同,结果如下:

在这里插入图片描述

四、队列

基于List实现类模板Queue

通过类模板继承来实现类模板Queue,代码如下:

Queue.h

#pragma once
#include "List.h"

template <typename QUEUETYPE> 
class Queue : private List<QUEUETYPE>
{
public:
	// 入队
	void enqueue(const QUEUETYPE& data)
	{
		this->insertAtBack(data);
	}

	// 出队
	bool dequeue(QUEUETYPE& data)
	{
		return this->removeFromFront(data);
	}

	bool isQueueEmpty() const
	{
		return this->isEmpty();
	}

	void printQueue() const
	{
		this->print();
	}
};

test.cpp

#include <iostream>
#include "Queue.h"
using namespace std;

int main()
{
	Queue<int> intQueue;

	cout << "Processing an integer Queue" << endl;

	for (int i = 0; i < 3; ++i)
	{
		intQueue.enqueue(i);
		intQueue.printQueue();
	}

	int popInteger;
	while (!intQueue.isQueueEmpty())
	{
		intQueue.dequeue(popInteger);
		cout << popInteger << " popped from Queue" << endl;
		intQueue.printQueue();
	}

	Queue<double> doubleQueue;
	double value = 1.1;

	cout << "Processing an double Queue" << endl;

	for (int i = 0; i < 3; ++i)
	{
		doubleQueue.enqueue(value);
		doubleQueue.printQueue();
		value += 1.1;
	}

	double popDouble;
	while (!doubleQueue.isQueueEmpty())
	{
		doubleQueue.dequeue(popDouble);
		cout << popDouble << " popped from Queue" << endl;
		doubleQueue.printQueue();
	}
}

运行结果:

在这里插入图片描述

五、树

上面介绍的链表、堆栈和队列都是线性结构。树是非线性的、二维的数据结构。树的节点可以包含两个或更多个链接,下面只讨论有两个链接的树,即二叉树。

实现一个二叉查找树

一个二叉查找树的基本特征是,左子树中的值一定小于其父节点的值,右子树的值一定大于其父节点的值。

我们在下面的代码中创建了一棵二叉树,并提供了三种遍历算法:中序、前序、后序。

TreeNode.h

#pragma once
// 提前声明存在类Tree
template <typename NODETYPE> class Tree;

template <typename NODETYPE>
class TreeNode
{
	// 注意此处只有与TreeNode类使用相同类型的特化才是友元
	// friend class template Tree<NODETYPE>是错误的写法
	// friend template class Tree<NODETYPE>也是错误的写法
	// template关键字在友元声明中只能用于引用模板参数列表,不能直接放在class的前面
	// 正确的用法是template <参数> friend class X;
	// 所以如果要表明另一个类模板所有的特化都是友元,则使用下面的写法
	// template <typename U> friend class Tree; 
	friend class Tree<NODETYPE>; // Tree<NODETYPE>已经是一个完整的模板实例化类型,不需要额外标记template来说明它是模板
public:
	TreeNode(const NODETYPE& d)
		:leftPtr(nullptr),
		data(d),
		rightPtr(nullptr)
	{

	}

	NODETYPE getDate() const
	{
		return data;
	}
private:
	TreeNode<NODETYPE>* leftPtr;
	NODETYPE data;
	TreeNode<NODETYPE>* rightPtr;
};

Tree.h

#pragma once
#include <iostream>
#include "TreeNode.h"

template <typename NODETYPE>
class Tree
{
public:
	Tree()
		:rootPtr(nullptr)
	{

	}

	void insertNode(const NODETYPE& value)
	{
		insertNodeHelper(&rootPtr, value);
	}

	void preOrderTraversal() const
	{
		preOrderHelper(rootPtr);
	}

	void inOrderTraversal() const
	{
		inOrderHelper(rootPtr);
	}
	
	void postOrderTraversal() const
	{
		postOrderHelper(rootPtr);
	}
private:
	TreeNode<NODETYPE>* rootPtr;

	// 要接收一个指向节点的指针,且避免传参时复制整个节点
	// 使用一个二级指针来接收
	void insertNodeHelper(
		TreeNode<NODETYPE>** ptr, const NODETYPE& value)
	{
		// ptr指向的子树是一棵空树
		if (*ptr == nullptr)
		{
			// 创建一个新节点,将值在存储在这个新节点中
			*ptr = new TreeNode<NODETYPE>(value);
		}
		else // 比较要插入的值和当前节点中的值的大小
		{
			// 要插入节点的值小于当前节点的值,插入左子树
			if (value < (*ptr)->data)
			{
				// 递归尝试将节点插入左子树中
				insertNodeHelper(&(*ptr)->leftPtr, value);
			}
			else // 插入右子树
			{
				if (value > (*ptr)->data)
				{
					insertNodeHelper(&(*ptr)->rightPtr, value);
				}
				else
				{
					// 重复的值,忽略
					std::cout << value << " dup " << std::endl;
				}
			}
		}
	}

	// 先(根)序遍历
	void preOrderHelper(TreeNode<NODETYPE>* ptr) const
	{
		// 判定ptr指向的子树是否为空树
		if (ptr == nullptr)
		{
			return; // 查找结束条件
		}
		else
		{
			// 输出根节点的值
			std::cout << ptr->data << ' ';
			// 对左子树进行先序遍历
			preOrderHelper(ptr->leftPtr);
			// 对右子树进行先序遍历
			preOrderHelper(ptr->rightPtr);
		}
	}

	// 中(根)序遍历
	void inOrderHelper(TreeNode<NODETYPE>* ptr) const
	{
		// 判定ptr指向的子树是否为空树
		if (ptr == nullptr)
		{
			return; // 查找结束条件
		}
		else
		{
			// 对左子树进行先序遍历
			inOrderHelper(ptr->leftPtr);
			// 输出根节点的值
			std::cout << ptr->data << ' ';
			// 对右子树进行先序遍历
			inOrderHelper(ptr->rightPtr);
		}
	}

	// 后(根)序遍历
	void postOrderHelper(TreeNode<NODETYPE>* ptr) const
	{
		// 判定ptr指向的子树是否为空树
		if (ptr == nullptr)
		{
			return; // 查找结束条件
		}
		else
		{
			// 对左子树进行先序遍历
			postOrderHelper(ptr->leftPtr);
			// 输出根节点的值
			// 对右子树进行先序遍历
			postOrderHelper(ptr->rightPtr);
			std::cout << ptr->data << ' ';
		}
	}
};

测试程序,test.cpp

#include <iostream>
#include <string>
#include <iomanip>
#include <cstdlib>
#include "Tree.h"
using namespace std;

template <typename T>
void testTree(Tree<T>& tree, const string& typeName)
{
	cout << "Enter 10 " << typeName << " values:\n";

	for (int i = 0; i < 10; ++i)
	{
		T value = 0;
		cin >> value;
		tree.insertNode(value);
	}

	cout << "\nPreorder traversal\n";
	tree.preOrderTraversal();

	cout << "\nInorder traversal\n";
	tree.inOrderTraversal();

	cout << "\nPostorder traversal\n";
	tree.postOrderTraversal();
}

int main()
{
	cout << fixed << setprecision(2);

	Tree<int> intTree;
	Tree<double> doubleTree;

	testTree(intTree, "int");
	cout << "\n\n";
	testTree(doubleTree, "double");
	cout << endl;
}

运行结果:

在这里插入图片描述

上面使用递归的方法实现新值的插入和三种方式的遍历。至于这三种遍历到底是什么,这里就不再赘述。

C++14/17新特性:变量模板和结构化绑定 C++面试题 阅读详情

相关推荐

ByteTrack C++模板化设计:解耦算法与硬件的关键实践

多目标跟踪(MOT)系统在嵌入式与边缘设备部署时,常面临性能、内存与实时性瓶颈。其根源在于传统实现将算法逻辑与Python运行时深度绑定,导致C++移植困难。模板化编程通过类型参数化、编译期优化与内存布局控制,实现算法逻辑与硬件约束的解耦——支持Eigen/数组/定点数等状态表示,适配内存池/环形缓冲等分配策略,并兼容OpenCV/DICOM/裸指针等异构输入源。这种设计不仅提升帧率与确定性延迟,更支撑车载ECU、FPGA协处理器及医疗影像等严苛场景落地,是工业级MOT工程化的技术基石。

weixin_32244597的博客 125

C++14、C++17新特性

C++14、C++17的新特性(主要的特性,配示例代码)

ZOEKlNG的博客 3148

ByteTrack C++模板化实现:高性能多目标跟踪工程落地方案

多目标跟踪(MOT)是智能视觉系统的核心能力,其工程落地常受限于算法与硬件、精度与性能、通用性与定制化的多重矛盾。ByteTrack凭借高置信度与低置信度检测框协同匹配机制,在ID稳定性与鲁棒性上显著优于传统SORT或DeepSORT,但Python原型难以满足嵌入式部署、跨平台兼容及实时性要求。C++实现需解决卡尔曼滤波高效计算、匈牙利匹配低延迟优化、轨迹状态内存安全管理等关键技术挑战。本方案基于现代C++17模板机制,实现硬件后端(CPU/CUDA/NNIE)、数值精度(float/double/hal

weixin_29038345的博客 205

matplotlib-cpp现代化编程:C++17/20新特性应用终极指南

matplotlib-cpp是一个功能强大的C++绘图库,它巧妙地将Python Matplotlib的强大可视化能力引入到C++环境中。通过利用现代C++17/20的新特性,这个库让C++开发者能够轻松创建高质量的数据可视化图表。🚀 ## 🎯 为什么选择matplotlib-cpp? matplotlib-cpp为C++开发者提供了与Python Matplotlib相似的API体验,同

gitblog_00666的博客 450

模板化ByteTrack C++实现:开箱即用的多目标跟踪骨架

多目标跟踪(MOT)是智能视觉系统的核心能力,其工程落地常受限于算法与部署的割裂——Python原型难以跨平台、嵌入式设备不支持深度学习框架、ID稳定性受卡尔曼滤波与匹配策略影响显著。ByteTrack作为兼顾速度与ID准确率的SOTA方法,其C++实现需解决类型安全、跨平台构建、低耦合扩展三大挑战。本方案以模板化设计解耦检测输入与跟踪逻辑,支持YOLO/RetinaNet等多检测器即插即用;内置Eigen加速卡尔曼预测、JV算法优化匈牙利匹配,并通过对象池管理轨迹生命周期。适用于Jetson、RK3588

weixin_32667955的博客 247

MediaPipeUnityPlugin(最新版)摇摆拳人脸识别

MediaPipeUnityPlugin(最新版)摇摆拳人脸识别

不忘初心 方得始终 2980

Unix运维_如何测试C11和C++11至C17和C++17标准的支持

Unix运维 如何测试C11和C++11至C17和C++17标准的支持。

goufeng93的博客 2144

51单片机智能小车

最近学校举办了一个智能小车比赛,比赛内容为用手机蓝牙控制小车拥有5个功能,分别是超声波舵机避障,测距(显示在LCD1602上), 红外跟随,循迹还有蓝牙控制小车前进后退左转又转以及停止。 文章目录 蓝牙控制所有功能 一、蓝牙控制前进后退左转右转停止 二、舵机超声波避障加测距 三、红外跟随 四、循迹 五、代码部分 六、完整代码 前言 基于51单片机的智能小车是比较基础的小车,很适合初学者学习 提示:以下是本篇文章正文内容,下面案例可供参考 一、整体思路 这款小...

m0_55977420的博客 1万+

Effective C++ 条款43:学习处理模板化基类内的名称

Effective_C++_43

君鼎的博客 644

Aix升级和安装ssh配置过程

Aix升级和安装ssh配置过程Aix升级和安装ssh配置过程配置前背景配置步骤 Aix升级和安装ssh配置过程 部门IBM小型机的Aix操作系统需要进行ssh补丁修复,修复其实就是ssh版本你的升级,对升级的过程和中途遇到的问题做了记录如下。 配置前背景 操作系统: gdboss2/>uname -u //操作系统的Aix5.3 IBM,020693E5P gdboss2/&gt...

阿阿阿唱的博客 2990

【天线与电波传播】电波传播部分 第7章 电波传播的基础知识3

菲涅尔区与菲涅尔带的基本知识

Chen59_2328的博客 1601

C++17新特性-使用constexpr-if简化编译

模板化编程中,通常要以不同的方式做某些事情,比如特化模板类型。C++17带了constexpr-if 表达式,可以在很多情况下简化代码。本节中,我们会实现一个很小的辅助模板类。它能处理不同模板类型的特化,因为它可以在完全不同的代码中,选取 相应的片段,依据这些片段的类型对模板进行特化:1.完成代码中的通用部分。2.假设类型T 是std::vector ,而类型U 是int。这里就有问题了,为整个vector 添加整数是为了什么呢?其应该是对vector 中的每个元素加上一个整型数。

Jamia 549

一张图看懂中国民航适航审定体系:从适航司到审定中心,到底谁管谁?

本文深入解析中国民航适航审定体系,从立法层到执行层的完整链条,揭示适航管理的金字塔结构和核心流程。涵盖适航司、审定中心等关键机构职能,以及型号审定、生产许可审定等全生命周期管理,展现中国适航体系的特色实践与创新探索。

weixin_30896511的博客 475

【C++17 core language features】C++17 属性命名空间的高效使用:避免重复的策略与实践

在现代C++编程中,属性(Attributes)作为一种向编译器提供额外信息的机制,极大地提升了代码的表达力和安全性。自C++11引入属性以来,随着C++标准的不断演进,属性系统也在逐步完善和扩展。正如老子所言:“治大国若烹小鲜。” 管理和组织属性也是如此,需要细致和精确,以达到最佳效果。本章将全面介绍属性命名空间的概述,探讨其在C++17中的发展和应用,强调通过命名空间避免属性重复的重要性。

探索C++编程的奥秘,分享深入的技术见解和实践,旨在激发读者创造力与解决问题的思维。 921

2026年沐曦集成电路数字IC笔试试卷带答案

本文摘要: 这是一份数字IC设计笔试模拟题,包含单选题、波形分析、多选题和代码设计题。重点考察ASIC设计流程(如逻辑综合将RTL转为门级网表)、时序分析(建立时间余量计算)、跨时钟域处理(格雷码应用)、复位机制、状态机设计等核心知识点。涉及沐曦特色考点如GPU架构(SIMT模型)、国产GPU芯片(曦云C系列)及MXMACA软件栈。代码题要求实现50%占空比三分频电路,综合考察Verilog设计能力。试题强调PPA优化、低功耗技术等工程实践要点,覆盖数字IC前端设计全流程核心内容。(149字)

Open 258

【定位模块算法】amcl(蒙特卡洛)粒子滤波定位算法(附C++代码)

系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 TODO:写完再整理 文章目录系列文章目录前言(1)amcl定位框架介绍(2)amcl定位框架组成(1)ROS-TF坐标转换树【ROS的工具】(2)robot_pose_ekf扩展卡尔曼位姿估计算法(3)定位amcl自适应蒙特卡洛定位,adaptive monte carlo localizaion**(3)amcl定位节点的订阅与发布(4)amcl定位功能包用法(1)编写amcl节点相关的launch(amcl_omni.l

qq_35635374的博客 4402

探索C++17的优雅:nano-signal-slot项目深度剖析与推荐

探索C++17的优雅:nano-signal-slot项目深度剖析与推荐 项目介绍 在C++的世界里,信号和槽(Signals and Slots)机制是实现组件间通信的强大工具。nano-signal-slot,作为一个纯C++17的库,轻量且高效,专为现代C++设计,提供了基于模板元编程的信号与槽实现,让异步通信变得简单而直接。 技术分析 nano-signal-slot的核心在于其高度模板化...

gitblog_00239的博客 920

计算机专业保研攻略(一):个人定位与院校推荐(必看干货)

写在最前 又是一年保研季,作为过来人,我深知保研就是一场信息战。在保研过程中,需要注意的信息太多了,最初我也因为经验不足,走了不少弯路。我写下这篇博客,希望能帮助更多有需要的同学。 怎样做好保研信息分析 我想举几个发生在我身边真实的例子: A同学推免排名不如B同学,但A保研去向比B同学好很多; C同学在年级里成绩并不算突出,但他拿到了最好的offer; D、E同学纷纷圆梦北大、浙大等名校,但是他们的成绩似乎并不突出; F同学取得了目标院校夏令营offer,但没有拿到本校推免资格; G同学认为自己拿不到推免

乔卿的博客 8388

C++从入门到精通的编程之旅

然而,能力越大,责任越大。因此,深刻理解栈内存、堆内存的区别,以及如何使用`new`和`delete`运算符进行动态内存分配和释放,是成为一名合格C++程序员的必经之路。现代C++标准引入了智能指针(如`unique_ptr`, `shared_ptr`),它们能够自动管理内存生命周期,极大地减轻了程序员的负担,是编写安全、高效代码的重要工具。”到基础的计算器,都是对这些概念的实践和巩固,为后续更复杂的学习打下坚实的基础。STL是一个强大的、模板化的通用库,包含了一系列精心设计的容器、算法和迭代器。

lhblg2012的博客 303

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

掌握现代 C++:Lambda 在 C++14、C++17 和 C++20 中的演变

lambda从C++14到C++20都有了不少的改进。但也还有更多没有总结进来。这些主要功能伴随着许多小特性让 lambda 代码更容易编写。深入研究 lambda 是更好了解 C++ 语言,值得投入时间。

m0_74824496的博客 346

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

Effective C++笔记_条款43 学习处理模板化基类内的名称

Effective C++笔记_条款43 学习处理模板化基类内的名称      开篇就来了一个示例代码,整个这个小节就围绕这个示例代码来描述模板化基类内的名称(函数)。主要是因为C++知道base class templates有可能被特化,而那个特化版本肯呢个不提供和一般性template相同的接口。因此它往往拒绝在templatized base classes(模板化基类)

zdy0_2004的专栏 516

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

C++ 17 标准!

ISO C++ 委员会已经正式发布了 C++ 17 标准,正式名称为 ISO/IEC 14882:2017。 基于 C++ 11,C++ 17 旨在使 C++ 成为一个不那么臃肿复杂的编程语言,以简化该语言的日常使用,使开发者可以更简单地编写和维护代码。 C++ 17 是对 C++ 语言的重大更新,引入了许多新的语言特性: UTF-8 字符文字 折叠表达式 (fold ex...

学无止境 3677

c语言最新标准17,C++ 17 标准正式发布:终于能更简单地编写和维护代码了!

原标题:C++ 17 标准正式发布:终于能更简单地编写和维护代码了!何为 C++ 17?C++17 是继 C++14 之后,C++ 编程语言 ISO/IEC 标准的下一次修订的非正式名称。而就在昨日,ISO C++ 委员会正式发布了 C++ 17 标准,官方名称为 ISO/IEC 14882:2017。 C++ 17 标准化图表 C ++ 17 主要特性基于 C++ 11,C++ 17 旨在使 C...

weixin_36221923的博客 1450

PerlinNoise项目:现代C++中实现Perlin噪声的终极指南 [特殊字符]

想要在C++项目中创建逼真的自然纹理、地形生成或程序化内容吗?siv::PerlinNoise库为你提供了完美的解决方案!这个**现代C++头文件库**专门用于生成高质量的Perlin噪声,是游戏开发、图形学和模拟应用中的**终极噪声生成工具**。 ## 什么是Perlin噪声?为什么它如此重要?🤔 Perlin噪声是由Ken Perlin在1983年发明的一种梯度噪声算法,广泛应用于计算机

gitblog_00176的博客 839

c语言标准c17,C语言的重大更新 C++17标准发布

最近,ISO C++++ 委员会正式发布了 C++ 17 标准,正式名称为 ISO/IEC 14882:2017。基于 C++ 11,C++ 17 旨在使 C++ 成为一个不那么臃肿复杂的编程语言,以简化该语言的日常使用,使开发者可以更简单地编写和维护代码。C++ 17 是对 C++ 语言的重大更新,引入了许多新的语言特性:UTF-8 字符文字折叠表达式 (fold expressions):用于...

weixin_34170737的博客 3403
上一篇: C++学习-入门到精通【16】自定义模板的介绍
下一篇: C++-入门到精通【18】string类和字符串流处理的深入剖析
YKPG
博客等级 码龄6年 344粉丝 37原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值