C语言指针——从入门到精通-创新互联

〇、前言

本文是本人在学习 C语言的过程中所积累的对 C语言指针的感悟,可能会有些地方描述不准确,还请指出。本文遵循一般文章结构,从简单到难,从基本概念到抽象总结。适合任何任何学习 C语言的人群。

我们提供的服务有:成都网站设计、做网站、微信公众号开发、网站优化、网站认证、来宾ssl等。为超过千家企事业单位解决了网站和推广的问题。提供周到的售前咨询和贴心的售后服务,是有科学管理、有技术的来宾网站制作公司一、指针的概念

指针的值就是某一个变量的内存地址,指针变量就是用来存放某个变量的内存地址的变量,和广义的变量没有什么区别。

在同一CPU构架下,不同类型的指针变量所占用的存储单元长度是相同的。这是因为操作系统的位数与其所能支持的大内存有直接的关系。由于计算机是按照字节寻址的,如在 32 位操作系统下,32位比特位一共能描述 2^32 个状态,一个状态标记大小为 1B(一般定义8位(bit,比特)为一字节),所以一共有 2^32*1B = 4GB。因此 32 位系统所能支持的大内存为 4GB。而对于 64 位操作系统(目前主流操作系统),所能支持的大内存为 2^64*1B = 17179869184GB,这是一个很大的数,基本上可以支持任何现实中任意存在的内存。

而指针最小就是以字节为单位进行指引,因此对于 32 位操作系统,它也要是 32 位的,即 4 字节大小;而对于 64 位操作系统,它就得是 64 位 即 8 字节大小。

我们可以写一个程序验证下:

#includeint main() {int a = 4;
    int* p = &a;
    printf("%zu\n", sizeof(p));
    // 指针变量占据 8 个字节
    printf("%p\n", p);
    // 变量 a 存放在以内存地址为0x16f85f71c开头的内存单元中
}

输出结果为:

8
0x16f85f71c

在 C/C++语言中,指针一般被认为是指针变量,指针变量的内容存储的是其指向的对象的首地址,指向的对象可以是变量(指针变量也是变量),数组,函数等占据存储空间的实体。

二、指针详解

以下的例子由易到难,每次增长一颗星。

(一)★
#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;
    printf("%p\n%p\n", p1, p2);
    return 0;
}

输出为:

0x16bacb718
0x16bacb714

这是一个很简单的例子,程序中创建了两个局部变量,然后利用指针输出它们的地址。其中&是取地址符,取出来以后赋给两个指针变量p1、p2,并将其打印出来。
细心的话可以发现这两个地址由大到小,相差为 4。这说明栈中的这两个变量恰好相邻,且一个int类型的变量占用 4B大小空间。而且还说明栈空间的增长方向是由大到小的,当然我们的主题是指针,这里不再赘述。

(二)★★
#includeint main() {int a = 0;
    int* p = &a;
    printf("通过指针修改前 a 的值为:%d\n", a);
    *p = 10;
    printf("通过指针修改后 a 的值为:%d\n", a);
    return 0;
}

输出为:

通过指针修改前 a 的值为:0
通过指针修改后 a 的值为:10

这里就比上面稍微多了一点东西,就是可以通过指针去修改被该指针所指的变量的值。换句话说,就是间接修改变量的值。其中*p的含义为解引用,指代的就是变量a

(三)★★★
#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;

    printf("a在%p\n", p1);
    printf("b在%p\n", p2);

    int **pp1 = &p1, **pp2 = &p2;

    printf("a的指针p1在%p\n", pp1);
    printf("a的指针p1在%p\n", pp2);
    
    return 0;
}

输出为:

a在0x16da73718
b在0x16da73714
a的指针p1在0x16da73708
a的指针p1在0x16da73700

可以看到,指针变量也有一个内存地址,它指向的是一个指针变量的内存地址。那么,我们同样可以通过二级指针对一级指针作出修改:

#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;

    printf("a在%p\n", p1);
    printf("b在%p\n", p2);

    int **pp1 = &p1, **pp2 = &p2;

    printf("a的指针p1在%p\n", pp1);
    printf("b的指针p2在%p\n", pp2);

    // 交换两个二级指针
    int** temp = pp1;
    pp1 = pp2;
    pp2 = temp;
    printf("a的指针p1在%p\n", pp1);
    printf("b的指针p2在%p\n", pp2);
    
    return 0;
}

输出为:

a在0x16fa0b718
b在0x16fa0b714
a的指针p1在0x16fa0b708
b的指针p2在0x16fa0b700
a的指针p1在0x16fa0b700
b的指针p2在0x16fa0b708

这里需要提一点,就是当一个变量(前提是一个变量)为表达式左值时,它代表一个变量,当其为右值时,它代表变量的值。关于左值右值的概念,不再赘述。

交换前的指针关系:
在这里插入图片描述
交换后的指针关系:
在这里插入图片描述
我们还可以尝试通过pp2来直接修改变量a的值:

#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;

    printf("a在%p\n", p1);
    printf("b在%p\n", p2);

    int **pp1 = &p1, **pp2 = &p2;

    printf("a的指针p1在%p\n", pp1);
    printf("a的指针p1在%p\n", pp2);

    // 交换两个二级指针
    int** temp = pp1;
    pp1 = pp2;
    pp2 = temp;
    printf("a的指针p1在%p\n", pp1);
    printf("a的指针p1在%p\n", pp2);
    
    **pp2 = 10;
    printf("修改后a的值为:%d\n", a);
    return 0;
}

输出为:

a在0x16b1ff718
b在0x16b1ff714
a的指针p1在0x16b1ff708
a的指针p1在0x16b1ff700
a的指针p1在0x16b1ff700
a的指针p1在0x16b1ff708
修改后a的值为:10

其中二级指针pp2解引用了两次,才与a等价。这里的例子大量使用了二级指针,即int**类型的变量。具体来说,指针类型有多种,比如int* pint** p,int*** p等等。

(四)★★★★

前面几节都是很简单的概念,接下来几节将会引入数组、字符串等来对指针的使用进行更深的阐述。

(1)关于数组名
#includeint main() {int a[3] = {1, 2, 3};
    printf("%p\n", a);
    printf("%zu\n", sizeof(a));
}

输出为:

0x16fc9b708
12

可以看到,数组名 a 是一个指针类型的值,它实际上指向的是大小为 12B 的连续空间的首地址。数组名其实是一个常量,因此它不能当做左值。当我们修改它的值时,它会提示array type 'int [3]' is not assignable,意思是这个变量不可被修改。另外,a 的大小为 12B 这表明和普通的指针并不一样,或者a并不是简单的指针。因此,很多参考书上说“数组名本质上就是一个指针”的说法是完全错误的。

(2)利用数组名来访问某个内存单元

我们刚提到,数组名是一个常量,因此我们可以利用一个指针变量来访问内存单元:

#includeint main() {int a[3] = {1, 2, 3};
    int* p = a;
    for (int i = 0; i< 3; i++) {printf("%d ", *(p + i));
    }
    return 0;
}

输出为:

1 2 3

其中p+i的含义是,指针指向了p所指的内存再向右偏移i个类型为int的内存单元。之所以是int,是因为我们声明的是int类型的指针。比如:

#includeint main() {int a[3] = {1, 2, 3};
    
    int* p = a;
    printf("%p\n", p);
    printf("%p\n", p + 1);
    printf("%p\n", p + 2);

    long *q = a;
    printf("%p\n", q);
    printf("%p\n", q + 1);

    return 0;
}

输出为:

0x16f1c3708
0x16f1c370c
0x16f1c3710
0x16f1c3708
0x16f1c3710

可以看到,因为long类型占 8 个字节(long的定义为不少于int类型的大小,有些计算机系统longint等价,但大部分long类型都占 8 字节),int占 4 个字节,所以long类型的指针偏移的长度为int类型的两倍。同样,我们可以搞点稍微复杂的事情:

#includeint main() {int a[3] = {1, 2, 3};

    printf("数组的首地址为:%p\n", a);

    int* p = (int*)(&a + 1);
    int* q = a + 3;
    printf("此时 p 指向:%p\n", p);
    printf("此时 q 指向:%p\n", q);
    return 0;
}

输出为:

数组的首地址为:0x16b25f708
此时 p 指向:0x16b25f714
此时 q 指向:0x16b25f714

这说明,&a + 1的偏移量为3个 int 类型大小。这里就需要格外注意,指针偏移一个单位是参考哪一种类型的变量指针的。这里参考的是int a[3],也就是说,a的数据类型为int[3],偏移一次当然偏移 12B了,因为int[3]的大小就是 12B!

我们就可以很简单地预见以下的程序输出:

#includeint main() {int a[3] = {1, 2, 3};

    int* p = (int*)(&a + 1);
    printf("%d\n", *(p - 1));
    return 0;
}

输出为:

3

因为p指向的是数组最后一个元素的下一个内存单元,又因为pint类型的指针,因此一个偏移量大小为int,所以p-1之后,就指向了数组的最后一个元素。

(3)字符串

字符串是一个重点,也是一个难点。但是只要掌握指针基本概念,把握字符数组和字符串的区别和联系,也是送分题。

首先得了解下字符串输出的基本原理:

#includeint main() {char c[3] = {'a', 'b', 'c'};
    printf("%s\n", c);
    return 0;
}

输出为:

abc*/

为什么会有这么奇怪的输出呢?这是因为printf("%s\n", c)输出字符串时,只要输入一个指针(任何类型的指针都可以),就会一直打印,直到遇到'\0'为止。比如下面这个程序:

#includeint main() {int a[3] = {100, 101, 102};
    printf("%s\n", a);
    return 0;
}

输出结果为:

d

这是因为数据存放为大端方式,100存在到第一个高地址字节空间后,后面三个存放的都是'\0',所以就停止打印了。我们可以印证一下:
假设我们想要打印出"abc",这就要保证内存单元里面放的是97\98\99\00。即0x61626300。大端方式存入内存单元就为:0x00636261,这个数字 10 进制大小为6513249,因此:

#includeint main() {int a[3] = {6513249,100,200};
    printf("%s\n", a);
    return 0;
}

程序输出为:

abc

果然输出了"abc",这无疑是一件令人激动的事情!当然,这里的重点是字符串输出,就不再赘述其它了。

经过上面的例子,可以充分地说明输出函数的特性,即只要是一个指针,丢给printf("%s\n", a)后,就会打印出结果。

所以字符串是使用空字符'\0'结尾的一组数据,就这么简单。我们可以很容易地构造出一些字符串,比如我们上面通过一些手段构造的"abc",以及下面用字符数组构造的字符串(常用手段):

#includeint main() {char string[12] = {'H','e','l','l','o','w','o','r','l','d','!','\0'};
    printf("%s\n",string);

    char *string1 = "Helloworld!";
    printf("%s\n",string1);

    return 0;
}

在用字符数组构造字符串时,一定要注意在最后一个内存单元加上’\0’,因为我们不能保证字符串结束后的下一个内存单元放的是不是’\0’,因此字符串可能不会正常终止。

还可以使用字面值常量来创建字符串,比如char *string1 = "Helloworld!"。这种不需要在在最后一个内存单元加上'\0',编译系统会自动加。

(4)字符串赋值

为了明白字符串赋值的本质,这里不会使用库函数提供的各种函数,比如strcpy(),strcat()等。

#includeint main() {char string[12] = {'H', 'e', 'l', 'l', 'o', 'w',
                       'o', 'r', 'l', 'd', '!', '\0'};
    char string1[12] = {0};  // 初始化与否都可以,因为紧接着我们就要对其赋值
    for (int i = 0; i< 12; i++) {string1[i] = string[i];
    }
    printf("%s\n", string1);

    return 0;
}

本例采用了逐个字符赋值的方法来完成对字符串的赋值。我们的主题是指针,那么可不可以像下面那样赋值:

#includeint main() {char string[12] = {'H', 'e', 'l', 'l', 'o', 'w',
                       'o', 'r', 'l', 'd', '!', '\0'};
    char string1[12] = {0};  // 初始化与否都可以,因为紧接着我们就要对其赋值
    string1 = string;
    printf("%s\n", string1);

    return 0;
}

当然不可以,前文已经提到,数组名只一个地址常量,既然是常量当然就不可以被修改,自然就不能当做左值。事实上,上文还提到了通过字面值常量来创造字符串,既然是常量,那么自然也就不能修改,比如:

#includeint main() {char* p = "Helloworld!";
    *(p + 1) = 'E';
    printf("%s\n", p);
    return 0;
}

这个程序的目的是把"Helloworld"改为"HElloworld",那么目的能达到吗?自然不能,编译器会报错bus error。我们不去管这个错误的具体含义,只需要知道常量字符串是无法修改的。也就是说,我们的指针并不是指在哪里就改哪里,那计算机系统就乱套了!

如果我们迫切的想要修改,可以这样做:

#include#include#includeint main() {char* p = "Helloworld!";
    int len = strlen(p);
    char* newString = (char*)malloc(sizeof(char) * (len + 1));
    for (int i = 0; i< len; i++) {newString[i] = p[i];
    }
    newString[len] = '\0';
    newString[1] = 'E';
    printf("%s\n", newString);

    return 0;
}

输出结果为:

HElloworld!

过程也很简单,就是申请一块内存空间,先复制过来,然后再修改成想要的样子。

(五)★★★★★

开始之前,我们先来谈谈什么是匿名数组。顾名思义,匿名就是藏起来名字的意思。比如:

#includeint main() {int* p = (int[2]){19, 20};
    return 0;
}

我们定义了一个匿名的数组,并赋给一个int*类型的变量 p。匿名类在 Java 中 的作用之一是起到很好的封装性,同样在 C语言中也有这样的作用。这个数组,只能通过 p 访问。你可能注意到了,我并没有用int类型的指针去描述 p,而是用int*类型来描述 p。

(1)数据类型

C语言中的数据类型有哪些?一般来说,有四大类型,分别是基本类型,构造类型,空类型以及我们讲的指针类型。
在这里插入图片描述

指针类型是花样最多的一种类型,它通常包括 5 类:int*char*int**int(*)[]int*[]
在这里插入图片描述

换句话说,从语法的角度讲,把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。比如,给出一个例子:

#includeint main() {int a[3] = {1, 2, 3};

    int* p;
    char* s;
    int** q;
    int(*pt)[3];
    int*[3];

    return 0;
}

在这个例子中,指针的类型分别为:int*char*int**int(*)[3]int*[3]。我们经常用到数组指针以及指针数组,因此重点理解它们就是关键。

(2)指针数组

对于指针数组,描述它的语法为int*p[3]。首先,它是一个数组,所以得是个p[3];其次要是一个指针类型的也就是int*。所以就产生了int*p[3]。数组 p 中每一个元素都是一个指针,指向一个一维数组。比如:

#includeint main() {int a[3] = {1,2,3};
    int b[3] = {4,5,6};
    int c[5] = {7,8,9,10,11};

    int *p[3] = {a,b,c};

    return 0;
}

为了更明显,不如把它写成int*(p[3]),但是由于[3]的结合度非常高,因此这个括号可以去掉。

(3)数组指针

顾名思义,数组指针就是一个指向数组的指针。我们这样定义:·int(*p)[3]·。首先得是一个指针,因此得是(int*p);然后得是一个数组类型的,因此就是(int*p)[3]。但这样明显歧义了,因此就成了int(*p)[3]。它指向一个类型为int[3]的变量。比如:

#includeint main() {int a[3] = {1,2,3};
    int (*p)[3] = &a;
}

a 的数据类型为int[3],p 的类型为int(*)[3]。要想让 p指向 a,我们只需要把变量 a 的地址赋值给 p,即int (*p)[3] = &a。为了更明显,我们可以这样修改一下程序:

#includeint main() {int a[3] = {1,2,3};
    int b[4] = {1,2,3,4};
    int (*p)[3];
    p = &a;
    p = &b;
}

可以看到这样的报错:incompatible pointer types assigning to 'int (*)[3]' from 'int (*)[4]' [-Wincompatible-pointer-types]。意思是从“int (*)[4]”[-Wincompatible-pointer-types] 分配给“int (*)[3]”的不兼容指针类型。这也从侧面反映了int(*)[3]是一种确定的指针类类型。

能看到这里,想必你已经功力深厚,返璞归真了,哈哈~

三、总结

这里只总结难点:

  • 对于声明一个指针数组int*p[3],这里的重点是 p 的数据类型为int*[3],数组每一个元素都是一个指针,指向int 类型的数组
  • 对于声明一个数组指针int(*p)[3],这里的重点是 p 的数据类型为int(*)[3],p 是一个指针,指向类型为 int[3]的变量

全文完,感谢你的阅读。

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


网页标题:C语言指针——从入门到精通-创新互联
文章源于:http://myzitong.com/article/dieopp.html