C语言03- 数组、字符串
10:数组
数组每个元素的类型相同,因此各个元素在内存中存放的长度也一样,即它们占用的空间是等长的,而数组名,就是这段内存的首地址。
数组是连续存储,所以它支持随机访问.
10.1:一维数组
image
下面关于数组的一些定义,有的是错误的:
int x = 10;
int a[x]; //错误,x是变量而不是常量
const int x1 = 10;
int a1[x1];//正确,x现在是常变量
int a2[10];//正确,10是整数常量
一些常见的一维数组定义:
int a[100];
char a[100];
int *a[10];//每个元素是一个指针
a[i]:数组元素值
&a[i]:第i个元素的地址
&a[0]:首个元素的地址
a,首地址
a5+1
&a[0]+1
sizeof是用来计算类型和数据的长度。strlen是用来计算字符串中非‘\0’的字符个数的。
sizeof(a):数组的长度计算
sizeof(a[0]):数组中一个元素的 长度
n=sizeof(a)/sizeof(a[0]):数组元素个数
#define ARRAYSIZE(a) (sizeof(a)/sizeof(a[0]))
10.2:二维数组
二维数组可以理解为数学中的矩阵的一个类似概念。二维数组定义的一般形式为:
类型说明符 数组名[常量表达式1][常量表达式2]
二维数组的初始化可以为下面的形式:
int a[5][3]={ {80,75,92},{61,65,71},{59,63,70},{85,87,90},{76,77,85} };
对于二维数组的遍历,可以先列后行(即先遍历列,再遍历行),也可以先行后列(即先遍历行,再遍历列)进行。比如,对于一个m行n列的整型二维数组a[m][n]进行遍历:
//先行后列:
for(int i = 0; i < m;i++) {
for(int j = 0; j < n; j++) {
printf(“%d/n”, a[i][j]);
}
}
//输出为:a[0][0] a[0][1] a[0][2] a[0][3]…a[0][9]...
//先列后行:
for(int i = 0; i < n;i++) {
for(int j = 0; j < m; j++) {
printf(“%d/n”, a[j][i]);
}
}
//输出为:a[0][0] a[1][0] a[2][0] a[3][0] a[4][0]
由于数组是按行的顺序存放的,同一列的数据就有可能存放在不同的页中,那么先列后行的访问将引起更多的缺页中断,降低了遍历的效率。
10.3:数组重要注意事项
int a1[10];
int a2[4][5];
//a1,&a1,a2,&a2都是数组的首地址,值相同,但是类型不同。
//类型为什么不一样呢?
//a1:int * -
//&a1:int (*a1)[10] -包含十个元素的一维数组的指针类型
//a2:int (*a2)[5] -数组指针,指向含有5个元素的数组,代表一行
//&a2:int (*a2)[4][5] -数组指针,指向还有4行5列的一个数组
--------------------------------------------
int a[3] = {0,1,2};
printf("a:%p\n", a);//a是int *类型;0x7ffee02f693c
printf("&a:%p\n", &a);//&a是int (*a)[3]类型;0x7ffee02f693c
printf("&a[0]:%p\n", &a[0]);//首个元素的地址;0x7ffee02f693c
//打印输出a, &a, &a[0]值一样;如地址0x7ffee02f693c
//sizeof判断数据类型长度符的关键字;其作用就是返回一个对象或者类型所占的内存字节数
/*
指针变量的sizeof
学过数据结构的你应该知道指针是一个很重要的概念,它记录了另一个对象的地址。既然是来存放地址的,那么它当然等于计算机内部地址总线的宽度。所以在32位计算机中,一个指针变量的返回值必定是4(注意结果是以字节为单位),但是,在64位系统中指针变量的sizeof结果为8。
*/
printf("sizeof(a):%lu\n", sizeof(a));//数组的sizeof值等于数组所占用的内存字节数
printf("sizeof(&a):%lu\n", sizeof(&a));//
printf("sizeof(&a[0]):%lu\n", sizeof(&a[0]));//
// sizeof(a):12
// sizeof(&a):8 //64位系统
// sizeof(&a[0]):8 //64位系统
printf("a:%p\n", a + 1);//a是int *类型,0x7ffee2d5c940 加一是其类型加一
printf("&a:%p\n", &a + 1);//&a是int (*a)[3]类型,0x7ffee2d5c948
printf("&a[0]:%p\n", &a[0] + 1);//首个元素的地址,0x7ffee2d5c940
---------------------------
一般地:
int a[m1][m2]...[mn]
a, &a, &a[0]...[0]
a+1//a的类型int (*a)[m2]...[mn],一行
&a+1//&a的类型int (*a)[m1]...[mn]
&a[0]...[0]//代表的第一个元素的类型int *
int a[5] = {1,2,3,4,5};
int *ptr1 = (int *)(&a+1);
int *ptr2 = (int *)((int)a+1);
printf("%x, %x", ptr1[-1], *ptr2)
//打印0x5(0x00000005), 0x02000000;
/*
ptr1[-1]:ptr1类型是int *,ptr1[-1]相当于ptr1-1既ptr1减去int *类型长度4的地址。
*/
内存存储示例.png
10.3.1:数组做参数传递给函数,在函数内部退化为指针
void func(int a[], size_t len) {//a是数组名,len是数组元素个数。数组做函数参数,一般都这么传递
printf(“sizeof(a) in func=%d\n”, sizeof(a));
}
int a[10] = {0};
printf(“sizeof(a)=%d\n”, sizeof(a));
func(a, 10);
/*
执行上面的代码,你会发现输出的两个结果为:
sizeof(a) = 40
sizeof(a) in func=4
也就是说,如果数组做了函数的参数,那么在函数内部,数组就变为了指针。实际上,数组是一个常量指针。比如:
int a[10];
a的类型为:int *const a;//a is a const pointer to int;
*/
10.3.2:数组溢出与预防
会造成程序的异常现象;C语言编译器对数组溢出不做检测,
image
学了函数之后会发现程序1,无法退出,i=16之后,又会重新被置为0。
程序2,a[9]不存在,越界溢出
11:字符串
字符串是编程语言中表示文本的数据类型。
11.1:字符串定义
字符串是由零个或多个字符组成的有限序列。C语言字符串可以定义为:”c1c2c3c4..cn\0”。
C语言的字符串是以’\0’结尾的。
程序在存放字符串的时候,会自动在字符后面加上一个’\0’作为结尾。
11.1.1:转义字符
C语言中常见的转义字符如下:
image
11.2:程序中的字符串
"a":字符串,存储在静态区。实际上是2个字符,末尾包好'\0'。使用方法char *str = "a";也就是可以赋值给字符指针,这个时候str指向"a"的首地址。
'a':字符常量,值类型,不存储在静态区,无内存。使用char ch='a';赋值给ch变量,ch存放在哪里,'a'就存放在哪里。不能使用char *p='a';因为'a'对应的类型是char。
并不是所有的常量都会被编译器放在常量区的,编译器认为普通的整型、浮点型或字符型常量在使用的时候是可以通过立即数来实现的,没有必要额外存储到数据区,如此节省了存储空间和运行时的访问时间。
常量区里存放的是一些不可改变的量,比如字符串常量。在实际的ELF(Executable and Linkable Format,可执行连接格式,是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的。Linux作为类unix系统其程序仍然沿用了该格式。)的程序数据是分段存储的,对应的常量区就是“.rodata(只读数据)段“。
常量区的数据被标记为只读,也就是程序只有访问权而没有写入权,因此如果开发者需要使用某些不希望被改变的数据时可以将其放入常量区。
在C语言中常量有很多种,比如常见的:
字符常量:‘a’, ’A’, ’*’。
字符串常量:”helloworld”,”ilovechina”,”12345”。
整常量: 25,10,012,0x0a,0b00001010。
浮点常量: 3.14,123.456, 3.0E-23;
11.2.1:多字节字符串与宽字符字符串
char表示一个ASCII字符占用1个字节,char类型字符串以”\0”结尾。
wchar_t表示一个UNICODE字符占用2个或4个字节,wchar_t类型字符串以”\0\0”结尾。
多字节字符串
在多字节字符串中,每个字符的编码宽度都不等,可以是一个字节,也可以是多个字节。比如:
char *str = “Hello, world!您好,世界!”。
上面是一个多字节字符串。其中的英文字符占一个字节,而中文字符占2个字节。
宽字符字符串
而宽字符串中,每个字母占用的字节数是一样的。比如:
wchar_t *wstr = L“Hello, world!您好,世界!”
上面是一个宽字符串,每个字符,无论英文字母还是中文字符,都占2个字节。
scanf_s("%ws", s, 100)//不能有空格,有空格打断
fgetws(s, 100, stdin)
fputws(s, stdout)
用wctomb()等函数将宽字符串与多字节串进行相互转换
11.2.2:使用方式
字符串常量可以赋值给一个字符指针或者一个字符数组,比如:
char *str = “this is a string”;
char str2[]= “this is a string”;
char str3[100] = “this is a string”;
语句1将”this is a string”赋值给了字符指针str。此时,str的值为”this is a string”的第一个字符的地址。”this is a string”这个常量字符串存储在内存常量区。而str即指向了存储这个常量字符串 的首地址。
语句2会将常量区中的”this is a string”拷贝到数组里面。并且数组的长度将为”this is a string” (包含’\0’)的长度。
语句3会将常量区中的”this is a string”拷贝到数组里面。并且数组的长度将为100个字节。语句3和语句2的区别是语句2没有指明数组的长度,那么数组的长度就是字符串的长度。
sizeof(str);//为指针的长度,所以在X86上是4,在X64上是8。
sizeof(str2)=17;//str2数组的长度,但str2没有显示指出数组的长度,而是按照分配给它的字符串的长度来分配。所以,值为17。
sizeof(str3)=100;//sizeof计算的是str3数组的长度,所以结果为100。
strlen(str)=16;//strlen计算的是字符串的字符个数(不包含’\0’)。
strlen(str2)=16;//原因同上。
strlen(str3)=16;//原因同上。
//把字符串存放在动态分配的内存空间中
char *p = (char *)malloc(100);
if (p == NULL)
return;
memset(p, 0, 100);
strcpy(p, “hello world”);//以p为首地址的内存中存这个字符串。
1,while循环遍历
char *str=“hello world!”;
while(*str!=‘\0’) {
printf(“%c”, *str);
str++;
}
11.3:字符串API
strlen:计算字符串中字符个数(不含结尾字符’\0’);
strstr:查找字符串中子串的位置
strcmp/stricmp:比较2个字符串是否相等,stricmp()多了个i(ignore)比较时忽略大小写
strchr/strrchr:strchr从左边开始在字符串中查找某个字符的位置,strrchr()右边开始
strcpy/strcpy_s:strcpy将字符串复制到目标缓存,但不检测目标缓存的长度是否大于字符串的长度,无论是否超过,都照拷不误;在Windows平台推出了新的安全拷贝函数strcpy_s()。strcpy_s将检测字符串的长度是否超过了目标缓存的大小,如果超过,则拒绝拷贝并返回失败。
strcat/strcat_s:用于字符串的拼接,并不检测目标缓冲的大小是否能够容纳下拼接之后的字符串,因此也容易造成缓冲区溢出漏洞。strcat_s()避免了这个问题。
strtok/strtok_s: 将字符串s拆分为为一组字符串,delim为分隔符。
image.png
11.4:自己实现字符串API
size_t _strlen(const char *str) {
size_t count = 0;
while(*str++!= '\0') {
count++;
}
return count;
}
char *_strcpy(char *dst, char *src) {
char *s = dst;//如果不用临时指针,最后dst指向尾部
while(*s++ = *src);
return dst;
}
/*
int strcmp(char *s1,char * s2),它的功能:比较字符串s1和s2:
当s1
当s1=s2时,返回值=0
当s1>s2时,返回值>0
*/
int _strcmp(const char *s1, const char *s2) {
assert(s1!==NULL && s2!=NULL)
while(*s1 && *s2 && (*s1==*s2)) {
s1++;
s2++;
}
return *s1-*s2
}
/*
strstr()的原型为:char *strstr(char *s1, char *s2)
功能是从字符串s1中寻找s2第一次出现的位置(不比较结束符NULL)。
*/
char *_strstr(char *s1, char *s2) {
if (s1== NULL || s2==NULL)
return NULL;
if (!*s2)
return ((char *)s1);
char *s3, *s4;
char *p = (char *)s1;
while(*p) {
s3 = p;
s4 = s2;
while(*s3 && *s4 && !(*s3-*s4))//*s3-*s4相等为0
s3++, s4++;
if(!*s4)//上面while在相等之后,s4指针到达尾部
return p;
p++;
}
return NULL;
}
/*
strtok()的原型为:char *strtok(char *s, char *delim)
功能为:分解字符串为一组字符串。s为要分解的字符串,delim为分隔符字符串。
*/
//由于此函数的功能比较难理解,在给出实现算法前先看看它的具体使用例子:
char s[] = "Nice to meet you!";
char *d = " ";
char *p = NULL;
p = strtok(s, d);
while(p) {
printf("%s\n",p);//”Nice” “to” “meet” “you!”
p = strtok(NULL,d);
}
//实现暂略
/*tolower()的原型:char tolower(char ch)
功能为:将大写字母转换为小写字母
*/
char _tolower(char ch) {
if(ch>=a && ch<=z) {
return ch;
}
if(ch>=A && ch=Z) {
return (ch + 'a' - 'A');
}
}
//删除特定字符
void deleteChar(char *str, char c) {
assert(str != NULL);
char *tmp = NULL;
while(*str != '\0') {
if(*str != c) {
*tmp++ = *str;
}
*str++;
}
}
void deleteChar(char *str, char c) {
assert(str != NULL);
int iDes = 0, iSrc = 0;
do {
if (str[iSrc] != c)
str[iDes++] = str[iSrc];
} while(str[iSrc++] != '\0');
}
//删除特定字符组void deleteChars(char *str, char chr[], int n)
//思路从字符串str中寻找chr第一次出现的位置,然后跑步指针赋值移位。
//逆置字符串
void reverseString(char *str) {
char c;
int n = strlen(str);
for (int i = 0; i < n/2; i++) {
c = str[i];
str[i] = str[n-1-i];
str[n-1-i] = c;
}
}