MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C标准库常用函数实现原理剖析

2024-09-293.4k 阅读

字符串处理函数

strlen函数

strlen函数用于计算字符串的长度,不包括字符串结束符'\0'。它的实现原理较为简单,通过遍历字符串,直到遇到'\0'字符,然后返回已经遍历过的字符个数。

size_t strlen(const char *str) {
    const char *s;
    for (s = str; *s != '\0'; ++s);
    return (s - str);
}

在上述代码中,定义了一个指针s,让它从str开始,不断移动,当遇到'\0'时停止。通过(s - str)计算出字符串的长度,这是因为指针相减得到的是两个指针之间的元素个数(这里的元素就是字符)。

strcpy函数

strcpy函数用于将一个字符串复制到另一个字符串中。它会从源字符串的起始位置开始,逐个复制字符,直到遇到'\0',并将'\0'也复制过去。

char *strcpy(char *dest, const char *src) {
    char *tmp = dest;
    while ((*dest++ = *src++) != '\0');
    return tmp;
}

代码中,定义了一个临时指针tmp保存目标字符串的起始地址,以便最后返回。然后通过while循环,在复制每个字符的同时进行判断,当复制到'\0'时,循环结束,这样就完成了字符串的复制。同时,返回目标字符串的起始地址,以便支持链式调用。

strcat函数

strcat函数用于将一个字符串追加到另一个字符串的末尾。它首先要找到目标字符串的末尾(即'\0'的位置),然后从源字符串的起始位置开始,将源字符串复制到目标字符串末尾,直到源字符串的'\0'也被复制过去。

char *strcat(char *dest, const char *src) {
    char *tmp = dest;
    while (*dest) dest++;
    while ((*dest++ = *src++) != '\0');
    return tmp;
}

在这个实现中,第一个while循环找到目标字符串的末尾,即'\0'的位置。第二个while循环将源字符串复制到目标字符串的末尾,和strcpy类似,当遇到源字符串的'\0'时停止复制,并将'\0'也复制过去。最后返回目标字符串的起始地址,以支持链式调用。

strcmp函数

strcmp函数用于比较两个字符串。它会逐个字符地比较两个字符串对应位置的字符,直到遇到不同的字符或者遇到某个字符串的'\0'。如果两个字符串完全相同,则返回0;如果第一个字符串小于第二个字符串,则返回小于0的值;如果第一个字符串大于第二个字符串,则返回大于0的值。

int strcmp(const char *s1, const char *s2) {
    while (*s1 && *s2 && *s1 == *s2) {
        s1++;
        s2++;
    }
    return (*s1 - *s2);
}

在上述代码中,通过while循环不断比较两个字符串的字符,当遇到不同字符或者某个字符串结束时,跳出循环。然后通过*s1 - *s2返回比较结果,如果*s1小于*s2,则结果小于0;如果*s1大于*s2,则结果大于0;如果*s1*s2都为'\0',则结果为0。

内存操作函数

memcpy函数

memcpy函数用于从源内存区域复制指定长度的字节到目标内存区域。它会按照字节为单位进行复制,不管内存中的数据类型是什么。

void *memcpy(void *dest, const void *src, size_t n) {
    char *d = (char *)dest;
    const char *s = (const char *)src;
    while (n--) {
        *d++ = *s++;
    }
    return dest;
}

在这个实现中,首先将destsrc转换为char *类型,因为要按字节复制。然后通过while循环,每次复制一个字节,循环次数由n决定。最后返回目标内存区域的起始地址,以便支持链式调用。

memmove函数

memmove函数同样用于内存复制,与memcpy不同的是,它可以处理源内存区域和目标内存区域重叠的情况。

void *memmove(void *dest, const void *src, size_t n) {
    char *d = (char *)dest;
    const char *s = (const char *)src;
    if (d < s) {
        while (n--) {
            *d++ = *s++;
        }
    } else {
        d += n;
        s += n;
        while (n--) {
            *--d = *--s;
        }
    }
    return dest;
}

这里通过判断目标地址是否小于源地址来决定复制的方向。如果目标地址小于源地址,即不重叠或者重叠但不会覆盖已复制的数据,就按照正常顺序从前往后复制;如果目标地址大于等于源地址,说明可能会覆盖已复制的数据,所以从后往前复制,这样就能正确处理重叠的情况。最后返回目标内存区域的起始地址。

memset函数

memset函数用于将一段内存区域填充为指定的值。

void *memset(void *s, int c, size_t n) {
    char *p = (char *)s;
    while (n--) {
        *p++ = (char)c;
    }
    return s;
}

在实现中,将void *类型的指针s转换为char *类型,以便按字节操作。通过while循环,每次将一个字节填充为指定的值c,循环次数由n决定。最后返回填充后的内存区域的起始地址。

数学函数

abs函数

abs函数用于计算一个整数的绝对值。对于整数类型,实现较为简单,通过判断正负来决定返回值。

int abs(int j) {
    return (j < 0)? -j : j;
}

这里通过条件表达式判断j是否小于0,如果小于0则返回其相反数,否则直接返回j本身,从而得到j的绝对值。

sqrt函数

sqrt函数用于计算一个数的平方根。常见的实现方法是使用牛顿迭代法。牛顿迭代法的基本思想是通过不断逼近函数的零点来求解方程。对于求平方根,即求解方程x² - a = 0a为要求平方根的数)。

double sqrt(double num) {
    double x = num;
    double y = 1;
    double e = 0.000001;
    while (x - y > e) {
        x = (x + y) / 2;
        y = num / x;
    }
    return x;
}

在上述代码中,首先初始化x为要计算平方根的数numy为1。然后通过while循环,不断更新xy的值,x(x + y) / 2ynum / x,当x - y的差值小于一个极小值e时,认为已经逼近平方根,此时返回x

pow函数

pow函数用于计算一个数的指定次幂。一种简单的实现方法是通过循环累乘。

double pow(double base, double exponent) {
    double result = 1.0;
    if (exponent > 0) {
        for (int i = 0; i < exponent; i++) {
            result *= base;
        }
    } else if (exponent < 0) {
        for (int i = 0; i < -exponent; i++) {
            result /= base;
        }
    }
    return result;
}

这里根据指数exponent的正负来决定计算方式。如果指数大于0,通过for循环将base累乘exponent次;如果指数小于0,通过for循环将1 / base累乘-exponent次。最后返回计算结果。

输入输出函数

printf函数

printf函数是C标准库中常用的格式化输出函数。它的实现较为复杂,涉及到格式化字符串的解析、参数的处理以及输出操作。

简单来说,printf函数会从格式化字符串的开头开始解析,遇到普通字符直接输出,遇到格式说明符(如%d%s等)时,根据格式说明符的类型从参数列表中取出相应类型的参数,并进行格式化输出。

#include <stdio.h>
#include <stdarg.h>

int my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    int count = 0;
    while (*format) {
        if (*format != '%') {
            putchar(*format);
            count++;
        } else {
            format++;
            switch (*format) {
                case 'd': {
                    int num = va_arg(args, int);
                    char buffer[20];
                    int len = snprintf(buffer, sizeof(buffer), "%d", num);
                    fwrite(buffer, 1, len, stdout);
                    count += len;
                    break;
                }
                case's': {
                    char *str = va_arg(args, char *);
                    fputs(str, stdout);
                    count += strlen(str);
                    break;
                }
                // 其他格式说明符的处理类似
            }
        }
        format++;
    }
    va_end(args);
    return count;
}

在这个简化的my_printf实现中,首先使用va_listva_start来处理可变参数列表。然后通过while循环解析格式化字符串,遇到普通字符直接输出并计数。遇到%时,根据后续的格式说明符从参数列表中取出相应参数,进行格式化处理并输出,同时更新计数。最后使用va_end结束可变参数的处理,并返回输出的字符总数。

scanf函数

scanf函数用于从标准输入读取格式化数据。它的实现也涉及到格式化字符串的解析以及将读取的数据存储到相应的变量中。

#include <stdio.h>
#include <stdarg.h>

int my_scanf(const char *format, ...) {
    va_list args;
    va_start(args, format);
    int count = 0;
    while (*format) {
        if (*format != '%') {
            int ch;
            do {
                ch = getchar();
            } while (ch!= *format && ch!= EOF);
            if (ch == EOF) break;
        } else {
            format++;
            switch (*format) {
                case 'd': {
                    int *num = va_arg(args, int *);
                    scanf("%d", num);
                    count++;
                    break;
                }
                case's': {
                    char *str = va_arg(args, char *);
                    scanf("%s", str);
                    count++;
                    break;
                }
                // 其他格式说明符的处理类似
            }
        }
        format++;
    }
    va_end(args);
    return count;
}

在这个简化的my_scanf实现中,同样使用va_listva_start处理可变参数。通过while循环解析格式化字符串,遇到普通字符时从输入中读取直到匹配该字符。遇到%时,根据格式说明符从输入中读取相应类型的数据并存储到对应的变量中,同时更新计数。最后使用va_end结束可变参数处理并返回读取的数据项数。

时间函数

time函数

time函数用于获取当前的日历时间,通常返回从1970年1月1日00:00:00 UTC到当前时间所经过的秒数。

#include <time.h>
#include <sys/time.h>

time_t time(time_t *t) {
    struct timeval tv;
    gettimeofday(&tv, NULL);
    time_t seconds = tv.tv_sec;
    if (t!= NULL) {
        *t = seconds;
    }
    return seconds;
}

在这个实现中,通过gettimeofday函数获取当前的时间,gettimeofday函数返回的timeval结构体中包含了秒数和微秒数,这里只取秒数部分。如果传入的指针t不为空,则将秒数存储到该指针指向的位置,最后返回秒数。

localtime函数

localtime函数用于将time_t类型表示的时间转换为本地时间,以tm结构体的形式返回。

#include <time.h>
#include <stdio.h>
#include <stdlib.h>

struct tm *localtime(const time_t *timer) {
    static struct tm result;
    time_t seconds = *timer;
    // 计算年
    int year = 1970;
    while (seconds >= 31536000) {
        if ((year % 4 == 0 && year % 100!= 0) || (year % 400 == 0)) {
            if (seconds >= 31622400) {
                seconds -= 31622400;
                year++;
            } else {
                break;
            }
        } else {
            seconds -= 31536000;
            year++;
        }
    }
    // 计算月
    int month = 0;
    int days_per_month[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    if ((year % 4 == 0 && year % 100!= 0) || (year % 400 == 0)) {
        days_per_month[1] = 29;
    }
    while (seconds >= days_per_month[month] * 86400) {
        seconds -= days_per_month[month] * 86400;
        month++;
    }
    // 计算日
    int day = 1;
    while (seconds >= 86400) {
        seconds -= 86400;
        day++;
    }
    // 计算时、分、秒
    int hour = seconds / 3600;
    seconds %= 3600;
    int minute = seconds / 60;
    int second = seconds % 60;

    result.tm_year = year - 1900;
    result.tm_mon = month;
    result.tm_mday = day;
    result.tm_hour = hour;
    result.tm_min = minute;
    result.tm_sec = second;
    // 其他字段的计算省略
    return &result;
}

在这个实现中,通过对秒数进行逐步计算,先计算年,再根据是否为闰年调整每月的天数来计算月,接着计算日,最后计算时、分、秒,并填充到tm结构体中返回。实际的标准库实现可能会更复杂,还会考虑时区等因素。

动态内存分配函数

malloc函数

malloc函数用于在堆上分配指定字节数的内存空间,并返回一个指向该内存块起始地址的指针。如果分配失败,则返回NULL

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define HEADER_SIZE sizeof(size_t)

void *malloc(size_t size) {
    if (size == 0) {
        return NULL;
    }
    size_t total_size = size + HEADER_SIZE;
    void *block = sbrk(total_size);
    if (block == (void *)-1) {
        return NULL;
    }
    *((size_t *)block) = size;
    return (void *)((char *)block + HEADER_SIZE);
}

在这个简单的实现中,首先考虑到要在分配的内存块前加上一个头部,用于存储分配的实际大小,所以total_size为请求的大小加上头部大小。然后使用sbrk函数来增加程序数据段的大小,从而获得新的内存块。如果sbrk返回-1,说明分配失败,返回NULL。否则,将分配的大小存储在头部,然后返回内存块实际数据部分的起始地址(即头部之后的位置)。

free函数

free函数用于释放由malloccallocrealloc分配的内存空间。

void free(void *ptr) {
    if (ptr == NULL) {
        return;
    }
    size_t size = *((size_t *)((char *)ptr - HEADER_SIZE));
    size_t total_size = size + HEADER_SIZE;
    if (sbrk(-total_size) == (void *)-1) {
        // 处理释放失败的情况,这里简单忽略
    }
}

free函数中,首先判断指针是否为NULL,如果是则直接返回。然后通过减去头部大小得到头部的位置,从而获取分配的实际大小。接着使用sbrk函数将程序数据段减小total_size,即释放内存。如果sbrk返回-1,说明释放失败,这里只是简单忽略,实际应用中可能需要更复杂的错误处理。

calloc函数

calloc函数用于分配一块指定数量和指定大小的内存空间,并将其初始化为0。

void *calloc(size_t nmemb, size_t size) {
    size_t total_size = nmemb * size;
    void *block = malloc(total_size);
    if (block == NULL) {
        return NULL;
    }
    memset(block, 0, total_size);
    return block;
}

在这个实现中,首先计算出需要分配的总大小total_size。然后调用malloc函数分配内存,如果分配失败则返回NULL。如果分配成功,使用memset函数将分配的内存块初始化为0,最后返回分配并初始化后的内存块指针。

realloc函数

realloc函数用于重新分配已经分配的内存块的大小。它可以增大或减小内存块的大小,如果新的大小大于原大小,可能会移动内存块的位置。

void *realloc(void *ptr, size_t size) {
    if (ptr == NULL) {
        return malloc(size);
    }
    if (size == 0) {
        free(ptr);
        return NULL;
    }
    size_t old_size = *((size_t *)((char *)ptr - HEADER_SIZE));
    if (size <= old_size) {
        return ptr;
    }
    void *new_block = malloc(size);
    if (new_block == NULL) {
        return NULL;
    }
    size_t copy_size = old_size < size? old_size : size;
    memcpy(new_block, ptr, copy_size);
    free(ptr);
    return new_block;
}

realloc函数中,首先处理特殊情况:如果ptrNULL,则相当于调用malloc分配新内存;如果size为0,则相当于调用free释放内存并返回NULL。然后获取原内存块的大小old_size,如果新大小size小于等于原大小,则直接返回原指针。否则,调用malloc分配新的内存块,计算需要复制的大小copy_size,使用memcpy将原内存块的数据复制到新内存块,最后释放原内存块并返回新内存块的指针。如果新内存分配失败,则返回NULL