C语言超详细讲解指针的概念与使用

目录
  • 一、指针与一维数组
    • 1. 指针与数组基础
    • 2. 指针与数组
    • 3. 一个思考
  • 二、指针与字符串
  • 三、指针和二维数组
    • 1. 指针数组与数组指针
    • 2. 指针数组
    • 3. 数组指针

一、指针与一维数组

1. 指针与数组基础

先说明几点干货:

1. 数组是变量的集合,并且数组中的多个变量在内存空间上是连续存储的。

2. 数组名是数组的入口地址,同时也是首元素的地址,数组名是一个地址常量,不能更改。

3. 数组的指针是指数组在内存中的起始地址,数组元素的地址是指数组元素在内存中的其实地址。

对于第一点数组中的变量在内存空间上是连续的相信没有什么疑问,这点在讲解数组的时候就已经提到过了。对于第二点,可以得到,数组名就是一个地址,并且是整个数组的内存起始地址。数组名一个是常量地址我们不能对数组名进行赋值操作,数组名是数组的起始地址,数组的第一个元素的地址也是数组的起始地址,他们两个在数值上是相等的。对于第三点,举个例子,假如定义一个 int 类型变量 a,int 占四个字节,假如他的第一个字节的地址是 0x00000010, 第四个字节的地址那么就是 0x00000013。而 &a 的地址是 a 的内存空间上的首地址,即 &a 的值为 0x00000010。

通过下面示例可以来证明一下上面讲述的结论。

源代码:

#include <stdio.h>
int main()
{
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int b = 20;
    char *pb = &b;
    /*a是地址常量,不能被赋值*/
    //a = pb;
    /*在数值上 a &a[0] &a 的数值是相等的 */
    printf("a = %p, &a[0] = %p, &a = %p\n", a, &a[0], &a);
    printf("&b = %p, pb = %p, pb + 3 = %p\n", &b, pb, pb + 3);
    return 0;
}

运行结果:

a = 0xbfb82f38, &a[0] = 0xbfb82f38, &a = 0xbfb82f38
&b = 0xbfb82f30, pb = 0xbfb82f30, pb + 3 = 0xbfb82f33

2. 指针与数组

以一个实例开始,数组的打印你会几种方法?

源代码:

#include <stdio.h>
int main()
{
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int *p = a, i;
//1. 数组元素访问法
    for (i = 0; i < 10; i++) {
        printf("a[%d] = %-3d", i, a[i]);
    }
    putchar(10);  //等价于 putchar('\n'), 因为 \n 的 ASCII 码就是 10
//2. 数组地址偏移访问法
    for (i = 0; i < 10; i++) {
        printf("a[%d] = %-3d", i, *(a + i));
    }
    putchar(10);
//3. 指针元素访问法
    for (i = 0; i < 10; i++) {
        printf("a[%d] = %-3d", i, p[i]);
    }
    putchar(10);
//4. 指针地址偏移访问法
    for (i = 0; i < 10; i++) {
        printf("a[%d] = %-3d", i, *(p + i));
    }
    putchar(10);
    return 0;
}

运行结果:

a[0] = 1  a[1] = 2  a[2] = 3  a[3] = 4  a[4] = 5  a[5] = 6  a[6] = 7  a[7] = 8  a[8] = 9  a[9] = 10 
a[0] = 1  a[1] = 2  a[2] = 3  a[3] = 4  a[4] = 5  a[5] = 6  a[6] = 7  a[7] = 8  a[8] = 9  a[9] = 10
a[0] = 1  a[1] = 2  a[2] = 3  a[3] = 4  a[4] = 5  a[5] = 6  a[6] = 7  a[7] = 8  a[8] = 9  a[9] = 10
a[0] = 1  a[1] = 2  a[2] = 3  a[3] = 4  a[4] = 5  a[5] = 6  a[6] = 7  a[7] = 8  a[8] = 9  a[9] = 10

从上面可以看到,如果一个指针p指向数组a,那么 a[i],*(a+i), p[i],*(p+i) 这四种写法是完全等价的,都是访问数组中第 i+1 个元素。其实数组对于元素的访问根本上就是地址的偏移,a[i] 之所以能够访问到第 i+1 个元素其实他所进行的操作和 *(a+i) 是一样的,都是在地址 a 的基础上偏移 i 个单位的内存单元进行元素访问。a 是一个地址,p 也是一个地址,且当 p 指向 a 的时候,能够以 a[i] 来访问第 i+1 元素,那么同理也能以 p[i] 的方式来访问第 i+1 个元素。同样的 *(p+i) 也是以地址偏移的方式来进行数组元素的访问。其实这几种写法唯一不同的就是 a 是地址常量,不能被赋值,也就是 a 不被允许再指向其他的内存空间,而 p 是指针变量,可以被任意赋值,可以指向其他的内存空间。

3. 一个思考

通过上面的讲解请大家看一下下面程序应该输出多少?

#include <stdio.h>
int main()
{
    int a[10] = {1, 2, 3, 4, 5};
    int *p = &a[1];
    p[0] = 20;
    p[1] = 30;
    p[-1] = 10;
    printf("a[0] = %d, a[1] = %d, a[2] = %d\n", a[0], a[1], a[2]);
    return 0;
}

其实大家可能或许能够猜到程序的输出结果是10,20,30。但是可能并不是所有人都能够清楚的明白编译器是怎么个处理逻辑的,在这里来为大家再做进一步的讲解,让大家对指针和数组的关系有更深一步的认识。

上图简单从地址偏移角度来画出了指针的指向,大家先看右边,地址 a 在数值上指向数组的首地址,那么 a+1 就是偏移了一个 int 类型,也就是 4 字节,所以 a+1 指向数组的第二个元素。大家再看左边,p 指向 a[1] 的首地址,那么从指针变量 p 的角度来看,p[0] 就是他指向的元素 a[1],所以 p[0] 和 a[1] 是完全相等的,呢么 p[1] ,也可以写成 *(p+1) 指向的就是 a[2] 元素的首地址,因为 p+1 要在 p 的基础上偏移 4 字节,p 指向 a[1] 的首地址,那么 p+1 就是指向 a[2] 的首地址,再用取值运算符 *(p+1) 的值就是a[2] 的值也就是 3。p+1 理解了那么 p-1 也就不能理解了,他们两个只是偏移的方向不同,p+1 是向右移,也就是地址增加的方向一定,而 p-1 是向左移,向地址减小的方向移动。

二、指针与字符串

C语言处理字符串通常实讲字符串放在字符数组中,因为C语言没有字符串类型,而字符串在地址空间上是连续的,而数组元素在内存空间上也是连续,字符串就是若干字符的集合,所以就用字符数组来处理字符串,对于常量字符串也可以用指针对其直接指向操作。

关于指针与字符串的关系大家可以先看看下面程序:

#include <stdio.h>
int main()
{
    char a[] = "hello world";
    char *b = "hello world";
    a[1] = 'z';
    //b[1] = 'z';   //error
    printf("a = %s\n", a);
    return 0;
}

其实如上面注释所示,可以对 a 里面的元素进行赋值操作,而不能对 b 里面的元素进行赋值操作。为什么呢?其实原因与内存单元的分配有关。a 是一个局部变量数组,局部变量分配在栈上,栈上的内容具有可读可写操作,所以对 a 里面的元素进行赋值操作没有问题。而 b 是一个指针,也是一个局部变量,但是 b 指向的是一个字符串常量,字符串常量存储在只读区,只读区里面的内容只有读权限而没有写权限,这点尤其注意,所以我们上述操作中对 b 指向的内容进行写操作编译器是会报错的。报错内容就是段错误。导致段错误的原因就是访问了非法内存,非法内存一般就是内存地址不存在或者对于该块地址没有权限。

三、指针和二维数组

1. 指针数组与数组指针

该部分主要讲解指针数组与数组指针。可能对于初学者而言对指针数组和数组指针比价容易弄混,其实记后面两个字就可以,指针数组 后面两个字是数组,说明指针数组是一个数组,那么数组里面存储的内容就是前两个字 指针。数组指针 后面两个字是指针,说明数组指针是一个指针,那么这个指针指向那里,前面两个字就有体现,数组指针指向一个数组。一句话概括之,指针数组是一个数组,数组里面每个元素存储的是一个指针;数组指针是一个指针,是指向数组的指针。

指针数组的定义方法:

char a[5]; //字符数组

char *a[5]; //指针数组

数组指针的定义方法:

char a[5]; //字符数组

char (*a)[5] //数组指针

上面数组指针和指针数组的定义方法很像,其实不管是这里的指针数组 数组指针 还是后面文章中会讲解的 指针函数 函数指针,其实分辨他们有一个诀窍,那就是右左法则,何谓右左法则,即在运算符的优先级范围内,先往右看,再往左看。打个比方,看上面定义的指针数组,先找到 a ,a 的右边与 a 结合是一个数组,那么这个定义就是一个数组,是个什么样的数组呢?再往左看,a 的左边与 a 结合是一个指针,那么就是一个指针数组。再来看看数组指针,先找到 a,a 的右边是一个括号,有括号先看括号里面的内容,也就是往左看,括号里面的内容是一个指针,是个什么样的指针呢?再往右看,是一个数组,所以就是数组指针。掌握了这个方法,不管是给出定义来辨别名字,还是告诉你名字让其写出定义都不在话下。

2. 指针数组

指针数组是一个数组,里面每个成员都是指针,定义一个指针数组相当于定义了多个指针变量的集合。 例如 int *a[3]; 这是一个指针数组,数组里面每个成员都是指向int类型地址的。为了让大家更加熟悉指针数组的使用,大家请看下面这个例子:

源代码:

#include <stdio.h>
int main()
{
    int a = 1, b= 2, c = 3;
    int *p[3];   //定义一个指针数组,数组里面有3个成员,每个成员都是指向int类型的指针。
    /*数组成员的初始化*/
    p[0] = &a;
    p[1] = &b;
    p[2] = &c;
    /*通过指针改变其指向地址中的内容*/
    *p[0] = 10;
    *p[1] = 20;
    *p[2] = 30;
    printf("a = %d, b = %d, c = %d\n", a, b, c);
    return 0;
}

运行结果:

a = 10, b = 20, c = 30

通过上面例子大家就不难发现,定义好指针数组之后,数组成员的使用方法和普通指针的使用是一样的,定义好一个指针数组唯独就是可以一次性定义好多个指向相同类型的指针。其实大家想一下,我们当时引入数组的时候说C语言引入数组就是因为数组可以一次性定义好多个具有相同类型的普通变量,其实这里的指针数组也是一样的,不同的是,普通数组里面的成员都是普通变量,而指针数组里面的成员都是指针。

3. 数组指针

数组指针是一个指针,指向一个数组的指针。 例如 int (*a)[3]; 这是一个数组指针,指向的是一个3列的数组,a+1 就相当于步进为1列,1列有3个int类型,相当于步进了 3 * 4 = 12(字节)。示例如下:

源代码:

#include <stdio.h>
int main(int argc, const char *argv[])
{
	int a[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
	/*定义数组指针*/
	int (*p)[3] = a;
	int (*q)[2] = a;
	/*数组指针步进加1为1行*/
	printf("p = %p, p + 1 = %p\n", p, p+1);
	/*先对行指针进行一次解引用就是普通指针,普通指针步进就是指向的普通数据类型的大小*/
	*(*(p+1) + 1) = 56;
	printf("%d\n", a[1][1]);
	printf("q = %p, q + 1 = %p\n", q, q+1);
	*(*(q+1) + 1) = 56;
	printf("%d\n", a[1][0]);
	return 0;
}

运行结果:

p = 0xbfc208ec, p + 1 = 0xbfc208f8
56
q = 0xbfc208ec, q + 1 = 0xbfc208f4
56

图示如下:

数组指针的步进大家一定要清楚,步进主要看定义时数组个数的大小,比如 int (*p)[3]; 步进加1就是步进 int [3] 大小;int (*q)[2]; 步进加1就是 int [2] 大小。数组指针就是行指针,因为数组指针指向相同行的数组时,指针偏移加1就是1行。

到此这篇关于C语言超详细讲解指针的概念与使用的文章就介绍到这了,更多相关C语言指针内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C语言的数组指针与函数指针详解

    目录 前言 函数指针语法 数组指针与指针数组 总结 前言 数组指针和函数指针都是C语言比较难的知识点,尤其是函数指针,并且函数指针在开发中有着巨大的作用. 函数指针语法 定义一个函数指针,并通过函数指针间接调用函数: int get_num(int a, int b) { return a + b; } int (*func)(int a, int b); //定义了一个函数指针func,它指向 返回值为int 参数为 int a, int b的函数 func = &get_num; //函数指

  • C语言全方位讲解指针与地址和数组函数堆空间的关系

    目录 一.一种特殊的变量-指针 二.深入理解指针与地址 三.指针与数组(上) 四.指针与数组(下) 五.指针与函数 六.指针与堆空间 七.指针专题经典问题剖析 一.一种特殊的变量-指针 指针是C语言中的变量 因为是变量,所以用于保存具体值 特殊之处,指针保存的值是内存中的地址 内存地址是什么? 内存是计算机中的存储部件,每个存储单元有固定唯一的编号 内存中存储单元的编号即内存地址 需要弄清楚的事实 程序中的一切元素都存在于内存中,因此,可通过内存地址访问程序元素. 内存示例 获取地址 C语言中通

  • C语言函数指针数组实现计算器功能

    目录 一.概念 二.用途 三.案例:计算器 (1)基础代码编译: (2)使用函数指针数组的实现: 一.概念 数组:一个存放相同类型数据的存储空间. int arr[10]; //数组arr的每个元素是int 指针数组:一个存放指针的数组. int* arr[10]; //数组arr的每个元素是int* 函数指针:一个指向函数的指针,一般用函数名表示. int Add(int x, int y) { return x + y; } int main() { int arr[10] = { 1, 2

  • C语言详解函数与指针的使用

    目录 一.函数类型 二.函数指针 三.回调函数 四.小结 一.函数类型 C 语言中的函数有自己特定的类型 函数的类型由返回值,参数类型和参数个数共同决定,如 int add(int i, int j)的类型为 int(int, int) C 语言中通过 typedef 为函数类型重命名 typedef type name(parameter list) 如 typedef int f(int, int); typedef void p(int); 二.函数指针 函数指针用于指向一个函数 函数名是

  • C语言返回值指针的函数详解

    #include<stdio.h> void main() { int a[5] = { 1,3,5,7,9 }; int* name[5] = { &a[0],&a[1],&a[2] ,&a[3] ,&a[4] }; int i; for (i = 0; i < 5; i++) { printf("%d ", *name[i]); } printf("\n\n"); } #include<stdio.

  • C语言进阶教程之函数指针详解

    目录 一.函数指针 1.概念 1.2函数指针的使用方法 1.3练习巩固 1.4小结一下 二.阅读两段有趣的代码 1.( *(void( *)( ))0 )( ) 2.void (* signal(int,void( * )( int ) ) )(int) 附:函数指针的应用——函数回调 总结 一.函数指针 1.概念 函数指针:首先它是一个指针,一个指向函数的指针,在内存空间中存放的是函数的地址: 请看示例: int main(){ int a = 10; int*pa = &a; char ch

  • C语言函数指针的老生常谈

    目录 函数指针 函数指针的应用 函数指针作为参数实例(qsort函数) 总结 函数指针 本质上是一个指针,只不过指向函数而已. 编译器在编译期间对函数开辟了一块空间,而这快空间的开始地址,就是它的函数指针 . 下面我们也直接用最直观的程序来了解函数指针: #if 1 void func() { printf("hello ptr!"); } int main() { void (*p)(); p = func; p(); } #endif 在这里我们给出了func()函数,并在其中打印

  • C语言超详细讲解指针的概念与使用

    目录 一.指针与一维数组 1. 指针与数组基础 2. 指针与数组 3. 一个思考 二.指针与字符串 三.指针和二维数组 1. 指针数组与数组指针 2. 指针数组 3. 数组指针 一.指针与一维数组 1. 指针与数组基础 先说明几点干货: 1. 数组是变量的集合,并且数组中的多个变量在内存空间上是连续存储的. 2. 数组名是数组的入口地址,同时也是首元素的地址,数组名是一个地址常量,不能更改. 3. 数组的指针是指数组在内存中的起始地址,数组元素的地址是指数组元素在内存中的其实地址. 对于第一点数

  • C语言超详细讲解指针的使用

    目录 指针概述 自身类型 指向类型 代码例子 数值型指针 字符型指针 单字符 字符数组 字符串型指针 字符数组总结 指针概述 C语言中指针也可以认为是一种类型,不同于数值型和字符型的类型.推演过去指针变量也就是相当于不同的变量类型,不同于数值变量.字符型变量.字符串变量. 指针变量两种类型:自身类型和指向的类型 自身类型:将变量名去掉,剩下的就是指针变量类型. 指向类型:将变量名和离它最近的一个*去掉,剩下的类型就是指针指向的类型 int num = 10; int* p = NULL; p =

  • C语言超详细讲解宏与指针的使用

    目录 1.关于define 2.初识指针 (1)内存 (2)示例 (3)指针的使用示例 (4)指针变量的大小 1.关于define define是一个预处理指令,有两种用法,一种是用define定义常量:另外一种是define定义宏. 下面的例子为利用define定义常量 #define _CRT_SECURE_NO_WARNINGS #define MAX 1000 #include <stdio.h> int main() { printf("%d\n",MAX); r

  • C语言超详细讲解函数指针的运用

    目录 前言 计算器的例子 回调函数 转移表 前言 前面我们学习了各种各样的指针类型,有些指针可以说是稀奇百怪,特别是函数指针,有些朋友可能觉得,函数指针有些多余,调用函数为什么要用指针调用,直接调用不好吗? 接下来我们从具体的实例来回答同学们的问题,加深对函数指针的理解. 计算器的例子 接下来我们写一个简单的计算器程序,完成不同的计算功能比如加减乘除: #include <stdio.h> //相加函数 int add(int a, int b) { return a + b; } //相减函

  • C语言超详细讲解指向函数的指针

    目录 一.函数的指针 二.指向函数的指针变量 三.调用函数的两种方式 四.指向函数的指针的作用 五.用指向函数的指针作函数参数(重点) 六.为什么要将指向函数的指针变量作为函数的形参(重点) 一.函数的指针 首先,函数名代表函数的起始地址,调用函数时,程序会从函数名获取到函数起始地址,并从该地址起执行函数中的代码,函数名就是函数的指针,所以我们可以定义一个指向函数的指针变量,用来存放函数的起始地址,这样一来,就可以通过该变量来调用其所指向的函数. 二.指向函数的指针变量 定义指向函数的指针变量

  • C语言 超详细讲解算法的时间复杂度和空间复杂度

    目录 1.前言 1.1 什么是数据结构? 1.2 什么是算法? 2.算法效率 2.1 如何衡量一个算法的好坏 2.2 算法的复杂度 2.3 复杂度在校招中的考察 3.时间复杂度 3.1 时间复杂度的概念 3.2 大O的渐进表示法 3.3 常见时间复杂度计算举例 4.空间复杂度 5. 常见复杂度对比 1.前言 1.1 什么是数据结构? 数据结构(Data Structure)是计算机存储.组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合. 1.2 什么是算法? 算法(Algorit

  • C语言超详细讲解数据结构中双向带头循环链表

    目录 一.概念 二.必备工作 2.1.创建双向链表结构 2.2.初始化链表 2.3.动态申请节点 2.4.打印链表 2.5.销毁链表 三.主要功能 3.1.在pos节点前插入数据 尾插 头插 3.2.删除pos处节点数据 尾删 头删 3.3.查找数据 四.总代码 List.h 文件 List.c 文件 Test.c 文件 五.拓展 一.概念 前文我们已经学习了单向链表,并通过oj题目深入了解了带头节点的链表以及带环链表,来画张图总体回顾下: 在我们学习的链表中,其实总共有8种,都是单双向和带不带

  • C语言超详细讲解队列的实现及代码

    目录 前言 队列的概念 队列的结构 队列的应用场景 队列的实现 创建队列结构 队列初始化 队列销毁 入队列 出队列 队列判空 获取队列元素个数 获取队列头部元素 获取队列尾部元素 总代码 Queue.h 文件 Queue.c 文件 Test.c 文件 前言 队列的概念 队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾 出队列:进行删除操作的一端称为队头 队列和前文所学的栈

  • C语言超详细讲解数据结构中的线性表

    目录 前言 一.分文件编写 1.分文件编写概念 2.代码展示 二.动态分布内存malloc 1.初识malloc 2.使用方法 三.创建链表并进行增删操作 1.初始化链表 2.在链表中增加数据 3.删除链表中指定位置数据 四.代码展示与运行效果 1.代码展示 2.运行效果 总结 前言 计算机专业都逃不了数据结构这门课,而这门课无疑比较难理解,所以结合我所学知识,我准备对顺序表做一个详细的解答,为了避免代码过长,采用分文件编写的形式,不仅可以让代码干净利落还能提高代码可读性,先解释部分代码的含义,

  • C语言 超详细讲解链接器

    目录 1 什么是链接器 2 声明与定义 3 命名冲突 3.1 命名冲突 3.2 static修饰符 4 形参.实参.返回值 5 检查外部类型 6 头文件 1 什么是链接器 典型的链接器把由编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体–该实体能够被操作系统直接执行. 链接器通常把目标模块看成是由一组外部对象组成的.每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别.因此,==程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个外部

随机推荐