• C++ string类介绍以及模拟实现


    string介绍

    何为string?

    string是一个类,准确的说string是一个类模板的实例化在这里插入图片描述
    类模板是basic_string,所以string是用char作为模板参数实例化的一个类对象在这里插入图片描述
    除了实例string,还有其他三个实例

    wstring:针对宽字符的类(国标)
    u16string:针对字符大小为2字节的类(utf-16)
    u32string:针对字符大小为4字节的类(utf-32)

    总之根据编码的标准不同,实例化的类也随之不同,string类的字符大小是1字节,是针对ASCII码的一个类。

    string的构造函数

    在这里插入图片描述
    上面是string的所用构造函数,下面列出几个常用的

    void stringTest1()
    {
    	string str1;// 构造一个空的string对象
    
    	string str2("hello world"); // 用const char *s构造一个string对象
    	cout << str2 << endl;
    
    	string str3(str2); // 用str2拷贝构造str3
    	cout << str3 << endl;
    
    	string str4 = str3; // 本质上不是赋值,而是拷贝构造
    	cout << str4 << endl;     
    
    	// 不太常用
    	string str5("hello world", 3); // 用字符串的第3个到结束的字符构造
    	cout << str5 << endl;
    
    	string str6(10, 'x'); //  用十个x字符构造
    	cout << str6 << endl;
    
    	string str7(str2, 3); // 用str2的第3个到结束的字符构造
    	cout << str7 << endl;
    
    	string str8(str2, 3, 7); // 用str2的第3个到第7个的字符构造
    	cout << str8 << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这里解释一下npos是什么
    在这里插入图片描述
    在这里插入图片描述
    npos是一个无符号整型,但用-1初始化无符号整形得到的值是整形中的最大值(-1的补码是全1,用无符号的方式看待全1的二进制序列,这时-1就是整形中最大的数)。它意味着直到字符串结束,所以使用第3个构造函数,但不传第3个参数,默认会构造从pos位置到字符串结束的string对象。

    string的赋值

    库中对=进行了重载,总共有三种形式
    在这里插入图片描述

    void stringTest2()
    {
    	string str1 = "hello world";
    	string str2; // 构造一个空的string对象
    	str2 = str1; // 将str1赋值给str2
    
    	cout << str1 << endl;
    	cout << str2 << endl;
    
    	str2 = "hello c++"; // 用const char*类型的字符串赋值给str2
    	cout << str2 << endl;
    
    	str2 = '!';         // 用字符赋值给str2
    	cout << str2 << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    string的遍历

    void stringTest3()
    {
    	// 遍历string的方式
    	string str = "hello";
    	// 第一种方式用下标遍历
    	for (size_t i = 0; i < str.size(); i++)
    	{
    		cout << str[i];
    	}
    	cout << endl;
    
    	// 第二种方式用迭代器
    	string::iterator it = str.begin();
    	while (it != str.end())
    	{
    		cout << *it;
    		it++;
    	}
    	cout << endl;
    
    	// 第三种方式范围for
    	for (auto& e : str)
    	{
    		cout << e;
    	}
    	cout << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    解释一下string的迭代器的两个接口,begin()返回的是string的第一个字符的位置的地址,end()返回的是最后一个字符位置的下一个位置的地址
    在这里插入图片描述
    所以当迭代器走到end()指向的地址时不能继续访问,否则会出现非法访问,这也是循环的结束条件

    可以通过下标也能通过at接口遍历string

    void stringTest4()
    {
    	string str1 = "hello";
    	string str2 = "hello";
    
    	str1[7];
    	str2.at(7);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    通过下标访问如果越界,程序会有越界提示
    在这里插入图片描述
    用at接口程序报的错就让人拿不准了
    在这里插入图片描述
    虽然用下标和at接口都能访问string的数据但使用下标访问能在出错时更快找到错误,所以推荐使用下标访问。

    在使用反向迭代器时,需要输入较长的类型名,string::reverse_iterator,而我们可以使用auto来让编译器根据函数返回类型自动推导类型名,这样也提高了编程效率。

    void stringTest5()
    {
    	string str = "hello";
    	//string::reverse_iterator it = str.rbegin();
    	auto it = str.rbegin(); // 与上面的写法等价
    	while (it != str.rend())
    	{
    		cout << *it;
    		it++;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    string的插入与删除

    在这里插入图片描述
    插入函数有很多重载版本

    void stringTest6()
    {
    	string str = "hello";
    	str.insert(0, 3, 'x'); // 向下标为0处插入3个x
    	cout << str << endl;
    
    	str.insert(0, "   "); // 向下标为0处插入3个空格
    	cout << str << endl;
    
    	str.insert(str.begin() + 3, 3, 'y'); // 向下标为3处插入三个y
    	cout << str << endl;
    
    	str.insert(3, 3, 'z'); // 向下标为3处插入三个z
    	cout << str << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在这里插入图片描述

    在这里插入图片描述

    void stringTest7()
    {
    	string str = "hello";
    	str.erase(); // 不传参默认删除所有数据
    	cout << str << endl;
    
    	str = "hello";
    	str.erase(str.begin() + 2); // 删除下标为2处的字符
    	cout << str << endl;
    
    	str = "hello world";
    	cout << str << endl;
    	str.erase(2, 5);   // 从下标为2向后删除5个字符
    	cout << str << endl;
    
    	str = "hello world";
    	cout << str << endl;
    	str.erase(str.begin() + 2, str.begin() + 5); // 删除从下标为2下标为5的字串
    	cout << str << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    string的交换

    void stringTest8()
    {
    	string str1 = "hello";
    	string str2 = "world";
    
    	cout << str1 << endl;
    	cout << str2 << endl;
    
    	str1.swap(str2);
    	//swap(str1, str2); // 两种交换效率不同
    
    	cout << str1 << endl;
    	cout << str2 << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    使用string的提供的交换接口与标准库中自带的交换函数两者有区别吗?使用string提供的交换只是交换两个指向字符串的指针与两个空间大小,而标准库中的交换则是创建一个中间变量,需要调用拷贝构造,但拷贝是深拷贝需要开辟空间,这样一比较显然string提供的交换效率更高。

    string的查找

    在这里插入图片描述
    函数参数基本就是“要查找的字符/字符串”和“要开始查找的位置”,而开始查找的位置默认为0,就是从头开始查找。下面的代码是查找函数的使用

    void stringTest9()
    {
    	string file = "test.cpp.txt";
    
    	size_t pos = file.rfind('.');
    
    	if (pos != string::npos)
    	{
    		cout << file << "后缀:" << file.substr(pos) << endl;
    	}
    
    	string url = "https://cplusplus.com/reference/string/string/?kw=string";
    	size_t pos1 = url.find("://");
    	// 输出协议
    	if (pos1 != string::npos)
    		cout << url.substr(0, pos1 + 3) << endl;
    	else
    		cout << "非法url" << endl;
    
    	size_t pos2 = url.find('/', pos1 + 3);
    	// 输出域名
    	if (pos2 != string::npos)
    		cout << url.substr(pos1 + 3, pos2 - (pos1 + 3)) << endl;
    	else
    		cout << "非法url" << endl;
    
    	// 输出资源
    	cout << url.substr(pos2 + 1) << endl;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    关于string的容量

    在这里插入图片描述
    pushback向string中尾插一个字符。string像一个动态顺序表,有capacity和size保存顺序表的容量和当前的大小,由于string本身存储了一个\0,所以即使是空string也会向内存申请空间以保存\0。

    在这里插入图片描述
    在vs下string的初始容量是15,写一段代码验证capacity的增长

    void stringTest10()
    {
    	string str;
    	unsigned int size = str.capacity(); //先记录初始的容量
    	cout << "capacity:" << str.capacity() << endl;
    
    	for (size_t i = 0; i < 1000; i++)
    	{
    		str.push_back('c');
    		if (size != str.capacity()) // 当扩容时打印扩容后的容量
    		{
    			size = str.capacity();
    			cout << "capacity:" << str.capacity() << endl;
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述
    (在vs下)可以看到除了第一次的扩容其他扩容基本都是1.5倍扩,string有一个接口能改变string的容量,当直到string要存储字符串的长度时可以先改变它的容量,以节省扩容开辟空间的时间。
    在这里插入图片描述
    reserve有保留的意思,与reverse要注意区别在这里插入图片描述
    将string的初始容量改为1000后,与刚刚的程序相比,减少了多次扩容
    在这里插入图片描述
    类似的函数还有一个resize,重置string的size,如果只传要重置的大小,这些空间默认会初始化为\0在这里插入图片描述
    如果再传一个字符,函数会用该字符初始化空间

    string的模拟实现

    string类的声明

    namespace myString
    {
    	class string
    	{
    	public:
    		// 构造和析构
    		string(const char* str = "");			 // string的构造,空串也开辟空间,只存储\0
    		~string();								 // string的析构
    		string(const string& str);               // string的拷贝构造
    		string& operator=(const string& str);	 // string的赋值
    		void swap(string& str);
    
    		// 修改
    		string& operator+=(const string& str);	 // string的追加
    		string& operator+=(char c);	             // string的追加
    		string& append(const char* str);		 // string的追加
    		string& append(char c);				     // string的追加
    		void push_back(char c);				     // string的尾插
    		string& insert(size_t pos, char c);
    		string& insert(size_t pos, const char* str);
    		string& erase(size_t pos, size_t n);     // 从pos位置删除n个字符
    		const char* c_str() const { return _str; } // 返回c类型的字符串
    
    		// 容量
    		void resize(size_t n, char c = '\0');	 // 修改string的大小
    		void reserve(size_t n);					 // 修改string的容量
    		size_t size() const { return _size; }					
    		size_t capacity() const { return _capacity; }			
    		
    		// 迭代器
    		typedef char* iterator;
    		typedef const char* const_iterator;      // 迭代器的重定义
    		iterator begin() { return _str; }
    		iterator end() { return _str + _size; }
    		const_iterator begin() const { return _str; }
    		const_iterator end() const { return _str + _size; }
    
    		// 下标访问
    		char& operator[](size_t pos);             // 通过下标访问string
    		const char& operator[](size_t pos) const; // 通过下标访问string
    
    	
    	private:
    		char* _str;
    		size_t _size;
    		size_t _capacity;
    
    		const static size_t npos;
    	};
    	const size_t string::npos = -1; // 静态变量在类中声明但没有定义,需要在类外定义
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    首先说明capacity表示的是string最大能存储的有效字符个数(不包括’\0’),size表示当前存储的有效字符个数,当然也不包括’\0’,str就是指向存储字符串的指针。

    构造和析构

    在这里插入图片描述

    首先实现的是string的构造和析构,构造函数呢,实现成无参的,这样不仅可以构造空串还能传字符串进行构造。先strlen求传入字符串的长度,将长度赋值给_size和_capacity,如果是空串长度就是0,然后为_str分配空间,大小是长度+1,这个1用来存储’\0’,最后再拷贝传入字符串到_str中。

    myString::string::string(const char* str)// 说明写了默认参数,定义不用也不能写
    {
    	_size = strlen(str);
    	_capacity = _size;
    	_str = new char[_capacity + 1];
    	strcpy(_str, str);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    析构就是将_str释放,_size和_capacity重置为0。

    myString::string::~string()
    {
    	delete[] _str;
    	_capacity = _size = 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    拷贝构造有两种写法一种是利用构造函数,一种是不利用构造函数,但实现的代码与构造函数重复度高,因此复用构造函数实现拷贝构造更方便。

    myString::string::string(const string& str) // 传统写法
    {
    	_size = str._size;
    	_capacity = str._capacity;
    	_str = new char[_capacity + 1];
    	strcpy(_str, str._str);
    }
    myString::string::string(const string& str) // 现代写法
    {
    	string tmp(str._str); // 先用str的字符串构造tmp对象
    	// 此时的tmp就是str的复制,只要把tmp与this交换
    	swap(tmp);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    当然还要实现swap函数,利用std库中的交换将两个指针交换,还有_capacity和_size也要交换

    void myString::string::swap(string& str)
    {
    	std::swap(_str, str._str);
    	std::swap(_capacity, str._capacity);
    	std::swap(_size, str._size);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    =的重载同样也是两种写法,传统的繁琐,现代的简洁。不过对于任何=的重载都要注意连续复制的情况,因此函数需要返回赋值完成的对象。

    对于传统赋值,先判断容量是否足够存储字符串,若不够需要释放之前的空间再开辟一块足够的空间存储。

    myString::string& myString::string::operator=(const string& str)
    {
    	if (&str != this) // 防止自己赋值给自己
    	{
    		if (str._size > _capacity)
    			_str = new char[str._size]; // 空间不够的扩容
    	
    		_size = str._size;
    		_capacity = str._capacity;
    		strcpy(_str, str._str);
    	}
    	return *this;
    }
    
    // 现代写法
    myString::string& myString::string::operator=(string str)
    {
    	swap(str); // 形参不是引用,所有调用了拷贝构造,构造了str,所以str是一个复制
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    显而易见,这样的复用构造函数让代码更简单也更简洁了

    关于修改

    在这里插入图片描述

    修改无非就是增加与删除,实现了在任意位置的插入与删除,其他的接口也就能复用这两个接口。

    说白了,插入就是检查容量,移动数据,插入数据,三个步骤

    myString::string& myString::string::insert(size_t pos, char c)
    {
    	if (_size == _capacity)
    		reserve(_capacity == 0 ? 4 : _capacity * 2); 
    	size_t end = _size + 1; // _str[_size]是'\0',一起移动
    	while (end > pos)
    	{
    		_str[end] = _str[end - 1];
    		end--;
    	}
    	_str[pos] = c;
    	_size++;
    	return *this;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    (这里需要补充size_t的一个注意点,如果上面end先指向_size - 1的位置,然后循环写为end >= pos,移动的代码写为 _str[end + 1] = _str[end],这就很有问题,当pos为0,end为0时再走一遍循环,而end-1为-1,对吗?

    myString::string& myString::string::insert(size_t pos, char c)
    {
    	assert(pos <= _size);
    	if (_size == _capacity)
    		reserve(_capacity + 1); 
    	size_t end = _size - 1; // 错误示范
    	while (end >= pos)
    	{
    		_str[end + 1] = _str[end];
    		end--;
    	}
    	_str[pos] = c;
    	_size++;
    	return *this;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    end是无符号数size_t,所以-1存储到end中是一个很大的数,这样使得循环继续,但越界访问。因此不能这样写,总结:使用无符号数比较要特别注意“负数”问题)

    插入的另一个重载:插入一串字符到string中,和插入一个字符类似,只是移动字符的距离变长了

    myString::string& myString::string::insert(size_t pos, const char* str)
    {
    	assert(pos <= _size);
    	int len = strlen(str);
    	if (len + _size >= _capacity) // 检查扩容
    	{
    		reserve(len + _size);
    	}
    	
    	size_t end = _size + len;
    	while (end - len + 1 > pos)
    	{
    		_str[end] = _str[end - len];
    		end--;
    	}
    	memcpy(_str + pos, str, len);
    	_size += len;
    	return *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    删除字符:函数第一个参数是要删除字符的位置,后一个参数是要删除字符的个数。如果位置加上字符个数大于字符串长度,就是把该位置后面的字符全删除,直接在该位置上放个’\0’。但如果不是全删除就需要移动后面的字符覆盖前面的字符。

    myString::string& myString::string::erase(size_t pos, size_t n)
    {
    	assert(pos < _size);
    	if (n + pos >= _size)
    	{
    		_str[pos] = '\0';
    		_size = pos; // n的值可以是npos,不能减去npos因为npos可能是-1,最大的数
    		return *this;
    	}
    	else
    	{
    		size_t end = pos + n; // 用end移动数据
    		while (end <= _size) // 把'\0'也移过去
    		{
    			_str[end - n] = _str[end];
    			end++;
    		}
    		size -= n;
    		return *this;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    npos是一个最大的数,无符号的-1,作为string的静态成员。

    剩下的接口就是复用insert和erase函数了

    myString::string& myString::string::operator+=(const string& str)
    {
    	return insert(_size, str._str);
    }
    
    myString::string& myString::string::operator+=(char c)
    {
    	return insert(_size, c);
    }
    
    myString::string&  myString::string::append(const char* str)
    {
    	return insert(_size, str);
    }
    
    
    myString::string&  myString::string::append(char c)
    {
    	return insert(_size, c);
    }
    
    void myString::string::push_back(char c)
    {
    	insert(_size, c);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    (范围for底层是迭代器,不实现迭代器就不能用范围for,并且迭代器的命名必须按约定走。假设我把模拟实现的迭代器屏蔽,程序报错)
    在这里插入图片描述

    关于容量

    在这里插入图片描述
    reserve为string扩容,先检查n是否大于当前容量,如果大于则扩容,小于不缩容。先开辟新的空间,将原来空间的数据拷贝到新空间,再释放原来的空间。

    void myString::string::reserve(size_t n)
    {
    	if (n > _capacity)
    	{
    		char* tmp = new char[n + 1];
    		strcpy(tmp, _str);
    		delete[] _str;
    		_str = tmp;	
    		_capacity = n;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    resize就是reserve加初始化了,没有给初始化的值就用’\0’初始化。

    void myString::string::reserve(size_t n, char c)
    {
    	if (n > _capacity)
    		reserve(n);
    	
    	while (_size < n)
    		_str[_size++] = c;
    	
    	_size = n; // 如果n小于_size,上面的while不会进去,但长度要减小	
    	_str[n] = '\0';  // 最后再结束的地方放'\0'
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    下标访问

    在这里插入图片描述
    重载[],通过下标访问字符串,但要注意如果string被const修饰则不能写入字符串,所以需要重载两个版本来支持const对象

    char& myString::string::operator[](size_t pos)
    {
    	assert(pos < _size);
    	return _str[pos];
    }
    
    const char& myString::string::operator[](size_t pos) const
    {
    	assert(pos < _size);
    	return _str[pos];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    cin和cout的重载

    cout的重载不能直接输出字符串,而是应该一个字符一个字符的输出,考虑到极端情况,string中存储了’\0’,直接输出的话字符串也就不完整

    ostream& operator<<(ostream& out, const myString::string& str)
    {
    	for (int i = 0; i < _size; i++)
    	{
    		out << _str[i];
    	}
    	return out; //  为了支持连续输出
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    cin的重载也是一个一个字符的读,直到读入的字符为空格或者换行,但是直接使用>>遇到空格和换行也是停止的,所以>>永远无法读入空格和换行,要用istream对象的get函数,每次读一个字符,但不会停止

    istream& operator>>(istream& in, myString::string& str)
    {
    	char ch;
    	ch = in.get();
    	while (ch != ' ' && ch != '\n')
    	{
    		push_back(ch);
    		ch = in.get();
    	}
    	return in;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 相关阅读:
    Service Mesh技术详解
    MapStruct的使用
    行缓冲,全缓冲,无缓冲的详细介绍
    前端基础入门之JS的call、apply和argument
    计算机算法分析与设计(12)---贪心算法(最优装载问题和哈夫曼编码问题)
    Qt的WebEngineView加载网页时出现Error: WebGL is not supported
    IP协议的特性
    外汇天眼周回顾:Equiti开设最新办事处,Vantage推出Vantage Connect服务
    还不到6个月,GPTs黄了
    010 gtsam/examples/ImuFactorsExample2.cpp
  • 原文地址:https://blog.csdn.net/weixin_61432764/article/details/126075245