指针计划分成3篇来讲: 上篇是指针的基本使用 、 中篇指针与数组的联系、下篇是指针与字符串的联系。
此篇文章讲作为指针部分的终章,后续若还有指针分支内容要阐明,再另开一篇文章来说。但是经过这3篇文章的熏陶小伙伴们应该能够顺利吧这块难啃的指针给拿下。上回说到数组篇有着承上启下的作用,字符串其实与数组还有这千丝万缕的联系,因为在C语言的世界中,没有字符串类型,它使用的是数组来表示字符串。
一、字符串定义
在C语言的世界中没有其他编程语言的字符串类型,比如 java 有 String类型, c++ 有 string类型。C语言里面描述自付出其实很容易“淳朴”: 字符串嘛!就是把一堆字符 串联起来即可。那么扮演着这个角色的只能是数组了。因为C语言里面也没有集合。在定义字符串的时候,要求数组的最后一位必须有 '\0' 用来表示字符串截止了。
#include <stdio.h>
int main(){
//1. 方式1:以字符的方式定时数组,最后一位为 \0
char c1[] = {'a' , 'b' , 'c' , '\0'};
printf("c1=%s \n" , c1) ; // 打印: abc
//2.方式2: 直接使用 "" 方式赋值编译器会自动附加 \0
char c2[] = "abc";
printf("c2=%s \n" , c2) ; // 打印: abc
//3. 方式3: 使用字符指针方式
char * c3 = "abc";
printf("c3=%s \n" , c3) ; // 打印: abc
return 0;
}
二、指针与常量
这一节内容到底放在基础篇还是放在这里心里一直犹豫不决,放在基础篇会加重学习难度,有劝返大家的意思。放在这里有现学现卖的意思【因为后面两个小节用到啦~~】。思来想去,还是放在这里吧,毕竟来到最后一篇了,大家的学习能力应该有所提升了,对指针的认知应该也有了更深的理解。
正如以前多次提起过,指针再怎么特殊,其实它也是一种特殊的变量。C语言的世界里有变量也有常量之分,由于指针存在指向性(我们人为的人为)。所以可否在定义指针的时候,加上常量的约定呢?可以约定指针的指向性不允许修改,或者约定指向目标位置的值为常量(从指针的角度理解,让指针以为指向的目标数据是常量)
2.1 指向常量的指针
指向常量的指针语法为: const 指针类型 * 指针名称 = 地址;或 指针类型 const * 指针名称 = 地址;这个指针指向的是一个常量,即它所指向的那个目标位置的值是不可以修改的。但是它可以接收新的地址值,转而指向别的地方。但是注意产生新的指向之后,也不能使用解引用修改值。这种写法其实等同于从认知上欺骗指针,告诉它,它所指向的目标位置的值是常量,你不能修改它。【但是目标位置的值是不是常量不重要了。只是为了不允许解引用操作它而已。】
#include <stdio.h>
int main(){
//定义变量 number
int number = 10 ;
//定义指向常量的指针
const int * p = &number;
//尝试通过解引用修改number的值:: 会报错!
*p = 20;
printf("number的值是:%d\n" , number);
//尝试指向其他的变量地址
int number2 = 30;
//尝试指向新的地方 number2 不会报错!
p = &number2;
//尝试通过解引用修改number2的值:: 会报错!
*p = 40;
return 0;
}
2.2 常量指针
常量指针的语法为:指针类型 * const 指针名字 = 地址 ; 这个指针是一个常量此处的const关键字修饰的是指针。即它所接受的地址值是不允许修改的,即这个指针一辈子只能指向这个地方,不允许指向其他的地方了(因为它不能接受新的地址)。但是它所指向的那个目标位置的值是可以修改的。
#include <stdio.h>
int main(){
//定义变量 number
int number = 10 ;
//定义常量指针
int * const p = &number;
//尝试通过解引用修改number的值 不会报错!
*p = 20;
printf("number的值是:%d\n" , number);
//尝试指向其他的变量地址
int number2 = 30;
//尝试指向新的地方 number2 会报错!
p = &number2;
return 0;
}
2.3 常量指针指向常量
前面我们分别学习了常量指针和指向常量的(普通)指针, 但是这两种语法是可以结合起来运用的。一旦一起使用就会出现了常量指针指向常量的场景。它的语法是: const 指针类型 * const 指针名称 = 地址; 只是这种语法比较特殊,比较少见。它的意思即是结合了前面的两种定义的功能: 不允许修改值,不允许修改指向
#include <stdio.h>
int main(){
//定义变量 number
int number = 10 ;
//定义常量指针
const int * const p = &number;
//尝试通过解引用修改number的值 会报错!
*p = 20;
//尝试指向其他的变量地址
int number2 = 30;
//尝试指向新的地方 number2 会报错!
p = &number2;
return 0;
}
三、字符串与数组
有时候小伙伴们在使用数组的时候,习惯的把数组当成了变量,然后就把一个新的数组赋值给了当前的数组,这在C语言里面是不允许的。比如: 有一个字符串 char c[] = "abc"; 则不允许出现这样的代码: c="xyz"; 如果以前我们不太明白是什么原因,那么把上面的指针与常量学习过后,就很容易看穿它背后的真相了。
#include <stdio.h>
int main(){
//定义字符串
char c[] ="abc";
//允许修改数组中的元素
c[0] = 'd';
c[1] = 'e';
c[2] = 'f';
//但是不允许这种写法: 会报错!出现提示: assignment to expression with array type
c = "xyz";
return 0;
}
柯南有话说:
上述代码之所以报错可以从两个角度理解:
1. C 是数组,数组类型不允许我们完成这样的赋值工作。虽然是我们希望数组中的内容修改成 "xyz" 或者换句话说是我们希望把字符串从 "abc" 改变为 "xyz" ,这是不允许的。柯南猜想是无从得知我们赋值进来的字符串长度是否与原来的字符串长度保持一致... 这可能会造成数据丢失。并且就算能赋值进来数组存储数据的地址连续性无法得到保证。因为无法确保原来"abc" 旁边的地址还是空的,可以继续申请。
2. 数组名字 c 可以看成是一个指针,始终爱着首元素地址(咳~是始终指向首元素 a 的地址) 不允许修改指向。新产生的字符串 "xyz" 中的 x 有自己的地址区域。不允许让 c指向新的区域,它必须永远指向首元素。
3. 这看起来是否与前面的常量指针一样呢? int * cont p = 地址; 被永远固定指向在当前的位置,不允许修改指向,但是可以允许修改指向位置的值。
四、字符串与指针
虽然上面的数组式的字符串写法不允许直接修改成一个新的字符串,但是令我们始料不及的是,字符串指针的写法允许我们直接把一个新的字符串赋值给它。这背后又有着怎样的缘故呢?
#include <stdio.h>
int main(){
//定义字符串
char * c ="abc";
printf("c=%s\n" , c );
printf("c指向的位置:%#x \n" , c);
// 允许直接赋值成新的字符串
c = "xyz";
printf("c=%s\n" , c );
printf("c指向的位置:%#x \n" , c);
// 不允许修改内部元素的值
c[0] = 'q';
printf("c=%s\n" , c );
// 不允许修改内部元素的值
*c = 'q';
printf("c=%s\n" , c );
return 0;
}
柯南有话说:
1. 首先得科普一下:这种字符串指针的写法会把 "abc" 三个字符放在字符串常量区(涉及到内存管理的知识了),只需要知道这个文字常量区的内容一经生成就不允许修改。
2. 虽然能够执行 c= "xyz"; 这句话,但是这并不是去修改原来的 "abc" 的值,而是在一个文字常量区,开辟了一块区域存放 "xyz", 让指针 c 指向了这块区域而已。运行程序就会发现指针c所指向的位置已经发生了变化。
3. 后面使用 c[0] 是打算按照数组的方式取修改字符串中的第0个元素 a ,发现不允许!即便使用 *c 的方式取修改第0个元素a也不允许! 不过这两段代码需要分别注释掉对方之后再测试。
五、总结
至此指针三部曲就已经结束了,从最早的声明定义、解引用到指针的运算、指针与数组的联系,最后到指针与字符串的联动。章节由浅入深的,大家按照顺序学习下来,越来越难了,同时也解开了越来越多的谜底。三部曲虽然结束了,但是指针在整个C语言的世界里扮演着至关重要的角色,谁敢横刀立马,唯我指针大将军!指针与函数、指针与内存这些都没有阐明,之所以没有排进三部曲中,主要是考虑函数和内存的内容过大、加重大家的学习难度。后续若有需要再函数与内存的篇幅中再 穿插讲解啦~
最后公布一下上一篇最后的思考题答案:
1. 数组采用一串连续的地址存放数据,首先是考虑取数据的时候比较方便,表面看起来使用 []下标方式,程序元只需要记住下标即可,背后取数据可以使用指针经过偏移即可到达下一个元素的位置,其次分配内存的时候不会随意放置,对内存管理有的放失。
2. 不允许对数组进行一整个赋值,虽然看起来有快捷修改数组的嫌疑,但是数组名字天生就永远指向首元素的地址,不允许指向其他的数组。而且无法预知赋值进来的数组元素长度是否匹配原数组空间,所以这种语法不被认可!【编译不通过呀~】
3. 可以对数组的元素进行修改,但是无法对数组长度进行增删。数组长度一经申请就已固定,不允许增加或删减。原因是若要对数组扩容,无法保证后面衔接的地址还处于空闲状态(允许我们分配)。若是删减掉这个空间,那么退出来的这个空间或许不够系统在下一次分配给其他程序。