【c语言】指针的速记 指针的作用 函数指针,指针数组,二维数组指针……

[1] c语言指针
[2]《深入理解计算机系统》大黑书
[3] C语言字符串指针(指向字符串的指针)详解

声明:

自己看书和网页做的一些笔记,没有条理,随便摘录整理,加上一点自己的理解
自留用。

前言:一切都是地址

计算机所有的数据都必须放在内存中才能运行,这涉及到代码数据的链接加载编译和链接过程的一项重要任务就是找到这些名称所对应的地址。

实际上,计算机无法分别内存中到底是数据还是代码。当数据被加载到内存内存中后,代码就变成了程序(从静态变成了动态),操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块是数据

CPU 只能通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。如果程序不小心出错(例如栈越界,数组越界,编译底层的一些反编译带代码导致的核心数据被修改),或者开发者有意为之,在 CPU 要写入数据时给它一个代码区域的地址,就会发生内存访问错误。这种内存访问错误会被硬件和操作系统拦截,强制程序崩溃。

我们知道,程序在运行过程中,是不能改变它自身的代码的,不然会带来各方面问题,例如安全性。就像一个制造者制造的小婴儿能自己改造自己,这是很诡异的。

在C语言中,变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址

不同类型的数据占用的字节不一样。例如 int 占用 4 个字节,char 占用 1 个字节,指针也是一种类型,占用8字节。c语言的指针对每种类型,都有准确的字节数,例如8字节的 char* p 指向的数据,长度就是1字节的char c='K' ,p的值是11A,c的值是‘K’,地址是11A。
在这里插入图片描述

内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 64 位环境,64位系统的最大寻址空间为 2 64 2^{64} 264 bytes,计算后其可寻址空间达到了惊人的16TB(treabytes),即16384GB, 但是,实际上限于种种原因,目前Windows 7 64位版仅能使用最大为192GB。内存最小的地址为 0,最大的地址为 0XFFFF FFFF FFFF FFFF

如果学习过《编译原理》和《深入理解计算机系统》就能更好地理解这个问题,例如地址和寄存器如下图:
在这里插入图片描述

c=a+b
也就是
(0x108)=(0x100)+(0x104)

内存寻址和寄存器寻址一览:
在这里插入图片描述

*和&符号

指针类型:datatype *name=value;
例如

int a=100;
int* p =&a; //&是取地址符号

通过指针变量取得数据
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*pointer;

这里的*称为指针运算符,用来取得某个地址上的数据

int a=100;
int* p =&a; //&是取地址符号
printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值

*p和a是等价的,但是使用指针是间接获取数据,使用变量名是直接获取数据,指针比直接读取的代价要高。而且指针除了可以获取内存上的数据,也可以修改内存上的数据,

*在不同的场景下有不同的作用:*可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加*表示获取指针指向的数据,或者说表示的是指针指向的数据本身。

关于 * 和 & 的谜题

假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*&a&*pa分别是什么意思呢?

*&a可以理解为*(&a),&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。

&*pa可以理解为&(*pa)*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。

指针和类型绑定:

指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1。
这其实方便了数组中指针的操作,例如
int *a=(int *)malloc(sizeof(int)*20);就定义了长度为20的数组,a指向a[0]也就是数组起始地址,a++,指向a[1],实际上,指针大小加了4而不是1!

可以通过以下任何一种方式来访问 a[i]:

int a = {1, 2, 3, 4, 5}, *p, i = 2;
p = a;
p[i];

p = a;
*(p + i);

p = a + i;
*p;

注意1:数组到底在什么时候会转换为指针

arr 本身就是一个指针”这种表述并不准确,
严格来说应该是“arr 被转换成了一个指针”
1) 用 a[i] 这样的形式对数组进行访问总是会被编译器改写成(或者说解释为)像 *(a+i) 这样的指针形式。
2) 指针始终是指针,它绝不可以改写成数组。你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组
3) 在特定的环境中,也就是数组作为函数形参,也只有这种情况,一个数组可以看做是一个指针。作为函数形参的数组始终会被编译器修改成指向数组第一个元素的指针。
3) 当希望向函数传递数组时,可以把函数参数定义为数组形式(可以指定长度也可以不指定长度),也可以定义为指针。不管哪种形式,在函数内部都要作为指针变量对待。

注意2:数组和指针不等价

数组和指针不等价的一个典型案例就是求数组的长度,这个时候只能使用数组名,不能使用数组指针.
对于数组 a,它的类型是int [6],表示这是一个拥有 6int 数据的集合,1int 的长度为 46int 的长度为 4×6 = 24sizeof 很容易求得。
对于指针变量 p,它的类型是int *,在 32 位环境下长度为 4,在 64 位环境下长度为 8。
与普通变量名相比,数组名既有一般性也有特殊性:
一般性表现在数组名也用来指代特定的内存块,也有类型和长度;
特殊性表现在数组名有时候会转换为一个指针,而不是它所指代的数据本身的值。

关于数组指针的谜题

假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?

*p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素

*++p 等价于 *(++p),先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。

(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。

C语言字符串指针(指向字符串的指针)

[1] C语言字符串指针(指向字符串的指针)详解
字符串数组char str[]=”test“;char* str="test":
相同:
它们都可以使用%s输出整个字符串,都可以使用*或[ ]获取单个字符.
区别:
它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区char* str="test":的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。
内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于char* str="test":的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。称为字符串常量,如果进行了修改,例如:

#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "I love C!";  //正确,只是改变指针的指向
    str[3] = 'P';  //运行时会出现段错误(Segment Fault)或者写入位置错误,不能修改字符串中的字符
    return 0;
}

注意:
在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;如果有写入(修改)操作,那么只能使用字符数组, 不能使用字符串常量。
获取用户输入的字符串就是一个典型的写入操作,只能使用字符数组,不能使用字符串常量,

例题:
在这里插入图片描述
输出应该是多少?

答案:

str1 = Programming
解析: 因为lines 指向了字符串数组char* [5],所以lines[1] 指向了字符串数组的第二个串。
str2 = is
解析: 其实可以写作lines[3],所以是is
c1 = f
解析: 相当于lines[5][6]='f'
c2 = 2
解析: (*lines+5)[5] 相当于 对地址 (*lines+5+5)取值,相当于 lines[0][10]=2。注意,lines[0] 等价于*(lines + 0);
c3= E
解析: *lines[0] 等价于*(*(lines+0)) 所以*lines[0] +2 等价于 lines[0][0]的值加上2,也就是‘C’的ascII码加上 2 ,得到‘E’

C语言指针变量作为函数参数

参考网址:[1] C语言指针变量作为函数参数
不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

C语言为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?

参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。

对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。

指针作为函数返回值:

用指针作为函数返回值时需要注意的一点是 函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数 函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。

函数运行结束后会销毁所有的局部数据,大部分C语言教材也都强调了这一点。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限, 弃之不理,后面的代码可以随意使用这块内存。对于上面的两个例子,func() 运行结束后 n 的内存依然保持原样,值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。

C语言空指针NULL以及void指针

[1]C语言空指针NULL以及void指针

空指针NULL
在C语言中,如果一个指针不指向任何数据,我们就称之为空指针,用NULL表示。例如:

int *p = NULL;

随意 初始化p,例如:

int* p;//它不是空指针

它的值是随机的,是垃圾值,如果不小心使用了它,运行时一般会引起段错误,导致程序退出,甚至会不知不觉地修改数据。

void 指针
C语言还有一种void指针类型,即可以定义一个指针变量,但不说明它指向哪一种类型数据。例如:

void *p = malloc(2);

在内存中分配2个字节的空间,但不确定它保存什么类型的数据。

C语言指针与二维数组

参考:[1] C语言指针与二维数组
二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。
C语言中的二维数组是按行排列的,也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4) = 48 个字节。C语言允许把一个二维数组分解成多个一维数组来处理。对于数组 a,它可以分解成三个一维数组,即 a[0]、a[1]、a[2]。每一个一维数组又包含了 4 个元素,

例如 a[0] 包含 a[0][0]、a[0][1]、a[0][2]、a[0][3]。

*(*(p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a; //二维数组指针,不能去掉括号,去掉括号变成了指针数组

a+i == p+i
a[i] == p[i] == *(a+i) == *(p+i)
a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)

函数指针

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

//函数定义,返回两个数中较大的一个
int max(int a, int b){
    return a>b ? a : b;
}

 //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
     maxval = (*pmax)(x, y);//调用
不同指针的区分:

[1] 彻底攻克C语言指针

int *p1[6]; //指针数组
int *(p2[6]); //指针数组,和上面的形式等价
int (*p3)[6]; //二维数组指针
int (*p4)(int, int); //函数指针

符号优先级:

定义中被括号( )括起来的那部分。
后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
前缀操作符:星号*表示“指向xxx的指针”。

main和控制台输入

main() 是C语言程序的入口函数,有且只能有一个,它实际上有两种标准的原型:

int main();
int main(int argc, char *argv[]);

第二种原型在实际开发中经常使用,它能够让我们在程序启动时给程序传递数据。
一个程序在启动时允许系统或用户给它传递数据,Windows 和 Linux 都支持,这些数据以字符串的形式存在,多份数据之间以空格分隔。也就是说,用户输入的多份数据在程序中表现为多个字符串。
例如:

【实用程序】判断用户输入的是否是素数。 
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
int isPrime(int n);
int main(int argc, char *argv[]){
 int i, n, result;
 if(argc <= 1){
 printf("Error: no input integer!\n");
 exit(EXIT_SUCCESS);
 }
 for(i=1; i<argc; i++){
 n = atoi(argv[i]);
 result = isPrime(n);
 if(result < 0){
 printf("%3d is error.\n", n);
 }else if(result){
 printf("%3d is prime number.\n", n);
 }else{
 printf("%3d is not prime number.\n", n);
 }
 }
 return 0;
}
//判断是否是素数
int isPrime(int n){
 int i, j;
 if(n <= 1){ //参数错误
 return -1;
 }else if(n == 2){ //2是特例,单独处理
 return 1;
 }else if(n % 2 == 0){ //偶数不是素数
 return 0;
 }else{ //判断一个奇数是否是素数
 j = (int)sqrt(n);
 for(i=3; i<=j; i+=2){
 if (n % i == 0){
 return 0;
 }
 }
 return 1;
 }
}

在windows下直接运行这段代码,
在这里插入图片描述

main(int argc,char *argv[ ])

1.argc为整数
2.argv为指针的指针(可理解为:char **argv or: char *argv[] or: char argv[][] ,argv是一个指针数组)

argc=2,表示除了程序名外还有1个参数。
argv[0]指向输入的程序路径及名称。
argv[1]指向参数para_1字符串

argc是命令行解析器自动计算的,不需要输入,只需要输入argv[1] 就行了,argv[0]是路径和程序名

总结:

[1] 指针总结

  1. 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。
  2. 指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关,例如 int 类型就会自动加4
  3. 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃。
  4. 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL。
  5. 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值