【C语言】字符串与字符函数详解(上)

C语言学习

字符函数和字符串函数上
友情链接:C语言专栏


文章目录


前言:

在C语言编程中,字符和字符串的处理是基础且重要的内容。本文将详细介绍C语言标准库中常用的字符分类函数、字符转换函数以及基础字符串操作函数(strlen、strcpy、strcat等),并通过模拟实现帮助读者深入理解这些函数的底层原理。学习这些函数不仅能提升编程效率,更能为后续复杂字符串处理打下坚实基础。


一、const和assert介绍

在讨论咱们下面要说的字符函数和字符串函数 和后面博客要说的内存函数 的模拟实现时,咱们要对关键字const函数assert有个有个认识,以便于咱们完成某些库函数的模拟实现。ok,咱们先看const吧。

1.1 const

const是一个在 C 和 C++ 中使用的关键字,主要用于定义常量和只读变量。它的主要作用包括:

  1. 限制变量的修改 :使用const 可以确保变量的值在初始化后不会被改变,从而提高程序的安全性和可靠性。
  2. 指针的使用 :在指针中,const可以用于定义常量指针和指向常量的指针,确保指针指向的地址或值不被修改。
  3. 增强代码可读性 :通过使用const,程序员可以清晰地表达某些值是不可变的,从而提高代码的可读性。

修饰变量的示例:

c 复制代码
int main()
{
	const int i = 10;
	i = 20;//错误,i不可被修改
	return 0;
}

修饰指针时要注意几个点:

代码1

c 复制代码
int main()
{
	int i = 10; 
	const int* pi = &i;
	//int const * pi = &i;//两种写法一样,都表示*pi不可改
	*pi = 20;//错误,不可改
	pi = NULL;//可改,合法
	return 0;
}

代码2:

c 复制代码
int main()
{
	int i = 10; 
	int* const pi = &i;
	*pi = 20;//可改,合法
	pi = NULL;//错误,不可改
	return 0;
}

那假如咱们需要将指针pi和*pi都不想让它被修改,想必聪明的你已经想到了,那就加两者const呗。

代码3:

c 复制代码
int main()
{
	int i = 10; 
	const int* const pi = &i;
	*pi = 20;//错误,不可改
	pi = NULL;//错误,不可改
	return 0;
}

const修饰函数参数

咱们再来说一下,const修饰函数参数,比如size_t strlen ( const char * str ):
const 的作用:

仅表示在函数内部 不能通过 str 修改它指向的字符串。
两个常见误区:

误区一const char*参数会让字符串变为只读。

事实:const仅限制函数内部的访问方式,不改变原始字符串的存储位置
误区二 :函数内部的str一定指向只读区。

事实:取决于主函数传入的是字面量(只读)还是数组(可写)。

对const咱们就了解这么多,后续可以在代码中慢慢体会。

总之,const 是一个重要的关键字,能够帮助开发者编写更安全和可维护的代码。

1.2 assert断言

在C/C++中,assert函数是一个宏,用于在程序运行时进行断言检查。如果条件为假,assert会打印错误信息并终止程序运行,这有助于开发者在开发阶段快速发现并修复错误。

assert函数的使用

assert函数定义在<assert.h>(C++中也可以是)头文件中,其基本用法如下:

c 复制代码
#include <assert.h>

//......

assert(条件表达式);

如果条件表达式的结果为假,assert会显示错误信息,格式通常为:"Assertion failed: 条件表达式, file 文件名, line 行号",然后程序崩溃并终止运行。如果条件表达式为真,则assert不会有任何操作。

示例:

c 复制代码
#include <stdio.h>
#include <assert.h>
int main() 
{
    int i = 10;
    //......
    assert(i > 0);  // 如果 i <= 0,程序终止
    printf("%d\n", i);
    return 0;
}

assert的作用

assert的主要作用是作为一个调试工具,帮助开发者在代码中检测和断言预期的条件。它通常用于:

  • 检查函数参数的有效性。
  • 验证代码中的假设条件。
  • 捕捉不应该发生的错误。

在代码发布前,通常会禁用assert,以避免影响程序的性能。这可以通过定义NDEBUG宏来实现,如:

c 复制代码
#define NDEBUG
#include <cassert>

assert的缺点是,因为引入了额外的检查,增加了程序的运行时间。

⼀般我们可以在Debug中使用,在Release版本中选择禁用assert就行。在我现在用的vs2022这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。

二、字符分类函数

C语⾔中有⼀系列的函数是专门做字符分类的,也就是⼀个字符是属于什么类型的字符的。

这些函数的使用都需要包含⼀个头文件是 ctype.

函数 描述
iscntrl 任何控制字符
isspace 空白字符:空格 ,换页 \f,换行 \n,回车 \r,制表符 \t 或垂直制表符 \v
isdigit 十进制数字 0~9
isxdigit 十六进制数字,包括所有十进制数字,小写字母 a~f,大写字母 A~F
islower 小写字母 a~z
isupper 大写字母 A~Z
isalpha 字母 a~zA~Z
isalnum 字母或数字,a~zA~Z0~9
ispunct 标点符号,任何不属于数字或字母的图形字符(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符

这些函数的使用方法非常类似且简单,我们就来看⼀个函数,其他的非常类似:

c 复制代码
int islower ( int c );

说明:

islower是能够判断参数部分的 c 是否是小写字母的。

通过返回值来说明是否是小写字母,如果是小写字母就返回非0的整数,如果不是小写字母,则返回0。

咱们可以试着写⼀个代码,将字符串中的小写字母转大写,其他字符不变:

c 复制代码
#include<stdio.h>
#include<ctype.h>
int main()
{
	char str[] = "Hello World!";
	char* pc = str;
	while (*pc)
	{
		if (islower(*pc))//小写则为真
			*pc -= 32;//大小写的ASCII码值相差32
		pc++;
	}
	printf(str);
	return 0;
}

测试:

字符分类函数就讲到这,咱们要多用才会熟悉。

三、字符转化函数

C语⾔提供了2个字符转换函数:

c 复制代码
int tolower ( int c ); //将参数传进去的大写字母转小写
int toupper ( int c ); //将参数传进去的小写字母转大写

上一个代码,我们将小写转大写,是-32完成的效果,有了转换函数,就可以直接使用toupper函数:

c 复制代码
#include<stdio.h>
#include<ctype.h>
int main()
{
	char str[] = "Hello World!";
	char* pc = str;
	while (*pc)
	{
		if (islower(*pc))//小写则为真
			//*pc -= 32;
			*pc = toupper(*pc);
		pc++;
	}
	printf(str);
	return 0;
}

字符函数就了解到这,接下来咱们来看看常见的字符串函数。

四、strlen的使用和模拟实现

strlen 函数是C语言中用于求字符串长度的函数。使用这个函数时,需要包含头文件 <string.h>

既然是求字符串长度的,那么在来再回顾一下字符串吧:字符串以'\0'作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含'\0')。 所以,参数指向的字符串必须要以'\0'结束。

函数原型:

c 复制代码
size_t strlen ( const char * str );

解释:

size_t:返回类型为无符号整数(unsigned int),因为函数算的是长度;
const char * str :参数指向待计算长度的字符串的指针,const表示在此函数内部不会修改字符串内容(安全性)。也就是说,函数strlen只负责计算字符串长度,不会改变字符串的内容。

strlen的使用:

c 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
	const char* str1 = "abcdef";
	const char* str2 = "bbb";
	if (strlen(str2) - strlen(str1) > 0)
	{
		printf("str2>str1\n");
	}
	else
	{
		printf("srt1>str2\n");
	}
	return 0;
}

输出:

怎么会这样呢?strlen(str2)确实为3,strlen(str1)也是6,那3 -6怎么会大于0呢?这里咱们就要知道,此时strlen返回值3和6为size_t,即 unsigned int,所以3-6之后的结果也是unsigned int型的数据,显然,一定是大于0的。故输出没问题。

那咱们确实需要通过程序知道哪个字符串更长,该怎么办,其实,很简单,咱们把它强制类型转化为int就行。或者用int型的变量来就接受strlen的返回值,然后,再进行比较,也行。

了解了如何让使用,咱们可以自己模拟实现一下strlen函数
strlen的模拟实现:

方法一:

c 复制代码
//计数法
#include<assert.h>
size_t my_strlen(const char* pc)
{
	assert(pc);//断言,防止空指针传入
	int count = 0;//计数器
	while (*pc != '\0')
	{
		count++;
		pc++;
	}
	return count;
}

方法二:

c 复制代码
//指针相减
#include<assert.h>
size_t my_strlen(const char* pc)
{
	assert(pc);//断言,防止空指针传入
	char* ch = pc;
	while (*ch != '\0')
	{
		ch++;
	}
	return ch-pc;
}

方法三:

c 复制代码
//递归实现
#include<assert.h>
size_t my_strlen(const char* pc)
{
	assert(pc);//断言,防止空指针传入
	if (*pc == '\0')
		return 0;
	else
		return 1 + my_strlen(pc + 1);
}

法三咱们可能没有想到,就是递归的应用。

五、strcpy的使用和模拟实现

strcpy 函数是C语言中用于字符串拷贝函数,用于将一个字符串复制到另一个字符串。使用这个函数时,需要包含头文件 <string.h>

既然要实现字符串拷贝,咱们对于源字符串和目标空间必须满足一定要求,才可以完成函数功能:

  • 源字符串必须以 '\0' 结束。(不然拷贝没有停止条件,且字符串本来就是以'\0'结尾)
  • 函数会将源字符串中的 '\0' 拷贝到目标空间。(字符串以'\0'结尾)
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可修改。(不然不能拷贝)

函数原型:

c 复制代码
char* strcpy(char * dest, const char * src );

解释:

dest:目标空间地址;
src:源字符地址;

返回值为目标函数地址,方便链式访问;

strcpy的使用:

咱们先来使用一下strcpy函数:

c 复制代码
#include <stdio.h>
#include <string.h> // 必须包含头文件

int main() 
{
    char src[] = "Hello, World!";
    char dest[20]; // 确保足够大以容纳 src + '\0'

    strcpy(dest, src); // 复制 src 到 dest
    printf("%s\n", dest); // 输出: Hello, World!

    return 0;
}

strcpy函数有了了解,咱们可以试着来模拟实现一下
strcpy的模拟实现:

c 复制代码
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
    assert(dest && src);
    char* p = dest;//保留目标空间的起始地址方便返回
    while (*src!='\0')
    {
        *dest++ = *src++;
    }
    *dest++ = *src++;//保证'\0'也会拷贝过去
    return p;
}

很多人可能会这样写,没问题,可以实现strcpy的功能,但咱们也可以看出来,代码有冗余的部分,咱们可以再来改进一下:

c 复制代码
#include<assert.h>
char* my_strcpy(char* dest, const char* src)
{
    assert(dest && src);
    char* p = dest;//保留目标空间的起始地址方便返回
    while (*dest++ = *src++)
    {
        ;
    }
    return p;
}

这里就解释是一个点:*dest++ = *src++表达式的结果 是 赋值的结果。所以当*str\0赋值给*dest时,循环的判断条件为0(假),退出循环。

六、strcat的使用和模拟实现

strcat 函数是C语言中用于字符串连接的函数,其功能是将一个字符串追加到另一个字符串的末尾。使用这个函数时,需要包含头文件 <string.h>

既然是追加,那么我们就要注意一些要点:

  • 源字符串必须以 '\0' 结束。
  • 目标字符串中也得有 '\0' ,否则没办法知道追加从哪里开始。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。

函数的原型:

c 复制代码
char *strcat(char *dest, const char *src);

解释:

dest :目标空间地址;
src :源字符串的地址;

返回类型:为目标空间地址。

strcat的使用:

c 复制代码
#include <stdio.h>
#include <string.h>

int main() 
{
	char arr1[20] = "hello ";
	char arr2[] = "world";
	strcat(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

输出:

那如果字符串自己给自己追加呢?

c 复制代码
#include <stdio.h>
#include <string.h>

int main() 
{
	char arr1[20] = "hello ";
	strcat(arr1, arr1);
	printf("%s\n", arr1);
	return 0;
}

未定义行为,函数会先找到 dest 的末尾('\0'),然后从该位置开始复制 src(即自身)。由于复制过程会覆盖原字符串内容,导致无限循环或内存访问越界(具体行为取决于实现)。图示:

数组arr1(未追加前):

数组arr1(开始追加):

没有'\0',无停止条件,会无限循环下去。

会导致非法访问或者其他未定义行为。

ok,有了这些了解。那咱们就模拟实现一下strcat函数吧。
strcat函数模拟实现:

c 复制代码
#include<assert.h>
char* my_strcat(char* dest, const char* src)
{
	assert(dest && src);
	char* str = dest;
	while (*++dest);//使得dest指向'\0'
	while (*dest++ = *src++);//等同于strcpy
	return str;
}

在这个实现中,首先使用while循环找到dest中的'\0'字符,然后使用另一个while循环将src中的所有字符复制到dest中,包括'\0'字符。

七、strncpy函数的使用

**strncpy **函数是C语言标准库中的一个函数,用于将一个字符串的前 n 个字符复制到另一个字符串中。

函数原型:

c 复制代码
char *strncpy(char *dest, const char *src, size_t n);

解释:

dest:指向目标数组的指针,用于存储复制的内容。
src:指向源字符串的指针。
n:要从源字符串中复制的字符数。

strncpy使用:

c 复制代码
#include <stdio.h>
#include <string.h>

int main() 
{
	char str1[15];
	char str2[15];

	strcpy(str1, "abcdef");
	strcpy(str2, "ABCDEF");

	strncpy(str1, str2, 4);

	printf("%s\n", str1);
	return 0;
}

输出:

到这里,或许大家有些疑问,咱们来看看一些注意事项:

  1. **空字节填充:**当源字符串的长度小于 n 时,目标数组的剩余部分将用\0填充。
  2. **不自动添加终止符:**如果源字符串的长度大于或等于 n,目标字符串将不会自动添加终止符\0,需要手动添加。
  3. **目标空间足够大:**确保目标数组的大小足够大,以容纳复制的字符和终止符\0

八、strncat函数的使用

strncat 函数是C语言标准库中的一个函数,用于将一个字符串的前n个字符连接到另一个字符串的末尾。这个函数定义在 <string.h> 头文件中。

函数原型:

c 复制代码
char *strncat(char *dest, const char *src, size_t n)

strncat的使用:

c 复制代码
#include <stdio.h>
#include <string.h>

int main() 
{
	char dest[15];
	char src[15];

	strcpy(dest, "Hello ");
	strcpy(src, "world!");

	strncat(dest, src, 4);

	printf("%s\n", dest);
	return 0;
}

输出:

在这个例子中,strncat 函数将scr字符串的前4个字符追加到dest字符串的末尾。如果src的长度小于n,则会复制整个src字符串。如果 n 大于src的长度,那么只会追加src的全部字符到dest的末尾。无论哪种情况,都会在新字符串的末尾添加空字符。
注意事项:

使用strncat函数时,必须确保目标字符串有足够的空间来容纳追加的字符,以避免溢出。此外,应该注意strncat会覆盖dest字符串最后的空字符,并在追加完成后再次添加一个空字符。


总结

本文系统讲解了C语言中const关键字的用法、assert断言的作用,以及字符分类/转换函数的使用。重点剖析了strlen、strcpy和strcat等字符串函数的实现原理,并通过三种方式模拟实现了strlen函数。掌握这些基础函数的使用和实现原理,是成为合格C程序员的必经之路。下篇我们将继续探讨字符串比较、查找等更高级的字符串处理函数。

附录

上文链接

《深入理解柔性数组:特点、使用与优势分析》

下文链接

《字符串与字符函数详解(下)》

专栏

C语言专栏

相关推荐
এ᭄画画的北北25 分钟前
力扣-55.跳跃游戏
算法·leetcode
GISer_Jing4 小时前
JavaScript 中Object、Array 和 String的常用方法
开发语言·javascript·ecmascript
耳总是一颗苹果5 小时前
C语言---动态内存管理
c语言·开发语言
手眼通天王水水5 小时前
【Linux】3. Shell语言
linux·运维·服务器·开发语言
仟濹6 小时前
【数据结构】「队列」(顺序队列、链式队列、双端队列)
c语言·数据结构·c++
小蜗牛狂飙记6 小时前
在github上传python项目,然后在另外一台电脑下载下来后如何保障成功运行
开发语言·python·github
一只小蒟蒻6 小时前
DFS 迷宫问题 难度:★★★★☆
算法·深度优先·dfs·最短路·迷宫问题·找过程
martian6656 小时前
深入详解随机森林在眼科影像分析中的应用及实现细节
人工智能·算法·随机森林·机器学习·医学影像
程序员JerrySUN7 小时前
Valgrind Memcheck 全解析教程:6个程序说明基础内存错误
android·java·linux·运维·开发语言·学习