C语言笔记

C语言关键字

CF4E23CD-2B95-4D54-A03B-CB827A549AD7.png

register

修饰变量存储在寄存器中,提升效率 但不能取地址。

restrict

  • 内存访问优化:编译器可以假设指针指向的内存区域不会被其他指针修改,从而优化内存访问。
  • 提高性能:通过减少不必要的内存访问和优化指令顺序,restrict 关键字可以显著提高程序的性能
    C语言中的负数 :取绝对值后按位取反 +1

指针

& 取地址 * 取值
数组名是数组第一个元素的地址

char*p;
p = a;
p = &a[0];等价

指针的运算:

当指针指向数组元素的时候,我们可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前或向后的第n个元素
数组名只是一个地址 指针是一个左值
在C语言中,左值(Lvalue)和右值(Rvalue)是表达式中的两种不同类型的值,它们在赋值操作中扮演不同的角色。
C语言中的指针是一个非常强大和灵活的特性,它允许程序直接操作内存地址。以下是对C语言指针的详细介绍以及各种使用场景的示例。

一、指针的基本概念

  • 指针变量的声明:指针变量用于存储内存地址。声明指针变量时需要使用 * 符号,例如:
    int *p; /* p是一个指向整数的指针 */
  • 指向一个变量
    • 使用 & 操作符获取变量的地址。
    • 例如:
    int a = 10; int *p = &a; /* p现在存储了变量a的地址 */

二、指针的基本使用

  • 访问指针指向的值:使用 * 操作符解引用指针,获取指针所指向的变量的值。
    int a = 10;
    int *p = &a;
    printf("a的值是:%d\n", *p); /* 输出a的值 */

三、指针和数组

  • 指针和数组的关系:数组名本身就是一个指针,指向数组的第一个元素。
    int arr[] = {1, 2, 3, 4, 5};
    int *p = arr; /* p指向数组的第一个元素 */
    printf("arr[0]的值是:%d\n", *p); /* 输出1 */
   **遍历数组**:可以通过指针来遍历数组。
    int arr[] = {1, 2, 3, 4, 5};
    int *p;
    for (p = arr; p < arr + 5; p++) {
    printf("%d\n", *p);
   }

四、指针和字符串

  • C语言中的字符串:C语言中的字符串实际上是一个字符数组,以 \0 结尾。
  • 字符串操作:可以使用指针来操作字符串。
    char str[] = "Hello";
    char *p = str;
    printf("字符串的内容是:%s\n", p); /* 输出Hello */

五、指针和函数

  • 函数参数传递:可以将指针作为函数参数传递,从而实现对变量的间接访问和修改。
  void swap(int *a, int *b) 
  {
    int temp = *a;
    *a = *b;
    *b = temp;
   }

   int main() {
   int x = 10, y = 20;
   swap(&x, &y);
   printf("x = %d, y = %d\n", x, y); /* 输出x = 20, y = 10 */
   return 0;
   }

六、指针和动态内存分配

  • 动态内存分配:可以使用 malloc、calloc 和 realloc 函数动态分配内存。
    int *p = (int *)malloc(5 * sizeof(int)); /* 分配5个整数的空间 */
    if (p != NULL) {
    for (int i = 0; i < 5; i++)
    {
        p[i] = i + 1;
    }
    for (int i = 0; i < 5; i++) 
    {
        printf("%d\n", p[i]);
    }
    free(p); /* 释放内存 */
   }

七、多级指针

  • 多级指针:指针可以指向另一个指针。
    int a = 10;
    int *p = &a;
    int **q = &p;
    printf("a的值是:%d\n", **q); /* 输出10 */

八、指针和结构体

  • 结构体指针:可以使用指针来访问结构体的成员。
    struct Person {
    char name[50];
    int age;
    };
    int main() {
    struct Person *p;
    p = (struct Person *)malloc(sizeof(struct Person));
    strcpy(p->name, "Alice");
    p->age = 25;
    printf("Name: %s, Age: %d\n", p->name, p->age);
    free(p);
    return 0;
    }

九、指针和函数指针

  • 函数指针:可以将函数地址赋值给指针变量,通过指针调用函数。
    int add(int a, int b) {
    return a + b;
    }
    int main() {
    int (*func)(int, int) = add;
    printf("1 + 2 = %d\n", func(1, 2)); /* 输出3 */
    return 0;
    }

十、指针和数组多维数组

  • 多维数组的指针:可以使用指针来访问多维数组。
    int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int (*p)[3] = arr; /* p是一个指向包含3个整数的数组的指针 */
    printf("%d\n", p[0][1]); /* 输出2 */

十一、指针和NULL

  • 空指针:NULL 是一个特殊的指针值,表示指针不指向任何有效的内存地址。
    int *p = NULL; /* p是一个空指针 */
    if (p == NULL) {
    printf("指针p没有指向任何有效的内存地址。\n");
    }

十二、指针和常量

  • 常量指针:指针本身是常量,不能修改它所指向的地址。
    const int *p = &a; /* p是一个指向常量整数的指针 */
    *p = 20; /* 错误:不能修改常量 */
  • 指针常量:指针指向的地址是常量,不能修改指针所指向的值。
    int *const p = &a; /* p是一个常量指针 */
    p = &b; /* 错误:不能修改常量指针 */

十三、指针和递归

  • 递归中的指针:指针可以用于递归算法中,例如递归遍历链表。
    struct Node {
    int data;
    struct Node *next;
    };

    void printList(struct Node *head) {
    if (head == NULL) {
    return;
    }
    printf("%d\n", head->data);
    printList(head->next);
    }

十四、指针和内存泄漏

  • 内存泄漏:如果动态分配的内存没有被释放,就会导致内存泄漏。
    int *p = (int *)malloc(sizeof(int));
    p = (int *)malloc(sizeof(int)); /* 前一个分配的内存没有被释放,导致内存泄漏 */
    free(p); /* 只释放了最后一次分配的内存 */

十五、指针和内存越界

  • 内存越界:访问超出数组或指针范围的内存会导致未定义行为。
    int arr[5];
    arr[5] = 10; /* 内存越界 */

十六、指针和联合体

  • 联合体指针:可以使用指针来访问联合体的成员。
    union Data {
    int i;
    float f;
    char str[20];
    };

    int main() {
    union Data *data;
    data = (union Data *)malloc(sizeof(union Data));
    data->i = 10;
    printf("data->i = %d\n", data->i);
    free(data);
    return 0;
    }

十七、指针和类型转换

  • 类型转换:可以使用 (type *) 进行指针类型转换。
    int a = 10;
    void *p = &a; /* p是一个void指针 */
    int *q = (int *)p; /* 类型转换 */
    printf("a的值是:%d\n", *q);

十八、指针和数组指针

  • 数组指针:指针可以指向整个数组。
    int arr[5];
    int (*p)[5] = &arr; /* p是一个指向包含5个整数的数组的指针 */
    printf("%d\n", (*p)[2]); /* 输出arr[2] */

十九、指针和位运算

  • 位运算和指针:指针不直接涉及位运算,但可以通过指针和整数之间的转换来操作位。
    int a = 0x1234;
    unsigned char *p = (unsigned char *)&a;
    printf("0x%02X\n", *p); /* 输出低位 */

二十、指针和模块化编程

  • 模块化编程中的指针:指针可以用于实现模块化编程,例如通过指针传递函数回调。
    void process(int (*func)(int)) {
    printf("Result: %d\n", func(5));
    }

    int square(int x) {
    return x * x;
    }

    int main() {
    process(square); /* 输出Result: 25 */
    return 0;
    }

二十一、指针和互斥操作

  • 多线程中的指针:在多线程环境中,指针需要小心使用,以避免竞争条件和数据不一致。
    #include <pthread.h>

    int count = 0;
    pthread_mutex_t mutex;
    void *increment(void *arg) {
    pthread_mutex_lock(&mutex);
    count++;
    pthread_mutex_unlock(&mutex);
    return NULL;
    }

    int main() {
    pthread_t threads[10];
    pthread_mutex_init(&mutex, NULL);
    for (int i = 0; i < 10; i++) {
    pthread_create(&threads[i], NULL, increment, NULL);
    }

    for (int i = 0; i < 10; i++) {
    pthread_join(threads[i], NULL);
    }
    printf("Count: %d\n", count); /* 输出正确值 */
    pthread_mutex_destroy(&mutex);
    return 0;
    }

总结

C语言中的指针是一个非常强大和灵活的工具,它允许直接操作内存,这在许多场景中都非常有用。然而,指针的使用也需要非常小心,以避免内存泄漏、内存越界和未定义行为等问题。通过本文介绍的这些示例和使用方法,您可以更好地理解和掌握C语言中的指针。

1、常量指针

常量指针的概念是:指针所指空间的值不能发生改变,不能通过指针解引用修改指针所指向空间的值,但是指针的指向是可以改的

2、指针常量

指针常量的概念是:指针本身就是一个常量,即指针的指向不能发生改变,但是指针所指向空间的值是可以发生改变的,可以通过解引用改变指针所指向空间的值

具体怎么更好的理解呢?
大家先看常量指针这个名字,以指针结尾,所以是具有指针的性质的,因此可以改变指向,又因为是常量指针,所以指向的值是常量的,即是不能改变的
再看指针常量这个名字,以常量结尾,应该具有常量的一些性质,所以自然不能改变指针的指向,只能修改指针所指向的值啦
那么const究竟加到代码的哪个位置才叫常量指针,加到哪个位置才叫指针常量呢?
说一个小窍门可以很快的帮助大家记住:
const如果在的左边,那就是常量指针
const如果在
的右边,那就是指针常量
至于为什么要这样记呢,可以思考一下,在指针中有解引用的作用,那么const在左边,可以理解为const修饰*,即不能修改指针所指向空间的值,但是指针的指向确是没有限制的

那么const在*右边,修饰的就是指针变量啦,即指针变量不能改变它的指向,但是指针所指向空间的值不受限制
常量指针:const int *pa = &a;、int const *pa = &a;
指针常量:int *const pa = &a;


152E46AA-E7A2-493D-BE8C-40531B29B648.png

52C7D085-A521-4E47-83F1-FE8C7F033885.png

91633C87-389C-4687-9E0F-88094B5E8493.png

6EEAE37C-C1E1-4566-9819-A3CFE6B76925.png

65187CC1-8598-4AC9-9AEC-6456B0BF5616.png

9FB075F2-9D97-440E-A94A-F2147AF8A15B.png

D72EA366-13CB-4786-B817-44A71BF228CA.png

D7BD1016-78C7-4904-BE4F-A64DCEF8BE41.png

2BF27510-49DB-45DD-9BE0-B354B7B30474.png

7E5319C7-43DA-4F39-A903-5A10E6BDB9DE.png

8FBB400E-1DC2-41E7-8754-19B970FC7483.png

F3F7F1E7-E4D7-4012-8B6E-95C6D9EB07D7.png

左值(Lvalue)

左值是指那些可以出现在赋值操作符左边的表达式,通常表示一个具有持久存储的对象。左值的主要特征包括:

  • 可寻址:左值可以获取其内存地址。
  • 可修改:左值可以被重新赋值。
  • 示例:变量名、数组元素、解除引用的指针等。

右值(Rvalue)

右值则是指那些没有持久存储的对象,通常表示临时计算的结果。右值的主要特征包括:

  • 不可寻址:右值不能获取其内存地址。
  • 临时性:右值通常在表达式求值后被销毁。
  • 示例:字面量(如整数常量42)、临时对象(如a + b的结果)。

左值和右值的区别

  • 存储位置:左值有持久存储,而右值没有持久存储。
  • 赋值操作:左值可以出现在赋值操作的左边,而右值只能出现在右边。
  • 生命周期:左值的生命周期较长,可以在表达式结束后依然存在;而右值的生命周期较短,通常在表达式结束后被销毁。
    指针和数组的区别
    数据名只是一个地址,而指针是一个左值
    sizeof(数组) 返回数组总大小,sizeof(指针) 返回指针大小(如 4/8 字节)。

动态内存分配函数有哪些?

  • malloc(size):分配未初始化的内存。
  • calloc(num, size):分配并初始化为 0 的内存。
  • realloc(ptr, new_size):调整已分配内存的大小。

反转链表:

struct Node {
    int data;
    struct Node* next;
};

struct Node* reverse_list(struct Node* head) {
    struct Node *prev = NULL, *current = head, *next = NULL;
    while (current != NULL) {
        next = current->next;
        current->next = prev;
        prev = current;
        current = next;
    }
    return prev;
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容