前言
在计算机编程的广阔天地中,指针作为一种独特的数据类型,它不仅是C语言的核心,也是理解计算机内存管理的基石。指针的概念虽然强大,但对于初学者来说,它常常是学习过程中的一个难点。本文旨在揭开指针的神秘面纱,带你一探究竟,从基础概念到高级应用,全面解析指针的奥秘。
指针:连接现实与抽象的桥梁
在现实世界中,地址是定位位置的一种方式。同样,在计算机的虚拟世界里,内存地址是定位数据的一种手段。指针,作为内存地址的直接表达,它允许程序员直接操作内存中的数据。这种能力是双刃剑,它既提供了无与伦比的灵活性和控制力,也带来了额外的复杂性和潜在的错误风险。
探索内存的深层结构
要深入理解指针,首先需要了解计算机内存的工作原理。内存被组织成一系列的单元,每个单元都有一个唯一的地址。指针存储的就是这些地址,通过指针,我们可以访问、读取和修改这些内存单元中的数据。这种直接与内存交互的能力,是理解计算机系统的关键。
指针的多面性
指针不仅仅是一个简单的地址,它还包含了类型信息,这决定了指针可以访问的数据的大小和排列方式。不同类型的指针,如
int*
、char*
、void*
等,它们在内存中的操作方式各不相同。了解这些差异,对于编写高效、安全代码至关重要。
指针与数组:内存连续性的体现
数组是内存连续性的一个完美体现,而指针则是操作数组的有力工具。通过指针,我们可以轻松遍历数组,访问任意元素。这种能力在处理大型数据集时显得尤为重要,它使得算法的实现更加简洁和高效。
指针的高级应用:函数和结构体
指针的应用不仅限于基本数据类型,它还可以指向函数、结构体等复杂数据结构。这种高级应用使得程序的模块化和功能扩展成为可能,同时也带来了编程的灵活性和强大能力。
安全与风险:指针的双刃剑
指针的强大能力伴随着风险。野指针、内存泄漏、越界访问等问题都与指针使用不当有关。因此,了解如何安全地使用指针,避免潜在的内存问题,对于每个程序员来说都是必备的技能。
我们将一起探索指针的这些奥秘,从基础的内存和地址概念,到指针的高级应用,我们将一一揭开。希望通过几篇文章,能够对指针有一个全面而深入的理解,在编程的道路上更加自信和从容。让我们一起启程,深入探索内存的秘密,揭开指针的奥秘。
1 内存和地址
1.1 计算机中的房间编号系统
在深入探讨指针之前,让我们先通过一个生活中的案例来理解内存和地址的概念。这个案例将帮助我们建立一个直观的认识,从而更好地理解计算机是如何管理内存的。
1.1.1 宿舍楼的比喻:房间编号系统
想象一下,如果你被放置在一栋有100个房间的宿舍楼中,而这些房间并没有编号。这时,如果一个朋友来找你玩,他需要逐个房间敲门,直到找到你为止。这种方法显然效率极低。为了提高效率,我们给每个房间分配一个唯一的编号,比如一楼的房间编号为101、102、103,二楼的房间编号为201、202、203,以此类推。有了这些编号,你的朋友只需根据房间号直接找到你,大大节省了时间。
1.1.2 计算机内存的管理
在计算机中,内存的管理与宿舍楼的房间编号系统有着异曲同工之妙。CPU在处理数据时,需要从内存中读取数据,处理完毕后再将数据写回内存。我们购买电脑时,会看到内存的大小如8GB、16GB、32GB等,这些内存空间需要被高效地管理。
为了实现这一点,计算机将内存划分为一个个内存单元,每个单元的大小为1个字节。在计算机中,数据的大小通常以字节为单位,而1个字节等于8个比特位。比特位是计算机中最小的数据单位,可以存储一个二进制位,即1或0。
1.1.3 计算机中的单位换算
在计算机科学中,我们经常会遇到各种数据单位,它们之间的换算关系如下:
- 1 byte(字节)= 8 bit(比特位)
- 1 KB(千字节)= 1024 byte
- 1 MB(兆字节)= 1024 KB
- 1 GB(吉字节)= 1024 MB
- 1 TB(太字节)= 1024 GB
- 1 PB(拍字节)= 1024 TB
这些单位帮助我们量化数据的大小,同时也反映了计算机内存的层次结构。
1.1.4 内存单元的编号:地址
每个内存单元都有一个唯一的编号,这个编号就像是宿舍房间的门牌号。在计算机中,我们称这个编号为地址。地址是内存单元的标识符,它允许CPU快速定位到特定的内存空间。
1.1.5 指针:C语言中的地址表示
在C语言中,我们用“指针”来表示内存单元的地址。指针是存储内存地址的变量,它指向内存中的某个位置。通过指针,我们可以访问、读取和修改该位置的数据。因此,我们可以将内存单元的编号、地址和指针视为同一概念:
内存单元的编号=地址=指针
理解了这些基本概念后,我们就可以开始探索指针的更多奥秘,包括它们在编程中的应用、指针运算以及如何通过指针来管理复杂的数据结构。指针不仅是C语言的核心,也是连接程序与硬件的桥梁,理解指针就是理解计算机如何工作的关键一步。
1.2 理解内存编址:计算机的地址系统
在计算机的世界里,内存是一个巨大的数据存储空间,它由无数个字节组成。要精确地访问这些字节中的任何一个,我们需要一个系统来标识每个字节的位置,这就是内存编址的概念。
1.2.1 为什么需要编址?
想象一下,如果一栋宿舍楼有成千上万个房间,但没有房间号,那么当你的朋友来访时,他可能需要逐个敲门来找到你,这显然非常低效。同样,CPU需要能够快速准确地找到内存中的特定数据。因此,就像宿舍楼里的每个房间都有唯一的编号一样,内存中的每个字节也需要一个唯一的地址。
1.2.2 硬件层面的编址设计
在计算机中,编址并不是简单地记录每个字节的地址,而是通过硬件设计来实现的。这就像乐器上的琴弦,虽然没有标记“都瑞咪发嗦啦”,但演奏者知道每个琴弦的位置,因为这是乐器设计的一部分,是一种共识。
1.2.3 硬件单元的协同工作
计算机内部有许多硬件单元,它们需要协同工作,进行数据传递。这些硬件单元之间是独立的,它们通过“线”连接起来,以便通信。CPU和内存之间也有大量的数据交互,它们通过地址总线连接。
1.2.4 地址总线的作用
在32位计算机中,有32根地址总线,每根线可以表示两种状态(0或1),即电脉冲的存在与否。这样,一根线可以表示两种含义,两根线可以表示四种含义,以此类推。32根地址线可以表示 (2^{32})种不同的状态,每一种状态都对应一个唯一的地址。
当地址信息通过地址总线传递给内存时,内存就能定位到对应的数据,然后通过数据总线将数据传输到CPU的寄存器中。
1.2.5 总结
内存编址是计算机能够高效运行的基础。它使得CPU能够快速准确地访问内存中的任何数据,就像我们能够通过房间号快速找到宿舍楼中的朋友一样。这种编址系统是硬件设计的一部分,是计算机内部通信和数据传递的基础。通过理解内存编址,我们可以更好地理解计算机的工作原理,以及如何有效地与硬件交互。
2. 指针变量和地址
2.1 掌握取地址操作符(&)
在C语言的世界里,变量不仅仅是存储数据的容器,它们还拥有一个特殊的属性——地址。每个变量在内存中都有自己的位置,这个位置可以通过取地址操作符(&)来获取。
2.1.1 变量和内存空间
当我们在C语言中创建一个变量时,我们实际上是在向内存申请空间。例如,当我们声明一个整型变量`a`并给它赋值为10时,我们实际上是在内存中申请了4个字节的空间来存储这个整数。这4个字节在内存中是连续的,并且每个字节都有自己的地址。
2.1.2 取地址操作符(&)
要获取变量的地址,我们使用取地址操作符(&)。这个操作符非常简单,只需要在变量名前加上`&`即可。例如,`&a`就会给出变量`a`的地址。
让我们来看一个简单的例子
#include <stdio.h>
int main() {int a = 10;printf("%p\n", &a); // 打印变量a的地址return 0;
}
在这个例子中,&a取出的是变量a所占4个字节中地址最小的那个字节的地址。在大多数情况下,这个地址就是变量a的起始地址。
2.1.3 为什么需要地址?
即使我们知道了变量占用的第一个字节的地址,我们也可以顺藤摸瓜地访问到整个变量的数据。这是因为变量在内存中是连续存储的,所以只要我们知道了起始地址,就可以通过偏移量来访问后续的字节。
2.1.4 总结
通过取地址操作符(&),我们可以获取变量在内存中的地址。这个地址是变量与内存交互的桥梁,也是我们使用指针进行高级操作的基础。理解了变量的地址,我们就能够更深入地理解程序是如何在内存中管理和操作数据的。这种能力是编写高效、灵活的C语言程序的关键。
2.2 指针变量和解引用操作符(*):深入指针的内部
在计算机编程中,指针变量是一种特殊的变量,它存储的是其他变量的内存地址。这使得指针变量成为操作内存的强大工具。让我们进一步探索指针变量的概念和如何使用它们。
2.2.1 指针变量的基本概念
当我们使用取地址操作符(&)获取一个变量的地址时,我们得到的是一个数值,比如`0x006FFD70`。这个数值代表的是内存中的一个具体位置。有时候,我们需要将这个地址保存起来,以便将来使用。这时,我们就需要一个指针变量来存储这个地址。
#include <stdio.h>
int main() {int a = 10;int* pa = &a; // 取出a的地址并存储到指针变量pa中return 0;
}
在上面的例子中,pa是一个指针变量,它的类型是int*,表示它存储的是整型变量的地址。pa指向的内存地址正好是变量a的地址。
2.2.2 理解指针的类型
指针变量的类型非常重要,因为它决定了指针可以指向的数据类型,以及如何通过指针来访问和操作数据。当我们声明一个指针变量时,类型声明告诉编译器指针应该指向什么类型的数据。
例如:
int a = 10;
int* pa = &a; // pa是指向整型数据的指针
在这里,pa左边的int*表示pa是一个指针变量,而*符号前的int表示pa指向的是整型(int)数据。这意味着,当我们通过pa来访问数据时,编译器会知道应该读取一个整型的数据。
2.2.3 指针类型的应用
如果我们有一个char类型的变量ch,我们想要获取它的地址并存储在一个指针变量中,我们应该使用什么样的指针类型呢?
char ch = 'w';
char* pc = &ch; // pc是指向字符型数据的指针
在这个例子中,pc被声明为char*类型,这意味着pc是一个指向字符型数据的指针。这样,我们就可以通过pc来访问和操作ch变量中的字符数据。
2.2.4 解引⽤操作符
#include <stdio.h>int main()
{int a = 100;int* pa = &a;*pa = 0;return 0;
}
2.2.5 总结
指针变量和它们的类型是C语言中非常强大的特性。它们允许我们直接操作内存地址,提供了一种灵活的方式来访问和修改数据。理解指针变量的类型和如何使用它们是编写高效C程序的关键。通过指针,我们可以深入到内存的层面,实现更复杂的数据操作和程序控制。
2.3 指针变量的大小:探秘32位与64位系统的差异
在深入理解指针变量之前,我们需要知道它们在内存中所占的空间大小。这个大小并不是随意的,而是由我们使用的计算机系统的架构决定的。
2.3.1 指针变量大小的决定因素
在32位系统中,CPU有32根地址总线,每根总线可以表示一个0或1的二进制位。这意味着,32位系统可以生成一个32位的地址,这个地址由2的32次方个不同的状态组成,足以覆盖4GB的内存空间。因此,一个地址需要4个字节来存储。
同理,在64位系统中,有64根地址总线,可以生成一个64位的地址,这个地址由2的64次方个不同的状态组成,远远超过当前任何实际使用的内存大小。这样的地址需要8个字节来存储。
2.3.2 指针变量大小的实际应用
指针变量是用来存储地址的,所以它们的大小必须足以容纳一个地址。在C语言中,我们可以通过sizeof`操作符来查看不同指针变量的大小。
#include <stdio.h>int main() {printf("%zd\n", sizeof(char *));printf("%zd\n", sizeof(short *));printf("%zd\n", sizeof(int *));printf("%zd\n", sizeof(double *));return 0;
}
在32位平台上运行上述程序,你会发现所有的指针变量大小都是4个字节。而在64位平台上,它们的大小会是8个字节。
2.3.3 结论
在32位平台上,由于地址是32位(4个字节),所以指针变量的大小也是4个字节。
在64位平台上,地址是64位(8个字节),指针变量的大小相应地是8个字节。
值得注意的是,指针变量的大小与其能够指向的数据类型无关。无论是指向char、short、int 还是double,只要是指针类型的变量,在相同的平台下,它们的大小都是相同的。
3. 指针变量类型的重要性
尽管指针变量的大小与其类型无关,但指针的类型对于它们如何操作内存至关重要。不同类型的指针允许我们以不同的方式访问和处理数据。
3.1 指针的解引用
通过比较以下两段代码,我们可以在调试时观察内存的变化,以理解指针类型的重要性。
//代码1
#include <stdio.h>int main() {int n = 0x11223344;int* pi = &n;*pi = 0;return 0;
}
//代码2
#include <stdio.h>int main() {int n = 0x11223344;char *pc = (char *)&n;*pc = 0;return 0;
}
在调试过程中,我们可以看到,代码1将变量n
的整个4个字节都设置为0,而代码2仅将n
的第一个字节设置为0。这一现象揭示了一个重要的结论:
结论: 指针的类型决定了在解引用时拥有的操作权限(即一次能操作多少个字节)。例如,char*
类型的指针在解引用时只能访问一个字节,而int*
类型的指针在解引用时可以访问四个字节。这种类型的区分确保了我们能够以正确的大小和格式访问内存中的数据,从而避免数据损坏和未定义行为。
3.2 指针+-整数
#include <stdio.h>
int main()
{int n = 10;char* pc = (char*)&n;int* pi = &n;printf("%p\n", &n);printf("%p\n", pc);printf("%p\n", pc + 1);printf("%p\n", pi);printf("%p\n", pi + 1);return 0;
}
结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。
3.3 void* 指针
#include <stdio.h>
int main()
{int a = 10;int* pa = &a;char* pc = &a;return 0;
}
#include <stdio.h>
int main()
{int a = 10;void* pa = &a;void* pc = &a;*pa = 10;*pc = 0;return 0;
}
4 const修饰指针
4.1 const修饰变量
#include <stdio.h>
int main()
{int m = 0;m = 20;//m是可以修改的const int n = 0;n = 20;//n是不能被修改的return 0;
}
上述代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就⾏修改,就不符合语法规则,就报错,致使没法直接修改n。
但是如果我们绕过n,使⽤n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。
#include <stdio.h>
int main()
{const int n = 0;printf("n = %d\n", n);int* p = &n;*p = 20;printf("n = %d\n", n);return 0;
}
4.2 const修饰指针变量
#include <stdio.h>
//代码1
void test1()
{int n = 10;int m = 20;int* p = &n;*p = 20;//ok?p = &m; //ok?
}
void test2()
{//代码2int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?
}
void test3()
{int n = 10;int m = 20;int* const p = &n;*p = 20; //ok?p = &m; //ok?
}
void test4()
{int n = 10;int m = 20;int const* const p = &n;*p = 20; //ok?p = &m; //ok?
}
int main()
{//测试⽆const修饰的情况test1();//测试const放在*的左边情况test2();//测试const放在*的右边情况test3();//测试*的左右两边都有consttest4();return 0;
}
结论:const修饰指针变量的时候const如果放在*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。const如果放在*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。
5 指针运算
5.1 指针+- 整数
int arr[ 10 ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 };
#include <stdio.h>
//指针+- 整数
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; i++){printf("%d ", *(p + i));//p+i 这⾥就是指针+整数}return 0;
}
5.2 指针-指针
//指针-指针
#include <stdio.h>
int my_strlen(char *s)
{char *p = s;while(*p != '\0' )p++;return p-s;
}
int main()
{printf("%d\n", my_strlen("abc"));return 0;
}
5.3 指针的关系运算
//指针的关系运算
#include <stdio.h>
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0];int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);while(p<arr+sz) //指针的⼤⼩⽐较{printf("%d ", *p);p++;}return 0;
}
6 野指针
6.1 野指针成因
#include <stdio.h>
int main()
{ int *p;//局部变量指针未初始化,默认为随机值*p = 20;return 0;
}
#include <stdio.h>
int main()
{int arr[10] = { 0 };int* p = &arr[0];int i = 0;for (i = 0; i <= 11; i++){//当指针指向的范围超出数组arr的范围时,p就是野指针*(p++) = i;}return 0;
}
(3) 指针指向的空间释放
#include <stdio.h>
int* test()
{int n = 100;return &n;
}
int main()
{int*p = test();printf("%d\n", *p);return 0;
}
6.2 如何规避野指针
6.2.1 指针初始化
#ifdef __cplusplus#define NULL 0#else#define NULL ((void *)0)#endif
6.2.2 小心指针越界
6.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性
int main()
{int arr[10] = {1,2,3,4,5,67,7,8,9,10};int *p = &arr[0];for(i=0; i<10; i++){*(p++) = i;}//此时p已经越界了,可以把p置为NULL
p = NULL;//下次使⽤的时候,判断p不为NULL的时候再使⽤//...p = &arr[0];//重新让p获得地址if(p != NULL) //判断{//...}return 0;
}
6.2.4 避免返回局部变量的地址
7. assert断言
assert(p!=NULL);
#define NDEBUG
#include <assert.h>
8 指针的使⽤和传址调⽤
8.1 strlen的模拟实现
库函数strlen的功能是求字符串⻓度,统计的是字符串中 \0 之前的字符的个数。
函数原型如下:
size_t strlen ( const char * str );
int my_strlen(const char * str)
{int count = 0;assert(str);while(*str){count++;str++;}return count;
}
int main()
{int len = my_strlen("abcdef");printf("%d\n", len);return 0;
}
8.2 传值调⽤和传址调⽤
#include <stdio.h>
void Swap1(int x, int y)
{int tmp = x;x = y;y = tmp;
}
int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap1(a, b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}
#include <stdio.h>void Swap2(int* px, int* py)
{int tmp = 0;tmp = *px;*px = *py;*py = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b); printf("交换前:a=%d b=%d\n", a, b);Swap2(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}