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

C语言字符串的本质剖析

2023-01-225.2k 阅读

C语言字符串的定义与表示

字符数组与字符串

在C语言中,字符串通常通过字符数组来表示。字符数组是一种特殊的数组,其元素类型为char。例如:

char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

这里定义了一个字符数组str1,它包含6个字符,最后一个字符'\0'是字符串结束标志。在C语言里,任何以'\0'结尾的字符数组都被视为字符串。

也可以采用更简洁的方式初始化字符数组表示字符串:

char str2[] = "Hello";

编译器会自动在字符串常量"Hello"的末尾添加'\0',所以str2实际上占用6个字节的内存空间,与str1类似。

字符串常量

字符串常量是用双引号括起来的字符序列,如"Hello World"。它们在程序中存储在静态存储区,并且具有只读属性。例如:

const char *ptr = "Hello";

这里ptr指向一个字符串常量,虽然可以通过ptr来访问字符串中的字符,但如果试图修改该字符串会导致未定义行为。

字符串在内存中的存储

字符数组的存储方式

对于字符数组表示的字符串,它们存储在栈区(如果是局部变量)或者静态存储区(如果是全局变量)。以局部字符数组为例:

void test() {
    char localStr[] = "Local";
    // localStr存储在栈区,其占用空间为6个字节(包括'\0')
}

对于全局字符数组:

char globalStr[] = "Global";
// globalStr存储在静态存储区,占用空间为7个字节(包括'\0')

字符串常量的存储

字符串常量存储在静态存储区,并且相同的字符串常量在程序中只会存储一份。例如:

const char *s1 = "Hello";
const char *s2 = "Hello";

s1s2指向的是同一个字符串常量在静态存储区的位置,通过比较它们的地址可以验证:

#include <stdio.h>

int main() {
    const char *s1 = "Hello";
    const char *s2 = "Hello";
    if (s1 == s2) {
        printf("s1 and s2 point to the same location\n");
    } else {
        printf("s1 and s2 point to different locations\n");
    }
    return 0;
}

运行上述代码会输出s1 and s2 point to the same location

字符串操作函数

字符串输入输出函数

  1. printfputs printf函数可以格式化输出字符串,例如:
#include <stdio.h>

int main() {
    char str[] = "Hello, printf!";
    printf("%s\n", str);
    return 0;
}

puts函数则更简单,用于输出字符串并自动换行:

#include <stdio.h>

int main() {
    char str[] = "Hello, puts!";
    puts(str);
    return 0;
}
  1. scanfgets(不推荐使用gets scanf可以用于输入字符串,但它遇到空格、制表符或换行符就会停止读取。例如:
#include <stdio.h>

int main() {
    char input[20];
    printf("Enter a string: ");
    scanf("%s", input);
    printf("You entered: %s\n", input);
    return 0;
}

gets函数曾经用于读取整行字符串,直到遇到换行符,但由于它存在缓冲区溢出风险,C11标准已将其弃用,推荐使用fgets替代。

  1. fgets fgets函数从指定的流中读取一行数据到指定的字符数组中。例如:
#include <stdio.h>

int main() {
    char buffer[100];
    printf("Enter a string: ");
    fgets(buffer, sizeof(buffer), stdin);
    // fgets会读取换行符,若要去掉换行符可如下处理
    buffer[strcspn(buffer, "\n")] = '\0';
    printf("You entered: %s\n", buffer);
    return 0;
}

字符串处理函数

  1. strlen strlen函数用于计算字符串的长度,不包括字符串结束标志'\0'。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello";
    size_t len = strlen(str);
    printf("Length of the string is %zu\n", len);
    return 0;
}
  1. strcpystrncpy strcpy函数用于将一个字符串复制到另一个字符数组中。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char source[] = "Hello";
    char destination[10];
    strcpy(destination, source);
    printf("Copied string: %s\n", destination);
    return 0;
}

strcpy存在缓冲区溢出风险,strncpy则更安全,它最多复制指定长度的字符。例如:

#include <stdio.h>
#include <string.h>

int main() {
    char source[] = "Hello World";
    char destination[6];
    strncpy(destination, source, sizeof(destination) - 1);
    destination[sizeof(destination) - 1] = '\0'; // 手动添加字符串结束标志
    printf("Copied string: %s\n", destination);
    return 0;
}
  1. strcmpstrncmp strcmp函数用于比较两个字符串,返回值为0表示两个字符串相等,小于0表示第一个字符串小于第二个字符串,大于0表示第一个字符串大于第二个字符串。例如:
#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "apple";
    char str2[] = "banana";
    int result = strcmp(str1, str2);
    if (result < 0) {
        printf("%s is less than %s\n", str1, str2);
    } else if (result > 0) {
        printf("%s is greater than %s\n", str1, str2);
    } else {
        printf("%s is equal to %s\n", str1, str2);
    }
    return 0;
}

strncmp则是比较指定长度的字符。 4. strcatstrncat strcat函数用于将一个字符串追加到另一个字符串的末尾。例如:

#include <stdio.h>
#include <string.h>

int main() {
    char str1[20] = "Hello, ";
    char str2[] = "World!";
    strcat(str1, str2);
    printf("Concatenated string: %s\n", str1);
    return 0;
}

同样,strcat有缓冲区溢出风险,strncat更安全,它最多追加指定长度的字符。

指针与字符串

字符指针指向字符串

字符指针可以指向字符串常量或者字符数组。例如:

#include <stdio.h>

int main() {
    const char *ptr1 = "Hello"; // 指向字符串常量
    char str[] = "World";
    char *ptr2 = str; // 指向字符数组
    printf("%s %s\n", ptr1, ptr2);
    return 0;
}

当字符指针指向字符串常量时,不能通过指针修改字符串内容,因为字符串常量是只读的。而指向字符数组时,可以修改数组中的字符。

指针操作字符串的优势与风险

使用指针操作字符串有时比使用数组更灵活,比如在函数参数传递中,传递指针比传递整个数组效率更高。例如:

#include <stdio.h>
#include <string.h>

void printString(const char *str) {
    printf("%s\n", str);
}

int main() {
    char str[] = "Using pointer in function";
    printString(str);
    return 0;
}

然而,指针操作字符串也存在风险,如指针越界访问。例如:

#include <stdio.h>

int main() {
    const char *ptr = "Hello";
    // 下面这行代码会导致未定义行为,因为试图访问越界内存
    printf("%c\n", *(ptr + 10)); 
    return 0;
}

字符串与内存管理

动态分配字符串内存

在一些情况下,我们需要根据实际需求动态分配字符串的内存。可以使用malloccallocrealloc函数。例如,使用malloc动态分配内存来存储用户输入的字符串:

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

int main() {
    int length;
    printf("Enter the length of the string: ");
    scanf("%d", &length);
    char *str = (char *)malloc(length + 1);
    if (str == NULL) {
        perror("malloc");
        return 1;
    }
    printf("Enter the string: ");
    scanf("%s", str);
    printf("You entered: %s\n", str);
    free(str);
    return 0;
}

这里先使用malloc分配了length + 1个字节的内存(加上'\0'的空间),使用完后通过free释放内存,避免内存泄漏。

避免内存泄漏与缓冲区溢出

  1. 内存泄漏 内存泄漏发生在动态分配的内存没有被释放的情况下。例如:
#include <stdio.h>
#include <stdlib.h>

void memoryLeak() {
    char *str = (char *)malloc(100);
    // 没有调用free(str),导致内存泄漏
}

为了避免内存泄漏,一定要确保在不再需要动态分配的内存时调用free函数。

  1. 缓冲区溢出 缓冲区溢出是指写入的数据超出了缓冲区的大小。例如,使用strcpy时如果目标缓冲区过小:
#include <stdio.h>
#include <string.h>

int main() {
    char smallBuffer[5];
    char *longString = "This is a long string";
    // 下面这行代码会导致缓冲区溢出
    strcpy(smallBuffer, longString); 
    return 0;
}

为避免缓冲区溢出,应使用更安全的字符串操作函数,如strncpystrncat等,并确保缓冲区大小足够。

字符串的应用场景

文件操作中的字符串

在文件操作中,字符串常用于读取和写入文本文件。例如,使用fgets从文件中读取一行字符串:

#include <stdio.h>

int main() {
    FILE *file = fopen("test.txt", "r");
    if (file == NULL) {
        perror("fopen");
        return 1;
    }
    char buffer[100];
    while (fgets(buffer, sizeof(buffer), file) != NULL) {
        printf("%s", buffer);
    }
    fclose(file);
    return 0;
}

也可以使用fprintf将字符串写入文件:

#include <stdio.h>

int main() {
    FILE *file = fopen("test.txt", "w");
    if (file == NULL) {
        perror("fopen");
        return 1;
    }
    char str[] = "This is a line written to the file.";
    fprintf(file, "%s\n", str);
    fclose(file);
    return 0;
}

网络编程中的字符串

在网络编程中,字符串常用于数据传输和解析。例如,在简单的TCP客户端 - 服务器模型中,服务器接收客户端发送的字符串并处理。以UNIX系统为例,下面是一个简单的服务器端代码片段:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    char buffer[BUFFER_SIZE];
    socklen_t len = sizeof(cliaddr);
    int n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, MSG_WAITALL, (struct sockaddr *) &cliaddr, &len);
    buffer[n] = '\0';
    printf("Client : %s\n", buffer);
    close(sockfd);
    return 0;
}

客户端发送字符串:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024

int main(int argc, char const *argv[]) {
    int sockfd;
    struct sockaddr_in servaddr;
    sockfd = socket(AF_INET, SOCK_DUDP, 0);
    if (sockfd < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    servaddr.sin_addr.s_addr = inet_addr(SERVER_IP);
    char buffer[BUFFER_SIZE] = "Hello, Server!";
    sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));
    printf("Message sent.\n");
    close(sockfd);
    return 0;
}

这里通过UDP协议在客户端和服务器之间传输字符串。

字符串在数据解析中的应用

在数据解析中,常常需要对字符串进行分割、提取等操作。例如,将一个以逗号分隔的字符串拆分成多个子字符串:

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "apple,banana,orange";
    char *token = strtok(str, ",");
    while (token != NULL) {
        printf("%s\n", token);
        token = strtok(NULL, ",");
    }
    return 0;
}

strtok函数用于分割字符串,这里以逗号为分隔符将字符串str分割成多个子字符串并输出。

通过以上对C语言字符串的深入剖析,包括其定义、存储、操作函数、指针应用、内存管理以及应用场景等方面,希望能帮助读者更全面、深入地理解C语言字符串的本质和使用方法,从而在实际编程中更准确、高效地运用字符串相关知识。