import{_ as s,c as i,o as a,a6 as n}from"./chunks/framework.hMCIpNYY.js";const l="/c/assets/4.DqDR6Thp.svg",p="/c/assets/5.BzSkS-4w.svg",e="/c/assets/6.BPY9ZGed.svg",b=JSON.parse('{"title":"第一章:颇具争议的指针","description":"","frontmatter":{},"headers":[],"relativePath":"notes/01_c-basic/07_xdx/index.md","filePath":"notes/01_c-basic/07_xdx/index.md","lastUpdated":1724809134000}'),t={name:"notes/01_c-basic/07_xdx/index.md"},h=n(`
IMPORTANT
指针
是 C 语言中最重要
的概念之一,也是最难以理解
的概念之一。指针
是 C 语言的精髓
,要想掌握 C 语言就需要深入地了解指针。强大
和灵活
的工具;但是,需要开发者小心谨慎的使用
,以确保程序的稳定性和安全性。NOTE
之所以指针在 C 语言中颇具争议,是因为一方面其功能强大,直接操作内存地址;另一方面,又很危险,不正确的使用指针,非常容易导致程序崩溃。
如果没有能很好的使用指针,就会带来一系列的问题,如:
空指针引用
(Null Pointer Dereference):当一个指针没有正确初始化或者被赋予了空(NULL)值时,如果程序尝试访问该指针所指向的内存,会导致运行时错误,甚至导致程序崩溃。野指针
(Dangling Pointers):指针指向的内存地址曾经分配给某个变量或对象,但后来该变量或对象被释放或者移动,导致指针仍指向已经无效的内存位置。对野指针进行操作可能会导致未定义的行为或程序崩溃。指针算术错误
:在进行指针运算时,如果没有正确管理指针的偏移量或者超出了数组的边界,可能会导致指针指向错误的内存位置,从而影响程序的正确性和安全性。内存泄漏
:如果动态分配的内存通过指针分配,但在不再需要时没有正确释放,会导致内存泄漏,长时间运行的程序可能会耗尽系统资源。为了减少指针带来的风险,开发人员可以采取以下的措施:
良好的编程实践
:确保指针的初始化和使用是安全的,避免空指针引用和野指针问题。边界检查
:在进行指针运算时,始终确保不会超出数组或内存分配的边界。使用指针和引用的适当性
:在可能的情况下,可以考虑使用更安全的语言特性,如:引用(在 C++ 等编程语言中)或者更高级别的数据结构来代替裸指针,从而减少指针使用时的潜在风险。IMPORTANT
C++
采用了如下的策略和机制,来解决指针危险操作的:
智能指针
: C++ 引入了智能指针(如std::shared_ptr
、std::unique_ptr
),这些指针提供了自动资源管理和所有权的语义。std::unique_ptr
确保只有一个指针可以访问给定的资源,从而避免了传统指针的悬空引用和内存泄漏问题。std::shared_ptr
允许多个指针共享一个资源,并在所有引用释放后自动释放。引用
: C++ 中的引用(如:&
符号)提供了更安全的间接访问方法,与指针相比,引用不能重新绑定到不同的对象,从而减少了意外的指针错误。Go
采用了如下的策略和机制,来解决指针危险操作的:
内存管理和垃圾回收
: Go 语言通过自动垃圾回收器管理内存,减少了手动内存管理所带来的指针操作错误。Go 的垃圾回收器定期扫描并释放不再使用的内存,避免了内存泄漏和悬空指针问题。指针的安全性
: Go 语言的指针是受限的,不支持指针运算,从而减少了指针操作可能带来的风险。Rust
采用了如下的策略和机制,来解决指针危险操作的:
所有权和借用
: Rust 引入了所有权和借用的概念,编译器在编译时静态分析所有权转移和引用的生命周期。这种机制避免了数据竞争和空指针解引用等运行时错误,使得在编译时就能够保证内存安全。生命周期
: Rust 的生命周期系统确保引用的有效性和安全性,防止了悬空引用和指针乱用。Java
采用了如下的策略和机制,来解决指针危险操作的:
引用类型和自动内存管理
: Java 中所有的对象引用都是通过引用来访问的,而不是直接的指针。Java 的自动垃圾回收器负责管理内存,从而避免了手动内存管理可能导致的指针错误,如:内存泄漏和悬空指针。强类型系统和异常处理
: Java 的强类型系统和异常处理机制减少了指针操作带来的风险,如:空指针解引用异常(NullPointerException)。编译器在编译时能够捕获许多潜在的类型错误,进一步增强了程序的安全性和可靠性。IMPORTANT
变量名(普通变量)
访问内存中存储的数据,如下所示:#include <stdio.h>
int main() {
// 定义变量,即:开辟一块内存空间,并将初始化值存储进去
int num = 10;
// 访问变量,即:访问变量在内存中对应的数据
printf("num = %d\\n", num);
// 给变量赋值,即:给变量在内存中占据的内存空间存储数据
num = 100;
// 访问变量,即:访问变量在内存中对应的数据
printf("num = %d\\n", num);
return 0;
}
直接访问
;当然,既然有直接访问
的方式,必然有间接访问
的方式,如:指针
。IMPORTANT
变量名(普通变量)
访问内存中变量存储的数据,之所以称为直接访问
的方式,是因为对于我们写程序而言,我们无需关心如何根据内存地址去获取内存中对应的数据,也无需关系如何根据内存地址将数据存储到对应的内存空间,这些操作步骤都是编译器
帮助我们在底层自动完成的(自动化)。内存地址
去操作内存中对应的数据(手动化),这种方式就称为间接访问
的方式了,相对于直接访问
方式来说,要理解
的概念
和操作
的步骤
和之前直接访问
的方式相比,要复杂和麻烦很多,但是效率高。内存地址
的概念了,即:操作系统为了更快的去管理内存中的数据,会将内存条
按照字节
划分为一个个的单元格
,并为每个独立的小的单元格
,分配唯一的编号
,即:内存地址
,如下所示:NOTE
有了内存地址,就能加快数据的存取速度,可以类比生活中的字典
,即:
#include <stdio.h>
int main() {
// 定义变量,即:开辟一块内存空间,并将初始化值存储进去
int num = 10;
return 0;
}
变量名(普通变量)
直接操作内存中的存储单元;但是,编译器底层还是会通过内存地址
来找到所需要的存储单元,如下所示:NOTE
通过内存地址
找到所需要的存储单元
,即:内存地址指向该存储单元。此时,就可以将内存地址
形象化的描述为指针👉
,那么:
变量
:命名的内存空间,用于存放各种类型的数据。变量名
:变量名是给内存空间取一个容易记忆的名字,方便我们编写程序。变量值
:变量所对应的内存中的存储单元中存放的数据值。变量的地址
:变量所对应的内存中的存储单元的内存地址(首地址),也可以称为指针
。总结:内存地址 = 指针。
普通变量
所对应的内存空间存储
的是普通的值
,如:整数、小数、字符等;指针变量
所对应的内存空间存储
的是另外一个变量的地址(指针)
,如下所示:NOTE
有的时候,为了方便阐述,我们会将指针变量
称为指针
。但是,需要记住的是:
下文中提及的指针
都是指针变量
,不再阐述!!!
IMPORTANT
如果你观察仔细的话,你可能会发现指针变量
和普通变量
在内存中占据的存储空间是不一样的,那么到底是什么原因造成这样的结果?
在 Java 中,引用数据类型的向上类型转换(upcasting)和向下类型转换(downcasting)是面向对象编程中常见的操作。这些转换是 Java 继承体系和多态性的重要部分。我们先分别介绍向上类型转换和向下类型转换,然后讨论它们在 C 语言中指针的类似操作。
向上类型转换是将一个子类对象引用转换为父类对象引用。由于子类继承了父类的所有方法和属性,子类对象也包含父类对象的所有部分,因此这种转换是安全且隐式的。
例子:
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
类型。
向下类型转换是将一个父类对象引用转换为子类对象引用。由于父类对象不一定具有子类的所有方法和属性,因此这种转换需要显式进行,并且在运行时进行类型检查(使用 instanceof
关键字来确保安全)。
例子:
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 语言没有继承和多态的概念,其转换更多是基于内存布局。
例子:
#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 没有类型检查,因此这种转换需要程序员自己确保安全。
总结:
在 C 语言中,普通变量
是直接存储数据
的变量
。对于普通变量,支持的操作包括:
int a = 5
。a + b
、a - b
、a * b
、a / b
a > b
、a == b
。a && b
、a || b
、!a
。a & b
、a | b
、a ^ b
、~a
、a << 2
、a >> 2
。a++
、--a
等。在 C 语言中,指针变量
存储的是另一个变量
的地址
。对于指针变量,支持的操作包括:
int *p = &a;
。*p = 10;
修改指向变量的值。p + 1
。p++
、p--
。ptr1 - ptr2
。p1 == p2
、p1 != p2
。*(p + i)
访问第 i
个元素。int **pp = &p;
。WARNING
在使用指针时,务必小心避免野指针和内存泄漏等问题。
在C语言中,同类指针相减的结果是一个整数,它表示两个指针之间相隔多少个指向的对象单位,而不是它们在内存中的字节偏移量。这种对象单位是指针所指向的具体类型的大小。
举个例子来说,如果你有两个指向整数数组元素的指针 p
和 q
,那么 p - q
的结果将是 p
指向的数组元素的索引与 q
指向的数组元素索引之间的差值。这个差值代表了在数组中相隔多少个整数元素,而不是它们在内存中的字节偏移量。
这种设计的优势在于,它使得指针运算更加直观和便于理解,特别是在处理数组和其他连续存储的数据结构时。因为指针运算结果的单位是根据指针所指向的具体类型来计算的,这样可以确保不同平台上的程序行为是一致的,不会受到底层硬件架构或者字节对齐规则的影响。
在C语言中,数组名和指针有很多相似之处,但数组名并不是指针变量。数组名实际是一个常量,它指向数组的第一个元素的地址。为了证明这一点,可以通过以下几个方面来说明:
数组名表示数组首地址: 数组名可以作为一个指针使用,数组名本身表示的是数组首地址。
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // 这句话是合法的,ptr现在指向arr[0]
printf("%p\\n", arr); // 打印数组名,会打印数组首地址
printf("%p\\n", &arr[0]); // 打印第一个元素的地址
数组名是常量指针: 数组名是一个常量指针,不能改变它指向的位置,而指针变量可以改变它指向的位置。
int arr[5];
int *ptr = arr; // 合法,ptr指向arr[0]
ptr++; // 合法,ptr现在指向arr[1]
// arr++; // 非法,编译错误,因为数组名是常量,不能改变
sizeof运算符的结果不同: 使用sizeof
运算符对数组名和指针变量会得到不同的结果,数组名会返回整个数组的大小,而指针变量会返回指针本身的大小。
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字节
地址运算符的结果不同: 使用地址运算符&
对数组名和指针变量会得到不同的结果,对数组名使用&
会返回数组的地址,而对指针变量使用&
会返回指针变量本身的地址。
int arr[5];
int *ptr = arr;
printf("Address of array: %p\\n", &arr); // 返回整个数组的地址
printf("Address of pointer: %p\\n", &ptr); // 返回指针变量ptr的地址
综上所述,通过这些示例和解释,可以看出数组名虽然在某些场合下可以像指针一样使用,但它并不是一个真正的指针变量,而是一个常量,表示数组的首地址。
`,63),k=[h];function d(r,c,o,E,g,u){return a(),i("div",null,k)}const F=s(t,[["render",d]]);export{b as __pageData,F as default};