跳转至

Array, Pointer and String

约 3580 个字 77 行代码 预计阅读时间 13 分钟

Chapter 4 数组和指针

重要:声明一个指针只会分配一个给指针变量的空间(这部分空间用来存储它指向的位置的地址值),而不会分配指向的空间。使一个指针可用可以将其它变量取地址赋值给它,这样它指向的位置就是有效的。或者通过 malloc 来新分配一块堆上的内存,malloc 的返回值就是这块内存的首地址,也是你可用的。

:二维数组不能退化为二级指针;数组名不能被重新赋值。

:数组是数组,指针是指针(这里指类型)。

:指针相减的意义是计算两个指针相差几个“单位”的距离,而不是将其值简单的相减。比如:

1
2
3
int a[] = {1, 2, 3, 4, 5};
int *p = a, *q = &a[2];
printf("%lu", q-p);         // Output: 2
double a[]={1, 2, 3, 4, 5};
printf("%d", (int)&a[3] - (int)&a[0]);  // Output: 24

神坑:变长数组不能通过int a[n] = {0};的方式初始化

Chapter 5:字符串和字符串函数

5.1 字符串的定义与初始化

字符串其实是以空字符\0结尾的char类型数组,因此,我们可以像处理一般数组的方式处理字符串,比如:

1
2
3
char words[81] = "I am a string in an array.";//定义字符串words
words[8] = 'p';//将字符串的第9个字符改为'p'
const char* MSG = "This cannot be changed";//定义只读字符串MSG

如果要打印MSG[22],则输出的是空字符,空字符不是空格,不会在输出窗口占用位置,只是标志字符串数组的结束。

我们一般用三种方法定义字符串:字符串常量、char类型数组、指向char类型的指针。被双引号括起来的内容被视为指向该字符串存储位置的指针,这类似于将数组名作为指向该数组的指针。比如以下程序:

printf("%s,%p,%c","We","are","champions");
//Output: We,0x10000f61,c

我们对字符串用%c%p进行转换的时候,转换过去的其实是字符串第一个元素的地址和其对应的字符

数组形式的字符串(如char arr1[] = "III" )在计算机的内存中分配一个内含4个元素的数组,每个元素作为一个字符,且最后一个元素为空字符。先将字符串常量存储在静态存储区中,程序开始运行之后为数组分配内存,初始化数组将静态存储区的字符串拷贝到数组中,编译器将数组名arr1作为该数组首元素地址的别名,而且作为地址常量,不能被改变。

一般来说,指针形式的定义一般于字符串字面量一起使用,被双引号括起来的内容是字符串字面量,而且被视为字符串的地址。指针形式(如char *pt1 = "III"让编译器在静态存储区中分配4个元素的空间,开始运行程序时,编译器为指针变量(*pt1)留出一个存储位置,该变量最初指向该字符串的首字母,但是它的值可以被改变,即可以使用递增运算符。

由于指针形式字符串的存储形式,一般建议将指针初始化为字符串自变量时使用const 限定符。

编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量,所以下面程序打印出来的都是"Jey"

1
2
3
4
char *p1 = "Hey";
p1[0] = 'J';
printf("Hey");
printf("%s","Hey");

由于数组名是一个指针变量,所以不能用str1 = str2 来简单地拷贝数组,这样只会让两个指针指向相同的内存区域。

我们可以定义字符串数组,也就是通过数组下标来访问不多个不同的字符串,有两种方式:使用存储字符串指针的数组或者多维数组:

const char* strarr1[3] = {
    "Hello",
    "Pardon",
    "Excuse me";
};
char strarr2[3][10] = {
    "Hello",
    "Pardon",
    "Excuse me";
};

这两种方式最后实现的效果是几乎一样的,都代表着五个字符串,只使用一个下标时只代表一个字符串。比如strarr1[0]strarr2[0]都代表着字符串 "Hello"

一般来说对数组的操作都是依赖于指针进行的。

5.2 字符串输入和输出

5.2.1 分配空间

最简单的分配空间的方式就是在 stack 上建立数组变量,而且还只能如此建立

char name[81];
scanf("%s",name);

再就是利用C库函数malloc()分配内存,比如char *name = (char *) malloc (sizeof(char)*8)这样就可以按照数组形式的字符串来使用字符串了

5.2.2 危险的gets()函数

C11标准中,废弃了不安全的gets()函数,但是大多数编译器为了兼容性,仍然保留gets()函数。

gets()函数读取一整行输入,直到遇到换行符,然后丢弃换行符,存储其余字符在传递进来的字符串指针指向的地址上,并在字符的末尾添加一个空字符,使其成为字符串。比如:

1
2
3
4
5
int strl = 10;
char words[strl];
puts("Enter a string please.");
gets(words);
puts(words):

puts()函数经常和gets()函数一起使用,这个函数用于显示字符串,并且在字符串的末尾添加换行符。

使用gets()函数时,gets()函数只知道数组的开始处,而不会检查数组的长度和字符串的长度是否相融洽。如果输入的字符串过长,超出了数组的存储范围,就会造成缓冲区溢出(buffer overflow),读取的数据将一直向后存储,覆盖掉后边内存上的内容,如果这些多余的字符只是占用了未被使用的内存,就不会立刻出现问题,而如果擦写掉了程序中的其余内存,这样就会让程序异常终止,或者出现其他情况。

出现fragmentation fault的错误的时候,一般是程序试图访问某些未被分配的内存。

5.2.3 gets()的替代品

  • fgets()fputs()函数

    fgets()函数接受三个参数:字符串存储的位置、读入字符的最大数量和要输入的文件。

    fgets()函数接受的第二个参数时读入数组的最大数量,如果该参数的值是n那么fgets()将读入n-1个字符,并且在最后加上一个空字符,或者读到第一个换行符号为止。

    fgets()函数的第三个参数指明要读入的文件,如果是从键盘读入数据,那么以stdin作为参数,或者输入文件指针。

    当输入行不溢出的时候,fgets()函数将换行符放在结尾,这与fputs()函数的特性相仿:这个函数在打印字符串的时候不会在最后加上换行符。可是如果使用puts()函数一起使用,那么可能就会发现出现了两个换行。

    fputs()函数接受两个参数:第一个指出要写入的字符串的位置,第二个指出目标写入的位置,如果输出到屏幕上,那么输入stdout作为参数。

    fgets()返回指向char的指针,如果一切顺利,函数返回的地址和传入的第一个参数相同,如果传到文件的结尾,将返回一个特殊的指针:空指针(null pointer),这个指针不会指向有效的数据,所以可以用来标识特殊情况。在代码钟可以用数字0来代替,但是C利用宏NULL来代替。下面是一个很有意思的例子:

    1
    2
    3
    4
    5
    6
    7
    char words[10];
    while(fgets(words,10,stdin) != NULL && words[0]!='\n') {
        fputs(words,stdout);
    }
    
    //Input :By the way,it returns a NULL pointer.
    //Output:By the way,it returns a NULL pointer.
    

    这个程序的实际操作过程是:首先fgets()函数读入9个字符,在后边加入\\0之后交给fputs()函数输出,但是此时不输出换行符,接着进入下一轮迭代,fgets()函数继续读入字符、交给fputs()函数输出……

  • gets_s()函数

  • s_gets()函数

    我们可以利用fgets()函数自行创建一个读取整行输入,并且利用空字符取代换行符、或者读取一部分字符,丢弃溢出的字符(其余部分的字符)的函数:

    char *s_gets(char *st, int n){
        char *ret_val;
        int i = 0;
        ret_val = fgets(st, n, stdin);
        if(ret_val){
            while(st[i] != '\n' && st[i] != '\0')
                i++;
            if(st[i] == '\n')
                st[i] = '\0';
            else
                while(getchar()!= '\n')
                    continue;
        }
        return ret_val;
    }
    

    利用字符串函数,我们可以对函数进行修改,让它更加简洁。

    char *s_gets(char *st, int n) {
        char *ret_val;
        char *find;
    
        ret_val = fgets(st, n, stdin);
        if(ret_val) {
            find = strchr(st, '\n');
            if(find)
                *find = '\0';
            else
                while(getchar() != '\n')
                    continue;
        }
        return ret_val;
    }
    

    如果fgets()函数返回NULL,则证明读到文件结尾或者读取错误,s_gets()函数跳过了这个过程。

    我们丢弃多余的字符的原因是:这些多余的字符都存储于缓冲区之中,如果我们下一个要读取的数据是double类型的,那么就可能造成程序崩溃(因为输入了char类型甚至char*类型的数据),丢弃剩余行的数据可以令读取语句和键盘输入同步。

    这个函数并不完美,因为它在遇到不合适的输入的时候毫无反应,并且丢弃多余的字符的时候,不会告诉程序也不会告诉用户。但是至少会比gets()函数安全的多;-)。

  • scanf()函数

    scanf()函数和%s转换说明可以读取字符串,但是scanf()函数在读取到空白字符(包括空格、换行符和空字符)的时候会终止对字符串的读取。scanf()函数还有另外一种确定输入结束的方法,也就是指定字符宽度,比如%5d,那么scanf()将在读取完五个字符或者读取到第一个空白字符后停止。

5.2.4 字符串输出

  1. 在输出字符串的时候,我们必须确定字符串末尾有指示终止的空字符,下面就是一个错误的例子:char words[]={'H','e','y','!'};

由于这个字符数组(不是字符串!)结尾并未有空字符,所以words不是字符串,如果我们使用这样的代码:put(words)puts()函数由于未识别到空字符,就会一直向下读取、输出后续内存中的内容,这或许是 garbage value ,直到读到内存中的空字符(内存中还是有不少空字符的)。

  1. puts()函数很容易使用,只需要传入需要输出的字符串的地址就可以了,它在输出的时候会在后边加上一个换行符。但是puts()函数的返回值众说纷纭:某些编译器返回的是输出的字符的个数,某些编译器输出的是输出的最后一个字符,有的干脆就返回一个非零的整数。

  2. fputs()函数需要接受两个参数,一个是字符串的地址,另一个是写入的地址:这一般是文件指针,如果需要输出到屏幕上,传入stdout则可。这个函数的特点在于不会输出换行符。

  3. printf()函数需要转换说明,它的形式更复杂些,需要输入更多的代码,计算机执行的时间会更长,但是优点在于可以更容易地输出更复杂、更多的字符串。

5.2.5 自定义输入输出函数

5.3 字符串函数

这里讲的字符串函数是指定义在头文件string.h内的函数。

  1. strlen()函数

strlen()函数的实现其实很简单,我们写一个while循环就好了(),strlen()函数接受字符串地址,返回一个unsigned int的值来表示字符串的长度。

重要的是我们可以利用strlen()函数来得到字符串的一些性质参数,进而更容易实现对字符串的操作,比如我们可以利用下面自行设计的函数来实现字符串的截断:

1
2
3
4
5
6
7
8
9
char *vit(char str[],unsigned int point) {
     unsigned int length = strlen(str);
     if(point > length - 1) {
         return str;
     } else {
         str[point]='\\0';
         return str;
     }   
 }
  1. strcat()函数

    strcat()函数接受两个字符串作为参数,用于将两个字符串拼接在一起,更确切地说是将第二个字符串的拷贝附加在第一个字符串的末尾,并且将拼接后的字符串作为第一个字符串,第二个字符串不变。strcat()函数返回第一个参数。

    strcat()函数和gets()函数一样,如果使用不当,也会导致缓冲区溢出。但是gets()函数被废弃的原因在于无法控制用户向程序里边输入什么,但是程序员是可以控制程序干什么的。因此,在经历输入的检查之后,我们认为至少程序是比较安全的,而使用strcat()函数不当导致缓冲区溢出的情况,被认为是程序员粗心导致的,而C语言相信程序员,程序员也有责任确保strcat()函数的使用安全。

  2. strncat()函数

    为了避免strcat()函数的不安全的可能,我们类似fputs()函数那样,添加第二个参数,确定最大添加字符数,这就是strncat()函数的逻辑。

    strncat()函数接受三个参数,两个字符串指针和最大添加字符量,在加到最大字符量或者遇到空字符的时候停止。

    配合strlen()函数,strncat()函数可以很好用。

  3. strcmp()函数和strncmp()函数

    首先,我们比较两个字符串的时候,比较的是字符串的内容,而不是字符串的地址,所以我们不能做判断指针是否相等的操作,而利用循环挨个判断还蛮复杂,这就是strcmp()函数诞生的逻辑。

    strcmp()函数接受两个字符串指针参数,如果字符串内容完全相等(包括大小写),strcmp()函数就会返回0,否则返回非零值。

    在字符串内容不一样的时候,如果第一个字符串的字符在ASCII码在第二个字符串的之前,strcmp()返回负数,反之返回正数;在某些编译器中,会作更加复杂的操作,也就是返回两字符的ASCII码的差。

    strcmp()函数会一直比较字符是否相同,直到出现不同或者字符串结束,这样的比较方式显得就非常笨重,而strncmp()函数提供了一种更为灵活的选择:strncmp()函数接受的第三个整数参数指定了比较到第几个字符(这里从1开始计数 ;-) )比如strncmp(str1,"strings",7)就指定只查找strings这七个字符。

  4. strcpy()函数和strncpy()函数 strcpy()函数

  5. sprint()函数
  6. memcpy()函数
  7. Others.

    • strchr()函数

    • strrchr()函数

    • strstr()函数

    • atoi()函数

    • Character Classification

      isalpha()函数

      isalpha()函数属于一类的函数还有

      tolower()toupper()函数

      cppreference 上对这两个函数归类为 Character Manipulation 解释是:converts a character to lowercase/uppercase.