Skip to content

字符串和内存函数

字符串函数

字符串求长strlen

函数声明

c
size_t strlen ( const char* string );

strlen函数计算\0之前的字符个数。字符串必须以\0结尾。

c
//1.
char arr1[] = "abcd";
printf("%d\n", strlen(arr1)); // 传过去的是以\0结尾的字符串,满足strlen的要求
//2.
char arr2[] = { 'a','b','c','d' };
printf("%d\n", strlen(arr2));// 字符数组而非字符串,不以\0结尾
//3.
int a = 10;
printf("%d\n", strlen(a));// 10被当作地址看待,访问0x0A地址处的非法内存,结果如下图

strlen函数返回类型为size_t,无符号数接收,以无符号的形式打印。

c
size_t ret = strlen("abcdef");
printf("%u\n", ret);

无符号数可以直接比较但不要进行运算。

c
if (strlen("abc") - strlen("abcdef") < 0)
    printf("hehe\n");
else
    printf("haha\n");

无符号数的运算结果被整形提升为无符号数,结果被视为无符号数是不会小于零的。

模拟实现

c
//1.计数器
size_t my_strlen(const char* str) {
	assert(str);
	size_t count = 0;
	while (*str) {
		count++;
		str++;
	}
	return count;
}
//2.指针相减
size_t my_strlen(const char* str) {
	assert(str);
	char* begin = str;
	while (*str) {
		str++;
	}
	return str - begin;
}
//3.递归
size_t my_strlen(const char* str) {
	assert(str);
    if (*str) {
		return 1 + my_strlen(str + 1);
	}
	else {
		return 0;
	}
}

字符串拷贝strcpy

函数声明

c
char* strcpy ( char* strDestination, const char* strSource );

strcpy将源字符串的内容(包括\0)依次拷贝到目标空间。目标空间可修改且足够大,确保能够且足以存放源字符串。

若源字符串不以\0结尾,则strcpy会向后一直访问到非法内存。

c
//1.
char arr1[] = "xxx";//目标空间不够大
char arr2[] = "abcdef";
//.2
const char arr1[] = "xxxxxxxxxx";//目标空间不可修改
char arr2[] = "abcdef";

strcpy(arr1, arr2);

strcpyvs2019环境下使用会报出不安全的警告,该函数在拷贝前不会去检查是否会发生数组越界,警告也是正常的。

模拟实现

c
char* my_strcpy(char* dest, const char* src) {
	assert(dest && src);
    char* begin = dest;
	while (*dest++ = *src++) {
		;
	}
	return begin;
}

字符串追加strcat

函数声明

c
char* strcat ( char* strDestination, const char* strSource );

strcat将源字符串包括\0,追加到目标字符串的结尾并覆盖掉原本的\0。目标空间必须足够大且可修改,确保能够且足以追加字符串。

模拟实现

c
char* my_strcat (char* dest, const char* src) {
	assert(dest && src);
	char* begin = dest;
	//移到末尾
	while (*dest) {
		dest++;
	}
	//追加
	while (*dest++ = *src++) {
		;
	}
	return begin;
}

下面三种while循环的特点:

c
while (*dest++) { ; } //1.
while (*dest) { dest++; } //2.
while (*dest++ = *src++) { ; } //3.
  • 第一种:指针解引用后就++,第二种指针解引用后进入循环才能++
  • 第一种:解引用的次数和++的次数是相等的,当指针指向\0时也要++访问下一个元素。而第二种为真后++,指向\0时便停留在\0处。
  • 第三种:将赋值运算*dest=*src放入while循环内,当src指向\0时也要赋值给dest,在这之后判断整个表达式为假,退出循环。

字符串比较strcmp

函数声明

c
int strcmp ( const char* string1, const char* string2 );

strcmp遍历比较两个字符串对应位置的字符的ASCII码值是否相等。

  • 函数的返回值
    1. 字符串1小于字符串2时,返回小于0的数字
    2. 字符串1等于字符串2时,返回0
    3. 字符串1大于字符串2时,返回大于0的数字

模拟实现

c
int my_strcmp(const char* str1, const char* str2) {
	assert(str1 && str2);
	while (*str1 == *str2) {
		if (*str1 == '\0') {
			return 0;
		}
		str1++;
		str2++;
	}
	return *str1 - *str2;
}

都是\0就返回0,不是就都++,直到不等时返回二者之差。

长度不受限制的字符串函数的通病是不安全,这些函数不做溢出检查直到程序出错才被迫停止。所以C语言还内置了相对安全的长度受限制的字符串函数,以及更加通用的内存函数。

字符串拷贝strncpy

函数声明

c
char* strncpy ( char* strDest, const char* strSource, size_t count );

strcpy函数从源字符串拷贝count个字符到目标字符串,若count大于源字符串长度则以\0填充。

目标字符串可修改且足够大,若count小于源字符串个数则不追加\0,若大于源字符串个数则以\0填充到count个字符。

模拟实现

c
char* my_strncpy(char* dest, const char* src, size_t count) {
	char* begin = dest;
	assert(dest && src);
	while (count && (*dest = *src)) {
		dest++, src++;
		count--;
	}
	while (count--) {
		*dest++ ='\0';
  }
	return begin;
}
int main()
{
	char arr1[10] = "abcdef";
	char arr2[] = "xxx";

	my_strncpy(arr1, arr2, 5);
	printf("%s\n", arr1);
 	return 0;
}

下列三种是进行字符串拷贝时的代码。当count小于源字符串个数时三者都没问题,但当count大于源字符串个数时,三者的情况却是不一样的。

c
//1.
while (count && *src) {
    *dest++ = *src++;
    count--;
}
//2.
while (count && (*dest++ = *src++)) {
    count--;
}
//3.
while (count-- && (*dest++ = *src++)) {
    ;
}
  1. 当指向源字符串的指针遇到\0时,不进行赋值和++也不进行count--操作。

赋值赋了3次,count也减了3次。此时count为2,随后追加了2个\0

  1. 指针指向\0时执行赋值操作后,赋值表达式为假,结束循环。

由于赋值操作放在循环的判断部分,故赋值执行了4次,而count减了3次。

即源字符串末尾\0也被拷贝了过去,但count仍为2,所以最后还要追加2个\0。但这样就改变了6个字符,显然是错误的。

  1. 将第二种代码稍作修改,将count--的操作也放入判断部分,这样就避免了上述问题。
  1. 如果循环条件只有循环变量count大小的判断,则进步条件放在判断部分还是循环体内没有影响。
  2. 但如果还有赋值或是其他语句,那么该语句相对于将其放在循环体内部的情况多被执行一次。
  3. 由于count是无符号数当条件为假退出后,随即--就会变成很大的数字。后续再使用该循环变量的时候就会出错。

字符串追加strncat

函数声明

c
char* strncat ( char* strDest, const char* strSource, size_t count );

在目标字符串末尾追加count个源字符串的字符,结尾默认添加\0

追加到目标字符串末尾默认补'\0'count超出源字符串个数不在追加。

模拟实现

c
char* my_strncat(char* dest, const char* src, size_t count) {
	assert(dest && src);
	char* begin = dest;
	//1. 来到目标字符串末尾
	while (*dest) {
		dest++;
	}
	//2. 追加
	while (count && (*dest++ = *src++)) {
		count--;
	}
	//3. count小于字符个数补0
	if (count == 0) {
		*dest = '\0';
	}
	return begin;
}

字符串比较stnrcmp

函数声明

c
int strncmp ( const char* string1, const char* string2, size_t count );

比较两个字符串的前count个字符,并返回相关的数值。

模拟实现

c
int my_strncmp(const char* str1, const char* str2, size_t count) {
	assert(str1 && str2);
    while (count-- && (*str1 == *str2)) {
		str1++;
		str2++;
	}
	return *str1 - *str2;
}

字符串查找strstr

函数声明

c
char* strstr ( const char* string, const char* strCharSet );

查找子字符串在目标字符串中首次出现的位置,有则返回之起始位置,无则返回空指针。

模拟实现

c
char* my_strstr(const char* str, const char* set) {
	assert(str && set);
	char* s1 = str;
	char* s2 = set;
	while (*s1) {
		//归位
		str = s1;
		set = s2;
		//防止s1=s1=\0
		while ((*str && *set) && (*str == *set)) {
			str++;
			set++;
		}
		//判断
		if (*set == '\0') {
			return s1;
		}
		//进位
		s1++;
	}
	return NULL;
}

s1s2存放strset每次归位时的位置,以便匹配错误时返回重新开始。

  • 特殊情况set="\0"时,\0是任意字符串的子字符串,即无任何字符的字符串是任意字符串的子字符串。所以返回母字符串
  • 一次匹配错误时,str和set要归位初始位置,即str=++s1;set=s2;要放在判断部分的后面。(set=s2下次循环再执行就相当于在后面)

KMP算法专门针对字符串匹配和查找这种功能。

字符串分割strtok

函数声明

c
char* strtok ( char* strToken, const char* strDelimit );

strtok函数通过以\0替换分隔符的方式修改需分割的字符串,并返回该标记的地址。 如字符串"192.168.11.1"或者"yyx@ms.com"二者字符串都有相似之处,即整个字符串由分隔符.,@分割。如果需要得到分隔符所分割出的每个小字符串,则可以用strtok函数切分。

  • 参数strDelimit是所有分隔符所组成字符串,参数strToken是由一个或多个分隔符和标记组成的字符串。

类似于.,@称为分隔符,而分隔符所分割出的子字符串如192,com被称作标记。

  • strstr将标记以\0结尾且返回指向该标记的指针。故被操作字符串会发生修改,故一般使用临时拷贝的内容。
  • 首个参数传入字符串地址时,找到其首个标记。随后保存分隔符的位置,此时仅需传入NULL则可访问该字符串的下一个标记。

第一次调用传入需分割字符串的地址,函数保存首个标记后的分隔符被\0替换的位置。之后的调用就无需在传入地址。当访问到最后一个标记时,返回NULL

c
char arr[] = "www.yourfriendyo.top";
printf("%s\n", strtok(arr2, "."));
printf("%s\n", strtok(NULL, "."));
printf("%s\n", strtok(NULL, "."));
c
int main()
{
	char arr[] = "yourfriendyo@ms.com";
	char* sep = "@.";
	for (char* i = strtok(arr, sep); i != NULL; i = strtok(NULL, sep)) {
		printf("%s\n", i);
	}
	return 0;
}

字符串报错strerror

函数声明

c
char *strerror ( int errnum );

strerror返回内置错误码所对应的错误信息的字符串地址。

c
printf("%s\n", strerror(40));//Function not implemented
printf("%s\n", strerror(30));//Read - only file system
printf("%s\n", strerror(20));//Not a directory

一个错误码对应一个错误信息,手动传参并打印函数的返回值。当然没有人会这样使用报错函数,这也不是报错函数设计的初衷。

  • 当程序发生错误时,程序会自动将错误码存入内置全局变量errno中,此时我们调用strerror函数即可获得此次错误的报错信息。
  • 更为直接的报错函数perror,优点简单方便加入自定义信息,缺点必须打印错误信息。
c
int main()
{
	FILE* pFile = fopen("D:test.txt", "r");
	if (pFile == NULL) {
		//text.txt: No such file or directory
		printf("text.txt: %s\n", strerror(errno));
		perror("text.txt");
	}
	return 0;
}

内存函数

字符串操作函数适用于字符串内容,而内存操作函数适用于任意类型的数据如整形数据或是结构体,更具普适性。

内存拷贝memcpy

函数声明

c
void* memcpy ( void* dest, const void* src, size_t count );

memcpy将源内存的前count个字节的内容拷贝到目标内存中。若源空间和目标空间有重叠,则拷贝时会覆盖源字符串内容。

C标准并未要求memcpy完成发生内存重叠的内容拷贝,但编译器也可能对其进行优化。对内存重叠的内容进行拷贝时,可以使用memmove

模拟实现

以字节为单位拷贝内存中的内容,以适用所有类型的数据。

c
void* my_memcpy(void* dest, const void* src, size_t count) {
	void* begin = dest;
	while (count--) {
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return begin;
}

内存移动memmove

函数声明

c
void* memmove ( void* dest, const void* src, size_t count );

memmove将源空间的前count个字节的内容拷贝到目标空间中,并支持完成内存重叠的拷贝。

模拟实现

当发生内存重叠时,在源空间该字节未拷贝之前要保护其不被修改。

当目标空间在源空间的后边时,从后向前拷贝,当目标空间在源空间的前面时,从前向后拷贝。

实现方案

c
void* my_memmove(void* dest, const void* src, size_t count) {
	assert(dest && src);
	char* begin = dest;
	if (dest > src) {
        //后->前
		while (count--) {
			*((char*)dest + count) = *((char*)src + count);
		}
	}
	else {
        //前—>后
        while (count--) {
			*(char*)dest = *(char*)src;
			(char*)dest += 1;
			(char*)src += 1;
		}
	}
	return begin;
}
int main() {
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	//1234->3456
	my_memmove(arr + 2, arr, 4 * sizeof(int));
	//3456->1234
	my_memmove(arr, arr + 2, 4 * sizeof(int));
	for (int i = 0; i < 10; i++) {
		printf("%d ", arr[i]);
	}
	return 0;
}

C语言没有字符串类型,但凡遇到对字符串的操作都要细化到操作每个字符。

内存比较memcmp

函数声明

c
int memcmp ( const void* buf1, const void* buf2, size_t count );

比较两块内存空间的前count个对应字节内容,并返回相关的数值。

模拟实现

c
int my_memcmp(const void* buf1, const void* buf2, size_t count) {
	assert(buf1 && buf2);
	while (count-- && (*(char*)buf1 == *(char*)buf2)) {
		(char*)buf1 += 1;
		(char*)buf2 += 1;
	}
	return *(char*)buf1 - *(char*)buf2;
}
int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[] = { 1,2,3,4,6 };
	int ret = my_memcmp(arr1, arr2, 1 * sizeof(int));
	printf("%d\n", ret);
	return 0;
}

内存初始化memset

函数声明

c
void* memset ( void* dest, int c, size_t count );

将目标空间前count个字节初始化为整形数据c