C语言内功修炼---指针详讲(初阶)

简介: C语言内功修炼---指针详讲(初阶)

前言:

都说会用一门语言几个礼拜就可以了。这句话我不敢苟同,至少在我学习C语言指针之后就不这么觉得了。

不信?来上才艺:

//代码1
(*(void (*)())0)();
//代码2
void (*signal(int , void(*)(int)))(int);

这两行代码出自《C陷阱和缺陷》

我相信大部分人在第一次看这俩行代码都是一脸懵逼。

是不是头皮发麻?这是啥东西?

如果你是这样,那么请收起你的骄傲,再也不要觉得C语言很“简单”,谦虚一点,好好学习!

如果不是,能一眼看出来是这俩行代码是什么意思的评论区告诉我,我给你点赞 (大佬抱抱)。

好了,其实无论你能否一眼看出来以上代码所表示的意思,我觉得都不应该轻视任何一门语言,编程世界,浩瀚无边,人外有天,天外有人。只有对知识怀着敬畏之心,知识才会源源不断的涌入你的脑袋,挤走水分。

打住!以下开始c语言指针的学习。

1.指针是什么?

要知道,我们的数据存放在计算机的内存里面,这些数据是非常多的,而要在这么多的数据里面找到我们所需的数据就需要对内存里面的每个单元编号,这样一来,每个内存单元都有了自己独立的编号,我们在存放数据以及查找数据时就只需要找到对应的编号在进行操作就可以了。

就像是在一栋二十楼的大厦里面找到张三的住处,如果不知道他的住房编号,那么就只能一个房间一个房间的查了,而如果有张三的住房编号,知道他住几楼几号,那找到他的房子就很简单了。

这样类似于房间编号的编码,就是地址,也就是指针。

既然每一个最小的内存单元都有一个地址,那么这个内存单元多大呢?

首先定义一个一维数组,因为一维数组的元素在内存中是连续存放的,每个元素的空间大小除以每个元素所占连续地址的数量,就是每一个地址所占得空间大小。

这里我们可以看到,a[0]的地址与a[1]的地址相差4,又因为int占四个字节,所以这四个字节都有一个地址。

计算机存储信息的最小单位,称之为位(bit,又称比特)存储器中所包含存储单元的数量称为存储容量,其计量基本单位是字节(Byte。简称B),8个二进制位称为1个字节,此外还有KB、MB、GB、TB等,它们之间的换算关系是1Byte=8bit,1KB=1024B,1MB=1024KB,1GB=1024MB,1TB=1024GB。

所以理解指针有两个要点:

1.指针就是最小内存单元的编号(地址),每个内存单元为一字节。

2.我们口头上表述的指针其实是指针变量,是一个用来存放地址的变量。

指针变量:

是一个存放地址的变量,返回值是指针类型,可以用取地址符&把地址取出来。(上面的代码有用到)。

好了,现在我们知道了指针变量就是存放地址值以及每一个地址都是一个字节的编号。

如何编址:

还有一个问题,就是这个地址是怎么来的呢?是如何编址的呢?

大概就是在计算机里面有一些地址线,如果是32位机器那就是32根线,每一根线在寻址的时候都会产生高电压或者低电压,也就对应着二进制的1和0,也就是说,这32根线可以组成多少个不同的01序列呢?2的32次方。

这些不同的01序列也就一一对应着一个地址,也就是有2的32次方个字节去编址。

0101001010101001010101001010………………

32个0/1位要用多大的空间去储存?1个字节8个比特位,那就是4个字节存放这么一串32位序列。

所以,一个用来存放地址的指针变量也就占4个字节的大小咯!(32位机器)。

那么2的32次方个字节是多大呢?

1GB=1024*1MB=1024*1024*KB=1024*1024*1024*bit=2^30bit.

所以2^32bit=4GB.

那么64位机器有64根地址线,能编多少个地址呢?

好大好大………

总结:

1.指针是用来存放地址的,地址是唯一的一块空间标识。

2.指针变量的大小跟机器操作位数有关,32位的话就是4个字节,64位的话就是8个字节。

2.指针类型:

指针有什么类型呢?给出以下类型:

char* pc = NULL;
int* pi = NULL;
short* ps = NULL;
long* pl = NULL;
float* pf = NULL;
double* pd = NULL;

指针变量的定义:类型 + *.

char* 类型的指针是为了存放 char 类型变量的地址。

short* 类型的指针是为了存放 short 类型变量的地址。

int* 类型的指针是为了存放 int 类型变量的地址等等。

那么问题来了,既然每个指针变量存放的都是地址,也就是一个编码,其本质上来说都是一样的,

2.1为什么还要给指针区分类型呢?

指针类型的意义主要体现在以下几个方面:

  1. 内存管理:指针类型允许我们动态地分配和释放内存。通过指针,我们可以在运行时分配内存块,并在不需要时释放它们,这样可以更有效地利用内存资源。例如,在C语言中,可以使用malloc()函数来动态分配内存,并使用free()函数来释放内存。
  2. 数据结构:在C语言中,指针类型非常适合用来构建复杂的数据结构,如链表、树和图等。通过指针,可以连接不同的数据节点,并通过指针进行遍历、插入和删除等操作,从而实现高效的数据操作。
  3. 函数传参:在C语言中,函数的参数传递通常是通过值传递的方式,也就是将实参的值复制给形参。但是对于大型的数据结构或者需要修改实参的情况,通过指针传递参数可以避免数据的复制,提高函数的执行效率,并且可以直接修改实参的值。
  4. 数组操作:数组在C语言中是通过指针进行访问的。数组名实际上是指向数组首元素的指针。通过指针可以对数组进行遍历、访问和修改等操作,使得数组操作更加灵活高效。

2.2指针加减一个整数:

char ch = 'a';
char* pc = &ch;
int num = 11;
int* pi = #
 
printf("ch地址 %p\n", pc);//输出char类型变量ch的地址
printf("ch地址+1 %p\n", pc + 1);//输出pc+1的地址
 
printf("num地址 %p\n", pi);
printf("num地址+1 %p\n", pi + 1);

指针的类型还在结构上决定了指针向前或者向后走一步有多大(距离)。

2.3指针的解引用

定义:指针的解引用是指通过指针访问或修改指针所指向的内存中存储的数据。当我们通过一个指针变量来间接访问它所指向的值时,就称为指针的解引用。

int x = 10;
int* p = &x;  // p指向变量x的地址
printf("%d\n", *p);  // 输出变量x的值,输出:10
*p = 20;  // 修改变量x的值
printf("%d\n", x);  // 输出修改后的变量x的值,输出:20

在上述代码中,通过使用"*p"来解引用指针p,我们实际上是在访问或修改p所指向的内存中的值,也就是变量x的值。

需要注意的是,当解引用一个指针时,要确保该指针已经被正确地初始化,且指向有效的内存位置,否则会导致未定义的行为。因此,在对指针进行解引用之前,经常需要对指针进行空指针判断或者有效性检查。不同的指针类型解引用有什么区别呢?

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节

3.野指针

3.1什么叫野指针?

野指针(Wild Pointer)是指指向非法内存地址的指针变量。这种指针没有被正确初始化,或者指向的内存已被释放,因此不能安全地访问或修改其所指向的数据。

3.2什么情况会引起野指针问题呢?

1.指针没有初始化

在定义指针变量的时候没有给其赋予初值,也没有使其指向有效的空间地址,这样一个没有被定义的指针在被解引用的时候就会因为找不到其指向地址而产生不确定的后果,甚至会导致系统崩溃。

2.指针指向的空间被释放

在一片空间被释放的时候,如果没有将其指针置为NULL或者指向其他的地址,如果继续使用已释放的指针进行解引用操作,可能会导致访问无效的内存,造成程序错误或崩溃。

3.指针越界

当访问或者修改一个指针指向的内存块范围外的空间位置时,该位置也许是一个无效的内存,也有可能已经存放了其他的变量的数据,所以这样的访问可能会导致程序崩溃,数据损坏等错误。

3.3如何避免野指针的出现呢?

1、定义指针时初始化。

2、在使用指针的时候检查有没有越界。

3、在释放内存后,将对应的指针置为NULL。

4、在指针超出其作用域后,将其置为NULL,以免被误用。

5、使用前检查其有效性。

总的来说,为了避免野指针问题,应该养成良好的指针使用习惯。

4.指针运算

4.1指针加减整数

指针+整数:

int arr[5] = { 1,2,3,4,5 };
int* p1 = &arr[0];
for (int i = 0; i < 5; i++) {
  printf("%p %d\n", p1 + i, *(p1 + i));
}

表示指针向地址增高的方向移动了若干个元素的距离,如果元素是整数,那么就移动4个字节的距离。

减法也是同理:

4.2指针减指针

 
  int arr[5] = { 1,2,3,4,5 };
  int* p1 = &arr[4];
  int* p2 = &arr[0];
  printf("%d\n", p1 - p2);
  char str[] = "abcdefghij";
  char pc1 = &str[0];
  char pc2 = &str[9];
  printf("%d\n", pc2 - pc1);

我们可以发现,两个指针相减得到的结果是中间的元素个数。

4.3指针的关系运算

指针是怎么进行比较的呢?

int arr[] = { 1, 2, 3 };
int* p0 = &arr[0];
int* p1 = &arr [1];
int* p2 = &arr[2];
//分别输出三个指针
printf("p0=%d\n", p0);
printf("p1=%d\n", p1);
printf("p2=%d\n", p2);
//比较三个指针,并输出表达式的值
printf("p0>p1=%d\n", p0 > p1);
printf("p0<p1=%d\n", p0 < p1);
printf("p0>p2=%d\n", p0 > p2);
printf("p0<p2=%d\n", p0 < p2);
printf("p2>p1=%d\n", p2 > p1);
printf("p2<p1=%d\n", p2 < p1);

我们发现,其实指针比较的就是地址大小,返回0表示假,1表示真。

我们可以利用这一点来比较数组元素的相对顺序。

值得注意的一点是,标准规定:

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。

5.指针和数组

大多数情况下,数组名和数组首元素的地址是一样的。

1. int arr[3] = { 1,2,3 };
2. printf("%p %p\n", arr, arr[0]);

所以数组名表示的就是数组首元素的地址。

只有两种情况例外:

1.sizeof数组名

int arr[3] = { 1,2,3 };
//printf("%p %p\n", arr, &arr[0]);
printf("%d\n", sizeof arr);//输出arr表示的字节大小
printf("%d\n", sizeof arr[0]);//输出首元素的字节的大小

我们可以看到,这个时候arr和arr[0]表示的意思不一样了,此时的arr表示的是整个数组,所以sizeof(arr) 也就是得到整个数组的字节大小。

2.&数组名

 
void test(int* p) {
  printf("%d\n", sizeof p);
}
 
int main() {
  int arr[5] = { 1,2,3,4,5 };
  test(&arr);
  return 0;
}

为什么此时的sizeof p=4呢?

其实这里的4表示是的是指针变量的空间大小,你换成char* 类型同样也是4。

当用数组名作为参数传参的时候,形参实际上上就是一个指针变量,sizeof 指针=4(32位机器)。

通过指针访问数组

既然可以把数组名当成一个地址存放在指针中,那我们就可以利用这个指针来访问这个数组。

int arr[5] = { 1,2,3,4,5 };
int* p = arr;
int sz = sizeof arr / sizeof arr[0];//得到数组大小
for (int i = 0; i < sz; i++) {
  printf(" & arr[%d] =%p   <====> p + %d = %p\n",i, &arr[i],i, p + i);
  printf(" arr[%d] =%d   <====> *(p + %d) = %d\n", i, arr[i], i, *(p + i));
}

所以 p+i 其实计算的是数组 arr 下标为i的地址,也就是说 *(p+i)=arr[i]。 那我们就可以直接通过指针来访问数组。

6.二级指针

看完以上内容相信我们已经初步知道了指针变量的由来以及用法。那么,问题又来了,既然指针变量也是一个变量,那指针变量又是存放在那里呢?指针变量的地址存在那里呢?

一级指针变量的地址存放在二级指针里。

来看以下代码:

int a = 10;
int* p1 = &a;//将变量a的地址赋给指针p1
printf("%p\n", p1);
int** p2 = &p1;//将指针p1的地址赋给p2
printf("%p\n", p2);
//分别进行解引用
printf("%d\n", *p1);
printf("%p\n", *p2);
printf("%p\n", p2);

我们可以看到,我们把变量a的地址赋给了指针p1,再把指针p1的地址赋给指针p2,这个时候解引用p1得到的是a的值,而解引用p2得到的是p1的值,也就是a的地址。

又因为p2存的是p1的地址,*p2得到的是变量a的地址,也就是说再对*p2解引用得到的就是变量a的值了。

int a = 10;
int* p1 = &a;//将变量a的地址赋给指针p1
int** p2 = &p1;//将指针p1的地址赋给p2
printf("%d\n", **p2);

输出10.

学习是一个循序渐进的过程,只有把这些指针的基本知识先了解了才能更好的深入了解指针,下一篇博客我将和大家更加深入的了解指针,感谢大家的支持!

相关文章
|
24天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
45 0
|
23天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
15 2
|
23天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
23天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
30天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
29天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
30天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
30天前
|
C语言
C语言指针(3)
C语言指针(3)
11 1
|
30天前
|
C语言
C语言指针(2)
C语言指针(2)
13 1
|
23天前
|
编译器 C语言
【c语言】指针就该这么学(2)
本文详细介绍了指针与数组的关系,包括指针访问数组、一维数组传参、二级指针、指针数组和数组指针等内容。通过具体代码示例,解释了数组名作为首元素地址的用法,以及如何使用指针数组模拟二维数组和传递二维数组。文章还强调了数组指针与指针数组的区别,并通过调试窗口展示了不同类型指针的差异。最后,总结了指针在数组操作中的重要性和应用场景。
17 0