在很多教程中,字符串不过是一个以0结束的字符数组,但是,在我看来,字符串虽然不是C语言基本数据类型,但它比任何数据类型都重要,因为字符串是最常用的数据。

一、字符串的概念

我们可以把字符串储存在char类型的数组中,如果char类型的数组末尾包含一个表示字符串末尾的空字符\0,则该数组中的内容就构成了一个字符串。

                                                      image.png

因为字符串需要用\0结尾,所以在定义字符串的时候,字符数组的长度要预留多一个字节用来存放\0,\0就是数字0。这是约定。

char strname[21];  // 定义一个最多存放20个英文字符或十个中文的字符串

字符串也可以存放中文和全角的标点符号,一个中文字符占两个字节(GBK编码)。char strname[21]用于存放中文的时候,只能存10个汉字。

字符串采用双引号包含起来,如:"hello"、"中华人民共和国"、"A"、"",这是约定。

二、占用内存的情况

一个字符占用一字节的内存,字符串定义时数组的大小就是字符串占用内存的大小。

  char str[21];     // 占用21字节的内存
  char str[1024];   // 占用1024字节的内存

三、字符串的初始化

  char strname[21];
  strname[0]=0;    // 把第一个元素的值置为0

  memset(strname,0,sizeof(strname));  // 把全部的元素置为0

strname[0]=0;不够规范,并且存有隐患,在实际开发中,一般采用memset的函数初始化字符串。

四、字符串与指针

在C语言中,数组名是数组元素的首地址,字符串是字符数组,所以在获取字符串的地址的时候,不需要用&取地址。

  char strname[21];
  memset(strname,0,sizeof(strname));
  strcpy(strname,"abcdefghijk");     // 把abcdefghijk赋值给strname
  printf("%s\n",strname);              // 输出abcdefghijk

五、字符串的结尾标志

字符串的结尾标志是0,如果没有结尾标志的情况我们在数组章节中已介绍过,现在我们介绍结尾标志后面的内容如何处理。

  char strname[21];
  memset(strname,0,sizeof(strname));
  strcpy(strname,"abcdefghijk");     // 把abcdefghijk赋值给strname
  strname[5]=0;     // 强制把第6个元素赋值0
  printf("%s",strname);   // 输出的结果是abcde

以上代码输出的结果是abcde,但是,在内存中的值仍是abcde0ghijk,后面的ghijk成了内存中的垃圾值。

不要让字符串的内存中有垃圾值,容易产生意外的后果,我们将在后面的内容中演示,所以字符串的初始化不建议采用把第一个元素的值置为0的方式(strname[0]=0)。

六、字符串的输出

字符串采用%s输出,可以加格式控制,常用的如下:

  printf("=%10s=\n","abcd");   // 输出10个字符宽度,右对齐,执行结果是=      abcd=
  printf("=%-10s=\n","abcd");  // 输出10个字符宽度,左对齐,执行结果是=abcd      =

如果输出的字符串的长度大于对齐格式中的数字,就按字符串的实际长度输出。

七、字符串越界

字符串是字符数组,字符串越界就是数组越界。字符串的越界是初学者经常犯的错误之一。

示例(book80.c)

/*
 * 程序名:book80.c,此程序演示字符串的越界。
 * 作者:C语言技术网(www.freecplus.net) 日期:20190525
*/
#include <stdio.h>
#include <string.h>
 
int main()
{
  char strname1[21];
  memset(strname1,0,sizeof(strname1));
  char strname2[21];
  memset(strname2,0,sizeof(strname2));
  strcpy(strname1,"真的只能存十个汉字吗,多几个行不行?");
  strcpy(strname2,"是的,只能十个,多了不行。");
  printf("=%s=\n",strname1);
  printf("=%s=\n",strname2);
 
  char strname[2][21];
  memset(strname,0,sizeof(strname));
  strcpy(strname[1],"是的,只能十个,多了不行。");
  strcpy(strname[0],"真的只能存十个汉字吗,多几个行不行?");
  printf("=%s=\n",strname[0]);
  printf("=%s=\n",strname[1]);
}

运行效果

image.png

我们来分析一下book80.c。

前8行代码定义了两个字符串变量,每个能存放20个字符或10个中文,但实际赋值都超过了10个中文,从输出结果看,没有问题。

后6行代码采用了二维数组的方式定义了字符串变量,理论上说,与分开定义的两个字符串变量没有区别,但是,从输出结果看,很有问题。

真正的原因是这样的,在C语言中,数组越界肯定是非法的,但非法操作并不一定会出问题,前8行代码的字符串是越界了,但是strname1和strname2变量的内存之后的内存空间是未分配的,所以对strname1和strname2赋值过长也没关系。后6行代码就不一样了,二维数组的两个变量之间的内存是连续的,第一个元素之后没有多余的空间,所以第一个元素的值就出问题了。

总的来说,在C语言中,非法操作内存不一定会报错,要看运气。

在现实生活中,一个农民把庄稼种到了自家的地盘之外,如果您的地盘之外的地没有主人,是不会有问题的,但如果有主人,这事就肯定会引起纠纷,系统对这种纠纷的裁决是内存越界的程序非法,强制终止它(段错误)。

八、字符串常用的库函数

1、获取字符串的长度(strlen)

size_t  strlen( const char*  str);

功能:计算字符串的有效长度,不包含0。

返回值:返回字符串的字符数 。

strlen 函数计算的是字符串的实际长度,遇到第一个0结束。

函数返回值一定是size_t,是无符号的整数,即typedef unsigned int size_t。

如果您只定义字符串没有初始化,求它的长度是没意义的,它会从首地址一直找下去,遇到0停止。

  char name[50];
  memset(name,0,sizeof(name));
  strcpy(name, "wucongzhou");
  printf("name 的长度是%d\n",strlen(name));     // 输出结果:name 的长度是10
  memset(name,0,sizeof(name));
  strcpy(name, "西施");
  printf("name 的长度是%d\n",strlen(name));     // 输出结果:name 的长度是4

还有一个注意事项,sizeof返回的是变量所占的内存数,不是实际内容的长度。

  char buf[10] = "abc";                           // 定义的时候初始化。
  printf("strlen(buf)=%d\n",strlen(buf));    // 输出结果strlen(buf)=3
  printf("sizeof(buf)=%d\n",sizeof(buf));    // 输出结果sizeof(buf)=10

2、字符串复制或赋值(strcpy)

char *strcpy(char* dest, const char* src);

功 能: 将参数src字符串拷贝至参数dest所指的地址。

返回值: 返回参数dest的字符串起始地址。

复制完字符串后,在dest后追加0。

如果参数dest所指的内存空间不够大,可能会造成缓冲溢出的错误情况。

3、字符串复制或赋值(strncpy)

char * strncpy(char* dest,const char* src, const size_t n);

功能:把src前n字符的内容复制到dest中

返回值:dest字符串起始地址。

如果src字符串长度小于n,则拷贝完字符串后,在dest后追加0,直到n个。

如果src的长度大于等于n,就截取src的前n个字符,不会在dest后追加0。

dest必须有足够的空间放置n个字符,否则可能会造成缓冲溢出的错误情况。

4、字符串拼接(strcat)

char *strcat(char* dest,const char* src);

功能:将src字符串拼接到dest所指的字符串尾部。

返回值:返回dest字符串起始地址。

dest最后原有的结尾字符0会被覆盖掉,并在连接后的字符串的尾部再增加一个0。

dest要有足够的空间来容纳要拼接的字符串,否则可能会造成缓冲溢出的错误情况。

5、字符串拼接(strncat)

char *strncat (char* dest,const char* src, const size_t n);

功能:将src字符串的前n个字符拼接到dest所指的字符串尾部。

返回值:返回dest字符串的起始地址。

如果n大于等于字符串src的长度,那么将src全部追加到dest的尾部,如果n小于字符串src的长度,只追加src的前n个字符。

strncat会将dest字符串最后的0覆盖掉,字符追加完成后,再追加0。

dest要有足够的空间来容纳要拼接的字符串,否则可能会造成缓冲溢出的错误情况。

6、字符串比较(strcmp、strncmp)

int strcmp(const char *str1, const char *str2 );

功能:比较str1和str2的大小。

返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;

int strncmp(const char *str1,const char *str2 ,const size_t n);

功能:比较str1和str2前n个字符的大小。

返回值:相等返回0,str1大于str2返回1,str1小于str2返回-1;

两个字符串比较的方法是比较字符的ASCII码的大小,从两个字符串的第一个字符开始,如果分不出大小,就比较第二个字符,如果全部的字符都分不出大小,就返回0,表示两个字符串相等。

在实际开发中,程序员一般只关心字符串是否相等,不关心哪个字符串更大或更小。

7、字符查找(strchr、strrchr)

char *strchr(const char *s,const int c);

返回一个指向在字符串s中第一个出现c的位置,如果找不到,返回0。

char *strrchr(const char *s,const int c);

返回一个指向在字符串s中最后一个出现c的位置,如果找不到,返回0。

8、字符串查找(strstr)

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

功能:检索子串在字符串中首次出现的位置。

返回值:返回字符串str中第一次出现子串substr的地址;如果没有检索到子串,则返回0。

九、应用经验

1、留有余地

字符串的strcpy和strcat函数要求dest参数有足够的空间,否则会造成内存的泄漏,所以在实际开发中,定义字符串的时候,可以大一些,例如姓名,中国人的姓名以两三个汉字为主,最多五个,少数民族可能十几个,外国人的很长,喜欢在自己的名字前加上爷爷的名字和外公的名字,那么我们在定义变量的时候,可以char name[301];存放他祖宗十八代的名字也没有问题。

内存不值钱,程序的稳定性高于一切。

2、变量初始化

字符串在每次使用前都要初化,减少入坑的可能,是每次,不是第一次。这是职业程序员的良好习惯。

3、位置(地址)偏移的用法

字符串的地址偏移其本质是指针的运算,常用于灵活的处理字符串。

  char strname[21];
  memset(strname,0,sizeof(strname));
  strcpy(strname,"abcdefghijk");     // 把abcdefghijk赋值给strname
 
  char strname1[21];
  memset(strname1,0,sizeof(strname1));
  strcpy(strname1,strname+1);       // 把bcdefghijk的值赋给strname1
  strncpy(strname1,strname+2,3);    // 把cde的值赋给strname1

当然,对strname1也可以使用偏移量。

4、不要在子函数中对字符指针用sizeof

如果把一个字符串(如char strname[21])的地址传给子函数,子函数用一个字符指针(如char *pstr)来存放传入的字符串的地址,如果在子函数中用sizeof(pstr),得到的不是字符串占用内存的字节数,而是字符指针变量占用内存的字节数(8字节)。

所以,不能在子函数中对传入的字符串进行初始化,除非字符串的长度也作为参数传入到了子函数中。

十、课后作业

本章节的课后作业非常要,一定要认真完成,字符串操作是C程序员的主要工作之一,这些都是职业程序员在日常开发中用到的技巧。还有,这些作业题可以培养写程序的感觉。

1)编写示例程序,实现字符串操作常用的库函数的功能,函数的声明如下:

size_t strlen1( const char*  str);          // 实现strlen函数的功能。
char *strcpy1(char* dest, const char* src);  // 实现strcpy函数的功能,下同。
char *strncpy1(char* dest, const char* src,size_t n);
char *strcat1(char* dest, const char* src);
char *strncat1 (char* dest, const char* src,size_t n);
char *strchr1(const char *s, const int c);
char *strrchr1(const char *s, const int c);

以下三个函数难度较大,如果无法完成,不要过于纠结,以后功力提升了再做。

int strcmp1( const char *str1, const char *str2 );  // 实现strcmp函数的功能,下同。
int strncmp1(const char *str1, const char *str2, const size_t n);
char *strstr1(const char* str, const char* substr);

以上作业的源代码

/*
 * 程序名:task.c,此程序是C语言字符串的课后作业。
 * 作者:C语言技术网(www.freecplus.net) 日期:20200505
*/
#include <stdio.h>
#include <string.h>

// 实现strlen函数的功能。
size_t strlen1(const char*  str);          

// 实现strcpy函数的功能
char *strcpy1(char* dest, const char* src);  

// 实现strncpy函数的功能
char *strncpy1(char* dest, const char* src,size_t n);

// 实现strcat函数的功能
char *strcat1(char* dest, const char* src);

// 实现strncat函数的功能
char *strncat1(char* dest, const char* src,size_t n);

// 实现strchr函数的功能
char *strchr1(const char *s, const int c);

// 实现strrchr函数的功能
char *strrchr1(const char *s, const int c);

// 实现strcmp函数的功能
int strcmp1( const char *str1, const char *str2 );

// 实现strncmp函数的功能
int strncmp1(const char *str1, const char *str2, const size_t n);

// 实现strstr函数的功能
char *strstr1(const char* str, const char* substr);   

int main()
{
  char strname[5];
  memset(strname,0,sizeof(strname));

  // printf("%d\n",strlen1(""));
  // printf("%d\n",strlen1("www.freecplus.net"));

  // strcpy1(strname,"");
  // printf("=%s=\n",strname);

  // strcpy1(strname,"www.freecplus.net");
  // printf("=%s=\n",strname);

  // strncpy1(strname,"12345",7);
  // printf("=%s=\n",strname);

  // strncpy1(strname,"1234567890",7);
  // printf("=%s=\n",strname);

  // strcat1(strname,"");
  // printf("=%s=\n",strname);

  // strcat1(strname,"www");
  // printf("=%s=\n",strname);

  // strcat1(strname,".freecplus.net");
  // printf("=%s=\n",strname);

  // strncat1(strname,"",2);
  // printf("=%s=\n",strname);

  // strncat1(strname,"www",5);
  // printf("=%s=\n",strname);

  // strncat1(strname,".freecplus.net",3);
  // printf("=%s=\n",strname);

  // printf("%p,%s\n",strchr1("www.freecplus.net",'a'),strchr1("www.freecplus.net",'a'));
  // printf("%p,%s\n",strchr1("www.freecplus.net",'f'),strchr1("www.freecplus.net",'f'));

  // printf("%p,%s\n",strrchr1("www.freecplus.net",'a'),strrchr1("www.freecplus.net",'a'));
  // printf("%p,%s\n",strrchr1("www.freecplus.net",'f'),strrchr1("www.freecplus.net",'f'));
  // printf("%p,%s\n",strrchr1("www.freecplus.net",'e'),strrchr1("www.freecplus.net",'e'));

  // printf("%d\n",strncmp1("freecplus","freecplus",9));
  // printf("%d\n",strncmp1("freecplus","freecplusB",9));
  // printf("%d\n",strncmp1("freecplusA","freecplusB",10));

  printf("=%p=,=%s=\n",strstr1("www.freecplus.net","aa"),strstr1("www.freecplus.net","aa"));
  printf("=%p=,=%s=\n",strstr1("www.freecplus.net","ee"),strstr1("www.freecplus.net","ee"));
  printf("=%p=,=%s=\n",strstr1("www.freecplus.net","ec"),strstr1("www.freecplus.net","ec"));
  

  return 0;
}

// 实现strlen函数的功能。
size_t strlen1(const char*  str)
{
  size_t ii=0;
  
  while (str[ii]!='\0') ii++;
  
  return ii;
}

// 实现strcpy函数的功能
char *strcpy1(char* dest, const char* src)
{
  size_t ii=0;
  
  while (src[ii]!=0) { dest[ii]=src[ii]; ii++; }

  dest[ii]=0;

  return dest;
}

// 实现strncpy函数的功能
char *strncpy1(char* dest, const char* src,size_t n)
{
  /*
  int ilen=strlen1(src);

  if (ilen>n) ilen=n;
  */

  int ilen=(strlen1(src)>n)?n:strlen1(src);

  size_t ii=0;
  
  for (ii=0;ii<ilen;ii++)
  { 
    dest[ii]=src[ii]; 
  }

  dest[ii]=0;

  return dest;
}

// 实现strcat函数的功能
char *strcat1(char* dest, const char* src)
{
  int idestlen=strlen1(dest);

  int ii=0;

  for (ii=0;ii<strlen1(src);ii++)
  {
    dest[idestlen+ii]=src[ii];
  }

  dest[idestlen+ii]=0;
}

// 实现strncat函数的功能
char *strncat1(char* dest, const char* src,size_t n)
{
  int ilen=strlen1(src);

  if (ilen>n) ilen=n;
  
  int idestlen=strlen1(dest);

  int ii=0;

  for (ii=0;ii<ilen;ii++)
  {
    dest[idestlen+ii]=src[ii];
  }

  dest[idestlen+ii]=0;
}

// 实现strchr函数的功能
char *strchr1(const char *s, const int c)
{
  int ii=0;

  int ilen=strlen1(s);

  for (ii=0;ii<ilen;ii++)
  {
    if (s[ii]==c) return (char *)s+ii; 
  }

  return 0;
}

// 实现strrchr1函数的功能
char *strrchr1(const char *s, const int c)
{
  int ii=0;

  for (ii=strlen1(s)-1;ii>=0;ii--)
  {
    if (s[ii]==c) return (char *)s+ii; 
  }

  return 0;

}

// 实现strstr函数的功能
char *strstr1(const char* str, const char* substr)
{
  char *pos=(char *)str; // 待搜索字符串的位置。
  char *pos1=0;

  while (1)
  {
    if (pos[0]==0) break;  // 如果待搜索的字符串已结束,break。

    pos1=strchr1(pos,substr[0]);  // 在pos中查找子字符串的首字符。

    if (pos1==0) return 0;  // 如果没有找到,直接返回0。

    if (strncmp1(pos1,substr,strlen1(substr))==0) return pos1; // 如果找到了,返回找到的地址。

    pos++;  // 待搜索的位置后移一个字节。
  }

  return 0;
}

// 实现strcmp函数的功能
int strcmp1(const char *str1, const char *str2 )
{
  int ii=0;
  int ilen=0;  // str1和str2中较短字符串的长度。

  if (strlen1(str1)>strlen1(str2)) ilen=strlen1(str2);
  else ilen=strlen1(str1);

  for (ii=0;ii<ilen;ii++)
  {
    if (str1[ii]==str2[ii]) continue;
    else break;
  }

  // 两个字符串相同。
  if ((ii==ilen) && (strlen1(str1)==strlen1(str2)) )return 0;

  // 两个字符串不同。
  if (str1[ii]>str2[ii]) return 1;

  return -1;
}

// 实现strncmp函数的功能
int strncmp1(const char *str1, const char *str2, const size_t n)
{
  int ii=0;

  for (ii=0;ii<n;ii++)
  {
    if (str1[ii]==str2[ii]) continue;
    else break;
  }

  // 两个字符串相同。
  if (ii==n) return 0;

  // 两个字符串不同。
  if (str1[ii]>str2[ii]) return 1;

  return -1;
}

2)丰富您的函数库,增加STRCPY、STRNCPY、STRCAT、STRNCAT四个安全的函数,弥补库函数的缺陷,解决dest的初始化和内存越界的问题,函数的声明如下:

char *STRCPY(char* dest,const size_t destsize,const char* src);
char *STRNCPY(char* dest,const size_t destsize,const char* src,size_t n);
char *STRCAT(char* dest,const size_t destsize,const char* src);
char *STRNCAT(char* dest,const size_t destsize,const char* src,size_t n);

注意,上述函数的第二个参数destsize是第一个参数dest占用内存的字节数。

3)丰富您的函数库,增加以下函数,这些是freecplus框架中的函数。

// 删除字符串左边指定的字符
void DeleteLChar(char *str,const char in_char);
// 删除字符串右边指定的字符
void DeleteRChar(char *str,const char in_char);
// 删除字符串两边指定的字符
void DeleteLRChar(char *str,const char in_char);
// 删除字符串中间的字符串
void DeleteMStr(char *str,const char *in_str);
// 在字符串的左边补字符到指定长度
void LPad(char *str,const char in_char,unsigned int in_len);
// 在字符串的右边补字符到指定长度
void RPad(char *str,const char in_char,unsigned int in_len);
// 把小写转换成大写,忽略不是字母的字符
void ToUpper(char *str);
// 把大写转换成小写,忽略不是字母的字符
void ToLower(char *str);
// 判断内容是否全部是数字,0-是,-1-不是。
int  IsDigit(const char *str);
// 判断内容是否全部是大写字母,0-是,-1-不是。
int  IsUpper(const char *str);
// 判断内容是否全部是小写字母,0-是,-1-不是。
int  IsLower(const char *str);
// 判断内容是否全部是ASCII字符,0-是,-1-不是。
int  IsASCII(const char *str);

十一、版权声明

C语言技术网原创文章,转载请说明文章的来源、作者和原文的链接。

来源:C语言技术网(www.freecplus.net

作者:码农有道

如果文章有错别字,或者内容有错误,或其他的建议和意见,请您联系我们指正,非常感谢!!!

 


C语言技术网(www.freecplus.net),粤ICP备19156379号

友情链接: 猿说编程    程序分享   

点击关闭