C语言归纳七-指针

目录

一、指针简介

二、定义指针变量

三、C语言数组指针详解

划重点:

自加优先级大于*优先级大于+优先级。

四、C语言字符数组和字符串常量指针详解

五、C语言字符串数组理解小检测

注意,数组初始化的时候,char str[20] = {0};是将数组初始化全部为'\0',int str[20]={0};是初始化全部为0。

用数组作为函数参数的话,实际上还是用数组指针作为函数参数,传递地址而不是内存拷贝会节省时间。

六、C语言指针作为返回值

七、C语言二级指针(指向指针的指针)详解,直接看最下面的图

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

8.1 NULL指针

8.2 void指针

九、C语言指针数组(数组每个元素都是指针)详解

9.1 字符串指针数组

9.2 玩转指针数组和二级指针

十、C语言(二维)数组指针(指向二维数组的指针)详解

10.1  数组指针,针对二维

十一、函数指针

十二、挑战一下最复杂的指针模式

十三、main()函数的高级用法:接收用户输入的数据


一、指针简介

我们将内存中字节的编号称为地址(Address)或指针(Pointer)。地址从 0 开始依次增加,对于 32 位环境,程序能够使用的内存为 4GB,最小的地址为 0,最大的地址为 0XFFFFFFFF。

下面的代码演示了如何输出一个地址:

#include <stdio.h>
int main(){
    int a = 100;
    char str[20] = "c.biancheng.net";
    printf("%#X, %#X\n", &a, str);
    return 0;
}

运行结果:
0X28FF3C, 0X28FF10

%#X表示以十六进制形式输出,并附带前缀0X。a 是一个变量,用来存放整数,需要在前面加&来获得它的地址;str 本身就表示字符串的首地址,不需要加&

C语言中有一个控制符 %p,专门用来以十六进制形式输出地址,不过 %p 的输出格式并不统一,有的编译器带 0x前缀,有的不带,所以此处我们并没有采用。

变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。

二、定义指针变量

datatype *name;

int a = 100;
int *p_a = &a;

//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;
定义指针变量时必须带*,给指针变量赋值时不能带*。

指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:
#include <stdio.h>
int main(){
    int a = 15, b = 99, c = 222;
    int *p = &a;  //定义指针变量
    *p = b;  //通过指针变量修改内存上的数据
    c = *p;  //通过指针变量获取内存上的数据
    printf("%d, %d, %d, %d\n", a, b, c, *p);
    return 0;
}
运行结果:
99, 99, 99, 99

*&a可以理解为*(&a)&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),

绕来绕去,又回到了原点,*&a仍然等价于 a。

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

所以&*pa等价于 pa。

三、C语言数组指针详解

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等价于arr[i]
    }
    printf("\n");
    return 0;
}
运行结果:
99  15  100  888  252

上面的arr是常量。数组指针是变量。

如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。

数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *

反过来想,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。

更改上面的代码,使用数组指针来遍历数组元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
    for(i=0; i<len; i++){
        printf("%d  ", *(p+i) );
    }
    printf("\n");
    return 0;
}

因为 p 只是一个指向 int 类型的指针,编译器并不知道它指向的到底是一个整数还是一系列整数(数组),

所以 sizeof(p) 求得的是 p 这个指针变量本身所占用的字节数,而不是整个数组占用的字节数

下面是上面p的解释思想体现。

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int *p = &arr[2];  //也可以写作 int *p = arr + 2;
    printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
    return 0;
}

划重点:

更改上面的代码,借助自增运算符来遍历数组元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
    for(i=0; i<len; i++){
        printf("%d  ", *p++ );
    }
    printf("\n");
    return 0;
}
运行结果:
99  15  100  888  252

第 8 行代码中,*p++ 应该理解为 *(p++),每次循环都会改变 p 的值(p++ 使得 p 自身的值增加),以使 p 指向下一个数组元素。该语句不能写为 *arr++,因为 arr 是常量,而 arr++ 会改变它的值,这显然是错误的

自加优先级大于*优先级大于+优先级。

四、C语言字符数组和字符串常量指针详解

字符数组归根结底还是一个数组,上节讲到的关于指针和数组的规则同样也适用于字符数组。更改上面的代码,使用指针的方式来输出字符串:
#include <stdio.h>
#include <string.h>
int main(){
    char str[] = "http://c.biancheng.net";
    char *pstr = str;
    int len = strlen(str), i;
    //使用*(pstr+i)
    for(i=0; i<len; i++){
        printf("%c", *(pstr+i));
    }
    printf("\n");
    //使用pstr[i]
    for(i=0; i<len; i++){
        printf("%c", pstr[i]);
    }
    printf("\n");
    //使用*(str+i)
    for(i=0; i<len; i++){
        printf("%c", *(str+i));
    }
    printf("\n");
    return 0;
}
运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net

除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:

char *str = "http://c.biancheng.net";
或者:
char *str;
str = "http://c.biancheng.net";

字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0  个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *。

两种表示方法最根本的区别是在内存中的存储区域不一样。

字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。

全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,

而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限

用代码解释一下:

#include <stdio.h>
int main(){
    char *str = "Hello World!";
    str = "I love C!";  //正确
    str[3] = 'P';  //错误
    return 0;
}
这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。

第4行代码是正确的,可以更改指针变量本身的指向;
第5行代码是错误的,不能修改字符串中的字符。

最好应用字符数组表示字符串,不要用字符串常量表示。

五、C语言字符串数组理解小检测

#include <stdio.h>
int main(){
    char str[20] = "c.biancheng.net";
   
    char *s1 = str;
    char *s2 = str+2;
   
    char c1 = str[4];
    char c2 = *str;
    char c3 = *(str+4);
    char c4 = *str+2;
    char c5 = (str+1)[5];
   
    int num1 = *str+2;
    long num2 = (long)str;
    long num3 = (long)(str+2);

    printf("  s1 = %s\n", s1);
    printf("  s2 = %s\n", s2);
    printf("  c1 = %c\n", c1);
   
   
    printf("num1 = %d\n", num1);
    printf("num2 = %ld\n", num2);
    printf("num3 = %ld\n", num3);
    return 0;
}
  运行结果:
  s1 = c.biancheng.net
  s2 = biancheng.net
  c1 = a
  c2 = c
  c3 = a
  c4 = e
  c5 = c
  num1 = 101
划重点:
num2 = 2686736
num3 = 2686738
//要是Int类型就不是+2了,是加8。

注意,数组初始化的时候,char str[20] = {0};是将数组初始化全部为'\0',int str[20]={0};是初始化全部为0。

用数组作为函数参数的话,实际上还是用数组指针作为函数参数,传递地址而不是内存拷贝会节省时间。

六、C语言指针作为返回值

总结而言,函数的内存被主函数弃用,但不是清空和不被其他线程应用覆盖。

七、C语言二级指针(指向指针的指针)详解,直接看最下面的图

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

8.1 NULL指针

C语言没有一种机制来保证指向的内存的正确性。

记住下面代码是错误的,因为它没有初始化指针指向。

#include <stdio.h>
int main(){
    char *str;
    gets(str);
    printf("%s\n", str);
    return 0;
}

强烈建议对没有初始化的指针赋值为 NULL,例如:

char *str = NULL;

很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作,或者给出提示信息。

  • gets() 不会让用户输入字符串,也不会向指针指向的内存中写入数据;
  • printf() 不会读取指针指向的内容,只是简单地给出提示,让程序员意识到使用了一个空指针。

从整体上来看,NULL 指向了地址为 0 的内存,而不是前面说的不指向任何数据。

在进程的虚拟地址空间中,最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。

关于虚拟地址空间的概念以及程序的内存分布,我们将在《C语言内存精讲》专题中深入讲解,现在读者只需要记住,在大多数操作系统中,极小的地址通常不保存数据,也不允许程序访问,NULL 可以指向这段地址区间中的任何一个地址。

8.2 void指针

void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。

void *表示一个有效指针,它确实指向实实在在的数据,

只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。

C语言动态内存分配函数 malloc() 的返回值就是void *类型,在使用时要进行强制类型转换,请看下面的例子:

#include <stdio.h>
int main(){
    //分配可以保存30个字符的内存,并把返回的指针转换为 char *
    char *str = (char *)malloc(sizeof(char) * 30);
    gets(str);
    printf("%s\n", str);
    return 0;
}

运行结果:
c.biancheng.net↙
c.biancheng.net

九、C语言指针数组(数组每个元素都是指针)详解

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。代码如下:

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定义一个指针数组
    int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
    //定义一个指向指针数组的指针
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}
运行结果:
16, 932, 100
16, 932, 100

9.1 字符串指针数组

指针数组还可以和字符串数组结合使用,请看下面的例子:

#include <stdio.h>
int main(){
    char *str[3] = {
        "c.biancheng.net",
        "C语言中文网",
        "C Language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

运行结果:
c.biancheng.net
C语言中文网
C Language

字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的。

理解一下:

#include <stdio.h>
int main(){
    char *str0 = "c.biancheng.net";
    char *str1 = "C语言中文网";
    char *str2 = "C Language";
    char *str[3] = {str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

9.2 玩转指针数组和二级指针

#include <stdio.h>
int main(){
    char *lines[5] = {
        "COSC1283/1284",
        "Programming",
        "Techniques",
        "is",
        "great fun"
    };
    char *str1 = lines[1];
    char *str2 = *(lines + 3);
    char c1 = *(*(lines + 4) + 6);
    char c2 = (*lines + 5)[5];
    char c3 = *lines[0] + 2;
    printf("str1 = %s\n", str1);
    printf("str2 = %s\n", str2);
    printf("  c1 = %c\n", c1);
    printf("  c2 = %c\n", c2);
    printf("  c3 = %c\n", c3);
    return 0;
}
运行结果:
str1 = Programming
str2 = is
  c1 = f
  c2 = 2
  c3 = E

char *lines[5]定义了一个指针数组,它的每个元素的类型都是char *

在表达式中使用 lines 时,它会转换为一个类型为char **的指针,lines 是二级指针,*(lines+i) 是一级指针,**(lines+i) 才是具体的字符。

还不理解,就去看:http://c.biancheng.net/view/vip_2021.html

十、C语言(二维)数组指针(指向二维数组的指针)详解

10.1  数组指针,针对二维

int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };

假设数组 a 中第 0 个元素的地址为 1000,那么每个一维数组的首地址如下图所示:
把二维数组拆解成一维数组

为了更好的理解指针和二维数组的关系,我们先来定义一个指向 a 的指针变量 p:

int (*p)[4] = a;

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。

[ ]的优先级高于*( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数指针。

代码如下:

#include <stdio.h>
int main(){
    int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
    int (*p)[4] = a;
    printf("%d\n", sizeof(*(p+1)));
    return 0;
}
运行结果:
16

可以很容易推出以下的等价关系:

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)

指针数组和二维数组指针有着本质上的区别:指针数组是一个数组,只是每个元素保存的都是指针,以上面的 p1 为例,在32位环境下它占用 4×5 = 20 个字节的内存。二维数组指针是一个指针,它指向一个二维数组,以上面的 p2 为例,它占用 4 个字节的内存。

十一、函数指针

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

它的定义方式的思想和数组指针很像。

函数指针的定义形式为:
returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerNmae 为指针名称,param list 为函数参数列表。
参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。

注意( )的优先级高于*,第一个括号不能省略。
如果写作returnType *pointerName(param list);
就成了函数原型,它表明函数的返回值类型为returnType *。
#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
    return a>b ? a : b;
}
int main(){
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);
    return 0;
}

运行结果:
Input two numbers:10 50↙
Max value: 50

十二、挑战一下最复杂的指针模式

http://c.biancheng.net/view/vip_2024.html

十三、main()函数的高级用法:接收用户输入的数据

#include <stdio.h>
int main(int argc, char *argv[]){
    int i;
    printf("The program receives %d parameters:\n", argc);
    for(i=0; i<argc; i++){
        printf("%s\n", argv[i]);
    }
    return 0;
}

将生成后的程序放在D:\demo目录下,命名为main.exe,打开 cmd(命令提示符程序),输入D:\demo\main.exe C语言中文网 c.biancheng.net C-Lang,程序的运行结果如下:

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值