C字符串函数内存函数的模拟实现和理解

news2024/11/28 18:35:55

目录

1. 字符串操作函数

1.1. 求字符串长度

1.1.1. strlen

1.2. 长度不受限制的函数

1.2.1. strcpy

1.2.2. strcat

1.2.3. strcmp

1.3. 长度受到限制的字符串函数

1.3.1. strncpy

1.3.2. strncat

1.3.3. strncmp

1.4. 字符串查找函数

1.4.1. strstr

1.4.2. strtok

1.5. 错误信息报告函数

1.5.1. strerror

 1.5.2. perror

1.6. 字符分类函数

2. 内存操作函数

2.1. memcpy

2.2. memmove

2.3. memcmp

2.4. memset


1. 字符串操作函数

1.1. 求字符串长度

1.1.1. strlen

//函数原型
size_t strlen( const char *string );

strlen这个函数就是求一个字符串的有效字符的个数。

1. strlen会以'\0'作为结束标志,其返回的是在字符串中'\0'之前出现的字符个数(且不包含'\0')

2. 参数指向的字符串必须要有'\0'作为结尾,如若没有,则结果不确定。

3. 其返回值类型为size_t

为了更好地理解strlen,接下来分别以三种方式模拟实现我们的strlen

// 通过一个计数器
size_t my_strlen1(const char* str)
{
	size_t count = 0;
    //如果当前字符不为'\0',就++计数器
	while (*str++ && ++count);
	return count;
}

// 通过递归思路
size_t my_strlen2(const char* str)
{
	//如果当前字符不为'\0', 递归调用
	if (*str)
		return my_strlen2(str + 1) + 1;
	// 如果当前字符为'\0',返回0
	else
		return 0;
}

// 通过指针 - 指针 等于 俩指针间的元素个数
size_t my_strlen3(const char* str)
{
	// 起始地址
	const char* start = str;
    // 结束地址
	const char* end = str;
	// 这里之所以用前置++,是因为循环结束时,此时的end是指向'\0'的
	while (*++end);
	return end - start;
}

1.2. 长度不受限制的函数

1.2.1. strcpy

// 函数原型
char *strcpy( char *strDestination, const char *strSource );

strcpy函数就是将一个字符串的内容拷贝给另一个字符串。

但有几点需要注意:

1. 源字符串必须要有'\0';

2. 目标字符串必须有足够的空间

3. 拷贝时,会将源字符串的'\0'也拷贝给目标字符串

4. 其返回值是目标字符串的起始位置

注意:strcpy函数的返回类型的设置是为了实现链式访问。

为了更深入的理解,我们自然要去模拟实现一下我们的strcpy:

char* my_strcpy(char* str_destination, const char* str_source)
{
	assert(str_destination && str_source);
	// 保存目标字符串的起始位置
	char* start = str_destination;
	// 下面这种方式还可以简化:
	/*while (*str_destination = *str_source)
	{
		++str_destination;
		++str_source;
	}*/
	while (*str_destination++ = *str_source++);
    //返回目标字符串的起始位置 
	return start;
}

1.2.2. strcat

// 函数原型
char *strcat( char *strDestination, const char *strSource );

strcat被称之为字符串追加函数,将源字符串的内容追加到目标字符串中。

同样要注意以下几点:

1. 源字符串必须要有'\0'
2. 目标字符串需要有足够的空间
3. 目标空间必须可修改
4. 追加过程中,会覆盖掉目标字符串中首次出现的'\0',同时将源字符串中的'\0'追加到目标字符串中
同样,为了更好地理解这个函数,我们需要自己模拟实现一下
注意:我们下面实现的这个函数不支持自己给自己追加。
char* my_strcat(char* str_destination, const char* str_source)
{
	assert(str_destination && str_source);
	// 1. 保存目标字符串的起始地址
	char* start = str_destination;
	// 2. 先找目标字符串中的'\0'
	while (*str_destination && str_destination++); 
	// 3. 从原目标字符串的'\0'开始,将源字符串的字符依次赋给目标字符串,包含'\0'
	while (*str_destination++ = *str_source++);
	// 4. 返回目标字符串的起始地址
	return start;
}

1.2.3. strcmp

// 函数原型
int strcmp( const char *string1, const char *string2 );

strcmp函数的作用是比较其两个字符串,标准规定如下:

当string1 less than string2 时,其返回 < 0

当string1 greater than string2 时,其返回 > 0

当string1 identical to string2 时,其返回0

那么如何比较呢?

注意:strcmp函数不是比较两个字符串的长度,而是比较字符串中对应位置上的字符ASCII的大小,如果相同,就比较下一对儿,直到不同(不同就判断谁大谁小)或者都遇到'\0'(返回0)。

为了更好地理解这个函数,我们模拟实现一下:

int my_strcmp(const char* str1, const char* str2)
{
	assert(str1 && str2);
	// 如果当前这对字符相等,那么就继续
	while (*str1 == *str2)
	{
		// 在继续之前,需要判断当前字符是否是相等
        // 如果是'\0',说明两个字符串都结束了,返回0
		if (*str1 == '\0')
		{
			return 0;
		}
		++str1;
		++str2;
	}
	// 出了循环,则说明当前遇到不相等的字符了,那么此时就进行比较当前两个字符的ASCII大小
	if (*str1 > *str2)
		return 1;
	else
		return -1;
}

1.3. 长度受到限制的字符串函数

首先,之所以会有长度受限制的字符串函数,是因为上面的这些长度不受限制的函数可能在某些场景下会导致进程crash。例如:

void Test5(void)
{
	char str1[5] = "haha";
	const char* str2 = "cowsay hello";
	strcpy(str1, str2);  // 此时编译可以通过
} 

但是我们发现,此时的进程就崩溃了。为什么呢?原因是因为strcpy这个函数再复制的时候会无脑去找'\0',不遇到'\0',strcpy不结束,但由于str1只能容纳5个有效字符,str2将自己的所有内容复制到str1中,就会导致非法访问,进而引发进程崩溃。

因此,人们为了解决这些问题,便有了长度受到限制的字符串函数。即你让我拷贝(追加、比较)几个字符,我就拷贝(追加、比较)几个字符。

1.3.1. strncpy

// 函数原型
char *strncpy( char *strDest, const char *strSource, size_t count );

strncpy函数就是将一个字符串的n个字符的内容拷贝给另一个字符串。

但有几点需要注意:

1. 源字符串必须要有'\0';

2. 目标字符串必须有足够的空间

3. 拷贝时,会将源字符串的n个字符拷贝给目标字符串

4. 其返回值是目标字符串的起始位置

5. 拷贝过程中,如果源字符串走到了'\0',并且此时n!=0,那么填充'\0',直到n等于0,循环结束

注意:strncpy函数的返回类型的设置是为了实现链式访问。

为了更深入的理解,我们自然要去模拟实现一下我们的strncpy:

char* my_strncpy(char* str_destination, const char* str_source,size_t count)
{
	assert(str_destination && str_source);
	// 保留目标字符串的起始地址
	char* start = str_destination;
	// 拷贝count个字符
	while (count--)
	{
		// 如果源字符串走到了'\0',并且此时count != 0,那么填充'\0'
		if (*str_source == '\0')
			*str_destination++ = '\0';
		else
			*str_destination++ = *str_source++;
	}
	// 返回目标字符串的起始地址
	return start;
}

1.3.2. strncat

// 函数原型:
char *strncat( char *strDest, const char *strSource, size_t count );

strncat被称之为字符串追加函数,将源字符串的n个字符内容追加到目标字符串中。

同样要注意以下几点:

1. 源字符串必须要有'\0'
2. 目标字符串需要有足够的空间
3. 目标空间必须可修改
4. 追加过程中,会覆盖掉目标字符串中首次出现的'\0',同时追加源字符串的n个字符,并且最后会将'\0'也追加过去。如果此时的n大于源字符串的长度,那么strncat只会将源字符串的内容追加过去(包括'\0'),但不会继续填充,这点与strncpy稍有差异。
同样,为了更好地理解这个函数,我们需要自己模拟实现一下
注意:strncat可以支持自己给自己追加
接下来,我们模拟实现一下我们的strncat
char* my_strncat(char* str_destination, const char* str_source,size_t count)
{
	assert(str_destination && str_source);
	// 保存目标字符串的起始地址
	char* start = str_destination;
	//先找目标字符串的'\0'
	while (*str_destination && str_destination++); 
	// 进行追加
	while (*str_source && count--)
	{
		*str_destination++ = *str_source++;
	}
	// 将目标字符串的末尾置为'\0'
	*str_destination = '\0';
    // 返回目标字符串的起始位置
	return start;
}
void Test7(void)
{

	char str1[25] = "fasgqw\0eeeeeee";
	const char* str2 = "cow";
	const char* str3 = "cowsay";
	my_strncat(str1, str2, 5);
}

1.3.3. strncmp

strncmp函数的作用是比较其两个字符串,标准规定如下:

当string1 less than string2 时,其返回 < 0

当string1 greater than string2 时,其返回 > 0

当string1 identical to string2 时,其返回0

那么如何比较呢?

注意:与strcmp不同的是,strncmp会只比较n个字符,即只比较n次,其比较逻辑与strcmp差异不大。

接下来我们模拟实现一下:

int my_strncmp(const char* str1, const char* str2,size_t count)
{
	assert(str1 && str2);
	// 如果当前这对字符相等 且 count != 0,那么就继续
	while (*str1 == *str2 && count--)
	{
		// 如果遇到了'\0'或者count == 0 那么返回0
		if (str1 == '\0' || count == 0)
			return 0;
		++str1;
		++str2;
	}
	// 出了循环,则说明当前遇到不相等的字符了且count != 0,那么此时就进行比较当前两个字符的ASCII大小
	if (*str1 > *str2)
		return 1;
	else
		return -1;
}

1.4. 字符串查找函数

1.4.1. strstr

//函数原型
char *strstr( const char *string, const char *strCharSet );

strstr函数是一个查找子串的C函数,其会在string这个字符串里查找是否有strCharSet这个子串。
返回值:

如果查找到了合适子串,那么返回这个子串的起始地址。

如果查完string这个目标串,都没有找到,那么返回NULL;

接下来,我们分析一下如何实现这个strstr呢?

// 假设当前我要在str1中查找是否存在str2这个子串
const char* str1 = "abcccdef";
const char* str2 = "ccde";

思考,如果我们此时只借助一对指针变量作为辅助,可行吗?如下:

其比较逻辑比较简单,当前的一对字符不相等,str1++,相等同时++,如果当str2走到了'\0',那么说明找到了合适的子串,反之如果,当str1走到了'\0',那么说明没有这个子串

有了上面的分析,我们认为,单靠一对指针变量是无法完成任务的,我们可以让str1、str2保持原位置不懂,让它们始终指向首元素的位置,我们定义s1和s2、以及cur,s1和cur的初始位置是str1,s2的初始位置是str2,如果cur走到了'\0'那么说明没有合适的子串,如果找到了合适的子串,那么返回cur即可,处理逻辑如下:

char* my_strstr(const char* string, const char* str_ret)
{
	assert(string && str_ret); 
	const char* s1 = string;
	const char* s2 = str_ret;
	char* cur = (char*)string;
	while (*cur)
	{
        // s1从cur的位置开始
		s1 = cur;
		// 如果当前字符相等,就判断下一对字符
		while (*s1 == *s2)
		{
			++s1;
			++s2;
			// 如果s2走到了'\0' 就说明当前cur所在的子串就是目标子串
			if (*s2 == '\0')
				return cur;
            // 如果s1走到了'\0' 那么说明当前cur所在的位置没有目标子串,因此需要判断下一个位置的cur
			if (*s1 == '\0')
				break;
		}
		// 走到这里说明当前的cur是不匹配的,因此++cur
		// 且str2要回到原点
		++cur;
		s2 = str_ret;
	}
	// 如果 cur走到了 '\0' 说明找不到这个子串,返回NULL
	return NULL;
}

1.4.2. strtok

//函数原型:
char *strtok( char *str, const char *sep );
sep 参数是个字符串,定义了用作分隔符的字符集合
第一个参数指定一个字符串,它包含了 0 个或者多个由 sep 字符串中一个或者多个分隔符分割的标记。
strtok 函数找到 str 中的下一个标记,并将其用 ' \0'  结尾,返回值:返回被分割子串的起始位置。(注: strtok 函数会改变被操作的字符串,所以在使用strtok 函数切分的字符串一般都是临时拷贝的内容并且可修改。)
strtok 函数的第一个参数不为 NULL ,函数将找到 str 中第一个标记, strtok 函数将保存它在字符串中的位置。
strtok 函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
如果字符串中不存在更多的标记,则返回 NULL 指针。
具体使用如下:
void Test10(void)
{
	const char* str = "forever forward@never give up.";
	char arr[40] = { 0 };
	const char* sep = "@.#";
	strcpy(arr, str);
	char* ret = strtok(arr, sep);
	/*
	*  forever forward@never give up.  被切割为: 将 @转化为 \0
	*  forever forward\0never give up.
	*  此时的ret的值就是forever forward\0的起始地址
	*/
	printf("%s\n", ret);     // forever forward
	ret = strtok(NULL, sep);  
	/*
	*  当第一个参数为NULL时,那么strtok函数,会继续切割上一个字符串
	*  此时相当于会从上次的分隔符的下一个位置开始
	*  具体为上面的 never give up. ,即从这个字符串的起始位置开始
	*  当遇到 . 这个分隔符
	*  never give up. 会被分割:  将. 转化为 \0
	*  never give up\0  
	*  此时ret的值就是 never give up\0的起始地址
	*/
	printf("%s\n", ret);  // never give up
}

有时候分隔符很多,被分割的字符串也很多,上面的写法就显得太粗糙了,因此,我们可以利用循环解决这个问题: 

void Test11(void)
{
	const char* str = "forever forward@never give up.";
	char arr[40] = { 0 };
	const char* sep = "@.#";
	strcpy(arr, str);
	for (char* ret = strtok(arr, sep); ret != NULL; ret = strtok(NULL, sep))
	{
		printf("%s\n", ret);
	}
}

相信很多人在看到strtok的操作时,感觉很迷惑,为什么当传递NULL时,它会继续在原有基础之上切割一个字符串呢?其实我们可以用一个全局的静态变量,简单实现下我们的strtok:

// 判断当前字符是否是分隔符
bool is_exist_sep(char ch, const char* sep)
{
	while (*sep)
	{
		if (ch == *sep)
			return true;
		++sep;
	}
	return false;
}

char* my_strtok(char* str, const char* sep)
{
    // 用于保存分隔符的下一个位置
    static char* _strtok = NULL;

	// start起始位置
	char* start = str;
	// 如果传递过来的第一个参数是NULL
	// 那么说明以前被分割过,因此取上次被置为'\0'的下一个位置
	// 也就是我们定义的那个局部静态变量
	if (start == NULL)
		start = _strtok;
	// 用于当前被分割子串的起始位置
	char* ret = start;
	while (!is_exist_sep(*start, sep))
	{
		++start;
		if (*start == '\0')
			return NULL;
	}
	// 走到这里说明于到了分隔符
	// 因此将分隔符置为'\0',同时将下一个位置保存到这个局部静态变量中
	*start = '\0';
	_strtok = ++start;
	// 返回被分割的子串的起始位置
	return ret;
}

1.5. 错误信息报告函数

1.5.1. strerror

// 函数原型
// 头文件包含 #include <errno.h> && #include <string.h>
char *strerror( int errnum );

上面这个函数没啥说的,就是当进程出现错误的时候,这个函数会根据对应的错误码打印对应错误信息。

strerror的使用:

void Test13(void)
{
	int *ptr = (int*)malloc(1024u * 1024u * 1024u * 2u);
	if (!ptr)
		printf("%s\n", strerror(errno));  // errno 是一个C标准定义的全局变量
	else
		printf("hehe");
}

 1.5.2. perror

// 函数原型   
// 头文件: #include <stdio.h>
void perror( const char *string );

perror也是一个打印进程错误信息的函数,只不过较strerror相比,它的使用更为简便。

void Test14(void)
{
	int *ptr = (int*)malloc(1024u * 1024u * 1024u * 2u);
	if (!ptr)
		perror("malloc");  // 这里面的信息需要我们显示传递,但没有限制
	else
		printf("hehe");
}

1.6. 字符分类函数

函数(返回值类型都为bool)  如果它的参数符合下列条件就为真
iscntrl
任何控制字符
isspace
空白字符:空格' ',换页'\f',换行'\n',回车'\r',制表符'\t'或者垂直制表符'\v'
isdigit
十进制数字 0~9
isxdigit
十六进制数字,包括所有十进制数字,小写字母a~f,大写字母A~F
islower
小写字母a~z
isupper
大写字母A~Z
isalpha
字母a~z或A~Z
isalnum
字母或者数字,a~z,A~Z,0~9
ispunct
标点符号,任何不属于数字或者字母的图形字符(可打印
isgraph
任何图形字符
isprint任何可打印字符,包括图形字符和空白字符

2. 内存操作函数

2.1. memcpy

// 函数原型
void *memcpy( void *dest, const void *src, size_t count );
  1. 函数memcpysrc的起始位置开始向后拷贝count个字节的数据到dest的内存位置。
  2. 这个函数在遇到 '\0' 的时候并不会停下来,因为它是针对内存级别的拷贝
  3. 如果srcdest有任何的重叠,拷贝的结果都是未定义的

那么如何模拟实现我们的memcpy函数呢?

// 这里之所以用void*,是因为void*可以接受任意类型的指针变量
void* my_memcpy(void* dest, void* src, size_t count)
{
	assert(src && dest);
	// 保存目标位置的起始位置
	void* start = dest;
	// 按字节处理
	while (count--)
	{
		// 由于dest和src的类型都是void*,不支持解引用,因此在这里要强转
		// 并且要求按一个一个字节处理,因此强转为char*
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	// 返回目标位置的起始位置
	return start;
}

注意:我们所实现的memcpy不支持src和dest的重叠拷贝,什么意思呢?如下:

为什么呢?因为当我们的memcpy的拷贝逻辑是从前往后拷贝的。当把1拷贝给4时,此时这个4就会被覆盖成1,当下次要用4的时候,此时已经是1了,这也就是为什么我们看到的结果是这样。于是为了解决重叠拷贝的问题,人们设计除了memmove,它是专门用来处理重叠拷贝的内存函数。 

2.2. memmove

// 函数原型
void *memmove( void *dest, const void *src, size_t count );
  1. memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
  2. 如果源空间和目标空间出现重叠,就得使用memmove函数处理。

那么如何设计我们的memmove函数呢?当发生重叠拷贝时,如何解决呢?有人说我们可以更改一下拷贝的顺序,但是如果是下面的场景呢?

void Test16(void)
{
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	my_memove(arr + 3, arr, 20);
	my_memove(arr, arr + 3, 20);
}

你会发现,如果我们将拷贝顺序设置为绝对的,那么此时上面两种情况比有一种出错。因此为了控制合理顺序我们需要分析一下src和dest的位置关系:

如果dest和src的内容没有交叉情况,不管你是从前往后还是从后往前拷贝,都无所谓。

因此,我们总结如下:

当src < dest时,我们以从后往前的方式将src的内容拷贝给dest

当src > dest时,我们以从前往后的方式将src的内容拷贝给dest

有了这样的理解,我们的代码如下:

void* my_memmove(void* dest, void* src, size_t count)
{
	assert(dest && src);

	//1. 保存目标位置的起始位置
	void* start = dest;

	//2. 拷贝逻辑:

	/* 
	*  如果src < dest,我们选择从后往前拷贝
	*  假设以我们刚刚画的图为准
	*  [01 00 00 00 02 00 00 00 03 00 00 00 {04 00 00 00 05 00 00 00] 06 00 00 00 07 00 00 00 08 00 00 00} ...
	*  即把5的最后一个字节赋值给8的最后一个字节,--count,循环继续
	*/
	if (src < dest)
	{
		while (count--)
		{
			//借助count,从后往前拷贝
			*((char*)dest + count) = *((char*)src + count);
		}
	}
	else
	{
		while (count--)
		{
			// 从前往后拷贝的逻辑,与memcpy的逻辑一致
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	//3. 返回目标地址的起始位置
	return start;
}

2.3. memcmp

// 函数原型
int memcmp( const void *buf1, const void *buf2, size_t count );
比较从buf1 和buf2 指针开始的 count 个字节
返回值如下:

 如很模拟实现我们的memcmp呢?

int my_memcmp(const void* buf1, const void* buf2, size_t count)
{
	assert(buf1 && buf2);
	// 最多比较count个字节
	// 如果当前的一对字节内容一致,继续比较下一对字节
	while (count-- && *(char*)buf1 == *(char*)buf2)
	{
		buf1 = (char*)buf1 + 1;
		buf2 = (char*)buf2 + 1;
		// 如果count == 0,那么说明比较的这些字节全部对应相等,返回0
		if (!count)
			return 0;
	}
	// 走到这里说明,count !=0 ,且buf1和buf2所指的这个字节的内容不一样
	if (*(char*)buf1 > *(char*)buf2)
		return 1;
	else
		return -1;
}

2.4. memset

// 函数原型
void *memset( void *dest, int ch, size_t count );

对memset的简单理解:

dest:目标空间的起始位置

ch:你要对每个字节设置的值

count:你要设置多少个字节

对于memset我们也简单模拟实现一下:

void* my_memset(void* dest, int ch, size_t count)
{
	assert(dest);
	// 保存目标空间的起始位置
	void* start = dest;
	// 将count个字节的值设置为ch
	while (count--)
	{
		*(char*)dest = ch;
		dest = (char*)dest + 1;
	}
	// 返回目标空间的起始位置
	return start;
}
void Test18(void)
{
	int arr[3] = { 0x11223344, 0x22222222, 0x33333333 };
	my_memset(arr, 0, 12);
}

memset是以字节为单位设置内存单元的。

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

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

相关文章

辅助驾驶功能开发-功能规范篇(22)-3-L2级辅助驾驶方案功能规范

1.3.3 TLA系统功能定义 1.3.3.1 状态机 1.3.3.2 状态迁移图 1.3.3.3 功能定义 1.3.3.3.1 信号需求列表 1.3.3.3.2 系统开启关闭 1)初始化 车辆上电后,交通灯辅助系统(TLA)进行初始化,控制器需在 220ms 内发出第一帧报文,并在 3s 内完成内部自检,同时上电 3s 内不进行…

wps表格按分隔符拆分单元格

有数据如下&#xff1b;看选中区域&#xff0c;一个单元格中有一个v&#xff0c;空格&#xff0c;然后有三个数值&#xff0c;以空格分开&#xff1b;点击菜单中的数据-分列&#xff1b; 弹出分列向导&#xff1b;选择 分隔符号&#xff1b; 选择分隔符为空格&#xff1b;出现预…

【vtk学习笔记1】编译安装vtk9.2.6,运行官方例子

一、编译安装vtk-9.2.6 1. 下载VTK。推荐从github下载。目前从VTK官网只能下载最新的RC版或者以前的老版本&#xff0c;我是在github上下载的vtk9.2.6 tag版本。 2. 用Cmake-gui配置Visual Studio工程。主要注意配置VTK安装的路径、是否支持QT&#xff0c;需要的话正确配置Qt5…

3.6 纹理压缩——包体瘦身术

一、什么是纹理压缩 纹理压缩是为了解决内存、带宽问题&#xff0c;专为在计算机图形渲染系统中存储纹理而使用的图像压缩技术。 二、为什么要纹理压缩 图片格式 图片格式是图片文件的存储格式&#xff0c;通常在磁盘、内存中存储和传输文件时使用。例如&#xff1a;JPG、PNG…

JAVA数据类型和变量

一、字面常量 常量即程序运行期间&#xff0c;固定不变,不可修改的量称为常量。 public class Demo{public static void main(String[] args){System.Out.println("hello world!");System.Out.println(100);System.Out.println(3.14);System.Out.println(A);System…

python项目之酒店客房入侵检测系统的设计与实现

项目简介 酒店客房入侵检测系统的设计与实现实现了以下功能&#xff1a; 1、控制台&#xff1a; 控制台是整个系统的首页面。在控制台中&#xff0c;酒店的客房管理人员能够在该页面中查看到当前的空余客房数量、当前在店的客房人数、当前的已用客房数量、当前酒店全部的客房…

【顺序栈的出栈,链栈的表示和实现,递归定义】

文章目录 顺序栈的出栈 链栈的表示和实现链表的初始化判断链栈是否为空链栈的入栈链栈的出栈 递归定义函数的调用过程 顺序栈的出栈 &#xff08;1&#xff09;判断是否栈空&#xff0c;若空则出错&#xff08;下溢&#xff09;。 &#xff08;2&#xff09;获取栈顶元素e。 &…

Java题:查找单链表中第 k 个节点元素的值

遇到过一道奇奇怪怪的Java题&#xff0c;就整理出自己的想法&#xff0c;不知道对不对&#xff0c;还望大佬们指导。 题目 给定一个单链表&#xff0c;查找单链表中第 k 个节点元素的值&#xff0c;同时要求使用时间复杂度低的算法实现。 单链表的定义如下&#xff1a; cla…

【深度学习】吴恩达课程笔记(一)——深度学习概论、神经网络基础

笔记为自我总结整理的学习笔记&#xff0c;若有错误欢迎指出哟~ 吴恩达课程笔记——深度学习概论、神经网络基础 一、概念区别1.深度学习与机器学习2.深度学习与神经网络 二、什么是神经网络1.分类2.特点3.工作原理4.神经网络示意图5.神经网络进行监督学习6.深度学习的发展 三、…

mathtype怎么更改编号 mathtype章节编号错乱怎么办

mathtype作为一款功能强大的公式编辑器&#xff0c;使用范围广泛&#xff0c;与多款软件兼容。但新手可能会对mathtype的操作不熟悉&#xff0c;不知道如何在mathtype中更改编号&#xff0c;以及解决章节编号错乱问题。本文将围绕mathtype怎么更改编号&#xff0c;mathtype章节…

Ansible 安装部署及常用命令和17个模块详解

目录 Ansible 1 ansible 环境安装部署 1.1 管理端安装 ansible 1.2 ansible 目录结构 1.3 配置主机清单 1.4 配置密钥对验证 2 ansible 命令行模块 2.1 command 模块 2.2 shell 模块 2.3 cron 模块 2.4 user 模块 2.5 group 模块 2.6 copy 模块 2.7 file 模块 2.…

磁盘的结构(磁道,扇区,盘面,柱面,物理地址)

目录 1.磁盘、磁道、扇区的概念1.磁盘2.磁道3.扇区 2.如何在磁盘中读/写数据3.盘面、柱面的概念4.磁盘的物理地址1.根据地址读取一个“块” 5.磁盘的分类1.活动头磁道2.固定头磁盘3.根据盘片是否可更换 1.磁盘、磁道、扇区的概念 1.磁盘 磁盘的表面由一些磁性物质组成&#xf…

VS Code打开新的文件夹,会覆盖原来的文件夹。如何保持原来的文件夹并新打开一个窗口

默认打开新文件夹时&#xff0c;会覆盖掉当前的窗口&#xff0c;导致每次只能看一个项目文件夹。想让其打开新文件夹时&#xff0c;以新窗口打开&#xff0c;不覆盖当前窗口&#xff0c;可以进行如下设置。 然后重启VS Code就可以生效了&#xff01;可以同时打开多个文件夹。效…

CSS宽度100%和宽度100vw之间有什么不同?

vw和vh分别代表视口宽度和视口高度。 使用width: 100vw代替的区别在于width: 100%&#xff0c;虽然100%将使元素适合所有可用空间&#xff0c;但视口宽度具有特定的度量&#xff0c;在这种情况下&#xff0c;可用屏幕的宽度 。 如果设置样式body { margin: 0 }&#xff0c;则1…

网络原理之IP协议

文章目录 前言IP 协议的协议头格式地址管理1. 动态分配 IP2. NAT机制&#xff08;网络地址转换&#xff09;NAT 机制是如何工作的 3. IPv6 网段划分子网掩码路由选择 前言 前面我们学习了关于 UDP 协议和 TCP 协议方面的内容&#xff0c;这些都是网络传输中传输层方面的协议&a…

WebGIS瓦片地图添加水印(矢量瓦片、栅格瓦片)

水印技术 水印能为收到版权信息产品归属提供有力的证据, 并能够监视被保护数据的传播, 真伪鉴别以及非法拷贝控制等.在现今流行的线上地图同样需要水印技术, 保护地图数据.本文将介绍如何实现瓦片地图水印添加, 包括栅格瓦片、矢量瓦片. 在探索过程中, 参考了《前端水印生成方案…

NTRU 加密方案

参考文献&#xff1a; [Rivest97] Rivest R L. All-or-nothing encryption and the package transform[C]//Fast Software Encryption: 4th International Workshop, FSE’97 Haifa, Israel, January 20–22 1997 Proceedings 4. Springer Berlin Heidelberg, 1997: 210-218.[…

云安全-云原生技术架构(Docker逃逸技术-特权与危险挂载)

0x00 云原生技术-docker docker容器和虚拟机的对比&#xff1a;前者是将运行环境打包&#xff0c;封装一个环境。后者是将整个系统打包&#xff0c;封装一个系统。在操作使用上来说各有利弊。 0x01 docker容器的三种逃逸类型 特权模式启动&#xff08;不安全的启动方式&…

Qt5.15:MinGW64位编译Oracle 19c数据库驱动及代码测试 - 安装时没有选Sources处理办法

文章目录 0 代码仓库1 环境以及条件说明2 准备一&#xff1a;下载Oracle 19c驱动&#xff0c;需要下载两个包&#xff0c;注意分x86和x642.1 32位2.2 64位2.3 新建目录并解压缩2.4 记录路径2.4.1 x86需要的路径2.4.2 x64需要的路径 3 准备二&#xff1a;下载Sources源代码的两种…

毅速丨金属3D打印能替代传统制造吗?

金属3D打印技术已经逐渐被很多行业认可和应用&#xff0c;但是目前&#xff0c;金属3D打印多数被作为传统制造技术的一种补充&#xff0c;暂时还不能完全替代传统制造。 金属3D打印使用的是金属粉末进行选择性激光烧结&#xff0c;打印时在成型缸里铺上金属粉末&#xff0c;打印…