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

C语言字符串与指针的操作

2022-10-012.3k 阅读

一、C 语言字符串基础

1.1 字符串的定义与表示

在 C 语言中,字符串是由字符组成的序列,并且以空字符 '\0' 作为结束标志。字符串通常有两种表示方式:字符数组和字符指针。

字符数组表示法

char str1[] = "Hello, World!";

在这个例子中,str1 是一个字符数组,编译器会自动在字符串常量 "Hello, World!" 的末尾添加空字符 '\0',所以 str1 实际占用的内存空间为 14 个字节(13 个字符加上 1 个 '\0')。

字符指针表示法

char *str2 = "Hello, World!";

这里 str2 是一个指向字符串常量首字符的指针。字符串常量 "Hello, World!" 存储在程序的只读数据段中,str2 指向这个只读区域。需要注意的是,虽然这种方式看起来和字符数组类似,但本质上是不同的。通过字符指针指向的字符串常量不能被修改,否则会导致未定义行为。例如:

char *str2 = "Hello, World!";
str2[0] = 'h'; // 这将导致未定义行为

1.2 字符串的输入与输出

1.2.1 使用 printf 函数输出字符串

printf 函数是 C 语言中最常用的输出函数之一,用于输出各种类型的数据,包括字符串。使用 %s 格式说明符来输出字符串。

#include <stdio.h>

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

在上述代码中,printf("%s\n", str) 将字符数组 str 中的字符串输出到标准输出设备(通常是控制台),并在末尾换行。

1.2.2 使用 scanf 函数输入字符串

scanf 函数可以用于从标准输入设备读取字符串。同样使用 %s 格式说明符,但需要注意 scanf 遇到空白字符(空格、制表符、换行符等)就会停止读取。

#include <stdio.h>

int main() {
    char str[50];
    printf("请输入一个字符串:");
    scanf("%s", str);
    printf("你输入的字符串是:%s\n", str);
    return 0;
}

在这个例子中,scanf("%s", str) 从标准输入读取一个字符串,并将其存储到字符数组 str 中。由于 scanf 遇到空白字符就停止读取,所以如果输入包含空格的字符串,scanf 只会读取到空格之前的部分。

1.2.3 使用 getsputs 函数

gets 函数可以读取一行字符串,包括空格,直到遇到换行符为止。换行符会被丢弃,并且会自动在字符串末尾添加 '\0'。但是,gets 函数存在缓冲区溢出的风险,因为它不会检查目标数组的大小,因此在现代 C 编程中不推荐使用。

#include <stdio.h>

int main() {
    char str[50];
    printf("请输入一个字符串:");
    gets(str);
    printf("你输入的字符串是:");
    puts(str);
    return 0;
}

puts 函数用于输出一个字符串,并在末尾自动添加换行符。它的安全性比 printf 输出字符串略高一些,因为它不会像 printf 那样容易因为格式控制字符串错误而导致问题。

二、指针基础与字符串操作

2.1 指针的基本概念

指针是 C 语言中的一个重要概念,它是一个变量,其值是另一个变量的地址。通过指针,我们可以间接地访问和操作其他变量。

int num = 10;
int *ptr = &num; // ptr 是一个指向 num 的指针

在上述代码中,&num 表示取 num 的地址,并将其赋值给指针变量 ptr。通过 *ptr 可以访问 num 的值,这被称为指针的解引用操作。

printf("num 的值是:%d\n", *ptr); // 输出 10

2.2 指针与字符串操作的联系

在字符串操作中,指针发挥着至关重要的作用。例如,我们可以通过指针来遍历字符串。

#include <stdio.h>

int main() {
    char str[] = "Hello";
    char *ptr = str;
    while (*ptr != '\0') {
        printf("%c", *ptr);
        ptr++;
    }
    printf("\n");
    return 0;
}

在这个例子中,ptr 初始指向 str 的首字符。通过 while 循环,每次解引用 ptr 输出字符,并将 ptr 移动到下一个字符的位置,直到遇到字符串结束标志 '\0'

2.3 利用指针进行字符串复制

字符串复制是常见的字符串操作之一。我们可以使用指针来实现字符串的复制。

#include <stdio.h>

void strcpy_custom(char *dest, const char *src) {
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';
}

int main() {
    char src[] = "Hello, World!";
    char dest[20];
    strcpy_custom(dest, src);
    printf("复制后的字符串:%s\n", dest);
    return 0;
}

strcpy_custom 函数中,src 是源字符串指针,dest 是目标字符串指针。通过 while 循环,将 src 指向的字符逐个复制到 dest 中,直到遇到 src 的结束标志 '\0',然后在 dest 的末尾添加 '\0'。注意,这里 src 被声明为 const char *,表示源字符串不会被修改。

三、字符串处理函数中的指针应用

3.1 strlen 函数

strlen 函数用于计算字符串的长度,不包括字符串结束标志 '\0'。其实现通常使用指针来遍历字符串。

#include <stdio.h>

size_t strlen_custom(const char *str) {
    size_t len = 0;
    while (*str != '\0') {
        len++;
        str++;
    }
    return len;
}

int main() {
    char str[] = "Hello, World!";
    size_t length = strlen_custom(str);
    printf("字符串长度是:%zu\n", length);
    return 0;
}

strlen_custom 函数中,通过指针 str 遍历字符串,每遇到一个非 '\0' 的字符,长度计数器 len 就加 1,直到遇到 '\0',最后返回 len

3.2 strcat 函数

strcat 函数用于将一个字符串连接到另一个字符串的末尾。它也利用指针来实现。

#include <stdio.h>

void strcat_custom(char *dest, const char *src) {
    // 移动 dest 指针到其末尾
    while (*dest != '\0') {
        dest++;
    }
    // 将 src 字符串连接到 dest 末尾
    while (*src != '\0') {
        *dest = *src;
        dest++;
        src++;
    }
    *dest = '\0';
}

int main() {
    char dest[50] = "Hello, ";
    char src[] = "World!";
    strcat_custom(dest, src);
    printf("连接后的字符串:%s\n", dest);
    return 0;
}

strcat_custom 函数中,首先将 dest 指针移动到其末尾(即 '\0' 处),然后将 src 字符串逐个字符复制到 dest 末尾,最后在连接后的字符串末尾添加 '\0'

3.3 strcmp 函数

strcmp 函数用于比较两个字符串的大小。它通过指针逐个比较两个字符串的字符。

#include <stdio.h>

int strcmp_custom(const char *str1, const char *str2) {
    while (*str1 != '\0' && *str2 != '\0') {
        if (*str1 != *str2) {
            return *str1 - *str2;
        }
        str1++;
        str2++;
    }
    if (*str1 == '\0' && *str2 == '\0') {
        return 0;
    } else if (*str1 == '\0') {
        return -1;
    } else {
        return 1;
    }
}

int main() {
    char str1[] = "apple";
    char str2[] = "banana";
    int result = strcmp_custom(str1, str2);
    if (result < 0) {
        printf("str1 小于 str2\n");
    } else if (result > 0) {
        printf("str1 大于 str2\n");
    } else {
        printf("str1 等于 str2\n");
    }
    return 0;
}

strcmp_custom 函数中,通过指针 str1str2 逐个比较两个字符串的字符。如果遇到不相等的字符,返回两个字符的差值(*str1 - *str2),以此来判断两个字符串的大小关系。如果两个字符串完全相同,返回 0;如果 str1 先结束,返回 -1;如果 str2 先结束,返回 1。

四、动态内存分配与字符串

4.1 malloc 与字符串

在处理字符串时,有时我们需要动态分配内存来存储字符串,特别是当我们不知道字符串的长度在编译时就确定的情况下。malloc 函数用于在堆上分配指定大小的内存块。

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

int main() {
    char *str;
    int length;
    printf("请输入字符串长度:");
    scanf("%d", &length);
    str = (char *)malloc(length + 1); // +1 用于存储 '\0'
    if (str == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    printf("请输入字符串:");
    scanf("%s", str);
    printf("你输入的字符串是:%s\n", str);
    free(str); // 释放分配的内存
    return 0;
}

在这个例子中,首先通过 malloc 分配 length + 1 个字节的内存空间,其中 +1 是为了存储字符串结束标志 '\0'。如果 malloc 返回 NULL,表示内存分配失败。使用完字符串后,通过 free 函数释放分配的内存,以避免内存泄漏。

4.2 realloc 调整字符串内存大小

realloc 函数可以用于调整已经分配的内存块的大小。这在处理字符串时非常有用,例如当我们需要扩展字符串的容量时。

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

int main() {
    char *str = (char *)malloc(10);
    if (str == NULL) {
        printf("内存分配失败\n");
        return 1;
    }
    strcpy(str, "Hello");
    // 尝试扩展字符串
    char *new_str = (char *)realloc(str, 20);
    if (new_str != NULL) {
        str = new_str;
        strcpy(str + 5, ", World!");
        printf("扩展后的字符串:%s\n", str);
        free(str);
    } else {
        printf("内存重新分配失败\n");
        free(str);
        return 1;
    }
    return 0;
}

在上述代码中,首先通过 malloc 分配 10 个字节的内存并存储字符串 "Hello"。然后使用 realloc 尝试将内存大小扩展到 20 个字节。如果 realloc 成功,new_str 不为 NULL,将 str 指向 new_str,并在扩展后的内存空间中继续添加字符串内容。如果 realloc 失败,需要释放原来分配的内存。

五、字符串与指针的常见错误及避免方法

5.1 指针未初始化

在使用指针之前,必须确保它已经被初始化,否则会导致未定义行为。在处理字符串指针时同样如此。

char *str;
printf("%s\n", str); // 未初始化指针,导致未定义行为

为了避免这种错误,在使用指针之前,要给它分配一个有效的地址,例如:

char *str = "Hello, World!";
printf("%s\n", str);

5.2 缓冲区溢出

当向字符数组中存储字符串时,如果超出了数组的大小,就会发生缓冲区溢出。这是一个严重的安全漏洞。

char str[5];
strcpy(str, "Hello"); // 缓冲区溢出,"Hello" 需要 6 个字节(包括 '\0')

为了避免缓冲区溢出,在进行字符串复制等操作时,要确保目标数组有足够的空间,或者使用更安全的字符串处理函数,如 strncpy

char str[6];
strncpy(str, "Hello", sizeof(str) - 1);
str[sizeof(str) - 1] = '\0'; // 手动添加 '\0',防止截断的字符串没有结束标志

5.3 内存泄漏

在动态分配内存后,如果忘记释放内存,就会发生内存泄漏。这会导致程序占用的内存不断增加,最终可能耗尽系统资源。

char *str = (char *)malloc(100);
// 使用 str
// 忘记调用 free(str),导致内存泄漏

为了避免内存泄漏,在使用完动态分配的内存后,一定要调用 free 函数释放内存,并且要确保在程序的所有可能执行路径中都能正确释放内存。

六、字符串与指针在实际项目中的应用

6.1 文件操作中的字符串与指针

在文件操作中,经常需要读取和写入字符串。例如,从文件中读取一行内容并存储为字符串,或者将字符串写入文件。

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

int main() {
    FILE *file = fopen("test.txt", "r");
    if (file == NULL) {
        perror("打开文件失败");
        return 1;
    }
    char *line = NULL;
    size_t len = 0;
    ssize_t read;
    while ((read = getline(&line, &len, file)) != -1) {
        printf("读取到的行:%s", line);
    }
    free(line);
    fclose(file);
    return 0;
}

在上述代码中,getline 函数用于从文件 file 中读取一行内容,并将其存储在动态分配的字符串 line 中。getline 会自动调整 line 的大小以适应读取的内容。使用完 line 后,要记得调用 free 释放内存。

6.2 网络编程中的字符串与指针

在网络编程中,字符串和指针也有广泛的应用。例如,在套接字编程中,需要将数据以字符串的形式发送和接收。

#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 sock = 0, valread;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
        printf("\nConnection Failed \n");
        return -1;
    }
    char *hello = "Hello from client";
    send(sock, hello, strlen(hello), 0);
    printf("发送消息:%s\n", hello);
    valread = read(sock, buffer, BUFFER_SIZE);
    buffer[valread] = '\0';
    printf("接收消息:%s\n", buffer);
    close(sock);
    return 0;
}

在这个简单的客户端示例中,send 函数将字符串 hello 发送到服务器,read 函数从服务器接收数据并存储在字符数组 buffer 中。这里通过指针和字符串的操作实现了网络数据的传输。

6.3 字符串与指针在图形界面编程中的应用

在图形界面编程(如使用 GTK 等库)中,也会涉及到字符串与指针的操作。例如,设置窗口标题、按钮文本等都需要使用字符串。

#include <gtk/gtk.h>

static void on_button_clicked(GtkWidget *widget, gpointer user_data) {
    GtkWidget *label = GTK_WIDGET(user_data);
    gtk_label_set_text(GTK_LABEL(label), "按钮被点击了!");
}

int main(int argc, char *argv[]) {
    GtkWidget *window;
    GtkWidget *button;
    GtkWidget *label;
    gtk_init(&argc, &argv);
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "字符串与指针示例");
    gtk_container_set_border_width(GTK_CONTAINER(window), 10);
    label = gtk_label_new("欢迎!");
    button = gtk_button_new_with_label("点击我");
    g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(on_button_clicked), label);
    gtk_box_pack_start(GTK_BOX(gtk_window_get_child(GTK_WINDOW(window))), label, TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(gtk_window_get_child(GTK_WINDOW(window))), button, TRUE, TRUE, 0);
    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    gtk_widget_show_all(window);
    gtk_main();
    return 0;
}

在这个 GTK 示例中,gtk_window_set_title 函数设置窗口标题,gtk_button_new_with_label 函数设置按钮文本,gtk_label_set_text 函数修改标签的文本,这些都涉及到字符串的操作。通过指针传递数据和回调函数,实现了图形界面的交互逻辑。

通过以上对 C 语言字符串与指针操作的详细介绍,包括基础概念、操作方法、常见错误以及在实际项目中的应用,希望读者能够对这一重要的编程领域有更深入的理解和掌握,从而编写出更高效、安全的 C 程序。在实际编程中,要始终注意指针的正确使用和内存管理,以避免出现各种难以调试的错误。同时,多通过实际项目练习,将这些知识应用到实际场景中,不断提升自己的编程能力。