import{_ as s,c as i,o as a,a6 as n}from"./chunks/framework.CZRoMP2i.js";const t="/c/assets/1.L8V3GBrc.png",l="/c/assets/2.CdvhiwcU.png",e="/c/assets/3.D74t3-Xt.png",d="/c/assets/4.DqDR6Thp.svg",p="/c/assets/5.BzSkS-4w.svg",h="/c/assets/6.BPY9ZGed.svg",C=JSON.parse('{"title":"第一章:颇具争议的指针","description":"","frontmatter":{},"headers":[],"relativePath":"notes/01_c-basic/06_xdx/index.md","filePath":"notes/01_c-basic/06_xdx/index.md","lastUpdated":1723516792000}'),k={name:"notes/01_c-basic/06_xdx/index.md"},r=n('

IMPORTANT

第一章:颇具争议的指针

1.1 概述

NOTE

之所以指针在 C 语言中颇具争议,是因为一方面其功能强大,直接操作内存地址;另一方面,又很危险,不正确的使用指针的方式,非常容易导致程序崩溃。

IMPORTANT

1.2 现代化高级编程语言是如何解决指针危险的?

IMPORTANT

总而言之,各种编程语言通过引入不同的策略和机制,如:智能指针、垃圾回收器、所有权和借用,以及强类型系统,有效地减少了指针操作所带来的各种安全性和可靠性问题,提升了程序的稳定性和开发效率。

第二章:回顾知识

2.1 变量

c
数据类型 变量名 = 值 ;

IMPORTANT

变量名(标识符)需要符合命名规则和命名规范!!!

2.2 普通变量和指针变量的区别

img

2.3 运算符

2.3.1 概述

img

NOTE

掌握一个运算符,需要关注以下几个方面:

IMPORTANT

普通变量支持上述的所有运算符;而指针变量并非支持上述的所有运算符,且支持运算符的含义和普通变量相差较大!!!

2.3.2 运算符的优先级

优先级运算符名称或含义结合方向
1[]数组下标➡️(从左到右)
()圆括号
.成员选择(对象)
->成员选择(指针)
2-负号运算符⬅️(从右到左)
(类型)强制类型转换
++自增运算符
--自减运算符
*取值运算符
&取地址运算符
!逻辑非运算符
~按位取反运算符
sizeof长度运算符
3/➡️(从左到右)
*
%余数(取模)
4+➡️(从左到右)
-
5<<左移➡️(从左到右)
>>右移
6>大于➡️(从左到右)
>=大于等于
<小于
<=小于等于
7==等于➡️(从左到右)
!=不等于
8&按位与➡️(从左到右)
9^按位异或➡️(从左到右)
10|按位或➡️(从左到右)
11&&逻辑与➡️(从左到右)
12||逻辑或➡️(从左到右)
13?:条件运算符⬅️(从右到左)
14=赋值运算符⬅️(从右到左)
/=除后赋值
*=乘后赋值
%=取模后赋值
+=加后赋值
-=减后赋值
<<=左移后赋值
>>=右移后赋值
&=按位与后赋值
^=按位异或后赋值
|=按位或后赋值
15,逗号运算符➡️(从左到右)

WARNING

IMPORTANT

第三章:指针的理解和定义(⭐)

3.1 变量的访问方式

c
#include <stdio.h>

int main() {

    // 定义变量,即:开辟一块内存空间,并将初始化值存储进去
    int num = 10;

    // 访问变量,即:访问变量在内存中对应的数据
    printf("num = %d\\n", num);

    // 给变量赋值,即:给变量在内存中占据的内存空间存储数据
    num = 100;

    // 访问变量,即:访问变量在内存中对应的数据
    printf("num = %d\\n", num);

    return 0;
}

IMPORTANT

3.2 内存地址和指针

NOTE

有了内存地址,就能加快数据的存取速度,可以类比生活中的字典,即:

c
#include <stdio.h>

int main() {

    // 定义变量,即:开辟一块内存空间,并将初始化值存储进去
    int num = 10;

    return 0;
}

NOTE

通过内存地址找到所需要的存储单元,即:内存地址指向该存储单元。此时,就可以将内存地址形象化的描述为指针👉,那么:

总结:内存地址 = 指针。

NOTE

有的时候,为了方便阐述,我们会将指针变量称为指针。但是,需要记住的是:

下文中提及的指针都是指针变量,不再阐述!!!

IMPORTANT

如果你观察仔细的话,你可能会发现指针变量普通变量在内存中占据的存储空间是不一样的,那么到底是什么原因造成这样的结果?

3.3 指针变量的定义

3.4 指针的作用

在 Java 中,引用数据类型的向上类型转换(upcasting)和向下类型转换(downcasting)是面向对象编程中常见的操作。这些转换是 Java 继承体系和多态性的重要部分。我们先分别介绍向上类型转换和向下类型转换,然后讨论它们在 C 语言中指针的类似操作。

向上类型转换(Upcasting)

向上类型转换是将一个子类对象引用转换为父类对象引用。由于子类继承了父类的所有方法和属性,子类对象也包含父类对象的所有部分,因此这种转换是安全且隐式的。

例子:

java
class Animal {
    void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    void makeSound() {
        System.out.println("Bark");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        Animal animal = dog; // Upcasting, 隐式转换
        animal.makeSound();  // 输出:Bark
    }
}

在这个例子中,Dog 类型的对象 dog 被转换为 Animal 类型。尽管 animal 引用的实际对象是 Dog,但在编译时,animal 被视为 Animal 类型。

向下类型转换(Downcasting)

向下类型转换是将一个父类对象引用转换为子类对象引用。由于父类对象不一定具有子类的所有方法和属性,因此这种转换需要显式进行,并且在运行时进行类型检查(使用 instanceof 关键字来确保安全)。

例子:

java
public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog(); // Upcasting
        if (animal instanceof Dog) {
            Dog dog = (Dog) animal; // Downcasting, 显式转换
            dog.makeSound();        // 输出:Bark
        }
    }
}

在这个例子中,animal 引用的对象实际是 Dog 类型,在向下转换之前使用 instanceof 检查以确保安全。

区别

C 语言中的指针转换

在 C 语言中,指针的转换类似于引用类型的转换,但由于 C 语言没有继承和多态的概念,其转换更多是基于内存布局。

例子:

c
#include <stdio.h>

void printInt(void* ptr) {
    printf("%d\\n", *(int*)ptr);
}

int main() {
    int x = 10;
    void* voidPtr = &x; // Upcasting, 隐式转换
    printInt(voidPtr);  // 输出:10
    
    int* intPtr = (int*)voidPtr; // Downcasting, 显式转换
    printf("%d\\n", *intPtr);     // 输出:10
    return 0;
}

在这个例子中,void* 是通用指针类型,可以指向任何类型的数据。将 int* 转换为 void* 是隐式的,而将 void* 转换为 int* 是显式的。由于 C 没有类型检查,因此这种转换需要程序员自己确保安全。

总结:

第四章:指针的运算(⭐)

4.1 概述

4.2 总结

WARNING

在使用指针时,务必小心避免野指针和内存泄漏等问题。

在C语言中,同类指针相减的结果是一个整数,它表示两个指针之间相隔多少个指向的对象单位,而不是它们在内存中的字节偏移量。这种对象单位是指针所指向的具体类型的大小。

举个例子来说,如果你有两个指向整数数组元素的指针 pq,那么 p - q 的结果将是 p 指向的数组元素的索引与 q 指向的数组元素索引之间的差值。这个差值代表了在数组中相隔多少个整数元素,而不是它们在内存中的字节偏移量。

这种设计的优势在于,它使得指针运算更加直观和便于理解,特别是在处理数组和其他连续存储的数据结构时。因为指针运算结果的单位是根据指针所指向的具体类型来计算的,这样可以确保不同平台上的程序行为是一致的,不会受到底层硬件架构或者字节对齐规则的影响。

在C语言中,数组名和指针有很多相似之处,但数组名并不是指针变量。数组名实际是一个常量,它指向数组的第一个元素的地址。为了证明这一点,可以通过以下几个方面来说明:

  1. 数组名表示数组首地址: 数组名可以作为一个指针使用,数组名本身表示的是数组首地址。

    c
    int arr[5] = {1, 2, 3, 4, 5};
    int *ptr = arr; // 这句话是合法的,ptr现在指向arr[0]
    printf("%p\\n", arr);  // 打印数组名,会打印数组首地址
    printf("%p\\n", &arr[0]);  // 打印第一个元素的地址
  2. 数组名是常量指针: 数组名是一个常量指针,不能改变它指向的位置,而指针变量可以改变它指向的位置。

    c
    int arr[5];
    int *ptr = arr;  // 合法,ptr指向arr[0]
    ptr++;  // 合法,ptr现在指向arr[1]
    
    // arr++;  // 非法,编译错误,因为数组名是常量,不能改变
  3. sizeof运算符的结果不同: 使用sizeof运算符对数组名和指针变量会得到不同的结果,数组名会返回整个数组的大小,而指针变量会返回指针本身的大小。

    c
    int arr[5];
    int *ptr = arr;
    
    printf("sizeof(arr) = %lu\\n", sizeof(arr));  // 返回数组的大小,5 * sizeof(int)
    printf("sizeof(ptr) = %lu\\n", sizeof(ptr));  // 返回指针的大小,通常是4或8字节
  4. 地址运算符的结果不同: 使用地址运算符&对数组名和指针变量会得到不同的结果,对数组名使用&会返回数组的地址,而对指针变量使用&会返回指针变量本身的地址。

    c
    int arr[5];
    int *ptr = arr;
    
    printf("Address of array: %p\\n", &arr);  // 返回整个数组的地址
    printf("Address of pointer: %p\\n", &ptr);  // 返回指针变量ptr的地址

综上所述,通过这些示例和解释,可以看出数组名虽然在某些场合下可以像指针一样使用,但它并不是一个真正的指针变量,而是一个常量,表示数组的首地址。

`,87),c=[r];function o(g,E,u,b,y,F){return a(),i("div",null,c)}const A=s(k,[["render",o]]);export{C as __pageData,A as default};