C语言 字符串、内存处理函数 qsort函数

关键字

非安全函数:strlen,sizeof,strcpy,strncpy,strcat,strncat,strcmp,strncmp,strtok,memcpy,atoi, memmove,memset
安全函数:所有的非安全函数后缀 _s
排序函数:qsort,对一维数组排序,二维数组排序,二维char数组排序
类型:size_t
本文所有函数参数定义来源于 Microsoft 文档


灵魂拷问

其实上述的函数都不是很难,但总是有一些小坑,所以我特意设置一些问题,如果你都答对的话基本上就都掌握了。

  • strlensizeof的返回值是什么类型?为什么要用这个类型?
  • sizeof一个指针变量(数组头),你得到的是它所代表的数组的大小,还是指针本身的大小?
  • int arr[10]; sizeof(arr)的答案又是什么?
  • strcpystrncpy source会不会自动加上截止符?
  • strcatstrncat source会不会自动加上截止符?
  • qsort 对二维数组排序怎么写,如果每排首个元素大小一样,就依次比下去。
  • strtok实际用法
  • memmovememcpy 会把最后的截止符也复制么?二者的区别是什么?
  • 你除了用memset给数组赋值过 0 ,还赋值过其他值么?如果第二个参数是 -1会怎么样? 1 呢?
int main()
{   
    int *arr = (int *)malloc(sizeof(int) * 10);
    memset(arr, -1, sizeof(int) * 10);  // 如果全都赋值 -1 会怎么样?
    for (int i = 0; i < 10; i++) {
        printf("%d\n", arr[i]);
    }
}

int main()
{   
    int *arr = (int *)malloc(sizeof(int) * 10);
    memset(arr, 1, sizeof(int) * 10);  // 如果全都赋值1会怎么样?
    for (int i = 0; i < 10; i++) {
        printf("%d\n", arr[i]);
    }
}
  • 请问这条代码,int arr[10] = {1};,arr的每一项分别是什么?
  • 请问这个qsort的cmp函数有什么问题么?
    int compare(const void *a, const void *b){
        return (struct element *)a->val - (struct element *)b->val;
    }

灵魂拷问答案

  • size_t类型,后面有解释
  • sizeof指针变量会得到这个指针本身的大小
  • int arr[10]; sizeof(arr) 会得到整个数组的大小
  • strcpy
    strncpy取决于 n 大小,如果n小于源长度,那么不会。大于源长度,会。
  • strcat strncat 都会在结尾加上 截止符。
  • qsort内容看后面。
  • memmove memcpy是纯内存操作,看你复制了什么过去。不会主动加东西。
  • memsetmemset填充,是以字节为单位,而不是以元素为单位。,这就是为什么你赋值0和-1都可以,但是1不行。因为赋值 -1 的时候,补码全都是1,最后也得到-1, 0也是一样。但是1不行。
  • arr的第一项是1,但是后面都是0(在我的编译器上是这样的)。
  • 优先级问题,强制类型转换优先级,小于 ->,所以程序会报错

size_t 类型

总结自 size_t 知乎的一篇文章,我觉得没必要研究的那么深入,先把他搞清楚是什么,怎么用,然后自己开始学着用就行了。
个人理解是,把它当成 unsigned int就行了,这是一个无符号数。C标准使用size_t的初衷是,为了方便移植。比如说在一个32位系统上,size_t代表了unsigned int,但是一到了64位系统上,它就适应系统,变成了unsigned long。因为size_t的取值range是目标平台下最大可能的数组尺寸。在我的电脑上,返回的是unsigned long long,每次printf都得用 "%llu"


qsort 快速排序

void qsort(
   void *base,
   size_t number,
   size_t width,
   int (__cdecl *compare )(const void *, const void *)
);

int cmp(const void *a, const void *b);
  • 为了让qsort可以对不同类型的指针进行排序,第一个参数是void类型的指针,不管是什么类型的指针,都可以被强制类型转换为void型的指针。这也解释了为什么qsort要使用第三个参数 width,因为不同类型的数组,每个元素占的大小是不同的,而qsort不清楚它需要每隔多少个字节去获得一个元素。
  • 最后一个参数cmp,是一个指向函数的指针。cmp的两个参数是待比较的,如果第一个数大于第二个数,正序排序,反之亦然。函数名作为参数时就是函数的地址,所以最后一个参数直接写cmp就好。
  • cmp函数的参数也是void类型的指针,也好理解,是为了让qosrt有通用性,第一个参数都是 void *base,cmp也得是void。
  • qsort对一维double数组排序,cmp写法
int cmp (const void *p1, const void *p2){
    const double *a1 = (const double *)p1;  // 因为p1的类型时void, 所以需要先强制类型转换成 double
    const double *a2 = (const double *)p2;
    return *a1 - *a2;
}
int cmp (const void *p1, const void *p2){
    return *((const double *)p1) - *((const double *)p1);  // 写的简短一点就这样子
}
  • qsort对二维double数组排序,cmp写法。
    有一块经常报错,记得qsort本体的第三个参数要改一下,因为这个是排和排之间排序,所以得把要排序的元素搞清楚
qsort(arr, arrSize, sizeof(arr[0]), cmp); 

如果只是想对第一个元素排序,那就用下面的代码。

int cmp(const void *p1,const void *p2)
{
    const int *a1 = (const int *)p1;
    const int *a2 = (const int *)p2;
    return *a1 - *a2;
}

下面这种排序是,排序每排的第一个元素,如果一样大,再排后面的。 但是这个代码有很大问题,假设第一排和第二排元素完全一致,这排到最后怕是要溢出。我自己试了一下,倒是没有报错,也不知道怎么回事。将来再探究下。

int cmp(const void *p1,const void *p2)
{
    const int *a1 = (const int *)p1;
    const int *a2 = (const int *)p2;
    if(*a1 != *a2)
        return *a1 - *a2;
    return cmp(a1 + 1, a2 + 1);
}
  • qsort对二维char数组排序
int cmp(const void *p1,const void *p2)
{
    const char *a1 = (const char *)p1;
    const char *a2 = (const char *)p2;
    return strcmp(a1,a2);
}
  • qsort 对结构体struct的一项进行排序
    有个小坑要注意一下,强制类型转换的优先级,是小于 ->符号的优先级的,所以cmp里面记得多包括号
 int compare(const void *a, const void *b){
    return ((struct element *)a)->val - ((struct element *)b)->val;
 }

strlen and sizeof

size_t strlen(
   const char *str
);

把他们放到一块说,是因为他们的返回值,都是size_t,也就是说,都是无符号数
他们都可以算一个字符串的长度,strlen不会把截止符'\0'算进去,但是sizeof会。
当然sizeof的用处更大一点,可以直接计算出来一个类型占据的空间。

不过sizeof也有个小坑,那就是不要使用sizeof对指针变量求值。如果你要用memset对一个被malloc出来的整型数组赋值,那么你可必须要用 sizeof(int) * arrSize 而不是 sizeof(arr)。因为在当前

int main()
{   
    int a[10];
    int *arr = (int *)malloc(sizeof(int) * 10);
    printf("%llu\n", sizeof(a));      // 这个是 40
    printf("%llu", sizeof(arr));      // 这个是 8,sizeof计算了int 类型指针变量的大小
}

strcpy and strncpy

下面是他们的定义

char *strcpy(
   char *strDestination,
   const char *strSource
);

char *strncpy(
   char *strDest,
   const char *strSource,
   size_t count
);

strncpy的第三个参数的意思是,从source复制 n 个字符到str,比strcpy智能一点,但是也不能保证这个 ndestination就小,所以想达到真正的安全,还是得用安全函数。
另外strcpy会包含截止符'\0',但是 strncpy 取决于参数 n 的大小
如果 第三个参数 n 小于source长度,那么只会把 n 个字符给整进去,不会以 '\0' 结尾,你要自己加。
如果n 大于 source 长度,那么会把整个source都给整进去,多出来的用截止符填充,所以用 '\0' 结尾。


strcat and strncat

char *strcat(
   char *strDestination,
   const char *strSource
);

char *strncat(
   char *strDest,
   const char *strSource,
   size_t count
);

上面那俩是直接覆盖,这两个是拼接,把source拼到destination后面。当然,都不安全。strncpy也就图一乐,真要安全还得安全函数。哈哈哈哈哈哈哈。
strcatsource会直接把dest的截止符'\0'给覆盖了,最后自带一个截止符。
strncatstrncpy不一样,不管n的大小,结尾自动带一个截止符'\0'。但是strncpy如果n过大,会填充截止符,但是strncat不会填充。


memmove and memcpy

void *memcpy(
   void *dest,
   const void *src,
   size_t count
);

void *memmove(
   void *dest,
   const void *src,
   size_t count
);

名字里一带 “mem”这说明是在内存层面上操作了。
所以就看第三个参数 n ,n是多大,就复制了多少字节过去。
内存操作不自动添加截止符,所以就看n的大小,有没有把截止符复制过去。
memmove相对来说比memcpy安全一点,因为它先把src复制到一个temp里面,然后再复制过去。所以避免了两个指针空间重叠的问题。缺点就是浪费资源,所以酌情使用


memset

void *memset(
   void *dest,
   int c,
   size_t count
);

切记,memset填充,是以字节为单位,而不是以元素为单位。

int main()
{   
    int *arr = (int *)malloc(sizeof(int) * 10);
    memset(arr, 1, sizeof(int) * 10);  // 如果全都赋值1会怎么样?
    for (int i = 0; i < 10; i++) {
        printf("%d\n", arr[i]);
    }
}

memset之后的存储空间的值是:
0x01010101 0x01010101 0x01010101 0x01010101 0x01010101 0x01010101 0x01010101 0x01010101 0x01010101 0x01010101


strtok

char *strtok(
   char *strToken,
   const char *strDelimit
);

下面直接给一个strtok的具体用法

int main(){
    char str[20] = "Nov 10th 2020";
    char *word;
    char **str2 = (char **)malloc(sizeof(char *) * 3);
    
    word = strtok(str, " ");
    int i = 0;
    while (word != NULL) {
        printf("%s\n", word);
        str2[i] = word;
        word = strtok(NULL, " ");
        i++;
    }

    for (int i =0; i < 3; i++) {
        printf("%s\n", str2[i]);
    }
    printf("now, str turns to %s", str);   // 这条代码为了检验 str是否被改变, 事实上他被改变成了 Nov
    return 0;
}

它的用法就是,第一次使用,把一个字符数组作为参数放进去,第二个参数输入词和词之间的间隔符,然后就能获得第一个间隔符前面的字符串。之后一次使用要在while里面,把NULL循环作为第一个参数,继续获得word,直到获得的最后一个word是截止符'\0'while中断,分割完成。

其实我也不知道为什么strtok需要在 while 循环里面用一个 NULL 来做第一个参数,但是,它就是这么用的。咱们先记住,先会用,再研究。

strtok的一个常见错误是,把常量赋值给指针,然后再作为strtok里面的第一个参数,因为该函数会改变字符串,所以会报出改变常量的 “Segmentation fault”错误。
所以不要把char str[20] = "Nov 10th 2020"; 改成 char *str = "Nov 10th 2020";


atoi

int atoi(
   const char *str
);

使用很简单,把字符串扔进去,然后返回int,注意用的时候别溢出int

int main(void) {
    char *str = " 110N2020NOV";
    //char str[20] = " 110N2020NOV";  // 不管是字符数组还是指针,二者均可执行
    int val = atoi(str);
    printf("%d", val);
}

但是我做了几个实验,我发现,第一个数字之前,可以有空格,但是不能有字母,这样子它才能读取到第一个整数,如果它先看到字符串的话,最后 val 返回值为 0。如果它看到了数字,那么遇到第一个不是数字的字符,读取就结束,返回已经读取的东西。

你可能感兴趣的:(C语言 字符串、内存处理函数 qsort函数)