C 进阶 — 指针的使用

C 进阶 --- 指针的使用

主要内容

1、字符指针

2、数组指针

3、指针数组

4、数组传参和指针传参

5、函数指针

6、函数指针数组

7、指向函数指针数组的指针

8、 回调函数

9、指针和数组练习题

前节回顾

1、指针就是个变量,用来存放地址,地址唯一标识一块内存空间

2、指针大小是固定的 4/8 个字节(32 位平台/64 位平台)

3、指针有类型,指针的类型决定了指针的 ± 整数的步长和指针解引用操作时的解释

4、指针的运算

一 字符指针

在指针的类型中我们知道有一种指针类型为字符指针 char*

c++ 复制代码
//使用方式一
int main()
{
    char ch = 'w';
    char *pc = &ch;
    *pc = 'x';
    return 0;
}

//使用方式二
int main()
{
    const char* pstr = "HELLO WORLD"; //如何理解该行
    printf("%s\n", pstr);
    return 0;
}

代码 const char* pstr = "HELLO WORLD"; 是把字符串 HELLO WORLD 首字符的地址放到了 pstr 中(可不能理解成把字符串 HELLO WORLD 放到字符指针 pstr 里了)

练习题

下面代码的最终输出是

c++ 复制代码
#include <stdio.h>
int main()
{
    char str1[] = "HELLO WORLD";
    char str2[] = "HELLO WORLD";
    const char *str3 = "HELLO WORLD";
    const char *str4 = "HELLO WORLD";
    
    if(str1 == str2) //这里比较的是数组首地址
        printf("str1 and str2 are same\n");
    else
        printf("str1 and str2 are not same\n"); //√

    if(str3 == str4) //这里比较的是 STR 存放的地址
        printf("str3 and str4 are same\n"); //√
    else
        printf("str3 and str4 are not same\n");

    return 0;
}

这里 str3 和 str4 指向的是一个同一个常量字符串。C/C++ 会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存

但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以 str1 和 str2 不同,str3 和 str4 相同

这里可以结合内存分配会更好理解,不同的数组在栈区分配的内存空间是不同的,而两个 str 指针在栈区的内存空间当然也不同,可是它们内存空间的值相同(指向常量区的同一个位置)

二 指针数组

指针数组是一个存放指针的数组,操作符 [] 的优先级大于 *

c++ 复制代码
//复习 下面的数组表示的意义
int* arr1[10];		 //整形指针数组
char *arr2[4]; 		 //一级字符指针数组
char **arr3[5];		 //二级字符指针数组

三 数组指针

3.1 数组指针定义

数组指针是指针,即指向数组的指针

c++ 复制代码
// p1, p2 分别是什么
int *p1[10];  //指针数组, 表示十个元素的整型指针数组
int (*p2)[10]; //数组指针, 表示指向十个整型元素数组的指针

//[] 优先级要高于 * 号, 加上 () 保证结合律
3.2 & 数组名和 数组名

int arr[10];

arr 和 &arr 分别的含义是 ?arr 是数组名,数组名表示数组首元素的地址。那 &arr 数组名是 ?

c++ 复制代码
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("%p\n", arr);
    printf("%p\n", &arr);
    return 0;
}

打印的内容是一样的,即值相同,但它们代表的含义却并不相同

c++ 复制代码
#include <stdio.h>
int main()
{
    int arr[10] = { 0 };
    printf("arr    = %p\n", arr);
    printf("&arr   = %p\n", &arr);
    printf("arr+1  = %p\n", arr+1);
    printf("&arr+1 = %p\n", &arr+1);
    return 0;
}


//打印结果
arr    = 000000D344CFF7D8
&arr   = 000000D344CFF7D8
arr+1  = 000000D344CFF7DC
&arr+1 = 000000D344CFF800

&arr 和 arr,虽然值相同,但意义不同。&arr 表示的是 数组的地址,而不是数组首元素的地址

本例中 &arr 的类型是: int(*)[10] ,是一种数组指针类型。数组的地址 + 1,跳过整个数组的大小,所以 &arr+1 相对于 &arr 的差值是 40

3.3 数组指针的使用
c++ 复制代码
//代码一
#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    int (*p)[10] = &arr;//把数组 arr 的地址赋值给数组指针变量 p
    return 0;
}
c++ 复制代码
//代码二
#include <stdio.h>

//普通二维数组的打印
void print_arr1(int arr[3][5], int row, int col)
{
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<col; j++)
            printf("%d ", arr[i][j]);
        printf("\n");
    }
}

void print_arr2(int (*arr)[5], int row, int col)
{
    for(int i=0; i<row; i++)
    {
        for(int j=0; j<col; j++)
            printf("%d ", arr[i][j]);
        printf("\n");
    }
}

int main()
{
    int arr[3][5] = {1,2,3,4,5,6,7,8,9,10};
    print_arr1(arr, 3, 5);
    //数组名 arr, 表示首元素的地址, 但二维数组的首元素是二维数组的第一行. 所以这里传递的 arr, 其实相当于第一行的地址, 是一维数组的地址. 可以数组指针来接收
    print_arr2(arr, 3, 5); //传递的是第一行的地址, 这里联想二维数组的内存分布会好理解
    return 0;
}

练习,解释下面代码的意思

c++ 复制代码
int arr[5];				//有五个元素的整型数组
int *parr1[10];			//有十个整型指针元素的数组
int (*parr2)[10];   	//指向十个整型元素数组的指针
int (*parr3[10])[5];	//有十个数组指针元素的数组(数组指针指向有五个元素的整型数组)

四 数组参数、指针参数

4.1 一维数组传参
c++ 复制代码
#include <stdio.h>
void test(int arr[]) {}     	// √
void test(int arr[10]) {}   	// √
void test(int *arr) {}      	// √
void test2(int *arr[20]) {} 	// √
void test2(int **arr) {}    	// √

int main()
{
    int arr[10] = {0};   //数组
    int *arr2[20] = {0}; //指针数组
    test(arr);
    test2(arr2);
}

第一个 test() 函数传参是一个数组,第一个 test() 函数用一个没有元素个数的数组来接收没问题(一维数组传参,函数形参可以省略数组元素个数)

第二个 test() 函数加上了数组元素个数自然也没问题

第三个 test() 函数用一级指针来接收一维数组也是没什么问题,数组名就是数组首元素的地址

第一个 test2() 函数参数是一个指针数组,就是一个存储一级指针的数组。函数用一个相同结构的指针数组来接收没问题,这里也可以省略数组的元素个数

第二个 test2() 函数用一个二级指针来接收也可以,因为数组名是首元素地址,而数组里面元素存放的也是一个地址,地址的地址用一个二级指针来接收没问题

总结一下:一维数组传参,函数的形参用相同的结构来接收没问题,数组元素个数可以省略,函数的形参用指针来接收就需要考虑指针和地址之间的关系了

4.2 二维数组传参
c++ 复制代码
void test(int arr[3][5]) {}   			//√
void test(int arr[][]) {}				//×
void test(int arr[][5]) {}				//√
void test(int *arr) {}					//√
void test(int* arr[5]) {}				//×
void test(int (*arr)[5]) {}				//√
void test(int **arr) {}					//×

int main()
{
    int arr[3][5] = {0};
    test(arr);
    return 0;
}

第一个 test() 函数的传参是一个二维数组,第一个 test() 函数用相同结构的二维数组接收没问题

第二个test() 函数用省略了元素个数的二维数组来接收,二维数组初始化时可以省略行数,不能忽略列数

第三个 test() 函数省略了行数,没有省略列数没问题

第四个 test() 函数用一级指针来接收二维数组是可行的,数组名是首元素地址,地址用一级指针接收没问题

第五个 test() 函数用一维指针数组来接收不行,指针数组本质是数组,用数组来接收数组需要用相同结构的数组

第六个 test() 函数用数组指针来接收二维数组是可以的,因为数组名是首元素地址就是第一行的地址,相当于是一个一维数组的地址,而数组指针也是一个一级指针,可以用来接收数组的地址,并且每一行有5个元素,数组指针也是接收 5 个元素

第七个 test() 函数用二级指针来接收二维数组不行,因为数组名是首元素地址是第一行的地址,而二级指针是需要接收地址的地址,匹配不上

4.3 一级指针传参
c++ 复制代码
#include <stdio.h>
void print(int *p, int sz)
{
    for(int i = 0; i < sz; i++)
    {
        printf("%d\n", *(p+i));
    }
}
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int *p = arr;
    int sz = sizeof(arr)/sizeof(arr[0]);
    print(p, sz); //一级指针 p,传给函数
    return 0;
}
c++ 复制代码
//思考: 当一个函数参数部分为一级指针时, 函数能接收什么参数 ?
void test1(int *p) {} //test1 函数能接收什么参数
void test2(char* p) {} //test2 函数能接收什么参数

//地址传参用一级指针
//数组名, 指针, 地址
4.4 二级指针传参
c++ 复制代码
#include <stdio.h>
void test(int** ptr)
{
    printf("num = %d\n", **ptr); 
}
int main()
{
    int n = 10;
    int*p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0;
}
c++ 复制代码
//思考: 当函数的参数为二级指针时, 可以接收什么参数 ?
void test(char **p) {}

int main()
{
    char c = 'b';
    char*pc = &c;
    char**ppc = &pc;
    char* arr[10];
    test(&pc);
    test(ppc);
    test(arr); //√
    return 0;
}

//地址的地址传参用二级指针

总结:指针传参只需要判断是地址还是地址的地址,只要是地址就可以用一级指针接收,是地址的地址就可以用二级指针来接收。相反用一级指针来接收就需要传地址,二级指针来接收就需要传地址的地址

五 函数指针

顾名思义就是一个指针指向函数

c++ 复制代码
#include <stdio.h>
void test() {}

int main()
{
    printf("%p\n", test);  // 00007FF7AFAB1159
    printf("%p\n", &test); // 00007FF7AFAB1159
    return 0;
}

上述是 test 函数的地址, 那如何保存函数地址 ?

c++ 复制代码
// pfun1 和 pfun2 哪个可以存放 test 函数的地址
void (*pfun1)();
void *pfun2();

// pfun1 可以, pfun1 先和 * 结合,说明 pfun1 是指针, 指针指向的是一个函数,指向的函数无参数,返回值类型为 void
函数指针经典代码

C 陷阱和缺陷中提及该代码,解释下述两行代码

c++ 复制代码
(*(void (*)())0)();  // 代码一
void ( *signal(int , void(*)(int)) )(int);  //代码二

代码一

void (*)() 中的 * 表明该类型为函数指针 (它指向的函数参数为空,返回值为空),(void (*)()) 0 表示把 0 地址强转为上述的函数指针类型,*(void (*)())0 表示对函数指针解引用 (使用函数指针的方式调用函数 (*ptr)() )

综上上述代码意思为,把 0 地址强转为指向参数为空,返回值为空类型的函数指针,并进行函数调用

代码二

void(*)(int) 和上述一样,是一个函数指针类型 (它指针的函数参数类型为 int,返回值为空 ),signal(int , void(*)(int)) 表明 signal 是一个函数,参数一类型是 int,参数二类型是函数指针,void ( *signal(int , void(*)(int)) )(int); 表示 signal 的返回类型为 void(*)(int)

综上上述代码意思为,signal 是一个函数,参数一类型为 int,参数二类型是函数指针且类型为 void(*)(int),返回值的类型为 void(*)(int)

代码二如何简化

c++ 复制代码
typedef void(*func_ptr)(int);
func_ptr signal(int, func_ptr);

六 函数指针数组

数组是一个存放相同类型数据的存储空间,指针数组比如 int *arr[10]; /数组的每个元素是 int*。把一组函数地址存到一个数组中,这个数组就叫函数指针数组

函数指针数组的定义,例如 int (*parr[10])();

parr先和 [] 结合,说明 parr 是数组,数组的内容是 int (*)() 类型的函数指针

函数指针数组的用途:转移表

c++ 复制代码
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    do
    {
        printf( "*************************\n" );
        printf( " 1:add 2:sub \n" );
        printf( " 3:mul 4:div \n" );
        printf( "*************************\n" );
        
        printf( "请选择:" );
        scanf( "%d", &input);
        
        switch (input)
        {
            case 1:
                printf( "输入操作数:" );
                scanf( "%d %d", &x, &y);
                ret = add(x, y);
                printf( "ret = %d\n", ret);
                break;
            case 2:
                printf( "输入操作数:" );
                scanf( "%d %d", &x, &y);
                ret = sub(x, y);
                printf( "ret = %d\n", ret);
                break;
            case 3:
                printf( "输入操作数:" );
                scanf( "%d %d", &x, &y);
                ret = mul(x, y);
                printf( "ret = %d\n", ret);
                break;
            case 4:
                printf( "输入操作数:" );
                scanf( "%d %d", &x, &y);
                ret = div(x, y);
                printf( "ret = %d\n", ret);
                break;
            case 0:
                printf("退出程序\n");
                breark;
            default:
                printf( "选择错误\n" );
                break;
        }
    } while (input);
    return 0;
}

将上述计算器例子用函数指针数组改写

c++ 复制代码
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int div(int a, int b) { return a / b; }

int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    do
    {
        printf( "*************************\n" );
        printf( " 1:add 2:sub \n" );
        printf( " 3:mul 4:div \n" );
        printf( "*************************\n" );
        
        int(*arr[5])(int x, int y) = {0, add, sub, mul, div}; //转移表
        
        printf( "请选择:" );
        scanf( "%d", &input);
        
        if (input >= 1 && input <= 4)
        {
            printf( "输入操作数:" );
            scanf( "%d %d", &x, &y);
            ret = (*arr[input])(x,y);
        }
        else
       		printf( "输入有误\n" );
        printf( "ret = %d\n", ret);
    } while (input);
    return 0;
}

七 指向函数指针数组的指针

指向函数指针数组的指针,是一个 指针。它指向一个数组,数组的元素都是函数指针

c++ 复制代码
void test(const char* str)
{
    printf("%s\n", str);
}

int main()
{
    //函数指针 pfun
    void (*pfun)(const char*) = test;
    
    //函数指针的数组 pfunArr
    void (*pfunArr[5])(const char* str);
    pfunArr[0] = test;
    
    //指向函数指针数组 pfunArr 的指针 ppfunArr
    void (*(*ppfunArr)[5])(const char*) = &pfunArr;
    return 0;
}

//在函数指针的数组的类型最里面加上 (*) 即可表明是指向它的指针类型

八 回调函数

回调函数就是一个通过函数指针调用的函数。把函数指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,就称为回调

回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应

演示一下 qsort 函数的使用

c++ 复制代码
#include <stdio.h>

//qosrt 函数的使用者得实现一个比较函数
int int_cmp(const void * p1, const void * p2)
{
    return (*( int *)p1 - *(int *) p2);
}
int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    int i = 0;

    qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
        printf( "%d ", arr[i]);
    return 0;
}

使用回调函数,模拟实现 qsort(采用冒泡的方式)

c++ 复制代码
#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
    return (*( int *)p1 - *(int *) p2);
}

void _swap(void *p1, void * p2, int size)
{
    for (int i = 0; i< size; i++) //循环 swap 的内存空间大小
    {
        char tmp = *((char *)p1 + i);
        *(( char *)p1 + i) = *((char *) p2 + i);
        *(( char *)p2 + i) = tmp;
    }
}

void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
    for (int i = 0; i< count - 1; i++)
    {
        for (int j = 0; j< count - i - 1; j++)
        {
            if (cmp ((char *) base + j * size , (char *)base + (j + 1) * size) > 0) //转成 char * 指针, 进行指针偏移
            {
                _swap(( char *)base + j * size, (char *)base + (j + 1) * size, size);
            }
        }
    }
}

int main()
{
    int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
    //char *arr[] = {"aaaa","dddd","cccc","bbbb"};
    int i = 0;
    bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
    for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
        printf( "%d ", arr[i]);
    return 0;
}

九 指针和数组练习题

通常情况下数组名就是数组首元素的地址,但是有两个特殊情况

1、sizeof(数组名) ------ 表示整个数组,计算的是整个数组的大小,单位是字

2、& + 数组名 ------ 数组名表示整个数组,取出的是整个数组的地址

数组练习题

一维数组

c++ 复制代码
int a[] = {1,2,3,4};
printf("%d\n",sizeof(a));		
//数组有 4 个元素,每个元素的类型都是 int, int 类型占 4 个字节, 所以打印出来的值是 16

printf("%d\n",sizeof(a+0));
//(数组名 + 0) 并不是单独的数组名, 那么此时表示的就不是整个数组,(数组名 + 0) 表示的就是首元素的地址, 所以打印出来的值是 8 (64 位下)
    
printf("%d\n",sizeof(*a));		
//不符合两种特殊情况, 所以 a 是首元素地址, *a就是首元素. 因为元素类型是 int, 所以打印值是 4

printf("%d\n",sizeof(a+1));
//sizeof(a+1) 中的 a 是数组首元素地址, 因为 + 1 跳过 1 个整型. 所以a + 1 是第二个元素的地址, 所以打印值是 8 (64位下)

printf("%d\n",sizeof(a[1]));	
//a[1] 表示第二个元素, 所以它的大小就是 4 个字节

printf("%d\n",sizeof(&a));		
//sizeof(&a) 中 &a 是数组的地址, 数组的地址也是一个地址, 只要是地址它的大小就是 8 个字节 (64 位下)
//数组的地址和首元素的地址只在类型上存在差别, 数组的地址为 int(*)[4],首元素地址的类型为 int *, 类型的差异仅仅决定了 +- 操作跳过几个地址. 因为它们都是指针,所以 sizeof 的值都是一样的

printf("%d\n",sizeof(*&a));
//* 和 & 符号相互抵消, 该代码等价于 sizeof(a), 所以打印值是 16

printf("%d\n", sizeof(&a + 1));
//&a + 1 跳过了整个数组, 指向了数值最后一个元素的下一个元素. 因为指针只是指向了最后一个元素的下一个元素, 并没有解引用, 所以不存在指针的越界访问. &a + 1 表示的仍然是一个指针, 打印值是 8(64 位下)

printf("%d\n",sizeof(&a[0]));
//&a[0] 表示取出数组首元素地址, 所以打印值应该是 8(64位)

printf("%d\n",sizeof(&a[0]+1));
//&a[0] + 1 表示数组第二个元素地址, 所以值是 8(64位)

字符数组

c++ 复制代码
char arr[] = {'a','b','c','d','e','f'};
printf("%d\n", sizeof(arr));
//数组名单独放到 sizeof 内部, 计算整个数组的大小 单位字节, 所以打印值应该是 6
printf("%d\n", sizeof(arr+0));
//arr+0 不满足两种特殊情况, arr 表示首元素地址, 所以打印出来的值应该是 8 (64位)
printf("%d\n", sizeof(*arr));
//*arr 表示对数组首元素地址进行解引用, 所以表示的是首元素, 大小为 1 个字节
printf("%d\n", sizeof(arr[1]));
//arr[1] 是第二个元素, 大小是 1 个字节
printf("%d\n", sizeof(&arr));
//&arr 表示整个数组的地址, 数组的地址也是一个地址, 所以打印值是 8
printf("%d\n", sizeof(&arr+1));
//&arr + 1 表示跳过整个数组, 指向数组最后一个元素下一个元素的地址, 所以打印值是 8
printf("%d\n", sizeof(&arr[0]+1));
//&arr[0]+1 表示数组第二个元素地址, 所以打印值是 8

printf("%d\n", strlen(arr));
//arr 是首元素地址, strlen 在求长度时只有遇到 \0 才会结束. 原数组中没有 \0 , 所以打印值是一个随机值
printf("%d\n", strlen(arr+0));
//arr+0 表示数组首元素地址, 原数组中没有 \0 , 所以打印值是一个随机值
printf("%d\n", strlen(*arr));
//*arr 表示数组首元素 'a', 因 strlen 必须传入地址(入参是指针类型) , 字符 a ASCII 码值是 97. 此时 strlen 函数会把 97 当作一个地址, 那么在打印时会存在多种情况 : 1. 打印随机值 2. 访问到不允许访问的数据, 程序崩溃
printf("%d\n", strlen(arr[1]));
//arr[1] 表示第二个元素 'b', 其余同上
printf("%d\n", strlen(&arr));
//&arr 表示取出整个数组的地址, 数组的地址也是从数组的第一个元素的地址开始的, 此时打印值是一个随机值
printf("%d\n", strlen(&arr+1));
//&arr + 1 指向数组最后一个元素下一个元素的地址, 得到一个随机值, 但与第一行打印中的随机值存在差异 (随机值少 6, 因为跳过了 6 个元素)
printf("%d\n", strlen(&arr[0]+1));
//&arr[0] + 1 表示数组第二个元素地址, 从 b 开始向后统计的, 打印值是一个随机值 (比第一行打印的随机值小 1)


char arr[] = "abcdef";
printf("%d\n", sizeof(arr));
//arr 取出的是数组中所有的元素, 所以打印值是 7
printf("%d\n", sizeof(arr+0));
//arr + 0 中 arr 表示数组首元素, arr + 0 表示数组首元素地址, 所以打印值是 8
printf("%d\n", sizeof(*arr));
//*arr 表示数组首元素, 大小是一个字节
printf("%d\n", sizeof(arr[1]));
//arr[1] 表示第二个元素, 大小是一个字节
printf("%d\n", sizeof(&arr));
//&arr 表示取出的是数组的地址, 是地址就是 8 个字节
printf("%d\n", sizeof(&arr+1));
//&arr + 1 表示跳过了整个数组, 它还是一个地址, 是地址就是 8 个字节
printf("%d\n", sizeof(&arr[0]+1));
//&arr[0] + 1 表示的是第二个元素的地址, 大小是 8 个字节
printf("%d\n", strlen(arr));
//取出的是数组首元素地址, 所以打印值是 6
printf("%d\n", strlen(arr+0));
//取出的是数组首元素地址, 所以打印值是 6
printf("%d\n", strlen(*arr));
//取出的是首元素 'a', a 的 ASCII码值是 97,此时 strlen 会把 97 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(arr[1]));
//取出的是首元素 'b', b 的 ASCII码值是 98,此时 strlen 会把 98 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(&arr));
//取出来的是数组的地址, 数组的地址也是首元素的地址, 所以打印值是 6
printf("%d\n", strlen(&arr+1));
// +1 跳过了整个数组, 此时指向的是数组最后一个元素下一个元素的地址, 所以打印值是一个随机值
printf("%d\n", strlen(&arr[0]+1));
//第二个元素的地址, 所以打印值是 5

char *p = "abcdef";
printf("%d\n", sizeof(p));
//p 是一个指针变量, 是首元素地址. 计算的就是一个指针变量的大小, 所以打印值是 8
printf("%d\n", sizeof(p+1));
//是第二个元素的地址, 所以打印值是 8
printf("%d\n", sizeof(*p));
//p 类型是 char*, *p 是 char 类型, 所以打印值是 1
printf("%d\n", sizeof(p[0]));
//表示首元素 'a', 打印值是 1
printf("%d\n", sizeof(&p));
//表示一个二级指针变量, 所以打印值是 8
printf("%d\n", sizeof(&p+1));
//表示一个二级指针变量, &p + 1 表示跳过 p 指针变量后的地址, 所以打印值是 8
printf("%d\n", sizeof(&p[0]+1));
//表示数组第二个元素地址, 所以打印值是 8
printf("%d\n", strlen(p));
//取出的是首元素地址, 所以打印值是 6
printf("%d\n", strlen(p+1));
//取出的是第二个元素地址, 所以打印值是 5
printf("%d\n", strlen(*p));
//*p 取出的是第一个元素, a 的 ASCII 码值为 97, strlen 会把 97 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(p[0]));
//*p 取出的是第一个元素, a 的 ASCII 码值为 97, strlen 会把 97 当作一个地址, 所以打印值是一个随机值或报错
printf("%d\n", strlen(&p));
//表示一个二级指针变量, 所以打印值是一个随机值
printf("%d\n", strlen(&p+1));
//表示一个二级指针变量,所以打印值是一个随机值
printf("%d\n", strlen(&p[0]+1));
//表示第二个元素地址, 所以打印值是 5

二维数组

c++ 复制代码
int a[3][4] = {0};
printf("%d\n",sizeof(a));
//二维数组的数组名表示数组的大小, 打印值是 48
printf("%d\n",sizeof(a[0][0]));
//表示第一行的第一个元素, 所以打印值是 4
printf("%d\n",sizeof(a[0]));
//表示第一行所有元素, 所以打印出值是 16 (a[0] 是第一行的数组名, 数组名单独放到 sizeof 内部, 计算是第一行数组的总大小)
printf("%d\n",sizeof(a[0]+1));
//数组名没有单独放到 sizeof 内部, 所以数组名 a[0] 就是数组首元素 a[0][0] 的地址, +1 后是 a[0][1] 的地址, 所以打印值是 8
printf("%d\n",sizeof(*(a[0]+1)));
//表示第一行的第二个元素, 所以打印值是 4
printf("%d\n",sizeof(a+1));
//数组名没有单独放到 sizeof 的内部, a 表示数组首元素地址,是二维数组的首元素的地址, 也就是第一行的地址(&a[0]). +1 跳过一行就是第二行的地址, 是一个数组指针变量, 所以打印值是 8
printf("%d\n",sizeof(*(a+1)));
//表示 a[1] 第二行所有元素, 所以打印值是 16
printf("%d\n",sizeof(&a[0]+1));
//a[0] 是第一行数组名, &a[0] 取出的就是数组的地址, 第一行的地址. 所以+ 1 就是第二行的地址, 所以打印值是 8
printf("%d\n",sizeof(*(&a[0]+1)));
//表示 a[1] 第二行所有元素, 所以打印值是 16
printf("%d\n",sizeof(*a));
//a 作为数组名没有单独放到 sizeof 内部, a 表示数组首元素地址, 是二维数组首元素的地址, 也就是第一行的地址, *a 就是第一行所有元素, 所以打印值是 16
printf("%d\n",sizeof(a[3]));
//a[3] 表示第四行数组名, 因为 sizeof 并不会计算, 也没有访问. 所以不存在越界访问, 所以打印值是 16. a[3]无需真实存在, 仅仅是通过类型的推断算出的长度

总结: 数组名的意义

1、sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。

2、&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

3、除此之外所有的数组名都表示首元素的地址

指针练习题

以下程序的结果是什么

c++ 复制代码
int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int *ptr = (int *)(&a + 1); //&a 表示的是数组的地址
    printf( "%d,%d", *(a + 1), *(ptr - 1)); //2 5
    return 0;
}

*(a+1) 输出 2 ,a 数组名为数组首元素地址,再加一解引用就是数组第二位元素

*(ptr - 1) 输出5,&a 是取整个数组的地址,然后加一跳过了数组 a,ptr 指向数组元素 5 的后面,再强制转换成 int*(本来类型应该为 int(*)[5] ) ,然后 ptr - 1 解引用就指向数组元素 5

c++ 复制代码
struct Test
{
    int Num;	  //4
    char *pcName; //4
    short sDate;  //2
    char cha[2];  //2
    short sBa[4]; //8
}*p;
//假设 p 的值为 0x100000, 如下表表达式的值分别为多少 ?
//已知结构体 Test 类型的变量大小是 20 个字节
int main()
{
    printf("%p\n", p + 0x1);  
    // 20 -> 2 进制 1100 -> 16 进制 = 14
    //0x100014
    
    printf("%p\n", (unsigned long)p + 0x1);
    //0x100001
    printf("%p\n", (unsigned int*)p + 0x1);
    //0x100004
    return 0;
}

考察 指针 ± 整数,结构体指针 +1 会跳过一个结构体,所以 +1 就会跳过20 个字节。 p + 0x1 表示 0x100000 + 20 = 0x100014,因为打印的值是一个地址,所以要补满8位,所以打印出来的值应该是 00100014

(unsigned long) p + 0x1 中,p 被强制类型转换为 unsigned long 类型,此时 p 就不是一个指针变量了,所以此时整型值 +1 就是 +1 本身,所以打印出来的值应该是 00100001

(unsigned int*)p + 0x1 中,p 被强制类型转换为 unsigned int* 类型,所以+1 就会跳过 4 个字节,所以 p + 0x1 就表示 0x100000 + 4 = 0x100004,因为打印的值是一个地址要补满 8 位,所以打印值是 00100004

c++ 复制代码
int main()
{
    int a[4] = { 1, 2, 3, 4 };
    int *ptr1 = (int *)(&a + 1); //&a 整个数组的地址
    int *ptr2 = (int *)((int)a + 1); //数组首元素地址值转整型+1 
    printf( "%x,%x", ptr1[-1], *ptr2); //%x 用于格式化的输出符号, 以十六进制形式输出整数
    //4, 2000000(显示数据时也需要倒着读)
    return 0;
}

数组首元素地址值转整型 + 1 ,在小端机器上,以十六进制显示

01 00 00 00

02 00 00 00

*ptr2 的内容就是 00 00 00 02 这部分,显示数据时也需要倒着读,即为 2000000

c++ 复制代码
#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int *p;
    p = a[0]; //a[0] 是第一个行的数组名, 数组名表示首元素的地址, 即 a[0][0] 的地址
    printf( "%d", p[0]); //*(p+0) = *p
    return 0;
}

注意第一行代码中的二维数组并不是如下形式

int a[3][2] = { {0, 1}, {2, 3}, {4, 5} };

它是用括号连接起来的,表示的是一个逗号表达式,逗号表达式从左向右依次计算,最后一个计算的值就是表达式的取值,所以数组的真实情况应该如下:

int a[3][2] = { 1,3,5 };

因为数组是三行两列的,所以数据 1 3 放到第一行,数据 5 放到第二行第一列,其它的三个位置上放的都是0 。因为 a[0] 是第一行的数组名,数组名表示首元素的地址,其实就是 &a[0 ][0] 的地址, p[0] = *(p+0) = *p ,所以打印值是 1

c++ 复制代码
int main()
{
    int a[5][5]; //二维数组, 5 行 5 列
    int(*p)[4];
    p = a; //a 表示 &a[0] , 类型为 int(*)[5]
    printf( "%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]); // &p[4][2] = p + 4 后再找第三个元素
    return 0;
}

首先创建了一个 5 行 5 列的数组,再创建了一个数组指针变量,p 指向的是 4 个整型元素的地址

接着进行了 &p[4][2] - &a[4][2] , &p[4][2] - &a[4][2] 两个操作,指针-指针得到的是两个指针之间的元素的个数

分析一下:a 的类型是 int(*)[5],p 的类型是 int(*)[4]

当把 a 赋予 p 时,两者的首地址都是指向 a 数组中的第一行的第一个元素,两者会有类型的差异,所以 ± 整数二者跳过的字节数不同

a 每次 +1 跳过的是 5 个整型,而 p 每次 +1 跳过的是 4 个整型,我们画图分析如下

由图可知,两个指针相减得到的值是 -4 ,所以 %d 打印出来的值是 -4 ;

而 %p 是打印地址, -4 在内存中是以补码的形式存放的,

-4 的原码为:10000000000000000000000000000100

-4 的补码是:111111111111111111111111111111111100

所以 %p 此时就把 -4 的补码当作一个地址打印出来,把它的值换算成 16 进制,得到的是:FFFFFFFC

所以打印值是 FFFFFFFC 和 -4

c++ 复制代码
int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    int *ptr1 = (int *)(&aa + 1); //&aa + 1 整个数组
    int *ptr2 = (int *)(*(aa + 1)); //a[1][0] = 6
    printf( "%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0;
}

*(aa+1) 等价于 aa[1],aa[1] 是第二行的数组名,因为数组名表示的是首元素的地址,所以 aa[1] == &aa[1][0]

由图可知,打印值分别是 10 和 5

c++ 复制代码
#include <stdio.h>
int main()
{
 char *a[] = {"work","at","alibaba"};
 char**pa = a;
 pa++; //pa[1]
 printf("%s\n", *pa); 
 return 0;
}

a 是一个字符指针数组,数组里面一共有三个元素,数组的每个元素都是 char* 类型。因为二级指针变量 pa 被赋予了 a,a 是一个数组名,表示的就是首元素的地址, pa++ 即为 pa[1] 因此,打印结果是 at

c++ 复制代码
int main()
{
    char *c[] = {"ENTER","NEW","POINT","FIRST"};
    char**cp[] = {c+3,c+2,c+1,c};
    char***cpp = cp;
    printf("%s\n", **++cpp);
    printf("%s\n", *--*++cpp+3);
    printf("%s\n", *cpp[-2]+3);
    printf("%s\n", cpp[-1][-1]+1);
    return 0;
}

题目比较复杂,试着画图像来分析

因为指针优先级的问题 ++cpp 会率先进行,此时 cpp 的指向就会改变

此时*++cpp 拿到的值就是c+2,所以**++cpp 表示的值就是*(c+2),此时打印出来的值就是 POINT

第二个代码:* -- * ++ cpp + 3

这个代码中,+的优先级是最低的,所以应该先计算 ++cpp

因为上一个代码已经进行了 ++cpp 的操作指向了c+2,此时在进行 ++ 操作指向的应该是 c+1

再依照优先级顺序进行解引用操作,此时拿到的是c+1,原代码就可以转化为*--(c+1)+3

接下来应该要执行 -- 操作,因为 -- 的对象是 c+1,所以在执行完--操作以后,c+1的值会变成 c

现在的原代码相当于 *c+3,所以*c+3打印出来的值应该是 ER

第三个代码:* cpp [-2] + 3

先把代码转换一下:* *(cpp-2) + 3

应该先算(cpp-2),此时应该拿到的是 c+3,再对 (c+3) 解引用拿到的是 FIRST 处的地址,再进行 +3 操作,所以打印出来的值应该是 ST

第四个代码:cpp[-1][-1] + 1

再来把代码转换一下:*(*(cpp - 1) - 1) + 1

此时的逻辑和之前的代码一模一样,所以打印值是 EW

相关推荐
人需要PID几秒前
【C语言练习(5)—回文数判断】
c语言·开发语言·学习·算法
M-x_y2 小时前
排序算法--C语言
c语言·算法·排序算法·设计语言
五味香2 小时前
Java学习,字符串搜索
java·c语言·开发语言·python·学习·golang·kotlin
Jack电子实验室2 小时前
STM32 出租车计价器系统设计(一) 江科大源码改写
c语言·stm32·单片机·嵌入式硬件·嵌入式
Coding~2 小时前
逆向攻防世界CTF系列56-easy_Maze
c语言·汇编·算法·安全·网络安全
PluviophileDD3 小时前
【笔记】C语言转C++
c语言·c++
绿白尼3 小时前
Linux C所有预定义的宏
linux·c语言
狄加山6753 小时前
C语言(函数)
java·c语言·算法
紫阡星影4 小时前
【模块系列】STM32&PCF8563
c语言·stm32·单片机·嵌入式硬件