jisuanjizhishizatan

本文原创,仅在博客园发布,若在其他平台发现均为盗取!!!

-1、写作目的

昨天我写过一个版本的字符串相关,浅谈指针系列:https://www.cnblogs.com/jisuanjizhishizatan/p/15545229.html
这篇文章由于时间仓促,写的比较水,因此今天有时间我来重置一篇,稍微写详细一点。

0、前言

字符串一直是C++的一个难点。昨天某个同学问我,关于字符串的比较问题:

char s[10];
...
if(s=="123456789")...

没有语法错误,但是运行结果不太对。
大家可以在这个地方停一停,想想是为什么。

1、字符串的比较

1.1.历史原因

最初,C语言刚刚面世的时候,还没有我们现在经常写C++时使用的string类型。当时的人们使用字符串,必须使用char的数组模拟字符串。
例如,输出hello world,是这样写的:

char s[]={\'H\',\'e\',\'l\',\'l\',\'o\',\' \',\'w\',\'o\',\'r\',\'l\',\'d\',\'\0\'};
for(int i=0;i<11;i++)putchar(s[i]);

这样的写法是很麻烦的,对此,C语言使用了""运算符,以及printf的%s参数。

char s[]={"Hello world"};
printf("%s",s);

这里注意,""运算符,返回的是一个char数组,如果在C++这样写:

string s="hello";

其本质是string类的运算符重载,重载了赋值运算符=。而并非表示""返回的是string类型。

1.2.数组

我之前应该写过(不过我忘记在哪篇里面提到过),数组名在表达式中,某些情况是可以解释做指针的。
也就是说,例如:

int a[10];
int *p;
p=a;

在这里,a表示指针,也就是数组a的首个元素的地址。
因此,执行这样的比较:

s=="123456789"

本质是对s的地址和"123456789"的地址进行比较。
我们来看一个简单的程序:

#include<bits/stdc++.h>
using namespace std;
char str[3];
int main(){
  printf("%p\n",str);
  printf("%p\n","abc");
  printf("%d\n",sizeof("abcd"));
}

输出在我的Dev c++环境内显示是:

我们可以看到,"abc"字符串常量也是保存在内存中的,保存在只读存储区中。并且,sizeof("abcd")返回5,表示"abcd"的本质是数组(而不是char的指针,否则会返回4)

1.3.比较函数

既然直接比较s和字符串常量会比较地址,那么,我们只能逐字符进行比较了。
标准库有个函数叫做strcmp,这个函数我们在下一章讨论。

2、strcmp函数

2.1.函数规范

很多字符串处理的函数都包含在<string.h>中。strcmp就是这样的,使用前需要#include<string.h>。
strcmp用于比较字符串的大小,只能比较char数组,返回值如下:

strcmp(a,b);
/*
a=b 返回0
a<b 返回负数
a>b 返回正数
*/

对于部分处理环境,a<b返回-1,a>b返回1,但是如果仅仅对1和-1判断,在部分环境还是过不去的,因为标准库没有这样规定。

2.2.自制strcmp函数

int strcmp(char *s1,char *s2){
	while(*s1==*s2 && *s1 && *s2){
		s1++;s2++;
	}
	return *s1-*s2;
}

上面的strcmp函数是我自己写的,其中使用了简单的指针运算。
在这里,s1和s2就是s1和s2指向的字符,如果字符相等,且两个字符不为0,那么就继续进行比较,也就是*s1 && *s2的含义。
s1-s2就是对字符作差,也就是对字符进行比较,如果大于返回正数,小于则返回负数。

2.3.指针运算

实际上,对指针进行加减运算(例如s1++一类的语句)叫做指针运算。在有些情况下,指针运算是晦涩难懂的,例如:

while(*p++);

相信大家都没看懂这句话是什么意思,实际这是strlen的实现代码中的一句,应该都没想到吧。
下面是我仿照的strlen实现,不太容易看懂的版本

int strlen(char *s){
  char *p=s;
  while(*p++);
  return p-s-1;
}

那么,我们为什么需要指针运算呢?首先,在很久以前的C语言中,遍历数组,需要使用到如下的语句:

for(int i=0;i<n;i++){
  //对a[i]进行操作
}

之前也一定提到过,a[i]是*(a+i)的缩略形式,那么,在循环体内,编译器需要计算多次a+i。
但是,如果使用指针运算:

for(int *p=a;*p!=a+n;p++){
  //对p进行操作
}

这样,使用p来代替数组中多次出现的a[i],a+i中的加法运算只会在结束时执行一次。
因此,在以前的C语言中,使用指针运算可以加快程序的运行速度。但是,现在已经不是这样了。以下引用自《征服C指针》:

如今,编译器在不断地被优化,对于循环内部重复出现的表达式的集中处理,是编译器优化的基本内容。对于现在一般的 C 编译器,无论你使用数组还是指针,效率上都不会出现明显的差距。基本上都是输出完全相同的机器码。
总的来说,C 的指针运算功能的出现,源自于早期的 C 自身没有优化手段。这一点并不奇怪,请大家回想一下在前面介绍过的内容,C 本来只是为了解决开发现场的人们眼前的问题而出现的一种语言。Unix 之前的 OS 几乎都是使用汇编写的,即使晦涩难懂,人们也不会大惊小怪。对于当时的环境,追求什么编译器优化实在有点勉为其难。因此,当初开发 C语言的时候,是完全有必要提供指针运算功能的。可是……

这应该就是大家认为“指针很难懂”或是“指针容易出错”的根本原因,都是复杂的指针运算所致的。
当然,凡事都有特例,比如,在“一个巨大的 char 数组中,参杂了各种类型的数据,并且我们试图读取第多少字节的数据”这样的情况下,还是使用指针运算写的程序比较容易理解。
无论如何,作为一个C++的程序员,我们应该学会阅读一些基本的指针运算。至于写指针运算,在非特殊情况下,我们还是一般不要使用指针运算,这样会降低程序的可读性。

3、strlen函数

3.1.函数规范

strlen(s)返回s的长度,不计\'\0\',只能用于char数组。

3.2.自制strlen

int strlen(char *s){
  int ret;
  for(ret=0;s[ret]!=\'\0\';ret++);
  return ret;
}

之前那个指针运算的strlen可能比较难懂,我们使用数组再写一个出来。这次,我们只对ret进行加法运算,如果s[ret]等于结束符\'\0\'表示字符串结束,那么就把他作为循环的条件。这样使用for循环来写,可能更加容易懂一些。

4、strcpy函数

4.1.函数规范

如果想要对字符串进行拷贝,例如把char数组的字符串s赋值为"abc",那么,

s="abc";

一句,如果s是数组将会报错,如果s为指针,那么会使得s指向字符串常量"abc",从而导致s是不可变的。
那么,我们需要使用strcpy函数:

strcpy(s,"abc");

4.2.自制strcpy

void strcpy(char *dest,const char *src){
	int i=0;
	while(src[i]!=\'\0\'){
		dest[i]=src[i];
		++i;
	}
}

这次我们同样没有使用指针运算。事实上,不使用指针运算的程序,在长度上并没有很长,使用指针运算也不太能够使程序简洁。

5、wchar_t

wchar_t是C95新增的一个功能,表示宽字符,可以存储中文。对应的字符串函数,需要使用wcscpy,wcslen等函数,使用方法与char类似。
这一部分只是简略提到一下,例如如下的程序:

#include<bits/stdc++.h>
int main(){
  wchar_t s[]= L"中文";
  for(int i=0;i<wcslen(s);i++)printf("%d ",s[i]);
}

输出ascii码。注意,对于DEV C++编译器,需要调整编译选项,不然报错:

今天我们讲解了几个常见的C语言字符串函数,以及指针运算。
完。

分类:

技术点:

相关文章: