C语言 字符串指针,指针作为函数返回值(指针函数),二级指针,空指针,void指针

继续上回指针的知识整理

字符串指针

  1. 我们将字符串一般都放入字符数组。同意字符串指针也可以像数组指针一样可以进行遍历等或者和hash表一起使用。
  2. 字符数组归根结底还是一个数组,关于指针和数组的规则同样也适用于字符数组。

下面展示在数组中的遍历方式:

1.#include <stdio.h>
2. #include <string.h>
3.
4. int main(){
5. char str[] = "http://c.biancheng.net";
6. char *pstr = str;
7. int len = strlen(str), i;
8.
9. //使用*(pstr+i)
10. for(i=0; i<len; i++){
11. printf("%c", *(pstr+i));
12. }
13. printf("\n");
14. //使用pstr[i]
15. for(i=0; i<len; i++){
16. printf("%c", pstr[i]);
17. }
18. printf("\n");
19. //使用*(str+i)
20. for(i=0; i<len; i++){
21. printf("%c", *(str+i));
22. }
23. printf("\n");
24.
25. return 0;
26. }
运行结果:
http://c.biancheng.net
http://c.biancheng.net
http://c.biancheng.net

我们可以看到三种打印结果是相同的,和数组效果中的使用方式相同,进一步印证的上述观点。

除了字符数组,C 语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
char *str = "[http://c.biancheng.net](http://c.biancheng.net/)";
或者:
char *str;
str = "[http://c.biancheng.net](http://c.biancheng.net/)";
字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0 个字符的地址
称为字符串的首地址。字符串中每个字符的类型都是 char,所以 str 的类型也必须是 char *

3.以上方法都可以使用%s 输出整个字符串,都可以使用*或[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢? (存储位置不同,权限不同)
有!它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,第二种形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

  注意:内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于第二种形式的字符串,一旦被定义后就***只能读取不能修改***,任何对它的***赋值都是错误的***
  1. 第二种形式的字符串 字符串常量

    字符串常量只能读取不能写入。请看下面的演示:

1. #include <stdio.h>
2. int main(){
3. char *str = "Hello World!";
4. str = "I love C!"; //正确
5. str[3] = 'P'; //错误
6.
7. return 0;
8. }

这段代码能够正常编译和链接,但在运行时会出现段错误(Segment Fault)或者写入位置错误。

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

到底用哪一种形式???

答:只涉及到对字符串的读取,字符数组和字符串常量都能够满足要求;

   有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。

总结

语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的
存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改

C语言数组灵活多变的访问形式

接代码!!!

1. #include <stdio.h>
2.
3. int main(){
4. char str[20] = "c.biancheng.net";
5. 
6. char *s1 = str;
7. char *s2 = str+2;
8. 
9. char c1 = str[4];
10. char c2 = *str;
11. char c3 = *(str+4);
12. char c4 = *str+2;
13. char c5 = (str+1)[5];
14. 
15. int num1 = *str+2;
16. long num2 = (long)str;
17. long num3 = (long)(str+2);
18.
19. printf(" s1 = %s\n", s1);
20. printf(" s2 = %s\n", s2);
21.
22. printf(" c1 = %c\n", c1);
23. printf(" c2 = %c\n", c2);
24. printf(" c3 = %c\n", c3);
25. printf(" c4 = %c\n", c4);
26. printf(" c5 = %c\n", c5);
27. 
28. printf("num1 = %d\n", num1);
29. printf("num2 = %ld\n", num2);
30. printf("num3 = %ld\n", num3);
31.
32. return 0;
33. }
运行结果:
 s1 = c.biancheng.net
 s2 = biancheng.net
 c1 = a
 c2 = c
 c3 = a
 c4 = e
 c5 = c
num1 = 101
num2 = 2686736
num3 = 2686738
  1. str 既是数组名称,也是一个指向字符串的指针;指针可以参加运算,加 1 相当于数组下标加 1。
    printf() 输出字符串时,要求给出一个起始地址,并从这个地址开始输出,直到遇见字符串结束标志\0。s1 为
    字符串 str 第 0 个字符的地址,s2 为第 2 个字符的地址,所以 printf() 的结果分别为 c.biancheng.net
    biancheng.net
  2. 指针可以参加运算,str+4 表示第 4 个字符的地址,c3 = *(str+4) 表示第 4 个字符,即 ‘a’。
  3. 其实,数组元素的访问形式可以看做 address[offset],address 为起始地址,offset 为偏移量:c1 = str[4]
    表示以地址 str 为起点,向后偏移 4 个字符,为 ‘a’;c5 = (str+1)[5]表示以地址 str+1 为起点,向后偏移 5 个
    字符,等价于 str[6],为 ‘c’。
  4. 字符与整数运算时,先转换为整数(字符对应的 ASCII 码)。num1 与 c4 右边的表达式相同,对于 num1,
    *str+2 == ‘c’+2 == 99+2 == 101,即 num1 的值为 101,对于 c4,101 对应的字符为 ‘e’,所以 c4 的
    输出值为 ‘e’。
  5. num2 和 num3 分别为字符串 str 的首地址和第 2 个元素的地址。
1. #include <stdio.h>
2. #include <stdlib.h>
3.
4. int main(){
5. char str[20] = {0};
6. int i;
7.
8. for(i=0; i<10; i++){
9. *(str+i) = 97+i; // 97 为字符 a 的 ASCII 码值
10. }
11. 
12. printf("%s\n", str);
13. printf("%s\n", str+2);
14. printf("%c\n", str[2]);
15. printf("%c\n", (str+2)[2]);
16. 
17. return 0;
18. }
运行结果:
	abcdefghij
	cdefghij
	ce

第 5 行代码用来将字符数组中的所有元素都初始化为\0,这样在循环结束时就无需添加字符串结束标志。
前面三个 printf() 比较容易理解,第四个 printf() 可以参照上面的说明 3),str+2 表示指向第 2 个元素,
(str+2)[2] 相当于 *(str+2+2),也就是取得第 4 个元素的值。

指针变量作为函数参数

为什么要用指针变量作为函数的参数?

可以将函数外部的地址传递到函数内部,使得在函数内部可以操作 函数外部的数据,并且这些数据不会随着 函数的结束而被销毁

典型例子:swap()

1. #include <stdio.h>
2.
3. void swap(int *p1, int *p2){
4. int temp; //临时变量
5. temp = *p1;
6. *p1 = *p2;
7. *p2 = temp;
8. }
9.
10. int main(){
11. int a = 66, b = 99;
12. swap(&a, &b);
13. printf("a = %d, b = %d\n", a, b);
14. return 0;
15. }
1. #include <stdio.h> 
2.
3. void swap(int a, int b){
4. int temp; //临时变量 
5. temp = a;
6. a = b; 
7. b = temp; 
8. }
9.
10. int main(){
11. int a = 66, b = 99;
12. swap(a, b);
13. printf("a = %d, b = %d\n", a, b);
14. return 0;
15. }

运行结果:
a = 99, b = 66
调用 swap() 函数时,将变量 a、b 的地址分别赋值给 p1、p2,这样 *p1、p2 代表的就是变量 a、b 本身,交换p1、p2 的值也就是交换 a、b 的值。函数运行结束后虽然会将 p1、p2 销毁,但它对外部 a、b 造成的影响是“持久化”的,不会随着函数的结束而“恢复原样”。
需要注意的是临时变量 temp,它的作用特别重要,因为执行
p1 = *p2;语句后 a 的值会被 b 的值覆盖,如果不先将 a 的值保存起来以后就找不到了

运行结果:
a = 66, b = 99
从结果可以看出,a、b 的值并没有发生改变,交换失败。这是因为 swap() 函数内部的 a、b 和 main() 函数内部的 a、b 是不同的变量,占用不同的内存,它们除了名字一样,没有其他任何关系,swap() 交换的是它内部 a、b
的值,不会影响它外部(main() 内部) a、b 的值。

注意: 如果想在函数内改变函数外(或反过来)的变量还可以用全局变量,但是太过浪费内存资源不建议使用。

用数组作函数参数

数组是一系列数据的集合,无法通过参数将它们一次性传递到函数内部,如果希望在函数内部操作数组,必须传递数组指针。 数组不像是全局变量在哪都可以调用修改,没办法一口气进来,更不能随意修改。

定义一个max()函数来查找一组数字的最大值:

1. #include <stdio.h>
2.
3. int max(int *intArr, int len){
4.     int i, maxValue = intArr[0]; //假设第0个元素是最大值
5.      for(i=1; i<len; i++){
6.       if(maxValue < intArr[i]){
7.          maxValue = intArr[i];
8.      }
9. }
10. 
11. return maxValue;
12. }
13.
14. int main(){
15. int nums[6], i;
16. int len = sizeof(nums)/sizeof(int);
17. //读取用户输入的数据并赋值给数组元素
18. for(i=0; i<len; i++){
19. scanf("%d", nums+i);
20. }
21. printf("Max value is %d!\n", max(nums, len));
22.
23. return 0;
24. }
运行结果:
12 55 30 8 93 27↙
Max value is 93!

参数 intArr 仅仅是一个数组指针,在函数内部无法通过这个指针获得数组长度,必须将数组长度作为函数参数传递到函数内部。数组 nums 的每个元素都是整数,scanf() 在读取用户输入的整数时,要求给出存储它的内存的地址,nums+i 就是第 i 个数组元素的地址.

1. int max(int *intArr[6], int len){
2. int i, maxValue = intArr[0]; //假设第0个元素是最大值
3. for(i=1; i<len; i++){
4. if(maxValue < intArr[i]){
5. maxValue = intArr[i];
6. }
7. }
8. return maxValue;
9. }

int *intArr[6]这种形式只是一个期望值,并不意味着数组只能有 6 个元素,真正传递
的数组可以有少于或多于 6 个的元素。所以一般只定义不赋值,形如 int *nums

不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度 例如: int len或者

int numsSize

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

指针作为函数返回值(指针函数)

什么是指针函数?

**允许函数的返回值是一个指针(地址)**,我们将这样的函数称为指针函数。
/*
*下面的例子定义了一个函数 strlong(),
* 用来返回两个字符串中较长的一个:
*/
1. #include <stdio.h>
2. #include <string.h>
3.
4. char *strlong(char *str1, char *str2){
5.    if(strlen(str1) >= strlen(str2))
        {
6.       return str1;
7.      }
      else
        {
8.        return str2;
9.      }
10. }
11.
12. int main(){
13.   char str1[30], str2[30], *str;
14.   gets(str1);
15.   gets(str2);
16.   str = strlong(str1, str2);
17.   printf("Longer string: %s\n", str);
18.
19.   return 0;
20. }
运行结果:
C Language↙
c.biancheng.net↙
Longer string: c.biancheng.net

指针作为函数返回值时需要注意的一点:

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

进一步解释:

1. #include <stdio.h>
2.
3. int *func(){
4.    int n = 100;
5.    return &n;  //返回n的地址
6. }
7.  //函数的思想是将func函数中n地址内的内容给main中定义的int n  ,判断n的值
8. int main(){     
9.     int *p = func(), n;
10.    printf("c.biancheng.net\n");//可以删减掉进行对比查看
11.    n = *p;
12.    printf("value = %d\n", n);
13.    return 0;
14. }
运行结果:
c.biancheng.net
value = -2

为什么会这样?

可以看到,现在 p 指向的数据已经不是原来 n 的值了,它变成了一个毫无意义的甚至有些怪异的值?
前面我们说函数运行结束后会销毁所有的局部数据,这个观点并没错,大部分 C 语言教材也都强调了这一点。但是,这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。

 如果没有    printf("c.biancheng.net\n");      值还是 100,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。

二级指针(指向指针的指针)

  1. 一上来就是指向指针的指针一听起来就头大,但其实指针的原理就像是剥洋葱,从外向内逐层进入,既可以改变数据又可以获取数据,在C语言的学习中有着不可或缺的地方,学好c语言,第一道难关便是指针,无论是在后续深入的学习,还是我们 的代码中你看到指针就知道这一看就是有技术的,而不是只会全局变量,既浪费资源又低级。
  2. 指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。
    如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
    假设有一个 int 类型的变量 a,p1 是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f25reltE-1663674657834)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2b2249df-bbd0-4fd4-b419-162bde9eb0e1/Untitled.png)]

1. int a =100;
2. int *p1 = &a;
3. int **p2 = &p1;
/*代码形式就是这样,如果更高级的指针也同样是这样的道理,
不过高级指针一般不用,不是因为难,最大的原因是太过浪费资源,你每次调用都需要跑一遍,
无论是cpu还是内存
都是高昂的消费*/

指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。C 语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。p1 是一级指针,指向普通类型的数据,定义时有一个*;p2 是二级指针,指向一级指针 p1,定义时有两个*。

1. #include <stdio.h>
2.
3. int main(){
4. int a =100;
5. int *p1 = &a;
6. int **p2 = &p1;
7. int ***p3 = &p2;
8.
9. printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
10. printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
11. printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
12. printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
13. return 0;
14. }

空指针(NULL)和void 指针

空指针NULL

一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C 语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。

程序员:我想怎么指就怎么指!

电脑:你☞啥?信不信我给你崩一个!!!

1. #include <stdio.h>
2.
3. int main(){
4. char *str;
5. gets(str);
6. printf("%s\n", str);
7. return 0;
8. }

NULL 是“零值、等于零”的意思,在 C 语言中表示空指针。从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。

这段程序没有语法错误,能够通过编译和链接,但当用户输入完字符串并按下回车键时就会发生错误,在 Linux下表现为段错误(Segment Fault),下面的代码错误相同

很多库函数都对传入的指针做了判断,如果是空指针就不做任何操作,或者给出提示信息。更改上面的代码,给 str 赋值 NULL,看看会有什么效果:

1. #include <stdio.h>
2.
3. int main(){
4. char *str = NULL;
5. gets(str);
6. printf("%s\n", str);
7. return 0;
8. }

运行程序后发现,还未等用户输入任何字符,printf() 就直接输出了(null)。我们有理由据此推断,gets() 和printf() 都对空指针做了特殊处理:
Linux Ubuntu 下 gets() 会让用户输入字符串,也不会向指针指向的内存中写入数据;
printf() 不会读取指针指向的内容,只是简单地给出提示,让程序员意识到使用了一个空指针

直接报错Segment Fault

总结:

注意事项整理:
1,空指针

空指针可以等于任何类型的指针 ,变量值是NUL L,实质是((void *)0)

2,坏指针

注意⚠️  指针变量是NULL 或是未知地址,导致程序意外终止。导致c语言bug的重要原因之一。

3,注意⚠️  对于指针 int型指向,即使数据一个字节就够了,但是依然会战占据四个字节。 0010 0000 0000 0000。其他类型数据原理相似。

4 ,void * 类型的指针 void * 不能进行解指针操作会进行崩溃。

5,传递指针的重要功能就是避免拷贝大型数据 。因为成本高效率大打折扣。例如1kb等

不允许把一个数赋值给指针变量

例如 int *p; p=1000; *p=&a; 错误的赋值

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

芯片烧毁大师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值