C语言08--指针数组结合

前言:

这次的指针数组结合乃作者呕心沥血之作,大家翻翻进度条就知道了,内容十分干货,各位读者若能全部耐心解析读懂了,想必也能理解掌握C语言中的数组指针这两把利剑了。

指针数组结合:

指针数组

概念:他是一个数组,该数组中每一个元素的类型是指针。

语法:

cpp 复制代码
int * arr[ 元素数量 ] ; // 整型指针数组
char * arr[ 元素数量 ] ; // 字符指针数组
float * arr[ 元素数量 ] ; // 浮点指针数组 
int * arr[ 元素数量 ] (int) ; // 函数指针数组

示例:

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    int a = 123 , b = 456 , c = 789 , d = 111 , e = 222 ;

    // 指针数组
    int *  arr1 [ 5 ] = {&a , &b , &c , &d , &e } ; // 整型指针数组

    for (int i = 0; i < 5; i++)
    {
        printf( "arr1[%d]:%d\n" , i ,*arr1[i] );
    }

    // arr1[0][1] --》 *(arr1[0]+1)  -- > *(*(arr1+0)+1)
    printf("arr[0][1]:%d\n" , arr1[0][1] ) ;//得到数组首元素即*(&a+1),加一个&a大小,即指向&b,解引用得到456
    printf("**arr:%d\n" , **arr1 ) ;  // a = 123,arr1为数组首元素地址,解引用得到&a,再次解引用得到a的值123
    printf("**arr1+1:%d\n" , **arr1+1 ) ;  // a=124,同上,a的值加一得到124
    printf("**(arr1+1):%d\n" , **(arr1+1) ) ;  // b=456,arr1为数组首元素地址,+1偏移一个数组首元素地址大小,即指向数组第二个元素地址,解引用两次得到b的值
    printf("*(*arr1+1):%d\n" , *(*arr1+1) ) ;  // b =456,arr1为数组首元素地址,解引用得到&a,&a可以看作一个指针,+1偏移一个int地址大小,解引用得到b的值
    printf("*(*arr1+1):%d\n" , **(&arr1+1) ) ;  // 【越界】,&arr1为整个数组的地址,+1加一整个数组地址大小,指向数组之外,越界
    printf("**(*(&arr1+1)-1):%d\n" , **(*(&arr1+1)-1) ) ;  // e = 222,&arr1为整个数组的地址,+1加一整个数组的地址大小,解引用得到数组,指向数组
                                                            //末尾元素地址,-1减去一个数组元素地址大小,指向数组最后一个元素地址,两次解引用得到222
    

    char * arr2 [ 5 ] ; // 字符指针数组
    float *arr3 [ 5 ] ; // 浮点指针数组 
    // int *  arr4 [ 5 ] (int) ; // 函数指针数组

    
    return 0;
}

数组指针数组:

概念:他是一个数组,该数组中每一个元素都是指针,而这些指针指向的类型是数组类型。

语法:

cpp 复制代码
type (* arr [5]) [N] ;

示例:

cpp 复制代码
#include <stdio.h>


int main(int argc, char const *argv[])
{

    // 整型数组
    int arr1[2] ={1,2};
    int arr2[2] ={10,20};
    int arr3[2] ={100,200};


    // 整型数组指针
    int (*p1 )[2] = &arr1 ;
    int (*p2 )[2] = &arr2 ;
    int (*p3 )[2] = &arr3 ;

    // 整型数组指针数组
    int (* arr[3]) [2] = { p1 , p2 , p3} ;

    // arr[i] --> p1 
    // p1 = &arr1 
    // (*arr[i]) --> (*p1) --> (*&arr1) --> arr1
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 2; j++)
        {
            printf("(*arr[%d])[%d]:%d\n", i , j , *((**(arr+i))+j) );
            //(*arr[i])[j]  --> *((*arr[i])+j) --> *((**(arr+i))+j))
            //arr为数组首元素地址,+i加i个数组首元素地址大小,指向数组的第i+1个元素,第一次解引用得到数组的元素,即指针,再次解引用得到
            //指针指向的值,即一维数组,这时候得到一维数组arr1即首元素地址,+加上j个数组首元素地址大小,偏移j个元素,解引用得到一维数组的元素
            //这里会逐个打印出一维数组的全部元素
        }
        
    }
    
    

    return 0;
}

指针数组数组:

概念:他是一个数组,该数组中每一个元素都是数组,而这些数组中每一个元素都是指针。

语法:

cpp 复制代码
 type * arr[3] [2] ;

数组指针:

概念:他是一个指针,该指针指向的是一个数组的类型

语法:

cpp 复制代码
int (*ptr) [5]  ; // 一个指针,该指针指向拥有5个整型数据的数组 【整型数组指针】
char (*ptr) [5]  ; //  一个指向用于5个字符型的数组  【字符数组指针、 字符串数组指针】
cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    int arr [5] = {1,20,3,4,5};

    

    int (*ptr) [5] ; 
    ptr = &arr ;

    int * p = arr ;
    // p == arr ;

    // ptr = &arr ;
    // **ptr = **&arr = *arr = 1
    printf("**ptr:%d\n", **ptr); // 1,ptr解引用得到arr,arr为数组首元素地址,解引用得到数组首元素,即1

    printf("**ptr+1:%d\n", **ptr+1); // 2,同上,1+1=2

    // *(*ptr+1) == *(*&arr+1) = *(arr+1)
    printf("*(*ptr+1):%d\n", *(*ptr+1)); // 20,ptr解引用得到数组arr,arr为数组首元素地址,+1加一个数组首元素地址大小,解引用得到数组第二个元素

    printf("*(*(ptr+0)+1):%d\n", *(*(ptr+0)+1)); // 20,ptr解引用得到arr,arr为数组首元素地址,+1加一个数组首元素地址大小,解引用得到数组第二个元素
    printf("ptr[0][1]:%d\n", ptr[0][1] ); // 20,相当于*(*(ptr+0)+1),同上
    

    // 【拓展】 预习函数的形参中出现数组
    // 一下两种写法是等价的,第一种写法是用于强调(告诉使用者)
    // 函数pipe 需要的地址必须有两个以上整型的合法内存空间
    // int pipe(int pipefd[2]);  
    // int pipe(int * pipefd);


    char msg1[] = "Hello";
    char msg2[] = "World";
    char msg3[] = "WangDaNiang";
    char msg4[] = "ShiDaSao";
    char msg5[] = "!";

    char * arr1[5] = { msg1, msg2, msg3 ,msg4 , msg5};
    char *(* ptr1) [5] = &arr1;
    
    // 操作练习通过ptr1来实现一下操作:
    //  1. 数组以上所有的字符串 "Hello" , "World" , "!" , "!" , "!"
    for (int i = 0; i < 5 ; i++)
    {
        // ptr1 = &arr1 
        //(*ptr1) = arr1
        printf("arr1[%d]:%s\n" , i , (*ptr1)[i] );
        //&arr1为整个数组的地址,ptr解引用得到数组arr1,arr1为数组首元素地址,arr1[i]得到数组的元素
    }
    
    //  2.1  尝试修改某一个字符
    //  arr1[2][5]  ->  (*ptr1)[2][5]
    //  ptr1 = &arr1 
    (*ptr1)[2][5] = 'A';
    //&arr1为整个数组的地址,ptr解引用得到数组arr1,arr1[2][5]得到数组第三个元素的第六个元素
    printf("arr1[2]:%s\n" , arr1[2] ); 


    //  2.2 尝试把所有的小写字符转换为大写
    for (int i = 0; i < 5 ; i++)
    {
        for (int j = 0; j < 32 ; j++)
        {
            if ((*ptr1)[i][j] != '\0' && 
                    (*ptr1)[i][j] >= 'a' && 
                        (*ptr1)[i][j] <= 'z'  )
            {
                (*ptr1)[i][j] -= 32 ;
            }
            
        }
        
    }

    for (int i = 0; i < 5 ; i++)
    {
        // ptr1 = &arr1 
        //(*ptr1) = arr1
        printf("arr1[%d]:%s\n" , i , (*ptr1)[i] );
        //&arr1为整个数组的地址,解引用得到数组arr1,然后arr1[i]访问数组每个元素,打印出改变大小写后的每个字符串
    }
    

    // char * p1 = &"Hello" ;
    
    return 0;
}

指针数组指针:

概念:他是一个指针,该指针所指向的内存是一个数组的类型,该数组存储的指针类型的元素。

语法:

cpp 复制代码
int * (*ptr) [5] ;

数组指针指针:

概念:他是一个指针,该指针指向的是另外一个指针,该指针指向的是数组的地址。

语法:

cpp 复制代码
int (*(*ptr)) [5] ;
    • 为什么数组名不可以直接赋值
cpp 复制代码
int arr[3];
arr = 123 ;  // 该操作在试图改变arr的地址 , 有明显的逻辑问题(语法错误)
arr[0] = 123 ;
*(arr+1) = 456 ;
    • 数组名为什么不能++
cpp 复制代码
int arr [3];
arr ++ ;  // arr++  -》 arr = arr + 1;    加完之后等式两边不等,左边为数组首元素地址,右边为数组第二个元素地址

指针数组详解

深入 理解char *,char ** ,char a[ ] ,char *a[] 的区别

1 数组的本质

数组是相同类型的多个元素的集合,在内存中分布在地址连续的单元中,所以可以通过其偏移量下标访问不同单元的元素。

2 指针

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的地址,我们就称它为指针变量。

在C语言中,允许用一个变量来存放指针,这种变量称为指针变量。指针变量的值就是某份数据的地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量

指针是一种变量,只不过它的内存单元中保存的是一个标识其他位置的地址。由于地址也是整数,在32位平台下,指针默认为32位,64位中默认为64位

面试题中如果没有指明系统的位数,32位和64系统的不同尺寸都要考虑到

3 指针的指向

指向的直接意思就是指针变量所保存的其他的地址单元中所存放的数据类型。

|------------|----------------------------|
| int * p ; | //p 变量保存的地址所在内存单元中的数据类型为整型 |

|------------|------------------------------------------------|
| float *q; | // ........................................浮点型 |

不论指向的数据类型为那种,指针变量其本身永远为整型,因为它保存的是地址,大小为系统字长。

32位中指针大小为4字节,64位指针大小为8字节

4 字符数组

字面意思是数组,数组中的元素是字符。

cpp 复制代码
char str[10];

定义了一个有十个元素的数组,元素类型为字符。

C语言中定义一个变量时可以初始化。

cpp 复制代码
char str[10] = {"hello world"};

当编译器遇到这句时,会把str数组中从第一个元素把hello world\0 逐个填入。。

C语言中规定数组代表数组所在内存位置的首地址,也是 str[0]的地址,即str =&str[0];

而printf("%s",str); 为什么用首地址就可以输出字符串。。

因为还有一个关键,在C语言中字符串常量的本质表示其实是一个地址,这是许多初学者比较难理解的问题。

举例:

char *s ;

s = "China";

为什么可以把一个字符串赋给一个指针变量。。

这不是类型不一致吗?

这就是上面提到的关键 。

C语言中编译器会给字符串常量分配地址,如果 "China", 存储在内存中的0x3000 0x3001 0x3002 0x3003 0x3004 0x3005 .

s = "China" ,意识是什么,对了,地址。

其实真正的意义是 s ="China" = 0x3000;

把China 看作是字符串,但是编译器把它看作是地址0x3000,即字符串常量的本质表现是代表它的第一个字符的地址。

s = 0x3000

那么 %s ,它的原理其实也是通过字符串首地址输出字符串,printf("%s ",s); 传给它的其实是s所保存的字符串的地址。

比如

cpp 复制代码
#include <stdio.h>
int main()
{
    char *s;
    s = "hello";    //这里"hello"为字符串常量,只能通过s访问字符串常量,不能通过s改变字符串常量
    printf("%p\n",s);
return 0;
}

可以看到 s = 0x00422020 ,这也是"hello"的首地址

所以,printf("%s",0x00422020);也是等效的。。

字符数组:

char str[10] = "hello";

前面已经说了,str = &str[0] , 也等于 "hello"的首地址。。

所以printf("%s",str); 本质也是printf("%s", 地址");

C语言中操作字符串是通过它在内存中的存储单元的首地址进行的,这是字符串的终极本质。

5. char * 与 char a[ ];

char *s;

char a[ ] ;

前面说到 a代表字符串的首地址,而s这个指针也保存字符串的地址(其实首地址),即第一个字符的地址,这个地址单元中的数据是一个字符,

这也与 s 所指向的 char 一致。

因此可以 s = a;

但是不能 a = s;

C语言中数组名(即首元素地址)可以赋值给指针表示地址, 但是却不能将指针赋给给数组名,它是一个常量类型,所以不能修改。

当然也可以这样:

cpp 复制代码
char a [ ] = "hello";
char *s =a;    //这里a为字符串数组,a为数组首元素地址,即h的地址,赋给指针s,指针s和a都可以访问且修改字符串
for(int i= 0; i < strlen(a) ; i++)
{    printf("%c", s[i]);
    //或 printf("%c",*s++);//这里优先级++比*高,会先运算++符号,但是++是先赋值再加一,s解引用得到数组首元素,之后s++指针指向下一个元素
}    

字符指针可以用 间接操作符 *取其内容,也可以用数组的下标形式 [ ],数组名也可以用*操作,因为它本身表示一个地址 。

比如 printf("%c",*a); 将会打印出 'h'

char * 与 char a[ ] 的本质区别:

当定义 char a[10 ] 时,编译器会给数组分配十个单元,每个单元的数据类型为字符。。

而定义 char *s 时, 这是个指针变量,只占四个字节,32位,用来保存一个地址。。

sizeof(a) = 10 ;//sizeof计算时,表示整个数组大小,char类型占一个字节,数组总共10个元素.所以数组大小为10个字节

sizeof(s) = ? //32位中指针为4字节,64位中为8字节

当然是4了,编译器分配4个字节32位的空间,这个空间中将要保存地址。

printf("%p",s);

这个表示 s 的单元中所保存的地址。

printf("%p",&s);

这个表示变量本身所在内存单元地址。

用一句话来概括,就是 char *s 只是一个保存字符串首地址的指针变量, char a[ ]是许多连续的内存单元,单元中的元素为char ,之所以用 char *能达到

char a [ ]的效果,还是字符串的本质,地址,即给你一个字符串地址,便可以随心所欲的操所他。。但是,char* 和 char a[ ] 的本质属性是不一样的。。

6 char ** 与char * a[ ] ;

先看 char *a [ ] ;

由于[ ] 的优先级高于* 所以a先和 [ ]结合,他还是一个数组,数组中的元素才是char *,前面讲到char * 是一个变量,保存的地址。。

所以 char *a[ ] ={"China","French","America","German"};

同过这句可以看到, 数组中的元素是字符串,那么sizeof(a)是多少呢,有人会想到是五个单词的占内存中的全部字节数 6+7+8+7 = 28;

但是其实sizeof(a) = 16;//sizeof()表示整个数字大小,数组中存放的元素是指针,指针在32位系统中是4字节,所以这里数组为16字节

为什么,前面已经说到, 字符串常量的本质是地址,a 数组中的元素为char *指针,指针变量占四个字节,那么四个元素就是16个字节了

看一下实例:

cpp 复制代码
#include <stdio.h>
int main()
{
char *a [ ] ={"China","French","America","German"};
printf("%p %p %p %p\n",a[0],a[1],a[2],a[3]);
return 0;
}

可以看到数组中的四个元素保存了四个内存地址,这四个地址中就代表了四个字符串的首地址,而不是字符串本身。

因此sizeof(a)当然是16了。

注意这四个地址是不连续的,它是编译器为"China","French","America","German"分配的内存空间的地址, 所以,四个地址没有关联,有关联的是&a[0].&a[1].&a[2]&a[3]的地址.

cpp 复制代码
#include <stdio.h>
int main()
{
    char *a [ ] ={"China","French","America","German"};
    printf("%p %p %p%p\n",a[0],a[1],a[2],a[3]); //数组元素中保存的地址
    printf("%p %p %p%p\n",&a[0],&a[1],&a[2],&a[3]);//数组元素单元本身的地址
return 0;
}

可以看到 0012FF38 0012FF3C 0012FF400012FF44,这四个是元素单元所在的地址,每个地址相差四个字节,这是由于每个元素是一个指针变量占四个字节。

char **s;

char **为二级指针, s保存一级指针 char *的地址

举例:

cpp 复制代码
char *a [ ] ={"China","French","America","German"};
char **s = a;  //a表示数组首元素地址,数组首元素又是"China"匿名字符串常量,匿名字符串常量

为什么能把 a赋给s,因为数组名a代表数组元素内存单元的首地址,即 a = &a[0] =0012FF38;

而 0x12FF38即 a[0]中保存的又是 00422FB8 ,这个地址,00422FB8为字符串"China"的首地址。

即 *s = 00422FB8 = "China";

这样便可以通过s 操作 a 中的数据

printf("%s",*s); //8s相当于*a,a为数组首元素的地址,*a取得数组首元素,数组首元素为"China"的地址,所以打印得到China

printf("%s",a[0]); //a[0],数组a的首元素为"China"的地址,打印得到China

printf("%s",*a); //a数组首元素地址,*解引用得到数组首元素,数组首元素为"China"字符串常量,"China"字符串常量实际上为

//匿名数组,字符型本身是它的数组名,所以这里"China"表示该匿名数组的首元素'C'的地址,打印得到"China"

都是一样的。

但还是要注意,不能a = s,前面已经说到,a 是一个常量。

cpp 复制代码
#include <stdio.h>

int main(int argc, char const *argv[])
{
    char *a[] = {"China", "French", "America", "German"};
    char **s = a;

    //程序打印hina,a为数组首元素地址,*a得到数组首元素,数组首元素为"China"匿名数组,
    //"China"的名字即是字符串本身,所以"China"为数组名,代表"China"首元素地址,所以*a+1
    //为"China"首元素地址加上一个首元素地址大小,指向"China"的第二个元素h的地址
    printf("%c\n", *a + 1); 
    

    //&"China"表示一整个数组的地址,+1表示加上一整个数组的地址大小,指向"China"匿名数组之外,
    //再解引用得到未知的值,其实这里&"China"是错误的,&只能取变量的地址,而"China"为字符串常量
    //这里只是为了更好理解才这样写
    printf("%s\n",*(&"China"+1));  //未知

    //a表示数组首元素地址,+1表示加一个数组首元素地址大小,指向数组的第二个元素,解引用得到数组第二个元素
    printf("%s\n",*(a+1));  //程序打印French

    return 0;
}
 

再看一个易错的点:

char **s = "hello world";

//s为二级指针,它的类型是char ** ,而"hello world"字符串常量的类型是char *,两边类型不匹配

这样是错误的,

因为 s 的类型是 char ** 而 "hello world "的类型是 char*

虽然都是地址,但是指向的类型不一样,因此,不能这样用。,从其本质来分析,"helloworld",代表一个地址,比如0x003001,这个地址中的内容是 'h',为 char 型,而 s 也保存一个地址 ,这个地址中的内容(*s)(*s解引用二级指针中保存的地址即一级指针的地址) 是char* ,是一个指针类型, 所以两者类型是不一样的。

如果是这样呢?

cpp 复制代码
char **s;
*s = "hello world";
//s为二级指针,指针必须初始化,不然就会变成野指针,这里二级指针即s没有初始化,为野指针.

貌似是合理的,编译也没有问题,但是 printf("%s",*s),就会崩溃

why??

咱来慢慢推敲一下。。

printf("%s",*s); 时,首先得有s 保存的地址,再在这个地址中找到 char * 的地址,即*s;

举例:

s = 0x1000;

在0x1000所在的内存单元中保存了"hello world"的地址 0x003001, *s = 0x003001;

这样printf("%s",*s);

这样会先找到 0x1000,然后找到0x003001;

如果直接 char **s;

*s = "hello world";

s 变量中保存的是一个无效随机不可用的地址, 谁也不知道它指向哪里,*s 操作会崩溃。

所以用 char **s 时,要给它分配一个内存地址。

char **s ;

s = (char **) malloc(sizeof(char**));

*s = "hello world";

这样 s 给分配了了一个可用的地址,比如 s = 0x412f;

然后在 0x412f所在的内存中的位置,保存 "hello world"的值。。

再如:

cpp 复制代码
#include <stdio.h>
void buf( char **s)
{
    *s = "message";
}
int main()
{
    char *s ;
    buf(&s);
    printf("%s\n",s);
}
//char *s定义了一个char类型的一级指针,取地址后&s为char ** 二级指针类型,然后传入buf函数,解引用得到一级指针,"message"为字符串常量实际上是匿名数组,
//存放在数据段中的常量区,所以函数buf在栈空间中被释放后,"message"字符串常量内存空间并不会被释放,所以后面主函数中一样可以访问字符串常量,但是只能访问
//不能通过指针对字符串常量进行修改删除等操作,字符串本身就是数字名,这里数组名即代表字符串常量的首元素地址,为char *类型,所以赋给函数中char *的*s一级
//指针,将主函数中的s指向数据段中的字符串常量"message"的地址,打印得到字符串

二级指针的简单用法,说白了,二级指针保存的是一级指针的地址,它的类型是指针变量,而一级指针保存的是指向数据所在的内存单元的地址,虽然都是地址,但是类型是不一样的。

注:字符串常量:

字符串常量是指在程序中直接使用双引号括起来的字符串。例如:"hello"、"world"等都是字符串常量。字符串常量在C语言中是不可变的,也就是说,一旦定义了字符串常量,就不能再修改它的值。

在C语言中,你可以使用双引号括起来的任何字符序列来定义字符串常量。

字符串常量的本质表现是代表它的第一个字符的地址,这是因为在C语言中,字符串常量实际上是一个匿名数组,被存储为字符数组,并以空字符 '\0' 结尾,并且字符串即是匿名数组的数组名,而数组名一般情况下又代表数组首元素地址。因此,当你声明一个字符串常量时,实际上是在数据段中创建一个字符数组,并将该字符串中每个字符的地址存储在匿名数组中。

在C语言中,字符串常量是不可变的,因此任何尝试修改字符串常量的操作都是不允许的。这也是为什么在前面的例子中,尝试使用指向字符串常量的指针来进行修改会导致崩溃的原因。

当你写下 char str[] = "helloworld"; 时,实际上发生了:

  1. 创建了一个字符数组 str,并分配了足够的内存来存储字符串 "hello world"。
  2. 在内存的数据段创建了一个匿名数组,匿名数组存放字符串常量"hello world"
  3. 将字符串 "hello world" 中的每个字符复制到了字符数组 str 中,并在末尾添加了一个空
  4. 字符 '\0' 来表示字符串的结束。
  5. 编译器会自动在字符数组的末尾添加一个空字符 '\0' 来表示字符串的结束。这个过程是自动进行的,你不需要手动添加空字符

因此,你可以对字符数组 str 进行任意操作,因为它是可变的。这包括修改、反转、拼接等操作,如果将数组的地址赋给一个指针,通过指针也可以对字符串进行任意操作。

但是,如果直接将字符串常量赋给指针,(char *p = "hello world",此时只能通过指针访问字符串常量,而通过指针直接对字符串常量"hello world" 进行操作,就会导致编译错误或者运行时错误

{"China","French","America","German"}这几个都是字符串常量,系统在内存中为他们分配了不同的地址,程序通过char *a[ ]来存放他们的地址,所以这几个字符串的地址不是数组空间的地址,数组空间地址是连续的,但是存放的字符串常量地址不是连续的,所以这几个地址没有关系.

cpp 复制代码
//"hello"为字符串常量实际上是匿名数组,"hello"字符串本身即数组名,而在这里数组名表示数组首元素地址, 所以指针msg存放了hello的首元素h的地址
    char *msg = "hello";
    printf("%c\n",*msg);  // 打印出'h',
    printf("%d\n",*msg);  // 打印出'h'的ASCII码值
    printf("%p\n",*msg);  // 打印出'h'的地址值,这是错误的用法
    //%p是用来打印指针的地址的,而*msg是字符,不是指针
   
    printf("%d\n",msg);   // 打印出msg的值,即存储的地址
    printf("%p\n",msg);   // 打印出msg的值,即存储的地址
    printf("%s\n",msg);   // 打印出整个字符串"hello"    
  1. printf("%c\n",*msg); - 这会打印出'h',因为*msg解引用了指针msg,得到了字符串的第一个字符'h'。
  2. printf("%d\n",*msg); - 这会打印出104,因为'h'的ASCII码值为104。
  3. printf("%p\n",*msg); - 这是错误的用法,%p是用来打印指针的地址的,而*msg是字符'h',不是指针,所以这里的用法是不正确的。
  4. printf("%d\n",msg); - 这会打印出msg的值,即存储的地址。
  5. printf("%p\n",msg); - 这也会打印出msg的值,即存储的地址。
  6. printf("%s\n",msg); - 这会打印出整个字符串"hello"。

请注意,对于指针变量msg,*msg表示的是指针所指向的值,而msg表示的是指针本身的值,即存储的地址。

*msg已经解引用msg的首字符的地址的值,即'h',所以打印的时候都是打印关于'h'的

/**总结:

*数组名字除了sizeof和定义和&情况下代表整个数组,其他情况下代表数组首元素的地址,注意是首元素的地址.

* 首元素地址和首元素不一样概念,首元素为s[0],首元素地址为&s[0]

*而&s[0]和&s又不一样,虽然他们地址都是一样的,都是数字首元素的地址,但是意义不同,&s[0]为数组首元素地址,&s为整个数组的地址

*+1时各自不同,&s[0]为数组首元素地址加上一个数组首元素地址大小,加完指向数组的第二个元素的地址

*&s为数组的地址加上一整个数组的地址大小,即数组地址加上数组类型字节大小和数组元素个数的乘积大小

*

*二维数组解引用数组首元素地址比较不一样,第一次解引用*s得到数组首元素,而数组首元素类型为数组类型,即此时*s同时也是得到了

* 二维数组首元素的首元素的地址,地址和二维数组首元素地址一样,但是意义不一样.

* 第二次解引用得到数组首元素的首元素

*

* 数组和指针+1时都需要注意原先为什么类型,多少字节大小,+1就是加上原来类型字节大小

*比如&s+1为整个数组地址即数组首元素地址加上一整个数组地址大小,大小为数组元素类型字节大小和数组元素个数大小乘积,加完指向数组末尾

* s+1为数组首元素地址加上一个首元素地址大小,加完指向数组第二个元素的地址

*还有一个在给数组指针赋值地址时,要给数组加上& 如 int arr[3] = {1,2,3}; int *p[3] = &arr; //这里指针p为int [3] 类型,arr为数组首元素地址,为int *类型,和指针p类型不匹配

,所以加上&,表示取一整个数组的地址,一整个数组的地址类型为int [3],两边类型匹配

*因为不加取址符,表示把数组首元素地址赋给指针,但是数组首元素地址为int类型,指针为数组类型(int [3]),加上取址符为整个数组类型的地址。

* 这时候*p相当于*(&arr) =arr,虽然*p和p地址数值都还是数组首元素地址,但是他们意义不同,*p为数组首元素类型,p为整个数组类型

*/

指针数组结合理解题:

理解题1:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    int (*p)[5] = NULL;  //数组指针,
    int arr[5] = {1,2,4,5,7};
    p = &arr;//这里给指针赋值,要加上&,arr只是代表数组首元素地址,数组首元素地址是int类型,而指针是int [5]类型,加上&表示取一整个数组的地址,类型为int [5]
    printf("arr    = %p\n",arr);      //数组首元素地址,arr为数组首元素地址
    printf("&arr   = %p\n",&arr);     //整个数组的地址,即首元素地址 
    printf("p      = %p\n",p);         //整个数组的地址,即首元素地址
    printf("*p     = %p\n",*p);       //p为数组地址,*p得到数组元素地址,即首元素地址
    printf("p+1    = %p\n",p+1 );      //p为一整个数组地址,+1表示加一整个数组地址大小,跳到了数组之外的地址,大小大概为首元素地址加20
    printf("(*p)+1 = %p\n",(*p)+1);   //p为一整个数组的地址,解引用得到数组首元素地址,+1表示加上一个数组首元素地址大小,指向数组第二个元素地址
   
    return 0;
}
/***输出结果:
    * arr    = 0xffffcbd0
   &arr   = 0xffffcbd0
   p      = 0xffffcbd0
   *p     = 0xffffcbd0
   p+1    = 0xffffcbe4
   (*p)+1 = 0xffffcbd4
*/
/**总结:
 * 数组的特殊性,首元素地址代表整个数组的地址,数组地址即首元素地址,
 * 但是在&arr中虽然数值和首元素地址一样,类型不一样,意义不同,+1也各不相同
 * arr+1为加一个数组首元素地址大小,即跳到数组第二个元素地址.
 * &arr+1为加一整个数组地址大小,由数组类型及元素个数乘积决定。
 * 数组首元素地址是首元素地址,&arr是代表整个数组的地址,只是刚好一样
*/

理解题2:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    int a[5] = {1,2,3,4,5};
    int b[3][4] = { {1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
//打印结果:
// &a     = 0xffffcbd0
// &a + 1 = 0xffffcbe4
// a      = 0xffffcbd0
// a + 1  = 0xffffcbd4
// &b     = 0xffffcba0
// &b + 1 = 0xffffcbd0
// b      = 0xffffcba0
// b + 1  = 0xffffcbb0
// *b     = 0xffffcba0
// *b + 1 = 0xffffcba4
// **b     = 1
// **b + 1 = 2
    printf("&a    = %p\n",&a);       //整个数组的地址,数值和数组首元素地址一样
    printf("&a+ 1 = %p\n",&a+1);     //整个数组的地址加上一整个数组地址大小,数值为首元素地址加20个字节
    printf("a     = %p\n",a);        //数组首元素地址  
    printf("a+ 1  = %p\n",a+1);      //数组首元素地址加一个首元素地址大小,即数组第二个元素的地址
    printf("\n");
    printf("&b    = %p\n",&b);       //整个数组的地址,数值和数组首元素地址一样
    printf("&b+ 1 = %p\n",&b+1);     //整个数组的地址加上一整个数组地址大小,数值为首元素地址加48字节
    printf("\n");
    printf("b     = %p\n",b);      //数组首元素的地址
    printf("b+ 1  = %p\n",b+1);    //数组首元素的地址加上一个数组首元素地址大小,即数组第二个元素,数组首元素地址加16字节
    printf("\n");
    printf("*b    = %p\n",*b);       //数组首元素地址解引用,得到第一个元素,即{1,2,3,4},而{1,2,3,4}的首元素地址和b数组首元素地址一样
    printf("*b+ 1 = %p\n",*b+1);     //由上一句可得,*b+1为加一个{1,2,3,4}里的首元素地址大小,即指向2的地址,数值为首元素地址加4字节
    printf("\n");
    printf("**b    = %d\n",**b);      //*b得到数组第一个元素{1,2,3,4},解引用第一个元素的首元素地址,得到1
    printf("**b+ 1 = %d\n",**b+1);   //由上一句,1+1=2
    return 0;
}
/**总结:
 * 数组名字除了sizeof和定义和&情况下代表整个数组,其他情况下代表数组首元素的地址,注意是首元素的地址.
 * 首元素地址和首元素不一样概念,首元素为s[0],首元素地址为&s[0]
 * 而&s[0]和&s又不一样,虽然他们地址都是一样的,都是数字首元素的地址,但是意义不同,&s[0]为数组首元素地址,&s为整个数组的地址
 * +1时各自不同,&s[0]为数组首元素地址加上一个数组首元素地址大小,加完指向数组的第二个元素的地址
 * &s为数组的地址加上一整个数组的地址大小,即数组地址加上数组类型字节大小和数组元素个数的乘积大小
 * 
 * 二维数组解引用数组首元素地址比较不一样,第一次解引用*s得到数组首元素,而数组首元素类型为数组类型,即此时*s同时也是得到了
 * 二维数组首元素的首元素的地址,地址和二维数组首元素地址一样,但是意义不一样.
 * 第二次解引用得到数组首元素的首元素
 * 
 * 数组和指针+1时都需要注意原先为什么类型,多少字节大小,+1就是加上原来类型字节大小
 * 比如&s+1为整个数组地址即数组首元素地址加上一整个数组地址大小,大小为数组元素类型字节大小和数组元素个数大小乘积,加完指向数组末尾
 * s+1为数组首元素地址加上一个首元素地址大小,加完指向数组第二个元素的地址
*/

理解题3:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    int arr[] = { 1, 3, 5, 7, 9};
    int len= sizeof(arr)/ sizeof(int);  //求数组长度
    int i;
//输出结果:
// *(arr+0) = 1
// *(arr+1) = 3
// *(arr+2) = 5
// *(arr+3) = 7
// *(arr+4) = 9 
    for(i=0; i<len; i++) 
    {
        //arr为数组首元素地址,+i为加上i个数组首元素地址大小
        //解引用表示数组第i+1个元素
        printf("*(arr+%d) = %d\n",i, *(arr+i) ); 
    }
        
    printf("\n");
    
    return 0;
} 

理解题4:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    int arr[] = { 1, 3, 5, 7, 9};
    int i, *p = arr;
    int len = sizeof(arr) / sizeof(int);
    
    for(i=0; i<len; i++)
    {
        //p为数组首元素的地址,加i为数组首元素地址加上i个数组首元素地址,
        //相当于数组的第i+1个数组元素地址,解引用得到数组元素
        printf("*(p+%d) = %d\n",i, *(p+i));  
    }
    printf("\n");
    
    return 0;
}

理解题5:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    int arr[] = { 1, 3, 5, 7, 9};
    int *p = &arr[2]; 
    
    //*p为数组第三个元素的地址,*p为数组第三个元素3
    //(p+---i)为数组第三个元素地址加减i个第三个元素地址大小,即偏移i个元素
    //解引用得到数组元素
    printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
    
    
    return 0;
}

理解题6:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    int arr1[] = { 12, 35, 54, 72, 99};
    int arr[] = { 1, 3, 5, 7, 9};
    int i, *p = arr, len = sizeof(arr) / sizeof(int);
    
    printf("arr = %p\n", arr);    //arr为数组首元素地址
    printf("p = %p\n", p);    //p为数组arr首元素地址
    for(i=0; i<len; i++)
    {
        //p存放数组首元素地址,这里运算符优先级 ++ > *,
        //所以会先计算p++,但是因为p++先赋值后运算,
        //所以p先解引用得到1,这里先输出1
        //接着p++,p为数组首元素地址,+1代表加一个数组首元素地址
        //p指向数组第二个元素地址
        printf("%d\n", *p++ );    
        //i为0,p在这里为数组第二个元素地址,表示第0次指向第二个元素地址
        printf("%d ---> %p\n",i, p);
        //p解引用得到3,接着*p打印得到3,然后*p自加一,得到4,*p索引到指针指向的目标,并把数组第二个元素变为4
        printf("%d\n", (*p)++ );  
        printf("======================\n");
    }
    printf("======================\n");
    for(i=0; i<len; i++)
    {
        
        printf("arr[%d] = %d\n",i,arr[i] );
    }
    printf("\n");
    return 0;
}
//打印结果:
// arr[0] = 1
// arr[1] = 4
// arr[2] = 6
// arr[3] = 8
// arr[4] = 10

理解题7:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    int a = 1, b = 2, c = 3;
    int *arr[3]= {&a, &b, &c};//整型指针数组
    int **parr = arr;
    
    //arr[0]为数组首元素,*解引用得到*(&a)即1,其他同理
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    //parr为数组首元素地址,parr+i为数组首元素地址加上i个数组首元素地址大小
    //即数组首元素地址偏移到第i+1个元素地址,*解引用得到&a,再解引用得到*(&a) = 1
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    return 0;
}

理解题8:

cpp 复制代码
#include <stdio.h>
int main(void)
{
    char *lines[5]=      //字符指针数组
    {
    "COSC1283/1984",
    "Programming",
    "Techniques",
    "is",
    "greatfun"
    };
  
  
    char *str1 = lines[1];         
    char *str2 = *(lines + 3); 
    char c1    = *(*(lines + 4) + 6);  
    char c2    = (*lines + 5)[5];  
  
    char c3    = *lines[0] + 2;
  
  //打印结果:
// str1 = Programming
// str2 = is
// c1   = f
// c2   = 9
// c3   = E
  //lines[1]是字符串数组的元素之一,lines[1]元素是"Programming"的首地址,
  //str1指向该首地址,所以打印"Programming"字符串
    printf("str1 = %s\n", str1);   
  
  //lines+3,代表lines首元素的地址加上3个首元素地址大小,即指向数组第四个元素
  //即"is"的地址,然后解引用得到"is",打印得到is
    printf("str2 = %s\n", str2);   
  //lines+4偏移到数组第五个元素的地址,解引用得到"great fun",然后
  //+6偏移6个"great fun"首元素地址大小,指向"great fun"第七个元素的地址,
  //解引用得到f  
    printf("c1   = %c\n", c1);     
  //lines数组首元素地址解引用得到"COSC1283/1984"的首地址,加五即
  // 加上五个"COSC1283/1984"的首地址大小,偏移到"COSC1283/1984"的元素2
  // 然后(&2)[5]等于*(&2+5),地址再偏移加上五个&2大小,即偏移到了9的地址,
  //然后解引用得到9
    printf("c2   = %c\n", c2);
  //lines[0]即"COSC1283/1984"即"COSC1283/1984"的首元素地址,然后解引用得到C
  //'C'+2得到E    
    printf("c3   = %c\n", c3);    
  
    return 0;
}
  • 理解题9:
cpp 复制代码
#include <stdio.h>
int main(void)
{
    int i;
    int num;
    int (*p)[5]= NULL;  //数组指针 
    int arr[5] =  {5,2,4,5,7};
    p = &arr;
    printf("=====================================\n");
    num = sizeof(arr)/sizeof(arr[0]);
//打印结果:
// *p[0] = 5
// *p[1] = 0
// *p[2] = -13232
// *p[3] = 1
// *p[4] = 0
    for(i=0;i<num;i++)
    {
        //*p[i]相当于**(p+i),p为数组的地址,加i表示加i个数组的地址
        //加一表示加一整个数组地址大小,然后p指向数组末尾地址
        printf("*p[%d] = %d\n",i,*p[i]);
    }
    printf("=====================================\n");
    
//打印结果:
// *(p+0) = 0xffffcbc0
// *(p+1) = 0xffffcbd4
// *(p+2) = 0xffffcbe8
// *(p+3) = 0xffffcbfc
// *(p+4) = 0xffffcc10  
    for(i=0;i<num;i++)
    {
        //p为数组的地址,加i表示加i个数组的地址
        //加一表示加一整个数组地址大小,然后p指向数组末尾地址,
        //输出地址每个地址相差数组元素字节大小和数组元素个数的乘积
        //即相差20字节
        printf("*(p+%d) = %p\n",i,*(p+i));  
    } 
    printf("=====================================\n");
    
//打印结果: 
// p[0] = 0xffffcbc0
// p[1] = 0xffffcbd4
// p[2] = 0xffffcbe8
// p[3] = 0xffffcbfc
// p[4] = 0xffffcc10
    for(i=0;i<num;i++)
    {
        //p[i]相当于*(p+i)
        //p为数组的地址,加i表示加i个数组的地址
        //加一表示加一整个数组地址大小,然后p指向数组末尾地址,
        //输出地址每个地址相差数组元素字节大小和数组元素个数的乘积
        //即相差20字节
        printf("p[%d] = %p\n",i,p[i]);     
    }
    printf("=====================================\n");
    
//打印结果: 
// (*p)[0] = 5
// (*p)[1] = 2
// (*p)[2] = 4
// (*p)[3] = 5
// (*p)[4] = 7
    for(i=0;i<num;i++)
    {
        //*p相当于*(&arr)相当于arr表示数组首元素地址,
        //然后arr[i]输出数组元素
        printf("(*p)[%d] = %d\n",i,(*p)[i]); 
    }
    
    return 0;
}
//总结:先判断是指针还是数组,具体看标识符和哪个优先级高的符号结合
//接着判断元素类型,数组的元素类型是什么,指针指向的类型是什么
//数组:判断数组名字含义,在sizeof运算符,&arr,定义时数组名字代表数组元素首地址
//注意:因为数组的特殊性,所以arr和&arr地址相同,但是他们意义不同,arr表示数组首元素地址
//&arr表示整个数组的地址,各自加一时也不同,arr为加一个数组首元素地址大小,&arr为加一个数组地址大小
//数组索引可以arr[0]直接索引,也可通过指针索引,arr[0]相当于:*(arr+i).
//注意数组的偏移量和第几个元素不一样,数组偏移量是从0开始偏移,第几个元素是从1开始计数
//将数组地址赋给指针时,要注意是赋值arr还是&arr,特别是二维数组更加不一样
//指针:指针是一个存放地址的变量,存放地址的类型要和指针的类型一致,特别是二级指针,数组指针等等
//指针+1运算时,要注意指针是什么类型的,是加多大的元素地址大小

结语:

写到这里,大概18550多字,不知道读者是否有耐心读到这里,但是我相信,看到这里的并且认真研究过的,相信你对指针数组的理解已经十分透彻明白了,恭喜你,掌握了C语言的数组指针这两把利剑。

相关推荐
jiao0000134 分钟前
数据结构——队列
c语言·数据结构·算法
铁匠匠匠38 分钟前
从零开始学数据结构系列之第六章《排序简介》
c语言·数据结构·经验分享·笔记·学习·开源·课程设计
C-SDN花园GGbond39 分钟前
【探索数据结构与算法】插入排序:原理、实现与分析(图文详解)
c语言·开发语言·数据结构·排序算法
Navigator_Z3 小时前
数据结构C //线性表(链表)ADT结构及相关函数
c语言·数据结构·算法·链表
菜菜想进步3 小时前
内存管理(C++版)
c语言·开发语言·c++
知星小度S3 小时前
C语言——自定义类型
c语言·开发语言
cleveryuoyuo4 小时前
二叉树的链式结构和递归程序的递归流程图
c语言·数据结构·流程图
科研小白_d.s4 小时前
vscode配置c/c++环境
c语言·c++·vscode
暮色_年华5 小时前
嵌入式C语言自我修养:C语言的模块化的编程思想
c语言
大母猴啃编程6 小时前
数据结构---非线性--树
c语言·数据结构·学习·算法·青少年编程