C语言的指针 (上篇) - 指针的概念

指针的理解与定义

1.变量的访问形式

访问内存中变量存储的数据:直接访问(通过变量名来访问)间接访问(通过首地址来访问)

地址==指针

变量:命名的内存空间,用于存放各种类型的数据。

变量名:变量名是给内存空间取的一个容易记忆的名字。

变量值:在变量单元中存放的数据值。

变量的地址:变量所使用的内存空间的地址,即 指针

指针变量:用一个变量来存储地址。我们可以通过访问指针变量达到访问内存中另一个变量数据的目的。

2.指针变量的定义

一般格式:数据类型 *变量名 [ = 初始地址值];

解释说明:

  • 字符 * 用于告知系统这里定义的是一个指针变量,通常跟在类型关键字的后面。
  • 指针变量的名字是 p,不是*p
  • 指针变量中只能存放地址,不要将一个整数(或任何其它非地址类型的数据)赋给一个指针变量。

同时声明两个指针变量:

// 正确
int * a, * b;
// 错误
int* a, b;   //此时a是整数指针变量,而b是整数变量
二级指针:一个指针指向的可能还是指针,这时就要用两个星号 ** 表示。
int **p;

3.指针的应用场景

场景1:使用指针 访问变量 或者 数组的元素 

场景2:应用在数据结构中,构造节点。例如:

             节点:数据+指针:指向下一个元素


指针的运算

      指针作为一种特殊的数据类型可以参与运算,但与其他数据类型不同的是,指针的运算都是 针对内存中的地址来实现的

1.取址运算符:&

取址运算符,使用& 符号来表示。作用:取出指定变量在内存中的地址。

  • 格式:&变量

举例:

int num = 10,*p1;
p1 = #
printf("%d",num);
printf("%p",pi)

说明:

1、在输出取址运算获得的地址时,需要使用“%p”作为格式输出符

2、这里num4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)。


指针变量赋值

  • 赋值给一个指针变量:int *p = #
  • 同时赋值两个指针变量;
int a = 10;
int *b, *c;
b = &a;
c = b;//a,b,c输出的地址相同

注意事项:

  1. 不能将任何其它非地址类型的数据赋给一个指针变量。
  2. 指针变量是“ 带类型的地址 。所以,一个指针变量只能指向同一个类型的变量,不能抛开类型随意赋值。
  3. 在没有对指针变量赋值时,指针变量的值是不确定的,可能系统会分配一个未知的地址,此时使用此指针变量可能会导致不可预料的后果甚至是系统崩溃。
  4. 不要在给指针变量赋值之前使用此变量。可以在调用前,给指针变量赋初始值为0(或null),称为 空指针变量。  

例题:通过指针变量修改内存中存储的数据。

方式1: 
 int num = 10,*p4;
    p4 = #
    printf("num1原值:%d\n",num);
    printf("请输入要改变的值:");
    scanf("%d",p4);//双引号中不能加其他的字符,会使结果出错。
    printf("num1改变后的值:%d\n",num);
方式2:
 int num = 10;
    int *p = #
    *p = 20;
    printf("num = %d\n",num);  //num = 20

2.取值运算符 :*

作用在指针变量上,取出地址代表的值。也称 ‘解引用符号’

* 指针表达式使用方式:*p(p是指针变量)或 *& a(a:数据变量)

注意:“ * 不同于定义指针变量的符号,这里是运算符。 

举例1: 

int main() {
    int a = 2024;
    int *p;
    p = &a;
    
    printf("%p\n",&a); //0000005cc43ff6d4
    printf("%p\n",p);  //0000005cc43ff6d4
    printf("%d\n", *p); //2024
    
    return 0;
}

举例2:通过指针变量修改指向内存地址位置上的值

int main() {
 int num = 10;
    int *p = #
    *p = 20;
    printf("num = %d\n",num);  //num = 20
    
    return 0;
}

举例3:定义指针变量 p1、p2,默认各自指向整数a、b;a、b从键盘输入。设计程序,使得 p1 指向其中的较大值,p2 指向其中的较小值 。 

   int a,b;
    scanf("%d%d",&a,&b);
    int *p1,*p2;
    p1 = &a;
    p2 = &b;
    if(a<b){
        int *t = p1;
        p1 = p2;
        p2 = t;
    }
    printf("%d%d",*p1,*p2);

&* ”和“ *& ” 的区别和含义:

int a = 10;
int *p;
p = &a;
  • &*p 的含义是什么?——&*p &a 相同,即变量a的地址。
  • *&a 的含义是什么? ——*&a *p 的作用是一样的,它们都等价于变量a。即 *&a a 等价。

3. 指针的常用运算

      指针本质上就是一个无符号整数,代表了内存地址。除了上面提到的取址运算外,指针还可以与整数加减、自增自减、同类指针相减运算等。但是规则并不是整数运算的规则。


a.指针与整数值的加减运算

格式:指针 +(-) 整数

作用:在一个连续的同类型数据区域内,通过与整数值的加减获取相应地址的值

指针加减运算的解释:     

  • 指针与整数值的加减运算,表示指针所指向的内存地址的移动(加,向后移动;减,向前移动)。
  • 指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。

举例1:

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = &arr[1];
    printf("p的地址为:%p,对应的值为%d\n", p, *p); 
//p1的地址为:000000df21bff6e4,对应的值为2
    printf("p+1=的地址为:%p,对应的值为%d\n", p + 1, *(p + 1)); 
//p1+1=的地址为:000000df21bff6e8,对应的值为3
    printf("p-1=的地址为:%p,对应的值为%d\n", p - 1, *(p - 1)); 
//p1-1=的地址为:000000df21bff6e0,对应的值为1
    return 0;
}

注意:只有 指向连续的同类型数据区域 ,指针加、减整数才有实际意义。

举例2 定义一个一维数组,遍历其中的元素(用传统的方式取地址的方式)

int array1[5] = {1,2,3,4,5};
    //方式1:(传统方式)
    for (int i = 0; i < 5; ++i) {
        printf("%d",array1[i]);
    }
    printf("\n");
    //方式2:(指针变量)
    int *arr = array1;
    for (int i = 0; i < 5; ++i) {
        printf("%d",*(arr + i));
    }
    //方式3:比较运算符
   for ( arr = array1; arr < array1 + 5; ++arr) {
        printf("%d",*arr);
        }
    printf("\n");

b.指针的自增、自减运算

指针类型变量也可以进行自增或自减运算,如下:

p++ 、 p-- 、 ++p 、--p

例1:

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p1 = &arr[0];
    int *p2 = &arr[3];
    printf("p1的值为:%d\n", *p1);        //1
    printf("++p1的值为:%d\n", *(++p1));  //2
    printf("p1的值为:%d\n", *p1);        //2
return 0;
}

例2: 

int a[5] = {10,20,30,40,50};
int *p = a;  //p开始时指向数组a的首元素
printf("%d\n",*p++); //10   分析:由于++和*同优先级,结合方向自右而左,因此它等价于*(p++)
//先取值打印出来,然后在执行++的操作。

printf("%d\n",*p);   //20

注意:自增和自减要分清楚 先赋值在自增(自减),还是先 自增(自减)再赋值。


c.同类指针相减运算

格式:指针 - 指针

注意:

  • 相同类型的指针允许进行减法运算,返回它们之间的距离,即 相隔多少个数据单位(注意:非字节数) 。高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。
  • 返回的值属于 ptrdiff_t 类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件 stddef.h 里面。

例:

#include <stdio.h>
#include <stddef.h>

int main() {
    int arr[5] = {1, 2, 3, 4, 5};
    int *p1 = &arr[0];
    int *p2 = &arr[3];
    printf("p1的地址为:%d\n", p1); //497022544
    printf("p2的地址为:%d\n", p2); //497022556
    
    ptrdiff_t dist = p2 - p1;// 返回值数据类型:prtdiff_t
    
    printf("p2-p1=%d\n", dist); //3 等同于 (497022556 - 497022544)/4 ==> 3
    return 0;
}
  •  两个指针相减,通常两个指针都是指向同一数组中的元素有意义结果两个地址之差除以数组元素的长度。
  • 两个指针相加:是非法的,结果没有意义。

d. 指针间的比较运算

指针之间的比较运算,比如 ==、!= 、<、 <= 、 >、 >=。比较的是各自的内存地址的大小,返回值是整数 1 true)或 0 false)。

例:定义一个一维数组,遍历其中的元素(方式3:比较运算+自增)

for ( arr = array1; arr < array1 + 5; ++arr) {
        printf("%d",*arr);
        }
    printf("\n");

例2:

int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[0];
int *p2 = &arr[3];
printf("%d\n",p1 > p2);  //0
printf("%d\n",p1 < p2);  //1
printf("%d\n",p1 == p2); //0
printf("%d\n",p1 != p2); //1

野指针

野指针:指针指向的位置不可知(随机性,不确定性,没有明确的限制)

野指针的成因及解决方法

情况1:指针使用前未初始化。

       定义:指针变量在定义时如果未初始化, 其值是随机的 ,此时操作指针就是去访问一个不确定的地址,所以结果是不可知的。

错误的例子: 

#include<stdio.h>
int main(){
 int* p;                      
 *p = 10;  
 
 return 0;
}

      在没有给指针变量显式初始化的情况下,一系列的操作(包括修改指向内存的数据的值)也是错误的。如上述代码所示。

避免方法: 

定义指针的时候,如果没有确切的地址赋值,为指针变量赋一个NULL 值是好的编程习惯。即;

int *p = NULL;
赋为 NULL 值的指针被称为 空指针 NULL 指针是一个定义在标准库 <stdio.h>中的值为零的常量 #define NULL 0

情况2:指针越界访问

        定义: *p 访问的内存空间不在数组有效范围内,此时 *p 就属于非法访问内存空间,p为野指针。

错误的例子:定义一个长度为10的一维数组,使用指针访问数组的元素时超出了数组的范围。

int arr[10] = {0};
    int *p = arr;
    for (int i = 0; i <= 10; i++,p++) {
        *p = i;  
//当i=10时,越界。

避免方法: 小心指针越界,进行仔细检查


情况3:指针指向已经释放的空间

假如,定义了一个函数,函数的返回值是一个定义在函数内部局部变量的地址,由于局部变量只在函数内有效,出了函数其内存空间就被释放,也就意味着 局部变量的地址编号不存在 若将函数返回的地址赋值给外部的一个指针变量,导致指针变量获取到的地址是 无效 的。

例:

//定义了一个函数,函数的返回值是内部局部变量的地址
int *test() {
    int a = 10;
    return &a;          //&a=0x0012ff40
}
//将上面函数的地址赋值给主函数中定义的一个指针变量
int main() {
    int *p = test();
    printf("%d", *p); //由于空间释放的原因,将不会输出值
    return 0;
}

避免方法: 避免返回局部变量的地址。使用前要查看是否返回的是局部变量的地址。


其他避免出现野指针的方法:

1. 指针指向空间释放,及时置NULL

int a = 10; 
int* pa = &a;
printf("%d\n", *pa);
pa = NULL; //把pa指针置成NULL
printf("%d\n",pa);

2.指针使用之前检查有效性

if (pa != NULL){
 //进行使用
}
if (pa == NULL){
 //不进行使用
}

二级指针(多重指针) 

1.定义

                   记录一个指针地址的指针变量,即二级指针。

格式:数据类型 **指针名

举例: 

int a = 10;
int *pa = &a;  //pa是一级指针
int **ppa = &pa; //ppa是二级指针,类型为int **

解引用:

 int var = 3000;
    int *ptr = &var;        // 一级指针指向 var
    int **pptr = &ptr;      // 二级指针指向 ptr
    
    printf("Value of var: %d\n", var);
    printf("Value of ptr: %d\n", *ptr);         // 解引用一次
    printf("Value of pptr: %d\n", **pptr);      // 解引用两次

//上面三个输出的都是变量var的值,3000

2.二级指针的练习 

举例1:使用malloc()函数创建一维数组。

int length = 5;
int *p1 = (int*)malloc(length * sizeof (int));//创建一个动态内存堆空间

free(p1);//释放空间。

举例2:使用malloc()函数创建二维数组。 

步骤:

  1.  第一步:row和column;
  2. 第二步:初始化外层数组;
  3. 第三步:初始化内层数组,并给内存数组元素进行赋值。
 int row,column;
    printf("请输入行和列数:");
    scanf("%d%d",&row,&column);
    //初始化外层数组,二维数组的行数。使用二级指针加上malloc()函数
    int **arr = (int**)malloc(row * sizeof(int*));//int*:每一个元素指向了一个一维数组
    //初始化内层数组,并给元素赋值
    for (int i = 0; i < row; ++i) {
        arr[i] = (int*) malloc(column * sizeof (int));
        //赋值

        for (int j = 0; j < column; ++j) {
            arr[i][j] = 1;
            printf("%d",arr[i][j]);
        }
        printf("\n");
    }
    //使用完回收此数组
    free(arr);
### 双指针算法概述 双指针是一种高效的算法设计技巧,在特定场景下可以通过减少嵌套循环次数显著优化时间复杂度。它通常适用于数组或链表中的问题,通过两个指针协同工作完成目标操作。 #### 双指针的主要类型 双指针可以根据具体的应用场景划分为以下几种主要形式[^1]: - **顺序双指针**: 两个指针按照一定规则依次向前移动,主要用于单调性问题或者单向遍历。 - **首尾双指针 (对撞双指针)**: 初始时一个指针位于序列起始位置,另一个位于末端,逐步相向而行,适合于查找满足某种条件的元素组合。 - **快慢双指针**: 一个指针快速前进,另一个较慢,典型应用如检测环形链表是否存在环。 - **滑动窗口**: 使用双指针动态调整窗口范围,寻找符合条件的最大/最小子区间等问题。 这些类型的共同特点是利用两组索引来代替单一索引进行数据结构上的多次扫描,从而降低整体的时间消耗。 #### 应用实例分析 以下是几个经典的例子说明如何运用不同种类的双指针解决问题: ##### 示例一:移除重复项(顺序双指针)[^2] 假设有一个已排序好的整数列表`nums=[0,0,1,1,1,2,2,3,3,4]`, 要求删除其中所有的冗余副本只保留唯一值并返回新长度。采用顺序双指针策略如下所示: ```python def removeDuplicates(nums): if not nums: return 0 slow = fast = 0 while fast < len(nums): if nums[slow] != nums[fast]: slow += 1 nums[slow] = nums[fast] fast += 1 return slow + 1 ``` 此代码片段展示了当遇到不相同的数值时更新慢速指针对应的位置;否则仅推进快速指针继续探索后续可能不同的数字直到整个数组被覆盖完毕为止。 ##### 示例二:两数之和等于指定目标值(首尾双指针)[^2] 对于升序排列的一维数组arr以及给定的目标sumValue=9 , 寻找任意一对加起来正好等于该targetSum 的index pair 。这里可以借助首尾双指针的方法来高效达成目的 : ```python def two_sum_sorted_array(arr,targetSum): left,right=0,len(arr)-1 result=[] while left<right : currentSum=arr[left]+arr[right] if currentSum==targetSum: result.append((left,right)) break elif currentSum<targetSum: left+=1 else: right-=1 return result or None ``` 上述函数定义了一个过程——不断比较当前两端所指向元素总合与期望值得关系进而决定哪一侧应该收缩直至找到匹配的结果或者是确认不存在这样的配对情况发生. #### 普通指针 vs. 双指针 尽管两者名称相似但实际上存在本质差异: - **普通指针**更多指的是C/C++等低级语言里直接操控内存单元地址的能力[^3], 它们允许程序员手动管理资源分配释放等工作但是也带来了额外的风险比如越界读写错误等等. - 相反,**双指针**则是在高级抽象层次上讨论的一种逻辑概念并不依赖具体的硬件设施支持而是基于现有集合对象之上构建出来的解决方案模式.[^4] 另外值得注意的是虽然理论上任何可以用双指针解答的任务也可以改写成暴力枚举的形式只不过前者往往具备更优性能表现特别是在大规模输入规模面前优势更加明显.[^5]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值