【C進階 三】字符串函數與內存函數

碼字不易,對你有幫助 點贊/轉發/關注 支持一下作者
微信搜公衆號:不會編程的程序圓
看更多幹貨,獲取第一時間更新

思維導圖


目錄


正文


  • strlen & strlen_s
  • getchar() & putchar()
  • strcmp & strncmp()
  • strcpy() & strncpy()
  • strcat & strncat()

以上這幾個函數在我的另一篇文章中我已經詳細講過了。文章鏈接


這篇文章中,我們先主要來簡單實現一下這幾個函數,然後再討論其他函數。

序 老朋友們

myStrlen & myStrcat & myStrcpy

#include<stdio.h>
#include<assert.h>

int myStrlen(const char* str);
char* myStrcat(char* dest, const char* src);
char* myStrcpy(char* dest, const char* src);

int main(void) {

	char str1[12] = "Today ";
	char str2[6] = "sucks";
	
	//str1 和 str2 不是空指針
	if (str1 && str2) {

		printf("str1 is :%s str2 is :%s\n", str1, str2);
		printf("str1 has %d characters, str2 has %d characters\n", myStrlen(str1), myStrlen(str2));
		printf("str1 + str2 = %s\n", myStrcat(str1, str2));
		printf("Copy str2 to str1,now str1 is:%s\n", myStrcpy(str1, str2));
	
	}

	return 0;
}

int myStrlen(const char* str) {

	assert(str != NULL);

	const char* start = str;

	while (*++str);

	return str - start;
	
}

char* myStrcat(char* dest, const char* src) {
	
	assert(dest && src);

	char* ret = dest;

	//找到 dest 中 '\0' 的位置
	while (*++dest);

	while (*dest++ = *src++);

	return ret;
}

char* myStrcpy(char* dest, const char* src) {

	assert(dest && src);

	char* ret = dest;

	while (*dest++ = *src++);

	return ret;
}

MyStrcmp

#define _CRT_SECURE_NO_WARNINGS 1

#include<stdio.h>
#include<string.h>
#include<assert.h>

// strcmp 返回值:
// 如果 str1 > str2, 返回的是一個比 0 大的數(不一定是 1)
// 如果 str1 < str2, 返回的是一個比 0 小的數(不一定是 -1)
// str1 > str2 在 VS 上可能是 1,在 linux gcc 中不一定是 1 
// 爲了方便實現,我們就選擇返回 -1 和 1

int MyStrcmp(char* str1, char* str2) {
	
	assert(str1 != NULL && str2 != NULL);
	
	while (*str1 != '\0' && *str2 != '\0') {
		
		if (*str1 > *str2) {
			return 1;
		}
		else if (*str1 < *str2) {
			return -1;
		}
		str1++;
		str2++;
	}

	// 當循環退出時,str1 或 str2 達到字符串末尾 '\0'
	// 對於 strcmp 來說,如果前面的字符都相同,那麼,短的字符串就小
	// 因爲 '\0' 的 ASCII 值是 0,它是最小的(達到字符串結尾的字符串用'\0'來進行最後一次比較)

	if (*str1 > * str2) {
		return 1;
	}
	else if (*str1 < *str2) {
		return -1;
	}
	else {
		return 0;
	}

}

int main(void) {
	
	char str1[] = "haha";
	char str2[] = "haha";

	int ret = MyStrcmp(str1, str2);
	
	if (ret > 0) {
		printf("str1 > str2\n");
	}
	else if (ret < 0) {
		printf("str1 < str2\n");
	}
	else {
		printf("str1 == str2\n");
	}

	return 0;
}

始 新朋友們

1. strstr

char *strstr( const char* str, const char* substr );

定義於頭文件:<string.h>

查找 substr 所指的空終止字節字符串在 str 所指的空終止字節字符串中的首次出現。不比較空終止字符。

strsubstr 不是指向空終止字節字符串的指針,則行爲未定義。

參數:

str - 指向要檢驗的空終止字節字符串的指針
substr - 指向要查找的空終止字節字符串的指針

返回值:

指向於 str 中找到的子串首字符的指針,或若找不到該子串則爲 NULL 。若 substr 指向空字符串,則返回 str

想象實現 MyStrstr 是我們產品經理的需求,先來看看他的需求是什麼:

  1. 查找 字符串 substr 在字符串 str 中首次出現的位置(返回找到的字串的首字符的指針)

思路

  1. 我們先在 str 中找到 substr 的第一個元素
  2. 比較 str 的下一個字符與 substr 的下一個字符是否相等(可以循環實現)
  3. 如果 substr 中有一個字符與 str 中的是不一樣的,那麼 substr 應該從首個元素開始重新在 str 中繼續向下尋找,直到找到或者 str 結束
  4. 我忽略了一個條件:當 substr 重新從第一個元素開始在 str 中尋找時,str 應該重置爲 上一次 str 所在位置的下一個字符處(比如 “cacacat” 中尋找 “cacat”)
實現
#include<stdio.h>
#include<string.h>
#include<assert.h>

char* MyStrstr(const char* str, const char* substr) {
	
	const char* substr_begin = substr;
	const char* str_next = str;

	assert(str != NULL && substr != NULL);

	if (*str == '\0' || *substr == '\0') {
		return NULL;
	}

	while (*str_next != '\0') {
		
		// substr 從頭開始
		substr = substr_begin;
		str = str_next;

		if (*str == *substr) {
			while (*str == *substr && *str != '\0' && *substr != '\0') {
				str++;
				substr++;
			}
			// 循環退出三種情況
			if (*substr == '\0') {
				return str_next;
			}
			if (*str == '\0') {
				return NULL;
			}
			// 剩下的一種就是 str 的字串 和 substr 不是完全匹配的,重新找 substr 的第一個元素 
		}
		
		str_next++;
	}
}

int main(void) {

	char str1[] = "Hello World";
	char str2[] = "lo";

	char* ptr = MyStrstr(str1, str2);
	
	if (MyStrstr != NULL) {
		printf("%s\n", ptr);
	}
	else {
		printf("str2 在 str1 中不存在\n");
	}
	
	return 0;
}

2. strtok

瞭解這個函數即可。

char *strtok( char *str, const char *delim )

定義於頭文件 < string.h >

參數:

str - 指向要記號化的空終止字節字符串的指針
delim - 指向標識分隔符的空終止字節字符串的指針

返回值:

返回指向下個記號起始的指針,或若無更多記號則返回 NULL

注意:

此函數是破壞性的:它寫入 ‘\0’ 字符於字符串 str 的元素。特別是,字符串字面量不能用作 strtok 的首參數。

每次對 strtok 的調用都會修改靜態對象:它不是線程安全的。

strtok 中有個 static 修飾的變量記錄下來上次位置

int main(void) {

	char str[] = "World is so beautiful!Hi,Look!";
	char* pch;

	pch = strtok(str, ",. !");
	while (pch != NULL) {
		printf("%s\n", pch);
		pch = strtok(NULL, ",. !");
	}

	return 0;
}
// 輸出:
World
is
so
beautiful
Hi
Look

3. memcpy

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

定義於頭文件 :<string.h>

src 所指向的對象複製 count 個字符到 dest 所指向的對象。兩個對象都被轉譯成 unsigned char 的數組。

若訪問發生在 dest 數組結尾後則行爲未定義。若對象重疊(這違背 restrict 契約) (C99 起),則行爲未定義。若 destsrc 爲非法或空指針則行爲未定義。

參數:

dest - 指向要複製的對象的指針
src - 指向複製來源對象的指針
count - 複製的字節數

返回值:

返回 dest 的副本,本質爲更底層操作的臨時內存地址,在實際操作中不建議直接使用此地址,操作完成以後,真正有意義的地址是dest本身。

注意:

memcpy 可用於設置分配函數所獲得對象的有效類型

memcpy 是最快的內存到內存複製子程序。它通常比必須掃描其所複製數據的 strcpy ,或必須預防以處理重疊輸入的 memmove 更高效。

許多 C 編譯器將適合的內存複製循環變換爲 memcpy 調用。

嚴格別名使用禁止檢驗同一內存爲二個不同類型的值處,可用 memcpy 轉換值。

注:

void* 只包含地址,沒有內存空間大小這樣的信息,所以 void* 不能解引用,也不能進行運算

void* 是爲了兼容各種類型的指針,算是一種簡單的“泛型編程”。

簡單的用法:

int main(void) {

	char src[] = "Once upon a midnight dreary", dest[4];
	memcpy(dest, src, sizeof dest);
	for (size_t i = 0; i < sizeof dest; i++)
		putchar(dest[i]);
	
	int* p = malloc(3 * sizeof(int));// 分配的內存沒有有效類型
	int arr[3] = { 1, 2, 3 };
	memcpy(p, arr, 3 * sizeof(int));// 分配到內存有了有效類型 int


	return 0;
}
實現
#include<stdio.h>
#include<assert.h>

void* MyMemcpy(void* dest, const void* src, size_t count) {
	
	assert(dest != NULL && src != NULL);

	void* ret = dest;

	for (size_t i = 0; i < count; i++) {
		*(char*)dest = *(char*)src;// 逐字節複製
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}

	return dest;
}

int main(void) {

	char src[] = "Once upon a midnight dreary", dest[4];
	MyMemcpy(dest, src, sizeof dest);
	for (size_t i = 0; i < sizeof dest; i++)
		putchar(dest[i]);
	
	int* p = malloc(3 * sizeof(int));// 分配的內存沒有有效類型
	int arr[3] = { 1, 2, 3 };
	MyMemcpy(p, arr, 3 * sizeof(int));// 分配到內存有了有效類型 int
	for (int i = 0; i < 3; i++) {
		printf("%d ", *(p + i));
	}

	return 0;
}
思考

請看下例:

#include<stdint.h>
#include<inttypes.h>
#include<stdio.h>
#include<string.h>

int main(void) {
	
	double d = 1.0;
	int64_t i = 1;
	
	i = d;
	// 輸出:0.000000 5509945

	printf("%f %d\n", i, i);

	memcpy(&i, &d, sizeof d);

	printf("%f %"PRId64" ", i, i);//#define PRId64 "lld"
	//輸出:1.000000 4607182418800017408

	return 0;
}

memcpy 該表了 i 作爲整型的內存佈局,所以 i 可以直接用 %f 輸出

4. memmove

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

定義於頭文件 :< string.h >

src 所指向的對象複製 count 個字節到 dest 所指向的對象。兩個對象都被轉譯成 unsigned char 的數組。對象可以重疊:如同複製字符到臨時數組,再從該數組到 dest 一般發生複製。

若出現 dest 數組末尾後的訪問則行爲未定義。若 destsrc 爲非法或空指針則行爲未定義。

參數:

dest - 指向複製目的對象的指針
src - 指向複製來源對象的指針
count - 要複製的字節數

返回值:

返回 dest 的副本,本質爲更底層操作的臨時內存地址,在實際操作中不建議直接使用此地址,操作完成以後,真正有意義的地址是dest本身。

注意:

memmove 可用於設置由分配函數獲得的對象的有效類型

儘管說明了“如同”使用臨時緩衝區,此函數的實際實現不會帶來二次複製或額外內存的開銷。常用方法( glibc 和 bsd libc )是若目標在源之前開始,則從緩衝區開始正向複製,否則從末尾反向複製,完全無重疊時回落到更高效的 memcpy

嚴格別名時用禁止檢驗同一內存爲二個不同類型的值時,可使用 memmove 轉換值。

重疊的含義

在這裏插入圖片描述

實現
#include<stdio.h>
#include<assert.h>

void* MyMemcpy(void* dest, const void* src, size_t count) {
	
	assert(dest != NULL && src != NULL);

	void* ret = dest;

	for (size_t i = 0; i < count; i++) {
		*(char*)dest = *(char*)src;// 逐字節複製
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}

	return dest;
}

void* MyMemmove(void* dest, const void* src, size_t count) {

	assert(dest != NULL && src != NULL);

	void* ret = dest;
	char* pdest = (char*)dest;
	char* psrc = (char*)src;
	
	// 先判斷有沒有重疊

	// 如果沒有重疊,應該用更爲高效的 memcpy
	if (dest >= (char*)src + count || src >= dest) {
		MyMemcpy(dest, src, count);
	}
	else {
		// 分別指向 dest 和 src 的最後一個字符
		pdest = pdest + count - 1;
		psrc = psrc + count - 1;
		
		while (count--) {
			*pdest = *psrc;
			pdest--;
			psrc--;
		}
		return dest;
	}
}


int main(void) {

	char str[] = "123456789";
	puts(str + 1);
	MyMemcpy(str + 1, str, 3);
	puts(str + 1);
	MyMemcpy(str, "123456789", sizeof(str));
	MyMemmove(str + 1, str, 3);
	puts(str + 1);
	
	return 0;
}
//輸出:
23456789
11156789
12356789

5. memcmp

int memcmp( const void* lhs, const void* rhs, size_t count );

參數:

lhs, rhs - 指向要比較的對象的指針
count - 要檢驗的字節數

返回值:

lhs 以字典序出現前於 rhs 則爲負值。

lhsrhs 比較相等,或 count 爲零則爲零。

lhs 以字典序出現後於 rhs 則爲正值。

注意:

此函數讀取對象表示,而非對象值,而且典型地只對字節數組有意義:結構體可以含有填充字節而其值不確定,存儲於聯合體最近存儲成員後的任何字節的值是不確定的,且一個類型可以對相同值擁有二種或多種表示(對於 +0 和 -0 或 +0.0 和 –0.0 的相異編碼、類型中不確定填充位)。

簡單的應用:

#include<stdio.h>
#include<string.h>

int main(void) {

	int arr1[] = { 0, 1, 2, 3 };
	int arr2[] = { 0, 1, 2, 3 };
	int arr3[] = { -0, 1, 2, 3 };

	if(memcmp(arr1, arr2, sizeof arr1) == 0){
		printf("arr1 == arr2\n");
	}
	else {
		printf("arr1 != arr2\n");
	}

	if (memcmp(arr1, arr3, sizeof arr1) == 0) {
		printf("arr1 == arr3\n");
	}
	else {
		printf("arr1 != arr3\n");
	}
	

	return 0;
}

6. memset

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

定義於頭文件 <string.h>

複製值 ch (如同以 (unsigned char)ch 轉換到 unsigned char 後)到 dest 所指向對象的首 count 個字節。

若出現 dest 數組結尾後的訪問則行爲未定義。若 dest 爲空指針則行爲未定義。

參數:

dest - 指向要填充的對象的指針
ch - 填充字節
count - 要填充的字節數

返回值:

  1. dest 的副本,本質爲更底層操作的臨時內存地址,在實際操作中不建議直接使用此地址,操作完成以後,真正有意義的地址是dest本身。

簡單應用:

#include<stdio.h>
#include<string.h>

int main(void) {

	char str[] = "Hello World";

	memset(str, '\0', sizeof str);

	puts(str);

	return 0;
}

在 Github 上看更全的目錄:

https://github.com/hairrrrr/C-CrashCourse

以後的這個系列的代碼都會上傳上去,歡迎 star


以上就是本次的內容。

如果文章有錯誤歡迎指正和補充,感謝!

最後,如果你還有什麼問題或者想知道到的,可以在評論區告訴我呦,我可以在後面的文章加上你們的真知灼見​​。

關注我,看更多幹貨!

我是程序圓,我們下次再見。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章