第二章:数据类型
第七节 字符和字符串以及指针
字符
字符在前面介绍printf函数的时候,其实已经介绍过了,在这里进一步详细说明。首先,字符的类型是char,这个类型还是8位有符号整数的数据类型。我们说过,计算机语言都是形式语言,就是你写的东西像啥,就是啥,专业点说,就是格式决定了含义。现在看char这个数据类型,此时你会发现,8位有符号整数和字符的格式竟然一样,哦,那么在计算机中这两个东西其实就是一个东西。
问题来了,很明显,根据前面的解释,字符其实是一个图片,这个图片的编号就是char类型数据的值。那么char类型啥时候是图片,啥时候是值呢?其实这个在C语言中是分的清楚的,就像我们的多义字一样,根据具体的语言环境,就知道这个字到底取那个意思了。
例如下面两句话:
char a='A';
printf("a = %c \n",a);
printf("a = %hhd\n",a);
这里,编译器可以通过占位符来明确知道,第一个printf中,a是一个字符(要显示图片),第二个printf中,a是一个数字,要显示a对应的8位有符号整数。如果变量a参加了四则算数运算,那更不用说了,图片不可能参加四则运算,只有数字可以。
编译器就是靠着这种简单的规则,来知道char到底是字符还是数字。
下面我们字符图片和编码的对应关系,这个对应关系我们把他叫做(ASCII表)。这个对照关系表在网上很多,随便输入ASCII就可以查到,这里我不贴处这个表了,但是给一个网址,我们以这个网站上的表来进行讲解。(所有表的对照关系都一样,你可以找其他表也没有问题。)
https://www.ascii-code.com
进入上面的网站以后,可以看见,ASCII字符分了三部分。控制字符,可打印字符以及扩展字符。0-31前32个编码是控制字符。32-127共96个可以打印的字符。128-255共128个扩展字符。根据网站上的表,我们还可以看出,当char作为字符类型,被看作是字符图片的编码的时候,其实是按照8位无符号整数来看的。
先来看看控制字符。第一个字符Null字符,在C语言中被用作字符串结尾标识,这个前面说过。重点我们知道一下这两个控制字符\n和\t就可以了,其他的控制字符我们一般用不到。
下面我们用程序来说明这三类字符。
/*****冒牌程序员-毛哥 char-null.c */
#include<stdio.h>
int main()
{
char a = '\n';
//或者可以写成 char a = '\012';
printf("回车换行控制字符的编码 char a = %#hhx\n",a);
printf("测试另外一种写法%c",'\012');
/*
这种写法是使用字符的编码来规定到底这个字符指的是ASCII字符中的那个字符。大家记住这种格式
'\XXX'
其中XXX是一个三位的8进制数,可以在网站的表上看到字符对应的8进制编码,这个XXX就代表这个
8进制编码。在控制字符的表中,可以看出换行符也就是line feed控制字符对应的8进制整数是
012,因此'\012'也代表'\n',是一个东西的不同写法。
*/
printf("现在来测试\t这个控制字符的作用\t他的作用\t和TAB键\t一样\n");
printf("\\t这个控制字符的值是%#hhx\n",'\t'); //显示位0x9,对应的解释是:Horizontal Tab
/*
注意上面这句话中的\\t这种写法,理解转义符的作用。
*/
/*******普通字符就更简单了********/
a='B';
printf("变量a代表字符 : %c\n",a); //按照字符来显示
printf("a = %#hhx\n",a); //其值是:0x42
printf("a = %hhd\n",a); //其值是:66
/*
按照ASCII字母表,大写字母和小写字母的编码差32,所以可以很方便的执行大小写转换,
例如我们要将字符变量a代表的字母转换成小写,只需要做如下处理就可以了。
*/
a+=32;
printf("转换以后 变量a代表字符 : %c\n",a);
/******扩展字符一般是可以显示的一些比较古怪的字符,通常我们看见的话会认为是乱码***/
a = '\221';
/*
扩展字符一般我们很难在键盘上打出来,当然有办法打,我以前知道,现在忘了,很无聊的东西
*/
printf("a代表的扩展字符:%c\n",a);
return 0;
}
字符串、数组和指针:
所谓字符串就是一串字符。按照直观感觉,字符串在内存中的记录方式应该是一串字符编码,每个字节一个字符编码,同时最后一个字符编码是0,代表字符串结尾。但实际情况是这样吗,确实是这样,符合直观感觉。那么我们用什么样的一个编码代表字符串呢?首先字符串中任何一个字符编码都不具备这个功能,现在我们想到了字符串所在内存位置,也就是地址,我们用字符串在内存中的地址,也就是字符串第一个字符的内存地址,来代表一个字符串。好了,这就很好的解释了为什么字符串类型是一个字符指针了。
字符串这种按照顺序保存同一种数据类型的方式,对于其他数据类型,例如整数,浮点数,也有类似的用法,例如我们把一系列int类型的有符号整数顺序放在一起,就形成了一个整数串,我们把这种整数串叫做数组,准确的可以叫int类型数组。既然数组是一系列某种数据类型的数据(在数组中,我们可以把每个数据叫做数组元素)按照顺序放在内存中,那么对于数组元素的个数就要有一个说法。是不是按照字符串的方式,最后一个元素是0就表示数组结束了呢?肯定不是,因为这样数组中就不能包含0这个元素了。那问题来了,数组元素个数应该放到哪里呢?C语言中没有说明,也没有规定,程序员爱放哪里放哪里,放到自己脑子里都可以(在后面的编程中会体会到这一点)。
现在的问题是,我们如何定义一个数组,如下:
数据类型 数组名[n];
n表示数组元素的个数;
数据类型 数组名[]={元素1,元素2,元素3,。。。};
上面这个数组元素个数,编译器会根据大括号中所写的数组元素的个数,自动给定。
如何使用数组呢?其实使用数组就是使用数组元素,如何表示一个数组元素呢?
数组名[i] 表示数组中第i个元素。数组中第一个元素的编号是0,后续依次加一。数组元素的性质和变量的性质一样,可以当变量使用。
其实对于编译器来说,数组元素个数有意义,对于真正运行起来的程序,数组元素个数已经基本没有这个概念了。(大家记住这句话,很重要)。
现在我们来解释数组元素个数对于编译器和程序员分别有啥用。对于编译器来说,知道数组元素的个数,才能够为数组分配内存,否则编译器不知道为数组分配多少内存,对吧。这就是数组元素对编译器的作用,除此之外,编译器不再关心数组元素的个数。
对于程序员来讲,素组元素的个数,意味着我们使用数组元素的时候,别用多了,也就是我们不能够把不是数组元素的数据当作数组元素来处理。例如一个数组有5个元素,我们在处理完这5个元素后,然后最后一个元素后面的二进制序列我们依然按照数组元素来处理,是不是就不合适了,可能会损害其他数据。当然,要处理一个数组所有元素的话,也必须知道数组元素的个数。通常多处理数组元素的情况时有发生,这个有个专业术语叫做越界。所以程序员时刻要注意这些个问题。这就是数组元素对程序员的意义。
至于程序运行起来以后,数组的概念就消失了,在运行程序中,数组元素对应的二进制串和其他二进制串没有区别。数组是为了程序员编写程序产生出来的概念,运行程序不需要这个。
现在再说指针。每种基本数据类型,都有对应的指针,并且指针还可以有多层。
例如:
int *ip;
unsigned int *uip;
float *fp;
char *cp;
下图说明了多级或者多层指针的概念。
总结,一个指针的定义可以分成两部分如下:
(前半部分)*
也就是,每个指针类型至少有一个*,我们把最右边的*分离出来以后,是不是剩下的还是一个数据类型(这个类型有可能是指针,有可能不是),那么这个数据类型就是指针变量指向的内存中,保存的二进制串的数据类型,例如,int *p中,p指向的内存中保存的二进制串对应的数据类型就是int,而float***** p1中,p1指向的内存中,保存的二进制串对应的数据类型就是float****类型,还是一个指针,这样一步一步退,或者说一层一层剥,最终能够找到一个float类型的数据的内存地址。怎么剥呢?例如p1这个多层指针,*p1的数据类型就是float****,而*(*p1)的数据类型就是float***,以此类推,*****p1的类型就是float,就是一个浮点数。(是不是把星号从右边全部挪到左边就可以了)
有了以上的知识,我们再用程序的方式来详细解释一下字符串,指针和数组。
/******冒牌程序员-毛哥 string.c */
#include<stdio.h>
char *s="maopaihuo-yangdamao";
char a[]="haoren-yangdamao"; //这是特殊的,字符数组的赋值方式,其他类型数组不行
/*
上面你可以认为s和a都是两个字符串,也可以认为s和a是两个数组,都可以。首先这两货都是全局的。
注意,我为什么说这两货都是全局的,不说这两个变量都是全局的?因为a根本就不是变量(看main中
的代码),s是char*类型的变量。
这里再说一下,因为常量的值是不能改变的,所以给常量赋值的时候,编译器会报错,告诉你不能改。
*/
int main()
{
char* p;
p=s;
s=a;//这里可以证明,s是一个变量,因为他可以被改变
printf("字符串s=%s\n",s);
printf("字符串a=%s\n",a); //s和a都是字符串
//a=p;
/*
如果你把上面//a=p;这句话中的注释符号//去掉,进行编译的话 gcc string.c -o 1 -g
立即会出现错误,如下:
string.c:23:6: error: assignment to expression with array type
23 | a=p;
这说明什么呢?说明a不是一个变量,因为变量的值可以被修改。
a其实是一个常量,那么问题来了,这个常量的数据类型是啥?(s的类型是char*)
我在前面说过,a和s都可以看作是字符串,那么毫无疑问,a的数据类型和s的数据类型一样。
都是char*。
*/
/*恢复s的原始值*/
s=p;
printf("字符串s的值:%s\n",s);
/*
p是一个指针变量,指向s代表的字符串,或者说指向s字符串中第一个字符,看你怎么看
或者怎么用。p可以是一个字符串,可以是一个字符指针。
*/
printf("字符串p=%s\n",p); //p是字符串,
printf("*p=%c *(p+1)=%c *(p+2)=%c\n",*p,*(p+1),*(p+2));//p是字符指针
printf("p[0]=%c,p[1]=%c,p[2]=%c\n",p[0],p[1],p[2]);//p可以看作是数组名
/*
其实s和p是同类的东西,都是一个指针变量。
*/
printf("字符串a=%s\n",a); //a是字符串,
printf("*a=%c *(a+1)=%c *(a+2)=%c\n",*a,*(a+1),*(a+2));//a是字符指针
printf("a[0]=%c,a[1]=%c,a[2]=%c\n",a[0],a[1],a[2]);//a可以看作是数组名
/*a
总结,数组名和指针其实很多方面都一样,除了数组名是一个常数外,其他都一样。
所以,使用数组元素的方法就有了两种,一种是把数组名当作指针,另一种是当作数组名。
*/
/*下面看一个真正的字符数组*/
char b[]={'a','b','c','d'};//这个数组有4个元素。
char c[]="hhhhhh";
printf("字符串b = %s\n",b);
/*
上面这个printf语句可能会打印出"字符串b = abcdhhhhhh",这个说明了字符数组和字符串的
区别。字符串用'/000'来表示字符串结束,字符数组没有这个要求。但把一个字符数组看作字符串
的时候,就出现了上面的问题。c这个字符数组就可以看作字符串,因为在c字符数组定义的时候,就
用一个字符串进行赋值。这说明字符串是一个字符数组,字符数组未必是一个字符串。
*/
/*再给大家看一个隐藏问题,大家一定要记住*/
char *a1="abcdef";
char a2[]={'a','b','\000'};
printf("s = %p a1 = %p,a2 = %p &a1 = %p c = %p a = %p\n",s,a1,a2,&a1,c,a);
/*
先回忆一下程序开头定义的s和a两个字符串,这两个字符串中的一堆字符被保存在哪里?
上面两句话中,a1字符串中的字符被保存在哪里?a2字符串中的字符变量被保存在哪里?
我们所知道的是,s这个字符串中的字符一定被保存在可执行文件中,然后被操作系统
在执行的时候装入内存。而a1是一个临时变量,在程序执行的时候,在栈中分配内存。
现在可以根据printf语句的打印结果进行推断了。
很奇怪的是,a1字符串中的字符,被保存在可执行文件中(虽然a1被保存在栈中),
而a2字符串中的字符被保存在栈中。咋理解这个事情呢?首先,a是一个全局常量,
那么全局常量肯定被写入可执行程序,然后在执行的时候装入内存。
临时变量a1的定义方式是一个字符串,在定义的时候被赋值,其值是字符串
常量"abcdef"的首地址。这个字符串常量被放在可执行程序中。
一般字符串常量都被保存在可执行程序中,除非下面的情况:
在char c[]="hhhhhh";语句中,"hhhhhh"也是一个字符串常量,但是编译器并不把
这个"hhhhhh"当作字符串常量,因为等式的左边定义的是一个字符数组。且这个数组是临时数组
所以,编译器就把这个数组放在了栈里面,因为是临时的。(记住这种特殊情况,其他情况下,
被双引号包围的字符串常量都被放在可执行程序中)。
以上的结论,有时候会搞混,但应该大概记住,记不住的时候,验证一下。上面的结论一般没有
啥用处,但是有些古怪的问题出现的时候可能有派上大用场了。
*/
/*****现在我们再看一个古怪的东西*****/
a[0]='A';
printf("%s\n",a); //a数组的第一个字符变成了A
s[0]='A';
/*
有了这个语句,你可以通过gcc string.c -o 1 -g编译好程序,然后运行程序,
你会发现程序停在哪里不动了。然后程序有立即停止运行,并且出现下面一句话:
Segmentation fault (core dumped)
翻译过来就是段缺陷。就是有问题了。出啥问题了?
char *s="maopaihuo-yangdamao";
这一句话中,s指向的这个字符串是一个常量字符串,里面的字符不允许修改。
char a[]="haoren-yangdamao";
这句话定义的是一个字符数组,但是这个字符数组符合字符串的要求,结尾是一个0。
因为他是一个数组,那么数组内的元素的类型就是char,a[i]元素是一个变量,
因此a这个字符串中的字符可以被修改。
总结,这就是这两种定义字符数组的区别。
*/
return 0;
}
下面的程序,再演示一下指针的一些特性:
/*****冒牌程序员-毛哥 pointer.c **********/
#include<stdio.h>
int main()
{
char *cp;
int *ip;
double *dp;
char *s="abceef";
int a[]={1,2,3,4,5};
double b[]={1.1,2.2,3.3,4.4,5.5};
cp=s;
ip=a;
dp=b;
printf("cp = %p,cp+1 = %p\n",cp,cp+1);
printf("ip = %p,ip+1 = %p\n",ip,ip+1);
printf("dp = %p,dp+1 = %p\n",dp,dp+1);
/*
上面三个输出语句输出如下内容:(你的可能和我的输出不一样)
cp = 0x6094000e2008,cp+1 = 0x6094000e2009
ip = 0x7ffff03e0270,ip+1 = 0x7ffff03e0274
dp = 0x7ffff03e0290,dp+1 = 0x7ffff03e0298
大家发现奇怪的问题没有?cp+1 - cp是多少,ip+1 - ip是都少,
dp+1 - dp是多少?
根据上面的输出,可以看出,三个差分别是1,4,8。
这三个值分别是char,int,double三个数据类型在内存中所占的字节数。
C的指针,在增加减少的时候,其粒度是指针数据类型所占字节数。一定要记住。
C的这种设定给我们带来了很多方便,同时也带来了一些不方便。方便的时候多,
不方便的时候少。
*/
printf("dp = %p, ((char*)dp)+1 = %p\n",dp,((char*)dp)+1);
/*
上面一句,解决了这个问题,可以按照自己的想法,来增加或者减少指针的值。
其方法就是对指针类型进行重新说明。因为char类型的指针,其增加和减少
的粒度就是1.
*/
printf("sizeof(cp)=%ld sizeof(ip)=%ld sizeof(dp)=%ld\n",sizeof(cp),
sizeof(ip),sizeof(dp));
/*
可以看出来,三个值都是8个字节,相当于64位。我们说,指针就是表示内存地址,
内存地址就是内存位置编号,这个编号占几个字节,和内存多少有关系,内存多,就
需要更多的位数来表示这个位置编号。现在的计算机内存都很多,所以需要64位二进制
数来表示内存地址,也就是8位。这种内存编码的系统叫做64位系统。
*/
return 0;
}
指针数据类型,可以看作是64位无符号整数,也就是unsigned long。很多时候,指针也被定义成unsigned long。在用作指针的时候,在进行重新说明。这种重新说明数据类型的方式,我们叫做强制转换。(这是C语言精髓的地方,也是被诟病的地方)