继续上回指针的知识整理
字符串指针
我们将字符串一般都放入字符数组。同意字符串指针也可以像数组指针一样可以进行遍历等或者和hash表一起使用。 字符数组归根结底还是一个数组,关于指针和数组的规则同样也适用于字符数组。
下面展示在数组中的遍历方式:
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.
10. for ( i= 0 ; i< len; i++ ) {
11. printf ( "%c" , * ( pstr+ i) ) ;
12. }
13. printf ( "\n" ) ;
14.
15. for ( i= 0 ; i< len; i++ ) {
16. printf ( "%c" , pstr[ i] ) ;
17. }
18. printf ( "\n" ) ;
19.
20. for ( i= 0 ; i< len; i++ ) {
21. printf ( "%c" , * ( str+ i) ) ;
22. }
23. printf ( "\n" ) ;
24.
25. return 0 ;
26. }
运行结果:
http:
http:
http:
我们可以看到三种打印结果是相同的,和数组效果中的使用方式相同,进一步印证的上述观点。
除了字符数组,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. #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
str 既是数组名称,也是一个指向字符串的指针;指针可以参加运算,加 1 相当于数组下标加 1。 printf() 输出字符串时,要求给出一个起始地址,并从这个地址开始输出,直到遇见字符串结束标志\0。s1 为 字符串 str 第 0 个字符的地址,s2 为第 2 个字符的地址,所以 printf() 的结果分别为 c.biancheng.net 和 biancheng.net 。 指针可以参加运算,str+4 表示第 4 个字符的地址,c3 = *(str+4) 表示第 4 个字符,即 ‘a’。 其实,数组元素的访问形式可以看做 address[offset],address 为起始地址,offset 为偏移量:c1 = str[4] 表示以地址 str 为起点,向后偏移 4 个字符,为 ‘a’;c5 = (str+1)[5]表示以地址 str+1 为起点,向后偏移 5 个 字符,等价于 str[6],为 ‘c’。 字符与整数运算时,先转换为整数(字符对应的 ASCII 码)。num1 与 c4 右边的表达式相同,对于 num1, *str+2 == ‘c’+2 == 99+2 == 101,即 num1 的值为 101,对于 c4,101 对应的字符为 ‘e’,所以 c4 的 输出值为 ‘e’。 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;
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 ] ;
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 ] ;
3. for ( i= 1 ; i< len; i++ ) {
4. if ( maxValue < intArr[ i] ) {
5. maxValue = intArr[ i] ;
6. }
7. }
8. return maxValue;
9. }
💡 int intArr[]虽然定义了一个数组,但*没有指定数组长度,好像可以接受任意长度的数组*。 **实际上这两种形式的数组定义都是假象,不管是 int intArr[6]还是 int intArr[]都不会创建一个数组出来**,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为 int *intArr 这样的指针。这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来。 没有省事那一说!
int *intArr[6]这种形式只是一个期望值,并不意味着数组只能有 6 个元素,真正传递 的数组可以有少于或多于 6 个的元素。所以一般只定义不赋值,形如 int *nums
不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度 例如: int len或者
int numsSize
参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
指针作为函数返回值(指针函数)
什么是指针函数?
**允许函数的返回值是一个指针(地址)**,我们将这样的函数称为指针函数。
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;
6. }
7.
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,如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。
二级指针(指向指针的指针)
一上来就是指向指针的指针一听起来就头大,但其实指针的原理就像是剥洋葱,从外向内逐层进入,既可以改变数据又可以获取数据,在C语言的学习中有着不可或缺的地方,学好c语言,第一道难关便是指针,无论是在后续深入的学习,还是我们 的代码中你看到指针就知道这一看就是有技术的,而不是只会全局变量,既浪费资源又低级。 指针可以指向一份普通类型的数据,例如 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;
指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。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 语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。
程序员:我想怎么指就怎么指!
电脑:你?啥?信不信我给你崩一个!!!
💡 在我们定义指针时,没有决定好指向时 就一直 int *nums;这是一个非常危险的行为,要么不定义,定义就赋值。 在不赋值的情况下要用NULL int *nums=NULL;
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; 错误的赋值