字符串

 

字符串
有人说过计算机世界里只有一种东西:数字(也就是0和1),根据这些数字描述对象的不同又可以分为代码(描述一组动作)和数据(描述一组内容),动作影响内容、内容决定行为就是程序。数据在现实世界中表示为声音、图像、数字、文字等等。
字符串无可厚非的解释为:一串字符。既然是一串自然就会有头有尾,因此对于每一个字符串来说都存在结尾这两个属性,少了一个就不能称之为字符串加之于字符串的操作也将失效。
1. 字符串的本质
前面提到字符串是一个拥有固定个数字符的组合,长度、内容都是它身份的象征。因此,字符串是常量的一种,不能被改变,任何属性的修改都会得到另一个字符串。这个问题在C Sharp中表现的比较明确,String str;那么str的长度就是固定不可变的,当进行字符串内容改变类操作时总会涉及到一个新String实例的创建与老实例的释放,因此它推荐使用StringBuilder来进行字符串修改操作以避免频繁的实例创建与销毁。
2. 字符串的表示
C语言没有专门的字符串类型,字符串被表示为一个特殊的字符数组(也就是一块用于存放相同类型数据的连续内存区域),这个数组中总是存在一个元素’\0’用以表示字符串的结尾。所有字符串操作函数都涉及对这个标识的识别操作。
3. 字符串的存储
这里包括两个内容:(1)字符串变量,例如 char szTmp[100],(2)字符串,就象”string”。如下两个定义就代表了不同的存储方式:
char szStr[100]="ABCDEFGHI";
char *pszStr = “abcdef”;
szStr是一个能够存放字符串的变量,因此字符串"ABCDEFGHI"就写在这个变量所在的内存空闲中。pszStr仅仅是一个指针变量,它只能存放一个地址,因此编译器为分配一块存储区域并将“abcdef”写入,pszStr存放这块区域的起始地址。
如果一个字符串的存储空间是编译器自动分配的(即程序没有显式给出),大多数编译器将为它分配一块只读区域。
4. 字符串长度计算
由于字符串仅仅是被表示为一串字符加上一个终结符,从头到尾数一下就是它的长度。对于char szTmp[] = “abc”来说,我们可以想到计算长度就是
int iLength = 0;
while(szTmp[iLength++] != ‘\0’);
可以想象,如果有一个字符串表示了10000个单词那么每次计算它的长度都要循环上几万次。如果没有找到’\0’,没关系,它会很不幸的执著下去,直到遇见一个’\0’或者某些错误发生。
标准库函数strlen使用了优化的算法(参见汇编代码),它每次判断4个字符,如果在这4个字符中出现了’\0’就再精确的判断’\0’出现在哪一个。这样计算长度为N的字符串时间复杂度就降为O(N/4)。但是不管怎么说计算长度的操作在字符串使用中应该是能免则免得。
前段时间记得在那里看到过关于strcat效率低的一个问题,其实strcat受到了冤枉。想一想,只给出两个字符串的起始地址来要求进行合并,就必须要把第一个字符串从头到尾找一遍才能确定结束位置在那里。当字符串比较短的情况还容易,但如果出现这种情况:有1000个字符串,每个长度为1024,把这1000个strcat到一个里面去,第一次只需要loop 256(找结尾)+ 256 (copy)次,第二次就要2*256+256,接着3*256+256, ….. 999*256+256。恐怕没有那个程序能轻松应付得了。在这类问题的解决方法就比较简单了,就是时刻记住字符串的当前长度。我们可以定义字符串对象结构为:
struct String
{
unsigned int uiLength;
char *pszBody;
};
然后给出一个新的strcat(struct String *pDest, struct String *pSrc)。或者在不改变当前结构的情况下定义一个新的操作STRCAT(char *pDest, char *pSrc, unsigned int uiDestLen)。总之,字符串的黑箱操作带来了简单,简单就要忍受简单的痛苦。
5.
{
char * s=”abcdef”; /* 它被编译器安排到只读数据区 */
*s=’A’;
}
能够顺利地通过编译,但是执行立即发生 Segmentation fault 。s是一个变量,用于存放字符串的地址。以上例子严格的说应该写成:const char * s=”abcdef”; 由于历史原因(那时候还没有const),允许不用const,所以只能靠自己保证。
然而:
int main(void)
{
char acStr[]="ABCDEFGHI";
acStr[0] = 'a';
return 0 ;
}
却不会发生错误,因为虽然字符串仍然是常量(这一点永远不会改变不管是c++, java还是c sharp),但是我们仅使用它来初始化数组,编译器在栈里分配空间给acStr,然后把字符串的所有字符复制到该内存空间。修改的不是原先的字符串,而是字符串再栈中的一份copy而已。
看一下它的汇编代码:
汇编代码为:
.section .rodata # 只读数据段
.LC0: # 字符串的起始地址标志
.string "ABCDEFGHI"
.text
.globl main
.type main,@function
main:
……
#前边通过栈顶指针移动,为局部变量腾出空间
#接下来将字符串copy到栈中
movl .LC0, %eax #move ABCD into eax
movl %eax, -24(%ebp) #store eax(that's ABCD) into lower of stack, that’s the start address of the array
movl .LC0+4, %eax #move EFGH into eax
movl %eax, -20(%ebp) #same process as ABCD
movw .LC0+8, %ax #move I and \0 into ax
movw %ax, -16(%ebp) #store I\0 into top of the stack
movb $97, -24(%ebp) #that's acArray[0] = 'a';
……
并不是所有的字符串数组都要通过放在只读数据区里的字符串来初始化,当定义全局字符串的时候:
char acArray[]="ABCDEFGHI";
int main(void)
{
acArray[0] = 'a';
return 0;
}
由于全局变量本身被分配在数据段中,因此编译器只需要为acArray安排这么一段空间并且指定这段空间需要以该字符串初始化即可。这时候修改任何元素自然也没有问题。

在java或c sharp中经常会有这样的语句:
String str = “string”; 或 String str = new String(”string”);
str是一个“对象变量”而不是对象,它存放着一个对象的引用(也就是地址)。前边一种方式仅仅定义一个对象变量;后边一种方式定义了一个对象(堆中分配了相应空间)把字符串复制到该空间,同时定义了一个对象变量存储对象的引用。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值